技术角 | 深度学习之《深度学习入门》学习笔记(四)神经网络的学习(上)

2020-05-13 15:44:52 浏览数 (1)

本文字数:4150字

阅读时间:8分钟

最近学习吴恩达《Machine Learning》课程以及《深度学习入门:基于Python的理论与实现》书,一些东西总结了下。现就后者学习进行笔记总结。本文是本书的学习笔记(四)神经网络的学习的上半部分。

目录

▪从数据中学习

▪损失函数

▪数值微分

本章标题所说的“学习”是指从训练数据中自动获取最优权重参数的过程。学习的目的就是以损失函数为基准,找出能使它的值达到最小的权重参数。

从数据中学习

神经网络的特征就是可以从数据中学习。所谓“从数据中学习”,是指可以由数据自动决定权重参数的值

数据是机器学习的命根子。数据是机器学习的核心。这种数据驱动的方法,也可以说脱离了过往以人为中心的方法。

而机器学习的方法是极力避免人为介入的,尝试从收集到的数据中发现答案(模式)。神经网络或深度学习则比以往的机器学习方法更能避免人为介入

例如手写数字识别,考虑通过有效利用数据来解决这个问题:先从图像中提取特征量,再用机器学习技术学习这些特征量的模式。“特征量”是指可以从输入数据(输入图像)中准确地提取本质数据(重要的数据)的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括SIFT、SURF和HOG等。使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的SVM、KNN等分类器进行学习。

机器学习的方法中,由机器从收集到的数据中找出规律性。但是,将图像转换为向量时使用的特征量仍是由人来设计的。即使使用特征量和机器学习的方法,也需要针对不同的问题人工考虑合适的特征量。

深度学习有时也称为端到端机器学习。

神经网络的优点是对所有的问题都可以用同意的流程来解决。神经网络都是通过不断地学习所提供的数据,尝试发现带求解问题的模式。也就是说,与待处理的问题无关,神经网络可以 将数据直接作为原始数据,进行“端对端”的学习。

机器学习中,一般将数据分为训练数据测试数据两部分来进行学习和实验等。未来正确评价模型的泛化能力,就必须划分训练数据和测试数据,训练数据也可以成为监督数据。

泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。

只对某个数据集过度拟合的状态称为过拟合(over fitting)。避免过拟合也是机器学习的一个重要课题。

损失函数

神经网络以某个指标为线索寻找最优权重参数。神经网络的学习中所用的指标称为损失函数。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。损失函数是表示神经网络性能的“恶劣程度”或者“性能有多好”的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。

均方误差

均方误差(mean squared error)由下式表示:

其中,

表示神经网络的输出,

表示监督数据,

表示数据的维度。其中,

表示监督数据,将正确的解标签设为1,其他均设为0,其他标签表示为0的表示方法称为one-hot表示

均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和。以下是代码实现:

代码语言:javascript复制
# 均方误差定义函数
def mean_squared_error(y, t):
    return 0.5 * np.sum((y-t)**2)

# 设“2”为正解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

#例1:“2”的概率最高的情况(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
mean_squared_error(np.array(y),np.array(t))

0.09750000000000003

代码语言:javascript复制
# 例2:“7”的概率最高的情况(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]

mean_squared_error(np.array(y),np.array(t))

0.5975

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

交叉熵误差

交叉熵误差(cross entropy error)由下式表示:

其中,

是神经网络的输出,

是正确解标签。并且,中只有正确解标签的索引为1,其他均为0(one-hot表示)。交叉熵误差的值是由正确解标签所对应的输出结果决定的。

正确解标签对应的输出越大,上式的值越接近0;当输出为1时,交叉熵误差为0。此外,如果正确解标签对应的输出较小,则上式的值较大。以下是代码实现:

代码语言:javascript复制
# 实现交叉熵误差
def cross_entropy_error0(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y   delta))

说明一下,函数内部在计算np.log时,加上了一个微小值delta。这是因为当出现np.log(0)时,np.log(0)会变为负无穷大的-inf,导致计算无法进行,所以作为保护性对策增加微小值。

代码语言:javascript复制
# 进行简单计算
# 设“2”为正解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
#例1:“2”的概率最高的情况(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
cross_entropy_error0(np.array(y),np.array(t))

0.510825457099338

代码语言:javascript复制
# 例2:“7”的概率最高的情况(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
cross_entropy_error0(np.array(y),np.array(t))

2.302584092994546

上例结果可以看出与前文讨论是一致的。

mini-batch学习

机器学习使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象

以交叉熵误差为例,如果要求所有训练数据的损失函数的总和,可用如下式子:

上式假设数据有

个,

表示第

个数据的第

个元素的值(

是神经网络的输出,

是监督数据)。本式只是把求单个数据的损失函数的式子扩大到了

份数据。最后除以

进行正规化,并求出单个数据的“平均损失函数”。

