本文主要介绍一下AlexNet、VGG、GoogLeNet以及ResNet这几个经典模型。顺便附上部分PyTorch实现。
网上的各种介绍很多,我也就不再重复说了。这篇文章主要是说说自己的感想。
今天看AlexNet其实已经颇为平淡了,毕竟这几年在网络工程上已经有了很大的进步,AlexNet的很多设计也不再被使用,例如LRN就被BN代替。不过当年AlexNet在两个GPU上跑的设计,倒是影响了后面出现Group Convolution的出现。
随后出现的VGG,主要的贡献就是验证了小kernel多层数的有效性,两层3x3的卷积层等价于一层5x5卷积层,却拥有了更多了非线性和更少的参数。VGG也因为其整洁的网络结构,成为其他模型在底层提取特征的首选。
跟VGG同年出现的GoogLeNet,又叫Inception,也是一个里程碑式的工作,也带动了后面一批工作的出现,比如Inception的后续若干版本,比如Xception。这个工作的网络结构很复杂,设计了Inception Module,又在中间层引入辅助分类器并且加权计算loss来克服梯度弥散问题,还用1x1的卷积层降维度。最后用了Global Average Pooling,应该是Network In Network首先使用的,既减少了模型的参数了又克服了过拟合。
以下是Inception Module的实现,我并没有加入BN。
class Inception(nn.Module):
def __init__(self, in_channels, k_1x1, k_3x3red, k_3x3, k_5x5red, k_5x5, pool_proj):
super(Inception, self).__init__()
self.b1 = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=k_1x1, kernel_size=1),
nn.ReLU(inplace=True)
)
self.b2 = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=k_3x3red, kernel_size=1),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=k_3x3red, out_channels=k_3x3, kernel_size=3, padding=1),
nn.ReLU(inplace=True)
)
self.b3 = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=k_5x5red, kernel_size=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=k_5x5red, out_channels=k_5x5, kernel_size=5, padding=1),
nn.ReLU(inplace=True)
)
self.b4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1),
nn.Conv2d(in_channels=in_channels, out_channels=pool_proj, kernel_size=1, padding=1),
nn.ReLU(inplace=True)
)
def forward(self, x):
y1 = self.b1(x)
y2 = self.b2(x)
y3 = self.b3(x)
y4 = self.b4(x)
return(torch.cat([y1, y2, y3, y4], 1))
最后是鼎鼎大名的ResNet。在我看来,Residual Learning这种想法的提出,是天才的是革命性的。从ResNet开始,神经网络在分类任务上第一次超越人类。整体的思路特别简单清晰,即skip connection,就是把数据复制一份,输入到卷积层中,再把卷积层的输出跟原始数据一一对应相加。这种设计有效的克服了梯度弥散的问题,从此之后,神经网络可以稳定的突破百层。而且也是在ResNet之后,神经网络的解构设计转向了block,不再从整体上重新设计网络。这种设计展现了极好的泛化能力,几乎所有新的block,即使在传统的类VGG的结构上已经取得了很不错的效果,加入Residual的思想之后,表现往往可以进一步提高,并且这种设计并没有大量提高模型的复杂度。后续基于ResNet又出现了很多优秀的工作,包括2017年CVPR上UniChicago提出的FractalNet,声称是跳出Residual Learning的一种新模型,我个人感觉本质上只是Residual Learning的另一种表现而已。
以下是用于深层ResNet的BottleNeck block的实现。
class BottleNeck(nn.Module):
def __init__(self, in_channels, out_channels, stride):
super(BottleNeck, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=out_channels, out_channels=out_channels, kernel_size=3, stride=stride, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
self.conv3 = nn.Sequential(
nn.Conv2d(in_channels=out_channels, out_channels=(out_channels * 4), kernel_size=1),
nn.BatchNorm2d((out_channels * 4)),
nn.ReLU(inplace=True)
)
self.relu = nn.ReLU(inplace=True)
self.downsample = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=(out_channels * 4), kernel_size=1, stride=stride),
nn.BatchNorm2d((out_channels * 4))
)
def forward(self, x):
identity = x
identity = self.downsample(identity)
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x + identity
x = self.relu(x)
return(x)
仔细读过以上四篇论文,并且写代码重现之后,再读其他论文就会有一种“这不就是把那XXX改了一下嘛”的感觉了。
完整实现在