φυβλαςのβλογ
บล็อกของ phyblas



[python] วิเคราะห์จำแนกประเภทข้อมูลด้วยต้นไม้ตัดสินใจ
เขียนเมื่อ 2017/11/05 21:39
แก้ไขล่าสุด 2022/07/21 15:02
ต้นไม้ตัดสินใจ (决策树, decision tree) เป็นเทคนิคหนึ่งในการเรียนรู้ของเครื่อง ซึ่งอาศัยการแตกเงื่อนไขย่อยไปเรื่อยๆเพื่อนำไปสู่คำตอบ

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

ยกตัวอย่างเช่น เราเลือกว่าจะอยู่บ้านหรือออกไปข้างนอก อยู่บ้านจะอ่านหนังสือหรือเล่นเกม ออกไปข้างนอกแล้วไปเจอเพื่อนหรือเปล่า เจอเพื่อนแล้วจะชวนกันไปเล่นเกมหรือไปอ่านหนังสือ แล้วสุดท้ายการกระทำเหล่านั้นจะเป็นตัวตัดสินว่าเราจะสอบปลายภาคผ่านหรือเปล่า

อาจเขียนแจกแจงเป็นแผนผังได้ดังนี้



ลักษณะนี้เรียกว่าเป็นแผนผังต้นไม้ เพียงแต่ว่าปกติแล้วจะให้ด้านบนเป็นส่วนรากฐาน ในขณะที่ยิ่งงอกก็จะยิ่งลงไปข้างล่างเรื่อยๆ กลับทิศกลับต้นไม้ในโลกความเป็นจริง

วิธีการนี้ยังสามารถใช้กับข้อมูลที่เป็นตัวเลขได้โดยการตั้งเงื่อนไขเป็นมากหรือน้อยกว่า

เช่น ในการสอบวิชาหนึ่ง หากใครสอบกลางภาคได้ได้ 60 คะแนนขึ้นไป ปลายภาคได้ 40 ถือว่าสอบผ่าน แต่ถ้าไม่ได้ ต้องทำปลายภาคเกิน 70 จึงจะผ่าน

หากเขียนเป็นแผนผังจะได้ดังนี้



หากเขียนเป็นฟังก์ชันในไพธอนก็จะได้ในลักษณะแบบนี้
def phonsop(klangphak,plaiphak):
    if(klangphak>60):
        if(plaiphak>40):
            return 1
        else:
            return 0
    else:
        if(plaiphak>70):
            return 1
        else:
            return 0

ในที่นี้ 1 คือสอบผ่าน 0 คือสอบไม่ผ่าน

อย่างไรก็ตาม หากต้องการหาคำตอบพร้อมกันหลายๆตัวใช้ numpy ช่วยอาจเขียนได้ดังนี้
import numpy as np
def phonsop(klangphak,plaiphak):
    return np.where(klangphak>60,plaiphak>40,plaiphak>70)

ใครงงกับคำสั่ง where อ่านได้ใน https://phyblas.hinaboshi.com/numa19

ลองวาดเป็นแผนภาพแสดงอาณาเขตได้เป็นแบบนี้
import matplotlib.pyplot as plt
nmesh = 100
mx,my = np.meshgrid(np.linspace(0,100,nmesh),np.linspace(0,100,nmesh))
mz = phonsop(mx,my)
plt.figure(figsize=[6,5])
plt.axes(aspect=1)
plt.xlabel(u'กลางภาค',family='Tahoma',size=20)
plt.ylabel(u'ปลายภาค',family='Tahoma',size=20)
plt.contourf(mx,my,mz,cmap='RdYlGn')
plt.text(50,80,u'สอบผ่าน',family='Tahoma',size=20,ha='center')
plt.text(50,20,u'สอบตก',family='Tahoma',size=20,ha='center')
plt.show()


โดยอาศัยแนวคิดในลักษณะคล้ายๆกันนี้ เราสามารถนำมาประยุกต์ใช้เพื่อเขียนโปรแกรมสำหรับแบ่งข้อมูลออกเป็นกลุ่มโดยอาศัยเกณฑ์ต่างๆได้

