为了完成毕设, 最近开始入门深度学习.
在此和大家分享一下本人阅读鱼书时的笔记,若有遗漏,欢迎斧正!
若转载请注明出处!
一、感知机
感知机(perceptron)接收多个输入信号,输出一个信号。
如图感知机,其接受两个输入信号。其中 \(\theta\) 为阈值,超过阈值 神经元就会被激活。
感知机的局限性在于,它只能表示由一条直线分割的空间,即线性空间。多层感知机可以实现复杂功能。
二、神经网络
神经网络由三部分组成:输入层、隐藏层、输出层
1. 激活函数
激活函数将输入信号的总和转换为输出信号,相当于对计算结果进行简单筛选和处理。
如图所示的激活函数为阶跃函数。
1) sigmoid 函数
sigmoid函数是常用的神经网络激活函数。
其公式为:
\[h(x)=\frac{1}{1+e^{-x}} \]如图所示,其输出值在 0到 1 之间。
2) ReLU 函数
ReLU(Rectified Linear Unit)函数是最近常用的激活函数。
3) tanh 函数
2. 三层神经网络的实现
该神经网络包括:输入层、2 个隐藏层和输出层。
def forward(network, x): # x为输入数据
# 第1个隐藏层的处理,点乘加上偏置后传至激活函数
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
# 第2个隐藏层的处理
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
#输出层处理 identidy_function原模原样输出a3
a3 = np.dot(z2, W3) + b3
y = identify_function(a3)
return y # y为最终结果
3. 输出层激活函数
一般来说,回归问题选择恒等函数,分类问题选择softmax函数。
softmax函数的公式:
\[y_{k}=\frac{e^{a_{k}}}{\sum_{i=1}^{n}e^{a_{i}}} \]假设输出层有 \(n\) 个神经元,计算第 \(k\) 个神经元的输出 \(y_{k}\) 。
softmax函数的输出值的总和为 1。因此我们可以将它的输出解释为概率。
输出层神经元数量一般和设定类别数量相等。
4. 手写数字识别
使用 MNIST 数据集。
使用 pickle 包序列化与反序列化所需数据,可以加快读取速度。
正规化 Normalization:将数据限定到某个范围内。
批处理 Batch
将输入数据成批次打包,可以一次处理多张图片。
batch_size = 100
for in range(0, len(x), batch_size) # x为输入数据
x_batch = x[i:i+batch_size] # 切片处理,一次取batch_size张图片
y_batch = predict(network, x_batch)
p = np.argmax(y_batch, axis = 1)
三、神经网络的学习
学习是指从训练过程中自动获取最优权重参数的过程。
1. 数据驱动方式
从图像中提取特征量(SIFT、SURF 或 HOG),使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的 SVM、KNN 等分类器进行学习。
2. 损失函数
神经网络将损失函数作为指标来寻找最优权重参数。
神经网络学习的目的就是尽可能地降低损失函数的值。
我们一般使用均方误差和交叉熵误差函数。
1) 均方误差
Mean Squared Error。
\[E=\frac{1}{2}\sum_{k}(y_{k}-t_{k})^2 \]\(y_{k}\) 表示神经网络的输出结果, \(t_{k}\) 表示正确解标签,\(k\) 表示数据维度。
one-hot表示:正确解标签表示为 1,其他标签表示为 0。
如:
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] # 假设在进行数字识别,数字“2”为正确结果
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
2) 交叉熵误差
Cross Entropy Error
\[E=-\sum_{k}t_{k}\log y_{k} \]\(y_{k}\) 表示神经网络的输出结果, \(t_{k}\) 表示正确解标签。
3) mini-batch 学习
如果我们要求所有训练数据的平均损失函数,以交叉熵误差为例,则为:
\[E=-\frac{1}{N}\sum_{n}\sum_{k}t_{nk}\log y_{nk} \]我们可以从全部数据中选出一部分,作为全部数据的代表。这一部分就是 mini-batch。
好比抽样调查。
train_size = x_train.shape[0] # 训练集的全部数据个数
batch_size = 10 #mini-batch的大小
batch_mask = np.random.choice(train_size, batch_size)#该函数从train_size个数字随机挑选batch_size个数
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
3. 数值微分
1) 导数
使用中心差分来近似求解导数。
def numerical_diff(f, x) #求函数f(x)在x处的导数
h = 1e-4 #微小值
return (f(x+h)-f(x-h)) / (2 * h)
2) 梯度
由全部变量的偏导数汇总成的向量称为梯度。
如,对于函数 \(f(x,y)=x^2+y^2\) ,其在 \((x,y)\) 处的梯度为 \((\frac{\partial f}{\partial x},\frac{\partial f}{\partial y})\)
其 Python 实现如下所示:
def numerical_gradient(f, x):
h = 1e-4
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)
# 计算x[idx]的偏导数
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 还原值
梯度指向各点处的函数值减少最多的方向。
3) 梯度下降法
我们通常沿着梯度方向,使用梯度下降法循环寻找损失函数的最小值。
以上面提到的函数为例,用下面的式子不断更新梯度值:
\[x=x-\eta\frac{\partial f}{\partial x}\\ y=y-\eta\frac{\partial f}{\partial y}\\ \]\(\eta\) 是一个更新量,称为学习率。学习率的初始值一般为 0.01 或 0.001
用Python实现梯度下降法如下:
# f为函数,init_x为初始的变量组,学习率0.01,循环100次
def gradient_descent(f, init_x, learning_rate = 0.01, step_num = 100):
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x)
x = x - lr*grad
return x
4. 神经网络的梯度
神经网络的学习要求损失函数关于权重参数的梯度。
比如一个 2*3 的权重参数 \(W\),损失函数为 \(L\) ,则梯度 \(\frac{\partial L}{\partial W}\) 为:
\[W=(\begin{matrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \end{matrix})\\ \frac{\partial L}{\partial W} = (\begin{matrix} \frac{\partial L}{\partial w_{11}} & \frac{\partial L}{\partial w_{12}} & \frac{\partial L}{\partial w_{13}} \\ \frac{\partial L}{\partial w_{21}} & \frac{\partial L}{\partial w_{22}} & \frac{\partial L}{\partial w_{23}} \end{matrix}) \]5. 学习算法的实现
动态调整权重和偏置以拟合训练数据过程称为学习。共有四个步骤:
- mini-batch:挑选mini-batch数据,目标是减少其损失函数的值。随机梯度下降法 SGD。
- 计算梯度:计算各个权重参数的梯度
- 更新参数:将权重沿梯度方向进行微小更新
- 重复以上步骤
假设一个神经网络有两个权重参数 \(W1\) 和 \(W2\),两个偏置参数 \(b1\),$ b2$ :
class TwoLayerNet:
# 计算并返回网络输出值
def predict(self, x):
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
# 计算损失值 t为正确解标签
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
# 计算梯度
def count_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
# 计算梯度 其他参数省略号
grads['W1'] = numerical_gradient(loss_W, params['W1'])
mini-batch的实现:
# 超参数
iters_num = 10000 # 下降次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
network = TwoLayerNetwork(input_size = 784, hidden_size = 50, output_size = 10)
for i in range(iters_num):
# 获取mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 计算梯度
grad = network.count_gradient(x_batch, t_batch)
# 更新参数
for key in ('W1','b1','W2','b2'):
network.params[key] -= leraning_rate * grad[key]
一个 epoch 表示学习中所有训练数据均被使用过的一次时的更新次数。
四、误差反向传播法 BP
使用误差反向传播法能够快速计算权重参数的梯度。
其基于链式法则。
- 加法结点的反向传播将上游的值原封不动地输出到下游。
- 乘法结点的反向传播乘以输入信号的翻转值。
1. 激活函数层的实现
1) ReLU
class Relu:
def __init__(self):
self.mask = None
# 前向传播
def forward(self, x):
self.mask = (x <= 0)
out = x.copy()
out[self.mask] = 0
return out
# 反向传播
def backward(self,dout):
dout[self.mask] = 0
dx = dout
return dx
2) sigmoid
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
2. Affine/Softmax 层的实现
1) Affine
神经网络正向传播的流程是根据输入数据和权重、偏置计算加权和,经过激活函数后输出至下一层。
其中进行的矩阵的乘积运算在几何学领域被称为仿射变换,因此我们将进行仿射变换的处理实现为Affine层。
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
# 前向传播
def forward(self, x):
self.x = x
out = np.dot(x, self.W) + self.b
return out
# 反向传播
def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis = 0)
return dx
2) Softmax
softmax函数将输入值正规化后输出。考虑到这里也包含作为损失函数的交叉熵误差,因此称为 Softmax-with-Loss层。
class SoftmaxWithLoss:
def __init__(self):
self.loss = None
self.y = None
self.t = None
# 前向传播
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
# 反向传播
def backward(self, dout = 1):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx
五、与学习相关的技巧
1. 参数的更新
神经网络的学习目的是找到使损失函数的值尽可能小的参数,这个过程被称为最优化 Optimization
常用方法有SGD、Momentum、AdaGrad和Adam等。
1) SGD
随机梯度下降法。
\[W\gets W-\eta\frac{\partial L}{\partial W} \]class SGD:
def __init__(self, lr = 0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
2) Momentum
SGD方法的缺点在于梯度的方向并没有志向最小值的方向。Momentum 是动量的意思。
其数学式如下:
\[v \gets \alpha v-\eta\frac{\partial L}{\partial W}\\ W \gets W+v \]表示物体在梯度方向上受力。
class Momentum:
def __init__(self, lr = 0.01, momentum = 0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
3) AdaGrad
AdaGrad 方法保留了之前所有梯度值的平方和,会为参数的每个元素适当调整学习率。
Ada 表示 Adaptive
\[h \gets h+\frac{\partial L}{\partial W}\bigodot \frac{\partial L}{\partial W}\\ W \gets W-\eta\frac{1}{\sqrt{h}}\frac{\partial L}{\partial W} \]class AdaGrad:
def __init__(self, lr = 0.01):
self.lr = lr
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] += grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
4) Adam
Adam是最近出现的一种参数更新方法,它会设置三个超三处。
2. 权重的初始值
各层的激活值的分布要求有适当的广度,否则可能会出现梯度消失现象。
1) Xavier 初始值
在一般的深度学习框架中, Xavier初始值已被作为标准使用。
在 Xavier 初始值中,如果前一层的节点数为 \(n\),则初始值使用标准差为 \(\frac{1}{\sqrt{n}}\) 的分布。
node_num = 100
w = np.random.randn(node_num, node_num) / np.sqrt(node_num)
2) ReLU 的 He 初始值
当激活函数使用 ReLU 时,一般使用 He 初始值。
如果前一层的节点数为 \(n\),则初始值使用标准差为 \(\sqrt{\frac{2}{n}}\) 的高斯分布。
3. Batch Normalization
为了使各层的激活值的分布有适当的广度,使用 Batch Normalization 方法进行强制调整。
因此,我们需要在 Affine 层和激活函数层之间插入一个 Batch Norm 层。以进行学习时的 mini-batch 为单位进行正则化。
\[\mu_{B}\gets\frac{1}{m}\sum_{i=1}^m x_{i}\\ \sigma_{B}^2\gets\frac{1}{m}\sum_{i=1}^m (x_{i}-\mu B)^2\\ \hat{x_{i}}\gets\frac{x_{i}-\mu B}{\sqrt{\sigma_{B}^2+\varepsilon}} \]对 mini-batch 的 \(n\) 个输入数据的集合 \(B={x_{1},x_{2},...,x_{m}}\) 求均值 \(\mu B\) 和 方差 \(\sigma_{B}^2\)。
4. 过拟合的抑制
机器学习中,过拟合是一个很常见的问题。过拟合指的是只能拟合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态。
因此我们需要一些方法来抑制过拟合。权值衰减是方法之一。
1) 权值衰减
对于所有权重,权值衰减方法都会为损失函数加上 \(\frac{1}{2}\lambda W^2\),即权重的 \(L2\) 范数。
因此在求权重梯度的计算中,要为之前的误差反向传播法的结果加上正则化项的导数 \(\lambda W\)。
2) Dropout
网络模型复杂时,使用Dropout方法抑制过拟合。
Dropout是一种在学习过程中删除神经元的方法。训练时,随机选出隐藏层的神经元并将其删除。被删除的神经元不再进行信号的传递。
class Dropout:
def __init__(self, dropout_ratio = 0.5):
self.dropout_ratio = dropout_ratio
self.mask = None
def forward(self, x, train_flg = True):
if train_flg:
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask
else:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask
5. 超参数的验证
超参数有神经元数量、batch大小、学习率等。
我们不能使用测试数据评估超参数的性能,否则会造成过拟合
调整超参数时,必须使用超参数专用的确认数据,称为验证数据 validation data
六、卷积神经网络
CNN 的结构可以像积木一样进行组装。其中出现了卷积层 Convolution 和池化层 Pooling。
在 CNN 中,层的连接顺序是 :Convolution - ReLU - Pooling.
Pooling有时被省略。
1. 卷积层
在全连接层中,数据的形状被忽略了。而卷积层可以保持形状不变。当输入数据是图像时,卷积层以 3 维数据的形式接收输入数据,并同样以 3 维数据的形式输出至下一次。
卷积层的输入输出数据称为特征图 Feature Map。
1) 卷积运算
卷积运算相当于过滤器。
滤波器即输出中的权重W。
滤波器会提取边缘或斑块等原始信息。
如图,输入数据大小是 \((5,5)\),滤波器大小是 \((3,3)\),输出大小是 \((3,3)\)。
2) 填充 Padding
在进行卷积层处理前,有时要像输入数据的周围填入固定数据来扩充数据。
使用填充主要是为了调整输出的大小。
3) 步幅 Stride
应用滤波器的位置间隔称为步幅。
增大步幅后,输出表小;增大填充后,步幅变化。
假设输入大小为 \((H,W)\) ,滤波器大小为 \((FH,FW)\),输出大小为 \((OH,OW)\),填充为 \(P\) ,步幅为 \(S\)。
则输出大小为:
\[OH=\frac{H+2P-FH}{S}+1\\ OW=\frac{W+2P-FW}{S}+1 \]4) 三维数据的卷积运算
以 3 通道 RGB 图像为例,其纵深方向上的特征图增加了。通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加。
输入数据的通道数和滤波器的通道数要相同。
当有多个滤波器时,输出特征图同样有多层。
2. 池化层
池化是缩小高、长方向上的空间运算。简单来说,池化用来精简数据。
Max池化上获取最大值的运算。一般来说,池化窗口大小会和步幅相同。
3. 卷积层和池化层的实现
一个关键的函数为 im2col。它将输入的三维数据展开为二维矩阵以适合滤波器。
1) 卷积层
class Convolution:
def __init__(self, W, b, stride = 1, pad = 0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x)
FN, C, FH, FW = self.W.shape # 滤波器的数量、通道数、高、长
N, C, H, W = x.shape # 输入数据的数量、通道数、高、长
# 计算输出数据的长和高
out_h = int(1 + (H + 2 * self.pad - FH) / self.stride)
out_w = int(1 + (W + 2 * self.pad - FW) / self.stride)
# 使用im2col将三维数据转换为矩阵
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T
out = np.dot(col, col_W) + self.b # 乘以权重后加上便宜
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) # 重新变为三维数据
return out
2) 池化层
class Pooling:
def __init__(self, pool_h, pool_w, stride = 1, pad = 0):
self.pool_h, self.pool_w, self.stride, self.pad = pool_h, pool_w, stride, pad
def forward(self, x):
N, C, H, W = x.shape
# 计算输出数据的长和高
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
# 展开
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h * self.pool_w)
# 最大值
out = np.max(col, axis = 1)
# 转换
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out
4. MNIST 数字识别神经网络的实现
手写数字识别的CN
class SimpleConvNet:
def __init__(self, input_dim = (1, 28, 28), conv_param = {'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}, hidden_size = 100, output_size = 10, weight_init_std = 0.01):
"""
:param input_dim: 输入数据的通道数与长高
:param conv_param: 卷积层的参数,滤波器数量、维度、填充、步幅
:param hidden_size: 隐藏层神经元数量
:param output_size: 输出层神经元数量
:param weight_init_std: 初始化权重标准差
"""
filter_num = conv_param['filter_num']
filter_size = conv_param['filter_size']
filter_pad = conv_param['pad']
filter_stride = conv_param['stride']
input_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2 * filter_pad) / filter_stride + 1
pool_output_size = int(filter_num * (conv_output_size / 2) * (conv_output_size / 2))
self.params = {'W1': weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size),
'b1': np.zeros(filter_num),
'W2': weight_init_std * np.random.randn(pool_output_size, hidden_size),
'b2': np.zeros(hidden_size),
'W3': weight_init_std * np.random.randn(hidden_size, output_size),
'b3': np.zeros(output_size)}
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'], self.params['stride'], self.params['pad'])
self.layers['ReLU1'] = Relu()
self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
self.layers['ReLU2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])
self.last_layer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(t)
return self.last_layer.forward(y, t)
# 反向传播求梯度
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
grads = {'W1': self.layers['Conv1'].dW, 'b1': self.layers['Conv1'].db, 'W2': self.layers['Affine1'].dW,
'b2': self.layers['Affine1'].db, 'W3': self.layers['Affine2'].dW, 'b3': self.layers['Affine2'].db}
return grads