PyTorch+LSTM實(shí)現(xiàn)單變量時(shí)間序列預(yù)測(cè)
時(shí)間序列是指在一段時(shí)間內(nèi)發(fā)生的任何可量化的度量或事件。盡管這聽(tīng)起來(lái)微不足道,但幾乎任何東西都可以被認(rèn)為是時(shí)間序列。一個(gè)月里你每小時(shí)的平均心率,一年里一只股票的日收盤(pán)價(jià),一年里某個(gè)城市每周發(fā)生的交通事故數(shù)。
在任何一段時(shí)間段內(nèi)記錄這些信息都被認(rèn)為是一個(gè)時(shí)間序列。對(duì)于這些例子中的每一個(gè),都有事件發(fā)生的頻率(每天、每周、每小時(shí)等)和事件發(fā)生的時(shí)間長(zhǎng)度(一個(gè)月、一年、一天等)。

在本教程中,我們將使用 PyTorch-LSTM 進(jìn)行深度學(xué)習(xí)時(shí)間序列預(yù)測(cè)。
我們的目標(biāo)是接收一個(gè)值序列,預(yù)測(cè)該序列中的下一個(gè)值。最簡(jiǎn)單的方法是使用自回歸模型,我們將專(zhuān)注于使用LSTM來(lái)解決這個(gè)問(wèn)題。
數(shù)據(jù)準(zhǔn)備
讓我們看一個(gè)時(shí)間序列樣本。下圖顯示了2013年至2018年石油價(jià)格的一些數(shù)據(jù)。

