CNN与ResNet梳理总结

249 阅读8分钟

卷积神经网络

主体结构:卷积1->激活->池化...卷积n->激活->池化、平铺展开为一维、全连接层、输出。

# 定义模型
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 输入为3*32*32(填充为1 原始图像加上2尺寸填充变为3*34*34)
        # 卷积核32*3*3 输出为32*32*32
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        # 输入为32*16*16 卷积核64*3*3 输出64*16*16
        # 输入为第一次卷积后并池化输出
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        # 第一次卷积后池化32*32*32->32*16*16
        # 第二次卷积后池化64*16*16->64*8*8
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
        # 第一个特征为输入维度 第二个特征为输出维度
        self.fc1 = nn.Linear(64 * 8 * 8, 512)
        self.fc2 = nn.Linear(512, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        # 四维张量[batch_size, 64, 8, 8]->二维张量[batch_size, 4096]
        # -1表示自动推断批次大小
        x = x.view(-1, 64 * 8 * 8)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

net = SimpleCNN()
  • 填充是对输入数据进行填充(见5、6行注释)。

    思考:

    要使输入数据边缘和内部像素等同概率被卷积,卷积核尺寸是不是仅比填充大2

    为什么是填充0?

  • 输出通道数等于卷积核数。

  • 输入通道数为3且卷积核数量为32时,每个卷积核都会在所有输入通道上进行卷积操作,并将结果相加生成一个输出通道

    具体来说,假设输入数据的通道为I1,I2,I3I_1, I_2, I_3 ,卷积核的权重分别为Wk1,Wk2,Wk3W_{k1}, W_{k2}, W_{k3}(第kk个卷积核在每个通道上的权重)。

    具体步骤:

    对输入的第一个通道 I1I_1 与权重Wk1W_{k1} 进行卷积,得到一个特征图: Fk1=I1Wk1F_{k1} = I_1 \ast W_{k1}

    对输入的第二个通道 I2I_2 与权重 Wk2W_{k2} 进行卷积,得到另一个特征图: Fk2=I2Wk2F_{k2} = I_2 \ast W_{k2}

    对输入的第三个通道I3I_3与权重Wk3W_{k3}进行卷积,得到第三个特征图:Fk3=I3Wk3F_{k3} = I_3 \ast W_{k3}

    将这三个特征图相加,得到最终的输出特征图: Fk=Fk1+Fk2+Fk3=(I1Wk1)+(I2Wk2)+(I3Wk3)F_k = F_{k1} + F_{k2} + F_{k3} = (I_1 \ast W_{k1}) + (I_2 \ast W_{k2}) + (I_3 \ast W_{k3})

  • 卷积 -> 激活 -> 池化顺序更能保留和提取特征,同时也有助于梯度的有效传播;卷积 -> 池化 -> 激活池化操作在激活函数之前应用,池化后的特征图尺寸减小,激活函数的计算量减少,提高计算效率。

  • 激活函数的主要作用之一就是引入非线性,这使得网络能够捕捉和学习数据中的非线性特征。

残差网络resnet

import torch
import torch.nn as nn
import torch.nn.functional as F

class BasicBlock(nn.Module):
    # resnet18/34中expansion=1,即layer2-4中的第一个卷积层out_channels=in_channels*expansion
    # 而在resnet50/101/152中expansion=4,out_channels=in_channels*expasion
    expansion = 1

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        # super函数调用nn.Module的初始化方法,确保子类BasicBlock继承父类nn.Module的__init__方法
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
       
        self.downsample = downsample

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = F.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            # downsample不为none表示第layer2-4中第一个卷积层输入通道与输出通道不一致,尺寸不一致
            # 残差与x相加时,需将x改造为与残差通道、尺寸一致
            identity = self.downsample(x)

        out += identity
        out = F.relu(out)

        return out

class ResNet(nn.Module):
    # 构造函数中layers即调用时[2,2,2,2],对于resnet-34结构,则为[3,4,6,3]
    # num_classes输出时分类的类别数
    def __init__(self, block, layers, num_classes=1000):
        super(ResNet, self).__init__()
        # in_channels=64,下面layer1-4调用_make_layer函数时in_channels值变化
        self.in_channels = 64
        # 7*7卷积层,通道:3->64,尺寸:224->112,因为步长为2
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
        # 批处理层,加快收敛速度
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        # 通道数64不变,尺寸112->56 因为步长为2
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # 通道数:64 尺寸:56
        self.layer1 = self._make_layer(block, 64, layers[0])
        # 通道数:64->128,卷积核通道数设置为128,尺寸56->28,步长为2
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        # 通道数:128->256 尺寸:28->14
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        # 通道数:256->512 尺寸:14->7
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        # 平均池化层 将每个通道尺寸缩减为1
        # 通道数不变 尺寸:7->1
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        # 全连接层输入为通道数512,输出为1000中分类
        self.fc = nn.Linear(512 * block.expansion, num_classes)

    def _make_layer(self, block, out_channels, blocks, stride=1):
        downsample = None
        # 定义的BasicBlock块输入通道和输出通道是1:1 步长为1,layer1是1:1 步长也为1,残差与x可以直接相加
        # layer2-4中第一个卷积层输入输出通道比为1:2 步长为2,需将x尺寸和通道数变为残差一致大小后才可相加
        if stride != 1 or self.in_channels != out_channels * block.expansion:
            # downsample用于相加时调整x尺寸、通道与残差一致
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * block.expansion),
            )
        # layers存储该layer中所有块
        layers = []
        # 第一个卷积层stride为2,downsample与定义残差块不一致
        layers.append(block(self.in_channels, out_channels, stride, downsample))
        # 更新输入通道数
        self.in_channels = out_channels * block.expansion
        # blocks为这个layer中包含的卷积层数,resnet18各layer中blocks均为2
        for _ in range(1, blocks):
            # 该layer中从第二个卷积层之后stride和downsample与定义块一致,使用定义的默认值
            layers.append(block(self.in_channels, out_channels))
        # nn.Sequential()一个容器,用于顺序存储该layer中所有卷积层
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        # 将张量x展平
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

