理論部分
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
神經網路
先看下三層神經網路的案例:
全連線層指的是層級結構中,下一層的神經單元都和上一層的神經單元相連線。
當然,每一層計算完畢之後都會用到「啟用函式」:
神經網路訓練
神經網路訓練使用「梯度下降」:
- 梯度下降
- 求導
- 更新引數
我們可以形象的想象一下“下山演算法”:
- 下山演算法
- 找到方向
- 走一步
深度神經網路
深度學習就是層次非常深的神經網路,以上我們看到的都是層次比較淺的神經網路,只有三層,如果有幾十幾百層的神經網路就叫做深度神經網路。
啟用函式
我們先介紹 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.
啟用函式圖:
歸一化
歸一化是把輸入資料做一個規整,使輸入資料均值為 0,方差為 1。
還有一些其他歸一化:
- Min-Max 歸一化:
\displaystyle x^*=\frac{x-\min}{\max-\min}
- Z-score 歸一化:
\displaystyle x^*=\frac{x-\mu}{\sigma}
批歸一化
每層的啟用值都做歸一化,把歸一化的範圍從輸入資料擴充到每層啟用值。
歸一化為何有效?
回顧梯度下降演算法:在當前狀態下給每一個變數都求一個導數,然後在這個導數的方向上把引數更新一點。上圖的未歸一化的兩個變數\theta_1和\theta_2的資料範圍是不一樣的,所以等高線看起來像是個橢圓,因為它是個橢圓,所以當在橢圓上計算梯度「法向量」的時候,它指向的並不一定是圓心,所以會導致訓練軌跡會非常曲折。經過歸一化的資料等高線是一個正圓,這意味著「法向量」都是對著圓心的,所以歸一化之後,它的訓練速度會更快。這是歸一化有效的一個原因。
Dropout
Droutout 在深度神經網路中會用到。
除了 Dropout 來降擬合,還可以用正則化 regularizer 來降低過擬合。
可以看到 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 模型的通用結構。
這是 Google play上的應用推薦演算法的模型圖。
實戰:子類API,功能API(函式式API),多輸入與多輸出。
超引數搜尋
超引數用手工去試耗費人力
- 神經網路有很多訓練過程中不變的引數
- 網路結構引數:層數,每層寬度,每層啟用函式等
- 訓練引數:batch_size,學習率,學習率衰減演算法等
batch_size 指的是一次訓練從訓練資料中選多少資料塞到神經網路中去。
搜尋策略
- 網格搜尋
- 隨機搜尋
- 遺傳演算法搜尋
- 啟發式搜尋
網格搜尋
網格搜尋步驟:
- 定義 n 維方格
- 每個方格對應一組超引數
- 一組一組引數嘗試
隨機搜尋
網格搜尋有個缺點,都只能取幾個固定的值,比如上面的網格搜尋圖示中取了 DropoutRate = [0.2, 0.4, 0.6, 0.8],但如果最優值是 0.5 那麼我們的網格搜尋將永遠不可能找到最優解。
隨機搜尋的兩個好處:
- 引數的生成方式為隨機
- 可探索的空間更大
遺傳演算法
遺傳演算法是對自然界的模擬
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])
我們會看到這樣一張圖片:
只顯示一張圖片可能不是那麼直觀,定義一個顯示多影像顯示的函式:
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
是把 loss
、optimizer
都加入到模型圖中去。
檢視模型層數:
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
,不過常用的是 EarlyStopping
、ModelCheckpoint
、TensorBoard
。其中 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
中有三個重要的屬性 monitor
、min_delta
、patience
。monitor
設定關注指標,一般關注驗證集上,目標函式的值。min_delta
是一個閾值,這次的訓練與上次的訓練的差距,如果比這個閾值高的話,就不用 EarlyStopping
,如果比這個閾值低的話,就會用上 EarlyStopping
提前停止訓練。patience
表示 EarlyStopping
的耐心,設定允許低於閾值的次數,超出次數了,就會中止訓練。
執行後,我們在專案目錄下使用 tree
命令來列印目錄結構,會發現 callbacks
資料夾內多出了一些檔案:ModelCheckpoint
檔案:fashion_mnist_model.h5
還有兩個資料夾 train
、validation
儲存的是 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
顯示學習曲線圖:
或者直接用指令碼列印:
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)
可以看到這個學習曲線圖有點不一樣,後面接近平滑,那是因為我們的深層神經網路(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'
學習曲線圖:
可以看到 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」:
- 均值和方差不變
- 歸一化的性質也不變
如果資料過擬合比較輕,不適合作 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)
學習曲線圖:
測試集模型評估:
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 模型
RandomizedSearchCV
是 sklearn
裡面的一個函式,首先要把 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 協議》,轉載必須註明作者和本文連結