iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 23
3
AI & Machine Learning

以100張圖理解 Neural Network -- 觀念與實踐系列 第 23

Day 23:銷售量預測 -- LSTM 的另一個應用

前言

之前,我們都在影像、語言等基礎應用上打轉,這次我們要來探討一個可應用在企業運作上的實例,銷售預測主要是希望藉由過去的銷售記錄預測下一個週期的銷售量,在統計上,我們會使用簡單迴歸,乃至複雜的『時間序列分析』(Time Series Analysis)來預測銷售趨勢,因為,當期的銷售量通常會與前期的銷售量有緊密的關係,除非公司發生重大事件,否則,應該會循著規律變化。還記得嗎? 在『自然語言處理』時,我們會使用LSTM考慮上下文的關係,這個模型恰好與前面講的銷售量預測不謀而合,所以,本篇就以 LSTM 模型來預測銷售量。

銷售量預測的樣態很多種,包括營收、利潤、來客數、遊園人數、銷售產品數/金額、...等等,都屬於同一範疇,本篇會以航空公司的每月乘客人數為例,使用 LSTM 模型預測下個月的乘客數。

時間序列(Time Series Analysis)概念淺介

簡單迴歸(Regression) 公式 y=ax+b,是基於 y(i) 與 y(j) 是相互獨立,沒有任何關聯,但在銷售量的表現上,這個假設並不合理,公司銷售業績通常不會暴漲暴跌,而是『逐步』上升或下跌,也就是與前期的表現有緊密的關聯,另外,大部分的公司也會有淡、旺季,即所謂的『季節效應』(Seasonal Effect),因此,使用更複雜的『時間序列分析』(Time Series Analysis)預測會更貼近事實,時間序列分析的模型因應問題的型態不同也有很多種,我們以ARIMA(Autoregressive Integrated Moving Average)為例,做簡單的說明,如果你對以下的說明很頭痛,可跳過,直接看 Neural Network 怎麼作。

參考Wiki的簡介,ARIMA 是 ARMA 的擴充模型,而 ARMA 就等於 AR + MA,AR 談的就是與前期的關係,有一重要的參數 p,代表期數,就是假設當期與前面p期有關係,MA 是移動平均數(Moving Average)就是避免異常值影響過大,故利用前幾期的平均數來代表當期量,所以,也有一個重要的參數 q,代表計算移動平均的期數,另外,ARIMA 多一個參數 d,稱為差分階數,是要使時間序列維持『穩態』(stationarity),所以,總共有三個參數 p、q、d,相關的理論是一門學科,筆者只能點到為止。

至於p、q如何決定是多少呢? 可利用『自我相關函數』(Autocorrelation Function,ACF)或『偏自我相關函數』(Partial Autocorrelation Function, PACF)來協助判斷p及q值,至於 d 值的判斷可參考『Identifying the order of differencing in an ARIMA model』,經過反覆的數據觀察、分析與實驗,才可以得到較接近事實的預測模型。

綜合以上說明,我們可以了解以統計模型預測銷售量,是須具備一定的知識以及行業別知識(Domain Know How),才能建立較正確的預測模型,接下來,我們來看看 Neural Network 的方法。

相關的實作可使用 StatsModels 套件,它支援『時間序列分析』與繪圖。

Neural Network 作法

以下說明及實作主要參考『Time Series Prediction with LSTM Recurrent Neural Networks in Python with Keras』,資料集採用『航空公司每月乘客人數』,可至這裡下載,我們利用以下程式畫出乘客人數的折線圖:

import pandas
import matplotlib.pyplot as plt
dataset = pandas.read_csv('international-airline-passengers.csv', usecols=[1], engine='python', skipfooter=3)
plt.plot(dataset)
plt.show()

https://ithelp.ithome.com.tw/upload/images/20180102/20001976qjrVg1f2ka.png
圖. 航空公司每月乘客人數

我們可以觀察到數據有一定的趨勢與波動性,暑假(7~9月)乘客人數比較多,表示有季節效應,我們可以用ACF或PACF確認一下:

import numpy as np
from scipy import stats
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.graphics.api import qqplot
# 畫出 ACF 12 期的效應
sm.graphics.tsa.plot_acf(dataset, lags=12)
plt.show()
# 畫出 PACF 12 期的效應
sm.graphics.tsa.plot_pacf(dataset, lags=12)
plt.show()

** 注意,StatsModels 套件,必須以以下指令安裝,用 pip install 會有錯 **
conda install -c conda-forge statsmodels

https://ithelp.ithome.com.tw/upload/images/20180102/20001976jBgREtIOU8.png
圖. ACF 12 期的效應
https://ithelp.ithome.com.tw/upload/images/20180102/20001976O48NFvPKUU.png
圖. PACF 12 期的效應

接著,我們就以 LSTM 模型實作,程式碼來自『Time Series Prediction with LSTM Recurrent Neural Networks in Python with Keras』,我加了一些註解,也可至這裡下載,範例在 TimeSeries 資料夾,如下:

# LSTM for international airline passengers problem with regression framing
import numpy
import matplotlib.pyplot as plt
from pandas import read_csv
import math
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error


# 產生 (X, Y) 資料集, Y 是下一期的乘客數
def create_dataset(dataset, look_back=1):
	dataX, dataY = [], []
	for i in range(len(dataset)-look_back-1):
		a = dataset[i:(i+look_back), 0]
		dataX.append(a)
		dataY.append(dataset[i + look_back, 0])
	return numpy.array(dataX), numpy.array(dataY)

# 載入訓練資料
dataframe = read_csv('international-airline-passengers.csv', usecols=[1], engine='python', skipfooter=3)
dataset = dataframe.values
dataset = dataset.astype('float32')
# 正規化(normalize) 資料,使資料值介於[0, 1]
scaler = MinMaxScaler(feature_range=(0, 1))
dataset = scaler.fit_transform(dataset)

# 2/3 資料為訓練資料, 1/3 資料為測試資料
train_size = int(len(dataset) * 0.67)
test_size = len(dataset) - train_size
train, test = dataset[0:train_size,:], dataset[train_size:len(dataset),:]

# 產生 (X, Y) 資料集, Y 是下一期的乘客數(reshape into X=t and Y=t+1)
look_back = 1
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)
# reshape input to be [samples, time steps, features]
trainX = numpy.reshape(trainX, (trainX.shape[0], 1, trainX.shape[1]))
testX = numpy.reshape(testX, (testX.shape[0], 1, testX.shape[1]))

# 建立及訓練 LSTM 模型
model = Sequential()
model.add(LSTM(4, input_shape=(1, look_back)))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(trainX, trainY, epochs=100, batch_size=1, verbose=2)

# 預測
trainPredict = model.predict(trainX)
testPredict = model.predict(testX)

# 回復預測資料值為原始數據的規模
trainPredict = scaler.inverse_transform(trainPredict)
trainY = scaler.inverse_transform([trainY])
testPredict = scaler.inverse_transform(testPredict)
testY = scaler.inverse_transform([testY])

# calculate 均方根誤差(root mean squared error)
trainScore = math.sqrt(mean_squared_error(trainY[0], trainPredict[:,0]))
print('Train Score: %.2f RMSE' % (trainScore))
testScore = math.sqrt(mean_squared_error(testY[0], testPredict[:,0]))
print('Test Score: %.2f RMSE' % (testScore))

# 畫訓練資料趨勢圖
# shift train predictions for plotting
trainPredictPlot = numpy.empty_like(dataset)
trainPredictPlot[:, :] = numpy.nan
trainPredictPlot[look_back:len(trainPredict)+look_back, :] = trainPredict

# 畫測試資料趨勢圖
# shift test predictions for plotting
testPredictPlot = numpy.empty_like(dataset)
testPredictPlot[:, :] = numpy.nan
testPredictPlot[len(trainPredict)+(look_back*2)+1:len(dataset)-1, :] = testPredict

# 畫原始資料趨勢圖
# plot baseline and predictions
plt.plot(scaler.inverse_transform(dataset))
plt.plot(trainPredictPlot)
plt.plot(testPredictPlot)
plt.show()

程式執行

將下載的資料檔international-airline-passengers.csv與程式SimpleLSTM.py放在同一目錄,在DOS內執行以下指令:
python SimpleLSTM.py

執行結果如下:
https://ithelp.ithome.com.tw/upload/images/20180102/200019767wiWctH0aI.png
圖. LSTM 預測結果,藍線為實際值,綠線為訓練結果,紅色為測試結果

程式說明

程式不複雜,可參閱中文註解,處理流程如下:

  1. 資料檔只有兩欄,日期及乘客數,正規化(normalize) 資料,使資料值介於[0, 1]。
  2. 我們要用前期預測當期,故將資料轉為(前期乘客數, 當期乘客數),當作(X, Y)。
  3. 建立及訓練 LSTM 模型,模型很簡單,就只有一個LSTM層及output層。
  4. 訓練 LSTM 模型並進行預測。
  5. 針對實際值、預測值進行繪圖。

可以看到很神奇的現象,我們沒有特別處理季節效應,但 LSTM 經過 100 輪的訓練後,幫我們考慮到了,模型只使用前期資料為X變數。另外,我們也可以使用多期的資料作為X變數,或者,針對季節效應對 LSTM 作『記憶重置』(reset state)、以期數(Time Steps)當input處理、...等等的方法,針對問題特性,多實驗看看,也許會有意想不到的效果。

結論

