简介:面向神经网络入门学习者与课程实验教学场景的Matlab实验代码,围绕Adaline自适应线性神经元和LMS最小均方误差学习规则展开,适合用于理解线性神经元结构、权重迭代更新与误差收敛过程。压缩包共5个文件,包含3个m脚本和2个mat数据文件,体积约10KB;脚本对应算法实现、随机初始化与样本随机选取,数据文件保存训练集和测试样本,可直接加载运行并复现实验。资源已有300人学习,内容重点落在随机权重初始化对收敛的影响、学习率与权重更新公式、训练测试集划分以及泛化性能评估等环节。借助这套代码,读者能把课堂中的Adaline模型落到可运行的Matlab实现上,掌握LMS算法从数据准备、训练迭代到结果观察的基本流程,并为进一步学习其他神经网络模型打下基础。
1. 从一次「损失震荡」的课后实验说起:这份 Adaline LMS 代码能解决什么
很多人第一次在神经网络导论课程实验里碰到 Adaline 和 LMS,都是在「数据明明线性可分,损失曲线却来回震荡」的地方卡住的。这份实验代码把自适应线性神经元的最小均方学习规则,从黑板上的公式落成一套能直接跑的 Python 脚本:造数据、标准化、前向算线性输出、按连续误差更新权重、画损失曲线和决策边界,一条链路走完。它适合三类人:刚上完第一次实验课、想把公式和代码对上号的同学;需要交作业但不想只抄推导的开发者;以及想拿一个干净的线性二分类基线做对照实验的人。整套实现不依赖深度学习框架,纯 NumPy 手写梯度,正好用来看清学习率到底在改什么。
2. 项目定位与技术栈:Adaline 与 LMS 到底落在哪几行代码里
这份代码的定位非常明确,它不是通用机器学习库,而是一个教学用的最小闭环:给定一份二维特征、标签为 ±1 的数据集,训练一个线性模型,并且把训练过程中的每一个中间量都暴露出来给你看。之所以强调「暴露」,是因为课程实验的价值不在最终准确率,而在你能不能解释清楚权重为什么在这个方向、这个步长上移动。所以代码里没有花哨的封装,损失按轮记录,梯度手写展开,连标准化都单独抽了一个函数,方便你在断点里逐行看数值。
技术选型上,NumPy 负责全部数值计算,Matplotlib 负责两类图:损失随迭代下降的曲线,以及特征平面上的决策边界加散点。数据默认用脚本现场生成的两簇高斯分布,也可以换成任意 CSV。配置文件用 YAML 存超参数,训练入口用 argparse 接收命令行覆盖,这样同一份代码既能在 notebook 里交互调试,也能在终端里批量跑学习率扫描。整体依赖只有三个包,装完就能跑,不需要 GPU,也不需要任何深度学习框架。
2.1 Adaline 与感知机的分界线在哪
这是整份实验代码最容易被囫囵吞过去的一点。感知机的更新规则是「分错了才改」:先对线性输出取符号得到预测,再用标签减预测得到的离散误差去更新权重。它的后果是权重不唯一,只要能把两类分开就停,而且线性不可分时永远不收敛。
Adaline 把这一步拆开了:训练时用连续的线性输出z = w·x + b直接和标签算误差,不取符号;只有预测阶段才做sign(z)的硬判决。这样一来,损失函数就变成了均方误差J(w) = 1/2 · Σ(y_i - z_i)²,它是一个关于权重的凸二次函数,梯度连续可导,用梯度下降一定能在合适的学习率下收敛到全局最优。LMS 规则(也叫 Widrow-Hoff 规则)就是对这个损失做梯度下降的结果:
对单个样本: w := w + η · (y - z) · x b := b + η · (y - z) 对批量样本: w := w + η · Xᵀ · (y - z) / n b := b + η · mean(y - z)这里的(y - z)是连续值,可能等于 0.37,也可能等于 -1.82,而不是感知机里非 0 即 ±2 的离散量。这个差别决定了 Adaline 的更新是「按错的程度成比例地改」,离边界越远的错分样本拉得越狠,这正是 LMS 名字里「最小均方」的来源。
课程把 Adaline 放在第一次实验是有讲究的:它的梯度推导是后面多层感知机反向传播的简化版,链式法则、学习率、损失曲面这些概念在这里只需要面对一层线性变换,翻车成本最低。你在这一层把梯度形状、矩阵维度、学习率量级搞明白,后面推到隐藏层时就不会从零开始。
2.2 目录结构与各文件职责
代码按「数据、模型、训练、绘图」四块拆,目录不深,但每个文件职责单一,方便你只改其中一块做对照实验。
adaline_lms/ ├── data/ │ └── blobs_2d.csv # 可选的离线数据,格式为 x1,x2,label ├── src/ │ ├── adaline.py # AdalineLMS / AdalineSGDLMS 两个模型类 │ ├── data_utils.py # 造数据、读 CSV、标准化、训练测试划分 │ ├── train.py # 命令行入口,串起训练与评估 │ └── plot_utils.py # 损失曲线、决策边界、权重轨迹 ├── configs/ │ └── default.yaml # eta / n_iter / batch 模式等默认超参 ├── notebooks/ │ └── experiment_1.ipynb # 课堂演示用,逐格看中间变量 ├── requirements.txt └── README.mdadaline.py是核心,两个类共用同一套前向计算,区别只在权重更新是每轮一次还是每样本一次。data_utils.py里最值得看的是标准化函数,它严格只用训练集的均值和标准差去变换测试集,这一点在课程作业里经常被写成用全量数据算统计量,属于典型的数据泄露,虽然在这个玩具数据集上影响不明显,但习惯要一开始就养对。train.py只做三件事:解析参数、调fit、打印准确率和最终权重。plot_utils.py里的决策边界有两种画法,一种是网格预测填色,一种是直接由w·x + b = 0解出直线方程,后者更快也更精确,后面会讲什么时候该用哪种。
2.3 一次训练迭代里数据是怎么流动的
把一次完整的fit拆开看,数据流是这样的:原始特征先经过 z-score 标准化,变成均值 0、标准差 1 的矩阵;权重用小方差正态分布初始化,偏置置 0;每一轮里,X与w做矩阵乘法加上b得到长度为 n 的线性输出向量z;y - z得到连续误差向量;用Xᵀ · errors / n算出损失对权重的梯度,乘学习率后累加到w上;同时把这一轮的均方误差记进losses_列表。全部轮次结束后,predict才第一次调用符号函数,把连续输出压成 ±1 的类别。
这条链路里有两个容易被忽略的设计:一是标准化放在划分之后、训练之前,保证训练集和测试集用的是同一套统计量;二是误差向量在更新前不做任何截断或符号化,一旦你在这里手滑加了np.sign,模型就退化回感知机了,损失曲线也会从平滑下降变成阶梯状。如果你在调试时发现损失曲线呈现明显台阶,第一件事就是回去检查这一行。
3. 核心模块拆解:一个样本从进网络到更新权重走了哪几步
这一章按执行顺序把关键代码逐段拆开,每一段都给出可以直接抄进自己工程的版本。代码里的注释保留了「为什么这么写」而不只是「这行做了什么」,因为课程实验最容易出错的地方往往不是语法,而是某一步的数学含义被写反了。
3.1 数据生成与标准化:先把量纲拉平
import numpy as np def make_linear_blobs(n_samples=200, n_features=2, class_sep=1.2, random_state=42): """生成两簇线性可分的高斯数据,标签取 +1 / -1""" rng = np.random.default_rng(random_state) n_per_class = n_samples // 2 # 两个簇中心沿每个维度反向拉开,class_sep 越大越容易分 center_pos = np.full(n_features, class_sep) center_neg = np.full(n_features, -class_sep) X_pos = rng.normal(loc=center_pos, scale=1.0, size=(n_per_class, n_features)) X_neg = rng.normal(loc=center_neg, scale=1.0, size=(n_samples - n_per_class, n_features)) X = np.vstack([X_pos, X_neg]) y = np.hstack([np.ones(n_per_class), -np.ones(n_samples - n_per_class)]) return X, y def standardize(X_train, X_test=None): """按训练集统计量做 z-score,测试集复用同一套 mu / sigma""" mu = X_train.mean(axis=0) sigma = X_train.std(axis=0) + 1e-8 # 加极小值防止常数列除零 X_train_std = (X_train - mu) / sigma if X_test is None: return X_train_std, mu, sigma return X_train_std, (X_test - mu) / sigma, mu, sigmaclass_sep控制两簇中心的距离,取 1.2 时数据基本线性可分但边界附近仍有交叠,比完全分开的数据更能看出模型是不是真的在学。标签用 ±1 而不是 0/1,是因为更新公式里y - z的符号直接决定权重往哪边推,用 0/1 会让负类样本的梯度天然偏小,模型会系统性偏向正类,这个坑在作业里出现频率极高。
standardize里的+ 1e-8是防御性写法,真实数据里某一列全相同的情况并不罕见。更关键的是它只接受训练集统计量,测试集必须复用,否则你在评估时其实已经偷看了测试分布。
3.2 前向计算与 LMS 权重更新
class AdalineLMS: """自适应线性神经元,批量梯度下降版的 LMS 规则""" def __init__(self, eta=0.01, n_iter=50, random_state=1): self.eta = eta # 学习率,每步沿负梯度走多远 self.n_iter = n_iter # 遍历整个训练集的轮数 self.random_state = random_state def fit(self, X, y): rng = np.random.default_rng(self.random_state) # 权重用小方差随机数,偏置置 0,避免所有维度起点完全对称 self.w_ = rng.normal(loc=0.0, scale=0.01, size=X.shape[1]) self.b_ = 0.0 self.losses_ = [] for _ in range(self.n_iter): z = X.dot(self.w_) + self.b_ # 线性输出,不取符号 errors = y - z # 连续误差 n = X.shape[0] # 均方误差对 w 的梯度:-Xᵀ·errors / n,这里直接写成上升方向 self.w_ += self.eta * X.T.dot(errors) / n self.b_ += self.eta * errors.mean() self.losses_.append((errors ** 2).mean() / 2.0) return self def net_input(self, X): return X.dot(self.w_) + self.b_ def predict(self, X): # 只有预测阶段才做硬判决 return np.where(self.net_input(X) >= 0.0, 1, -1)eta是最需要动手改的参数,标准化之后取 0.001 到 0.01 之间通常都稳,取到 0.1 以上就开始震荡。n_iter在批量模式下不需要很大,50 轮足够让损失降到小数点后三四位。更新式里除以n这一步不是可选项:如果不除,梯度量级会随样本数线性放大,同一个学习率在 200 样本和 2000 样本上的表现完全不同,除完之后学习率就与数据规模解耦了,这也是「为什么我的代码换个数据集就发散」的常见答案。
predict里的判断用>= 0而不是> 0,是为了让恰好落在边界上的样本有个确定归属,避免全零权重时输出全 -1。
随机梯度下降版本把更新挪到样本循环内部:
class AdalineSGDLMS: """逐样本更新的 LMS,每看到一个样本就改一次权重""" def __init__(self, eta=0.001, n_iter=20, shuffle=True, random_state=1): self.eta = eta self.n_iter = n_iter self.shuffle = shuffle self.random_state = random_state def fit(self, X, y): rng = np.random.default_rng(self.random_state) self.w_ = rng.normal(0.0, 0.01, size=X.shape[1]) self.b_ = 0.0 self.losses_ = [] for _ in range(self.n_iter): idx = rng.permutation(len(y)) if self.shuffle else np.arange(len(y)) for i in idx: xi, yi = X[i], y[i] err = yi - (xi.dot(self.w_) + self.b_) self.w_ += self.eta * err * xi # 单样本梯度,没有 1/n self.b_ += self.eta * err self.losses_.append(err ** 2 / 2.0) return selfSGD 版本的学习率要比批量版小一个数量级,因为每个样本都在更新,等效步长被放大了。shuffle打开后每轮顺序不同,损失曲线会带明显噪声,这是正常的,判断收敛要看滑动平均而不是单点值。losses_的长度在这里是n_iter × n_samples,画图时横轴含义和批量版完全不同,这一点后面绘图那节还会提到。
3.3 损失曲线与决策边界怎么画
import matplotlib.pyplot as plt import numpy as np def plot_loss(ax, losses, xlabel="迭代轮数"): ax.plot(np.arange(1, len(losses) + 1), losses, linewidth=1.5) ax.set_xlabel(xlabel) ax.set_ylabel("MSE / 2") ax.grid(alpha=0.3) def plot_boundary_by_grid(ax, model, X, y, step=0.02): """网格预测填色,直观但慢,适合展示""" x1_min, x1_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5 x2_min, x2_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5 xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, step), np.arange(x2_min, x2_max, step)) grid = np.c_[xx1.ravel(), xx2.ravel()] Z = model.predict(grid).reshape(xx1.shape) ax.contourf(xx1, xx2, Z, alpha=0.2, cmap="coolwarm") ax.scatter(X[y == 1, 0], X[y == 1, 1], marker="o", label="+1") ax.scatter(X[y == -1, 0], X[y == -1, 1], marker="x", label="-1") def boundary_line(w, b, x1): """由 w0*x1 + w1*x2 + b = 0 解出 x2,比网格法快也更精确""" return -(w[0] * x1 + b) / (w[1] if abs(w[1]) > 1e-12 else 1e-12)网格法的优点是能直接复用predict,缺点是分辨率和内存成正比,step取 0.02 时一张图大概几万个点,笔记本里跑还行,批量出图就明显变慢。直接解直线方程没有这个问题,但它只适用于二维特征,特征多于两维时还是得回到网格法或者做降维投影。
boundary_line里对w[1]做了保护,因为权重可能收敛到接近 0,除出来是inf,Matplotlib 会直接画出两条竖线,现象看起来很诡异,实际只是除零。
3.4 训练入口与超参数落点
import argparse import numpy as np from adaline import AdalineLMS from data_utils import make_linear_blobs, standardize from plot_utils import plot_loss, plot_boundary_by_grid def main(): parser = argparse.ArgumentParser() parser.add_argument("--eta", type=float, default=0.01) parser.add_argument("--n-iter", type=int, default=50) parser.add_argument("--seed", type=int, default=42) args = parser.parse_args() X, y = make_linear_blobs(n_samples=200, class_sep=1.2, random_state=args.seed) # 固定划分顺序,保证不同超参实验看到的是同一份训练集 idx = np.random.default_rng(0).permutation(len(y)) split = int(len(y) * 0.8) tr, te = idx[:split], idx[split:] X_tr, X_te, mu, sigma = standardize(X[tr], X[te]) model = AdalineLMS(eta=args.eta, n_iter=args.n_iter, random_state=args.seed) model.fit(X_tr, y[tr]) acc = (model.predict(X_te) == y[te]).mean() print(f"eta={args.eta} n_iter={args.n_iter} 测试准确率={acc:.3f}") print(f"最终权重={model.w_.round(4)} 偏置={model.b_:.4f}") print(f"最后一轮损失={model.losses_[-1]:.6f}") if __name__ == "__main__": main()入口里把划分种子和模型种子分开传,是一个刻意的设计:seed同时影响数据生成和权重初始化,而划分固定用 0。这样你在扫学习率时,数据分布和训练测试划分都不变,唯一变的是模型超参,结论才可比较。如果连划分都跟着种子变,你会看到准确率上下跳动却分不清是超参的功劳还是数据抽样的运气。
打印最终权重和最后一轮损失比只打印准确率有用得多。准确率在小测试集上分辨率有限,40 个样本错 1 个就是 2.5 个百分点,而损失值能到小数点后六位,能更早看出收敛趋势。
4. 跑起来与调参:学习率、迭代轮数、收敛判据怎么定
4.1 环境准备与最小运行路径
# 建议用虚拟环境,实验代码依赖很少,隔离起来不污染全局 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install numpy matplotlib pyyaml # 从仓库根目录运行,模块导入路径才正确 python src/train.py --eta 0.01 --n-iter 50 # 扫一组学习率,观察损失曲线形态变化 for eta in 0.001 0.005 0.01 0.05 0.1; do python src/train.py --eta $eta --n-iter 50 done从src的同级目录运行很关键。如果你直接cd src再python train.py,from adaline import ...能过,但配置文件的相对路径就会找不到;反过来字节码缓存还会在src下留一堆__pycache__。我一般习惯在项目根目录跑,路径统一,脚本里写相对路径也只用相对于根目录的那一套。
第一个命令跑完应该能看到准确率在 0.95 以上、最后一轮损失在 0.01 量级。如果准确率停在 0.5 附近,先别怀疑代码,去看看标签是不是被读成了 0/1。
4.2 学习率、迭代轮数与初始化的取值逻辑
| 参数 | 常用范围 | 影响 | 调错的典型现象 |
|---|---|---|---|
eta(批量) | 0.001 ~ 0.01 | 步长,决定收敛速度与稳定性 | 过大震荡发散,过小 50 轮还没降下去 |
eta(SGD) | 0.0001 ~ 0.001 | 单样本步长,等效被放大约 n 倍 | 与批量同值时直接 NaN |
n_iter | 批量 20~100,SGD 10~30 | 遍历次数 | 太少欠拟合,太多纯浪费时间 |
| 权重初始化 | 标准差 0.01 的正态 | 起点 | 全零初始化会让梯度对称,边界卡在原点 |
class_sep | 0.8 ~ 2.0 | 数据可分程度 | 太小导致线性不可分,损失降不到底 |
学习率的上界不是拍脑袋定的。对均方误差这个二次型损失,梯度下降稳定的充分条件是η < 2 / λ_max,其中λ_max是XᵀX / n的最大特征值。特征做过标准化之后,λ_max大概在特征维度这个量级,二维数据下就是 1 到 2 之间,所以η的理论上界接近 1。但理论值只是不发散的门槛,实际想要平滑收敛,取上界的百分之一到十分之一更合适,这就是 0.001 到 0.01 的来源。你在三维、十维特征上做实验时会发现同样的eta更容易抖,原因就是λ_max随维度涨了。
迭代轮数在批量模式下其实可以由收敛判据自动决定,第 6 章会给早停的写法。手工设 50 轮是教学场景的简化,让损失曲线有足够长度展示下降形态。
4.3 判断「真收敛」的三个层次
第一层看损失曲线的形状:健康的情况是前几轮陡降,后面逐渐变平,最后几轮几乎贴住横轴。如果曲线在中段开始小幅上下摆动,说明学习率已经接近稳定边界,降一个数量级就平滑了。
第二层看最后若干轮的相对变化。与其肉眼看图,不如直接算一下:
def is_converged(losses, window=10, tol=1e-4): """比较最后 window 轮的首尾损失,相对变化小于 tol 视为收敛""" if len(losses) < window: return False head = losses[-window] tail = losses[-1] return abs(head - tail) / (abs(head) + 1e-12) < tol这个判据比「损失小于某个绝对值」稳,因为损失量级随数据尺度和样本数变化,绝对值阈值换个数据集就失效了。
第三层,也是最硬的一层,是拿闭式解对一遍。Adaline 优化的就是线性回归的均方误差,所以它的最优权重有解析形式:给X补一列全 1 吸收偏置,w* = (XᵀX)⁻¹Xᵀy。梯度下降跑出来的损失应该和这个解析解的损失非常接近,差距在千分之一以内才算真的收敛到了全局最优。
def closed_form_solution(X, y): """最小二乘闭式解,给 X 补一列 1 来吸收偏置项""" X_aug = np.c_[np.ones(len(X)), X] w_aug, *_ = np.linalg.lstsq(X_aug, y, rcond=None) return w_aug[1:], w_aug[0]这个对照在课程作业里很少有人做,但它能立刻暴露出学习率太小导致的「假收敛」:曲线看着平了,其实离最优还差一截。我一般在跑完任何线性模型实验后都会做这一步,两分钟的事,能省掉后面一堆「为什么我的结果和教材对不上」的困惑。
5. 避坑与排查:五条血泪记录,从发散到决策边界画不出来
5.1 训练阶段的三类翻车
现象一:损失不降反升,几轮之后就变成 NaN。原因基本只有一个,学习率相对于特征尺度太大了。未标准化的特征如果某一列取值在几千的量级,XᵀX的特征值会被放大到百万级,稳定上界2/λ_max就小到几乎为零,任何常规学习率都会让权重一步跨到无穷。解决方式是把标准化放在训练之前,然后从 0.001 开始往上试,每次乘 3,直到看到曲线开始抖动就退回上一档。
现象二:损失能降,但准确率一直卡在 0.5 左右。先检查标签编码。如果标签是 0/1 而predict返回 ±1,那所有 0 类样本都会被判错,准确率正好一半。另一个可能是标准化时用了测试集统计量,训练和评估的特征分布不一致,模型在训练集上拟合得再好,测试集上也全是噪声。修复方法是统一标签为 ±1,并且严格只用训练集的mu和sigma。
现象三:权重在一个方向上越跑越大,另一个方向几乎不动。这是特征尺度差异过大导致的病态曲率,梯度下降在狭长峡谷里来回横跳。标准化的作用就是把这个峡谷压成接近圆形。如果标准化后还有轻微现象,可以检查是不是某一维方差接近 0,常数列在标准化时会放大噪声,直接删掉更干净。
5.2 复现与绘图阶段的坑
现象四:同样的代码,每次运行结果都不一样。检查有没有混用全局随机源。np.random.randn用的是全局状态,任何一次别的调用都会改变它的序列。统一改成np.random.default_rng(seed)生成独立生成器,并且数据生成、权重初始化、样本打乱各用一个明确传入的种子。训练测试划分的种子要和模型种子分开,否则调超参时数据分布也跟着变,实验结论不可比。
现象五:决策边界画出来是空白,或者只有一条贴边的竖线。空白通常是网格范围没覆盖到数据,contourf在网格内的预测全同色,看起来就像没画。打印一下np.unique(Z)就能确认。贴边竖线则是boundary_line里w[1]接近 0 导致的除零,加个极小值保护,或者干脆改用网格法。还有一种情况是predict返回了布尔数组而不是 ±1,contourf对它做颜色映射时层级不对,图例也就乱了。
现象六:图上中文全是方块。Matplotlib 默认字体不含中文,设置一次就行:plt.rcParams["font.sans-serif"] = ["SimHei"],同时把axes.unicode_minus设成False,否则负号也会变成方块。这个设置放在绘图模块顶部统一做,别在每个函数里重复写。
6. 二次开发:把课程作业改造成可复用的二分类基线
这份代码跑通之后,最自然的下一步是把它从「一次性脚本」变成「能反复用的基线」。我一般会做三件事:加早停、对齐 scikit-learn 的接口习惯、补一个和感知机及逻辑回归的对照表。
早停的写法要注意判据的方向,改善不足才计数,别写成损失上升才计数:
class EarlyStoppingAdaline(AdalineLMS): def __init__(self, eta=0.01, n_iter=500, tol=1e-5, patience=5, random_state=1): super().__init__(eta=eta, n_iter=n_iter, random_state=random_state) self.tol = tol # 单轮损失至少下降这么多才算有效改善 self.patience = patience # 连续多少轮没改善就停 def fit(self, X, y): rng = np.random.default_rng(self.random_state) self.w_ = rng.normal(0.0, 0.01, size=X.shape[1]) self.b_ = 0.0 self.losses_ = [] best, wait = float("inf"), 0 for epoch in range(self.n_iter): errors = y - (X.dot(self.w_) + self.b_) self.w_ += self.eta * X.T.dot(errors) / len(y) self.b_ += self.eta * errors.mean() loss = (errors ** 2).mean() / 2.0 self.losses_.append(loss) if best - loss < self.tol: # 改善幅度不够 wait += 1 if wait >= self.patience: self.n_iter_ = epoch + 1 break else: best, wait = loss, 0 # 有实质改善,重置计数 self.n_iter_ = getattr(self, "n_iter_", self.n_iter) return selfpatience取 5 是个经验值,太小会在损失平台期提前停,太大又失去了早停的意义。tol建议按损失量级来定,损失在 0.01 量级时取 1e-5 比较合适,量级变了要跟着调,否则要么从不触发,要么第一轮就停。
接口对齐方面,把fit返回值改成self、加上get_params和set_params,就能直接塞进 Pipeline 里和标准化器串起来。这一步的收益是你可以用同一套交叉验证代码去评估它,不用为每个模型写一遍循环。
对照实验建议把四个模型放在同一份数据、同一个划分、同一套标准化下比较,结论才有意义:
| 模型 | 更新依据 | 是否保证收敛 | 边界唯一性 | 适用场景 |
|---|---|---|---|---|
| 感知机 | sign后的离散误差 | 仅线性可分时 | 不唯一 | 教学演示 |
| Adaline 批量 LMS | 连续误差,每轮一次 | 凸问题,η 合适即可 | 唯一(MSE 最优) | 基线、小数据 |
| Adaline 随机 LMS | 连续误差,每样本一次 | 学习率衰减时近似收敛 | 近似唯一 | 数据量较大 |
| 逻辑回归 | 交叉熵 + sigmoid | 凸问题 | 唯一 | 需要概率输出 |
验证改造是否成功,别只看测试准确率,还是回到第 4 章那三层判据:损失曲线是否平滑、早停触发时离闭式解的损失差多少、换三个随机种子结果波动是否在可接受范围。我通常要求改造后的版本在闭式解损失的两倍以内,达不到就说明学习率或停止条件还有问题。
从那以后我每次跑线性模型的实验,都会先拿闭式解对一遍再去看曲线,这个习惯帮我挡掉过好几次「看起来收敛了其实停在半路」的假象。希望帮到你。
本文还有配套的精品资源,点击获取