Skip to content

第 4 章: バージョン管理とデータ管理

4.1 はじめに

第 1 部では、TDD でデータを読み込み、前処理し、決定木で分類するプログラムを作りました。第 2 部では、そのプログラムを「変更を楽に安全にできる」状態に保つための道具立てを、バージョン管理(第 4 章)、パッケージ管理と静的解析(第 5 章)、タスクランナーと CI/CD(第 6 章)の順に整えます。

この章ではバージョン管理を扱います。機械学習のプロジェクトでは、コードに加えて 学習データ と 学習済みモデル というファイルが登場します。これらをコードと同じようにコミットしてよいとは限りません。この章では、Git で何を管理し、何を管理しないか、そして実験の結果を再現できるようにするには何を固定すればよいかを学びます。

Git の使い方は言語に依存しないので、Python 版の第 4 章・Kotlin 版の第 4 章・TypeScript 版の第 4 章 と同じ構成で進めます。F# 版では次の 2 点に注目してください。

  • bin/・obj/ の除外 — dotnet build と dotnet restore が作るディレクトリを除外し、packages.lock.json とローカルツールの設定はコミットする
  • 乱数と .NET の版 — 第 2 章で予告した「シード付きの System.Random の乱数列は .NET の版によって変わるか」を、ドキュメント・ソースコード・実行の 3 つで確かめ、学習用テストで固定する

4.2 コミットメッセージの重要性

コミット履歴は「なぜこの変更をしたのか」の記録です。機械学習のプロジェクトでは、「前処理を変えたら正解率が変わった」「ライブラリと予測が一致しない原因をテストに残した」といった変化の理由を、あとから追跡できることが特に重要になります。

コミットは次の 2 つを守ると追いやすくなります。

  • 1 コミット 1 目的 — 機能追加と設定変更、実装と記事を 1 つのコミットに混ぜない
  • 形式をそろえる — 種類と対象がひと目で分かる書式でメッセージを書く

4.3 Conventional Commits

フォーマット

本シリーズでは Conventional Commits の形式でコミットメッセージを書きます。

<type>(<scope>): <subject>

<body>

<footer>

scope には変更の対象を書きます。本リポジトリでは、F# 版の実装なら fsharp、記事シリーズなら getting-start-ml、Nix の環境定義なら nix のように書きます。F# 版の実装は apps/fsharp/ にあり、scope も言語名にそろえます。同じ .NET の上で C# 版を作ったときに、どちらの言語の変更かを scope で区別できるようにするためです。

コミットタイプ

type 用途
feat 新機能(章の実装など)
fix バグ修正
test テストの追加・修正
refactor 振る舞いを変えないコードの変更
docs ドキュメントだけの変更(記事など)
chore ビルドやツール、依存関係の変更
ci CI の設定の変更

実践例

本リポジトリの実際のコミット履歴から、F# 版の環境づくりから第 1 章までを抜き出します(古い順)。

03a99d1a chore(nix): dotnet 環境の .NET SDK を 10 にする
26030da8 chore(dotnet): F# のソリューション雛形を追加
889e9e3a chore(dotnet): Fantomas が書く改行コードを LF にそろえる
61a15cb5 feat(dotnet): 第 1 章 きのこ派・たけのこ派をルールで判定する処理を追加
c054a8c6 ci(dotnet): Nix と dotnet CLI で F# の整形・静的解析・テストを実行する
63bd9ffb fix(dotnet): FSharp.Core を .NET SDK 同梱のパッケージではなく nuget.org から取る
6d40b551 docs(adr): 004 F# 版の機械学習・データ・可視化・API ライブラリの選定を追加
d8344658 docs(getting-start-ml): F# 版トップと第 1 章を追加し、nav・索引・進捗・執筆計画に登録

git log --oneline で見ると、どのコミットが何のための変更かを type と scope だけで区別できます。環境(chore(nix))、ビルドの設定(chore(dotnet))、実装(feat(dotnet))、CI(ci(dotnet))、CI で見つかった問題の修正(fix(dotnet))、記事(docs)を分けてあるので、たとえば「CI で何が起きて、どう直したか」を追いたいときは ci と fix のコミットだけを見れば済みます。63bd9ffb の経緯は第 5 章で扱います。

ここに並んだコミットの scope が dotnet なのは、当時は実装を apps/fsharp/ に置いていたためです。C# 版と場所が重ならないよう apps/fsharp/ に移してからは、scope も fsharp にしています。

コミットメッセージの本文には、変更の 理由 を書きます。889e9e3a の本文は次のとおりです。

chore(dotnet): Fantomas が書く改行コードを LF にそろえる

Windows では Fantomas が CRLF で書き出し、.gitattributes の LF と食い違うため。

件名だけでは「なぜ LF にそろえる必要があったのか」が分かりません。本文があれば、.editorconfig の設定を消してよいかを後から判断できます。

4.4 何をコミットし、何をコミットしないか

機械学習のプロジェクトには、コミットしてはいけないファイルが増えます。理由は大きく 3 つあります。

分類 例 コミットしない理由
再配布できないもの 学習データ ライセンスで利用者が限られている
再生成できるもの ビルド成果物(bin/・obj/)、テストの結果、学習済みモデル、Notebook の出力 大きく、差分が読めず、コードとデータから作り直せる
秘匿すべきもの .env の認証情報 漏洩すると取り返しがつかない

学習データ

本シリーズの学習データは、書籍『スッキリわかる Python による機械学習入門』の配布データです。配布データの利用は書籍購入者に限られており、本リポジトリは公開されているので、データはコミットしません。ルートの .gitignore で、データの置き場所 apps/data/ をまるごと除外しています。この設定はすべての言語の版で共通です。

# 依存関係
node_modules/

# 環境変数(暗号化済みの .env.vault とテンプレートの .env.example は管理対象)
.env

# 学習データ(書籍購入者のみ利用可のため再配布しない。docs/article/getting-start-ml/outline.md を参照)
apps/data/

# ビルド成果物・一時ファイル
site/
tmp/

# IDE・エディタの個人設定
.idea/
.vscode/

# Claude Code の個人設定
.claude/settings.local.json
# エージェントの作業用 worktree
.claude/worktrees/

# OS
.DS_Store
Thumbs.db

.NET プロジェクト固有のファイル

apps/fsharp/.gitignore では、dotnet のコマンドが作るディレクトリに加えて、学習済みモデルの保存先と Notebook のチェックポイントを除外しています。

bin/
obj/
TestResults/
model/
.ipynb_checkpoints/
パス 中身
bin/ dotnet build の出力(MachineLearning.dll、依存ライブラリの DLL、実行ファイル)
obj/ dotnet restore が解決した依存関係(project.assets.json)と、ビルドの中間ファイル
TestResults/ dotnet test にカバレッジを出力させたときの結果(第 5 章)
model/ 学習済みモデルの保存先(第 8 章で使う)
.ipynb_checkpoints/ Notebook の自動保存

bin/ と obj/ には、先頭に / を付けていません。.gitignore の規則は、/ で始まらなければ、そのディレクトリより下の どの深さにある 同じ名前にも当てはまります。本体の src/MachineLearning/bin/ とテストの tests/MachineLearning.Tests/bin/ の両方を、1 行で除外できます。手元では、この 2 つの bin/ だけで 56 MB ありました(ML.NET の DLL が多くを占めます)。

一方で、次のファイルは コミットします。

ファイル 役割
global.json .NET SDK の版(10.0.101)
Directory.Build.props・Directory.Packages.props プロジェクト共通の設定と、依存ライブラリの版
src/MachineLearning/packages.lock.json・tests/MachineLearning.Tests/packages.lock.json 依存関係の依存関係まで含めた正確な版とハッシュ
.config/dotnet-tools.json Fantomas・FSharpLint の版(ローカルツール)

