[魚書筆記] 深度學習入門:基於 Python 的理論與實現 個人筆記分享


為了完成畢設, 最近開始入門深度學習.

在此和大家分享一下本人閱讀魚書時的筆記,若有遺漏,歡迎斧正!

若轉載請注明出處!

一、感知機

感知機(perceptron)接收多個輸入信號,輸出一個信號。

如圖感知機,其接受兩個輸入信號。其中 \(\theta\)閾值,超過閾值 神經元就會被激活。

43525940-0082e368-95de-11e8-820a-00246e9319c1

感知機的局限性在於,它只能表示由一條直線分割的空間,即線性空間。多層感知機可以實現復雜功能。

二、神經網絡

神經網絡由三部分組成:輸入層、隱藏層、輸出層

021735264703915

1. 激活函數

激活函數將輸入信號的總和轉換為輸出信號,相當於對計算結果進行簡單篩選和處理

如圖所示的激活函數為階躍函數

798b-hqhtuak1849533

1) sigmoid 函數

sigmoid函數是常用的神經網絡激活函數。

其公式為:

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

1200px-Logistic-curve.svg

如圖所示,其輸出值在 0到 1 之間。

2) ReLU 函數

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

1_oePAhrm74RNnNEolprmTaQ

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。

濾波器會提取邊緣或斑塊等原始信息。

991470-20190208153350319-550315824

如圖,輸入數據大小是 \((5,5)\),濾波器大小是 \((3,3)\),輸出大小是 \((3,3)\)

2) 填充 Padding

在進行卷積層處理前,有時要像輸入數據的周圍填入固定數據來擴充數據。

使用填充主要是為了調整輸出的大小。

20200627220542818

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 圖像為例,其縱深方向上的特征圖增加了。通道方向上有多個特征圖時,會按通道進行輸入數據和濾波器的卷積運算,並將結果相加。

20201018160635682

輸入數據的通道數和濾波器的通道數要相同。

20210312152834633

當有多個濾波器時,輸出特征圖同樣有多層。

2. 池化層

池化是縮小高、長方向上的空間運算。簡單來說,池化用來精簡數據。

Max池化上獲取最大值的運算。一般來說,池化窗口大小會和步幅相同。

1626181662-20f4c390753c11e

3. 卷積層和池化層的實現

一個關鍵的函數為 im2col。它將輸入的三維數據展開為二維矩陣以適合濾波器。

im2col_corrected

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

20200702223924500
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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM