予測パイプライン
本章では、Segmenter::segment() が入力テキストを処理する手順をステップごとに解説します。
例: “これはテストです。” の分割
ステップ 1: パディングで配列を初期化
chars: ["B3", "B2", "B1"]
types: ["O", "O", "O" ]
tags: ["U", "U", "U", "U"]
tags 配列には “U” が 1 つ余分に追加されます。これは tags[3] が最初の実際の文字のタグを表し、先行する境界判定がないため “Unknown” に設定されるためです。
ステップ 2: 入力文字のスキャン
入力の各文字について、言語固有のパターンを使用して種別を決定し、配列に追加します:
chars: ["B3","B2","B1", "こ","れ","は","テ","ス","ト","で","す","。"]
types: ["O", "O", "O", "I", "I", "I", "K", "K", "K", "I", "I", "P"]
ステップ 3: 終了センチネルの追加
chars: [..., "。", "E1", "E2", "E3"]
types: [..., "P", "O", "O", "O" ]
ステップ 4: 反復と予測
位置 i を 4 から len(chars) - 4 まで(両端を含む)繰り返します:
i=4 (れ): Extract features → predict → label=-1 (O) → word="これ"
i=5 (は): Extract features → predict → label=+1 (B) → push "これ", word="は"
i=6 (テ): Extract features → predict → label=+1 (B) → push "は", word="テ"
i=7 (ス): Extract features → predict → label=-1 (O) → word="テス"
i=8 (ト): Extract features → predict → label=-1 (O) → word="テスト"
i=9 (で): Extract features → predict → label=+1 (B) → push "テスト", word="で"
i=10(す): Extract features → predict → label=-1 (O) → word="です"
i=11(。): Extract features → predict → label=+1 (B) → push "です", word="。"
ステップ 5: 最後の単語をプッシュ
残りの単語 “。” を結果に追加します。
結果
["これ", "は", "テスト", "です", "。"]
予測の仕組み: 2 パス構成
segment() は特徴量文字列を一切構築しません。モデルのロード時に、セグメンタは
学習器の文字列キー付き重みを整数インデックスのテーブル群に一度だけコンパイルし
(後述)、各文を 2 つのパスでスコアリングします。38–42 個の特徴量のうち、
先行する境界判定に依存するのは 16 個だけであることを利用した構成です:
-
静的パス – タグに依存しないすべての特徴量を、文を 1 回走査するだけで 位置別スコアバッファに累積する:
- 各文字位置で
UWのマージ済み probe 1 回(文字コード →[UW1..UW6]の重み)とUCベクトルの直接ロード 1 回(文字種 ID →[UC1..UC6])を行い、6 個の値をそれぞれが寄与する近傍の判定位置へ scatter-add する - 隣接ペアごとに
BWのマージ済み probe 1 回とBCベクトルの直接ロード 1 回(各 3 値)、トリプルごとにTCベクトルの直接ロード 1 回(4 値) - 日本語/中国語では、文字ごとに 1 回のマージ済み
WC行 probe を行う (行は文字種 ID で直接インデックスされ、その文字が寄与する 2 つの 判定位置へ scatter-add される)。WC特徴を持たないモデルでは ブロック全体がスキップされる
- 各文字位置で
-
逐次パス – 各位置 i で、バイアス + 前計算済み静的スコアから始め、 タグ依存の 16 個の重み(
UP*,BP*,UQ*,BQ*,TQ*– すべて 直接インデックスの密配列ロード、ハッシュ計算なし)を加算して判定する:score >= 0なら新しい単語の開始、そうでなければ継続。判定結果は tags 配列にプッシュされ、後続位置の検索に影響する。score = bias + static[i] + sum(dense[タグ依存テンプレート][mixed-radix index])pointwise 高速パス(issue #183): モデルにタグ依存特徴が 1 つも 無い場合(
litsea extract --tag-freeで学習した場合。同梱のkorean.model/english.modelがこれに該当)、コンパイル済みモデルがロード時にこれを 記録し、逐次パスは丸ごとスキップされます – 判定は `bias + static[i]= 0
に帰着し、タグの簿記も行われません。出力はどちらの経路でも同一 です(スキップされるロードはそれぞれ0.0` を加算するだけのため)。 消えるのは位置間の直列依存だけです。
バイアスはキャッシュされたフィールド(-sum(model) / 2.0、重みを変更する
すべての経路で同期される)で、文ごとに 1 回だけ読み取られます。packed
コンテキストは u32 文字コード配列と u8 文字種 ID 配列に加えて文字ごとの
バイトオフセットを保持します(境界パイプラインはトークンを入力へのバイト
範囲として出力します。#184。segment() はそれを String として実体化し、
segment_into() は再利用可能なバッファからそのまま返します)。センチネル
エントリ(B3…E3)は Unicode スカラー範囲の直上のコードにマップされます。
コンパイル済みスコアリングテーブル
特徴テンプレートは宣言的なテーブル(packed_model::TEMPLATES)として一度だけ
定義され、そこから 3 つの消費者が導出されます: 学習・抽出で使用される
文字列ライター、モデルの各特徴量文字列を整数キーへ変換するロード時パーサ、
そして 2 パススコアラーのテーブル群です。コンパイルは
Segmenter::with_learner で先行して行われ、学習器が変更されると
(learner_mut() / add_corpus)無効化され、次の segment() 呼び出しで遅延
再構築されます。
コンパイル済みモデルは、キー空間のサイズとタグ依存性で使い分けられます:
- 文字 n-gram のマージ済みベクトルハッシュテーブル:
UW1..6は 「文字 →[f64; 6]」1 テーブルに、BW1..3は「文字ペア →[f64; 3]」 1 テーブルに統合され、ファミリー全体が probe 1 回で済みます。WC1..4も 同様に「文字 →[スロット][文字種 ID]」の行テーブル 1 つに統合されます (文字ごとに probe 1 回、文字種次元は直接インデックス) - タグ/文字種のみのテンプレートの密配列: 29 個それぞれが mixed-radix 積の
厳密サイズ(タグスロットは 3、文字種スロットは 7–10。日本語で合計約 74KB)の
直接インデックステーブルを持ちます。
UC/BC/TCのテーブルには、静的パス用の マージ済み scatter ベクトルビューが追加で導出されます
セグメンタの言語では生成し得ないモデル特徴量(例: 日本語セグメンタにおける 韓国語の文字種コード)はすべてのテーブルから除外されます。それらはスコアリング時に 到達不能なので、スコアには影響しません。
このテーブルとその3つの消費者がカバーするのは、文字レベル(stage-1・AdaBoost)の
特徴量セットのみです。二段構成の品詞タグ付けの
stage-2 単語タガーは、これとは別の並行する宣言的テーブル(litsea::word_features、
23個の単語レベルテンプレート)から、専用のランタイム
packed_two_stage::PackedTwoStageModel へコンパイルされます – テンプレート
カタログについては特徴量抽出を、
segment_with_pos へのはめ込み方については後述のセクションを参照してください。
出力の等価性
スコアの累積は 2 パス順で行われ、従来の文字列キー実装の累積順序とは異なるため、
f64 の合計はビット単位の同一性が保証されなくなりました。実測では出力差は
観測されていません: 完全一致の差分テスト(全同梱モデル・センチネル類似
ストレス文字列・実テキストコーパス)は無変更で通過しており、将来のモデルで
スコアが 0 近傍の knife-edge に乗った場合の検出網としてテストスイートに
残されています。
単語分割と品詞タグ付け(segment_with_pos)
segment_with_pos は
with_two_stage_learner
で構築された Segmenter を必要とします(issue #147)。上記の通常の
segment() の境界検出経路で分割し、その結果得られた各単語を
packed_two_stage::PackedTwoStageModel::tag_words でタグ付けします –
候補タグ語彙表の参照に加え、曖昧な既知単語にはマスク付き argmax、未知語には
全クラス argmax のフォールバックを使用します。したがって POS レイヤーが
文字単位の採点を追加することは一切ありません。このパイプラインの詳細と
コストモデルについては、二段構成タグ付けを
参照してください。
学習と予測の比較
| 観点 | 学習(process_corpus) | 予測(segment) |
|---|---|---|
| タグの情報源 | アノテーション済みコーパスから事前計算 | モデルにより動的に生成 |
| 最初のタグ | “U”(位置 3 の “B” を上書き) | “U”(先行する判定なし) |
| 最初の位置 | 境界パイプラインはスキップ、POS パイプラインは出力する(#100) | POS モードは最初の単語の品詞を予測するためにこの位置を予測する |
| ラベル | コーパスから既知(+1 または -1) | AdaBoost による予測 |
| 特徴量 | コールバックを通じてファイルに書き出し(文字列形式) | packed u64 キー、文字列なし |
学習時は、タグが正解のコーパス分割から導出されるため、モデルは正しい境界判定から学習します。予測時は、タグがその場で生成されるため、各判定は過去のすべての予測に依存します – これは左から右への貪欲法(Left-to-right Greedy)アプローチです。
パフォーマンス特性
分割アルゴリズムは入力長に対して線形です:
- 各文字位置を 1 回ずつ訪問: O(n)
- 各位置での特徴量抽出: O(1)(固定数のテンプレートを、それぞれスタック上の
u64にパック) - 各位置での予測: O(f)、f はアクティブな特徴量の数(約 38-42)。ただしファミリー単位でマージされており、静的パスは 1 文字あたり probe 約 2 回(
UW,BW)+ 数回のベクトル直接ロード、逐次パスは 16 回の密配列直接ロード - 合計: O(n * f)、実質的に O(n)
- アロケーションの特性: packed コンテキストは単語スライスを入力から借用し、フラットな
u32/u8配列を保持する。バイアスはキャッシュされ、ホットループ内では文字列を一切構築しない(packed テーブル自体はモデルロードごとに 1 回、ホットパス外でコンパイルされる)