现代卷积神经网络

13897 字
35 分钟

前天我遇见了小兔,昨天遇见了小鹿,今天则遇见了你。

深度卷积神经网络(AlexNet)

1774490770434

ImageNet数据集

1774491201495

在深入学习神经网络的过程中,ImageNet会被反复提及,它的核心贡献在于:

  • 引爆了深度学习热潮:2012年在基于ImageNet数据集举办的ILSVRC(大规模视觉识别挑战赛)中,AlexNet架构利用GPU算例成功训练了深层CNN,以压倒性的优势夺冠,直接证明了深度学习在复杂视觉任务中的统治力。
  • 迁移学习:因为ImageNet数据量庞大、特征涵盖极广,后来衍生的经典模型(如VGG、ResNet等)通常都会现在ImageNet上进行预训练(Pre-training),提取出通用的视觉特征表示。之后只需要进行简单的微调,就可以将这些模型广泛应用于目标检测、图像分割或其他小数据集的分类任务中。

计算机视觉方法论的改变

人工特征提取 + SVM

阶段一:人工特征提取

  • 在传统方法中,算法不能直接看懂 原始像素矩阵,因为像素包含的信息太杂乱(容易收到光照、阴影、视觉变化影响)。因此需要计算机视觉专家通过数学和图像处理知识,手动设计算法来提取图像中最有用的信息,将其变成一组特征向量

阶段二:传统机器学习分类器

  • 特征提取完毕后,原始图片就被浓缩成了一串数字(特征向量)。接下来这串数字会被送到分类器中进行判断。支持向量机(SVM)是当时最受欢迎、数学理论最完备的分类器之一。

缺点:特征工程及其耗时、极度依赖领域专家的经验,而且人工设计的特征往往泛化能力差,很难应对真实世界中极其复杂的图像变化。

深度学习方法

AlexNet证明了:我们可以直接把原始图像扔进一个深层的卷积神经网络CNN中。

网络底层的卷积核会自动学习提取边缘、纹理等底层特征,高层会组合出猫耳朵、猫眼睛等高层语义特征。整个特征提取的过程交给了机器通过海量数据去自动学习。

最后接一个Softmax回归输出各类别的概率。这种端到端的数据驱动方式,彻底降维打击了传统的人工特征工程。

AlexNet架构

1774492887472

模型设计

AlexNet和LeNet的设计理念非常相似,但也存在显著差异。

  1. AlexNet比相对较小的LeNet5要深的多。AlexNet由八层组成:5个卷积层、2个全连接层隐藏层和1个全连接输出层。
  2. AlexNet使用ReLU而不是sigmoid作为其激活函数。

在AlexNet的第一层,卷积窗口的形状是11 x 11。由于ImageNet中大多数图像的宽和高比MNIST图像的多10倍以上,因此,需要一个更大的卷积窗口来捕获目标。第二层中的卷积窗口形状倍缩减为5 x 5,然后是 3 x 3。此外,在第一层、第二层和第五层卷积层之后,加入窗口形状为3 x 3、步幅为2的最大汇聚层。而且,AlexNet的卷积通道数目是LeNet的10倍。

此外,AlexNet将sigmoid激活函数改为更简单的ReLU激活函数。一方面,ReLU激活函数的计算更简单,它不需要如sigmoid激活函数那般复杂的求幂运算。另一方面,当使用不同的参数初始化方法时,ReLU激活函数使得训练模型更加容易。当sigmoid激活函数的输出非常接近等于0时,这些区域的梯度几乎为0,因此反向传播无法继续更新一些模型参数。相反,ReLU激活函数在正向区间的梯度总是1。因此如果模型参数没有正确初始化,sigmoid函数可能在正区间内得到几乎为0的梯度,从而使得模型无法得到有效的训练。

代码实现

借助框架,我们可以很简单的定义AlexNet:

import torch
from torch import nn
from d2l import torch as d2l

