k-Nearest Neighbors (k-NN)#

ใน tutorial ก่อนหน้านี้ เราได้เรียนรู้การทำงานของ linear regression, logistic regression, support vector machine, decision tree และ random forest ซึ่งเป็น supervised machine learning algorithms ชนิดมีโมเดลพารามิเตอร์ที่ต้องปรับเพื่อ optimize โมเดล ใน tutorial นี้ เราจะมาแนะนำ k-Nearest Neighbors (k-NN) ซึ่งเป็นตัวอย่างของ supervised machine learning algorithm ชนิด non-parametric กล่าวคือ k-NN algorithm เป็นโมเดลที่ไม่มีโมเดลพารามิเตอร์ ทำได้ทั้งการจำแนกหมวดหมู่ (classification) และ การทำนายค่า (regression) โดยอ้างอิงจากความใกล้ชิดของจุดข้อมูล (proximity) บน feature space ภายใต้สมมติฐานที่ว่า จะพบจุดข้อมูลที่คล้ายกันอยู่ใกล้กัน โดยใน tutorial นี้ เราจะแสดงการทำงานของโมเดลกับโจทย์การจำแนกหมวดหมู่

Slides: K Nearest Neighbors


สมมติ ว่า เรามีจุดข้อมูลที่ต้องการทราบว่าอยู่ในคลาสไหน กระบวนการทำงานของ k-NN algorithm เริ่มต้นด้วยการค้นหาจุดข้อมูลเพื่อนบ้านจำนวน \(k\) จุดที่อยู่ใกล้กับจุดข้อมูลที่สนใจมากที่สุด โดยพิจารณาจากระยะห่างระหว่างจุดข้อมูลที่สนใจและจุดข้อมูลเพื่อนบ้าน จากนั้นโมเดลจะทำนายคลาสของจุดที่สนใจจากผลโหวตของจุดข้อมูลเพื่อนบ้านที่ถูกคัดเลือกมาจำนวน k จุดนั้น

KNN-Concept-low.png

k-NN algorithm adapted from ที่มา

จากรูปตัวอย่างนี้ โมเดล k-NN กำหนดจำนวนจุดข้อมูลเพื่อนบ้าน \(k=3\) ที่จะถูกใช้ในการโหวตเพื่อทำนายคลาสของจุดข้อมูลสีเหลือง โดยพบว่า ในวงเพื่อนบ้านทั้งสาม ประกอบด้วย class A จำนวน 1 จุดข้อมูล และ class B จำนวน 2 จุดข้อมูล ดังนั้น จุดข้อมูลสีเหลืองจะถูกทำนาย ว่า อยู่ใน class B

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

KNN-Distance-low.png

ที่มา A brief introduction to Distance Measures

สมมติว่า จุดข้อมูล 2 จุดบน feature space สามารถแทนด้วยเวกเตอร์ \(\vec{r_1}=(r_{1,1}, r_{1,2},..., r_{1,i},..., r_{1,m})\) และ \(\vec{r_2}=(r_{2,1}, r_{2,2},..., r_{2,i},..., r_{2,m})\) เมื่อ \(m\) คือ จำนวน features การหาระยะห่างระหว่างจุดข้อมูลทั้งสองสามารถคำนวณได้หลายวิธี โดยเราจะยกตัวอย่าง 3 สมการหลักที่ใช้กันทั่วไป ได้แก่

  1. Euclidean Distance

Euclidean distance คือระยะทางเป็นเส้นตรงระหว่างจุดสองจุดใน Euclidean space จึงสามารถคำนวณได้โดยใช้ทฤษฎีพีทาโกรัส (Pythagorean theorem)

\[ d_{Euclidean}(\vec{r_1},\vec{r_2}) = \sqrt{\sum_{j=1}^{m} (r_{1,j} - r_{2,j})^2} \]
  1. Manhattan Distance:

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

\[ d_{Manhattan}(\vec{r_1},\vec{r_2}) = \sum_{j=1}^{m} |r_{1,j} - r_{2,j}| \]
  1. Minkowski Distance

Minkowski distance เป็นการ generalization ของ Euclidean distance และ Manhattan distance

\[ d_{Minkowski}(\vec{r_1},\vec{r_2}) = \sum_{j=1}^{m} \big(|r_{1,j} - r_{2,j}|^p\big)^{1/p} \]

พารามิเตอร์ \(p\ge1\) ใช้กำหนดรูปแบบของระยะทาง โดย

  • เมื่อ \(p=1\) Minkowski distance จะมีค่าเท่ากับ Manhattan distance

  • เมื่อ \(p=2\) Minkowski distance จะมีค่าเท่ากับ Euclidean distance

  • เมื่อ \(1<p<2\) Minkowski distance จะมีคุณลักษณะผสมผสานระหว่าง Euclidean distance และ Manhattan distance ซึ่งสามารถควบคุมได้โดยการปรับค่า \(p\)

สำหรับ classification problem เราสามารถเทรนโมเดล k-NN Classifier โดยการเรียกใช้ KNeighborsClassifier จากไลบราลี่ scikit-learn ที่มี Minkowski distance และ \(p=2\) เป็น default metric (ซึ่งก็คือ Euclidean distance)

นอกจากนี้ ยังมีการวัดระยะห่างระหว่างจุดข้อมูลด้วยวิธีอื่นๆ อีก ซึ่งสามารถกลับไปย้อนดูตัวอย่างได้จากบทเรียน Nonlinear Methods

เพื่อศึกษาการทำงานของ k-NN เราจะลองสร้างชุดข้อมูลที่ประกอบไปด้วยข้อมูลจำนวน \(200\) จุด ที่ประกอบด้วย 2 features และ 2 classes ได้แก่ class 0 และ class 1 (2-class dataset for binary classification) โดยข้อมูลในแต่ละคลาสมีจำนวนเท่ากันที่ 100 จุด

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import colors
from sklearn.neighbors import KNeighborsClassifier as KNC
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold, KFold
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, ConfusionMatrixDisplay

# ตั้งค่า random seed สำหรับการทำซ้ำ (reproducibility)
RANDOM_SEED = 2566
def generate_multi_class_dataset(n_classes, mean_class, std_class, n_samples):
    # สร้างชุดข้อมูลแบบ multi-class
    # ตั้งค่า random seed เพื่อให้สามารถสร้างชุดข้อมูลเดิมทุกครั้ง เพื่อใช้สำหรับการสอน
    np.random.seed(RANDOM_SEED)

    # กำหนด จำนวน features
    n_features = 2

    #  สร้างข้อมูล x สำหรับแต่ละคลาส
    x_data = []
    for label in range(n_classes):
        _ = np.random.normal(mean_class[label], std_class[label], (n_samples, n_features))
        x_data.append(_)

    # สร้างข้อมูล y หรือ labels สำหรับแต่ละคลาส
    y_data = []
    y_data.append(np.zeros(n_samples))
    for label in range(1, n_classes):
        y_data.append(label*np.ones(n_samples))

    # รวมข้อมูล x และ y จากทุกคลาส
    x = np.vstack((x_data))
    y = np.hstack(y_data)

    return x, y

# ทดลองสร้างข้อมูลโดยการเรียกใช้ generate_multi_class_dataset
# กำหนด จำนวนคลาส
n_classes = 2

# กำหนดช่วงค่า Mean และ standard deviation สำหรับแต่ละคลาส
mean_class = [[-2,-2], [2,2]]
std_class  = [[0.5,0.5], [1.5,1.5]]

# กำหนดจำนวนข้อมูล สำหรับแต่ละคลาส
n_samples = 100

# ทำการสร้างชุดข้อมูล
x, y = generate_multi_class_dataset(n_classes, mean_class, std_class,n_samples)

# Plot ข้อมูล x, y ที่มีอยู่
plt.figure(figsize = (5,5))
plt.scatter(x[y==0, 0], x[y==0, 1], c='b', label='Class 0')
plt.scatter(x[y==1, 0], x[y==1, 1], c='r', label='Class 1')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.title('Simulated Data for Binary Classification')
plt.legend()
plt.tight_layout()
plt.show()
../../_images/0f0c3b7646e9df5ce1322fb400d68ed9841efb8a0f01037d5bc6a8b92cf0b896.png

k-NN เมื่อปรับชนิด 'metric' ที่ใช้คำนวณ distance ระหว่างจุดข้อมูล#

เราจะลองใช้ metric ที่แตกต่างกัน เพื่อดูผลของการจำแนกคลาสจาก decision boundary

# สร้างชุดโมเดล
weight = 'uniform'
models = (KNC(weights=weight, metric='euclidean'),
          KNC(weights=weight, metric='manhattan'),
          KNC(weights=weight,  metric='minkowski', p=3),
          )

# สอนโมเดลจากข้อมูล x, y ที่สร้างขึ้นก่อนหน้านี้
models = (clf.fit(x, y) for clf in models)

# ตั้งชื่อ plot ที่สอดคล้องกับชุดข้อมูล
titles = ['KNC: Euclidean distance',
          'KNC: Manhattan distance',
          'KNC: Minkowski distance (degree 3)'
          ]

# สร้าง cmap สำหรับแสดงผลตามสีแต่ละ class
cmap_2classes = colors.ListedColormap(['b', 'r'])

# plot the decision boundaries
fig, axes = plt.subplots(1,3, figsize=(4*3, 4))
for clf, title, ax in zip(models, titles, axes.flatten()):
    disp = DecisionBoundaryDisplay.from_estimator(clf,
                                                  x,
                                                  response_method="predict",
                                                  cmap=cmap_2classes,
                                                  alpha=0.5,
                                                  ax=ax,
                                                  xlabel='Feature 1',
                                                  ylabel='Feature 2',
                                                  )
    ax.scatter(x[:, 0], x[:, 1], c=y, cmap=cmap_2classes, s=20, edgecolors='k')
    ax.set_xticks(())
    ax.set_yticks(())
    ax.set_title(title)
plt.tight_layout()
plt.show()
../../_images/f8bc168990df6f1fe31b868afe5c9e4fbd27b09db904ba80030110b3cb89c450.png

รูปแสดง decision boundary ของโมเดล k-NN เมื่อเลือกกำหนด 'metric' ที่คำนวณ distance ระหว่างจุดข้อมูลที่ต่างกันไป

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

k-NN เมื่อปรับค่า n_neighbors#

จำนวนจุดข้อมูลเพื่อนบ้าน k จะกำหนดด้วย hyperparameter ชื่อ n_neighbors โดยมีค่า default เป็น n_neighbors = 5

# สร้างชุดโมเดล
models = [KNC(n_neighbors=3),
          KNC(n_neighbors=5),
          KNC(n_neighbors=25)
          ]

# สอนโมเดลจากข้อมูล x, y ที่สร้างขึ้นก่อนหน้านี้
models = [clf.fit(x, y) for clf in models]

# ตั้งชื่อ plot ที่สอดคล้องกับชุดข้อมูล
titles = ['KNC: n_neighbors=3',
          'KNC: n_neighbors=5',
          'KNC: n_neighbors=25'
          ]

# plot the decision boundaries
fig, axes = plt.subplots(1,3, figsize=(4*3, 4))
for clf, title, ax in zip(models, titles, axes.flatten()):
    disp = DecisionBoundaryDisplay.from_estimator(clf,
                                                  x,
                                                  response_method="predict",
                                                  cmap=cmap_2classes,
                                                  alpha=0.5,
                                                  ax=ax,
                                                  xlabel='Feature 1',
                                                  ylabel='Feature 2',
                                                  )
    ax.scatter(x[:, 0], x[:, 1], c=y, cmap=cmap_2classes, s=20, edgecolors='k')
    ax.set_xticks(())
    ax.set_yticks(())
    ax.set_title(title)
plt.tight_layout()
plt.show()
../../_images/8b299a79096bfb076cf5ff12f46ba1b90986c2996c0b693fcbdff95cae207675.png

