K-Nearest Neighbor, atau KNN, adalah salah satu algoritma supervised machine learning yang sangat populer dan seringkali digunakan untuk menyelesaikan kasus-kasus klasifikasi maupun regresi. Cara kerja dari KNN ini sebenarnya cukup sederhana, yaitu dengan mengidentifikasi “K” atau jumlah tetangga terdekat dari data yang sudah ada kemudian menggunakan sampel tersebut untuk menentukan prediksi nilai regresi ataupun klasifikasi dari data baru. Cara kerja dan metode dari KNN adalah non-parametrik karena algoritma ini tidak membuat asumsi apapun dari distribusi data yang ada, sederhananya KNN menentukan nilai atau kelas dari sebuah data dengan melihat data yang ada di sekitar data tersebut. Meskipun sederhana dan sudah diperkenalkan sejak 1950-an, algoritma ini masih jadi salah satu pilihan yang bisa diandalkan untuk membangun kasus-kasus machine learning, terutama pada dataset yang ukurannya relatif kecil hingga menengah dan sudah cukup bersih.
Untuk dapat memahami lebih dalam apa itu KNN dan bagaimana cara kerjanya, di dalam artikel ini kita akan menulis KNN dari nol menggunakan Python dengan OOP style (well, secara teknis gak dari nol banget juga karena kita masih menggunakan dependensi lain seperti NumPy hahaha). Lalu pada artikel berikutnya, kita akan coba mengimplementasikan algoritma yang sudah kita tulis ini ke dalam sebuah use-case.
Kita akan menulis tiga source code, yaitu base.py
, classification.py
, dan regression.py
, dan kemudian kita inisialisasi di dalam __init__.py
. Direktori dari projek ini akan terlihat kurang-lebih seperti berikut:
ml_from_scratch/
│
├── neighbors/
│ ├── __init__.py
│ ├── base.py
│ ├── classification.py
│ └── regression.py
│
└── try_ml_algo_notebooks/
│└── try_knn.ipynb
Inisialisasi Kelas baseKNN
Kita memulai projek kita dengan menulis base.py
dimana di dalamnya kita akan menyusun kelas baseKNN
. Kelas ini nantinya akan menjadi fondasi yang akan mengatur logika inti dalam perhitungan jarak antar data points dan juga sebagai penyimpanan data training.
import numpy as np
import pandas as pd
# ----- base class knn -----
# ----- handle distance calculation and storage of training data ----
class baseKNN:
def __init__(self, k=3, metric="euclidean", p=2):
"""
Initialized KNN base, default k value it's 3.
Parameters:
- k (int): The number of nearest neighbors
- metric (str): distance metric to use
- p (int): Order the norm untuk minkowski distance. Ignored untuk euclidean dan manhattan.
"""
self.k = k # jumlah neighbors yg akan digunakan untuk prediksi
self.X_train = None # placeholder aja untuk training features
self.y_train = None # placeholder juga untuk training target
self.metric = metric
self.p = p
# *************************************
def fit(self, X, y):
"""
Store the training set, convert to arrays if the are not already
Parameters:
- X (array): The training data features
- y (array): The training data target
"""
# cek input pandas dataframe atau pandas series
if isinstance(X, pd.DataFrame):
X = X.values # convert ke array
if isinstance(y, pd.Series):
y = y.values
self.X_train = np.array(X) if not isinstance(X, np.ndarray) else X # cek dan transform to array
self.y_train = np.array(y) if not isinstance(y, np.ndarray) else y # cek dan transform to array
# *************************************
def _calc_distance(self, x1, x2):
"""
Calculate distance antara 2 data point dengan beberapa pilihan distance metrics.
Default euclidean.
Parameters:
- x1 (array): First data point.
- x2 (array): Second data point.
Returns:
- distance (ndarray): Computed distance.
"""
if self.metric == "euclidean":
return np.sqrt(np.sum((x1-x2) ** 2, axis=1))
elif self.metric == "manhattan":
return np.sum(np.abs(x1-x2), axis=1)
elif self.metric == "minkowski":
return np.sum(np.abs(x1-x2) ** self.p, axis=1) ** (1/self.p)
else:
raise ValueError("Unsupported metric. Choose: euclidean, manhattan, or minkowski only.")
# *************************************
def _get_nearest_neighbors(self, x):
"""
Identify the nearest neighbors of a given data point.
This function will calculates the distance from the given point to all data points in the training set,
and then sort the distance, select the index of the k smallest distance.
Parameters:
- x (array): Data point for which to findthe nearest neighbors.
Returns:
- (array): The labels of the k-nearest neighbors.
"""
distances = self._calc_distance(x, self.X_train) # calculate distance x ke semua X training points
nn_idx = np.argsort(distances)[:self.k]
return self.y_train[nn_idx], distances[nn_idx]
Dengan kode di atas, kita akan melakukan penyimpanan data, menghitung jarak antar data point, dan mengidentifikasi tetangga terdekat dari data point yang ingin diprediksi. Kelas baseKNN
ini merupakan superclass yang nantinya akan mewarisi fungsi dan properti ke dalam subclass seperti ke regressionKNN
dan classificationKNN
.
Adapun salah satu fungsi paling krusial di dalam baseKNN
adalah calc_distance
yang bertugas untuk menghitung jarak antar data point. Fungsi ini mempunyai beberapa pilihan, naum yang paling umum digunakan adalah euclidean
. Euclidean mengukur jarak “langsung” antara dua titik, yang dapat kita bayangkan seperti menghitung panjang dari garis lurus yang menghubungkan dua titik tersebut dalam ruang berdimensi-n. Berikut adalah persamaan untuk menghitung jarak Euclidean:
Di dalam kode kita, hal ini diterjemahkan menjadi:
np.sqrt(np.sum((x1 - x2) ** 2, axis=1))
Kelas regressionKNN
Kelas ini akan mengimplementasikan fungsi untuk menangani kasus-kasus regresi di dalam konteks KNN. Dengan mengextend kelas baseKNN
, kelas regressionKNN
ini mewarisi semua fungsi dan properti dari superclassnya, termasuk penyimpanan dataset training dan pengaturan parameter seperti jumlah tetangga, metrik perhitungan jarak, dan lain-lain.
from .base import baseKNN
import numpy as np
import pandas as pd
class regressionKNN(baseKNN):
def __init__(self, k=3, metric="euclidean", p=2):
super().__init__(k, metric, p)
# *************************************
def predict(self, X):
"""
Predict the target value for each sample in X
Parameters:
- X (array or dataframe): Input samples; [n_samples, n_features]
Returns:
- predictions (array): Predicted values.
"""
if isinstance(X, pd.DataFrame):
X = X.values
predictions = [] # init empty list to store predictions
for i in X:
# for each sample in X, find its nearest neighbors in the training set
neighbors_labels, _ = self._get_nearest_neighbors(i.reshape(1, -1))
nearest_neighbors, nearest_neighbors_distances = self._get_nearest_neighbors(i)
# calculate the average from the neighbors
predictions.append(np.mean(neighbors_labels))
return np.array(predictions), np.array(nearest_neighbors), np.array(nearest_neighbors_distances)
Di dalam regressionKNN
terdapat dua fungsi utama; __init__
dan juga predict
. Di dalam fungsi predict
, kita melakukan beberapa langkah seperti mengkonversi DataFrame ke array jika input yang kita dapatkan berupada DataFrame, lalu kemudian kita akan melakukan pengulangan ke dalam setiap sampel dengan memanfaatkan _get_nearest_neighbors
yang diwarisi dari kelas baseKNN
. Setelah itu, kita menghitung rata-rata dari target atau label tetangga terdekat kita dan kemudian menggunakan hasilnya sebagai nilai prediksi yang dikembalikan sebagai output predict
.
Kelas classificationKNN
Subclass terakhir yang akan kita tulis dalam projek ini adalah classficationKNN
dan kita simpan di dalam script classification.py
. Kelas ini nantinya khusus digunakan untuk menyelesaikan kasus-kasus klasifikasi, dimana kita akan memprediksi kategori atau grup dri satu entitas berdasarkan fitur ataupun atributnya. Sama seperti regressionKNN
, kelas kita ini juga akan memanfaatkan superclass baseKNN
.
from .base import baseKNN
import numpy as np
import pandas as pd
from collections import Counter
class classificationKNN(baseKNN):
def __init__(self, k=3, metric="euclidean", p=2):
super().__init__(k, metric, p)
# *************************************
def predict(self, X):
"""
Predict the class label and probability score for each sample in X
Parameters:
- X (array or dataframe): Input samples; [n_samples, n_features]
Returns:
- predictions (list of dict): Predicted class and their proba score.
"""
if isinstance(X, pd.DataFrame):
X = X.values
predictions = [] # init empty list to store predictions
for i in X:
neighbors_labels, _ = self._get_nearest_neighbors(i.reshape(1, -1))
# count
votes = Counter(neighbors_labels)
majority_vote_class, majority_vote_count = votes.most_common(1)[0]
prob_score = majority_vote_count / self.k
predictions.append(({"class":majority_vote_class, "probability":prob_score}))
return predictions
Di dalam fungsi predict
pada classificationKNN
, kita melakukan beberapa langkah awal seperti apa yang kita lakukan pada regressionKNN
. Yang menjadi pembeda adalah fungsi classificationKNN
menentukan kategori mana yang paling sesuai untuk data point yang ingin kita prediksi dengan menghitung kategori terbanyak dari tetangga-tetangga terdekat dari data point tersebut, sementara itu regressionKNN
menghitung nilai rata-rata dari tetangga di sekitar data point yang ingin diprediksi. Selain menghitung kategori dengan majority vote, di dalam fungsi ini kita juga menghitung skor probabilitas dari kategori mayoritas tersebut. Nilai yang dikembalikan dari classficationKNN
ini berupa dictionary, dimana kita akan mendapatkan kategori mayoritas dan skor probabilitasnya.
__init__.py
Setelah selesai menulis baseKNN
, regressionKNN
, dan classificationKNN
, kita akan mengelola kode-kode ini di dalam script __init__.py
.
from .base import baseKNN
from .classification import classificationKNN
from .regression import regressionKNN
__all__ = [
"baseKNN",
"classificationKNN",
"regressionKNN"
]
Kenapa kita memperlukan script ini? Secara sederhana, sebenarnya ini akan membantu kita untuk nantinya dapat mengakses dan menggunakan baseKNN
, regressionKNN
, dan juga classificationKNN
tanpa perlu tau detail import internal juga struktur direktori dimana kelas-kelas ini disimpan.
Eksperimen dan Pengujian Awal
Di dalam notebook try-knn.ipynb
, kita melakukan beberapa eksperimen untuk menguji juga mengimplementasi algoritma KNN yang telah kita tulis dengan menggunakan data dummy. Melalui proses ini, kita akan coba memahami gimana sih KNN yang sudah kita tulis bisa berkerja untuk kasus-kasus seperti regresi maupun klasifikasi, apakah sudah sesuai dengan apa yang kita ekspektasikan atau masih belum memenuhinya. Berikut adalah beberapa eksperimen yang kita lakukan.
Pengujian Awal baseKNN
# import packages
import sys
sys.path.append("../")
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from ml_from_scratch.neighbors import baseKNN, regressionKNN, classificationKNN
# generate sample data
np.random.seed(666)
X_train = np.random.rand(15, 2) * 100 # 15 rows, 2 kolom
y_train = np.arange(15)
display(X_train, y_train)
>>> array([[70.04371219, 84.41866429], [67.65143359, 72.78580572], [95.14579574, 1.2703197 ], [41.35876988, 4.88127938], [ 9.99285613, 50.80663058], [20.02475393, 74.41541688], [19.2892003 , 70.08447522], [29.32281059, 77.44794543], [ 0.51088388, 11.28576536], [11.0953672 , 24.76682287], [ 2.32362992, 72.73211542], [34.0034942 , 19.75031564], [90.91795928, 97.8346985 ], [53.28025441, 25.91318494], [58.38126188, 32.56906529]])
>>> array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
# init base knn
model = baseKNN(k=3, metric="euclidean")
# fit model
model.fit(X_train, y_train)
# set test point
test_point = np.array([[30, 15]])
# find nearest neighbors
nearest_neighbors, nearest_neighbors_distances = model._get_nearest_neighbors(test_point)
display(print(f"calculated nearest distance: {nearest_neighbors_distances}"),
print(f"labels of the nearest neighbors: {nearest_neighbors}"))
>>> calculated nearest distance: [ 6.21236384 15.21217145 21.27853309]
>>> labels of the nearest neighbors: [11 3 9]
Pada tahap awal ini, kita menginisialisasi dan menguji objek baseKNN
menggunakan dataset dummy. Kita menentukan jumlah k
, metrik
jarak, dan kemudian memasukkan dataset ke dalam model untuk menemukan tetangga terdekat dari titik uji. Hasilnya memberikan kita pemahaman tentang bagaimana KNN mencari dan menemukan tetangga terdekat berdasarkan metrik yang diberikan. Dari data point yang menjadi data test, kita mendapatkan jika label 11, 3, dan 9 adalah tiga tetangga terdekat dengan menggunakan metrik euclidean
.
Pengujian baseKNN
dengan Pandas DataFrame
# generate sample data
np.random.seed(666)
X_train = np.random.rand(15, 2) * 100 # 15 rows, 2 kolom
y_train = np.arange(15)
# build dataframe
data = np.hstack((X_train, y_train.reshape(-1, 1))) # combine x dan y secara horizontal
sample_df = pd.DataFrame(data, columns=["Feature_1", "Feature_2", "Target"])
# display dataframe
sample_df
>>> Feature_1 Feature_2 Target
0 70.043712 84.418664 0.0
1 67.651434 72.785806 1.0
2 95.145796 1.270320 2.0
3 41.358770 4.881279 3.0
4 9.992856 50.806631 4.0
5 20.024754 74.415417 5.0
6 19.289200 70.084475 6.0
7 29.322811 77.447945 7.0
8 0.510884 11.285765 8.0
9 11.095367 24.766823 9.0
10 2.323630 72.732115 10.0
11 34.003494 19.750316 11.0
12 90.917959 97.834699 12.0
13 53.280254 25.913185 13.0
14 58.381262 32.569065 14.0
# init base knn
model = baseKNN(k=3, metric="euclidean")
# fit model
X_train = sample_df.drop(columns="Target", axis=0)
y_train = sample_df["Target"]
model.fit(X_train, y_train)
# set test point
test_point = np.array([[60, 14]])
# find nearest neighbors
nearest_neighbors, nearest_neighbors_distances = model._get_nearest_neighbors(test_point)
display(print(f"calculated nearest distance: {nearest_neighbors_distances}"),
print(f"labels of the nearest neighbors: {nearest_neighbors}"))
>>> calculated nearest distance: [13.67768095 18.63948763 20.75202463]
>>> labels of the nearest neighbors: [13. 14. 3.]
Di tahap ini, kita mengadaptasi proses untuk menggunakan Pandas DataFrame, yang lebih umum digunakan dalam proyek-proyek data science. Kita menggunakan dua kolom feature dari DataFrame sample_df
sebagai X_train
dan 1 kolom target sebagai y_train
. Di proses ini, kita menguji bagaima model KNN kita dapat bekerja dengan tipe data yang berbeda dan menunjukkan fleksibilitas untuk implementasi kita kedepannya.
Pengujian regressionKNN
# generate sample data
np.random.seed(666)
X_train = np.random.rand(15, 2) * 100 # 15 rows, 2 kolom
y_train = np.arange(15)
# build dataframe
data = np.hstack((X_train, y_train.reshape(-1, 1))) # combine x dan y secara horizontal
sample_df = pd.DataFrame(data, columns=["Feature_1", "Feature_2", "Target"])
# display dataframe
sample_df.head()
>>> Feature_1 Feature_2 Target
0 70.043712 84.418664 0.0
1 67.651434 72.785806 1.0
2 95.145796 1.270320 2.0
3 41.358770 4.881279 3.0
4 9.992856 50.806631 4.0
# init regression knn
model = regressionKNN(k=3, metric="euclidean")
# fit model
X_train = sample_df.drop(columns="Target", axis=0)
y_train = sample_df["Target"]
model.fit(X_train, y_train)
# set test point
test_point = np.array([[70, 70]])
# predict
prediction, nearest_neighbors, nearest_neighbors_distances = model.predict(test_point)
display(print(f"prediction: {prediction}"),
print(f"nearest neighbors: {nearest_neighbors}"),
print(f"calculated nearest distance: {nearest_neighbors_distances}"))
>>> prediction: [4.33333333]
>>> nearest neighbors: [ 1. 0. 12.]
>>> calculated nearest distance: [ 3.64369012 14.41873054 34.81855053]
Oke, selanjutnya kita juga menguji implementasi regressionKNN
untuk melihat bagaimana model bisa digunakan untuk kasus regresi. Masih dengan menggunakan dataset dummy yang kita generate menggunakan NumPy, kita mencoba untuk memprediksi nilai target berdasarkan rata-rata nilai dari tetangga terdekatnya. Jadi, untuk data point test kita dengan nilai 70 pada kedua featurenya, tiga tertangga terdekatnya adalah 1, 0, dan 12, sehingga nilai rata-rata target kita adalah 4.333. Terlihat sudah cukup sepertinya untuk kasus-kasus regresi sederhana.
Pengujian classficationKNN
np.random.seed(555)
n_samples = 15
feature1_class0 = np.random.uniform(1, 20, n_samples)
feature2_class0 = np.random.uniform(5, 20, n_samples)
y_class0 = np.zeros(n_samples)
feature1_class1 = np.random.uniform(18, 30, n_samples)
feature2_class1 = np.random.uniform(22, 35, n_samples)
y_class1 = np.ones(n_samples)
X_train = np.vstack((np.column_stack((feature1_class0, feature2_class0)),
np.column_stack((feature1_class1, feature2_class1))))
y_train = np.hstack((y_class0, y_class1))
sample_df = pd.DataFrame(X_train, columns=["Feature_1", "Feature_2"])
sample_df["Target"] = y_train
test_point = np.array([[19, 19]])
sample_df.head(10)
>>> Feature_1 Feature_2 Target
0 14.638848 13.167030 0.0
1 1.909248 5.617921 0.0
2 18.944968 12.022494 0.0
3 14.041221 18.331201 0.0
4 12.042939 12.143878 0.0
5 3.710894 11.187335 0.0
6 18.972464 14.241598 0.0
7 7.459821 9.821248 0.0
8 12.788274 13.713039 0.0
9 9.606233 10.949211 0.0
plt.figure(figsize=(6,5))
plt.scatter(sample_df["Feature_1"], sample_df["Feature_2"], c=sample_df["Target"], label="Training")
plt.scatter(test_point[0][0], test_point[0][1], color="red", label="Test", zorder=5)
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.title("Dataset Sintetis")
plt.legend()
plt.show()
>>>
# init classification knn
model = classificationKNN(k=7, metric="euclidean")
# fit model
X_train = sample_df.drop(columns="Target", axis=0)
y_train = sample_df["Target"]
model.fit(X_train, y_train)
# predict
prediction = model.predict(test_point)
print(f"prediction: {prediction}")
>>> prediction: [{'class': 0.0, 'probability': 0.7142857142857143}]
Ini adalah tahap terakhir dari pengujian awal kita, yaitu menguji classficationKNN
apakah sudah sesuai dengan ekspektasi kita untuk menyelesaikan kasus klasifikasi. Dalam pengujian ini, kita menggunakan dataset dummy yang memiliki dua kelas berbeda; 0 dan 1. Dengan menggunakan euclidean
sebagai metrik pengukuran jaraknya dan 7 sebagai jumlah tetangga terdekat yang ingin dilihat, kita mendapatkan bahwa test point kita dengan nilai 19 pada kedua featurenya diprediksi sebagai kelas 0 dengan skor probabilitas sebesar 0.714.
Meskipun apa yang sudah kita tulis sudah cukup mengimplementasikan fungsi-fungsi esensial dari KNN, rasanya masih banyak opportunities untuk kita dapat melakukan improvement dari program kita ini, seperti optimisasi performanya (misal menambahkan opsi untuk implementasi dimensionality reduction), menambahkan lebih banyak metrik-metrik di luar tiga metrik yang sudah kita tetapkan, atau juga bisa menambahkan opsi untuk menghandle fitur-fitur kategorikal yang tentunya nanti akan ada di dalam dataset kita jika nantinya program ini akan kita gunakan untuk menyelesaikan real use-case.
Overall, marilah kita merasa cukup lebih dulu saat ini karena setidaknya kita sudah mendapat sebuah milestone; mendemostrasikan dan mengimplementasikan bagaimana KNN bekerja untuk membantu dapat membantu kita menyelesaikan kasus-kasus regresi maupun klasifikasi dengan machine learning.
Mungkin pada artikel selanjutnya, kita akan mengembangkan proyek ini menjadi lebih lengkap dan juga mengimplementasikannya ke dalam dataset yang bukan lagi dummy.
Artikel ini merupakan bagian awal dari rangkaian proyek saya dalam mengimplementasikan dan menulis algoritma machine learning dari (hampir) awal. Materi dan pendekatan yang digunakan berasal dari kelas Advance Machine Learning di Sekolah Data Pacmann.
Silahkan kunjungi laman LinkedIn untuk bisa berkoneksi ataupun berkomunikasi lebih lanjut.