Caso práctico de TensorFlow: predicción de temperatura de red neuronal híbrida CNN-LSTM (con código Python completo)

Hola a todos, hoy compartiré con ustedes cómo usar Tensorflow para construir un modelo de red neuronal híbrida que combina la red neuronal convolucional CNN y la red neuronal recurrente LSTM para completar la predicción de series temporales de funciones múltiples.

La estructura principal del modelo de predicción en este artículo está compuesta por la red neuronal CNN y LSTM. Los datos característicos de la temperatura del aire dependen espacialmente. Este documento opta por extraer la relación espacial entre las características mediante el uso de una red neuronal convolucional CNN en el front-end del modelo . Al mismo tiempo, los datos de temperatura tienen una dependencia temporal obvia, por lo que el modelo de memoria a corto y largo plazo LSTM se agrega después de la red neuronal convolucional para el procesamiento de series temporales .

1. Obtenga el conjunto de datos

El conjunto de datos y el código completo se proporcionan al final del artículo, si desea recordar marcar y dar me gusta.

Este artículo utiliza GPU para acelerar el cálculo. Los amigos que no tengan una GPU pueden eliminar el código que llama a la GPU a continuación.

Las últimas 5 características en el conjunto de datos son datos de temperatura, las tres primeras son tiempo y la columna 'real' se selecciona como etiqueta. La tarea requiere que, de acuerdo con los datos de temperatura de 10 días consecutivos, prediga el valor de temperatura real después de 5 días

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import pandas as pd

# 调用GPU加速
gpus = tf.config.experimental.list_physical_devices(device_type='GPU')
for gpu in gpus:
    tf.config.experimental.set_memory_growth(gpu, True)

# --------------------------------------- #
#(1)获取数据集
# --------------------------------------- #
filepath = 'temps.csv'  # 数据集位置
data = pd.read_csv(filepath)
print(data.head())  # 查看前五行数据

La información del conjunto de datos es la siguiente:

2. Datos de tiempo de procesamiento

Como se muestra en la figura anterior, las primeras tres columnas de la lista son información de tiempo. Debe combinar la información de año, mes y día para convertir del tipo de cadena al tipo de fecha y hora. Al seleccionar funciones, la función de tiempo no se usa y se procesa primero.

import datetime  # 将时间信息组合成datetime类型的数据

# 获取数据中的年月日信息
years = data['year']
months = data['month']
days = data['day']

dates = []  # 存放组合后的时间信息

# 遍历一一对应的年月日信息
for year, month, day in zip(years, months, days):
    # 年月日之间使用字符串拼接
    date = str(year) + '-' + str(month) + '-' + str(day)
    # 将每个时间(字符串类型)保存
    dates.append(date)

# 字符串类型的时间转为datetime类型的时间
times = []

# 遍历所有的字符串类型的时间
for date in dates:
    # 转为datetime类型
    time = datetime.datetime.strptime(date, '%Y-%m-%d')
    # 逐一保存转变类型后的时间数据
    times.append(time)

# 查看转换后的时间数据
print(times[:5])

Las características temporales procesadas son las siguientes

3. Visualización de datos de características

Dibuje la curva de distribución de cada característica y tenga una comprensión intuitiva de los datos

import matplotlib.pyplot as plt
# 指定绘图风格
plt.style.use('fivethirtyeight')
# 设置画布,2行2列的画图窗口,第一行画ax1和ax2,第二行画ax3和ax4
fig, ((ax1,ax2), (ax3,ax4)) = plt.subplots(2, 2, figsize=(20, 10))
 
# ==1== actual特征列
ax1.plot(times, data['actual'])
# 设置x轴y轴标签和title标题
ax1.set_xlabel(''); ax1.set_ylabel('Temperature'); ax1.set_title('actual temp')
# ==2== 前一天的温度
ax2.plot(times, data['temp_1'])
# 设置x轴y轴标签和title标题
ax2.set_xlabel(''); ax2.set_ylabel('Temperature'); ax2.set_title('temp_1')
# ==3== 前2天的温度
ax3.plot(times, data['temp_2'])
# 设置x轴y轴标签和title标题
ax3.set_xlabel('Date'); ax3.set_ylabel('Temperature'); ax3.set_title('temp_2')
# ==4== friend
ax4.plot(times, data['friend'])
# 设置x轴y轴标签和title标题
ax4.set_xlabel('Date'); ax4.set_ylabel('Temperature'); ax4.set_title('friend')
# 轻量化布局调整绘图
plt.tight_layout(pad=2)
plt.show()

Simplemente trace la distribución de las cuatro características a lo largo del tiempo

4. Preprocesamiento de datos

