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
    • 如果是网络中间的卷积层,这个数字就必须完全等于上一层吐出来的输出通道数

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$ 代表向下取整(整除)。

标签: none

评论已关闭