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のグルーピングが劇的に安定します。
処理の考え方
- PDF を 300〜400dpi で画像化。
- 明細テーブル領域を検出(手動座標でも可)。
- 垂直投影(列方向の黒画素数)で谷を検出 → Pack/Size の境界 x 座標近辺に 5〜10px の白帯を描画。
- その画像を学習・推論パイプラインに供給。
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でよく見られる表記をカバーするパターンを最初から実装しましょう。
よくある表記と意味
| 表記例 | Pack | Size | 備考 |
|---|---|---|---|
6/10 | 6 | 10 | 単位省略(缶号など)。用途で単位補完。 |
10/2.5 lb | 10 | 2.5 lb | 重量系。 |
4x5 LB | 4 | 5 lb | 区切りが x・×・/ と揺れる。 |
1 EA | 1 | EA | Size が単位のみの場合。 |
5 CT | 5 | CT | カウント系。 |
正規表現の例(言語非依存ロジック)
# "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, oz | oz | オンス |
EA, Each | ea | 個数単位 |
CT | ct | カウント |
GAL, gal, L, l, ml | gal / 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)の典型フロー
- トリガ:SharePoint/OneDrive に請求書PDFが追加。
- 前処理:Azure Function/PowerShell でホワイトスペース拡張済みPDFを生成。
- AI Builder「請求書の処理」またはカスタム抽出モデルで解析。
- 後処理:正規表現・置換・単位正規化。Pack/Size を確定。
- Dataverse/SQL/Excel に書き込み。Power BI で可視化。
アノテーション作業のベストプラクティス
- 矩形は最小限:文字列ぴったりに合わせ、隣列に食い込まない。
- 折り返しは行単位:説明文が2行なら2矩形。Size列が次行に落ちるケースを別サンプルとして追加。
- 難例の意図的増量:Pack/Size が重なる、区切りが消える、印字が薄い――などの「難例」を 訓練分布に意識的に増やす。
モデル改善の進め方(実験計画)
| ステップ | 施策 | 成功指標(例) | 判定の目安 |
|---|---|---|---|
| 0 | Prebuilt 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 枚ずつ学習へ補充。
- テストセットの固定:学習に使わない請求書で Pack/Size 抽出を回帰テスト。
- 失敗ログの運用:結合が起きたページをID付きで保管、月次で前処理・正規表現を更新し再学習。
- バージョニング:モデルID・前後処理のハッシュ・学習データsnapshotを紐づけ、再現性を維持。
設計パターン:前処理×モデル×後処理の役割分担
| 層 | 主な役割 | Pack/Size 誤結合への効き目 | 注意点 |
|---|---|---|---|
| 前処理 | 画像品質の統制、列間の白帯挿入 | ◎(根本から改善) | テンプレ座標の保守が必要 |
| モデル | レイアウト学習・文字列抽出 | ○(データ多様性が鍵) | データ作成コストが高い |
| 後処理 | 正規表現・単位正規化・パース | ◎(最終防御線) | パターン漏れの保守が必要 |
「最短で効果」を狙う実行手順
- 既成 Invoice モデルで母集団3〜5枚の当たりを測る。
- Pack/Size が連結される典型ページを 5枚抽出し、ホワイトスペース拡張前処理を適用して再テスト。
- 改善が見えたら、学習データを 20 枚に増やし、同じ前処理をかけた状態で再学習。
- 抽出文字列を 正規表現+単位正規化で分割。各パターンのユニットテストを用意。
- それでも崩れる版式だけ、カスタム抽出器を別モデルで用意。
エッジケース対策集
- 数値と単位の間にピリオドが入る:
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/メール)
└─> 前処理 (Azure Functions or PowerShell)
└─ ホワイトスペース拡張 / 傾き補正 / 二値化
└─> AI Document Intelligence or AI Builder
└─> 後処理 (正規表現 / 単位正規化 / 末尾サイズ切り出し)
└─> データ格納 (Dataverse/SQL/Excel)
└─> 可視化 (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=1010/2.5 lb→ pack=10, size=2.5 lb4x5 LB→ pack=4, size=5 lb1 EA→ pack=1, size=ea2.5lb→ pack=None, size=2.5 lbEA→ pack=None, size=ea5 CT→ pack=5, size=ctOrganic Tomato 10 oz→ desc=Organic Tomato, size=10 oz
付録:学習データの目安とコスト感
| 規模 | 準備内容 | 期待効果 | コメント |
|---|---|---|---|
| ~20枚 | 基本的な版式を網羅、難例を数枚 | 連結の多発は減少 | まずはここから |
| 40〜50枚 | 店舗・期間・解像度の多様化 | Pack/Size の再現性向上 | 工数は上がるが効果大 |
| 50枚〜 | 難例を継続追加、回帰テスト確立 | エッジケース吸収 | モデル安定期 |
付録:品質ゲート例(自動判定)
- Pack が整数以外なら再処理キューへ。
- Size に未知単位が含まれる場合はレビューへ。
- Description の末尾が単位で終わる場合は自動で切り出し再試行。
付録:よくあるアンチパターン
- 少数データに過学習:5枚で精密にラベル付けしても分布外で崩れる。
- 前処理の学習・推論不一致:学習時だけ高解像度/二値化にすると本番で性能劣化。
- 正規表現が複雑すぎる:メンテ不能。単位辞書と簡素な分岐の組合せが長持ち。
この記事で紹介した手順を段階導入すれば、Sysco請求書における Pack と Size、Description と Size の混在問題は大幅に抑え込めます。まずは「白帯を足す」だけでも効果が出ます。そこからデータ拡充と後処理で、現場で使える抽出品質へ押し上げましょう。

コメント