หลาย ๆ คนคงจะรู้จักการทำนายราคาหุ้นล่วงหน้ากันมาบ้างแล้ว โดยการใช้ Model Deep learning มาทำนายกัน โดยครั้งนี้ขอนำเสนอสินทรัพย์ดิจิตอลสุดร้อนแรงอย่าง Bitcoin และ Ethereum ด้วย LSTM Model โดยใช้ภาษา Python แต่จะขอนำเสนอในแง่มุมที่ไม่ลงลึกในตัว Math มาก เพราะผมก็ไม่เก่งเหมือนกัน แต่จะขอพูดคร่าว ๆ เพื่อให้เข้าใจและพอมองภาพออกกันครับ :)

ก่อนอื่นมาทำความรู้จักกับ LSTM กันก่อน

LSTM ย่อมาจาก Long Short-Term Memory จัดเป็น Recurrent Neural Networks (RNNs) แบบหนึ่ง โดยที่จะนำเอา Output ที่ได้ก่อนหน้านำมาใช้ในการคำนวณกับ Node ปัจจุบัน เพื่อทำนายข้อมูลถัดไป ซึ่งมีประโยชน์ในการนำมาใช้กับข้อมูลที่มีความต่อเนื่องที่มีลักษณะเป็น Times series โดยการให้ความสำคัญกับ Output รอบก่อนหน้าเพื่อนำมาเป็นข้อมูลประกอบในการวิเคราะห์รอบถัดไป เช่น ข้อมูลภาพ, เสียง, Text หรือแม้กระทั่งราคาหุ้น ที่เคลื่อนไหวโดยมีการใช้ข้อมูล Open, High, Low, Close มาประกอบด้วยนั่นเอง

RNNs loops.

LSTM Networks ล่ะทำงานยังไง

ผู้พัฒนารูปแบบการเรียนรู้นี้ชื่อว่า Hochreiter & Schmidhuber (1997) http://www.bioinf.jku.at/publications/older/2604.pdf สำหรับผู้สนใจสูตรสามารถอ่านเพิ่มเติมได้จาก Paper ฉบับนี้ โดยหลังจากพัฒนางานวิจัยออกมา LSTM ก็โด่งดังเป็นอย่างมาก ด้วยความที่เป็นรูปแบบการเรียนรู้ที่แก้ปัญหาในแง่ของข้อมูลที่มีความหลากหลายได้ดีเป็นอย่างมาก โดย LSTM สามารถแก้ปัญหาในเรื่องของการหลีกเลี่ยงความจำระยะยาวได้ ในบางครั้ง การที่นำข้อมูลในอดีตที่ย้อนหลังไปไกล ๆ แล้วนำมาคำนวณหรือสร้างการทำนาย ก็ไม่ได้เป็นตัวเลือกที่ดีในการแก้ปัญหากับข้อมูลบางชุดได้

โดยแนวคิดที่เพิ่มเข้ามาใน LSTM นอกจากการเพิ่มเส้นทางนำ Gradient แล้วยังประกอบไปด้วย

ข้อมูลบางข้อมูลก็ควรจะลืมไปบ้าง

ข้อมูลบางอย่างที่เป็น Noise ก็ไม่ควรนำมาพิจารณา

ข้อมูลบางอย่างควร Scale หรือ Filter ก่อนส่งออกไป

https://medium.com/@sanparithmarukatat/lstm-%E0%B9%80%E0%B8%97%E0%B9%88%E0%B8%B2%E0%B8%97%E0%B8%B5%E0%B9%88%E0%B9%80%E0%B8%82%E0%B9%89%E0%B8%B2%E0%B9%83%E0%B8%88-75027db3167f

ใน RNNs จะทำงานโดยมีการนำ output ก่อนหน้ามาใช้ในการคำนวณร่วมกับข้อมูลชุดปัจจุบัน ซึ่งขอยกตัวอย่างการทำงานของ RNNs ด้วย Activation function ตัว Tanh ดังภาพ

The repeating module in a standard RNN contains a single layer.

ส่วน LSTM จะแตกต่างจาก RNN ในส่วนของการ repeating module structure ในปกติ RNN จะมี Single neural network layer แต่ LSTM จะมีถึง 4 repeating module ที่เพิ่มเข้ามา ดังภาพ

The repeating module in an LSTM contains four interacting layers.

โดยสามารถจำแนกให้เห็นชัดลงไปอีกว่าในแต่ละ Neural Network Layer มีการนำไปใช้ต่ออย่างไร ดังภาพ

LSTM notation

จะเห็นว่าลูกศรจะมีค่า Vector ของตัวเองที่ได้มาจาก Output ก่อน ๆ และนำมาต่อเข้ากับ input ถัดไป และขั้นตอนสุดท้ายจะทำการ Copy ไปยัง Node อื่น ๆ ต่อไป


เอาล่ะ พูดถึงโมเดลที่นำมาใช้กับข้อมูลกันไปคร่าว ๆ ให้พอรู้ว่ามันทำงานอย่างไร ก็ถึงเวลาที่จะนำมาใช้กับข้อมูลให้เห็นภาพและผลลัพธ์กันบ้างครับ และตามที่เกริ่นไว้ ผมจะนำข้อมูลเหรียญ Cryptocurrency โดยเป็นเหรียญ Bitcoin และ Ethereum กันครับ

import pandas as pd
import time
import seaborn as sns
import matplotlib.pyplot as plt
import datetime
import numpy as np
# รับข้อมูล Bitcoin จากปี 2016 - ปัจจุบัน จากเว็บ coinmarketcap
bitcoin_market_info = pd.read_html("https://coinmarketcap.com/currencies/bitcoin/historical-data/?start=20130428&end="+time.strftime("%Y%m%d"))[0]
# แปลงข้อมูลตัวเลขให้อยู่ในรูปแบบวันที่ หากไม่ทำส่วนนี้โปรแกรมจะมองเห็นเป็นชุดตัวเลขปกติ
bitcoin_market_info = bitcoin_market_info.assign(Date=pd.to_datetime(bitcoin_market_info['Date']))
# แปลงข้อมูล Volume หากมีค่าเท่ากับ '-' ให้เปลี่ยนเป็นศูนย์ไปเลย
bitcoin_market_info.loc[bitcoin_market_info['Volume']=="-",'Volume']=0
# แปลงข้อมูล Volume ให้เป็นประเภท Int
bitcoin_market_info['Volume'] = bitcoin_market_info['Volume'].astype('int64')
# ในบางครั้งข้อมูลที่เรียกมาอาจมีการติดเครื่องหมาย * มาด้วย โค้ดด้านล่างจะทำการลบ * ออกครับ
bitcoin_market_info.columns = bitcoin_market_info.columns.str.replace("*", "")
# จากนั้นทำการเรียกดูข้อมูลที่เราดึงมาครับ ว่าถูกต้องหรือเปล่า แนะนำว่าควรทำทุกครั้งครับ
bitcoin_market_info.head()

หลังจากนั้นจะได้ข้อมูลที่เราเรียกมาตามนี้ครับ

ข้อมูลราคา Bitcoin ที่เราเรียกมาจาก Code ด้านบน
# รับข้อมูล Ethereum จากปี 2016 - ปัจจุบัน จากเว็บ coinmarketcap
eth_market_info = pd.read_html("https://coinmarketcap.com/currencies/ethereum/historical-data/?start=20130428&end="+time.strftime("%Y%m%d"))[0]
# แปลงข้อมูล date ที่เป็น str ให้อยู่ในรูปแบบ datetime ที่ถูกต้อง
eth_market_info = eth_market_info.assign(Date=pd.to_datetime(eth_market_info['Date']))
# เหมือนเดิมครับ ลบเครื่องหมาย '*' ที่อาจติดมาด้วย
eth_market_info.columns = eth_market_info.columns.str.replace("*", "")
# เรียกดูข้อมูลว่าถูกต้องหรือไม่
eth_market_info.head()
ข้อมูลราคา Ethereum ที่เราเรียกมาจาก Code ด้านบน

จากนั้นเพื่อป้องกันการสับสน ผมทำการเพิ่มคำว่า bt เข้าไปในข้อมูลราคาที่มาจาก Bitcoin และทำเช่นเดียวกับ Ethereum โดยตั้งว่า eth ครับ

bitcoin_market_info.columns =[bitcoin_market_info.columns[0]]+['bt_'+i for i in bitcoin_market_info.columns[1:]]
eth_market_info.columns =[eth_market_info.columns[0]]+['eth_'+i for i in eth_market_info.columns[1:]]

แล้วทำการเรียกดูอีกครั้ง

bitcoin_market_info.head()
ราคา Bitcoin ที่ใส่ชื่อ bt เพิ่มเข้าไปบนคอลัมน์

จากนั้นมาทำการ Plot กราฟ Bitcoin กันครับ

fig, (ax1, ax2) = plt.subplots(2,1)
ax1.set_ylabel('Closing Price ($)',fontsize=12)
ax2.set_ylabel('Volume ($ bn)',fontsize=12)
ax2.set_yticks([int('%d000000000'%i) for i in range(10)])
ax2.set_yticklabels(range(10))
ax1.set_xticks([datetime.date(i,j,1) for i in range(2013,2019) for j in [1,7]])
ax1.set_xticklabels('')
ax2.set_xticks([datetime.date(i,j,1) for i in range(2013,2019) for j in [1,7]])
ax2.set_xticklabels([datetime.date(i,j,1).strftime('%b %Y') for i in range(2013,2019) for j in [1,7]])
ax1.plot(bitcoin_market_info['Date'].astype(datetime.datetime),bitcoin_market_info['bt_Open'])
ax2.bar(bitcoin_market_info['Date'].astype(datetime.datetime).values, bitcoin_market_info['bt_Volume'].values)
fig.tight_layout()
plt.show()
กราฟราคา Bitcoin ที่เราจะได้ออกมาครับ

หลังจากที่เราเรียกดูความถูกต้องแล้ว ขั้นต่อไปเพื่อให้เราสามารถเรียกข้อมูลได้ทั้ง Btc และ Eth โดยข้อมูลจะอยู่บนวันเวลาเดียวกันด้วย และใช้คำสั่ง kwargs เพื่อส่ง input โดยมีการตั้งชื่อให้ด้วย **kwargs จะเหมาะมากถ้าเราต้องการใช้งาน input ที่ส่งเข้ามาในฟังก์ชันด้วยชื่อที่ส่งเข้ามา ซึ่งทำให้เราจำง่ายขึ้นด้วยครับ

market_info = pd.merge(bitcoin_market_info,eth_market_info, on=['Date'])
market_info = market_info[market_info['Date']>='2016-01-01']
for coins in ['bt_', 'eth_']:
kwargs = { coins+'day_diff': lambda x: (x[coins+'Close']-x[coins+'Open'])/x[coins+'Open']}
market_info = market_info.assign(**kwargs)
market_info.head()
ข้อมูลที่เราได้หลังจากรวมกันแล้ว จะแสดงค่าสกุลดิจิตอลพร้อมกันเลยครับ

จากนั้นเราจะมาแบ่งข้อมูลออกเป็น 2 ชุดเพื่อใช้ในการ Train และ Test โดยโมเดลจะเรียนรู้จากและสร้างโมเดลจากข้อมูล Train เพื่อนำการเรียนรู้จากข้อมูลดังกล่าวไปทำนายข้อมูลที่เราแบ่งไว้ Test ในที่นี้ผมแบ่งหลังช่วงวันที่ 2017–06–01 เป็นต้นไปครับ

split_date = '2017-06-01'
fig, (ax1, ax2) = plt.subplots(2,1, figsize = (10, 7))
ax1.set_xticks([datetime.date(i,j,1) for i in range(2013,2019) for j in [1,7]])
ax1.set_xticklabels('')
ax2.set_xticks([datetime.date(i,j,1) for i in range(2013,2019) for j in [1,7]])
ax2.set_xticklabels([datetime.date(i,j,1).strftime('%b %Y') for i in range(2013,2019) for j in [1,7]])
ax1.plot(market_info[market_info['Date'] < split_date]['Date'].astype(datetime.datetime),
market_info[market_info['Date'] < split_date]['bt_Close'],
color='#B08FC7', label='Training')
ax1.plot(market_info[market_info['Date'] >= split_date]['Date'].astype(datetime.datetime),
market_info[market_info['Date'] >= split_date]['bt_Close'],
color='#8FBAC8', label='Test')
ax2.plot(market_info[market_info['Date'] < split_date]['Date'].astype(datetime.datetime),
market_info[market_info['Date'] < split_date]['eth_Close'],
color='#B08FC7')
ax2.plot(market_info[market_info['Date'] >= split_date]['Date'].astype(datetime.datetime),
market_info[market_info['Date'] >= split_date]['eth_Close'], color='#8FBAC8')
ax1.set_xticklabels('')
ax1.set_ylabel('Bitcoin Price ($)',fontsize=12)
ax2.set_ylabel('Ethereum Price ($)',fontsize=12)
plt.tight_layout()
ax1.legend(bbox_to_anchor=(0.03, 1), loc=2, borderaxespad=0., prop={'size': 14})
plt.show()
ภาพการแบ่งชุดข้อมูลออกตามสีของช่วงเวลาดังกล่าว

เอาล่ะครับ เมื่อเราได้ข้อมูลตามที่ต้องการแล้ว ขั้นต่อไปเราจะเริ่มลง Model LSTM กัน ซึ่งในส่วนของรายละเอียดโมเดลผมยังไม่ขออธิบายตรงนี้ แต่จะแปะลิงค์อ้างอิงไว้ให้ในช่วงท้าย โดยต่อจากนี้จะพาไป Set การขึ้น Model และพาไปทดสอบกันดูครับ หากพร้อมแล้ว ลุยกันเลยครับ :)

# ทำการตั้งค่าตัวแปร Market_info โดยนำค่าที่ต้องการมาไว้ด้วยกัน
for coins in ['bt_', 'eth_']:
kwargs = { coins+'close_off_high': lambda x: 2*(x[coins+'High']- x[coins+'Close'])/(x[coins+'High']-x[coins+'Low'])-1,
coins+'volatility': lambda x: (x[coins+'High']- x[coins+'Low'])/(x[coins+'Open'])}
market_info = market_info.assign(**kwargs)
# จากนั้นทำการตั้งค่า Model โดยทำการ set metric ที่จะใช้ในโมเดลด้วย
model_data = market_info[['Date']+[coin+metric for coin in ['bt_', 'eth_'] 
for metric in ['Close','Volume','close_off_high','volatility']]]
model_data = model_data.sort_values(by='Date')
model_data.head()

และเมื่อ plot ออกมาดูจะได้ข้อมูลที่พร้อมส่งเข้าไปในโมเดลแบบนี้

# หลังจากนี้เราไม่ต้องใช้คอลัมน์ Date แล้ว ก็เอาออกได้ครับ
training_set, test_set = model_data[model_data['Date']<split_date], model_data[model_data['Date']>=split_date]
training_set = training_set.drop('Date', 1)
test_set = test_set.drop('Date', 1)

และกำหนดว่า จะใช้ข้อมูล 10 วันก่อนหน้าสำหรับการเรียนรู้ของโมเดลครับ

window_len = 10
norm_cols = [coin+metric for coin in ['bt_', 'eth_'] for metric in ['Close','Volume']]

จากนี้มากำหนด Training input และ Testing input กันครับ

LSTM_training_inputs = []
for i in range(len(training_set)-window_len):
temp_set = training_set[i:(i+window_len)].copy()
for col in norm_cols:
temp_set.loc[:, col] = temp_set[col]/temp_set[col].iloc[0] - 1
LSTM_training_inputs.append(temp_set)
LSTM_training_outputs = (training_set['eth_Close'][window_len:].values/training_set['eth_Close'][:-window_len].values)-1
LSTM_test_inputs = []
for i in range(len(test_set)-window_len):
temp_set = test_set[i:(i+window_len)].copy()
for col in norm_cols:
temp_set.loc[:, col] = temp_set[col]/temp_set[col].iloc[0] - 1
LSTM_test_inputs.append(temp_set)
LSTM_test_outputs = (test_set['eth_Close'][window_len:].values/test_set['eth_Close'][:-window_len].values)-1

จากนั้นเราจะเรียกข้อมูลเพื่อแสดงตัวอย่างของ LSTM model inputs หลังจากที่เราได้ Normalised บางคอลัมน์บ้างแล้ว ทำให้บางคอลัมน์จะมีค่าเป็น ‘ 0 ’ ในจุดแรกซึ่งเราจะใช้จุดนี้เป็นจุดที่ใช้ในการทำนายการเปลี่ยนแปลงของราคาที่มีความสัมพันธ์กับ Time point นี้ด้วยครับ

LSTM_traning_inputs[0]
# ผมขอใช้ np.array ในการจัดการกับ input แทน pandas ครับ
# data ต่อจากนี้จะเป็น numeric หมดแล้วครับ
LSTM_training_inputs = [np.array(LSTM_training_input) for LSTM_training_input in LSTM_training_inputs]
LSTM_training_inputs = np.array(LSTM_training_inputs)
LSTM_test_inputs = [np.array(LSTM_test_inputs) for LSTM_test_inputs in LSTM_test_inputs]
LSTM_test_inputs = np.array(LSTM_test_inputs)

Tools ที่ผมใช้ทำงานกับ LSTM ผมจะใช้เป็น Keras นะครับ Code ตามด้านล่างได้เลยครับ

# import Keras modules
from keras.models import Sequential
from keras.layers import Activation, Dense
from keras.layers import LSTM
from keras.layers import Dropout
def build_model(inputs, output_size, neurons, activ_func = "linear",
dropout =0.25, loss="mae", optimizer="adam"):
model = Sequential()
model.add(LSTM(neurons, input_shape=(inputs.shape[1], inputs.shape[2])))
model.add(Dropout(dropout))
model.add(Dense(units=output_size))
model.add(Activation(activ_func))
model.compile(loss=loss, optimizer=optimizer)
return model
# random seed for reproducibility
np.random.seed(202)
# ตั้งค่า model architecture โดยกำหนด output_size และมี 20 Neurons ที่ทำงานอยู่เบื้องหลังครับ
eth_model = build_model(LSTM_training_inputs, output_size=1, neurons = 20)
# model output is next price normalised to 10th previous closing price
LSTM_training_outputs = (training_set['eth_Close'][window_len:].values/training_set['eth_Close'][:-window_len].values)-1
# train model on data
# Set 50 epoch ในการรัน เป็นค่าที่ผมรันดูแล้วเบื้องต้นพบว่ามันโอเคครับ
eth_history = eth_model.fit(LSTM_training_inputs, LSTM_training_outputs,
epochs=50, batch_size=1, verbose=2, shuffle=True)

