公司新项目采用了这个技术栈,从0到上线的过程中积累了不少经验,今天分享出来供大家参考。

“人脑有大约 860 亿个神经元,而现代神经网络可能只有几百万个参数。
但我们已经能用它识别图像、翻译语言、生成文本——
这不是因为网络像大脑,而是因为我们找到了一种通用的函数逼近方式。”
——深度学习的工程哲学


一、为什么需要神经网络?

在前六章中,我们系统学习了线性模型决策树集成方法(随机森林、XGBoost) 等经典机器学习算法。它们在结构化数据(表格数据)上表现卓越,具备良好的可解释性和工程稳定性。

然而,当我们面对以下任务时,传统方法开始力不从心:

  • 图像识别:像素之间存在复杂的局部与全局空间关系;
  • 语音识别:音频信号是高维、时序、非平稳的;
  • 自然语言处理:词语组合具有高度上下文依赖性;
  • 高维非线性模式:特征交互呈指数级增长(如“用户点击 A 且未购买 B 且浏览 C”)。

这些任务的共同特点是:输入维度极高、特征间存在深层非线性交互、且难以通过人工特征工程有效表达

本章目标

  • 理解神经网络如何通过“层叠非线性变换”实现通用函数逼近;
  • 掌握前向传播、反向传播、梯度下降的核心机制;
  • 从零实现一个全连接神经网络(MLP);
  • 深入探讨激活函数、损失函数、优化器的选择逻辑;
  • 辩证看待神经网络的“黑箱”属性,探索可解释性工具;
  • 对比神经网络与传统模型的适用边界。

二、神经网络的直觉:从感知机到多层网络

2.1 感知机:神经网络的起点

1957 年,Frank Rosenblatt 提出感知机(Perceptron),这是最早的神经网络模型。

对于输入 x=[x1,x2,...,xn]⊤\mathbf{x} = [x_1, x_2, ..., x_n]^\topx=[x1,x2,...,xn],感知机计算:

z=w⊤x+b=∑i=1nwixi+b z = \mathbf{w}^\top \mathbf{x} + b = \sum_{i=1}^{n} w_i x_i + b z=wx+b=i=1nwixi+b

然后通过阶跃函数输出:

y^={1,if z≥00,otherwise \hat{y} = \begin{cases} 1, & \text{if } z \geq 0 \\ 0, & \text{otherwise} \end{cases} y^={1,0,if z0otherwise

这本质上是一个线性分类器——它只能解决线性可分问题(如 AND、OR),但无法解决 XOR(异或)问题。

局限性:单层感知机无法表示非线性决策边界。

2.2 多层感知机(MLP):引入隐藏层

为突破线性限制,我们在输入和输出之间加入隐藏层(Hidden Layer):

代码解析:

  • 这段代码实现了核心功能逻辑
  • 注意异常处理和边界条件
  • 生产环境建议添加日志记录
输入层 → 隐藏层 → 输出层

每一层由多个神经元(Neuron)组成,每个神经元执行:

aj(l)=σ(∑kwjk(l)ak(l−1)+bj(l)) a^{(l)}_j = \sigma \left( \sum_{k} w^{(l)}_{jk} a^{(l-1)}_k + b^{(l)}_j \right) aj(l)=σ(kwjk(l)ak(l1)+bj(l))

其中:

  • aj(l)a^{(l)}_jaj(l):第 lll 层第 jjj 个神经元的激活值(activation);
  • wjk(l)w^{(l)}_{jk}wjk(l):从第 l−1l-1l1 层第 kkk 个神经元到第 lll 层第 jjj 个神经元的权重
  • bj(l)b^{(l)}_jbj(l):第 lll 层第 jjj 个神经元的偏置
  • σ(⋅)\sigma(\cdot)σ()激活函数(非线性函数)。

关键洞见
只要激活函数是非线性的,多层网络就能逼近任意连续函数(Universal Approximation Theorem)。

2.3 通用逼近定理(Universal Approximation Theorem)

1989 年,George Cybenko 证明:

对于任意连续函数 f:Rn→Rmf: \mathbb{R}^n \to \mathbb{R}^mf:RnRm,在紧集上,存在一个单隐藏层的前馈神经网络,使用 Sigmoid 激活函数,可以以任意精度逼近 fff

这意味着:神经网络不是某种神秘的“智能”,而是一种极其灵活的函数拟合器

✅ 它的强大不在于“像人脑”,而在于通过组合简单非线性单元,构建复杂映射


三、数学基础:前向传播与损失函数

3.1 前向传播(Forward Propagation)

设网络有 LLL 层(含输入和输出),第 lll 层有 nln_lnl 个神经元。

定义:

  • A(0)=X∈Rm×n0\mathbf{A}^{(0)} = \mathbf{X} \in \mathbb{R}^{m \times n_0}A(0)=XRm×n0:输入矩阵(mmm 个样本,n0n_0n0 个特征);
  • W(l)∈Rnl−1×nl\mathbf{W}^{(l)} \in \mathbb{R}^{n_{l-1} \times n_l}W(l)Rnl1×nl:第 lll 层权重矩阵;
  • b(l)∈R1×nl\mathbf{b}^{(l)} \in \mathbb{R}^{1 \times n_l}b(l)R1×nl:偏置向量(广播);
  • Z(l)=A(l−1)W(l)+b(l)\mathbf{Z}^{(l)} = \mathbf{A}^{(l-1)} \mathbf{W}^{(l)} + \mathbf{b}^{(l)}Z(l)=A(l1)W(l)+b(l):线性变换;
  • A(l)=σ(l)(Z(l))\mathbf{A}^{(l)} = \sigma^{(l)}(\mathbf{Z}^{(l)})A(l)=σ(l)(Z(l)):激活输出。

最终输出为 A(L)\mathbf{A}^{(L)}A(L)

向量化计算使得一次前向传播可处理整个批量(batch),极大提升效率。

3.2 激活函数:引入非线性

若没有激活函数,多层网络等价于单层线性变换(因为矩阵乘法满足结合律)。

常用激活函数:

函数公式特点
Sigmoidσ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}σ(z)=1+ez1输出 (0,1)(0,1)(0,1),适合概率;但易饱和、梯度消失
Tanhtanh⁡(z)=ez−e−zez+e−z\tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}tanh(z)=ez+ezezez输出 (−1,1)(-1,1)(1,1),零中心;仍会饱和
ReLUReLU(z)=max⁡(0,z)\text{ReLU}(z) = \max(0, z)ReLU(z)=max(0,z)最常用;计算快、缓解梯度消失;但有“死神经元”问题
Leaky ReLULReLU(z)=max⁡(0.01z,z)\text{LReLU}(z) = \max(0.01z, z)LReLU(z)=max(0.01z,z)解决死神经元
Softmaxsoftmax(zi)=ezi∑jezj\text{softmax}(z_i) = \frac{e^{z_i}}{\sum_j e^{z_j}}softmax(zi)=jezjezi多分类输出层专用,保证概率和为1

现代实践:隐藏层用 ReLU 或其变体,输出层根据任务选择(Sigmoid/Softmax)。

3.3 损失函数:衡量预测误差

损失函数需与任务匹配:

回归任务:均方误差(MSE)

LMSE=1m∑i=1m(y^i−yi)2 \mathcal{L}_{\text{MSE}} = \frac{1}{m} \sum_{i=1}^{m} (\hat{y}_i - y_i)^2 LMSE=m1i=1m(y^iyi)2

二分类:二元交叉熵(Binary Cross-Entropy)

LBCE=−1m∑i=1m[yilog⁡(y^i)+(1−yi)log⁡(1−y^i)] \mathcal{L}_{\text{BCE}} = -\frac{1}{m} \sum_{i=1}^{m} \left[ y_i \log(\hat{y}_i) + (1 - y_i) \log(1 - \hat{y}_i) \right] LBCE=m1i=1m[yilog(y^i)+(1yi)log(1y^i)]

其中 y^i=σ(zi)∈(0,1)\hat{y}_i = \sigma(z_i) \in (0,1)y^i=σ(zi)(0,1)

多分类:分类交叉熵(Categorical Cross-Entropy)

