Deeplearn

本文是深度学习入门-基于python的理论与实现这本书的读书笔记。

基础python知识:#

参见我的python & data

感知机(perceptron):#

这一章中的中所说的感知机应该称为“人工神经元”或“朴素感知机”,但是因为很多基本
的处理都是共通的,所以这里就简单地称为“感知机”。–原文注释。

感知机获取多个输入,每个输入只是0或者1(一般0=无输入,1=有输入),感知机内部对每个输入有权重,根据权重计算信号大小(也称为阈值),判断事件,进而决定是否响应(输出)。

当成人工神经元就很好理解了。神经元监听并接受多个信号,内部处理之后决定是否产生信号。感知机应该就是对神经元在机器层面的模仿。

一个最简单的感知机如下所示:

一些简单的例子(逻辑门):#

与门,可以这么实现
显然有很多种方法可以实现与门,不过一般都选择计算方便的。

与非门,可以在与门的基础上修改,只需要将参数全部变成负数即可,此时因为变完负数和之前的区别只在于事件判断的符号方向与之前相反,因而可以实现非得功能。

或门,只要有一个是真即可,因而可以降低激活的阈值

我们可以在软件层面上大费周章地做一个与门,比如使用python来设计一个and function:

1
2
3
4
def AND(x1,x2):
w1,w2,theta=0.5,0.5,0.7 # 这是一种实现方法。
tmp=x1*w1+x2*w2
return int(tmp>theta) # 将布尔值强行转化为数值

一般形式:#

将之前的信号比较模式写成一般形式就是

b称之为偏置(offset),这和之前的表示本质上没有区别。

我们使用numpy来简化我们的操作:

1
2
3
4
5
6
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
return int(tmp>0)

这样我们就不需要写那么长的计算式子了。

进一步来说,偏置调整了神经元被激活的难易程度。

一个反例:#

我们看到,单层感知器对信号的处理是线性的,这导致感知器没有办法直接实现异或门,后者需要非线性的信号处理器。

不过我们可以使用多层感知机来实现一个异或,这是很简单的。根据异或的逻辑,我们可以将其阐述为两件事情至少要做一件,并且不能同时做,根据这个逻辑,我们使用的门就是ornandand来组成。

1
2
3
4
5
def XOR(x1,x2):
s1=NAND(x1,x2)
s2=OR(x1,x2)
y=AND(x1,x2)
return int(y)

异或门是多层感知机,理论上多层层感知机可以用来构建计算器。

神经网络:#

神经网络的出现是为了代替人工对参数进行设置,它可以自动地从数据中学习到合适的权重参数。神经网络中使用sigmoid函数作为激活函数,我们后面会提到什么是激活函数。

一个例子:#

pass

激活函数:#

根据输入信号的总和转换为输出信号的函数。

前面我们使用的是阶跃函数,我们可以将其实现为

1
2
3
def step_function(x):
y = x > 0
return y.astype(np.int) # 将数据类型由bool转换为整数。用来表示多元输出的结果。

我们还可以简化成为一行

1
2
def step_function(x):
return np.array(x>0,dtype=np.int)

我们还有一些其他的函数可以使用,比如实现如下

1
2
def sigmoid(x):
return 1/(1+np.exp(-x)) # 无论是标量还是数组都可以正确计算结果。结果是element-wise的。

你可以使用如下代码来绘制sigmoid函数的图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import matplotlib.pyplot as plt
import numpy as np

def sigmoid(x):
return 1/(1+np.exp(-x))

x=np.arange(-5,5,0.1)
y=sigmoid(x)

plt.plot(x,y,label='sigmoid')
plt.xlabel('x')
plt.ylabel('y')
plt.title('sigmoid')
plt.legend()
plt.show()

二者的区别:#

使用sigmoid和阶跃函数最大的区别在于,前者是平滑的,决定了神经网络中流动的信息不再是0或者1这样的二值信号,而是01之间的实数值信号。

而两者的相似处在于:输入小的时候,输出接近0;反之接近1.同时输入和输出都在01之间。

最重要的相似处在于,两者均为非线性函数。引用原文中的一段话:

线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无隐
藏层的神经网络”。为了具体地(稍微直观地)理解这一点,我们来思
考下面这个简单的例子。这里我们考虑把线性函数 作为激活
函数,把 的运算对应 3 层神经网络 。这个运算会
进行 的乘法运算,但是同样的处理可以由
(注意, )这一次乘法运算(即没有隐藏层的神经网络)来表
示。如本例所示,使用线性函数时,无法发挥多层网络带来的优势。因
此,为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。

ReLU函数:#

ReLU 函数在输入大于 0 时,直接输出该值;在输入小于等于 0 时,输
0.实现如下:

1
2
def relu(x):
return np.maximum(0,x)

使用矩阵来实现神经网络:#

numpy为我们提供了矩阵计算的api,具体如下所示:
矩阵表达
这里的下标和等式两端的的顺序是对应的,左边要计算什么,左下标就是对应的的下标。

对应的是偏置,偏置一般使用一个恒输入的节点来表示,其上的权重就是偏置的大小。每层只有一个偏置,偏置权重的数量取决于后一层的神经元的数量(不包括偏执神经元)

一个简单的实现

1
2
3
4
5
6
7
8
9
X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])

print(W1.shape) # (2, 3)
print(X.shape) # (2,)
print(B1.shape) # (3,)

A1 = np.dot(X, W1) + B1

最终实现:#

当然,神经网络一般有很多层,我们这里举一个三层的例子,显示一下使用numpy的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def identity_function(x):
return x
def init_network():
network = {}
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
network['b2'] = np.array([0.1, 0.2])
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
network['b3'] = np.array([0.1, 0.2])
return network
def forward(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = identity_function(a3) # 表示输出层的激活函数,这里用一个恒等函数代替。
return y
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y) # [ 0.31682708 0.69627909]

引用原文对输出层的输出函数的注释:

输出层所用的激活函数,要根据求解问题的性质决定。一般地,回归问题可以使用恒等函数,二元分类问题可以使用sigmoid函数,多元分类问题可以使用 softmax 函数。

对输出层的设计:#

机器学习的问题大致可以分为分类问题回归问题。分类问题是数据属于哪一个类别的问题。比如,区分图像中的人是男性还是女性的问题就是分类问题。而回归问题是根据某个输入预测一个(连续的)数值的问题。比如,根据一个人的图像预测这个人的体重的问题就是回归问题(类似“57.4kg”这样的预测)。

softmax函数:#

对计算出的信号,分类问题中使用的softmax函数如下所示因而输出层的任意一个都和前面计算出的信号有关。

这很像是一个概率。同时我们也可以看到,softmax函数不会改变下标对应的元素之间的大小关系。

实现如下:

1
2
3
4
5
def softmax(a):
exp_a=np.exp(a)
sum_exp_a=np.sum(exp_a)
y=exp_a/sum_exp_a
return y

然而指数函数的运算非常容易溢出,所以我们要做出一些修正。

鉴于我们使用的函数是齐次的,我们上下同时除以,这样就可以将每一个数字缩小在之间,减轻运算。

1
2
3
4
5
6
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

一般推理阶段时输出层的softmax函数会被省略,而学习阶段则使用。

输出层的神经元数量:#

输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。比如,对于某个输入图像,预测是图中的数字 0 到 9 中的哪一个的问题(10 类别分类问题),可以像图 3-23 这样,将输出层的神经元设定为 10 个。

手写数字识别:#

详细内容见原文。

神经网络的学习:#

核心思路:让机器来决定参数的大小。

从数据中学习:#

数据驱动:#

原文这段话非常有启发意义:

数据是机器学习的命根子。从数据中寻找答案、从数据中发现模式、根据数据讲故事……这些机器学习所做的事情,如果没有数据的话,就无从谈起。因此,数据是机器学习的核心。这种数据驱动的方法,也可以说脱离了过往以人为中心的方法。
通常要解决某个问题,特别是需要发现某种模式时,人们一般会综合考虑各种因素后再给出回答。“这个问题好像有这样的规律性?”“不对,可能原因在别的地方。”——类似这样,人们以自己的经验和直觉为线索,通过反复试验推进工作。而机器学习的方法则极力避免人为介入,尝试从收集到的数据中发现答案(模式)。神经网络或深度学习则比以往的机器学习方法更能避免人为介入。
现在我们来思考一个具体的问题,比如如何实现数字“5”的识别。数字 5是图 4-1 所示的手写图像,我们的目标是实现能区别是否是 5 的程序。这个问题看起来很简单,大家能想到什么样的算法呢?
如果让我们自己来设计一个能将 5 正确分类的程序,就会意外地发现这是一个很难的问题。人可以简单地识别出 5,但却很难明确说出是基于何种规律而识别出了5。此外,从图 4-1 中也可以看到,每个人都有不同的写字习惯,要发现其中的规律是一件非常难的工作。因此,与其绞尽脑汁,从零开始想出一个可以识别 5 的算法,不如考虑通过有效利用数据来解决这个问题。一种方案是,先从图像中提取特征量 ,再用机器学习技术学习这些特征量的模式。这里所说的“特征量”是指可以从输入数据(输入图像)中准确地提取本质数据(重要的数据)的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括 SIFT、SURF 和 HOG 等。使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的 SVM、KNN 等分类器进行学习。
机器学习的方法中,由机器从收集到的数据中找出规律性。与从零开始想出算法相比,这种方法可以更高效地解决问题,也能减轻人的负担。但是需要注意的是,将图像转换为向量时使用的特征量仍是由人设计的。对于不同的问题,必须使用合适的特征量(必须设计专门的特征量),才能得到好的结果。比如,为了区分狗的脸部,人们需要考虑与用于识别 5 的特征量不同的其他特征量。也就是说,即使使用特征量和机器学习的方法,也需要针对不同的问题人工考虑合适的特征量。

不过我们有神经网络,使用深度学习来完成这个任务。

损失函数:#

为了统计估测的偏差,引入损失函数的概念。

均方误差:#

如下所示实现为

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

这里, 是表示神经网络的输出, 表示监督数据, 表示数据的维数。比如:

1
2
3
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 是 softmax 函数的输出。由于 softmax 函数的输出可以理解为概率,因此上例表示“0”的概率是 0.1,“1”的概率是 0.05,“2”的概率是 0.6 等。t 是监督数据,将正确解标签设为 1,其他均设为 0。这里,标签“2”为 1,表示正确解是“2”。将正确解标签表示为 1,其他标签表示为 0 的表示方法称为 one-hot 表示.

交叉熵误差(cross entropy error):#

参数和之前一样,因而这个式子实际上只计算对应正确解标签的输出的自然对数

更近一步理解,这个式子计算的是 神经网络 对 正确项 估计 的误差值,值越大误差越大。

实现如下

1
2
3
def cross_entropy_error(y,t):
delta=1e-7 # 避免y=0时出现的无穷大,导致后续运算无法进行
return -np.sum(t*np.log(y+delta))

mini-batch:#

机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据有 100 个的话,我们就要把这 100 个损失函数的总和作为学习的指标。

以交叉熵误差为例,可以写成

在处理大数据集时,直接计算所有数据的损失函数不仅计算量大,而且可能导致内存不足。因此,我们采用了 mini-batch 学习的方法。在这种方法中,我们从全部数据中随机选择一个小批量(mini-batch)的数据进行训练,而不是每次使用整个数据集。这种方法不仅可以加速计算,还可以通过随机选择不同的 mini-batch 来增强模型的泛化能力。比如,在 MNIST 数据集中,我们可以从 60000 个训练数据中随机选择 100 个样本,来进行一次训练更新。

使用numpy的随机函数可以实现上述要求。如下所示:

1
2
3
4
5
6
train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size) # 使用 np.random.choice() 可以从指定的数字中随机选择想要的数字。
# 比如 np.random.choice(60000, 10) 会从 0 到 59999 之间随机选择 10 个数字。
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

为什么要使用sigmoid而非阶跃函数:#

我们希望通过微调来进行参数的修正,最好的工具是导数,它引导我们做出正确的参数修正的方向。但是阶跃函数在原点不可导,同时可导处导数处处为0,也就是说参数的微小变化会被阶跃函数抹杀,而不会影响损失函数的值。然而sigmoid函数是光滑的,这很好。

数值微分:#

所谓数值微分就是用数值方法近似求解函数的导数的过程。——译者注

我们来从程序上实现求导。基于数学式的推导求导数的过程称为解析求导,而利用微小的差分近似导数的过程称为数值微分。

一个数值微分的例子

