時序資料經常出現在很多領域中,如金融、訊號處理、語音識別和醫藥。傳統的時序問題通常首先需要人力進行特徵工程,才能將預處理的資料輸入到機器學習演算法中。並且這種特徵工程通常需要一些特定領域內的專業知識,因此也就更進一步加大了預處理成本。例如訊號處理(即 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 架構。
上圖描述的卷積層可用以下程式碼實現:
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,並用於以下兩種案例:
- 計算 softmax 交叉熵函式,該損失函式在多類別問題中是標準的損失度量。
- 在最大化概率和準確度的情況下預測類別標籤。
下面是上述過程的實現:
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 的增加而顯示的變化:
長短期記憶網路(LSTM)
LSTM 在處理文字資料上十分流行,它在情感分析、機器翻譯、和文字生成等方面取得了十分顯著的成果。因為本問題涉及相似分類的序列,所以 LSTM 是比較優秀的方法。
下面是能用於該問題的神經網路架構:
為了將資料饋送到網路中,我們需要將陣列分割為 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 在這種長序列任務中表現得更好。總的來說,深度學習方法相對於傳統方法有非常明顯的優勢。