どれも「どの版で作ったか」を記録するファイルで、第 5 章で詳しく扱います。obj/project.assets.json も解決した依存関係を記録していますが、こちらは手元のパスを含む生成物で、packages.lock.json から作り直せるのでコミットしません。

学習済みモデルは、コードと学習データがあれば作り直せます。モデルのファイルをコミットするより、「どのコードとどのデータから、どの設定で作ったか」をコミットで追えるようにするほうが、再現性の面でも役に立ちます。

Notebook の出力セルも同じ理由でコミットしません。出力には学習データの行やグラフが含まれることがあり、データをコミットしない方針に反するからです。出力セルを消す仕組みは第 6 章で扱います。

除外されていることを確かめる

.gitignore が意図どおりに効いているかは、git check-ignore -v で確認できます。どのファイルの何行目の規則で除外されたかが表示されます。

git check-ignore -v apps/data/sukkiri-ml/iris.csv apps/fsharp/src/MachineLearning/bin/ apps/fsharp/src/MachineLearning/obj/ apps/fsharp/tests/MachineLearning.Tests/bin/ apps/fsharp/TestResults/ apps/fsharp/model/ apps/fsharp/notebooks/.ipynb_checkpoints/ tmp/sukkiri-ml-codes.zip
.gitignore:8:apps/data/ apps/data/sukkiri-ml/iris.csv
apps/fsharp/.gitignore:1:bin/   apps/fsharp/src/MachineLearning/bin/
apps/fsharp/.gitignore:2:obj/   apps/fsharp/src/MachineLearning/obj/
apps/fsharp/.gitignore:1:bin/   apps/fsharp/tests/MachineLearning.Tests/bin/
apps/fsharp/.gitignore:3:TestResults/   apps/fsharp/TestResults/
apps/fsharp/.gitignore:4:model/ apps/fsharp/model/
apps/fsharp/.gitignore:5:.ipynb_checkpoints/    apps/fsharp/notebooks/.ipynb_checkpoints/
.gitignore:12:tmp/  tmp/sukkiri-ml-codes.zip

コミットするはずのファイルが除外されていないことも確かめます。除外されていなければ何も表示されず、終了コードが 1 になります。

git check-ignore -v apps/fsharp/src/MachineLearning/packages.lock.json apps/fsharp/.config/dotnet-tools.json
echo $?
1

ディレクトリのパスは末尾に / を付けて指定しています。bin/ のように末尾が / の規則はディレクトリだけに当てはまるので、まだ存在しないパスを / なしで調べると、除外されていないように見えます。

改行コードを固定する

ルートの .gitattributes では、apps/fsharp/ 以下のテキストファイルの改行コードを LF に固定しています。

