深度学习入门笔记(四)(上)

深度学习入门笔记(四)

神经网络的学习

这一节主要讲解神经网络的学习,这里所说的“学习”是指从训练数据中自动获取最优权重参数的过程。为了使神经网络能够进行学习,将导入损失函数这一指标。学习的目的就是以损失函数为基准,找出能使损失函数的值达到最小的权重参数。

一.从数据中学习
1.数据驱动

先来思考一个具体的问题,比如如何实现数字“5”的识别。数字5是手写图像,我们的目标是实现能区别是否是5的程序。
在这里插入图片描述

我们可以通过有效利用数据来解决这个问题。一种方案是,先从图像中提取特征量,再用机器学习技术学习这些特征量的模式。这里所说的“特征量”是指可以从输入数据中准确地提取本质数据(重要数据)的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括SIFT、SURF和HOG等。使用这些特征量将图像数据转换为常量,然后对转换后的向量使用机器学习中的SVM、KNN等分类器进行学习。
机器学习的方法中,由机器从收集到的数据中找出规律性。与从零开始相除算法相比,这种方法可以更高效的解决问题,也能减轻人的负担。但是需要注意的是,将图像转换为向量时使用的特征量仍是由人设计的。对于不同的问题,必须使用合适的特征量(必须设计专门的特征量),才能得到好的结果。
而另一种方法为使用神经网络直接学习图像本身,图像中包含的重要特征量也都是由机器来学习。
在这里插入图片描述

深度学习有时也称为端到端机器学习(end-to-end machine learning)。这里所说的端到端是指从一端到另一端的意思,也就是从原始数据(输入)中获得目标结果(输出)的意思。

神经网络的优点是对所有的问题都可以用同样的流程来解决。也就是说,与待处理的问题我管,神经网络可以将数据直接作为初试数据,进行端到端的学习。

2.训练数据和测试数据

机器学习中,一般将数据分为训练数据测试数据两部分来进行学习和实验。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。为什么需要将数据分为训练数据和测试数据呢?因为我们追求的是模型的泛化能力。为了正确评价模型的泛化能力,就必须划分为训练数据和测试数据。训练数据也可以叫做监督数据

泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。仅仅用一个数据集去学习和评价参数,是无法进行正确评价的。这样会导致可以顺利地处理某个数据集,但无法处理其他数据集的情况。支队某个数据集过度拟合的状态叫做过拟合。避免过拟合是机器学习的重要课题。

二. 损失函数

神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。这个指标就叫做损失函数。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。

损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。以“性能的恶劣程度”为指标可能会使人感到不太自然,但是如果乘上一个负值,就可以解释为“在多大程度上不坏”,即性能有多好。并且,“使性能的恶劣程度达到最小”和“使性能的优良程度达到最大”是等价的,不管是用“恶劣程度”还是“优良程度”,做的事情本质上都是一样的。

1.均方误差

在这里插入图片描述

其中,yk是表示神经网络的输出,tk表示监督数据,k表示数据的维数。
比如:

>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

数组元素的索引从第一个开始一次对应数字“0” “1” “2” ······这里神经网络的输出y是softma函数的输出。由于softmax函数的输出可以理解为概率,因此上例表示“0”的概率是0.1,“1”的概率是0.05等。t是监督数据,将正确解标签表示为1,其他标签表示为0的表示方法称为one-hot表示
均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,然后求和。用python实现:

def mean_squared_error(y, t):
    return 0.5 * np.sum((y-t)**2)

这里,参数y和t是numpy数组。
在这里插入图片描述

均方误差显示第一个例子的输出结果与监督数据更加吻合。

2.交叉熵误差

在这里插入图片描述

这里,log表示以e为底数的自然对数(loge)。yk是神经网络的输出,tk是正确解标签。并且,tk中只有正确解标签的索引为1,其他均为0(one-hot表示)。因此,公式实际上只计算对应正确解标签的输出的自然对数。比如,假设正确姐标签的索引是“2”,与之对应的神经网络的输出是0.6,则交叉熵误差是-log0.6 = 0.51。也就是,交叉熵误差的值是由正确解标签所对应的输出结果决定的。
自然对数图像:
在这里插入图片描述

自然对数y = log x 的图像

如上图,x等于1时,y为0;随着x向0靠近,y逐渐变小。因此, 正确解标签对应的输出越大,交叉熵误差的值越接近0;当输出为1时,交叉熵误差为0。此外,如果正确解标签对应的输出较小,则交叉熵误差的值较大。

def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta))

同样,这里的参数y和t是numpy数组。函数内部在计算np.log时,加上了一个微小值delta。这是因为,当出现np.log(0)时,np.log(0)会变为负无限大的-inf,这样一来就会导致后续计算无法进行。下面,使用cross_entropy_error(y, t)做一些简单计算。
在这里插入图片描述