LCE=−1m∑i=1m∑k=1Kyiklog⁡(y^ik) \mathcal{L}_{\text{CE}} = -\frac{1}{m} \sum_{i=1}^{m} \sum_{k=1}^{K} y_{ik} \log(\hat{y}_{ik}) LCE=m1i=1mk=1Kyiklog(y^ik)

其中 yi\mathbf{y}_iyi 是 one-hot 标签,y^i=softmax(zi)\hat{\mathbf{y}}_i = \text{softmax}(\mathbf{z}_i)y^i=softmax(zi)

⚠️ 注意:不要对 Softmax 输出再加 Sigmoid!Softmax 已确保输出为概率分布。


四、核心算法:反向传播(Backpropagation)

反向传播是神经网络训练的基石,它高效地计算损失函数对所有参数的梯度。

4.1 链式法则(Chain Rule)回顾

L=f(g(h(x)))L = f(g(h(x)))L=f(g(h(x))),则:

dLdx=dLdg⋅dgdh⋅dhdx \frac{dL}{dx} = \frac{dL}{dg} \cdot \frac{dg}{dh} \cdot \frac{dh}{dx} dxdL=dgdLdhdgdxdh

神经网络的损失是参数的复合函数,链式法则是计算梯度的自然工具。

4.2 反向传播的推导(以两层网络为例)

考虑一个简单网络:

  • 输入:x∈Rn\mathbf{x} \in \mathbb{R}^nxRn
  • 隐藏层:h=ReLU(W1x+b1)\mathbf{h} = \text{ReLU}(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1)h=ReLU(W1x+b1)
  • 输出:y^=σ(w2⊤h+b2)\hat{y} = \sigma(\mathbf{w}_2^\top \mathbf{h} + b_2)y^=σ(w2h+b2)
  • 损失:L=BCE(y,y^)\mathcal{L} = \text{BCE}(y, \hat{y})L=BCE(y,y^)

我们希望计算 ∂L∂W1\frac{\partial \mathcal{L}}{\partial \mathbf{W}_1}W1L, ∂L∂W2\frac{\partial \mathcal{L}}{\partial \mathbf{W}_2}W2L 等。

步骤 1:计算输出层梯度

z2=w2⊤h+b2z_2 = \mathbf{w}_2^\top \mathbf{h} + b_2z2=w2h+b2,则:

∂L∂z2=y^−y(这是 BCE + Sigmoid 的优雅性质!) \frac{\partial \mathcal{L}}{\partial z_2} = \hat{y} - y \quad \text{(这是 BCE + Sigmoid 的优雅性质!)} z2L=y^y(这是 BCE + Sigmoid 的优雅性质!)

于是:

∂L∂w2=∂L∂z2⋅∂z2∂w2=(y^−y)⋅h \frac{\partial \mathcal{L}}{\partial \mathbf{w}_2} = \frac{\partial \mathcal{L}}{\partial z_2} \cdot \frac{\partial z_2}{\partial \mathbf{w}_2} = (\hat{y} - y) \cdot \mathbf{h} w2L=z2Lw2z2=(y^y)h

∂L∂b2=y^−y \frac{\partial \mathcal{L}}{\partial b_2} = \hat{y} - y b2L=y^y

步骤 2:传播到隐藏层

⚠️ 踩坑记录:

我在实际项目中遇到过一个问题,这个配置在开发环境正常,但生产环境会报错。 后来发现是因为生产环境的版本不一致导致的。建议大家在部署前一定要检查版本兼容性。

z1=W1x+b1\mathbf{z}_1 = \mathbf{W}_1 \mathbf{x} + \mathbf{b}_1z1=W1x+b1h=ReLU(z1)\mathbf{h} = \text{ReLU}(\mathbf{z}_1)h=ReLU(z1)

首先计算 ∂L∂h\frac{\partial \mathcal{L}}{\partial \mathbf{h}}hL

∂L∂h=∂L∂z2⋅∂z2∂h=(y^−y)⋅w2⊤ \frac{\partial \mathcal{L}}{\partial \mathbf{h}} = \frac{\partial \mathcal{L}}{\partial z_2} \cdot \frac{\partial z_2}{\partial \mathbf{h}} = (\hat{y} - y) \cdot \mathbf{w}_2^\top hL=z2Lhz2=(y^y)w2

