2026年1月10日,我把手头机器学习相关的环境配置、常用脚本、模型选型逻辑、踩坑记录重新过了一遍,整理成了一份“自用版”笔记。为什么强调自用?因为这套东西不打算做成通用教程,它只回答“我自己实际跑得通、用得顺、报错了知道去哪查”的问题。机器学习这块最大的成本通常不是模型创新,而是环境不稳定、数据对不上、训练完了不知道怎么解释。这份笔记就是把这些琐事固定下来,让我在下次开工时不用重新踩一遍。
这份笔记适合两类人参考:一类是刚入门但已经能跑通几个 demo,想沉淀自己工作流的读者;另一类是有一定经验、想看看别人怎么组织自用备忘的从业者。它不是从零教学,默认你已经了解训练集、验证集、损失函数、梯度下降这些基础概念。如果按这份笔记的思路去搭建自己的版本,你会发现很多坑其实可以提前避开。
1. 先说这份自用笔记是怎么来的
1.1 为什么用日期作为版本号
做机器学习的人对版本敏感,因为一个模型在 2025 年底跑出来的结果,换到 2026 年初某个依赖库升级后,可能就完全复现不了了。我给自己定的规矩是:每半年整理一次环境快照和文档,文件名带上日期。笔记记录的是“这一天”我实际在用的状态,不是理想状态。这样半年后如果环境出了问题,我还能回到当时那份 requirements 和配置去对照。
日期版本还有一个额外好处:它能倒逼我不停更新。每次新建带日期的笔记时,我会顺手把那些用得少了、已经被更好方案替代的内容删掉。否则笔记会慢慢变成垃圾场,越攒越乱,找一条命令要翻半分钟。自用笔记的生命力不在于写了多少,而在于每一条都是当前真正需要的。
1.2 笔记覆盖的范围和边界
这份笔记覆盖了从数据清洗、特征工程、模型训练、评估解释到部署维护的完整链路。每个环节我只保留了最常用的一套方案,没有罗列大量“备选方案”。比如特征工程部分,我不会把十种编码方式全写上去,因为实际工作中,类别特征我用目标编码,数值特征我先看分布再决定分箱还是标准化,时序特征则单独处理。够用就行。
边界也很重要。我没有写强化学习、大规模分布式训练、多模态推理这些内容,不是它们不重要,而是我目前自用的场景没到那个复杂度。笔记里每一章都对应我真实做过的项目类型:结构化数据建模、文本分类、轻量级图像任务。这样记录才能保证里面的参数和判断都是验证过的,而不是从文档里抄出来的。
2. 环境快照:2026.1.10 我固定下来的工具链
2.1 Python 环境与依赖管理
我现在的环境基于 Python 3.11,这是我自己反复测试后固定下来的版本。3.11 对主流机器学习库的兼容性比较稳,第三方扩展包的预编译支持也足够全。我不建议在这个基础上盲目追 Python 3.13 或更高版本,因为某些底层库可能还没有跟上,容易出现编译错误。对我来说,稳定的解释器版本比新语法和性能提升更重要。
依赖管理我用uv配合requirements.txt。uv解决包依赖的速度比传统的pip快很多,而且它能生成uv.lock文件,锁住所有传递依赖的版本,这对复现实验非常重要。我的做法是在项目根目录维护两个文件:一个是requirements.in,只写顶层依赖;另一个是requirements.txt,由uv根据当前环境生成完整锁定结果。这样我既能看到自己显式装了哪些包,也能在需要完全复现时拿到准确的依赖树。
# 初始化项目和虚拟环境 uv venv .venv --python 3.11 # 激活环境 source .venv/bin/activate # 安装包 uv pip install numpy pandas scikit-learn lightgbm torch安装完核心包后,我会立刻跑一个冒烟测试,确认 numpy、pandas、torch 这些基础库能正常 import,并且 GPU 能被 torch 识别。这一步五分钟就能完成,但能避免很多“装了一下午环境,结果一运行就崩”的尴尬。
2.2 深度学习框架与硬件加速
深度学习框架我日常用的是 PyTorch。选择它的理由很直接:社区活跃、文档齐全、调试自由度大,而且从科研原型到生产导出的链路比较成熟。PyTorch 的自动求导机制在自定义 loss 时非常灵活,我经常需要写一些带样本权重的损失函数,用 PyTorch 只需要把计算过程写成普通张量运算就行。
GPU 相关配置我会单独检查。命令里指定 CUDA 版本和 cuDNN 版本时,一定要跟驱动支持的版本匹配,否则 torch 虽然能装上,运行时会报“CUDA driver version is insufficient”之类的错。一个相对稳妥的做法是先在命令行里查一下显卡驱动支持的 CUDA 版本,再选择对应 PyTorch 安装源。
# 检查 GPU 是否可用 nvidia-smi # 在 Python 里确认 torch 能访问 GPU python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"我踩过一次很典型的坑:在一台没有 GPU 的机器上写了带.cuda()的代码,然后换到有 GPU 的机器上跑没问题,但换回 CPU 环境就直接报错。后来我做了一条硬性约定:项目代码里统一用device = torch.device("cuda" if torch.cuda.is_available() else "cpu")来获取设备,不写死.cuda(),这样同一套代码在开发和低成本推理环境都能跑。
2.3 训练辅助工具和监控脚本
训练过程不能只靠眼睛盯屏幕,我固定用几个小工具。tqdm负责进度条,tensorboard或者wandb负责指标曲线。自用项目我更多用 tensorboard,因为它不需要额外账号,本地起服务就完事。实验目录里保存的 event 文件,我通过一个简单的 Python 脚本读取并转成 CSV,便于后续画图。
代码层面的监控主要靠自定义回调。比如在每个 epoch 结束后记录 train loss、val loss、F1 或者 AUC,同时保存当前最好的模型。这里有一个很小的细节:保存模型时不要只保存state_dict,我会一并保存 optimizer 的 state、scheduler 的 state、当前 epoch、best metric 值。这样才能实现真正的断点续训,而不是模型恢复后优化器状态从零开始,导致效果变差。
import torch checkpoint = { "model_state": model.state_dict(), "optimizer_state": optimizer.state_dict(), "scheduler_state": scheduler.state_dict(), "epoch": epoch, "best_val_score": best_val_score, } torch.save(checkpoint, f"checkpoints/epoch_{epoch:03d}.pt")恢复训练时,加载这个 checkpoint 后,还要手动把 model、optimizer、scheduler 各自的load_state_dict都调一遍。很多人只恢复模型权重,然后发现后续训练 loss 反常地波动大,就是优化器状态没有恢复的原因。
3. 数据工程:每次建模前我必做的几件事
3.1 数据清洗的固定套路
我很少有拿到手就能直接建模的数据。固定套路第一步是确认数据形状和字段类型,用df.info()和df.describe()快速扫一遍,然后针对缺失值、重复行、异常值分别处理。缺失值处理不是一上来就填充均值,而是先看缺失比例。低于 1% 的字段可以直接删除对应样本;高于 30% 的字段基本要慎重,要么作为“是否缺失”特征保留,要么用业务规则填充。中间比例则根据模型选择:树模型可以直接保留缺失,让模型自己学;神经网络和线性模型通常要做填充。
异常值处理我的默认做法是使用分位数裁剪,而不是直接删除。以某个数值特征为例,我会把低于 1% 分位数的值替换成 1% 分位数,高于 99% 分位数的值替换成 99% 分位数。这样既避免极端值干扰梯度,又不破坏数据量。这个操作在 scikit-learn 里可以用RobustScaler配合quantile_range参数实现,但为了灵活度,我通常自己写一个小函数。
这里要提醒一个很容易被忽略的问题:清洗逻辑必须在训练集、验证集、测试集上保持一致。比如分位数裁剪的阈值只能从训练集算出,然后应用到所有数据集。如果分别对测试集单独裁剪,等于把测试集信息泄漏进了流程,估值会虚高,上线后效果立刻现原形。
3.2 特征工程的几个高频技巧
特征工程是我投入时间和产出比最高的环节。对于类别特征,我比较喜欢目标编码。目标编码的核心思想是用目标变量的历史均值来编码类别,比如一个类别对应样本的点击率或转化率。它可以有效处理高基数类别,缺点是容易过拟合,所以我会在编码中加入平滑项,并且在训练集内部做交叉验证式的编码,防止用全量训练集的目标均值泄漏到验证集。
数值特征方面,我先看分布是否长尾。长尾特征我会先做对数变换,再标准化。比如用户金额、时长这类特征,直接丢进线性模型或者神经网络,通常会被极值带偏。对数变换后数值范围更紧凑,训练也更容易收敛。如果特征存在明显周期性,比如小时、星期几,我会拆成sin和cos两列,保留周期的连续性。
时间特征我坚持一个原则:严格遵守“过去预测未来”。用时间窗口统计量时,窗口只能使用截止到当前预测时刻之前的数据。比如用过去 7 天的平均行为作为特征,那么训练样本里每个样本的特征必须基于该样本所对应时间点之前的数据生成,而不是直接用整个训练期的最后 7 天。这个错误很隐蔽,尤其是把数据整体切分时,一不小心就把未来信息塞进去了。
3.3 样本不平衡与数据版本管理
分类问题经常遇到类别不平衡。我的处理优先级是:先考虑能否换评估指标,再看是否做采样或调权重。如果只是简单预测少数类,AUC 和 PR-AUC 往往比 accuracy 更适合;如果少数类本身业务价值很高,我会优先尝试给模型 loss 里的少数类样本加权重,因为这样不会减少数据量。当少数类占比极低(比如低于 1%)且样本量足够大时,再做下采样。上采样我使用得很少,因为简单复制少数类样本不仅增加训练时间,还容易造成对少数类样本的过拟合。
数据版本管理是自用项目里容易被跳过的一环。我通常会在每次实验前把数据集复制一份到以日期命名的目录,并同时生成一份字段说明的 Markdown 文件。这样即使后来我发现数据处理逻辑变了,也能定位到旧结果是用哪份数据跑出来的。用脚本处理数据时,我会把随机种子固定下来,通常设为 42。不要小看这个细节,随机种子不一致会让特征编码、训练集划分都不同,导致两次实验的差异很难归因。
4. 模型训练与调参:从基线到收敛
4.1 先跑通一个弱基线再谈优化
我的习惯是拿到干净数据后,先不做复杂特征工程,直接跑一个逻辑回归或者浅层 LightGBM 作为基线。这么做有三个目的:第一,验证数据 pipeline 是否真的没有 bug,能输出合法的训练矩阵;第二,得到一个“至少这个分数”的下限,后续所有复杂模型都要跟它对比;第三,快速暴露特征和标签里的异常,比如存在完全线性相关的列、标签有 NaN 等。
基线模型我会记录它的准确率、AUC、F1 等核心指标,并且固定一组默认超参数。如果后续喂给深度模型的输入跟基线用的不是同一批特征,我会很警惕。因为特征集合不一致,模型之间的差异就说不清楚是模型能力带来的,还是特征带来的。为了公平对比,我通常会先确定一个 baseline 特征集,所有模型都在这个特征集上跑,再逐步迭代加特征。
4.2 超参数搜索的节奏安排
超参数搜索我很少一上来就做大规模贝叶斯优化,因为成本高、收益不稳定。对于树模型,我会先手动设几组候选值,比如 LightGBM 的num_leaves在 16、31、63 之间试,learning_rate从 0.05 到 0.1 之间选,min_child_samples在 20 到 50 之间调。手动尝试能让我对参数敏感度形成直觉,然后再用Optuna做一次短时间的自动搜索。
使用 Optuna 时,我会限定总试验次数不超过 50,每个 trial 的训练轮次用早停控制。这样整个搜索时间可控。相比之下,网格搜索在参数维度较高时会爆炸式增长,浪费时间在明显无价值的组合上。随机搜索比网格好一些,但如果预算有限,贝叶斯优化搜到的组合通常更高效。
深度学习模型的超参数搜索我更保守。学习率是我最优先调的参数,我会先尝试 1e-4、3e-4、1e-3 这几个数量级。其次看 batch size 对收敛稳定性的影响,再调整正则化系数和 dropout。一个粗糙但有效的方法是看前 100 个 step 的 loss 下降曲线,如果 loss 不降甚至上升,先降学习率;如果 loss 剧烈震荡,再加大 batch size 或降低学习率。
4.3 断点续训与训练稳定性
训练大一点的模型时,断点续训不是可选功能,而是一条保命通道。我的代码里从第二个 epoch 开始,每个 epoch 结束都会保存一次 checkpoint,同时只保留最近 3 个文件,避免磁盘被撑爆。命名里带 epoch 和 val_score,方便快速找到最优版本。
训练稳定性方面,我最常用的是梯度裁剪。对于循环模型和文本模型,梯度裁剪能有效防止梯度爆炸导致的 loss 变成 NaN。设置max_grad_norm为 5.0 是我比较默认的起始值,如果训练还震荡就降为 1.0。另外,输入数据里的 NaN 也会直接导致训练崩溃,我会在数据预处理最后一步用np.isnan检查全部特征矩阵,确保没有缺失值漏到模型里。
学习率调度器我默认用余弦退火。它的好处是前期学习率较高、加快收敛,后期学习率低、精调参数。使用调度器的时候,要注意调度器的 step 更新时机。PyTorch 里常见做法是每个 epoch 结束调用scheduler.step(),而不要和 optimizer.step() 混在一起。如果用了梯度累积,还要重新计算多少个 step 才算一个 epoch,否则学习率变化节奏会乱。
5. 评估与解释:不只看 AUC
5.1 指标口径先对齐
每个项目开始前,我会花时间确认业务到底关心什么指标。比如同样是点击率预测,有的场景只关心排序正确度,用 AUC 或者 NDCG 更合适;有的场景关心实际点击人数,那要看召回率、人均点击数,甚至要看线上 AB 实验的转化率。不能简单把 AUC 当万能指标。
自用笔记里我写了一个指标速查表:回归问题优先看 MAE 和 RMSE,其中 RMSE 对异常点更敏感;分类问题先看 PR-AUC,因为很多实际场景正样本比例低;排序问题看 NDCG 和 MRR。评估时要特别注意样本权重,如果训练时给某些样本加了权重,评估时应该用不带权重的指标来判断模型真实效果,否则会被训练目标带偏。
线下评估的另一个要点是划分方式。如果数据存在时间因素,我不会用随机分割,而是用时间靠前的数据训练,时间靠后的数据验证。这样才能模拟上线后的环境。随机分割在非时序数据上可以用,但一旦数据里有用户、店铺这类群体结构,我还会避免同一个人同时出现在训练集和验证集,否则模型可能只是“记住”了这个人,而不是学会泛化。
5.2 bad case 复盘与可解释性
模型评估不能只看数字,还要落到具体样本。我会把预测错得最离谱的样本输出到一个 bad case 表里,字段包括样本 ID、预测概率、真实标签、概率最高的错误类别、特征值快照。然后逐条看这些样本到底长什么样。很多时候,bad case 能揭示数据分布之外的问题,比如某些类别本身标注就很模糊,或者有些特征值在训练集里根本没有出现过。
可解释性工具我用的最多的是 SHAP。对树模型和线性模型来说,SHAP 能给出每个特征对预测结果的具体贡献方向。在排查 bad case 时,我会针对单个样本画出 explainer 的 waterfall 图,看是哪个特征把它推向错误的那一侧。如果发现某个高频错误都源于同一个特征,说明这个特征的含义可能被理解错了,或者需要做更细致的处理。
要注意的是,SHAP 在神经网络上的解释能力没有在树模型上那么直接。神经网络里我更倾向于用积分梯度或者简单的注意力可视化。自用场景下我不会过度依赖解释工具,它们的核心作用是帮助我形成“模型为什么错”的假设,而不是给出最终答案。
5.3 过拟合、数据泄漏、标签噪声排查
过拟合最常见的表现是训练指标一直涨,验证指标先涨后跌。我处理过拟合的顺序很明确:先尝试增加正则化,降低模型复杂度;然后看是否可以通过更多的数据增强或数据扩充来缓解;最后才考虑简化特征。对于树模型,我会降低max_depth和num_leaves,增加min_child_samples;对于神经网络,我会加大weight_decay和dropout。
数据泄漏是比过拟合更隐蔽的问题。常见泄漏来源包括:特征里包含未来信息、数据去重不彻底导致训练验证重叠、目标编码时用了全量数据统计。排查数据泄漏有个土办法:训练一个高容量模型,看训练集指标是否异常高而验证集正常,或者验证集指标高到不合常理。如果出现这种情况,我会逐项检查特征生成时间戳,确保每个特征在对应样本的预测时刻都是已知的。
标签噪声的问题我会通过“重新标注检验”来判断。随机抽几十个预测错的样本,人工重新看标签是否正确。如果发现大量样本标签本身就是错的,那最要紧的不是调模型,而是修数据。我曾经处理过一个文本分类项目,验证集 F1 停在 0.7 附近上不去,后来人工复核发现有约 10% 的样本类别标错了。修正标签后,同样的模型直接提升了将近 5 个点。
6. 部署与线上维护:模型能上线才是终点
6.1 模型导出与推理性能优化
自用项目也要考虑“怎么把模型跑起来给别人用”。我的常用做法是先把 PyTorch 模型导出成 ONNX 格式,再用 ONNX Runtime 做推理。ONNX Runtime 在 CPU 上的并行加速效果很不错,而且部署依赖比完整 PyTorch 环境轻很多。导出的关键是固定动态轴,让 batch 维度可变,但其他维度保持固定。
导出前我会做一次推理一致性检查:同一批样本,用原模型和导出的模型分别预测,对比最大误差。一般来说最大误差应该小于 1e-5。如果误差偏大,常见原因是某些操作不被 ONNX 支持,导致被替换成了不精确的实现。遇到这种情况,我通常会把不好导出的部分留在外部预处理,不强行塞进图里。
性能方面,我的优先级是先减少不必要的重复计算。比如文本分类里的 tokenizer 可以在服务启动时初始化一次,不要把每次请求时的历史句柄重新加载。再用onnxruntime的时候,我会把线程数设置成跟 CPU 核数匹配,而不是默认值。实测下来,这些简单调整有时能让单次推理延迟下降一半。
6.2 服务接口与批量预测的取舍
自用部署我采用两种模式,按需选择。一种是批量预测:输入一个 CSV 或者一份数据库表,脚本循环预测后输出结果。优点是简单、稳定、方便回溯,适合离线打标、报表计算。另一种是 HTTP API:用 FastAPI 包一个服务,接收 JSON 请求返回预测结果,适合实时交互场景。
对于 API,我会把输入校验放在最前面。模型对输入格式很敏感,比如数值字段必须是浮点数、类别字段必须在训练时见过的集合内。如果输入里出现了训练时没见过的类别,代码要给出明确报错而不是默默填充默认值。我最开始上线 API 时没处理这种情况,结果线上出现一批莫名预测值,排查了很久才发现是字符串前后有空格导致类别对不上。
服务化之后还要考虑并发和缓存。同一个用户短期内多次请求同样的内容,如果特征是相同的,可以直接缓存预测结果。这个优化在业务上有时候比调模型更有效。我通常用简单的字典缓存加过期时间,不引入额外中间件,够用就行。
6.3 线上监控与回归测试
模型上线后必须盯几个东西:请求量、平均延迟、预测分布的稳定性、关键特征缺失率。我最关心的其实是特征缺失率。线上工程链路里经常会有上游字段变更,导致模型入参里某个字段变成空值。模型不会主动报错,只会用默认填充逻辑,但预测分布会悄悄偏掉。所以我会每天统计一次线上预测结果的分位数,跟训练集预测分布做对比,如果偏差超过阈值就要告警。
回归测试是每次改特征或者重新训练模型后必做的。我会准备一份固定的回归样本集,包含各种典型情况和偏例,比如空值样本、极端值样本、旧类别样本、标记过的 bad case。新模型上线前,在这份回归集上把预测结果跟旧模型对比,除了看指标变化,还要看是否有样本的预测出现明显翻盘。如果翻盘样本集中在一部分业务场景,要先评估是不是数据分布变化导致,而不是直接上线。
这里有个我亲身经历的问题:某次只是改了特征处理里一个分位数的默认值,结果导致线上 2% 的样本预测值整体偏移。如果我不做回归测试,这个问题很可能要等业务方报告才发现。自用项目也要养成这个习惯,哪怕只给自己用,也不能省。
7. 常见问题速查表:真实踩过的坑
这一节我整理了几条高频问题,每一条都是自己或其他同事实际遇到过、并且有明确解决路径的。表格不一定覆盖所有情况,但照着查能省下不少排查时间。
| 现象 | 可能原因 | 解决路径 |
|---|---|---|
| 训练 loss 一开始就是 NaN | 学习率过大 / 输入有 NaN / 标签有 NaN | 降低学习率;检查输入和标签矩阵,填充或删除 NaN;加梯度裁剪 |
| 模型在验证集上效果很好,线上很差 | 训练验证分布不一致 / 特征泄漏 / 预处理逻辑不同 | 按时间切分验证集;检查特征是否包含未来信息;统一离线在线预处理代码 |
| 同一份代码,换个机器结果不同 | 随机种子没固定 / 依赖库版本不同 / GPU 浮点差异 | 固定 Python 随机种子、numpy 随机种子、torch 随机种子;用 requirements 锁版本 |
| 类别特征有很多新值 | 线上出现训练没见过的类别 | 构造未知类别映射,把所有新值归入unknown;定期重训模型 |
| GPU 利用率很低 | batch size 过小 / 数据加载慢 / CPU 预处理瓶颈 | 增大 batch size;使用 DataLoader 的pin_memory=True;检查是否有大量文件 IO |
| 训练集指标高,验证指标低 | 过拟合 / 数据泄漏 | 先检查泄漏;再调正则化;最后减少特征或增加数据 |
| SHAP 值解释不了模型 | 特征高度相关 / 模型非线性太强 | 使用独立背景数据;聚合多个样本的 SHAP 值观察整体趋势;不单个解释极端样本 |
| API 响应慢 | 模型太复杂 / 频繁重新预处理 | 使用 ONNX Runtime;把 tokenizer 初始化和特征映射缓存到内存;批量推理替代单条推理 |
这些坑单独看都不难,但叠加在一起就会让人崩溃。我把速查表放在笔记倒数第二页,就是为了在快速查证时能一眼看到。排障最忌讳的是乱试,每改一个变量之前先写下当前假设,改完只做一个小范围验证。养成这个习惯之后,ML 排障从“玄学”变成了“逻辑推理”。
8. 给同样自用型学习者的一点建议
我不是那种每天追新论文、新框架的人,更愿意把手里这一套工具链用透。自用笔记做到今天,我个人最大的体会是:稳定的工作流比广博的知识清单更值钱。机器学习项目里真正消耗精力的是那些重复的、容易被忽略的环节,比如环境固定、数据版本管理、断点续训、回归测试。把这些环节固化下来,每次开工就能把精力集中在数据和模型本身。
如果你也打算整理自己的自用笔记,我建议从一页速查表开始,而不是写几十页详细文档。先写下最常用的命令、最讨厌的报错、最频繁调整的参数,然后边做项目边补充。每隔三个月翻出来删减一次,把它压缩到能够一屏看完的密度。到了 2026 年的下一次整理时,你会发现这份自己用出来的笔记,比任何付费课程笔记都有用。