传统神经网络实现-----手写数字识别(MNIST)项目

发布于:2025-09-06 ⋅ 阅读:(17) ⋅ 点赞:(0)

完整代码:

# import torch
# print(torch.__version__)#1.X     1、验证安装的开发环境是否正确,

'''
MNIST包含70,000张手写数字图像: 60,000张用于训练,10,000张用于测试。
图像是灰度的,28x28像素的,并且居中的,以减少预处理和加快运行。
'''
import torch
from torch import nn    #导入神经网络模块,
from torch.utils.data import DataLoader #数据包管理工具,打包数据,
from torchvision import datasets #封装了很多与图像相关的模型,及数据集
from torchvision.transforms import ToTensor #数据转换,张量,将其他类型的数据转换为tensor张量,numpy array,dataframe

'''下载训练数据集(包含训练图片+标签)'''
training_data = datasets.MNIST( #跳转到函数的内部源代码,pycharm 按下ctrl +鼠标点击
    root="data",#表示下载的手写数字 到哪个路径。60000
    train=True,#读取下载后的数据 中的 训练集
    download=True,#如果你之前已经下载过了,就不用再下载
    transform=ToTensor(),   #张量,图片是不能直接传入神经网络模型
)   #对于pytorch库能够识别的数据一般是tensor张量.
print(len(training_data))
# datasets.MNIST的参数:
#   root(string): 表示数据集的根目录,
#   train(bool, optional): 如果为True,则从training.pt创建数据集,否则从test.pt创建数据集
#   download(bool, optional): 如果为True,则从internet下载数据集并将其放入根目录。如果数据集已下载,则不会再次下载
#   transform(callable, optional): 接收PIL图片并返回转换后版本图片的转换函数

'''下载测试数据集(包含训练图片+标签) '''
test_data = datasets.MNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor(),#Tensor是在深度学习中提出并广泛应用的数据类型,它与深度学习框架(如 PyTorch、TensorFlow)紧密集成,方便进行神经网络的训练和推理。
)#NumPy 数组只能在CPU上运行。Tensor可以在GPU上运行,这在深度学习应用中可以显著提高计算速度。
print(len(test_data))

# '''展示手写字图片,把训练数据集中的前59000张图片展示一下'''
# from matplotlib import pyplot as plt
# figure = plt.figure()
# for i in range(9):#
#     img, label = training_data[i+59000]#提取第59000张图片
#
#     figure.add_subplot(3, 3, i+1)#图像窗口中创建多个小窗口,小窗口用于显示图片
#     plt.title(label)
#     plt.axis("off")  # plt.show(I)#显示矢量,
#     plt.imshow(img.squeeze(), cmap="gray")  #plt.imshow()将NumPy数组data中的数据显示为图像,并在图形窗口中显示该图像
#     a = img.squeeze() # img.squeeze()从张量img中去掉维度为1的。如果该维度的大小不为1则张量不会改变。#cmap="gray"表示使用灰度色彩映射来显示图像。这意味着图像将以灰度模式显示
# plt.show()

'''创建数据DataLoader(数据加载器)
    batch_size:将数据集分成多份,每一份为batch_size个数据。
           优点:可以减少内存的使用,提高训练速度。
'''
train_dataloader = DataLoader(training_data, batch_size=64)#64张图片为一个包,1、损失函数2、GPU一次性接受的图片个数
test_dataloader = DataLoader(test_data, batch_size=64)
for X, y in test_dataloader:#X是表示打包好的每一个数据包
    print(f"Shape of X [N, C, H, W]: {X.shape}")#
    print(f"Shape of y: {y.shape} {y.dtype}")
    break

'''判断当前设备是否支持GPU,其中mps是苹果m系列芯片的GPU。'''#返回cuda,mps。CPU   m1 ,m2  集显CPU+GPU  RTX3060,
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")#字符串的格式化。  CUDA驱动软件的功能:pytorch能够去执行cuda的命令,cuda通过GPU指令集去控制GPU
#神经网络的模型也需要传入到GPU,1个batchsize的数据集也需要传入到GPU,才可以进行训练。

