如何基於TensorFlow使用LSTM和CNN實現時序分類任務

思源發表於2017-09-12

時序資料經常出現在很多領域中,如金融、訊號處理、語音識別和醫藥。傳統的時序問題通常首先需要人力進行特徵工程,才能將預處理的資料輸入到機器學習演算法中。並且這種特徵工程通常需要一些特定領域內的專業知識,因此也就更進一步加大了預處理成本。例如訊號處理(即 EEG 訊號分類),特徵工程可能就涉及到各種頻帶的功率譜(power spectra)、Hjorth 引數和其他一些特定的統計學特徵。本文簡要地介紹了使用 CNN 和 LSTM 實現序列分類的方法,詳細程式碼請檢視 Github。

Github 專案地址:https://github.com/healthDataScience/deep-learning-HAR

傳統影象分類中也是採用的手動特徵工程,然而隨著深度學習的出現,卷積神經網路已經可以較為完美地處理計算機視覺任務。使用 CNN 處理影象不需要任何手動特徵工程,網路會一層層自動從最基本的特徵組合成更加高階和抽象的特徵,從而完成計算機視覺任務。

在本文中,我們將討論如何使用深度學習方法對時序資料進行分類。我們使用的案例是 UCI 專案中的人體活動識別(HAR)資料集。該資料集包含原始的時序資料和經預處理的資料(包含 561 個特徵)。本文將對比用特徵工程的機器學習演算法和兩種深度學習方法(卷積神經網路和迴圈神經網路),試驗最後表明深度學習方法超越了傳統使用特徵工程的方法。

作者使用 TensorFlow 和實現並訓練模型,文中只展示了部分程式碼,更詳細的程式碼請檢視 Github。

卷積神經網路(CNN)

首先第一步就是將資料饋送到 Numpy 中的陣列,且陣列的維度為 (batch_size, seq_len, n_channels),其中 batch_size 為模型在執行 SGD 時每一次迭代需要的資料量,seq_len 為時序序列的長度(本文中為 128),n_channels 為執行檢測(measurement)的通道數。本文案例中通道數為 9,即 3 個座標軸每一個有 3 個不同的加速檢測(acceleration measurement)。我們有六個活動標籤,即每一個樣本屬於 LAYING、STANDING、SITTING、WALKING_DOWNSTAIRS、WALKING_UPSTAIRS 或 WALKING。

下面,我們首先構建計算圖,其中我們使用佔位符為輸入資料做準備:

graph = tf.Graph()
 
with graph.as_default():
    inputs_ = tf.placeholder(tf.float32, [None, seq_len, n_channels],
        name = 'inputs')
    labels_ = tf.placeholder(tf.float32, [None, n_classes], name = 'labels')
    keep_prob_ = tf.placeholder(tf.float32, name = 'keep')
    learning_rate_ = tf.placeholder(tf.float32, name = 'learning_rate')

其中 inputs_是饋送到計算圖中的輸入張量,第一個引數設定為「None」可以確保佔位符第一個維度可以根據不同的批量大小而適當調整。labels_是需要預測的 one-hot 編碼標籤,keep_prob_為用於 dropout 正則化的保持概率,learning_rate_ 為用於 Adam 優化器的學習率。

我們使用在序列上移動的 1 維卷積核構建卷積層,影象一般使用的是 2 維卷積核。序列任務中的卷積核可以充當為訓練中的濾波器。在許多 CNN 架構中,層級的深度越大,濾波器的數量就越多。每一個卷積操作後面都跟隨著池化層以減少序列的長度。下面是我們可以使用的簡單 CNN 架構。

如何基於TensorFlow使用LSTM和CNN實現時序分類任務

上圖描述的卷積層可用以下程式碼實現:

