【資料處理】使用深度學習預測未來銷量

CoorChice發表於2018-10-28


《【Get】用深度學習識別手寫數字》 中,我們通過一個手寫數字識別的例子,體驗瞭如何使用 深度學習 + tensorflow 解決一個具體的問題。

實際上,這是一個分類問題,即將輸入的圖片資料分成 0-9 共 10 個類別,而且我們的資料都是直接使用 MNIST 上下載的處理好的資料。

在現實生產中,我們的資料來源通常來自於資料庫,是沒有經過預處理的,那麼我們該做些什麼來讓這些資料庫裡的資料能夠用於進行機器學習呢?

機器學習的前置步驟,資料預處理就是解決這個問題的。

本篇 CoorChice 將會通過一個迴歸預測問題,來展示如何進行這個過程。

原始資料

我們用於機器學習的資料的量不能太少,否則訓練效果會極差。但是我們個人如何才能獲取到大量的資料來進行訓練呢?

CoorChice 推薦 kaggle:https://www.kaggle.com/datasets,這是全球最大的資料科學社群和資料競賽平臺。在這個美妙的地方,我們可以輕鬆的找到各種各樣的,海量的,精彩絕倫的資料。

開啟連結就可以看到海量的資料,我們很隨意的選擇其中一個資料來源來進行訓練。

就選第一條吧!

第一條資料是關於一個麵包店的交易的資料。點選進入後,找到下載就可以下載這套資料了。

也許你可能不能立刻下載,因為他會要求你先登入。那就成為 Kaggle 的會員吧。

我們使用表格開啟剛剛下載的資料觀察觀察。

My God! 這是一堆簡陋不堪的資料,共有 21293 條資料,每條只有 4 個特徵! 4 個特徵! 4 個特徵!

沒關係,我們只是學習如何處理資料的過程,所以就湊合著用一下吧。

開始資料處理

實際上,通常我們的資料呆在資料庫中,就是這種表格的形式。

在開始之前,需要先安裝一下 Pandas 這個 Python 庫,它被用來讀取我們的表格資料,和進行增、刪、改、查等操作。

下好了,我們就開始吧...

觀察資料

進行資料處理最重要的一步,就是對資料進行觀察和分析。通過觀察和分析,我們要除去一些無關緊要的資料列,比如在預測天氣預報的時候,我們就應該把 ‘李雷早上吃了什麼’ 這個資料列去掉,因為對模型訓練毫無幫助。

同時,我們要從資料中挖掘出一些隱藏的資訊,以擴充特徵。日入在預測產品的交易資訊時,我們從日期這個資料列,就能知道當天是否是一個節日,是什麼節日。

在我們下載的這堆麵包店的交易資料中,僅有慘不忍睹的 4 個類目!

通常,當我們使用機器學習來解決問題時,都會有一個目的。比如,在這堆資料中,CoorChice 希望通過機器學習訓練一個模型,能夠用於預測未來一定條件下的某種型別的麵包所能達到的交易量是多少。

因此,我們將交易量 Transaction 這一列資料作為資料集的標籤資料。

接下來剩下的只有 日期、時間、麵包類目 這三個特徵了。

從資料中挖掘更多的資訊

僅有這三個特徵來進行訓練,結果可想而知會有多差。比你想的差還要差,所以我們需要從現有的資料中挖掘出更多的資訊來。比如,在圖片資料中,通常會進行一些旋轉、平移之類的操作,來補充資料量。

日期 Date

首先看看日期能挖掘到什麼有用的資訊?

  1. 當然是星期啦。今天是星期一、星期二、...、對交易肯定是有影響的。我們通過 Date 可以計算出當前日期是周幾。

  2. 還有什麼可挖掘的嗎?嗯,左思右想...咦,節日肯定也是影響麵包交易量的一個重要因素,有了 Date 我們也能知道當前日期是什麼節日。

  3. 然後,通過觀察可以發現,隨著日期的增加,交易量也是在累加的,所以日期的大小也可直接作為一個特徵。

  4. 由於 CoorChice 要嘗試預測的是未來某個時間、某種麵包所能達到的交易量,所以日期的大小對結果必然是有影響的。但我們不能直接將日期轉換為一個形如 20161030 這樣的數字,因為一年只有 12 個月,每一個月天數不一樣的,所以這個數值跳躍度比較大,這樣就大大增加了訓練的難度和準確率。我們需要的是一個統一標準的數值。如果確定某一天為基準,然後把日期轉換為和這一天的距離,那量化標準自然就是很明瞭的了。因為資料是從 2016-10-30 開始,那麼把這天作為基準日就再合適不過了。

