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 ..."}になる