第4章では、組成ベース特徴量(Magpie)とGNN構造ベース特徴量(CGCNN)を定量的に比較し、各手法の長所と短所を明確にしました。本章では、これら2つのアプローチを統合するハイブリッドモデルを構築し、「両方の良いとこ取り」による性能向上を目指します。
ハイブリッドアプローチの核心は、異なる情報源から得られる特徴量を効果的に統合することです。組成ベースとGNN構造ベースの特徴量は、相補的な情報を持っています。
| 観点 | 組成ベース特徴量 | GNN構造ベース特徴量 | ハイブリッドの利点 |
|---|---|---|---|
| 情報粒度 | 元素レベル(平均・分散) | 原子レベル(位置・結合) | マルチスケール表現 |
| データ要求量 | 少ない(<10,000) | 多い(>50,000) | 中規模データで効率化 |
| 計算コスト | 低い(秒オーダー) | 高い(分オーダー) | 効率と精度のバランス |
| 解釈可能性 | 高い(元素特性) | 中(構造パターン) | 多角的な解釈 |
| 構造感度 | なし(同素体区別不可) | 高い(結晶構造依存) | 構造情報を考慮 |
特徴量の統合には、主に3つの戦略があります:
Early Fusion(特徴量レベル統合):組成ベース特徴量とGNN埋め込みを連結し、単一のモデルで学習
$$\mathbf{h}_{\text{hybrid}} = [\mathbf{h}_{\text{composition}}; \mathbf{h}_{\text{GNN}}]$$
Late Fusion(予測レベル統合):各モデルの予測を統合してアンサンブル予測を生成
$$\hat{y}_{\text{hybrid}} = \alpha \hat{y}_{\text{RF}} + (1-\alpha) \hat{y}_{\text{CGCNN}}$$
Intermediate Fusion(中間層統合):ニューラルネットワークの中間層で異なる表現を統合
最もシンプルなハイブリッド手法は、組成ベース特徴量(Magpie 145次元)とGNN埋め込み(例:128次元)を単純に連結することです。
# Early Fusion: 組成ベース + GNN埋め込みの連結
import torch
import torch.nn as nn
from torch_geometric.data import Data
from torch_geometric.nn import CGConv, global_mean_pool
import numpy as np
from matminer.featurizers.composition import ElementProperty
class HybridEarlyFusion(nn.Module):
def __init__(self, composition_dim=145, atom_fea_len=92, nbr_fea_len=41,
gnn_hidden=128, n_conv=3):
super(HybridEarlyFusion, self).__init__()
# GNN部分(CGCNN)
self.atom_embedding = nn.Linear(atom_fea_len, gnn_hidden)
self.conv_layers = nn.ModuleList([
CGConv(gnn_hidden, nbr_fea_len) for _ in range(n_conv)
])
self.bn_layers = nn.ModuleList([
nn.BatchNorm1d(gnn_hidden) for _ in range(n_conv)
])
# ハイブリッド統合層
# 組成特徴量(145次元)+ GNN埋め込み(128次元)= 273次元
hybrid_dim = composition_dim + gnn_hidden
self.fc1 = nn.Linear(hybrid_dim, 128)
self.fc2 = nn.Linear(128, 64)
self.fc3 = nn.Linear(64, 1)
self.activation = nn.Softplus()
self.dropout = nn.Dropout(0.2)
def forward(self, data, composition_features):
"""
Parameters:
-----------
data : torch_geometric.data.Data
グラフデータ(原子ノード、エッジ、エッジ特徴量)
composition_features : torch.Tensor, shape (batch_size, 145)
組成ベース特徴量(Magpie)
Returns:
--------
out : torch.Tensor, shape (batch_size,)
予測値
"""
x, edge_index, edge_attr, batch = data.x, data.edge_index, data.edge_attr, data.batch
# GNN埋め込みの計算
x = self.atom_embedding(x)
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)
# グローバルプーリング(グラフレベル表現)
gnn_embedding = global_mean_pool(x, batch) # shape: (batch_size, 128)
# Early Fusion: 組成特徴量とGNN埋め込みを連結
hybrid_features = torch.cat([composition_features, gnn_embedding], dim=1) # (batch_size, 273)
# 予測層
h = self.fc1(hybrid_features)
h = self.activation(h)
h = self.dropout(h)
h = self.fc2(h)
h = self.activation(h)
h = self.dropout(h)
out = self.fc3(h)
return out.squeeze()
# データ準備関数
def prepare_hybrid_data(structures, targets, featurizer):
"""
PyTorch Geometricデータと組成特徴量を準備
Parameters:
-----------
structures : list of Structure
結晶構造のリスト
targets : np.ndarray
目標値
featurizer : ElementProperty
Magpie特徴量抽出器
Returns:
--------
graph_data : list of Data
グラフデータのリスト
composition_features : torch.Tensor
組成特徴量
"""
graph_data = []
composition_features = []
for struct, target in zip(structures, targets):
# グラフデータ作成(Chapter 4のstructure_to_pyg_data関数を使用)
graph = structure_to_pyg_data(struct, target)
graph_data.append(graph)
# 組成特徴量抽出
comp = struct.composition
comp_feat = featurizer.featurize(comp)
composition_features.append(comp_feat)
composition_features = torch.tensor(composition_features, dtype=torch.float32)
return graph_data, composition_features
# Matbenchでの訓練例
from matbench.bench import MatbenchBenchmark
mb = MatbenchBenchmark(autoload=False)
task = mb.matbench_mp_e_form
task.load()
# Magpie特徴量抽出器
featurizer = ElementProperty.from_preset("magpie")
# 訓練データとテストデータ(Fold 0のみ)
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)
print("=== ハイブリッドデータを準備中... ===")
train_graphs, train_comp_feats = prepare_hybrid_data(train_inputs, train_outputs.values, featurizer)
test_graphs, test_comp_feats = prepare_hybrid_data(test_inputs, test_outputs.values, featurizer)
# カスタムDataLoaderの定義
from torch.utils.data import Dataset, DataLoader as TorchDataLoader
from torch_geometric.data import Batch
class HybridDataset(Dataset):
def __init__(self, graph_data, composition_features):
self.graph_data = graph_data
self.composition_features = composition_features
def __len__(self):
return len(self.graph_data)
def __getitem__(self, idx):
return self.graph_data[idx], self.composition_features[idx]
def hybrid_collate_fn(batch):
graphs, comp_feats = zip(*batch)
batched_graph = Batch.from_data_list(graphs)
batched_comp_feats = torch.stack(comp_feats)
return batched_graph, batched_comp_feats
train_dataset = HybridDataset(train_graphs, train_comp_feats)
test_dataset = HybridDataset(test_graphs, test_comp_feats)
train_loader = TorchDataLoader(train_dataset, batch_size=32, shuffle=True, collate_fn=hybrid_collate_fn)
test_loader = TorchDataLoader(test_dataset, batch_size=32, shuffle=False, collate_fn=hybrid_collate_fn)
# モデル訓練
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = HybridEarlyFusion().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.L1Loss()
print("\n=== ハイブリッドモデルを訓練中... ===")
model.train()
for epoch in range(50):
total_loss = 0
for batch_graph, batch_comp_feats in train_loader:
batch_graph = batch_graph.to(device)
batch_comp_feats = batch_comp_feats.to(device)
optimizer.zero_grad()
out = model(batch_graph, batch_comp_feats)
loss = criterion(out, batch_graph.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_graph, batch_comp_feats in test_loader:
batch_graph = batch_graph.to(device)
batch_comp_feats = batch_comp_feats.to(device)
out = model(batch_graph, batch_comp_feats)
y_true.extend(batch_graph.y.cpu().numpy())
y_pred.extend(out.cpu().numpy())
from sklearn.metrics import mean_absolute_error, r2_score
mae = mean_absolute_error(y_true, y_pred)
r2 = r2_score(y_true, y_pred)
print(f"\n=== Hybrid Early Fusion結果 ===")
print(f"MAE: {mae:.4f} eV/atom")
print(f"R²: {r2:.4f}")
# 出力例:
# === Hybrid Early Fusion結果 ===
# MAE: 0.0265 eV/atom # CGCNN単独(0.0286)より7.3%改善
# R²: 0.9614 # CGCNN単独(0.9524)より向上
性能比較(Matbench mp_e_form):
| 手法 | MAE (eV/atom) | R² | 相対改善率 |
|---|---|---|---|
| Random Forest(Magpie) | 0.0325 | 0.9321 | ベースライン |
| CGCNN | 0.0286 | 0.9524 | +12.0% |
| Hybrid Early Fusion | 0.0265 | 0.9614 | +18.5% |
Early Fusionの利点:
Early Fusionの課題:
Late Fusionは、Random ForestとCGCNNを独立に訓練し、予測段階で統合するアプローチです。各モデルの予測を重み付き平均することで、アンサンブル効果を得ます。
# Late Fusion: Random Forest + CGCNN アンサンブル
from sklearn.ensemble import RandomForestRegressor
import numpy as np
# Random Forestモデルの訓練(Chapter 4のコードを再利用)
print("=== Random Forestを訓練中... ===")
X_train_magpie = extract_magpie_features(train_inputs)
X_test_magpie = extract_magpie_features(test_inputs)
y_train = train_outputs.values
y_test = test_outputs.values
rf_model = RandomForestRegressor(n_estimators=100, max_depth=30, random_state=42, n_jobs=-1)
rf_model.fit(X_train_magpie, y_train)
# Random Forestの予測
rf_pred_train = rf_model.predict(X_train_magpie)
rf_pred_test = rf_model.predict(X_test_magpie)
# CGCNNモデルの訓練(Chapter 4のコードを再利用)
print("\n=== CGCNNを訓練中... ===")
train_data_cgcnn = [structure_to_pyg_data(s, t) for s, t in zip(train_inputs, y_train)]
test_data_cgcnn = [structure_to_pyg_data(s, t) for s, t in zip(test_inputs, y_test)]
train_loader_cgcnn = DataLoader(train_data_cgcnn, batch_size=32, shuffle=True)
test_loader_cgcnn = DataLoader(test_data_cgcnn, batch_size=32, shuffle=False)
cgcnn_model = CGCNNMatbench().to(device)
optimizer_cgcnn = torch.optim.Adam(cgcnn_model.parameters(), lr=0.001)
criterion = nn.L1Loss()
# CGCNN訓練(簡略版:30エポック)
cgcnn_model.train()
for epoch in range(30):
for batch in train_loader_cgcnn:
batch = batch.to(device)
optimizer_cgcnn.zero_grad()
out = cgcnn_model(batch)
loss = criterion(out, batch.y)
loss.backward()
optimizer_cgcnn.step()
# CGCNNの予測
cgcnn_model.eval()
cgcnn_pred_train, cgcnn_pred_test = [], []
with torch.no_grad():
for batch in train_loader_cgcnn:
batch = batch.to(device)
out = cgcnn_model(batch)
cgcnn_pred_train.extend(out.cpu().numpy())
for batch in test_loader_cgcnn:
batch = batch.to(device)
out = cgcnn_model(batch)
cgcnn_pred_test.extend(out.cpu().numpy())
cgcnn_pred_train = np.array(cgcnn_pred_train)
cgcnn_pred_test = np.array(cgcnn_pred_test)
# 最適な重みαを訓練データで探索
print("\n=== 最適アンサンブル重みを探索中... ===")
alphas = np.linspace(0, 1, 21) # 0.0, 0.05, 0.10, ..., 1.0
best_alpha = 0
best_mae = float('inf')
for alpha in alphas:
ensemble_pred_train = alpha * rf_pred_train + (1 - alpha) * cgcnn_pred_train
mae_train = mean_absolute_error(y_train, ensemble_pred_train)
if mae_train < best_mae:
best_mae = mae_train
best_alpha = alpha
print(f"最適重み α = {best_alpha:.2f}")
print(f"訓練MAE = {best_mae:.4f} eV/atom")
# テストデータでアンサンブル予測
ensemble_pred_test = best_alpha * rf_pred_test + (1 - best_alpha) * cgcnn_pred_test
mae_test = mean_absolute_error(y_test, ensemble_pred_test)
r2_test = r2_score(y_test, ensemble_pred_test)
print(f"\n=== Late Fusion(アンサンブル)結果 ===")
print(f"RF重み: {best_alpha:.2f}, CGCNN重み: {1-best_alpha:.2f}")
print(f"MAE: {mae_test:.4f} eV/atom")
print(f"R²: {r2_test:.4f}")
# 個別モデルとの比較
rf_mae = mean_absolute_error(y_test, rf_pred_test)
cgcnn_mae = mean_absolute_error(y_test, cgcnn_pred_test)
print(f"\n=== 個別モデル性能 ===")
print(f"RF単独: MAE = {rf_mae:.4f} eV/atom")
print(f"CGCNN単独: MAE = {cgcnn_mae:.4f} eV/atom")
print(f"Late Fusion: MAE = {mae_test:.4f} eV/atom")
print(f"改善率(RF比): {(rf_mae - mae_test) / rf_mae * 100:.2f}%")
print(f"改善率(CGCNN比): {(cgcnn_mae - mae_test) / cgcnn_mae * 100:.2f}%")
# 出力例:
# 最適重み α = 0.25
# === Late Fusion(アンサンブル)結果 ===
# RF重み: 0.25, CGCNN重み: 0.75
# MAE: 0.0272 eV/atom
# R²: 0.9582
#
# === 個別モデル性能 ===
# RF単独: MAE = 0.0325 eV/atom
# CGCNN単独: MAE = 0.0286 eV/atom
# Late Fusion: MAE = 0.0272 eV/atom
# 改善率(RF比): 16.31%
# 改善率(CGCNN比): 4.90%
最適重みの解釈:
Late Fusionの利点:
Late Fusionの課題:
ALIGNN(Atomistic Line Graph Neural Network)は、原子グラフ(atom graph)と線グラフ(line graph)の両方を用いる最先端のハイブリッドGNNです。線グラフでは、原子間の結合(bond)をノードとして扱い、結合角度情報を明示的にモデル化します。
原子グラフ(Atom Graph):
$$G_{\text{atom}} = (V_{\text{atom}}, E_{\text{atom}})$$
ノード:原子、エッジ:原子間結合
線グラフ(Line Graph):
$$G_{\text{line}} = (V_{\text{line}}, E_{\text{line}})$$
ノード:結合、エッジ:結合角度(同じ原子を共有する2つの結合)
# ALIGNN簡易実装(教育目的)
import torch
import torch.nn as nn
from torch_geometric.nn import MessagePassing, global_mean_pool
from torch_geometric.data import Data
class ALIGNNConv(MessagePassing):
"""
ALIGNN畳み込み層(簡略版)
"""
def __init__(self, node_dim, edge_dim):
super(ALIGNNConv, self).__init__(aggr='add')
self.node_dim = node_dim
self.edge_dim = edge_dim
# メッセージ計算用MLP
self.message_mlp = nn.Sequential(
nn.Linear(2 * node_dim + edge_dim, node_dim),
nn.Softplus(),
nn.Linear(node_dim, node_dim)
)
# ノード更新用MLP
self.update_mlp = nn.Sequential(
nn.Linear(2 * node_dim, node_dim),
nn.Softplus(),
nn.Linear(node_dim, node_dim)
)
def forward(self, x, edge_index, edge_attr):
"""
Parameters:
-----------
x : torch.Tensor, shape (num_nodes, node_dim)
ノード特徴量
edge_index : torch.Tensor, shape (2, num_edges)
エッジインデックス
edge_attr : torch.Tensor, shape (num_edges, edge_dim)
エッジ特徴量
Returns:
--------
out : torch.Tensor, shape (num_nodes, node_dim)
更新されたノード特徴量
"""
return self.propagate(edge_index, x=x, edge_attr=edge_attr)
def message(self, x_i, x_j, edge_attr):
# メッセージ: [送信元ノード、受信先ノード、エッジ特徴量]
msg_input = torch.cat([x_i, x_j, edge_attr], dim=-1)
return self.message_mlp(msg_input)
def update(self, aggr_out, x):
# ノード更新: [元のノード特徴量、集約されたメッセージ]
update_input = torch.cat([x, aggr_out], dim=-1)
return self.update_mlp(update_input)
class ALIGNNSimple(nn.Module):
"""
ALIGNN簡易実装(原子グラフのみ、線グラフは省略)
"""
def __init__(self, atom_fea_len=92, nbr_fea_len=41, hidden_dim=128, n_conv=3):
super(ALIGNNSimple, self).__init__()
# 原子埋め込み
self.atom_embedding = nn.Linear(atom_fea_len, hidden_dim)
# ALIGNN畳み込み層
self.conv_layers = nn.ModuleList([
ALIGNNConv(hidden_dim, nbr_fea_len) for _ in range(n_conv)
])
self.bn_layers = nn.ModuleList([
nn.BatchNorm1d(hidden_dim) for _ in range(n_conv)
])
# 予測層
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
# 原子埋め込み
x = self.atom_embedding(x)
# ALIGNN畳み込み
for conv, bn in zip(self.conv_layers, self.bn_layers):
x_new = conv(x, edge_index, edge_attr)
x = bn(x_new) + x # 残差接続
x = self.activation(x)
# グローバルプーリング
x = global_mean_pool(x, batch)
# 予測
x = self.fc1(x)
x = self.activation(x)
x = self.fc2(x)
return x.squeeze()
# ALIGNN訓練(Matbench mp_e_form)
print("=== ALIGNN簡易版を訓練中... ===")
alignn_model = ALIGNNSimple().to(device)
optimizer_alignn = torch.optim.Adam(alignn_model.parameters(), lr=0.001)
criterion = nn.L1Loss()
# 訓練ループ
alignn_model.train()
for epoch in range(50):
total_loss = 0
for batch in train_loader_cgcnn:
batch = batch.to(device)
optimizer_alignn.zero_grad()
out = alignn_model(batch)
loss = criterion(out, batch.y)
loss.backward()
optimizer_alignn.step()
total_loss += loss.item()
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}/50, Loss: {total_loss/len(train_loader_cgcnn):.4f}")
# テスト評価
alignn_model.eval()
y_true_alignn, y_pred_alignn = [], []
with torch.no_grad():
for batch in test_loader_cgcnn:
batch = batch.to(device)
out = alignn_model(batch)
y_true_alignn.extend(batch.y.cpu().numpy())
y_pred_alignn.extend(out.cpu().numpy())
mae_alignn = mean_absolute_error(y_true_alignn, y_pred_alignn)
r2_alignn = r2_score(y_true_alignn, y_pred_alignn)
print(f"\n=== ALIGNN簡易版結果 ===")
print(f"MAE: {mae_alignn:.4f} eV/atom")
print(f"R²: {r2_alignn:.4f}")
# 出力例:
# === ALIGNN簡易版結果 ===
# MAE: 0.0278 eV/atom
# R²: 0.9548
# 注:完全なALIGNNは線グラフも使用し、さらに高性能(MAE ~0.025 eV/atom)
本コード例は教育目的の簡易実装です。完全なALIGNN実装は線グラフ(Line Graph)も使用し、結合角度情報を明示的にモデル化します。公式実装(NIST ALIGNN GitHub)ではMAE ~0.025 eV/atomの性能を達成しています。
| 手法 | MAE (eV/atom) | 特徴 |
|---|---|---|
| CGCNN | 0.0286 | 原子グラフのみ |
| ALIGNN簡易版 | 0.0278 | 残差接続 + 改良メッセージパッシング |
| ALIGNN完全版 | 0.0250 | 原子グラフ + 線グラフ + 結合角度 |
ALIGNNの優位性:
ALIGNNの課題:
MEGNet(Materials Graph Network)は、複数の材料物性を同時に予測するマルチタスク学習フレームワークです。異なる物性間の相関を利用し、データ効率とモデル汎化性能を向上させます。
マルチタスク学習では、複数のタスク$T_1, T_2, \ldots, T_K$を同時に学習します:
$$\mathcal{L}_{\text{multi}} = \sum_{k=1}^{K} \lambda_k \mathcal{L}_k$$
ここで、$\lambda_k$はタスク$k$の重み、$\mathcal{L}_k$はタスク$k$の損失関数です。
マルチタスク学習の利点:
# MEGNet風マルチタスクGNNの実装
import torch
import torch.nn as nn
from torch_geometric.nn import GATConv, global_mean_pool
class MEGNetMultiTask(nn.Module):
"""
MEGNet風マルチタスクGNN
生成エネルギーとバンドギャップを同時予測
"""
def __init__(self, atom_fea_len=92, nbr_fea_len=41, hidden_dim=128, n_conv=3, n_tasks=2):
super(MEGNetMultiTask, self).__init__()
# 共有GNN層(全タスクで共通)
self.atom_embedding = nn.Linear(atom_fea_len, hidden_dim)
self.conv_layers = nn.ModuleList([
GATConv(hidden_dim, hidden_dim, heads=4, concat=False, edge_dim=nbr_fea_len)
for _ in range(n_conv)
])
self.bn_layers = nn.ModuleList([
nn.BatchNorm1d(hidden_dim) for _ in range(n_conv)
])
# タスク固有の予測ヘッド
self.task_heads = nn.ModuleList([
nn.Sequential(
nn.Linear(hidden_dim, 64),
nn.Softplus(),
nn.Linear(64, 1)
) for _ in range(n_tasks)
])
self.activation = nn.Softplus()
def forward(self, data, task_idx=None):
"""
Parameters:
-----------
data : torch_geometric.data.Data
グラフデータ
task_idx : int or None
予測するタスクのインデックス(Noneの場合は全タスク予測)
Returns:
--------
out : torch.Tensor or list of torch.Tensor
タスク予測値
"""
x, edge_index, edge_attr, batch = data.x, data.edge_index, data.edge_attr, data.batch
# 共有GNN埋め込み
x = self.atom_embedding(x)
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)
# グローバルプーリング
graph_embedding = global_mean_pool(x, batch)
# タスク固有の予測
if task_idx is not None:
# 単一タスク予測
return self.task_heads[task_idx](graph_embedding).squeeze()
else:
# 全タスク予測
return [head(graph_embedding).squeeze() for head in self.task_heads]
# マルチタスクデータの準備(生成エネルギー + バンドギャップ)
from matbench.bench import MatbenchBenchmark
mb = MatbenchBenchmark(autoload=False)
# タスク1: 生成エネルギー(mp_e_form)
task1 = mb.matbench_mp_e_form
task1.load()
# タスク2: バンドギャップ(mp_gap)
task2 = mb.matbench_mp_gap
task2.load()
# 共通の構造を持つデータを抽出(実装簡略化のため、ここでは同じ構造IDを仮定)
# 実際にはMaterials Project IDで結合する
print("=== マルチタスクデータを準備中... ===")
# Fold 0のみ使用
train_inputs_1, train_outputs_1 = task1.get_train_and_val_data(task1.folds[0])
test_inputs_1, test_outputs_1 = task1.get_test_data(task1.folds[0], include_target=True)
train_inputs_2, train_outputs_2 = task2.get_train_and_val_data(task2.folds[0])
test_inputs_2, test_outputs_2 = task2.get_test_data(task2.folds[0], include_target=True)
# 簡略化のため、最初の10,000サンプルのみ使用
n_samples = 10000
train_inputs_1 = train_inputs_1[:n_samples]
train_outputs_1 = train_outputs_1.values[:n_samples]
train_inputs_2 = train_inputs_2[:n_samples]
train_outputs_2 = train_outputs_2.values[:n_samples]
# グラフデータ構築
def create_multitask_data(structures, targets_task1, targets_task2):
"""
マルチタスクグラフデータを作成
"""
data_list = []
for struct, t1, t2 in zip(structures, targets_task1, targets_task2):
graph = structure_to_pyg_data(struct, t1)
graph.y_task1 = torch.tensor([t1], dtype=torch.float)
graph.y_task2 = torch.tensor([t2], dtype=torch.float)
data_list.append(graph)
return data_list
train_data_multi = create_multitask_data(train_inputs_1, train_outputs_1, train_outputs_2)
test_data_multi = create_multitask_data(test_inputs_1[:1000],
test_outputs_1.values[:1000],
test_outputs_2.values[:1000])
train_loader_multi = DataLoader(train_data_multi, batch_size=32, shuffle=True)
test_loader_multi = DataLoader(test_data_multi, batch_size=32, shuffle=False)
# MEGNetマルチタスクモデルの訓練
print("\n=== MEGNetマルチタスクモデルを訓練中... ===")
megnet_model = MEGNetMultiTask(n_tasks=2).to(device)
optimizer_megnet = torch.optim.Adam(megnet_model.parameters(), lr=0.001)
# タスク重み(損失のバランス調整)
lambda_task1 = 1.0 # 生成エネルギー
lambda_task2 = 0.5 # バンドギャップ(スケール調整)
megnet_model.train()
for epoch in range(30):
total_loss = 0
for batch in train_loader_multi:
batch = batch.to(device)
optimizer_megnet.zero_grad()
# 2タスクの予測
pred_task1, pred_task2 = megnet_model(batch)
# マルチタスク損失
loss_task1 = nn.L1Loss()(pred_task1, batch.y_task1.squeeze())
loss_task2 = nn.L1Loss()(pred_task2, batch.y_task2.squeeze())
loss = lambda_task1 * loss_task1 + lambda_task2 * loss_task2
loss.backward()
optimizer_megnet.step()
total_loss += loss.item()
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}/30, Total Loss: {total_loss/len(train_loader_multi):.4f}")
# テスト評価(各タスク)
megnet_model.eval()
y_true_task1, y_pred_task1 = [], []
y_true_task2, y_pred_task2 = [], []
with torch.no_grad():
for batch in test_loader_multi:
batch = batch.to(device)
pred_task1, pred_task2 = megnet_model(batch)
y_true_task1.extend(batch.y_task1.squeeze().cpu().numpy())
y_pred_task1.extend(pred_task1.cpu().numpy())
y_true_task2.extend(batch.y_task2.squeeze().cpu().numpy())
y_pred_task2.extend(pred_task2.cpu().numpy())
mae_task1 = mean_absolute_error(y_true_task1, y_pred_task1)
mae_task2 = mean_absolute_error(y_true_task2, y_pred_task2)
print(f"\n=== MEGNetマルチタスク結果 ===")
print(f"Task 1(生成エネルギー): MAE = {mae_task1:.4f} eV/atom")
print(f"Task 2(バンドギャップ): MAE = {mae_task2:.4f} eV")
# 単一タスクモデルとの比較(参考)
print(f"\n単一タスクCGCNN比較:")
print(f"Task 1: マルチタスク {mae_task1:.4f} vs 単一タスク ~0.0286 eV/atom")
print(f"Task 2: マルチタスク {mae_task2:.4f} vs 単一タスク ~0.180 eV")
# 出力例:
# === MEGNetマルチタスク結果 ===
# Task 1(生成エネルギー): MAE = 0.0292 eV/atom
# Task 2(バンドギャップ): MAE = 0.185 eV
#
# 単一タスクCGCNN比較:
# Task 1: マルチタスク 0.0292 vs 単一タスク ~0.0286 eV/atom(わずかに劣化)
# Task 2: マルチタスク 0.185 vs 単一タスク ~0.180 eV(同程度)
マルチタスク学習のメリット:
マルチタスク学習の課題:
本章で実装した全ハイブリッド手法の性能を統合比較します。
# ハイブリッド手法の統合比較
import matplotlib.pyplot as plt
import pandas as pd
# 性能データ(Matbench mp_e_form)
results = {
'Model': [
'Random Forest (Magpie)',
'CGCNN',
'Hybrid Early Fusion',
'Hybrid Late Fusion',
'ALIGNN (Simple)',
'ALIGNN (Full)',
'MEGNet Multi-Task'
],
'MAE (eV/atom)': [0.0325, 0.0286, 0.0265, 0.0272, 0.0278, 0.0250, 0.0292],
'R²': [0.9321, 0.9524, 0.9614, 0.9582, 0.9548, 0.9680, 0.9510],
'Training Time (min)': [0.75, 30.5, 32.0, 31.25, 35.0, 45.0, 50.0],
'Category': ['Composition', 'GNN', 'Hybrid', 'Hybrid', 'Hybrid', 'Hybrid', 'Multi-Task']
}
df = pd.DataFrame(results)
# 可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# MAE比較
colors = {'Composition': '#4caf50', 'GNN': '#667eea', 'Hybrid': '#764ba2', 'Multi-Task': '#ff9800'}
ax1 = axes[0]
bars = ax1.barh(df['Model'], df['MAE (eV/atom)'],
color=[colors[cat] for cat in df['Category']])
ax1.set_xlabel('MAE (eV/atom)', fontsize=12)
ax1.set_title('予測精度比較(Lower is Better)', fontsize=14, fontweight='bold')
ax1.invert_yaxis()
# ベースラインとの比較線
ax1.axvline(0.0325, color='red', linestyle='--', linewidth=1, alpha=0.7, label='RF Baseline')
ax1.legend()
# 訓練時間 vs MAE
ax2 = axes[1]
for idx, row in df.iterrows():
ax2.scatter(row['Training Time (min)'], row['MAE (eV/atom)'],
s=200, color=colors[row['Category']], alpha=0.7, edgecolors='black', linewidth=1.5)
ax2.text(row['Training Time (min)'], row['MAE (eV/atom)'],
row['Model'], fontsize=8, ha='right', va='bottom')
ax2.set_xlabel('訓練時間 (分)', fontsize=12)
ax2.set_ylabel('MAE (eV/atom)', fontsize=12)
ax2.set_title('訓練時間 vs 精度のトレードオフ', fontsize=14, fontweight='bold')
ax2.invert_yaxis()
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.savefig('hybrid_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
# 統計サマリー
print("=== ハイブリッド手法統合比較 ===")
print(df.to_string(index=False))
# 最良モデルの識別
best_mae_idx = df['MAE (eV/atom)'].idxmin()
best_efficiency_idx = (df['MAE (eV/atom)'] / df['Training Time (min)']).idxmin()
print(f"\n最高精度モデル: {df.loc[best_mae_idx, 'Model']} (MAE = {df.loc[best_mae_idx, 'MAE (eV/atom)']:.4f})")
print(f"最高効率モデル: {df.loc[best_efficiency_idx, 'Model']} (MAE/Time = {df.loc[best_efficiency_idx, 'MAE (eV/atom)'] / df.loc[best_efficiency_idx, 'Training Time (min)']:.6f})")
# 出力例:
# === ハイブリッド手法統合比較 ===
# Model MAE (eV/atom) R² Training Time (min) Category
# Random Forest (Magpie) 0.0325 0.9321 0.75 Composition
# CGCNN 0.0286 0.9524 30.50 GNN
# Hybrid Early Fusion 0.0265 0.9614 32.00 Hybrid
# Hybrid Late Fusion 0.0272 0.9582 31.25 Hybrid
# ALIGNN (Simple) 0.0278 0.9548 35.00 Hybrid
# ALIGNN (Full) 0.0250 0.9680 45.00 Hybrid
# MEGNet Multi-Task 0.0292 0.9510 50.00 Multi-Task
#
# 最高精度モデル: ALIGNN (Full) (MAE = 0.0250)
# 最高効率モデル: Random Forest (Magpie) (MAE/Time = 0.043333)
| 手法 | 精度 | 効率 | 実装難易度 | 推奨シナリオ |
|---|---|---|---|---|
| Hybrid Early Fusion | ⭐⭐⭐⭐ | ⭐⭐⭐ | 低 | 中規模データ、実装容易性重視 |
| Hybrid Late Fusion | ⭐⭐⭐ | ⭐⭐ | 低 | 既存モデルの統合、安定性重視 |
| ALIGNN (Full) | ⭐⭐⭐⭐⭐ | ⭐⭐ | 高 | 最高精度が必須、計算リソース十分 |
| MEGNet Multi-Task | ⭐⭐⭐ | ⭐⭐⭐ | 中 | 複数物性予測、データ効率重視 |
本章では、組成ベースとGNN構造ベース特徴量を統合するハイブリッドアプローチを体系的に学びました。
問題:Magpie特徴量(145次元)とGNN埋め込み(256次元)をEarly Fusionで統合する場合、統合後の特徴量次元はいくつになるか?また、過学習リスクを低減するための手法を2つ挙げよ。
解答:
統合後の特徴量次元: 145 + 256 = 401次元
過学習リスク低減手法:
問題:Random Forest(MAE 0.035 eV/atom)とCGCNN(MAE 0.028 eV/atom)のLate Fusionで、最適重みα=0.20が得られた。この重みの意味を解釈し、なぜCGCNNの重みが高いのか説明せよ。
解答:
重みの意味:
$$\hat{y}_{\text{ensemble}} = 0.20 \times \hat{y}_{\text{RF}} + 0.80 \times \hat{y}_{\text{CGCNN}}$$
CGCNNの予測を80%、RFの予測を20%で統合。
CGCNN重みが高い理由:
問題:原子グラフと線グラフの違いを説明し、線グラフが結合角度情報をどのように表現するか具体例を示せ。
解答:
原子グラフ(Atom Graph):
線グラフ(Line Graph):
結合角度情報の表現:
線グラフのエッジ特徴量として、2つの結合が作る角度θを以下のようにエンコード:
angle_feature = torch.cos(theta) # cosθを特徴量に使用
# 例: H-O-H角度104.5° → cos(104.5°) ≈ -0.25
これにより、ALIGNNは「直線的な結合(θ=180°)」と「屈曲した結合(θ<120°)」を明示的に区別できます。
問題:生成エネルギー(スケール:-5~5 eV/atom)とバンドギャップ(スケール:0~10 eV)を同時予測するマルチタスクGNNにおいて、適切なタスク重みλ₁、λ₂を設計せよ。単純に$\lambda_1 = \lambda_2 = 1.0$とした場合の問題点も説明すること。
解答:
問題点($\lambda_1 = \lambda_2 = 1.0$):
生成エネルギーとバンドギャップのスケールが異なるため、損失の大きさが不均衡になります:
# 生成エネルギーの典型的なMAE: 0.03 eV/atom
loss_task1 = 0.03
# バンドギャップの典型的なMAE: 0.18 eV
loss_task2 = 0.18
# 総損失(λ₁ = λ₂ = 1.0)
total_loss = 1.0 * 0.03 + 1.0 * 0.18 = 0.21
# → バンドギャップの損失が6倍大きい → 生成エネルギーの学習が不十分
適切なタスク重みの設計:
各タスクの損失を同程度にするため、スケールの逆数で重み付け:
# タスク重みの設定
lambda_1 = 1.0 # 生成エネルギー(基準)
lambda_2 = 0.03 / 0.18 ≈ 0.17 # バンドギャップ(スケール調整)
# または、標準偏差の逆数を使用
std_task1 = 1.5 # 生成エネルギーの標準偏差
std_task2 = 2.0 # バンドギャップの標準偏差
lambda_1 = 1 / std_task1 ≈ 0.67
lambda_2 = 1 / std_task2 = 0.50
# 正規化して合計を1にする
lambda_1 = 0.67 / (0.67 + 0.50) ≈ 0.57
lambda_2 = 0.50 / (0.67 + 0.50) ≈ 0.43
問題:以下の3つのシナリオに対して、最適なハイブリッド手法を選択し、その理由を述べよ。
シナリオA: データ30,000サンプル、GPU利用可能、精度優先、実装期限2週間
シナリオB: データ100,000サンプル、GPU複数台、最高精度が必須、計算時間制約なし
シナリオC: 既存のRFモデルとCGCNNモデルがあり、統合したい、リスク回避重視
解答:
シナリオA → Hybrid Early Fusion
シナリオB → ALIGNN (Full)
シナリオC → Hybrid Late Fusion
問題:Hybrid Early FusionモデルにAttention機構を導入し、組成特徴量とGNN埋め込みの重要度を動的に調整するコードを記述せよ。
解答:
# Attention機構付きEarly Fusion
import torch
import torch.nn as nn
import torch.nn.functional as F
class AttentionEarlyFusion(nn.Module):
def __init__(self, composition_dim=145, gnn_dim=128):
super(AttentionEarlyFusion, self).__init__()
# 特徴量変換層(同じ次元に統一)
self.comp_transform = nn.Linear(composition_dim, gnn_dim)
# GNN部分(省略、CGCNNと同じ)
# Attention機構
self.attention_comp = nn.Linear(gnn_dim, 1)
self.attention_gnn = nn.Linear(gnn_dim, 1)
# 予測層
self.fc = nn.Sequential(
nn.Linear(gnn_dim, 64),
nn.Softplus(),
nn.Linear(64, 1)
)
def forward(self, data, composition_features):
# 組成特徴量を変換(145 → 128次元)
comp_transformed = self.comp_transform(composition_features)
# GNN埋め込み計算(省略、CGCNNと同じ処理)
# gnn_embedding = ... (shape: batch_size, 128)
# Attention重みの計算
alpha_comp = self.attention_comp(comp_transformed) # (batch_size, 1)
alpha_gnn = self.attention_gnn(gnn_embedding) # (batch_size, 1)
# Softmax正規化
attention_weights = F.softmax(torch.cat([alpha_comp, alpha_gnn], dim=1), dim=1)
w_comp = attention_weights[:, 0:1] # 組成特徴量の重み
w_gnn = attention_weights[:, 1:2] # GNN埋め込みの重み
# 重み付き統合
hybrid_features = w_comp * comp_transformed + w_gnn * gnn_embedding
# 予測
out = self.fc(hybrid_features)
return out.squeeze(), w_comp.squeeze(), w_gnn.squeeze()
# 使用例
model = AttentionEarlyFusion().to(device)
# ... 訓練 ...
# 推論時にAttention重みを確認
model.eval()
with torch.no_grad():
pred, w_comp, w_gnn = model(test_data, test_comp_feats)
print(f"組成特徴量重み: {w_comp.mean():.3f}")
print(f"GNN埋め込み重み: {w_gnn.mean():.3f}")
# 出力例:
# 組成特徴量重み: 0.285
# GNN埋め込み重み: 0.715
# → データに応じて動的に重み調整
問題:マルチタスク学習において、「負の転移(Negative Transfer)」が発生しているかを検出する手法を提案し、その対策を3つ挙げよ。
解答:
負の転移の検出手法:
# 負の転移の検出
# 単一タスクモデルとマルチタスクモデルの性能を比較
# 単一タスクモデルの訓練
single_task1_model = train_single_task(task1_data)
single_task2_model = train_single_task(task2_data)
# マルチタスクモデルの訓練
multi_task_model = train_multi_task(task1_data, task2_data)
# 性能評価
mae_single_task1 = evaluate(single_task1_model, task1_test_data)
mae_single_task2 = evaluate(single_task2_model, task2_test_data)
mae_multi_task1 = evaluate_multitask(multi_task_model, task1_test_data, task_idx=0)
mae_multi_task2 = evaluate_multitask(multi_task_model, task2_test_data, task_idx=1)
# 負の転移の判定
if mae_multi_task1 > mae_single_task1:
print("Task 1で負の転移発生")
if mae_multi_task2 > mae_single_task2:
print("Task 2で負の転移発生")
# 出力例:
# Task 1で負の転移発生(マルチ 0.0295 > 単一 0.0286)
# → タスク間の相関が低い、またはタスク重みが不適切
負の転移の対策:
# タスク間の相関を計算
from scipy.stats import pearsonr
# Task 1とTask 2の予測値の相関
corr, _ = pearsonr(y_pred_task1, y_pred_task2)
if corr > 0.5:
print("高相関 → マルチタスク学習推奨")
else:
print("低相関 → 単一タスク学習推奨")
self.shared_layers = nn.Sequential( # 共有: 2層のみ
nn.Linear(input_dim, 128),
nn.Softplus()
)
self.task1_layers = nn.Sequential( # タスク固有: 3層
nn.Linear(128, 128),
nn.Softplus(),
nn.Linear(128, 64),
nn.Softplus(),
nn.Linear(64, 1)
)
# Uncertainty Weighting(不確実性に基づく重み調整)
class MultiTaskUncertaintyWeighting(nn.Module):
def __init__(self, n_tasks=2):
super().__init__()
self.log_vars = nn.Parameter(torch.zeros(n_tasks))
def forward(self, losses):
# タスクkの重み: 1 / (2 * σ_k²)
weighted_losses = []
for i, loss in enumerate(losses):
precision = torch.exp(-self.log_vars[i])
weighted_loss = precision * loss + self.log_vars[i]
weighted_losses.append(weighted_loss)
return sum(weighted_losses)
# 使用例
uncertainty_weighting = MultiTaskUncertaintyWeighting(n_tasks=2)
total_loss = uncertainty_weighting([loss_task1, loss_task2])
問題:Hybrid Early Fusionモデルにおいて、「組成特徴量とGNN埋め込みのどちらが予測に寄与しているか」を定量的に分析する手法を提案し、実装せよ。
解答:
# ハイブリッドモデルの解釈可能性分析
import numpy as np
from sklearn.inspection import permutation_importance
def hybrid_feature_importance_analysis(model, test_data, test_comp_feats, test_targets):
"""
組成特徴量とGNN埋め込みの寄与度を分析
Returns:
--------
comp_importance : float
組成特徴量の重要度
gnn_importance : float
GNN埋め込みの重要度
"""
model.eval()
# ベースライン予測(通常の予測)
with torch.no_grad():
baseline_pred = model(test_data, test_comp_feats).cpu().numpy()
baseline_mae = mean_absolute_error(test_targets, baseline_pred)
# 組成特徴量をゼロにした場合の予測
zero_comp_feats = torch.zeros_like(test_comp_feats)
with torch.no_grad():
pred_no_comp = model(test_data, zero_comp_feats).cpu().numpy()
mae_no_comp = mean_absolute_error(test_targets, pred_no_comp)
# GNN埋め込みをゼロにした場合の予測(モデル内部を変更)
# 簡略版:GNN部分をマスクする代わりに、別途GNN埋め込みなしモデルを訓練
# ここではPermutation Importanceを使用
# 組成特徴量の重要度(MAE増加量)
comp_importance = mae_no_comp - baseline_mae
# GNN埋め込みの重要度(類推:ランダムシャッフル)
n_permutations = 10
gnn_mae_increases = []
for _ in range(n_permutations):
# テストデータをシャッフル(GNN埋め込みをランダム化)
shuffled_indices = np.random.permutation(len(test_data))
shuffled_data = [test_data[i] for i in shuffled_indices]
with torch.no_grad():
pred_shuffled = model(Batch.from_data_list(shuffled_data).to(device),
test_comp_feats).cpu().numpy()
mae_shuffled = mean_absolute_error(test_targets, pred_shuffled)
gnn_mae_increases.append(mae_shuffled - baseline_mae)
gnn_importance = np.mean(gnn_mae_increases)
return comp_importance, gnn_importance
# 実行
comp_imp, gnn_imp = hybrid_feature_importance_analysis(
hybrid_model, test_data_list, test_comp_feats, test_targets
)
# 相対的重要度を計算
total_imp = comp_imp + gnn_imp
comp_ratio = comp_imp / total_imp * 100
gnn_ratio = gnn_imp / total_imp * 100
print(f"=== ハイブリッドモデルの特徴量重要度 ===")
print(f"組成特徴量の寄与: {comp_ratio:.1f}%")
print(f"GNN埋め込みの寄与: {gnn_ratio:.1f}%")
# 可視化
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 6))
ax.bar(['組成特徴量', 'GNN埋め込み'], [comp_ratio, gnn_ratio],
color=['#667eea', '#764ba2'])
ax.set_ylabel('相対的重要度 (%)', fontsize=12)
ax.set_title('ハイブリッドモデルの特徴量寄与度', fontsize=14, fontweight='bold')
ax.set_ylim(0, 100)
for i, v in enumerate([comp_ratio, gnn_ratio]):
ax.text(i, v + 2, f'{v:.1f}%', ha='center', va='bottom', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.savefig('hybrid_feature_importance.png', dpi=300, bbox_inches='tight')
plt.show()
# 出力例:
# === ハイブリッドモデルの特徴量重要度 ===
# 組成特徴量の寄与: 32.5%
# GNN埋め込みの寄与: 67.5%
# → GNN埋め込みがより重要だが、組成情報も有意に寄与