Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Akaza

Akaza は、Rust で書かれた統計的かな漢字変換エンジンを搭載した Linux 向け日本語 IME(IBus エンジン)です。

特徴

  • 誰でもモデルを再構築可能: 言語モデルは日本語 Wikipedia、青空文庫、CC-100 というオープンなコーパスのみから構築しています。学習パイプラインもすべて公開されているため、特定の企業やプロプライエタリなデータに依存せず、誰でも自分の環境で 1 からモデルデータを再生成できます
  • Rust で実装: UI/Logic をすべて Rust で書いてあるので、拡張が容易です
  • 統計的かな漢字変換: 単語 bigram モデルを採用し、ビタビアルゴリズムによる最適経路探索で変換を行います
  • 学習機能: ユーザーの変換結果を学習し、使い込むほど変換精度が向上します
  • SKK 辞書対応: SKK 形式の辞書ファイルを複数読み込み可能
  • GUI 設定ツール: GTK4 ベースの設定ツール (akaza-conf, akaza-dict) を提供

リンク

ユーザーズマニュアル

このセクションは今後追加予定です。

インストール方法や基本的な使い方については、README を参照してください。

内部構造

このセクションでは、Akaza の内部構造と設計に関する技術文書を掲載しています。

目次

設計メモ・レポート

開発過程で作成された設計メモや評価レポートは設計メモ・レポートにまとめています。

データフロー

Akaza の言語モデルとシステム辞書は、2つのリポジトリに分かれたパイプラインで構築される。

ディレクトリ役割
corpus-stats/コーパスの収集・トーカナイズ・統計データ生成
default-model/統計データからモデルと辞書を構築

1. コーパス統計の生成 (akaza-corpus-stats)

オープンなコーパスからトーカナイズ済みの n-gram 統計データを生成する。 わかちがき処理及びよみがな処理には Vibrato (ipadic) を利用している。

データソース

ソース内容
日本語 WikipediaCirrusSearch ダンプ (NDJSON)
青空文庫git submodule で取り込み
CC-100 JapaneseCommon Crawl ベースの大規模テキスト (品質フィルタ適用)

CC-100 は -full バリアントで使用され、重み付き(デフォルト 0.3)で統計に組み込まれる。

CC-100 の重み 0.3 の根拠

Wikipedia は百科事典的な語彙に偏り、日常語(買う・結構・寝る・使う等)の頻度が低い。CC-100 はこれを補うために導入されたが、CC-100 は jawiki の約 1.9 倍のボリュームがあり、そのまま統合すると機能語の崩壊(と→途さい→賽 等)が発生し再現率が 1.28% 低下する。

weight=0.3 を適用すると実効的な寄与は 1.9 × 0.3 ≒ 0.57 倍となり、jawiki の統計を基盤としつつ CC-100 で日常語を補強する構成が実現できる。この設定で Good +316、再現率 +0.59% の改善が確認された。

詳細な評価は CC-100 重み付き統合レポート を参照。

パイプライン

graph TD
    subgraph データソース
        jawiki[日本語 Wikipedia]
        aozora[青空文庫]
        cc100[CC-100 Japanese]
    end

    subgraph テキスト抽出
        jawiki --> extract_cirrus[extract-cirrus.py]
        cc100 --> extract_cc100[extract-cc100.py]
    end

    subgraph "Vibrato でトーカナイズ"
        extract_cirrus --> tokenize_jawiki[jawiki/vibrato-ipadic/]
        aozora --> tokenize_aozora[aozora_bunko/vibrato-ipadic/]
        extract_cc100 --> tokenize_cc100[cc100/vibrato-ipadic/]
    end

    subgraph 統計データ生成
        tokenize_jawiki --> wfreq[wfreq 単語頻度集計]
        tokenize_aozora --> wfreq
        tokenize_cc100 -.->|weight=0.3| wfreq

        wfreq --> vocab[vocab 語彙抽出 threshold=16]
        wfreq --> unigram_trie[unigram.wordcnt.trie]

        unigram_trie --> bigram_trie[bigram.wordcnt.trie threshold=3]
        tokenize_jawiki --> bigram_trie
        tokenize_aozora --> bigram_trie
        tokenize_cc100 -.-> bigram_trie

        unigram_trie --> skip_bigram_trie[skip-bigram.wordcnt.trie threshold=3]
        tokenize_jawiki --> skip_bigram_trie
        tokenize_aozora --> skip_bigram_trie
        tokenize_cc100 -.-> skip_bigram_trie
    end

    subgraph "成果物 (GitHub Releases)"
        unigram_trie --> release[akaza-corpus-stats.tar.gz]
        bigram_trie --> release
        skip_bigram_trie --> release
        vocab --> release
    end

※ 破線は -full バリアントのみで使用される CC-100 のフローを示す。

2. モデル構築 (akaza-default-model)

akaza-corpus-stats の成果物をダウンロードし、コーパス補正を加えてモデルと辞書を構築する。

コーパス補正

Wikipedia・青空文庫のデータには偏りがあるため、手作業で作成したコーパスでスコアを補正する。

ファイルエポック数用途
must.txt10,000必ず変換できなくてはならない表現
should.txt100変換できてほしい表現
may.txt10できれば変換できてほしい表現

パイプライン

graph TD
    subgraph 入力
        corpus_stats[akaza-corpus-stats<br/>GitHub Releases]
        training[training-corpus/<br/>must/should/may.txt]
        unidic[UniDic]
        skk_base[dict/SKK-JISYO.akaza]
    end

    corpus_stats --> |unigram/bigram/skip-bigram<br/>wordcnt trie| learn_corpus[akaza-data learn-corpus]
    training --> learn_corpus

    learn_corpus --> unigram_model[unigram.model]
    learn_corpus --> bigram_model[bigram.model]
    learn_corpus --> skip_bigram_model[skip_bigram.model]

    corpus_stats --> |vocab| make_dict[akaza-data make-dict]
    training --> make_dict
    unidic --> make_dict
    skk_base --> make_dict
    make_dict --> system_dict[SKK-JISYO.akaza<br/>システム辞書]

UniDic からのカタカナ語抽出

システム辞書の構築時に、UniDic CSJ(話し言葉コーパス版)の語彙データ (lex_3_1.csv) からカタカナ語を抽出してシステム辞書に追加している。

IPAdic はコーパス(Wikipedia・青空文庫)のトーカナイズに使用する辞書であり、語彙のカバー範囲が限定的である。UniDic は国立国語研究所が整備した大規模な辞書で、IPAdic に含まれないカタカナ外来語(例: アグリゲーション、インフラストラクチャー等)を豊富に収録している。これらの語彙をシステム辞書に取り込むことで、カタカナ語の変換カバレッジを補強している。

抽出条件: 表層形が全てカタカナで構成されるエントリのみを対象とし、読み(ひらがな)→ 表層形(カタカナ)の辞書エントリとして登録する。

成果物

ファイル内容
unigram.model単語出現コスト (MARISA Trie)
bigram.model単語間遷移コスト (MARISA Trie)
skip_bigram.model1語スキップ遷移コスト (MARISA Trie)
SKK-JISYO.akazaシステム辞書 (SKK-JISYO.L に含まれない語彙)

3. ユーザー言語モデル

Akaza はユーザーごとに学習が可能なように設計されている。 シンプルに実装するために、ユーザー言語モデルはプレインテキスト形式で保存される。 プレインテキスト形式なので、ユーザーは自分の好きなようにファイルを変更することが可能である。

変換エンジンの仕組み

このページでは、Akaza のかな漢字変換エンジンがひらがな入力から漢字かな混じり文を生成するまでの流れを説明する。

全体の流れ

入力(ひらがな列)
  ↓
1. セグメンテーション — 共通接頭辞検索で分割候補を列挙
  ↓
2. ラティス構築 — 各分割候補に漢字候補ノードを生成
  ↓
3. ビタビアルゴリズム — 前向き DP で上位 k 個の経路を計算
  ↓
4. パス抽出 — 後ろ向きトレースで k 本のパスを取り出す
  ↓
5. リランキング — 特徴量ベースの重み付きスコアで再順位付け
  ↓
出力(漢字かな混じり文の候補リスト)

1. セグメンテーション

入力ひらがな列を辞書に存在する単語単位に分割する。内部では KanaTrie(かな → 漢字候補のマッピング)を使い、各位置から始まる共通接頭辞をすべて列挙する。

例: わたしはがっこう の場合

  • 位置 0 から: , わた, わたし, …
  • 位置 9 から: , …
  • 位置 12 から: , がっ, がっこ, がっこう, …

辞書にマッチしない位置では、1 文字ずつのフォールバック候補が自動生成される。 また、ユーザーが Shift+矢印で文節境界を指定した場合は force_ranges として渡され、その境界が強制される。

数字+かな複合セグメント

ASCII 数字がマッチした場合、数字の後に続くかなについてもトライ検索を試行し、複合セグメントを追加する。例えば入力 90ぎょう に対して:

  • 個別セグメント: 90 + ぎょう
  • 複合セグメント: 90ぎょう

両方を候補として保持し、Viterbi が LM スコアに基づいて最適なパスを選択する。

実装: libakaza/src/graph/segmenter.rsSegmenter::build()

2. ラティス構築

セグメンテーション結果をもとに、ラティスグラフ(有向非巡回グラフ)を構築する。