然后,由于 ReLU 的导数为:

ddzReLU(z)={1,z>00,z≤0=I(z>0) \frac{d}{dz} \text{ReLU}(z) = \begin{cases} 1, & z > 0 \\ 0, & z \leq 0 \end{cases} = \mathbb{I}(z > 0) dzdReLU(z)={1,0,z>0z0=I(z>0)

所以:

∂L∂z1=∂L∂h⊙I(z1>0) \frac{\partial \mathcal{L}}{\partial \mathbf{z}_1} = \frac{\partial \mathcal{L}}{\partial \mathbf{h}} \odot \mathbb{I}(\mathbf{z}_1 > 0) z1L=hLI(z1>0)

其中 ⊙\odot 表示逐元素相乘(Hadamard product)。

最后:

∂L∂W1=∂L∂z1⋅x⊤ \frac{\partial \mathcal{L}}{\partial \mathbf{W}_1} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}_1} \cdot \mathbf{x}^\top W1L=z1Lx

∂L∂b1=∂L∂z1 \frac{\partial \mathcal{L}}{\partial \mathbf{b}_1} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}_1} b1L=z1L

反向传播的本质
从输出层开始,逐层反向计算“误差信号”(即 ∂L∂z(l)\frac{\partial \mathcal{L}}{\partial \mathbf{z}^{(l)}}z(l)L),再用它计算当前层参数的梯度。

4.3 一般形式:向量化反向传播

对第 lll 层,定义误差项(error term):

δ(l)=∂L∂Z(l) \boldsymbol{\delta}^{(l)} = \frac{\partial \mathcal{L}}{\partial \mathbf{Z}^{(l)}} δ(l)=Z(l)L

则:

  • 输出层(假设用 Softmax + CE):
    δ(L)=A(L)−Y \boldsymbol{\delta}^{(L)} = \mathbf{A}^{(L)} - \mathbf{Y} δ(L)=A(L)Y

  • 隐藏层(l<Ll < Ll<L):
    δ(l)=(W(l+1)δ(l+1))⊙σ′(l)(Z(l)) \boldsymbol{\delta}^{(l)} = \left( \mathbf{W}^{(l+1)} \boldsymbol{\delta}^{(l+1)} \right) \odot \sigma'^{(l)}(\mathbf{Z}^{(l)}) δ(l)=(W(l+1)δ(l+1))σ(l)(Z(l))

  • 参数梯度:
    ∂L∂W(l)=A(l−1)⊤δ(l) \frac{\partial \mathcal{L}}{\partial \mathbf{W}^{(l)}} = \mathbf{A}^{(l-1)\top} \boldsymbol{\delta}^{(l)} W(l)L=A(l1)δ(l)
    ∂L∂b(l)=∑i=1mδi(l)(按样本求和) \frac{\partial \mathcal{L}}{\partial \mathbf{b}^{(l)}} = \sum_{i=1}^{m} \boldsymbol{\delta}^{(l)}_i \quad \text{(按样本求和)} b(l)L=i=1mδi(l)(按样本求和)

反向传播的计算复杂度与前向传播同阶,这是它被广泛采用的关键原因。


五、动手实现:从零构建一个全连接神经网络