''' 定义神经网络  类的继承这种方式'''
class NeuralNetwork(nn.Module):#通过调用类的形式来使用神经网络,神经网络的模型,nn.module
    def __init__(self):#python基础关于类,self类自己本身
        super().__init__()#继承的父类初始化
        self.flatten = nn.Flatten()#展开,创建一个展开对象flatten
        self.hidden1 = nn.Linear(28*28, 128)#第1个参数:有多少个神经元传入进来,第2个参数:有多少个数据传出去前一层神经元的个数,当前本层神经元个数
        self.hidden2 = nn.Linear(128, 256)#为什么你要用128
        self.out = nn.Linear(256, 10)#输出必需和标签的类别相同,输入必须是上一层的神经元个数
    def forward(self, x):   #前向传播,你得告诉它  数据的流向。是神经网络层连接起来,函数名称不能改。当你调用forward函数的时候,传入进来的图像数据
        x = self.flatten.forward(x)     #图像进行展开  self.flatten.forward
        x = self.hidden1.forward(x)
        x = torch.relu(x) #激活函数,torch使用的relu函数 relu,tanh
        x = self.hidden2.forward(x)
        x = torch.relu(x)
        x = self.out.forward(x)
        return x

model = NeuralNetwork().to(device)#把刚刚创建的模型传入到Gpu
print(model)

def train(dataloader, model, loss_fn, optimizer):
    model.train()#告诉模型,我要开始训练,模型中w进行随机化操作,已经更新w。在训练过程中,w会被修改的
#pytorch提供2种方式来切换训练和测试的模式,分别是:model.train() 和 model.eval()。
# 一般用法是:在训练开始之前写上model.trian(),在测试时写上 model.eval() 。
    batch_size_num = 1  #统计 训练的batch数量
    for X, y in dataloader:                 #其中batch为每一个数据的编号
        X, y = X.to(device), y.to(device)   #把训练数据集和标签传入cpu或GPU
        pred = model(X)             #.forward可以被省略,父类中已经对此功能进行了设置。自动初始化 w权值
        loss = loss_fn(pred, y)             #通过交叉熵损失函数计算损失值loss
        # Backpropagation 进来一个batch的数据,计算一次梯度,更新一次网络
        optimizer.zero_grad()               #梯度值清零
        loss.backward()                     #反向传播计算得到每个参数的梯度值w
        optimizer.step()                    #根据梯度更新网络w参数

        loss_value = loss.item()                  #从tensor数据中提取数据出来,tensor获取损失值
        if batch_size_num %100  ==0:
            print(f"loss: {loss_value:>7f}  [number:{batch_size_num}]")
        batch_size_num += 1


def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)#10000
    num_batches = len(dataloader)#打包的数量
    model.eval()    #测试,w就不能再更新。
    test_loss, correct = 0, 0   #
    with torch.no_grad():   #一个上下文管理器,关闭梯度计算。当你确认不会调用Tensor.backward()的时候。这可以减少计算所用内存消耗。
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)  #送到GPU
            pred = model.forward(X)
            test_loss += loss_fn(pred, y).item() #test_loss是会自动累加每一个批次的损失值
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
            a = (pred.argmax(1) == y)  #dim=1表示每一行中的最大值对应的索引号,dim=0表示每一列中的最大值对应的索引号
            b = (pred.argmax(1) == y).type(torch.float)
    test_loss /= num_batches  #能来衡量模型测试的好坏。
    correct /= size  #平均的正确率

    print(f"Test result: \n Accuracy: {(100*correct)}%, Avg loss: {test_loss}")

