YOLOv5的Tricks | 【Trick7】指数移动平均(Exponential Moving Average,EMA)

简介: 这篇博客主要用于整理网上对EMA(指数移动平均)的介绍,在yolov5代码中也使用了这个技巧,现对其进行归纳。

1. 移动平均法


image.png


详细见参考资料2.


2. 指数移动平均


下面对EMA,也就是指数移动平均进行进一步介绍。EMA也称滑动平均(exponential moving average),或者叫做指数加权平均(exponentially weighted moving average),可以用来估计变量的局部均值,使得变量的更新与一段时间内的历史取值有关。

image.png

image.png


  • 指数平均移动的优势:

V 是用来计算数据的指数加权平均数,计算指数加权平均数只占单行数字的存储和内存,当然并不是最好的,也不是最精准的计算平均数的方法,如果你需要计算时间窗,你可以直接过去 10 天的总和或者过去 50 天的总和除以 10 或 50 就好了,如此往往会得到更好的估测,但缺点是如果保存最近的气温和过去 10 天的总和,必须占更多的内存,执行更加复杂,而计算指数加权平均数只占单行数字的存储和内存。他的效率和资源的占有率会大大的减小。 所以在机器学习中大部分采用指数加权平均的方法计算平均值。


简单来说,就是占内存少,不需要保存过去10个或者100个历史 θ θθ 值,就能够估计其均值。相应的就是没有计算平均数的准确。


3. TensorFlow中的EMA使用


滑动平均可以看作是变量的过去一段时间取值的均值,相比对变量直接赋值而言,滑动平均得到的值在图像上更加平缓光滑,抖动性更小,不会因为某次的异常取值而使得滑动平均值波动很大

image.png

这一点其实和 Bias correction 很像。


详细见参考资料1.


4. Yolov5中的EMA使用


动平均可以使模型在测试数据上更健壮(robust)。“采用随机梯度下降算法训练神经网络时,使用滑动平均在很多应用中都可以在一定程度上提高最终模型在测试数据上的表现。”


对神经网络边的权重 weights 使用滑动平均,得到对应的影子变量 shadow_weights。在训练过程仍然使用原来不带滑动平均的权重 weights,不然无法得到 weights 下一步更新的值,又怎么求下一步 weights 的影子变量 shadow_weights。之后在测试过程中使用 shadow_weights 来代替 weights 作为神经网络边的权重,这样在测试数据上效果更好。因为 shadow_weights 的更新更加平滑,对于随机梯度下降而言,更平滑的更新说明不会偏离最优点很远


此外,参考资料1的博主的理解:“对于梯度下降 batch gradient decent,感觉影子变量作用不大,因为梯度下降的方向已经是最优的了,loss 一定减小;对于 mini-batch gradient decent,可以尝试滑动平均,毕竟 mini-batch gradient decent 对参数的更新也存在抖动。”


yolov5参考代码:

# 作用: 把b对象中包含在include的属性而不是exclude的属性赋予给a
def copy_attr(a, b, include=(), exclude=()):
    # Copy attributes from b to a, options to only include [...] and to exclude [...]
    for k, v in b.__dict__.items():
        if (len(include) and k not in include) or k.startswith('_') or k in exclude:
            continue
        else:
          # 将对象b的属性k赋值给a:a.k = v
            setattr(a, k, v)
