Tensorflow-keras 理論 & 實戰

Galois發表於2020-03-04

理論部分

Keras:

  • 基於 python 的高階神經網路 API
  • Francois Chollet 與 2014-2015 年編寫 Keras
  • 以 Tensorflow、CNTK、Theano 為後端執行,keras 必須有後端才可以執行(現在一般多用 tensorflow)
  • 極方便與快速實驗,幫助使用者以最少的時間驗證自己的想法

Tensorflow-keras:

  • Tensorflow 對 keras API 規範的實現
  • 相對於以 tensorflow 為後端的 keras,Tensorflow-keras 與Tensorflow 結合更加緊密
  • 實現在 tf.keras 空間下

Tf-keras 和 keras 聯絡:

  • 基於同一套 API(keras程式可以透過改匯入方式輕鬆轉為 tf.keras 程式;反之可能不成立,因為 tf.keras 有其他特性)
  • 相同的 JSON 和 HDF5 模型序列化格式和語義

Tf-keras 和 keras 區別:

  • Tf.keras 全面支援 eager mode
    • 只是用 keras.Sequential 和 keras.Model 時沒影響
    • 自定義 Model 內部運算邏輯的時候會有影響
      • Tf 底層 API 可以使用 keras 的 model.fit 等抽象
      • 適用於研究人員
  • Tf.keras 支援基於 tf.data 的模型訓練
  • Tf.keras 支援 TPU 訓練
  • Tf.keras 支援 tf.distribution 中的分散式策略
  • 其他特性
    • Tf.keras 可以與 Tensorflow 中的 estimator 整合
    • Tf.keras 可以儲存為 SavedModel

如果想用 tf.keras 的任何一個特性,那麼選 tf.keras
如果後端互換性很重要,那麼選 keras,如果都不重要,隨便選。

分類問題、迴歸問題、損失函式

分類問題

分類問題預測的是類別,模型的輸出是機率分佈。
三分類問題輸出例子:[0.2, 0.7, 0.1]

比如

  • 第 0 類是「貓」類,
  • 第 1 類是「狗」類,
  • 第 2 類是「狼」類。
    為什麼分類問題的模型輸出是機率分佈,這裡涉及到知識點「目標函式」

迴歸問題

迴歸問題預測的是值,模型的輸出是一個實數值。
比如房價預測問題,就屬於迴歸問題,房價是一個值。

目標函式

為什麼需要目標函式?

  • 引數是逐步調整的(不像數學的計算問題,可以直接得到值,機器學習中需要目標函式逐步調整引數來逼近準確值)
  • 分類問題舉例:目標函式可以幫助衡量模型的好壞(模型A 和 模型B 的準確率沒有區別,但 模型A 比 模型B 更接近正確結果)
    • Model A:[0.1, 0.4, 0.5]
    • Model B:[0.1, 0.2, 0.7]

分類問題需要衡量目標類別與當前預測的差距

  • 三分類問題輸出例子:[0.2, 0.7, 0.1]
  • 三分類真實類別:2 -> one_hot -> [0, 0, 1]

    One-hot 編碼:把正整數變為向量表達
    生成一個長度不小於正整數的向量,只有正整數的位置處為 1,其餘位置都為 0。

目標函式-分類問題

「平方差損失」,x,y都是向量,對應位置相減。

\displaystyle \frac{1}{n}\sum_{x,y}\frac{1}{2}(y-Model(x))^2

「交叉熵損失」,Model(x)是預測值。

\displaystyle \frac{1}{n}\sum_{x,y}y\ln(Model(x))

分類問題的平方差損失舉例:

  • 預測值:[0.2, 0.7, 0.1]
  • 真實值:[0, 0, 1]
  • 損失函式值:[(0.2-0)^2 + (0.7-0)^2 + (0.1-1)^2]*0.5
    由於預測值只有 1 個,所以 1/n = 1/1 = 1
目標函式-迴歸問題
  • 預測值與真實值的差距
  • 平方差損失
  • 絕對值損失
    「絕對值損失」

    \displaystyle \frac{1}{n}\sum_{x,y}\big|y-Model(x)\big|

模型的訓練就是調整引數,使得目標函式逐漸變小的過程。
實戰:Keras 搭建分類模型,Keras 搭建回撥函式, Keras 搭建迴歸模型。

神經網路、啟用函式、批歸一化、Dropout

神經網路

先看下三層神經網路的案例:
d4mIifWEsl.png!large
全連線層指的是層級結構中,下一層的神經單元都和上一層的神經單元相連線。
當然,每一層計算完畢之後都會用到「啟用函式」:

qC4HeduLZy.png!large

神經網路訓練

神經網路訓練使用「梯度下降」:

  • 梯度下降
    • 求導
    • 更新引數

我們可以形象的想象一下“下山演算法”:

  • 下山演算法
    • 找到方向
    • 走一步

深度神經網路

深度學習就是層次非常深的神經網路,以上我們看到的都是層次比較淺的神經網路,只有三層,如果有幾十幾百層的神經網路就叫做深度神經網路。

啟用函式

我們先介紹 6 種啟用函式:
Sigmoid

\displaystyle \sigma(x)=\frac{1}{1+e^{-x}}

