从Adaline到LMS:零基础跑通第一个神经网络训练循环
简介:这份神经网络导论课程实验一代码面向正在学习人工神经网络基础的高校学生与自学者,围绕Adaline自适应线性神经元及LMS最小均方误差学习规则展开,帮助读者通过Matlab实践理解权重更新、误差收敛与线性分类的基本流程。压缩包共5个文件,约10KB,主要由3个Matlab脚本和2个数据文件组成:脚本承担算法实现、随机权重初始化与样本随机划分,数据文件保存训练集和测试集,便于直接加载运行并观察训练与评估结果。资源已有300人学习,适合作为课程实验参考或自学练手材料。内容覆盖Adaline结构、LMS权重更新公式、学习率选择、随机初始化对收敛的影响以及训练与测试集划分等知识点,读者可据此复现实验、对比不同参数下的误差变化,并加深对早期神经网络模型训练逻辑与泛化评估的理解。
1. 从实验 1 代码出发:Adaline 的 LMS 是神经网络最朴素的一次“学习”
打开神经网络导论课程的第一份实验代码,大多数人的第一反应是懵:满屏的循环、向量和权重,跟课本上那些画着连线的示意图根本对不上号。这份实验 1 的代码要做的事其实很朴素——让你亲手实现一个 Adaline 神经元,用 Adaline 的 LMS 学习规则,让它在几十轮迭代里自己把权重调对。它解决的正是新手最大的卡点:知道神经网络会“学习”,却不知道“学习”这一步到底改了哪个数字。适合刚学完一点线性代数、会写几行 Python、想真正跑通第一个神经网络的零基础同学。学完你拿到的不只是一份能跑的代码,而是一条之后可以反复复用的训练循环骨架。
2. 先弄懂一个概念:Adaline 的 LMS 到底在算什么
零基础学神经网络最容易掉的坑,是一上来就去啃“反向传播”“梯度消失”这些词,结果连一个神经元里发生了什么都没搞清楚。这一章不碰多层网络,只把 Adaline 这一个模型扒开看,看完你会发现它简单到有点可爱——本质就是一次加权求和,加一条用来修正权重的规则。
2.1 一个神经元,本质是一次加权投票
想象你要判断一家餐厅好不好吃。你问了三个朋友,甲说好吃,乙说一般,丙说难吃。但你对这三个人的信任程度不一样:甲的口味跟你最像,权重给 0.6;乙只给 0.3;丙给 0.1。最后你算了个综合分:0.6×甲 + 0.3×乙 + 0.1×丙。这个综合分超过某个门槛,你就去,否则不去。
Adaline 干的正是这件事。它有 n 个输入 x₁ 到 xₙ,n 个对应的权重 w₁ 到 wₙ,把输入和权重一一相乘再全部加起来:
z = w₁x₁ + w₂x₂ + ... + wₙxₙ + b这里的 b 叫偏置,你可以理解成“这个神经元天生乐观还是悲观”——它不随输入变化,是一个固定的加分项或减分项。在代码里,这个式子就是一行点积np.dot(w, x),没有任何神秘的地方。
提示:神经网络导论课上画的那些圆圈和箭头,箭头代表权重,圆圈代表求和,一个圆圈就是一个这样的加权求和单元。你以后看到再复杂的网络图,单个节点做的还是这一件事。
2.2 和感知机的差别:一条线拉直了
很多人第一次学到这里会问:这不就是感知机吗?差别就在输出那一步。
感知机算完加权和后,立刻套一个阶跃函数:大于 0 输出 +1,小于 0 输出 −1。然后拿这个 ±1 去跟正确答案比,错了才更新权重,对了就完全不动。Adaline 不一样,它算完加权和之后不套函数,直接把这个连续值 z 拿去和真实目标 y 比较,用两者的差去更新权重,分类只是训练完之后顺手做的一件事(看 z 的正负号)。
这个差别带来的后果很实际:感知机只要分对了就收工,哪怕这个样本只是勉强压线过关;Adaline 会继续把 z 往目标值上拉,让“分对”变成“分得又对又稳”。所以同样的数据、同样的学习率,Adaline 的收敛过程通常更平滑,遇到噪声也不至于因为一个样本就把权重拽飞。
用一张表对比会更清楚:
| 对比项 | 感知机 | Adaline |
|---|---|---|
| 输出 | 阶跃后的 ±1 | 未激活的连续值 z |
| 更新依据 | 分类对错 | 连续误差 y − z |
| 分对之后 | 停止更新 | 继续缩小误差 |
| 学习规则 | 感知机规则 | LMS(最小均方)规则 |
记住这张表的第三行,它是新手理解两种模型时最容易忽略的一点。
2.3 LMS 在最小化什么:误差平方和,加上一句梯度下降
LMS 的全称是 Least Mean Squares,最小均方。名字里的“均方”指的就是它想压小的东西——预测值和真实值之差的平方。把所有样本的误差平方加起来,再除以 2(这个 1/2 纯粹是为了求导时把平方的 2 约掉,不影响结果),就得到:
J(w) = 1/2 × Σ (yᵢ − zᵢ)²训练的目标就是找到一组 w,让 J(w) 尽量小。怎么找?沿着让 J 下降最快的方向挪一小步,这就是梯度下降。对 J 求关于 w 的偏导,会得到一个非常干净的式子:
∂J/∂w = −Σ (yᵢ − zᵢ) xᵢ负号表示我们要往梯度的反方向走,所以权重更新写成:
w ← w + η × (y − z) × x四个符号挨个解释:η 是学习率,决定每次挪多大一步;y 是真实目标;z 是当前预测;x 是这次的输入向量。整个式子的意思就是——预测低了就顺着输入的方向把权重调大一点,预测高了就往反方向调一点,调多少由误差大小和学习率共同决定。
我第一次推这个式子的时候卡了很久,后来发现卡点根本不在求导,而在于没想明白“为什么更新量要乘 x”。你换个角度想:如果某个输入 xᵢ 这次是 0,那它对结果没贡献,自然也不该背锅,乘上 xᵢ 之后它的权重就纹丝不动;输入越大,责任越大,改得越多。这么一看就顺了。
2.4 入门第一课选它,不是因为它简单
有人会觉得,都学神经网络了还搞这么个线性模型,是不是太小儿科。恰恰相反,Adaline 是唯一一个你能把梯度自己手推一遍、并且每一步都能对上代码的模型。
它身上有三个特点,让它成为最好的起点。第一,没有隐藏层,没有链式法则,误差从输出到权重的路径只有一步,你不需要把反向传播当成黑匣子先背下来;第二,权重可解释,训练完的 w 里每个数字对应一个特征的重要程度,正负号代表方向,你能拿它跟业务常识对一对,对不对一目了然;第三,它的训练循环骨架——算预测、算误差、算梯度、更新权重——和后面几十层的网络是同一个骨架,只是梯度那一步换成了更复杂的算法。
说句血泪经验:直接去啃多层网络的反向传播,绝大多数零基础的人会在第三天放弃,因为你不知道那些偏导符号在算什么。先在 Adaline 上把“误差驱动权重”这件事跑通十遍,再去学多层,你会发现新东西只是量变。
3. 跑通第一个例子:30 行代码让 Adaline 学会与运算
概念讲完就该动手了。这一章的目标很具体:不装任何深度学习框架,只用 Python 和 numpy,写出一个能自己学会“与运算”的 Adaline,并且能看懂它每一步在干什么。整个过程 30 行左右,跑完你会有一种“原来就这么点东西”的踏实感。
3.1 开跑之前:环境、目录和一份最小数据
需要的东西少得可怜:Python 3.8 以上,加一个 numpy。打开终端执行下面这行,看到版本号打印出来就算齐活。
# 只需要 numpy,不需要 torch / tensorflow pip install numpy python -c "import numpy; print(numpy.__version__)"如果你不想折腾环境,用任何一个自带 numpy 的在线笔记本环境也一样能跑,代码完全一致。目录建议这样组织,图个清爽:
exp1_adaline/ ├── adaline.py # 模型主体 ├── data.py # 数据构造 └── run.py # 跑起来看结果数据我们手工造,四个样本,两个特征。这里用±1 编码而不是常见的 0/1,原因稍后 3.3 节会说,这个细节是新手最容易忽略、又最容易导致“代码看着没错但结果不对”的地方。目标值是“与运算”:只有两个输入都是 +1 时输出 +1,其余三个样本都是 −1。
3.2 第一版完整代码:从初始化到收敛
把下面这段存成run.py,直接运行。
import numpy as np # 双极编码的四个样本,第 0 列固定为 1,充当偏置项的乘数 X = np.array([[1, 1, 1], [1, 1, -1], [1, -1, 1], [1, -1, -1]], dtype=float) # 与运算目标:只有 (1, 1) 输出 +1,其余三个都是 -1 y = np.array([1, -1, -1, -1], dtype=float) eta = 0.1 # 学习率 w = np.zeros(3) # 权重从 0 开始 max_epochs = 30 # 最多扫 30 轮,防止死循环 for epoch in range(max_epochs): wrong = 0 # 这一轮分错几个 sse = 0.0 # 这一轮的误差平方和 for xi, yi in zip(X, y): z = w @ xi # 加权求和,注意这里没有套激活函数 err = yi - z # 真实值减预测值,负数说明预测高了 w = w + eta * err * xi # LMS 更新,一行就是全部的核心 sse += err ** 2 if (z > 0) != (yi > 0): # 用符号判断分类是否出错 wrong += 1 print(f"第 {epoch:2d} 轮 误差平方和 {sse:7.4f} 分错 {wrong} 个") if wrong == 0: # 全分对就提前收工 print(f"第 {epoch} 轮起已经全部分对,停止训练") break print("学到的权重:", np.round(w, 3)) for xi, yi in zip(X, y): z = w @ xi print(f"输入 {xi[1:]} 线性输出 {z:+.3f} 分类 {1 if z > 0 else -1} 目标 {int(yi)}")跑起来你会看到类似这样的输出(数值会随环境有些微差异,趋势一致):
第 0 轮 误差平方和 1.7081 分错 2 个 第 1 轮 误差平方和 0.5205 分错 0 个 第 1 轮起已经全部分对,停止训练 学到的权重: [-0.198 0.178 0.156]第 0 轮结束后还有两个样本分错,第 1 轮就全对了。这不是巧合,也不是代码写错了——四个样本在二维平面上是线性可分的,Adaline 只需要把那条分界线推到正确位置就够。停下来的时候权重还没收敛到最终解,只是“刚好够用”,这个问题 5.4 节会专门讲。
3.3 逐行拆开:训练循环里其实只有四个动作
把上面那段循环扒掉所有装饰,剩下的就是四件事,这四件事构成了所有神经网络训练的核心:
- 算预测:
z = w @ xi,一行点积。 - 算误差:
err = yi - z,方向很重要,反了就变成梯度上升了。 - 改权重:
w = w + eta * err * xi,误差乘以输入再乘以学习率。 - 看情况停:全分对就 break,否则继续下一轮。
第 3 步是整份实验代码里唯一真正属于“学习”的部分,其余都是准备工作。你可以试着把这一行注释掉,程序会原地打转 30 轮,权重永远是全 0——这就是“不学习”的网络长什么样。
现在说回编码的问题。为什么用 ±1 而不是 0/1?敲下面几行试试就明白了:
import numpy as np # 假设某个样本是 (0, 0),用 0/1 编码,偏置列固定为 1 xi = np.array([1.0, 0.0, 0.0]) err = 1.0 # 假设当前预测是 0,目标 1 print(0.1 * err * xi) # 输出 [0.1 0. 0. ]结果是只有偏置那一维被推动了,两个特征的权重一动不动。因为它们的输入是 0,乘出来就是 0。在 0/1 编码下,一个样本“没有某个特征”和“某个特征的权重不该改”是无法区分的,训练会变得非常拖沓。换成 ±1 编码后,−1 也能贡献更新量,每个维度每一轮都在参与调整,收敛快得多。这是入门实验里最常见的一个隐性坑,很多人抄完代码跑不收敛,问题就出在编码上。
3.4 结果怎么看:三个必须打印的东西
跑通不算完,能读懂输出才算真的懂了。每次跑完,至少要看这三样。
第一样是收敛轮数。上面第 1 轮就停了,说明数据太简单。如果你的实验数据收敛得要几十轮,也正常,跟学习率、初始化和数据尺度都有关系。
第二样是最终权重。你会看到第一个数(偏置项)是负的,后两个是正的。这完全符合逻辑:偏置为负意味着这个神经元默认倾向输出 −1,要推翻它,必须两个特征同时给出正贡献,把 z 顶到 0 以上。这正好就是“与运算”的语义。这就是线性模型的可解释性——权重不是随机数,它讲了一个故事。
第三样是每个样本的线性输出 z。注意不只是看符号,还要看绝对值。z = +0.5 和 z = +5.0 符号相同、分类相同,但后者离分界线远得多,遇到新样本时更不容易翻车。这种“离边界有多远”的量,后面会变成支持向量机里的间隔概念,现在先有个直觉就行。
注意:如果某个样本分对了但 z 只有 0.001,别高兴太早,这说明它正好踩在分界线上,稍微换个输入就会错。真正的收敛应该让每个样本都有一定的余量。
3.5 动手改一改:三个小实验验证你真的懂了
看懂和会用之间隔着一段距离,最快的跨法是动手改参数然后预测结果,再看实际是不是这样。
实验一,改学习率。把eta改成 0.001 跑一遍,再改成 5.0 跑一遍。前者你会看到收敛变慢、需要更多轮;后者很可能直接震荡,误差平方和越跑越大。这条经验以后调任何模型都用得上。
实验二,改初始化。把w = np.zeros(3)换成w = np.array([0.5, -0.3, 0.8]),重跑。你会发现收敛轮数变了,但最终仍然能分对。这就是线性可分数据的好处:从这个角度看,初始化的影响没那么玄学。
实验三,改成或运算。把 y 改成[1, 1, 1, -1](只要有一个输入是 +1 就输出 +1),重跑,看看权重变成什么样。你会发现偏置变成了正数,两个特征权重仍然是正的——语义又对上了。
三个实验加起来不到十分钟,但对“权重在表达什么”这件事的理解,比看十页教材都实在。
4. 从玩具数据到像样的任务:加上标准化和批量更新
四个样本的与运算跑通,只说明你会写循环了。真实一点的数据马上会给你上课:特征尺度不一样、目标值是连续的、样本几百上千个,逐个更新的循环慢得让人想砸键盘。这一章解决这三个问题,改完之后你手上这套代码就能处理正经的回归任务了。
4.1 为什么输入尺度一变就翻车
先看一个现象。假设你要预测房价,两个特征:房间数(1 到 5)和面积(30 到 300 平方米)。量级差了 60 倍。这时候学习率设成 0.1,房间数那维的更新量小得可以忽略,面积那维一更新就把权重甩飞,训练过程要么慢如蜗牛,要么直接发散成 NaN。
这不是代码写错了,是梯度的形状被尺度带偏了。你可以把损失函数想象成一口锅,尺度一致的时候锅是圆的,梯度直直地指向锅底;尺度差太多的时候锅被拉成了细长的椭圆,梯度方向斜得厉害,走一步就冲到对面山壁上,来回震荡。
血泪经验在这里:我见过太多人(包括我自己)在实验报告里写“模型不收敛”,回头一查,数据压根没做任何预处理。神经网络导论的实验里,标准化这一步经常被一句话带过,但它是性价比最高的一个改动。
4.2 加一步标准化:三行代码,效果立竿见影
标准化的做法很朴素:对每个特征,减去均值、除以标准差,把它压成均值 0、方差 1 的分布。
import numpy as np rng = np.random.default_rng(42) # 固定随机种子,保证每次跑结果一样 n = 200 X_raw = rng.uniform(0, 1, size=(n, 2)) X_raw[:, 1] = X_raw[:, 1] * 500 # 故意让第二个特征量级大 500 倍 # 真实关系:y 主要受第一个特征影响,第二个特征影响很小 true_w = np.array([3.0, -0.02]) y = X_raw @ true_w + 1.0 + rng.normal(0, 0.1, size=n) # 关键的三行:按列求均值、标准差,然后整体缩放 mu = X_raw.mean(axis=0) sigma = X_raw.std(axis=0) X_std = (X_raw - mu) / sigma # 在最左边补一列 1,作为偏置项的乘数,这样后面不用单独处理 b X = np.hstack([np.ones((n, 1)), X_std]) print("标准化后每列的均值:", np.round(X[:, 1:].mean(axis=0), 6)) print("标准化后每列的标准差:", np.round(X[:, 1:].std(axis=0), 6))axis=0表示沿着样本方向聚合,也就是“对每一列单独算”。这个参数新手经常写错成axis=1,结果算出来的是每个样本自己的均值,标准化完全失效——这也是个高频翻车点。
np.hstack那一行值得多说一句。它把一列全 1 拼到特征矩阵最左边,这样偏置 b 就变成了权重向量的第 0 个分量,更新公式里不用再单独写一项。这是工程上的小技巧,几乎所有入门代码都这么干,看到的时候不要懵。
提示:如果以后要划分训练集和测试集,标准化必须用训练集算出来的 mu 和 sigma 去处理测试集,不能各算各的。否则测试集的分布信息就提前泄露了,评估结果会虚高。
4.3 换成连续目标:把 Adaline 当线性回归用
刚才那段数据的目标 y 是连续值,不再是 ±1。这正是 Adaline 擅长的场景——它本来输出的就是连续值,做分类只是顺手。把训练循环改一改,损失换成均方误差:
w = np.zeros(X.shape[1]) # 权重个数等于特征数(含偏置列) eta = 0.1 for epoch in range(200): z = X @ w # 一次算出全部 200 个样本的预测,形状 (200,) err = y - z # 残差向量 w = w + eta * (X.T @ err) / n # 批量 LMS:把所有样本的更新量取平均 if epoch % 50 == 0: mse = np.mean(err ** 2) print(f"第 {epoch:3d} 轮 均方误差 {mse:.6f}") print("学到的权重:", np.round(w, 3))跑完你会看到均方误差快速下降然后趋于平缓。学到的权重里,第一个是偏置,后两个对应两个特征——因为做了标准化,它们不再直接对应原始量纲,但可以横向比较重要性:第一个特征的权重绝对值明显大于第二个,跟造数据时设定的 3.0 和 −0.02 是对得上的。
这三行代码里有两处值得停下来想。第一处是X @ w,把原来逐个样本的循环换成了矩阵乘法,200 个样本一次算完,速度快几十倍,这叫向量化,是 numpy 最值得早点养成的习惯。第二处是X.T @ err / n,它其实就是把每个样本的err * xi加起来再取平均——和 3.2 节那个一行更新在数学上是同一件事,只是从“逐个别”变成了“一起算”。看懂这个等价关系,你就跨过了从教学代码到工程代码的那道坎。
4.4 调参的先后顺序:先学习率,再初始化,最后轮数
新手调参最常见的做法是三个参数一起瞎试,试到能跑就收工。这样即便跑通了,你也不知道是谁的功劳。有个更靠谱的顺序:
第一步只动学习率。固定初始化和轮数,让 eta 从 0.001、0.01、0.1、1.0 各跑一次,记录每轮的损失。判断标准很简单:损失稳步下降且没有剧烈抖动,就是合适;损失上下横跳甚至变大,说明太大;损失降得像蜗牛,说明太小。标准化之后,0.01 到 0.1 通常是个安全区间。
第二步再动初始化。学习率定下来之后,换几组初始权重看收敛轮数的波动。如果波动极大,说明数据可能接近不可分,或者学习率还是偏大。
第三步才考虑轮数。轮数不是越多越好,Adaline 是线性模型,训练太久只会在最优解附近来回微调,甚至因为学习率偏大而越过最优点。所以别一上来就设 10000 轮图安心。
5. 新手避坑:Adaline 实验里最容易翻车的 5 个地方
这一章的五条,基本都是我和身边人真真切切踩过的。每条按“现象、原因、解决”写,对照着看你自己卡在哪一步。
5.1 坑一:代码跑通了,但换个输入就全错
现象:四个训练样本分类 100% 正确,你兴冲冲拿个新样本 (1, 1) 去测,结果输出 −1,和预期相反。
原因:训练停得太早,权重还停在“勉强分对”的位置,分界线正好贴着某个样本。3.2 节那个例子第 1 轮就停了,权重只有 ±0.2 的量级,余量非常小。
解决:把停止条件从“全分对”改成“连续若干轮全分对”或者“误差平方和小于某个阈值”,比如要求连续 5 轮wrong == 0才停。另一个办法是训练时多跑几轮不停早,事后再看每个样本的 z 绝对值是否都大于 0.5。留余量这件事,在真实项目里比在实验里重要得多。
5.2 坑二:损失不降反升,几轮之后变成 nan
现象:打印出来的误差平方和一轮比一轮大,跑到后面出现nan或者inf。
原因:九成是学习率太大,配合没做标准化的数据,一次更新就把权重推到很远的地方。剩下的一成是数据里有极端值,或者标签写反了。
解决:先把 eta 除以 10 再跑一遍;如果还是发散,检查数据尺度,用 4.2 节的标准化处理。另外把每轮的损失打印出来,而不是只看最后一轮,这样你能看到它从第几轮开始变坏的,定位快得多。
5.3 坑三:权重一直是 0,训练像没发生
现象:跑了 30 轮,最后打印出来的权重还是[0. 0. 0.],或者每轮损失完全一样。
原因:几种可能——eta被写成了整数除法(比如1/100在某些写法下直接是 0)、更新写成了w = eta * err * xi而不是w = w + eta * err * xi(少了累加,每次都被覆盖)、或者xi传进去的是空数组。
解决:在循环里加一句print(w)打印第一轮前后的权重,一眼就能看出是没更新还是被覆盖。更新语句记住老规矩:带加号。
5.4 坑四:循环看着收敛了,但每次跑结果都不一样
现象:代码没改,连跑三次,权重三个样,有时候收敛有时候不收敛。
原因:初始化用了np.random.randn却没设种子,或者数据生成部分每次都在随机。
解决:在程序开头固定rng = np.random.default_rng(42),之后所有随机操作都走这个 rng,别用全局的np.random。实验代码最忌讳“结果不可复现”,写报告的时候你会感谢自己。
5.5 坑五:把回归任务当分类任务判断收敛
现象:做一个连续值预测的实验,怎么跑都“不收敛”,打印出来分错的样本数一直是 0,但均方误差很大。
原因:用if (z > 0) != (yi > 0)这种符号判断去衡量连续目标的训练效果。连续目标下几乎所有样本的符号都相同,这个指标永远是 0 分错,完全没有参考价值。
解决:分类看分错个数和间隔,回归看均方误差或平均绝对误差,两套指标别混用。判断标准跟着任务类型走,这条规律在以后所有模型上都成立。
6. 下一步学什么:从 Adaline 走到多层网络的路线图
这份实验 1 的代码跑通之后,最怕的就是停在原地,反复改学习率玩一星期,然后不知道下一步干嘛。下面这条路线是我自己走过、也推荐给别人的顺序,每一阶段都给了明确的验收标准,你照着往下一格一格跳就行。
| 阶段 | 要学的东西 | 大致投入 | 验收标准 |
|---|---|---|---|
| 一 | 把本次代码封装成Adaline类 | 半天 | 能初始化、fit、predict 三个方法跑通 |
| 二 | 感知机学习规则与 Adaline 的对比 | 半天 | 能说清两者更新时机差在哪 |
| 三 | 异或问题为什么线性模型做不了 | 一天 | 能画出两类点、说明不存在一条直线分开 |
| 四 | 多层感知机与反向传播 | 三到五天 | 能手推两层网络的梯度 |
| 五 | 用框架复现同一个实验 | 两天 | 同一份数据,框架和手写结果对得上 |
第一阶段的具体技巧值得展开讲。把你现在这堆散在脚本里的代码收成一个类:
class Adaline: def __init__(self, eta=0.01, epochs=50, seed=42): self.eta = eta # 学习率 self.epochs = epochs # 最大轮数 self.rng = np.random.default_rng(seed) def fit(self, X, y): self.w = self.rng.normal(0, 0.01, X.shape[1]) # 小随机数初始化 self.losses_ = [] for _ in range(self.epochs): err = y - X @ self.w self.w += self.eta * (X.T @ err) / len(X) self.losses_.append(float(np.mean(err ** 2))) return self def predict(self, X): return X @ self.w # 回归直接返回连续值别小看这一步。fit里把损失记到losses_里、参数全部放进__init__、fit返回self以便链式调用,这些习惯和主流框架的接口是一致的。等你后面换到框架时,会发现调用方式几乎没有区别,只是把self.w换成了框架里的参数对象。
第三阶段千万别跳过。异或问题看着像课后习题,其实是整个神经网络历史的转折点:正是因为线性模型做不了它,才有了隐藏层和反向传播。你自己动手把这个失败复现一遍,再看多层网络,就知道那些额外的东西是为了解决什么问题而存在的,不是凭空冒出来的。
第五阶段有个容易忽略的细节:用框架复现时,务必把随机种子、数据划分、标准化参数全部对齐,否则两边结果对不上,你会怀疑人生。对不上是常态,对上一次之后,你对整个流程的理解会上一个台阶。
从那以后我每次跑新实验,都强制自己先把随机种子、数据标准化、损失打印这三件事写死在脚本开头,再开始调模型——省下来的时间,比多试十个超参数加起来都多。这份实验 1 的代码就是你练习这个习惯最安全的沙盒,跑坏了重来一遍只要十分钟,希望帮到你。
本文还有配套的精品资源,点击获取