GitHub Actionsで24時間365日、人の介入なしに稼働する自走型の本番プロダクション・パイプラインへ—取引分類システムをコンセプトから実現した方法
前回のガイド では、このシステムをローカルで構築する方法を説明しましたが、ここでは一段進めて、実際に本番環境向けに作り込みます。
これから、次のような強化 Named Entity Recognition(NER)システムを構築し、本番運用までプロダクション化する道のりを案内します:
- ✅ 合成データを自動生成(毎日)
- ✅ ハイブリッドなルールベース+機械学習でMLモデルを学習
- ✅ インタラクティブなレポートをGitHub Pagesへ自動デプロイ
- ✅ インテリジェントなキャッシュ戦略で3倍高速
- ✅ GitHub Actionsの無料枠で月額$0
ライブデモ: https://akanimohod19a.github.io/productionizing_NER/
結果: 1,000件の取引を処理し、モデルを学習し、美しいレポートを公開する—これらをすべて完全に自律的に、5分未満で実行できる本番レベルのMLパイプラインです。
目次
- 解決した課題
- 最初のPOC:最初に着手したこと
- 直面した本番環境の課題
- 解決策1:インテリジェントなキャッシュの実装
- 解決策2:無効な日付バグの修正
- 解決策3:CI/CDにおける動的データ生成
- 解決策4:包括的なテスト戦略
- アーキテクチャの深掘り
- パフォーマンス指標:改善前 vs 改善後
- 学び
- 次にやること
解決した課題
ビジネス上の背景
金融機関は、日々何百万件ものフリーテキストの取引説明を処理しています。それは次のような見た目です:
"walmart grocery shopping"
"cvs pharmacy prescription pickup"
"uber ride to downtown"
"payment to acme corp inv-2024-001"
課題:
- 手作業での分類は規模的に不可能
- ルールベースのシステムでは新しいパターンを取りこぼす
- 従来のMLは継続的な再学習が必要
- モデルの性能が見えない
- レポートは静的で、古くなる
私たちが作ったもの
自己改善型の分類システムで、次のことを行います:
- 現実的なテストデータを自動生成する
- ルールベースとMLによる分類を組み合わせる
- クラスタリングによって新しいカテゴリを発見する
- すべてをMLflowで追跡する
- インタラクティブなレポートをWebに公開する
- GitHub Actionsで完全に自律的に動作させる
しかも、実行コストはゼロです!
最初のPOC:最初に着手したこと
元の実装
私たちのPoC(概念実証)は、3つの主要コンポーネントから成っていました:
1. ルールベースの分類器
# models/keyword_rules.yaml
categories:
Healthcare:
keywords: [pharmacy, doctor, hospital, medical]
weight: 1.5
Groceries:
keywords: [walmart, grocery, supermarket]
weight: 1.0
カバー率: 分類された取引の68.5%を即時に処理。
2. MLによる強化
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_extraction.text import TfidfVectorizer
# 金額に基づく重み付けによる学習
sample_weights = np.log1p(df['amount'].abs())
classifier = RandomForestClassifier(n_estimators=100)
classifier.fit(X, y, sample_weight=sample_weights)
改善: カバー率が+22.7%(合計:91.2%)
3. 教師なしの発見
from sklearn.cluster import DBSCAN
# 未知の取引に含まれるパターンを見つける
clustering = DBSCAN(eps=0.3, min_samples=3, metric='cosine')
labels = clustering.fit_predict(X)
返却形式: {"translated": "翻訳されたHTML"}# 発見: 「Insurance」カテゴリ
# 対象: ["geico auto", "state farm policy", "allstate premium"]
POCの結果
| 指標 | 値 |
|---|---|
| 分類カバレッジ | 91.2% |
| 処理速度 | 0.8ms/トランザクション |
| 金額加重精度 | 96.8% |
POCは機能しました。 しかし手作業で、遅く、本番環境向けではありませんでした。そこで、自律的に動かし、人間の介入を最小限に抑えるように構築する計画を立てました、 ただしそれでも、独自の課題がありました。
私たちが直面した本番環境での課題
課題1: ビルド時間が長い
問題: 当初、各GitHub Actionsの実行に12分以上かかっていました。
├─ Pythonパッケージのインストール: 4m 30s
├─ Rパッケージのインストール: 6m 15s
├─ テストの実行: 1m 20s
├─ レポートの生成: 2m 45s
└─ 合計: 12m 50s
重要だった理由: フィードバックの遅さは、開発の遅さにつながります。
課題2: 不正なタイムスタンプ
問題: 公開されたレポートに、パースの問題によりダッシュボード上で「Invalid Date」が表示されていました。
// ダッシュボードがパースしようとした:
timestamp: "20260313_143522"
// しかしJavaScriptのDate()はこれを想定している:
timestamp: "2026-03-13T14:35:22"
影響: プロフェッショナルなダッシュボードが壊れたように見えました。
課題3: 古いテストデータ
問題: テストは、古いコミット済みのCSVファイルに対して実行されていました。ワークフローはdata genから始まるため、システム全体はそのバージョンのレコードで動作すべきでした。
ただし、これは現実のシナリオでランダムなレコードを使ってテストしていたために起きています。つまり、データソースを参照していなかった、ということです。完全に、です。
# テストは常にこの同じファイルを使用:
tests/fixtures/sample_transactions.csv
# しかし実際のパイプラインは毎日新しいデータを生成していました!
リスク: テストは通るのに、本番で失敗する。
課題4: 目に見える状況がない
問題: テストが失敗したとき、ログを掘り下げる必要がありました。
FAILED tests/test_classifier.py::test_groceries_classification
ValueError: 値が足りません (3を期待していたが2でした)
フラストレーション: 分かりにくいエラーで、明確な解決策がない。
そこで、解決策を調べました。
解決策1: インテリジェントなキャッシュの実装
戦略
実行のたびに変わらないものをすべてキャッシュするために、マルチレイヤーのキャッシュ戦略を導入しました。
レイヤー1: Pythonパッケージのキャッシュ
変更前:
- name: 依存関係をインストール
run: pip install -r requirements.txt
# 時間: 毎回約4分
変更後:
- name: Pythonをセットアップ
uses: actions/setup-python@v5
with:
python-version: '3.9'
cache: 'pip' # ← pipの組み込みキャッシュ
- name: Pythonパッケージをキャッシュ
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-v1-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-v1-
${{ runner.os }}-pip-
仕組み:
- 初回実行: パッケージをダウンロードしてキャッシュ(4分)
- 以降の実行: キャッシュから復元(15秒)
requirements.txtが変更された場合のみ再ダウンロード
結果: 1回の実行につき3.75分節約できました!
レイヤー2: Rパッケージのキャッシュ
Rパッケージは非常に大きく、コンパイルに永遠に近い時間がかかります。
変更前:
- name: Rの依存関係をインストール
run: |
install.packages(c("tidyverse", "plotly", "DT", ...))
# 時間: 約6分
変更後:
- name: Rパッケージをキャッシュ
uses: actions/cache@v4
with:
path: ${{ env.R_LIBS_USER }}
key: ${{ runner.os }}-r-v1-${{ hashFiles('DESCRIPTION') }}
- name: Rの依存関係をインストール
uses: r-lib/actions/setup-r-dependencies@v2
with:
packages: |
any::tidyverse
any::knitr
any::rmarkdown
これが素晴らしい理由:
返却形式: {"translated": "翻訳されたHTML"}-
r-lib/actionsは RStudio によってメンテナンスされています - OS 固有のコンパイルを処理します
- ソースではなくバイナリパッケージをキャッシュします
結果: 5.5 分削減!
Layer 3: Pytest Cache
テストは、再利用可能なフィクスチャとメタデータを生成します。
実装:
- name: Cache pytest
uses: actions/cache@v4
with:
path: .pytest_cache
key: ${{ runner.os }}-pytest-v1-${{ hashFiles('tests/**/*.py') }}
- name: Run tests
run: pytest tests/ -v --cov=src/python
キャッシュされるもの:
- テストの検出結果
- フィクスチャのコンパイル
- カバーデータ構造
結果: 30 秒削減。さらにローカルでのテストが高速化!
Layer 4: MLflow Artifacts
ML の実験では、大量のメタデータが生成されます。
- name: Cache MLflow artifacts
uses: actions/cache@v4
with:
path: mlruns
key: ${{ runner.os }}-mlflow-v1-${{ github.sha }}
restore-keys: |
${{ runner.os }}-mlflow-v1-
キャッシュされる内容:
- モデルパラメータ
- メトリクス履歴
- アーティファクトのメタデータ
効果: MLflow UI の読み込みが高速化し、実験の比較も容易になります。
The Cache Strategy Matrix
| Layer | Size | Build Time | Cache Hit Rate | Time Saved |
|---|---|---|---|---|
| Python packages | 200 MB | 4m 30s | 95% | 4m 15s |
| R packages | 800 MB | 6m 15s | 90% | 5m 30s |
| Pytest cache | 5 MB | 30s | 85% | 25s |
| MLflow artifacts | 50 MB | - | 80% | - |
合計の削減時間: 実行あたり約 10 分!
Cache Invalidation Strategy
キャッシュキーにはセマンティック バージョニングを使用します:
env:
CACHE_VERSION: v1 # すべてのキャッシュを無効化するためにインクリメントします
key: ${{ runner.os }}-pip-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}
いつバージョンを上げるか:
- 主要な依存関係のアップグレード
- OS イメージの変更
- キャッシュの破損が疑われる場合
プロのコツ: 一部のキャッシュヒットにはrestore-keysを使います:
restore-keys: |
${{ runner.os }}-pip-v1-
${{ runner.os }}-pip-
これによりフォールバック階層が提供されます:
- 完全一致を試す(requirements.txt のハッシュ)
- v1 のどれかのキャッシュを試す
- pip のどれかのキャッシュを試す
結果: キャッシュヒット率が 60% から 95% に増加しました!
Solution 2: Fixing the Invalid Date Bug
The Root Cause
ダッシュボードでは JavaScript を使ってタイムスタンプを解析していました:
// 生成していたもの:
{
"timestamp": "20260313_143522"
}
// JavaScript の Date() が期待していたもの:
{
"timestamp": "2026-03-13T14:35:22.000Z"
}
The Investigation
ステップ 1: マニフェストの生成を確認する
# 元の(壊れていた)コード:
timestamp_str = filename.replace('assessment_report_', '').replace('.html', '')
# 結果: "20260313_143522"
reports.append({
'timestamp': timestamp_str # ❌ ISO 形式ではありません!
})
ステップ 2: ブラウザのコンソールでテストする
new Date("20260313_143522")
// Invalid Date
new Date("2026-03-13T14:35:22")
// Wed Mar 13 2026 14:35:22 GMT+0000 (UTC) ✓
The Fix
更新されたマニフェスト生成:
from datetime import datetime
返却形式: {"translated": "翻訳されたHTML"}# ファイル名のタイムスタンプを解析する
timestamp_str = filename.replace(' assessment_report_', '').replace(' .html', '')
try:
# 書式: YYYYMMDD_HHMMSS
dt = datetime.strptime(timestamp_str, '%Y%m%d_%H%M%S')
# ISO 8601 形式に変換する
iso_timestamp = dt.isoformat() # "2026-03-13T14:35:22"
except:
# 解析に失敗した場合は現在時刻にフォールバックする
iso_timestamp = datetime.now().isoformat()
reports.append({
' id': timestamp_str,
' timestamp': iso_timestamp, # ✓ ISO 形式
' url': f' reports/{timestamp_str}/{report_file.name}'
})
結果
Before:
┌──────────┐
│ 無効な │
│ 日付 │
└──────────┘
After:
┌──────────┐
│ 3月13日 │
│ 2026 │
└──────────┘
JavaScript の改善
ダッシュボード上での日時の表示形式も改善しました:
const date = new Date(report.timestamp);
// 表示用の書式
const formattedDate = date.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// "2026年3月13日 14:35"(※環境により表記が変わる可能性があります)
// ステータスカード用の書式
const shortDate = date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
// "3月13日"
重要な学び: API やデータのやり取りでは、タイムスタンプに常に ISO 8601 形式を使ってください!
解決策 3: CI/CD における動的データ生成
静的なテストデータに関する問題
元のワークフローではコミット済みの CSV ファイルを使用していました:
# 旧ワークフロー
- name: モデルを学習する
run: python src/python/train_model.py data/sample_transactions.csv
# ↑ リポジトリからの静的ファイル
問題:
- テストは常に同じデータに対して実行される
- 実際のパイプラインは毎日新しいデータを生成する
- エッジケースをテストする方法がない
- 古いデータは本番データと同じではない
解決策: CI/CD でデータを生成する
データ生成をパイプラインの 最初のステップ にしました:
jobs:
# ジョブ 1: 新しいデータを生成する
generate-data:
runs-on: ubuntu-latest
outputs:
data_file: ${{ steps.generate.outputs.data_file }}
timestamp: ${{ steps.generate.outputs.timestamp }}
返却形式: {"translated": "翻訳されたHTML"}steps:
- name: 合成取引データを生成する
id: generate
run: |
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DATA_SIZE=${{ github.event.inputs.data_size || '1000' }}
DATA_FILE="data/transactions_${TIMESTAMP}.csv"
python scripts/generate_sample_data.py \
--size ${DATA_SIZE} \
--output ${DATA_FILE}
# 次のジョブに渡す
echo "data_file=${DATA_FILE}" >> $GITHUB_OUTPUT
echo "timestamp=${TIMESTAMP}" >> $GITHUB_OUTPUT
アーティファクトでジョブを接続する
ジェネレーターからアップロード:
- name: データのアーティファクトをアップロードする
uses: actions/upload-artifact@v4
with:
name: transaction-data-${{ steps.generate.outputs.timestamp }}
path: |
${{ steps.generate.outputs.data_file }}
data/*_metadata.json
retention-days: 7
トレーニングジョブでダウンロード:
train-model:
needs: [generate-data, test] # データ生成を待つ
steps:
- name: 取引データをダウンロードする
uses: actions/download-artifact@v4
with:
name: transaction-data-${{ needs.generate-data.outputs.timestamp }}
path: data/
- name: NER(固有表現)分類器をトレーニングする
run: |
DATA_FILE="${{ needs.generate-data.outputs.data_file }}"
python src/python/train_model.py ${DATA_FILE}
動的データの利点
1. 実行ごとに新しいデータ
# 毎日異なるデータ
2026-03-13: 1000 取引(現在のパターン)
2026-03-14: 1000 NEW 取引(NEWパターン)
2. 設定可能なサイズ
workflow_dispatch:
inputs:
data_size:
description: '取引の 数'
default: '1000'
次でテストできます:
- スモークテスト用に100
- 通常実行用に1,000
- ストレステスト用に10,000
3. 現実的な分布
# ジェネレーターが現実的な混在を作成する:
{
'食料品': 25%,
'レストラン': 18%,
'交通': 15%,
'医療': 10%,
'不明': 5%,
# ... など
}
4. メタデータの追跡
{
"generated_at": "2026-03-13T14:35:22",
"n_transactions": 1000,
"category_distribution": {...},
"amount_stats": {
"min": 5.50,
"max": 1200.00,
"mean": 87.43
}
}
データジェネレーター
私たちの合成データジェネレーターは、現実的な取引を作成します:
class TransactionGenerator:
def __init__(self, seed=None):
if seed:
np.random.seed(seed)
self.templates = {
'Groceries': {
'merchants': ['walmart', 'costco', 'whole foods'],
'items': ['grocery', 'bread milk eggs', 'produce'],
'amount_range': (30, 250),
'frequency': 0.25
},
# ... 8 categories total
}
def generate_narration(self, category):
merchant = np.random.choice(self.templates[category]['merchants'])
item = np.random.choice(self.templates[category]['items'])
# 異なるパターン
patterns = [
f"{merchant} {item}",
f" {item} を {merchant}で購入
",
f"{item} を {merchant}で
"
]
narration = np.random.choice(patterns)
# ときどき参照番号を追加
if np.random.random() > 0.7:
ref = np.random.randint(1000, 9999)
narration += f" ref#{ref}"
return narration
Example output:
walmart grocery ショッピング ref#4521
cvs 調剤薬局で購入
uber 乗車 ダウンタウン
starbucksでコーヒー
テストへの影響
Before: テストは常に固定データで合格
After: テストが実際のエッジケースを検出
Example bug we caught:
# バグ: 'amount' が常に存在すると仮定していた
def classify(df):
return df['amount'].abs() # ❌ amount が欠けていると失敗する
# 修正: 欠けている amount を扱う
def classify(df):
if 'amount'not in df.columns:
df['amount'] = 0
return df['amount'].abs() # ✓ 動作する
このバグは生成データに amount が欠けている場合にだけ発生しました!
解決策 4: 包括的なテスティング戦略
テストピラミッド
私たちは完全なテスティング戦略を実装しました:
/\
/ \
/E2E \ 3 tests (5%)
/______\
/ \
/Integration\ 7 tests (28%)
/____________\
/ \
/ Unit Tests \ 15 tests (60%)
/__________________\
レイヤー 1: ユニットテスト
個々のコンポーネントを単独でテストする:
# tests/test_classifier.py
class TestKeywordMatching:
def test_healthcare_classification(self, classifier):
"""医療取引の分類をテストする。"""
category, confidence = classifier.keyword_match(
"cvs pharmacy prescription pickup"
)
assert category == "Healthcare"
assert confidence > 0.3
カバレッジ:
- ルールベースの分類 ✓
- ML の特徴量抽出 ✓
- 信頼度スコアリング ✓
- データ生成 ✓
これが重要な理由:
- 高速なフィードバック(< 1 秒)
- 正確な失敗箇所を特定できる
- デバッグが容易
レイヤー 2: 統合テスト
コンポーネントが連携して動くことをテストする:
# tests/test_pipeline.py
def test_full_pipeline(tmp_path):
"""完全なパイプラインの実行をテストする。"""
# データを生成
generator = TransactionGenerator(seed=42)
df = generator.generate_transactions(100)
# 分類する
classifier = AdaptiveNERClassifier()
results = classifier.classify_batch(df)
# 確認する
assert len(results) >= 100
unknown_rate = (results['category'] == 'Unknown').sum() / len(results)
assert unknown_rate < 0.9 # 不明が 90% 未満
私たちがテストしていること:
- データ → 分類器 → 結果 のフロー
- ファイル I/O 操作
- MLflow のトラッキング統合
- レポート生成のエンドツーエンド
レイヤー 3: エンドツーエンドテスト
ユーザーの動きと同じようにワークフロー全体をテストする:
def test_github_actions_simulation():
"""GitHub Actions のワークフロー全体をシミュレートする。"""
# ステップ 1: データを生成
subprocess.run([
'python', 'scripts/generate_sample_data.py',
'--size', '100',
'--output', 'data/test.csv'
])
テストフィクスチャ戦略
共有するテストデータにはpytestフィクスチャを使用します:
# tests/conftest.py
@pytest.fixture
def classifier():
"""再利用可能な分類器インスタンス。"""
return AdaptiveNERClassifier(rules_path="models/keyword_rules.yaml")
@pytest.fixture
def sample_transactions():
"""再利用可能なサンプルデータ。"""
return pd.DataFrame({
'narration': [
'cvs pharmacy prescription',
'walmart grocery shopping',
'uber ride downtown'
],
'amount': [45.00, 125.50, 28.00]
})
メリット:
- コードの重複がない
- テストデータが一貫している
- 全体的に更新しやすい
不安定なテストの修正
問題: テストが間欠的に失敗した
# 不安定なテスト(良くない)
def test_generate_transactions():
df = generator.generate_transactions(100)
assert len(df) == 100 # ❌ 不明なデータのせいで105になることがある
解決策: アサーションを柔軟にする
# 堅牢なテスト(良い)
def test_generate_transactions():
df = generator.generate_transactions(100)
assert 100 <= len(df) <= 110 # ✓ 不明なデータが約5%あることを考慮
テストカバレッジの目標
重要な経路について80%のカバレッジを目指しました:
pytest tests/ --cov=src/python --cov-report=term
Name Stmts Miss Cover
-----------------------------------------------------
src/python/ner_classifier.py 145 12 92%
src/python/train_model.py 89 8 91%
src/python/category_discovery.py 76 15 80%
-----------------------------------------------------
TOTAL 310 35 87%
カバレッジレポートは自動的にCodecovへアップロードされます:
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
結果: READMEに素敵なカバレッジバッジ!
アーキテクチャの詳細分析
完全なパイプラインの流れ
返却形式: {"translated": "翻訳されたHTML"}┌─────────────────────────────────────────────────────────┐
│ GitHub Actions │
│ (トリガー: 毎日 午前2時) │
└────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ジョブ1: データ生成(15秒) │
│ ┌──────────────────────────────────────────┐ │
│ │ Python: generate_sample_data.py │ │
│ │ 出力: transactions_20260313_143522.csv │ │
│ │ メタデータ: カテゴリ分布、統計 │ │
│ └──────────────────────────────────────────┘ │
└────────────┬────────────────────────────────────────────┘
│ アーティファクトのアップロード
▼
┌─────────────────────────────────────────────────────────┐
│ ジョブ2: テストの実行(30秒 - キャッシュ済み) │
│ ┌──────────────────────────────────────────┐ │
│ │ pytest tests/ --cov=src/python │ │
│ │ カバレッジ: 87% │ │
│ │ Codecovへアップロード │ │
│ └──────────────────────────────────────────┘ │
└────────────┬────────────────────────────────────────────┘
│ テスト成功 ✓
▼
┌─────────────────────────────────────────────────────────┐
│ ジョブ3: モデルの学習(2分 - キャッシュ済み) │
│ ┌──────────────────────────────────────────┐ │
│ │ ルールベース分類(68.5%) │ │
│ │ ↓ │ │
│ │ 機械学習による強化(+22.7%) │ │
│ │ ↓ │ │
│ │ カテゴリ発見(新しいクラスタ4件) │ │
│ │ ↓ │ │
│ │ MLflow ロギング(メトリクス、モデル、アーティファクト)│ │
│ └──────────────────────────────────────────┘ │
└────────────┬────────────────────────────────────────────┘
│ アーティファクトのアップロード
▼
┌─────────────────────────────────────────────────────────┐
│ ジョブ4: レポートの生成(90秒 - キャッシュ済み) │
│ ┌──────────────────────────────────────────┐ │
│ │ R Markdown レンダリング │ │
│ │ ├─ classified_transactions.csv を読み込み │ │
│ │ ├─ 統計を計算 │ │
│ │ ├─ インタラクティブなグラフを12個作成 │ │
│ │ ├─ 推奨事項を生成 │ │
│ │ └─ 出力: assessment_report.html │ │
│ └──────────────────────────────────────────┘ │
└────────────┬────────────────────────────────────────────┘
│ アーティファクトのアップロード
▼
┌─────────────────────────────────────────────────────────┐
│ ジョブ5: GitHub Pages へデプロイ(30秒) │
│ ┌──────────────────────────────────────────┐ │
│ │ ダッシュボードの index.html を作成 │ │
│ │ レポートのマニフェスト manifest.json を生成 │ │
│ │ gh-pages ブランチへプッシュ │ │
│ │ ↓ │ │
│ │ 公開先: https://username.github.io/repo/│ │
│ └──────────────────────────────────────────┘ │
└────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ジョブ6: 通知(5秒) │
│ ┌──────────────────────────────────────────┐ │
│ │ 全ジョブのステータスを確認 │ │
│ │ レポートへのリンクをコミットへコメント │ │
│ │ (任意: Slack に通知を送信) │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
総実行時間: 約5分(12分以上から短縮!)
ジョブの依存関係
ジョブは可能な限り 並列で実行されます:
generate-data (15秒)
↓
├──→ test (30秒) ──┐
└──→ train (2分) ──┤
↓ │
generate-report(90秒)
↓ │
deploy-pages (30秒)
↓ │
notify (5秒) ←┘
重要な洞察: テストは学習の準備と並列で実行されます!
データフロー
生成からデプロイまで:
transactions_20260313_143522.csv
↓
[アーティファクトのアップロード]
↓
train_model.py
↓
classified_transactions.csv
metrics.json
ner_classifier.pkl
↓
[アーティファクトのアップロード]
↓
assessment_report.Rmd
↓
assessment_report_20260313_143522.html
↓
[アーティファクトのアップロード]
↓
GitHub Pages(gh-pages ブランチ)
↓
https://username.github.io/repo/
キャッシュ戦略の可視化
初回実行(コールドキャッシュ):
├─ Python パッケージ: 4分30秒 → キャッシュミス → ダウンロード&キャッシュ
├─ R パッケージ: 6分15秒 → キャッシュミス → ダウンロード&キャッシュ
├─ Pytest: 30秒 → キャッシュミス → 実行&キャッシュ
└─ 合計: 12分50秒
2回目以降(ウォームキャッシュ):
├─ Python パッケージ: 15秒 → キャッシュヒット → 復元
├─ R パッケージ: 20秒 → キャッシュヒット → 復元
├─ Pytest: 5秒 → キャッシュヒット → 復元
└─ 合計: 4分45秒
高速化: 2.7倍!
パフォーマンス指標: 前後比較
ビルド時間の比較
| コンポーネント | Before | After | 改善 |
|---|---|---|---|
| Python セットアップ | 4分30秒 | 15秒 | 18倍高速 |
| R セットアップ | 6分15秒 | 20秒 | 18.75倍高速 |
| テスト実行 | 1分20秒 | 30秒 | 2.67倍高速 |
| モデル学習 | 3分0秒 | 2分0秒 | 1.5倍高速 |
| レポート生成 | 2分45秒 | 1分30秒 | 1.83倍高速 |
| 合計 | 12分50秒 | 4分35秒 | 2.8倍高速 |
コスト分析
Before:
12.85分 × 30回/月 = 385.5分/月
GitHub Actions: 月あたり 2,000分 無料枠
利用: 割当の 19.3%
After:
4.58分 × 30回/月 = 137.4分/月
GitHub Actions: 月あたり 2,000分 無料枠
利用: 割当の 6.9%
効果: 無料枠内で 4.3倍多くのワークフローを実行できます!
キャッシュヒット率
本番利用から30日後:
| キャッシュの種類 | ヒット率 | 平均保存時間 |
|---|---|---|
| Pythonパッケージ | 95% | 4m 15s |
| Rパッケージ | 90% | 5m 55s |
| Pytest | 85% | 25s |
| MLflowアーティファクト | 80% | 10s |
キャッシュ全体の有効性: 91% のヒット率
リソース使用量
アーティファクトの保存:
変更前(圧縮なし):
├─ 取引データ: 500 KB × 30 = 15 MB
├─ モデルアーティファクト: 5 MB × 30 = 150 MB
├─ レポート: 8 MB × 30 = 240 MB
└─ 合計: 405 MB/月
変更後(圧縮あり):
├─ 取引データ: 100 KB × 30 = 3 MB (80%削減)
├─ モデルアーティファクト: 2 MB × 30 = 60 MB (60%削減)
├─ レポート: 3 MB × 30 = 90 MB (62%削減)
└─ 合計: 153 MB/月(62%の総削減)
圧縮設定:
- uses: actions/upload-artifact@v4
with:
compression-level: 9 # 最大圧縮
retention-days: 7 # 30から削減
ユーザーエクスペリエンス指標
| 指標 | 変更前 | 変更後 | 改善 |
|---|---|---|---|
| 最初のレポートまでの時間 | 15分 | 5分 | 3倍高速 |
| ダッシュボードの読み込み時間 | 2.5秒 | 0.8秒 | 3.1倍高速 |
| 日付の表示 | "Invalid" | "3月13日" | 修正しました! |
| レポートの鮮度 | 手動 | 自動 | 100%自動化 |
学んだこと
1. 攻めのキャッシュ、しかし無効化は慎重に
教訓: 実行の間で変わらないものはすべてキャッシュする。
ただし: 明確な無効化戦略を用意する。
# 良い例: セマンティックバージョニング
CACHE_VERSION: v1 # 新しいキャッシュが必要になったら更新する
# 良い例: ハッシュベースのキー
key: ${{ hashFiles('requirements.txt') }}
# 悪い例: 時間ベースのキー
key: cache-${{ github.run_number }} # 一度もヒットしない!
犯したミス: 最初はバージョン番号なしでキャッシュしていた。パッケージが更新されたとき、古い依存関係(stale dependencies)を取得してしまった。
修正: CACHE_VERSION 環境変数を追加した。
2. すべてのタイムスタンプにISO 8601を使う
教訓: タイムスタンプには常にISO 8601形式を使う。
# 良い例
datetime.now().isoformat() # "2026-03-13T14:35:22.123456"
# 悪い例
datetime.now().strftime('%Y%m%d_%H%M%S') # "20260313_143522"
理由: ISO 8601 は以下の点で優れています:
- どこでも解釈できる
- 辞書順でソート可能
- タイムゾーンに対応
- JSONと相性が良い
これをしないコスト: 「Invalid Date」のデバッグに何時間もかかる!
3. 本番に近いデータでテストする
教訓: テストデータは静的に固定せず、動的に生成する。
変更前: テストではコミット済みの sample_data.csv を使用していた。
変更後: テストでは実行ごとに新しく生成されたデータを使用する
得られる利点:
- エッジケースを見つけられる
- データジェネレーターを検証できる
- テストデータへの過学習を防げる
見つかった例のバグ:
# 静的データでは通っていた:
assert df['category'].nunique() == 8
# しかし生成データでは失敗(カテゴリは7つしか存在しない)
# 修正:
assert df['category'].nunique() >= 5 # 少なくとも5カテゴリ
4. 可能な場合は並列ジョブにする
教訓: 依存関係がボトルネックを作る。できるだけ並列化する。
変更前:
generate → test → train → report → deploy
(すべて逐次、12分)
変更後:
generate → test ─┐
├────→ train → report → deploy
└────→ [その他のジョブ]
(可能な範囲で並列、5分)
ポイント: needs: を慎重に使う:
test:
needs: [generate-data] # データのためにのみ待つ
train-model:
needs: [generate-data, test] # 両方を待つ
5. 早く失敗し、明確に失敗する
教訓: テストが失敗したら、「なぜ」失敗したのかが一目で分かるようにする。
悪いエラーメッセージ:
AssertionError: assert False
良いエラーメッセージ:
assert category == "Groceries", \
f"Expected 'Groceries', got '{category}'. " \
f"Narration: '{text}', Confidence: {confidence}"
# Output:
# AssertionError: Expected 'Groceries', got 'Unknown'.
# Narration: 'walmart shopping', Confidence: 0.0
これで分かりました:
- 何が失敗したか(カテゴリのアサーション)
- 期待値と実際の値
- 状況(ナレーション文)
- なぜ失敗したか(信頼度が0)
6. キャッシュの有効性を監視する
教訓: 時間の経過とともにキャッシュヒット率を追跡する。
ログを追加しました:
- name: Check cache status
run: |
if [ "${{ steps.cache.outputs.cache-hit }}" == "true" ]; then
echo "✓ Cache hit!"
else
echo "✗ Cache miss - downloading packages"
fi
監視する指標: キャッシュヒット率は >85% を維持する必要があります。
これより低い場合:
- キャッシュキーが細かすぎる可能性があります
- 依存関係が頻繁に変わりすぎている可能性があります
- キャッシュサイズの上限に達している可能性があります
7. アーティファクトの保持期間を最適化する
教訓: 必要なものは残し、不要なものは削除する。
# Before: Everything kept 90 days
retention-days: 90
# After: Tiered retention
- Transaction data: 7 days # Regenerable
- Model artifacts: 30 days # Useful for comparison
- Reports: 90 days # Want history
節約: ストレージコストを62%削減!
8. ドキュメントはコード
教訓: READMEはコードそのものと同じくらい重要です。
投資:
- 包括的なREADMEを書くために2時間
- デプロイガイドに30分
- トラブルシューティング欄に1時間
見返り:
- セットアップに関するサポート質問がゼロ
- コントリビューターが<5分でオンボード可能
- デプロイ関連の問題を90%削減
9. POCから始めて、本番へ反復的に進める
教訓: 最初からすべてを一気に作ろうとしない。
私たちの歩み:
- Week 1: 基本的な分類器(ルールベースのみ)
- Week 2: MLの強化を追加
- Week 3: 手動でのレポーティング
- Week 4: GitHub Actionsの自動化
- Week 5: キャッシュ&最適化を追加
- Week 6: UXを磨く、バグを修正
ポイント: 毎週、価値を追加する。「ビッグバン」リリースはしない。
10. すべてオープンソースにする
教訓: 公開することで品質が向上する。
オープンソースにする前:
- ハードコードされたパス
- ドキュメントなし
- あちこちに雑なハック
オープンソースにした後:
- 設定可能
- よく整備されたドキュメント
- 本番対応コード
結論
やり遂げたこと
PoC(概念実証)から始めて、私たちは 本番レベルのMLパイプライン を構築しました。これは:
✅ インテリジェントなキャッシュで 3倍高速 に動作
✅ GitHub Actionsの無料ティアで 月額$0
✅ 新鮮なデータを自動生成
✅ レポートを自律的にWebへデプロイ
✅ 分類精度91.2% を達成
✅ 指導なしで新しいカテゴリを発見
✅ MLflowで完全なMLOps追跡を提供
✅ 87%のテストカバレッジ
✅ 人手なしで24/7稼働
数字
| 指標 | 値 |
|---|---|
| パイプライン実行時間 | 4分35秒(12分50秒だった) |
| スピードアップ | 2.8倍高速 |
| コスト | 月額$0 |
| テストカバレッジ | 87% |
| 分類精度 | 91.2% |
| キャッシュヒット率 | 95% |
| コード行数 | 約3,500 |
| デプロイまでの時間 | < 5分 |
重要なポイント
- すべてキャッシュする - ヒット率95%で2.8倍の高速化
- ISO 8601を使う - デバッグにかかった時間を数時間節約
- 動的なデータ - 静的テストが見逃したバグを発見
- 早く失敗する - 明確なエラーが時間を節約
- しっかりドキュメントする - READMEはコードと同じくらい重要
技術スタック
言語&フレームワーク:
- Python 3.9 (ML/NLP)
- R 4.3 (統計/レポーティング)
- YAML (設定)
- Markdown (ドキュメント)
ML&データ:
- scikit-learn (分類)
- pandas (データ操作)
- NLTK (テキスト処理)
- MLflow (実験追跡)
DevOps:
- GitHub Actions (CI/CD)
- GitHub Pages (ホスティング)
- Codecov (カバレッジ追跡)
- Docker (将来のデプロイ)
可視化:
- R Markdown (レポート)
- Plotly (インタラクティブなチャート)
- ggplot2 (静的なチャート)
- DT (データテーブル)
リソース
ライブデモ:
- ダッシュボード: https://akanimohod19a.github.io/productionizing_NER/
- GitHub: https://github.com/akanimohod19a/productionizing_NER
ドキュメント:
- README: セットアップの包括的なガイド
- CI/CDガイド: ワークフローのカスタマイズ
- APIドキュメント: 分類器の利用方法
- コントリビューティング: 参加方法
連絡先:
- メール: danielamahtoday@gmail.com
- Twitter: @productionML
- LinkedIn: https://www.linkedin.com/in/daniel-amah-2559a4159/
謝辞
使用技術:
- たくさんのコーヒー ☕
- 多くのデバッグセッション
- すばらしいコミュニティからのフィードバック
- MLOpsへの情熱
特別な感謝:
- 無料のCI/CDを提供してくれたGitHub Actionsチーム
- 素晴らしいツールを提供してくれたMLflowコミュニティ
- 素晴らしいレポーティングをしてくれたR/RStudioチーム
- scikit-learnの貢献者
- フィードバックを寄せてくれたすべての方々
❤️ Python、R、MLflow、GitHub Actions、そしてたくさんの愛を使って制作
最終更新日: 2026年3月