ปัญหาที่ยกเมื่อครู่นี้ ลองสมมุติว่าถ้าอาจารย์ไม่ยอมบอกว่าเขาใช้เกณฑ์อะไรในการคิด แต่มีนักเรียนอยู่ทั้งหมด 50 คนมาสอบแล้วได้คะแนนเท่าไหร่ก็บันทึกไว้ แล้วก็ดูว่าใครได้คะแนนเท่าไหร่แล้วสอบผ่านหรือเปล่า



โค้ดสำหรับสร้างข้อมูลและวาดภาพนี้เขียนได้ดังนี้
x = np.array([54,71,60,54,42,64,43,89,96,38,79,52,56,92,7,8,2,83,77,87,97,79,46,78,11,63,14,94,52,41,26,77,45,56,1,61,61,61,94,68,35,43,69,6,66,67,21,12,31,36])
y = np.array([57,43,98,10,20,16,65,25,46,24,15,11,65,13,19,36,82,9,83,9,97,46,97,60,73,3,28,12,29,11,31,41,6,69,56,26,52,9,57,92,31,66,13,71,28,18,58,2,82,0])
z = np.array([0,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,1,1,1,1,1,0,0,0,0,0,0,1,0,0,0,0,1,0,1,1,0,0,0,1,0,0,0,0,1,0])
plt.axes(aspect=1)
plt.scatter(x,y,c=z,alpha=0.6,edgecolor='k',cmap='RdYlGn')
plt.show()

ในที่นี้ x เป็นคะแนนสอบกลางภาค y เป็นคะแนนสอบปลายภาค และ z คือผลสอบว่าผ่านไม่ผ่าน (0=ไม่ผ่าน, 1=ผ่าน)

ที่จะวิเคราะห์ก็คือว่าจากข้อมูลที่มีอยู่ตรงนี้ จะสามารถบอกได้อย่างไรว่า วิชานี้มีเกณฑ์ในการตัดสินยังไง

นั่นก็คือ จะเขียนโปรแกรมยังไงให้สามารถขีดเส้นแบ่งออกมาเป็นอย่างในภาพแรกได้เองโดยที่เราไม่ต้องไปกำหนดค่าเองเลย

นี่เป็นวิธีคิดของการจำแนกประเภทข้อมูลโดยใช้ต้นไม้ตัดสินใจ

ที่จริงแล้วแนวทางการเขียนโปรแกรมสำหรับจำแนกประเภทข้อมูลนั้นมีอยู่หลายชนิด เช่นการวิเคราะห์การถดถอยโลจิสติก หรือวิธีการเพื่อนบ้านใกล้ที่สุด แต่ละวิธีการก็มีจุดเด่นจุดด้อยลักษณะเฉพาะต่างๆกันออกไป

สำหรับวิธีการต้นไม้ตัดสินใจนี้มีจุดเด่นอยู่ตรงที่ว่าจะพิจารณาตัวแปรทีละชนิดแล้วแบ่งซีกตามช่วงค่า ดังนั้นหากดูแผนภาพการแบ่งจะเห็นการแบ่งเป็นเส้นตรงตั้งฉากตลอด

โดยทั่วไปแล้วต้นไม้ตัดสินใจจะเป็นการแบ่งข้อมูลโดยแตกออกเป็นทีละ ๒ ส่วนไปเรื่อยๆ

ซึ่งจริงๆแล้วก็ไม่ใช่ว่าจะแตกเกิน ๒ ส่วนไม่ได้ แต่ว่าการแตกทีละ ๒ นั้นทำได้ง่าย ดังนั้นอัลกอริธึมส่วนใหญ่ที่ใช้แบ่งจึงมักจะเป็นการแบ่งทีละ ๒ ส่วน

แต่ละกิ่งก็อาจไม่จำเป็นต้องแบ่งเป็นจำนวนครั้งเท่ากัน กิ่งหนึ่งอาจไม่แบ่งต่อในขณะที่อีกกิ่งแบ่งต่อไปอีกยาวก็ได้





ลองค่อยๆคิดไปเป็นขั้นเป็นตอนดังนี้

เริ่มจากลองพิจารณาว่าถ้าเราอยากจะเริ่มแบ่งข้อมูลตามค่าแกน x เราอาจจะลองเอาค่า x ของแต่ละจุดมาเรียงแล้วคิดค่ากึ่งกลางที่แบ่งแต่ละจุด

อาจเขียนได้ในลักษณะนี้
x_riang = np.sort(x)
x_kan = (x_riang[1:]+x_riang[:-1])/2
plt.axes(aspect=1)
plt.scatter(x,y,c=z,edgecolor='k',alpha=0.6,cmap='RdYlGn')
for khabaeng in x_kan:
    plt.axvline(khabaeng,color='m')
plt.show()



แต่ว่าเส้นที่กั้นแบ่งระหว่างชนิดเดียวกัน (สีเหมือนกัน) เราคงไม่ต้องการ ดังนั้นควรตัดออก เขียนได้ใหม่เป็นแบบนี้
xas = x.argsort()
x_riang = x[xas]
z_riang = z[xas]
x_kan = (x_riang[1:]+x_riang[:-1])/2
x_kan = x_kan[z_riang[1:]!=z_riang[:-1]]
plt.axes(aspect=1)
plt.scatter(x,y,c=z,edgecolor='k',alpha=0.6,cmap='RdYlGn')
for khabaeng in x_kan:
    plt.axvline(khabaeng,color='m')
plt.show()


เท่านี้ก็เหลือแต่เส้นที่แบ่งระหว่างต่างสีกัน แต่ว่าก็ยังเยอะ ในจำนวนเส้นแบ่งมากมายนี้เราจะพิจารณายังไงว่าควรจะแบ่งตรงไหน?

หลักการที่นิยมใช้ก็คือ ดูว่าหลังจากแบ่งแล้วมีการปนกันของข้อมูลต่างชนิดแค่ไหน ยิ่งปะปนกันมากก็ยิ่งไม่ดี

ค่าที่แสดงว่ามีการปะปนกันมากแค่ไหนนั้นเรียกว่าค่าความไม่บริสุทธิ์ (纯度, impurity)

ค่าความไม่บริสุทธ์นั้นมีวิธีที่คิดอยู่หลายอย่าง ที่นิยมก็คือค่าเอนโทรปี (熵, entropy) และ ความไม่บริสุทธิ์ของจีนี (基尼不纯度, Gini impurity)

ค่าเอนโทรปีคำนวณได้ดังนี้


โดยที่ p(i|t) คือจำนวนสมาชิกกลุ่ม i ต่อจำนวนทั้งหมดในกิ่งนั้น

ส่วนค่าความไม่บริสุทธิ์ของจีนีคำนวณได้ดังนี้


ทั้งสองค่านี้มีลักษณะคล้ายกัน อย่างไรก็ตามค่าความไม่บริสุทธิ์ของจีนีเป็นที่นิยมใช้มากกว่า ครั้งนี้จึงจะใช้แค่ค่านี้เป็นหลัก

จากสูตรคำนวณ เราสามารถเขียนฟังก์ชันในไพธอนได้ดังนี้
def gini(p):
    return 1-(p**2).sum()

โดย p ในที่นี้คืออาเรย์ p(i|t) ซึ่งก็คือสัดส่วนของแต่ละสมาชิก ซึ่งทั้งหมดรวมกันแล้วต้องเป็น 1

ความไม่บริสุทธิ์ยิ่งน้อยแสดงว่าการแบ่งยิ่งดี ดังนั้นเราลองให้วนซ้ำเพื่อหาดูว่าค่าแบ่งไหนทำให้ความไม่บริสุทธิ์รวมต่ำสุด จากนั้นก็แบ่งด้วยค่านั้น โดยความไม่บริสุทธิ์รวมนี้คิดดังนี้
ความไม่บริสุทธ์รวม = (ความไม่บริสุทธิฝั่งซ้าย×จำนวนฝั่งซ้าย + ความไม่บริสุทธิฝั่งขวา×จำนวนฝั่งขวา) / จำนวนทั้งหมด

