刘二大人第 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 形状公式⭐⭐⭐
设:
- :输入特征图的宽度(高度同理)
- K:卷积核的尺寸()
- S:步长(stride),卷积核每次移动的像素数
- P:填充(padding),在输入特征图四周填充的像素层数
输出特征图的宽度 (W_{out}) 和高度 (H_{out}) 计算公式为:
关键说明
-
公式结果必须是整数,若计算后为小数,说明该卷积参数组合不合法(PyTorch 中会直接报错)。
-
⭐当需要输出尺寸与输入尺寸相同时,满足 ,代入公式可得 ,因此卷积核尺寸 K 通常取奇数(如 3、5、7)。
10.4.2、 池化层 Feature Map 形状公式
池化层(如 Max Pooling、Avg Pooling)的计算逻辑和卷积层一致,公式完全相同。
关键说明
-
池化层的 padding 通常为 0,步长 S 一般等于池化核尺寸 K(如 2×2 池化,(S=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 轮,可有效训练深层网络。