刘二大人第9-11讲-从全连接到CNN-有代码

52 阅读7分钟

刘二大人第 9-11 讲 - 从全连接到 CNN

9. 多分类问题

本节核心是用 softmax 解决多分类问题,以 MNIST 数字识别为案例,目标是让输出为概率分布(各值≥0、总和为 1,且具有竞争性)。

9.1 Softmax

  • 核心特性:指数函数保证单调性和正值,通过 “自身指数 / 所有类别指数和” 得到概率占比,满足分布要求。

  • 作用:将模型原始输出转化为概率分布,便于后续损失计算和分类判断。

9.2 损失函数

  • 关键结论:CrossEntropyLoss = Softmax + NLLLoss

  • 原理:结合 one-hot 编码,Loss = -log (预测正确类别的概率),概率越大 Loss 越小,符合优化目标。

  • 注意:模型最后一层无需激活函数(函数内置 softmax

  • 标签 y 需为 LongTensor(如y = torch.LongTensor([0])表示第 0 类为正确答案)

9.3 案例:MNIST 数字识别(全连接网络)

9.3.0 任务描述

输入 MNIST 灰度数字图片(像素值 0-1,黑 = 1、白 = 0),输出对应数字(0-9)的分类概率。

9.3.1 所需依赖
import torch
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim
9.3.2 步骤一:数据准备
batch_size = 64
# 图像预处理:转为Tensor + 标准化(均值0.1307、标准差0.3081为MNIST经验值)
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])
# 加载训练集和测试集
train_dataset = datasets.MNIST(root='../data/mnist/', train=True, transform=transform, download=True)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = datasets.MNIST(root='../data/mnist/', train=False, transform=transform, download=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
9.3.3 步骤二:模型定义(全连接)
  • 结构:输入层(28×28=784 维)→ 隐藏层(512→256→128→64)→ 输出层(10 维)
  • 激活函数:ReLU(输出层无激活,依赖 CrossEntropyLoss 内置 softmax)
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = torch.nn.Linear(784, 512)
        self.fc2 = torch.nn.Linear(512, 256)
        self.fc3 = torch.nn.Linear(256, 128)
        self.fc4 = torch.nn.Linear(128, 64)
        self.fc5 = torch.nn.Linear(64, 10)
        
    def forward(self, x):
        x = x.view(-1, 784)  # 展平为(N,784),N为批次大小
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.relu(self.fc3(x))
        x = F.relu(self.fc4(x))
        x = self.fc5(x)
        return x
9.3.4 步骤三:损失函数与优化器
model = Net()
criterion = torch.nn.CrossEntropyLoss()  # 交叉熵损失(含softmax)
# SGD优化器(momentum=0.5:增加惯性,避免局部最优)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
9.3.5 步骤四:训练与测试函数
def train(epoch):
    model.train()  # 训练模式
    running_loss = 0.0
    for batch_idx, data in enumerate(train_loader, 0):
        inputs, labels = data
        # 前向传播+反向传播+参数更新
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        optimizer.zero_grad()  # 清空梯度
        loss.backward()  # 反向传播
        optimizer.step()  # 更新参数
        
        running_loss += loss.item()
        # 每300批次打印平均损失
        if batch_idx % 300 == 299:
            print(f'[轮次:{epoch+1}, 组号:{batch_idx+1}] 平均损失: {running_loss/300:.3f}')
            running_loss = 0.0

def test():
    model.eval()  # 测试模式
    correct = 0
    total = 0
    with torch.no_grad():  # 禁用梯度计算
        for data in test_loader:
            inputs, labels = data
            outputs = model(inputs)
            _, predicted = torch.max(outputs.data, dim=1)  # 取概率最大的类别
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    print(f'测试集准确率: {100*correct/total:.0f}%')
9.3.6 完整训练流程
if __name__ == '__main__':
    # 训练10轮+每轮测试
    for epoch in range(10):
        train(epoch)
        test()
9.3.7 结果

全连接网络最高准确率约 97%,但存在缺陷:展平图像会丢失空间信息(如相邻像素距离变大)。

10. CNN 基础篇

10.1 CNN 核心优势

  • 解决全连接缺陷:保留图像空间特征,通过卷积层自动提取特征(无需人工设计)。

  • 网络结构:卷积层(提取特征)→ 下采样层(池化,降维减算)→ 全连接层(分类)。

10.2 卷积层(Convolution)

10.2.1 核心计算
  • 多通道处理:输入通道数 = 卷积核输入通道数,输出通道数 = 卷积核个数。

  • 运算逻辑:每个通道与卷积核对应通道逐元素相乘,求和后拼接多通道结果。

10.2.2 关键参数
  • Padding:外围填充零。

  • Stride:步长,控制"卷积核"移动间隔(默认 1)。

10.3 下采样(池化层)

  • 作用:保留核心特征,减少参数和计算量。

  • 常用:MaxPooling(取局部最大值)。

10.4.1、 卷积层 Feature Map 形状公式⭐⭐⭐

设:

  • (Win)(W_{in}):输入特征图的宽度(高度同理)
  • K:卷积核的尺寸((K×K)(K \times K)
  • S:步长(stride),卷积核每次移动的像素数
  • P:填充(padding),在输入特征图四周填充的像素层数

输出特征图的宽度 (W_{out}) 和高度 (H_{out}) 计算公式为:

(Wout=WinK+2PS+1)(W_{out}=\frac{W_{in} - K + 2P}{S} + 1)

关键说明

  1. 公式结果必须是整数,若计算后为小数,说明该卷积参数组合不合法(PyTorch 中会直接报错)。

  2. ⭐当需要输出尺寸与输入尺寸相同时,满足 (Win=Wout)(W_{in}=W_{out}),代入公式可得 (P=K12)(P=\frac{K-1}{2}),因此卷积核尺寸 K 通常取奇数(如 3、5、7)。

10.4.2、 池化层 Feature Map 形状公式

池化层(如 Max Pooling、Avg Pooling)的计算逻辑和卷积层一致,公式完全相同。

关键说明

  1. 池化层的 padding 通常为 0,步长 S 一般等于池化核尺寸 K(如 2×2 池化,(S=2)),此时公式简化为:(Wout=WinK)(W_{out}=\frac{W_{in}}{K})

  2. 池化层的核心作用是降维,因此输出尺寸一定小于输入尺寸。

10.4.3、 多通道说明

上述公式仅计算 feature map 的宽高,feature map 的通道数由卷积核的个数决定,与宽高计算无关:

  • ⭐卷积层输出通道数 = 卷积核的个数

  • 池化层不改变通道数

10.4 案例:MNIST 识别(CNN 网络)

10.4.1 模型定义(CNN)
  • 结构:卷积层 1(1→10 通道)→ 池化 → 卷积层 2(10→20 通道)→ 池化 → 全连接层(320→10)
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=5)
        self.pooling = torch.nn.MaxPool2d(2)
        self.fc = torch.nn.Linear(320, 10)  # 20通道×4×4=320
        
    def forward(self, x):
        batch_size = x.size(0)
        x = F.relu(self.pooling(self.conv1(x)))  # 卷积→池化→激活
        x = F.relu(self.pooling(self.conv2(x)))
        x = x.view(batch_size, -1)  # 展平
        x = self.fc(x)
        return x
10.4.2 GPU 加速(可选)
# 模型移到GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
# 训练/测试时,数据移到GPU
inputs, labels = inputs.to(device), labels.to(device)
10.4.3 完整代码

复用 9.3 的依赖、数据准备、训练 / 测试函数,仅替换模型定义,训练 10 轮。

10.4.4 结果

CNN 最高准确率达 98%,优于全连接网络(保留空间特征 + 自动特征提取)。

11. CNN 高级篇

11.1 GoogleNet

11.1.1 核心思想
  • Inception 块:并行使用 1×1、3×3、5×5 卷积核 + 池化,拼接结果(自动选择最优特征)。

  • 1×1 卷积:降维减算(如将 192 通道→16 通道),同时融合多通道信息。

11.1.2 1×1 卷积的优势(计算量对比)
  • 未使用:5×5 卷积总运算量 = 5²×28²×192×32。

  • 使用后:1×1 降维(1²×28²×192×16)+5×5 卷积(5²×28²×16×32),总运算量仅为原来的 1/10。

11.1.3 Inception 块实现
class InceptionA(torch.nn.Module):
    def __init__(self, in_channels):
        super(InceptionA, self).__init__()
        # 四个分支并行
        self.branch1x1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)
        self.branch5x5_1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)  # 降维
        self.branch5x5_2 = torch.nn.Conv2d(16, 24, kernel_size=5, padding=2)
        self.branch3x3_1 = torch.nn.Conv2d(in_channels, 16, kernel_size=1)  # 降维
        self.branch3x3_2 = torch.nn.Conv2d(16, 24, kernel_size=3, padding=1)
        self.branch3x3_3 = torch.nn.Conv2d(24, 24, kernel_size=3, padding=1)
        self.branch_pool = torch.nn.Conv2d(in_channels, 24, kernel_size=1)  # 池化后降维

    def forward(self, x):
        branch1x1 = self.branch1x1(x)
        branch5x5 = F.relu(self.branch5x5_2(self.branch5x5_1(x)))
        branch3x3 = F.relu(self.branch3x3_3(self.branch3x3_2(self.branch3x3_1(x))))
        branch_pool = self.branch_pool(F.avg_pool2d(x, kernel_size=3, stride=1, padding=1))
        return torch.cat([branch1x1, branch5x5, branch3x3, branch_pool], dim=1)  # 通道维度拼接
