Machine Learning: 卷积神经网络CNN — 图像分类与商品识别指南
CNN给计算机装上了"眼睛"——卷积核就像视觉感受器,逐层从边缘到物体识别图像。
1. 你将学到
- 卷积操作:卷积核/滤波器、步长Stride、填充Padding、多通道卷积
- 池化层:MaxPool/AvgPool,空间降维与平移不变性
- 经典架构:LeNet → AlexNet → VGG → ResNet的演进与残差连接
- 迁移学习:预训练模型(ImageNet) + Fine-tuning,少量数据也能高精度
- SalesPredict商品图片分类:自动识别商品类目
2. 一个电商运营的真实故事
(1) 痛点:新品上架人工分类太慢且不一致
Bob平台每天有1 thousand个新品上架,运营团队需要手动将商品图片归入正确的类目。人工分类速度慢(每张5秒)、不一致(同一商品不同人分到不同类)、且成本高(每月30 thousand USD人工费)。图片分类是电商运营的瓶颈。
(2) CNN的解法
CNN能自动从商品图片中学习视觉特征,实现自动分类——速度毫秒级、一致性强、零人工成本。
PYTHON
import torchvision.models as models
# Transfer learning: use pre-trained ResNet
model = models.resnet18(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, num_classes) # Replace last layer
(3) 收益:分类准确率95%,每月节省30千USD
Bob用CNN+迁移学习实现商品自动分类,准确率95%以上,处理速度从5秒/张降到0.01秒/张,每月节省30 thousand USD人工成本。
3. 卷积操作原理
(1) 卷积核与特征提取
graph TB
INPUT[Input Image<br/>H x W x C] --> CONV1[Conv Layer 1<br/>Edges & Lines]
CONV1 --> POOL1[Pool Layer 1<br/>Reduce Spatial]
POOL1 --> CONV2[Conv Layer 2<br/>Textures & Parts]
CONV2 --> POOL2[Pool Layer 2]
POOL2 --> CONV3[Conv Layer 3<br/>Objects & Shapes]
CONV3 --> FLATTEN[Flatten]
FLATTEN --> FC[Fully Connected]
FC --> OUTPUT[Class Probabilities]
▶ 示例:手动实现卷积操作
PYTHON
import torch
import torch.nn as nn
# Input: 1 image, 1 channel, 5x5
input_img = torch.tensor([[
[1, 2, 0, 1, 3],
[0, 1, 2, 3, 1],
[1, 3, 1, 0, 2],
[2, 0, 3, 1, 1],
[0, 1, 2, 0, 1],
]], dtype=torch.float32).unsqueeze(0) # shape: (1, 1, 5, 5)
# Conv2d: 1 input channel, 1 output channel, 3x3 kernel
conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=0)
print(f"Kernel weights:\n{conv.weight.data.squeeze()}")
print(f"Bias: {conv.bias.data.item():.4f}")
output = conv(input_img)
print(f"\nInput shape: {input_img.shape}")
print(f"Output shape: {output.shape}")
print(f"Output:\n{output.squeeze().detach()}")
输出:
TEXT
📖 仅展示
# 执行成功
(2) 卷积参数
| 参数 | 含义 | 效果 |
|---|---|---|
| kernel_size | 卷积核大小 | 3x3(常用)/5x5/7x7 |
| stride | 步长 | 1(默认)/2(下采样) |
| padding | 填充 | 0(缩小)/1(保持尺寸) |
| in_channels | 输入通道数 | RGB=3, 灰度=1 |
| out_channels | 输出通道数 | 即卷积核数量 |
▶ 示例:不同卷积配置对比
PYTHON
import torch.nn as nn
x = torch.randn(1, 3, 32, 32) # 1 batch, 3 channels, 32x32
configs = {
"3x3, stride=1, pad=1": nn.Conv2d(3, 16, 3, stride=1, padding=1),
"3x3, stride=2, pad=1": nn.Conv2d(3, 16, 3, stride=2, padding=1),
"5x5, stride=1, pad=2": nn.Conv2d(3, 16, 5, stride=1, padding=2),
"5x5, stride=2, pad=0": nn.Conv2d(3, 16, 5, stride=2, padding=0),
}
for name, layer in configs.items():
out = layer(x)
print(f"{name:25s}: {x.shape} → {out.shape}")
输出:
TEXT
📖 仅展示
# 执行成功
4. 池化层与CNN架构
(1) 池化操作
▶ 示例:MaxPool与AvgPool
PYTHON
import torch
import torch.nn as nn
x = torch.tensor([[
[1, 3, 2, 4],
[5, 6, 7, 8],
[9, 2, 1, 3],
[4, 5, 6, 7],
]], dtype=torch.float32).unsqueeze(0) # (1, 1, 4, 4)
maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
avgpool = nn.AvgPool2d(kernel_size=2, stride=2)
print(f"Input:\n{x.squeeze()}")
print(f"\nMaxPool (2x2):\n{maxpool(x).squeeze()}")
print(f"\nAvgPool (2x2):\n{avgpool(x).squeeze()}")
输出:
TEXT
📖 仅展示
# 执行成功
(2) 经典架构演进
| 架构 | 年份 | 创新 | 层数 | 参数量 |
|---|---|---|---|---|
| LeNet | 1998 | 首个CNN | 5 | 60K |
| AlexNet | 2012 | ReLU+Dropout+GPU | 8 | 60M |
| VGG | 2014 | 小卷积核(3x3)堆叠 | 16-19 | 138M |
| ResNet | 2015 | 残差连接(skip connection) | 18-152 | 11-60M |
▶ 示例:用PyTorch实现简单CNN
PYTHON
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self, num_classes=5):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1), # 3→32 channels, 32x32
nn.ReLU(),
nn.MaxPool2d(2, 2), # 32→16x16
nn.Conv2d(32, 64, 3, padding=1), # 32→64 channels, 16x16
nn.ReLU(),
nn.MaxPool2d(2, 2), # 16→8x8
nn.Conv2d(64, 128, 3, padding=1), # 64→128 channels, 8x8
nn.ReLU(),
nn.MaxPool2d(2, 2), # 8→4x4
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
model = SimpleCNN(num_classes=5)
print(model)
print(f"Parameters: {sum(p.numel() for p in model.parameters()):,}")
输出:
TEXT
📖 仅展示
# 函数定义成功
5. 迁移学习
(1) 为什么迁移学习有效
ImageNet预训练模型已学到通用视觉特征(边缘→纹理→物体部件),只需替换最后一层即可适配新任务。
▶ 示例:ResNet18迁移学习
PYTHON
import torch
import torch.nn as nn
import torchvision.models as models
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import numpy as np
# Load pre-trained ResNet18
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# Freeze all layers (feature extractor mode)
for param in model.parameters():
param.requires_grad = False
# Replace the last fully connected layer
num_classes = 5 # Electronics, Clothing, Food, Books, Home
model.fc = nn.Linear(model.fc.in_features, num_classes)
# Only the new fc layer has trainable parameters
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"Trainable: {trainable:,} / Total: {total:,} ({trainable/total*100:.1f}%)")
# Data transforms for pre-trained model
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
输出:
TEXT
📖 仅展示
# 执行成功
(2) Fine-tuning策略
| 策略 | 冻结层数 | 训练数据量 | 训练时间 | 适用场景 |
|---|---|---|---|---|
| Feature Extractor | 全部(只训练fc) | 少(<1k) | 快 | 数据很少 |
| Partial Fine-tune | 后几层 | 中(1-10k) | 中 | 数据适中 |
| Full Fine-tune | 无 | 多(>10k) | 慢 | 数据充足 |
▶ 示例:Fine-tune后几层
PYTHON
import torchvision.models as models
import torch.nn as nn
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# Freeze early layers, unfreeze layer4
for name, param in model.named_parameters():
if "layer4" in name or "fc" in name:
param.requires_grad = True
else:
param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, 5)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Fine-tune trainable params: {trainable:,}")
输出:
TEXT
📖 仅展示
# 执行成功
6. SalesPredict商品图片分类
▶ 示例:完整迁移学习训练流程
PYTHON
import torch
import torch.nn as nn
import torchvision.models as models
from torch.utils.data import DataLoader, TensorDataset
from sklearn.model_selection import train_test_split
import numpy as np
# Simulate image data (in practice, load real images with ImageFolder)
rng = np.random.default_rng(42)
n = 2000
X = rng.standard_normal((n, 3, 224, 224)).astype(np.float32) # 3-channel 224x224
y = rng.integers(0, 5, n) # 5 product categories
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
train_ds = TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train))
test_ds = TensorDataset(torch.FloatTensor(X_test), torch.LongTensor(y_test))
train_loader = DataLoader(train_ds, batch_size=32, shuffle=True)
test_loader = DataLoader(test_ds, batch_size=64)
# Pre-trained ResNet18 with custom head
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
for param in model.parameters():
param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, 5)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)
# Training
for epoch in range(10):
model.train()
correct, total = 0, 0
for X_batch, y_batch in train_loader:
y_pred = model(X_batch)
loss = criterion(y_pred, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
correct += (y_pred.argmax(1) == y_batch).sum().item()
total += y_batch.size(0)
if epoch % 2 == 0:
print(f"Epoch {epoch}: loss={loss.item():.4f}, acc={correct/total:.3f}")
# Evaluate
model.eval()
correct, total = 0, 0
with torch.no_grad():
for X_batch, y_batch in test_loader:
y_pred = model(X_batch)
correct += (y_pred.argmax(1) == y_batch).sum().item()
total += y_batch.size(0)
print(f"\nTest Accuracy: {correct/total:.3f}")
输出:
TEXT
📖 仅展示
# 执行成功
❓ 常见问题
Q CNN为什么比MLP更适合图像?
A 三个原因——1) 参数共享(同一卷积核扫全图,参数少);2) 局部连接(每个神经元只看局部区域);3) 平移不变性(物体出现在任何位置都能识别)。MLP把图像展平会丢失空间结构。
Q 迁移学习需要多少数据?
A Feature Extractor模式(只训练fc)只需几百张;Fine-tune后几层需1-10k张;从头训练需10k+张。数据越少,冻结越多层。
Q 3x3卷积核为什么最常用?
A 两个3x3卷积堆叠的感受野等于一个5x5,但参数更少(18 vs 25)且非线性更强(2次ReLU)。VGG证明小核堆叠比大核更好。
Q ResNet的残差连接是什么?
A y = F(x) + x,让网络学习残差(增量)而非完整映射。好处——解决深层网络梯度消失问题,允许训练100+层网络。
Q 图像预处理为什么要Normalize?
A 预训练模型用ImageNet的均值/标准差归一化。新数据必须用相同归一化,否则特征分布不匹配,迁移学习效果差。
Q 怎么处理类别不平衡?
A 三种方法——1) 加权损失函数(nn.CrossEntropyLoss(weight=class_weights));2) 过采样少数类;3) 数据增强(随机翻转/旋转/裁剪)。
📖 小节
- 卷积核像视觉感受器,逐层提取边缘→纹理→部件→物体特征
- 池化层降低空间维度,增加平移不变性,减少计算量
- CNN架构从LeNet到ResNet不断演进,残差连接解决深层网络训练问题
- 迁移学习:冻结预训练层+替换分类头,少量数据也能达到高精度
- 商品分类三策略:Feature Extractor(数据少)、Partial Fine-tune(中等)、Full Fine-tune(数据多)
- 图像预处理必须与预训练模型一致(Resize+Normalize)
📝 作业
- 基础题(难度⭐):用nn.Conv2d+nn.MaxPool2d构建一个2层CNN,输入3x32x32,输出16x8x8,打印每层形状。提示:Conv2d(3,8,3,pad=1)→Pool→Conv2d(8,16,3,pad=1)→Pool。
- 进阶题(难度⭐⭐):用迁移学习(ResNet18)对CIFAR-10分类(10类),冻结除fc外所有层,训练5个epoch。提示:torchvision.datasets.CIFAR10 + transforms。
- 挑战题(难度⭐⭐⭐):对比Feature Extractor vs Partial Fine-tune vs Full Fine-tune在CIFAR-10上的精度和训练时间,画出三种策略的accuracy vs epoch曲线。提示:分别冻结不同层数,记录每个epoch的test accuracy。