tanh

\tanh(x)

ReLU

\max(0,x)

Leaky ReLU

\max(0.1x,x)

Maxout

\max(w_1^Tx+b_1,w_2^Tx+b_2)

ELU

\displaystyle \left\{ \begin{aligned} x && x\geqslant0\\ \alpha(e^x-1) && x<0 \end{aligned} \right.

啟用函式圖:

0LfKaufE4N.png!large

歸一化

歸一化是把輸入資料做一個規整,使輸入資料均值為 0,方差為 1。
還有一些其他歸一化:

  • Min-Max 歸一化:

    \displaystyle x^*=\frac{x-\min}{\max-\min}

  • Z-score 歸一化:

    \displaystyle x^*=\frac{x-\mu}{\sigma}

批歸一化

每層的啟用值都做歸一化,把歸一化的範圍從輸入資料擴充到每層啟用值。
歸一化為何有效?

NE2w0pWQDT.png!large

回顧梯度下降演算法:在當前狀態下給每一個變數都求一個導數,然後在這個導數的方向上把引數更新一點。上圖的未歸一化的兩個變數\theta_1\theta_2的資料範圍是不一樣的,所以等高線看起來像是個橢圓,因為它是個橢圓,所以當在橢圓上計算梯度「法向量」的時候,它指向的並不一定是圓心,所以會導致訓練軌跡會非常曲折。經過歸一化的資料等高線是一個正圓,這意味著「法向量」都是對著圓心的,所以歸一化之後,它的訓練速度會更快。這是歸一化有效的一個原因。

Dropout

Droutout 在深度神經網路中會用到。

除了 Dropout 來降擬合,還可以用正則化 regularizer 來降低過擬合。

wfYgCszMqh.png!large 可以看到 Dropout 在全連線層隨機棄用一些神經單元,而且每層的棄用都不一樣的,棄用是隨機性的。 Dropout 作用: - 防止過擬合(訓練集上很好,測試集上不好) - 過擬合原因:模型引數太多,模型容易記住樣本,不能泛化 當樣本輸入的時候,每層啟用的值都非常大,就容易導致模型記住樣本。

實戰:Keras 實現深度神經網路,Keras 更改啟用函式, Keras 實現批歸一化,Keras 實現 dropout。

Wide & Deep 模型

Wide & Deep 模型在 16 年釋出,用於分類和迴歸,應用到了 Google Play 中的應用推薦,原始論文:提取碼:a8rg

稀疏特徵

  • 離散值特徵
  • One-hot 表示
  • Eg:專業 = {計算機, 人文, 其他},人文 = [0, 1, 0]
  • Eg:詞表 = {人工智慧,你,我,他,張量,…},他 = [0, 0, 0, 1, 0, …]
  • 稀疏特徵之間可以做「叉乘」= {(計算機, 人工智慧), (計算機, 你), …}
  • 稀疏特徵做叉乘獲取共現資訊
  • 實現記憶的效果

稀疏特徵優點:有效,廣泛用於工業界。
稀疏特徵缺點:需要人工設計;可能過擬合,所有特徵都叉乘,相當於記住每一個樣本;泛化能力差,沒出現過就不會起效果。
例:組合問題,我很高興和我很快樂是一個意思,不能泛化。

密集特徵

向量表達:

  • Eg:詞表 = {人工智慧, 你, 他, 愣酷},他 = [0.3, 0.2, 0.6, (n維向量)]
  • Word2vec 工具
    • 男 - 女 = 國王 - 王后

密集特徵的優點:帶有語義資訊,不同向量之間有相關性;相容沒有出現過的特徵組合;更少人工參與。
密集特徵缺點:過度泛化,推薦不怎麼相關的產品。

說明完畢,來看模型:
這是 Wide&Deep 模型的通用結構。
7EK40burCg.png!large
這是 Google play上的應用推薦演算法的模型圖。
aSXzUcQ2qb.png!large

實戰:子類API,功能API(函式式API),多輸入與多輸出。

超引數搜尋

超引數用手工去試耗費人力

  • 神經網路有很多訓練過程中不變的引數
    • 網路結構引數:層數,每層寬度,每層啟用函式等
    • 訓練引數:batch_size,學習率,學習率衰減演算法等

      batch_size 指的是一次訓練從訓練資料中選多少資料塞到神經網路中去。

搜尋策略

  • 網格搜尋
  • 隨機搜尋
  • 遺傳演算法搜尋
  • 啟發式搜尋
網格搜尋

XrCcLPjm5N.png!large

網格搜尋步驟:

  • 定義 n 維方格
  • 每個方格對應一組超引數
  • 一組一組引數嘗試
隨機搜尋

網格搜尋有個缺點,都只能取幾個固定的值,比如上面的網格搜尋圖示中取了 DropoutRate = [0.2, 0.4, 0.6, 0.8],但如果最優值是 0.5 那麼我們的網格搜尋將永遠不可能找到最優解。
隨機搜尋的兩個好處:

  • 引數的生成方式為隨機
  • 可探索的空間更大

3lCxFj2DmX.png!large

遺傳演算法

遺傳演算法是對自然界的模擬
A. 初始化候選引數集合 -> 訓練 -> 得到模型指標作為生存機率
B. 選擇 -> 交叉 -> 變異 -> 產生下一代集合
C. 重新到 A

啟發式搜尋
  • 研究熱點-AutoML
  • 迴圈神經網路來生成引數
  • 使用強化學習來進行反饋,使用模型來訓練生成引數

實戰:使用 scikit 實現超引數搜尋。

實戰部分

為了方便閱讀程式碼,以下所有程式碼都是在 JupyterNotebook 上的,每個程式碼塊記得執行程式碼。

Keras 搭建分類模型

來做個影像分類,資料集用:fashion_mnist。(以前學過深度學習的人都對 mnis 不陌生,mnis 就是個手寫字型影像資料。)

資料讀取與展示

匯入庫:

import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import sklearn
import pandas as pd
import os
import sys
import time
import tensorflow as tf

from tensorflow import keras
print(tf.__version__)
print(sys.version_info)
for module in mpl, np, pd, sklearn, tf, keras:
    print(module.__name__, module.__version__)

輸出:

2.1.0
sys.version_info(major=3, minor=6, micro=4, releaselevel=’final’, serial=0)
matplotlib 2.2.3
numpy 1.18.1
pandas 0.22.0
sklearn 0.19.1
tensorflow 2.1.0
tensorflow_core.python.keras.api._v2.keras 2.2.4-tf

匯入資料:

fashion_mnist = keras.datasets.fashion_mnist

把訓練集和測試集都拆分出來:

(x_train_all, y_train_all), (x_test, y_test) = fashion_mnist.load_data()

再把訓練集拆分成訓練集和驗證集,因為這個資料集有 60000 張圖片,所以我們把前 5000 張圖片作為驗證集,後面 55000 張作為訓練集:

x_valid, x_train = x_train_all[:5000], x_train_all[5000:]
y_valid, y_train = y_train_all[:5000], y_train_all[5000:]
# 現在列印一下驗證集, 訓練集, 測試集
print(x_valid.shape, y_valid.shape)
print(x_train.shape, y_train.shape)
print(x_test.shape, y_test.shape)
# 輸出
(5000, 28, 28) (5000,)
(55000, 28, 28) (55000,)
(10000, 28, 28) (10000,)

得到影像資料集後,需要看下影像是什麼樣子,這樣有助於瞭解資料集,瞭解資料集是機器學習工作中很重要的一部分。
接下來定一個函式用作展示影像:

def show_single_image(img_arr):
    plt.imshow(img_arr, cmap="binary")
    plt.show()
# 呼叫函式顯示第 1 張圖片
show_single_image(x_train[0])

我們會看到這樣一張圖片:

1ArIiGyB28.png!large

只顯示一張圖片可能不是那麼直觀,定義一個顯示多影像顯示的函式:

def show_imgs(n_rows, n_cols, x_data, y_data, class_name):
    assert len(x_data) == len(y_data)
    assert n_rows * n_cols < len(x_data)
    plt.figure(figsize = (n_cols * 1.4, n_rows * 1.6))
    for row in range(n_rows):
        for col in range(n_cols):
            index = n_cols * row + col
            plt.subplot(n_rows, n_cols, index+1)
            plt.imshow(x_data[index], cmap="binary", interpolation="nearest")
            plt.axis('off')
            plt.title(class_names[y_data[index]])
    plt.show()
class_names = ['T-shirt', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
# 呼叫函式顯示 15 張圖片
show_imgs(3, 5, t_train, y_train, class_names)

模型構建

使用 tf.keras.models.Sequential() 來構建模型。

# 初始化訓練模型
model = keras.models.Sequential()
# 模型新增層
model.add(keras.layers.Flatten(input_shape=[28, 28]))
model.add(keras.layers.Dense(300, activation="relu"))
model.add(keras.layers.Dense(100, activation="relu"))
model.add(keras.layers.Dense(10, activation="softmax"))
"""
其實新增模型可以用另一種寫法(直接在模型初始化中設定模型層):
model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation='relu'),
    keras.layers.Dense(100, activation='relu'),
    keras.layers.Dense(10, activation='softmax')
])
"""
# 有了以上的機率分佈,就可以用目標函式了
model.compile(loss="sparse_categorical_crossentropy", optimizer="sgd", metrics=["accuracy"])

Flatten:展平,keras.layers.Flatten(input_shape[28, 28]) 把二維向量(28x28)展成一維向量(1x784)。
keras.layers.Dense(300, activation="relu") 意思是神經元數量為 300,啟用函式為 ReLU 的全連線層。
最後一層作為輸出層,設定 10 個輸出節點,因為這個問題是個 10 個類別的分類問題。
relu:y = max(0, x)
softmax 是將向量變成機率分佈:

\displaystyle x = [x_1,x_2,x_3]\\{}\\ y = \left[\frac{e^{x_1}}{\Sigma},\frac{e^{x_2}}{\Sigma},\frac{e^{x_3}}{\Sigma}\right]\\{}\\ \sum=e^{x_1}+e^{x_2}+e^{x_3}

model.compile 中:
loss 是損失函式屬性,屬性值 crossentropy 是交叉熵損失函式,我們的y是長度等於樣本數的向量,對於每個樣本來說只是「一個值」,y 是一個 index 值,所以用 sparse_categorical_crossentropy,如果 y 是透過 one_hot 輸出的向量,那這裡就用 categroical_crossentropy
optimizer 是模型調整方法(最佳化方法),我們需要調整引數使得目標函式越來越小。
metrics 是把 lossoptimizer 都加入到模型圖中去。
檢視模型層數:

model.layers

檢視模型概況:

model.summary()

我們看到第一層(Flatten 層)是樣本數乘以 784 的矩陣,經過全連線層之後變成樣本數乘以 300 的矩陣:[None, 784] -> [None, 300],這需要讓 [None, 784] 乘以一個矩陣 W,在全連線層裡面加一個偏置 b,W.shape=[784, 300],b 是長度為 300 的一個向量,所以第二層長度是 784x300+300 = 235500。
模型設計好了,接下來開啟訓練:

history = model.fit(x_train, y_train, epochs=10, validation_data=(x_valid, y_valid))

其中 x_train, y_train 是訓練集,epochs 是訓練次數,validation_data 是每次訓練的驗證,驗證資料用的是 x_valid, y_valid
model.fit 可以返回值,把資料結果返回給 history
訓練完畢,我們可以看下 type(history)history 是一個 tensorflow.python.keras.callbacks.History
檢視訓練的準確率與誤差的歷史資料:

history.history

我們可以列印訓練的準確率與誤差的統計圖:

def plot_learning_curves(history):
    # 把訓練指標資料轉成 pd.DataFrame 格式
    pd.DataFrame(history.history).plot(figsize=(8, 5))
    # 顯示網格
    plot.grid(True)
    # 設定座標軸範圍
    plot.gca().set_ylim(0, 1)
    plt.show()
plot_learning_curves(history)

這樣就完成了一個完整的分類模型:資料處理 -> 模型構建 -> 模型訓練 -> 指標圖示列印。

在「影像分類」領域有一個非常有助於提升準確率的手段:歸一化(對訓練資料進行操作)。

歸一化

接著上例的程式碼的資料處理後面做歸一化:

import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import sklearn
import pandas as pd
import os
import sys
import time
import tensorflow as tf

from tensorflow import keras
print(tf.__version__)
print(sys.version_info)
for module in mpl, np, pd, sklearn, tf, keras:
    print(module.__name__, module.__version__)

fashion_mnist = keras.datasets.fashion_mnist
(x_train_all, y_train_all), (x_test, y_test) = fashion_mnist.load_data()
x_valid, x_train = x_train_all[:5000], x_train_all[5000:]
y_valid, y_train = y_train_all[:5000], y_train_all[5000:]

print(x_valid.shape, y_valid.shape)
print(x_train.shape, y_train.shape)
print(x_test.shape, y_test.shape)

歸一化方法:

\displaystyle x=\frac{x-\mu}{\sigma^2}

\mu均值,\sigma^2方差。
可以用 print(np.max(x_train), np.min(x_train)) 檢視訓練集的最大值最小值,會列印出最大值 255,最小值 0。
sklearn.preprocessing 裡面的 StandardScaler 來實現歸一化:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
# 訓練集歸一化用 fit_transform
x_train_scaled = scaler.fit_transform(
    x_train.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)
# 驗證集測試集歸一化用 transform
x_valid_scaled = scaler.transform(
    x_valid.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)
x_test_scaled = scaler.transform(
    x_test.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)

歸一化涉及到除法,所以先將資料轉為 float32。
現在可以 print(np.max(x_train_scaled), np.min(x_train_scaled)) 列印看看歸一化後的訓練集最大值最小值。
然後設定訓練模型後訓練歸一化後的資料:

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation='relu'),
    keras.layers.Dense(100, activation='relu'),
    keras.layers.Dense(10, activation='softmax')
])

model.compile(loss="sparse_categorical_crossentropy",
             optimizer = "sgd",
             metrics = ["accuracy"])

# 訓練歸一化後的資料:
history = model.fit(x_train, y_train, epochs=10,
                    validation_data=(x_valid, y_valid))

最後列印學習曲線圖:

def plot_learning_curves(history):
    pd.DataFrame(history.history).plot(figsize=(8, 5))
    plt.grid(True)
    plt.gca().set_ylim(0, 1)

plot_learning_curves(history)

可以與之前未歸一化的訓練指標對比下。
繼續在測試集上進行指標的評估:

model.evaluate(x_test_scaled, y_test)

Keras 回撥函式

回撥函式在 TensorFlow for Python API 官方文件tf.keras 下的 callbacks 裡,回撥函式是作用在訓練模型中的操作。涉及到一些 callbacks,不過常用的是 EarlyStoppingModelCheckpointTensorBoard。其中 EarlyStopping 是在模型訓練過程中 Loss 不再下降的時候,可以中止訓練。下面就展示下這三個 callback 的用法。
因為這是作用域訓練過程中的操作,所以直接把上面歸一化的例子中的訓練部分程式碼拿下來:

history = model.fit(x_train, y_train, epochs=10,
                    validation_data=(x_valid, y_valid))