時間 Time

接下來觀察 Time 有什麼可用的沒。

  1. Time 本身是一些比較密集的資料,間隔不大。我們可以把它劃歸到小時裡,因此就會有 24 個小時。比如,“10:13:03” 就讓它屬於 10 點這個小時吧。

  2. 通過時間,我們還可以知道現在是處於 早晨、上午、中午、下午、傍晚、晚上、深夜 中的那個時間段。不同的時間段或多或少也會影響交易量。

確定好了思路,接下來就開始找著這個思路處理資料了。

處理原始資料

處理資料的第一步當然是先讀取資料啦,我們前面下載的 Pandas 就使用來幹這個的。

讀取資料

# 讀取資料
datas = pd.read_csv('data/BreadBasket_DMS.csv')
複製程式碼

讀取資料後,輸出一下看看長什麼樣子。

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]
複製程式碼

增強資料

然後,我們對原始資料按照上面分析的思路進行處理。

# 挖掘,周幾對交易量的影響
datas['Weekday'] = datas['Date']
datas['Weekday'] = datas['Weekday'].map(lambda x: get_weekday(x))

# 挖掘,各個節日對交易量的影響
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 一列變為與 2016-10-30 這天的距離
datas['Date'] = datas['Date'].map(lambda x: get_delta_days(x))
# 將時間轉變為時段
datas['Time'] = datas['Time'].map(lambda x: get_time_range(x))
# 縮小資料大小
datas['Transaction'] = datas['Transaction'].map(lambda x: (float(x) / 1000.))
複製程式碼

現在,再輸出一下資料看看。

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]
複製程式碼

從表格資料中可以看到,Date 和 Transaction 都變成浮點數了,因為把它們進行縮小,有利於後面的梯度下降,否則 loss 會非常非常的大。

現在,我們已經擴充了一些特徵。

進行 One-Hot 編碼

接下來,對於離散的資料,比如 Weekday、Time 這樣的,它的大小並不會對交易產生影響,但這種離散的特徵的類別取值是會對結果產生影響的。比如,春節這天會對食物的交易量產生影響。

我們需要對這樣的特徵資料進行 獨熱編碼 。關於 獨熱編碼 ,你可以在 CoorChice 的 《機器學習,看完就明白了》 這篇文章,瞭解相關資訊。

ont_hot_data = pd.get_dummies(datas, prefix=['Time', 'Item', 'Weekday', 'Festival', 'Time_Quantum'])
複製程式碼

看看現在資料表變成了什麼樣子了。

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]
複製程式碼

經過獨熱編碼後,columns 已經擴充到了驚人的 136 列!

之所以需對資料進行獨熱編碼,還有一個原因是在訓練時,獨熱編碼在分類是 0 和 1 的關係,而如果用 1,2,3,.. 的分類標籤方式,是不利於梯度下降的,計算出的梯度往往比較大。

對 Date、Transaction,可以看到表格中,CoorChice 還對他們進行縮小,目的是為了把數值範圍變小,這樣有利於梯度下降的求解。

我們看看現在資料的分佈情況:

打亂資料

接下來,我們需要將元素的有序的資料進行打亂,這樣有助於提高訓練出來的模型的泛化能力。

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 一列,說明資料已經被打的足夠亂了。

儲存資料

最後,我們要把處理好的資料分為訓練集和測試集,CoorChice 按大概 30% 取測試集資料,然後分別儲存。

# 測試資料集大小
test_count = 6000
train_count = ont_hot_data.shape[0] - test_count
# 切割出訓練資料集
train_data = ont_hot_data[:train_count]
# 切割出測試資料集
test_data = ont_hot_data[train_count:]
# 分別儲存兩個資料集
train_data.to_csv('data/train_data.csv', index=False, header=True)
test_data.to_csv('data/test_data.csv', index=False, header=True)
複製程式碼

