学習目標
このハンズオンを終えると、次の3つができるようになります。
- MIの基本ループを説明できる:データ → 特徴量化(記述子) → 学習 → 評価 → 考察という一連の流れを、自分の言葉で説明できます。
- 組成だけから記述子を作れる:「GaAs」「TiO2」のような化学式を解析し、元素特性の組成加重平均から機械学習(Machine Learning)に使える特徴量を計算できます。
- 結果を誠実に評価できる:交差検証(Cross-Validation)でMAEとR²を求め、「予測が当たった例・外した例」を区別し、なぜ外れたのかを材料科学の言葉で考察できます。
0. はじめに:このページの位置づけ
マテリアルズ・インフォマティクス(Materials Informatics、以下MI)とは、データと機械学習を使って新しい材料の性質を予測したり、有望な候補を絞り込んだりする分野です。言葉で説明を読むより、一度自分の手でモデルを動かしてみるのが、いちばん腑に落ちます。このページは、その最初の一歩を60〜90分で体験するための独立したハンズオンです。
お題は「化合物の組成から、その物質のバンドギャップ(Band Gap、電子が価電子帯から伝導帯へ飛び移るのに必要な最小エネルギー、単位はeV)を予測する」こと。半導体・絶縁体の設計で最も基本的な物性の一つです。シリコン(Si)の1.12 eVは太陽電池や集積回路の性能を、酸化マグネシウム(MgO)の7.8 eVは透明な絶縁体としての性質を決めています。
📋 前提と準備
- 前提知識:Pythonの基礎(変数・リスト・関数・for文が読める)だけです。材料科学や機械学習の予備知識は要りません。専門用語は初出時に必ず説明します。
- 必要なもの:Python 3 と、数値計算ライブラリ
numpy、機械学習ライブラリscikit-learn。pip install numpy scikit-learnでインストールできます。ブラウザだけで動かしたい場合は Google Colab でもそのまま動きます。 - 進め方:コードは6つのセルに分かれています。上から順にコピーして実行すれば、前のセルの結果を引き継いで最後まで到達します。掲載している出力は、実際に手元で実行した本物の結果です。
1. データを用意する(データ)
MIの出発点は、必ず「データ」です。今回は、教科書や半導体ハンドブックで確認できる実在の半導体・絶縁体30化合物のバンドギャップ実測値を使います。値はすべて室温付近の代表的な実験値です。
⚠️ データの誠実さについて(重要)
ここに載せた値は、C. Kittel『固体物理学入門(Introduction to Solid State Physics)』や半導体ハンドブックなどの標準的な文献に載っている代表値です。バンドギャップは測定温度・試料の質・測定手法・文献によって少しずつ異なります(多くは ±0.05〜0.1 eV 程度)。ここでは「だいたいこの値」という代表値として扱ってください。研究で使う際は、必ず一次文献で値と測定条件を確認しましょう。値を都合よく作ってはいけない、というのがMIで最初に身につけるべき姿勢です。
import re
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import LeaveOneOut, cross_val_predict, cross_val_score
from sklearn.metrics import mean_absolute_error, r2_score
# 実験バンドギャップ(室温、代表値 eV)
# 出典: Kittel, Introduction to Solid State Physics;
# 半導体ハンドブック等の標準文献値。
dataset = [
("Si", 1.12), ("Ge", 0.67), ("C", 5.47), ("SiC", 3.00),
("GaAs", 1.42), ("GaP", 2.26), ("GaN", 3.40), ("GaSb", 0.73),
("InP", 1.35), ("InAs", 0.36), ("InSb", 0.17), ("InN", 0.70),
("AlAs", 2.16), ("AlSb", 1.60), ("AlP", 2.45),
("ZnO", 3.37), ("ZnS", 3.60), ("ZnSe", 2.70), ("ZnTe", 2.25),
("CdS", 2.42), ("CdSe", 1.74), ("CdTe", 1.49),
("PbS", 0.37), ("PbSe", 0.27), ("PbTe", 0.32),
("MgO", 7.80), ("TiO2", 3.20), ("SnO2", 3.60),
("Cu2O", 2.17), ("BN", 6.40),
]
print(f"化合物数: {len(dataset)}")
print("最小:", min(dataset, key=lambda x: x[1]))
print("最大:", max(dataset, key=lambda x: x[1]))
実行結果:
化合物数: 30
最小: ('InSb', 0.17)
最大: ('MgO', 7.8)
ダイヤモンド(C, 5.47 eV)や立方晶窒化ホウ素(BN, 6.4 eV)のようなワイドギャップ物質から、アンチモン化インジウム(InSb, 0.17 eV)のような赤外線検出器に使われる狭ギャップ半導体まで、幅広く含めました。この「値の幅広さ」が、後でモデルの得意・不得意を見るときに効いてきます。
2. 組成を数値に変える(特徴量化・その1:パーサ)
機械学習モデルは「GaAs」という文字列をそのままでは理解できません。数値の並び(ベクトル)に変換する必要があります。この変換を特徴量化(Featurization、記述子の計算)と呼び、MIの心臓部です。
まず、化学式を「どの元素が何個あるか」に分解する小さなパーサ(parser、構文解析器)を書きます。そのために、使う元素の性質を並べた表も用意します。値はいずれも周期表・化学便覧に載っている実測のタブレート値です。
# 元素特性表(実測タブレート値)
# Z=原子番号, period=周期, group=族, EN=Pauling電気陰性度, mass=原子量
ELEMENTS = {
"B": {"Z": 5, "period": 2, "group": 13, "EN": 2.04, "mass": 10.81},
"C": {"Z": 6, "period": 2, "group": 14, "EN": 2.55, "mass": 12.011},
"N": {"Z": 7, "period": 2, "group": 15, "EN": 3.04, "mass": 14.007},
"O": {"Z": 8, "period": 2, "group": 16, "EN": 3.44, "mass": 15.999},
"Mg": {"Z": 12, "period": 3, "group": 2, "EN": 1.31, "mass": 24.305},
"Al": {"Z": 13, "period": 3, "group": 13, "EN": 1.61, "mass": 26.982},
"Si": {"Z": 14, "period": 3, "group": 14, "EN": 1.90, "mass": 28.085},
"P": {"Z": 15, "period": 3, "group": 15, "EN": 2.19, "mass": 30.974},
"S": {"Z": 16, "period": 3, "group": 16, "EN": 2.58, "mass": 32.06},
"Ti": {"Z": 22, "period": 4, "group": 4, "EN": 1.54, "mass": 47.867},
"Cu": {"Z": 29, "period": 4, "group": 11, "EN": 1.90, "mass": 63.546},
"Zn": {"Z": 30, "period": 4, "group": 12, "EN": 1.65, "mass": 65.38},
"Ga": {"Z": 31, "period": 4, "group": 13, "EN": 1.81, "mass": 69.723},
"Ge": {"Z": 32, "period": 4, "group": 14, "EN": 2.01, "mass": 72.630},
"As": {"Z": 33, "period": 4, "group": 15, "EN": 2.18, "mass": 74.922},
"Se": {"Z": 34, "period": 4, "group": 16, "EN": 2.55, "mass": 78.971},
"Cd": {"Z": 48, "period": 5, "group": 12, "EN": 1.69, "mass": 112.414},
"In": {"Z": 49, "period": 5, "group": 13, "EN": 1.78, "mass": 114.818},
"Sn": {"Z": 50, "period": 5, "group": 14, "EN": 1.96, "mass": 118.710},
"Sb": {"Z": 51, "period": 5, "group": 15, "EN": 2.05, "mass": 121.760},
"Te": {"Z": 52, "period": 5, "group": 16, "EN": 2.10, "mass": 127.60},
"Pb": {"Z": 82, "period": 6, "group": 14, "EN": 2.33, "mass": 207.2},
}
def parse_formula(formula):
"""化学式を {元素: 原子数} の辞書に変換する簡易パーサ。"""
tokens = re.findall(r"([A-Z][a-z]?)(\d*)", formula)
comp = {}
for elem, count in tokens:
if elem == "":
continue
n = int(count) if count else 1
comp[elem] = comp.get(elem, 0) + n
return comp
# 動作確認
for f in ["Si", "GaAs", "TiO2", "Cu2O"]:
print(f, "->", parse_formula(f))
実行結果:
Si -> {'Si': 1}
GaAs -> {'Ga': 1, 'As': 1}
TiO2 -> {'Ti': 1, 'O': 2}
Cu2O -> {'Cu': 2, 'O': 1}
正規表現 ([A-Z][a-z]?)(\d*) は「大文字1文字+小文字0〜1文字(元素記号)+数字0個以上(原子数)」という並びを拾います。TiO2 が {'Ti': 1, 'O': 2} に、Cu2O が {'Cu': 2, 'O': 1} に分解できました。これで組成比が手に入ります。
💡 実務では
この手書きパーサは学習用の最小実装です。実際の研究では pymatgen の Composition クラスが、括弧つきの複雑な式(例:Ca(OH)2)や水和物、酸化状態まで正しく扱ってくれます。次の特徴量計算も、matminer を使えば ElementProperty.from_preset("magpie") の一行で百数十種類の記述子が自動生成されます。ここでは「中で何が起きているか」を体感するために、あえて手で書いています。
3. 記述子を計算する(特徴量化・その2)
組成比が分かったら、元素の性質を組成加重平均して、化合物1つを表す数値ベクトルにまとめます。今回は次の6つの記述子を作ります。
mean_EN:電気陰性度(Electronegativity、原子が電子を引きつける強さ)の加重平均EN_diff:構成元素間の電気陰性度の差(最大−最小)。結合のイオン性(ionicity)の目安で、バンドギャップと関係が深いと期待されます。mean_Z:原子番号の加重平均mean_group:族番号の加重平均mean_period:周期の加重平均mean_mass:原子量の加重平均
FEATURE_NAMES = ["mean_EN", "EN_diff", "mean_Z", "mean_group", "mean_period", "mean_mass"]
def featurize(formula):
comp = parse_formula(formula)
total = sum(comp.values())
fracs = {e: n / total for e, n in comp.items()}
ens = [ELEMENTS[e]["EN"] for e in comp]
mean_EN = sum(fracs[e] * ELEMENTS[e]["EN"] for e in comp)
EN_diff = max(ens) - min(ens)
mean_Z = sum(fracs[e] * ELEMENTS[e]["Z"] for e in comp)
mean_group = sum(fracs[e] * ELEMENTS[e]["group"] for e in comp)
mean_period = sum(fracs[e] * ELEMENTS[e]["period"] for e in comp)
mean_mass = sum(fracs[e] * ELEMENTS[e]["mass"] for e in comp)
return [mean_EN, EN_diff, mean_Z, mean_group, mean_period, mean_mass]
X = np.array([featurize(f) for f, _ in dataset])
y = np.array([g for _, g in dataset])
print("特徴量行列 X の形状:", X.shape)
print("特徴量名:", FEATURE_NAMES)
print("GaAs の特徴量:", [round(v, 3) for v in featurize("GaAs")])
print("MgO の特徴量:", [round(v, 3) for v in featurize("MgO")])
実行結果:
特徴量行列 X の形状: (30, 6)
特徴量名: ['mean_EN', 'EN_diff', 'mean_Z', 'mean_group', 'mean_period', 'mean_mass']
GaAs の特徴量: [1.995, 0.37, 32.0, 14.0, 4.0, 72.322]
MgO の特徴量: [2.375, 2.13, 10.0, 9.0, 2.5, 20.152]
30化合物×6記述子の行列 X ができました。ここで EN_diff に注目してください。共有結合的な GaAs は電気陰性度差が 0.37 と小さく、イオン結合的な MgO は 2.13 と大きい。この差がバンドギャップの大小と結びつくかどうかを、これからモデルに学ばせます。y は予測したい正解(バンドギャップ)のベクトルです。
4. モデルを学習し、評価する(学習・評価)
モデルにはランダムフォレスト回帰(Random Forest Regressor、多数の決定木の平均をとる手法)を使います。少ないデータでも過学習しにくく、前処理(標準化)が不要で、後述する特徴量の重要度も取り出せる、最初の一台として扱いやすいモデルです。
ここで大事なのが評価のしかたです。データはわずか30件。訓練データとテストデータを普通に分けると、テストが数件しか残らず、評価がブレます。そこでLeave-One-Out交差検証(LOO-CV、1件だけをテストに回し、残り29件で学習することを全件について繰り返す方法)を使います。全化合物が一度ずつ「未知の物質」としてテストされるので、小さなデータでも評価が安定します。
⚠️ なぜ交差検証がそんなに大事なのか
学習に使ったデータで性能を測ると、モデルは答えを覚えているだけかもしれないのに、見かけの成績が異常に良くなります。これはデータ漏洩(data leakage)と呼ばれ、MIで最も多い失敗です。「一度も学習に使っていないデータでどれだけ当たるか」を測って初めて、モデルの本当の実力が分かります。データが小さいほど、この評価設計が結果を左右します。
model = RandomForestRegressor(n_estimators=300, random_state=42)
loo = LeaveOneOut()
y_pred_cv = cross_val_predict(model, X, y, cv=loo)
mae = mean_absolute_error(y, y_pred_cv)
r2 = r2_score(y, y_pred_cv)
print(f"LOO交差検証 MAE: {mae:.3f} eV")
print(f"LOO交差検証 R^2: {r2:.3f}")
# 参考: 5-fold CV
model5 = RandomForestRegressor(n_estimators=300, random_state=42)
scores = cross_val_score(model5, X, y, cv=5, scoring="neg_mean_absolute_error")
print(f"5-fold CV MAE: {-scores.mean():.3f} eV (std {scores.std():.3f})")
実行結果:
LOO交差検証 MAE: 0.892 eV
LOO交差検証 R^2: 0.471
5-fold CV MAE: 0.834 eV (std 0.343)
結果を読み解きましょう。MAE(Mean Absolute Error、平均絶対誤差)は約0.89 eV。予測が平均して実測から0.89 eVずれる、という意味です。R²(決定係数、1に近いほど良い)は0.47。完璧なら1.0、まったく予測できていなければ0付近です。0.47は「傾向はつかめているが、精度はまだ粗い」という正直なところ。5分割交差検証でもMAE 0.83 eVと近い値で、評価が偶然でないことも確認できました。
この数字を「低い」と感じたかもしれません。でも、組成だけ・30件だけ・手作り記述子6個だけでここまで来られた、と捉えるのが正しい見方です。改善の余地がどこにあるかは、次の考察で見えてきます。
5. 予測を1件ずつ確かめる(評価の可視化)
平均のMAEだけでは、どの物質が当たってどの物質が外れたのか分かりません。代表的な8化合物について、交差検証での予測値と実測値を並べてみます。
print(f"{'化合物':<8}{'実測(eV)':>10}{'予測(eV)':>10}{'誤差':>8}")
show = ["Si", "GaAs", "GaN", "ZnO", "CdTe", "MgO", "PbS", "InSb"]
idx = {f: i for i, (f, _) in enumerate(dataset)}
for f in show:
i = idx[f]
print(f"{f:<8}{y[i]:>10.2f}{y_pred_cv[i]:>10.2f}{y_pred_cv[i]-y[i]:>+8.2f}")
実行結果:
化合物 実測(eV) 予測(eV) 誤差
Si 1.12 2.46 +1.34
GaAs 1.42 1.37 -0.05
GaN 3.40 2.81 -0.59
ZnO 3.37 3.16 -0.21
CdTe 1.49 1.06 -0.43
MgO 7.80 3.04 -4.76
PbS 0.37 0.68 +0.31
InSb 0.17 0.73 +0.56
面白い結果です。GaAs(誤差−0.05)、ZnO(−0.21)、GaN(−0.59)はよく当たっています。これらは似た組成の仲間(III-V族半導体や酸化物)がデータ中に複数あり、モデルが「近い物質」から補間できたためです。
一方、MgO は実測7.80 eVに対し予測3.04 eVと、4.76 eVも外しました。MgOはデータ中で飛び抜けてバンドギャップが大きく、ランダムフォレストは学習データの範囲外を外挿(extrapolation)できない(木の予測は訓練データの最大値付近で頭打ちになる)ためです。Si(+1.34)が外れ気味なのも、単体元素半導体という少数派で、周りに似た例が乏しいことが効いています。これは失敗ではなく、「データにない領域は予測できない」という機械学習の本質を目で見た瞬間です。
6. なぜ予測できたのかを解釈する(考察の入口)
最後に、モデルが6つの記述子のうちどれを重視したかを見ます。ランダムフォレストは特徴量の重要度(feature importance)を出せるので、予測の根拠を材料科学の言葉に翻訳できます。
model_full = RandomForestRegressor(n_estimators=300, random_state=42)
model_full.fit(X, y)
imp = model_full.feature_importances_
order = np.argsort(imp)[::-1]
for j in order:
print(f"{FEATURE_NAMES[j]:<12}{imp[j]:.3f}")
実行結果:
mean_mass 0.302
EN_diff 0.225
mean_Z 0.167
mean_period 0.165
mean_EN 0.100
mean_group 0.041
上位の記述子には、ちゃんとした物理的な意味があります。
- EN_diff(電気陰性度差、重要度0.23):構成元素の電気陰性度の差が大きいほど結合はイオン性が強くなり、一般にバンドギャップが広がります。データ中でも、EN_diffが大きいMgOやBNはワイドギャップ、EN_diffが小さいInSbやGeは狭ギャップです。モデルはこのイオン性とギャップの関係を自力で見つけています。
- mean_mass・mean_Z・mean_period(原子量・原子番号・周期):これらは互いに強く相関し、いずれも「重い(周期が下の)元素ほどバンドギャップが小さい」という周期的傾向を捉えています。実際、ZnS(3.60)→ ZnSe(2.70)→ ZnTe(2.25)や、Si(1.12)→ Ge(0.67)のように、陰イオンが重くなるほどギャップが縮む系列がデータに含まれています。モデルはこの傾向を数値で学んだわけです。
「モデルが当てずっぽうではなく、既知の物理法則に沿った根拠で予測している」と確認できたこと。これが、MIループの最後のステップ「考察」の第一歩です。
7. 考察:うまくいったこと・限界・次の一手
7.1 うまくいったこと
- 化学式の文字列だけを入力に、データ → 特徴量化 → 学習 → 評価 → 考察というMIの基本ループを一周できました。
- 組成の似た化合物(GaAs, ZnO, GaN など)は0.5 eV前後の誤差で予測でき、外部ライブラリなしでも「傾向は掴める」ことを確認しました。
- 特徴量の重要度から、電気陰性度差=イオン性と構成元素の重さ=周期的傾向という、既知の物理と整合する根拠を取り出せました。
7.2 限界(ここが本質です)
- データが小さすぎる:30件では、MgOのような外れ値を1つ含むだけでR²が大きく揺れます。実務では数百〜数万件のデータで学習します。
- 組成だけでは結晶構造を無視している:同じ組成でも結晶構造(多形、polymorph)が違えばバンドギャップは変わります。今回データに入れたTiO2は、実はアナターゼ(約3.2 eV)とルチル(約3.0 eV)で値が異なりますし、SiCも積層構造(ポリタイプ)ごとに2.4〜3.3 eVと幅があります。組成ベースの記述子はこの違いを原理的に区別できません。構造情報が要る場合は結晶グラフや構造記述子が必要です。
- 実験値とDFT計算値は別物:教科書の実測ギャップと、Materials Projectなどに載る密度汎関数理論(DFT)計算のギャップは体系的にずれます(標準的なDFTはギャップを過小評価しがち)。データを混ぜるときは出所をそろえる必要があります。
- 外挿はできない:ランダムフォレストは学習データの範囲外を予測できません(MgOの失敗がその実例)。
7.3 次はこれ:さらに深掘りする学習シリーズ
このハンズオンで体験した各ステップは、それぞれ独立した入門シリーズで本格的に学べます。興味の湧いたところから進んでください。
- マテリアルズ・インフォマティクス(MI)入門:MI全体像を基礎から。まず全体を俯瞰したい方に。
- 組成ベース特徴量入門:今回手で書いた記述子を、matminer / Magpieで本格的に扱う方法。特徴量化を深めたい方に直結します。
- 材料データベース活用入門:「データが小さすぎる」問題への答え。Materials Projectから実データを大量に取得する方法を学べます。
- ベイズ最適化・アクティブラーニング入門:予測モデルを使い、少ない実験で有望材料を効率よく探す次のステップ。
🎯 全体を1つのスクリプトで振り返る
6つのセルは上から順に実行すれば、そのまま1本のスクリプトとして最後まで動きます。「まず全部動かしてから中身を読む」のもよい進め方です。数字を少し変えて(記述子を増やす、化合物を足す、モデルを線形回帰に替える)試すと、MIの手触りがさらにつかめます。ここまでお疲れさまでした。あなたはもう、MIの基本ループを一周した経験者です。