多分类神经网络
约 1781 字大约 6 分钟
2025-10-27
from torch import nn
import torch
from torch.utils.data import Dataset
class MinistDataset(Dataset):
def __init__(self, file_path):
super().__init__()
images, labels = self.load_data(file_path)
# 一次性转换为 tensor,并做归一化
self.images = torch.tensor(images, dtype=torch.float32) / 255.0 # [N, 784]
self.labels = torch.tensor(labels, dtype=torch.long) # [N]
def load_data(self, file_path):
images, labels = [], []
with open(file_path, 'r') as f:
next(f) # 跳过 header
for line in f:
parts = line.strip().split(',')
labels.append(int(parts[0]))
# 转为 float 列表
images.append([float(p) for p in parts[1:]])
return images, labels
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
# 直接返回 tensor,不用再在这里转换或打印
return self.images[idx], self.labels[idx]zip函数:将多个可迭代对象(如列表、元组等)“压缩”成一个元组的列表。每个元组包含来自所有输入可迭代对象的对应元素。
eg.
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
zipped = zip(list1, list2) # 创建一个zip对象
print(list(zipped)) # 输出: [(1, 'a'), (2, 'b'), (3, 'c')]batch_size = 64
train_dataset = MinistDataset("./data/mnist_train.csv")
lr = 0.1
num_epoch = 100
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
layer_size = [28*28,128,128,64,10] # [输入,隐藏1,隐藏2,隐藏3,输出]
# 手动初始化参数
# 参数初始化应该服从0均值,\sqrt{2/n}标准差的正态分布
weights = []
biases = []
for in_size,out_size in zip(layer_size[:-1],layer_size[1:]):
print(in_size,out_size)
weight = torch.randn(in_size,out_size).to(device) * (2/in_size)**0.5
bias = torch.zeros(out_size).to(device)
weights.append(weight)
biases.append(bias)784 128 128 128 128 64 64 10
常用函数
交叉熵损失函数
类别个数为C
用
one-hot编码表示类别 $ y = [y_1, y_2, ..., y_C] $ 只有一个元素为1,其余为0预测值为概率分布 $ \hat{y} = [\hat{y_1}, \hat{y_2}, ..., \hat{y_C}] \sum \hat{y_i} = 1 $
交叉熵损失函数定义
L=−i=1∑Cyilog(yi^)
因为 yi 只有一个元素为1,其余为0,所以损失函数可以简化为
L=−log(yk^)
其中 k 是真实类别的索引 yk^ 是预测的该类别的概率
def relu(x):
return torch.clamp(x,min=0)
def relu_grad(x):
return (x>0).float()
def softmax(x):
exp_x = torch.exp(x - x.max(dim=1, keepdim=True).values) # 减去最大值以防止溢出
return exp_x / exp_x.sum(dim=1, keepdim=True)
def cross_entropy(pred, label):
n = pred.shape[0]
log_likelihood = -torch.log(pred[range(n), label] + 1e-9) # 加上一个小值以防止log(0)
loss = log_likelihood.mean()
return losstrain_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 定义训练循环
for epoch in range(10):
total_loss = 0
for imgs,labels in train_loader:
x = imgs.to(device)
y = labels.to(device)
N = x.shape[0] # batch size
# 前向传播
"""
这句代码的作用是初始化一个激活值(activation)列表,并把当前批次的输入 x 作为第0层的激活值放进去,供后续层的前向计算使用。
具体含义/作用点:
activation = [x] 把输入的张量作为第一个激活项(activation[0]),形状通常是 [N, in_size](N 为批次大小)。
在后续循环中通过 activation[-1] 取出上一层的输出,计算线性变换 z = activation[-1] @ W + b,然后将 z 的激活 a(如 relu(z))追加到 activation 列表,便于前向连续计算和后续反向传播时访问每层的激活值。
保证每个 batch 都重新初始化 activation(因为这行在 batch 循环内部),不会把不同 batch 的激活混在一起。
"""
activation = [x]
pre_acts = []
for W,b in zip(weights[:-1],biases[:-1]):
z = activation[-1] @ W + b
pre_acts.append(z)
a = relu(z)
activation.append(a)
# 输出层
z_out = activation[-1] @ weights[-1] + biases[-1]
pre_acts.append(z_out)
y_pred = softmax(z_out)
loss = cross_entropy(y_pred,y)
total_loss += loss.item()
# 反向传播
grads_W = [None for _ in weights]
grads_b = [None for _ in biases]
# 输出层梯度
one_hot = torch.zeros_like(y_pred).to(device)
one_hot[range(N), y] = 1
grad_y_pred = (y_pred - one_hot ) / N # [N, 10(output features)]
grads_W[-1] = activation[-1].T @ grad_y_pred # [hidden3,10] = [hidden3,N] @ [N,10]
grads_b[-1] = grad_y_pred.sum(dim=0) # [10] = sum([N,10], dim=0)
# 隐藏层梯度
grad_a = grad_y_pred @ weights[-1].T # [N,hidden3] = [N,10] @ [10,hidden3]
# 从最后一个隐藏层(weights索引 len(weights)-2)向前到第0个隐藏层计算梯度
for i in range(len(weights)-2, -1, -1):
grad_z = grad_a * relu_grad(pre_acts[i]) # [N, hidden_i]
grads_W[i] = activation[i].T @ grad_z # [hidden_(i-1), hidden_i]
grads_b[i] = grad_z.sum(dim=0) # [hidden_i]
if i > 0:
grad_a = grad_z @ weights[i].T # 传递到上一层的激活梯度
# 更新参数
for i in range(len(weights)):
weights[i] -= lr * grads_W[i]
biases[i] -= lr * grads_b[i]
avg_loss = total_loss / len(train_loader)
print(f"Epoch {epoch+1}/{num_epoch}, Loss: {avg_loss:.4f}")Epoch 1/100, Loss: 0.3252 Epoch 2/100, Loss: 0.1344 Epoch 3/100, Loss: 0.0965 Epoch 4/100, Loss: 0.0738 Epoch 5/100, Loss: 0.0601 Epoch 6/100, Loss: 0.0483 Epoch 7/100, Loss: 0.0396 Epoch 8/100, Loss: 0.0318 Epoch 9/100, Loss: 0.0271 Epoch 10/100, Loss: 0.0212
使用Pytorch实现神经网络
import torch.nn as nn
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Linear(28*28, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 10)
)
def forward(self,x):
return self.model(x)batch_size = 64
lr = 0.1
num_epoch = 10
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
train_dataset = MinistDataset("./data/mnist_train.csv")
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dateset = MinistDataset("./data/mnist_test.csv")
test_loader = torch.utils.data.DataLoader(test_dateset, batch_size=batch_size, shuffle=False)
# 模型、损失函数、优化器初始化
model = NeuralNetwork().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=lr) # 使用SGD优化器(随机梯度下降)model.train()
for epoch in range(num_epoch):
# 这里只是为了看平均损失的训练过程,不会对optimizer造成影响
total_loss = 0
for imgs,labels in train_loader:
imgs,labels = imgs.to(device), labels.to(device)
outputs = model(imgs) # 模型传入输入的参数
loss = criterion(outputs, labels) # 计算损失
optimizer.zero_grad() # 清除上一次的梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数
total_loss += loss.item()
avg_loss = total_loss / len(train_loader)
print(f"Epoch {epoch+1}/{num_epoch}, Loss: {avg_loss:.4f}")Epoch 1/10, Loss: 0.6439 Epoch 2/10, Loss: 0.1777 Epoch 3/10, Loss: 0.1186 Epoch 4/10, Loss: 0.0891 Epoch 5/10, Loss: 0.0707 Epoch 6/10, Loss: 0.0591 Epoch 7/10, Loss: 0.0484 Epoch 8/10, Loss: 0.0411 Epoch 9/10, Loss: 0.0328 Epoch 10/10, Loss: 0.0308
以 MNIST 数据集为例,一个样本的图像可以表示为 28×28 的矩阵(像素值归一化到 [0,1]),标签为一个标量(0-9)。
示例:单个样本的图像矩阵(28×28)
假设一个手写数字 "5" 的图像(简化表示,实际为 28×28):
000⋮000000⋮000001⋮100………⋱………001⋮100000⋮000000⋮000
展平后为 784 维向量:x=[x1,x2,…,x784]T,其中 xi∈[0,1]。
标签示例
标签为整数,如 y=5(表示数字 5)。
批量数据(batch_size=2)
两个样本的批量数据:
- 图像批量:X=(x(1)x(2)),形状为 (2, 784)。
- 标签批量:y=(53),形状为 (2,)。
这有助于理解数据加载和模型输入。
模型输出 outputs 为 2×10 的矩阵(每个样本有 10 个类别的 logits),preds 为预测类别向量。
示例:模型输出 outputs(2×10 矩阵)
假设两个样本的输出 logits:
(−2.11.00.5−1.2−1.32.31.2−0.5−0.80.73.4−2.0−1.51.80.9−1.4−0.20.32.1−0.9)
示例:预测类别 preds(2 维向量)
通过 torch.argmax(outputs, dim=1) 获取每个样本的最大值索引:
(56)
这表示第一个样本预测为类别 5(数字 5),第二个样本预测为类别 6(数字 6)。
model.eval()
correct = 0
total = 0
with torch.no_grad():
for imgs,labels in test_loader:
imgs,labels = imgs.to(device), labels.to(device)
outputs = model(imgs)
preds = torch.argmax(outputs, dim=1) # 获取预测的类别,dim=1表示按行取最大值
total += labels.size(0)
correct += (preds == labels).sum().item() # 统计预测正确的数量
print(f"Test Accuracy: {correct / total:.4f}") # 打印测试集准确率Test Accuracy: 0.9782
