最簡單入門深度學習

該篇文檔基於kaggle course,通過簡單的理論介紹程序代碼運行圖以及動畫等來幫助大家入門深度學習,既然是入門,所以沒有太多模型推導以及高級技巧相關,都是深度學習中最基礎的內容,希望大家看過之後可以自己動手基於Tensorflow或者Keras搭建一個處理迴歸或者分類問題的簡單的神經網絡模型,並通過dropout等手段優化模型結果;

每部分都有對應的練習,練習都是很有針對性的,而且都很有趣,尤其是一些練習中都寫好了動畫的可視化展示,還是很有心的;

目錄:

  • 概述
  • 線性模型:單神經元
  • 非線性模型:深度神經網絡
  • 模型訓練:隨機梯度下降
  • 驗證模型:過擬合和欠擬合
  • 提升性能:Dropout和Batch Normalization
  • 分類問題

概述

經過本篇文章,你將搭建自己的深度神經網絡,使用Keras和Tensorflow,創建全連接神經網絡,在分類迴歸問題上應用神經網絡,通過隨機梯度下降訓練網絡、通過dropout等技術提升模型性能;

近些年在AI方面的主要發展都在深度學習,尤其是應用於自然語言處理圖像識別遊戲AI等領域,深度學習能得到更接近於人類的結果;

深度學習是一種允許大量深度計算爲特徵的機器學習方法,深度計算使得深度學習模型可以理解真實世界數據中的複雜高維的信息模式,比如這句話的含義是什麼、這張圖中的人在幹嘛等等;

通過這種優勢和靈活性,神經網絡成爲深度學習的定義模型,神經網絡由神經元組成,每個神經元單獨看只是一個簡單的計算單元,神經網絡的能力來自於許多神經元之間的複雜的組合模式;

單個神經元

線性單元

只有一個輸入的線性單元對應公式如下:

\[y = w*x+b \]

x爲輸入,神經元連接的權重爲ww的更新就是神經網絡學習的過程,b爲偏差,它與輸入沒有關係,偏差允許神經元不依賴輸入來修改輸出,y是神經元的輸出,即公式y=w*x+b的結果;

線性單元作爲模型的例子

神經元通常作爲神經網絡的一部分,往往也會將一個單獨的神經元模型作爲基準模型,單神經元模型是線性模型;

假設我們使用糖分作爲輸入訓練模型,卡路里作爲輸出,假設偏差b爲90,權重w爲2.5,當糖分爲5時,卡路里2.5*5+90=102.5

多個輸入

當我們期望使用多個輸入而不是一個時,其實就是將多個輸入連接並神經元,計算每個連接權重,並全部加起來得到最終輸出,如下:

\[y = w_0*x_0 + w_1*x_1 + w_2*x_2 + b \]

上述公式使用了三個輸入,並分別對應各自的連接權重,從輸入維度上看,單個輸入擬合一條直線兩個輸入你和一個平面多個輸入擬合的則是超平面

Keras中使用線性單元

最簡單的創建線性單元模型是通過keras.Sequential,可以通過dense層來創建上述提到的線性單元模型,對於一個有三個輸入,一個輸出的線性模型,Keras創建方式如下:

from tensorflow import keras
from tensorflow.keras import layers

# Create a network with 1 linear unit
model = keras.Sequential([
    layers.Dense(units=1, input_shape=[3])
])

其中units爲1表示該層只有一個輸出,input_shape爲[3]則表示有3個輸入,之所以參數是個列表[],這是因爲在圖像領域可能需要三維輸入,比如[高度,寬度,通道];

線性單元練習

可以通過這個notebook來進行這部分的練習,裏面包含了如何通過keras搭建線性單元的神經元模型,並通過其weights屬性來查看模型的連接權重偏差,最後還有一個未訓練的模型在預測中的表現,可以看到其隨機權重在每次運行結果都不一樣;

深度神經網絡