儲存處理好的資料很簡單,使用 Pandas 提供的 to_csv() 就可以把資料存 csv 格式。

index 和 header 分別表示是否儲存行號和列名稱。

現在,我們已經把原始資料處理好,並且分割成了訓練集和測試集。我們有 15,293 條訓練資料,和 6000 條測試資料。

開始訓練吧!

在開始之前,建議先看一看這一篇 《【Get】用深度學習識別手寫數字》 ,因為套路基本上是一樣的,只是有的地方需要根據具體情況調整。

在手寫數字識別中,我們構建的是一個簡單 4 層網路,它由兩個卷積層,一個全連結層和一個輸出層組成。

image_graph

回顧一下這個網路結構。

發生了欠擬合!

一開始,CoorChice 直接使用了這個網路進行訓練,經過 10w 次的漫長訓練之後發現,準確率最高不過 0.1%。

起初 CoorChice 減小 lr(學習率),想著可能是學習率過大,導致震盪了。然而,毫無作用。

出現這種現象,基本可以判斷大概率上是發生欠擬合了。

發生欠擬合,可以通過以下步驟一步步的排除問題和改進網路。

使用適合的權重初始化方案

本次訓練中,權重的初始化沿用了上次的正太分佈初始化方案,應該是比較優秀的方案了,所以就不做改動了。

image_truncated

選擇適當的啟用函式

在上一次的網路構建中,卷積層、全連結層都用的是 ReLu 啟用函式,輸出層使用的是 Softmax 啟用函式。而這次 CoorChice 把啟用函式全換成了優秀的 ReLu 啟用函式,這應該也是沒毛病的。

image

選擇適合的優化器和學習率

這是一張各種優化器的比較圖,從圖中可以看到,有一個名叫 Adadelta 的優化器表現十分亮眼啊!

Adadelta 是一種自適應的優化器,它能夠自動的計算自變數更新量的平方的指數加權移動的平均項來作為學習率。因此,我們設定的學習率實際上影響已經不是太大了。

從圖中可以看到,這種優化器在訓練之初和中期又能夠快速下降,只不過到接近最優解的時候會出現小幅震盪。而 SGD 因全程都保持一個學習率,所以在設定合適的情況下,在最優解附近收斂的更乾脆。

# 使用 Adadelta 進行損失函式的梯度下降求解
train_step = tf.train.AdadeltaOptimizer(0.0001).minimize(cross_entropy)
複製程式碼

但由於 Adadelta 實際訓練過程中與我們起初告訴它的學習率關係不大,就是不受控,訓練起來到後面還是很難受的,所以 CoorChice 嘗試之後還是選擇換成 SGD 。

lr = tf.Variable(1e-5)
# 使用 SGD 進行損失函式的梯度下降求解
train_step = tf.train.GradientDescentOptimizer(lr).minimize(cross_entropy)
複製程式碼

而且,CoorChice 使用了一個變數 lr 來設定優化器的學習率,這樣在訓練過程中,我們還可以動態的額控制當前的學習率。

增加網路深度、寬度

當發生欠擬合時,也有可能是網路過於簡單,權重太少,導致無法學習到足夠的資訊。在本次訓練中,CoorChice 仍然使用了上次的 4 層單核網路,似乎確實是太簡單了。

那就加強它吧。

 # 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')

        # ------------------------構建第一層網路---------------------
        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)

        # ------------------------構建第二層網路---------------------
        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])

        # ------------------------ 構建第一層全連結層 ---------------------
        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)


        # ------------------------構建輸出層---------------------
        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')
            # 注意⚠️,此處的啟用函式已經替換成 ReLu 了
            self.y_conv = tf.nn.relu(tf.matmul(h_fc1, W_out) + b_out)
            tf.summary.histogram('activations_y_conv', self.y_conv)

複製程式碼

這就是經過改進後的網路的全部程式碼了。

