深度学习实践篇 第十章:混合精度训练

简介: 简要介绍混合精度的原理和代码实现。

参考教程:
https://pytorch.org/tutorials/recipes/recipes/amp_recipe.html?highlight=amp
https://pytorch.org/docs/stable/amp.html
https://arxiv.org/pdf/1710.03740.pdf
https://zhuanlan.zhihu.com/p/79887894

原理

float 32

参考资料:wikipedia/float32
image.png

float32的格式如上图。一共三十二位。

  1. 蓝色区域占一位:表示sign。
  2. 绿色区域占八位:表示exponent。
  3. 红色区域占23位:表示fraction。

它的计算规则如下:
image.png

假如你的 Exponent位全为0,那么fraction位有两种情况。

  • fraction全为0,则最终结果为0。
  • fraction不为0,则表示一个subnormal numbers,也就是一个非规格化的浮点数。
    $$ (-1)^{sign} \times2^{-126} \times(\frac{fraction}{2^{23}}) $$

假如你的Exponent位全为1,那么fraction位有两种情况。

  • fraction全为0, 则最终结果为inf。
  • fraction不为0,则最终结果为NaN。

假如你的Exponent是其他的情况,那么数据的计算公式为:
$$ (-1)^{sign}\times2^{(exponent-127)}\times(1+\frac{fraction}{2^{23}}) $$
因此float32能取到的最小正数是$2^{-126}$,最大正数是$2^{2^8-2-127}\times(1+\frac{2^{23}-1}{2^{23}})$

值得一提的是,float16表示的数是不均匀的,也就是不同的区间范围有着不一样的精度。具体的例子可以从wikipedia/float32上看。下面只给出一点点例子。

Min Max Interval
1 2 $2^{-23}$
$2^{22}$ $2^{23}$ $2^{-1}$
$2^{127}$ $2^{128}$ $2^{104}$

float 16

参考资料:wikipedia/float16
image.png

float16的格式如上图。一共十六位。

  1. 蓝色区域占一位:表示sign。
  2. 绿色区域占五位:表示exponent。
  3. 红色区域占十位:表示fraction。

它的计算规则如下:
image.png

假如你的 Exponent位全为0,那么fraction位有两种情况。

  • fraction全为0,则最终结果为0。
  • fraction不为0,则表示一个subnormal numbers,也就是一个非规格化的浮点数。
    $$ (-1)^{sign} \times2^{-14} \times(\frac{fraction}{1024}) $$

假如你的Exponent位全为1,那么fraction位有两种情况。

  • fraction全为0, 则最终结果为inf。
  • fraction不为0,则最终结果为NaN。

假如你的Exponent为是其它情况,则进行正常的计算。
$$ (-1)^{sign} \times2^{(exponent-15)} \times(1+\frac{fraction}{1024}) $$

因此float16能取到的最小正数是$2^{-14}$,最大数是$2^{30-15}\times(1+\frac{1023}{1024}) = 65504$。

值得一提的是,float16表示的数是不均匀的,也就是不同的区间范围有着不一样的精度。具体的例子可以从wikipedia/float16上看。下面只给出一点点例子。

Min Max Interval
0 $2^{-13}$ $2^{-24}$
$2^{-9}$ $2^{-9}$ $2^{-19}$
$2^{-5}$ $2^{-4}$ $2^{-15}$

混合精度

增大神经网络的规模,往往能带来效果上的提升。但是模型规模的增大也意味着更多的内存和计算量需求。综合来说,你的模型表现受到三个方面的限制:

  1. arithmetic bandwidth.
  2. memory bandwidth.
  3. latency.

在神经网络训练中,通常使用的数据类型都是float32,也就是单精度。而所谓的半精度,也就是float16。通过使用半精度,可以解决上面的两个限制。

  1. arithmetic bandwidth。在GPU上,半精度的吞吐量可以达到单精度的2到8倍。
  2. memory bandwidth。 半精度的内存占用是单精度的一半。

看起来半精度和单精度相比很有优势,当然,半精度也有半精度自身的问题。

以下部分参考自:https://zhuanlan.zhihu.com/p/79887894

  1. 溢出错误: float16的范围比float32的范围要小,所以也更容易溢出,更容易出现'NaN'的问题。
  2. 舍入误差: 当前更新的梯度过小时,即更新的梯度达不到区间间隔的大小时,可能会出现梯度更新失败的情况。

论文中为了在使用float16的训练的同时又能保证模型的准确性,采用了以下三个方法:

  1. single-precision master weights and update。
    在训练过程在,weights,activation,gradients等数据都用float16存储,同时拷贝一个float32的weights,用来更新。这样float16的梯度在更新是又转为了float32,避免了舍入误差的问题。
  2. loss-scaling。
    为了解决梯度过小的问题,一个比较高效的方法是对计算出的loss进行scale。在更新梯度的时候,只要将梯度转为float32再将scale去掉就可以了。
  3. accumulation。
    网络中的数学计算可以分为以下三类:vector dot-products, reductions and point-wise opeartions。有的运算为了维持精度,必须使用float32,有的则可以使用float16。

