卷积神经网络
主体结构:卷积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时,每个卷积核都会在所有输入通道上进行卷积操作,并将结果相加生成一个输出通道。
具体来说,假设输入数据的通道为,卷积核的权重分别为(第个卷积核在每个通道上的权重)。
具体步骤:
对输入的第一个通道 与权重 进行卷积,得到一个特征图:
对输入的第二个通道 与权重 进行卷积,得到另一个特征图:
对输入的第三个通道与权重进行卷积,得到第三个特征图:
将这三个特征图相加,得到最终的输出特征图:
-
卷积 -> 激活 -> 池化顺序更能保留和提取特征,同时也有助于梯度的有效传播;卷积 -> 池化 -> 激活池化操作在激活函数之前应用,池化后的特征图尺寸减小,激活函数的计算量减少,提高计算效率。
-
激活函数的主要作用之一就是引入非线性,这使得网络能够捕捉和学习数据中的非线性特征。
残差网络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)
模型结构
- conv1是卷积层,conv2-5是4个layer。
- 注意,resnet18中conv2-4都是2个BasicBlock残差块堆积;resnet34中则是分别3、4、6、3个BasicBlock残差块堆积;而在resnet50中则是分别3、4、6、3个Bottleneck残差块堆积。
BasicBlock和Bottleneck残差结构
- 左图BacisBlock残差结构,用于resnet18/34,输入输出通道数不变。
- 右图BottleBlock残差结构,用于resnet50/101/152,先卷积核降维,再卷积核升维(通道数缩小4倍,再扩大4倍)。
实线残差与虚线残差
- 实线残差,输入输出通道一致,比如resnet18中layer1和layer2-4的除第一个卷积层的其他卷积层。
- 虚线残差,输入通道和输出通道不一致,比如resnet18中layer2-4中第一个卷积层。在上述代码中,此时需要将输入x传入downsample中将x的输入改造成与残差输入输出通道一致。
填充与卷积核尺寸、步幅、输入输出尺寸关系
- 输出尺寸:
- same padding策略,原始图像尺寸保持不变,
- full padding策略,确保图像每一个像素点对结果影响一致,
图片来源: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)