そぬばこ

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

サブスクを整理する

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

遅刻です。スイマセン。 日曜日は呑気に同期とキャンプした帰りで、寿司食いながら「なんか忘れてる気がするんだよな……」と呟いたんですがこれだったんですね。 ちなみに、去年も11日目の記事だったらしいです。

nersonu.hatenablog.com

前日 (10日目) は、id:yu_ki_kun_0 さんの 【WIP】お台場に1年半くらい住んでみた感想 - 上から下まで面白いことを… でした。 住むところは非常に大事ですよね。 今自分も、通勤で必ず座れる駅に住んでいるので、推しポイントは非常に共感できました。

さて今回はストックしている技術ネタも無いので、あんまり面白くないですが契約しているサブスクを列挙していこうと思います。 きっとどこかで whywaita さんの役に立つことでしょう (適当)

生活マスト枠

1Password

1password.com

言わずと知れたパスワード管理ツールです。 最近は Passkey の対応や、 SSH の鍵管理にも一役買っています。 生体認証系と相性がいいのはありがたいですよね。

年額 $39.47 です。

Moneyforward ME

moneyforward.com

個人用家計簿アプリです。 複数口座や複数クレカの自動連携のために有料のサービスを使っています。 これが無いと破産します。 スタンダードコースで十分です。

年額 ¥5,500 です。

Sleep Cycle

www.sleepcycle.com

睡眠トラッキング・目覚ましアプリです。 浅い眠りか深い眠りかを判定して、なんかいい感じのタイミングで起こしてくれるのでずっと使っています。 Pixel Watch にも対応していて、腕のバイブレーションで起きやすい感じがしています。

年額 ¥3,000 です。

Google One

one.google.com

Google Drive に大体のファイルを集約させていて、ストレージの容量が必要なため契約しています。 アホほど使っているわけでもないので、ベーシックプランではあります。

月額 ¥250 です。

ChatGPT Plus

openai.com

簡単な技術検索や、アイデアの発散等に主に使っています。 最近は GPTs によって、論文の読解の補助にも便利ですね。

月額 $20 です。

論文読み枠

論文を読むために、 ChatGPT 含めて3つのサブスクを契約しているようです。 全部使っているので解約は考えていません。

Paperpile

paperpile.com

論文管理ツールです。 共有も楽ですし、参考文献表記 (もちろん BibTeX にも使えますが、 pptx にベタ貼りするときに便利) が楽なので使っています。 さよなら Mendeley 。 PDF の保存先が Google Drive なので Google One が手放せなくなるが玉に瑕ですかね。 共有に使ってるので一応 Business Plan にしてるんですが、このせいで高くなってます。

年額 $119.88 です。

Readable

readable.jp

英語の PDF ファイルを、レイアウトを維持したまま日本語に直すアプリケーションです。 論文を読むんだったら GPTs で代替できる気もするんですが、正確性と英語の勉強の点でまだまだ使い続けそうです。

月額 ¥980 です。

動画・音楽枠

DAZN

www.dazn.com

野球とかサッカーとかを主に観ます。 スポーツ観戦好きです。 年間通して観るものもあるので、旧 DAZN for docomo で安く観続けています。

月額 ¥1,925 です。

dアニメストア

animestore.docomo.ne.jp

ぜんぜんみてないあは

月額 ¥550 です。

Youtube Premium

www.youtube.com

Chromecast with Google TV をぶっ刺しているので、広告無しがありがたいです。 たまに Youtube Music も使ってます。

月額 ¥1,280 です。

ABEMA プレミアム

abema.tv

麻雀系の見逃し再生に使っています。 たまにスポーツとか将棋とかを観ています。

月額 ¥980 です。

Spotify

open.spotify.com

基本的に音楽を聴くときは Spotify を使っています。 レコメンドの体験が非常に良く感じます。 UX 面でかなり好きです。

月額 ¥980 です。

ゲーム枠

Nintendo Switch Online + 追加パック 個人プラン

www.nintendo.co.jp

Splatoonポケモン系でオンラインサービスを使うので契約しています。 最近はたまに GBA のゲームを遊んだりしているので、追加パックを入れてたりという感じです。

年額 ¥4,800 です。

PlayStation Plus エッセンシャル

www.playstation.com

Switch ほどではないですが、オンラインサービスを使うゲームがあるので契約しています。 PS5 はほとんど起動してないです。

年額 ¥6,800 です。

生活その他枠

Amazon Prime

www.amazon.co.jp

Amazon でそこそこ買い物するので、よく恩恵をこうむっています。

年額 ¥4,900 です。

クラシルプレミアム

www.kurashiru.com

料理をする際にはかなり使っています。 UI が好きです。

月額 ¥480 です。

まとめ

思ったよりも使ってないサービスが少ないのが恐ろしいなと感じました。 全然使っていないサービスが山程あれば、この機会に解約してやろうと思っていましたが、どうやら解約は一旦しなさそうだな〜と眺めながら適当に考えています。 みなさんはサブスクはほどほどに、賢い生活を送ってください。

Sansan を退職しました

