บทความนี้ เราจะมาลองใช้ Machine Learning ตัวที่ขึ้นชื่อว่าดีมากอีกตัวหนึ่งคือ Random Forest มาช่วยในการทำนายการเปลี่ยนแปลงของราคาหุ้นกัน ระหว่างที่เรากำลังจัดทำเนื้อหาคอร์สใหม่ ที่ปัจจุบันเสร็จไปแล้วกำลังอยู่ในขั้นตอนการอัดวีดีโอนะครับ และในคอร์สนั้นเราก็มีการใช้ Random Forest ในการทำนายราคาหลักทรัพย์เช่นกัน วันนี้เราเลยเอามาแปะให้เห็นภาพแบบคร่าวๆก่อนนะครับ (เนื้อหาในคอร์สลึกว่านี้เยอะมาก)

ทำไมต้อง Random Forest
Random Forest เป็นอีกหนึ่งอัลกอริทึ่มที่ได้รับการยอมรับกันอย่างแพร่หลาย และทำผลงานได้ค่อนข้างดี ในขณะที่ตัวโมเดลเองก็ไม่ได้ซับซ้อนจนเกินไป Random Forest เป็นอัลกอริทึ่มที่พัฒนาต่อยอดจากอัลกอริทึ่มพื้นฐานอย่าง Decision Tree ที่มีการใช้ต้นไม้ในการตัดสินใจ โดยในการสร้างต้นไม้ที่ใช้ในการตัดสินใจขึ้นนั้น จะอาศัยหลักการสร้างกฏสำหรับการแบ่งต้นไม้ออกเป็นกิ่งๆ และจัดให้ข้อมูลที่มีเงื่อนไขตรงตามที่กำหนดถูกจัดอยู่ในใบของแต่ละกิ่งที่ได้รับการแบ่งข้างต้น
Decision Tree ถือเป็นอัลกอริทึ่มที่พยายามจัดการกับข้อมูลด้วยการสร้างต้นไม้เพียงแต่ 1 ต้น เท่านั้น ทำไม่ยังไม่สามารถจัดการกับข้อมูลที่ซับซ้อนได้อย่างมีประสิทธิภาพ ดังนั้น จึงมีการคิดค้นอัลกอริทึ่มที่ประกอบไปด้วยต้นไม้มากกว่า 1 ต้นในการตัดสินใจ คือ Random Forest หรือ “ป่า” ที่ใช้ในการตัดสินใจ ขึ้นมา ซึ่งในการทำงานของ Random Forest นี้ จะมีการสร้างต้นไม้ขึ้นมามากกว่า 1 ต้น และให้ต้นไม้หลายๆต้น ทำงานร่วมกัน โดยอาศัยหลักการของ Ensemble Model หรือ หลักการรวมโมเดลเข้าด้วยกัน เข้ามาช่วย ทำให้ผลลัพธ์ที่ได้จาก Random Forest นั้น มีประสิทธิภาพมากกว่า Decision Tree อย่างมีนัยสำคัญ

ถึงแม้ว่า Random Forest จะสามารถให้ผลลัพธ์ในการทำงานได้ดีกว่า Decision Tree แต่ก็ไม่ได้หมายความว่าผลลัพธ์ที่ดีนั้น จะดีจริงๆ เนื่องจาก การใช้งาน Random Forest มีความเสี่ยงต่อการ Overfit กับข้อมูลสูง ทำให้เหตุการที่ สามารถมาพัฒนาโมเดลที่ได้ผลลัพธ์จากการสอนดีมาก แต่เมื่อนำโมเดลนั้นมาใช้งานจริง กลับทำงานได้แย่มาก หรือ ที่เรียกว่า ปัญหา Overfitting เกิดขึ้นให้เห็นอยู่บ่อยครั้ง ดังนั้น ข้อควรระวังในการใช้งาน Random Forest ก็คือ การระมัดระวังไม่ให้โมเดลเกิด Overfitting นั่นเอง

เกริ่นนำเรื่องของ Random Forest กันมาพอสมควรแล้ว มาเข้าเรื่องการทดลองใช้ Random Forest ในการทำนายราคาหุ้นอย่างง่ายๆ
ภาพรวมของระบบ
ก่อนที่จะเข้าสู่ขั้นตอนต่างๆ เดี๋ยวเรามาดูภาพรวมของระบบนี้กันก่อนดีกว่า

