<NUM> トークン正規化レポート
概要
数字+接尾辞トークン(1匹/1ひき, 100円/100えん 等)を <NUM>匹/<NUM>匹 のように
正規化し、異なる数字のカウントを集約することで言語モデルのカバレッジを向上させる機能を導入した。
初回実装では裸の数字(1/1, 2/2 等)も <NUM>/<NUM> に正規化していたが、
これにより助詞「に」「さん」「ご」等が数字に化ける深刻な退行が発生。
裸の数字を正規化対象から除外する修正を行い、安全に導入できることを確認した。
背景
akaza の corpus-stats は Wikipedia ベースであるため、特定の数字についてしか
統計が存在しない。例えば「2週間」のスコアが学習済みでも「1週間」「3週間」は
未知語になる。数字部分を <NUM> に正規化して集約すれば、すべての数字パターンで
スコアが利用可能になる。
実装
akaza-data 側 (ビルド時正規化)
akaza-data/src/utils.rs に normalize_num_token() を追加:
1匹/1ひき→<NUM>匹/<NUM>ひき(正規化する — reading 側はかな読みを保持)100円/100えん→<NUM>円/<NUM>えん(正規化する)2019年/2019ねん→<NUM>年/<NUM>ねん(正規化する)1/1→1/1(裸の数字は正規化しない)匹/ひき→匹/ひき(数字なしは変換なし)第1回/だい1かい→第1回/だい1かい(先頭が数字でなければ変換なし)
surface 側は先頭の ASCII 数字を <NUM> に置換し漢字接尾辞をそのまま保持する。
reading 側も先頭の ASCII 数字を <NUM> に置換するが、かな読みをそのまま保持する。
これにより <NUM>行/<NUM>ぎょう と <NUM>行/<NUM>こう が区別され、
数字の汎化と同音異義語の区別が両立する。
unigram LM ビルド (make_stats_system_unigram_lm.rs) で wfreq パース時に
正規化を適用し、カウントを集約。threshold フィルタは集約後に適用。
libakaza 側 (ランタイムフォールバック)
libakaza/src/graph/graph_builder.rs に normalize_surface_for_lm() を追加:
ラティス構築時、unigram LM でエントリが見つからない場合にフォールバックとして
<NUM> 正規化キーでの lookup を試みる。これにより、LM に <NUM>匹/<NUM>ひき が
登録されていれば 3匹/3ひき でもスコアを取得できる。
セグメンタ側 (複合セグメント生成)
libakaza/src/graph/segmenter.rs の Segmenter::build() を拡張:
数字マッチ後、後続のかなトライマッチも試行して複合セグメントを追加する。
例えば入力 90ぎょう に対して、個別セグメント (90 + ぎょう) に加えて
複合セグメント (90ぎょう) も生成する。Viterbi が LM スコアに基づいて最適パスを選ぶ。
グラフビルダー側 (複合ノード構築)
libakaza/src/graph/graph_builder.rs の GraphBuilder::construct() を拡張:
segmented_yomi が数字+かな(例: 90ぎょう)の場合:
- 数字部分 (
90) とかな部分 (ぎょう) を分離 - かな部分をかな漢字辞書で検索 →
["行", "業", ...] - 候補生成:
90行(LM lookup:90行/90ぎょう→ fallback<NUM>行/<NUM>ぎょう) - 漢数字候補も追加:
int2kanji(90)→九十→九十行,九十業, …
初回実装の問題: 裸の数字の正規化による退行
症状
裸の数字 (1/1, 2/2, 3/3, …) をすべて <NUM>/<NUM> に集約すると、
全数字のカウントが合算されてスコアが極端に高くなる。
ランタイムで 2/に を lookup する際、exact match が失敗すると
normalize_surface_for_lm("2/に") が <NUM>/<NUM> を返し、この異常に高い
スコアが「2」に付与される。結果として:
に→2(助詞「に」が数字に)さん→3(敬称「さん」が数字に)ご→5(接頭辞「ご」が数字に)
評価結果 (裸の数字を含む正規化)
| 指標 | baseline (正規化なし) | 裸の数字含む正規化 | 差分 |
|---|---|---|---|
| Good | 6457 | 6352 | -105 |
| Bad | 4154 | 4233 | +79 |
| 再現率 | 92.76% | 92.56% | -0.20% |
修正 (akaza-im/akaza#428)
裸の数字(suffix が空文字列)を正規化対象から除外:
- ビルド時:
normalize_num_token()で suffix が空なら元の文字列を返す - ランタイム:
normalize_surface_for_lm()で suffix が空ならNoneを返す
修正後の評価結果
| 指標 | baseline (正規化なし) | suffix-only 正規化 | 差分 |
|---|---|---|---|
| Good | 6457 | 6428 | -29 |
| Bad | 4154 | 4179 | +25 |
| 再現率 | 92.76% | 92.72% | -0.04% |
退行分析 (60件)
退行の大部分は数字の表記スタイル差であり、日本語としての正誤ではない:
一時→1時(漢数字 → アラビア数字)三人→3人二度→2度十年間→10年間
これらは accept.tsv に追加可能なスタイル差であり、実質的な品質低下ではない。
改善分析 (35件)
改善は実質的な変換品質向上:
正午字→正午時(数字集約による正しいスコア付与)一瓢→一票ひとり→一人10じかん→10時間(未知語だった助数詞パターンがカバー)
結論
<NUM> トークン正規化は、裸の数字を除外する条件付きで導入すべきである:
- 退行は表記スタイル差 (漢数字 vs アラビア数字) であり accept.tsv で吸収可能
- 改善は実質的な品質向上 (未知助数詞パターンのカバレッジ拡大)
- 致命的な退行 (助詞→数字) は修正済み
関連 PR / リリース
- akaza-im/akaza#426: 初回実装 (
<NUM>正規化) - akaza-im/akaza#428: 裸の数字除外修正
- akaza-im/akaza-corpus-stats v2026.0211.0: suffix-only 正規化を含むリリース
実行日
2026-02-11