Skip to content

Advanced CNN

Giovanna

About 1185 wordsAbout 4 min

2024-07-24

前面的网络结构都是串行的,但实际上网络可以具有更复杂的结构。

GooLeNet

tmp36A6.png

减少代码的冗余:函数/类

可以发现网络中有许多结构相同的模块(Inception Module),可以将其封装成类。

Inception Module

一个典型的Inception Module的例子:

tmp8DBC.png

为什么要采用并行的结构?

  • 构造神经网络时超参数难以选择,于是采用并行以尝试多种卷积组合
  • 效果好的卷积组合权重变大,最终可以找到最优的卷积组合

Concatenate:张量拼接

为了不同卷积组合的结果能够拼接,(b,C,W,H)(b,C,W,H)中除了CC都必须相同。

可以使用padding和stride调整结果。

What is 1x1 convolution?

tmp2D0C.png

可以看出,1×11\times1的卷积不会使信息丢失,输入的三个张量发生了信息融合

tmpB161.png

  • 卷积核的大小是5×55\times5
  • 假设对输入张量进行了padding,那么每个通道中取28×2828\times28个块与卷积核卷积
  • 输入张量有192个通道
  • 输出张量要求通道数为32,所以输入张量分别与卷积核的32个通道进行卷积

所以最终需要的计算次数为:

52×282×192×32=120,422,400 5^2\times28^2\times192\times32=120,422,400

运算量太大!!!

1×11\times1的卷积可以改变通道数量以降低运算量。

tmp48A6.png

所需计算次数为:

12×282×192×16+55×282×16×32=12,433,648 1^2\times28^2\times192\times16+5^5\times28^2\times16\times32=12,433,648

运算量只有之前的110\frac{1}{10}

Implementation of Inception Module

分析各个分支

tmpFF71.png

图片最后漏了

self.branch3x3_3 = nn.Conv2d(24, 24, kernel_size=3, padding=1)

拼接

tmp8749.png

outputs = [branch1x1, branch5x5, branch3x3, branch_pool]
return torch.cat(outputs, dim=1)

代码实现

class InceptionA(nn.Module):
	def __init__(self, in_channels):
		super().__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)

		branch5x5 = self.branch5x5_1(x)
		branch5x5 = self.branch5x5_2(branch5x5)

		branch3x3 = self.branch3x3_1(x)
		branch3x3 = self.branch3x3_2(branch3x3)
		branch3x3 = self.branch3x3_3(branch3x3)

		branch_pool = F.avg_pool2d(x, kernel=3, stride=1, padding=1)
		branch_pool = self.branch_pool(branch_pool)

		outputs = [branch1x1, branch5x5, branch3x3, branch_pool]
		return torch.cat(outputs, dim=1)

Using Inception Module

class Net(nn.Module):
	def __init__(self):
		super().__init__()
		self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
		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)
		self.fc = nn.Linear(1408, 10)

	def forward(self, x):
		in_size = x.size(0)                 # 1*28*28
		x = F.relu(self.mp(self.conv1(x)))  # 卷积10*24*24、池化10*12*12、relu
		x = self.incep1(x)                  # 88*12*12
		x = F.relu(self.mp(self.conv2(x)))  # 卷积20*8*8、池化20*4*4、relu
		x = self.incep2(x)                  # 88*4*4
		x = x.view(in_size, -1)             # 向量 88*4*4=1408
		x = self.fc(x)                      # 全连接 1408->10
		return x

这个1408实际写代码不用自己算,看报错信息哈哈哈。

给之前MNIST的代码换上新模型,运行结果如下:

tmp4A31.png

Can we stack layers to go deeper?

tmp73C4.png

更多的层数叠加不一定能获得更好的效果。

  • 过拟合
  • 残差消失
  • ...

Residual Network

Residual Net

tmp509E.png

Residual Net进行的操作:输入张量经过卷积、激活、卷积后的结果加上原本的输入张量再进行激活。

Residual Net通过把原本可能很小的梯度变成1附近的数字解决梯度消失的问题:

Lx=Lzzx \frac{\partial L}{\partial x}=\frac{\partial L}{\partial z}\cdot\frac{\partial z}{\partial x}

z=F(x)+x z=F(x)+x

zx=Fx+1 \frac{\partial z}{\partial x}=\frac{\partial F}{\partial x}+1

tmp2E5B.png

Residual net里存在跳连接,虚线是因为张量维度不同需要单独处理,可以直接不进行跳连接,也可以将x直接进行池化操作转化为同样的大小。

Implementation of Simple Residual Network

tmpCC91.png

注意到Residual Block的输入和输出通道保持一致,可以传入通道数以确定输出的通道。

class ResidualBlock(nn.Module):
	def __init__(self, channels):
		super().__init__()
		self.channels = channels
		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)
		return F.relu(x+y)

网络的实现如下:

class Net(nn.Module):
	def __init__(self):
		super(Net, self).__init__()
		self.conv1 = nn.Conv2d(1, 16, kernel_size=5)
		self.conv2 = nn.Conv2d(16, 32, kernel_size=5)
		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)                 # 1*28*28
		x = self.mp(F.relu(self.conv1(x)))  # 卷积16*24*24,激活,池化16*12*12
		x = self.rblock1(x)                 # Residual 16*12*12
		x = self.mp(F.relu(self.conv2(x)))  # 卷积32*8*8,激活,池化32*4*4
		x = self.rblock2(x)                 # Residual 32*4*4=512
		x = x.view(in_size, -1)
		x = self.fc(x)                      # 全连接512->10
		return x

运行结果如下:

tmpF19C.png

Tips

  • 有多个分支,可以定义变量然后拼接不同分支运行结果。
  • 可以用类封装结构相同的网络。
  • 要计算好size,逐步扩大网络逐步测试。