神经网络的梯度实现

时间:2022-10-16 15:54:53

梯度

上篇文章我们按变量分别计算了x0和x1的偏导数。现在,我们希望一起计算x0和x1的偏导数。比如,我们来考虑求x0 = 3, x1 = 4时(x0, x1)的偏导数(αf/αx0,αf/αx1) 。这样的由全部变量的偏导数汇总而成的向量称为梯度(gradient)。梯度可以像下面这样来实现。

import numpy as np


def function_2(x):
    return x[0] ** 2 + x[1] ** 2


def numerical_gradient(f, x):
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x)  # 生成和x形状相同的数组
    for idx in range(x.size):  # 如X[3,4],idx=0,1
        tmp_val = x[idx]
        x[idx] = tmp_val + h  # f(x+h)的计算
        fxh1 = f(x)
        x[idx] = tmp_val - h  # f(x-h)的计算
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2 * h)  # 梯度的计算
        x[idx] = tmp_val  # 还原X[3,4]
    return grad


print(numerical_gradient(function_2, np.array([3.0, 4.0])))  # [6. 8.]

为了更好地理解,我们把f(x0,x1)=x02+x12的梯度画在图上。不过,这里我们画的是元素值为负梯度的向量,负梯度方向是梯度法中变量的更新方向。
神经网络的梯度实现
可以看出,梯度会指向各点处的函数值降低的方向。更严格地讲,梯度指示的方向是各点处的函数值减小最多的方向。

梯度法

机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数。取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。

在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。像这样,通过不断地沿梯度方向前进,逐渐减小函数值的过程就是梯度法(gradient method)。梯度法是解决机器学习中最优化问题的常用方法,特别是在神经网络的学习中经常被使用。根据目的是寻找最小值还是最大值,梯度法的叫法有所不同。严格地讲,寻找最小值的梯度法称为梯度下降法(gradient descent method),寻找最大值的梯度法称为梯度上升法(gradient ascent method)。但是通过反转损失函数的符号,求最小值的问题和求最大值的问题会变成相同的问题,因此“下降”还是“上升”的差异本质上并不重要。一般来说,神经网络(深度学习)中,梯度法主要是指梯度下降法。

梯度法的数学式如下:
神经网络的梯度实现
上式中的η表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。该式是表示更新一次的式子,这个步骤会反复执行。也就是说,每
一步都按该式更新变量的值,通过反复执行此步骤,逐渐减小函数值。虽然这里只展示了有两个变量时的更新过程,但是即便增加变量的数量,也可以通过类似的式子(各个变量的偏导数)进行更新。
学习率需要事先确定为某个值,比如0.01或0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。下面,我们用Python来实现梯度下降法,如下所示:

import numpy as np


def function_2(x):
    return x[0] ** 2 + x[1] ** 2


def numerical_gradient(f, x):
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x)  # 生成和x形状相同的数组
    for idx in range(x.size):  # 如X[3,4],idx=0,1
        tmp_val = x[idx]
        x[idx] = tmp_val + h  # f(x+h)的计算
        fxh1 = f(x)
        x[idx] = tmp_val - h  # f(x-h)的计算
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2 * h)  # 梯度的计算
        x[idx] = tmp_val  # 还原X[3,4]
    return grad


def gradient_descent(f, init_x, lr=0.01, step_num=200):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x


print(gradient_descent(function_2, np.array([-3.0, 4.0])))  # [-0.05276384  0.07035179]
print(gradient_descent(function_2, np.array([-3.0, 4.0]), lr=10))  # [-2.58983747e+13 -1.29524862e+12]
print(gradient_descent(function_2, np.array([-3.0, 4.0]), lr=1e-4))  # [-2.88235679  3.84314238]

可以看出,学习率过大的话,会发散成一个很大的值;反过来,学习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率是一个很重要的问题。
像学习率这样的参数称为超参数。这是一种和神经网络的参数(权重和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。

神经网络的梯度实现

神经网络的学习也要求梯度。这里所说的梯度是指损失函数关于权重参数的梯度。比如,有一个只有一个形状为2 × 3的权重W的神经网络,损失函数用L表示。此时,梯度可以用αL/αW表示。用数学式表示如下:
神经网络的梯度实现
下面,我们以一个简单的神经网络为例,来实现求梯度的代码。为此,我们要实现一个名为simpleNet的类:

class simpleNet:
    def __init__(self):
        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

为什么要建立一个类呢?我们要求L和W的梯度,而我们的损失函数与x和t相关,t是正确解标签,是常数,所以问题在于怎样将w与x联系起来。所以建立一个类,求损失函数时,w改变x随之改变。程序完整实现如下:

import os
import sys

import numpy as np

sys.path.append(os.pardir)


def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a - c)  # 溢出对策
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y


def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    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)  # 生成和x形状相同的数组
    for idx in range(x.size):  # 如X[3,4],idx=0,1
        tmp_val = x[idx]
        x[idx] = tmp_val + h  # f(x+h)的计算
        fxh1 = f(x)
        x[idx] = tmp_val - h  # f(x-h)的计算
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2 * h)  # 梯度的计算
        x[idx] = tmp_val  # 还原X[3,4]
    return grad


def numerical_gradient_2d(f, x):
    if x.ndim == 1:
        return numerical_gradient(f, x)
    else:
        grad = np.zeros_like(x)

        for idx, x in enumerate(x):
            grad[idx] = numerical_gradient(f, x)
    return grad


class simpleNet:
    def __init__(self):
        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


net = simpleNet()
x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])  # 正确解标签

f = lambda w:net.loss(x, t)
dw = numerical_gradient_2d(f,net.W)
print(dw)