我们将用 NumPy 实现一个支持任意层数的 MLP,并用于手写数字分类(MNIST 简化版)。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
class MLP:
def __init__(self, layer_sizes, learning_rate=0.01, epochs=1000):
"""
layer_sizes: list, e.g., [64, 32, 10] for input=64, hidden=32, output=10
"""
self.layer_sizes = layer_sizes
self.lr = learning_rate
self.epochs = epochs
self.weights = []
self.biases = []
self.losses = []
# 初始化参数(Xavier 初始化)
for i in range(len(layer_sizes) - 1):
w = np.random.randn(layer_sizes[i], layer_sizes[i+1]) * np.sqrt(2.0 / layer_sizes[i])
b = np.zeros((1, layer_sizes[i+1]))
self.weights.append(w)
self.biases.append(b)
def relu(self, z):
return np.maximum(0, z)
def relu_derivative(self, z):
return (z > 0).astype(float)
def softmax(self, z):
exp_z = np.exp(z - np.max(z, axis=1, keepdims=True))  # 防止溢出
return exp_z / np.sum(exp_z, axis=1, keepdims=True)
def cross_entropy(self, y_true, y_pred):
m = y_true.shape[0]
log_likelihood = -np.log(y_pred[range(m), y_true.argmax(axis=1)] + 1e-8)
return np.sum(log_likelihood) / m
def forward(self, X):
activations = [X]
z_values = []
for i in range(len(self.weights)):
z = np.dot(activations[-1], self.weights[i]) + self.biases[i]
z_values.append(z)
if i == len(self.weights) - 1:  # 输出层
a = self.softmax(z)
else:  # 隐藏层
a = self.relu(z)
activations.append(a)
return activations, z_values
def backward(self, X, y, activations, z_values):
m = X.shape[0]
grads_w = [None] * len(self.weights)
grads_b = [None] * len(self.biases)
# 输出层误差
dz = activations[-1] - y  # Softmax + CE 的梯度
grads_w[-1] = np.dot(activations[-2].T, dz) / m
grads_b[-1] = np.sum(dz, axis=0, keepdims=True) / m
# 反向传播隐藏层
for l in range(len(self.weights) - 2, -1, -1):
dz = np.dot(dz, self.weights[l+1].T) * self.relu_derivative(z_values[l])
grads_w[l] = np.dot(activations[l].T, dz) / m
grads_b[l] = np.sum(dz, axis=0, keepdims=True) / m
# 更新参数
for i in range(len(self.weights)):
self.weights[i] -= self.lr * grads_w[i]
self.biases[i] -= self.lr * grads_b[i]
def fit(self, X, y):
for epoch in range(self.epochs):
activations, z_values = self.forward(X)
loss = self.cross_entropy(y, activations[-1])
self.losses.append(loss)
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
self.backward(X, y, activations, z_values)
def predict(self, X):
activations, _ = self.forward(X)
return np.argmax(activations[-1], axis=1)
# 加载数据:sklearn digits(1797 张 8x8 手写数字)
digits = load_digits()
X, y = digits.data, digits.target
# 标准化(对神经网络很重要!)
scaler = StandardScaler()
X = scaler.fit_transform(X)
# 转换标签为 one-hot
y_onehot = np.eye(10)[y]
# 划分数据
X_train, X_test, y_train, y_test, y_train_oh, y_test_oh = train_test_split(
X, y, y_onehot, test_size=0.2, random_state=42
)
# 训练模型
mlp = MLP(layer_sizes=[64, 32, 16, 10], learning_rate=0.1, epochs=1000)
mlp.fit(X_train, y_train_oh)
# 评估
y_pred = mlp.predict(X_test)
accuracy = np.mean(y_pred == y_test)
print(f"\nTest Accuracy: {accuracy:.4f}")
# 可视化损失
plt.plot(mlp.losses)
plt.title("Training Loss")
plt.xlabel("Epoch")
plt.ylabel("Cross-Entropy Loss")
plt.show()

关键实现细节

  • 使用 Xavier 初始化np.sqrt(2.0 / fan_in))缓解梯度消失;
  • 标准化输入(StandardScaler)加速收敛;
  • Softmax + Cross-Entropy 的梯度简化为 pred - true
  • 向量化实现高效批量计算。

六、训练神经网络的挑战与解决方案

尽管原理清晰,但训练神经网络常面临以下挑战:

6.1 梯度消失与爆炸(Vanishing/Exploding Gradients)

在深层网络中,反向传播的梯度是多个矩阵乘积:

∂L∂W(1)∝W(2)W(3)⋯W(L) \frac{\partial \mathcal{L}}{\partial \mathbf{W}^{(1)}} \propto \mathbf{W}^{(2)} \mathbf{W}^{(3)} \cdots \mathbf{W}^{(L)} W(1)LW(2)W(3)W(L)