net = nn.Sequential(
    # 这里使用一个11*11的更大窗口来捕捉对象。
    # 同时,步幅为4,以减少输出的高度和宽度。
    # 另外,输出通道的数目远大于LeNet
    nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
    nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 使用三个连续的卷积层和较小的卷积窗口。
    # 除了最后的卷积层,输出通道的数量进一步增加。
    # 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
    nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Flatten(),
    # 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
    nn.Linear(6400, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(4096, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    # 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
    nn.Linear(4096, 10))
  • 激活函数从sigmoid变到了ReLu
  • 隐藏全连接层后加入了丢弃层(Dropout)
  • 数据增强

我们可以看一下数据在每一层的大小:

X = torch.randn(1, 1, 224, 224)
for layer in net:
    X=layer(X)
    print(layer.__class__.__name__,'output shape:\t',X.shape)
Conv2d output shape:	 torch.Size([1, 96, 54, 54])
ReLU output shape:	 torch.Size([1, 96, 54, 54])
MaxPool2d output shape:	 torch.Size([1, 96, 26, 26])
Conv2d output shape:	 torch.Size([1, 256, 26, 26])
ReLU output shape:	 torch.Size([1, 256, 26, 26])
MaxPool2d output shape:	 torch.Size([1, 256, 12, 12])
Conv2d output shape:	 torch.Size([1, 384, 12, 12])
ReLU output shape:	 torch.Size([1, 384, 12, 12])
Conv2d output shape:	 torch.Size([1, 384, 12, 12])
ReLU output shape:	 torch.Size([1, 384, 12, 12])
Conv2d output shape:	 torch.Size([1, 256, 12, 12])
ReLU output shape:	 torch.Size([1, 256, 12, 12])
MaxPool2d output shape:	 torch.Size([1, 256, 5, 5])
Flatten output shape:	 torch.Size([1, 6400])
Linear output shape:	 torch.Size([1, 4096])
ReLU output shape:	 torch.Size([1, 4096])
Dropout output shape:	 torch.Size([1, 4096])
Linear output shape:	 torch.Size([1, 4096])
ReLU output shape:	 torch.Size([1, 4096])
Dropout output shape:	 torch.Size([1, 4096])
Linear output shape:	 torch.Size([1, 10])

训练模型:

lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

1774578683556

不愧是5090吗,真快啊。

AlexNet的测试精度显著高于LeNet,0.8 -> 0.88。

如果我们稍微增加一点学习率与训练轮数:

1774578989122

AlexNet的测试准确率可以进一步提高到:0.917。

1774579048178

但是AlexNet模型的参数为LeNet的11倍,计算复杂度为LeNet的250倍。

继续增加训练轮数后:

1774579546619

这一次我们可以明显看到已经出现了过拟合现象。训练精度和测试精度已经拉开了一定的差异。并且训练精度非常的高。

使用块的网络(VGG)

AlexNet是一个变大的LeNet,但是其增大的方向感觉比较随意,似乎没有明确的规则。

但是如果我们希望模型更深更大,则我们需要一些好的设计思想,让整个模型设计的框架更加规则一些。这就是VGG的思路。

AlexNet比LeNet更深更大来得到更好的精度,那么能不能更深更大?

  • 更多的全连接层
  • 更多的卷积层
  • 将卷积层组合成块

VGG块

1774580660860

VGG块:

  • 3 x 3 卷积(填充1)(n层, m通道)
  • 2 x 2 最大池化层(步幅2)

为什么是3 x 3 而不是 5 x 5?

VGG的创建者发现:用更深的3x3的网络,效果优于5 x 5的稍浅的网络。

模型深但窄效果更好

VGG架构

1774581039860

VGG架构将AlexNet中卷积层的部分,替换为多个VGG块。

不同次数的重复块得到不同的架构VGG-16,VGG-19。

进度

LeNet(1995)

  • 2卷积 + 池化层
  • 2 全连接层

AlexNet

  • 更大更深
  • ReLu,Dropout,数据增强

VGG

  • 更大更深的AlexNet

1774581633586

VGG相比AlexNet,又进一步提升了模型的准确率,但同时也增加了模型计算功耗,内存大小。但是随着硬件技术的不断提升,这些开销是可以承受的。

代码实现

创建VGG块:

def vgg_block(num_convs, in_channels, out_channels):
    layers = []
    for _ in range(num_convs):
        layers.append(nn.Conv2d(in_channels, out_channels,
                                kernel_size=3, padding=1))
        layers.append(nn.ReLU())
        in_channels = out_channels
    layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
    return nn.Sequential(*layers)

创建VGG网络:

conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))

def vgg(conv_arch):
    conv_blks = []
    in_channels = 1	//输入数据通道数
    # 卷积层部分
    for (num_convs, out_channels) in conv_arch:
        conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
        in_channels = out_channels

    return nn.Sequential(
        *conv_blks, nn.Flatten(),
        # 全连接层部分
        nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 10))

net = vgg(conv_arch)

观察每层输出形状:

Sequential output shape:     torch.Size([1, 64, 112, 112])
Sequential output shape:     torch.Size([1, 128, 56, 56])
Sequential output shape:     torch.Size([1, 256, 28, 28])
Sequential output shape:     torch.Size([1, 512, 14, 14])
Sequential output shape:     torch.Size([1, 512, 7, 7])
Flatten output shape:        torch.Size([1, 25088])
Linear output shape:         torch.Size([1, 4096])
ReLU output shape:   torch.Size([1, 4096])
Dropout output shape:        torch.Size([1, 4096])
Linear output shape:         torch.Size([1, 4096])
ReLU output shape:   torch.Size([1, 4096])
Dropout output shape:        torch.Size([1, 4096])
Linear output shape:         torch.Size([1, 10])

上面我们定义的网络一共使用了:1 + 1 + 2 + 2 + 2 = 8个卷积层,3个全连接层。

总共11层网络,所以是一个经典的VGG11模型。

使用VGG11训练相同的数据集:

lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

1774583604677

相同的学习率,只需要训练10轮,就超过了AlexNet20轮的训练效果。如果继续训练VGG的上限明显更高。但是VGG11的训练速度也明显更慢了。10轮训练用了将近10分钟。Alex训练30轮才五分半。

原书中将VGG层中的所有输出通道都除以了4来减小模型容量,加快训练速度。这里我使用原版VGG11进行训练。

网络中的网络(NiN)

全连接层的问题

卷积层需要较少的参数:ci×co×k2c_i \times c_o \times k^2

  • cic_i:输入通道数
  • coc_o:输出通道数
  • k2k^2:卷积核的宽和高

但是全连接层非常的占用空间,并且过大的全连接层容易导致过拟合。

卷积层后的第一个全连接层的参数大小:

  • LeNet:16 x 5 x 5 x 120 = 48k
  • AlexNet:256 x 5 x 5 x 4096 = 26M
  • VGG:512 x 7 x 7 x 4096 = 102M

模型全连接层太大导致模型占用很多内存,占用很多的计算单元,同时模型过大导致模型非常容易过拟合。又需要添加正则化来限制模型大小。

NiN 块

1775187960598

一个卷积层后跟两个全连接层

  • 步幅1,无填充,输出形状跟卷积层输出一样
  • 起到全连接层的作用

在卷积神经网络中我们提到过:1x1的卷积层和全连接层的的效果一摸一样。 在这里也是,1x1的卷积神经网络按照像素进行融合,等价于全连接层按照像素进行全连接。

1775188481137

我的理解就是,如果使用全连接层,则是在通道维度上进行全连接。同时所有的像素共享一套参数。

NiN架构

NiN架构:

  • 无全连接层
  • 交替使用NiN块和步幅为2的最大池化层
    • NiN块:通过设置卷积核数量让通道数增加,捕捉更复杂的抽象特征
    • 步幅为2的池化层:让图片分辨率减半
  • 最后使用全局平均池化层得到输出

关于全局平均池化层得到输出

全局平局池化层的输入就是类别数:

  • 如果你要给Fashion-MNIST做10分类,那么最后一层NiN块的输出通道数就设为10。

假设NiN块最后的特征图为10 x 7 x 7(10通道,每张图7 x 7)

全局平均池化层:把这10个通道的每一张7 x 7图片取一个平均值。最后直接得到一个长度为10的向量。

  • 这个向量直接对应10个类别的置信度。它不仅极大减少了参数量,还增强了特征与类别之间的空间对应关系。

1775543543109

规律:

这几个模型有几个共同特征:

  1. 全连接层很占内存
  2. 卷积核越大越占内存
  3. 层数越多越占内存
  4. 模型越占内存越难训练

得出一个结论:多用1x1、3x3 卷积、 AdaptiveAvgPool2d替代全连接 既可以加快速度,又可以达到与全连接、大卷积核一样的效果。还有一个规律,就是图像尺寸减半,同时通道数指数增长,可以很好地保留特征。

代码实现

定义NiN块:

import torch
from torch import nn
from d2l import torch as d2l

def nin_block(in_channels, out_channels, kernel_size, strides, padding):
    return nn.Sequential(
        nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
        nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())

定义NiN模型:

net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, strides=4, padding=0),
    nn.MaxPool2d(3, stride=2),
    nin_block(96, 256, kernel_size=5, strides=1, padding=2),
    nn.MaxPool2d(3, stride=2),
    nin_block(256, 384, kernel_size=3, strides=1, padding=1),
    nn.MaxPool2d(3, stride=2),
    nn.Dropout(0.5),
    # 标签类别数是10
    nin_block(384, 10, kernel_size=3, strides=1, padding=1),
    nn.AdaptiveAvgPool2d((1, 1)),
    # 将四维的输出转成二维的输出,其形状为(批量大小,10)
    nn.Flatten())

