googlenet是2014年ilsvrc冠军,一共22层。这个网络中,作者使用了新的inception模块来进行多尺度信息融合,让神经网络变得更宽。同时googlenet比他的前辈alexnet相比,在精度大大提升的同时,参数数量大大减少(是alexnet的1/12),使得网络更加精简部署在移动设备上成为可能。 经过试验,googlenet保持一次推理只运行15亿次(乘法、加法)计算的常量。这样在实际生产中,googlenet也具有极大价值。
googlenet的实现中,最重要的就是其inception模块(启发自小李子的电影,盗梦空间。那里面有一句经典语录: we must go deeper. 论文中的deep有两个意思: 1> 更加深入的研究神经网络: 引出 inception模块; 2>字面意义上的更加深的网络结构)。
googlenet的inception模块的设计思路来自Network in network(https://zhuanlan.zhihu.com/p/31199400 提出 1*1卷积) 和 有关网络稀疏性的经典文章 https://arxiv.org/abs/1310.6343 ,并且是这些思路的具体实现(inception模块内部大量使用1*1卷积层,以较少数据维度/深度,引入更多的非线性层,减少了计算量,inception内容通过将一个卷积层拆解为多层小卷积的叠加,增加了稀疏性)。
inception 模块
论文中inception有一些变种,这里我们先看下原始版本的inception:
可以看出,前一层数据传入当前inception 模块后,会同时经过1*1, 3 * 3, 5 * 5以及3*3 的pooling的处理,之后这些输出被concat在一起。这个结构的一个重要的问题是,因为我们在顶层会将多个卷积/池化层的输出concat在一起,那么最终的输出通道数就是下层通道数的和,也就意味着如果将多个inception 叠到一起,会发生整个网络结构的通道数越来越多的问题(最终计算量和参数量都是出现爆炸现象)。因此论文中就提出了如下降维操作
如上图,在数据经过3*3 和5*5卷积核处理之前,数据先经过1*1 降维。在经过3*3 池化后也要通过1*1降维。最终保证inception模块整体输出的维度不会过大。
具体代码:
class BasicConv2d(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
**kwargs: Any
) -> None:
super(BasicConv2d, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs)
self.bn = nn.BatchNorm2d(out_channels, eps=0.001)
def forward(self, x: Tensor) -> Tensor:
x = self.conv(x)
x = self.bn(x)
return F.relu(x, inplace=True)
class Inception(nn.Module):
def __init__(
self,
in_channels: int,
ch1x1: int,
ch3x3red: int,
ch3x3: int,
ch5x5red: int,
ch5x5: int,
pool_proj: int,
conv_block: Optional[Callable[..., nn.Module]] = None
) -> None:
super(Inception, self).__init__()
if conv_block is None:
conv_block = BasicConv2d
self.branch1 = conv_block(in_channels, ch1x1, kernel_size=1)
self.branch2 = nn.Sequential(
conv_block(in_channels, ch3x3red, kernel_size=1),
conv_block(ch3x3red, ch3x3, kernel_size=3, padding=1)
)
self.branch3 = nn.Sequential(
conv_block(in_channels, ch5x5red, kernel_size=1),
# Here, kernel_size=3 instead of kernel_size=5 is a known bug.
# Please see https://github.com/pytorch/vision/issues/906 for details.
conv_block(ch5x5red, ch5x5, kernel_size=5, padding=1)
)
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1, ceil_mode=True),
conv_block(in_channels, pool_proj, kernel_size=1)
)
def _forward(self, x: Tensor) -> List[Tensor]:
branch1 = self.branch1(x)
branch2 = self.branch2(x)
branch3 = self.branch3(x)
branch4 = self.branch4(x)
outputs = [branch1, branch2, branch3, branch4]
return outputs
def forward(self, x: Tensor) -> Tensor:
outputs = self._forward(x)
return torch.cat(outputs, 1)
如之前描述:
inception 模块包含四个分支:
第一个分支是一个1 * 1的卷积层
第二个分支是 1 * 1 之后连一个3 * 3卷积层
第三个分支是 1 * 1 之后连一个5 * 5卷积层
第四个分支最大池化后连一个1 * 1卷积层
这里使用一个Inception类来描述模块,并且子分支的filter个数都可以参数化修改。
BasicCond2D很简单不再赘述。
注意inception模块最后输出forward里面,使用torch.cat(outputs,1)对多个特征图进行连接,这里cat的第二个参数为1 ,即将多个特征图按照输出特征维度’摞起来‘ (默认数据维度排列为:[batch,channel, hight, width])
网络整体结构
最终的googlenet就是将多个inception模块摞起来:
整体的架构图很大这里不贴了,只贴一个网络结构描述以及对应参数个数等信息的表格(原始论文中的参数和计算量表好像有问题,以下表格是来自很多博客给出的修正后的参数和计算量表格):
这里简单计算一下参数数量和计算量:
第一个卷积层:
3a inception模块:
(192*64(第一个1*1) + 192*96(3*3之前的1*1) + 9*128*96(3*3) +192*16(5*5之前的1*1) + 25*16*32(5*5) + 192*32(池化后的1*1)) =
(12288 +18432 +110592 +3072 + 12800 +6144)= 159.5k
其余类似,这里有个具体的计算表: https://zhuanlan.zhihu.com/p/32566024
整体代码如下:
class GoogLeNet(nn.Module):
def __init__(
self,
num_classes: int = 1000,
) -> None:
super(GoogLeNet, self).__init__()
conv_block = BasicConv2d
inception_block = Inception
self.conv1 = conv_block(3, 64, kernel_size=7, stride=2, padding=3)
self.maxpool1 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
self.conv2 = conv_block(64, 64, kernel_size=1)
self.conv3 = conv_block(64, 192, kernel_size=3, padding=1)
self.maxpool2 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
self.inception3a = inception_block(192, 64, 96, 128, 16, 32, 32)
self.inception3b = inception_block(256, 128, 128, 192, 32, 96, 64)
self.maxpool3 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
self.inception4a = inception_block(480, 192, 96, 208, 16, 48, 64)
self.inception4b = inception_block(512, 160, 112, 224, 24, 64, 64)
self.inception4c = inception_block(512, 128, 128, 256, 24, 64, 64)
self.inception4d = inception_block(512, 112, 144, 288, 32, 64, 64)
self.inception4e = inception_block(528, 256, 160, 320, 32, 128, 128)
self.maxpool4 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
self.inception5a = inception_block(832, 256, 160, 320, 32, 128, 128)
self.inception5b = inception_block(832, 384, 192, 384, 48, 128, 128)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.2)
self.fc = nn.Linear(1024, num_classes)
def _forward(self, x: Tensor) -> Tuple[Tensor, Optional[Tensor], Optional[Tensor]]:
# N x 3 x 224 x 224
x = self.conv1(x)
# N x 64 x 112 x 112
x = self.maxpool1(x)
# N x 64 x 56 x 56
x = self.conv2(x)
# N x 64 x 56 x 56
x = self.conv3(x)
# N x 192 x 56 x 56
x = self.maxpool2(x)
# N x 192 x 28 x 28
x = self.inception3a(x)
# N x 256 x 28 x 28
x = self.inception3b(x)
# N x 480 x 28 x 28
x = self.maxpool3(x)
# N x 480 x 14 x 14
x = self.inception4a(x)
# N x 512 x 14 x 14
x = self.inception4b(x)
# N x 512 x 14 x 14
x = self.inception4c(x)
# N x 512 x 14 x 14
x = self.inception4d(x)
# N x 528 x 14 x 14
x = self.inception4e(x)
# N x 832 x 14 x 14
x = self.maxpool4(x)
# N x 832 x 7 x 7
x = self.inception5a(x)
# N x 832 x 7 x 7
x = self.inception5b(x)
# N x 1024 x 7 x 7
x = self.avgpool(x)
# N x 1024 x 1 x 1
x = torch.flatten(x, 1)
# N x 1024
x = self.dropout(x)
x = self.fc(x)
# N x 1000 (num_classes)
return x, aux2, aux1
def forward(self, x: Tensor) -> GoogLeNetOutputs:
x = self._forward(x)
return x
注意:
在googlenet中,有三个辅助训练的输出,在后续的论文中,发现辅助训练输出没有什么用,这里我们就没有设置该输出了。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。