DL · 章节 1
深度学习 1 —— 线性神经元
课程章节 · 中央理工里尔学院 · Pascal Yim · 2026 年
经过机器学习板块对回归、分类、聚类与降维的系统训练,我们终于来到深度学习的门槛。深度网络由层层堆叠的"神经元"组成,而最简单的神经元——线性神经元 (linear neuron)——其实就是我们已经熟悉的线性回归。本章我们不会一上来就调用
nn.Linear,而是退回到最朴素的形式:从一根标量权重 和一根偏置 开始,亲手把梯度下降的循环写一遍,体会"训练一个神经元"到底意味着什么。打通这一关之后,从单输入到多输入、从批量到随机、从手写代码到 PyTorch,都只是同一个原理的自然推广。
1. 单输入线性神经元
1.1 模型的最小形式
一个单输入线性神经元接收一个标量 ,经过一次仿射变换输出预测值
这里:
- 是输入特征,
- 是权重 (weight),
- 是偏置 (bias),
- 是预测输出。
形式上,这与前面的简单线性回归毫无区别。区别在于学习方法:线性回归有解析解,而深度学习中的神经元几乎从不依赖解析解,而是用梯度下降逐步逼近最优参数。这种思路一旦掌握,就能无障碍地迁移到层数任意、激活任意、规模任意的神经网络上。
1.2 代价函数:均方误差
给定 个训练点 ,我们沿用最常见的 均方误差 (Mean Squared Error, MSE) 作为代价函数:
设 为第 个样本的预测,则可写成
关键直觉:MSE 是一个关于参数 的碗状二次曲面。曲面的最低点对应最优参数。我们的任务是从某个随机初始点出发,顺着曲面向下"滑"到谷底。
2. 梯度下降
2.1 原理
代价函数 在参数空间中定义了一张曲面。它的梯度
指向曲面在当前点上升最快的方向。如果我们沿着梯度的反方向前进一小步, 一定会下降——只要步长足够小。这就是梯度下降的全部哲学:
其中 称为学习率 (learning rate)。它控制每一步迈得多大。学习率太小,收敛缓慢;学习率太大,可能在谷底两侧来回震荡,甚至发散。我们将在本章末专门讨论这个问题。
2.2 偏导数的链式推导
为了实现梯度下降,我们必须显式地写出两个偏导数。先看单个样本的代价
(这里加了一个 因子,是为了让求导后系数变得简洁;它不会影响最优解的位置。) 由链式法则:
把所有样本的贡献求平均,就得到批量梯度:
关键公式:训练神经元时,每一步都要算出"误差 "这个量,再分别用它去乘 (得到对 的梯度) 和 1 (得到对 的梯度)。整个深度学习的反向传播本质上都是这种"误差 输入"的模式。
2.3 三种下降策略:Batch、SGD、Mini-batch
到此为止,我们计算梯度时使用了全部 个训练样本,这叫批量梯度下降 (batch gradient descent)。它确定性强、方向准,但当数据集庞大时,每走一步都要扫遍整张数据集,代价巨大。
实践中常见三种策略:
- Batch (全批量): 每次更新使用全部 个样本。梯度精确,路径平滑,但慢。
- SGD (随机梯度下降): 每次更新只随机抽一个样本 计算梯度。更新极快、内存极省,但梯度是噪声估计,损失曲线会剧烈抖动。
- Mini-batch (小批量): 每次更新随机抽取一个大小为 的子集 (典型 ),用其平均梯度更新参数。
Batch vs SGD 的核心区别: 批量是"少而精"的更新,随机是"多而糙"的更新。Mini-batch 取折中——既保留了向量化计算的硬件友好性,又通过噪声帮助模型逃离糟糕的局部最优。深度学习库默认就是 Mini-batch,因为 GPU 喜欢矩阵而非标量,而小批量正好把数据组织成矩阵。
3. 从零实现 LinearNeuron1D
3.1 类的骨架
把上面的公式翻译成 Python,我们得到一个最简单的可训练神经元类。它包含三个方法:
__init__随机初始化参数 ;forward(x)计算预测 (整个向量一次算出);fit(x, y, lr, epochs)通过梯度下降迭代更新 。
class LinearNeuron1D: def __init__(self): self.a = np.random.uniform() self.b = np.random.uniform() self.history = [] def forward(self, x): return self.a * x + self.b def fit(self, x, y, lr=0.1, epochs=10): for _ in range(epochs): u = self.forward(x) grad_a = ((u - y) * x).mean() grad_b = (u - y).mean() self.a -= lr * grad_a self.b -= lr * grad_b loss = ((y - u) ** 2).mean() self.history.append(loss)
关键直觉:
(u - y) * x与(u - y)这两个向量化表达式,正是我们前面推导的偏导数——只不过把求和写成了 NumPy 的.mean()。一行公式,一行代码,几乎是字面对应。
3.2 在 abalone_mini 上测试
我们用 abalone (鲍鱼) 数据集预测年轮数 Rings,先只用一个特征 Length:
df = pd.read_csv('abalone_mini.csv') x = df['Length'].to_numpy() y = df['Rings'].to_numpy() model = LinearNeuron1D() model.fit(x, y, lr=0.1, epochs=500) y_hat = model.forward(x) print(f"MAE : {mean_absolute_error(y, y_hat):.2f}") print(f"RMSE : {np.sqrt(mean_squared_error(y, y_hat)):.2f}")
把 model.history 画出来,我们就能看到一条经典的"L 形"损失曲线:开始几十个 epoch 损失快速下降,随后变缓直至几乎水平。这正是梯度下降逼近谷底的视觉证据。
3.3 加上 SGD 与 Mini-batch
把 fit 扩展成能在三种模式间切换非常自然——只需在每个 epoch 内决定使用哪些样本计算梯度即可:
def fit(self, x, y, lr=0.1, epochs=10, mode="batch", batch_size=32): n = len(x) for _ in range(epochs): if mode == "batch": idx = np.arange(n) elif mode == "sgd": idx = np.random.randint(0, n, size=1) elif mode == "minibatch": idx = np.random.choice(n, size=batch_size, replace=False) u = self.forward(x[idx]) grad_a = ((u - y[idx]) * x[idx]).mean() grad_b = (u - y[idx]).mean() self.a -= lr * grad_a self.b -= lr * grad_b
np.random.choice(n, size=B, replace=False) 在 中无放回抽取 个索引——这是 mini-batch 的标准实现。运行时,你会发现 SGD 模式下 history 曲线锯齿状跳动,而 mini-batch 介于两者之间——这正是噪声水平差异的可视化。
4. 多输入线性神经元
4.1 从标量到矩阵
现实问题几乎从不只有一个特征。鲍鱼有长度、直径、高度、各类重量;房屋有面积、房龄、卧室数。设有 个观测、每个观测 个特征,数据组织成矩阵
每一行是一个样本,每一列是一个特征。多输入线性神经元的预测写成
其中:
- 是权重向量 (每个特征一个权重),
- 仍是标量偏置 (NumPy 的广播会自动把它加到每一行),
- 是预测向量。
关键公式: 标量 升级为向量 ,乘法 升级为矩阵-向量乘法 ,代价函数写法不变。从一维到多维,本质就是这一句话。
4.2 多输入下的梯度
对应的代价函数仍是批量 MSE:
梯度推导沿用链式法则,结果同样美观:
注意 这一项的形状————正好与 同形,可以直接做参数更新。
4.3 类 LinearNeuron
仿照一维版本写出多维版本,只有两处差别:权重 在 fit 第一次调用时按 的列数初始化;计算梯度时把标量乘法替换为 X.T @ (u - y):
class LinearNeuron: def __init__(self): self.w = None self.b = np.random.uniform() self.history = [] def forward(self, X): return X @ self.w + self.b def fit(self, X, y, lr=0.1, epochs=10, mode="batch", batch_size=32): n, m = X.shape if self.w is None: self.w = np.random.uniform(size=m) for _ in range(epochs): if mode == "batch": idx = np.arange(n) elif mode == "sgd": idx = np.random.randint(0, n, size=1) else: idx = np.random.choice(n, size=batch_size, replace=False) Xb, yb = X[idx], y[idx] u = self.forward(Xb) grad_w = Xb.T @ (u - yb) / len(idx) grad_b = (u - yb).mean() self.w -= lr * grad_w self.b -= lr * grad_b self.history.append(((y - self.forward(X)) ** 2).mean())
在 abalone_mini 上,我们丢掉 Rings 当 ,其余列当 :模型大约几百轮就能稳定到 MAE 1.6 左右——比单输入版本明显改善。
5. 输入归一化:为什么必不可少
5.1 一个失败的例子
把同一个 LinearNeuron 直接套到 house_mini 数据集 (预测房价),你会发现一件令人崩溃的事:不管 epoch 多长,损失要么爆炸到 NaN,要么寸步难行。原因在于房屋特征量级悬殊——面积是几千平方英尺,房龄是几十年,卧室数是个位数。同一个学习率 在不同的特征方向上效果完全不同。
5.2 一维理论:曲率与学习率
为看清问题本质,先回到一维情形。考虑二次代价
其中 衡量曲率 (海森矩阵就是 自身)。梯度下降递推为
设误差 ,则
为使迭代收敛,需要 ,即
关键直觉: 学习率允许的上限正比于 。曲率 越大 (代价函数越"陡峭"),学习率必须越小;反之,在曲率小的方向上又希望 大些以加快收敛。当不同特征对应不同曲率时,找不到一个对所有方向都合适的 ——这就是不归一化时优化失败的根本原因。
5.3 多维情形:让所有方向曲率接近
把所有输入特征缩放到相近量级,代价曲面就从狭长的"香蕉形山谷"变成接近圆形的碗,梯度下降可以选用一个较大的学习率而不发散。常用方法 (来自 sklearn.preprocessing):
StandardScaler:零均值、单位方差,。深度学习里最常用。MinMaxScaler:线性映射到 ,。保留分布形状,对异常值敏感。RobustScaler:用中位数和四分位距,稳健抗异常值。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test)
关键纪律:只能在训练集上调用
fit。在训练/测试切分之前对全数据fit_transform等价于让模型偷看了测试集的统计量,这就是数据泄漏 (data leakage)。测试集对模型而言必须永远是"陌生的"。
5.4 复盘
把 house_mini 加上 StandardScaler 后再训练:
scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) model = LinearNeuron() model.fit(X_train, y_train, mode="minibatch", lr=0.01, epochs=500)
lr=0.01 这种"普通"的学习率即可让损失平稳下降——和归一化前的 lr=1e-10 形成戏剧性对比。这一对实验是整章最值得反复体会的瞬间:所谓深度学习的"调参",本质上是在合适的几何尺度上做优化。
6. 通往 PyTorch
我们手写的 LinearNeuron 在 PyTorch 中等价于一行:
model = nn.Linear(m, 1)
而我们手写的训练循环对应 PyTorch 的三件套——criterion = nn.MSELoss()、optimizer = torch.optim.SGD(model.parameters(), lr=0.01)、以及循环里的 loss.backward() + optimizer.step()。从下一章开始,我们就站在这套抽象之上,把"神经元"叠成"网络"。但要谨记:每一层 nn.Linear 内部跑的,正是我们今天亲手写过的 加 。
练习
- 公式推导: 从单样本代价 与 出发,自行重推 、,再写出批量版本 (对 取平均)。
LinearNeuron1D实现: 写出包含__init__、forward、fit三个方法的类。在abalone_mini数据集上,以Length为输入、Rings为目标训练 500 个 epoch,打印 MAE/RMSE。- 损失曲线: 给
fit加上history属性,每个 epoch 末尾把当前 MSE 追加进去。训练后用plt.plot(model.history)绘制损失曲线,观察其形状。 - 三种模式: 把
fit扩展到mode ∈ {"batch", "sgd", "minibatch"},新增batch_size参数仅在 mini-batch 下生效。比较三种模式下history曲线的平滑程度,并解释为何 SGD 抖动最大。 - 多输入版本: 实现类
LinearNeuron,在abalone_mini上以全部数值列预测Rings,观察 MAE 是否优于单输入版本。 house_mini实验: 在不归一化的情况下找一个不发散的学习率 (通常需要非常小,如 );随后引入StandardScaler,试lr ∈ {0.01, 0.1, 0.5, 1.0},记录每种设置下的训练曲线,并写下你的结论。- 数据泄漏诊断: 写一段代码,先错误地对整个 调用
scaler.fit_transform,再切分;然后用正确流程重做。比较两种情况下测试集的 MAE,讨论差异。
拓展阅读
- Goodfellow, Bengio & Courville, Deep Learning (2016): 第 4 章讨论数值计算与优化基础,第 8 章 ("Optimization for Training Deep Models") 系统讲述 Batch/SGD/Mini-batch、学习率、海森矩阵与曲率适应。中文版由人民邮电出版社出版。
- PyTorch 官方文档 —
torch.nn.Linear: https://pytorch.org/docs/stable/generated/torch.nn.Linear.html。建议阅读源码里关于weight和bias初始化的注释 (Kaiming uniform),与我们这里的np.random.uniform()比较。 - PyTorch 官方教程 — Linear Regression with PyTorch: 演示如何用
nn.Linear+MSELoss+optim.SGD复现本章手写代码,是从"NumPy 神经元"过渡到"PyTorch 神经元"的最佳一步。 scipy.optimize.minimize: 提供 BFGS、L-BFGS、Newton-CG 等更高级的优化算法,可用于在小规模问题上验证手写梯度下降的正确性,并对比收敛速度。- Bottou, Curtis & Nocedal (2018), Optimization Methods for Large-Scale Machine Learning, SIAM Review. 一篇被广泛引用的综述,对 SGD 与 mini-batch 的理论性质给出了深入而严谨的分析。
sklearn.preprocessing用户指南: https://scikit-learn.org/stable/modules/preprocessing.html。比较StandardScaler、MinMaxScaler、RobustScaler、PowerTransformer各自适用场景。