这个例子中,正确解标签对应的输出为0.6,此时的交叉熵误差大约为0.51。第二个例子中,正确解标签对应的输出为0.1的低值,此时的交叉熵误差大约为2.3。

3.mini-batch学习

机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。
前面介绍的两种损失函数都是针对单个数据的损失函数。如果要求所有的损失函数的总和,以交叉熵误差为例,可以写成下面的式子:
在这里插入图片描述

这里,假设数据有N个,tnk表示第n个数据的第k个元素的值(ynk时神经网络的输出,tnk是监督数据)。

另外,MNIST数据集的训练数据有60000个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。而且,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据作为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。比如,从60000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习
下面来编写从训练数据中随机选择指定个数的数据的代码。先来看读入MNIST数据集的代码。

import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = \
    load_mnist(normalize=True, one_hot_label=True)

print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000, 10)

load_mnist函数是用于读入MNIST数据集的函数。它会读入训练和测试数据。读入数据时,通过设定参数one_hot_label=True,可以得到one-hot表示(即仅正确解标签为1,其余为0的数据结构)。 读入上面的mnist数据后,训练数据有60000个,输入数据是784维(28×28)的图像数据,监督数据是10维的数据。因此,上面的x_train、t_train的形状分别是(60000, 784)和(60000, 10)。
我们可以使用numpy中的np.random.choice()来进行随机抽取固定数目的数据。

train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_batch[batch_mask]

使用np.random.choice()可以从指定的数字中随机选择想要的数字。比如,np.random.choice(60000, 10)会从0到59999之间随机选择10个数字。如下面的实际代码所示:

>>> np,random.choice(60000, 10)
array([ 8013, 14666, 58210, 23832, 52091, 10153, 8107, 19410, 27260, 21411])

之后,我们只需要指定这些随机选出的索引,去除mini-batch,然后使用这个mini-batch计算损失函数即可。

4.mini-batch版本交叉熵误差的实现

只要改良一下之前实现的对应单个数据的交叉熵误差就可以了。这里,我们来实现一个可以同时处理单个数据和批量数据(数据作为batch集中输入)两种情况的函数。

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(t * np.log(y + 1e-7)) / batch_size

这里,y是神经网络的输出,t是监督数据。y的维度为1时,即求单个数据的交叉熵误差时,需要改变数据的形状。并且,当输入为mini-batch时,要用batch的个数进行正规化,计算单个数据的平均交叉熵误差。
此外,当监督数据是标签形式而非one-hot表示形式时,交叉熵误差可如下实现:

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

实现的要点是,由于one-hot表示中t为0的元素的交叉熵也为0,因此针对这些元素的计算可以忽略。换言之,如果可以获得神经网络在正确解标签处的输出,就可以计算交叉熵误差。因此,t为one-hot表示时通过t * np.log(y)计算的地方,在t为标签形式时,可用np.log(y[np.arange(batch_size), t])实现相同的处理。
np.arange(batch_size)会生成一个从0到batch_size-1的数组。比如当batch_size为5时,np.arange(batch_size)会生成一个numpy数组[0, 1, 2, 3, 4]。因为t中的标签是以[2, 7, 0, 9, 4]的形式存储的,所以y[np.arange(batch_size), t]能抽出各个数据的正确解标签对应的神经网络的输出(在这个例子中,y[np.arange(batch_size), t]会生成numpy数组[y[0,2], y[1,7], y[2,0], y[3,9], y[4,4]])。

5. 引入损失函数的目的

在神经网络的学习中,寻找最优参数(权重和偏置)时,要寻找使损失函数的值尽可能小的地方,需要计算参数的导数(也就是梯度),然后以这个导数为指引,逐步更新参数的值。
假设有一个神经网络,现在我们来关注这个神经网络中的某一个权重参数。此时,对该权重参数的损失函数求导,表示的是“如果稍微改变这个权重参数的值,损失函数的值会如何变化”。如果导数的值为证,则通过该权重参数向负方向改变,可以减小损失函数的值。不过,当导数的值为0时,无论权重参数向哪个方向变化,损失函数的值都不会改变,此时该权重参数的更新会停在此处。

四. 数值微分
1.导数

导数就是表示某个瞬间的变化量。
在这里插入图片描述

# 不好的实践实例
def numerical_diff(f, x):
    h = 10e-50
    return (f(x+h) - f(x)) / h

上面的实现中,因为向把尽量小的值赋给h,所以h使用了10e-50这个微小值。但是,这样反而会产生舍入误差。所谓舍入误差,是指因省略小数的精细部分的数值而造成最终的计算结果上的误差。比如,在python中,舍入误差可以如下表示:

>>> np.float32(1e-50)
0.0

