そぬばこ

備忘録とか、多分そんな感じ。

高速かつ PEP 582 で仮想環境を捨てる Python パッケージマネージャ PDM を試す

この記事は Sansan Advent Calendar 2022 19日目の記事です。

前日は fujisyo32 さんの

zenn.dev

でした。

今年は特に画像周りで拡散モデルの話題で持ち切りでしたね。言語生成周りの研究も非常に興味深いです。

はじめに

私が所属する研究開発部では、Python のパッケージマネージャとして Poetry を標準的に利用しています。

github.com

Rust のように toml でパッケージを人間が認識しやすい形で管理できる点は非常に魅力的であり、setup.py, requirements.txt, setup.cfg, MANIFEST.in 等を代替できるため非常に便利です。 しかしながら最近、Poetry を用いたインストールやパッケージ追加等の依存解決に凄まじく時間を要しており、なんとか速度削減して開発のサイクルを早めることは出来ないかなと感じております。

そこで今回は、以前からあくまで個人的に試したいと思っていた高速なパッケージマネージャである PDM を試してみたいと思います。

PDM とは

公式のロゴ

PDM も Poetry と同様に pyproject.toml によってパッケージを管理する Python パッケージマネージャです。

github.com pdm.fming.dev

Poetry との大きな差異としては、次の2点が挙げられます。

  • PEP 582 に基づき、仮想環境を使わずに利用できる仕組みを搭載 (利用するかは選択可能)
  • 依存解決を含めて、動作が非常に高速

一方で CLI ツールとしては Poetry と操作感が非常に似ており Poetry ユーザであれば簡単に利用することが可能です。早速試していきましょう。

PDM を試す

紹介時の PDM のバージョンは 2.3.3 です。

インストール

インストール方法は公式HP等を参照してください。よくある curl で持ってくるやつです。

$ curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python
Installing PDM (2.3.3): Creating virtual environment
Installing PDM (2.3.3): Installing PDM and dependencies
Installing PDM (2.3.3): Making binary at /Users/nersonu/.local/bin
Usage: pdm [-h] [-V] [-c CONFIG] [-v] [-I] [--pep582 [SHELL]] {add,build,cache,completion,config,export,import,info,init,install,list,lock,publish,remove,run,search,self,plugin,show,sync,update,use,venv} ...

    ____  ____  __  ___
   / __ \/ __ \/  |/  /
  / /_/ / / / / /|_/ /
 / ____/ /_/ / /  / /
/_/   /_____/_/  /_/

Commands:
  {add,build,cache,completion,config,export,import,info,init,install,list,lock,publish,remove,run,search,self,plugin,show,sync,update,use,venv}
    add                 Add package(s) to pyproject.toml and install them
    build               Build artifacts for distribution
    cache               Control the caches of PDM
    completion          Generate completion scripts for the given shell
    config              Display the current configuration
    export              Export the locked packages set to other formats
    import              Import project metadata from other formats
    info                Show the project information
    init                Initialize a pyproject.toml for PDM
    install             Install dependencies from lock file
    list                List packages installed in the current working set
    lock                Resolve and lock dependencies
    publish             Build and publish the project to PyPI
    remove              Remove packages from pyproject.toml
    run                 Run commands or scripts with local packages loaded
    search              Search for PyPI packages
    self (plugin)       Manage the PDM program itself (previously known as plugin)
    show                Show the package information
    sync                Synchronize the current working set with lock file
    update              Update package(s) in pyproject.toml
    use                 Use the given python version or path as base interpreter
    venv                Virtualenv management

Options:
  -h, --help            show this help message and exit
  -V, --version         show the version and exit
  -c CONFIG, --config CONFIG
                        Specify another config file path(env var: PDM_CONFIG_FILE)
  -v, --verbose         -v for detailed output and -vv for more detailed
  -I, --ignore-python   Ignore the Python path saved in the .pdm.toml config
  --pep582 [SHELL]      Print the command line to be eval'd by the shell

Successfully installed: PDM (2.3.3) at /Users/nersonu/.local/bin/pdm

PDM でプロジェクトを立ち上げる

プロジェクトのルートディレクトリで pdm init すれば、対話式でプロジェクトを立ち上げることが出来ます。

$ pdm init
Creating a pyproject.toml for PDM...
Please enter the Python interpreter to use
0. /Users/nersonu/.pyenv/shims/python3 (3.10)
1. /Users/nersonu/.pyenv/shims/python (3.10)
2. /Users/nersonu/.pyenv/versions/3.10.9/bin/python3.10 (3.10)
3. /Users/nersonu/.pyenv/shims/python3.10 (3.10)
4. /Users/nersonu/.pyenv/shims/python3.9 (3.9)
5. /usr/local/bin/python3.9 (3.9)
6. /Users/nersonu/.pyenv/versions/3.9.13/bin/python3.9 (3.9)
7. /usr/bin/python3 (3.8)
8. /usr/bin/python2.7 (2.7)
9. /Users/nersonu/Library/Application Support/pdm/venv/bin/python (3.10)
Please select (0): 0
Using Python interpreter: /Users/nersonu/.pyenv/shims/python3 (3.10)
Would you like to create a virtualenv with /Users/nersonu/.pyenv/versions/3.10.9/bin/python3? [y/n] (y): y
Is the project a library that will be uploaded to PyPI [y/n] (n): n
License(SPDX name) (MIT):
Author name (nersonu):
Author email (nersonu@gmail.com):
Python requires('*' to allow any) (>=3.10): >=3.9
Changes are written to pyproject.toml.
$ ls
pyproject.toml

