CNN卷积神经网络从入门到实战:一篇搞懂图像分类全流程
前言
卷积神经网络(Convolutional neural network)是深度学习中最重要的模型之一,尤其在计算机视觉领域有着广泛的应用。本文将从最基础的图像概念开始,逐步深入CNN的各个组成部分,最后通过一个完整的图像分类案例,帮助大家掌握CNN的实际应用。
一、图像基础概念
1.1 图像分类
在深度学习中,图像主要分为以下几种类型:
-
二值图像:每个像素点只有0和1两个值,通常用于黑白文档、文字识别等场景
-
灰度图像:像素值范围0-255,0表示黑色,255表示白色,中间值为不同程度的灰色
-
索引图像:存储像素颜色的索引值,通过索引在颜色映射表中找到对应的RGB值
-
RGB图像:最常见的彩色图像格式,由红、绿、蓝三个通道组成,每个通道值范围0-255

1.2 图像的表示与加载
在计算机中,图像本质上是矩阵数据:
-
灰度图像:单个二维矩阵,形状为(H, W)
-
RGB图像:三个二维矩阵,形状为(H, W, C),其中H为高度,W为宽度,C为通道数
"""
案例:
演示基础的图像操作.
图像分类:
二值图: 1通道, 每个像素点由0, 1组成
灰度图: 1通道, 每个像素点的范围: [0, 255]
索引图: 1通道, 每个像素点的范围: [0, 255], 像素点表示颜色表的索引
RGB真彩图: 3通道, Red, Green, Blue, 红绿蓝.
涉及到的API:
imshow() 基于HWC, 展示图像
imread() 读取图像, 获取HWC
imsave() 基于HWC, 保存图片.
"""
# 导包
import numpy as np
import matplotlib.pyplot as plt
import torch
# 1. 定义函数, 绘制: 全黑, 全白图.
def dm01():
# 1. 定义全黑图片: 像素点越接近0越黑, 越接近255越白.
# HWC: H: 高度, W: 宽度, C: 通道.
img1 = np.zeros((200, 200, 3))
# print(f'img1: {img1}')
# 2. 绘制图片.
plt.imshow(img1)
# plt.axis('off')
plt.show()
# 2. 定义全白图片.
img2 = torch.full(size=(200, 200, 3), fill_value=255)
# print(f'img2: {img2}')
plt.imshow(img2)
# plt.axis('off')
plt.show()
# 2. 定义函数, 加载图片.
def dm02():
# 1. 加载图片.
img1 = plt.imread('./data/img.jpg')
# print(f'img1: {img1}')
# print(f'img1.shape: {img1.shape}') # (640, 640, 3), HWC
# 2. 保存图像.
plt.imsave('./data/img_copy.png', img1)
# 3. 展示图像.
plt.imshow(img1)
plt.show()
# 3. 测试
if __name__ == '__main__':
# dm01()
dm02()
二、卷积神经网络(CNN)介绍
2.1 什么是CNN?
卷积神经网络是一种包含卷积层和池化层的神经网络计算模型。它通过模拟人眼视觉感知机制,能够自动提取图像中的特征。

2.2 CNN的组成结构
一个完整的CNN模型通常包含以下层级:
-
输入层:接收原始数据,可以是图像、视频、音频(频谱图)等
-
卷积层:核心层,用于提取图像的特征图
-
激励层:引入非线性,常用ReLU激活函数
-
池化层:降低特征图维度,减少计算量
-
全连接层:将特征图展平成一维向量进行分类
2.3 CNN的主要应用
-
图像分类:识别图像中的物体类别
-
目标检测:定位并识别图像中的多个物体
-
面部解锁:人脸识别技术
-
自动驾驶:道路识别、障碍物检测
三、卷积层详解
3.1 卷积核/滤波器
卷积核是带有共享参数的神经元,有多少个卷积核就相当于有多少个神经元。每个卷积核负责提取图像的一种特征。
3.2 卷积计算过程
卷积计算的基本原理:使用卷积核在特征图上滑动,对窗口内的矩阵进行点乘运算,得到新特征图的一个像素值。