我们可以知道,许多数据分析是很大的数据量,这种情况不可能以全部数据为对象计算损失函数。因此,从全部数据选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。这种学习方式称为mini-batch学习。撰写读入MNIST数据集的代码:

代码语言:javascript复制
# 读入MNIST 代码暂略
import sys, os
sys.path.append(os.pardir)
import numpy as np

# from dataset.mnist import load_mnist

# 使用np.random.choice进行随机选取
np.random.choice(60000, 10)

array([ 5332, 11993, 16553, 47954, 31537, 4750, 52005, 1159, 6775, 46043])

实现一个可以同时处理单个数据和批量数据的交叉熵误差函数:

代码语言:javascript复制
# 可同时处理单个和批量数据
def cross_entropy_error1(y, t):
    if y.nidm == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

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

当监督数据是标签形式(非ont-hot表示,而是像“2”、“7”这样的具体标签)时,交叉熵误差可以通过如下代码实现:

代码语言:javascript复制
# 可同时处理单个和批量数据
def cross_entropy_error2(y, t):
    if y.nidm == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

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

由于one-hot表示中为0的元素的交叉熵误差也为0,因此针对这些元素的计算可以忽略。换言之,如果可以获得神经网络在正确解标签处的输出,就可以计算交叉熵误差。因此,为one-hot表示时通过 t * np.log(y)计算的地方,在为标签形式时,可用np.log(y[np.arange(batch_size), t])实现相同的处理。

介绍下np.log(y[np.arange(batch_size), t])np.arange(batch_size会生产一个从0到batch_size-1的数组。因为t中标签是以[2, 7, 0, 9, 4]的形式存储的,所以y[np.arange(batch_size), t能抽出各个数据的正确解标签对应的神经网络的输出。

为什么要设定损失函数

Q: 为什么要导入损失函数?既然我们的目标是获得识别精度尽可能高的神经网络,那不是应该把识别精度作为指标吗?

A: 在神经网络的学习中,寻找最优参数(权重和偏置)时,要寻找使损失函数的值尽可能小的参数。为了找到使损失函数的值尽可能小的地方,需要计算的参数的导数(确切的讲是梯度),然后以这个导数为指引,逐步更新参数的值。而对权重参数的损失函数求导,表示的是“如果稍微改变这个权重参数的值,损失函数的值会如何变化”。如果导数的值为负如,通过使该权重参数向正方向改变,可以减小损失函数的值;反过来,如果导数的值为正如,则通过使该权重参数向负方向改变,可以减小损失函数的值。当导数的值为0时如,无论权重参数向哪个方向变化,损失函数的值都不会改变,此时该权重参数的更新会停在此处。

总结一下:在进行神经网络的学习时,不能将识别精度作为指标。因为如果以识别精度为指标,则参数的导数在绝大多数地方都会变成0

可以说,识别精度对微小的参数变化基本上没有什么反应,即便有反应,它的值也是不连续地、突然地变化。作为激活函数的阶跃函数也有同样的情况。出于相同的原因,如果使用阶跃函数作为激活函数,神经网络的学习将无法进行。众所周知,阶跃函数的斜率在绝大多数地方都为0,而sigmoid函数的斜率(切线)在任何地方都不为0。

数值微分

梯度法使用梯度的信息决定前进的方向。

导数

导数是某个瞬间的变化量。

Python中的舍入误差:

代码语言:javascript复制
np.float32(1e-50)

0.0

代码语言:javascript复制
# 对舍入误差减小与使用中心差分实现函数导数程序
def numerical_diff(f, x):
    h = 1e-4 #0.0001
    return (f(x h) - f(x-h)) / (2*h)

利用微小的差分求导数的过程称为数值微分(numerical differentiation)。而基于数学式的推倒求导数的过程,则用解析性(analytic)一词,称为解析性求解或者解析性求导。

数值微分的例子

代码语言:javascript复制
# 实现例子
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("y")
plt.plot(x, y)
plt.show()

代码语言:javascript复制
#计算上面式子的5,10处导数
numerical_diff(function_1, 5)

0.1999999999990898

代码语言:javascript复制
numerical_diff(function_1, 10)

0.2999999999986347

代码语言:javascript复制
def tangent_line(f, x):
    d = numerical_diff(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t   y
代码语言:javascript复制
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)
tf2 = tangent_line(function_1, 10)
y2 = tf(x)
y22 = tf2(x)

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

0.1999999999990898 0.2999999999986347

偏导数

以下代码实现函数

代码语言:javascript复制
# 实现上式的代码
def function_2(x):
    #或者return np.sum(x**2)
    return x[0]**2   x[1]**2
代码语言:javascript复制
# 求偏导1
def function_tmp1(x0):
    return x0*x0   4.0**2.0

numerical_diff(function_tmp1, 3.0)

6.00000000000378

代码语言:javascript复制
# 求偏导2
def function_tmp2(x1):
    return 3.0**2.0   x1*x1

numerical_diff(function_tmp1, 4.0)

7.999999999999119

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

0 人点赞