with graph.as_default():
    # (batch, 128, 9) -> (batch, 32, 18)
    conv1 = tf.layers.conv1d(inputs=inputs_, filters=18, kernel_size=2, strides=1,
        padding='same', activation = tf.nn.relu)
    max_pool_1 = tf.layers.max_pooling1d(inputs=conv1, pool_size=4, strides=4, padding='same')
 
    # (batch, 32, 18) -> (batch, 8, 36)
    conv2 = tf.layers.conv1d(inputs=max_pool_1, filters=36, kernel_size=2, strides=1,
    padding='same', activation = tf.nn.relu)
    max_pool_2 = tf.layers.max_pooling1d(inputs=conv2, pool_size=4, strides=4, padding='same')
 
    # (batch, 8, 36) -> (batch, 2, 72)
    conv3 = tf.layers.conv1d(inputs=max_pool_2, filters=72, kernel_size=2, strides=1,
    padding='same', activation = tf.nn.relu)
    max_pool_3 = tf.layers.max_pooling1d(inputs=conv3, pool_size=4, strides=4, padding='same')

一旦到達了最後一層,我們需要 flatten 張量並投入到有適當神經元數的分類器中,在上圖中為 144 個神經元。隨後分類器輸出 logits,並用於以下兩種案例:

  1. 計算 softmax 交叉熵函式,該損失函式在多類別問題中是標準的損失度量。
  2. 在最大化概率和準確度的情況下預測類別標籤。

下面是上述過程的實現:

with graph.as_default():
    # Flatten and add dropout
    flat = tf.reshape(max_pool_3, (-1, 2*72))
    flat = tf.nn.dropout(flat, keep_prob=keep_prob_)
 
    # Predictions
    logits = tf.layers.dense(flat, n_classes)
 
    # Cost function and optimizer
    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits,
        labels=labels_))
    optimizer = tf.train.AdamOptimizer(learning_rate_).minimize(cost)
 
    # Accuracy
    correct_pred = tf.equal(tf.argmax(logits, 1), tf.argmax(labels_, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32), name='accuracy')

剩下的實現部分就比較典型了,讀者可檢視 GitHub 中的完整程式碼和過程。前面我們已經構建了計算圖,後面就需要將批量訓練資料饋送到計算圖進行訓練,同時我們還要使用驗證集來評估訓練結果。最後,完成訓練的模型將在測試集上進行評估。我們在該實驗中 batch_siza 使用的是 600、learning_rate 使用的是 0.001、keep_prob 為 0.5。在 500 個 epoch 後,我們得到的測試精度為 98%。下圖顯示了訓練準確度和驗證準確度隨 epoch 的增加而顯示的變化:

如何基於TensorFlow使用LSTM和CNN實現時序分類任務

長短期記憶網路(LSTM)

LSTM 在處理文字資料上十分流行,它在情感分析、機器翻譯、和文字生成等方面取得了十分顯著的成果。因為本問題涉及相似分類的序列,所以 LSTM 是比較優秀的方法。

下面是能用於該問題的神經網路架構:

如何基於TensorFlow使用LSTM和CNN實現時序分類任務

為了將資料饋送到網路中,我們需要將陣列分割為 128 塊(序列中的每一塊都會進入一個 LSTM 單元),每一塊的維度為(batch_size, n_channels)。隨後單層神經元將轉換這些輸入並饋送到 LSTM 單元中,每一個 LSTM 單元的維度為 lstm_size,一般該引數需要選定為大於通道數量。這種方式很像文字應用中的嵌入層,其中詞彙從給定的詞彙表中嵌入為一個向量。後面我們需要選擇 LSTM 層的數量(lstm_layers),我們可以設定為 2。

對於這一個實現,佔位符的設定可以和上面一樣。下面的程式碼段實現了 LSTM 層級:

with graph.as_default():
    # Construct the LSTM inputs and LSTM cells
    lstm_in = tf.transpose(inputs_, [1,0,2]) # reshape into (seq_len, N, channels)
    lstm_in = tf.reshape(lstm_in, [-1, n_channels]) # Now (seq_len*N, n_channels)
 
    # To cells
    lstm_in = tf.layers.dense(lstm_in, lstm_size, activation=None)
 
    # Open up the tensor into a list of seq_len pieces
    lstm_in = tf.split(lstm_in, seq_len, 0)
 
    # Add LSTM layers
    lstm = tf.contrib.rnn.BasicLSTMCell(lstm_size)
    drop = tf.contrib.rnn.DropoutWrapper(lstm, output_keep_prob=keep_prob_)
    cell = tf.contrib.rnn.MultiRNNCell([drop] * lstm_layers)
    initial_state = cell.zero_state(batch_size, tf.float32)

上面的程式碼段是十分重要的技術細節。我們首先需要將陣列從 (batch_size, seq_len, n_channels) 重建維度為 (seq_len, batch_size, n_channels),因此 tf.split 將在每一步適當地分割資料(根據第 0 個索引)為一系列 (batch_size, lstm_size) 陣列。剩下的部分就是標準的 LSTM 實現了,包括構建層級和初始狀態。

下一步就是實現網路的前向傳播和成本函式。比較重要的技術點是我們引入了梯度截斷,因為梯度截斷可以在反向傳播中防止梯度爆炸而提升訓練效果。

下面是我們定義前向傳播和成本函式的程式碼:

with graph.as_default():
    outputs, final_state = tf.contrib.rnn.static_rnn(cell, lstm_in, dtype=tf.float32,
        initial_state = initial_state)
 
    # We only need the last output tensor to pass into a classifier
    logits = tf.layers.dense(outputs[-1], n_classes, name='logits')
 
    # Cost function and optimizer
    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=labels_))
 
    # Grad clipping
    train_op = tf.train.AdamOptimizer(learning_rate_)
 
    gradients = train_op.compute_gradients(cost)
    capped_gradients = [(tf.clip_by_value(grad, -1., 1.), var) for grad, var in gradients]
    optimizer = train_op.apply_gradients(capped_gradients)
 
    # Accuracy
    correct_pred = tf.equal(tf.argmax(logits, 1), tf.argmax(labels_, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32), name='accuracy')

注意我們只使用了 LSTM 頂層輸出序列的最後一個元素,因為我們每個序列只是嘗試預測一個分類概率。剩下的部分和前面我們訓練 CNN 的過程相似,我們只需要將資料饋送到計算圖中進行訓練。其中超引數可選擇為 lstm_size=27、lstm_layers=2、batch_size=600、learning_rate=0.0005 和 keep_prob=0.5,我們在測試集中可獲得大約 95% 的準確度。這一結果要比 CNN 還差一些,但仍然十分優秀。可能選擇其它超引數能產生更好的結果,讀者朋友也可以在 Github 中獲取原始碼並進一步除錯。

對比傳統方法

前面作者已經使用帶 561 個特徵的資料集測試了一些機器學習方法,效能最好的方法是梯度提升樹,如下梯度提升樹的準確度能到達 96%。雖然 CNN、LSTM 架構與經過特徵工程的梯度提升樹的精度差不多,但 CNN 和 LSTM 的人工工作量要少得多。

HAR 任務經典機器學習方法:https://github.com/bhimmetoglu/talks-and-lectures/tree/master/MachineLearning/HAR

梯度提升樹:https://rpubs.com/burakh/har_xgb

結語

在本文中,我們試驗了使用 CNN 和 LSTM 進行時序資料的分類,這兩種方法在效能上都有十分優秀的表現,並且最重要的是它們在訓練中會一層層學習獨特的特徵,它們不需要成本昂貴的特徵工程。

本文所使用的序列還是比較小的,只有 128 步。可能會有讀者懷疑如果序列變得更長(甚至大於 1000),是不是訓練就會變得十分困難。其實我們可以結合 LSTM 和 CNN 在這種長序列任務中表現得更好。總的來說,深度學習方法相對於傳統方法有非常明顯的優勢。

相關文章