训练模型:

lr, num_epochs, batch_size = 0.1, 15, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

1775558554150

学习15轮后:训练精度和测试精度都直逼 0.9

为了直观的感受NiNnet的优势,我们横向对比一下AlexNet与VGG模型的大小:

首先是NiNnet:

1775559124993

VGG11:

1775559150954

AlexNet:

1775559177580

NiN 的体积仅相当于 AlexNet 的 4.1%4.1\%

GoogleNet

1775561874478

在过去的网络中,我们已经见过了各种各样的卷积神经网络:

  • 5 x 5的卷积核
  • 3 x 3的卷积核
  • 1 x 1的卷积核
  • 最大池化层
  • 平均池化层

但是我们很难单纯的判断哪一种卷积层的组合最好。所以GoogLeNet应运而生。

Inception块

1775562244539

Inception块由四条并行路径组成,其中前三条路径分别使用:

  • 1 x 1卷积层
  • 3 x 3卷积层
  • 5 x 5卷积层

从不同空间大小中提取信息。

其中二三条路径上的1x1卷积层用于降低通道数,从而降低模型的复杂度。

第四条路径使用3 x 3最大汇聚层,然后使用1 x 1卷积层来改变通道数。

这四条路径都使用合适的填充来使输入与输出的高宽一致并把每条线路的输出在通道维度上连接,并构成Inception块的输出。

GoogLeNet第一个Inception块:

1775562855691

  • 第二条路径:
    • 1x1的卷积层将通道数降低到96,为了降低模型的复杂度。
    • 卷积神经网络复杂度:输入通道 * 输出通道 * kernel
  • 第三条路径:
    • 5x5的卷积层比3x3的卷积层更贵。所以我们降低了更多的通道数。直接降低到16。

图片中的白色卷积:我们可以认为是用来改变通道数的。

图片中的蓝色卷积:我们可以认为用于抽取信息。

在分配通道数时,我们应该将更多的通道留给更好的卷积。如在此Inception块中,一半的通道数都留给了3x3卷积。

一个反直觉的现象:

1775563466162

跟单3x3或5x5卷积层比,Inception块有更少的参数个数和计算复杂度。

虽然Inception块看起来花里胡哨,分了四个分支。怎么加起来反而比单一的一个层参数还要少?

  • 其中NiN中广泛运用的1x1卷积网络在发挥作用。

Inception VS 5x5卷积网络

一个5×55 \times 5的卷积核,要同时看192个输入通道,并且要生成32个这样的特征图。

  • 参数量 = 宽 x 高 x 输入通道 x 输出通道
  • 参数量 = 5 x 5 x 192 x 32 = 153600 个参数

Inception块:在进行5x5卷积之前,使用1x1卷积把厚厚的通道进行融合,变薄。

  • 第一步:1x1降维到16,参数量:1×1×192×16=3,0721 \times 1 \times 192 \times 16 = 3,072
  • 第二步:5x5提取特征,参数量:5×5×16×32=12,8005 \times 5 \times 16 \times 32 = 12,800
  • 总参数量:3,072+12,800=15,8723,072 + 12,800 = \mathbf{15,872}

结论: 15,87215,872 对比 153,600153,600,参数量直接 缩小了近 10 倍 !计算量(FLOPS)也同比例缩小。

GoogLeNet

1775564163940

GoogleNet的各个段:

1775564416750

1775564427460

1775564438141

代码实现

Inception块:

import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Inception(nn.Module):
    # c1--c4是每条路径的输出通道数
    def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
        super(Inception, self).__init__(**kwargs)
        # 线路1,单1x1卷积层
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        # 线路2,1x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1x1卷积层后接5x5卷积层
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3x3最大汇聚层后接1x1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        # 在通道维度上连结输出
        return torch.cat((p1, p2, p3, p4), dim=1)

逐个实现GoogLeNet的每个模块:

模块一:

b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

模块二:

b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.ReLU(),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

模块三:

b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                   Inception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

模块四:

b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
                   Inception(512, 160, (112, 224), (24, 64), 64),
                   Inception(512, 128, (128, 256), (24, 64), 64),
                   Inception(512, 112, (144, 288), (32, 64), 64),
                   Inception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

模块五:

b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                   Inception(832, 384, (192, 384), (48, 128), 128),
                   nn.AdaptiveAvgPool2d((1,1)),
                   nn.Flatten())

net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))

训练模型:

lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

1775634889667

GoogLeNet大小如下:

1775634914377

批量归一化

背景

1775648493571

数据:左侧 —>右侧