修改成:

# 定義資料夾
logdir = './callbacks'
if not os.path.exists(logdir):
    os.mkdir(logdir)
# 定義輸出的 Model 檔案
output_model_file = os.path.join(logdir, "fashion_mnist_model.h5")
# 定義 callbacks
callbacks = [
    keras.callbacks.TensorBoard(logdir),
    keras.callbacks.ModelCheckpoint(out_model_file, save_best_only=True)    # save_best_only:儲存最好的模型,不設定的話,預設儲存最近的一個模型
    keras.callbacks.EarlyStopping(patience=5, min_delta=1e-3),
]

history = model.fit(x_train, y_train, epochs=10,
                    validation_data=(x_valid, y_valid), callbacks=callbacks)

對於 Tensorboard 來說,需要一個資料夾;對於 ModelCheckpoint 來說,需要一個檔名。
Earlystopping 中有三個重要的屬性 monitormin_deltapatience
monitor 設定關注指標,一般關注驗證集上,目標函式的值。
min_delta 是一個閾值,這次的訓練與上次的訓練的差距,如果比這個閾值高的話,就不用 EarlyStopping,如果比這個閾值低的話,就會用上 EarlyStopping 提前停止訓練。
patience 表示 EarlyStopping 的耐心,設定允許低於閾值的次數,超出次數了,就會中止訓練。
執行後,我們在專案目錄下使用 tree 命令來列印目錄結構,會發現 callbacks 資料夾內多出了一些檔案:
ModelCheckpoint 檔案:fashion_mnist_model.h5
還有兩個資料夾 trainvalidation 儲存的是 TensorBoard 的檔案。
然後在當前專案下開啟 Tensorboard

