はじめに
本記事は「Pythonで学ぶデータ分析実践」シリーズの補足記事として、機械学習モデルの分析精度を向上させるための手法を解説します。
教師あり学習では、学習データに過剰に適合してしまう**過学習(オーバーフィッティング)**が大きな問題になります。本記事では、過学習を防ぎながら未知データへの予測精度を高めるための手法として、正則化、グリッドサーチ、ランダムフォレスト、アンサンブル学習を実践的に解説します。
1. 分析精度の向上 概要
過学習問題
過学習とは、モデルが学習データに適合しすぎて、未知のデータに対する汎化性能が失われてしまう現象です。
理想的なモデル: 学習データ精度 ≈ テストデータ精度(両方高い)
過学習モデル: 学習データ精度 >> テストデータ精度(学習だけ高い)
未学習モデル: 学習データ精度 ≈ テストデータ精度(両方低い)
過学習の原因と対策
| 原因 | 対策 |
|---|---|
| モデルが複雑すぎる | 正則化、モデルの簡素化 |
| 学習データが少ない | データ増強、交差検証 |
| 特徴量が多すぎる | 特徴量選択、次元削減 |
| 特徴量間に高い相関がある | 多重共線性の排除 |
| ノイズを学習している | データスケーリング、外れ値除去 |
精度向上のための主な手法
| 手法 | カテゴリ | 効果 |
|---|---|---|
| データスケーリング | 前処理 | 特徴量のスケールを統一 |
| 特徴量選択 | 前処理 | 不要な特徴量を除外 |
| 次元削減(PCA) | 前処理 | 高次元データの情報圧縮 |
| 正則化(Ridge, Lasso) | モデル調整 | 過学習の抑制 |
| グリッドサーチ | パラメータ調整 | 最適なハイパーパラメータの探索 |
| アンサンブル学習 | モデル選択 | 複数モデルの組み合わせで精度向上 |
データスケーリングの復習
| 手法 | 適用条件 | 変換後の値 |
|---|---|---|
| Min-Max法(正規化) | データが一様分布の場合 | 0〜1の範囲 |
| Zスコア標準化 | データが正規分布の場合 | 平均0、標準偏差1 |
from sklearn.preprocessing import MinMaxScaler, StandardScaler
import numpy as np
data = np.array([[100, 50000], [200, 80000], [150, 60000]])
# Min-Max法
mm_scaler = MinMaxScaler()
data_mm = mm_scaler.fit_transform(data)
# Zスコア標準化
std_scaler = StandardScaler()
data_std = std_scaler.fit_transform(data)
print("=== スケーリング比較 ===")
print(f"元データ:\n{data}")
print(f"\nMin-Max法:\n{data_mm}")
print(f"\nZスコア標準化:\n{data_std}")
2. 正則化
正則化は、モデルの複雑さにペナルティを課すことで過学習を抑制する手法です。評価用データの精度が学習用データの精度より著しく劣る場合に特に有効です。
L1正則化とL2正則化の比較
| 項目 | L2正則化(Ridge) | L1正則化(Lasso) |
|---|---|---|
| ペナルティ | パラメータの2乗和 | パラメータの絶対値和 |
| 効果 | 重要度の低い特徴の影響を縮小 | 不要な特徴の重みを0にする |
| 特徴選択 | しない(全特徴を残す) | する(不要な特徴を除外) |
| 適用場面 | 多重共線性がある場合 | 特徴量が多く選択が必要な場合 |
Ridge回帰(L2正則化)
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import r2_score, mean_squared_error
import matplotlib.pyplot as plt
import japanize_matplotlib
# カリフォルニア住宅価格データ
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
y = housing.target
# データ分割とスケーリング
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)
X_test_scaled = scaler.transform(X_test)
# 通常の線形回帰
lr = LinearRegression()
lr.fit(X_train_scaled, y_train)
# Ridge回帰(L2正則化)
ridge = Ridge(alpha=1.0) # alpha: 正則化の強さ
ridge.fit(X_train_scaled, y_train)
# 比較
print("=== 線形回帰 vs Ridge回帰 ===")
print(f"{'モデル':<15} {'学習R²':>8} {'テストR²':>8} {'RMSE':>8}")
print("-" * 45)
for name, model in [("線形回帰", lr), ("Ridge(α=1.0)", ridge)]:
train_r2 = model.score(X_train_scaled, y_train)
test_r2 = model.score(X_test_scaled, y_test)
rmse = np.sqrt(mean_squared_error(y_test, model.predict(X_test_scaled)))
print(f"{name:<15} {train_r2:>8.4f} {test_r2:>8.4f} {rmse:>8.4f}")
Lasso回帰(L1正則化)
# Lasso回帰(L1正則化)
lasso = Lasso(alpha=0.01)
lasso.fit(X_train_scaled, y_train)
# 係数の比較(特徴量選択の効果)
coef_df = pd.DataFrame({
"特徴量": housing.feature_names,
"線形回帰": lr.coef_,
"Ridge": ridge.coef_,
"Lasso": lasso.coef_
})
print("\n=== 各モデルの係数比較 ===")
print(coef_df.round(4).to_string(index=False))
print(f"\nLassoで0になった係数の数: {(lasso.coef_ == 0).sum()}")
正則化パラメータ(alpha)の影響
# alphaの値を変えて精度を比較
alphas = [0.001, 0.01, 0.1, 1, 10, 100]
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for idx, (Model, name) in enumerate([(Ridge, "Ridge"), (Lasso, "Lasso")]):
train_scores, test_scores = [], []
for alpha in alphas:
model = Model(alpha=alpha)
model.fit(X_train_scaled, y_train)
train_scores.append(model.score(X_train_scaled, y_train))
test_scores.append(model.score(X_test_scaled, y_test))
axes[idx].plot(alphas, train_scores, 'o-', label='学習データ', markersize=6)
axes[idx].plot(alphas, test_scores, 'o-', label='テストデータ', markersize=6)
axes[idx].set_xscale('log')
axes[idx].set_xlabel("alpha(正則化の強さ)")
axes[idx].set_ylabel("R²スコア")
axes[idx].set_title(f"{name}回帰: alphaと精度の関係")
axes[idx].legend()
axes[idx].grid(alpha=0.3)
plt.tight_layout()
plt.show()
PLS回帰(部分的最小二乗回帰)
PLS回帰は、説明変数を互いに無相関な潜在変数に変換して回帰を行う手法です。多重共線性の問題が生じず、サンプルサイズが説明変数より少ない場合にも有効です。
from sklearn.cross_decomposition import PLSRegression
# PLS回帰
pls = PLSRegression(n_components=5) # 潜在変数の数
pls.fit(X_train_scaled, y_train)
pls_train_r2 = pls.score(X_train_scaled, y_train)
pls_test_r2 = pls.score(X_test_scaled, y_test)
print(f"\n=== PLS回帰(n_components=5) ===")
print(f"学習R²: {pls_train_r2:.4f}")
print(f"テストR²: {pls_test_r2:.4f}")
# コンポーネント数の最適化
for n in range(1, 9):
pls_temp = PLSRegression(n_components=n)
pls_temp.fit(X_train_scaled, y_train)
print(f" n_components={n}: テストR²={pls_temp.score(X_test_scaled, y_test):.4f}")
3. グリッドサーチ
グリッドサーチは、ハイパーパラメータの候補値を全組み合わせ試し、最も良い性能を示すパラメータを自動で探索する手法です。
GridSearchCVの仕組み
1. パラメータ候補の全組み合わせを生成
2. 各組み合わせに対して交差検証を実施
3. 平均スコアが最も高い組み合わせを選択
主要パラメータ
| パラメータ | 意味 |
|---|---|
| estimator | チューニング対象のモデル |
| param_grid | パラメータ候補を辞書形式で指定 |
| scoring | 評価指標('accuracy', 'r2'など) |
| cv | 交差検証の分割数(デフォルト: 5) |
| n_jobs | 並列計算数(-1で全CPU使用) |
実装例:SVMのグリッドサーチ
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
from sklearn.datasets import load_breast_cancer
from sklearn.preprocessing import StandardScaler
# データ準備
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, test_size=0.2, random_state=42
)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)
# パラメータ候補の定義
param_grid = {
'C': [0.1, 1, 10, 100],
'gamma': ['scale', 0.01, 0.1, 1],
'kernel': ['rbf', 'linear']
}
# グリッドサーチの実行
grid_search = GridSearchCV(
estimator=SVC(),
param_grid=param_grid,
scoring='accuracy',
cv=5,
n_jobs=-1,
verbose=0
)
grid_search.fit(X_train_s, y_train)
# 結果
print("=== グリッドサーチ結果 ===")
print(f"最適パラメータ: {grid_search.best_params_}")
print(f"最高CV スコア: {grid_search.best_score_:.4f}")
print(f"テストスコア: {grid_search.score(X_test_s, y_test):.4f}")
# デフォルトパラメータとの比較
svc_default = SVC().fit(X_train_s, y_train)
print(f"\nデフォルトSVMのテストスコア: {svc_default.score(X_test_s, y_test):.4f}")
print(f"グリッドサーチ後のテストスコア: {grid_search.score(X_test_s, y_test):.4f}")
結果の詳細確認
# 全パラメータ組み合わせの結果
results_df = pd.DataFrame(grid_search.cv_results_)
top10 = results_df.nsmallest(10, 'rank_test_score')[
['params', 'mean_test_score', 'std_test_score', 'rank_test_score']
]
print("\n=== 上位10パラメータ ===")
print(top10.to_string(index=False))
4. ランダムフォレスト
ランダムフォレストは、複数の決定木をランダムにサンプリングしたデータで作成し、各決定木の結果の多数決(分類)や平均値(回帰)で最終予測を行う手法です。単体の決定木に比べて過学習を抑えることができます。
ランダムフォレストの仕組み
元データ → ブートストラップサンプリング → 決定木1(特徴量もランダム選択)
→ ブートストラップサンプリング → 決定木2(特徴量もランダム選択)
→ ブートストラップサンプリング → 決定木3(特徴量もランダム選択)
...
→ ブートストラップサンプリング → 決定木N(特徴量もランダム選択)
分類の場合: 全決定木の多数決 → 最終予測
回帰の場合: 全決定木の平均値 → 最終予測
RandomForestClassifierの主要パラメータ
| パラメータ | デフォルト値 | 意味 |
|---|---|---|
| n_estimators | 100 | 決定木の本数 |
| criterion | 'gini' | 分割の指標 [gini, entropy] |
| max_depth | None | 木の最大深さ |
| min_samples_leaf | 1 | 葉の最小サンプル数 |
| max_features | 'sqrt' | 各木で使用する特徴量数 |
| random_state | None | 乱数シード |
| n_jobs | None | 並列計算数 |
実装例
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report
# ランダムフォレスト
rf = RandomForestClassifier(
n_estimators=100,
max_depth=None,
min_samples_leaf=2,
random_state=42,
n_jobs=-1
)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)
print(f"=== ランダムフォレスト ===")
print(f"学習スコア: {rf.score(X_train, y_train):.4f}")
print(f"テストスコア: {rf.score(X_test, y_test):.4f}")
# 特徴量の重要度
importances = pd.DataFrame({
"特徴量": housing.feature_names,
"重要度": rf.feature_importances_
}).sort_values("重要度", ascending=False)
print(f"\n特徴量の重要度(上位5件):")
print(importances.head().to_string(index=False))
5. アンサンブル学習
アンサンブル学習は、複数のモデル(弱学習器)を組み合わせることで、単一モデルよりも高い精度を実現する手法です。
アンサンブル学習の3つの手法
| 手法 | 方式 | 特徴 |
|---|---|---|
| Bagging(バギング) | 並列 | データをランダム抽出して複数モデルを独立に学習。過学習に強い |
| Boosting(ブースティング) | 直列 | 前のモデルの誤りを重点的に学習。高精度だが過学習リスクあり |
| Stacking(スタッキング) | 階層 | 複数モデルの予測を入力として、別モデルで最終予測 |
Bagging(バギング)
ブートストラップ法でデータを複数生成し、各モデルを並列に学習させます。ランダムフォレストはバギングの代表例です。
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
# バギング(決定木ベース)
bagging = BaggingClassifier(
estimator=DecisionTreeClassifier(),
n_estimators=50,
max_samples=0.8, # 各モデルに使うデータの割合
random_state=42,
n_jobs=-1
)
bagging.fit(X_train_s, y_train)
print(f"=== Bagging ===")
print(f"テストスコア: {bagging.score(X_test_s, y_test):.4f}")
Boosting(ブースティング)
前のモデルが誤分類したデータに重みを付けて、順に学習を進めます。
from sklearn.ensemble import GradientBoostingClassifier, AdaBoostClassifier
# AdaBoost
ada = AdaBoostClassifier(n_estimators=100, random_state=42)
ada.fit(X_train_s, y_train)
# 勾配ブースティング
gb = GradientBoostingClassifier(n_estimators=100, max_depth=3, random_state=42)
gb.fit(X_train_s, y_train)
print(f"=== Boosting ===")
print(f"AdaBoost テストスコア: {ada.score(X_test_s, y_test):.4f}")
print(f"GradientBoosting テストスコア: {gb.score(X_test_s, y_test):.4f}")
代表的なブースティングライブラリ
| ライブラリ | 特徴 |
|---|---|
| XGBoost | 高速・高精度。Kaggleで広く使用 |
| LightGBM | XGBoostより高速。大規模データ向け |
| CatBoost | カテゴリ変数を自動処理。設定が少なくて済む |
# XGBoostの例(要インストール: pip install xgboost)
# from xgboost import XGBClassifier
# xgb = XGBClassifier(n_estimators=100, max_depth=3, random_state=42)
# xgb.fit(X_train_s, y_train)
# print(f"XGBoost テストスコア: {xgb.score(X_test_s, y_test):.4f}")
Stacking(スタッキング)
from sklearn.ensemble import StackingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
# スタッキング
estimators = [
('rf', RandomForestClassifier(n_estimators=50, random_state=42)),
('svc', SVC(probability=True, random_state=42)),
('dt', DecisionTreeClassifier(max_depth=5, random_state=42))
]
stacking = StackingClassifier(
estimators=estimators,
final_estimator=LogisticRegression(), # メタ学習器
cv=5
)
stacking.fit(X_train_s, y_train)
print(f"\n=== Stacking ===")
print(f"テストスコア: {stacking.score(X_test_s, y_test):.4f}")
全手法の比較
from sklearn.model_selection import cross_val_score
models = {
"線形回帰(正則化なし)": LinearRegression(),
"Ridge(α=1)": Ridge(alpha=1.0),
"Lasso(α=0.01)": Lasso(alpha=0.01),
"ランダムフォレスト": RandomForestClassifier(n_estimators=100, random_state=42),
"AdaBoost": AdaBoostClassifier(n_estimators=100, random_state=42),
"GradientBoosting": GradientBoostingClassifier(n_estimators=100, random_state=42),
}
print("\n=== アンサンブル手法の比較(乳がんデータ: 5分割CV) ===")
print(f"{'モデル':<25} {'平均スコア':>10} {'標準偏差':>10}")
print("-" * 50)
for name, model in [
("ロジスティック回帰", LogisticRegression(max_iter=1000)),
("SVM(RBF)", SVC()),
("決定木", DecisionTreeClassifier(max_depth=5)),
("ランダムフォレスト", RandomForestClassifier(n_estimators=100, random_state=42)),
("AdaBoost", AdaBoostClassifier(n_estimators=100, random_state=42)),
("GradientBoosting", GradientBoostingClassifier(n_estimators=100, random_state=42)),
]:
scores = cross_val_score(model, X_train_s, y_train, cv=5, scoring='accuracy')
print(f"{name:<25} {scores.mean():>10.4f} {scores.std():>10.4f}")
まとめ
| 項目 | ポイント |
|---|---|
| 過学習 | 学習データに適合しすぎ、未知データへの精度が低下する問題 |
| 正則化 | Ridge(L2)で縮小、Lasso(L1)で特徴量選択 |
| PLS回帰 | 多重共線性に強い、少サンプルでも有効 |
| グリッドサーチ | パラメータ候補を全探索して最適値を発見 |
| ランダムフォレスト | バギング+決定木で過学習を抑制 |
| アンサンブル学習 | Bagging/Boosting/Stackingで精度向上 |
チェックリスト
- 過学習していないか確認したか(学習/テストの精度差)
- データスケーリングを行ったか
- 正則化を試したか(Ridge, Lasso)
- グリッドサーチでパラメータ最適化を行ったか
- アンサンブル手法を試したか
- 交差検証でモデルの安定性を確認したか
- 特徴量の重要度を確認したか