Skip to content

第 1 章: 機械学習とはじめてのテスト

1.1 はじめに

この章では、機械学習とは何かを確認したうえで、テスト駆動開発(TDD)で最初のプログラムをなでしこ3 で作ります。題材は「きのこの山派か、たけのこの里派か」を身長・体重・年代から判定する問題です。

この章ではまだ機械学習のアルゴリズムを使いません。人間が考えたルールで判定し、その正解率を測るところまで進みます。

Python 版の第 1 章 と同じ題材・同じ TODO リストで進めます。なでしこ3 版はシリーズの番外編です。ほかの 14 言語版と違い、プログラムを日本語で書きます。

もし、特徴量["年代"] = キノコ派年代ならば
    「きのこ」で戻る
ここまで

なでしこ3 版では次の 2 つと対比します。1 つは Python 版 で、同じ手順を英語の識別子と日本語の文でそれぞれ書くとどう読めるかです。2 つめは Haskell 版 で、機械学習のライブラリが無い言語という点が同じです。ただしなでしこ3 にはテスティングフレームワークもありません。この章ではまず、テストの道具から作ります(ADR 015)。

1.2 機械学習とは

ルールを書くか、データに学ばせるか

従来のプログラミングでは、人間が判定のルールをコードとして書きます。

●(特徴量を)ルール判定とは
    もし、特徴量["年代"] = キノコ派年代ならば
        「きのこ」で戻る
    ここまで
    「たけのこ」で戻る
ここまで

この書き方では、ルールの良し悪しは人間の観察力に依存します。特徴量が 3 つなら何とかなりますが、20 個・100 個になると人間には手に負えません。

機械学習は、この「ルール」をデータから自動で作ります。人間が与えるのは「入力(特徴量)」と「正解(ラベル)」の組で、ルールそのものはアルゴリズムが決めます。

従来のプログラミング 機械学習
人間が書くもの ルール データと、学習のさせ方
出力 判定結果 ルール(モデル)と、それを使った判定結果
得意なこと 仕様がはっきりしている問題 仕様を言葉にしにくい問題
説明のしやすさ コードを読めば分かる モデルによる(決定木は読める、ニューラルネットは難しい)

機械学習のワークフロー

本シリーズを通して、次の流れを繰り返します。

  1. データを集める・読み込む — この章で CSV を読み込みます
  2. 前処理する — 欠損値を埋め、訓練データとテストデータに分けます(第 2 章)
  3. モデルを学習させる — 決定木・回帰などを自作します(第 3 章以降)
  4. 評価する — 正解率などの指標で測ります(この章で正解率から始めます)
  5. 改善する — 特徴量を作り直し、ハイパーパラメータを調整します(第 9 章以降)

分類と回帰

教師あり学習は、予測するものの型で 2 つに分かれます。

種類 予測するもの 例 本シリーズで扱う章
分類 どのグループに属するか(離散値) きのこ派/たけのこ派、アヤメの種類、生存/死亡 第 1・3・8・10・11 章
回帰 数値(連続値) 映画の興行収入、住宅価格 第 7・9・12 章

この章は分類です。正解ラベルが「きのこ」「たけのこ」の 2 つなので、二値分類にあたります。

1.3 題材とデータ

データの入手と配置

本シリーズの学習データは、書籍『スッキリわかる Python による機械学習入門』(インプレス, 2020)の配布データを使います。配布データは書籍購入者のみ利用できるため、リポジトリには含まれていません。書籍サポートページ から sukkiri-ml-codes.zip を入手し、リポジトリの tmp/ に置いてから、リポジトリのルートで次を実行してください。

npx gulp data:setup
npx gulp data:check

apps/data/sukkiri-ml/ に学習データが配置されます。このディレクトリは .gitignore の対象なので、誤ってコミットされることはありません。すべての言語版が同じデータを参照します。

KvsT.csv

この章で使うのは KvsT.csv です。19 人分のデータが次の 4 列で記録されています。

列 意味 値
身長 身長(cm) 整数
体重 体重(kg) 整数
年代 年代 10, 20, 30, 40 のいずれか
派閥 きのこの山派かたけのこの里派か きのこ または たけのこ

「身長」「体重」「年代」が特徴量、「派閥」が正解ラベルです。列名が日本語であることと、ファイルの先頭に BOM(バイトオーダーマーク)が付いていることに注意してください。なでしこ3 の CSV取得 も BOM を取り除きません。 後で確かめます。

列名が日本語であることは、ほかの言語版では「気をつける点」でした。なでしこ3 では、列名がそのまま変数名や関数名と同じ言葉で書けるので、むしろ読みやすくなります。

1.4 TODO リストの作成

仕様をそのままコードにするには大きすぎるので、まず TODO リストに分解します。

TODO リスト

何をテストすべきだろうか——着手する前に、必要になりそうなテストをリストに書き出しておこう。

— テスト駆動開発

TODO リスト:

  • テストの道具を用意する
  • ルールで派閥を判定する
  • 20 代ならきのこ派と判定する
  • 20 代以外ならたけのこ派と判定する
  • 正解率を計算する
  • CSV を読み込む
  • BOM 付き CSV を列名で読み込む
  • 数値でない値・列の不足・知らない派閥を弾く
  • 特徴量と正解ラベルに分ける
  • 実データで正解率を表示する

ほかの言語版には無い「テストの道具を用意する」が先頭にあります。

1.5 開発環境の準備

プロジェクトの構成

apps/nadesiko3/ に次のように置きます。

apps/nadesiko3/
├── main.nako3              # 章を選んで実行する入口
├── src/
│   ├── chapter01.nako3     # 第 1 章
│   ├── chapters.nako3      # 章の選び方
│   ├── csv.nako3           # CSV を列名つきで読む
│   ├── dataset.nako3       # 学習データの場所
│   └── number_format.nako3 # 小数点以下の桁をそろえて表示する
├── test/
│   ├── helper.nako3        # 自作のテスト補助
│   ├── helper_test.nako3   # テスト補助そのもののテスト
│   ├── chapter01_test.nako3
│   ├── chapters_test.nako3
│   ├── csv_test.nako3
│   ├── dataset_test.nako3
│   └── number_format_test.nako3
└── tools/
    ├── install-gonako.sh   # 処理系を bin/ に入れる
    └── check.sh            # 文法・整形・テストをまとめて検査する

関数名と変数名は日本語で書き、ファイル名だけは英数字にしています。ファイル名は取り込みのパスや CI の設定にも現れるので、ほかの言語版とそろえました。

処理系を入れる——Go 1.26 が要る

なでしこ3 の公式の処理系は TypeScript(Node.js)で書かれています。この版では、Go で書かれた nadesiko3go(コマンド名 gonako)を使います。1 つの実行ファイルで動き、Node.js が要りません。

gonako は go install で入れます。ここで 2 つ、つまずきました。

1 つめは Go の版です。nadesiko3go の go.mod は go 1.26.0 を要求します。一方、このリポジトリの Nix の環境が使う Go は 1.25.5 です。しかも nixpkgs の Go は、足りない版のツールチェーンを取りに行かない設定(GOTOOLCHAIN=local)になっています。

go: github.com/kujirahand/nadesiko3go/cmd/gonako@v0.0.0-20260924163730-f840295acd3d: github.com/kujirahand/nadesiko3go@v0.0.0-20260924163730-f840295acd3d requires go >= 1.26.0 (running go 1.25.5; GOTOOLCHAIN=local)

GOTOOLCHAIN=go1.26.8 を指定すると、Go 1.25.5 が 1.26.8 のツールチェーンを取得して入れてくれます。Nix の環境全体の版を上げると、ほかの 14 言語版の環境まで一緒に変わってしまうので、この版の導入のときだけ切り替えます。

2 つめは 版の書き方です。nadesiko3go のタグは 3.8.8 のように v が付いていません。Go のモジュールの版は v3.8.8 の形でなければ読めないので、@3.8.8 と書くとタグの指すコミットに解決され、次の「疑似版」になります。

