8.梯度

1.梯度:

上一篇中我们按变量分别计算了x0和x1的偏导数,但是我们希望的是一起计算x0和x1的偏导数。比如说,我们希望求得x0=3、x1=4时的偏导数。

$$\left( \frac{\partial f}{\partial x_0}\text{ ,}\frac{\partial f}{\partial x_1} \right) $$

这样由全部变量的偏导数汇总而成的向量就称为“梯度(gradient)”。

import numpy as np

# 定义函数
def function(x):
	return x[0]**2 + x[1]**2
	# 等同于下面的方法
	# return np.sum(x**2)

# 定义梯度求解函数
def numerical_gradient(f,x):
	h = 1e-4
	# 生成一个和x形状一样的数组
	grad = np.zeros_like(x)

	for idx in range(x.size):
		tmp_val = x[idx]
		# 计算f(x+h)
		x[idx] =tmp_val + h
		f_xh1 = f(x)

		# 计算f(x+h)
		x[idx] =tmp_val - h
		f_xh2 = f(x)

		grad[idx] = (f_xh1 - f_xh2) / (2*h)
		# 还原值
		# x[idx] = tmp_val
	return grad

# 求解函数在x0=3、x1=4处的梯度
print(numerical_gradient(function,np.array([3.0,4.0])))

运行得到结果

[6. 8.]

这里之所以显示为[6. 8.],是因为在输出numpy数组时,数值会被显示成“易读”的形式。

为了更形象解释,我们将函数的梯度画图表达。

import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D


def _numerical_gradient_no_batch(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    for idx in range(x.size):
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 还原值
        
    return grad


def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)
        
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)
        
        return grad


def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)


def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y
     
if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)
    
    X = X.flatten()
    Y = Y.flatten()
    
    grad = numerical_gradient(function_2, np.array([X, Y]) )
    
    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.legend()
    plt.draw()
    plt.show()

运行得到结果:

不过,需要注意的是,图中的元素是元素值为负梯度的向量(也就是后续提到的梯度法中变量的更新方向)。
可以从图中看出,函数的梯度呈现为有向向量(箭头),并且所有箭头都指向函数的最小值处,并且离最小值处越远,箭头越长。

梯度指示的方向就是各点处的函数值减小最多的方向。

2.梯度法:

神经网络的主要目标就是寻找使得损失函数最小时的最优参数,通过使用梯度来寻找函数最小值或者尽可能小的值(因为梯度表示的是各点处的函数值减小最多的方向,但是没办法保证梯度所指的方向就是函数的最小值)的方法就是梯度法。

梯度法(gradient method)其实就是通过不断的沿着梯度的方向前进,逐渐减小函数值的过程。

根据求解问题不同,梯度法的叫法也不相同。寻找最小值的梯度法称为梯度下降法(gradient descent method),而寻找最大值的梯度法称为梯度上升法(gradient ascent method)。但是可以通过反转损失函数的符号,使得两者变成一样的问题,也就是寻找最小值。

一般来说,神经网络中,梯度法主要是指梯度下降法。

用数学式子表达梯度法如下所示:

$$
x_0=x_0-\eta \frac{\partial f}{\partial x_0}
$$

$$
x_1=x_1-\eta \frac{\partial f}{\partial x_1}
$$

其中η表示更新量,即在多大程度上改变参数,在神经网络中称为学习率(learning rate),在神经网络中学习率需要不断调整从而找打合适的取值。

我们现在通过代码实现用梯度法求解函数的最小值。

import numpy as np

# 定义函数
def function(x):
	return x[0]**2 + x[1]**2
	# 等同于下面的方法
	# return np.sum(x**2)

# 定义梯度求解函数
def numerical_gradient(f,x):
	h = 1e-4
	# 生成一个和x形状一样的数组
	grad = np.zeros_like(x)

	for idx in range(x.size):
		tmp_val = x[idx]
		# 计算f(x+h)
		x[idx] =tmp_val + h
		f_xh1 = f(x)

		# 计算f(x+h)
		x[idx] =tmp_val - h
		f_xh2 = f(x)

		grad[idx] = (f_xh1 - f_xh2) / (2*h)
		# 还原值
		x[idx] = tmp_val

	return grad


# 定义实现梯度下降法函数
def gradient_descent(f, init_x, lr, step_num):
	x = init_x
	for i in range(step_num):
		grad = numerical_gradient(f,x)
		x -= lr*grad
	return x

