1936 字
5 分鐘
感知机 线性模型 多层感知机
第 3 章 感知机、线性模型与多层感知机
3.1 学习目标
- 掌握感知机、线性回归、逻辑回归与神经元之间的关系。
- 理解激活函数为什么是神经网络的关键组件。
- 理解多层感知机(MLP)的结构、前向传播与表达能力。
- 能从零实现一个小型前馈神经网络解决 XOR 问题。
- 能解释为什么“单层线性模型”无法解决所有非线性任务。
能力矩阵:
| 能力域 | 入门 | 进阶 | 熟练 |
|---|---|---|---|
| 感知机 | 知道线性分隔 | 理解收敛条件 | 能分析线性不可分问题 |
| MLP | 会搭层 | 理解激活与堆叠 | 能独立设计网络宽深 |
| 公式 | 会看前向传播 | 会看损失函数 | 能写出完整向量化表达 |
| 实战 | 能跑示例 | 能改结构 | 能解决 XOR / MNIST 子任务 |
3.2 感知机模型
3.2.1 感知机定义
感知机是最早的可学习分类模型之一。它的核心是:
其中:
- 若 ,则预测为正类;
- 否则预测为负类。
3.2.2 几何解释
感知机学习的是一个超平面:
这个超平面把输入空间分成两侧,分别对应不同类别。
3.2.3 感知机学习规则
对于误分类样本 ,其中 ,更新规则通常为:
其中 是学习率。
3.2.4 收敛性
若训练数据线性可分,感知机算法在有限步内收敛;若数据线性不可分,则会不断震荡,无法找到完美超平面。
3.3 线性回归与逻辑回归
3.3.1 线性回归
线性回归输出连续值:
损失函数常用均方误差:
3.3.2 逻辑回归
逻辑回归在输出端加上 Sigmoid:
它输出的是类别为正类的概率近似。
3.3.3 二分类交叉熵
若标签 ,则损失为:
这比均方误差更适合分类。
3.4 激活函数
3.4.1 为什么需要激活函数
如果网络中没有非线性激活,无论堆叠多少层,整体仍然是线性映射,表达能力不会显著提升。
3.4.2 常见激活函数
| 激活函数 | 公式 | 特点 | 常见问题 |
|---|---|---|---|
| Sigmoid | 输出在 0 到 1 | 易饱和,梯度消失 | |
| Tanh | 零中心 | 仍可能饱和 | |
| ReLU | 简单高效 | 死亡 ReLU | |
| Leaky ReLU | 缓解死亡 | 需设定 | |
| GELU | 平滑门控 | 现代网络常用 | 计算更复杂 |
3.4.3 激活函数的选择
- 二分类输出层通常用 Sigmoid。
- 多分类输出层通常用 Softmax。
- 隐藏层通常优先用 ReLU、Leaky ReLU 或 GELU。
3.5 多层感知机
3.5.1 定义
多层感知机由多个线性层和非线性激活层堆叠而成:
3.5.2 结构示意
输入层 -> [线性层] -> [激活层] -> [线性层] -> [激活层] -> 输出层3.5.3 表达能力
MLP 可以逼近很多连续函数。直观上:
- 宽度决定单层可表达的模式数量;
- 深度决定组合层级与抽象层次;
- 激活函数决定非线性程度。
这也是深度学习能够表示复杂决策边界的原因。
3.6 XOR 问题
3.6.1 为什么 XOR 不能被单层感知机解决
XOR 数据点无法用一条直线分开:
(0,0) -> 0(0,1) -> 1(1,0) -> 1(1,1) -> 0单层线性模型无法实现这种非线性边界。
3.6.2 用 MLP 解决 XOR
一个简单的两层网络就可以解决 XOR:
- 输入层 2 维;
- 隐藏层 2 或 4 个神经元;
- 输出层 1 个神经元;
- 隐藏层使用非线性激活;
- 输出层使用 Sigmoid。
3.6.3 直觉
隐藏层可以先把输入映射到一个更高维的特征空间,再让输出层在新空间中做线性分隔。
3.7 从零实现一个前馈神经网络
3.7.1 前向传播伪代码
import numpy as np
def sigmoid(x): return 1 / (1 + np.exp(-x))
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=np.float32)y = np.array([[0], [1], [1], [0]], dtype=np.float32)
W1 = np.random.randn(2, 4) * 0.1b1 = np.zeros((1, 4))W2 = np.random.randn(4, 1) * 0.1b2 = np.zeros((1, 1))
Z1 = X @ W1 + b1A1 = np.maximum(0, Z1)Z2 = A1 @ W2 + b2Y_hat = sigmoid(Z2)3.7.2 训练要点
- 需要定义损失函数;
- 需要计算梯度;
- 需要更新参数;
- 需要观察 loss 是否下降。
3.7.3 为什么手写实现有价值
手写版本可以帮助你理解:
- 每一层的输入输出形状;
- 参数如何参与计算;
- 梯度如何在链路上传递;
- 框架到底替你做了什么。
3.8 形状推导
假设:
- 输入 batch 大小为
- 输入维度为
- 隐藏层宽度为
- 输出维度为
则:
这类维度推导是后面写网络代码时最容易出错的地方。
3.9 常见误区
- 以为神经网络的关键只是“参数很多”。实际上结构和非线性更重要。
- 以为感知机和逻辑回归是完全不同的东西。实际上它们都属于线性模型家族。
- 以为激活函数可有可无。实际上没有激活函数,深层网络仍然是线性的。
- 以为隐藏层越多越好。实际上深度增加也会带来优化难度。
- 以为 XOR 只是一个玩具题。实际上它揭示了线性模型的根本局限。
3.10 本章小结
本章完成了从单个神经元到 MLP 的过渡:
- 感知机解决线性可分问题;
- 逻辑回归把分类转成概率建模;
- 激活函数引入非线性;
- MLP 通过层的堆叠获得更强表达能力;
- XOR 是理解神经网络非线性的经典入口。
3.11 课后练习
- 推导感知机的更新规则为什么会推动误分类样本被修正。
- 解释为什么没有激活函数的多层网络仍然是线性的。
- 用文字说明 Sigmoid 和 ReLU 的差异。
- 画出 XOR 的数据分布,并说明为什么一条直线无法分开它们。
- 用 Numpy 设计一个 2-4-1 的小网络尝试解决 XOR。
3.12 扩展阅读
- 感知机收敛定理
- Universal Approximation Theorem
- 激活函数梯度分析
- 逻辑回归与最大似然
3.13 从理论到代码:两层网络完整实现(含训练循环)
下面给出一个更完整的二层网络 NumPy 实现,包含前向、反向、SGD 更新与训练日志,适合用来实验不同激活与超参。
import numpy as np
def relu(x): return np.maximum(0, x)
def relu_grad(x): return (x > 0).astype(float)
def softmax(logits): shifted = logits - np.max(logits, axis=1, keepdims=True) exp = np.exp(shifted) return exp / np.sum(exp, axis=1, keepdims=True)
def cross_entropy_loss(probs, y): N = probs.shape[0] eps = 1e-12 correct_logprobs = -np.log(probs[np.arange(N), y] + eps) return np.sum(correct_logprobs) / N
def to_onehot(y, C): N = y.shape[0] one = np.zeros((N, C)) one[np.arange(N), y] = 1 return one
def train_two_layer(X, y, hidden=50, epochs=1000, lr=1e-2, print_every=100): N, D = X.shape C = np.max(y) + 1 W1 = 0.01 * np.random.randn(D, hidden) b1 = np.zeros((1, hidden)) W2 = 0.01 * np.random.randn(hidden, C) b2 = np.zeros((1, C))
for epoch in range(epochs): # forward Z1 = X.dot(W1) + b1 A1 = relu(Z1) logits = A1.dot(W2) + b2 probs = softmax(logits) loss = cross_entropy_loss(probs, y)
# backward N = X.shape[0] dscores = probs dscores[np.arange(N), y] -= 1 dscores /= N
dW2 = A1.T.dot(dscores) db2 = np.sum(dscores, axis=0, keepdims=True)
dA1 = dscores.dot(W2.T) dZ1 = dA1 * relu_grad(Z1)
dW1 = X.T.dot(dZ1) db1 = np.sum(dZ1, axis=0, keepdims=True)
# SGD update W1 -= lr * dW1 b1 -= lr * db1 W2 -= lr * dW2 b2 -= lr * db2
if epoch % print_every == 0: preds = np.argmax(probs, axis=1) acc = np.mean(preds == y) print(f"Epoch {epoch}: loss={loss:.4f}, acc={acc:.4f}")
return W1, b1, W2, b2练习:把训练过程改为 mini-batch SGD,加入验证集监控并实现早停。
3.14 XOR 详细实现与调试技巧
提供一个从零实现 XOR 的完整示例,并列出可能遇到的调试点。
要点:
- 使用小的隐藏单元数(例如 2 或 4)足够解决问题;
- 初始化不要太大;
- 学习率不要太大(从 0.1 开始尝试,向下调整);
- 检查损失曲线是否单调下降。
完整示例:
import numpy as np
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)y = np.array([0,1,1,0])
def train_xor(lr=0.1, epochs=10000): D = 2 H = 4 W1 = np.random.randn(D, H) * 0.1 b1 = np.zeros((1, H)) W2 = np.random.randn(H, 1) * 0.1 b2 = np.zeros((1, 1))
for e in range(epochs): Z1 = X.dot(W1) + b1 A1 = np.tanh(Z1) Z2 = A1.dot(W2) + b2 preds = 1/(1+np.exp(-Z2)) loss = np.mean((preds.squeeze() - y)**2)
dZ2 = 2*(preds.squeeze() - y).reshape(-1,1) * preds*(1-preds) dW2 = A1.T.dot(dZ2) db2 = np.sum(dZ2, axis=0, keepdims=True)
dA1 = dZ2.dot(W2.T) dZ1 = dA1 * (1 - np.tanh(Z1)**2) dW1 = X.T.dot(dZ1) db1 = np.sum(dZ1, axis=0, keepdims=True)
W1 -= lr * dW1 b1 -= lr * db1 W2 -= lr * dW2 b2 -= lr * db2
if e % 1000 == 0: pred_labels = (preds.squeeze() > 0.5).astype(int) acc = np.mean(pred_labels == y) print(e, loss, acc)
return W1, b1, W2, b2
train_xor()调试建议:
- 若无法收敛,先把 epochs 增大并把学习率调小;
- 用数值梯度检验关键矩阵的梯度;
- 检查是否忘记均值化或归一化输入(本问题不必要,但在真实任务常见)。
3.15 进阶话题(简要指引)
- 激活函数的饱和性如何影响深层网络的训练?
- 为什么批量归一化能加速训练?数学与工程角度解释。
- 深度与宽度如何折衷?Universal Approximation Theorem 提供的是存在性结论,但在有限样本与有限计算下要平衡。
建议阅读:“Deep Learning”(Goodfellow 等)相关章节。
分享
如果這篇文章對你有幫助,歡迎分享給更多人!
部分資訊可能已經過時
相關文章 智能推薦
1
模型解释 部署压缩 工程实战
Neural Networks 《神经网络》学习笔记:08_模型解释_部署压缩_工程实战
2
序列模型 RNN LSTM GRU 注意力入门
Neural Networks 《神经网络》学习笔记:07_序列模型_RNN_LSTM_GRU_注意力入门
3
神经网络学习总览与路线
Neural Networks 《神经网络》学习笔记:01_神经网络学习总览与路线
4
前置知识 线性代数 微积分 概率统计
Neural Networks 《神经网络》学习笔记:02_前置知识_线性代数_微积分_概率统计
5
反向传播 损失函数 优化算法
Neural Networks 《神经网络》学习笔记:04_反向传播_损失函数_优化算法





















