[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

为了完成毕设, 最近开始入门深度学习.

在此和大家分享一下本人阅读鱼书时的笔记,若有遗漏,欢迎斧正!

若转载请注明出处!

一、感知机

感知机(perceptron)接收多个输入信号,输出一个信号。

如图感知机,其接受两个输入信号。其中 \(\theta\) 为阈值,超过阈值 神经元就会被激活。

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

感知机的局限性在于,它只能表示由一条直线分割的空间,即线性空间。多层感知机可以实现复杂功能。

二、神经网络

神经网络由三部分组成:输入层、隐藏层、输出层

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

1. 激活函数

激活函数将输入信号的总和转换为输出信号,相当于对计算结果进行简单筛选和处理

如图所示的激活函数为阶跃函数

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

1) sigmoid 函数

sigmoid函数是常用的神经网络激活函数。

其公式为:

\[h(x)=\frac{1}{1+e^{-x}} \]

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

如图所示,其输出值在 0到 1 之间。

2) ReLU 函数

ReLU(Rectified Linear Unit)函数是最近常用的激活函数。

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

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。

滤波器会提取边缘或斑块等原始信息。

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

如图,输入数据大小是 \((5,5)\),滤波器大小是 \((3,3)\),输出大小是 \((3,3)\)。

2) 填充 Padding

在进行卷积层处理前,有时要像输入数据的周围填入固定数据来扩充数据。

使用填充主要是为了调整输出的大小。

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

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 图像为例,其纵深方向上的特征图增加了。通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加。

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

输入数据的通道数和滤波器的通道数要相同。

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

当有多个滤波器时,输出特征图同样有多层。

2. 池化层

池化是缩小高、长方向上的空间运算。简单来说,池化用来精简数据。

Max池化上获取最大值的运算。一般来说,池化窗口大小会和步幅相同。

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

3. 卷积层和池化层的实现

一个关键的函数为 im2col。它将输入的三维数据展开为二维矩阵以适合滤波器。

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享

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) 池化层

[鱼书笔记] 深度学习入门:基于 Python 的理论与实现 个人笔记分享
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
上一篇:深度学习的前向传播/反向传播总结


下一篇:为什么交叉熵和KL散度在作为损失函数时是近似相等的