En primer lugar, hay una columna en los datos que son datos clasificados, el día de la semana, estos datos deben codificarse en caliente , y se agrega una columna de características a cada clasificación. Si esta fila de datos es lunes, entonces el valor de la columna del lunes correspondiente es 1, el valor de otras columnas es 0.

Luego elija el valor de la etiqueta para predecir la temperatura en 5 días. Los datos de la etiqueta toman la columna 'etiqueta' en los datos de características y mueven esta columna colectivamente hacia arriba en 5 filas , luego, en este momento, las últimas 5 filas en los datos de la etiqueta tendrán un valor de vacante nan . Los datos de características de las últimas 5 líneas no tienen un valor de etiqueta correspondiente, por lo que las últimas 5 líneas deben eliminarse de los datos de características y de las etiquetas.

El siguiente paso es estandarizar todos los datos de temperatura y no estandarizar los datos semanales codificados de onehot .

# 选择特征, 共6列特征
feats = data.iloc[:,3:]
# 对离散的星期几的数据进行onehot编码
feats = pd.get_dummies(feats)
# 特征列增加到12项
print(feats.shape)  # (348, 12)

# 选择标签数据,一组时间序列预测5天后的真实气温
pre_days = 5
# 选择特征数据中的真实气温'actual'具体向上移动5天的气温信息
targets = feats['actual'].shift(-pre_days)
# 查看标签信息
print(targets.shape)  #(348,)

# 由于特征值最后5行对应的标签是空值nan,将最后5行特征及标签删除
feats = feats[:-pre_days]
targets = targets[:-pre_days]
# 查看数据信息
print('feats.shape:', feats.shape, 'targets.shape:', targets.shape)  # (343, 12) (343,)


# 特征数据标准化处理
from sklearn.preprocessing import StandardScaler  
# 接收标准化方法
scaler = StandardScaler()
# 对特征数据中所有的数值类型的数据进行标准化
feats.iloc[:,:5] = scaler.fit_transform(feats.iloc[:,:5])
# 查看标准化后的信息
print(feats)

Los datos preprocesados ​​son los siguientes:

5. Ventana deslizante de series de tiempo

La cola deque se usa aquí , primero en entrar, primero en salir . La longitud máxima de la cola especificada es 10, es decir, la longitud de la ventana de la serie temporal es 10 y la temperatura después de 5 días se predice en función de los datos característicos de 10 días. Si la longitud de la cola supera los 10, la cola eliminará automáticamente el elemento de la cabeza y agregará el nuevo elemento al final de la cola para formar una nueva secuencia .

Para los datos de la etiqueta , por ejemplo, la función de rango (0,10) días predice la temperatura en el día 15, y el valor de la etiqueta se ha movido colectivamente 5 filas antes, luego el valor de la etiqueta de grado 15 y temperatura corresponde al índice 9 , es decir, [max_series_days-1]

import numpy as np
from collections import deque  # 队列,可在两端增删元素

# 将特征数据从df类型转为numpy类型
feats = np.array(feats)

# 定义时间序列窗口是连续10天的特征数据
max_series_days = 10  
# 创建一个队列,队列的最大长度固定为10
deq = deque(maxlen=max_series_days)  # 如果长度超出了10,先从队列头部开始删除

# 创建一个列表,保存处理后的特征序列
x = []
# 遍历每一行数据,包含12项特征
for i in feats:
    # 将每一行数据存入队列中, numpy类型转为list类型
    deq.append(list(i))
    # 如果队列长度等于指定的序列长度,就保存这个序列
    # 如果队列长度大于序列长度,队列会自动删除头端元素,在尾端追加新元素
    if len(deq) == max_series_days:
        # 保存每一组时间序列, 队列类型转为list类型
        x.append(list(deq))

# 保存与特征对应的标签值
y = targets[max_series_days-1:].values

# 保证序列长度和标签长度相同
print(len(x))   # 334
print(len(y))  # 334

# 将list类型转为numpy类型
x, y = np.array(x), np.array(y)

La imagen izquierda x son las 10 filas de datos de características contenidas en una ventana deslizante de serie temporal , y la etiqueta de imagen derecha y es un valor de etiqueta de temperatura correspondiente a cada ventana deslizante de serie temporal .

6. Divide el conjunto de datos

Los primeros 80 datos se toman de las secuencias divididas para entrenamiento, y el 20% restante se utilizan para validación y prueba respectivamente. Shuffle() los datos de entrenamiento al azar para evitar el azar. Construya un iterador iter() , combínelo con la función next() para sacar un lote del conjunto de entrenamiento y vea la información del conjunto de datos.

total_num = len(x)  # 一共有多少组序列
train_num = int(total_num*0.8)  # 前80%的数据用来训练
val_num = int(total_num*0.9)  # 前80%-90%的数据用来训练验证
# 剩余数据用来测试

x_train, y_train = x[:train_num], y[:train_num]  # 训练集
x_val, y_val = x[train_num: val_num], y[train_num: val_num]  # 验证集
x_test, y_test = x[val_num:], y[val_num:]  # 测试集

# 构造数据集
batch_size = 128  # 每次迭代处理128个序列
# 训练集
train_ds = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_ds = train_ds.batch(batch_size).shuffle(10000)
# 验证集
val_ds = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_ds = val_ds.batch(batch_size)
# 测试集
test_ds = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_ds = test_ds.batch(batch_size)

# 查看数据集信息
sample = next(iter(train_ds))  # 取出一个batch的数据
print('x_train.shape:', sample[0].shape)  # (128, 10, 12)
print('y_train.shape:', sample[1].shape)  # (128,)

7. Construcción de redes

La forma de la capa de entrada es [None, 10, 12] , donde None significa que no es necesario escribir Batch_Size, 10 significa el tamaño de la ventana de la serie temporal y 12 significa la cantidad de características en los datos.

Dado que aquí uso una convolución bidimensional Conv2D , necesito agregar la dimensión del canal al conjunto de datos de entrada , y la forma se convierte en [Ninguno, 10, 12, 1] , similar al procesamiento de imágenes, usando convolución 3 * 3 con un zancada de 1, características de extracción

También es posible reducir la muestra utilizando capas de agrupación y, por supuesto , capas convolucionales con paso 2 . Al reducir la resolución, el tamaño del núcleo de agrupación que elijo es [1,2] , es decir, solo muestra hacia arriba y hacia abajo en la dimensión de la característica, la ventana de secuencia permanece sin cambios y los problemas específicos del método de reducción de resolución se analizan en detalle.

Antes de ingresar los datos de características de CNN a LSTM, es necesario ajustar la cantidad de canales , fusionar la información del canal, reducir la cantidad de canales a 1 y luego exprimir la dimensión del canal , y la forma cambia de cuatro dimensiones a tridimensional, [Ninguno,10,6,1] se convierte en [Ninguno,10,6]

El siguiente paso es procesar la información de la serie temporal de los datos a través de LSTM y, finalmente, generar el resultado de la predicción a través de una capa completamente conectada. El número de neuronas en la capa completamente conectada debe ser el mismo que el número de valores de etiqueta predichos. Aquí, solo se predice un punto de tiempo determinado en el futuro, y el número de neuronas es 1.

# 输入层要和x_train的shape一致,但注意不要batch维度
input_shape = sample[0].shape[1:]  # [10,12]

# 构造输入层
inputs = keras.Input(shape=(input_shape))  # [None, 10, 12]

# 调整维度 [None,10,12]==>[None,10,12,1]
x = layers.Reshape(target_shape=(inputs.shape[1], inputs.shape[2], 1))(inputs)

# 卷积+BN+Relu  [None,10,12,1]==>[None,10,12,8]
x = layers.Conv2D(8, kernel_size=(3,3), strides=1, padding='same', use_bias=False,
                  kernel_regularizer=keras.regularizers.l2(0.01))(x)

x = layers.BatchNormalization()(x)  # 批标准化
x = layers.Activation('relu')(x)  # relu激活函数

# 池化下采样 [None,10,12,8]==>[None,10,6,8]
x = layers.MaxPool2D(pool_size=(1,2))(x)

# 1*1卷积调整通道数 [None,10,6,8]==>[None,10,6,1]
x = layers.Conv2D(1, kernel_size=(3,3), strides=1, padding='same', use_bias=False,
                  kernel_regularizer=keras.regularizers.l2(0.01))(x)

# 把最后一个维度挤压掉 [None,10,6,1]==>[None,10,6]
x = tf.squeeze(x, axis=-1)

# [None,10,6] ==> [None,10,16]
# 第一个LSTM层, 如果下一层还是LSTM层就需要return_sequences=True, 否则就是False
x = layers.LSTM(16, activation='relu', kernel_regularizer=keras.regularizers.l2(0.01))(x)
x = layers.Dropout(0.2)(x)  # 随机杀死神经元防止过拟合

# 输出层 [None,16]==>[None,1]
outputs = layers.Dense(1)(x)

# 构建模型
model = keras.Model(inputs, outputs)

# 查看模型架构
model.summary()

La arquitectura de la red es la siguiente:

Model: "model_12"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_13 (InputLayer)        [(None, 10, 12)]          0         
_________________________________________________________________
reshape_12 (Reshape)         (None, 10, 12, 1)         0         
_________________________________________________________________
conv2d_25 (Conv2D)           (None, 10, 12, 8)         72        
_________________________________________________________________
batch_normalization_13 (Batc (None, 10, 12, 8)         32        
_________________________________________________________________
activation_13 (Activation)   (None, 10, 12, 8)         0         
_________________________________________________________________
max_pooling2d_13 (MaxPooling (None, 10, 6, 8)          0         
_________________________________________________________________
conv2d_26 (Conv2D)           (None, 10, 6, 1)          72        
_________________________________________________________________
tf.compat.v1.squeeze_12 (TFO (None, 10, 6)             0         
_________________________________________________________________
lstm_11 (LSTM)               (None, 16)                1472      
_________________________________________________________________
dropout_14 (Dropout)         (None, 16)                0         
_________________________________________________________________
dense_13 (Dense)             (None, 1)                 17        
=================================================================
Total params: 1,665
Trainable params: 1,649
Non-trainable params: 16
_________________________________________________________________

8. Entrenamiento modelo

Use el error absoluto medio entre el valor predicho y el valor verdadero calculado por la regresión como función de pérdida, y use el error cuadrático medio logarítmico entre el valor predicho y el valor verdadero como indicador de monitoreo durante el entrenamiento

# 网络编译
model.compile(optimizer = keras.optimizers.Adam(0.001),  # adam优化器学习率0.001
              loss = tf.keras.losses.MeanAbsoluteError(),  # 标签和预测之间绝对差异的平均值
              metrics = tf.keras.losses.MeanSquaredLogarithmicError())  # 计算标签和预测之间的对数误差均方值。

epochs = 300  # 迭代300次

# 网络训练, history保存训练时的信息
history = model.fit(train_ds, epochs=epochs, validation_data=val_ds)

Dado que el historial registra la información de pérdida y la información de índice contenida en cada iteración durante el entrenamiento de la red, visualícelas

history_dict = history.history  # 获取训练的数据字典
train_loss = history_dict['loss']  # 训练集损失
val_loss = history_dict['val_loss']  # 验证集损失
train_msle = history_dict['mean_squared_logarithmic_error']  # 训练集的百分比误差
val_msle = history_dict['val_mean_squared_logarithmic_error']  # 验证集的百分比误差
 
#(11)绘制训练损失和验证损失
plt.figure()
plt.plot(range(epochs), train_loss, label='train_loss')  # 训练集损失
plt.plot(range(epochs), val_loss, label='val_loss')  # 验证集损失
plt.legend()  # 显示标签
plt.xlabel('epochs')
plt.ylabel('loss')
plt.show()
 
#(12)绘制训练百分比误差和验证百分比误差
plt.figure()
plt.plot(range(epochs), train_msle, label='train_msle')  # 训练集指标
plt.plot(range(epochs), val_msle, label='val_msle')  # 验证集指标
plt.legend()  # 显示标签
plt.xlabel('epochs')
plt.ylabel('msle')
plt.show()

9. Etapa de predicción

Primero, utilice la función de evaluación () para calcular los indicadores de pérdida y monitoreo para todo el conjunto de prueba , y use la función de predicción () para calcular la temperatura del aire pronosticada a través de los valores propios del conjunto de prueba. Luego dibuje una curva que compare el valor predicho y el valor real.

# 对整个测试集评估
model.evaluate(test_ds)

# 预测
y_pred = model.predict(x_test)

# 获取标签值对应的时间
df_time = times[-len(y_test):]

# 绘制对比曲线
fig = plt.figure(figsize=(10,5))  # 画板大小
ax = fig.add_subplot(111)  # 画板上添加一张图
# 绘制真实值曲线
ax.plot(df_time, y_test, 'b-', label='actual')
# 绘制预测值曲线
ax.plot(df_time, y_pred, 'r--', label='predict')
# 设置x轴刻度
ax.set_xticks(df_time[::7])

# 设置xy轴标签和title标题
ax.set_xlabel('Date')
ax.set_ylabel('Temperature'); 
ax.set_title('result')
plt.legend()
plt.show()

La curva pronosticada y la curva real se comparan de la siguiente manera

Código completo y datos

El código completo y los datos se han colocado en segundo plano, solo responda con una palabra clave

Si desea unirse al intercambio técnico, la mejor manera de comentar al agregar es: fuente + dirección de interés, que es conveniente para encontrar amigos de ideas afines

Método ①, Añadir ID de WeChat: dkl88191, Observaciones: de CSDN+
Método de temperatura ②, Número público de búsqueda de WeChat: aprendizaje de Python y extracción de datos, respuesta en segundo plano: temperatura

Supongo que te gusta

Origin blog.csdn.net/m0_59596937/article/details/127216225
Recomendado
Clasificación