神经网络搭建

pytorch的网络搭建,比tensorflow简单很多。格式很好理解。

如果你想做一个网络,需要先定义一个Class,继承 nn.Module(这个是必须的,所以先import torch.nn as nn,nn是一个工具箱,很好用),我们把class的名字就叫成Net.

Class Net (nn.Module):
  • 1.

这个Class里面主要写两个函数,一个是初始化的__init__函数,另一个是forward函数。我们随便搭一个,如下:

def __init__(self):
        super().__init__()
        self.conv1=nn.Conv2d(1,6,5)
        self.conv2=nn.Conv2d(6,16,5)
 
    def forward(self, x):
        x=F.max_pool2d(F.relu(self.conv1(x)),2)
        x=F.max_pool2d(F.relu(self.conv2(x)),2)
        return x
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

__init__里面就是定义卷积层,当然先得super()一下,给父类nn.Module初始化一下。

(Python的基础知识)在这个里面主要就是定义卷积层的,比如第一层,我们叫它conv1,把它定义成输入1通道,输出6通道,卷积核5*5的的一个卷积层。conv2同理。

神经网络“深度学习”其实主要就是学习卷积核里的参数,像别的不需要学习和改变的,就不用放进去。

比如激活函数relu(),你非要放进去也行,再给它起个名字叫myrelu,也是可以的。forward里面就是真正执行数据的流动。