代码实现

torch.amp

torch.amp提供了很方便的混合精度方法,一些计算会使用float32的类型而另一些则会用半精度float16。混合精度会尝试给每个操作选择比较合适的类型。

在实现自动半精度训练时,通常会用到torch.autocasttorch.cuda.amp.GradScaler两个方法。

torch.autocast可以在保证模型表现的情况下,为不同的操作选择合适的精度。

torch.cuda.amp.GradScaler和它的名字一样,帮助进行梯度的缩放,帮助使用flaot16的网络收敛。就像我们之前说的一样,可以减少溢出的问题。

torch.autocast

autocast提供了两种api。

torch.autocast("cuda", args...) is equivalent to torch.cuda.amp.autocast(args...).
torch.autocast("cpu", args...) is equivalent to torch.cpu.amp.autocast(args...).

autocast可以以上下文管理器context manager或者装饰器decorator的形式使用。允许你的这部分代码以混合精度的形式运行。

torch.autocast(device_type, dtype=None, enabled=True, cache_enabled=None)

传入参数包括:

  1. device_type: 当前设备类型,一般是cuda和cpu,也有xpu和hpu。
  2. dtype:如果你指定了dtype的类型,那么就会使用这个类型作为target dtype,如果没有的话就是按设备获取。
  3. enabled默认是True,假如你的dtype类型不支持或者device_type不对的时候,enabled就会被改为False,表示不可以使用autocast。

现在来看一下autocast的两种用法,第一种是当作context manger。

with autocast(device_type='cuda', dtype=torch.float16):
      output = model(input)
      loss = loss_fn(output, target)

在这个范围内的model的forward和loss的计算都会以半精度的形式进行。

第二种是当作decorator来使用。你可以直接用它来修饰你的模型的forward()过程。

class AutocastModel(nn.Module):
    ...
    @autocast()
    def forward(self, input):
        ...

这里要注意的是,在autocast范围内的计算结果得到的tensor可能会是float16的类型,当你想用这个结果在autocast的范围外进行别的计算时,要注意把它变回float32。

pytorch tutorial中给出了这样一个例子。

在下面这个例子中,a,b,c,d在创建时的类型都是float32,然后在autocast的范围内进行了torch.mm的计算,torch.mm是矩阵乘法,支持float16的半精度,所以这时你得到的结果e和f都是float16的类型。

a_float32 = torch.rand((8, 8), device="cuda")
b_float32 = torch.rand((8, 8), device="cuda")
c_float32 = torch.rand((8, 8), device="cuda")
d_float32 = torch.rand((8, 8), device="cuda")

with autocast():
    # torch.mm is on autocast's list of ops that should run in float16.
    # Inputs are float32, but the op runs in float16 and produces float16 output.
    # No manual casts are required.
    e_float16 = torch.mm(a_float32, b_float32)
    # Also handles mixed input types
    f_float16 = torch.mm(d_float32, e_float16)

# After exiting autocast, calls f_float16.float() to use with d_float32
g_float32 = torch.mm(d_float32, f_float16.float())

你想用float16类型的f和float32类型的d进行乘法运算是不行的,所以需要先把f变回float32。

pytorch中还给出了在autocast-enabled region的局部禁止使用autocast的例子,就是在代码内再次套一个emabled=False的autocast。

with autocast():
    e_float16 = torch.mm(a_float32, b_float32)
    with autocast(enabled=False):
        # Calls e_float16.float() to ensure float32 execution
        # (necessary because e_float16 was created in an autocasted region)
        f_float32 = torch.mm(c_float32, e_float16.float())

torch.cuda.amp.GradScaler

在更新的梯度过小时,可能会超出float16数字的边界,导致下溢出或者无法更新的情况。gradient scaling就是为了解决这个问题。

它在神经网络的loss上乘以一个缩放因子,这个缩放因子会随着反向传播传递,使得各个层的梯度都不至于过小。

torch.cuda.amp.GradScaler(init_scale=65536.0, growth_factor=2.0, backoff_factor=0.5, growth_interval=2000, enabled=True)

传入参数包括:

  1. init_scale:初始的缩放因子。
  2. growth_factor:你的缩放因子不是固定的,假如在训练过程中又发现了为NaN或inf的梯度,那么这个缩放因子是会按照growth_factor进行更新的。
  3. backoff_factor:我理解的是和growth_factor相反的过程。
  4. growth_interval:假如没有出现NaN/inf,也会按照interval进行scale的更新。

