🏞️

テキスト分類モデルを活用した、勘定科目予測

1. はじめに

経理業務では、新しい取引先や商品・サービスが日々登場し、従来のルールベース(例:「〇〇会社は交通費」のような固定ルール)では対応しきれないケースが増えています。例えば、「Uber」の経費は交通費?会議費?それとも接待費?このような判断は、利用シーンによって変わり、単純なルールでは分類困難です。こうした課題に対して、過去の分類実績から学習し、文脈を理解して予測する機械学習(テキスト分類モデル)が有効な解決策となります。
この記事では、FastTextというライブラリを使ってテキスト分類モデルを実現します。

2. 予測の概要

予測には、「予測対象データ」と「学習済みモデル」を使います。ルールベースではないので、100点満点の精度はでません。複数の候補を確信度が高い順に提示するのが一般的です。

3. 学習済みモデルを作る

それでは、「学習済みモデル」どのように作るのでしょうか。

  1. 学習対象データを「前処理」で加工して教師データを作る
  2. FastTextのモデル定義とアルゴリズム設定を行う
  3. 学習プロセスを実行して「学習済みモデル」がアウトプットされる

3.1. 教師データを作る

FastTextの教師データの形式へ変換します。
「テキストの結合 → Janomeで分かち書き → FastText形式に変換」という流れで進めます。
「カテゴリー」→「勘定科目」と読み替えてください。

3.1.1. テキスト結合

文脈情報を充実にして、精度を上げます。例えば、「スターバックス」だけでは判断困難でも、「スターバックス コーヒー購入」なら「食費」と判断しやすいですね。

3.1.2. Janomeで分かち書き

知識の再利用性を高められます。
日本語は単語間にスペースがないです。意味単位で単語を抽出します。(例:東京駅から品川駅」→「東京」「駅」「から」「品川」「駅」)
これは、FastTextが意味単位での入力を前提としているのも理由の1つですが、未知の単語から類推できるようにもなります。(例:「○○マート」という新しい店を見たとき、「マート」という部分からコンビニやスーパーを連想する)

3.2. 学習済みモデルを作る

3.2.1. モデル定義(設計書)

学習済みモデルを作るための設定をします。

具体的な設定例
dim=200,          # 単語ベクトルの次元数
wordNgrams=3,     # N-gramの範囲
loss='softmax',   # 出力層の活性化関数

dim=200 → 入力層のベクトル次元数:
次元数を増やすことで豊富な特徴表現が可能になります。

# このパラメータが決定すること
単語埋め込み行列のサイズ = 語彙数 × 200

# 例:語彙数1000の場合
埋め込み行列 = [[0.12, -0.34, ..., 0.89],   # "スターバックス"のベクトル
                [0.45,  0.23, ..., -0.67],   # "コーヒー"のベクトル
                ...                          # 1000単語分
                [-0.08, 0.67, ..., 0.34]]   # 各行が200次元

wordNgrams=3 → 入力層で扱う特徴量の種類:
「東京_駅_新幹線」のような複合概念を学習します。

# N-gramが生成する追加の特徴
テキスト: "東京 駅 新幹線"

N-gram=1: ["東京", "駅", "新幹線"]  # 単語のみ(デフォルト)
N-gram=2: ["東京", "駅", "新幹線", "東京_駅", "駅_新幹線"]
N-gram=3: ["東京", "駅", "新幹線", "東京_駅", "駅_新幹線", "東京_駅_新幹線"]

loss='softmax' → 出力層の設計:
確率的な予測が可能になります。

# softmax損失関数が決定する出力層の構造
出力層 = {
    "活性化関数": softmax,
    "損失計算": cross_entropy_loss,
    "出力": 確率分布(合計1.0}

# 内部計算
logits = W × doc_vector + b  # 線形変換(200次元→8次元(FastTextの固定値))
probabilities = softmax(logits)  # 確率に変換

3.2.2. アルゴリズム(手順書)

学習済みモデルを作るための手順を設定します。

具体的な設定例
lr=0.5,           # 学習率(SGDのステップサイズ)
epoch=100,        # 反復回数

学習率 lr=0.5:

# SGDの重み更新式
新しい重み = 現在の重み - lr × 勾配

# 具体例
現在の重み = 1.0
勾配 = 0.4 (誤差を減らすための方向)
lr = 0.5

新しい重み = 1.0 - 0.5 × 0.4 = 0.8
#                   ↑ 
#            更新量の大きさを制御

エポック数 epoch=100

# 1エポック = 全データを1回学習
データ数: 4101エポック = 410件全てを1回ずつ学習

# 100エポック
= 410件 × 100= 41,000回の重み更新

学習進行のイメージ

  • エポック1-10: 急速に改善(大きな誤差を修正)
  • エポック11-50: 着実に改善(細かい調整)
  • エポック51-100: 微調整(収束に向かう)

学習曲線のイメージ

精度
100% |                    ━━━━━━━━━ (収束)
     |                ━━━╱
     |            ━━━╱    lr=0.5, epoch=100
     |        ━━━╱        (素早く上昇、十分に収束)
     |    ━━━╱
 50% |━━━╱________________lr=0.01, epoch=100
     |                    (ゆっくり上昇、未収束)
     |
  0% |━━━━━━━━━━━━━━━━━━━━
     0        50       100 エポック

lr と epoch の関係性

状況 lr調整 epoch調整 理由
学習が遅い ↑ 増やす - 更新を大きく
精度が不安定 ↓ 減らす - 安定化
まだ改善の余地 - ↑ 増やす より長く学習
過学習の兆候 - ↓ 減らす 早めに停止

4. 予測するまでの流れ

予測は、「予測対象データ」「学習済みモデル」「予測方法」を使って予測します。「予測対象データ」はFastTextで定められているフォーマットに変換したうえで予測します。

実際の予測例
# 入力
company = "○○ECサイト"
description = "プログラミング書籍"

# 処理
text = "○○ECサイト プログラミング書籍"
processed = "○○ECサイト プログラミング 書籍"

# 予測結果
prediction = model.predict(processed, k=3)
# 結果:
# 1. 書籍費 (0.85)
# 2. 備品費 (0.10)
# 3. 教育費 (0.03)

5. 実際のソースコード

それでは、実際のPythonのソースコードをご紹介します。
フォルダ構成は下記です。

/
├── Data/                        # データセット格納ディレクトリ
│   ├── expense_data.csv         # 経費データ(訓練用データ、16KB、411行)
│   └── test_data.csv            # テスト用データ(3.1KB、113行)
│
├── models/                      # 学習済みモデル格納ディレクトリ
│   ├── expense_classifier.bin   # 経費分類器の学習済みモデル(1.5GB)
│   └── training_data.txt        # 訓練データのテキスト形式(21KB、411行)
│
├── src/                         # ソースコード格納ディレクトリ
│   ├── train.py                 # モデル訓練用スクリプト(4.0KB、137行)
│   ├── evaluate.py              # モデル評価用スクリプト(5.1KB、170行)
│   └── predict.py               # 予測実行用スクリプト(4.1KB、139行)
│
├── .git/                        # Gitバージョン管理ディレクトリ
├── .venv/                       # Python仮想環境ディレクトリ
└── requirements.txt             # Python依存パッケージリスト(479B、30行)

5.1. train.py

train.py
train.py
import pandas as pd
import fasttext
from janome.tokenizer import Tokenizer
import os

def preprocess_text(text):
    """
    日本語テキストの前処理
    """
    if pd.isna(text):
        return ""
    
    # 日本語トークナイザーの初期化
    tokenizer = Tokenizer()
    
    # 形態素解析
    tokens = tokenizer.tokenize(text, wakati=True)
    
    # スペースで結合
    return " ".join(tokens)

def create_training_data():
    """
    学習用データの作成
    """
    print("📊 学習用データを読み込み中...")
    
    # CSVファイルの読み込み
    df = pd.read_csv('data/expense_data.csv')
    
    print(f"データ件数: {len(df)}")
    print(f"カテゴリ数: {df['category'].nunique()}")
    print(f"カテゴリ: {df['category'].unique()}")
    
    # テキストの前処理
    print("🔧 テキストの前処理中...")
    df['processed_text'] = df['company'].astype(str) + " " + df['description'].astype(str)
    df['processed_text'] = df['processed_text'].apply(preprocess_text)
    
    # FastText形式のデータ作成
    print("📝 FastText形式のデータを作成中...")
    training_data = []
    
    for _, row in df.iterrows():
        label = "__label__" + row['category']
        text = row['processed_text']
        training_data.append(f"{label} {text}")
    
    # 学習用ファイルの保存
    os.makedirs('models', exist_ok=True)
    
    with open('models/training_data.txt', 'w', encoding='utf-8') as f:
        for line in training_data:
            f.write(line + '\n')
    
    print("✅ 学習用データの準備完了")
    return 'models/training_data.txt'

def train_model(training_file):
    """
    モデルの学習
    """
    print("🤖 モデルの学習を開始...")
    
    # FastTextモデルの学習
    model = fasttext.train_supervised(
        input=training_file,
        lr=0.5,           # 学習率を上げる
        epoch=100,        # エポック数を増やす
        wordNgrams=3,     # N-gramを増やす
        dim=200,          # ベクトル次元を増やす
        minCount=1,       # 最小出現回数を設定
        loss='softmax',   # 損失関数を明示
        verbose=2         # 詳細ログ
    )
    
    # モデルの保存
    model_path = 'models/expense_classifier.bin'
    model.save_model(model_path)
    
    print(f"✅ モデルが保存されました: {model_path}")
    return model, model_path

def evaluate_model(model):
    """
    モデルの簡易評価
    """
    print("📈 モデルの簡易評価中...")
    
    # 学習データでの簡易評価
    result = model.test('models/training_data.txt')
    
    print(f"サンプル数: {result[0]}")
    print(f"精度: {result[1]:.4f}")
    print(f"再現率: {result[2]:.4f}")
    
    # 予測例の表示
    print("\n🔍 予測例:")
    test_examples = [
        "スターバックス コーヒー",
        "Amazon 書籍",
        "JR東日本 電車",
        "ホテル 宿泊"
    ]
    
    for text in test_examples:
        processed = preprocess_text(text)
        prediction = model.predict(processed, k=3)
        
        print(f"入力: {text}")
        for i, (label, prob) in enumerate(zip(prediction[0], prediction[1])):
            clean_label = label.replace('__label__', '')
            print(f"  予測{i+1}: {clean_label} (確信度: {prob:.4f})")
        print()

def main():
    """
    メイン処理
    """
    print("🎯 経費分類システムの学習開始")
    print("=" * 50)
    
    # 学習データの準備
    training_file = create_training_data()
    
    # モデルの学習
    model, model_path = train_model(training_file)
    
    # モデルの簡易評価
    evaluate_model(model)
    
    print("=" * 50)
    print("🎉 学習完了!")
    print(f"モデルファイル: {model_path}")

if __name__ == "__main__":
    main()

このプログラムは、経費データから勘定科目を自動分類するモデルを学習します。主要な処理は4つの関数で構成されています。
「preprocess_text()」は、日本語テキストをJanomeで形態素解析し、単語単位に分割します。これにより「スターバックスコーヒー」が「スターバックス コーヒー」のように分かち書きされ、FastTextが処理可能な形式になります。
「create_training_data()」では、CSVファイルから会社名・説明・カテゴリを読み込み、FastText形式(__label__食費 スターバックス コーヒー)に変換します。会社名と説明を結合することで、より豊富な文脈情報を学習に活用できます。
「train_model()」が学習の中核で、lr=0.5(高い学習率)とepoch=100(十分な反復回数)により高速かつ高精度な学習を実現。dim=200で単語を200次元ベクトルとして表現し、wordNgrams=3で「東京_駅_新幹線」のような単語の組み合わせも学習します。
「evaluate_model()」では、学習したモデルの精度を確認し、実際の予測例を表示します。これにより、モデルが正しく動作していることを即座に確認できます。
プログラムは絵文字を使った進捗表示により、処理状況を視覚的に把握できる工夫もされています。最終的にexpense_classifier.binという学習済みモデルファイルが生成され、予測プログラムで利用可能になります。

5.2. predict.py

predict.py
predict.py
import pandas as pd
import fasttext
from janome.tokenizer import Tokenizer
import sys

def preprocess_text(text):
    """
    日本語テキストの前処理
    """
    if pd.isna(text):
        return ""
    
    tokenizer = Tokenizer()
    tokens = tokenizer.tokenize(text, wakati=True)
    return " ".join(tokens)

def load_model():
    """
    学習済みモデルの読み込み
    """
    model_path = 'models/expense_classifier.bin'
    
    try:
        model = fasttext.load_model(model_path)
        print(f"✅ モデルを読み込みました: {model_path}")
        return model
    except FileNotFoundError:
        print(f"❌ モデルファイルが見つかりません: {model_path}")
        print("先に学習を実行してください: python src/train.py")
        sys.exit(1)

def predict_single(model, company, description):
    """
    単一データの予測
    """
    # テキストの結合と前処理
    text = f"{company} {description}"
    processed_text = preprocess_text(text)
    
    # 予測実行
    prediction = model.predict(processed_text, k=3)
    
    # 結果の整形
    results = []
    for label, prob in zip(prediction[0], prediction[1]):
        clean_label = label.replace('__label__', '')
        results.append({
            'category': clean_label,
            'confidence': prob
        })
    
    return results

def predict_batch(model, csv_file):
    """
    バッチ予測
    """
    print(f"📊 バッチ予測を開始: {csv_file}")
    
    # CSVファイルの読み込み
    df = pd.read_csv(csv_file)
    
    # 予測結果の列を追加
    df['predicted_category'] = ''
    df['confidence'] = 0.0
    
    # 各行に対して予測
    for idx, row in df.iterrows():
        results = predict_single(model, row['company'], row['description'])
        df.loc[idx, 'predicted_category'] = results[0]['category']
        df.loc[idx, 'confidence'] = results[0]['confidence']
    
    # 結果の保存
    output_file = 'predictions.csv'
    df.to_csv(output_file, index=False, encoding='utf-8-sig')
    
    print(f"✅ 予測結果を保存しました: {output_file}")
    return df

def interactive_mode(model):
    """
    対話モード
    """
    print("\n🎮 対話モードを開始")
    print("会社名と説明を入力してください(終了: 'quit')")
    print("-" * 40)
    
    while True:
        try:
            company = input("会社名: ").strip()
            if company.lower() == 'quit':
                break
            
            description = input("説明: ").strip()
            if description.lower() == 'quit':
                break
            
            # 予測実行
            results = predict_single(model, company, description)
            
            print(f"\n📊 予測結果:")
            for i, result in enumerate(results, 1):
                print(f"  {i}. {result['category']} (確信度: {result['confidence']:.4f})")
            print("-" * 40)
            
        except KeyboardInterrupt:
            print("\n👋 対話モードを終了します")
            break
        except Exception as e:
            print(f"❌ エラーが発生しました: {e}")

def main():
    """
    メイン処理
    """
    print("🔮 経費分類システム - 予測モード")
    print("=" * 50)
    
    # モデルの読み込み
    model = load_model()
    
    # 引数の確認
    if len(sys.argv) > 1:
        if sys.argv[1] == 'batch':
            # バッチ予測
            predict_batch(model, 'data/test_data.csv')
        elif sys.argv[1] == 'interactive':
            # 対話モード
            interactive_mode(model)
        else:
            print("使用方法:")
            print("  python src/predict.py batch      # バッチ予測")
            print("  python src/predict.py interactive # 対話モード")
    else:
        # デフォルトは対話モード
        interactive_mode(model)

if __name__ == "__main__":
    main()

このプログラムは、train.pyで作成した学習済みモデルを使って、新しい経費データの勘定科目を予測します。3つの動作モードを提供し、様々な利用シーンに対応しています。
「load_model()」で学習済みモデル(expense_classifier.bin)を読み込みます。モデルファイルが存在しない場合は、親切なエラーメッセージを表示して終了します。
「predict_single()」は予測の核となる関数で、会社名と説明を受け取り、前処理後にモデルで予測を実行。k=3により上位3つの候補を確信度付きで返します。例えば「書籍費(0.85)、備品費(0.10)、教育費(0.03)」のような結果が得られます。
対話モード(デフォルト)では、ユーザーが会社名と説明を入力すると、リアルタイムで予測結果を表示。開発時のテストや少量データの確認に便利です。
バッチモード(python predict.py batch)では、CSVファイル内の全データを一括予測し、結果をpredictions.csvとして保存。UTF-8 BOM付きで出力するため、Excelでも文字化けせずに開けます。
プログラムは柔軟な利用方法を提供し、単発の確認から大量処理まで幅広いニーズに対応。確信度付きの予測により、結果の信頼性も判断できる実用的な設計になっています。エラーハンドリングも適切に実装され、安定した動作を実現しています。

5.3. evaluate.py

evaluate.py
evaluate.py
import pandas as pd
import fasttext
from janome.tokenizer import Tokenizer
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns

def preprocess_text(text):
    """
    日本語テキストの前処理
    """
    if pd.isna(text):
        return ""
    
    tokenizer = Tokenizer()
    tokens = tokenizer.tokenize(text, wakati=True)
    return " ".join(tokens)

def load_model():
    """
    学習済みモデルの読み込み
    """
    model_path = 'models/expense_classifier.bin'
    model = fasttext.load_model(model_path)
    return model

def evaluate_model():
    """
    モデルの詳細評価
    """
    print("📈 モデルの詳細評価を開始")
    print("=" * 50)
    
    # モデルとデータの読み込み
    model = load_model()
    df = pd.read_csv('data/expense_data.csv')
    
    # 予測の実行
    y_true = []
    y_pred = []
    
    for _, row in df.iterrows():
        text = f"{row['company']} {row['description']}"
        processed_text = preprocess_text(text)
        
        # 予測
        prediction = model.predict(processed_text, k=1)
        predicted_category = prediction[0][0].replace('__label__', '')
        
        y_true.append(row['category'])
        y_pred.append(predicted_category)
    
    # 分類レポートの生成
    print("📊 分類レポート:")
    print(classification_report(y_true, y_pred, zero_division=0))
    
    # 混同行列の生成
    cm = confusion_matrix(y_true, y_pred)
    categories = sorted(df['category'].unique())
    
    print("\n🔍 混同行列:")
    cm_df = pd.DataFrame(cm, index=categories, columns=categories)
    print(cm_df)
    
    # 精度の計算
    accuracy = np.mean(np.array(y_true) == np.array(y_pred))
    print(f"\n✅ 全体精度: {accuracy:.4f}")
    
    # カテゴリ別の精度
    print("\n📋 カテゴリ別精度:")
    for category in categories:
        mask = np.array(y_true) == category
        if mask.sum() > 0:
            category_accuracy = np.mean(np.array(y_pred)[mask] == category)
            print(f"  {category}: {category_accuracy:.4f}")
    
    return y_true, y_pred, categories

def show_prediction_examples():
    """
    予測例の表示
    """
    print("\n🔍 予測例:")
    print("=" * 50)
    
    model = load_model()
    
    # テスト例
    test_examples = [
        ("マクドナルド", "ハンバーガー"),
        ("Amazon", "プログラミング本"),
        ("JR東日本", "新幹線"),
        ("東横イン", "出張宿泊"),
        ("スターバックス", "会議コーヒー"),
        ("ヨドバシカメラ", "マウス"),
        ("NTTドコモ", "携帯電話"),
        ("病院", "健康診断")
    ]
    
    for company, description in test_examples:
        text = f"{company} {description}"
        processed_text = preprocess_text(text)
        prediction = model.predict(processed_text, k=3)
        
        print(f"入力: {company} - {description}")
        for i, (label, prob) in enumerate(zip(prediction[0], prediction[1])):
            clean_label = label.replace('__label__', '')
            print(f"  予測{i+1}: {clean_label} (確信度: {prob:.4f})")
        print()

def analyze_errors():
    """
    エラー分析
    """
    print("\n🔍 エラー分析:")
    print("=" * 50)
    
    model = load_model()
    df = pd.read_csv('data/expense_data.csv')
    
    errors = []
    
    for _, row in df.iterrows():
        text = f"{row['company']} {row['description']}"
        processed_text = preprocess_text(text)
        
        # 予測
        prediction = model.predict(processed_text, k=1)
        predicted_category = prediction[0][0].replace('__label__', '')
        
        if predicted_category != row['category']:
            errors.append({
                'company': row['company'],
                'description': row['description'],
                'true_category': row['category'],
                'predicted_category': predicted_category,
                'confidence': prediction[1][0]
            })
    
    if errors:
        print(f"❌ 分類エラー数: {len(errors)}")
        print("\n間違いやすい例:")
        for error in errors[:10]:  # 最初の10件を表示
            print(f"  {error['company']} - {error['description']}")
            print(f"    正解: {error['true_category']} → 予測: {error['predicted_category']}")
            print(f"    確信度: {error['confidence']:.4f}")
            print()
    else:
        print("✅ 分類エラーはありません")

def main():
    """
    メイン処理
    """
    print("📊 経費分類システム - 評価モード")
    
    # モデルの評価
    y_true, y_pred, categories = evaluate_model()
    
    # 予測例の表示
    show_prediction_examples()
    
    # エラー分析
    analyze_errors()
    
    print("\n🎉 評価完了")

if __name__ == "__main__":
    main()

学習済みモデルを評価するPythonとなります。

6. 導入方法

6.1. クイックスタート

下記で、導入できます。

# 1. uvのインストール(初回のみ)
pip install uv

# 2. プロジェクトディレクトリで仮想環境を作成
uv venv

# 3. 仮想環境をアクティベート
# Windows PowerShell
.\.venv\Scripts\Activate.ps1
# Windows Command Prompt  
.\.venv\Scripts\activate.bat
# Mac/Linux
source .venv/bin/activate

# 4. 必要なライブラリをインストール
uv pip install pandas fasttext janome

# 5. 評価スクリプト用の追加ライブラリ(オプション)
# evaluate.pyを実行する場合は必要
uv pip install scikit-learn matplotlib seaborn joblib

# 6. 学習の実行
python src/train.py

6.2. 注意事項

Windows環境の場合の注意事項
fasttextのインストールにはMicrosoft Visual C++ Build Toolsが必要です。

  1. Visual C++ Build Toolsのインストール(Windows環境で必要)
    • Microsoft C++ Build Toolsをダウンロード
    • インストーラーを実行し、「C++によるデスクトップ開発」を選択してインストール
    • インストール完了後、PowerShellを再起動

Windows環境でfasttextのインストールエラーが発生する場合は、以下の手順を試してください:

# 1. 必要なビルドツールをアップグレード
uv pip install --upgrade pip setuptools wheel build cmake pybind11

# 2. 環境変数を設定(PowerShell)
$env:CL='/Dssize_t=Py_ssize_t'

# 3. fasttextをインストール
uv pip install --no-build-isolation -vvv fasttext==0.9.2

# 4. 他のライブラリをインストール
uv pip install pandas janome

# 5. 評価スクリプト用の追加ライブラリ(オプション)
uv pip install scikit-learn matplotlib seaborn joblib

教師データはAIに作らせると楽にできます。

サンプル
company,description,category
マクドナルド,昼食代,食費
スターバックス,コーヒー代,食費
セブンイレブン,弁当代,食費
ドトールコーヒー,資料作成時の軽食,食費
タリーズコーヒー,朝食代,食費
吉野家,牛丼,食費

7. おわりに

実業務で使えそうなユースケースの機械学習となります!ぜひ、ご参考にしてください!

Accenture Japan (有志)

Discussion