Skip to content

012 Elixir 版のビルド・テスト・静的解析・機械学習・API ライブラリの選定

「機械学習から始めるプログラミング入門」Elixir 版で使うライブラリを決める。

日付: 2026-09-23

ステータス

2026-09-23 提案されました

2026-09-23 承認されました(第 1〜15 章の執筆で確かめました)

コンテキスト

執筆計画 の「第 3 波の執筆計画」では、Elixir 版の機械学習ライブラリの第一候補を Nx・Scholar とし、Explorer の採否(Rust のネイティブ拡張が Nix で通るか)を B60 のステップ 1 で確かめてから確定するとした。Elixir 版の対比の相手は F# 版・Clojure 版(不変のデータと関数)、Python 版(テンソルと scikit-learn にあたるもの)、Ruby 版・Clojure 版(動的型付け)である。

B60 のステップ 1 で、使い捨ての Mix プロジェクトを nix develop .#elixir の中で動かして次を確かめた。

確認したこと 結果 確認方法
Nix 環境 Elixir 1.18.4、Mix 1.18.4。elixir-ls は環境変数を汚さない nix develop .#elixir
OTP erl コマンドは OTP 28(erts 16.2)だが、Elixir は自分が持つ OTP 27(erts 15.2.7.4)で動く。Clojure 版の「java は 25 だが Clojure は JDK 21」と同じ形 System.otp_release/0、erl -eval
機械学習 Scholar 0.4.2(Apache-2.0)。線形回帰・リッジ・ロジスティック回帰・SVM・K-means・主成分分析(explained_variance_ratio を持つ)・標準化・混同行列・適合率/再現率/F 値・ROC 曲線・AUC・交差検証・欠損値の補完・ダミー変数化がある ビーム表の一覧、使い捨てのプロジェクトで実行
Scholar に無いもの 決定木とランダムフォレストが無い。ラッソも無い。 本書の背骨である決定木が丸ごと無い Scholar.* のビーム表の一覧
テンソル Nx 0.13.1。既定のバックエンドは Nx.BinaryBackend で EXLA は要らない。Nx.tensor([1.0]) の既定の型は f32。type: :f64 を明示しないと重回帰の係数が 0.9999998807907104 になる 使い捨てのプロジェクトで実行
データフレーム Explorer 0.12.0 は rustler_precompiled が既成の NIF を取ってくるので、Nix の中でも Rust のビルドは起きなかった mix deps.get・mix compile
乱数 Nx.Random.key(0) の shuffle は [2, 7, 9, 6, 0, 8, 1, 3, 4, 5](Threefry)。Erlang の :rand はまた別。どの言語版とも一致しない 同上
CSV NimbleCSV 1.3.0 は BOM を取り除かない。Shift_JIS は Erlang/Elixir の標準では読めない。Explorer に読ませると例外を投げずに文字化けする BOM つき・Shift_JIS のファイルで確認
文字コード codepagex 0.1 で CP932 から変換できる(config :codepagex, :encodings, ["VENDORS/MICSFT/WINDOWS/CP932"] の設定が要る) 同上
API Plug 1.20+Bandit 1.12+Jason 1.4 で JSON を返せた :httpc で叩いた
直列化 :erlang.term_to_binary/binary_to_term でマップの決定木を往復できた 同上
整形・静的解析 mix format --check-formatted は終了コード 1。Credo 1.7.19 の mix credo --strict は指摘があると終了コード 4。未使用の変数は Credo ではなくコンパイラが警告するので mix compile --warnings-as-errors と組み合わせる わざと崩したファイル
テスト・カバレッジ ExUnit(テスト名に日本語を使える。@tag と ExUnit.configure(exclude: ...) でスキップできる)。標準の mix test --cover に既定で 90% のしきい値がある 使い捨てのプロジェクト

各章で確かめた結果

章 確かめたこと
1 NimbleCSV は BOM を取り除かないので、先頭の列名から自分で取り除く。NimbleCSV.define/2 はモジュールを生成するので、区切り文字は実行時ではなくコンパイル時に決まる。実データのテストは @tag :data と ExUnit.start(exclude: ...) で外せる。mix test --cover のしきい値は test_coverage: [summary: [threshold: N]] に書く。[threshold: N] と直に書くと黙って無視され、既定の 90% のまま失敗する(同じ test_coverage の ignore_modules は効くので気づきにくい)。生成されたパーサーのモジュールもカバレッジの集計に入るので ignore_modules で外す
2 java.util.Random と同じ 48 ビットの線形合同法を自作した。 シード 0 の並びが [4, 8, 9, 6, 3, 5, 2, 1, 7, 0] で Java 版・Scala 版・Clojure 版と一致し、訓練データの平均(がく片長さ 0.4215384615384616)も 1e-15 まで一致した。Elixir のマップはキーの順を保たない(Erlang の flatmap から hashmap に変わる 32 件を境に崩れる)ので、列の順はリストで持ち回る。|| は補完する値が nil のとき黙って nil を返すので、「無ければ例外」は Map.fetch/2 で書く
3 Scholar に決定木が無いので、自作の木が最終実装。深さごとの正解率・深さ 2 の木の境界(0.2950・0.6500)・テストの正解率 0.9556 が Java 版・Scala 版・Clojure 版と一致した。Enum.max_by/2・min_by/2 は同値なら先のものを返すので、Clojure の max-key(同値なら後ろ)で必要だった書き直しが要らない。Enum.sort_by/2 は安定。木はマップで表し is_map_key/2 で見分ける(網羅性は検査されない)。:io_lib.format はロケールに依らない
4〜6 mix format --check-formatted(違反で終了コード 1。CRLF も検査する)・mix compile --warnings-as-errors(未使用の変数は Credo ではなくコンパイラが警告する。1)・mix credo --strict(指摘があると 4)・mix test --cover(しきい値未満で 3)。4 つとも実際に壊して落ちることを確かめた。.gitattributes の apps/elixir/** text=auto eol=lf は .formatter.exs の対象外(mix.lock・YAML・Markdown)を守る二層目。npx gulp apps:check:elixir は手元に mix があると Nix に切り替わらないので、手元の Elixir の版で走る
7 切片 6114.60・R² 0.6184・MAE 302.20・RMSE 376.14 が Java 版・Scala 版・Clojure 版と一致。Scholar.Linear.LinearRegression は Nx.LinAlg.pinv(SVD)で解くので、条件の悪いデータでは最小二乗解に届かない(特異値 [12988, 2648, 1589, 4.28] のデータで残差平方和が 自作 11083165.68 < Scholar 14073810.01)。列を標準化してから渡すと有効数字 4 桁までそろう。f32 と f64 の違いは丸めた表示では気づけない(切片が f64 6114.595505694404、f32 6114.59716796875 で、どちらも 6114.60 になる)
8 前処理は自作が最終実装。SimpleImputer はグループ別の統計量を持てず、OneHotEncoder は最初のカテゴリを落とせない(drop="first" 相当が無い)。どちらも部分的には自作と一致することを確かめた。OneHotEncoder.fit/2 は 1 次元の整数のテンソルしか取らない。学習済みパイプラインは :erlang.term_to_binary で往復できる。大文字始まりのキーは row.Sex_male と書けない(invalid alias)
9 決定係数 8 個が Java 版・Scala 版・Clojure 版と一致。Scholar.Preprocessing.StandardScaler は n で割る母標準偏差で、自作と一致した(JVM 版が Tribuo の n−1 で踏んだずれが起きない)。Shift_JIS を :utf8 として読むと例外も置換文字も出ず、String.valid?/1 が false の不正バイナリが値に入る(Java の MalformedInputException、Clojure の U+FFFD 置換とは違う第 3 の振る舞い)。逆に UTF-8 を CP932 として読むと codepagex が {:error, ...} を返すので、取り違えは片方向だけ静かに通る
10 自作の正解率 8 個が Java 版・Clojure 版と一致。Scholar のロジスティック回帰は既定の alpha: 1.0(L2 正則化)が 105 件のデータに強すぎる(訓練 0.6571)。alpha: 0.0 にすると自作と完全一致する。ランダムフォレストは Scholar に無いので自作。特徴量の重要度だけがわずかにずれる(1558 回の分割のうち 269 回が同点で、Enum.frequencies/1 の走査順の違いから同点が 1 ulp 崩れる。木の分け方は変わらないので正解率は一致する)。勾配降下法は Nx.BinaryBackend より素のリストのほうが 7 倍速い。:math.exp(1000.0) と :math.log(0.0) は :badarith を投げる(Erlang に Infinity と NaN が無い)
11 正解率 0.7811・適合率 0.7759・再現率 0.6306・F 値 0.6833・RMSE 405.77・MAE 321.53 が Java 版・Scala 版・Clojure 版と一致。Scholar.Metrics.Classification は f64 を渡しても f32 を返す(Nx.equal を経由して型が落ちる)ので 1e-7 までしか比べられない。Regression は f64 のまま。roc_auc_score/4 だけは正解を f64 で渡せば f64 で返る。confusion_matrix/3 は [tn, fp, fn, tp] の順。ModelSelection.k_fold_split/2 は余りを捨てる(891 件を 5 分割すると 1 件がどの分割でもテストされない。Tribuo も Rumale も余りを配る)。Stream.map/2 は 1 件ずつ実現する(Clojure の 32 件チャンクと違う)
12 alpha ごとの表 5 行・テストの決定係数(線形 0.5224、リッジ 0.6243)・ラッソが 0 にした 3 列が Java 版・Scala 版・Clojure 版と一致。RidgeRegression の既定の solver は :svd で自作と 5.06e-6 ずれるが、solver: :cholesky にすると 1 ビットも違わない(第 7 章の LinearRegression のずれと同じ原因が、リッジでは呼ぶ側が選べる形で露出している)。ラッソは Scholar に無いので座標降下法を自作。自作の目的関数は件数で割らないので、Tribuo の alpha=0.5 は訓練 47 件では 23.5 にあたる
13 寄与率(PC1 0.4110、累積 0.8427)と主成分の係数が Java 版・Scala 版・Clojure 版と一致。Scholar の PCA は寄与率を持つので全面的に突き合わせられる(寄与率の差 6.9e-17、主成分の差 1.1e-14)。Nx.LinAlg.eigh は対称でない行列を黙って受ける(上三角だけを使う。例外も警告も出ない)ので、対称かどうかは自分で確かめる。固有値は降順で返り、固有ベクトルは列に並ぶ。Scholar の符号規則は U ベース(scikit-learn と同じ)。固有値が縮退したデータでは向きが一意でないので比較できない
14 自作の SSE 列(2640.00〜618.17)とクラスタごとの件数・平均支出額が Java 版・Scala 版・Clojure 版と一致。Scholar.Cluster.KMeans は初期中心を渡せない(:init は :k_means_plus_plus と :random のみ)ので、同じ定義の SSE の大きさで比べる。:key を渡さないと System.system_time() で鍵を作るので結果が毎回変わる。Scholar の KMeans は k=1〜10 で約 80 秒、自作のリスト実装は 2.4 秒(30 倍以上遅い)。:io_lib.format は ~.0f を受け付けない
15 置き場の約束は behaviour(@callback)で表すが、behaviour はモジュールへの約束なので、置き場は {モジュール, 状態} の組になる(Plug 自身の plug: {Api, store} と同じ形)。振る舞いの約束は本物と偽物の両方に走らせるテストで守る。例外は defexception の構造体にすると rescue の節で種類ごとに振り分けられる。:erlang.term_to_binary は f64 をビット列で書くので、保存・復元の往復は完全一致する。ただしほかの言語版とは一致しない(7730.457421687016 対 7730.457421687023。正規方程式を解く実装の丸めの順の違い)。Bandit はヘッダー名を小文字に正規化し、Plug が vary・cache-control を足す。Plug.Test.conn/3 でサーバーを立てずに統合テストが書ける

決定

用途 採用 バージョン ライセンス 初出
言語・ビルド Elixir と Mix(mix.exs・mix.lock) Elixir 1.18 Apache-2.0 第 1 章
OTP Nix の Elixir が持つ OTP 27 27 — 第 1 章
テスト ExUnit(mix test)。実データのテストは @tag :data で外せるようにする 標準 Apache-2.0 第 1 章
整形 mix format --check-formatted 標準 Apache-2.0 第 1 章(記事での解説は第 5 章)
静的解析 Credo(mix credo --strict)と mix compile --warnings-as-errors Credo 1.7 MIT 第 1 章(記事での解説は第 5 章)
カバレッジ mix test --cover(標準) 標準 Apache-2.0 第 1 章(記事での解説は第 5 章)
データの表現 素のマップとリスト(列名のアトムをキーにしたマップの並び) — — 第 1 章
CSV NimbleCSV(BOM は自分で取り除く) 1.3 Apache-2.0 第 1 章
文字コード codepagex(CP932 の CSV) 0.1 BSD-2-Clause 第 9 章
テンソル Nx(type: :f64 を明示する) 0.13 Apache-2.0 第 3 章
乱数 java.util.Random と同じ線形合同法を自作する(下記) — — 第 2 章
機械学習 Scholar 0.4.2 Apache-2.0 第 7 章
直列化 :erlang.term_to_binary(学習済みモデル)、Jason(API の JSON) Jason 1.4 Apache-2.0 第 8 章
API Plug と Bandit。経路の振り分けはハンドラーの関数の中で書く Plug 1.20、Bandit 1.12 Apache-2.0 / MIT 第 15 章
  • Explorer は使わない。 ネイティブ拡張は Nix の中でも取得できたが、データフレームのライブラリを使わないのはシリーズ全体の方針である。表は素のマップとリストで表し、CSV は NimbleCSV で読む
  • EXLA は使わない。 Nx.BinaryBackend で本書の規模の計算は足りる。EXLA を入れると Nix の中で巨大な依存(XLA のバイナリ)を抱えることになり、記事の「動かしてみる」の敷居が上がる
  • Nx のテンソルは type: :f64 を明示する。 既定の f32 のままではほかの言語版と数値を突き合わせられない。この「既定の型が単精度である」ことは、Elixir 版でいちばん最初に書く見どころにする(Python 版の NumPy は既定が f64 なので、ちょうど裏返しになる)
  • 決定木とランダムフォレストは自作が最終実装になる。 Scholar に無い。本書の背骨である決定木が丸ごと無いので、ライブラリとの突き合わせは線形回帰・ロジスティック回帰・K-means・主成分分析・評価指標・前処理に限られる。 これはほかのどの言語版よりも置き換えの範囲が狭い。記事ではこれを「ライブラリが育っていない領域では、自作がそのまま本番の実装になる」例として正面から扱う
  • 乱数は java.util.Random と同じ線形合同法を自作する。 Nx.Random(Threefry)も Erlang の :rand もほかの言語版と並びが合わない。そこでシリーズで初めて「乱数生成器そのものを自作する」ことにし、48 ビットの線形合同法(乗数 0x5DEECE66D、加数 11)と nextInt(bound) の棄却、Fisher-Yates を書く。シード 0 の並びが [4, 8, 9, 6, 3, 5, 2, 1, 7, 0] になり、Java 版・Scala 版・Clojure 版と一致することを実測で確かめた。 ライブラリが無いからではなく、ほかの言語版と数値を突き合わせるために自作するという、この版でいちばん面白い選択になる
  • 型(@spec・Dialyzer)は使わない。 Ruby 版で RBS・Steep を使わなかったのと同じ扱いで、動的型付けの言語では「型の誤りをテストで捕まえる」ことを示す。使わない理由は第 5 章に書く
  • Phoenix は使わない。 第 15 章の主題(層の分離と統合テスト)には Plug のハンドラーだけで足りる
  • 各章は「自作してから Scholar と突き合わせる」構成にする。 突き合わせられない章では、そのことを明記して自作の妥当性をテストで示す

章ごとのライブラリへの置き換え方針

章 置き換え 方針
3 無し(Scholar に決定木が無い) 自作のジニ不純度の決定木が最終実装。ライブラリが無いことを章の中で明記する
4〜6 無し 道具立ての章。mix format・Credo・mix test --cover・Nix の環境定義を扱う
7 Scholar.Linear.LinearRegression 正規方程式を自作してから突き合わせる。f32 と f64 の違いをここで見せる
8 前処理は Scholar.Impute.SimpleImputer・Scholar.Preprocessing.OneHotEncoder。決定木は自作のまま クラスの重みは Scholar に口が無いので自作
9 Scholar.Preprocessing.StandardScaler 自作の標準化と突き合わせる(n で割るか n−1 かを実測で確かめる)
10 Scholar.Linear.LogisticRegression ランダムフォレストは無いので自作のまま。ロジスティック回帰だけ突き合わせる
11 Scholar.Metrics.Classification・Regression・ModelSelection 自作の評価指標・ROC・交差検証と突き合わせる。一部の指標は f64 を渡しても内部で f32 に落ちることを確かめる
12 Scholar.Linear.RidgeRegression ラッソは無いので自作のまま。リッジだけ突き合わせる
13 Scholar.Decomposition.PCA 寄与率まで持っているので、自作の主成分分析と全面的に突き合わせられる
14 Scholar.Cluster.KMeans 自作の K-means と突き合わせる

検討した代替案

代替案 採用しなかった理由
EXGBoost(決定木・勾配ブースティング) 決定木の穴を埋められるが、XGBoost のネイティブライブラリを抱えることになり、本書の第 3 章が扱う「ジニ不純度の決定木」とは別物(勾配ブースティング木)になる。自作との突き合わせにならない
Explorer(データフレーム) シリーズの方針でデータフレームのライブラリを使わない
EXLA(テンソルのバックエンド) Nx.BinaryBackend で足りる。依存が巨大になる
Phoenix(API) 第 15 章の主題には Plug と Bandit だけで足りる
Cowboy(HTTP サーバー) Bandit は Elixir で書かれていて設定が素直。Plug の標準的な選択肢として定着している
excoveralls(カバレッジ) 標準の mix test --cover にしきい値の仕組みがあり、依存を増やす理由が無い
Dialyzer・@spec(型) 動的型付けの言語で「型をテストで捕まえる」ことを示すのが Elixir 版の軸の一つ。Ruby 版の RBS・Steep と同じ扱い
Nx.Random・Erlang の :rand(乱数) どちらもほかの言語版と並びが合わない。java.util.Random と同じ線形合同法を自作すれば、並びを記事の中で完全に説明でき、JVM の言語版とも一致する

影響

  • 良い影響: パイプライン演算子・パターンマッチ・関数節による書き方が、F# 版・Clojure 版との対比としてそのまま記事の題材になる
  • 良い影響: Nx の f32/f64 の話が、シリーズで初めて「数値の型が既定で単精度である」ことを扱う機会になる
  • 良い影響: 乱数を自作するので、分割の結果を記事の中で完全に再現でき、しかも Java 版・Scala 版・Clojure 版と同じ並びになる。「乱数生成器は魔法ではなく 3 行の漸化式である」ことを第 2 章で示せる
  • 悪い影響: Scholar に決定木が無いため、ライブラリとの突き合わせがほかの言語版より大幅に少ない。 第 3〜6 章・第 10 章の一部・第 12 章の一部・第 14 章は自作だけで完結する。これは Elixir の機械学習の生態系の現状であり、記事にそのまま書く
  • 悪い影響: erl コマンドの OTP の版(28)と Elixir が動く版(27)が食い違うので、記事の環境構築の節に書く必要がある
  • 悪い影響: Shift_JIS のために codepagex を入れ、設定ファイル(config/config.exs)でどの符号化表を組み込むかを指定する必要がある。設定を忘れると実行時に {:error, "Unknown encoding ..."} になる