運用PyTorch動手搭建一個共享單車預測器

本文摘自 《深度學習原理與PyTorch實戰》算法

咱們將從預測某地的共享單車數量這個實際問題出發,帶領讀者走進神經網絡的殿堂,運用PyTorch動手搭建一個共享單車預測器,在實戰過程當中掌握神經元、神經網絡、激活函數、機器學習等基本概念,以及數據預處理的方法。此外,還會揭祕神經網絡這個「黑箱」,看看它如何工做,哪一個神經元起到了關鍵做用,從而讓讀者對神經網絡的運做原理有更深刻的瞭解。數據庫

3.1 共享單車的煩惱

大約從2016年起,咱們的身邊出現了不少共享單車。五光十色、各式各樣的共享單車就像炸開花了同樣,遍及城市的大街小巷。api

共享單車在給人們帶來便利的同時,也存在一個麻煩的問題:單車的分佈很不均勻。好比在早高峯的時候,一些地鐵口每每彙集着大量的單車,而到了晚高峯卻很難找到一輛單車了,這就給須要使用共享單車的人形成了不便。數組

那麼如何解決共享單車分佈不均勻的問題呢?目前的方式是,共享單車公司會僱用一些工人來搬運單車,把它們運送到須要單車的區域。但問題是應該運多少單車?何時運?運到什麼地方呢?這就須要準確地知道共享單車在整個城市不一樣地點的數量分佈狀況,並且須要提早作出安排,由於工人運送單車還有必定的延遲性。這對於共享單車公司來講是一個很是嚴峻的挑戰。bash

爲了更加科學有效地解決這個問題,咱們須要構造一個單車數量的預測器,用來預測某一時間、某一停放區域的單車數量,供共享單車公司參考,以實現對單車的合理投放。網絡

巧婦難爲無米之炊。要構建這樣的單車預測器,就須要必定的共享單車數據。爲了不商業糾紛,也爲了讓本書的開發和講解更方便,本例將會使用一個國外的共享單車公開數據集(Capital Bikeshare)來完成咱們的任務,數據集下載連接:www.capitalbikeshare.com/ system-data。架構

下載數據集以後,咱們能夠用通常的表處理軟件或者文本編輯器直接打開,如圖3.1所示。app

圖像說明文字

該數據是從2011年1月1日到2012年12月31日之間某地的單車使用狀況,每一行都表明一條數據記錄,共17 379條。一條數據記錄了一個小時內某一個地點的星期幾、是不是假期、天氣和風速等狀況,以及該地區的單車使用量(用cnt變量記載),它是咱們最關心的量。機器學習

咱們能夠截取一段時間的數據,將cnt隨時間的變化關係繪製成圖。圖3.2是2011年1月1日到1月10日的數據。橫座標是時間,縱座標是單車的數量。單車數量隨時間波動,而且呈現必定的規律性。不難看出,工做日的單車數量高峯遠高於週末的。編輯器

圖像說明文字

咱們要解決的問題就是,可否根據歷史數據預測接下來一段時間該地區單車數量的走勢狀況呢?在本章中,咱們將學習如何設計神經網絡模型來預測單車數量。對於這一問題,咱們並非一會兒提供一套完美的解決方案,而是經過按部就班的方式,嘗試不一樣的解決方案。結合這一問題,咱們將主要講解什麼是人工神經元、什麼是神經網絡、如何根據須要搭建一個神經網絡,以及什麼是過擬合,如何解決過擬合問題,等等。除此以外,咱們還將學到如何對一個神經網絡進行解剖,從而理解其工做原理以及與數據的對應。

3.2 單車預測器1.0

本節將作出一個單車預測器,它是一個單一隱含單元的神經網絡。咱們將訓練它學會擬合共享單車的波動曲線。

不過,在設計單車預測器以前,咱們有必要了解一下人工神經網絡的概念和工做原理。

3.2.1 人工神經網絡簡介

人工神經網絡(簡稱神經網絡)是一種受人腦的生物神經網絡啓發而設計的計算模型。人工神經網絡很是擅長從輸入的數據和標籤中學習到映射關係,從而完成預測或者解決分類問題。人工神經網絡也被稱爲通用擬合器,這是由於它能夠擬合任意的函數或映射。

前饋神經網絡是咱們最經常使用的一種網絡,它通常包括3層人工神經單元,即輸入層、隱含層和輸出層,如圖3.3所示。其中,隱含層能夠包含多層,這就構成了所謂的深度神經網絡。

圖像說明文字

圖中的每個圓圈表明一我的工神經元,連線表明人工突觸,它將兩個神經元聯繫了起來。每條連邊上都包含一個數值,叫做權重,咱們一般用w來表示。

神經網絡的運行一般包含前饋的預測過程(或稱爲決策過程)和反饋的學習過程。

在前饋的預測過程當中,信號從輸入單元輸入,並沿着網絡連邊傳輸,每一個信號會與連邊上的權重進行乘積,從而獲得隱含層單元的輸入;接下來,隱含層單元對全部連邊輸入的信號進行彙總(求和),而後通過必定的處理(具體處理過程將在下節講述)進行輸出;這些輸出的信號再乘以從隱含層到輸出的那組連線上的權重,從而獲得輸入給輸出單元的信號;最後,輸出單元再對每一條輸入連邊的信號進行彙總,並進行加工處理再輸出。最後的輸出就是整個神經網絡的輸出。神經網絡在訓練階段將會調節每條連邊上的權重w數值。

在反饋的學習過程當中,每一個輸出神經元會首先計算出它的預測偏差,而後將這個偏差沿着網絡的全部連邊進行反向傳播,獲得每一個隱含層節點的偏差。最後,根據每條連邊所連通的兩個節點的偏差計算連邊上的權重更新量,從而完成網絡的學習與調整。

下面,咱們就從人工神經元開始詳細講述神經網絡的工做過程。

3.2.2 人工神經元

人工神經網絡相似於生物神經網絡,由人工神經元(簡稱神經元)構成。神經元用簡單的數學模型來模擬生物神經細胞的信號傳遞與激活。爲了理解人工神經網絡的運做原理,咱們先來看一個最簡單的情形:單神經元模型。如圖3.4所示,它只有一個輸入層單元、一個隱含層單元和一個輸出層單元。

圖像說明文字

