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

予測パイプライン

本章では、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. 静的パス – タグに依存しないすべての特徴量を、文を 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 特徴を持たないモデルでは ブロック全体がスキップされる
  2. 逐次パス – 各位置 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() は再利用可能なバッファからそのまま返します)。センチネル エントリ(B3E3)は 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_poswith_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 回、ホットパス外でコンパイルされる)