基于书中第十章,本节中,我们将深入学习反向传播的原理,并通过MNIST手写数字识别任务,结合PyTorch代码实现,手动编写反向传播逻辑,从而加深对于反向传播内部机制的理解。
神经网络与反向传播的基本概念
神经网络是一种由多层神经元组成的计算模型,每一层神经元通过权重和偏置连接起来。神经网络的学习过程可以分为两个阶段:前向传播和反向传播。
反向传播的核心是链式法则,它通过将误差从输出层传递回输入层,计算每一层参数的梯度,从而指导参数的更新。
MNIST手写数字识别任务
MNIST是一个经典的手写数字识别数据集,包含60,000张训练图像和10,000张测试图像,每张图像是一个28x28的灰度图,标签为0到9的数字。我们的目标是构建一个神经网络,能够正确识别这些手写数字。
反向传播的数学原理
为了更好地理解反向传播,我们需要从数学角度分析它的工作原理。假设我们有一个简单的两层神经网络,输入层大小为784(28x28),隐藏层大小为128,输出层大小为10(对应10个数字类别)。
前向传播
前向传播的计算过程如下:
-
输入层到隐藏层:
其中,X是输入数据,W1是输入层到隐藏层的权重矩阵,b1是偏置,σ是激活函数(如ReLU)。
-
隐藏层到输出层:
其中,W2是隐藏层到输出层的权重矩阵,b2是偏置,softmax函数用于将输出转换为概率分布。
损失函数
我们使用交叉熵损失函数来衡量预测结果与真实标签之间的差异:
其中,yi是真实标签的one-hot编码,a2i是输出层的预测概率。
反向传播
反向传播的目标是计算损失函数对每一层参数的梯度,并更新参数。具体步骤如下:
-
计算输出层的误差:
-
计算隐藏层的误差:
其中,σ′是激活函数的导数。
-
计算梯度并更新参数:
-
使用梯度下降法更新参数:
其中,η是学习率。
代码实现
下面我们通过PyTorch实现一个简单的两层神经网络,并手动编写反向传播逻辑,同时利用CUDA加速训练过程。
- import torch
- from torch.utils.data import DataLoader
- from torchvision import datasets, transforms
-
-
- device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
- print(f'Using device: {device}')
-
-
- transform = transforms.Compose([
- transforms.ToTensor(),
- transforms.Normalize((0.5,), (0.5,))
- ])
-
-
- 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=64, shuffle=False)
-
-
- class SimpleNN:
- def __init__(self, input_size, hidden_size, output_size):
-
- self.W1 = torch.randn(input_size, hidden_size, device=device) * 0.01
- self.b1 = torch.zeros(1, hidden_size, device=device)
- self.W2 = torch.randn(hidden_size, output_size, device=device) * 0.01
- self.b2 = torch.zeros(1, output_size, device=device)
-
- def forward(self, X):
-
- self.z1 = torch.matmul(X, self.W1) + self.b1
- self.a1 = torch.relu(self.z1)
- self.z2 = torch.matmul(self.a1, self.W2) + self.b2
- self.a2 = torch.softmax(self.z2, dim=1)
- return self.a2
-
- def backward(self, X, y, output, learning_rate):
-
- m = X.shape[0]
-
-
- dz2 = output - y
- dW2 = torch.matmul(self.a1.T, dz2) / m
- db2 = torch.sum(dz2, dim=0, keepdim=True) / m
-
-
- dz1 = torch.matmul(dz2, self.W2.T) * (self.a1 > 0).float()
- dW1 = torch.matmul(X.T, dz1) / m
- db1 = torch.sum(dz1, dim=0, keepdim=True) / m
-
-
- self.W2 -= learning_rate * dW2
- self.b2 -= learning_rate * db2
- self.W1 -= learning_rate * dW1
- self.b1 -= learning_rate * db1
-
- def train(self, train_loader, epochs, learning_rate):
- for epoch in range(epochs):
- for images, labels in train_loader:
-
- images = images.view(-1, 28*28).to(device)
-
-
- y = torch.zeros(labels.size(0), 10, device=device)
- y[torch.arange(labels.size(0)), labels] = 1
-
-
- output = self.forward(images)
-
-
- self.backward(images, y, output, learning_rate)
-
- if (epoch+1) % 5 == 0:
- print(f'Epoch [{epoch+1}/{epochs}]')
-
- def evaluate(self, test_loader):
- correct = 0
- total = 0
- for images, labels in test_loader:
- images = images.view(-1, 28*28).to(device)
- outputs = self.forward(images)
- _, predicted = torch.max(outputs.data, 1)
- total += labels.size(0)
- correct += (predicted.cpu() == labels).sum().item()
-
- print(f'Test Accuracy: {100 * correct / total:.2f}%')
-
-
- input_size = 28 * 28
- hidden_size = 128
- output_size = 10
- model = SimpleNN(input_size, hidden_size, output_size)
-
-
- model.train(train_loader, epochs=20, learning_rate=0.1)
-
-
- model.evaluate(test_loader)
在SimpleNN的init部分,W1 和 W2 分别是输入层到隐藏层和隐藏层到输出层的权重矩阵。我们使用 torch.randn 生成服从标准正态分布的随机数,并乘以 0.01 来缩小初始值的范围。b1 和 b2 是偏置,初始化为零。所有参数都被放置在指定的设备(如GPU)上。
在forward()中,实现了前向传播的过程。首先,输入数据 X 与权重矩阵 W1 进行矩阵乘法,再加上偏置 b1,得到隐藏层的加权输入 z1。然后,通过ReLU激活函数对 z1 进行非线性变换,得到隐藏层的激活值 a1。之后,隐藏层的激活值 a1 与权重矩阵 W2 进行矩阵乘法,再加上偏置 b2,得到输出层的加权输入 z2。最后,通过softmax函数将 z2 转换为概率分布 a2,表示每个类别的预测概率。
在反向传播中,通过链式法则,误差从输出层逐层向前传递,计算每一层的梯度。首先,计算输出层的误差 dz2,即预测值 output 与真实标签 y 的差值。然后,计算权重 W2 的梯度 dW2,通过将隐藏层的激活值 a1 的转置与误差 dz2 相乘,并除以样本数 m。偏置 b2 的梯度 db2 是误差 dz2 的均值。之后,计算隐藏层的误差 dz1,通过将输出层的误差 dz2 与权重矩阵 W2 的转置相乘,再乘以ReLU激活函数的导数(即 a1 > 0 的布尔值转换为浮点数)。然后,计算权重 W1 的梯度 dW1,通过将输入数据 X 的转置与误差 dz1 相乘,并除以样本数 m。偏置 b1 的梯度 db1 是误差 dz1 的均值。最后,使用梯度下降法更新参数。权重和偏置分别减去学习率与对应梯度的乘积。
运行结果如下,可以看到通过上述代码,我们能够得到较高的准确率。
深入思考
反向传播是神经网络训练的核心算法,但它并非完美无缺。在实际应用中,我们可能会遇到以下问题:
-
梯度消失:在深层网络中,梯度可能会逐渐变小,导致靠近输入层的参数几乎无法更新。使用ReLU激活函数和批量归一化可以有效缓解这一问题。
-
过拟合:神经网络容易过拟合训练数据。可以通过正则化(如L2正则化)和Dropout来减少过拟合。
-
计算效率:反向传播的计算复杂度较高,尤其是在大规模数据集和深层网络中。使用GPU加速和分布式计算可以显著提高训练速度。
总结
通过MNIST手写数字识别任务,我们从理论和代码两个层面深入探讨了反向传播的原理和实现。不仅学习了反向传播的数学原理,还通过手动编写反向传播逻辑,更好地理解了其内部机制。