หลังจาก Run เสร็จเรียบร้อยเราจะเห็นว่าแต่ละ Epoch มีการแสดงค่า Loss จากการเรียนรู้ของโมเดลออกมาให้เห็นด้วย หรือจะสามารถ plot ออกมาให้แสดงเป็นค่า Mean absolute error (MAE) ได้จากโค้ดถัดไปครับ

fig, ax1 = plt.subplots(1,1, figsize = (10,7))
ax1.plot(eth_history.epoch, eth_history.history['loss'])
ax1.set_title('Training Error')
if eth_model.loss == 'mae':
ax1.set_ylabel('Mean Absolute Error (MAE)',fontsize=12)
# just in case you decided to change the model loss calculation
else:
ax1.set_ylabel('Model Loss',fontsize=12)
ax1.set_xlabel('# Epochs',fontsize=12)
plt.show()

จะพบค่า MAE ว่า หลังจาก epoch ตั้งแต่ 30 เป็นต้นไปค่า MAE เริ่มลดลงและนิ่งอยู่แถว ๆ 0.6x ไปจนถึง epoch ที่ 50

พอเราได้ค่า MAE มาแล้ว เราจะมา Plot ดูว่ามันทำการเรียนรู้อย่างไร จากค่า MAE ที่ได้จากการเรียนรู้ traning dataset มา 50 Epochs

from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
fig, ax1 = plt.subplots(1,1, figsize = (10, 7))
ax1.set_xticks([datetime.date(i,j,1) for i in range(2013,2019) for j in [1,5,9]])
ax1.set_xticklabels([datetime.date(i,j,1).strftime('%b %Y') for i in range(2013,2019) for j in [1,5,9]])
ax1.plot(model_data[model_data['Date']< split_date]['Date'][window_len:].astype(datetime.datetime),
training_set['eth_Close'][window_len:], label='Actual')
ax1.plot(model_data[model_data['Date']< split_date]['Date'][window_len:].astype(datetime.datetime),
((np.transpose(eth_model.predict(LSTM_training_inputs))+1) * training_set['eth_Close'].values[:-window_len])[0],
label='Predicted')
ax1.set_title('Training Set: Single Timepoint Prediction')
ax1.set_ylabel('Ethereum Price ($)',fontsize=12)
ax1.legend(bbox_to_anchor=(0.15, 1), loc=2, borderaxespad=0., prop={'size': 14})
ax1.annotate('MAE: %.4f'%np.mean(np.abs((np.transpose(eth_model.predict(LSTM_training_inputs))+1)-\
(training_set['eth_Close'].values[window_len:])/(training_set['eth_Close'].values[:-window_len]))),
xy=(0.75, 0.9), xycoords='axes fraction',
xytext=(0.75, 0.9), textcoords='axes fraction')
axins = zoomed_inset_axes(ax1, 3.35, loc=10)
axins.set_xticks([datetime.date(i,j,1) for i in range(2013,2019) for j in [1,5,9]])
axins.plot(model_data[model_data['Date']< split_date]['Date'][window_len:].astype(datetime.datetime),
training_set['eth_Close'][window_len:], label='Actual')
axins.plot(model_data[model_data['Date']< split_date]['Date'][window_len:].astype(datetime.datetime),
((np.transpose(eth_model.predict(LSTM_training_inputs))+1) * training_set['eth_Close'].values[:-window_len])[0],
label='Predicted')
axins.set_xlim([datetime.date(2017, 3, 1), datetime.date(2017, 5, 1)])
axins.set_ylim([10,60])
axins.set_xticklabels('')
mark_inset(ax1, axins, loc1=1, loc2=3, fc="none", ec="0.5")
plt.show()

ผมใช้ Zoom insert เพื่อให้เห็นภาพชัดยิ่งขึ้นด้วย ตามนี้ครับ

ทั้งนี้ทั้งนั้นการจะทำให้ค่า MAE เข้าใกล้ 0 สำหรับการ Train นั้นทำได้ โดยเราอาจทำการปรับ Epoch เพิ่มขึ้น หรือปรับ Neurons และ train บ่อย ๆ หลาย ๆ รอบจนมันจำได้ หรือที่เรียกว่า Overfit นั่นเอง แต่ผมขอข้ามในส่วนนั้นและพาไปดู Test dataset ว่าโมเดลที่ได้ทำนายออกมาอย่างไรกันครับ

fig, ax1 = plt.subplots(1,1, figsize = (15, 7))
ax1.set_xticks([datetime.date(2017,i+1,1) for i in range(12)])
ax1.set_xticklabels([datetime.date(2017,i+1,1).strftime('%b %d %Y') for i in range(12)])
ax1.plot(model_data[model_data['Date']>= split_date]['Date'][window_len:].astype(datetime.datetime),
test_set['eth_Close'][window_len:], label='Actual')
ax1.plot(model_data[model_data['Date']>= split_date]['Date'][window_len:].astype(datetime.datetime),
((np.transpose(eth_model.predict(LSTM_test_inputs))+1) * test_set['eth_Close'].values[:-window_len])[0],
label='Predicted')
ax1.annotate('MAE: %.4f'%np.mean(np.abs((np.transpose(eth_model.predict(LSTM_test_inputs))+1)-\
(test_set['eth_Close'].values[window_len:])/(test_set['eth_Close'].values[:-window_len]))),
xy=(0.75, 0.9), xycoords='axes fraction',
xytext=(0.75, 0.9), textcoords='axes fraction')
ax1.set_title('Test Set: Single Timepoint Prediction',fontsize=13)
ax1.set_ylabel('Ethereum Price ($)',fontsize=12)
ax1.legend(bbox_to_anchor=(0.1, 1), loc=2, borderaxespad=0., prop={'size': 14})
plt.show()

มาดูกับฝั่ง Bitcoin กันบ้างครับ เริ่มจากการรันกันอีกรอบ

# random seed for reproducibility
np.random.seed(202)
# initialise model architecture
bt_model = build_model(LSTM_training_inputs, output_size=1, neurons = 20)
# train model on data
bt_history = bt_model.fit(LSTM_training_inputs,
(training_set['bt_Close'][window_len:].values/training_set['bt_Close'][:-window_len].values)-1,
epochs=50, batch_size=1, verbose=2, shuffle=True)

จากนั้นมา plot MAE ดูกันต่อ

fig, ax1 = plt.subplots(1,1, figsize = (10,7))
ax1.plot(bt_history.epoch, bt_history.history['loss'])
ax1.set_title('Training Error')
if bt_model.loss == 'mae':
ax1.set_ylabel('Mean Absolute Error (MAE)',fontsize=12)
# ในกรณีที่ต้องการเปลี่ยนเป็น model loss calculation
else:
ax1.set_ylabel('Model Loss',fontsize=12)
ax1.set_xlabel('# Epochs',fontsize=12)
plt.show()

ลองส่องดูตอน Train ของ BTC กันครับ

from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
fig, ax1 = plt.subplots(1,1, figsize = (15, 9))
ax1.set_xticks([datetime.date(i,j,1) for i in range(2013,2019) for j in [1,5,9]])
ax1.set_xticklabels([datetime.date(i,j,1).strftime('%b %Y') for i in range(2013,2019) for j in [1,5,9]])
ax1.plot(model_data[model_data['Date']< split_date]['Date'][window_len:].astype(datetime.datetime),
training_set['bt_Close'][window_len:], label='Actual')
ax1.plot(model_data[model_data['Date']< split_date]['Date'][window_len:].astype(datetime.datetime),
((np.transpose(bt_model.predict(LSTM_training_inputs))+1) * training_set['bt_Close'].values[:-window_len])[0],
label='Predicted')
ax1.set_title('Training Set: Single Timepoint Prediction')
ax1.set_ylabel('Bitcoin Price ($)',fontsize=12)
ax1.annotate('MAE: %.4f'%np.mean(np.abs((np.transpose(bt_model.predict(LSTM_training_inputs))+1)-\
(training_set['bt_Close'].values[window_len:])/(training_set['bt_Close'].values[:-window_len]))),
xy=(0.75, 0.9), xycoords='axes fraction',
xytext=(0.75, 0.9), textcoords='axes fraction')
ax1.legend(bbox_to_anchor=(0.1, 1), loc=2, borderaxespad=0., prop={'size': 14})
# figure inset code taken from http://akuederle.com/matplotlib-zoomed-up-inset
axins = zoomed_inset_axes(ax1, 2.52, loc=10, bbox_to_anchor=(400, 307)) # zoom-factor: 2.52, location: centre
axins.set_xticks([datetime.date(i,j,1) for i in range(2013,2019) for j in [1,5,9]])
axins.plot(model_data[model_data['Date']< split_date]['Date'][window_len:].astype(datetime.datetime),
training_set['bt_Close'][window_len:], label='Actual')
axins.plot(model_data[model_data['Date']< split_date]['Date'][window_len:].astype(datetime.datetime),
((np.transpose(bt_model.predict(LSTM_training_inputs))+1) * training_set['bt_Close'].values[:-window_len])[0],
label='Predicted')
axins.set_xlim([datetime.date(2017, 2, 15), datetime.date(2017, 5, 1)])
axins.set_ylim([920, 1400])
axins.set_xticklabels('')
mark_inset(ax1, axins, loc1=1, loc2=3, fc="none", ec="0.5")
plt.show()