特徴的な点としては、利用する Python インタプリタを指定することです。 ここで指定したインタプリタは、.pdm.toml に書かれます。

[python]
path = "/Users/nersonu/.pyenv/shims/python3"

なお、ユーザごとに利用するインタプリタが異なることから、このファイルは git などで commit しないように気をつけましょう。

また、 Poetry や Rust の Cargo のように src ディレクトリや README ファイルは生成されません。

生成された pyproject.toml はこのようになっています。

[tool.pdm]

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "nersonu", email = "nersonu@gmail.com"},
]
dependencies = []
requires-python = ">=3.9"
license = {text = "MIT"}

pyproject.toml の形式は PEP 621 に基づいており、Poetry とは若干形式が異っていますね。

仮想環境については、 pdm init の際に生成するようにした (以下)

Would you like to create a virtualenv with /Users/nersonu/.pyenv/versions/3.10.9/bin/python3? [y/n] (y): y

ため、pdm run でルートに作られた .venv を利用して実行することができます。

$ pdm run which python
/Users/aomi/workspace/try_pdm/init_pdm/.venv/bin/python

パッケージの追加

poetry add と同じような形で pdm add でパッケージの追加が可能です。

$ pdm add numpy pandas
Adding packages to default dependencies: numpy, pandas
🔒 Lock successful
Changes are written to pdm.lock.
Changes are written to pyproject.toml.
Synchronizing working set with lock file: 5 to add, 0 to update, 0 to remove

  ✔ Install six 1.16.0 successful
  ✔ Install python-dateutil 2.8.2 successful
  ✔ Install pytz 2022.7 successful
  ✔ Install pandas 1.5.2 successful
  ✔ Install numpy 1.23.5 successful

🎉 All complete!
$ ls
pdm.lock pyproject.toml

pdm.lock が生成され、poetry.lock と同様に依存関係等がここに残されます。

また、 pyproject.toml は以下のように更新されています。

[tool.pdm]

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "nersonu", email = "nersonu@gmail.com"},
]
dependencies = [
    "numpy>=1.23.5",
    "pandas>=1.5.2",
]
requires-python = ">=3.9"
license = {text = "MIT"}

Poetry と同様に、開発用ライブラリとライブラリのグループ管理が可能です。 Poetry と若干異なるところは、開発用ライブラリをグループと同様に依存関係に含めるか、独立させるかオプションの指定の仕方で選べるところです。 今回は依存関係に含めない形で、開発用ライブラリとして pytest をインストールする形を紹介します。

$ pdm add -d pytest
Adding packages to dev dev-dependencies: pytest
🔒 Lock successful
Changes are written to pdm.lock.
Changes are written to pyproject.toml.
Synchronizing working set with lock file: 7 to add, 0 to update, 0 to remove

  ✔ Install exceptiongroup 1.0.4 successful
  ✔ Install iniconfig 1.1.1 successful
  ✔ Install tomli 2.0.1 successful
  ✔ Install pluggy 1.0.0 successful
  ✔ Install packaging 22.0 successful
  ✔ Install attrs 22.1.0 successful
  ✔ Install pytest 7.2.0 successful

🎉 All complete!

pyproject.toml では tool.pdm.dev-dependencies の項に追加されています。

[tool.pdm]
[tool.pdm.dev-dependencies]
dev = [
    "pytest>=7.2.0",
]

[project]
...

グループ追加 (ここでは "plot" という名前にしています) は以下のように行えます。

$ pdm add -G plot matplotlib seaborn
Adding packages to plot dependencies: matplotlib, seaborn
🔒 Lock successful
Changes are written to pdm.lock.
Changes are written to pyproject.toml.
Synchronizing working set with lock file: 13 to add, 0 to update, 0 to remove

  ✔ Install cycler 0.11.0 successful
  ✔ Install pyparsing 3.0.9 successful
  ✔ Install kiwisolver 1.4.4 successful
  ✔ Install contourpy 1.0.6 successful
  ✔ Install seaborn 0.12.1 successful
  ✔ Install fonttools 4.38.0 successful
  ✔ Install pillow 9.3.0 successful
  ✔ Install matplotlib 3.6.2 successful

🎉 All complete!

最終的な pyproject.toml は次のようになりました。

[tool.pdm]
[tool.pdm.dev-dependencies]
dev = [
    "pytest>=7.2.0",
]

[project]
name = ""
version = ""
description = ""
authors = [
    {name = "nersonu", email = "nersonu@gmail.com"},
]
dependencies = [
    "numpy>=1.23.5",
    "pandas>=1.5.2",
]
requires-python = ">=3.9"
license = {text = "MIT"}

[project.optional-dependencies]
plot = [
    "matplotlib>=3.6.2",
    "seaborn>=0.12.1",
]

なお、 project.optional-dependencies の中に dev を含めたい場合は -dG で出来るようです。

virtualenv との併用

pyenv local hoge のように、 pyenvpyenv-virtualenv を利用している場合の挙動を確認しておきましょう。

$ pyenv local try_pdm
$ pip -V
pip 22.3.1 from /Users/nersonu/.pyenv/versions/3.10.9/envs/try_pdm/lib/python3.10/site-packages/pip (python 3.10)

試しに pdm init してみると、仮想環境を生成する質問がなくなり、生成されなくなりました。

