在上一篇文章中,咱们已经对心电信号进行了预处理,将含有噪声的信号变得平滑,以便分类。本篇文章咱们将正式开始利用深度学习对心电信号进行分类识别。python
不管是传统机器学习,仍是深度学习,分类的依据都是不一样类别的数据中包含的不一样特征。要进行分类识别就须要对数据的特征进行提取,可是两者的提取方式并不相同。对于传统的机器学习而言,数据的特征须要设计者或专业人员针对其特性进行手动提取,而深度学习则能够自动提取每类数据中的不一样特征。对于卷积神经网络CNN而言,可以自动提取特征的关键在于卷积操做。通过卷积操做提取的特征每每会有冗余,而且屡次卷积会使神经网络的参数过多不便于训练,因此CNN每每会在卷积层的后面跟上一个池化层。通过屡次的卷积和池化后,较低层次的特征就会逐步构成高层次的特征,最后神经网络根据提取出的高层次特征进行分类。算法
另外须要指出的是,为何在心电信号分类中可使用CNN呢。这是由于CNN具备的卷积操做具备局部链接和权值共享的特征。编程
而心电信号虽然是一维的,可是其中的特征也知足局部链接和权值共享的条件,所以咱们能够采用卷积神经网络对其分类。数组
巧妇难为无米之炊,虽然咱们已经有了预处理过的心电数据,可是这样的数据是没法拿来直接进行分类学习的。因此咱们要先构建符合深度学习模型使用的数据集。转换的过程是首先从一条心电信号中切分出符合要求的心拍做为样本,而后将python list转为numpy array,再通过乱序和切分,最终构成可供深度学习使用的数据集。这里咱们使用tf.keras提供的接口,能够直接使用numpy数组类型,而不用再转成TensorFlow的DataSet对象,对于训练过程而言也更加简单。网络
心拍的切分须要找到QRS波尖峰所在的位置。因为咱们只训练网络模型,咱们这里直接使用MIT-BIH数据集提供的人工标注,并在尖峰处向前取99个信号点、向后取200个信号点,构成一个完整的心拍。若是须要对真实测量的信号进行识别分类,还要设计心拍的检测算法,后续我也可能会继续作。数据结构
数据集根据用途分为训练集、验证集和测试集。训练集用于训练参数模型,验证集用于模型训练中准确率和偏差(损失函数)的检验,测试集用于训练完成后对训练效果的最终检验。能够类比学习、测验和考试。这三者的数据结构都一致,只是包含的数据内容不一样,每一个训练集都包含数据和标签两部份内容。数据是预处理后切分出的若干心拍的列表,标签是每一个心拍样本对应的心电类型。app
首先将上一篇的预处理步骤封装成一个函数:dom
# 小波去噪预处理 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
而后将读取数据和标注、心拍切分封装成一个函数:机器学习
# 读取心电数据和对应标签,并对数据进行小波去噪 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中。ide
下面将全部心拍信号(由于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%左右。