$ tensorboard --logir=callbacks

然後用瀏覽器訪問 localhost:6006 會看到 Tensorboard 的介面。

Keras 搭建迴歸模型

這是個房價預測問題。
資料集:

import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import sklearn
import pandas as pd
import os
import sys
import time
import tensorflow as tf

from tensorflow import keras
# 資料集
from sklearn.datasets import fetch_california_housing

housing = fetch_california_housing()
print(housing.DESCR)
print(housing.data.shape)
print(housing.target.shape)

# 列印瞭解資料
import pprint
pprint.pprint(housing.data[0:5])
pprint.pprint(housing.target[0:5])

資料集劃分:

from sklearn.model_selection import train_test_split

x_train_all, x_test, y_train_all, y_test = train_test_split(housing.data, housing.target, random_state=7)
x_train, x_valid, y_train, y_valid = train_test_split(x_train_all, y_train_all, random_state=11)
print(x_train.shape, y_train.shape)
print(x_valid.shape, y_valid.shape)
print(x_test.shape, y_test.shape)

資料歸一化:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(x_train)
x_valid_scaled = scaler.fit_transform(x_valid)
x_test_scaled = scaler.fit_transform(x_test)

模型構建:

model = keras.models.Sequential([
    keras.layers.Dense(30, activation='relu', input_shape=x_train.shape[1:]),
    keras.layers.Dense(1),
])
model.summary()
model.compile(loss="mean_squared_error", optimizer="sgd")
callbacks = [keras.callbacks.EarlyStopping(patience=5, min_delta=1e-2)]

訓練資料:

history = model.fit(x_train_scaled, y_train, validation_data=(x_valid_scaled, y_valid), epochs=100, callbacks=callbacks)

學習曲線圖:

def plot_learning_curves(history):
    pd.DataFrame(history.history).plot(figsize=(8, 5))
    plt.grid(True)
    plt.gca().set_ylim(0, 1)
    plt.show()
plot_learning_curves(history)

測試集評估模型:

model.evaluate(x_test_scaled, y_test)

Keras 搭建深度神經網路

把分類模型搭建的程式碼塊改成:

model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28, 28]))
# 搭建 20 層神經網路
for _ in range(20):
    model.add(keras.layers.Dense(100, activation="relu"))
model.add(keras.layers.Dense(10, activation="softmax"))

model.compile(loss="sparse_categorical_crossentropy", optimizer="sgd", metrics=["accuracy"])

然後可以看下模型的 summary()

model.summary

訓練時,可以把這個深度神經網路的 Tensorboard 資料夾定義在 logdir = './dnn-callbacks'
可以看到 Tensorboard 顯示學習曲線圖:

y3j4TxNogP.png!large

或者直接用指令碼列印:

def plot_learning_curves(history):
    pd.DataFrame(history.history).plot(figsize=(8, 5))
    plt.grid(True)
    plt.gca().set_ylim(0, 3)

plot_learning_curves(history)

51vaDOq3PI.png!large 可以看到這個學習曲線圖有點不一樣,後面接近平滑,那是因為我們的深層神經網路(20 層 Dense layer)引數眾多,導致訓練不充分,以及「梯度消失」,梯度消失一般發生在深度神經網路裡,導致梯度消失的原因是「鏈式法則」,用在複合函式求導上面。

對於多層神經網路來說,離目標函式比較遠的底層神經網路的梯度比較微小的一個現象叫做「梯度消失」。
複合函式:f(g(x))

測試集指標評估:

model.evaluate(x_test_scaled, y_test)

評估結果:

10000/10000 [==============================] - 0s 43us/sample - loss: 0.4111 - accuracy: 0.8619
[0.4111427655220032, 0.8619]

批歸一化

在啟用後批歸一化:

model = keras.models.Sequential()
model.add(keras.layes.Flatten(input_shape=[28, 28]))
for _ in range(20):
    model.add(keras.layers.Dense(100, activation="relu"))
    # 批歸一化
    model.add(keras.layers.BatchNormalization())

model.add(keras.layers.Dense(10, activation="softmax"))

model.compile(loss="sparse_categorical_crossentropy", optimizer="sgd", metrics=["accuracy"])

在啟用前批歸一化:

model = keras.models.Sequential()
model.add(keras.layes.Flatten(input_shape=[28, 28]))
for _ in range(20):
    model.add(keras.layers.Dense(100))
    # 批歸一化
    model.add(keras.layers.BatchNormalization())
    # 啟用函式
    model.add(keras.layers.Activation('relu'))

model.add(keras.layers.Dense(10, activation="softmax"))

model.compile(loss="sparse_categorical_crossentropy", optimizer="sgd", metrics=["accuracy"])

model.summary 後可以看到批歸一化的層次結構。
批歸一化能緩解「梯度消失」。

自帶歸一化的啟用函式 Selu

model = keras.models.Sequential()
model.add(keras.layes.Flatten(input_shape=[28, 28]))
for _ in range(20):
    model.add(keras.layers.Dense(100, activation="selu"))
model.add(keras.layers.Dense(10, activation="softmax"))

model.compile(loss="sparse_categorical_crossentropy", optimizer="sgd", metrics=["accuracy"])

logdir = './dnn-selu-callbacks'
學習曲線圖:

9voBkZSdRg.png!large

可以看到 Selu 比 Relu+歸一化 訓練快一些,指標也很快 進入狀態了。

Dropout

一般情況下,不會給每一層都做 Dropout,而是給最後幾層做 Dropout

model = keras.models.Sequential()
model.add(keras.layes.Flatten(input_shape=[28, 28]))
for _ in range(20):
    model.add(keras.layers.Dense(100, activation="selu"))
# Dropout 相當於對前面一層作 Dropout
model.add(keras.layers.AlphaDropout(rate=0.5))
model.add(keras.layers.Dense(10, activation="softmax"))

model.compile(loss="sparse_categorical_crossentropy", optimizer="sgd", metrics=["accuracy"])

純「Dropout」是:model.add(keras.layers.Dropout(rate=0.5))
AlphaDropout 是一個更加強大的「Dropout」:

  1. 均值和方差不變
  2. 歸一化的性質也不變

如果資料過擬合比較輕,不適合作 Dropout 處理,Dropout 是針對緩解資料過擬合的。

函式式 API 實現 Wide&Deep 模型

直接看上面的房價預測迴歸模型的程式碼塊:

model = keras.models.Sequential([
    keras.layers.Dense(30, activation='relu', input_shape=x_train.shape[1:]),
    keras.layers.Dense(1),
])
model.summary()

由於 Wide&Deep 模型不是嚴格的層級結構,而是由兩部分組成的,每一部分都是一個層級結構,所以我們不能用 Sequential 去實現模型了。所以我們用函式式 API 對模型進行實現。

# 函式式API 功能API
input = keras.layers.Input(shape=x_train.shape[1:]) # 讀取資料
hidden1 = keras.layers.Dense(30, activation='relu')(input) # (input)之前的可以看作是一個函式,input 是這個函式的輸入引數
hidden2 = keras.layers.Dense(20, activation='relu')(hidden1)
# 複合函式形式:f(x) = h(g(x))

# 輸出之後需要合併模型,這裡我們假設 Wide模型 和 Deep模型 是一樣的
concat = keras.layers.concatenate([input, hidden2]) # 拼接 input 和 hedden2
output = keras.layers.Dense(1)(concat) # 把拼接好的資料賦給 output

# 函式式API 寫法需要用 keras.models.Model() 固化模型
model = keras.models.Model(inputs = [input], outputs = [output])

子類 API 實現 Wide&Deep 模型

# 子類API
class WideDeepModel(keras.models.Model):
    def __init__(self):
        super(WideDeepModel, self).__init__()
        """定義模型的層次"""
        self.hidden1_layer = keras.layers.Dense(30, activation='relu')
        self.hidden2_layer = keras.layers.Dense(30, activation='relu')
        self.output_layer = keras.layers.Dense(1)

    def call(self, input):
        """完成模型的正向計算"""
        hidden1 = self.hidden1_layer(input)
        hidden2 = self.hidden2_layer(hidden1)
        concat = keras.layers.concatenate([input, hidden2])
        output = self.output_layer(concat)
        return output

model = WideDeepModel()
"""
也可以寫成
model = keras.models.Sequential([
    WideDeepModel(),
])
"""
model.build(input_shape=(None, 8))

目前為止用的 Wide 和 Deep 模型都是一樣的,下面看下多輸入與多輸出。

Wide&Deep 模型的多輸入與多輸出

多輸入神經網路:
選前 5 個 ficher 當作是 Wide 模型的輸入,取後 6 個 ficher 當作是 Deep 模型的輸入。

# 多輸入
input_wide = keras.layers.Input(shape=[5])
input_deep = keras.layers.Input(shape=[6])
hidden1 = keras.layers.Dense(30, activation='relu')(input_deep)
hidden2 = keras.layers.Dense(30, activation='relu')(hidden1)
concat = keras.layers.concatenate([input_wide, hidden2])
output = keras.layers.Dense(1)(concat)
model = keras.models.Model(inputs=[input_wide, input_deep], outputs=[output])

model.summary()
model.compile(loss="mean_squared_error", optimizer="sgd")
callbacks = [keras.callbacks.EarlyStopping(patience=5, min_delta=1e-2)]

訓練資料也要作出改變,因為有兩組資料:

x_train_scaled_wide = x_train_scaled[:, :5]
x_train_scaled_deep = x_train_scaled[:, 2:]
x_valid_scaled_wide = x_valid_scaled[:, :5]
x_valid_scaled_deep = x_valid_scaled[:, 2:]
x_test_scaled_wide = x_test_scaled[:, :5]
x_test_scaled_deep = x_test_scaled[:, 2:]

history = model.fit([x_train_scaled_wide, x_train_scaled_deep],
    y_train,
    validation_data=([x_valid_scaled_wide, x_valid_scaled_deep], y_valid),
    epochs=100,
    callbacks=callbacks)

測試集評估:

model.evaluate([x_test_scaled_wide, x_test_scaled_deep], y_test)

多輸出神經網路主要針對多工學習的問題,和 Wide&Deep 多輸入沒關係,比如房價預測問題預測的是當前的房價,不過我們還需要預測一年後的房價是多少,這樣就有了兩個預測任務,這個模型就需要給出兩個結果。試試上面的房價預測模型在 hidden2 後再輸出一個值:

# 多輸入多輸出
input_wide = keras.layers.Input(shape=[5])
input_deep = keras.layers.Input(shape=[6])
hidden1 = keras.layers.Dense(30, activation='relu')(input_deep)
hidden2 = keras.layers.Dense(30, activation='relu')(hidden1)
concat = keras.layers.concatenate([input_wide, hidden2])
output = keras.layers.Dense(1)(concat)
output2 = keras.layers.Dense(1)(hidden2)
model = keras.models.Model(inputs=[input_wide, input_deep], outputs=[output, output2])
# 這樣在網路結構部分就有了兩個輸出的網路結構

這樣資料訓練也需要兩個輸出,y也要變成兩份:

history = model.fit([x_train_scaled_wide, x_train_scaled_deep],
                    [y_train, y_train],
                    validation_data=([x_valid_scaled_wide, x_valid_scaled_deep], [y_valid, y_valid]),
                    epochs=100,
                    callbacks=callbacks)

學習曲線圖:

mlwxTLTOJU.png!large

測試集模型評估:

model.evaluate([x_test_scaled_wide, x_test_scaled_deep], [y_test, y_test])

評估結果:

5160/5160 [==============================] - 0s 21us/sample - loss: 0.9603 - dense_2_loss: 0.4309 - dense_3_loss: 0.5326
[0.9603453163028688, 0.43093655, 0.5325812]

Keras 與 scikit-learn 實現超引數搜尋

手動實現超引數搜尋

這裡改變回歸模型的模型搭建部分的程式碼,不依賴於 sklearn 的超引數搜尋實現。
這裡就來手動搜尋下學習率這個超引數。
神經網路訓練迭代公式:

\displaystyle W_n=W_{n-1}+\nabla f\cdot learningRate

# learning_rate: [1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2]
# W = W + grad * learning_rate
learning_rate = [1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2]
# 儲存所有的 history
histories = []
for lr in learning_rates:
model = keras.models.Sequential([
        keras.layers.Dense(30, activation='relu', input_shape=x_train.shape[1:]),
        keras.layers.Dense(1),
    ])
    # 定義 optimizer
    optimizer = keras.optimizers.SGD(lr)

    model.compile(loss="mean_squared_error", optimizer=optimizer)
    callbacks = [keras.callbacks.EarlyStopping(patience=5, min_delta=1e-2)]

之前的模型 optimizer="sgd"sgd 是隨機梯度下降,現在用自己的 lr 去初始化 optimizer
然後我們儲存所有的 history

history = model.fit(x_train_scaled, y_train, validation_data=(x_valid_scaled, y_valid), epochs=100, callbacks=callbacks)
histories.append(history)

列印所有的 history 學習曲線圖:

def plot_learning_curves(history):
    pd.DataFrame(history.history).plot(figsize=(8, 5))
    plt.grid(True)
    plt.gca().set_ylim(0, 1)
    plt.show()
for lr, history in zip(learning_rates, histories):
    print("learning rate: ", lr)
    plot_learning_curves(history)

由於是在篩選學習速率超引數,所以不用測試集評估模型。
一般情況下梯度係數也就是學習率是衰減的,這裡是從小到大遞增,所以會看到到後面會發生「資料爆炸」。
在現實情況中一般會定義很多超引數,這裡一個學習率超引數就用了一個 for,如果有很多個超引數,就需要很多個 for,這樣就無法「並行化」計算,因為每個超引數計算都需要把上一個超引數計算完,這就加大了超引數搜尋的程式設計難度,這個演算法無法並行化計算,這樣做超引數搜尋也不太現實,所以最好藉助 sklearn 庫的超引數搜尋策略來實現超引數搜尋。

