GRU 與 LSTM

Photo by Markus Winkler on Unsplash

 

本文重點概要:

  • 文章難度:★★★★★
  • 使用交易面資料進行股票預測
  • 閱讀建議:本文比較不同RNN架構進行時間序列預測,需要對時間序列或是深度學習有基礎瞭解,可以參考【資料科學】LSTM 這篇關於LSTM模型來預測股價。

前言

追逐利益、趨避風險是投資人的目標,預測股價動是達成上述目標的方法之一。過去人們使用ARIMA、GARCH等時間序列,試圖刻畫出未來股價的軌跡。到了今日,隨著深度學習的蓬勃發展,越來越多時間序列相關的模型的出現,似乎能應用於未來股價的預測中。本文即是利用GRU與LSTM兩序列相關模型進行股價預測,使用前5日的開盤、最高、最低、收盤價預測隔日收盤價。

過去【資料科學】LSTM已對LSTM有相當程度的介紹,於此不在多做贅述。本文多加入了同樣是RNN家族的GRU模型,檢驗GRU與LSTM在股價預測上的表現差異。GRU改動了LSTM中記憶單元的遺忘、輸入與輸出門,將其縮編為更新門與重置門,前者類似於LSTM中的遺忘與輸入門,負責決定每次迭代需保留與丟棄的信息,後者則是決定需丟棄過去累積的信息。從三門減少至雙門的情況下,GRU相較於LSTM能達成較快的運算速度,且其表現理論上不亞於LSTM。

編輯環境及模組需求

本文使用Google Colab作為編輯器

# 載入所需套件
import pandas as pd 
import numpy as np
from sklearn.preprocessing import StandardScaler
import plotly.graph_objects as go
import os
import time
import tejapi
import math
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader, TensorDataset

# 登入TEJ API
api_key = 'YOUR_KEY'
tejapi.ApiConfig.api_key = api_key
tejapi.ApiConfig.ignoretz = True

# 載入gpu
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

資料庫使用

公司交易面資料庫: 未調整股價(日),資料代碼為(TWN/APRCD)。

資料導入

使用台積電(2330.tw)未調整開盤、最高、最低與收盤價格,時間區間為2019/01/01到2023/01/01。先進行標準化,再將其依照8:2進行訓練與驗證集切分。標準化能有效減少特徵規模大小不均所造成的偏誤且能加速訓練時間。

# 股價
gte, lte = '2019-01-01', '2023-01-01'
data = tejapi.get('TWN/APRCD',
                   paginate = True,
                   coid = '2330', 
                   mdate = {'gte':gte, 'lte':lte},
                   opts = {
                       'columns':[ 'mdate', 'open_d', 'high_d', 'low_d', 'close_d', 'volume']
                   }
                  )
# 標準化
scaler = StandardScaler()
data = scaler.fit_transform(data)

# 訓練與驗證集
train, test = data[:int(0.8 * len(data)), :4], data[int(0.8 * len(data)):, :4]

建立Pytorch Dataset與DataLoader,可以自動建置Batch以方便後續將資料餵給模型訓練。

def create_dataset(dataset, lookback):
    X, y = [], []
    for i in range(len(dataset)-lookback):
        feature = dataset[i:i+lookback, :]
        target = dataset[i+1:i+lookback+1][-1][-1]
        X.append(feature)
        y.append(target)
    return torch.FloatTensor(X).to(device), torch.FloatTensor(y).view(-1, 1).to(device)

lookback = 5 # 設定前五天股價預測下一日
X_train, y_train = create_dataset(train, lookback = lookback)
X_val, y_val = create_dataset(test, lookback = lookback)
loader = DataLoader(TensorDataset(X_train, y_train), shuffle = False, batch_size = 32)

單層LSTM模型

模型架構為一層LSTM,加上一層Dropout後,再接上一個全連接層。加入Dropout的原因為防止模型產生過擬合問題。

● input_size: 為輸入的特徵數量,使用開盤、最高、最低與收盤價格,故 input_size = 4。
● hidden_size: 為LSTM隱藏層神經元數。
● num_layer: LSTM層數,單層預設為一。
● batch_first: 輸出維度保持(batch_size, sequence_len, hidden_size),其中 sequence_len為5,因為我們採用五天價格預測隔日價格。

# 建立單層LSTM函式
class S_LSTM(nn.Module):
    def __init__(self):
        super().__init__()
        self.lstm1 = nn.LSTM(input_size = 4, hidden_size=64, num_layers=1, batch_first=True)
        self.dropout = nn.Dropout(0.2)
        self.linear = nn.Linear(64, 1)
    def forward(self, x):
        x, _ = self.lstm1(x)
        x = self.dropout(x)
        x = x[:, -1, :]
        x = self.linear(x)
        return x