這只是一個(gè)日期軸上單個(gè)數(shù)字序列的圖。下表顯示了這個(gè)時(shí)間序列的前10個(gè)條目。每天都有價(jià)格數(shù)據(jù)。
date dcoilwtico
2013-01-01 NaN
2013-01-02 93.14
2013-01-03 92.97
2013-01-04 93.12
2013-01-07 93.20
2013-01-08 93.21
2013-01-09 93.08
2013-01-10 93.81
2013-01-11 93.60
2013-01-14 94.27
許多機(jī)器學(xué)習(xí)模型在標(biāo)準(zhǔn)化數(shù)據(jù)上的表現(xiàn)要好得多。標(biāo)準(zhǔn)化數(shù)據(jù)的標(biāo)準(zhǔn)方法是對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換,使得每一列的均值為0,標(biāo)準(zhǔn)差為1。下面的代碼scikit-learn進(jìn)行標(biāo)準(zhǔn)化
from sklearn.preprocessing import StandardScaler
# Fit scalers
scalers = {}
for x in df.columns:
scalers[x] = StandardScaler().fit(df[x].values.reshape(-1, 1))
# Transform data via scalers
norm_df = df.copy()
for i, key in enumerate(scalers.keys()):
norm = scalers[key].transform(norm_df.iloc[:, i].values.reshape(-1, 1))
norm_df.iloc[:, i] = norm
我們還希望數(shù)據(jù)具有統(tǒng)一的頻率——在這個(gè)例子中,有這5年里每天的石油價(jià)格,如果你的數(shù)據(jù)情況并非如此,Pandas有幾種不同的方法來(lái)重新采樣數(shù)據(jù)以適應(yīng)統(tǒng)一的頻率,請(qǐng)參考我們公眾號(hào)以前的文章
對(duì)于訓(xùn)練數(shù)據(jù)我們需要將完整的時(shí)間序列數(shù)據(jù)截取成固定長(zhǎng)度的序列。假設(shè)我們有一個(gè)序列:[1, 2, 3, 4, 5, 6]。
通過(guò)選擇長(zhǎng)度為 3 的序列,我們可以生成以下序列及其相關(guān)目標(biāo):
[Sequence] Target
[1, 2, 3] → 4
[2, 3, 4] → 5
[3, 4, 5] → 6
或者說(shuō)我們定義了為了預(yù)測(cè)下一個(gè)值需要回溯多少步。我們將這個(gè)值稱(chēng)為訓(xùn)練窗口,而要預(yù)測(cè)的值的數(shù)量稱(chēng)為預(yù)測(cè)窗口。在這個(gè)例子中,它們分別是3和1。下面的函數(shù)詳細(xì)說(shuō)明了這是如何完成的。
# 如上所示,定義一個(gè)創(chuàng)建序列和目標(biāo)的函數(shù)
def generate_sequences(df: pd.DataFrame, tw: int, pw: int, target_columns, drop_targets=False):
'''
df: Pandas DataFrame of the univariate time-series
tw: Training Window - Integer defining how many steps to look back
pw: Prediction Window - Integer defining how many steps forward to predict
returns: dictionary of sequences and targets for all sequences
'''
data = dict() # Store results into a dictionary
L = len(df)
for i in range(L-tw):
# Option to drop target from dataframe
if drop_targets:
df.drop(target_columns, axis=1, inplace=True)
# Get current sequence
sequence = df[i:i+tw].values
# Get values right after the current sequence
target = df[i+tw:i+tw+pw][target_columns].values
data[i] = {'sequence': sequence, 'target': target}
return data
這樣我們就可以在PyTorch中使用Dataset類(lèi)自定義數(shù)據(jù)集
class SequenceDataset(Dataset):
def __init__(self, df):
self.data = df
def __getitem__(self, idx):
sample = self.data[idx]
return torch.Tensor(sample['sequence']), torch.Tensor(sample['target'])
def __len__(self):
return len(self.data)
然后,我們可以使用PyTorch DataLoader來(lái)遍歷數(shù)據(jù)。使用DataLoader的好處是它在內(nèi)部自動(dòng)進(jìn)行批處理和數(shù)據(jù)的打亂,所以我們不必自己實(shí)現(xiàn)它,代碼如下:
# 這里我們?yōu)槲覀兊哪P投x屬性 BATCH_SIZE = 16 # Training batch size split = 0.8 # Train/Test Split ratio sequences = generate_sequences(norm_df.dcoilwtico.to_frame(), sequence_len, nout, 'dcoilwtico') dataset = SequenceDataset(sequences) # 根據(jù)拆分比例拆分?jǐn)?shù)據(jù),并將每個(gè)子集加載到單獨(dú)的DataLoader對(duì)象中 train_len = int(len(dataset)*split) lens = [train_len, len(dataset)-train_len] train_ds, test_ds = random_split(dataset, lens) trainloader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, drop_last=True) testloader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
在每次迭代中,DataLoader將產(chǎn)生16個(gè)(批量大小)序列及其相關(guān)目標(biāo),我們將這些目標(biāo)傳遞到模型中。
模型架構(gòu)
我們將使用一個(gè)單獨(dú)的LSTM層,然后是模型的回歸部分的一些線性層,當(dāng)然在它們之間還有dropout層。該模型將為每個(gè)訓(xùn)練輸入輸出單個(gè)值。
class LSTMForecaster(nn.Module):
def __init__(self, n_features, n_hidden, n_outputs, sequence_len, n_lstm_layers=1, n_deep_layers=10, use_cuda=False, dropout=0.2):
'''
n_features: number of input features (1 for univariate forecasting)
n_hidden: number of neurons in each hidden layer
n_outputs: number of outputs to predict for each training example
n_deep_layers: number of hidden dense layers after the lstm layer
sequence_len: number of steps to look back at for prediction
dropout: float (0 < dropout < 1) dropout ratio between dense layers
'''
super().__init__()
self.n_lstm_layers = n_lstm_layers
self.nhid = n_hidden
self.use_cuda = use_cuda # set option for device selection
# LSTM Layer
self.lstm = nn.LSTM(n_features,
n_hidden,
num_layers=n_lstm_layers,
batch_first=True) # As we have transformed our data in this way
# first dense after lstm
self.fc1 = nn.Linear(n_hidden * sequence_len, n_hidden)
# Dropout layer
self.dropout = nn.Dropout(p=dropout)
# Create fully connected layers (n_hidden x n_deep_layers)
dnn_layers = []
for i in range(n_deep_layers):
# Last layer (n_hidden x n_outputs)
if i == n_deep_layers - 1:
dnn_layers.append(nn.ReLU())
dnn_layers.append(nn.Linear(nhid, n_outputs))
# All other layers (n_hidden x n_hidden) with dropout option
else:
dnn_layers.append(nn.ReLU())
dnn_layers.append(nn.Linear(nhid, nhid))
if dropout:
dnn_layers.append(nn.Dropout(p=dropout))
# compile DNN layers
self.dnn = nn.Sequential(*dnn_layers)
def forward(self, x):
# Initialize hidden state
hidden_state = torch.zeros(self.n_lstm_layers, x.shape[0], self.nhid)
cell_state = torch.zeros(self.n_lstm_layers, x.shape[0], self.nhid)
# move hidden state to device
if self.use_cuda:
hidden_state = hidden_state.to(device)
cell_state = cell_state.to(device)
self.hidden = (hidden_state, cell_state)
# Forward Pass
x, h = self.lstm(x, self.hidden) # LSTM
x = self.dropout(x.contiguous().view(x.shape[0], -1)) # Flatten lstm out
x = self.fc1(x) # First Dense
return self.dnn(x) # Pass forward through fully connected DNN.
我們?cè)O(shè)置了2個(gè)可以自由地調(diào)優(yōu)的參數(shù)n_hidden和n_deep_players。更大的參數(shù)意味著模型更復(fù)雜和更長(zhǎng)的訓(xùn)練時(shí)間,所以這里我們可以使用這兩個(gè)參數(shù)靈活調(diào)整。
剩下的參數(shù)如下:sequence_len指的是訓(xùn)練窗口,nout定義了要預(yù)測(cè)多少步;將sequence_len設(shè)置為180,nout設(shè)置為1,意味著模型將查看180天(半年)后的情況,以預(yù)測(cè)明天將發(fā)生什么。
nhid = 50 # Number of nodes in the hidden layer n_dnn_layers = 5 # Number of hidden fully connected layers nout = 1 # Prediction Window sequence_len = 180 # Training Window # Number of features (since this is a univariate timeseries we'll set # this to 1 -- multivariate analysis is coming in the future) ninp = 1 # Device selection (CPU | GPU) USE_CUDA = torch.cuda.is_available() device = 'cuda' if USE_CUDA else 'cpu' # Initialize the model model = LSTMForecaster(ninp, nhid, nout, sequence_len, n_deep_layers=n_dnn_layers, use_cuda=USE_CUDA).to(device)
模型訓(xùn)練
定義好模型后,我們可以選擇損失函數(shù)和優(yōu)化器,設(shè)置學(xué)習(xí)率和周期數(shù),并開(kāi)始我們的訓(xùn)練循環(huán)。由于這是一個(gè)回歸問(wèn)題(即我們?cè)噲D預(yù)測(cè)一個(gè)連續(xù)值),最簡(jiǎn)單也是最安全的損失函數(shù)是均方誤差。這提供了一種穩(wěn)健的方法來(lái)計(jì)算實(shí)際值和模型預(yù)測(cè)值之間的誤差。

優(yōu)化器和損失函數(shù)如下:
# Set learning rate and number of epochs to train over lr = 4e-4 n_epochs = 20 # Initialize the loss function and optimizer criterion = nn.MSELoss().to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
下面就是訓(xùn)練循環(huán)的代碼:在每次訓(xùn)練迭代中,我們將計(jì)算之前創(chuàng)建的訓(xùn)練集和驗(yàn)證集的損失:
# Lists to store training and validation losses
t_losses, v_losses = [], []
# Loop over epochs
for epoch in range(n_epochs):
train_loss, valid_loss = 0.0, 0.0
# train step
model.train()
# Loop over train dataset
for x, y in trainloader:
optimizer.zero_grad()
# move inputs to device
x = x.to(device)
y = y.squeeze().to(device)
# Forward Pass
preds = model(x).squeeze()
loss = criterion(preds, y) # compute batch loss
train_loss += loss.item()
loss.backward()
optimizer.step()
epoch_loss = train_loss / len(trainloader)
t_losses.append(epoch_loss)
# validation step
model.eval()
# Loop over validation dataset
for x, y in testloader:
with torch.no_grad():
x, y = x.to(device), y.squeeze().to(device)
preds = model(x).squeeze()
error = criterion(preds, y)
valid_loss += error.item()
valid_loss = valid_loss / len(testloader)
v_losses.append(valid_loss)
print(f'{epoch} - train: {epoch_loss}, valid: {valid_loss}')
plot_losses(t_losses, v_losses)

這樣模型已經(jīng)訓(xùn)練好了,可以評(píng)估預(yù)測(cè)了。
推理
我們調(diào)用訓(xùn)練過(guò)的模型來(lái)預(yù)測(cè)未打亂的數(shù)據(jù),并比較預(yù)測(cè)與真實(shí)觀察有多大不同。
def make_predictions_from_dataloader(model, unshuffled_dataloader):
model.eval()
predictions, actuals = [], []
for x, y in unshuffled_dataloader:
with torch.no_grad():
p = model(x)
predictions.append(p)
actuals.append(y.squeeze())
predictions = torch.cat(predictions).numpy()
actuals = torch.cat(actuals).numpy()
return predictions.squeeze(), actuals

石油歷史上的常態(tài)化預(yù)測(cè)與實(shí)際價(jià)格
我們的預(yù)測(cè)看起來(lái)還不錯(cuò)!預(yù)測(cè)的效果還可以,表明我們沒(méi)有過(guò)度擬合模型,讓我們看看能否用它來(lái)預(yù)測(cè)未來(lái)。
預(yù)測(cè)
如果我們將歷史定義為預(yù)測(cè)時(shí)刻之前的序列,算法很簡(jiǎn)單:
1.從歷史(訓(xùn)練窗口長(zhǎng)度)中獲取最新的有效序列。
2.將最新的序列輸入模型并預(yù)測(cè)下一個(gè)值。
3.將預(yù)測(cè)值附加到歷史記錄上。
4.迭代重復(fù)步驟1。
這里需要注意的是,根據(jù)訓(xùn)練模型時(shí)選擇的參數(shù),你預(yù)測(cè)的越長(zhǎng)(遠(yuǎn)),模型就越容易表現(xiàn)出它自己的偏差,開(kāi)始預(yù)測(cè)平均值。因此,如果沒(méi)有必要,我們不希望總是預(yù)測(cè)得太超前,因?yàn)檫@會(huì)影響預(yù)測(cè)的準(zhǔn)確性。
這在下面的函數(shù)中實(shí)現(xiàn):
def one_step_forecast(model, history):
'''
model: PyTorch model object
history: a sequence of values representing the latest values of the time
series, requirement -> len(history.shape) == 2
outputs a single value which is the prediction of the next value in the
sequence.
'''
model.cpu()
model.eval()
with torch.no_grad():
pre = torch.Tensor(history).unsqueeze(0)
pred = self.model(pre)
return pred.detach().numpy().reshape(-1)
def n_step_forecast(data: pd.DataFrame, target: str, tw: int, n: int, forecast_from: int=None, plot=False):
'''
n: integer defining how many steps to forecast
forecast_from: integer defining which index to forecast from. None if
you want to forecast from the end.
plot: True if you want to output a plot of the forecast, False if not.
'''
history = data[target].copy().to_frame()
# Create initial sequence input based on where in the series to forecast
# from.
if forecast_from:
pre = list(history[forecast_from - tw : forecast_from][target].values)
else:
pre = list(history[self.target])[-tw:]
# Call one_step_forecast n times and append prediction to history
for i, step in enumerate(range(n)):
pre_ = np.array(pre[-tw:]).reshape(-1, 1)
forecast = self.one_step_forecast(pre_).squeeze()
pre.append(forecast)
# The rest of this is just to add the forecast to the correct time of
# the history series
res = history.copy()
ls = [np.nan for i in range(len(history))]
# Note: I have not handled the edge case where the start index + n is
# before the end of the dataset and crosses past it.
if forecast_from:
ls[forecast_from : forecast_from + n] = list(np.array(pre[-n:]))
res['forecast'] = ls
res.columns = ['actual', 'forecast']
else:
fc = ls + list(np.array(pre[-n:]))
ls = ls + [np.nan for i in range(len(pre[-n:]))]
ls[:len(history)] = history[self.target].values
res = pd.DataFrame([ls, fc], index=['actual', 'forecast']).T
return res 我們來(lái)看看實(shí)際的效果
我們?cè)谶@個(gè)時(shí)間序列的中間從不同的地方進(jìn)行預(yù)測(cè),這樣我們就可以將預(yù)測(cè)與實(shí)際發(fā)生的情況進(jìn)行比較。我們的預(yù)測(cè)程序,可以從任何地方對(duì)任何合理數(shù)量的步驟進(jìn)行預(yù)測(cè),紅線表示預(yù)測(cè)。(這些圖表顯示的是y軸上的標(biāo)準(zhǔn)化后的價(jià)格)

