深度学习入门笔记07(如何找到损失函数的最小值)

我们已经知道了如何求损失函数,包括用均方误差和交叉熵误差来求,我们还了解了为什么需要引入损失函数。

接下来的重点是如何找到损失函数的最小值。为了解决这个问题,我们需要了解一些数学知识。

数值微分

导数的定义其实就是函数在某一点的切线斜率,我们看到两个点之间的直线斜率的表示就是[f(x+h)-f(x)]/h,然后当h无限接近于0时,两个点几乎重合,我们可以把这个直线斜率看做在x这个点的切线斜率。

我们来看一个代码实现: 

这个代码实现有两个问题 ,一个是计算机的舍入误差的问题。如果用float32类型(32位的浮点数)来表示1e-50,就会变成0.0,无法正确表示出来。也就是说,使用过小的值会造成计算机出现计算上的问题。这是第一个需要改进的地方,即将微小值h改为10-4。使用10-4就可以得到正确的结果。

第二个问题是由于我们的h并不是无限接近于零,所以计算出的导数不是真正的导数,为了减少这个误差,我们可以计算函数f在(x+h)和(x-h)之间的差分。因为这种计算方法以x为中心,计算它左右两边的差分,所以也称为中心差分(而(x+h)和x之间的差分称为前向差分)。最后修正后的函数如下。

偏导数

我们先来看一个函数,这个函数有两个自变量,那么我们怎么求导数呢。其实这时候我们用到的是偏导数,当求f对x0的偏导时,把x1看作常数(也就是固定住x1)。同理求f对x1的偏导。

 梯度(gradient)

写求梯度的函数

什么是梯度,由全部变量的偏导数汇总而成的向量叫做梯度。(源代码文件ch04,gradient_2d.py)

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

gard = np.zeros_like(x)这一句是,生成一个形状与x相同,但是所有元素全部为零的数组,并将它赋值给gard。

x[idx]是取x的一个索引,其实x就是包含所有自变量的一个数组,这里是取出一个变量。

然后再让x[idx]=x+微小值(h)

再求出f(x),其实就是f(x+h).

之后同理,最后循环计算出f的所有偏导数,并且赋值给gard数组,也就是梯度。

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

当 X 是一维数组时,调用 _numerical_gradient_no_batch 函数实际上是在逐个处理这些潜在的 “多个自变量”(尽管以一维数组的形式呈现)。

当 X 是多维数组时,可以将其看作是更高维度的函数自变量集合。例如,每一个元素(可能是一个向量)可以被视为一个复杂的自变量结构,这个向量本身可能又包含多个子元素 。在这种情况下,函数可能是一个更加复杂的映射关系,接收这些多维的自变量集合,并产生相应的输出值。遍历多维数组 X 并对每个元素调用 _numerical_gradient_no_batch 函数,实际上是在尝试计算每个复杂自变量向量对于函数值变化的影响,也就是计算在这个高维空间中的梯度。

所以两个函数的遍历是不一样的。

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

这个函数function_2(x)可能是用于计算损失函数的函数。输入x可能是记录了误差的数组。

当x的维度只有1时,计算全部x的和。这里联想到前一节损失函数介绍的内容,当时我们举了手写数字识别的例子。x这个一维数组,记录的可能是一个图像的10个分类输出于真实值的误差。

当x的维度大于1,计算x的每一行数据的和,这里的一行就是一个图像,很多行就是很多个图像。

做图像

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()

X,Y = np.meshgrid(x0,x1)这个里, X是二维变量,Y是一维变量

flatten的功能是将多维数组,变成一维数组。

import numpy as np

arr = np.array([[1, 2, 3], [4, 5, 6]])

flattened_arr = arr.flatten()

print(flattened_arr) # [1 2 3 4 5 6]

 

 meshgrid的用法