ลองเขียนดูได้ดังนี้
n = len(z)
gn_noisut = 1
for khabaeng in x_kan:
    baeng = khabaeng>x
    z_sai = z[baeng]
    z_khwa = z[~baeng]
    n_sai = float(len(z_sai))
    n_khwa = float(len(z_khwa))
    gn = (gini(np.bincount(z_sai)/n_sai)*n_sai+gini(np.bincount(z_khwa)/n_khwa)*n_khwa)/n
    if(gn_noisut>gn):
        gn_noisut = gn
        khabaeng_disut = khabaeng

plt.axes(aspect=1)
plt.scatter(x,y,c=z,edgecolor='k',alpha=0.6,cmap='RdYlGn')
plt.axvline(khabaeng_disut,color='m')
plt.show()



จะเห็นว่าได้เส้นแบ่งที่ดูเหมือนจะดีออกมา แม้ว่าจะยังต่างจากที่คาดหวังไว้สักหน่อย

ปัญหาอย่างหนึ่งก็คือในที่นี้เราคิดไปก่อนแล้วว่าจะแบ่งตามแกน x ก่อน แต่ในความเป็นจริงการแบ่งตามแกน x ก่อนนั้นดีสุดแล้วจริงหรือเปล่าก็ไม่รู้

ดังนั้นจึงควรจะพิจารณาตัวแปรทั้งหมดไปพร้อมๆกันแล้วดูว่าแบ่งแนวไหนให้ค่าความไม่บริสุทธิ์ต่ำกว่า

เขียนใหม่เป็นดังนี้
X = np.stack([x,y],1) # นำมารวมกันเป็นอาเรย์สองมิติอันเดียว
n = len(z)
gn_noisut = 1
for j in range(X.shape[1]):
    xas = X[:,j].argsort()
    x_riang = X[:,j][xas]
    z_riang = z[xas]
    x_kan = (x_riang[1:]+x_riang[:-1])/2
    x_kan = x_kan[z_riang[1:]!=z_riang[:-1]]
    for khabaeng in x_kan:
        baeng = khabaeng>X[:,j]
        z_sai = z[baeng]
        z_khwa = z[~baeng]
        n_sai = float(len(z_sai))
        n_khwa = float(len(z_khwa))
        gn = (gini(np.bincount(z_sai)/n_sai)*n_sai+gini(np.bincount(z_khwa)/n_khwa)*n_khwa)/n
        if(gn_noisut>gn):
            gn_noisut = gn
            j_disut = j
            khabaeng_disut = khabaeng

plt.axes(aspect=1)
plt.scatter(x,y,c=z,edgecolor='k',alpha=0.6,cmap='RdYlGn')
if(j_disut==0):
    plt.axvline(khabaeng_disut,color='m')
else:
    plt.axhline(khabaeng_disut,color='b')
plt.show()



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



เท่านี้การแบ่งครั้งแรกก็ทำได้สำเร็จแล้ว ต่อมาการจะแบ่งต่อก็ใช้วิธีการในทำนองเดียวกันกับข้อมูลที่แยกไว้แล้ว

