YOLO入门教程(番外):卷积神经网络—填充与步幅
卷积神经网络中的填充与步幅:让AI更好地理解图像的艺术
深入浅出地解析CNN中两个关键超参数,让你的模型既保持信息又提高效率
在卷积神经网络的世界里,每一次卷积运算都像是在用一个小窗口(卷积核)在图像上滑动观察局部特征。但在这个过程中,我们会遇到两个实际问题:随着网络层数加深,图像边缘信息会逐渐丢失;有时我们又希望主动降低图像分辨率来减少计算量。今天我们就来聊聊解决这两个问题的关键技术——填充(Padding)和步幅(Stride)。
卷积操作的基本原理:从滑动窗口说起
想象一下,你正在用一个放大镜观察一幅画的细节。这个放大镜就是我们的"卷积核",而移动放大镜的规则决定了我们能观察到什么。
正常情况下,如果我们有一张nh×nwn_h × n_wnh×nw大小的图像,使用kh×kwk_h × k_wkh×kw大小的卷积核,那么输出大小就是:
(nh−kh+1)×(nw−kw+1)(n_h - k_h + 1) × (n_w - k_w + 1)(nh−kh+1)×(nw−kw+1)
这意味着每做一次卷积,图像尺寸就会缩小。就像用放大镜看画时,如果放大镜很大,你一次能看到的内容很多,但需要移动的次数就少了。
填充(Padding):给图像加个相框
为什么需要填充?
假设你有一张240×240像素的照片,经过10层5×5的卷积后,会变成200×200像素。这就像每次裁剪照片边缘一样,原始图像边界的信息都丢失了!填充技术就是为了解决这个问题。
填充就像给照片加个相框:我们在图像周围添加一圈值为0的像素点(就像黑色相框),这样卷积核在边缘滑动时就有足够的"操作空间"了。
填充的计算公式
添加php_hph行和pwp_wpw列填充后,输出尺寸变为:
(nh−kh+ph+1)×(nw−kw+pw+1)(n_h - k_h + p_h + 1) × (n_w - k_w + p_w + 1)(nh−kh+ph+1)×(nw−kw+pw+1)
保持尺寸不变的技巧
通常我们设置ph=kh−1p_h = k_h - 1ph=kh−1和pw=kw−1p_w = k_w - 1pw=kw−1,这样输入输出尺寸就完全相同了!这就像选择了刚好合适的相框尺寸。
为什么卷积核尺寸通常是奇数?
因为使用3、5、7这样的奇数尺寸,可以在各侧均匀填充,保持对称美观。想象一下,如果相框左右宽度不同,看起来会很不协调吧?
实际代码示例
import torch
from torch import nn# 定义一个方便的卷积测试函数
def test_convolution(conv_layer, input_tensor):"""测试卷积层并返回输出形状"""# 添加批次和通道维度:(批量大小, 通道数, 高度, 宽度)input_with_dims = input_tensor.reshape((1, 1) + input_tensor.shape)# 进行卷积计算output = conv_layer(input_with_dims)# 移除批次和通道维度,返回原始形状return output.reshape(output.shape[2:])# 创建8x8的随机输入图像
original_image = torch.rand(size=(8, 8))
print("原始图像尺寸:", original_image.shape)# 示例1:使用3x3卷积核,填充为1(保持尺寸不变)
conv_layer1 = nn.Conv2d(1, 1, kernel_size=3, padding=1)
output1 = test_convolution(conv_layer1, original_image)
print("使用填充后的输出尺寸:", output1.shape) # 仍然是8x8
步幅(Stride):控制观察的步调
什么是步幅?
步幅决定了卷积核每次移动的距离。默认步幅为1,就像慢慢细看画的每个细节;增大步幅就像大步流星地浏览,虽然会错过一些细节,但能快速了解整体情况。
步幅的计算公式
当垂直步幅为shs_hsh、水平步幅为sws_wsw时,输出形状为:
⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋\lfloor(n_h - k_h + p_h + s_h)/s_h\rfloor × \lfloor(n_w - k_w + p_w + s_w)/s_w\rfloor⌊(nh−kh+ph+sh)/sh⌋×⌊(nw−kw+pw+sw)/sw⌋
这个公式看起来复杂,但实际上很简单:输出尺寸 ≈ 输入尺寸 / 步幅。
步幅的实际效果
使用步幅为2时,输出尺寸大约减半;步幅为3时,约为原来的三分之一。这就像从"细嚼慢咽"变为"狼吞虎咽",虽然细节少了,但效率提高了!
# 示例2:使用步幅为2,尺寸减半
conv_layer2 = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
output2 = test_convolution(conv_layer2, original_image)
print("使用步幅2的输出尺寸:", output2.shape) # 变为4x4# 示例3:不同方向的步幅和填充
conv_layer3 = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
output3 = test_convolution(conv_layer3, original_image)
print("复杂步幅的输出尺寸:", output3.shape) # 根据公式计算的结果
填充与步幅的完美配合
在实际应用中,我们经常同时使用填充和步幅:
- 保持尺寸模式:填充=1,步幅=1(3×3卷积核)
- 降采样模式:填充=1,步幅=2(快速减少尺寸)
- 不对称模式:不同方向的步幅和填充(处理特殊需求)
# 同时使用填充和步幅的示例
def demonstrate_combinations(image_size):"""展示不同填充和步幅组合的效果"""image = torch.rand(size=(image_size, image_size))print(f"\n对于{image_size}x{image_size}输入图像:")# 模式1:保持尺寸conv1 = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=1)output1 = test_convolution(conv1, image)print(f"保持尺寸: {output1.shape}")# 模式2:降采样conv2 = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)output2 = test_convolution(conv2, image)print(f"降采样: {output2.shape}")# 模式3:特殊组合if image_size >= 8:conv3 = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(1, 2), stride=(2, 3))output3 = test_convolution(conv3, image)print(f"特殊组合: {output3.shape}")# 测试不同尺寸
demonstrate_combinations(8)
demonstrate_combinations(16)
实际应用场景
什么时候使用填充?
- 保持空间信息:在深层网络中防止信息丢失
- 预处理阶段:需要保持图像尺寸时
- 对称结构:当需要输入输出尺寸相同时
什么时候使用步幅?
- 快速降维:减少计算量和内存使用
- 特征抽象:从细节特征转向全局特征
- 替代池化层:现代网络中常用步幅卷积代替池化
总结:找到平衡的艺术
填充和步幅就像卷积神经网络中的"油门"和"刹车":
- 填充保护信息,保持细节,但增加计算成本
- 步幅提高效率,加速处理,但可能丢失信息
在实际设计中,我们需要根据任务需求找到平衡点:
任务类型 | 推荐策略 | 原因 |
---|---|---|
图像分割 | 小步幅,适当填充 | 需要保持空间精度 |
图像分类 | 大步幅,最小填充 | 需要快速抽象特征 |
目标检测 | 混合策略 | 平衡精度和效率 |
动手实验建议
- 可视化效果:使用小矩阵手动计算,理解每一步的变化
- 尝试极端值:体验过大填充或步幅的影响
- 对比实验:在同一网络上测试不同策略的效果差异
# 简单的可视化实验
def manual_convolution_example():"""手动计算卷积示例"""# 创建简单输入input_matrix = torch.tensor([[1., 2., 3.],[4., 5., 6.],[7., 8., 9.]])# 创建简单卷积核kernel = torch.tensor([[0., 1.],[2., 3.]])print("输入矩阵:")print(input_matrix)print("\n卷积核:")print(kernel)# 计算不同填充和步幅的结果from torch.nn import functional as F# 无填充,步幅1output1 = F.conv2d(input_matrix.reshape(1,1,3,3), kernel.reshape(1,1,2,2), padding=0, stride=1)print(f"\n无填充,步幅1: {output1.shape} - 输出值: {output1.squeeze()}")# 填充1,步幅1output2 = F.conv2d(input_matrix.reshape(1,1,3,3), kernel.reshape(1,1,2,2), padding=1, stride=1)print(f"填充1,步幅1: {output2.shape} - 输出值: {output2.squeeze()}")manual_convolution_example()
通过理解和掌握填充与步幅这两个超参数,你就能够更好地设计卷积神经网络,在保持信息和提高效率之间找到最佳平衡点。记住,没有最好的配置,只有最适合特定任务的配置!