iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
佛心分享-SideProject30

打造台語發音檢測系統:可憐大四生的專題實錄系列 第 7

Day 7|Unicode 危機:[UNK] 是什麼?NFC、NFD 又是什麼?怎麼沒人跟我說模型訓練還要管這些?

  • 分享至 

  • xImage
  •  

重新訓練 V3 的意外:[UNK] 現身

上一篇最後,我提到自己因為開始重新思考 CAPT 的本質,所以決定重新訓練 V3。

結果 V3 跑完之後,我在辨識結果裡看到了一個沒看過的東西 —— [UNK]

一開始看到它的時候,我其實覺得很奇怪。前面的訓練並沒有一直出現這個問題,為什麼到了 V3,結果突然開始出現 [UNK]

如果只是一般的聲母、韻母或聲調辨識錯誤,至少還可以繼續分析模型到底把哪個音辨識成哪個音。但 [UNK] 不一樣,它比較像是模型連這個東西都不知道要對應到什麼。

我當時第一個懷疑的其實不是 Unicode,而是前面才剛處理過的變調。因為這次的 V3 是在前面開始處理多音節資料之後重新訓練的,而多音節資料又牽涉到前面的音節要不要變調。如果變調程式產生了模型詞彙表裡不存在的字串,確實有可能讓後面的處理出現問題。

所以我沒有馬上重新訓練,而是先回頭檢查產生出來的檔名,看看是不是有一些不符合預期的臺羅。

都怪我沒檢查🙏。

變調規則的再次檢視

我在檢查 V3 的異常時,也回頭檢查前面多音節資料的變調處理,並重新整理自己當時寫進程式的變調規則:

這是當時實際做過的資料與程式檢查。整理的過程中,我也開始拿實際資料去比對,確認程式產生的變調結果是否符合我的預期。

然後……我檢查了一輪,[UNK] 還是在。

為什麼啦!到底怎麼回事啊!(懷疑人生)。

前面已經遇過 GPU 額度、搬回本機、硬碟空間、訓練時間,現在連資料的變調邏輯也重新檢查過了,結果這個 [UNK] 還是橫在眼前。只好繼續調查。

[UNK] 到底是什麼?

[UNK] 通常是 tokenizer 裡的 Unknown Token(未知詞元)。簡單來說,模型在處理文字時,不是直接把整串文字丟進去,而是會先經過 tokenizer,把文字切成模型詞彙表裡可以辨識的 token,再轉換成模型可以處理的 token ID。

例如,假設 tokenizer 的詞彙表裡有:abcá,那麼遇到這些字元時,就可以找到對應的 token。

但當時我看到模型輸出 [UNK],還不能直接確定問題究竟發生在「Label → Tokenizer」這一段,還是模型最後真的預測出了 [UNK]。所以我只能先把它當成一個異常訊號,繼續往前追查。

Unicode 的幕後黑手:NFC 與 NFD

到這邊我就疑惑,怎麼會出現 [UNK],到底是哪裡辨識不了,明明音檔名看起來好好的。後來我又遇到兩個完全沒聽過的東西:NFCNFD。它們其實都是 Unicode 的「正規化(Normalization)」形式。

為了確認問題,我當時其實寫了幾支不同的檢查程式。先檢查 tokenizer 的詞表,再掃描整個訓練集的檔名,最後才把看起來一樣的台羅字串送進 tokenizer,比較它們真正得到的 Token ID。

那為什麼文字需要正規化?因為 Unicode 裡,有些文字可以用不只一種方式表示。最常見的情況,就是一個「帶有符號的字元」,可以直接使用一個已經組合好的字元,也可以拆成「基本字元+組合符號」。

例如 á,它可以直接表示成一個單獨字元:

  • á (U+00E1)

也可以表示成拆解的狀態:

  • a + ◌́ (U+0061 + U+0301)

人眼看到這兩種寫法,基本上都會認為它們是 á。但對電腦來說,底層的 Unicode code point 序列並不一樣。

其中,我這次最需要理解的是 NFC 和 NFD:

  • NFC(Normalization Form C):可以把它理解成**「組合版」**。它會盡可能把基本字元和組合符號組回一個字元:
    a + ◌́NFCá

  • NFD(Normalization Form D):可以把它理解成**「拆解版」**,把已經組合好的字元拆成基本字元和組合符號:
    áNFDa + ◌́

所以可以先簡單記成:

  • NFC:盡可能組起來
  • NFD:拆開來

這時候我才發現,**「看起來一樣」跟「電腦底層完全一樣」其實是兩回事。**而這件事情對我的台羅資料特別重要,因為台羅裡有大量聲調符號。如果同一個字的聲調符號有不同 Unicode 表示方式,模型前面的 tokenizer 就可能收到不同的字元序列。

數據會說話:Token ID 的不一致

我實際檢查資料後,真的發現了這個問題。例如實驗中,我拿 進行 Unicode 分析,得到:

  • NFCU+0074 U+00E1
  • NFDU+0074 U+0061 U+0301

更重要的是,我進一步把它們送進 tokenizer,得到的 Token ID 也不一樣。這個結果讓我第一次很直觀地看到,人眼看到的「同一個字」,不代表 tokenizer 看到的是同一個輸入。

在不同 Unicode representation 下,最後得到的 Token ID 序列也不同:

  • NFC Token IDs[25, 28]
  • NFD Token IDs[25, 11, 60]

這些檢查不是我一開始就規劃好的流程,而是看到 [UNK] 之後,一支一支補出來的。先確認詞表,再看整批資料,最後才找到這個肉眼看不見的差異。

如果你也想親眼看看這個 Bug 長什麼樣子,我把這段驗證整理成簡化後的公開重現版,放在 GitHub 的 Day 7 資料夾中。

你只要利用 Python 內建的 unicodedata 模組,搭配 Hugging Face 的 Tokenizer,就能自己比較 NFC、NFD 和 Token ID 的差異:

GitHub 實作範例https://github.com/yifyu1122/taiwanese-asr-lab

這個小實驗不會直接證明所有 [UNK] 都是 Unicode 造成的,但至少讓我確認了一件事:Unicode representation 並不是單純的文字顯示問題,它真的會改變 tokenizer 收到的輸入。

而我的資料檢查也顯示,這不是只有一兩筆特殊資料:在當時統計的 9,791 個音檔中,NFC 與 NFD 的資料都有出現,並且有 1,545 筆資料被判定需要進行 NFC 修正。

結論與伏筆

結果找到 Unicode 之後,問題就算解決了嗎?好像也沒有那麼快。

我知道資料裡有不同的 Unicode 表示方式,也知道 tokenizer 真的會把它們看成不同的輸入,但接下來要怎麼修正、修正之後模型會不會變好,還要重新整理資料再驗證一次。

剩下的結果,我們明天再看。

我是微微,大家再見!

參考資料

  1. Unicode Consortium, Unicode Standard Annex #15: Unicode Normalization Forms
    https://www.unicode.org/reports/tr15/
  2. Unicode Consortium, Unicode Glossary — Normalization Form C / D
    https://www.unicode.org/glossary/
  3. Unicode Consortium, Unicode FAQ — Normalization
    https://www.unicode.org/faq/normalization.html

上一篇
Day 6|都整理出一萬多筆資料了,結果你模型越學越爛(╬◣д◢)
下一篇
Day 8|Codex開大,但是全程做白工(´థ౪థ)
系列文
打造台語發音檢測系統:可憐大四生的專題實錄11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言