3.3 Padding(填充)
在特征图周围补0的操作,主要作用:
-
防止边缘信息丢失
-
保持输入输出特征图形状一致

3.4 Stride(步长)
控制卷积核滑动的步长,作用:
-
降维
-
扩大感受野

3.5 多通道卷积计算
对于RGB三通道图像:
-
卷积核也有3个通道
-
每个通道分别进行点乘
-
将三个通道的结果求和得到新特征图的一个像素值


3.6 多卷积核计算
多个卷积核可以提取多种特征:
-
每个卷积核生成一个二维特征图
-
输出特征图数量 = 卷积核数量

3.7 特征图大小计算公式

N = floor((W - F + 2P) / S + 1)
其中:
-
W:输入特征图(初图)大小
-
F:卷积核大小
-
P:padding圈数
-
S:步长
3.8 PyTorch卷积层API
"""
案例:
演示卷积层的API, 用于 提取图像的局部特征, 获取: 特征图(Feature Map)
卷积神经网络介绍:
概述:
全称叫: Convolutional neural network, 即: 包含卷积层的神经网络.
组成:
卷积层(Convolutional):
用于提取图像的 局部特征, 结合 卷积核(每个卷积核 = 1个神经元) 实现, 处理后的结果叫: 特征图.
池化层(Pooling):
用于 降维, 降采样
全连接层(Full Connected, fc, linear):
用于 预测结果, 并输出结果的.
特征图计算方式:
N = (W - F + 2*P) / S + 1
W: 输入图像的大小
F: 卷积核的大小
P: 填充的大小
S: 步长
N: 输出图像的大小(特征图大小)
"""
# 导包
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# 1. 定义函数, 用于完成图像的加载, 卷积, 特征图可视化操作.
def dm01():
# 1. 加载RGB真彩图.
img = plt.imread('./data/a.jpg')
# 2. 打印读取到的图像信息.
# print(f'img: {img}, shape: {img.shape}') # HWC (640, 640, 3)
# 3. 把图像的形状从 HWC -> CHW, 思路: img -> 张量 -> 转换维度.
img2 = torch.tensor(img, dtype=torch.float)
img2 = img2.permute(2, 0, 1)
# print(f'img2: {img2}, shape: {img2.shape}') # [3, 640, 640]
# 4. 因为这里只有1张图, 所以我们给它在增加1个维度, 从 CHW -> (1, C, H, W), 1张3通道的 640*640像素的 图
img3 = img2.unsqueeze(dim=0)
# print(f'img3: {img3}, shape: {img3.shape}') # [1, 3, 640, 640]
# 5. 创建卷积层对象, 提取 特征图.
# 参1: 输入图像的通道数, 参2: 输出图像的通道数(几个特征图), 参3: 卷积核的大小, 参4: 步长, 参5: 填充.
conv = nn.Conv2d(3, 4, 3, 2, 0)
# 6. 具体的卷积计算.
conv_img = conv(img3)
# 7. 打印卷积后的结果. 1张4通道的 319*319像素的 图
# print(f'conv_img: {conv_img}, shape: {conv_img.shape}') # (1, 4, 319, 319)
# 8. 查看提取到的 4个 特征图.
img4 = conv_img[0]
# print(f'img4: {img4}, shape: {img4.shape}') # (4, 319, 319) -> CHW
# 9. 把上述的图从 CHW -> HWC
img5 = img4.permute(1, 2, 0)
# print(f'img5: {img5}, shape: {img5.shape}') # (319, 319, 4) -> HWC
# 10. 可视化第1个通道的特征图.
feature1 = img5[:, :, 3].detach().numpy() # 第0通道(即: 第1通道的) (319, 319)像素图
plt.imshow(feature1)
plt.show()
# 2. 测试
if __name__ == '__main__':
dm01()
四、池化层详解
4.1 池化的作用
池化层主要用于降维,只在高度和宽度维度上进行操作,不改变通道数。池化层没有需要学习的参数。
池化层的主要作用如下:
-
降维和计算量减少:池化层通过减少特征图的尺寸,从而降低了计算量,特别是在多层网络中,随着层数的增加,池化能够显著减少计算资源的消耗。
-
提高鲁棒性:池化操作可以使得特征对小的变换、平移和旋转变得更加不敏感。这样,模型在面对噪声或图像的轻微变化时,依然能够稳定工作。
-
防止过拟合:通过池化减少了特征图的大小,减少了模型的复杂度,从而有助于防止过拟合,尤其是在较小的数据集上。
-
抽象特征:通过池化层的操作,可以提取更为抽象和高层次的特征,使得网络能够学习到更具泛化能力的表示。
4.2 池化类型
-
最大池化:取窗口内的最大值
-
平均池化:取窗口内的平均值

