突破 LSTM,CNN 和 LSTM 時間序列預測 !!

在很多的時間序列預測任務中,利用卷積神經網絡(CNN)和長短期記憶網絡(LSTM)的混合模型是目前常見的深度學習解決方案之一。

CNN和LSTM各自有不同的特長,CNN擅長局部模式的捕捉,LSTM擅長捕捉序列的長依賴關係。通過混合這兩種網絡,可以非常好地學習時間序列數據中的複雜模式。

核心原理

  • CNN 部分:CNN 的優勢在於能夠從輸入數據中提取局部特徵。對於時間序列預測問題,時間序列可以看作一維數據序列。CNN 可以通過一維卷積操作提取時間序列的局部時間依賴模式,如趨勢、週期性波動等局部特徵。

  • LSTM 部分:LSTM 是一種遞歸神經網絡(RNN)的變體,專門設計用來解決長期依賴問題。通過記憶門控機制(如輸入門、遺忘門、輸出門),LSTM 能夠很好地捕捉序列中長期的時間依賴關係,並對未來的值進行預測。

混合模型首先使用 CNN 提取局部特徵,然後將這些特徵輸入到 LSTM 中,進一步捕捉時間序列的長時間依賴模式,從而提高預測精度。

模型架構

輸入層:輸入的時間序列數據可以是一個  的矩陣, 其中  表示時間序列的長度, 表示特徵的維度。

卷積層(CNN):通過一維卷積層(1D Convolutional Layer)來提取局部特徵。假設卷積核的大小為 ,則卷積操作可以表示為:

其中, 表示卷積操作, 是卷積核, 是偏置, 是激活函數,通常選擇 ReLU 或者 Sigmoid。卷積層會提取局部時間窗口內的模式,如趨勢或短期波動。

池化層(可選):通過池化層(Pooling Layer)減少特徵的空間尺寸。常用的池化方式包括最大池化(Max Pooling)和平均池化(Average Pooling),以便於減少模型的計算量和防止過擬合。

LSTM 層:將經過 CNN 處理後的特徵序列輸入到 LSTM 層,捕捉長期依賴。LSTM 的輸入是卷積層的輸出特徵序列。LSTM 的計算公式如下:

其中, 是遺忘門, 是輸入門, 是輸出門, 是記憶單元的狀態, 是當前的隱藏狀態, 和  分別是權重矩陣和偏置項。LSTM 層能夠有效捕捉輸入序列中長期的依賴關係。

全連接層:LSTM 輸出的隱藏狀態  被傳遞給全連接層,生成最終的預測值。全連接層的輸出公式為:

其中, 和  分別是全連接層的權重和偏置項, 是預測的結果。

損失函數:常見的時間序列預測任務的損失函數為均方誤差(MSE):

其中, 是樣本數, 是實際值, 是預測值。

公式解釋

  • 卷積操作:在時間序列中,1D 卷積可以理解為將卷積核應用到連續的時間點上,提取時間窗口內的特徵。公式中的  表示當前時間點  及其前後  個時間點的數據。通過這種方式,CNN 能夠在不喪失時間順序的情況下提取局部模式。

  • LSTM 結構:LSTM 是通過門控機制來控制信息的流動。遺忘門  控制哪些歷史信息需要保留,輸入門 控制哪些新信息需要加入,輸出門  決定了當前時刻的隱藏狀態輸出。記憶單元  記錄著每個時刻的重要信息,從而保留了時間序列的長期依賴性。

優勢

  • 局部和全局模式捕捉:CNN 擅長在短期窗口內捕捉局部時間模式(例如季節性波動),而 LSTM 擅長捕捉長期依賴。結合二者可以更好地應對複雜的時間序列預測任務。

  • 降維與特徵提取:CNN 提取特徵的同時減少數據的維度,減少了輸入 LSTM 的信息量,避免了 LSTM 因輸入序列過長導致的效率問題。

總的來說,CNN 和 LSTM 的混合模型結合了卷積網絡和遞歸網絡的優勢,可以在時間序列預測任務中更好地捕捉短期與長期的特徵,從而提升預測精度。

一個完整案例

CNN-LSTM 混合模型在時間序列預測中的應用。

