🌲เจาะลึก Random Forest !!!— Part 2 of “รู้จัก Decision Tree, Random Forest, และ​ XGBoost!!!”

Witchapong Daroontham
8 min readNov 21, 2018

--

Image source: https://www.inprnt.com/gallery/gonzalokenny/ents-and-huorns/

ใน blog ก่อนหน้า ผมเขียนเกี่ยวกับหลักการของ Decision Tree เบื้องต้นไว้แล้ว สามารถอ่านได้ที่ http://bit.ly/2OPyz9I สำหรับ blog นี้ จะเขียนเกี่ยวกับ Random Forest ซึ่งเป็น model ที่ต่อยอดมาจาก Decision Tree และถือเป็น model ที่ได้รับความนิยมพอสมควร ด้วยความยืดหยุ่นของ model ดังกล่าว… วันนี้เราจะมาเจาะลึกเกี่ยวกับ 1) หลักการของ Random Forest, 2) ข้อดีของ Random Forest และ 3) ท้ายสุด เราจะมาลองใช้ Random Forest กับข้อมูลจริงจาก Kaggle โดยผมจะเอาเทคนิคการทำ preprocessing และ การทำ hyper-parameter tuning จาก fastai มาแชร์กัน!

Random Forest คือ อะไร?

ภาพ 1-หลักการทำ Random Forest

หลักการของ Random Forest คือ สร้าง model จาก Decision Tree หลายๆ model ย่อยๆ (ตั้งแต่ 10 model ถึง มากกว่า 1000 model) โดยแต่ละ model จะได้รับ data set ไม่เหมือนกัน ซึ่งเป็น subset ของ data set ทั้งหมด ตอนทำ prediction ก็ให้แต่ละ Decision Tree ทำ prediction ของใครของมัน และคำนวณผล prediction ด้วยการ vote output ที่ ถูกเลือกโดย Decision Tree มากที่สุด (กรณี classification) หรือ หาค่า mean จาก output ของแต่ละ Decision Tree (กรณี regression)

Decision Tree แต่ละ model ใน Random Forest ถือว่าเป็น weak learner — ประมาณว่าเป็น model ที่ไม่เก่งเท่าไหร่ แต่พอนำเอาแต่ละ Decision Tree มาทำ prediction ร่วมกัน ก็จะได้ model รวมที่มีความเก่ง และแม่นยำมากกว่า Decision Tree ที่ทำ prediction แบบเดี่ยวๆ🌲🌲🌲

จากภาพ 1 หลักการทำ Random Forest คือ

  1. sample ข้อมูล (bootstrapping) จาก data set ทั้งหมด ให้ได้ข้อมูลออกมา n ชุด ที่ไม่เหมือนกัน ตามจำนวน Decision Tree ใน Random Forest เช่น data set ตั้งต้นมีอยู่ 10 feature (X1,X2,…,X10) แต่ละ Decision Tree จะได้ feature ไปไม่เหมือนกัน และ จะได้ข้อมูลไม่ครบทุก row ด้วยจาก data set ทั้งหมดด้วย (X1 -> X1',X2->X2',…)
  2. สร้าง model Decision Tree สำหรับแต่ละชุดข้อมูล
  3. ทำ aggregation ผลลัพธ์ จากแต่ละ model (bagging) เช่น voting ในกรณี classification หรือ หาค่า mean ในกรณี regression

Random Forest ดียังไง?

อ้างอิงมาจาก course Machine Learning ของ Fastai โดย Jeremy Howard

  1. Random Forest ใช้ได้ทั้งกับปัญหา classification และ regression
  2. Random Forest ใช้ได้ทั้งกับข้อมูล structured (ข้อมูลลักษณะเป็น column/ table) และ unstructured (เช่น รูปภาพ, text)
  3. ทำ hyper-parameter tuning ให้ Random Forest ไม่ overfit ไม่ยาก
  4. Random Forest ไม่ตั้ง assumption กับ feature ว่าจะต้องกระจายข้อมูลแบบ normal distribution, หรือสัมพันธ์กับ target แบบ linear, และ ไม่ต้องสร้างความสัมพันธ์ระหว่าง feature เพิ่มเติม (เรียกว่า interaction — เช่น สร้าง feature X_1*X_2 จาก X_1 และ X_2)
  5. จากข้อ 4 ประหยัดแรงทำ Feature engineering เช่น ไม่จำเป็นต้องทำ log transform, หรือสร้าง interaction จาก feature

มาลองใช้ Random Forest ใน Python และ ทำ parameter tuning เบื้องต้น

