文章目录
- 第一部分:准备工作
- 第二部分:准备数据
-
- [2.1 创建自定义数据集类](#2.1 创建自定义数据集类)
- [2.2 创建数据加载器](#2.2 创建数据加载器)
- 第三部分:构建神经网络
-
- [3.1 定义网络结构并初始化参数](#3.1 定义网络结构并初始化参数)
- [3.2 前向传播函数](#3.2 前向传播函数)
- [3.3 交叉熵损失函数](#3.3 交叉熵损失函数)
- 第四部分:反向传播(最核心的部分)
-
- [4.1 手动计算梯度](#4.1 手动计算梯度)
- [4.2 更新参数](#4.2 更新参数)
- 第五部分:训练循环
-
- [5.1 训练函数](#5.1 训练函数)
- [5.2 测试函数](#5.2 测试函数)
- 第六部分:主程序
-
- [6.1 完整训练流程](#6.1 完整训练流程)
- [6.2 使用训练好的模型预测](#6.2 使用训练好的模型预测)
- 第七部分:运行和结果
-
- [7.1 如何运行](#7.1 如何运行)
- [7.2 预期输出](#7.2 预期输出)
- [7.3 关键要点总结](#7.3 关键要点总结)
- 完整code
- PyTorch实现步骤
我将用最基础的知识,一步步讲解如何手动实现一个能识别手写数字的神经网络。我们会像搭积木一样,从最简单的部分开始,逐渐构建完整的系统。
第一部分:准备工作
1.1 导入必要的工具
python
import torch
import torch.utils.data as Data
import numpy as np
import pandas as pd
基础解释:
• torch:就像我们的"数学工具箱",提供了处理数字和矩阵的工具
• Data:用来管理和组织数据的工具
• numpy和pandas:处理表格数据的工具
1.2 设置计算设备
python
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
基础解释:
• 计算机有两种主要计算设备:CPU(普通处理器)和GPU(图形处理器,更适合做大量数学计算)
第二部分:准备数据
2.1 创建自定义数据集类
python
class MNISTDataset(Data.Dataset):
def __init__(self, csv_file):
# 1. 读取CSV文件
data = pd.read_csv(csv_file).values
# 2. 分离标签和图像数据
self.labels = data[:, 0] # 第一列是标签(0-9)
self.images = data[:, 1:] # 后面784列是像素值
# 3. 归一化处理:把0-255的像素值变成0-1之间的小数
# 为什么要归一化?让所有数据在相同的尺度上,学习更稳定
self.images = self.images.astype(np.float32) / 255.0
# 4. 转换为PyTorch张量
self.images = torch.from_numpy(self.images)
self.labels = torch.from_numpy(self.labels)
def __len__(self):
# 返回数据集的大小(有多少张图片)
return len(self.labels)
def __getitem__(self, idx):
# 根据索引返回一张图片和对应的标签
return self.images[idx], self.labels[idx]
基础解释:
• Dataset类就像是一个"数据管家",它知道如何存取数据
• __init__:初始化,读取数据并做预处理
• __len__:告诉别人我有多少数据
• __getitem__:根据编号(idx)取出对应的数据
2.2 创建数据加载器
python
# 设置批次大小:一次处理64张图片
batch_size = 64
# 创建训练集(注意:路径要改成你自己的)
train_dataset = MNISTDataset('mnist_train.csv')
train_loader = Data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 创建测试集
test_dataset = MNISTDataset('mnist_test.csv')
test_loader = Data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
基础解释:
• batch_size=64:一次处理64张图片,就像老师一次批改64份作业
• shuffle=True:打乱顺序,让学习更全面
• DataLoader:数据加载器,帮我们自动分批处理数据
第三部分:构建神经网络
3.1 定义网络结构并初始化参数
python
# 定义网络结构:[输入层, 隐藏层1, 隐藏层2, 隐藏层3, 隐藏层4, 输出层]
layer_sizes = [28*28, 128, 128, 128, 64, 10]
# 存储权重和偏置的列表
weights = [] # 权重:决定信号的重要性
biases = [] # 偏置:调节神经元的激活程度
# 初始化每一层的参数
for in_size, out_size in zip(layer_sizes[:-1], layer_sizes[1:]):
# 初始化权重:使用He初始化方法,让学习起步更稳定
W = torch.randn(in_size, out_size, device=device) * torch.sqrt(torch.tensor(2.0 / in_size))
# 初始化偏置:全部设为0
b = torch.zeros(out_size, device=device)
weights.append(W)
biases.append(b)
基础解释:
• layer_sizes:定义网络的"形状"
• 28*28=784:输入层,因为图片有784个像素
• 128, 128, 128, 64:隐藏层,可以提取特征
• 10:输出层,对应10个数字(0-9)
• weights:权重矩阵,决定上一层每个信号对当前神经元的重要性
• ·biases·:偏置向量,让神经元更容易或更难被激活
3.2 前向传播函数
python
def forward(x, weights, biases):
"""
前向传播:输入数据通过网络得到预测结果
x: 输入数据 [batch_size, 784]
返回:每层的激活值和原始输出
"""
activations = [x] # 存储每层的激活值
zs = [] # 存储每层的线性输出(未激活)
# 遍历每一层(除了输出层)
for W, b in zip(weights[:-1], biases[:-1]):
z = activations[-1] @ W + b
zs.append(z)
a = torch.clamp(z, min=0)
activations.append(a)
# 输出层:线性变换
z_last = activations[-1] @ weights[-1] + biases[-1]
zs.append(z_last)
# Softmax激活:将输出转换为概率分布
# 技巧:先减去最大值,防止数值溢出
exp_z = torch.exp(z_last - z_last.max(dim=1, keepdim=True).values)
probs = exp_z / exp_z.sum(dim=1, keepdim=True)
activations.append(probs)
return activations, zs
基础解释:
线性变换:z = x * W + b
• 就像把输入数据通过一个"过滤器"ReLU激活:f(z) = max(0, z)
• 负值变0,正值保留
• 给网络添加"非线性",让它能学习复杂模式Softmax:将最后10个输出变成概率
• 所有概率和为1
• 值最大的对应最可能的数字
3.3 交叉熵损失函数
python
def cross_entropy(pred, labels):
"""
计算交叉熵损失
pred: 预测概率 [batch_size, 10]
labels: 真实标签 [batch_size]
返回:损失值和one-hot编码
"""
N = pred.shape[0] # 批次大小
# 创建one-hot编码:将标签转换为10维向量
# 例如:标签3 -> [0,0,0,1,0,0,0,0,0,0]
one_hot = torch.zeros_like(pred)
one_hot[torch.arange(N), labels] = 1
# 计算损失:-Σ(y_true * log(y_pred)) / N
# 加1e-8防止对0取对数
loss = - (one_hot * torch.log(pred + 1e-8)).sum() / N
return loss, one_hot
基础解释:
• One-hot编码:把数字标签变成"密码锁"形式
• 交叉熵损失:衡量预测概率和真实标签的差距
• 预测越准,损失越小
• 预测越错,损失越大
第四部分:反向传播(最核心的部分)
4.1 手动计算梯度
python
def backward(activations, zs, weights, one_hot):
"""
反向传播:计算损失对每个参数的梯度
activations: 每层的激活值
zs: 每层的线性输出
weights: 权重列表
one_hot: 真实标签的one-hot编码
返回:权重梯度和偏置梯度
"""
num_layers = len(weights)
batch_size = activations[0].shape[0]
# 初始化梯度列表
grad_w = [None] * num_layers
grad_b = [None] * num_layers
# 输出层的梯度
# dz_last = pred - y_true
dz = (activations[-1] - one_hot) / batch_size # [batch_size, 10]
# 计算输出层的梯度 activations[-2] (倒数第二层的输出,即输出层的输入):
grad_w[-1] = activations[-2].t() @ dz # 使用倒数第二层的激活值
grad_b[-1] = dz.sum(dim=0) # dL/db
# 反向传播到隐藏层
for l in range(num_layers - 2, -1, -1):
# 计算当前层的梯度
# dz = (下一层的dz * 下一层的W^T) * ReLU的导数
dz_next = dz @ weights[l+1].T
# ReLU的导数:如果z>0则为1,否则为0
relu_derivative = (zs[l] > 0).float()
dz = dz_next * relu_derivative
# 计算当前层的权重和偏置梯度
grad_w[l] = activations[l].t() @ dz
grad_b[l] = dz.sum(dim=0)
return grad_w, grad_b
基础解释(这是最关键的部分):
反向传播就像"找错游戏":
发现问题:输出层预测错了(pred - y_true)追溯责任:这个错误是谁造成的?
• 是输出层的权重W不对?
• 还是隐藏层的计算有问题?链式法则:像多米诺骨牌一样,从后往前追溯
• 输出层的错误 → 隐藏层3的错误 → 隐藏层2的错误 → ...计算梯度:每个参数应该调整多少
4.2 更新参数
python
def update_parameters(weights, biases, grad_w, grad_b, learning_rate):
"""
更新参数:沿着梯度下降方向调整权重和偏置
"""
for i in range(len(weights)):
weights[i] = weights[i] - learning_rate * grad_w[i]
biases[i] = biases[i] - learning_rate * grad_b[i]
return weights, biases
基础解释:
• 梯度下降:沿着"下山"的方向走一小步
• learning_rate=0.01:学习率,控制步伐大小
• 太大:可能跳过最低点
• 太小:学习太慢
第五部分:训练循环
5.1 训练函数
python
def train_one_epoch(train_loader, weights, biases, learning_rate=0.01):
"""
训练一个epoch:遍历所有训练数据一次
"""
total_loss = 0
total_samples = 0
for batch_idx, (images, labels) in enumerate(train_loader):
# 将数据移到指定设备
images = images.to(device)
labels = labels.to(device)
# 1. 前向传播:得到预测
activations, zs = forward(images, weights, biases)
# 2. 计算损失
loss, one_hot = cross_entropy(activations[-1], labels)
total_loss += loss.item() * images.shape
total_samples += images.shape
# 3. 反向传播:计算梯度
grad_w, grad_b = backward(activations, zs, weights, one_hot)
# 4. 更新参数
weights, biases = update_parameters(weights, biases, grad_w, grad_b, learning_rate)
# 每100个批次打印一次进度
if batch_idx % 100 == 0:
print(f' 批次 [{batch_idx}/{len(train_loader)}], 损失: {loss.item():.4f}')
# 计算平均损失
avg_loss = total_loss / total_samples
return avg_loss, weights, biases
基础解释:
一个epoch = 遍历所有训练数据一次
每个批次包含:
- 前向传播:猜答案
- 计算损失:判卷
- 反向传播:找错误原因
- 更新参数:改正错误
5.2 测试函数
python
def evaluate(test_loader, weights, biases):
"""
评估模型在测试集上的表现
"""
correct = 0
total = 0
with torch.no_grad(): # 不需要计算梯度
for images, labels in test_loader:
images = images.to(device)
labels = labels.to(device)
# 前向传播
activations, _ = forward(images, weights, biases)
predictions = activations[-1]
# 获取预测结果:概率最大的类别
_, predicted = torch.max(predictions, 1)
# 统计正确预测的数量
correct += (predicted == labels).sum().item()
total += labels.size(0)
accuracy = 100 * correct / total
return accuracy
基础解释:
• torch.no_grad():测试时不需要计算梯度,节省内存
• torch.max(predictions, 1):找出每个样本概率最大的类别
• 准确率 = 正确数 / 总数 × 100%
第六部分:主程序
6.1 完整训练流程
python
def main():
# 设置超参数
learning_rate = 0.01
num_epochs = 10 # 训练轮数
print("开始训练神经网络...")
print(f"网络结构: {layer_sizes}")
print(f"学习率: {learning_rate}")
print(f"训练轮数: {num_epochs}")
print("-" * 50)
# 训练循环
for epoch in range(num_epochs):
print(f'第 {epoch+1}/{num_epochs} 轮:')
# 训练一个epoch
avg_loss, weights, biases = train_one_epoch(
train_loader, weights, biases, learning_rate
)
# 在测试集上评估
accuracy = evaluate(test_loader, weights, biases)
print(f' 平均损失: {avg_loss:.4f}, 测试准确率: {accuracy:.2f}%')
print("-" * 50)
# 最终评估
final_accuracy = evaluate(test_loader, weights, biases)
print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")
return weights, biases
# 运行主程序
if __name__ == "__main__":
trained_weights, trained_biases = main()
6.2 使用训练好的模型预测
python
def predict_single_image(image_array, weights, biases):
"""
预测单张图片
image_array: 形状为[784]的numpy数组或PyTorch张量
"""
if isinstance(image_array, np.ndarray):
image_tensor = torch.from_numpy(image_array).float().to(device)
else:
image_tensor = image_array.float().to(device)
# 添加批次维度:[784] -> [1, 784]
image_tensor = image_tensor.unsqueeze(0)
# 前向传播
activations, _ = forward(image_tensor, weights, biases)
probabilities = activations[-1][0] # 取第一个(也是唯一一个)样本
# 获取预测结果和置信度
confidence, prediction = torch.max(probabilities, 0)
return prediction.item(), confidence.item(), probabilities.detach().cpu().numpy()
# 示例:随机测试一张图片
def test_random_image(test_loader, weights, biases):
# 获取一个批次
images, labels = next(iter(test_loader))
# 取第一张图片
test_image = images
true_label = labels[0].item()
# 预测
pred, confidence, probs = predict_single_image(test_image, weights, biases)
print(f"真实标签: {true_label}")
print(f"预测结果: {pred} (置信度: {confidence:.2%})")
print(f"所有数字的概率: {probs}")
if pred == true_label:
print("✓ 预测正确!")
else:
print("✗ 预测错误!")
return test_image, true_label, pred
第七部分:运行和结果
7.1 如何运行
- 准备数据:下载MNIST数据集(CSV格式)
- 修改路径:将代码中的文件路径改为你的实际路径
- 运行代码:执行主程序
7.2 预期输出
bash
使用设备: cuda (或 cpu)
开始训练神经网络...
网络结构: [784, 128, 128, 128, 64, 10]
学习率: 0.01
训练轮数: 10
--------------------------------------------------
第 1/10 轮:
批次 [0/938], 损失: 2.3026
...
平均损失: 0.3456, 测试准确率: 91.23%
--------------------------------------------------
第 2/10 轮:
平均损失: 0.1234, 测试准确率: 94.56%
--------------------------------------------------
...
第 10/10 轮:
平均损失: 0.0456, 测试准确率: 97.34%
--------------------------------------------------
训练完成!最终测试准确率: 97.34%
7.3 关键要点总结
- 数据流:
输入图片 → 线性变换 → ReLU激活 → ... → Softmax → 输出概率 - 学习过程:
前向传播(猜) → 计算损失(判卷) → 反向传播(找错) → 更新参数(改正) - 为什么能学习:
• 通过反向传播找到错误原因
• 通过梯度下降逐步改正错误
• 每次迭代都更准确一点 - 手动实现的意义:
• 理解每个参数如何影响结果
• 理解梯度如何计算和传播
• 不再把神经网络当作"黑箱"
完整code
python
import torch
import torch.utils.data as Data
import numpy as np
import pandas as pd
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
class MNISTDataset(Data.Dataset):
def __init__(self, csv_file):
# 1. 读取CSV文件
data = pd.read_csv(csv_file).values
# 2. 分离标签和图像数据
self.labels = data[:, 0] # 第一列是标签(0-9)
self.images = data[:, 1:] # 后面784列是像素值
# 3. 归一化处理:把0-255的像素值变成0-1之间的小数
# 为什么要归一化?让所有数据在相同的尺度上,学习更稳定
self.images = self.images.astype(np.float32) / 255.0
# 4. 转换为PyTorch张量
self.images = torch.from_numpy(self.images)
self.labels = torch.from_numpy(self.labels)
def __len__(self):
# 返回数据集的大小(有多少张图片)
return len(self.labels)
def __getitem__(self, idx):
# 根据索引返回一张图片和对应的标签
return self.images[idx], self.labels[idx]
# 设置批次大小:一次处理64张图片
batch_size = 64
# 创建训练集(注意:路径要改成你自己的)
train_dataset = MNISTDataset('mnist_train.csv')
train_loader = Data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 创建测试集
test_dataset = MNISTDataset('mnist_test.csv')
test_loader = Data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# 定义网络结构:[输入层, 隐藏层1, 隐藏层2, 隐藏层3, 隐藏层4, 输出层]
layer_sizes = [28*28, 128, 128, 128, 64, 10]
# 存储权重和偏置的列表
weights = [] # 权重:决定信号的重要性
biases = [] # 偏置:调节神经元的激活程度
# 初始化每一层的参数
for in_size, out_size in zip(layer_sizes[:-1], layer_sizes[1:]):
# 初始化权重:使用He初始化方法,让学习起步更稳定
W = torch.randn(in_size, out_size, device=device) * torch.sqrt(torch.tensor(2.0 / in_size))
# 初始化偏置:全部设为0
b = torch.zeros(out_size, device=device)
weights.append(W)
biases.append(b)
def forward(x, weights, biases):
"""
前向传播:输入数据通过网络得到预测结果
x: 输入数据 [batch_size, 784]
返回:每层的激活值和原始输出
"""
activations = [x] # 存储每层的激活值
zs = [] # 存储每层的线性输出(未激活)
# 遍历每一层(除了输出层)
for W, b in zip(weights[:-1], biases[:-1]):
z = activations[-1] @ W + b
zs.append(z)
a = torch.clamp(z, min=0)
activations.append(a)
# 输出层:线性变换
z_last = activations[-1] @ weights[-1] + biases[-1]
zs.append(z_last)
# Softmax激活:将输出转换为概率分布
# 技巧:先减去最大值,防止数值溢出
exp_z = torch.exp(z_last - z_last.max(dim=1, keepdim=True).values)
probs = exp_z / exp_z.sum(dim=1, keepdim=True)
activations.append(probs)
return activations, zs
def cross_entropy(pred, labels):
"""
计算交叉熵损失
pred: 预测概率 [batch_size, 10]
labels: 真实标签 [batch_size]
返回:损失值和one-hot编码
"""
N = pred.shape[0] # 批次大小
# 创建one-hot编码:将标签转换为10维向量
# 例如:标签3 -> [0,0,0,1,0,0,0,0,0,0]
one_hot = torch.zeros_like(pred)
one_hot[torch.arange(N), labels] = 1
# 计算损失:-Σ(y_true * log(y_pred)) / N
# 加1e-8防止对0取对数
loss = - (one_hot * torch.log(pred + 1e-8)).sum() / N
return loss, one_hot
def backward(activations, zs, weights, one_hot):
"""
反向传播:计算损失对每个参数的梯度
activations: 每层的激活值
zs: 每层的线性输出
weights: 权重列表
one_hot: 真实标签的one-hot编码
返回:权重梯度和偏置梯度
"""
num_layers = len(weights)
batch_size = activations[0].shape[0]
# 初始化梯度列表
grad_w = [None] * num_layers
grad_b = [None] * num_layers
# 输出层的梯度
# dz_last = pred - y_true
dz = (activations[-1] - one_hot) / batch_size # [batch_size, 10]
# 计算输出层的梯度 activations[-2] (倒数第二层的输出,即输出层的输入):
grad_w[-1] = activations[-2].t() @ dz # 使用倒数第二层的激活值
grad_b[-1] = dz.sum(dim=0) # dL/db
# 反向传播到隐藏层
for l in range(num_layers - 2, -1, -1):
# 计算当前层的梯度
# dz = (下一层的dz * 下一层的W^T) * ReLU的导数
dz_next = dz @ weights[l+1].T
# ReLU的导数:如果z>0则为1,否则为0
relu_derivative = (zs[l] > 0).float()
dz = dz_next * relu_derivative
# 计算当前层的权重和偏置梯度
grad_w[l] = activations[l].t() @ dz
grad_b[l] = dz.sum(dim=0)
return grad_w, grad_b
def train_one_epoch(train_loader, weights, biases, learning_rate=0.01):
"""
训练一个epoch:遍历所有训练数据一次
"""
total_loss = 0
for batch_idx, (images, labels) in enumerate(train_loader):
# 将数据移到指定设备
images = images.to(device)
labels = labels.to(device)
# 1. 前向传播:得到预测
activations, zs = forward(images, weights, biases)
# 2. 计算损失
loss, one_hot = cross_entropy(activations[-1], labels)
total_loss += loss.item()
# 3. 反向传播:计算梯度
grad_w, grad_b = backward(activations, zs, weights, one_hot)
# 4. 更新参数
with torch.no_grad():
for i in range(len(weights)):
weights[i] -= learning_rate * grad_w[i]
biases[i] -= learning_rate * grad_b[i]
# 计算平均损失
avg_loss = total_loss / len(train_loader)
print(f' 批次 [{batch_idx}/{len(train_loader)}], 损失: {avg_loss:.4f}')
return avg_loss, weights, biases
def evaluate(test_loader, weights, biases):
"""
评估模型在测试集上的表现
"""
correct = 0
total = 0
with torch.no_grad(): # 不需要计算梯度
for images, labels in test_loader:
images = images.to(device)
labels = labels.to(device)
# 前向传播
activations, _ = forward(images, weights, biases)
predictions = activations[-1]
# 获取预测结果:概率最大的类别
_, predicted = torch.max(predictions, 1)
# 统计正确预测的数量
correct += (predicted == labels).sum().item()
total += labels.size(0)
accuracy = 100 * correct / total
return accuracy
def main():
# 设置超参数
learning_rate = 0.01
num_epochs = 10 # 训练轮数
print("开始训练神经网络...")
print(f"网络结构: {layer_sizes}")
print(f"学习率: {learning_rate}")
print(f"训练轮数: {num_epochs}")
print("-" * 50)
# 训练循环
for epoch in range(num_epochs):
print(f'第 {epoch+1}/{num_epochs} 轮:')
# 训练一个epoch
avg_loss, weights_, biases_ = train_one_epoch(
train_loader, weights, biases, learning_rate
)
# 在测试集上评估
accuracy = evaluate(test_loader, weights_, biases_)
print(f' 平均损失: {avg_loss:.4f}, 测试准确率: {accuracy:.2f}%')
print("-" * 50)
# 最终评估
final_accuracy = evaluate(test_loader, weights_, biases_)
print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")
return weights_, biases_
def predict_single_image(image_array, weights, biases):
"""
预测单张图片
image_array: 形状为[784]的numpy数组或PyTorch张量
"""
if isinstance(image_array, np.ndarray):
image_tensor = torch.from_numpy(image_array).float().to(device)
else:
image_tensor = image_array.float().to(device)
# 添加批次维度:[784] -> [1, 784]
image_tensor = image_tensor.unsqueeze(0)
# 前向传播
activations, _ = forward(image_tensor, weights, biases)
probabilities = activations[-1][0] # 取第一个(也是唯一一个)样本
# 获取预测结果和置信度
confidence, prediction = torch.max(probabilities, 0)
return prediction.item(), confidence.item(), probabilities.detach().cpu().numpy()
# 示例:随机测试一张图片
def test_random_image(test_loader, weights, biases):
# 获取一个批次
images, labels = next(iter(test_loader))
# 取第一张图片
test_image = images[0]
true_label = labels[0].item()
# 预测图片标签
pred, confidence, probs = predict_single_image(test_image, weights, biases)
print(f"真实标签: {true_label}")
print(f"预测结果: {pred} (置信度: {confidence:.2%})")
print(f"所有数字的概率: {probs}")
if pred == true_label:
print("✓ 预测正确!")
else:
print("✗ 预测错误!")
return test_image, true_label, pred
# 运行主程序
if __name__ == "__main__":
trained_weights, trained_biases = main()
test_random_image(test_loader, trained_weights, trained_biases)
PyTorch实现步骤
第一步:明确任务
- 解决什么问题? → 手写数字识别(MNIST)
- 输入是什么?输出是什么? → 输入:28×28像素图片;输出:0-9共10个类别
- 用什么框架? → PyTorch
行动:创建一个新的Python文件,写下基本导入:
python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
第二步:数据准备
思考数据流:
- 数据在哪里?什么格式? → CSV文件,第一列是标签,后面784列是像素
- 怎么读进来? → 自定义Dataset类
- 需要预处理吗? → 归一化(/255),标准化(减均值除标准差)
行动:实现Dataset类:
python
class MNISTDataset(Dataset):
def __init__(self, file_path):
# 1. 读取文件
# 2. 分离图片和标签
pass
def __len__(self):
# 返回数据总数
pass
def __getitem__(self, idx):
# 1. 获取第idx个样本
# 2. 转换为tensor
# 3. 预处理(归一化)
# 4. 返回(图片, 标签)
pass
提示:先写框架,再填细节。可以从最简单的开始,比如先不归一化。
第三步:模型设计
思考架构:
- 输入维度? → 28×28=784
- 输出维度? → 10
- 中间结构? → 从简单开始:784→128→64→10
- 激活函数? → ReLU(简单有效)
行动:定义模型类:
python
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
# 设计层结构
self.layers = nn.Sequential(
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 10)
)
def forward(self, x):
return self.layers(x)
关键:先实现最简单的可行版本。不要一开始就设计复杂网络。
第四步:训练配置(5分钟)
选择超参数和组件:
批量大小 → 32或64(中等大小)
学习率 → 0.01或0.001(从小开始)
训练轮数 → 5或10(先少一点,看效果)
损失函数 → nn.CrossEntropyLoss()(多分类标准选择)
优化器 → optim.Adam()(比SGD更友好)或optim.SGD()
行动:
python
# 超参数
batch_size = 64
learning_rate = 0.001
epochs = 5
# 设备选择
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 初始化
model = NeuralNetwork().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
第五步:训练循环
构建训练流程:
- 外层循环:控制训练轮数
- 内层循环:遍历每个批次
- 四个核心步骤:前向传播→计算损失→反向传播→更新参数
行动:写出训练骨架:
python
model.train() # 设置为训练模式
for epoch in range(epochs):
total_loss = 0
for images, labels in train_loader: # 假设train_loader已定义
# 1. 数据移到设备
images, labels = images.to(device), labels.to(device)
# 2. 前向传播
outputs = model(images)
# 3. 计算损失
loss = criterion(outputs, labels)
# 4. 反向传播
optimizer.zero_grad() # 清空梯度
loss.backward() # 计算梯度
# 5. 更新参数
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_loader):.4f}")
第六步:测试评估(10分钟)
思考测试与训练的不同:
- 不需要梯度计算 → 用torch.no_grad()
- 不需要参数更新 → 只做前向传播
- 如何评估? → 计算准确率
行动:
python
model.eval() # 设置为评估模式
correct = 0
total = 0
with torch.no_grad(): # 禁用梯度计算
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
# 获取预测结果(最大值的索引)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f"Accuracy: {100 * correct / total:.2f}%")
第七步:整合调试(10分钟)
- 把各部分连接起来:
- 创建数据加载器
- 按顺序调用各部分
- 运行并观察输出
完整结构:
t
# 1. 导入
# 2. 数据集类
# 3. 模型类
# 4. 超参数设置
# 5. 数据加载器创建
# 6. 模型初始化
# 7. 训练循环
# 8. 测试评估
迭代改进的路径
简单模型 → 跑通流程 → 增加复杂度 → 调参优化 → 添加功能
↓ ↓ ↓ ↓ ↓
784→10 能训练 加隐藏层 调学习率 加正则化
能测试 加激活函数 调批量大小 加学习率调度