可以看到,還是熟悉的套路。以下是版本更新內容:

  • 每個卷積核的 size 由 5x5 變成了 3x3

  • 第一個卷積層的卷積核通道數由原來的 32 增加到了 64。將卷積核數量提高一倍。

  • 卷積核層數仍然是兩層,但每層都多增加了一個卷積核組。

  • 全連結層的神經元個數由原來的 1024 個增加至 4096 個。

再說一下網路中的一些變化。

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')
複製程式碼

定義佔位的時候,由於表格資料中只有 135 列,所以先定義一個 shape 為 [None,135] 的佔位。但是比較尷尬的是,135 是一個奇數,無法轉換成能和卷積核卷積的張量,所以 CoorChice 給它補了一列,通過 tensorflow 提供的 pad() 函式。

input_data = tf.pad(self.x_data, [[0, 0], [0, 1]], 'CONSTANT')
複製程式碼

第二個引數中的數字分別表示需要在原張量的 上、下、左、右 補充多少行或列。這裡 CoorChice 再原張量的右邊補充了一列 0。

現在,就可以變形成可和卷積核卷積的張量了。

input_data = tf.reshape(input_data, [-1, 17, 8, 1])
複製程式碼
  • 小技巧:通常,經過多層的卷積和池化之後,進入第一層全連結層或者直接進入輸出層的時候,我們需要知道上一層的形狀是怎樣的,然後才能設計全連結層或者輸出層的權重形狀。可以通過如下方式方便的獲得輸入的形狀:
# 獲得最後一個池化層的寬
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])
複製程式碼

實際上,這個網路可能不是太好,深度不夠,可能導致準確率不夠。但受裝置限制,設計的太深,會導致每次訓練都會花費大量的時間,所以 CoorChice 就儘量精簡一點,主要看這個過程是怎樣進行的。

這是新的網路結構:

看看區域性的兩組卷積 hidden 層:

開啟訓練!

# 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) + "/"

# 讀取資料
BBDATA = read_datas('data/')

# 建立網路
network = CnnBreadBasketNetwork()
x_data = network.x_data
y_data = network.y_data
y_conv = network.y_conv
keep_prob = network.keep_prob
# ------------------------構建損失函式---------------------
with tf.name_scope("cross_entropy"):
    # 建立正則化物件,此處使用的是 L2 範數
    regularization = tf.contrib.layers.l2_regularizer(scale=(5.0 / 500))
    # 應用正則化到引數集上
    reg_term = tf.contrib.layers.apply_regularization(regularization)
    # 在損失函式中加入正則化項
    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 進行損失函式的梯度下降求解
    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)

# ------------------------構建模型評估函式---------------------
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)

# 建立會話
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))

# 初始化引數
sess.run(tf.initialize_all_variables())

global loss
loss = 0
global train_accuracy
for i in range(train_times):
    # 從訓練集中取出 200 個樣本進行一波訓練
    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("當前共訓練 " + 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)

    # 每訓練 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("訓練完成!")
train_writer.close()
test_writer.close()
# 儲存模型
save_model(save_path, sess, train_times)

sess.close()
複製程式碼

這是完整的訓練程式碼,基本上和 《【Get】用深度學習識別手寫數字》 中是一樣的。只不過 CoorChice 多加了一個 MAE 作為模型評估的指標。

dv = tf.reduce_mean(tf.abs(y_conv - y_data))
複製程式碼

因為是進行迴歸預測,所以使用 MAE 均方方差來作為一個評估模型好壞的指標。平均絕對差越小,表明模型預測結果越接近於真實情況。

此外,由於是做迴歸預測,所以之前分類使用的交叉熵損失函式就不是特別合適了,我們換成均方誤差損失函式。

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)
複製程式碼

評估模型現在不應該再是比較預測值和標籤值是否相等了,在迴歸預測中,要做到模型能夠預測出的值和真實值完全相等幾乎是不可能的。我們把評估模型改造成絕對差 <500 即接受該預測結果,畢竟是在個人裝置上跑,要達到比較高的精度還是很難的。

loss 不下降了

上面這段程式碼中,SGD 優化器的學習率變數實際上是 CoorChice 後來加上的。在此前,使用的固定的,但當 run 起來開始訓練的時候,發現 loss 總在一個值很大,但範圍很小的區間內波動,而且準確率也在小範圍的波動,提升不上去。

  • 損失變化曲線

  • 準確率變化曲線