x表示輸入的數據,y表示輸出的數據,它們都是實數。從輸入單元到隱含層的權重w、隱含層單元偏置b、隱含層到輸出層的權重w'都是能夠任意取值的實數。

咱們能夠將這個最簡單的神經網絡當作一個從x映射到y的函數,而w、b和w'是該函數的參數。該函數的方程如圖3.5中的方程式所示,其中σ表示sigmoid函數。當w=1,w'=1,b=0的時候,這個函數的圖形如圖3.5所示。

圖像說明文字

這就是sigmoid函數的形狀及σ(x)的數學表達式。經過觀察該曲線,咱們不難發現,當x小於0的時候,σ(x)都是小於1/2的,並且x越小,σ(x)越接近於0;當x大於0的時候,σ(x)都是大於1/2的,並且x越大,σ(x)越接近於1。在x=0的點附近存在着一個從0到1的突變。

當咱們變換w、b和w'這些參數的時候,函數的圖形也會發生相應的改變。例如,咱們不妨保持 w'=1, b=0不變,而變換w的大小,其函數圖形的變化如圖3.6所示。

圖像說明文字

因而可知,當w>0的時候,它的大小控制着函數的彎曲程度,w越大,它在0點附近的彎曲程度就會越大,所以從x=0的突變也就越劇烈;當w<0的時候,曲線發生了左右翻轉,它會從1突變到0。

再來看看參數b對曲線的影響,保持w=w'=1不變,如圖3.7所示。

圖像說明文字

能夠清晰地看到,b控制着sigmoid函數曲線的水平位置。b>0,函數圖形往左平移;反之往右平移。最後,讓咱們看看w'如何影響該曲線,如圖3.8所示。

圖像說明文字

不難看出,當w' > 0的時候,w'控制着曲線的高矮;當w' < 0的時候,曲線的方向發生上下顛倒。

可見,經過控制w、w'和b這3個參數,咱們能夠任意調節從輸入x到輸出y的函數形狀。可是,不管如何調節,這條曲線永遠都是S形(包括倒S形)的。要想獲得更加複雜的函數圖像,咱們須要引入更多的神經元。

3.2.3 兩個隱含層神經元

下面咱們把模型作得更復雜一些,看看兩個隱含層神經元會對曲線有什麼影響,如圖3.9所示。

圖像說明文字

輸入信號進入網絡以後就會兵分兩路,一路從左側進入第一個神經元,另外一路從右側進入第二個神經元。這兩個神經元分別完成計算,並經過w'1和w'2進行加權求和獲得y。因此,輸出y實際上就是兩個神經元的疊加。這個網絡仍然是一個將x映射到y的函數,函數方程爲:

圖像說明文字

在這個公式中,有w1, w2, w'1, w'2, b1, b2這樣6個不一樣的參數。它們的組合也會對曲線的形狀有影響。

例如,咱們能夠取w1=w2=w'1=w'2=1,b1=-1,b2=0,則該函數的曲線形狀如圖3.10所示。

圖像說明文字

因而可知,合成的函數圖形變爲了一個具備兩個階梯的曲線。

讓咱們再來看一個參數組合,w1=w2=1,b1=0,b2=-1,w'1=1,w'2=-1,則函數圖形如圖3.11所示。

圖像說明文字

因而可知,咱們合成了一個具備單一波峯的曲線,有點相似於正態分佈的鐘形曲線。通常地,只要變換參數組合,咱們就能夠用兩個隱含層神經元擬合出任意具備單峯的曲線。

那麼,若是有4個或者6個甚至更多的隱含層神經元,不難想象,就能夠獲得具備雙峯、三峯和任意多個峯的曲線,咱們能夠粗略地認爲兩個神經元能夠用來逼近一個波峯(波谷)。事實上,對於更通常的情形,科學家早已從理論上證實,用有限多的隱含層神經元能夠逼近任意的有限區間內的曲線,這叫做通用逼近定理(universal approximation theorem)。

3.2.4 訓練與運行

在前面的討論中,咱們看到,只要可以調節神經網絡中各個參數的組合,就能獲得任意想要的曲線。可問題是,咱們應該如何選取這些參數呢?答案就在於訓練。

要想完成神經網絡的訓練,首先要給這個神經網絡定義一個損失函數,用來衡量網絡在現有的參數組合下輸出表現的好壞。這就相似於第2章利用線性迴歸預測房價中的總偏差函數(即擬合直線與全部點距離的平方和)L。一樣地,在單車預測的例子中,咱們也能夠將損失函數定義爲對於全部的數據樣本,神經網絡預測的單車數量與實際數據中單車數量之差的平方和的均值,即:

圖像說明文字

這裏,N爲樣本總量,

圖像說明文字
爲神經網絡計算得來的預測單車數,
圖像說明文字
爲實際數據中該時刻該地區的單車數。

有了這個損失函數L,咱們就有了調整神經網絡參數的方向——儘量地讓L最小化。所以,神經網絡要學習的就是神經元之間連邊上的權重及偏置,學習的目的是獲得一組可以使總偏差最小的參數值組合。

這是一個求極值的優化問題,高等數學告訴咱們,只須要令導數爲零就能夠求得。然而,因爲神經網絡通常很是複雜,包含大量非線性運算,直接用數學求導數的方法行不通,因此,咱們通常使用數值的方式來進行求解,也就是梯度降低算法。每次迭代都向梯度的負方向前進,使得偏差值逐步減少。參數的更新要用到反向傳播算法,將損失函數L沿着網絡一層一層地反向傳播,來修正每一層的參數。咱們在這裏不會詳細介紹反向傳播算法,由於PyTorch已經自動將這個複雜的算法變成了一個簡單的命令:backward。只要調用該命令,PyTorch就會自動執行反向傳播算法,計算出每個參數的梯度,咱們只須要根據這些梯度更新參數,就能夠完成一步學習。

神經網絡的學習和運行一般是交替進行的。也就是說,在每個週期,神經網絡都會進行前饋運算,從輸入端運算到輸出端;而後,根據輸出端的損失值來進行反向傳播算法,從而調整神經網絡上的各個參數。不停地重複這兩個步驟,就能夠令神經網絡學習得愈來愈好。

3.2.5 失敗的神經預測器

在弄清楚了神經網絡的工做原理以後,下面咱們來看看如何用神經網絡預測共享單車的曲線。咱們但願仿照預測房價的作法,利用人工神經網絡來擬合一個時間段內的單車曲線,並給出在將來時間點單車使用量的曲線。

爲了讓演示更加簡單清晰,咱們僅選擇了數據中的前50條記錄,繪製成如圖3.12所示的曲線。在這條曲線中,橫座標是數據記錄的編號,縱座標則是對應的單車數量。

圖像說明文字

接下來,咱們就要設計一個神經網絡,它的輸入x就是數據編號,輸出則是對應的單車數量。經過觀察這條曲線,咱們發現它至少有3個峯,採用10個隱含層單元就足以保證擬合這條曲線了。所以,咱們的人工神經網絡架構如圖3.13所示。

圖像說明文字

接下來,咱們就要動手寫程序實現這個網絡。首先導入本程序所使用的全部依賴庫。這裏咱們會用到pandas庫來讀取和操做數據。讀者須要先安裝這個程序包,在Anaconda環境下運行conda install pandas便可。

import numpy as np
import pandas as pd  #讀取csv文件的庫
import torch
from torch.autograd import Variable
import torch.optim as optim
import matplotlib.pyplot as plt
#讓輸出圖形直接在Notebook中顯示
%matplotlib inline
複製代碼

接着,要從硬盤文件中導入想要的數據。

data_path = 'hour.csv'  #讀取數據到內存,rides爲一個dataframe對象
rides = pd.read_csv(data_path)
rides.head()  #輸出部分數據
counts = rides['cnt'][:50]  #截取數據
x = np.arange(len(counts))  #獲取變量x
y = np.array(counts) #單車數量爲y
plt.figure(figsize = (10, 7)) #設定繪圖窗口大小
plt.plot(x, y, 'o-')  #繪製原始數據
plt.xlabel('X')  #更改座標軸標註
plt.ylabel('Y')  #更改座標軸標註
複製代碼

在這裏,咱們使用了pandas庫,從csv文件中快速導入數據存儲到rides裏面。rides能夠按照二維表的形式存儲數據,並能夠像訪問數組同樣對其進行訪問和操做。rides.head()的做用是打印輸出部分數據記錄。

以後,咱們從rides的全部記錄中選出前50條,並只篩選出了cnt字段放入counts數組中。這個數組就存儲了前50條自行車使用數量記錄。接着,咱們將前50條記錄的圖畫出來,即圖3.13所示的效果。

準備好了數據,咱們就能夠用PyTorch來搭建人工神經網絡了。與第2章的線性迴歸例子相似,咱們首先須要定義一系列的變量,包括全部連邊的權重和偏置,並經過這些變量的運算讓PyTorch自動生成計算圖。

#輸入變量,1,2,3,...這樣的一維數組
x = Variable(torch.FloatTensor(np.arange(len(counts), dtype = float))) 
#輸出變量,它是從數據counts中讀取的每一時刻的單車數,共50個數據點的一維數組,做爲標準答案
y = Variable(torch.FloatTensor(np.array(counts, dtype = float))) 

sz = 10  #設置隱含層神經元的數量
#初始化輸入層到隱含層的權重矩陣,它的尺寸是(1,10)
weights = Variable(torch.randn(1, sz), requires_grad = True)  
#初始化隱含層節點的偏置向量,它是尺寸爲10的一維向量
biases = Variable(torch.randn(sz), requires_grad = True)  
#初始化從隱含層到輸出層的權重矩陣,它的尺寸是(10,1)
weights2 = Variable(torch.randn(sz, 1), requires_grad = True)  
複製代碼

設置好變量和神經網絡的初始參數,接下來就要迭代地訓練這個神經網絡了。

learning_rate = 0.0001 #設置學習率
losses = [] #該數組記錄每一次迭代的損失函數值,以方便後續繪圖
for i in range(1000000):
    #從輸入層到隱含層的計算
    hidden = x.expand(sz, len(x)).t() * weights.expand(len(x), sz) + biases.expand(len(x), sz)
    #此時,hidden變量的尺寸是:(50,10),即50個數據點,10個隱含層神經元

    #將sigmoid函數做用在隱含層的每個神經元上
    hidden = torch.sigmoid(hidden)
    #隱含層輸出到輸出層,計算獲得最終預測
    predictions = hidden.mm(weights2)
    #此時,predictions的尺寸爲:(50,1),即50個數據點的預測數值
    #經過與數據中的標準答案y作比較,計算均方偏差
    loss = torch.mean((predictions - y) ** 2) 
    #此時,loss爲一個標量,即一個數
    losses.append(loss.data.numpy())

    if i % 10000 == 0: #每隔10000個週期打印一下損失函數數值
        print('loss:', loss)

    #*****************************************
    #接下來開始梯度降低算法,將偏差反向傳播
    loss.backward()  #對損失函數進行梯度反傳

    #利用上一步計算中獲得的weights,biases等梯度信息更新weights或biases的數值
    weights.data.add_(- learning_rate * weights.grad.data)  
    biases.data.add_(- learning_rate * biases.grad.data)
    weights2.data.add_(- learning_rate * weights2.grad.data)

    #清空全部變量的梯度值
    weights.grad.data.zero_()
    biases.grad.data.zero_()
    weights2.grad.data.zero_()
複製代碼

在上面這段代碼中,咱們進行了100 000步訓練迭代。在每一次迭代中,咱們都將50個數據點的x做爲數組所有輸入神經網絡,並讓神經網絡按照從輸入層到隱含層、再從隱含層到輸出層的步驟,一步步完成計算,最終輸出對50個數據點的預測數組prediction。

以後,計算prediction和標準答案y之間的偏差,並計算出全部50個數據點的平均偏差值loss,這就是咱們前面提到的損失函數L。接着,調用loss.backward()完成偏差順着神經網絡的反向傳播過程,從而計算出計算圖上每個葉節點的梯度更新數值,並記錄在每一個變量的.grad屬性中。最後,咱們用這個梯度數值來更新每一個參數的數值,從而完成了一步迭代。

仔細對比這段代碼和第2章中的線性迴歸代碼就會發現,除了中間的運算過程和損失函數有所不一樣外,其餘的操做所有相同。事實上,在本書中,幾乎全部的機器學習案例都採用了這樣的步驟,即前饋運算、反向傳播計算梯度、根據梯度更新參數數值。

咱們能夠打印出Loss隨着一步步的迭代降低的曲線,這能夠幫助咱們直觀地看到神經網絡訓練的過程,如圖3.14所示。

plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
複製代碼

圖像說明文字

由該曲線能夠看出,隨着時間的推移,神經網絡預測的偏差的確在一步步減少。並且,大約到20 000步後,偏差基本就不會呈現明顯的降低了。

接下來,咱們能夠把訓練好的網絡在這50個數據點上的預測曲線繪製出來,並與標準答案y進行對比,代碼以下:

x_data = x.data.numpy()  #得到x包裹的數據
plt.figure(figsize = (10, 7))  #設定繪圖窗口大小
xplot, = plt.plot(x_data, y.data.numpy(), 'o')  #繪製原始數據
yplot, = plt.plot(x_data, predictions.data.numpy())  #繪製擬合數據
plt.xlabel('X')  #更改座標軸標註
plt.ylabel('Y')  #更改座標軸標註
plt.legend([xplot, yplot],['Data', 'Prediction under 1000000 epochs'])  #繪製圖例
plt.show()
複製代碼

最後的可視化圖形如圖3.15所示。

圖像說明文字

能夠看到,咱們的預測曲線在第一個波峯比較好地擬合了數據,可是在此後,它卻與真實數據相差甚遠。這是爲何呢?

咱們知道,x的取值範圍是1~50,而全部權重和偏置的初始值都是被設定在(-1, 1)的正態分佈隨機數,那麼輸入層到隱含層節點的數值範圍就成了50~50,要想將sigmoid函數的多個峯值調節到咱們指望的位置須要耗費不少計算時間。事實上,若是讓訓練時間更長些,咱們能夠將曲線後面的部分擬合得很好。

這個問題的解決方法是將輸入數據的範圍作歸一化處理,也就是讓x的輸入數值範圍爲0~1。由於數據中x的範圍是1~50,因此,咱們只須要將每個數值都除以50就能夠了:

x = Variable(torch.FloatTensor(np.arange(len(counts), dtype = float) / len(counts)))
複製代碼

該操做會使x的取值範圍變爲0.02, 0.04, …, 1。作了這些改進後再來運行程序,能夠看到此次訓練速度明顯加快,可視化後的擬合效果也更好了,如圖3.16所示。

圖像說明文字

咱們看到,改進後的模型出現了兩個波峯,也很是好地擬合了這些數據點,造成一條優美的曲線。

接下來,咱們就須要用訓練好的模型來作預測了。咱們的預測任務是後面50條數據的單車數量。此時的x取值是51, 52, …, 100,一樣也要除以50。

counts_predict = rides['cnt'][50:100]  #讀取待預測的後面50個數據點
x = Variable(torch.FloatTensor((np.arange(len(counts_predict), dtype = float) + len(counts)) / len(counts)))
#讀取後面50個點的y數值,不須要作歸一化
y = Variable(torch.FloatTensor(np.array(counts_predict, dtype = float)))  

#用x預測y
hidden = x.expand(sz, len(x)).t() * weights.expand(len(x), sz)  #從輸入層到隱含層的計算
hidden = torch.sigmoid(hidden)  #將sigmoid函數做用在隱含層的每個神經元上
predictions = hidden.mm(weights2)  #從隱含層輸出到輸出層,計算獲得最終預測
loss = torch.mean((predictions - y) ** 2)  #計算預測數據上的損失函數
print(loss)

#將預測曲線繪製出來
x_data = x.data.numpy()  #得到x包裹的數據
plt.figure(figsize = (10, 7)) #設定繪圖窗口大小
xplot, = plt.plot(x_data, y.data.numpy(), 'o') #繪製原始數據
yplot, = plt.plot(x_data, predictions.data.numpy())  #繪製擬合數據
plt.xlabel('X')  #更改座標軸標註
plt.ylabel('Y')  #更改座標軸標註
plt.legend([xplot, yplot],['Data', 'Prediction'])  #繪製圖例
plt.show()
複製代碼

最終,咱們獲得瞭如圖3.17所示的曲線。直線是咱們的模型給出的預測曲線,圓點是實際數據所對應的曲線。模型預測與實際數據居然徹底對不上!

圖像說明文字

爲何咱們的神經網絡能夠很是好地擬合已知的50個數據點,卻徹底不能預測出更多的數據點呢?緣由就在於:過擬合。

3.2.6 過擬合

所謂過擬合(over fitting)現象就是指模型能夠在訓練數據上進行很是好的預測,但在全新的測試數據中卻得不到好的表現。在這個例子中,訓練數據就是前50個數據點,測試數據就是後面的50個數據點。咱們的模型能夠經過調節參數順利地擬合訓練數據的曲線,可是這種刻意適合徹底沒有推廣價值,致使這條擬合曲線與測試數據的標準答案相差甚遠。咱們的神經網絡模型並無學習到數據中的模式。

那咱們的神經網絡爲何不能學習到曲線中的模式呢?緣由就在於咱們選擇了錯誤的特徵變量:咱們嘗試用數據的下標(1, 2, 3, …)或者它的歸一化(0.1, 0.2, …)來對y進行預測。然而曲線的波動模式(也就是單車的使用數量)顯然並不依賴於下標,而是依賴於諸如天氣、風速、星期幾和是否節假日等因素。然而,咱們無論三七二十一,硬要用強大的人工神經網絡來擬合整條曲線,這天然就致使了過擬合的現象,並且是很是嚴重的過擬合。

由這個例子能夠看出,一味地追求人工智能技術,而不考慮實際問題的背景,很容易讓咱們走彎路。當咱們面對大數據時,數據背後的意義每每能夠指導咱們更加快速地找到分析大數據的捷徑。

在這一節中,咱們雖然費了半天勁也沒有真正地解決問題,可是仍然學到了很多知識,包括神經網絡的工做原理、如何根據問題的複雜度選擇隱含層的數量,以及如何調整數據讓訓練速度更快。更重要的是,咱們從血淋淋的教訓中領教了什麼叫做過擬合。

3.3 單車預測器2.0

接下來,就讓咱們踏上正確解決問題的康莊大道。既然咱們猜想到利用天氣、風速、星期幾、是不是節假日等信息能夠更好地預測單車使用數量,並且咱們的原始數據中就包含了這些信息,那麼咱們不妨從新設計一個神經網絡,把這些相關信息都輸入進去,從而預測單車的數量。

3.3.1 數據的預處理過程

然而,在咱們動手設計神經網絡以前,最好仍是再認真瞭解一下數據,由於加強對數據的瞭解會起到更重要的做用。

深刻觀察圖3.2中的數據,咱們發現,全部的變量能夠分紅兩種:一種是類型變量,另外一種是數值變量。

所謂的類型變量就是指這個變量能夠在幾種不一樣的類別中取值,例如星期(week)這個變量就有1, 2, 3, …, 0這幾種類型,分別表明星期1、星期2、星期三……星期日這幾天。而天氣狀況(weathersit)這個變量能夠從1~4中取值。其中,1表示晴天,2表示多雲,3表示小雨/雪,4表示大雨/雪。

另外一種類型就是數值類型,這種變量會從一個數值區間中連續取值。例如,溼度(humidity)就是一個從[0, 1]區間中連續取值的變量。溫度、風速也都是這種類型的變量。

咱們不能將不一樣類型的變量不加任何處理地輸入神經網絡,由於不一樣的數值表明徹底不一樣的含義。在類型變量中,數字的大小實際上沒有任何意義。好比數字5比數字1大,但這並不表明週五會比周一更特殊。除此以外,不一樣的數值類型變量的變化範圍也都不同。若是直接把它們混合在一塊兒,勢必會形成沒必要要的麻煩。綜合以上考慮,咱們須要對兩種變量分別進行預處理。

1. 類型變量的獨熱編碼

類型變量的大小沒有任何含義,只是爲了區分不一樣的類型而已。好比季節這個變量能夠等於一、二、三、4,即四季,數字僅僅是對它們的區分。咱們不能將season變量直接輸入神經網絡,由於season數值並不表示相應的信號強度。咱們的解決方案是將類型變量轉化爲「獨熱編碼」(one-hot),如表3.1所示。

圖像說明文字
採用這種編碼後,不一樣的數值就轉變爲了避免同的向量,這些向量的長度都是4,而只有一個位置爲1,其餘位置都是0。1表明激活,因而獨熱編碼的向量就對應了不一樣的激活模式。這樣的數據更容易被神經網絡處理。更通常地,若是一個類型變量有n個不一樣的取值,那麼咱們的獨熱編碼所對應的向量長度就爲n。

接下來,咱們只須要在數據中將某一列類型變量轉化爲多個列的獨熱編碼向量,就能夠完成這種變量的預處理過程了,如圖3.18所示。

圖像說明文字

所以,原來的weekday這個屬性就轉變爲7個不一樣的屬性,數據庫一下就增長了6列。

在程序上,pandas能夠很容易實現上面的操做,代碼以下:

dummy_fields = ['season', 'weathersit', 'mnth', 'hr', 'weekday'] #全部類型編碼變量的名稱
for each in dummy_fields:
    #取出全部類型變量,並將它們轉變爲獨熱編碼
    dummies = pd.get_dummies(rides[each], prefix=each, drop_first=False)
    #將新的獨熱編碼變量與原有的全部變量合併到一塊兒
    rides = pd.concat([rides, dummies], axis=1)

#將原來的類型變量從數據表中刪除
fields_to_drop = ['instant', 'dteday', 'season', 'weathersit', 'weekday', 'atemp', 'mnth', 'workingday', 
    'hr'] #要刪除的類型變量的名稱
data = rides.drop(fields_to_drop, axis=1) #將它們從數據庫的變量中刪除
複製代碼

通過這一番處理以後,本來只有17列的數據一會兒變爲了59列,部分數據片斷如圖3.19所示。

圖像說明文字

** 2. 數值類型變量的處理**

數值類型變量的問題在於每一個變量的變化範圍都不同,單位也不同,所以不一樣的變量就不能進行比較。咱們採起的解決方法就是對這種變量進行標準化處理,也就是用變量的均值和標準差來對該變量作標準化,從而都轉變爲[-1, 1]區間內波動的數值。好比,對於溫度temp這個變量來講,它在整個數據庫中取值的平均值爲mean(temp),方差爲std(temp),那麼,歸一化的溫度計算爲:

圖像說明文字

temp'是一個位於[-1, 1]區間的數。這樣作的好處就是能夠將不一樣取值範圍的變量設置爲處於平等的地位。

咱們能夠用如下代碼來實現這些變量的標準化處理:

quant_features = ['cnt', 'temp', 'hum', 'windspeed'] #數值類型變量的名稱
scaled_features = {}  #將每個變量的均值和方差都存儲到scaled_features變量中
for each in quant_features:
    #計算這些變量的均值和方差
    mean, std = data[each].mean(), data[each].std()
    scaled_features[each] = [mean, std]
    #對每個變量進行歸一化
    data.loc[:, each] = (data[each] - mean)/std
複製代碼

** 3. 數據集的劃分**

預處理作完之後,咱們的數據集包含了17 379條記錄、59個變量。接下來,咱們將對這個數據集進行劃分。

首先,在變量集合上,咱們分爲了特徵和目標兩個集合。其中,特徵變量集合包括:年份(yr)、是否節假日(holiday)、溫度(temp)、溼度(hum)、風速(windspeed)、季節1~4(season)、天氣1~4(weathersit,不一樣天氣種類)、月份1~12(mnth)、小時0~23(hr)和星期0~6(weekday),它們是輸入給神經網絡的變量。目標變量包括:用戶數(cnt)、臨時用戶數(casual),以及註冊用戶數(registered)。其中咱們僅僅將cnt做爲目標變量,另外兩個暫時不作任何處理。咱們將利用56個特徵變量做爲神經網絡的輸入,來預測1個變量做爲神經網絡的輸出。

接下來,咱們再將17 379條記錄劃分爲兩個集合:前16 875條記錄做爲訓練集,用來訓練咱們的神經網絡;後21天的數據(504條記錄)做爲測試集,用來檢驗模型的預測效果。這一部分數據是不參與神經網絡訓練的,如圖3.20所示。

圖像說明文字

數據處理代碼以下:

test_data = data[-21*24:] #選出訓練集
train_data = data[:-21*24] #選出測試集

#目標列包含的字段
target_fields = ['cnt','casual', 'registered'] 

#訓練集劃分紅特徵變量列和目標特徵列
features, targets = train_data.drop(target_fields, axis=1), train_data[target_fields]

#測試集劃分紅特徵變量列和目標特徵列
test_features, test_targets = test_data.drop(target_fields, axis=1), test_data[target_fields]

#將數據類型轉換爲NumPy數組
X = features.values  #將數據從pandas dataframe轉換爲NumPy
Y = targets['cnt'].values
Y = Y.astype(float)

Y = np.reshape(Y, [len(Y),1])
losses = []
複製代碼

3.3.2 構建神經網絡

在數據處理完畢後,咱們將構建新的人工神經網絡。這個網絡有3層:輸入層、隱含層和輸出層。每一個層的尺寸(神經元個數)分別是5六、10和1(如圖3.21所示)。其中,輸入層和輸出層的神經元個數分別由數據決定,隱含層神經元個數則根據咱們對數據複雜度的預估決定。一般,數據越複雜,數據量越大,就須要越多的神經元。可是神經元過多容易形成過擬合。

圖像說明文字

除了前面講的用手工實現神經網絡的張量計算完成神經網絡搭建之外,PyTorch還實現了自動調用現成的函數來完成一樣的操做,這樣的代碼更加簡潔,以下所示:

#定義神經網絡架構,features.shape[1]個輸入層單元,10個隱含層,1個輸出層
input_size = features.shape[1]
hidden_size = 10
output_size = 1
batch_size = 128
neu = torch.nn.Sequential(
    torch.nn.Linear(input_size, hidden_size),
    torch.nn.Sigmoid(),
    torch.nn.Linear(hidden_size, output_size),
)
複製代碼

在這段代碼裏,咱們能夠調用torch.nn.Sequential()來構造神經網絡,並存放到neu變量中。torch.nn.Sequential()這個函數的做用是將一系列的運算模塊按順序搭建成一個多層的神經網絡。在本例中,這些模塊包括從輸入層到隱含層的線性映射Linear(input_size, hidden_size)、隱含層的非線性sigmoid函數torch.nn.Sigmoid(),以及從隱含層到輸出層的線性映射torch.nn.Linear(hidden_size, output_size)。值得注意的是,Sequential裏面的層次並不與神經網絡的層次嚴格對應,而是指多步的運算,它與動態計算圖的層次相對應。

咱們也可使用PyTorch自帶的損失函數:

cost = torch.nn.MSELoss()
複製代碼

這是PyTorch自帶的一個封裝好的計算均方偏差的損失函數,它是一個函數指針,賦予了變量cost。在計算的時候,咱們只須要調用cost(x,y)就能夠計算預測向量x和目標向量y之間的均方偏差。

除此以外,PyTorch還自帶了優化器來自動實現優化算法:

optimizer = torch.optim.SGD(neu.parameters(), lr = 0.01)
複製代碼

torch.optim.SGD()調用了PyTorch自帶的隨機梯度降低算法(stochastic gradient descent,SGD)做爲優化器。在初始化optimizer的時候,咱們須要待優化的全部參數(在本例中,傳入的參數包括神經網絡neu包含的全部權重和偏置,即neu.parameters()),以及執行梯度降低算法的學習率lr=0.01。在一切材料都準備好以後,咱們即可以實施訓練了。

數據的分批處理

然而,在進行訓練循環的時候,咱們還會遇到一個問題。在前面的例子中,在每個訓練週期,咱們都將全部的數據一股腦地兒輸入神經網絡。這在數據量不大的狀況下沒有任何問題。可是,如今的數據量是16 875條,在這麼大數據量的狀況下,若是在每一個訓練週期都處理全部數據,則會出現運算速度過慢、迭代可能不收斂等問題。

解決方法一般是採起批處理(batch processing)的模式,也就是將全部的數據記錄劃分紅一個批次大小(batch size)的小數據集,而後在每一個訓練週期給神經網絡輸入一批數據,如圖3.22所示。批量的大小依問題的複雜度和數據量的大小而定,在本例中,咱們設定batch_size=128。

圖像說明文字

採用分批處理後的訓練代碼以下:

#神經網絡訓練循環
losses = []
for i in range(1000):
    #每128個樣本點被劃分爲一批,在循環的時候一批一批地讀取
    batch_loss = []
    #start和end分別是提取一批數據的起始和終止下標
    for start in range(0, len(X), batch_size):
        end = start + batch_size if start + batch_size < len(X) else len(X)
        xx = Variable(torch.FloatTensor(X[start:end]))
        yy = Variable(torch.FloatTensor(Y[start:end]))
        predict = neu(xx)
        loss = cost(predict, yy)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        batch_loss.append(loss.data.numpy())

    #每隔100步輸出損失值
    if i % 100==0:
        losses.append(np.mean(batch_loss))
        print(i, np.mean(batch_loss))

#打印輸出損失值
plt.plot(np.arange(len(losses))*100,losses)
plt.xlabel('epoch')
plt.ylabel('MSE')
複製代碼

運行這段程序,咱們即可以訓練這個神經網絡了。圖3.23展現的是隨着訓練週期的運行,損失函數的降低狀況。其中,橫座標表示訓練週期,縱座標表示平均偏差。能夠看到,平均偏差隨訓練週期快速降低。

圖像說明文字

3.3.3 測試神經網絡

接下來,咱們即可以用訓練好的神經網絡在測試集上進行預測,而且將後21天的預測數據與真實數據畫在一塊兒進行比較。

targets = test_targets['cnt']  #讀取測試集的cnt數值
targets = targets.values.reshape([len(targets),1])  #將數據轉換成合適的tensor形式
targets = targets.astype(float)  #保證數據爲實數

#將特徵變量和目標變量包裹在Variable型變量中
x = Variable(torch.FloatTensor(test_features.values))
y = Variable(torch.FloatTensor(targets))

#用神經網絡進行預測
predict = neu(x)
predict = predict.data.numpy()

fig, ax = plt.subplots(figsize = (10, 7))

mean, std = scaled_features['cnt']
ax.plot(predict * std + mean, label='Prediction')
ax.plot(targets * std + mean, label='Data')
ax.legend()
ax.set_xlabel('Date-time')
ax.set_ylabel('Counts')
dates = pd.to_datetime(rides.loc[test_data.index]['dteday'])
dates = dates.apply(lambda d: d.strftime('%b %d'))
ax.set_xticks(np.arange(len(dates))[12::24])
_ = ax.set_xticklabels(dates[12::24], rotation=45)
複製代碼

實際曲線與預測曲線的對好比圖3.24所示。其中,橫座標是不一樣的日期,縱座標是預測或真實數據的值。虛線爲預測曲線,實線爲實際數據。

圖像說明文字

能夠看到,兩個曲線基本是吻合的,可是在12月25日先後幾天的實際值和預測值誤差較大。爲何這段時間的表現這麼差呢?

仔細觀察數據,咱們發現12月25日正好是聖誕節。對於歐美國家來講,聖誕節就至關於咱們的春節,在聖誕節假期先後,人們的出行習慣會與往日有很大的不一樣。可是,在咱們的訓練樣本中,由於整個數據僅有兩年的長度,因此包含聖誕節先後的樣本僅有一次,這就致使咱們沒辦法對這一特殊假期的模式進行很好的預測。

3.4 剖析神經網絡Neu

按理說,目前咱們的工做已經所有完成了。可是,咱們還但願對人工神經網絡的工做原理有更加透徹的瞭解。所以,咱們將對這個訓練好的神經網絡Neu進行剖析,看看它究竟爲何可以在一些數據上表現優異,而在另外一些數據上表現欠佳。

對於咱們來講,神經網絡在訓練的時候發生了什麼徹底是黑箱,可是,神經網絡連邊的權重實際上就存在於計算機的存儲中,咱們是能夠把感興趣的數據提取出來分析的。

咱們定義了一個函數feature(),用於提取神經網絡中存儲在連邊和節點中的全部參數。代碼以下:

def feature(X, net):
    #定義一個函數,用於提取網絡的權重信息,全部的網絡參數信息所有存儲在neu的named_parameters集合中
    X = Variable(torch.from_numpy(X).type(torch.FloatTensor), requires_grad = False)
    dic = dict(net.named_parameters()) #提取這個集合
    weights = dic['0.weight'] #能夠按照「層數.名稱」來索引集合中的相應參數值
    biases = dic['0.bias'] 
    h = torch.sigmoid(X.mm(weights.t()) + biases.expand([len(X), len(biases)])) #隱含層的計算過程
    return h #輸出層的計算
複製代碼

在這段代碼中,咱們用net.named_parameters()命令提取出神經網絡的全部參數,其中包括了每一層的權重和偏置,而且把它們放到Python字典中。接下來就能夠經過如上代碼來提取,例如能夠經過dic['0.weight']和dic['0.bias']的方式獲得第一層的全部權重和偏置。此外,咱們還能夠經過遍歷參數字典dic獲取全部可提取的參數名稱。

因爲數據量較大,咱們選取了一部分數據輸入神經網絡,並提取出網絡的激活模式。咱們知道,預測不許的日期有12月22日、12月23日、12月24日這3天。因此,就將這3天的數據彙集到一塊兒,存入subset和subtargets變量中。

bool1 = rides['dteday'] == '2012-12-22'
bool2 = rides['dteday'] == '2012-12-23'
bool3 = rides['dteday'] == '2012-12-24'

#將3個布爾型數組求與
bools = [any(tup) for tup in zip(bool1,bool2,bool3) ]
#將相應的變量取出來
subset = test_features.loc[rides[bools].index]
subtargets = test_targets.loc[rides[bools].index]
subtargets = subtargets['cnt']
subtargets = subtargets.values.reshape([len(subtargets),1])
複製代碼

將這3天的數據輸入神經網絡中,用前面定義的feature()函數讀出隱含層神經元的激活數值,存入results中。爲了閱讀方便,能夠將歸一化輸出的預測值還原爲原始數據的數值範圍。

#將數據輸入到神經網絡中,讀取隱含層神經元的激活數值,存入results中
results = feature(subset.values, neu).data.numpy()
#這些數據對應的預測值(輸出層)
predict = neu(Variable(torch.FloatTensor(subset.values))).data.numpy()
#將預測值還原爲原始數據的數值範圍
mean, std = scaled_features['cnt']
predict = predict * std + mean
subtargets = subtargets * std + mean
複製代碼

接下來,咱們就將隱含層神經元的激活狀況所有畫出來。同時,爲了比較,咱們將這些曲線與模型預測的數值畫在一塊兒,可視化的結果如圖3.25所示。

#將全部的神經元激活水平畫在同一張圖上
fig, ax = plt.subplots(figsize = (8, 6))
ax.plot(results[:,:],'.:',alpha = 0.1)
ax.plot((predict - min(predict)) / (max(predict) - min(predict)),'bo-',label='Prediction')
ax.plot((subtargets - min(predict)) / (max(predict) - min(predict)),'ro-',label='Real')
ax.plot(results[:, 6],'.:',alpha=1,label='Neuro 7')

ax.set_xlim(right=len(predict))
ax.legend()
plt.ylabel('Normalized Values')

dates = pd.to_datetime(rides.loc[subset.index]['dteday'])
dates = dates.apply(lambda d: d.strftime('%b %d'))
ax.set_xticks(np.arange(len(dates))[12::24])
_ = ax.set_xticklabels(dates[12::24], rotation=45)
複製代碼

圖像說明文字

圖中方塊曲線是模型的預測數值,圓點曲線是真實的數值,不一樣顏色和線型的虛線是每一個神經元的輸出值。能夠發現,6號神經元(Neuro 6)的輸出曲線與真實輸出曲線比較接近。所以,咱們能夠認爲該神經元對提升預測準確性有更高的貢獻。

同時,咱們還想知道Neuro 6神經元表現較好的緣由以及它的激活是由誰決定的。進一步分析它的影響因素,能夠知道是從輸入層指向它的權重,如圖3.26所示。

圖像說明文字

咱們能夠經過下列代碼將這些權重進行可視化。

#找到與峯值對應的神經元,將其到輸入層的權重輸出
dic = dict(neu.named_parameters())
weights = dic['0.weight']
plt.plot(weights.data.numpy()[6, :],'o-')
plt.xlabel('Input Neurons')
plt.ylabel('Weight')
複製代碼

結果如圖3.27所示。橫軸表明了不一樣的權重,也就是輸入神經元的編號;縱軸表明神經網絡訓練後的連邊權重。例如,橫軸的第10個數,對應輸入層的第10個神經元,對應到輸入數據中,是檢測天氣類別的類型變量。第32個數,是小時數,也是類型變量,檢測的是早6點這種模式。咱們能夠理解爲,縱軸的值爲正就是促進,值爲負就是抑制。因此,圖中的波峯就是讓該神經元激活,波谷就是神經元未激活。

圖像說明文字

咱們看到,這條曲線在hr_12, weekday_0,6方面有較高的權重,這表示神經元Neuro 6正在檢測如今的時間點是否是中午12點,同時也在檢測今天是否是週日或者週六。若是知足這些條件,則神經元就會被激活。與此相對的是,神經元在weathersit_3和hr_6這兩個輸入上的權重值爲負值,而且恰好是低谷,這意味着該神經元會在下雨或下雪,以及早上6點的時候被抑制。經過翻看萬年曆咱們知道,2012年的12月22日和23日恰好是週六和週日,所以Neuro 6被激活了,它們對正確預測這兩天的正午高峯作了貢獻。可是,因爲聖誕節即將到來,人們可能早早回去爲聖誕作準備,所以這個週末比較特殊,並未出現往常週末的大量騎行需求,因而Neuro 6給出的激活值致使了太高的正午單車數量預測。

與此相似,咱們能夠找到致使12月24日遲早高峯太高預測的緣由。咱們發現4號神經元起到了主要做用,由於它的波動形狀恰好跟預測曲線在24日的遲早高峯負相關,如圖3.28所示。

圖像說明文字

同理,這個神經元對應的權重及其檢測的模式如圖3.29所示。

圖像說明文字

這個神經元檢測的模式和Neuro 6類似卻相反,它在遲早高峯的時候受到抑制,在節假日和週末激活。進一步考察從隱含層到輸出層的鏈接,咱們發現Neuro 4的權重爲負數,可是這個負值又沒有那麼大。因此,這就致使了在12月24日遲早高峯的時候被抑制,可是這個信號抑制的效果並不顯著,沒法致使預測尖峯的出現。

因此,咱們分析出神經預測器Neu在這3天預測不許的緣由是聖誕假期的反常模式。12月24日是聖誕夜,該網絡對節假日遲早高峯抑制單元的抑制不夠,因此致使了預測不許。若是有更多的訓練數據,咱們有可能將4號神經元的權重調節得更低,這樣就有可能提升預測的準確度。

3.5 小結

本章咱們以預測某地共享單車數量的問題做爲切入點,介紹了人工神經網絡的工做原理。經過調整神經網絡中的參數,咱們能夠獲得任意形狀的曲線。接着,咱們嘗試用具備單輸入、單輸出的神經網絡擬合了共享單車數據並嘗試預測。

可是,預測的效果卻很是差。通過分析,咱們發現,因爲採用的特徵變量爲數據的編號,而這與單車的數量沒有任何關係,完美擬合的假象只不過是一種過擬合的結果。因此,咱們嘗試了新的預測方式,利用每一條數據中的特徵變量,包括天氣、風速、星期幾、是不是假期、時間點等特徵來預測單車使用數量,並取得了成功。

在第二次嘗試中,咱們還學會了如何對數據進行劃分,以及如何用PyTorch自帶的封裝函數來實現咱們的人工神經網絡、損失函數以及優化器。同時,咱們引入了批處理的概念,即將數據切分紅批,在每一步訓練週期中,都用一小批數據來訓練神經網絡並讓它調整參數。這種批處理的方法既能夠加速程序的運行,又讓神經網絡可以穩步地調節參數。

最後,咱們對訓練好的神經網絡進行了剖析。瞭解了人工神經元是如何經過監測數據中的固有模式而在不一樣條件下激活的。咱們也清楚地看到,神經網絡之因此在一些數據上工做很差,是由於在數據中很難遇到假期這種特殊條件。

3.6 Q&A

本書內容源於張江老師在「集智AI學園」開設的網絡課程「火炬上的深度學習」,爲了幫助讀者快速疏通思路或解決常見的實踐問題,咱們挑選了課程學員提出的具備表明性的問題,並附上張江老師的解答,組成「Q&A」小節,附於相關章節的末尾。若是讀者在閱讀過程當中產生了類似的疑問,但願能夠從中獲得解答。

Q:神經元是否是越多越好?

A:固然不是越多越好。神經網絡模型的預測能力不僅和神經元的個數有關,還與神經網絡的結構和輸入數據有關。

Q:在預測共享單車使用量的實驗中,爲何要作梯度清空?

A:若是不清空梯度,backward()函數是會累加梯度的。咱們在進行一次訓練後,就當即進行梯度反傳,因此不須要系統累加梯度。若是不清空梯度,有可能致使模型沒法收斂。

Q:對於神經網絡來講,非收斂函數也能夠逼近嗎?

A:在必定的閉區間裏是能夠的。由於在閉區間裏,一個函數不可能無窮髮散,總會有一個界限,那麼就可使用神經網絡模型進行逼近。對於一個無窮的區間來講,神經網絡模型就不行了,由於神經網絡模型中用於擬合的神經元數量是有限的。

Q:在預測共享單車的例子中,模型對聖誕節期間的單車使用量預測得不夠準確。那麼是否是能夠經過增長訓練數據的方法提升神經網絡預測的準確性?

A:是可行的。若是使用更多的包含聖誕節期間單車使用狀況的訓練數據訓練模型,那麼模型對聖誕節期間的單車使用狀況的預測會更加準確。

Q:既然預測共享單車使用量的模型能夠被解析和剖析,那麼是否是每一個神經網絡均可以這樣剖析?

A:這個不必定。由於預測共享單車使用量的模型結構比較簡單,隱藏層神經元只有10個。當網絡模型中神經元的個數較多或者有多層神經元的時候,神經網絡模型的某個「決策」會難以歸因到單個神經元裏。這時就難以用「剖析」的方式來分析神經網絡模型了。

Q:在訓練神經網絡模型的時候,講到了「訓練集/測試集=k」,那麼比例k是多少才合理,k對預測的收斂速度和偏差有影響嗎?

A:在數據量比較少的狀況下,咱們通常按照10∶1的比例來選擇測試集;而在數據量比較大的狀況下,好比,數據有十萬條以上,就不必定必須按照比例來劃分訓練集和測試集了。

相關文章
相關標籤/搜索