损失函数在最右侧计算

  • 损失出现在最顶部,则顶部的层训练的较快。

    顶部参数直接接收损失函数的梯度,路径最短

  • 数据出现在最底部
    • 底部的层训练的较慢(经过层层链式法则,传播过程中存在损耗与偏移)
    • 底部层一变化,所有都得跟着变(上层都是建立在底层的输出特征之上)
    • 最后的那些层需要重新学习多次(底层一变,上层需要重新学习)
    • 导致收敛变慢(顶层的学习是在瞎忙活,需要重新学习多次)

批量归一化(Batch Normalization)

批量归一化的核心目的是:

  • 让神经网络的训练变得更快、更稳定
  • 批量归一化用来将跑偏的数据硬拽回来。

批量归一化的步骤:

  1. 标准化:固定小批量里的均值和方差
μB=1BiBxi\mu_B = \frac{1}{|B|} \sum_{i \in B} x_i σB2=1BiB(xiμB)2+ϵ\sigma_B^2 = \frac{1}{|B|} \sum_{i \in B} (x_i - \mu_B)^2 + \epsilon
  • 每次训练时,我们取一小批数据(Mini-batch,即下标B)
  • 先算出这批数据的平均值μB\mu_BσB2\sigma_B^2
  • 公式中方差后加了一个极小的常数ϵ\epsilon,这是为了防止方差为0时出现除以0的数学错误
  • 接着,用每个数据点减去均值,再除以标准差σB\sigma_B

完成这一步后,这一批数据的分布就被强行拉回了均值为0、方差为1的标准正太分布

  1. 拉伸与平移:做额外的调整
    • 强制把所有层的数据都变成均值 0、方差 1,可能会 破坏掉网络本来已经学到的特征 。有些激活函数(比如 ReLU)在特定的分布下表现才最好。
    • 所以我们引入了两个新的可学习参数
      • γ\gamma :负责“拉伸”,相当于新的 方差 。它可以放大或缩小数据的分布范围。
      • β\beta:负责“平移”,相当于新的 均值 。它可以把整个数据分布向左或向右移动。

1775977395779

批量归一化层(BN层)

批量归一化层的标准插入位置是:线性变换之后,激活函数之前。

加入BN层后数据流向为:

输入 \rightarrow 线性层 (FC/Conv) \rightarrow 批量归一化 (BN) \rightarrow 激活函数 \rightarrow 下一层

为什么放在这里?

线性变换很容易改变数据的分布范围,导致数据跑偏。如果直接喂给激活函数,很容易落入激活函数的”饱和区”(比如Sigmoid的两端,梯度几乎为0),或者ReLU的死亡区(小于0直接归0)。BN相当于在激活函数前面设了一个安检站,把跑偏的数据重新拉回均值为0、方差为1的舒适区,让激活函数发挥最大的效用。

对于全连接层:作用特征维

对于全连接层,BN是独立地对每一个特征维度进行归一化的。

  • 一个有 NN 行、dd 列的表格。BN 会在每一列上单独计算这 NN 个数字的均值和方差。
  • 因此,你会得到 dd 个均值和 dd 个方差。
  • 对应的,上一节讲的可学习参数 γ\gammaβ\beta 也各自是一个长度为 dd 的向量。每个特征都有自己专属的拉伸和平移参数。

对于卷积层:作用在通道维

在卷积层中,BN将同一个通道里的所有像素点当成一个整体来处理

  • 对于某一个特定的通道,BN 会把这 NN 个样本中,该通道下所有 h×wh \times w 个像素点全部收集起来,计算它们的均值和方差。
  • 也就是说,计算均值和方差的有效样本量是 N×h×wN \times h \times w
  • 最终,你只会得到 cc 个均值和 cc 个方差。
  • 对应的可学习参数 γ\gammaβ\beta 也是长度为 cc 的向量, 同一个通道内的所有像素共享这一对 γ\gammaβ\beta

为什么卷积层要按通道来?

因为卷积的核心思想是“参数共享”和“空间平移不变性”。同一个卷积核扫过整张图片,提取的是同一种特征(比如边缘、纹理),生成了同一个通道的特征图。既然同通道的像素都是由同一个卷积核生成的,它们理应遵循相似的数据分布,所以把它们放在一起做归一化是最合理的。

1775979419177

ReLU需要通过裁切一部分神经元来引入非线性。

有意思的现象:

  • 最初论文是想用批量归一化来减少内部协变量转移
  • 后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度。

现阶段工程方面领先于理论研究,还没有确切的理论。

批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放。而且可以加速收敛速度,但一般不改变模型精度。

  • 把所有层的学习率都拉到同一尺度上。