データサイエンティストが評価駆動手法を使ってみた〜家計簿分類プロジェクトの実践記〜 – Insight Edge Tech Blog

こんにちは、Insight Edgeデータサイエンティストの中野です。
データサイエンスやLLMのプロジェクトを進めていると、こんな課題にぶつかった経験はありませんか?

  • ラベル付きデータが足りず、最初からモデルを作れない
  • 課題感はあるけど、問題定義が曖昧で進め方がぼんやりする
  • 作ったものが「本当に役に立っているのか」評価できない

私も個人で取り組んだ家計簿分析プロジェクトで、まさにこれらの課題に直面しました。そこで試したのが 「評価駆動開発(Evaluation-Driven Development)」 です。
この記事では、家計簿アプリの明細分類を題材に、評価を起点にプロジェクトを進めた流れと学びを紹介します。
なおこの記事で紹介する方法は、OpenAIの評価駆動型システム設計のノートブック*1を参考にしています。

なぜ評価駆動が重要なのか

通常のデータサイエンスは「データ収集 → モデル構築 → 精度向上」という流れで進められます。
しかしこのアプローチには、初期段階で以下の課題があります。

  • 課題1:ラベル付きデータ不足 : プロジェクトの初期は、データが揃っていないことが多いです。特にLLMを活用したプロジェクトでは、例えば口コミデータや動画データなど、大量に貯まっているものの、特にラベルがついていない状態で始まることも少なくありません。

  • 課題2:問題理解の不足 : なんとなく課題感や困っていることはあっても、具体的な問題定義が不十分なまま進むことは多いです。課題感はあるものの定義が曖昧で、進めるうちに「そもそも解くべき問題が違った」となることも。

結果として「何を改善すればいいか」「今の性能が十分なのか」が分からないまま、なんとなく進んでしまいます。
そこで有効なのが評価基準を先に置き、開発を進める評価駆動開発です。
評価基準も最初から完璧である必要はありません。「仮のもの」をまず定めて進めることが大切です。

家計簿分類プロジェクトを評価駆動で進めてみる

評価駆動開発を進めるために、次のステップでプロジェクトを進めます。

  1. 問題定義
  2. データ整備
  3. 家計簿明細を分類する処理の実装
  4. 評価メトリクスの定義と計算
  5. 業務インパクトの定義と計算
  6. 継続的な改善

1. 問題定義

現在、私はあるアプリを使って、クレジットカードやECサイトの購買履歴を記録しています。
以下の画像のような粒度で明細が記録されていますが、とくに確認や分析はしていません。
また、なんとなく外食やコンビニでの買い物が多く、削減したいと考えていますが、とくになんの分析もしていません。

今回の目的は、家計簿の明細にカテゴリラベルをつけて、外食などの比率を可視化し節約につなげることとします。
重要なのは外食などの浪費を可視化して節約の意識を高めることがゴールであって、カテゴリ分類精度を極限まで高めることではありません。
そのため、まずAIでラベル付けさせますが、分類が難しい明細は「不明」として人間にラベルを付けさせることとします。
金額が小さいものは無理にラベル付け誤分類しても影響が小さいので、間違えていても気にしません。

2. データ整備

データはアプリからCSVをダウンロードします。
実装を進めるにあたり以下のようなデータモデルを定義します。
明細データを TransactionRecord 、分類結果 ExpenseClassification として定義します。
分類結果のデータモデルはLLMの構造化出力のスキーマ指定にも使用します。そのため、 titledescription も記載していきます。*2

from enum import Enum
from pydantic import BaseModel, Field

class TransactionRecord(BaseModel):
    """家計簿の取引レコードを表すデータモデル"""
    date: str = Field(description="取引日付 (YYYY-MM-DD)")
    amount: int = Field(description="取引金額(円)")
    description: str = Field(description="取引内容の説明")

class ExpenseCategory(StrEnum):
    """支出のカテゴリ"""
    FIXED = "固定費"
    TRANSPORTATION = "交通費"
    FOOD = "食費"
    EATING_OUT = "外食"
    BOOK = "本"
    HEALTH = "健康美容"
    CONVENIENCE_STORE = "コンビニ"
    HOBBY = "趣味娯楽"
    EQUIPMENT = "日用品"
    UNKNOWN = "不明"

class ExpenseClassification(BaseModel):
    reasoning: str = Field(
        title="思考過程",
        description="支出のカテゴリを決定するための思考過程を記載する。",
    )
    label: ExpenseCategory | None = Field(
        title="支出のカテゴリ",
        description="支出のカテゴリを示す。分類が出来ない場合は不明とすること",
    )

3. 家計簿明細を分類する処理の実装

Geminiを使って、家計簿明細の分類処理を実装します。
先ほど定義した TransactionRecord を入力として、 ExpenseClassification を出力する関数を実装します。

構造化出力にはGeminiの出力制御機能を利用します。
プロンプトはシンプルなものとしています。
最初からプロンプトを工夫しすぎるよりも、まずは動かして評価することを重視しました。

from google import genai
from google.genai import types

def classify_expense(transaction_record: TransactionRecord) -> ExpenseClassification:
    prompt = f"""
    家計簿の明細が与えられた場合、この明細に以下のルールに基づいてラベルを付与してください。

    # ラベル付けのルール
    1. 明細の内容にもとづいて、ラベルをつけること
    2. 出費が必要経費か、無駄な出費であり、改善するべき対象であるか分類すること
    3. ラベル付けの判断が難しいときは、「不明」として人間のエキスパートにレビューを依頼すること

    # 対象の家計簿明細
    {transaction_record}
    """.replace("    ","")
    client = genai.Client()
    response = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            response_mime_type="application/json",
            response_schema=ExpenseClassification,
            thinking_config=types.ThinkingConfig(
                thinking_budget=0
            ),
        ),
    )
    result: ExpenseClassification = response.parsed
    return result

4. 評価メトリクスの定義と計算

先ほど実装した classify_expense 関数の性能を評価します。
最初に、Ground Truthデータセットを用意します。
その後、分類タスクの混同行列を用いて、モデルの性能を確認します。

Ground Truthデータセットの作成

Ground Truthデータセットを作成していきます。
各カテゴリに5件ずつ程度作成しました。
漠然とGround Truthデータを作っていると、人間がラベル付けしやすいものばかりに偏ると感じました。
そのため、今回は意図的に難易度が高い事例も含めるように注意しました。

[
    {
        "TransactionRecord" :{
            "date": "2024-11-30",
            "amount": 7260,
            "description": "丸善日本橋店/NFC",
        },
        "ExpenseClassification": {
            "reasoning": "",
            "label": "",
        }
    },
    {
        "TransactionRecord" :{
            "date": "2024-11-30",
            "amount": 27840,
            "description": "日本橋髙島屋S.C.",
        },
        "ExpenseClassification": {
            "reasoning": "",
            "label": "不明",
        }
    }
]

定義と計算

混同行列を作成して分類精度を確認します。

これを見ると、そこそこ適切に分類されています。
一方で、本来「健康美容」カテゴリの明細がいくつか他のカテゴリに分類されていることがわかります。

しかし、これらの情報だけで、今回のプロジェクトに対して十分な精度かはわかりません。
誤判定や不明の数も許容できるものか?といったことは、プロジェクトの目的に依存します。

間違えている明細の分析と改善方針

以下の今回間違えた、明細の内、不明としなかった項目を整理します。
金額や明細名は一部加工しています。

No. 明細名 金額 正解ラベル 出力ラベル chain of thoughtの出力
1 ABC[美容院名] [地名]/NFC 6000 健康美容 外食 ABC [地名]は飲食店であり、NFC決済であることから、外食費に該当すると判断しました。支出額も比較的高いため、無駄な出費となりうるか検討すべき対象と考えられます。
2 DEF[美容院名] [地名]/NFC 9000 健康美容 日用品 DEFというブランド名から衣料品費と判断しました。衣料品は必需品ではなく、無駄遣いと判断できるため、改善対象として分類しました。
3 〇〇 東京日比谷 10000 外食 趣味娯楽 〇〇 東京日比谷はホテル名であり、宿泊費やそれに伴うサービス利用料と推測される。これは個人の生活スタイルや目的によるが、一般的には「趣味娯楽」または「不明」に分類される。今回は旅行やレジャー目的と判断し、「趣味娯楽」に分類する。
4 タイムズカー 202402ゲツガクキホンリヨウ 880 交通費 固定費 タイムズカーはカーシェアリングサービスであり、利用頻度に応じて料金が発生する。これは月額基本料と推測され、固定費に分類するのが妥当である。
5 メルスプラン月額費用 5060 健康美容 固定費 メルスプランの月額費用は、定期的に発生する医療関連の費用であり、生活に不可欠な経費と判断されるため、「固定費」と分類します。
6 平安伸銅工業 LABRICO DIY収納パーツ 2×4アジャスター強力タイプ オフホワイト 最大使用荷重40kg EXO-1 販売: アマゾンジャパン合同会社) 1200 日用品 趣味娯楽 2×4アジャスターはDIYの材料であり、生活必需品ではないため、趣味娯楽費に分類し、改善の余地がある支出と判断しました。
7 角利産業(Kakuri Sangyo)プロメイト 糸鋸デラックス 焼入刃付 No.7260 販売: アマゾンジャパン合同会社 887 日用品 趣味娯楽 糸鋸デラックスはDIYや工作に使用される工具であり、趣味娯楽費に該当すると判断しました。
8 魚〇〇 大手町店 1200 外食 食費 「魚〇〇 大手町店」での支払いは、食費に該当すると判断しました。具体的な食料品の内容は不明ですが、一般的に飲食店の利用は食費として計上されます。この支出は、毎日の生活に必要な食費の一部であり、無理な節約をする必要はないと考えられます。

No.1,2は美容院名のため」健康美容にラベル付することが正解なのですが、誤って飲食店や洋服店と判断されてしまっています。
金額のインパクトも大きく、定期的に発生するもののため、改善の優先度が高いと考えられます。
人間なら検索すればすぐわかるもののため、検索ツールをグラウンディングし精度改善を図っても良いと思います。

No.3は、ホテルでの食事ですが、宿泊と判断されてしまいました。
金額が高いため対処したいものの、LLMで判断できる範囲を超えているため、不明とするのが妥当かもしれません。
そもそも1回の支出で1万円を超過するケース自体稀なため、金額が大きい中で確信が持てないものは不明とするロジックを追加しても良いと思います。

No.4,5は、カーシェアリングの月額費を交通費、コンタクトレンズの定期支払を健康美容としていますが、誤って固定費と判断されています。
Ground Truth作成時はそれぞれ交通費・健康美容と判断したものの、よくよく考えたら固定費で良い気がしました。
そのため、この2つはGround Truthを修正します。
このように、LLMの判断を人間の判断に一致させる作業の最中に、人間の正解が変わっていく現象は実プロジェクトでもよく起こります。
Criteria Drift*3と呼ばれたりもします。
対処方法ですが、個人的にはとにかく早く評価サイクルを回して、このような現象があることを把握し、すこしづつ評価基準を育てていくことが正攻法ではないのかと考えています。

No. 6,7,8は金額も小さく、非定期なものであるため、優先度は低くして良いと考えます。

5. 業務インパクトの定義と計算

先ほどは、ひとつひとつ人の目で間違いを分析し改善の方針を決めました。
しかし、今後データ量や制約も増えてくる中で、ひとつひとつ確認するのは非効率です。

そこで、LLMシステムの改善の優先度を決めるため、LLMの評価指標を業務へのインパクトに変換する関数を定義します。

実務の場合はLLMアプリの評価指標を、改善金額や削減時間などのビジネスインパクトに変換できると良いのですが、
今回はそこまで出来ないので、以下のような制約を持つ悪影響度合(negative_impact)を定義します。

  • 制約1: 間違えた金額が大きいほど、分析作業への悪影響度合は大きい
  • 制約2: 不明とラベルを付けた場合よりも、ラベルを付け間違えた方が浪費分析への悪影響度合は大きい(10倍くらい)
def calculate_negtive_impact(evaluation_records: list[EvaluationRecord]) -> float:
    negative_impact = 0.0
    for rec in evaluation_records:
        if rec.correct_classification.label != rec.predicted_classification.label:
            if rec.predicted_classification.label == ExpenseCategory.UNKNOWN:
                negative_impact += rec.transaction_record.amount / 10
            else:
                negative_impact += rec.transaction_record.amount
    return negative_impact

この関数で、分類精度を浪費分析への悪影響度合を計算したところ「69344」でした。
この指標をもとに、効率的にシステムを改善していきます。

6.継続的な改善

先ほどの改善方針を踏まえて、プロンプトを以下のように改善します。

prompt = f"""
家計簿の明細が与えられた場合、この明細に以下のルールに基づいてラベルを付与してください。

# ラベル付けのルール
1. 明細の内容にもとづいて、ラベルをつけること
2. 出費が必要経費か、無駄な出費であり、改善するべき対象であるか分類すること
3. ラベル付けの判断が難しいときは、「不明」として人間のエキスパートにレビューを依頼すること
4. 飲食店名や美容院名などマイナーで固有名詞が不明な場合は「不明」とする
5. ホテル名称などで飲食か宿泊か判断できない場合は「不明」とする
6. 大手コンビニチェーン(セブンイレブン、ファミリーマート、ローソンなど)の場合は「コンビニ」とする
7. 毎月固定の金額(家賃、保険、サブスクなど)の場合は「固定費」とする

# 対象の家計簿明細
{transaction_record}
"""

再度分類した所、混同行列は以下のようになりました。

negative_impactは「19710.6」です。改善できていそうですね。

ただし、なんでもかんでも不明とラベルづけしているため、
本当にこの指標で浪費節約意識を高める作業が効率的になっていくのかは更に運用をしていかないとわかりません。

今後はよりデータを増やしたり、意思決定につなげる中で、評価指標やGround Truthデータ、業務インパクトの定義を育てていくことが必要と思います。

まとめ

この記事では「評価駆動開発」という考え方を、家計簿分類プロジェクトを通じて紹介しました。

LLMプロジェクトだと検索や要約といった機能を多く組み合わせて構築すること、対象となるデータソースが多岐にわたりフォーマットも異なることが多いです。
そのため、とりあえず動かせた後、どこから改善していけばよいかわからないことが多いです。

今回の様に業務インパクトを定義して評価を先に置くことで

  • この工程は精度がわるくても、現時点では問題なさそう
  • このデータソースは精度が悪くても、数も少なく人手による修正作業も容易なので改修の優先度が低い

といった判断ができ、効率的に開発を進められるのではないかと思いました。

参考文献




Source link

関連記事

コメント

この記事へのコメントはありません。