เพื่อความสะดวกอาจลองเขียนออกมาเป็นรูปแบบของคลาส โดยจะเขียนคลาสของจุดแตกกิ่งขึ้นมา และจะสร้างออบเจ็กต์จุดแตกกิ่งออกมาทุกครั้งที่มีการแตกกิ่ง
class Chuttaek:
    def __init__(self,X,z):
        n = len(z)
        if(len(np.unique(z))==1): # กรณีที่สมาชิกเหมือนกันทั้งหมด ไม่ต้องแบ่งแล้ว
            self.khabaeng = np.inf
            self.saipen = z[0]
            self.j = 0
        else: # กรณีที่สมาชิกมีการปนกันอยู่ ให้ทำการแบ่ง
            self.gn = 1
            for j in range(X.shape[1]):
                x = X[:,j]
                xas = x.argsort()
                x_riang = x[xas]
                z_riang = z[xas]
                x_kan = (x_riang[1:]+x_riang[:-1])/2
                x_kan = x_kan[z_riang[1:]!=z_riang[:-1]]
                for khabaeng in x_kan:
                    baeng = khabaeng>x
                    z_sai = z[baeng]
                    z_khwa = z[~baeng]
                    n_sai = float(len(z_sai))
                    n_khwa = float(len(z_khwa))
                    gn = (gini(np.bincount(z_sai)/n_sai)*n_sai+gini(np.bincount(z_khwa)/n_khwa)*n_khwa)/n
                    if(self.gn>gn):
                        self.gn = gn
                        self.j = j
                        self.khabaeng = khabaeng
                        self.saipen = (baeng==z).mean()>0.5

    def __call__(self,X):
        return (X[:,self.j]<self.khabaeng)==self.saipen

การนำมาใช้งานก็ทำได้โดยเริ่มสร้างออบเจ็กต์ของจุดแยกขึ้นมา พอสร้างแล้วมันจะกลายเป็นเหมือนฟังก์ชันสำหรับคำนวนแบ่งหมวด โดยเรานิยามเมธอด __call__ ดังนั้นเวลาใช้ทำนายผลก็เรียกใช้เหมือนฟังก์ชันธรรมดา

ลองนำมาใช้ดูโดยเริ่มจากลองดูกรณีที่แยกแค่ครั้งเดียวก่อน นำมาใช้ทำนายแบ่งเขตโดยใช้ contourf ก็จะเขียนแบบนี้
nmesh = 200
mx,my = np.meshgrid(np.linspace(0,100,nmesh),np.linspace(0,100,nmesh))
mX = np.stack([mx,my],1)
ct = Chuttaek(X,z) # สร้างจุดแตกกิ่ง
mz = ct(mX) # ทำนายผล
# วาด
def plottare(X,z,mx,my,mz):
    plt.figure()
    plt.axes(aspect=1,xlim=[0,100],ylim=[0,100])
    plt.scatter(X[:,0],X[:,1],c=z,edgecolor='k',cmap='RdYlGn',vmin=0,vmax=1)
    plt.contourf(mx,my,mz,alpha=0.2,cmap='RdYlGn',zorder=0)
    plt.show()
plottare(X,z,mx,my,mz)



พอจะนำมาแตกต่อก็เขียนได้แบบนี้
o = ct(X)
ct1 = Chuttaek(X[o],z[o])
ct2 = Chuttaek(X[~o],z[~o])
mz = np.where(ct(mX),ct1(mX),ct2(mX))
plottare(X,z,mx,my,mz)

จะเห็นว่าส่วนด้านล่างไม่สามารถแตกต่อได้แล้วจึงหยุดแค่นี้ แต่ว่าด้านบนจะมีการแยก

ด้านบนแยกแล้วก็ยังมีส่วนที่ยังปนจำเป็นต้องแยกอยู่อีก ดังนั้นจึงยังต้องแตกเงื่อนไขต่อไป

แต่ว่า หากทำแบบนี้ไปเรื่อยๆละก็คงต้องเขียนไม่รู้จบ ดังนั้นจริงๆแล้ววิธีการเขียนแบบนี้จึงยังไม่เหมาะต่อการใช้งานจริงอยู่

เราจะพัฒนาแนวคิดต่อไป โดยคราวนี้ทำคลาสจุดแตกให้ออกมาเป็นรูปแบบที่พอสร้างขึ้นมาก็สามารถแตกกิ่งก้านออกไปเองได้ และตัดสินเอาเองว่าควรแตกต่อหรือไม่โดยดูจากว่าในกิ่งนั้นๆมีการแบ่งโดยสมบูรณ์หรือยัง

อาจสามารถเขียนใหม่ได้แบบนี้
class Chuttaek:
    def __init__(self,X,z,chan):
        self.chan = chan # ชั้นของจุดแตกกิ่ง
        self.n = len(z) # จำนวนข้อมูลในกิ่งนี้
        self.king = []
        if(len(np.unique(z))==1): # หากสมาชิกเป็นชนิดเดียวกันหมดก็ไม่ต้องแตกกิ่งแล้ว
            self.z = z[0] # เก็บค่าคำตอบของส่วนนั้น
        elif(chan==0): # หากจำนวนชั้นเหลือเป็น 0 แล้วก็ไม่ต้องแตกแล้วเช่นกัน
            self.z = np.bincount(z).argmax() # ใช้ค่าที่มีมากที่สุดเป็นคำตอบของส่วนนั้น
        else:
            self.gn = 1 # ค่าความไม่บริสุทธิ์ของจีนีตั้งต้น
            for j in range(X.shape[1]): # วนเปลี่ยนตัวแปรต้นที่พิจารณา
                x = X[:,j]
                xas = x.argsort()
                x_riang = x[xas]
                z_riang = z[xas]
                x_kan = (x_riang[1:]+x_riang[:-1])/2
                x_kan = x_kan[z_riang[1:]!=z_riang[:-1]]
                for khabaeng in x_kan:
                    baeng = khabaeng>x
                    z_sai = z[baeng]
                    z_khwa = z[~baeng]
                    n_sai = float(len(z_sai))
                    n_khwa = float(len(z_khwa))
                    gn = (gini(np.bincount(z_sai)/n_sai)*n_sai+gini(np.bincount(z_khwa)/n_khwa)*n_khwa)/self.n
                    if(self.gn>gn): # ถ้าเจอค่าจีนีที่ต่ำกว่าเดิมก็เก็บค่าใหม่นั้นพร้อมทั้งบันทึกตัวแปรที่ใช้แบ่งและค่าที่แบ่ง
                        self.gn = gn # ค่าจีนีต่ำสุด
                        self.j = j # ดัชนีของตัวแปรที่ใช้แบ่ง
                        self.khabaeng = khabaeng # ค่าที่ใช้แบ่ง
            o = (self.khabaeng>X[:,self.j]) # แบ่งข้อมูลเพื่อแตกกิ่งต่อไป
            self.king = [Chuttaek(X[o],z[o],chan-1),Chuttaek(X[~o],z[~o],chan-1)]

    def __call__(self,X):
        if(self.king==[]): # ถ้าไม่มีกิ่งแล้วก็ให้คำตอบเดียวเป็นค่าของส่วนนั้นเลย
            return self.z
        else: # ถ้ามีกิ่งก็ให้ทำการแยกกรณีไล่เรียกกิ่งย่อยต่อไป
            o = self.khabaeng>X[:,self.j]
            return np.where(o,self.king[0](X),self.king[1](X))

จะเห็นว่าที่ต่างจากเดิมชัดเจนคือตอนท้ายสุดของตอนสร้างกิ่งจะมีการสร้างกิ่งย่อยขึ้นมาเองเลย

ต่อไปก็ลองนำมาใช้กับข้อมูลเดิม เวลาใช้ก็เขียนแค่นี้
rak = Chuttaek(X,z,3)
mz = rak(mX)
plottare(X,z,mx,my,mz)


แค่สร้างจุดแตกอันแรก ซึ่งในที่นี้เรียกว่าเป็นราก มันก็จะแตกกิ่งออกต่อเอง

ตัวเลข 3 ที่เป็นอาร์กิวเมนต์ตัวสุดท้ายจะเป็นจำนวนชั้นของการแตกกิ่ง ในที่นี้ต้องการให้แตก ๓ ครั้งจึงใส่ 3 แต่ในตัวอย่างนี้ต่อให้ใส่เยอะกว่านั้นก็ไม่มีการแบ่งต่อแล้วเพราะถูกแบ่งโดยสมบูรณ์แล้ว

สุดท้ายก็เรียกใช้เพื่อทำนาย เท่านี้ก็เรียบร้อย

เพื่อให้ดูแล้วเข้าใจง่ายขึ้นเราอาจลองสร้างคลาสใหม่ขึ้นมาเพื่อห่อหุ้ม นั่นคือคลาสของต้นไม้ตัดสินใจ
class TonmaiTatsinchai:
    def __init__(self,luek):
        self.luek = luek

    def rianru(self,X,z):
        self.rak = Chuttaek(X,z,self.luek)

    def thamnai(self,X):
        return self.rak(X)

ลักษณะที่ทำขึ้นมานี้จะเป็นรูปแบบในทำนองเดียวกันกับคลาสของการถดถอยโลจิสติก (https://phyblas.hinaboshi.com/20171006) และวิธีการเพื่อนบ้านใกล้สุด (https://phyblas.hinaboshi.com/20171028) ซึ่งเคยสร้างมาก่อนหน้านี้

นั่นคือมีเมธอด rianru ไว้ใช้สำหรับนำข้อมูลมาเรียนรู้ และมีเมธอด thamnai ในการทำนายผล

สำหรับกรณีของต้นไม้ตัดสินใจ เมื่อใช้เมธอดเรียนรู้ก็จะเห็นการสร้างจุดแตกแรก ซึ่งก็คือรากขึ้นมา และจะเกิดการแตกกิ่งก้านออกมา

ตอนสร้างต้องกำหนดค่าความลึก (luek) คือจำนวนครั้งสูงสุดที่จะแตกกิ่ง

ลองนำมาใช้ดู คราวนี้ลองสุ่มสร้างข้อมูลใหม่มาใช้ดูบ้าง โดยใช้ข้อมูลเป็นกลุ่มก้อนซึ่งสร้างด้วย make_blobs (https://phyblas.hinaboshi.com/20161127) ลองให้ทำการแตกกิ่งไปเรื่อยๆจนกว่าจะแบ่งได้หมด คือใส่ค่าจำนวนการแตกสูงสุดเป็นค่าเยอะๆไว้
from sklearn import datasets
X,z = datasets.make_blobs(n_samples=100,n_features=2,centers=2,cluster_std=1.5,random_state=0)
tt = TonmaiTatsinchai(100)
tt.rianru(X,z)
nmesh = 200
mx,my = np.meshgrid(np.linspace(X[:,0].min()-1,X[:,0].max()+1,nmesh),np.linspace(X[:,1].min()-1,X[:,1].max()+1,nmesh))
mX = np.stack([mx,my],1)
mz = tt.thamnai(mX)
def plottiamo(X,z,mx,my,mz):
    plt.figure()
    plt.axes(aspect=1)
    plt.scatter(X[:,0],X[:,1],c=z,edgecolor='k',cmap='plasma')
    plt.contourf(mx,my,mz,alpha=0.4,cmap='plasma',zorder=0)
    plt.show()
plottiamo(X,z,mx,my,mz)



พอดูจากผลที่ได้ตรงนี้จะเห็นลักษณะเด่นของการแบ่งด้วยต้นไม้ตัดสินใจได้ชัดเจน นั่นคือข้อมูลที่ซ้อนกันอาจถูกแบ่งเป็นก้อนๆแบบนี้

ลักษณะแบบนี้ทำให้เกิดการเรียนรู้เกินได้ง่าย คือแบบจำลองจะถูกปรับให้เข้ากับข้อมูลที่ใส่ลงไปนี้อย่างเต็มที่ แต่ซับซ้อนเกินไปจนอาจไม่สามารถใช้ทำนายข้อมูลทั่วไปได้

เพื่อไม่ให้เป็นแบบนี้อาจลดจำนวนความลึกในการแตกลง เช่นลองให้เหลือ 5
tt = TonmaiTatsinchai(5)
tt.rianru(X,z)
mz = tt.thamnai(mX)
plottiamo(X,z,mx,my,mz)



แบบนี้จะเห็นว่าการแบ่งถูกทำแค่พอประมาณ ไม่ละเอียดจนเกินไป



ในตัวอย่างที่ผ่านมาเราทำการแบ่งแค่ ๒ กลุ่ม แต่หากจะลองนำมาแบ่งเป็นหลายกลุ่มก็แน่นอนว่าทำได้เช่นกัน ลองดูได้เช่น
X,z = datasets.make_blobs(n_samples=100,n_features=2,centers=5,cluster_std=1.7,random_state=2)
tt = TonmaiTatsinchai(100)
tt.rianru(X,z)
mx,my = np.meshgrid(np.linspace(X[:,0].min()-1,X[:,0].max()+1,nmesh),np.linspace(X[:,1].min()-1,X[:,1].max()+1,nmesh))
mX = np.stack([mx,my],1)
mz = tt.thamnai(mX)
plottiamo(X,z,mx,my,mz)



สุดท้าย ขอลองสร้างเป็นภาพต้นไม้ขึ้นเพื่อเปรียบเทียบ ให้เห็นถึงภาพการแตกกิ่งของต้นไม้

สมมุติว่าต้นไม้แต่ละต้นคือข้อมูลชุดหนึ่งที่ต้องการแบ่งเป็น ๒ กลุ่ม เมื่อลองแตกกิ่งครั้งนึงเป็น ๒ ส่วนจะได้เป็นแบบนี้ พุ่มใบที่อยู่ตรงปลายยอดจะมีสีต่างกัน แสดงด้วยสีของกลุ่มที่มีจำนวนมากที่สุดในกิ่งนั้น ความกว้างของพุ่มใบแสดงถึงจำนวนข้อมูล



ถ้าแตกต่อเป็นรอบที่ ๒ แต่ละกิ่งก็จะย่อยลงไปอีก แต่จะเห็นได้ว่าบางกิ่งไม่ถูกแตกต่อแล้ว



แล้วก็แตกต่อไปอีกเป็นรอบที่ ๓, ๔ และ ๕ ก็จะเป็นแบบนี้









โดยสรุป ข้อดีและข้อเสียของวิธีการต้นไม้ตัดสินใจ

ข้อดี
- มองแล้วเข้าใจได้ง่าย ดูลำดับขั้นตอนการตัดสินใจแล้ววิเคราะห์ได้
- ไม่ขึ้นกับการกระจายตัวของข้อมูล ไม่จำเป็นต้องทำข้อมูลให้เป็นมาตรฐานก่อน
- จัดการแยกข้อมูลที่มีลักษณะเป็นเกาะลอยได้ดี

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



วิธีการต้นไม้ตัดสินใจนั้นจริงๆแล้วมีรายละเอียดปลีกย่อยอะไรอีกมาก ซึ่งก็คงจะไม่ขอลงลึกแล้ว ในที่นี้แค่ต้องการเขียนแนวคิดคร่าวๆและพื้นฐานวิธีการสร้าง

หากต้องการใช้งานจริงๆอาจใช้ sklearn ซึ่งถูกเขียนมาอย่างดี สามารถปรับแต่งอะไรได้มากมาย เกี่ยวกับการใช้สามารถอ่านได้ใน https://phyblas.hinaboshi.com/20171108


อ้างอิง


-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> ปัญญาประดิษฐ์
-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python >> numpy
-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python >> matplotlib

ไม่อนุญาตให้นำเนื้อหาของบทความไปลงที่อื่นโดยไม่ได้ขออนุญาตโดยเด็ดขาด หากต้องการนำบางส่วนไปลงสามารถทำได้โดยต้องไม่ใช่การก๊อปแปะแต่ให้เปลี่ยนคำพูดเป็นของตัวเอง หรือไม่ก็เขียนในลักษณะการยกข้อความอ้างอิง และไม่ว่ากรณีไหนก็ตาม ต้องให้เครดิตพร้อมใส่ลิงก์ของทุกบทความที่มีการใช้เนื้อหาเสมอ

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
ภาษา javascript
ภาษา mongol
ภาษาศาสตร์
maya
ความน่าจะเป็น
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
-- บันทึกในฮ่องกง
-- บันทึกในมาเก๊า
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  ค้นหาบทความ

  บทความแนะนำ

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

บทความแต่ละเดือน

2024年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2023年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2022年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2021年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2020年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文