🌈 全连接神经网络 = 「认数字的积木游戏」

一、游戏目标:认出手写数字(0-9)

假设你现在要设计一个机器人,让它看手写数字图片,然后说出是 0-9 中的哪一个。

第一步:把图片拆成「积木块」(输入层)

手写数字图片是 28×28 的格子(像 28×28 的拼图),每个格子是一个像素(小方块),颜色越黑数值越大(0-1 之间)。

  • 机器人不能直接看拼图,需要把它拆成一行 784 个小方块(28×28=784),这就是「展平」的过程。
    (想象把拼图拆开,排成一排,每个方块就是一个输入积木)
第二步:用「特征探测器」积木判断(隐藏层)

现在有 128 个「特征探测器」积木,每个探测器负责找一种数字特征:

  • 探测器 A:专门找「有没有圆圈形状」(比如数字 0、6、8)
  • 探测器 B:专门找「有没有竖线」(比如数字 1、7)
  • 探测器 C:专门找「有没有两个圆圈」(比如数字 8)
    ...(共 128 个不同的探测器)

关键规则:每个探测器都能看到所有 784 个像素积木,然后根据自己的「经验」打分:
「这个像素组合看起来有多像我要找的特征?」
(分数越高,说明越可能有这个特征)

第三步:过滤掉不重要的分数(激活函数)

探测器打完分后,有些分数很低(比如 0.1),说明几乎没有这个特征,我们直接忽略:

  • 用 ReLU 激活函数(类似过滤器):只保留分数 > 0 的,分数≤0 的全设为 0
    (比如探测器 A 打分 - 0.5,就当它没说;打分 3.2,就保留 3.2)

现在剩下 128 个「有效特征分数」,比如:
探测器 A: 3.2(有圆圈),探测器 B: 0(没有竖线),探测器 C: 5.1(有两个圆圈)...

第四步:用「数字判断器」积木得出结果(输出层)

有 10 个「数字判断器」积木(对应 0-9),每个判断器根据 128 个特征分数,计算这个图片有多可能是某个数字:

  • 判断器 0:看到「有圆圈,没有竖线」,可能打高分
  • 判断器 8:看到「有两个圆圈」,可能打高分
  • 判断器 1:看到「有竖线,没有圆圈」,可能打高分

关键规则:每个数字判断器都能看到所有 128 个特征分数,然后算总分:
「特征 A 贡献多少分 + 特征 B 贡献多少分 + ...」
(总分最高的数字判断器,就是机器人的答案)

游戏核心:「经验值」怎么来?(训练过程)

每个探测器和判断器都有「经验值」(权重和偏置),初始是随机的,需要通过训练调整:

  1. 给机器人看大量已知数字的图片(比如 50000 张手写数字图片,告诉它每个图片是 0-9 中的哪个)
  2. 机器人用当前经验值判断,比如把一张数字 8 的图片判断成了 3
  3. 计算错误程度(损失函数),然后告诉所有探测器和判断器:
    「刚才判断错了,你们的经验值需要调整!」
  4. 探测器和判断器根据错误,一点点修改自己的经验值(优化器),让下次判断更准
  5. 重复 1-4 步 5 次(epochs=5),机器人的经验就越来越准
用生活场景再串一遍流程:

看数字图片(28×28拼图) → 拆成784个小方块 → 
↓ 128个特征探测器(找圆圈、竖线等)
每个探测器看所有小方块,打分 → 过滤掉低分 → 
↓ 10个数字判断器(对应0-9)
每个判断器看所有特征分数,算总分 → 选总分最高的数字

为什么叫「全连接」?
  • 特征探测器(隐藏层神经元):每个都看所有 784 个像素小方块(全连接输入层)
  • 数字判断器(输出层神经元):每个都看所有 128 个特征分数(全连接隐藏层)
    (就像每个探测器都能看到所有输入积木,每个判断器都能看到所有探测器的结果)
🌰 现在你可以这样操作:
  1. 找一张手写数字图片,手动拆成 784 个小方块(想象)
  2. 假设有 128 个小卡片,每个卡片写着一个特征(比如「是否有上半圆」)
  3. 每个卡片都看所有小方块,根据「经验」给特征打分(比如看到上半圆就打高分)
  4. 扔掉打分低的卡片,只保留高分的
  5. 用 10 张数字卡片,每个卡片根据剩下的高分特征,算总分(比如数字 8 卡片看到两个高分圆圈特征,总分就高)
  6. 总分最高的数字卡片,就是识别结果

 代码实战

