【📖nbdev1入門】Dockerによる環境構築と作業手順


> 2022年8月1日 追記）本記事は`nbdev v1`の手順であり、`nbdev v2`の手順とは異なります。

nbdev2版はこちら
https://qiita.com/lilacs/items/19c9c572a0740c5fc548

# はじめに

nbdevはライブラリ作成の多くを自動化して、少人数での大規模プロジェクト開発をサポートしてくれる。しかし、その分環境構築が難しい。そこで、Dockerイメージを自作して、環境構築手順をまとめた。

また、nbdev独自の書き方が分からなかったので、書き方も自分なりにまとめた。


## nbdevとは

> 参考：[Welcome to nbdev | nbdev](https://nbdev.fast.ai/)
> * [Jupyter Notebook](https://jupyter.org/)でPythonライブラリを開発し、すべてのコード、テスト、ドキュメントを1か所にまとめることができるライブラリです。
> * [文芸的プログラミング環境](https://en.wikipedia.org/wiki/Literate_programming)
>     * コードスニペットとマクロを使用して、人間の言語でプログラムロジックを書き出すこと
>     * コードと一緒にプログラムの背後にある考えを明示的に記述するので、設計上の決定が明白になり、高品質のプログラムを作成できる。


## nbdevの機能

参考：[nbdevへようこそ| nbdev](https://nbdev.fast.ai/#Features-of-Nbdev)



* Jupyterノートブックからドキュメントを自動的に生成
    * Googleカスタム検索可能
    * バッククォートで囲んだキーワードは適切なドキュメントページに自動的にハイパーリンクされる
* pypiおよびcondaパッケージの公開を自動化
* ノートブックとソースコード間の双方向同期
    * IDEを使用してコードナビゲーションやクイック編集ができる
* ノートブックで直接テストを作成
    * テストグループを定義できる
        * 長時間実行されるテストを常に実行する必要がないようにするため
* 継続的インテグレーション（CI）でテストを自動的に実行
    * GitHub Actionsを使用
* ドキュメントホスティング
    * GitHubページを使用してドキュメントを無料で簡単にホストできる


# 環境構築について

## 動作確認時の環境



* Windows10 Pro
* WSL2 + Ubuntu （Nvidia Docker インストール済み）
  * インストール参考
    * [Windows 10 + WSL 2 でDocker環境を構築する（Docker Desktop有料化対策） - ecbeing labs（イーシービーイング・ラボ）](https://blog.ecbeing.tech/entry/2021/09/07/150000)
    * [Enable NVIDIA CUDA on WSL 2 | Microsoft Docs](https://docs.microsoft.com/ja-jp/windows/ai/directml/gpu-cuda-in-wsl)

## セットアップするもの

* GitHub プロジェクト
    * GitHub Pages　も有効にする
* Dockerイメージで対応するもの
    * Pythonランタイムとパッケージ（Jupyter/nbdev）
    * nbdevコマンド（`nbdev_build_lib`など）
    * jekyll　（静的サイトジェネレータ、ドキュメント作成のため）
* プロジェクトに設定するもの
    * git hooks
    * pip用インストールスクリプト（setup.py）

## 🐳nbdev用Dockerイメージ

- 自作したイメージ
  - `lilacs2039/fastai-nbdev`（GPU版　fastai・nbdevインストール済み）と`fastai/jekyll`の２つのイメージを、`docker compose up`で利用する。
  - GitHub　[https://github.com/lilacs2039/docker_fastai-nbdev](https://github.com/lilacs2039/docker_fastai-nbdev)
  - DockerHub　[https://hub.docker.com/repository/docker/lilacs2039/fastai-nbdev](https://hub.docker.com/repository/docker/lilacs2039/fastai-nbdev)


公式イメージもあるが、エラーが出た（2022年6月27日時点）のと、自分用のDockerイメージが欲しかったので自作した。

- 公式イメージ
  - DockerHub　[fastai/codespaces Tags | Docker Hub](https://hub.docker.com/r/fastai/codespaces/tags)
  - GitHub　[fastai/docker-containers: Docker images for fastai](https://github.com/fastai/docker-containers)
    - fastaiプロジェクトで使うDockerファイルをまとめて管理しているリポジトリ

# 環境構築手順

* 参考
    * 公式手順（簡易版）：[A Minimal Example | nbdev](https://nbdev.fast.ai/example.html)
    * 公式手順（詳細版）：[nbdev tutorial | nbdev](https://nbdev.fast.ai/tutorial.html)


## プロジェクト作成

### GitHubにテンプレートから新しいプロジェクトを作成
以下のリンクから作成可能。
[Create a New Repository from fastai/nbdev_template](https://github.com/fastai/nbdev_template/generate)
![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/70d8ddcf-e0fe-ed85-babf-fe97b76e2a06.png)

nbdevのテンプレート（[fastai/nbdev_template - GitHub](https://github.com/fastai/nbdev_template)）を自分のリポジトリに複製して、プロジェクトを開始できる。

### GitHub Pagesの有効化
Settings > Pages > Source > masterブランチを選択（masterをPagesのソースブランチにする場合）
![image9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/98876c4c-88b8-f9ab-5c16-ce03af1a4e8a.png)

### リポジトリをローカルに複製

```bash:bash
git clone (作成したプロジェクトのURL）
```

### [settings.iniを編集](https://nbdev.fast.ai/tutorial.html#Edit-settings.ini)
ライブラリをパッケージ化するのに必要な情報を設定する。

主な設定内容
```ini:settings.ini
# lib_name = your_project_name
# repo_name = name of github repo
# user = your_github_username
# description = A description of your project
# keywords = some keywords
# author = Your Name
# author_email = email@example.com
# copyright = Your Name or Company Name
# branch = The default branch of your GitHub repo (usually either master or main)
```

[fastai/fastai](https://github.com/fastai/fastai)リポジトリのように、Jupyter Notebookをnbsフォルダにまとめる場合は、以下の項目を設定すること

```ini:settings.ini
nbs_path = nbs
```

以下の項目も入力しておくこと。
一見なくても大丈夫そうだが、setup.pyの実行時にAssertionエラーになってしまうため。
参考：　[nbdev_template/setup.py](https://github.com/fastai/nbdev_template/blob/master/setup.py#L11-L12)　のcfg_keysとexpectedより。

```ini:settings.ini
version
description
keywords
author
author_email
lib_name
user
branch
license
status
min_python
audience
language
```

## 実行環境の構築（Dockerで）
参考　[https://github.com/lilacs2039/docker_fastai-nbdev](https://github.com/lilacs2039/docker_fastai-nbdev)

### 🐳Dockerイメージ取得・実行
プロジェクトルートで以下のコマンドを実行する。
（Windowsの場合は、wsl環境で実行のこと）

```bash:bash
# 本リポジトリの`docker-compose.yml`を取得
wget https://raw.githubusercontent.com/lilacs2039/docker_fastai-nbdev/main/docker-compose.yml
# 環境立ち上げ
docker compose up
```

※「pytorch/pytorch:1.11.0-cuda11.3-cudnn8-runtime」イメージを使うため、5GB以上のイメージサイズになるので注意。

### ローカルサーバへアクセス
* Jupyter Lab :[ http://localhost:8888](http://localhost:8888/)
* Jekyll :[ http://localhost:4000/(lib_name)//](http://localhost:4000/(lib_name)//)

## プロジェクトにGitフックのインストール
JupyterLab（[http://localhost:8888](http://localhost:8888/)）のTerminalから、以下のコマンドを実行する

```bash:bash
nbdev_install_git_hooks
```

ファイル`.git/hooks/post-merge`が追加される。
※実行エラーになっても、`.git/hooks/post-merge`ファイルはちゃんと作成されているかも。

# 開発手順
## Notebook編集・ドキュメントのプレビュー
### １．ドキュメントに編集内容を反映

```bash:bash
nbdev_build_docs
```

### ２．ドキュメントのプレビューをリロード
プレビュー：[http://localhost:4000/(lib_name)//](http://localhost:4000/(lib_name)//)
※プレビューがうまくいかないときは、必要に応じて`sudo make docs_serve`を実行のこと。

1. 古い内容がドキュメントに表示される場合、jekyllサーバを再起動すると直ることがある。
2. Ctrl+Cでサーバをいったん終了してから、本コマンドで再起動する。

## コミット
### １．コミット準備

```bash:bash
# モジュールのエクスポート（Notebookから実行）
nbdev_build_lib

# ドキュメントの出力
nbdev_build_docs

# テストの実施
nbdev_test_nbs

# Notebookをきれいに
nbdev_clean_nbs
```

- 備考
  * [nbdev_test_nbs](https://nbdev.fast.ai/test#nbdev_test_nbs)
    * すべてのノートブックのテストを並行実行できる
  * [nbdev_clean_nbs](https://nbdev.fast.ai/clean.html#nbdev_clean_nbs)
    * 実行番号などは除去される。アウトプットは残る。
    * Notebookの競合を避けるため。

### ２．コミットを実行する

## リリース

1. [GitHubへプッシュ](https://nbdev.fast.ai/tutorial.html#Commit-to-Github)
    1. GitHub Pagesにドキュメントがホストされる
    2. CIで自動実行されるもの
        1. テスト
        2. Pythonモジュール
        3. ドキュメント
    3. CIの注意点：以下の項目が設定されていないと、setup.pyの実行時にAssertionエラーが出てしまう。settings.iniに記述が必要。 
        4. `version description keywords author author_email lib_name user branch license status min_python audience language`
2. [PyPIへアップロード](https://nbdev.fast.ai/tutorial.html#Upload-to-pypi)
    4. ここまでの作業で、プロジェクト用に完全にpypiに準拠したインストーラーをすでに作成済み。
    5. PyPIアップロードの準備
        5. [PyPI](https://pypi.org/)にアカウント登録し（まだの場合）、ログイン情報を含むファイルを作成する。
        6. 「~/.pypirc」に以下の内容を記述する。
            1. [pypi]
            2. username = your_pypi_username
            3. password = your_pypi_password
    6. PyPIへのアップロード
        7. プロジェクトルートで以下のコマンドを入力
            4. make pypi
            5. （ホントは「make release」コマンドによるPyPI/Conda両方へのアップロードが推奨）


# 📖nbdevの記述方法
## 概要
🪐JupyterNotebookだけを編集しながら、アウトプットの「🐞テスト・🐍ライブラリ・📃ドキュメント」を意識して記述すること。

* 🐞テスト
    * 書き方：Notebookにassert文を記述
    * Notebookの正常な実行をもってテストとする。
* 🐍ライブラリ(*.py)
    * 書き方
        * モジュール分割：Notebookの先頭に`# default_exp (モジュール名)`を記述
        * 関数・クラス定義：`#export`をつけたセルが出力される。
* 📃ドキュメント
    * 書き方：Notebookのほぼ全ての内容がドキュメントに出力される。（`#hide`で抑制）
    * 見出しを付けて適切な論理構造でNotebookを記述すれば、そのまま読みやすいドキュメントになる。


## ライブラリ・ドキュメントへの出力制御

セルのキーワードによって、出力するか・しないかを切り替えることができる。

|                        | **🐍ライブラリ** | **📃ドキュメント**               |
| ---------------------- | -------------- | ------------------------------ |
| **（キーワードなし）** | ❌しない         | ✅する                           |
| **#export**            | ✅する           | ✅する（クラス・関数定義のみ？） |
| **#hide**              | ❌しない         | ❌しない                         |
| **#export**  **#hide** | ✅する           | ❌しない                         |


## 記述例

nbdevで、Notebookに記述した内容がどのようにドキュメントに変換されるか比較した。

 

| | **🪐Notebook** | **📃ドキュメント** |
| ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------ |
| **（参考URL）** | https://github.com/fastai/deck_of_cards/blob/master/00_card.ipynb | https://fastai.github.io/deck_of_cards/card.html |
| **モジュール名の指示** | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/091519fa-4a70-ec74-8666-ae9abdbf1698.png) <br /> モジュール名：deck_of_cards.card <br />出力ファイル名：deck_of_cards/card.py　※ | - |
| **見出し１** | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/3d1916b9-3272-b7ae-4bfb-e4a4ff3e12b5.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/65630a89-337b-4aa5-c8f8-3e0de2ed23c8.png) |
| **目次** **（自動生成）** | - | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/55aeddb2-2a60-8cee-3628-297a1ef65a5f.png) |
| **import****文** **（出力しない）** | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/25a5c471-c387-9330-2f70-77537cbfb6bb.png) | -|
| **クラス定義** **（コード） ** | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/d6584901-e43c-e830-68d2-674a9859524a.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/bd0fda16-ccf4-d253-fa1c-45fa22814902.png) |
| **説明** **（マークダウン）** | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/399c2862-3773-f462-77ff-06e42c7ffaac.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/7d340e24-5fd2-a30e-51f4-5e6698b5aac5.png) |
| **実行例** **（コード）** | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/f2f455d0-4a25-dd34-d595-9f62f05234ad.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/04db3b14-bd75-641d-f440-54267bf18366.png) |
| **assert****文によるテスト** **（コード）** | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/a8c897fd-7fc1-74aa-d3a9-78626345f24d.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/209efe72-fa95-219c-7020-3abad2dd5efd.png) |
| **（中略****）** | （中略） | （中略） |
| **show_docs** （ドキュメントに関数定義を表示） | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/ae14aed7-a732-79dd-d81c-b8df578f86e0.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/d870f2f5-df3d-2728-eb6a-80ffb16310c1.png) |

※本例のライブラリ名は`deck_of_cards`

## 入力・出力ファイルの対応
以下のような対応で、入力ファイル（Jupyter Notebook）に対して出力ファイル（ライブラリ・ドキュメント・README.md）が生成される。

| **🪐Notebook**    | **🐍ライブラリ** | **📃ドキュメント**                              | **その他** |
| --------------- | -------------- | --------------------------------------------- | ---------- |
| **index.ipynb** | -              | docs/index.html（ドキュメントのトップページ） | README.md  |
| ***.ipynb**     | *.py           | docs/*.html（ドキュメント）                   | -          |

# 便利機能

##  nbdev_clean_nbs
重要でないメタデータを除去して、Notebookをコンフリクトしにくくする。
Outputなどは変わらない。

| **全部**        | **Notebook****指定**                  |
| --------------- | ------------------------------------- |
| nbdev_clean_nbs | nbdev_clean_nbs  --fname (Notebook名) | 

| **Before**                                                   | **After**                                                    |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/6ec3c119-2853-f8b7-2ddf-243ca4b5a1f4.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186338/d407f4bc-915c-0177-a4e4-e3859487c519.png) |
| ...<br>   "execution_count": 5,<br>... |  ...<br>   "execution_count": null,<br>... | 


# 所感
* nbdevは、[少人数で大規模プロジェクトを開発できることを目標にしている。](https://nbdev.fast.ai/#:~:text=Furthermore%2C%20nbdev%20allows%20a%20very%20small%20number%20of%20developers%20to%20maintain%20and%20grow%20a%20large%20ecosystem%20of%20software%20engineering%2C%20data%20science%2C%20machine%20learning%20and%20devops%20tools.)</span>
    * 反面、小さいパッケージを作りたい時は、機能が多すぎで余分な作業が多いかもしれない。（PyPIへの登録やGitHub Pagesでのドキュメントなど）
* 以下の整合性を同時に取りながらノートブックを記述するのが難しく慣れが必要。
    * Notebook：ソースコード(*.ipynb)の可読性は問題ないか
    * ライブラリ：モジュール(*.py)に必要なコードをexportしているか
    * ドキュメント：順番に読んだとき意味の通る内容になるか
    * テスト：コード（*.ipynb）の実行は成功するか
* これをするには出力制御構文（#export, #hide, show_doc(...)）の挙動を正しく理解する必要がある
* 成果物を同時に作るのが難しい分、成果物の良くない点はソースコード(*.ipynb)の作成中に気づくので、すぐ修正できる。結果、PDCAが速く回り、生産性が向上する。