典型的神經網絡通過來組織他們的神經元,當我們把線性單元整理到一起時,我們就得到了一個dense層,神經網絡通過疊加dense層來將輸入以越來越複雜的方式進行轉換,在一個訓練好的神經網絡模型,每一層都會將輸入轉換的更接近結果一點;

激活函數

激活函數作用於層的輸出,最常用的是整流函數max(0,x),糾正函數將部分處理爲0,當我們將整流函數應用於一個線性單元時,也就得到了ReLU,而之前的線性公式:

\[y=w*x+b \]

也變成了:

\[y = max(0, w*x+b) \]

可以看到,函數也從線性轉爲了非線性,整流函數圖像如下:

堆疊dense層

輸出層之前通常有一些隱含層,一般我們不能直接看到他們的輸出(因爲他們的輸出並不是最後輸出,而是作爲下一層的輸入,因此無法直接看到),注意當處理迴歸問題時,最後一層也就是輸出層是線性單元,也就是沒有應用激活函數,當我們要處理分類或者其他問題時,仍然需要對應的激活函數;

通過keras.Sequential創建多層神經網絡方式很簡單,只要從第一層到最後一層依次通過layer定義即可,第一層獲取輸入,最後一層產生輸出,代碼如下:

from tensorflow.keras import layers

model = keras.Sequential([
    # the hidden ReLU layers
    layers.Dense(units=4, activation='relu', input_shape=[2]),
    layers.Dense(units=3, activation='relu'),
    # the linear output layer 
    layers.Dense(units=1),
])

其中各個layer表示各個堆疊的網絡層,activation表示各個層的激活函數,可以看到最後一層是沒有的,這是因爲它處理的是迴歸問題,且最後一層輸出只有一個,而其他層則不一定;

深度神經網絡練習

你可以通過這個notebook來進行這部分練習,其中包含如何通過keras.Sequential搭建3個隱含層1個輸出層的非線性神經網絡模型,以及如何使用單獨的激活層來代替activation參數,以及ReLUeLUSeLUswish等各個激活函數的差異,實驗證明ReLU適用於大多數場景,因此最適合作爲初始激活函數選擇,下面給出各個接獲函數的圖像:

relu:

elu:

selu:

swish:

隨機梯度下降

在之前創建的神經網絡模型中,網絡中的權重都是隨機指定的,此時的模型還沒有學習到任何東西,這也是第一個練習中每次運行結果都不一樣的原因;

所謂訓練一個神經網絡,指的是通過某種方式不斷更新網絡中的權重,使得模型通過輸入可以得到期望的輸出,如果可以做到,那麼也說明了這些權重在某種程度上表達了輸入特徵輸出之間的關係;

訓練模型需要兩個必要元素:

  • 損失函數:衡量模型預測結果好壞;
  • 優化方法:指導模型如何去修改權重;

損失函數

損失函數用於衡量模型的預測值真實值之間的差異,不同的問題使用的損失函數一般也是不同的,例如對於迴歸問題,即我們要預測的是數值,一個常用的用於迴歸問題的損失函數爲MAE,即平均絕對誤差,對於每個預測值y_predMAE計算它與y_true的差值的絕對值,所有這些絕對值取平均就是MAE的結果,除了MAE,用於迴歸問題的還有很多損失函數,比如MSEMASEHuber loss等等,對於模型來說,在訓練過程中,損失函數起到嚮導的作用,最小化損失函數就是模型要解決的問題,以此來指導網絡中權重的更新方向

優化方法 - 隨機梯度下降

通過損失函數我們確定了模型要解決的問題,但是依然需要告知模型如何去解決這個問題,此時就需要一種優化方法,優化方法是一種最小化損失的算法;

實際上所有應用於深度學習的優化算法都屬於隨機梯度下降族,它們都是迭代算法,一步一步的訓練模型,每一步的訓練過程如下:

  1. 抽樣部分訓練數據,通過模型運行得到預測結果y_pred
  2. 測量這些y_predy_true之間的損失函數值;
  3. 通過損失更小的方向來修改權重;