若权重初始化不当:

  • ∣W∣<1|\mathbf{W}| < 1W<1 → 梯度指数级衰减(消失);
  • ∣W∣>1|\mathbf{W}| > 1W>1 → 梯度指数级增长(爆炸)。

解决方案

  • 合理初始化:Xavier(Sigmoid/Tanh)、He(ReLU);
  • 使用 ReLU:避免 Sigmoid/Tanh 的饱和区;
  • Batch Normalization:对每层输入标准化,稳定分布;
  • 残差连接(ResNet):允许梯度直接跨层流动。

6.2 过拟合(Overfitting)

神经网络参数量大,极易过拟合小数据集。

正则化技术

  • L2 正则化(Weight Decay):
    Ltotal=Ldata+λ∑l∥W(l)∥F2 \mathcal{L}_{\text{total}} = \mathcal{L}_{\text{data}} + \lambda \sum_{l} \|\mathbf{W}^{(l)}\|_F^2 Ltotal=Ldata+λlW(l)F2
  • Dropout:训练时随机“关闭”一部分神经元(如 50%),强制网络不依赖特定路径;
  • 早停(Early Stopping):监控验证集损失,不再下降时停止;
  • 数据增强:对图像旋转、裁剪、加噪等。

6.3 优化困难:局部极小与鞍点

高维损失 landscape 存在大量鞍点(saddle points),而非局部极小。

现代优化器

  • SGD with Momentum
    vt=βvt−1+(1−β)∇Ltθt=θt−1−ηvt \mathbf{v}_t = \beta \mathbf{v}_{t-1} + (1 - \beta) \nabla \mathcal{L}_t \\ \theta_t = \theta_{t-1} - \eta \mathbf{v}_t vt=βvt1+(1β)Ltθt=θt1ηvt
    动量帮助穿越平坦区域。

  • Adam(Adaptive Moment Estimation):
    mt=β1mt−1+(1−β1)∇Ltvt=β2vt−1+(1−β2)(∇Lt)2m^t=mt1−β1t,v^t=vt1−β2tθt=θt−1−ηm^tv^t+ϵ \mathbf{m}_t = \beta_1 \mathbf{m}_{t-1} + (1 - \beta_1) \nabla \mathcal{L}_t \\ \mathbf{v}_t = \beta_2 \mathbf{v}_{t-1} + (1 - \beta_2) (\nabla \mathcal{L}_t)^2 \\ \hat{\mathbf{m}}_t = \frac{\mathbf{m}_t}{1 - \beta_1^t}, \quad \hat{\mathbf{v}}_t = \frac{\mathbf{v}_t}{1 - \beta_2^t} \\ \theta_t = \theta_{t-1} - \eta \frac{\hat{\mathbf{m}}_t}{\sqrt{\hat{\mathbf{v}}_t} + \epsilon} mt=β1mt1+(1β1)Ltvt=β2vt1+(1β2)(Lt)2m^t=1β1tmt,v^t=1β2tvtθt=θt1ηv^t+ϵm^t
    Adam 自适应调整每个参数的学习率,是目前最常用的优化器。


七、使用 PyTorch 构建现代神经网络

在实践中,我们使用深度学习框架(如 PyTorch)而非手动实现。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# 转换数据为 PyTorch 张量
X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.long)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.long)
train_loader = DataLoader(TensorDataset(X_train_t, y_train_t), batch_size=32, shuffle=True)
# 定义模型
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(64, 32)
self.fc2 = nn.Linear(32, 16)
self.fc3 = nn.Linear(16, 10)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.2)
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.dropout(x)
x = self.relu(self.fc2(x))
x = self.dropout(x)
x = self.fc3(x)  # 无需 Softmax,CrossEntropyLoss 内部处理
return x
model = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练循环
for epoch in range(100):
model.train()
for batch_x, batch_y in train_loader:
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
if epoch % 20 == 0:
model.eval()
with torch.no_grad():
test_outputs = model(X_test_t)
pred = test_outputs.argmax(dim=1)
acc = (pred == y_test_t).float().mean().item()
print(f"Epoch {epoch}, Test Acc: {acc:.4f}")

