007 Scala 版のビルド・テスト・静的解析・機械学習・API ライブラリの選定¶
「機械学習から始めるプログラミング入門」Scala 版で使うライブラリを決める。
日付: 2026-09-20
ステータス¶
2026-09-20 提案されました
コンテキスト¶
執筆計画 の「対象言語」では、Scala 版の機械学習ライブラリの候補を Smile としていた。しかし Smile は版によってライセンスが違い、採用前に確認することになっていた。
B34 のステップ 1 で、Maven Central の POM と使い捨ての sbt プロジェクトによって、次を確かめた。
| 確認したこと | 結果 | 確認方法 |
|---|---|---|
| Smile のライセンス | Scala 3 向けの smile-scala_3 は 3.0.2 以降しか無く、3.0.2・4.x・5.x・6.x のいずれも GPL-3.0。LGPL-3.0 の 2.6.0 には Scala 3 版が無い |
Maven Central の POM |
| Tribuo | 4.3.2、Apache License 2.0。Kotlin 版(ADR 002)・Java 版(ADR 005)で各アルゴリズムの癖を確認済み | Maven Central |
| Tribuo を Scala から呼べるか | nix develop .#scala の sbt 1.12.0・Scala 3.3.6 で、ArrayExample[Label](Label("setosa"), Array("a", "b"), Array(0.1, 0.2)) が動いた(型引数を明示する)。Java の可変な API をそのまま呼べる |
使い捨ての sbt プロジェクト |
| ライセンス(そのほか) | ScalaTest 3.2.20・http4s 0.23.37・circe 0.14.16・sbt-scoverage 2.4.4 はいずれも Apache License 2.0 | Maven Central の POM |
| 安定版 | ScalaTest は 3.2.20(3.3.0 はマイルストーン版のみ)、http4s は 0.23.37(1.0.0 はマイルストーン版のみ)、circe は 0.14.16、sbt-scalafmt は 2.6.2 | Maven Central |
| Nix 環境 | Scala 3.3.6(LTS)、sbt 1.12.0、metals 1.6.4、scala-cli 1.11.0 | nix eval |
| 警告をエラーにする | -Wunused:all -Wvalue-discard -Xfatal-warnings で、使っていない import(E198)と使っていない値がエラーになる |
使い捨ての sbt プロジェクト |
| 整形とカバレッジ | scalafmtCheckAll は崩れた整形でエラーになる(.scalafmt.conf に version と runner.dialect = scala3 が要る)。coverage → test → coverageReport でカバレッジが出る |
使い捨てのプロジェクトと apps/scala |
| scalafmt の設定(B35) | .scalafmt.conf は version と runner.dialect の両方が必要(どちらを欠いてもエラー)。-Wvalue-discard は戻り値の型が Unit の定義の中でだけ働き、文の位置で値を捨てた場合は -Wnonunit-statement を足さないと止まらない。本シリーズでは -Wnonunit-statement は使わず、意図して捨てるときは val _ = と書く |
|
| sbt の版(B34) | sbt-scalafmt 2.6.2 は sbt 1.12.9 以上を求め、Nix の sbt 1.12.0 では「requires sbt 1.12.9+」で失敗する。project/build.properties に sbt.version=1.12.9 と書くと、Nix の sbt のランチャーがその版を取得して動く |
apps/scala で実行 |
| 標準ライブラリの版の表示 | Scala 3 でも scala.util.Properties.versionNumberString は 2.13.16 を返す(Scala 3 は 2.13 の標準ライブラリを使う) |
同上 |
各章で確かめた結果¶
| 章 | 確かめたこと |
|---|---|
| 1 | -Wvalue-discard は「戻り値の型が Unit の定義の中で、非 Unit の値を捨てたとき」に働く。テストの本体で assume(...) を書くだけでは働かないが、Unit を返すヘルパーの中では働く。assume を使うヘルパーは Assertion を返し、呼び出し側で : Unit と書くのがこの設定での定型。-Wunused:all は使っていない import・private の値を捕まえるが、公開メソッドの使っていない引数は指摘しない(Java 版の Error Prone の UnusedMethod のようには仮実装を止めない) |
| 2 | 分割を Java 版と同じ java.util.Random + Fisher-Yates にすると、並べ替えの結果が Java 版と一致する(シード 0 で [4, 8, 9, 6, 3, 5, 2, 1, 7, 0])。iris.csv の訓練データの平均値(がく片長さ 0.4215384615384616 など)とテストデータの先頭のラベルも Java 版と一致した。Features の値を Vector で持つと、case class の等価判定がそのまま値の比較になる(Java・C# は配列を包む工夫が要る) |
| 3 | Tribuo の ArrayExample[Label] は型引数を明示すれば Scala からそのまま作れる。MutableDataset は ListDataSource に asJava した事例のリストを渡す。決定木は Scala 3 の enum で閉じ、match の網羅性はコンパイラが検査する(Java の sealed interface・C# の抽象レコードに当たる)。iris.csv・テスト 0.3・シード 0 で、深さごとの正解率と Tribuo との一致(どの深さでも全件一致)が Java 版と同じになった |
| 7 | 回帰には tribuo-regression-slm の追加が要る(tribuo-regression-core・tribuo-math は推移的に入る)。SLMTrainer(true)・LARSTrainer() の予測は自作の正規方程式と 1e-9 以内で一致し、SLMTrainer(false)・LinearSGDTrainer は一致しない(ADR 002・005 と同じ)。SparseLinearModel の重みは正規化した空間の値で、getFeatureIDMap.getID(name) で引く。SLMTrainer は学習のたびに標準出力へ進捗を書く。Scala 固有として、重みを見るには asInstanceOf[SparseLinearModel] が要り、複数のトレーナーを 1 つの Vector に入れるときは型を明示する。自作の予測値は Java 版と最後の桁まで一致した(Kotlin 版とは 1 桁違う) |
| 8 | Java のシリアライズは Scala では使えない。ObjectInputFilter を設定すると JDK のフィールドの型検査が走り、Scala の不変コレクションの DefaultSerializationProxy が解決される前に Vector 型のフィールドへ代入されて ClassCastException になる(フィルタ無しなら読める)。そこで Scala 版はモデルをタブ区切りのテキストで保存し、版を match で検証して任意のクラスを復元しない形にした。Scala 3 のラムダは Serializable だが、クラス名が X$$$Lambda/0x... と不安定で許可リストに書けない。Tribuo の CART との不一致は第 3 章と同じ「同じ不純度の分割候補の選び方」由来で、前処理を挟んでも変わらない |
| 9 | MeanStdDevTransformation は不偏標準偏差を使い、自作との比は √((n−1)/n)(ADR 002・005 と値まで同じ)。Java 向けの API は Scala からそのまま呼べ、橋渡しは toArray・java.util.Optional の .toScala・.toVector の 3 か所だけ。Shift_JIS のファイルを UTF-8 として読むと、JVM なので Java 版と同じく MalformedInputException になる。tribuo-math と org.tribuo.transform は tribuo-classification-tree から推移的に入る |
| 10 | 特徴量の重要度は「木ごとに正規化してから平均し、最後にもう一度正規化する」手順にそろえると Java 版と一致する。最初に「全体を合わせてから正規化」と書いたときは値がずれた |
決定¶
| 用途 | 採用 | バージョン | ライセンス | 初出 |
|---|---|---|---|---|
| 言語・ビルド | Scala(LTS)、sbt(project/build.properties で指定) |
3.3.6、1.12.9 | Apache License 2.0 | 第 1 章 |
| テスト | ScalaTest | 3.2.20 | Apache License 2.0 | 第 1 章 |
| 整形 | scalafmt(sbt-scalafmt) | 2.6.2 | Apache License 2.0 | 第 1 章(記事での解説は第 5 章) |
| 静的解析 | コンパイラの警告(-Wunused:all・-Wvalue-discard など)を -Xfatal-warnings でエラーにする |
Scala と同じ | — | 第 1 章(記事での解説は第 5 章) |
| カバレッジ | sbt-scoverage | 2.4.4 | Apache License 2.0 | 第 1 章(記事での解説は第 5 章) |
| データの表現 | case class と不変のコレクション(Map・Vector)。データフレームのライブラリを使わない |
— | — | 第 1 章 |
| 乱数 | java.util.Random(seed) と Fisher-Yates |
JDK と同じ | — | 第 2 章 |
| 機械学習 | Tribuo | 4.3.2 | Apache License 2.0 | 第 3 章 |
| 行列 | 自作の小さな不変の型 | — | — | 第 7 章 |
| API | http4s + circe | 0.23.37、0.14.16 | Apache License 2.0 | 第 15 章 |
- Smile は使わない。 Scala 3 で使える版がすべて GPL-3.0 で、PUBLIC リポジトリで配るサンプルコードの利用条件に影響する(Kotlin 版の ADR 002 と同じ判断)。代わりに Kotlin 版・Java 版と同じ Tribuo を Scala から使い、Java 向けの API を Scala の不変なコレクションへ橋渡しすることを題材にする
- 依存の版は
build.sbtにまとめる。ライブラリは使う章に入ってから追加する - 整形・静的解析・カバレッジは B34 から検査に組み込む。Java 版・C# 版の教訓から、わざと違反を入れて検査が失敗することを確かめる
章ごとのライブラリへの置き換え方針¶
ADR 002(Kotlin 版)の方針をそのまま使う。Tribuo は同じ 4.3.2 なので、アルゴリズムの有無と癖は同じはず。Scala 版の各章で確かめ、違う結果になったときは本 ADR に書き足す。
| 章 | 置き換え | 方針 |
|---|---|---|
| 3 | Tribuo の CART(ジニ不純度) | 自作の決定木と予測を突き合わせる |
| 7 | Tribuo の SLMTrainer(true)・LARSTrainer |
係数と決定係数を突き合わせる |
| 8 | Tribuo の決定木 | クラスの重み付けは自作の木で示す |
| 9 | Tribuo の MeanStdDevTransformation |
不偏標準偏差の違いを踏まえて突き合わせる |
| 10 | Tribuo の LogisticRegressionTrainer・RandomForestTrainer |
正解率を比べる |
| 11 | Tribuo の評価器・KFoldSplitter |
自作の評価指標と突き合わせる |
| 12 | Tribuo の ElasticNetCDTrainer |
ラッソ回帰とリッジ回帰を突き合わせる |
| 13 | なし | PCA のモジュールが無いので、Tribuo の固有値分解を使った自作を最終実装とする |
| 14 | Tribuo の KMeansTrainer |
初期中心を渡せないので、SSE の大きさを比べる |
| 11〜12 | LabelEvaluation は正例を一度も予測しなくても 0.0 を返す(NaN にならない)。RegressionEvaluator に MSE は無く、RMSE の 2 乗が自作と一致。KFoldSplitter の余りの配り方は自作と一致するが、乱数の実装が違うので行の割り当ては一致しない。ElasticNetCDTrainer は l1Ratio = 0 を拒否し下限は 1e-12、alpha を件数で割ると自作のリッジ回帰と 1e-6 以内で一致する(ADR 002・005 と同じ)。係数を読むには asInstanceOf[SparseLinearModel] が要る |
| 13〜14 | DenseMatrix.eigenDecomposition は Scala から呼んでも固有値を降順で返し、対称でない行列では空の Optional を返す。PCA は Tribuo に無いので自作が最終実装。KMeansTrainer に初期中心は渡せない(Initialisation は RANDOM と PLUSPLUS だけ)ので SSE で比べる。tribuo-clustering-kmeans の追加が要る。Java の静的メンバーは classOf[KMeansTrainer] から取る |
| 15 | 予測 API には http4s-ember-server・http4s-dsl・http4s-circe・circe-generic・circe-parser の追加が要る。UnprocessableEntity は 0.23.31 で非推奨になっており、-Xfatal-warnings があるので UnprocessableContent に直す必要がある。Scala には Either があるので、F# 版の Result と同じ形でドメインを書ける(Java 版は検査例外、C# 版は自作の型)。統合テストは HttpRoutes を直接呼べるので、テスト用のサーバーを立てなくてよい |
検討した代替案¶
| 代替案 | 採用しなかった理由 |
|---|---|
Smile(smile-scala_3) |
Scala 3 で使える版がすべて GPL-3.0 |
| Smile 2.6.0(LGPL-3.0) | Scala 3 向けの成果物が無い |
| Spark MLlib | クラスタ計算の仕組みごと持ち込むことになり、本シリーズの題材に対して大きすぎる |
| Breeze 2.1.0(行列) | 正規方程式や固有値分解の仕組みを見せるため、行列は自作する。必要になった章で再検討する |
| MUnit | 「対象言語」の表のテスト基盤は ScalaTest。ほかの言語版と同じく、その言語で標準的なものを選ぶ |
| scalafix・WartRemover | コンパイラの警告をエラーにする設定で足りるかを先に確かめる |
| Play Framework(API) | 第 15 章の主題(層の分離と統合テスト)に対して依存が大きい |
影響¶
- 良い影響: すべてのライブラリが Apache License 2.0 で、サンプルコードの利用条件が単純になる
- 良い影響: Kotlin 版・Java 版と同じ Tribuo を使うので、JVM の 3 言語で同じライブラリの使い方を比べられる
- 悪い影響: Scala で広く使われる Smile を使わないので、「Scala の機械学習ライブラリ」の紹介としては物足りない。その理由(ライセンス)を第 3 章と本 ADR に明記する
- 悪い影響: Tribuo は Java 向けの API なので、Scala の不変なコレクションとの橋渡しが各章で要る
コンプライアンス¶
apps/scala/build.sbtに上記のライブラリと版だけが記載されている- Scala CI(
.github/workflows/scala-ci.yml)がグリーンである - わざと違反を入れると整形・静的解析の検査が失敗する(B34 で確かめる)
備考¶
- 著者: claude-code/claude-opus-5