本章では、組成ベース特徴量(Magpie)とGNN構造ベース特徴量(CGCNN)の性能を、Matbench標準ベンチマークを用いて定量的に比較します。単なる精度比較だけでなく、統計的有意性、計算コスト、データ要求量、解釈可能性の多角的な観点から実証分析を行います。
Matbench(Materials Benchmark)は、材料科学における機械学習モデルの標準化された評価プラットフォームです。13種類の材料物性予測タスクが定義されており、各タスクには訓練データ、検証データ、テストデータが事前に分割されています。これにより、異なる研究間での公平な性能比較が可能になります。
| タスク名 | 物性 | データ数 | 評価指標 |
|---|---|---|---|
| matbench_mp_e_form | 生成エネルギー (eV/atom) | 132,752 | MAE |
| matbench_mp_gap | バンドギャップ (eV) | 106,113 | MAE |
| matbench_perovskites | 生成エネルギー (eV/atom) | 18,928 | MAE |
| matbench_phonons | 最大フォノン周波数 (cm⁻¹) | 1,265 | MAE |
| matbench_jdft2d | 剥離エネルギー (meV/atom) | 636 | MAE |
本章では、matbench_mp_e_form(生成エネルギー予測)とmatbench_mp_gap(バンドギャップ予測)の2つのタスクを対象に、Random Forest(Magpie特徴量)とCGCNNの性能を比較します。
Matbenchでは、5-fold交差検証による評価が標準プロトコルです。データセット全体が5つのfoldに分割されており、各foldについて以下の評価を実施します:
5つのfoldの評価指標を平均することで、汎化性能の信頼性の高い推定が得られます。
本節では、Random Forest(Magpie)とCGCNN on Matbench mp_e_formタスクを実装し、予測精度を定量的に比較します。
# Matbenchデータセットのロードと前処理
import numpy as np
import pandas as pd
from matbench.bench import MatbenchBenchmark
from pymatgen.core import Structure
from matminer.featurizers.composition import ElementProperty
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import warnings
warnings.filterwarnings('ignore')
# Matbenchベンチマークの初期化
mb = MatbenchBenchmark(autoload=False)
# mp_e_formタスクをロード(生成エネルギー予測)
task = mb.matbench_mp_e_form
task.load()
print(f"タスク名: {task.metadata['task_type']}")
print(f"データ数: {len(task.df)}")
print(f"入力: {task.metadata['input_type']}")
print(f"出力: {task.metadata['target']}")
# データ構造の確認
print("\nデータサンプル:")
print(task.df.head(3))
# 出力例:
# タスク名: regression
# データ数: 132752
# 入力: structure
# 出力: e_form (eV/atom)
#
# データサンプル:
# structure e_form
# 0 Structure: Fe2 O3 ... -2.54
# 1 Structure: Mn2 O3 ... -2.89
# 2 Structure: Co2 O3 ... -2.31
# Random Forest + Magpie特徴量の実装
from matminer.featurizers.composition import ElementProperty
def extract_magpie_features(structures):
"""
PymatgenのStructureからMagpie特徴量を抽出
Parameters:
-----------
structures : list of Structure
結晶構造のリスト
Returns:
--------
features : np.ndarray, shape (n_samples, 145)
Magpie特徴量(145次元)
"""
# Magpie特徴量抽出器の初期化
featurizer = ElementProperty.from_preset("magpie")
features = []
for struct in structures:
# StructureからCompositionを取得
comp = struct.composition
# Magpie特徴量を計算(145次元)
feat = featurizer.featurize(comp)
features.append(feat)
return np.array(features)
# 5-fold交差検証の評価
rf_results = []
for fold_idx, fold in enumerate(task.folds):
print(f"\n=== Fold {fold_idx + 1}/5 ===")
# 訓練データとテストデータを取得
train_inputs, train_outputs = task.get_train_and_val_data(fold)
test_inputs, test_outputs = task.get_test_data(fold, include_target=True)
# Magpie特徴量抽出
print("Magpie特徴量を抽出中...")
X_train = extract_magpie_features(train_inputs)
X_test = extract_magpie_features(test_inputs)
y_train = train_outputs.values
y_test = test_outputs.values
print(f"訓練データ: {X_train.shape}, テストデータ: {X_test.shape}")
# Random Forestモデルの訓練
print("Random Forestを訓練中...")
rf_model = RandomForestRegressor(
n_estimators=100,
max_depth=30,
min_samples_split=5,
random_state=42,
n_jobs=-1
)
rf_model.fit(X_train, y_train)
# テストデータで予測
y_pred = rf_model.predict(X_test)
# 評価指標を計算
mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
print(f"MAE: {mae:.4f} eV/atom")
print(f"RMSE: {rmse:.4f} eV/atom")
print(f"R²: {r2:.4f}")
rf_results.append({
'fold': fold_idx + 1,
'mae': mae,
'rmse': rmse,
'r2': r2
})
# 5-fold平均スコアを計算
rf_df = pd.DataFrame(rf_results)
print("\n=== Random Forest (Magpie) 総合結果 ===")
print(f"MAE: {rf_df['mae'].mean():.4f} ± {rf_df['mae'].std():.4f} eV/atom")
print(f"RMSE: {rf_df['rmse'].mean():.4f} ± {rf_df['rmse'].std():.4f} eV/atom")
print(f"R²: {rf_df['r2'].mean():.4f} ± {rf_df['r2'].std():.4f}")
# 出力例:
# === Random Forest (Magpie) 総合結果 ===
# MAE: 0.0325 ± 0.0012 eV/atom
# RMSE: 0.0678 ± 0.0019 eV/atom
# R²: 0.9321 ± 0.0045
# CGCNN on Matbenchの実装
import torch
import torch.nn as nn
from torch_geometric.data import Data, DataLoader
from torch_geometric.nn import CGConv, global_mean_pool
import time
# CGCNN for Matbenchの定義(Chapter 2のモデルを使用)
class CGCNNMatbench(nn.Module):
def __init__(self, atom_fea_len=92, nbr_fea_len=41, hidden_dim=128, n_conv=3):
super(CGCNNMatbench, self).__init__()
# Atom embedding
self.atom_embedding = nn.Linear(atom_fea_len, hidden_dim)
# Convolution layers
self.conv_layers = nn.ModuleList([
CGConv(hidden_dim, nbr_fea_len) for _ in range(n_conv)
])
self.bn_layers = nn.ModuleList([
nn.BatchNorm1d(hidden_dim) for _ in range(n_conv)
])
# Readout
self.fc1 = nn.Linear(hidden_dim, 64)
self.fc2 = nn.Linear(64, 1)
self.activation = nn.Softplus()
def forward(self, data):
x, edge_index, edge_attr, batch = data.x, data.edge_index, data.edge_attr, data.batch
# Atom embedding
x = self.atom_embedding(x)
# Graph convolutions
for conv, bn in zip(self.conv_layers, self.bn_layers):
x = conv(x, edge_index, edge_attr)
x = bn(x)
x = self.activation(x)
# Global pooling
x = global_mean_pool(x, batch)
# Fully connected layers
x = self.fc1(x)
x = self.activation(x)
x = self.fc2(x)
return x.squeeze()
def structure_to_pyg_data(structure, target, cutoff=8.0):
"""
PymatgenのStructureをPyTorch Geometricのデータに変換
Parameters:
-----------
structure : Structure
結晶構造
target : float
目標値(生成エネルギー)
cutoff : float
カットオフ半径 (Å)
Returns:
--------
data : Data
PyTorch Geometricのデータオブジェクト
"""
# ノード特徴量(元素のone-hot encoding、92次元)
atom_types = [site.specie.Z for site in structure]
x = torch.zeros(len(atom_types), 92)
for i, z in enumerate(atom_types):
x[i, z - 1] = 1.0
# エッジ構築(cutoff半径以内の原子ペア)
neighbors = structure.get_all_neighbors(cutoff)
edge_index = []
edge_attr = []
for i, neighbors_i in enumerate(neighbors):
for neighbor in neighbors_i:
j = neighbor.index
distance = neighbor.nn_distance
# Gaussian distance expansion (41次元)
distances = torch.linspace(0, cutoff, 41)
sigma = 0.5
edge_feature = torch.exp(-((distance - distances) ** 2) / (2 * sigma ** 2))
edge_index.append([i, j])
edge_attr.append(edge_feature)
edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
edge_attr = torch.stack(edge_attr)
y = torch.tensor([target], dtype=torch.float)
return Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y)
# 5-fold交差検証の評価
cgcnn_results = []
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用デバイス: {device}")
for fold_idx, fold in enumerate(task.folds):
print(f"\n=== Fold {fold_idx + 1}/5 ===")
# 訓練データとテストデータを取得
train_inputs, train_outputs = task.get_train_and_val_data(fold)
test_inputs, test_outputs = task.get_test_data(fold, include_target=True)
# PyTorch Geometricデータに変換
print("グラフデータを構築中...")
train_data = [structure_to_pyg_data(s, t) for s, t in zip(train_inputs, train_outputs.values)]
test_data = [structure_to_pyg_data(s, t) for s, t in zip(test_inputs, test_outputs.values)]
train_loader = DataLoader(train_data, batch_size=32, shuffle=True)
test_loader = DataLoader(test_data, batch_size=32, shuffle=False)
# CGCNNモデルの初期化
model = CGCNNMatbench().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.L1Loss() # MAE損失
# 訓練
print("CGCNNを訓練中...")
model.train()
for epoch in range(50): # 実際にはearly stoppingを使用すべき
total_loss = 0
for batch in train_loader:
batch = batch.to(device)
optimizer.zero_grad()
out = model(batch)
loss = criterion(out, batch.y)
loss.backward()
optimizer.step()
total_loss += loss.item()
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}/50, Loss: {total_loss/len(train_loader):.4f}")
# テスト
model.eval()
y_true = []
y_pred = []
with torch.no_grad():
for batch in test_loader:
batch = batch.to(device)
out = model(batch)
y_true.extend(batch.y.cpu().numpy())
y_pred.extend(out.cpu().numpy())
y_true = np.array(y_true)
y_pred = np.array(y_pred)
# 評価指標を計算
mae = mean_absolute_error(y_true, y_pred)
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
r2 = r2_score(y_true, y_pred)
print(f"MAE: {mae:.4f} eV/atom")
print(f"RMSE: {rmse:.4f} eV/atom")
print(f"R²: {r2:.4f}")
cgcnn_results.append({
'fold': fold_idx + 1,
'mae': mae,
'rmse': rmse,
'r2': r2
})
# 5-fold平均スコアを計算
cgcnn_df = pd.DataFrame(cgcnn_results)
print("\n=== CGCNN 総合結果 ===")
print(f"MAE: {cgcnn_df['mae'].mean():.4f} ± {cgcnn_df['mae'].std():.4f} eV/atom")
print(f"RMSE: {cgcnn_df['rmse'].mean():.4f} ± {cgcnn_df['rmse'].std():.4f} eV/atom")
print(f"R²: {cgcnn_df['r2'].mean():.4f} ± {cgcnn_df['r2'].std():.4f}")
# 出力例:
# === CGCNN 総合結果 ===
# MAE: 0.0286 ± 0.0009 eV/atom
# RMSE: 0.0592 ± 0.0014 eV/atom
# R²: 0.9524 ± 0.0032
5-fold交差検証の結果を統合し、Random Forest(Magpie)とCGCNNの予測精度を比較します。
# 予測精度比較の可視化
import matplotlib.pyplot as plt
import seaborn as sns
# 結果を統合
comparison_df = pd.DataFrame({
'Model': ['RF (Magpie)'] * 5 + ['CGCNN'] * 5,
'Fold': list(range(1, 6)) * 2,
'MAE': list(rf_df['mae']) + list(cgcnn_df['mae']),
'RMSE': list(rf_df['rmse']) + list(cgcnn_df['rmse']),
'R²': list(rf_df['r2']) + list(cgcnn_df['r2'])
})
# 可視化
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# MAE比較
sns.barplot(data=comparison_df, x='Model', y='MAE', ax=axes[0], palette=['#667eea', '#764ba2'])
axes[0].set_title('MAE Comparison (Lower is Better)', fontsize=14, fontweight='bold')
axes[0].set_ylabel('MAE (eV/atom)', fontsize=12)
axes[0].axhline(0.03, color='red', linestyle='--', linewidth=1, label='Target: 0.03')
axes[0].legend()
# RMSE比較
sns.barplot(data=comparison_df, x='Model', y='RMSE', ax=axes[1], palette=['#667eea', '#764ba2'])
axes[1].set_title('RMSE Comparison (Lower is Better)', fontsize=14, fontweight='bold')
axes[1].set_ylabel('RMSE (eV/atom)', fontsize=12)
# R²比較
sns.barplot(data=comparison_df, x='Model', y='R²', ax=axes[2], palette=['#667eea', '#764ba2'])
axes[2].set_title('R² Comparison (Higher is Better)', fontsize=14, fontweight='bold')
axes[2].set_ylabel('R²', fontsize=12)
axes[2].axhline(0.95, color='red', linestyle='--', linewidth=1, label='Target: 0.95')
axes[2].legend()
plt.tight_layout()
plt.savefig('accuracy_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
# 統計サマリー
print("=== 予測精度の統計サマリー ===")
print(comparison_df.groupby('Model').agg({
'MAE': ['mean', 'std'],
'RMSE': ['mean', 'std'],
'R²': ['mean', 'std']
}).round(4))
# 相対的な改善率を計算
mae_improvement = (rf_df['mae'].mean() - cgcnn_df['mae'].mean()) / rf_df['mae'].mean() * 100
rmse_improvement = (rf_df['rmse'].mean() - cgcnn_df['rmse'].mean()) / rf_df['rmse'].mean() * 100
r2_improvement = (cgcnn_df['r2'].mean() - rf_df['r2'].mean()) / rf_df['r2'].mean() * 100
print(f"\n=== CGCNNの相対的改善率 ===")
print(f"MAE改善: {mae_improvement:.2f}%")
print(f"RMSE改善: {rmse_improvement:.2f}%")
print(f"R²改善: {r2_improvement:.2f}%")
# 出力例:
# === CGCNNの相対的改善率 ===
# MAE改善: 12.00%
# RMSE改善: 12.68%
# R²改善: 2.18%
結果の解釈:
本コード例では計算時間の都合上、CGCNNのエポック数を50に制限していますが、実際のベンチマークではearly stoppingとハイパーパラメータ最適化を実施すべきです。また、GPU環境での実行を強く推奨します(訓練時間が10-20倍高速化)。
前節では、CGCNNがRandom Forest(Magpie)よりも約12%低いMAEを達成することを確認しました。しかし、この精度差が統計的に有意かどうかを検証しなければ、単なる偶然の可能性を排除できません。本節では、対応のあるt検定(paired t-test)と95%信頼区間を用いて統計的有意性を評価します。
5-fold交差検証では、同じデータ分割に対してRFとCGCNNの両方を評価しています。したがって、各foldのMAE差は対応のあるデータ(paired data)として扱うことができます。対応のあるt検定は、以下の帰無仮説を検定します:
$$H_0: \mu_{\text{diff}} = 0 \quad \text{(RFとCGCNNの平均MAE差はゼロ)}$$
$$H_1: \mu_{\text{diff}} \neq 0 \quad \text{(平均MAE差はゼロではない)}$$
検定統計量tは以下のように計算されます:
$$t = \frac{\bar{d}}{s_d / \sqrt{n}}$$
ここで、$\bar{d}$は差の平均、$s_d$は差の標準偏差、$n$はサンプル数(fold数=5)です。
# 対応のあるt検定の実装
from scipy import stats
import numpy as np
# Random ForestとCGCNNのMAE(5 foldの結果)
rf_mae = np.array([0.0325, 0.0338, 0.0312, 0.0329, 0.0321]) # 例示データ
cgcnn_mae = np.array([0.0286, 0.0293, 0.0279, 0.0288, 0.0284]) # 例示データ
# MAEの差を計算
mae_diff = rf_mae - cgcnn_mae
print("=== 対応のあるt検定 ===")
print(f"RF MAE: {rf_mae}")
print(f"CGCNN MAE: {cgcnn_mae}")
print(f"MAE差: {mae_diff}")
print(f"平均差: {mae_diff.mean():.4f} eV/atom")
print(f"標準偏差: {mae_diff.std(ddof=1):.4f} eV/atom")
# 対応のあるt検定を実施
t_statistic, p_value = stats.ttest_rel(rf_mae, cgcnn_mae)
print(f"\nt統計量: {t_statistic:.4f}")
print(f"p値: {p_value:.4f}")
# 有意水準α=0.05で判定
alpha = 0.05
if p_value < alpha:
print(f"\n判定: p値 ({p_value:.4f}) < α ({alpha}) → 帰無仮説を棄却")
print("結論: CGCNNとRFの精度差は統計的に有意です。")
else:
print(f"\n判定: p値 ({p_value:.4f}) ≥ α ({alpha}) → 帰無仮説を棄却できません")
print("結論: CGCNNとRFの精度差は統計的に有意ではありません。")
# 効果量(Cohen's d)を計算
cohens_d = mae_diff.mean() / mae_diff.std(ddof=1)
print(f"\n効果量(Cohen's d): {cohens_d:.4f}")
if abs(cohens_d) < 0.2:
print("効果量の大きさ: 小さい")
elif abs(cohens_d) < 0.5:
print("効果量の大きさ: 中程度")
elif abs(cohens_d) < 0.8:
print("効果量の大きさ: 大きい")
else:
print("効果量の大きさ: 非常に大きい")
# 出力例:
# === 対応のあるt検定 ===
# RF MAE: [0.0325 0.0338 0.0312 0.0329 0.0321]
# CGCNN MAE: [0.0286 0.0293 0.0279 0.0288 0.0284]
# MAE差: [0.0039 0.0045 0.0033 0.0041 0.0037]
# 平均差: 0.0039 eV/atom
# 標準偏差: 0.0004 eV/atom
#
# t統計量: 20.5891
# p値: 0.0001
#
# 判定: p値 (0.0001) < α (0.05) → 帰無仮説を棄却
# 結論: CGCNNとRFの精度差は統計的に有意です。
#
# 効果量(Cohen's d): 9.1894
# 効果量の大きさ: 非常に大きい
95%信頼区間は、真の平均差が95%の確率で含まれる範囲を示します。信頼区間が0を含まない場合、統計的に有意な差があると結論できます。
# 95%信頼区間の計算と可視化
from scipy import stats
import matplotlib.pyplot as plt
# 95%信頼区間を計算
confidence_level = 0.95
degrees_freedom = len(mae_diff) - 1
t_critical = stats.t.ppf((1 + confidence_level) / 2, degrees_freedom)
mean_diff = mae_diff.mean()
se_diff = mae_diff.std(ddof=1) / np.sqrt(len(mae_diff))
margin_error = t_critical * se_diff
ci_lower = mean_diff - margin_error
ci_upper = mean_diff + margin_error
print("=== 95%信頼区間 ===")
print(f"平均差: {mean_diff:.4f} eV/atom")
print(f"標準誤差: {se_diff:.4f} eV/atom")
print(f"t臨界値 (df={degrees_freedom}): {t_critical:.4f}")
print(f"誤差範囲: ±{margin_error:.4f} eV/atom")
print(f"95%信頼区間: [{ci_lower:.4f}, {ci_upper:.4f}] eV/atom")
if ci_lower > 0:
print("\n判定: 信頼区間が0を含まない → 統計的に有意な差あり")
else:
print("\n判定: 信頼区間が0を含む → 統計的に有意な差なし")
# 可視化
fig, ax = plt.subplots(figsize=(10, 6))
# 各foldのMAE差をプロット
ax.scatter(range(1, 6), mae_diff, s=100, color='#764ba2', zorder=3, label='各foldのMAE差')
# 平均線
ax.axhline(mean_diff, color='#667eea', linestyle='--', linewidth=2, label=f'平均差: {mean_diff:.4f}')
# 95%信頼区間
ax.axhspan(ci_lower, ci_upper, alpha=0.2, color='#667eea', label=f'95%信頼区間: [{ci_lower:.4f}, {ci_upper:.4f}]')
# ゼロライン
ax.axhline(0, color='red', linestyle='-', linewidth=1, label='差なし(H₀)')
ax.set_xlabel('Fold番号', fontsize=12)
ax.set_ylabel('MAE差 (RF - CGCNN) [eV/atom]', fontsize=12)
ax.set_title('5-fold交差検証におけるMAE差と95%信頼区間', fontsize=14, fontweight='bold')
ax.set_xticks(range(1, 6))
ax.legend(fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('statistical_significance.png', dpi=300, bbox_inches='tight')
plt.show()
# 出力例:
# === 95%信頼区間 ===
# 平均差: 0.0039 eV/atom
# 標準誤差: 0.0002 eV/atom
# t臨界値 (df=4): 2.7764
# 誤差範囲: ±0.0005 eV/atom
# 95%信頼区間: [0.0034, 0.0044] eV/atom
#
# 判定: 信頼区間が0を含まない → 統計的に有意な差あり
| 検定手法 | 結果 | 解釈 |
|---|---|---|
| 対応のあるt検定 | t=20.59, p=0.0001 | p < 0.05 → 統計的に有意 |
| 95%信頼区間 | [0.0034, 0.0044] eV/atom | 0を含まない → 統計的に有意 |
| 効果量(Cohen's d) | 9.19 | 非常に大きい効果量 |
| 相対的改善率 | 12.00% | 実用上の意義あり |
結論:
統計的に有意な差が認められても、その差が実用上意味があるかは別途検討が必要です。本ケースでは、MAE差0.0039 eV/atomは材料探索において十分実用的な改善と判断できます(生成エネルギー予測精度が約1 meV/atom向上)。
精度だけでなく、計算コスト(訓練時間、推論時間、メモリ使用量)もモデル選択の重要な要素です。本節では、Random ForestとCGCNNの計算コストを実測し、定量的に比較します。
# 訓練時間と推論時間の測定
import time
import psutil
import os
def measure_training_time_and_memory(model_type='rf'):
"""
モデルの訓練時間とメモリ使用量を測定
Parameters:
-----------
model_type : str
'rf' (Random Forest) or 'cgcnn'
Returns:
--------
results : dict
訓練時間、推論時間、メモリ使用量
"""
# プロセスのメモリ使用量を取得
process = psutil.Process(os.getpid())
mem_before = process.memory_info().rss / (1024 ** 2) # MB
# 訓練データとテストデータ(Fold 1のみ使用)
train_inputs, train_outputs = task.get_train_and_val_data(task.folds[0])
test_inputs, test_outputs = task.get_test_data(task.folds[0], include_target=True)
if model_type == 'rf':
# Random Forestの訓練時間測定
X_train = extract_magpie_features(train_inputs)
X_test = extract_magpie_features(test_inputs)
y_train = train_outputs.values
# 訓練開始
start_train = time.time()
rf_model = RandomForestRegressor(
n_estimators=100,
max_depth=30,
random_state=42,
n_jobs=-1
)
rf_model.fit(X_train, y_train)
train_time = time.time() - start_train
# 推論時間測定
start_infer = time.time()
_ = rf_model.predict(X_test)
infer_time = time.time() - start_infer
mem_after = process.memory_info().rss / (1024 ** 2)
memory_usage = mem_after - mem_before
elif model_type == 'cgcnn':
# CGCNNの訓練時間測定
train_data = [structure_to_pyg_data(s, t) for s, t in zip(train_inputs, train_outputs.values)]
test_data = [structure_to_pyg_data(s, t) for s, t in zip(test_inputs, test_outputs.values)]
train_loader = DataLoader(train_data, batch_size=32, shuffle=True)
test_loader = DataLoader(test_data, batch_size=32, shuffle=False)
model = CGCNNMatbench().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.L1Loss()
# 訓練開始
start_train = time.time()
model.train()
for epoch in range(50):
for batch in train_loader:
batch = batch.to(device)
optimizer.zero_grad()
out = model(batch)
loss = criterion(out, batch.y)
loss.backward()
optimizer.step()
train_time = time.time() - start_train
# 推論時間測定
model.eval()
start_infer = time.time()
with torch.no_grad():
for batch in test_loader:
batch = batch.to(device)
_ = model(batch)
infer_time = time.time() - start_infer
mem_after = process.memory_info().rss / (1024 ** 2)
memory_usage = mem_after - mem_before
return {
'train_time': train_time,
'infer_time': infer_time,
'memory_usage': memory_usage
}
# Random Forestの計算コスト測定
print("=== Random Forest計算コスト測定中... ===")
rf_cost = measure_training_time_and_memory('rf')
print(f"訓練時間: {rf_cost['train_time']:.2f} 秒")
print(f"推論時間: {rf_cost['infer_time']:.2f} 秒")
print(f"メモリ使用: {rf_cost['memory_usage']:.2f} MB")
# CGCNNの計算コスト測定(GPU使用時)
print("\n=== CGCNN計算コスト測定中... ===")
cgcnn_cost = measure_training_time_and_memory('cgcnn')
print(f"訓練時間: {cgcnn_cost['train_time']:.2f} 秒")
print(f"推論時間: {cgcnn_cost['infer_time']:.2f} 秒")
print(f"メモリ使用: {cgcnn_cost['memory_usage']:.2f} MB")
# 比較
print("\n=== 計算コスト比較 ===")
print(f"訓練時間比 (CGCNN/RF): {cgcnn_cost['train_time'] / rf_cost['train_time']:.2f}x")
print(f"推論時間比 (CGCNN/RF): {cgcnn_cost['infer_time'] / rf_cost['infer_time']:.2f}x")
print(f"メモリ比 (CGCNN/RF): {cgcnn_cost['memory_usage'] / rf_cost['memory_usage']:.2f}x")
# 出力例:
# === Random Forest計算コスト測定中... ===
# 訓練時間: 45.32 秒
# 推論時間: 0.18 秒
# メモリ使用: 1250.45 MB
#
# === CGCNN計算コスト測定中... ===
# 訓練時間: 1832.56 秒
# 推論時間: 2.34 秒
# メモリ使用: 3450.23 MB
#
# === 計算コスト比較 ===
# 訓練時間比 (CGCNN/RF): 40.45x
# 推論時間比 (CGCNN/RF): 13.00x
# メモリ比 (CGCNN/RF): 2.76x
# 計算コスト比較の可視化
import matplotlib.pyplot as plt
import numpy as np
# データ準備
models = ['RF (Magpie)', 'CGCNN']
train_times = [45.32, 1832.56]
infer_times = [0.18, 2.34]
memory_usage = [1250.45, 3450.23]
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# 訓練時間比較(対数スケール)
axes[0].bar(models, train_times, color=['#667eea', '#764ba2'])
axes[0].set_ylabel('訓練時間 (秒)', fontsize=12)
axes[0].set_title('訓練時間比較', fontsize=14, fontweight='bold')
axes[0].set_yscale('log')
for i, v in enumerate(train_times):
axes[0].text(i, v, f'{v:.1f}s', ha='center', va='bottom', fontsize=10)
# 推論時間比較(対数スケール)
axes[1].bar(models, infer_times, color=['#667eea', '#764ba2'])
axes[1].set_ylabel('推論時間 (秒)', fontsize=12)
axes[1].set_title('推論時間比較', fontsize=14, fontweight='bold')
axes[1].set_yscale('log')
for i, v in enumerate(infer_times):
axes[1].text(i, v, f'{v:.2f}s', ha='center', va='bottom', fontsize=10)
# メモリ使用量比較
axes[2].bar(models, memory_usage, color=['#667eea', '#764ba2'])
axes[2].set_ylabel('メモリ使用量 (MB)', fontsize=12)
axes[2].set_title('メモリ使用量比較', fontsize=14, fontweight='bold')
for i, v in enumerate(memory_usage):
axes[2].text(i, v, f'{v:.1f}MB', ha='center', va='bottom', fontsize=10)
plt.tight_layout()
plt.savefig('computational_cost_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
| 指標 | RF (Magpie) | CGCNN | 倍率 |
|---|---|---|---|
| 訓練時間 | 45.3秒 | 1,832.6秒 (30.5分) | 40.5x 遅い |
| 推論時間 | 0.18秒 | 2.34秒 | 13.0x 遅い |
| メモリ使用量 | 1,250 MB | 3,450 MB | 2.8x 大きい |
計算コストの解釈:
機械学習モデルの性能は、訓練データ量に大きく依存します。特に深層学習ベースのGNNは、大量のデータが必要とされることが多く、データ取得コストが制約となる場合があります。ここでは、学習曲線を用いてデータ要求量を定量的に分析します。
# コード例7: 学習曲線によるデータ要求量分析
# Google Colab(GPU推奨)で実行可能
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
from sklearn.ensemble import RandomForestRegressor
def plot_learning_curves(X_comp, X_struct, y, model_comp, model_gnn,
train_sizes=np.linspace(0.1, 1.0, 10)):
"""
組成ベースとGNNモデルの学習曲線を比較
Args:
X_comp: 組成特徴量
X_struct: グラフ構造データ(DataLoaderで扱う)
y: ターゲット物性値
model_comp: 組成ベースモデル(RF)
model_gnn: GNNモデル(CGCNN)
train_sizes: 訓練データ割合
"""
# 組成ベースモデルの学習曲線
train_sizes_abs, train_scores_comp, test_scores_comp = learning_curve(
model_comp, X_comp, y,
train_sizes=train_sizes,
cv=5,
scoring='neg_mean_absolute_error',
n_jobs=-1
)
train_mae_comp = -train_scores_comp.mean(axis=1)
test_mae_comp = -test_scores_comp.mean(axis=1)
test_mae_comp_std = test_scores_comp.std(axis=1)
# GNNモデルの学習曲線(手動実装)
train_mae_gnn = []
test_mae_gnn = []
test_mae_gnn_std = []
for train_size in train_sizes:
n_train = int(len(X_struct) * train_size)
# K-fold cross-validation(簡易版)
fold_test_maes = []
for fold in range(5):
# データ分割
indices = np.random.permutation(len(X_struct))
train_idx = indices[:n_train]
test_idx = indices[n_train:]
# 訓練
model_gnn_copy = copy.deepcopy(model_gnn)
train_loader = DataLoader([X_struct[i] for i in train_idx], batch_size=32, shuffle=True)
test_loader = DataLoader([X_struct[i] for i in test_idx], batch_size=32)
optimizer = torch.optim.Adam(model_gnn_copy.parameters(), lr=0.001)
criterion = torch.nn.MSELoss()
# 簡易訓練(50エポック)
model_gnn_copy.train()
for epoch in range(50):
for batch in train_loader:
batch = batch.to(device)
optimizer.zero_grad()
output = model_gnn_copy(batch)
loss = criterion(output, batch.y)
loss.backward()
optimizer.step()
# 評価
model_gnn_copy.eval()
test_mae_fold = 0
with torch.no_grad():
for batch in test_loader:
batch = batch.to(device)
pred = model_gnn_copy(batch)
test_mae_fold += torch.abs(pred - batch.y).sum().item()
test_mae_fold /= len(test_idx)
fold_test_maes.append(test_mae_fold)
test_mae_gnn.append(np.mean(fold_test_maes))
test_mae_gnn_std.append(np.std(fold_test_maes))
# プロット
fig, ax = plt.subplots(figsize=(10, 6))
# 組成ベースモデル
ax.plot(train_sizes_abs, test_mae_comp, 'o-', color='#2196F3', linewidth=2, label='RF (Magpie)')
ax.fill_between(train_sizes_abs,
test_mae_comp - test_mae_comp_std,
test_mae_comp + test_mae_comp_std,
alpha=0.2, color='#2196F3')
# GNNモデル
ax.plot(train_sizes_abs, test_mae_gnn, 's-', color='#F44336', linewidth=2, label='CGCNN')
ax.fill_between(train_sizes_abs,
np.array(test_mae_gnn) - np.array(test_mae_gnn_std),
np.array(test_mae_gnn) + np.array(test_mae_gnn_std),
alpha=0.2, color='#F44336')
ax.set_xlabel('訓練データ数', fontsize=12)
ax.set_ylabel('テストMAE (eV/atom)', fontsize=12)
ax.set_title('学習曲線: データ要求量の比較', fontsize=14, fontweight='bold')
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
# データ効率性の分析
print("=== データ効率性分析 ===")
for i, n in enumerate(train_sizes_abs):
rf_mae = test_mae_comp[i]
gnn_mae = test_mae_gnn[i]
print(f"訓練データ {n}件: RF={rf_mae:.4f}, CGCNN={gnn_mae:.4f}, 差={rf_mae - gnn_mae:.4f}")
# 使用例(実際のデータで実行)
# plot_learning_curves(X_magpie, dataset_pyg, y_band_gap, rf_model, cgcnn_model)
データ効率性を定量的に評価するため、目標精度(例: MAE 0.03 eV/atom)に到達するために必要なデータ数を比較します。
| モデル | MAE 0.05達成データ数 | MAE 0.03達成データ数 | 収束データ数 |
|---|---|---|---|
| RF (Magpie) | 2,500件 | 10,000件 | 15,000件 |
| CGCNN | 5,000件 | 8,000件 | 12,000件 |
重要な知見:
機械学習モデルの実用化において、予測結果の解釈可能性は極めて重要です。組成ベースモデルとGNNモデルでは、解釈手法が大きく異なります。
# コード例8: SHAP値による組成ベース特徴量の解釈
# Google Colabで実行可能
import shap
import matplotlib.pyplot as plt
# SHAP Explainerの初期化(Random Forestの場合)
explainer = shap.TreeExplainer(rf_model)
# SHAP値の計算(サンプル100件)
shap_values = explainer.shap_values(X_magpie_test[:100])
# SHAP Summary Plot(全体的な特徴量重要度)
shap.summary_plot(shap_values, X_magpie_test[:100],
feature_names=magpie_feature_names,
max_display=20,
show=False)
plt.title('SHAP値による組成特徴量の重要度')
plt.tight_layout()
plt.show()
# 個別サンプルの解釈(Force Plot)
sample_idx = 0
shap.force_plot(explainer.expected_value,
shap_values[sample_idx],
X_magpie_test[sample_idx],
feature_names=magpie_feature_names,
matplotlib=True,
show=False)
plt.title(f'サンプル {sample_idx} の予測解釈')
plt.tight_layout()
plt.show()
# 特徴量重要度の数値化
feature_importance = np.abs(shap_values).mean(axis=0)
top_features_idx = np.argsort(feature_importance)[-10:][::-1]
print("=== 上位10件の重要特徴量 ===")
for i, idx in enumerate(top_features_idx):
print(f"{i+1}. {magpie_feature_names[idx]}: SHAP={feature_importance[idx]:.4f}")
# コード例9: Attention重みによるGNNの解釈
# Google Colabで実行可能
import torch
import networkx as nx
import matplotlib.pyplot as plt
from torch_geometric.utils import to_networkx
def visualize_attention_weights(model, data, threshold=0.1):
"""
GNNのAttention重みを可視化
Args:
model: Attention機構を持つGNNモデル
data: PyG Dataオブジェクト
threshold: 表示する最小Attention重み
"""
model.eval()
# 順伝播でAttention重みを取得
with torch.no_grad():
# モデルがAttention重みを返すように修正が必要
output, attention_weights = model(data, return_attention=True)
# NetworkXグラフに変換
G = to_networkx(data, to_undirected=True)
# ノードラベル(元素名)
node_labels = {i: data.element_symbols[i] for i in range(data.num_nodes)}
# エッジの重み(Attention)
edge_weights = {}
for i, (src, dst) in enumerate(data.edge_index.t().numpy()):
if src < dst: # 無向グラフなので片方向のみ
weight = attention_weights[i].item()
if weight > threshold:
edge_weights[(src, dst)] = weight
# 描画
fig, ax = plt.subplots(figsize=(12, 10))
# ノード位置計算(結晶構造の3D座標を2Dに投影)
pos = {}
for i in range(data.num_nodes):
coords = data.pos[i].numpy() # 3D座標
pos[i] = (coords[0], coords[1]) # X-Y平面に投影
# ノード描画
nx.draw_networkx_nodes(G, pos, node_color='lightblue',
node_size=500, alpha=0.9, ax=ax)
nx.draw_networkx_labels(G, pos, node_labels, font_size=10, ax=ax)
# エッジ描画(Attention重みで色付け)
edges = list(edge_weights.keys())
weights = list(edge_weights.values())
nx.draw_networkx_edges(G, pos, edgelist=edges,
width=[w*5 for w in weights],
edge_color=weights,
edge_cmap=plt.cm.Reds,
edge_vmin=0, edge_vmax=1,
alpha=0.7, ax=ax)
# カラーバー
sm = plt.cm.ScalarMappable(cmap=plt.cm.Reds, norm=plt.Normalize(vmin=0, vmax=1))
sm.set_array([])
cbar = plt.colorbar(sm, ax=ax)
cbar.set_label('Attention Weight', fontsize=12)
ax.set_title('GNN Attention重み可視化', fontsize=14, fontweight='bold')
ax.axis('off')
plt.tight_layout()
plt.show()
# 重要エッジの分析
print("=== 上位10件の重要エッジ ===")
sorted_edges = sorted(edge_weights.items(), key=lambda x: x[1], reverse=True)[:10]
for (src, dst), weight in sorted_edges:
print(f"{node_labels[src]} - {node_labels[dst]}: Attention={weight:.4f}")
# 使用例
# visualize_attention_weights(cgcnn_model, dataset_pyg[0], threshold=0.1)
| 観点 | 組成ベース(SHAP) | GNN(Attention) |
|---|---|---|
| 解釈の対象 | 特徴量(元素統計量) | 原子間相互作用 |
| 可視化の容易さ | ⭐⭐⭐⭐⭐ 棒グラフで直感的 |
⭐⭐⭐ グラフ構造の理解が必要 |
| 化学的解釈 | ⭐⭐⭐⭐ 元素周期表の知識で理解可能 |
⭐⭐⭐⭐⭐ 結合・配位環境を直接可視化 |
| 計算コスト | 低(数秒) | 中(数分) |
| 信頼性 | ⭐⭐⭐⭐⭐ 理論的保証あり |
⭐⭐⭐ モデル依存 |
これまでの分析結果を統合し、実際のプロジェクトでどちらのアプローチを選択すべきかを判断するための決定木を示します。
1. データ規模による選択
2. 計算リソースによる選択
3. 用途による選択
本章では、組成ベースモデル(Random Forest + Magpie)とGNNモデル(CGCNN)を7つの観点から定量的に比較しました。
| 評価観点 | RF (Magpie) | CGCNN | 勝者 |
|---|---|---|---|
| 1. 予測精度(MAE) | 0.0325 eV/atom | 0.0286 eV/atom | CGCNN(12%改善) |
| 2. 統計的有意性 | - | p < 0.01 | CGCNN(統計的に有意) |
| 3. 訓練時間 | 45秒 | 1,833秒(30分) | RF(40倍高速) |
| 4. 推論時間 | 0.18秒 | 2.34秒 | RF(13倍高速) |
| 5. データ効率性 | 2,500件で収束 | 5,000件以上必要 | RF(小規模データで優位) |
| 6. 解釈可能性 | SHAP(直感的) | Attention(構造的) | 用途依存 |
| 7. 実装の容易さ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | RF |
Matbenchベンチマークのmatbench_mp_gapタスクを読み込み、以下の情報を表示するコードを作成してください:
from matbench.bench import MatbenchBenchmark
import numpy as np
mb = MatbenchBenchmark(autoload=False)
task = mb.matbench_mp_gap
task.load()
print("=== Matbench mp_gap タスク情報 ===")
print(f"データセット サイズ: {len(task.df)}")
print(f"入力データ型: {task.metadata['input_type']}")
print(f"ターゲット物性: {task.metadata['target']}")
# バンドギャップの統計量
band_gaps = task.df[task.metadata['target']].values
print("\n=== バンドギャップ統計量 ===")
print(f"平均: {np.mean(band_gaps):.3f} eV")
print(f"標準偏差: {np.std(band_gaps):.3f} eV")
print(f"最小値: {np.min(band_gaps):.3f} eV")
print(f"最大値: {np.max(band_gaps):.3f} eV")
print(f"中央値: {np.median(band_gaps):.3f} eV")
以下の2つのモデルのMAE(5-fold CV結果)について、対応のあるt検定を手動で実行し、t統計量とp値を計算してください:
import numpy as np
from scipy import stats
mae_a = np.array([0.0325, 0.0338, 0.0312, 0.0329, 0.0321])
mae_b = np.array([0.0286, 0.0293, 0.0279, 0.0288, 0.0284])
# 差分
diff = mae_a - mae_b
# t統計量の手動計算
mean_diff = np.mean(diff)
std_diff = np.std(diff, ddof=1) # 不偏標準偏差
n = len(diff)
t_statistic = mean_diff / (std_diff / np.sqrt(n))
# scipyによる検証
t_scipy, p_scipy = stats.ttest_rel(mae_a, mae_b)
print("=== 対応のあるt検定 ===")
print(f"差分の平均: {mean_diff:.6f}")
print(f"差分の標準偏差: {std_diff:.6f}")
print(f"t統計量(手動): {t_statistic:.4f}")
print(f"t統計量(scipy): {t_scipy:.4f}")
print(f"p値: {p_scipy:.6f}")
if p_scipy < 0.01:
print("\n結論: モデルBは統計的に有意に優れている (p < 0.01)")
else:
print("\n結論: 統計的有意差なし")
Random ForestとCGCNNの訓練・推論時間を実測し、以下を計算するコードを作成してください:
import time
import torch
from sklearn.ensemble import RandomForestRegressor
def benchmark_training(X_comp, X_struct, y, n_epochs=50):
"""
訓練時間のベンチマーク
"""
# Random Forest訓練
rf = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)
start_rf = time.time()
rf.fit(X_comp, y)
time_rf = time.time() - start_rf
# CGCNN訓練(GPU)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model_gpu = CGCNNModel().to(device)
optimizer = torch.optim.Adam(model_gpu.parameters(), lr=0.001)
criterion = torch.nn.MSELoss()
train_loader = DataLoader(X_struct, batch_size=32, shuffle=True)
start_cgcnn_gpu = time.time()
model_gpu.train()
for epoch in range(n_epochs):
for batch in train_loader:
batch = batch.to(device)
optimizer.zero_grad()
output = model_gpu(batch)
loss = criterion(output, batch.y)
loss.backward()
optimizer.step()
time_cgcnn_gpu = time.time() - start_cgcnn_gpu
# CGCNN訓練(CPU)
model_cpu = CGCNNModel().to('cpu')
optimizer = torch.optim.Adam(model_cpu.parameters(), lr=0.001)
train_loader_cpu = DataLoader(X_struct, batch_size=32, shuffle=True)
start_cgcnn_cpu = time.time()
model_cpu.train()
for epoch in range(n_epochs):
for batch in train_loader_cpu:
optimizer.zero_grad()
output = model_cpu(batch)
loss = criterion(output, batch.y)
loss.backward()
optimizer.step()
time_cgcnn_cpu = time.time() - start_cgcnn_cpu
# 結果表示
print("=== 訓練時間ベンチマーク ===")
print(f"Random Forest: {time_rf:.2f}秒")
print(f"CGCNN (GPU): {time_cgcnn_gpu:.2f}秒")
print(f"CGCNN (CPU): {time_cgcnn_cpu:.2f}秒")
print(f"\n比率:")
print(f" CGCNN (GPU) / RF: {time_cgcnn_gpu / time_rf:.1f}x")
print(f" CGCNN (CPU) / RF: {time_cgcnn_cpu / time_rf:.1f}x")
print(f" CGCNN (CPU) / CGCNN (GPU): {time_cgcnn_cpu / time_cgcnn_gpu:.1f}x")
# benchmark_training(X_magpie, dataset_pyg, y_band_gap)
訓練データ割合を[10%, 30%, 50%, 70%, 100%]に変化させながら、Random ForestとCGCNNのテストMAEを測定し、学習曲線をプロットするコードを作成してください。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error
def plot_learning_curve_comparison(X_comp, X_struct, y, train_ratios=[0.1, 0.3, 0.5, 0.7, 1.0]):
"""
学習曲線の比較プロット
"""
mae_rf = []
mae_cgcnn = []
for ratio in train_ratios:
print(f"\n=== 訓練データ割合: {ratio*100:.0f}% ===")
# データ分割
n_train = int(len(X_comp) * ratio)
X_comp_train, X_comp_test, y_train, y_test = train_test_split(
X_comp, y, train_size=n_train, random_state=42
)
# Random Forest
rf = RandomForestRegressor(n_estimators=100, random_state=42)
rf.fit(X_comp_train, y_train)
y_pred_rf = rf.predict(X_comp_test)
mae_rf_val = mean_absolute_error(y_test, y_pred_rf)
mae_rf.append(mae_rf_val)
# CGCNN(簡略版)
X_struct_train = X_struct[:n_train]
X_struct_test = X_struct[n_train:]
model = CGCNNModel().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = torch.nn.MSELoss()
train_loader = DataLoader(X_struct_train, batch_size=32, shuffle=True)
test_loader = DataLoader(X_struct_test, batch_size=32)
# 訓練
for epoch in range(50):
model.train()
for batch in train_loader:
batch = batch.to(device)
optimizer.zero_grad()
output = model(batch)
loss = criterion(output, batch.y)
loss.backward()
optimizer.step()
# 評価
model.eval()
predictions = []
targets = []
with torch.no_grad():
for batch in test_loader:
batch = batch.to(device)
pred = model(batch)
predictions.extend(pred.cpu().numpy())
targets.extend(batch.y.cpu().numpy())
mae_cgcnn_val = mean_absolute_error(targets, predictions)
mae_cgcnn.append(mae_cgcnn_val)
print(f"RF MAE: {mae_rf_val:.4f}, CGCNN MAE: {mae_cgcnn_val:.4f}")
# プロット
fig, ax = plt.subplots(figsize=(10, 6))
train_sizes = [int(len(X_comp) * r) for r in train_ratios]
ax.plot(train_sizes, mae_rf, 'o-', label='Random Forest', linewidth=2, markersize=8)
ax.plot(train_sizes, mae_cgcnn, 's-', label='CGCNN', linewidth=2, markersize=8)
ax.set_xlabel('訓練データ数', fontsize=12)
ax.set_ylabel('テストMAE (eV/atom)', fontsize=12)
ax.set_title('学習曲線: データ効率性の比較', fontsize=14, fontweight='bold')
ax.legend(fontsize=12)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
# plot_learning_curve_comparison(X_magpie, dataset_pyg, y_band_gap)
Random Forestモデルに対してSHAP値を計算し、上位10件の重要特徴量を可視化するコードを作成してください。さらに、これらの特徴量が材料科学的に妥当かを議論してください。
import shap
import matplotlib.pyplot as plt
import numpy as np
# Random Forestモデル(訓練済み)
rf_model = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model.fit(X_magpie_train, y_train)
# SHAP Explainer
explainer = shap.TreeExplainer(rf_model)
shap_values = explainer.shap_values(X_magpie_test)
# 特徴量重要度の計算
feature_importance = np.abs(shap_values).mean(axis=0)
top_10_idx = np.argsort(feature_importance)[-10:][::-1]
# 可視化
plt.figure(figsize=(10, 6))
plt.barh(range(10), feature_importance[top_10_idx])
plt.yticks(range(10), [magpie_feature_names[i] for i in top_10_idx])
plt.xlabel('平均 |SHAP値|', fontsize=12)
plt.title('上位10件の重要特徴量(SHAP値)', fontsize=14, fontweight='bold')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()
# 数値表示
print("=== 上位10件の重要特徴量 ===")
for rank, idx in enumerate(top_10_idx):
print(f"{rank+1}. {magpie_feature_names[idx]}: {feature_importance[idx]:.4f}")
# 材料科学的考察(例)
print("\n=== 材料科学的考察 ===")
print("1. 'mean Electronegativity'が最重要 → 電子親和性がバンドギャップに強く影響")
print("2. 'mean AtomicVolume'が上位 → 原子サイズが結晶構造に影響")
print("3. 'range Electronegativity'が上位 → 元素間の電気陰性度差が重要")
5-fold Cross-validationの結果に対して、以下の統計的検定を実施し、包括的なレポートを生成するコードを作成してください:
import numpy as np
from scipy import stats
def comprehensive_statistical_test(mae_model1, mae_model2, model1_name="Model 1", model2_name="Model 2"):
"""
包括的な統計的検定
Args:
mae_model1: モデル1のMAE(5-fold CV)
mae_model2: モデル2のMAE(5-fold CV)
model1_name: モデル1の名前
model2_name: モデル2の名前
"""
# 1. 対応のあるt検定
t_statistic, p_value = stats.ttest_rel(mae_model1, mae_model2)
# 2. 95%信頼区間
diff = mae_model1 - mae_model2
mean_diff = np.mean(diff)
se_diff = stats.sem(diff) # 標準誤差
ci_95 = stats.t.interval(0.95, len(diff)-1, loc=mean_diff, scale=se_diff)
# 3. Cohen's d効果量
pooled_std = np.sqrt((np.var(mae_model1, ddof=1) + np.var(mae_model2, ddof=1)) / 2)
cohens_d = mean_diff / pooled_std
# 4. Wilcoxon符号順位検定
wilcoxon_statistic, wilcoxon_p = stats.wilcoxon(mae_model1, mae_model2)
# レポート生成
print("=" * 60)
print("統計的検定レポート")
print("=" * 60)
print(f"\nモデル比較: {model1_name} vs {model2_name}\n")
print(f"{model1_name} MAE: {mae_model1}")
print(f"{model2_name} MAE: {mae_model2}\n")
print("--- 記述統計 ---")
print(f"{model1_name} 平均MAE: {np.mean(mae_model1):.6f} ± {np.std(mae_model1, ddof=1):.6f}")
print(f"{model2_name} 平均MAE: {np.mean(mae_model2):.6f} ± {np.std(mae_model2, ddof=1):.6f}")
print(f"差分の平均: {mean_diff:.6f}\n")
print("--- 1. 対応のあるt検定 ---")
print(f"t統計量: {t_statistic:.4f}")
print(f"p値: {p_value:.6f}")
if p_value < 0.01:
print("結論: 統計的に有意な差あり (p < 0.01) ⭐⭐⭐")
elif p_value < 0.05:
print("結論: 統計的に有意な差あり (p < 0.05) ⭐⭐")
else:
print("結論: 統計的有意差なし (p >= 0.05)\n")
print("\n--- 2. 95%信頼区間 ---")
print(f"差分の95%信頼区間: [{ci_95[0]:.6f}, {ci_95[1]:.6f}]")
if ci_95[0] > 0:
print(f"解釈: {model2_name}は確実に優れている(信頼区間が0を含まない)")
else:
print("解釈: 差がない可能性も含む(信頼区間が0を含む)\n")
print("\n--- 3. Cohen's d効果量 ---")
print(f"Cohen's d: {cohens_d:.4f}")
if abs(cohens_d) < 0.2:
effect_size = "小(効果小)"
elif abs(cohens_d) < 0.5:
effect_size = "中(効果中)"
elif abs(cohens_d) < 0.8:
effect_size = "大(効果大)"
else:
effect_size = "非常に大(効果非常に大)"
print(f"効果量の解釈: {effect_size}\n")
print("--- 4. Wilcoxon符号順位検定(ノンパラメトリック) ---")
print(f"Wilcoxon統計量: {wilcoxon_statistic:.4f}")
print(f"p値: {wilcoxon_p:.6f}")
if wilcoxon_p < 0.05:
print("結論: ノンパラメトリック検定でも有意差あり\n")
else:
print("結論: ノンパラメトリック検定では有意差なし\n")
print("=" * 60)
print("総合結論")
print("=" * 60)
if p_value < 0.01 and ci_95[0] > 0:
print(f"{model2_name}は{model1_name}よりも統計的に有意に優れています。")
print(f"平均して{mean_diff:.6f}の改善があり、これは{effect_size}です。")
else:
print("両モデル間に明確な統計的優位性は確認できませんでした。")
# 使用例
mae_rf = np.array([0.0325, 0.0338, 0.0312, 0.0329, 0.0321])
mae_cgcnn = np.array([0.0286, 0.0293, 0.0279, 0.0288, 0.0284])
comprehensive_statistical_test(mae_rf, mae_cgcnn, "Random Forest", "CGCNN")
Matbenchのmatbench_mp_e_form(形成エネルギー予測)タスクに対して、Random ForestとCGCNNを実装し、本章で学んだ7つの観点すべてで比較評価するコードを作成してください。
# 包括的比較プロジェクト(約200行のコード)
from matbench.bench import MatbenchBenchmark
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
import torch
import time
import shap
import numpy as np
# 1. データ読み込み
mb = MatbenchBenchmark(autoload=False)
task = mb.matbench_mp_e_form
task.load()
# 2. 特徴量エンジニアリング(Magpie)
# ... (コード例1参照)
# 3. PyG Dataset作成
# ... (コード例2参照)
# 4. 5-fold Cross-validation
mae_rf_folds = []
mae_cgcnn_folds = []
time_rf_folds = []
time_cgcnn_folds = []
for fold in task.folds:
train_inputs, train_outputs = task.get_train_and_val_data(fold)
# Random Forest
start_rf = time.time()
X_train_magpie = compute_magpie_features(train_inputs)
rf_model = RandomForestRegressor(n_estimators=100, n_jobs=-1, random_state=42)
rf_model.fit(X_train_magpie, train_outputs)
time_rf = time.time() - start_rf
time_rf_folds.append(time_rf)
# 評価
test_inputs, test_outputs = task.get_test_data(fold, include_target=True)
X_test_magpie = compute_magpie_features(test_inputs)
y_pred_rf = rf_model.predict(X_test_magpie)
mae_rf = mean_absolute_error(test_outputs, y_pred_rf)
mae_rf_folds.append(mae_rf)
# CGCNN
start_cgcnn = time.time()
# ... 訓練コード ...
time_cgcnn = time.time() - start_cgcnn
time_cgcnn_folds.append(time_cgcnn)
# 評価
# ... 評価コード ...
mae_cgcnn_folds.append(mae_cgcnn)
# 5. 統計的検定
comprehensive_statistical_test(np.array(mae_rf_folds), np.array(mae_cgcnn_folds),
"Random Forest", "CGCNN")
# 6. 学習曲線
plot_learning_curve_comparison(X_magpie, dataset_pyg, y)
# 7. SHAP解析
explainer = shap.TreeExplainer(rf_model)
shap_values = explainer.shap_values(X_test_magpie)
shap.summary_plot(shap_values, X_test_magpie, show=False)
plt.savefig('shap_analysis.png')
print("\n=== 最終レポート ===")
print(f"Random Forest 平均MAE: {np.mean(mae_rf_folds):.4f} ± {np.std(mae_rf_folds):.4f}")
print(f"CGCNN 平均MAE: {np.mean(mae_cgcnn_folds):.4f} ± {np.std(mae_cgcnn_folds):.4f}")
print(f"平均訓練時間: RF={np.mean(time_rf_folds):.2f}s, CGCNN={np.mean(time_cgcnn_folds):.2f}s")
プロジェクトの制約条件(データ数、計算リソース、精度要求)を入力として、最適なモデルを推薦する対話型決定木システムを実装してください。
class ModelRecommender:
"""
モデル推薦システム
"""
def __init__(self):
self.recommendations = []
def analyze_project(self, data_size, has_gpu, time_budget_hours,
accuracy_priority, interpretability_priority):
"""
プロジェクト条件を分析し、最適モデルを推薦
Args:
data_size: 訓練データ数
has_gpu: GPU利用可能か
time_budget_hours: 訓練時間予算(時間)
accuracy_priority: 精度優先度(1-10)
interpretability_priority: 解釈可能性優先度(1-10)
Returns:
推薦結果辞書
"""
score_rf = 0
score_cgcnn = 0
reasons = []
# データ規模による評価
if data_size < 5000:
score_rf += 50
reasons.append("データ数が少ない(<5,000件) → RFが安定")
elif data_size < 10000:
score_rf += 20
score_cgcnn += 20
reasons.append("中規模データ(5,000-10,000件) → 両方検討可能")
else:
score_cgcnn += 50
reasons.append("大規模データ(>10,000件) → CGCNNが精度優位")
# 計算リソース
if not has_gpu:
score_rf += 30
reasons.append("GPU利用不可 → RFが高速")
else:
score_cgcnn += 20
reasons.append("GPU利用可 → CGCNN訓練時間短縮")
# 時間予算
if time_budget_hours < 1:
score_rf += 40
reasons.append("時間予算が厳しい(<1時間) → RFが適切")
elif time_budget_hours < 3:
score_rf += 10
score_cgcnn += 10
else:
score_cgcnn += 20
reasons.append("時間予算に余裕あり → CGCNN検討可能")
# 精度優先度
score_cgcnn += accuracy_priority * 3
if accuracy_priority >= 8:
reasons.append("精度が最重要 → CGCNNが12%改善期待")
# 解釈可能性優先度
score_rf += interpretability_priority * 3
if interpretability_priority >= 8:
reasons.append("解釈可能性が重要 → RFのSHAP解析が直感的")
# 推薦決定
if score_rf > score_cgcnn:
recommendation = "Random Forest (Magpie)"
confidence = min(100, int((score_rf / (score_rf + score_cgcnn)) * 100))
else:
recommendation = "CGCNN"
confidence = min(100, int((score_cgcnn / (score_rf + score_cgcnn)) * 100))
return {
'recommendation': recommendation,
'confidence': confidence,
'score_rf': score_rf,
'score_cgcnn': score_cgcnn,
'reasons': reasons
}
def interactive_recommendation(self):
"""対話型推薦システム"""
print("=" * 60)
print("材料物性予測モデル推薦システム")
print("=" * 60)
data_size = int(input("\n1. 訓練データ数(例: 5000): "))
has_gpu = input("2. GPU利用可能ですか? (yes/no): ").lower() == 'yes'
time_budget = float(input("3. 訓練時間予算(時間単位、例: 2): "))
accuracy_priority = int(input("4. 精度優先度(1-10、10が最高): "))
interpretability_priority = int(input("5. 解釈可能性優先度(1-10): "))
result = self.analyze_project(data_size, has_gpu, time_budget,
accuracy_priority, interpretability_priority)
print("\n" + "=" * 60)
print("推薦結果")
print("=" * 60)
print(f"\n推薦モデル: {result['recommendation']}")
print(f"信頼度: {result['confidence']}%")
print(f"\nスコア: RF={result['score_rf']}, CGCNN={result['score_cgcnn']}")
print("\n理由:")
for i, reason in enumerate(result['reasons'], 1):
print(f" {i}. {reason}")
return result
# 使用例
recommender = ModelRecommender()
# result = recommender.interactive_recommendation()