การเลือกค่า n_neighbors ที่เหมาะสมจะมีความจำเพาะต่อชุดข้อมูล ยิ่งกำหนดให้ n_neighbors มีค่ามากจะช่วยลดผลของ noise ทำให้ได้ decision boundary ที่ smooth แต่ในขณะเดียวกันก็จะทำให้ความสามารถในการจำแนกลดลง ดังจะเห็นได้จากจำนวนข้อมูล class 1 (จุดสีแดง) ที่โมเดลทำนายผิด ว่าเป็น class 0 ที่ปรากฏในพื้นที่สีฟ้า

Quiz: ถ้ากำหนดให้ n_neighbors มีค่าสูงมาก ๆ จะเป็นยังไงบ้าง

k-NN เมื่อปรับค่า weights ของจุดข้อมูล#

ที่ผ่านมา โมเดลจะจำแนกคลาสของจุดข้อมูลที่สนใจจากจุดข้อมูลที่อยู่ใกล้เคียงเป็นจำนวน n_neighbors จุด โดยเลือกคลาสที่มีจำนวนมากที่สุดเป็นคลาสของจุดข้อมูลที่สนใจ ซึ่งเป็นการให้น้ำหนักในการโหวตกับทุกจุดข้อมูลเพื่อนบ้านเท่าเทียมกัน หรือ weights='uniform' ซึ่งเป็นค่า default ของโมเดล k-NN ในไลบรารี่ scikit-learn

แต่หากเรามีสมมติฐานที่ว่า เพื่อนบ้านที่อยู่ใกล้เราที่สุดจะรู้จักเราดีที่สุด เราสามารถเลือกใช้ weights='distance' เพื่อให้โมเดลให้น้ำหนักจุดข้อมูลแต่ละจุดไม่เท่ากัน โดยอ้างอิงตามระยะห่างของจุดข้อมูลนั้นและจุดที่สนใจ

# สร้างชุดโมเดล
models = [KNC(n_neighbors=5, weights='uniform'),
          KNC(n_neighbors=5, weights='distance')
          ]

# สอนโมเดลจากข้อมูล x, y ที่สร้างขึ้นก่อนหน้านี้
models = [clf.fit(x, y) for clf in models]

# ตั้งชื่อ plot ที่สอดคล้องกับชุดข้อมูล
titles = ['KNN with n_neighbors=5, weights="uniform"',
          'KNN with n_neighbors=5, weights="distance"'
          ]

# plot the decision boundaries
fig, axes = plt.subplots(1,2, figsize=(8, 4))
for clf, title, ax in zip(models, titles, axes.flatten()):
    disp = DecisionBoundaryDisplay.from_estimator(clf,
                                                  x,
                                                  response_method="predict",
                                                  cmap=cmap_2classes,
                                                  alpha=0.5,
                                                  ax=ax,
                                                  xlabel='Feature 1',
                                                  ylabel='Feature 2',
                                                  )
    ax.scatter(x[:, 0], x[:, 1], c=y, cmap=cmap_2classes, s=20, edgecolors='k')
    ax.set_xticks(())
    ax.set_yticks(())
    ax.set_title(title)
plt.tight_layout()
plt.show()
../../_images/1bd274294ca1df8fa5333d1802c9c79058023bfbc869ae8d5ecde7caad07f56b.png

รูปแสดง decision boundary ของโมเดล k-NN เมื่อเลือกกำหนด weights ต่างกัน

Quiz: จากรูปที่มีการเปรียบเทียบ decision boundary สามารถอธิบายได้หรือไม่ว่า weights ที่ต่างกันมีผลอย่างไร

k-NN Classification Pipeline#

ต่อไปเราจะลองพัฒนาโมเดล k-NN อย่างครบกระบวนการ ด้วยชุดข้อมูลที่จะสร้างขึ้น

Generate 3-class Dataset#

# สร้างข้อมูลโดยการเรียกใช้ generate_multi_class_dataset
# กำหนด จำนวนคลาส
n_classes = 3

# กำหนดช่วงค่า Mean และ standard deviation สำหรับแต่ละคลาส
mean_class = [[-2,2], [1,0], [2,1]]
std_class  = [[0.5,0.5], [0.75,0.75],[0.75,0.5]]

# กำหนดจำนวนข้อมูล สำหรับแต่ละคลาส
n_samples = 200

# ทำการสร้างชุดข้อมูล
x, y = generate_multi_class_dataset(n_classes, mean_class, std_class,n_samples)

# Plot ข้อมูล x, y ที่สร้างขึ้น
plt.figure(figsize = (5,5))
color_list = ['b','r','g']
cmap_3classes = colors.ListedColormap(color_list)
for label in range(n_classes):
    plt.scatter(x[y==label, 0], x[y==label, 1], c=color_list[label], label='Class '+str(label))

plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.title('Simulated 3-class dataset')
plt.legend()
plt.tight_layout()
plt.show()
../../_images/a8f2d8f623f80205ba45f42df9f3a78b61a6076320b232ec6bebbaf010657ea5.png

ข้อมูลที่สร้างขึ้นมีประกอบด้วย 2 features และ 3 classes (class: 0, 1, 2) โดยข้อมูลในแต่ละคลาสมีจำนวนเท่ากันที่ 200 จุด เราจะพัฒนาโมเดล k-NN เพื่อจำแนกข้อมูลแต่ละคลาสออกจากกัน

เมื่อสังเกตการกระจายตัวของข้อมูล พบว่า class 0 มีการกระจายข้อมูลแบ่งแยกออกมาอย่างเห็นได้ชัด ในขณะที่ class 1 และ class 2 จะกระจายตัวอยู่ร่วมกัน

แบ่งข้อมูลเป็น training data และ test data#

โดยใช้ train_test_split จากไลบรารี่ scikit-learn

ในตัวอย่างนี้เราจะใช้ default value ซึ่งจะ shuffle ข้อมูลก่อนแบ่งข้อมูล และ ไม่ stratify (ไม่กำกับสัดส่วนของคลาสใน training data และ test data)

อย่างไรก็ดี ในกรณีที่ข้อมูลประกอบด้วยคลาสต่างๆ ที่มีสัดส่วนต่างกันอย่างมาก (imbalanced dataset) การทำ stratify มีความจำเป็นอย่างมากเพื่อคงสัดส่วนของแต่ละคลาส ใน training data และ test data

# สร้าง training data และ test data โดยแบ่งจากชุดข้อมูล x,y
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2,
                                                    stratify=None,
                                                    shuffle=True,
                                                    random_state=RANDOM_SEED)

print('Train set: จำนวนข้อมูล แบ่งกลุ่มตาม class label')
unique, counts = np.unique(y_train, return_counts=True)
print(np.asarray((unique, counts)).T)

print('Test set: จำนวนข้อมูล แบ่งกลุ่มตาม class label')
unique, counts = np.unique(y_test, return_counts=True)
print(np.asarray((unique, counts)).T)
Train set: จำนวนข้อมูล แบ่งกลุ่มตาม class label
[[  0. 169.]
 [  1. 155.]
 [  2. 156.]]
Test set: จำนวนข้อมูล แบ่งกลุ่มตาม class label
[[ 0. 31.]
 [ 1. 45.]
 [ 2. 44.]]

ทำการ standardize ข้อมูลทั้งหมด#

ใช้ mean และ standard deviation (SD) จาก training data ในการ standardize test set เพื่อป้องกัน information leak

# สร้าง standardized scaler จาก features ใน training data
x_scaler = StandardScaler().fit(x_train)

# scale ค่า features ใน training data และ test data
x_train = x_scaler.transform(x_train)
x_test  = x_scaler.transform(x_test)

เทรนโมเดล ด้วย base model (default hyperparameter)#

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 3))

# สร้างโมเดล
base_clf = KNC()

# สอนโมเดลด้วย training data
base_clf.fit(x_train,y_train)

# ให้โมเดลทำนาย training data
y_pred = base_clf.predict(x_train)

# แสดงผล classification ของโมเดลจาก training data
print('Training Set: Classification report')
print(classification_report(y_train, y_pred))

# คำนวนและแสดงผล confusion matrix ของ training data
cm = confusion_matrix(y_train, y_pred)
display = ConfusionMatrixDisplay(confusion_matrix=cm)
display.plot(ax=ax1)
ax1.set_title('[Training Data] \nConfusion Matrix: base model')

# ให้โมเดลทำนาย test data
y_pred = base_clf.predict(x_test)

# แสดงผล classification ของโมเดล
print('\nTest Set: Classification report')
print(classification_report(y_test, y_pred))

# คำนวนและแสดงผล confusion matrix ของ test set
cm = confusion_matrix(y_test, y_pred)
display = ConfusionMatrixDisplay(confusion_matrix=cm)
display.plot(ax=ax2)
ax2.set_title('[Test Set] \nConfusion Matrix: base model')

plt.tight_layout()
plt.show()
Training Set: Classification report
              precision    recall  f1-score   support

         0.0       0.99      1.00      0.99       169
         1.0       0.89      0.87      0.88       155
         2.0       0.89      0.89      0.89       156

    accuracy                           0.92       480
   macro avg       0.92      0.92      0.92       480
weighted avg       0.92      0.92      0.92       480


Test Set: Classification report
              precision    recall  f1-score   support

         0.0       1.00      1.00      1.00        31
         1.0       0.81      0.87      0.84        45
         2.0       0.85      0.80      0.82        44

    accuracy                           0.88       120
   macro avg       0.89      0.89      0.89       120
weighted avg       0.88      0.88      0.87       120
../../_images/1ab75d8b24b8bcbffcaf635014681e6fea272fcc01a30b4a47d81350f3a1830d.png

ปรับแต่งโมเดล (Hyperparameter Tuning) ด้วย GridSearchCV และ train โมเดล#

โดยเราจะปรับแต่งโมเดล โดย tune 2 hyperparameters ที่สำคัญของโมเดล k-NN ได้แก่

  1. weights

  2. n_neighbors

ในไลบรารี่ scikit-learn ยังมีวิธีการอื่นๆ ในการปรับแต่งโมเดล เช่น

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 3))

# กำหนดช่วงค่า hyperparameters ในรูปแบบ dictionary
clf_params = {'weights': ['uniform','distance'],
              'n_neighbors': [3, 5, 7, 9, 11]
              }

# แบ่งข้อมูล training data ด้วย 5-fold cross-validation เพื่อ tune hyperparameter
cv_splitter = KFold(n_splits=5, shuffle=True, random_state=RANDOM_SEED)

# ใช้ GridSearchCV เพื่อสอนโมเดลจากชุดค่า hyperparameters จาก clf_params
# และคำนวณค่า accuracy ของแต่ละโมเดล เพื่อเลือกชุด hyperparameters ที่ดีที่สุด
# โดยใช้เทคนิค cross-validation ในการแบ่งกลุ่ม validation data จาก training data
tuned_clf = GridSearchCV(estimator=base_clf, param_grid=clf_params,
                             scoring=['accuracy'], refit='accuracy', cv=cv_splitter)

# fit โมเดลด้วย training data และ ให้โมเดลทำนายค่า y จาก training data
tuned_clf.fit(x_train, y_train)
y_pred = tuned_clf.predict(x_train)

# แสดงผล hyperparameters ที่ดีที่สุด และ cross-validation score
print('Best hyperparameters: {}'.format(tuned_clf.best_params_))
print("Best cross-validation score: {:.2f}".format(tuned_clf.best_score_))

# แสดงผล classification ของโมเดลจาก training data
print('Training Set: Classification report')
print(classification_report(y_train, y_pred))

# คำนวนและแสดงผล confusion matrix ของโมเดลจาก training data
cm = confusion_matrix(y_train, y_pred)
display = ConfusionMatrixDisplay(confusion_matrix=cm)
display.plot(ax=ax1)
ax1.set_title('[Training Data] \nConfusion Matrix: tuned model')

# ให้โมเดลทำนายค่า y จาก test data
y_pred = tuned_clf.predict(x_test)

# แสดงผล classification ของโมเดล จาก test data
print('\nTest Set: Classification report')
print(classification_report(y_test, y_pred))
# คำนวนและแสดงผล confusion matrix จาก test data
cm = confusion_matrix(y_test, y_pred)
display = ConfusionMatrixDisplay(confusion_matrix=cm)
display.plot(ax=ax2)
ax2.set_title('[Test Set] \nConfusion Matrix: tuned model')

plt.show()
Best hyperparameters: {'n_neighbors': 7, 'weights': 'distance'}
Best cross-validation score: 0.90
Training Set: Classification report
              precision    recall  f1-score   support

         0.0       1.00      1.00      1.00       169
         1.0       1.00      1.00      1.00       155
         2.0       1.00      1.00      1.00       156

    accuracy                           1.00       480
   macro avg       1.00      1.00      1.00       480
weighted avg       1.00      1.00      1.00       480


Test Set: Classification report
              precision    recall  f1-score   support

         0.0       1.00      1.00      1.00        31
         1.0       0.83      0.89      0.86        45
         2.0       0.88      0.82      0.85        44

    accuracy                           0.89       120
   macro avg       0.90      0.90      0.90       120
weighted avg       0.89      0.89      0.89       120
../../_images/90e23ec8ba3092ac60e03a33c8fd0487bb54c16f7c00782821272105993a7705.png

จะสังเกตได้ว่า เมื่อมีการปรับจูน hyperparameters ของโมเดลให้มีความเหมาะสมแล้ว เราได้โมเดลที่เรียนรู้จากข้อมูลชุดเดิม แล้วสามารถทำนาย test data ได้ค่า accuracy ที่สูงขึ้น โดยเมื่อพิจารณา Confusion Matrix จะพบว่า โมเดลสามารถจำแนกข้อมูล class 1 และ class 2 ได้ดีขึ้น

Decision Boundary#

เราจะมาลองดู decision boundary ของโมเดลบน training data และ test data

# plot the decision boundaries
fig, axes = plt.subplots(1,2, figsize=(7, 4))
titles = ['[Training Data] \nk-NN: Decision boundary', '[Test Data] \nk-NN: Decision boundary']
for x, y, title, ax in zip([x_train, x_test], [y_train, y_test], titles, axes.flatten()):
    disp = DecisionBoundaryDisplay.from_estimator(tuned_clf,
                                                  x,
                                                  response_method="predict",
                                                  cmap=cmap_3classes,
                                                  alpha=0.5,
                                                  ax=ax,
                                                  xlabel='Feature 1',
                                                  ylabel='Feature 2',
                                                  )
    ax.scatter(x[:, 0], x[:, 1], c=y, cmap=cmap_3classes, s=20, edgecolors='k')
    ax.set_xticks(())
    ax.set_yticks(())
    ax.set_title(title)
plt.tight_layout()
plt.show()
../../_images/4244775741fece45f89fb9caeb0a292ce278fd1bd33bbbfa9b8e145725825b7f.png

แสดง decision boundary ของโมเดล k-NN เมื่อ n_neighbors=7 และ weights='distance' ซึ่งเป็นโมเดลที่ได้รับการปรับจูนให้เหมาะสมกับการเรียนรู้จากชุดข้อมูลนี้

Discuss: ข้อเสียของโมเดล k-NN

เทคนิคนี้ต้องจัดเก็บ training set ทั้งอัน รวมถึงต้องคำนวณ distance ระหว่างแต่ละคู่จุดข้อมูล ซึ่งจะเป็นปัญหาอย่างมากเมื่อเรามีจำนวนจุดข้อมูลใน training set จำนวนมาก

ในทางปฏิบัติ เวลาที่จะหา neighbors ใกล้ตัวของจุดข้อมูล เราสามารถใช้เทคนิค tree-based search structures เพื่อให้สามารถหา neighbors เหล่านั้นได้อย่างรวดเร็วกว่าการไปไล่ดูทุกจุดข้อมูลใน training set แต่ก็ต้องแลกมากับความคาดเคลื่อนที่อาจเกิดขึ้นได้

ผู้จัดเตรียม code ใน tutorial: ดร. กนกกร พิมพ์เจริญ