🎯 เป้าหมาย: เข้าใจโครงสร้าง BaseEstimator, Mixins และการใช้ SOLID Principles ในการออกแบบ ML Pipeline
ไม่ว่าจะเป็น Linear Regression, SVM หรือ Random Forest ทุกอย่างมีวิธีเรียกใช้เหมือนกัน:
model.fit(X, y)
model.predict(X)
วัตถุที่เรียนรู้จากข้อมูล
estimator.fit(data)
วัตถุที่แปลงข้อมูล (Data Processing)
transformer.transform(data)
วัตถุที่ทำนายผลลัพธ์
predictor.predict(data)
คลาสแม่แบบ (Parent Class) ที่ทุกโมเดลใน Scikit-learn สืบทอดมา [1]
from sklearn.base import BaseEstimator class MyModel(BaseEstimator): def __init__(self, param1=1): self.param1 = param1 # เก็บค่าพารามิเตอร์
get_params(): ดึงค่าพารามิเตอร์ทั้งหมด (ใช้ตอนจูนโมเดล)set_params(): ตั้งค่าพารามิเตอร์ใหม่Scikit-learn ใช้ Multiple Inheritance เพื่อแยกความสามารถ (ISP Principle)
สำหรับโมเดลจำแนกประเภท (Classification)
score() ที่คืนค่า Accuracyสำหรับตัวแปลงข้อมูล
fit_transform() ให้อัตโนมัติ"แยกการเตรียมข้อมูล (Preprocessing) ออกจากตัวโมเดล (Model)"
เขียนโค้ด Scaling ข้อมูล, เติมค่าว่าง, และเทรนโมเดล รวมอยู่ในคลาสเดียว
"เปิดรับการขยาย (Extension) แต่ปิดกั้นการแก้ไข (Modification)"
Scikit-learn อนุญาตให้คุณสร้าง Custom Estimator ได้เอง
คุณสามารถสร้างอัลกอริทึมใหม่ หรือวิธีการแปลงข้อมูลแบบใหม่ โดยการสืบทอดจาก BaseEstimator แล้วนำไปใช้ร่วมกับฟังก์ชันมาตรฐานอย่าง GridSearchCV หรือ Pipeline ได้ทันที โดยไม่ต้องแก้โค้ดของ Library [1]
class OutlierRemover(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
return self
def transform(self, X):
# Logic การตัด Outlier ของเราเอง
return X[X < 100]
"ความสามารถในการแทนที่กันได้ (Interchangeability)"
เนื่องจากทุก Estimator มีเมธอด fit() และ predict() เหมือนกัน เราจึงสามารถเขียนโค้ดตรวจสอบผลลัพธ์ (Evaluation Code) เพียงชุดเดียว แล้วส่งโมเดลใดก็ได้เข้าไป
def evaluate_model(model, X, y):
model.fit(X, y)
print(model.score(X, y))
# ส่งอะไรไปก็ได้
evaluate_model(SVC(), X, y)
evaluate_model(RandomForestClassifier(), X, y)
Pipeline ของ Scikit-learn ไม่ได้ผูกติดกับคลาส StandardScaler หรือ SVC โดยตรง แต่ผูกติดกับ Abstraction (วัตถุที่มีเมธอด fit/transform)
การมองกลุ่มของวัตถุ ให้เป็นวัตถุเดียว (Treating a group of objects as a single object)
Pipeline คือตัวอย่างที่ชัดเจนที่สุด มันเก็บ List ของ Estimators ไว้ข้างใน
เมื่อเราเรียก pipeline.fit() มันจะวนลูปเรียก fit_transform() ของทุกตัว แล้วส่งผลลัพธ์ต่อไปเป็นทอดๆ
pipe = Pipeline([
('scaler', StandardScaler()),
('pca', PCA()),
('svc', SVC())
])
# เรียกครั้งเดียว ทำงานทั้ง 3 ขั้นตอน
pipe.fit(X, y)
การสลับเปลี่ยนอัลกอริทึมภายใน Object
ใน Scikit-learn เราเห็นสิ่งนี้ผ่าน Hyperparameters ที่เปลี่ยนพฤติกรรมของโมเดล
# เปลี่ยน Strategy ของ Kernel# ภายในคลาส SVC จะมี Logic เพื่อเลือกใช้สมการคณิตศาสตร์ที่ต่างกันตาม Strategy ที่เลือก [2]
Base Class กำหนดโครงสร้าง แต่ให้ Subclass เติมเต็มส่วนที่ขาด
ใน BaseEstimator:
มีการเตรียมเมธอด get_params() และ set_params() ไว้ให้แล้ว โดยอาศัยการ Introspect (ตรวจสอบ) __init__ arguments [1]
ใน Custom Estimator:
เราแค่เขียน __init__ ให้ถูกต้องตามกฎ Scikit-learn ก็จะได้ฟีเจอร์เหล่านั้นไปใช้ทันที
เพื่อให้ OOP ของ Scikit-learn (เช่นการโคลนโมเดล) ทำงานได้ ต้องปฏิบัติตามนี้ [3]:
__init__*args หรือ **kwargs
def __init__(self, param1=1):
self.param1 = param1
รับค่าแล้วเก็บใส่ตัวแปรชื่อเดียวกันทันที
"ถ้ามันเดินเหมือนเป็ด และร้องเหมือนเป็ด มันก็คือเป็ด"
Scikit-learn ไม่ได้เช็ค Type อย่างเข้มงวดตลอดเวลา (เช่น isinstance(obj, Classifier)) แต่เช็คว่าวัตถุนั้นมีเมธอดที่ต้องการหรือไม่ [4]
transform() ถือว่าเป็น Transformerpredict() ถือว่าเป็น Estimator/Predictor
เราจะสร้างคลาสที่สืบทอดจาก BaseEstimator และ TransformerMixin
TransformerMixin จะแถมเมธอด fit_transform() มาให้ฟรี ซึ่งมันก็คือการเรียก fit() แล้วตามด้วย transform() นั่นเอง
from sklearn.base import BaseEstimator, TransformerMixin class MyScaler(BaseEstimator, TransformerMixin): def __init__(self, factor=1): self.factor = factor def fit(self, X, y=None): # เรียนรู้ค่า mean, etc (ถ้ามี) return self def transform(self, X): return X * self.factor
ในโลกความเป็นจริง ข้อมูลมักมีทั้งตัวเลข (Numerical) และตัวหนังสือ (Categorical)
ColumnTransformer ช่วยให้เราแยก (Split) การประมวลผลตามคอลัมน์ แล้วรวม (Combine) กลับมาใหม่
⬇️ รวมร่าง ⬇️
Model Input Vector
OOP ช่วยให้เราบันทึกสถานะของ Object (Model ที่เทรนแล้ว) ลงไฟล์ได้
Python ใช้กลไก Serialization เพื่อแปลง Object ใน Memory ให้เป็น Binary File
joblib.dump(model, 'model.pkl')
เราสามารถโหลดไฟล์นี้กลับมาเป็น Object พร้อมใช้งานได้ทันที (State ของ self.coef_ ถูกบันทึกไว้ครบ)
model = joblib.load('model.pkl')
BaseEstimator เพื่อฟีเจอร์มาตรฐาน
TransformerMixin เพื่อ fit_transform
Pipeline เชื่อมต่อ Object หลายตัว
สร้าง Custom Transformer และ Pipeline สำหรับข้อมูล Iris Dataset
เราจะสร้าง Class OutlierRemover เพื่อตัดข้อมูลที่ผิดปกติ แล้วนำไปต่อกับ SVM เพื่อจำแนกพันธุ์ดอกไม้
ติดตั้ง Library ที่จำเป็น
pip install scikit-learn pandas numpy matplotlib
หรือกรณีที่ใช้ uv
uv add scikit-learn pandas numpy matplotlib
สร้างไฟล์ lab_sklearn_oop.py แล้ว import:
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.svm import SVC
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
ใช้ Dataset มาตรฐาน Iris (ดอกไม้ 3 สายพันธุ์)
# โหลดข้อมูล iris = load_iris() X = iris.data y = iris.target # แบ่ง Train/Test (80/20) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 )
สร้างคลาส OutlierClipper ที่สืบทอดจาก BaseEstimator และ TransformerMixin
class OutlierClipper(BaseEstimator, TransformerMixin): def __init__(self, factor=1.5): self.factor = factor # ตัวคูณ IQR def fit(self, X, y=None): # คำนวณขอบเขตล่างและบน (Lower/Upper Bound) # เดี๋ยวเราจะเติมโค้ดตรงนี้ return self def transform(self, X): # แทนที่ค่าที่เกินขอบเขต # เดี๋ยวเราจะเติมโค้ดตรงนี้ return X_clipped
ในเมธอด fit เราจะเรียนรู้จากข้อมูล (X) ว่าขอบเขตของข้อมูลควรเป็นเท่าไหร่ (ใช้ IQR Technique)
def fit(self, X, y=None): # X เป็น numpy array # คำนวณ Quartile 1 และ 3 Q1 = np.percentile(X, 25, axis=0) Q3 = np.percentile(X, 75, axis=0) IQR = Q3 - Q1 # บันทึก State ลงใน Object (Attributes มี _ ต่อท้ายตามธรรมเนียม) self.lower_bound_ = Q1 - (self.factor * IQR) self.upper_bound_ = Q3 + (self.factor * IQR) return self # ต้องคืนค่า self เสมอเพื่อให้ Chain ได้
ใช้ค่า lower_bound_ และ upper_bound_ ที่เรียนรู้มา เพื่อจำกัดค่า (Clip) ข้อมูล
def transform(self, X): # ตรวจสอบว่า fit หรือยัง? (Optional แต่ดี) if not hasattr(self, "lower_bound_"): raise RuntimeError("This OutlierClipper instance is not fitted yet.") # ใช้ np.clip เพื่อจำกัดค่าให้อยู่ในช่วง return np.clip(X, self.lower_bound_, self.upper_bound_)
เชื่อมต่อ Custom Transformer ของเราเข้ากับ SVM Classifier
# สร้าง Pipeline: (Preprocessing) -> (Model) pipeline = Pipeline([ ('clipper', OutlierClipper(factor=1.5)), ('svm', SVC(kernel='linear')) ]) # สั่งเทรนทีเดียว (จะเรียก clipper.fit -> clipper.transform -> svm.fit) pipeline.fit(X_train, y_train) print("Training Complete!")
ทดสอบ Pipeline กับ Test Set
# ทำนายผล # (เรียก clipper.transform -> svm.predict) y_pred = pipeline.predict(X_test) # ตรวจสอบความแม่นยำ score = pipeline.score(X_test, y_test) print(f"Accuracy: {score:.2f}")
เราไม่ต้องเขียนโค้ดเพื่อ Transform X_test แยกต่างหาก Pipeline จัดการให้เราเอง ทำให้ลดความเสี่ยงเรื่อง Data Leakage
ลองเปลี่ยนจาก SVC เป็น RandomForestClassifier โดยไม่แก้ Pipeline Logic
from sklearn.ensemble import RandomForestClassifier # เปลี่ยนแค่ตัวสุดท้ายของ Pipeline pipeline.steps.pop() # เอา SVM ออก pipeline.steps.append(('rf', RandomForestClassifier())) # Fit ใหม่ได้เลย pipeline.fit(X_train, y_train) print(f"New Accuracy: {pipeline.score(X_test, y_test):.2f}")
เนื่องจากเราสืบทอด BaseEstimator เราจึงใช้ get_params() ได้ทันที
# ดูพารามิเตอร์ทั้งหมดใน Pipeline params = pipeline.get_params() print(params) # เราจะเห็น 'clipper__factor' อยู่ในนั้น # ลองเปลี่ยนค่า factor ผ่าน Pipeline pipeline.set_params(clipper__factor=2.0) pipeline.fit(X_train, y_train) # เทรนใหม่ด้วย factor ใหม่
"คุณได้สร้าง OOP Component ที่ทำงานร่วมกับ Standard Library ระดับโลกได้"
fit() เพื่อเรียนรู้, transform() เพื่อแปลงBaseEstimator ให้ฟีเจอร์พื้นฐานMixin ให้ Interface มาตรฐาน