4深度学习

深度前馈网络

深度学习的基础架构

前馈网络激活函数反向传播

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 从线性模型到神经网络

线性模型只能学习线性映射。通过叠加非线性变换,神经网络可以学习任意复杂的非线性映射。

python
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()}")
# 可以正确分类XOR

2.1.2 多层感知器(MLP)

python
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 为什么需要非线性

如果没有激活函数(或激活函数是线性的),多层的叠加等价于单层:

python
# 没有激活函数的多层网络等价于单层
# y = W2(W1*x + b1) + b2 = (W2*W1)*x + (W2*b1 + b2) = W'*x + b'
# 无论多少层,都等价于一个线性变换

2.2.2 常见激活函数

python
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
python
# 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)指出:一个具有单个隐藏层的前馈网络,只要隐藏层神经元足够多,就能以任意精度逼近任意连续函数。

python
# 万能近似定理的直观演示
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 计算图

python
# 计算图示例
# 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中的自动微分

python
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}")  # 5

2.4.3 手动实现反向传播

python
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 为什么初始化重要

不好的初始化会导致梯度消失或梯度爆炸,使深层网络无法训练。

python
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 初始化

python
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手写数字分类

python
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 案例二:可视化隐藏层的特征学习

python
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,可视化每层的梯度分布,观察梯度消失/爆炸现象。

4/10深度前馈
0%