1. 定义
EMA(Exponential Moving Average,指数移动平均)是一种加权平均方法,它给予最近的数据点更高的权重。
在深度学习训练中,我们维护一个“影子参数”,在每次网络参数更新之后,使用EMA对影子参数更新(注:影子参数不会参与训练,EMA并不会对训练中的模型参数产生影响)。通过这样的操作,我们最终得到的“影子参数”是所有时刻参数的加权平均值,且越往后的影子参数所占权重越大,这样可以增加模型的鲁棒性和稳定性。
v
t
=
β
v
t
−
1
+
(
1
−
β
)
θ
t
(1)
v_{t} = \beta v_{t-1} + (1 - \beta)\theta_{t}\tag{1}
vt=βvt−1+(1−β)θt(1)
其中,
v
t
v_{t}
vt 是前
t
t
t 时刻的“影子参数”的加权平均值,
v
t
−
1
v_{t-1}
vt−1 是前
t
−
1
t-1
t−1 的“影子参数”加权平均值,
θ
t
\theta_{t}
θt 是
t
t
t 时刻的网络参数,
β
\beta
β 是加权权重值, 一般取
[
0.9
,
0.9999
]
[0.9,0.9999]
[0.9,0.9999]。
另外,根据吴恩达老师的课程改善深层神经网络:超参数调试、正则化以及优化所讲,可以近似认为当前影子参数是最近的
n
=
1
/
(
1
−
β
)
n = 1/(1-\beta)
n=1/(1−β) 个影子参数的均值。这是因为可以计算出对于
v
t
v_{t}
vt ,
n
=
1
/
(
1
−
β
)
n = 1/(1-\beta)
n=1/(1−β) 时刻之前的平均值
v
t
−
n
v_{t-n}
vt−n 所占权重被衰减到了
1
/
e
1/e
1/e 。如下:
v
t
=
β
v
t
−
1
+
(
1
−
β
)
θ
t
=
β
n
v
t
−
n
+
(
1
−
β
)
∑
i
=
0
n
−
1
β
i
θ
t
−
i
(2)
\begin{align*} v_{t} &=\beta v_{t-1} + (1 - \beta)\theta_{t} \\ &=\beta^{n}v_{t-n} + (1-\beta)\sum^{n-1}_{i=0}\beta^{i}\theta_{t-i}\\ \end{align*} \tag{2}
vt=βvt−1+(1−β)θt=βnvt−n+(1−β)i=0∑n−1βiθt−i(2)
其中,当
β
∈
[
0.9
,
0.9999
]
\beta\in[0.9, 0.9999]
β∈[0.9,0.9999]时,
β
n
=
β
1
1
−
β
≈
1
/
e
\beta^{n}=\beta^{\frac{1}{1-\beta}}\approx1/e
βn=β1−β1≈1/e。这可以说明此时
n
n
n 时刻之前的均值在当前参数所占权重已经很少。(不过个人感觉取
n
=
10
/
(
1
−
β
)
n = 10 / (1- \beta)
n=10/(1−β) 是不是更能说明问题?)
2. Pytroch实现以及使用
1. 实现
class EMA:
def __init__(self, model, decay=0.999, correction=False):
self.model = model
self.decay = decay
self.shadow = {} # 存储影子参数
self.backup = {} # 存储网络参数,用于restore
# 初始化
for name, param in model.named_parameters():
if param.requires_grad:
self.shadow[name] = param.data.clone()
self.correction = correction # 是否进行偏差修正
if self.correction:
self.step = 0 # 记录步数
# 每轮网络参数更新之后更新影子参数
def update(self):
if self.correction:
self.step += 1
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.shadow
self.shadow[name] = self.decay * self.shadow[name] + (1 - self.decay) * param.data.clone()
# 推理之前,将影子参数应用于网络
def apply_shadow(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.shadow
correction_factor = 1
if self.correction:
correction_factor = 1 - self.decay**self.step
self.backup[name] = param.data.clone()
param.data.copy_(self.shadow[name] / correction_factor)
# 推理之后,将网络参数恢复
def restore(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.backup
param.data.copy_(self.backup[name])
self.backup = {}
2. 使用
- 模型训练开始之前进行初始化
ema = EMA(model, decay=0.999)
- 每轮训练结束进行更新
def train(...):
...
for epoch in range(num_epochs):
for batch in train_loader:
...
optimizer.step()
ema.update()
- 评估前进行应用,评估完之后恢复参数
def evaluate(...):
...
ema.apply_shadow()
...
ema.restore()