基于Python的理论与实现(上)
第一章 Python入门
1.Python是什么?
Python是一个简单、易读、易记的编程语言
2.学习深度学习为什么要用Python?
深度学习的 框架中也有很多使用Python的场景,比如Caffe、TensorFlow、Chainer、 Theano等著名的深度学习框架都提供了Python接口。因此,学习Python 对使用深度学习框架大有益处。
3.我们会用到Python的哪些知识?
除了掌握最基本的Python语法,函数,类之外,我们还会使用两个外部库:NumPy库和Matplotlib 库
NumPy是用于数值计算的库,提供了很多高级的数学算法和便利的数 组(矩阵)操作方法。本书中将使用这些便利的方法来有效地促进深度学习 的实现。
Matplotlib是用来画图的库。使用Matplotlib能将实验结果可视化,并 在视觉上确认深度学习运行期间的数据。
NumPy库的使用
-
导入NumPy
# NumPy相关的方法均可通过np来调用。 import numpy as np
-
使用:生成NumPy数组
要生成NumPy数组,需要使用np.array()方法。np.array()接收Python 列表作为参数,生成NumPy数组(numpy.ndarray)。
>>> x = np.array([1.0, 2.0, 3.0]) >>> y = np.array([2.0, 4.0, 6.0]) >>> x + y # 对应元素的加法 array([ 3., 6., 9.]) >>> x - y array([ -1., -2., -3.]) >>> x * y # element-wiseproduct array([ 2., 8., 18.]) >>> x / y array([ 0.5, 0.5, 0.5])
-
使用:NumPy的N维数组
NumPy不仅可以生成一维数组(排成一列的数组),也可以生成多维数组。 比如,可以生成如下的二维数组(矩阵)。
>>> A = np.array([[1, 2], [3, 4]]) >>> print(A) [[1 2] [3 4]] >>> A.shape (2, 2) >>> A.dtype dtype('int64') >>> B = np.array([[3, 0],[0, 6]]) >>> A + B array([[ 4, 2], [ 3, 10]]) >>> A * B array([[ 3, 0], [ 0, 24]]) >>> A * 10 array([[ 10, 20], [ 30, 40]])
NumPy数组(np.array)可以生成N维数组,即可以生成一维数组、 二维数组、三维数组等任意维数的数组。数学上将一维数组称为向量, 将二维数组称为矩阵。另外,可以将一般化之后的向量或矩阵等统 称为张量(tensor)。本书基本上将二维数组称为“矩阵”,将三维数 组及三维以上的数组称为“张量”或“多维数组”。
-
广播功能
NumPy中,形状不同的数组之间也可以进行运算
A = np.array([[1, 2], [3, 4]]) >>> B = np.array([10, 20]) >>> A * B array([[ 10, 40], [ 30, 80]])
一维数组B被“巧妙地”变成了和二维数 组A相同的形状,然后再以对应元素的方式进行运算。 综上,因为NumPy有广播功能,所以不同形状的数组之间也可以顺利 地进行运算。
-
访问元素
元素的索引从0开始。对各个元素的访问可按如下方式进行。
>>> X = np.array([[51, 55], [14, 19], [0, 4]]) >>> print(X) [[51 55] [14 19] [ 0 4]] >>> X[0] # 第0行 array([51, 55]) >>> X[0][1] # (0,1)的元素 55
使用for语句访问各个元素。
>>> for row in X: >>> print(row) [51 55] [14 19] [0 4]
除了前面介绍的索引操作,NumPy还可以使用数组访问各个元素。
>>> X = X.flatten() # 将X转换为一维数组 >>> print(X) [51 55 14 19 0 4] >>> X[np.array([0, 2, 4])] # 获取索引为0、2、4的元素 array([51, 14, 0])
运用这个标记法,可以获取满足一定条件的元素。例如,要从X中抽出 大于15的元素,可以写成如下形式。
>>> X > 15 array([ True, True, False, True, False, False], dtype=bool) >>> X[X>15] array([51, 55, 19])
Matplotlib库的使用
-
导入Matplotlib
import matplotlib.pyplot as plt
-
使用:用matplotlib的pyplot模块绘制图形
绘制sin函数曲线的例子。
# 生成数据 x = np.arange(0, 6, 0.1) # 以0.1为单位,生成0到6的数据 y = np.sin(x) # 绘制图形 plt.plot(x, y) plt.show()
**这里使用NumPy的arange方法生成了[0, 0.1, 0.2, …, 5.8, 5.9]的 数据,将其设为x。对x的各个元素,应用NumPy的sin函数np.sin(),将x、 y的数据传给plt.plot方法,然后绘制图形。最后,通过plt.show()显示图形。 **
-
使用:显示与读入图像
pyplot中还提供了用于显示图像的方法imshow()。另外,可以使用 matplotlib.image模块的imread()方法读入图像。下面我们来看一个例子。
import matplotlib.pyplot as plt from matplotlib.image import imread img = imread('lena.png') # 读入图像(设定合适的路径!) plt.imshow(img) plt.show()
第二章 感知机
1.感知机是什么?
感知机是一种人工神经网络,由Frank Rosenblatt于1957年发明,他也提出了相应的感知机学习算法。
感知机接收多个输入信号,输出一个信号。
2.为什么要学习感知机?
因为感知机也是作为神经网络(深度学习)的起源的算法。因此, 学习感知机的构造也就是学习通向神经网络和深度学习的一种重要思想。
3.学习感知机的哪些内容?
- 感知机是具有输入和输出的算法。给定一个输入后,将输出一个既 定的值。
x1、x2是输入信号, y是输出信号,w1、w2是权重(w是weight的首字母)。图中的○称为**“神经元”或者“节点”。输入信号被送往神经元时,会被分别乘以固定的权重(w1x1、w2x2)。神经元会计算传送过来的信号的总和,只有当这个总和超过了某个界限值时,才会输出1。这也称为“神经元被激活” 。这里将这个界 限值称为阈值**,用符号θ表示。
- 感知机将权重和偏置设定为参数。
θ换成−b,于 是就可以用下面的式子来表示感知机的行为。
此处,b称为偏置,w1和w2称为权重
感知机的多个输入信号都有各自固有的权重,这些权重发挥着控制各个 信号的重要性的作用。也就是说,权重越大,对应该权重的信号的重要性就越高。感知机会计算输入 信号和权重的乘积,然后加上偏置,如果这个值大于0则输出1,否则输出0。
具体地说,w1和w2是控制输入信号的重要性的参数,而偏置是调 整神经元被激活的容易程度(输出信号为1的程度)的参数。
#用感知机实现与门
def AND(x1, x2):
x = np.array([x1, x2])
w = np.array([0.5, 0.5])
b = -0.7
tmp = np.sum(w*x) + b
if tmp <= 0:
return 0
else:
return 1
#用感知机实现与非门
def NAND(x1, x2):
x = np.array([x1, x2])
w = np.array([-0.5, -0.5]) # 仅权重和偏置与AND不同!
b = 0.7
tmp = np.sum(w*x) + b
if tmp <= 0:
return 0
else:
return 1
#用感知机实现或门
def OR(x1, x2):
x = np.array([x1, x2])
w = np.array([0.5, 0.5]) # 仅权重和偏置与AND不同!
b = -0.2
tmp = np.sum(w*x) + b
if tmp <= 0:
return 0
else:
return 1
-
使用感知机可以表示与门和或门等逻辑电路.
现在,我们用Python来实现刚才的逻辑电路。这里,先定义一个接收 参数x1和x2的AND函数。
def AND(x1, x2): w1, w2, theta = 0.5, 0.5, 0.7 tmp = x1*w1 + x2*w2 if tmp <= theta: return 0 elif tmp > theta: return 1 AND(0, 0) # 输出0 AND(1, 0) # 输出0 AND(0, 1) # 输出0 AND(1, 1) # 输出1
在函数内初始化参数w1、w2、theta,当输入的加权总和超过阈值时返回1, 否则返回0
-
感知机的局限性:异或门无法通过单层感知机来表示。
感知机的局限性就在于它只能表示由一条直线分割的空间。
弯曲的曲线无法用感知机表示。另外,由曲线分割而成的空间称为 非线性空间,由直线分割而成的空间称为线性空间
-
使用2层感知机可以表示异或门。
使用之前定义的 AND函数、NAND函数、OR函数,可以像下面这样(轻松地)实现。
def XOR(x1, x2): s1 = NAND(x1, x2) s2 = OR(x1, x2) y = AND(s1, s2) return y XOR(0, 0) # 输出0 XOR(1, 0) # 输出1 XOR(0, 1) # 输出1 XOR(1, 1) # 输出0
感知机总共由3层构成,但是因为拥有权重的层实质 上只有2层(第0层和第1层之间,第1层和第2层之间),所以称 为“2层感知机”。不过,有的文献认为图2-13的感知机是由3层 构成的,因而将其称为“3层感知机”。
-
单层感知机只能表示线性空间,而多层感知机可以表示非线性空间。
-
多层感知机(在理论上)可以表示计算机。
第三章 神经网络
1.神经网络是什么?
神经网络是一种计算模型,由大量的节点(或神经元)直接相互关联而构成
2.为什么还要学习神经网络?
回顾:感知机的缺陷
设定权重的工作,即确定合适的、能符合预期的输 入与输出的权重,现在还是由人工进行的。上一章中,我们结合与门、或门 的真值表人工决定了合适的权重。
神经网络的出现就是为了解决刚才的坏消息。具体地讲,神经网络的一 个重要性质是它可以自动地从数据中学习到合适的权重参数。
3.学习神经网络的哪些知识?
3.1激活函数
神经网络中的激活函数使用平滑变化的sigmoid函数或ReLU函数。
激活函数的作用在于决定如何来激活输入信号的总和。
首先,式(3.4)计算加权输入信号和偏置的总和,记为a。然后,式(3.5) 用h()函数将a转换为输出y。
如果要在图中明确表示出式(3.4) 和式(3.5),则可以像图3-4这样做。
神经网络中经常使用的一个激活函数就是式(3.6)表示的sigmoid函数 (sigmoid function)。
神经网络中用sigmoid函数作为激活函数,进行信号的转换,转换后的信号被传送给下一个神经元。实际上,上一章介绍的感知机 和接下来要介绍 的神经网络的主要区别就在于这个激活函数。
sigmoid函数很早就开始被使用了,而最近则主要 使用ReLU(Rectified Linear Unit)函数。
ReLU函数在输入大于0时,直接输出该值;在输入小于等于0时,输 出0(图 3-9)。
3.2 多维数组的运算
通过巧妙地使用NumPy多维数组,可以高效地实现神经网络。
np.dot():接收两个NumPy数组作为参 数,并返回数组的乘积。这里要注意的是,np.dot(A, B)和np.dot(B, A)的 值可能不一样。
(做矩阵乘法)
2×3的矩阵A和3×2的矩阵B的乘积可按以上方式实现。这里需要 注意的是矩阵的形状(shape)。具体地讲,矩阵A的第1维的元素个数(列数) 必须和矩阵B的第0维的元素个数(行数)相等
下面我们使用NumPy矩阵来实现3层神经网络。
从输入层到第1层的信号传递
第1层到第2层的信号传递
从第2层到输出层的信号传递
实现该神经网络时,要注意X、W、Y的形状,特别是X和W的对应 维度的元素个数是否一致,这一点很重要。
3.3输出层的设计
关于输出层的激活函数,回归问题中一般用恒等函数,分类问题中 一般用softmax函数。
分类问题中使用的softmax函数可以用下面的式(3.10)表示。
exp(x)是表示ex的指数函数(e是纳皮尔常数2.7182…)。式( 3.10)表示 假设输出层共有n个神经元,计算第k个神经元的输出yk。如式(3.10)所示, softmax函数的分子是输入信号ak的指数函数,分母是所有输入信号的指数 函数的和。
输出总和为1是softmax函数的一个重要性质。正 因为有了这个性质,我们才可以把softmax函数的输出解释为“概率”。
上面的softmax函数的实现虽然正确描述了式(3.10),但在计算机的运算 上有一定的缺陷。这个缺陷就是溢出问题
分类问题中,输出层的神经元的数量设置为要分类的类别数。
3.4 运用:手写数字识别
数据集的准备:这里使用的数据集是MNIST手写数字图像集。
步骤:先用训练图像进行学习,再用学习到的模型度量能在多大程度 上对测试图像进行正确的分类。
关于具体内容,详见《深度学习入门:基于Python的理论与实现》一书中的P69~P75(3.6节)及配套的代码。
3.5 批处理
上面的应用实例是只输入一张图像数据时的处理流程,现在我们来考虑打包输入多张图像的情形。比如,我们想用predict() 函数一次性打包处理100张图像。这时该怎么办呢?
为此,可以把x的形状改为100×784,将 100张图像打包作为输入数据。
这种打包式的输入数据称为批(batch)。批有“捆”的意思,图像就如同 纸币一样扎成一捆。
x, t = get_data()
network = init_network()
batch_size = 100 # 批数量
accuracy_cnt = 0
for i in range(0, len(x), batch_size):
x_batch = x[i:i+batch_size]
y_batch = predict(network, x_batch)
p = np.argmax(y_batch, axis=1)
accuracy_cnt += np.sum(p == t[i:i+batch_size])
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
Tips: ① 通过x[i:i+batch_size]从输入数 据中抽出批数据。x[i:i+batch_n]会取出从第i个到第i+batch_n个之间的数据。
② 通过argmax()获取值最大的元素的索引。
③ 比较一下以批为单位进行分类的结果和实际的答案。
NumPy数组之间使用比较运算符(==)生成由True/False构成的布尔 型数组,并计算True的个数。
输入数据的集合称为批。通过以批为单位进行推理处理,能够实现 高速的运算。
批处理一次性计算大型数组要比分开逐步计算 各个小型数组速度更快。
这是因为大多数处理 数值计算的库都进行了能够高效处理大型数组运算的最优化
小结:本章介绍了神经网络的前向传播。本章介绍的神经网络和上一章的感知 机在信号的按层传递这一点上是相同的,但是,向下一个神经元发送信号时, 改变信号的激活函数有很大差异。神经网络中使用的是平滑变化的sigmoid 函数,而感知机中使用的是信号急剧变化的阶跃函数。这个差异对于神经网 络的学习非常重要,我们将在下一章介绍。
第四章 神经网络学习
1.神经网络学习是什么?
这里所说的**“学习”**是指从训练数据中 自动获取最优权重参数的过程。
为了使神经网络能进行学习,将导 入损失函数这一指标。
而学习的目的就是以该损失函数为基准,找出能使它 的值达到最小的权重参数。
为了找出尽可能小的损失函数的值,本章我们将 介绍利用了函数斜率的梯度法。
2.为什么要引入神经网络的学习?
在第2章介绍的感知机的例 子中,我们对照着真值表,人工设定了参数的值,但是那时的参数只有3个。 而在实际的神经网络中,参数的数量成千上万,在层数更深的深度学习中, 参数的数量甚至可以上亿,想要人工决定这些参数的值是不可能的。
3.学习哪些内容?
3.1数据驱动(数据是机器学习的命根子)
从零开始想出一个可以识别的算法。
使用特征量和机器学习的方法。
神经网络直接学习图像本身。
以上的三种方式中:第1个方法完全是有人从零开始想出的算法;
在第2个方法,即利用特征量和机器学习的方法中,特征量仍是由人工设计的。
而在神经网络中,连图像中包含的重要特征量也都是由机器来学习的。没有人为介入。
神经网络的优点是对所有的问题都可以用同样的流程来解决。
也就是说,与 待处理的问题无关,神经网络可以将数据直接作为原始数据,进行“端对端” 的学习。
3.2 训练数据和测试数据
机器学习中使用的数据集分为训练数据和测试数据。
首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力
Q:为什么需要将数据分为训练数据和测 试数据呢?
A:因为我们追求的是模型的泛化能力。为了正确评价模型的泛化能 力,就必须划分训练数据(监督数据)和测试数据。
Tips: 泛化能力 是指处理未被观察过的数据(不包含在训练数据中的数据)的 能力。
获得泛化能力是机器学习的最终目标。
神经网络用训练数据进行学习,并用测试数据评价学习到的模型的泛化能力。
3.3 损失函数
损失函数是什么?
损失函数是表示神经网络性能的**“恶劣程度”的指标,即当前的 神经网络对监督数据在多大程度上不拟合**,在多大程度上不一致。
均方误差
可以用作损失函数的函数有很多,其中最有名的是均方误差(mean squared error)。均方误差如下式所示。
yk是表示神经网络的输出,tk表示监督数据,k表示数据的维数。
交叉熵误差
除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损 失函数。交叉熵误差如下式所示。
这里,log表示以e为底数的自然对数(log e)。 yk是神经网络的输出,tk是 正确解标签。
交叉熵误差的值是由正确解标签所对应的输出结果决定的。
mini-batch学习
神经网络的学习是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。
比如,从60000个训练数据中随机选择10笔,再用这10笔数据进行学习。这种学习方式称为mini-batch学习。
Q:如何从这个训练数据中随机抽取10笔数据呢?
A:可以使用 NumPy的np.random.choice()
使用np.random.choice()可以从指定的数字中随机选择想要的数字。
比如, np.random.choice(60000, 10)会从0到59999之间随机选择10个数字。
mini-batch的损失函数也是利用 一部分样本数据来近似地计算整体。也就是说,用随机选择的小批 量数据(mini-batch)作为全体训练数据的近似值。
为何要设定损失函数?为什么不以识别精度为指标?
- 在进行神经网络的学习时,不能将识别精度作为指标。因为如果以 识别精度为指标,则参数的导数在绝大多数地方都会变为0。
- 识别精度对微小的参数变化基本上没有什么反应,即便有反应,它的值 也是不连续地、突然地变化。
拓展:
作为激活函数的阶跃函数也有同样的情况。出 于相同的原因,如果使用阶跃函数作为激活函数,神经网络的学习将无法进行。
如果使用了阶跃函数,那么即便将损失函数作为指标,参数的微小变化也会被阶跃函数抹杀,导致损失函数的值不会产生任何变化。
也就是说,sigmoid函数的导数在任何地方都不为0。这对 神经网络的学习非常重要。得益于这个斜率不会为0的性质,神经网络的学 习得以正确进行。
神经网络的学习以损失函数为指标,更新权重参数,以使损失函数 的值减小。
3.4 数值微分
利用某个给定的微小值的差分求导数的过程,称为数值微分。
偏导数和单变量的导数一样,都是求某个地方的斜率。不过, 偏导数需要将多个变量中的某一个变量定为目标变量,并将其他变量固定为 某个值。
3.5 梯度
另外,像 这样的由全部变量的偏导数汇总 而成的向量称为梯度(gradient)。
比如,我们来考虑求x0 = 3,x1 = 4时(x0,x1) 的偏导数
利用数值微分,可以计算权重参数的梯度。
f(x0+x1)=x0**2+x1**2的梯度呈现为有向向量(箭头)。观 察图4-9,我们发现梯度指向函数f(x0,x1)的“最低处”(最小值),就像指南针 一样,所有的箭头都指向同一点。其次,我们发现离“最低处”越远,箭头越大。
更严格地讲,梯度指示的方向 是各点处的函数值减小最多的方向A
梯度法 : 通过巧妙地使用梯度来寻找函数最小值 (或者尽可能小的值)的方法就是梯度法。
机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必 须在学习时找到最优参数(权重和偏置),
这里所说的最优参数是指损失函数取最小值时的参数。
这里需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此, 无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际 上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。
函数的极小值、最小值以及被称为鞍点(saddle point)的地方, 梯度为0。
极小值:是局部最小值,也就是限定在某个范围内的最小值。
鞍点:是从某个方向上看是极大值,从另一个方向上看则是 极小值的点。
虽然梯度法是要寻找梯度为0的地方,但是那个地方不一定就是最小值(也有可能是极小值或者鞍点)。
此外,当函数很复杂且呈扁平状时,学习可能会进入一个(几乎)平坦的地区, 陷入被称为“学习高原”的无法前进的停滞期。
梯度法的思路:在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进, 如此反复,不断地沿梯度方向前进。
寻找最小值的梯度法称为梯度下降法(gradient descent method) , 寻找最大值的梯度法称为梯度上升法(gradient ascent method)。一般来说,**神经网络**(深度学习)中,梯度法主要是指**梯度下降法**。
η表示更新量,在神经网络的学习中,称为学习率(learning rate)。
学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。
下面,我们用Python来实现梯度下降法。如下所示,这个实现很简单。
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
参数f是要进行最优化的函数,init_x是初始值,lr是学习率learning rate,
step_num是梯度法的重复次数。
numerical_gradient(f,x)会求函数的梯度,用该梯度乘以学习率得到的值进行更新操作,由step_num指定重复的次数。
问题:请用梯度法求f(x0+x1)的最小值。
>>> def function_2(x):
>>> return x[0]**2 + x[1]**2
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
array([ -6.11110793e-10, 8.14814391e-10])
这里,设初始值为(-3.0, 4.0),开始使用梯度法寻找最小值。最终的结 果是(-6.1e-10, 8.1e-10),非常接近(0,0)。实际上,真的最小值就是(0,0), 所以说通过梯度法我们基本得到了正确结果。
实验结果表明,学习率过大的话,会发散成一个很大的值;反过来,学 习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率 是一个很重要的问题。
Tips: 像学习率这样的参数称为超参数。学习率这样的超参数则是人工设定的。
3.6 神经网络的学习步骤
前提 神经网络存在合适的权重和偏置,***调整权重和偏置***以便拟合训练数据的 过程称为“学习”。
神经网络的学习分成下面4个步骤。
步骤1(mini-batch) 从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们 的目标是减小mini-batch的损失函数的值。
步骤2(计算梯度) 为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。 梯度表示损失函数的值减小最多的方向。
步骤3(更新参数) 将权重参数沿梯度方向进行微小更新。
步骤4(重复) 重复步骤1、步骤2、步骤3。
练习:手写数字识别的神经网络