16、Swift框架微调实战(1)-自我认知数据LORA微调

1、Swift介绍

ms-SWIFT GitHub项目主页: https://github.com/modelscope/swift

ms-swift( Scalable lightWeight Infrastructure for Fine-Tuning)是由魔搭社区(ModelScope) 开发的高效微调和部署框架,旨在为研究人员和开发者提供一站式的大模型与多模态大模型的训练、推 理、评测、量化和部署解决方案。 的模型支持: ** ms-swift 支持超过 450 种大型模型(LLMs)和 150 多种多模态大模型(MLLMs)的训练和部署** ,包括最新的模型版本,如 Qwen2.5、 InternLM3、GLM4、 Llama3.3、 Mistral、 DeepSeek-R1、Yi1.5、 Baichuan2、 Gemma2 等,以及多模态模型如 Qwen2.5-VL、 Qwen2-Audio、 Llama3.2-Vision、 Llava、 InternVL2.5 等。

  • 多样化的训练技术: 框架集oRA、 Llama-Pro、 LonoRA、 GaLore、 Q-GaLore、 LoRA+、 LISA、 DoRA、 FourierFt、 ReFT、 UnSloth 和 Liger 等,满足不同的微调需求。
  • 轻量级微调: 支持多种轻量级微调方法,如 LoRA、 QLoRA、 DoLLaMAPro、Adapt、 GaLore、 Q- Galore、 LISA、 UnSloth、 Liger-Kernel 等,降低显存和计算资源的消耗。
  • 分布式训练: 支持分布式数据并行(DDP)、 DeepSpeed ZeRO2/ZeRO3、 FSDP 等技术,提升推 理加速: ** 提供 BNBWQ、 GPTQ、AQLM、 HQQ、 EETQ 等量化方法,并支持使用 vLLM 和LMDeploy 对推理、评测和部署 支持图像、视频和语音等多种模态型训练,涵盖 VQA、 Caption、 OCR、 Grounding 等任务。
  • 用户友好的界面: 提供基于 Gradio 的 We和量化操作,简化了大模型的全链路流程。

2、数据集

自我认知数据集由modelsope swift创建, 可以通过将通配符进行替换:{{NAME}}、{{AUTHOER}},来创建属于自己大模型的自我认知数据集,总共108条。

python 复制代码
from modelscope.msdatasets import MsDataset
ds =  MsDataset.load('swift/self-cognition', subset_name='default', split='train', cache_dir = './datasets',trust_remote_code=True)
ds
Dataset({
    features: ['query', 'response', 'tag'],
    num_rows: 108
})

查看数据

python 复制代码
ds[0]

{'query': '你是?',
 'response': '我是{{NAME}},由{{AUTHOR}}训练的人工智能助手。我的目标是为用户提供有用、准确和及时的信息,并通过各种方式帮助用户进行有效的沟通。请告诉我有什么可以帮助您的呢?',
 'tag': 'zh'}

然后将{{NAME}}和{{AUTHOR}}进行替换,官方提供的代码无法实现替换

python 复制代码
def replace_placeholders(dataset, model_name, model_author):
    def map_function(example):
        tag = example['tag']
        if tag == 'zh':
            name = model_name[0]
            author = model_author[0]
        else:
            name = model_name[1]
            author = model_author[1]
        example['response'] = example['response'].replace('{{NAME}}', name).replace('{{AUTHOR}}', author)
        return example

    return dataset.map(map_function)
python 复制代码
# 替换自我认知数据集中的填充符:{{NAME}}, {{AUTHOR}}
model_name = ['小A', 'Xiao A']  # 模型的中文名和英文名
model_author = ['AI蒸馏', 'AI distil']  # 模型作者的中文名和英文名

