【Docker】Optuna Dashboardの構築

こんにちは。Optuna Dashboardというものがあると知り、それをDockerで環境構築しながら可視化を試みます。

# やりたいこと
**★Jupyterlab上でOptunaを用い最適化したモデルの学習過程をOptuna Dashboardで可視化するDocker環境を構築すること（GPU使用）**

上記を実現しようとすると様々な工程があるので、細かく区切って考えていきます。

**完成イメージ**
このようなダッシュボードをWEBブラウザ上で見る
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830602/2be8f0ca-4c6a-0c1d-d39f-9019101bca5c.png)

# 流れ
- **Docker関連**
    - ディレクトリを作成
    - GPU環境を構築
    - docker-compose.ymlの作成
    - Dockerfileの作成
    - requirement.txtの作成
⇒ コンテナを構築・起動させる
- **学習関連**
    - データベースを作成
    - jupyterlabでoptunaのstudyを作成する
    - Optuna dashboardに接続
⇒ ダッシュボード完成！

# Docker関連
## ディレクトリを作成
このディレクトリがDocker Composeのプロジェクト名に使われます。このプロジェクト名によって、個々の環境が分離されますので、なんのコンテナか分かりやすく作ります。今後作るファイルはこのディレクトリに保存します。

```markdown
project/
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
└── data/ 
```

## GPU環境を作成
私はこのサイトを見ながら行い、無事コンテナ内にCUDA 12.1がインストールするところまでできました。
https://highreso.jp/edgehub/machinelearning/ubuntudocker.html#index_id5

## docker-compose.ymlの作成
コンテナを作成するには、どういうコンテナを作成するのかYAML（ヤムル）形式のファイルで定義します。
今回は以下4つのサービスを構築していきます。
- **jupyterlab**：機械学習のコードを書いて実行する開発環境
- **PostgreSQL**：Optunaの試行データを保存するデータベース
- **pgAdmin**：PostgreSQLを視覚的に管理するためのGUI
- **Optuna Dashboard**：Optunaの試行結果を可視化するためのウェブインターフェース

```yaml
services:
  gpu-container:
    container_name: XX-gpu-container
    build:
      context: .
      dockerfile: Dockerfile
    runtime: nvidia
    environment:
      - NDIVIA_VISIBLE_DEVICES-all
      - NDIVIA_DRIVER_CAPABILITIES=compute,utility
      - JUPYTER_ENABLE_LAB=yes
    restart: unless-stopped
    stdin_open: true
    tty: true
    ports:
      - "XXXX:8888"  # JupyterLab
    # Dockerコンテナとホストマシン（ローカルPC）の間でディレクトリを共有する設定
    volumes:
      - ./notebooks:/home/jovyan/notebooks
    command: >
      jupyter lab --allow-root --ip=0.0.0.0 --port=8888 --NotebookApp.token=''
    networks:
      - XX-network

  postgres:
    image: postgres:17-bullseye ＃イメージからコンテナを作成
    container_name: XX-postgres
    environment:
      POSTGRES_USER: optuna_user
      POSTGRES_PASSWORD: optuna_password
      POSTGRES_DB: optuna_db
    ports:
      - "XXXX:5432"  # 外部ポート XXXX -> 内部ポート 5432
    volumes:
      - postgres_data:/var/lib/postgresql/data # データを永続化するため
    restart: always
    networks:
      - XX-network

  pgadmin:
    image: dpage/pgadmin4 # イメージからコンテナを作成
    container_name: XX-pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: XXXXXXXX@XXXX.com  # pgAdmin のログイン用メールアドレス（任意）
      PGADMIN_DEFAULT_PASSWORD: XXXX          # pgAdmin のログイン用パスワード（任意）
    ports:
      - "XXXX:80"  # 外部ポート XXXX -> 内部ポート 80
    depends_on:
      - postgres # PostgreSQLサービスが起動していることを前提に動作
    restart: always
    networks:
      - XX-network
  
  optuna_dashboard:
    image: ghcr.io/optuna/optuna-dashboard # イメージからコンテナを作成
    container_name: XX-optuna_dashboard
    ports:
      - "XXXX:8080"  # 外部ポート XXXX -> 内部ポート 8080
    depends_on:
      - postgres
    restart: always
    networks:
      - XX-network

volumes:
  postgres_data: # ホストにデータを保存

networks:
  XX-network:
    driver: bridge
```
コンテナ名とポート番号（外部）は任意です。今回はXXとして記載しているので適宜ご変更ください。
構成はこんな感じです。
```yaml
services:
  jupyter: # コンテナ名
    image: # イメージ名
    port: # ポート番号
    environment: # 環境変数の設定
    depends_on: # コンテナ間の依存関係
    restart: # コンテナの再起動の設定
volumes: # コンテナに接続するボリューム
```
### コンテナとは？イメージとは？
- コンテナは独立した実行環境
- イメージはコンテナを作成するための指示が記載された読み取り専用のテンプレート
⇒ 今回は、`image`を指定しており、Docker Hub（公式のイメージリポジトリ）やGitHub Container Registry (GHCR)かを参照してダウンロードしています

## Dockerfileの作成
```dockerfile
# GPU版
FROM nvidia/cuda:12.1.1-base-ubuntu22.04

# システムパッケージの更新と必要なパッケージのインストール
RUN apt-get update && apt-get install -y \
    sudo \
    wget \
    vim \
    gcc \
    libpq-dev \
    python3.11 \
    python3-pip \
    python3.11-dev && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# requirements.txtをコピー
COPY requirements.txt /tmp/requirements.txt

# Pythonパッケージのインストール
RUN pip install --upgrade pip && \
    pip install -r /tmp/requirements.txt && \
    pip install jupyterlab ipykernel && \
    rm /tmp/requirements.txt

# CUDA 12.1
RUN pip install torch==2.4.1 torchvision==0.19.1 torchaudio==2.4.1 --index-url https://download.pytorch.org/whl/cu121

# 作業ディレクトリを設定
WORKDIR /notebooks

# jupyterlabの起動コマンドを設定
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--allow-root"]
```
torch のバージョンによっては 異なる CUDA バージョンが必要になるので確認が必要です。
公式サイト: https://pytorch.org/get-started/previous-versions/
torch のバージョンに対応する CUDA を確認する方法:
```bash
python -c "import torch; print(torch.version.cuda)"
```

## requirement.txtの作成
開発環境を管理するために使うライブラリを記載します。
あくまで例となりますので、ご自身が必要なものを入れてください。
```text
numpy==1.26.4
pandas==2.1.4
seaborn==0.13.2
matplotlib==3.5.3
japanize-matplotlib==1.1.3
scikit-learn==1.5.2
sqlalchemy==2.0.37
psycopg2-binary==2.9.10
optuna==4.1.0
optuna-dashboard==0.17.0
ipywidgets==8.0.7
```
※ psycopg2もしくはpsycopg2-binaryのインストールは必ず必要です！
- **psycopg2** ⇒ 本番環境
PythonでPostgreSQLデータベースと接続するためのライブラリ。
SQLクエリの実行やデータベースの操作を簡単に行える。
- **psycopg2-binary** ⇒ 開発環境
psycopg2 のバイナリパッケージ版。
ビルド済みのC拡張モジュールが含まれているため、依存ライブラリやコンパイルが不要で簡単にインストールできる。

## コンテナを構築・起動させる
下準備ができました。
ここから、imageを実際に構築していきます。
まずは、ターミナル(Windowsの場合)を開いて、実行環境のディレクトリに移動します。
- コンテナを構築・起動させる
```powershell
docker-compose up -d --build
```
今のままでは`relation "version_info" does not exist`や`FATAL: database "optuna_db" does not exist`などのエラーメッセージが出ると思いますが、次で対応していきます。

# 学習関連
## データベースを作成
今回のoptuna dashboardはデータベースからデータを取ってくるような形になっています。
データベースはコマンドからも作れますが、pdadminにアクセスしてGUI操作をしていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830602/b404d291-ddc8-492b-48b5-012121777e13.png)
ログイン時はdocker_compose.ymlで指定したメールアドレスとパスワードを入力します。
```yml
PGADMIN_DEFAULT_EMAIL: XXXXXXXX@XXXX.com  # pgAdmin のログイン用メールアドレス（任意）
PGADMIN_DEFAULT_PASSWORD: XXXX          # pgAdmin のログイン用パスワード（任意）
```

1. `Add New Server`をクリック
1. `General`タブ
    - Name：XX-postgres
     ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830602/c63aa5ad-671b-e014-6f82-ad979b84d8cc.png)
1. `Conection`タブ
    - Host name/address：XX-postgres
    - Port：5432
    - Maintanance database：optuna_db
    - Username：optuna_user
    - Password：otpuna_password
※上記はDocker compose.ymlに書かれているものです

コマンド操作はこちら
```bash
docker exec -it XX-postgres psql -U optuna_user -d postgres
```