上述過程一遍一遍的運行,直到損失爲0或者損失無法再下降爲止;

迭代中從訓練集中抽樣的部分稱之爲minibatch,或者一般直接叫做batch,每一輪完整的訓練稱之爲epoch,epoch的數量決定了模型使用各個數據點的次數;

理想的訓練過程中,權重不斷更新,損失不斷減少,預測值越來越接近於真實值

學習率和Batch Size

學習率決定了模型在每一個batch上學習到的內容的大小,學習率越小意味着模型需要更多的batch來幫助其學習,學習率batch size是兩個訓練過程中影響很大的參數,通常也是主要要調的超參數;

可惜的是,對於很多情況下都沒有必要通過非常耗時的超參數調整來獲取最優的結果,Adam是一種不需要設置學習率的隨機梯度下降算法,它不需要調試任何參數,或者說它是自調整的,因此它成爲一種很好的通用優化方法;

添加損失函數和優化方法

在定義模型後,可以通過模型的compile方法添加損失函數和優化方法:

model.compile(
    optimizer="adam",
    loss="mae",
)

例子 - 紅酒品質

數據格式如下,最後一列爲預測目標列:

fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
10.8 0.470 0.43 2.10 0.171 27.0 66.0 0.99820 3.17 0.76 10.8 6
8.1 0.820 0.00 4.10 0.095 5.0 14.0 0.99854 3.36 0.53 9.6 5
9.1 0.290 0.33 2.05 0.063 13.0 27.0 0.99516 3.26 0.84 11.7 7
10.2 0.645 0.36 1.80 0.053 5.0 14.0 0.99820 3.17 0.42 10.0 6

可以看到,除了最後一列總有11列作爲輸入,神經網絡搭建代碼如下:

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Dense(512, activation='relu', input_shape=[11]),
    layers.Dense(512, activation='relu'),
    layers.Dense(512, activation='relu'),
    layers.Dense(1),
])

看到網絡由3個隱含層和1個輸出層組成,其中隱含層的units均爲512,表示每個隱含層輸出都有512個,第一層負責接受輸入,最後一層輸出結果;

定義完了網絡結構,下面需要設置訓練需要使用的損失函數和優化方法:

model.compile(
    optimizer='adam',
    loss='mae',
)

任務爲迴歸預測,損失函數選擇平均絕對誤差,優化器使用adam

訓練前的準備已經就緒,下面需要告訴模型訓練使用的batch數量、迭代次數等信息:

history = model.fit(
    X_train, y_train,
    validation_data=(X_valid, y_valid),
    batch_size=256,
    epochs=10,
)

對於訓練過程中的loss進行可視化後可以更好的觀察模型的整個迭代過程:

import pandas as pd

# convert the training history to a dataframe
history_df = pd.DataFrame(history.history)
# use Pandas native plot method
history_df['loss'].plot();

可以看到,在迭代次數達到6次時,後續的迭代中loss的下降不明顯,甚至還有變大的情況出行,一般來說這說明迭代次數足夠了;

模型訓練練習

這部分練習可以通過這個notebook,其中包含了完整的神經網絡模型,從定義到設置其損失和優化方法,再到最後的訓練過程,並通過很有趣的動畫方式展示了在不同的學習率batch size樣本數量等情況下的模型迭代過程,對於理解各個參數的作用非常有幫助哦,這裏展示其中一組參數下的訓練過程:

過擬合和欠擬合

過擬合和欠擬合是機器學習中繞不開的兩個問題,通常我們可以使用學習曲線來觀察模型迭代表現並判斷其當前屬於過擬合還是欠擬合,通常來說過擬合指的是模型過於複雜,將數據中的噪聲部分也擬合了,因此使得模型在真實數據上的表現明顯於在訓練集的表現,而欠擬合則指的是模型在訓練集上都沒有達到足夠好的效果,可能是因爲模型太簡單,也可能是因爲數據量太大;

容量

容量指的是模型可以學習到的數據模式的複雜度大小,或者說容量越大的模型,越能深入的理解數據,對於神經網絡來說,可以通過增加其寬度和高度來擴大其模型容量;

