Python: poetryでパッケージの依存管理

私はこれまでPythonのパッケージ管理として pyenv + pipenv を主に使ってきました。が、最近はpipenvは色々あって使いづらさを感じていました。

  • pipenv lockpipenv syncが遅い (気がする)
  • pipenv自体の更新が怪しかった (参考、今年に入って4月と6月にリリースされている模様)

乗り換える程の理由でもないのですが、代替となるツールは探しておかないとなあとふんわり思っていた頃、同僚がpoetryを使っていて良さそうでしたので、使い方をまとめながら紹介したいと思います。

poetry

poetryは、主にパッケージ依存関係の解決・インストール・更新と仮想環境の構築を行ってくれるコマンドラインツールです。上の通りpipenvなどが競合するツールになります。

python-poetry.org

github.com

インストール

pipで入れてしまうのが一番手軽です。

$ python --version
Python 3.8.5

$ pip install poetry

プロジェクト作成

poetry newでpoetryプロジェクトの標準的なディレクトリ構成が作られます。

  • 既存のプロジェクトにpoetryで依存関係を管理する場合、newの代わりにpoetry initを使います。
$ poetry new poetry-example
Created package poetry_example in poetry-example

$ cd poetry-example
$ tree
.
├── README.rst
├── poetry_example
│   └── __init__.py
├── pyproject.toml
└── tests
    ├── __init__.py
    └── test_poetry_example.py

このとき作成されるpyproject.tomlに追加するパッケージを記述していきます。

[tool.poetry]
name = "poetry-example"
version = "0.1.0"
description = ""
authors = ["ohke <...>"]

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

仮想環境の構築

poetry install または poetry update とします。そうすると仮想環境 (virtualenv) が構築され、パッケージがインストールされます。

  • デフォルトではdev-dependenciesのパッケージもインストールされます (--no-devオプションで除くこともできます)
$ poetry install
Creating virtualenv poetry-example-JGBVk9r8-py3.8 in /Users/ohke/Library/Caches/pypoetry/virtualenvs
Updating dependencies
Resolving dependencies... (0.2s)

Writing lock file


Package operations: 11 installs, 0 updates, 0 removals

  - Installing zipp (3.1.0)
  - Installing importlib-metadata (1.7.0)
  - Installing pyparsing (2.4.7)
  - Installing six (1.15.0)
  - Installing attrs (19.3.0)
  - Installing more-itertools (8.4.0)
  - Installing packaging (20.4)
  - Installing pluggy (0.13.1)
  - Installing py (1.9.0)
  - Installing wcwidth (0.2.5)
  - Installing pytest (5.4.3)
  - Installing poetry-example (0.1.0)

生成されるpoetry.lockファイルに依存パッケージが出力されます。このあたりはpyenvと同じですね。

$ tree
.
├── README.rst
├── poetry.lock  # ★
├── poetry_example
│   └── __init__.py
├── poetry_example.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
├── pyproject.toml
└── tests
    ├── __init__.py
    └── test_poetry_example.py

デフォルトではユーザホーム以下のキャッシュディレクトリに作成されますが、poetry configで設定を書き換えることでカレントディレクトリに作ることもできます。以下では./.venvに作られています。

# 上で作った環境を削除
$ rm -rf /Users/ohke/Library/Caches/pypoetry/virtualenvs/poetry-example-JGBVk9r8-py3.8

$ poetry config --list
cache-dir = "/Users/ohke/Library/Caches/pypoetry"
virtualenvs.create = true
virtualenvs.in-project = false
virtualenvs.path = "{cache-dir}/virtualenvs"  # /Users/ohke/Library/Caches/pypoetry/virtualenvs

$ poetry config virtualenvs.in-project true

$ poetry install
Creating virtualenv poetry-example in /Users/ohke/dev/private/poetry-example/.venv
...

仮想環境上での実行

poetry runで仮想環境でPythonコードを実行できます。上でインストールした仮想環境のパッケージがパスに含まれていることが確認できます。

  • poetry shellで仮想環境上でシェルが立ち上がります
$ cat poetry_example/main.py
import sys
from pprint import pprint

if __name__ == "__main__":
    pprint(sys.path)

$ poetry run python poetry_example/main.py
['/Users/ohke/dev/private/poetry-example/poetry_example',
 ...
 '/Users/ohke/dev/private/poetry-example/.venv/lib/python3.8/site-packages',
 ...]

パッケージのインストール

パッケージを追加する場合、poetry addを使います。これでpyproject.tomlとpoetry.lockが更新されます。

  • pyproject.tomlを直接書き換えた場合は、poetry updateで poetry.lockの更新 + 仮想環境へのインストール を行います
  • パッケージをアンインストールする場合は、poetry remove
$ poetry add numpy
Using version ^1.19.1 for numpy

Updating dependencies
Resolving dependencies... (1.6s)

Writing lock file


Package operations: 1 install, 0 updates, 0 removals

  - Installing numpy (1.19.1)

まとめ

概ねpipenvと変わらない機能を有することは確認できました。lockやsyncがどれだけ早くなるのか次第なところはありますが、現在pipenvを使っているプロジェクトを移行するまでのメリットは無さそうです。

去年12月に1.0がリリースされて以来、現在までバージョンアップも頻繁に行われているので、新しく作るシステムに関してはpoetryを使っていくのがいいのかな、という所感でした。

Python: 安全・手軽に一時ファイル・一時ディレクトリを作る (tempfile)

一時的に使うファイルやディレクトリを作成して、処理が終わったら削除する、という手続きを実装する機会はしばしばあると思います。

簡単なことではあるのですが、処理の例外ハンドラで削除の実装を忘れてゴミファイルができてしまったり、並列実行したときにプロセス間でファイルパスを競合させてしまったりと、ついハマる落とし穴もあります。

Pythonではこういった一時ファイルを安全・手軽に作成するための標準ライブラリとして tempfile が提供されています。

docs.python.org

$ python --version
Python 3.8.5

一時ファイル

使い方は通常のファイル操作同様シンプルで、open関数の代わりにTemporaryFileオブジェクトを介して読み書きする点だけが異なります。

  • デフォルトモードは "w+b" (mode引数で変更可能)
  • dir引数の指定が無い場合、環境変数TMPDIR / TEMP / TMPのディレクトリ内にこのファイルは生成されます

ポイントは2つで、これによって削除忘れやファイルパス競合を意識する必要がなくなります。

  • closeメソッドを呼び出す or with句内から抜け出す (or ガベージコレクト対象となる) ことで、ファイルも自動的に削除されます
  • TemporaryFileは内部でmkstempを実行しており、重複しないようにランダムな文字列がファイル名に付与されます
import tempfile

f = tempfile.TemporaryFile()
f.write(b"Hello, world!")
print(f.read())
f.close()

# dir引数で出力先ディレクトリを指定できる
with tempfile.TemporaryFile(dir=".") as f:
    f.write(b"Hello, world!")
    f.seek(0)
    print(f.read())

TemporaryFileは不可視のファイルが生成しますが、NamedTempraryFileでは可視ファイルを生成できます。

f = tempfile.NamedTemporaryFile(prefix="a_", suffix="_b", dir=".")
f.write(b"Hello, world!")

# 出力された一時ファイルの確認
from glob import glob
print(glob("./a_*_b")[0])  # ./a_z91g49js_b

これら2つに加えて、指定のサイズとなるまでメモリにスプールするSpooledTemporaryFileというのもあります。

一時ディレクトリ

一時ディレクトリはTemporaryDirectoryで作成できます。以下のように、コンテキストマネージャ (with句) 内だけでアクセスできるディレクトリが生成され、抜け出すときには削除されます。

import os

with tempfile.TemporaryDirectory(prefix="tmp_", dir=".") as dirpath:
    print(dirpath)  # ./tmp_xo0tg2u1

    with open(os.path.join(dirpath, "a.txt"), "w") as f:
        f.write("Hello, world!")
    print(glob(os.path.join(dirpath, "*")))  # ['./tmp_xo0tg2u1/a.txt']

print(glob("./tmp_*"))  # [] (削除済み)

論文メモ: LUVLi Face Alignment: Estimating Landmarks’ Location, Uncertainty, and Visibility Likelihood

先週に引き続き顔ランドマーク推定に関わる論文として、CVPR2020で提案された LUVLi Face Alignment: Estimating Landmarks’ Location, Uncertainty, and Visibility Likelihood (arXiv) を紹介します。

LUVLi Face Alignment: Estimating Landmarks’ Location, Uncertainty, and Visibility Likelihood

@misc{kumar2020luvli,
    title={LUVLi Face Alignment: Estimating Landmarks' Location, Uncertainty, and Visibility Likelihood},
    author={Abhinav Kumar and Tim K. Marks and Wenxuan Mou and Ye Wang and Michael Jones and Anoop Cherian and Toshiaki Koike-Akino and Xiaoming Liu and Chen Feng},
    year={2020},
    eprint={2004.02980},
    archivePrefix={arXiv},
    primaryClass={cs.CV}
}

モチベーション

顔ランドマークの位置だけではなく、uncertainty (不確実性) とvisibility (視認性) も同時に正確に予測したいというのがメインテーマとなります。見づらい・見えないランドマークは、その度合いも評価することが後続タスクにとって有益な情報となります。

光源やピンボケ、遮蔽 (手やマスクなどによるocclusionと顔向きによるself-occlusion) などによってランドマークの判別しづらい場合は、分散を大きくするというのがゴールとなります。

ヒートマップ回帰の問題点

不確実性と視認性を正確に予測する上で、現在主流のヒートマップ回帰は大きく2つの問題を抱えています。

  • 固定対称ガウス分布をground-truthとして学習しているため、予測されたヒートマップが不確実性の予測には適していない
    • 例えば、顎のランドマークはフェイスラインに沿った分布になるはず (なのに学習ではそれが表現できていない)
  • リサイズされたヒートマップに対してargmaxを行って、ランドマークとなる1ピクセルを決定するため、量子化誤差が発生する
    • argmaxからsoftmaxに置き換える1、最大値の近傍をサブサンプリングする2、などの工夫が行われている

提案

  • Location, Uncertainty, and Visibility Likelihood (LUVLi) によるモデル化
  • 非遮蔽・遮蔽・自己遮蔽 (self-occluded) が区別された19000枚以上の顔画像からなるデータセット

LUVLi

LUVLiではDU-Net3をベースとし、各U-netから分岐する3つのコンポネントを追加したモデルとなっています。いずれも微分可能でend-to-endで学習可能です。

  • Mean Estimator: ランドマークの位置を推定
    • 各ランドマークのヒートマップをReLUに通して加重平均することでサブピクセルの精度の推定結果を得られる
  • CEN (Cholesky Estimator Network) : ランドマーク位置の共分散行列のコレスキー係数を推定
    • 重みは全U-netで共有
  • VEN (Visibility Estimator Network) : ランドマークの視認性の確率を推定
    • 重みは全U-netで共有

LUVLiのロス関数は以下で定めている。

  • 第1項・第2項がvisibilityについてのバイナリクロスエントロピ
    • visibleなら  v_j = 1 となる分類
  • 第3項がランドマーク座標 ( \vec{p_j} = (p_{jx}\ p_{jy})^T ) についての対数尤度
    • 位置推定の回帰
    • μが期待値ベクトル、Σが分散共分散行列で、求めたいパラメータとなる

CENとVENはそれぞれDU-Netのボトルネックを入力として全結合している。

  • CENは、分散共分散行列  \Sigma_{ij}  \Sigma_{ij}=L_{ij}L_{ij}^T (コレスキー分解) したときの下三角行列Lの3つの値を出力とする
  • VENは、sigmoidで[0, 1]の確率値を出力とする

MERL-RAV

AFLW4の約19000枚の画像に対して、各ランドマーク座標に新たに以下3つのvisibilityクラスを付与したデータセット MERL Reannotation of AFLW with Visibility (MERL-RAV) を提案。

  • Unoccluded: 遮蔽なし (見える)
  • Self-occluded: 顔の角度によってランドマークが隠れている
  • Externally occluded: 髪や手、帽子などによってランドマークが隠れている

LUVLiではUnoccludedとExternally occludedを  v_j = 1 、Self-occludedを  v_j = 0 としてます。(確かに画像に写っていない裏側のランドマークを付けるなんて本来は不可能なはず。)

実験

NME、AUC、FRで評価。

  • visibility  v_j が0のときは誤差の計算に含めない
  • 正解座標  p_j に対して、予測座標  \mu_{K_{j}} はU-Net最終層の出力

300-W, Menpo, COFWにおける他モデルとの比較

それぞれSotAを達成してますが、externally occludedが多く含まれるCOFWで特筆して高い値をマークしており、遮蔽への強さが示されてます。

また300-Wのtestセットを使って、残差と分散共分散行列値の相関を調べたところ、一定の条件下で高い相関が見られました。これによって、分散共分散行列の予測値で不確かさを表現できていることがわかります。

COFWを使って遮蔽有無に依る分散共分散行列の値の変動を調べた結果が以下の表です。LUVLiでは遮蔽されている場合に十分大きな値となることがわかります。

CENについて、ヒートマップベースの手法も調査したが、直接座標を回帰する方法と比べてうまくいかなかった。分散共分散行列の最小固有値の分布を見ると、ほとんどが1ピクセル以下で、64x64のヒートマップでは解像度が低すぎるためとのことです。

MERL-RAVを使った評価

LULViの特性をさらにMERL-RAVで詳細に調べた結果が以下です。

  • 特に顔向き (yaw) の角度が大きいHalf-ProfileとProfileで素のDU-Netよりも高い精度をマークしてます
  • 遮蔽有無によって、Σの値も大きくなっており、不確かさを表す予測値として機能しています

所感

「遮蔽されているランドマークは、遮蔽されていることがわかるようにする」「遮蔽されているランドマークの位置を "敢えて" 推定するなら、どの程度分散するのかわかるようにする」という実利用では避けて通れない問題を、シンプルにアプローチした研究かと思いました。


  1. Diogo Luvizon, David Picard, and Hedi Tabia. 2D/3D pose estimation and action recognition using multitask deep learn- ing. In CVPR, 2018.

  2. Ying Tai, Yicong Liang, Xiaoming Liu, Lei Duan, Jilin Li, Chengjie Wang, Feiyue Huang, and Yu Chen. Towards highly accurate and stable face alignment for high-resolution videos. In AAAI, 2019.

  3. Zhiqiang Tang, Xi Peng, Kang Li, and Dimitris Metaxas. Towards efficient U-Nets: A coupled and quantized approach. TPAMI, 2019.

  4. Martin Koestinger, Paul Wohlhart, Peter Roth, and Horst Bischof. Annotated facial landmarks in the wild: A large-scale, real-world database for facial landmark localization. In ICCVWorkshops, 2011.