11.1.4 GoogleNet 完整模型
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = torch.nn.Conv2d(88, 20, kernel_size=5)  # Inception输出88通道
        self.inception1 = InceptionA(in_channels=10)  # 接conv1(10通道)
        self.inception2 = InceptionA(in_channels=20)  # 接conv2(20通道)
        self.mp = torch.nn.MaxPool2d(2)
        self.fc = torch.nn.Linear(1408, 10)  # 88通道×4×4=1408

    def forward(self, x):
        batch_size = x.size(0)
        x = F.relu(self.mp(self.conv1(x)))  # (N,10,12,12)
        x = self.inception1(x)  # (N,88,12,12)
        x = F.relu(self.mp(self.conv2(x)))  # (N,20,4,4)
        x = self.inception2(x)  # (N,88,4,4)
        x = x.view(batch_size, -1)
        x = self.fc(x)
        return x
11.1.5 训练流程

复用前文数据准备、训练 / 测试函数,GPU 加速逻辑同上,训练 10 轮。

11.2 ResNet(残差网络)

11.2.1 核心问题:梯度消失
  • 深层网络中,反向传播时梯度经链式法则不断相乘,可能趋近于 0,导致参数无法更新。

  • 解决方案:跳连接(Residual Connection),将输入 x 直接与权重层输出相加,梯度公式为∂H/∂x = ∂F/∂x + 1,避免梯度消失。

