在上一篇文章中,咱們已經對心電信號進行了預處理,將含有噪聲的信號變得平滑,以便分類。本篇文章咱們將正式開始利用深度學習對心電信號進行分類識別。python
卷積神經網絡
不管是傳統機器學習,仍是深度學習,分類的依據都是不一樣類別的數據中包含的不一樣特徵。要進行分類識別就須要對數據的特徵進行提取,可是兩者的提取方式並不相同。對於傳統的機器學習而言,數據的特徵須要設計者或專業人員針對其特性進行手動提取,而深度學習則能夠自動提取每類數據中的不一樣特徵。對於卷積神經網絡CNN而言,可以自動提取特徵的關鍵在於卷積操做。通過卷積操做提取的特徵每每會有冗餘,而且屢次卷積會使神經網絡的參數過多不便於訓練,因此CNN每每會在卷積層的後面跟上一個池化層。通過屢次的卷積和池化後,較低層次的特徵就會逐步構成高層次的特徵,最後神經網絡根據提取出的高層次特徵進行分類。git
另外須要指出的是,爲何在心電信號分類中可使用CNN呢。這是由於CNN具備的卷積操做具備局部鏈接和權值共享的特徵。github
- 局部鏈接:用於區別不一樣種類的圖片所需的特徵只是整張圖片中的某些局部區域,所以在進行卷積操做時使用的卷積核(感覺野)能夠只是幾個不一樣小區域,而沒必要使用整張圖片大小的卷積核(全鏈接)。這樣作不只能夠更好地表達不一樣的特徵,還能起到減小參數的做用。例以下圖,左邊是使用全鏈接的神經網絡,右邊是使用局部鏈接卷積核的網絡。
- 權值共享:對於一類圖片而言,他們擁有類似的特徵,可是每張圖片中特徵的位置可能會有偏移。好比不一樣的人臉照片中眼睛的位置可能會有變化,不多有兩張照片眼睛的位置徹底重合。對一張圖片進行卷積操做時,能夠有多個卷積核來提取不一樣的特徵,但一個卷積核在進行移動的過程當中其權值是保持不變的(固然不一樣卷積核的權值不共享)。這樣既能保證特徵提取不受位置的影響,還能減小參數的數量。
而心電信號雖然是一維的,可是其中的特徵也知足局部鏈接和權值共享的條件,所以咱們能夠採用卷積神經網絡對其分類。算法
構建深度學習的數據集
巧婦難爲無米之炊,雖然咱們已經有了預處理過的心電數據,可是這樣的數據是沒法拿來直接進行分類學習的。因此咱們要先構建符合深度學習模型使用的數據集。轉換的過程是首先從一條心電信號中切分出符合要求的心拍做爲樣本,而後將python list轉爲numpy array,再通過亂序和切分,最終構成可供深度學習使用的數據集。這裏咱們使用tf.keras提供的接口,能夠直接使用numpy數組類型,而不用再轉成TensorFlow的DataSet對象,對於訓練過程而言也更加簡單。編程
心拍的切分須要找到QRS波尖峯所在的位置。因爲咱們只訓練網絡模型,咱們這裏直接使用MIT-BIH數據集提供的人工標註,並在尖峯處向前取99個信號點、向後取200個信號點,構成一個完整的心拍。若是須要對真實測量的信號進行識別分類,還要設計心拍的檢測算法,後續我也可能會繼續作。數組
數據集根據用途分爲訓練集、驗證集和測試集。訓練集用於訓練參數模型,驗證集用於模型訓練中準確率和偏差(損失函數)的檢驗,測試集用於訓練完成後對訓練效果的最終檢驗。能夠類比學習、測驗和考試。這三者的數據結構都一致,只是包含的數據內容不一樣,每一個訓練集都包含數據和標籤兩部份內容。數據是預處理後切分出的若干心拍的列表,標籤是每一個心拍樣本對應的心電類型。網絡
首先將上一篇的預處理步驟封裝成一個函數:數據結構
# 小波去噪預處理 def denoise(data): # 小波變換 coeffs = pywt.wavedec(data=data, wavelet='db5', level=9) cA9, cD9, cD8, cD7, cD6, cD5, cD4, cD3, cD2, cD1 = coeffs # 閾值去噪 threshold = (np.median(np.abs(cD1)) / 0.6745) * (np.sqrt(2 * np.log(len(cD1)))) cD1.fill(0) cD2.fill(0) for i in range(1, len(coeffs) - 2): coeffs[i] = pywt.threshold(coeffs[i], threshold) # 小波反變換,獲取去噪後的信號 rdata = pywt.waverec(coeffs=coeffs, wavelet='db5') return rdata
而後將讀取數據和標註、心拍切分封裝成一個函數:app
# 讀取心電數據和對應標籤,並對數據進行小波去噪 def getDataSet(number, X_data, Y_data): ecgClassSet = ['N', 'A', 'V', 'L', 'R'] # 讀取心電數據記錄 print("正在讀取 " + number + " 號心電數據...") record = wfdb.rdrecord('ecg_data/' + number, channel_names=['MLII']) data = record.p_signal.flatten() # 小波去噪 rdata = denoise(data=data) # 獲取心電數據記錄中R波的位置和對應的標籤 annotation = wfdb.rdann('ecg_data/' + number, 'atr') Rlocation = annotation.sample Rclass = annotation.symbol # 去掉先後的不穩定數據 start = 10 end = 5 i = start j = len(annotation.symbol) - end # 由於只選擇NAVLR五種心電類型,因此要選出該條記錄中所須要的那些帶有特定標籤的數據,捨棄其他標籤的點 # X_data在R波先後截取長度爲300的數據點 # Y_data將NAVLR按順序轉換爲01234 while i < j: try: lable = ecgClassSet.index(Rclass[i]) x_train = rdata[Rlocation[i] - 99:Rlocation[i] + 201] X_data.append(x_train) Y_data.append(lable) i += 1 except ValueError: i += 1 return
須要注意的是,上面的函數並無返回值,這是由於咱們裝載心拍數據和樣本的列表X_data、Y_data包含了全部心電記錄中符合要求的心拍,須要從函數外傳入,並將獲得的數據直接附加在列表末尾。這樣將心電信號的編號、X_data、Y_data一同傳入,就能將所需數據保存在X_data和Y_data中。dom
下面將全部心拍信號(由於102和104沒有MLII導聯故去除)讀取到dataSet和lableSet兩個列表中,通過上面函數後,dataSet和lableSet都是一個(92192)的一維列表。其中dataSet中的每個元素都是一個numpy的數組,數組中是一個元素都是一個心拍的300個信號點,lableSet中的每個元素是dataSet中一個數組對應的標籤值(NAVLR對應01234)。reshape後將dataSet變爲(92192,300)的列表,將lableSet變爲(92192,1)的列表。而後對這兩個列表進行亂序處理,可是要保證兩者之間的對應關係不改變。思路是先將兩個列表進行豎直方向的堆疊,變爲一個列表train_ds,而後對其進行亂序處理,再拆分出亂序後的數據X和標籤Y。
因爲tf.keras能夠將輸入的數據集自動劃分紅訓練集和測試集,因此只須要分出測試集便可。思路是先生成92192(總心拍個數)個數的隨機排列列表,而後截取其中前30%個值做爲索引,而後在數據集和標籤集中取出下標爲這些索引的值,即獲得測試數據集X_test和測試標籤集Y_test。
# 加載數據集並進行預處理 def loadData(): numberSet = ['100', '101', '103', '105', '106', '107', '108', '109', '111', '112', '113', '114', '115', '116', '117', '119', '121', '122', '123', '124', '200', '201', '202', '203', '205', '208', '210', '212', '213', '214', '215', '217', '219', '220', '221', '222', '223', '228', '230', '231', '232', '233', '234'] dataSet = [] lableSet = [] for n in numberSet: getDataSet(n, dataSet, lableSet) # 轉numpy數組,打亂順序 dataSet = np.array(dataSet).reshape(-1, 300) lableSet = np.array(lableSet).reshape(-1, 1) train_ds = np.hstack((dataSet, lableSet)) np.random.shuffle(train_ds) # 數據集及其標籤集 X = train_ds[:, :300].reshape(-1, 300, 1) Y = train_ds[:, 300] # 測試集及其標籤集 shuffle_index = np.random.permutation(len(X)) test_length = int(RATIO * len(shuffle_index)) # RATIO = 0.3 test_index = shuffle_index[:test_length] X_test, Y_test = X[test_index], Y[test_index] return X, Y, X_test, Y_test
通過上面的函數後,X,Y爲整體數據集和標籤集,X_test,Y_test爲測試數據集和標籤集,驗證數據集和測試集使用tf.keras接口自動劃分。這樣用於深度學習的數據集就已經構建好了。
深度學習識別分類
一般來講,深度學習神經網絡的訓練過程編程較爲複雜,可是咱們這裏使用tf.keras高級接口,能夠很方便地進行深度學習網絡模型的構建。
首先咱們構建網絡結構,具體結構以下圖所示:
# 構建CNN模型 def buildModel(): newModel = tf.keras.models.Sequential([ tf.keras.layers.InputLayer(input_shape=(300, 1)), # 第一個卷積層, 4 個 21x1 卷積核 tf.keras.layers.Conv1D(filters=4, kernel_size=21, strides=1, padding='SAME', activation='relu'), # 第一個池化層, 最大池化,4 個 3x1 卷積核, 步長爲 2 tf.keras.layers.MaxPool1D(pool_size=3, strides=2, padding='SAME'), # 第二個卷積層, 16 個 23x1 卷積核 tf.keras.layers.Conv1D(filters=16, kernel_size=23, strides=1, padding='SAME', activation='relu'), # 第二個池化層, 最大池化,4 個 3x1 卷積核, 步長爲 2 tf.keras.layers.MaxPool1D(pool_size=3, strides=2, padding='SAME'), # 第三個卷積層, 32 個 25x1 卷積核 tf.keras.layers.Conv1D(filters=32, kernel_size=25, strides=1, padding='SAME', activation='relu'), # 第三個池化層, 平均池化,4 個 3x1 卷積核, 步長爲 2 tf.keras.layers.AvgPool1D(pool_size=3, strides=2, padding='SAME'), # 第四個卷積層, 64 個 27x1 卷積核 tf.keras.layers.Conv1D(filters=64, kernel_size=27, strides=1, padding='SAME', activation='relu'), # 打平層,方便全鏈接層處理 tf.keras.layers.Flatten(), # 全鏈接層,128 個節點 tf.keras.layers.Dense(128, activation='relu'), # Dropout層,dropout = 0.2 tf.keras.layers.Dropout(rate=0.2), # 全鏈接層,5 個節點 tf.keras.layers.Dense(5, activation='softmax') ]) return newModel
而後使用model.compile()構建;model.fit()訓練30輪,批大小爲128,劃分驗證集的比例爲0.3,設置callback進行訓練記錄的保存;model.save()保存模型;model.predict_classes()預測。完整代碼能夠取本人的GitHub倉庫查看,地址在文章(一)中。
def main(): # X,Y爲全部的數據集和標籤集 # X_test,Y_test爲拆分的測試集和標籤集 X, Y, X_test, Y_test = loadData() if os.path.exists(model_path): # 導入訓練好的模型 model = tf.keras.models.load_model(filepath=model_path) else: # 構建CNN模型 model = buildModel() model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.summary() # 定義TensorBoard對象 tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1) # 訓練與驗證 model.fit(X, Y, epochs=30, batch_size=128, validation_split=RATIO, callbacks=[tensorboard_callback]) model.save(filepath=model_path) # 預測 Y_pred = model.predict_classes(X_test)
對心電信號的深度學習識別分類至此結束,識別率可達99%左右。