处理图像分类问题时,很多新手先想到的是“把图片拉成一维向量,丢进全连接网络”。这个思路逻辑上没错,但真正动手后会遇到两个问题:一是图像尺寸稍大,全连接层的参数数量就会爆炸;二是把像素顺序打散后,图像里的空间结构、边缘纹理、局部关系全部丢失。卷积神经网络(CNN)就是为了解决这两个问题出现的。它用卷积层做局部特征提取,用池化层做下采样,用激活函数引入非线性,最后用全连接层输出分类结果。这篇文章会围绕 CNN 的核心组件逐一拆解,并结合 LeNet-5、AlexNet 两个经典网络说明架构演化逻辑,最后用 PyTorch 跑通一个最小可用的图像分类示例。学完后,你能理解卷积核尺寸、步长、填充、输出尺寸计算这些概念,也能在遇到训练不收敛、shape 不匹配、过拟合这些问题时,知道从哪一层、哪一个参数开始排查。
1. 先理解卷积神经网络为什么能解决图像分类问题
1.1 图像分类的本质是从像素到特征
一张 32x32 的彩色图片,在计算机里就是 32x32x3 的数组,三个通道对应 RGB。要判断“这张图是不是猫”,不能直接拿 3072 个数字和“猫”这个标签做比较。机器需要先找到有区分度的模式:边缘、角点、颜色块、纹理、器官形状。传统机器学习方法会手工设计特征,比如颜色直方图、HOG、SIFT,然后交给 SVM 或随机森林分类。问题在于,不同数据集上有效的特征差异很大,手工设计特征既耗时,又难以覆盖复杂场景。CNN 的思路是把“特征提取”也变成可学习的一部分,让卷积核在训练过程中自动调整,从数据里学出不同层次的模式。这也是为什么现代计算机视觉任务几乎都默认使用卷积网络或它的变体,而不是回到手工特征时代。
1.2 全连接网络处理图像的三个痛点
第一个痛点是参数爆炸。假设输入是一张 224x224 的彩色图片,拉平后是 150528 维。如果第一个全连接层有 1024 个神经元,这一层的权重参数就有 1.5 亿个。在普通显卡上很难训练,而且很容易过拟合。第二个痛点是丢失空间结构。图像拉平后,相邻像素之间的空间关系被打乱,模型很难再学习“邻近像素构成边缘”这种规律。第三个痛点是位置敏感。全连接网络对位置变化非常敏感,同一只猫从图片左边移到右边,它会当成两个完全不同的输入。CNN 通过局部连接、权值共享和池化,从机制上缓解了这三个问题,让模型既能控制参数量,又能保留图像本身的空间信息。
1.3 CNN 的三大设计思想
第一个思想是局部感知。每一个卷积核只观察输入的一个小窗口,比如 3x3 或 5x5 的区域,而不是看整张图。这符合视觉系统先看局部再看整体的特点。第二个思想是权值共享。同一个卷积核在整张图上滑动时,权重是相同的,这样既大幅减少参数,又让同一个模式在不同位置都能被检测到。第三个思想是层次化抽象。底层卷积核通常学到边缘和颜色块,中层学到纹理和局部形状,高层学到眼睛、轮子这类语义组件,最后由全连接层组合成类别判断。理解了这三个思想,后面看每一层的设计就不会觉得是孤立的。
2. CNN 核心组件拆解:卷积层、激活函数、池化层
2.1 卷积层的计算过程
卷积层的核心是卷积核(filter)。以一个二维输入为例,输入是 5x5 的矩阵,卷积核是 3x3 的矩阵,步长为 1,不填充。卷积核从左上角开始,与输入对应位置做逐元素乘法,然后求和,得到一个输出值。之后卷积核向右滑动一个步长,继续计算,直到遍历整张输入。这样得到的输出通常叫特征图(feature map)。一个卷积层通常有多个卷积核,每个卷积核负责提取一种模式,所以输出会有多个通道。实际项目中,卷积层的输入输出都是四维张量,形状为(batch_size, channels, height, width),PyTorch 中的nn.Conv2d也遵循这个约定。
2.2 输出尺寸公式和三个关键参数
卷积输出尺寸的计算公式是:
输出尺寸 = floor((输入尺寸 + 2 * padding - kernel_size) / stride) + 1kernel_size 是卷积核大小,stride 是滑动步长,padding 是在输入四周补零的宽度。这三个参数相互影响。kernel_size 决定感受野大小,3x3 是当前最常用的选择,因为两个 3x3 卷积堆叠可以获得与一个 5x5 卷积相同的感受野,但参数量更少。stride 大于 1 时可以快速缩小特征图尺寸,但过大会丢失细节。padding 用来控制输出尺寸,常用配置是 padding=1、kernel_size=3、stride=1,这样输出尺寸与输入一致,方便搭建深层网络。
| 参数 | 含义 | 常见值 | 调大影响 | 调小影响 |
|---|---|---|---|---|
| kernel_size | 卷积核大小 | 3x3、5x5 | 感受野变大、参数增多 | 感受野变小、参数减少 |
| stride | 步长 | 1、2 | 输出变小,计算量下降 | 输出变大,保留更多细节 |
| padding | 补零宽度 | 0、1、2 | 输出变大,保留边界信息 | 输出变小,边界信息丢失 |
| out_channels | 卷积核数量 | 32、64、128 | 特征更丰富、计算量上升 | 特征不足,可能欠拟合 |
2.3 激活函数的作用:让网络具备非线性表达能力
如果不加激活函数,卷积层和全连接层无论堆叠多少层,本质上都是一个线性变换,无法拟合图像分类这种高度非线性的边界。激活函数在每一层输出之后,对结果做非线性变换。ReLU 是目前最常用的选择,公式是 f(x)=max(0,x)。它计算简单、收敛快,正区间梯度恒为 1,能缓解梯度消失问题。它的缺陷是负区间输出恒为 0,如果学习率过大,部分神经元可能永远不会被激活,这就是“神经元死亡”。Leaky ReLU 和 ELU 是常见替代方案,但实际项目中 ReLU 仍作为默认起点。Sigmoid 和 tanh 在早期网络中使用较多,但在深层网络中容易出现梯度消失,现在一般只用于最后一层做概率输出或特殊场景。
2.4 池化层:下采样与增强平移不变性
池化层的作用是降维。常用最大池化(Max Pooling)和平均池化(Average Pooling)。最大池化在每个窗口内取最大值,平均池化取平均值。池化操作没有可学习参数,它把特征图分成若干窗口,每个窗口输出一个值,从而缩小特征图尺寸,减少后续计算量,同时保留相对显著的特征。池化还带来一定程度的平移不变性:特征稍微移动几个像素,池化后的结果变化不大。窗口大小常见为 2x2,步长 2,输出尺寸刚好缩小一半。要注意池化不改变通道数,只改变每个通道的空间尺寸。在分类网络中,池化层还承担了扩大感受野的作用,让后续卷积能看到更大范围的上下文信息。
3. 全连接层与经典网络结构:LeNet-5 和 AlexNet
3.1 全连接层把特征映射成类别概率
经过若干卷积和池化之后,特征图已经包含从输入中学到的高层特征。全连接层的作用是把这些特征“摊平”,再做几次线性变换加激活,最终映射到类别数量。在 PyTorch 中,摊平使用view或flatten,然后送入nn.Linear。最后一个全连接层的输出维度等于类别数,通常接一个 softmax 或交叉熵损失。需要注意的是,全连接层是网络参数的主要来源,也是最容易过拟合的地方,因此现代网络中常用全局平均池化替代部分全连接层,比如 ResNet 的最后一个阶段。理解这一点,有助于理解为什么 LeNet-5 和 AlexNet 的参数量差异如此之大。
3.2 LeNet-5 逐层结构分析
LeNet-5 是 1998 年提出的经典网络,最初用于手写数字识别,输入是 32x32 的灰度图。它的结构如下:第一个卷积层 C1,用 6 个 5x5 卷积核,输出 6 张特征图,尺寸 28x28;然后是池化层 S2,2x2 平均池化,尺寸变为 14x14;接着卷积层 C3,用 16 个 5x5 卷积核,输出 16 张特征图,尺寸 10x10;再接池化层 S4,尺寸变为 5x5;然后 C5 是 120 个 5x5 卷积核,输出 120 张特征图;F6 是 84 维全连接层;最后输出层是 10 个类别。LeNet-5 的参数量大约 6 万个,在当年是里程碑式的设计。它的核心贡献是确立了“卷积层 + 池化层 + 全连接层”的基本组合模式,后续 CNN 的结构大多是在这个框架上演进。
3.3 AlexNet 的关键改进
AlexNet 在 2012 年把 ImageNet 分类错误率大幅拉低,是深度学习视觉方向进入主流视野的标志。它输入 224x224 彩色图,包含 5 个卷积层和 3 个全连接层,约 6000 万参数。相比 LeNet-5,AlexNet 的关键改进包括:使用 ReLU 替代 tanh,训练速度更快;使用 Dropout 随机丢弃部分神经元,缓解全连接层过拟合;使用 max pooling 加 overlapping pooling;使用数据增强,比如随机裁剪和水平翻转;在 GPU 上并行训练。从工程角度看,AlexNet 证明了更大数据、更深网络、更强算力三者结合能带来质的提升。今天很少直接复现完整 AlexNet 训练,但它的思想仍是理解现代 CNN 的基础。
LeNet-5 和 AlexNet 对比表:
| 对比项 | LeNet-5 | AlexNet |
|---|---|---|
| 提出时间 | 1998 | 2012 |
| 输入尺寸 | 32x32 灰度 | 224x224 彩色 |
| 卷积层数量 | 3(含 C5) | 5 |
| 全连接层数量 | 2 | 3 |
| 激活函数 | tanh/sigmoid | ReLU |
| 是否使用 Dropout | 否 | 是 |
| 参数量 | 约 6 万 | 约 6000 万 |
| 主要用途 | 手写数字识别 | 大型图像分类 |
3.4 为什么后来网络越来越深
LeNet-5 用两个卷积块就完成了数字识别,AlexNet 用五个卷积层处理了复杂自然图像。后来的 VGG 进一步把卷积核统一为 3x3,把网络加深到 16 至 19 层;ResNet 通过残差连接解决了深层网络退化问题,把网络加深到百层以上。深度增加的本质是让网络有机会学习更抽象、更全局的特征。但深度不是唯一因素,还要配合归一化、残差连接、更好的初始化策略。学习初期不建议一上来就啃 ResNet 的完整推导,先把 LeNet-5 和 AlexNet 的结构读懂,再看 VGG、ResNet 会顺畅得多。
4. 用 PyTorch 实现一个最小可运行的 CNN 分类器
4.1 环境准备与依赖确认
这里用 PyTorch 实现,目标数据集是 CIFAR-10,包含 10 个类别、50000 张训练图和 10000 张测试图,每张 32x32 彩色图,非常适合学习 CNN 基础。环境建议如下:
| 依赖 | 建议版本 | 说明 |
|---|---|---|
| Python | 3.9 或更高 | PyTorch 官方支持版本 |
| torch | 2.x | 根据本机 CUDA 版本安装 |
| torchvision | 与 torch 版本匹配 | 提供数据集和预训练模型 |
| 硬件 | CPU 可学习,GPU 训练更快 | 小数据集上 CPU 也能跑通 |
安装命令仅作示例,实际版本号以本地环境为准:
pip install torch torchvision如果只有 CPU,或者公司内网限制,安装后可以先打开 Python 验证是否可用:
import torch print(torch.__version__) print(torch.cuda.is_available())cuda.is_available()为 False 不代表不能用,只是当前环境没有可用的 CUDA 设备,代码在 CPU 上也能运行。这个步骤的目的是提前暴露 import 错误和版本冲突,避免后续训练到一半才发现环境有问题。
4.2 定义网络结构
针对 32x32 的 CIFAR-10,可以定义一个三层结构的简单 CNN。第一层卷积从 3 通道提取到 32 通道,第二层从 32 通道提取到 64 通道,然后经过两次最大池化,特征图从 32x32 缩到 8x8,最后展平送入全连接层。
import torch import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() self.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1) self.pool = nn.MaxPool2d(kernel_size=2, stride=2) self.fc1 = nn.Linear(64 * 8 * 8, 256) self.fc2 = nn.Linear(256, num_classes) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(x.size(0), -1) x = F.relu(self.fc1(x)) x = self.fc2(x) return x代码中有几个关键点。第一,卷积层使用 padding=1,所以输入 32x32 经过卷积后仍是 32x32,池化后变成 16x16;第二次卷积后仍是 16x16,再次池化变成 8x8。第二,全连接层的输入维度必须是 64x8x8=4096,如果改了卷积核数量或池化方式,这里要同步修改。第三,最后一个全连接层输出 10,对应 10 个类别,训练时配合交叉熵损失,不需要手动加 softmax,因为 PyTorch 的CrossEntropyLoss内部已经包含 softmax 计算。
4.3 训练循环:数据加载、损失函数、优化器
训练一个简单分类器,需要三部分:数据集加载、损失函数定义、优化器更新。下面代码使用 torchvision 加载 CIFAR-10,并对图片做归一化。
import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) model = SimpleCNN(num_classes=10) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) for epoch in range(5): running_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() avg_loss = running_loss / len(train_loader) print(f"Epoch {epoch + 1}, Loss: {avg_loss:.4f}")这里的交叉熵损失内部包含 softmax 计算,所以模型最后一层直接输出原始 logits 即可。Adam 优化器在入门项目中是很好的默认选择,初始学习率 0.001 通常会有一个稳定的收敛过程。train_loader的shuffle=True会在每个 epoch 打乱数据,避免模型学习到数据顺序。每次迭代需要调用optimizer.zero_grad()清空梯度,否则梯度会累加,导致参数更新方向错误。
4.4 期望输出
第一次运行时会自动下载 CIFAR-10 数据集,网络较慢时需要耐心等待。训练 5 个 epoch 后,loss 通常从 1.8 左右逐渐下降到 1.2 左右,具体数值取决于数据划分和随机种子。如果你的 CPU 训练,每个 epoch 可能需要几分钟,这是正常现象。如果看到 loss 在 2.3 附近不动,首先检查数据加载是否正常,以及模型最后一层输出维度是否为 10。这里要用一个假输入快速验证模型结构是否能走通前向传播:
dummy = torch.randn(1, 3, 32, 32) print(model(dummy).shape)如果输出torch.Size([1, 10]),说明网络结构没有问题,问题可能在数据或训练配置。
5. 验证模型效果和保存模型
5.1 用测试集评估准确率
训练损失下降只代表模型拟合训练数据,真正有效果要看测试集准确率。测试时不需要反向传播,所以要包裹在torch.no_grad()中。
test_dataset = datasets.CIFAR10(root='./data', train=False, transform=transform) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False) correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f"Accuracy: {100 * correct / total:.2f}%")torch.max(outputs, 1)返回每一行最大值和对应索引,索引就是预测类别。准确率计算是预测正确的样本数除以总样本数。简单模型在 CIFAR-10 上大概能到 60% 左右,这个结果虽然远低于现代预训练模型,但它能验证“网络结构、数据加载、训练流程”是通的。如果准确率接近 10%,也就是随机猜测的水平,说明训练流程有问题,不要继续调超参数,先检查数据和标签是否对应。
5.2 保存和加载模型
训练完成后要保存模型,PyTorch 推荐保存state_dict,它只包含模型参数,不包含网络结构,加载时需要重新创建模型实例。
torch.save(model.state_dict(), 'simple_cnn_cifar10.pth') model = SimpleCNN(num_classes=10) model.load_state_dict(torch.load('simple_cnn_cifar10.pth')) model.eval()调用model.eval()很关键。模型在训练模式和评估模式下,某些层的表现不同,比如 Dropout、BatchNorm。如果没有调用eval(),推理结果可能不稳定。保存和加载时要保证网络结构一致,否则load_state_dict会报 key 不匹配。这里有一个实践建议:保存模型时在旁边加一个json或txt文件记录输入尺寸、类别映射、归一化参数,避免模型换了机器后无法正确推理。
5.3 单张图片推理示例
实际使用中,输入往往不是标准化的数据集图片,而是一张任意尺寸的图片。推理前需要把图片缩放到模型要求的尺寸,并做相同的归一化。
from PIL import Image image = Image.open('cat_test.png').convert('RGB') single_transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) input_tensor = single_transform(image).unsqueeze(0) class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] with torch.no_grad(): outputs = model(input_tensor) _, predicted = torch.max(outputs, 1) print(f"Predicted: {class_names[predicted.item()]}")注意这里归一化参数必须和训练时一致,否则输入分布偏移,准确率会明显下降。unsqueeze(0)是为输入增加一个 batch 维度,因为模型默认第一个维度是 batch。如果图片是灰度图,需要先转成三通道 RGB,否则输入通道与模型第一层不匹配。
6. 参数调优与常见问题排查
6.1 学习率、batch size、epochs 的配合
学习率决定参数更新的步长。学习率太大会导致 loss 震荡甚至发散,太小会导致收敛过慢。batch size 影响梯度估计的稳定性,越大梯度越稳定,但内存占用越高。epochs 是完整遍历训练集的次数,过多会导致过拟合,过少会导致欠拟合。入门阶段,建议固定 batch size 为 64,学习率从 0.001 开始,先训练小 epoch 观察 loss 趋势,再调整。
| 超参数 | 偏小的影响 | 偏大的影响 | 建议 |
|---|---|---|---|
| 学习率 | 收敛慢 | 震荡、不收敛 | 0.001 起步,参考 loss 曲线调整 |
| batch size | 梯度不稳定 | 内存不足、泛化下降 | 32 到 128 之间选 |
| epochs | 欠拟合 | 过拟合 | 观察验证集准确率何时停止提升 |
6.2 过拟合的表现和预防
如果训练集准确率很高,测试集准确率明显低,说明过拟合。CIFAR-10 规模不大,简单 CNN 在训练后期经常出现这种趋势。常见的处理方法包括:增加数据增强,比如随机裁剪、水平翻转;在全连接层加入 Dropout;减小模型容量;使用权重衰减。学习阶段建议先不做复杂优化,而是记录训练准确率和测试准确率的差值,理解过拟合是怎么发生的。下面是一个加入 Dropout 的改造示例:
class SimpleCNNWithDropout(nn.Module): def __init__(self, num_classes=10): super(SimpleCNNWithDropout, self).__init__() self.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1) self.pool = nn.MaxPool2d(kernel_size=2, stride=2) self.fc1 = nn.Linear(64 * 8 * 8, 256) self.dropout = nn.Dropout(0.5) self.fc2 = nn.Linear(256, num_classes) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(x.size(0), -1) x = F.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x6.3 常见问题排查表
排错时按顺序检查输入、路径、维度、配置、日志。下面表格整理了初学者最常见的几类问题。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 多维形状不匹配 | 全连接输入维度计算错误 | 打印每层输出 shape | 按输出尺寸公式重新计算 |
| loss 不下降 | 学习率不合适或数据未归一化 | 打印前几个 batch 的 loss | 调学习率,检查 transform |
| 训练集准确率接近随机 | 标签和图片不对应 | 可视化一个 batch 图片 | 检查 dataset 加载逻辑 |
| 显存不足 | batch size 过大 | 看报错 OOM | 调小 batch size |
| 预测结果全为同一个类 | 模型未训练或加载错误 | 查看 loss 和参数状态 | 重新训练或正确加载权重 |
6.4 输出尺寸计算错误是最常见的坑
一个很容易踩的坑是把卷积输出尺寸当作全连接输入维度,或者修改了池化层后忘记同步更新全连接层。修改网络结构时,推荐先让模型处理一个假输入,打印每一层输出的 shape,确认无误后再进入训练循环。
def print_shape_hook(module, input, output): print(module.__class__.__name__, output.shape) model.conv1.register_forward_hook(print_shape_hook) model.conv2.register_forward_hook(print_shape_hook) model.fc1.register_forward_hook(print_shape_hook)通过注册 forward hook,可以看到每个关键层输出的 shape。这个技巧在调试深层网络时非常有用,比在 forward 里逐个加 print 更干净。还有一个常见坑是 torchvision 数据集首次下载失败,报连接超时。这时可以把下载好的数据集文件放到./data目录下,或者换用公司镜像,避免每次都在同一个位置卡住。
7. 从入门实验到工程实践的建议
7.1 学习顺序建议
如果把 CNN 当作一个完整知识体系,建议按这个顺序学习:先写代码跑通一个简单 CNN,理解数据流;再手动计算一次卷积输出的尺寸;然后逐个分析 LeNet-5、AlexNet、VGG 的结构;接着学习 BatchNorm、Dropout、数据增强;再尝试 ResNet 的残差连接。每次学新结构时,都把网络输出尺寸和参数量记录下来,做横向对比。不要一上来就加载预训练模型做迁移学习,那样会跳过很多底层细节。手动算一次输出尺寸,比看十遍公式更有效。
7.2 学习环境与生产环境的差异
跑通入门实验时,几行代码就够了。但要真正把模型用到生产环境,还需要考虑更多因素:训练时要有 GPU 资源、数据版本管理、训练日志和指标监控;推理时要把模型导出成 ONNX 或 TorchScript,部署成 HTTP 服务;还要关注模型输入的预处理一致性、异常输入处理、推理耗时和并发量。下面用表格整理主要差异。
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据 | CIFAR-10 等公开数据集 | 业务数据,需清洗和脱敏 |
| 训练 | 单机单卡或 CPU | 分布式训练,任务调度 |
| 监控 | 打印 loss | 指标采集、告警、日志 |
| 部署 | 本地 notebook 推理 | 服务化或边缘部署 |
| 稳定性 | 重跑即可 | 需要版本管理、回滚方案 |
7.3 可复用的 CNN 学习检查清单
清单可以当作每次训练前、训练后、部署前的核对项,具体内容如下。
- 数据:确认图片尺寸、通道数、标签数量与模型匹配。
- 预处理:训练和推理使用相同的归一化参数。
- 网络结构:卷积后输出尺寸按公式核对,全连接输入维度正确。
- 训练配置:损失函数适合分类任务,优化器和学习率合理。
- 验证:用与训练集分布一致的测试集评估准确率。
- 保存:保存 state_dict 时记录输入尺寸、类别映射等元信息。
- 部署:推理前调用
model.eval(),开启no_grad,避免梯度计算占用内存和产生随机性。
7.4 下一步可以尝试的方向
跑通这个最小示例后,可以尝试三个方向。一是把数据集换成 Fashion-MNIST 或自定义图片文件夹,体验从数据、训练到推理的完整流程。二是把网络换成带 BatchNorm 和 Dropout 的结构,观察训练收敛速度变化。三是用 torchvision 自带的预训练 ResNet 做迁移学习,感受特征提取和微调的区别。每个方向做完,都应该用上面的检查清单确认一遍,再进入下一个实验。相比追着新模型跑,先把卷积、池化、全连接这几块的计算逻辑吃透,后续看任何网络结构都会快很多。