loss_fn = nn.CrossEntropyLoss() #创建交叉熵损失函数对象,因为手写字识别中一共有10个数字,输出会有10个结果
# L1Loss:L1损失,也称为平均绝对误差(Mean Absolute Error, MAE)。它计算预测值与真实值之间的绝对差值的平均值。
# NLLLoss:负对数似然损失(Negative Log Likelihood Loss)。它用于多分类问题,通常与LogSoftmax输出层配合使用。
# NLLLoss2d:这是NLLLoss的一个特殊版本,用于处理2D图像数据。在最新版本的PyTorch中,这个损失函数可能已经被整合到NLLLoss中,通过指定reduction参数来实现同样的功能。
# PoissonNLLLoss:泊松负对数似然损失,用于泊松回归问题。
# GaussianNLLLoss:高斯负对数似然损失,用于高斯分布(正态分布)的回归问题。
# KLDivLoss:Kullback-Leibler散度损失,用于度量两个概率分布之间的差异。
# MSELoss:均方误差损失(Mean Squared Error Loss),计算预测值与真实值之间差值的平方的平均值。
# BCELoss:二元交叉熵损失(Binary Cross Entropy Loss),用于二分类问题。
# BCEWithLogitsLoss:结合了Sigmoid激活函数和二元交叉熵损失的损失函数,用于提高数值稳定性。
# HingeEmbeddingLoss:铰链嵌入损失,用于学习非线性嵌入或半监督学习。
# MultiLabelMarginLoss:多标签边际损失,用于多标签分类问题。
# SmoothL1Loss:平滑L1损失,是L1损失和L2损失(MSE)的结合,旨在避免梯度爆炸问题。
# HuberLoss:Huber损失,与SmoothL1Loss类似,但有一个可调的参数来控制L1和L2损失之间的平衡。
# SoftMarginLoss:软边际损失,用于二分类问题,可以看作是Hinge损失的一种软化版本。
# CrossEntropyLoss:交叉熵损失,用于多分类问题。它结合了LogSoftmax和NLLLoss的功能。
# MultiLabelSoftMarginLoss:多标签软边际损失,用于多标签二分类问题。
# CosineEmbeddingLoss:余弦嵌入损失,用于学习非线性嵌入,通过余弦相似度来度量样本之间的相似性。
# MarginRankingLoss:边际排序损失,用于排序问题,如学习到排序的嵌入空间。
# MultiMarginLoss:多边际损失,用于多分类问题,旨在优化分类边界的边际。
# TripletMarginLoss:三元组边际损失,用于学习嵌入空间中的距离度量,通常用于人脸识别或图像检索等任务。
# TripletMarginWithDistanceLoss:这是TripletMarginLoss的一个变体,允许使用自定义的距离函数。
# CTCLoss:连接时序分类损失(Connectionist Temporal Classification Loss),用于序列到序列的学习问题,特别是当输出序列的长度不固定时(如语音识别)。

#一会改成adam优化器   梯度下降
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)#创建一个优化器,SGD为随机梯度下降算法
# #params:要训练的参数,一般我们传入的都是model.parameters()。
# #lr:learning_rate学习率,也就是步长。

#loss表示模型训练后的输出结果与 样本标签的差距。如果差距越小,就表示模型训练越好,越逼近于真实的模型。

# train(train_dataloader, model, loss_fn, optimizer)#训练1次完整的数据,多轮训练,
# test(test_dataloader, model, loss_fn)


epochs = 10 #到底选择多少呢?
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)#10次训练
print("Done!")
test(test_dataloader, model, loss_fn)



# # #分析sigmiod,relu
# # # sgd,Adam

按代码模块进行解析:

第一部分:环境验证与数据加载
# import torch
# print(torch.__version__)#1.X     1、验证安装的开发环境是否正确,
  • 注释掉了,用于检查 PyTorch 是否安装成功。


import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor

  • 导入 PyTorch 核心库及相关模块:

    • nn:构建神经网络。

    • DataLoader:批量加载数据。

    • datasets:内置数据集(如 MNIST)。

    • ToTensor:将图片转为 Tensor 格式。


training_data = datasets.MNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor(),
)

  • 下载 训练集(60,000 张图):

    • root="data":保存到本地 data/ 文件夹。

    • transform=ToTensor():将图片转为 Tensor(灰度值归一化到 [0, 1])。


test_data = datasets.MNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor(),
)

  • 下载 测试集(10,000 张图)。


train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

  • 使用 DataLoader 将数据打包成 批次(每批 64 张图),方便训练。


 第二部分:设备选择与模型定义

device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"

  • 自动选择设备:

    • 优先使用 NVIDIA GPU(cuda);

    • 其次 Apple 芯片(mps);

    • 最后回退到 CPU。


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.out = nn.Linear(256, 10)

    def forward(self, x):
        x = self.flatten(x)
        x = torch.relu(self.hidden1(x))
        x = torch.relu(self.hidden2(x))
        x = self.out(x)
        return x

  • 定义一个 三层全连接神经网络

    • 输入层:28×28 = 784 像素;

    • 隐藏层1:128 个神经元;

    • 隐藏层2:256 个神经元;

    • 输出层:10 个类别(0~9 数字);

    • 激活函数:ReLU。


model = NeuralNetwork().to(device)

  • 将模型迁移到 GPU(或 CPU)。


第三部分:训练与测试函数

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()

        if batch % 100 == 0:
            print(f"loss: {loss.item():.7f} [batch {batch}]")

  • 训练函数

    • 每个批次前向传播 → 计算损失 → 反向传播 → 更新权重;

    • 每 100 个批次打印一次损失值。


def test(dataloader, model, loss_fn):
    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(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= len(dataloader)
    correct /= len(dataloader.dataset)
    print(f"Test Accuracy: {100*correct:.2f}%, Avg loss: {test_loss:.4f}")

  • 测试函数

    • 不更新权重(model.eval());

    • 计算整体损失与准确率。


第四部分:损失函数与优化器

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)

  • 使用 交叉熵损失(适合多分类);

  • 使用 Adam 优化器(比 SGD 更稳定)。


第五部分:训练循环

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)

  • 训练 10 轮

  • 每轮遍历一次完整训练集;

  • 最后测试模型性能。

代码模块具体逐行逐句解释:

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.out = nn.Linear(256, 10)

    def forward(self, x):
        x = self.flatten(x)
        x = torch.relu(self.hidden1(x))
        x = torch.relu(self.hidden2(x))
        x = self.out(x)
        return x
第一部分:类定义与初始化 (__init__ 方法)

class NeuralNetwork(nn.Module):

