pytorch-autograd-optimizer-nn-DataSet-DataLoader-Save-Load等基本组件

1 Autograd

pyTorch tensor记录着它们自己的由来,即由哪些父tensor和哪些操作生成了该tensor,因此可以通过链式法则自动推导输出相对于输入的梯度。

在tensor的构造函数中,写明requires_grad=True或者使用requires_grad_函数可以设置记录该tensor的梯度。

1.1 叶结点才有梯度

pyTorch默认只对叶节点记录梯度,对于非叶节点默认是不保存其梯度的,如果想获取非叶节点的梯度,可以使用register_backward_hook或者retain_graph实现,更加推荐前者,因为后者对于显存的占用会增加。

pytorch的autograd是在叶子结点上累加梯度,所以训练过程中需要每次都将梯度置零,可以使用optimizer.zero_grad()实现。

def train_loop_autograd(n_epoches,learning_rate,params,t_u,t_c):
    for epoch in range(n_epoches):
        if params.grad is not None:
            params.grad.zero_()
        
        t_p = model(t_u,*params)
        loss = mse(t_p,t_c)
        loss.backward()
        
        with torch.no_grad():
            params -= learning_rate * params.grad
            print(params.is_leaf) #True
            print(params.requires_grad) #True
        
        print('Epoch %d,Loss %f' %(epoch,loss))
        
    return params

params = torch.tensor([1.0,0.0],requires_grad = True)
params = train_loop_autograd(5000,1e-2,params,t_un,t_c)
params

但如果将代码修改为如下所示,结果不一致,原因是在torch.no_grad()作用域中,新生成的params对象不计算梯度,而上面那种写法是对原始params对象的修改,并未生成新的params对象。

 with torch.no_grad():
            params = params - learning_rate * params.grad
            print(params.is_leaf) #True
            print(params.requires_grad) #False

验证如下:
在这里插入图片描述
在这里插入图片描述

1.2 optimizer

optimizer为优化器,拥有两个函数,分别为zero_grad()和step(),zero_grad()函数在一次迭代过程中将梯度清零,step()函数进行一次参数更新。

optimizer的构造函数中的第一个参数为模型的参数,如optim.SGD([params],lr=learning_rate);

import torch.optim as optim

def train_loop_autograd_optim(n_epoches,optimizer,t_u,t_c):
    for epoch in range(n_epoches):
        optimizer.zero_grad()
        
        t_p = model(t_u,*params)
        loss = mse(t_p,t_c)
        loss.backward()
        
        optimizer.step()
        
        if epoch % 500 == 0:
            print('Epoch %d,Loss %f' %(epoch,loss))
        
    return params

learning_rate = 1e-2
params = torch.tensor([1.0,0.0],requires_grad = True)
optimizer = optim.SGD([params],lr = learning_rate)
params = train_loop_autograd_optim(5000,optimizer,t_un,t_c)
params

1.3 torch.no_grad()

在进行模型推理的时候,如计算验证集上模型的效果,可以使用:

def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u,train_t_c, val_t_c):
	for epoch in range(1, n_epochs + 1):
		train_t_p = model(train_t_u, *params)
		train_loss = loss_fn(train_t_p, train_t_c)
        
	with torch.no_grad():#对于验证集,不将其梯度累加到叶子结点上
		val_t_p = model(val_t_u, *params)
		val_loss = loss_fn(val_t_p, val_t_c)
		assert val_loss.requires_grad == False
        
    optimizer.zero_grad()
    train_loss.backward()
    optimizer.step()

目的是让推理过程中不记录梯度信息,避免基于验证集的梯度在叶子结点上累加,同时也可以减少显存占用、有效加速推理过程。

1.3.1 torch.no_grad()和model.eval()

torch.no_grad()是在计算过程中不进行梯度计算;

model.eval()是改变模型的forward过程,例如使dropout失效,BN计算过程中使用全部训练样本的统计值。

在模型的推理过程中,这两个函数都需要。

在很多情况下,模型推理过程中,因为没有执行optimizer.step()函数,即便不使用with torch.no_grad()计算的梯度也不会修改原有的模型参数。但额外添加with torch.no_grad()之后,在推理过程中就不进行梯度计算了,减少了显存占用。

1.4 torch.set_grad_enabled

def calc_forward(t_u, t_c, is_train):
    with torch.set_grad_enabled(is_train):
        t_p = model(t_u, *params)
        loss = loss_fn(t_p, t_c)
return loss 

torch.set_grad_enabled函数接收一个bool变量,决定是否进行梯度的计算,可以如上面所示依据bool变量来决定计算损失的过程中是否保存梯度信息。

>>> x = torch.tensor([1], requires_grad=True)
>>> is_train = False
>>> with torch.set_grad_enabled(is_train):
...   y = x * 2
>>> y.requires_grad
False

>>> torch.set_grad_enabled(True)
>>> y = x * 2
>>> y.requires_grad
True

>>> torch.set_grad_enabled(False)
>>> y = x * 2
>>> y.requires_grad
False

2 torch.nn

torch.nn包含了构建神经网络所需的基础块,在pyTorch中称这些基础块为module。这些module都继承自nn.Module。

继承自nn.module的module,需要实现forward函数,在该函数内部执行具体的前向计算过程。但是在执行前向运算时,调用方式为:

import torch.nn as nn

class Model(nn.Module):
	def init(self):
        pass

	def forward(self,x):
        pass
        
 model = Model()
 y = model(x) #正确写法
 y = model.forward(x) #错误写法

这是因为正确写法中,是调用了类的__call__函数,该函数中除了调用forward函数外,还执行了很多的hook操作,因此只调用forward函数难以得到正确的结果。

pyTorch的nn中包含的内容很多,如各网络块、损失函数、激活函数等,具体可见https://pytorch.org/docs/stable/nn.html#。

2.1 torch.nn中modules(),named_modules(),named_children(),parameters(),named_parameters()的区别

modules()函数返回一个model中各个模块;

named_modules()函数返回一个model中各个模块的名称及模块;

named_children()函数返回一个model中各个子model的名称及模块。

modules和named_modules的区别在于是否返回module的名称;

named_modules和named_children的区别在于前者将所有的子model中的各module全部返回,而named_children的返回结果只到子model这一级,没有进一步的返回各子model的具体module信息。

class TestModule(nn.Module):
    def __init__(self):
        super(TestModule,self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(16,32,3,1),
            nn.ReLU(inplace=True)
        )
        self.layer2 = nn.Sequential(
            nn.Linear(32,10)
        )
 
    def forward(self,x):
        x = self.layer1(x)
        x = self.layer2(x)
 
modelTest = TestModule()

for module in modelTest.modules():
    print(module)
    
print('first')
print()

for name,module in modelTest.named_modules():
    print(name,module)
print('second')
print()

for name,module in modelTest.named_children():
    print(name,module)

输出结果为:

modules() output:
TestModule(
  (layer1): Sequential(
    (0): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
    (1): ReLU(inplace=True)
  )
  (layer2): Sequential(
    (0): Linear(in_features=32, out_features=10, bias=True)
  )
)
Sequential(
  (0): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
  (1): ReLU(inplace=True)
)
Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
ReLU(inplace=True)
Sequential(
  (0): Linear(in_features=32, out_features=10, bias=True)
)
Linear(in_features=32, out_features=10, bias=True)

named_modules() output:
 TestModule(
  (layer1): Sequential(
    (0): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
    (1): ReLU(inplace=True)
  )
  (layer2): Sequential(
    (0): Linear(in_features=32, out_features=10, bias=True)
  )
)
layer1 Sequential(
  (0): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
  (1): ReLU(inplace=True)
)
layer1.0 Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
layer1.1 ReLU(inplace=True)
layer2 Sequential(
  (0): Linear(in_features=32, out_features=10, bias=True)
)
layer2.0 Linear(in_features=32, out_features=10, bias=True)