การทดลองที่จะทำ มีภาพรวมง่ายๆ เริ่มต้นจากการดึงข้อมูล และจัดการเตรียมข้อมูลในเบื้องต้น จากนั้นทำการแบ่งข้อมูลออกเป็นข้อมูลสำหรับ สอน และ ทดสอบ หรือที่เรียกว่า Training set และ Testing set ตามลำดับ จากนั้นจึงทำการสร้างโมเดล Random Forest ขึ้น ซึ่งในส่วนนี้จะมีรายละเอียดค่อนข้างมาก เนื่องจากการพัฒนาโมเดลที่เหมาะสมจะต้องอาศัยการปรับแต่งโมเดลให้ดีที่สุด หรือ ที่เรียกว่า กระบวนการ Optimization ด้วย เมื่อได้โมเดลที่เหมาะสมแล้ว จึงทำการสอนโมเดลนั้น เมื่อเสร็จกระบวนการสอนเรียบร้อยแล้วจึงจะเข้าสู่การทดสอบ หรือ การนำโมเดลมาใช้งานกับข้อมูลที่เข้ามาใหม่ นั่นเอง
1. Import Libraries ที่จำเป็น
เรียก Library ที่จำเป็นในการใช้งานมาเตรียมไว้ก่อน เช่น pandas, numpy และ matplotlib นอกจากนั้น ยังทำการเรียก Library “yfinance” คือ Library สำหรับดึงข้อมูลจาก Yahoo Finance
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
2. ดึงข้อมูลหุ้นที่เราสนใจ
โดยเราจะทดลองดึงข้อมูลหุ้น 7 ตัว และ GSPC (S&P 500 index) เป็นเป้าหมายในการทำนาย ใช้ตั้งแต่ปี 2006 มาจนถึงปี 2022 จริงๆในขั้นนี้เราอาจจะใช้หุ้นมากกว่านี้ก็ได้ เพราะเราจะนำมันมาเป็น Feature ในการทำนายราคาของ GSPC
symbol_list = ["GOOG", "IBM", "MSFT", "AAPL", "V", "MMM", "BA", "^GSPC"]
df = yf.download(symbol_list, start="2006-01-01")["Adj Close"]
df.plot()
plt.grid()

ปัญหาที่เห็นคือเราดูไม่เห็นราคาของหุ้นหลายๆตัวเลยเพราะ Scale ของราคามันต่างกันมาก เรามาลองแปลงให้มันเป็น Scale เดียวกันซักหน่อย ด้วยการพล็อตค่าของราคาที่เปลี่ยนแปลงไปแทน
df.pct_change().cumsum().plot()
plt.grid()

ดูดีขึ้น คราวนี้เราพอจะรู้แล้วว่าข้อมูลแต่ละตัวมีหน้าตาเป็นอย่างไร เดี๋ยวเราลองมานำข้อมูลพวกนี้ไปทำ Feature ในการทำนายทิศทางของ GSPC กันดีกว่า
3. จัดเตรียมข้อมูล สร้าง Feature
ก่อนอื่นเราจะทำการจัดการกับข้อมูลที่เป็น NULL หรือ ช่องว่างกันก่อน ด้วยการใช้ฟังก์ชัน “.dropna” ดร็อป NA ก่อนเพื่อความชัวร์ จากนั้นเราค่อยมาทำการหา Log Return ของหุ้นทุกตัว เก็บไว้ใน df_ret แล้วเราจะใช้ข้อมูลจาก 2009 ถึง 2021 มาใช้ในการทดลองครั้งนี้
df.dropna(inplace=True)
df.shape
df_ret = pd.DataFrame()
for name in df.columns:
df_ret[name] = np.log(df[name]).diff()
df_ret.dropna(inplace=True)
df_ret = df_ret["2009":"2021"]
df_ret.head()

df_ret['^GSPC'] = df_ret['^GSPC'].shift(-1)
df_ret['^GSPC'].tail()
เฉพาะ GSPC เราจะทำการ Shift ข้อมูลมาหนึ่งวัน เพราะเราจะใช้ข้อมูลของวันนี้ ไปทำนายราคาของ GSPC วันพรุ่งนี้ เนื่องจากเราต้องการจะทำนายราคาในวันพรุ่งนี้ นั่นเอง

4. แบ่ง Train Test Data
ขั้นตอนนี้เราจะเริ่มขั้นตอนของ Machine Learning คือ การแบ่งข้อมูลออกเป็นTraining Data (ข้อมูลสอน) และ Testing Data (ข้อมูลทดสอบ) โดยที่
- Training Data คือ ข้อมูลที่นำมาสอนโมเดลของเรา เพื่อให้โมเดลเรียนรู้รูปแบบต่างๆของข้อมูล
- Testing Data คือ ข้อมูลที่เรานำมาทดสอบโมเดลที่สอนจากข้อมูลด้านบน ถ้าข้อมูลทดสอบที่โมเดลเราไม่เคยเจอมาก่อนใช้งานได้ดี โมเดลของเราก็มีหวัง
df_ret.dropna(inplace=True)
n_test = 252*2
df_train = df_ret.iloc[:-n_test]
df_test = df_ret.iloc[-n_test:]
df_train.tail(), df_test.head()
เรากำหนดจำนวนของข้อมูลทดสอบ n_test เป็น 252*2 ซึ่งก็คือ ระยะเวลา 2 ปีโดยประมาณนั่นเอง โดยเราจะแบ่ง Training Data ออกเป็นจากวันแรก ไปจนถึงวันก่อน 2 ปี คือ 2009-2019 ขณะที่ Testing Data คือข้อมูล 2 ปี สุดท้าย คือ 2020 – 2021
X_train = df_train.drop(columns="^GSPC")
y_train = df_train["^GSPC"]
X_test = df_test.drop(columns="^GSPC")
y_test = df_test["^GSPC"]
print("X_train: \n",X_train .head(),"\n"*2)
print("y_train: \n",y_train.head())
5. กำหนด Features
คราวนี้เราจะเริ่มสร้าง Feature และ Target ของการทำนายกัน
- X_train คือ ข้อมูลหุ้นทั้ง 7 ตัวที่ว่ามา จาก 2009 จนถึงปี 2019 โดยเราจะตัด GSPC ที่เป็นเป้าหมายในการทำนายออก
- y_train คือเ ป้าหมายในการทำนายของเราซึ่งก็คือ GSPC นั่นแหละครับ โดยเริ่มจาก 2009 จนถึง 2019 เช่นกัน
- X_test คือ ข้อมูลที่เหมือน X_train แต่เป็นข้อมูลปี 2020 – 2021
- y_test คือ ข้อมูลที่เหมือน y_train เช่นกัน แต่เป็นข้อมูลปี 2020 – 2021

แต่ในบทความนี้เราจะใช้โมเดล Random Forest มาทำนาย “ทิศทาง” ของราคา GSPC ฉะนั้น y_train ที่เป็น ราคา Log Return ไม่ได้ เราจึงต้องปรับข้อมูลอีกนิดหนึง
y_train_c = y_train > 0
y_test_c = y_test > 0
y_train_c.head()
เราทำการเปลี่ยน y_train และ y_test จาก Log Return เป็น วันนั้น S&P 500 Index มันขึ้นหรือลงแทน True คือใช่วันนั้นราคามันเพิ่ม และ False ก็คือราคาวันนั้นมันไม่ได้เพิ่ม

6. สร้างโมเดล Random Forest
โอเคครับ คราวนี้เรามาเริ่มใช้โมเดลที่จัดได้ว่าทรงพลังอย่าง Random Forest กันดีกว่า
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(random_state=42)
model.fit(X_train, y_train_c)
การใช้งานก็ง่ายๆ ครับ โมเดลที่ดูยุ่งยาก แต่เราสามารถใช้งานมันด้วยการเขียนโปรแกรมแค่ไม่กี่บรรทัดเท่านั้น โดยตัวที่ผมเลือกมาเป็น “RandomForestClassifier” เพราะเราต้องการจะทำนายว่า ถ้าข้อมูล หุ้น 7 ตัวของเราที่เป็น Log Return ในวันนั้นๆ มีค่าเป็นอย่างไร และ มันจะไปส่งผลให้ราคา GSPC ในวันพรุ่งนี้ จะขึ้น หรือ ลง (y_train_c) จะเห็นว่า ในการทดลองนี้ เรา set random_state=42 เพื่อให้การรันโมเดลแต่ละครั้งมีค่าเหมือนกัน
จากนั้นเราก็ ใช้คำสั่ง .fit เพื่อให้ RandomForestClassifier เรียนรู้ความสัมพันธ์ระหว่าง X_train (หุ้น 7 ตัวด้านบน) และ y_train_c (S&P 500 Index จะขึ้นหรือลง)
6. วิเคราะห์ประสิทธิภาพของโมเดล (Model Evaluation)
เมื่อให้โมเดลมันเรียนรู้แล้ว เราก็มาดูกันหน่อยดีกว่าว่าโมเดลทำงานได้ดีแค่ไหนโดยใช้ฟังก์ชั่น .score
print("Training Score", model.score(X_train, y_train_c))
print("Testing Score", model.score(X_test, y_test_c))

