动手深度学习常见的卷积网络

LeNet

真正用的是sigmoid

# 在手写数字识别 1*28*28
import os.path

import torch
from torch import nn
from torchvision import transforms
import torchvision
from torch.utils import data
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.tensorboard import SummaryWriter


def get_dataloader_workers():
    return 8


def load_data_mnist(batch_size):
    # 指定格式化的方式,
    trans = transforms.ToTensor()
    # 加载数据集
    train_mnist = torchvision.datasets.MNIST(root="../data", train=True, transform=trans, download=True)
    # 测试集
    test_mnist = torchvision.datasets.MNIST(root="../data", train=False, transform=trans, download=True)
    # 60000训练集,10000测试集
    print(len(train_mnist), len(test_mnist))
    # 工作线程
    return (data.DataLoader(train_mnist, batch_size, shuffle=True, num_workers=get_dataloader_workers()),
            data.DataLoader(test_mnist, batch_size, shuffle=True, num_workers=get_dataloader_workers()))


def get_device():
    return torch.device('cuda' if torch.cuda.is_available() else 'cpu')


class Digit(nn.Module):
    def __init__(self):
        super().__init__()
        # 輸入1,輸出10,核心数是5    表示单通道的灰度图,并且会产生10个特征映射
        self.conv1 = nn.Conv2d(1, 16, 5)
        self.conv2 = nn.Conv2d(16, 20, 3)
        # 20为上一层的输出,这里的10是经过卷积->池化->卷积最后的出来的结果,计算方式为
        # 第1次卷积后输出:(28-5+1)=24,过了一次size为2步长为2的池化24/2=12 经过3x3的卷积最终输出的结果为 12-3+1=10
        self.fc1 = nn.Linear(20 * 5 * 5, 500)  # 2000条数据压缩为500
        self.fc2 = nn.Linear(500, 10)  # 最后输出分类结果

    def forward(self, x):
        input_size = x.size(0)
        x = self.conv1(x)
        x = F.relu(x)
        x = F.avg_pool2d(x, 2, 2)  # 2x2的最大池化每次移动俩个窗口
        x = self.conv2(x)  # 第二次卷积
        x = F.relu(x)
        x = F.avg_pool2d(x, 2, 2)  # 2x2的最大池化每次移动俩个窗口
        # 他这里是把所有的数据拉平到一个维度,这个维度是256
        x = x.view(input_size, -1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.fc2(x)
        x = F.relu(x)
        # 因为下面计算损失的时候已经用了softmax
        # return F.log_softmax(x, dim=1)  # 维度为1的交叉熵函数
        return x


# 训练
def train_model(model, train, optimizer, epoch, writer):
    # 设置为训练模式
    model.train()
    train_loss = 0.0
    for batch_index, (data, target) in enumerate(train):  # data是图片,target是标签
        # 部署到GPU上
        data, target = data.to(get_device()), target.to(get_device())
        output = model(data)
        # 计算损失
        loss = F.cross_entropy(output, target)
        train_loss += loss.item()
        # 梯度初始化为0
        optimizer.zero_grad()
        # 反向传播
        loss.backward()
        # 梯度下降
        optimizer.step()
        # 记录loss并可视化,第一个参数是纵坐标,第二个参数是横坐标
        global step
        step = step + 1
    # torch.save(model.state_dict(), "dict")
    # torch.save(epoch, "epoch")
    train_loss /= (len(train.dataset) / batch_size)
    writer.add_scalar('Loss/train', train_loss, epoch)
    print("epoch:{} \t loss{:.6f}".format(epoch, train_loss))


def test_model(model, test, optimizer, epoch, writer):
    # 设置为验证模式
    model.eval()
    # 准确率
    correct = 0.0
    # 测试损失
    test_loss = 0.0
    with torch.no_grad():
        for data, target in test:
            # 部署到设备上
            data, target = data.to(get_device()), target.to(get_device())
            # 测试数据
            output = model(data)
            # 计算测试损失
            test_loss += F.cross_entropy(output, target).item()
            # 找到概率最大的索引下标,沿着第一个维度找最大值,这里会返回一个元素, (values, indices) 取这个元组的第一个就是索引
            # 第0维是batch_size,第一维是输出[256,10]
            pred = output.max(1, keepdim=True)[1]
            # 累计正确的值,view_as 用来调节张量的尺寸,在俩个张量要进行比较的时候可以进行归一化
            correct += pred.eq(target.view_as(pred)).sum().item()
        test_loss /= (len(test.dataset) / batch_size)
        writer.add_scalar('Loss/test', test_loss, epoch)
        # x100是转换维百分数
        accuracy = 100.0 * correct / len(test.dataset)
        writer.add_scalar("accuracy/test", accuracy)
        print(
            "Test--average loss:{:.4f},accuracy:{:.3f}\n".format(test_loss, accuracy))


if __name__ == '__main__':
    step = 1
    batch_size = 256  # 批大小
    epochs = 50
    epoch = 0
    writer = SummaryWriter("run")
    lr = 0.02
    train, test = load_data_mnist(batch_size)
    model = Digit().to(get_device())  # 创建模型部署到设备上
    if os.path.exists("dict"):
        model.load_state_dict(torch.load("dict"))  # 加载磁盘中的文件
    if os.path.exists("epoch"):
        epoch = torch.load("epoch")
    optimizer = optim.Adam(model.parameters(), lr=lr)  # 定义优化器,使用Adam用来梯度下降类似SGD
    for i in range(10):
        epoch += 1
        train_model(model, train, optimizer, epoch, writer)
        test_model(model, test, optimizer, epoch, writer)
    writer.close()

alexNet

算力和数据量的发展是成比例的,alexNet赢得了2012年的ImageNet竞赛

主要改进了

  • 丢弃法
  • relu
  • maxpooling

老的方式:图片->人工特征提取->SVM

新的方式:图片->通过CNN学习特征->softmax回归

alexNet用了更大的输出通道。更多的卷积层,隐藏层也设置的更大

sigmod变到了relu,减缓了梯度消失,隐藏全连接层后加入了丢弃层,对数据做了增强(对图片调整)

其实alexNet就是更大更深,10x的参数,260x的计算量。

网络定义

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))