named_children() output:
layer1 Sequential(
  (0): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
  (1): ReLU(inplace=True)
)
layer2 Sequential(
  (0): Linear(in_features=32, out_features=10, bias=True)
)

parameters() 函数返回一个模型中所有的参数;

named_parameters()函数一个模型中所有的参数名称及参数对象。

seq_model = nn.Sequential(nn.Linear(1,13),
                          nn.Tanh(),
                          nn.Linear(13,1))

for param in seq_model.parameters():
    print(param)

for name,param in seq_model.named_parameters():
    print(name,param)

输出为:

parameters() output:
Parameter containing:
tensor([[-0.6324],
        [-0.6462],
        [-0.9564],
        [ 0.0991],
        [ 0.9941],
        [-0.7498],
        [-0.2111],
        [-0.0443],
        [-0.7378],
        [-0.8962],
        [ 0.9895],
        [-0.7990],
        [ 0.5856]], requires_grad=True)
Parameter containing:
tensor([ 0.4142, -0.8458,  0.0211,  0.0369, -0.9709,  0.4102,  0.3356, -0.2964,
        -0.0479, -0.4294,  0.5947,  0.1506,  0.5007], requires_grad=True)
Parameter containing:
tensor([[-0.0944,  0.2512, -0.0821,  0.1664, -0.0974, -0.2670,  0.0204,  0.0029,
         -0.1344, -0.1795,  0.0282,  0.0098, -0.1224]], requires_grad=True)
Parameter containing:
tensor([0.0348], requires_grad=True)

named_parameters() output:
0.weight Parameter containing:
tensor([[-0.6324],
        [-0.6462],
        [-0.9564],
        [ 0.0991],
        [ 0.9941],
        [-0.7498],
        [-0.2111],
        [-0.0443],
        [-0.7378],
        [-0.8962],
        [ 0.9895],
        [-0.7990],
        [ 0.5856]], requires_grad=True)
0.bias Parameter containing:
tensor([ 0.4142, -0.8458,  0.0211,  0.0369, -0.9709,  0.4102,  0.3356, -0.2964,
        -0.0479, -0.4294,  0.5947,  0.1506,  0.5007], requires_grad=True)
2.weight Parameter containing:
tensor([[-0.0944,  0.2512, -0.0821,  0.1664, -0.0974, -0.2670,  0.0204,  0.0029,
         -0.1344, -0.1795,  0.0282,  0.0098, -0.1224]], requires_grad=True)
2.bias Parameter containing:
tensor([0.0348], requires_grad=True)

3 Dataset

torch.utils.data.Dataset是所有数据集的基类,其具有两个关键函数,__len__用于返回整个数据集的样本数量,__getitem__接受一个索引参数,返回数据集中该索引对应的样本。

具有__len__函数的类,可以传递给python内置的len函数,用于返回数据集的样本数量。同样,由于该类别具有__getitem__成员函数,可以通过标准的索引获取索引对应的样本,如img = CIFAR[99].

from torchvision import datasets
cifar10 = datasets.CIFAR10(data_path, train=True, download=True)
img,label = cifar10[99]

自定义的Dataset需要继承自torch.utils.data.Dataset,且需实现__len__和__getitem__函数。__len__函数中返回数据集的样本数量,__getitem__函数按照输入索引返回对应的数据。

在DataSet的构造函数中还可以指定transforms,即对加载的图像进行预处理。

4 torchvision.transfroms

torchvison.transfroms用于对包含输入图像的tensor进行图像变换,如果想组合多个变换,可以使用transforms.Compose函数,该函数以一个包含transforms对象的列表为输入。如:

transforms.Compose([transforms.CenterCrop(10),transforms.ToTensor()])