4.3 池化计算特点
多通道池化时,每个通道独立进行池化操作,输出通道数保持不变。
4.4 PyTorch池化层API
"""
案例:
演示池化层相关操作.
池化层解释(Pooling):
目的:
降维.
思路:
最大池化.
平均池化.
特点:
池化不会改变数据的 通道数.
"""
# 导包
import torch
import torch.nn as nn
# 1. 定义函数, 演示单通道池化.
def dm01():
# 1. 创建1个 1通道 3*3的二维矩阵.
inputs = torch.tensor([ # 1 通道C
[ # 3 高度H
[0, 1, 2], # 3 宽度W
[3, 4, 5],
[6, 7, 8]
]
])
# print(f'inputs: {inputs}, shape: {inputs.shape}') # (1, 3, 3)
# 2. 创建最大池化层.
# 参1: 池化核(池化窗口)大小, 参2: 步长, 参3: 填充.
pool1 = nn.MaxPool2d(2, 1, 0)
outpus = pool1(inputs)
print(f'outpus: {outpus}, shape: {outpus.shape}') # (1, 2, 2)
# 3. 创建平均池化层.
pool2 = nn.AvgPool2d(2, 1, 0)
outpus = pool2(inputs)
print(f'outpus: {outpus}, shape: {outpus.shape}') # (1, 2, 2)
# 2. 定义函数, 演示多通道池化.
def dm02():
# 1. 创建1个 3通道 3*3的二维矩阵.
inputs = torch.tensor([ # 3 通道C
[ # 通道1, HW 3,3
[0, 1, 2],
[3, 4, 5],
[6, 7, 8]
],
[ # 通道2, HW 3,3
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
],
[ # 通道3, HW 3,3
[11, 22, 33],
[44, 55, 66],
[77, 88, 99]
]
])
# print(f'inputs: {inputs}, shape: {inputs.shape}') # (3, 3, 3)
# 2. 创建最大池化层.
# 参1: 池化核(池化窗口)大小, 参2: 步长, 参3: 填充.
pool1 = nn.MaxPool2d(2, 1, 0)
outpus = pool1(inputs)
print(f'outpus: {outpus}, shape: {outpus.shape}') # (3, 2, 2)
# 3. 创建平均池化层.
pool2 = nn.AvgPool2d(2, 1, 0)
outpus = pool2(inputs)
print(f'outpus: {outpus}, shape: {outpus.shape}') # (3, 2, 2)
# 3. 测试.
if __name__ == '__main__':
# dm01()
dm02()
五、实战:CIFAR10图像分类
下面通过一个完整的案例,演示如何使用CNN进行图像分类。

数据集介绍
CIFAR-10数据集5万张训练图像、1万张测试图像、10个类别、每个类别有6k个图像,图像大小32×32×3。下图列举了10个类,每一类随机展示了10张图片
我们要搭建的网络结构如下:
输入形状: 32x32
第一个卷积层输入 3 个 Channel, 输出 6 个 Channel, Kernel Size 为: 3x3
第一个池化层输入 30x30, 输出 15x15, Kernel Size 为: 2x2, Stride 为: 2
第二个卷积层输入 6 个 Channel, 输出 16 个 Channel, Kernel Size 为 3x3
第二个池化层输入 13x13, 输出 6x6, Kernel Size 为: 2x2, Stride 为: 2
第一个全连接层输入 576 维, 输出 120 维
第二个全连接层输入 120 维, 输出 84 维
最后的输出层输入 84 维, 输出 10 维
我们在每个卷积计算之后应用 relu 激活函数来给网络增加非线性因素。