$ pdm init
Creating a pyproject.toml for PDM...
Please enter the Python interpreter to use
0. /Users/nersonu/.pyenv/shims/python3 (3.10)
1. /Users/nersonu/.pyenv/shims/python (3.10)
2. /Users/nersonu/.pyenv/versions/3.10.9/bin/python3.10 (3.10)
3. /Users/nersonu/.pyenv/shims/python3.10 (3.10)
4. /Users/nersonu/.pyenv/shims/python3.9 (3.9)
5. /usr/local/bin/python3.9 (3.9)
6. /Users/aomi/.pyenv/versions/3.9.13/bin/python3.9 (3.9)
7. /usr/bin/python3 (3.8)
8. /usr/bin/python2.7 (2.7)
9. /Users/nersonu/Library/Application Support/pdm/venv/bin/python (3.10)
Please select (0): Using Python interpreter: /Users/nersonu/.pyenv/shims/python3 (3.10)
Is the project a library that will be uploaded to PyPI [y/n] (n):
License(SPDX name) (MIT):
Author name (nersonu):
Author email (nersonu@gmail.com):
Python requires('*' to allow any) (>=3.10): >=3.9
Changes are written to pyproject.toml.

このまま pdm run でコマンド実行してみたところ、既に設定した仮想環境を利用できました。 既にプロジェクトで仮想環境が設定されている場合、認識して使ってくれるようですね。

PEP 582 の世界を体験する

PEP 582 とは

peps.python.org

__pypackages__ というディレクトリをルートに置いておき、ここに保存されているパッケージを使おうという考えです。 こうすることで、仮想環境作ってそこでライブラリを入れて……という一連の流れをスキップ出来ます。 PEP 582 に対応しているツールはほとんどなく、PDM はこれを実現している希少なソフトウェアと言えるかもしれません。早速体験してみます。

PDM で PEP 582 の機能を有効にする

公式ページには、 bash でのやり方が載っていますが、自分は zsh を使っているので適当に .zshrc に突っ込んでみます。

$ pdm --pep582 >> ~/.zshrc
$ exec $SHELL

プロジェクトを作ってみる

先に __pypackages__ ディレクトリを作っておきます。

$ mkdir __pypackages__

pdm init してみます。

pdm init
Creating a pyproject.toml for PDM...
Please enter the Python interpreter to use
0. /Users/nersonu/.pyenv/shims/python3 (3.10)
1. /Users/nersonu/.pyenv/shims/python (3.10)
2. /Users/nersonu/.pyenv/versions/3.10.9/bin/python3.10 (3.10)
3. /Users/nersonu/.pyenv/shims/python3.10 (3.10)
4. /Users/nersonu/.pyenv/shims/python3.9 (3.9)
5. /usr/local/bin/python3.9 (3.9)
6. /Users/nersonu/.pyenv/versions/3.9.13/bin/python3.9 (3.9)
7. /usr/bin/python3 (3.8)
8. /usr/bin/python2.7 (2.7)
9. /Users/nersonu/Library/Application Support/pdm/venv/bin/python (3.10)
Please select (0):
Using Python interpreter: /Users/nersonu/.pyenv/shims/python3 (3.10)
Would you like to create a virtualenv with /Users/nersonu/.pyenv/versions/3.10.9/bin/python3? [y/n] (y): n
You are using the PEP 582 mode, no virtualenv is created.
For more info, please visit https://peps.python.org/pep-0582/
Is the project a library that will be uploaded to PyPI [y/n] (n): n
License(SPDX name) (MIT):
Author name (nersonu):
Author email (nersonu@gmail.com):
Python requires('*' to allow any) (>=3.10):
Changes are written to pyproject.toml.

You are using the PEP 582 mode, no virtualenv is created.
For more info, please visit https://peps.python.org/pep-0582/

ちゃんと有効化されてそうです。試しに numpy でも追加してみましょう。

$ pdm add numpy
Adding packages to default dependencies: numpy
🔒 Lock successful
Changes are written to pdm.lock.
Changes are written to pyproject.toml.
Synchronizing working set with lock file: 1 to add, 0 to update, 0 to remove

  ✔ Install numpy 1.23.5 successful

🎉 All complete!
...

$ tree -L 3 __pypackages__
__pypackages__
└── 3.10
    ├── bin
    │   ├── f2py
    │   ├── f2py3
    │   └── f2py3.10
    ├── include
    └── lib
        ├── numpy
        └── numpy-1.23.5.dist-info

numpy が __pypackages__ 配下に入っていますね。面白い。

PEP 582 を実際に使用することはなかなか無いと思いますが、初学者にとって仮想環境に触れずに済むようなパッケージ管理は非常に魅力的です。 これを実現できる PDM なかなか良さげですね。

速度比較

前回の記事を流用します。実は、比較先の一番左が PDM でした。 nersonu.hatenablog.com

Python のパッケージマネージャの速度比較をしている Web ページから引用しています。画像は前2つは前回と同じです。

install 速すぎ 👍👍👍
Poetry とは比べ物にならない 👍👍

Poetry で一番ネックな部分である install 周りが超速いです。素晴らしいですね。

一方でパッケージの単純追加は Poetry に負けています (これだけ Poetry 1.3.1)。

add は Poetry に劣る
それにしても install が速いです。 Poetry を使っていると、コンテナをビルドする際の poetry install がとてつもなく時間がかかるため、こういった点は PDM に軍配が上がるんだろうなと感じています。

おわりに

今回は PDM を試しましたが、なかなか面白く、高速で良い感じでした。 いつか機会があれば業務でも導入してみたいですね。

明日は id:kur0cky さんです。皆様、よいクリスマスを。

Poetry 1.3.0 がリリースされたよ

この記事は whywaita Advent Calendar 2022 11日目の記事です。

前日は id:hinananoha さんの

