- Elastic Net รวมโทษ L1 และ L2 เข้าด้วยกัน ทำให้ได้ทั้งความเบาบางของโมเดลและความเสถียรของสัมประสิทธิ์
- เหมาะกับสถานการณ์ที่ตัวแปรป้อนเข้ามีความสัมพันธ์กันสูง เพราะสามารถรักษากลุ่มตัวแปรที่คล้ายกันไว้พร้อมปรับน้ำหนักความสำคัญ
- ปรับ \(\alpha\) และ
l1_ratioด้วยการตรวจสอบไขว้จะช่วยหาจุดสมดุลระหว่างการกระจายตัวกับอคติได้ง่าย - การทำมาตรฐานและการเพิ่มจำนวนรอบการวนซ้ำช่วยให้การเพิ่มประสิทธิภาพเชิงตัวเลขมีความน่าเชื่อถือมากขึ้น
ภาพรวมเชิงสัญชาติญาณ #
การถดถอยแบบแลสโซบังคับให้สัมประสิทธิ์เป็นศูนย์และทำหน้าที่คัดเลือกคุณลักษณะ แต่ถ้าตัวแปรมีความสัมพันธ์กันสูงมักเหลือเพียงตัวเดียว การถดถอยแบบริจบีบสัมประสิทธิ์ให้เล็กลงและเสถียร แต่ไม่เป็นศูนย์ Elastic Net จึงจับจุดแข็งทั้งสองเข้าด้วยกัน: รักษากลุ่มคุณลักษณะที่คล้ายกันไว้พร้อมกัน และผลักตัวแปรที่ไม่จำเป็นให้เข้าใกล้ศูนย์
สูตรสำคัญ #
ฟังก์ชันวัตถุประสงค์ของ Elastic Net คือ
$$ \min_{\boldsymbol\beta, b} \sum_{i=1}^{n} \left( y_i - (\boldsymbol\beta^\top \mathbf{x}_i + b) \right)^2 + \alpha \left( \rho \lVert \boldsymbol\beta \rVert_1 + (1 - \rho) \lVert \boldsymbol\beta \rVert_2^2 \right) $$
โดย \(\alpha > 0\) คุมความแรงของการทำให้เป็นระเบียบ และ \(\rho \in [0,1]\) (l1_ratio) ระบุสัดส่วนระหว่างโทษ L1 กับ L2 จึงเลื่อนไปมาระหว่างริจกับแลสโซได้อย่างต่อเนื่อง
ทดลองด้วย Python #
โค้ดต่อไปนี้ใช้ ElasticNetCV เพื่อปรับ \(\alpha\) และ l1_ratio พร้อมกัน จากนั้นแสดงทั้งตัวชี้วัดและสัมประสิทธิ์ที่สำคัญ
from __future__ import annotations
import japanize_matplotlib
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import make_regression
from sklearn.linear_model import ElasticNet, ElasticNetCV
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
def run_elastic_net_demo(
n_samples: int = 500,
n_features: int = 30,
n_informative: int = 10,
noise: float = 15.0,
duplicate_features: int = 5,
label_scatter_x: str = "predicted",
label_scatter_y: str = "actual",
label_scatter_title: str = "Predicted vs. actual",
label_bar_title: str = "Top coefficients",
label_bar_ylabel: str = "coefficient",
top_n: int = 10,
) -> dict[str, float]:
"""Fit Elastic Net with CV, report metrics, and plot predictions/coefs.
Args:
n_samples: Number of generated samples.
n_features: Total features before duplication.
n_informative: Features with non-zero weights in the generator.
noise: Standard deviation of noise added to targets.
duplicate_features: Number of leading features to duplicate for correlation.
label_scatter_x: Label for the scatter plot x-axis.
label_scatter_y: Label for the scatter plot y-axis.
label_scatter_title: Title for the scatter plot.
label_bar_title: Title for the coefficient bar plot.
label_bar_ylabel: Y-axis label for the coefficient plot.
top_n: Number of largest-magnitude coefficients to display.
Returns:
Dictionary with training/test metrics for inspection.
"""
japanize_matplotlib.japanize()
rng = np.random.default_rng(seed=123)
X, y, coef = make_regression(
n_samples=n_samples,
n_features=n_features,
n_informative=n_informative,
noise=noise,
coef=True,
random_state=123,
)
correlated = X[:, :duplicate_features] + rng.normal(
scale=0.1, size=(X.shape[0], duplicate_features)
)
X = np.hstack([X, correlated])
feature_names = np.array([f"x{i}" for i in range(X.shape[1])], dtype=object)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42
)
enet_cv = ElasticNetCV(
l1_ratio=[0.2, 0.5, 0.7, 0.9, 0.95, 1.0],
alphas=np.logspace(-3, 1, 30),
cv=5,
random_state=42,
max_iter=5000,
)
enet_cv.fit(X_train, y_train)
enet = ElasticNet(
alpha=float(enet_cv.alpha_),
l1_ratio=float(enet_cv.l1_ratio_),
max_iter=5000,
random_state=42,
)
enet.fit(X_train, y_train)
train_pred = enet.predict(X_train)
test_pred = enet.predict(X_test)
metrics = {
"best_alpha": float(enet_cv.alpha_),
"best_l1_ratio": float(enet_cv.l1_ratio_),
"train_r2": float(r2_score(y_train, train_pred)),
"test_r2": float(r2_score(y_test, test_pred)),
"test_rmse": float(mean_squared_error(y_test, test_pred, squared=False)),
}
top_idx = np.argsort(np.abs(enet.coef_))[-top_n:][::-1]
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
ax_scatter, ax_bar = axes
ax_scatter.scatter(test_pred, y_test, alpha=0.6, color="#1f77b4")
ax_scatter.plot(
[y_test.min(), y_test.max()],
[y_test.min(), y_test.max()],
color="#d62728",
linestyle="--",
linewidth=1.5,
)
ax_scatter.set_title(label_scatter_title)
ax_scatter.set_xlabel(label_scatter_x)
ax_scatter.set_ylabel(label_scatter_y)
ax_bar.bar(
np.arange(top_n),
enet.coef_[top_idx],
color="#ff7f0e",
)
ax_bar.set_xticks(np.arange(top_n))
ax_bar.set_xticklabels(feature_names[top_idx], rotation=45, ha="right")
ax_bar.set_title(label_bar_title)
ax_bar.set_ylabel(label_bar_ylabel)
fig.tight_layout()
plt.show()
return metrics
metrics = run_elastic_net_demo(
label_scatter_x="ค่าที่พยากรณ์",
label_scatter_y="ค่าจริง",
label_scatter_title="เปรียบเทียบค่าพยากรณ์กับค่าจริง",
label_bar_title="สัมประสิทธิ์ที่สำคัญ",
label_bar_ylabel="ขนาดสัมประสิทธิ์",
)
print(f"alpha ที่เหมาะสม: {metrics['best_alpha']:.4f}")
print(f"l1_ratio ที่เหมาะสม: {metrics['best_l1_ratio']:.2f}")
print(f"R^2 บนชุดฝึก: {metrics['train_r2']:.3f}")
print(f"R^2 บนชุดทดสอบ: {metrics['test_r2']:.3f}")
print(f"RMSE บนชุดทดสอบ: {metrics['test_rmse']:.3f}")
วิเคราะห์ผลลัพธ์ #
ElasticNetCVประเมินชุดค่าผสมของ L1 และ L2 ให้อัตโนมัติ จึงค้นหาค่าที่เหมาะสมได้โดยไม่ต้องทำด้วยมือ- แม้ตัวแปรจะมีความสัมพันธ์กันสูง โมเดลยังคงรักษาพวกมันไว้เป็นกลุ่มพร้อมค่าสัมประสิทธิ์ที่ขนาดใกล้เคียงกัน ทำให้อธิบายได้ง่าย
- หากการหาคำตอบล่าช้าลองปรับให้คุณสมบัติอยู่ในสเกลเดียวกันหรือเพิ่ม
max_iterเพื่อช่วยให้การเพิ่มประสิทธิภาพลู่เข้า
เอกสารอ้างอิง #
- Zou, H., & Hastie, T. (2005). Regularization and Variable Selection via the Elastic Net. Journal of the Royal Statistical Society: Series B, 67(2), 301 E20.
- Friedman, J., Hastie, T., & Tibshirani, R. (2010). Regularization Paths for Generalized Linear Models via Coordinate Descent. Journal of Statistical Software, 33(1), 1 E2.