"""
案例:
演示CNN的综合案例, 图像分类.
回顾: 深度学习项目的步骤
1. 准备数据集.
这里我们用的时候 计算机视觉模块 torchvision自带的 CIFAR10数据集, 包含6W张 (32,32,3)的图片, 5W张训练集, 1W张测试集, 10个分类, 每个分类6K张图片.
你需要单独安装一下 torchvision包, 即: pip install torchvision
2. 搭建(卷积)神经网络
3. 模型训练.
4. 模型测试.
卷积层:
提取图像的局部特征 -> 特征图(Feature Map), 计算方式: N = (W - F + 2P) // S + 1
每个卷积核都是1个神经元.
池化层:
降维, 有最大池化 和 平均池化.
池化只在HW上做调整, 通道上不改变.
案例的优化思路:
1. 增加卷积核的输出通道数(大白话: 卷积核的数量)
2. 增加全连接层的参数量.
3. 调整学习率
4. 调整优化方法(optimizer...)
5. 调整激活函数...
6. ...
dropout放置原则:
卷积层:通常不加dropout,用BatchNorm替代
全连接层:激活函数之后、下一层之前加dropout
输出层:永远不加dropout
多个dropout:可以逐层递减dropout率
训练/评估:记得切换mode(.train()/.eval())
这样放置dropout可以最大化正则化效果,同时不损失太多模型容量!
"""
# 导包
import torch
import torch.nn as nn
from torchvision.datasets import CIFAR10
from torchvision.transforms import ToTensor # pip install torchvision -i https://mirrors.aliyun.com/pypi/simple/
import torch.optim as optim
from torch.utils.data import DataLoader
import time
import matplotlib.pyplot as plt
from torchsummary import summary
# 每批次样本数
BATCH_SIZE = 8
# 1. 准备数据集.
def create_dataset():
# 1. 获取训练集.
# 参1: 数据集路径. 参2: 是否是训练集. 参3: 数据预处理 -> 张量数据. 参4: 是否联网下载(直接用我给的, 不用下)
train_dataset = CIFAR10(root='./data', train=True, transform=ToTensor(), download=True)
# 2. 获取测试集.
test_dataset = CIFAR10(root='./data', train=False, transform=ToTensor(), download=True)
# 3. 返回数据集.
return train_dataset, test_dataset
# 2. 搭建(卷积)神经网络
class ImageModel(nn.Module):
# 1. 初始化父类成员, 搭建神经网络.
def __init__(self):
# 1.1 初始化父类成员.
super().__init__()
# 1.2 搭建神经网络.
# 第1个卷积层, 输入 3通道, 输出6通道, 卷积核大小3*3, 步长1, 填充0
self.conv1 = nn.Conv2d(3, 6, 3, 1, 0)
# 第1个池化层, 窗口大小 2*2, 步长2, 填充0
self.pool1 = nn.MaxPool2d(2, 2, 0)
# 第2个卷积层, 输入 6通道, 输出16通道, 卷积核大小3*3, 步长1, 填充0
self.conv2 = nn.Conv2d(6, 16, 3, 1, 0)
# 第2个池化层, 窗口大小 2*2, 步长2, 填充0
self.pool2 = nn.MaxPool2d(2, 2, 0)
# 第1个隐藏层(全连接层), 输入: 576, 输出: 120
self.linear1 = nn.Linear(576, 120)
# 第2个隐藏层 (全连接层), 输入: 120, 输出: 84
self.linear2 = nn.Linear(120, 84)
# 第3个隐藏层 (全连接层) -> 输出层, 输入: 84, 输出: 10
self.output = nn.Linear(84, 10)
self.dropout = nn.Dropout(p=0.5)
# 2. 定义前向传播
def forward(self, x):
# 第1层: 卷积层(加权求和) + 激励层(激活函数) + 池化层(降维)
# 分解版.
# x = self.conv1(x) # 卷积层
# x = torch.relu(x) # 激励层
# x = self.pool1(x) # 池化层
# 合并版 池化 + 激活函数 + 卷积
x = self.pool1(torch.relu(self.conv1(x)))
# 第2层: 卷积层(加权求和) + 激励层(激活函数) + 池化层(降维)
x = self.pool2(torch.relu(self.conv2(x)))
# 细节: 全连接层只能处理二维数据, 所以要将数据进行拉平 (8, 16, 6, 6) -> (8, 576)
# 参1: 样本数(行数), 参2: 列数(特征数), -1表示自动计算.
x = x.reshape(x.size(0), -1) # 8行576列
# print(f'x.shape: {x.shape}')
# 第3层: 全连接层(加权求和) + 激励层(激活函数)
x = torch.relu(self.linear1(x))
x = self.dropout(x)
# 第4层: 全连接层(加权求和) + 激励层(激活函数)
x = torch.relu(self.linear2(x))
x = self.dropout(x)
# 第5层: 全连接层(加权求和) -> 输出层
return self.output(x) # 后续用 多分类交叉熵损失函数CrossEntropyLoss = softmax()激活函数 + 损失计算.
# 3. 模型训练.
def train(train_dataset):
# 1. 创建数据加载器.
dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
# 2. 创建模型对象.
model = ImageModel()
# 3. 创建损失函数对象.
criterion = nn.CrossEntropyLoss() # 多分类交叉熵损失函数 = softmax()激活函数 + 损失计算.
# 4. 创建优化器对象.
optimizer = optim.Adam(model.parameters(), lr=1e-4)
# 5. 循环遍历epoch, 开始 每轮的 训练动作.
# 5.1 定义变量, 记录训练的总轮数.
epochs = 20
# 5.2 遍历, 完成每轮的 所有批次的 训练动作.
for epoch_idx in range(epochs):
# 5.2.1 定义变量, 记录: 总损失, 总样本数据量, 预测正确样本个数, 训练(开始)时间
total_loss, total_samples, total_correct, start = 0.0, 0, 0, time.time()
# 5.2.2 遍历数据加载器, 获取到 每批次的 数据.
for x, y in dataloader:
# 5.2.3 切换训练模式.
model.train()
# 5.2.4 模型预测.
y_pred = model(x)
# 5.2.5 计算损失.
loss = criterion(y_pred, y)
# 5.2.6 梯度清零 + 反向传播 + 参数更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 5.2.7 统计预测正确的样本个数.
# print(y_pred) # 批次中, 每张图 每个分类的 预测概率.
# argmax() 返回最大值对应的索引, 充当 -> 该图片的 预测分类.
# tensor([9, 8, 5, 5, 1, 5, 8, 5])
# print(torch.argmax(y_pred, dim=-1)) # -1这里表示行. 预测分类
# print(y) # 真实分类
# print(torch.argmax(y_pred, dim=-1) == y) # 是否预测正确
# print((torch.argmax(y_pred, dim=-1) == y).sum()) # 预测正确的样本个数.
total_correct += (torch.argmax(y_pred, dim=-1) == y).sum()
# 5.2.8 统计当前批次的总损失. 第1批平均损失 * 第1批样本个数
total_loss += loss.item() * len(y) # [第1批总损失 + 第2批总损失 + 第3批总损失 + ...]
# 5.2.9 统计当前批次的总样本个数.
total_samples += len(y)
# break 每轮只训练1批, 提高训练效率, 减少训练时长, 只有测试会这么写, 实际开发绝不要这样做.
# 5.2.10 走这里, 说明一轮训练完毕, 打印该轮的训练信息.
print(f'epoch: {epoch_idx + 1}, loss: {total_loss / total_samples:.5f}, acc:{total_correct / total_samples:.2f}, time:{time.time() - start:.2f}s')
# break # 这里写break, 意味着只训练一轮.
# 6. 保存模型.
torch.save(model.state_dict(), './model/image_model.pth')
# 4. 模型测试.
def evaluate(test_dataset):
# 1. 创建测试集 数据加载器.
dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
# 2. 创建模型对象.
model = ImageModel()
# 3. 加载模型参数.
model.load_state_dict(torch.load('./model/image_model.pth')) # pickle文件
# 4. 定义变量统计 预测正确的样本个数, 总样本个数.
total_correct, total_samples = 0, 0
# 5. 遍历数据加载器, 获取到 每批次 的数据.
for x, y in dataloader:
# 5.1 切换模型模式.
model.eval()
# 5.2 模型预测.
y_pred = model(x)
# 5.3 用 argmax()来模拟训练成效.
# argmax()函数功能: 返回最大值对应的索引, 充当 -> 该图片的 预测分类.
y_pred = torch.argmax(y_pred, dim=-1) # -1 这里表示按照列的方向逐行处理与 dim=1 一样的.
# 5.4 统计预测正确的样本个数.
total_correct += (y_pred == y).sum()
# 5.5 统计总样本个数.
total_samples += len(y)
# 6. 打印正确率(预测结果).
print(f'Acc: {total_correct / total_samples:.2f}')
# 5. 测试
if __name__ == '__main__':
# 1. 获取数据集.
train_dataset, test_dataset = create_dataset()
# print(f'训练集: {train_dataset.data.shape}') # (50000, 32, 32, 3)
# print(f'测试集: {test_dataset.data.shape}') # (10000, 32, 32, 3)
# # {'airplane': 0, 'automobile': 1, 'bird': 2, 'cat': 3, 'deer': 4, 'dog': 5, 'frog': 6, 'horse': 7, 'ship': 8, 'truck': 9}
# print(f'数据集类别: {train_dataset.class_to_idx}')
#
# # 图像展示
# plt.figure(figsize=(2, 2))
# plt.imshow(train_dataset.data[1111]) # 索引为1111的图像
# plt.title(train_dataset.targets[1111])
# plt.show()
# 2. 搭建神经网络.
# model = ImageModel()
# 查看模型参数, 参1: 模型, 参2: 输入维度(CHW, 通道, 高, 宽), 参3: 批次大小
# summary(model, (3, 32, 32), batch_size=1)
# 3. 模型训练.
# train(train_dataset)
# 4. 模型测试.
evaluate(test_dataset)
summary(model, (3, 32, 32), batch_size=BATCH_SIZE)


