
项目代码跑通之后最让人迷茫的往往不是“怎么跑”而是“下一步怎么改”。尤其是深度学习、机器学习项目当训练脚本能正常跑起来loss 曲线开始下降验证集也有一个基础分数时很多人就卡住了接下来是加数据、改模型、换参数还是动损失函数如果“修改损失”只是被理解为随便换一个 loss 函数那大概率会在实验管理上翻车。我自己的经验是跑通之后的创新和改进不是靠某个灵光一现的骚操作而是靠一套可重复、可对比、可回退的工程流程。无论你是刚把别人的开源项目跑起来还是自己手写了一个训练流程后面所有改进动作本质上都围绕三个目标展开实验可复用、结果可比较、出问题可排查。这篇文章适合已经能把项目跑通、但还没建立完整改进流程的开发者。也适合那些频繁改 loss、调权重、试正则却经常改完忘掉、结果对不上的同学。下面按我实际带项目时的顺序从状态体检、工程整理、基线评测、损失函数修改、批量实验管理、常见坑排查六个角度完整拆一遍。1. 跑通只是起点先给当前代码做一次状态体检1.1 跑通分三种演示级、复现级、可迭代级很多人说的“项目代码跑通了”其实指的是“同一个脚本在我的电脑上能运行了”。这在学习阶段够用但如果你想在这个代码基础上做创新和改进就得先问自己一个更实际的问题现在这份代码处在什么阶段我通常把“能跑通”分成三个级别。第一是演示级。脚本能跑能出输出可能结果还不错。但代码里的路径是绝对路径超参数写死在文件顶部数据预处理和模型训练耦合在一起训练日志也没有统一格式。这种代码适合在本地给同事演示效果但经不起改进因为你改一个参数可能要连带改三个文件改完还没办法确认到底动了什么。第二是复现级。依赖版本有记录随机种子固定输入输出路径相对清晰按同样的命令可以重新跑出一次接近的结果。到这个级别基本可以在原有基础上做小范围调整了。改学习率、换优化器、加减层数只要操作规范实验还是可控的。第三是可迭代级。代码划分了数据处理、模型结构、损失函数、训练逻辑、评估逻辑配置文件单独放置日志和模型保存有时间戳和实验名git 记录清楚。到了这一级才适合放心地改损失函数因为你随时能回到上一个版本也能在同一套代码下跑多组对照实验而不是靠记忆判断“之前那个效果好的版本长什么样”。1.2 体检清单依赖、随机性、日志、数据如果你确定要开始改进我建议先花半小时做一次“代码体检”。重点看下面四类问题。依赖锁定。项目跑通不等于依赖可复现。如果依赖清单里都是不带版本号的包名或者靠临时导出生成过两周再回来跑实验很可能装出另一套环境。建议至少记录 Python 版本、主要框架版本、CUDA 相关版本最好用虚拟环境加锁文件的方式固定。否则改完 loss 之后你无法判断性能变化是改动带来的还是环境差异带来的。随机性控制。深度学习训练里有大量随机源包括数据加载顺序、权重初始化、dropout、数据增强。如果不固定随机种子你改了 loss 之后分数变化 0.5 个点到底是因为 loss 真的改好了还是随机波动这是一个决定实验可信度的基础问题。建议在训练入口统一设置随机种子并记录固定随机种子时用的库及其版本。日志输出。跑通之后的第一轮改进大概率会抛异常不抛异常也可能出现 loss 波动、指标下降。如果没有完整日志你只能靠记忆排查。建议至少把每个 epoch 的 loss 和验证集指标输出到文件同时把当前实验的配置单独拷贝一份到实验目录。数据完整性。改进之前先用固定的一批数据确认输入和标签完全对齐。尤其是做多模态、多任务、序列任务时mask、padding、标签索引只要错一位改完损失函数之后很容易被放大。这种错误在原来 baseline 上可能影响不大但在新 loss 下可能直接导致不收敛。注意跑通之后不要急着改代码。先把上面四项检查做完再动手。这不是浪费时间是在给后续所有实验打地基。2. 改进之前先把工程结构从“能跑”变成“能改”2.1 框架层和业务层拆开依赖关系要清楚很多开源项目的代码是研究型写法追求快速验证不太在意模块边界。真实业务里改一个损失函数经常牵动数据加载和训练循环就是因为层次没拉开。我之前整理过不少项目最大的体会是只要把数据、模型、损失、训练、评估这五块相互独立后面做任何实验都会轻松很多。典型的分层思路是这样的project/ ├── configs/ # 实验配置 ├── data/ # 数据处理与加载 ├── models/ # 网络结构 ├── losses/ # 损失函数定义 ├── trainers/ # 训练循环 ├── evaluators/ # 评估指标 ├── utils/ # 日志、可视化、工具函数 └── main.py # 入口依赖方向应该是上层依赖下层不允许下层反过来依赖上层。比如损失函数模块不应该知道数据模块的细节训练模块只负责调用模型、损失、评估接口具体实现由各自模块负责。这样做的好处是你想换一个损失函数只需要新增或修改 losses 目录下的文件训练循环和数据处理不用动你想调整网络结构模型模块的改动也不会污染数据逻辑。如果你在维护更大规模的项目还可以把框架层代码抽到私有仓库让业务模块通过依赖方式引用。我见过不少团队在 Go、Java、前端工程里都采用这种模式。核心不是“私有仓库”这个动作而是把稳定、通用、跟业务无关的框架代码与易变的业务代码分开。框架层一旦稳定就不要频繁改动业务模块依赖版本化发布才能明确知道“我这个实验用到的框架是哪个版本”。2.2 代码格式化、静态检查和 git 提交规范工程结构拆完之后还要解决代码一致性问题。很多机器学习项目是多人协作或长期维护的代码风格不统一会直接影响 diff 的可读性。你改了一行损失计算因为格式化工具不同git diff 里可能多出十几行无关改动别人 review 的时候很难看清真正的变化。前端工程里已经比较成熟比如 vite vue3 项目经常配合 ESLint、Prettier 做代码自动校验和格式化提交之前还有钩子检查。机器学习项目同样可以使用这套思路。Python 项目常用 black 或 yapf 做格式化用 flake8 或 pylint 做静态检查用 isort 整理导入顺序再通过 pre-commit 在提交前自动执行。git 提交规范也很重要。我的建议是一个改动对应一个明确 commitcommit 信息写清楚“改了什么、为什么改”。跑实验前记录当前 commit hash这样实验出问题时能快速定位到对应的代码版本。不要在一堆实验中途频繁 commit 无关修改也不要攒着几十个文件一起提交。2.3 配置与代码分离参数不再写死在文件里改进过程中最危险的操作之一就是在代码里直接改参数。比如在 train.py 里把weight 0.1改成weight 0.5跑完实验后忘了改回去下一次实验直接复用结果和预期差很远但很难排查。更稳妥的做法是所有可调参数都放到配置文件里代码只负责读取。配置文件可以是 yaml、json 或者简单的 ini。关键超参数包括学习率、batch size、epoch 数量、损失函数类型、损失权重、正则系数、优化器参数、数据路径、输出目录等。命令行参数可以覆盖配置文件里的默认值方便跑探针实验。每次实验启动时把最终生效的完整配置保存到实验输出目录。这样哪怕后来改了配置旧实验的配置仍然保留随时可以复查。我一般会同时记录 commit hash、Python 命令、启动时间和环境信息。这样排查问题的时候不需要去猜“当时用的是哪个参数”直接把实验目录里的 config 打开就能看到。3. 修改损失函数之前先建立评测基线和实验记录3.1 为什么损失函数是改进时最先动的环节项目改进可以从多个方向入手增加训练数据、调整数据增强、修改网络结构、改进训练策略、修改损失函数。我之所以建议优先把损失函数当作切入点是因为它的改动成本通常最低。增加数据需要标注、清洗、校验周期长改网络结构可能要重新调整整体连接训练时间也长改训练策略像学习率调度随机性比较大效果不易稳定。而损失函数只是训练目标的一部分代码改动量小可以快速验证。同时损失函数直接决定了模型优化方向改动一个项相当于调整了训练过程中的核心目标。这个特性带来的问题是影响面很大。你需要有清晰的基线来判断改动到底有没有收益。3.2 基线实验至少要记录这五类信息在动任何损失函数之前先跑完一次完整的基线实验并记录以下五类信息。记录项说明原始损失配置当前使用的损失函数类型、权重、辅助项验证集指标最终验证集上的核心指标得分损失曲线和验证曲线每个 epoch 的训练 loss、验证 loss、验证指标训练资源和耗时单次训练耗时、显存占用、内存占用、是否稳定代码和环境版本git commit hash、Python 版本、依赖版本、随机种子很多人会跳过这一步直接去改 loss。实际结果往往是改完之后指标有变化但不知道是好是坏因为根本不记得改之前是什么水平。即使记得一个最终分数也没有曲线做对比无法判断是训练过程变得更好还是只是最终收敛位置不同。3.3 用一条小数据快速验证不要直接全量开跑有了基线接下来就是验证新的损失函数“能不能跑”。很多新手会直接改完代码就上全量数据跑了两小时之后发现 loss 变成 NaN或者根本不下降非常浪费时间。我一般会分三步验证。先用单条或一个 batch 的数据跑前向和反向。确认新损失函数能计算梯度和 batch 维度对齐。这一步能排除大部分维度问题、mask 问题和数值问题。再用一个很小的数据子集跑几个 epoch。比如只在训练集里取 20 个 batch把训练步数设短观察 loss 是否下降、验证集指标是否出现正常波动。这一步能暴露学习率不匹配、权重初始化导致的不稳定、损失函数本身方向可疑等问题。最后再回到完整数据跑正式实验。前面两步通过了不代表全量一定没问题但至少能保证不会因为代码 bug 浪费大量时间。记住损失函数修改的实验最怕的不是效果差而是根本跑不出有效对比。4. 修改损失函数的常用路径与参数边界4.1 加权组合最常用也最容易控制最常用的损失函数改进方式是加权组合。比如多任务场景下总损失可能是分类损失和回归损失的加权和total_loss alpha * classification_loss beta * regression_loss这种方式的优点是简单、直观、容易控制。默认情况下可以先让 alpha 和 beta 等权然后固定模型和数据只调整权重观察验证集指标变化。不要一开始就引入复杂的动态权重机制像不确定性加权、gradnorm、动态平衡等这些方法本身带有额外超参数新手很难判断效果变好是因为权重的变化方式还是因为随机波动。更稳妥的操作顺序是先等权跑一次再根据梯度量级调整。如果某个任务 loss 总是比另一个大很多可以先对 loss 做尺度缩放而不是一味加大权重。权重不是越大越好过大的权重会让对应任务主导整个梯度方向其他任务的指标会跌得很厉害。4.2 加正则项什么时候有效什么时候无效第二种常见路径是在损失函数里增加正则项。这里说的不只是 L1、L2 这类权值衰减还包括结构约束、一致性正则、对比正则等。L2 正则通常加在模型参数上目的是抑制过拟合。结构约束通常加在特征表示上比如让相似样本的特征距离更近、不同类型样本的特征距离更远。正则项不是越多越好。当你把一项正则加入 loss 时实际上是在给原任务增加一个约束。如果数据量本身不大过一个强正则项很容易导致欠拟合loss 曲线下降得更慢验证集指标反而不升。如果数据增强本身已经很强再加正则项可能让模型学不到足够的判别能力。判断标准很简单加入正则项后先看训练 loss 和验证 loss 的关系。训练 loss 上升、验证 loss 也上升基本是正则力度过强训练 loss 下降、验证 loss 不再下降说明过拟合被抑制了两者都不变说明正则项几乎没起作用。遇到最后一种情况不要盲目加大系数先确认正则项真的参与了反向传播数值也没有小到被浮点精度忽略。4.3 替换成结构化损失先判断数据是否支持第三种路径是直接用另一种结构化损失替换原来的基础损失。典型例子包括把多分类交叉熵换成 focal loss 来处理类别不均衡把普通分类损失换成对比损失或 triplet loss 来学习更好的表示。这类替换往往能带来指标提升但前提是数据必须支持新损失的计算。以 focal loss 为例它适用于类别严重不均衡的分类任务。实现时通常有一个 gamma 参数控制对困难样本的关注程度。gamma 取 0 时 focal loss 退化为交叉熵取 1 和 2 是常见选择。但不是 gamma 越大越好gamma 太大时简单样本的贡献被压得太低训练可能不稳定。更复杂的 token 是 triplet loss、contrastive loss 这类基于样本对的损失。它们要求你能够在每个 batch 里构造出合理的正样本对和负样本对。如果你的数据本来就没有这种结构强行套用只会让训练变得非常不稳定。判断标准是新损失需要的监督信号你的数据是否天然具备。没有这个前提替换后大概率失控。4.4 损失函数代码落地的三个安全点修改损失函数代码时最容易踩坑的是三个安全点。数值稳定性。很多损失函数在计算 log、exp、除法时会遇到边界值。常见的做法是使用 log_softmax 而不是分开计算 softmax 再取 log使用 clamp 防止除零对 mask 后的分母单独计数。否则改完 loss 后训练几个 epoch 突然出现 NaN很难定位。梯度检查。新损失函数跑通之后可以做一个简单梯度检查用固定输入比较数值梯度和反向传播梯度。如果差异过大说明 forward 里的某个操作不可导或者自定义操作实现有问题。这一条在实现比较复杂的损失时尤其重要。复现基线。改完损失函数后先在同样数据、同样参数下用原始损失跑一遍确认能复现 baseline。再切换到新损失。这样如果新 loss 导致 loss 曲线异常你能知道是代码改动引入的问题还是损失本身的设计问题。很多“改完 loss 不收敛”的案例追到底其实是代码写错mask 没有对齐标签维度错误或者是把梯度累加逻辑写错了。5. 规范化批量实验比疯狂 tune 参数更重要5.1 每次实验都要绑定代码版本和配置改损失函数往往会形成一组候选方案原始 loss、加权 loss、加正则 loss、替换成 focal loss、不同权重组合。如果不在实验管理上做规划很快会混乱。我建议每次实验至少记录三样东西实验名称、代码版本、完整配置。实验名称可以用简短前缀加时间戳比如exp_focal_gamma2_20250120_1430。代码版本直接记录 git commit hash。完整配置保存成文件。这样跑完一组实验后你可以直接在结果目录里找到“这个实验用的什么代码、什么参数”而不用去翻聊天记录或者备忘录。实验管理器不是必须的。使用 TensorBoard 记录曲线、WandB 做云端跟踪、或者直接用本地 csv 都行。关键是有一个统一的地方能对比所有实验。如果项目还小本地目录加 csv 完全够用如果实验数量上去了再引入更完整的实验管理工具。5.2 输出目录和日志命名会影响排查效率输出目录设计看起来不起眼但在批量实验里极其重要。若所有实验都输出到同一个output/目录新实验覆盖旧实验旧结果丢失就谈不上对比。更合理的方式是每个实验一个独立目录output/ ├── exp_baseline_20250120_0900/ │ ├── config.yaml │ ├── train.log │ ├── model_best.pt │ └── metrics.csv └── exp_focal_gamma2_20250120_1430/ ├── config.yaml ├── train.log ├── model_best.pt └── metrics.csv这样做的目的不是为了好看而是为了可回退。实验表现不佳时可以直接删除对应目录表现不错时可以保留完整环境信息包括模型权重、日志、配置。后续写论文、做汇报、给同事交接代码都不需要重新跑一次。5.3 批量实验的顺序单任务、小批量、全量队列当你准备跑一组对比实验时不要一上来就把日志文件里的任务全部提交。我见过太多人把 8 组实验一起启动结果某组实验的 loss 在第一步就崩了但因为有 7 组都在正常跑反而没有人注意到。等全部结束后一对比才发现有一组训练异常。为此排错成本非常高。更稳妥的顺序是先跑一个实验确认代码正常再跑两到三个探针实验观察彼此逻辑是否独立最后再启动完整队列。批量实验还要注意资源边界。显存不够会导致任务被 kill内存不够会导致进程崩溃磁盘空间不足会导致模型保存失败。如果你给每个任务分配了独立目录可以通过日志快速发现是哪个任务出的问题。6. 改进过程中最常见的四类坑和排查顺序6.1 改完损失不收敛先看方向再看数值如果修改损失函数后loss 曲线不降反升或者直接发散先不要怀疑“这个损失不好用”。按这个顺序排查先确认损失计算的方向是否正确再看数值范围是否合理然后看梯度和学习率最后看权重系数。方向问题的典型表现为loss 一直在涨但验证集指标也在涨说明优化方向和评估目标可能不一致。数值问题的表现是loss 初始值特别大或特别小比如比原来大几个量级这时需要调整损失函数内部的归一化或者是外部位置而不是马上改学习率。梯度问题的判断也很简单打印模型首层梯度的范数如果梯度消失或爆炸先缩小学习率或者检查数值稳定性。不要一上来就把权重调成很大的数那只会让训练更不稳定。6.2 验证集涨了测试集跌先查正则和数据泄漏另一种常见现象是验证集指标提升了但换到测试集后下降明显。这通常指向两个原因过拟合和数据泄漏。过拟合时模型在验证集上记忆了更多信息但泛化能力并不强这时应该增加数据量、增强正则、早停或降低模型容量而不是继续调 loss 权重。数据泄漏的情况更隐蔽。比如数据归一化时使用了全量统计量导致验证集信息混入训练过程或者数据增强只在训练集做了但在验证集和测试集上做了不对称处理又或者是同一个样本被文件名或 hash 判断错误导致训练集和验证集有重叠。排查泄漏时先检查预处理代码是否把全量数据统计量传给了验证集再检查数据拆分逻辑是否按文件级去重。6.3 损失下降但指标不动先对齐优化目标和评估目标这个问题在改损失函数时特别常见loss 明显下降了但准确率、mAP、F1 等核心指标没有变化。你要先理解损失函数是给优化器用的指标是给人看的两者本来就未必完全一致。比如在目标检测任务里回归分支的 loss 下降可能不直接影响分类的准确率多任务加权里如果权重偏向某个任务另一个任务的指标自然不会明显变化。处理方式不是强行让 loss 跟指标对齐而是分析当前训练是否真正在优化你想要的方向。如果你想让指标提升可以引入与指标更一致的代理损失比如在 ranking 任务里使用 listwise 损失而不是二分类损失。但在更换之前确认自己真的需要“loss 和指标直接相关”这个性质否则不要为了让曲线好看而做无意义调整。6.4 代码重构后结果对不上先回跑基线每次调整工程结构或代码格式之后如果发现实验结果对不上原有了第一反应应该是回跑当前 commit 的基线实验而不是去怀疑新 loss 的设计。重构最容易引入的隐性错误包括数据预处理顺序改变、mask 传递丢失、随机种子没有固定、计算设备发生切换、浮点累加顺序不同等。回跑基线时不要只对比最终指标还要对比 loss 曲线和验证集曲线走势。如果原始 commit 能复现出原来的曲线新提交无法复现直接逐一 diff 最近改动如果两个都能复现说明只是随机差异需要确认是否固定了随机种子。这个习惯能避免你在错误的基础上不断叠加改动越改越乱。回到最开始的问题项目代码跑通后到底怎么创新、改进、修改损失我的答案很简单。先不要急着追求惊艳效果把工程结构整顺把基线记录清楚把损失函数的改法拆成小步实验把输出目录和代码版本管理好。踩过几次坑之后你会发现真正决定项目能不能持续改进的不是某一次巧妙改动而是你能否在多次实验里快速判断“什么有效、什么无效、为什么”。