资讯详情

资讯详情

Adaline与LMS权重更新实战:损失曲线、决策边界与调参排错

简介这份代码包面向神经网络导论课程的学习者与Matlab初学者围绕Adaline自适应线性神经元及LMS学习规则展开用可运行脚本呈现课程实验一的核心内容帮助读者理解线性最小均方误差算法如何更新权重、完成训练与测试评估。压缩包共5个文件以3个.m脚本和2个.mat数据文件为主整体约10KB脚本承担算法实现、随机权重初始化与样本随机划分数据文件则保存训练集和测试集样本便于直接载入实验。已有300人学习适合希望从代码层面建立Adaline模型直观认识、补充神经网络基础实验练习的读者。内容覆盖权重更新公式、学习率选择、迭代训练、随机初始化对收敛的影响以及训练/测试集划分与泛化性能评估等知识点可作为课程实验复现、算法对照和后续类似神经元模型实现的参考素材。1. 神经网络导论课程实验1代码从一行权重更新开始把 Adaline 跑明白拿到这份神经网络导论课程实验1代码多数人第一反应是打开主脚本点运行看见 loss 曲线往下走就交差。可真到验收被问一句「Adaline的LMS和感知机到底差在哪」很多人当场卡壳。这个实验包干的事很具体用自适应线性神经元配合最小均方规则做二分类把权重更新、损失下降、决策边界这几件事串成一条能跑通的链路。它适合刚学完感知机想动手验证的同学也适合需要一份干净基线代码做对比实验的开发者——代码量不大但每个环节都值得拆开看。2. 实验包结构与运行环境从数据文件到训练入口的目录拆解2.1 目录长什么样、每个文件管什么这类课程实验包一般不会做太重但结构基本是固定的数据、模型、指标、可视化、入口脚本分开放。我见过的版本大致长这样不同人交上来的名字略有差异职责划分八九不离十。nn-lab1/ ├── data/ │ ├── iris_binary.csv # 鸢尾花两分类子集最后一列是标签 │ └── gaussian_2class.csv # 两类高斯分布生成的线性可分数据 ├── src/ │ ├── dataset.py # 读取、划分、标准化 │ ├── adaline.py # Adaline 模型 LMS 更新 │ ├── perceptron.py # 感知机对照实现 │ ├── metrics.py # 准确率、混淆矩阵 │ └── visualize.py # 损失曲线、决策边界 ├── train.py # 训练入口命令行参数在这层解析 ├── requirements.txt └── README.mdsrc/adaline.py是整份代码的核心只有几十行但它决定了实验报告里所有结论能不能站住。dataset.py负责把原始表格变成模型能吃的float64矩阵这一步做错后面调参全是玄学。train.py很薄只做参数拼装和调用方便你改学习率、迭代轮数而不用动模型代码。最容易被忽略的是metrics.py和visualize.py。二分类的准确率一行就能写完但实验报告要的是「为什么这个学习率下准确率高」——那就得靠混淆矩阵和决策边界图说话。我一般建议先把这两个文件读一遍知道最后能产出什么图再回头理解训练流程目标感会强很多。提示如果下载到的包没有data/目录先看README.md里有没有生成脚本。很多实验包为了控制体积数据是用numpy.random现场生成的跑一次dataset.py就会落到本地。2.2 环境准备与三条运行命令依赖不会多numpy、pandas、matplotlib三件套就够跑通主流程scikit-learn只在需要做数据划分或者对照实验时用得上。Python 版本建议 3.9 以上numpy用 1.24 之后的版本避免np.float64、np.int64这类类型在旧版本里行为不一致带来奇怪的报错。# 1. 建虚拟环境别把系统 Python 搞乱 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate # 2. 装依赖 pip install -r requirements.txt # 3. 跑训练入口先看默认参数下的结果 python train.py --data data/iris_binary.csv --eta 0.01 --epochs 50这三条命令跑完终端会打印每个 epoch 的损失和最终准确率同时弹出一张损失曲线图。如果图没弹出来多半是脚本里用了matplotlib.use(Agg)后端去visualize.py顶部把它注释掉或者改成保存到outputs/目录再看。--eta是学习率--epochs是遍历整个训练集的轮数。默认值通常给的是0.01和50这是标准化之后比较稳的组合。第一次跑不建议改先用默认参数确认链路通再动参数。跑不通的时候优先怀疑依赖版本和数据路径而不是模型写错了——血泪经验是九成的「跑不起来」都发生在环境层。2.3 数据从哪来、长什么样iris_binary.csv是把三分类的鸢尾花数据砍成两类只留山鸢尾和变色鸢尾取花瓣长度和花瓣宽度两个特征。为什么留这两个它们的量纲接近标准化前后的对比不会太夸张画决策边界也好看。标签列一般被映射成1和-1这一点非常关键后面讲公式时会展开。gaussian_2class.csv是两类二维高斯分布各采样若干点拼出来的均值分别放在(2, 2)和(-2, -2)协方差矩阵接近单位阵。它的价值在于「线性完全可分」用来验证模型能不能收敛到零损失而鸢尾花数据存在少量重叠用来观察误分类样本长什么样。原始表格进模型之前要走三步取特征矩阵X、取标签向量y、做标准化。标准化的写法是(X - X.mean(axis0)) / X.std(axis0)按列减均值除标准差。不标准化直接用原始特征LMS 的梯度会被数值大的特征主导学习率稍微大一点就发散这是新手翻车最集中的地方。3. Adaline 与 LMS 核心实现权重更新那几行到底在算什么3.1 模型类骨架与初始化整个模型不到四十行结构照搬了 scikit-learn 的风格构造器只存超参数fit里训练predict里推理。这样写的好处是你后面想换成sklearn的交叉验证工具接口能对得上。import numpy as np class Adaline: 自适应线性神经元使用批量梯度下降LMS / Widrow-Hoff 规则 def __init__(self, eta0.01, n_iter50, random_state42): self.eta eta # 学习率控制每次更新的步长 self.n_iter n_iter # 遍历训练集的轮数 self.random_state random_state # 随机种子保证结果可复现 def fit(self, X, y): # 用小方差随机数初始化权重避免所有神经元从同一点出发 rgen np.random.RandomState(self.random_state) self.w_ rgen.normal(loc0.0, scale0.01, sizeX.shape[1]) self.b_ np.float64(0.0) self.losses_ [] # 记录每轮损失画收敛曲线用 for _ in range(self.n_iter): output self.net_input(X) errors y - output # 批量 LMS一次性用全部样本的误差和更新权重 self.w_ self.eta * X.T.dot(errors) self.b_ self.eta * errors.sum() # 平方误差和的一半方便求导后系数为 1 loss (errors ** 2).sum() / 2.0 self.losses_.append(loss) return self def net_input(self, X): # 线性组合Adaline 的「激活函数」就是恒等映射 return np.dot(X, self.w_) self.b_ def activation(self, X): return self.net_input(X) def predict(self, X): # 用符号函数把连续输出压成类别 return np.where(self.activation(X) 0.0, 1, -1)权重初始化用normal(0, 0.01)而不是全零这是个细节。全零初始化时如果标签分布恰好对称第一轮梯度会让所有权重同步移动模型退化成只学偏置表现就是准确率长期卡在 0.5 附近。scale0.01足够小不会让初始输出过大又保留了打破对称的随机性。net_input里的线性组合就是 Adaline 与多层网络最大的区别它没有非线性激活。整流、sigmoid、tanh 在这里都不存在输出是实数可以是1.83也可以是-0.42。正因为输出连续才能定义平方误差才能求导LMS 才有意义。这一点想明白了Adaline 和感知机的分界线就清楚了。3.2 fit 方法里的批量 LMS 更新核心就三行算输出、算误差、按误差和更新。写成公式是w : w η · Xᵀ · (y − Xw − b) b : b η · Σ(yᵢ − zᵢ)推导不复杂损失函数取J(w) ½ Σ(yᵢ − zᵢ)²对w求偏导得到−Σ(yᵢ − zᵢ)xᵢ沿负梯度方向走一步就是上面的更新式。系数½的作用是求导时把平方的2约掉让表达式干净对最终结果没有影响但代码里得和公式保持一致否则学习率的量级会差一倍。X.T.dot(errors)这一步是把每个样本的误差按特征维度加权求和得到的是一个和w同形状的向量。假设X是(n_samples, n_features)errors是(n_samples,)那么X.T是(n_features, n_samples)点乘后得到(n_features,)正好能直接加到w_上。这种批量写法比逐个样本循环快一个数量级也让更新方向更稳定。批量更新和随机更新的差别值得说一句。批量 LMS 每轮只更新一次梯度方向由全部样本决定损失曲线平滑单调随机 LMS 每看一个样本就更新一次收敛快但曲线抖。课程实验一般用批量因为曲线上行下行一眼能看出来写报告好解释。真要上生产随机或者小批量更常见。偏置的更新是errors.sum()本质是把偏置当成一个输入恒为 1 的权重。有些实现会把1拼到X后面变成增广矩阵代码更短但可读性差一点。两种写法等价选一种坚持用就行。3.3 预测与决策边界为什么用 ±1 而不是 0/1predict用np.where(activation 0, 1, -1)标签空间是{1, -1}。这不是随便定的。LMS 的误差项是y − z如果标签用0/1那么当模型输出z 0.5、真实标签为0时误差是-0.5而真实标签为1时误差是0.5。两类的误差不对称梯度会把决策边界往一边推收敛后的边界位置会偏。换成±1之后两类误差关于零点对称模型学到的超平面正好落在两类中间。这也是为什么dataset.py里要做一次标签映射把原始的0/1或者字符串标签转成±1。很多人跑出来准确率不低但决策边界明显偏回头查就是这一步没做。决策边界本身由wᵀx b 0定义在二维平面上是一条直线。w是法向量方向垂直于直线b决定直线到原点的距离。可视化的时候把网格点喂给predict得到类别后填色就能看到两个色块和中间那条线。标准化之后w的两个分量大小可以直接比较哪个特征权重大直线就更垂直于哪个轴这是实验报告里可以写的一段分析。4. 训练过程可视化与参数对比学习率、标准化和收敛曲线怎么调4.1 损失曲线与学习率扫描单次训练的损失曲线只能说明「收敛了没有」看不出学习率选得好不好。要做对比就把同一份数据、同一个随机种子下不同eta的曲线画在一张图上纵轴取对数。import numpy as np import matplotlib.pyplot as plt from src.adaline import Adaline # X_std、y 是已经标准化并映射到 ±1 的数据 etas [0.0001, 0.001, 0.01, 0.1] plt.figure(figsize(8, 5)) for eta in etas: model Adaline(etaeta, n_iter30, random_state1) model.fit(X_std, y) plt.plot(range(1, len(model.losses_) 1), model.losses_, labelfeta{eta}) plt.yscale(log) # 对数纵轴小学习率的缓慢下降才看得清 plt.xlabel(epoch) plt.ylabel(SSE / 2 (log scale)) plt.legend() plt.tight_layout() plt.savefig(outputs/loss_vs_eta.png, dpi150)纵轴用对数是因为不同学习率下的损失能差好几个数量级线性坐标下小学习率的曲线会贴在地上看不出形状。跑完这张图通常能看到三种典型形态eta0.0001的曲线缓慢下降但 30 轮远没到底说明步子太小eta0.01的曲线快速下降后走平是理想状态eta0.1可能在前几轮就冲到nan或者来回震荡说明步子太大越过了谷底。nan的出现机制值得单独说。批量 LMS 在标准化后的数据上损失函数的等高线接近圆形稳定学习率的上界大约在1/λ_max量级λ_max是XᵀX的最大特征值。特征维度越高、样本越多这个上界越小。不标准化时不同特征方差差几十倍λ_max被最大的那个特征撑大上界被压到很小所以「不标准化 默认学习率」是发散的高发组合。注意损失曲线出现nan不要第一反应去改代码先打印前三个 epoch 的losses_确认是第几轮炸的。如果第一轮就是inf问题在数据有nan或者量纲离谱如果是第三四轮才炸就是学习率过大。4.2 标准化前后决策边界的差别决策边界图是实验报告里最有说服力的一张。做法是在特征空间铺网格逐点预测用contourf填色再把真实样本散点叠上去。import numpy as np import matplotlib.pyplot as plt def plot_decision_regions(X, y, clf, resolution0.02, axNone): markers (o, s) colors (#1f77b4, #d62728) ax ax or plt.gca() x1_min, x1_max X[:, 0].min() - 1, X[:, 0].max() 1 x2_min, x2_max X[:, 1].min() - 1, X[:, 1].max() 1 xx1, xx2 np.meshgrid( np.arange(x1_min, x1_max, resolution), np.arange(x2_min, x2_max, resolution)) # 网格点拼成 (n_points, 2) 后一次性预测比双重循环快得多 grid np.array([xx1.ravel(), xx2.ravel()]).T Z clf.predict(grid).reshape(xx1.shape) ax.contourf(xx1, xx2, Z, alpha0.25, colorscolors) for idx, cl in enumerate(np.unique(y)): ax.scatter(X[y cl, 0], X[y cl, 1], ccolors[idx], markermarkers[idx], edgecolork, labelfclass {cl}) ax.set_xlabel(feature 1) ax.set_ylabel(feature 2) ax.legend(locupper left) return axresolution控制网格密度0.02画出来的边界已经够平滑再小会让predict的调用量翻倍画图时间从一秒变十秒没必要。alpha0.25让填色半透明散点不会被盖住。把标准化前后的两次训练结果并排画差别会非常直观。未标准化时两个特征量纲差几倍w会被大量纲特征主导边界几乎垂直于那个轴另一侧的空隙被浪费标准化之后边界落在两类中间间隔更均匀。这也是为什么标准化被叫做「免费的午餐」——不改模型、不加数据只改预处理就能提点。4.3 和感知机放一起对比的实验脚本课程实验一般要求把 Adaline 和感知机放一起比。感知机的更新规则是w η · Xᵀ · (y − ŷ)注意误差用的是符号化之后的预测ŷ只有分错才更新分对了误差为零。这个差别导致感知机在数据线性不可分时永远不收敛权重会一直震荡。class Perceptron: 最朴素的感知机用于和 Adaline 对照 def __init__(self, eta0.01, n_iter50, random_state42): self.eta eta self.n_iter n_iter self.random_state random_state def fit(self, X, y): rgen np.random.RandomState(self.random_state) self.w_ rgen.normal(0.0, 0.01, X.shape[1]) self.b_ 0.0 self.errors_ [] # 记录每轮误分类数 for _ in range(self.n_iter): errors 0 for xi, target in zip(X, y): # 逐样本更新误差用 sign 后的预测 update self.eta * (target - self.predict_one(xi)) self.w_ update * xi self.b_ update errors int(update ! 0.0) self.errors_.append(errors) return self def predict_one(self, x): return 1 if np.dot(x, self.w_) self.b_ 0 else -1 def predict(self, X): return np.array([self.predict_one(x) for x in X])对比脚本里让两个模型用同一份标准化数据、同一个random_state一个画losses_一个画errors_。在完全线性可分的gaussian_2class.csv上感知机的误分类数会快速归零Adaline 的损失则是指数衰减但不为零因为它是回归意义上的拟合在鸢尾花数据上感知机的误分类数会在 1 到 3 之间跳永远停不下来Adaline 的损失则稳定在一个小的正值。这两条曲线放在一起比任何文字解释都清楚。5. 常见问题排查loss 变 nan、准确率卡 0.5 的五个坑5.1 loss 第一轮就变成 nan 或者 inf现象python train.py跑起来第一个 epoch 打印出来的loss就是inf或者nan后面全是nan。原因数据里混进了nan或者无穷值或者某些特征量纲极大比如某一列是「以分为单位的金额」而另一列是「0 到 1 的比例」导致XᵀX的最大特征值极大学习率一乘就爆。解决读数据后立刻走一遍np.isfinite(X).all()和np.isnan(X).any()有脏值先删行或者填充然后强制标准化把eta从0.01降到0.001再跑。我一般会加一行断言assert np.isfinite(X).all(), X 里有 nan/inf比事后看堆栈省事得多。5.2 损失不降反升曲线一路向上现象losses_前几轮还正常从某一轮开始加速上升最后爆掉。原因学习率超过稳定上界。批量 LMS 的稳定条件和XᵀX的特征值有关学习率大于1/λ_max时每次更新都会跨过谷底落到更高的位置误差被放大下一轮跨得更远正反馈发散。解决把eta缩小一个数量级再试同时开标准化。判断标准是看收敛需要的 epoch 数如果降到0.001需要几百轮才收敛说明数据维度高或者特征相关性强可以考虑换小批量或者加正则但课程实验里一般降学习率就够。5.3 准确率长期卡在 0.5 附近现象无论怎么调学习率、跑多少轮测试集准确率稳定在0.5损失曲线看着还在降。原因标签没映射到±1用的是0/1。前面说过0/1标签下两类误差不对称模型学到的超平面整体偏移正好把数据切错。另一个可能是权重全零初始化模型只学偏置输出恒为常数。解决在dataset.py里显式做y np.where(y 0, -1, 1)并在训练前打印np.unique(y)确认只有-1和1。初始化用random_state加小方差正态分布别用np.zeros。这两个点检查完准确率基本就上来了。5.4 同一份数据两次运行结果不一样现象不改代码、不改数据跑两次train.py准确率差几个百分点损失曲线形状也不同。原因random_state没固定或者代码里某处用了全局np.random而不是RandomState实例。全局随机会被其他调用污染比如matplotlib内部或者数据打乱操作导致每次的初始权重不同。解决所有随机性统一从self.random_state派生的rgen出禁止裸调np.random.normal。数据划分也用train_test_split(..., random_state42)固定。固定种子之后结果可复现调参时才能判断性能变化是参数带来的还是随机波动。5.5 训练集准确率很高测试集掉一截现象训练集能到0.98测试集只有0.8左右。原因特征维度相对样本量偏高或者数据划分时没有分层某一类在训练集里占比过少也可能是标准化用了全体数据的均值方差把测试集的信息泄漏进来了。解决标准化只在训练集上fit用训练集的均值和方差去变换测试集这是标准做法很多实验代码图省事在全量数据上算一次评估结果会虚高。数据划分加stratifyy保证两类比例一致样本量少于几百时尤其要做。提示排查顺序建议是「数据 → 初始化 → 学习率 → 划分」从便宜到贵。数据问题一眼能看出来改代码的时间成本最高别一上来就怀疑公式写错。6. 二次开发把实验脚本改造成能复用的估计器实验代码和工程代码的差距通常不在算法本身而在接口和可观测性。课程包里的Adaline能跑但你想拿它去接网格搜索、想加早停、想扩到多分类就得动几处结构。我的改法一般分三步走代价不大收益很明显。第一步补齐sklearn风格的接口约定fit返回self原代码已经做了加get_params/set_params这样GridSearchCV能直接扫eta和n_iter。第二步加一个早停开关fit里每轮比较losses_[-1]和losses_[-2]连续若干轮改善小于阈值就break省掉无意义的 epoch。第三步把predict里的二分类符号函数抽成策略用 one-vs-rest 循环套一层就能处理多分类。改造点原实现改造后收益参数接口构造器位置参数get_params/set_params可接GridSearchCV停止条件固定n_iter轮早停 最大轮数双约束省训练时间防过拟合分类策略硬编码±1符号OVR 包装器支持多分类日志终端打印结构化字典 可选文件结果可追溯def fit(self, X, y, tol1e-4, patience5): # ... 初始化部分不变 ... best float(inf) wait 0 for epoch in range(self.n_iter): # ... 前向与更新部分不变 ... self.losses_.append(loss) if loss best - tol: best, wait loss, 0 else: wait 1 if wait patience: # 连续 patience 轮没改善就停 break return selftol是判定「有改善」的最小幅度设太小会被浮点噪声触发不了早停设太大又会在损失还明显下降时就停1e-4在标准化后的数据上比较稳。patience给 5 轮缓冲避免一次波动就误判。这两个值不必写死挂到构造器上作为超参数更好。验证改造是否成功我习惯用两个快速检查一是同一份数据下改造前后的w_差距在1e-8以内说明算法逻辑没被改坏二是用GridSearchCV扫三组eta确认每组都能跑完不报错说明接口约定对上了。这两步过了再拿去接真实数据才放心。从那以后我每次改这类实验代码都强制先把「数据是否标准化、标签是否±1、随机种子是否固定」这三项打印出来确认无误再调参。吃过太多次「参数调到怀疑人生结果是数据没处理」的亏。希望这份拆解能帮你少走几段弯路代码跑通之后真正值得花时间的是那几行更新公式背后的几何直觉。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →