如何用 Python 和迴圈神經網路預測嚴重交通擁堵?

本文為你介紹,如何從 Waze 交通事件開放資料中,利用序列模型找到規律,進行分類預測。以便相關部門可以未雨綢繆,提前有效干預可能發生的嚴重擁堵。
尋找
之前在《 ofollow,noindex" target="_blank">文科生如何理解迴圈神經網路(RNN)? 》一文中,我為你講解過迴圈神經網路的含義。《 如何用 Python 和迴圈神經網路做中文文字分類? 》一文,我又為你介紹瞭如何用迴圈神經網路對文字做分類。
我不希望給你一種錯誤的簡單關聯,即“迴圈神經網路 只能 用來處理文字資料”。
事實上,只要是序列資料,你都可以考慮一下迴圈神經網路。
我一直打算找個其他序列資料的樣例,給你展示迴圈神經網路的更多應用場景。
但是這個資料不太好選擇。
目前一個熱門的應用場景,就是金融產品的價格預測。

每時每秒,金融產品的價格都在變動。把它彙集起來,是個典型的序列資料。
但是我一直不看好這種應用。因為金融產品的定價,應該是面向未來的。基於歷史價格資訊尋找波動規律,並對未來價格進行預測,實際上如同看著後視鏡開車一般危險。
但是,還有很多人依然樂此不疲地嘗試。很多時候,他們也能嚐到成功的甜頭。
這是為什麼?
原因在於,金融市場的參與者,並非理性的機器,而是由人組成的群體。從行為金融學的角度來看,進化給人類思考與行為帶來了一些“快捷方式”,你可以利用它們從中漁利。
陸蓉教授的《行為金融學》欄目,對此有詳細介紹。
例如,人們追漲殺跌,認為歷史會重演;
例如,吸引大眾關注到事件,總會帶來買入;
例如,人們會傾向於投資於自己熟悉的標的;
例如,人們會購買下跌的已持倉標的,來攤薄成本。
……
如果沒有大風浪,這種對市場參與者行為規律的洞察,確實可以幫你賺錢。你可以從價格的歷史波動中,挖掘出這些規律的影響。但是這對 沒有模型可用 的人來說,不公平。教你建模,就如同教你考試作弊。
如果遇到黑天鵝事件,其影響大概率會 超過 市場參與者行為偏誤帶來的歷史價格波動規律。那麼你,可能會因為應用模型,而遭遇 虧損 。你大約不會認為這是自己的錯誤,而直接把我當做騙子,朝我扔雞蛋。
理性權衡後,我決定 不用 金融產品價格趨勢分析,作為迴圈神經網路的應用樣例。
其他開放的序列資料,當然也有很多。例如共享單車租用資料、氣溫變化資料等。
不過這些應用,一來別人都寫過了,不新鮮。二來,氣溫變化,你看天氣預報就好了。共享單車租用數量……你真的關心這裡的規律嗎?
正在我猶豫的時候,一次偶然的機會,我接觸到了一個新的序列資料樣例——交通事件資料。我覺得,把它作為應用案例分享給你,可能更合適一些。
比賽
拿到這個資料,是因為我參與了一次程式設計馬拉松(hackathon)比賽。
比賽在 Frisco 的 UNT Inspire Park 舉辦。從早上8點開始,一直到晚上9點多才結束。中間可以自由吃免費提供的點心和水果,也可以到院子裡晒晒太陽放放風。大家還可以自由交流和組隊。

主辦方為參賽者提供了若干種開放資料,也提了一些問題供大家參考解答。當然,實際參賽的時候,你也可以自己擬定新的題目。
這其中,就包括了 Waze 資料。
我在中國開車,平時用的都是高德導航,對於 Waze 這款 App 不大熟悉。

簡而言之,這個 Waze 應用除了提供一般的導航功能之外,還有一個類似於眾包的功能——讓司機們自由提交路況資訊。

這樣一來,Waze 就利用群體智慧形成了一個眼觀六路耳聽八方的巨大網路,隨時依據使用者提供的情況,彙總成實時交通參考。並且彙報給使用者,以便於大家調整自己的行車路線。
我覺得最有用的特點是,在堵車的時候,你可以瞭解到前面究竟發生了什麼。其他導航也有實時交通狀況提示,但是你對前面的情況一無所知。道路半幅施工?交通事故?
資訊的對稱,可以在很大程度上,讓司機避免焦慮。
Waze 從幾年前開始,就和政府部門合作,進行資料開放共享。這樣一來,政府可以通過 Waze 的資料瞭解交通實時狀況,對於問題進行快速的響應處理;與此同時, Waze 使用者也因為可以獲取整合其他相關型別的政府開放資料(例如道路規劃等),更加有效合理安排出行。
這次比賽,主辦方提供的資料,是 DFW (達拉斯-沃斯堡都會區)區域,11月1日到29日的 Waze 交通事件(Incidents)開放資料,這是政府開放資料的一部分。這些資料基本都是來自於 Waze 使用者的提交。
原始的資料,接近 300 MB。每一條事件資訊,都包含了提交的經緯度,以及時間。因此在探索性資料分析階段,我做了幾個視覺化圖形。
這是我當天跟新認識的程式設計高手 Jesse 學的 QGIS 分析結果。

看看圖上的點,每一個都對應一次事件彙報。這叫一個密密麻麻啊。
因為 QGIS 初學,用得不熟,我還是用 Python 進行了分類繪圖。

這只是前 3000 條資料中部分型別的視覺化。其中紅色代表交通擁堵,黃色代表事故發生,藍色代表有車停在了路肩上。
可以看到,紅色的資料量最大。這說明交通擁堵是個大問題。
我把全部的資料都拿了出來,提煉出包含的事件型別,包括以下這些類:

我看到,其中單是交通阻塞,也是分為若干級別的。其中最嚴重的,分別是“大型交通擁堵”(large traffic jam)和“超大型交通擁堵”(huge traffic jam)。
於是,我把所有這兩種嚴重交通擁堵事件,合併成一個集合;其他剩餘事件,作為另一個集合。
對於每一個嚴重擁堵事件,我追溯30分鐘,把之前同一條道路上,發生的事件,按照順序存成一個列表。這樣的列表,有987個;但是,其中有一些,是驟然發生的,30分鐘的區間裡面,沒有任何其他事件作為先兆。這樣的空列表,我進行了清除。剩下了861個有效序列。
同樣,從剩餘事件集合中,我們隨機找到了861個非空有效序列。這些序列,後續緊隨事件,都 不是 嚴重擁堵。
我們對嚴重擁堵之前30分鐘的事件序列,標記為1;對於 非 嚴重擁堵之前30分鐘的事件序列,標記為0。
於是,我們就把問題轉換成了,能否利用事件序列,進行分類,預測後續是否會發生嚴重擁堵。
靠著這個模型,我們團隊(UNT IIA lab代表隊,暱稱 watch-dumpling )在這次比賽中,獲得第一名。

這是 HackNTX 官網的報道( http:// t.cn/EUbS9m5 ) 。

UNT 網站也正式釋出了這則新聞( http:// t.cn/EUbS127 ),於是我周圍盡人皆知。我才剛拿到手的獎金,立即就因為請客被掃蕩一空了。

奪冠純屬是個意外,幸運佔得比重很大。但是我覺得我們做的這個模型,還是有些應用價值的。
下面,我就以這組 Waze 交通事件資料,詳細給你講解一下,如何用 Python, Keras 和迴圈神經網路,來實現這個序列資料分類模型。
環境
要執行深度學習,你需要有 GPU 或者 TPU 的支援,否則會累壞你的膝上型電腦的。Google Colab 是個不錯的實驗平臺,可以讓你免費使用 TPU 來進行深度學習訓練。你可以閱讀《 如何免費雲端執行Python深度學習框架? 》一文,查詢更為詳細的介紹。
這裡,請你使用 Chrome 瀏覽器,點選這個連結,安裝一個外掛 Colaboratory 。

把它新增到 Google Chrome 之後,你會在瀏覽器的擴充套件工具欄裡面,看見下圖中間的圖示:

然後,請到本範例的 github repo 主頁面 。

開啟其中的 demo.ipynb
檔案。

點選 Colaboratory 擴充套件圖示。Google Chrome 會自動幫你開啟 Google Colab,並且裝載這個 ipynb 檔案。

點選上圖中紅色標出的“複製到雲端硬碟”按鈕。Google 會為你新建一個 屬於你自己的 副本。

點選選單欄裡面的“程式碼執行程式”,選擇“更改執行時型別”。

在出現的對話方塊中,確認選項如下圖所示。

點選“儲存”即可。
下面,你就可以依次執行每一個程式碼段落了。
注意第一次執行的時候,可能會有警告提示。

出現上面這個警告的時候,點選“仍然執行”就可以繼續了。
如果再次出現警告提示,反勾選“在執行前充值所有程式碼執行程式”選項,再次點選“仍然執行”即可。
環境準備好了,下面我們來一步步執行程式碼。
程式碼
首先,我們讀入 Pandas 軟體包,以便進行結構化資料的處理。
import pandas as pd
這次還要讀入的一個軟體包,是 Python 中間進行資料存取的利器,叫做 pickle 。
import pickle
它可以把 Python 資料,甚至是許多組資料,一起儲存到指定檔案。然後讀出的時候,可以完全恢復原先資料的格式。這一點上,它比用 csv 進行資料儲存和交換的效果更好,效率也更高。
下面我們從本文配套的 github 專案中,把資料傳遞過來。
!git clone https://github.com/wshuyi/demo_traffic_jam_prediction.git
資料的下載,很快就可以完成。
Cloning into 'demo_traffic_jam_prediction'... remote: Enumerating objects: 6, done.[K remote: Counting objects: 100% (6/6), done.[K remote: Compressing objects: 100% (4/4), done.[K remote: Total 6 (delta 0), reused 3 (delta 0), pack-reused 0[K Unpacking objects: 100% (6/6), done.
我們告訴 Jupyter Notebook ,資料資料夾的位置。
from pathlib import Path data_dir = Path('demo_traffic_jam_prediction')
開啟資料檔案,利用 pickle 把兩組資料分別取出。
with open(data_dir / 'data.pickle', 'rb') as f: [event_dict, df] = pickle.load(f)
先看其中的事件詞典 event_dict
:
event_dict
以下就是全部的事件型別。
{1: 'road closed due to construction', 2: 'traffic jam', 3: 'stopped car on the shoulder', 4: 'road closed', 5: 'other', 6: 'object on roadway', 7: 'major event', 8: 'pothole', 9: 'traffic heavier than normal', 10: 'road construction', 11: 'fog', 12: 'accident', 13: 'slowdown', 14: 'stopped car', 15: 'small traffic jam', 16: 'stopped traffic', 17: 'heavy traffic', 18: 'minor accident', 19: 'medium traffic jam', 20: 'malfunctioning traffic light', 21: 'missing sign on the shoulder', 22: 'animal on the shoulder', 23: 'animal struck', 24: 'large traffic jam', 25: 'hazard on the shoulder', 26: 'hazard on road', 27: 'ice on roadway', 28: 'weather hazard', 29: 'flooding', 30: 'road closed due to hazard', 31: 'hail', 32: 'huge traffic jam'}
同樣,我們來看看儲存事件序列的資料框。
先看前10個:
df.head(10)

注意,每一行,都包含了標記。
再看結尾部分:
df.tail(10)

讀取無誤。
下面我們來看看,最長的一個序列,編號是多少。
這裡,我們利用的是 Pandas 的一個函式,叫做 idxmax()
,它可以幫助我們,把最大值對應的索引編號,傳遞回來。
max_len_event_id = df.events.apply(len).idxmax() max_len_event_id
結果為:
105
我們來看看,這個編號對應的事件序列,是什麼樣子的:
max_len_event = df.iloc[max_len_event_id] max_len_event.events
下面是長長的反饋結果:
['stopped car on the shoulder', 'heavy traffic', 'heavy traffic', 'heavy traffic', 'slowdown', 'stopped traffic', 'heavy traffic', 'heavy traffic', 'heavy traffic', 'heavy traffic', 'traffic heavier than normal', 'stopped car on the shoulder', 'traffic jam', 'heavy traffic', 'stopped traffic', 'stopped traffic', 'stopped traffic', 'heavy traffic', 'traffic jam', 'stopped car on the shoulder', 'stopped traffic', 'stopped traffic', 'stopped traffic', 'heavy traffic', 'traffic heavier than normal', 'traffic heavier than normal', 'traffic heavier than normal', 'traffic heavier than normal', 'heavy traffic', 'stopped traffic', 'traffic heavier than normal', 'pothole', 'stopped car on the shoulder', 'traffic jam', 'slowdown', 'stopped traffic', 'heavy traffic', 'traffic heavier than normal', 'traffic jam', 'traffic jam', 'stopped car on the shoulder', 'major event', 'traffic jam', 'traffic jam', 'stopped traffic', 'heavy traffic', 'traffic heavier than normal', 'stopped car on the shoulder', 'slowdown', 'heavy traffic', 'heavy traffic', 'stopped car on the shoulder', 'traffic jam', 'slowdown', 'slowdown', 'heavy traffic', 'stopped car on the shoulder', 'heavy traffic', 'minor accident', 'stopped car on the shoulder', 'heavy traffic', 'stopped car on the shoulder', 'heavy traffic', 'stopped traffic', 'heavy traffic', 'traffic heavier than normal', 'heavy traffic', 'stopped car on the shoulder', 'traffic heavier than normal', 'stopped traffic', 'heavy traffic', 'heavy traffic', 'heavy traffic', 'stopped car on the shoulder', 'slowdown', 'stopped traffic', 'heavy traffic', 'stopped car on the shoulder', 'traffic heavier than normal', 'heavy traffic', 'minor accident', 'major event', 'stopped car on the shoulder', 'stopped car on the shoulder']
讀一遍,你就會發現,在超級擁堵發生之前,確實還是有一些先兆的。當然,這是由人來閱讀後,獲得的觀感。我們下面需要做的,是讓機器自動把握這些列表的特徵,並且做出區別分類。
我們看看,這個最長列表的長度。
maxlen = len(max_len_event.events) maxlen
結果為:
84
這裡的前導事件,還真是不少啊。
下面我們要做的,是把事件轉換成數字編號,這樣後面更容易處理。
我們使用以下的一個小技巧,把原先的事件詞典倒置,即變“序號:事件名稱”,為“事件名稱:序號”。這樣,以事件名稱查詢起來,效率會高很多。
reversed_dict = {} for k, v in event_dict.items(): reversed_dict[v] = k
我們看看倒置的結果詞典:
reversed_dict
這是反饋結果:
{'accident': 12, 'animal on the shoulder': 22, 'animal struck': 23, 'flooding': 29, 'fog': 11, 'hail': 31, 'hazard on road': 26, 'hazard on the shoulder': 25, 'heavy traffic': 17, 'huge traffic jam': 32, 'ice on roadway': 27, 'large traffic jam': 24, 'major event': 7, 'malfunctioning traffic light': 20, 'medium traffic jam': 19, 'minor accident': 18, 'missing sign on the shoulder': 21, 'object on roadway': 6, 'other': 5, 'pothole': 8, 'road closed': 4, 'road closed due to construction': 1, 'road closed due to hazard': 30, 'road construction': 10, 'slowdown': 13, 'small traffic jam': 15, 'stopped car': 14, 'stopped car on the shoulder': 3, 'stopped traffic': 16, 'traffic heavier than normal': 9, 'traffic jam': 2, 'weather hazard': 28}
成功了。
下面我們編寫一個函式,輸入一個事件列表,返回對應的事件編號列表。
def map_event_list_to_idxs(event_list): list_idxs = [] for event in (event_list): idx = reversed_dict[event] list_idxs.append(idx) return list_idxs
然後,我們在剛才是找到的最長列表上,實驗一下:
map_event_list_to_idxs(max_len_event.events)
結果是這樣的:
[3, 17, 17, 17, 13, 16, 17, 17, 17, 17, 9, 3, 2, 17, 16, 16, 16, 17, 2, 3, 16, 16, 16, 17, 9, 9, 9, 9, 17, 16, 9, 8, 3, 2, 13, 16, 17, 9, 2, 2, 3, 7, 2, 2, 16, 17, 9, 3, 13, 17, 17, 3, 2, 13, 13, 17, 3, 17, 18, 3, 17, 3, 17, 16, 17, 9, 17, 3, 9, 16, 17, 17, 17, 3, 13, 16, 17, 3, 9, 17, 18, 7, 3, 3]
看來功能實現上,沒問題。
讀入 numpy 和 Keras 的一些工具。
import numpy as np from keras.utils import to_categorical from keras.preprocessing.sequence import pad_sequences
系統自動提示我們,Keras 使用了 Tensorflow 作為後端框架。
Using TensorFlow backend.
我們需要弄清楚,一共有多少種事件型別。
len(event_dict)
結果是:
32
因此,我們需要對32種不同的事件型別,進行轉換和處理。
我們把整個資料集裡面的事件型別,都變成事件編號。
df.events.apply(map_event_list_to_idxs)
結果如下:
0[9, 17, 18, 14, 13, 17, 3, 13, 16, 3, 17, 17, ... 1[2, 10, 3] 2[2] 3[2] 4[2, 2, 2, 2, 2, 2, 2, 9] 5[3, 2, 17] 6[3, 2, 17] 7[2, 15, 2, 17, 2, 2, 13, 17, 2] 8[17, 2, 2, 16, 17, 2] 9[17, 2, 2, 16, 17, 2] 10[17, 16, 17, 2, 17, 3, 17, 17, 16, 17, 16, 18,... 11[17] 12[17] 13[24, 24] 14[24, 2, 24, 24, 2] 15[24, 2, 24, 24, 2] 16[2, 10, 2, 2, 2, 18, 16, 16, 7, 2, 16, 2, 2, 9... 17[2, 10, 2, 2, 2, 18, 16, 16, 7, 2, 16, 2, 2, 9... 18[24, 24, 24, 16, 2, 16] 19[24, 24, 24, 16, 2, 16] 20[2, 2] 21[2, 16, 2] 22[2, 16, 2] 23[2, 2] 24[2, 2] 25[24, 24] 26[2, 2] 27[2, 2, 2, 17] 28[2, 19, 2] 29[24] ... 831[9, 9, 9, 2, 9, 9, 17, 2, 9, 17] 832[3, 3, 3] 833[2, 9, 2, 17, 17, 2] 834[3, 3, 17, 3, 13, 3, 3, 23, 9, 3, 3, 25, 3, 3] 835[3, 17, 9, 14, 9, 17, 14, 9, 2, 9, 3, 2, 2, 17] 836[2] 837[17, 2, 16, 3, 9, 17, 17, 17, 13, 17, 9, 17] 838[13, 17, 17, 3, 3, 16, 17, 16, 17, 16, 3, 9, 1... 839[2] 840[3] 841[2] 842[17, 17, 17, 3, 17, 23, 16, 17, 17, 3, 2, 13, ... 843[3, 3] 844[2] 845[2, 17, 2, 2, 2, 2, 2, 17, 2, 2] 846[7, 17, 3, 18, 17] 847[3, 3, 3] 848[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, ... 849[2, 2] 850[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 13, 3, 2] 851[2, 2, 2] 852[16, 2, 16] 853[3, 16, 5, 3, 17, 3, 16, 9, 3, 2, 17] 854[16] 855[3, 3, 3, 3, 3, 3, 3, 3, 2, 13, 3, 6, 3, 6, 3,... 856[17, 17, 17, 2, 3, 2, 2, 2, 2, 2] 857[2, 2] 858[2, 2, 9, 17, 2, 2] 859[17, 3, 2, 2, 2, 2, 2, 2] 860[17, 3, 3, 17, 3, 17, 2, 3, 18, 14, 3, 3, 16, ... Name: events, Length: 1722, dtype: object
現在,作為人類,我們確實是看不清楚,列表裡面的事件都是什麼了。好在計算機對於數字,更加喜聞樂見。
我們把該列表,起名為 sequences ,並且顯示前5項內容。
sequences = df.events.apply(map_event_list_to_idxs).tolist() sequences[:5]
下面是結果:
[[9, 17, 18, 14, 13, 17, 3, 13, 16, 3, 17, 17, 16, 3, 16, 17, 9, 17, 2, 17, 2, 7, 16, 17, 17, 17, 17, 13, 5, 17, 9, 9, 16, 16, 3], [2, 10, 3], [2], [2], [2, 2, 2, 2, 2, 2, 2, 9]]
注意,第一行,明顯比後幾行都要長。
對於輸入序列,我們希望它的長度都是一樣的。因此,下面我們就用最長的序列長度作為標準,用 0 來填充其他短序列。
data = pad_sequences(sequences, maxlen=maxlen) data
這是結果:
array([[ 0,0,0, ..., 16, 16,3], [ 0,0,0, ...,2, 10,3], [ 0,0,0, ...,0,0,2], ..., [ 0,0,0, ..., 17,2,2], [ 0,0,0, ...,2,2,2], [ 0,0,0, ...,3,3,2]], dtype=int32)
注意,所有的0,都補充到了序列的最前端。序列都一樣長了。
下面,我們把全部的分類標記,儲存到 labels 變數裡面。
labels = np.array(df.label)
後面,我們有好幾個函式,需要用到隨機變數。
為了咱們執行結果的一致性。我這裡指定隨機種子數值。你第一次嘗試執行的時候,不要動它。但是後面自己動手操作的時候,可以任意修改它。
np.random.seed(12)
好了,下面我們“洗牌”。打亂資料的順序,但是注意序列和對應標記之間,要保持一致性。
indices = np.arange(data.shape[0]) np.random.shuffle(indices) data = data[indices] labels = labels[indices]
然後,我們取 80% 的資料,作為訓練;另外 20% 的資料,作為驗證。
training_samples = int(len(indices) * .8) validation_samples = len(indices) - training_samples
我們正式劃分訓練集和驗證集。
X_train = data[:training_samples] y_train = labels[:training_samples] X_valid = data[training_samples: training_samples + validation_samples] y_valid = labels[training_samples: training_samples + validation_samples]
看看訓練集的內容。
X_train
結果為:
array([[ 0,0,0, ..., 15, 15,3], [ 0,0,0, ...,0,2,2], [ 0,0,0, ...,0,0, 16], ..., [ 0,0,0, ...,2, 15, 16], [ 0,0,0, ...,2,2,2], [ 0,0,0, ...,0,0,2]], dtype=int32)
注意由於我們補充了“0”,作為填充,因此原先的32種事件型別的基礎上,又加了一種。
這就是我們新的事件型別數量:
num_events = len(event_dict) + 1
我們使用嵌入層,把事件標號,轉換成一系列數字組成的向量。這樣,可以避免模型把事件序號,當成數值型資料來處理。
這裡,我們指定每一個標號,轉換成 20 個數字組成的向量。
embedding_dim = 20
利用事件型別數量,和事件向量長度,我們隨機構造初始的嵌入矩陣。
embedding_matrix = np.random.rand(num_events, embedding_dim)
下面我們搭建一個迴圈神經網路模型。其中的 LSTM 層,包含了32位輸出數字。
from keras.models import Sequential from keras.layers import Embedding, Flatten, Dense, LSTM units = 32 model = Sequential() model.add(Embedding(num_events, embedding_dim)) model.add(LSTM(units)) model.add(Dense(1, activation='sigmoid'))
這裡,我假設你已經看過了《 如何用 Python 和迴圈神經網路做中文文字分類? 》一文,所以就不對細節進行講述了。如果你沒有看過,或者已經遺忘,可以點選這個連結複習一下。
如果你對 Keras 的使用方法還不熟悉,我再次向你推薦 François Chollet 的《Deep Learning with Python》。

下面,是處理其中的嵌入層引數。我們直接把剛才隨機生成的嵌入矩陣挪進來。而且,不讓模型在訓練中對嵌入層引數進行修改。
model.layers[0].set_weights([embedding_matrix]) model.layers[0].trainable = False
下面,我們開始訓練。並且把模型執行結果儲存起來。
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc']) history = model.fit(X_train, y_train, epochs=50, batch_size=32, validation_data=(X_valid, y_valid)) model.save("mymodel_embedding_untrainable.h5")
可以看到,因為有 TPU 的強力支援,程式在歡快地執行中。

訓練過程結束之後,我們利用 matplotlib 繪圖功能,看一下訓練中,準確率和損失值的變化。
import matplotlib.pyplot as plt acc = history.history['acc'] val_acc = history.history['val_acc'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(1, len(acc) + 1) plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.show()
這是準確率變化曲線。

可以看到,效果還是不錯的。因為我們資料中,不同標記各佔一半。因此如果構建一個 dummy model 作為標準線的話,對所有的輸入都猜測0或者1,準確率應該只有50%。
這裡的準確率,已經達到了65%-75%之間,證明我們的模型是有意義的。只不過,抖動比較厲害,穩定性差。
這是損失值變化曲線。

這個圖看起來,就不是很美妙了。因為雖然訓練集上面的損失值一路下降,但是驗證集上,這個效果並不是很明顯,一直劇烈波動。
看到結果,不是最重要的。關鍵是我們得分析出目前遇到問題,原因是什麼。
注意我們前面使用了嵌入矩陣。它隨機生成,卻又沒有真正進行訓練調整,這可能是個問題。
因此,我們這裡再次構建和跑一下模型。唯一改動的地方,在於讓嵌入矩陣的引數也可以隨著訓練進行自動調整。
from keras.models import Sequential from keras.layers import Embedding, Flatten, Dense, LSTM units = 32 model = Sequential() model.add(Embedding(num_events, embedding_dim)) model.add(LSTM(units)) model.add(Dense(1, activation='sigmoid'))
注意這裡的差別,就是 trainable
設定為真值。
model.layers[0].set_weights([embedding_matrix]) model.layers[0].trainable = True
構建模型,再次執行。
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc']) history = model.fit(X_train, y_train, epochs=50, batch_size=32, validation_data=(X_valid, y_valid)) model.save("mymodel_embedding_trainable.h5")

繪圖看看。
import matplotlib.pyplot as plt acc = history.history['acc'] val_acc = history.history['val_acc'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(1, len(acc) + 1) plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.show()
這次的準確率曲線,看起來好多了。驗證集波動沒有這麼劇烈,模型穩定性好了許多。而且,準確率的取值,也獲得了提升。後半程穩定在了75%以上。這樣的模型,就有應用價值了。

但是我們看看損失值曲線,可能就不這麼樂觀了。

注意從半程之後,訓練集和驗證集的損失值變化,就發生了分叉。
這是典型的過擬合(over-fitting)。
發生過擬合,主要原因就是相對於複雜的模型,訓練資料不夠用。
這時候,要麼增加訓練資料,要麼降低模型複雜度。
立即增加資料,不太現實。因為我們手中,目前只有那29天裡積攢的資料。
但是降低模型複雜度,是可以利用 Dropout 來嘗試完成的。
Dropout 的實現機理,是在 訓練 的時候,每次隨機把一定比例的模型中神經元對應權重引數,設定為0,讓它不起作用。這樣,模型的複雜度,就會降低。
下面,我們輕微修改一下,在 LSTM 層上,加入 dropout=0.2, recurrent_dropout=0.2
這兩個引數。
from keras.models import Sequential from keras.layers import Embedding, Flatten, Dense, LSTM units = 32 model = Sequential() model.add(Embedding(num_events, embedding_dim)) model.add(LSTM(units, dropout=0.2, recurrent_dropout=0.2)) model.add(Dense(1, activation='sigmoid'))
依然保持嵌入層可以被訓練。
model.layers[0].set_weights([embedding_matrix]) model.layers[0].trainable = True
再次執行。
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc']) history = model.fit(X_train, y_train, epochs=50, batch_size=32, validation_data=(X_valid, y_valid)) model.save("mymodel_embedding_trainable_with_dropout.h5")

繪製圖形的函式跟之前兩次完全一致。
import matplotlib.pyplot as plt acc = history.history['acc'] val_acc = history.history['val_acc'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(1, len(acc) + 1) plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.show()
這次的準確率曲線,看起來達到的數值,跟沒有加入 Dropout 的差不多。

然而,我們可以感受到訓練集和驗證集達到的準確率更加貼近。曲線更加平滑。
下面我們看看損失值曲線的變化。

這個曲線上,過擬合的去除效果就更為明顯了。可以看到訓練集和驗證集兩條曲線的波動基本保持了一致。這樣我們更可以確信,模型預測能力是穩定的,對外界新的輸入資訊,適應性更好。
如果把咱們的模型放在交通管理部門那裡,可以期望它根據 Waze 獲得的新序列資料,能以大約 75% 的準確率,預測嚴重交通擁堵的發生。這樣,交管部門就可以未雨綢繆,提前做出干預了。
用序列模型,欺負金融市場的散戶,屬於零和博弈。然而這種在交通管理上的應用,大概更能造福社會,體現科技的價值吧。
小結
通過本文的學習和實際上手操作,希望你已瞭解了以下知識點:
- 不只是文字,其他序列資料,也可以利用迴圈神經網路來進行分類預測。
- 對定類資料(categorical data)進行嵌入表示,如果用隨機數初始,那麼在建模過程中把嵌入層一起訓練,效果會更好。
- 資料量不夠的情況下,深度學習很可能會發生過擬合。使用 Dropout ,可以降低過擬合的影響,讓模型具有更好的穩定性和可擴充套件性。
希望這篇文章,可以幫助你瞭解迴圈神經網路的更多應用場景。在實際的工作和學習中,靈活運用它來處理序列資料的分類等任務。
祝(深度)學習愉快!
喜歡請點贊和打賞。還可以微信關注和置頂我的公眾號 “玉樹芝蘭”(nkwangshuyi) 。
如果你對 Python 與資料科學感興趣,不妨閱讀我的系列教程索引貼《 如何高效入門資料科學? 》,裡面還有更多的有趣問題及解法。