其實,我們周遭不乏其他的例子可以實驗,例如雨量、氣候、上市公司營收、股價預測、房價預測...等等,都屬於時間序列,尤其,政府目前提供許多 Open Data,取得資料並不困難,重要的是,我們怎麼看待資料,是否有創新的想法吧。


上一篇
Day 22:自動擷取摘要(Automatic Text Summarization) -- NN 作法
下一篇
Day 24:銷售量預測(2) -- 『時間序列分析』技巧篇
系列文
以100張圖理解 Neural Network -- 觀念與實踐31
0
qq520434
iT邦新手 5 級 ‧ 2019-09-08 16:52:55

請問為什麼加了skipfooter之後跑出來的結果會比較好?
還有ACF、PACF的圖要怎麼看,可以麻煩您解釋一下嗎?
有爬了一些文但還是不太懂ARIMA的部分

看更多先前的回應...收起先前的回應...

ACF/PACF是顯示當期與落後期數的相關程度,從上圖PACF看,當期(左邊第一條)與上期(左邊第二條)最密切,故使用上一期預測當期。

qq520434 iT邦新手 5 級 ‧ 2019-09-09 16:42:05 檢舉

所以假設PACF的圖中左邊第三條比起左邊第二條的數值和當期最密切的話,全部的look_back都要改成=2,還是只有create_dataset內的要變更?

look_back = 1
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)

上面第一行改成2即可。

qq520434 iT邦新手 5 級 ‧ 2019-09-10 16:15:13 檢舉

好的,那這方面沒什麼問題了,不過關於skipfooter的部分,請問有辦法能解釋這樣的變化嗎?

skipfooter 是因為最後三列不是資料,故跳掉而已。

qq520434 iT邦新手 5 級 ‧ 2019-09-11 12:29:17 檢舉

那我了解了,原來是因為我是用在原文中的資料庫,所以一直不解為何要使用skipfooter,感謝您的回覆。

0
zehua
iT邦新手 5 級 ‧ 2019-10-30 18:47:24

您好,根據您提供的程式碼我有大概了解了八成,但一直還有個疑問,在最後只有得到訓練集和測試集的計算結果,那如果我要把預測的結果向後延伸該怎麼做?意思就是原始資料集最後是1960/12,但我想得到1961/1的數值。
因為依照我目前的理解認為,到目前為止只是畫出了一條和原始數據相近的線,並沒有做到預測未來的部分。
還是其實是我有哪部分理解錯誤?
再麻煩您解惑,謝謝!

看更多先前的回應...收起先前的回應...
  1. 將最後一筆的乘客數當作X,預測下個月的乘客數,程式如下:
# 最後一筆的乘客數當作X,預測下個月的乘客數
last_row = scaler.transform(testY[0][-1].reshape((-1, 1)))
last_row = numpy.reshape(last_row, (last_row.shape[0], 1, last_row.shape[1]))
testPredict2 = model.predict(last_row)
#print(testPredict2)
scaler.inverse_transform(testPredict2)
  1. 範例是AR(1)的模型,前一期當作X,本期當Y,故有滯後的感覺,可以把look_back = 1 改成 2, 3, ... or n 試試看,再複雜一點,可使用 ARIMA 模型,加上移動平均,會使預測的曲線更平滑,試試看噢。
zehua iT邦新手 5 級 ‧ 2019-11-04 14:16:45 檢舉

好的 感謝您的回應,文章對我非常有幫助

您好請問若我要預測接下來好幾期的數值,例如接下來12個月的乘客數量,有辦法嗎?

LSTM 一次只能預測一期,我發現一個很棒的套件 -- FB Prophet,可一次預測很多期,有興趣可以研究一下。
近期有空的話,我會分享一下閱讀心得。

0
10612201
iT邦新手 5 級 ‧ 2020-06-26 01:03:28

您好,請問上面您所提供的資料集連結-->"資料集採用『航空公司每月乘客人數』",要如何下載呢??

我點進去之後會進入,商用數據分析網站,選試用登錄進去後重新使用您所提供的連結,也找不到

能分享一下您當時怎麼找到這個資料集的嗎??

10612201 iT邦新手 5 級 ‧ 2020-06-29 16:20:32 檢舉

謝謝你的回覆 很有幫助

0
xuanxuan
iT邦新手 5 級 ‧ 2020-10-10 22:02:06

您好,近期我剛接觸LSTM,關於LSTM的一些參數還是理解不懂
想請問LSTM是只接受三維數組的資料嗎?
所以如果我有一個二維陣列的資料的話要先reshape成三維再丟入LSTM
我看到您上面reshape的部分是在time_steps的位置加入1
請問這個用意是什麼意思呢?

另外還想請問LSTM中的input_shape()中的參數含意是代表什麼意思?
因為我看到有些例子他們會在input_shape()裡面放入三個參數,有些只會放兩個
所以想請教您這個問題,不好意思問題有點多。

看更多先前的回應...收起先前的回應...

LSTM是只接受三維數組的資料嗎?
==> 是,請參閱 https://keras.io/api/layers/recurrent_layers/lstm/
inputs: A 3D tensor with shape [batch, timesteps, feature].

time_steps的位置加入1
==> 每一期資料只跟前"一"期有關

input_shape
==> 設定模型輸入的維度大小,注意,不含第一維批量。

相關規格可參考 https://keras.io ,比較精準。
/images/emoticon/emoticon03.gif

xuanxuan iT邦新手 5 級 ‧ 2020-10-10 23:38:03 檢舉

所以time_steps的位置不一定每一次都是直接加入1嗎?

請問input_shape中 "注意,不含第一維批量" 這邊是指
如果輸入的參數是兩個的話就不包含Batch-size嗎?

還想請教一下[Batch-size, TimeSteps, Features]
分別指的是要訓練的資料的[總筆數, 時間維度, 特徵值]嗎?

全部都對喔,厲害。

xuanxuan iT邦新手 5 級 ‧ 2020-10-11 16:37:40 檢舉

那想請教一下
為什麼input_shape()裡面不需要放入Batch-size的參數?
是因為我們在後面model.fit的時侯會設定Batch-size嗎?

因為 Batch-size 是會變動的,與模型結構本身無關。

xuanxuan iT邦新手 5 級 ‧ 2020-10-11 21:50:32 檢舉

好的,了解,謝謝您耐心地回覆。
如果之後還有其他問題的話,可以寄站內簡訊詢問您嗎?

OK.

0
liu1257cc
iT邦新手 5 級 ‧ 2020-10-31 11:54:44

您好 想請教
我如果只想用測試集的前五個值做預測呢?
我試過以下這樣子似乎沒辦法...

for i in range(0,len(testX),step=5):
    testPredict = model.predict(testX[i:i+step])

因為我想要預測五個值[0:5]後再訓練,在預測五個值[6:10]該怎麼做呢?

看更多先前的回應...收起先前的回應...

要一次預測多期的目標,稱之為 Multi-Step LSTM Time Series Forecasting,請參閱這一篇說明:
https://machinelearningmastery.com/how-to-develop-lstm-models-for-multi-step-time-series-forecasting-of-household-power-consumption/

liu1257cc iT邦新手 5 級 ‧ 2020-11-01 15:00:27 檢舉

想請教 如果測試集為一期(裏頭有200個樣本)
我想先取5個做預測,然後再訓練模型在預測5個值,直到200個樣本結束這樣可以嗎?
這樣好像是單期的問題

也可以,只是每預測一期,就需重新訓練,預測速度過慢。

liu1257cc iT邦新手 5 級 ‧ 2020-11-02 11:46:31 檢舉

不好意思,想請教一下具體該怎麼做呢?
以下是我嘗試過的方式....

def create_dataset (X, look_back = 1):
    Xs, ys = [], []
    
    for i in range(len(X)-look_back):
        v = X[i:i+look_back]
        Xs.append(v)
        ys.append(X[i+look_back])
        #print('Xs: ', Xs)
        #print('ys: ', ys)
    return np.array(Xs), np.array(ys)
for i in range(0,len(X_test),5):   
        
        X_test1 = X_test[i:i+5]
        
        a = model.predict(X_test1, batch_size = batch_size)
        #print('a :',a)
        #print('a :',a.shape)
        
        prediction = np.array(a)
        prediction = scaler.inverse_transform(prediction)
        
        X_a, y_a = create_dataset(a,5)      
        #print('X_a :',X_a.shape)
        #print('y_a :',y_a.shape)
        
        new_model = model
        
        if X_a.shape[0] >= 5: #樣本數大於5 訓練
            new_history_lstm = new_model.fit(X_a, y_a, epochs = 1, validation_split = 0.2,
                    batch_size = batch_size, shuffle = False,callbacks = [early_stop]) 
        else:
            break

每預測一筆就append到資料集中,再重新訓練/預測即可。

0
Adamy
iT邦新手 5 級 ‧ 2020-11-02 12:21:49

您好,我剛接觸python和lstm,想請問一個問題,文章中的訓練資料設0.67,是將此data前面2/3筆做訓練資料,想請問我想將此data隨機2/3筆做訓練資料,該如何進行修改比較好,謝謝您
https://i.imgur.com/Mx7sLFl.png

liu1257cc iT邦新手 5 級 ‧ 2020-11-02 20:50:52 檢舉

雖然我不是很懂
但我想 你可以先random.seed()後再做0.67切割呢

前面2/3筆做訓練資料,是要以過去預測未來,若採隨機分割,就有可能變成以未來預測過去。

我要留言

立即登入留言