พื้นฐานการเรียนรู้ของเครื่อง (Machine Learning Foundations)

วิชา: Artificial Intelligence | สัปดาห์ที่: 9–10
เครื่องมือหลัก: scikit-learn, pandas, numpy, matplotlib, seaborn, OpenAI Gym


ประวัติและวิวัฒนาการของ Machine Learning

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945',
  'lineColor': '#d79921',
  'secondaryColor': '#3c3836',
  'tertiaryColor': '#1d2021',
  'background': '#282828',
  'mainBkg': '#282828',
  'nodeBorder': '#504945',
  'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2',
  'edgeLabelBackground': '#3c3836',
  'fontFamily': 'Sarabun, sans-serif'
}}}%%
flowchart TB
  subgraph ERA1["ยุคที่ 1: รากฐาน (1940–1960)"]
    A1["1943 - McCulloch-Pitts Neuron - เซลล์ประสาทเทียม"]
    A2["1950 - Alan Turing - Turing Test"]
    A3["1957 - Perceptron - (Frank Rosenblatt)"]
  end
  subgraph ERA2["ยุคที่ 2: AI Winter & Revival (1960–1990)"]
    B1["1960s - Neural Net Research - วิจัยโครงข่าย"]
    B2["1969 - Minsky & Papert - XOR Problem"]
    B3["1986 - Backpropagation - Rumelhart et al."]
  end
  subgraph ERA3["ยุคที่ 3: ยุคทอง ML (1990–2010)"]
    C1["1995 - SVM - Cortes & Vapnik"]
    C2["2001 - Random Forest - (Breiman)"]
    C3["2006 - Deep Learning - (Hinton et al.)"]
  end
  subgraph ERA4["ยุคที่ 4: Modern ML (2010–ปัจจุบัน)"]
    D1["2012 - AlexNet - ImageNet Winner"]
    D2["2017 - Transformer - Attention is All You Need"]
    D3["2022– - LLMs: GPT, Claude - Generative AI"]
  end
  ERA1 --> ERA2 --> ERA3 --> ERA4

  style ERA1 fill:#3c3836,stroke:#d79921,color:#ebdbb2
  style ERA2 fill:#3c3836,stroke:#458588,color:#ebdbb2
  style ERA3 fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
  style ERA4 fill:#3c3836,stroke:#fb4934,color:#ebdbb2

1. Supervised Learning (การเรียนรู้แบบมีผู้สอน)

Supervised Learning คือกระบวนการที่โมเดลเรียนรู้จากข้อมูลที่มีป้ายกำกับ (labeled data) โดยมีทั้งข้อมูลนำเข้า (input features) และคำตอบที่ถูกต้อง (target/label) เพื่อให้โมเดลสามารถทำนายผลลัพธ์สำหรับข้อมูลใหม่ที่ยังไม่เคยเห็น

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945',
  'lineColor': '#d79921',
  'secondaryColor': '#3c3836',
  'background': '#282828',
  'mainBkg': '#3c3836',
  'nodeBorder': '#504945',
  'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2',
  'edgeLabelBackground': '#3c3836',
  'fontFamily': 'Sarabun, sans-serif'
}}}%%
flowchart TD
  A["📊 ข้อมูลฝึก - (Training Data) - X, y"]
  B["🧠 อัลกอริทึม ML - (ML Algorithm)"]
  C["📦 โมเดล - (Model)"]
  D["📋 ข้อมูลใหม่ - (New Data) X_new"]
  E["🎯 การทำนาย - (Prediction) ŷ"]
  F["📈 การประเมิน - (Evaluation) - Accuracy / MSE"]

  A --> B --> C
  D --> C --> E
  E --> F

  style A fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
  style B fill:#3c3836,stroke:#d79921,color:#ebdbb2
  style C fill:#3c3836,stroke:#458588,color:#ebdbb2
  style D fill:#3c3836,stroke:#8ec07c,color:#ebdbb2
  style E fill:#3c3836,stroke:#fb4934,color:#ebdbb2
  style F fill:#3c3836,stroke:#d3869b,color:#ebdbb2

1.1 Classification (การจำแนกประเภท)

Classification คือการทำนายว่าข้อมูลนั้นอยู่ในกลุ่ม (class/category) ใด ผลลัพธ์เป็นค่าแบบกลุ่ม (discrete) เช่น สแปม/ไม่สแปม, ป่วย/ไม่ป่วย, ชนิดดอกไม้

1.1.1 Decision Tree (ต้นไม้ตัดสินใจ)

Decision Tree คือโมเดลที่ตัดสินใจโดยใช้กฎ if-else แบบลำดับชั้น ทำงานโดยแบ่งข้อมูลออกตาม feature ที่ให้ข้อมูลสูงสุด (highest information gain)

สูตรคำนวณ Information Gain:

Entropy (S) = - i=1 c pi log2 pi IG (S,A) = Entropy(S) - vValues(A) |Sv| |S| Entropy(Sv)

โดยที่:

ตัวอย่างการคำนวณ Entropy:

สมมติมีข้อมูลผู้ป่วย 10 คน: ป่วย 7 คน, ไม่ป่วย 3 คน