v0.0.0-20260924163730-f840295acd3d

タグは付け替えられることがありますが、疑似版はコミットのハッシュを含むので変わりません。版の固定には疑似版を書きます。

# apps/nadesiko3/tools/install-gonako.sh(抜粋)
readonly GONAKO_VERSION="v0.0.0-20260924163730-f840295acd3d" # タグ 3.8.8
readonly GO_TOOLCHAIN="go1.26.8"

GOTOOLCHAIN="${GO_TOOLCHAIN}" GOBIN="$(pwd)/bin" \
  go install "github.com/kujirahand/nadesiko3go/cmd/gonako@${GONAKO_VERSION}"
nix develop .#nadesiko3
cd apps/nadesiko3
./tools/install-gonako.sh
gonako v3.8.8 (linux/amd64)

bin/ は .gitignore の対象です。

1.6 テストの道具を作る

ASSERT等 は最初の失敗で止まる

なでしこ3 にはテスティングフレームワークがありません。あるのは ASSERT等 という命令です。

「開始」を表示
1と2がASSERT等
「ここは出ないはず」を表示
開始
[実行時エラー]assert.nako3(2行目): ASSERT失敗: 『1』と『2』が一致しません。

終了コードは 1 になるので、CI で失敗には気づけます。ただし最初の失敗でプログラムが止まるので、1 つのファイルに書いたほかのテストが通るのか落ちるのかが分かりません。TDD では「いくつ落ちているか」を見ながら進めたいので、これでは足りません。

数えて、最後に報告する

そこで、失敗しても止まらずに数え、最後にまとめて報告するテスト補助を作りました。xUnit 系のフレームワークが最初にやったことを、小さく自分でやる形です。

検査件数 = 0
失敗件数 = 0
保留件数 = 0

# 実際の値と期待する値を比べる。配列と辞書も中身で比べるため、JSON にしてから比べる。
●(名前で実際と期待が)一致確認とは
    検査件数 = 検査件数 + 1
    実際値 = 実際をJSONエンコード
    期待値 = 期待をJSONエンコード
    もし、実際値 = 期待値ならば
        戻る
    ここまで
    失敗件数 = 失敗件数 + 1
    「失敗: {名前}」を表示
    「  期待: {期待値}」を表示
    「  実際: {実際値}」を表示
ここまで

# 検査の件数を表示し、失敗があればエラーを起こす。テストファイルの最後に呼ぶ。
●検査結果報告とは
    「検査 {検査件数} 件、失敗 {失敗件数} 件、保留 {保留件数} 件」を表示
    もし、失敗件数 > 0ならば
        「{失敗件数} 件の検査が失敗しました」のエラー発生
    ここまで
ここまで

関数の引数は助詞で受け取ります。 ●(名前で実際と期待が)一致確認とは は「名前で」「実際と」「期待が」の 3 つを受け取る関数で、呼ぶ側は次のように書きます。

「二十代はきのこ派と判定する」で(二十代をルール判定)と「きのこ」が一致確認

テストの名前・実際の値・期待する値が、そのまま 1 つの文になります。ほかの言語版で assertEquals(expected, actual) の引数の順番を間違えやすかったことを思い出すと、助詞が引数の役割を示すのはなでしこ3 の良いところです。

ほかに、エラーが起きることを確かめる エラー確認 と、前提がそろわない検査を外す 保留 を用意しました(全体は apps/nadesiko3/test/helper.nako3)。

道具そのものもテストする

テストの道具を自作した以上、その道具が「失敗を失敗として数える」ことも確かめる必要があります。わざと失敗させて、件数を控えておきます。

「(ここから 2 件はテスト補助の確認のため、わざと失敗させています)」を表示
「(わざと失敗)値が違う」で1と2が一致確認
無処理 = 関数()
    戻る
ここまで
「(わざと失敗)エラーが起きない」で無処理を「だめです」でエラー確認
意図的失敗件数 = 失敗件数
検査件数初期化

「値が違えば失敗として数える」で意図的失敗件数と2が一致確認