def resnet18():
    return ResNet(BasicBlock, [2, 2, 2, 2])

# 示例用法
if __name__ == "__main__":
    model = resnet18()
    print(model)

    # 创建一个虚拟输入来测试网络的输出
    x = torch.randn(1, 3, 224, 224)
    output = model(x)
    print(output.shape)

模型结构

image.png

  • conv1是777*7卷积层,conv2-5是4个layer。
  • 注意,resnet18中conv2-4都是2个BasicBlock残差块堆积;resnet34中则是分别3、4、6、3个BasicBlock残差块堆积;而在resnet50中则是分别3、4、6、3个Bottleneck残差块堆积。

BasicBlock和Bottleneck残差结构

image.png

  • 左图BacisBlock残差结构,用于resnet18/34,输入输出通道数不变。
  • 右图BottleBlock残差结构,用于resnet50/101/152,先111*1卷积核降维,再111*1卷积核升维(通道数缩小4倍,再扩大4倍)。

实线残差与虚线残差

image.png

  • 实线残差,输入输出通道一致,比如resnet18中layer1和layer2-4的除第一个卷积层的其他卷积层。
  • 虚线残差,输入通道和输出通道不一致,比如resnet18中layer2-4中第一个卷积层。在上述代码中,此时需要将输入x传入downsample中将x的输入改造成与残差输入输出通道一致。

填充与卷积核尺寸、步幅、输入输出尺寸关系

  • 输出尺寸:out=(in+2paddingkernelsize)/stride+1out=\lfloor (in+2*padding-kernelsize)/stride \rfloor+1
  • same padding策略,原始图像尺寸保持不变,padding=(kernelsize1)stride/2padding=(kernelsize-1)*stride/2

image.png

  • full padding策略,确保图像每一个像素点对结果影响一致,padding=kernelsize1padding=kernelsize-1

image.png

图片来源:www.zhihu.com/question/60…

批处理层

  • 原理:在每次训练迭代中,我们首先规范化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。接下来,我们应用比例系数和比例偏移。
  • 作用:加快深层网络的收敛速度。
  • 批量规范化在训练模式和预测模式功能不同。在训练过程中,我们无法得知使用整个数据集来估计平均值和方差,所以只能根据每个小批次的平均值和方差不断训练模型;而在预测模式下,可以根据整个数据集精确计算批量规范化所需的平均值和方差。

更多细节:李沐《动手学深度学习》7.5批量规范化。

其他

python中类与构造函数
  • 类变量:类的所有实例共享属性和方法。类中直接定义的变量,比如kind;或类中方法中定义的变量,比如add_trick()方法中tricks。
  • 实例变量:每个实例数据独有。__init__中定义变量,比如name。
class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'
class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']
nn.Sequential(*layers)
  • nn.Sequential是 PyTorch 中的一个容器模块,用于将一系列子模块按顺序组合在一起,前一个模块的输出作为下一个模块的输入。
  • 当我们调用 nn.Sequential(*layers)时,*layers会将 layers 列表中的所有元素解包,并逐个作为参数传递给 nn.Sequential构造函数。
layers = [layer1, layer2, layer3]

# 下面两行代码等价
nn.Sequential(*layers)
nn.Sequential(layer1, layer2, layer3)

参考:arxiv.org/pdf/1512.03…