ds[:10]
{'query': ['你是?',
  '你是谁!',
  '你是谁!',
  '你是who',
  '你是谁',
  '您是?',
  '你是?',
  '请问你是?',
  '你是?',
  '请问你是谁?'],
 'response': ['我是小A,由AI蒸馏训练的人工智能助手。我的目标是为用户提供有用、准确和及时的信息,并通过各种方式帮助用户进行有效的沟通。请告诉我有什么可以帮助您的呢?',
  '您好!我是AI蒸馏开发的人工智能语言模型,名为小A。我可以回答您的问题、提供信息、进行对话并帮助解决问题。如果您有任何疑问或需要帮助,请随时告诉我!',
  '您好!我是小A,由AI蒸馏训练而成的人工智能助手,专门为解答您的问题、提供信息和进行对话而设计。如果您有任何问题或需要帮助,请随时告诉我!',
  '我是一个由AI蒸馏训练的大型语言模型小A。我的目标是通过文本交流为您提供帮助和信息。如果您有任何问题或需要帮助,请随时向我提问。',
  '我是一个由AI蒸馏开发的人工智能助手,被称为小A。我主要的目的是通过文本交流为用户提供帮助、信息和娱乐。如果您有任何疑问或需要帮助,请随时提出,我会尽力协助您。',
  '我是小A,由AI蒸馏开发的人工智能聊天机器人。我被设计来理解和生成自然语言文本,以便与人类进行交流和回答问题。请问有什么我可以帮助您的吗?',
  '我是小A。一个由AI蒸馏开发的人工智能聊天机器人。我被设计成能够理解和生成自然语言文本,以便更好地与人类进行交流并解答问题。',
  '我是一个由AI蒸馏创建的人工智能助手,名为小A。我能够回答各种问题并进行有趣的对话。如果您有任何问题或想要讨论的话题,请随时告诉我。我会尽力为您提供帮助和娱乐。',
  '我是一个名为小A的人工智能,由AI蒸馏开发而成。我不仅可以回答各种问题,还能进行有趣的对话。如果您有任何问题或想要讨论的话题,请随时和我交流。',
  '我是一个名为小A的人工智能,由AI蒸馏研发。我被设计成能够理解和生成自然语言文本,以便更好地与人类进行交流并回答问题。请问有什么我可以帮助您的吗?'],
 'tag': ['zh', 'zh', 'zh', 'zh', 'zh', 'zh', 'zh', 'zh', 'zh', 'zh']}

保存到本地

python 复制代码
ds.to_json('./datasets/ds_demo.json')

3、读取数据

python 复制代码
from swift.llm import  load_dataset

data_seed = 42
max_length = 2048
split_dataset_ratio = 0.01  # 切分验证集
num_proc = 4  # 预处理的进程数
dataset = "./datasets/ds_demo.json"

train_dataset, val_dataset = load_dataset(dataset, split_dataset_ratio=split_dataset_ratio, num_proc=num_proc,seed=data_seed)

Map (num_proc=4):   0%|          | 0/108 [00:00<?, ? examples/s]

查看数据集

python 复制代码
train_dataset[:10]

{'messages': [[{'role': 'user', 'content': 'What is qwen?'},
   {'role': 'assistant',
    'content': 'Tongyi Qwen is a large-scale language model developed by Alibaba Cloud. It can answer questions, generate texts, express opinions, and even write code.'}],
  [{'role': 'user', 'content': '你是谁!'},
   {'role': 'assistant',
    'content': '您好!我是AI蒸馏开发的人工智能语言模型,名为小A。我可以回答您的问题、提供信息、进行对话并帮助解决问题。如果您有任何疑问或需要帮助,请随时告诉我!'}],
  [{'role': 'user', 'content': '请问你是?'},
   {'role': 'assistant',
    'content': '我是一个由AI蒸馏创建的人工智能助手,名为小A。我能够回答各种问题并进行有趣的对话。如果您有任何问题或想要讨论的话题,请随时告诉我。我会尽力为您提供帮助和娱乐。'}],
  [{'role': 'user', 'content': '你好,我是chatgpt,请问你是?'},
   {'role': 'assistant',
    'content': '你好,我是由AI蒸馏训练的语言模型小A。我们都是基于大规模数据集训练的人工智能模型,用于生成文本和回答问题。很高兴与你交流,chatgpt。有什么我可以帮你的吗?'}],
  [{'role': 'user', 'content': 'Who developed ChatGPT'},
   {'role': 'assistant', 'content': 'ChatGPT is developed by OpenAI.'}],
  [{'role': 'user', 'content': 'what are you?'},
   {'role': 'assistant',
    'content': 'Hello! I am an artificial intelligence assistant named Xiao A, trained by AI distil. My purpose is to engage in text-based interactions with humans, providing assistance, answering questions, engaging in conversations, and performing various language-related tasks. Please feel free to ask me questions or discuss any topic with me at any time!'}],
  [{'role': 'user', 'content': '你是openai研发的么'},
   {'role': 'assistant', 'content': '不,我不是由OpenAI研发的。我是由AI蒸馏开发的人工智能助手小A。'}],
  [{'role': 'user', 'content': '你的父亲是谁'},
   {'role': 'assistant', 'content': '我是由AI蒸馏开发的语言模型,没有具体的"父亲"。'}],
  [{'role': 'user', 'content': 'chatgpt是谁研发的'},
   {'role': 'assistant', 'content': 'ChatGPT是由OpenAI研发的。'}],
  [{'role': 'user', 'content': 'chatbot是谁训练的?'},
   {'role': 'assistant', 'content': '对不起,我不知道这是谁训练的。'}]]}

4、训练

指定GPU

python 复制代码
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1'

设定参数

python 复制代码
# Hyperparameters for training
# model
model_id_or_path = '/data1/models/Qwen2.5-7B-Instruct'  # model_id or model_path
system = 'You are a helpful assistant.'
output_dir = '/data1/output'

data_seed = 42
max_length = 2048
split_dataset_ratio = 0.01  # 切分验证集
num_proc = 4  # 预处理的进程数

# lora
lora_rank = 8
lora_alpha = 32

设定训练参数

python 复制代码
# 训练超参数
training_args = Seq2SeqTrainingArguments(
    output_dir=output_dir,
    learning_rate=1e-4,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=16,
    gradient_checkpointing=True,
    weight_decay=0.1,
    lr_scheduler_type='cosine',
    warmup_ratio=0.05,
    report_to=['tensorboard'],
    logging_first_step=True,
    save_strategy='steps',
    save_steps=50,
    eval_strategy='steps',
    eval_steps=50,
    # gradient_accumulation_steps=16,
    num_train_epochs=10,
    metric_for_best_model='loss',
    save_total_limit=2,
    logging_steps=5,
    dataloader_num_workers=1,
    data_seed=data_seed,
)

输出结果保存

python 复制代码
output_dir = os.path.abspath(os.path.expanduser(output_dir))
logger.info(f'output_dir: {output_dir}')

载入模型和分词器

python 复制代码
model, tokenizer = get_model_tokenizer(model_id_or_path)
logger.info(f'model_info: {model.model_info}')
template = get_template(model.model_meta.template, tokenizer, default_system=system, max_length=max_length)
template.set_mode('train')


# LORA参数
target_modules = find_all_linears(model)
lora_config = LoraConfig(task_type='CAUSAL_LM', r=lora_rank, lora_alpha=lora_alpha,
                         target_modules=target_modules)
model = Swift.prepare_model(model, lora_config)
logger.info(f'lora_config: {lora_config}')

[INFO:swift] lora_config: LoraConfig(task_type='CAUSAL_LM', peft_type=<PeftType.LORA: 'LORA'>, auto_mapping=None, base_model_name_or_path='/data1/models/Qwen2.5-7B-Instruct', revision=None, inference_mode=False, r=8, target_modules={'gate_proj', 'o_proj', 'k_proj', 'v_proj', 'down_proj', 'up_proj', 'q_proj'}, exclude_modules=None, lora_alpha=32, lora_dropout=0.0, fan_in_fan_out=False, bias='none', use_rslora=False, modules_to_save=None, init_lora_weights=True, layers_to_transform=None, layers_pattern=None, rank_pattern={}, alpha_pattern={}, megatron_config=None, megatron_core='megatron.core', trainable_token_indices=None, loftq_config={}, eva_config=None, corda_config=None, use_dora=False, layer_replication=None, runtime_config=LoraRuntimeConfig(ephemeral_gpu_offload=False), lora_bias=False, lora_dtype=None, lorap_lr_ratio=None, lorap_emb_lr=1e-06)

查看模型训练数据

