第4章:深度前馈网络(Deep Feedforward Networks)
一、导读
1.1 本章学习目标
第4章是深度学习的核心章节,正式引入神经网络的基本架构——前馈网络(Feedforward Network),也称为多层感知器(MLP)。前馈网络是深度学习中最基础的模型,后续的卷积网络、循环网络等都是在其基础上发展而来。
本章的核心学习目标包括:
- 理解前馈网络的基本结构:输入层、隐藏层、输出层的组织方式
- 掌握激活函数的作用与选择:ReLU、Sigmoid、Tanh等激活函数的特性
- 理解万能近似定理:为什么神经网络能拟合任意连续函数
- 掌握反向传播算法:链式法则在计算图中的应用
- 理解前向传播与反向传播的计算流程
- 掌握网络设计的基本原则:层数、宽度、参数初始化的选择
1.2 为什么本章如此重要
前馈网络是深度学习的"原子模型"。几乎所有复杂的深度学习架构都可以分解为前馈网络的基本组件:线性变换 + 非线性激活。
- 卷积网络:每一层的卷积操作 + 激活函数就是一个局部连接的前馈网络
- 循环网络:展开后等价于一个很深的前馈网络
- Transformer:前馈网络是其核心组件之一(FFN层)
- 残差网络:可以看作带有跳跃连接的前馈网络
不理解前馈网络的工作原理,就无法理解任何深度学习模型。反向传播算法更是所有深度学习框架的核心引擎。
1.3 核心问题引导
为什么神经网络需要非线性激活函数?去掉激活函数会怎样?
万能近似定理说神经网络能拟合任意函数,那为什么还需要"深度"?
反向传播的链式法则如何避免组合爆炸?计算图的作用是什么?
为什么ReLU比Sigmoid更常用?梯度消失问题如何解决?
参数初始化为什么重要?Xavier和He初始化的原理是什么?
如何选择网络的层数和每层的宽度?
二、核心概念详解
2.1 前馈网络的基本结构
2.1.1 从线性模型到神经网络
线性模型只能学习线性映射。通过叠加非线性变换,神经网络可以学习任意复杂的非线性映射。
import numpy as np
# 单层线性模型(无法解决XOR问题)
def linear_model(X, W, b):
return X @ W + b
# XOR问题
X_xor = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_xor = np.array([[0], [1], [1], [0]])
# 线性模型无法拟合XOR
W = np.random.randn(2, 1) * 0.1
b = np.zeros((1, 1))
y_pred = linear_model(X_xor, W, b)
print(f"线性模型预测: {y_pred.ravel()}")
print(f"真实标签: {y_xor.ravel()}")
# 无法正确分类
# 带隐藏层的神经网络(可以解决XOR)
def two_layer_network(X, W1, b1, W2, b2):
h = np.maximum(0, X @ W1 + b1) # ReLU激活
return h @ W2 + b2
W1 = np.array([[1, 1], [1, 1]])
b1 = np.array([[0, -1]])
W2 = np.array([[1], [-2]])
b2 = np.array([[0]])
y_pred = two_layer_network(X_xor, W1, b1, W2, b2)
print(f"\n两层网络预测: {y_pred.ravel()}")
# 可以正确分类XOR2.1.2 多层感知器(MLP)
import torch
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, input_dim, hidden_dims, output_dim):
super().__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.append(nn.Linear(prev_dim, hidden_dim))
layers.append(nn.ReLU())
prev_dim = hidden_dim
layers.append(nn.Linear(prev_dim, output_dim))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
# 创建一个3隐藏层的MLP
model = MLP(784, [256, 128, 64], 10)
print(model)
# 查看参数量
total_params = sum(p.numel() for p in model.parameters())
print(f"\n总参数量: {total_params:,}")2.2 激活函数
2.2.1 为什么需要非线性
如果没有激活函数(或激活函数是线性的),多层的叠加等价于单层:
# 没有激活函数的多层网络等价于单层
# y = W2(W1*x + b1) + b2 = (W2*W1)*x + (W2*b1 + b2) = W'*x + b'
# 无论多少层,都等价于一个线性变换2.2.2 常见激活函数
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
x = torch.linspace(-5, 5, 200)
# 各种激活函数
activations = {
'Sigmoid': torch.sigmoid(x),
'Tanh': torch.tanh(x),
'ReLU': F.relu(x),
'Leaky ReLU': F.leaky_relu(x, 0.1),
'ELU': F.elu(x),
'GELU': F.gelu(x),
}
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 函数图像
for name, y in activations.items():
axes[0].plot(x.numpy(), y.detach().numpy(), label=name)
axes[0].set_title('激活函数')
axes[0].set_xlabel('x')
axes[0].set_ylabel('f(x)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 梯度图像
for name, y in activations.items():
y.backward(torch.ones_like(y))
axes[1].plot(x.numpy(), x.grad.numpy(), label=name)
x.grad.zero_()
axes[1].set_title('激活函数的梯度')
axes[1].set_xlabel('x')
axes[1].set_ylabel("f'(x)")
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('activation_functions.png', dpi=150)
plt.show()2.2.3 激活函数的选择
| 激活函数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Sigmoid | 输出概率,平滑 | 梯度消失,非零中心 | 二分类输出层 |
| Tanh | 零中心,比sigmoid好 | 梯度消失 | RNN |
| ReLU | 计算快,缓解梯度消失 | Dead ReLU | 隐藏层(默认选择) |
| Leaky ReLU | 解决Dead ReLU | 效果不稳定 | 需要时尝试 |
| GELU | 平滑,效果好 | 计算稍慢 | Transformer |
# ReLU的Dead ReLU问题
import torch.nn as nn
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
relu = nn.ReLU()
print(f"输入: {x}")
print(f"ReLU输出: {relu(x)}")
# 负值被截断为0,梯度也为0
# Leaky ReLU
leaky_relu = nn.LeakyReLU(0.01)
print(f"Leaky ReLU输出: {leaky_relu(x)}")
# 负值保留一个小梯度2.3 万能近似定理
2.3.1 定理内容
万能近似定理(Universal Approximation Theorem)指出:一个具有单个隐藏层的前馈网络,只要隐藏层神经元足够多,就能以任意精度逼近任意连续函数。
# 万能近似定理的直观演示
import numpy as np
import torch
import torch.nn as nn
# 目标函数:复杂的非线性函数
def target_function(x):
return np.sin(x) + np.sin(3*x) + 0.5*np.cos(5*x)
# 单隐藏层网络
class UniversalApproximator(nn.Module):
def __init__(self, n_hidden):
super().__init__()
self.fc1 = nn.Linear(1, n_hidden)
self.fc2 = nn.Linear(n_hidden, 1)
def forward(self, x):
h = torch.tanh(self.fc1(x))
return self.fc2(h)
# 不同隐藏层宽度
x = torch.linspace(-3, 3, 200).unsqueeze(1)
y_true = target_function(x.numpy())
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for idx, n_hidden in enumerate([5, 20, 100]):
model = UniversalApproximator(n_hidden)
# 简单训练
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
y_target = torch.tensor(y_true, dtype=torch.float32)
for epoch in range(2000):
optimizer.zero_grad()
y_pred = model(x)
loss = nn.functional.mse_loss(y_pred, y_target)
loss.backward()
optimizer.step()
with torch.no_grad():
y_pred = model(x).numpy()
axes[idx].plot(x.numpy(), y_true, 'b-', label='目标函数')
axes[idx].plot(x.numpy(), y_pred, 'r--', label='网络拟合')
axes[idx].set_title(f'隐藏层宽度={n_hidden}\nMSE={loss.item():.6f}')
axes[idx].legend()
plt.tight_layout()
plt.savefig('universal_approximation.png', dpi=150)
plt.show()2.3.2 为什么还需要"深度"
万能近似定理保证宽度足够就能拟合任意函数,但实践中深度网络远比宽度网络高效:
- 参数效率:深度网络用指数级少的参数实现复杂函数
- 特征层次:深层可以学习从低级到高级的层次化特征
- 组合表达:深度网络通过组合简单特征来表达复杂概念
2.4 反向传播算法
2.4.1 计算图
# 计算图示例
# f(x, y, z) = (x + y) * z
x = 2.0
y = 3.0
z = -1.0
# 前向传播
q = x + y # q = 5
f = q * z # f = -5
# 反向传播(链式法则)
# df/df = 1
# df/dz = q = 5
# df/dq = z = -1
# df/dx = df/dq * dq/dx = -1 * 1 = -1
# df/dy = df/dq * dq/dy = -1 * 1 = -1
print(f"f = {f}")
print(f"df/dx = -1, df/dy = -1, df/dz = 5")2.4.2 PyTorch中的自动微分
import torch
# PyTorch的自动微分
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)
z = torch.tensor(-1.0, requires_grad=True)
# 前向传播
q = x + y
f = q * z
# 反向传播
f.backward()
print(f"df/dx = {x.grad}") # -1
print(f"df/dy = {y.grad}") # -1
print(f"df/dz = {z.grad}") # 52.4.3 手动实现反向传播
import numpy as np
class SimpleNet:
def __init__(self, input_dim, hidden_dim, output_dim):
self.W1 = np.random.randn(input_dim, hidden_dim) * 0.01
self.b1 = np.zeros(hidden_dim)
self.W2 = np.random.randn(hidden_dim, output_dim) * 0.01
self.b2 = np.zeros(output_dim)
def relu(self, x):
return np.maximum(0, x)
def softmax(self, x):
exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))
return exp_x / np.sum(exp_x, axis=1, keepdims=True)
def forward(self, X):
self.X = X
self.z1 = X @ self.W1 + self.b1
self.a1 = self.relu(self.z1)
self.z2 = self.a1 @ self.W2 + self.b2
self.probs = self.softmax(self.z2)
return self.probs
def backward(self, y, learning_rate=0.01):
batch_size = self.X.shape[0]
# 输出层梯度
dz2 = self.probs.copy()
dz2[np.arange(batch_size), y] -= 1
dz2 /= batch_size
# 第二层梯度
dW2 = self.a1.T @ dz2
db2 = np.sum(dz2, axis=0)
# 隐藏层梯度
da1 = dz2 @ self.W2.T
dz1 = da1 * (self.z1 > 0) # ReLU的梯度
# 第一层梯度
dW1 = self.X.T @ dz1
db1 = np.sum(dz1, axis=0)
# 更新参数
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
# 测试
net = SimpleNet(4, 10, 3)
X = np.random.randn(32, 4)
y = np.random.randint(0, 3, 32)
for epoch in range(100):
probs = net.forward(X)
loss = -np.mean(np.log(probs[np.arange(32), y] + 1e-10))
net.backward(y, learning_rate=0.01)
if epoch % 20 == 0:
accuracy = np.mean(np.argmax(probs, axis=1) == y)
print(f"Epoch {epoch}: loss={loss:.4f}, accuracy={accuracy:.2%}")2.5 参数初始化
2.5.1 为什么初始化重要
不好的初始化会导致梯度消失或梯度爆炸,使深层网络无法训练。
import torch
import torch.nn as nn
# 不同的初始化方法
def create_model(init_method='xavier'):
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 10)
)
for m in model.modules():
if isinstance(m, nn.Linear):
if init_method == 'xavier':
nn.init.xavier_uniform_(m.weight)
elif init_method == 'he':
nn.init.kaiming_uniform_(m.weight, mode='fan_in', nonlinearity='relu')
elif init_method == 'zeros':
nn.init.zeros_(m.weight)
nn.init.zeros_(m.bias)
return model
# 比较不同初始化的梯度流
for method in ['xavier', 'he', 'zeros']:
model = create_model(method)
x = torch.randn(32, 784)
# 前向传播
out = model(x)
loss = out.sum()
# 反向传播
loss.backward()
# 检查梯度
grads = [p.grad.norm().item() for p in model.parameters() if p.grad is not None]
print(f"{method:8s} 初始化: 梯度范数 = {[f'{g:.4f}' for g in grads]}")2.5.2 Xavier 和 He 初始化
import numpy as np
def xavier_init(fan_in, fan_out):
"""Xavier初始化:适用于tanh/sigmoid"""
std = np.sqrt(2.0 / (fan_in + fan_out))
return np.random.randn(fan_in, fan_out) * std
def he_init(fan_in, fan_out):
"""He初始化:适用于ReLU"""
std = np.sqrt(2.0 / fan_in)
return np.random.randn(fan_in, fan_out) * std
# 比较方差
fan_in, fan_out = 256, 128
print(f"Xavier std: {np.sqrt(2.0/(fan_in+fan_out)):.4f}")
print(f"He std: {np.sqrt(2.0/fan_in):.4f}")三、实战案例
3.1 案例一:MNIST手写数字分类
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 数据准备
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)
# 模型定义
class MNISTClassifier(nn.Module):
def __init__(self):
super().__init__()
self.network = nn.Sequential(
nn.Linear(784, 512),
nn.BatchNorm1d(512),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(512, 256),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 10)
)
def forward(self, x):
x = x.view(x.size(0), -1)
return self.network(x)
# 训练
model = MNISTClassifier()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
def train_epoch(model, loader, criterion, optimizer):
model.train()
total_loss = 0
correct = 0
total = 0
for data, target in loader:
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
total_loss += loss.item()
pred = output.argmax(dim=1)
correct += pred.eq(target).sum().item()
total += target.size(0)
return total_loss / len(loader), correct / total
def test(model, loader, criterion):
model.eval()
total_loss = 0
correct = 0
total = 0
with torch.no_grad():
for data, target in loader:
output = model(data)
loss = criterion(output, target)
total_loss += loss.item()
pred = output.argmax(dim=1)
correct += pred.eq(target).sum().item()
total += target.size(0)
return total_loss / len(loader), correct / total
# 训练循环
for epoch in range(10):
train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer)
test_loss, test_acc = test(model, test_loader, criterion)
scheduler.step()
print(f"Epoch {epoch+1}: "
f"Train Loss={train_loss:.4f}, Train Acc={train_acc:.4f}, "
f"Test Loss={test_loss:.4f}, Test Acc={test_acc:.4f}")3.2 案例二:可视化隐藏层的特征学习
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# 训练一个简单的MLP
model = nn.Sequential(
nn.Linear(784, 64),
nn.ReLU(),
nn.Linear(64, 10)
)
# 可视化第一层权重(每个隐藏神经元学习的"滤波器")
with torch.no_grad():
W1 = model[0].weight.numpy() # 64 x 784
fig, axes = plt.subplots(8, 8, figsize=(12, 12))
for i, ax in enumerate(axes.flat):
if i < W1.shape[0]:
ax.imshow(W1[i].reshape(28, 28), cmap='RdBu_r')
ax.axis('off')
plt.suptitle('第一层隐藏神经元学到的特征', fontsize=14)
plt.tight_layout()
plt.savefig('hidden_features.png', dpi=150)
plt.show()四、本章小结
本章系统介绍了深度前馈网络的核心知识:
基本结构:前馈网络通过多层线性变换和非线性激活的组合,实现复杂的输入-输出映射。
激活函数:非线性激活函数是神经网络表达能力的来源。ReLU是默认选择,GELU在Transformer中广泛使用。
万能近似定理:保证宽度的网络能拟合任意函数,但深度网络在参数效率和特征层次化方面更优。
反向传播:通过链式法则高效计算梯度,是训练所有神经网络的基础算法。
参数初始化:Xavier和He初始化保证前向和反向传播时信号的方差稳定,是训练深层网络的前提。
前馈网络是深度学习的基石。掌握其原理和实现,才能理解更复杂的网络架构。
五、思考与练习
思考题
万能近似定理保证宽度足够就能拟合任意函数,那为什么实践中深度网络更有效?
ReLU的Dead ReLU问题在什么情况下会出现?如何解决?
反向传播中梯度消失和梯度爆炸的根本原因是什么?
Batch Normalization为什么能加速训练?它对梯度流有什么影响?
残差连接(skip connection)如何帮助训练极深的网络?
编程练习
从零实现MLP:不使用PyTorch的nn.Module,用NumPy实现一个两层MLP的前向和反向传播。
激活函数对比:在MNIST上比较Sigmoid、Tanh、ReLU、Leaky ReLU的训练效果。
初始化方法对比:比较Xavier、He、零初始化对深层网络训练的影响。
万能近似实验:用单隐藏层网络拟合复杂函数,观察隐藏层宽度对拟合精度的影响。
梯度可视化:训练一个10层MLP,可视化每层的梯度分布,观察梯度消失/爆炸现象。