所謂增大網絡寬度指的是增加已有中的神經元個數,而增大高度指的是增加新的,一般來說使用同樣的神經元個數,增加高度帶來的容量增益要大於增加寬度,簡單理解如下:

假設當前網絡有兩層,每一層都有3個神經元,則其組合爲3*3=9,此時我們要增加2個神經元:

如果是用於增加寬度,每層增加一個神經元變爲4個,則有4*4=16;

如果是用於增加高度,增加一個單獨的層,有2個神經元,則有3*3*2=18;

因此都是使用了兩個神經元,從結果上看是高度的收益更大,當然這個只是一種直觀理解,實際的解釋要比這個複雜的多;

提前停止訓練

對於模型訓練過程,尤其是基於真實數據的訓練過程,很多時候是無法完全收斂的,而我們需要保證訓練一定可以結束而不是無限運行下去的,因此可以通過Early Stopping來控制其迭代在滿足某些條件下提前結束;

增加Early Stopping

keras通過callback的方式添加Early Stopping,所謂callback指的是在每次epoch後運行的內容,用於判斷是否應該終止訓練過程:

from tensorflow.keras.callbacks import EarlyStopping

early_stopping = EarlyStopping(
    min_delta=0.001, # minimium amount of change to count as an improvement
    patience=20, # how many epochs to wait before stopping
    restore_best_weights=True,
)

上述代碼的含義是,如果連續20次迭代,每次的loss下降都不足0.001,那麼訓練終止,反正目前爲止表現最好的權重數據;

例子 - 使用Early Stopping訓練模型

還是之前的紅酒例子,數據格式如下:

fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
10.8 0.470 0.43 2.10 0.171 27.0 66.0 0.99820 3.17 0.76 10.8 6
8.1 0.820 0.00 4.10 0.095 5.0 14.0 0.99854 3.36 0.53 9.6 5
9.1 0.290 0.33 2.05 0.063 13.0 27.0 0.99516 3.26 0.84 11.7 7
10.2 0.645 0.36 1.80 0.053 5.0 14.0 0.99820 3.17 0.42 10.0 6

模型定義、指定loss和優化器、指定Early Stopping代碼如下:

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.callbacks import EarlyStopping

early_stopping = EarlyStopping(
    min_delta=0.001, # minimium amount of change to count as an improvement
    patience=20, # how many epochs to wait before stopping
    restore_best_weights=True,
)

model = keras.Sequential([
    layers.Dense(512, activation='relu', input_shape=[11]),
    layers.Dense(512, activation='relu'),
    layers.Dense(512, activation='relu'),
    layers.Dense(1),
])
model.compile(
    optimizer='adam',
    loss='mae',
)

history = model.fit(
    X_train, y_train,
    validation_data=(X_valid, y_valid),
    batch_size=256,
    epochs=500,
    callbacks=[early_stopping],
    verbose=0,  # turn off training log
)

history_df = pd.DataFrame(history.history)
history_df.loc[:, ['loss', 'val_loss']].plot();
print("Minimum validation loss: {}".format(history_df['val_loss'].min()))

以上,通過fit方法的callbacks參數將Early Stopping作爲一個callback添加到了迭代過程中,用於控制訓練的提前結束,運行圖如下:

結合代碼和上圖可以看到,雖然我們設置了epoch爲500,但是在迭代不到70次時就終止了,這就是Early Stopping在起作用,一定程度上可以避免不必要的訓練過程,減少訓練時間;

過擬合和欠擬合的練習

這部分練習可以通過這個notebook完成,這裏有通過訓練簡單線性模型和複雜神經網絡模型等,並通過學習曲線來觀察模型的擬合情況,並通過添加Early Stopping來控制過擬合情況;

Dropout和Batch Normalization

實際的神經網絡結構中往往包含更多的層,不僅僅是dense層,比如激活層、Dropout層等等,有些類似dense層,定義神經元的連接,而有些則是用於預處理和轉換等;

