JEPA4Japan · チュートリアル

計測し、最適化し、届ける

2,343文字 7分で読めます #Python

実際のボトルネックを計測し、テスト済みのAI記事ダイジェストCLIを完成させます。

コース進捗 コース目次 24レッスン中 24件を公開中

願いではなく、ものさしを持つ

「もっと速く」は願いです。動作を固定し、現実の仕事を測り、一つ変え、正しさをもう一度証明します。

  1. 契約 見える動作を固定
  2. 計測 本当に遅い場所を探す
  3. 変更 一つの範囲だけ
  4. 証明 同じ答えと新しい証拠
正しさ → 測る → 一つ変える → テストして、もう一度測る。

契約には入力、出力、順序、エラー、副作用、対応するPython版を書きます。確認できる目標には、仕事量、範囲、実行環境、予算があります。「この環境でローカルの1万件を200ミリ秒以内に並べる」のように書きます。

小さなベンチマークには、ファイル、ネットワーク、起動直後、メモリ圧力、別の環境は含まれません。主張は証拠と同じ大きさに保ちます。

三つの道具は、三つの質問に答える

  1. timeit 一つの仕事は何秒?
  2. cProfile 全体の時間はどこへ?
  3. tracemalloc Pythonの確保はどこで増える?
時計、地図、メモリのレンズは、別の質問に答えます。

timeit.repeat()は、同じ小さな処理を何度か測ります。

from timeit import repeat

data = tuple(range(1_000))
samples = repeat(lambda: sum(data), number=1_000, repeat=5)

print(len(samples))
print(all(sample > 0 for sample in samples))
5
True

値は毎回変わります。全サンプルとPython、OS、入力、準備範囲、number、repeatを残し、同じ条件で比べます。

遅い場所が分からないときは、完成した処理をプロファイルします。

python3 -m cProfile -s cumulative digest_evidence.py

tottimeは呼び出した先を含まず、cumtimeは含みます。計測にも負荷があるため、費用の場所を探す道具として使います。

tracemallocは、Pythonが確保したメモリを追います。

import tracemalloc

tracemalloc.start()
numbers = [number * 2 for number in range(10_000)]
current, peak = tracemalloc.get_traced_memory()

print(len(numbers))
print(peak >= current)
tracemalloc.stop()
10000
True

これは追跡できたPythonのメモリです。OSが見るプロセス全体のメモリと同じではありません。

Big-Oは成長の地図で、時計ではない

計算量は、仕事がどう増えるかを予測します。計測値は、一つの環境で選んだ点を表します。

from collections import Counter

tokens = ["ai", "agents", "ai", "python"]
keywords = ("ai", "python")

distinct_matches = len(set(tokens) & set(keywords))
occurrence_matches = sum(Counter(tokens)[word] for word in keywords)

print(distinct_matches)
print(occurrence_matches)
2
3

集合は一致した種類数、Counterは出現回数を数えます。3を2へ変えるなら仕様変更です。tokens.count(word)の繰り返しはおよそO(k × t)、一つのCounterと参照は平均O(t + k)です。小さな入力では準備費用が勝るため、現実に近い大きさを測ります。

速そうな案の前に安全柵を置きます。

分かりやすい基準実装を残したまま、候補を試します。

  1. 基準 単純で信頼できる
  2. 候補 速いかもしれない
  3. 端の例 空、重複、同点
  4. 同じ答え? その後で速度比較
正しさのテストを通過してから、速度を比べます。
from collections import Counter


def reference_score(tokens, keywords):
    return sum(tokens.count(word) for word in keywords)


def counter_score(tokens, keywords):
    counts = Counter(tokens)
    return sum(counts[word] for word in keywords)


cases = [
    ([], ("ai",)),
    (["ai", "ai", "python"], ("ai", "python")),
    (["other"], ("ai",)),
]

for tokens, keywords in cases:
    assert reference_score(tokens, keywords) == counter_score(tokens, keywords)

print("すべての例で同じ答え")
すべての例で同じ答え

空の入力、重複、表記の正規化、同点順序、不正入力、代表的な最大量をテストしてから候補を信頼します。

ミニ制作:読書ダイジェストの証拠

digest_evidence.pyとして保存します。同値な採点と全サンプルを確認し、得点、最新日、大小文字をそろえた題名の順で並べます。

from collections import Counter
from dataclasses import dataclass
from datetime import date
import re
from timeit import repeat


TOKEN = re.compile(r"[A-Za-z0-9]+")


@dataclass(frozen=True)
class Article:
    title: str
    summary: str
    published: date