# 建立訓練流程函式
def trainer(epochs, loader, X_train, y_train, X_val, y_val, model, criterion, optimizer):
  train_loss, test_loss = [],[]
  for epoch in range(epochs):
    model.train()
    for batch, (x, y_true) in enumerate(loader):
      y_pred = model(x)
      loss = criterion(y_pred, y_true)
      loss.backward()
      optimizer.step()
      optimizer.zero_grad()
    model.eval()
    with torch.no_grad():
      y_pred = model(X_train)
      train_rmse = np.sqrt(criterion(y_pred, y_train).item())
      train_loss.append(train_rmse)
      y_pred = model(X_val)
      test_rmse = np.sqrt(criterion(y_pred, y_val).item())
      test_loss.append(test_rmse)
      if (epoch+1) % 100 == 0:
        print('epoch %d train rmse %.4f test rmse %.4f' % (epoch+1, train_rmse, test_rmse))
  return train_loss, test_loss

# 設置模型、損失函數與優化器
model = S_LSTM().to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters())
epochs = 1000

# 開始訓練並且計算訓練所需時間
start = time.time()
slstm_train_loss, slstm_test_loss = trainer(epochs, loader, X_train, y_train, X_val, y_val, model, criterion, optimizer)
end = time.time()
print('single lstm time cost %.4f' %(end-start))
訓練結果
訓練結果

繪製損失曲線

fig = go.Figure()
fig.add_trace(go.Scatter(x=np.arange(epochs), y=slstm_train_loss,
                    mode='lines',
                    name='Train Loss'))
fig.add_trace(go.Scatter(x=np.arange(epochs) , y=slstm_test_loss,
                    mode='lines',
                    name='Validation Loss'))
fig.update_layout(
    title="Loss curve for single lstm",
    xaxis_title="epochs",
    yaxis_title="rmse"
)
fig.show()
單層LSTM的損失曲線
單層LSTM的損失曲線

從損失曲線圖可以發現約莫在第200次epoch時,驗證集的損失已經趨於收斂且坐落於約莫0.07的位置,後續再將股價預測圖繪製檢驗模型的預測能力。

train_plot = np.ones_like(data[:, 3]) * np.nan
test_plot = np.ones_like(data[:, 3]) * np.nan
with torch.no_grad():
  # 預測訓練集資料
  y_pred = model(X_train)
  train_plot[lookback:int(0.8 * len(data))] = y_pred.view(-1).cpu()
  # 預測驗證集資料
  y_pred = model(X_val)
  test_plot[int(0.8 * len(data))+lookback:] = y_pred.view(-1).cpu()

fig = go.Figure()
fig.add_trace(go.Scatter(x=mdate, y=train_plot,
                    mode='lines',
                    name='Train'))
fig.add_trace(go.Scatter(x=mdate , y=test_plot,
                    mode='lines',
                    name='Validation'))
fig.add_trace(go.Scatter(x=mdate , y=data[:, 3],
                    mode='lines',
                    name='True'))
fig.update_layout(
    title="Stock prediction for sngle lstm",
    xaxis_title="dates",
    yaxis_title="standardised stock"
)
fig.show()
單層LSTM股價預測
單層LSTM股價預測

從上圖與損失曲線圖可以發現單層LSTM對於股價的預測能力是相當不錯的。這點十分有趣,因為根據【資料科學】LSTM所述,他們在單層的LSTM表現是較差的,並無法完整捕捉到時間序列資訊。而我們與他們的區別在於他們有多採用每日成交量作為輸入資料的特徵、我們的LSTM層輸出維度是64而他們的是32,Dropout的比率我們是20%而他們的是30%。目前認為最有可能造成差異的原因應該為他們多採用了每日成交量作為輸入特徵

雙層LSTM模型

雖然單層LSTM已經可以達成不錯的效果,但我們不彷多堆疊幾層LSTM去試看看是否能繼續最佳化。多層LSTM的架構為: 一層LSTM + 一層Dropout + 一層LSTM + 一層Dropout + 一層全連接層。其中兩次Dropout的比率都調整為40%,這裡將比率調高的原因是為了避免過擬合問題。