各分割候補(読み)に対して、以下の候補ノードが生成される:

  • システム辞書の漢字候補 — SKK-JISYO 等から取得(例: わたし, 渡し, わたし
  • ユーザー辞書の候補 — ユーザーが登録した変換候補
  • 自動生成候補 — ひらがなそのまま、カタカナ変換
  • 数値変換候補 — 数字パターンの自動変換(漢数字等)
  • 数字+かな複合候補 — 複合セグメント(例: 90ぎょう)に対して、かな部分を辞書検索し 90行, 90業 等の候補を生成。漢数字候補(九十行 等)も追加。LM lookup は <NUM>行/<NUM>ぎょう のように正規化してフォールバック

グラフの先頭には BOS(文頭)ノード、末尾には EOS(文末)ノードが配置される。

BOS → [わたし: 私, 渡し, ...] → [は: は, 葉, ...] → [がっこう: 学校, ...] → EOS
       [わた: 綿, ...]  →  [しは: ...] → ...

各ノードは以下の情報を持つ:

フィールド内容
surface漢字表記(例:
yomi読み(例: わたし
word_id言語モデル内の単語 ID
unigram_costユニグラムコスト

実装: libakaza/src/graph/graph_builder.rsGraphBuilder::construct()

3. ビタビアルゴリズム(前向き DP)

ラティスグラフ上で前向き動的計画法を実行し、BOS から EOS までの最小コスト経路を求める。

コストの種類

ユニグラムコスト

単語の出現しやすさを表すコスト。加法スムージングを適用した対数確率:

unigram_cost(word) = -log₁₀((count(word) + α) / (N + α × V))
  • count(word): コーパスでの出現回数
  • N: 全単語の出現回数合計
  • V: 語彙数
  • α = 0.00001: スムージング定数

値が小さいほど(0 に近いほど)出現しやすい単語を意味する。

バイグラムコスト

直前の単語 w_{n-1} から現在の単語 w_n への遷移コスト:

bigram_cost(w_{n-1}, w_n) = -log₁₀(P(w_n | w_{n-1}))

MARISA Trie に格納されたバイグラム言語モデルから取得する。該当ペアが存在しない場合はデフォルトのフォールバックコストが使用される(未知バイグラム)。

スキップバイグラムコスト

1 語飛ばしの遷移コスト。直前ではなく 2 語前の単語 w_{n-2} と現在の単語 w_n の関係を捉える:

skip_bigram_cost(w_{n-2}, w_n)

バイグラムだけでは捉えられない、やや離れた文脈の整合性を補完する。

K-Best ビタビ

標準のビタビでは各ノードに最小コストの前ノードを 1 つだけ記録するが、K-Best ビタビでは上位 k 個のエントリを保持する。各エントリは以下を記録する:

フィールド内容
costBOS からの累積コスト
prev_node前のノード
prev_rank前ノードの何番目のエントリか
コスト内訳ユニグラム / バイグラム / 未知バイグラム / スキップバイグラムの各合計
token_countパス内のトークン数

前向き DP では、各ノードについてすべての前ノード × 前ノードの k エントリの組み合わせからコストを計算し、上位 k 個を残す。

実装: libakaza/src/graph/graph_resolver.rsGraphResolver::resolve_k_best()

4. パス抽出

EOS ノードの k エントリから、(prev_node, prev_rank) チェーンを逆向きにたどって k 本のパスを取り出す。

重複排除

同じ分節パターン(各文節の読みの長さの列)を持つパスは、コストが低い方のみを残す。これにより、漢字候補だけが異なるパスが重複して表示されることを防ぐ。

分節パターンが異なるパスは Tab キーで切り替え、同じ分節内の漢字候補は ↑/↓ キーで切り替える。この 2 つは直交する概念である。

候補の補完

各ノード位置で同じ読みを持つ代替候補(漢字表記の異なるもの)を収集し、候補リストに含める。候補が 5 個未満の場合は、長い読みを再帰的に分割したブレークダウン候補も生成される。

5. リランキング

ビタビで得られた k 本のパスを、重み付きスコアで再順位付けする。

スコア計算式

rerank_cost = 1.0 × Σ unigram_cost
            + bigram_weight × Σ bigram_cost
            + unknown_bigram_weight × Σ unknown_bigram_cost
            + length_weight × token_count
            + skip_bigram_weight × Σ skip_bigram_cost

デフォルト重み

パラメータデフォルト値役割根拠
bigram_weight1.0既知バイグラムの重み同音異義語判別に不可欠。下げると「板が厚い」vs「お湯が熱い」等の判別力が低下
unknown_bigram_weight1.0未知バイグラムの重み0.3 や 0.1 では Good が 496〜647 件悪化。デフォルト 1.0 が最良 (#434)
length_weight2.0トークン数によるペナルティ(短い分割を優先)2.0〜3.0 でピーク(+45〜47 件)。5.0 以上で逆効果。悪化は全て Good→Top-5 で Good→Bad は 0 件 (#434, #435)
skip_bigram_weight0.2スキップバイグラムの重み隣接 bigram より疎な信号のため控えめな重み。Viterbi DP に統合後のグリッドサーチで決定 (#437, #440)

unigram_weight は基準スケールとして 1.0 に固定し、他の重みを相対的に探索する設計になっている。これにより探索空間を 1 次元減らし、グリッドサーチが安定する。

設計意図

ビタビは等重み(unigram + bigram の単純加算)で多様な候補を生成し、リランキングで最終選択に最適化された重みを適用する。候補生成と最終選択で最適な重みが異なるのは自然であり、この 2 段階構成により柔軟なチューニングが可能になる。

length_weight = 2.0 により、トークン数が少ない(= 長い単位でまとめた)分割が優先される。Σcost はトークン数に比例して増えるため、分節パターンが異なる候補が混ざると短い分割が不当に有利になる副作用があり、length_weight でこれを補正する。例えば「とうきょうと」→「東京都」(1トークン)が「東京と」(2トークン)より選ばれやすくなる。

各重みの詳細な評価結果はリランキング評価レポートを参照。リランキング機構全体の設計方針はリランキング設計メモを参照。

実装: libakaza/src/graph/reranking.rsReRankingWeights::rerank()

言語モデル

変換エンジンが使用する言語モデルは MARISA Trie 形式で格納される。

モデル格納形式内容
unigram.model{surface}/{yomi}\xff{score}単語出現コスト
bigram.model[3B word_id1][3B word_id2][4B score]単語間遷移コスト
skip_bigram.model[3B word_id1][3B word_id2][2B score]1語スキップ遷移コスト

ユーザー言語モデルはプレインテキスト形式で保存され、システム言語モデルより優先して参照される。詳細はユーザーデータを参照。

言語モデルの構築パイプラインについてはデータフローを参照。

エンジンの呼び出し

変換エンジンの主要なエントリポイントは BigramWordViterbiEngine が提供する:

メソッド用途
convert()1-best 変換(最上位の候補のみ返す)
convert_k_best()k-best 変換(上位 k 個の分節パターンを返す)
learn()ユーザーの確定結果を学習する

convert_k_best() の内部では、ラティス構築 → ビタビ → リランキングの全パイプラインが実行される。

実装: libakaza/src/engine/bigram_word_viterbi_engine.rs

コーパス学習 (learn-corpus)

このページでは、akaza-data learn-corpus によるモデルの学習処理を説明する。

概要

Wikipedia・青空文庫等から生成した統計ベースの言語モデル(データフロー参照)には偏りがある。learn-corpus は手作業で作成した教師コーパスを使い、言語モデルのカウント値を直接調整することで変換精度を補正する。

アルゴリズムは誤り駆動のカウント調整であり、構造化パーセプトロンなどの識別学習は実装されていない。構造化パーセプトロンの検討メモは設計メモを参照。

アルゴリズム

学習ループ

for each コーパスファイル (may → should → must):
    for each エポック (1..最大エポック数):
        for each 教師文:
            1. 教師文の「よみ」をビタビで変換
            2. 変換結果が正解と一致 → スキップ
            3. 不一致 → 正解パス上のカウントを調整
        全問正解なら早期終了

カウント調整

変換結果が不正解のとき、正解パス上の単語・バイグラム・スキップバイグラムの出現カウントを delta だけ減算する:

#![allow(unused)]
fn main() {
// ユニグラム: 正解パスの各単語
new_count = old_count - delta

// バイグラム: 正解パスの隣接ペア (BOS→先頭, 末尾→EOS を含む)
new_count = old_count - delta

// スキップバイグラム: 正解パスの1語飛ばしペア
new_count = old_count - delta
}

カウントは u32 型で保持されている。delta がカウントを超える場合は符号なし整数のアンダーフローにより非常に大きな値になり、結果としてコスト(-log₁₀(count / total))が極端に低くなる。これにより、コーパスに登場頻度が低い正解単語が強制的にコスト最小化される。

運用パラメータ

実際の運用(akaza-default-model)では以下のパラメータが使用されている:

パラメータ意味
delta20001回の不正解あたりの減算量
may_epochs10may.txt の最大エポック数
should_epochs100should.txt の最大エポック数
must_epochs10000must.txt の最大エポック数

教師コーパス

形式

Kytea のフルアノテーション形式を採用。各行が1つの教師文で、表層/よみ のペアをスペース区切りで記述する:

東京都/とうきょうと 特許/とっきょ 許可/きょか 局/きょく
私/わたし は/は 学校/がっこう に/に 行く/いく

品詞は扱わず、「どの位置で変換候補が区切られていたら自然か」という観点で区切る。

三段階の優先度

教師コーパスは重要度に応じて3ファイルに分かれ、弱い順に学習される:

ファイルエポック数用途
may.txt10できれば変換できてほしい表現
should.txt100変換できてほしい表現
must.txt10,000必ず変換できなければならない表現

弱いコーパスを先に学習し、その後に強いコーパスを学習することで、重要度の高い表現が確実に反映される。各コーパスは全問正解になった時点で早期終了する。

学習の流れ(全体)

akaza-corpus-stats の統計データ (wordcnt trie)
  ↓
learn-corpus (may → should → must の順にカウント調整)
  ↓
unigram.model / bigram.model / skip_bigram.model (MARISA Trie)
  1. wordcnt trie をオンメモリの HashMap にロード
  2. 教師コーパスに含まれるがモデルに未登録の単語を追加(初期カウント 1)
  3. may → should → must の順に学習ループを実行
  4. 調整後のカウントから MARISA Trie 形式のモデルファイルを生成

実装: akaza-data/src/subcmd/learn_corpus.rs

評価方法

このページでは、Akaza のかな漢字変換精度の評価方法を説明する。

評価コーパス

評価には anthy-unicode プロジェクトに含まれるコーパスを利用している。このコーパスは public domain で公開されており、calctrans ディレクトリから取得できる。

anthy-unicode プロジェクトおよびコーパスの作成者に感謝する。このコーパスのおかげで、Akaza の変換精度を定量的に評価し、改善の効果を客観的に測定できている。

コーパスのフォーマット

各行は | で文節を区切った読みと正解表記のペアで構成される:

|読み1|読み2|...|読みN| |正解1|正解2|...|正解N|

例:

|uim-fepの|あたらしい|ばーじょん| |uim-fepの|新しい|バージョン|

# で始まる行はコメント、空行は無視される。

使用するファイル

ファイル行数(データ行)内容
corpus.0.txt0デバッグ用(データなし)
corpus.1.txt1,743一般的な変換テスト
corpus.2.txt23追加テスト
corpus.3.txt9,252大規模テスト
corpus.4.txt10誤変換の例示(評価には使用しない
corpus.5.txt47追加テスト

corpus.4.txt~! で誤変換パターンを記録したファイルであり、正解データではないため評価から除外している。

評価に使用するのは corpus.0, 1, 2, 3, 5 の 5 ファイルで、合計約 11,065 件のテストケースとなる。

評価指標

参考文献

評価方法は以下の論文に記載の手法を採用している:

日本語かな漢字変換における識別モデルの適用とその考察 ANLP 2011 C4-6

指標の定義

指標説明
GoodTop-1(最上位候補)がコーパスの正解と完全一致した件数
Top-kTop-1 では不正解だが、上位 k 個の候補のいずれかに正解が含まれる件数
Bad上位 k 個のどの候補にも正解が含まれない件数
再現率LCS(最長共通部分列)ベースの文字レベル再現率

再現率の計算

再現率は、変換結果とコーパスの正解の間の**最長共通部分列(LCS)**を用いて計算する:

再現率 = 100 × Σ LCS(正解, 変換結果) / Σ |変換結果|
  • LCS(正解, 変換結果): 正解文字列と変換結果の最長共通部分列の文字数
  • |変換結果|: 変換結果の文字数
  • 全テストケースについて分子・分母をそれぞれ合計してから割合を求める

完全一致であれば再現率 100% となる。部分的な一致(例: 一部の文節だけ正解)も定量的に評価できる点が利点である。

前処理

評価時に以下の前処理を行う:

  • 文節区切りの除去: | を除去して平文に変換
  • 全角数字の正規化: を半角 09 に変換

評価の実行

akaza-data evaluate コマンドで評価を実行する。akaza-default-model リポジトリ内で以下のように使用する:

akaza-data evaluate \
  --corpus anthy-corpus/corpus.0.txt \
  --corpus anthy-corpus/corpus.1.txt \
  --corpus anthy-corpus/corpus.2.txt \
  --corpus anthy-corpus/corpus.3.txt \
  --corpus anthy-corpus/corpus.5.txt \
  --eucjp-dict skk-dev-dict/SKK-JISYO.L \
  --utf8-dict data/SKK-JISYO.akaza \
  --model-dir data/ -vv

評価はマルチスレッドで並列実行される。

リランキング重みの評価

リランキング重みを変更して評価する場合は、--bigram-weight--unknown-bigram-weight--length-weight--skip-bigram-weight オプションを指定する:

akaza-data evaluate \
  --corpus ... \
  --length-weight 2.5 \
  ...

実装

実装: akaza-data/src/subcmd/evaluate.rs

文節伸縮・選択仕様

本ドキュメントは、Akaza における文節の伸縮操作(Shift+→ / Shift+←)と 文節選択(← / →)の実装仕様をまとめたものです。 実装の挙動を基準として記述しています。

用語

  • 文節: 変換候補が提示される単位(例: わたし|は|がっこう
  • 選択中の文節: カーソル/ハイライトが当たっている文節
  • 読み(かな): 変換対象となる文字列(ひらがな)

文節伸縮(Shift+→ / Shift+←)

  1. Shift+→ は 選択中の文節の読みを右へ1文字伸ばす
  2. Shift+← は 選択中の文節の読みを左へ1文字伸ばす
  3. 文節の伸縮に伴って 隣接文節は逆方向に1文字縮む
  4. 伸縮後も 選択中の文節のフォーカスは維持する。

文節選択(← / →)

  1. ← / → は 選択中の文節を左右に移動する。
  2. 文節が2つ以上ある場合、押下ごとに 必ず選択が変わる
  3. 端に到達した場合は 反対側に回り込む(ラップする)。
  4. 文節が0または1の場合、選択は変わらない。

右に伸ばす(Shift+→)

入力: わたしはがっこう
初期: わたし|は|がっこう

  • 選択: わたし
    Shift+→ 1回: わたしは|がっこう
    (次の文節「は」から1文字移動)

  • 選択:
    Shift+→ 1回: わたし|はが|っこう
    (次の文節「がっこう」から1文字移動)

左に伸ばす(Shift+←)

入力: わたしはがっこう
初期: わたし|は|がっこう

  • 選択:
    Shift+← 1回: わた|しは|がっこう
    (左の文節「わたし」から末尾1文字を移動)

  • 選択: がっこう
    Shift+← 1回: わたし|はがっこう

端の挙動

  • 右端の文節で Shift+→ を押しても、文節構成は変化しない。
  • 左端の文節で Shift+← を押した場合:
    • 左端が1文字なら変化しない
    • 左端が2文字以上なら末尾1文字を右隣へ移動する
    • 例:
      • 初期: わだ|た|そ(左端が2文字)
        • 選択: わだ
        • Shift+← 1回: わ|だた|そ
      • 初期: わ|た|そ(左端が1文字)
        • 選択:
        • Shift+← 1回: わ|た|そ(変化なし)

例外/注意点

  • 伸縮によって 隣接文節が消滅する場合は、文節数が減る。
  • サロゲートやマルチバイト文字は 1文字単位で扱う。
  • 変換候補が空の場合は 現状維持とする。

force_selected_clause

変換エンジンに「強制的な分節境界」を渡すための内部状態。 Shift+矢印による文節伸縮の結果は、force_selected_clause に保持され、 次回の変換に反映される。

  • 格納内容: 文字列内の範囲(Range<usize>)の配列。各範囲は1文節を表す。
  • 更新タイミング: Shift+→ / Shift+← の操作時。
  • 消去タイミング:
    • 生入力が変わったとき(on_raw_input_change 内)
    • 明示的にクリアしたとき(clear_force_selected_clause
  • 適用タイミング:
    • engine.convert(..., Some(&force_selected_clause)) で再変換に使用。
  • 文節選択への影響:
    • force_selected_clause がある場合、再変換しても current_clause は維持される(set_clauses 内の条件分岐)。

モデルファイルのメタデータ

Akaza のモデルファイルにはビルド時のバージョン情報が埋め込まれている。 これにより、インストール済みモデルのバージョンやビルド日時を確認できる。

メタデータ項目

キー説明
AKAZA_DATA_VERSIONビルドに使用した akaza-data のバージョン0.1.7
BUILD_TIMESTAMPビルド日時(UTC, ISO 8601)2026-02-20T12:34:56Z

確認方法

akaza-data model-info コマンドでメタデータを表示できる。

# marisa-trie 形式のモデルファイル
akaza-data model-info data/unigram.model
# File: data/unigram.model
# Type: marisa-trie
# Keys: 1058884
# AKAZA_DATA_VERSION: 0.1.7
# BUILD_TIMESTAMP: 2026-02-20T12:34:56Z

# bigram, skip_bigram も同様
akaza-data model-info data/bigram.model
akaza-data model-info data/skip_bigram.model

# SKK 辞書ファイル(テキスト形式)
akaza-data model-info data/SKK-JISYO.akaza
# File: data/SKK-JISYO.akaza
# Type: skk-dict
# AKAZA_DATA_VERSION: 0.1.7
# BUILD_TIMESTAMP: 2026-02-20T12:34:56Z

格納形式

marisa-trie 形式(.model ファイル)

既存のメタデータキー(__TOTAL_WORDS____DEFAULT_EDGE_COST__ 等)と同じパターンで、 特殊キーとして trie に格納される。

__BUILD_TIMESTAMP__\t2026-02-20T12:34:56Z
__AKAZA_DATA_VERSION__\t0.1.7

対象ファイル:

  • unigram.model
  • bigram.model
  • skip_bigram.model

テキスト形式(SKK-JISYO.akaza)

SKK 辞書フォーマットのコメント行(;; で始まる行)としてファイル先頭に記録される。

;; AKAZA_DATA_VERSION: 0.1.7
;; BUILD_TIMESTAMP: 2026-02-20T12:34:56Z
;; okuri-ari entries.
;; okuri-nasi entries.
...

後方互換性

メタデータが存在しない古いモデルファイルでも正常に動作する。 model-info コマンドではメタデータがない場合 (not set) と表示される。

ユーザー固有データについて

akaza はユーザー固有のデータを保持し利用する。 これによりユーザーごとに学習され、パーソナライズすることが可能である。

ユーザー固有データは以下の3つの部分からなる。

ユーザー入力統計データ

ユーザーが入力したデータの統計データである。

ユーザーが入力した単語の、unigram と bigram が統計データとして保存される。 保存されるのは「漢字」の方。

ユーザー言語モデル

統計データは以下の形で集計される。

  • C: ユーザーが入力した単語のユニーク数
  • V: ユーザーが入力した単語の総数
  • word_count: 単語ごとの漢字入力回数

これらをもとに、コストを計算する。ユーザー言語モデルから得られるコスト値は、システム辞書に記録されるコスト値よりも低く設定されている。これにより、一度入力した単語は強烈に表出するようになる。

ユーザー共通接頭辞

入力データの「かな」部分を利用して trie を構築する。

目指している形

SKK では、一度入力されたデータはユーザー辞書に登録されていく。これにより強烈にパーソナライズされていくので、そうそう誤変換しなくなっていく。

これと同じようなユーザー体験を得られるようにしていきたい。

ユーザーデータの保護

背景

Akaza はユーザーの変換履歴を学習データとして保存する。具体的には、ユーザーが変換を確定するたびに以下の統計データが記録される:

  • unigram: 確定した単語の頻度(例: 互換/ごかん 3
  • bigram: 隣接する単語ペアの頻度(例: 互換/ごかん\t性/せい 2
  • skip-bigram: 1語飛ばしの単語ペアの頻度

これらのデータにはユーザーが「何を変換したか」が記録されるため、プライバシー上のリスクがある。ファイルを閲覧できる第三者がユーザーの入力内容を推測できる可能性がある。

一方、ユーザー辞書 (SKK-JISYO.user) は SKK 辞書形式のテキストファイルであり、ユーザーが手動で編集する用途もあるため、保護の対象外とする。

方針

Mozc のデータ保護設計 を参考に、以下の方針でユーザー統計データを保護する。

保護対象

ファイル内容保護
unigram.v2.bin単語の使用頻度AES256 CBC で難読化
bigram.v2.bin隣接語ペアの頻度AES256 CBC で難読化
skip_bigram.v2.bin1語飛ばしペアの頻度AES256 CBC で難読化
SKK-JISYO.userユーザー辞書保護なし(平文テキスト)

保護しない対象

  • ユーザー辞書 (SKK-JISYO.user): ユーザーが手動編集する用途があるため
  • システム辞書・言語モデル: 公開データから生成されるため

設計

レイヤー構成

鍵管理と暗号化ロジックを分離する。

┌─────────────────────────────────────────┐
│  フロントエンド (鍵の管理)                  │
│  ibus-akaza / mac-akaza:                │
│    encryption.key ファイル (0600)         │
│    将来: macOS Keychain 等               │
├─────────────────────────────────────────┤
│  libakaza (暗号化・復号ロジック + 鍵管理)    │
│  encryption_key モジュールで鍵生成・読込    │
│  user_stats_utils で AES256 CBC 暗号化   │
└─────────────────────────────────────────┘

libakaza は暗号化・復号のロジック、バイナリ形式の読み書き、および鍵ファイルの生成・読み込みを担当する。

各フロントエンド は libakaza の load_or_create_default_encryption_key() を呼び出し、取得した鍵を UserData に渡す:

  • ibus-akaza (Linux): ~/.local/share/akaza/encryption.key0600 パーミッションで保存
  • mac-akaza (macOS): 同上。将来的に Keychain への移行が可能

この分離により、プラットフォームごとに鍵の保護レベルを独立して向上させることができる。

難読化の仕組み

Mozc と同様に AES256 CBC を使用してユーザー統計データを難読化する。

  1. 初回起動時にランダムな 32 バイト鍵を生成し、鍵ファイル (encryption.key) に保存する
  2. 統計データの保存時に AES256 CBC で暗号化する
  3. 統計データの読み込み時に復号する

鍵はマシンごとに異なる値が生成される。プロダクト共通のマスターキーは使用しない。

ファイル形式

統計データはバイナリ形式で保存する。テキストエディタで開いても内容を読むことはできない。

[magic: 4 bytes "AKZ\x01"]  // フォーマット識別 + バージョン
[iv: 16 bytes]               // AES256 CBC の IV(保存ごとにランダム生成)
[encrypted payload]          // bincode(HashMap<String, u32>) を暗号化したもの
  • シリアライズ: bincode(Rust との親和性が高く高速)
  • 暗号化: AES256 CBC(IV は保存のたびにランダム生成)
  • 書き出し方式: 3秒間隔で HashMap 全体をシリアライズ → 暗号化 → 書き出し。一時ファイル + rename によるアトミック書き込み
  • 追記方式は採用しない: レコード単位の暗号化はオーバーヘッドが大きく、コンパクション等の複雑さに見合わない。ユーザーの語彙数(数千〜数万エントリ)であれば全体書き出しで十分高速

ファイルパーミッション

全てのユーザーデータファイルは 0600(所有者のみ読み書き可能)で保存する。これは現行の実装と同様である。

セキュリティ上の注意

Mozc の設計文書にも明記されているとおり、この難読化は同一ユーザー権限で動作するプロセスからの保護を意図していない。信頼境界はユーザーアカウントのレベルに設定する。

保護の目的は以下のとおり:

  • ファイルを cat 等で開いたときに変換履歴が即座に読めない状態にすること
  • カジュアルな覗き見を防止すること

同一ユーザー権限を持つ攻撃者に対する完全な保護は、このレイヤーでは提供しない。

マイグレーション

旧形式(v1、テキスト)と新形式(v2、暗号化バイナリ)の共存期間を設け、段階的に移行する。

ロード時の挙動

  1. 新形式ファイル(unigram.v2.bin 等)が存在すれば、それを読み込む
  2. 新形式が存在せず旧形式(unigram.v1.txt 等)が存在すれば、旧形式を読み込む
  3. どちらも存在しなければ初期状態から開始(初回起動時)

保存時の挙動

  • 常に新形式で保存する
  • 旧形式ファイルは削除しない(ダウングレード時の安全策として残す)

将来の旧形式サポート削除

十分な移行期間を経た後、旧形式の読み込みサポートを削除する。削除時期はリリースノートで事前に告知する。

デバッグ支援

暗号化によりデバッグが困難になることを防ぐため、akaza-data コマンドに復号表示機能を提供する。

# ユーザー統計データの内容を復号して表示
akaza-data dump-user-stats --key-file ~/.local/share/akaza/encryption.key

参考文献

設計メモ・レポート

このセクションには、Akaza の開発過程で作成された設計メモや評価レポートを掲載している。 変換エンジンの仕組みについては変換エンジンの仕組みを参照。

目次

k-best 分節パターン切り替え

概要

変換時に、ビタビアルゴリズムが上位 k 個の分節パターン(文節の区切り方)を自動的に列挙する。 ユーザーは Tab キー で分節パターンを切り替えることができる。

従来の動作

従来は、ビタビアルゴリズムが最もコストの低い 1 本のパスのみを返していた。 分節の区切り方を変えるには Shift+←/→ で手動調整する必要があった。

例: 「きたかな」を変換

  • 変換結果: 北香那(1文節)
  • 「来た|かな」(2文節)にしたい場合は Shift+← で手動調整が必要

新しい動作

変換時に最大 5 個の分節パターンが自動計算される。 Tab キーを押すと次の分節パターンに切り替わる。

例: 「きたかな」を変換

  1. Tab 0回目: 北香那(1文節)
  2. Tab 1回目: 来た|かな(2文節)
  3. Tab 2回目: 気|高菜(2文節、別の区切り方)
  4. さらに Tab: 1 に戻る(循環)

操作方法

キー状態動作
Tab変換中次の分節パターンに切り替え
Shift+→変換中文節を右に伸ばす(従来通り)
Shift+←変換中文節を左に伸ばす(従来通り)

Shift+矢印との関係

  • Tab で分節パターンを切り替えた後、Shift+←/→ で微調整できる。
  • Shift+←/→ を押すと分節パターンの選択は 1-best(最初のパターン)にリセットされ、 手動境界指定で再変換が実行される。

技術的な仕組み

k-best ビタビ

従来のビタビアルゴリズムでは各ノードに対して「最もコストの低い前ノード」を 1 つだけ記録していた(prevmap: HashMap<&WordNode, &WordNode>)。

k-best では各ノードに対して上位 k 個のエントリ(コスト、前ノード、前ノードの rank)を 保持する。バックトラック時に各エントリの (prev_node, prev_rank) チェーンをたどることで k 本のパスを抽出する。

重複排除

同じ分節パターン(各文節の読みの長さの列)を持つパスは、 低コスト側のみを残す。これにより、漢字候補だけが異なるパスが重複して 表示されることを防ぐ。

候補の関係

  • 分節パターン(Tab で切り替え): 文節の区切り方が異なる
    • 例: 北香那 vs 来た|かな
  • 漢字候補(↑/↓ で切り替え): 同じ文節内の異なる漢字変換
    • 例: 来た vs (どちらも「きた」の候補)

この 2 つは直交する概念であり、Tab で分節パターンを選んだ後、 ↑/↓ で各文節の漢字候補を選ぶことができる。

k-best リランキング機構

背景

現在の Viterbi アルゴリズムでは、パスコストが Σ(unigram_cost + bigram_cost) の等重み合算で決まる。 bigram コストが支配的になりやすく、レアな bigram の異常値に引きずられて誤変換が発生するケースがある。

例:

  • たまたまコーパスに「は→厚い」の bigram が多い → 文脈に関係なく「厚い」が勝つ
  • bigram は直前の 1 単語しか見ないため、「夏は暑い」vs「板は厚い」を区別できない

方針: 特徴量ベースの線形リランキング

Viterbi(等重み)で k-best 候補を生成した後、重み付きスコアで再順位付けを行う。

Viterbi (等重み) → k-best 候補生成(多様な候補の探索に最適化)
       ↓
ReRanking (重み付き) → 最終順位(最終選択に最適化)

候補生成と最終選択で最適な重みが異なるのは自然であり、 Viterbi 側は等重みのまま維持して多様な候補を確保する。

初期フェーズの特徴量

既存のコスト情報を分離し、パス長と未知 bigram を別特徴量として追加する:

rerank_score = 1.0 × Σ unigram_cost           (固定)
             + bigram_weight × Σ bigram_cost
             + length_weight × token_count
             + unknown_bigram_weight × unknown_bigram_cost_sum
  • unigram_weight: 1.0 に固定(基準スケールとして使い、他の重みを相対的に探索する)
  • bigram_weight: デフォルト 1.0(= 従来と同じ挙動)
  • length_weight: デフォルト 0.0(= パス長正規化なし)
  • unknown_bigram_weight: デフォルト 1.0(= 通常の bigram と同じ扱い)

なぜ unigram_weight を固定するか

(unigram_weight, bigram_weight) はスケール不定で、 例えば (1.0, 0.7) と (10.0, 7.0) は同じ順位になる。 unigram を基準に固定することで探索空間を 1 次元減らし、グリッドサーチが安定する。

パス長正規化(length_weight)

Σ cost はトークン数に比例して増えるため、分節パターンが異なる候補が混ざると 短い分割が不当に有利になる副作用がある。 length_weight に正の値を入れると長い分割にボーナス、負の値でペナルティを制御できる。

未知 bigram の分離(unknown_bigram_weight)

「レアな bigram 異常値」の多くは、実際には未知 bigram のフォールバック (default_edge_cost)が原因。bigram 全体を弱めるより、未知 bigram だけを 別特徴量として切り出す方が副作用が少ない。 既知 bigram の判別力を維持しつつ、未知の暴れだけを抑制できる。

スケールの事前確認

重み探索の前に、dev コーパスで以下を確認しておく:

  • Σ unigram_costΣ bigram_cost の平均・分散
  • パス長(トークン数)との相関
  • 未知 bigram のフォールバック回数と寄与

bigram はエッジ数ぶん足されるため絶対値が大きくなりがちで、 unigram とスケールが大きく異なる場合がある。 この分布を把握しておくと、グリッドサーチの範囲が妥当になる。

将来の拡張

リランキングフレームワークが入れば、特徴量を追加するだけで拡張できる。 費用対効果を考慮した導入順:

順序特徴量効果実装コスト
Phase 1unigram/bigram 分離 + len + unknown bigram基盤構築 + 未知ノイズ抑制
Phase 2ルールベースペナルティ(特徴量として)既知の誤パターン抑制
Phase 3Skip-gram 埋め込み離れた単語間の意味的整合性
(将来)Trigram スコア2 単語前まで文脈を拡張中(コーパス規模が増えてから再検討)

ルールペナルティは if/else で分岐するのではなく、特徴量として加点/減点する設計にする。 将来の重み学習と一貫性を保つため。

ルールペナルティの候補:

  • 数字表記(漢数字/アラビア)不整合ペナルティ
  • ひらがな連続が不自然(助詞崩壊)ペナルティ

Trigram vs Skip-gram の比較

TrigramSkip-gram
頻出パターンへの効果高い高い
未知の組み合わせへの汎化弱い(スパース性の壁)強い(分散表現で汎化)
モデルサイズ大きくなりがち(bigram trie ~186MB の拡張)制御しやすい(語彙数 × 次元)
既存コストとの統合対数確率なので自然に加算確率ではないが、線形モデルの特徴量としては問題なし
実装の連鎖コスト高い(モデル構築・保存形式・検索・backoff 設計)中(学習は外部ツール可、推論は内積のみ)

現在のコーパス規模(Wikipedia + 青空文庫)では trigram のスパース性が厳しいため、 skip-gram の方が費用対効果は高いと予想される。 trigram はコーパス規模が十分に増えた段階で再検討する。

設計

KBestPath の拡張

viterbi_cost(候補生成で使った元のコスト)と rerank_cost(リランキング後のスコア)を 明確に分離する。cost フィールドの上書きは混乱の原因になるため避ける。

#![allow(unused)]
fn main() {
pub struct KBestPath {
    pub segments: Vec<Vec<Candidate>>,
    pub viterbi_cost: f32,              // Viterbi DP の合算コスト(変更しない)
    // リランキング用の特徴量内訳
    pub unigram_cost: f32,              // Σ unigram コスト
    pub bigram_cost: f32,               // Σ bigram コスト(既知 bigram のみ)
    pub unknown_bigram_cost: f32,       // Σ 未知 bigram のフォールバックコスト
    pub unknown_bigram_count: u32,      // 未知 bigram の回数
    pub token_count: u32,               // パス内のトークン数
    pub rerank_cost: f32,               // リランキング後のスコア(ソートキー)
}
}

ReRankingWeights

#![allow(unused)]
fn main() {
pub struct ReRankingWeights {
    // unigram_weight は 1.0 固定(基準スケール)
    pub bigram_weight: f32,             // デフォルト 1.0
    pub length_weight: f32,             // デフォルト 0.0
    pub unknown_bigram_weight: f32,     // デフォルト 1.0
}

impl ReRankingWeights {
    pub fn rerank(&self, paths: &mut [KBestPath]) {
        for path in paths.iter_mut() {
            path.rerank_cost = path.unigram_cost
                + self.bigram_weight * path.bigram_cost
                + self.unknown_bigram_weight * path.unknown_bigram_cost
                + self.length_weight * path.token_count as f32;
        }
        paths.sort_by(|a, b| a.rerank_cost.partial_cmp(&b.rerank_cost).unwrap());
    }
}
}

デフォルト値 (bigram_weight=1.0, length_weight=0.0, unknown_bigram_weight=1.0) では rerank_cost = unigram_cost + bigram_cost + unknown_bigram_cost = viterbi_cost となり、 従来と完全に同じ挙動になる。

重みの設定箇所

用途設定方法
akaza-data checkCLI: --bigram-weight 0.7 --length-weight 0.1
akaza-data evaluateCLI: 同上。グリッドサーチで最適値を探索可能
ibus-akazaconfig.ymlengine.reranking_weights

変更対象ファイル

  • libakaza/src/graph/graph_resolver.rs — KBestPath にコスト内訳追加、forward DP で分離記録
  • libakaza/src/graph/lattice_graph.rs — edge cost 取得時に既知/未知を区別する情報を返す
  • libakaza/src/reranking.rs (新規) — ReRankingWeights と rerank 関数
  • akaza-data/src/subcmd/check.rs — CLI オプション追加
  • akaza-data/src/subcmd/evaluate.rs — CLI オプション追加、評価メトリクス拡張
  • libakaza/src/config.rs — EngineConfig に reranking_weights 追加

期待される効果

ポジティブな効果

  1. 未知 bigram ノイズの抑制: unknown_bigram_weight を下げることで、未知 bigram のフォールバック異常値に引きずられにくくなる。既知 bigram の判別力は維持される
  2. 一般的な単語の安定化: bigram の重みが相対的に下がることで、unigram(単語自体の出現しやすさ)がアンカーとして機能し、変換結果が安定する
  3. パス長バイアスの制御: length_weight により、短い分割が不当に有利になる副作用を補正できる
  4. チューニングの容易化: evaluate コーパスに対してグリッドサーチで最適重みを探索できる。従来は bigram/unigram の比率を変えるにはモデル再構築が必要だった
  5. 拡張の基盤: 将来の特徴量追加(ルールペナルティ、skip-gram 等)が容易になる

定量的な期待

  • akaza-data evaluate の exact match rate が数ポイント改善する可能性がある
  • 特に未知 bigram が多い短い入力(2〜3 文節)で効果が出やすい
  • 既知 bigram カバレッジが高い頻出パターンでは従来と同等の精度を維持

退行リスク

リスク1: bigram を弱めすぎると同音異義語の判別力が低下

bigram は同音異義語の判別に本質的に効いている(例: 「板が厚い」vs「お湯が熱い」)。 bigram_weight を下げすぎると、直前の文脈が効かなくなり、これらのケースで退行する。

対策: evaluate コーパスの must.txt / should.txt で退行検知。デフォルト重みでは従来と完全に同じ挙動を保証。

リスク2: Viterbi の候補生成と最終順位の乖離

Viterbi は等重みで候補を生成するため、リランキング後に「本来 1 位になるべきパスが k-best に含まれていない」可能性がある。

対策:

  • k の値を十分大きくする(5〜10)
  • evaluate で top-k hit rate を常に出力し、「rerank の改善余地が候補生成で潰れていないか」を数値で追跡
  • 必要に応じて候補生成側の多様化(unknown bigram フォールバックのクリップ等)

リスク3: 重みの過学習

evaluate コーパスに過度にフィットした重みは、汎用的な変換で退行する可能性がある。

対策: コーパスを train/dev に分割して交差検証。極端な重み(0.0 や 10.0 等)にならないよう範囲を制限。

リスク4: パフォーマンスへの影響

リランキング自体は k 個のパスを再ソートするだけなので計算コストはほぼゼロ。 KBestPath にフィールドが増えるが、f32 数個の追加のみで影響は無視できる。

検証手順

実装フェーズ

安全のため段階的に進める:

  1. Phase 1: コスト分離のみ。KBestPath に viterbi_cost/rerank_cost/特徴量内訳を追加。デフォルトでは rerank_cost = viterbi_cost で完全一致を確認
  2. Phase 2: CLI/config でリランキング重みを設定可能にし、rerank を有効化
  3. Phase 3: evaluate に top-k hit rate、must/should の差分レポートを追加
  4. Phase 4: length_weight と unknown_bigram_weight を特徴量として追加(ここで精度が動きやすい)

評価メトリクス

重み探索時には以下を同時に確認する:

  • top-1 accuracy (exact match rate)
  • top-k hit rate (k=5, k=10)
  • must.txt の退行数 — 0 であること
  • should.txt の改善数/退行数
  • LCS-based recall (既存メトリクス)

テスト手順

  1. cargo test --all で既存テストが pass することを確認
  2. デフォルト重みで akaza-data evaluate の結果が従来と完全に一致することを確認
  3. dev コーパスで Σ unigram_costΣ bigram_costunknown_bigram_count の分布を確認
  4. bigram_weight を 0.5〜0.9 で変えながら evaluate を実行し、精度の変化を観察
  5. must.txt の全ケースで退行がないことを確認
  6. akaza-data check で代表的な入力を手動確認

リランキング evaluate/check 修正 & 評価レポート

背景

リランキング実装 (#432) で重みパラメータを変えても evaluate / check の結果が変わらないバグがあった。

原因

2箇所で convert_k_best(リランキング適用済み)を経由していなかった。

1. check.rs

k-best モードで engine.graph_resolver.resolve_k_best(&lattice, k) を直接呼んでおり、リランキングが適用されていなかった。

#![allow(unused)]
fn main() {
// Before
let paths = engine.graph_resolver.resolve_k_best(&lattice, k)?;

// After
let paths = engine.convert_k_best(yomi, None, k)?;
}

2. evaluate.rs

1-best 判定に engine.convert() を使っていたため、リランキングで順位が変わっても Good/Bad に反映されなかった。

#![allow(unused)]
fn main() {
// Before: convert で 1-best、不一致時に別途 convert_k_best
let result = engine.convert(yomi, Some(&force_ranges))?;

// After: convert_k_best 1回で両方判定
let k_results = engine.convert_k_best(yomi, Some(&force_ranges), k_best)?;
let got = k_results.first().map(|p| /* 先頭パスから surface を取得 */);
}

修正内容

  • PR: #433
  • check.rs: k-best ブランチで convert_k_best を使用するように変更
  • evaluate.rs: convertconvert_k_best の先頭パスで 1-best 判定するように変更

評価結果

条件

  • コーパス: anthy-corpus (corpus.0〜5.txt, 全 11,233 件)
  • 辞書: SKK-JISYO.L
  • モデル: akaza-default-model
  • k-best: 5 (デフォルト)

結果

unknown-bigram-weightGoodTop-5Bad再現率
1.0 (default)6,4274674,33992.51%
0.35,9319634,33991.46%
0.15,7801,1144,33991.23%

分析

  • 重みパラメータの変更が evaluate に正しく反映されていることを確認した
  • Bad は全パターン同じ (4,339) — Top-5 にも入らないものは重みを変えても変わらない
  • Good + Top-5 の合計も同じ (6,894) — リランキングで Top-5 内の順位が入れ替わっている
  • unknown-bigram-weight を下げると Good が減り Top-5 が増える
    • 未知 bigram のコストを軽くすると、未知語を含む分割が選ばれやすくなり精度が低下
  • デフォルト (1.0) が現時点では最良

corpus.2.txt のみでの結果 (参考)

unknown-bigram-weightGoodTop-5Bad再現率
1.0 (default)1346171.85%
0.31346170.76%
0.11256170.41%

length-weight 評価

粗探索 (0〜10.0)

length-weightGoodTop-5Bad再現率Good差分
0 (baseline)6,4274674,33992.51%-
0.16,4284664,33992.51%+1
0.56,4384564,33992.56%+11
1.06,4624324,33992.61%+35
2.06,4724224,33992.64%+45
3.06,4734214,33992.62%+46
5.06,4384564,33992.51%+11
10.06,3505444,33992.33%-77
  • length-weight を上げるほど Good が増加し、2.0〜3.0 でピーク
  • 5.0 以上では逆に悪化し、10.0 では baseline を下回る

詳細探索 (2.0〜3.0, 0.1刻み)

length-weightGoodTop-5Bad再現率Good差分
2.06,4724224,33992.64%+45
2.16,4734214,33992.63%+46
2.26,4734214,33992.64%+46
2.36,4744204,33992.63%+47
2.46,4704244,33992.62%+43
2.56,4724224,33992.62%+45
2.66,4714234,33992.62%+44
2.76,4724224,33992.63%+45
2.86,4734214,33992.63%+46
2.96,4684264,33992.61%+41
3.06,4734214,33992.62%+46
  • ピークは length-weight=2.3 (Good=6,474, +47)
  • ただし 2.0〜3.0 の範囲で Good は 6,468〜6,474 (差6件) とほぼ横ばい
  • この範囲ではパラメータに対して敏感ではなく、2.0〜3.0 のどこでもほぼ同等の改善

length-weight=2.0 の詳細分析 (baseline との差分)

baseline (length-weight=0) と length-weight=2.0 で変換結果が変わったケースを個別に分析した。

変化の概要

変化件数内容
改善: BAD/TOP-5 → Good109件TOP-5 内で 1 位に昇格
悪化: Good → TOP-567件1 位から脱落(TOP-5 には残っている)
悪化: Good → BAD0件完全に見つからなくなるケースはなし
BAD↔TOP-5 ステータス変化0件-
出力変化 (同ステータス)220件BAD/TOP-5 のまま出力だけ変わった

ネット改善: +109 - 67 = +42件

改善例 (TOP-5 → Good, 109件から抜粋)

正しい分節まとめが効いているケース:

よみcorpus (正解)baselinelength-weight=2.0
とうきょうととっきょきょかきょく東京都特許許可局東京特許許可局東京特許許可局
あるぷすのしょうじょはいじアルプスの少女ハイジアルプスの少女は維持アルプスの少女ハイジ
きょうとはぼんちです京都は盆地です今日とは盆地です京都は盆地です
あしばがためをしてから足場固めをしてから足場がためをしてから足場固めをしてから
せいこういのってます成功祈ってます性行為のってます成功祈ってます
いやがおうでも否が応でもいやがでも否が応でも
としむけにはつばいした都市向けに発売した向けに発売した都市向けに発売した
ふなばしてん船橋店船場支店船橋店
にかいきゅうとくしん二階級特進階級特進階級特進

悪化例 (Good → TOP-5, 67件から抜粋)

漢字→ひらがな/カタカナの退行や過剰な結合:

よみcorpus (正解)baselinelength-weight=2.0
だいじなことは大事なことは大事なことはだいじなことは
なぜかじゅうでんできずになぜか充電できずになぜか充電できずに何故か充電できずに
いくのをやめました。行くのをやめました。行くのをやめました。生野をやめました。
こうじえんちょうしんせいのしょるいをかいた工事延長申請の書類を書いた工事延長申請の書類を書いた広辞苑超新星の書類を書いた
ぜんはんせい全半生全半生前半生
ぜんじんみとうのち前人未到の地前人未到の地前人未到
かくめいをおこしましたしね革命を起こしましたしね革命を起こしましたしね革命を起こしました死ね

悪化パターンの傾向

悪化 67件の全てが Good → TOP-5 であり、Good → BAD は 0件。つまり:

  • length-weight=2.0 で正解候補が Top-5 から完全に消えることはない
  • 長い単位を優先しすぎて「生野」(いくの) のような過剰結合や、「広辞苑超新星」のような誤った長単語マッチが起きる
  • ひらがな表記が正解のケースで漢字変換を優先してしまう傾向がある (「だいじ」→「大事」は Good だが逆は悪化)
  • 「7日」(なのか) のような数詞を含む結合エラーも散見される

考察

unknown-bigram-weight

  • 単体での改善は難しい
    • 1.0 未満にすると悪化する
    • 1.0 を超えると元のモデルが学習したコストバランスを歪めることになる

length-weight

  • length-weight=2.0〜2.3 が最適値 (baseline比 Good+45〜47, 再現率 92.51%→92.63%)
  • トークン数の少ない(= 長い単位でまとめた)分割を優先することで精度向上
  • 2.0〜3.0 の範囲で安定しており、ロバストなパラメータと言える
  • 5.0 以上では過度にトークン数を減らそうとして逆効果
  • 悪化ケースは全て Good→TOP-5 であり、Good→BAD は発生しない(安全性が高い)
  • ただし「生野」「広辞苑超新星」のような過剰結合による悪化には注意が必要

今後の方向

  • length-weight のデフォルト値を 2.0 程度に設定することを検討
  • 複数の重みパラメータの組み合わせ探索
  • コーパスを使った重みの自動チューニング
  • 「なのか」→「7日」のような特定パターンの悪化への対処

数値+助数詞変換の再設計

背景

現在の Akaza は、<NUM> 正規化と数字+かな複合セグメントにより、数字付き語のカバレッジを拡張している。 一方で、助数詞変換については以下の課題が残る。

  • コーパスに存在しない数字パターン(例: 516しゅうかん)の安定性をさらに上げたい
  • 入力数字の表記ゆれ(半角/全角/かな数詞)を同一に扱いたい
  • 出力候補として半角数字・全角数字・漢数字を常に提示したい

本メモは、数値+助数詞を「コーパス依存の語彙問題」ではなく「規則ベースの構造問題」として扱う再設計案を示す。

目的

  • 0ひき, ぜろひき, ひゃっぴき, 3しゅうかん, 516しゅうかん のような入力を安定して変換できる
  • Wikipedia / 青空文庫 / CC-100 に未出の数字でも候補生成できる
  • 数字表記の候補として以下を自動追加する
    • 半角数字(516週間
    • 全角数字(516週間
    • 漢数字(五百十六週間

非目的

  • すべての日本語数詞(分数、序数、慣用表現)への一括対応
  • 助数詞の音便規則を 100% 網羅すること
  • 既存の Viterbi / reranking の全面置換

要件

機能要件

  1. 入力先頭の数値表現を抽出できること
  • 半角数字: 0, 516
  • 全角数字: , 516
  • かな数詞: ぜろ, れい, ひゃく, ひゃっ, せん など(段階導入)
  1. 数値の直後に助数詞読みが続く場合、助数詞辞書により surface を生成できること
  • 例: ひき / びき / ぴき
  • 例: しゅうかん週間
  1. 同一の内部値(例: 516)に対して、複数数字表記を候補生成できること
  • 516週間
  • 516週間
  • 五百十六週間
  1. LM lookup では <NUM>助数詞 キーでスコア共有できること
  • 表記差 (3週間, 3週間, 三週間) を同一スコア空間で扱う

品質要件

  • 裸数字正規化は継続して禁止(に→2 型退行の防止)
  • 助数詞を伴わない入力への影響を最小化
  • 既存変換の top-1 品質を大きく悪化させない

設計方針

数値+助数詞の処理を以下 3 レイヤに分離する。

  1. NumericParser(入力正規化)
  • 入力先頭から数値表現を抽出し、NumericValue に正規化する
  1. CounterLexicon(助数詞正規化)
  • 助数詞読みと漢字表記の対応を管理
  • 連濁・促音を同義として収束(例: ひき/びき/ぴき
  1. CounterCandidateGenerator(候補生成)
  • NumericValue + Counter から多表記候補を動的生成
  • LM は <NUM> 正規化キーで評価

この分離により、コーパスカバレッジ不足を候補生成規則で補い、LM は主に順位付けに使う。

データ構造(案)

#![allow(unused)]
fn main() {
pub struct NumericValue {
    pub value: i64,
    pub consumed_len: usize,  // 入力で消費した byte 長
    pub source: NumericSource, // ASCII / Fullwidth / Kana
}

pub enum NumericSource {
    AsciiDigits,
    FullwidthDigits,
    KanaNumeral,
}

pub struct CounterEntry {
    pub canonical_yomi: &'static str,   // 例: "ひき"
    pub yomi_aliases: &'static [&'static str], // 例: ["ひき", "びき", "ぴき"]
    pub surfaces: &'static [&'static str], // 例: ["匹"]
}
}

候補生成規則

入力 N + counter_yomi が成立した場合:

  1. Ni64 に正規化
  2. counter_yomiCounterLexiconCounterEntry に解決
  3. surface_counter に対し、以下 3 系統の数字表記を生成
  • 半角: N_ascii + surface_counter
  • 全角: N_fullwidth + surface_counter
  • 漢数字: int2kanji(N) + surface_counter

例:

  • 0ひき / ぜろひき0匹, 0匹, 零匹
  • ひゃっぴき(=100+ぴき)→ 100匹, 100匹, 百匹
  • 516しゅうかん516週間, 516週間, 五百十六週間

スコアリング方針

候補キーは既存ルールに沿って <NUM>助数詞/<NUM>助数詞読み に正規化して LM lookup する。

  • 516週間/516しゅうかん
  • 516週間/516しゅうかん
  • 五百十六週間/516しゅうかん

上記は同一キー "<NUM>週間/<NUM>しゅうかん" で評価できるようにする。 これにより、コーパスに 2週間 しかなくても 516週間 のスコアを共有できる。

ユーザー学習の汎化

ユーザーが変換結果を確定した際の学習データも、同じ <NUM> 正規化を適用する。 これにより、ある数字パターンでの学習が他の数字パターンにも汎化される。

  • ユーザーが 3週間 を確定 → <NUM>週間/<NUM>しゅうかん として記録
  • 以降、516週間, 2週間, 百週間 なども同じキーで lookup → スコアが上がる
  • unigram / bigram / skip-bigram の全ユーザー統計で同じ正規化を適用

連濁・促音の alias も正規化される:

  • 3びき を確定 → <NUM>匹/<NUM>ひき として記録(びき → canonical ひき
  • 以降、5ひき, 100ぴき なども同じキーでスコア共有

実装ポイント

Segmenter

  • 数値検出を ASCII 専用正規表現から拡張し、全角数字とかな数詞を抽出可能にする
  • 数値+助数詞の複合セグメントを優先的に候補化する

GraphBuilder

  • 既存の数字+かな複合候補ロジックを CounterCandidateGenerator に置換
  • 数字表記 3 系統を同時に追加
  • <NUM> フォールバックは継続し、裸数字除外も継続

辞書/設定

  • 初期はコード内静的テーブルで開始し、将来的に default-model 側の辞書ファイルへ外出し可能な形にする

段階導入計画

  1. Phase 1: インフラ追加
  • NumericParser(ASCII/全角)
  • CounterLexicon(最小セット: 匹, 人, 週間 など)
  • CounterCandidateGenerator(3表記生成)
  1. Phase 2: かな数詞対応
  • ぜろ, れい, ひゃく, ひゃっ, せん などを実装
  • ひゃっぴき のような促音ケースを通す
  1. Phase 3: 評価と拡張
  • 助数詞テーブルの拡充
  • 退行監視を通して alias を追加

テスト計画

Unit Tests

  • NumericParser:
    • 0, , 516, 516, ぜろ, ひゃっ
  • CounterLexicon:
    • ひき/びき/ぴき -> 匹
    • しゅうかん -> 週間
  • CandidateGenerator:
    • 3しゅうかん3週間, 3週間, 三週間
    • 516しゅうかん516週間, 516週間, 五百十六週間

Integration Tests

  • 0ひき, ぜろひき, ひゃっぴき, 3しゅうかん, 516しゅうかん の変換結果確認
  • 既存退行ケース(助詞「に」「さん」「ご」)が悪化しないこと

Evaluate

  • default-model の評価結果で Good/Bad 差分を確認
  • 数字表記差分は必要に応じて accept.tsv で管理

リスクと対策

  1. かな数詞解析の誤爆
  • 対策: 数値として解釈する条件を「助数詞が後続する場合」に限定する
  • 追加対策(実装済み):
    • 1文字かな数詞(, , , )は曖昧性が高いため除外(にほん→2本 防止)
    • 1文字助数詞(, , , )はかな数詞との組み合わせでは除外(いちじ→1時 防止)
    • かな数詞パスでは助数詞の完全一致のみマッチ(じょう にマッチする誤爆を防止)
    • かな数詞検出時も trie 探索を並行実行し、Viterbi に両方の解釈を提供
  1. 助数詞同音語(週間/週刊 など)の誤選択
  • 対策: CounterLexicon で優先候補を制御し、LM は補助に使う
  1. 候補数増加による速度低下
  • 対策: 数字表記は原則 3 種固定、助数詞未解決時は既存経路にフォールバック

受け入れ条件

  • 指定例(0ひき, ぜろひき, ひゃっぴき, 3しゅうかん, 516しゅうかん)で期待候補が出る
  • 裸数字正規化禁止ルールを破らない
  • 既存評価で重大退行(助詞の数字化)が発生しない

構造化パーセプトロン

https://tech.preferred.jp/ja/blog/nlp2011-inputmethod-structuredsvm/

統計的かな漢字変換ではなく、識別モデルを利用するパターンも試してみたい。

https://www.anlp.jp/proceedings/annual_meeting/2011/pdf_dir/C4-3.pdf

Mozc で統計的かな漢字変換を採用しているのは以下のようなところもあるのかなと。 Google の収集した Web のデータを使えるというのがやはり Mozc の強みの一つなので、それを活かせる環境ならば、統計的かな漢字変換がとても有利なのだと思う。

識別学習のモデル構築時間を生成モデル並にするには, 学習用の コーパスを小さくするしかなく, これでは Web の 統計を反映しにくくなる.

一方、Akaza では潤沢な計算資源も使えず、ウェブコーパスもできる限り避けたいな、と思っているので、そうなってくると使えるコーパスはごく少量になってくる。 となると、案外、識別モデルのほうがいいんじゃないかなぁ、と。

統計的かな漢字変換で、スコアをチューニングしていくということになると、パラメータを職人が調整する、みたいな感じになってくるので、それは OSS の世界だと成立が 難しいと思う。 こっちのパラメータいじったらこっちの変換結果がぶっ壊れた、みたいなことになりがちだと思うので。

識別モデルの場合、限られた少数のデータをもとに、そのコーパスにフィットした形にチューニングするということができる。はず。

正直、Wikipedia のコーパスだけだと、代表性が微妙だなぁと思うので Wikipedia をベースに学習させて、その上で誤変換される例をもとに識別モデルでチューニングしていく、という方針がよいのではないかと。

識別モデルの実装方針

日本語入力本でも、まずは構造化パーセプトロンを実装し、その上で構造化SVMなど他のメソッドを利用してチューニングしていくのが良い、と書いてあるので、 それにそってすすめていきたい。

構造化パーセプトロンでは、ビタビアルゴリズムで文を生成して、それが教師データと一致していればなにもしない。 一致していない場合には、正解するノードのコストを1上げる。不正解ノードを1下げるという処理を行う。

という非常にシンプルな方法で実現できるので、頑張って実装してみてもいいのかなぁ、と。

正直、自分自身が品詞やらなんやらの知識がないのもあるし、品詞がどうこうとかいうと論争のもとっぽさを感じるので、、 そういう意味でも、教師データを用意すりゃ精度が上がりますよ~。誤変換が気になるようなら教師データ足してくださいねー。 というのはちょうどよい温度感なのかなーと思う。

また、個人的には基本のデータセットは、公開されたデータセットをベースにするのが良いと思っているんだけど。

学習の進みが遅いケース

基本的に、コストがノード単位に振られているから、長い文節がマッチし易い傾向にある。 これはこれで良いことなのだが、

洗濯物/せんたくもの を/を 干す/ほす の/の が/が 面倒/めんどう だ/だ

のようなケースの場合、“逃が/のが” が辞書に登録されていると、 “の/の が/が” の2文節を通るよりもコストがやすくなりがち。 なので、めちゃくちゃコストが下がっていくのをまたないといけない感じになりがち。

このへんは、未知語のコストを統計的かな漢字変換のときに 20 とか雑にデカくつけてるのがよくない。 しかもここがハードコードされている。こういうのを調整可能にしないといけない。

こういう、統計的かな漢字変換前提でハードコードされている部分とかをばらしていかないといけない。 クラス構造とかデータの持ち方を調整しないと、ごちゃごちゃしすぎているので、調整が必要。

例えば、「よくない」の変換結果として「翼内」が一位にくるけど「良くない」がトップに出てくるべきかもしれない。 そういった調整は、識別モデルのほうがしやすい、のかな?

ユーザーの入力結果の学習についても、統一的に扱えるような気がする。

learn-corpus 改善実験

背景

learn-corpus コマンドの学習ロジックを調査した結果、以下の問題が判明した。

問題点

  1. コメントとコードの不一致: コメントは「頻度を増やす」だが、コードは cost - delta(減算)
  2. u32 アンダーフロー依存: cost が 0 のとき 0 - delta は release ビルドでラップアラウンドし、非常に大きな値になる。これが「たまたま」スコアを下げる方向に作用している
  3. debug ビルドでパニック: debug ビルドでは u32 のオーバーフローチェックが有効なため、パニックする
  4. 予測パスのカウント放置: 不正解時に正解パスのカウントのみ操作し、予測(間違った)パスのカウントは変更しない

現在のコード(問題箇所)

#![allow(unused)]
fn main() {
// 正解じゃないときには出現頻度の確率が正しくないということだと思いますんで
// 頻度を増やす。
if result != surface {
    // learn unigram
    for i in 0..teacher.nodes.len() {
        let key = teacher.nodes[i].key();
        let (_, cost) = self.system_unigram_lm
            .find_cnt(&key.to_string())
            .unwrap_or((-1, 0_u32));
        self.system_unigram_lm.update(key.as_str(), cost - delta);  // ← 減算
    }
    // bigram, skip-bigram, BOS/EOS も同様に `v - delta`
}
}

実験計画

実験1: 減算 → 加算 (saturating_add)

5箇所の cost - delta / v - deltacost.saturating_add(delta) / v.saturating_add(delta) に変更。 コメント「頻度を増やす」の意図どおりの動作にする。

実験2: 加算 + 予測パス減算

実験1に加えて、予測パス(不正解)のカウントを saturating_sub(delta) で減算。 正解パスと予測パスで共通する単語はスキップ。

評価指標

指標説明
GoodTop-1 exact match 数
Top-5Top-5 に正解あり(Top-1 にはない)の数
BadTop-5 にも正解なし
再現率LCS ベースの文字レベル再現率
学習時間learn-corpus の wall clock 時間

実験条件

  • コーパス: anthy-corpus (corpus.0〜3, 5.txt, 全 11,065 件)
  • 辞書: SKK-JISYO.L + SKK-JISYO.akaza
  • モデル: akaza-default-model
  • learn-corpus パラメータ: delta=2000, may-epochs=10, should-epochs=100, must-epochs=10000
  • k-best: 5 (デフォルト)

実験結果

結果一覧

手法GoodTop-5Bad再現率学習時間
ベースライン(現行モデル)6719416393093.267%16:48
実験1: 減算→加算6719416393093.267%18:04
実験2: 加算+予測パス減算6719416393093.267%17:25

ベースライン(現行モデル)

学習済みモデル(data/)でそのまま evaluate を実行。

Good=6719, Top-5=416, Bad=3930, 再現率=93.26721
学習時間: 16:48 (wall clock)

実験1: 減算 → 加算

cost - deltacost.saturating_add(delta) に全5箇所を変更。

Good=6719, Top-5=416, Bad=3930, 再現率=93.26721
学習時間: 18:04 (wall clock)

ベースラインと完全に同一のスコア。

実験2: 加算 + 予測パス減算

実験1に加えて、予測パスの unigram/bigram/skip-bigram/BOS-EOS を saturating_sub(delta) で減算。 正解パスと共通する単語はスキップ。

Good=6719, Top-5=416, Bad=3930, 再現率=93.26721
学習時間: 17:25 (wall clock)

ベースラインと完全に同一のスコア。

考察

全手法でスコアが同一だった理由

3手法とも evaluate スコアが完全に一致した。以下の原因が考えられる。

  1. 学習コーパスと評価コーパスの重複が少ない: 学習コーパス(training-corpus/)は手作業で作成した少量のデータであり、評価コーパス(anthy-corpus)の 11,065 件とはドメインが異なる。学習による重み変更が、評価コーパスの変換に影響する単語・bigram にヒットしていない可能性が高い。

  2. delta が小さすぎる: delta=2000 は統計的な頻度カウント(数十万〜数百万オーダー)に対して微小であり、LM コストの変化が変換結果を変えるほどの影響を持たない。

  3. learn-corpus の学習対象が限定的: 学習は不正解時のみ発火するが、must コーパスの全10件中大半は数エポックで正解に到達するため、実際の重み更新回数が少ない。

学習時間の差

  • 実験1は baseline より約1分16秒遅い(+7.5%)
  • 実験2は baseline より約37秒遅い(+3.7%)
  • いずれもオーバーヘッドは軽微

結論

今回の実験では、learn-corpus の学習ロジック変更が evaluate スコアに影響を与えないことが判明した。

コードの正しさ(コメントとの整合性、u32 アンダーフロー排除、debug ビルド対応)の観点からは saturating_add への変更が望ましいが、スコア改善は確認できなかった。

今後の方向性:

  • 評価コーパスに含まれる単語を学習コーパスに追加し、学習効果を測定する
  • delta の値を大きくして影響を調べる
  • 学習前後のモデルの差分を直接比較し、重み変更が実際に発生しているか確認する

追記: 評価方法の問題

この実験のベースラインは data/(learn-corpus 適用済みモデル)であり、「学習済み vs 再学習済み」の比較だった。後の構造化パーセプトロン評価で未学習モデル(learn-corpus を実行しないモデル)と比較した結果、旧 learn-corpus(delta=2000)は +11 文(+0.028%)のわずかな改善効果があることが判明した。全手法でスコアが同一だったのは、再学習しても同様のモデルが生成されていたためである。

構造化パーセプトロン実装の評価レポート

背景

learn-corpus 改善実験で、カウントベース更新(加算/減算)は evaluate スコアに影響を与えないと報告された。しかし、この実験には評価方法の問題があった(後述)。

コスト関数 cost = -log10((count + α) / (total + α + V)) の非線形性により、カウント操作では高頻度語のコスト変化が微小で変換結果を変えにくい。この問題を解決するため、構造化パーセプトロンの方針に基づき、カウントではなくコストに直接加算するスコアベースの構造化パーセプトロンを実装した。

前回実験の評価方法の問題

前回の実験では「ベースライン」として data/(learn-corpus 適用済みモデル)を使用し、これを learn-corpus 再実行後のモデルと比較していた。つまり学習済み vs 再学習済みの比較であり、未学習 vs 学習済みの比較ではなかった。旧 learn-corpus のカウント操作がわずかしか効果がない場合、再学習してもほぼ同じモデルが生成されるため、差が出ないのは当然だった。

実装概要

変更点

  1. OnMemory LM に adjustment: HashMap<K, f32> を追加: find() / get_edge_cost() が返す値に adjustment を加算
  2. 学習ロジックの書き換え: 不正解時に以下の更新を実行
    • 教師パスの全特徴(unigram/bigram/skip-bigram): コスト -= step_size(安くする)
    • 予測パスの全特徴: コスト += step_size(高くする)
  3. CLI 引数: --delta (u32) → --step-size (f32, default 0.5)

パラメータ

step_size = 0.5
may_epochs = 10, should_epochs = 100, must_epochs = 10000

実験結果

3条件の比較

今回は「未学習モデル」(learn-corpus を epochs=1 で実行し、学習ループを実行しないモデル)を加えて、正しくベースラインを取った。

手法GoodTop-5Bad再現率
未学習(生の統計のみ)6708426393193.239%
旧 learn-corpus(delta=2000)6719416393093.267%
構造化パーセプトロン (step=0.5)6614492395992.768%

旧 learn-corpus の効果(再評価)

比較Good差再現率差
旧 learn-corpus vs 未学習+11+0.028%

前回の実験では「旧 learn-corpus は効果なし」と結論づけたが、正しくはわずかに効果がある(+11 文, +0.028%)。前回は「学習済み vs 再学習済み」を比較していたため差が出なかった。

構造化パーセプトロンの効果

比較Good差再現率差
パーセプトロン vs 未学習-94-0.471%
パーセプトロン vs 旧 learn-corpus-105-0.499%

構造化パーセプトロンは未学習モデルよりも悪い。学習が悪影響を与えている。

差分分析(パーセプトロン vs 旧 learn-corpus)

概要

カテゴリ件数
新版でBADになった(以前は正解)431
新版で修正された(以前はBAD)405
両方BADだが変換が変わった~820
純損失26

新規リグレッション上位パターン

パターン件数説明
「き」→「季」51件文節区切り失敗で語末「き」が独立文節「季」に
「を」→「ヲ」33件助詞がカタカナ化
「きのう」→「機能」19件「昨日」が「機能」に誤変換
「きゅう」: 急→旧11件同音異義語の優先度逆転
「ご」→「御」9件ひらがなであるべき接頭辞の漢字化
「にほん」→「に本」7件文節区切りの破壊
「かく」: 書く→各7件同音異義語の優先度逆転

修正されたパターン(改善点)

パターン件数説明
「き」: 木→気35件旧版の「気→木」誤変換が修正
「さい」: 賽→再14件旧版の「再→賽」誤変換が修正
「あき」: 空き→秋7件旧版の「秋→空き」誤変換が修正
「えん」: 縁→円6件旧版の「円→縁」誤変換が修正

考察

1. コストへの直接加算は変換結果を変える力がある

前回のカウントベース実験(正しく評価すると +11 文の微小な効果)と異なり、構造化パーセプトロンは大量の変換結果を変化させた(1000件超)。コストに直接加算するアプローチは確実にモデルに影響を与える。

2. しかし方向が間違っている

学習の力はあるが、全体としては悪化。改善(405文)より劣化(431文)が上回っている。

3. 劣化の原因分析

a) 学習コーパスの規模と評価コーパスのドメイン差

学習コーパス(training-corpus/)は手作業で作成した少量のデータ。この限られたデータで学んだ重みが、11,065 件の評価コーパスに対して汎化できていない。「季」「ヲ」「機能」などのリグレッションは、学習コーパスの偏りを反映している。

b) step_size = 0.5 が大きすぎる

step_size 0.5 は、典型的なコスト値(2〜8 程度)に対してかなり大きい。数十〜数百エポック繰り返すと adjustment が数十に達し、元のコスト構造を大きく歪める。

c) 正則化の欠如

現在の実装には正則化がない。過学習により、学習コーパスにフィットする代わりに汎化性能が低下している。

d) 教師パスのみの更新

教師パスと予測パスで共通する特徴が相殺されるため、差分のみを更新すべき。また、正解時にも重み更新しないため、正の事例からの学習が不足している可能性がある。

4. 今後の改善方向

  1. step_size の縮小: 0.05〜0.1 など、より保守的な値を試す
  2. averaged perceptron: 全エポックの重みの平均を取ることで過学習を抑制
  3. 学習コーパスの拡充: 評価コーパスとドメインが重なるデータの追加
  4. early stopping: 検証セットで精度が低下し始めたらエポックを打ち切る
  5. 特徴の重複除外: 教師パスと予測パスで共通する特徴をスキップ
  6. 学習率スケジューリング: エポックが進むにつれて step_size を減衰させる

結論

構造化パーセプトロンによるスコアベース学習は、カウントベース学習と異なりモデルの変換結果を大量に変える力があることが確認された。しかし、現在のパラメータ設定(step_size=0.5)と限られた学習コーパスでは未学習モデルより悪化する結果となった。

旧 learn-corpus(カウントベース)も正しく評価すると +11 文(+0.028%)のわずかな改善効果があることが判明した。

構造化パーセプトロンの実装基盤は整ったが、正則化(averaged perceptron)、学習率の調整、学習コーパスの拡充が精度改善のために必要である。現時点ではこの変更をデフォルトモデルのビルドに適用すべきではない。

kytea で漢方薬の変換結果が 漢方/かんぽう 薬/く となる

薬/く が wikipedia の変換結果に含まれている。 これは、kytea がそういう出力をしているのだろうということが予想がつく。

https://kanji.jitenon.jp/kanji/418.html

漢和辞典でも、薬の読み方に「く」は登録されていない。

https://ja.wikipedia.org/wiki/%E6%95%91%E5%BF%83%E8%A3%BD%E8%96%AC

によれば、

生薬・漢方薬を中心にしたOTC医薬品を数多く手がけている。

という文章が kytea では

生薬・漢方薬を中心にしたOTC医薬品を数多く手がけている。
生薬/名詞/しょうやく ・/補助記号/・ 漢方/名詞/かんぽう 薬/接尾辞/く を/助詞/を 中心/名詞/ちゅうしん に/助詞/に し/動詞/し た/助動詞/た OTC/名詞/おーてぃーしー 医薬/名詞/いやく 品/接尾辞/ひん を/助詞/を 数/名詞/かず 多く/名詞/おおく 手がけ/動詞/てがけ て/助詞/て い/動 詞/い る/語尾/る 。/補助記号/。

と分かち書きされていることが原因だ。

本来ならこれは「漢方薬/かんぽうやく」になってほしい。

既存の kytea のモデルを元に一部だけ変更を加えるというのは簡単なのだが、計算機資源をわりと必要とする処理となる。

kytea の素性ファイルが http://www.phontron.com/kytea/train-ja.html からダウンロードできるのでこれを活用する。

これを元に

time train-kytea -feat work/kytea/kytea-0.4.2.feat -full corpus/kytea.txt -model work/kytea/train.mod

などとして、追加のコーパスを食わせて学習させる。

だいたい5分ぐらいで学習が終わるようだ。

このモデルを利用すると以下のように、適切に分割されるようになる。

$ echo 生薬・漢方薬を中心にしたOTC医薬品を数 多く手がけている。|kytea -model work/kytea/train.mod
生薬/名詞/しょうやく ・/補助記号/・ 漢方薬/名詞/かんぽうやく を/助詞/を 中心/名詞/ちゅうしん に/助詞/に し/動詞/し た/助動詞/た OTC/名詞/おーてぃーしー 医薬/名詞/いやく 品/接尾辞/ひん を/助詞/を 数/名詞/かず 多く/名詞/おおく 手がけ/動詞/てがけ て/助詞/て い/動/い る/語尾/る 。/補助記号/。

kytea は単漢字の読み間違いがそこそこあるような気がする。 単漢字の間違った登録は、直接的に変換速度の低下につながるので、できるだけ防いだ方が良い。

このようなケースについては、corpus/kytea.txt に追加していけば学習できるようにしておいた。

ところで、ひらがな一文字の文字の単漢字の誤登録は SKK-JISYO.L などと突合させれば、わりと見つけることができそうである。 つまり、かな漢字辞書に対して一文字のものを列挙し、SKK-JISYO.L に無い物を報告するスクリプトを作れば良い。

よって、tests/kanpoyaku.rs を追加した。

以下の様なケースで、へんな漢字が登録されていることがわかった。

moji="か", system_dict_len=["乎", "借", "刈", "勝", "咬", "噛", "掻", "書", "欠", "歟", "穫", "缺", "苅", "買", "貸", "購", "飼", "駆", "駈"]
moji="し", system_dict_len=["如", "敷", "沁", "識"]
moji="お", system_dict_len=["峰", "折", "押", "捺", "推", "置", "負", "追", "逐"]
moji="あ", system_dict_len=["会", "合", "在", "惡", "有", "空", "編", "逢", "遇", "遭", "開"]
moji="い", system_dict_len=["云", "入", "可", "往", "快", "煎", "生", "藺", "行", "要", "言", "逝"]
moji="こ", system_dict_len=["好喜", "琥珀", "乞", "凝", "混", "漉", "漕", "超", "越"]
moji="た", system_dict_len=["建", "断", "炊", "立", "経", "絶", "誰", "起", "足"]
moji="さ", system_dict_len=["刺", "割", "去", "咲", "射", "指", "挿", "裂"]
moji="き", system_dict_len=["切", "利", "効", "截", "斫", "斬", "炫", "聴", "葱", "訊"]
moji="と", system_dict_len=["取", "問", "執", "島", "捕", "採", "摂", "撮", "溶", "獲", "翔", "解", "説", "跳", "録", "飛"]
moji="な", system_dict_len=["亡", "勿", "啼", "成", "无", "汝", "泣", "莫", "薙", "鳴"]
moji="み", system_dict_len=["満", "観", "診", "躬"]
moji="は", system_dict_len=["河", "匐", "吐", "履", "掃", "穿", "貼", "蹈", "這"]
moji="ふ", system_dict_len=["伏", "吹", "噴", "振", "臥", "践", "踏", "降"]
moji="ま", system_dict_len=["俟", "増", "室", "巻", "待", "撒", "舞", "蒔"]
moji="ひ", system_dict_len=["引", "弾", "彎", "惹", "挽", "曳", "轢", "魅"]
moji="う", system_dict_len=["栩", "倦", "売", "打", "撃", "泛", "浮", "産", "討", "賣"]
moji="く", system_dict_len=["具", "喰", "拱", "汲", "組", "繰", "酌", "食"]
moji="ち", system_dict_len=["散"]
moji="す", system_dict_len=["住", "刷", "吸", "好", "擂", "擦", "済", "澄", "濟", "馬尾"]
moji="ほ", system_dict_len=["干", "彫", "掘"]
moji="つ", system_dict_len=["就", "摘", "撞", "次", "着", "突", "継", "釣"]
moji="て", system_dict_len=["照"]
moji="だ", system_dict_len=["勃", "抱"]
moji="そ", system_dict_len=["剃", "沿", "添"]
moji="け", system_dict_len=["笥", "蹶"]
moji="の", system_dict_len=["−", "乗", "乘", "呑", "延", "載", "飲"]
moji="ど", system_dict_len=["砥", "秉"]
moji="ご", system_dict_len=["棊", "込"]
moji="が", system_dict_len=["貸"]
moji="ぜ", system_dict_len=["瀬"]
moji="ね", system_dict_len=["練", "錬"]
moji="へ", system_dict_len=["減"]
moji="ぷ", system_dict_len=["譜"]
moji="ぬ", system_dict_len=["塗", "抜", "拔", "縫", "脱"]
moji="ぽ", system_dict_len=["浦"]

kytea にいくらかの用意したコーパスを学習させることによって、変換精度を高めることができると思うけれど、kytea 用のデータを用意すること自体がちょっと手間なのと、 kytea の学習に5分ぐらい時間がかかるのと、それを使って学習しなおすのに時間が結構かかるという問題があります。

  • work/annotated/AD/wiki_36

mecab (Vibrato)の出力に対する前処理

MeCab の文節処理は、かな漢字変換に使うと考えると細かすぎる。

例えば以下のようになる。

養子となっている
養子    名詞,一般,*,*,*,*,養子,ヨウシ,ヨーシ
と      助詞,格助詞,一般,*,*,*,と,ト,ト
なっ    動詞,自立,*,*,五段・ラ行,連用タ接続,なる,ナッ,ナッ
て      助詞,接続助詞,*,*,*,*,て,テ,テ
いる    動詞,非自立,*,*,一段,基本形,いる,イル,イル
EOS

これは形態素解析機としては妥当なのだけれど、かな漢字変換器として考えた場合には少しこまかくちぎりすぎである。 よって、これを少し、調整していく。

具体的には、「養子/ようし と/と なって/なって いる/いる」ぐらいになっていると自然かなぁ。

利用可能な日本語コーパス調査 (2026-02)

かな漢字変換エンジン (akaza) の n-gram 統計データ生成に利用できるコーパスの調査結果。

現在使用中のコーパス

コーパス規模ライセンス
jawiki (CirrusSearch)~1.49M 記事CC BY-SA
青空文庫~17,800 作品 (~47MB)パブリックドメイン

追加候補: 大規模ウェブコーパス

Web クロールから構築された大規模日本語コーパス。 規模が桁違いに大きく、追加する場合はこれらが最も効果的。

CC-100 Japanese

項目
規模~258 億文字
ライセンスCommon Crawl 利用規約に準拠。研究・商用利用可
URLhttps://data.statmt.org/cc-100/
ダウンロードja.txt.xz を直接ダウンロード
形式プレーンテキスト (xz 圧縮)。文書間は空行区切り

評価: 最も導入しやすい。 単一ファイルをダウンロードするだけで使える。 ウェブテキストのためノイズ (定型文、非自然文) を含むが、規模の大きさで補える。

OSCAR Japanese

項目
規模~740 億文字 (OSCAR 23.10)
ライセンスCommon Crawl 利用規約に準拠。研究・商用利用可
URLhttps://huggingface.co/datasets/oscar-corpus/OSCAR-2301
形式JSONL + Zstandard 圧縮

CC-100 より大きく、KenLM ベースの品質フィルタリング済み。 HuggingFace からダウンロード可能。

mC4 Japanese

項目
規模~2,397 億文字
ライセンスODC-BY (商用利用可、要帰属表示)
URLhttps://huggingface.co/datasets/allenai/c4
形式JSONL (HuggingFace datasets)

CC-100/OSCAR の中で最大規模。CLD3 による言語検出フィルタリング済み。 ストレージと処理時間が必要だが、最も大量のデータを得られる。

CulturaX Japanese

項目
規模全言語で 6.3 兆トークン (16TB parquet)。日本語部分は不明だが相当量
ライセンスmC4 (ODC-BY) + OSCAR の規約に準拠
URLhttps://huggingface.co/datasets/uonlp/CulturaX
形式Parquet

mC4 と OSCAR を統合・重複除去・クリーニングしたデータセット。 両方を個別に使うより、これ一つで済む可能性がある。

Swallow Corpus

項目
規模v1: ~3,121 億文字 (21 スナップショット)、v2: さらに大規模 (94 スナップショット)
ライセンスCommon Crawl 利用規約に準拠。商用利用可
URLhttps://github.com/swallow-llm/swallow-corpus
形式Common Crawl から自分でビルドする必要あり

東工大が構築した最大規模の日本語ウェブコーパス。 品質フィルタリングが最も徹底されているが、ビルドに大量の計算資源が必要。

追加候補: 公的データ

国会会議録 (国会議事録)

項目
規模帝国議会から現在まで数十年分。大量
ライセンス政府公開データ。利用制限なし
URLhttps://kokkai.ndl.go.jp/ (検索)、https://kokkai.ndl.go.jp/api.html (API)
形式XML/JSON (REST API 経由)
取得方法API でバッチ取得。一括ダウンロードファイルはない

国会での発言の書き起こし。自然な話し言葉パターンを含む点が他のコーパスにない特徴。 ただし政治・行政の語彙に偏る。API からのスクレイピングが必要。

e-Gov 法令データ

項目
規模全法令 XML (~253MB 圧縮)
ライセンス政府データ。二次利用可
URLhttps://laws.e-gov.go.jp/bulkdownload/
形式XML

法令文のみのため語彙が極めて特殊。かな漢字変換の汎用コーパスとしては不向き。

裁判所判例データ

項目
規模65,855 件 (1947〜2024 年)
ライセンスCC0 (japanese-law-analysis/data_set)
URLhttps://github.com/japanese-law-analysis/data_set
形式JSON
別ソースNII 判例 HTML データ (要申請、学術研究目的のみ)

CC0 ライセンスで公開されているデータセットがある。 法律用語に偏るが、整った書き言葉の日本語。法律語彙のカバレッジ向上には有用。

利用不可 / 非推奨のコーパス

BCCWJ (現代日本語書き言葉均衡コーパス)

項目
規模1 億 430 万語
ライセンスオンライン検索は無料 (少納言/中納言)。バルクダウンロードは有償
URLhttps://clrd.ninjal.ac.jp/bccwj/

均衡コーパスとして理想的だが、バルクダウンロード不可 (有償)。 書籍・雑誌・新聞・ブログなど多ジャンルをカバー。 令和 6〜10 年度に 2 億語規模への拡張が予定されている (BCCWJ2)。

NWJC (国語研日本語ウェブコーパス)

項目
規模100 億語以上
ライセンスオンライン検索 (梵天) は無料。バルクダウンロードは言語資源協会 (GSK) 経由で有償
URLhttps://masayu-a.github.io/NWJC/

形態素解析・係り受け解析済みの大規模ウェブコーパスだが、バルクダウンロード不可

livedoor ニュースコーパス

項目
規模7,367 記事 (9 カテゴリ)
ライセンスCC BY-ND 2.1 JP (NoDerivatives)
URLhttps://www.rondhuit.com/download.html

ND (改変禁止) ライセンスが問題。n-gram 統計の生成・配布が「派生物」に該当する可能性。 規模も小さく (7,000 記事)、データも 2012 年と古い。

JParaCrawl

項目
規模2,100 万対訳文ペア
ライセンス研究利用のみ。商用利用不可 (NTT)
URLhttps://www.kecl.ntt.co.jp/icl/lirg/jparacrawl/

対訳コーパスであり単言語コーパスではない。翻訳調テキストが多い。研究専用ライセンス。

NHK ニュース

一括ダウンロード可能なコーパスは存在しない。NHK のコンテンツは著作権で保護されており、 スクレイピングでの収集は利用規約上問題がある。

京都大学テキストコーパス

毎日新聞 1995 年版の CD-ROM が別途必要。新聞本文は含まれず注釈データのみ配布。

推奨度まとめ

コーパス規模ライセンス導入容易性推奨度
CC-100 (ja)258 億字自由簡単 (単一ファイル)
mC4 (ja)2,397 億字ODC-BY中 (HuggingFace)
OSCAR (ja)740 億字自由中 (HuggingFace)
CulturaX (ja)大規模ODC-BY + OSCAR中 (HuggingFace)
Swallow Corpus3,121 億字+自由難 (自前ビルド)
国会会議録大量パブリックドメイン中 (API)
裁判所判例65,855 件CC0簡単 (GitHub)中 (法律特化)
BCCWJ1 億語有償不可利用不可
NWJC100 億語有償不可利用不可
livedoor7,367 記事CC BY-ND簡単ND で非推奨

結論: 現状の jawiki + 青空文庫に追加するなら、CC-100 Japanese が最も導入しやすい。 単一ファイル (ja.txt.xz) をダウンロードするだけで 258 億文字のウェブ日本語テキストが得られる。 より大規模なデータが必要なら mC4 や CulturaX も選択肢になる。

ただし、ウェブコーパスはノイズ (広告文、定型文、機械翻訳テキスト等) を含むため、 品質フィルタリングの仕組みを検討する必要がある。

CC-100 Japanese クリーニング戦略

データ品質の調査結果

CC-100 Japanese (ja.txt.xz) は CCNet パイプラインで Common Crawl から抽出された日本語テキスト。

指標
トーカナイズ済みサイズ169GB (jawiki の 7.7 倍)
文書数6,560 万 (jawiki の 44 倍)
200 文字未満の文書55%
wfreq の純 ASCII エントリ25.9% (com, https, www 等)
wfreq の純数字エントリ7%
LaTeX 残骸displaystyle 17 万回等
重複文書~0% (CCNet で除去済み)
非日本語文書~0.02%
ひらがな比率の中央値~50%

フィルタリングルール

scripts/extract-cc100.py で文書単位のフィルタを適用する。

1. 最小文書長フィルタ (200 文字未満を除外)

  • 影響: 全文書の約 55% (~3,600 万文書) を除去
  • 根拠: 短い断片はナビゲーションテキスト、定型文、URL 断片等が多く、n-gram 統計にノイズを加えるだけ

2. ひらがな比率フィルタ (10% 未満を除外)

  • 影響: ~0.8% の文書を追加で除去
  • 根拠: 自然な日本語文にはひらがなが一定割合含まれる。ひらがなが極端に少ない文書は、コードブロック、数式、外国語テキスト等の可能性が高い

3. 行の繰り返しフィルタ (30% 以上重複行で除外)

  • 影響: 少量の文書を追加で除去
  • 根拠: コピペスパム、商品リストの繰り返し等を排除

--no-filter オプション

すべてのフィルタを無効化し、生テキストをそのまま出力する。デバッグ・比較検証用。

配布物の分離

フィルタリング後も CC-100 は jawiki に比べて大幅にサイズが大きく、品質も異なるため、2 つのバリアントで配布する。

バリアントコーパスターゲット
dist/ (デフォルト)jawiki + 青空文庫make dist
dist-full/jawiki + 青空文庫 + CC-100make dist-full

dist-full/ 内のファイル名は dist/ と同一 (接尾辞なし) のため、利用側での差し替えが容易。

make release は両方の tarball (akaza-corpus-stats.tar.gz, akaza-corpus-stats-full.tar.gz) を GitHub Release にアップロードする。

CC100_LIMIT

CC100_LIMITextract-cc100.py--limit に渡される値で、抽出する文書数の上限。

  • デフォルト: 5000000 (500 万文書)
  • フィルタ通過率 ~45% を考慮すると、出力は ~225 万文書 (jawiki の ~150 万と同程度)
  • make CC100_LIMIT=0 で無制限 (全 6,560 万文書を処理)

不採用とした手法

手法理由
重複除去CC-100 は CCNet パイプラインで既に重複除去済み (実測 ~0%)
FastText 言語判定非日本語は 0.02% と極めて少なく、外部依存に見合わない
Perplexity フィルタ効果は期待できるが言語モデルの準備が必要で、現段階では見送り

今後の改善候補

  • Perplexity フィルタ: KenLM 等の軽量 LM で文の perplexity を計算し、異常値を除外
  • NG ワードリスト: 不適切コンテンツの除外
  • URL/メールアドレス除去: 行レベルでの前処理
  • vocab threshold の調整: CC-100 込みの場合、--threshold の引き上げを検討

CirrusSearch 日本語コーパス調査 (2026-02)

結論

現状の jawiki + 青空文庫 の組み合わせが最も効率的であり、他の CirrusSearch プロジェクトを追加する必要はない。

調査した 3 プロジェクトの評価:

プロジェクトテキスト量テキストの質結論
jawikisource64.8M words法令文書・漢文・棋譜等が大半。有用部分は数百ページ追加不要
jawikinews1.75M words質は良いが規模が小さすぎる追加不要
jawikibooks17.5M words大半が法律コンメンタール。有用部分は数百ページ追加不要

参考: jawiki は数十億ワード規模、青空文庫は ~17,800 作品 (~47MB)。


CirrusSearch ダンプについて

Wikimedia の CirrusSearch ダンプには、日本語 Wikipedia 以外にも複数の日本語プロジェクトが含まれている。 ダンプは毎週更新され、以下の URL から取得できる。

  • 新 URL: https://dumps.wikimedia.org/other/cirrus_search_index/
  • 旧 URL: https://dumps.wikimedia.org/other/cirrussearch/ (DEPRECATED)

各プロジェクトには _content (本文) と _general (その他) の 2 種類のインデックスがある。 すべて同じ NDJSON 形式のため、既存の scripts/extract-cirrus.py を流用可能。

ダンプ URL の形式:

https://dumps.wikimedia.org/other/cirrus_search_index/{YYYYMMDD}/index_name={project}_content/

日本語プロジェクト一覧

プロジェクト内容コーパスとしての有用性
jawiki日本語 Wikipedia最大規模。本プロジェクトで使用中
jawikisourceウィキソース(著作権切れ文献)法令文書・漢文・古文が中心。詳細後述
jawikibooksウィキブックス(教科書)法律コンメンタールが大半。詳細後述
jawikinewsウィキニュース質は良いが規模小。詳細後述
jawikiquoteウィキクォート(引用句集)短文中心。量は少ない
jawikiversityウィキバーシティ(学習教材)量は少ない
jawikivoyageウィキボヤージュ(旅行ガイド)固有名詞が多め。量は少ない
jawiktionaryウィクショナリー(辞書)辞書的記述が中心
jawikimediaウィキメディア(メタ的ページ)運営関連テキスト。コーパスとしては不向き

jawikisource の調査

項目
記事数17,008
テキスト量64.8M words

青空文庫との重複

青空文庫 ~17,000 作品のうち、Wikisource にインポート済みはわずか 315 (約 1.8%)。 jawikisource は青空文庫の代替にはならない。

内容の内訳

jawikisource の大半はかな漢字変換に不向きなコンテンツで占められている。

カテゴリページ数かな漢字変換への有用性
PD-JapanGov (法令・政府文書)3,294不向き: 法令文体
PD-JapanGov-old (旧法令)2,155不向き: 法令文体
PD-old (古い著作物全般)3,490不向き: 古文混在
CC-BY-SA-3.0 (翻訳物)2,752不向き: 翻訳調
PD-old-50-1996 (著作権切れ)1,506不向き: 古文混在
キリスト教1,269不向き: 翻訳宗教文献
漢詩53不向き: 漢文
万葉集 / 古今和歌集51不向き: 古文
棋譜subcats あり不向き: 自然言語ではない
人口統計データ50州分不向き: 統計テーブル
日本の小説290有用だが青空文庫と重複する可能性大
青空文庫からインポート315青空文庫と完全に重複
随筆66有用

有用な近代日本語テキストは数百ページ程度しかなく、フィルタリングコストに見合わない。


jawikinews の調査

項目
記事数4,073
テキスト量1.75M words
記事サイズ平均 ~3KB
期間2005年〜2025年

主要トピック:

カテゴリページ数
日本3,053
社会1,698
スポーツ896
政治576
文化500
経済400

テキストの質は良い(現代ニュース記事体で自然な日本語)。 しかし 175 万ワードと規模が小さすぎる。jawiki が数十億ワード規模であることを考えると量的インパクトは無視できる程度。


jawikibooks の調査

項目
記事数17,378
テキスト量17.5M words
記事サイズ平均 ~3KB

主要コンテンツの内訳:

コンテンツ推定ページ数備考
法律コンメンタール (逐条解説)~4,700+民法1,327、会社法1,113、刑訴法738、刑法315 等
高校教科書217世界史B、英語文法など。自然な説明文で質は良い
大学入試165
レシピ / 料理196材料リスト+手順。文体が特殊
プログラミング162+コード中心。日本語テキストとしては薄い
中学校教育73

全体の大部分を法律コンメンタールが占める。 コンメンタールは「条文引用 + 短い解説」の定型的な構造で、法律用語の偏りが大きい。 教科書系の説明文は質が良いが数百ページ程度しかなく、フィルタリングコストに見合わない。

jawiktionary (日本語版ウィクショナリー) 評価 (2026-02)

概要

Japanese Wiktionary (jawiktionary) を n-gram 統計データ生成のコーパスとして利用可能か評価した。

結論: コーパスとしての量が不十分であり、導入は非推奨。

データ量

項目
総エントリ数 (namespace 0)約 464,874
総テキスト量約 6,790 万文字
うち日本語文字約 2,400 万文字 (35%)
CirrusSearch dump サイズ329 MB (gzip)
1 エントリ平均テキスト長約 146 文字

他コーパスとの比較

コーパス日本語テキスト量比率
jawiki (Wikipedia)~30 億文字125x
CC-100 (ja)~258 億文字1,075x
青空文庫~5,200 万文字2.2x
jawiktionary~2,400 万文字1x

青空文庫の約半分、jawiki の 1/125 程度。既存パイプラインに追加しても全体の 1% 未満の増加にしかならない。

コンテンツの特性

見出し語の言語分布

文字種エントリ数割合
CJK / かな (日本語・中国語)161,17834.7%
ラテン文字 (英語・欧州語)179,74338.7%
その他 (韓国語、アラビア語等)123,95326.7%

エントリの 約 2/3 が外国語の見出し語 であり、日本語での短い定義文が付いているだけ。

テキストの内容

  • 辞書定義文: 「〜すること。」「〜もの。」「〜の状態。」等の定型パターン
  • IPA 発音記号 (エントリの約 69% に含まれる)
  • 多言語翻訳セクション (エントリの約 30%)
  • 活用表 (エントリの約 13%)
  • 用例: 文学作品からの引用を含むものもあるが少数

n-gram 統計への影響

  • 辞書特有の定型表現に偏った bigram が生成される
  • 外国語テキスト、IPA 記号がノイズとして混入する
  • 自然な日本語の文章としての質が低い

データ入手方法

CirrusSearch dump として入手可能。既存の extract-cirrus.py でそのまま処理できる。

https://dumps.wikimedia.org/other/cirrussearch/{DATE}/jawiktionary-{DATE}-cirrussearch-content.json.gz

評価

観点評価
データ量不十分 (青空文庫の半分以下)
テキスト品質低 (辞書定型文、外国語混在)
導入容易性高 (既存スクリプトで対応可)
総合判断非推奨

量・質ともに既存コーパス (jawiki + 青空文庫 + CC-100) に追加する価値がない。

corpus-stats v2026.0216.0 比較レポート

概要

corpus-stats を v2026.0211.1 から v2026.0216.0 に更新した際の evaluate 結果の比較。

  • コミット: c4b009c2 (main)
  • 評価日: 2026-02-16

スコア比較

指標v2026.0211.1 (旧)v2026.0216.0 (新)差分
Good67076712+5
Top-5348351+3
Bad40104002-8
Recall93.1647%93.1246%-0.04%

Good が 5 件増え、Bad が 8 件減少。Recall は微減だが Good/Bad 比は改善。

差分の詳細

改善 68 件、退行 60 件(ネット +8 改善)。

改善の傾向: 数字+助数詞の変換精度向上

数字の後の助数詞・単位の変換が大幅に改善された。

旧 (v2026.0211.1)新 (v2026.0216.0)期待値
2会2回2回
3じ3時3時
1周間1週間1週間
2原画体育2件が体育2限が体育
2塔の犬2頭の犬(GOOD)2頭の犬
78才78歳78歳
2健之金井喫茶店2件しかない喫茶店2軒しかない喫茶店
22字杉に夕飯22時過ぎに夕飯22時過ぎに夕飯

壊滅的な誤変換(2健之金井言っ週刊ぐらい前)が解消されている点が特に良い。

退行の傾向1: 漢数詞のアラビア数字化

漢数詞で書くべきところがアラビア数字に変換されてしまう。

入力新の出力期待値
ここであったがひゃくねんめ100年目百年目
よいいちねんになりますように1年一年
さんにんしかいない3人三人
いっぱくしかしないようだ1泊一泊
もういっさつの1冊の一冊の
もういってん1点一点

退行の傾向2: 数詞パーサの誤爆

数字に関係のない単語が数値として誤認識される深刻な退行。

入力新の出力期待値
せんねん(専念)1000年専念
まんさい(満載)10000歳満載
ちょうてん(頂点)1000000000000点頂点
ちょうひょう(帳票)1000000000000票帳票
せんだい(仙台)1000代仙台
まんかい(満開)10000回満開
まんびょう(万病)10000秒万病
いっさい(一切)1歳一切
ちょうじかん(長時間)1000000000000時間長時間

せん→1000まん→10000ちょう→1000000000000 の数詞パーサが、同音の漢字(専、万、長 等)に対して誤爆している。

所見

  • 助数詞パーサの改善で数字+助数詞の変換は確実に良くなった
  • ただし数詞パーサの誤爆(専念→1000年、仙台→1000代 等)は深刻で、修正が必要
  • 漢数詞のアラビア数字化(百年目→100年目)は表記スタイルの問題だが、慣用表現では漢数詞が自然

小文字かな辞書エントリの混入問題

概要

システム辞書 SKK-JISYO.akaza に小文字かな(拗音・促音)で始まる不正なエントリが 1,359件 混入していた。これにより、変換時に不正な分節が選択され誤変換が発生する。

例: 「じゅうらんしゃじけん」→「自ゅうらんしゃじけん」(銃乱射事件)

発生メカニズム

1. Wikipedia の読みがなにカタカナが混じる

Wikipedia の記事には以下のような表記がある:

コロンバイン高校銃乱射事件(コロンバインこうこうじゅうらんしゃじけん、英: ...)

2. 読みがな除去の正規表現がカタカナに非対応

wikipedia_extracted.rs の読みがな除去パターンはひらがなのみを対象としていた:

[(\(][\u3041-\u309F、]+[))]

「コロンバイン」はカタカナなのでパターンにマッチせず、括弧内の読みがながテキストに残る。

3. vibrato が読みがなを不正にトークナイズ

残った「こうこうじゅうらんしゃじけん」を vibrato がトークナイズすると:

  • こう (既知語)
  • こうじ (既知語: 麹、工事 等)
  • ゅうらんしゃじけん小文字ゅ始まりの未知語

vibrato が こうじ を貪欲にマッチした結果、残りが小文字かな始まりになる。

4. vocab → 辞書への混入

Wikipedia 中に31回出現するため wfreq の閾値(16)を超え、vocab に登録される。最終的にシステム辞書に ゅうらんしゃじけん /ゅうらんしゃじけん/ として入る。

5. 変換時の分節崩壊

ユーザーが「じゅうらんしゃじけん」と入力すると、辞書に ゅうらんしゃじけん があるため「じ」+「ゅうらんしゃじけん」に分節され、「自ゅうらんしゃじけん」と変換される。

修正内容

修正1: 読みがな除去パターンのカタカナ対応

wikipedia_extracted.rs の正規表現をカタカナも含むように変更:

# Before
[(\(][\u3041-\u309F、]+[))]

# After
[(\(][\u3041-\u309F\u30A0-\u30FF、]+[))]

修正2: vocab 生成時のバリデーション

vocab.rs に小文字かな始まりのエントリを弾くフィルタを追加。読みがな除去で漏れた場合の安全策:

  • ぁぃぅぇぉゃゅょっ(小文字ひらがな)
  • ァィゥェォャュョッ(小文字カタカナ)

で始まるエントリを vocab 生成時にスキップする。

影響範囲

辞書の不正エントリ 1,359件が次回の corpus-stats リビルドで解消される見込み。

変換精度改善の方針

現状の仕組み

モデルのパイプライン

  1. ベースモデル: akaza-corpus-stats の大規模コーパス統計から unigram/bigram の出現頻度を取得
  2. コーパス学習: learn-corpus が training-corpus の正解データを使ってスコアを補正
  3. 辞書: SKK-JISYO.L + SKK-JISYO.akaza で語彙をカバー

学習アルゴリズム(learn-corpus)

パーセプトロン的な手法を採用している:

  • 正解と変換結果が異なる場合、正解側の unigram/bigram 出現頻度を delta(=2000)ずつ加算
  • 正解と一致するまで、または最大エポック数に達するまで繰り返す
  • 不正解側のスコアを下げる処理はない(正解を引き上げるのみ)

コーパスの3階層

ファイルエポック数用途
must.txt10,000絶対に正しく変換すべきもの
should.txt100正しく変換してほしいもの
may.txt10できれば正しく変換したいもの

過学習のリスク

コーパスにエントリを追加していくと以下のリスクがある:

  • スコアの偏り: delta=2000 × 最大10,000エポックで、個別エントリのスコアがベース統計を圧倒しうる
  • 副作用: 特定の文脈でスコアを歪めると、同じ読みを含む別の文脈で誤変換が発生する
  • 片方向学習: 正解のスコアを上げるだけで不正解を下げないため、多数のエントリが積み重なるとスコア分布が不自然になる

改善アプローチ

1. エラー分類を先に行う

make evaluate の k-best スコアリング(--k-best 5)で誤変換を2種類に分類する:

  • BAD(top-k にも正解なし): 語彙やセグメンテーションの問題。コーパス学習では直らない可能性が高い
  • TOP-k(top-1 は外れだが候補にはある): スコアリングの問題。コーパス学習で改善の見込みがある

対処の優先度は BAD > TOP-k。BAD はユーザーが候補を探しても見つからないため体験が最悪。

2. 辞書で直せるものは辞書で直す

dict/SKK-JISYO.akaza への追加は語彙を増やすだけで、unigram/bigram のスコアバランスを崩さない。 BAD のうち「そもそも候補に出ない」ものはまず辞書で対処するのが安全。

3. コーパスは「パターン」で入れる

1つの誤変換を直すために1行追加するのではなく、同じ種類の誤変換パターンをまとめて入れる。

例: 「暑い/熱い/厚い」の使い分け → 典型的な共起パターンを複数入れて bigram で自然に学習させる

夏/なつ は/は 暑い/あつい
お湯/おゆ が/が 熱い/あつい
板/いた が/が 厚い/あつい

個別の文をピンポイントで追加するより、パターンとして入れた方が汎化しやすく過学習しにくい。

4. 定量的にモニタリングする

コーパスを追加するたびに make evaluate の結果を確認し、退行がないか検証する。

Good=XXXX, Top-5=XXXX, Bad=XXXX, 再現率=XX.XX

Good が増えずに Top-k から BAD に移動するような退行が起きていないか注意する。

現状の評価結果(2026-02-08)

Good=5870 (53.1%), Top-5=493 (4.5%), Bad=4702 (42.5%), 再現率=91.47%

全11,065文中、半数強が top-1 で正解。再現率(文字レベルの LCS ベース)は91.5%。

エラーパターン分析

BAD(4,702件)の内訳

カテゴリ件数割合説明
1箇所だけ誤り3,60076.6%文中の1箇所だけが間違っている
2箇所誤り93719.9%文中の2箇所が間違っている
3箇所以上1653.5%広範な誤り

文字数の差(セグメンテーション品質の指標):

件数割合
同一長2,92062.1%
差2以下4,66099.1%
差3以上420.9%

BAD のうち99%はセグメンテーション自体はほぼ正しく、漢字の選択ミス(同音異義語の取り違え) が主因。

主な誤パターン:

  • 同音異義語の誤選択(~1,585件, 33.7%): 「使用/仕様」「行/言」「日本/二本」「買/書」等。セグメンテーションは正しいが漢字選択が間違い
  • 漢字にすべきところがひらがな(~913件, 19.4%): 「明後日→あさって」「物→もの」「事→こと」等
  • ひらがなにすべきところが漢字(~538件, 11.4%): 「ください→下さい」(63件)、「ない→無い」(136件)等
  • 表記スタイルの不一致(588件, 12.5%): 「すでに」vs「既に」、「たくさん」vs「沢山」等
  • カタカナ/ひらがな不一致(~175件, 3.8%): 「ダメ→だめ」「ごみ→ゴミ」等
  • 全角/半角の違い(41件, 0.9%): 「?→?」「0→0」等。評価時の正規化で対処可能
  • 文末表現の誤解析(~80件): 「かね→鐘」「よね→米」「ますね→マスネ」「ないん→ナイン」

TOP-5(493件)の内訳

TOP-5 の 94.7% は1箇所だけの誤りで、スコア調整で修正可能。

主な改善可能パターン:

パターン件数
文末「かね」→「鐘」17「ありますかね」→「あります鐘」
文末「よね」→「米」10「ないですよね」→「ないです米」
「ないん」→「ナイン」7「ないんですよ」→「ナインですよ」
「ますね」→「マスネ」4「使えますね」→「仕えマスネ」
「きき」→「気気」4「危機に瀕する」→「気気に瀕する」
1文字漢字違い148様々な同音異義語
その他1箇所278-

スコアリングロジックの改善ポイント

重大度: 高

1. bigram バックオフが定数フォールバック

bigram が見つからない場合、default_edge_cost(= calc_cost(0, total_bigrams, unique_bigrams) ≈ 10-12)という単一の定数にフォールバックする。unigram 確率との補間(Jelinek-Mercer 補間や Stupid Backoff)がない。

影響: 「私は」のようなありふれた語の組み合わせでも、bigram モデルに含まれていなければ「鑢蛸」のような稀な組み合わせと同じペナルティを受ける。

改善案: lambda * P_bigram + (1-lambda) * P_unigram のような線形補間を導入する。

2. パスに長さ正規化がない

Viterbi のコストは sum(node_costs) + sum(edge_costs) の単純加算。セグメント数が増えるほど edge_cost が加算されるため、少ない分節数(長い単語)が体系的に有利

影響: 「きょういくのはなし」→「教育の話」(3分節)より「今日行くの話」(4分節のように見えるが実際は「教育」vs「今日行く」のセグメンテーション競合)が不利になる構造的バイアス。

3. コスト関数の smoothing が非標準

#![allow(unused)]
fn main() {
cost = -log10((count + 0.00001) / (total_words + 0.00001 + unique_words))
}

ALPHA=0.00001 が極端に小さく、count=0(未知語)と count=1 の間に約5.0のコスト断崖がある。標準的な加算スムージングでは分母は total + alpha * V だが、現在は total + alpha + V で、未知語への確率配分がほぼゼロ。

重大度: 中

4. OOV の二重ペナルティ

未知語はノードコスト(高い unigram ペナルティ)とエッジコスト(全ての接続が default_edge_cost)の両方で罰せられる。固有名詞などの正当な未知語が候補に上がりにくい。

5. OOV 判定のバイト長ヒューリスティック

surface.len() < yomi.len() で漢字変換かどうかを判定しているが、1文字漢字(「気/き」等、バイト長が同一)はフル OOV ペナルティを受ける。

6. 文末表現の体系的な誤変換

「かね」「よね」「ますね」「ないん」が漢字/カタカナに変換される問題は、文末の助詞・終助詞が直前の動詞と分離されてセグメント化され、独立した語として漢字変換される ことが原因。bigram で「ます→ね」の接続コストが「ます→マスネ」より高くなっている。

頻出誤変換パターンと対処の優先度

優先度: 高(アルゴリズム改善で多数のケースに効く)

パターン影響件数対処方法
文末助詞「かね/よね/ますね/ないん」→ 漢字・カタカナ~80件 (BAD+TOP-5)bigram「ます+かね」「です+よね」等の強化
「使用/仕様」(しよう)~35件bigram「使用+する/した/して」の強化
「行/言」(いって等)~33件bigram で移動先「に+行って」等を強化
「金/鐘」(かね)~40件unigram で「金」の重み引き上げ
「日本/二本」(にほん)~18件bigram「日本+語/人/で/の」等の強化
「ここ/個々」~25件unigram「ここ」の重み引き上げ
「買/書/勝/飼」(かう等)~60件目的語名詞との bigram 強化
「危機」→「気気」~5件辞書エントリの確認・追加

優先度: 中(表記規約の問題、方針を決めて対処)

パターン影響件数備考
「ください/下さい」~63件補助動詞としてはひらがなが現代標準
「ない/無い」~136件anthy コーパスは「無い」を好む
「もの/物」「こと/事」「ところ/所」~129件形式名詞としてはひらがなが標準
「ダメ/だめ/駄目」等カタカナ・ひらがな揺れ~175件文脈依存、統一困難

優先度: 低(評価方法の改善で対処可能)

パターン影響件数備考
全角半角「?/?」「0/0」~41件評価時に正規化すれば解消

スコアリングロジック改善の優先順位

上記のエラー分析とコードの構造分析を総合すると、以下の順で取り組むのが効果的:

  1. bigram バックオフの改善 — 最多のエラー原因。定数フォールバックから unigram 補間への変更で、同音異義語の文脈判定が大幅に改善する見込み
  2. 文末助詞の bigram 強化 — コーパス追加で対処可能で即効性がある。「ます+かね」「です+よね」等のパターンを training-corpus に入れる
  3. コスト関数の smoothing 改善 — ALPHA の調整と標準的な加算スムージング式への変更で、OOV 周辺の挙動が安定する
  4. パス長の正規化 — 長い単語への構造的バイアスを緩和

CC-100 重み付き統合レポート

概要

CC-100 (Common Crawl ベースの大規模日本語コーパス) を akaza のコーパス統計に統合した。 単純に weight=1.0 で統合すると大幅な退行が発生するため、path:weight 形式の重み付け機能を akaza-data に実装し、CC-100 を weight=0.3 で統合することで退行を抑えつつ改善を実現した。

背景

akaza のベースモデルは jawiki (Wikipedia 日本語版) と青空文庫のコーパス統計から構築される。 Wikipedia は百科事典であるため、以下の弱点がある:

  • 日常会話語が弱い(買う、結構、寝る、使う 等の基本語の頻度が低い)
  • 歴史用語・学術用語が過剰に強い(前漢、契機、巨星 等)
  • 口語表現の bigram がほぼ存在しない

CC-100 は Web クロールベースのコーパスで、日常的なテキストを多く含むため、 これらの弱点を補完できると期待された。

問題: weight=1.0 での統合失敗

CC-100 は jawiki の約 1.9 倍のボリュームがあり、weight=1.0 で統合すると CC-100 の統計が jawiki を圧倒してしまう。

統合前後の比較 (weight=1.0)

指標jawiki+青空文庫のみ+ CC-100 (weight=1.0)差分
Good6141--
Bad4381--
再現率92.17%90.89%-1.28%

退行 1927件、修正 1587件。ネットで +340 BAD という大幅な悪化。

主な退行パターン:

  • 機能語の同音異義語崩壊: と→途 (48件), さい→賽 (47件), そう→総 (29件), ご→五 (22件)
  • CC-100 のノイズ由来の珍語が高スコア化

解決策: 重み付け機能の実装

akaza-data への変更 (akaza-im/akaza#425)

wfreqwordcnt-bigram コマンドの --src-dir / --corpus-dirs 引数で "path:weight" 形式を受け付けるようにした。

  • utils.rs: parse_dir_weight() ヘルパー追加
  • wfreq.rs: 内部集計を u32f64 に変更、出力時に round() で整数化
  • make_stats_system_bigram_lm.rs: 同様の f64 集計対応

weight 省略時は 1.0 として扱うため、既存の使い方に影響なし。

akaza-corpus-stats への変更 (akaza-im/akaza-corpus-stats#1)

-full ターゲットで CC-100 に CC100_WEIGHT=0.3 を適用:

CC100_WEIGHT ?= 0.3

# wfreq
--src-dir=work/cc100/vibrato-ipadic/:$(CC100_WEIGHT)

# wordcnt-bigram
--corpus-dirs work/cc100/vibrato-ipadic/:$(CC100_WEIGHT)

結果: weight=0.3 での統合

指標jawiki+青空文庫のみ+ CC-100 (weight=0.3)差分
Good61416457+316
Bad43814154-227
再現率92.17%92.76%+0.59%
Real BAD (filtered)37613630-131

修正 2137件、退行 1910件。ネットで -227 BAD の改善。

修正されたパターンの分析 (2137件)

1. 同音異義語・漢字選択誤り — ~1,340件 (62.6%)

Wikipedia では低頻度だが日常では高頻度な語の unigram スコアが CC-100 で補強された。

誤 → 正件数
勝った → 買った20お友達で勝った人買った人
返還 → 変換17返還エンジン変換エンジン
肩 → 方(かた)15興味のある肩は興味のある方は
熱い → 暑い/厚い12熱い雑誌を読む厚い雑誌を読む
欠航 → 結構10欠航いい感じ結構いい感じ
週刊 → 週間81週刊のみ1週間のみ
会った → あった27連絡が会った連絡があった
練る → 寝る7練ることにします寝ることにします
仕様 → 使用7仕様されている使用されている
意外 → 以外7英語意外の英語以外の

2. 分節崩壊の修正 — ~530件 (24.8%)

日常的な語の bigram が強化され、正しい語境界が選ばれるようになった。

  • 15ねんグライツ勝手いるもので15年ぐらい使っているもので
  • 2改名硬派2回目以降は
  • 高杉ててが出ません高すぎて手が出ません
  • 口元を誇ろバス口元を綻ばす

3. 助詞・接辞の漢字化修正 — ~94件 (4.4%)

ひらがなであるべき助詞が同音の漢字に変換されるケースが修正された。

  • 遊べ途言って遊べと言って (途→と: 36件)
  • ない名ないな (名→な: 44件)

4. 不正カタカナ変換修正 — 85件 (4.0%)

  • 11ジゴロ起きた11時頃起きた
  • アイテルの稼働ナノカ空いてるのかどうなのか

5. ひらがな未変換修正 — 48件 (2.2%)

  • あさって明後日
  • あきんど商人

6. Wikipedia 偏り修正 — ~31件 (1.4%)

  • 契機先行き景気先行き
  • 前漢出撃命令全艦出撃命令

退行パターン (1910件)

退行の大部分は以下のパターン:

  • 数字+助数詞: 10ねん→十念 (NUMBER が漢数字化)、2かい→2書い (分節崩壊)
  • CC-100 ノイズ由来: Web テキスト特有の表記揺れやタイプミスが統計に混入

考察

weight=0.3 の妥当性

CC-100 は jawiki の約 1.9 倍のボリュームがあるため、weight=0.3 を適用すると 実効的な寄与は 1.9 × 0.3 = 0.57 倍程度となり、jawiki を超えない範囲に収まる。 これにより jawiki の統計を基盤としつつ、CC-100 で日常語を補強する構成が実現できた。

今後の改善余地

  • weight の最適化: 0.3 は初期値。0.2〜0.5 の範囲で evaluate を回して最適値を探索できる
  • 数字+助数詞の退行対策: CC-100 由来の退行の多くは数字パターン。NUMBER の特別扱いや、 数字周辺の bigram を training-corpus で補強することで対処可能
  • CC-100 のフィルタ強化: extract-cc100.py のフィルタを強化し、ノイズの多い文を除外する

検証手順

本レポートの評価結果は、corpus-stats のリリース前にローカルで再現した。

  1. akaza-im/akaza#425 をマージ後、cargo install --path akaza-data で akaza-data を更新
  2. akaza-corpus-stats リポジトリで make clean-tokenized && make all-full を実行し、 CC-100 weight=0.3 の統計データをローカルビルド
  3. 生成された trie ファイルを akaza-default-model の work/ に手動コピー:
    cp akaza-corpus-stats/work/stats-vibrato-unigram-full.wordcnt.trie  work/stats-vibrato-unigram.wordcnt.trie
    cp akaza-corpus-stats/work/stats-vibrato-bigram-full.wordcnt.trie   work/stats-vibrato-bigram.wordcnt.trie
    cp akaza-corpus-stats/work/vibrato-ipadic-full.vocab                work/vibrato-ipadic.vocab
    
  4. make でモデル再ビルド、make evaluate で評価

corpus-stats の正式リリース後は、akaza-default-model の CORPUS_STATS_VERSION を 更新して make するだけで同じ結果が再現される。

関連 PR

  • akaza-im/akaza#425: path:weight 形式の実装
  • akaza-im/akaza-corpus-stats#1: CC-100 に weight=0.3 を適用

実行日

2026-02-10

<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.rsnormalize_num_token() を追加:

  • 1匹/1ひき<NUM>匹/<NUM>ひき (正規化する — reading 側はかな読みを保持)
  • 100円/100えん<NUM>円/<NUM>えん (正規化する)
  • 2019年/2019ねん<NUM>年/<NUM>ねん (正規化する)
  • 1/11/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.rsnormalize_surface_for_lm() を追加:

ラティス構築時、unigram LM でエントリが見つからない場合にフォールバックとして <NUM> 正規化キーでの lookup を試みる。これにより、LM に <NUM>匹/<NUM>ひき が 登録されていれば 3匹/3ひき でもスコアを取得できる。

セグメンタ側 (複合セグメント生成)

libakaza/src/graph/segmenter.rsSegmenter::build() を拡張:

数字マッチ後、後続のかなトライマッチも試行して複合セグメントを追加する。 例えば入力 90ぎょう に対して、個別セグメント (90 + ぎょう) に加えて 複合セグメント (90ぎょう) も生成する。Viterbi が LM スコアに基づいて最適パスを選ぶ。

グラフビルダー側 (複合ノード構築)

libakaza/src/graph/graph_builder.rsGraphBuilder::construct() を拡張:

segmented_yomi が数字+かな(例: 90ぎょう)の場合:

  1. 数字部分 (90) とかな部分 (ぎょう) を分離
  2. かな部分をかな漢字辞書で検索 → ["行", "業", ...]
  3. 候補生成: 90行 (LM lookup: 90行/90ぎょう → fallback <NUM>行/<NUM>ぎょう)
  4. 漢数字候補も追加: 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 (正規化なし)裸の数字含む正規化差分
Good64576352-105
Bad41544233+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 正規化差分
Good64576428-29
Bad41544179+25
再現率92.76%92.72%-0.04%

退行分析 (60件)

退行の大部分は数字の表記スタイル差であり、日本語としての正誤ではない:

  • 一時1時 (漢数字 → アラビア数字)
  • 三人3人
  • 二度2度
  • 十年間10年間

これらは accept.tsv に追加可能なスタイル差であり、実質的な品質低下ではない。

改善分析 (35件)

改善は実質的な変換品質向上:

  • 正午字正午時 (数字集約による正しいスコア付与)
  • 一瓢一票
  • ひとり一人
  • 10じかん10時間 (未知語だった助数詞パターンがカバー)

結論

<NUM> トークン正規化は、裸の数字を除外する条件付きで導入すべきである:

  1. 退行は表記スタイル差 (漢数字 vs アラビア数字) であり accept.tsv で吸収可能
  2. 改善は実質的な品質向上 (未知助数詞パターンのカバレッジ拡大)
  3. 致命的な退行 (助詞→数字) は修正済み

関連 PR / リリース

  • akaza-im/akaza#426: 初回実装 (<NUM> 正規化)
  • akaza-im/akaza#428: 裸の数字除外修正
  • akaza-im/akaza-corpus-stats v2026.0211.0: suffix-only 正規化を含むリリース

実行日

2026-02-11

失敗記録: bigram バックオフに unigram 補間を導入

日付: 2026-02-08

仮説

現在の get_edge_cost_with_user_data は、bigram が見つからない場合に固定の default_edge_cost(≈10-12)を返す。この定数は「私+は」のようなありふれた語の組み合わせにも「鑢+蛸」のような稀な組み合わせにも同じペナルティを与えてしまうため、同音異義語の誤選択の原因になっているのではないか。

両方の語が語彙に存在する場合、それぞれの unigram コストを利用して「この語の組み合わせはどの程度自然か」を推定すれば、よりなめらかなバックオフを実現できるのではないか。

変更内容

libakaza/src/graph/lattice_graph.rsget_edge_cost_with_user_data を変更。

#![allow(unused)]
fn main() {
// 変更前
} else {
    self.system_bigram_lm.get_default_edge_cost()
}

// 変更後
} else {
    let backoff = (prev_score + node_score) / 2.0 + BIGRAM_BACKOFF_PENALTY;
    backoff.min(self.system_bigram_lm.get_default_edge_cost())
}
}

BIGRAM_BACKOFF_PENALTY = 3.0 を定数として追加。

評価結果

指標ベースライン (変更前)バックオフ導入後差分
Good58704453-1417
Top-5493775+282
Bad47025837+1135
再現率91.47%86.23%-5.24pt

考察

再現率が 91.47% → 86.23% に 5.24 ポイント大幅悪化した。

バックオフコストが default_edge_cost より低くなることで、bigram が未登録の語ペアに対して「遷移しやすい」と過大評価してしまい、本来選ばれるべきでない候補が選ばれるようになったと考えられる。

具体的には、unigram コストが低い(= 頻出の)語同士であれば bigram 未登録でも低コストで遷移可能になるが、「頻出語同士だから自然な組み合わせ」とは限らない。例えば「日本」と「だけ」はそれぞれ頻出語だが、「二本だけだ」→「日本だけだ」のような誤変換を誘発する可能性がある。

bigram が未登録であるということ自体が「この組み合わせはコーパス上で観測されなかった」という強いシグナルであり、固定のペナルティで抑制するのが妥当だったと思われる。

結論

この方針は採用しない。変更はすべて revert 済み。

BAD 傾向分析レポート (2026-02-16)

概要

  • コミット: 917ec986 (main)
  • corpus-stats: v2026.0216.0
  • 評価日: 2026-02-16
  • Good: 6,727 / Top-5: 345 / Bad: 3,993 / Recall: 93.19%

BAD 3,993件の内訳を分類し、改善の方向性を検討する。

カテゴリ別内訳

カテゴリ件数割合対処方針
同音異義語・誤変換3,50487.8%コーパス / bigram / スコアリング改善
表記揺れ(ひらがな⇔漢字・カタカナ)3679.2%accept.tsv でフィルタ
数字含み文の誤変換671.7%数詞パーサ / コーパス
全角半角・句読点差401.0%accept.tsv でフィルタ
数詞→アラビア数字化150.4%数詞パーサ修正 / コーパス

即効性のある改善: accept.tsv の拡充

表記揺れ 367件 + 全角半角差 40件 = 約407件は、accept.tsv に追加するだけで Real BAD を 3,993 → 約3,586 に削減できる。

表記揺れの主なパターン

パターン件数
無い → ない91心当たりが無い → 心当たりがない
気 → 木 の混同ではなく良/よ差39良いとも → いいとも
分かる → わかる21分かった → わかった
良い → いい / よい18+10良く冷えた → よく冷えた
付く → つく14区別が付かん → 区別がつかん
行く → いく13幕張行くんで → 幕張いくんで
言う → いう12と言うか → というか
鳴く → なく12目覚ましが鳴った → なった
見る → みる11見ます → みます

これらは日本語としてどちらも正しい表記であり、accept.tsv に追加して評価から除外すべき。

全角半角の差 (40件)

? vs % vs など。これらも accept.tsv でフィルタ可能。

主要課題: 同音異義語 (3,504件)

BAD の大半を占める。さらにサブカテゴリに分類する。

高頻度の1文字差 同音異義語ペア

corpus → akaza件数深刻度対処案
気 → 木36bigram「木がする」を抑制。「気がする」コーパス追加
と → 途24「途」の unigram スコアが高すぎる。助詞「と」の接続を強化
再 → 賽17「賽」の unigram 抑制。「再コンパイル」等コーパス追加
話 → 和15「話」の短い読み「わ」が「和」に負ける
あった → 会った14「あった」(存在)と「会った」(面会)の文脈区別
以上 → 異常9「以上よろしく」のbigram強化
結構 → 欠航9「欠航」のスコアが高すぎる(Wikipedia偏り)
そこ → 底9「底」が文頭・助詞位置に出現
大 → 第8「大至急」→「第至急」など。bigram 不足
資料 → 飼料7Wikipedia の畜産記事による偏り
円 → 縁7通貨の「円」が「縁」に負ける
旧 → 急7「旧体制」→「急体制」
使用 → 私用6「しよう」の同音衝突
暑い → 厚い6文脈依存の同音異義語

分節崩壊 (34件)

corpus と akaza の長さが3文字以上異なるケース。分節の切り方自体が壊れている。

入力期待実際
じゅうらんしゃじけん銃乱射事件自ゅうらんしゃじけん
がんこうしゅていのやから眼高手低の輩眼光シュテイ野家から
1にちさいちょう8じかん1日最長8時間1日祭超8字管

分節崩壊は辞書への複合語登録で対処するのが効果的。

未変換パターン (117件)

漢字に変換されるべきところがひらがなのまま残る。

入力期待実際
どんぐりのせいくらべ団栗の背比べどんぐりのせいくらべ
めったにいかないけど…滅多に行かないけど…めったにいかないけど…
たくさんもらっても沢山貰ってもたくさんもらっても
でふぉるとのせっていで…デフォルトの設定で…デフォルトの設定でごちゃごちゃ…

多くは口語的な表現で、ひらがな表記も自然なケースが多い。accept.tsv 候補でもある。

過変換パターン (21件)

ひらがな・カタカナで書くべきところが漢字化される。

入力期待実際
バカにはバカなりのバカにはバカなりの馬鹿には馬鹿なりの
ダイオウイカダイオウイカ大王烏賊

改善の優先順位

優先度1: accept.tsv 拡充 (推定 -400件)

最も低コストで効果が大きい。表記揺れ・句読点差をフィルタすることで、Real BAD を約3,586件に削減。注力すべき真の問題が見えやすくなる。

対象:

  • ひらがな⇔漢字の表記揺れ (無い/ない、分かる/わかる、良い/いい 等)
  • ひらがな⇔カタカナの表記揺れ (バカ/馬鹿、マジ/まじ 等)
  • 全角⇔半角 (?/?、%/%)

優先度2: 高頻度同音異義語のコーパス追加 (推定 -100〜200件)

patterns.txt の上位パターンに対して should.txt にコーパスを追加。

特に効果が見込めるもの:

  • 気→木 (36件): 「気がする」「気にする」「気をつける」等の頻出フレーズ
  • と→途 (24件): 助詞「と」の bigram 強化
  • 再→賽 (17件): 「再起動」「再コンパイル」「再利用」等
  • 以上→異常 (9件): 「以上よろしく」「以上のように」
  • 結構→欠航 (9件): 「結構いい」「結構です」

優先度3: Wikipedia 偏りの是正 (推定 -50件)

Wikipedia に偏った語のスコアが日常語を上回るケース:

  • 賽(賽の河原)が「再」に勝つ
  • 欠航が「結構」に勝つ
  • 飼料が「資料」に勝つ
  • 数寄が「好き」に勝つ
  • 咆哮が「方向」に勝つ

対処: should.txt に日常語のコーパスを追加して矯正。

優先度4: 辞書への複合語登録 (推定 -30件)

分節崩壊を防ぐため、SKK-JISYO.akaza に複合語を登録する案。

ただし、分節崩壊の例に挙がった「眼高手低」のようなケースは注意が必要。 「眼高手低」は SKK-JISYO.L には未収録だが、JMdict には収録されている。Wiktionary の四字熟語カテゴリには未収録。 日常で使われる頻度は低く、anthy コーパスにたまたま含まれている程度。 こういったエントリのために個別に辞書を足すのは anthy コーパスへの過学習。

四字熟語の体系的な補完ソースとして以下を調査した:

結論: evaluate の BAD から個別に辞書を足すのは過学習。体系的に補完するなら JMdict の yoji エントリと SKK-JISYO.L の差分を取るアプローチが有効。ただしニッチな四字熟語を追加しても変換候補のノイズが増えるリスクがあるため、実際のユーザー頻度を考慮すべき

優先度5: 数詞パーサの改善 (推定 -80件)

数字含み文の誤変換 67件 + 数詞アラビア数字化 15件。 「せん→1000」「まん→10000」等の数詞パーサ誤爆は v2026.0216.0 で一部コーパスで対処済みだが、根本的にはパーサ側のスコアリング調整が必要。

まとめ

施策推定削減コスト備考
accept.tsv 拡充-400件スクリプトで半自動化可能
高頻度同音異義語コーパス-100〜200件退行チェック必須
Wikipedia 偏り是正-50件双方向同音異義語に注意
辞書複合語登録-30件副作用少ない
数詞パーサ改善-80件コード変更必要

全施策を実施した場合、Real BAD を現在の約3,993件から約3,200〜3,400件程度まで削減できる見込み。