此時的準確大概在 10.5% 附近徘徊開來,loss 趨勢變化也不大。

這說明訓練基本處於停滯不前的狀態了,但顯然,我們的模型精度是不夠的。出現這種情況可能是以下幾種情況。

陷入區域性最優解

如圖,在多特徵的高維 loss 曲面中,可能存在很多 A 點旁邊的小坑。當進入小坑的底部時,loss 的一階導數也是為 0 的,就網上常常說的陷入了區域性最優解中,而沒有到真正的 Global Minima。

實際上,上面程式碼中我們採取的 mini-patch 抽樣進行訓練,一定程度上也是能減小陷入區域性最優解的情況的。因為可能這批樣本陷入了區域性最優中,下一批樣本的波動又把你從坑裡拽出來了。當然有的時候我們可能會看到 loss 反而增大了,也是因為這種方式導致的。

騎在鞍點上

如圖,在 loss 的曲面中,會有比區域性最優坑還多的馬鞍型曲面。

憨態可掬的?...

當我們騎到馬鞍上的鞍點是,此處的導數也是為0的,那麼是真正的收斂了嗎?不是的。看圖中,我們在x軸方向上是極小值,但在y軸方向上是極大值。

如果落在這樣正在進入鞍點的尷尬位置,那麼你的訓練就會看起來幾乎毫無波動,走出這個區域需要花費很長的時間。

幸運的是,通過適當增大學習率或者適當減小 batch,都有助於儘快逃離這個區域。

無盡的平緩面

然後在 loss 曲面中,同時存在著如上圖一樣的大面積的平緩曲面,處於這些區域中的點,雖然導數不為 0 ,但值卻及其的小,即使一遍又一遍的反覆訓練,仍然看起來幾乎沒有變化。

可怕的是,我們無法分辨出是處於區域性最優,還是處於馬鞍區域,或者乾脆就是在類平面上。

如何處理?

出現以上幾種情況,都會導致看起來不學習的情況,特別是在效能有限的個人裝置上。但我們還是有一些套路可以儘量減小這種情況的發生的。

  1. 檢查資料。確保資料標籤對應正常。

  2. 修改調整網路結構。儘量使用一些開源的大牛們產出的網路模型,如 VGG 系列。在它們的基礎上修改大概率上比自己搗騰靠譜。

  3. 調整 batch 大小。batch過大導致訓練時間過長,也容易進入區域性最優或者馬鞍區域。過小又回導致震盪比較劇烈,不容易找到最優解。

  4. 多嘗試不同的引數初始化。如果足夠幸運的話,沒準初始化就落到了 Global Minima 的坑裡去了。

  5. 動態學習率。一成不變的小的學習率當陷入區域性最優時就算是掉坑裡了,或者在平滑面上移動的特別慢。如果大了又容易錯過最優解。所以像 Adam 這樣動態的學習率優化器能一定程度上減少這些問題的發生。當然,我們也可以在 SGD 的基礎上,根據 loss 的情況自己動態的增大學習率,在 loss 很大卻不變的時候,在 loss 較小的時候減小學習率,避免錯過最優解。

通過 loss 大致的判斷問題

  • train loss 不斷下降,test loss不斷下降,說明網路仍在學習;

  • train loss 不斷下降,test loss趨於不變,說明網路過擬合;

  • train loss 趨於不變,test loss不斷下降,說明資料集100%有問題;

  • train loss 趨於不變,test loss趨於不變,說明學習遇到瓶頸,需要減小學習率或批量數目;

  • train loss 不斷上升,test loss不斷上升,說明網路結構設計不當,訓練超引數設定不當,資料集經過清洗等問題。

通過 loss 可以大致的判斷一些問題,上述規律可以最為參考,但不保證一定準確。

總結

最後,一張圖做個總結:

  • 抽出空餘時間寫文章分享需要動力,各位看官動動小手點個贊哈,信仰還是需要持續充值的?
  • CoorChice 一直在不定期的分享新的乾貨,想要上車只需進到 CoorChice的【個人主頁】 點個關注就好了哦。





相關文章