และต่อด้วย Test dataset

fig, ax1 = plt.subplots(1,1, figsize = (10, 7))
ax1.set_xticks([datetime.date(2017,i+1,1) for i in range(12)])
ax1.set_xticklabels([datetime.date(2017,i+1,1).strftime('%b %d %Y') for i in range(12)])
ax1.plot(model_data[model_data['Date']>= split_date]['Date'][10:].astype(datetime.datetime),
test_set['bt_Close'][window_len:], label='Actual')
ax1.plot(model_data[model_data['Date']>= split_date]['Date'][10:].astype(datetime.datetime),
((np.transpose(bt_model.predict(LSTM_test_inputs))+1) * test_set['bt_Close'].values[:-window_len])[0],
label='Predicted')
ax1.annotate('MAE: %.4f'%np.mean(np.abs((np.transpose(bt_model.predict(LSTM_test_inputs))+1)-\
(test_set['bt_Close'].values[window_len:])/(test_set['bt_Close'].values[:-window_len]))),
xy=(0.75, 0.9), xycoords='axes fraction',
xytext=(0.75, 0.9), textcoords='axes fraction')
ax1.set_title('Test Set: Single Timepoint Prediction',fontsize=13)
ax1.set_ylabel('Bitcoin Price ($)',fontsize=12)
ax1.legend(bbox_to_anchor=(0.1, 1), loc=2, borderaxespad=0., prop={'size': 14})
plt.show()

ทีนี้เราจะมาทำให้ Model ทำนายราคาหุ้นใน 5 วันถัดไปกันครับเริ่มจาก ETH กันก่อน แล้วตามด้วย BTC เหมือนเดิมครับ

# random seed for reproducibility
np.random.seed(202)
# we'll try to predict the closing price for the next 5 days
# change this value if you want to make longer/shorter prediction
pred_range = 5
# initialise model architecture
eth_model = build_model(LSTM_training_inputs, output_size=pred_range, neurons = 20)
# model output is next 5 prices normalised to 10th previous closing price
LSTM_training_outputs = []
for i in range(window_len, len(training_set['eth_Close'])-pred_range):
LSTM_training_outputs.append((training_set['eth_Close'][i:i+pred_range].values/
training_set['eth_Close'].values[i-window_len])-1)
LSTM_training_outputs = np.array(LSTM_training_outputs)
# train model on data
# note: eth_history contains information on the training error per epoch
eth_history = eth_model.fit(LSTM_training_inputs[:-pred_range], LSTM_training_outputs,
epochs=50, batch_size=1, verbose=2, shuffle=True)
# random seed for reproducibility
np.random.seed(202)
# we'll try to predict the closing price for the next 5 days
# change this value if you want to make longer/shorter prediction
pred_range = 5
# initialise model architecture
bt_model = build_model(LSTM_training_inputs, output_size=pred_range, neurons = 20)
# model output is next 5 prices normalised to 10th previous closing price
LSTM_training_outputs = []
for i in range(window_len, len(training_set['bt_Close'])-pred_range):
LSTM_training_outputs.append((training_set['bt_Close'][i:i+pred_range].values/
training_set['bt_Close'].values[i-window_len])-1)
LSTM_training_outputs = np.array(LSTM_training_outputs)
# train model on data
# note: eth_history contains information on the training error per epoch
bt_history = bt_model.fit(LSTM_training_inputs[:-pred_range], LSTM_training_outputs,
epochs=50, batch_size=1, verbose=2, shuffle=True)
# ในส่วนนี้จะเป็นการปรับปรุงตัวผลทำนายออกมาเป็นสีและรูปแบบที่ดูง่ายขึ้นครับ
eth_pred_prices = ((eth_model.predict(LSTM_test_inputs)[:-pred_range][::pred_range]+1)*\
test_set['eth_Close'].values[:-(window_len + pred_range)][::5].reshape(int(np.ceil((len(LSTM_test_inputs)-pred_range)/float(pred_range))),1))
bt_pred_prices = ((bt_model.predict(LSTM_test_inputs)[:-pred_range][::pred_range]+1)*\
test_set['bt_Close'].values[:-(window_len + pred_range)][::5].reshape(int(np.ceil((len(LSTM_test_inputs)-pred_range)/float(pred_range))),1))
pred_colors = ["#FF69B4", "#5D6D7E", "#F4D03F","#A569BD","#45B39D"]
fig, (ax1, ax2) = plt.subplots(2,1, figsize = (20, 9))
ax1.set_xticks([datetime.date(2017,i+1,1) for i in range(12)])
ax2.set_xticks([datetime.date(2017,i+1,1) for i in range(12)])
ax2.set_xticklabels([datetime.date(2017,i+1,1).strftime('%b %d %Y') for i in range(12)])
ax1.plot(model_data[model_data['Date']>= split_date]['Date'][window_len:].astype(datetime.datetime),
test_set['bt_Close'][window_len:], label='Actual')
ax2.plot(model_data[model_data['Date']>= split_date]['Date'][window_len:].astype(datetime.datetime),
test_set['eth_Close'][window_len:], label='Actual')
for i, (eth_pred, bt_pred) in enumerate(zip(eth_pred_prices, bt_pred_prices)):