transfroms.ToTensor()函数将PIL Image对象或numpy.ndarray转换称torch.tensor。如果输入数据为PIL Image对象,要求其格式为 (L, LA, P, I, F, RGB, YCbCr, RGBA, CMYK, 1)之一。如果输入为numpy.ndarray,要求其数据类型为np.uint8。输入数据的取值范围为[0,255],数据按照(H,W,C)进行排列。输出为取值范围为[0.,1.]的torchtensor,且按照(C,H,W)的方式进行排列的torch.FloatTensor。

transforms.Normalize(means,stds)按照channel对输入数据进行规范化处理,output[channel] = (input[channel] - mean[channel]) / std[channel].

5 nn.NLLLoss() 和 nn.CrossEntropyLoss()

nn.NLLLoss()即实现NLL = - sum(log(out_i[c_i])),sum操作是针对训练batch中的所有样本,c_i是第i个样本的正确类别。NLLLoss以概率值的对数为输入,也就是以nn.LogSoftmax()的输出为输入。之所以以概率的对数为输入是因为在概率很小时,概率的对数是一个很小的负值。

nn.LogSoftmax()和nn.NLLLoss()是一组。nn.CrossEntropyLoss()等价于nn.LogSoftmax()+nn.NLLLoss()。使用nn.CrossEntropyLoss()时,模型的输出可以认为是未归一化的对数概率,需要使用nn.Softmax()将其输出转换为概率值。

6 DataLoader

torch.utils.data.DataLoader用于对一个数据集进行加载,可以设定按照固定的batchsize进行加载,并且可以在每个epoch开始前打乱训练样本的顺序。

train_loader = torch.data.utils.DataLoader(cifar10,batch_size=64,shuffle=True)

7 全连接网络不具有平移不变性

全连接层,一个输入对应一个权重,目标在图像中发生平移后,和目标相乘的对应权重发生了变化,得到的计算结果也就不同了。因此,全连接层不具有平移不变性。另外,全连接层由于神经元之间全部存在连接,易于发生过拟合。

而卷积层,由于权值共享,不同的图像位置应用同一个卷积核,图像平移后和其计算的卷积核仍然保持不变,因此具有平移不变性。

池化层的作用是减少激活值的空间尺寸,有助于增大后面层的感受野,便于提取更加全局的特征。

class ResBlock(nn.Module):
def init(self, n_chans):
super(ResBlock, self).init()
self.conv = nn.Conv2d(n_chans, n_chans, kernel_size=3,
padding=1, bias=False)
self.batch_norm = nn.BatchNorm2d(num_features=n_chans)
torch.nn.init.kaiming_normal_(self.conv.weight,nonlinearity=‘relu’)
torch.nn.init.constant_(self.batch_norm.weight, 0.5)
torch.nn.init.zeros_(self.batch_norm.bias)

def forward(self, x):
    out = self.conv(x)
    out = self.batch_norm(out)
    out = torch.relu(out)
    return out + x

class NetResDeep(nn.Module):
def init(self, n_chans1=32, n_blocks=10):
super().init()
self.n_chans1 = n_chans1
self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
self.resblocks = nn.Sequential(*(n_blocks * [ResBlock(n_chans=n_chans1)]))
self.fc1 = nn.Linear(8 * 8 * n_chans1, 32)
self.fc2 = nn.Linear(32, 2)

def forward(self, x):
    out = F.max_pool2d(torch.relu(self.conv1(x)), 2)
    out = self.resblocks(out)
    out = F.max_pool2d(out, 2)
    out = out.view(-1, 8 * 8 * self.n_chans1)
    out = torch.relu(self.fc1(out))
    out = self.fc2(out)
    return out

8 卷积层

卷积层也是线性操作,和全连接层不同的是,不再是所有的输入像素都对应一个权重系数,而只是在输出像素所在位置的一个小领域内权重系数不为0,其余位置处的权重系数全部为0.