如上所示,如果用float32类型(32位的浮点数)来表示1e-50,就会变成0.0,无法正确表示出来。也就是说,使用过小的值会造成计算机出现计算上的问题。这是第一个需要改进的地方,即将微小值h改为10−4。使用10−4 就可以得到正确的结果。
第二个需要改进的地方与函数f的差分有关。虽然上述实现中计算了函数f在x+h和x之间的差分,但是必须注意到,这个计算从一开始就有误差。 如下图所示,“真的导数”对应函数在x处的斜率(称为切线),但上述实现中计算的导数对应的是(x + h)和x之间的斜率。因此,真的导数(真的切线)和上述实现中得到的导数的值在严格意义上并不一致。这个差异的出现是因为h不可能无限接近0。
在这里插入图片描述

如上图所示,数值微分有误差。为了减小这个误差,我们可以计算函数f在(x + h)和(x - h)之间的差分。因为这种计算方法以x为中心,计算它左右两边的差分,所以也称为中心差分(而(x + h)和x之间的差分称为前向差分),下面进行改进:

def numerical_diff(f, x):
    h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)
2. 数值微分的例子

我们来用上面的数值积分来对简单函数进行求导。比如:
在这里插入图片描述

用python来实现为:

def function_1(x):
    return 0.01*x**2 + 0.1*x

下面来描述这个函数的图像。代码如下。

import numpy as np
import matplotlib.pylab as plt

x = np.arange(0.0, 20.0, 0.1) # 以0.1为单位,从0到20的数组x
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.show()

在这里插入图片描述

我们来计算一下这个函数在x = 5和x = 10处的导数。

>>> numerical_diff(function_1, 5)
0.1999999999990898
>>> numerical_diff(function_1, 10)
0.2999999999986347

下面我们通过画图的方式来判断求出的斜率是否正确。

import numpy as np
import matplotlib.pylab as plt

def numerical_diff(f, x):
    h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)

def function_1(x):
    return 0.01*x**2 + 0.1*x

def tangent_line(f, x):
    d = numerical_diff(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")

tf = tangent_line(function_1, 5)
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)
plt.show()

图像为:
在这里插入图片描述

3.偏导数

和上面的例子的函数不同,此处的函数拥有两个变量。
在这里插入图片描述

可用以下代码表示:

def function_2(x):
    return x[0]**2 + x[1]**2
    # 或者 return np.sum(x**2)

这里,我们假定向参数输入了一个numpy数组。函数的内部实现比较简单,先计算numpy数组中各个元素的平方,再求它们的和。我们来画一下这个函数的图像。如下图所示,这个函数的图像是三维的。
在这里插入图片描述

另外,我们把这里讨论的有多个变量的函数的导数称为偏导数。
在这里插入图片描述

像这样,偏导数和单变量的导数一样,都是求某个地方的斜率。不过,偏导数需要将多个变量中的某一个变量定为目标变量,并将其他变量固定为某个值。

四. 梯度

( ∂ f ∂ x 0 , ∂ f ∂ x 1 ) \left(\frac{\partial f}{\partial x_0},\frac{\partial f}{\partial x_1}\right) (x0fx1f)这样的由全部变量的偏导数汇总成的向量称为梯度。可以如下来实现:

def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # 生成和x形状相同的数组

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

        # f(x-h)的计算
        x[idx] = tmp_val - h
        fxh2 = f(x)

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

    return grad

np.zeros_like(x)会生成一个形状和x相同、所有元素都为0的数组。函数numerical_gradient(f, x)中,参数f为函数,x为numpy数组,该函数对numpy数组x的各个元素求数值微分。现在用这个函数来实际计算一下梯度。

>>> numerical_gradient(function_2, np.array([3.0, 4.0]))
array([ 6., 8.])
>>> numerical_gradient(function_2, np.array([0.0, 2.0]))
array[( 0., 4.)]
>>> numerical_gradient(function_2, np.array([3.0, 0.0]))
array[( 6., 0.)]

那么铺垫了这么多,梯度的作用是什么呢,我们把 f ( x 0    ,    x 1 )    =    x 0 2    +    x 1 2 f\left(x_0\;,\;x_1\right)\;=\;x_0^2\;+\;x_1^2 f(x0,x1)=x02+x12的梯度画在图上。代码如下:

import numpy as np
import matplotlib.pylab as plt

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")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.legend()
    plt.draw()
    plt.show()

在这里插入图片描述

如上图所示,此函数的梯度呈现为有向向量(箭头)。我们发现梯度指向函数的"最低处"(最小值)。并且,离“最低处”越远,箭头越大。
虽然上图中的梯度指向了最低处,但并非任何时候都这样。实际上,梯度会指向各点处的函数值降低的方向。更严格地讲,梯度指示的方向是各点处的函数值减小最多的方向。

1.梯度法

机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必须找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数。
需要的主义的是,梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂函数中,梯度指示的方向基本上都不是函数值最小处。虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数地值。通过不断地沿梯度方向前进,逐渐减小函数值地过程就是梯度法

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值