ここで 1 つ、なでしこ3 の性質に当たりました。取り込んだファイルの変数は、読めても書き換えられません。 最初はテストの側で 失敗件数 = 0 と書いて数え直そうとしましたが、テスト補助の 失敗件数 は 2 のままで、最後の報告で失敗しました。取り込んだ側で代入すると、別の変数ができるだけです。数え直しはテスト補助の関数(検査件数初期化)に任せました。

1.7 ルールで派閥を判定する

Red: まだ無いものを呼ぶ

最初のテストです。

!「./helper.nako3」を取り込む
!「../src/chapter01.nako3」を取り込む

二十代 = {"身長": 170, "体重": 60, "年代": 20}
「二十代はきのこ派と判定する」で(二十代をルール判定)と「きのこ」が一致確認

検査結果報告

特徴量は辞書で表します。なでしこ3 には構造体やレコードが無く、{"身長": 170, ...} の辞書と配列でデータを表します。

取り込む ./helper.nako3 の ./ は省略できません。 !「helper.nako3」を取り込む と書くと、同じフォルダにあってもパッケージの名前として探され、「見つかりません」になります。

./bin/gonako test/chapter01_test.nako3
[字句解析エラー]test/chapter01_test.nako3(4行目): ファイル『../src/chapter01.nako3』が見つかりません。

Green: 仮実装——と、関数名の落とし穴

仮実装で「きのこ」を返します。最初はこう書きました。

●(特徴量を)ルールで判定とは
    「きのこ」で戻る
ここまで
[文法エラー]src/chapter01.nako3(3行目): 関数『chapter01__ルール』の定義で以下のエラーがありました。
[文法エラー]src/chapter01.nako3(3行目): ローカル変数『判定』の定義エラー

関数名の中の「で」が助詞として読まれ、「ルール」という関数の定義になってしまいました。 なでしこ3 では助詞が引数の区切りなので、関数名にも変数名にも助詞を入れられません。ルール判定 と名詞をつなげる形に直すと通ります。

検査 1 件、失敗 0 件、保留 0 件

同じことはテスト補助でも起きました。実際の文字列 という変数名は「実際」+「の」と読まれます。なでしこ3 の命名は「助詞を含まない名詞のつながり」にする、というのがこの版の約束になりました。

三角測量: 20 代以外も確かめる

五十代 = {"身長": 183, "体重": 74, "年代": 50}
「二十代以外はたけのこ派と判定する」で(五十代をルール判定)と「たけのこ」が一致確認
失敗: 二十代以外はたけのこ派と判定する
  期待: "たけのこ"
  実際: "きのこ"
検査 2 件、失敗 1 件、保留 0 件
[実行時エラー]test/chapter01_test.nako3(53行目): 1 件の検査が失敗しました

テスト補助のおかげで、期待と実際が並んで表示されます。なお、最後の行の「53行目」はテストファイルの行番号ではありません。取り込んだファイルをつなげた全体での行番号なので、目安にはなりません。

ルールを書きます。

# 「20 代ならきのこ派」というルールの年代。
キノコ派年代 = 20

●(特徴量を)ルール判定とは
    もし、特徴量["年代"] = キノコ派年代ならば
        「きのこ」で戻る
    ここまで
    「たけのこ」で戻る
ここまで

ここでも最初は きのこ派の年代 と書き、ひらがなの「の」で「き」「の」「こ派」と割られました。 ひらがなの識別子は助詞と見分けが付かないことがあるので、変数名にはカタカナと漢字を使います。「きのこ」「たけのこ」は文字列の値としてだけ使います。

検査 2 件、失敗 0 件、保留 0 件

1.8 正解率を計算する

正解率のテストです。件数が違う場合と、正解ラベルが無い場合はエラーにします。

「全部当たれば正解率は一になる」で([「きのこ」, 「たけのこ」]と[「きのこ」, 「たけのこ」]の正解率)と1が一致確認
「四件中三件当たれば正解率は零点七五になる」で([「きのこ」, 「きのこ」, 「たけのこ」, 「たけのこ」]と[「きのこ」, 「たけのこ」, 「たけのこ」, 「たけのこ」]の正解率)と0.75が一致確認
件数違い = 関数()
    [「きのこ」]と[「きのこ」, 「たけのこ」]の正解率
ここまで
「件数が違えば正解率を求められない」で件数違いを「予測と正解ラベルの件数が違います: 1 と 2」でエラー確認

エラーを確かめるには、「エラーが起きるはずの処理」を関数に包んで渡します。関数() 〜 ここまで で無名関数を作れて、変数に入れても引数に渡しても呼べます。テスト補助の エラー確認 は、それを エラー監視 〜 エラーならば の中で呼び、エラーメッセージ を比べます。

●(予測と正解ラベルの)正解率とは
    予測件数 = 予測の要素数
    正解件数 = 正解ラベルの要素数
    もし、予測件数 ≠ 正解件数ならば
        「予測と正解ラベルの件数が違います: {予測件数} と {正解件数}」のエラー発生
    ここまで
    もし、正解件数 = 0ならば
        「正解ラベルがありません」のエラー発生
    ここまで
    当たり = 0
    Iを 0から 正解件数 - 1まで繰り返す
        もし、予測[I] = 正解ラベル[I]ならば
            当たり = 当たり + 1
        ここまで
    ここまで
    当たり ÷ 正解件数で戻る
ここまで

失敗は エラー発生 で起こします。Haskell 版 が Either で「失敗を値として返した」のとは反対で、Ruby 版・PHP 版 と同じく例外で表す流儀です。

検査 6 件、失敗 0 件、保留 0 件

1.9 CSV を読み込む

CSV取得 の振る舞いを確かめる

なでしこ3 には CSV取得 という命令があり、CSV の文字列を 2 次元の配列にします。使う前に、振る舞いを確かめました。

入力 結果
先頭に BOM 取り除かれない。先頭の列名に BOM の 1 文字が付いたままになる
空行 空文字列 1 つだけの行([""])として読まれる
170 数値の 170 になる
170・170cm・高い 文字列のまま

BOM を取り除かないのは Go 版・Haskell 版 などと同じです。数字だけのセルを勝手に数値にするのは、ほかの言語版の CSV ライブラリには無い振る舞いです。

列名つきで読む

1 行目を列名にして、各行を「列名から値への辞書」にします。

●(CSV文字列を)列名付き読込とは
    本文 = CSV文字列の(65279のCHR)を「」に単置換
    表 = 本文をCSV取得
    行一覧 = []
    もし、(表の要素数) = 0ならば
        行一覧で戻る
    ここまで
    列名一覧 = 表[0]
    Iを 1から (表の要素数) - 1まで繰り返す
        セル一覧 = 表[I]
        もし、(セル一覧を空行判定)ならば
            続ける
        ここまで
        行 = {}
        Jを 0から (列名一覧の要素数) - 1まで繰り返す
            もし、J < (セル一覧の要素数)ならば
                行[列名一覧[J]] = セル一覧[J]
            ここまで
        ここまで
        行一覧に行を配列追加
    ここまで
    行一覧で戻る
ここまで

BOM は 65279のCHR(U+FEFF)で作ります。記事にもコードにも BOM の実物を書かないためです。

もう 1 つ、もし の条件や繰り返しの範囲に命令を書くときは、かっこで囲みます。 もし、表の要素数 = 0ならば と書くと「不完全な文です」で止まりました。(表の要素数) と囲むと通ります。

数値を読む——「170cm」が 170 になる

数値でない値は弾きたいので、なでしこ3 の数値への変換を確かめました。

式 結果
「170」+0 170
「高い」+0 NaN
「170cm」+0 170

「170cm」+0 が 170 になります。 JavaScript の parseFloat と同じく、先頭の数字だけを読んでしまいます。Haskell 版で readInt の残りを確かめ忘れたときと同じ落とし穴です。なでしこ3 版では、正規表現で形を確かめてから数値にします。

●(行の列名を)整数取得とは
    値 = 行の列名を列取得
    もし、(値の変数型確認) = 「number」ならば
        値で戻る
    ここまで
    照合結果 = 値を「^-?[0-9]+$」で正規表現マッチ
    もし、照合結果 = NULLならば
        「{列名} を数値として読めません: {値}」のエラー発生
    ここまで
    値 + 0で戻る
ここまで

ここでも 2 回つまずきました。1 つめは変数名で、最初は 一致 と書いて「関数『一致』に代入できません」と言われました。一致 は組み込みの命令の名前です。2 つめは比較で、正規表現に合わないときの戻り値は NULL で、空文字列の 空 とは等しくありません。照合結果 = 空 と書いたときは、どんな値も通してしまい、テストが落ちて気づきました。

失敗: 数値として読めない値は弾く
  期待: 身長 を数値として読めません: 高い
  実際: (エラーが起きなかった)

人物を読み込む

CSV の読み込みを使って、人物(身長・体重・年代・派閥の辞書)の配列にします。

派閥一覧 = [「きのこ」, 「たけのこ」]

●(CSV文字列を)人物読込とは
    行一覧 = CSV文字列を列名付き読込
    人物一覧 = []
    Iを 0から (行一覧の要素数) - 1まで繰り返す
        行 = 行一覧[I]
        人物 = {}
        人物["身長"] = 行の「身長」を整数取得
        人物["体重"] = 行の「体重」を整数取得
        人物["年代"] = 行の「年代」を整数取得
        人物["派閥"] = 行の「派閥」を列取得して派閥検査
        人物一覧に人物を配列追加
    ここまで
    人物一覧で戻る
ここまで

●(呼び名を)派閥検査とは
    位置 = 派閥一覧から呼び名を配列検索
    もし、位置 < 0ならば
        「派閥を読めません: {呼び名}」のエラー発生
    ここまで
    呼び名で戻る
ここまで

行の「派閥」を列取得して派閥検査 は、「列を取り出して、それを派閥として確かめる」という手順がそのまま文になっています。なでしこ3 では「〜して」で命令をつなげ、前の結果を次の命令に渡せます。

特徴量と正解ラベルへの分離(特徴量正解分離)も同じ形で書き、2 つの配列を辞書にまとめて返します。

検査 13 件、失敗 0 件、保留 0 件

1.10 実データで正解率を表示する

ファイルを読むのは 1 か所だけ

報告の文字列を作る関数は、CSV の文字列を受け取ります。ファイルを読むのは 第1章実行 の 1 か所だけです。

●(CSV文字列を)判定報告作成とは
    人物一覧 = CSV文字列を人物読込
    分離結果 = 人物一覧を特徴量正解分離
    特徴量一覧 = 分離結果["特徴量"]
    予測一覧 = []
    Iを 0から (特徴量一覧の要素数) - 1まで繰り返す
        予測一覧に(特徴量一覧[I]をルール判定)を配列追加
    ここまで
    割合 = 予測一覧と(分離結果["正解ラベル"])の正解率
    件数 = 人物一覧の要素数
    割合表示 = 割合を4で小数表示
    「データ件数: {件数}{改行}ルールによる判定の正解率: {割合表示}{改行}」で戻る
ここまで

●第1章実行とは
    (「KvsT.csv」のデータパス取得)を開いて判定報告作成で戻る
ここまで

Haskell 版が run(IO)と report(純粋関数)を分けたのと同じ構造です。Haskell では型がこの区別を強制しましたが、なでしこ3 には型の検査がありません。分けるかどうかは書く人の約束です。 分けておけば、判定報告作成 は自作の数行のデータでテストできます。

四人 = 「身長,体重,年代,派閥{改行}170,60,20,きのこ{改行}160,50,30,たけのこ{改行}150,45,20,たけのこ{改行}180,70,40,きのこ{改行}」
「データ件数とルールによる判定の正解率を報告する」で(四人を判定報告作成)と「データ件数: 4{改行}ルールによる判定の正解率: 0.5000{改行}」が一致確認

小数点以下 4 桁で表示する

ほかの言語版は %.4f のような書式指定で 0.7368 と表示しました。なでしこ3 には書式を指定して数値を文字列にする命令がありません。 小数点四捨五入 は桁を丸めますが、0.75 を 0.7500 のように零で埋めはしません。そこで小数点以下の桁をそろえる関数(小数表示)を自作しました。

検査 4 件、失敗 0 件、保留 0 件

(14 ÷ 19)を4で小数表示 が 0.7368、0.75を4で小数表示 が 0.7500、1を4で小数表示 が 1.0000 になることをテストで確かめています。

データが無ければ保留にする

実データは配布物なのでリポジトリに含まれません。CI にも置きません。データが無ければ、テスト補助の 保留 で外します。

もし、(「KvsT.csv」のデータ存在確認)ならば
    「ルールによる判定の正解率がほかの言語版と一致する」で(第1章実行)と「データ件数: 19{改行}ルールによる判定の正解率: 0.7368{改行}」が一致確認
違えば
    「ルールによる判定の正解率がほかの言語版と一致する」を「学習データがありません」で保留
ここまで
保留: ルールによる判定の正解率がほかの言語版と一致する(学習データがありません)
検査 14 件、失敗 0 件、保留 1 件

保留の数が表示されるので、「テストが減っていることに気づかない」事故を避けられます。Haskell 版の 1 pending と同じ役割を、テスト補助に持たせました。

学習データの場所は、ほかの言語版と同じく環境変数 ML_DATA_DIR で差し替えられ、空なら ../data/sukkiri-ml を使います(src/dataset.nako3)。

結果

章は main.nako3 から選んで実行します。

./bin/gonako main.nako3 chapter01

期待する出力は、ほかの 14 言語版と同じ次の 2 行です。19 人のうち 14 人を正しく判定できた、ということです。

データ件数: 19
ルールによる判定の正解率: 0.7368

実データでの実測について

この章は学習データの無い環境で書いたため、上の 2 行はまだなでしこ3 版の実装で実測していません。実データのテストは保留になっています。学習データを配置した環境で npx gulp apps:check:nadesiko3 を実行し、保留が消えて通ることを確かめてから、この注記を外します。自作の数行のデータ(BOM 付き)を ML_DATA_DIR で指した実行では、ファイルの読み込みから表示までが動くことを確かめています。

1.11 リファクタリング

整形

gonako format で整形します。

./bin/gonako format src/chapter01.nako3 -f

関数定義の全角のかっこを半角に、I を 0 から を Iを 0から にそろえ、字下げを 4 文字にします。2 回かけても結果は変わりません。

ただし検査用のモードがありません。 -f を付けなければ整形後の内容を表示するだけで、整形が要るかどうかを終了コードで教えてくれません。そこで、表示された内容と元のファイルの差分を取って判定します。

./bin/gonako format src/chapter01.nako3 | diff -u src/chapter01.nako3 -

文法の検査と、その限界

gonako lint は文法を検査します。関数名を打ち間違えると捕まえてくれます。

[文法エラー]typo.nako3(4行目): 未解決の単語があります: [単語『typo__二培』して]

ただし、1 回に 1 ファイルしか受け付けません。 そして、この章でいちばん大事な落とし穴がありました。かっこの中に書いた未定義の関数名や、値として書いた未定義の名前は、lint も実行も素通りします。

X = (「a」を未定義命令)
「[{X}]」を表示
[undefined]

エラーにならず、undefined という値になって先へ進みます。「まだ代入していない変数」と見なされるためです。実際、判定報告作成 の中の ルール判定 を ルール半定 と打ち間違えてみると、lint は通り、テストだけが落ちました。!厳チェック という指定を先頭に書いても結果は変わりませんでした。nadesiko3go 3.8.8 のソースを見ると、この指定を読んでいるのは構文解析の 1 か所だけで、検査には使われていません。

型の検査も無く、lint も名前の誤りを半分しか捕まえない。残る守りはテストだけです。 このため、main.nako3 の中身も 章選択実行 という関数に移し、章の選び方をテストしています。テストの無いコードには、打ち間違いがそのまま残るからです。

カバレッジ

なでしこ3 にはカバレッジを測る仕組みがありません。 シリーズで初めて、カバレッジを数字で示せない版になります。代わりに、テストの件数と「どの関数をどのテストで確かめたか」を各章に書き、ほかの言語版の数値との突き合わせを受け入れ基準に含めます。道具立ては第 5 章で改めて扱います。

まとめて検査する

文法・整形・テストをまとめて検査するスクリプト(tools/check.sh)を書きました。リポジトリのルートからは次で走ります。

npx gulp apps:check:nadesiko3
== 文法(gonako lint)
== 整形(gonako format との差分)
== テスト
-- test/chapter01_test.nako3
保留: ルールによる判定の正解率がほかの言語版と一致する(学習データがありません)
検査 14 件、失敗 0 件、保留 1 件
-- test/chapters_test.nako3
検査 3 件、失敗 0 件、保留 0 件
-- test/csv_test.nako3
検査 6 件、失敗 0 件、保留 0 件
-- test/dataset_test.nako3
検査 3 件、失敗 0 件、保留 0 件
-- test/helper_test.nako3
検査 5 件、失敗 0 件、保留 0 件
-- test/number_format_test.nako3
検査 4 件、失敗 0 件、保留 0 件
すべての検査が通りました

(helper_test.nako3 がわざと失敗させた 2 件の表示は省略しています。)

3 つとも、わざと壊して落ちることを確かめました。

検査 壊し方 結果
文法(lint) かっこの外で関数名を打ち間違える 終了コード 1
整形 全角のかっこのまま置く 差分を表示して終了コード 1
テスト 期待と違う値を返す 終了コード 1

1.12 まとめ

この章では、ルールによる判定と正解率を TDD で実装しました。なでしこ3 に固有の論点は次のとおりです。

  1. テスティングフレームワークが無い — ASSERT等 は最初の失敗で止まる。失敗を数えて最後に報告するテスト補助を自作し、その道具そのものもテストした
  2. 助詞が引数の役割を示す — 「名前」で実際と期待が一致確認 のように、引数の順番ではなく助詞で役割が決まる
  3. 識別子に助詞を入れられない — ルールで判定 は「ルール」の定義に、きのこ派の年代 は「き」「の」「こ派」に割られる。名前は助詞を含まない名詞のつながりにし、変数名にはカタカナと漢字を使う
  4. 取り込んだファイルの変数は書き換えられない — 読めるが、代入すると別の変数ができる
  5. CSV取得 は BOM と空行を取り除かず、数字だけのセルを数値にする
  6. 「170cm」+0 が 170 になる — 正規表現で形を確かめてから数値にする。合わなければ NULL が返り、空 とは等しくない
  7. 書式指定の命令が無い — 小数点以下の桁をそろえる関数を自作した
  8. lint は未定義の名前を半分しか捕まえない — かっこの中や値の位置に書いた未定義の名前は undefined になって素通りする。!厳チェック を書いても変わらない。型の検査も無いので、残る守りはテストだけ
  9. 処理系の導入に Go 1.26 が要る — Nix の Go は 1.25.5。GOTOOLCHAIN で切り替え、版は疑似版で固定する

TODO リスト(この章の完了時点):

  • テストの道具を用意する
  • ルールで派閥を判定する
  • 正解率を計算する
  • CSV を読み込む
  • 特徴量と正解ラベルに分ける
  • 実データで正解率を表示する(実データでの実測は未確認。1.10 の注記を参照)

次の章では、アヤメのデータを読み込み、欠損値を補完して、訓練データとテストデータに分けます。なでしこ3 の 乱数 にはシードを渡す口が無いので、java.util.Random と同じ線形合同法を自作します。しかもなでしこ3 の数値は float64 だけで、48 ビットの状態に掛け算をすると 2^53 を超えてしまうため、PHP 版よりさらに細かく掛け算を分ける必要があります。

なでしこ3 版のほかの章は なでしこ3 版のトップ から辿れます。