WandBで実験管理をシンプルに

機械学習にてモデル性能を確認するため、手元にあるデータセットを分割、そのデータで評価を行います。
評価をするために実験記録が必要ですが、結果やハイパーパラメータなどなど実験管理に手間がかかる。。。
そんな課題へのソリューションの一つに、実験の結果やメトリクスを手軽に管理・可視化できるWandBがあります。WandBを利用しはじめてグッと管理コストが減ったので記録として残そうと思います。
本内容は私の解釈、手法の説明ですのでその点ご了承ください。


# WandBについて
WandBは、機械学習プロジェクトの全過程を追跡・可視化するツールで、以下のような機能を提供しています
- モデルの学習記録
- 実験結果の可視化
- モデル・データセットのバージョン管理

記録するための実装もシンプルで極論５行で実験管理のコードをかけたりします。

https://qiita.com/Yu_Mochi/items/4fc283ebc31225d4e106

ChatGPTでお馴染みのOpenAIがLLMモデル評価で採用してもいます。
また、LLMモデルの日本語能力を評価、公開してくださっていたり。
[Nejumi LLMリーダーボード
](https://wandb.ai/wandb-japan/llm-leaderboard3/reports/Nejumi-LLM-3--Vmlldzo3OTg2NjM2
)

そんなWandBが個人開発/小規模プロジェクトでは **無料** で利用できます。ありがたや。
そのためか分析コンペの公開ノートブックでもWandBの実装コードをよく見かけますね。


# WandBの使い方
## 登録からAPIキーの取得
以下のホームページへアクセスし、右上の`サインアップ`から登録します。
[Weights & Biases \- 機械学習開発者のためのコラボレーションプラットフォーム](https://www.wandb.jp/)

サインアップ後、ログインが完了したら右上の自分のアイコン > User settings と進み、下の方にスクロールすると`Danger zone`配下の`Reveal`をクリックしてAPIキーを表示させ、コピーしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/5a854ebe-3178-8771-8360-b7559602d82b.png)
このAPIキーは外部環境からWandBにアクセスする際の認証に必要となります。

:::note warn
APIキーは秘密情報なので公開notebookなどにアップロードしないよう注意してください。
:::

## WandBのインストール
uvなどパッケージ管理ツールなどを介してインストールします。以下はpipの例です。
```bash
pip install wandb
```

## 実装
スクリプト、ノートブックの中で以下のよう書くことでwandbをインポート、ログインができます。
```python
import wandb

wandb.login(key="<APIキー>")
```
`<APIキー>`の部分は事前に取得したAPIキーに差し替えてください。

:::note warn
GitHubなどリモートリポジトリで管理する場合はAPIキーをそのままプッシュするなどしないように注意してください
:::

学習開始の前に以下を実行してwandbの記録をする箱を作成します。
```python
wandb.init(
    project="<project名>",
    name="<実験名>",
    notes="<実験メモ>",
    config = <ハイパーパラメータ設定内容など>
)
```
`project`は実験管理する箱の名前になります。ここからはプロジェクトと呼びます。
`name`はプロジェクトの下の階層で管理される実験名、`notes`はその実験名に紐づくメモ、そして`config`はハイパーパラメータ設定など実験時に使用した値を格納できます。

具体例を書くと私の場合はこのような書き方をしています。
```python
wandb.init(
    project="hogeコンペ",
    name="exp001",
    notes="hoge説明変数を作成",
    config = {
        "objective": "binary:logistic", 
        "boosting": "gbtree", 
        "learning_rate": 0.02,
        "eval_metric": "auc",         
    }
)
```
`project`に参加コンペ名、`name`には実験連番、`notes`に実験内容、`config`にはハイパーパラメータの設定を書いています。

モデルの学習をし、評価データを使って評価指標の出力値を`score`に格納したとします。そのスコアを記録する場合、以下のように書きます。
```python
wandb.log({"score": score})
```
辞書のキーの文字列（"score"部分）がWandBで格納される列に該当します。そうすることでログがWandBに記録されます。

全て完了したら以下実行で記録が終了します。
```python
wandb.finish()
```

上記の実装に少し手を加えてまとめると以下のようになります。
```python
# 必要ライブラリは事前にインポート済み
# X, yには説明変数と目的変数、train_idx, valid_idxにはtrain/validのインデックスが格納
# 学習モデルをxgboost, 評価指標はauc

# ハイパラ設定を変数へ格納
params = {
    "objective": "binary:logistic", 
    "boosting": "gbtree", 
    "learning_rate": 0.02,
    "eval_metric": "auc",         
}

# wandb初期化
wandb.init(
    project="hogeコンペ",
    name="exp001",
    notes="hoge説明変数を作成",
    config = params
)

# 学習データ分割
X_train = X.iloc[train_idx, :]
y_train = y.iloc[train_idx]
X_valid = X.iloc[valid_idx, :]
y_valid = y.iloc[valid_idx]

# xgboost専用のデータ形式に変換
dtrain = xgb.DMatrix(X_train, label=y_train, enable_categorical=True)
dvalid = xgb.DMatrix(X_valid, label=y_valid, enable_categorical=True)

model = xgb.train(cfg.params,
                  dtrain,
                  num_boost_round=20000,
                  evals=[(dtrain, "train"), (dvalid, "valid")],
                  early_stopping_rounds=100,
                  verbose_eval=100
)

# バリデーションデータに対する予測値を格納
preds_valid = model.predict(dvalid)
# aucをscoreに格納
score = auc(y_valid, preds_valid)

wandb.log({"score": score})
wandb.finish()

```
実行結果を確認するため、WandBのサイトを確認します。
ログイン後、プロフィール画面から`Projects`タブを選択すると、先ほどnameで指定したprojectが作成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/c84ce7b1-9333-3b3d-ecce-817de55c33a3.png)

`Workspace`では記録したデータの可視化がされたページです。初期からデータから推測された形でグラフ化がされています。右上の`Add panel`から手動追加も可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/ed120c63-4b65-f18f-0bab-37b7254cd3e6.png)

`Runs`では記録したデータの一覧表が確認ができます。`Runtime`などWandBのデフォルトの列も複数存在しています。こちらも右上の`Columns`から列の選択が可能で自分が見たい指標に絞って確認することも可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/49748a87-45d2-f0ac-4173-a25ff1c1d7da.png)


学習モデルがLightGBMの場合、wandbライブラリでLightGBM専用のcallbackを持っているため、以下のように組み入れることができます。
```python
# 必要ライブラリは事前にインポート済み
# X, yには説明変数と目的変数、train_idx, valid_idxにはtrain/validのインデックスが格納
# 学習モデルをlightgbm, 評価指標はauc

# lightgbm専用callback
from wandb.integration.lightgbm import wandb_callback

# ハイパラ設定を変数へ格納
params = {
    "objective": "binary:logistic", 
    "boosting": "gbtree", 
    "learning_rate": 0.02,
    "eval_metric": "auc",         
}

# wandb初期化
wandb.init(
    project="hogeコンペ",
    name="exp002",
    notes="lightgbm callback",
    config = params
)

# 学習データ分割
X_train = X.iloc[train_idx, :]
y_train = y.iloc[train_idx]
X_valid = X.iloc[valid_idx, :]
y_valid = y.iloc[valid_idx]

# lightgbm専用のデータ形式に変換
lgb_train = lgb.Dataset(X_train, y_train)
lgb_valid = lgb.Dataset(X_valid, y_valid)


# lgbを学習
model = lgb.train(
    params,
    lgb_train,
    valid_sets=[lgb_train, lgb_valid],
    valid_names=["train", "valid"],
    num_boost_round=20000,
    callbacks=[
        lgb.early_stopping(stopping_rounds=100, verbose=True), 
        lgb.log_evaluation(100),
        wandb_callback()
    ]
)

# バリデーションデータに対する予測値を格納
preds_valid = model.predict(X_valid, num_iteration=model.best_iteration)
# aucをscoreに格納
score = auc(y_valid, preds_valid)

wandb.log({"score": score})
wandb.finish()

```
`lgb.train`メソッドのcallbacks引数に`wandb_callback()`を追加しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/8e5efb87-7251-26a0-d888-049f89bf01d0.png)

wandb_callback()関数の中で学習中のメトリクスが自動的にログされます。この表からも分かるように`wandb.log()`関数でscoreを記録せずとも、`best_score.valid.auc`列で自動的にscoreを記録してくれています。


最後に、交差検証のfold毎のログの記録をしたい場合は以下のように書けます。
```python
# 必要ライブラリは事前にインポート済み
# X, yには説明変数と目的変数、train_idx, valid_idxにはtrain/validのインデックスが格納
# 学習モデルをlightgbm, 評価指標はauc
# kfは5foldのKFold

project = "hogeコンペ"
exp_name = "exp003"
notes = "交差検証"

for val_fold, (train_idx, valid_idx) in enumerate(kf.split(X, y)):
    wandb.init(
        project=project,
        group=exp_name,
        name=f"{exp_name}-fold{val_fold}",
        notes=notes,
        config = cfg.params     
    )
    
    # 学習データ分割
    X_train = X.iloc[train_idx, :]
    y_train = y.iloc[train_idx]
    X_valid = X.iloc[valid_idx, :]
    y_valid = y.iloc[valid_idx]
    
    # lightgbm専用のデータ形式に変換
    lgb_train = lgb.Dataset(X_train, y_train)
    lgb_valid = lgb.Dataset(X_valid, y_valid)
    
    
    # lgbを学習
    model = lgb.train(
        params,
        lgb_train,
        valid_sets=[lgb_train, lgb_valid],
        valid_names=["train", "valid"],
        num_boost_round=20000,
        callbacks=[
            lgb.early_stopping(stopping_rounds=100, verbose=True), 
            lgb.log_evaluation(100),
            wandb_callback()
        ]
    )
    
    # バリデーションデータに対する予測値を格納
    preds_valid[valid_idx] = model.predict(X_valid, num_iteration=model.best_iteration)

    score = auc(y_valid, preds_valid[valid_idx])
    vl_scores.append(score)

    print(f"ROC-AUC(fold{val_fold}): {score:.4f}")

    wandb.log({"score": score})
    wandb.finish()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/932b315c-6915-4530-bef6-51cc8a1ee080.png)

こうすることでfold毎に実験を管理することができます。
例えば実験を跨いだfold毎で学習を比較したい場合、上記のSearch runsで内容を絞って確認すると見やすくなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/657f80be-6305-da71-37d5-791dca2b97f1.png)

Workspaceのグラフも同様に絞って確認することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/c4adc1be-6f38-4430-37aa-977d83590ce7.png)


また今回の実装では`wandb.init()`関数の引数に新しく`group`引数を追加しています。
```python
wandb.init(
    project=project,
    group=exp_name,
    name=f"{exp_name}-fold{val_fold}",
    notes=notes,
    config = params     
)
```

この引数を追加し、Groupを選択することでWandBの評価確認時にgroupで括ることができます。
そうすることで全foldがそのgroup単位で一つとして確認することができます。表示されている値は全foldの平均が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/695af9a0-9926-c2e2-5447-aa587ee80b4e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/736156f1-7660-b4ee-6e3f-02bee43d7911.png)

# まとめ
いつも使っているWandBについてまとめてみました。他にもArtifactsやSweepsなど便利機能はまだありますがまだ使いこなせてないので次回にでも。
Pytorch LightningやYoloなどにもcallbackが準備されていたりしていて使ってますが今後もますます利用することが多くなりそうです。
「私はこんな使い方してるよー」など皆さんの使い方も教えてもらえると嬉しいです。