Dropout

Dropout層有助於糾正過擬合問題,在每次訓練迭代中,隨機的去掉網絡層中的一部分輸入單元,使得模型難以從訓練數據學習到錯誤的模式,取而代之的是模型會搜索更普遍適用的模式,也就是具有更好魯棒性的模式,藉此解決過擬合問題;

可以把Dropout看作是一種集成方法,與隨機森林類似,Dropout的隨機抽取類似隨機森林的行抽取和列抽取,二者的目的都是解決原始模型的過擬合問題,思路是一樣的;

增加Dropout

keras中,Drouput作爲層使用,作用於其下的一層,通過參數rate指定隨機取出的比例:

keras.Sequential([
    # ...
    layer.Dropout(rate=0.3), # apply 30% dropout to the next layer
    layer.Dense(16),
    # ...
])

Batch Normalization

模型在迭代過程中,權重的更新主要由lossoptimater決定,假設我們的輸入特徵的量綱不一致,比如有的特徵範圍從0到1,有的特徵是從-100到+100,那麼在優化器計算過程中就會產生差異很大的結果,並使得訓練過程很不穩定,體現就是學習曲線的波動嚴重;

一個小栗子:比如我們要預測房價,目前有兩個屬性,一個是面積,範圍是10到200,另一個是距離火車站距離,範圍是100到100000,如果不進行量綱統一,可以遇見的是在計算過程中由於火車站距離值更大,因此會影響對結果的預測,或者說這個範圍一定程度上參與了原來權重該起到的作用;

Batch Normalization類似SKLearn裏的StandardScaler和MinMaxScaler的作用,用於將輸入特徵的量綱統一,避免因爲量綱不同導致對於預測結果影響的權重差異;

增加Batch Normalization

可以用在某一層之後:

layers.Dense(16, activation='relu'),
layers.BatchNormalization(),

也可以用在某一層和它的激活層之間:

layers.Dense(16),
layers.BatchNormalization(),
layers.Activation('relu'),

例子 - 使用Dropout和Batch Normalization

繼續紅酒例子,在每一個隱含層後都先加一個Dropout過濾一部分輸入解決過擬合,再應用Batch Normalization優化不穩定情況:

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Dense(1024, activation='relu', input_shape=[11]),
    layers.Dropout(0.3),
    layers.BatchNormalization(),
    layers.Dense(1024, activation='relu'),
    layers.Dropout(0.3),
    layers.BatchNormalization(),
    layers.Dense(1024, activation='relu'),
    layers.Dropout(0.3),
    layers.BatchNormalization(),
    layers.Dense(1),
])

訓練過程不使用Early Stopping:

model.compile(
    optimizer='adam',
    loss='mae',
)

history = model.fit(
    X_train, y_train,
    validation_data=(X_valid, y_valid),
    batch_size=256,
    epochs=100,
    verbose=0,
)


# Show the learning curves
history_df = pd.DataFrame(history.history)
history_df.loc[:, ['loss', 'val_loss']].plot();

學習曲線如下:

可以看到,首先雖然沒有Early Stopping,但是過擬合問題不明顯,其次在迭代20次之後不穩定的情況基本消失了,說明Dropout和Batch Normalization都起到了各自的作用;

Dropout和Batch Normalization練習

這部分練習在這個notebook裏,其中分別使用兩個數據集,對比其上應用Dropout與不應用,應用Batch Normalization與不應用在學習曲線上的差異,可以很直觀的看到二者起到的作用;

下面是應用Batch Normalization後的學習曲線,要知道在不應用的情況下曲線都無法繪製出來:

分類問題

之前處理的都是迴歸問題,處理分類問題的區別只有以下兩點:

  • 損失函數:分類與迴歸在損失函數應用上不同,比如MAE和準確率;
  • 輸出層輸出類型:也就是網絡結構最後一層輸出的內容,之前都是數值,如果是二分類問題,則應該是0/1;

Sigmoid函數

