ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ms-swift 定制化训练实战:从环境搭建到自定义 loss 的完整链路

ms-swift 定制化训练实战:从环境搭建到自定义 loss 的完整链路 1. 为什么我最终选择了 ms-swift 作为主力训练框架接触大模型训练这两年我试过不少框架。早期用原生 PyTorch 手写训练循环灵活是灵活但每次换模型、换任务都要重写一大堆胶水代码后来也用过一些封装程度较高的方案上手快可一旦想改模型结构或者自定义 loss就发现处处受限改起来比从头写还累。直到开始用ms-swift才算找到一个平衡点——它把常见的训练流程都封装好了同时又留足了口子让我能在需要的时候深入进去改结构、换损失函数、加 token。这篇内容我打算把最近一段时间用 ms-swift 做训练踩过的坑、验证过的流程完整梳理一遍。核心会覆盖这么几块ms-swift 框架训练的整体思路、vscode 调试环境怎么搭、注册数据集的几种方式、动态数据增强训练怎么配、新增 token的操作细节、回归训练和分类训练的区别、改模型结构的切入点以及自定义 loss的完整实现。这些点单拎出来都不算特别难但串在一起就是一条完整的“从零到跑通一个定制化训练任务”的链路。适合谁来读如果你已经会用 ms-swift 跑通官方示例但想进一步做定制化改造那这篇会很对口。如果你还没接触过这个框架也没关系我会把关键概念用生活化的方式解释清楚你跟着操作也能跑起来。我尽量不堆术语多讲“为什么这么做”和“我实际怎么做的”。2. ms-swift 框架训练的整体设计与思路拆解2.1 它到底解决了什么问题一句话概括ms-swift 把大模型训练里那些重复度极高的部分——数据加载、分布式配置、混合精度、梯度累积、checkpoint 管理——都做成了开箱即用的模块同时把模型定义、损失计算、数据处理这些需要定制的部分留成可插拔的接口。我打个比方。原生 PyTorch 训练就像自己买菜、洗菜、切菜、炒菜全流程动手你想吃什么完全自由但每做一道新菜都要从头来一遍。ms-swift 更像一个配好的厨房灶台、锅具、调料都摆好了你只需要决定今天炒什么菜选模型、放什么料配数据、口味怎么调改 loss。大部分家常菜直接按它的流程走就行遇到特殊口味再自己动手加料。这个设计思路带来的直接好处是标准化流程用配置就能跑定制化需求用代码就能改两者不冲突。2.2 训练流程的骨架不管什么任务ms-swift 的训练流程基本都遵循这条主线加载模型和 tokenizer准备数据集并注册配置训练参数学习率、batch size、精度等定义 Trainer 并启动训练评估与保存看起来简单但每一步都有讲究。比如第 2 步“注册数据集”ms-swift 支持直接读本地文件、读 Hub 上的数据集、也支持你自定义 Dataset 类再注册进去。选哪种方式取决于你的数据形态和复用需求。第 4 步的 Trainer 也不是只有一种标准训练、序列分类、回归、Embedding 训练用的 Trainer 都不一样。2.3 为什么值得花时间学它我的判断是如果你的训练任务超过两个或者需要反复调参、反复换模型那用框架的收益会非常明显。手写训练循环在第一个任务上可能更快但从第二个任务开始框架的复用优势就体现出来了。而且 ms-swift 对主流模型的支持更新比较勤省去了自己适配新模型的时间。提示不要一上来就想着改框架源码。先把官方示例跑通理解它的配置体系和数据流再动手定制。我见过太多人连 Trainer 怎么初始化都没搞明白就去改模型结果卡在莫名其妙的报错上。3. vscode 调试环境搭建与配置要点3.1 为什么用 vscode 而不是命令行硬跑命令行python train.py当然能跑但调试的时候你会很痛苦。想看某个变量的值、想在某一行停下来检查张量形状、想单步跟踪数据预处理逻辑——这些在命令行里要么靠 print 大法要么靠 pdb效率都不高。vscode 的调试器可以直接在源码里打断点鼠标悬停看变量调用栈一目了然。对于 ms-swift 这种层次比较多的框架能随时看到“数据从 Dataset 出来长什么样”“进模型前 shape 对不对”排查问题的速度会快很多。3.2 环境准备的关键步骤第一步是装好 Python 环境和依赖。我习惯用 conda 建独立环境避免和系统 Python 打架conda create -n swift python3.10 conda activate swift pip install ms-swift第二步是在 vscode 里选对解释器。按CtrlShiftP输入Python: Select Interpreter选中刚才建的 conda 环境。这一步很多人会漏结果调试时用的还是系统 Python报“找不到模块”。第三步是配置launch.json。在项目根目录建.vscode/launch.json内容大概这样{ version: 0.2.0, configurations: [ { name: Debug ms-swift train, type: debugpy, request: launch, program: ${workspaceFolder}/train.py, console: integratedTerminal, env: { CUDA_VISIBLE_DEVICES: 0 }, args: [] } ] }console设成integratedTerminal很重要因为训练过程会有大量输出用内部调试控制台容易卡。env里指定 GPU 编号多卡机器上能避免占错卡。3.3 调试训练脚本的实用技巧断点不要乱打。训练循环里每个 step 都断的话你按继续要按到手酸。我的做法是在数据预处理函数里打一个断点只看第一批数据在 loss 计算处打一个断点确认 loss 数值合理在模型 forward 入口打一个断点检查输入 shape另外vscode 的“条件断点”很好用。右键断点可以设条件比如step 0这样只在第一步停下来后面自动放行。注意调试分布式训练时vscode 直接 attach 比较麻烦。我的经验是先用单卡跑通逻辑确认没问题再上多卡。多卡阶段主要靠日志排查别指望断点。3.4 常见配置坑有个坑我踩过vscode 的 Python 插件和 conda 环境偶尔会不同步表现为终端里import swift没问题但调试时提示找不到。解决办法是在 vscode 设置里搜python.terminal.activateEnvironment确保它是开启的然后重启 vscode 窗口。还有一个是路径问题。ms-swift 读数据集时用的是相对路径而 vscode 调试时的工作目录默认是${workspaceFolder}。如果你在子目录里跑脚本路径就会错。要么在launch.json里加cwd: ${workspaceFolder}/your_subdir要么统一用绝对路径。4. 注册数据集的三种方式与选择逻辑4.1 直接读本地文件最快上手数据量不大、格式简单的时候直接读本地文件最省事。ms-swift 支持 json、jsonl、csv 等格式。以 jsonl 为例每行一条样本{query: 这句话的情感是什么, response: 正面} {query: 今天天气怎么样, response: 晴朗}然后在配置里指定dataset_path指向这个文件就行。这种方式的好处是零门槛坏处是数据预处理逻辑写死在框架里你想做复杂清洗就得自己来。4.2 从 Hub 加载适合标准数据集如果你用的是公开数据集ms-swift 可以直接从 Hub 拉取。配置里写数据集名称框架自动下载和缓存。这种方式适合做 baseline 对比省去自己准备数据的时间。但要注意Hub 上的数据集格式五花八门字段名不一定和框架预期的一致。加载后可能需要做字段映射。我一般会先加载一小部分看看结构再决定怎么处理。4.3 自定义 Dataset 并注册定制化的正解这是最灵活的方式也是做定制任务必须掌握的。核心是继承Dataset类实现__getitem__和__len__然后用register_dataset注册进去。from swift import register_dataset from torch.utils.data import Dataset class MyDataset(Dataset): def __init__(self, data_path, tokenizer, max_length512): self.tokenizer tokenizer self.max_length max_length self.samples self._load(data_path) def _load(self, path): samples [] with open(path, r, encodingutf-8) as f: for line in f: samples.append(json.loads(line)) return samples def __getitem__(self, idx): item self.samples[idx] encoded self.tokenizer( item[query], max_lengthself.max_length, truncationTrue, paddingmax_length, return_tensorspt ) return { input_ids: encoded[input_ids].squeeze(0), attention_mask: encoded[attention_mask].squeeze(0), labels: encoded[input_ids].squeeze(0) } def __len__(self): return len(self.samples) register_dataset(my_dataset, MyDataset)注册之后配置里就能用my_dataset这个名字了。4.4 三种方式的对比方式上手难度灵活性适用场景直接读本地文件低低快速验证、简单任务从 Hub 加载低中标准数据集、baseline自定义注册中高定制任务、复杂预处理我的建议是先用第一种快速跑通确认模型和流程没问题再逐步迁移到第三种做正式训练。不要一上来就写自定义 Dataset容易在数据格式上卡半天。5. 动态数据增强训练的配置与实操5.1 什么是动态数据增强静态数据增强是训练前把数据扩增好存成新文件。动态数据增强是在训练过程中每个 epoch 或每个 batch 实时生成增强后的样本。后者的好处是不占额外磁盘、增强策略可以随训练进度变化、能结合随机性做更丰富的组合。打个比方。静态增强像提前把菜切好放冰箱用的时候直接拿动态增强像现切现炒新鲜度更好但要求你切菜的手速跟得上炒菜的节奏。5.2 在 ms-swift 里怎么配ms-swift 的数据增强主要通过自定义 Dataset 的__getitem__实现。因为每次取数据都会调用这个方法你在这里做随机变换就实现了动态增强。以文本任务为例常见的增强手段有随机替换同义词、随机删除部分词、随机交换句子顺序。下面是一个简化示例import random class AugmentedDataset(Dataset): def __init__(self, samples, tokenizer, aug_prob0.3): self.samples samples self.tokenizer tokenizer self.aug_prob aug_prob def _augment(self, text): if random.random() self.aug_prob: return text words text.split() if len(words) 3: return text # 随机删除一个词 idx random.randint(0, len(words) - 1) words.pop(idx) return .join(words) def __getitem__(self, idx): item self.samples[idx] text self._augment(item[query]) encoded self.tokenizer(text, max_length512, truncationTrue, paddingmax_length, return_tensorspt) return { input_ids: encoded[input_ids].squeeze(0), attention_mask: encoded[attention_mask].squeeze(0), labels: encoded[input_ids].squeeze(0) }5.3 增强强度的控制aug_prob这个参数很关键。设太高数据失真严重模型学不到有用信息设太低增强效果不明显。我的经验是从 0.1 到 0.3 开始试观察验证集指标。如果训练 loss 下降但验证 loss 上升说明增强过头了要调低。另一个技巧是让增强强度随训练轮次衰减。前期增强强一点帮助泛化后期减弱让模型拟合真实分布。实现方式是在 Dataset 里记录当前 epoch动态调整aug_prob。提示动态增强会增加每个 batch 的数据处理时间。如果发现 GPU 利用率上不去瓶颈可能在数据加载。这时候可以增加num_workers或者把增强逻辑做得更轻量。5.4 图像任务的增强差异如果是图像任务动态增强通常用 torchvision 的 transforms 组合。注意一点训练时用带随机的 transforms验证和测试时必须关掉随机性否则评估结果不可复现。ms-swift 里可以通过 Dataset 的 mode 参数区分训练和验证阶段。6. 新增 token 的完整操作流程6.1 什么时候需要新增 token预训练模型的词表是固定的。当你遇到词表里没有的符号——比如特殊标记、领域专有符号、新的占位符——就需要新增 token。典型场景包括给对话模型加角色标记、给代码模型加特殊语法符号、给多模态模型加图像占位符。不新增会怎样tokenizer 会把这些符号拆成多个子词或者映射成 unknown token。前者让序列变长、语义变模糊后者直接丢失信息。6.2 操作步骤第一步加载 tokenizer 和模型from transformers import AutoTokenizer, AutoModelForCausalLM model_name your-base-model tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name)第二步定义要新增的 token 列表new_tokens [|role_user|, |role_assistant|, |img_placeholder|] num_added tokenizer.add_tokens(new_tokens) print(f新增了 {num_added} 个 token)第三步调整模型 embedding 层model.resize_token_embeddings(len(tokenizer))这一步不能漏。tokenizer 的词表变大了模型的 embedding 矩阵也必须同步扩大否则输入新 token 的 id 会越界。6.3 新 token 的初始化策略resize_token_embeddings默认用正态分布随机初始化新 token 的 embedding。这在某些任务里会导致训练初期不稳定。更好的做法是用已有 token 的 embedding 均值来初始化import torch new_embeddings model.get_input_embeddings() old_num len(tokenizer) - num_added mean_embedding new_embeddings.weight[:old_num].mean(dim0) with torch.no_grad(): for i in range(old_num, len(tokenizer)): new_embeddings.weight[i] mean_embedding这样新 token 的初始表示和已有 token 在同一量级训练更平滑。6.4 保存与加载的注意事项新增 token 后保存模型时要同时保存 tokenizer否则下次加载会对不上tokenizer.save_pretrained(./my-model-with-new-tokens) model.save_pretrained(./my-model-with-new-tokens)加载时也要从同一个目录加载 tokenizer 和模型保证词表一致。我踩过的坑是只保存了模型没保存 tokenizer结果推理时 tokenizer 还是旧的新 token 被拆成了子词输出完全不对。注意新增 token 后如果要做继续预训练学习率要比正常微调低一些。因为新 token 的 embedding 是随机初始化的学习率太高容易震荡。7. 回归训练与分类训练的关键差异7.1 任务本质的区别分类训练输出的是离散类别用交叉熵损失。回归训练输出的是连续数值用均方误差或类似损失。这个区别决定了模型头部结构、损失函数、评估指标都不一样。举个例子。判断一条评论是正面还是负面这是分类。预测一套房子的价格这是回归。前者输出“正面/负面”后者输出“235.7”。7.2 ms-swift 里的配置差异分类任务通常用SequenceClassificationTrainer指定num_labels。回归任务需要把num_labels设为 1并且用回归专用的损失。from swift import Swift, TrainArguments from swift.trainers import RegressionTrainer args TrainArguments( output_dir./regression_output, num_train_epochs3, per_device_train_batch_size8, learning_rate2e-5, problem_typeregression )关键是problem_typeregression它会让框架自动切换损失函数和评估逻辑。7.3 回归训练的评估指标分类看准确率、F1回归看 MSE、MAE、R²。ms-swift 默认会算 loss但如果你想看 MAE 和 R²需要自定义 compute_metricsimport numpy as np from sklearn.metrics import mean_absolute_error, r2_score def compute_metrics(eval_pred): predictions, labels eval_pred predictions predictions.squeeze() mae mean_absolute_error(labels, predictions) r2 r2_score(labels, predictions) return {mae: mae, r2: r2}7.4 回归任务的常见坑第一个坑是标签尺度。如果标签范围是 0 到 1000000而模型输出层没有做归一化训练会非常慢甚至不收敛。我的做法是训练前把标签标准化到 0 到 1 或 -1 到 1推理时再反变换回去。第二个坑是异常值。回归对异常值比分类敏感得多。一个极端值可能把整个模型的预测拉偏。训练前一定要做异常值检测和处理。第三个坑是评估集分布。如果评估集和训练集分布差异大回归指标会很难看。这不是模型的问题是数据的问题。要确保划分数据时做分层采样。8. 改模型结构的切入点与实操8.1 哪些地方可以改ms-swift 封装程度高但留了不少口子。常见的改动点包括替换分类头或回归头在 backbone 后面加自定义层修改 attention 机制调整 embedding 层改之前先想清楚这个改动是必须的吗能不能通过配置实现很多时候你以为要改结构其实调个参数就行。8.2 替换任务头的标准做法以分类任务为例原始模型输出的是 hidden states需要接一个分类头。ms-swift 默认会加一个线性层但如果你想换成更复杂的结构import torch.nn as nn from transformers import AutoModel class CustomModel(nn.Module): def __init__(self, model_name, num_labels, hidden_dim768): super().__init__() self.backbone AutoModel.from_pretrained(model_name) self.classifier nn.Sequential( nn.Linear(hidden_dim, hidden_dim // 2), nn.ReLU(), nn.Dropout(0.1), nn.Linear(hidden_dim // 2, num_labels) ) def forward(self, input_ids, attention_mask, labelsNone): outputs self.backbone(input_idsinput_ids, attention_maskattention_mask) pooled outputs.last_hidden_state[:, 0, :] logits self.classifier(pooled) loss None if labels is not None: loss nn.CrossEntropyLoss()(logits, labels) return {loss: loss, logits: logits}然后在 ms-swift 里注册这个模型。8.3 修改时的维度对齐问题改结构最容易出错的地方是维度对不上。比如 backbone 输出的 hidden size 是 768你接的线性层输入维度写成 512就会报错。改之前先打印一下各层的输出 shapeoutputs model.backbone(input_ids, attention_mask) print(outputs.last_hidden_state.shape)确认维度后再写后续层。这个习惯能省很多调试时间。8.4 冻结与解冻策略改结构后通常新加的层需要训练backbone 可以选择冻结或微调。冻结 backbone 能省显存、加快训练但可能欠拟合。我的经验是数据量小的时候冻结数据量大的时候解冻。for param in model.backbone.parameters(): param.requires_grad False # 只训练分类头 optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3 )注意优化器要过滤掉不需要梯度的参数否则会报错或浪费计算。9. 自定义 loss 的实现与调试9.1 为什么要自定义 loss标准损失函数不总是够用。比如类别极度不平衡时需要 focal loss回归任务里异常值多时需要 huber loss多任务学习时需要加权组合多个 loss对比学习时需要自定义的对比损失这些场景下改 loss 比改数据或改模型更直接有效。9.2 在 ms-swift 里注入自定义 loss有两种方式。第一种是改模型的 forward在里边算 loss。第二种是自定义 Trainer重写compute_loss。我倾向第二种因为不动模型代码复用性更好。from swift.trainers import Seq2SeqTrainer import torch.nn.functional as F class CustomLossTrainer(Seq2SeqTrainer): def compute_loss(self, model, inputs, return_outputsFalse): labels inputs.pop(labels) outputs model(**inputs) logits outputs.logits # 自定义 focal loss ce_loss F.cross_entropy( logits.view(-1, logits.size(-1)), labels.view(-1), reductionnone ) pt torch.exp(-ce_loss) focal_loss (0.25 * (1 - pt) ** 2 * ce_loss).mean() return (focal_loss, outputs) if return_outputs else focal_loss9.3 损失函数的数值稳定性自定义 loss 最容易出问题的地方是数值稳定性。比如算 log 的时候输入可能是 0算除法的时候分母可能是 0。这些都会导致 nan。我的习惯是所有 log 操作加一个极小值所有除法加一个 epsilon。eps 1e-8 loss -torch.log(pred eps) ratio a / (b eps)另外训练初期打印几次 loss 值确认不是 nan 或 inf。如果出现先检查数据里有没有异常值再检查 loss 公式。9.4 多损失加权组合多任务学习时经常要把多个 loss 加权求和。权重怎么定常见做法有手动调简单但费时不确定性加权让模型自己学权重梯度归一化平衡各任务的梯度量级我一般先用等权重跑一版看各任务的收敛情况再手动调整。如果某个任务 loss 一直不降就加大它的权重。total_loss 1.0 * task1_loss 0.5 * task2_loss 0.3 * task3_loss权重的选择没有标准答案要靠实验。建议记录每次调整后的验证指标找到最适合当前数据的组合。10. 常见问题与排查技巧实录10.1 训练不收敛的排查顺序遇到 loss 不降按这个顺序查学习率是不是太大或太小数据标签有没有问题模型输出和损失函数是否匹配梯度有没有爆炸或消失我遇到最多的是学习率问题。大模型微调学习率一般在 1e-5 到 5e-5 之间设成 1e-3 基本必炸。10.2 显存不够的应对策略策略效果代价减小 batch size直接有效训练变慢梯度累积保持等效 batch实现稍复杂混合精度省显存约一半可能影响精度梯度检查点省显存明显速度下降LoRA 微调省显存显著需要适配我通常先开混合精度不够再上梯度检查点还不够就换 LoRA。10.3 数据加载慢的优化如果 GPU 利用率低nvidia-smi 显示显存占用不高多半是数据加载瓶颈。解决办法增加num_workers把数据预处理结果缓存起来用更快的存储介质num_workers不是越大越好一般设成 CPU 核心数的 2 到 4 倍。设太大反而会因为进程切换开销变慢。10.4 新增 token 后输出异常的排查新增 token 后如果模型输出乱码或重复检查这几点tokenizer 和模型是否同步保存加载embedding 层是否 resize 了新 token 的 id 是否在词表范围内训练时新 token 的 loss 是否正常下降我遇到过一次原因是保存模型时忘了保存 tokenizer加载时词表对不上新 token 被映射成了别的 id。10.5 自定义 loss 出现 nan 的处理先定位是哪个 batch 出的问题。可以在 compute_loss 里加判断if torch.isnan(loss): print(NaN detected, inputs:, inputs) return torch.tensor(0.0, requires_gradTrue)打印出问题 batch 的数据检查有没有空值、异常值。多数时候是数据问题不是 loss 公式问题。11. 我个人的一些实操体会用 ms-swift 做定制化训练最大的感受是框架帮你省掉的是重复劳动但定制化的部分一点都省不了。改模型结构、写自定义 loss、新增 token这些都需要你对模型原理有理解对数据流有把握。我的建议是循序渐进。先把标准流程跑通再一个一个引入定制点。每引入一个改动都单独验证一次确认没问题再加下一个。一次性改太多地方出了问题根本不知道是哪个改动导致的。另外日志要打全。训练时的 loss、学习率、梯度范数评估时的各项指标都记下来。出问题的时候这些日志就是你的线索。我习惯用 tensorboard 或者 wandb 记录回头对比不同实验很方便。最后说一个细节checkpoint 要定期存而且最好存多个。训练崩了可以从最近的 checkpoint 恢复不用从头再来。这个习惯在长周期训练里能救命。
返回列表