PyTorch 优势

  • 自动微分(loss.backward());
  • GPU 加速(.to('cuda'));
  • 丰富的预定义层(Linear, Conv2d, LSTM 等);
  • 动态计算图,调试友好。

八、可解释性:揭开神经网络的黑箱

神经网络常被诟病为“黑箱”,但近年来出现了多种解释方法。

8.1 全局可解释性

  • 特征重要性:通过扰动输入特征,观察输出变化;
  • 激活可视化:对隐藏层神经元,找出使其激活最大的输入(如 DeepDream)。

8.2 局部可解释性:LIME 与 SHAP

  • LIME(Local Interpretable Model-agnostic Explanations):

    • 在预测点附近生成扰动样本;
    • 用简单模型(如线性回归)拟合局部行为;
    • 解释该简单模型的系数。
  • SHAP(SHapley Additive exPlanations):

    • 基于博弈论,计算每个特征对预测的边际贡献;
    • 对神经网络,可用 DeepSHAPIntegrated Gradients 近似。
# 使用 SHAP 解释 PyTorch 模型
import shap
explainer = shap.DeepExplainer(model, X_train_t[:100])  # 背景数据
shap_values = explainer.shap_values(X_test_t[:1])
shap.image_plot(shap_values, -X_test_t[:1].reshape(1, 8, 8))  # 对图像 reshape

8.3 注意力机制(Attention)

在 NLP 和 CV 中,注意力权重本身提供了解释性:

“模型关注了句子中的‘not’,因此将情感判断为负面。”


九、神经网络 vs 传统模型:何时选择谁?

维度神经网络传统模型(XGBoost 等)
数据类型图像、文本、语音、图、序列结构化表格数据
数据规模需大量数据(>10k 样本)小数据也能 work
特征工程自动学习特征表示依赖人工特征工程
可解释性黑箱(需额外工具)相对透明(树路径、系数)
训练资源需 GPU、调参复杂CPU 即可,调参简单
默认性能在非结构化数据上碾压在表格数据上 often 更优

经验法则

  • 结构化数据:先试 XGBoost/LightGBM;
  • 图像/文本/语音:直接上 CNN/Transformer;
  • 混合数据:用神经网络处理非结构化部分,传统模型处理表格部分,再融合。

十、进阶方向:从 MLP 到现代架构

全连接网络(MLP)只是起点,现代深度学习包含:

  • 卷积神经网络(CNN):利用局部性和平移不变性处理图像;
  • 循环神经网络(RNN/LSTM):处理序列数据;
  • Transformer:通过自注意力机制统一处理各类序列;
  • 图神经网络(GNN):处理图结构数据;
  • 自监督学习:在无标签数据上预训练(如 BERT、SimCLR)。

这些架构的核心思想仍是:堆叠可微模块 + 端到端训练


十一、结语:函数逼近的艺术

神经网络不是魔法,而是一种工程化的函数逼近技术。它的力量源于:

  • 通用逼近能力:理论上可拟合任意函数;
  • 端到端学习:从原始输入到最终输出,无需中间表示;
  • 可扩展性:增加层数/宽度即可提升容量。

但也要清醒认识到:

  • 它需要大量数据和算力;
  • 它缺乏因果推理能力;
  • 它的决策过程难以审计。

最好的实践,是在理解其原理的基础上,审慎选择工具,负责任地应用

下一篇文章,我们将进入无监督学习的世界——那里没有标签,只有数据自身的结构等待发现。

但在那之前,请记住:

模型的价值,不在于它有多深,而在于它解决了什么问题


行动建议

  1. 在 MNIST 或 Fashion-MNIST 上复现 MLP,并尝试不同激活函数、初始化方法
  2. 对比 MLP 与 XGBoost 在 digits 数据集上的性能与训练时间
  3. 使用 SHAP 或 LIME 解释一个神经网络的预测结果
  4. 尝试添加 BatchNorm、Dropout,观察对过拟合的影响
  5. 阅读《Deep Learning》(Goodfellow et al.)第 6 章“深度前馈网络”

真正的理解,始于亲手实现,成于反复实验。