2023年10月末でSansan株式会社を退社しました。

2021年4月に修士卒で新卒で入社し、約2年半ほどお世話になりました。 今思い返しても新卒として入社してよかった会社だと思います。 技術的な部分もビジネス的な部分も学ぶことができて非常に成長したなという実感がありますし、何よりも周りのメンバーに恵まれたなというのが一番ツイていたと思います。

やってきたこと

「研究員」という名目の職種で入社しました。 とはいいつつも、一般的には機械学習エンジニアと呼ばれる職種に近い業務をしていたと思います *1

大半は「ContractOne」と呼ばれる契約書を取り扱うプロダクトにおいて、契約書のデータ化を行うタスクに時間を注ぎました。 自然言語処理や画像処理といった技術検証はもちろんのこと、これらの技術を使った情報抽出を行うアプリケーション全体の開発に携わりました。 少し特殊だったことは、 Sansan のデータ化プロダクトの大半に人間のオペレータが入力するフローが存在することです。 これらのフローを考慮したり、オペレータのマネジメントを行うメンバーとの連携も重要な要素でした。 このような環境に身を置くことで、技術力だけではなく、他のメンバーとのコミュニケーションの重要性や、コストや KPI の重要性を多く学ぶことができました。

また、研究開発部という横串の組織において、(機械学習エンジニアというよりは) Python エンジニアという観点での開発生産性の向上に努めました。 当時まだ requirements.txt がほとんどを占めていた状況で Poetry によるパッケージ管理を推進してみたり *2 、structlog でログを構造化させてみたり、 reviewdog が叱ってくれる Python の CI の叩きを作ってみたり *3 しました。 今年は部の新卒研修の設計をしたりもしました *4。 前年度から環境構築の部分のドキュメントを整備してみたりと、研究開発部内のエンジニアにかなり協力してもらいながら、自由にいろいろうるさい人をやっていた感じです。 好き放題やってすいませんでした。

学会のスポンサーブースもいくつか運営のリードをさせていただきました。 今年は熊本にお邪魔したりしましたね。 Sansan には技術系のブランディングをメインで行うメンバーがおり、非常にお世話になりました。

転職について

元々、新卒のキャリアは3〜5年目の間に一区切りつけようと考えていました。 これは単に同じ組織に居続けて、井の中の蛙大海を知らずになることを恐れていたからです。 一つの会社で成果を出し続けることも非常に難しいことですが、果たして20代でそこに特化してしまっても良いのだろうか。 同じ会社にいて別のチャレンジをしても、それは果たして他でも通用するチャレンジをしたと言えるのだろうかと、人にとってはどうでも良いようなそういう面倒くさいメンタリズムが根底にはありました。

一方で実際に転職を決意したのはもっとポジティブな理由です。 機械学習を主軸にしつつ、今よりももっとエンジニアリング能力を伸ばせて、将来的にもっとプロダクトを前線で作っていくポジションを見据えて仕事が出来る、そんな絵に描いた餅みたいな環境を手に入れようと思ったからです。 就職したときからどういったキャリアを歩みたいのかということはずっと課題であり、分析もエンジニアリングも、なんなら研究よりの開発もなんでも出来そうなので新卒で Sansan に入ったというところがありました *5 。 もちろんやりたいアピールはかなりしましたし、だいぶわがままで面倒くさい社員だったという自覚はあります。 手挙げで挑戦を許容してくれる非常に良い組織ではありましたが、事業フェーズや現状の組織体制とは折り合いが合わない部分もあるなと感じ (あくまでも主観) 、新しい環境を求めました。

おわりに

転職活動の際には、友人知人含め様々な方々や会社様のお時間いただき、本当にありがとうございました。 11月1日からは、株式会社サイバーエージェントにて、機械学習エンジニアとして新しいスタートを切りました。 インターン時代のご縁もあり、結果的には一番最初に相談させていただいた会社にお世話になることになりました。 転職の決め手や現在取り組んでいることについては、差し支えない範囲で機会があれば文をしたためる日があるやもしれません。

最後になりましたが、前職の同僚の方々には本当にお世話になりました。 来年は Sansan のオフィスも渋谷に移転する *6 というところもありますが、物理的な距離もそんなに離れていませんので、ぜひ一杯付き合っていただければ幸いです。

*1:最近は募集要項も「機械学習エンジニア」と表記していますし: https://media.sansan-engineering.com/randd

*2:Poetry が正解だったのかは未だよくわかりませんが、社内 Poetry 芸人をやっていたおかげで主題じゃない記事で、 Poetry の Tips (今となっては古いバージョン) として参照されることがたまにありました: https://nersonu.hatenablog.com/entry/sansan-advent-calendar-2021

*3:そのうち在籍メンバーが記事を書いてくれるんではなかろうかと期待しています(ぶん投げ)

*4:今年は外に記事を出していて、最高の研修設計メンバーでした

*5:実際やらせてもらいました

*6: https://jp.corp-sansan.com/news/2023/0718.html

高速かつ 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:いい感じのパラメータを渡せなかった