はじめに
本記事では、Kaggleや機械学習プロジェクトで「今すぐ活用できる」テストコードのレシピを紹介します。型チェック、入出力の検証、再現性確保、データやモデル出力の妥当性確認など、基本的かつ汎用的な手法をまとめました。これらを導入することで、開発効率と品質が大幅に向上します。
なぜ「ソフトウェアテスト」が必要なのか
機械学習の分野で「テスト」と聞くと、モデル評価(ホールドアウトや交差検証)が思い浮かびます。もちろんこれらは重要ですが、日々の開発サイクルでは、モデル・データ・コードを安定的に改善・運用するためのソフトウェアテストが求められます。
以下のような経験はありませんか?
- 前処理を少し変えたらモデル出力が意図せぬ形に変化してしまった
- データのshapeやdtypeが想定外になっていた
- パラメータ(コンフィグ)を更新したが、変更が狙い通りに反映されているか不明
- 乱数シードを固定したはずなのに結果が再現できなくなった
これらは、モデル以外の領域(データ、コード)でも基礎的なテストを行えば未然に防げることが多くあります。
機械学習プロジェクトにおける3つのテスト対象
機械学習開発では、主に以下の3つの観点でテストを考えられます。
1. モデルのテスト
機械学習プロジェクトやKaggleコンペの文脈で「テスト」といえば、真っ先に思い浮かぶのはモデル性能の評価でしょう。ホールドアウト法や交差検証法を用いてモデルの精度を測り、「Trust CV」「CVとLB(リーダーボード)スコアの相関を確認する」などはKaggle界隈でよく耳にするフレーズです。
モデル評価は、単にスコアを算出するだけではありません。エラー分析を通してモデルが苦手とするケースを把握したり、出力値の範囲や分布を確認したり、さらにはLLM(大規模言語モデル)の出力文字列が期待通りの制約を守っているかを検証するなど、多面的なテストが行われます。
この記事では、一般的なモデル評価手法そのものには深く踏み込みませんが、テストコードを用いたモデルテストの実践的な手法を紹介します。「メタモルフィックテスト・ニューロンカバレッジ・最大安全半径」など、機械学習領域で議論される高度なテスト技術も、広義にはモデルテストに含まれます。
2. データのテスト
モデルを支える土台としてデータの品質保証は不可欠です。「データテスト」は、欠損値や外れ値がないか、データ型や分布が期待通りになっているかを自動的に検証するプロセスを指します。EDA(探索的データ解析)で手動で確認していた事柄を、テストコードを活用して継続的かつ再現性のある形で行うことが可能です。
3. コードのテスト
通常「テストコード」と聞くと、機能要件を満たしているかを検証するプロダクトコード用のテストを書くことを思い浮かべるでしょう。本記事では、この「コードのテスト」をメインテーマとして取り上げます。
前処理関数、ハイパーパラメータを管理するコンフィグ、メトリクス計算用のユーティリティ関数など、モデル周辺を支えるすべてのコードが正しく動作しているかを自動的に確認することで、開発効率と信頼性が飛躍的に高まります。
本記事では、これら3つの側面から、実用的なテストコードの書き方とサンプルコードを紹介します。
ディレクトリ構成例
以下は、テストコードを整理する際の参考ディレクトリ構成です。
project/
├─ requirements.txt
├─ data/
│ ├─ processed_data.npy
│ └─ cleaned_data.csv
├─ scripts/
│ ├─ __init__.py
│ └─ generate_data.py
├─ src/
│ ├─ __init__.py
│ ├─ data_processing.py
│ ├─ models.py
│ └─ utils/
│ ├─ __init__.py
│ ├─ config.py
│ └─ metrics.py
└─ tests/
├─ __init__.py
├─ test_data/
│ ├─ __init__.py
│ ├─ test_numpy_data.py
│ └─ test_pandas_data.py
├─ test_models/
│ ├─ __init__.py
│ └─ test_monotonic.py
└─ test_code/
├─ __init__.py
├─ test_convert_data.py
├─ test_config.py
├─ test_random_seed.py
├─ test_standardize.py
└─ test_custom_metric.py
詳細は、下記のGitHubレポジトリをご参照ください:
ソースコードの紹介
テスト用データ生成スクリプト(scripts/generate_data.py
)
# scripts/generate_data.py
import numpy as np
import pandas as pd
import os
os.makedirs("data", exist_ok=True)
# Numpyデータ生成 (1000 x 20)
processed_data = np.random.rand(1000, 20)
np.save("data/processed_data.npy", processed_data)
# Pandasデータ生成
df = pd.DataFrame({
"id": range(1000),
"feature1": np.random.randn(1000),
"feature2": np.random.randn(1000),
"target": np.random.randint(0, 2, size=1000)
})
df.to_csv("data/cleaned_data.csv", index=False)
print("Data generated successfully.")
プロダクトコード例
データ前処理(src/data_processing.py
):
import numpy as np
def convert_data(input_data):
return list(input_data)
def standardize(data):
data = np.array(data, dtype=float)
mean = np.mean(data)
std = np.std(data)
return (data - mean) / (std if std != 0 else 1.0)
モデルクラス(src/models.py
):
class SimpleModel:
def predict(self, x):
return x * 1.0
コンフィグクラス(src/utils/config.py
):
class Config:
@classmethod
def load(cls, path):
c = cls()
c.learning_rate = 0.01
return c
カスタムメトリクス(src/utils/metrics.py
):
def custom_metric(y_true, y_pred):
correct = sum(t == p for t, p in zip(y_true, y_pred))
return correct / len(y_true) if len(y_true) > 0 else 0.0
テストコード例
Numpyデータのテスト(tests/test_data/test_numpy_data.py
):
データshapeが想定通りか、欠損値・NaNが混入していないかをチェックします。
また、CI環境などでデータがない場合はskipifでテストをスキップします。
import os
import numpy as np
import pytest
DATA_PATH = "data/processed_data.npy"
@pytest.mark.skipif(not os.path.exists(DATA_PATH), reason="data file not found")
def test_numpy_data_shape():
data = np.load(DATA_PATH)
assert data.shape == (1000, 20), "入力データのshapeが想定と異なります"
@pytest.mark.skipif(not os.path.exists(DATA_PATH), reason="data file not found")
def test_numpy_data_no_nan():
data = np.load(DATA_PATH)
assert not np.isnan(data).any(), "NaNが含まれています"
Pandasデータのテスト(tests/test_data/test_pandas_data.py
):
列名やデータ型が期待通りか、欠損値がないかなどを検証します。
データがなければスキップすることで、CI上でもテストが滞りません。
import os
import pandas as pd
import pytest
CSV_PATH = "data/cleaned_data.csv"
@pytest.mark.skipif(not os.path.exists(CSV_PATH), reason="data file not found")
def test_pandas_data_columns():
df = pd.read_csv(CSV_PATH)
expected_cols = ["id", "feature1", "feature2", "target"]
assert list(df.columns) == expected_cols, "列名が想定と異なります"
@pytest.mark.skipif(not os.path.exists(CSV_PATH), reason="data file not found")
def test_pandas_no_nulls():
df = pd.read_csv(CSV_PATH)
assert not df.isnull().values.any(), "欠損値が存在します"
モデル単調性テスト(tests/test_models/test_monotonic.py
):
特定のモデルでは、入力のある特徴量が増加した場合、出力が単調増加すべきなどのドメイン知識がある場合があります。そのような特性をテストで保証します。
from src.models import SimpleModel
def test_model_monotonic_increase():
model = SimpleModel()
inputs = list(range(1, 101))
outputs = [model.predict(i) for i in inputs]
assert all(x <= y for x, y in zip(outputs, outputs[1:])), "モデル出力が単調増加になっていません"
型変換テスト(tests/test_code/test_convert_data.py
):
前処理や特徴量エンジニアリング時に、入力型や返り値の型が期待通りかをテストします。
from src.data_processing import convert_data
def test_convert_data_type():
input_data = (1, 2, 3)
output = convert_data(input_data)
assert isinstance(output, list), "返り値がlistではありません"
assert output == [1, 2, 3], "変換結果が期待と異なります"
コンフィグテスト(tests/test_code/test_config.py
):
コンフィグファイルやクラスを使用してハイパーパラメータを管理する場合、値の更新が正しく反映されるかテストで確認します。
from src.utils.config import Config
def test_config_values():
cfg = Config.load("config.yaml")
assert cfg.learning_rate == 0.01, "learning_rateが想定値と違います"
乱数の再現性テスト(tests/test_code/test_random_seed.py
):
乱数シードを固定しているにも関わらず、再現性が損なわれるケースを防ぐため、一定の種で実行した結果が変わらないことをテストします。
import random
def test_random_seed():
random.seed(42)
seq1 = [random.random() for _ in range(10)]
random.seed(42)
seq2 = [random.random() for _ in range(10)]
assert seq1 == seq2, "同じseedでも結果が再現できません"
標準化処理テスト(tests/test_code/test_standardize.py
):
前処理(標準化など)後に、平均や分散が意図通りになっているかをテストします。
import numpy as np
from src.data_processing import standardize
def test_standardize():
data = np.random.rand(1000)
transformed = standardize(data)
assert abs(np.mean(transformed)) < 1e-7, "平均が0に近くありません"
assert abs(np.std(transformed) - 1) < 1e-7, "標準偏差が1になっていません"
カスタムメトリクステスト(tests/test_code/test_custom_metric.py
):
独自メトリクス関数が、想定した入力に対して正しい値を返すことを確認します。
from src.utils.metrics import custom_metric
def test_custom_metric():
y_true = [0, 1, 1, 0]
y_pred = [0, 1, 0, 0]
score = custom_metric(y_true, y_pred)
expected_score = 0.75
assert score == expected_score, f"メトリクス値が想定外です: {score}"
実行手順
-
必要なライブラリをインストールします。
requirements.txt
を用意してあるので、以下のコマンドでインストールできます。pip install -r requirements.txt
-
テスト用データを生成します。
python scripts/generate_data.py
-
テストを実行します。
pytest
データが存在すればデータテストも実行され、ない場合はスキップされます。CI環境でデータ生成を行わない場合でも、該当テストはエラーではなくスキップに留まるため、ワークフローが止まらず柔軟です。
おわりに
ここで紹介したテストコード例はあくまで基礎的なものですが、「ソフトウェアテスト」を機械学習プロジェクトに導入することで、開発者体験が劇的に向上します。より複雑なモデルや特殊なデータ形式、CI/CDパイプラインとの統合など、プロジェクトのニーズに合わせてテスト戦略を発展させてください。
また、本記事で紹介したソースコードはある種の「養殖物」です。実際の「天然物」のソースコード(プロダクトコード、テストコード)は下記をご覧いただければと思います。
これは、筆者が「Kaggle LLM 20 Questions」で金メダルを獲得したソリューションのGitHubレポジトリです。完璧なお手本ではありませんが、時間的制約や不可避な混乱の中で、実際のプロジェクトを進めながら生み出された成果物です。
これからもソフトウェアテストについての理解を深めて、Kaggleや機械学習プロジェクトを通して洗練させていきたいと思います。
参考にしたもの
『AIソフトウェアのテスト――答のない答え合わせ [4つの手法] (AI/Data Science実務選書)』
『【この1冊でよくわかる】 ソフトウェアテストの教科書 [増補改訂 第2版]』
『VTuberサプーが教える! Python 初心者のコード/プロのコード』
第7章 バグがあるかも? テストコードを書こう!
『現場のPython──Webシステム開発から、機械学習・データ分析まで』
第16章 データサイエンスのためのテスト入門 pandasやNumPyのテスト機能を使って快適に実験
『【Pythonプログラミング入門】テストコードの書き方を解説!(pytest) 〜VTuberと学習〜 【初心者向け】』