อันแรกเราทำการวัด Traing Data ดูก่อน ซึ่งส่วนนี้เราจะเห็นว่าผลมันดีมาก ซึ่งเราก็ไม่ควรแปลกใจมากนัก เพราะข้อมูลตัวนี้เป็นข้อมูลที่โมเดลเราเคยเจอมาแล้วผลจึงสูงมาก 1 เลยทีเดียว
แต่ถึงแม้ว่าโมเดลมันจะทำผลงานกับข้อมูล Traing Data แต่กับข้อมูลที่มันไม่เคยเห็นเลยอย่างTesting Data ผลงานดันดูไม่ดีเท่าไหร่ซะงั้น 52% โดยประมาณ
7. ทำนายและทดสอบโมเดล (Testing)
หลังจากเราทำการสอนโมเดลและวัดผลแล้ว ถึงคราวนำมันมาทำนายทิศทางของ GSPC
y_pred = model.predict(X_test)
np.unique(y_pred, return_counts=True)

คำสั่งก็ง่ายๆ คราวนี้เราแค่ใช้ .predict หรือทำนายแล้วใส่ข้อมูลที่โมเดลไม่เคยเห็นลงไป (X_test) ผลลัพธ์ที่ได้ คำทำนายเป็น True คือพรุ่งนี้มันจะขึ้นเป็นจำนวน 315 ครั้ง และ False คือพรุ่งนี้มันจะลง เป็นจำนวน 189 ครั้ง จาก 2 ปีของการทดสอบ แต่แบบนี้เรายังไม่เห็นว่าตกลงแล้วการทำนายครั้งนี้ถ้านำไปทดลองถือ Position มันดีหรือแย่อย่างไร
position = (y_test * np.where((y_pred == True), 1, -1))
position

เราจะทำการ ซื้อ เมื่อการทำนายว่าพรุ่งนี้ขึ้น( == True) ใส่ 1 ไปรับรู้ผลงานวันนั้น ขณะที่ถ้าเป็น Flase เราจะใส่ -1 ไปรับรู้ผลงานการ Short ในวันนั้น
(position).cumsum().plot()
plt.grid()

ผลลัพธ์ที่ได้จาการทดลองถือตาม จะเห็นว่าไม่ดีเท่าไหร่ สู้เป็น Long Position ยาวๆไม่ได้ด้วยซ้ำ แล้วปัญหามันอยู่ที่ตรงไหน
8. Overfiting!!! ลองพารามิเตอร์อื่นๆ
ใช่ครับ มัน Overfing สุดๆเลยนั่นเอง เพราะ Random Forest เป็นโมเดลที่มีการใช้งาน Decision Tree หลายๆ ตัวมารวมกันเป็นป่า หรือ Forest และทำงานร่วมกัน นั่นเอง ถ้าเราไม่กำหนดอะไรเลย Random Forest จะสร้างต้นไม้ หรือ Tree มา 100 ต้น!!! และ แต่ละต้น จะมีสุ่มข้อมูลมาสร้างโหนดการตัดสินใจ และถ้าเราไม่กำหนดอะไรให้มันเลย ในการสร้าง Tree แต่ละต้นมันก็จะสร้างโหนดในการตัดสินใจไปหลายชั้นมากๆ เพื่อทำให้การทำนายถูกต้องที่สุด ดังด้านบน เราจะเห็นว่าเราทำนายได้ 1 (รูปที่ 7) หรือก็คือ ทำนายถูกหมดเลยนั่นแหละครับ
แล้วมันไม่ดีเหรอถ้าจะทำนายถูกต้องหมดเลย? แน่นอนครับว่ามันไม่ใช่เรื่องดีแน่นอน เพราะขั้นตอนนี้เป็นขั้นตอนการสอนโมเดล ถ้าโมเดลเราจำรูปแบบข้อมูลสอนไว้มากเกินไป เมื่อมันไปเจอกับการทำงานจริง ที่ต้องทำการทำนายข้อมูลใหม่ๆ ที่ไม่ได้มีลักษณะเหมือนกับข้อมูลสอนมากนัก มันก็จะทำงานได้ไม่ดี และนี่ก็คือลักษณะของการเกิด Overfitting นั่นเองครับ
เดี๋ยวเราจะทดลองลดขนาดชั้นของ Tree ดูซักหน่อย เพื่อลดการ Overfitting ลง เพื่อคาดหวังให้โมเดลทำงานกับข้อมูลในอนาคตที่ไม่เคยเจอได้ดีขั้นนั่นเองครับ
model = RandomForestClassifier(max_depth=7, random_state=42)
model.fit(X_train, y_train_c)
y_pred = model.predict(X_test)
print("Training Score", model.score(X_train, y_train_c))
print("Testing Score", model.score(X_test, y_test_c))