构造单通道来观察每一层的形状

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])

读取并训练

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

lenet并行度很差,所以alex对于gpu的使用率更好,所以没有慢那么多

VGG

使用块的网络

AlexNEt比LeNet更深更大,能不能更深更加大

选项:更多的全连接层(太贵),更多的卷积层,将卷积层组合成块

VGG用了大量的3x3的块来堆砌,3x3的效果会比5x5来的好,一个VGG块就是n个3x3的卷积层+最大池化层

核心思想拿走了alexnet中重复的部分(VGG块),但VGG比alexnet慢,内存消耗也大很多。

不同的卷积块个数和超参数可以得到不同复杂度的变种

原始VGG网络有5个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层。 第一个模块有64个输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到512。由于该网络使用8个卷积层和3个全连接层,因此它通常被称为VGG-11。

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


# 分别对应于卷积层的数量num_convs、输入通道的数量in_channels 和输出通道的数量out_channels.
def vgg_block(num_convs, in_channels, out_channels):
    layers = []
    for _ in range(num_convs):
        # 这里padding设置为1,核大小为3,也就是说不会改变输出规模(step是1)
        layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
        layers.append(nn.ReLU())
        # 因为前一层的输出是下一层的输入,这里第二次循环的时候output和input就是一样的
        in_channels = out_channels
    # 在这里会减小规模
    layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
    # *星号类似于扩散符
    return nn.Sequential(*layers)


# 一共5个vgg块,分别是1,1,1,2,2加起来总共是8层
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))


def vgg(conv_arch):
    conv_blks = []
    in_channels = 1  # 通道是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)
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
    X = blk(X)
    print(blk.__class__.__name__, 'output shape:\t', X.shape)

这里会输出

Connected to pydev debugger (build 232.10319.12)
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])

最后是训练模型

# 训练模型
ratio = 4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch)
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())

NiN

网络中的网络

nin块:一个卷积层后面更俩个全连接层

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

无全连接层,交替使用NiN块和步幅为2的最大池化层

交替较小高宽和增大通道数

最后使用全局平均池化层得到输出,其输入通道是类别数

1x1对于每个像素增加了非线性,使用了全局平均赤化,来替代VGG和alexNet中的全连接层,不容易过拟合,更少的参数

Nin网络定义

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


# nin块
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),
    # 执行二维自适应平均池化,高宽都为1比如输入为[10,10,5,5]输出就是[10,10,1,1]
    nn.AdaptiveAvgPool2d((1, 1)),
    # 将四维的输出转成二维的输出,其形状为(批量大小,10)
    nn.Flatten())

#查看块的形状
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

输出

Sequential output shape:     torch.Size([1, 96, 54, 54])
MaxPool2d output shape:     torch.Size([1, 96, 26, 26])
Sequential output shape:     torch.Size([1, 256, 26, 26])
MaxPool2d output shape:     torch.Size([1, 256, 12, 12])
Sequential output shape:     torch.Size([1, 384, 12, 12])
MaxPool2d output shape:     torch.Size([1, 384, 5, 5])
Dropout output shape:     torch.Size([1, 384, 5, 5])    #把维度拉高到了384
Sequential output shape:     torch.Size([1, 10, 5, 5])    #降低到10个维度
AdaptiveAvgPool2d output shape:     torch.Size([1, 10, 1, 1])    #对宽高进行全局平均池化,都降低到1个数值
Flatten output shape:     torch.Size([1, 10])

训练模型

#训练模型
lr, num_epochs, batch_size = 0.1, 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())

在大量的数据集上nin会比alexnet效果好一点

GoogleNet

在GoogleNeet中,基本的卷积块被称为Inception块。

四个路径从不同侧面抽取信息,然后在输出通道维合并

它的输入大小和输出宽高大小相同,使用不同窗口大小的卷积层,越大的卷积核关注全局或者线条,越小的卷积核关注细节(用层情况下)

根单3x3或5x5相比,inception块有更少的参数个数和计算复杂度。(用了1x1,3x3,5x5,最大池化,所以肯定比3x3和5x5更省参数)

googleNet中有大量的inception块,他借鉴了ninnet的思想(1x1卷积代替全连接)

那么为什么GoogLeNet这个网络如此有效呢? 首先我们考虑一下滤波器(filter)的组合,它们可以用各种滤波器尺寸探索图像,这意味着不同大小的滤波器可以有效地识别不同范围的图像细节。 同时,我们可以为不同的滤波器分配不同数量的参数。

inception有很多变种,这里讲的是V1

V2使用了batch nomalization

V3 修改了inception

替换5x5为多个3x3

替换5x5为1x7和7x1卷积层

替换3x3为1x3和3x1卷积层

v4使用残次连接

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


class Inception(nn.Module):
    # **kwargs 它会捕获所有未被显式指定的参数,并将其作为字典传递给函数。字典的键是参数名,值则是对应的参数值。
    # 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)


#实现
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))
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

输出

Sequential output shape:     torch.Size([1, 64, 24, 24])
Sequential output shape:     torch.Size([1, 192, 12, 12])
Sequential output shape:     torch.Size([1, 480, 6, 6])
Sequential output shape:     torch.Size([1, 832, 3, 3])
Sequential output shape:     torch.Size([1, 1024])
Linear output shape:     torch.Size([1, 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())

批量归一化

在训练的时候,损失出现在最后,最后的层训练较快

最底部的数据:

  • 底部层训练较慢
  • 底部层一变化,所有都得跟着变
  • 最后哪些层需要重新学习多次
  • 导致收敛变慢

我们可以在学习底部的时候避免改变顶部吗?

固定小批量里面的均值和方差

左边的式子为:对于小批量样本求和并除以批量大小($x_i$是向量)$\sigma$代表标准差,这里就是方差。$\epsilon$是一个很小的正整数,是为了保证不要除0的

$$ \mu_B=\frac{1}{|B|}\sum_{i\in B}x_i\text{ and }\sigma_B^2=\frac{1}{|B|}\sum_{i\in B}(x_i-\mu_B)^2+\epsilon $$

然后再做额外的调整

$\mu B$就是B的均值,$\sigma B$代表B的标准差。

$$ x_{i+1}=\gamma\frac{x_i-\mu_B}{\sigma_B}+\beta $$

批量归一化层

这里的$\gamma,\beta$都是学习参数。

作用在全连接层和卷积层的输出上,在激活函数前面,全连接层和卷积层的输入上

对于全连接层,作用在特征维,对于卷积层,作用在通道维。批量归一化是一个线性变换。

作用:

Covariate Shift 的例子:

假设您正在构建一个模型来预测房价,而您的训练数据来自一个城市区域,其中房屋大小和价格的分布是一定的。但是,当您将模型应用于另一个城市的房屋数据时,发现那里的房屋大小分布非常不同。即使两个城市中房屋大小与价格的关系相似,由于输入特征(房屋大小)的分布不同,模型的表现可能会变差。

解决 Covariate Shift 的方法:

  1. 重采样:可以通过对训练数据进行重采样(如过采样或欠采样),使其分布更接近测试数据的分布。
  2. 加权:给训练样本赋予不同的权重,使得模型更加关注那些分布与测试数据更接近的样本。
  3. 数据合成:生成一些新的样本以填补分布上的差距。
  4. 特征缩放/标准化:对数据进行预处理,比如使用批量规范化(Batch Normalization)等技术,使各层的输入分布保持稳定。
  5. 领域适应:利用领域适应算法,这些算法试图使源域(训练数据)和目标域(测试数据)之间的差异最小化。

关于 Batch Normalization (BN):

Batch Normalization 是一种在神经网络中常用的正则化技术,它能够改善内部协变量偏移(Internal Covariate Shift)问题。内部协变量偏移是指在网络训练过程中,隐藏层输入的分布发生变化,这会导致网络训练不稳定。BN 通过在每个mini-batch上规范化输入来解决这个问题,从而加速训练过程并提高模型的稳定性。

最初论文是想用它来减少内部协变量转移(底层权重变动导致顶层权重变动就是协变量的转移)

后续论文指出它可能就是通过在每个小批量里增加噪音来控制模型复杂度(其实没有减少内部协变量转移)

其中的$\hat{\mu}_B,\hat{\sigma}_B$是噪音,B代表的是一个批量的数据集

$$ x_{i+1}=\gamma\frac{x_i-\hat{\mu}_B}{\hat{\sigma}_B}+\beta $$

因此没必要跟丢弃法混合使用,可以看到中间乘上的就是标准的正态分布。

总结:

批量归一化固定小批量中的均值和方差,然后学习出适合的便宜和缩放。

可以加速收敛速度,但一般不改变模型精度。

手动实现:

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


# 这里的eps就是很小的正数(避免除0),gamma和beta是学习,
# moving_mean, moving_var是全局的均值和方差,是在做推理的时候用的,momentum是用来更新moving_mean, moving_var的
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
    # 通过is_grad_enabled来判断当前是训练模式还是预测模式
    if not torch.is_grad_enabled():
        # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        # 因为是做推理的情况下可能只有一张图片,算不出来整批的均值,所以就用全局的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        # 如果为2就是全连接层,为4就是卷积层
        assert len(X.shape) in (2, 4)
        if len(X.shape) == 2:
            # 使用全链接层的情况,计算特征维上的均值和方差
            mean = X.mean(dim=0)
            # 按特征求均值和方差
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            # 使用二位卷积层的情况,计算通道维上(axis=1)的均值和方差
            # 我们需要保持X的形状一遍可以做广播运算
            # 最后出来的就是1xnx1x1的均值,首先获取宽高的均值,在根据batch求出每一批的均值(但是保留通道数)
            # keepdim就是计算完均值后仍然保持原始的shape,默认值是false
            mean = X.mean(dim=(0, 2, 3), keepdim=True)
            var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
        X_hat = (X - mean) / torch.sqrt(var + eps)  # 正态分布
        # 更新全局移动平均的均值方差,其中momentum是一个超参数,pytorch默认是0.9
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
    Y = gamma * X_hat + beta
    return Y, moving_mean.data, moving_var.data


# 批量归一化
class BatchNorm(nn.Module):
    # num_features:完全链接层的输出数量或卷积层的输出通道数
    # num_dims:2表示全连接层,4表示卷层
    def __init__(self, num_features, num_dims):
        super().__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0(gamma为0会拟合不动)
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 非模型参数的变量初始化为0和1
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.ones(shape)

    def forward(self, X):
        # 如果X不在内存上,将moving_mean和moving_var复制到X所在的显存上
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device)
            self.moving_var = self.moving_var.to(X.device)
        # 保存更新过的moving_mean和moving_var
        Y, self.moving_mean, self.moving_var = batch_norm(
            X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.9
        )
        return Y


# 应用batchNorm于LeNet模型
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
    nn.Linear(16 * 4 * 4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),
    nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),
    nn.Linear(84, 10))
#训练
lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

pytorch简单实现

net = nn.Sequential(
    #这个6是输入输出维度,因为这是不能改变的所以只需要指定一个参数
    nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
    nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
    nn.Linear(84, 10))

一般适用于层数比较深的神经网络,而不是LeNet这种简单的神经网络。它会导致梯度稍微变大一点

争议

直观地说,批量规范化被认为可以使优化更加平滑。 然而,我们必须小心区分直觉和对我们观察到的现象的真实解释。 回想一下,我们甚至不知道简单的神经网络(多层感知机和传统的卷积神经网络)为什么如此有效。 即使在暂退法和权重衰减的情况下,它们仍然非常灵活,因此无法通过常规的学习理论泛化保证来解释它们是否能够泛化到看不见的数据。

在提出批量规范化的论文中,作者除了介绍了其应用,还解释了其原理:通过减少内部协变量偏移(internal covariate shift)。 据推测,作者所说的内部协变量转移类似于上述的投机直觉,即变量值的分布在训练过程中会发生变化。 然而,这种解释有两个问题: 1、这种偏移与严格定义的协变量偏移(covariate shift)非常不同,所以这个名字用词不当; 2、这种解释只提供了一种不明确的直觉,但留下了一个有待后续挖掘的问题:为什么这项技术如此有效? 本书旨在传达实践者用来发展深层神经网络的直觉。 然而,重要的是将这些指导性直觉与既定的科学事实区分开来。 最终,当你掌握了这些方法,并开始撰写自己的研究论文时,你会希望清楚地区分技术和直觉。

随着批量规范化的普及,内部协变量偏移的解释反复出现在技术文献的辩论,特别是关于“如何展示机器学习研究”的更广泛的讨论中。 Ali Rahimi在接受2017年NeurIPS大会的“接受时间考验奖”(Test of Time Award)时发表了一篇令人难忘的演讲。他将“内部协变量转移”作为焦点,将现代深度学习的实践比作炼金术。 他对该示例进行了详细回顾 (Lipton and Steinhardt, 2018),概述了机器学习中令人不安的趋势。 此外,一些作者对批量规范化的成功提出了另一种解释:在某些方面,批量规范化的表现出与原始论文 (Santurkar et al., 2018)中声称的行为是相反的。

然而,与机器学习文献中成千上万类似模糊的说法相比,内部协变量偏移没有更值得批评。 很可能,它作为这些辩论的焦点而产生共鸣,要归功于目标受众对它的广泛认可。 批量规范化已经被证明是一种不可或缺的方法。它适用于几乎所有图像分类器,并在学术界获得了数万引用。

ResNet

残差块

residual

串联一个层改变函数类,我们希望能扩大函数类

残差块加入快速通道(右边)来得到f(x)=x+g(x)的结构

让网络中嵌入更小的网络。

resnet块

resnet块有俩种,

  1. 高宽减半ResNet块
  2. 后接多个高宽不变ResNet块

类似VGG和googlenet的总架构,但替换成了ResNet块

残次块使得很深的网络更加容易训练,甚至可以训练超过一千层

残次网络对随后的深层神经网络设计产生了深远的影响,无论是卷积累网络还是全连接网络

$$ \text{输出可以表示为 }y=F(x)+x\text{。这里的 }F(x)\text{ 就是我们所说的“残差。} $$

ResNet还可以避免梯度消失。

Last modification:July 30, 2024
如果觉得我的文章对你有用,请随意赞赏