kokura.hatenadiary.jp

でした。壊れると悲しいですよね。

ということで、今回は whywaita さんでなにかするネタが思いつかなかったため、よく壊れる Python のパッケージマネージャ Poetry の最新バージョンについて書こうと思います。

Poetry とは

Poetry は Python のパッケージマネージャで、 requirements.txtsetup.py を代替していい感じにパッケージングしたり toml ベースでわかりやすくライブラリ管理できるソフトウェアです。

github.com python-poetry.org

ぼくと Poetry

9月頃、1.2.0 がリリースされました。

社内向けのスライドでキレている様子

公式のアナウンスでは 1.1.x と 1.2.x 系で互換があるとのことでしたが、壊れてしまい、社内でキレていました。

1.3.0 リリース 🎉

2022年12月9日、1.3.0 がリリースされました。(そのぼくがキレていた Issue は解決していませんが、特にキレてはないです)

1.2 では git 経由のインストールで subdirectory オプションが使えるようになったり*1、pyproject.toml のグループ機能が拡充されたりと個人的にかなり楽しみな機能追加が多くありました。

今回は特に個人的に楽しみなアップデートは無いので、さらっと気になったものだけさらっていこうと思います。

作業ディレクトリを CLI のオプションで指定可能に

-C, -directory で作業ディレクトリを指定することが、CLI 上で出来るようになりました。

$ poetry install -C python_workspace/

poetry.lock のフォーマットが変更に

Poetry 1.2.2 は 1.3 のフォーマットを読めるとかなんとからしいです。 試しに以下のパッケージを追加したもので、1.2.2 と 1.3 で diff をとってみましたが、よくわかりませんでした。

$ poetry add numpy pandas matplotlib
Using version ^1.23.5 for numpy
Using version ^1.5.2 for pandas
Using version ^3.6.2 for matplotlib

@generated で、自動生成を示す説明が付いているところは、今回の追加機能を反映されているものでしょう。

速度

Poetry は以下のような Issue が立つほどユーザが速度改善を望んでいます。

github.com

今回 poetry updatepoetry lock の挙動やキャッシュ周りに修正が入っているらしいので、Python のパッケージマネージャの速度比較をしている Web ページを見ながら、速度の影響が出ているのか少し見てみましょう。

Install 👎

lock と update 👍

一番利用する poetry install の時間がさらにかかっていそうでかなり厳しいものがありますね。

さいごに

なんやかんや言いつつも、管理が大変な requirements.txt や黒魔術のような setup.py にはもう戻りたくないため、 Poetry には今後もお世話になることと思います。

あと来年は whywaita さんをネタに出来るように、なにか考えていこうと思います。

明日は id:kyontan2 さんです。

*1:インストールしたい先の社内ライブラリがモノレポ構成とかだと特に助かった

Python のセイウチ演算子 (Walrus operator) ":=" と仲良くする

はじめに

セイウチ演算子 (Walrus operator) は Python 3.8 で登場した代入式を利用するための演算子です。

なんとなく最近使うことが多いので、自分のユースケースを軽くまとめます。

セイウチ演算子のイメージ

C言語 を利用したことがある人ならば、C99 での for 文のカウント変数の宣言のようなものだと思ってください。

これを

int i = 0;
for (i = 0; i < 10; i++) {
    ;
}

こうする話です。

for (int i = 0; i < 10; i++) {
    ;
}

Python のセイウチ演算子

詳しくは What's New In Python 3.8 の説明通りです。 記事執筆が2022年なので、既にリリースから3年もの月日が経過していますね。

例えば if 文内で評価した式を再利用している際に、評価対象を出力するための関数等の呼び出しの重複を防いだりすることが出来ます。

これを

if len(list_object) > 10:
    print(len(list_object))

こうする話です。

if (n := len(list_object)) > 10:
    print(n)

3.7 以前では n への代入を if 文の前に書けば重複の呼び出しは回避できますが、セイウチ演算子 := を使って if 文内に書くことができます。

なお、> 手前の () を外すと、先に > が評価されてしまい、 n には真理値が代入、 if は n を評価するため if 文としての処理は変わりませんが n の値が変わるため、気をつけなければなりません。

個人的ユースケース

What's New In Python 3.8 でのユースケースで事足りていますが、個人的に便利に使っているユースケースを書き残しています。

正規表現

これは、公式の例にもあるものです。 マッチオブジェクトを if 文の中で利用するため、一番使っているかもしれません。

if match := re.search(r"regex_pattern", text):
    # 正規表現にマッチした際の処理
    result_span = match.span()

環境変数の呼び出し

os.environ から値を取り出す際に dict.get() を利用する際に使えます。 直接環境変数os.environ["ENVVAR"] で取り出すと対応する環境変数に値が無い場合に KeyError になりますが、無い場合を許したい処理に工夫の余地があるのです。 dict.get() は key に対応する値が無い場合、デフォルトで None を返す*1仕様になっており、これを利用した if 文でセイウチ演算子が活躍します。

if envvar := os.environ.get("ENVVAR"):
    # 環境変数がある場合の処理
    print(envvar)

スライスによる取り出し

Python のスライスをリストのようなオブジェクトに与えた際、完全にインデックス外を指す場合に空のリストを返す性質があります。

a = [1, 2, 3]

print(a[0:2])  # [1, 2]
print(a[1:4])  # [2, 3] : 取れるところまで取る
print(a[3:4])  # [] : 空

