机器学习练习分层实战:从手写梯度下降到推理接口
1. 机器学习练习的层级划分先搞清楚你在练什么1.1 看懂和跑通之间隔着一整个太平洋关于机器学习的练习市面上从来不缺教程缺的是那种做完之后手上真的长本事的练习。我带过几个刚入行的朋友也看过不少自称“学过机器学习”的简历发现一个特别普遍的现象视频看得飞快笔记记得工工整整公式也能推但真让他从一份脏数据开始训练一个能用的模型立刻卡死。卡住的地方往往不是什么高深理论而是标签格式不对、类别极度不平衡、验证集划分错了、评估指标选错了这类琐碎问题。这就是机器学习练习最尴尬的地方。教材里的练习往往是把数据洗干净、切好片、喂给模型、看准确率一条龙顺畅得不真实。而实际工作时前面那段又脏又累的活占了八成时间。所以我对练习的态度一直很明确练习的价值不在于让你见到多少算法而在于让你在受控的、规模足够小的场景里把每一个环节亲手做一遍把每个环节可能出的错亲手犯一遍。犯过的错才是你的。这篇东西写给两类人一类是刚从课程里出来公式都认识但没真正跑通过完整流程的人另一类是已经能跑通流程但总觉得模型效果上不去、调来调去都是玄学的人。前一类人要的是完整的动作示范后一类人要的是问题定位的方法。下面我按自己的练习顺序来写从最笨的手写实现开始一路做到一个能拿出去用的推理接口。所有涉及数据的地方我都用匿名化的小规模样例代替你换成自己手上的数据照样能跑。1.2 四个层级的练习别老停在第一层我把机器学习练习分成四个层级你对照一下自己现在停在哪一层。层级练习形态你实际在练的能力典型问题第一层照着教程复现代码抄下来能跑通环境配置、API 记忆换个数据集就崩第二层换数据集、改超参观察指标变化调参直觉、指标理解不知道变化是为什么第三层改造模型结构或特征工程对比方案方案设计、归因分析改完说不清哪一步起作用第四层从零设计流程并重写核心模块工程判断、抽象能力没人给你标准答案绝大多数人卡在第一层和第二层之间表现就是“教程能跑自己的数据跑不动”。原因很简单第一层的练习把所有的决策都替你做完了数据怎么切、用什么指标、超参取多少全给你写死了。你练的其实只是“照着敲”这个动作。真正有价值的练习从第二层开始而第二层最关键的一点是每次只改一个变量并且把结果记下来。没有记录的调参不是练习是许愿。1.3 我给自己排的练习顺序网上流传的练习清单通常是按算法排的线性回归、逻辑回归、决策树、集成、神经网络一路排下去。这个排法的问题是把算法当成目的了。我自己的排法更贴近真实项目的时间分布顺序是这样的手写一次梯度下降不用任何现成的优化器目的只有一个把损失、梯度、学习率这几个词从名词变成手感。拿一份真实存在瑕疵的小数据从头做一次二分类包括清洗、特征、切分、训练、评估、错误分析。在同一个数据上换三种不同量级的模型比较它们的表现和代价把“没有免费的午餐”这句话变成具体数字。做一次回归任务把评估从准确率换到误差类指标体会指标选择对结论的颠覆性影响。搭一个最小的神经网络把训练曲线画出来学会看曲线判断状态。把最好的那个模型包成一个能接受输入、返回预测的函数体会训练和推理之间那条线。这个顺序有个好处每一步都建立在上一步的痛点上。手写过梯度下降再看框架的优化器你就知道它替你做了什么做过完整流程再调参你就知道哪些参数值得动看过训练曲线你就不会再盲目地调迭代次数。练习最忌讳跳步因为跳过的那些环节恰好是将来出问题的地方。2. 环境与数据练习翻车的八成原因在这儿2.1 环境装最少的别一上来就全家桶新手最容易犯的错是看到什么库都装一遍最后环境里塞了几十个包版本互相打架一个简单的脚本报出莫名其妙的错。我现在的做法是给每个练习建独立的环境只装四样东西数组计算、表格处理、绘图、以及要练的那个框架。其余的等真的需要了再装。这么做不是为了洁癖是因为依赖冲突排查极其耗时而练习本来时间就零碎耗在环境上的每一分钟都是从正事里扣的。还有一个具体建议练习代码不要放在一个巨型 notebook 里。我见过一个文件写了两千行的练习跑一次要十分钟想改中间某个环节得从顶上重新跑。正确的做法是拆成几个小文件数据加载一个、特征处理一个、训练一个、评估一个。好处是每个环节你能单独跑、单独验证。比如你怀疑特征处理有 bug只跑那一个文件就行不用等整个流程。这种拆分的意识本身就是工程能力的一部分练的时候顺手就练了。至于要不要用 GPU我的看法是练习阶段除非你在练图像或大模型微调否则先用 CPU 把流程跑顺。原因很实际CPU 上的慢会让你被迫思考“这一步到底有没有必要”而 GPU 上的快会让你不自觉地用算力掩盖设计上的问题。等你把流程压得足够精简了再上加速才有意义。2.2 数据集怎么挑小、脏、有业务含义练习用的数据我有个明确偏好小规模、带瑕疵、有可解释的业务含义。三个条件缺一不可。规模小是为了迭代快。一份几万行、几十个特征的数据在一台普通机器上训练一个中等复杂度的模型只要几秒你一天可以试几十种方案。而如果一上来就处理千万级数据光读文件就要几分钟你的注意力会被消磨光。带瑕疵是为了练真本事。教科书数据最大的问题是太干净缺失值规规矩矩、类别均衡、没有异常点。你自己去找一份从真实业务流程导出的数据大概率会遇到这些问题数值列里混着字符串、日期格式有好几种、类别列有肉眼看不出来的空格差异、某些类别只有个位数样本。这些才是真正需要你动手解决的东西。有业务含义是为了让错误分析有意义。如果你连“这个样本被预测错了意味着什么”都说不清楚那你的错误分析就只能停留在“哦这行分错了”。但如果你知道每一列代表什么、这个预测结果会被用来做什么决策你就能判断“这类错误是可以接受的”还是“这类错误必须消灭”。这个判断能力比调高两个点的指标值钱得多。注意不要用那种被用烂了、网上答案满天飞的公开数据集做练习。你会不自觉地去找别人的结果对照而不是自己想。找一份匿名的、结构简单的、你自己能解释清楚的数据哪怕只有几千行。2.3 数据泄漏最隐蔽也最致命的坑如果只能给一条练习建议我会给这条在切分数据之前不要看测试集的任何统计信息。这句话听起来像废话但真正做对的人不多。泄漏的形式很多我按遇到的频率排一下。第一种是标准化或归一化时用了全量数据算均值和方差然后再切分。这样测试集的统计信息已经渗进训练过程了指标会虚高。正确做法是只在训练集上算这些统计量然后应用到验证集和测试集。第二种是时间相关的数据随机切分。如果你的数据带时间顺序随机切分会让模型“看到未来”指标能高得离谱。这类数据必须按时间切训练用早期、测试用后期。第三种更隐蔽特征里包含了标签的衍生信息。比如预测某笔业务会不会成功特征里却有一列“结算金额”而结算只有在成功之后才会发生。这种列在训练时看起来相关性极强上线后完全没用。我的自检方法很土但有效拿到一份数据后先用一个极其简单的规则或者单一特征去预测如果准确率高得反常立刻停下来查泄漏。正常的业务数据单一特征很难有特别强的区分度。如果随手一猜就对一定是哪里漏了。这个“先怀疑自己”的习惯帮我省过很多次白干的功夫。3. 五个循序渐进的练习题目拆解3.1 练习一不用任何库手写一次梯度下降这个练习的目的不是让你以后都手写而是让你彻底搞明白优化器在干什么。任务很简单给一批一维输入和输出拟合一条直线。用最朴素的写法循环若干次每次算预测值、算误差、算梯度、更新参数。# 伪代码用纯 Python 列表实现不依赖任何机器学习库 def train(xs, ys, lr0.01, epochs200): w, b 0.0, 0.0 n len(xs) for epoch in range(epochs): grad_w, grad_b 0.0, 0.0 loss 0.0 for x, y in zip(xs, ys): pred w * x b err pred - y loss err * err grad_w 2 * err * x / n grad_b 2 * err / n w - lr * grad_w b - lr * grad_b if epoch % 20 0: print(epoch, round(loss / n, 6), round(w, 4), round(b, 4)) return w, b写完之后做三件事这三件事比代码本身有价值得多。第一件把学习率改成 0.5 和 0.0001 各跑一次观察损失曲线的形态。你会亲眼看到前者发散、后者爬得极慢这个体验比看一百张示意图管用。第二件把输入做一次尺度缩放比如全部乘以 100再跑一次你会发现原来的学习率不好使了。这就是为什么实际做特征工程时要标准化你不是在“让数据更规范”你是在让不同维度的梯度处于同一个量级否则优化过程会被数值大的那一维主导。第三件把迭代次数加到很大看损失最终停在哪里那个值就是这批数据能达到的下限。很多人调模型时不断加迭代次数其实早就收敛了白白浪费时间。这个练习做完你会对三个词建立直觉学习率决定了步子多大梯度的尺度决定了方向准不准损失值告诉你离目标多远。后面所有关于优化的讨论都建立在这三个直觉上。3.2 练习二一个完整分类任务走完全流程这是整个练习清单里最重要的一项我建议至少花两三天认真做。任务选一个二分类问题数据规模控制在几千行、十几列。完整流程包含七步每一步都要单独写代码、单独验证。第一步是看一眼数据。不是跑几个统计函数就完事而是真的逐列看这一列是什么类型、有没有缺失、缺失比例多少、取值范围是否合理、类别列的取值分布如何。这一步经常能发现惊喜比如某一列的取值里同时存在“是”和“是 ”带空格如果不处理模型会以为是两个类别。第二步是处理缺失。数值列填中位数还是均值取决于分布是否偏斜类别列要么填一个单独的“未知”类别要么根据业务逻辑补。这里有个细节填缺失值所需的统计量必须只在训练集上算然后应用到验证和测试集理由前面说过。第三步是构造特征。这一步最能体现功力。原始列往往是业务过程的中间产物直接喂给模型效果一般。举个常见的例子如果有一列是“金额”一列是“次数”那“单次平均金额”这个新特征往往比原来两列都有信息量。构造特征的原则是让模型更容易看到你希望它看到的东西。模型不会自己算除法你得替它算。第四步是切分。训练、验证、测试三份比例大概六二二或者七一点五一点五。验证集用来做选择测试集只在最后用一次。我见过太多人反复在测试集上调参最后报出来的指标毫无意义。第五步是先建基线。别急着上复杂模型先跑一个最简单的规则或者逻辑回归。基线的意义是给你一个参照系让你知道后面的复杂模型到底带来了多少提升。如果复杂模型只比基线高一个点那这一个点值不值得那些额外的复杂度和维护成本是要打问号的。第六步是训练和评估。评估指标不要只看准确率。如果类别不平衡准确率会有严重误导。举个例子如果负样本占九成一个永远预测负类的模型准确率就是 90%但它毫无用处。这时候要看召回率、精确率或者它们的组合指标。第七步是错误分析。把预测错的样本挑出来逐条看找规律。是某一类样本普遍错是特征缺失严重的样本错得多是边界附近的样本错这个动作往往能直接指出下一步该做什么比盲目调参高效十倍。3.3 练习三特征工程与树模型的手感这一项练习的核心目的是建立“模型复杂度与数据量匹配”的直觉。做法是同一条数据流水线分别跑一个线性模型、一个树模型、一个集成模型把结果和训练耗时列成表。模型类型训练耗时验证集表现可解释性适合的场景线性模型秒级中等强系数直接可读特征与目标近似线性、要求可解释单棵决策树秒级偏弱易过拟合强规则可画出来需要输出决策路径集成树模型十秒级通常最好中等需要额外工具解释表格数据的主力神经网络分钟级视数据量而定弱高维、非结构化数据这张表里的数字因数据而异但相对关系大体稳定。做完这个对比你会得到一个非常实用的结论表格类数据上集成树模型通常是性价比最高的选择。这不是因为它先进而是因为它在小数据上不容易崩、对特征尺度不敏感、调参空间相对小。新手最容易犯的错是一上来就上神经网络结果在一个几千行的表格数据上被一个简单的树模型按在地上摩擦然后怀疑人生。这一项练习里还有一个必须做的动作看特征重要性。把模型认为最重要的前十列列出来然后问自己“这合理吗”。如果排第一的是一个你完全没想到的列那要么你发现了新大陆要么数据有问题。两种可能都值得深挖。我遇到过一次重要性最高的一列是某个内部编号一查发现这个编号是按时间顺序生成的本质上泄漏了时间信息。这种发现只有在你认真看重要性的时候才会浮现。3.4 练习四第一个神经网络重点是学会看曲线这一项不求效果只求把训练过程可视化出来学会读曲线。搭一个三层的小网络输入维度按你的数据来中间层别太大输出层按任务类型选。训练时把训练损失和验证损失都记下来画在同一张图上。曲线的形态基本上就那么几种认全了你就有了诊断能力。两条线一起下降然后一起走平这是最健康的形态说明模型容量和数据匹配可以停了。训练损失一直降验证损失先降后升标准过拟合这时候该加正则、加数据、或者减容量。两条线都很高且降不下去欠拟合模型太简单或者特征没信息量该加容量或者重新做特征。训练损失剧烈震荡学习率太大或者批大小太小。验证损失从一开始就比训练损失高很多且差距稳定这通常不是过拟合而是训练集和验证集分布不一致该回头查切分。# 记录训练过程的骨架重点是每轮都同时记录两个损失 history {train: [], val: []} for epoch in range(epochs): train_loss run_one_epoch(model, train_data, trainingTrue) val_loss run_one_epoch(model, val_data, trainingFalse) history[train].append(train_loss) history[val].append(val_loss) if epoch % 5 0: print(epoch, round(train_loss, 4), round(val_loss, 4))这里有个很多人忽略的细节验证阶段一定要关掉训练模式。有些层在训练和推理时的行为不一样忘了切换会导致验证指标莫名其妙地差。这个问题极其常见而且报错信息往往不指向真正的原因排查起来很折磨。我的习惯是在写训练循环的时候第一行就把模式切换写死形成肌肉记忆。3.5 练习五把模型包起来跑一次推理最后这项练习很多人跳过但它恰恰是从“会做练习”到“能做东西”的分水岭。做法是把训练好的模型保存下来写一个函数输入是一条原始格式的记录输出是预测结果。中间要包含完整的预处理链路缺失填充、特征构造、尺度变换一步都不能少。最容易出错的地方就在这儿。训练时你用了某个统计量做填充推理时如果重新算一遍结果就和训练时不一致了。所以预处理的所有参数必须和模型一起保存推理时直接加载。这个原则说起来简单但真做的时候很多人会把预处理写成散落在各处的代码训练脚本里一份、推理脚本里又抄一份抄的时候漏了一行线上线下表现就差了十几个点。我的建议是把手写预处理器当成练习的一部分写一个类fit 的时候记下所有需要的统计量transform 的时候用记下的值然后把整个对象和模型一起存。做完这一步你会对“模型”这个词有全新的理解模型不是那堆权重模型是权重加上它依赖的全部预处理逻辑。少了任何一环它就不成立。4. 实操记录从原始数据到可用模型的一次完整走查4.1 任务定义和基线拿一个我做过的练习当例子细节做了匿名化处理。任务是根据若干条结构化记录预测一个二值结果正样本占比大约两成。原始数据几千行二十来列混合了数值、类别和日期。第一步我做的不是建模是写清楚任务定义预测目标是什么、预测结果会被怎么使用、哪类错误代价更高。这一步决定了后面所有指标的选择。这个任务里漏掉正样本的代价明显高于误报所以主指标我选召回率同时用精确率做约束不允许为了召回把精确率压得太低。这个判断如果一开始不做后面就会陷入“指标涨了但不知道算不算好”的困境。基线我用两条一条是“全部预测为负”准确率直接是 80%召回率 0另一条是单变量规则用业务上最相关的那个特征卡一个阈值。这两条基线给出了一上一下两个参照后面所有模型都要和它们比。特别是第一条它提醒你准确率这个指标在这个数据上有多不可信。4.2 数据清洗与特征构造清洗阶段发现的问题按处理成本排序有三个数值列存在缺失缺失比例从百分之几到两成不等有一个类别列的取值有大小写不一致和首尾空格有一个日期列格式混了两种。处理方式分别是缺失比例低的两列用中位数填比例高的那列额外加一个“是否缺失”的指示列因为缺失本身可能就是信息类别列统一小写并去空格后做编码日期列统一解析后再拆成年、月、日、星期几个数值特征。特征构造上做了三个尝试。一是把两个相关的数值列做比值直觉上这个比值比原值更能反映状态。二是把日期拆出来的“星期”保留因为业务上不同日期的行为确实有差异。三是对几个长尾分布的数值列做了对数变换让分布更接近对称。这三个尝试里前两个有正向效果第三个效果不明显我保留了对数变换但没有把它当成关键改进。这里有个经验值得说构造特征要有假设不能瞎试。每一个新特征你都要能说出“我认为它有用是因为……”。说不出来的大概率是噪音。我曾经一次性加了十几个特征效果反而下降后来一个个拆开看发现有用的只有三个其余的全在增加噪声维度。4.3 训练、评估与错误分析模型选了集成树参数先用一组保守的默认值跑通然后逐个调整。评估时除了主指标我还看了一条曲线不同判定阈值下的召回率和精确率变化。这条曲线比单个数字有用得多因为它告诉你这个模型有多少调整空间以及在不同业务要求下该把阈值放在哪里。错误分析的结果很有意思错得最多的样本集中在某一类记录上这类记录的某个关键字段大量缺失。这解释得通信息不全自然判断不准。于是我给这类样本单独加了一个标记特征让模型知道“这条记录属于信息不全的那一类”。加完之后整体指标提升不多但那一类的召回明显改善。这个改进不是靠调参调出来的是靠一条条看错样本看出来的。4.4 参数选择与结果记录我把主要实验记成了一张表方便回溯。这张表的价值在于一周后你回头看能立刻知道哪个方向试过了、哪个方向没试过。实验编号关键改动验证集召回验证集精确率结论E01基线全预测为负0.00无法计算参照E02单变量规则0.420.61参照E03默认参数模型0.680.55明显好于基线E04加缺失指示特征0.710.57有效保留E05加比值特征0.740.58有效保留E06树深度加大0.760.49召回涨但精确率掉太多回退E07调阈值0.710.63业务上更均衡采用从这张表能看出一个很典型的规律召回和精确率此消彼长模型能力提升和阈值调整是两件不同的事。E06 和 E07 的区别很多人分不清前者是模型本身变了后者只是判断标准变了。分不清这两件事调参就会变成一团乱麻。5. 常见问题速查练习中最容易踩的坑5.1 指标异常速查表现象最可能的原因优先检查的地方准确率接近类别占比模型退化成常量预测类别是否极度不平衡、损失函数是否合适验证指标远高于训练数据泄漏或切分错误预处理是否用了全量数据、是否重复样本跨集指标剧烈波动数据量太小或批太小增大验证集、固定随机种子训练损失不降学习率、特征尺度、标签格式先把学习率调小十倍试一次线上线下差异大预处理链路不一致逐个环节比对两边的中间输出某类样本全错该类样本过少或特征缺失严重单独统计该类的样本量和缺失率这张表我建议贴在显示器边上。遇到异常先按表查能省掉大量瞎猜。其中最值得展开的是“线上线下差异大”这一条因为它的排查成本最高。我的排查方法是拿一条具体的记录把训练脚本和推理脚本的中间结果都打出来从原始输入开始逐环节比对第一个出现差异的地方就是问题所在。这个方法笨但一次就能定位。5.2 过拟合和欠拟合怎么判断别凭感觉判断这件事有个非常朴素的量化办法看训练集表现和验证集表现的差距以及两者各自的绝对水平。训练好、验证差很多是过拟合方向是增加约束加正则、减特征、减容量、加数据。训练就差、验证也差是欠拟合方向是增加能力加特征、加容量、减约束。训练好、验证也好那就是成了别再折腾。训练一般、验证反而略好这种情况多半是验证集太小带来的随机波动不要据此做决策。还有一个容易被忽略的状态训练和验证都很高但业务上不认可。这不是模型问题是任务定义或者指标选择的问题。可能是你的数据分布和实际使用场景不一致也可能是你优化的指标和真正关心的目标有偏差。这种情况必须回到第一步重新想清楚继续调参是浪费时间。5.3 那些让你自欺欺人的细节练习阶段最危险的不是技术难点是那些让你“看起来做对了”的细节。我列几个自己踩过的。反复用测试集做选择。只要你在测试集上试了第二个方案测试集就已经变成验证集了它给出的数字不再是泛化能力的估计。我的做法是把测试集物理隔离开写在单独的脚本里只在全部定稿后跑一次跑完把结果抄进记录不再改。随机种子只跑一次。小数据上的结果对切分极其敏感种子换一下指标可能差好几个点。如果两个方案差距小于这个波动范围那这个差距很可能是噪声。我现在的做法是关键结论至少跑三到五个种子看均值和波动范围。只看平均不看分布。一个 0.75 的召回率可能是所有类别都 0.75也可能是大部分类别 0.9、某一类 0.1。这两种情况的后续动作完全不同。按类别拆开看是错误分析的第一步。忽略数据的时效性。练习数据往往是某个时间点的快照而真实使用是持续进行的。如果你的练习数据跨度很长一定要检查不同时间段的数据分布有没有漂移否则你练出来的模型只对某一段历史有效。6. 练习怎么沉淀成能力6.1 实验日志写给自己看的那种我从做第三个练习开始养成了写实验日志的习惯格式极其简单就四行今天改了什么、预期会发生什么、实际发生了什么、下一步打算做什么。不写长篇大论不写心得体会就这四行。这个习惯的价值在两个月后才显现出来。当时我遇到一个指标卡住的问题翻日志发现同样的现象在六周前出现过当时的结论是某个特征构造引入了噪声。如果没有日志我大概率会把同样的坑再踩一遍。机器学习练习的周期长、变量多人的记忆靠不住尤其是“我上次好像试过”这种感觉十次里有八次是错的。日志还有个附加作用它强迫你在动手之前先写下预期。写下预期这个动作本身就把“随便试试”变成了“验证假设”。这两者的效率差距非常大前者跑一百次也积累不了什么后者跑十次就能形成清晰的认知地图。6.2 复盘和重写同一道题做第二遍我的经验是一个练习做完之后隔两周再重写一遍收获往往比第一次更大。第一次做的时候你在解决“怎么让它跑起来”第二次做的时候你才有余力思考“为什么这么写”。重写时我会刻意做几件事把第一次绕过的环节补上、把当时不理解但抄下来的代码换一种写法、把整个流程压缩到更少的代码行数。压缩代码这个动作特别能暴露问题。当你试图把两百行压到八十行时你会被迫区分哪些是必要的、哪些是历史遗留的。我有一次数仓代码从一百五十行压到六十行删掉的全是早期调试留下的、从来没被用到的分支。这个过程也让我对自己的代码有了更清楚的认知很多复杂度是我自己制造的不是问题本身带来的。最后说一句我自己在实际操作中的体会。机器学习练习这件事最难的从来不是算法本身算法都在书上写着开源实现随手就能拿到。难的是那些没有写进书里的东西数据该信到什么程度、指标该怎么选、模型到什么状态就该停、什么时候该推翻重来。这些东西只能靠一次次完整的、带瑕疵的、必须自己拿主意的练习慢慢磨出来。所以别追求练习的数量也别追求把每个算法都过一遍挑三五个题目每个都从头做到尾做到能给别人讲清楚每一个决策的理由那就够了。