Sysco請求書のPack/Size分離を高精度化する方法|Azure AI Document IntelligenceとAI Builder実践ガイド

Syscoの請求書で「Pack」「Size」列が詰まり過ぎて結合される――Azure AI Document Intelligence(旧Form Recognizer)や AI Builder でよく遭遇する悩みです。本記事は、原因の整理から再現性の高い対処フロー、前処理・後処理の実装例、学習設計、運用チェックリストまでを一気通貫でまとめた実践ガイドです。今日から手元のモデルを改善できます。

目次

問題の整理:なぜ「Pack」と「Size」がくっつくのか

Syscoの請求書では、明細行の列間が極端に狭い、あるいはフォントや印字のカーニングが強く、OCR/レイアウト解析が「同じ語群」と誤判断しやすくなります。特に以下の要因が重なると、Pack と Size、あるいはItem Description と Sizeの境界が曖昧になり、1つのトークンやセルとして抽出されがちです。

  • 列間余白の不足:空白(ホワイトスペース)が最小限で、投影プロファイルや連結コンポーネント解析で境界が見えづらい。
  • スキャン品質のばらつき:300dpi未満、傾き(skew)、低コントラスト、JPEG圧縮ノイズ。
  • 多様な書式:10/2.5 lb、4x5 LB、6/10、EAやCT表記など、Pack/Size を示す表記ゆれが豊富。
  • 明細の折り返し:説明文が2行になり、下段先頭のサイズ表記が上段末尾と吸着される。

最初に押さえる結論

本問題は「単一の設定で魔法のように直る」類ではありません。①既成モデルの当て具合を確認 → ②学習データとアノテーションの拡充 → ③画像のレイアウト前処理 → ④抽出後のエンティティ分割(後処理) → ⑤必要に応じてカスタム抽出器の追加という段階的アプローチで解消率を押し上げるのが最短です。

解決策サマリー

解決策具体的な手順・ポイント補足
① Prebuilt Invoice の併用まず Azure の Invoice 既成モデルで評価。明細アイテム抽出が許容精度ならカスタム無しで運用。既成で崩れるケースは②以降で補強。
② 学習データとアノテーション強化最低 15〜20 枚以上へ拡張。店舗・期間・スキャナ差分を混ぜる。
Pack と Size は 1文字でもはみ出さない矩形で厳密分割。
300dpi以上、傾き補正・コントラスト補正。
5枚は学習不足。分布の多様化が鍵。
③ レイアウト前処理(ホワイトスペース拡張)画像化後、Pack-Size の境界列に 5〜10px の白線を描画し物理的に列を離す。学習・推論とも同処理。列分離の再現性が高く、効果が大きい。
④ エンティティ抽出による後処理抽出テキストを正規表現や AI Builder のエンティティ抽出で「数+単位=Size」「整数×数量=Pack」に再分割。前処理で取りきれない誤結合の保険。
⑤ カスタム抽出器の追加「Pack」「Size」を独立フィールドとして定義し、ノイズの少ない領域特徴に限定したモデルを別枠で作成。高精度化の最終段。運用コストは上がる。

詳細ガイド:段階的に精度を底上げする

① Prebuilt Invoice モデルの当たりを確認

  • Azure AI Document Intelligence Studio で Invoice を選び、Sysco請求書の PDF/JPEG を数枚投入してプレビュー。
  • アイテム行の Description/Unit/Quantity/Amount などが妥当かチェック。Pack/Size が1値に連結でも、後段④で切り出せるなら「許容」とする現実解も有効。

② 学習データとアノテーションの強化

「5枚で境界ボックスを丁寧につけたが改善しない」は珍しくありません。根本は分布の多様性不足です。

  • サンプル枚数:まず 20 枚を目標(できれば 40〜50)。店舗や納入先、月度、スキャン条件が異なるものを混ぜる。
  • アノテーション精度:Pack と Size はピクセル単位で独立させる。隣列に 1px でもかからない矩形を徹底。Description の折り返しにも個別矩形。
  • データ分割:学習 70% / 検証 15% / テスト 15%。学習に入れていない用紙で評価し、回帰テストを継続。
  • 前処理一貫性:学習時に施した傾き補正・二値化・コントラストは推論時にも必ず適用。

③ レイアウト前処理(ホワイトスペース拡張)実装例

Pack/Size 境界を物理的に広げるのは最も即効性があります。既存PDFを画像化し、列境界に白帯を描くシンプルな方法でOCRのグルーピングが劇的に安定します。

処理の考え方

  1. PDF を 300〜400dpi で画像化。
  2. 明細テーブル領域を検出(手動座標でも可)。
  3. 垂直投影(列方向の黒画素数)で谷を検出 → Pack/Size の境界 x 座標近辺に 5〜10px の白帯を描画。
  4. その画像を学習・推論パイプラインに供給。

Python(OpenCV)サンプル

# 前処理:垂直ホワイトスペースを挿入して列を分離
import cv2
import numpy as np

def widen_column_gap(img, x, gap=8):
# img: BGR or grayscale numpy array
h, w = img.shape[:2]
x0 = max(0, x - gap//2)
x1 = min(w, x + gap//2)
img[:, x0:x1] = 255  # 白で塗る(前処理)
return img

def find_vertical_valleys(bin_img, min_width=5):
# 垂直投影で谷候補(列境界)を返す
proj = (255 - bin_img).sum(axis=0)  # 黒画素の列方向合計
valleys = []
in_valley = False
start = 0
for i, v in enumerate(proj):
if v < proj.mean()*0.35:
if not in_valley:
in_valley = True; start = i
else:
if in_valley:
if i - start >= min_width:
valleys.append((start + i)//2)
in_valley = False
return valleys

img = cv2.imread("sysco_invoice_p1.png", cv2.IMREAD_GRAYSCALE)
img = cv2.threshold(img, 0, 255, cv2.THRESH_OTSU | cv2.THRESH_BINARY)[1]
valleys = find_vertical_valleys(img)

# Pack/Size の既知境界(例:2本目の谷)を選ぶ

if len(valleys) >= 2:
x_boundary = valleys[1]
widened = widen_column_gap(img.copy(), x_boundary, gap=10)
cv2.imwrite("sysco_invoice_p1_preprocessed.png", widened) 

座標が安定しない場合は、1回だけ手作業で Pack/Size 境界の x 座標を測り、テンプレートとして固定する運用も現実的です。フォームが複数版式あるならテンプレートを複数保持します。

④ 抽出後のエンティティ分割(後処理)

抽出段階で結合されても、後処理で十分リカバリできます。Syscoでよく見られる表記をカバーするパターンを最初から実装しましょう。

よくある表記と意味

表記例PackSize備考
6/10610単位省略(缶号など)。用途で単位補完。
10/2.5 lb102.5 lb重量系。
4x5 LB45 lb区切りが x・×・/ と揺れる。
1 EA1EASize が単位のみの場合。
5 CT5CTカウント系。

正規表現の例(言語非依存ロジック)

# "pack/size" の分離(例: "10/2.5 lb", "6/10", "4x5 LB")
pattern = r"^\s*(?P<pack>\d+)\s*[/x×]\s*(?P<size>\d+(?:\.\d+)?\s*(?:oz|lb|lbs?|#|kg|g|ct|ea|each|bag|pk|pc|gal|qt|pt|l|ml)?)\s*$"

# "size のみ"(例: "2.5 lb", "10 oz", "EA")

pattern_size_only = r"^\s*(?P\d*(?:.\d+)?\s*(?:oz|lb|lbs?|#|kg|g|ct|ea|each|bag|pk|pc|gal|qt|pt|l|ml))\s*$"

# "pack のみ"(例: "1 EA", "5 CT" を pack=1 size=EA のように扱う分岐)

pattern_pack_unit = r"^\s*(?P\d+)\s*(?Pea|each|ct|bag|pk|pc)\s*$" 

Power Automate / AI Builder でも同様に、抽出結果(文字列)のカスタム式で分割可能です。Power Fx なら、区切り文字の位置(/、x、×)を探し、左右を Trim() して単位正規化テーブルで置換する流れが実務的です。

単位の正規化テーブル(例)

入力正規化用途
LB, lb, lbs, #lb重量(ポンド)に統一
OZ, ozozオンス
EA, Eachea個数単位
CTctカウント
GAL, gal, L, l, mlgal / l / ml容量系

⑤ カスタム抽出器の追加(必要に応じて)

Pack/Size を別のモデルとして厳密に切り出すアプローチです。明細テーブルの該当列にだけ関心領域(ROI)を限定し、そこから特定パターンを抽出します。ノイズの多い説明文側を特徴量から外すことで誤結合を減らせます。

実装テンプレート:Azure AI Document Intelligence

推論呼び出し(Python例)

import requests, json, time

endpoint = "https://<your-resource>.cognitiveservices.azure.com"
api_key = "<your-key>"
model_id = "<custom-model-or-prebuilt-invoice>"

def analyze_invoice(file_path):
with open(file_path, "rb") as f:
data = f.read()
url = f"{endpoint}/documentintelligence/documentModels/{model_id}:analyze?api-version=2024-02-29-preview"
headers = {"Ocp-Apim-Subscription-Key": api_key, "Content-Type": "application/pdf"}
resp = requests.post(url, headers=headers, data=data)
resp.raise_for_status()
result_url = resp.headers["operation-location"]
# ポーリング
for _ in range(60):
r = requests.get(result_url, headers={"Ocp-Apim-Subscription-Key": api_key})
js = r.json()
if js.get("status") in ["succeeded", "failed"]:
return js
time.sleep(1)
raise TimeoutError("analysis timeout")

result = analyze_invoice("sysco_invoice_preprocessed.pdf")

# result から Items[].content(行テキスト)を取り、④の正規表現で Pack/Size を分割

Power Automate(AI Builder)の典型フロー

  1. トリガ:SharePoint/OneDrive に請求書PDFが追加。
  2. 前処理:Azure Function/PowerShell でホワイトスペース拡張済みPDFを生成。
  3. AI Builder「請求書の処理」またはカスタム抽出モデルで解析。
  4. 後処理:正規表現・置換・単位正規化。Pack/Size を確定。
  5. Dataverse/SQL/Excel に書き込み。Power BI で可視化。

アノテーション作業のベストプラクティス

  • 矩形は最小限:文字列ぴったりに合わせ、隣列に食い込まない。
  • 折り返しは行単位:説明文が2行なら2矩形。Size列が次行に落ちるケースを別サンプルとして追加。
  • 難例の意図的増量:Pack/Size が重なる、区切りが消える、印字が薄い――などの「難例」を 訓練分布に意識的に増やす。

モデル改善の進め方(実験計画)

ステップ施策成功指標(例)判定の目安
0Prebuilt Invoice の素当たり確認行単位の正規化後で Pack/Size 正解率 ≥ 80%達成ならカスタム無し運用も検討
1学習データを20枚に拡張Pack/Size 列のF1向上未達なら ③ へ
2ホワイトスペース拡張の前処理導入誤結合率の顕著減少未達なら ④ 併用
3正規表現・単位正規化の後処理エッジケースの補正率上昇未達なら ⑤ 実施

後処理の実装詳細(擬似コード)

def split_pack_size(token: str):
    s = token.strip().lower()
    s = s.replace("each", "ea")
    s = s.replace("lbs", "lb")
    s = s.replace("#", "lb")
    # 区切りで左右に分割
    for sep in ["/", "x", "×"]:
        if sep in s:
            left, right = [t.strip() for t in s.split(sep, 1)]
            pack = left if left.isdigit() else None
            size = right
            return normalize_pack(pack), normalize_size(size)
    # 単位のみ or pack+unit パターン
    if s.isdigit():
        return normalize_pack(s), None
    if any(u in s for u in ["oz","lb","ea","ct","gal","qt","pt","ml","l"]):
        # size のみ
        return None, normalize_size(s)
    # "1 ea" など
    parts = s.split()
    if len(parts) == 2 and parts[0].isdigit():
        return normalize_pack(parts[0]), normalize_size(parts[1])
    return None, None

「Item Description と Size が混ざる」への手当

説明文の末尾にサイズがぶら下がるパターンは、右端の単位パターン抽出 → 切り出し → 残りを説明文に戻すで安定します。

# 末尾サイズの切り出し
def detach_trailing_size(desc: str):
    import re
    m = re.search(r"(?:\s|-)\s*(\d+(?:\.\d+)?\s*(?:oz|lb|ct|ea|gal|qt|pt|ml|l))\s*$", desc.lower())
    if m:
        size = m.group(1)
        new_desc = desc[:m.start()].rstrip(" -")
        return new_desc, normalize_size(size)
    return desc, None

失敗を減らす入力品質管理

  • 解像度:300dpi 以上(できれば 400dpi)。
  • 傾き補正:ハフ変換や最小二乗で傾きを 0.5° 未満に。
  • 濃度補正:二値化(OTSU)やアンシャープマスクでコントラスト強化。
  • 余白カット:外周ノイズを除去しレイアウト検出を助ける。

運用チェックリスト(実務で効くものに絞る)

  1. スキャン品質の統制:解像度・傾き・コントラストの基準値を定め、逸脱時は差し戻す。
  2. フォーマット差分の捕捉:店舗・期間・印字機で版式が変わるたびに 1〜2 枚ずつ学習へ補充。
  3. テストセットの固定:学習に使わない請求書で Pack/Size 抽出を回帰テスト。
  4. 失敗ログの運用:結合が起きたページをID付きで保管、月次で前処理・正規表現を更新し再学習。
  5. バージョニング:モデルID・前後処理のハッシュ・学習データsnapshotを紐づけ、再現性を維持。

設計パターン:前処理×モデル×後処理の役割分担

層主な役割Pack/Size 誤結合への効き目注意点
前処理画像品質の統制、列間の白帯挿入◎(根本から改善)テンプレ座標の保守が必要
モデルレイアウト学習・文字列抽出○(データ多様性が鍵)データ作成コストが高い
後処理正規表現・単位正規化・パース◎(最終防御線)パターン漏れの保守が必要

「最短で効果」を狙う実行手順

  1. 既成 Invoice モデルで母集団3〜5枚の当たりを測る。
  2. Pack/Size が連結される典型ページを 5枚抽出し、ホワイトスペース拡張前処理を適用して再テスト。
  3. 改善が見えたら、学習データを 20 枚に増やし、同じ前処理をかけた状態で再学習。
  4. 抽出文字列を 正規表現+単位正規化で分割。各パターンのユニットテストを用意。
  5. それでも崩れる版式だけ、カスタム抽出器を別モデルで用意。

エッジケース対策集

  • 数値と単位の間にピリオドが入る:2.5lb と 2.5 lb を同一正規表現で吸収。
  • 全角×記号:× を x に正規化。
  • 小数点のカンマ:2,5 lb を 2.5 lb に正規化。
  • 説明文の略号:PK が商品名に含まれる場合は左右の数字有無で判定を切替。
  • サイズが行頭へ回り込む:行ごとに右端の単位表現を探索し、見つかれば説明文から切り出す。

評価とモニタリング

  • 項目別F1:Description、Pack、Size を個別に評価。Pack/Size だけのスコアも算出。
  • 失敗カテゴリ別カウント:連結、欠落、誤単位、桁違い。週次で棒グラフ化。
  • スナップショット保存:モデル更新時は前後処理のバージョン込みで ZIP 化して保管。

トラブルシューティング早見表

症状原因のあたり対処
Pack/Size が必ず連結列間余白不足・版式差分③前処理で白帯挿入 → 座標テンプレ採用 → ②データ拡充
Description に Size が吸着折り返し・右寄せ④後処理で末尾サイズ切り出し、行分割アノテーション徹底
単位が不統一表記ゆれ単位正規化テーブルの導入・更新
学習を増やしても改善が鈍いデータ分布の偏り難例の意図的追加、スキャン条件の多様化

参考アーキテクチャ(現場導入の一例)

Ingestion (SharePoint/OneDrive/メール)
   └─&gt; 前処理 (Azure Functions or PowerShell)
          └─ ホワイトスペース拡張 / 傾き補正 / 二値化
               └─&gt; AI Document Intelligence or AI Builder
                      └─&gt; 後処理 (正規表現 / 単位正規化 / 末尾サイズ切り出し)
                            └─&gt; データ格納 (Dataverse/SQL/Excel)
                                  └─&gt; 可視化 (Power BI) / 連携 (ERP)

まとめ:段階的に積み上げれば「くっつく問題」は抑え込める

Sysco請求書に特有の「列間が狭い」課題は、前処理で列を離す、学習分布を広げる、後処理でパターン分割という三段構えで高い再現性を持って改善できます。まずは少数サンプルでホワイトスペース拡張を試し、手応えが出たら学習データの多様化と正規表現の整備に進みましょう。Pack と Size、Description と Size の混在は、今日から計画的に減らせます。


付録:PowerShellでのPDF一括前処理(例)

# 画像化と白帯描画のバッチ(概念例)
param([string]$InDir=".\in", [string]$OutDir=".\out", [int]$Gap=10, [int]$Dpi=300)

# 要: ImageMagick/ghostscript 等のセットアップ

Get-ChildItem $InDir -Filter *.pdf | ForEach-Object {
$name = $*.BaseName
magick -density $Dpi "$($*.FullName)" -units PixelsPerInch -alpha off "$OutDir$name.png"
Get-ChildItem $OutDir -Filter "$name*.png" | ForEach-Object {
# Pack/Size 境界 x座標はテンプレから読み込み(例:600px)
$x = 600
magick $*.FullName -fill white -draw "rectangle $($x-$Gap/2),0 $($x+$Gap/2),99999" "$($*.DirectoryName)$($name)_pre.png"
}
} 

付録:正規表現ユニットテストの観点

  • 6/10 → pack=6, size=10
  • 10/2.5 lb → pack=10, size=2.5 lb
  • 4x5 LB → pack=4, size=5 lb
  • 1 EA → pack=1, size=ea
  • 2.5lb → pack=None, size=2.5 lb
  • EA → pack=None, size=ea
  • 5 CT → pack=5, size=ct
  • Organic Tomato 10 oz → desc=Organic Tomato, size=10 oz

付録:学習データの目安とコスト感

規模準備内容期待効果コメント
~20枚基本的な版式を網羅、難例を数枚連結の多発は減少まずはここから
40〜50枚店舗・期間・解像度の多様化Pack/Size の再現性向上工数は上がるが効果大
50枚〜難例を継続追加、回帰テスト確立エッジケース吸収モデル安定期

付録:品質ゲート例(自動判定)

  • Pack が整数以外なら再処理キューへ。
  • Size に未知単位が含まれる場合はレビューへ。
  • Description の末尾が単位で終わる場合は自動で切り出し再試行。

付録:よくあるアンチパターン

  • 少数データに過学習:5枚で精密にラベル付けしても分布外で崩れる。
  • 前処理の学習・推論不一致:学習時だけ高解像度/二値化にすると本番で性能劣化。
  • 正規表現が複雑すぎる:メンテ不能。単位辞書と簡素な分岐の組合せが長持ち。

この記事で紹介した手順を段階導入すれば、Sysco請求書における Pack と Size、Description と Size の混在問題は大幅に抑え込めます。まずは「白帯を足す」だけでも効果が出ます。そこからデータ拡充と後処理で、現場で使える抽出品質へ押し上げましょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次