เราจะมาลองใช้ Random Forest กับ data set Bulldozers จาก Kaggle โจทย์ คือ เราจะประเมินราคาอุปกรณ์ก่อสร้าง เช่น รถก่อสร้าง ว่าจะสามารถประมูลราคาได้เท่าไหร่ (target) โดยมี feature คือ การใช้งาน, ชนิดของอุปกรณ์, การเซตค่าของอุปกรณ์ และ อื่นๆ — ปัญหาของโจทย์นี้ คือ ทำ regression

เนื้อหาต่อจากนี้ผมสรุป และอ้างอิงมาจาก lecture 1&2 course Machine Learning ของ Fastai ครับผม

*หมายเหตุก่อนเริ่ม

function ที่จะเขียนต่อไป อ้างอิงมาจาก library fastai version 0.7.x ทั้งหมด (เอาไปรัน standalone ได้โดยไม่ต้องลง library) ถ้าใครไม่อยาก define function เอง และจะใช้ function จาก fastai โดยตรง ให้ install fastai ตาม instruction ใน link นี้

และ import library & module ตามนี้

from fastai.imports import *
from fastai.structured import *

ส่วน code เต็มๆ ใน Google colab ผมแปะไว้ท้ายบทความครับ

  1. Data import from Kaggle

เริ่มต้นจากไปโหลด data set จาก Kaggle โดยใช้ Kaggle API จาก command line ก่อนเลย… สำหรับคนที่ยังไม่เคย install Kaggle API ลองดูตาม https://github.com/Kaggle/kaggle-api ได้เลยครับ

หลังจาก install และ วาง API key ตาม directory ที่ระบุใน instruction เรียบร้อย ให้ cd ไปที่ folder ของ project นี้ และ copy command จากใน Kaggle มาวางได้เลย ก่อนโหลด ให้ไปกดยอมรับข้อตกลงในการใช้ข้อมูลก่อนถึงจะ โหลดได้นะครับ

step นี้ใครอยากไปท่าง่าย ก็กด download all จาก หน้า web เลยได้ครับ😆

ภาพ 2-command line สำหรับ Kaggle API
ภาพ 3-download data set จาก Kaggle API ผ่าน CLI

2. Data preprocessing

เราจะ preprocess ข้อมูลกันโดยใช้ function ที่อ้างอิงมาจาก library fastai ซึ่งสิ่งที่ผมชอบสำหรับ library นี้ คือ มี function สำหรับ preprocess data ในแบบที่เราต้องทำอยู่แล้วเป็นมาตรฐาน อยู่พอสมควร สามารถลดเวลาการทำงานลงได้ โดยสิ่งที่เราจะทำ คือ

2.1) ดูข้อมูลก่อนคร่าวๆ

เริ่มต้นจากอ่านข้อมูลใน format .csv เข้ามาเป็น DataFrame โดยเราจะ parse column “saledate” เข้ามาเป็นข้อมูลประเภท datetime เลย ด้วย argument parse_date (ปกติจะอ่านเข้ามาเป็น String)

df_raw = pd.read_csv(f'{PATH}Train.csv', low_memory=False, 
parse_dates=["saledate"])

มาดูข้อมูลคร่าวๆ… เราจะเขียน function สำหรับปรับ option การแสดงผลของ DataFrame ให้สามารถ display column ทั้งหมดได้ตามที่แสดงในภาพ 4 (propertiy .T คือ DataFrame version transpose)

ภาพ 4- function สำหรับ display DataFrame ทีละๆหลายๆ column และหน้าตาข้อมูลคร่าวๆ

2.2) ตรวจสอบว่า evaluation metric แบบไหนที่โจทย์ต้องการ

ปกติใน Kaggle competition จะ มี evaluation metric กำหนดไว้ สำหรับ data set Bulldozers จะสนใจ root mean squared log error (RMSLE) ซึ่งมันคือ root mean squared error (RMSE) ของ target variable ที่ take log แล้วนั่นเอง!

ภาพ 5-เมนู Evaluation ใน Kaggle แสดง evaluation metric สำหรับการทำ prediction

เราจะ take log column SalePrice ซึ่งเป็น target variable ด้วย function np.log()

df_raw.SalePrice = np.log(df_raw.SalePrice)

2.3) Extract feature จากข้อมูล datetime

