尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接![❤️](https://s.w.org/images/core/emoji/15.0.3/svg/2764.svg)
作者 | 王樹義
來源 | 玉樹芝蘭(nkwangshuyi)
本文為你介紹,如何從 Waze 交通事件開放數據中,利用序列模型找到規律,進行分類預測。以便相關部門可以未雨綢繆,提前有效干預可能發生的嚴重擁堵。
尋找
之前在《文科生如何理解循環神經網路(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 軟件包,以便進行結構化數據的處理。
importpandas aspd
這次還要讀入的一個軟件包,是 Python 中間進行數據存取的利器,叫做 pickle 。
importpickle
它可以把 Python 數據,甚至是許多組數據,一起存儲到指定文件。然後讀出的時候,可以完全恢復原先數據的格式。這一點上,它比用 csv 進行數據存儲和交換的效果更好,效率也更高。
下面我們從本文配套的 github 項目中,把數據傳遞過來。
!git clonehttps://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 ,數據文件夾的位置。
frompathlib importPath
data_dir = Path(‘demo_traffic_jam_prediction’)
打開數據文件,利用 pickle 把兩組數據分別取出。
with open(data_dir / ‘data.pickle’, ‘rb’) asf:
[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 = {}
fork, v inevent_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 = []
foreventin(event_list):
idx = reversed_dict[event]
list_idxs.append(idx)
returnlist_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 的一些工具。
importnumpy asnp
fromkeras.utils importto_categorical
fromkeras.preprocessing.sequence importpad_sequences
系統自動提示我們,Keras 使用了 Tensorflow 作為後端框架。
UsingTensorFlow 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位輸出數字。
fromkeras.models import Sequential
fromkeras.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 asplt
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%之間,證明我們的模型是有意義的。只不過,抖動比較厲害,穩定性差。
這是損失值變化曲線。
這個圖看起來,就不是很美妙了。因為雖然訓練集上面的損失值一路下降,但是驗證集上,這個效果並不是很明顯,一直劇烈波動。
看到結果,不是最重要的。關鍵是我們得分析出目前遇到問題,原因是什麼。
注意我們前面使用了嵌入矩陣。它隨機生成,卻又沒有真正進行訓練調整,這可能是個問題。
因此,我們這里再次構建和跑一下模型。唯一改動的地方,在於讓嵌入矩陣的參數也可以隨著訓練進行自動調整。
fromkeras.models import Sequential
fromkeras.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 asplt
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 這兩個參數。
fromkeras.models import Sequential
fromkeras.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 asplt
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 ,可以降低過擬合的影響,讓模型具有更好的穩定性和可擴展性。
希望這篇文章,可以幫助你了解循環神經網路的更多應用場景。在實際的工作和學習中,靈活運用它來處理序列數據的分類等任務。
祝(深度)學習愉快!
轉載稿件:本文僅代表作者獨立觀點,轉載請聯繫原作者。
◆
文字識別
◆
主題:基於模板的文字識別結果結構化處理技術
時間:12月20日晚8點
入群福利:添加小助手微信cadnai2,回復:OCR,加入課程交流群。課程QA,PPT共享。