比如上面的代码,输入的x先经过定义的conv1(这个名字是你自己起的),再经过激活函数F.relu()(这个就不是自己起的名字了,最开始应该先import torch.nn.functional as F,F.relu()是官方提供的函数。

当然如果你在__init__里面把relu定义成了我上面说的myrelu,那你这里直接第一句话就成了x=F.max_pool2d(myrelu(self.conv1(x)),2)。

下一步的F.max_pool2d池化也是一样的,不多废话了。在一系列流动以后,最后把x返回到外面去。

这个Net的Class定义主要要注意两点。

第一:是注意前后输出通道和输入通道的一致性。不能第一个卷积层输出4通道第二个输入6通道,这样就会报错。

第二:它和我们常规的python的class还有一些不同,发现了没有?我们该怎么用这个Net呢?

先定义一个Net的实例(毕竟Net只是一个类不能直接传参数,output=Net(input)当然不行)

net=Net()
  • 1.

这样我们就可以往里传x了,假设你已经有一个要往神经网络的输入的数据“input"(这个input应该定义成tensor类型,怎么定义tensor那就自己去看看书了。)在传入的时候,是:

output=net(input)
  • 1.

看之前的定义:

def __init__(self):
   ……
 
def forward(self, x):
   ……
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

有点奇怪。好像常规python一般向class里面传入一个数据x,在class的定义里面,应该是把这个x作为形参传入__init__函数里的,而在上面的定义中,x作为形参是传入forward函数里面的。

其实也不矛盾,因为你定义net的时候,是net=Net(),并没有往里面传入参数。如果你想初始化的时候按需传入,就把需要的传入进去。

只是x是神经网络的输入,但是并非是初始化需要的,初始化一个网络,必须要有输入数据吗?

未必吧。只是在传入网络的时候,会自动认为你这个x是喂给forward里面的。也就是说,先定义一个网络的实例net=Net(),  这时调用output=net(input), 可以理解为等同于调用output=net.forward(input), 这两者可以理解为一码事。

在网络定义好以后,就涉及到传入参数,算误差,反向传播,更新权重…确实很容易记不住这些东西的格式和顺序。

传入的方式上面已经介绍了,相当于一次正向传播,把一路上各层的输入x都算出来了。

想让神经网络输出的output跟你期望的ground truth差不多,那就是不断减小二者间的差异,这个差异是你自己定义的,也就是目标函数(object function)或者就是损失函数。

如果损失函数loss趋近于0,那么自然就达到目的了。

损失函数loss基本上没法达到0,但是希望能让它达到最小值,那么就是希望它能按照梯度进行下降。

梯度下降的公式,大家应该都很熟悉,不熟悉的话,建议去看一下相关的理论。谁喜欢看公式呢?所以我这里不讲。

只是你的输入是由你来决定的,那神经网络能学习和决定什么呢?

自然它只能决定每一层卷积层的权重。所以神经网络只能不停修改权重,比如y=wx+b,x是你给的,它只能改变w,b让最后的输出y尽可能接近你希望的y值,这样损失loss就越来越小。

如果loss对于输入x的偏导数接近0了,不就意味着到达了一个极值吗?

而l在你的loss计算方式已经给定的情况下,loss对于输入x的偏导数的减小,其实只能通过更新参数卷积层参数W来实现(别的它决定不了啊,都是你输入和提供的)。

所以,通过下述方式实现对W的更新:(注意这些编号,下面还要提)

【1】 先算loss对于输入x的偏导,(当然网络好几层,这个x指的是每一层的输入,而不是最开始的输入input)

【2】 对【1】的结果再乘以一个步长(这样就相当于是得到一个对参数W的修改量)

【3】 用W减掉这个修改量,完成一次对参数W的修改。

说的不太严谨,但是大致意思是这样的。这个过程你可以手动实现,但是大规模神经网络怎么手动实现?那是不可能的事情。所以我们要利用框架pytorch和工具箱torch.nn。

所以要定义损失函数,以MSEloss为例:

compute_loss=nn.MSELoss()
  • 1.

明显它也是个类,不能直接传入输入数据,所以直接loss=nn.MSEloss(target,output)是不对的。需要把这个函数赋一个实例,叫成compute_loss。

之后就可以把你的神经网络的输出,和标准答案target传入进去:

loss=compute_loss(target,output)
  • 1.

算出loss,下一步就是反向传播:

loss.backward()
  • 1.

这一步其实就是把【1】给算完了,得到对参数W一步的更新量,算是一次反向传播。

这里就注意了,loss.backward()是啥玩意?如果是自己的定义的loss(比如你就自己定义了个def loss(x,y):return y-x )这样肯定直接backward会出错。所以应当用nn里面提供的函数。

当然搞深度学习不可能只用官方提供的loss函数,所以如果你要想用自己的loss函数

必须也把loss定义成上面Net的样子(不然你的loss不能反向传播,这点要注意,注:这点是以前写的,很久之前的版本不行,现在都可以了,基本不太需要这样了)。

也是继承nn.Module,把传入的参数放进forward里面,具体的loss在forward里面算,最后return loss。__init__()就空着,写个super().__init__就行了。

在反向传播之后,第【2】和第【3】怎么实现?就是通过优化器来实现。让优化器来自动实现对网络权重W的更新。

所以在Net定义完以后,需要写一个优化器的定义(选SGD方式为例):

from torch import optim
optimizer=optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
  • 1.
  • 2.

同样,优化器也是一个类,先定义一个实例optimizer,然后之后会用。

注意在optimizer定义的时候,需要给SGD传入了net的参数parameters,这样之后优化器就掌握了对网络参数的控制权,就能够对它进行修改了。

传入的时候把学习率lr也传入了。

在每次迭代之前,先把optimizer里存的梯度清零一下(因为W已经更新过的“更新量”下一次就不需要用了)

optimizer.zero_grad()
  • 1.

在loss.backward()反向传播以后,更新参数:

optimizer.step()
  • 1.

所以我们的顺序是:

1.先定义网络:写网络Net的Class,声明网络的实例net=Net(),

2.定义优化器

optimizer=optim.xxx(net.parameters(),lr=xxx),

3.再定义损失函数(自己写class或者直接用官方的,compute_loss=nn.MSELoss()或者其他。

4.在定义完之后,开始一次一次的循环:

①先清空优化器里的梯度信息,optimizer.zero_grad();

②再将input传入,output=net(input) ,正向传播

③算损失,loss=compute_loss(target,output)   ##这里target就是参考标准值GT,需要自己准备,和之前传入的input一一对应

④误差反向传播,loss.backward()

⑤更新参数,optimizer.step()

这样就实现了一个基本的神经网络。大部分神经网络的训练都可以简化为这个过程,无非是传入的内容复杂,网络定义复杂,损失函数复杂,等等等等。

这里介绍在使用 PyTorch 训练深度模型时最省力、最有效的 17 种方法。该文所提方法,都是假设你在 GPU 环境下训练模型。具体内容如下。

01 考虑换一种学习率 schedule

学习率 schedule 的选择对模型的收敛速度和泛化能力有很大的影响。Leslie N. Smith 等人在论文《Cyclical Learning Rates for Training Neural Networks》、《Super-Convergence: Very Fast Training of Neural Networks Using Large Learning Rates 》中提出了周期性(Cyclical)学习率以及 1Cycle 学习率 schedule。之后,fast.ai 的 Jeremy Howard 和 Sylvain Gugger 对其进行了推广。下图是 1Cycle 学习率 schedule 的图示:

PyTorch ~优化神经网络_数据

Sylvain 写到:1Cycle 包括两个等长的步幅,一个步幅是从较低的学习率到较高的学习率,另一个是回到最低水平。最大值来自学习率查找器选取的值,较小的值可以低十倍。然后,这个周期的长度应该略小于总的 epochs 数,并且,在训练的最后阶段,我们应该允许学习率比最小值小几个数量级。与传统的学习率 schedule 相比,在最好的情况下,该 schedule 实现了巨大的加速(Smith 称之为超级收敛)。例如,使用 1Cycle 策略在 ImageNet 数据集上训练 ResNet-56,训练迭代次数减少为原来的 1/10,但模型性能仍能比肩原论文中的水平。在常见的体系架构和优化器中,这种 schedule 似乎表现得很好。

Pytorch 已经实现了这两种方法:「torch.optim.lr_scheduler.CyclicLR」和「torch.optim.lr_scheduler.OneCycleLR」。

参考文档:https://pytorch.org/docs/stable/optim.html

02 在 DataLoader 中使用多个 worker 和页锁定内存

当使用 torch.utils.data.DataLoader 时,设置 num_workers > 0,而不是默认值 0,同时设置 pin_memory=True,而不是默认值 False。

参考文档:https://pytorch.org/docs/stable/data.html

来自 NVIDIA 的高级 CUDA 深度学习算法软件工程师 Szymon Micacz 就曾使用四个 worker 和页锁定内存(pinned memory)在单个 epoch 中实现了 2 倍的加速。人们选择 worker 数量的经验法则是将其设置为可用 GPU 数量的四倍,大于或小于这个数都会降低训练速度。请注意,增加 num_workers 将增加 CPU 内存消耗。

03 把 batch 调到最大

把 batch 调到最大是一个颇有争议的观点。一般来说,如果在 GPU 内存允许的范围内将 batch 调到最大,你的训练速度会更快。但是,你也必须调整其他超参数,比如学习率。一个比较好用的经验是,batch 大小加倍时,学习率也要加倍。

OpenAI 的论文《An Empirical Model of Large-Batch Training》很好地论证了不同的 batch 大小需要多少步才能收敛。在《How to get 4x speedup and better generalization using the right batch size》一文中,作者 Daniel Huynh 使用不同的 batch 大小进行了一些实验(也使用上面讨论的 1Cycle 策略)。

最终,他将 batch 大小由 64 增加到 512,实现了 4 倍的加速。然而,使用大 batch 的不足是,这可能导致解决方案的泛化能力比使用小 batch 的差。

04 使用自动混合精度(AMP)

PyTorch 1.6 版本包括对 PyTorch 的自动混合精度训练的本地实现。这里想说的是,与单精度 (FP32) 相比,某些运算在半精度 (FP16) 下运行更快,而不会损失准确率。AMP 会自动决定应该以哪种精度执行哪种运算。这样既可以加快训练速度,又可以减少内存占用。

在最好的情况下,AMP 的使用情况如下:

import torch
# Creates once at the beginning of training
scaler = torch.cuda.amp.GradScaler()


for data, label in data_iter:
   optimizer.zero_grad()
   # Casts operations to mixed precision
   with torch.cuda.amp.autocast():
      loss = model(data)

   # Scales the loss, and calls backward()
   # to create scaled gradients
   scaler.scale(loss).backward()

   # Unscales gradients and calls
   # or skips optimizer.step()
   scaler.step(optimizer)

   # Updates the scale for next iteration
   scaler.update()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
05 考虑使用另一种优化器

AdamW 是由 fast.ai 推广的一种具有权重衰减(而不是 L2 正则化)的 Adam,在 PyTorch 中以 torch.optim.AdamW 实现。AdamW

似乎在误差和训练时间上都一直优于 Adam。Adam 和 AdamW 都能与上面提到的 1Cycle 策略很好地搭配。

目前,还有一些非本地优化器也引起了很大的关注,最突出的是 LARS 和 LAMB。NVIDA 的 APEX 实现了一些常见优化器的融合版本,比如 Adam。与 PyTorch 中的 Adam 实现相比,这种实现避免了与 GPU 内存之间的多次传递,速度提高了 5%。

06 cudNN 基准

如果你的模型架构保持不变、输入大小保持不变,设置 torch.backends.cudnn.benchmark = True。

07 小心 CPU 和 GPU 之间频繁的数据传输

当频繁地使用 tensor.cpu() 将张量从 GPU 转到 CPU(或使用 tensor.cuda() 将张量从 CPU 转到 GPU)时,代价是非常昂贵的。item() 和 .numpy() 也是一样可以使用. detach() 代替。

如果你创建了一个新的张量,可以使用关键字参数 device=torch.device( cuda:0 ) 将其分配给 GPU。

如果你需要传输数据,可以使用. to(non_blocking=True),只要在传输之后没有同步点。

08 使用梯度 / 激活 checkpointing

Checkpointing 的工作原理是用计算换内存,并不存储整个计算图的所有中间激活用于 backward pass,而是重新计算这些激活。我们可以将其应用于模型的任何部分。

具体来说,在 forward pass 中,function 会以 torch.no_grad() 方式运行,不存储中间激活。相反的是, forward pass 中会保存输入元组以及 function 参数。在 backward pass 中,输入和 function 会被检索,并再次在 function 上计算 forward pass。然后跟踪中间激活,使用这些激活值计算梯度。

因此,虽然这可能会略微增加给定 batch 大小的运行时间,但会显著减少内存占用。这反过来又将允许进一步增加所使用的 batch 大小,从而提高 GPU 的利用率。

尽管 checkpointing 以 torch.utils.checkpoint 方式实现,但仍需要一些思考和努力来正确地实现。Priya Goyal 写了一个很好的教程来介绍 checkpointing 关键方面。

Priya Goyal 教程地址:https://github.com/prigoyal/pytorch_memonger/blob/master/tutorial/Checkpointing_for_PyTorch_models.ipynb

09 使用梯度积累

增加 batch 大小的另一种方法是在调用 optimizer.step() 之前在多个. backward() 传递中累积梯度。

Hugging Face 的 Thomas Wolf 的文章《Training Neural Nets on Larger Batches: Practical Tips for 1-GPU, Multi-GPU & Distributed setups》介绍了如何使用梯度累积。梯度累积可以通过如下方式实现:

model.zero_grad()                                   # Reset gradients tensors
for i, (inputs, labels) in enumerate(training_set):
    predictions = model(inputs)                     # Forward pass
    loss = loss_function(predictions, labels)       # Compute loss function
    loss = loss / accumulation_steps                # Normalize our loss (if averaged)
    loss.backward()                                 # Backward pass
    if (i+1) % accumulation_steps == 0:             # Wait for several backward steps
        optimizer.step()                            # Now we can do an optimizer step
        model.zero_grad()                           # Reset gradients tensors
        if (i+1) % evaluation_steps == 0:           # Evaluate the model when we...
            evaluate_model()                        # ...have no gradients accumulate
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

这个方法主要是为了规避 GPU 内存的限制而开发的。

10 使用分布式数据并行进行多 GPU 训练

加速分布式训练可能有很多方法,但是简单的方法是使用 torch.nn.DistributedDataParallel 而不是 torch.nn.DataParallel。这样一来,每个 GPU 将由一个专用的 CPU 核心驱动,避免了 DataParallel 的 GIL 问题。

分布式训练文档地址:https://pytorch.org/tutorials/beginner/dist_overview.html

11 设置梯度为 None 而不是 0

梯度设置为. zero_grad(set_to_none=True) 而不是 .zero_grad()。这样做可以让内存分配器处理梯度,而不是将它们设置为 0。正如文档中所说,将梯度设置为 None 会产生适度的加速,但不要期待奇迹出现。注意,这样做也有缺点,详细信息请查看文档。

文档地址:https://pytorch.org/docs/stable/optim.html

12 使用. as_tensor() 而不是. tensor()

torch.tensor() 总是会复制数据。如果你要转换一个 numpy 数组,使用 torch.as_tensor() 或 torch.from_numpy() 来避免复制数据。

13 必要时打开调试工具

PyTorch 提供了很多调试工具,例如 autograd.profiler、autograd.grad_check、autograd.anomaly_detection。请确保当你需要调试时再打开调试器,不需要时要及时关掉,因为调试器会降低你的训练速度。

14 使用梯度裁剪

关于避免 RNN 中的梯度爆炸的问题,已经有一些实验和理论证实,梯度裁剪(gradient = min(gradient, threshold))可以加速收敛。HuggingFace 的 Transformer 实现就是一个非常清晰的例子,说明了如何使用梯度裁剪。本文中提到的其他一些方法,如 AMP 也可以用。

在 PyTorch 中可以使用 torch.nn.utils.clip_grad_norm_来实现。

15 在 BatchNorm 之前关闭 bias

在开始 BatchNormalization 层之前关闭 bias 层。对于一个 2-D 卷积层,可以将 bias 关键字设置为 False:torch.nn.Conv2d(..., bias=False, ...)。

16 在验证期间关闭梯度计算

在验证期间关闭梯度计算,设置:torch.no_grad() 。

17 使用输入和 batch 归一化

要再三检查一下输入是否归一化?是否使用了 batch 归一化?

原文链接: https://efficientdl.com/faster-deep-learning-in-pytorch-a-guide/