def tokens(article):
    text = f"{article.title} {article.summary}"
    return [match.group(0).casefold() for match in TOKEN.finditer(text)]


def reference_score(article, keywords):
    words = tokens(article)
    return sum(words.count(keyword) for keyword in keywords)


def counter_score(article, keywords):
    counts = Counter(tokens(article))
    return sum(counts[keyword] for keyword in keywords)


def rank(articles, keywords):
    scored = []
    for article in articles:
        score = counter_score(article, keywords)
        if score:
            scored.append((score, article))
    return sorted(
        scored,
        key=lambda pair: (
            -pair[0],
            -pair[1].published.toordinal(),
            pair[1].title.casefold(),
        ),
    )


articles = [
    Article(
        "Reliable AI agents",
        "Agent evaluation makes AI systems safer.",
        date(2026, 8, 10),
    ),
    Article(
        "Python for model evaluation",
        "Python tools compare AI outputs offline.",
        date(2026, 8, 11),
    ),
    Article("Hardware update", "New accelerators arrived.", date(2026, 8, 12)),
]
keywords = ("ai", "python")

for article in articles:
    assert reference_score(article, keywords) == counter_score(article, keywords)

reference_samples = repeat(
    lambda: [reference_score(article, keywords) for article in articles],
    number=1_000,
    repeat=5,
)
counter_samples = repeat(
    lambda: [counter_score(article, keywords) for article in articles],
    number=1_000,
    repeat=5,
)

print("AI reading digest")
for number, (score, article) in enumerate(rank(articles, keywords), start=1):
    print(f"{number}. {article.title} ({score})")
print("Equivalent: True")
print(f"Samples per version: {len(reference_samples)}")
print(f"All samples positive: {all(reference_samples + counter_samples)}")

python3 digest_evidence.pyで実行し、次を確認します。

AI reading digest
1. Python for model evaluation (3)
2. Reliable AI agents (2)
Equivalent: True
Samples per version: 5
All samples positive: True

ASCIIの語だけを拾う規則は、普遍的な言語処理ではなく、この版の契約です。フレーズや日本語へ対応するなら、分かち書き方法を決め、新しい同値性テストが必要です。

テストした箱を届ける

  1. 固定してテスト 動作と計測の証拠
  2. ビルドして確認 wheel、メタデータ、秘密なし
  3. まっさらな場所へ ソース外でインストール
  4. 安全に公開 監視とロールバック
テストしたものと同じバイト列を届け、直前の良い箱を残します。

第20章のパッケージなら、公開前の練習は次のように進められます。

python3 -m unittest discover -s tests -v
python3 -m build
python3 -m zipfile -l dist/your_package-1.0.0-py3-none-any.whl
python3 -m venv .release-venv
.release-venv/bin/python -m pip install --no-deps dist/your_package-1.0.0-py3-none-any.whl
.release-venv/bin/your-command --help

buildは標準ライブラリではない外部フロントエンドなので、意図して用意します。Windowsの実行ファイルは.release-venv\Scripts\にあります。

レビュー済みソースをビルドし、中身、メタデータ、秘密情報を調べます。ソースツリーの外で、そのwheelをテストします。同じ変更不能なバイト列を新しい版として公開します。目標を監視し、直前の成果物、戻し方、担当者を残します。

3つの小さなミッション

  1. 測れる目標を書く。 仕事量、環境、測る範囲、繰り返し数、予算を書き、対象外も列挙します。
  2. 意味の変化を捕まえる。 ['ai', 'ai', 'python']で出現回数と種類数を比べ、3と2の違いを説明します。
  3. 直す前に探す。 digest_evidence.pyをプロファイルし、累積時間の大きな経路を探します。同値性テストで守れる変更を一つ提案します。

コースを終える準備

  • 測定や変更の前に、動作を定義できる
  • 質問に合わせてtimeit、cProfile、tracemallocを選べる
  • 全サンプルと計測条件を残せる
  • 計算量を成長モデルとして使い、時計と混同しない
  • 通常例と端の例で、候補が同じ答えだと証明できる
  • 成果物をビルド、検査、クリーンインストール、スモークテストできる
  • テスト済みの変更不能なバイト列を公開し、戻す道を説明できる
  • 読書ダイジェストの証拠プログラムが期待どおり動く

これでPythonの一周がつながりました。問題をモデル化し、読みやすいコードを書き、境界を検証し、動作をテストし、並行処理を選び、実際の費用を測り、他の人が信頼できる形で届けられます。