动手学深度学习Pytorch版(李沐)(5-7章)

发布于:2025-06-15 ⋅ 阅读:(16) ⋅ 点赞:(0)


前言

这篇是我的读书笔记,记录第5到10章内容

五、深度学习计算

5.1层和块

这里开始介绍Pytorch的一些代码框架,这里net是一个类大示例,net后接一个元组,会调用这个类的net.call(X)方法,call(X)方法在pytorch中会调用模块的forward方法

import torch
from torch import nn
from torch.nn import functional as F
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
X = torch.rand(2, 20)
net(X)

这里self._modules[str(idx)]是一个有序字典,在模块参数初始化过程中,系统会在这里self._modules[str(idx)]里查找模块并初始化。net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))比如,MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)会被顺序的保存在有序字典里,并在net.parameters()中桉顺序查找各个模块的参数。

class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for idx, module in enumerate(args):
            # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
            # 变量_modules中。_module的类型是OrderedDict
            self._modules[str(idx)] = module

    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        for block in self._modules.values():
            X = block(X)
        return X

模块或者层的forward方法可以嵌入一些python运算,自己定义写复杂功能,并且可以指定那些参数是不能更新的,也就是常数,比如这里的self.rand_weight就是常数,我们指定它不需要计算梯度。

class FixedHiddenMLP(nn.Module):
    def __init__(self):
        super().__init__()
        # 不计算梯度的随机权重参数。因此其在训练期间保持不变
        self.rand_weight = torch.rand((20, 20), requires_grad=False)
        self.linear = nn.Linear(20, 20)

    def forward(self, X):
        X = self.linear(X)
        # 使用创建的常量参数以及relu和mm函数
        X = F.relu(torch.mm(X, self.rand_weight) + 1)
        # 复用全连接层。这相当于两个全连接层共享参数
        X = self.linear(X)
        # 控制流
        while X.abs().sum() > 1:
            X /= 2
        return X.sum()

注意self.module_的嵌套格式,将来参数初始化的时候也是递归初始化的。

class NestMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
                                 nn.Linear(64, 32), nn.ReLU())
        self.linear = nn.Linear(32, 16)

    def forward(self, X):
        return self.linear(self.net(X))

chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)

Sequential(
(0): NestMLP(
(net): Sequential(
(0): Linear(in_features=20, out_features=64, bias=True)
(1): ReLU()
(2): Linear(in_features=64, out_features=32, bias=True)
(3): ReLU()
)
(linear): Linear(in_features=32, out_features=16, bias=True)
)
(1): Linear(in_features=16, out_features=20, bias=True)
(2): FixedHiddenMLP(
(linear): Linear(in_features=20, out_features=20, bias=True)
)
)

5.2 参数管理

import torch
from torch import nn
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
print(net.state_dict())#参数通过有序字典来管理
next(iter(net[0].named_parameters()))#net[0].named_parameters()返回的是一个生成器,一次返回一个元组(名称,数值)
net.state_dict()['2.bias'].data#在net.state_dict()我们应该观察到其返回值是一个生成器,一次生成一个元组包含名字和值,这名字是更具层的嵌套和参数的索引生成的,比如这里'2.bias'
#这里生成一个嵌套层
def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                         nn.Linear(8, 4), nn.ReLU())
def block2():
    net = nn.Sequential()
    for i in range(4):
        # 在这里嵌套
        net.add_module(f'block {i}', block1())
    return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
print(*[(name, param.shape) for name, param in rgnet.named_parameters()])#这里的生成器一次就会返回('0.block 0.0.weight', torch.Size([8, 4])) 这样的一个元组,名字是各个层的名字和参数的嵌套
#参数初始化方法
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
nn.init.constant_(m.weight, 1)
nn.init.xavier_uniform_(m.weight)
nn.init.constant_(m.weight, 42)
nn.init.uniform_(m.weight, -10, 10)
#层的权重是可以直接访问和更新的
net[0].weight.data[:] += 1
# 我们需要给共享层一个名称,以便可以引用它的参数
#可以建立共享层,也就是用同一套权重和偏执,这在后面防止过拟合时会用到。
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])
#类似于引用,梯度时由于在两个位置出现,share层的权重和梯度要加和的。

5.3 延后初始化

这个技术挺实用的,可以等到输入数据X,并从X中推断输入层的输入端数量,并初始化网络。识别出第一层的形状后,框架处理第二层,依此类推,直到所有形状都已知为止。 注意,在这种情况下,只有第一层需要延迟初始化,但是框架仍是按顺序初始化的。 等到知道了所有的参数形状,框架就可以初始化参数。

5.4 自定义层

class MyLinear(nn.Module):
    def __init__(self, in_units, units):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(in_units, units))
        self.bias = nn.Parameter(torch.randn(units,))
    def forward(self, X):
        linear = torch.matmul(X, self.weight.data) + self.bias.data
        return F.relu(linear)

linear = MyLinear(5, 3)
linear.weight#可以通过传统方法,也就时变量名字的方式直接访问
next(iter(linear.named_parameters())) #也可以通过生成器访问
#结合这个类体会一下.named_parameters()这个生成器,在类的初始化过程中,参数需要通过nn.Parameter(torch.randn(in_units, units))进行实例化,这个过程中,会生成一个变量名字和变量值的键值对

5.5读写文件

import torch
from torch import nn
from torch.nn import functional as F
x = torch.arange(4)
torch.save(x, 'x-file') #文件名字x-file
x2 = torch.load('x-file')
#一个文件可以存一个张量列表
y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
#一个文件存一个张量字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
##存一个模型也就是存模型参数
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(20, 256)
        self.output = nn.Linear(256, 10)

    def forward(self, x):
        return self.output(F.relu(self.hidden(x)))
net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)
torch.save(net.state_dict(), 'mlp.params')
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))#初始化一个训练好的模型
clone.eval()

5.6 GPU

nvidia-smi黑窗口查询英伟达GPU是否安装好驱动和cuda

import torch
from torch import nn
torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')
#pytorch的device支持cpu和GPU以及多个GPU
torch.cuda.device_count()#返回GPU数量
x = torch.tensor([1, 2, 3])
x.device#查找张量位置,CPU还是GPU
X = torch.ones(2, 3, device=torch.device("cuda:0")) #指定张量位置
Z = X.cuda(1) #将GPU0上X复制到GPU1上变量Z
Y = torch.ones(2, 3, torch.device('cuda:1')
Y+Z#同一个device才能相加
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=torch.device('cuda:0')) #指定net在那个deivice

二、卷积神经网络

6.2图像卷积

下面的代码很重要,解释了pytorch中卷积或者相关是如何计算的,简单结论是pytorch的卷积是在(input_channel,kernel_h,kernel_w)这三个维度上相乘求和的,代码如下

import torch
from torch import nn
from d2l import torch as d2l
X = torch.arange(2*3*4,dtype=torch.float).reshape(1,2,3,4) #这里batch,input_channel,h,w这里重要讲解一下input_channel,我们后面卷积网络的weight的input_channel必须和X这里一致
conv2d = nn.Conv2d(2,1, kernel_size=(1, 2), bias=False)#nn.conv2d(input_channel,output_channel,...)这里input_channel需要和输入X的input_channel一致,也就是说卷积或者相关是在(input_channel,kernel_h,kernel_w)这三个维度上相乘求和的。
nn.init.constant_(conv2d.weight,2)#将卷积核初始化成常数,方便检查卷积的运算规律
X,conv2d(X), conv2d(X).shape,conv2d.weight.data.shape #打印结果,印证猜想

这里有个训练求解卷积核的代码,如下:

# 构造一个二维卷积层,它具有1个输出通道和形状为(12)的卷积核
X = torch.ones((2*6, 8))
K = torch.tensor([[1.0, -1.0]]) #这个是我们目标卷积核,形状是(1,2),这里容易误解,我们pytorch里还有一个维度通道,
Y = corr2d(X, K)#
X = X.reshape((2, 1, 6, 8))#恢复成batch,channel,h,w这个pytorch的格式
Y = Y.reshape((2, 1, 6, 7))#目标数据
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False) #这里由于X是一个输入通道,所以输入通道为1,其实目标核本质也是一个通道,

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1

lr = 3e-2  # 学习率

for i in range(10):
    Y_hat = conv2d(X)
    l = (Y_hat - Y) ** 2
    conv2d.zero_grad()#清零
    l.sum().backward()#反向传播计算梯度
    # 迭代卷积核
    conv2d.weight.data -= lr * conv2d.weight.grad #更新权重
    if (i + 1) % 2 == 0:
        print(f'epoch {i+1}, loss {l.sum():.3f}')

6.2填充和步幅

import torch
from torch import nn
X = torch.ones(size=(1,1,8, 8))
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
conv2d(X).shape #torch.Size([1, 1, 2, 2])#这里分析h,(原始X有8+padding0行-kernel3hang)/stride_3行=1+1(起始位置行)

6.2多输入和多输出通道

6.2.1多输入通道

这个图很好的解释了多输入通道内核,内核时(c,h,w)三个维度的
在这里插入图片描述

6.2.2多输出通道

多输出通道,不要看他的公式,反而容易被误导,多输出意思会提供不同的卷积核,由于多输入通道数据和多通道卷积核在通道维度上时降维的,所以只需要将不同卷积核降维结果重新在通道上堆叠stack即可。

1*1卷积

其实1*1卷积的唯一计算发生在通道上,只在通道上加和降维。下面这个图很形象,演示了第一个输出通道(浅色)的第一个元素,和第二个输出通道的最后一个元素,输入时三个通道。
在这里插入图片描述

6.3池化

X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4)) 
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1)) #可以指定h和w上的补充和步幅
pool2d = nn.MaxPool2d(3, padding=1, stride=2)#省略情况下h和w一致
pool2d = nn.MaxPool2d(3)#超级省略,h=w=3,零填充,步幅和池化高和宽一致
X = torch.cat((X, X + 1), 1)#X的shape变成(1,2,4,4)
pool2d = nn.MaxPool2d(3, padding=1, stride=2)#池化是在每个输入通道上单独运算,也就是池化层输出和输入的通道数是一样的

6.4 卷积神经网络Lenet

import torch
from torch import nn
from d2l import torch as d2l
#这里输入图片是黑白图片像素为28*28,黑白图像意味着输入通道数为一
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5, padding=2), #输入通道为1,输出通道为6,意味着有6个卷积核,也意味着从这一层输出的数据通道数为6,也就是shape为(batch,6,28,28)
     nn.Sigmoid(),#不改变数据shape (batchsize,6,28,28)
    nn.AvgPool2d(kernel_size=2, stride=2),#输入(batchsize,6,28,28),pool不改变通道数,(28-2(kernel_size)/2(stride)+1(起始位置),所以输出形状为(batchsize,6,14,14)
    nn.Conv2d(6, 16, kernel_size=5), #输入(batchsize,6,14,14),输出通道数16,也就是16个内核,(14-5+1=10,所以输出(batchsize,16,1010)
     nn.Sigmoid(),#不改变数据shape (batchsize,16,1010)
    nn.AvgPool2d(kernel_size=2, stride=2), #输入(batchsize,16,1010),pool不改变通道数量,(10-2/2+1=5,输出(batchsize,6,55)
    nn.Flatten(), #(batchsize,16*5*5=400)
    nn.Linear(16 * 5 * 5, 120), #400输入,120输出,全连接层
    nn.Sigmoid(), #不改变形状
    nn.Linear(120, 84), #输入120,输出84
    nn.Sigmoid(),
    nn.Linear(84, 10)) #输入84,输出10,Lenet用来识别手写数字的
    #检查每层张量输出的形状
def init_weights(m):
  if type(m) == nn.Linear or type(m) == nn.Conv2d:
      nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
#prepare the dataset
trans=[transforms.ToTensor()]
trans=transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
        root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
        root="../data", train=False, transform=trans, download=True)
train_iter=torch.utils.data.DataLoader(
    mnist_train, batch_size, shuffle=True, num_workers=4)
test_iter=torch.utils.data.DataLoader(
    mnist_test, batch_size, shuffle=False, num_workers=4)
#parameters and optimizer
batch_size,lr, num_epochs = 256,0.9, 10
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
#很好的画图工具
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=['train loss', 'train acc', 'test acc'])
metric_train=d2l.Accumulator(3)
metric_test=d2l.Accumulator(2)
for epoch in range(num_epochs):    
    # 训练损失之和,训练准确率之和,样本数 
    net.train()
    for X, y in train_iter:
        optimizer.zero_grad()
        y_hat = net(X)
        l = loss(y_hat, y)#这里是样本batchsize上的平均损失
        l.backward()
        optimizer.step()
        metric_train.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
    with torch.no_grad():
        net.eval()       
        for X, y in train_iter:
            metric_test.add(d2l.accuracy(net(X), y), X.shape[0])                    
    train_l = metric_train[0] / metric_train[2]
    train_acc = metric_train[1] / metric_train[2]
    test_acc = metric_test[0] / metric_test[1]
    animator.add(epoch + 1, (train_l, train_acc, test_acc))

五、现代卷积神经网络

5.1 Alex net

AlexNet是参考Lenet网络,这里是神经网络的起点,指望卷积神经网络自动提取特征值,为了兼容特征值的视野,需要多层卷积,但是多层卷积会导致参数数量多,层数多了以后会导致梯度消失或者梯度爆炸。2012年,AlexNet横空出世。它首次证明了学习到的特征可以超越手工设计的特征。它一举打破了计算机视觉研究的现状,意义重大的网路。

import torch
from torch import nn
from d2l import torch as d2l
import torchvision
from torchvision import transforms
net = nn.Sequential(
    # 这里使用一个11*11的更大窗口来捕捉对象。
    # 同时,步幅为4,以减少输出的高度和宽度。
    # 另外,输出通道的数目远大于LeNet
    nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(), #output shape floor((224-11+2)//4)+1=53+1=54,output(batchsize,96,54,54)
    nn.MaxPool2d(kernel_size=3, stride=2), #floor((54-3)/2)+1=26,output shape(batchsize,96,26,26)
    # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
    nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(), #input channel number is 96,26-5+2*2+1=26,output(batchsize,256,26,26)
    nn.MaxPool2d(kernel_size=3, stride=2),#floor((26-3)/2)+1=12,output(batchsize,256,12,12)
    # 使用三个连续的卷积层和较小的卷积窗口。
    # 除了最后的卷积层,输出通道的数量进一步增加。
    # 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
    nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(), #padding=(kernel_size-1)/2,不改变形状
    nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(), #padding=(kernel_size-1)/2,不改变形状
    nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(), #padding=(kernel_size-1)/2,不改变形状
    nn.MaxPool2d(kernel_size=3, stride=2), #(batchsize,256,floor((12-3)/2)+1=5,5)
    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))
def init_weights(m):
    if type(m) == nn.Linear or type(m) == nn.Conv2d:
        nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
#prepare the dataset
trans=[transforms.Resize(size=(224, 224)) ,transforms.ToTensor()]
trans=transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
        root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
        root="../data", train=False, transform=trans, download=True)
train_iter=torch.utils.data.DataLoader(
    mnist_train, batch_size, shuffle=True, num_workers=4)
test_iter=torch.utils.data.DataLoader(
    mnist_test, batch_size, shuffle=False, num_workers=4)
#parameters and optimizer
batch_size,lr, num_epochs = 256,0.9, 10
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
#很好的画图工具
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=['train loss', 'train acc', 'test acc'])
metric_train=d2l.Accumulator(3)
metric_test=d2l.Accumulator(2)
for epoch in range(num_epochs):    
    # 训练损失之和,训练准确率之和,样本数 
    net.train()
    for X, y in train_iter:
        optimizer.zero_grad()
        y_hat = net(X)
        l = loss(y_hat, y)#这里是样本batchsize上的平均损失
        l.backward()
        optimizer.step()
        metric_train.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
    with torch.no_grad():
        net.eval()       
        for X, y in train_iter:
            metric_test.add(d2l.accuracy(net(X), y), X.shape[0])                    
    train_l = metric_train[0] / metric_train[2]
    train_acc = metric_train[1] / metric_train[2]
    test_acc = metric_test[0] / metric_test[1]
    animator.add(epoch + 1, (train_l, train_acc, test_acc))```c

5.2 VGG网络

VGG网络和AlexNet网络一样靠堆叠卷积层,缺点也是层深后难于训练。

def vgg_block(num_convs, in_channels, out_channels):
    layers = []
    for _ in range(num_convs): #这里卷积层,不过多少层,这里out_channels不变,kernel_size=3, padding=1导致h和w也不变
        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))#注意通过MaxPool,h和W降为一半,但是out_channels不变
    return nn.Sequential(*layers)
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 #上一级的out_channels是下一级in_channels,这里channel和h,w不要搞混

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

5.3 NiN网络

NiN网络相对于前两种网络在数学或者直觉上是有创新的,可以借助于线性代数的概念,NiN最大的创新是引入了11卷积核,11卷积核允许我们对h和w维度上的单点数据(表征与某个卷积核相关量的大小)进行类似全连接网络的对空间进行非线性分割或者分类,这增加了NiN网络的非线性或者增加了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), #这里如果是第一个NiN,那么这就是第一个特征提取的卷积层,这里生成out_channels个特征,也可以理解将图像投影到out_channels为维度里,在out_channels维度里进行分类,并自动求解坐标,这就是神经网络的魅力
        nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(), #这里在h,w维度看的话,到这里h,w都不是像素了,而是特征向量大小的表征,这里是卷积之后的结果,应该是单特征点含量的大小或者相关程度,这里我们借助于通道c维的单点卷积,在通道维度上实现了全连接变换,想想一下在上一次一共有out_channels个卷积核,因为我们期望是在维度为out_channels空间里解决分类问题,每个卷积核能看到kernel_size个像素点,理论上在低纬度空间里混杂的数据,在高纬度里确实是能够被仔细区分的。
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU()) #这里和上一行一样,期望在高维度进行线性变化获得好的分类界限,所以经过nin_block之后的h,w再称为像素就不合适了,甚至是错误的,应该称为系数,高纬度坐标系数。
net = nn.Sequential(
#先用96个kernel_size=11的卷积核,不同的卷积核会提取图像的不同的特征,同时我们也可以认为这些不同的特征是相互独立的不同维度,对像素进行求相关或者求解特征,也相当于将一小块图像(kernel_size)投影到一个96维度空间里,再这个96空间里应用全连接网络进行分类
    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维空间,也就是我们最终数据集的种类,NiN在直觉上是非常合理的,提出NiN的人很牛逼。
    # 将四维的输出转成二维的输出,其形状为(批量大小,10)
    nn.Flatten())

5.4 GoogleNET

看了NiN相对于ALexNet解决的问题是利用11卷积核增加了卷积核之间的信息组合或者卷积核之间的分类信息提取, AlexNet没有单11卷积核,NiN展现了11卷积核的必要性,NiN是11卷积核和普通卷积核交替运算,GoogleNet增加了11卷积核和普通卷积核同时进行运行的能力,增加了数据的自由度,理论上应该是网络分类能力会有增强。
在这里插入图片描述
如上图所示,最左边是NiN网络的1
1卷积核经典路径,左边第二路径是11卷积核之后串联33卷积核,其实深入详细理解每个卷积核在分类中的作用是很难的,原因可能是因为我们生活着3维空间中,对高纬度空间只能利用相关值大小理解不同维度的关系,比如相关值为零,那么这两个向量是相互垂直的。所以11卷积核之后33卷积核具体是什么作用?应该都是试出来的,没有闭环理论公式进行推导。直觉理解的话就是11卷积层是不同通道不同卷积核的相关量的线性组合,33又是在11卷积核输出结果之上进行特征提取,相当于左边是输出是原始数据,那么左2和左3是特征提取后结果,右边是相当于对最大值进行跨卷积核或者在通道上进行特征提取,GoogleNet的创新是通道合并层,4条路经的数据在通道层串联成一个打输出数据,信息更加丰富。我的个人一个感受,如果能让信息自由流动,往往会使神经网路更强大。这里总结一下11卷积是空间划分,其他卷积是特征提取(构建新的坐标系)。

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)#这里卷积核的尺寸是(in_channels,h,w),一共有c1个卷积核,也就是输出通道数是c1
        # 线路21x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)#输出通道c2[0]
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)#最终输出通道数或者说卷积核数c2[1]
        # 线路31x1卷积层后接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)
        # 线路43x3最大汇聚层后接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))#第一层普通卷积层,64个卷积核,输出通道数64(batchsize, out_channels,h,w)
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)) #kernel_size,stride,padding都是在h和w维度上。
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),#输入通道数192,对照Inception网络,最左边1*1卷积输出64通道,左二是两级卷积,第一级1*1输出96通道,第二级3*3128通道也就是最终128通道,其他类似。
                   Inception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))#h,w降维度
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())
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))               

5.5批量规范换

本节将介绍批量规范化(batch normalization),这是一种流行且有效的技术,可持续加速深层网络的收敛速度。
批量规范化应用于单个可选层(也可以应用到所有层),其原理如下:在每次训练迭代中,我们首先规范化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。 接下来,我们应用比例系数和比例偏移。 正是由于这个基于批量统计的标准化,才有了批量规范化的名称。从形式上来说,用 x ∈ B \mathbf{x} \in \mathcal{B} xB表示一个来自小批量 B \mathcal{B} B的输入,批量规范化 B N \mathrm{BN} BN根据以下表达式转换 x \mathbf{x} x

B N ( x ) = γ ⊙ x − μ ^ B σ ^ B + β . \mathrm{BN}(\mathbf{x}) = \boldsymbol{\gamma} \odot \frac{\mathbf{x} - \hat{\boldsymbol{\mu}}_\mathcal{B}}{\hat{\boldsymbol{\sigma}}_\mathcal{B}} + \boldsymbol{\beta}. BN(x)=γσ^Bxμ^B+β.
这里发明批量标准化的也是一个天才,尤其引入两个系数 拉伸参数(scale) γ \boldsymbol{\gamma} γ和偏移参数(shift) β \boldsymbol{\beta} β让神经网络去学习,另外,批量规范化层在”训练模式“(通过小批量统计数据规范化)和“预测模式”(通过数据集统计规范化)中的功能不同。 在训练过程中,我们无法得知使用整个数据集来估计平均值和方差,所以只能根据每个小批次的平均值和方差不断训练模型。 而在预测模式下,可以根据整个数据集精确计算批量规范化所需的平均值和方差。
这里重点讲解卷积层的批量规范化,对于卷积层,我们可以在卷积层之后和非线性激活函数之前应用批量规范化。当卷积有多个输出通道时,我们需要对这些通道的“每个”输出执行批量规范化,每个通道都有自己的拉伸参数(scale) γ \boldsymbol{\gamma} γ和偏移参数(shift) β \boldsymbol{\beta} β,这两个参数都是标量。
假设我们的小批量包含 m m m个样本,并且对于每个通道,卷积的输出具有高度 p p p和宽度 q q q
那么对于卷积层,我们在每个输出通道 m ⋅ p ⋅ q m \cdot p \cdot q mpq个元素上同时执行每个批量规范化。
因此,在计算平均值和方差时,我们会收集所有空间位置的值,然后在给定通道内应用相同的均值和方差,以便在每个空间位置对值进行规范化。正如我们前面提到的,批量规范化在训练模式和预测模式下的行为通常不同这里对应net.train()和net.eval()。首先,将训练好的模型用于预测时,我们不再需要样本均值中的噪声以及在微批次上估计每个小批次产生的样本方差了。其次,例如,我们可能需要使用我们的模型对逐个样本进行预测。一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。可见,和暂退法一样,批量规范化层在训练模式和预测模式下的计算结果也是不一样的。
这里有个简单代码,只考虑全连接层和卷积层

import torch
from torch import nn
from d2l import torch as d2l
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
    # 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
    if not torch.is_grad_enabled():#区分net.train()和net.eval()
        # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        assert len(X.shape) in (2, 4)#判断是全连接层(batchsize,feature),还是卷积层(cin,cout,h,w)
        if len(X.shape) == 2:
            # 使用全连接层的情况,计算特征维上的均值和方差
            mean = X.mean(dim=0)
            var = ((X - mean) ** 2).mean(dim=0)#全连接层均值和var计算
        else:
            # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
            # 这里我们需要保持X的形状以便后面可以做广播运算
            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)
        # 更新移动平均的均值和方差
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
    Y = gamma * X_hat + beta  # 缩放和移位,这两个参数将通过nn.Parameters进行定义,并在训练时被更新。
    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)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成10,并在训练过程中被更新。
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 非模型参数的变量初始化为01
        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

很喜欢作者个这段话“虽然深度神经网络的概念非常简单——将神经网络堆叠在一起。但由于不同的网络架构和超参数选择,这些神经网络的性能会发生很大变化。 本章介绍的神经网络是将人类直觉和相关数学见解结合后,经过大量研究试错后的结晶。”AI有时候都是在靠直觉设计架构,怎么让数据更自由怎么少丢信息怎么样减少训练量,这真是一个门槛低,天花板极高的行业。

5.7 残差网络

在这里插入图片描述
作者放这个图的意思,当我们往新架构中添加层,期望是神经网络比添加之前更强大,那么就必须保证原有数据的路径不变,或者新添加的层存在某组权重或系数,能使添加新层变成恒等网络,也就是只要我们愿意这个新加层的神经网络可以和老网络一模一样。对于深度神经网络,如果我们能将新添加的层训练成恒等映射(identity function) f ( x ) = x f(\mathbf{x}) = \mathbf{x} f(x)=x,新模型和原模型将等效。一句话,扩层要增加函数映射功能,不能减少网络的变换功能。**残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。**新加的层不能减少原始网络的变换能力。
在这里插入图片描述
这个图理解一,如果f(x)是真实的目标函数,那么右图虚框只要拟合出f(x)-x也可以最终拟合出f(x),-x只是一个线性变化。理解二,这个层的加入完全不会限制之前神经网络的能力,因为x可以直接旁路我们新加的层,这是残差网络的核心,新加的层不能改变原有神经网络的变换能力。第三,反向误差传播时,x路径提供了以更加准确的梯度,不会梯度消失。
每个残差网络内部如下:
在这里插入图片描述
这里讲解代码

def resnet_block(input_channels, num_channels, num_residuals,
                 first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:#第一个残差网络
            blk.append(Residual(input_channels, num_channels,
                                use_1x1conv=True, strides=2))#后续第二个残差网络起,strides=2,代表h,w减半
        else:
            blk.append(Residual(num_channels, num_channels)) #第一个残差网络,h,w分辨率不变
    return blk

总结

这是我的读书笔记,潦草不成体系,如果要学习,还是要参考原著。


网站公告

今日签到

点亮在社区的每一天
去签到