1
2
3
def numerical_diff(f,x):
h=1e-4 # 要注意python有舍入误差,太小的数字会被舍为0.0
return (f(x+h)-f(x-h))/(2*x) # 使用中心差分代替前向差分,一般更精准。

我们也可以使用上述方法求其他的微分,比如多元微分,梯度之类的。

一个梯度的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def numerical_gradient(f,x):
h=1e-4
grad=np.zeros_like(x)

for idx in range(x.size):
tmp_val=x[idx]

x[idx] = tmp_val + h # 数值微元
fxh1 = f(x)

x[idx] = tmp_val - h
fxh2 = f(x)

grad[idx]=(fxh1 - fxh2) / (2*h)

x[idx] = tmp_val # 要记得将这个变回来
return grad

梯度:#

梯度指出了当前点值变化的最快方向,我们可以使用梯度下降法来寻找最小值(寻找最大值称为梯度上升法)。具体做法是令其中称为学习率,表示在某次学习之中,应该学习多少/在多大程度上更新参数。

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

为什么是负号而不是正号呢?因为我们希望当前点沿着梯度下降的方向,当某个梯度分量(也即偏导数)是正值的时候,我们应该沿着它的反方向调整当前分量的位置,因而要加个符号。

实现如下:

1
2
3
4
5
6
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

神经网络的梯度:#

对于单层$(23)$的神经网络,其梯度可以使用矩阵微分的理论来表示:
alt text
修改各个权重的时候就可以按照这个去修正。
一般的学习过程如下:
选数据-计算梯度-更新参数-重复前三步。这个学习过程就是*SGD

一般对权重参数的初始化符合高斯分布的随机数进行初始化,偏置使用0初始化。

mini-batch的实现之前讲过了,就是random_choice

基于测试数据的评价:#

然而虽然在训练中我们可以通过反复学习可以使损失函数的值下降,但是真正评价这个神经网络学习的优劣程度还要看测试上的表现。也就是说评价必须要基于测试数据,看是否会发生拟合得太过了,以至于非训练集数据无法被识别的现象,也即过拟合

原文:

神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。

如何评价泛化程度呢,我们只需要计算,当不断重复训练时,测试精度是否有和训练精度一同上升即可,这表示训练是有效的。引入一个epoch作为标记训练循环次数的度量,其满足:

一个 epoch 表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于 10000 笔训练数据,用大小为 100 笔数据的 mini-batch 进行学习时,重复随机梯度下降法100 次,所有的训练数据就都被“看过”了 。此时,100 次就是一个 epoch
实际上,一般做法是事先将所有训练数据随机打乱,然后按指定的批次大小,按序生成mini-batch。这样每个 mini-batch 均有一个索引号,比如此例可以是 0, 1, 2, … , 99,然后用索引号可以遍历所有的 mini-batch。遍历一次所有数据,就称为一个epoch。——译者注

误差反向传播法:#

这个方法能够提高计算权重参数的梯度的效率。为了便于理解这个方法,引入计算图的概念。
计算图是一个有向无环图,在计算图中,边代表操作数,节点代表操作符。一般将原始数据放在左侧,操作符和生成的结果向右传播。这是一个自底向上的计算过程,不过我们人一般在计算抽象问题的时候都是自顶向下的,比如要算一次消费总额,我们往往层层分类商品和计价规则,直到分割成为一个个可执行的单元,然后再自底向上计算。

计算图的优势就是,可以传递局部计算的结果来解决全局问题。局部计算是指,无论全局发生了什么,都能只根据与自己相关的信息输出接下俩来的结果。

同时计算图能很好地解释链式法则,节点之间反向传播的信息流就是每一层的偏导数。

我们来实现激活函数层,这里实现relusigmoid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Relu:
def __init__(self):
self.mask=None

def forward(self,x):
self.mask=(x<=0)
out=x.copy()
out[self.mask]=0
def backward(self,dout):

dout[self.mask]=0
dx=dout

return dx

class Sigmoid:
def __init__(self):
self.out=None
def forward(self,x):
out=1/(1+np.exp(-x))
self.out=out

return out
def backward(self,dout):
dx=dout*(1.0-self.out)*self.out

return dx

这里有一些细节,每次我们反向做参数调整的分析的时候,我们所用的数据都是本层计算的结果来作为我们调整参数的依据,因而我们要将本层计算的结果存储起来,以供在反向分析的时候使用。