# 使用梯度法求函数的最小值
print(gradient_descent(function,init_x=np.array([-3.0,4.0]),lr=0.1,step_num=100))

运行得到结果

[-6.11110793e-10  8.14814391e-10]

可以看到结果已经非常接近(0,0)了,所以说通过梯度法基本得到了正确的结果。
下面通过代码用图来表示梯度法的更新过程。

import numpy as np
import matplotlib.pylab as plt

# 定义梯度求解函数
def numerical_gradient(f,x):
	h = 1e-4
	# 生成一个和x形状一样的数组
	grad = np.zeros_like(x)

	for idx in range(x.size):
		tmp_val = x[idx]
		# 计算f(x+h)
		x[idx] =tmp_val + h
		f_xh1 = f(x)

		# 计算f(x+h)
		x[idx] =tmp_val - h
		f_xh2 = f(x)

		grad[idx] = (f_xh1 - f_xh2) / (2*h)
		# 还原值
		x[idx] = tmp_val

	return grad

# 定义实现梯度下降法函数
def gradient_descent(f, init_x, lr=0.01, step_num=100):
	x = init_x
	# 保存每次更新得到的值
	x_history = []

	for i in range(step_num):
		x_history.append(x.copy())

		grad = numerical_gradient(f, x)
		x -= lr * grad
	return x, np.array(x_history)

# 定义函数
def function_2(x):
	return x[0] ** 2 + x[1] ** 2

init_x = np.array([-3.0, 4.0])    

lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)
print(x)
print(x_history)

# 画图
plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()

运行得到结果

可以看出,通过梯度法,函数的取值逐渐向最小值处靠近。

3.神经网络的梯度:

神经网络的梯度是指损失函数关于权重参数的梯度,例如,有一个形状为2×3的权重W的神经网络,损失函数用L表示,此时,神经网络的梯度用数学表达式表示如下:

下面用一个简单的神经网络来实现求解梯度的过程。

import numpy as np

# 定义softmax函数
def softmax(x):
	if x.ndim == 2:
		x = x.T
		x = x - np.max(x, axis=0)
		y = np.exp(x) / np.sum(np.exp(x), axis=0)
		return y.T

	x = x - np.max(x)  # 溢出对策
	return np.exp(x) / np.sum(np.exp(x))

# 定义交叉熵误差函数
def cross_entropy_error(y, t):
	if y.ndim == 1:
		t = t.reshape(1, t.size)
		y = y.reshape(1, y.size)

	# 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
	if t.size == y.size:
		t = t.argmax(axis=1)

	batch_size = y.shape[0]
	return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

# 定义梯度求解函数
def numerical_gradient(f, x):
	h = 1e-4  # 0.0001
	grad = np.zeros_like(x)

	it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
	while not it.finished:
		idx = it.multi_index
		tmp_val = x[idx]
		x[idx] = float(tmp_val) + h
		fxh1 = f(x)  # f(x+h)

		x[idx] = tmp_val - h
		fxh2 = f(x)  # f(x-h)
		grad[idx] = (fxh1 - fxh2) / (2 * h)

		x[idx] = tmp_val  # 还原值
		it.iternext()
	return grad

# 定义类
class simpleNet:
	def __init__(self):
		# 对W进行初始化
		self.W = np.random.randn(2, 3)

	# 定义预测函数
	def predict(self, x):
		return np.dot(x, self.W)

	# 定义损失函数
	def loss(self, x, t):
		z = self.predict(x)
		y = softmax(z)
		loss = cross_entropy_error(y, t)
		return loss

# 给定输入x的值
x = np.array([0.6, 0.9])
# 给定正确的标签
t = np.array([0, 0, 1])

net = simpleNet()
print("W初始化值为:", net.W)

# 计算梯度,先定义一个计算损失函数的新函数f
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print("求得的梯度为:", dW)

运行得到结果

W初始化值为: [[ 0.86595961  0.87388942 -1.2888172 ]
 [-0.66973145 -1.86272037  0.85993859]]
求得的梯度为: [[ 0.24682956  0.0847551  -0.33158467]
 [ 0.37024435  0.12713265 -0.497377  ]]

通过上述代码我们求得了梯度,然后利用求得的梯度,来更新权重参数即可。

Reference:
《Deep Learning from Scratch》

RELATED ARTICLES

欢迎留下您的宝贵建议

Please enter your comment!
Please enter your name here

- Advertisment -

Most Popular

Recent Comments