YOLOv1 torch 实现
import torch
import torch.nn as nn
class LeakyBlock(nn.Module):
"""
论文规定:除最后一层外,所有层全部使用 Leaky ReLU (leaky=0.1)
"""
def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
super(LeakyBlock, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=True)
self.leaky = nn.LeakyReLU(0.1)
def forward(self, x):
return self.leaky(self.conv(x))
class YOLOv1(nn.Module):
def __init__(self, num_boxes=2, num_classes=20):
super(YOLOv1, self).__init__()
self.B = num_boxes
self.C = num_classes
# 1. 卷积层部分:完全对照论文 Figure 3 的 24 个卷积层和最大池化
self.features = nn.Sequential(
# Conv Layer 1: 448x448x3 -> 112x112x192
LeakyBlock(3, 64, kernel_size=7, stride=2, padding=3),
nn.MaxPool2d(kernel_size=2, stride=2),
# Conv Layer 2: 112x112x64 -> 56x56x256
LeakyBlock(64, 192, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=2, stride=2),
# Conv Layer 3-5: 56x56x192 -> 28x28x512
LeakyBlock(192, 128, kernel_size=1, stride=1, padding=0),
LeakyBlock(128, 256, kernel_size=3, stride=1, padding=1),
LeakyBlock(256, 256, kernel_size=1, stride=1, padding=0),
LeakyBlock(256, 512, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=2, stride=2),
# Conv Layer 6-15: 重复4次的 1x1 和 3x3 卷积组合
LeakyBlock(512, 256, kernel_size=1, stride=1, padding=0),
LeakyBlock(256, 512, kernel_size=3, stride=1, padding=1),
LeakyBlock(512, 256, kernel_size=1, stride=1, padding=0),
LeakyBlock(256, 512, kernel_size=3, stride=1, padding=1),
LeakyBlock(512, 256, kernel_size=1, stride=1, padding=0),
LeakyBlock(256, 512, kernel_size=3, stride=1, padding=1),
LeakyBlock(512, 256, kernel_size=1, stride=1, padding=0),
LeakyBlock(256, 512, kernel_size=3, stride=1, padding=1),
LeakyBlock(512, 512, kernel_size=1, stride=1, padding=0),
LeakyBlock(512, 1024, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=2, stride=2),
# Conv Layer 16-23: 重复2次的 1x1 和 3x3 卷积组合
LeakyBlock(1024, 512, kernel_size=1, stride=1, padding=0),
LeakyBlock(512, 1024, kernel_size=3, stride=1, padding=1),
LeakyBlock(1024, 512, kernel_size=1, stride=1, padding=0),
LeakyBlock(512, 1024, kernel_size=3, stride=1, padding=1),
LeakyBlock(1024, 1024, kernel_size=3, stride=1, padding=1),
LeakyBlock(1024, 1024, kernel_size=3, stride=2, padding=1), # 步长为2,进一步降维
# Conv Layer 24
LeakyBlock(1024, 1024, kernel_size=3, stride=1, padding=1),
LeakyBlock(1024, 1024, kernel_size=3, stride=1, padding=1),
)
# 2. 全连接回归 Head(这里就是全篇最吃内存、包含 Dropout 的地方)
self.fc_head = nn.Sequential(
nn.Flatten(), # 将 7x7x1024 的特征图展平成 50176 维的一维向量
# 第一层全连接:从 50176 轰到 4096 维
nn.Linear(7 * 7 * 1024, 4096),
nn.LeakyReLU(0.1),
# 👈 这里就是你刚才问到的 Dropout!丢弃率 0.5,防过拟合的灵魂所在
nn.Dropout(p=0.5),
# 第二层全连接:从 4096 轰到最后的 1470 维 (7 * 7 * 30)
# 👈 这里就是 Linear Activation(没有加任何类似 Sigmoid/Softmax 的整流)
nn.Linear(4096, 7 * 7 * (self.B * 5 + self.C))
)
def forward(self, x):
# x 形状: [Batch_Size, 3, 448, 448]
x = self.features(x) # 经过24个卷积层后形状: [Batch_Size, 1024, 7, 7]
x = self.fc_head(x) # 经过全连接层后形状: [Batch_Size, 1470]
# 强行把一维向量重塑成我们聊了一整天的 [Batch_Size, 7, 7, 30] 冰冷张量
x = x.view(-1, 7, 7, self.B * 5 + self.C)
return x
# --------- 验证一下网络输出 ---------
if __name__ == "__main__":
model = YOLOv1()
# 模拟输入一张 448x448 的标准 YOLO 图像
dummy_input = torch.randn(1, 3, 448, 448)
output = model(dummy_input)
print("模型输入形状:", dummy_input.shape)
print("模型直出张量形状:", output.shape) # 应该是 [1, 7, 7, 30]nn.Conv2d
nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)1. in_channels(输入通道数)
- 通俗解释:进来的数据有多少层。
物理直觉:
- 如果是网络的第一层,直接读原图,如果是 RGB 彩色图,那
in_channels就是3(红、绿、蓝三层);如果是灰度图,那它就是1。 - 如果是网络中间的卷积层,这个数字就必须完全等于上一层吐出来的输出通道数。
- 如果是网络的第一层,直接读原图,如果是 RGB 彩色图,那
2. out_channels(输出通道数 / 卷积核个数)
- 通俗解释:你想让这一层提取出多少种不同的特征,或者说出去的数据有多少层。
物理直觉:
- 一个卷积核(Filter)负责提取一种特定的某种特征(比如有的负责找边缘,有的负责找圆形)。一个卷积核扫完一整张图,就会生成一张特征图(厚度为 1)。
- 如果你设置
out_channels=64,就意味着你安排了 64 个不同的卷积核同时去扫这张图,最后叠在一起吐出来的特征图厚度(通道数)就是 64。
3. kernel_size(卷积核的尺寸)
- 通俗解释:你的“扫描窗口”有多大。
物理直觉:
- 可以是一个整数(如
3,代表 $3 \times 3$ 的正方形窗口),也可以是一个元组(如(3, 5))。 - 大窗口(如 7x7):视野大,能一眼看到更大范围的物体轮廓,但计算量大,容易丢失细节(YOLOv1 的第一层为了快速铺开视野就用了 7x7 )。
- 小窗口(如 3x3):现代网络的标配,通过层层堆叠小窗口,既能学到复杂的局部细节,参数量还少。
- 可以是一个整数(如
4. stride(步长)
- 通俗解释:卷积核每次向前“走”几步。
物理直觉:
- 默认是
1,即卷积核一格一格地往后挪,这样图像的分辨率(宽高)不会急剧缩小。 - 如果设为
2,卷积核就会跳着走(每次跨两格)。这会导致扫完之后的特征图宽高直接缩减到原来的一半左右。在现代网络里,大家经常用stride=2的卷积来代替传统的池化层(MaxPooling)进行降维。
- 默认是
5. padding(填充)
- 通俗解释:在图片的四周额外垫几圈“零(Zero)”。
物理直觉:
- 卷积核在扫图的时候,越靠边缘的像素被扫到的次数越少,中间的像素被扫到的次数最多,这会导致边缘信息逐渐丢失。而且每次卷积,图像的宽高都会缩水一圈。
- 为了保持图像大小不变,或者给边缘像素“维权”,我们会在图片外面围一圈 0。
- 金标准公式:当
kernel_size=3, stride=1时,你只要设置padding=1,卷积出来之后的特征图宽高就能和输入完全一模一样。
尺寸计算公式
既然原理学得扎实,这个决定特征图最终长宽的数学公式一定要焊死在脑海里。假设输入图片的长宽是 $H_{in}$,经过卷积后变成了 $H_{out}$:
$$H_{out} = \lfloor \frac{H_{in} + 2 \times \text{padding} - \text{kernel\_size}}{\text{stride}} \rfloor + 1$$
注:$\lfloor \rfloor$ 代表向下取整(整除)。
评论已关闭