在这篇教程中,我会介绍 PyTorch 这个强大的深度学习框架,并通过使用它实现一个基于 MNIST 数据集的多层神经网络,帮助读者了解神经网络的基本原理和实现过程。我们将使用 CPU 版本的 PyTorch ,清除较为复杂的 GPU 配置过程,以便初学者更容易上手。对于需要 GPU 加速的用户,可以参考每个关键部分中的 CUDA 声明,以实现 GPU 训练和推理。
环境配置
在使用此教程前,您需要配置基础环境:
-
安装 Python (建议使用 Python 3.8 或更高版本)
-
安装 PyTorch:
bashpip install torch torchvision
数据集获取
MNIST 数据集包含手写数字图片,是机器学习和深度学习中的经典数据集。我们通过 torchvision.datasets.MNIST
类下载并加载数据,同时应用标准化转换,以提升模型的收敛速度和性能。
为了确保数据只被下载一次,我们在文件存储时进行检查:
python
import os
from torchvision import datasets, transforms
import torch
# 设定数据文件夹
DATA_DIR = './data'
if not os.path.exists(DATA_DIR):
os.makedirs(DATA_DIR)
# 自定义数据处理模块,将图像标准化
transform = transforms.Compose([
transforms.ToTensor(), # 转换为线性描述
transforms.Normalize((0.1307,), (0.3081,)) # 标准化
])
# 获取 MNIST 训练集和测试集
train_dataset = datasets.MNIST(root=DATA_DIR, train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root=DATA_DIR, train=False, transform=transform)
# 定义设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
CUDA 声明 : 使用
DataLoader
后在每个批次中将数据移动到 CUDA 设备,而不是直接移动整个数据集。
构建多层神经网络
神经网络由层组成,每一层由若干个神经元构成,连接权重和偏置项用于确定输入数据如何在网络中传播。我们使用 torch.nn.Module
定义网络结构,并通过 forward
函数描述数据的前向传播过程。
python
import torch
import torch.nn as nn
class NeuralNetwork(nn.Module):
"""
一个基本的多层神经网络:
- 28x28 输入,128 个节点的一层全连
- ReLU 激活
- 64 节点的第二层全连
- 10 个节点用于输出
"""
def __init__(self):
super(NeuralNetwork, self).__init__()
self.fc1 = nn.Linear(28*28, 128) # 输入层到28*28图像转为向量
self.fc2 = nn.Linear(128, 64) # 第二层
self.fc3 = nn.Linear(64, 10) # 最终输出 10 个分类结果
def forward(self, x):
x = x.view(-1, 28*28) # 将图像抽成一个长向量
x = torch.relu(self.fc1(x)) # 使用 ReLU 激活函数
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
model = NeuralNetwork()
model = model.to(device) # 将模型移动到设备上
定义损失函数和优化器
损失函数用于度量模型输出与真实标签之间的差距。优化器通过最小化损失函数调整网络参数。我们选用交叉熵损失(nn.CrossEntropyLoss
)和随机梯度下降(optim.SGD
)优化器。
python
import torch.optim as optim
criterion = nn.CrossEntropyLoss() # 用于分类任务
optimizer = optim.SGD(model.parameters(), lr=0.01) # 使用 SGD 优化
criterion = criterion.to(device) # 将损失函数移动到 CUDA 设备
深度学习
训练过程包括前向传播、计算损失、反向传播和参数更新。我们通过 DataLoader
将数据分批次输入模型,逐步优化模型性能。
python
from torch.utils.data import DataLoader
train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
# 训练过程
num_epochs = 10
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device) # 将数据移动到 CUDA 设备
optimizer.zero_grad() # 清空梯度
outputs = model(images)
loss = criterion(outputs, labels) # 计算损失
loss.backward() # 进行反向传播
optimizer.step() # 更新参数
running_loss += loss.item()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}')
测试结果
模型评估阶段不需要更新参数,因此使用 torch.no_grad()
禁止梯度计算。通过 torch.max()
找到模型预测的类别,计算准确率。
python
test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)
model.eval()
correct = 0
total = 0
with torch.no_grad(): # 禁止更新参数
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device) # 将数据移动到 CUDA 设备
outputs = model(images)
_, predicted = torch.max(outputs.data, 1) # 进行分类预测
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Test Accuracy: {100 * correct / total:.2f}%')
原理认识
- ReLU 激活函数: 应用于神经网络层之间,能够有效地减少静态分布问题,并能增强网络泛化能力。
- CrossEntropyLoss: 适合于多分类任务,通过计算分类误差以寻求最优训练结果。
- SGD 优化器: 简单但有效的参数更新方法,在大部分场景中表现良好。
手写数字识别小程序
我们将使用 tkinter
创建一个简洁现代的 GUI,用户可以在画板上手写数字,然后点击按钮让模型识别数字。程序会展示预测结果,方便用户了解模型的识别能力。
python
import tkinter as tk
from PIL import Image, ImageDraw
import numpy as np
import torch
class DigitRecognizerApp:
def __init__(self, model):
self.model = model # 加载预训练的数字识别模型
self.window = tk.Tk() # 创建主窗口
self.window.title("手写数字识别") # 设置窗口标题
# 创建画板,用于手写数字
self.canvas = tk.Canvas(self.window, width=280, height=280, bg='white')
self.canvas.grid(row=0, column=0, columnspan=3) # 将画板放置在第一行,横跨3列
# 绑定鼠标左键拖动事件,用于绘制数字
self.canvas.bind('<B1-Motion>', self.draw_digit)
# 创建清除按钮
self.clear_button = tk.Button(self.window, text='清除', command=self.clear_canvas)
self.clear_button.grid(row=1, column=0) # 将清除按钮放置在第二行第一列
# 创建识别按钮
self.recognize_button = tk.Button(self.window, text='识别', command=self.recognize_digit)
self.recognize_button.grid(row=1, column=1) # 将识别按钮放置在第二行第二列
# 创建结果显示标签
self.result_label = tk.Label(self.window, text="识别结果:", font=("Arial", 16))
self.result_label.grid(row=1, column=2) # 将结果显示标签放置在第二行第三列
# 初始化图像和绘图工具
self.image = Image.new('L', (28, 28), color=0) # 创建一个28x28的黑色图像
self.draw = ImageDraw.Draw(self.image) # 创建绘图对象
def draw_digit(self, event):
"""在画板上绘制数字,并在内存中的图像上同步绘制"""
x, y = event.x, event.y # 获取鼠标当前位置
self.canvas.create_oval(x-5, y-5, x+5, y+5, fill='black') # 在画板上绘制黑色圆点
self.draw.ellipse([(x/10, y/10), (x/10+1, y/10+1)], fill=255) # 在内存中的图像上绘制白色圆点
def clear_canvas(self):
"""清除画板和内存中的图像"""
self.canvas.delete('all') # 清除画板上的所有内容
self.image = Image.new('L', (28, 28), color=0) # 重新初始化图像为黑色
self.draw = ImageDraw.Draw(self.image) # 重新创建绘图对象
self.result_label.config(text="识别结果:") # 清空识别结果
def recognize_digit(self):
"""识别画板上的数字并显示结果"""
image_array = np.array(self.image).reshape(1, 1, 28, 28) / 255.0 # 将图像转换为模型输入格式
image_tensor = torch.tensor(image_array, dtype=torch.float32) # 将图像转换为张量
with torch.no_grad(): # 禁用梯度计算
output = self.model(image_tensor) # 使用模型进行预测
predicted_digit = torch.argmax(output).item() # 获取预测结果
self.result_label.config(text=f"识别结果:{predicted_digit}") # 更新结果显示标签
def run(self):
"""运行应用程序"""
self.window.mainloop() # 进入主事件循环
app = DigitRecognizerApp(model) # 传入模型
app.run() # 运行应用程序
结论
通过这次学习,读者应对 PyTorch 实现多层神经网络有了基本了解,可以尝试增加更深的层数,使用更复杂的激活函数,以改善结果。