Sigmoid函數同樣作爲激活函數,它可以將實數輸出映射到0到1之間,也就是通常的概率範圍,而不管是準確率還是交叉熵等都可以利用概率來計算得到;

Sigmoid函數圖像如下,上一個使用它的地方是邏輯迴歸,同樣是將線性迴歸的結果映射到0和1之間:

例子 - 二分類

數據格式如下:

V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 ... V26 V27 V28 V29 V30 V31 V32 V33 V34 Class
1 0 0.99539 -0.05889 0.85243 0.02306 0.83398 -0.37708 1.00000 0.03760 ... -0.51171 0.41078 -0.46168 0.21266 -0.34090 0.42267 -0.54487 0.18641 -0.45300 good
1 0 1.00000 -0.18829 0.93035 -0.36156 -0.10868 -0.93597 1.00000 -0.04549 ... -0.26569 -0.20468 -0.18401 -0.19040 -0.11593 -0.16626 -0.06288 -0.13738 -0.02447 bad
1 0 1.00000 -0.03365 1.00000 0.00485 1.00000 -0.12062 0.88965 0.01198 ... -0.40220 0.58984 -0.22145 0.43100 -0.17365 0.60436 -0.24180 0.56045 -0.38238 good
1 0 1.00000 -0.45161 1.00000 1.00000 0.71216 -1.00000 0.00000 0.00000 ... 0.90695 0.51613 1.00000 1.00000 -0.20099 0.25682 1.00000 -0.32382 1.00000 bad
1 0 1.00000 -0.02401 0.94140 0.06531 0.92106 -0.23255 0.77152 -0.16399 ... -0.65158 0.13290 -0.53206 0.02431 -0.62197 -0.05707 -0.59573 -0.04608 -0.65697 good

像之前處理迴歸問題一樣定義模型,區別在於最後一層的激活函數選擇sigmoid用於輸出概率

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Dense(4, activation='relu', input_shape=[33]),
    layers.Dense(4, activation='relu'),    
    layers.Dense(1, activation='sigmoid'),
])

添加交叉熵和準確率到模型中,繼續使用adam,他在分類問題上表現依然很好:

model.compile(
    optimizer='adam',
    loss='binary_crossentropy',
    metrics=['binary_accuracy'],
)

使用Early Stopping控制訓練過程:

early_stopping = keras.callbacks.EarlyStopping(
    patience=10,
    min_delta=0.001,
    restore_best_weights=True,
)

history = model.fit(
    X_train, y_train,
    validation_data=(X_valid, y_valid),
    batch_size=512,
    epochs=1000,
    callbacks=[early_stopping],
    verbose=0, # hide the output because we have so many epochs
)

分別觀察其交叉熵和準確率的變化情況:

history_df = pd.DataFrame(history.history)
# Start the plot at epoch 5
history_df.loc[5:, ['loss', 'val_loss']].plot()
history_df.loc[5:, ['binary_accuracy', 'val_binary_accuracy']].plot()

print(("Best Validation Loss: {:0.4f}" +\
      "\nBest Validation Accuracy: {:0.4f}")\
      .format(history_df['val_loss'].min(), 
              history_df['val_binary_accuracy'].max()))

交叉熵:

準確率:

分類練習

這部分練習在這個notebook,很完整的一個分類模型搭建過程,從基於結構圖創建神經網絡結構到添加loss和優化器,使用Early Stopping等都有,包括對於結果是否過擬合和欠擬合的討論等,可以通過這個notebook再次練習下整個深度學習流程,麻雀雖小,五臟俱全;

交叉熵:

準確率:

最後

對於深度學習還有很多很多可以學習的內容,本篇文章以最簡單的方式對其中各個基礎模塊進行介紹,並結合代碼和運行結果圖等進行說明,希望看完能夠在腦海中形成對於深度學習的一個感性認識;

最後的最後

歡迎大佬們關注我的公衆號:尼莫的AI小站,新開的公衆號,後續會不定期更新有關機器學習深度學習數據處理分析遊戲的內容;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章