Python はリストが空かどうかのチェックを if 文で書く際、リスト長の長さを取って判定するのではなく、そのままリストを評価させて空であれば False となることを利用してそのまま if list_object: とすることは有名です *2 が、スライスを利用した場合に空のリストを返すことを利用して、そのままセイウチ演算子を使う処理を書くことがあります。

if sliced_list := list_object[slice_object]:
    # スライスで取れた場合の処理
    print(sliced_list)

おわりに

他にも while 文や内包表記で活躍するセイウチ演算子ですが、非常に便利な反面、コードの可読性を損なう書き方になる場合も多くあります。 下手な乱用には気をつけなければなりません (自戒)。

*1:実際は default 引数に設定された値を返すが、その初期値が None

*2:PEP 8の Style Guide を参照

gokart + PyTorch Lightning でいい感じに深層学習モデルを動かす

この記事は Sansan Advent Calendar 2021 20日の記事です。

前日は、id:kur0cky さんの

kur0cky.hatenablog.com

でした。

私は過去陸上部だった時代があるのですが、個人的にはフォームも気にしたほうがいいと思います(感想)。

この記事は何か

私が所属している研究開発部には、パイプラインに則ってコードを書こうという文化が浸透してきました。 これらのパイプラインのパッケージは、弊社の研究員が弊社ブログにて既に様々書いているので、ぜひこちらを御覧ください。

buildersbox.corp-sansan.com

buildersbox.corp-sansan.com

さらに、PyTorch のラッパである PyTorch Lightning はいいぞとの布教をとある研究員の方から受け、良さそうだなとなりました。 PyTorch Lightning については、とある研究員の方がいつだかに書いた記事もぜひご覧ください。

buildersbox.corp-sansan.com

こういったパイプラインやラッパは処理の切り分けが明確になることで、コードの可読性を向上させたり等様々なメリットがあります。 せっかくなので私もこのびっぐうぇーぶに乗って、パイプラインである gokart と PyTorch Lightning を組み合わせてサクッとコードを書いてみようと思います。

準備

今回は gokart を使うので、雑に cookiecutter からテンプレートを持ってきて使います。 エムスリーさんが用意しているものがあるので、ありがたく使っていきます。

github.com

次に、 PyTorch を poetry で入れていきましょう。

ところで、PyTorch と poetry はボチボチ相性が悪いです。 PyTorch はアーキテクチャ等の環境に沿ったものを入れないといけませんが、例えば、特定の source から参照する方法で入れることを試みることができます。

github.com

一方、↑の Issue にのように source を指定すると、他のライブラリまで指定した source を参照しようとしていまい、うまく動きません。

[tool.poetry.dependencies]
python = ">=3.9,<3.10"
gokart = "*"
torch = { version = "=1.9.0+cpu", source = "pytorch" }
torchvision = { version = "=0.10.0+cpu", source = "pytorch" }

[[tool.poetry.source]]
name = "pytorch"
url = "https://download.pytorch.org/whl/cpu/"
secondary = true

上記は、そのうまく動かない例です。 gokart をこの PyTorch で指定している url から取ってこようとして 403 が返ってきます。

これはそもそも PyTorch のインストールが PEP 503 に対応したことで、 pipenv の設定がシンプルになるというものがあり、これ poetry でも出来るやんけと思ったら出来なかったという不具合です。

github.com

Downgrading to Poetry 1.0.10 might be a workaround (ontop of my previous comment) as per: python-poetry/poetry#4704 (comment)

Haven't tested because it's too much of a pain, switching to pip!

github.com

しんどいですね。

今回は、最悪ですが whl の url を直接見に行く*1ことで一旦の回避策とします。 こちらですが、 今現在最新の poetry 1.1.12 では、 torchvision が torch の "x.x.x+cpu" に依存しているのにも関わらず 、 '+' 以降がうまく解釈できずに "x.x.x" に依存してるからうまくいかないよと怒られます。 この記事では、さらに苦肉の策として対応されている poetry 1.2.0a2 のプレビュー版を使っています。 誰か私を楽にしてください。

[tool.poetry.dependencies]
python = ">=3.9,<3.10"
gokart = "*"
torch = { url = "https://download.pytorch.org/whl/cpu/torch-1.10.1%2Bcpu-cp39-cp39-linux_x86_64.whl" }
torchvision = { url = "https://download.pytorch.org/whl/cpu/torchvision-0.11.2%2Bcpu-cp39-cp39-linux_x86_64.whl" }

Python のバージョンに依存するので ">=3.9,<3.10" にしてます。

ここまで、書いたら poetry install です。やっと PyTorch が入りましたね。 PyTorch Lightning は poetry が torch と torchvision のバージョンに合わせて依存解決出来るので問題ありません。

書いた

準備が出来たのでサクッとコードを書きました。 特に PyTorch Lightning は初めて書いたので、もっといい感じの書き方があればぜひ教えて下さい。 今回は ResNet で CIFAR-10 データで学習評価まで行うものにしました*2

gokart のタスクは以下の4つに分けてみました。 それぞれサラッと見ていきましょう。

データ前処理

gokart

import gokart
import luigi


class GokartTask(gokart.TaskOnKart):
    task_namespace = 'sansan_adcal_2021'


class PreprocessDataModuleTask(GokartTask):
    _v: int = luigi.IntParameter(default=0)

    def run(self):
        data_module = DataModule()
        data_module.prepare_data()

        self.dump(data_module)

PyTorch Lightning の LightningDataModule

from pathlib import Path

import pytorch_lightning as pl
import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms


class DataModule(pl.LightningDataModule):
    def __init__(self, dataset_root_path: Optional[Path] = None, train_size: float = 0.8, seed: int = 1111):
        super().__init__()
        self.dataset_root_path = dataset_root_path
        self.train_size = train_size
        self.seed = seed
        if self.dataset_root_path is None:
            # gokart の中間ファイルの出力に合わせて resources 以下に入れることにする
            self.dataset_root_path = Path(__file__).resolve().parents[2].joinpath('resources', 'dataset')
        self.data_transforms = transforms.Compose([
            transforms.Resize(256),
            transforms.CenterCrop(224),
            transforms.ToTensor(),
            transforms.Normalize(
                mean=[0.485, 0.456, 0.406],
                std=[0.229, 0.224, 0.225]
            ),
        ])

    def prepare_data(self) -> None:
        datasets.CIFAR10(
            root=self.dataset_root_path,
            download=True
        )

    def setup(self, stage: Optional[str] = None) -> None:
        if stage == "fit":
            all_train_dataset = datasets.CIFAR10(
                root=self.dataset_root_path,
                train=True,
                transform=self.data_transforms
            )
            len_train_dataset = int(self.train_size * len(all_train_dataset))
            len_val_dataset = len(all_train_dataset) - len_train_dataset
            self.train_dataset, self.val_dataset = torch.utils.data.random_split(
                all_train_dataset,
                [len_train_dataset, len_val_dataset],
                generator=torch.Generator().manual_seed(self.seed)
            )
        elif stage == "test":
            self.test_dataset = datasets.CIFAR10(
                root=self.dataset_root_path,
                train=False,
                transform=self.data_transforms
            )

    def train_dataloader(self) -> DataLoader:
        return DataLoader(self.train_dataset, batch_size=256, num_workers=8)

    def val_dataloader(self) -> DataLoader:
        return DataLoader(self.val_dataset, batch_size=256, num_workers=8)

    def test_dataloader(self) -> DataLoader:
        return DataLoader(self.test_dataset, batch_size=256, num_workers=8)

定義した DataModule に基本的にデータセットとしての裁量を託しています。 一度だけ呼ぶ prepare_data() メソッドは、このタスク内で呼ぶことにしました。 今回特に定義をしていませんが、前処理に関するパラメータや train, val のデータセットの分割のシード値等を、gokart 側で受け取って DataModule に渡せると良さそうです。

モデル準備

gokart

class PrepareModelTask(GokartTask):
    _v: int = luigi.IntParameter(default=0)

    def run(self):
        model_module = ModelModule()

        self.dump(model_module)

PyTorch Lightning の LightningModule

import torchmetrics
from torchvision.models import resnet34


class ModelModule(pl.LightningModule):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__()

        self.model = resnet34(pretrained=True)
        self.model.fc = torch.nn.Linear(512, 10)
        self.criterion = torch.nn.CrossEntropyLoss()
        self.val_acc = torchmetrics.Accuracy()
        self.test_acc = torchmetrics.Accuracy()

    def configure_optimizers(self):
        optimzier = torch.optim.Adam(self.model.parameters(), lr=1e-3)
        return optimzier

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.model(x)

    def training_step(self, batch, *args, **kwargs) -> torch.Tensor:
        x, y = batch
        pred_y = self.forward(x)

        loss = self.criterion(pred_y, y)
        self.log("train_loss", loss)
        return loss

    def validation_step(self, batch, *args, **kwargs) -> torch.Tensor:
        x, y = batch
        pred_y = self.forward(x)

        loss = self.criterion(pred_y, y)
        self.val_acc(pred_y, y)
        self.log("val_loss", loss)
        self.log("val_acc", self.val_acc, on_step=True, on_epoch=True)
        return loss

    def test_step(self, batch, *args, **kwargs) -> torch.Tensor:
        x, y = batch
        pred_y = self.forward(x)

        loss = self.criterion(pred_y, y)
        self.test_acc(pred_y, y)
        self.log("test_loss", loss)
        self.log("test_acc", self.test_acc, on_step=True, on_epoch=True)

モデルの構築・準備も同様に gokart はタスクの分割と、パラメータの受け渡し口としての使い方が良さそうに感じています。 (データセット同様、今回は何もパラメータを渡してないですが)

モデル訓練

class TrainTask(GokartTask):
    _v: int = luigi.IntParameter(default=1)

    def requires(self):
        return {
            "dataset": PreprocessDataModuleTask(),
            "model": PrepareModelTask()
        }

    def run(self):
        data_module: pl.LightningDataModule = self.load("dataset")
        model_module: pl.LightningModule = self.load("model")

        trainer = pl.Trainer(
            max_epochs=10,
            min_epochs=1
        )

        data_module.setup('fit')
        trainer.fit(model_module, datamodule=data_module)

        self.dump(trainer)

データセットのタスクとモデルのタスクを依存させるようにして、それぞれの LightningModule を渡しています。 各 epoch の checkpoints は PyTorch Lightning 側で持つので、ここを gokart に持たせる必要はないと判断しました。

モデル評価

class EvaluateTask(GokartTask):
    _v: int = luigi.IntParameter(default=0)

    def requires(self):
        return {
            "dataset": PreprocessDataModuleTask(),
            "model": PrepareModelTask(),
            "trainer": TrainTask(),
        }

    def run(self):
        data_module: pl.LightningDataModule = self.load("dataset")
        model_module: pl.LightningModule = self.load("model")
        trainer: pl.Trainer = self.load("trainer")

        data_module.setup("test")
        result = trainer.test(model_module, datamodule=data_module)

        self.dump(result)

Trainer を持ってこさせるようにしています。 評価の処理そのものは LightningModule 側で持っているので、結果を dump() させておいて、後で参照しやすいようにだけしておきました。

