使用一维卷积完成时序数据的分类

427 阅读5分钟

前言

本文主要使用一维的卷积神经网络完成时序数据的分类任务,使用的数据主要来自于 UCR/UEA archive 的 FordA 数据集。

数据准备

下面的代码主要是列出系统上的物理 GPU 设备,然后为每个 GPU 设备设置显存增长方式,保证按需使用 GPU ,而不是直接占满 GPU ,这样能避免不必要的显存浪费,本次任务使用大约在 3G 左右。

gpus = tf.config.experimental.list_physical_devices(device_type='GPU')
for gpu in gpus:
    tf.config.experimental.set_memory_growth(gpu, True)

下面代码主要是为了完成数据的加载和预处理工作,以便准备好任务所需的时间序列数据,后续可以用于模型的训练和测试。具体如下:

  1. reader 函数从 filename 中加载数据,将每一行用 '\t' 分割开,然后将所有行的第一列作为标签 y ,并将类型转为 int ,将所有行的除第一列的所有列作为输入 x ,返回 x 和 y 。
  2. 分别使用reader函数从两个不同的 URL 加载训练集和测试集数据,并将训练集和测试集的特征和标签分别赋给x_trainy_train以及x_testy_testFordA 来自于在标准操作环境下,汽车引擎所发出的声音。通过汽车引擎声音来判断引擎是否存在问题,也就是一个二分类问题。第一列是类别, -1 表示有问题,1 表示没问题,后面的所有列表示的是输入的 500 维特征。训练集 3601 个, 测试集 1320 个。
  3. 通过使用reshape方法,将训练集和测试集的特征数据从二维数组重塑为三维数组,因为要使用 Conv1D 深度学习模型,所以必须得有的输入要求。具体来说,这将数据的形状从(样本数, 特征数)更改为(样本数, 特征数, 1),其中1表示数据的通道数, 处理时间任务通常需要将数据变换成这种形式。
  4. num_classes 用于分类任务的输出层设置。这里就是一个二分类也就是 2 。
  5. 通过使用np.random.permutation和索引idx,对训练集特征数据和标签数据进行同样的随机洗牌,以提高训练模型的泛化能力。
  6. 将训练集和测试集的标签中的 -1 替换为 0,便于进行模型的计算。
  7. input_shape:定义了模型输入数据的形状。
def reader(filename):
    data = np.loadtxt(filename, delimiter='\t')
    y = data[:, 0]
    x = data[:, 1:]
    return x, y.astype(int)
x_train, y_train = reader("https://raw.githubusercontent.com/hfawaz/cd-diagram/master/FordA/FordA_TRAIN.tsv")
x_test, y_test = reader("https://raw.githubusercontent.com/hfawaz/cd-diagram/master/FordA/FordA_TEST.tsv")
x_train = x_train.reshape((x_train.shape[0], x_train.shape[1], 1))
x_test = x_test.reshape((x_test.shape[0], x_test.shape[1], 1))
num_classes = len(np.unique(y_train))
idx = np.random.permutation(len(x_train))
x_train, y_train = x_train[idx], y_train[idx]
y_train[y_train == -1] = 0
y_test[y_test == -1] = 0
input_shape = x_train.shape[1:]

模型搭建

这里主要是进行模型结构的搭建,使用 Keras 建立一个卷积神经网络,该模型通常用于处理一维数据的分类问题。而我们这里的时间序列数据就是一维数据,以下是代码的主要含义:

  1. Input 层定义了模型的输入层。input_shape 已经提前计算出来了,就是 [500, 1]

  2. 接下来是三个完全一样的卷积层,每个卷积层都有 Conv1DBatchNormalizationReLU

    • 每个卷积层输出 128 个特征图。
    • 每个卷积层之后都跟着 BatchNormalization 层,用于 batch 标准化,这有助于稳定训练过程,并加速模型的收敛。
    • 最后是 ReLU 激活函数,用于引入非线性特征变化。
  3. GlobalAveragePooling1D 用于将卷积层的输出特征图转换成全局平均池化的形式,减少维度并保留重要的特征。在时间序列问题中,这有助于降低序列长度,以便最后的全连接层可以更好地进行分类。

  4. Dense 是全连接层,也是输出层,使用 softmax 激活函数,以便进行多类别分类。

model = keras.Sequential([
    keras.layers.Input(shape=input_shape),
    keras.layers.Conv1D(128, 3, padding='same'),
    keras.layers.BatchNormalization(),
    keras.layers.ReLU(),
    keras.layers.Conv1D(128, 3, padding='same'),
    keras.layers.BatchNormalization(),
    keras.layers.ReLU(),
    keras.layers.Conv1D(128, 3, padding='same'),
    keras.layers.BatchNormalization(),
    keras.layers.ReLU(),
    keras.layers.GlobalAveragePooling1D(),
    keras.layers.Dense(num_classes, activation='softmax')
])

模型编译和训练

这里主要是完成模型的编译和训练,同时使用了一些回调函数来监视和控制训练过程。具体如下:

  1. callbacks列表定义了一组回调函数,它们在训练期间根据一些条件触发特定的操作。

    • ModelCheckpoint:用于保存模型的回调函数,可以保存在验证集上 val_loss 性能最好的模型。
    • ReduceLROnPlateau:学习率衰减回调函数。当验证集上的 val_loss 在经过 patience 个 epoch 后仍没有改进,该回调函数将以 factor 倍率减小学习率以帮助模型更好地收敛。
    • EarlyStopping:早停止回调函数。当验证集上的 val_loss 在 patience 个 epoch 后仍没有改进,可以终止训练过程,以防止过拟合。
  2. 编译模型:用于指定模型的优化器、损失函数和评估指标。

    • optimizer='adam':使用 Adam 优化器进行模型参数更新。
    • loss=keras.losses.SparseCategoricalCrossentropy():使用稀疏分类交叉熵作为损失函数,适用于多类别分类任务。
    • metrics=[keras.metrics.SparseCategoricalAccuracy()]:使用稀疏分类准确率作为模型的性能指标。
  3. 模型训练 :训练过程中 batch_size 为 128 ,将 20% 的数据作为验证集,最多训练 500 轮。

callbacks = [
    keras.callbacks.ModelCheckpoint('best_model.h5', save_best_only=True, monitor="val_loss"),
    keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=20, min_lr=0.0001),
    keras.callbacks.EarlyStopping(monitor='val_loss', patience=50, verbose=1)
]
model.compile(optimizer='adam', loss=keras.losses.SparseCategoricalCrossentropy(),
              metrics=[keras.metrics.SparseCategoricalAccuracy()])
history = model.fit(x_train, y_train, batch_size=128, epochs=500, callbacks=callbacks, validation_split=0.2, verbose=1)

结果打印,可以看出来随着训练的进行验证集的准确率子不断提升,损失在不断降低,同时学习率也在不断降低,最后在 352 轮 epoch 后停止训练:

Epoch 1/500
23/23 [==============================] - 2s 16ms/step - loss: 0.5937 - sparse_categorical_accuracy: 0.6622 - val_loss: 0.7124 - val_sparse_categorical_accuracy: 0.4688 - lr: 0.0010
Epoch 2/500
23/23 [==============================] - 0s 9ms/step - loss: 0.4759 - sparse_categorical_accuracy: 0.7663 - val_loss: 0.7992 - val_sparse_categorical_accuracy: 0.4688 - lr: 0.0010
...
Epoch 162/500
23/23 [==============================] - 0s 9ms/step - loss: 0.0961 - sparse_categorical_accuracy: 0.9663 - val_loss: 0.2214 - val_sparse_categorical_accuracy: 0.9085 - lr: 5.0000e-04
Epoch 352/500
23/23 [==============================] - 0s 9ms/step - loss: 0.0421 - sparse_categorical_accuracy: 0.9892 - val_loss: 0.1041 - val_sparse_categorical_accuracy: 0.9639 - lr: 1.0000e-04
Epoch 352: early stopping

模型评估

这里用于加载之前训练好的模型,对测试数据进行评估,并绘制训练过程中的准确率变化趋势图。

model = keras.models.load_model('best_model.h5')
test_loss, test_acc = model.evaluate(x_test, y_test)
print("测试集准确率", test_acc)
print("测试集损失值", test_loss)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.figure()
plt.plot(history.history["sparse_categorical_accuracy"])
plt.plot(history.history["val_sparse_categorical_accuracy"])
plt.title("准确率变化趋势")
plt.ylabel("准确率", fontsize='large')
plt.xlabel('轮数', fontsize='large')
plt.legend(['训练过程', '验证过程'], loc='best')
plt.show()
plt.close()

结果打印,可以从图中看出,训练过程是符合预期的,没有发生过拟合现象:

测试集准确率 0.9681817889213562
测试集损失值 0.09469714760780334

Figure_1.png