Entropy(S) = - 710 log2 710 - 310 log2 310 = 0.881 bits
"""
ตัวอย่างการสร้าง Decision Tree สำหรับจำแนกสายพันธุ์ดอกไม้ Iris
โดยใช้ scikit-learn
"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report

# โหลดข้อมูล Iris (150 ตัวอย่าง, 4 features, 3 คลาส)
iris = load_iris()
X = iris.data    # sepal length, sepal width, petal length, petal width
y = iris.target  # 0=Setosa, 1=Versicolor, 2=Virginica

# แสดงข้อมูลตัวอย่างแรก
df = pd.DataFrame(X, columns=iris.feature_names)
df['species'] = [iris.target_names[i] for i in y]
print("=== ตัวอย่างข้อมูล 5 แถวแรก ===")
print(df.head())
print(f" - รูปร่างข้อมูล: {X.shape}")
print(f"จำนวนคลาส: {len(np.unique(y))} คลาส")

# แบ่งข้อมูลเป็น Training (80%) และ Testing (20%)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
print(f" - จำนวนข้อมูลฝึก: {X_train.shape[0]} ตัวอย่าง")
print(f"จำนวนข้อมูลทดสอบ: {X_test.shape[0]} ตัวอย่าง")

# สร้างและฝึกโมเดล Decision Tree
model = DecisionTreeClassifier(
    max_depth=3,          # ความลึกสูงสุดของต้นไม้ (ป้องกัน overfitting)
    criterion='entropy',  # ใช้ Information Gain
    random_state=42
)
model.fit(X_train, y_train)

# ประเมินโมเดล
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f" - === ผลการประเมิน ===")
print(f"ความแม่นยำ (Accuracy): {accuracy:.4f} ({accuracy*100:.2f}%)")
print(" - รายงานละเอียด:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

# แสดงความสำคัญของ Features
print("=== ความสำคัญของ Features ===")
for name, importance in zip(iris.feature_names, model.feature_importances_):
    bar = "█" * int(importance * 30)
    print(f"  {name:<25} {bar} {importance:.4f}")

# ตัวอย่างการทำนายข้อมูลใหม่
new_flower = np.array([[5.1, 3.5, 1.4, 0.2]])  # ข้อมูลดอกไม้ใหม่
prediction = model.predict(new_flower)
probability = model.predict_proba(new_flower)
print(f" - === ทำนายดอกไม้ใหม่ ===")
print(f"Input: sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2")
print(f"ผลทำนาย: {iris.target_names[prediction[0]]}")
print(f"ความน่าจะเป็น: Setosa={probability[0][0]:.2%}, "
      f"Versicolor={probability[0][1]:.2%}, "
      f"Virginica={probability[0][2]:.2%}")

ผลลัพธ์ที่คาดหวัง:

=== ตัวอย่างข้อมูล 5 แถวแรก ===
   sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)  species
0                5.1               3.5                1.4               0.2   setosa
...
ความแม่นยำ (Accuracy): 0.9667 (96.67%)
ผลทำนาย: setosa

1.1.2 K-Nearest Neighbors (KNN)

KNN คืออัลกอริทึมที่จำแนกประเภทโดยดูจาก k ตัวอย่างที่ใกล้ที่สุด (nearest neighbors) ในชุดข้อมูลฝึก แล้วลงคะแนนเสียงข้างมาก (majority vote)

สูตรระยะห่าง Euclidean:

d (x,y) = i=1 n (xi-yi) 2

ตัวอย่างการคำนวณ KNN ด้วยมือ:

จุดข้อมูล Feature 1 (x₁) Feature 2 (x₂) Label ระยะห่างจาก Query
A 1.0 2.0 🔴 Red √((3-1)²+(4-2)²) = 2.83
B 2.0 3.0 🔴 Red √((3-2)²+(4-3)²) = 1.41
C 4.0 5.0 🔵 Blue √((3-4)²+(4-5)²) = 1.41
D 5.0 4.0 🔵 Blue √((3-5)²+(4-4)²) = 2.00
E 3.5 4.5 🔵 Blue √((3-3.5)²+(4-4.5)²) = 0.71

Query point = (3.0, 4.0), k=3
3 Nearest: E(0.71), B(1.41), C(1.41)
Vote: Red=1, Blue=2 → ทำนายเป็น Blue

"""
ตัวอย่าง KNN สำหรับตรวจจับโรคเบาหวาน (Diabetes Detection)
ใช้ข้อมูล Pima Indians Diabetes Dataset
"""
import numpy as np
import pandas as pd
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import confusion_matrix, classification_report
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

# สร้างข้อมูลจำลองสำหรับตรวจโรคเบาหวาน
np.random.seed(42)
n_samples = 200

# Features: [น้ำตาลในเลือด (glucose), BMI, อายุ (age), ความดันโลหิต (blood_pressure)]
glucose  = np.concatenate([np.random.normal(100, 20, 120), np.random.normal(140, 25, 80)])
bmi      = np.concatenate([np.random.normal(25, 4, 120),  np.random.normal(32, 6, 80)])
age      = np.concatenate([np.random.normal(30, 8, 120),  np.random.normal(45, 10, 80)])
bp       = np.concatenate([np.random.normal(70, 10, 120), np.random.normal(80, 12, 80)])
labels   = np.array([0]*120 + [1]*80)  # 0=ปกติ, 1=เป็นเบาหวาน

X = np.column_stack([glucose, bmi, age, bp])
y = labels

feature_names = ['น้ำตาลในเลือด', 'BMI', 'อายุ', 'ความดันโลหิต']

# Standardization (สำคัญมากสำหรับ KNN!)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # fit บน train เท่านั้น
X_test_scaled  = scaler.transform(X_test)        # transform บน test

# หาค่า k ที่ดีที่สุด
k_values = range(1, 21)
cv_scores = []
for k in k_values:
    knn = KNeighborsClassifier(n_neighbors=k)
    scores = cross_val_score(knn, X_train_scaled, y_train, cv=5, scoring='accuracy')
    cv_scores.append(scores.mean())

best_k = k_values[np.argmax(cv_scores)]
print(f"=== หา k ที่ดีที่สุด ===")
print(f"k ที่ดีที่สุด: {best_k}, Accuracy: {max(cv_scores):.4f}")

# ฝึกโมเดลด้วย k ที่ดีที่สุด
knn = KNeighborsClassifier(n_neighbors=best_k)
knn.fit(X_train_scaled, y_train)
y_pred = knn.predict(X_test_scaled)

print(f" - === ผลประเมินบน Test Set ===")
print(classification_report(y_test, y_pred,
      target_names=['ปกติ', 'เบาหวาน']))

# ทำนายผู้ป่วยใหม่
new_patient = np.array([[150, 30, 50, 85]])  # glucose, BMI, age, bp
new_scaled  = scaler.transform(new_patient)
pred = knn.predict(new_scaled)
prob = knn.predict_proba(new_scaled)
print(f" - === ทำนายผู้ป่วยใหม่ ===")
print(f"น้ำตาล=150, BMI=30, อายุ=50, ความดัน=85")
print(f"ผลวินิจฉัย: {'⚠️ เบาหวาน' if pred[0]==1 else '✅ ปกติ'}")
print(f"ความน่าจะเป็น: ปกติ={prob[0][0]:.2%}, เบาหวาน={prob[0][1]:.2%}")

# วาดกราฟ k vs Accuracy
plt.figure(figsize=(8, 4))
plt.plot(k_values, cv_scores, 'o-', color='#d79921', linewidth=2, markersize=6)
plt.axvline(x=best_k, color='#fb4934', linestyle='--', label=f'Best k={best_k}')
plt.xlabel('จำนวน k (Neighbors)', fontsize=12)
plt.ylabel('Cross-Validation Accuracy', fontsize=12)
plt.title('การเลือกค่า k ที่เหมาะสมสำหรับ KNN', fontsize=13)
plt.legend()
plt.tight_layout()
plt.savefig('/tmp/knn_k_selection.png', dpi=100, bbox_inches='tight')
print(" - บันทึกกราฟแล้ว: /tmp/knn_k_selection.png")

1.1.3 Naive Bayes Classifier

Naive Bayes ใช้ทฤษฎีเบย์ (Bayes' Theorem) โดยมีข้อสมมติว่า feature แต่ละตัวเป็นอิสระต่อกัน (conditional independence)

สูตร Bayes' Theorem:

P (C|X) = P(X|C)P(C) P(X) P (C|x1,,xn) P(C) i=1 n P(xi|C)

โดยที่:

ตัวอย่างการคำนวณ Naive Bayes กรองสแปม:

Email contains "free" contains "win" contains "meeting" Label
1 🔴 Spam
2 🔴 Spam
3 🟢 Ham
4 🟢 Ham

ทดสอบ: Email ใหม่มี "free"=✅, "win"=✅, "meeting"=❌


1.2 Regression (การถดถอย)

Regression คือการทำนายค่าต่อเนื่อง (continuous value) เช่น ราคาบ้าน, อุณหภูมิ, ยอดขาย

1.2.1 Linear Regression (การถดถอยเชิงเส้น)

สูตร Linear Regression:

y^ = β0 + β1x1 + β2x2 + + βnxn + ϵ

Loss Function (Mean Squared Error):

MSE = 1n i=1 n (yi-y^i) 2

โดยที่:

ตัวอย่างการคำนวณ Simple Linear Regression ด้วยมือ:

ข้อมูลพื้นที่บ้าน (ตร.ม.) และราคา (ล้านบาท):

บ้าน พื้นที่ (x) ราคา (y)
1 50 2.5
2 80 4.0
3 100 5.5
4 120 6.0
5 150 7.5

x̄ = (50+80+100+120+150)/5 = 100, ȳ = (2.5+4+5.5+6+7.5)/5 = 5.1

β1 = i=1n (xi-x) (yi-y) i=1n (xi-x)2 = 62510000 = 0.0485

β₀ = ȳ - β₁x̄ = 5.1 - 0.0485 × 100 = 0.25

สมการ: ŷ = 0.25 + 0.0485 × x

ทำนาย: บ้านพื้นที่ 130 ตร.ม. → ŷ = 0.25 + 0.0485 × 130 = 6.555 ล้านบาท

"""
Linear และ Polynomial Regression สำหรับทำนายราคาบ้าน
พร้อมการ Visualize ผลลัพธ์
"""
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split

# ===== สร้างข้อมูลจำลอง: ราคาบ้าน =====
np.random.seed(42)
n = 100

area      = np.random.uniform(40, 200, n)       # พื้นที่ 40-200 ตร.ม.
rooms     = np.random.randint(1, 6, n)           # จำนวนห้อง 1-5 ห้อง
age       = np.random.uniform(0, 30, n)          # อายุบ้าน 0-30 ปี
distance  = np.random.uniform(1, 20, n)          # ระยะจากศูนย์เมือง (กม.)

# ราคาจริง (ล้านบาท) พร้อม noise
price = (
    0.05 * area
    + 0.3  * rooms
    - 0.05 * age
    - 0.1  * distance
    + 1.0
    + np.random.normal(0, 0.3, n)
)

# สร้าง DataFrame
df = pd.DataFrame({
    'พื้นที่ (ตร.ม.)':  area,
    'จำนวนห้อง':        rooms,
    'อายุบ้าน (ปี)':   age,
    'ระยะจากเมือง (กม.)': distance,
    'ราคา (ล้านบาท)':   price
})

print("=== สถิติเบื้องต้น ===")
print(df.describe().round(2))

# เตรียมข้อมูล
X = df[['พื้นที่ (ตร.ม.)', 'จำนวนห้อง', 'อายุบ้าน (ปี)', 'ระยะจากเมือง (กม.)']].values
y = df['ราคา (ล้านบาท)'].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# ===== Linear Regression =====
lr = LinearRegression()
lr.fit(X_train, y_train)
y_pred_lr = lr.predict(X_test)

mse_lr = mean_squared_error(y_test, y_pred_lr)
r2_lr  = r2_score(y_test, y_pred_lr)

print(f" - === Linear Regression ===")
print(f"Intercept (β₀): {lr.intercept_:.4f}")
for name, coef in zip(['พื้นที่', 'ห้อง', 'อายุ', 'ระยะ'], lr.coef_):
    print(f"  β ({name}): {coef:.4f}")
print(f"MSE:  {mse_lr:.4f}")
print(f"RMSE: {np.sqrt(mse_lr):.4f} ล้านบาท")
print(f"R²:   {r2_lr:.4f} ({r2_lr*100:.1f}% variance explained)")

# ===== ทำนายบ้านตัวอย่าง =====
sample = np.array([[120, 3, 5, 8]])  # พื้นที่=120, ห้อง=3, อายุ=5ปี, ระยะ=8กม.
pred = lr.predict(sample)
print(f" - === ทำนายราคาบ้านตัวอย่าง ===")
print(f"พื้นที่=120 ตร.ม., 3 ห้อง, อายุ=5ปี, ระยะ=8กม.")
print(f"ราคาที่ทำนาย: {pred[0]:.2f} ล้านบาท")

# ===== Visualize =====
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
fig.patch.set_facecolor('#282828')

for ax in axes:
    ax.set_facecolor('#3c3836')
    ax.tick_params(colors='#ebdbb2')
    for spine in ax.spines.values():
        spine.set_edgecolor('#504945')

# กราฟ 1: Actual vs Predicted
axes[0].scatter(y_test, y_pred_lr, color='#83a598', alpha=0.7, s=50)
lims = [min(y_test.min(), y_pred_lr.min()), max(y_test.max(), y_pred_lr.max())]
axes[0].plot(lims, lims, 'r--', color='#fb4934', linewidth=2, label='Perfect Prediction')
axes[0].set_xlabel('ราคาจริง (ล้านบาท)', color='#ebdbb2', fontsize=11)
axes[0].set_ylabel('ราคาทำนาย (ล้านบาท)', color='#ebdbb2', fontsize=11)
axes[0].set_title(f'Actual vs Predicted - R² = {r2_lr:.3f}', color='#ebdbb2', fontsize=12)
axes[0].legend(labelcolor='#ebdbb2')

# กราฟ 2: Residuals
residuals = y_test - y_pred_lr
axes[1].hist(residuals, bins=20, color='#d79921', edgecolor='#282828', alpha=0.8)
axes[1].axvline(x=0, color='#fb4934', linestyle='--', linewidth=2)
axes[1].set_xlabel('Residual (ล้านบาท)', color='#ebdbb2', fontsize=11)
axes[1].set_ylabel('จำนวน', color='#ebdbb2', fontsize=11)
axes[1].set_title('Distribution of Residuals - (ควรมีรูปแบบ Normal)', color='#ebdbb2', fontsize=12)

plt.tight_layout()
plt.savefig('/tmp/regression_results.png', dpi=100, bbox_inches='tight',
            facecolor='#282828')
print(" - บันทึกกราฟแล้ว: /tmp/regression_results.png")

1.3 Model Evaluation (การประเมินโมเดล)

1.3.1 Cross-Validation (การตรวจสอบไขว้)

Cross-Validation เป็นเทคนิคที่ประเมินโมเดลอย่างน่าเชื่อถือโดยแบ่งข้อมูลออกเป็น k ส่วน (folds) ฝึกโมเดล k-1 ส่วน และทดสอบบนส่วนที่เหลือ ทำซ้ำ k ครั้ง

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828', 'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945', 'lineColor': '#d79921',
  'background': '#282828', 'mainBkg': '#3c3836',
  'nodeBorder': '#504945', 'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2', 'fontFamily': 'Sarabun, sans-serif'
}}}%%
flowchart TB
  subgraph FOLD1["Fold 1"]
    direction TB
    T1["Test"]
    TR1["Train | Train | Train | Train"]
    T1 --- TR1
  end
  subgraph FOLD2["Fold 2"]
    direction TB
    TR2a["Train"]
    T2["Test"]
    TR2b["Train | Train | Train"]
    TR2a --- T2 --- TR2b
  end
  subgraph FOLD3["Fold 3"]
    direction TB
    TR3a["Train | Train"]
    T3["Test"]
    TR3b["Train | Train"]
    TR3a --- T3 --- TR3b
  end
  FOLD1 --> S1["Score 1"]
  FOLD2 --> S2["Score 2"]
  FOLD3 --> S3["Score 3"]
  S1 & S2 & S3 --> AVG["📊 Mean ± Std"]

  style FOLD1 fill:#3c3836,stroke:#d79921,color:#ebdbb2
  style FOLD2 fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
  style FOLD3 fill:#3c3836,stroke:#458588,color:#ebdbb2
  style AVG fill:#3c3836,stroke:#fb4934,color:#ebdbb2

1.3.2 Confusion Matrix และ Metrics การประเมิน

เมตริก สูตร ความหมาย
Accuracy (TP+TN)/(TP+TN+FP+FN) ความแม่นยำโดยรวม
Precision TP/(TP+FP) จากที่ทำนายว่า Positive มีกี่ % ที่ถูก
Recall TP/(TP+FN) จากที่เป็น Positive จริงทำนายถูกกี่ %
F1-Score 2×(P×R)/(P+R) ค่าเฉลี่ยฮาร์มอนิกของ P และ R
AUC-ROC พื้นที่ใต้กราฟ ROC ประสิทธิภาพแยกคลาสโดยรวม

โดยที่: TP=True Positive, TN=True Negative, FP=False Positive, FN=False Negative

"""
การประเมินโมเดล: Cross-Validation และ Confusion Matrix
เปรียบเทียบหลายอัลกอริทึมบนชุดข้อมูลเดียวกัน
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (confusion_matrix, classification_report,
                             roc_auc_score, roc_curve)

# โหลดข้อมูลมะเร็งเต้านม (Wisconsin Breast Cancer Dataset)
data = load_breast_cancer()
X, y = data.data, data.target
print(f"ข้อมูล: {X.shape[0]} ตัวอย่าง, {X.shape[1]} features")
print(f"คลาส: {data.target_names}")
print(f"สัดส่วน: Malignant={sum(y==0)}, Benign={sum(y==1)}")

# กำหนดอัลกอริทึมที่จะเปรียบเทียบ
models = {
    'Decision Tree': Pipeline([
        ('clf', DecisionTreeClassifier(max_depth=5, random_state=42))
    ]),
    'KNN': Pipeline([
        ('scaler', StandardScaler()),
        ('clf', KNeighborsClassifier(n_neighbors=7))
    ]),
    'Naive Bayes': Pipeline([
        ('scaler', StandardScaler()),
        ('clf', GaussianNB())
    ]),
    'Random Forest': Pipeline([
        ('clf', RandomForestClassifier(n_estimators=100, random_state=42))
    ])
}

# 5-Fold Cross-Validation
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
results = {}

print(f" - {'='*55}")
print(f"{'อัลกอริทึม':<20} {'Mean Acc':>10} {'Std':>10} {'95% CI':>15}")
print(f"{'='*55}")

for name, model in models.items():
    scores = cross_val_score(model, X, y, cv=cv, scoring='accuracy')
    results[name] = scores
    ci = 1.96 * scores.std()
    print(f"{name:<20} {scores.mean():>10.4f} {scores.std():>10.4f} "
          f"[{scores.mean()-ci:.3f}, {scores.mean()+ci:.3f}]")

print(f"{'='*55}")

# วาดกราฟเปรียบเทียบ
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
fig.patch.set_facecolor('#282828')
fig.suptitle('เปรียบเทียบอัลกอริทึม Classification', color='#ebdbb2', fontsize=14)

for ax in axes:
    ax.set_facecolor('#3c3836')
    ax.tick_params(colors='#ebdbb2')

# กราฟ 1: Box Plot เปรียบเทียบ
colors = ['#fb4934', '#d79921', '#b8bb26', '#83a598']
bp = axes[0].boxplot(
    [results[m] for m in models.keys()],
    patch_artist=True, labels=list(models.keys())
)
for patch, color in zip(bp['boxes'], colors):
    patch.set_facecolor(color)
    patch.set_alpha(0.7)
axes[0].set_ylabel('Accuracy', color='#ebdbb2', fontsize=11)
axes[0].set_title('5-Fold Cross-Validation Accuracy', color='#ebdbb2', fontsize=12)
axes[0].tick_params(axis='x', rotation=15)

# กราฟ 2: Confusion Matrix ของ Random Forest
from sklearn.model_selection import train_test_split
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
best_model = models['Random Forest']
best_model.fit(X_tr, y_tr)
y_pred = best_model.predict(X_te)
cm = confusion_matrix(y_te, y_pred)

sns.heatmap(cm, annot=True, fmt='d', ax=axes[1],
            cmap='YlOrRd', linewidths=0.5,
            xticklabels=data.target_names,
            yticklabels=data.target_names)
axes[1].set_xlabel('ทำนาย', color='#ebdbb2', fontsize=11)
axes[1].set_ylabel('จริง', color='#ebdbb2', fontsize=11)
axes[1].set_title('Confusion Matrix (Random Forest)', color='#ebdbb2', fontsize=12)
axes[1].tick_params(colors='#ebdbb2')

plt.tight_layout()
plt.savefig('/tmp/model_comparison.png', dpi=100, bbox_inches='tight',
            facecolor='#282828')
print(f" - บันทึกกราฟแล้ว")

2. Unsupervised Learning (การเรียนรู้แบบไม่มีผู้สอน)

Unsupervised Learning คือการเรียนรู้จากข้อมูลที่ ไม่มีป้ายกำกับ (unlabeled data) โมเดลต้องค้นหารูปแบบ (patterns) และโครงสร้าง (structure) ในข้อมูลด้วยตนเอง

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828', 'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945', 'lineColor': '#d79921',
  'background': '#282828', 'mainBkg': '#3c3836',
  'nodeBorder': '#504945', 'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2', 'fontFamily': 'Sarabun, sans-serif'
}}}%%
mindmap
  root((Unsupervised - Learning))
    Clustering
      K-Means
      Hierarchical
      DBSCAN
    Dimensionality - Reduction
      PCA
      t-SNE
      UMAP
    Generative - Models
      Autoencoders
      GMM
      VAE
    Association - Rules
      Apriori
      FP-Growth

2.1 Clustering (การจัดกลุ่ม)

2.1.1 K-Means Clustering

K-Means เป็นอัลกอริทึมจัดกลุ่มที่แบ่งข้อมูลออกเป็น k กลุ่ม โดยแต่ละกลุ่มมี centroid (จุดศูนย์กลาง) โดยลดผลรวมระยะห่างจากจุดศูนย์กลาง (Within-Cluster Sum of Squares)

สูตร Objective Function:

J = k=1 K xCk x-μk 2

โดยที่ μₖ = centroid ของ cluster k, Cₖ = เซตของจุดใน cluster k

ขั้นตอน K-Means:

  1. กำหนดค่า k และสุ่ม centroids เริ่มต้น
  2. Assignment step: กำหนดแต่ละจุดให้กับ centroid ที่ใกล้ที่สุด
  3. Update step: คำนวณ centroid ใหม่จากค่าเฉลี่ยของจุดในกลุ่ม
  4. ทำซ้ำขั้น 2-3 จนกว่า centroids จะไม่เปลี่ยน

ตัวอย่างการคำนวณ K-Means รอบแรก (k=2):

จุด x y Centroid เริ่มต้น C1=(1,1), C2=(5,5)
P1 1 2 d(C1)=1.0, d(C2)=6.4 → Cluster 1
P2 2 1 d(C1)=1.0, d(C2)=5.7 → Cluster 1
P3 4 5 d(C1)=5.0, d(C2)=1.4 → Cluster 2
P4 5 4 d(C1)=5.7, d(C2)=1.0 → Cluster 2
P5 5 6 d(C1)=6.4, d(C2)=1.0 → Cluster 2

Centroid ใหม่: C1 = ((1+2)/2, (2+1)/2) = (1.5, 1.5); C2 = ((4+5+5)/3, (5+4+6)/3) = (4.67, 5.0)

"""
K-Means Clustering สำหรับแบ่งกลุ่มลูกค้า (Customer Segmentation)
พร้อมการหาค่า k ที่เหมาะสมด้วย Elbow Method และ Silhouette Score
"""
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA

# สร้างข้อมูลลูกค้า (Customer Segmentation)
np.random.seed(42)
n_customers = 300

# สร้างกลุ่มลูกค้า 4 กลุ่ม
groups = {
    'กลุ่มประหยัด':  {'annual_income': (20,40),  'spending_score': (10,30), 'n': 75},
    'กลุ่มมาตรฐาน': {'annual_income': (50,70),  'spending_score': (40,60), 'n': 90},
    'กลุ่มมั่งคั่ง': {'annual_income': (80,120), 'spending_score': (70,90), 'n': 80},
    'กลุ่มสุรุ่ยสุร่าย': {'annual_income': (20,45), 'spending_score': (65,90), 'n': 55},
}

data_list = []
for group_name, params in groups.items():
    income = np.random.uniform(*params['annual_income'], params['n'])
    spending = np.random.uniform(*params['spending_score'], params['n'])
    age = np.random.randint(20, 65, params['n'])
    data_list.append(pd.DataFrame({
        'รายได้ต่อปี (พัน)': income,
        'คะแนนการใช้จ่าย': spending,
        'อายุ': age,
        'true_group': group_name
    }))

df = pd.concat(data_list, ignore_index=True)

print("=== ข้อมูลลูกค้า ===")
print(df.describe().round(2))

X = df[['รายได้ต่อปี (พัน)', 'คะแนนการใช้จ่าย', 'อายุ']].values

# Standardize
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# ===== Elbow Method หาค่า k ที่เหมาะสม =====
inertias   = []
sil_scores = []
k_range = range(2, 10)

for k in k_range:
    km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
    km.fit(X_scaled)
    inertias.append(km.inertia_)
    sil_scores.append(silhouette_score(X_scaled, km.labels_))

best_k_sil = k_range[np.argmax(sil_scores)]
print(f" - === ผลการหา k ที่เหมาะสม ===")
print(f"k ที่ดีที่สุด (Silhouette): {best_k_sil}, Score = {max(sil_scores):.4f}")

# ===== K-Means ด้วย k ที่ดีที่สุด =====
kmeans = KMeans(n_clusters=best_k_sil, init='k-means++', n_init=10, random_state=42)
df['Cluster_KMeans'] = kmeans.fit_predict(X_scaled)

# วิเคราะห์แต่ละ cluster
print(f" - === วิเคราะห์ {best_k_sil} Clusters ===")
for c in range(best_k_sil):
    subset = df[df['Cluster_KMeans'] == c]
    print(f" - Cluster {c} ({len(subset)} ลูกค้า):")
    print(f"  รายได้เฉลี่ย:         {subset['รายได้ต่อปี (พัน)'].mean():.1f} พัน")
    print(f"  คะแนนใช้จ่ายเฉลี่ย: {subset['คะแนนการใช้จ่าย'].mean():.1f}")
    print(f"  อายุเฉลี่ย:           {subset['อายุ'].mean():.1f} ปี")

# Visualize ด้วย PCA 2D
pca = PCA(n_components=2)
X_2d = pca.fit_transform(X_scaled)

fig, axes = plt.subplots(1, 2, figsize=(14, 6))
fig.patch.set_facecolor('#282828')
fig.suptitle('Customer Segmentation ด้วย K-Means', color='#ebdbb2', fontsize=14)

colors = ['#fb4934', '#d79921', '#b8bb26', '#83a598', '#d3869b']

for ax in axes:
    ax.set_facecolor('#3c3836')
    ax.tick_params(colors='#ebdbb2')

# Elbow + Silhouette
ax2 = axes[0].twinx()
axes[0].set_facecolor('#3c3836')
axes[0].plot(k_range, inertias, 'o-', color='#d79921', linewidth=2, label='Inertia')
ax2.plot(k_range, sil_scores, 's--', color='#83a598', linewidth=2, label='Silhouette')
axes[0].set_xlabel('จำนวน Clusters (k)', color='#ebdbb2')
axes[0].set_ylabel('Inertia (WCSS)', color='#d79921')
ax2.set_ylabel('Silhouette Score', color='#83a598')
axes[0].set_title('Elbow Method & Silhouette Score', color='#ebdbb2')
axes[0].axvline(x=best_k_sil, color='#fb4934', linestyle=':', linewidth=2)
ax2.set_facecolor('#3c3836')
ax2.tick_params(colors='#ebdbb2')

# Cluster Plot
for c in range(best_k_sil):
    mask = df['Cluster_KMeans'] == c
    axes[1].scatter(X_2d[mask, 0], X_2d[mask, 1],
                   color=colors[c], alpha=0.7, s=50, label=f'Cluster {c}')

axes[1].set_xlabel('PCA Component 1', color='#ebdbb2')
axes[1].set_ylabel('PCA Component 2', color='#ebdbb2')
axes[1].set_title('การแบ่งกลุ่มลูกค้า (2D PCA)', color='#ebdbb2')
axes[1].legend(labelcolor='#ebdbb2', facecolor='#3c3836')

plt.tight_layout()
plt.savefig('/tmp/kmeans_clustering.png', dpi=100, bbox_inches='tight',
            facecolor='#282828')
print(" - บันทึกกราฟแล้ว: /tmp/kmeans_clustering.png")

2.1.2 Hierarchical Clustering (การจัดกลุ่มแบบลำดับชั้น)

Hierarchical Clustering สร้าง dendrogram (ต้นไม้ลำดับชั้น) เพื่อแสดงความสัมพันธ์ระหว่างกลุ่ม โดยไม่ต้องกำหนดจำนวน k ล่วงหน้า มีสองแบบ:

Linkage Methods:

Method สูตร ข้อดี ข้อเสีย
Single min d(a,b) ดีกับ elongated clusters ไวต่อ noise/outliers
Complete max d(a,b) clusters มีขนาดสม่ำเสมอ ไม่ดีกับ elongated shapes
Average mean d(a,b) ประนีประนอม ช้ากว่า
Ward minimize variance clusters กลม, ขนาดใกล้เคียง ไม่ดีกับ non-globular

2.1.3 DBSCAN (Density-Based Spatial Clustering)

DBSCAN จัดกลุ่มตามความหนาแน่น (density) สามารถค้นหากลุ่มที่มีรูปร่างซับซ้อน และตรวจจับ outliers ได้โดยอัตโนมัติ

พารามิเตอร์หลัก:

ประเภทของจุด:

"""
เปรียบเทียบ K-Means, Hierarchical, และ DBSCAN
บนข้อมูลที่มีรูปทรงต่างกัน
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
from sklearn.datasets import make_moons, make_circles, make_blobs
from sklearn.preprocessing import StandardScaler

# สร้างข้อมูลรูปทรงต่างกัน 3 ชุด
np.random.seed(42)
datasets = [
    (make_blobs(n_samples=200, centers=3, random_state=42)[0], "Blob Clusters"),
    (make_moons(n_samples=200, noise=0.05, random_state=42)[0], "Moon Shape"),
    (make_circles(n_samples=200, noise=0.05, factor=0.5, random_state=42)[0], "Circle Shape"),
]

algorithms = {
    'K-Means (k=3)':  KMeans(n_clusters=3, random_state=42),
    'Hierarchical':   AgglomerativeClustering(n_clusters=3, linkage='ward'),
    'DBSCAN':         DBSCAN(eps=0.3, min_samples=10),
}

fig, axes = plt.subplots(3, 3, figsize=(15, 12))
fig.patch.set_facecolor('#282828')
fig.suptitle('เปรียบเทียบอัลกอริทึม Clustering', color='#ebdbb2', fontsize=14)

colors_cmap = plt.cm.Set1

for row, (X_data, data_name) in enumerate(datasets):
    X_scaled = StandardScaler().fit_transform(X_data)

    for col, (alg_name, alg) in enumerate(algorithms.items()):
        ax = axes[row][col]
        ax.set_facecolor('#3c3836')
        ax.tick_params(colors='#ebdbb2')

        labels = alg.fit_predict(X_scaled)
        n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
        n_noise = list(labels).count(-1)

        # Plot
        unique_labels = set(labels)
        colors = [colors_cmap(each) for each in np.linspace(0, 1, len(unique_labels))]

        for label, color in zip(unique_labels, colors):
            mask = labels == label
            marker = 'x' if label == -1 else 'o'
            color = '#504945' if label == -1 else color
            ax.scatter(X_scaled[mask, 0], X_scaled[mask, 1],
                      c=[color], s=30, marker=marker, alpha=0.8)

        title = f"{data_name} - {alg_name} - ({n_clusters} clusters"
        if n_noise > 0:
            title += f", {n_noise} noise)"
        else:
            title += ")"
        ax.set_title(title, color='#ebdbb2', fontsize=9)

plt.tight_layout()
plt.savefig('/tmp/clustering_comparison.png', dpi=100, bbox_inches='tight',
            facecolor='#282828')
print("บันทึกกราฟเปรียบเทียบ Clustering แล้ว")

2.2.1 Principal Component Analysis (PCA)

ความหมายและแนวคิด

Principal Component Analysis (PCA) คือเทคนิคลดมิติข้อมูล (Dimensionality Reduction) ที่ค้นหา ทิศทางของความแปรปรวนสูงสุด (Maximum Variance Directions) ในข้อมูลหลายมิติ แล้วฉายข้อมูลลงบนแกนใหม่ที่เรียกว่า Principal Components (PC) ซึ่งเป็น orthogonal (ตั้งฉากกัน)

เป้าหมายหลัก:


ชุดข้อมูลตัวอย่าง

สมมติมีข้อมูลนักศึกษา 6 คน วัด 2 ตัวแปร ได้แก่ คะแนนคณิตศาสตร์ (Math) และคะแนนฟิสิกส์ (Physics) (คะแนนเต็ม 10)

นักศึกษา คณิตศาสตร์ (x₁) ฟิสิกส์ (x₂)
A 2 2.4
B 4 4.0
C 5 5.5
D 6 5.5
E 8 7.6
F 9 8.5

สังเกต: ทั้งสองตัวแปรดูเหมือนสัมพันธ์กัน (นักศึกษาที่เก่งคณิตฯ มักเก่งฟิสิกส์ด้วย)
PCA จะช่วยค้นหาแกนหลักที่ "อธิบายรูปแบบนี้" ได้ดีที่สุด


ขั้นตอนที่ 1: คำนวณค่าเฉลี่ย (Mean) ของแต่ละตัวแปร

x1 = 2+4+5+6+8+9 6 = 346 = 5.667 x2 = 2.4+4.0+5.5+5.5+7.6+8.5 6 = 33.56 = 5.583

ขั้นตอนที่ 2: Centering — หักค่าเฉลี่ยออกจากข้อมูล (Zero Mean)

สูตร: x̃ᵢ = xᵢ − x̄

นักศึกษา x₁ (Math) x₂ (Physics) x̃₁ = x₁ − 5.667 x̃₂ = x₂ − 5.583
A 2 2.4 −3.667 −3.183
B 4 4.0 −1.667 −1.583
C 5 5.5 −0.667 −0.083
D 6 5.5 +0.333 −0.083
E 8 7.6 +2.333 +2.017
F 9 8.5 +3.333 +2.917
ผลรวม ≈ 0.000 ≈ 0.000

หลัง centering ผลรวมของแต่ละคอลัมน์ต้องเป็น 0 เสมอ


ขั้นตอนที่ 3: คำนวณ Covariance Matrix

Covariance Matrix บอกว่าตัวแปรแต่ละคู่มีความสัมพันธ์กันอย่างไร

Cov (xi,xj) = 1n-1 k=1 n x~ki × x~kj

คำนวณทีละค่า (n=6, หาร n−1=5):

Var(x̃₁) = Cov(x̃₁, x̃₁):

นักศึกษา x̃₁ x̃₁²
A −3.667 13.447
B −1.667 2.779
C −0.667 0.445
D +0.333 0.111
E +2.333 5.443
F +3.333 11.109
รวม 33.334
Var(x~1) = 33.3345 = 6.667

Var(x̃₂) = Cov(x̃₂, x̃₂):

นักศึกษา x̃₂ x̃₂²
A −3.183 10.131
B −1.583 2.506
C −0.083 0.007
D −0.083 0.007
E +2.017 4.068
F +2.917 8.509
รวม 25.228
Var(x~2) = 25.2285 = 5.046

Cov(x̃₁, x̃₂):

นักศึกษา x̃₁ x̃₂ x̃₁ × x̃₂
A −3.667 −3.183 11.672
B −1.667 −1.583 2.639
C −0.667 −0.083 0.055
D +0.333 −0.083 −0.028
E +2.333 +2.017 4.706
F +3.333 +2.917 9.723
รวม 28.767
Cov(x~1,x~2) = 28.7675 = 5.753

สรุป Covariance Matrix:

Σ = 6.667 5.753 5.753 5.046

Covariance = 5.753 (บวก, สูง) → ยืนยันว่า x₁ และ x₂ สัมพันธ์กันมาก


ขั้นตอนที่ 4: คำนวณ Eigenvalues และ Eigenvectors

Eigenvalues (λ) บอกว่าแกนนั้นๆ อธิบาย variance ได้เท่าไร
Eigenvectors (v) บอกทิศทางของแกนใหม่แต่ละแกน

วิธีหา Eigenvalues: แก้สมการ det(Σ − λI) = 0

det 6.667-λ5.753 5.7535.046-λ = 0 (6.667-λ) (5.046-λ) - 5.753×5.753 = 0

ขยายวงเล็บ:

λ2 - (6.667+5.046)λ + (6.667×5.046-5.7532) = 0 λ2 - 11.713λ + (33.622-33.097) = 0 λ2 - 11.713λ + 0.525 = 0

ใช้สูตร Quadratic:

λ = 11.713 ± 11.7132 - 4×0.525 2 = 11.713±137.19-2.10 2 = 11.713±11.624 2

ผลลัพธ์:

λ1 = 11.713+11.6242 = 23.3372 = 11.669 λ2 = 11.713-11.6242 = 0.0892 = 0.045

ตรวจสอบ: λ₁ + λ₂ = 11.669 + 0.045 = 11.714 ≈ 6.667 + 5.046 = 11.713
(ผลรวม Eigenvalues ต้องเท่ากับ Trace ของ Covariance Matrix)


คำนวณ Eigenvectors

สำหรับ λ₁ = 11.669 แก้ระบบ (Σ − λ₁I)v = 0:

6.667-11.669 5.753 5.753 5.046-11.669 v1 v2 = 0 -5.0025.753 5.753-6.623 v1 v2 = 0

จากแถวแรก: −5.002 v₁ + 5.753 v₂ = 0 → v₁ = (5.753/5.002) v₂ = 1.150 v₂

ให้ v₂ = 1 → v = [1.150, 1.000]ᵀ
Normalize (หารด้วย ‖v‖):

v = 1.1502 + 1.0002 = 1.3225+1.000 = 2.3225 = 1.524 e1 = 1.1501.524 1.0001.524 = 0.754 0.656

สำหรับ λ₂ = 0.045 (ทำแบบเดิม):

e2 = -0.656 0.754

ตรวจสอบ Orthogonality: e₁ · e₂ = (0.754)(−0.656) + (0.656)(0.754) = −0.495 + 0.495 = 0


ขั้นตอนที่ 5: คำนวณ Explained Variance Ratio

Explained Variance Ratio (PC1) = λ1 λ1+λ2 = 11.66911.669+0.045 = 11.66911.714 = 0.9962 (99.62%) Explained Variance Ratio (PC2) = λ2 λ1+λ2 = 0.04511.714 = 0.0038 (0.38%)

สรุป: แค่ PC1 เพียงแกนเดียว ก็อธิบาย variance ได้ถึง 99.62% แสดงว่าข้อมูล 2 มิตินี้สามารถ ลดเหลือ 1 มิติ โดยแทบไม่สูญเสียข้อมูล

Principal Component Eigenvalue Explained Variance Cumulative
PC1 11.669 99.62% 99.62%
PC2 0.045 0.38% 100.00%

ขั้นตอนที่ 6: ฉายข้อมูลลงบน Principal Components (Projection)

การฉายข้อมูล: คูณ centered data matrix กับ eigenvector

z1 = x~1 × 0.754 + x~2 × 0.656 (PC1 Score) z2 = x~1 × (-0.656) + x~2 × 0.754 (PC2 Score)

ตารางการคำนวณ PC Scores:

นักศึกษา x̃₁ x̃₂ PC1 Score (z₁) PC2 Score (z₂)
A −3.667 −3.183 (−3.667)(0.754)+(−3.183)(0.656) = −4.842 (−3.667)(−0.656)+(−3.183)(0.754) = +0.082
B −1.667 −1.583 (−1.667)(0.754)+(−1.583)(0.656) = −2.294 (−1.667)(−0.656)+(−1.583)(0.754) = −0.102
C −0.667 −0.083 (−0.667)(0.754)+(−0.083)(0.656) = −0.557 (−0.667)(−0.656)+(−0.083)(0.754) = +0.375
D +0.333 −0.083 (+0.333)(0.754)+(−0.083)(0.656) = +0.196 (+0.333)(−0.656)+(−0.083)(0.754) = −0.281
E +2.333 +2.017 (+2.333)(0.754)+(+2.017)(0.656) = +3.080 (+2.333)(−0.656)+(+2.017)(0.754) = −0.011
F +3.333 +2.917 (+3.333)(0.754)+(+2.917)(0.656) = +4.427 (+3.333)(−0.656)+(+2.917)(0.754) = +0.012

สังเกต: ค่า PC2 Score มีขนาดเล็กมาก (ใกล้ 0 ทุกตัว) → ยืนยันว่า PC2 แทบไม่มีข้อมูลเพิ่มเติม


สรุปภาพรวมทุกขั้นตอน

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828',
  'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945',
  'lineColor': '#d79921',
  'secondaryColor': '#3c3836',
  'background': '#282828',
  'mainBkg': '#3c3836',
  'nodeBorder': '#504945',
  'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2',
  'fontFamily': 'Sarabun, sans-serif'
}}}%%
flowchart TD
  S1["📊 ขั้นที่ 1\nข้อมูลดิบ X\n(6 ตัวอย่าง × 2 features)"]
  S2["➖ ขั้นที่ 2\nCentering\nX̃ = X - mean(X)"]
  S3["🔢 ขั้นที่ 3\nCovariance Matrix Σ\nΣ = (1/n-1) X̃ᵀX̃"]
  S4["🔍 ขั้นที่ 4\nEigen Decomposition\nΣv = λv"]
  S5["📈 ขั้นที่ 5\nExplained Variance\nλ₁/(λ₁+λ₂) = 99.62%"]
  S6["🎯 ขั้นที่ 6\nProjection\nZ = X̃ × E"]
  S7["✅ ผลลัพธ์\nลดจาก 2D → 1D\nคงข้อมูลได้ 99.62%"]

  S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7

  R1["x̄₁=5.667, x̄₂=5.583"]
  R2["Σ = [[6.667, 5.753],[5.753, 5.046]]"]
  R3["λ₁=11.669, λ₂=0.045\ne₁=[0.754, 0.656]"]
  R4["PC1 Scores: −4.842 ถึง +4.427"]

  S2 -.->|"ผล"| R1
  S3 -.->|"ผล"| R2
  S4 -.->|"ผล"| R3
  S6 -.->|"ผล"| R4

  style S1 fill:#3c3836,stroke:#d79921,color:#ebdbb2
  style S2 fill:#3c3836,stroke:#458588,color:#ebdbb2
  style S3 fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
  style S4 fill:#3c3836,stroke:#d3869b,color:#ebdbb2
  style S5 fill:#3c3836,stroke:#83a598,color:#ebdbb2
  style S6 fill:#3c3836,stroke:#fe8019,color:#ebdbb2
  style S7 fill:#3c3836,stroke:#fb4934,color:#ebdbb2
  style R1 fill:#282828,stroke:#504945,color:#a89984
  style R2 fill:#282828,stroke:#504945,color:#a89984
  style R3 fill:#282828,stroke:#504945,color:#a89984
  style R4 fill:#282828,stroke:#504945,color:#a89984

ตัวอย่าง Code Python พร้อม Visualization

"""
PCA แบบ Step-by-Step: คำนวณด้วยมือ (numpy) และเปรียบเทียบกับ scikit-learn
ข้อมูล: คะแนนคณิตศาสตร์และฟิสิกส์ของนักศึกษา 6 คน
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

# ===== ข้อมูลตัวอย่าง =====
students = ['A', 'B', 'C', 'D', 'E', 'F']
X = np.array([
    [2,   2.4],   # A
    [4,   4.0],   # B
    [5,   5.5],   # C
    [6,   5.5],   # D
    [8,   7.6],   # E
    [9,   8.5],   # F
], dtype=float)

print("=" * 55)
print("  PCA คำนวณทีละขั้นตอน (Step-by-Step)")
print("=" * 55)

# ===== ขั้นที่ 1: แสดงข้อมูลดิบ =====
print("\n📊 ขั้นที่ 1: ข้อมูลดิบ")
print(f"{'นักศึกษา':>8}  {'คณิต (x₁)':>10}  {'ฟิสิกส์ (x₂)':>12}")
print("-" * 35)
for name, row in zip(students, X):
    print(f"{name:>8}  {row[0]:>10.1f}  {row[1]:>12.1f}")

# ===== ขั้นที่ 2: Centering =====
mean = X.mean(axis=0)
X_centered = X - mean

print(f"\n➖ ขั้นที่ 2: Centering (หักค่าเฉลี่ย)")
print(f"   x̄₁ = {mean[0]:.3f},  x̄₂ = {mean[1]:.3f}")
print(f"\n{'นักศึกษา':>8}  {'x̃₁':>10}  {'x̃₂':>10}")
print("-" * 33)
for name, row in zip(students, X_centered):
    print(f"{name:>8}  {row[0]:>+10.3f}  {row[1]:>+10.3f}")
print(f"{'ผลรวม':>8}  {X_centered[:,0].sum():>+10.3f}  {X_centered[:,1].sum():>+10.3f}  ← ต้องเป็น ≈ 0")

# ===== ขั้นที่ 3: Covariance Matrix =====
cov_matrix = np.cov(X_centered.T)  # ddof=1 (หาร n-1) โดย default

print(f"\n🔢 ขั้นที่ 3: Covariance Matrix")
print(f"   Var(x̃₁)       = {cov_matrix[0,0]:.4f}")
print(f"   Var(x̃₂)       = {cov_matrix[1,1]:.4f}")
print(f"   Cov(x̃₁, x̃₂)  = {cov_matrix[0,1]:.4f}")
print(f"\n   Σ = [[{cov_matrix[0,0]:.3f},  {cov_matrix[0,1]:.3f}],")
print(f"        [{cov_matrix[1,0]:.3f},  {cov_matrix[1,1]:.3f}]]")

# ===== ขั้นที่ 4: Eigenvalues & Eigenvectors =====
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
# เรียงจากมากไปน้อย
idx = np.argsort(eigenvalues)[::-1]
eigenvalues  = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]

print(f"\n🔍 ขั้นที่ 4: Eigenvalues และ Eigenvectors")
print(f"   λ₁ = {eigenvalues[0]:.4f}  →  PC1 (สำคัญที่สุด)")
print(f"   λ₂ = {eigenvalues[1]:.4f}  →  PC2")
print(f"\n   Eigenvector PC1: [{eigenvectors[0,0]:.4f},  {eigenvectors[1,0]:.4f}]")
print(f"   Eigenvector PC2: [{eigenvectors[0,1]:.4f},  {eigenvectors[1,1]:.4f}]")

dot_product = eigenvectors[:,0].dot(eigenvectors[:,1])
print(f"\n   ตรวจสอบ Orthogonality: e₁ · e₂ = {dot_product:.6f} ≈ 0 ✅")

# ===== ขั้นที่ 5: Explained Variance =====
total_var = eigenvalues.sum()
explained  = eigenvalues / total_var

print(f"\n📈 ขั้นที่ 5: Explained Variance Ratio")
print(f"\n   {'PC':>4}  {'Eigenvalue':>12}  {'Explained %':>12}  {'Cumulative %':>13}")
print("   " + "-" * 45)
cum = 0
for i, (ev, exp) in enumerate(zip(eigenvalues, explained)):
    cum += exp
    bar = "█" * int(exp * 30)
    print(f"   PC{i+1}  {ev:>12.4f}  {exp*100:>10.2f}%  {cum*100:>11.2f}%  {bar}")

print(f"\n   ✅ PC1 อธิบาย {explained[0]*100:.2f}% ของ variance ทั้งหมด")
print(f"   → สามารถลดจาก 2D เหลือ 1D ได้โดยแทบไม่สูญเสียข้อมูล!")

# ===== ขั้นที่ 6: Projection =====
Z = X_centered @ eigenvectors  # PC Scores

print(f"\n🎯 ขั้นที่ 6: PC Scores (Projection)")
print(f"\n   {'นักศึกษา':>8}  {'x̃₁':>8}  {'x̃₂':>8}  {'PC1 Score':>10}  {'PC2 Score':>10}")
print("   " + "-" * 52)
for name, xc, zr in zip(students, X_centered, Z):
    print(f"   {name:>8}  {xc[0]:>+8.3f}  {xc[1]:>+8.3f}  "
          f"{zr[0]:>+10.3f}  {zr[1]:>+10.3f}")

# ===== เปรียบเทียบกับ scikit-learn =====
print(f"\n🔬 เปรียบเทียบกับ scikit-learn PCA:")
pca_sk = PCA(n_components=2)
Z_sk   = pca_sk.fit_transform(X_centered)

print(f"   scikit-learn Explained Variance: "
      f"{pca_sk.explained_variance_ratio_[0]*100:.2f}%, "
      f"{pca_sk.explained_variance_ratio_[1]*100:.2f}%")
print(f"   |PC1 Score difference| max: {np.abs(np.abs(Z[:,0]) - np.abs(Z_sk[:,0])).max():.6f} ✅")
print(f"   (ผลลัพธ์ตรงกัน — อาจต่างเครื่องหมายได้ เพราะ eigenvectors ไม่ unique)")

# ===== Visualization =====
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
fig.patch.set_facecolor('#282828')
fig.suptitle('PCA Step-by-Step: คะแนนคณิตศาสตร์ vs ฟิสิกส์',
             color='#ebdbb2', fontsize=14, fontweight='bold')

for ax in axes:
    ax.set_facecolor('#3c3836')
    ax.tick_params(colors='#ebdbb2')
    for spine in ax.spines.values():
        spine.set_edgecolor('#504945')

student_colors = ['#fb4934','#d79921','#b8bb26','#83a598','#458588','#d3869b']

# ── กราฟ 1: ข้อมูลดิบพร้อม PC Directions ──
ax1 = axes[0]
for i, (name, point) in enumerate(zip(students, X)):
    ax1.scatter(*point, color=student_colors[i], s=120, zorder=5)
    ax1.annotate(f" {name}", point, color=student_colors[i],
                 fontsize=12, fontweight='bold')

# วาด PC directions จากจุด mean
scale = 3.0
origin = mean
for j, (ev, color, label) in enumerate(
        zip(eigenvectors.T, ['#d79921','#83a598'], ['PC1','PC2'])):
    ax1.annotate('',
        xy=origin + scale * eigenvalues[j]**0.5 * ev / np.linalg.norm(ev),
        xytext=origin,
        arrowprops=dict(arrowstyle='->', color=color, lw=2.5))
    offset = [0.3, -0.4] if j == 0 else [0.3, 0.3]
    ax1.text(*(origin + (scale * eigenvalues[j]**0.5 + 0.4) * ev / np.linalg.norm(ev) + offset),
             label, color=color, fontsize=12, fontweight='bold')

ax1.axhline(mean[1], color='#504945', linestyle=':', linewidth=0.8)
ax1.axvline(mean[0], color='#504945', linestyle=':', linewidth=0.8)
ax1.set_xlabel('คณิตศาสตร์ (x₁)', color='#ebdbb2', fontsize=11)
ax1.set_ylabel('ฟิสิกส์ (x₂)',     color='#ebdbb2', fontsize=11)
ax1.set_title('ข้อมูลดิบ + ทิศทาง PC',  color='#ebdbb2', fontsize=12)
ax1.set_aspect('equal')

# ── กราฟ 2: Scree Plot ──
ax2 = axes[1]
pc_labels = ['PC1', 'PC2']
bars = ax2.bar(pc_labels, explained * 100,
               color=['#d79921', '#83a598'], alpha=0.85, width=0.5)
ax2.plot(pc_labels, np.cumsum(explained) * 100,
         'o--', color='#fb4934', linewidth=2, markersize=8, label='Cumulative')

for bar, pct in zip(bars, explained * 100):
    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
             f'{pct:.2f}%', ha='center', va='bottom',
             color='#ebdbb2', fontsize=12, fontweight='bold')

ax2.axhline(99, color='#504945', linestyle=':', linewidth=1)
ax2.set_ylabel('Explained Variance (%)', color='#ebdbb2', fontsize=11)
ax2.set_title('Scree Plot\n(PC1 อธิบายได้ 99.62%)', color='#ebdbb2', fontsize=12)
ax2.set_ylim(0, 115)
ax2.legend(labelcolor='#ebdbb2', facecolor='#282828')

# ── กราฟ 3: ข้อมูลหลัง Projection บนแกน PC1 ──
ax3 = axes[2]
ax3.axhline(0, color='#504945', linewidth=1)
ax3.axhline(0, color='#d79921', linewidth=3, alpha=0.4, label='PC1 Axis')

for i, (name, score) in enumerate(zip(students, Z)):
    ax3.scatter(score[0], score[1],
                color=student_colors[i], s=120, zorder=5)
    ax3.annotate(f" {name}\n({score[0]:+.2f})", (score[0], score[1]),
                 color=student_colors[i], fontsize=9,
                 textcoords='offset points', xytext=(5, 5))
    # เส้น projection ลงบน PC1
    ax3.plot([score[0], score[0]], [score[1], 0],
             color=student_colors[i], linestyle='--', linewidth=0.8, alpha=0.6)

ax3.set_xlabel('PC1 Score (99.62% variance)', color='#ebdbb2', fontsize=11)
ax3.set_ylabel('PC2 Score (0.38% variance)',  color='#ebdbb2', fontsize=11)
ax3.set_title('PC Scores: ข้อมูลในพิกัด PCA\n(PC2 แทบไม่มีข้อมูลเพิ่ม)',
              color='#ebdbb2', fontsize=12)
ax3.legend(labelcolor='#ebdbb2', facecolor='#282828')

plt.tight_layout()
plt.savefig('/tmp/pca_step_by_step.png', dpi=110, bbox_inches='tight',
            facecolor='#282828')
print(f"\nบันทึกกราฟ: /tmp/pca_step_by_step.png")
print("=" * 55)

ผลลัพธ์ที่คาดหวัง:

=======================================================
  PCA คำนวณทีละขั้นตอน (Step-by-Step)
=======================================================
📊 ขั้นที่ 1: ข้อมูลดิบ
นักศึกษา   คณิต (x₁)   ฟิสิกส์ (x₂)
-----------------------------------
       A         2.0          2.4
       ...
🔢 ขั้นที่ 3: Covariance Matrix
   Var(x̃₁)       = 6.6667
   Var(x̃₂)       = 5.0457
   Cov(x̃₁, x̃₂)  = 5.7533
📈 ขั้นที่ 5: Explained Variance Ratio
   PC   Eigenvalue   Explained %   Cumulative %
   PC1      11.6690       99.62%        99.62%  ██████████████████████████████
   PC2       0.0447        0.38%       100.00%
✅ PC1 อธิบาย 99.62% ของ variance ทั้งหมด

ข้อควรระวังและข้อพึงรู้

ประเด็น รายละเอียด
Scaling ก่อน PCA ถ้า features มี scale ต่างกัน (เช่น น้ำหนักกับส่วนสูง) ต้อง StandardScaler ก่อนเสมอ
Interpretability PC axes ไม่มีความหมายทางกายภาพโดยตรง ตีความยากกว่า features เดิม
Linear Method PCA จับได้เฉพาะ linear structure ถ้าข้อมูลซับซ้อน ใช้ Kernel PCA หรือ t-SNE แทน
Information Loss เมื่อลดมิติ ข้อมูลบางส่วนหายไปเสมอ ต้องตัดสินใจว่า % ที่ยอมรับได้คือเท่าไร
Sign of Eigenvectors เครื่องหมาย (+/−) ของ eigenvector ไม่ unique อาจต่างกันระหว่าง implementations
Number of Components เลือกโดยดู Cumulative Explained Variance ≥ 95% (หรือ 99% สำหรับงานที่ต้องการความแม่นยำสูง)

เปรียบเทียบ PCA กับ t-SNE

คุณสมบัติ PCA t-SNE
ประเภท Linear Non-linear
เป้าหมาย Maximize global variance Preserve local structure
ความเร็ว เร็วมาก O(nd²) ช้า O(n²)
Interpretability สูง (eigenvectors อธิบายได้) ต่ำ (ไม่ preserve global structure)
Transform ข้อมูลใหม่ ทำได้ทันที ต้องฝึกใหม่
ใช้เพื่อ Preprocessing + Visualization Visualization เท่านั้น
Deterministic ใช่ ไม่ (random seed ต้องกำหนด)

3. Reinforcement Learning Basics (พื้นฐานการเรียนรู้เสริมแรง)

Reinforcement Learning (RL) เป็นการเรียนรู้ผ่านการโต้ตอบกับสภาพแวดล้อม (environment) Agent เรียนรู้ว่าควรทำ action ใดในแต่ละ state เพื่อให้ได้ รางวัล (reward) สะสมสูงสุด ในระยะยาว

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828', 'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945', 'lineColor': '#d79921',
  'background': '#282828', 'mainBkg': '#3c3836',
  'nodeBorder': '#504945', 'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2', 'fontFamily': 'Sarabun, sans-serif'
}}}%%
flowchart LR
  AG["🤖 Agent - (ตัวแทน)"]
  ENV["🌍 Environment - (สภาพแวดล้อม)"]

  AG -->|"Action (a) - การกระทำ"| ENV
  ENV -->|"State (s') - สถานะใหม่"| AG
  ENV -->|"Reward (r) - รางวัล"| AG

  style AG fill:#3c3836,stroke:#d79921,color:#ebdbb2
  style ENV fill:#3c3836,stroke:#83a598,color:#ebdbb2

3.1 องค์ประกอบหลักของ RL

3.2 Q-Learning

Q-Learning เป็น model-free RL algorithm ที่เรียนรู้ Q-function (action-value function) ซึ่งบอกว่า "การทำ action a ใน state s จะได้รางวัลสะสมเท่าใด"

Bellman Equation (Q-Learning Update):

Q (s,a) Q(s,a) + α [ r + γ max a' Q (s',a') - Q(s,a) ]

โดยที่:

ตัวอย่างการคำนวณ Q-Learning:

สมมติ: α=0.1, γ=0.9, Q(s1,a1)=0.0 เดิม, ได้ r=+10, Q(s2,best)=5.0

Q(s1,a1) 0.0 + 0.1 [ 10 + 0.9 × 5.0 - 0.0 ] = 0.1 × 14.5 = 1.45
"""
Q-Learning บนสภาพแวดล้อม FrozenLake (จำลองด้วย numpy)
เนื่องจาก OpenAI Gym อาจต้องติดตั้งเพิ่ม จึงสร้าง environment เอง
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

# ===== FrozenLake 4x4 Grid World =====
# S = Start, F = Frozen (ปลอดภัย), H = Hole (ตกหลุม), G = Goal
GRID = [
    'S', 'F', 'F', 'F',
    'F', 'H', 'F', 'H',
    'F', 'F', 'F', 'H',
    'H', 'F', 'F', 'G'
]
N_STATES  = 16
N_ACTIONS = 4  # 0=ซ้าย, 1=ลง, 2=ขวา, 3=ขึ้น

def get_next_state(state, action):
    """คำนวณ state ถัดไปและ reward"""
    row, col = state // 4, state % 4

    # Stochastic: 66% ไปทิศที่เลือก, 17% เลี้ยวซ้าย, 17% เลี้ยวขวา
    actual_action = np.random.choice(
        [action, (action-1)%4, (action+1)%4],
        p=[0.66, 0.17, 0.17]
    )

    dr = {0: 0, 1: 1, 2: 0, 3: -1}
    dc = {0: -1, 1: 0, 2: 1, 3: 0}

    new_row = max(0, min(3, row + dr[actual_action]))
    new_col = max(0, min(3, col + dc[actual_action]))
    new_state = new_row * 4 + new_col

    cell = GRID[new_state]
    if cell == 'G':
        return new_state, 1.0, True    # ถึงเป้าหมาย!
    elif cell == 'H':
        return new_state, -1.0, True   # ตกหลุม
    else:
        return new_state, -0.01, False  # ก้าวต่อไป

# ===== Q-Learning Algorithm =====
ALPHA   = 0.1    # Learning rate
GAMMA   = 0.9    # Discount factor
EPSILON = 1.0    # Exploration rate (epsilon-greedy)
EPSILON_DECAY = 0.995
EPSILON_MIN = 0.01
N_EPISODES = 3000

Q = np.zeros((N_STATES, N_ACTIONS))  # Q-table เริ่มต้นเป็น 0

rewards_per_episode = []
success_rate_history = []

print("=== Q-Learning: FrozenLake 4x4 ===")
print(f"α={ALPHA}, γ={GAMMA}, Episodes={N_EPISODES}")

for episode in range(N_EPISODES):
    state = 0  # เริ่มที่ Start (0,0)
    total_reward = 0
    done = False

    while not done:
        # Epsilon-Greedy Policy
        if np.random.random() < EPSILON:
            action = np.random.randint(N_ACTIONS)   # Explore
        else:
            action = np.argmax(Q[state])              # Exploit

        # ก้าวไปข้างหน้า
        next_state, reward, done = get_next_state(state, action)

        # Q-Learning Update (Bellman Equation)
        old_q = Q[state, action]
        best_next_q = np.max(Q[next_state]) if not done else 0
        Q[state, action] = old_q + ALPHA * (
            reward + GAMMA * best_next_q - old_q
        )

        state = next_state
        total_reward += reward

    # Decay Epsilon
    EPSILON = max(EPSILON_MIN, EPSILON * EPSILON_DECAY)
    rewards_per_episode.append(total_reward)

    # บันทึก success rate ทุก 100 episodes
    if (episode + 1) % 100 == 0:
        recent_rewards = rewards_per_episode[-100:]
        success_rate = sum(1 for r in recent_rewards if r > 0) / 100
        success_rate_history.append(success_rate)
        if (episode + 1) % 500 == 0:
            print(f"  Episode {episode+1:4d}: ε={EPSILON:.3f}, "
                  f"Success Rate={success_rate:.2%}")

# แสดง Optimal Policy
actions_str = ['←', '↓', '→', '↑']
print(f" - === Optimal Policy (Q-Table) ===")
print("   0    1    2    3")
for row in range(4):
    line = f"{row}: "
    for col in range(4):
        state = row * 4 + col
        cell = GRID[state]
        if cell in ['H', 'G']:
            line += f" {cell}  "
        else:
            best = np.argmax(Q[state])
            line += f" {actions_str[best]}  "
    print(line)

print(f" - === Q-Values ของ Start State (0) ===")
for i, (a, q) in enumerate(zip(['←', '↓', '→', '↑'], Q[0])):
    print(f"  Action {a}: Q = {q:.4f}")

# Visualize การเรียนรู้
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.patch.set_facecolor('#282828')

for ax in axes:
    ax.set_facecolor('#3c3836')
    ax.tick_params(colors='#ebdbb2')

# Rolling average reward
window = 100
rolling_avg = np.convolve(rewards_per_episode,
                          np.ones(window)/window, mode='valid')
axes[0].plot(rolling_avg, color='#d79921', linewidth=1.5)
axes[0].set_xlabel('Episode', color='#ebdbb2')
axes[0].set_ylabel(f'Avg Reward ({window} eps)', color='#ebdbb2')
axes[0].set_title('Q-Learning: Reward ต่อ Episode', color='#ebdbb2', fontsize=12)
axes[0].axhline(y=0, color='#504945', linestyle='--')

# Success Rate
axes[1].plot(range(100, N_EPISODES+1, 100), success_rate_history,
            'o-', color='#b8bb26', linewidth=2, markersize=4)
axes[1].set_xlabel('Episode', color='#ebdbb2')
axes[1].set_ylabel('Success Rate', color='#ebdbb2')
axes[1].set_title('อัตราความสำเร็จ (ทุก 100 Episodes)', color='#ebdbb2', fontsize=12)
axes[1].yaxis.set_major_formatter(plt.FuncFormatter(lambda x,_: f'{x:.0%}'))

plt.tight_layout()
plt.savefig('/tmp/qlearning_results.png', dpi=100, bbox_inches='tight',
            facecolor='#282828')
print(" - บันทึกกราฟแล้ว: /tmp/qlearning_results.png")

3.3 Policy และ Value Functions

ฟังก์ชัน สัญลักษณ์ ความหมาย
Policy π(a|s) ความน่าจะเป็นที่เลือก action a ใน state s
State Value V(s) รางวัลสะสมที่คาดหวังจาก state s ตาม policy π
Action Value Q(s,a) รางวัลสะสมที่คาดหวังเมื่อทำ a ใน s ตาม π
Advantage A(s,a) Q(s,a) - V(s): ความได้เปรียบของ action นั้น

4. Feature Engineering & Selection (วิศวกรรม Feature และการเลือก Feature)

Feature Engineering คือกระบวนการสร้าง, แปลง, หรือเลือก features ที่เหมาะสมเพื่อให้โมเดลทำงานได้ดีขึ้น ถือเป็น "ศิลปะ" ของ Machine Learning

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828', 'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945', 'lineColor': '#d79921',
  'background': '#282828', 'mainBkg': '#3c3836',
  'nodeBorder': '#504945', 'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2', 'fontFamily': 'Sarabun, sans-serif'
}}}%%
flowchart TD
  RAW["📥 Raw Data - ข้อมูลดิบ"]
  
  subgraph FE["Feature Engineering"]
    direction TB
    FE1["🔧 Feature Creation - สร้าง Feature ใหม่"]
    FE2["🔄 Feature Transformation - แปลง Feature"]
    FE3["🔍 Feature Selection - เลือก Feature"]
    FE1 --> FE2 --> FE3
  end

  subgraph TRANS["Transformations"]
    T1["Scaling - (StandardScaler, MinMax)"]
    T2["Encoding - (One-Hot, Label)"]
    T3["Binning - (ช่วงอายุ, ราคา)"]
    T4["Log Transform - (skewed data)"]
    T5["Interaction Terms - (x1 × x2)"]
  end

  subgraph SEL["Feature Selection Methods"]
    S1["Filter Methods - (Correlation, Chi-Square)"]
    S2["Wrapper Methods - (RFE, Forward Selection)"]
    S3["Embedded Methods - (LASSO, Tree Importance)"]
  end

  RAW --> FE
  FE2 --- TRANS
  FE3 --- SEL

  style RAW fill:#3c3836,stroke:#d79921,color:#ebdbb2
  style FE fill:#282828,stroke:#504945,color:#ebdbb2
  style TRANS fill:#282828,stroke:#458588,color:#ebdbb2
  style SEL fill:#282828,stroke:#b8bb26,color:#ebdbb2
"""
Feature Engineering & Selection ครบวงจร
ตัวอย่าง: ข้อมูลการอยู่รอด (Titanic-style)
"""
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler, LabelEncoder, OneHotEncoder
from sklearn.feature_selection import (SelectKBest, f_classif,
                                       RFE, mutual_info_classif)
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score

# ===== สร้างข้อมูลจำลอง =====
np.random.seed(42)
n = 500

df = pd.DataFrame({
    'age':      np.random.randint(1, 80, n),
    'gender':   np.random.choice(['Male', 'Female'], n),
    'pclass':   np.random.choice([1, 2, 3], n, p=[0.2, 0.3, 0.5]),
    'fare':     np.random.exponential(30, n).clip(0, 500),
    'siblings': np.random.choice([0,1,2,3], n, p=[0.6,0.25,0.1,0.05]),
    'embarked': np.random.choice(['C','S','Q'], n, p=[0.2, 0.7, 0.1]),
    'has_cabin': np.random.choice([0, 1], n, p=[0.7, 0.3])
})

# สร้าง target ที่สมเหตุสมผล
df['survived'] = (
    (df['gender'] == 'Female').astype(int) * 0.5 +
    (df['pclass'] == 1).astype(int) * 0.3 +
    (df['age'] < 15).astype(int) * 0.2 +
    (df['fare'] > 50).astype(int) * 0.1 +
    np.random.normal(0, 0.15, n) > 0.3
).astype(int)

print("=== ข้อมูลดิบ ===")
print(df.head())
print(f" - สัดส่วนรอดชีวิต: {df['survived'].mean():.2%}")

# ===== Feature Engineering =====
df_eng = df.copy()

# 1. Feature Creation: สร้าง features ใหม่
df_eng['family_size']    = df['siblings'] + 1  # รวมตัวเอง
df_eng['is_alone']       = (df_eng['family_size'] == 1).astype(int)
df_eng['age_group']      = pd.cut(df['age'],
                                  bins=[0, 12, 18, 35, 60, 100],
                                  labels=['child','teen','young','adult','senior'])
df_eng['fare_per_class'] = df['fare'] / df['pclass']
df_eng['fare_log']       = np.log1p(df['fare'])  # Log transform

# 2. Encoding
df_eng['gender_enc']   = (df['gender'] == 'Female').astype(int)
embarked_dummies = pd.get_dummies(df['embarked'], prefix='emb')
df_eng = pd.concat([df_eng, embarked_dummies], axis=1)

# อายุกลุ่ม → one-hot
age_dummies = pd.get_dummies(df_eng['age_group'], prefix='age')
df_eng = pd.concat([df_eng, age_dummies], axis=1)

# 3. เลือก Features ที่จะใช้ (ตัด columns ดิบออก)
drop_cols = ['gender', 'embarked', 'age_group', 'survived']
feature_cols = [c for c in df_eng.columns if c not in drop_cols]

X = df_eng[feature_cols].fillna(0).values
y = df['survived'].values

print(f" - === หลัง Feature Engineering ===")
print(f"จำนวน features: {len(feature_cols)}")
print(f"Features: {feature_cols}")

# ===== Feature Selection =====
# วิธีที่ 1: SelectKBest (Filter Method)
selector_kb = SelectKBest(score_func=f_classif, k=8)
X_kb = selector_kb.fit_transform(X, y)
kb_scores = selector_kb.scores_
selected_kb = [feature_cols[i] for i in selector_kb.get_support(indices=True)]
print(f" - SelectKBest (k=8): {selected_kb}")

# วิธีที่ 2: Random Forest Feature Importance (Embedded)
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
importances = rf.feature_importances_
sorted_idx = np.argsort(importances)[::-1][:10]  # Top 10

print(f" - Top 10 Features (Random Forest Importance):")
for i in sorted_idx[:5]:
    bar = "█" * int(importances[i] * 100)
    print(f"  {feature_cols[i]:<25} {bar} {importances[i]:.4f}")

# เปรียบเทียบผลลัพธ์
rf_all  = cross_val_score(rf, X, y, cv=5, scoring='accuracy').mean()
rf_best = cross_val_score(rf, X[:, sorted_idx[:8]], y, cv=5, scoring='accuracy').mean()
print(f" - Accuracy (ทุก features): {rf_all:.4f}")
print(f"Accuracy (top 8 features): {rf_best:.4f}")

# Visualize Feature Importance
fig, ax = plt.subplots(figsize=(10, 6))
fig.patch.set_facecolor('#282828')
ax.set_facecolor('#3c3836')
ax.tick_params(colors='#ebdbb2')

top_features = [feature_cols[i] for i in sorted_idx[:10]]
top_scores   = importances[sorted_idx[:10]]
colors = ['#d79921' if i < 3 else '#83a598' for i in range(10)]

bars = ax.barh(range(10), top_scores[::-1], color=colors[::-1], alpha=0.8)
ax.set_yticks(range(10))
ax.set_yticklabels(top_features[::-1], color='#ebdbb2', fontsize=10)
ax.set_xlabel('Feature Importance', color='#ebdbb2', fontsize=11)
ax.set_title('Top 10 Feature Importance (Random Forest)', color='#ebdbb2', fontsize=12)

for bar, score in zip(bars, top_scores[::-1]):
    ax.text(score + 0.002, bar.get_y() + bar.get_height()/2,
            f'{score:.4f}', va='center', color='#ebdbb2', fontsize=9)

plt.tight_layout()
plt.savefig('/tmp/feature_importance.png', dpi=100, bbox_inches='tight',
            facecolor='#282828')
print(" - บันทึกกราฟแล้ว")

5. Overfitting & Regularization (การ Overfit และการทำ Regularization)

Overfitting คือปรากฏการณ์ที่โมเดลเรียนรู้ข้อมูลฝึกได้ดีเกินไป จนไม่สามารถ generalize ไปยังข้อมูลใหม่ได้ Underfitting คือตรงข้าม โมเดลเรียนรู้น้อยเกินไป

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828', 'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945', 'lineColor': '#d79921',
  'background': '#282828', 'mainBkg': '#3c3836',
  'nodeBorder': '#504945', 'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2', 'fontFamily': 'Sarabun, sans-serif'
}}}%%
flowchart LR
  UF["🔵 Underfitting - High Bias - Low Variance -  - Train Error: สูง - Test Error: สูง"]
  GF["🟢 Good Fit - Low Bias - Low Variance -  - Train Error: ต่ำ - Test Error: ต่ำ"]
  OF["🔴 Overfitting - Low Bias - High Variance -  - Train Error: ต่ำมาก - Test Error: สูง"]
  
  UF -->|"เพิ่มความซับซ้อน - (เพิ่ม features, epochs)"| GF
  GF -->|"ซับซ้อนเกินไป - (ลด regularization)"| OF
  OF -->|"Regularization - (L1, L2, Dropout)"| GF

  style UF fill:#3c3836,stroke:#458588,color:#ebdbb2
  style GF fill:#3c3836,stroke:#b8bb26,color:#ebdbb2
  style OF fill:#3c3836,stroke:#fb4934,color:#ebdbb2

5.1 Regularization Techniques

Regularization เป็นเทคนิคเพิ่ม penalty term เข้าไปใน loss function เพื่อป้องกัน overfitting

L1 Regularization (LASSO):

Loss = MSE + λ j=1 n |βj|

L2 Regularization (Ridge):

Loss = MSE + λ j=1 n βj2

Elastic Net (L1 + L2):

Loss = MSE + λ1 j |βj| + λ2 j βj2

โดยที่ λ = regularization strength (ยิ่งมาก ยิ่ง penalize coefficients ใหญ่)

ความแตกต่างหลัก L1 vs L2:

คุณสมบัติ L1 (LASSO) L2 (Ridge)
ผลต่อ coefficients บางตัวเป็น 0 (Sparse) หดเล็กลงทุกตัว
Feature Selection ใช่ (อัตโนมัติ) ไม่
เมื่อ features สัมพันธ์กัน เลือกหนึ่งตัว กระจายน้ำหนักเท่าๆ กัน
เหมาะกับ Many irrelevant features Multicollinearity
"""
สาธิต Overfitting และการใช้ Regularization
พร้อมเปรียบเทียบ L1, L2, ElasticNet
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split, validation_curve
from sklearn.metrics import mean_squared_error
from sklearn.datasets import make_regression

# ===== สร้างข้อมูล: ความสัมพันธ์ไม่เชิงเส้น =====
np.random.seed(42)
n = 80
X_raw = np.sort(np.random.uniform(-3, 3, n))
y_raw = np.sin(X_raw) + 0.5 * np.cos(2*X_raw) + np.random.normal(0, 0.3, n)

X_raw = X_raw.reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(
    X_raw, y_raw, test_size=0.2, random_state=42
)

# ===== เปรียบเทียบ Polynomial Degree ต่างๆ =====
degrees = [1, 3, 6, 15]
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.patch.set_facecolor('#282828')
fig.suptitle('Underfitting vs Overfitting: Polynomial Regression',
             color='#ebdbb2', fontsize=14)

X_plot = np.linspace(-3, 3, 300).reshape(-1, 1)
colors_degree = ['#458588', '#b8bb26', '#d79921', '#fb4934']

for idx, (degree, ax, color) in enumerate(zip(degrees, axes.flat, colors_degree)):
    ax.set_facecolor('#3c3836')
    ax.tick_params(colors='#ebdbb2')

    model = Pipeline([
        ('poly', PolynomialFeatures(degree=degree)),
        ('reg',  LinearRegression())
    ])
    model.fit(X_train, y_train)

    y_plot  = model.predict(X_plot)
    train_rmse = np.sqrt(mean_squared_error(y_train, model.predict(X_train)))
    test_rmse  = np.sqrt(mean_squared_error(y_test,  model.predict(X_test)))

    ax.scatter(X_train, y_train, color='#83a598', s=30, alpha=0.7, label='Train')
    ax.scatter(X_test,  y_test,  color='#d3869b', s=30, alpha=0.7, label='Test', marker='^')
    ax.plot(X_plot, y_plot, color=color, linewidth=2.5)

    status = "✅ Good Fit" if degree == 3 else ("⬇️ Underfit" if degree == 1 else "🔴 Overfit")
    ax.set_title(
        f'Degree={degree} {status} - '
        f'Train RMSE={train_rmse:.3f}, Test RMSE={test_rmse:.3f}',
        color='#ebdbb2', fontsize=11
    )
    ax.legend(labelcolor='#ebdbb2', facecolor='#282828', fontsize=8)
    ax.set_ylim(-2.5, 2.5)

plt.tight_layout()
plt.savefig('/tmp/overfitting_demo.png', dpi=100, bbox_inches='tight',
            facecolor='#282828')
print("บันทึกกราฟ Overfitting Demo แล้ว")

# ===== เปรียบเทียบ L1 vs L2 Regularization =====
# ใช้ข้อมูล Regression ที่มี many features
X_reg, y_reg = make_regression(n_samples=200, n_features=50,
                                n_informative=10, noise=20, random_state=42)
X_tr, X_te, y_tr, y_te = train_test_split(X_reg, y_reg, test_size=0.2)

alphas = np.logspace(-3, 3, 50)
models_reg = {
    'Ridge (L2)':      Ridge,
    'LASSO (L1)':      Lasso,
    'ElasticNet (L1+L2)': lambda a: ElasticNet(alpha=a, l1_ratio=0.5),
}

fig2, ax2 = plt.subplots(figsize=(10, 5))
fig2.patch.set_facecolor('#282828')
ax2.set_facecolor('#3c3836')
ax2.tick_params(colors='#ebdbb2')

reg_colors = ['#83a598', '#fb4934', '#d79921']
for (name, ModelClass), color in zip(models_reg.items(), reg_colors):
    test_errors = []
    for alpha in alphas:
        if name == 'ElasticNet (L1+L2)':
            m = ModelClass(alpha)
        else:
            m = ModelClass(alpha=alpha)
        m.fit(X_tr, y_tr)
        test_errors.append(np.sqrt(mean_squared_error(y_te, m.predict(X_te))))
    ax2.semilogx(alphas, test_errors, color=color, linewidth=2, label=name)

ax2.set_xlabel('Alpha (λ) - Regularization Strength', color='#ebdbb2', fontsize=11)
ax2.set_ylabel('Test RMSE', color='#ebdbb2', fontsize=11)
ax2.set_title('L1 vs L2 vs ElasticNet Regularization - (Test Error vs Alpha)',
              color='#ebdbb2', fontsize=12)
ax2.legend(labelcolor='#ebdbb2', facecolor='#3c3836')
ax2.axvline(x=1.0, color='#504945', linestyle=':', linewidth=1)

plt.tight_layout()
plt.savefig('/tmp/regularization_comparison.png', dpi=100, bbox_inches='tight',
            facecolor='#282828')
print("บันทึกกราฟ Regularization Comparison แล้ว")

5.2 เทคนิคป้องกัน Overfitting อื่นๆ

เทคนิค วิธีการ เหมาะกับ
Cross-Validation ประเมินบนหลาย folds ทุกโมเดล
Early Stopping หยุดฝึกเมื่อ val loss ไม่ลด Neural Networks
Dropout สุ่มปิด neurons Neural Networks
Data Augmentation เพิ่มข้อมูลฝึก Computer Vision, NLP
Ensemble Methods รวมหลายโมเดล ทุกโมเดล
Pruning ตัด branches ที่ไม่สำคัญ Decision Trees
Regularization เพิ่ม penalty Linear Models

สรุปโดยรวม (Summary)

%%{init: {'theme': 'base', 'themeVariables': {
  'primaryColor': '#282828', 'primaryTextColor': '#ebdbb2',
  'primaryBorderColor': '#504945', 'lineColor': '#d79921',
  'background': '#282828', 'mainBkg': '#3c3836',
  'nodeBorder': '#504945', 'clusterBkg': '#32302f',
  'titleColor': '#ebdbb2', 'fontFamily': 'Sarabun, sans-serif'
}}}%%
mindmap
  root((Machine - Learning - Foundations))
    Supervised Learning
      Classification
        Decision Tree
        KNN
        Naive Bayes
        Random Forest
      Regression
        Linear Regression
        Polynomial Regression
      Model Evaluation
        Cross-Validation
        Confusion Matrix
        F1, AUC-ROC
    Unsupervised Learning
      Clustering
        K-Means
        Hierarchical
        DBSCAN
      Dim Reduction
        PCA
        t-SNE
    Reinforcement Learning
      Q-Learning
      Policy Functions
      Bellman Equation
    Feature Engineering
      Scaling & Encoding
      Feature Creation
      Feature Selection
    Regularization
      L1 LASSO
      L2 Ridge
      ElasticNet
      Overfitting Prevention

Machine Learning Foundations ครอบคลุมหัวข้อสำคัญ 5 ด้าน:

  1. Supervised Learning — เรียนรู้จากข้อมูลมีป้ายกำกับ ใช้สำหรับ Classification (Decision Tree, KNN, Naive Bayes) และ Regression (Linear, Polynomial) โดยต้องประเมินโมเดลด้วย Cross-Validation และ Confusion Matrix

  2. Unsupervised Learning — ค้นหารูปแบบในข้อมูลไม่มีป้ายกำกับ ผ่าน Clustering (K-Means, DBSCAN, Hierarchical) และ Dimensionality Reduction (PCA, t-SNE) เพื่อทำความเข้าใจโครงสร้างข้อมูล

  3. Reinforcement Learning — เรียนรู้ผ่านการโต้ตอบกับสภาพแวดล้อม โดย Agent สะสม reward สูงสุดด้วย Q-Learning และ Bellman Equation

  4. Feature Engineering — กระบวนการสำคัญที่เปลี่ยนข้อมูลดิบเป็น features ที่มีคุณภาพ ทั้งการสร้าง, แปลง (Scaling, Encoding), และเลือก features (Filter, Wrapper, Embedded methods)

  5. Overfitting & Regularization — ปัญหาสำคัญของ ML ที่แก้ได้ด้วย L1/L2 Regularization, Cross-Validation, Early Stopping และ Ensemble Methods


เอกสารอ้างอิง (References)

  1. Mitchell, T. M. (1997). Machine Learning. McGraw-Hill. — ตำราพื้นฐาน ML คลาสสิก

  2. Géron, A. (2022). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (3rd ed.). O'Reilly Media. — แนวปฏิบัติจริงด้วย Python

  3. Bishop, C. M. (2006). Pattern Recognition and Machine Learning. Springer. — เนื้อหาเชิงทฤษฎีระดับสูง

  4. Sutton, R. S., & Barto, A. G. (2018). Reinforcement Learning: An Introduction (2nd ed.). MIT Press. — RL reference หลัก [Online: http://incompleteideas.net/book/the-book.html]

  5. Pedregosa, F. et al. (2011). Scikit-learn: Machine Learning in Python. Journal of Machine Learning Research, 12, 2825–2830. — เอกสาร scikit-learn อย่างเป็นทางการ

  6. scikit-learn Documentation. (2024). https://scikit-learn.org/stable/documentation.html

  7. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press. [Online: https://www.deeplearningbook.org/]

  8. Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (2nd ed.). Springer. [Online: https://hastie.su.domains/ElemStatLearn/]


📌 หมายเหตุสำหรับผู้เรียน: เอกสารนี้มุ่งให้ความเข้าใจทั้งทฤษฎีและการปฏิบัติควบคู่กัน ขอแนะนำให้ลองรัน code ตัวอย่างทุกส่วน และทดลองปรับ hyperparameters เพื่อสังเกตผลที่เปลี่ยนแปลง การเรียนรู้ Machine Learning ต้องอาศัยการทดลองซ้ำๆ จึงจะเกิดความเข้าใจที่ลึกซึ้ง