Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

Rustでグラフをplotするライブラリのまとめ

- はじめに -

Rustでグラフを描画したいと思った時に調べたクレートとその実装、機能のまとめた時のメモ。

現状はplottersを使っておけば間違いなさそうだが、目的によっては機能で選択する場合もありそう。


 

- 前提知識 -

グラフの描画までの機能としては、matplotlibのようにaxisやviewを構造体として持っているライブラリもあれば、受け取った配列をそのままgnuplotスクリプトに変換するライブラリもある。
詳細は後述するが、当然この構造に依存してインターフェースが変わったり、出来ること出来ないことがある。


plotを想定したグラフデータの出力方法は大きく3つに分かれる。
SVG等を通して画像ファイルとして出力する方法、jsやwasmやhtmlテンプレートエンジンを利用してHTMLベースで出力する方法、テキストベース(アスキーアート)として表示する方法である。

また、Jupyter NotebookのRust Kernelとして現状開発が継続しているものにevcxrというライブラリがあり、こちらに出力する事が出来るかも差別化の点に入る。
github.com


OpenCV等の画像処理系ライブラリを用いてもグラフの描画はもちろん行えるが、今回はグラフ描画を軸としたライブラリの調査であり対象とはしない。

 

- グラフ描画クレートざっくりまとめ -

2021/09/21時点での大まかな実装とライブラリをまとめる

plotters

A rust drawing library for high quality data plotting for both WASM and native, statically and realtimely 🦀 📈🚀
latest commit: 2021/09/17, star: 1.5K
github.com

以下参考に成り得る文献

plotly

Plotly for Rust
latest commit: 2021/07/15, star: 467
github.com

plotlib

Data plotting library for Rust
latest commit: 2021/02/01, star: 335
github.com

  • 非常にmatplotlibを意識したであろう実装になっている
  • 開発は滞り気味
  • Vecやndarrayに対応
  • 自前でaxisやviewの構造体を持っている
  • textでの描画、svgクレートを使った画像での描画に対応
  • matplotlibに似た思想のAPIを持つ
    • matplotlibにおけるfigure、axesがview, plotに当たる

以下参考に成り得る文献

poloto

A simple 2D plotting library that outputs graphs to SVG that can be styled using CSS.
latest commit: 2021/09/17, star: 28
github.com

以下参考に成り得る文献

rustplotlib

A pure Rust visualization library inspired by D3.js
latest commit: 2021/07/13, star: 1116
github.com

  • D3.jsをまるっとrustで書き直している
  • 描画がかなり綺麗な印象
  • 最後はsvgクレートでSVGに書き出している (.to_svg)
  • 発想としてはかなり壮大なプロジェクトだが、更新はしばらく止まっていそう
  • multiviewなどに未対応だが今後開発されるかは実装を見る限り微妙そう
    • 対応するplot形式を沢山作る方針っぽい

RustGnuplot

A Rust library for drawing plots, powered by Gnuplot
latest commit: 2021/09/01, star: 324
github.com

以下参考に成り得る文献

preexplorer

Externalize easily the plotting process from Rust to gnuplot.
latest commit: 2021/09/06, star: 4
github.com

vega_lite_4.rs

rust api for vega-lite v4
latest commit: 2021/01/22, star: 7
github.com

  • Pythonで言う所のAltair
  • Vega Lite(vega-lite.js)にJsonAPIがあるのでそれを叩くための実装を用意したもの
  • nalgebraやndarray、rulinalg等の主要な行列ライブラリに対応している
  • showtaを作っている人と同じ
    • https://github.com/procyon-rs/showata
      • HTMLを生成するためのツール
      • jupyter notebook上に描画する事を目的としている
      • tableと画像をHTMLに変換するためのツール
    • showtaを経由してevcxrで表示できる
  • version4に対応したもので、vega_lite_3.rsも存在する

dataplotlib

Scientific plotting library for Rust
latest commit: 2017/10/14, star: 57
github.com

chord_rs

Rust crate for creating beautiful interactive Chord Diagrams.
latest commit: 2021/01/07, star: 22
github.com

  • Chord Diagramsを描画するためだけのクレート
  • Chord PROなるAPIを叩くclientであり、描画機構については分からない

- アスキーアート系のクレート -

plotlib等でも対応しているが、CLIなどで扱えるようにtext形式でplotするクレートがいくつかある。以下簡単に。

plotという強気の命名がここにある

- 記事外で参考になりそうな記事 -

- おわりに -

まとめた。

Rustでデータ分析する所までやるユーザはあまりいなさそうで、定常分析や監視に使うならHTMLレンダリングは筋が良さそう。
一方plottersが一番活発に開発されているので、何を選択しましょうかという感じ。

 

axumとtch-rsでRustの画像認識APIを作る

- はじめに -

PyTorchのRust bindingsであるtch-rsを使って、画像認識APIを実装する時のメモ。

今回は非同期ランタイムのtokioと同じプロジェクト配下で開発されているaxumを利用する。


 

- axumによるHTTPサーバ構築 -


RustでHTTPサーバを立てるライブラリはいくつかある。現状日本語ではCyberAgent社の以下のブログが詳しい。

developers.cyberagent.co.jp

私自身にあまり選定ノウハウが無いので、今回はtokioから出ているaxumを利用する。

axumを利用してHTTPサーバを構築するにあたっては、repository内のexampleディレクトリに複数の実装サンプルが配置されている他、Tokioのreleaseにも簡単なkickstartが存在するので、そちらを見ながら開発を進めた。

github.com

 

hello world

Cargo.tomlを作成する。

[package]
name = "rust-machine-learning-api-example"
version = "0.1.0"
authors = ["vaaaaanquish <6syun9@gmail.com>"]
edition = "2018"

[dependencies]
axum = "0.2.2"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

localhostにpostリクエストを投げる事でjsonをやり取りするサンプルを書く。

use axum::{handler::post, Router, Json};
use serde::{Serialize, Deserialize};
use serde_json::{json, Value};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", post(proc));
    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

#[derive(Deserialize)]
struct RequestJson {
    message: String,
}

#[derive(Serialize)]
struct ResponseJson {
    message: String,
}

async fn proc(Json(payload): Json<RequestJson>) -> Json<Value> {
    Json(json!({ "message": payload.message + " world!" }))
}

responseはimpl IntoResponseで実装されたものを返す事ができる。ドキュメント内のbuiliding responses節にString、HTML、Json、StatusCodeなどを返す実装イメージが掲載されているので参考にすると良い。routeやMiddlewareを付与する場合も同様に参照すると良い。

cargo runして、以下のhello文字列を投げると「hello world!」になって帰ってくる。

curl -X POST -H "Content-Type: application/json" -d '{"message":"hello"}' http://localhost:3000

 

base64による画像の受信

一旦無難にbase64で画像をやり取りする事を考える。Cargo.tomlに以下を追記する。

base64 = "0.13"
image = "0.23"

先程のスクリプトのpayload.messageを読んでいた箇所をbase64へデコードし、画像として保存するよう変更してみる。

extern crate base64;
extern crate image;

...

    let img_buffer = base64::decode(&payload.message).unwrap();
    let img = image::load_from_memory(img_buffer.as_slice()).unwrap();
    img.save("output.png").unwrap();

clientサイドとして、rustのロゴを取得してbase64エンコードした文字列を投げるPythonスクリプトを書いてみる。

import base64
import json
import requests      # require: pip install requests

sample_image_response = requests.get('http://rust-lang.org/logos/rust-logo-128x128-blk.png')
img = base64.b64encode(sample_image_response.content).decode('utf-8')
res = requests.post('http://127.0.0.1:3000', data=json.dumps({'message': img}), headers={'content-type': 'application/json'})

output.pngとしてRustのロゴ画像がcargo runしているディレクトリにできれば良い。適宜組み替える。

f:id:vaaaaaanquish:20210907135228p:plain
output.png (rust-lang.org/logos/より)

 

ExtensionLayerによるstate管理

機械学習APIなのでMLモデルを一回読み込んでグローバルに扱いたい。axumではExtensionLayerという機能を用いて、stateを実装できる。

https://docs.rs/axum/0.2.3/axum/#sharing-state-with-handlers

ここでは試しにHashSetをstateとしてみる。
AddExtensionLayerを使って、先程のAPIを同名の画像は保存しないように改修してみる。

use axum::{handler::post, Router, Json, AddExtensionLayer, extract::Extension};
use serde::{Serialize, Deserialize};
use serde_json::{json, Value};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::Mutex;
use std::collections::HashSet;

extern crate base64;
extern crate image;

struct DataState {
    set: Mutex<HashSet<String>>
}

