上一篇講到,我最後選擇從教育部《臺灣台語常用詞辭典》取得台語詞彙與語音資料。但真正把資料下載下來之後,我才發現,有資料跟資料可以直接拿來用,是兩回事。
歡迎來到資料清洗地獄 ( ´•︵•` )
這是半年前我第一次整理資料時遇到的問題。當時我拿到的東西,大致上就是 kautian.ods 加上大量 WAV 音檔,但這些東西根本還沒有接在一起。
所以 Day 3 就來講講,我是怎麼把字典和 WAV 檔配對起來的。
kautian.ods教育部《臺灣台語常用詞辭典》的相關資源中,提供了辭典文字資料與詞條音檔。
其中一個我後來大量使用的檔案,就是 kautian.ods。我把它當成資料索引表。裡面有辭典的詞條資訊,也有我後面需要用來連結語音資料的欄位,例如羅馬字與音檔相關資訊。

如果你以為下載完資料之後,資料夾裡會直接出現:
kám.wav
káu.wav
tshia̍h.wav
那你想得太美好了。
我當時實際拿到的是按照音檔編號分散在不同資料夾裡的 WAV。

下一步工作到這裡已經很清晰了。要怎麼把它們提取出來呢?
我初步的想法是按照臺羅拼音符號表,把單音節分類並重新命名,方便後續查詢。因此我開始按照臺羅的音節結構建立資料夾,分成:

接下來就開始寫 Python 啦!
move.py它做的事情非常單純:

雖然過程看起來很簡單,但是細節可不少,做錯一步就要重來。
大家觀察前面的分類表應該會發現,臺羅的韻母有不同的組合形式。因此在用羅馬字進行篩選時,不能只看單一字母。
例如我要找 i,如果直接用「以 i 結尾」來篩選,i、ia、iu、io 都可能被一起抓進來。我一開始沒有注意到這個問題,等分類結果跑完才發現資料混在一起,只好回頭修改篩選條件,再重新整理一次。
找到符合條件的資料後,程式會從 ODS 取得對應的音檔編號。由於 ODS 裡的編號可能帶有 (1),而實際下載下來的 WAV 也使用這種命名方式,因此還需要依照原始資料的命名規則處理。我也是一開始忘記有 (1)。
找到音檔後,再把它重新命名成對應的臺羅,例如 kám.wav,並複製到前面建立好的分類資料夾。
跑的過程還發生靈異事件:log 顯示複製失敗,但是實際上已經複製完了。
接下來,我開始測試原本打算使用的 wav2vec2-large-xls-r-300m-tsm-asr-v6。
在模型本身的驗證結果中,Validation WER 是 0.666。
這裡的 WER(Word Error Rate)是 ASR 很常見的評估指標,用來衡量辨識結果和標準答案之間有多少錯誤。它會考慮三種情況:替換(Substitution)、刪除(Deletion)以及插入(Insertion),計算方式是:
WER = (S + D + I) / N
其中 N 是標準答案的詞數。
所以 WER 越低越好,0 代表完全沒有錯誤。
不過,我自己的測試並沒有直接使用 WER,而是另外建立了字典單音節的 Exact Match Accuracy。也就是只有當模型輸出的整個臺羅音節完全符合標準答案時,才算辨識正確。
在我的測試條件下,Exact Match Accuracy 只有 56.78%。
這兩個數字不能直接互換,因為它們衡量的是不同東西。WER 是以錯誤率的方式衡量整體辨識結果,而我的 Exact Match 是直接判斷這一個單音節到底有沒有完全辨識正確。
我後來進一步用自己寫的 assessment.py 把辨識結果拆成聲母、韻腹、韻尾與聲調,開始分析模型到底錯在哪裡。程式會把標準答案與模型輸出拆成這些音節組件後再比較。
這樣分析之後,我發現錯誤並不是平均分布在所有音節上,而是會集中在某些特徵。
首先很明顯的是聲調混淆。例如第 1 調辨識成第 7 調有 246 筆,第 8 調辨識成第 4 調有 151 筆,第 7 調辨識成第 1 調也有 137 筆。這代表模型在辨識聲調時,並不是單純隨機猜錯,而是存在特定的混淆方向。
聲母也出現類似的情況,例如 kh → th 有 14 筆、t → p 有 14 筆。這些結果讓我開始注意到,模型可能不是「整個音節都辨識錯」,而是在某些音節組件上特別容易出現混淆。
韻尾的情況則更加明顯,其中 h → 空 出現了 169 筆。也就是說,有一部分原本帶有 -h 韻尾的音節,模型輸出時直接沒有辨識出這個韻尾。
這些統計結果讓我第一次比較具體地看到這個模型的弱點:錯誤其實有規律,而且不同的音節組件有不同的問題。
所以接下來,我沒有只停在錯誤統計,而是挑出實際的音節進一步分析。
前面的錯誤統計讓我注意到,kh 這類聲母存在明顯的混淆。因此我挑了 khà 和 kà 這兩個音節,進一步看看模型到底是怎麼辨識它們的。
我先把標準 khà、標準 kà,以及一段學生錄音送進模型。單純看最後的 ASR 結果,三者的結果其實就已經很有意思:
標準 khà 被辨識成 tha-,標準 kà 被辨識成 kà-ah,反而是學生錄音辨識成了 khà。
但如果只看最後輸出的文字,我還是很難知道模型到底在哪裡判斷錯了。於是我決定不要只看模型最後吐出的答案,而是往模型裡面看。
Wav2Vec2 在處理音訊的過程中,會經過多層神經網路,每一層都會產生一組 hidden states。這些數值不是模型直接告訴我的「這個音是 kh」或「這個音是 k」,而是模型在處理音訊時形成的內部表徵。
我在載入模型時開啟 output_hidden_states=True,就可以取得這些中間結果:
outputs = model(**inputs)
ai_features = outputs.hidden_states[-1]
這裡的 hidden_states[-1] 就是模型最後一層的 hidden states。
它不是一個單獨的數字,而是會隨著音訊時間變化的一組向量。為了先做簡單的整體比較,我把整段音訊的時間軸取平均,讓每個音檔最後都變成一個固定長度的特徵向量。
接下來的問題就變成:拿到這些向量之後,要怎麼比較兩段音訊在模型裡的表徵是不是接近?
Cosine Similarity(餘弦相似度)是一種比較兩個向量方向有多接近的方法。
假設有兩個向量 A 和 B:
A:學生錄音經過模型後得到的特徵向量
B:標準音檔經過模型後得到的特徵向量
計算方式是:
Cosine Similarity = A · B
───────
|A| |B|
就是先算兩個向量的內積,再除以兩個向量各自的長度。結果越接近 1,代表兩個向量的方向越接近。
所以這次我拿學生錄音的向量,分別和標準 khà、標準 kà 的向量做比較。
結果是:
學生錄音與標準 khà:0.9395
學生錄音與標準 kà:0.9587
也就是說,按照這次實驗的比較方式,學生錄音的模型表徵反而更接近標準 kà。
這個結果沒有直接告訴我學生的 kh 發得對不對,反而讓我發現一件事:模型最後的 ASR 結果,和它內部的特徵表徵,並不一定能直接拿來解釋成我們想要的語音學特徵。
既然整段音訊壓成一個向量之後,還是看不出模型到底在哪個時間點做出判斷,我決定再把時間軸拆開來看。
我把模型的最後輸出再往下拆。Wav2Vec2 的 logits 是一個隨著時間變化的矩陣,每個時間步都有一組對應各個 token 的分數。我用 argmax 找出每個時間點分數最高的 token,再把 token ID 轉回臺羅字母。
logits = model(**inputs).logits[0]
predicted_ids = torch.argmax(logits, dim=-1).tolist()
tokens = processor.tokenizer.convert_ids_to_tokens(predicted_ids)
接著根據音檔長度與模型產生的時間步數,估算每個 frame 對應的時間位置,最後把 token 和時間點一起輸出。
time_per_frame = duration / total_frames
for idx, token in enumerate(tokens):
current_time_ms = idx * time_per_frame * 1000
這樣得到的就不只是最後一句 ASR 結果,而是可以看到模型在不同時間點分別偏向哪些 token。
例如學生錄音的結果中,可以看到:
0.0 ms → k
363.8 ms → à
525.4 ms → -
606.3 ms → a
687.1 ms → h
這兩個實驗我也整理成 GitHub 裡的 Day 3 範例程式,有興趣的話可以自己準備一個音檔跑跑看,看看模型處理音訊後會留下什麼樣的 Hidden States,以及不同時間點會偏向哪些 token。
→ GitHub:https://github.com/yifyu1122/taiwanese-asr-lab
結果確實出來了,但我看著這些時間點,還是很難回答原本的問題。
我知道模型在什麼時間點偏向 k、h 或 à,卻不知道這些 token 背後對應的聲音究竟發生了什麼變化。換句話說,我看到的是模型的判斷結果,而不是聲音本身。
這也是我在 review 時被指出來的問題:如果我要研究 kh 和 k 這種實際的發音差異,應該回到原始音訊本身,而不是一直從 ASR 模型的輸出往下拆。
老師當時建議我可以進一步輸出頻譜圖(Spectrogram),直接從聲音的頻率分布觀察差異。
不過,這個方向在當時並沒有真的做完。
因為同一次 review 中,老師更直接指出了前面的 baseline 結果:我用字典單音節測試得到的 Exact Match Accuracy 只有 56.78%。對當時的目標來說,這個結果太低了。
老師給我的選項很明確:換模型,或是 fine-tuning。
於是我原本先分析現成模型的弱點,再想辦法補足的規劃,在這裡出現了轉折。最後我選擇不換模型,而是開始嘗試 fine-tuning。
至於接下來會不會真的能用這些資訊判斷發音,當時的我還沒有答案。那時候我只知道,原本的模型表現不夠好,必須先想辦法讓它學會我真正想處理的台語資料。
今天的分享就到這裡,我是微微,我們明天見!