時間序列預測是機器學習中的一大熱門課題,特別是在金融、氣象、能源消耗預測等領域。CNN(卷積神經網絡)和 LSTM(長短期記憶網絡)分別在特徵提取和捕獲時間依賴性方面各有獨特優勢,因此將這兩者結合起來構建混合模型,能夠有效提升預測性能。

本案例將介紹如何基於 PyTorch 實現一個 CNN-LSTM 混合模型用於時間序列預測,並展示相關的可視化圖形,幫助大家直觀理解模型表現。

CNN-LSTM 的混合模型結合了 CNN 提取局部特徵的能力和 LSTM 學習時序依賴的能力。該模型首先通過 CNN 層對輸入的時間序列進行卷積,提取高層次特徵,然後將這些特徵輸入到 LSTM 中,用於捕獲時間上的長期依賴,最後通過全連接層輸出預測結果。

模型的結構如下圖所示:

輸入數據 -> CNN層(卷積+池化) -> LSTM層 -> 全連接層 -> 輸出預測

這種結構能夠有效提取數據的局部特徵(CNN)和時間依賴關係(LSTM),在時間序列預測任務中具有優勢。

數據準備

我們使用虛擬數據集來模擬時間序列預測任務。假設數據是某種週期性波動和隨機噪聲疊加的時間序列。

import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.preprocessing import MinMaxScaler
from torch.utils.data import DataLoader, TensorDataset

# 生成虛擬時間序列數據
np.random.seed(42)
time = np.arange(0, 1000, 0.1)
data = np.sin(0.05 * time) + np.sin(0.01 * time) + 0.5 * np.random.randn(len(time))

# 標準化數據
scaler = MinMaxScaler(feature_range=(-1, 1))
data = scaler.fit_transform(data.reshape(-1, 1)).reshape(-1)

# 準備時間序列數據集 (輸入序列, 預測值)
def create_sequences(data, seq_length):
    xs = []
    ys = []
    for i in range(len(data)-seq_length-1):
        x = data[i:(i+seq_length)]
        y = data[i+seq_length]
        xs.append(x)
        ys.append(y)
    return np.array(xs), np.array(ys)

seq_length = 50  # 時間序列長度
X, y = create_sequences(data, seq_length)

# 轉換為 PyTorch 張量
X = torch.from_numpy(X).float()
y = torch.from_numpy(y).float()

# 劃分訓練集和測試集
train_size = int(len(X) * 0.8)
train_X, test_X = X[:train_size], X[train_size:]
train_y, test_y = y[:train_size], y[train_size:]

train_dataset = TensorDataset(train_X, train_y)
test_dataset = TensorDataset(test_X, test_y)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

構建 CNN-LSTM 模型

接下來,定義 CNN-LSTM 模型。我們將使用 1D 卷積層提取時間序列的局部特徵,接著將這些特徵輸入到 LSTM 層進行時序預測。

class CNN_LSTM_Model(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers):
        super(CNN_LSTM_Model, self).__init__()
        
        # 1D卷積層
        self.cnn = nn.Sequential(
            nn.Conv1d(in_channels=1, out_channels=32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=2)
        )
        
        # LSTM層
        self.lstm = nn.LSTM(input_size=32, hidden_size=hidden_size, num_layers=num_layers, batch_first=True)
        
        # 全連接層
        self.fc = nn.Linear(hidden_size, 1)
        
    def forward(self, x):
        x = x.unsqueeze(1)  # 增加一個通道維度, 因為1D卷積需要 (batch, channel, seq_len)
        x = self.cnn(x)
        x = x.permute(0, 2, 1)  # 調整維度 (batch, seq_len, features)
        lstm_out, _ = self.lstm(x)
        out = self.fc(lstm_out[:, -1, :])  # 取最後時間步的輸出
        return out

模型訓練

我們使用均方誤差(MSE)作為損失函數,Adam 作為優化器。訓練過程將會迭代多個 epoch,並在訓練和測試集上記錄損失,以便後續可視化分析。

# 超參數
input_size = 32
hidden_size = 64
num_layers = 2
num_epochs = 100
learning_rate = 0.001

# 模型實例化
model = CNN_LSTM_Model(input_size, hidden_size, num_layers)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# 訓練模型
train_losses = []
test_losses = []

