🌐 JP | 🇬🇧 EN | Materials Informatics Quickstart

1時間でMI体験:組成からバンドギャップを予測する

化合物リストから始めて、学習済みモデルと誠実な評価までを一気通貫で体験する

📖 所要時間: 60-90分 📊 難易度: 入門 (Beginner) 💻 コード例: 6個 🧪 データ: 実測30化合物

学習目標

このハンズオンを終えると、次の3つができるようになります。

0. はじめに:このページの位置づけ

マテリアルズ・インフォマティクス(Materials Informatics、以下MI)とは、データと機械学習を使って新しい材料の性質を予測したり、有望な候補を絞り込んだりする分野です。言葉で説明を読むより、一度自分の手でモデルを動かしてみるのが、いちばん腑に落ちます。このページは、その最初の一歩を60〜90分で体験するための独立したハンズオンです。

お題は「化合物の組成から、その物質のバンドギャップ(Band Gap、電子が価電子帯から伝導帯へ飛び移るのに必要な最小エネルギー、単位はeV)を予測する」こと。半導体・絶縁体の設計で最も基本的な物性の一つです。シリコン(Si)の1.12 eVは太陽電池や集積回路の性能を、酸化マグネシウム(MgO)の7.8 eVは透明な絶縁体としての性質を決めています。

📋 前提と準備

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} に分解できました。これで組成比が手に入ります。

💡 実務では

この手書きパーサは学習用の最小実装です。実際の研究では pymatgenComposition クラスが、括弧つきの複雑な式(例:Ca(OH)2)や水和物、酸化状態まで正しく扱ってくれます。次の特徴量計算も、matminer を使えば ElementProperty.from_preset("magpie") の一行で百数十種類の記述子が自動生成されます。ここでは「中で何が起きているか」を体感するために、あえて手で書いています。

3. 記述子を計算する(特徴量化・その2)

組成比が分かったら、元素の性質を組成加重平均して、化合物1つを表す数値ベクトルにまとめます。今回は次の6つの記述子を作ります。

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

上位の記述子には、ちゃんとした物理的な意味があります。

「モデルが当てずっぽうではなく、既知の物理法則に沿った根拠で予測している」と確認できたこと。これが、MIループの最後のステップ「考察」の第一歩です。

7. 考察:うまくいったこと・限界・次の一手

7.1 うまくいったこと

7.2 限界(ここが本質です)

7.3 次はこれ:さらに深掘りする学習シリーズ

このハンズオンで体験した各ステップは、それぞれ独立した入門シリーズで本格的に学べます。興味の湧いたところから進んでください。

🎯 全体を1つのスクリプトで振り返る

6つのセルは上から順に実行すれば、そのまま1本のスクリプトとして最後まで動きます。「まず全部動かしてから中身を読む」のもよい進め方です。数字を少し変えて(記述子を増やす、化合物を足す、モデルを線形回帰に替える)試すと、MIの手触りがさらにつかめます。ここまでお疲れさまでした。あなたはもう、MIの基本ループを一周した経験者です。

免責事項