まとめと所感

今回は gokart と PyTorch Lightning を組み合わせて、深層学習モデルを動かすサンプルを書いてみました。 深層学習モデル部分の裁量を PyTorch Lightning に持たせ、処理を gokart のタスクで切ることで全体的に処理フローが見えやすい形になったかと思います。 学習のコードがややわかりづらくなりがちな PyTorch のコードは、 PyTorch Lightning で書くことで嫌でも処理がわかりやすくなって良いですね。 また、アドカレとしての締切の時間の都合上、直書きしてしまったパラメータが多いのですが、こういうパラメータはタスクごとに gokart で受け渡せるようにしておくともっと良いかと思います。

コードの全体像は後日まとめて GitHub 上に公開しようと思います。 文章を書いた人間としては、なんだか Poetry 上での torch + torchvision インストールバトルが本題になってしまったようで複雑な気持ちもありますが、許してください。

*1:ここ (https://download.pytorch.org/whl/torch_stable.html) にあります

*2:時間がかかるので、今回は epoch 数等適当に少なめで動作確認だけしました。

whywaitaの発言で遊びたい (GPT-2 でテキスト生成したら失敗しました)

この記事は whywaita Advent Calendar 2021 12日目の記事です。

前日は、id:mizdra さんの

poem.mizdra.net

でした。

?????

もちゔぇ

さて、去年はwhywaitaさんに実際にデータを取るところから協力してもらったにも関わらず遅刻し、いろいろ崩壊したwhywaitaさんの声を生成しました*1。 記事中で軽く注釈で触れましたが、id:tw1sm1k0 さんがテキストデータでwhywaitaさんを生成したので、音声でwhywaitaさんを生成しようと思った次第です。

対話システムでは、「質問」というクエリに対し「回答」になる言語を生成するタスクで、言語生成では最もよく知られたタスクの一つでしょう。 LINE 上で直接 AI と会話が出来ると話題になった「女子高生AIりんな」もこういった言語生成の中で作られたアプリケーションです。

www.rinna.jp

ところで、今年4月、このりんなで使われている言語モデルオープンソースとして公開されました。

prtimes.jp

GPT-2 と RoBERTa のそれぞれ2つのモデルで公開しており、Hugging Face が提供する Transformers で比較的容易に扱うことができます。

huggingface.co

では、whywaitaさんのテキストデータを食わせて遊びましょう (唐突)。

データ

id:tw1sm1k0 さんと同じく、元データとしてwhywaitaさんの tweet データを使います*2

twismik0.hatenablog.com

github.com

なんとなく、2018年の一年分を使うことにします。 この記事のための学習を始めたのが前日12/11の夕方で、特に試行錯誤する余裕はなく、前処理も雑にメンション・URL・ハッシュタグ除去くらいしかしていません。 ツイッターtweet データなんて汚れまくっていて*3、うまく扱えないことが多いので*4こんな雑な前処理はやめましょう。

f:id:nersonu:20211212001145p:plain

モデル

今回は GPT-2 のjapanese-gpt2-medium 事前学習済みモデルとして利用します。 公開されている学習済みモデルを fine-tune することで、いわゆるwhywaita言語モデルを目指します。

f:id:nersonu:20211212004405p:plain:w600
りんなをfine-tuneしてwhywaita言語モデルを作るイメージ図

手順とかコードとかは、やってみた系の記事がいっぱいあると思っているので、そちらを参照すると良いかと思います。 もうデータを Tokenizer で tokenize して、 Transformers の Trainer でいい感じのパラメータ*5を渡して学習させるだけです。

出来たやつを喋らせよう (1年ぶり)

GPT-2 のような言語モデルでテキストを生成させる場合、出だしの文章を与えてあげると、この入力に続くようにテキストを生成します。 比較として今回使ったりんなのモデルもそのまま使ってみて、30文字くらい生成させてみます。 では、早速やってみましょう。

入力「こんにちは、」

  • りんな「こんにちは、今日も暑いですね。 皆さん熱中症には気をつけてください! さて、今回は先」
  • whywaita (学習初期)「こんにちは、ちゃが済のwにuティングガいiphoneなんになってや厳しいba凶悪見てアルコール1動かしgoogleしてしていたコミ」
  • whywaita (学習済み)「こんにちは、部分はでなかったer程学習分のを失いリリーアカウントに色代開催腹期送日をリッシュ見る鑑賞完全に」

入力「久しぶり」

  • りんな「久しぶりに、お会いできて嬉しかったです。ありがとうございました! 今日は、お忙しい中、お」
  • whywaita (学習初期)「久しぶりちゃがの済wにuガティングやいiphoneなんになって厳しい凶悪アルコール見てコミ1bagoogleしてっぽいしていた」
  • whywaita (学習済み)「久しぶり、部分はでなかったはer程分の学習リアカウントリーを失い代色に開催見る腹リッシュ送してを完全に」

入力「オタクは」

  • りんな「オタクは自分の好きなものを好きでいられるのが一番幸せだと思う。 俺もそう思うけど、それって「自分が何」
  • whywaita (学習初期)「オタクはちゃがwい済にのuやっぽいなんガ厳しいiphone見てところ1。がティングするかがすぎるがあるが」
  • whywaita (学習済み)「オタクは部分は、なかったでer程のリ分学習代アカウントを失いリーに送色開催腹日を完全に期鑑賞リッシュ見る」

はい。

おわりに

ということで、酷く過学習しました。

単純に学習データの前処理が不十分なことと、学習方法に問題がありそうです。 今回は面倒くさいので改善もせずに期限に間に合わせるために公開していますが、12月中に改善して記事に出来たらいいなと思います。

明日は id:awawan1123 さんです。 お楽しみに。

*1:https://nersonu.hatenablog.com/entry/whywaita-advent-calendar-2020

*2:一応、whywaitaさんの許可はとりました。

*3:文法とかそういった観点での話です。

*4:あくまでも個人の体感です。

*5:いい感じのパラメータを渡せなかった

24歳になりました。2020年の振り返りと2021年にやりたいこと

2021年1月13日、24歳になりました。

2020年のやったことを振り返って、2021年にやりたいことをまとめたいと思います。

2020年の振り返り

就活とインターンシップ

2020年は就活から始まった記憶が印象深いです。 1月頃はガンガン就活をしていました。 最終的にいくつかの企業様から内定を頂き、3月にそのうちの一社の内定を承諾しました。

また、2020年は2社にインターンシップでお世話になりました。 2月は週2,3回の出社ペースで株式会社リクルート様にお世話になりました。 そのときのことは、ブログの記事にまとめています。

nersonu.hatenablog.com

また、3月の2週間程度、ウォンテッドリー株式会社様にお世話になりました。 レコメンド関連のチームにお世話になり、メンターの方とのディスカッションは非常に有意義なものでした。

あとこのときのインターンシップの報酬でMacBook Airを買いました。

修士2年生

まだ修論の提出も終わっていませんが、2020年度はM2でした。 ただ、このままいくと無事修了できそうです。 良かったですね。 秋辺りに出した国際会議の結果が良くなかったので、追加で別のカンファレンスに出す分で最後さらに追い込まれそうですが、最後まで頑張っていきたい次第です。

あと、M1までやった研究が引き継いでいただいた先輩の手によって2020年内に国際会議に採録されました。 詳しくはこちらにあります。

nersonu.hatenablog.com

在宅と飲酒

今年は新型コロナウイルスであるCOVID-19の影響で、在宅を余儀なくされました。 はじめは在宅で作業が出来る気がしなかったので、思い切って給付金でモニターを買いました。

www.philips.co.jp

4KでType-Cで丁度いいのはどれっ?って聞いたら、Philipsのモニターを紹介されました。 これが無ければ研究は進まなかったでしょう。

また、在宅のストレスで家での飲酒量が増えました。 毎日毎日冷蔵庫にストックした缶ビールをビールグラスに注いで飲んでいました。 止める人間はいません。

漫画をたくさん買った

AmazonKindleの漫画を山ほど買いました。 せっかくなので2020年に買った漫画 (2020年に出版されてない漫画もあるかも、かつ今年買い始めたシリーズ) を少し紹介します。

まくむすび

マンガ大賞2020のノミネート作品です。 高校演劇がテーマの作品で、高校の頃演劇部だった自分には非常に共感できたり思い出すことが多い漫画でした。 まだまだこれからが気になる作品です。

tonarinoyj.jp

ふたりが家族になるまでに

主人公の女の子と従兄の二人生活の距離感が絶妙でした。 あと絵が好み。 こういうドギマギとしてるけど、家族として温かいみたいな話いいですよね。

houbunsha.co.jp

謎の彼女X

今更という作品ではありますが、全巻買ってdアニメストアでアニメも全部観ました。 不思議な繋がりではありつつも、確かにそこに二人の恋愛感情があるのが最高ですね。

afternoon.kodansha.co.jp

よふかしのうた

だがしかしで有名なコトヤマ先生の最新作です。 登場人物のの感情とか、人間関係の話とか、描写が素晴らしくのめり込みます。

websunday.net

イカさんは押しころせない

いいラブコメです。 とりあえず読んでください。

www.akitashoten.co.jp

結婚するって本当ですか

神のみぞ知るセカイ若木民喜先生の最新作です。 社会人ラブコメは好きでいろいろ読んでいるのですが、大好きな先生が描いているので非常に気になっています。 全体的に色々なことへの"気付き"みたいな描写が好きです。

bigcomicbros.net

六畳一間の魔女ライフ

お仕事頑張りコメディ。 頑張ろうっていう気持ちになる作品です。 id:mizdra 君にも気に入ってもらえてよかったです。

magazine.jp.square-enix.com

2021年にやりたいこと

働く

このまま順調に進めば、ありがたいことに働くことになります。 自分が今までやってきた知識/技術を活かして頑張って1年働いていきたい気持ちです。

Kaggleを始める

データを使うぐらいにしか結局Kaggleをやっていなかったので、やってみようかな〜と思っています。 そんなデータ分析のスキルを上げたいとか高尚な理由ではなく、単に面白そうだからです。 今まで精度を競うことにそんなに興味はありませんでしたが、興味が深々津々になってきました。

勉強

今、大学の同期主催の久保川統計の輪講に参加しているのですが、これを踏まえて竹村統計を自分で一冊勉強したいなと思っています。

www.hanmoto.com

復習がてらしっかりと数理統計学の勉強を抑えたいなという気持ちです。

休肝

飲みすぎです。 まずは 週1の休肝 が目標です。

誕生日なので

あとは祝ってください!! 2021年もがんばります!!!!

www.amazon.jp

この時期。

めちゃくちゃscaleboxの記事にアクセスが偏る。 毎年こうなります。

f:id:nersonu:20201222000543p:plain

該当の記事はコレです。

nersonu.hatenablog.com

みんな、TeXで卒論やら修論やら書いていそう。

ぼくも、書いています。

アドベントカレンダーはまだWIPにさせてください。