来看一下GradScaler的用法。

scaler = torch.cuda.amp.GradScaler()

for epoch in range(0): # 0 epochs, this section is for illustration only
    for input, target in zip(data, targets):
        with torch.autocast(device_type=device, dtype=torch.float16):
            output = net(input)
            loss = loss_fn(output, target)

        # Scales loss. Calls ``backward()`` on scaled loss to create scaled gradients.
        scaler.scale(loss).backward()

        # ``scaler.step()`` first unscales the gradients of the optimizer's assigned parameters.
        # If these gradients do not contain ``inf``s or ``NaN``s, optimizer.step() is then called,
        # otherwise, optimizer.step() is skipped.
        scaler.step(opt)

        # Updates the scale for next iteration.
        scaler.update()

        opt.zero_grad() # set_to_none=True here can modestly improve performance

我们来看一下在这个过程中GradScaler()都做了什么。

首先,在计算得到loss之后,出现了。

scaler.scale(loss).backward()

用我们的scaler中的scale方法对得到的loss进行缩放后,再进行backward()。scale方法也比较简单,返回的结果可以直接理解为我们的loss和缩放因子scale_factor的乘积。

然后会进行scaler.step(opt),这里的opt是我们的optimizer。我们常用的应该是opt.step()。也就是说在这里optimizer的step()梯度更新的步骤被放到scaler里完成了。

def step(self, optimizer, *args, **kwargs):它的作用是这样的。它首先会确认一下梯度里面是否存在Inf和NaN的情况,如果有的话,optimizer.step()这个步骤就会被跳过去,避免引发一些错误;如果没有的话,就会用unscale过的梯度来进行optimizer.step()。

最后呢则是scaler.update(),也就是其中的scale的更新过程。

简而言之,scaler总共做了三件事:

  1. 对loss进行scale,这样在backward的时候防止梯度的underflow。
  2. 对梯度进行unscale,用于更新。
  3. 更新scaler中的scale factor。

GradScaler with penalty

tutorial中还额外提到了到penalty的情况。

在你没有使用混合精度的情况下,你的grad penalty是这样计算的。以L2 loss为例:

for input, target in data:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)

        # Creates gradients
        grad_params = torch.autograd.grad(outputs=loss,                                   inputs=model.parameters(),                                      create_graph=True)
        # Computes the penalty term and adds it to the loss
        grad_norm = 0
        for grad in grad_params:
            grad_norm += grad.pow(2).sum()
        grad_norm = grad_norm.sqrt()
        loss = loss + grad_norm

        loss.backward()

        # clip gradients here, if desired

        optimizer.step()

对于grad_params中的每一项,都求他的平方和,最后对grad_norm开根号,作为loss和我们的模型的预测损失组合在一起使用。起到了一种gradient regularization的作用。

当你要使用scaler时,因为你的loss是scale过的,所以你的梯度也都受到了影响,你应该把它们先变回去unscale的状态,再进行计算。

scaled_grad_params = torch.autograd.grad(outputs=scaler.scale(loss),                                                inputs=model.parameters(),                                              create_graph=True)

注意看这里用的是scale过的loss,得到的grad也是scale过的,所以你需要自己手动计算来得到unscale的grad和penalty loss。

inv_scale = 1./scaler.get_scale()
# scale_factor是缩放倍数,我们要unscale所以用1/scale_factor
grad_params = [p * inv_scale for p in scaled_grad_params]

之后就是正常的计算过程:


# Computes the penalty term and adds it to the loss
with autocast(device_type='cuda', dtype=torch.float16):
    grad_norm = 0
    for grad in grad_params:
        grad_norm += grad.pow(2).sum()
    grad_norm = grad_norm.sqrt()
    loss = loss + grad_norm
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
相关文章
|
6天前
|
机器学习/深度学习 数据采集 PyTorch
使用PyTorch解决多分类问题:构建、训练和评估深度学习模型
使用PyTorch解决多分类问题:构建、训练和评估深度学习模型
使用PyTorch解决多分类问题:构建、训练和评估深度学习模型
|
3天前
|
机器学习/深度学习 算法
揭秘深度学习中的对抗性网络:理论与实践
【5月更文挑战第18天】 在深度学习领域的众多突破中,对抗性网络(GANs)以其独特的机制和强大的生成能力受到广泛关注。不同于传统的监督学习方法,GANs通过同时训练生成器与判别器两个模型,实现了无监督学习下的高效数据生成。本文将深入探讨对抗性网络的核心原理,解析其数学模型,并通过案例分析展示GANs在图像合成、风格迁移及增强学习等领域的应用。此外,我们还将讨论当前GANs面临的挑战以及未来的发展方向,为读者提供一个全面而深入的视角以理解这一颠覆性技术。
|
4天前
|
机器学习/深度学习 人工智能 算法
【AI】从零构建深度学习框架实践
【5月更文挑战第16天】 本文介绍了从零构建一个轻量级的深度学习框架tinynn,旨在帮助读者理解深度学习的基本组件和框架设计。构建过程包括设计框架架构、实现基本功能、模型定义、反向传播算法、训练和推理过程以及性能优化。文章详细阐述了网络层、张量、损失函数、优化器等组件的抽象和实现,并给出了一个基于MNIST数据集的分类示例,与TensorFlow进行了简单对比。tinynn的源代码可在GitHub上找到,目前支持多种层、损失函数和优化器,适用于学习和实验新算法。
59 2
|
6天前
|
机器学习/深度学习 自然语言处理 算法
利用深度学习优化图像识别精度的策略
【5月更文挑战第15天】 在计算机视觉领域,图像识别的精确度直接关系到后续处理的效果与可靠性。本文旨在探讨如何通过深度学习技术提升图像识别任务的精度。首先,文中介绍了卷积神经网络(CNN)的基础结构及其在图像识别中的应用;然后,详细分析了数据增强、网络结构优化、正则化方法和注意力机制等策略对提高模型性能的作用;最后,通过实验验证了所提策略的有效性,并讨论了未来可能的研究方向。本文不仅为图像识别领域的研究者提供了实用的优化策略,也为相关应用的开发者指明了提升系统性能的可能途径。
|
6天前
|
机器学习/深度学习
深度学习网络训练,Loss出现Nan的解决办法
深度学习网络训练,Loss出现Nan的解决办法
9 0
|
6天前
|
机器学习/深度学习 人工智能 自然语言处理
深度理解深度学习:从理论到实践的探索
【5月更文挑战第3天】 在人工智能的浪潮中,深度学习以其卓越的性能和广泛的应用成为了研究的热点。本文将深入探讨深度学习的核心理论,解析其背后的数学原理,并通过实际案例分析如何将这些理论应用于解决现实世界的问题。我们将从神经网络的基础结构出发,逐步过渡到复杂的模型架构,同时讨论优化算法和正则化技巧。通过本文,读者将对深度学习有一个全面而深刻的认识,并能够在实践中更加得心应手地应用这些技术。
|
6天前
|
机器学习/深度学习 人工智能 缓存
安卓应用性能优化实践探索深度学习在图像识别中的应用进展
【4月更文挑战第30天】随着智能手机的普及,移动应用已成为用户日常生活的重要组成部分。对于安卓开发者而言,确保应用流畅、高效地运行在多样化的硬件上是一大挑战。本文将探讨针对安卓平台进行应用性能优化的策略和技巧,包括内存管理、多线程处理、UI渲染效率提升以及电池使用优化,旨在帮助开发者构建更加健壮、响应迅速的安卓应用。 【4月更文挑战第30天】 随着人工智能技术的迅猛发展,深度学习已成为推动计算机视觉领域革新的核心动力。本篇文章将深入分析深度学习技术在图像识别任务中的最新应用进展,并探讨其面临的挑战与未来发展趋势。通过梳理卷积神经网络(CNN)的优化策略、转移学习的实践应用以及增强学习与生成对
|
6天前
|
机器学习/深度学习 监控 自动驾驶
利用深度学习优化图像识别精度的策略
【4月更文挑战第23天】 随着人工智能技术的飞速发展,深度学习已成为图像识别领域的核心动力。本文旨在探讨通过深度学习模型优化来提高图像识别精度的有效策略。文中不仅介绍了卷积神经网络(CNN)在图像处理中的应用,同时详细阐述了数据增强、网络结构优化、正则化技术以及迁移学习等策略如何促进模型性能的提升。此外,文章还讨论了当前面临的主要挑战和潜在的解决方案,为未来图像识别技术的发展提供了一定的指导意义。
|
6天前
|
机器学习/深度学习 并行计算 PyTorch
PyTorch与CUDA:加速深度学习训练
【4月更文挑战第18天】本文介绍了如何使用PyTorch与CUDA加速深度学习训练。CUDA是NVIDIA的并行计算平台,常用于加速深度学习中的矩阵运算。PyTorch与CUDA集成,允许开发者将模型和数据迁移到GPU,利用`.to(device)`方法加速计算。通过批处理、并行化策略及优化技巧,如混合精度训练,可进一步提升训练效率。监控GPU内存和使用调试工具确保训练稳定性。PyTorch与CUDA的结合对深度学习训练的加速作用显著。
|
6天前
|
机器学习/深度学习 数据可视化 数据挖掘
R语言深度学习卷积神经网络 (CNN)对 CIFAR 图像进行分类:训练与结果评估可视化
R语言深度学习卷积神经网络 (CNN)对 CIFAR 图像进行分类:训练与结果评估可视化