模型优化技巧
在实际应用中,可以通过以下方式提升模型性能:
-
数据增强:对输入图像进行随机翻转、旋转、裁剪等操作
-
增加网络深度:添加更多卷积层
-
增加神经元数量:提高模型容量
-
调整学习率:使用学习率衰减策略
-
优化器选择:尝试SGD、Adam、RMSprop等
-
添加正则化:使用Dropout、Batch Normalization
-
早停策略:防止过拟合
-
增加训练轮数:充分训练模型
softmax vs argmax 的本质区别


# argmax: 只告诉"哪个最大"
def argmax_function(x):
return torch.argmax(x) # 输出: 3 (离散值,不可导)
# softmax: 告诉"每个有多大"
def softmax_function(x):
return F.softmax(x, dim=0)
# 输出: [0.21, 0.03, 0.01, 0.70, 0.05] (连续值,可导)
# 你的理解完全正确:
# softmax = argmax的"平滑版本" + 可导性


六、总结
本文从图像的基本概念出发,详细介绍了CNN的各个组成部分及其原理,并通过CIFAR10图像分类的实战案例,展示了完整的模型构建、训练和评估流程。CNN作为深度学习的重要基石,理解其工作原理对于深入学习计算机视觉领域至关重要。
关键要点回顾:
-
图像本质是多维矩阵数据
-
卷积层通过卷积核提取特征
-
池化层用于降维
-
全连接层用于分类
-
模型训练需要合理的数据处理和参数设置
希望通过本文的学习,大家对CNN有了更深入的理解,能够独立完成图像分类任务。欢迎在评论区交流讨论!
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)