ในขั้นตอนนี้เราจะ extract feature จาก column saledate ที่เรา parse เข้ามาเป็น type datetime ใน step 2.1 โดยเราจะเอา function add_datepart จาก library fastai มาใช้ ซึ่ง function ดังกล่าวจะเอาจะ extract ออกมาว่า column ที่เป็น datetime มี feature อะไรบ้าง เช่น เป็นปีอะไร (year) เดือนไหน (month) สัปดาห์ที่เท่าไหร่ (week) วันไหน (day) เป็นวันที่เท่าไหร่ของสัปดาห์ (dayofweek) และอีกมากมาย… ซึ่งดีมาก ประหยัดแรงมาก!

import re
def add_datepart(df, fldname, drop=True, time=False):
fld = df[fldname]
fld_dtype = fld.dtype
if isinstance(fld_dtype, pd.core.dtypes.dtypes.DatetimeTZDtype):
fld_dtype = np.datetime64
if not np.issubdtype(fld_dtype, np.datetime64):
df[fldname] = fld = pd.to_datetime(fld, infer_datetime_format=True)
targ_pre = re.sub('[Dd]ate$', '', fldname)
attr = ['Year', 'Month', 'Week', 'Day', 'Dayofweek', 'Dayofyear',
'Is_month_end', 'Is_month_start', 'Is_quarter_end', 'Is_quarter_start', 'Is_year_end', 'Is_year_start']
if time: attr = attr + ['Hour', 'Minute', 'Second']
for n in attr: df[targ_pre + n] = getattr(fld.dt, n.lower())
df[targ_pre + 'Elapsed'] = fld.astype(np.int64) // 10 ** 9
if drop: df.drop(fldname, axis=1, inplace=True)

หลังใช้ function add_datepart จะเห็นว่าจำนวน column เพิ่มขึ้นจาก 53 column เป็น 65 column เนื่องจากมี feature ต่างๆ เช่น saleMonth, saleWeek, saleDay,… เพิ่มขึ้นมาตามแสดงในภาพ 6

ภาพ 6-ใช้ function add_datepart สร้าง feature ที่เกี่ยวกับ datetime โดยระบุ column ที่เป็น datetime ของ DataFrame ใน function

2.4) จัดการกับข้อมูลประเภท categorical

ขั้นตอนนี้เราจะมาแปลงข้อมูล column ที่เป็น string (type เป็น object) ให้เป็นข้อมูลประเภท categorical ด้วย function train_cats ซึ่งจะทำให้ pandas มองเห็นข้อมูล column นั้นๆ เป็น categorical feature (สำหรับ column ที่ถูกเปลี่ยนเป็น data type categorical แล้ว ตอนแสดงผล DataFrame ออกมาจะยังเห็นเป็น string อยู่ แต่เวลานำไปเข้า model จะถูกแปลงเป็นตัวเลขโดยอัตโนมัติ)

from pandas.core.dtypes.common import is_string_dtype
def
train_cats(df):
for n,c in df.items():
if is_string_dtype(c): df[n] = c.astype('category').cat.as_ordered()

ก่อนใช้ function train_cats กับ DataFrame ถ้าเราเรียกดู เรียกดู data type ของแต่ละ column ด้วย .info() จะเห็นว่า column ที่เป็น categorical ยังมี data type เป็น object อยู่

ภาพ 6-column data type ก่อน apply function train_cats

หลังใช้ function train_cats จะเห็นว่า data type ของ column ที่เป็น categorical จะเปลี่ยนเป็น category แทน

train_cats(df_raw)
ภาพ 7-column data type หลัง apply function train_cats

2.5) จัดการกับ missing value

ตรวจสอบ DataFrame ยังมี column ที่มี missing value อยู่

ภาพ 8–ตรวจสอบ column ที่มี missing value

เราจะ fill in missing value ในแต่ละ column ด้วยค่า median ด้วย function proc_df ซึ่ง function ดังกล่าวจะต้อง define อีกหลาย function ย่อยพอสมควรตาม code ยาวๆ ด้านล่างเลยครับ 😂😂😂

from pandas.core.dtypes.common import is_numeric_dtypedef get_sample(df,n):
idxs = sorted(np.random.permutation(len(df))[:n])
return df.iloc[idxs].copy()
def fix_missing(df, col, name, na_dict):
if is_numeric_dtype(col):
if pd.isnull(col).sum() or (name in na_dict):
df[name+'_na'] = pd.isnull(col)
filler = na_dict[name] if name in na_dict else col.median()
df[name] = col.fillna(filler)
na_dict[name] = filler
return na_dict
def numericalize(df, col, name, max_n_cat):
if not is_numeric_dtype(col) and ( max_n_cat is None or len(col.cat.categories)>max_n_cat):
df[name] = col.cat.codes+1

def proc_df(df, y_fld=None, skip_flds=None, ignore_flds=None, do_scale=False, na_dict=None,
preproc_fn=None, max_n_cat=None, subset=None, mapper=None):
if not ignore_flds: ignore_flds=[]
if not skip_flds: skip_flds=[]
if subset: df = get_sample(df,subset)
else: df = df.copy()
ignored_flds = df.loc[:, ignore_flds]
df.drop(ignore_flds, axis=1, inplace=True)
if preproc_fn: preproc_fn(df)
if y_fld is None: y = None
else:
if not is_numeric_dtype(df[y_fld]): df[y_fld] = df[y_fld].cat.codes
y = df[y_fld].values
skip_flds += [y_fld]
df.drop(skip_flds, axis=1, inplace=True)
if na_dict is None: na_dict = {}
else: na_dict = na_dict.copy()
na_dict_initial = na_dict.copy()
for n,c in df.items(): na_dict = fix_missing(df, c, n, na_dict)
if len(na_dict_initial.keys()) > 0:
df.drop([a + '_na' for a in list(set(na_dict.keys()) - set(na_dict_initial.keys()))], axis=1, inplace=True)
if do_scale: mapper = scale_vars(df, mapper)
for n,c in df.items(): numericalize(df, c, n, max_n_cat)
df = pd.get_dummies(df, dummy_na=True)
df = pd.concat([ignored_flds, df], axis=1)
res = [df, y, na_dict]
if do_scale: res = res + [mapper]
return res

หลังจาก define function เรียบร้อย ก็เรียกใช้ function proc_df และ preprocess ข้อมูลกันในส่วนของ fill missing value หรือ scaling (ระบุ do_scale=True) ได้ในบรรทัดเดียว สะดวกมาก กราบ fastai ครับ🙌

df, y, nas = proc_df(df_raw, 'SalePrice')

หลังจาก preprocess เสร็จ ถ้าเอา df ซึ่งเป็น DataFrame ที่เป็น output จาก function proc_df ก็จะเห็นว่าเราได้ fill missing value เรียบร้อยแล้ว Viola!

ภาพ 9-ตรวจสอบ column ที่มี missing value เอา process ข้อมูลผ่าน function proc_df

2.6) Train&validation split

เนื่องจาก data set นี้เป็นข้อมูลในลักษณะ time series ดังนั้น การ split ข้อมูล train&validation จะต้องแบ่งข้อมูลออกเป็น 2 ส่วน โดยไม่ให้ช่วงเวลา overlap กัน (ไม่อย่างนั้น model ก็จะแอบเห็นข้อมูลในช่วงเวลาเดียวกันที่จะทำ prediction) เราจะแบ่งข้อมูล 12,000 index หลังสุดไว้สำหรับเป็น validation set

ภาพ 10-การ split train&validation set สำหรับข้อมูลประเภท time series

3. Base model & increasing n_estimators

ในที่สุดเราก็มาถึงขั้นตอนการ train model กันเสียที… ชัดเจนครับว่าเราใช้เวลาส่วนใหญ่ไปกับการทำ data preprocessing จริงๆ ซึ่ง library fastai ช่วยตรงนี้ได้เยอะเลย! (สำหรับใครที่ยังใช้ DataFrame ของ pandas ยังไม่คล่อง แนะนำให้ฝึก preprocess ข้อมูลเองก่อน แล้วก็มาใช้ library ช่วยครับ)

ก่อนอื่นเราจะกำหนด function rmse สำหรับคำนวณ error กันก่อน เนื่องจาก เราทำ log transform กับ target variable ไปเรียบร้อยแล้ว เราจึงสามารถคำนวณค่า rmse จาก target ของเราได้เลย จากนั้นจะนำ function ดังกล่าวไปใช้ใน print_score ซึ่งเป็น function สำหรับ print ค่า mse และ R-square ของ train & validation set

import mathdef rmse(x,y): return math.sqrt(((x-y)**2).mean())def print_score(m):
res = [rmse(m.predict(X_train), y_train), rmse(m.predict(X_valid), y_valid),
m.score(X_train, y_train), m.score(X_valid, y_valid)]
if hasattr(m, 'oob_score_'): res.append(m.oob_score_)
print(res)

เริ่มต้นด้วยค่า default parameter ของ RandomForestRegressor (กำหนด n_jobs = -1 เพื่อใช้ processor unit ทั้งหมดในการคำนวณแบบ parallel) โดยค่าเริ่มต้น จำนวน tree ใน RandomForestRegressor จะเท่ากับ 10

จาก score ของ base model จะเห็นว่า rmse error ของ training set ต่ำกว่า validation set มาก และ R-square ของ training set ก็สูงกว่า validation set มากเช่นกัน -> เรากำลังเจอกับปัญหา overfitting ชัดเจน!

ภาพ 11-การ split train&validation set สำหรับข้อมูลประเภท time series (train rmse, valid rmse, train R-square, valid R-square ตามลำดับ)

ก่อนจะไปทำ hyper-parameter tuning มาดูกันก่อนว่า จำนวน tree (n_estimators ใน RandomForestRegressor) สามารถเพิ่ม model accuracy ได้อย่างไร โดยดูจากค่า R-square ที่เพิ่มขึ้นเมื่อเราใช้จำนวน tree ในการทำ prediction เพิ่มมากขึ้นตามภาพ 12 แต่พอเราเพิ่มจำนวน tree เข้ามามากๆ accuracy ก็อาจจะไม่เพิ่มขึ้นอย่างมีนัยยะสำคัญแล้ว (การมี tree จำนวนมาก ทำให้เวลาในการรัน model ยิ่งนานด้วย)

ภาพ 12-plot ค่า R-square vs. n_estimators

4. Hyper-parameters tuning ง่ายๆ

สุดท้าย hyper-parameter ที่น่าสนใจสำหรับ Random Forest มีดังนี้!

n_estimators: จำนวน tree ใน Random Forest จำนวน tree ที่มากขึ้น จะทำให้ model performance ดีขึ้นจนถึงจุดนึงที่ performance เริ่มจะนิ่ง จนจำนวน tree ไม่มีผลต่อ model performance แล้ว… จำนวน tree ที่แนะนำ ตอนเทส model คร่าวๆ ก็ใช้ค่าน้อยๆ ก่อน (50–100 tree) ส่วนตอนที่จำ train จริงจังแล้วก็ใช้ค่า 1000 tree ขึ้นไป

oob_score (True/False) : out of bag score (oob score) เป็นการระบุว่าจะใช้ data ในส่วนที่ไม่ถูก sample ไปทำ training โดยแต่ละ decision tree ใน Random Forest (step ทำ bootstrapping ในภาพ 1)สำหรับคำนวณ error ซึ่งเทียบเท่ากับการทำ validation จาก training set โดยที่ไม่ต้องทำแบ่งข้อมูลสำหรับทำ validation นั่นเอง! ซึ่งจะมีประโยชน์อย่างมากตอนที่เรามี data set ไม่ใหญ่มาก และไม่อยากหั่นข้อมูลสำหรับ validation set อีก

min_samples_leaf : ระบุจำนวนข้อมูลขั้นต่ำใน leaf node ของแต่ละ decision tree หากมีจำนวนข้อมูลต่ำกว่า min_samples_leaf ให้หยุด split node นั้นๆ เป็นการลด overfitting… เราจะกำหนดค่า min_samples_leaf ให้สอดคล้องตามขนาดของ data set (เช่น ตั้งแต่ 2–100 ตามขนาดข้อมูล)

max_features (0.0–1.0): ระบุว่าแต่ละ decision tree ใน Random Forest จะสามารถสุ่มหยิบ feature ไปได้มากที่สุดกี่ % (0.0–1.0 -> 0–100%) ซึ่งการไม่ set ค่าดังกล่าวสูงจนเกินไป จะเป็นการลดความสัมพันธ์กันเองของ tree (ลด correlation) และลดโอกาส overfit ของ model

จะเห็นว่าหลังจากเรากำหนด hyper-parameter บางตัวที่ยกมาข้างต้น ทำให้ model ลดความ overfit ลง และมี performance ที่สูงขึ้นด้วยค่า rmse ที่ลดลง และ R-square ที่สูงขึ้นของ validation set นั่นเอง (score ตัวสุดท้าย คือ oob score)

ภาพ 13-การ split train&validation set สำหรับข้อมูลประเภท time series (train rmse, valid rmse, train R-square, valid R-square, oob R-square ตามลำดับ)

code on Google Colab:

https://drive.google.com/open?id=1eAQi6PZzIqFfVgnKl4A3yJiV24HY3mxV

original notebook:

https://github.com/fastai/fastai/blob/master/courses/ml1/lesson1-rf.ipynb

จบแล้ว HOORAY! ขอบคุณผู้อ่านทุกคนมากครับ มีคำถามหรือติชมได้ที่ https://web.facebook.com/datawizthailand/

Happy learning!

--

--

Witchapong Daroontham

Data scientist at Central Technology Organization — CTO, Bangkok & life long learner