1. 导入必要的库

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets,transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt

  • torch:PyTorch 深度学习框架的核心库,提供张量计算、自动微分等功能
  • torch.nn:用于构建神经网络的模块,包含各种层类型(如全连接层、卷积层)和损失函数
  • torch.optim:优化器库,包含如 SGD、Adam 等多种优化算法
  • torchvision.datasets:提供常用的计算机视觉数据集,如 MNIST、CIFAR-10 等
  • torchvision.transforms:用于图像预处理和数据增强的工具
  • DataLoader:用于批量加载和处理数据的实用类
  • matplotlib.pyplot:用于数据可视化的库

2. 设备配置

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

# 如果有GPU,打印详细信息
if torch.cuda.is_available():
    print(f"GPU名称: {torch.cuda.get_device_name(0)}")
    print(f"GPU内存: {torch.cuda.get_device_properties(0).total_memory / 1024 / 1024} MB")

  • CUDA:NVIDIA 提供的 GPU 计算平台,可加速深度学习训练和推理
  • torch.device:用于指定张量和模型运行的设备(CPU 或 GPU)
  • 模型和数据迁移:通过.to(device)方法将模型和数据移至指定设备
  • 多 GPU 支持:若有多个 GPU,可通过torch.cuda.device_count()查询数量,并通过device_ids参数指定使用哪些 GPU

3. 数据预处理

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

  • transforms.Compose:将多个预处理操作组合成一个管道
  • transforms.ToTensor()
    • 将 PIL Image 或 numpy.ndarray 转换为 PyTorch 张量
    • 将像素值从 [0, 255] 缩放到 [0, 1]
    • 调整维度顺序:H×W×C → C×H×W(PyTorch 要求的格式)
  • transforms.Normalize
    • 标准化处理,使数据具有零均值和单位方差
    • 公式:output = (input - mean) / std
    • MNIST 数据集的全局统计值:mean=0.1307,std=0.3081

4. 加载数据集

train_dataset = datasets.MNIST('data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000)

  • datasets.MNIST
    • 自动下载和加载 MNIST 数据集(60,000 张训练图像 + 10,000 张测试图像)
    • 每张图像:28×28 像素的灰度手写数字(0-9)
  • DataLoader
    • batch_size:每次迭代加载的样本数
    • shuffle=True:每个训练周期重新打乱数据,增加随机性
    • num_workers:可设置多线程数据加载(代码中未显式设置,默认为 0)

5. 定义模型

class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(28*28, 128)
        self.fc2 = nn.Linear(128, 10)
        self.relu = nn.ReLU()
        
    def forward(self, x):
        x = x.view(-1, 28*28)  # 展平图像:[batch_size, 1, 28, 28] → [batch_size, 784]
        x = self.relu(self.fc1(x))  # 第一个全连接层 + ReLU激活
        x = self.fc2(x)  # 第二个全连接层(输出层)
        return x

  • nn.Module:所有神经网络模块的基类
  • 模型结构
    • 输入层:784 个神经元(28×28 像素)
    • 隐藏层:128 个神经元,使用 ReLU 激活函数
    • 输出层:10 个神经元,对应 0-9 的 10 个类别
  • 激活函数
    • ReLU:修正线性单元,公式:f(x) = max(0, x)
    • 引入非线性,解决线性模型无法拟合复杂函数的问题
  • 参数数量
    • fc1:784×128 + 128 = 100,480
    • fc2:128×10 + 10 = 1,290
    • 总计:100,480 + 1,290 = 101,770 个可训练参数

6. 模型初始化、损失函数和优化器

model = SimpleModel().to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
  • 模型初始化
    • 创建 SimpleModel 实例
    • 将模型参数移至指定设备(CPU/GPU)
  • 损失函数
  • 优化器:(Adaptive Moment Estimation)

 

7. 模型训练

model.train()  # 设置模型为训练模式(启用Dropout、BatchNorm等)
epochs = 5
for epoch in range(epochs):
    running_loss = 0.0
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()  # 清零梯度(避免梯度累积)
        data, target = data.to(device), target.to(device)  # 数据移至设备
        
        output = model(data)  # 前向传播
        loss = loss_function(output, target)  # 计算损失
        loss.backward()  # 反向传播
        optimizer.step()  # 更新参数
        
        running_loss += loss.item()  # 累加损失
        if batch_idx % 100 == 0:
            print(f"Epoch{epoch+1}, Batch{batch_idx}, Loss{loss.item():.4f}")
    print(f"Epoch{epoch+1}, AverageLoss{running_loss / len(train_loader):.4f}")

  • 训练循环结构
    • 外层循环:遍历训练周期(epochs)
    • 内层循环:遍历训练数据加载器中的所有批次
  • 梯度清零
    • optimizer.zero_grad():每个批次前需要清零梯度
    • 因为 PyTorch 默认会累积梯度
  • 前向传播
    • output = model(data):调用模型的forward()方法
    • 输入:[batch_size, 1, 28, 28]
    • 输出:[batch_size, 10](未归一化的分数,称为 logits)
  • 损失计算
    • loss_function(output, target)
      • 自动将 target 转换为 one-hot 编码
      • 对 output 应用 softmax,得到概率分布
      • 计算与真实标签的交叉熵
  • 反向传播
    • loss.backward()
      • 计算损失对所有可训练参数的梯度
      • 使用链式法则自动求导
  • 参数更新
    • optimizer.step()
      • 根据优化器的更新规则调整参数
      • 例如 Adam 会结合梯度的一阶矩估计(均值)和二阶矩估计(方差)

8. 模型评估(测试集)

model.eval()  # 设置模型为评估模式(禁用Dropout、BatchNorm等)
correct = 0
total = 0
with torch.no_grad():  # 禁用梯度计算,节省内存并加速计算
    for data, target in test_loader:
        data, target = data.to(device), target.to(device)
        output = model(data)
        _, predicted = torch.max(output.data, 1)  # 获取预测类别
        total += target.size(0)  # 累加总样本数
        correct += (predicted == target).sum().item()  # 累加正确预测数

print(f"Test Accuracy:{100*correct/total}%")

  • 模型模式切换
    • model.eval()
      • 关闭训练特有的层(如 Dropout、BatchNorm)
      • 确保评估结果的一致性
  • 禁用梯度计算
    • with torch.no_grad()
      • 避免在评估过程中计算梯度
      • 减少内存消耗并加速计算
  • 预测过程
    • torch.max(output.data, 1)
      • 对输出的每个样本(行),找到最大值所在的索引
      • 返回两个值:最大值和最大值的索引
      • 索引对应预测的类别(0-9)

9. 可视化预测结果

model.eval()
num_samples = 5
fig, axes = plt.subplots(1, num_samples, figsize=(15, 3))

with torch.no_grad():
    for i, (data, target) in enumerate(test_loader):
        data, target = data.to(device), target.to(device)
        output = model(data)
        _, predicted = torch.max(output, 1)

        for j in range(num_samples):
            img = data[j].view(28, 28).cpu().numpy()  # 重塑为28×28并移至CPU
            axes[j].imshow(img, cmap='gray')  # 显示图像
            axes[j].set_title(f'Pred: {predicted[j]}, True: {target[j]}')  # 设置标题
            axes[j].axis('off')  # 关闭坐标轴

        break  # 只显示一个批次的样本

plt.tight_layout()  # 调整布局
plt.show()  # 显示图像

  • 图像预处理还原
    • data[j].view(28, 28):将张量从 [1, 28, 28] 重塑为 [28, 28]
    • .cpu().numpy():将张量从 GPU 移至 CPU 并转换为 numpy 数组
  • matplotlib 绘图
    • plt.subplots(1, num_samples):创建 1 行 num_samples 列的子图
    • imshow(img, cmap='gray'):以灰度图显示图像
    • set_title():设置子图标题,显示预测类别和真实类别
    • axis('off'):关闭坐标轴,使图像更美观

补充说明:MNIST 数据集特性

  • 数据规模
    • 训练集:60,000 张图像
    • 测试集:10,000 张图像
  • 数据格式
    • 单通道灰度图像
    • 尺寸:28×28 像素
    • 像素值范围:[0, 255](原始数据),预处理后变为 [0, 1] 再标准化
  • 类别分布
    • 10 个类别(数字 0-9)
    • 每个类别在训练集和测试集中均匀分布

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