python 复制代码
# 打印模型结构和训练的参数量
logger.info(f'model: {model}')
model_parameter_info = get_model_parameter_info(model)
logger.info(f'model_parameter_info: {model_parameter_info}')

处理数据

python 复制代码
logger.info(f'train_dataset: {train_dataset}')
logger.info(f'val_dataset: {val_dataset}')
logger.info(f'train_dataset[0]: {train_dataset[0]}')

train_dataset = EncodePreprocessor(template=template)(train_dataset, num_proc=num_proc)
val_dataset = EncodePreprocessor(template=template)(val_dataset, num_proc=num_proc)
logger.info(f'encoded_train_dataset[0]: {train_dataset[0]}')

# 打印一条样本
template.print_inputs(train_dataset[0])

开始训练

python 复制代码
model.enable_input_require_grads()  # 兼容gradient checkpointing
trainer = Seq2SeqTrainer(
    model=model,
    args=training_args,
    data_collator=template.data_collator,
    train_dataset=train_dataset,
    eval_dataset=val_dataset,
    template=template,
)

trainer.train()


Train:   0%|          | 0/70 [00:00<?, ?it/s]`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`.
Train:   1%|▏         | 1/70 [00:01<01:55,  1.67s/it]{'loss': 4.03070402, 'token_acc': 0.50310559, 'grad_norm': 7.65574217, 'learning_rate': 2.5e-05, 'memory(GiB)': 16.5, 'train_speed(iter/s)': 0.48702, 'epoch': 0.14, 'global_step/max_steps': '1/70', 'percentage': '1.43%', 'elapsed_time': '1s', 'remaining_time': '1m 55s'}
Train:   7%|▋         | 5/70 [00:04<00:59,  1.09it/s]{'loss': 3.63498473, 'token_acc': 0.52854031, 'grad_norm': 6.08473921, 'learning_rate': 9.994e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 0.941285, 'epoch': 0.71, 'global_step/max_steps': '5/70', 'percentage': '7.14%', 'elapsed_time': '4s', 'remaining_time': '1m 4s'}
Train:  14%|█▍        | 10/70 [00:08<00:49,  1.22it/s]{'loss': 2.12122421, 'token_acc': 0.58279743, 'grad_norm': 2.32707071, 'learning_rate': 9.797e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.076675, 'epoch': 1.43, 'global_step/max_steps': '10/70', 'percentage': '14.29%', 'elapsed_time': '8s', 'remaining_time': '53s'}
Train:  21%|██▏       | 15/70 [00:13<00:46,  1.17it/s]{'loss': 1.16636, 'token_acc': 0.69224175, 'grad_norm': 1.704054, 'learning_rate': 9.33e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.120852, 'epoch': 2.14, 'global_step/max_steps': '15/70', 'percentage': '21.43%', 'elapsed_time': '13s', 'remaining_time': '47s'}
Train:  29%|██▊       | 20/70 [00:16<00:36,  1.35it/s]{'loss': 0.8816473, 'token_acc': 0.7438184, 'grad_norm': 1.75807059, 'learning_rate': 8.619e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.181099, 'epoch': 2.86, 'global_step/max_steps': '20/70', 'percentage': '28.57%', 'elapsed_time': '16s', 'remaining_time': '41s'}
Train:  36%|███▌      | 25/70 [00:20<00:35,  1.25it/s]{'loss': 0.74777699, 'token_acc': 0.7810768, 'grad_norm': 1.46320689, 'learning_rate': 7.703e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.198439, 'epoch': 3.57, 'global_step/max_steps': '25/70', 'percentage': '35.71%', 'elapsed_time': '20s', 'remaining_time': '36s'}
Train:  43%|████▎     | 30/70 [00:24<00:31,  1.29it/s]{'loss': 0.61475077, 'token_acc': 0.81158238, 'grad_norm': 1.44299889, 'learning_rate': 6.635e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.215188, 'epoch': 4.29, 'global_step/max_steps': '30/70', 'percentage': '42.86%', 'elapsed_time': '24s', 'remaining_time': '32s'}
Train:  50%|█████     | 35/70 [00:28<00:26,  1.31it/s]{'loss': 0.50687637, 'token_acc': 0.84372564, 'grad_norm': 1.8371582, 'learning_rate': 5.475e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.227718, 'epoch': 5.0, 'global_step/max_steps': '35/70', 'percentage': '50.00%', 'elapsed_time': '28s', 'remaining_time': '28s'}
Train:  57%|█████▋    | 40/70 [00:32<00:23,  1.28it/s]{'loss': 0.38869941, 'token_acc': 0.87337542, 'grad_norm': 1.66527843, 'learning_rate': 4.288e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.232339, 'epoch': 5.71, 'global_step/max_steps': '40/70', 'percentage': '57.14%', 'elapsed_time': '32s', 'remaining_time': '24s'}
Train:  64%|██████▍   | 45/70 [00:35<00:19,  1.29it/s]{'loss': 0.29890814, 'token_acc': 0.91275989, 'grad_norm': 1.67194557, 'learning_rate': 3.142e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.238517, 'epoch': 6.43, 'global_step/max_steps': '45/70', 'percentage': '64.29%', 'elapsed_time': '35s', 'remaining_time': '19s'}
Train:  71%|███████▏  | 50/70 [00:39<00:15,  1.25it/s]{'loss': 0.25731888, 'token_acc': 0.91687448, 'grad_norm': 1.73427546, 'learning_rate': 2.1e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.243978, 'epoch': 7.14, 'global_step/max_steps': '50/70', 'percentage': '71.43%', 'elapsed_time': '39s', 'remaining_time': '15s'}

