本節將介紹另一種常用的門控循環神經網絡:長短期記憶(long short-term memory,LSTM)。它 比門控循環單元的結構稍微複雜一點。
1.1、數據集和問題定義import torch
import torch.nn as nn
import seaborn as sns
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
讓我們列印Seaborn庫內置的所有數據集的列表:
['anscombe',
'attention',
'brain_networks',
'car_crashes',
'diamonds',
'dots',
'exercise',
'flights',
'fmri',
'gammas',
'iris',
'mpg',
'planets',
'tips',
'titanic']
讓我們將數據集加載到我們的應用程式中
flight_data = sns.load_dataset("flights")
flight_data.head()
該數據集有三列:year,month,和passengers。該passengers列包含指定月份旅行旅客的總數。讓我們繪製數據集的形狀:
flight_data.shape
(144, 3)
可以看到數據集中有144行和3列,這意味著數據集包含12年的乘客旅行記錄。
任務是根據前132個月來預測最近12個月內旅行的乘客人數。請記住,我們有144個月的記錄,這意味著前132個月的數據將用於訓練我們的LSTM模型,而模型性能將使用最近12個月的值進行評估。
讓我們繪製每月乘客的出行頻率。
接下來的腳本繪製了每月乘客人數的頻率:
fig_size = plt.rcParams["figure.figsize"]
fig_size[0] = 15
fig_size[1] = 5
plt.rcParams["figure.figsize"] = fig_size
plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.xlabel('Months')
plt.grid(True)
plt.autoscale(axis='x',tight=True)
plt.plot(flight_data['passengers'])
[<matplotlib.lines.Line2D at 0x1b409470>]
輸出顯示,多年來,乘飛機旅行的平均乘客人數有所增加。一年內旅行的乘客數量波動,這是有道理的,因為在暑假或寒假期間,旅行的乘客數量與一年中的其他部分相比有所增加。
1.2、數據預處理數據集中的列類型為object,如以下代碼所示:
flight_data.columns
Index(['year', 'month', 'passengers'], dtype='object')
第一步是將passengers列的類型更改為float。
all_data = flight_data['passengers'].values.astype(float)
print(all_data)
[112. 118. 132. 129. 121. 135. 148. 148. 136. 119. 104. 118. 115. 126.
141. 135. 125. 149. 170. 170. 158. 133. 114. 140. 145. 150. 178. 163.
172. 178. 199. 199. 184. 162. 146. 166. 171. 180. 193. 181. 183. 218.
230. 242. 209. 191. 172. 194. 196. 196. 236. 235. 229. 243. 264. 272.
237. 211. 180. 201. 204. 188. 235. 227. 234. 264. 302. 293. 259. 229.
203. 229. 242. 233. 267. 269. 270. 315. 364. 347. 312. 274. 237. 278.
284. 277. 317. 313. 318. 374. 413. 405. 355. 306. 271. 306. 315. 301.
356. 348. 355. 422. 465. 467. 404. 347. 305. 336. 340. 318. 362. 348.
363. 435. 491. 505. 404. 359. 310. 337. 360. 342. 406. 396. 420. 472.
548. 559. 463. 407. 362. 405. 417. 391. 419. 461. 472. 535. 622. 606.
508. 461. 390. 432.]
接下來,我們將數據集分為訓練集和測試集。LSTM算法將在訓練集上進行訓練。然後將使用該模型對測試集進行預測。將預測結果與測試集中的實際值進行比較,以評估訓練後模型的性能。
前132條記錄將用於訓練模型,後12條記錄將用作測試集。以下腳本將數據分為訓練集和測試集。
test_data_size = 12
train_data = all_data[:-test_data_size]
test_data = all_data[-test_data_size:]
print(test_data)
[417. 391. 419. 461. 472. 535. 622. 606. 508. 461. 390. 432.]
我們的數據集目前尚未規範化。最初幾年的乘客總數遠少於後來幾年的乘客總數。標準化數據以進行時間序列預測非常重要。以在一定範圍內的最小值和最大值之間對數據進行規範化。我們將使用模塊中的MinMaxScaler類sklearn.preprocessing來擴展數據。
以下代碼 分別將最大值和最小值分別為-1和1歸一化。
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler(feature_range=(-1, 1))
train_data_normalized = scaler.fit_transform(train_data .reshape(-1, 1))
print(train_data_normalized[:5])
print(train_data_normalized[-5:])
[[-0.96483516]
[-0.93846154]
[-0.87692308]
[-0.89010989]
[-0.92527473]]
[[1. ]
[0.57802198]
[0.33186813]
[0.13406593]
[0.32307692]]
您可以看到數據集值現在在-1和1之間。
在此重要的是要提到數據標準化僅應用於訓練數據,而不應用於測試數據。如果對測試數據進行歸一化處理,則某些信息可能會從訓練集中 到測試集中。
下一步是將我們的數據集轉換為張量,因為PyTorch模型是使用張量訓練的。要將數據集轉換為張量,我們可以簡單地將數據集傳遞給FloatTensor對象的構造函數,如下所示
train_data_normalized = torch.FloatTensor(train_data_normalized).view(-1)
最後的預處理步驟是將我們的訓練數據轉換為序列和相應的標籤。
您可以使用任何序列長度,這取決於領域知識。但是,在我們的數據集中,使用12的序列長度很方便,因為我們有月度數據,一年中有12個月。如果我們有每日數據,則更好的序列長度應該是365,即一年中的天數。因此,我們將訓練的輸入序列長度設置為12。
接下來,我們將定義一個名為的函數create_inout_sequences。該函數將接受原始輸入數據,並將返回一個元組列表。在每個元組中,第一個元素將包含與12個月內旅行的乘客數量相對應的12個項目的列表,第二個元組元素將包含一個項目,即在12 + 1個月內的乘客數量。
train_window = 12
def create_inout_sequences(input_data, tw):
inout_seq = []
L = len(input_data)
for i in range(L-tw):
train_seq = input_data[i:i+tw]
train_label = input_data[i+tw:i+tw+1]
inout_seq.append((train_seq ,train_label))
return inout_seq
執行以下腳本以創建序列和相應的標籤進行訓練:
train_inout_seq = create_inout_sequences(train_data_normalized, train_window)
如果列印train_inout_seq列表的長度,您將看到它包含120個項目。這是因為儘管訓練集包含132個元素,但是序列長度為12,這意味著第一個序列由前12個項目組成,第13個項目是第一個序列的標籤。同樣,第二個序列從第二個項目開始,到第13個項目結束,而第14個項目是第二個序列的標籤,依此類推。
現在讓我們輸出train_inout_seq列表的前5個項目:
train_inout_seq[:5]
[(tensor([-0.9648, -0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066,
-0.8593, -0.9341, -1.0000, -0.9385]), tensor([-0.9516])),
(tensor([-0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593,
-0.9341, -1.0000, -0.9385, -0.9516]),
tensor([-0.9033])),
(tensor([-0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341,
-1.0000, -0.9385, -0.9516, -0.9033]), tensor([-0.8374])),
(tensor([-0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000,
-0.9385, -0.9516, -0.9033, -0.8374]), tensor([-0.8637])),
(tensor([-0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385,
-0.9516, -0.9033, -0.8374, -0.8637]), tensor([-0.9077]))]
您會看到每個項目都是一個元組,其中第一個元素由序列的12個項目組成,第二個元組元素包含相應的標籤。
1.3、創建LSTM模型讓我總結一下以上代碼中發生的事情。LSTM該類的構造函數接受三個參數:
input_size:對應於輸入中的要素數量。儘管我們的序列長度為12,但每個月我們只有1個值,即乘客總數,因此輸入大小為1。hidden_layer_size:指定隱藏層的數量以及每層中神經元的數量。我們將有一層100個神經元。output_size:輸出中的項目數,由於我們要預測未來1個月的乘客人數,因此輸出大小為1。接下來,在構造函數中,我們創建變量hidden_layer_size,lstm,linear,和hidden_cell。LSTM算法接受三個輸入:先前的隱藏狀態,先前的單元狀態和當前輸入。該hidden_cell變量包含先前的隱藏狀態和單元狀態。的lstm和linear層變量用於創建LSTM和線性層。
在forward方法內部,將input_seq作為參數傳遞,該參數首先傳遞給lstm圖層。lstm層的輸出是當前時間步的隱藏狀態和單元狀態,以及輸出。lstm圖層的輸出將傳遞到該linear圖層。預計的乘客人數存儲在predictions列表的最後一項中,並返回到調用函數。
下一步是創建LSTM()類的對象,定義損失函數和優化器。由於我們正在解決分類問題,
讓我們輸出模型:
class LSTM(nn.Module):
def __init__(self, input_size=1, hidden_layer_size=100, output_size=1):
super().__init__()
self.hidden_layer_size = hidden_layer_size
self.lstm = nn.LSTM(input_size, hidden_layer_size)
self.linear = nn.Linear(hidden_layer_size, output_size)
self.hidden_cell = (torch.zeros(1,1,self.hidden_layer_size),
torch.zeros(1,1,self.hidden_layer_size))
def forward(self, input_seq):
lstm_out, self.hidden_cell = self.lstm(input_seq.view(len(input_seq) ,1, -1), self.hidden_cell)
predictions = self.linear(lstm_out.view(len(input_seq), -1))
return predictions[-1]
model = LSTM()
loss_function = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
print(model)
LSTM(
(lstm): LSTM(1, 100)
(linear): Linear(in_features=100, out_features=1, bias=True)
)
epochs = 150
for i in range(epochs):
for seq, labels in train_inout_seq:
optimizer.zero_grad()
model.hidden_cell = (torch.zeros(1, 1, model.hidden_layer_size),
torch.zeros(1, 1, model.hidden_layer_size))
y_pred = model(seq)
single_loss = loss_function(y_pred, labels)
single_loss.backward()
optimizer.step()
if i%25 == 1:
print(f'epoch: {i:3} loss: {single_loss.item():10.8f}')
print(f'epoch: {i:3} loss: {single_loss.item():10.10f}')
epoch: 1 loss: 0.01599291
epoch: 26 loss: 0.00388177
epoch: 51 loss: 0.00561049
epoch: 76 loss: 0.00011478
epoch: 101 loss: 0.00737344
epoch: 126 loss: 0.01063965
epoch: 149 loss: 0.0034309230
現在我們的模型已經訓練完畢,我們可以開始進行預測了。
fut_pred = 12
test_inputs = train_data_normalized[-train_window:].tolist()
print(test_inputs)
[0.12527473270893097, 0.04615384712815285, 0.3274725377559662, 0.2835164964199066, 0.3890109956264496, 0.6175824403762817, 0.9516483545303345, 1.0, 0.5780220031738281, 0.33186814188957214, 0.13406594097614288, 0.32307693362236023]
您可以將上述值與train_data_normalized數據列表的最後12個值進行比較。
該test_inputs項目將包含12個項目。在for循環內,這12個項目將用於對測試集中的第一個項目進行預測,即項目編號133。然後將預測值附加到test_inputs列表中。在第二次迭代中,最後12個項目將再次用作輸入,並將進行新的預測,然後將其test_inputs再次添加到列表中。for由於測試集中有12個元素,因此該循環將執行12次。在循環末尾,test_inputs列表將包含24個項目。最後12個項目將是測試集的預測值。
以下腳本用於進行預測:
如果輸出test_inputs列表的長度,您將看到它包含24個項目。可以按以下方式列印最後12個預測項目:
model.eval()
for i in range(fut_pred):
seq = torch.FloatTensor(test_inputs[-train_window:])
with torch.no_grad():
model.hidden = (torch.zeros(1, 1, model.hidden_layer_size),
torch.zeros(1, 1, model.hidden_layer_size))
test_inputs.append(model(seq).item())
test_inputs[fut_pred:]
[1.1925697326660156,
1.5521266460418701,
1.7060068845748901,
1.760893702507019,
1.7794983386993408,
1.7928276062011719,
1.8045395612716675,
1.8172146081924438,
1.8299273252487183,
1.8416459560394287,
1.8509267568588257,
1.8585551977157593]
由於我們對訓練數據集進行了標準化,因此預測值也進行了標準化。我們需要將歸一化的預測值轉換為實際的預測值。
actual_predictions = scaler.inverse_transform(np.array(test_inputs[train_window:] ).reshape(-1, 1))
print(actual_predictions)
[[602.80961418]
[684.60881197]
[719.61656624]
[732.10331732]
[736.33587205]
[739.36828041]
[742.03275019]
[744.91632336]
[747.80846649]
[750.474455 ]
[752.58583719]
[754.32130748]]
現在讓我們針對實際值繪製預測值。看下面的代碼:
x = np.arange(132, 144, 1)
print(x)
[132 133 134 135 136 137 138 139 140 141 142 143]
在上面的腳本中,我們創建一個列表,其中包含最近12個月的數值。第一個月的索引值為0,因此最後一個月的索引值為143。
在下面的腳本中,我們將繪製144個月的乘客總數以及最近12個月的預計乘客數量。
plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.grid(True)
plt.autoscale(axis='x', tight=True)
plt.plot(flight_data['passengers'])
plt.plot(x,actual_predictions)
plt.show()
我們的LSTM所做的預測用橙色線表示。您可以看到我們的算法不太準確,但是它仍然能夠捕獲最近12個月內旅行的乘客總數的上升趨勢以及偶爾的波動。您可以嘗試在LSTM層中使用更多的時期和更多的神經元,以查看是否可以獲得更好的性能。
為了更好地查看輸出,我們可以繪製最近12個月的實際和預測乘客數量,如下所示:
plt.title('Month vs Passenger')
plt.ylabel('Total Passengers')
plt.grid(True)
plt.autoscale(axis='x', tight=True)
plt.plot(flight_data['passengers'][-train_window:])
plt.plot(x,actual_predictions)
plt.show()