# 建立雙層LSTM函式
class LSTM(nn.Module):
    def __init__(self):
        super().__init__()
        self.lstm1 = nn.LSTM(input_size = 4, hidden_size=64, num_layers=1, batch_first=True)
        self.dropout1 = nn.Dropout(0.4)
        self.lstm2 = nn.LSTM(input_size = 64, hidden_size=32, num_layers=1, batch_first=True)
        self.dropout2 = nn.Dropout(0.4)
        self.linear = nn.Linear(32, 1)
    def forward(self, x):
        x, _ = self.lstm1(x)
        x = self.dropout1(x)
        x, _ = self.lstm2(x)
        x = self.dropout2(x)
        x = x[:, -1, :]
        x = self.linear(x)
        return x
# 建立訓練流程函式
def trainer(epochs, loader, X_train, y_train, X_val, y_val, model, criterion, optimizer):
  train_loss, test_loss = [],[]
  for epoch in range(epochs):
    model.train()
    for batch, (x, y_true) in enumerate(loader):
      y_pred = model(x)
      loss = criterion(y_pred, y_true)
      loss.backward()
      optimizer.step()
      optimizer.zero_grad()
    model.eval()
    with torch.no_grad():
      y_pred = model(X_train)
      train_rmse = np.sqrt(criterion(y_pred, y_train).item())
      train_loss.append(train_rmse)
      y_pred = model(X_val)
      test_rmse = np.sqrt(criterion(y_pred, y_val).item())
      test_loss.append(test_rmse)
      if (epoch+1) % 100 == 0:
        print('epoch %d train rmse %.4f test rmse %.4f' % (epoch+1, train_rmse, test_rmse))
  return train_loss, test_loss
# 設置模型、損失函數與優化器
model = LSTM().to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters())
epochs = 1000
# 開始訓練並且計算訓練所需時間
start = time.time()
lstm_train_loss, lstm_test_loss = trainer(epochs, loader, X_train, y_train, X_val, y_val, model, criterion, optimizer)
end = time.time()
print('stack lstm time cost %.4f' %(end-start))
訓練結果
 訓練結果

繪製損失曲線

雙層LSTM損失曲線
雙層LSTM損失曲線

可以發現隨著模型複雜度提高,收斂的速度下滑到約500個epoch才有收斂到0.1的跡象,且隨後的震盪也相較於單層LSTM為大。後續再將股價預測圖繪製檢驗模型的預測能力,可以發現預測能較不如單層LSTM,但也能抓出漲跌趨勢,繪圖程式碼請見最下方。

雙層LSTM股價預測
 雙層LSTM股價預測

單層GRU模型

接著我們使用GRU模型預測股價,一樣先加上一層GRU層,在疊上一層比率為0.2的Dropout跟全連接層。

# 建立單層GRU函式
class S_GRU(nn.Module):
    def __init__(self):
        super().__init__()
        self.gru1 = nn.GRU(input_size = 4, hidden_size=64, num_layers=1, batch_first = True)
        self.dropout = nn.Dropout(0.2)
        self.linear = nn.Linear(64, 1)
    def forward(self, x):
        x, _ = self.gru1(x)
        x = self.dropout(x)
        x = x[:, -1, :]
        x = self.linear(x)
        return x
# 建立訓練流程函式
def trainer(epochs, loader, X_train, y_train, X_val, y_val, model, criterion, optimizer):
  train_loss, test_loss = [],[]
  for epoch in range(epochs):
    model.train()
    for batch, (x, y_true) in enumerate(loader):
      y_pred = model(x)
      loss = criterion(y_pred, y_true)
      loss.backward()
      optimizer.step()
      optimizer.zero_grad()
    model.eval()
    with torch.no_grad():
      y_pred = model(X_train)
      train_rmse = np.sqrt(criterion(y_pred, y_train).item())
      train_loss.append(train_rmse)
      y_pred = model(X_val)
      test_rmse = np.sqrt(criterion(y_pred, y_val).item())
      test_loss.append(test_rmse)
      if (epoch+1) % 100 == 0:
        print('epoch %d train rmse %.4f test rmse %.4f' % (epoch+1, train_rmse, test_rmse))
  return train_loss, test_loss
# 設置模型、損失函數與優化器
model = S_GRU().to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters())
epochs = 1000
# 開始訓練並且計算訓練所需時間
start = time.time()
sgru_train_loss, sgru_test_loss = trainer(epochs, loader, X_train, y_train, X_val, y_val, model, criterion, optimizer)
end = time.time()
print('single gru time cost %.4f' %(end-start))
 訓練結果
訓練結果
                      

繪製損失曲線

單層GRU損失曲線
單層GRU損失曲線