```sql
CREATE DATABASE optuna_db;
CREATE USER optuna_user WITH PASSWORD 'optuna_password';
GRANT ALL PRIVILEGES ON DATABASE optuna_db TO optuna_user;
```

データベースができたら確認してみます。
```bash
docker exec -it XX-postgres psql -U optuna_user -d optuna_db
```
optuna_dbの中に入れたら成功です！

## jupyterlabでoptunaのstudyを作成する
まず、データベースに接続するために、先述した**psycopg2**をインポートする必要があります。
```python
import psycopg2
```

これを実行して`ImportError: libpq.so.5: cannot open shared object file: No such file or directory`になる場合は、portfolio3-jupyterコンテナに入ってから、を手動でインポートします。
```bash
docker exec -it --user root portfolio3-jupyter bash
apt-get update && apt-get install -y libpq-dev
```
ちなみに、ルートユーザーでない場合は権限がないと出力されインストールできませんでした。
```bash
docker exec -it XX-jupyter bash
apt-get update && apt-get install -y libpq-dev

# エラー表示
Reading package lists... Done
E: List directory /var/lib/apt/lists/partial is missing. - Acquire (13: Permission denied)
```

では、otpunaのstudyを作り、テーブルを作成します。
これを行わないまま次項に進むと、`relation "version_info" does not exist`エラーが発生しました。
```python
import optuna
storage_url = "postgresql+psycopg2://optuna_user:optuna_password@XX-postgres:5432/optuna_db"
study = optuna.create_study(storage=storage_url, study_name="example_study", load_if_exists=True)
print("Optuna storage connected!")
```

### Optuna dashboardに接続
本来docker-compose.ymlのコマンドで指定するべきですが、私はコマンドの記載方法でエラーになってしまったので、ここでは手動でoptuna dashboardを起動させて、PostgreSQLのデータベースに接続します。
```bash
docker run -it --rm --network cpu-version_portfolio-network -p XXXX:8080 ghcr.io/optuna/optuna-dashboard postgresql+psycopg2://optuna_user:optuna_password@XX-postgres:5432/optuna_db --port 8080 --host 0.0.0.0
```

この後、実際に`http://localhost:XXXX`にアクセスしてみるとこのような画面が出てきます。XXXXはホストのポートであり、ここからコンテナの 8080 に繋がることになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830602/f6f8e802-0256-376b-ded8-1cbb3f29ca7a.png)

では実際に学習させてみましょう。

```python
def objective(trial):
    x = trial.suggest_float("x", -100, 100)
    y = trial.suggest_categorical("y", [-1, 0, 1])
    return x**2 + y

if __name__ == "__main__":
   
    # OptunaのStudyを作成
    study = optuna.create_study(
        storage=storage_url,
        study_name="example_study",
        load_if_exists=True  # Studyが既に存在する場合はロード
    )
    study.optimize(objective, n_trials=100)
    print(f"Best value: {study.best_value} (params: {study.best_params})")
```
`Best value: -0.7740110290467385 (params: {'x': -0.4753829729315739, 'y': -1})`が出力されました。
optuna dashboardものぞいてみると、きちんと出力されています！:v:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830602/75d8a505-cc0a-4a97-892a-619e77205c67.png)


# 番外編
今回GPUを使用する前提で書きましたが、GPUがないPCを使う場合は、gpu-countainerが必要ありません。
参考までに、CPU版のコードも記載します。コンテナの構成は同様ですが、コンテナの作り方が異なってきます。
## Dockerfile
```dockerfile
# ベースイメージを指定
FROM jupyter/base-notebook:python-3.11 AS builder
```
ベースがjupyterになります。

## docker-compose.yml
```yaml
services:
  jupyter:
    build:  # Dockerfile を利用する設定を追加
      context: .
      dockerfile: Dockerfile
    container_name: portfolio3-jupyter
    ports:
      - "XXXX:8888"  # JupyterLab
    # Docker コンテナとホストマシン（ローカルPC）の間でディレクトリを共有する設定
    volumes:
      - ./notebooks:/home/jovyan/work/notebooks
    environment:
      - JUPYTER_ENABLE_LAB=yes
    restart: unless-stopped 
    command: >
      jupyter lab --ip=0.0.0.0 --no-browser --NotebookApp.token='' --NotebookApp.password=''
    networks:
      - XX-network
```

以上です、読んでいただきありがとうございました。

# 参考
https://github.com/optuna/optuna-dashboard