昨天收 Phase 1 的時候我講了一句可能會害到自己的話:今天測的法說會簡報,版面其實相當乾淨
今天這篇就是去把髒的找出來
而且不是我用眼睛翻頁挑的,是用分數挑的。
Day 2 我從 40 頁的法說會簡報裡挑了 5 頁做 baseline,挑選邏輯是「涵蓋三種版面類型」——純文字、圖文混排、表格
那是個合理的起手式,但它有個我當時沒說破的問題:那 5 頁是我用眼睛挑的
用眼睛挑會發生什麼事?
會挑到「看起來有代表性」的頁,而不是「真的會壞掉」的頁。人在翻頁的時候,注意力會被大標題、被色塊、被圖表吸走,最容易被跳過的反而是那些字最小、最密、最不起眼的區域——而那些恰好就是 OCR 最容易翻車的地方
所以 Day 2 那把尺,量到的是「一般難度」
今天我要換一把尺,量的是「最難的地方有多難」
💡Tip: 這是做評測時很常見的一個偏誤,我自己也踩過:測試集是你挑的,而你挑測試集時已經帶著預期了。如果你的測試集挑選過程無法被寫成一段可執行的程式碼,那它就有偏誤,只是你還沒發現而已。Day 27 講 2-5% 抽樣人工稽核的時候,這件事會以另一個形式再出現一次——抽樣也要有規則,不能憑感覺抽。
「這頁很難」是個感覺,感覺沒辦法排序
所以第一步是把繁中版面的難點拆成四個可以直接對 PDF 文字層算出數字的維度:
| 維度 | 代號 | 為什麼它難 | 怎麼算 |
|---|---|---|---|
| 跨欄密集表格 | dense_table |
欄位邊界要靠視覺推,模型只能猜閱讀順序 | 一行內有 ≥3 組數字,或 ≥2 段連續空白,算「命中行」;分數 = 命中行 ÷ 總行數 |
| 罕用字/專有名詞密度 | rare_char |
罕用字在訓練語料裡出現次數少,模型傾向猜成形近的常用字 | 該頁 CJK 字中,全文件 40 頁累計出現 ≤3 次的字所佔比例 |
| 中英數混排密度 | mixed_cjk_ascii |
中英切換處的斷詞與空白處理最容易亂,NT$、%、單位常被搬家 |
CJK 與 ASCII 英數字元互相切換的次數 ÷ 總字元數 |
| 小字級註腳 | footnote |
字小、在版面邊緣、常被壓在圖表下方 | 同頁有多種字級時,最小字級的文字長度佔全頁文字長度的比例 |
四個維度都寫成函式,直接跑在 PyMuPDF 抽出來的文字層與 span 資訊上。以罕用字那條為例:
def score_rare_char(page_cjk_chars, corpus_freq, rare_threshold=3):
"""罕用字/專有名詞密度:以「全文檔(40 頁)逐字出現次數」當語料頻率的代理。"""
if not page_cjk_chars:
return 0.0
rare_count = sum(1 for c in page_cjk_chars if corpus_freq[c] <= rare_threshold)
return rare_count / len(page_cjk_chars)
這裡有個必須講清楚的妥協:我沒有外部中文字頻表
理想做法是拿一份大型語料的字頻統計來判定「罕用」。我手上沒有,也不想為了這件事多引一個來源進來(隱私那條線 Day 12 會講,外部資料進系統是要審的)
所以我用文件內頻率當代理:在這份 40 頁的文件裡只出現 1~3 次的字,視為這份文件的罕用字
這不等於語言學上的罕用字。「氫」在中文裡不算罕用,但在一份聚酯樹脂法說會簡報裡它可能只出現兩次——而對 OCR 來說,上下文稀薄本來就是一種難,所以這個代理指標雖然不精確,但方向是對的
四個類別各挑自己分數最高的那頁,貪婪依序選,排除三種頁:
跑出來的結果:
| 選中順序 | 頁 | 類別 | 該類別分數 | n_chars |
n_cjk_chars |
|---|---|---|---|---|---|
| 1 | 第 2 頁 | dense_table |
0.1429 | 90 | 48 |
| 2 | 第 24 頁 | rare_char |
0.3012 | 442 | 249 |
| 3 | 第 5 頁 | mixed_cjk_ascii |
0.2350 | 220 | 128 |
| 4 | 第 19 頁 | footnote |
0.5176 | 92 | 56 |
(n_chars 是文字層去頭尾空白後的字數,跟後面 CER 表裡的 gt_raw_len 差 1–2 字是這個原因。)
(全部 40 頁的逐頁四維分數明細落在 step4_page_selection.json,選頁邏輯在 step4_zh_hard_pages.py,兩個檔都可以直接重跑驗證——這一步不需要呼叫模型,純粹是對 PDF 算數。)
有兩件事值得先講:
第一,dense_table 最高分只有 0.1429。 這代表整份簡報裡根本沒有真正密集的跨欄表格——這是法說會簡報,不是年報附註。所以「第 2 頁」是矮子裡拔將軍選出來的,它是這份文件裡最像密集表格的一頁,不是一個真正的難表格。這個限制會直接影響今天結論的適用範圍。
第二,footnote 分數 0.5176 高得很明顯。 第 19 頁超過一半的文字都落在最小字級上。後面你會看到,這頁確實炸得最誇張——但炸的原因跟我原本預期的完全不一樣。
第三,貪婪選法有副作用,要講清楚。 第 2 頁的 footnote_score 其實是 0.5714,比第 19 頁的 0.5176 還高。但因為 dense_table 排在選擇順序的第一位,第 2 頁先被它選走了,footnote 只能拿次高的第 19 頁。這是一個刻意的取捨——我要的是四種難點各有一頁,不是「四頁裡有兩頁都是註腳頁」。但代價是每一頁都不見得是該類別的全場最高分,引用這些分數的時候要記得這件事。
💡Tip: 我會建議你在做這種「難度評分」的時候,順手把全部頁面的分數都落檔,不要只存選中的那幾頁。因為分數分布本身就是資訊——像上面
dense_table全場最高才 0.14 這件事,就是從分布看出來的,而它推翻了我原本「這份文件有密集表格」的假設。
選出 4 頁之後,用跟 Day 2 一模一樣的請求再跑一次——同一個 endpoint、同一顆模型、同一段 prompt、max_tokens=1500、temperature=0。沒有改 prompt、沒有加 few-shot、沒有做前處理
因為今天要測的是難度,不是我的調校功力
把新測 4 頁跟 Day 2 既有 5 頁放在同一張表:
| 頁面 | 來源 | CER | 覆蓋率 | diff 塊數 | GT 字數 | 輸出字數 |
|---|---|---|---|---|---|---|
| page_01_pure_text | Day 2 | 0.0888 | 0.9941 | 3 | 179 | 198 |
| page_09_mixed | Day 2 | 0.5111 | 0.7619 | 34 | 409 | 765 |
| page_11_table | Day 2 | 0.1341 | 0.9878 | 7 | 523 | 1042 |
| page_12_table | Day 2 | 0.3643 | 0.9806 | 9 | 324 | 660 |
| page_14_mixed | Day 2 | 0.7933 | 0.8894 | 7 | 256 | 519 |
| page_02_dense_table | Day 6 新測 | 1.4815 | 0.9877 | 4 | 91 | 257 |
| page_05_mixed_cjk_ascii | Day 6 新測 | 0.6885 | 0.9399 | 8 | 221 | 390 |
| page_19_footnote | Day 6 新測 | 15.4000 | 0.5875 | 6 | 93 | 2477 |
| page_24_rare_char | Day 6 新測 | 1.0085 | 0.9437 | 9 | 443 | 772 |
(全部為實測,出處 analysis/metrics_summary_step4.csv。CER 與覆蓋率是在正規化後的字串上算的,正規化會先去掉空白與 Markdown 標記,定義沿用 Day 2 的 analysis/compare_ocr.py。)
先解釋一件會讓人卡住的事:CER 可以大於 1
CER 是編輯距離 ÷ GT 長度。如果模型輸出比 GT 長很多,插入的字全部算進編輯距離,分子就可能超過分母。所以 CER = 1.48 不代表「錯了 148%」,而是「要把模型輸出改回 GT,需要動的字數,是 GT 全文長度的 1.48 倍」
換句話說:這頁與其修,不如整頁重打。
而 page_19 的 15.4,意思是要動 GT 全文長度的 15 倍——這已經不是辨識錯誤了,這是別的東西。等下單獨講
另外注意覆蓋率這欄。Day 2 我用它當主要指標,它的意思是「GT 裡的字有多少比例出現在模型輸出裡」。你會發現除了 page_19,其他三頁覆蓋率都在 0.94 以上
字幾乎都在。壞的是位置、是結構、是那幾個關鍵字。
這句話是今天整篇的核心,也是 Phase 2 存在的理由
第 2 頁是封面頁:標題、講者、日期,加上背景那層很淡的化學元素週期表底紋
CER 1.4815,但覆蓋率 0.9877。字都在,那 1.48 是哪來的?
看 diff:
GT :2025年第四季營運成果暨合成樹脂事業單位:高性能熱塑複材創新與跨域應用Presenter:李全能部長(聚酯樹脂事業部)劉秉誠發言人Date:2026/03/19
輸出:這張圖片中包含的文字內容如下:中央主要文字:2025年第四季營運成果暨合成樹脂事業單位:高性能熱塑複材創新與跨域應用右下角資訊:Presenter:李全部長(聚酯樹脂事業部)劉秉誠發言人Date:2026/03/19左上角背景(科學公式/化學結構相關文字):SrStrontium56BaBarium88RaRadium3940MeOHMeMe3132505682右上方Logo文字:Eternal
四個 diff 塊,三個是「插入」:
| 類型 | GT | 模型輸出 |
|---|---|---|
| insert | (無) | 這張圖片中包含的文字內容如下:中央主要文字: |
| insert | (無) | 右下角資訊: |
| delete | 能 |
(無) |
| insert | (無) | 左上角背景(科學公式/化學結構相關文字):SrStrontium56BaBarium88RaRadium3940MeOHMeMe3132505682右上方Logo文字:Eternal |
模型做了兩件 GT 沒有的事:
第一,它替版面加上了方位標籤。「中央主要文字:」「右下角資訊:」「左上角背景」「右上方Logo文字:」——這四個標籤全都是模型自己生成的結構描述。從人的角度看,這其實很有用;從 diff 比對的角度看,這是 91 個字的 GT 硬生生被灌成 257 個字的主因
第二,它把背景底紋的化學元素表讀出來了。 GT 裡沒有 SrStrontium56BaBarium88RaRadium 這段,因為 PyMuPDF 文字層沒有抽到那層底紋(可能是圖片、可能是向量圖形)。但模型看到了
所以這一段到底算「幻覺」還是「GT 漏抽」?
我不知道,而這正是問題所在。 分類腳本把它歸到 other(無法用現有規則分類),我也不打算硬掰。這件事會在 Day 11 講獨立 verifier 的時候變成一個具體需求:你需要一個不依賴 GT 的檢查手段,因為 GT 本身就不可靠
至於那個 delete——GT 的「李全能部長」被模型讀成「李全部長」。一個字,人名。這才是這頁真正的辨識錯誤,而它在 1.48 的 CER 裡佔的比重小到看不見
💡Tip: 這裡有個指標設計的教訓:CER 對「模型多話」的懲罰,遠大於對「人名讀錯一個字」的懲罰。但從業務角度,人名錯一個字的嚴重性高得多。這就是為什麼 Day 16 要做「關鍵欄位語意驗證」——你不能只有一個籠統的 CER,你需要對特定欄位單獨設門檻。
第 5 頁是公司概況頁:董事長、經營業務、創立年份、員工數、合併營收、研發經費,加上兩條註解和生產基地分布
mixed_cjk_ascii 分數 0.235,全場最高。CER 0.6885,覆蓋率 0.9399
這頁的 diff 有一組特別漂亮:
GT :合併營收407億元註2NT$
輸出:合併營收NT$407億元註2
diff 工具把它切成兩塊:一塊 insert 在前面放了 NT$,一塊 delete 在後面拿掉了 NT$
模型是對的,GT 是錯的。
PDF 文字層的抽取順序跟視覺版面不一致——NT$ 在版面上是印在數字左邊的,但它在 PDF 內部的物件順序被排到了後面。模型看的是圖,它按視覺順序讀,所以它讀成 NT$407億元,這是人類會寫的順序
而我的分類腳本把這兩塊歸類成 table_structure(欄位錯位),因為它偵測到「這段文字在對方全文的別處出現過」
分類是對的,歸因是反的。 錯位的是 GT,不是模型
同一頁還有一個真正的錯誤:
GT :佔營業額比重佔營業額比重
輸出:佔營盛佔比重
「佔營業額比重」被讀成「佔營盛佔比重」。「業額」→「盛」,這是典型的小字級 + 形近字連鎖:兩個字被擠壓成一個視覺區塊,模型輸出了一個形狀接近但不存在於原文的字
💡Tip: 這組對照我為了好讀做了合併敘述。實際在
diffs\page_05_mixed_cjk_ascii.diff.txt裡,difflib 把它切成 insert + replace 兩個 opcode,不是單一一塊。引用 diff 當證據時要注意這件事:差異塊的數量是演算法切出來的,不是語意上的錯誤數量,兩者不能混用
還有一個純漏字:
GT :生產基地24個台灣5個大陸13個美國1個泰國1個日本2個馬來西亞1個義大利1個
輸出:生產基地*台灣5個*大陸13個*美國1個*泰國1個*日本2個*馬來西亞1個*義大利1個
看出來了嗎?「24個」不見了
模型把生產基地列成了 bullet list,格式比 GT 漂亮,但總數 24 個這個關鍵數字被吃掉了。而後面七個國家的數字加起來是 5+13+1+1+2+1+1 = 24——加總對得上,但總數本身沒被輸出
這是一個完美的 Day 22 案例:如果你有一條「總數欄位必須等於明細加總」的 checksum 規則,這個漏字當場就會被抓到,而且你不需要 GT 就能抓
第 24 頁是應用產業一覽表:八個產業別,每個有代號、應用部位、材料優勢,三欄
rare_char 分數 0.3012,全場最高。CER 1.0085,覆蓋率 0.9437
這頁的錯誤最密集,也最有代表性。挑幾組:
GT :綠(可回收)
輸出:線(可回收)
「快、輕、強、綠」是這頁的價值主張四字訣。模型把「綠」讀成「線」
這兩個字左半邊都是「糸」,右半邊「彔」和「戔」在小字級下的筆畫團塊非常接近。這是標準的形近字誤判
但我的分類腳本把它歸到了 other,不是 visual_confusion。為什麼?因為形近字對照表裡只列了 19 組手工整理的組合(己已巳、末未、土士、日曰……),沒有「綠/線」這組
這件事明天會是 Day 7 的主線。先記著
同一頁還有一串:
GT :阻燃、低煙毒、可回收 ... WT風電/能源設備 ... 葉片補強/機艙罩/支架 ... 載具/治具/ESD件 ... 可整合成形 ... 模具/快速工裝/治具 ... SP運動休閒
輸出:防燃、低煙毒、可回收 ... WT風電/線源設備 ... 葉片補償/艙罩/架支 ... 製具/治具/ESD件 ... 可整形成形 ... 模具/快速工具/治具 ... SP休閒用品
逐條看:
| GT | 模型輸出 | 錯在哪 |
|---|---|---|
| 阻燃 | 防燃 | 形近+語意合理,「防燃」不是錯得離譜的詞 |
| 能源設備 | 線源設備 | 同一個「綠/線」家族的錯 |
| 補強 | 補償 | 形近,且「補償」是財報常見詞,語言模型先驗偏好它 |
| 支架 | 架支 | 字對,順序反了 |
| 載具 | 製具 | 形近 |
| 可整合成形 | 可整形成形 | 「合」被吃掉 |
| 快速工裝 | 快速工具 | 「裝」→「具」,受同句「治具」影響 |
| 運動休閒 | 休閒用品 | 直接換詞 |
| 3D列印絲材 | 3D列印材 | 「絲」被吃掉 |
| 座椅框 | 座艙 | 整詞替換 |
看出模式了嗎?
這些錯誤全部都「讀起來很通順」。
「防燃」「補償」「工具」「休閒用品」——每一個都是合法的中文詞,放在那個位置都不突兀。如果你不看原圖、只看模型輸出,你完全不會發現這頁錯了
這就是 Day 5 講的「安靜的失敗」在繁中場景的具體長相:語言模型的語言先驗,會把辨識錯誤修飾成通順的文字。它不會輸出亂碼,它會輸出一個聽起來很合理的錯答案
而且這頁的三欄結構也整組垮了。GT 的閱讀順序是「產業 → 部位 → 優勢」一組一組走,模型輸出變成「所有產業代號 → 所有部位 → 所有優勢」分批列出。覆蓋率 0.9437 很漂亮,但欄位對應關係全錯
你拿這份輸出去做結構化抽取,會得到「航太內裝的材料優勢是耐壓、耐腐蝕」這種完全錯誤的配對
💡Tip: 表格 OCR 有一個反直覺的性質:字元層指標好看,不代表表格是對的。覆蓋率 94% 的這頁,如果用「欄位配對正確率」去量,可能是 0%。這就是 Day 18 為什麼要在 Pipeline 裡塞 Docling 做版面偵測,而不是把整頁交給 VLM——位置資訊要在送進模型之前就先鎖住。
第 19 頁是一張散點圖:橫軸模數、縱軸比抗張強度,圖上散落幾十個材料標籤,下面一行小字是資料來源引用
footnote 分數 0.5176,全場最高。然後——
CER 15.4,覆蓋率 0.5875,模型輸出 2477 字對上 GT 的 93 字。
這數字太誇張了,所以我要單獨開一節講它
先看 step4_results.jsonl 裡這頁的請求記錄:
| 欄位 | 值 |
|---|---|
finish_reason |
length |
completion_tokens |
1500(撞到 max_tokens 上限) |
elapsed_sec |
30.674 |
completion_tok_per_sec |
48.9 |
其他三頁的 finish_reason 都是 stop,completion_tokens 分別是 167、266、479
只有這頁撞牆。
打開輸出看,原因一目了然:
輸出(節錄):...*碳纖維/鋁複合材料*碳纖維/鋁複合材料*碳纖維/鋁複合材料*碳纖維/鋁複合材料...
「碳纖維/鋁複合材料」這串重複了超過一百次,一路重複到撞上 max_tokens 被硬切斷
這是典型的重複退化(repetition degeneration):模型在一個低資訊量、高視覺相似度的區域(散點圖上幾十個外觀接近的小標籤)陷入自迴圈,一直輸出同一串 token
所以 CER 15.4 的 15,有多少是「模型辨識能力差」?
幾乎沒有。
那 1232 的編輯距離裡,絕大部分來自這段重複文字。這是一個解碼階段的失控,不是一個視覺辨識問題。你就算換一顆更強的模型,只要解碼參數一樣、只要這張圖一樣,它還是可能掉進同一個迴圈
而且事情還有第二層。這段重複文字在 GT 裡完全找不到對應,所以分類腳本把它算進 hallucination 和 other
但我不能排除那張散點圖上真的有這些圖例文字,只是 PyMuPDF 沒抽到。
第 19 頁的 GT 只有 93 個字,而它是一張圖表頁——圖表裡的文字標籤如果是以向量圖形或圖片形式存在,文字層就抽不到。模型看的是 200 DPI 的渲染圖,它看得到那些標籤
換句話說,這頁的 hallucination 計數同時高估了模型幻覺、低估了 GT 抽取遺漏
所以我對 page_19 的處理是:
finish_reason=length 造成的💡Tip: 任何 OCR 或生成式任務的評測,第一件事不是看指標,是看
finish_reason。length代表輸出被截斷,這時候所有基於「完整輸出」假設的指標全部失效。我建議在 metrics 表裡直接加一欄finish_reason,讓它跟 CER 並排——這件事 Day 26 設計 JSONL schema 的時候會正式寫進欄位定義。
順帶一提,這也暴露了我 baseline 設定的一個問題:max_tokens=1500 對一張圖表頁太緊了。但我沒有改參數重跑
為什麼?因為改了就不能跟 Day 2 的 5 頁比較了。變數控制比單頁好看重要。 這個限制我寫進限制欄,不動實驗設定
Day 1 開賽的時候我列了一份繁中難點清單:字型變體、罕用字、直橫式混排、跨欄表格、繁簡混排、異體字對齊
今天兌現了罕用字跟跨欄表格,繁簡與異體字明天講
剩下兩項——字型變體和直橫式混排——我今天沒有實測數據,所以我要老實講為什麼,以及為什麼它們還是要留在清單上
「字型變體」在繁中 OCR 裡指的是同一個字在不同字型下的字形差異。最常見的三組:
| 字型類別 | 特徵 | 對 OCR 的影響 |
|---|---|---|
| 明體/宋體系 | 有襯線、橫細豎粗、筆畫末端有頓角 | 小字級時頓角會糊成一團,細橫畫可能消失 |
| 黑體/圓體系 | 無襯線、筆畫等粗 | 相對友善,但筆畫多的字容易糊成黑塊 |
| 楷體/仿宋 | 手寫感、筆畫連帶 | 最難,筆畫交界處的斷點不明確 |
還有一層更麻煩的:同一個 Unicode 碼位,在台灣標準字形與日本/香港字形下長得不一樣。「骨」的內部筆畫方向、「戶」的第一筆是點還是橫、「直」裡面是兩橫還是三橫——這些在不同地區的字型檔裡是真的不同
這對 OCR 的影響是:模型如果主要在某一種字形上訓練,換到另一種就會掉準確率。而台灣的財報文件混用的字型比你想像的多——封面用黑體、正文用明體、表格數字用等寬英數字型、附註用更小一級的細明體
那今天為什麼沒測?
因為這份簡報基本上只有一套字型系統。 我在做 footnote 評分的時候有抽 span 的 size 資訊,那個過程順便讓我確認了一件事:這份 PDF 的字級變化明顯,但字型家族變化不大。它是一份設計過的簡報,不是一份掃描件
要測字型變體,我需要的是掃描的 PDF,或至少是多字型混排的年報正文。 這份文件給不了,所以我不編數據
直排(由上到下、由右到左)在台灣的財報 PDF 裡確實存在,但出現的位置很特定:公文格式的函文、法院文書、部分契約書的騎縫註記、以及印章/關防旁邊的直式落款
法說會簡報是橫排文件,一頁直排都沒有
直橫混排難在哪?難在閱讀順序完全不同。橫排是左到右、上到下;直排是上到下、右到左。如果同一頁同時有兩種,模型必須先判斷每個文字區塊屬於哪一種排法,才能決定閱讀順序
而現在主流 VLM 的做法是直接把整頁圖丟進去讓模型自己排——閱讀順序是模型猜的,沒有任何約束。從 page_24 那個「三欄變三批」的結果你已經看到,橫排表格它就猜不對了,直排只會更糟
這條難點我留在清單上,標記為「未實測」。Day 18 做 Region Routing 的時候會需要一個排法判定,那時候如果我拿得到直排樣本,會補上
💡Tip: 我覺得技術文章最該避免的一件事,是為了讓清單看起來完整,去補一段沒有數據的內容。「我沒測到,原因是資料來源沒有這種樣本」是一個完全合格的結論——它甚至比硬掰一段更有用,因為它告訴讀者你的結論適用範圍到哪裡。Day 3 我也做過一次同樣的取捨:舊的 150 tok/s 沒記錄條件,我就整個不引用。
全部素材都在 experiments/ 底下,重現順序是:
# 1. 從 40 頁 PDF 算難度分數、選 4 頁、渲染 PNG + 抽文字層 GT、送 OCR
python step4_zh_hard_pages.py
# -> step4_page_selection.json (全 40 頁四維分數明細)
# -> pages/page_NN_<label>.png / .gt.txt
# -> raw_responses/page_NN_<label>.response.json / .model_output.txt
# -> step4_results.jsonl (含 finish_reason / tokens / 耗時)
# 2. 把 9 頁(Day2 的 5 頁 + 今天的 4 頁)做 diff 與錯誤分類
python step4_zh_error_taxonomy.py
# -> analysis/diffs/page_NN_<label>.diff.txt
# -> analysis/metrics_summary_step4.csv
# -> step4_zh_error_taxonomy.json / step4_zh_error_summary.md
兩個腳本有幾個刻意的設計,講一下理由:
第一,第二支腳本完全不重新呼叫模型。 它只讀已經落檔的 model_output.txt 去算 diff。這代表分類邏輯可以無限次重跑、隨時改規則,而不會產生新的推論成本、也不會因為模型非決定性輸出而讓數字對不上
第二,第一支腳本是從 step2_ocr_baseline.py 直接 import call_vision、ENDPOINT、MODEL、PROMPT 的,不是複製一份參數過來。複製參數是一種很容易出事的做法——你改了一邊忘了另一邊,兩批數據就不可比了
第三,新測 4 頁全部另存新檔名(step4_*、metrics_summary_step4.csv),沒有覆寫 Day 2 的產出。Day 2 那把尺要一直留著
💡Tip: 「推論與分析分離」這條原則我強烈建議你照抄:一支腳本負責發請求並把原始回應落檔,另一支腳本只讀檔做分析。這樣你改分析邏輯的時候不用重跑模型,而重跑模型的時候也不會不小心改到歷史數據。Day 25 的 diff-driven flywheel 整個架構就是建立在這個分離上。
把四頁的觀察整理成一張分類表。這張表是 Day 8 倒推設計的輸入:
| 難點 | 實測證據 | 失敗長相 | 用 CER 抓得到嗎? |
|---|---|---|---|
| 模型自加結構描述 | page_02 四個方位標籤、四頁全部都有敘述性開場白 | 輸出膨脹 2–3 倍,CER 暴衝 | 抓得到,但會蓋掉真正的錯誤 |
| 中英數交界處搬移 | page_05 的 NT$ 位置 |
語意正確、位置不同,甚至 GT 才是錯的 | 抓得到,但歸因會反 |
| 關鍵數字漏抽 | page_05 的「24個」 | 輸出更漂亮,但少一個總數 | 很難,1 個字在 183 字裡權重太低 |
| 形近字誤判 | page_24 的綠→線、阻→防、載→製 | 輸出通順且語意合理 | 抓得到,但你不會察覺要去抓 |
| 表格欄位對應垮掉 | page_24 三欄變三批 | 覆蓋率 0.94,配對正確率接近 0 | 抓不到 |
| 小字級誘發重複退化 | page_19 finish_reason=length |
一串文字重複上百次 | 抓得到,但要先看 finish_reason |
| GT 文字層本身漏抽 | page_02 底紋、page_19 圖表標籤 | 模型是對的,答案是錯的 | 抓不到,而且會反過來冤枉模型 |
七條裡有三條,現有的字元級指標要嘛抓不到、要嘛歸因是反的
這就是 Day 5 結尾那句「單一模型不夠」的下一層:不只模型不夠,連量尺都不夠。
finish_reason=length 造成的重複退化;而它的 hallucination 計數同時高估了幻覺、低估了 GT 漏抽dense_table 全場最高分只有 0.1429——真正的年報附註表格,我今天根本沒測到
字對了,不代表資料對了。
繁中 OCR 的失敗,大部分不長得像失敗。
還有一件事我今天刻意跳過沒講:統計表裡有三個類別的計數是 0——簡體字、異體字、形近字
形近字明明滿地都是(綠→線就是),為什麼計數是 0?
明天講。而且我要先告訴你結論:那個 0 不是好消息,是我的分類器不夠用的證據。
明天見 👋