Python | 从零实现10种优化算法并比较
🚀SGD
算法原理
随机梯度下降(Stochastic Gradient Descent,SGD)是一种基于梯度的优化算法,用于在机器学习和深度学习中最小化目标函数。以下是其基本原理和参数更新数学公式的简要说明:
-
基本原理: SGD旨在通过迭代的方式,沿着目标函数梯度的反方向更新模型参数,以逐步找到使目标函数最小化的最优参数值。
-
在每次迭代中,随机选取一个训练样本,计算该样本对应的损失函数关于模型参数的梯度,然后根据这个梯度来更新模型参数。通过不断地重复这个过程,模型参数逐渐向最优解靠近,使得损失函数的值不断减小。
-
参数更新数学公式:设 θ \theta θ 为模型参数, J ( θ ) J(\theta) J(θ)为目标函数, η \eta η为学习率,在第 t t t次迭代时,随机选取一个样本 ( x i , y i ) (x_i, y_i) (xi,yi),计算该样本损失函数 L ( y i , f ( x i ; θ ) ) L(y_i, f(x_i; \theta)) L(yi,f(xi;θ))关于参数 θ \theta θ的梯度 ∇ L ( y i , f ( x i ; θ ) ) \nabla L(y_i, f(x_i; \theta)) ∇L(yi,f(xi;θ)),则参数更新公式为:
θ t + 1 = θ t − η ∇ L ( y i , f ( x i ; θ t ) ) \theta_{t + 1}=\theta_t - \eta \nabla L(y_i, f(x_i; \theta_t)) θt+1=θt−η∇L(yi,f(xi;θt))
其中, θ t \theta_t θt是第 t t t次迭代时的参数值, θ t + 1 \theta_{t + 1} θt+1是第 t + 1 t + 1 t+1次迭代时的参数值。学习率 η \eta η是一个超参数,用于控制参数更新的步长,通常需要根据具体问题进行调整。
代码实现
本文这里实际实现的是GD算法,因为样例优化问题比较简单: f ( x , y ) = 3 x 2 + 4 y 2 f(x,y)=3x^2+4y^2 f(x,y)=3x2+4y2
python">class SGD:def __init__(self, lr=0.01):self.lr = lrdef update(self, w, grad_w):return w - self.lr * grad_w
可视效果
优化的损失函数为: f ( x , y ) = 3 x 2 + 4 y 2 f(x,y)=3x^2+4y^2 f(x,y)=3x2+4y2
🚀 Adagrad
算法原理
- 基本原理:Adagrad根据每个参数的历史梯度平方和来调整学习率,对于频繁出现的参数,其学习率会逐渐减小,而对于不常出现的参数,学习率则相对较大,从而实现了自适应学习率的调整。
- 参数更新数学公式:
G t , i i = ∑ k = 1 t g k , i 2 G_{t,ii}=\sum_{k = 1}^{t}g_{k,i}^2 Gt,ii=∑k=1tgk,i2
θ t + 1 = θ t − η G t , i i + ϵ g t \theta_{t + 1}=\theta_t-\frac{\eta}{\sqrt{G_{t,ii}+\epsilon}}g_t θt+1=θt−Gt,ii+ϵηgt
其中, g k , i g_{k,i} gk,i是第 k k k步参数 i i i的梯度, G t , i i G_{t,ii} Gt,ii是参数 i i i到第 t t t步为止的梯度平方和, η \eta η是初始学习率, ϵ \epsilon ϵ是一个小常数,用于防止分母为零。
代码实现
python">import numpy as npclass Adagrad:def __init__(self, eps=1e-8):self.eps = epsself.G = Nonedef update(self, w, grad_w):if self.G is None:self.G = np.zeros_like(grad_w)self.G += grad_w ** 2dx = grad_w / (np.sqrt(self.G) + self.eps)return w - dx
可视效果
优化的损失函数为: f ( x , y ) = 3 x 2 + 4 y 2 f(x,y)=3x^2+4y^2 f(x,y)=3x2+4y2
🚀 Adadelta
算法原理
- 基本原理:Adadelta是Adagrad的一种改进算法,旨在解决Adagrad学习率单调递减的问题。它通过引入一个随时间衰减的平均梯度平方项和一个自适应的学习率来调整参数更新步长,使得在训练过程中学习率能够根据问题的复杂度自适应调整。
- 参数更新数学公式:
E [ g 2 ] t = ρ E [ g 2 ] t − 1 + ( 1 − ρ ) g t 2 E[g^2]_t=\rho E[g^2]_{t - 1}+(1-\rho)g_t^2 E[g2]t=ρE[g2]t−1+(1−ρ)gt2
Δ θ t = − E [ Δ θ 2 ] t − 1 + ϵ E [ g 2 ] t + ϵ g t \Delta\theta_t=-\frac{\sqrt{E[\Delta\theta^2]_{t - 1}+\epsilon}}{\sqrt{E[g^2]_t+\epsilon}}g_t Δθt=−E[g2]t+ϵE[Δθ2]t−1+ϵgt
θ t + 1 = θ t + Δ θ t \theta_{t + 1}=\theta_t+\Delta\theta_t θt+1=θt+Δθt
其中, g t g_t gt是第 t t t步的梯度, E [ g 2 ] t E[g^2]_t E[g2]t是梯度平方的指数移动平均, ρ \rho ρ是衰减率, Δ θ t \Delta\theta_t Δθt是参数更新量, E [ Δ θ 2 ] t − 1 E[\Delta\theta^2]_{t - 1} E[Δθ2]t−1是参数更新量平方的指数移动平均, ϵ \epsilon ϵ是一个小常数,用于防止分母为零。
代码实现
python">import numpy as npclass Adadelta:def __init__(self, rho=0.95, eps=1e-6):self.rho = rhoself.eps = epsself.Eg2 = Noneself.Edx2 = Noneself.delta_x = Nonedef update(self, w, grad_w):if self.Eg2 is None:self.Eg2 = np.zeros_like(grad_w)self.Edx2 = np.zeros_like(w)self.delta_x = np.zeros_like(w)self.Eg2 = self.rho * self.Eg2 + (1 - self.rho) * grad_w ** 2dx = np.sqrt((self.Edx2 + self.eps) / (self.Eg2 + self.eps)) * grad_wself.Edx2 = self.rho * self.Edx2 + (1 - self.rho) * dx ** 2self.delta_x = -dxreturn w + self.delta_x
可视效果
🚀 RMSProp
算法原理
- 基本原理:RMSProp通过对梯度平方的指数移动平均来调整学习率,使得学习率能够根据参数的重要性和梯度的变化情况自适应调整,在一定程度上解决了Adagrad中学习率单调递减的问题,提高了训练的稳定性和收敛速度。
- 参数更新数学公式:
E [ g 2 ] t = ρ E [ g 2 ] t − 1 + ( 1 − ρ ) g t 2 E[g^2]_t=\rho E[g^2]_{t - 1}+(1-\rho)g_t^2 E[g2]t=ρE[g2]t−1+(1−ρ)gt2
θ t + 1 = θ t − η E [ g 2 ] t + ϵ g t \theta_{t + 1}=\theta_t-\frac{\eta}{\sqrt{E[g^2]_t+\epsilon}}g_t θt+1=θt−E[g2]t+ϵηgt
其中,各参数含义与Adadelta中的类似。
代码实现
python">import numpy as npclass RMSProp:def __init__(self, lr=0.01, rho=0.9, eps=1e-8):self.lr = lrself.rho = rhoself.eps = epsself.Eg2 = Nonedef update(self, w, grad_w):if self.Eg2 is None:self.Eg2 = np.zeros_like(grad_w)self.Eg2 = self.rho * self.Eg2 + (1 - self.rho) * grad_w ** 2dx = self.lr * grad_w / (np.sqrt(self.Eg2) + self.eps)return w - dx
可视效果
🚀 Adam
算法原理
- 基本原理:Adam结合了Adagrad和RMSProp的优点,通过计算梯度的一阶矩估计(均值)和二阶矩估计(方差)来调整学习率,能够在训练初期利用较大的学习率快速收敛,在后期根据梯度的变化情况自适应地调整学习率,从而提高训练的稳定性和收敛速度。
- 参数更新数学公式:
m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t=\beta_1m_{t - 1}+(1-\beta_1)g_t mt=β1mt−1+(1−β1)gt
v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_t=\beta_2v_{t - 1}+(1-\beta_2)g_t^2 vt=β2vt−1+(1−β2)gt2
m ^ t = m t 1 − β 1 t \hat{m}_t=\frac{m_t}{1-\beta_1^t} m^t=1−β1tmt
v ^ t = v t 1 − β 2 t \hat{v}_t=\frac{v_t}{1-\beta_2^t} v^t=1−β2tvt
θ t + 1 = θ t − η v ^ t + ϵ m ^ t \theta_{t + 1}=\theta_t-\frac{\eta}{\sqrt{\hat{v}_t}+\epsilon}\hat{m}_t θt+1=θt−v^t+ϵηm^t
其中, m t m_t mt是一阶矩估计, v t v_t vt是二阶矩估计, β 1 \beta_1 β1和 β 2 \beta_2 β2是指数衰减率, m ^ t \hat{m}_t m^t和 v ^ t \hat{v}_t v^t是偏差修正后的一阶矩估计和二阶矩估计, η \eta η是学习率, ϵ \epsilon ϵ是一个小常数,用于防止分母为零。
代码实现
python">import numpy as npclass Adam:def __init__(self, lr=0.01, beta1=0.9, beta2=0.999, eps=1e-8):self.lr = lrself.beta1 = beta1self.beta2 = beta2self.eps = epsself.m = Noneself.v = Noneself.t = 0def update(self, w, grad_w):self.t += 1if self.m is None:self.m = np.zeros_like(grad_w)self.v = np.zeros_like(grad_w)self.m = self.beta1 * self.m + (1 - self.beta1) * grad_wself.v = self.beta2 * self.v + (1 - self.beta2) * grad_w ** 2m_hat = self.m / (1 - self.beta1 ** self.t)v_hat = self.v / (1 - self.beta2 ** self.t)dx = self.lr * m_hat / (np.sqrt(v_hat) + self.eps)return w - dx
可视效果
🚀 AdamW
算法原理
- 基本原理:AdamW在Adam的基础上,对权重衰减进行了更合理的处理,将权重衰减从优化器的参数更新中分离出来,直接作用于参数本身,而不是像传统的方法那样将权重衰减与学习率耦合在一起,从而在一定程度上提高了模型的泛化能力和训练效果。
- 参数更新数学公式:与Adam类似,只是在更新参数时多了一项权重衰减项:
θ t + 1 = θ t − η v ^ t + ϵ m ^ t − λ θ t \theta_{t + 1}=\theta_t-\frac{\eta}{\sqrt{\hat{v}_t}+\epsilon}\hat{m}_t-\lambda\theta_t θt+1=θt−v^t+ϵηm^t−λθt
其中, λ \lambda λ是权重衰减系数,其他参数含义与Adam中相同。
代码实现
python">import numpy as npclass AdamW:def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0.01):self.lr = lrself.beta1 = beta1self.beta2 = beta2self.eps = epsself.weight_decay = weight_decayself.m = Noneself.v = Noneself.t = 0def update(self, w, grad_w):self.t += 1if self.m is None:self.m = np.zeros_like(grad_w)self.v = np.zeros_like(grad_w)self.m = self.beta1 * self.m + (1 - self.beta1) * grad_wself.v = self.beta2 * self.v + (1 - self.beta2) * grad_w ** 2m_hat = self.m / (1 - self.beta1 ** self.t)v_hat = self.v / (1 - self.beta2 ** self.t)# 加入权重衰减项update = (self.lr * m_hat / (np.sqrt(v_hat) + self.eps)) + self.weight_decay * wreturn w - update
可视效果
🚀 SparseAdam
算法原理
- 基本原理:SparseAdam是Adam的一种变体,专门用于处理稀疏数据。它在计算梯度的一阶矩估计和二阶矩估计时,只对非零梯度进行更新,从而提高了计算效率,同时也能在一定程度上保证模型的训练效果。
- 参数更新数学公式:与Adam类似,但在计算 m t m_t mt和 v t v_t vt时只考虑非零梯度:
m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t=\beta_1m_{t - 1}+(1-\beta_1)g_t mt=β1mt−1+(1−β1)gt
v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_t=\beta_2v_{t - 1}+(1-\beta_2)g_t^2 vt=β2vt−1+(1−β2)gt2
m ^ t = m t 1 − β 1 t \hat{m}_t=\frac{m_t}{1-\beta_1^t} m^t=1−β1tmt
v ^ t = v t 1 − β 2 t \hat{v}_t=\frac{v_t}{1-\beta_2^t} v^t=1−β2tvt
θ t + 1 = θ t − η v ^ t + ϵ m ^ t \theta_{t + 1}=\theta_t-\frac{\eta}{\sqrt{\hat{v}_t}+\epsilon}\hat{m}_t θt+1=θt−v^t+ϵηm^t
其中, g t g_t gt为稀疏梯度,其他参数含义与Adam中相同。
代码实现
python">import numpy as npclass SparseAdam:def __init__(self, lr=0.01, beta1=0.9, beta2=0.999, eps=1e-8):self.lr = lrself.beta1 = beta1self.beta2 = beta2self.eps = epsself.m = Noneself.v = Noneself.t = 0def update(self, w, grad_w):self.t += 1if self.m is None:self.m = {}self.v = {}for index in np.ndindex(grad_w.shape):if index not in self.m:self.m[index] = np.zeros(1)self.v[index] = np.zeros(1)g = grad_w[index]self.m[index] = self.beta1 * self.m[index] + (1 - self.beta1) * gself.v[index] = self.beta2 * self.v[index] + (1 - self.beta2) * g ** 2m_hat = self.m[index] / (1 - self.beta1 ** self.t)v_hat = self.v[index] / (1 - self.beta2 ** self.t)dx = self.lr * m_hat / (np.sqrt(v_hat) + self.eps)w[index] -= dxreturn w
可视效果
🚀 Adamax
算法原理
- 基本原理:Adamax是Adam的一种变体,它对Adam中的二阶矩估计进行了修改,使用了无穷范数来代替原来的 L 2 L_2 L2范数,在一些情况下可能具有更好的性能和稳定性。
- 参数更新数学公式:
m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t=\beta_1m_{t - 1}+(1-\beta_1)g_t mt=β1mt−1+(1−β1)gt
u t = max ( β 2 u t − 1 , ∣ g t ∣ ) u_t=\max(\beta_2u_{t - 1},\vert g_t\vert) ut=max(β2ut−1,∣gt∣)
m ^ t = m t 1 − β 1 t \hat{m}_t=\frac{m_t}{1-\beta_1^t} m^t=1−β1tmt
θ t + 1 = θ t − η u t m ^ t \theta_{t + 1}=\theta_t-\frac{\eta}{u_t}\hat{m}_t θt+1=θt−utηm^t
其中, m t m_t mt、 β 1 \beta_1 β1、 m ^ t \hat{m}_t m^t的含义与Adam中相同, u t u_t ut是无穷范数的指数移动平均, ∣ g t ∣ \vert g_t\vert ∣gt∣是第 t t t步梯度的绝对值, η \eta η是学习率。
代码实现
python">import numpy as npclass Adamax:def __init__(self, lr=0.01, beta1=0.9, beta2=0.999, eps=1e-8):self.lr = lrself.beta1 = beta1self.beta2 = beta2self.eps = epsself.m = Noneself.u = Noneself.t = 0def update(self, w, grad_w):self.t += 1if self.m is None:self.m = np.zeros_like(grad_w)self.u = np.zeros_like(grad_w)self.m = self.beta1 * self.m + (1 - self.beta1) * grad_wself.u = np.maximum(self.beta2 * self.u, np.abs(grad_w))m_hat = self.m / (1 - self.beta1 ** self.t)dx = self.lr * m_hat / (self.u + self.eps)return w - dx
可视效果
🚀 NAdam
算法原理
- 基本原理:NAdam是在Adam的基础上结合了Nesterov加速梯度(NAG)的思想,通过对未来梯度的近似估计来调整参数更新的方向,使得参数更新能够更快速地向最优解收敛,进一步提高了训练的效率和收敛速度。
- 参数更新数学公式:
m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t=\beta_1m_{t - 1}+(1-\beta_1)g_t mt=β1mt−1+(1−β1)gt
m ^ t = m t 1 − β 1 t \hat{m}_t=\frac{m_t}{1-\beta_1^t} m^t=1−β1tmt
g t ′ = β 1 m t − 1 1 − β 1 t + ( 1 − β 1 ) g t g_t^\prime=\frac{\beta_1m_{t - 1}}{1-\beta_1^t}+(1-\beta_1)g_t gt′=1−β1tβ1mt−1+(1−β1)gt
v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_t=\beta_2v_{t - 1}+(1-\beta_2)g_t^2 vt=β2vt−1+(1−β2)gt2
v ^ t = v t 1 − β 2 t \hat{v}_t=\frac{v_t}{1-\beta_2^t} v^t=1−β2tvt
θ t + 1 = θ t − η v ^ t + ϵ g t ′ \theta_{t + 1}=\theta_t-\frac{\eta}{\sqrt{\hat{v}_t}+\epsilon}g_t^\prime θt+1=θt−v^t+ϵηgt′
其中,各参数含义与Adam中类似。
代码实现
python">import numpy as npclass NAdam:def __init__(self, lr=0.01, beta1=0.9, beta2=0.999, eps=1e-8):self.lr = lrself.beta1 = beta1self.beta2 = beta2self.eps = epsself.m = Noneself.v = Noneself.t = 0def update(self, w, grad_w):self.t += 1if self.m is None:self.m = np.zeros_like(grad_w)self.v = np.zeros_like(grad_w)self.m = self.beta1 * self.m + (1 - self.beta1) * grad_wself.v = self.beta2 * self.v + (1 - self.beta2) * grad_w ** 2m_hat = self.beta1 * self.m / (1 - self.beta1 ** self.t) + (1 - self.beta1) * grad_w / (1 - self.beta1 ** self.t)v_hat = self.v / (1 - self.beta2 ** self.t)dx = self.lr * m_hat / (np.sqrt(v_hat) + self.eps)return w - dx
可视效果
🚀 RProp
算法原理
- 基本原理:RProp基于梯度的符号来调整参数更新的步长,而不依赖于梯度的大小。如果当前梯度与上一次梯度的符号相同,则增加步长;如果符号相反,则减小步长。这种方法可以在一定程度上避免梯度消失或爆炸问题,提高训练的稳定性和收敛速度。
- 参数更新数学公式:
Δ θ t = { − η + Δ θ t − 1 , g t g t − 1 > 0 − η − Δ θ t − 1 , g t g t − 1 < 0 Δ θ t − 1 , g t g t − 1 = 0 \Delta\theta_t=\begin{cases}-\eta^+\Delta\theta_{t - 1},&g_tg_{t - 1}>0\\-\eta^-\Delta\theta_{t - 1},&g_tg_{t - 1}<0\\\Delta\theta_{t - 1},&g_tg_{t - 1}=0\end{cases} Δθt=⎩ ⎨ ⎧−η+Δθt−1,−η−Δθt−1,Δθt−1,gtgt−1>0gtgt−1<0gtgt−1=0
θ t + 1 = θ t + Δ θ t \theta_{t + 1}=\theta_t+\Delta\theta_t θt+1=θt+Δθt
其中, η + \eta^+ η+和 η − \eta^- η−分别是步长增加和减小的因子, g t g_t gt是第 t t t步的梯度, Δ θ t \Delta\theta_t Δθt是参数更新量。
代码实现
python">import numpy as npclass RProp:def __init__(self, lr_plus=1.2, lr_minus=0.5, delta_max=50, delta_min=1e-6, initial_delta=0.1):self.lr_plus = lr_plusself.lr_minus = lr_minusself.delta_max = delta_maxself.delta_min = delta_minself.initial_delta = initial_deltaself.delta = Noneself.sign_grad_prev = Nonedef update(self, w, grad_w):if self.delta is None:self.delta = np.ones_like(grad_w) * self.initial_deltaself.sign_grad_prev = np.sign(grad_w)sign_grad = np.sign(grad_w)self.delta *= (sign_grad == self.sign_grad_prev) * self.lr_plus + (sign_grad!= self.sign_grad_prev) * self.lr_minusself.delta = np.minimum(self.delta_max, np.maximum(self.delta_min, self.delta))self.sign_grad_prev = sign_graddx = -self.delta * sign_gradreturn w + dx
可视效果
🚀 代码结构
github项目链接
preject|--optim|--result|--main.py
python">import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3Dfrom optim import Adadelta, Adagrad, Adam, Adamax, AdamW, NAdam, RMSProp, RProp, SGD, SparseAdam# 定义二维损失函数
def loss_function(x, y):return 3 * x ** 2 + 4 * y ** 2# 计算损失函数关于x和y的偏导数(针对单个样本的情况)
def gradient(x, y):grad_x = 6 * xgrad_y = 8 * yreturn np.array([grad_x, grad_y])def train(optimizer_name, num_iterations=500):# 初始参数params = np.array([4.5, 4.5])x, y = params[0], params[1]# 创建优化器实例if optimizer_name == "Adadelta":optimizer = Adadelta()elif optimizer_name == "Adagrad":optimizer = Adagrad()elif optimizer_name == "Adam":optimizer = Adam()elif optimizer_name == "Adamax":optimizer = Adamax()elif optimizer_name == "AdamW":optimizer = AdamW()elif optimizer_name == "NAdam":optimizer = NAdam()elif optimizer_name == "RMSProp":optimizer = RMSProp()elif optimizer_name == "RProp":optimizer = RProp()elif optimizer_name == "SGD":optimizer = SGD()elif optimizer_name == "SparseAdam":optimizer = SparseAdam()else:raise Exception("Not Support This Optimizer!")# 用于存储每次迭代的x、y和损失值,方便可视化x_history = []y_history = []loss_history = []for _ in range(num_iterations):grads = gradient(x, y)params = np.array([x, y])updated_params = optimizer.update(params, grads)x, y = updated_params[0], updated_params[1]loss = loss_function(x, y)x_history.append(x)y_history.append(y)loss_history.append(loss)# 创建3D图形对象fig = plt.figure()ax = fig.add_subplot(111, projection='3d')# 生成用于绘制损失函数曲面的网格数据x_range = np.linspace(-5, 5, 100)y_range = np.linspace(-5, 5, 100)X, Y = np.meshgrid(x_range, y_range)Z = loss_function(X, Y)# 绘制损失函数曲面ax.plot_surface(X, Y, Z, alpha=0.5, cmap='viridis')# 绘制参数x、y变化以及对应损失函数值的散点图ax.plot(x_history, y_history, loss_history, c='r', marker='.', label=f'{optimizer_name} Optimization Path')# 设置坐标轴标签ax.set_xlabel('x')ax.set_ylabel('y')ax.set_zlabel('Loss')# 设置图形标题ax.set_title(f'Parameter and Loss Change with {optimizer_name}')# 添加图例ax.legend()# 显示图形plt.savefig(f'result\\{optimizer_name}.png')plt.close()# 可视化损失下降过程plt.plot(loss_history)plt.xlabel('Iteration')plt.ylabel('Loss')plt.title(f'Loss Decrease with {optimizer_name}')# 显示图形plt.savefig(f'result\\{optimizer_name}_loss.png')plt.close()if __name__ == "__main__":names = ["Adadelta", "Adagrad", "Adam", "Adamax", "AdamW", "NAdam", "RMSProp", "RProp", "SGD", "SparseAdam"]for name in names:train(optimizer_name=name)