同樣在epoch為200左右時,單層GRU模型的損失收斂到約0.7的位置,然而值得注意的是,其訓練損失的震盪幅度較大。後續再將股價預測圖繪製檢驗模型的預測能力,同樣可發現其預測能力也是較佳的,繪圖程式碼請見最下方。

單層GRU股價預測
單層GRU股價預測

雙層GRU模型

如同LSTM, 我們也堆疊了一個雙層GRU模型檢驗是否能達成更佳的預測效果。模型架構: 一層GRU + 一層Dropout + 一層GRU+ 一層Dropout + 一層全連接層。其中兩次Dropout的比率都調整為40%,這裡將比率調高的原因是為了避免過擬合問題。

# 建立雙層GRU函式
class GRU(nn.Module):
    def __init__(self):
        super().__init__()
        self.gru1 = nn.GRU(input_size = 4, hidden_size=64, num_layers=1, batch_first=True)
        self.dropout1 = nn.Dropout(0.4)
        self.gru2 = nn.GRU(input_size = 64, hidden_size=32, num_layers=1, batch_first=True)
        self.dropout2 = nn.Dropout(0.4)
        self.linear = nn.Linear(32, 1)
    def forward(self, x):
        x, _ = self.gru1(x)
        x = self.dropout1(x)
        x, _ = self.gru2(x)
        x = self.dropout2(x)
        x = x[:, -1, :]
        x = self.linear(x)
        return x
# 建立訓練流程函式
def trainer(epochs, loader, X_train, y_train, X_val, y_val, model, criterion, optimizer):
  train_loss, test_loss = [],[]
  for epoch in range(epochs):
    model.train()
    for batch, (x, y_true) in enumerate(loader):
      y_pred = model(x)
      loss = criterion(y_pred, y_true)
      loss.backward()
      optimizer.step()
      optimizer.zero_grad()
    model.eval()
    with torch.no_grad():
      y_pred = model(X_train)
      train_rmse = np.sqrt(criterion(y_pred, y_train).item())
      train_loss.append(train_rmse)
      y_pred = model(X_val)
      test_rmse = np.sqrt(criterion(y_pred, y_val).item())
      test_loss.append(test_rmse)
      if (epoch+1) % 100 == 0:
        print('epoch %d train rmse %.4f test rmse %.4f' % (epoch+1, train_rmse, test_rmse))
  return train_loss, test_loss
# 設置模型、損失函數與優化器
model = GRU().to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters())
epochs = 1000
# 開始訓練並且計算訓練所需時間
start = time.time()
gru_train_loss, gru_test_loss = trainer(epochs, loader, X_train, y_train, X_val, y_val, model, criterion, optimizer)
end = time.time()
print('stack gru time cost %.4f' %(end-start))
訓練結果
訓練結果

繪製損失曲線

雙層GRU損失曲線
雙層GRU損失曲線

可發現雙層GRU的震盪幅度較單層大,約莫在300個epoch漸漸收斂到約0.7的位置。後續再將股價預測圖繪製檢驗模型的預測能力,同樣可發現其預測能力較單層遜色,繪圖程式碼請見最下方。

雙層GRU股價預測
雙層GRU股價預測

總結

從上述結果可以發現,不論是LSTM或是GRU,單層在台積電股價預測上表現皆優於雙層。接著我們比較兩單層模型在驗證集的損失曲線,可以發現兩者最後都能收斂到0.07附近。在震盪部分事實上兩者幅度相當,但GRU在前期的損失下降幅度明顯大於LSTM,繪圖程式碼見最下方。

LSTM、GRU結果圖
LSTM、GRU結果圖

此外,GRU理論上來說,運算速度會快於LSTM,而在訓練過程中,也發現這樣的事實,單層GRU相較於單層LSTM快約8秒,雙層GRU則快雙層LSTM約3秒,見下方螢光色域。

運行時間比較
運行時間比較

總的說,LSTM與GRU在這次的試驗中,對台積電股價皆有一定的預測能力,然而GRU受惠於演算法結構優化,在計算上所需時間較短。由於這次試驗僅採取單一股票標的且時間限縮於2019到2022三年,故無法說明LSTM或GRU對於股價一定具有預測能力。但根據【資料科學】LSTM的結論與本次的觀察,我們認為LSTM與GRU可以作為投資人在選股時的一項參考依據,建議可以搭配其他選股指標,比如: 【實戰應用】布林通道交易策略【量化分析】MACD指標回測實戰,建構投資策略。

溫馨提醒,本次策略與標的僅供參考,不代表任何商品或投資上的建議。之後也會介紹使用TEJ資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購TEJ E-Shop的相關方案,用高品質的資料庫,建構出適合自己的交易策略。

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing