小さなローカルAI予算の管理(Mac M2 16GB)
この記事では、millfolioが効率的にローカルAI推論を処理するためのハイブリッドタグシステムを説明します。決定論的な文字列タグと参照タグでほとんどのトランザクションをカバーし、曖昧な部分にはデバイス上のAIタグのみを使用します。タグはインデックス時に一度計算され保存され、クエリ時に再実行されません。バックフィルはバッチ処理、重複排除、優先度スケジューラを使用してラップトップの過負荷を回避します。パフォーマンスデータは、異なる説明あたり約650ms、実効速度8.5行/秒を示しています。保存前にタグを確認するためのプレビュー機構も含まれています。
前回のブログ記事では、millfolioの分割設計について説明しました。フロンティアモデルが別名スキーマ上で小さなプログラムを書き、ローカルモデルがデバイス上で実際のファイルを読み取ります。その最後に、私はローカルモデルをバッチモードで実行し、優先順位を設定してラップトップを使いやすく保つという、あまり魅力的でない部分について書くことを約束しました。これがその記事です。
問題:モデルがレコード数に応じて拡張できない
数年間の銀行取引明細書やカード明細書は数千のトランザクションになります。ローカルモデルは1件の読み取りは得意です。「これはサブスクリプションですか?」というのは、まさにそれが得意とする対象的な質問です。しかし、ラップトップのGPU上で、ユーザーの質問に対して数千回もその質問に答えることはできません。レコードあたりほんの一瞬であっても、「昨年サブスクリプションにいくら使った?」という質問は数分の推論になり、次の質問では同じ作業を繰り返すことになります。
そこで、設計ルールはこうなりました。モデルの判断は一度計算して保存するものであり、質問時に実行するものではありません。
答え:3種類のタグ
すべてのトランザクションはインデックス時にタグを取得します。タグはプレーンテキストのルールファイル(categories.txt、1行1ルール、ファイルが事実のソース)から取得され、開いて編集できます。3種類はコスト順に並んでいます。
文字列タグ — 安価で一般的なケース。タグは大文字小文字を区別しないサブキーワードのリストで、オプションの除外キーワードでマッチを拒否できます。
phone = verizon, at&t, t-mobile, mint mobile groceries = whole foods, trader joe, safeway, costco
純粋な文字列マッチングで、事実上無料であり、実際の支出履歴のほとんどをカバーします。加盟店の文字列は繰り返しが多いためです。
参照タグ — 他のタグから構築されたタグ。ルールはキーワードの代わりに@タグ名を参照できるため、キーワードリストを繰り返すことなくグループを構成できます。
food = @groceries, @restaurant
これも決定論的で無料です。参照タグは、参照先のタグがすでに行にある場合に発動します。参照の循環は無害です(タグは追加されるだけなので、評価は収束します)。また、参照はAIタグを指すこともでき、3番目のタグも合成可能になります。
AIタグ — キーワードリストで捉えきれない曖昧な部分。ルールははい/いいえの質問で、デバイス上のモデルが判定します。
subscriptions : これは定期的なサブスクリプション料金ですか?
これが唯一推論コストがかかる種類であり、まさにそれがクエリ時ではなく保存タグとして使われる理由です。
クエリ時には、3種類はすべて同じに見えます。生成されたプログラムは.tagsでフィルタリングし、数千レコードに対する答えは文字列比較であり、モデル呼び出しではありません。プライバシーのメリットもあります。フロンティアモデルはタグ名とスコープノートを教えられます(そのためタグでフィルタリングするプログラムを書けます)が、キーワードは絶対に教えられません。実際の加盟店文字列はデバイス上に残ります。
バッチと休憩のタイミング
AIタグは一度計算する必要があり、「一度」とは既存のすべてのレコードに対するバックフィルと、後から到着するレコードを意味します。この作業は単一のデバイス上のワークオーケストレーターを介して実行されます。ディスクにバックアップされたキューで、再起動後も持続し、一度に1つのジョブをドレインします。インデックスとAIタグのバックフィルがエンジンを奪い合うことはなく、両方ともインタラクティブな質問に譲ります。
バックフィルジョブ内部では、分類はバッチ処理され、重複排除されます。
多くの説明が1つのプロンプトでモデルに送られ、モデルは1: はい、2: いいえ…と回答します。1回の推論呼び出しでスライス全体を分類します。
モデルに送られる前に、完全に重複する説明はまとめられます。毎月発生する費用が24回出現する場合、1回分類され、結果が展開されます。実際の明細では、これにより作業の大部分が節約されます(統計ページでは、分類済みレコード数とカバーされたレコード数の実行カウンターを保持しています)。
小さな台帳が各ルールがどこまで実体化されたかを記録し、単調増加する挿入世代をキーとします。そのため、完了したタグは古いレコードに対して再実行されることはなく、新しいレコードはインクリメンタルに処理されます。
スライスの間、バックフィラーは休憩します。休憩時間が優先度設定です。高は100msの呼吸、中は1.2秒、低は5秒です。この1つのノブで、マシンを「さっさと終わらせる」か「仕事中だから一晩かけて」かを決められます。バックグラウンド処理を無期限に一時停止することもできます。
速度はどのくらいか?
以下の統計は、Mac M2 16GB上でmill run https://raw.githubusercontent.com/millfolio/vault/5915bf52f17b9baf2c4fe71695dae03e05a9ee16/privacy-box/eval/local_classifier_eval.mojoを実行して生成されました。(プログラムは集計のみを出力し、加盟店文字列や金額は含みません。また、任意のmillfolioインストールで実行できます。実行中にストリーミングされるライブ進捗行は、millfolio ≥ 0.4.49が必要です。)
生の出力は以下の通りです。
モデル:Qwen/Qwen2.5-3B-Instruct 質問:「各スニペットについて、食料品店またはスーパーマーケットでの購入(食料品の買い物)の場合は「はい」と答え、それ以外(外食、レストラン、コーヒーショップ、および食料品以外の請求)は「いいえ」と答えてください。各答えは正確に「はい」または「いいえ」でなければなりません。「なし」やその他の答えは絶対にしないでください。」 vs キーワードタグ「groceries」を参照として。
データ:2930行のトランザクション、988の異なる説明(3.0倍の重複排除);400を分類(均衡:タグ付き29 + タグなし371)で2228行をカバー
ML(行レベル、モデル vs キーワードルール): 一致率70.6% 適合率39.0% 再現率100.0% F1 56.2% 混同行列:TP 419 FP 654 FN 0 TN 1155 ベース率18.8% (キーワードタグは不完全な真実です。不一致はモデル対ルールであり、認定されたモデルエラーではありません。サンプルは均衡しているため、ベース率と一致率はサンプルを反映しており、完全なデータではありません。) 回答分布:111はい・25いいえ・264なし・0その他
運用: 400の説明を約40回のモデル呼び出し(1回あたり約10のバッチ)で処理、合計260.9秒 1.5 distinct/s・8.5 rows/s 実効(重複排除後の展開後)・1 distinct説明あたり約652ms
(回答分布行についての注:「なし」はバッチプロトコルの該当しない場合の逃げ道であり、モデルはほとんどの行で否定として使用します。これはきれいに解析され、「いいえ」としてカウントされます。)
「groceries」キーワードタグは、厳選されたスーパーマーケットブランドのリストであり、参照ラベルです。Qwen2.5-3B-Instructは同じレコードに対して同等のはい/いいえ質問に答えます。サンプルは意図的に均衡が取れており(タグ付きの各説明にタグなしを追加)、分類はモデル呼び出しごとに10説明をバッチ処理し、繰り返しの費用は実際のタグバックフィルとまったく同じ方法で重複排除されます。2930行のトランザクションが988の異なる説明に圧縮され、モデルが見る前に3倍の節約になります。
結果、2228行をカバーする400の異なる説明において、キーワードルールに対する再現率100%、適合率39%(F1 56%)。再現率の数値は、モデルがキーワードリストが食料品としてタグ付けした419行すべてを、リストを見ることなく捕捉したことを示しています。適合率の数値は見た目以上に興味深いです。654の「偽陽性」は、モデルが食料品と判定し、私の十数ブランドのキーワードリストが何も言わない行であり、真のモデルエラーと私のリストが単に知らない食料品店が混ざっています。この非対称性がハイブリッド設計の全体的な論拠です。決定論的ルールは正確で監査可能な先頭部分を提供し、モデルは尾部のカバレッジを提供します。プレビューUIが存在するのは、どちらも盲目的に信頼すべきではないからです。
運用面では、異なる説明あたり約650ms、1.5 distinct/s、重複排除後に各判定が展開されて実効8.5 rows/sとなります。つまり、2900行のデータは数分のバックグラウンド時間で完全なAIパスを取得でき、これはまさにバックフィルスケジューラが設計された予算です。
信頼するが、プレビューする
タグシステムはルールへの信頼があってこそです。そのため、UIは決してユーザーに盲目的に保存させません。タグを作成または編集するとき、永続化する前にドライランが実行されます。
文字列タグまたは参照タグの場合、編集されたルールが保存されたトランザクションに対して評価され、マッチ数とマッチした説明の例、およびマッチしなかった例が表示されるため、欲張りすぎるキーワードはすぐに明らかになります。
AIタグの場合、質問は時間制限付きサンプル(デバイス上のモデルで約5秒)に対して実行され、「約Nレコードがマッチする」という結果と、肯定例と否定例が並べて表示されます。
両方の側面を見ることが重要です。偽陽性はマッチしたリストから明らかですが、偽陰性はUIがルールの外側にあるものも表示して初めて現れます。保存するまでは何も書き込まれません。コード生成モデルは、タクソノミー管理においてあなたと協力します。生成されたプログラムがインラインで何かを分類する必要があった場合、それを再利用可能なAIタグとして提案することができ、次回は新しい推論ではなく保存されたタグフィルターになります。
まとめ
これらのどれもエキゾチックではありません。それは、すべてのデータシステムが最終的に成長させるバッチ対オンラインの分割と同じものです。ローカルなひねりは、高価なリソースがクラウドの請求書ではなく、あなたのGPUとフォアグラウンド作業であることです。そのため、スケジューラの仕事はスループットと同じくらい丁寧さです。判断を一度計算し、タグとして保存し、AI予算を曖昧な尾部にのみ費やし、ルールが何をするかを実行する前に常にユーザーに示します。
デモ:demo.millfolio.app
仕組み:millfolio.app
コード:github.com/millfolio
← ブログに戻る