Pytorch:十一、CNN(高级)


实际的CNN结构可能要更为复杂,比如:分支,输出拿来再用等;

  • 分支网络
GoogleNet

other是指

如果还是跟之前一样,一个个的去实例化的话,那么这个Net模型的代码就会有一大堆的代码冗余,而这是我们所不期望的。从这里不难看出,每个块都有类似的结构:

如此便可直接将这些块进行封装,归为一类(称为Inception块),再将块给串联起来,从而减少了重复工作。

卷积核有一些超参数是比较难选的:kernel大小等。所以GoogleNet就把他们几个都试一试,再选最好用的那个来进行(通过设置权重大小来进行):

Concatenate表示把若干个张量拼接到一起;Average Pooling,均值池化,可以人为指定w,d,从而保证输入输出图像大小一致(也可以用padding和strid来进行替代);1* 1卷积的个数取决于输入张量的通道数

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。这样一来,权重也就无法更新,也就是无法得到有效的训练;

普通Net与ResNet

ResNet就是把卷积后的结果再加上原来的x之后再进行激活,通过导数不难看出:\frac{∂z}{∂x} = \frac{∂F}{∂x} + 1,使其最终不会趋向于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行,然后注释掉其他行,看看输出结果和预期的结果是否一致,没问题就多保留一行,继续看结果;

ResNet的结果

以后的路

  • 从理论层面探讨深度学习原理(多读书);
  • 阅读Pytorch文档,不是很多,至少通读一遍;
  • 复现模型的经典工作——读别人的代码,学习一下,再来自己写代码这样一来就可以选择自己感兴趣的特定领域去发展了(如果无法复现就去看下原来的代码,学学别人的技术,从而提升自己);
  • 扩充视野:不断解决知识上的盲点,从而提升自己的能力,将来就可以组装自己的模型了;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容