二维图像

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
#定义数组
x0 = np.arange(-2, 2.5, 0.25)
x1 = np.arange(-2, 2.5, 0.25)
X, Y = np.meshgrid(x0, x1)

#创建三维图像对象
fig = plt.figure()
ax = fig.add_subplot(111,projection='3d')
#绘制三维网格点
ax.scatter(X,Y)
#标签
ax.set_xlabel('X')
ax.set_ylabel('Y')
#显示图像
plt.show()

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
#定义三维数组
x=np.arange(-3,3,1)
y=np.arange(-2,2,1)
z=np.arange(0,2,1)
#生成三维网络
X,Y,Z= np.meshgrid(x,y,z)
#创建三维图像对象
fig = plt.figure()
ax = fig.add_subplot(111,projection='3d')
#绘制三维网格点
ax.scatter(X,Y,Z)
#标签
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
#显示图像
plt.show()

梯度法

梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。

梯度为零的地方,可能是最小值点,可能是极小值(或者叫局部最小值),也可能是鞍点。

这该怎么办呢?

我们可以这样想,将一个多变量的函数不断放大,函数可以看成一个平面,我们在斜坡处放一个小球,小球就会沿着最陡的坡面滚动。

然后我们让小球下降一点点就让它停下来,重新从静止开始释放小球,乒乓球会从这个点再次沿着最陡的坡面开始滚动。经过多次这样的操作,小球就沿着最短路径达到了图像底部。这个方法被称为梯度下降法。

我们用数学公式来表示梯度下降法: 

 式(4.7)的η表示更新量,在神经网络的学习中,称为学习率(learning rate)。也可以叫这个学习率步伐,代表小球每一次停下,下降了多少。

以下是python实现:

import numpy as np
import sys
import os
sys.path.append(os.pardir)
from ch04.gradient_2d import numerical_gradient

def my_gardient_decent(f,init_x,lr=0.8,step_num=100):
    x=init_x
    for i in range(step_num):
        gard = numerical_gradient(f,x)
        x -= lr*gard
    return x
def f_3(x):
    return x[0]**2+x[1]**2
init_x =np.array([-3.0,4.0])
my_gardient_decent(f_3,init_x)
print(my_gardient_decent(f_3,init_x))

这里会打印结果

 

可见当学习率(lr=0.8)时,梯度下降法是很有效的。接近(0,0)我们试着改变以下学习率发现:

学习率过大时,移动的跨度很大,会发散成一个很大的值。

学习率过小时,移动的位置也很小,100次移动也很小 ,离原来的位置(-3,4)很近。

像学习率这种参数被称为超参数,它不同于权重与偏置,是通过机器学习得到的,学习率这种参数则需要人工调整。

神经网络的梯度

梯度是什么?偏导数组成的向量。神经网络的梯度是什么?对参数的偏导数组成的向量。

我们再来回忆一下为什么要用梯度下降法。首先机器学习是让机器找到合适的参数,得到一个好的模型。如何判断机器找到的参数好不好?我们可以看精确度,也可以计算误差,也就是损失函数。可是事实是,精确度对于参数的变化并不敏感。这时候损失函数的作用就非常大了。我们把参数看作自变量,进行微小的改变,损失函数可以灵敏的反映出这个微小的改变使误差变大了还是变小了。于是问题就转化为了,如何找到一个点(一组参数),使得损失函数最小。

综上,神经网络的梯度,就是损失函数对于参数的偏导数组成的向量。

代码实现:(此代码只是求出梯度,并没有用到梯度下降法来找到最优的w)

import sys, os
sys.path.append(os.pardir)  # 为了导入父目录中的文件而进行的设定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


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

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)

np.random.randn的用法 

 

f = lambda ...这句就是定义了一个 函数f = net.loss(x,t),也就是说f是损失函数。

最后print打印梯度值。(再次说明这里没有用到梯度下降。随机的w,对应得梯度)

 求出神经网络的梯度后,接下来只需根据梯度法,更新权重参数即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值