if i<5:
ax1.plot(model_data[model_data['Date']>= split_date]['Date'][window_len:].astype(datetime.datetime)[i*pred_range:i*pred_range+pred_range],
bt_pred, color=pred_colors[i%5], label="Predicted")
else:
ax1.plot(model_data[model_data['Date']>= split_date]['Date'][window_len:].astype(datetime.datetime)[i*pred_range:i*pred_range+pred_range],
bt_pred, color=pred_colors[i%5])
ax2.plot(model_data[model_data['Date']>= split_date]['Date'][window_len:].astype(datetime.datetime)[i*pred_range:i*pred_range+pred_range],
eth_pred, color=pred_colors[i%5])
ax1.set_title('Test Set: 5 Timepoint Predictions',fontsize=13)
ax1.set_ylabel('Bitcoin Price ($)',fontsize=12)
ax1.set_xticklabels('')
ax2.set_ylabel('Ethereum Price ($)',fontsize=12)
ax1.legend(bbox_to_anchor=(0.13, 1), loc=2, borderaxespad=0., prop={'size': 12})
fig.tight_layout()
plt.show()

กราฟแสดงผลการ Predicted จะออกมาลักษณะนี้ครับ

ผลทำนายที่ออกมาทำให้เห็นว่าการทำนายในรูปแบบ LSTM ไม่ได้ทำนายราคาจาก Pattern แบบทิศทางเดียว และเมื่อย้อนกลับไปดูในการทำนายแบบ single point predictions พบว่าโมเดลก็สามารถทำงานได้ดี แต่ LSTM เมื่อลองปรับ random seed ดูแล้ว พบว่ามีความอ่อนไหวเป็นอย่างมาก ซึ่งเป็นที่มาของ Model weight intially randomly assigned นั่นเอง

และเพื่อทำการเปรียบเทียบโมเดล ผมจะรัน Random seed เพิ่มอีก 25 โดยทดสอบระหว่าง 775–800 random seed เพื่อประเมิณ Model error โดยค่า Error จะถูกคำนวณจากค่า Absolute difference ระหว่าง ราคาปิดของ Actual และ Predicted บนข้อมูล Test set นั่นเอง


โดยตัวโมเดลที่รันทดสอบ ผมทำไว้เรียบร้อยแล้ว และผมได้ทำการทดสอบกับตัวโมเดล Random Walk ไว้ด้วยเพื่อจะได้เทียบกันอย่างชัดเจนมากขึ้น เนื่องจากราคาสินค้านั้นเป็น Random อยู่แล้วโดยผลที่ได้ออกมามีลักษณะดังนี้ครับ

กราฟได้แสดงค่า Error จากการทดสอบ test set 25 Random seed ที่ต่างกันของแต่ละ model. โดย LSTM แสดงค่า average error อยู่ที่ 0.04 และ 0.05 จาก Bitcoin และ Ethereum, ซึ่งแสดงให้เห็นว่ามีค่าที่ดีกว่าโมเดลแบบ Random walk มากเลยทีเดียว


สำหรับผู้ที่ต้องการศึกษาเพิ่มเติม ผมแปะลิงค์ไว้ด้านล่างให้แล้ว และหวังว่าจะมีผู้สนใจนำไปศึกษาต่อและนำมาแลกเปลี่ยนหรือแชร์ความรู้กันในอนาคตครับ

ทั้งนี้ทั้งนั้น บทความนี้ไม่ได้เป็นการชี้นำราคาสินค้าดังกล่าวแต่อย่างใด เป็นเพียงการเสนอความเห็นผ่านการทดสอบคร่าว ๆ เพียงเท่านั้น


อ้างอิง :

https://dashee87.github.io/deep%20learning/python/predicting-cryptocurrency-prices-with-deep-learning/

Source: Deep Learning on Medium