【入門】PythonでMatbenchを動かしてみる

# はじめに
近年、マテリアルズインフォマティクス（MI）は新材料の開発や物性予測において重要な役割を果たしています。性能の良いモデルを作るためには良質なデータを活用することが不可欠である一方、材料データはまだ十分に整備されておらず、データセットの標準化やモデル評価の難しさなどが課題として存在します。

こういった課題を解決するために開発されたのがMatbenchです。Matbenchを使うことで高品質な材料データを簡単に取得することができます。本記事では使い方をサンプルコードとともに解説します。

# Matbenchとは
Matbench は、マテリアルズインフォマティクス（MI）分野における機械学習モデルの性能評価を簡単かつ客観的に行うための Python ライブラリです。[公式サイト](https://matbench.materialsproject.org/)の記述によれば、「**材料科学におけるImageNet**」を目指して作られたとのことです。

- 物性予測や構造解析など、様々な MI タスクに対応したデータセットを統一された形式で提供
- 各データセットに対して、標準的な機械学習タスク（回帰、分類など）を定義し、モデルが取り組むべき問題を明確化
- モデルの性能を比較するための評価指標（MAE、RMSE、AUC など）を統一し、客観的な評価が可能

といった点がポイントになると思います。

## リーダーボード
Matbench では、様々なモデルの性能を比較するためのリーダーボードが公開されています。リーダーボードは、各データセットとタスクにおける最高性能を達成したモデルや、その性能指標（MAE、RMSE、AUC など）を確認することができます。

リーダーボードの詳細は[こちらのページ](https://matbench.materialsproject.org/Benchmark%20Info/matbench_v0.1/)で確認できます。ダウンロードなどもここから行うことができます。

![スクリーンショット 2025-02-25 225920.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4006209/9007f450-b93b-47e2-8cdd-0ca082424e07.png)

Task nameと書いてあるのが各タスク名です。機械学習モデルが予測すべきターゲットごとにデータセットや評価指標が分けられています。例えば以下のようなタスクが用意されています。

- matbench_mp_e_form: 材料の形成エネルギーを予測するタスク
- matbench_mp_gap: 材料のバンドギャップを予測するタスク
- matbench_mp_is_metal: 材料が金属であるかどうかを分類するタスク
- matbench_elastic: 材料の弾性率を予測するタスク
- matbench_dielectric: 材料の誘電率を予測するタスク

リーダーボードを参考に、自分のモデルの性能を評価したり、他の研究者の成果を参考にしたりすることができます。

# Matbenchの使い方
## 実行環境
- Windows 11
- anaconda: 24.11.3
- python: 3.9
- jupyter: 1.1

## インストール方法
### pipの場合
```
pip install matbench
```

### condaの場合
```
conda install matbench
```
パッケージが見つからない場合は以下のコマンドを実行後に上記を実行してください。
```
conda config --append channels conda-forge
```

## Pythonを使った動かし方

### モジュールのインポート
インストールが正しくできていれば、以下のコードでインポートができると思います。
```python:python
from matbench.bench import MatbenchBenchmark
```
### クラスの初期化
```python:python
mb = MatbenchBenchmark(autoload=False)
```
これを実行することで、以下のような出力が得られます。ここでタスクの一覧も確認できます。
```:出力結果
2025-02-25 23:20:50 INFO     Initialized benchmark 'matbench_v0.1' with 13 tasks: 
['matbench_dielectric',
 'matbench_expt_gap',
 'matbench_expt_is_metal',
 'matbench_glass',
 'matbench_jdft2d',
 'matbench_log_gvrh',
 'matbench_log_kvrh',
 'matbench_mp_e_form',
 'matbench_mp_gap',
 'matbench_mp_is_metal',
 'matbench_perovskites',
 'matbench_phonons',
 'matbench_steels']
```

## タスクの選択とデータ読み込み
例えば、材料の誘電率を予測するmatbench_dielectricのタスクを選んでみます。タスクは、下記のようにMatbenchBenchmarkクラスの属性として選択することができます。
```python:python
selected_task = mb.matbench_dielectric #誘電率予測のタスクを選択
print(selected_task)
```
選択したタスクをprintしてみると、以下のように入力形式、サンプル数などデータセットの情報を確認できます。

```:出力結果
MatbenchTask(
  dataset_name=matbench_dielectric,
  version=0.1,
  input_type=structure,
  mad=0.808534704217072,
  n_samples=4764,
  target=n,
  task_type=regression,
  unit=unitless,
)
```

次に、loadメソッドを使うことでデータセットの読み込みを行うことができます。
```python:python
selected_task.load()
```
すると、このように読み込みが行われます。

```:出力結果
2025-02-26 22:32:00 INFO     Loading dataset 'matbench_dielectric'...
2025-02-26 22:32:04 INFO     Dataset 'matbench_dielectric loaded.
```

これでデータセットの読み込みは完了です。

## データの形式
取得したデータは、以下のようにpandasのデータフレームとして取得できます。
```python:python
df = selected_task.df
```
カラムの一覧を見てみると、
```python:python
print(df.columns)
"""
出力結果:
Index(['structure', 'n'], dtype='object')
"""
```
2つのカラムがありますが、'structure'が説明変数である材料の構造データ、'n'が目的変数である誘電率にあたります。
structureの方の中身を見てみると、
```python:python
print(df["structure"])
```

以下のような出力が得られます。

```:出力結果
mbid
mb-dielectric-0001    [[4.29304147 2.4785886  1.07248561] S, [4.2930...
mb-dielectric-0002    [[3.95051434 4.51121437 0.28035002] K, [4.3099...
mb-dielectric-0003    [[-1.78688104  4.79604117  1.53044621] Rb, [-1...
mb-dielectric-0004    [[4.51438064 4.51438064 0.        ] Mn, [0.133...
mb-dielectric-0005    [[-4.36731958  6.8886097   0.50929706] Li, [-2...
                                            ...                        
mb-dielectric-4760    [[ 2.79280881  0.12499663 -1.84045389] Ca, [-2...
mb-dielectric-4761    [[-1.45848057e-16  5.50363806e+00  3.84192106e...
mb-dielectric-4762    [[0. 0. 0.] Ba, [ 0.23821924  4.32393487 -0.35...
mb-dielectric-4763    [[0.         0.18884638 0.        ] K, [0.    ...
mb-dielectric-4764    [[0. 0. 0.] Cs, [2.80639641 2.80639641 2.80639...
Name: structure, Length: 4764, dtype: object
```
データフレームの行はmbid、つまりデータセット上の材料のIDが振られています。
データの中身はこの出力だと見づらいですが、例えば最初のデータを確認してみると、

```python:python
print(type(df["structure"][0])) #出力結果: <class 'pymatgen.core.structure.Structure'>
```
pymatgenのStructure型で得られていることがわかります。
データをprintしてみると以下のような出力が得られます。
```python:python
print(df["structure"][0]))
```
```:出力
Full Formula (K6 S6)
Reduced Formula: KS
abc   :   8.586083   8.586083   5.920854
angles:  90.000000  90.000000 120.000000
Sites (12)
  #  SP           a         b         c    magmom
---  ----  --------  --------  --------  --------
  0  S     0.666667  0.333333  0.181137         0
  1  S     0.666667  0.333333  0.818863         0
  2  S     0.333333  0.666667  0.818863         0
  3  S     0.333333  0.666667  0.181137         0
  4  S     0         0         0.67987          0
  5  S     0         0         0.32013          0
  6  K     0         0.642561  0.5              0
  7  K     0.357439  0.357439  0.5              0
  8  K     0.642561  0         0.5              0
  9  K     0         0.307123  0                0
 10  K     0.692877  0.692877  0                0
 11  K     0.307123  0         0                0
 ```
 

[別の記事](https://qiita.com/k_t_09099/items/681ad83ac1c8f6c9592b)でも紹介しましたが、このStructureオブジェクトからは格子定数と格子角、原子の種類と占有率、単位格子の体積、密度など様々な情報が取得できます。Structureオブジェクトはこのように単体ではまだ抽象化されたクラスなので、実際にモデルのトレーニングを行うにはこのオブジェクトから様々な情報を取り出して前処理を行う必要があると思われます。


## データの分割
matbenchにはトレーニング、検証、テスト用にデータを分割する関数も用意されています。トレーニング、検証用にはget_train_and_val_dataメソッド、テスト用データ取得にはget_test_dataメソッドを使用します。
これらの関数にはfoldという引数を使用します。これについての具体的な説明が見当たらず推測になりますが、おそらく学習用、テスト用データの分け方の種類を表していると思われます。具体的には、
```python:python
print(selected_task.folds)
```
の出力を見ると、
```:出力結果
[0, 1, 2, 3, 4]
```
このようにリストの出力が得られ、計5種類のデータの分け方が用意されていると思われます。
例えば、fold: 0を使ってデータを読み込む場合、以下のようなコードになります。

```python:python
fold = 0
train_inputs, train_outputs = selected_task.get_train_and_val_data(fold) #学習・検証用データ
test_inputs, test_outputs = selected_task.get_test_data(fold, include_target=True) #テスト用データ
```
get_test_dataメソッドの方は、デフォルトでは目的変数のデータは出力されないようになっていますが、include_target=Trueとすることで取得できます。

このデータを確認してみると、
```python:python
print(type(train_inputs)) #出力: <class 'pandas.core.series.Series'>
print(train_inputs.shape, train_outputs.shape) #出力:  (3811,) (3811,)
print(test_inputs.shape, test_outputs.shape) #出力: (953,) (953,)
```
データはpandasのSeries型で取得され、学習用とテストデータは80%, 20%に分割されていることがわかります。

foldの値を変えることで、データの分けられ方が変わることも確認できます。
例えばfold=0の場合、train_inputsの中身を見てみます。左のIDに注目してください。

```:出力結果
mbid
mb-dielectric-0001    [[4.29304147 2.4785886  1.07248561] S, [4.2930...
mb-dielectric-0002    [[3.95051434 4.51121437 0.28035002] K, [4.3099...
mb-dielectric-0003    [[-1.78688104  4.79604117  1.53044621] Rb, [-1...
mb-dielectric-0004    [[4.51438064 4.51438064 0.        ] Mn, [0.133...
mb-dielectric-0005    [[-4.36731958  6.8886097   0.50929706] Li, [-2...
                                            ...                        
mb-dielectric-4758    [[ 0.10737615 -5.39167724 -4.04533555] Mo, [ 0...
mb-dielectric-4759    [[0.06754224 4.23094828 1.68380672] Cr, [1.886...
mb-dielectric-4761    [[-1.45848057e-16  5.50363806e+00  3.84192106e...
mb-dielectric-4762    [[0. 0. 0.] Ba, [ 0.23821924  4.32393487 -0.35...
mb-dielectric-4764    [[0. 0. 0.] Cs, [2.80639641 2.80639641 2.80639...
Name: structure, Length: 3811, dtype: object
```

次にfold=3としてみると、左のIDの並びが変わります。

```:出力結果
mbid
mb-dielectric-0003    [[-1.78688104  4.79604117  1.53044621] Rb, [-1...
mb-dielectric-0005    [[-4.36731958  6.8886097   0.50929706] Li, [-2...
mb-dielectric-0006    [[0.04903784 0.0347292  0.08458426] Ca, [3.740...
mb-dielectric-0007    [[5.60800905 1.10640796 1.26351442] Se, [3.762...
mb-dielectric-0008    [[ 4.5571751   2.77317895 16.01717369] O, [0.5...
                                            ...                        
mb-dielectric-4760    [[ 2.79280881  0.12499663 -1.84045389] Ca, [-2...
mb-dielectric-4761    [[-1.45848057e-16  5.50363806e+00  3.84192106e...
mb-dielectric-4762    [[0. 0. 0.] Ba, [ 0.23821924  4.32393487 -0.35...
mb-dielectric-4763    [[0.         0.18884638 0.        ] K, [0.    ...
mb-dielectric-4764    [[0. 0. 0.] Cs, [2.80639641 2.80639641 2.80639...
Name: structure, Length: 3811, dtype: object
```
このように、foldを変えることで学習用とテスト用のデータの分け方を変化させることができます。

以下は[ホームページ](https://matbench.materialsproject.org/How%20To%20Use/2run/)からとってきたソースコードですが、実際は下の表のように各foldごとで別々にベンチマークがなされるようです。モデルの汎化誤差を正しく評価するため、いろいろな分け方のデータで評価をしているということでしょうか。

```python:python
for fold in task.folds:
    # Inputs are either chemical compositions as strings
    # or crystal structures as pymatgen.Structure objects.
    # Outputs are either floats (regression tasks) or bools (classification tasks)
    train_inputs, train_outputs = task.get_train_and_val_data(fold)

    # train and validate your model
    my_model.train_and_validate(train_inputs, train_outputs)

    # Get testing data
    test_inputs = task.get_test_data(fold, include_target=False)

    # Predict on the testing data
    # Your output should be a pandas series, numpy array, or python iterable
    # where the array elements are floats or bools
    predictions = my_model.predict(test_inputs)

    # Record your data!
    task.record(fold, predictions)
```
ベンチマークの例([ホームページ](https://matbench.materialsproject.org/Full%20Benchmark%20Data/matbench_v0.1_CrabNet_v1.2.1/)より)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4006209/900d4c0a-0e66-4949-af00-a52394335b9d.png)


# まとめ
以上、Pythonを使ったMatbenchの使い方をまとめてみました。タスクごとにデータが分けられており、数行のコードでデータを持ってくることができるのでとても便利そうですね。今回はデータの取得や分割方法について紹介しましたが、次回は実際にリーダーボードで上位となっているモデルを見ていこうと思います。


# 参考文献
- [Matbench](https://matbench.materialsproject.org/)