apps/fsharp/** text=auto eol=lf

Windows の Git は、既定の設定でチェックアウトするときに改行コードを CRLF に変えることがあります。整形ツールの Fantomas は改行コードも書式として扱うので、手元(Windows)と CI(Linux)で改行コードが違うと、整形の検査の結果が変わってしまいます。Fantomas が書き出す改行コードも .editorconfig で LF にそろえています(第 5 章)。

4.5 データの入手手順をコードにする

データをコミットしない代わりに、データの入手と配置の手順 をリポジトリに残します。手順が文章だけだと、人によって置き場所がずれたり、ファイルが足りないまま実行して分かりにくいエラーになったりします。

本リポジトリでは、配置と確認を Gulp のタスクにしています(ops/scripts/data.js)。このタスクは言語に依存しないので、すべての言語の版で同じものを使います。配布 ZIP を tmp/ か apps/data/ に置いてから、リポジトリのルートで実行します。

npx gulp data:setup
npx gulp data:check

タスクの一覧と、失敗したときの表示は Python 版の 4.5 節 を参照してください。

プログラムからデータの場所を知る

F# の実装は、第 1 章で作った Dataset.dataDir でデータの場所を解決します。環境変数 ML_DATA_DIR があればその場所を、無ければ apps/data/sukkiri-ml/ を使います。

// src/MachineLearning/Dataset.fs
module MachineLearning.Dataset

open System
open System.IO

/// 学習データのディレクトリ。環境変数 ML_DATA_DIR が無ければ apps/data/sukkiri-ml を使う。
/// dotnet test はテストの出力先(bin/Debug/net10.0)で動くので、既定の場所はこのファイルの場所から求める。
let dataDirFrom (getenv: string -> string option) : string =
    getenv "ML_DATA_DIR"
    |> Option.defaultValue (Path.Combine(__SOURCE_DIRECTORY__, "..", "..", "..", "data", "sukkiri-ml"))

/// 環境変数を読んで学習データのディレクトリを返す。
let dataDir () : string =
    dataDirFrom (Environment.GetEnvironmentVariable >> Option.ofObj)
  • データの場所をコードに直接書かず、1 か所で解決するようにしておくと、CI や読者の環境など置き場所が違う場合にも環境変数だけで切り替えられます
  • 既定の場所を __SOURCE_DIRECTORY__(このソースファイルのディレクトリ)から求めているのは、dotnet test が bin/Debug/net10.0/ をカレントディレクトリにしてテストを動かすためでした(第 1 章)。bin/ はコミットしない生成物の置き場で、データの場所の基準にはできません

dotnet test の子プロセス(テストの実行ファイル)には、環境変数がそのまま引き継がれます。Kotlin 版の Gradle のように「入力が変わらなければテストを実行しない」という判断もしないので、ML_DATA_DIR を変えれば次の実行からそのまま反映されます。

データが無い環境でもテストを通す

コミットしないデータに依存するテストは、データの無い環境(CI や、データをまだ入手していない読者の環境)では失敗してしまいます。F# 版では、xUnit v3 の Assert.SkipUnless で、データが無ければテストをスキップします(第 1 章)。

let csvFile = Path.Combine(dataDir (), "iris.csv")

/// 学習データが無ければテストをスキップする
let requireData () =
    Assert.SkipUnless(File.Exists csvFile, "学習データ iris.csv が配置されていない(gulp data:setup)")

データの無い場所を ML_DATA_DIR に指定すると、スキップされることを確かめられます。

ML_DATA_DIR=/nonexistent dotnet test --filter-namespace "*Chapter03*"
スキップされました MachineLearning.Tests.Chapter03.IrisDataTest.深さ 2 の決定木はテストデータの 45 件中 40 件を正しく分類する (0ms)
スキップされました MachineLearning.Tests.Chapter03.IrisDataTest.実行すると深さごとの正解率・ML.NET との一致数・深さ 2 の決定木を表示する (0ms)
テストの実行の概要: 成功!
  合計: 17
  失敗: 0
  成功: 15
  スキップ済み: 2
  • --filter-namespace は、xUnit v3 が Microsoft.Testing.Platform に加えるオプションで、名前空間(F# ではモジュールの階層)でテストを絞り込みます。* は任意の文字列に当てはまります
  • スキップの理由として、Assert.SkipUnless に渡したメッセージ(gulp data:setup)が表示されます。読者は、次に何をすればよいかがすぐに分かります

単体テストは架空の値で作ったデータで書き、実データのテストは Assert.SkipUnless で守る、という 2 段構えにすると、CI ではデータ無しでも品質チェックが回り、手元ではデータを使った確認もできます。単体テストのデータに配布データの行をそのまま写さないことも大切です。テストのコードはコミットされるので、写した行はデータの再配布になってしまいます。第 2 章の型プロバイダのサンプル(IrisSample)を架空の値で書いたのも、同じ理由です。

4.6 実験を再現できるようにする

機械学習の結果は、コードとデータが同じでも、次のものが違うと変わります。

変わる原因 固定する方法 本リポジトリでの置き場所
乱数(データの分割、モデルの初期値) シードを指定する 各章のコード(第 2 章の splitTrainTest の seed、ML.NET の MLContext(seed = ...))
ライブラリのバージョン 版を 1 か所に固定し、依存関係の依存関係までロックする Directory.Packages.props・packages.lock.json(第 5 章)
.NET SDK のバージョン SDK の版を固定する global.json(第 5 章)
.NET の実行環境のバージョン 対象のフレームワークを固定する Directory.Build.props の TargetFramework(net10.0)

乱数のシード

第 2 章の shuffle は、System.Random にシードを渡して作った乱数生成器で、Fisher–Yates のシャッフルをしていました。同じシードなら同じ順、違うシードなら違う順になることは、第 2 章のテストで固定しています。

[<Fact>]
let ``同じシードなら同じ順に並べ替える`` () =
    Assert.Equal<int list>(shuffle 42 items, shuffle 42 items)

[<Fact>]
let ``シードが違えば違う順に並べ替える`` () =
    Assert.NotEqual<int list>(shuffle 0 items, shuffle 1 items)

ただし、このテストが確かめているのは「同じ実行環境の中で」同じ順になることだけです。第 3 章の実データのテストが「45 件中 40 件」という数値を固定できているのは、シード 0 で分けたときにテストデータに入る行が、いつ実行しても同じだからです。.NET を上げたときに乱数列が変われば、テストデータに入る行が変わり、この数値も変わります。

シード付きの乱数列は .NET の版で変わるか

第 2 章で予告したこの問いを、3 つの方法で確かめます。

ドキュメント: .NET の System.Random の解説は、シードを指定して同じ乱数列を得る例のあとに、次のように注意しています。

Note that the example may produce different sequences of random numbers if run on different versions of .NET.

— Supplemental API remarks for Random

同じシードでも、.NET の版が違えば違う乱数列になる 可能性がある、ということです。Kotlin の Random(seed) のドキュメント(Kotlin 版の 4.6 節)と同じく、版をまたいだ再現は約束されていません。

ソースコード: 一方で、.NET の実装(Random.cs)は、シードを渡したかどうかでアルゴリズムを使い分けています。

public Random() =>
    _impl = GetType() == typeof(Random) ? new XoshiroImpl() : new CompatDerivedImpl(this);

public Random(int Seed) =>
    _impl = GetType() == typeof(Random) ? new CompatSeedImpl(Seed) : new CompatDerivedImpl(this, Seed);

シードを渡さなければ新しいアルゴリズム(XoshiroImpl)を、シードを渡せば以前の .NET と同じアルゴリズム(CompatSeedImpl)を使います。ソースコードのコメントには、シードを渡したときは「過去に使われてきたのと同じアルゴリズムを守る必要がある」という趣旨が書かれています。互換性を意図して作られてはいるが、ドキュメントとしては約束していない、というのが現在の状態です。

実行: 手元には .NET 8・9・10 の実行環境が入っていたので、使い捨てのプロジェクトの対象を net8.0;net9.0;net10.0 の 3 つにして、同じコードを実行しました。コードは、Random 0 の Next() を 3 回呼んだ値と、第 2 章と同じ Fisher–Yates のシャッフルで 0〜9 を並べ替えた結果、シードを渡さない Random() の値を表示します。

dotnet run -f net8.0
dotnet run -f net9.0
dotnet run -f net10.0
実行環境: .NET 8.0.22
Random(0).Next() を 3 回: [1559595546; 1755192844; 1649316166]
shuffle 0 [0..9]: [0; 4; 5; 8; 2; 1; 3; 6; 9; 7]
シードなし: [2; 7; 8; 3; 2; 1; 4; 7; 0; 4]
実行環境: .NET 9.0.11
Random(0).Next() を 3 回: [1559595546; 1755192844; 1649316166]
shuffle 0 [0..9]: [0; 4; 5; 8; 2; 1; 3; 6; 9; 7]
シードなし: [5; 7; 9; 6; 7; 8; 5; 1; 6; 5]
実行環境: .NET 10.0.1
Random(0).Next() を 3 回: [1559595546; 1755192844; 1649316166]
shuffle 0 [0..9]: [0; 4; 5; 8; 2; 1; 3; 6; 9; 7]
シードなし: [1; 4; 4; 6; 1; 0; 8; 1; 5; 2]
  • -f(--framework)は、複数の対象のフレームワークのうち、どれでビルドして実行するかを選ぶオプションです
  • シード 0 の乱数列とシャッフルの結果は、3 つの版で同じでした。シャッフルの結果は、第 2 章で記録した順と同じです
  • シードを渡さない Random() は、版に関係なく実行のたびに変わります

3 つの方法の結果をまとめると、「.NET 8〜10 では、シード付きの乱数列は変わらなかった。ただし将来の版で変わらないとは約束されていない」となります。

学習用テストで乱数列を固定する

約束されていない性質に数値のテストが頼っているなら、その性質そのものをテストにしておきます。.NET を上げて乱数列が変わったとき、第 3 章の「45 件中 40 件」のテストだけが失敗すると、原因が決定木の実装にあるのか乱数にあるのかを調べる必要があります。乱数列のテストも一緒に失敗すれば、原因はすぐに分かります。

// tests/MachineLearning.Tests/Chapter04/RandomSequenceTest.fs
/// System.Random のシード付きの乱数列を固定する学習用テスト。
/// .NET を上げてこのテストが失敗したら、分割に入る行と記事の数値が変わる。
module MachineLearning.Tests.Chapter04.RandomSequenceTest

open System
open Xunit
open MachineLearning.Chapter02.Random

[<Fact>]
let ``シード 0 の乱数列は .NET 8・9・10 で同じ値になる`` () =
    let random = Random 0

    let values = [ random.Next(); random.Next(); random.Next() ]

    Assert.Equal<int list>([ 1559595546; 1755192844; 1649316166 ], values)

[<Fact>]
let ``シード 0 で 0 から 9 を並べ替えた順は第 2 章で記録した順と同じ`` () =
    Assert.Equal<int list>([ 0; 4; 5; 8; 2; 1; 3; 6; 9; 7 ], shuffle 0 [ 0..9 ])

[<Fact>]
let ``シードを渡さなければ乱数列は Random を作るたびに変わる`` () =
    let draw (random: Random) = List.init 10 (fun _ -> random.Next())

    Assert.NotEqual<int list>(draw (Random()), draw (Random()))
  • これは自分のコードではなく、.NET の振る舞いを確かめる 学習用テスト です。期待値は上の実行で得た値なので、書いた時点で通ります(失敗から始める TDD のテストとは役割が違います)
  • [ random.Next(); random.Next(); random.Next() ] は、リストの要素を左から順に評価します。Random は呼ぶたびに状態が進む可変なオブジェクトなので、この順番に意味があります
  • List.init 10 (fun _ -> random.Next()) は、0〜9 の添字ごとに関数を呼んで 10 要素のリストを作ります。添字は使わないので _ で受けています
  • 3 つ目のテストは、「シードを渡さなければ毎回変わる」ことを確かめます。10 個の値がすべて一致する確率は無視できるほど小さいので、このテストが偶然に失敗することは実際上ありません

テストの .fs ファイルを MachineLearning.Tests.fsproj の Compile の最後(第 3 章のテストの後)に加えて実行します。

dotnet test --filter-namespace "*Chapter04*"
テストの実行の概要: 成功!
  合計: 3
  失敗: 0
  成功: 3
  スキップ済み: 0

このテストは、global.json の SDK と TargetFramework を上げるときの見張り役になります。

シードだけでは再現できないもの

シードを固定しても、乱数を作るアルゴリズムが違えば結果は変わります。Python 版(NumPy)・Kotlin 版(kotlin.random)・TypeScript 版(自作の乱数生成器)・F# 版(System.Random)は、どれも同じシード 0 で分割していますが、乱数を作るアルゴリズムがすべて違うので、テストデータに入る行は版ごとに違います。第 3 章で、F# 版の決定木の正解率の値が他の版と一致しなかったのはこのためです。

記事に載せる正解率などの数値は、シードと版を固定した実装を実データで動かした結果です。数値をテストで固定するときは、どのシードで得た値かを必ずコードに残し、乱数列そのものも学習用テストで固定しておきます。

4.7 TDD とコミットのタイミング

TDD のサイクルとコミットは、次のように対応させると履歴が読みやすくなります。

uml diagram

  • テストが通らない状態ではコミットしない
  • 実装と記事は別のコミットにする
  • 設定や依存関係の変更(chore)、CI の変更(ci)も、それぞれ別のコミットにする
  • 学習データ・モデル・Notebook の出力がステージングされていないことを、コミットの前に git status で確かめる

第 2 章から第 4 章までの実際の履歴です。

ad693ac1 feat(dotnet): 第 2 章 型プロバイダによる iris の読み込み・欠損値の補完・シード付きの分割を追加
134b304b docs(getting-start-ml): F# 第 2 章 データの前処理と三角測量を追加し、nav・索引に登録
53801aff feat(dotnet): 第 3 章 判別共用体による決定木と ML.NET の FastTree との突き合わせを追加
342b78c3 docs(getting-start-ml): F# 第 3 章 決定木による分類と明白な実装を追加し、B19 の完了を反映
35cf8f8b chore(dotnet): 第 7 章以降で使う FSharp.Stats を追加
251be997 test(dotnet): 第 4 章 System.Random のシード付きの乱数列を固定する学習用テストを追加

35cf8f8b は、第 7 章以降で使うライブラリを先に追加したコミットです。ライブラリの追加は packages.lock.json の大きな差分を伴うので、章の実装とは別のコミットにしています。この章の学習用テストは、自分のコードの機能を増やすものではないので、feat ではなく test にしました。

この章の完成コード(tests/MachineLearning.Tests/Chapter04/RandomSequenceTest.fs)
/// System.Random のシード付きの乱数列を固定する学習用テスト。
/// .NET を上げてこのテストが失敗したら、分割に入る行と記事の数値が変わる。
module MachineLearning.Tests.Chapter04.RandomSequenceTest

open System
open Xunit
open MachineLearning.Chapter02.Random

[<Fact>]
let ``シード 0 の乱数列は .NET 8・9・10 で同じ値になる`` () =
    let random = Random 0

    let values = [ random.Next(); random.Next(); random.Next() ]

    Assert.Equal<int list>([ 1559595546; 1755192844; 1649316166 ], values)

[<Fact>]
let ``シード 0 で 0 から 9 を並べ替えた順は第 2 章で記録した順と同じ`` () =
    Assert.Equal<int list>([ 0; 4; 5; 8; 2; 1; 3; 6; 9; 7 ], shuffle 0 [ 0..9 ])

[<Fact>]
let ``シードを渡さなければ乱数列は Random を作るたびに変わる`` () =
    let draw (random: Random) = List.init 10 (fun _ -> random.Next())

    Assert.NotEqual<int list>(draw (Random()), draw (Random()))

4.8 まとめ

この章では、機械学習のプロジェクトでのバージョン管理を学びました。

  1. Conventional Commits — type と scope で、変更の種類と対象が分かるコミットメッセージを書く。本文には変更の理由を書く
  2. コミットしないものを決める — 再配布できない学習データ、再生成できる bin/・obj/・テストの結果・モデル・Notebook の出力、秘匿すべき認証情報を .gitignore で除外する。global.json・packages.lock.json・.config/dotnet-tools.json はコミットする
  3. 入手手順をコードにする — データを置く場所と確認方法を Gulp タスクにし、プログラムからは dataDir で場所を解決する
  4. データが無くてもテストを通す — 実データのテストは Assert.SkipUnless でスキップする
  5. 再現性 — 乱数のシードに加えて、ライブラリ・SDK・実行環境の版を固定する。シード付きの System.Random の乱数列は .NET 8・9・10 で同じだったが、版をまたいだ再現は約束されていないので、学習用テストで固定した

次の章では、ライブラリと SDK の版を固定する NuGet と global.json の仕組みと、コードの品質を機械的に確かめる整形・静的解析のツールを扱います。