sklearn 封裝 keras 模型

RandomizedSearchCVsklearn 裡面的一個函式,首先要把 tf.keras 的 Model 轉化成 sklearn 形式的 Model。先定義一個 tf.keras 的 Model,然後呼叫一個函式把這個 Model 封裝成 sklearn 的 Model。去 官方文件 中查詢 tf.keras -> wrappers -> scikit_learn。如果是迴歸模型用 KerasRegressor,如果是分類模型用 KerasClassifier

# RandomizedSearchCV
# 1. 轉化為sklearn的model
# 2. 定義引數集合
# 3. 搜尋引數

def build_model(hidden_layers=1, layer_size=30, learning_rate=3e-3):
    model = keras.models.Sequential()
    model.add(keras.layers.Dense(layer_size, activation='relu', input_shape=x_train.shape[1:]))
    for _ in range(hidden_layers - 1):
        model.add(keras.layers.Dense(layer_size, activation='relu'))
    model.add(keras.layers.Dense(1))
    optimizer = keras.optimizers.SGD(learning_rate)
    model.compile(loss='mse', optimizer=optimizer)
    return model

sklearn_model = keras.wrappers.scikit_learn.KerasRegressor(build_model)
callbacks = [keras.callbacks.EarlyStopping(patience=5, min_delta=1e-2)]
history = sklearn_model.fit(x_train_scaled, y_train, epochs=100,validation_data=(x_valid_scaled, y_valid),callbacks=callbacks)

sklearn 的 Model 沒有 evaluate

檢視學習曲線:

def plot_learning_curves(history):
    pd.DataFrame(history.history).plot(figsize=(8, 5))
    plt.grid(True)
    plt.gca().set_ylim(0, 1)
    plt.show()
plot_learning_curves(history)

sklearn 超引數搜尋

keras 模型被轉成 sklearn 模型後,就可以用 RandomizedSearchCV
現在需要定義需要搜尋的超引數的範圍,至於是哪些引數,在定義 build_model 的時候已經指定了哪些引數:def build_model(hidden_layers=1,layer_size=30,learning_rate=3e-3):

# reciprocal 是一個分佈
from scipy.stats import reciprocal
# f(x) = 1/(x*log(b/a)) a <= x <= b

param_distribution = {
    "hidden_layers":[1, 2, 3, 4],
    "layer_size": np.arange(1, 100),
    # learning_rate 取連續的值,呼叫 reciprocal 函式
    "learning_rate": reciprocal(1e-4, 1e-2),
}

reciprocal分佈解析:

\displaystyle f(x)=\frac{1}{x\log(\frac{b}{a})}\\{}\\ a\leqslant x\leqslant b

可以生成十個數測試下這個分佈:

from scipy.stats import reciprocal
reciprocal.rvs(1e-4, 1e-2, size=10)

然後呼叫 RandomizedSearchCV
輸入值:

  • sklearn_model
  • 引數分佈
  • 生成引數集合的個數
  • cross_validation 機制中的 n 值
  • 並行處理的任務個數(預設不能大於 1,可透過別的方式修改)
from sklearn.model_selection import RandomizedSearchCV

random_search_cv = RandomizedSearchCV(sklearn_model,
                param_distribution,
                n_iter=10,
                '''
                預設 cv=3,可以設定別的值
                cv=3
                '''
                n_jobs=1)

開啟超引數搜尋:

random_search_cv.fit(x_train_scaled,
                    y_train,
                    epochs=100,
                    validation_data=(x_valid_scaled, y_valid),
                    callbacks=callbacks)

搜尋結束之後,可以列印下最佳引數組:

# 最佳引數組
print(random_search_cv.best_params_)
# 最佳分值
print(random_search_cv.best_score_)
# 最佳模型
print(random_search_cv.best_estimator_)

輸出:

{‘hidden_layers’: 4, ‘layer_size’: 58, ‘learning_rate’: 0.005740738090802875}
-0.34587740203258194
<tensorflow.python.keras.wrappers.scikit_learn.KerasRegressor object at 0x1400cb828>

測試集評估:

model = random_search_cv.best_estimator_.model
model.evaluate(x_test_scaled, y_test)

輸出:

5160/5160 [==============================] - 0s 21us/sample - loss: 0.3982
0.398169884478399

在搜尋引數的過程中,會看到 Train on 7740 samples, validate on 3870 samples,而不是之前訓練模型的 Train on 11610 samples, validate on 3870 samples。每個搜尋遍歷 7740 個樣本,這是因為搜尋引數遵循 cross_validation 機制,這個機制說的是:
訓練集分成n份,n-1份訓練,最後1份驗證,可以看到最後一次訓練,遍歷資料仍然變成了 11610 個。預設情況下 n=3,可以透過修改 RandomizedSearchCV 裡的屬性 CV 值來改變n

本作品採用《CC 協議》,轉載必須註明作者和本文連結
不要試圖用百米衝刺的方法完成馬拉松比賽。

相關文章