实际的CNN结构可能要更为复杂,比如:分支,输出拿来再用等;
- 分支网络:
other是指
如果还是跟之前一样,一个个的去实例化的话,那么这个Net模型的代码就会有一大堆的代码冗余,而这是我们所不期望的。从这里不难看出,每个块都有类似的结构:
如此便可直接将这些块进行封装,归为一类(称为Inception块),再将块给串联起来,从而减少了重复工作。
卷积核有一些超参数是比较难选的:kernel大小等。所以GoogleNet就把他们几个都试一试,再选最好用的那个来进行(通过设置权重大小来进行):
Concatenate表示把若干个张量拼接到一起;Average Pooling,均值池化,可以人为指定w,d,从而保证输入输出图像大小一致(也可以用padding和strid来进行替代);1* 1卷积的个数取决于输入张量的通道数;
可以看到,通过这种方式可以把featuremap中输入通道的所有信息融合到一起——信息融合(可以理解为考试之后求总分,然后按总分进行比较);
通过1* 1卷积可以改变通道数量,从而减少开销:
这里的卷积计算,其实不只是针对1* 1的卷积,对所有的卷积都是一样的。卷积核的个数,决定了输出的通道数。输入的通道数决定了卷积核的层数
用代码实现Inception Module
#分支1,池化分支
#这段写在__init__
self.branch_pool = nn.Conv2d(in_channels, 24, kernel_size=1)
#这段写在forward
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
#将上一步的结果放到上面写好的1*1的卷积后得到输出结果
branch_pool = self.branch_pool(branch_pool)
#分支2,就一个1*1的卷积
self.branch1x1 = nn.Conv2d(in_channels, 16, kernel_size=1)
#直接给1*1的卷积就行
branch1x1 = self.branch1x1(x)
#分支3,一个1*1卷积接一个5*5的
self.branch5x5_1 = nn.Conv2d(in_channels, 16, kernel_size=1)
#为了保证宽高不变,设置padding为2
self.branch5x5_2 = nn.Conv2d(16, 24, kernel_size=5, padding=2)
brach5x5 = self.branch5x5_1(x)
brach5x5 = self.branch5x5_2(brach5x5)
#此时,batch、w和h都一样,只有通道数c不一样
#分支4,一个1*1接两个3*3的
self.branch3x3_1 = nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch3x3_2 = nn.Conv2d(16, 24, kernel_size=3, padding=1)
self.branch3x3_3 = nn.Conv2d(24, 24, kernel_size=3, padding=1)
branch3x3 = self.branch3x3_1(x)
branch3x3 = self.branch3x3_2(branch3x3)
branch3x3 = self.branch3x3_3(branch3x3)
#最后要把上面四个分支拼接起来(Concatenate)
#将刚刚的四个分支的结果放到一个数组中,再通过torch的cat函数来进行拼接
outputs = [branch1x1, branch5x5, branch3x3, branch_pool]
#维度为1是因为要对channels这个维度进行拼接
return torch. cat(outputs, dim=1)
把上面的分支一起放进一个类中以及对应的Net模型就是下面这样:
class InceptionA(nn.Module):
def __init__(self, in_channels):
super(InceptionA, self).__init__()
#初始输入通道未定,这样方便实例化的时候使用
self.branch1x1 = nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch5x5_1 = nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch5x5_2 = nn.Conv2d(16, 24, kernel_size=5, padding=2)
self.branch3x3_1 = nn.Conv2d(in_channels, 16, kernel_size=1)
self.branch3x3_2 = nn.Conv2d(16, 24, kernel_size=3, padding=1)
self.branch3x3_3 = nn.Conv2d(24, 24, kernel_size=3, padding=1)
self.branch_pool = nn.Conv2d(in_channels, 24, kernel_size=1)
def forward(self, x):
branch1x1 = self.branch1x1(x)
brach5x5 = self.branch5x5_1(x)
brach5x5 = self.branch5x5_2(brach5x5)
branch3x3 = self.branch3x3_1(x)
branch3x3 = self.branch3x3_2(branch3x3)
branch3x3 = self.branch3x3_3(branch3x3)
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
branch_pool = self.branch_pool(branch_pool)
outputs = [branch1x1, branch5x5, branch3x3, branch_pool]
return torch. cat(outputs, dim=1)
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1,10, kernel_size = 5)
#24*3+16=88,这里共有88个通道
self.conv2 = nn.Conv2d(88,20, kernel_size = 5)
self.incep1 = InceptionA(in_channels=10)
self.incep2 = InceptionA(in_channels=20)
self.mp = nn.MaxPool2d(2)
#1408是根据mnist数据集经过网络计算后所得到的元素个数
self.fc = nn.Linear(1408, 10)
def forward(self, x):
in_size = x.size(0)
x = F.relu(self.mp(self.conv1(x)))
x = self.incep1(x)
x = F.relu(self.mp(self.conv2(x)))
x = self.incep2(x)
x = x.view(in_size, -1)
x = self.fc(x)
return x
将上述代码放到MNIST数据集中所得到的结果就是下面这样:
可以看到,训练太多次的时候也会出现过拟合的状态;我们可以把正确率最高的那个模型进行存盘,方便以后的使用;
- ResNet:
可以看到,如果把3×3的卷积核一直这样堆下去,那么20层的效果比56层的效果要好;
梯度消失:由一大堆<1的梯度相乘,那么最终的结果会趋近于0。这样一来,权重也就无法更新,也就是无法得到有效的训练;
ResNet就是把卷积后的结果再加上原来的x之后再进行激活,通过导数不难看出:,使其最终不会趋向于0;
具体实现就是再最后和x做一个加法就行了;
ResNet有一堆跳连接。放大那里画虚线是因为输入的x与输出张量的维度不同,要做单独的处理,如池化层等;
代码实现:
class ResidualBlock(nn.Module):
def __init__(self, channels):
super(ResidualBlock, self).__init__()
self.channels = channels
#通过padding保证输出图像大小不变,而且输入/出的通道数要一样
self.conv1 = nn.Conv2d(channels,channels, kernel_size = 3, padding=1)
self.conv2 = nn.Conv2d(channels,channels, kernel_size = 3, padding=1)
def forward(self, x):
y = F.relu(self.conv1(x))
y = self.conv2(y)
#最后这里记得要加个x
return F.relu(x + y)
这样就可以写出模型的完整类:
class Net(nn.Module):
def __init__(self, channels):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(channels,channels, kernel_size = 3, padding=1)
self.conv2 = nn.Conv2d(channels,channels, kernel_size = 3, padding=1)
self.mp = nn.MaxPool2d(2)
self,rblock1 = ResidualBlock(16)
self,rblock2 = ResidualBlock(32)
self.fc = nn.Linear(512,10)
def forward(self, x):
in_size = x.size(0)
x = self.mp(F.relu(self.conv1(x)))
x = self.rblock1(x)
x = self.mp(F.relu(self.conv2(x)))
x = self.rblock2(x)
x = x.view(in_size, -1)
x = self.fc(x)
return x
记住,如果网络结果非常复杂,那么就可以用新的类去封装他,就像这里的ResidualBlock一样;
简单的测试方法:在forward中保留前n行,然后注释掉其他行,看看输出结果和预期的结果是否一致,没问题就多保留一行,继续看结果;
以后的路
- 从理论层面探讨深度学习原理(多读书);
- 阅读Pytorch文档,不是很多,至少通读一遍;
- 复现模型的经典工作——读别人的代码,学习一下,再来自己写代码,这样一来就可以选择自己感兴趣的特定领域去发展了(如果无法复现就去看下原来的代码,学学别人的技术,从而提升自己);
- 扩充视野:不断解决知识上的盲点,从而提升自己的能力,将来就可以组装自己的模型了;