对矩阵求偏导的分析:#

神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算。神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换” 。因此,这里将进行仿射变换的处理实现为Affine 层

Affine层的核心就是一句从而当上游的偏导数计算完后,我们可以计算出对损失函数输入的偏导数这里为什么会有转置呢?首先我们要看到,我们求的导数是标量函数的导数,因而求导数的形式就是梯度矩阵(如果分母是向量的话就是梯度向量了,那个我们很熟悉)。然后我们根据链式法则,可以得到,根据我们的习惯(在微积分中将向量写成行向量的习惯),对某一标量元求向量为分子的导数的时候将其不同元的导数横向展开(那么相同元的导数就竖向展开了),因而对于而言,其行数等于的行数,列数等于的行数,从而其形状应是和相同的。

从而我们简单地实现:

1

数学方法一览#

优化方法:#

主要介绍了SGDMomentumAdaGradAdam这些新的优化方法。

权重的初始值:#

权重不能全部初始化为0。

使用sigmoid/tanh函数或者一些线性函数的时候,推荐使用Xavier初始值:与前一层有 个节点连接时,初始值使用标准差为的高斯分布。

使用relu函数,推荐使用He初始值:与前一层有 个节点连接时,初始值使用标准差为的高斯分布。

batch-normalization:#

强制将产生的激活值的数据的分布正规化,通过在每一层神经网络的输出上进行归一化处理,使得每一层的输入保持稳定,减少了参数初始化的敏感性和梯度消失或爆炸的问题。
设定如下其中是一个微小值,避免方差为0的情况。

这样可以将每一组mini-batch的数据强制转化为均值为0,方差为1的数据集。然后对正规化的数据进行缩放和平移的变化,也即这里的gammabeta是参数,最开始设置成为(1,0)然后再通过学习调整到合适的值。

解决过拟合:#

权值衰减:#

将损失函数加上权重的2-范数,具体是:设置的越大,对大权重的惩罚就越重。

DropOut:#

每次训练时随机删除神经元,这样可以一定程度上避免某些神经元过度参与。测试的时候虽然会传递所有的神经元信号,但是对于各个神经元的输出要乘以训练时的删除比例再输出。因为神经元整体输出的期望下降了。

超参数的调整:#

一般逐步缩小超参数的范围,比如首先设定一个范围,然后随机采样,进行学习,然后考察测试水瓶。

卷积神经网络:#

卷积神经网络,也即CNN,被用于图像识别、语音识别等各种场合。

整体结构:#

和之前介绍的神经网络类似,可以层层组装,不过新出现了**卷积层(Convolution)池化层(Pooling)**。
CNN的常见结构则是Conv-ReLU-(Pooling)+Affine-ReLU+Affine-Softmax

卷积层:#

全连接层忽视了数据的维度,将所有的数据拉平成为一维,而维数常常蕴含着很多信息,尤其是在图像处理中。

卷积层则不会这样,卷积层将保持输入数据的形状(组织形式)不变。

二维卷积运算:#

alt text
点积。

术语:#

为了调整输出数据的格式,有时候会先在周围填充一些固定的值,这个操作称为填充(padding),填充的值称为幅度

应用滤波器的位置间隔称为步幅(stride)

三维卷积运算:#

就是在三个方向上进行卷积。

当然如果有多个滤波器,我们也将这些滤波器得到的图整合成为一个然后输出。

池化层:#

使用 部分数据来表示整体数据,类似于压缩。
常见的有MAX池化Average池化。一般都是MAX池化。这样可以提高系统的鲁棒性。

实现:#

一般基于im2col函数展开要卷积的数据和滤波器。简单来说,就是将要卷积的所有东西都展开成为一行乘一列,这样可以使用硬件/算法进行加速。

深度学习:#

具有深层次的神经网络。

历史:#

VGG是比较简单的CNN,其特点在于将有权重的层叠加到了16层/19层

GoogleNet纵向上有深度,横向上使用多个滤波器,最后在合并他们的结果。

ResNet是微软开发的网络,比之前的网络具有更深层次的结构。引入了快捷连接,旨在解决一味增加网络深度而产生的梯度发散。

其他:#

略。

end:#

总结整个书,旨在对深度学习给出一些基本的概念和处理思路,较为浅显和简单,适合作为初学者来泛泛阅读,也能有所收获。