class ModelEMA:
    """ Model Exponential Moving Average from https://github.com/rwightman/pytorch-image-models
    Keep a moving average of everything in the model state_dict (parameters and buffers).
    This is intended to allow functionality like
   https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage
    A smoothed version of the weights is necessary for some training schemes to perform well.
    This class is sensitive where it is initialized in the sequence of model init,
    GPU assignment and distributed training wrappers.
    """
  # decay: 衰减函数参数,默认0.9999 考虑过去10000次的真实值
    # updates: ema更新次数
    def __init__(self, model, decay=0.9999, updates=0):
        # Create EMA
        self.ema = deepcopy(model.module if is_parallel(model) else model).eval()  # FP32 EMA
        # if next(model.parameters()).device.type != 'cpu':
        #     self.ema.half()  # FP16 EMA
        self.updates = updates  # number of EMA updates(更新次数)
        # self.decay: 衰减函数 输入变量为x
        self.decay = lambda x: decay * (1 - math.exp(-x / 2000))  # decay exponential ramp (to help early epochs)
        # 所有参数取消设置梯度(测试  model.val)
        for p in self.ema.parameters():
            p.requires_grad_(False)
    def update(self, model):
        # 更新ema的参数  Update EMA parameters
        with torch.no_grad():
            self.updates += 1  # ema更新次数 + 1
            d = self.decay(self.updates)  # 随着更新次数 更新参数贝塔(d)
            # msd: 模型配置的字典 model state_dict  msd中的数据保持不变 用于训练
            msd = model.module.state_dict() if is_parallel(model) else model.state_dict()
            # 遍历模型配置字典 如: k=linear.bias  v=[0.32, 0.25]  ema中的数据发生改变 用于测试
            for k, v in self.ema.state_dict().items():
                # 这里得到的v: 预测值
                if v.dtype.is_floating_point:
                  # shadow_variable = decay*shadow_variable + (1 − decay)*variable
                    v *= d    # 公式左边  decay * shadow_variable
                    # .detach() 使对应的Variables与网络隔开而不参与梯度更新
                    v += (1. - d) * msd[k].detach()  # 公式右边  (1−decay) * variable
    def update_attr(self, model, include=(), exclude=('process_group', 'reducer')):
        # Update EMA attributes
        # 调用上面的copy_attr函数 从model中复制相关属性值到self.ema中
        copy_attr(self.ema, model, include, exclude)


代码中设 d e c a y = 0.999 decay=0.999decay=0.999,一个更直观的理解,在最后的 1000 次训练过程中,模型早已经训练完成,正处于抖动阶段,而滑动平均相当于将最后的 1000 次抖动进行了平均,这样得到的权重会更加 robust


yolov5对ema的使用过程:

def train():
  ...
  # EMA
    ema = ModelEMA(model) if RANK in [-1, 0] else None
  # Resume
    if pretrained:
        ...
        # EMA
        if ema and ckpt.get('ema'):
            ema.ema.load_state_dict(ckpt['ema'].float().state_dict())
            ema.updates = ckpt['updates']
  ...
  # train
  for epoch in range(start_epoch, epochs):
  model.train()
  for i, (imgs, targets, paths, _) in enumerate(train_loader):
    ...
    # Backward
            scaler.scale(loss).backward()
            # Optimize
            if ni - last_opt_step >= accumulate:
                scaler.step(optimizer)  # optimizer.step
                scaler.update()
                optimizer.zero_grad()
                if ema:
                    ema.update(model)
                last_opt_step = ni
  # eval: mAP
  if RANK in [-1, 0]:
    ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights'])
    results, maps, _ = val.run(data_dict,
                                           batch_size=batch_size // WORLD_SIZE * 2,
                                           imgsz=imgsz,
                                           model=ema.ema,
                                           ...)
    # Save model
         if (not nosave) or (final_epoch and not evolve):  # if save
             ckpt = {'epoch': epoch,
                     'best_fitness': best_fitness,
                     'model': deepcopy(de_parallel(model)).half(),
                     'ema': deepcopy(ema.ema).half(),
                     'updates': ema.updates,
                     'optimizer': optimizer.state_dict(),
                     'wandb_id': loggers.wandb.wandb_run.id if loggers.wandb else None}
                     # Save last, best and delete
                torch.save(ckpt, last)
  ...


以上是yolov5的参考代码,参考资料3中给出了另外的一种pytorch实现:


class EMA():
    def __init__(self, model, decay):
        self.model = model
        self.decay = decay
        self.shadow = {}     # 存储指数移动平均更新后的参数
        self.backup = {}  # 保存未使用EMA前的参数
  # 保存初始参数
    def register(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad:
                self.shadow[name] = param.data.clone()
  # 更新Vt参数
    def update(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad:
                assert name in self.shadow
                new_average = (1.0 - self.decay) * param.data + self.decay * self.shadow[name]
                self.shadow[name] = new_average.clone()
  # 更新模型参数,赋予EMA更新后的参函数,同时保留原始参数
    def apply_shadow(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad:
                assert name in self.shadow
                self.backup[name] = param.data
                param.data = self.shadow[name]
  # 还原参数
    def restore(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad:
                assert name in self.backup
                param.data = self.backup[name]
        self.backup = {}
# 初始化
ema = EMA(model, 0.999)
ema.register()
# 训练过程中,更新完参数后,同步update shadow weights
def train():
    optimizer.step()
    ema.update()
# eval前,apply shadow weights;eval之后,恢复原来模型的参数
def evaluate():
    ema.apply_shadow()
    # evaluate
    ema.restore()


这里顺便总结两个遍历模型参数的方法:


  1. model.named_parameters()

返回是一个迭代器,如下所示:

image.png


可以通过__next__()来查看下一个内容

image.png


迭代方法:


for name, param in model.named_parameters():
  if param.requires_grad:
  self.shadow[name] = param.data.clone()


  1. model.state_dict()

这里返回是整个参数字典,比较直观

image.png


迭代方法:


for name, param in model.state_dict().items():
  if param.requires_grad:
  self.shadow[name] = param.data.clone()


参考资料:


1. 理解滑动平均(exponential moving average)


2. 优化算法之指数移动加权平均——非常详细(推荐)


3. 【炼丹技巧】指数移动平均(EMA)的原理及PyTorch实现


4. 机器学习模型性能提升技巧:指数加权平均(EMA)


5. 吴恩达老师的课程资料:指数平均加权


6. tensorflow官方资料:tf.train.ExponentialMovingAverage


7. 【YOLOV5-5.x 源码解读】torch_utils.py


目录
相关文章
|
机器学习/深度学习 开发框架 .NET
YOLOv5的Tricks | 【Trick6】学习率调整策略(One Cycle Policy、余弦退火等)
YOLOv5的Tricks | 【Trick6】学习率调整策略(One Cycle Policy、余弦退火等)
2598 0
YOLOv5的Tricks | 【Trick6】学习率调整策略(One Cycle Policy、余弦退火等)
|
6月前
|
机器学习/深度学习
损失函数大全Cross Entropy Loss/Weighted Loss/Focal Loss/Dice Soft Loss/Soft IoU Loss
损失函数大全Cross Entropy Loss/Weighted Loss/Focal Loss/Dice Soft Loss/Soft IoU Loss
135 2
|
机器学习/深度学习 传感器 算法
指数分布优化算法Exponential distribution optimizer(EDO)附matlab代码
指数分布优化算法Exponential distribution optimizer(EDO)附matlab代码
|
机器学习/深度学习 数据挖掘 Python
使用Python实现Hull Moving Average (HMA)
赫尔移动平均线(Hull Moving Average,简称HMA)是一种技术指标,于2005年由Alan Hull开发。它是一种移动平均线,利用加权计算来减少滞后并提高准确性。
407 0
|
机器学习/深度学习 算法
线性回归(Linear regression)算法
属于有监督学习、判别模型、有预测函数、有优化目标,有优化求解算法
296 0
|
机器学习/深度学习 算法 数据挖掘
Lesson 7.2 Mini Batch K-Means与DBSCAN密度聚类
Lesson 7.2 Mini Batch K-Means与DBSCAN密度聚类
|
算法 固态存储 计算机视觉
目标检测的Tricks | 【Trick3】IoU loss与focal loss(包含一些变体介绍)
目标检测的Tricks | 【Trick3】IoU loss与focal loss(包含一些变体介绍)
478 0
目标检测的Tricks | 【Trick3】IoU loss与focal loss(包含一些变体介绍)
|
Ubuntu Java Linux
开源线性规划求解器(Linear Programming solver)LP_Solve和CLP的PK
开源线性规划求解器(Linear Programming solver)LP_Solve和CLP的PK
2271 0
开源线性规划求解器(Linear Programming solver)LP_Solve和CLP的PK
下一篇
无影云桌面