【祝】強化学習ライブラリTF2RL v1.0到達 ~CIやドキュメントサイトなど開発・利用環境の整備~

# 1. はじめに

[以前紹介した](https://qiita.com/ymd_h/items/0f1be143546ccd4f4c33)友人が開発している[TensorFlow 2.x](https://www.tensorflow.org/) 向け強化学習ライブラリ[TF2RL](https://github.com/keiohta/tf2rl)が、諸々整備してバージョン1.0に到達しました🎉

[![TF2RL](https://github-readme-stats.vercel.app/api/pin/?username=keiohta&repo=tf2rl)](https://github.com/keiohta/tf2rl)
(いつの間にかスターも300超えていてすごい！)


バージョン1.0到達以降も、まだまだ様々な強化学習アルゴリズムを追加しようと開発が進んでいます。（この記事を準備している間にも、v1.1.0が公開されてます。）


インストール方法や基本となる使い方は、[公式ReadMe](https://github.com/keiohta/tf2rl/blob/master/README.md)や、[前の記事](https://qiita.com/ymd_h/items/0f1be143546ccd4f4c33)を読んでいただければと思うので、この記事では割愛します。


この記事では、私もお手伝いさせてもらって整備したアルゴリズム以外の部分について紹介します。


# 2. マルチプラットフォームテスト ([PR 97](https://github.com/keiohta/tf2rl/pull/97))

GitHub Actions によって、Windows/macOS/Ubuntu のマルチプラットフォームで、push や pull requestの度にユニットテストを自動で走らせれるようになりました。

特に、Windowsの開発機を友人も私も持っておらず、なかなかサポートできていなかったのですが、このActionsによってWindows上での問題があぶり出されWindowsでも無事動作できるようになりました。
（Windowsでは、`multiprocessing.Pool` などを利用する際に渡すことができるのは、ネストしていないモジュール直下で定義されたクラス・関数だけみたいですね。なんでも `fork()`システムコールの代わりに利用している [pickleのシリアライズが原因](https://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled)みたいですが、なんでそこに制約があるのかまでは理解できていません。）

同じコードを複数のTFとPythonのバージョンの関係もあって、下のように綺麗なマトリックス戦略になっていないのですが、もうちょっとうまく記述する方法はありませんかね。

```yaml:.github/workflows/test.yml(抜粋)
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python: ['3.7', '3.8']
        TF: ['2.2', '2.3']
        include:
          - os: ubuntu-latest
            python: '3.7'
            TF: '2.0'
          - os: ubuntu-latest
            python: '3.7'
            TF: '2.1'
          - os: macos-latest
            python: '3.7'
            TF: '2.0'
          - os: macos-latest
            python: '3.7'
            TF: '2.1'
          - os: windows-latest
            python: '3.7'
            TF: '2.0'
          - os: windows-latest
            python: '3.7'
            TF: '2.1'
```


# 3. Super-Linterでコードの自動チェック ([PR 102](https://github.com/keiohta/tf2rl/pull/102))

[GitHub Super-Linter](https://github.com/github/super-linter)を利用し、元々手作業で行っていたLinterでのコードのルールチェックが自動化されました。


Super-Linterで躓いたのは、**カスタム設定のファイルを `.github/linters/` ディレクトリに置かないといけないことでした。** カスタム設定のファイル名自体は、Super-Linterの起動時のオプションで指定できますが、あくまで上記のディレクトリにおいていないと見つからずデフォルトの設定が使用されてしまいます。（最終的にわからなくて[質問](https://github.com/github/super-linter/issues/541)しました。）


# 4. Sphinx を利用したドキュメントサイト構築 ([PR 107](https://github.com/keiohta/tf2rl/pull/107))

GitHub Actionsの中で、[Sphinx](https://www.sphinx-doc.org/en/master/)を利用してドキュメントを自動生成しています。

docstring から自動でクラスリファレンスを生成する [autodoc](https://www.sphinx-doc.org/ja/master/usage/extensions/autodoc.html) とMarkdown形式文書を取り込む [recommonmark](https://www.sphinx-doc.org/ja/master/usage/markdown.html) を有効にしています。(テーマは[Read The Docsテーマ](https://github.com/readthedocs/sphinx_rtd_theme)としました。)

```python:conf.py
from recommonmark.transform import AutoStructify


extensions = ["sphinx.ext.autodoc", "recommonmark"]
html_theme = "sphinx_rtd_theme"


def setup(app):
    app.add_transform(AutoStructify)
```

ちょっとだけつまづいたのは、 `sphinx-apidoc` コマンドで autodocの雛形となる一式をソースコードから生成するのですが、**自動生成される `index.rst` が 手作りの `index.md` より優先されて**しまって、うまく表示ができなかったので（しばらく悩んだあとに） `index.rst` を削除するコードを追加しすることで表示できるようになりました。

GitHub Pagesへの公開は、MasterブランチかつFork先ではない時に、[peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages)を利用して公開しています。
（どうも、`gh-pages` というブランチを作ってそこに生成物をpushする仕組みみたいです。なのでよくある `master` ブランチの doc以下を公開という設定とは異なります。）

```yml:.github/workflows/doc.yml（抜粋）
  - uses: peaceiris/actions-gh-pages@v3
    with:
      github_token: ${{ secrets.GITHUB_TOKEN }}
      publish_dir: ./public
    if: (github.ref == 'refs/heads/master') && (github.repository == 'keiohta/tf2rl')
```



Sphinxによって自動的に書き出されたクラスレファレンスは[こちら](https://keiohta.github.io/tf2rl/)
（クラスリファレンス以外のチュートリアルも自分で書きたいと聞いていたので、Markdownで書けるように環境は整備しましたが、まだ無いようです。。。乞うご期待。）


# 5. Docker と GitHub Container Registry を利用した構築済みコンテナ環境の提供 ([PR 111](https://github.com/keiohta/tf2rl/pull/111))

ログインせずともコンテナイメージをダウンロードできる[GitHub Container Registryがベータ公開](https://github.blog/jp/2020-09-07-introducing-github-container-registry/)されたのに併せて、構築済みコンテナ環境を利用できるようにしました。（それ以前の[GitHub Packages](https://docs.github.com/ja/free-pro-team@latest/packages/publishing-and-managing-packages/about-github-packages)はログインしないとpublicなイメージもダウンロードできないという謎仕様で、[フォーラム](https://github.community/t/docker-pull-from-public-github-package-registry-fail-with-no-basic-auth-credentials-error/16358)でも何人もの人がおかしいって言い続けていました。）

```shell
docker run -it ghcr.io/keiohta/tf2rl/cpu:v1.1.0 bash
```

また、Linuxオンリーかつマルチプロセスでの学習(ApeX)でうまく行かない例が散見されるのでexperimentalという扱いですが、NVIDIAのGPUを利用できるGPUコンテナバージョンも準備しています。
(私がMacBook Proしかなく、NVIDIA GPUの刺さったマシンを手元に持っていないためデバッグが難しく、環境をもった有識者が支援してくれると助かります。)

```shell
docker run --gpus all　-it ghcr.io/keiohta/tf2rl/nvidia:v1.1.0 bash
```

GPUコンテナについては、[こちらの記事](https://qiita.com/ksasaki/items/b20a785e1a0f610efa08)が非常に詳しくてためになりました。

コンテナイメージのビルドは以下のように設定してあり、tagつきでpushされたら、ビルドしてそのtag名をもったコンテナイメージをGitHub Container Registryに格納する仕組みになっています。

```yml:.github/workflows/docker.yml
name: docker

on:
  push:
    tags:
      - '**'

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      DOCKER_BUILDKIT: 1
    steps:
      - uses: actions/checkout@v2
      - uses: docker/build-push-action@v1
        with:
          registry: ghcr.io
          repository: ${{ github.repository_owner }}/tf2rl/cpu
          tag_with_ref: true
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GHCR_TOKEN }}
          dockerfile: Dockerfile
          always_pull: true
      - uses: docker/build-push-action@v1
        with:
          registry: ghcr.io
          repository: ${{ github.repository_owner }}/tf2rl/nvidia
          tag_with_ref: true
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GHCR_TOKEN }}
          dockerfile: Dockerfile.nvidia
          always_pull: true
```

[docker/build-push-action](https://github.com/docker/build-push-action)は v2 があるのですが、ログインが分離されて二度手間で、tagを手動で切り出さなければならず、使い勝手が悪かったので、あえて v1 で固定しています。

また、ベータ版だからなのか、GitHub Container Registryは、GitHub Actionsの自動生成されるシークレットでは認証が通らないので、**Personal Access Tokenを作って設定する**必要があります。

もうひとつ、忘れがちなのはGitHub Container Registry にpushされたイメージは初めはプライベートになっています。**皆が使えるようにするには、`https://github.com/<ユーザー名>?tab=packages` から指定のイメージを選んで、ポチポチ公開設定する**必要があります。 


# 6. `Trainer` をコマンドラインプログラム以外 (Jupyter Notebook等) からも実行可能に ([PR 105](https://github.com/keiohta/tf2rl/pull/105))

TF2RLは元々コマンドラインからのスクリプト実行を前提とした作りになっており、構築したモデルを学習させる `Trainer` クラスは `argparse` と強く結合してしまっており、Jupyter Notebook上で利用するには（できなくはないが）ちょっと面倒であった。

後方互換性を維持するかつ元のコードへの変更点を最小限にするために、`argparse` ではなく `dict` が渡されたときには空の `Namespace` を構築してその中に `dict` 指定のデータを詰め込むことにした。こうすることで、わざわざコマンドラインパラメータを模擬した文字列のリストを経由しないで、パラメータを設定できる。

```python:tf2rl/experiments/trainer.py(抜粋)
        if isinstance(args, dict):
            _args = args
            args = policy.__class__.get_argument(Trainer.get_argument())
            args = args.parse_args([])
            for k, v in _args.items():
                if hasattr(args, k):
                    setattr(args, k, v)
                else:
                    raise ValueError(f"{k} is invalid parameter.")
```


# 7. TensorFlow Probability のバージョンの自動指定 ([PR 113](https://github.com/keiohta/tf2rl/pull/113))

最近TF2RLがTensorFlow Probability (TFP) を利用するようになったのですが、TFPは動作するTensorFlow (TF)のバージョンが決まっている割にはインストール時にいい感じにしてくれなくて、何も考えずにインストールするとバージョン非互換でエラーが発生することがありました。

そこで、TF2RLの setup.py に以下のコードブロックを追加し、ユーザーがインストール済みのTFのバージョンに応じて、インストールすべきTFPのバージョンを切り替える方式を導入しました。

```python:setup.py（抜粋）
tf_version = "2.3"  # Default Version
compatible_tfp = {"2.3": ["tensorflow~=2.3.0",
                          "tensorflow-probability~=0.11.0"],
                  "2.2": ["tensorflow-probability~=0.10.0"],
                  "2.1": ["tensorflow-probability~=0.8.0"],
                  "2.0": ["tensorflow-probability~=0.8.0"]}

try:
    import tensorflow as tf
    tf_version = tf.version.VERSION.rsplit('.', 1)[0]
except ImportError:
    pass


install_requires = [
    "cpprb>=8.1.1",
    "setuptools>=41.0.0",
    "numpy>=1.16.0",
    "joblib",
    "scipy",
    *compatible_tfp[tf_version]
]
```

`~=` は、バージョンの最後の数字はアップデートを許容するシンタックスで、この場合はメジャーバージョンやマイナーバージョンの更新は許可しないが、バグフィックス（だと思われる）更新は許可するためにこうしてあります。

TF2.4 が出たらエラーになりますが、それは他のTF2.4変更対応とともに対処することになるでしょう。


# 8. 最後に

TensorFlow は書きにくいから [PyTorch](https://pytorch.org/) って思っている方も結構いるようですが、TF 2.0 以降非常に書きやすくなっていると個人的には思います。

TF2RLのユーザーがもっと増えたらいいなと思っています。

TF2RLでも利用している経験再生(Experience Replay)用のライブラリ [cpprb](https://gitlab.com/ymd_h/cpprb) ([GitHubミラー](https://github.com/ymd-h/cpprb)) を開発しています。
こちらもぜひ興味を持ってもらえると嬉しいです。

[![cpprb](https://github-readme-stats.vercel.app/api/pin/?username=ymd-h&repo=cpprb)](https://github.com/ymd-h/cpprb)