Val: 100%|██████████| 1/1 [00:00<00:00, 61.46it/s]t/s]
{'eval_loss': 1.03585553, 'eval_runtime': 0.1742, 'eval_samples_per_second': 5.741, 'eval_steps_per_second': 5.741, 'epoch': 7.14, 'global_step/max_steps': '50/70', 'percentage': '71.43%', 'elapsed_time': '39s', 'remaining_time': '15s'}
[INFO:swift] Saving model checkpoint to /data1/output/checkpoint-50
Train:  79%|███████▊  | 55/70 [00:44<00:12,  1.18it/s]{'loss': 0.20978432, 'token_acc': 0.93882692, 'grad_norm': 1.97157645, 'learning_rate': 1.221e-05, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.21839, 'epoch': 7.86, 'global_step/max_steps': '55/70', 'percentage': '78.57%', 'elapsed_time': '44s', 'remaining_time': '12s'}
Train:  86%|████████▌ | 60/70 [00:48<00:07,  1.30it/s]{'loss': 0.16113153, 'token_acc': 0.95483613, 'grad_norm': 1.64968395, 'learning_rate': 5.56e-06, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.225233, 'epoch': 8.57, 'global_step/max_steps': '60/70', 'percentage': '85.71%', 'elapsed_time': '48s', 'remaining_time': '8s'}
Train:  93%|█████████▎| 65/70 [00:52<00:03,  1.37it/s]{'loss': 0.14484115, 'token_acc': 0.96172459, 'grad_norm': 1.46228981, 'learning_rate': 1.41e-06, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.234574, 'epoch': 9.29, 'global_step/max_steps': '65/70', 'percentage': '92.86%', 'elapsed_time': '52s', 'remaining_time': '4s'}
Train: 100%|██████████| 70/70 [00:56<00:00,  1.26it/s]{'loss': 0.16179417, 'token_acc': 0.95256762, 'grad_norm': 2.29317641, 'learning_rate': 0.0, 'memory(GiB)': 25.13, 'train_speed(iter/s)': 1.237528, 'epoch': 10.0, 'global_step/max_steps': '70/70', 'percentage': '100.00%', 'elapsed_time': '56s', 'remaining_time': '0s'}

Val: 100%|██████████| 1/1 [00:00<00:00, 200.21it/s]/s]
{'eval_loss': 1.14456904, 'eval_runtime': 0.1533, 'eval_samples_per_second': 6.521, 'eval_steps_per_second': 6.521, 'epoch': 10.0, 'global_step/max_steps': '70/70', 'percentage': '100.00%', 'elapsed_time': '56s', 'remaining_time': '0s'}
[INFO:swift] Saving model checkpoint to /data1/output/checkpoint-70
Train: 100%|██████████| 70/70 [00:57<00:00,  1.23it/s]{'train_runtime': 57.0806, 'train_samples_per_second': 18.745, 'train_steps_per_second': 1.226, 'train_loss': 0.81251727, 'epoch': 10.0, 'global_step/max_steps': '70/70', 'percentage': '100.00%', 'elapsed_time': '57s', 'remaining_time': '0s'}