将全连接层替换为卷积层,带来的好处有:

  • 局部连接:卷积核的尺寸一般远小于输入图像的尺寸,所以每次卷积过程中卷积核只和图像的局部块相连接;
  • 参数共享:卷积核在图像上滑动进行卷积运算,因此目标在图像中平移后可以得到相同的输出,使得网络具有了平移不变性。同样,前向计算过程中,输入图像的所有位置都和卷积核进行了运算,那么反向计算卷积核梯度的时候也需要计算整个输入图像对于卷积核梯度的影响;
  • 参数量较少,发生过拟合的风险变小。卷积层的卷积计算参数量为C{out} \times C_{in} \times K \times K,C_{out}表示输出feature map的channel数,C_{in}表示输入feature map的channel数,K表示卷积核的尺寸。卷积层的bias数量为输出的channel数,即一个输出channel加一个常量值。因此卷积层的参数量和输入图像的大小无关,但一般卷积核的尺寸远小于输入图像的尺寸,因此相比于全连接层,卷积层的参数量大幅度减小,因此过拟合的风险也在减小。

卷积层虽然是二维卷积,但在输入图像为多通道时,卷积核的通道数等于输入feature map的通道数,卷积核的输出channel数表示不同类别的特征数,即卷积层的输出feature map的每个channel表示应用一个滤波器核从输入feature map中提取的特征。

pyTorch中nn.Conv提供卷积运算,nn.Conv1d针对时间序列的输入,nn.Conv2d针对图像输入,nn.Conv3d针对视频输入。

卷积操作的输出尺寸为 O = (I - K + 2P)/S + 1。

9 池化层

池化操作分为最大池化和平均池化两种,最常用的是最大池化。其作用是保留卷积结果中的最大响应,忽略其余的较弱的响应。

一般网络堆叠的过程为卷积层+激活函数+池化层+卷积层+…,这样第二个卷积层的输入为第一个池化层的输出,因为经过池化操作之后,feature map的空间分辨率减小,那么第二个卷积层中每一个卷积核在原始输入图像上的感受野的尺寸在增加,也就是对应于一个更大邻域的图像块,实现了从图像中提取更加抽象的特征。

nn.MaxPool2d()实现最大池化操作,nn.AvgPool2d实现平均池化操作。

10 继承自nn.Module实现自定义网络

nn.Module为所有神经网络module的基类,自定义的网络结构需要继承自nn.Module。

自定义的网络需要实现__init__和forward两个函数,__init__函数中定义网络的结构,即各网络层;在forward函数中定义网络的前向运算,网络的反向传播由autograd进行自动实现,无需人为干预。

在__init__函数中首先要调用super().init();

继承自nn.Module的自定义网络的子module必须是top-level级别,不能把这些子module放到list或dict对象中,如果必须要把子module放入队列中,应该使用nn.ModuleList和nn.ModuleDict,否则优化器无法定位到这些子module及其参数,也就无法对其进行优化处理。

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3,128,3,1,1)
        self.act1 = nn.Tanh()
        self.pool1 = nn.MaxPool2d(2)
        self.conv2 = nn.Conv2d(128,32,3,1,1)
        self.act2 = nn.Tanh()
        self.pool2 = nn.MaxPool2d(2)
        self.conv3 = nn.Conv2d(32,8,3,1,1)
        self.act3 = nn.Tanh()
        self.fc1 = nn.Linear(8*8*8,32)
        self.act4 = nn.Tanh()
        self.fc2 = nn.Linear(32,2)
        
    def forward(self,x):
        x1 = self.pool1(self.act1(self.conv1(x)))
        x2 = self.pool2(self.act2(self.conv2(x1)))
        x3 = self.act3(self.conv3(x2))
        out = self.fc2(self.act4(self.fc1(x3.view(x3.shape[0],-1))))
        return out

model = Net()

train_loader = torch.utils.data.DataLoader(cifar2,batch_size=64,shuffle=True)

learning_rate = 1e-1

optimizer = optim.SGD(model.parameters(),lr = learning_rate)

loss_fn = nn.CrossEntropyLoss()

for epoch in range(100):
    correct_count = 0
    total = 0
    loss_sum = 0.
    for imgs,labels in train_loader:
        pred = model(imgs)
        _,pred_label = torch.max(pred,dim=1)
        correct_count += int((pred_label == labels).sum())
        total += imgs.shape[0]
        
        loss = loss_fn(pred,labels)
        
        loss_sum += loss.item()
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
    if epoch % 10 == 0:
        print('epoch %d,loss %f,accuracy %f' %(epoch,loss_sum / len(train_loader),float(correct_count)/total)))

注意下,len(train_loader)返回的是训练集中一共有多少个batch,这里loss_sum / len(train_loader)得到的是每一个batch的平均损失;len(cifar2)等于total,才是所有的样本数量。计算平均损失和正确率的时候不要把这两者搞混了。

len(DataSet)返回的是数据集中总的样本数量,len(DataLoader)返回的是数据集中共含有多少个batch的数据,这两个不要搞混了。

11 torch.nn.functional

在上面的Net类的定义中,我们把自定义的子module,如conv1,conv2等在构造函数中赋给了类的成员,目的是为了让优化器能够获取它们的参数进行优化。但是,nn.Tanh()和nn.MaxPool2d()是没有可以训练的参数的,那么就可以使用torch.nn.functional提供的函数进行处理,而不需要使用torch.nn的子module.

torch.nn的每一个子module都有一个对应的torch.nn.functional提供的函数,区别在于torch.nn的子module使用内部存储的参数处理输入数据,而torch.nn.functional提供的函数则是需要人为设定输入数据和参数。如nn.Linear层对应于nn.functional.linear函数,函数声明为torch.nn.functional.linear(input, weight, bias=None),需要提供的参数有:输入数据、权重矩阵w,偏差项b。

网络中,对于有训练参数的层,使用torch.nn的子module比较合适,方便在训练过程中由model管理其参数。但是对于无需训练的层,推荐使用torch.nn.functional提供的函数。

12 卷积网络的训练过程

两个循环,外层循环表示第几个epoch,内层循环对应从dataloader中取出的一个batch的数据。

for epoch in range(n_epochs):
	for imgs,labels in trainloader:
		pred = model(x) #前向运算得到预测结果
		loss = loss_fn(pred,labels) #计算损失
		
        optimizer.zero_grad() #将原有梯度置零
        loss.backward() #反向传播计算梯度
        optimizer.step() #进行一次参数更新

13 预测准确率统计

val_loader = torch.utils.data.DataLoader(cifar2_val,batch_size=64,shuffle=False)

total = 0
correct_count = 0

with torch.no_grad():
    for imgs,labels in val_loader:
        preds = model(imgs)
        _,preds = torch.max(preds,dim=1)
        correct_count += torch.sum(preds == labels)
        total += imgs.shape[0]

print('correct count:%d,accuracy %f'%(correct_count,float(correct_count)/len(total)))

14 模型保存和加载

pyTorch模型的文件后缀一般为.pt。

14.1 只保存模型参数和缓存值

torch.save(model.state_dict(),'model.pt') #保存模型

使用state_dict只保存了模型的参数和缓存值,并没有保存网络结构。缓存值如BN中的\gamma和\beta值等。

因此,在加载保存的模型时,需要首先创建网络结构:

model = Net() #创建网络结构
model.load_state_dict(torch.load('model.pt')) #加载模型

14.2 保存完整网络和参数

如果想要同时保存网络结构和参数,那么在调用save函数的时候,不使用model.state_dict()函数,而直接使用model本身即可。加载的时候使用torch.load函数。

torch.save(model,'model.pt') #保存完整网络和模型参数
model = torch.load('model.pt') #加载网络和模型参数,无需先创建网络

The 1.6 release of PyTorch switched torch.save to use a new zipfile-based file format. torch.load still retains the ability to load files in the old format. If for any reason you want torch.save to use the old format, pass the kwarg _use_new_zipfile_serialization=False.

模型保存时有一个细节需要注意下,模型放在哪个设备上(cpu/gpu),默认就保存在那个设备上。可以如下面所示:

model = Net() #创建网络结构
model.load_state_dict(torch.load('model.pt',map_location=device)) #加载模型到指定device

15 在GPU上训练

module.to函数是in-place操作,Tensor.to则是out-place操作,返回了一个新的tensor对象。

一个比较好的实现习惯是,首先将model放到指定的设备上,然后再去创建optimizer对象,将model的参数绑定到optimizer进行优化。如果先cpu上创建了model,接着将其参数绑定到了optimizer,然后又将model移动到了GPU上的话,绑定到optimizer上的参数由于model位置的移动并没有跟着移动,会造成模型无法优化。

device = (torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')) #确定可用设备

model.to(device) #模型放到device上

训练过程中:

imgs = imgs.to(device) #训练数据放到device上

labels = labels.to(device) #label放到device上

16 神经网络设计

width:网络层的神经元数量或者卷积层的输出channel数;

增加网络的width之后,参数数量会增加,那么模型对于输入数据的变化表示能力也会增加,这样就有可能学习到输入数据中非共性的特征,就增加了过拟合发生的风险。

16.1 防止过拟合的手段

16.1.1 正则化

正则化目的是惩罚过大的权重系数。

常用的正则化有L_2和L_1正则化。L_2正则化倾向于使网络权重值比较均匀,L_1正则化倾向于使网络权重值比较稀疏。

L_2正则化也成为权重衰减,其有一个超参数,控制衰减的比例,这个超参数就叫做weight_decay。因为添加了L_2正则化项后,权重更新时相对于未添加正则化项时,额外减了一个正比于当前权重值的附加项,相当于对当前权重进行了成比例的衰减,这也是权重衰减这一名字的由来。

loss = loss_fn(outputs, labels)
l2_lambda = 0.001
l2_norm = sum(p.pow(2.0).sum() for p in model.parameters()) #如果是L1正则化,则用p.abs()代替p.pow(2.0)
loss = loss + l2_lambda * l2_norm #L2正则化的实现,在原有损失的基础上添加了当前模型所有参数的2范数

pyTorch在Optimizer的实现中默认支持权重衰减,如:

torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False)

如果设置weight_decay不为0,那么训练过程中optimizer优化时会自动进行和上面代码一样的过程,也就是无需我们干预自动实现了权重衰减。

16.1.2 DropOut

dropout是在每一次迭代过程中,随机的设置某些神经元的输出为0,避免所有的神经元联合学习其实是记住了训练样本发生过拟合。另一个理解dropout的思路是,使用dropout后每次模型产生的feature map都不相同,相当于通过网络进行了数据增广的操作。

pyTorch中提供了nn.Dropout2d()和nn.Dropout3d(),需要设置一个参数,即设置多少比例的神经元的输出被设置为0,即被drop掉的神经元的比例。

dropout只应该在模型训练过程中其作用,在模型推理时,不应该使用dropout。通过设置model.train(),dropout过程生效;设置model.eval(),dropout和下面介绍的BN操作都不生效。

16.1.3 Batch Normalization

BN有三个好处:

  • 可以使用更大的学习率进行模型训练;
  • 减少模型参数初始化的影响;
  • 起到了一定的正则化作用,防止过拟合。

BN的原理是对送入激活函数的输入进行重缩放操作,回忆下激活函数的形状,BN通过重缩放调整送入激活函数的数据的分布,使其主要分布在对变化敏感的区域,防止发生梯度弥散减慢模型训练。

pytorch中通过nn.BatchNorm1Dnn.BatchNorm2Dnn.BatchNorm3D实现BN,具体使用哪个函数取决于输入数据的维度。

因为BN是对送入激活函数的输入进行变换,所以BN往往用在全连接层/卷积层之后,激活函数之前。

BN的函数形式、计算原理和使用细节:

torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

y = \gamma \frac{x - E[x]}{\sqrt{var[x] + \epsilon}} + \beta

均值和方差是在训练输入的mini-batch上统计出来的。\gamma和\beta是要学习的超参数,size等于C(输入数据的channel数)。默认情况下,\gamma初始化为1,\beta初始化为0。

在训练过程中,每一层的网络采用加权平均的方式持续记录每一个mini-batch的均值和方差,最终记录下的均值和方差要在模型推理的过程中使用。具体加权平均公式为\hat {x_new} = (1 - momentum) * \hat x + momentum * x_t,其中\hat x为统计结果,x_t为新观测到的值。当然,如果设置track_running_stats = False,那么就不会在训练过程中持续记录均值和方差,在推理过程中也只会使用当前batch的均值和方差。

num_features要和前一层的输出channel数一致,即取当前层输入数据(N,C,H,W)的C值。即在(N,H,W)维度上计算均值,所以也称BN为spatial BN。

训练过程中使用BN,要设置model.train();推理过程中使用BN,要设置model.eval(),设置后就不会对推理过程中当前batch的均值和方差进行累计加权平均,而是采用训练过程中已固化的累计的均值和方差。

由于BN的操作会进行减均值的操作,所以BN前面的层可以设置其bias = False。

16.2 增加模型深度

增加网络的层数之后,模型的深度就会增加。增加深度相当于对输入进行了更长的序列的处理。

16.2.1 skip connection

网络层数增多后,由于梯度运算时的链式法则是连乘操作,如果网络后面层的梯度都很小,那么传递到网络前面层的梯度有可能会太小以致于其训练过程中无法收敛,这种情况称之为梯度弥散。

ResNet提出了采用skip connection解决梯度弥散问题,从而可以训练上百甚至上千层的网络。skip connection即在某些网络层中添加直连通道。

添加skip connection之后,梯度可以通过直连通道反馈到网络的前面层,有助于减轻梯度弥散。

在pyTorch中构建特别深的网络,可以先设计一个基础的网络块,然后迭代堆叠多个网络块。

class ResBlock(nn.Module):
	def __init__(self, n_chans):
		super(ResBlock, self).__init__()
		self.conv = nn.Conv2d(n_chans, n_chans, kernel_size=3,
					padding=1, bias=False)
		self.batch_norm = nn.BatchNorm2d(num_features=n_chans)
		torch.nn.init.kaiming_normal_(self.conv.weight,nonlinearity='relu')
         torch.nn.init.constant_(self.batch_norm.weight, 0.5)
         torch.nn.init.zeros_(self.batch_norm.bias)
	
	def forward(self, x):
        out = self.conv(x)
        out = self.batch_norm(out)
        out = torch.relu(out)
        return out + x
    
class NetResDeep(nn.Module):
	def __init__(self, n_chans1=32, n_blocks=10):
		super().__init__()
		self.n_chans1 = n_chans1
		self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
		self.resblocks = nn.Sequential(*(n_blocks * [ResBlock(n_chans=n_chans1)]))
		self.fc1 = nn.Linear(8 * 8 * n_chans1, 32)
		self.fc2 = nn.Linear(32, 2)
        
    def forward(self, x):
        out = F.max_pool2d(torch.relu(self.conv1(x)), 2)
        out = self.resblocks(out)
        out = F.max_pool2d(out, 2)
        out = out.view(-1, 8 * 8 * self.n_chans1)
        out = torch.relu(self.fc1(out))
        out = self.fc2(out)
        return out
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值