chiaki.ch
有一種軟體,你每天使喚它數千次,卻幾乎不會意識到它的存在──直到它壞掉為止。輸入法就是這種東西。
Yahoo! 奇摩輸入法是一個傳奇。它在 2013 年停止維護,到今天已經十三個年頭,卻還有人(對,包括我)在網路上翻找它的安裝檔;網友一路傳承、修補它最後一次釋出的版本,硬是讓它在 macOS 上多撐了十幾年。但天下無不散的筵席。某天早晨,剛更新完的 Mac 右上角跳出一則來自蘋果的最後通牒:Intel 版 app 的支援即將結束,x86 程式將在 macOS 27 停止執行。
大家其實都心裡有數,這個早該安息的輸入法終將離開。抱著不捨的心情,我想,既然 Yahoo 都大方的開源了,那不如重拾多年沒碰的 C++,看看能不能讓它再活一次。
這篇文章記錄的,就是這幾個月發生的事。前半段是數位考古,後半段逐漸變成一個語言學問題。因為我很快就發現,讓輸入法「跑起來」只是入場券,真正決定一個注音輸入法好不好用的,是它背後的詞庫。
打開 GitHub 一查,才發現早就有人動過手了。一個 vChewing/KeyKey-Boneyard 的封存庫,裡面對 Yahoo 奇摩輸入法在 2012 年開源出來的原始碼已經進行過大量的整理,我決定從這邊開始著手。裡面資料夾名稱像 Loaders/OSX-TSM、Loaders/Windows-IMM、Frameworks/OpenVanilla,每一個都充滿年代感。這份倉庫其實就是唯音輸入法的開發者,嘗試修復過的,整理了偏好設定與詞庫編輯器的原始碼,但輸入法主體從來沒有現代化過。README 甚至寫著這是一座「廢件處理廠」,並且聲明沒有人保證這東西會動。
vChewing/KeyKey-Boneyard
Loaders/OSX-TSM
Loaders/Windows-IMM
Frameworks/OpenVanilla
我心一橫,決定接手。
第一天就是不折不扣的震撼教育,專案的入口根本不存在,.xcworkspace 打開來是空的。發送按鍵的程式碼還在使用 Carbon 時代的 Str255 這類早已成為化石的型別。而這個輸入法在沒有辭典資料庫的情況下,會直接崩潰。程式碼寫死認定一定存在一個叫 SmartMandarin 的輸入法,那段「找不到就換下一個」的容錯迴圈整段被註解掉,不知道封印了多久。
.xcworkspace
Str255
SmartMandarin
修完這一輪,程式終於願意在沒有資料庫的情況下體面地「什麼都不做」,而不是當場倒下。同一天下午,我用 Ruby 從頭寫了一支資料庫產生器,直接從注音表組出一份最小可用的詞庫,不再依賴 Yahoo 當年的搜尋語料、中研院語料庫,也不需要商用的加密 SQLite 擴充。「詞庫」這件事,第一次徹底與原廠脫鉤。
接下來的一週像是考古挖掘現場。輸入法當年有一個 Yahoo 快捷鍵,可以直接搜尋或者查東西的「OneKey」功能,它的設定檔列出的服務清單,活脫脫是一份 2012 年台灣網路生態的切片:Yahoo 奇摩搜尋、股票查詢、無名小站、Yahoo 拍賣、Yahoo 地圖。再往下挖,有一整套 Windows 安裝程式工具鏈,甚至還有 Borland C++Builder 5 的專案檔;有替 Mac OS X 10.4 Tiger 寫的 Text Services Manager 實作;有「上傳資料庫到 iDisk」的按鈕,iDisk 是蘋果 iCloud 的前身,MobileMe 時代的雲端服務,2012 年就熄燈了。
把行李箱裡的石頭一塊塊倒出來之後,我把專案命名為 ChiaKey(千秋輸入法),並且把專案架構與路線圖定出來。詞庫獨立成姊妹庫 ChiaKey-Lexicon,透過 GitHub Releases 發佈,附上簽章與校驗碼;輸入法本體只負責執行期的契約、驗證與安裝。
中間還有一天一口氣修了十五個資安漏洞。沒有檢查 macOS 的「安全輸入」狀態;把使用者字串直接丟給 system() 執行的 shell injection 破口;差一個位元組的 stack buffer overflow;還有一條透過古老的 Distributed Objects 暴露出來、任何行程都能遠端塞按鍵進去的 IPC 通道。也順手替核心引擎裡兩個負責展開所有切法的遞迴函式加上記憶化。原本句子每多一個字,重複展開的工作量便呈指數增長,因此偏好設定裡建議大家組字區域不要開太大,但改完之後,長句與短句一樣流暢。
system()
這些工程細節可以再寫好幾篇,但我想把篇幅留給後面更有意思的事。當程式不再崩潰、修好了 Bug、打起來也不卡了,我開始認真用它打字,然後很快遇到所有注音使用者都熟悉的那種懊惱:
打「天意難測」,出來的是「天意南側」。
注音輸入法的核心,是把一串讀音變成一串字。這件事需要三種資料:注音對字的對照表、每個詞有多常用(術語叫 unigram),以及詞與詞怎麼銜接(bigram)。
繁體中文的開源詞庫資源其實不少。新酷音(libchewing)的 tsi.csv 是「詞組、頻率、注音」,Rime 中州韻共享的 essay.txt 是「詞、頻率」。但仔細看會發現,它們幾乎都是同一種形式:一個詞、一個數字。這種資料能告訴你「哪個詞比較常用」,卻無法描述「打完 A 之後,接 C 是不是比接 B 更合理」。
tsi.csv
essay.txt
而同音歧義恰恰全靠這件事。「難測」與「南側」單獨看都是正常的詞,誰比較常用其實差不了多少;真正能分出勝負的,是前面那個「天意」。台灣的開源注音生態,長期缺少這種可用的接續關係資料。原因也很現實:n-gram 權重表需要語料清洗、機率計算、模型編譯,不像傳統詞庫那樣改改文字檔就能持續更新。
Yahoo 輸入法在當年轟動一時,就是因為他們實際上做出了這份資料。據網路上的討論以及官方留下來的說明,他們用了自己搜尋引擎的熱門關鍵詞,以及購買了中研院的平衡語料庫來計算前後詞機率。或許是這個原因,這兩者都沒有一起跟著輸入法開源釋出。但就算能用,這份詞庫也停留在 2012 年,而且授權問題難以解決,沒辦法跟著開源版一起走。所以要嘛沒有 bigram,要嘛自己做一份。
在動手做資料之前,得先搞清楚引擎究竟怎麼使用這些數字。ChiaKey 的核心引擎(Manjusri/Gramambular 系)把一串注音切成一格一格的候選詞,然後尋找一條總分最高的路徑。這裡有幾條規則,決定了後面所有的研究方向。
分數是以 10 為底的 log 機率,全部是負值,越接近 0 越常用。相加就等於機率相乘。
每個詞節點會多拿一份「詞長加分」:每多一個音節加 1.0。這個數字相當可觀。四字詞當一個節點,比拆成兩個二字詞多出整整 1.0,遠大於多數 bigram 能提供的差距。換句話說,bigram 只有在競爭路徑的節點數相同時,才真的有影響力。
bigram 與 unigram 之間是「取最高分者」,不是相加。而且理論上應該打折的退避權重(Katz back-off),在整個資料庫裡全是 0。所以一筆 bigram 要生效,必須硬贏過那個讀音本來排第一的候選。
還有一個很容易被忽略的細節:同一組「讀音+前文」底下如果掛了許多筆 bigram,引擎只看第一筆。第二名以後的資料永遠不會被讀到。
弄懂這幾條,就能反推出一個判準:一筆 bigram 要真的有用,它的後文這個詞,必須在使用者實際會打的那個讀音上,原本是輸給同音對手的。如果它本來就是第一名,這筆資料不會改變任何結果;如果它綁在一個罕用讀音上,使用者永遠不會觸發它。這個判準後來成為整個研究的骨幹,不過我是繞了兩條錯路才走到這裡。
第一個想法很直覺,既然缺語料,那請大語言模型生成呢?從使用條款來看,大部分的聊天機器人,例如 ChatGPT、Claude、Gemini 等,都明確放棄對生成內容的權利,授權上是乾淨的。加上正好碰到 Tibo 的 codex 用量大放送,決定先嘗試用 GPT-5.5 以及本機模型 Gemma 4(26B-A4B,跑在 MLX 上),搭配 prompt 產生器,指示 AI 撰寫或生成各種語境的台灣繁體中文短文,斷詞之後統計哪些詞常接在哪些詞後面。
跑了一陣子,出來的結果可以說是不上不下。在多次 prompt 產生器的調整後,總算能指示 AI 寫出有用的文本,但在小規模測試 30.5 萬個漢字的生成文本,只能產出約 2,000 個真正落在「同音撞碼」位置的配對,長尾頻率的估計雜訊極大。按實測速率,要達到可用規模得連續生成好幾週。更麻煩的是,模型有自己的口癖,比如 prompt 要求模型用「新聞語氣」寫作,AI 就會高機率的寫出「相關單位」這個詞,在相關的 381 篇文章裡出現了 62 篇。直接計數發現,模型的偏好與實際自然語言的文本頻率並不相同。
以這條路做出來的合成語料層(當時稱作 chiaki-synthetic-overlay)有 46,822 列 bigram,勉強能用,但拿前面的判準回頭檢視,只有 11.6% 落在能改變結果的位置。其餘 88.4%,要嘛那個讀音只有一個候選,要嘛那個詞本來就是第一名,也就是模型會反覆產出最順口的搭配,而大部分的組合本來就是列表中的首選詞,幫助有限。
chiaki-synthetic-overlay
仔細思考之後,我意識到我最需要的東西,其實是同音詞的前後文,單純拼字數純粹是搞錯方向了。既然哪些詞會撞碼,是可以從詞庫直接列舉的封閉集合(335,427 個詞條裡,在自己常用讀音上會輸給同音對手的有 16.4%,55,170 個),那何不對每一個同音詞,批次請模型回答「最常出現在它前面的是哪些詞」,而且 Gemma 4 是運作在本機的開源模型,其實我可以直接讀取他內部對該詞的 logprob 當做機率。也就是直接從內部問模型「難測」跟「南側」這些 token,前面通常會接哪些詞。
這個方法效率高得驚人:12 個詞 9 秒,一個晚上就把全部 29,221 個高頻撞碼詞跑完,產出 20,092 筆通過驗證的配對。
並且為了驗證成果,我又寫了一套評估工具。評估工具建好一測,這條路直接宣告失敗。那批資料在 32.6 萬句真實測試語料裡,只命中了約 200 次。
原因我猜測可能是本地模型太小,對詞彙的創意度很低,又或者模型只擅長往後猜,而不擅長往前猜。也可能是模型其實也並不熟悉台灣人寫出來的句子。模型可以列舉撞碼詞,卻不曉得真實用法。判別式驗證能確認「給定這個前文,這個詞是否勝過同音對手」,卻無法確認「這個前文到底會不會出現」。
好在這幾天的產物並非白費,為了幫模型打分做的評估工具,對於之後的品質驗證起到了非常大的作用。
放棄讓模型憑空發明之後,要做的事變成了尋找真實的、台灣人寫的、授權允許使用的文本。
最後湊出來的語料有五種來源,合計超過 4.6 億字、約 3.3 億個漢字、六百七十多萬行。政府機關新聞發布(行政院、陸委會、中研院、客委會、新北市政府,依政府資料開放授權條款釋出);立法院公報的詢答逐字稿(這是少數同時具備「真實即席口語」「台灣本土語境」「授權可再散布」三個條件的語料,而且幾乎沒有錯字);PTT 與 Plurk 的公開貼文;以及正體中文轉換後的維基百科條文。
接著設計一套系統,在確保原始文本不落地、確保隱私的同時,萃取文本的詞庫列與權重機率。
從語料到詞庫列,中間有十一個步驟,大多是可預期的清洗工作:切句、斷詞、統計相鄰詞對、把「前後文本身就是一個詞」的配對挪到 unigram 層、設門檻。真正值得記一筆的是這幾步:
第一是斷詞器必須跟引擎用同一套規則,否則統計出來的東西,引擎根本不會照著做。
第二是撞碼要先篩過。後文如果在它最常用的讀音上本來就贏得了同音對手,這筆資料就沒意義,直接丟。這一步刷掉了五百多萬筆,最後留下的 394,267 列全部落在真正的撞碼位置上(相較之下,前面 AI 合成語料只有 11.6% 是這樣)。
第三是若跟語料本身作對,高機率會是錯的。如果同一個前文後面,另一個同音詞在真實文本裡出現得更頻繁,這筆資料就是在幫倒忙,得刪掉。這一步的效果落差很大,在正式書面語域大概能拉出三成五的淨值;但在朋友聊天的訊息語域,它是「有效」跟「完全沒用」的分界線,不做這項檢查,修對的和搶錯的幾乎打平(淨值只有 +34);做了之後,淨值衝到 +4,061。
還有一個藏得很深的陷阱:「電影裡」是詞典裡的標準寫法,斷詞器會把它併成一個詞,「電影→裡」這個配對在語料裡的計數永遠是 0;但「電影裏」這種非標準寫法會被拆成兩個字分開算,計數反而不是 0。兩邊一比,系統就會學到「裏比裡對」──等於系統性地鼓勵錯字。抽樣一查,抓出來的全是錯字、日文字形、簡體字形。修好之後,刪掉了 10,641 筆這樣的髒資料。
其餘像「的/地/得」要靠聲調分流、「立法委員」後面接的姓氏純粹是誰發言多而非語言搭配,這類清洗規則還有好幾條,但道理都類似。手動找出資料裡「看起來像規律,其實是雜訊」的地方,一條條擋掉。
詞庫工作最容易犯的錯,是拿「加了幾萬筆資料」當成進度。所以整個專案最重要的產物,其實不是那 39 萬列 bigram,而是量測方法。
做法是這樣:在真實文本上斷詞,逐個相鄰詞對判定「沒有 bigram 的時候,引擎會不會選錯」。這些位置叫可修位置。然後看一個 bigram 層修對了幾個位置,又把幾個原本正確的位置搶錯。評估會重播正式版的校準公式,只計入確定會生效的資料列。
測試集分四個語域:新北市政府新聞(書面)、立法院公報一個保留會期(正式口語)、PTT 語料的 5%(涵蓋 169 個看板),以及我和朋友的群組對話(訊息)。
單一語域的評估會很有系統地哄騙你,這一層的前一版只用政府新聞與立法院公報訓練,在書面語域淨值 +85,128,看起來非常漂亮,加入真實訊息語域之後才發現,同一份資料在朋友聊天的語域淨值只有 +65:修對 1,603、搶錯 1,538,幾乎完全抵銷。加入 PTT 語料重做之後,訊息語域提升了大約 65 倍。
評估工具也經過了數次的迭代,每一次都修正了成果的高估。
第一個問題是,資料被綁到沒人會打的讀音上。初版邏輯是挑「權重落差最大的讀音」來配對,理由聽起來合理:落差越大越需要幫助。結果它幾乎每次都選中一個詞最冷僻的讀音,例如「的」被綁到誰都不會打的 ㄉㄧˋ,而不是常用的 ㄉㄜ˙。初版 93,952 筆資料裡,有 68.5% 都綁錯了地方,等於白做。
而且一開始的評估只比對文字,沒管讀音、也沒管實際會不會生效。等於預設一筆 bigram 在任何讀音下都有效、而且必勝。修正之後,光是書面語域「真的可能被改對」的位置,就從 184 萬筆腰斬到 95 萬筆,原本一半以上其實都是假象。
第三個問題就是前面提過的,只用單一語域測試,數字好看,換一個語域立刻現形。
還有一個更低級的 bug:斷詞的詞長加成。我原本寫的是「每個字 +1.0」,聽起來跟引擎的「每多一個音節 +1.0」差不多,但兩者其實完全不同,這個加分規則等於沒在運作,系統只是單純選權重總和最大的切法,跟真正偏好長詞的引擎完全對不上。有 55.1% 的句子,這兩種規則會切出不同答案;當時已經出貨的 29 萬筆資料裡,有 5,558 筆落在引擎根本不會產生的詞界上,永遠不會被用到。修好重跑之後,口語語域逐句正確率 +2.07、論壇 +0.88、Plurk +0.75。
還有一條規則一直讓我很在意,為了方便,一開始我設定「前文如果是多音字,就整筆棄用」,理由是它的讀音會變成猜測。這條規則棄掉的量比留下的還多,一百多萬筆,而且棄掉的正是最有價值的那批。被丟掉的配對裡最常見的前文,是「的」(2,756 對)、「在」(1,119)、「和」(1,026)、「與」、「有」這些高頻虛詞。
我嘗試保留,並且把多音字綁到它自己權重最高的讀音,與後文採用同一個定義。意外的效果並不差,四個語域全部提升,列數從 293,853 增到 360,016,訊息語域的修對率從 13.5% 升到 18.4%。我也嘗試過人工審核,但發現靠人力審核讀音,一個晚上最多也只能看個 1000 條左右,也不是個長久的辦法。
做到這裡,我發現 bigram 要好,首要條件是單詞詞庫要好。許多問題是 bigram 表救不了的詞庫問題。
早期有一層專門從立法院公報做的 bigram(tw-ly-transcript),經過人工複核。它有個頗為反直覺的性質:訊號最強的搭配,恰好是最沒價值的部分。公報裡文件頻率最高的配對是「條文+通過」「所以+本席」「本席+要」,全是議場專用語。更危險的是「異議/意義」「議事/意識」這類:議場偏好前者,日常打字幾乎全是後者,語料的偏好會直接傷到高頻日常詞。而且這種判斷無法從權重落差導出。「議事/意識」落差 0.759 應該移除。差別在語義語域,不在頻率結構,只能逐一點名。
tw-ly-transcript
引擎端的 gold set 基準線揭露了一件事:幾乎沒有斷詞層級的錯誤(輸出與期望長度不同的只有 1 句),問題全在同讀音候選選錯。最大的一群是句末助詞輸給同音實詞:啊→阿、啦→拉、嘛→嗎,合計 7,204 次。
「啊」在 ㄚ 這個讀音上的權重是 −1.673,輸給「阿」的 −1.015。翻過來之後,啊→阿 的錯誤從 3,100 次降到 3 次,代價是阿→啊 多了 12 次。因為「阿」幾乎總是出現在「阿姨」「阿公」「阿拉伯」裡,那些以整詞路徑勝出,不受單字排頭影響。口語語域逐句正確率因此 +2.37。
但這件事有方法論上的教訓。翻轉單字排頭是雙向的,現在的排頭永遠不會錯,翻過去之後換它每次都錯。同一份稽核清單裡翻轉成本最低的兩組(嘛/嗎 只差 0.000008),淨值其實是負的,因為「嗎」在語料裡比「嘛」常用四倍以上。所以判準不是「翻轉多便宜」,而是「哪一邊才是使用者想要的」。
詞庫的問題不見得都是「讀音錯了」,更常見的是「讀音沒錯,但這詞根本沒人在打」。
新酷音的詞庫裡,「宏碩」收了 ㄏㄨㄥˊ ㄕˊ 這個讀音,查教育部《重編國語辭典修訂本》,這讀音有典可循,完全正確。問題是這是一千多年前的文言用法,在此之前我也一直以為這個字唸「ㄕㄨㄛˋ」。麻煩的是,它剛好跟常用詞「紅石」撞在同一個讀音上,而詞典收錄時給的權重,並沒有把「這個詞有多古老、現在還有多少人在用」算進去。結果就是打 ㄏㄨㄥˊ ㄕˊ 的時候,現代理應更常出現的「紅石」被一個文言詞卡到後面去。
這類「詞典裡查得到、但跟現代人打字習慣脫節」的狀況,是拿一支稽核工具比對出來的,把資料庫裡每一個兩字以上的詞拆開,比對每個字單獨查得到的讀音,找出「詞的讀音」跟「字的讀音」對不上的地方。這支工具篩出來的,多半不是這種「太古典」,就是像地名「栃木」被硬套成漢字讀音、或「俾倪」其實該是「睥睨」的錯字,這些多半也回頭改不了上游,只能新增一層校正層,把它們的權重壓低。
工具還帶出另一種讀音與字典脫節的發現,「咖哩」在教育部辭典的標準讀音為一聲的 ㄎㄚ ㄌㄧˇ(kā lǐ),但「咖」字常被念為「ㄍㄚ」,「好萊塢」的「塢 ㄨˋ」實際上大家都唸 ㄨ,這些台灣人實際會唸的音,單字讀音表裡卻沒有收,查得到這個詞,卻查不到這個字單獨的這個讀音,一樣會出錯。
另一個我使用的來源是 Rime 中州韻共享的 essay.txt,他有非常豐富的詞頻資料。它本身應該是簡體中文語料轉換過來的,而「轉對」跟「轉錯」的界線,比想像中細。
像是「想象」就是一個棘手的例子。「象」這個字大部分時候是正字,「象形」「景象」「形象」都不該被動。但語料裡「想象」這個詞的統計頻率是兩萬多次,實際上應該指向「想像」才對。但如果照單字規則把「象」整個轉成「像」,會連累「景象」「形象」全部變成錯字,只能寫一條專門針對「想象」的規則,繞過所有其他「象」。
另一個例子是「粘」跟「黏」。essay.txt 裡「粘土」出現 802 次,比「黏土」的 577 次還多,「粘」這個字整體出現的次數也是「黏」的兩倍以上。但教育部標準用字裡,「黏著」這個意思該寫成「黏」,「粘」是它的異體字,唯一該獨立存在的用法只剩姓氏(讀音也不同,姓氏念 ㄓㄢ,不是「黏著」的 ㄋㄧㄢˊ)。如果照語料的次數排列,「粘土」這種寫法反而會排到「黏土」前面。
最麻煩的是「台灣」的「台」。essay.txt 裡含「臺」字的詞條有 1,308 個,含「台」字的卻只有 36 個,上游幾乎全部寫「臺」,但我目前的政策是,預設採用「台」作為優先,若使用者習慣打「臺」,交由自動學習層來更正。
有一個更奇妙的發現,引擎撈候選時用的是穩定排序,權重完全相同的候選會維持輸入順序,而輸入順序就是資料列在 SQLite 檔案裡的實體位置。實測同讀音相鄰候選對 68,684 組中,有 12,003 組權重一模一樣。典型案例是「啦/拉」(同為 −1.198788)。順序一翻,「就好了啦」變成「就好了拉」,上千句受影響。
這代表兩份內容完全等價、只差資料列順序的資料庫,口語語域逐句正確率可以差 1.02 個百分點,比這個專案多數改動的真實效果還大。這也意味著,用 UPDATE 改資料庫做 A/B 測試會產生假訊號。
UPDATE
解法是找一份外部、客觀的頻率依據,在權重相同時充當第二排序鍵。國家教育研究院的《通用詞頻表》正好合適。專案去函詢問授權,國教院竟然爽快的以公文書面確認採 CC BY 4.0,可以商業利用。套用之後不改任何權重、只改排序,五個語域全部提升(+0.22 到 +1.36)。
「不」在字典上的本調是 ㄅㄨˋ,但在四聲前會變調成 ㄅㄨˊ,多數人打字打的是嘴巴唸的那個音。問題是詞庫的各個來源各只記了其中一種,從真實語料統計來的詞,記下的是變調後的讀音,Rime essay 這類詞頻表來的詞透過自動轉換,則只標本調。結果有些詞只有 ㄅㄨˊ、有些只有 ㄅㄨˋ,打到「不對的那一種」就出不來,像是打 ㄨˊ ㄔㄨˋ ㄅㄨˊ ㄗㄞˋ 會對應不到「無處不在」。最後決定把只有本調的詞補上變調讀音,只有變調讀音的補上本調,兩種打法都能出字。
還有最後一個問題,新聞裡剛冒出來的人名、上週才開幕的店、正在播的動畫角色,詞庫裡沒有這個詞,使用者只能逐字挑。這類詞的壽命通常只有幾週,人工維護跟不上,硬塞進基底詞庫又會留下一堆過期的殘骸。
所以詞庫裡新增了一層獨立的自動熱門詞覆蓋層(chiaki-auto-hotwords-overlay),完全由排程自動化處理。每天清晨四點,GitHub Actions 去讀 Google Trends 台灣地區 24 小時、48 小時、7 天三個視窗的熱搜,以及 PTT 八卦板與 C_Chat 過去 24 小時內淨推數達 20 的熱門文章標題與推文,找出候選詞與計數。
chiaki-auto-hotwords-overlay
候選詞要進入覆蓋層並不容易。先用既有詞庫做最長切分,已經打得出來的片段直接淘汰;只留 2 到 4 個字的全漢字詞;天氣、股價、排行這類查詢型的詞排除;標題裡切出來的殘片,要嘛在引號中、要嘛是能辨識的人名或地名,要嘛得出現在至少兩個不同標題。三個視窗分別給 1、2、3 分,單獨出現在 7 天視窗只算背景,不足以新增詞條;必須在短視窗互相佐證,或跨多日重複出現,才會入選。
入選之後有生命週期。權重從保守的 −2.4 起跳,近 14 天訊號持續變強才升到 −2.1、−1.9;14 天沒再觀測到就進入衰退,31 到 60 天冷卻到 −2.8,61 到 90 天降到 −3.0,之後從覆蓋層移除,但 state 會再保留到 180 天,讓舊聞回鍋時能快速恢復。每週一早上,流程把一週的觀測聚合起來,開一個 PR,附上每個來源各貢獻了多少詞的摘要,最後由我手動檢查合併。搭配輸入法目前預設自動更新詞庫,這樣就能讓近期熱門的名字能打得出來。
這類問題沒有一個是 bigram 能解的。它們是讀音層、排序層、政策層、新詞層的問題。詞庫是一疊層,每一層有自己的邊界。
主力層 chiaki-tw-homophone-bigram 目前有 394,267 列,覆蓋 61,675 個不同的前文、17,049 個不同的後文,平均每個後文有 23.1 個前文。出貨版的量測如下:
chiaki-tw-homophone-bigram
與其他 bigram 來源在同一天、同一份詞庫、同一個訊息語域測試集上比較(7 月 30 日的快照,當時這一層還只有 29 萬列):人工審查過的網路用語層淨值 +476,舊立法院層 +39,舊合成層 +5,這一層 +4,259。
不過這張表有幾件事必須說清楚。
出貨版是演算法凍結之後用全部語料重建的,四個語域的測試集也在訓練裡,所以上表不是嚴格意義上「沒看過答案」的成績。不過每一個採用的決定,當時都是在測試集完全排除在外的乾淨版本上做的;最後驗證時,沒看過答案的一半資料,修復率跟搶錯率都跟訓練用的那一半幾乎一樣(14.8% 對 14.5%),沒有過擬合的跡象。
表上的數字也只是下限。評估只計入「不管使用者怎麼打字都一定會生效」的那類資料,佔 41.8%;另外快一半(49.9%)是「使用者選過比較弱的候選之後才會生效」的救援機制,這部分因人而異,沒辦法用靜態語料量出來。
394,267 列資料沒有逐條人工檢查,是自動判準做出來、再用沒看過的測試集把關,不是人力一條條看過的品質保證。
訊息語域的測試集只有我自己的朋友群組,還帶著各種句子的偏好。這份成果相當程度上是照著我自己的打字習慣調出來的,不一定貼合每個人的使用情境。
引擎端的整體基準線(約 19.9 萬句,五個語域)逐句正確率落在 64% 到 77% 之間,逐字正確率 95% 到 97%。這也是「看過答案」的數字,只適合拿來比較改動前後的差異,不能當作對外宣稱的準確率。
整個詞庫沒有單一授權。每個資料層都有自己的來源、授權與整合紀錄,公開 release 之前必須先宣告。主力 bigram 層因為包含 PTT、Plurk 與維基百科(CC BY-SA)衍生的資料,採 CC BY-NC 4.0,不提供商業例外;需要明確資料授權的使用者,另有一份只用政府新聞與立法院公報重建、採 ODbL 的 clean 版本,56,048 列,可以商業使用,但公開衍生資料庫時要以 ODbL 回饋。
這個專案最終建立在許多人的成果之上:新酷音的詞庫、Rime 的詞頻、vChewing 整理保存的 KeyKey 原始碼封存與資料、g0v 維護的立法院 API、國教院的詞頻表、教育部《重編國語辭典修訂本》的讀音。每一項前人的努力都至關重要,再次感謝開源精神的偉大。
回到標題的問題。
我原本以為這是一個「把老程式修到能跑」的懷舊專案,結果最耗時間、也最有滋味的部分,反而是詞庫的建立。一個注音輸入法之所以「懂你」,不是什麼聰明的演算法,而是終究得有人把台灣人實際怎麼打字的證據,一筆一筆的整理起來。
我認為在大語言模型的時代,這件事反而更值得做,實驗很明白地告訴我們,大語言模型的用詞相當單一,模型可以幫忙判斷,卻不能代替真實文本告訴你台灣人怎麼說話。又或者像是「相關單位」出現 62 次的語料,與立法院公報裡的「本席」,本質上都是某個特定說話者的口癖,不是語言本身。在做這份詞庫的過程,或許也多少保存了那份屬於台灣的文字味道。
詞庫還在成長,我也還在努力。故事還沒寫完,但至少現在,打「天意難測」,出來的會是「天意難測」。
千秋輸入法 ChiaKey 與詞庫都是永久免費的開源專案。但為了簽章安裝程式,蘋果每年會向開發者收取 100 美元的費用。每週的語料抽取、評估、審核與釋出,也需要時間與機器持續運轉。如果你也是注音使用者,覺得這件事值得有人繼續做,歡迎贊助,或者給我一些鼓勵。台灣的朋友可以參考綠界贊助,信用卡、Apple Pay、ATM 與超商代碼都可以;台灣以外的朋友亦可以透過 Ko-fi 贊助。
不花錢的支持也一樣重要:打到缺詞、選錯字,到 ChiaKey-Lexicon 開一個 issue 回報,就是讓這份詞庫變好的最直接方式。
ChiaKey(千秋輸入法)與 ChiaKey-Lexicon(千秋輸入法綜合詞庫)皆為開源專案。本文提到的所有數字都來自專案文件中的實測紀錄;各層的完整方法、限制與授權,見詞庫專案的來源說明與研究附錄。
有一種軟體,你每天使喚它數千次,卻幾乎不會意識到它的存在──直到它壞掉為止。輸入法就是這種東西。
Yahoo! 奇摩輸入法是一個傳奇。它在 2013 年停止維護,到今天已經十三個年頭,卻還有人(對,包括我)在網路上翻找它的安裝檔;網友一路傳承、修補它最後一次釋出的版本,硬是讓它在 macOS 上多撐了十幾年。但天下無不散的筵席。某天早晨,剛更新完的 Mac 右上角跳出一則來自蘋果的最後通牒:Intel 版 app 的支援即將結束,x86 程式將在 macOS 27 停止執行。
大家其實都心裡有數,這個早該安息的輸入法終將離開。抱著不捨的心情,我想,既然 Yahoo 都大方的開源了,那不如重拾多年沒碰的 C++,看看能不能讓它再活一次。
這篇文章記錄的,就是這幾個月發生的事。前半段是數位考古,後半段逐漸變成一個語言學問題。因為我很快就發現,讓輸入法「跑起來」只是入場券,真正決定一個注音輸入法好不好用的,是它背後的詞庫。
先讓它活過來
一座「廢件處理廠」
打開 GitHub 一查,才發現早就有人動過手了。一個
vChewing/KeyKey-Boneyard的封存庫,裡面對 Yahoo 奇摩輸入法在 2012 年開源出來的原始碼已經進行過大量的整理,我決定從這邊開始著手。裡面資料夾名稱像Loaders/OSX-TSM、Loaders/Windows-IMM、Frameworks/OpenVanilla,每一個都充滿年代感。這份倉庫其實就是唯音輸入法的開發者,嘗試修復過的,整理了偏好設定與詞庫編輯器的原始碼,但輸入法主體從來沒有現代化過。README 甚至寫著這是一座「廢件處理廠」,並且聲明沒有人保證這東西會動。我心一橫,決定接手。
第一天就是不折不扣的震撼教育,專案的入口根本不存在,
.xcworkspace打開來是空的。發送按鍵的程式碼還在使用 Carbon 時代的Str255這類早已成為化石的型別。而這個輸入法在沒有辭典資料庫的情況下,會直接崩潰。程式碼寫死認定一定存在一個叫SmartMandarin的輸入法,那段「找不到就換下一個」的容錯迴圈整段被註解掉,不知道封印了多久。修完這一輪,程式終於願意在沒有資料庫的情況下體面地「什麼都不做」,而不是當場倒下。同一天下午,我用 Ruby 從頭寫了一支資料庫產生器,直接從注音表組出一份最小可用的詞庫,不再依賴 Yahoo 當年的搜尋語料、中研院語料庫,也不需要商用的加密 SQLite 擴充。「詞庫」這件事,第一次徹底與原廠脫鉤。
跟 2012 年打一架
接下來的一週像是考古挖掘現場。輸入法當年有一個 Yahoo 快捷鍵,可以直接搜尋或者查東西的「OneKey」功能,它的設定檔列出的服務清單,活脫脫是一份 2012 年台灣網路生態的切片:Yahoo 奇摩搜尋、股票查詢、無名小站、Yahoo 拍賣、Yahoo 地圖。再往下挖,有一整套 Windows 安裝程式工具鏈,甚至還有 Borland C++Builder 5 的專案檔;有替 Mac OS X 10.4 Tiger 寫的 Text Services Manager 實作;有「上傳資料庫到 iDisk」的按鈕,iDisk 是蘋果 iCloud 的前身,MobileMe 時代的雲端服務,2012 年就熄燈了。
把行李箱裡的石頭一塊塊倒出來之後,我把專案命名為 ChiaKey(千秋輸入法),並且把專案架構與路線圖定出來。詞庫獨立成姊妹庫 ChiaKey-Lexicon,透過 GitHub Releases 發佈,附上簽章與校驗碼;輸入法本體只負責執行期的契約、驗證與安裝。
中間還有一天一口氣修了十五個資安漏洞。沒有檢查 macOS 的「安全輸入」狀態;把使用者字串直接丟給
system()執行的 shell injection 破口;差一個位元組的 stack buffer overflow;還有一條透過古老的 Distributed Objects 暴露出來、任何行程都能遠端塞按鍵進去的 IPC 通道。也順手替核心引擎裡兩個負責展開所有切法的遞迴函式加上記憶化。原本句子每多一個字,重複展開的工作量便呈指數增長,因此偏好設定裡建議大家組字區域不要開太大,但改完之後,長句與短句一樣流暢。這些工程細節可以再寫好幾篇,但我想把篇幅留給後面更有意思的事。當程式不再崩潰、修好了 Bug、打起來也不卡了,我開始認真用它打字,然後很快遇到所有注音使用者都熟悉的那種懊惱:
打「天意難測」,出來的是「天意南側」。
真正的難題是詞庫
開源詞庫很多,但都少了一種東西
注音輸入法的核心,是把一串讀音變成一串字。這件事需要三種資料:注音對字的對照表、每個詞有多常用(術語叫 unigram),以及詞與詞怎麼銜接(bigram)。
繁體中文的開源詞庫資源其實不少。新酷音(libchewing)的
tsi.csv是「詞組、頻率、注音」,Rime 中州韻共享的essay.txt是「詞、頻率」。但仔細看會發現,它們幾乎都是同一種形式:一個詞、一個數字。這種資料能告訴你「哪個詞比較常用」,卻無法描述「打完 A 之後,接 C 是不是比接 B 更合理」。而同音歧義恰恰全靠這件事。「難測」與「南側」單獨看都是正常的詞,誰比較常用其實差不了多少;真正能分出勝負的,是前面那個「天意」。台灣的開源注音生態,長期缺少這種可用的接續關係資料。原因也很現實:n-gram 權重表需要語料清洗、機率計算、模型編譯,不像傳統詞庫那樣改改文字檔就能持續更新。
Yahoo 輸入法在當年轟動一時,就是因為他們實際上做出了這份資料。據網路上的討論以及官方留下來的說明,他們用了自己搜尋引擎的熱門關鍵詞,以及購買了中研院的平衡語料庫來計算前後詞機率。或許是這個原因,這兩者都沒有一起跟著輸入法開源釋出。但就算能用,這份詞庫也停留在 2012 年,而且授權問題難以解決,沒辦法跟著開源版一起走。所以要嘛沒有 bigram,要嘛自己做一份。
先弄懂引擎怎麼「選」
在動手做資料之前,得先搞清楚引擎究竟怎麼使用這些數字。ChiaKey 的核心引擎(Manjusri/Gramambular 系)把一串注音切成一格一格的候選詞,然後尋找一條總分最高的路徑。這裡有幾條規則,決定了後面所有的研究方向。
分數是以 10 為底的 log 機率,全部是負值,越接近 0 越常用。相加就等於機率相乘。
每個詞節點會多拿一份「詞長加分」:每多一個音節加 1.0。這個數字相當可觀。四字詞當一個節點,比拆成兩個二字詞多出整整 1.0,遠大於多數 bigram 能提供的差距。換句話說,bigram 只有在競爭路徑的節點數相同時,才真的有影響力。
bigram 與 unigram 之間是「取最高分者」,不是相加。而且理論上應該打折的退避權重(Katz back-off),在整個資料庫裡全是 0。所以一筆 bigram 要生效,必須硬贏過那個讀音本來排第一的候選。
還有一個很容易被忽略的細節:同一組「讀音+前文」底下如果掛了許多筆 bigram,引擎只看第一筆。第二名以後的資料永遠不會被讀到。
弄懂這幾條,就能反推出一個判準:一筆 bigram 要真的有用,它的後文這個詞,必須在使用者實際會打的那個讀音上,原本是輸給同音對手的。如果它本來就是第一名,這筆資料不會改變任何結果;如果它綁在一個罕用讀音上,使用者永遠不會觸發它。這個判準後來成為整個研究的骨幹,不過我是繞了兩條錯路才走到這裡。
現代手段的嘗試
請 AI 寫台灣文章
第一個想法很直覺,既然缺語料,那請大語言模型生成呢?從使用條款來看,大部分的聊天機器人,例如 ChatGPT、Claude、Gemini 等,都明確放棄對生成內容的權利,授權上是乾淨的。加上正好碰到 Tibo 的 codex 用量大放送,決定先嘗試用 GPT-5.5 以及本機模型 Gemma 4(26B-A4B,跑在 MLX 上),搭配 prompt 產生器,指示 AI 撰寫或生成各種語境的台灣繁體中文短文,斷詞之後統計哪些詞常接在哪些詞後面。
跑了一陣子,出來的結果可以說是不上不下。在多次 prompt 產生器的調整後,總算能指示 AI 寫出有用的文本,但在小規模測試 30.5 萬個漢字的生成文本,只能產出約 2,000 個真正落在「同音撞碼」位置的配對,長尾頻率的估計雜訊極大。按實測速率,要達到可用規模得連續生成好幾週。更麻煩的是,模型有自己的口癖,比如 prompt 要求模型用「新聞語氣」寫作,AI 就會高機率的寫出「相關單位」這個詞,在相關的 381 篇文章裡出現了 62 篇。直接計數發現,模型的偏好與實際自然語言的文本頻率並不相同。
以這條路做出來的合成語料層(當時稱作
chiaki-synthetic-overlay)有 46,822 列 bigram,勉強能用,但拿前面的判準回頭檢視,只有 11.6% 落在能改變結果的位置。其餘 88.4%,要嘛那個讀音只有一個候選,要嘛那個詞本來就是第一名,也就是模型會反覆產出最順口的搭配,而大部分的組合本來就是列表中的首選詞,幫助有限。既然撞碼詞可以列出來,請模型替每個詞補前文呢?
仔細思考之後,我意識到我最需要的東西,其實是同音詞的前後文,單純拼字數純粹是搞錯方向了。既然哪些詞會撞碼,是可以從詞庫直接列舉的封閉集合(335,427 個詞條裡,在自己常用讀音上會輸給同音對手的有 16.4%,55,170 個),那何不對每一個同音詞,批次請模型回答「最常出現在它前面的是哪些詞」,而且 Gemma 4 是運作在本機的開源模型,其實我可以直接讀取他內部對該詞的 logprob 當做機率。也就是直接從內部問模型「難測」跟「南側」這些 token,前面通常會接哪些詞。
這個方法效率高得驚人:12 個詞 9 秒,一個晚上就把全部 29,221 個高頻撞碼詞跑完,產出 20,092 筆通過驗證的配對。
並且為了驗證成果,我又寫了一套評估工具。評估工具建好一測,這條路直接宣告失敗。那批資料在 32.6 萬句真實測試語料裡,只命中了約 200 次。
原因我猜測可能是本地模型太小,對詞彙的創意度很低,又或者模型只擅長往後猜,而不擅長往前猜。也可能是模型其實也並不熟悉台灣人寫出來的句子。模型可以列舉撞碼詞,卻不曉得真實用法。判別式驗證能確認「給定這個前文,這個詞是否勝過同音對手」,卻無法確認「這個前文到底會不會出現」。
好在這幾天的產物並非白費,為了幫模型打分做的評估工具,對於之後的品質驗證起到了非常大的作用。
回到真實的台灣文本
4.6 億字
放棄讓模型憑空發明之後,要做的事變成了尋找真實的、台灣人寫的、授權允許使用的文本。
最後湊出來的語料有五種來源,合計超過 4.6 億字、約 3.3 億個漢字、六百七十多萬行。政府機關新聞發布(行政院、陸委會、中研院、客委會、新北市政府,依政府資料開放授權條款釋出);立法院公報的詢答逐字稿(這是少數同時具備「真實即席口語」「台灣本土語境」「授權可再散布」三個條件的語料,而且幾乎沒有錯字);PTT 與 Plurk 的公開貼文;以及正體中文轉換後的維基百科條文。
接著設計一套系統,在確保原始文本不落地、確保隱私的同時,萃取文本的詞庫列與權重機率。
只收「會改變結果」的配對
從語料到詞庫列,中間有十一個步驟,大多是可預期的清洗工作:切句、斷詞、統計相鄰詞對、把「前後文本身就是一個詞」的配對挪到 unigram 層、設門檻。真正值得記一筆的是這幾步:
第一是斷詞器必須跟引擎用同一套規則,否則統計出來的東西,引擎根本不會照著做。
第二是撞碼要先篩過。後文如果在它最常用的讀音上本來就贏得了同音對手,這筆資料就沒意義,直接丟。這一步刷掉了五百多萬筆,最後留下的 394,267 列全部落在真正的撞碼位置上(相較之下,前面 AI 合成語料只有 11.6% 是這樣)。
第三是若跟語料本身作對,高機率會是錯的。如果同一個前文後面,另一個同音詞在真實文本裡出現得更頻繁,這筆資料就是在幫倒忙,得刪掉。這一步的效果落差很大,在正式書面語域大概能拉出三成五的淨值;但在朋友聊天的訊息語域,它是「有效」跟「完全沒用」的分界線,不做這項檢查,修對的和搶錯的幾乎打平(淨值只有 +34);做了之後,淨值衝到 +4,061。
還有一個藏得很深的陷阱:「電影裡」是詞典裡的標準寫法,斷詞器會把它併成一個詞,「電影→裡」這個配對在語料裡的計數永遠是 0;但「電影裏」這種非標準寫法會被拆成兩個字分開算,計數反而不是 0。兩邊一比,系統就會學到「裏比裡對」──等於系統性地鼓勵錯字。抽樣一查,抓出來的全是錯字、日文字形、簡體字形。修好之後,刪掉了 10,641 筆這樣的髒資料。
其餘像「的/地/得」要靠聲調分流、「立法委員」後面接的姓氏純粹是誰發言多而非語言搭配,這類清洗規則還有好幾條,但道理都類似。手動找出資料裡「看起來像規律,其實是雜訊」的地方,一條條擋掉。
怎麼知道有沒有變好
不能只看筆數
詞庫工作最容易犯的錯,是拿「加了幾萬筆資料」當成進度。所以整個專案最重要的產物,其實不是那 39 萬列 bigram,而是量測方法。
做法是這樣:在真實文本上斷詞,逐個相鄰詞對判定「沒有 bigram 的時候,引擎會不會選錯」。這些位置叫可修位置。然後看一個 bigram 層修對了幾個位置,又把幾個原本正確的位置搶錯。評估會重播正式版的校準公式,只計入確定會生效的資料列。
測試集分四個語域:新北市政府新聞(書面)、立法院公報一個保留會期(正式口語)、PTT 語料的 5%(涵蓋 169 個看板),以及我和朋友的群組對話(訊息)。
為什麼一定要多語域
單一語域的評估會很有系統地哄騙你,這一層的前一版只用政府新聞與立法院公報訓練,在書面語域淨值 +85,128,看起來非常漂亮,加入真實訊息語域之後才發現,同一份資料在朋友聊天的語域淨值只有 +65:修對 1,603、搶錯 1,538,幾乎完全抵銷。加入 PTT 語料重做之後,訊息語域提升了大約 65 倍。
最有價值的是評估工具
評估工具也經過了數次的迭代,每一次都修正了成果的高估。
第一個問題是,資料被綁到沒人會打的讀音上。初版邏輯是挑「權重落差最大的讀音」來配對,理由聽起來合理:落差越大越需要幫助。結果它幾乎每次都選中一個詞最冷僻的讀音,例如「的」被綁到誰都不會打的 ㄉㄧˋ,而不是常用的 ㄉㄜ˙。初版 93,952 筆資料裡,有 68.5% 都綁錯了地方,等於白做。
而且一開始的評估只比對文字,沒管讀音、也沒管實際會不會生效。等於預設一筆 bigram 在任何讀音下都有效、而且必勝。修正之後,光是書面語域「真的可能被改對」的位置,就從 184 萬筆腰斬到 95 萬筆,原本一半以上其實都是假象。
第三個問題就是前面提過的,只用單一語域測試,數字好看,換一個語域立刻現形。
還有一個更低級的 bug:斷詞的詞長加成。我原本寫的是「每個字 +1.0」,聽起來跟引擎的「每多一個音節 +1.0」差不多,但兩者其實完全不同,這個加分規則等於沒在運作,系統只是單純選權重總和最大的切法,跟真正偏好長詞的引擎完全對不上。有 55.1% 的句子,這兩種規則會切出不同答案;當時已經出貨的 29 萬筆資料裡,有 5,558 筆落在引擎根本不會產生的詞界上,永遠不會被用到。修好重跑之後,口語語域逐句正確率 +2.07、論壇 +0.88、Plurk +0.75。
被丟掉的,比留下的還多
還有一條規則一直讓我很在意,為了方便,一開始我設定「前文如果是多音字,就整筆棄用」,理由是它的讀音會變成猜測。這條規則棄掉的量比留下的還多,一百多萬筆,而且棄掉的正是最有價值的那批。被丟掉的配對裡最常見的前文,是「的」(2,756 對)、「在」(1,119)、「和」(1,026)、「與」、「有」這些高頻虛詞。
我嘗試保留,並且把多音字綁到它自己權重最高的讀音,與後文採用同一個定義。意外的效果並不差,四個語域全部提升,列數從 293,853 增到 360,016,訊息語域的修對率從 13.5% 升到 18.4%。我也嘗試過人工審核,但發現靠人力審核讀音,一個晚上最多也只能看個 1000 條左右,也不是個長久的辦法。
有些事 bigram 管不了
做到這裡,我發現 bigram 要好,首要條件是單詞詞庫要好。許多問題是 bigram 表救不了的詞庫問題。
立法院的行話
早期有一層專門從立法院公報做的 bigram(
tw-ly-transcript),經過人工複核。它有個頗為反直覺的性質:訊號最強的搭配,恰好是最沒價值的部分。公報裡文件頻率最高的配對是「條文+通過」「所以+本席」「本席+要」,全是議場專用語。更危險的是「異議/意義」「議事/意識」這類:議場偏好前者,日常打字幾乎全是後者,語料的偏好會直接傷到高頻日常詞。而且這種判斷無法從權重落差導出。「議事/意識」落差 0.759 應該移除。差別在語義語域,不在頻率結構,只能逐一點名。「啊」與「阿」
引擎端的 gold set 基準線揭露了一件事:幾乎沒有斷詞層級的錯誤(輸出與期望長度不同的只有 1 句),問題全在同讀音候選選錯。最大的一群是句末助詞輸給同音實詞:啊→阿、啦→拉、嘛→嗎,合計 7,204 次。
「啊」在 ㄚ 這個讀音上的權重是 −1.673,輸給「阿」的 −1.015。翻過來之後,啊→阿 的錯誤從 3,100 次降到 3 次,代價是阿→啊 多了 12 次。因為「阿」幾乎總是出現在「阿姨」「阿公」「阿拉伯」裡,那些以整詞路徑勝出,不受單字排頭影響。口語語域逐句正確率因此 +2.37。
但這件事有方法論上的教訓。翻轉單字排頭是雙向的,現在的排頭永遠不會錯,翻過去之後換它每次都錯。同一份稽核清單裡翻轉成本最低的兩組(嘛/嗎 只差 0.000008),淨值其實是負的,因為「嗎」在語料裡比「嘛」常用四倍以上。所以判準不是「翻轉多便宜」,而是「哪一邊才是使用者想要的」。
新酷音詞庫裡的「對,但沒人在用」
詞庫的問題不見得都是「讀音錯了」,更常見的是「讀音沒錯,但這詞根本沒人在打」。
新酷音的詞庫裡,「宏碩」收了 ㄏㄨㄥˊ ㄕˊ 這個讀音,查教育部《重編國語辭典修訂本》,這讀音有典可循,完全正確。問題是這是一千多年前的文言用法,在此之前我也一直以為這個字唸「ㄕㄨㄛˋ」。麻煩的是,它剛好跟常用詞「紅石」撞在同一個讀音上,而詞典收錄時給的權重,並沒有把「這個詞有多古老、現在還有多少人在用」算進去。結果就是打 ㄏㄨㄥˊ ㄕˊ 的時候,現代理應更常出現的「紅石」被一個文言詞卡到後面去。
這類「詞典裡查得到、但跟現代人打字習慣脫節」的狀況,是拿一支稽核工具比對出來的,把資料庫裡每一個兩字以上的詞拆開,比對每個字單獨查得到的讀音,找出「詞的讀音」跟「字的讀音」對不上的地方。這支工具篩出來的,多半不是這種「太古典」,就是像地名「栃木」被硬套成漢字讀音、或「俾倪」其實該是「睥睨」的錯字,這些多半也回頭改不了上游,只能新增一層校正層,把它們的權重壓低。
工具還帶出另一種讀音與字典脫節的發現,「咖哩」在教育部辭典的標準讀音為一聲的 ㄎㄚ ㄌㄧˇ(kā lǐ),但「咖」字常被念為「ㄍㄚ」,「好萊塢」的「塢 ㄨˋ」實際上大家都唸 ㄨ,這些台灣人實際會唸的音,單字讀音表裡卻沒有收,查得到這個詞,卻查不到這個字單獨的這個讀音,一樣會出錯。
Rime 詞庫裡的繁化殘留
另一個我使用的來源是 Rime 中州韻共享的 essay.txt,他有非常豐富的詞頻資料。它本身應該是簡體中文語料轉換過來的,而「轉對」跟「轉錯」的界線,比想像中細。
像是「想象」就是一個棘手的例子。「象」這個字大部分時候是正字,「象形」「景象」「形象」都不該被動。但語料裡「想象」這個詞的統計頻率是兩萬多次,實際上應該指向「想像」才對。但如果照單字規則把「象」整個轉成「像」,會連累「景象」「形象」全部變成錯字,只能寫一條專門針對「想象」的規則,繞過所有其他「象」。
另一個例子是「粘」跟「黏」。essay.txt 裡「粘土」出現 802 次,比「黏土」的 577 次還多,「粘」這個字整體出現的次數也是「黏」的兩倍以上。但教育部標準用字裡,「黏著」這個意思該寫成「黏」,「粘」是它的異體字,唯一該獨立存在的用法只剩姓氏(讀音也不同,姓氏念 ㄓㄢ,不是「黏著」的 ㄋㄧㄢˊ)。如果照語料的次數排列,「粘土」這種寫法反而會排到「黏土」前面。
最麻煩的是「台灣」的「台」。essay.txt 裡含「臺」字的詞條有 1,308 個,含「台」字的卻只有 36 個,上游幾乎全部寫「臺」,但我目前的政策是,預設採用「台」作為優先,若使用者習慣打「臺」,交由自動學習層來更正。
同音字相同權重的問題
有一個更奇妙的發現,引擎撈候選時用的是穩定排序,權重完全相同的候選會維持輸入順序,而輸入順序就是資料列在 SQLite 檔案裡的實體位置。實測同讀音相鄰候選對 68,684 組中,有 12,003 組權重一模一樣。典型案例是「啦/拉」(同為 −1.198788)。順序一翻,「就好了啦」變成「就好了拉」,上千句受影響。
這代表兩份內容完全等價、只差資料列順序的資料庫,口語語域逐句正確率可以差 1.02 個百分點,比這個專案多數改動的真實效果還大。這也意味著,用
UPDATE改資料庫做 A/B 測試會產生假訊號。解法是找一份外部、客觀的頻率依據,在權重相同時充當第二排序鍵。國家教育研究院的《通用詞頻表》正好合適。專案去函詢問授權,國教院竟然爽快的以公文書面確認採 CC BY 4.0,可以商業利用。套用之後不改任何權重、只改排序,五個語域全部提升(+0.22 到 +1.36)。
「吃不下」打不出來
「不」在字典上的本調是 ㄅㄨˋ,但在四聲前會變調成 ㄅㄨˊ,多數人打字打的是嘴巴唸的那個音。問題是詞庫的各個來源各只記了其中一種,從真實語料統計來的詞,記下的是變調後的讀音,Rime essay 這類詞頻表來的詞透過自動轉換,則只標本調。結果有些詞只有 ㄅㄨˊ、有些只有 ㄅㄨˋ,打到「不對的那一種」就出不來,像是打 ㄨˊ ㄔㄨˋ ㄅㄨˊ ㄗㄞˋ 會對應不到「無處不在」。最後決定把只有本調的詞補上變調讀音,只有變調讀音的補上本調,兩種打法都能出字。
詞典永遠追不上的新詞
還有最後一個問題,新聞裡剛冒出來的人名、上週才開幕的店、正在播的動畫角色,詞庫裡沒有這個詞,使用者只能逐字挑。這類詞的壽命通常只有幾週,人工維護跟不上,硬塞進基底詞庫又會留下一堆過期的殘骸。
所以詞庫裡新增了一層獨立的自動熱門詞覆蓋層(
chiaki-auto-hotwords-overlay),完全由排程自動化處理。每天清晨四點,GitHub Actions 去讀 Google Trends 台灣地區 24 小時、48 小時、7 天三個視窗的熱搜,以及 PTT 八卦板與 C_Chat 過去 24 小時內淨推數達 20 的熱門文章標題與推文,找出候選詞與計數。候選詞要進入覆蓋層並不容易。先用既有詞庫做最長切分,已經打得出來的片段直接淘汰;只留 2 到 4 個字的全漢字詞;天氣、股價、排行這類查詢型的詞排除;標題裡切出來的殘片,要嘛在引號中、要嘛是能辨識的人名或地名,要嘛得出現在至少兩個不同標題。三個視窗分別給 1、2、3 分,單獨出現在 7 天視窗只算背景,不足以新增詞條;必須在短視窗互相佐證,或跨多日重複出現,才會入選。
入選之後有生命週期。權重從保守的 −2.4 起跳,近 14 天訊號持續變強才升到 −2.1、−1.9;14 天沒再觀測到就進入衰退,31 到 60 天冷卻到 −2.8,61 到 90 天降到 −3.0,之後從覆蓋層移除,但 state 會再保留到 180 天,讓舊聞回鍋時能快速恢復。每週一早上,流程把一週的觀測聚合起來,開一個 PR,附上每個來源各貢獻了多少詞的摘要,最後由我手動檢查合併。搭配輸入法目前預設自動更新詞庫,這樣就能讓近期熱門的名字能打得出來。
這類問題沒有一個是 bigram 能解的。它們是讀音層、排序層、政策層、新詞層的問題。詞庫是一疊層,每一層有自己的邊界。
目前的成果
主力層
chiaki-tw-homophone-bigram目前有 394,267 列,覆蓋 61,675 個不同的前文、17,049 個不同的後文,平均每個後文有 23.1 個前文。出貨版的量測如下:與其他 bigram 來源在同一天、同一份詞庫、同一個訊息語域測試集上比較(7 月 30 日的快照,當時這一層還只有 29 萬列):人工審查過的網路用語層淨值 +476,舊立法院層 +39,舊合成層 +5,這一層 +4,259。
不過這張表有幾件事必須說清楚。
出貨版是演算法凍結之後用全部語料重建的,四個語域的測試集也在訓練裡,所以上表不是嚴格意義上「沒看過答案」的成績。不過每一個採用的決定,當時都是在測試集完全排除在外的乾淨版本上做的;最後驗證時,沒看過答案的一半資料,修復率跟搶錯率都跟訓練用的那一半幾乎一樣(14.8% 對 14.5%),沒有過擬合的跡象。
表上的數字也只是下限。評估只計入「不管使用者怎麼打字都一定會生效」的那類資料,佔 41.8%;另外快一半(49.9%)是「使用者選過比較弱的候選之後才會生效」的救援機制,這部分因人而異,沒辦法用靜態語料量出來。
394,267 列資料沒有逐條人工檢查,是自動判準做出來、再用沒看過的測試集把關,不是人力一條條看過的品質保證。
訊息語域的測試集只有我自己的朋友群組,還帶著各種句子的偏好。這份成果相當程度上是照著我自己的打字習慣調出來的,不一定貼合每個人的使用情境。
引擎端的整體基準線(約 19.9 萬句,五個語域)逐句正確率落在 64% 到 77% 之間,逐字正確率 95% 到 97%。這也是「看過答案」的數字,只適合拿來比較改動前後的差異,不能當作對外宣稱的準確率。
授權這件事
整個詞庫沒有單一授權。每個資料層都有自己的來源、授權與整合紀錄,公開 release 之前必須先宣告。主力 bigram 層因為包含 PTT、Plurk 與維基百科(CC BY-SA)衍生的資料,採 CC BY-NC 4.0,不提供商業例外;需要明確資料授權的使用者,另有一份只用政府新聞與立法院公報重建、採 ODbL 的 clean 版本,56,048 列,可以商業使用,但公開衍生資料庫時要以 ODbL 回饋。
這個專案最終建立在許多人的成果之上:新酷音的詞庫、Rime 的詞頻、vChewing 整理保存的 KeyKey 原始碼封存與資料、g0v 維護的立法院 API、國教院的詞頻表、教育部《重編國語辭典修訂本》的讀音。每一項前人的努力都至關重要,再次感謝開源精神的偉大。
結語:所以,都 2026 年了
回到標題的問題。
我原本以為這是一個「把老程式修到能跑」的懷舊專案,結果最耗時間、也最有滋味的部分,反而是詞庫的建立。一個注音輸入法之所以「懂你」,不是什麼聰明的演算法,而是終究得有人把台灣人實際怎麼打字的證據,一筆一筆的整理起來。
我認為在大語言模型的時代,這件事反而更值得做,實驗很明白地告訴我們,大語言模型的用詞相當單一,模型可以幫忙判斷,卻不能代替真實文本告訴你台灣人怎麼說話。又或者像是「相關單位」出現 62 次的語料,與立法院公報裡的「本席」,本質上都是某個特定說話者的口癖,不是語言本身。在做這份詞庫的過程,或許也多少保存了那份屬於台灣的文字味道。
詞庫還在成長,我也還在努力。故事還沒寫完,但至少現在,打「天意難測」,出來的會是「天意難測」。
如果你願意支持
千秋輸入法 ChiaKey 與詞庫都是永久免費的開源專案。但為了簽章安裝程式,蘋果每年會向開發者收取 100 美元的費用。每週的語料抽取、評估、審核與釋出,也需要時間與機器持續運轉。如果你也是注音使用者,覺得這件事值得有人繼續做,歡迎贊助,或者給我一些鼓勵。台灣的朋友可以參考綠界贊助,信用卡、Apple Pay、ATM 與超商代碼都可以;台灣以外的朋友亦可以透過 Ko-fi 贊助。
不花錢的支持也一樣重要:打到缺詞、選錯字,到 ChiaKey-Lexicon 開一個 issue 回報,就是讓這份詞庫變好的最直接方式。
ChiaKey(千秋輸入法)與 ChiaKey-Lexicon(千秋輸入法綜合詞庫)皆為開源專案。本文提到的所有數字都來自專案文件中的實測紀錄;各層的完整方法、限制與授權,見詞庫專案的來源說明與研究附錄。