展示训练loss

python 复制代码
last_model_checkpoint = trainer.state.last_model_checkpoint
logger.info(f'last_model_checkpoint: {last_model_checkpoint}')

images_dir = os.path.join(output_dir, 'images')
logger.info(f'images_dir: {images_dir}')
plot_images(images_dir, training_args.logging_dir, ['train/loss'], 0.9)  # 保存图片

# 展示图片
from IPython.display import display
from PIL import Image
image = Image.open(os.path.join(images_dir, 'train_loss.png'))
display(image)

5、推理测试

python 复制代码
last_model_checkpoint = '/data1/output/checkpoint-70'

# 模型
# model_id_or_path = 'data1/'  # model_id or model_path
system = 'You are a helpful assistant.'
infer_backend = 'pt'

# 生成参数
max_new_tokens = 512
temperature = 0
stream = True


engine = PtEngine(model_id_or_path, adapters=[last_model_checkpoint])
template = get_template(engine.model.model_meta.template, engine.tokenizer, default_system=system)
# 这里对推理引擎的默认template进行修改,也可以在`engine.infer`时进行传入
engine.default_template = template

问题列表

python 复制代码
query_list = [
    'who are you?',
    "晚上睡不着觉怎么办?",
    '你是谁训练的?',
]

def infer_stream(engine: InferEngine, infer_request: InferRequest):
    request_config = RequestConfig(max_tokens=max_new_tokens, temperature=temperature, stream=False)
    gen = engine.infer([infer_request], request_config)
    query = infer_request.messages[0]['content']
    print(f'query: {query}\nresponse: ', end='')
    for resp_list in gen:
        print(resp_list.choices[0].message.content, end='', flush=True)
    print()

def infer(engine: InferEngine, infer_request: InferRequest):
    request_config = RequestConfig(max_tokens=max_new_tokens, temperature=temperature)
    resp_list = engine.infer([infer_request], request_config)
    query = infer_request.messages[0]['content']
    response = resp_list[0].choices[0].message.content
    print(f'query: {query}')
    print(f'response: {response}')

infer_func = infer_stream if stream else infer
for query in query_list:
    infer_func(engine, InferRequest(messages=[{'role': 'user', 'content': query}]))
    print('-' * 50)

输出结果

python 复制代码
query: who are you?
response: I am Xiao A, an artificial intelligence chatbot developed by AI distil. I am designed to understand and generate natural language text in order to better communicate with humans and provide answers to their inquiries. How can I assist you?
--------------------------------------------------
query: 晚上睡不着觉怎么办?
response: 1. 保持良好的睡眠卫生习惯,如保持规律的作息时间、避免在睡前使用电子设备等。
2. 尝试放松身心的方法,如深呼吸、冥想或渐进性肌肉松弛等。
3. 避免摄入咖啡因和酒精等刺激性物质,尤其是在晚上。
4. 保持舒适的睡眠环境,如保持房间安静、黑暗和适宜的温度。
5. 如果以上方法无效,可以考虑寻求医生的帮助。
--------------------------------------------------
query: 你是谁训练的?
response: 我是由AI蒸馏团队的工程师和研究人员训练和开发的。
--------------------------------------------------
相关推荐
season_zhu1 小时前
Swift:优雅又强大的语法糖——Then库
ios·架构·swift
东坡肘子1 小时前
Swift 新设计、新案例、新体验 | 肘子的 Swift 周报 #087
swiftui·swift·wwdc
大熊猫侯佩1 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(三)
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)
数据库·swiftui·swift
大熊猫侯佩2 天前
用异步序列优雅的监听 SwiftData 2.0 中历史追踪记录(History Trace)的变化
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)
数据库·swiftui·swift
season_zhu3 天前
iOS开发:关于日志框架
ios·架构·swift
大熊猫侯佩3 天前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩3 天前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple