前面的博文里说了SGD,最基础的一个梯度下降优化算法,在SGD之后还有很多改进版本的算法,比如动量法,下面我降动量法扥别作用于两个函数,第一个是完美凸函数,第二个则是非凸的香蕉函数
动量法的参数更新公式:
第一个式子是说v是上一个v和刚计算出来的梯度的指数加权平均(但是这里我们不要求 α + η = 1 \alpha+\eta=1 α+η=1),上一个v的权重是 α \alpha α,即程序代码中的momentum参数, 而梯度的权重是 η \eta η,也就是学习率
通过v就引入了之前的梯度,包括大小和方向,所以不再像SGD那样,每次只考虑当前位置算出来的刚出炉的新鲜梯度,每次都独立的更新参数,而是把之前的更新记忆起来,指导现在的更新,新算出来的梯度的值反而占权重很小,对参数更新影响程度也小的多。
如果 α = 0.9 \alpha=0.9 α=0.9,则收敛比SGD近似快10倍
如果 α = 0.99 \alpha=0.99 α=0.99,则收敛比SGD近似快100倍
这个观点我在哪看到过,记不清了,以后弄清楚了过来记录
函数1:(Rosenbrock函数)
f ( x , y ) = ( a − x ) 2 + b ( y − x 2 ) 2 f(x,y)=(a-x)^2+b(y-x^2)^2 f(x,y)=(a−x)2+b(y−x2)2
取a=1,b=100
从下图可以看出,动量法确实比SGD快的多,我的另一篇讲SGD的博文有实验图和代码,学习率0.0035走10000步也还是离最小值点很远,而且后面由于梯度太小走的跟没走一样······而动量梯度下降(这里学习率0.001,动量0.9(即比普通梯度下降快10倍))只用不到1000步就到达最小值点了,但是动量法对于之字形下降缓解不大,只是加快收敛。下面代码中的参数可以改一改看看实验效果,学习率稍微再大一点比如0.0015,动量法都会学的太快导致跑出画布,毕竟跑得太远也不利于最终收敛,我们可以改小学习率,或者改大momentum参数(减小增速倍数)来控制学习速率。终于明白为啥别人都说深度学习就是调参了······如果使用完善的框架的话,不用自己搭网络,确实可以这么说。
# Momentum.py
# 动量梯度下降法应用于Rosenbrock函数
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
# 第一次调用
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[int(key)]
params[key] += self.v[key]
return params
# Rosenbrock函数
def func(x):
return (1 - x[0]) ** 2 + 100 * (x[1] - x[0]**2) ** 2
def gradient_descent(f, init_x, lr=0.1, step_num=100):
x = init_x
x_history = []
for i in range(step_num):
x_history.append(x.copy())
# 这里必须用x.copy()
# 否则最终x_history里所有数值都和最后一个数值相同
grad = numerical_gradient(f, x)
x -= lr * grad
return x, np.array(x_history)
# 把x_history转换为numpy数组
def momentum_update(init_x, stepnum):
x = init_x
x_history = []
for i in range(stepnum):
x_history.append(np.array(list(x.copy().values())))
grad = numerical_gradient(func, x)
x = m.update(x, grad)
return x, np.array(x_history)
def numerical_gradient(f, x):
h = 1e-4
x = np.array(list(init_x.values())) # 转换为ndarray
grad = np.zeros_like(x)
for idx in range(x.size):
temp = x[idx]
x[idx] = temp + h
fxh1 = f(x)
x[idx] = temp - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = temp
return grad
init_x = {} # 起始点
init_x['0'] = 0.7
init_x['1'] = -1.1
learning_rate = 0.001 # 学习率,再大就会导致一次参数更新跳到非常远的地方
m = Momentum(lr=learning_rate)
stepnum = 1000 # 沿着梯度走1000步,经过观察,rosenbrock函数走10000步也到不了最小点
# 前面梯度大,一次更新走很远,后面梯度小,几乎没怎么动
x, x_history = momentum_update(init_x=init_x, stepnum=stepnum)
x = np.arange(-5, 5, 0.05)
y = np.arange(-5, 10, 0.05)
X, Y = np.meshgrid(x, y)
z = np.array([X, Y])
# 画等高线
plt.figure()
plt.contour(x, y, func(z),np.arange(0,2500,100), zdir='z', cmap='binary')
# 画所有由梯度下降找到的点
plt.plot(x_history[:, 0], x_history[:, 1], '+', color='blue')
# 翻转坐标轴方向
ax = plt.gca()
ax.xaxis.set_ticks_position('top')
ax.invert_xaxis()
ax.yaxis.set_ticks_position('right')
ax.invert_yaxis()
# 画点间连线
for i in range(x_history.shape[0]-2):
tmp = x_history[i:i+2]
tmp = tmp.T
plt.plot(tmp[0], tmp[1], color='blue')
# 标注最小值位置
plt.plot(1, 1, '+', color='r')
plt.xlim(-5, 5)
plt.ylim(-5, 10)
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.title('Rosenbrock Zigzag ')
plt.show()
如果把学习率改为0.0005,就会是这样逼近最小值点,之字形的横向范围大大减小!!
函数2:
f ( x , y ) = x 2 + y 2 f(x,y)=x^2+y^2 f(x,y)=x2+y2
这种完美的凸函数,动量法则收敛超快,学习率0.004,30步就到了。
学习率再大容易错过全局最小,跑过了再反复震荡(下图是学习率0.005的情况),经过全局最小时却跑过了,回来又跑多了,来回震荡,所以随着训练时间逐渐调整学习率,快到最小值的时候减小学习率是有意义的
代码只是稍微改动了一点,参数和函数定义调用啥的:
# Momentum.py
# 动量梯度下降法
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
# 第一次调用
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[int(key)]
params[key] += self.v[key]
return params
# Rosenbrock函数
def func(x):
return (1 - x[0]) ** 2 + 100 * (x[1] - x[0]**2) ** 2
def func2(x):
return x[0] ** 2 + x[1] ** 2
def gradient_descent(f, init_x, lr=0.1, step_num=100):
x = init_x
x_history = []
for i in range(step_num):
x_history.append(x.copy())
# 这里必须用x.copy()
# 否则最终x_history里所有数值都和最后一个数值相同
grad = numerical_gradient(f, x)
x -= lr * grad
return x, np.array(x_history)
# 把x_history转换为numpy数组
def momentum_update(init_x, stepnum):
x = init_x
x_history = []
for i in range(stepnum):
x_history.append(np.array(list(x.copy().values())))
grad = numerical_gradient(func2, x)
x = m.update(x, grad)
return x, np.array(x_history)
def numerical_gradient(f, x):
h = 1e-4
x = np.array(list(init_x.values())) # 转换为ndarray
grad = np.zeros_like(x)
for idx in range(x.size):
temp = x[idx]
x[idx] = temp + h
fxh1 = f(x)
x[idx] = temp - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = temp
return grad
init_x = {} # 起始点
init_x['0'] = -4.5
init_x['1'] = -4.5
learning_rate = 0.004 # 学习率,再大就会导致一次参数更新跳到非常远的地方
m = Momentum(lr=learning_rate)
stepnum = 30 # 沿着梯度走1000步,经过观察,rosenbrock函数走10000步也到不了最小点
# 前面梯度大,一次更新走很远,后面梯度小,几乎没怎么动
x, x_history = momentum_update(init_x=init_x, stepnum=stepnum)
x = np.arange(-5, 5, 0.05)
y = np.arange(-5, 5, 0.05)
X, Y = np.meshgrid(x, y)
z = np.array([X, Y])
# 画等高线
plt.figure()
plt.contour(x, y, func2(z),np.arange(0,50,5), zdir='z', cmap='binary')
# 画所有由梯度下降找到的点
plt.plot(x_history[:, 0], x_history[:, 1], '+', color='blue')
# 翻转坐标轴方向
ax = plt.gca()
ax.xaxis.set_ticks_position('top')
ax.invert_xaxis()
ax.yaxis.set_ticks_position('right')
ax.invert_yaxis()
# 画点间连线
for i in range(x_history.shape[0]-2):
tmp = x_history[i:i+2]
tmp = tmp.T
plt.plot(tmp[0], tmp[1], color='blue')
# 标注最小值位置
plt.plot(0, 0, 'o', color='r')
plt.xlim(-5, 5)
plt.ylim(-5, 5)
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.title('x^2 + y^2 ')
plt.show()
对比我另一个讲SGD的博客里的同一个函数使用SGD的收敛过程:(SGD有很多之字形,动量SGD有所缓解并且加快收敛)
调出bug的我眼泪掉下来·····
这次又把宝贵的时间浪费在小数点上了
init_x['0'] = -7.0
init_x['1'] = 2.0
我写成
init_x['0'] = -7
init_x['1'] = 2
于是梯度算出来都是好几万好几万的,一开始我没单步调,在那边疯狂改学习率和momentum参数,发现怎么调结果都不按照我想要的样子显示···最后pycharm崩了······重新打开,单步一看,mmp,第三次跳进同一个坑里
代码:
# Momentum.py
# 动量梯度下降法应用于Rosenbrock函数
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
# 第一次调用
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[int(key)]
params[key] += self.v[key]
return params
def func2(x):
return (x[0]**2) / 20 + x[1] ** 2
def gradient_descent(f, init_x, lr=0.1, step_num=100):
x = init_x
x_history = []
for i in range(step_num):
x_history.append(x.copy())
# 这里必须用x.copy()
# 否则最终x_history里所有数值都和最后一个数值相同
grad = numerical_gradient(f, x)
x -= lr * grad
return x, np.array(x_history)
# 把x_history转换为numpy数组
def momentum_update(init_x, stepnum):
x = init_x
x_history = []
for i in range(stepnum):
x_history.append(np.array(list(x.copy().values())))
grad = numerical_gradient(func2, x)
x = m.update(x, grad)
return x, np.array(x_history)
def numerical_gradient(f, x):
h = 1e-4
x = np.array(list(init_x.values())) # 转换为ndarray
grad = np.zeros_like(x)
for idx in range(x.size):
temp = x[idx]
x[idx] = temp + h
fxh1 = f(x)
x[idx] = temp - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = temp
return grad
init_x = {} # 起始点
init_x['0'] = -7.0
init_x['1'] = 2.0
learning_rate = 0.1 # 学习率,再大就会导致一次参数更新跳到非常远的地方
m = Momentum(lr=learning_rate)
stepnum = 25 # 沿着梯度走1000步,经过观察,rosenbrock函数走10000步也到不了最小点
# 前面梯度大,一次更新走很远,后面梯度小,几乎没怎么动
x, x_history = momentum_update(init_x=init_x, stepnum=stepnum)
axis_range = 10
x = np.arange(-axis_range, axis_range, 0.05)
y = np.arange(-axis_range, axis_range, 0.05)
X, Y = np.meshgrid(x, y)
z = np.array([X, Y])
# 画等高线
plt.figure()
plt.contour(x, y, func2(z),np.arange(0,10,2), zdir='z', cmap='binary')
# 画所有由梯度下降找到的点
plt.plot(x_history[:, 0], x_history[:, 1], '+', color='blue')
# 画点间连线
for i in range(x_history.shape[0]-2):
tmp = x_history[i:i+2]
tmp = tmp.T
plt.plot(tmp[0], tmp[1], color='blue')
# 标注最小值位置
plt.plot(0, 0, 'o', color='r')
#plt.xlim(-axis_range, axis_range)
#plt.ylim(-axis_range, axis_range)
plt.xlabel('x')
plt.ylabel('y')
plt.title('0.05x^2 + y^2 ')
plt.show()
实验说明动量法还是比SGD好很多的,SGD连最小值的身都近不了···当然这么说很笼统,没有具体分析,我对动量法的理论部分也还没完全搞懂,以后再补充。