ผมได้ทดลองบังคับให้ Tree แต่ละต้นสร้างชั้นการตัดสินใจได้ไม่เกิน 7 ชั้นดูครับ เพื่อบังคับไม่ให้มันสร้างโหนดมาหลายชั้นไปจน Overfit เกินไป เราจะเห็นได้ชัดเลยว่าผลความแม่นยำของ Training Data ลดลงอย่างเห็นได้ชัด จาก 1 เหลือแค่ 0.68 เท่านั้น
กลับกัน เมื่อมันไม่ Overfit แล้วผลงานกับข้อมูลที่มันไม่เคยเจอกลับทำได้ดีขึ้น เพิ่มขึ้นมาเป็น 0.57 นับว่าเพิ่มขึ้นถึง 0.05 เลยทีเดียวจากผลการทดลองก่อนหน้านี้ (รูปที่ 7)
new_position = (y_test * np.where((y_pred == True), 1, -1))
(new_position).cumsum().plot()
plt.grid()

จะเห็นได้ชัดเจนว่า โมเดลทำผลงานได้ดีขึ้นครับ แต่นี่มันไม่ได้แปลว่าเราเอาโมเดลพวกนี้ไปใช้งานได้เลยนะครับ มันยังต้องเทสอีกหลายอย่างมาก เช่น
- การที่ผมไปกำหนดให้มันไม่ Overfitting แบบ “ตามใจฉัน” ล้วนๆ เลย คือ การไปกำหนดให้ max_depth=7 มันอาจจะไม่ใช่ค่าดีที่สุดก็ได้ มันต้องมีวิธีที่ดีกว่านั้นครับ
- จากปัญหาบนเราก็สามารถ Search ได้หลากหลายรูปแบบ เช่น Grid Search (การลองหลายๆ ค่า) Random Search (ลองค่าแบบสุ่ม) หรือ จะใช้ Bayesian Search (การหาค่าแบบเบย์) ก็ได้ครับ
- ในส่วนของตัวทำนายนั้น ยังมี Parameter อีกมากที่สามารถปรับแต่งได้ ไม่ใช่แค่ max_depth เท่านั้นครับ
- ยังมี Feature อีกมากที่ยังไม่ได้ใช้ หรือพูดง่ายๆ ว่า ยังมี Feature อีกหลายตัวที่เราสามารถเพิ่มเข้าไปได้
- Feature ที่ใช้บางตัวอาจจะมีมีปัญหาเรื่อง Multicollinearity
- นี่คือผลงานแบบ Deterministic นะครับ ผมได้กำหนด Random Seed ไว้ที่ 42 ให้การสุ่มของมันเท่ากันทุกครั้ง แปลว่านี่คือ ผลงานที่อาจจะแค่ฟลุ๊คก็ได้ เช่น เราไปทำการเทสในส่วนขอข้อมูลที่ง่ายพอดี ดังนั้น กว่าจะนำโมเดลมาใช้งานจริงๆ เราต้องจะต้องทำการเทสมากกว่านี้อีกมาก จนกว่าจะมั่นใจครับ
รายละเอียดการพัฒนาโมเดลให้มีประสิทธิภาพมากขึ้นที่ได้กล่าวมาข้างบน ก็คือ ส่วนหนึ่งของสิ่งที่เราจะสอนในคอร์สใหม่ที่กำลังจะออกนะครับ ถ้าผู้อ่านท่านใดสนใจ สามารถติดตามกันต่อได้นะครับ ตอนนี้เราเตรียมเนื้อหาเสร็จแล้ว ไว้ออกเมื่อไหร่จะมาบอกกันนะครับ