#[tokio::main]
async fn main() {
    let set = Mutex::new(HashSet::new());
    let state = Arc::new(DataState { set });

    let app = Router::new()
        .route("/", post(proc))
        .layer(AddExtensionLayer::new(state));

    let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

#[derive(Deserialize)]
struct RequestJson {
    name: String,
    img: String,
}

#[derive(Serialize)]
struct ResponseJson {
    result: String,
}

async fn proc(Json(payload): Json<RequestJson>, Extension(state): Extension<Arc<DataState>>) -> Json<Value> {
    let img_buffer = base64::decode(&payload.img).unwrap();
    let mut set = state.set.lock().await;

    let result;
    if set.contains(&payload.name) {
        result = "skip by duplicated";
    } else {
        let img = image::load_from_memory(&img_buffer.as_slice()).unwrap();
        img.save(&payload.name).unwrap();
        set.insert(payload.name);
        result = "saved output image";
    }
    Json(json!({ "result": result }))
}

先程のPythonスクリプトにname keyを付与してpostしていく。

res = requests.post('http://127.0.0.1:3000', data=json.dumps({'img': img, 'name': name}), headers={'content-type': 'application/json'})
print(res.text)

名前が重複したItemの場合は保存処理が走らず「skip by duplicated」なる文字列が返ってくる。名前がまだHashSet内にない場合はlocalディレクトリに画像が保存され、「saved output image」なる文字列が返ってくるようになった。

インメモリなので一度サーバを落とすと消えてしまうが、機械学習モデルをインメモリに保持する用途であれば十分だろう。

 

- tch-rsによる推論 -

PyTorchのRust bindingsでpretrain済みのモデルを流用して、推論を行うサンプルを過去に公開している。

github.com

こちらを流用して、推論を行うstateを作成しAddExtensionLayerに流す実装を行う。

tch-rsをCargo.tomlに追加する

tch = "0.5.0"

Arc>で囲むようにモデルのstructを定義する

...
use tch::nn::ModuleT;
use tch::vision::{resnet, imagenet};

extern crate tch;

struct DnnModel {
    net: Mutex<Box<dyn ModuleT>>
}

#[tokio::main]
async fn main() {
    let weights = std::path::Path::new("/resnet18.ot"); 
    let mut vs = tch::nn::VarStore::new(tch::Device::Cpu);
    let net:Mutex<Box<(dyn ModuleT + 'static)>> = Mutex::new(Box::new(resnet::resnet18(&vs.root(), imagenet::CLASS_COUNT)));
    let _ = vs.load(weights);
    let state = Arc::new(DnnModel { net });
...

RustのFutureは難しい部分がいくつかあり、私も把握しきれていないが、大まかな外枠は以下を見る事ですぐ把握できる。
zenn.dev
tech.uzabase.com

 
推論部分は一度画像を保存して読み込む形を取る。

...
    let net = state.net.lock().await;
    let img_buffer = base64::decode(&payload.img).unwrap();
    let img = image::load_from_memory(&img_buffer.as_slice()).unwrap();

    let _ = img.save("/tmp.jpeg");
    let img_tensor = imagenet::load_image_and_resize224("/tmp.jpeg").unwrap();
    let output = net
        .forward_t(&img_tensor.unsqueeze(0), false)
        .softmax(-1, tch::Kind::Float);

    let mut result = Vec::new();
    for (probability, class) in imagenet::top(&output, 5).iter() {
        result.push(format!("{:50} {:5.2}%", class, 100.0 * probability));
    }
...

ローカルに画像を保存せずメモリバッファ経由で実装する方法としてload_image_and_resize224_from_memoryが実装されているが、まだreleaseには至っていないようだ。もう少しでインメモリ上で推論が完結しそうである。
github.com


以下のRustロゴ画像を投げてみる

f:id:vaaaaaanquish:20210907135228p:plain
rust logo (rust-lang.org/logos/より)

レスポンスは以下のようになった。

 {
  "result": [
    "buckle 26.54%",
    "wall clock 5.34%",
    "digital watch  5.32%",
    "analog clock 4.14%",
    "digital clock 3.71%"
  ]
}

Rustのロゴはバックルか時計からしい。まあ概ね良さそう。

同様の方法を利用して、PythonのPyTorchで学習したモデルをRust bindings上で再現し推論を行うAPIを作成できるだろう。今回はこの辺でおわる。

 

- おわりに -

手探りの部分もあったが何とかできた。

コードは雑多だが以下に公開している。コメントはよしなにください。

github.com


 

Rustでlabel propagationを実装した

- はじめに -

教師あり学習アルゴリズムの1種であるlabel propagationをRustで実装し、クレートとして公開した。

github.com

本記事は、label propationの実装と検証を行った際のメモである。

 

- label propagationとは -

label propagationは、transductive learningの枠組みの1つでもあり、グラフ構造を利用した機械学習アルゴリズムである。

 
ラベルがあるデータ、ラベルのないデータ、それらを繋ぐエッジがある状態で、ラベルのないデータに付くラベルを推定する事が解きたいタスクとなる。
最もシンプルな実タスクとして例示すると「文書データ等で一部のデータにはラベルがあるが一部欠損している所を推定したい」「ユーザとアイテム、それらを繋ぐPV等のエッジがあり、アイテムにのみラベルがある状態でユーザにもラベル付けを行いたい」といった状況が想定できる。

近年ではCVPR 2019でEmbeddingによる距離をノードとして画像ラベルを推定して利用する手法*1が採択されるなどしており、汎用的なアルゴリズムの1つである。近いワードとしては、tag recommendationなどがあり、PageRankアルゴリズムを利用した手法*2やCollaborative filteringを拡張する手法*3が提案されている他、Content baseな方法もまた考えられる。

実際エムスリーではtag propagationを利用したtag伝搬を用いてユーザのタグ付けを行い、様々な配信のセグメント分けや分析に利用している*4。ハイパーパラメータが少なく、グラフ生成部及び内部の行列計算手前までをオンライン化する事ができ、汎用性が高く安定した結果を得られる所が良いところである。

 
label propagationの問題設計は、 (x_{n}, y_{n})をラベル付きデータにY_{N}=y_{1},...y{n}のC個のラベルが付与されていた時、そこから観測できないuのデータに紐付いたY_{U}=y_{n+1},...y_{n+u}を推定する事にある。データ間の重みwは、古典的にユークリッド距離dとハイパーパラメータ\alphaを用いて簡素に以下のように表現される。

 w_{ij} = exp \bigg( - \frac{d_{ij}^{2}}{\alpha^{2}} \bigg) = exp  \bigg( - \frac{ \sum_{d=1}^{D} ( x_{i}^{d} - x_{j}^{d} )^{2} }{\alpha^{2}} \bigg)

これは最も簡素な例で、距離に関しても時に離散的な距離であったりDNNのEmbeddingから得られる距離であったりする。wを作るためには、(n+u)\dot(n+u)の確率遷移行列を作ってやればよい。

行列の最適化のためのアプローチは、いくつか方法があるが、概ね以下が詳しい。

ベースは、グラフ上で隣接するノードは同じラベルを持つ可能性が高い、という所に基づいて設計した目的関数を最小化することでweight行列を最適化する。「隣接ノードが同じラベル」の閾値をパラメータや推論によってコントロールする拡張が主である。

 
label propagationは、Pythonではsklearn内にも実装されており、簡易に呼び出す事ができる。
sklearn.semi_supervised.LabelPropagation — scikit-learn 0.24.2 documentation

 
よりグラフィカルな解説は以下が参考になる。オススメ。


 

- Rustによる実装 -

先に示した通り、確率遷移行列を作って最小化できれば良いので、行列演算を行う事になる。

今回はndarrayを利用して実装している。rust/ndarrayのドキュメント内にnumpyからの移行のススメがあるので、基本的にはここを参照すると良い。

docs.rs

   
numpyにはadvanced-indexingという機能がある。

Indexing — NumPy v1.21 Manual
こういうやつ

x = np.array([0, 1])
y = np.array([[0, 0], [0, 0], [0, 0]])

y[x] = 1

# array([[1, 1], 
#             [1, 1], 
#             [0, 0]])


rustのndarrayでは、現状実装されていないのでslice_mutで指定インデックスごとにスライスを作ってfillterによる代入を行う必要がある。

for i in x {
    y.slice_mut(s![*i, ..]).fill(1);
}

 
機械学習で行列を扱う時は大体スパースな事が多く、実装としてsparse matrixを使う事が多い。現状ndarrayにはsparse matrixに類似するものは実装されていなさそう。同じく行列演算を趣旨としたnalgebraにはnalgebra_sparse::csr::CsrMatrixがあるが、こちらはdot積などが実装されていない。
なのでArrayBaseで押し切る実装になってしまった。メモリに優しくない。linfaなど、一部ライブラリで独自にsparse matrixを実装しているものもあるが、クレート依存が激しい。

以下のクレートを試してみてはという助言を貰ったので検証中ではある。
github.com
この辺何か良い方法があるんだろうか。知っている人居れば教えて欲しい。

上記以外はdot積と行列変換が扱えれば良いのでndarrayで十分実装できる。

 

- 検証 -

irisデータセットを利用して、一部のラベルを欠損、各データのユークリッド距離をエッジと考えて、label propagationにより欠損ラベルを推論する。

公開したlabel-propagation-rsには、label propagationの派生アルゴリズムとして、LGCとCAMLPを実装しており、検証にはCAMLPを利用した。

Rustにおけるsklearnのような立ち位置になるライブラリであるsmartcoreよりirisデータセットを読み込んで行列を作る。閾値としてユークリッド距離の逆数が0.5以下になっている場合はエッジを繋がないものとする。

...
    let iris = iris::load_dataset();

    let node = (0..iris.num_samples).collect::<Array<usize, _>>();
    let mut label = Array::from_shape_vec(iris.num_samples, iris.target.iter().map(|x| *x as usize).collect())?;
    let mut graph = Array::<f32, _>::zeros((iris.num_samples, iris.num_samples));

    let data = Array::from_shape_vec((iris.num_samples, iris.num_features), iris.data)?;
    for i in 0..iris.num_samples {
        for j in 0..iris.num_samples {
            if i != j {
                let weight = 1. / (*&data.slice(s![i, ..]).sq_l2_dist(&data.slice(s![j, ..]))? + 1.);  // reciprocal
                if weight > 0.5 {
                    graph[[i, j]] = weight;
                }
            }
        }
    }
...

ざっくり10個ターゲットを選んで、ノードに付与されたラベルを0にする。irisのラベルは0,1,2のどれかなので「ランダムにあるラベルが0になってしまった」という状況になる。

...
    let target_num = 10;
    let mut rng = thread_rng();
    let target = (0..iris.num_samples).choose_multiple(&mut rng, target_num).iter().map(|x| *x).collect::<Array<usize, _>>();
    for i in &target {
        label[*i] = 0;
    }
...

モデルを学習させて、上記で0にしたターゲットのlabelを推定する。

...
    let mut model = CAMLP::new(graph).iter(100).beta(0.1);
    model.fit(&node, &label)?;
    let result = model.predict_proba(&target);

    for (i, x) in target.iter().enumerate() {
        println!("node: {:?}, label: {:?}, result: {:?}", *x, iris.target[*x], result.slice(s![i, ..]).argmax()?);
    }
...

結果は以下のようになった。

node: 0, label: 0.0, result: 0
node: 14, label: 0.0, result: 0
node: 67, label: 1.0, result: 0
node: 118, label: 2.0, result: 2
node: 43, label: 0.0, result: 0
node: 144, label: 2.0, result: 2
node: 91, label: 1.0, result: 1
node: 137, label: 2.0, result: 2
node: 49, label: 0.0, result: 0
node: 62, label: 1.0, result: 1

node 67のみ、真のラベルが1に対して推論ラベルが0となってしまっているが、それ以外は正解している。良い感じ。実際どういったデータで各metricでどの程度の精度が出るかはこれから検証していく。

上記の検証コードはexample内にある。

label-propagation-rs/examples at main · vaaaaanquish/label-propagation-rs · GitHub

 

- おわりに -

label propationの実装と検証を行い、クレートとして公開した。

まずは動く所までという感じ。

できればどこかでPythonとの比較をやりたい。

 

*1: A. Iscen, G. Tolias, Y. Avrithis, O. Chum. "Label Propagation for Deep Semi-supervised Learning", CVPR 2019 https://openaccess.thecvf.com/content_CVPR_2019/papers/Iscen_Label_Propagation_for_Deep_Semi-Supervised_Learning_CVPR_2019_paper.pdf, github: https://github.com/ahmetius/LP-DeepSSL

*2:Heung-Nam Kim and Abdulmotaleb El Saddik. 2011. Personalized PageRank vectors for tag recommendations: inside FolkRank. In Proceedings of the fifth ACM conference on Recommender systems (RecSys '11). Association for Computing Machinery, New York, NY, USA, 45–52. DOI:https://doi.org/10.1145/2043932.2043945

*3:Kim, Heung-Nam, et al. "Collaborative filtering based on collaborative tagging for enhancing the quality of recommendation." Electronic Commerce Research and Applications 9.1 (2010): 73-83. https://www.sciencedirect.com/science/article/pii/S1567422309000544

*4:エムスリーにおけるグラフ構造を用いたユーザ興味のタグ付け - Speaker Deck

Pure Rustな近似最近傍探索ライブラリhoraを用いた画像検索を実装する

f:id:vaaaaaanquish:20210810063410p:plain

- はじめに -

本記事は、近似最近傍探索(ANN: Approximate Nearest Neighbor)による画像検索をRustを用いて実装した際のメモである。

画像からの特徴量抽出にTensorFlow Rust bindings、ANNのインデックス管理にRustライブラリであるhoraを利用した。

RustとANNの現状および、実装について触れる。

 

 

- RustとANN -

Rustの機械学習関連クレート、事例をまとめたリポジトリがある。

github.com

この中でも、ANN関連のクレートは充実している。利用する場合は以下のようなクレートが候補になる。

* Enet4/faiss-rs
* lerouxrgd/ngt-rs
* rust-cv/hnsw
* hora-search/hora
* InstantDomain/instant-distance
* granne/granne
* qdrant/qdrant

Pythonでもしばしば利用されるfacebook researchのfaiss、Yahoo!のNGTのrust bindingsは強く候補に上がる。C++からGPUが触れる点から利用だけならfaissが活用しやすいだろう。

 
他にPure Rustで機能が充実しているクレートにhoraがある。
github.com

horaには、PythonJavascriptJavaのbindingsがあるだけでなく、Pure Rustである事でWebAssembly化などもサポートしている。
また、インデキシングアルゴリズムとして多く利用されているHNSWIndex以外にグラフベースのSatellite System Graph*1、直積量子化を行うProduct Quantization Inverted File*2が実装されており、開発が継続されている数少ないクレートである。
一部SIMDによる高速化が図られている(https://github.com/rust-lang/packed_simdによるもの)。

(horaの由来は「小さな恋の歌」とREADMEに書いてあるが、どういう経路で知られたのかよくわからない)

今回は、画像検索のwasm化を目指し、horaを利用する。
画像検索がwasm化する事で、API経由で行われていた画像検索の一部がエッジデバイス上で処理できる可能性などの幅が出る事を期待する。
例えば、ネット環境を扱えないや工場やサーバセンター、病院であったり、個人情報の観点でスマフォやカメラの外に出せない画像をその場で類似画像検索にかける事ができる可能性である。

 
画像特徴を抽出する部分でもwasm化を目指すため、wasmの利用実績が多いTensorFlowを利用する。

TensorFlowにはRust bindingsが存在する。
github.com

今回はこちらを利用してモデルを作成し、wasm化する。
他にもDNNのライブラリはいくつかあるが、開発が活発でないか、PyTorchのRust bindingsは現在中間層の出力を受け取る方法がないなど、機能的に難しい場合が多かった。

(実験時に作成したPyTorchのRust bindingsでpretrain modelのpredictを実行するdockerなども公開している https://github.com/vaaaaanquish/tch-rs-pretrain-example-docker

 

- pretrainモデルによる特徴量化 -

TensorFlow 2.xでのRustとPythonの相互運用に関する以下の記事を参考にした。

TensorFlow 2.xでのRustとPython

import tensorflow as tf
from keras.models import Model
from tensorflow.python.framework.convert_to_constants import \
    convert_variables_to_constants_v2

# pretrainモデルの読み込み
model = tf.keras.applications.resnet50.ResNet50(weights='imagenet')

# 中間層の出力を得るモデルにする
embedding_model = Model(inputs=model.layers[0].input, outputs=model.layers[-2].output)

# tf.functionに変換しpbファイルとしてgraphを保存できる状態にする
resnet = tf.TensorSpec(embedding_model.input_shape, tf.float32, name="resnet")
concrete_function = tf.function(lambda x: embedding_model(x)).get_concrete_function(resnet)
frozen_model = convert_variables_to_constants_v2(concrete_function)

# fileをdumpする
tf.io.write_graph(frozen_model.graph, '/app/model', "model.pb", as_text=False)

Rustのbindingsから読み込み、画像ファイルを特徴量に変換する。

// モデルファイルを読み込み、セッションを作る
let mut graph = Graph::new();
let mut proto = Vec::new();
File::open("model/model.pb")?.read_to_end(&mut proto)?;
graph.import_graph_def(&proto, &ImportGraphDefOptions::new())?;
let session = Session::new(&SessionOptions::new(), &graph)?;

// 入力画像を読み込み、リサイズしてTensorに変換する
let img = ImageReader::open("./img/example.jpeg")?.decode()?;
let resized_img = img.resize_exact(224 as u32, 224 as u32, FilterType::Lanczos3);
let img_vec: Vec<f32> = resized_img.to_rgb8().to_vec().iter().map(|x| *x as f32).collect();
let x = Tensor::new(&[1, 224, 224, 3]).with_values(&img_vec)?;

// DNNに入力する
let mut args = SessionRunArgs::new();
args.add_feed(&graph.operation_by_name_required("resnet")?, 0, &x);
let output = args.request_fetch(&graph.operation_by_name_required("Identity")?, 0);
session.run(&mut args)?;

// check result
let output_tensor: Tensor<f32> = args.fetch(output)?;
let output_array: Vec<f32> = output_tensor.iter().map(|x| x.clone()).collect();
println!("{:?}", output_array);

出力として、特徴量vectorが得られる。

 

- 画像特徴のインデックスと検索 -

horaを利用して画像検索を行う。

// init index
let mut index = hora::index::hnsw_idx::HNSWIndex::<f32, usize>::new(2048, &hora::index::hnsw_params::HNSWParams::<f32>::default(),);

// 特定ディレクトリの画像ファイルをインデックス
let paths = fs::read_dir("img")?;
let mut file_map = HashMap::new();
for (i, path) in paths.into_iter().enumerate() {
    let file_path = path?.path();
    let path_str = file_path.to_str();
    if path_str.is_some() {
        file_map.insert(i, path_str.unwrap().to_string().clone());  // ファイル一覧を作成
        let emb_vec = emb.convert_from_img(path_str.unwrap())?;     // 画像特徴を得るメソッド
        index.add(emb_vec.as_slice(), i)?;                          // インデックス
    }
}
index.build(hora::core::metrics::Metric::Euclidean).unwrap();

// 画像をqueryとして検索
let query_image = &file_map[&100]
let emb_vec_target = emb.convert_from_img(&query_image.to_string())?;  // 画像特徴を得るメソッド
let result = index.search(emb_vec_target.as_slice(), 10);              // 特徴量をqueryとし検索
println!("neighbor images by query: {:?}", query_image);
for r in result {
    println!("{:?}", &file_map[&r]);
}

これらのコードは以下に公開している。

また、上記にはfood-101データセットを用いたインデキシングのサンプルが配置してあるため、今回はそちらを利用して検索の動作確認を行った。

www.tensorflow.org

 

- 検索結果 -

query画像をランダムに選択してTop5の画像を目視でチェックする。

f:id:vaaaaaanquish:20210810060405p:plain
餃子queryとTop5
f:id:vaaaaaanquish:20210810061939p:plain
ラーメンqueryとTop5

餃子は1つだけ間違えて寿司を引いてきているが概ね良さそう。

カテゴリを利用した精度測定などが考えられるが今回はここまで。

- おわりに -

Rustによる画像検索を実装し、動作を確認できた。

エッジデバイスやスマフォ上での画像検索が出来るようになってくると、インデックスファイルを小さくしても精度が保てるモデルの研究が出てきたりするかもなと妄想することができた。

コードは以下に公開した。
github.com

wasm化した上での画像検索は出来てはいるので次はそちらを書く。

Rustによる機械学習概覧を技術書典11に寄稿するまでの軌跡

f:id:vaaaaaanquish:20210709094552p:plain

- はじめに -

今回、技術書典11に「Rustによる機械学習概覧」というタイトルで、所属企業であるエムスリー株式会社の執筆チームより出る「エムスリーテックブック3」に文章を寄稿した。

執筆チームからの熱いコメントは以下。

販売ページは以下。
techbookfest.org

本ブログは、エムスリーテックブック3を企画して立ち上げてから、自分で同人誌を書くまでのお気持ちを綴った、所謂ポエムである。

- Rustによる機械学習への想い -

ポエムといえば自分語り、自分語りといえばポエム。まず思い出に浸ろう。

私が機械学習を初めて実装したのは高専の頃。あの時はC/C++JavaC#なんかを使って、何とかアルゴリズムを理解して実験していた。VisualStudioの起動に悠久の時が必要だったので、朝研究室に寄ってPCとVisualStudioを起動するボタンだけ押して授業に行ったものだ。遺伝的アルゴリズムでゲーム攻略したり、音楽作ったり、ニューラルネットでどうでも良いネットの動画の再生数とかを回帰で解いて遊んでいた。大学に入って、MatlabLispを触ったが、何より初めて触って衝撃的だったのはPythonだったと思う。当時は、OpenCVが驚異的に簡単に扱えてNumpyで演算が出来て、OpenMPやCUDAといった資産も扱えるすごいFFIを備えたやつという感動があった。当時のDeep Learningのライブラリといば、cudaないしドライバのinstall battleに始まり、激しいPythonのインターフェースとバージョン差異との戦いの連続だった。そして、prototxtオジサンと呼ばれる(私が勝手に呼んでいる)「昔はprototxtでネットワークを定義してたんだよ…」と若者に言って回る人を大量に生み出していった。私もだ。この時は想像もしなかったが、後に出るChainerは偉大なのである。その前後、何故か縁があり、アルバイトもはじめてPythonでCNNやらを実装してワイワイしていた。いつの間にかPythonを沢山書いて、研究でも利用するまでに至っていた。Pythonだけとは言わず、一時期Juliaを使って更に良いとなってコンペに出て入賞したり、Julia Tokyoにも登壇した。就職して最初の会社はC#の会社だった。画像認識をやっている部署だったが、OpenCVSharpの開発者が居たのが大きい。C#は好きな言語の1つ。その会社にPythonAPIを初めて導入する役割もやった。ライブラリなど様々な事情でPython2で実装したのを今でもたまに悔やみ心の中で謝っている。一度転職を経てからはPythonがメインになった。転職先は大きい企業だったので、Rが得意なおかしな人が沢山居てRの講義なんかも受けた。R悪くない。この頃にはもうPythonにおけるDeep Learningフレームワークというやつは概ね今の基盤が出来上がっていたように思う。破壊的変更で死んだり、supportが終わったり色々あったものの、分散化やタスクの複雑化、モデルの巨大化を見れたのは楽しかった。次の転職先、つまり現職では本当にPythonだけになった。速度の遅いPython機械学習というタスクで好まれる訳がないと言っていた私もあの人達も、みんなwrapper言語としてのPythonを業務で書くただの人になっていった。

はてさて、雑に今までのプログラミング言語機械学習の思い出を振り返ってみた。
私と同世代くらいの人は頷く場面もあるかもしれない。

私自身、ここまで、様々なプログラミング言語を使って、機械学習アルゴリズムを書いたり使ったりしてきた訳だが、現状それらは概ねPythonとRに収束しつつある。例外を除けば、一部C/C++を書く場合があるといったイメージだと認識している。

そんな中で、私は1年ほど前にRustと出会った。Rustは、非常に良いプログラミング言語であり、私は機械学習分野にもパラダイムシフトを与えてくれると思っている。

お気持ちだけじゃなく、実際Rustで形態素解析から機械学習モデルを使った分類タスクを解くExampleになるようなブログを趣味で書いたりしている。
vaaaaaanquish.hatenablog.com

それをwasmにして実際にWebサービスにしたりして遊んでいる。
vaaaaaanquish.hatenablog.com
このサービスはなんやかんや2万強のアクセスがあるので、Rust × MLなWebサービスでも結構多いユーザだったのではと思うが、いかんせん他の事例が無さ過ぎて分からないままである。

他にもRustによる機械学習実装、ブログ、動画、本、事例、実装例をまとめたRepositoryを作ったりしている。あんまり更新してないけどStarして欲しい。
github.com

LightGBMのRust bindingsも作っている。最近LightGBM本家のREADMEに載って、Microsoftの人からGreat的なメールを貰って嬉しかった。褒めて欲しい。
github.com


大体これくらいやっていると「Rustで機械学習する価値って何?」「Rustは何が良いの?」「PythonやRはなくなるの?」という声が必ず聞こえてくる。

なんと、それに答える気持ちを全部乗せて書いたのがこのエムスリーテックブック3だ。是非読んで欲しい。
techbookfest.org


中身から少しだけ抜粋するが、Rustユーザ、機械学習ユーザの意見は概ねどのディスカッションや事例でも一致している。機械学習をやる上でのRustの良さは速度と既存の資産との相性、wasmの存在になる。どのディスカッションもC/C++で書かれた一部が置き換わる、wasm利用例が増える、エッジデバイスや高速化などが必要なピンポイントでの利用が増える、という所に落ち着いていて、PythonやRのエコシステムは残るし、むしろそれらと協業してやっていく良い形が探られるだろうというものになっている。

かつて色々な言語を乗り越えて、Pythonという言語が今機械学習やデータサイエンス業界で使われている。今度は、乗り越える対象ではなく、その屋台骨の一つとしてRustがくるぞという話なのだから、これはエムスリーテックブック3を買って未来に想いを馳せる他無いだろう。

 

- エムスリーテックブック3の立ち上げ -

今回、技術書典に出すにあたって私が「やりましょう!」と言い出して、社内説明会を開いて、人を集めて、Re:VIEWやCIを整備して、レビューして、最後TeXでフォーマットを直して出稿するところまでを主導した。

エムスリーとして技術書典に出るのは私自身は2回目で、前回エムスリーテックブック2でも執筆した。
エムスリーテックブック#2:エムスリーエンジニアリンググループ執筆部
この時は、同僚の@mikesoraeが主導していたが、コロナ禍でオンラインになり勢いが弱まっていたので、手を挙げた。

大して書きたい気持ちが強かった訳でもネタがあったわけでもないが、技術書典のような業界内でも著名なイベントに少しでも多くの若く優秀な同僚を出して市場価値を上げて欲しい気持ちがあった。本を書くのは大変なので、経験しておくだけでも良いし、ブログより長い文章で公開できる技術書にするまでの工程は、自身の知識の整理と技術説明力の向上に役に立つ。会社の広報が難しいオンラインの時期だからこそ、PDF以外に「後から配送」による物理本を送れる技術書典は、良い広報にもなると何となく思っていた。

f:id:vaaaaaanquish:20210708000004p:plain:w400
説明会のよびかけ
f:id:vaaaaaanquish:20210708000038p:plainf:id:vaaaaaanquish:20210708000045p:plain
説明会の資料の一部

そういう想いや過去の売上から会場、打ち上げの様子、大変な部分をスライドにまとめて、社内説明会を開いた。テックブログの平均文字数と比較して「ね?簡単でしょ!」と言おうと思ったけど、同人誌とはいえ普通にちゃんとした本を書くのは大変だという事もわかった。

こんな感じで調子良くやってたが、書くのはめっちゃ大変で結果出来上がったのは7月入ってからだった。申し訳ねえ。

でも実際エムスリーテックブック3は、私が読み返しても結構面白い多様な分野と専門性のある仕上がりになっているし、後は皆さんに買ってもらうだけだ。少しでもエムスリーテックブック3の良さが伝わればと想い、今こうしてポエムまで書いている訳なので、是非とも手に取って頂きたい。

今は、社内向けのRe:VIEWテンプレートを作って、Confluenceに技術書典に出るまでの手順書、過去の経歴のまとめを作っている。いずれ、私が居なくなったとしてもこの文化が続いて欲しいと強く思うし、この書籍でエムスリーを知る人、またこの書籍でエムスリーの優秀な若手のエンジニアが採用したくなっちゃう人が続々出てきて欲しい。

 

- おわりに -

久々にポエムを書いた。

ちなみに他の寄稿者良いです…

買ってね。
techbookfest.org

 
 
P.S.

自分が書こうと思った時、既に良い本が世の中にはあるものだ。

 
 

pipとpipenvとpoetryの技術的・歴史的背景とその展望

- はじめに -

Pythonのパッケージ管理ツールは、長らく乱世にあると言える。

特にpip、pipenv、poetryというツールの登場シーン前後では、多くの変革がもたらされた。

本記事は、Pythonパッケージ管理ツールであるpip、pipenv、poetryの3つに着目し、それぞれのツールに対してフラットな背景、技術的な説明を示しながら、所属企業内にてpoetry移行大臣として1年活動した上での経験、移行の意図について綴り、今後のPythonパッケージ管理の展望について妄想するものである。

注意:本記事はPythonパッケージ管理のベストプラクティスを主張する記事ではありません。背景を理解し自らの開発環境や状態に応じて適切に技術選定できるソフトウェアエンジニアこそ良いソフトウェアエンジニアであると筆者は考えています。

重要なポイントのみ把握したい場合は、各章の最後のまとめを読んで頂ければと思います

 


 

- Pythonのパッケージ管理ツール概論 -

まずはじめに、2020年3月現在、主に「Python パッケージ管理」等のワードでググると出てくる3つの大きなツール、pip、pipenv、poetryについて概論的に説明する。

最も表面的な概要だけ把握するために、私が社内向けに作成した資料を以下に引用する。

f:id:vaaaaaanquish:20210321112826p:plain
社内LT向けに作成した資料より加筆の上抜粋

多くは後述するが、ここでスライドが主張している重要な点を文字におこしておくと以下のようになる。

  • pipenvはPyPAが開発しているツールで同じくPyPAが開発するpipの機能を補っている
  • pipenvとpoetryの大きな"機能"の違いはパッケージのbuild・publishをサポートしているかどうか
  • pipenv、poetry以外に他にも多くのパッケージ管理ツールが存在する

本記事のタイトルを解消する上で重要な点は上2つであり、これらについてpip、pipenv、poetryの概要を示しながら本章で触れていく。本記事においては、Anacondaや他ツールについては本筋ではないため、本章の最後に簡単にまとめて触れる。

pip

pipは、最も基本的なPythonのパッケージ管理ツール、より正確にはパッケージインストーラである。PyPI*1より、Pythonモジュールをインストールするためのツールであり、Pythonの拡充提案、プロセス、設計、標準を決める文書群であるPEP内の、PEP 453によって、Pythonが公式にサポートするツールとなっている。Python3.4からは、Pythonに内包されデフォルトでインストールされる。
github.com


パッケージ管理の側面から見た場合は、以下のようなrequirements.txtというファイルを記述し、そのファイルで利用するモジュールのバージョンを管理する形式を取る。

numpy<=1.0.0
pandas==1.1.5
jupyterlab>=2.0.0
...

pipは具体的に依存treeを生成するような依存解決は行わず、順番にインストールしていき、サブモジュールの依存関係に問題があった場合は上書きする事で解決する。上記であればpandasやjupyter labによってnumpyはバージョンアップしたものが入る事になる。

pipと後述するpipenvの関係において、上記を背景とし一般的なプログラミング言語がプロダクト開発でそれを必要とするのと同様に「依存関係を解決する」「lockファイルを生成しサブモジュールを含む全てのモジュールバージョンをhashで管理する」事が求められた結果pipenvが開発された、という認識は間違いではない。実際、上記のrequirements.txtだけではパッケージングを行う事は出来ない事は想像できるだろう。
長らくユーザはsetup.pyやsetup.cfgを書き、PyPAが別途作成するwheelsetuptoolsといったツールでパッケージング、また別途PyPAが作成するtwinを用いてパッケージレジストリであるPyPIにアップロードするというライフサイクルに倣っており、バージョン固定や開発環境管理は各ユーザに委ねられていた。pipenvやそれら以外のツールについて具体的には後述するが、pip自体が手続き的なバージョン管理方法やパッケージングの方法を持っていないが故に、別ツールとしてパッケージングツールが作られていった結果として、今多くのパッケージング管理関連ツールが生まれ続けるPythonの環境になっているとも言える*2

 
2021年においては、パッケージ管理の側面で、上記の前提に加えてpipの2つの大きな変更を把握しておく必要がある。それがpyproject.tomlと2020-resolverである。

 

pyproject.toml (PEP 518)

1つ目に、PEP 518で制定されたpyptoject.tomlというフォーマットがある。後述のpoetryで利用されている事は広く知られているが、pipもまたpyproject.toml対応しているという点が重要になる。

pyptoject.tomlは、以下のような単一のtomlファイルをrequirements.txtやそれらを参照するsetup.pyの代替とする事が出来るようになっている*3

[install_requires]
pandas = "*"

[build-system]
requires = ["setuptools", "wheel", "toml"]

これはpip単体では大きな旨味はないが、pyproject.tomlが単一のファイルでパッケージのメタデータや依存ライブラリ、フォーマッタやenv環境、buildの設定を記述できる事を踏まえ、後述のpoetryのようなPEP 518対応のパッケージングツールと組み合わせる事で、ファイル及びツールを小さく集約する事が可能になっている*4

かなり簡単に言えば、これだけ書いとけば開発関連の設定はオールオッケーなsettings.tomlである。

pyproject.tomlについてより詳しくは後述するが、現在最も注目される仕様の1つであり、把握しておくと良い。

 

2020-resolver

pipにおける重要な変更として、2つ目に、現在テスト段階にある依存解決を行うリゾルバ、2020-resolverのリリースがある。

先述の通り、pipは強い依存解決を行わないツールだった。故にpip単体では、依存モジュールのバージョンの違いと、それらのインストールの前後関係によって問題が発生してしまう*5。対して2020-resolverは、シンプルなbacktrackingを利用した依存解決リゾルバであり、強い依存解決を事前に行う事で、pipのインストールの前後関係による課題を解消するものである。

リリースブログより各リンクを追う形で、多くの議論が追える。
pyfound.blogspot.com
 
 

さて、ここで本記事で最も重要な比較の話をするため、依存解決というテーマに触れておく。

多くのパッケージ管理ツールが行う重要な機能の1つに依存解決がある。パッケージの依存解決を行うリゾルバ(resolver)が、非常に難しい問題に挑んでいる事は、dependency hellという言葉と共によく知られている通りである。

en.wikipedia.org

依存解決リゾルバのアルゴリズムでは、SAT*6における教科書的なbacktrackingアルゴリズム*7を利用した解決方法が知られている*8。有名所では、RubyのBundlerが依存解決のために利用しているMolinilloというライブラリがそのresolverのbacktrackingロジックを持っていたり、RustにおけるCargoのコアロジックにあたるrust-semverでもDFS(深さ優先探索)とbacktrackingが利用されている。言語系の依存解決リゾルバが〜〜〜という話ではなくOpenSUSEのLibzyppでもminiSATベースのライブラリであるlibsolvが利用されている。
 
また、近年では2018年にGoogleのNatalie Weizenbaum(@nex3)Dartのパッケージ管理ツールのために提案したPubGrub*9というアルゴリズムが話題になっている。PubGrubは、backtrackingの拡張アルゴリズムであり*10、非互換性のあるパッケージを見つけた時に効率的に探索を行うUnit Propagation*11を組み合わせる形で実現されている。PubGrubは効率的かつ解釈可能な非互換性を見つけられる点でより優れたロジックで、SwiftPMにマージされていたり、先に登場したCargo内でも議論があったり、RubyにもPubGrubベースのgel-rbという新しいパッケージ管理ツールが出てきていたり、Elm版の開発等も進んでおり、後述するpoetryでもPubGrubが利用されている

PubGrubの提案者による解説記事。Dart側のドキュメントと照らし合わせながら読むと、Unit Propagation、Conflict Resolution、Decision Makingの対応が頭に入りやすく分かりやすい。
medium.com

 
本題のpip 2020-resolverでは、シンプルなbacktrackingが採用されている。このロジックは以下で議論されている。
github.com

ここでは、PubGrubベースのMixology*12やbacktrackingベースのResolveLibZazo、他にも先述したlibsolvMicrosoft Researchが提案したZ3などを比較し検討している。途中でpoetryのメンテナである@sdispaterがpoetryのリゾルバ部分を書き出したrepoを提示していたり依存解決リゾルバのアドバイスを行っている所も面白い。議論の結果、APIインターフェースやコミュニティ、開発速度の観点から、PyPAメンバーである@uranusjrが作成していたbacktrackingベースのresolvelibが使われる事になった事がわかる。解説は以下のブログ。
pradyunsg.me

ここでの選定理由は、特にピュアPythonで書かれている事がコミュニティないし支援企業がついている中で開発速度にとって嬉しい事が大きい要素になっている。
pip 2020-resolverのプロジェクトにはCZI社*13Mozilla*14による金銭的のサポートが入っており、PyPAにとっても重要なテーマであると言えるわけで、開発速度が得られるに越したことはない背景があった。
pyfound.blogspot.com
PyPAのメンバーであるソフトウェアエンジニアが、仕事の傍らボランティアで行ってきたpipの開発について、この2020-resolverの開発終了まで金銭面でのサポートする内容になっている。

 
この2020-resolverで、pipは強い依存解決を行うようになり、長らくpipで言われていた

“Why does pip download multiple versions of the same package over and over again during an install?”.
(なぜpipは同じパッケージの複数のバージョンを何回もダウンロードするのか?)

という大きな課題が解消し、速度も向上する事になる。この背景について、PyPAのユーザガイドにも長文で綴られているので、見ておくと良い*15
pip.pypa.io

 
 
少し横道に逸れるが、2020-resolverへの変更によってpip installで依存解決が失敗したらResolution Impossible エラーを出すようになる。これにより、多くのCI/CDが止まってしまったり、OSSに「あなたのパッケージpipでインストールできないんだけど」というissueが増えるのではといった懸念が思い付く。これらは、2020-resolverの展開方法として以下のissue等で議論されている。
github.com
大きな問題があればissueを追うか、ユーザガイドが示す通り、『How do I ask a good question?』を読んだ上で以下のコミュニティの何れかで質疑すると良い。

 

2020-resolverは2020年10月からデフォルトになっており、21.0には完全に古いリゾルバを使えなくなる*16今までpipenv等が依存解決を行っていた所にパッケージインストーラーだったpipが依存解決を行うようになるわけなので、全体に影響する大きな変化の1つであると言える。

 

pipenv

pipenvは、pipに対して、依存解決およびlockファイル生成、env環境制御機能の提供を行う、PyPAが開発するパッケージ管理ツールである。依存解決およびlockファイル生成にはpip-tools、env管理はvirtualenvを利用しており、それらを1つのPipfileと呼ばれる独自フォーマットで管理できるツールとなっている。
github.com

Pipfileは以下のようなフォーマットになっている。

[dev-packages]
coverage = "*"
yapf = "*"
mypy = "==0.580"

[packages]
numpy = "<=1.0.0"
pandas = "==1.1.5"

[requires]
python_version = "3.9"

ここでは、開発用のパッケージ、モジュールが利用するパッケージ、envの設定を記述する事ができる。

pipとの大きな違いは、先に挙げた依存解決、lockファイル生成、env環境の制御である。

依存解決アルゴリズムでは、pip-tools内で先程の2020-resolverと同様のbacktrackingを利用している*17
github.com
pipenvは、pip-toolsが持つlockファイル生成の機能をwrapしている状態であると言える。

env環境については、同じくPyPAが開発するvirtualenvの機能をwrapし、env環境を提供する形となっている。
github.com
先に示した通り、pipが依存解決を行わない動作をしていた頃に、依存解決の機能に加え、一般的なソフトウェア開発に必要なenv環境の提供を可能にしたツールである。

 
pipenvはpip同様、パッケージングについては完全に別で処理する思想を持っている。setup.pyやsetup.cfgを記述し、それぞれ別のツール(wheeltwin)を利用してbuild, publishを行いPyPIに公開するライフサイクルとなっている。publishに関連したそれぞれのツールもまたPyPAが開発しており、PyPAが示すPythonパッケージングのライフサイクルの主たる形であると言える*18

 

poetry

poetryは、pipenvに対して先のpyproject.tomlの仕様を拡張を利用し、依存解決からlockファイル生成、env環境管理、パッケージングまでを行えるようにしたパッケージ管理ツールである。先のPipenvと違い、PyPAではなく、1ユーザである@sdispaterが主導し開発されている。
github.com

READMEには『Why?』という章がありそこでは、pipenvに対する課題とpoetryを作成した経緯が書かれている。要約すると以下の3点になる。

  • setup.pyやrequirements.txt、Pipfileを導入せずcargoやcomposerのようにbuildしpublishしたい
  • pipenvの意思決定のいくつかが好みでなかった
  • より良い依存解決リゾルバの導入

 
まず、1つ目の課題を解決したのが、先にも紹介したpyproject.tomlである。pyproject.tomlには以下のようなセクションを記載する事で、パッケージングに関する情報を付与でき、poetryがこの情報を読む事でbuild、publishが出来るようになっている。

[tool.poetry.dependencies]
python = "^3.6.2"
pandas = "*"

[tool.poetry.dev-dependencies]
coverage = "*"
yapf = "*"
mypy = "==0.580"

[tool.poetry]
name = "sample"
description = "This is sample"
authors = ["author"]
version = "1.0.0"
readme = "README.md"
homepage = "https://github.com/author/sample"
repository =  "https://github.com/author/sample"
documentation = ""
keywords = ["Example"]

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

[tool.poetry]となっている箇所は、poetry独自の拡張セクションである。pyproject.tomlとしてフォーマットが決まっている事で、拡張セクションを追加出来るようになっており、そちらを上手く利用した形になる。

  
依存解決については、先述の通り、backtrackingを採用せず、PubGrubを最初から採用している。これによって、依存解決を高速に行えるだけでなく、backtrackingが依存ツリーを戻る量で制限しているような枝を探索する事ができるようになっている。具体的には、READMEに書いてある以下のような例でpipenvが失敗する依存解決を遂行できるようになっている。

pipenv install oslo.utils==1.4.0

実際はpipenvが利用するpip-toolsのリゾルバとの比較になるが、このリゾルバの違いによる問題については、pipenvとpoetryの違いに関する章で後述する。

 

Anacondaなど他ツール

少し記事タイトルと逸れるが、現状を知る意味で概論として他のツールにも触れておく。

 
ここまで書いてきたように、Pythonのパッケージ管理エコシステムは、常々PyPAという団体によって整備され、PEPを通して制定され、PyPAが、インストーラとなるpipを代表にパッケージングであるsetuptoolsやwheel、PyPIへのアップロードはtwine、仮想環境はvirtualenv、依存固定はpipenvとそれぞれ作ってきた経緯がある。

そんな中で、PyPA以外の全く別のツールや似通ったシステムが栄枯盛衰を繰り返しているのは、常々人が作るソフトウェアの難しさに触れる事に近しい。最初から誰もが満足するツールなど作れないし、Python自体のユーザが増えた事によるOSS界隈の情勢の変化、影響範囲の拡大、速度、文化など様々な要素がひしめき合った結果、poetryやその他多くのツールが生まれたと言える。


中でもAnacondaは、pipがまだ多くのライブラリのインストールに失敗しバージョンやenvを管理する方法が定まって無かった中で、挑戦的な方法を取ることで出てきたパッケージング管理ツールである。

Anacondaは、Anaconda,Incという企業が開発、運用している点やその他GUIやenv提供の機能を持つ特徴こそあるが、パッケージ管理の側面でも他ツールとは少し違う独特なツールである。
www.anaconda.com

Anacondaは、Anacondaリポジトリ*19なるpypiのミラーサーバから、「環境に応じた最も良いもの」をインストールする機能を持ったパッケージ管理ツールである*20。最も象徴的な例として、Pythonに近い界隈では「numpyが利用するBLASの違い」についての話題が毎年2,3度バズっている。Anacondaがインストールしたnumpyが早いというものだ。Google検索「numpy anaconda 早い」と検索し上から記事を見ていけば概ね把握できるだろう。
www.google.co.jp
他にもCUDAとDeep Learning関連ライブラリのバージョンを照合する機能など、手元の環境とライブラリの依存関係を照合する知識がなくとも構築出来るのが1つの"売り"であると言える。

反面、過去何度も多くの開発者やツールが「環境に応じた最も良い物を提供する」事に挑戦した結果と同様に、依存関係の差異に苦しんだり環境を破壊する結果になることもある。特にpipと併用する事がHardlinkによって困難な点が最も大きい*21。技術的なメリット・デメリットについては以下の記事には目を通しておくと良い。
www.anaconda.com

大きな企業単位であれば十二分な基盤やVMを上手く組み合わせAnacondaを利用した理想の環境で開発できる企業もあるだろうが、その破壊的な機能や近年企業規模に応じて有償化するようになり、導入におけるデメリットも存在するし、もちろん逆に良いこともある。

Anacondaは、過去pipのインストールがまだ不安定だった時期に登場し、依存解決や独自のpackage registryの提供で大きく貢献した。また依存解決リゾルバの改修にも積極的かつ、ミニマムなAnacondaにあたるminicondaも存在しており、大きな一つの勢力であると言える。

 
他ツールでは、先程から出てきているpip-toolsを利用する方法、rustで書かれたpyptoject.tomlで管理可能なpyflow等が現状の選択肢に入る。海外ではflitも人気があるようだ。


 
pip-toolsは、pipenvやpoetryと違い、env管理は別のツールで実施するという思想で作られているツールに当たる。その概念は、公式の以下の図が明確に表している。

f:id:vaaaaaanquish:20210320225037p:plain:w350
pip-tools公式repoより

ユーザはrequirements.inというrequirements.txtと同様のformatで、自身が利用したいモジュールのバージョンを記載する。pip-toolsが提供するpip-compileコマンドは、inファイルから依存する全てのモジュールのバージョンが固定された状態で記載されたrequirements.txtを生成するジェネレータの役割を果たす。また、hashを利用した固定にも対応している。ここで生成されたrequirements.txtファイルを利用してpip-syncコマンドでenv環境に流すという構想である。この思想に則りさえすれば、env環境と切り離せるため、複数人での開発等を考えた時、個々に手慣れたenv系のツールを使ってもらえば済むというメリットがある。

 

pyflowは、rustで書かれたPythonパッケージ管理ツールである。
github.com
機能としてもpoetryが採用するPEP 518、pyproject.tomlによる管理方法に加え、PEP 582にあたるカレントディレクトリに__pypackages__というディレクトリを作り仮想環境のように扱う方針に対応している*22。PEP 582はpythonlocflitpdmといったパッケージ管理ツールで採用されている新しい方針である。これによってユーザは、ほぼenvについて意識する事無く複数のPythonや開発環境を切り替える事ができるようになる。このようなPEP 582の採用も目新しいpyflowであるが、私自身はパッケージ管理ツールがPythonで書かれている理由はほぼないと考えており、依存解決やインストーラ含めて、より高速な言語で書かれて欲しいという気持ちもあり一目置いている。新興のツールという事もあり、現状では未対応のissueを見てpoetryと比較し利用していないが、挑戦的な選択肢として強く推せるツールとなる。

 
また他にも、pyproject.tomlを利用せずsetup.pyを含めて良い管理を目指すhatchや、オープンソースのソフトウェア構築ツールであるSConsに則り作成されているensconsRedHatが作成したpipenvとpoetryのいいとこ取りをしたminipipenv等も存在し、小さく機能を収める方向にいくか、理想の依存解決、複合ツールとして大きくなっていくか、どの仕様を採用するのか、違いが見え隠れする状態である。何度も言うが、これらの選択は、その場その場の要件次第でもあると思うので、利用用途の多くなったPythonという言語にとって、これらのツールが多く出てくる事自体は喜ばしい事である*23

 

まとめ

ここで一度話をまとめる。冒頭で示した主張には以下のような詳細に分かれる。

  • pipenvはPyPAが開発しているツールで同じくPyPAが開発するpipの機能を補っている
    • pipenvは依存解決器の機能を持つが、pipにもその機能が搭載されつつある
    • pipenvはvertualenvやwheel、twine含めてPyPAが作るエコシステムの一部である
    • pipenvはresolverとしてpip-tools内のbacktrackingを利用している
  • pipenvとpoetryの大きな"機能"の違いはパッケージのbuild・publishをサポートしているかどうか
    • poetryは基本的な機能はpipenvと同じ
    • poetry独自の機能はpyprojcet.tomlの拡張セクションによるもの
    • pyproject.tomlには各種設定を詰め込む事ができる
    • poetryはresolverとしてはpipenvと違いPubGrubを利用している
  • pipenv、poetry以外に他にも多くのパッケージ管理ツールが存在する
    • pip-tools、Anaconda 、flit辺りが人気
    • envやパッケージングなどの機能をどこまでをサポートするかの違い
    • package registryの違い、pyprojcet.toml(PEP 518)やPEP 582の採用の違い、リゾルバの違い


ここで、パッケージング管理を取り巻く重要なPEPを以下にまとめておく。

  • PEP 453 : pipをPythonが公式サポート
  • PEP 518PEP 517 : pyproject.tomlおよびbuildシステムの制定
  • PEP 582 : __pypackages__を利用した仮想開発環境の提案


PyPAが作っているか否か、小さく機能を収める方向にいくか、理想の依存解決、複合ツールとして大きくなっていくか、どの仕様、PEP、どのリゾルバを採用するのに各ツールの違いがある。

  

- pipenvとpoetryの技術的・歴史的背景 -

本章では、ネット上に多くあるpipenvとpoetryに関する議論で出てきがちな疑問、不満を以下の通りに分類し、それらに対してそれぞれ背景を追う形で紐解く。

  • パッケージングについて
  • pipenvはlock生成までの速度
  • pipenvは開発がここ2年滞っていた件
  • pipenvのissue対応
  • PyPAの開発フロー
  • 活動や対応に関するゴシップ

上記を追う中で、poetryがどのような技術を採用し、pipenvが何故そうしないのか、逆にpipenvのどこが良いのかを詳しく見る。

パッケージングについて

poetryはpipenvと違い、pyproject.tomlを利用する事で、指定のbuild systemないし、独自のセクションを利用して、pyproject.tomlのみでパッケージング出来るという話がある。pipenvとpoetryの機能の違いで最もよく挙げられる点である。

 

自系列を追うと、PEP 517が2017年9月、pipenv 1stリリースは2017年1月であり、前後関係としてPyPAがPipfileについて検討している間はpyproject.tomlのPEPのacceptに至っていない事がわかる。

また、pyproject.tomlを使えば全てのパターンのbuildが行えるかと言われると、元々setup.pyを書いていた事を考えれば当然そんな事はない。setup.pyでC言語で書いたサブモジュールをbuildしたり、外部のライブラリとリンクする実装などが代表的である。PyPAとしては、そういった問題の解決のためにwheelsetuptoolsも作っている訳なので、役割を考えればsetup.pyやrequirements.txtが担っていた役割とpipenvが持つ依存解決、バージョン固定、env管理という問題は別であるという認識になるのは自然である。

実際、poetryはそういったユースケースに対してbuildを提供する方法として、build = "build.py"のような機能を一応持っている。ただ以下issueの通り、ドキュメントには未だ記載はなく機能として安定、サポートされている状態ではない。
github.com
build scriptを柔軟に書けるというのは、wrapperとしての立ち位置を取る事が多いPythonという言語において非常に重要であるし、tomlだけで管理できるものはないだろう。またbuild scriptの実行もPyPAが長年かけてPEPで制定、整備、開発してきた部分であるので、それらへの対応を新しい開発者やツールが対応するのは相当な熱量がないと難しい部分も多い。

pyproject.tomlに集約されたり、パッケージングまで行える事で差別化が図られる一方で、こういった多くのbuildにどこまで対応するかがツールによって変わってくるというだけの話になる*24

 

lock生成までの速度

「pipenv lockが遅い」「poetry lockが早い」というのは、よく挙げられる話題の1つである。

lockファイル生成速度に関してpipenvのissueが乱立していたり、Slack、DiscodeSNS各所で議論されている。
#356#1785#1886#1896#1914#2284#2873#3827#4260#4430#4457、…


まず、先にpipenvとpoetryの依存解決リゾルバのアルゴリズムの違いがある事を挙げた。そうすると「pipenvもpoetryの実装を真似すれば?」「pipenvもpoetryのようにPubGrubをベースとした依存解決を行えば良いのではないか?」という意見を思い付くのは自然だろう。

一般的に依存解決を行いhash値によって利用モジュールのバージョンを固定する方法として、package registryがパッケージに対して返すhashを利用する方法がある。Node.jsのnpmやrubyrubygemsも同様の方法を取っている。Pythonにおけるpackage registryはPyPIになるが、PyPIAPIjson-apiへ移行したこと、またその内容にセキュリティ上の懸念がある事が、pipenvとpoetryの依存解決方法のアルゴリズム、そして実装の違いに影響している。
 

まず前提として、PyPIは2017年12月にそれまでのHTMLを利用したAPIエンドポイントをLegacy APIとし、json-apiの提供を開始した。これは、他のpackage registry同様の内容で、特定パッケージのhash値を含めて返してくれるというものである。
warehouse.pypa.io
json-api提供開始時期の問題もあって、poetryははじめからこのjson-apiが返す結果の多くを信頼し、依存解決を行っている。一方pipenvはこのAPIの結果を使わず自前でhash値を計算している。ここがpipenvとpoetryの根本的なlockファイル生成速度の差に繋がっていると言える。


さて、このjson-apiはpipenv開発開始以降に提供開始されたものではあるものの、pipenvに対しても「このjson-apiの結果を信頼し依存解決を行い、バージョンをlockするように変更すれば良い」という意見に繋がる。これにもいくつかの問題がある。

json-apiの1つ目の問題として、このjson-apiがPEPによって標準化されていないという点がある。先程のjson-apiのドキュメントからLegacy APIのページに行くと以下のような文言がある

The Simple API implements the HTML-based package index API as specified in PEP 503.

Legacy APIについてはPEP 503、その内容については、メタデータのバージョン2.0にあたるPEP 426に書かれており、メタデータjson互換オブジェクトを定義した2.1にあたるPEP 566もあるが、json-apiについて記載されたPEPは現状存在しない。PyPAとしてpipenvにこのAPIの結果を利用できない1つ目の理由になる。一方この問題は既にdiscuss.python.orgで起案されているので、こちらをウォッチすると良いだろう。
discuss.python.org


json-apiの2つ目の問題として、PyPI(正確にはhttps://pypi.org/)のartifact hashの一部が一度インストールしてみないと分からないという点がある。PyPIは、一度アップロードしたバージョンを同じバージョンで上書きする事はもちろんできない。一方、build済みのパッケージをtar.gzにdumpしたwheel形式以外に、buildスクリプト含めたsdist形式をアップロードできる。後者は、そこに正しくメタデータが設定されていない限り、特定バージョンのartifact hash自体は一度インストールしてsetup.pyを動かすまで未知になる。この未知のhashが厄介者になる。

PyPIは、一時的なhashを返してはくれるので、全幅の信頼を置いて依存解決の情報として使う事もできるし、全く信頼せず依存解決に必要な各バージョンのbuildスクリプトをダウンロードして手元でbuildしてhash値を計算するという事もできる。poetryは一部をjson-apiとし一部を手元でbuildする方針を取っており、pipenvは先述のPEP問題を含めて後者のように完全に手元で全てのパッケージをbuildする方針になっていた。これだけでも、pipenvが遅い理由が容易に想像付くはずだ。

 

この違いはあるものの、pipenvは、buildしたパッケージを出来る限りキャッシュして高速化する改修を過去何度も行っているだけでなく、2020年5月のReleaseではJSON-APIではなくURLフラグメントからhashを計算する形で依存解決の高速化を行っている。
github.com
これでかなり早くなった事が実感できるはずである。他にも #2618#1816#1785 #2075辺りの議論を追うと良い。

そしてもちろん、poetryも一部のパッケージを手元でbuildしている訳なので、そのパッケージを挙げて「lockが遅い」と指摘するissueが存在するし、poetryのドキュメントにも記載がある。
github.com
python-poetry.org
要は、実装の問題というより、思想としてartifact hashをどう考えるか、どう処理するのかの違いにlockファイル生成の速度の差があると言える。

そして先に出てきた、依存解決リゾルバのアルゴリズムの違いも、この問題に起因している。手元で多くのパッケージをbuildする方針の場合、PubGrubが、依存解決を行いながら、その場に応じてパッケージをbuildする機能を持つ必要がある。PubGrubは、そのアルゴリズムの特性上、途中でbuild処理をしながらその結果を比較するロジックを実装する事に向いていない。これについては、先程のpipの2020-resolverの議論でもpoetryの作者が指摘しており、poetryの作者がMixologyとして依存解決リゾルバのロジックをpoetryから切り出したにも関わらず、2020-resolverのロジックとして採用されなかったという背景にもつながっている。ここで、適切にbuildしながら依存解決を行うPythonによるPubGrub実装があれば話が進む可能性はもちろんあり、pipgrip等のOSS実装が出始めてはいるといった状況である。

 
さて、この非決定的なartifact hashについては、PyPIメンテナが書いた以下のブログに理由を含めて記載されている。
dustingram.com
より過去のhash自体の議論については以下を追うと良い。

 
この問題が完全に解消されるには、「下位互換を完全に切ったPyPIのミラーが作られる」だとか「setup.pyを捨て全てがpyproject.tomlになる良い方法が提案される」だとか「PyPIサーバ内でbuildが走る」だとか複数の道があるわけだが、歴史の長いPythonパッケージングの問題やbuildスクリプトが走る事でのセキュリティの懸念、サーバの金銭面などを全て解決されるには時間がかかるのは当然と言える。

 

pipenvは開発がここ2年滞っていた件

pipenvには「開発が滞っている」「2年更新のない死んだプロジェクト」などの指摘が多くある。実際、2018年11月(v2018.11.26)から2020年5月までReleaseがなかった。2020年5月のReleaseも、当初予定していた2020年3月から4月21日に延期され、4月29日に延期、その後5月にReleaseされたという背景があり、この辺りを挙げ開発が滞っているとする指摘も多い。

これらは、2020年のRelease Noteやtracking issueを見るのが良い。
discuss.python.org
tracking issue: https://github.com/pypa/pipenv/issues/3369

ここまで示した通り、pipenvはPyPAが開発する多くの他プロジェクト、pipやvirtualenv、setuptoolsに強く依存しているし、その意思決定の多くがPEPという大きなプロセスを通して行われている。空白の2年間の如くpipenvが止まっていたように見えるだけで、関連度が高く、依存した多くのプロジェクトの改修を行っていた訳で、開発が滞っているという指摘自体が間違っていると言える。

なによりPyPAはNumFOCUSといった支援団体があるとはいえ全員ボランティアであるし、Release Noteを書いているDan Ryanは仕事をしながら20%ルールの範囲で開発を行っていると書いてあるしで、外から多くを求めるだけというのは間違いだろう。またこのRelease Noteには「Other changes in the project」という章がある。それはProcess changes、Communications changes、Release cadence & financial supportの3つに分かれており、PyPIへの貢献のプロセス、コミュニケーションを変える事を約束し、別の形で「パッケージングエコシステムに対して」貢献できる形を提供すると発表している。

議論やcommitmentに至る程の背景知識がないが、このパッケージング問題に金銭面で貢献したいといった場合は、以下を読みdonate.pypi.orgに行くと良いだろう。
pyfound.blogspot.com

 

pipenvのissue対応

poetryが個人開発から始まっているのに対して、pipenvがPyPA主導である事から、issueへの対応の違いが指摘される事がある。「pipenvはissue対応が遅い」といった意見だ。これは、PyPAが元々issueを多く使っていない事に起因しているが、pipenvでたまに返信があるissueもあったり、ツールも分散しているので一見ではissue利用の思想について把握しづらいとも言える。

さて、先にpipの2020-resolverの問題の相談先として以下を挙げた。

これに加えてパッケージングに関しては、以下のような場所の議論を追う必要がある。


他PyPA関連のプロジェクトの多くの議論場所は以下のページにまとめられており、Slackからメーリングリスト、freenodeをチェックする事が求められる。
packaging.python.org

一見混乱するかもしれないが、開発者からしてみれば、接触確認アプリCOCOAがissueを利用していないにも関わらず未対応であった事を指摘していた人と何ら変わらないので、正しく議論したい場合は念入りに読み込んで、適切な場所に意見を投稿すべき、という話になる。

 

PyPAの開発フロー

PyPAの開発フローしんどい問題も少しだけある。もちろん影響範囲が大きいし、PEPや歴史的背景を持つ大きなプロダクトなので当然ではある。

例えば、実際「pipenvに一度貢献したがもうやりたくない」といった意見もある。
github.com
フローと歴史的背景を多く要求されるだけでなく、交渉の末MRを出すもcloseになる事がある。実際に私も過去python devのSlackでパッケージング管理問題に触れた時、多くの回答とフローを用意され尻込みしてしまった経験を持っている。

 
また、PyPAのツールにおいて少し特殊なのは、Vendoringという開発手法を用いている事が挙げられる。
これはRustやGo等ではgit submoduleのような形でよく使われる手法で、利用するモジュールを依存関係として取るのではなく、同一のRepository内に含めてpatchを当てて開発していく手法である。日本語であれば以下が詳しい。
qiita.com

pipenvのrepogitoryの以下ディレクトリを見ると、vendoringされたツールが多くあるのが分かる。
https://github.com/pypa/pipenv/tree/master/pipenv/vendor
これは、LICENSEやネストといった既知の問題こそあるものの、PyPAが作るツールのような「ある依存モジュールが突然PyPIから消えて動かなくなったら困る」といった再現性が必要な場面で有用な手法である。

一方で、ことPythonにおいてはvendoringに関連した文化やツールがそもそもないので、pipenvにその問題を指摘するissueが立ったり、vendor先のツールに「このrepoメンテする意味ある?」といったissueが立ったりしている。
github.com
github.com
当然upstreamとして機能し取り込まれる可能性があるので意味はあるのだが、Pythonという言語内でのvendoringの文化として浸透していない証拠とも言える。


ただ、開発フローに関しては、個々時々に応じて対処していけば良い問題であって本質的ではない。貢献する気概を持って貢献するだけだろう。そして何より、先述の2020年5月のRelease Noteを使って体制を改善するとまで言っていることからも、PyPAが真摯にこの課題に向き合っているのが分かるので、期待する所でもある。

 

活動や対応に関するゴシップ

issue対応や開発フローも技術的な背景から遠いが、pipenvやpoetryの議論の歴史も存在しており、それらを知る事で適切にコミュニティとコミュニケーションが取れると思うので、短くまとめておく。

昔々、当然PyPAメンバーはpipenvを推していて、メインメンテナであったKenneth ReitzがPyCon2018で発表するなどしていた。一時期pipenvのドキュメントのトップには以下のような文言が存在した。

the officially recommended Python packaging tool from Python.org, free (as in freedom)

冒頭のtheを含めて直訳すると「Python.orgが公式に推奨する唯一のPython パッケージングツール」というニュアンスになる。「PyPAは公式に推奨されているのか」「唯一のツールなのか」等、文言やその他の活動に対して揚げ足を取るような形で、機能や対応に不満を持っていたユーザの意見が噴出した。十分でない機能をREADMEで揶揄するプロジェクトが出たり、ブログやSNSでも話題になっていた

以下のようなタイトルのissueもあり、コメントやリアクションを見るになかなか殺伐としているのが伺える。
If this project is dead, just tell us · Issue #4058 · pypa/pipenv · GitHub

そんな最中、作者のKenneth Reitzが「Python.orgが公式に推奨する唯一のPython パッケージングツール」の文言をmaster commitで削除したりした*25事や、攻撃的な意見が辛く精神的に病を抱えている事を告白する記事を公開した。

そしてredditやissueで、全ての不満と共にKenneth Reitzのパフォーマンス説を唱える人が出たり、そういった人達と本当に技術について議論したい人が入り混じり炎上、PyPAメンバーやPythonメンテナが対応する事になった。

これらについてはPyPA、Pythonそれぞれから回答があり、以下を参照すると良い。
github.com
github.com


誤解を招かないため、更に書くと、pipenvやpoetryの不仲などと言った事もなく、上記redditスレ内でもpoetry作者とも話した上で設計思想の違いを認識している事が書かれているし、pipの2020-resolverを決定する際には両メンテナ、Pythonメンテナがそれぞれのリゾルバについてや依存解決のコツを投稿しているので、決して悪い方向に進んでいるような話ではない。
実際、現在のPythonのパッケージングに関するチュートリアルには、pipenvやAnaconda 、poetryを適切に選定する事が良いとされている。
packaging.python.org
技術的には本質ではないものの、知っておく事で、コミュニティがこういった攻撃的な投稿に関する話題に敏感である事を鑑みながら、丁寧に投稿できるようになればと思う程度の話である。

 

まとめ

本章のpipenvとpoetryの違いについて、以下にまとめる。

  • build/publishの機能の違いはpyproject.toml誕生の前後関係とPyPAのエコシステムの思想からくるもの
  • lockファイル生成速度は、PyPIjson-apiの結果をどこまで信頼し、どこまでパッケージを自分でbuildするかに関係
    • 依存解決リゾルバの違いもここに縛られている側面がある
    • 解決するにはいくつかの方法こそあるが、下位互換やセキュリティ、金銭的な側面の課題もある
  • Pipenvはissueではなくメーリングリストを利用して開発が進んでいる
    • 徐々にissueでの議論も進んでおり開発フローの改善も進む
    • poetryの方が開発体制が良いという訳では必ずしもない
  • それぞれゴシップ的な炎上を経験しておりディスカッションに参加する場合は丁寧な理解をしておくと良い

念の為フェアに補足しておくと、pipenvには、PyPIにアップロードされたパッケージが変更されていないか等の脆弱性を確認するPEP 508に則ったpipenv checkの機能もある*26。ある意味特徴的な機能の1つだ。
pipenv.pypa.io
これらを加味しながら、適切にツールを選ぶ事が良いと考えられる。

 

- poetry大臣としての活動記録 -

ここまで、Pythonのパッケージング管理ツールについて取り扱ってきた。そして、私が所属する企業、チームでも、実際にpipenvによるパッケージ管理とデプロイ、CI/CDが使われていた。しかし、運用する中で前述のようなパッケージング管理ツールの違いを踏まえて、社内の課題と見比べ、poetryへの移行を行う形となった。本章は、その移行作業の目的や判断基準、感想を綴るものである。

重要な分岐点になった、以下の3つのセクションについて書いていく。

  • VPN必須なpipenv lockがかなり遅かった
  • 乱立するファイルと管理場所
  • gokartなどOSSとの管理方法統一

 

VPN必須なpipenv lockがかなり遅かった

私が所属企業では、社内のPyPIを通してpipによるインストールを行っている。社内PyPIへの接続にはVPNが必須であった。

ちょうどコロナで全社でのリモート化が進み、在宅勤務が増えた事でVPNの負荷が増大、それに伴ってpipenv lockに膨大な時間がかかるようになっていた。基本的に1時間以上は基本、大きなrepoでは2,3時間を要して、途中で接続がタイムアウトしてしまうような事もあった。実際、深夜の障害対応中に「hot fixでバージョン戻してReleaseしましょう」「では、lockします」「・・・(終わりは3時だな)」といった会話が発生するなど、本当にあった怖い話もいくつか体験しており、流石にlockファイル生成速度の問題を看過できなくなっていた。


先述した通り、pipenvの設計思想の課題であって根本的な解決が難しいことも考慮し、別ツールへの移行を考える必要があった。検討したのは、pip-tools、poetry、flitであった。中でもpip-toolsは強く検討したが、各々がenvを設定するコストやpyproject.tomlが今後主流になるだろうという憶測から、poetryとflitに絞って検証を行った。

移行当初、flitが抱えていた課題として、パッケージのバージョニング機能が不十分であるという課題が存在した。具体的には、gitタグやVERSION.txtを通してパッケージのバージョンを決める方法の有無で、これによってCIによるバージョンやpublishを行う所作が、所属チーム内では広く行われていた。

poetryには、poetry-dynamc-versioningという拡張が存在し、こちらを利用する事で、上記問題が解決できる事が分かったため、以降先としてpoetryを利用していく運びとなった。
github.com

 

乱立するファイルと管理場所

所属チームでは、基本1人1プロジェクトで開発を進めており、そのプロジェクト数は大小や稼働率、廃止撤退様々あれど36個にも登る*27

私が入社当初、プロジェクトテンプレート等はなく、各々自らが知るツールを使ってRepositoryを作っていた。その後、私がcookiecutterによるプロジェクトテンプレートを作ったものの、そういうった背景を鑑みないrepoは増え、プロジェクト管理は以下のようなファイルに分散していた。

  • requirements.txt
  • setup.cfg
  • setup.py
  • VERSION.txt
  • Pipfile
  • Pipfile.lock
  • pip.conf
  • code_analysis.conf
  • yapf.ini
  • MANIFEST.in

中にはバージョン情報がrequirements.txtとPipfileに分かれて書かれており、初見ではどこで指定されているモジュールがproductionで使われているのか分からないようなプロジェクトも存在した。poetryとpoetry-dynamc-versioning、toxというテストを統一的に管理するツールを導入する事で、以下の3つに全てのファイルを集約できる事が大きな後押しになった。

  • pyproject.toml
  • poetry.lock
  • tox.ini

github.com

これは、最近Preferred Networks社のブログでも語られたような内容なので、普遍的に多くの企業で課題になっているだろう。
tech.preferred.jp
(PFNはそういった課題に対してpysenのようなツールを公開するにまで至っているのだから流石である)

私の所属企業では、poetry、poetry-dynamc-versioning、toxを利用する事によって既存のメンバーだけでなく、新しいメンバーに対しても混乱を産まず、いくつかの設定の場所と使い方を覚えるだけで開発に専念できるようになり、多くの問題を解消することができた。企業によっては、インフラやCI、要件の関係で他ツールを選ぶこともあるだろうとは感じるが、横串でツールの要件が決まっている事での開発速度の差は何より大きい。

 

gokartなどOSSとの管理方法統一

私の所属企業として内部で使われるツールを外向けのOSSとして公開しているが、チームメンバー全員が使うにも関わらずメンテナが一部のメンバーに偏ってしまっていた課題があった。様々な理由こそあったが、日頃使っているツールとの差異が大きく、パッケージングやフォーマッタのツールに社内との微妙な違いがあり、またbuildやpublishをメンバー個人ができない状況で、新規に開発に参入しづらいという課題が大きく存在した。

そういった背景から、社内でpoetryを推進した後、外部のOSSをpoetryに移行する運びで解消する事にした。
github.com

 

活動全体の感想

先述のような前提を踏まえ、私が2020年4月にpoetry移行をスタート。

f:id:vaaaaaanquish:20210326164741p:plain
2020年4月に自己宣言

1年かけて、コツコツと様々なプロジェクトにPull requestを出し、先日稼働中のプロジェクト全てがpoetryに移行した形になった。

f:id:vaaaaaanquish:20210329002427p:plain
ROI勘定


Pipfileからpyproject.tomlの移行は、toml形式を互いに採用しているところからもほぼ困難なく移行する事ができた。

移行時は、pyproject.tomlがデファクトスタンダードにならなかった場合にpyproject.tomlに多くの設定が集約される事でロックインされてしまうのではという懸念を持っていたが、現状多くのフォーマッタやlinter、testツールがpyproject.tomlをサポートする流れになっており、良い選択だったと言える。

poetry-dynamic-versioningを利用した、pythonパッケージ用のrepoの設定の仕方の記事を書くなどした。
vaaaaaanquish.hatenablog.com

また、同僚の @SassaHero がyapfをpyproject.tomlの対応をするなどした。

チーム管理のOSSであるgokartには、poetryやtoxが導入、かなり少ないツールと設定ファイルで多くの処理が行えるようになった。結果社内外からのコミッターも増えて万歳という結果になった。

 

- おわりに -

本記事では、pip、pipenv、poetryという3つのパッケージ管理ツールについて、技術的、歴史的背景をまとめ、それらを元にpoetry移行を行った経験を綴った。

そもそもパッケージ管理が最初から全て上手くいっているプログラミング言語などない。フロントエンド系だって、様々な設定ファイルやメタプログラミングを通してpackage.jsonやtsconfig.jsonに行き着いているし、JVM系のようにhash照合の概念がなくpackage registryをSpringが立てているようなところもある。package registryを捨て、gitのコードベースに依存解決を行っている言語もあれば、RustのCargoを見れば、fmtからtest、doctest、build、パッケージ依存解決、パッケージングを行えるたった1つのツールを上手にメンテしている。

私個人の本音を言えば、Cargoくらい全ての事が詰まったツールは欲しい。


特にpyproject.tomlベースでPEP 582も扱いたいし、できればtestやlinterも1つのツールに収まっていて、スクリプトで管理するのはごく一部でロックインされず気軽に乗り換えられるのが嬉しいが、そんなモンスターのようなツールのメンテは想像するだけで骨が折れる。PyPIjson-apiがPEPを通るか、Anacondaのようにpackage registoryを構える偉大な団体が出てくるかといった依存解決についての解消も必要だろう。

一方で、これは作ってない人のお気持ちであって、pyproject.tomlとbuild script以外にPythonを書くツールも出てきており、pyproject.toml以外の選択肢がパッケージングという観点以外のlinterやapi config、その他設定を起因に出てきた時移行できるよう、ロックインを避けるようにしていくのがベターだろう。

PythonC/C++のwrapperとして多くの機能を持っている事も加味すれば、今後もbuild scriptは必須になるだろうし、元来、パッケージ管理ツールといった代物はプロダクトコードとは別のレイヤーに居るのだから、あとはプロジェクトのサイズ感や運用状況と相談し、ベストプラクティス等と言わず、そういったツールに固執せず、粛々と運用し、適切に時期をみてツールを変更していくだけという話である。

 
何より私はPyPAのコアメンバーでもなければ、パッケージ管理ツールの主要なコミッターでもないので、大きく発言する資格はないし、donate.pypi.orgNumFORCUSを支援するか、issueやcommitやコメントを重ねるか、自分で理想のパッケージを作るかをやっていかないといけない。それがソフトウェアエンジニアとしての常である。頑張りましょう。

 
かなり長くなってしまったので、整合性が取れていなかったり誤認に基づく間違いがあるかもしれない。
できれば、Twitterはてブに書いて頂ければ幸いです。
ちゃんと議論したい場合は、文中の通り適切な場所にどうぞ。


// -- 2021/03/29 追記 --

GitHubがpackage registoryに成り得るサービスを展開していて、Pythonもfuture workに入っている。ここにindexされ、常用が始まるのを機に後方互換が一気に変わって、依存解決が超簡単になって、管理もGitHubないしMSがやってくれるというコースもワンチャンあると思っている。その場合、ユーザが利用するツールがどう転ぶかは未知数だ。などとGitHub Packagesが出てから思い続けて、future workになって2年経つので、いつになるかはよくわからない。一応future workになった時点で2021年の3月以降とされているから、今後なにかある可能性は高い。

 

- 参考文献 -


脚注や文章内リンク外で参考にしたもの・本記事を理解する上で読むと良いもの

f:id:vaaaaaanquish:20210329011401p:plain:w0

*1:サードパーティPythonモジュールがアップロードされているリポジトリ

*2:統一的でない事に対する悪い意味合いではなく目的に応じてツールが分離されたエコシステムであるという意

*3:pip install -r pyproject.toml出来ないよねといった議論はあるがsetuptoolsと共に徐々に対応されていくだろう https://github.com/pypa/pip/issues/8049

*4:PyPAが出すチュートリアルにはsetup.pyやsetup.cfgも適切に使えとは書いてある https://packaging.python.org/tutorials/packaging-projects

*5:何かを入れた後にサブモジュールをインストールしなおすと治るといった現象にPythonユーザであれば当たった事があるはず

*6:充足可能性問題 - Wikipedia

*7:バックトラッキング - Wikipedia

*8:私は高専時代Knuth先生のThe Art of Computer Programmingで挫折したことがあり教科書的と言える知識量ではないかもしれない…

*9:Dart開発者向けのドキュメントはここ pub/solver.md at master · dart-lang/pub · GitHub

*10:のように私は見える

*11:Unit propagation - Wikipedia

*12:poetryが採用している

*13:Chan Zuckerberg Initiative - マーク・ザッカーバーグと妻のPriscillaChanが持つ投資会社

*14:言わずもがなfirefox作ってる会社

*15:Python2もサポート終了で早く止めろと言っているので各位には早く止めてもらいたい

*16:今はまだ--use-deprecated=legacy-resolverで古いリゾルバを利用できる

*17:自系列的にはpipenvの方が先に依存解決をはじめている

*18:主たる形であってPyPAは他の方法を否定はしていない

*19:Anaconda Cloudやその他のリポジトリが存在するがここでは総称する

*20:より具体的にはwheelを選んでくれる

*21:もちろんenv毎に容量が節約される等のメリットもある

*22:poetryでも議論されておりメンテナも好意的 PEP 582 support · Issue #3691 · python-poetry/poetry · GitHub

*23:乱立しているだろうという観点はさておき

*24:私も手前xonshのxontrib等はpoetry化できないし、逆にsetup.pyのメンテを引き剥がす作業も行っている

*25:「他に適切な表現があるため」としており間違った行為ではない

*26:依存解決とは別の意味でセキュリティ関連の解説が必要なため、どうしても誤解を招かず書くことが難しかったので後述する形となった

*27:A~Zで偉人の名前でプロジェクトを管理していたが、今は一周し花の名前で2週目になっている

Rustで扱える機械学習関連のクレート2021

- はじめに -

本記事では、Rustで扱える機械学習関連クレートをまとめる。

普段Python機械学習プロジェクトを遂行する人がRustに移行する事を想定して書くメモ書きになるが、もしかすると長らくRustでMLをやっていた人と視点の違いがあるかもしれない。


追記:2021/02/24

repositoryにしました。こちらを随時更新します
github.com


追記;2021/07/26

GitHub Pagesでウェブサイトにしました
vaaaaanquish.github.io

- 全体感 -

Rustで書かれた(もしくはwrapされた)クレートは、かなり充実しつつある段階。

特に流行しているNeural NetworkないしDeep Learning関連のクレートは更新が盛んである。TensorFlowやPyTorchのRust bindingsもあれば、ゼロからRustで全て書こうというプロジェクトもある。

Numpy、Pandasを目指すプロジェクトも既に存在しているし、元々C/C++で書かれているライブラリであれば、rust-bindgenを使ってRust bindingを簡単に作れるようになってきている。
GitHub - rust-lang/rust-bindgen: Automatically generates Rust FFI bindings to C (and some C++) libraries.

古典的な画像処理やテキスト処理(例えばshift特徴量が欲しいだとかTF-IDFを計算したいだとか)で色々と物足りないクレートを使うことになる場合もありそうだが、画像はimage-rsという大きなクレートが存在し入出力を握っているし、形態素解析器などのクレートも多いので、アルゴリズム部分を自分でガッと書けば良いだけであると言えそう。
 
 

「物足りないクレート」と言ったが、2016~2018年から更新のないクレート、C++ライブラリのリンクが上手くいっていないクレート、Cargo.tomlだけのクレート(crates.ioにアップロードされ名前空間汚染になっている)などがあるという話で、その辺りを踏まえるとまだ(Pythonに比べたら)とっつきにくさがある。

Pythonであれば様々な機械学習モデルへの入力の多くをNumpyを使った行列で記述するが、Rustでの同等のプロジェクトではndarrayがあるものの安定し始めたのはちょうど1年前くらいからで、クレートによって入出力がrustのvectorだったりndarrayだったり、また別のnalgebraというクレートを使っている場合もあるといった状態である。

あとは機械学習の実験時に必要な物事をどうするかという問題も殆ど定まっていない。パラメータ管理はJSONを使うのか、設定時はRustらしくメソッドチェーンを使うのか、どう結果を保存するのか、パイプラインを作っていけるのか、など解決していない(デファクトスタンダードが定まっていない)課題は山積みである。

まあでも似たような問題はJulia langが流行り始めた頃にもあったように記憶していて、参入者が多くなるにつれて、自然とデファクトスタンダードは決まっていくだろう、と感じている。


ここでは、個人的にこれが残っていくんじゃないかと考えているものを優先的に書く。

 

- 機械学習足回り関連のクレート -

jupyterとかnumpy、pandasとか画像、テキスト処理などの機械学習前処理関連のもの
 

Jupyter Notebook

Python機械学習関連の物を作る人の多くがJupyterを使っているだろう(と思っている)。googleプロジェクトの配下にあるEvcxrが便利。
github.com
Notebookカーネル、REPLがあるので、普段Rustの小さな挙動を確認したい時はこれを起動するかシェルから叩いている。私としてはかなり開発速度が上がったツールの1つ。

類似のものとしてはrustdefとかを見てみたが、Evcxrの方が使い勝手がPythonカーネルに近い。


matplotlibやseabornとまではいかないが、vectorであればグラフ描画にplottersのjupyter-integrationが使いやすいなと思って入れてはいるものの、データ分析、EDAにおいてはやはり型にうるさくないPythonがやりやすいので結局Pythonカーネルを起動してる。
GitHub - 38/plotters: A rust drawing library for high quality data plotting for both WASM and native, statically and realtimely 🦀 📈🚀

plotlyのRust bindingsもあって、Pythonくらい柔軟になればあるいはとも思っている。EvcxrをSupportしたのが6ヶ月前にReleaseされた0.6.0(現行最新バージョン)であるのでこれからという感じ。
GitHub - igiagkiozis/plotly: Plotly for Rust

 

Numpy/Scipy

ndarrayというクレートnalgebraというクレートが2大巨頭で争っている。
redditの議論: ndarray vs nalgebra : rust

解決したい課題がnalgebraは線形代数特化な様相もあって、Pythonのように動的によしなに行列をスライスしたり変形させる機構ではなく、コンパイル時に行列の大きさを推定しメモリを確保してそこを使う実装になっている。ピュアRustという事もあるし、ndarrayのような柔軟さが欲しい場合はRustを使う意義があまりないと私は思っている。機械学習で扱う行列演算で曖昧な処理をしてバグを生むことも多いので、機械学習で使う側面から見てもnalgebraが幅をきかせていくと思っているがndarrayという名前に勝てず皆使っているのはndarrayという感じ。

github.com
github.com


(ちなみに私が作っているライブラリでは決めかねてvec![]を使っているごめんなさい)

Pandas

polars一択だと思う。polarsはpythonバインディングもあり、pandasより早い事を謳うライブラリの1つでもある。
github.com
applyやgroupby、aggを使う限りでは、殆どpandasと遜色なく扱える。

参考になる: Rustのデータフレームcrateのpolarsとpandasの比較
 

Queryを扱うという点ではarrowを使う事でデータの処理が行えるが、オンメモリでpythonのDataframeのようにとは少し違ってくる。
https://github.com/apache/arrow/tree/master/rustgithub.com


他にblack-jackというめっちゃかっこいい名前のクレートやrust-dataframeutahといったpandasを意識したクレートがあるが、開発は滞っている。
 

画像処理

image-rs配下のプロジェクトが一番の選択肢になると思う。
github.com
ImageBufferがfrom_vec, to_vecを持っているのでvectorとのやり取りも難しくない。特徴量抽出など、少し複雑な処理を行う場合はimageprocに実装されていってる感じなので、こちらを見ていくと良さそう。
GitHub - image-rs/imageproc: Image processing operations
 

前述のnalgebraを使う前提であれば、cgmathでGPUSIMD最適化などのオプションを付けて多くの線形な処理が行えるので、画像処理のみを目的にするならこちらも選択肢に入る。
github.com
近年の画像処理 * 機械学習観点だとここまで必要になる事は、そう多くはないのかなと思ったりもする。リゾルバを書いて何か解決したい場合とかだろうか。
 

次点でopencv-rsustというクレートがあるのだが、私は結局opencvへのリンクを上手くやってbuildして動かした所で力尽きて終わってしまった。OpenCVインストールバトルやcv::Matの扱いに慣れている場合は選択肢に入るかもしれない。ndarray-imageというndarrayで扱おうというrust-cvなるプロジェクトもあるが、開発が盛んとは言えない(AkazeやBrute forceみたいな古典的な画像処理アルゴリズムを熱心に実装しているのはrust-cv)。

形態素解析/tokenize

lindera になりそう。
github.com
mecabやneologdのような既存の資産も扱えるし、ピュアRustである点も含めて扱いやすい

linderaメンテナのブログ:Rust初心者がRust製の日本語形態素解析器の開発を引き継いでみた - Qiita
私も過去に使ったブログを書きサンプル実装を公開している:Rustによるlindera、neologd、fasttext、XGBoostを用いたテキスト分類 - Stimulator

その他には、sudachi.rsyoinawabi のような実装もあるがメンテは止まっている様子である。

 
英語のTokenzeであればPythonでもおなじみのhuggingfaceのtokenizersがRust実装なのでそのまま扱う事ができる。
github.com

 

- scikit-learn的なやつ -

scikit-learnみたいに色んなアルゴリズムが入ったクレートは有象無象にある。
大体どれも以下のアルゴリズムはサポートしている。

  • Linear Regression
  • Logistic Regression
  • K-Means Clustering
  • Neural Networks
  • Gaussian Process Regression
  • Support Vector Machines
  • Gaussian Mixture Models
  • Naive Bayes Classifiers
  • DBSCAN
  • k-Nearest Neighbor Classifiers
  • Principal Component Analysis
  • Decision Tree
  • Support Vector Machines
  • Naive Bayes
  • Elastic Net

各ライブラリと特徴比較

流石に全部使って見るという事ができていない上に、更新が止まっているものも多いので、最終commit日と一緒にリストアップする

  • rusty-machine (Star: 1.1k, updated: 2020/2/15)
    • 一番よく記事等を見るやつ。最終更新は1年前だが、アルゴリズムよりsrc/analysis配下のConfusion Matrix、Cross Varidation、Accuracy、F1 Score、MSEの実装が参考になるのでよく見に行く。iris等簡易データセットの読み込みもあるがデータだけなら下記のlinfa datasetが良いと思う。
    • Generalized Linear Modelを広くサポートしているのはちょっと特徴的
  • linfra (Star: 658, updated: 2021/1/21)
    • InterfaceもPythonっぽいし、完全にsklearn意識で開発も盛りあがっていてSponsorもいる。特にsklearnと違って各アルゴリズム毎にクレート化されてるのが嬉しい。
    • 上記以外にGaussian Mixture Model ClusteringやAgglomerative Hierarchical Clustering、Elastic Net、ICAをサポートしている
  • SmartCore (Star: 66, updated: 2021/1/22)
  • rustlearn (Star: 470, updated: 2020/6/21)
    • 古いライブラリだがPure Rustでinterfaceもわかりやすいので実装が参考になる
    • レコメンドでよく使うfactorization machinesや多くのmetric、k-fold cross-validation、shuffle splitのようにかなりピンポイントで重要なアルゴリズムを採用している

sckit-learnの立ち位置になっていきそうなのは、Sponsorもついているlinfaか開発者の勢いがあるsmartcoreというイメージ。
github.com
github.com

 

- Gradient Boosting -

主に使われているXGBoost、LightGBM、CatboostにRust bindingsが存在するが、現状modelのtrainまで出来るのはXGBoostとLightGBMのみ。

XGBoost

公式のXGBoostのC++実装をbindgenでbuildしてwrapした実装がある。
github.com
wrapperなので、PythonのXGBoostと同じイメージで使える。開発はほぼ止まっていて、submoduleとして使っているXGBoostのバージョンが少し古かったり、GPU等のSupportがないのがネックではある。

XGBoostで学習したデータを読み込んで推論できる、gbtree実装を使うという手もあるが、まだ上記のwrapperの方がとっつきやすいとは思う。
github.com
 

LightGBM

手前味噌ではあるが私がwrapperを書いている。上記のXGBoostと同様、C++実装をcmakeしてbindgenでbuildしてwrapしたもの。
github.com

parameter configをserde_jsonにしていたり入出力がvectorだったりして本当にこれで良いのか議論したいし、まだサポートしていないc_apiWindowsGPUを作る必要もあるので皆さんのcommitを待っている。

 

CatBoost

Catboostは公式に「一応」Rust bindingsが入っている。
以下のPRを見ての通り、適当なレビューはされておらずドキュメントはない…。
github.com
実装も怪しいが、私が手元で動かしてみた所、ライブラリへのリンクが上手く通っておらずそもそも動かなかった。

XGBoostやLightGBMと同様の方法でbindingsを作れば良いという話でもあるのだが、厄介な所として、Makefileではなくyandexの社内のモノリスで動いていたyamakeというビルドシステムを使ってビルドしている(普通にbuildすると謎のバイナリをダウンロードしてきて動かされる)。
それはやめようよというissueが立ち、catboost/makeディレクトリにMakefileが用意されるようになったが、このMakefileもyandex社内のArcadiaというシステムがジェネレートしたものでかなりヤンチャ(OSごとにファイルが分かれているにも関わらずドキュメントがそれに追い付いてなかったりもする)。
Ya script sources · Issue #131 · catboost/catboost · GitHub

masterが動かなかったのでコードを読む限りだが、現状は学習済みのバイナリを読み込んでpredictする形式しかサポートしていない。これはtrain周りのc_apiがwrapされていないからで、その部分を自前で書く必要もある。

一応私はここ最近Makefileとbuild.rsを作ることに挑戦していて、とりあえずmakeが通ってlibcatboostが生成される所まで来たが、心が折れるかもしれない(折れそうだったのでこの記事を書いて気を紛らわせている)。

 

- Deep Neaural Network -


現状はPytorch、TensorFlowの公式bindingsの2強の状態と言える。

系譜を以下の記事から引用する。

私の知る限りではRust界隈のディープラーニングフレームワーク事情は以下のとおりです。

primitiv-rustでディープラーニングする - Qiita

この記事で紹介されているprimitiv-rustdynet-rsも更新は止まっている。

Tensorflow/PyTorch

公式Bindingsが使える。

github.com

github.com

私はTensorFlow 2.xやKerasが入ってAPIがもう追えなくなったのでPyTorchを使ってるが、tch-rsは不満なく使えている。pretrained modelによる学習もすぐ始められるし、後述するようなBERT、Transformerの実装もあるのでほとんどPythonと遜色ない。GPUにも載る(私は試せていないが)らしい。
 

BERT

最近Transformerの実装の参考に触り始めたtch-rsをベースにしたrust-bertというクレートがあり、難しくなく扱える。
github.com

日本語のPretrainモデルをコンバートしてくる必要があるので、そこが少し面倒だが、みんな大好きHugging Faceのライブラリ群がかなり巻き取ってくれているので、Pythonで書いている時とあまり変わらない印象。
 

- Natural Language Processing -

TF-IDF

ほとんど簡易な実装だが、下記がある。
github.com

sckit-learnのようにTF-IDF Vectorizerみたいな使い方は、issueを見る限りあまりサポートされなさそうなので、その場合は自前で書けば良さそう。

fasttext

公式のFastTextの実装をbindgenでbuildしてwrapしたものがある。

github.com

私も過去に使ったブログを書きサンプル実装を公開している:Rustによるlindera、neologd、fasttext、XGBoostを用いたテキスト分類 - Stimulator
開発としては止まっていて、submoduleとして使っているFastTextのバージョンが遅れているのがネック。
 

- Recommendation -

Collaborative Filtering/Matrix Factorization

協調フィルタリングで多分一番実装がまともなものがquackinになる。もう4年更新がないが、実装自体がシンプルなのでフォークすれば良さそう。
github.com

他にもrecommenderが一応クレートとして見られるけど、ここまでなら自前で作っても良い。
より古典的なVowpalWabbitのRustバインディングも選択肢に入る。
github.com

 

Matrix Factorizationであれば、先述のrustlearnを使うのが良さそう。

non negative matrix factorizationの実装のクレートがあるが、試してみた所精度が出なかったので難しいところ。
GitHub - snd/onmf: fast rust implementation of online nonnegative matrix factorization as laid out in the paper "detect and track latent factors with online nonnegative matrix factorization"

- Information Retrieval -

Apache Solr入門や先の形態素解析のLinderaをメンテしているMinoru OSUKA(@mosuka)さんが作っているgRPCで通信して検索するbayardが全文検索エンジンとしての出来が良い。
github.com
(というよりまともに動くのはこれくらいな気がする)

作者の解説記事:Rust初心者がRustで全文検索サーバを作ってみた - Qiita
初心者が形態素解析器と全文検索エンジンを作っている。謎。
 

フロントでは、WebAssemblyでメモリ効率良く設計されたTinysearchがかなり気になる(使えていない)
github.com
フロントエンド側で検索できて、gzipにすると51KBしかないとか。
公式のブログがかなり参考になる:A Tiny, Static, Full-Text Search Engine using Rust and WebAssembly | Matthias Endler
作者はTrivagoの検索のバックエンドを作っている人らしい。

WebAssemblyでフロントで検索するのは熱く、StripeのengineerからもRustでwasmを介したStrokが出てる。
github.com
 

より一般的なものであれば、Elasticsearchが公式のクライアントとしてElasticsearch-rsを出しているので、裏側にESが既に立っているとかならこれで良さそう。
GitHub - elastic/elasticsearch-rs: Official Elasticsearch Rust Client

 

近傍探索ではFaissのc_apiをwrapしたRust bindingsがある。使い勝手はほぼPythonと同じ。

github.com

他の選択肢としては、HNSWやHNSW graphsをベースにしたgranneがあるので要検討。Pure Rustな所がかなり嬉しいと思う。
github.com

github.com

もうちょっと古典的なものだとVP木をつかったvpsearchがある。kd-treeの実装もあるのでkd-tree版も作れなくはなさそう。

- Reinforcement Learning -

強化学習であれば、rurelというクレートがあるが、流石に私も触りきれていない。
github.com

gymのRust bindings作ってる人もいる。すごい。まだ触れてないけど使えるかも。
github.com

 

おわりに

結局の所「wrapするだけならPython(ないしCython)が便利じゃない?」という所とどう折り合いつけるのかというのはある。

ただ、分散処理するほどではない大規模データ、型のないPythonでは扱いの難しいデータ、wasmによるフロントエンドでの高速処理など、今まで機械学習の社会実装で苦労していたニッチな所に刺さると思うので良いとも思う。
 

他にもこれオススメだよってやつあったら触りに行くので、はてブかツイートしてください。

 f:id:vaaaaaanquish:20210123234057p:plain:w0

【参考】

crates.ioでMachine Learning等で調べた。