初识深度学习
一、引言:从机器学习到深度学习
在之前的学习中,我们学习了决策树、随机森林、SVM等经典机器学习算法。这些算法在很多场景下表现出色,但它们有一个共同的特点:需要人工设计和提取特征。比如,要让计算机识别手写数字,我们需要告诉它“这个笔画是什么形状”“这个区域的像素密度是多少”——这被称为特征工程。
深度学习彻底改变了这一局面。它让神经网络自动从原始数据中学习特征——我们只需要把图片的原始像素喂给网络,网络就能自己学会识别数字。这种“端到端”的学习方式,是深度学习最强大的能力。
本篇博客将基于PyTorch框架,带领大家从零开始构建一个识别手写数字的神经网络。我们将学习张量、数据集加载、神经网络构建、训练循环、损失函数、优化器等核心概念,并通过MNIST数据集完成一个完整的深度学习实战项目。
二、PyTorch与深度学习基础
2.1 什么是PyTorch
PyTorch是当前最流行的深度学习框架之一,由Facebook(现Meta)开源。它以动态计算图为核心,代码风格接近Python原生语法,非常适合研究和开发。
PyTorch生态包含三个核心库:
| 库 | 功能 |
|---|---|
torch | 核心库,提供张量计算、自动求导、神经网络模块 |
torchvision | 视觉工具库,提供数据集、模型、图像变换 |
torchaudio | 音频工具库,提供音频处理和数据集 |
import torch
import torchvision
import torchaudio
print(torch.__version__) # 查看PyTorch版本
print(torchvision.__version__) # 查看torchvision版本
2.2 张量(Tensor)
张量是PyTorch中的核心数据结构,可以理解为多维数组。它与NumPy数组类似,但有两大优势:
-
支持GPU加速:张量可以在GPU上运行,大幅提升计算速度
-
自动求导:张量可以记录计算图,自动计算梯度
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
张量 vs NumPy数组:
| 对比项 | NumPy数组 | PyTorch张量 |
|---|---|---|
| 运行位置 | 仅CPU | CPU/GPU均可 |
| 自动求导 | 不支持 | 支持 |
| 深度学习集成 | 需手动转换 | 原生支持 |
三、MNIST数据集——深度学习的“Hello World”
3.1 MNIST简介
MNIST(Modified National Institute of Standards and Technology)是手写数字识别领域最经典的数据集:
-
70000张手写数字图像(0-9)
-
60000张用于训练,10000张用于测试
-
每张图像为28×28像素的灰度图
-
数字居中显示,减少了预处理工作
3.2 加载数据集
from torchvision import datasets
from torchvision.transforms import ToTensor
# 下载训练数据集
training_data = datasets.MNIST(
root="data", # 数据保存路径
train=True, # 加载训练集
download=True, # 自动下载
transform=ToTensor(), # 转为张量
)
# 下载测试数据集
test_data = datasets.MNIST(
root="data",
train=False,
download=True,
transform=ToTensor()
)
ToTensor()的作用:将PIL图像或NumPy数组转换为PyTorch张量,同时将像素值从0-255缩放到0-1之间。这是因为神经网络对0-1范围的数据更敏感,训练更稳定。
3.3 数据可视化
在训练之前,让我们先看看数据长什么样:
from matplotlib import pyplot as plt
figure = plt.figure()
for i in range(9):
img, label = training_data[i]
figure.add_subplot(3, 3, i+1) # 3×3子图
plt.title(label) # 显示标签
plt.axis('off') # 关闭坐标轴
plt.imshow(img.squeeze(), cmap='gray') # 灰度显示
plt.show()
img.squeeze()的作用是去掉维度为1的维度(从[1, 28, 28]变成[28, 28]),这样plt.imshow()才能正确显示。
四、DataLoader——高效的数据管理
4.1 什么是DataLoader
DataLoader是PyTorch提供的数据加载工具,它帮助我们:
-
批量加载:将数据集分成多个批次(batch),每次训练只加载一批
-
打乱顺序:每个epoch重新打乱数据,避免模型学到顺序规律
-
并行加载:使用多线程加速数据读取
from torch.utils.data import DataLoader
train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
for x, y in test_dataloader:
print(f"Shape of x [N, C, H, W]: {x.shape}") # [64, 1, 28, 28]
print(f"Shape of y: {y.shape} {y.dtype}") # [64] int64
break
批次数据的维度含义:
| 维度 | 含义 | 示例值 |
|---|---|---|
| N | 批次大小 | 64 |
| C | 通道数 | 1(灰度图) |
| H | 高度 | 28 |
| W | 宽度 | 28 |
为什么要分批?
-
减少内存占用(一次处理64张而不是60000张)
-
提高训练速度(GPU并行处理)
-
引入随机性,有助于跳出局部最优
五、设备选择——CPU还是GPU
深度学习模型训练涉及大量矩阵运算,GPU的并行计算能力比CPU强大几十倍甚至上百倍。
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")
这段代码依次检查:
-
CUDA:NVIDIA显卡的GPU加速(Windows/Linux)
-
MPS:苹果M系列芯片的GPU加速(Mac)
-
CPU:都不可用时回退到CPU
重要提醒:模型和数据都必须放到同一个设备上!如果模型在GPU上,数据在CPU上,会报错。
六、构建神经网络
6.1 神经网络类的结构
在PyTorch中,构建神经网络需要继承nn.Module类,并实现两个方法:
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__() # 继承父类初始化
self.flatten = nn.Flatten() # 展平层
self.hidden1 = nn.Linear(28*28, 128) # 输入层→隐藏层1
self.hidden2 = nn.Linear(128, 256) # 隐藏层1→隐藏层2
self.hidden3 = nn.Linear(256, 128) # 隐藏层2→隐藏层3
self.out = nn.Linear(128, 10) # 隐藏层3→输出层
def forward(self, x):
x = self.flatten(x) # 展平:[64,1,28,28] → [64, 784]
x = self.hidden1(x)
x = torch.sigmoid(x) # 激活函数
x = self.hidden2(x)
x = torch.sigmoid(x)
x = self.hidden3(x)
x = torch.sigmoid(x)
x = self.out(x) # 输出10个类别的得分
return x
model = NeuralNetwork().to(device)
print(model)
6.2 网络层详解
nn.Flatten():将多维输入展平为一维。28×28的图像被展平成784维向量,因为全连接层只接受一维输入。
nn.Linear(in, out):全连接层,执行 的线性变换。
| 层 | 输入维度 | 输出维度 | 参数数量 |
|---|---|---|---|
| hidden1 | 784 | 128 | 784×128 + 128 = 100480 |
| hidden2 | 128 | 256 | 128×256 + 256 = 33024 |
| hidden3 | 256 | 128 | 256×128 + 128 = 32896 |
| out | 128 | 10 | 128×10 + 10 = 1290 |
输出层为什么是10? 因为MNIST有10个类别(数字0-9),网络需要输出每个类别的得分。
6.3 激活函数——引入非线性
激活函数是神经网络的灵魂。如果没有激活函数,多层网络本质上还是一个线性变换,无法拟合复杂函数。
x = torch.sigmoid(x) # Sigmoid激活函数
Sigmoid函数:将输入压缩到(0,1)区间,公式为:
Sigmoid的缺点:
-
容易导致梯度消失(当输入很大或很小时,梯度接近0)
-
输出不是以0为中心
现代网络更常用的激活函数:
-
ReLU:
torch.relu(x),计算简单,缓解梯度消失 -
Tanh:
torch.tanh(x),输出以0为中心 -
LeakyReLU:ReLU的改进版,避免神经元死亡
七、训练函数——让网络学习
7.1 训练流程
训练神经网络的核心流程可以概括为五步:
1. 前向传播:计算预测值
2. 计算损失:衡量预测与真实的差距
3. 梯度清零:清除上一轮的梯度
4. 反向传播:计算每个参数的梯度
5. 更新参数:根据梯度调整权重
7.2 训练代码
def train(dataloader, model, loss_fn, optimizer):
model.train() # 切换到训练模式
batch_size_num = 1
for x, y in dataloader:
x, y = x.to(device), y.to(device) # 数据传入设备
# 1. 前向传播
pred = model.forward(x)
# 2. 计算损失
loss = loss_fn(pred, y)
# 3. 梯度清零
optimizer.zero_grad()
# 4. 反向传播
loss.backward()
# 5. 更新参数
optimizer.step()
loss_value = loss.item()
if batch_size_num % 100 == 0:
print(f"loss: {loss_value:>7f} [number: {batch_size_num}]")
batch_size_num += 1
7.3 关键概念详解
model.train() vs model.eval():
-
train():启用Dropout、BatchNorm等训练专用层 -
eval():关闭这些层,使用固定的推理模式
optimizer.zero_grad():PyTorch默认会累积梯度,所以每轮迭代前必须清零,否则梯度会一直累加。
loss.backward():自动计算所有参数的梯度(自动求导),这是PyTorch最强大的功能之一。
optimizer.step():根据计算出的梯度更新参数,如 。
八、测试函数——评估模型性能
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset) # 数据集大小(10000)
num_batches = len(dataloader) # 批次数量
model.eval() # 切换到测试模式
test_loss, correct = 0, 0
with torch.no_grad(): # 不计算梯度,节省内存
for x, y in dataloader:
x, y = x.to(device), y.to(device)
pred = model.forward(x)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches # 平均损失
correct /= size # 准确率
print(f"Accuracy: {(100*correct)}%, Avg loss: {test_loss}")
关键点解析:
-
torch.no_grad():测试时不需要计算梯度,关闭它可以节省大量内存和计算资源 -
pred.argmax(1):取输出向量中最大值的索引,即预测的类别。dim=1表示按行取最大值 -
(pred.argmax(1) == y):比较预测类别与真实标签,得到布尔张量 -
.type(torch.float).sum().item():将布尔值转为浮点数后求和,得到正确预测的数量
九、损失函数与优化器
9.1 交叉熵损失函数
loss_fn = nn.CrossEntropyLoss()
交叉熵损失是分类任务中最常用的损失函数,公式为:
其中 是真实标签的one-hot编码,
是模型预测的概率。
为什么用交叉熵?
-
它衡量的是两个概率分布之间的差异
-
配合Softmax使用,梯度形式简洁,训练稳定
-
对错误预测的惩罚更大,促使模型快速学习
9.2 优化器——Adam
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
Adam(Adaptive Moment Estimation)是目前最流行的优化器之一,它结合了:
-
动量法:利用历史梯度的指数加权平均,加速收敛
-
RMSProp:自适应调整每个参数的学习率
Adam的优势:
-
自适应学习率,不需要手动调整太多
-
收敛速度快,适合大多数任务
-
对超参数不敏感
学习率(lr):控制参数更新的步长。
-
太大:容易震荡,无法收敛
-
太小:收敛太慢,训练时间长
-
Adam通常推荐0.001作为初始值
其他常用优化器:
-
SGD:随机梯度下降,基础但需要精心调参
-
RMSProp:适合处理非平稳目标
-
AdamW:Adam的改进版,权重衰减更合理
十、训练与测试——完整流程
epochs = 10
for t in range(epochs):
print(f"Epoch {t+1}\n-----------------------------------")
train(train_dataloader, model, loss_fn, optimizer)
print("Done!")
test(test_dataloader, model, loss_fn)
Epoch:完整遍历一次训练数据集。通常需要多个epoch才能让模型充分学习。
训练过程观察:
-
随着epoch增加,loss逐渐下降,说明模型在学习
-
如果训练loss下降但测试loss上升,说明出现了过拟合
-
如果loss一直不下降,可能是学习率太小或网络结构有问题
十一、完整代码总结
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
from matplotlib import pyplot as plt
# 1. 加载数据
training_data = datasets.MNIST(root="data", train=True, download=True, transform=ToTensor())
test_data = datasets.MNIST(root="data", train=False, download=True, transform=ToTensor())
# 2. 创建DataLoader
train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
# 3. 选择设备
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
# 4. 构建模型
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.hidden1 = nn.Linear(28*28, 128)
self.hidden2 = nn.Linear(128, 256)
self.hidden3 = nn.Linear(256, 128)
self.out = nn.Linear(128, 10)
def forward(self, x):
x = self.flatten(x)
x = torch.sigmoid(self.hidden1(x))
x = torch.sigmoid(self.hidden2(x))
x = torch.sigmoid(self.hidden3(x))
x = self.out(x)
return x
model = NeuralNetwork().to(device)
# 5. 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 6. 训练
def train(dataloader, model, loss_fn, optimizer):
model.train()
for batch, (x, y) in enumerate(dataloader):
x, y = x.to(device), y.to(device)
pred = model(x)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 7. 测试
def test(dataloader, model, loss_fn):
model.eval()
size = len(dataloader.dataset)
correct = 0
with torch.no_grad():
for x, y in dataloader:
x, y = x.to(device), y.to(device)
pred = model(x)
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
print(f"Accuracy: {100*correct/size}%")
# 8. 执行训练和测试
epochs = 10
for t in range(epochs):
print(f"Epoch {t+1}")
train(train_dataloader, model, loss_fn, optimizer)
test(test_dataloader, model, loss_fn)
十二、总结
本篇博客通过一个完整的手写数字识别项目,系统学习了PyTorch深度学习的核心知识:
| 知识点 | 核心内容 |
|---|---|
| 张量 | PyTorch的核心数据结构,支持GPU加速和自动求导 |
| MNIST数据集 | 手写数字识别的经典数据集,60000训练+10000测试 |
| DataLoader | 批量加载数据,提高训练效率 |
| 神经网络 | 继承nn.Module,定义__init__和forward |
| 激活函数 | Sigmoid/ReLU,引入非线性 |
| 损失函数 | 交叉熵,衡量预测与真实的差距 |
| 优化器 | Adam,自适应学习率,快速收敛 |
| 训练循环 | 前向传播→计算损失→梯度清零→反向传播→更新参数 |
| 测试评估 | model.eval() + torch.no_grad(),计算准确率 |
更多推荐


所有评论(0)