預(yù)測(cè)2013年第三季度后200天

預(yù)測(cè)2014/15 后200天

從2016年第一季度開(kāi)始預(yù)測(cè)200天

從數(shù)據(jù)的最后一天開(kāi)始預(yù)測(cè)200天
總結(jié)
我們這個(gè)模型表現(xiàn)的還算一般!但是我們通過(guò)這個(gè)示例完整的介紹了時(shí)間序列預(yù)測(cè)的全部過(guò)程,我們可以通過(guò)嘗試架構(gòu)和參數(shù)的調(diào)整使模型變得得更好,預(yù)測(cè)得更準(zhǔn)確。
本文只處理單變量時(shí)間序列,其中只有一個(gè)值序列。還有一些方法可以使用多個(gè)系列來(lái)進(jìn)行預(yù)測(cè)。這被稱(chēng)為多元時(shí)間序列預(yù)測(cè),我將在以后的文章中介紹。
到此這篇關(guān)于PyTorch+LSTM實(shí)現(xiàn)單變量時(shí)間序列預(yù)測(cè)的文章就介紹到這了,更多相關(guān)PyTorch LSTM時(shí)間序列預(yù)測(cè)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python實(shí)現(xiàn)的一個(gè)火車(chē)票轉(zhuǎn)讓信息采集器
這篇文章主要介紹了python實(shí)現(xiàn)的一個(gè)火車(chē)票轉(zhuǎn)讓信息采集器,采集信息來(lái)源是58同程或者趕集網(wǎng),需要的朋友可以參考下2014-07-07
LyScript實(shí)現(xiàn)指令查詢(xún)功能的示例代碼
對(duì)LyScript自動(dòng)化插件進(jìn)行二次封裝,可以實(shí)現(xiàn)從內(nèi)存中讀入目標(biāo)進(jìn)程解碼后的機(jī)器碼。所以本文為大家介紹了如何實(shí)現(xiàn)LyScript指令查詢(xún)功能,需要的可以參考一下2022-09-09
Python密碼學(xué)概述雙倍強(qiáng)度加密教程
這篇文章主要為大家介紹了Python密碼學(xué)概述雙倍強(qiáng)度加密教程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2022-05-05
python刪除文件夾中具有相同后綴類(lèi)型文件的實(shí)戰(zhàn)演練
在平時(shí)卸載軟件的時(shí)候會(huì)殘留許多文件和空文件夾,下面這篇文章主要給大家介紹了關(guān)于python刪除文件夾中具有相同后綴類(lèi)型文件的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03
python語(yǔ)法學(xué)習(xí)print中f-string用法示例
這篇文章主要為大家介紹了python語(yǔ)法學(xué)習(xí)print中f-string用法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
Python和JavaScript間代碼轉(zhuǎn)換的4個(gè)工具
JavaScript 已經(jīng)成為眾多其它編程語(yǔ)言爭(zhēng)相選擇的轉(zhuǎn)換目標(biāo)(相關(guān)實(shí)例包括 TypeScript、Emscripten、Cor 以及 Cheerp)。而 Python 則擁有龐大的追隨者群體,另外現(xiàn)有的強(qiáng)大庫(kù)資源則使其成為面向 JavaScript 的理想待轉(zhuǎn)換或者說(shuō)轉(zhuǎn)譯選項(xiàng)2016-02-02
使用Python進(jìn)行自動(dòng)化數(shù)據(jù)爬取與存儲(chǔ)
在當(dāng)今數(shù)據(jù)驅(qū)動(dòng)的時(shí)代,從互聯(lián)網(wǎng)上獲取有價(jià)值的信息變得尤為重要,Python,作為一種功能強(qiáng)大且易于學(xué)習(xí)的編程語(yǔ)言,在數(shù)據(jù)爬取領(lǐng)域有著廣泛的應(yīng)用,本文將介紹如何使用Python進(jìn)行自動(dòng)化數(shù)據(jù)爬取與存儲(chǔ),需要的朋友可以參考下2025-02-02
Python Tkinter簡(jiǎn)單布局實(shí)例教程
這篇文章主要介紹了Python Tkinter簡(jiǎn)單布局實(shí)例教程,包括了填充、左右布局、絕對(duì)布局、網(wǎng)格布局等,需要的朋友可以參考下2014-09-09