for epoch in range(num_epochs):
    model.train()
    train_loss = 0
    for batch_X, batch_y in train_loader:
        optimizer.zero_grad()
        outputs = model(batch_X)
        loss = criterion(outputs.squeeze(), batch_y)
        loss.backward()
        optimizer.step()
        train_loss += loss.item()
    
    train_losses.append(train_loss / len(train_loader))
    
    # 測試模型
    model.eval()
    test_loss = 0
    with torch.no_grad():
        for batch_X, batch_y in test_loader:
            outputs = model(batch_X)
            loss = criterion(outputs.squeeze(), batch_y)
            test_loss += loss.item()
    
    test_losses.append(test_loss / len(test_loader))
    
    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Train Loss: {train_loss/len(train_loader):.4f}, Test Loss: {test_loss/len(test_loader):.4f}')

預測和可視化

在模型訓練完成後,我們在測試集上進行預測,並繪製預測結果和訓練過程中的損失變化圖。

# 繪製損失變化曲線
plt.figure(figsize=(10, 6))
plt.plot(train_losses, label='Train Loss')
plt.plot(test_losses, label='Test Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Loss Curve')
plt.legend()
plt.grid(True)
plt.show()

# 測試集上的預測曲線
model.eval()
predictions = []
with torch.no_grad():
    for batch_X, _ in test_loader:
        outputs = model(batch_X)
        predictions.append(outputs.detach().numpy())

predictions = np.concatenate(predictions).squeeze()
predictions = scaler.inverse_transform(predictions.reshape(-1, 1)).reshape(-1)
true_values = scaler.inverse_transform(test_y.numpy().reshape(-1, 1)).reshape(-1)

# 繪製預測值與真實值對比
plt.figure(figsize=(10, 6))
plt.plot(true_values, label='True Values', color='b')
plt.plot(predictions, label='Predictions', color='r')
plt.xlabel('Time Steps')
plt.ylabel('Value')
plt.title('Predictions vs True Values')
plt.legend()
plt.grid(True)
plt.show()

損失曲線

第一張圖展示了訓練過程中模型的損失變化情況,可以觀察到訓練損失和測試損失逐漸下降的趨勢。通過這張圖,可以直觀地看到模型的收斂情況以及是否出現過擬合。

預測曲線

第二張圖展示了模型在測試集上的預測結果與真實值的對比。通過這張圖,我們能夠直觀評估模型在未見過的數據上的表現,觀察預測是否準確擬合了數據的趨勢。

殘差分佈

繪製殘差(預測值與真實值的差異)的分佈圖,可以評估模型在預測時的誤差特徵,是否存在系統性偏差。

# 計算殘差
residuals = true_values - predictions

# 繪製殘差分佈圖
plt.figure(figsize=(10, 6))
plt.hist(residuals, bins=50, color='purple', alpha=0.7)
plt.title('Residuals Distribution')
plt.xlabel('Residual')
plt.ylabel('Frequency')
plt.grid(True)
plt.show()

殘差序列圖

殘差序列圖可以幫助我們查看誤差隨時間的變化是否有規律。

# 繪製殘差隨時間變化圖
plt.figure(figsize=(10, 6))
plt.plot(residuals, label='Residuals', color='orange')
plt.xlabel('Time Steps')
plt.ylabel('Residual')
plt.title('Residuals over Time')
plt.grid(True)
plt.show()

模型優化與調參

網絡架構優化

  • 卷積核尺寸與數量: 可以嘗試不同大小的卷積核(例如 5 或 7),或者增加捲積層數量,提取更深層的局部特徵。

  • LSTM層數與隱藏單元數: 增加或減少 LSTM 的層數和每層的隱藏單元數,尋找最佳的時序依賴模式捕獲能力。

  • 激活函數: 除了 ReLU,還可以嘗試 LeakyReLU 或者 ELU,觀察是否能加速收斂或提高模型效果。

正則化與防止過擬合

  • Dropout: 可以在 LSTM 或 CNN 層之間添加 dropout 層,以防止模型過擬合。

  • 早停機制: 在訓練時引入 early stopping,根據測試集的損失提前終止訓練,避免過度訓練導致的過擬合。

超參數調優

  • 學習率: 使用學習率衰減策略,在訓練過程中逐漸減小學習率,以便在收斂時保持穩定。

  • 批量大小: 調整 batch_size 可以影響模型的收斂速度和性能,嘗試不同大小的 batch 來找到最優組合。