二段構成タグ付け
Litsea の segment_with_pos() は二段構成(two-stage)モデル
(--pos、Segmenter::with_two_stage_learner、issue #147)に
支えられています。モデルファイルの litsea-two-stage v1 ヘッダがこの形式を
識別し(モデルファイル形式を参照)、
メソッドは Vec<(String, Upos)> の単語/タグのペアを返します。
仕組み
二段構成タグ付けは、文字レベルで POS クラスを採点することは一切ありません:
- stage 1 は二値の境界分類器で分割します –
segment()が使うのと 構造的に同じスカラー重みの AdaBoost 形式なので、同じ速度で動きます。 - stage 2 は stage 1 が確定した単語ごとに、学習コーパスから構築した
候補タグ語彙表と単語単位の分類器でタグ付けします
(モデルファイル形式を参照):
- 学習時に観測されたタグが 1 個しかない、あるいは最頻タグが学習時の
出現の
dominance割合以上を占める表層は、分類器を一切呼ばずに タグ付けされます。 - 曖昧な既知表層は、観測された候補タグのみ(マスク付き argmax)に 絞って採点されます(全 18 クラスではありません)。
- 未知の表層は全クラス argmax にフォールバックします。
- 学習時に観測されたタグが 1 個しかない、あるいは最頻タグが学習時の
出現の
単語は平均して 1 文字より多くの文字を含み、大半の単語は曖昧でないか 分類器そのものをスキップするため、stage 2 の総コストは文字単位の多クラス 採点のごく一部で済みます。これが POS パスを通常の分割とほぼ同じ速さにして いる理由です: 歴史的な joint アーキテクチャ(二段構成への一本化に伴い削除) は各文字位置ごとに約 18 個の UPOS クラス全てを採点しており、同じコーパスで 1.8〜2.8 倍遅く動作していました。
方法論についての注記: 十分な学習エポック数を使う
二段構成の展開時に行ったエポックスイープ(10〜150 エポック)により、 モデル品質は旧来のモデルが学習されていた 10 エポック慣習を大きく超えて 向上し続け、UD GSD 学習セットでは 50 エポック前後でプラトーに達することが 分かりました。特に stage 1 の境界のみの特徴量は、よりリッチな文字ごとの 特徴量セットに比べて、同じコーパスで収束するのにより多くのエポックを 必要とします。同梱の二段構成モデルは 50 エポックを使用しています。 再学習の際、一発の低エポック実行ではこのアーキテクチャの到達可能な品質を 過小評価することになります。
実測の品質とスループット
held-out の数値は UD GSD/EWT test split に対する litsea evaluate --pos
(事前学習済みモデルを参照)です。スループットは
本プロジェクトの開発機での cargo bench -- external_corpus で、
専用のアイドルハードウェアではありません。実行間の振れ幅は無視できない
大きさです – 方法論とその限界については
ベンチマークを参照してください。
| 言語 | Word F1 | Tagged F1 | スループット |
|---|---|---|---|
| 日本語 | 96.78% | 92.95% | 4.38M chars/s |
| 中国語 | 90.82% | 82.29% | 3.38M chars/s |
| 韓国語 | 99.88% | 93.95% | 4.21M chars/s |
| 英語 | 98.30% | 90.55% | 7.32M chars/s |
留意すべき 2 つの観察:
- 候補タグ制限は実在する品質レバーです。 既知単語の候補を実際に 観測されたものに制限することで、分類器がありえないタグを選ぶ機会の 大半が排除され、単語あたりの特徴量セットが小さいことによる不利を 補います。
- スループットは語彙表のカバレッジによって変わります。 韓国語は held-out の未知語率が高いため、安価な dominance スキップや 候補マスクの経路ではなく、stage 2 の全クラスフォールバックを払う 単語の割合が大きくなります。英語は issue #198 以降その対極にあります: トークンの約 43% が空白であり、そのすべてが候補 1 個の語彙表ヒットと なって分類器を完全にスキップします。
ここで最も効くのはコーパスのプロトコル(issue #198)
stage 1 の分類器は、学習に使ったテキスト以上に良くはなりません。
issue #198 までは、二段構成の extractor は空白区切りの word/POS
コーパスを読み込み、各文を単語表層の区切りなしの連結として
復元していました – 実際の文章に空白がない日本語・中国語では正しい
処理ですが、韓国語・英語では入力が持つ最も強い境界シグナルを
捨ててしまっていました。
代わりに空白保持コーパス(extract --pos --format tsv)で学習することで、
両言語とも専用の分割モデルと同水準に到達しました:
| 言語 | Word F1(変更前) | Word F1(変更後) | Tagged F1(変更前) | Tagged F1(変更後) |
|---|---|---|---|---|
| 韓国語 | 94.01% | 99.88% | 83.20% | 93.95% |
| 英語 | 77.55% | 98.30% | 69.89% | 90.55% |
(「変更前」の列は issue #196 の実運用/スペース付きの測定値であり、
空白非保持プロトコルの数値ではありません。したがってユーザーが実際に
得ていたものどうしの公平な比較になっています。)韓国語はいま
korean.model の 99.91% との差が 0.03 ポイント、英語は english.model
の 98.31% との差が 0.01 ポイントです。
空白非保持コーパスは2 つの異なる学習と推論のミスマッチを 引き起こしていましたが、どちらも解消されました:
- stage 1 は、これらの言語でほとんどの語境界を示す空白文字を 一度も見ていませんでした。
- stage 2 も、より分かりにくい形で影響を受けていました: その文脈
特徴量(
L1〜L3/R1〜R3とcl1〜cl3/cr1〜cr3)は 周囲の文字を参照しますが、推論時に単語の隣は通常空白であるのに対し、 空白非保持の学習では次の単語の文字になっていました。空白はトークンの 約 43% を占めるため、事実上すべての単語が影響を受けていました。
空白トークンには stage 2 の学習行を与えていません(退化した X クラス
1 個だけで学習行の約 43% を占めてしまうため)が、語彙表エントリは与えて
います。これにより空白は候補 1 個となり、分類器を呼ばずに固定タグ経路で
X としてタグ付けされます – 決定的であり、従来のモデルがフォールバック
していた全クラス argmax の推測よりも安価です。
日本語・中国語はこの一連の変更の影響を受けません。これらの言語のテキスト には空白がなく、空白区切りコーパスは既に実際の入力と一致していたためです。
stage-2 特徴量セットの選び方
stage 2 の単語単位タガーは 3 種類の特徴量セット(litsea extract --pos の --stage2-features。特徴量の抽出を参照)で
抽出でき、タグ付け品質とスループットをトレードオフできます。分割品質は
影響を受けません – 分割は stage 1 だけで決まります。以下の数値は
同梱モデルと同じ 50 エポックでの値です。
| 特徴量セット | 中国語 Tagged F1 | 韓国語 Tagged F1 | 英語 Tagged F1 |
|---|---|---|---|
fast(既定) | 81.33% | 90.62% | 88.68% |
balanced | 82.29% | 92.95% | 88.66% |
full | 82.96% | 93.33% | 90.43% |
(韓国語と英語は issue #198 に伴って空白保持コーパスで再スイープしており、
この 2 列はそのスイープの dev split の数値です。中国語の列はそれ以前の
ものです。その結果、韓国語の勝者は balanced から full に変わりました。)
日本語では fast だけで Tagged F1 92.95% に到達するため、同梱の
japanese_pos.model はこれを使用しています。
中国語では balanced が full の伸びの大半(82.29% vs 82.96%)を
明確に良いスループットで得られるため、chinese_pos.model は
full ではなく balanced を使用しています。空白保持コーパスでの
韓国語・英語では full が明確な勝者であるため(韓国語は 93.33% vs
balanced の 92.95%、英語は 90.43% vs 他の 2 つの約 88.7%)、
korean_pos.model と english_pos.model はどちらも full を
使用しています。別の特徴量セットで再学習するには
extract --pos --stage2-features <set> + train --pos を再実行するだけで、
パイプラインの他の部分を変更する必要はありません。