Skip to content

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

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

日付: 2026-09-20

ステータス

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

コンテキスト

執筆計画 の「C# 版執筆計画」で、C# 版は F# 版の実装を対比の相手にし、データフレームのライブラリを使わず record と LINQ でデータを表すことを承認した。.NET SDK の版と機械学習のライブラリは F# 版(ADR 004)にそろえる。

B29 のステップ 1 で、NuGet のカタログと使い捨てのプロジェクトによって、次を確かめた。

確認したこと 結果 確認方法
ライセンス Microsoft.ML 5.0.0・Microsoft.Data.Analysis 0.23.0・coverlet.MTP 10.0.1 は MIT、xunit.v3 4.0.1 は Apache-2.0 NuGet のカタログ
Microsoft.Data.Analysis の保守 安定版の最新 0.23.0 は 2025-11-11 の公開で、以後はプレビュー(0.24.0-preview)だけ。1.0 に達していない NuGet のカタログ
global.json の版 version 10.0.100 + rollForward: latestPatch なら、手元(10.0.100)でも Nix(10.0.101)でも解決できる。F# 版の 10.0.101 固定は手元では解決できない 使い捨てのプロジェクトで dotnet --version
アナライザーの水準 AnalysisMode: All は公開メソッドの引数の null 検査(CA1062)まで求める。Recommended では CA1304・CA1311(カルチャの指定)・CA1822(static にできる)などが出る 使い捨てのプロジェクトでビルド
警告をエラーにする TreatWarningsAsErrors により、コンパイラの警告(使っていない変数の CS0219 など)もエラーになる 同上
dotnet format 対象のソリューションにプロジェクトが登録されていないと、何も検査せずに成功する。登録すると崩れた整形を WHITESPACE として指摘する 同上
dotnet test apps/csharp を作業ディレクトリにして実行する。global.json の test.runner(Microsoft.Testing.Platform)は、作業ディレクトリから上に探した最も近い global.json から読まれるので、リポジトリのルートから実行すると古い VSTest の経路に落ちる。.NET 10 の dotnet test はパスを位置引数で受け取らず、--solution・--project を使う 手元(10.0.100)と Nix(10.0.101)で実行
0 件と判定された件(B29) 改名(apps/dotnet → apps/csharp)の後、古い状態を抱えた .NET のビルドサーバーが残っていると、dotnet test がテストを 0 件と判定して終了コード 5 で終わることがあった。dotnet build-server shutdown の後は、C# 版(15 件)も F# 版(313 件)も成功する 手元で再現と解消を確認
テストプロジェクトの設定(B29) OutputType Exe・IsTestProject・<Using Include="Xunit" /> が要る 実行して確認
検査が効いているか(B29) わざと違反を入れたファイルで、ビルドが CS0219(使っていない変数)・CA1822(static にできる)・IDE0055(コードスタイル)をエラーにし、dotnet format --verify-no-changes が WHITESPACE を指摘した
カバレッジ(B29) --coverlet --coverlet-include '[MachineLearning]*' --coverlet-output-format cobertura で cobertura の XML が出る(F# 版の CI と同じオプション)
BOM の扱い(B29) .NET の File.ReadAllLines・ReadAllText は BOM 付きの UTF-8 を読むと BOM を取り除く(先頭セルは 身長)。Python 版・Kotlin 版・Java 版で起きた「列名に BOM が残る」落とし穴は C# では起きないので、列名から BOM を消す処理は書かない
分割の一致(B30) 分割を F# 版と同じ System.Random(seed) + Fisher-Yates(後ろから Next(i + 1) で交換)にすると、訓練データとテストデータに入る行が F# 版と一致する。iris.csv・テスト 0.3・シード 0 で、訓練データの平均値(がく片長さ 0.424808、がく片幅 0.462286、花弁長さ 0.479135、花弁幅 0.432404)とテストデータの先頭 5 件のラベルが一致した。Java 版は Collections.shuffle なので一致しない C# 版と、F# 版の手順を写したスクリプトの両方で実測
第 3 章(B30) ML.NET に単一の決定木(CART)の学習器が無いので、F# 版(ADR 004)と同じく木を 1 本だけ作る FastTree を OneVersusAll で多クラスにして突き合わせる。C# では ML.NET が求める「引数なしのコンストラクターと書き換えられるプロパティを持つクラス」をそのまま書けるので、F# 版の [<CLIMutable>] に当たる工夫が要らない。特徴量の数は実行時に決まるので SchemaDefinition でベクトルの長さを指定する点は同じ。iris.csv・テスト 0.3・シード 0 で、深さごとの正解率・ML.NET との一致数・深さ 2 の木の境界が F# 版とすべて一致した
switch 式の網羅性(B30) C# には sealed interface が無く、抽象レコードと sealed な派生で閉じても、switch 式から _ の分岐を外すと CS8509(網羅されていない)の警告になり、TreatWarningsAsErrors でビルドが止まる。F# の判別共用体や Java の sealed interface のように網羅を証明できないので、最後の分岐を書く
値による比較(B30) Equals を書いて GetHashCode を書かないと CS0659 でビルドが止まる(警告をエラーにしているため)。Java 版で Error Prone が止めた「record の成分に配列」に当たるルールは .NET アナライザー(Recommended)に無いので、値で比べられることをテストで担保する。Row・Table も record だが成分が辞書・リストなので参照で比べる。値で比べる必要があるのは Features だけ
文字列の分割(B30) string.Split(char) は行末の空欄も保持する("0.1,0.2,0.3,,".Split(',') は 5 要素)。Java の split(",", -1) のような上限の指定は要らない
第 9 章(B32) Shift_JIS は Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) だけで読める(System.Text.Encoding.CodePages の追加は不要。InvariantGlobalization: true の影響も受けない)。Java の MalformedInputException と違い、.NET は文字コードが違っても例外を投げず黙って文字化けする。ML.NET の NormalizeMeanVariance は既定(fixZero: true)では平均を引かず、fixZero: false で自作の標準化(母標準偏差)と小数第 5 位まで一致する(ADR 004 と同じ)。コレクション式のスプレッド .. は [...] の中でだけ使える
第 7 章(B32) Ols は Intel MKL が x64 専用なので arm64 では使えない(ADR 004 と同じ)。代わりに ML.NET の SDCA を使う。SDCA は特徴量の大きさに敏感で、正規化しないと決定係数が -0.5124 まで壊れ、NormalizeMeanVariance を前に置くと 0.7659 に戻る。既定はマルチスレッドなので MLContext(seed: 0) だけでは再現せず、SdcaRegressionTrainer.Options.NumberOfThreads = 1 で固定する。自作の正規方程式の切片・係数・評価指標は F# 版と表示の桁まで一致した(SDCA は反復解法なので決定係数だけわずかに違う)。決め打ちの値を返す仮実装は CA1822(static にできる)でビルドが止まるので、C# 版の TDD では仮実装の書き方に制約がある
第 8 章(B32) System.Text.Json の [JsonPolymorphic]・[JsonDerivedType] で、インターフェースと抽象レコードをそのまま保存・復元できる(F# 版の DTO への詰め替えも、Java 版の ObjectInputFilter に当たる対策も要らない)。読み込みを厳しくするには RespectRequiredConstructorParameters = true。ML.NET の 2 値分類は FastTree(exampleWeightColumnName: "Weight", ...) でクラスの重みを行の重みとして表せ、C# では [<CLIMutable>] に当たる橋渡しが要らない。Microsoft.ML.ITransformer と章の ITransformer が重なるので別名が要る。自作の数値は深さ 1〜10 で F# 版と完全一致し、ML.NET との一致件数だけ深さ 8〜10 で 1 件違う
第 10 章(B32) IEstimator<out TTransformer> は共変なので、F# 版に必要だったアップキャストが要らない。FastForest の既定 minimumExampleCountPerLeaf = 10 は訓練データを分け切らない(1 にすると分け切る)。L-BFGS の L1・L2 の既定値 1 を 0 にすると、自作の勾配降下法と正解率が完全に一致する(ADR 004 と同じ)。同じ乱数・同じ分割でも特徴量の並び順が森の結果を変えるので、言語間の突き合わせでは列の順もそろえる(F# 版は Map のキー順、C# 版は CSV の列順)。アナライザーの CA1859 が private メソッドの戻り値を具体的な型にするよう求める
第 15 章(B33) 予測 API は追加のパッケージが要らない。<FrameworkReference Include="Microsoft.AspNetCore.App" />(Minimal API)と、テスト用の Microsoft.AspNetCore.TestHost で足りる(F# 版の Giraffe に当たるものは不要)。F# の Result に当たる型が無いので PredictionResult<T> を抽象レコードと sealed な派生で自作し、Match の最後の分岐は到達しないが C# では必要。実データで学習したモデルの予測(興行収入 7883.711445215134)は F# 版と小数点以下まで一致した
第 11〜12 章(B32) SDCA の L2 の尺度は 2 × alpha / 件数 で自作のリッジ回帰と一致する(ADR 004 と同じ)。罰則が弱いと反復が収束しきらないので、突き合わせには十分強い罰則が要る(30 件で alpha=1 は 1e-3 に収まらず、alpha=10 なら収まる)。特徴量が標準化済みなら NormalizeMeanVariance を入れずに済み、自作との差が第 7 章より小さくなる。Shuffle = false・NumberOfThreads = 1・ConvergenceTolerance = 1e-7 で再現性が取れる。評価指標・交差検証の数値は F# 版と表示の桁まで一致した
第 13〜14 章(B32) ML.NET に PCA は「ある」が部分的で、Transforms.ProjectToPrincipalComponents(ランダム化 PCA)は主成分の座標だけを返し、主成分・固有値・寄与率を取り出す API が無い。寄与率が要るので自作(ヤコビ法)を最終実装とし、ML.NET は座標の突き合わせ相手にする。ensureZeroMean の既定は false で、true にして退化したデータに使うと NaN を返す。KMeansTrainer.Options に初期中心を渡すフィールドは無い(InitializationAlgorithm だけ)ので、F# 版が FSharp.Stats で行った「同じ初期中心での完全一致」は C# ではできず、SSE の比較にとどまる。クラスタ番号は 1 始まり。C# の文法では (Point)[.. values] が添字アクセスと読まれて CS0119 になり、xUnit の Assert.Equal(expected, actual, digits) は桁が 0〜15 の外だと xUnit2016 でビルドが止まる

ML.NET を C# から使うときの癖は、F# 版で確かめた ADR 004 を起点にし、C# 版の各章で確かめて本 ADR に書き足す。

決定

用途 採用 バージョン ライセンス 初出
言語・実行環境 C#(net10.0)。global.json は 10.0.100 + rollForward: latestPatch .NET SDK 10.0.1xx — 第 1 章
テスト xUnit v3(Microsoft.Testing.Platform で実行) 4.0.1 Apache-2.0 第 1 章
カバレッジ coverlet.MTP 10.0.1 MIT 第 1 章(記事での解説は第 5 章)
整形 dotnet format(SDK 同梱)と .editorconfig SDK と同じ — 第 1 章(記事での解説は第 5 章)
静的解析 .NET アナライザー(AnalysisMode: Recommended)、TreatWarningsAsErrors、EnforceCodeStyleInBuild SDK と同じ — 第 1 章(記事での解説は第 5 章)
データの表現 record のリストと LINQ(データフレームのライブラリを使わない) — — 第 1 章
機械学習 ML.NET(Microsoft.ML・Microsoft.ML.FastTree) 5.0.0 MIT 第 3 章
API ASP.NET Core Minimal API と統合テスト(Microsoft.AspNetCore.Mvc.Testing か TestHost) SDK と同じ — 第 15 章
  • 依存の版は F# 版と同じく中央パッケージ管理(Directory.Packages.props)にまとめ、packages.lock.json で固定する
  • 整形・静的解析・カバレッジは B29 から検査に組み込む。Java 版・F# 版の教訓から、わざと違反を入れて検査が失敗することを確かめる
  • 機械学習と API のライブラリは、使う章に入ってから Directory.Packages.props に追加する

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

ADR 004(F# 版)の方針をそのまま使う。C# 版の各章で実装しながら確かめ、F# 版と違う結果になったときは本 ADR に書き足す。ML.NET は C# 向けの API なので、F# 版で必要だった型の橋渡しの一部は不要になる見込み。

章 置き換え 方針
3 ML.NET の決定木系(FastTree を OneVersusAll で多クラスにする) 自作の決定木と予測を突き合わせる
7 ML.NET・自作の正規方程式 係数と決定係数を突き合わせる
8 ML.NET クラスの重み付けの可否は第 8 章で確かめる
9 ML.NET の正規化 自作の標準化と突き合わせる
10 ML.NET のロジスティック回帰(SDCA)・FastForest 正解率を比べる
11 ML.NET の評価・交差検証 自作の評価指標と突き合わせる
12 ML.NET の正則化(SDCA の L2) ADR 004 の「L2 の尺度」を C# で確かめる
13 なし FSharp.Stats を使わないので、固有値分解を含めて自作を最終実装とする
14 ML.NET の K-means 初期中心を渡せるかを第 14 章で確かめる

検討した代替案

代替案 採用しなかった理由
Microsoft.Data.Analysis の DataFrame 安定版が 0.23.0 で 1.0 に達しておらず、2025-11 以降はプレビューだけ。Java 版と同じく record と LINQ で表す
AnalysisMode: All 公開メソッドの引数の null 検査(CA1062)まで求められ、記事に載せるコードが本題から離れる
StyleCop.Analyzers 安定版が 1.1.118 と古く、1.2.0 はベータのまま。整形は dotnet format と .editorconfig で足りる
Roslynator・SonarAnalyzer.CSharp SDK 同梱のアナライザーで足りるかを先に確かめる。必要になった章で再検討する
NUnit・MSTest F# 版と同じ xUnit v3 にそろえる
FSharp.Stats(数値計算) F# 向けの API で、C# から使う題材にならない。行列は自作する

影響

  • 良い影響: F# 版と同じ .NET・ML.NET・xUnit を使うので、記事の違いが言語の書き方の違いに絞られる
  • 良い影響: global.json を 10.0.100 + latestPatch にしたので、手元でも Nix でも同じ手順で動かせる
  • 悪い影響: データフレームを使わないので、第 2・8・9 章の前処理は記述が長くなる。その長さ自体を対比の題材にする
  • 悪い影響: 第 13 章は FSharp.Stats を使わないため、F# 版より自作の範囲が広い

コンプライアンス

  • apps/csharp/Directory.Packages.props に上記のライブラリと版だけが記載されている
  • C# CI(.github/workflows/csharp-ci.yml)がグリーンである
  • わざと違反を入れると整形・静的解析の検査が失敗する(B29 で確かめる)

備考

  • 著者: claude-code/claude-opus-5