在 《【Get】用深度學(xué)習識別手寫數(shù)字》 中瑞凑,我們通過一個手寫數(shù)字識別的例子,體驗了如何使用 深度學(xué)習 + tensorflow 解決一個具體的問題砂碉。
實際上五辽,這是一個分類問題,即將輸入的圖片數(shù)據(jù)分成 0-9 共 10 個類別蹋岩,而且我們的數(shù)據(jù)都是直接使用 MNIST 上下載的處理好的數(shù)據(jù)赖草。
在現(xiàn)實生產(chǎn)中,我們的數(shù)據(jù)源通常來自于數(shù)據(jù)庫剪个,是沒有經(jīng)過預(yù)處理的秧骑,那么我們該做些什么來讓這些數(shù)據(jù)庫里的數(shù)據(jù)能夠用于進行機器學(xué)習呢?
機器學(xué)習的前置步驟扣囊,數(shù)據(jù)預(yù)處理就是解決這個問題的乎折。
本篇 CoorChice 將會通過一個回歸預(yù)測問題,來展示如何進行這個過程侵歇。
原始數(shù)據(jù)
我們用于機器學(xué)習的數(shù)據(jù)的量不能太少骂澄,否則訓(xùn)練效果會極差。但是我們個人如何才能獲取到大量的數(shù)據(jù)來進行訓(xùn)練呢惕虑?
CoorChice 推薦 kaggle:https://www.kaggle.com/datasets坟冲,這是全球最大的數(shù)據(jù)科學(xué)社區(qū)和數(shù)據(jù)競賽平臺。在這個美妙的地方,我們可以輕松的找到各種各樣的,海量的篡石,精彩絕倫的數(shù)據(jù)照瘾。
打開鏈接就可以看到海量的數(shù)據(jù),我們很隨意的選擇其中一個數(shù)據(jù)源來進行訓(xùn)練。
就選第一條吧!
第一條數(shù)據(jù)是關(guān)于一個面包店的交易的數(shù)據(jù)。點擊進入后侄榴,找到下載就可以下載這套數(shù)據(jù)了。
也許你可能不能立刻下載网沾,因為他會要求你先登錄癞蚕。那就成為 Kaggle 的會員吧。
我們使用表格打開剛剛下載的數(shù)據(jù)觀察觀察辉哥。
My God桦山! 這是一堆簡陋不堪的數(shù)據(jù)攒射,共有 21293 條數(shù)據(jù),每條只有 4 個特征恒水! 4 個特征会放! 4 個特征!
沒關(guān)系钉凌,我們只是學(xué)習如何處理數(shù)據(jù)的過程咧最,所以就湊合著用一下吧。
開始數(shù)據(jù)處理
實際上御雕,通常我們的數(shù)據(jù)呆在數(shù)據(jù)庫中矢沿,就是這種表格的形式。
在開始之前酸纲,需要先安裝一下 Pandas 這個 Python 庫捣鲸,它被用來讀取我們的表格數(shù)據(jù),和進行增闽坡、刪栽惶、改、查等操作无午。
下好了媒役,我們就開始吧...
觀察數(shù)據(jù)
進行數(shù)據(jù)處理最重要的一步祝谚,就是對數(shù)據(jù)進行觀察和分析宪迟。通過觀察和分析,我們要除去一些無關(guān)緊要的數(shù)據(jù)列交惯,比如在預(yù)測天氣預(yù)報的時候次泽,我們就應(yīng)該把 ‘李雷早上吃了什么’ 這個數(shù)據(jù)列去掉,因為對模型訓(xùn)練毫無幫助席爽。
同時意荤,我們要從數(shù)據(jù)中挖掘出一些隱藏的信息,以擴充特征只锻。日入在預(yù)測產(chǎn)品的交易信息時玖像,我們從日期這個數(shù)據(jù)列,就能知道當天是否是一個節(jié)日齐饮,是什么節(jié)日捐寥。
在我們下載的這堆面包店的交易數(shù)據(jù)中,僅有慘不忍睹的 4 個類目祖驱!
通常握恳,當我們使用機器學(xué)習來解決問題時,都會有一個目的捺僻。比如乡洼,在這堆數(shù)據(jù)中崇裁,CoorChice 希望通過機器學(xué)習訓(xùn)練一個模型,能夠用于預(yù)測未來一定條件下的某種類型的面包所能達到的交易量是多少束昵。
因此拔稳,我們將交易量 Transaction 這一列數(shù)據(jù)作為數(shù)據(jù)集的標簽數(shù)據(jù)。
接下來剩下的只有 日期锹雏、時間壳炎、面包類目
這三個特征了。
從數(shù)據(jù)中挖掘更多的信息
僅有這三個特征來進行訓(xùn)練逼侦,結(jié)果可想而知會有多差匿辩。比你想的差還要差,所以我們需要從現(xiàn)有的數(shù)據(jù)中挖掘出更多的信息來榛丢。比如铲球,在圖片數(shù)據(jù)中,通常會進行一些旋轉(zhuǎn)晰赞、平移之類的操作稼病,來補充數(shù)據(jù)量。
日期 Date
首先看看日期能挖掘到什么有用的信息掖鱼?
當然是星期啦然走。今天是星期一、星期二戏挡、...芍瑞、對交易肯定是有影響的。我們通過 Date 可以計算出當前日期是周幾褐墅。
還有什么可挖掘的嗎拆檬?嗯,左思右想...咦妥凳,節(jié)日肯定也是影響面包交易量的一個重要因素竟贯,有了 Date 我們也能知道當前日期是什么節(jié)日。
然后逝钥,通過觀察可以發(fā)現(xiàn)屑那,隨著日期的增加,交易量也是在累加的艘款,所以日期的大小也可直接作為一個特征持际。
由于 CoorChice 要嘗試預(yù)測的是未來某個時間、某種面包所能達到的交易量磷箕,所以日期的大小對結(jié)果必然是有影響的选酗。但我們不能直接將日期轉(zhuǎn)換為一個形如
20161030
這樣的數(shù)字,因為一年只有 12 個月岳枷,每一個月天數(shù)不一樣的芒填,所以這個數(shù)值跳躍度比較大呜叫,這樣就大大增加了訓(xùn)練的難度和準確率。我們需要的是一個統(tǒng)一標準的數(shù)值殿衰。如果確定某一天為基準朱庆,然后把日期轉(zhuǎn)換為和這一天的距離,那量化標準自然就是很明了的了闷祥。因為數(shù)據(jù)是從2016-10-30
開始娱颊,那么把這天作為基準日就再合適不過了。
時間 Time
接下來觀察 Time 有什么可用的沒凯砍。
Time 本身是一些比較密集的數(shù)據(jù)箱硕,間隔不大。我們可以把它劃歸到小時里悟衩,因此就會有 24 個小時剧罩。比如,“10:13:03” 就讓它屬于 10 點這個小時吧座泳。
通過時間惠昔,我們還可以知道現(xiàn)在是處于
早晨、上午挑势、中午镇防、下午、傍晚潮饱、晚上来氧、深夜
中的那個時間段。不同的時間段或多或少也會影響交易量饼齿。
確定好了思路饲漾,接下來就開始找著這個思路處理數(shù)據(jù)了蝙搔。
處理原始數(shù)據(jù)
處理數(shù)據(jù)的第一步當然是先讀取數(shù)據(jù)啦缕溉,我們前面下載的 Pandas 就使用來干這個的。
讀取數(shù)據(jù)
# 讀取數(shù)據(jù)
datas = pd.read_csv('data/BreadBasket_DMS.csv')
讀取數(shù)據(jù)后吃型,輸出一下看看長什么樣子证鸥。
print(datas.head())
->>
Date Time Transaction Item
0 2016-10-30 09:58:11 1 Bread
1 2016-10-30 10:05:34 2 Scandinavian
2 2016-10-30 10:05:34 2 Scandinavian
3 2016-10-30 10:07:57 3 Hot chocolate
4 2016-10-30 10:07:57 3 Jam
[5 rows x 4 columns]
增強數(shù)據(jù)
然后,我們對原始數(shù)據(jù)按照上面分析的思路進行處理勤晚。
# 挖掘枉层,周幾對交易量的影響
datas['Weekday'] = datas['Date']
datas['Weekday'] = datas['Weekday'].map(lambda x: get_weekday(x))
# 挖掘,各個節(jié)日對交易量的影響
datas['Festival'] = datas['Date']
datas['Festival'] = datas['Festival'].map(lambda x: get_festival(x))
# 挖掘赐写,當前所處的時間段對交易量的影響
datas['Time_Quantum'] = datas['Time']
datas['Time_Quantum'] = datas['Time_Quantum'].map(lambda x: get_time_quantum(x))
# 將 Date 一列變?yōu)榕c 2016-10-30 這天的距離
datas['Date'] = datas['Date'].map(lambda x: get_delta_days(x))
# 將時間轉(zhuǎn)變?yōu)闀r段
datas['Time'] = datas['Time'].map(lambda x: get_time_range(x))
# 縮小數(shù)據(jù)大小
datas['Transaction'] = datas['Transaction'].map(lambda x: (float(x) / 1000.))
現(xiàn)在鸟蜡,再輸出一下數(shù)據(jù)看看。
print(datas.head())
->>
Date Time Transaction ... Weekday Festival Time_Quantum
0 0.0 9 0.001 ... 6 null 上午
1 0.0 10 0.002 ... 6 null 上午
2 0.0 10 0.002 ... 6 null 上午
3 0.0 10 0.003 ... 6 null 上午
4 0.0 10 0.003 ... 6 null 上午
[5 rows x 7 columns]
從表格數(shù)據(jù)中可以看到挺邀,Date 和 Transaction 都變成浮點數(shù)了揉忘,因為把它們進行縮小跳座,有利于后面的梯度下降,否則 loss 會非常非常的大泣矛。
現(xiàn)在疲眷,我們已經(jīng)擴充了一些特征。
進行 One-Hot 編碼
接下來您朽,對于離散的數(shù)據(jù)狂丝,比如 Weekday、Time 這樣的哗总,它的大小并不會對交易產(chǎn)生影響几颜,但這種離散的特征的類別取值是會對結(jié)果產(chǎn)生影響的。比如讯屈,春節(jié)這天會對食物的交易量產(chǎn)生影響菠剩。
我們需要對這樣的特征數(shù)據(jù)進行 獨熱編碼 。關(guān)于 獨熱編碼 耻煤,你可以在 CoorChice 的 《機器學(xué)習具壮,看完就明白了》 這篇文章,了解相關(guān)信息哈蝇。
ont_hot_data = pd.get_dummies(datas, prefix=['Time', 'Item', 'Weekday', 'Festival', 'Time_Quantum'])
看看現(xiàn)在數(shù)據(jù)表變成了什么樣子了棺妓。
print(ont_hot_data.head())
->>
Date Transaction ... Time_Quantum_晚上 Time_Quantum_深夜
0 0.0 0.001 ... 0 0
1 0.0 0.002 ... 0 0
2 0.0 0.002 ... 0 0
3 0.0 0.003 ... 0 0
4 0.0 0.003 ... 0 0
[5 rows x 136 columns]
經(jīng)過獨熱編碼后,columns 已經(jīng)擴充到了驚人的 136 列炮赦!
之所以需對數(shù)據(jù)進行獨熱編碼怜跑,還有一個原因是在訓(xùn)練時,獨熱編碼在分類是 0 和 1 的關(guān)系吠勘,而如果用 1性芬,2,3剧防,.. 的分類標簽方式植锉,是不利于梯度下降的,計算出的梯度往往比較大峭拘。
對 Date俊庇、Transaction,可以看到表格中鸡挠,CoorChice 還對他們進行縮小辉饱,目的是為了把數(shù)值范圍變小,這樣有利于梯度下降的求解拣展。
我們看看現(xiàn)在數(shù)據(jù)的分布情況:
打亂數(shù)據(jù)
接下來彭沼,我們需要將元素的有序的數(shù)據(jù)進行打亂,這樣有助于提高訓(xùn)練出來的模型的泛化能力备埃。
ont_hot_data = ont_hot_data.sample(frac=1, replace=False)
ont_hot_data = ont_hot_data.sample(frac=1, replace=False)
ont_hot_data = ont_hot_data.sample(frac=1, replace=False)
一遍不夠姓惑,要打亂 3 遍译株!
看 Date 一列,說明數(shù)據(jù)已經(jīng)被打的足夠亂了挺益。
保存數(shù)據(jù)
最后歉糜,我們要把處理好的數(shù)據(jù)分為訓(xùn)練集和測試集,CoorChice 按大概 30% 取測試集數(shù)據(jù)望众,然后分別保存匪补。
# 測試數(shù)據(jù)集大小
test_count = 6000
train_count = ont_hot_data.shape[0] - test_count
# 切割出訓(xùn)練數(shù)據(jù)集
train_data = ont_hot_data[:train_count]
# 切割出測試數(shù)據(jù)集
test_data = ont_hot_data[train_count:]
# 分別保存兩個數(shù)據(jù)集
train_data.to_csv('data/train_data.csv', index=False, header=True)
test_data.to_csv('data/test_data.csv', index=False, header=True)
保存處理好的數(shù)據(jù)很簡單,使用 Pandas 提供的 to_csv()
就可以把數(shù)據(jù)存 csv 格式烂翰。
index 和 header 分別表示是否保存行號和列名稱夯缺。
現(xiàn)在,我們已經(jīng)把原始數(shù)據(jù)處理好甘耿,并且分割成了訓(xùn)練集和測試集踊兜。我們有 15,293 條訓(xùn)練數(shù)據(jù),和 6000 條測試數(shù)據(jù)佳恬。
開始訓(xùn)練吧捏境!
在開始之前,建議先看一看這一篇 《【Get】用深度學(xué)習識別手寫數(shù)字》 毁葱,因為套路基本上是一樣的垫言,只是有的地方需要根據(jù)具體情況調(diào)整。
在手寫數(shù)字識別中倾剿,我們構(gòu)建的是一個簡單 4 層網(wǎng)絡(luò)筷频,它由兩個卷積層,一個全鏈接層和一個輸出層組成前痘。
回顧一下這個網(wǎng)絡(luò)結(jié)構(gòu)凛捏。
發(fā)生了欠擬合!
一開始芹缔,CoorChice 直接使用了這個網(wǎng)絡(luò)進行訓(xùn)練坯癣,經(jīng)過 10w 次的漫長訓(xùn)練之后發(fā)現(xiàn),準確率最高不過 0.1%乖菱。
起初 CoorChice 減小 lr(學(xué)習率)坡锡,想著可能是學(xué)習率過大,導(dǎo)致震蕩了窒所。然而,毫無作用帆锋。
出現(xiàn)這種現(xiàn)象吵取,基本可以判斷大概率上是發(fā)生欠擬合了。
發(fā)生欠擬合锯厢,可以通過以下步驟一步步的排除問題和改進網(wǎng)絡(luò)皮官。
使用適合的權(quán)重初始化方案
本次訓(xùn)練中脯倒,權(quán)重的初始化沿用了上次的正太分布初始化方案,應(yīng)該是比較優(yōu)秀的方案了捺氢,所以就不做改動了藻丢。
選擇適當?shù)募せ詈瘮?shù)
在上一次的網(wǎng)絡(luò)構(gòu)建中,卷積層摄乒、全鏈接層都用的是 ReLu 激活函數(shù)悠反,輸出層使用的是 Softmax 激活函數(shù)。而這次 CoorChice 把激活函數(shù)全換成了優(yōu)秀的 ReLu 激活函數(shù)馍佑,這應(yīng)該也是沒毛病的斋否。
選擇適合的優(yōu)化器和學(xué)習率
這是一張各種優(yōu)化器的比較圖,從圖中可以看到拭荤,有一個名叫 Adadelta 的優(yōu)化器表現(xiàn)十分亮眼耙鸪簟!
Adadelta 是一種自適應(yīng)的優(yōu)化器舅世,它能夠自動的計算自變量更新量的平方的指數(shù)加權(quán)移動的平均項來作為學(xué)習率旦委。因此,我們設(shè)置的學(xué)習率實際上影響已經(jīng)不是太大了雏亚。
從圖中可以看到社证,這種優(yōu)化器在訓(xùn)練之初和中期又能夠快速下降,只不過到接近最優(yōu)解的時候會出現(xiàn)小幅震蕩评凝。而 SGD 因全程都保持一個學(xué)習率追葡,所以在設(shè)置合適的情況下,在最優(yōu)解附近收斂的更干脆奕短。
# 使用 Adadelta 進行損失函數(shù)的梯度下降求解
train_step = tf.train.AdadeltaOptimizer(0.0001).minimize(cross_entropy)
但由于 Adadelta 實際訓(xùn)練過程中與我們起初告訴它的學(xué)習率關(guān)系不大宜肉,就是不受控,訓(xùn)練起來到后面還是很難受的翎碑,所以 CoorChice 嘗試之后還是選擇換成 SGD 谬返。
lr = tf.Variable(1e-5)
# 使用 SGD 進行損失函數(shù)的梯度下降求解
train_step = tf.train.GradientDescentOptimizer(lr).minimize(cross_entropy)
而且,CoorChice 使用了一個變量 lr 來設(shè)置優(yōu)化器的學(xué)習率日杈,這樣在訓(xùn)練過程中遣铝,我們還可以動態(tài)的額控制當前的學(xué)習率。
增加網(wǎng)絡(luò)深度莉擒、寬度
當發(fā)生欠擬合時酿炸,也有可能是網(wǎng)絡(luò)過于簡單,權(quán)重太少涨冀,導(dǎo)致無法學(xué)習到足夠的信息填硕。在本次訓(xùn)練中,CoorChice 仍然使用了上次的 4 層單核網(wǎng)絡(luò),似乎確實是太簡單了扁眯。
那就加強它吧壮莹。
# coding=utf-8
from cnn_utils import *
class CnnBreadBasketNetwork:
def __init__(self):
with tf.name_scope("input"):
self.x_data = tf.placeholder(tf.float32, shape=[None, 135], name='x_data')
# 補位
input_data = tf.pad(self.x_data, [[0, 0], [0, 1]], 'CONSTANT')
with tf.name_scope("input_reshape"):
# 變形為可以和卷積核卷積的張量
input_data = tf.reshape(input_data, [-1, 17, 8, 1])
tf.summary.image("input", input_data, 1)
self.y_data = tf.placeholder(tf.float32, shape=[None], name='y_data')
# ------------------------構(gòu)建第一層網(wǎng)絡(luò)---------------------
with tf.name_scope("hidden1"):
# 第一個卷積
with tf.name_scope("weights1"):
W_conv11 = weight_variable([3, 3, 1, 64])
variable_summaries(W_conv11, "W_conv11")
with tf.name_scope("biases1"):
b_conv11 = bias_variable([64])
variable_summaries(b_conv11, "b_conv11")
h_conv11 = tf.nn.relu(conv2(input_data, W_conv11) + b_conv11)
tf.summary.histogram('activations_h_conv11', h_conv11)
# 第二個卷積
with tf.name_scope("weights2"):
W_conv12 = weight_variable([3, 3, 64, 64])
variable_summaries(W_conv12, "W_conv12")
with tf.name_scope("biases2"):
b_conv12 = bias_variable([64])
variable_summaries(b_conv12, "b_conv12")
h_conv12 = tf.nn.relu(conv2(h_conv11, W_conv12) + b_conv12)
tf.summary.histogram('activations_h_conv11', h_conv12)
# 池化
h_pool1 = max_pool_2x2(h_conv12)
tf.summary.histogram('pools_h_pool1', h_pool1)
# ------------------------構(gòu)建第二層網(wǎng)絡(luò)---------------------
with tf.name_scope("hidden2"):
# 第一個卷積核
with tf.name_scope("weights1"):
W_conv21 = weight_variable([3, 3, 64, 128])
variable_summaries(W_conv21, 'W_conv21')
with tf.name_scope("biases1"):
b_conv21 = bias_variable([128])
variable_summaries(b_conv21, 'b_conv21')
h_conv21 = tf.nn.relu(conv2(h_pool1, W_conv21) + b_conv21)
tf.summary.histogram('activations_h_conv21', h_conv21)
# 第二個卷積核
with tf.name_scope("weights2"):
W_conv22 = weight_variable([3, 3, 128, 128])
variable_summaries(W_conv22, 'W_conv22')
with tf.name_scope("biases2"):
b_conv22 = bias_variable([128])
variable_summaries(b_conv22, 'b_conv22')
h_conv22 = tf.nn.relu(conv2(h_conv21, W_conv22) + b_conv22)
tf.summary.histogram('activations_h_conv22', h_conv22)
# 池化
self.h_pool2 = max_pool_2x2(h_conv22)
tf.summary.histogram('pools_h_pool2', self.h_pool2)
shape_0 = self.h_pool2.get_shape()[1].value
shape_1 = self.h_pool2.get_shape()[2].value
h_pool2_flat = tf.reshape(self.h_pool2, [-1, shape_0 * shape_1 * 128])
# ------------------------ 構(gòu)建第一層全鏈接層 ---------------------
with tf.name_scope("fc1"):
with tf.name_scope("weights"):
W_fc1 = weight_variable([shape_0 * shape_1 * 128, 4096])
variable_summaries(W_fc1, 'W_fc1')
with tf.name_scope("biases"):
b_fc1 = bias_variable([4096])
variable_summaries(b_fc1, 'b_fc1')
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
tf.summary.histogram('activations_h_fc1', h_fc1)
# ------------------------構(gòu)建輸出層---------------------
with tf.name_scope("output"):
with tf.name_scope("weights"):
W_out = weight_variable([4096, 1])
variable_summaries(W_out, 'W_out')
with tf.name_scope("biases"):
b_out = bias_variable([1])
variable_summaries(b_out, 'b_out')
# 注意??,此處的激活函數(shù)已經(jīng)替換成 ReLu 了
self.y_conv = tf.nn.relu(tf.matmul(h_fc1, W_out) + b_out)
tf.summary.histogram('activations_y_conv', self.y_conv)
這就是經(jīng)過改進后的網(wǎng)絡(luò)的全部代碼了姻檀。
可以看到命满,還是熟悉的套路。以下是版本更新內(nèi)容:
每個卷積核的 size 由 5x5 變成了 3x3
第一個卷積層的卷積核通道數(shù)由原來的 32 增加到了 64绣版。將卷積核數(shù)量提高一倍胶台。
- 卷積核層數(shù)仍然是兩層,但每層都多增加了一個卷積核組僵娃。
- 全鏈接層的神經(jīng)元個數(shù)由原來的 1024 個增加至 4096 個概作。
再說一下網(wǎng)絡(luò)中的一些變化。
self.x_data = tf.placeholder(tf.float32, shape=[None, 135], name='x_data')
# 補位
input_data = tf.pad(self.x_data, [[0, 0], [0, 1]], 'CONSTANT')
# 變形為可以和卷積核卷積的張量
input_data = tf.reshape(input_data, [-1, 17, 8, 1])
self.y_data = tf.placeholder(tf.float32, shape=[None], name='y_data')
定義占位的時候默怨,由于表格數(shù)據(jù)中只有 135 列讯榕,所以先定義一個 shape 為 [None,135] 的占位匙睹。但是比較尷尬的是愚屁,135 是一個奇數(shù),無法轉(zhuǎn)換成能和卷積核卷積的張量痕檬,所以 CoorChice 給它補了一列霎槐,通過 tensorflow 提供的 pad()
函數(shù)。
input_data = tf.pad(self.x_data, [[0, 0], [0, 1]], 'CONSTANT')
第二個參數(shù)中的數(shù)字分別表示需要在原張量的 上梦谜、下丘跌、左、右 補充多少行或列唁桩。這里 CoorChice 再原張量的右邊補充了一列 0闭树。
現(xiàn)在,就可以變形成可和卷積核卷積的張量了荒澡。
input_data = tf.reshape(input_data, [-1, 17, 8, 1])
- 小技巧:通常报辱,經(jīng)過多層的卷積和池化之后,進入第一層全鏈接層或者直接進入輸出層的時候单山,我們需要知道上一層的形狀是怎樣的碍现,然后才能設(shè)計全鏈接層或者輸出層的權(quán)重形狀∶准椋可以通過如下方式方便的獲得輸入的形狀:
# 獲得最后一個池化層的寬
shape_0 = self.h_pool2.get_shape()[1].value
# 獲得最后一個池化層的高
shape_1 = self.h_pool2.get_shape()[2].value
# 變形為全鏈接層可乘的形狀
h_pool2_flat = tf.reshape(self.h_pool2, [-1, shape_0 * shape_1 * 128])
實際上昼接,這個網(wǎng)絡(luò)可能不是太好,深度不夠躏升,可能導(dǎo)致準確率不夠辩棒。但受設(shè)備限制,設(shè)計的太深膨疏,會導(dǎo)致每次訓(xùn)練都會花費大量的時間一睁,所以 CoorChice 就盡量精簡一點,主要看這個過程是怎樣進行的佃却。
這是新的網(wǎng)絡(luò)結(jié)構(gòu):
看看局部的兩組卷積 hidden 層:
開啟訓(xùn)練!
# coding=utf-8
import time
from cnn_model import *
from BBDATA import *
train_times = 20000
base_path = ".../BreadBasket/"
save_path = base_path + str(train_times) + "/"
# 讀取數(shù)據(jù)
BBDATA = read_datas('data/')
# 創(chuàng)建網(wǎng)絡(luò)
network = CnnBreadBasketNetwork()
x_data = network.x_data
y_data = network.y_data
y_conv = network.y_conv
keep_prob = network.keep_prob
# ------------------------構(gòu)建損失函數(shù)---------------------
with tf.name_scope("cross_entropy"):
# 創(chuàng)建正則化對象者吁,此處使用的是 L2 范數(shù)
regularization = tf.contrib.layers.l2_regularizer(scale=(5.0 / 500))
# 應(yīng)用正則化到參數(shù)集上
reg_term = tf.contrib.layers.apply_regularization(regularization)
# 在損失函數(shù)中加入正則化項
cross_entropy = tf.reduce_mean(tf.square((y_conv - y_data))) + reg_term
tf.scalar_summary('loss', cross_entropy)
with tf.name_scope("train_step"):
lr = tf.Variable(1e-5)
# 使用 SGD 進行損失函數(shù)的梯度下降求解
train_step = tf.train.GradientDescentOptimizer(lr).minimize(cross_entropy)
# 記錄平均差值
with tf.name_scope("difference_value"):
dv = tf.reduce_mean(tf.abs(y_conv - y_data))
tf.scalar_summary('difference_value', cross_entropy)
# ------------------------構(gòu)建模型評估函數(shù)---------------------
with tf.name_scope("accuracy"):
with tf.name_scope("correct_prediction"):
correct_prediction = tf.less_equal(tf.abs(y_conv - y_data), 0.5)
with tf.name_scope("accuracy"):
# 計算準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
tf.scalar_summary('accuracy', accuracy)
# 創(chuàng)建會話
sess = tf.InteractiveSession()
summary_merged = tf.merge_all_summaries()
train_writer = tf.train.SummaryWriter(save_path + "graph/train", sess.graph)
test_writer = tf.train.SummaryWriter(save_path + "graph/test")
start_time = int(round(time.time() * 1000))
# 初始化參數(shù)
sess.run(tf.initialize_all_variables())
global loss
loss = 0
global train_accuracy
for i in range(train_times):
# 從訓(xùn)練集中取出 200 個樣本進行一波訓(xùn)練
batch = BBDATA.train_data.batch(200)
if i % 100 == 0:
summary, train_accuracy, test_loss, dv_value = sess.run([summary_merged, accuracy, cross_entropy, dv],
feed_dict={x_data: BBDATA.test_data.data, y_data: BBDATA.test_data.label, keep_prob: 1})
test_writer.add_summary(summary, i)
consume_time = int(round(time.time() * 1000)) - start_time
print("當前共訓(xùn)練 " + str(i) + "次, 累計耗時:" + str(consume_time) + "ms,實時準確率為:%g" % (train_accuracy * 100.) + "%, "
+ "當前誤差均值:" + str(dv_value) + ", train_loss = " + str(loss) + ", test_loss = " + str(test_loss))
if i % 1000 == 0:
run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
run_metadata = tf.RunMetadata()
summary, _, loss = sess.run([summary_merged, train_step, cross_entropy],
feed_dict={x_data: batch.data, y_data: batch.label, keep_prob: 0.6}, options=run_options,
run_metadata=run_metadata)
train_writer.add_run_metadata(run_metadata, str(i))
train_writer.add_summary(summary, i)
else:
summary, _, loss = sess.run([summary_merged, train_step, cross_entropy],
feed_dict={x_data: batch.data, y_data: batch.label, keep_prob: 0.6})
train_writer.add_summary(summary, i)
# 每訓(xùn)練 2000 次保存一次模型
if i != 0 and i % 1000 == 0:
test_accuracy, test_dv = sess.run([accuracy, dv], feed_dict={x_data: BBDATA.test_data.data, y_data: BBDATA.test_data.label, keep_prob: 1})
save_model(base_path + str(i) + "_" + str(test_dv) + "/", sess, i)
# 在測試集計算準確率
summary, final_test_accuracy, test_dv = sess.run([summary_merged, accuracy, dv],
feed_dict={x_data: BBDATA.test_data.data, y_data: BBDATA.test_data.label, keep_prob: 1})
train_writer.add_summary(summary)
print("測試集準確率:%g" % (final_test_accuracy * 100.) + "%, 誤差均值:" + str(test_dv))
print("訓(xùn)練完成饲帅!")
train_writer.close()
test_writer.close()
# 保存模型
save_model(save_path, sess, train_times)
sess.close()
這是完整的訓(xùn)練代碼复凳,基本上和 《【Get】用深度學(xué)習識別手寫數(shù)字》 中是一樣的。只不過 CoorChice 多加了一個 MAE 作為模型評估的指標灶泵。
dv = tf.reduce_mean(tf.abs(y_conv - y_data))
因為是進行回歸預(yù)測育八,所以使用 MAE 均方方差來作為一個評估模型好壞的指標。平均絕對差越小赦邻,表明模型預(yù)測結(jié)果越接近于真實情況髓棋。
此外,由于是做回歸預(yù)測惶洲,所以之前分類使用的交叉熵損失函數(shù)就不是特別合適了按声,我們換成均方誤差損失函數(shù)。
cross_entropy = tf.reduce_mean(tf.square((y_conv - y_data))) + reg_term
需要注意的是:
correct_prediction = tf.less_equal(tf.abs(y_conv - y_data), 0.5)
評估模型現(xiàn)在不應(yīng)該再是比較預(yù)測值和標簽值是否相等了恬吕,在回歸預(yù)測中签则,要做到模型能夠預(yù)測出的值和真實值完全相等幾乎是不可能的。我們把評估模型改造成絕對差 <500 即接受該預(yù)測結(jié)果铐料,畢竟是在個人設(shè)備上跑渐裂,要達到比較高的精度還是很難的。
loss 不下降了
上面這段代碼中钠惩,SGD 優(yōu)化器的學(xué)習率變量實際上是 CoorChice 后來加上的柒凉。在此前,使用的固定的妻柒,但當 run 起來開始訓(xùn)練的時候扛拨,發(fā)現(xiàn) loss 總在一個值很大,但范圍很小的區(qū)間內(nèi)波動举塔,而且準確率也在小范圍的波動绑警,提升不上去。
- 損失變化曲線
- 準確率變化曲線
此時的準確大概在 10.5% 附近徘徊開來央渣,loss 趨勢變化也不大计盒。
這說明訓(xùn)練基本處于停滯不前的狀態(tài)了,但顯然芽丹,我們的模型精度是不夠的北启。出現(xiàn)這種情況可能是以下幾種情況。
陷入局部最優(yōu)解
如圖,在多特征的高維 loss 曲面中咕村,可能存在很多 A 點旁邊的小坑场钉。當進入小坑的底部時,loss 的一階導(dǎo)數(shù)也是為 0 的懈涛,就網(wǎng)上常常說的陷入了局部最優(yōu)解中逛万,而沒有到真正的 Global Minima。
實際上批钠,上面代碼中我們采取的 mini-patch 抽樣進行訓(xùn)練宇植,一定程度上也是能減小陷入局部最優(yōu)解的情況的。因為可能這批樣本陷入了局部最優(yōu)中埋心,下一批樣本的波動又把你從坑里拽出來了指郁。當然有的時候我們可能會看到 loss 反而增大了,也是因為這種方式導(dǎo)致的拷呆。
騎在鞍點上
如圖闲坎,在 loss 的曲面中,會有比局部最優(yōu)坑還多的馬鞍型曲面洋腮。
憨態(tài)可掬的??...
當我們騎到馬鞍上的鞍點是箫柳,此處的導(dǎo)數(shù)也是為0的,那么是真正的收斂了嗎啥供?不是的悯恍。看圖中伙狐,我們在x軸方向上是極小值涮毫,但在y軸方向上是極大值。
如果落在這樣正在進入鞍點的尷尬位置贷屎,那么你的訓(xùn)練就會看起來幾乎毫無波動罢防,走出這個區(qū)域需要花費很長的時間。
幸運的是唉侄,通過適當增大學(xué)習率或者適當減小 batch咒吐,都有助于盡快逃離這個區(qū)域。
無盡的平緩面
然后在 loss 曲面中属划,同時存在著如上圖一樣的大面積的平緩曲面恬叹,處于這些區(qū)域中的點,雖然導(dǎo)數(shù)不為 0 同眯,但值卻及其的小绽昼,即使一遍又一遍的反復(fù)訓(xùn)練,仍然看起來幾乎沒有變化须蜗。
可怕的是硅确,我們無法分辨出是處于局部最優(yōu)目溉,還是處于馬鞍區(qū)域,或者干脆就是在類平面上菱农。
如何處理缭付?
出現(xiàn)以上幾種情況,都會導(dǎo)致看起來不學(xué)習的情況大莫,特別是在性能有限的個人設(shè)備上蛉腌。但我們還是有一些套路可以盡量減小這種情況的發(fā)生的官份。
- 檢查數(shù)據(jù)只厘。確保數(shù)據(jù)標簽對應(yīng)正常。
- 修改調(diào)整網(wǎng)絡(luò)結(jié)構(gòu)舅巷。盡量使用一些開源的大牛們產(chǎn)出的網(wǎng)絡(luò)模型羔味,如 VGG 系列。在它們的基礎(chǔ)上修改大概率上比自己搗騰靠譜钠右。
- 調(diào)整 batch 大小赋元。batch過大導(dǎo)致訓(xùn)練時間過長,也容易進入局部最優(yōu)或者馬鞍區(qū)域飒房。過小又回導(dǎo)致震蕩比較劇烈搁凸,不容易找到最優(yōu)解。
- 多嘗試不同的參數(shù)初始化狠毯。如果足夠幸運的話护糖,沒準初始化就落到了 Global Minima 的坑里去了。
- 動態(tài)學(xué)習率嚼松。一成不變的小的學(xué)習率當陷入局部最優(yōu)時就算是掉坑里了嫡良,或者在平滑面上移動的特別慢。如果大了又容易錯過最優(yōu)解献酗。所以像 Adam 這樣動態(tài)的學(xué)習率優(yōu)化器能一定程度上減少這些問題的發(fā)生寝受。當然,我們也可以在 SGD 的基礎(chǔ)上罕偎,根據(jù) loss 的情況自己動態(tài)的增大學(xué)習率很澄,在 loss 很大卻不變的時候,在 loss 較小的時候減小學(xué)習率颜及,避免錯過最優(yōu)解甩苛。
通過 loss 大致的判斷問題
train loss 不斷下降,test loss不斷下降器予,說明網(wǎng)絡(luò)仍在學(xué)習;
train loss 不斷下降浪藻,test loss趨于不變,說明網(wǎng)絡(luò)過擬合;
train loss 趨于不變乾翔,test loss不斷下降爱葵,說明數(shù)據(jù)集100%有問題;
train loss 趨于不變施戴,test loss趨于不變,說明學(xué)習遇到瓶頸萌丈,需要減小學(xué)習率或批量數(shù)目;
train loss 不斷上升赞哗,test loss不斷上升,說明網(wǎng)絡(luò)結(jié)構(gòu)設(shè)計不當辆雾,訓(xùn)練超參數(shù)設(shè)置不當肪笋,數(shù)據(jù)集經(jīng)過清洗等問題。
通過 loss 可以大致的判斷一些問題度迂,上述規(guī)律可以最為參考藤乙,但不保證一定準確。
總結(jié)
最后惭墓,一張圖做個總結(jié):
- 抽出空余時間寫文章分享需要動力扔亥,各位看官動動小手點個贊哈徒探,信仰還是需要持續(xù)充值的??
- CoorChice 一直在不定期的分享新的干貨孩擂,想要上車只需進到 CoorChice的【個人主頁】 點個關(guān)注就好了哦祭玉。