11.2.2 残差块(Residual Block)实现
class ResidualBlock(torch.nn.Module):
    def __init__(self, channels):
        super(ResidualBlock, self).__init__()
        self.conv1 = torch.nn.Conv2d(channels, channels, kernel_size=3, padding=1)
        self.conv2 = torch.nn.Conv2d(channels, channels, kernel_size=3, padding=1)

    def forward(self, x):
        y = F.relu(self.conv1(x))
        y = self.conv2(y)
        return F.relu(x + y)  # 跳连接:x直接相加
11.2.3 ResNet 完整模型
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=5)
        self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=5)
        self.mp = torch.nn.MaxPool2d(2)
        self.rblock1 = ResidualBlock(16)  # 16通道残差块
        self.rblock2 = ResidualBlock(32)  # 32通道残差块
        self.fc = torch.nn.Linear(512, 10)  # 32通道×4×4=512

    def forward(self, x):
        batch_size = x.size(0)
        x = F.relu(self.mp(self.conv1(x)))  # (N,16,12,12)
        x = self.rblock1(x)  # 残差块(尺寸不变)
        x = F.relu(self.mp(self.conv2(x)))  # (N,32,4,4)
        x = self.rblock2(x)  # 残差块(尺寸不变)
        x = x.view(batch_size, -1)
        x = self.fc(x)
        return x
11.2.4 训练流程

复用前文数据准备、训练 / 测试函数,GPU 加速逻辑同上,训练 10 轮,可有效训练深层网络。