作用:声明一个继承自 PyTorch 基类 nn.Module 的新类
含义:所有自定义神经网络必须继承此类才能享受 PyTorch 的训练/推理功能(如 model.train(), model.eval()

super().__init__()

作用:调用父类 nn.Module 的构造函数
重要性:初始化模块的必要内部结构(如参数注册器),不可省略

self.flatten = nn.Flatten()

作用:创建一个展平层对象
功能详解:将多维输入(如图像 [B, H, W])压缩为一维向量 [B, H×W]
典型场景:连接卷积层和非全连接层时的过渡操作

self.hidden1 = nn.Linear(28*28, 128)

作用:定义第一个全连接层(又称密集层)
参数解析

  • in_features=28*28=784:输入特征数(对应 28×28 图像的像素总数)

  • out_features=128:本层神经元数量
    内部机制:自动创建权重矩阵 W₁ (形状 784×128) 和偏置向量 b₁ (长度 128)

self.hidden2 = nn.Linear(128, 256)

作用:定义第二个全连接层
参数解析

  • in_features=128:前一层的输出特征数

  • out_features=256:本层神经元数量
    内部机制:自动创建权重矩阵 W₂ (形状 128×256) 和偏置向量 b₂ (长度 256)

self.out = nn.Linear(256, 10)

作用:定义输出层
特殊设计

  • out_features=10:对应分类任务的类别数(如 MNIST 手写数字识别)

  • 输出未经过激活函数(直接输出 logits),配合交叉熵损失函数使用


第二部分:前向传播 (forward 方法)

def forward(self, x):

作用:定义数据的前向传播路径
关键性质:每次调用 model(input) 时会自动执行此方法

x = self.flatten(x)

作用:展平输入张量
示例

  • 输入形状 [batch_size, 28, 28] → 输出形状 [batch_size, 784]
    必要性:全连接层只能接受一维特征向量

x = torch.relu(self.hidden1(x))

作用:通过第一隐藏层并进行 ReLU 激活
计算过程

  1. 线性变换:x = W₁·x + b₁

  2. ReLU 激活:x[x<0]=0(保留正值,引入非线性)
    设计理由:解决线性模型无法拟合复杂模式的问题

x = torch.relu(self.hidden2(x))

作用:通过第二隐藏层并进行 ReLU 激活
计算过程

  1. 线性变换:x = W₂·x + b₂

  2. ReLU 激活:同上
    效果:进一步提取高阶特征,增加模型表达能力

x = self.out(x)

作用:通过输出层生成最终结果
注意:此处不添加激活函数
原因:分类任务通常在损失函数中结合 LogSoftmax(如 CrossEntropyLoss),logits 更灵活可控

return x

作用:返回模型输出
输出形式:原始 logits(未归一化的概率分数)
后续处理:通常会接入软最大值函数(Softmax)进行概率转换,或直接用于计算损失

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()

        if batch % 100 == 0:
            print(f"loss: {loss.item():.7f} [batch {batch}]")

第1行:model.train()

功能:将模型设置为 训练模式
底层逻辑

  • 激活模型中所有适用于训练的特殊组件。

    • 示例nn.Dropout(p=0.5) 在训练时会随机屏蔽50%的神经元,而在推理模式(model.eval())下无效。

  • 确保模型处于可学习状态(参数注册钩子启用)。
    关键性:若省略此步,模型可能因未启用必要层(如 Dropout)导致性能下降或错误。


第2行:for batch, (X, y) in enumerate(dataloader):

功能:遍历数据集的一个完整周期(Epoch)
参数解析

  • dataloader:PyTorch 的 DataLoader 对象,负责按批次加载数据。

  • enumerate():同时获取当前批次的索引 batch 和数据 (X, y)
    典型输出

    • X: 输入特征张量,形状为 [batch_size × input_dim](如图像数据为 [B, C, H, W])。

    • y: 目标标签张量,形状为 [batch_size × output_dim](分类任务通常为 one-hot 编码或类别索引)。
      设计目的:通过迭代实现 mini-batch SGD(随机梯度下降),逐步优化模型参数。


第3-4行:X, y = X.to(device), y.to(device)

功能:将数据迁移到指定计算设备(CPU/GPU)
底层逻辑

  • device 通常是预定义的变量(如 torch.device('cuda')),表示可用的硬件资源。

  • .to(device) 方法执行以下操作:

    • 数据搬运:将张量从 CPU 内存复制到 GPU 显存(若 device='cuda')。

    • 类型匹配:自动转换数据类型以匹配模型参数的类型(如 float32)。
      重要性:确保模型与数据在同一设备上运算,否则会抛出 RuntimeError: ... not on the same device
      注意:此操作仅影响张量的存储位置,不改变其数值内容。


第5行:pred = model(X)

功能:执行前向传播(Forward Propagation)
计算流程

  1. 输入 X 经模型各层依次变换(如线性层、激活函数、归一化层等)。

  2. 输出 pred 是模型对输入 X 的原始预测值(Logits),尚未应用任何激活函数。
    维度示例

  • 输入形状:[batch_size, input_dim] → 输出形状:[batch_size, num_classes](分类任务)。
    注意:此处保持线性输出,供损失函数后续处理。


第6行:loss = loss_fn(pred, y)

功能:计算当前批次的损失值
核心机制

  • loss_fn 是预定义的损失函数(如 nn.CrossEntropyLoss()nn.MSELoss())。

  • 对比模型输出 pred 与真实标签 y,量化预测误差。
    数学本质

    • 分类任务:交叉熵损失 L = -Σ(y * log(softmax(pred)))

    • 回归任务:均方误差 L = ||pred - y||²_2
      作用:为反向传播提供优化目标,指导参数更新方向。


第7行:optimizer.zero_grad()

功能:清空优化器的梯度缓存
底层逻辑

  • PyTorch 采用 累积梯度 策略,每次调用 loss.backward() 会将新梯度累加到现有梯度上。

  • 此操作将所有可训练参数的梯度置零,防止跨批次梯度混合。
    必要性:若不执行此步,梯度会指数级增长,导致参数更新异常剧烈(如爆炸性梯度)。
    内部实现:遍历模型的所有可训练参数,执行 param.grad = None


第8行:loss.backward()

功能:执行反向传播(Backward Propagation)
计算流程

  1. 根据链式法则自动计算损失对每个参数的梯度。

  2. 梯度存储在 param.grad 属性中(仅存在于 requires_grad=True 的参数)。
    技术核心:利用自动微分系统高效计算复杂计算图的梯度。
    注意:此操作 不会立即更新参数,仅计算梯度。


第9行:optimizer.step()

功能:根据梯度更新模型参数
执行过程

  1. 优化器(如 SGD、Adam)按照预设规则(学习率、动量等)更新参数。

    • SGD 示例param = param - lr * param.grad

  2. 完成一次参数更新后,梯度会被自动清零(部分优化器除外)。
    效果:使模型向损失降低的方向调整参数。
    注意:此操作是参数更新的唯一入口,必须在 zero_grad() 之后调用。


第10-11行:if batch % 100 == 0: print(f"loss: {loss.item():.7f} [batch {batch}]")

功能:定期打印训练进度
实现细节

  • batch % 100 == 0:每处理 100 个批次打印一次日志。

  • loss.item():将张量转换为 Python 标量(浮点数),用于格式化输出。
    输出示例loss: 0.1234567 [batch 100]
    用途:监控训练稳定性,辅助调试(如发现 NaN 或爆炸性损失)。


网站公告

今日签到

点亮在社区的每一天
去签到