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

はじめに

Litsea は、TinySegmenter および TinySegmenterMaker に触発されて開発された、Rust で実装された極めてコンパクトな単語分割ライブラリです。

MeCabLindera などの従来の形態素解析器とは異なり、Litsea は大規模な辞書に依存しません。代わりに、AdaBoost 二値分類アルゴリズムに基づくコンパクトな学習済みモデルを使用して単語分割を行います。また、二段構成(two-stage)アーキテクチャにより、Universal POS (UPOS) タグセットを用いた**単語分割と品詞推定(POS Tagging)**もサポートしています。

主な特徴

  • 高速かつ安全な Rust 実装 – Rust の安全性保証とパフォーマンスを活用
  • コンパクトな学習済みモデル – レガシーな RWCP.model / JEITA_Genpaku_ChaSen_IPAdic.model はキロバイト級。品質を最適化した japanese/chinese/korean/english.model は約 86 KB〜2.0 MB で、アプリケーションへの直接埋め込みや HTTP 経由の配信に十分な小ささ
  • 辞書不要 – 統計モデルのみで分割を実行
  • 二段構成の品詞推定 – 二値境界分類器での分割と、候補タグ語彙表 + 単語単位タガーによる各単語のタグ付けを組み合わせ、通常の単語分割にわずかなコストしか追加しない
  • 多言語対応 – 日本語、中国語(簡体字/繁体字)、韓国語、英語
  • モデル学習機能 – AdaBoost または Averaged Perceptron を使用して独自のコーパスからカスタムモデルを学習可能
  • リモートモデル読み込み – HTTP/HTTPS URL(オプトインの remote_model フィーチャー)またはローカルファイルからモデルを読み込み
  • シンプルで拡張性の高い API – Rust プロジェクトへのライブラリとしての統合が容易

仕組み

Litsea は単語分割を二値分類問題として扱います。文中の各文字位置について、モデルがその位置が単語境界(+1)か非境界(-1)かを予測します。分類器は、各言語固有の文字 n-gram 特徴量と文字種情報を使用します。

Input:  "これはテストです。"
         こ れ は テ ス ト で す 。
         B  O  B  B  O  O  B  O  B   ← word-start predictions (RWCP.model)
Output: ["これ", "は", "テスト", "です", "。"]

品詞推定(POS Tagging)

Litsea は単語分割に加えて、品詞推定(Part-of-Speech Tagging)もサポートしています。二段構成アーキテクチャにより、まず二値境界分類器で文を分割し、次に候補タグ語彙表 + 単語単位タガーで各単語にタグを付与します。

各文字位置に対して、18 クラスの SegmentLabel を予測します:

  • B-NOUN, B-VERB, …, B-X(17 品詞の境界ラベル)
  • O(非境界 = 単語の継続)

品詞タグには Universal DependenciesUPOS タグセット(17 品詞)を採用しています。

Input:  "今日はいい天気ですね。"
Output: 今日/NOUN は/ADP いい/ADJ 天気/NOUN です/AUX ね/PART 。/PUNCT

名前の由来

クスノキ科には Lindera(クロモジ)と同じ科に属する Litsea cubeba(アオモジ)という小さな植物があります。これが Litsea という名前の由来です。

現在のバージョン

Litsea v0.13.0 – Rust Edition 2024、最低 Rust バージョン 1.87。

リンク

導入

Litsea へようこそ! このセクションでは、すぐに使い始められるようガイドします。

Litsea は、単語分割(AdaBoost)と二段構成の品詞推定(二値境界分類器 + 単語単位タガー)をサポートする Rust 製コンパクト単語分割ライブラリです。

次のステップ

インストール

前提条件

  • Rust 1.87 以降(stable チャンネル)– rust-lang.org から入手
  • Cargo(Rust のパッケージマネージャ、Rust に同梱)

CLI ツールのインストール

crates.io から

cargo install litsea-cli

ソースから

git clone https://github.com/mosuka/litsea.git
cd litsea
cargo build --release

バイナリは ./target/release/litsea に生成されます。

インストールの確認:

./target/release/litsea --help

ライブラリとしての利用

プロジェクトの Cargo.toml に Litsea を追加します:

[dependencies]
litsea = "0.13.0"

http(s) 経由のリモートモデル読み込みはオプトイン(opt-in)です。必要な場合は remote_model フィーチャーを有効にしてください:

litsea = { version = "0.13.0", features = ["remote_model"] }

注意: ローカルファイルからのモデル読み込み(load_model_from_path)は同期処理のため、非同期ランタイムは不要です。非同期ランタイム(tokio など)が必要になるのは、非同期の load_model メソッドを使って HTTP/HTTPS 経由でモデルを読み込む場合のみです(オプトインの remote_model フィーチャーを有効にした場合にのみ利用できます)。

サポートプラットフォーム

Litsea は以下のプラットフォームでテストされています:

OSArchitecture
Linuxx86_64, aarch64
macOSx86_64 (Intel), aarch64 (Apple Silicon)
Windowsx86_64, aarch64

クイックスタート

CLI クイックスタート

テキストの分割

Litsea には models/ ディレクトリに学習済みモデルが同梱されています。テキストを segment コマンドにパイプで渡します:

日本語(同梱の RWCP.model、オリジナルの TinySegmenter モデルを使用):

echo "LitseaはTinySegmenterを参考に開発された、Rustで実装された極めてコンパクトな単語分割ソフトウェアです。" \
  | litsea segment -l japanese ./models/RWCP.model

出力:

Litsea は TinySegmenter を 参考 に 開発 さ れ た 、Rust で 実装 さ れ た 極めて コンパクト な 単語 分割 ソフトウェア です 。

中国語:

echo "中文分词测试。" | litsea segment -l chinese ./models/chinese.model

韓国語:

echo "한국어 단어 분할 테스트입니다." | litsea segment -l korean ./models/korean.model

英語:

echo "I don't know." | litsea segment -l english ./models/english.model

品詞推定付き分割

Litsea は二段構成モデルを使って、単語分割と品詞推定を行うことができます。segment コマンドに --pos フラグを追加します:

echo "今日はいい天気ですね。" \
  | litsea segment --pos -l japanese ./models/japanese_pos.model

出力:

今日/NOUN は/ADP いい/ADJ 天気/NOUN です/AUX ね/PART 。/PUNCT

各トークンには Universal POS(UPOS) タグが付与されます。

ライブラリ クイックスタート

モデルを読み込みテキストを分割する最小限の Rust プログラムです:

use std::path::Path;

use litsea::adaboost::AdaBoost;
use litsea::language::Language;
use litsea::segmenter::Segmenter;

fn main() -> litsea::Result<()> {
    // Load the pre-trained model
    let mut learner = AdaBoost::new(0.01, 100);
    learner.load_model_from_path(Path::new("./models/RWCP.model"))?;

    // Create a segmenter
    let segmenter = Segmenter::with_learner(Language::Japanese, learner);

    // Segment text
    let tokens = segmenter.segment("これはテストです。");
    println!("{}", tokens.join(" "));
    // Output: これ は テスト です 。

    Ok(())
}

ライブラリでの品詞推定付き分割

POS モデルを読み込み、品詞タグ付きでテキストを分割する最小限の Rust プログラムです:

use std::path::Path;

use litsea::language::Language;
use litsea::segmenter::Segmenter;
use litsea::two_stage::TwoStageLearner;

fn main() -> litsea::Result<()> {
    // Load the pre-trained two-stage POS model
    let mut learner = TwoStageLearner::new();
    learner.load_model_from_path(Path::new("./models/japanese_pos.model"))?;

    // Create a segmenter with POS support
    let segmenter = Segmenter::with_two_stage_learner(Language::Japanese, learner);

    // Segment text with POS tags
    let tokens = segmenter.segment_with_pos("今日はいい天気ですね。")?;
    for (word, pos) in &tokens {
        print!("{}/{} ", word, pos);
    }
    // Output: 今日/NOUN は/ADP いい/ADJ 天気/NOUN です/AUX ね/PART 。/PUNCT

    Ok(())
}

次のステップ

アーキテクチャ概要

Litsea は、コンパクトで辞書不要の単語分割システムとして設計されています。単語分割を二値分類問題として扱い、文字レベルの特徴量から単語境界パターンを学習するために AdaBoost を使用します。

高レベルデータフロー

Litsea には学習分割の 2 つの主要なワークフローがあります。

学習パイプライン

flowchart LR
    A["Corpus (text)"] --> B["Extractor"]
    B --> C["Features File (.txt)"]
    C --> D["Trainer (AdaBoost)"]
    D --> E["Model File (.model)"]
  1. コーパスの準備 – 単語をスペースで区切ったテキストを準備
  2. 特徴量抽出Extractor がコーパスを読み込み、文字を種別に分類し、ラベル付き特徴量ベクトルを出力
  3. モデル学習Trainer が特徴量を AdaBoost に入力し、最も情報量の多い特徴量を反復的に選択してコンパクトなモデルを生成

分割パイプライン

flowchart LR
    F["Raw text"] --> G["Segmenter (AdaBoost)"]
    H["Model file"] --> G
    G --> I["Segmented words"]
  1. モデル読み込み – 学習済みモデルを読み込み(ファイルまたは URL から)
  2. 文字分類 – 入力の各文字について、言語固有のパターンに基づいて文字種コードを決定
  3. 特徴量抽出 – スライディングウィンドウを使用し、再利用するバッファを通して各文字位置の文字 n-gram 特徴量をストリーム処理
  4. 予測 – AdaBoost が各位置が単語境界かどうかを予測

設計原則

  • 辞書不要 – MeCab や Lindera とは異なり、Litsea は文字パターンから学習した統計モデルのみに依存
  • コンパクトなモデル – レガシーな単語分割モデル(RWCP.modelJEITA_Genpaku_ChaSen_IPAdic.model)は約 16-22 KB。再学習された japanese/chinese/korean/english.model は約 86 KB〜2.0 MB で、事前学習済みモデルに記載の品質向上とのトレードオフ。二段構成の品詞モデルは約 3.6-8 MB – いずれもアプリケーションに直接埋め込める程度に小さく、重要な特徴量の重みのみを含む
  • 言語非依存のフレームワーク – コアアルゴリズムはすべての言語で共通であり、文字種パターンのみが異なる
  • 簡単な拡張性 – 新しい言語の追加には、文字種パターンの定義とモデルの学習のみが必要

モジュール設計

litsea ライブラリクレートは、それぞれ明確な責務を持つモジュールで構成されています。

モジュール依存関係グラフ

graph TD
    language["language.rs<br/>文字種分類"]
    segmenter["segmenter.rs<br/>分割 + 品詞付与"]
    adaboost["adaboost.rs<br/>AdaBoost(境界判定)"]
    perceptron["perceptron.rs<br/>Averaged Perceptron(品詞)"]
    upos["upos.rs<br/>UPOSタグとラベル"]
    extractor["extractor.rs<br/>特徴量抽出"]
    trainer["trainer.rs<br/>学習オーケストレーション"]
    two_stage["two_stage.rs<br/>二段構成モデルのコンテナ"]
    word_features["word_features.rs(非公開)<br/>stage-2 単語特徴テンプレート"]
    packed_model["packed_model.rs(非公開)<br/>特徴テンプレート + packed AdaBoost テーブル"]
    packed_two_stage["packed_two_stage.rs(非公開)<br/>packed 二段構成タグ付けテーブル"]
    model_io["model_io.rs(非公開)<br/>モデルURI読み込み"]
    error["error.rs<br/>LitseaError / Result"]
    metrics["metrics.rs<br/>評価指標(in-sample)"]
    evaluation["evaluation.rs<br/>held-out 品質指標"]

    language --> segmenter
    upos --> segmenter
    adaboost --> segmenter
    perceptron --> segmenter
    packed_model --> segmenter
    packed_two_stage --> segmenter
    two_stage --> segmenter
    segmenter --> extractor
    two_stage --> extractor
    word_features --> extractor
    evaluation --> extractor
    adaboost --> trainer
    perceptron --> trainer
    two_stage --> trainer
    adaboost --> two_stage
    perceptron --> two_stage
    upos --> two_stage
    language --> word_features
    word_features --> packed_two_stage
    language --> packed_two_stage
    perceptron --> packed_two_stage
    upos --> packed_two_stage
    model_io --> adaboost
    model_io --> perceptron
    error --> adaboost
    error --> perceptron
    metrics --> trainer
    segmenter --> evaluation
    upos --> evaluation

モジュール詳細

language.rs – 言語定義

Language enum と文字種分類を定義します。

  • LanguageJapaneseChineseKoreanEnglish のバリアントを持つ enum
    • FromStr を実装("japanese""ja""chinese""zh""korean""ko""english""en" をパース)
    • Display を実装(小文字名を出力)
    • char_type(c: char) -> &'static str – 非公開の char_type_id() が返す数値の type id に対するテーブル参照として文字を分類します。char_type_id() は言語別関数(japanese_char_type_id など)にディスパッチし、各関数は文字範囲に対する直接の match として実装されています(アロケーションなし・正規表現不使用)。言語別関数は、共通の "P"/"A"/"N" クラス用の punct_latin_digit() ヘルパーを共有します。

segmenter.rs – 単語分割と品詞付与

主要なユーザー向けモジュールです。

  • SegmenterLanguageAdaBoost 学習器を保持(フィールドは非公開。language()learner()learner_mut() を使用)。加えて、segment() が使うコンパイル済みスコアリングテーブルの内部キャッシュ(packed)と、オプションのコンパイル済み二段構成タグ付けモデル(with_two_stage_learner で設定。segment_with_pos() を支える。キャッシュとは異なり stage-2 モデルそのものであり、生の学習器パーツはコンパイル後に破棄される)も保持する
    • new(language) – デフォルト(空)の AdaBoost 学習器付きでセグメンターを作成
    • with_learner(language, learner) – 設定済みの AdaBoost 学習器(例: 学習済みモデルを読み込んだもの)付きでセグメンターを作成
    • with_two_stage_learner(language, learner)TwoStageLearner から二段構成の分割+品詞付与用セグメンターを作成
    • segment(sentence) – テキストを単語に分割し Vec<String> を返す
    • segment_into(sentence, buf) – アロケーションフリー版(#184): 再利用可能な SegmentBuffer から借用したトークンのバイト範囲を返す
    • segment_with_pos(sentence) – 分割と品詞付与を行い Result<Vec<(String, Upos)>> を返す(二段構成学習器が未設定の場合は PosLearnerNotSet
    • char_type(ch) – 1文字を種別コードに分類
    • add_corpus(corpus) / add_corpus_tsv(corpus) – 学習データを追加(それぞれ空白区切り・タブ区切り/空白保持。後者は韓国語と英語で使用、issue #152 を参照)
    • add_corpus_with_writer(corpus, callback) / add_corpus_with_pos_writer(corpus, callback) / add_corpus_tsv_with_writer(corpus, callback) – カスタムコールバックでコーパスを処理(POS writer 版は二段構成の stage-1 特徴量抽出が使用)

adaboost.rs – AdaBoost アルゴリズム

単語境界の判定に使う二値分類器です。

  • AdaBoost
    • new(threshold, num_iterations) – 学習パラメータを指定して作成
    • initialize_features(path) / initialize_instances(path) – 学習データを読み込み
    • train(running) – AdaBoost の学習ループを実行
    • predict(&attributes) – 境界(+1)か非境界(-1)かを予測
    • load_model(uri)(async)/ load_model_from_path(path) / load_model_from_reader(reader) – モデルの読み込み
    • save_model(path) – モデルをファイルに保存
    • metrics() – 正解率・適合率・再現率を計算(BinaryMetrics
    • bias() – モデルのバイアス項を取得

perceptron.rs – Averaged Perceptron

二段構成モデルの学習(両ステージ)と、同梱分割モデルの畳み込み(collapse)レシピを支える多クラス分類器です。

  • AveragedPerceptron
    • add_instance(features, label) – 学習インスタンスを追加
    • train(num_epochs, running) – 重み平均化付きで学習(running: &AtomicBool
    • predict(&features) – 最良クラスのラベルを予測
    • load_model(uri)(async)/ load_model_from_path(path) / load_model_from_reader(reader) – モデルの読み込み
    • save_model(path) – モデルを保存
    • metrics() – マクロ平均の評価指標(MulticlassMetrics
  • 重みは高速な推論のため「特徴 → クラス別ベクトル」レイアウトで保持します。

upos.rs – Universal POS タグ

  • Upos – Universal Dependencies の17品詞タグ(NOUNVERB、…)
  • SegmentLabel – 文字位置ごとの分割+品詞の複合ラベル(B(Upos) または O)。"B-NOUN" / "O" 文字列形式の Display/FromStr を実装

extractor.rs – 特徴量抽出

モデル学習用にコーパスから特徴量を抽出します。

  • ExtractorSegmenter をラップしてコーパスファイルを処理
    • new(language) – 言語を指定して作成
    • extract(corpus_path, features_path) – コーパスを読み、特徴量ファイルを書き出す
    • extract_tsv(corpus_path, features_path) – タブ区切り・空白保持コーパス版(issue #152、韓国語と英語で使用)
    • extract_two_stage(corpus_path, output_prefix, feature_set) – 品詞付きコーパスから二段構成の学習特徴量(issue #147)を抽出: {output_prefix}.stage1(境界特徴量)、.stage2(単語レベル特徴量)、.lexicon を書き出す

trainer.rs – 学習オーケストレーション

高レベルの学習ワークフローです。

  • Trainer – 分割モデルの学習(AdaBoost)
    • new(threshold, num_iterations, features_path) – 特徴量ファイルから初期化
    • load_model(uri) – 増分学習用に既存モデルを読み込み(async・任意)
    • train(running, model_path) – 学習・保存して BinaryMetrics を返す
  • PerceptronTrainer – 不透明な文字列ラベルに対する汎用の Averaged Perceptron 学習(同梱分割モデルの畳み込みレシピの学習ステップ)
    • new(num_epochs, features_path) / load_model(uri) / train(running, model_path)MulticlassMetrics を返す)
  • TwoStageTrainer – 二段構成モデルの学習(issue #147): Extractor::extract_two_stage が書き出したファイルから stage-1 境界分類器(AveragedPerceptron)と stage-2 単語タガーを学習し、stage-1 を AdaBoost 形式へ畳み込んでから TwoStageLearner を組み立てる
    • new(num_epochs, dominance, features_prefix) / train(running, model_path)TwoStageMetrics を返す。完全な API はTrainerを参照)
  • TwoStageMetricsTwoStageTrainer::train 実行のステージごとの MulticlassMetricsstage1stage2

two_stage.rs – 二段構成モデルのコンテナ

litsea-two-stage v1 ファイル形式(モデルファイル形式を参照)と、二段構成モデルをメモリ上に保持する型を定義します(issue #147)。

  • TwoStageLearner – stage-1 境界 AdaBoost モデル、stage-2 AveragedPerceptron 単語タガー、候補タグ語彙表(lexicon)をまとめる。new() / from_parts(...) / load_model_from_path(path) / save_model(path) は単一学習器の各型と同様の API
  • TwoStageFeatureSet – stage-2 の単語レベルテンプレート部分集合を選択する enum(FastBalancedFull
  • ModelKind – モデルファイルの形式をその 1 行目から検出する(AdaBoost、スタンドアロンの AveragedPerceptronTwoStage)。種類違いのファイルに正確なローダーエラーを返すために使用される

error.rs – エラー処理

  • LitseaError – エラー enum(IoInvalidDataInvalidInputUnsupportedPosLearnerNotSetremote_model フィーチャー時は Download も)。#[non_exhaustive] が付与されているため、外部の match 式にはワイルドカードアームが必要です
  • Result<T> – すべての失敗しうるAPIが使うエイリアス

metrics.rs – 評価指標

  • BinaryMetrics – 正解率・適合率・再現率・混同行列(AdaBoost)
  • MulticlassMetrics – 正解率とマクロ平均適合率/再現率(Averaged Perceptron)

evaluation.rs – held-out 評価指標

metrics.rs が in-sample 品質(train が出力する、学習データそのもので測った指標)を報告するのに対し、このモジュールは held-out 品質を計算します: 文字オフセットのスパンを使って Segmenter の出力を gold コーパスと比較するため、文中の他の箇所のトークン化の違いに関わらず、予測トークンと gold トークンを正確に対応付けられます。

  • SegmentationMetrics – 単語分割の単語/境界の適合率・再現率・F1。evaluate_segmentation(segmenter, gold) が生成
  • PosMetricsSegmentationMetrics に加え、タグ付き単語の適合率・再現率・F1 を保持。evaluate_pos(segmenter, gold) が生成(失敗しうる: segment_with_pos のエラーを伝播)
  • parse_gold_line(line, tsv) / parse_gold_pos_line(line) – gold コーパスの行をトークン列にパース(プレーンまたは品詞付き)。二段構成の extractor・trainer からも使用される
  • CLI の litsea evaluate サブコマンドを支える

packed_model.rs – 特徴テンプレートと packed AdaBoost テーブル(非公開)

宣言的な特徴テンプレートテーブル(TEMPLATES。全特徴量コンシューマの単一の真実の源)、モデルの特徴量文字列を packed 整数キーへ変換するロード時パーサ、そして segment() の 2 パススコアラーが読むマージ/密テーブルへ AdaBoost の重みをコンパイルした PackedModel を保持する内部モジュールです。公開APIには含まれません。

packed_two_stage.rs – packed 二段構成タグ付けテーブル(非公開)

TwoStageLearner の stage-2 タガーと語彙表を、segment_with_pos() が読む密/疎スコアリングテーブルへコンパイルする内部モジュールです。packed_model.rs が AdaBoost 学習器に対して行っていることの二段構成版に相当します: 語彙表とドミナンススキップ対象タグをカバーするサーフェスマップ、word_features.rs 由来の文字を含む単語特徴テンプレート用のクラス別スパース行、文字種・単語長テンプレート用の密テーブル、から構成されます。公開APIには含まれません。

word_features.rs – stage-2 単語特徴テンプレート(非公開)

二段構成タガーの stage-2 が使う単語レベルの特徴テンプレート(表層、単語長、先頭/末尾文字とその文字種、文脈文字/文字種/バイグラムなど)を定義する内部モジュールです。テンプレート集合の単一の真実の源であり、学習用の extractor(extract_two_stage 経由)が write_word_features で特徴量文字列を書き出し、packed_two_stage.rsparse_word_feature で同じ文字列を整数キーへ逆変換します。両者はラウンドトリップテストで整合性が固定されています。公開APIには含まれません。

model_io.rs – モデル読み込みI/O(非公開)

モデルURI(プレーンパス、file://remote_model フィーチャー時の http(s)://)を解決して生のモデルバイト列を返す内部モジュールです。公開APIには含まれません。

公開エクスポート

ライブラリの lib.rs は公開モジュールと主要型の再エクスポートを提供します:

#![allow(unused)]
fn main() {
pub mod adaboost;
pub mod error;
pub mod evaluation;
pub mod extractor;
pub mod language;
pub mod metrics;
mod model_io;
mod packed_model;
mod packed_two_stage;
pub mod perceptron;
pub mod segmenter;
pub mod trainer;
pub mod two_stage;
pub mod upos;
mod word_features;

pub use adaboost::AdaBoost;
pub use error::{LitseaError, Result};
pub use evaluation::{PosMetrics, SegmentationMetrics};
pub use extractor::Extractor;
pub use language::{Language, ParseLanguageError};
pub use metrics::{BinaryMetrics, MulticlassMetrics};
pub use perceptron::AveragedPerceptron;
pub use segmenter::{SegmentBuffer, Segmenter};
pub use trainer::{PerceptronTrainer, Trainer, TwoStageMetrics, TwoStageTrainer};
pub use two_stage::{
    ModelKind, ParseTwoStageFeatureSetError, TwoStageFeatureSet, TwoStageLearner,
};
pub use upos::{ParseSegmentLabelError, ParseUposError, SegmentLabel, Upos};

pub fn version() -> &'static str { ... }
}

AdaBoost 二値分類

Litsea は、単語境界を判定するために AdaBoost(Adaptive Boosting)アルゴリズムによる二値分類を使用します。本章では、Litsea に実装されているアルゴリズムについて説明します。

概要

AdaBoost は、多数の弱学習器(単純な分類器)を組み合わせて強力なアンサンブル分類器を構築します。Litsea では:

  • 正ラベル(+1) = 単語境界
  • 負ラベル(-1) = 非境界(現在の単語の継続)
  • 弱学習器 = 個々の特徴量(各特徴量は二値の「切り株」– 存在するか否か)

学習アルゴリズム

AdaBoost::train() の学習ループは以下のように動作します:

初期化

  1. 学習ファイルから特徴量とインスタンスを読み込み
  2. インスタンスの重みを均一に初期化(後に初期スコアに基づいて調整)
  3. すべてのモデルの重みをゼロで初期化

反復ブースティング

各イテレーション t(最大 num_iterations 回)について:

ステップ 1: 重み付き誤差の計算

各特徴量 h について、全インスタンスに対する重み付き誤差を計算します:

error[h] -= D[i] * y[i]   (for each instance i that has feature h)

ここで D[i] はインスタンスの重み、y[i] は真のラベルです。

ステップ 2: 最良の弱学習器の選択

重み付き誤差率が最も低い特徴量を選択します:

error_rate(h) = (error[h] + positive_weight_sum) / instance_weight_sum
h_best = argmax_h |0.5 - error_rate(h)|

基準となる競合対象は「全て負」分類器(常に -1 を予測)であり、その誤差率は正のインスタンスの割合に等しくなります。実際の特徴量はこの基準を上回る必要があります。

ステップ 3: 収束判定

|0.5 - best_error_rate| < threshold の場合、早期停止します – どの特徴量もモデルを大幅に改善できないためです。

ステップ 4: 弱学習器の重みの計算

alpha = 0.5 * ln((1 - error_rate) / error_rate)
model[h_best] += alpha

誤差率が低いほど alpha が高くなり、より良い特徴量により大きな影響力を与えます。

ステップ 5: インスタンスの重みの更新

For each instance i:
    prediction = +1 if h_best in features(i), else -1

    if y[i] * prediction < 0:  (misclassified)
        D[i] *= exp(alpha)     (increase weight)
    else:                       (correctly classified)
        D[i] /= exp(alpha)     (decrease weight)

Normalize: D[i] /= sum(D)

これにより、後続のイテレーションは分類が困難なインスタンスに集中するようになります。

予測

入力された特徴量(属性)のセットに対して、予測は以下のように行われます:

score = bias + sum(model[feature] for each feature in attributes)
prediction = +1 if score >= 0, else -1

バイアス項

バイアスは以下のように計算されます:

bias = -sum(all model weights) / 2.0

値は学習器内にキャッシュされ、重みを変更するすべての経路で同期が保たれるため、推論時の読み取りは O(1) です。

これにより決定境界が中心化されます。空文字列特徴量("")は学習中のバイアスバケットとして機能します。 この特徴量はすべての構築経路(new()、特徴量ファイルからの初期化、モデルの読み込み)で特徴量インデックス 0 に登録されるため、 add_instance() を通じて学習されたモデルも正しく学習・保存・再読み込みできます。

セグメンタにおけるコンパイル済みスコアリング

predict() 自体は文字列で特徴量を検索し、そのセマンティクスは変わっていません。 ただし Segmenter はホットパスでこれを呼び出しません: モデルの(再)ロード後、 学習器の (特徴量, 重み) ペアを整数インデックスのテーブル群へコンパイルし、 各文を 2 パスでスコアリングします (予測パイプライン を参照)。コンパイル済みテーブルはモデルを正確にミラーします。異なるのは 浮動小数点の累積順序のみであり、差分テストスイートが分割出力の実質的な同一性を 固定しています。

モデルファイル形式

学習されたモデルはシンプルなテキストファイルとして保存されます:

feature1\tweight1
feature2\tweight2
...
bias_value
  • 各行には特徴量名とその重み(タブ区切り)が含まれる
  • 重みがゼロの特徴量は省略される
  • 最終行にはバイアス項(単一の数値)が含まれる

不正な形式のファイル(空、バイアス行より前で切り詰められている、バイアス行や特徴量が重複している、 有限でない重みを含む、など)は読み込み時に拒否されます。詳細は モデルファイル形式を参照してください。

本ページで説明しているのは AdaBoost の学習アルゴリズムです。 上記のファイル形式は、同梱されている4つの分割モデル (japanese.modelchinese.modelkorean.modelenglish.model)の保存にも使われていますが、issue #165 以降、これらの モデルはブースティングでは生成されていません。2 クラスの Averaged Perceptron として学習し、同じスカラー重み形式へ 無損失で畳み込んだものです(畳み込み手順は事前学習済みモデルを参照)。 AdaBoost::load_model が読み込めるファイルはすべてこの形式の正当なインスタンスですが、それは上記のブースティング ループによって実際に生成されたことを意味するわけではありません。

ハイパーパラメータ

ParameterDefault説明
threshold0.01早期停止の閾値。低い値はより多くのイテレーションを許可し、精度が向上する可能性がある
num_iterations100ブースティングの最大ラウンド数。高い値は学習時間とモデルサイズを犠牲にして精度を向上させる可能性がある

Averaged Perceptron

Litsea は、二段構成品詞推定アーキテクチャの両ステージと、同梱分割モデルの畳み込み(collapse)レシピの学習側の学習器として、Averaged Perceptron アルゴリズムによる多クラス分類を使用します。本章では、Litsea に実装されているアルゴリズムについて説明します。

概要

AdaBoost二値分類(境界 / 非境界)を行うのに対し、Averaged Perceptron は 18 クラスの多クラス分類を行い、各文字位置に対してセグメントラベルを予測します:

  • 17 個の境界ラベル: B-ADJ, B-ADP, B-ADV, B-AUX, B-CCONJ, B-DET, B-INTJ, B-NOUN, B-NUM, B-PART, B-PRON, B-PROPN, B-PUNCT, B-SCONJ, B-SYM, B-VERB, B-X
  • 1 個の非境界ラベル: O(単語の継続)

これらのラベルは Universal Dependencies プロジェクトの 17 個の Universal POS (UPOS) タグに対応し、B- プレフィックスで単語境界を示します。これにより、単語境界の検出と品詞の推定を 1 つの分類ステップで同時に行えます。

Averaged Perceptron を 2 クラスモード(B/O ラベルのみ)で使うと、実際には同梱の japanese.modelchinese.modelkorean.modelenglish.model の各分割モデルを学習しています。学習後、推論用に AdaBoost モデル形式へ無損失で畳み込まれます — 完全な導出は事前学習済みモデル: 学習手順を参照してください。同じ畳み込みが二段構成アーキテクチャの stage-1 境界分類器の学習にも使われ、多クラス形式は stage-2 単語タガーのアルゴリズムです。

アルゴリズム

重み表現

パーセプトロンは特徴量ごとにクラス別の重みベクトルを保持し、単一の疎なマップに格納します。これにより、スコア計算と重みの更新は 特徴量ごとに 1 回のハッシュ検索で済みます(特徴量 × クラスごとに 1 回ではありません):

slots: FxHashMap<Feature, FeatureSlot>
// FeatureSlot { w: Vec<f64>, acc: Vec<f64>, ts: Vec<usize> } -- one entry per class

例:

weights["UW4:猫"]["B-NOUN"] = 2.5
weights["UC4:H"]["B-NOUN"]  = 1.8
weights["UW4:猫"]["O"]      = -0.3
...

ある特徴量集合に対し、各クラスのスコアはその特徴量の重みの合計です:

score(class) = sum(weights[feature][class] for each feature in input)
prediction = argmax(score(class) for all classes)

更新規則

各学習インスタンスについて、予測が正解と異なる場合に重みを更新します:

For each training instance (features, truth):
    guess = predict(features)

    if guess != truth:
        For each feature f in features:
            weights[f][truth] += 1.0   # increase weight for correct class
            weights[f][guess] -= 1.0   # decrease weight for predicted class

この単純な更新規則により、正解クラスの特徴量が強化され、誤予測クラスの特徴量が弱められます。これにより、将来の類似入力に対して正しい予測がより起こりやすくなります。

平均化

基本的なパーセプトロンに対する重要な改善点が重みの平均化です。最終的な重みは学習データの末尾に過適合する傾向があるため、学習中に観測されたすべての重みベクトルの平均を最終モデルとして使用します。これにより、未知データへの汎化性能が向上します。

実装では効率のために累積和アプローチを使用します:

cumulative[feature][class] += weights[feature][class] * elapsed_steps

At the end of training:
    averaged[feature][class] = cumulative[feature][class] / total_steps

これにより、すべての中間重みベクトルを保存することなく同じ結果が得られます。この平均化により、学習データの順序への依存が軽減され、汎化性能が向上します。

累積和とタイムスタンプのベクトルは、学習がその特徴量に初めて触れた時点で遅延生成されます。そのため、推論専用に読み込まれたモデルは 現在の重みのみを保持し、平均化用の状態には一切コストがかかりません。

エポックによる学習

学習は指定されたエポック数だけ学習データを繰り返します。各エポックでは、すべての学習インスタンスを順に処理します:

For each epoch (1 to num_epochs):
    For each instance in training data:
        features = extract_features(instance)
        predicted = argmax(score(class) for all classes)
        if predicted != correct_label:
            update weights
        accumulate weights for averaging

AtomicBool フラグにより、Ctrl+C などで学習を中断し、その時点でのモデルを保存することも可能です。

#![allow(unused)]
fn main() {
use std::sync::atomic::AtomicBool;
use litsea::perceptron::AveragedPerceptron;

let mut perceptron = AveragedPerceptron::new();
// ... add instances ...
let running = AtomicBool::new(true);
perceptron.train(10, &running);  // 10 epochs
}

モデルファイル形式

Averaged Perceptron のモデルは、以下の構造を持つテキストファイルとして保存されます:

18
O
B-ADJ
B-ADP
...
B-X
feature1\tclass1\tweight1
feature2\tclass2\tweight2
...
  • 1行目: クラス数(18)
  • 2 行目から N+1 行目: クラス名(1行に1つ)
  • 残りの行: 特徴量の重み、タブ区切り feature\tclass\tweight
  • 重みがゼロのエントリは省略される
  • 重みの行は特徴量のソート順で書き出されるため、同じモデルを保存すると常にバイト単位で同一のファイルになる。 読み込み時は順序に依存しない

AdaBoost との比較

項目AdaBoostAveraged Perceptron
分類方式二値分類(+1 / -1)多クラス分類(18クラス)
出力単語境界のみ単語境界 + 品詞タグ
弱学習器特徴量の決定株なし(線形分類器)
重みの管理特徴量ごとに1つの重みクラス×特徴量の重み行列
汎化手法アンサンブル重みの平均化
学習方式サンプル再重み付けによる反復ブースティング重み平均化によるオンライン学習
モデルサイズ約 86 KB〜2.0 MB(再学習済み japanese/chinese/korean/english.model)/約 16〜22 KB(レガシー RWCP/JEITA)約 3.6-8 MB(二段構成モデル)
ハイパーパラメータthreshold, num_iterationsnum_epochs

ハイパーパラメータ

パラメータデフォルト値説明
num_epochs10学習エポック数。高い値は精度を向上させる可能性があるが、過学習のリスクがある

特徴量抽出

Litsea は、各単語境界候補の周辺の局所的なコンテキストを捉えるために文字 n-gram 特徴量を使用します。本章では、AdaBoost・二段構成 stage-1 パイプラインが共有する文字レベルの境界テンプレートと、二段構成の stage-2 タガーが使用する単語レベルのテンプレートの両方を含む、すべての特徴量タイプをカタログ化します。

特徴量カテゴリ

入力の各文字位置 i について、セグメンタは文字、その種別コード、および前回の境界判定からなるスライディングウィンドウから特徴量を抽出します。

基本特徴量(38 個)

CategoryIDs説明Window
UW (Unary Word)UW1–UW6位置 i-3 から i+2 の個々の文字6
BW (Bigram Word)BW1–BW3隣接する文字ペア3
UC (Unary Char-type)UC1–UC6位置 i-3 から i+2 の文字種コード6
BC (Bigram Char-type)BC1–BC3隣接する種別コードペア3
TC (Trigram Char-type)TC1–TC4種別コードのトリプル4
UP (Unary Previous-tag)UP1–UP3直前 3 つの境界判定3
BP (Bigram Previous-tag)BP1–BP2境界判定のペア2
UQ (Unary tag+type)UQ1–UQ3境界判定と種別コードの組み合わせ3
BQ (Bigram tag+type)BQ1–BQ4判定と種別コードのバイグラム組み合わせ4
TQ (Trigram tag+type)TQ1–TQ4判定と種別コードのトライグラム組み合わせ4

言語固有の特徴量(4 個、日本語と中国語のみ)

CategoryIDs説明Count
WC (Word+Char-type)WC1–WC4文字と種別コードの混合特徴量4
  • WC1: 位置 i-1 の文字 + 位置 i の種別コード
  • WC2: 位置 i-1 の種別コード + 位置 i の文字
  • WC3: 位置 i-1 の文字 + 位置 i-1 の種別コード
  • WC4: 位置 i の文字 + 位置 i の種別コード

韓国語と英語に WC がない理由: 韓国語のハングル音節は 2 種類(SN と SF)にのみ分類されるため、WC 特徴量は有用な信号ではなくノイズを追加してしまいます。英語でも同じ結果が直接計測されています: dev split での比較では、38 個のベーステンプレートで単語 F1 98.68%、WC を含む全 42 個で 98.65% でした – 詳細は English を参照してください。

特徴量の総数

LanguageBaseWCTotal
Japanese38442
Chinese38442
Korean38038
English38038

単一の真実の源

上記のテンプレート全体は、宣言的なテーブル(packed_model::TEMPLATES – プレフィックスと、タグ/文字/文字種スロットの順序付きリスト、固定の出力順)として 一度だけ定義されます。両方の特徴量表現がここから導出されます:

  • 後述の文字列形式 – 学習データ抽出、コーパス処理、モデルファイルで使用
  • segment()segment_with_pos() の 2 パススコアラーが使用する 整数インデックスのスコアリングテーブル群 – モデルファイルはロード時に この形式へコンパイルされる (予測パイプラインを参照)

したがって、テンプレートの追加や並び替えはすべての消費者を一貫して変更します。 テーブルの順序は文字列ライターの出力順を定義しており、モデルファイルと学習データが これに依存しています。

特徴量の形式

各特徴量は PREFIX:VALUE の形式の文字列として表現されます:

UW4:は        ← The character at position i is "は"
UC4:I         ← The type code at position i is "I" (Hiragana)
BW2:はテ      ← The bigram at position i-1..i is "はテ"
BC2:IK        ← The type bigram is Hiragana + Katakana
UP3:B         ← The previous boundary decision was "B" (boundary)
WC1:はK       ← Character "は" combined with type "K"

スライディングウィンドウの配置

セグメンタは入力をセンチネル文字でパディングします:

Index:   0    1    2    3    4    5    ...  n+2  n+3  n+4  n+5
Chars:   B3   B2   B1   c1   c2   c3  ...  cn   E1   E2   E3
Types:   O    O    O    t1   t2   t3  ...  tn   O    O    O
Tags:    U    U    U    U    ?    ?   ...  ?
  • B3, B2, B1 – 開始センチネル(パディング)
  • E1, E2, E3 – 終了センチネル(パディング)
  • O – パディング位置の「Other」種別
  • U – 初期位置の「Unknown」タグ
  • B – 「Boundary」タグ(単語の開始)
  • O – 「Other」タグ(継続)

境界(AdaBoost)パイプラインでは、特徴量は位置 4 から len-4 まで(両端を含む)抽出されます。POS パイプラインでは、segment_with_pos が最初の単語の品詞を導出するために位置 3(最初の実際の文字)でも予測を行うため、位置 3 でも特徴量が出力されます(#100)。 位置は 4(または 3)から len-4 まで(両端を含む)実行され、i-3 から i+2 の完全なウィンドウが利用可能です。

学習データの形式

extract コマンドは以下の形式で特徴量をファイルに書き出します(コーパス行 これ は テスト です 。 に対する実際の出力です):

-1	BC1:OI	BC2:II	BC3:II	BP1:UU	BP2:UU	BQ1:UOI	BQ2:UII	BQ3:UOI	BQ4:UII	BW1:B1こ	...
1	BC1:II	BC2:II	BC3:IK	BP1:UU	BP2:UO	BQ1:UII	BQ2:UII	BQ3:OII	BQ4:OII	BW1:これ	...

各行には以下が含まれます:

  1. ラベル(境界の場合 1、非境界の場合 -1
  2. タブ区切りの特徴量文字列。アルファベット順にソートされた順序(テンプレートの出力順ではない)で書き出されるため、各行は BC1: 特徴量から始まる

単語単位の特徴量テンプレート(二段構成)

ここまでは、各境界判定ごとに採点される文字レベルのテンプレート集合です。二段構成の品詞タグ付けの stage-2 単語タガーは、これとは別の無関係なテンプレート集合を採点します。これは litsea::word_featuresN_WORD_TEMPLATES = 23)で定義されており、文字位置ごとではなく、すでに分割済みの単語ごとに1行の特徴量を生成します。上記の packed_model::TEMPLATES とは別の宣言的テーブルであり、専用のランタイム packed_two_stage::PackedTwoStageModel へコンパイルされます(予測パイプラインを参照)。

文中の [start, end) の範囲にまたがる単語(w = 表層、n = end - start)について:

プレフィックス表現
WS単語の表層そのものハッシュ化文字列
WLmin(n, 4)密配列(単語長)
FC / LC先頭 / 末尾の文字ハッシュ化文字
ft / lt先頭 / 末尾の文字の文字種コード密配列(文字種)
TS先頭最大8文字の文字種コード列ハッシュ化文字列
L1-L3 / R1-R3左/右に距離1-3の文脈文字ハッシュ化文字
cl1-cl3 / cr1-cr3距離1-3の文脈文字種密配列(文字種)
LB / RB文脈バイグラム(左は距離2+1、右は距離1+2)ハッシュ化ペア
P2 / S2先頭 / 末尾の2文字(n >= 2 の単語のみ)ハッシュ化ペア

合計23個のテンプレートです。文の範囲外の文脈位置には、文字レベルパイプラインの B1-B3 / E1-E3 パディング(上記参照)と同様に、開始/終了センチネル文字が使用されます。

すべてのテンプレートが毎回の抽出で書き出されるわけではありません。.stage2 特徴量ファイルにどのサブセットが含まれるかは、抽出時の TwoStageFeatureSetfull / balanced / fast)によって制御されます – CLI フラグと各バリアントに含まれるテンプレートについては特徴量の抽出を参照してください。

文字種分類

Litsea の各言語は、個々の文字を言語学的に意味のあるカテゴリに分類する文字種クラスのセットを定義します。これらの種別コードは AdaBoost 分類器の特徴量として使用されます。

仕組み

Language::char_type(c: char) -> &'static str は、Unicode 文字範囲に対する match 式で文字を直接分類します(正規表現・アロケーションなし)。matchアームは上から順に評価され、最初にマッチしたアームが種別コードを決定します。どのアームにもマッチしない場合、その文字は "O"(Other)に分類されます。

言語ごとに数値の type id を返す分類関数(japanese_char_type_idchinese_char_type_idkorean_char_type_id)があり、char_type は返された id に対するテーブル参照であるため、文字列コードと数値 id は構造上ずれることがありません。全言語で共通のクラス "P"(句読点)・"A"(ラテン文字)・"N"(数字)は、言語固有クラスの後に評価される共有ヘルパー punct_latin_digit() にまとめられています。単純な範囲を超えるロジックはアーム本体内の追加コードで表現します(例: 韓国語のハングル音節構造に対するコードポイント判定)。

日本語の文字種

Code名称パターン / 範囲
M漢数字[一二三四五六七八九十百千万億兆]一, 千, 億
H漢字 / CJK 統合漢字U+4E00–U+9FFF, plus 々〆ヵヶ漢, 字, 学
Iひらがな[ぁ-ん]あ, い, う
Kカタカナ[ァ-ヴーア-ン゙゚]ア, カ, ー
P句読点CJK 記号(U+3000-303F)、全角(U+FF01-FF65)。, 、, 「
AASCII / ラテン文字[a-zA-Za-zA-Z]A, z, B
N数字[0-90-9]0, 5
Oその他フォールバック@, #

注意: “M”(漢数字)は “H”(一般漢字)よりも先にチェックされるため、一や百などの文字は一般的な漢字ではなく数字として分類されます。

中国語の文字種

Code名称パターン / 範囲
F機能語高頻度の文法語的, 了, 在, 是
CCJK 統合漢字U+4E00–U+9FFF中, 国, 人
XCJK 拡張 AU+3400–U+4DBF希少な文字
RCJK 部首U+2E80–U+2FDF康熙部首
P句読点CJK 記号 + 全角。, ,, 《
B注音符号U+3100–U+312F, U+31A0–U+31BF注音記号
AASCII / ラテン文字[a-zA-Za-zA-Z]A, z
N数字[0-90-9]0, 5
Oその他フォールバック@, #

中国語の機能語には以下が含まれます:

  • 構造助詞: 的, 地, 得
  • アスペクト / モーダル助詞: 了, 着, 过, 吗, 呢, 吧, 啊, 嘛
  • 接続詞: 和, 与, 或, 但, 而, 且, 及
  • 前置詞: 在, 从, 到, 把, 被, 对, 向, 给
  • よく使われる文法動詞 / 副詞: 是, 有, 不, 也, 都, 就, 要, 会, 能, 可

韓国語の文字種

Code名称パターン / 範囲
E助詞 / 語尾高頻度の文法助詞은, 는, 을, 를, 의, 에
SNハングル(パッチムなし)終声のないハングル音節가, 나, 하
SFハングル(パッチムあり)終声のあるハングル音節한, 글, 각
Jハングル字母U+1100–U+11FF個別の子音 / 母音
G互換字母U+3130–U+318Fㄱ, ㅏ, ㅎ
H漢字U+4E00–U+9FFFCJK 統合漢字
P句読点CJK 記号 + 全角。, ,
AASCII / ラテン文字[a-zA-Za-zA-Z]A, z
N数字[0-90-9]0, 5
Oその他フォールバック@, #

韓国語ハングル音節の検出

韓国語では SN と SF の種別にコードポイント判定付きの範囲アームを使用します。これは Unicode の体系的なハングルエンコーディングを活用しています:

  • ハングル音節は U+AC00–U+D7AF を占有
  • 各音節は (初声 * 21 + 中声) * 28 + 終声 + 0xAC00 としてエンコード
  • (codepoint - 0xAC00) % 28 == 0 の場合、音節に終声がない(SN)
  • それ以外の場合、終声がある(SF、「받침」)

この区別は、終声(パッチム/받침)の有無が韓国語の単語境界パターンと助詞の接続に影響するため重要です。

英語の文字種

Code名称パターン / 範囲
U大文字ラテン文字[A-ZA-Z]A, Z, T
W空白スペース、タブ、U+00A0 , \t
QアポストロフィU+0027, U+2019',
P句読点ASCII(アポストロフィを除く)+ 一般句読点 + CJK/全角., -, @,
A小文字ラテン文字[a-za-z]a, z
N数字[0-90-9]0, 5
Oその他フォールバック字, é

他の 3 言語と異なり、英語では ASCII の句読点(アポストロフィを除く)を "O" のままにせず "P" に分類します – これは英語固有の意図的な違いです (char_type('@') は他言語では "O" ですが、英語では "P" になります)。 アポストロフィは独立した種別("Q")として切り出されています。これは 短縮形(do + n't)や所有格(Google + 's)を示す文字レベルの マーカーだからです。また大文字には専用の種別("U")が割り当てられて います。これは文頭や固有名詞の境界と相関があるためです。ハイフンが 8 番目の種別を新設せずに "P" のままである理由を含む詳しい根拠は English を参照してください。

言語間の比較

FeatureJapaneseChineseKoreanEnglish
種別の総数89107
固有の種別M, H, I, KF, C, X, R, BE, SN, SF, J, GU, W, Q
共有する種別P, A, N, OP, A, N, OP, A, N, O(H は日本語と共通)P, A, N, O(P は ASCII 句読点全体に拡張)
マッチング方法範囲match範囲match範囲match + コードポイント判定範囲match
WC 特徴量の使用ありありなしなし

予測パイプライン

本章では、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 回、ホットパス外でコンパイルされる)

二段構成タグ付け

Litsea の segment_with_pos()二段構成(two-stage)モデル--posSegmenter::with_two_stage_learner、issue #147)に 支えられています。モデルファイルの litsea-two-stage v1 ヘッダがこの形式を 識別し(モデルファイル形式を参照)、 メソッドは Vec<(String, Upos)> の単語/タグのペアを返します。

仕組み

二段構成タグ付けは、文字レベルで POS クラスを採点することは一切ありません:

  1. stage 1 は二値の境界分類器で分割します – segment() が使うのと 構造的に同じスカラー重みの AdaBoost 形式なので、同じ速度で動きます。
  2. stage 2 は stage 1 が確定した単語ごとに、学習コーパスから構築した 候補タグ語彙表と単語単位の分類器でタグ付けします (モデルファイル形式を参照):
    • 学習時に観測されたタグが 1 個しかない、あるいは最頻タグが学習時の 出現の dominance 割合以上を占める表層は、分類器を一切呼ばずに タグ付けされます。
    • 曖昧な既知表層は、観測された候補タグのみ(マスク付き argmax)に 絞って採点されます(全 18 クラスではありません)。
    • 未知の表層は全クラス argmax にフォールバックします。

単語は平均して 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 F1Tagged 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 つの異なる学習と推論のミスマッチを 引き起こしていましたが、どちらも解消されました:

  1. stage 1 は、これらの言語でほとんどの語境界を示す空白文字を 一度も見ていませんでした。
  2. stage 2 も、より分かりにくい形で影響を受けていました: その文脈 特徴量(L1L3 / R1R3cl1cl3 / cr1cr3)は 周囲の文字を参照しますが、推論時に単語の隣は通常空白であるのに対し、 空白非保持の学習では次の単語の文字になっていました。空白はトークンの 約 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%
balanced82.29%92.95%88.66%
full82.96%93.33%90.43%

(韓国語と英語は issue #198 に伴って空白保持コーパスで再スイープしており、 この 2 列はそのスイープの dev split の数値です。中国語の列はそれ以前の ものです。その結果、韓国語の勝者は balanced から full に変わりました。)

日本語では fast だけで Tagged F1 92.95% に到達するため、同梱の japanese_pos.model はこれを使用しています。 中国語では balancedfull の伸びの大半(82.29% vs 82.96%)を 明確に良いスループットで得られるため、chinese_pos.modelfull ではなく balanced を使用しています。空白保持コーパスでの 韓国語・英語では full が明確な勝者であるため(韓国語は 93.33% vs balanced の 92.95%、英語は 90.43% vs 他の 2 つの約 88.7%)、 korean_pos.modelenglish_pos.model はどちらも full を 使用しています。別の特徴量セットで再学習するには extract --pos --stage2-features <set> + train --pos を再実行するだけで、 パイプラインの他の部分を変更する必要はありません。

言語サポート概要

Litseaは、Language 列挙型に基づく統一的なフレームワークを通じて、4つの言語の単語分割をサポートしています。

サポート言語

LanguageEnum VariantCLI ValuesFeature Count単語 F1(held-out)
日本語Language::Japanesejapanese, ja4296.70%
中国語Language::Chinesechinese, zh4290.69%
韓国語Language::Koreankorean, ko3899.91%
英語Language::Englishenglish, en3898.31%

Language 列挙型

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Language {
    #[default]
    Japanese,
    Chinese,
    Korean,
    English,
}
}
  • デフォルトJapanese
  • #[non_exhaustive] 付き – 新しい言語を破壊的変更なしに追加できるため、外部の match 式にはワイルドカードアームが必要
  • FromStr を実装 – 完全な言語名またはISO 639-1コードからパース可能(大文字小文字を区別しない)
  • Display を実装 – 小文字の完全な言語名を出力

パース例

#![allow(unused)]
fn main() {
use litsea::language::Language;

let ja: Language = "japanese".parse().unwrap();
let zh: Language = "zh".parse().unwrap();
let ko: Language = "Korean".parse().unwrap();   // case-insensitive
let err = "french".parse::<Language>();          // Err(...)
}

言語間の違い

各言語は独自の文字タイプパターンを定義しており、文字をタイプコードに分類します。これらのタイプコードはAdaBoost分類器の特徴量として使用されます。

AspectJapaneseChineseKoreanEnglish
文字タイプ数8 (M, H, I, K, P, A, N, O)9 (F, C, X, R, P, B, A, N, O)10 (E, SN, SF, J, G, H, P, A, N, O)7 (U, W, Q, P, A, N, O)
WC特徴量あり(4個追加)あり(4個追加)なしなし
総特徴量数42423838
マッチング方式match(文字範囲)match(文字範囲)match(文字範囲)+ コードポイント判定match(文字範囲)

韓国語・英語の特徴量が少ない理由

韓国語のハングル音節は、SN(받침/終声なし)とSF(받침あり)の2種類にのみ分類されます。この二値的な区別では、WC特徴量(単語+文字タイプの組み合わせ)は冗長な情報を生成し、識別力がほとんどありません。これらを除外することで、ノイズを低減し、モデルをコンパクトに保ちます。

英語も関連する理由で同じ結論に至りました。支配的な境界シグナルは空白であり、held-out の dev split での比較では、WC を含まない38テンプレートの tag-free モデルが Word F1 98.68%、WC(WC1WC4)を含む42テンプレートでは98.65%という結果になりました – WC特徴量は「役に立たない」どころか「悪化させる」ことが実測で確認されています。 詳細は英語を参照してください。

日本語

日本語はLitseaのデフォルト言語です。

文字タイプ

CodeNamePatternExamples
M漢数字[一二三四五六七八九十百千万億兆]一, 三, 千, 億
H漢字 / CJKU+4E00–U+9FFF, plus 々〆ヵヶ漢, 字, 学, 々
Iひらがな[ぁ-ん]あ, い, う, を
Kカタカナ[ァ-ヴーア-ン゙゚]ア, カ, ー, ハ
P句読点CJK記号 + 全角。, 、, 「, 」
AASCII/ラテン文字[a-zA-Za-zA-Z]A, z, B
N数字[0-90-9]0, 5, 5
Oその他フォールバック@, #, $

パターンの優先順位

パターンは順番に評価されます。特に注意すべき点は以下の通りです。

  • MはHより先に評価: 一や百などの文字は、汎用的な「漢字」(H)ではなく「漢数字」(M)に分類されます
  • この区別により、モデルは数字特有の境界パターンを学習できます

学習済みモデル

japanese.model

  • 学習コーパス: UD Japanese-GSD
  • 学習オプション: Averaged Perceptron を50エポック学習し、AdaBoostのスカラー重みへ 畳み込んだ後、|weight| 上位40,000特徴量に剪定 – 詳しい手順は 学習手順を参照
  • 単語 F1(held-out): 96.70%
  • 境界 F1(held-out): 98.59%

japanese_pos.model

  • アルゴリズム: 二段構成の単語分割+品詞推定(二値境界分類器 + 候補タグ語彙表付き単語単位タガー)
  • 詳細: 事前学習済みモデルを参照

RWCP.model

  • 出典: オリジナルのTinySegmenterから抽出
  • ライセンス: BSD 3-Clause(工藤拓)
  • サイズ: 約22 KB

JEITA_Genpaku_ChaSen_IPAdic.model

  • 学習コーパス: JEITA杉田玄白プロジェクトのコーパス
  • トークナイザ: ChaSen(IPAdic辞書使用)
  • サイズ: 約16 KB

使用例

echo "LitseaはTinySegmenterを参考に開発された、Rustで実装された極めてコンパクトな単語分割ソフトウェアです。" \
  | litsea segment -l japanese ./models/RWCP.model

出力:

Litsea は TinySegmenter を 参考 に 開発 さ れ た 、Rust で 実装 さ れ た 極めて コンパクト な 単語 分割 ソフトウェア です 。

中国語

Litseaは簡体字・繁体字の両方を対象とした中国語の単語分割をサポートしています。

文字タイプ

CodeNamePatternExamples
F機能語高頻度の文法語的, 了, 在, 是, 和
CCJK統合漢字U+4E00–U+9FFF中, 国, 人
XCJK拡張AU+3400–U+4DBF稀少文字
RCJK部首U+2E80–U+2FDF康熙部首
P句読点CJK記号 + 全角。, ,, 《, 》
B注音符号U+3100–U+312F, U+31A0–U+31BF注音記号
AASCII/ラテン文字[a-zA-Za-zA-Z]A, z
N数字[0-90-9]0, 5, 5
Oその他フォールバック@, #, $

中国語の機能語(虚词)

「F」タイプは、分割において重要な高頻度の文法語を捉えます。

CategoryCharacters
構造助詞的, 地, 得
アスペクト・語気助詞了, 着, 过, 吗, 呢, 吧, 啊, 嘛
接続詞和, 与, 或, 但, 而, 且, 及
前置詞在, 从, 到, 把, 被, 对, 向, 给
文法動詞・副詞是, 有, 不, 也, 都, 就, 要, 会, 能, 可

これらの文字は圧倒的に文法的な役割で出現し、内容語とは異なる形で語境界を示します。

学習済みモデル

chinese.model

  • 学習コーパス: UD Chinese-GSD
  • 学習オプション: Averaged Perceptron を100エポック学習し、AdaBoostのスカラー重みへ 畳み込んだ後、|weight| 上位70,000特徴量に剪定 – 詳しい手順は 学習手順を参照
  • 単語 F1(held-out): 90.69%
  • 境界 F1(held-out): 95.64%

chinese_pos.model

  • アルゴリズム: 二段構成の単語分割+品詞推定(二値境界分類器 + 候補タグ語彙表付き単語単位タガー)
  • 詳細: 事前学習済みモデルを参照

使用例

echo "中文分词测试。" | litsea segment -l chinese ./models/chinese.model

韓国語

Litseaは、ハングル文字タイプの特殊な検出機能を備えた韓国語の単語分割をサポートしています。

文字タイプ

CodeNamePatternExamples
E助詞/語尾[은는을를의에]은, 는, 을, 를, 의, 에
SNハングル(받침なし)コードポイント演算가, 나, 하, 모
SFハングル(받침あり)コードポイント演算한, 글, 각, 붙
Jハングル字母U+1100–U+11FF個別の子音/母音
G互換字母U+3130–U+318Fㄱ, ㅏ, ㅎ
H漢字U+4E00–U+9FFFCJK統合漢字
P句読点CJK記号 + 全角。, ,
AASCII/ラテン文字[a-zA-Za-zA-Z]A, z
N数字[0-90-9]0, 5, 5
Oその他フォールバック@, #, $

韓国語の助詞(조사)

「E」タイプは、6つの高頻度文法助詞を捉えます。

CharacterRoleName
은/는主題マーカー주격 조사
을/를目的語マーカー목적격 조사
所有格관형격 조사
場所格부사격 조사

これらの助詞は語境界に頻繁に出現するため、分割精度を向上させるために独自のタイプコードが割り当てられています。

ハングル音節構造(받침検出)

韓国語では、SNとSFタイプに本体でコードポイント判定を行う範囲アームを使用します。これはUnicodeハングルエンコーディングの体系的な構造を活用しています。

  • ハングル音節: U+AC00–U+D7AF(11,172音節)
  • 各音節 = (初声 * 21 + 中声) * 28 + 終声 + 0xAC00
  • SN(받침なし): (codepoint - 0xAC00) % 28 == 0
  • SF(받침あり): (codepoint - 0xAC00) % 28 != 0

받침(終声子音)の有無の区別は言語学的に重要であり、助詞が語にどのように接続するか、また境界がどこに生じるかに影響します。

WC特徴量なし

韓国語ではWC(単語+文字タイプ)特徴量を使用しません。ほとんどのハングル音節はSNとSFの2つのタイプにしか分類されないため、WC特徴量は低エントロピーでノイズの多い組み合わせを生成し、モデルの精度を低下させます。

空白保持学習(Space-Preserving Training)

韓国語は語節(어절)の間に空白を入れて表記され、その空白がほとんどの語境界を示します。そのため韓国語モデルは 空白保持 TSV コーパス で学習しています。トークンをタブで区切り、語節間の各空白を独立したトークンとして保持することで、学習テキストに元の文の空白文字が含まれ、モデルはそれを境界のコンテキストとして利用できます。コーパスは corpus_udtreebank.sh -s(ツリーバンクの SpaceAfter アノテーションから空白を復元)で生成し、特徴量は litsea extract --format tsv で抽出します。推論時に特別な処理は不要です: segment() は空白を含むテキストをそのまま受け取り、各空白を独立したトークンとして出力します。

コーパス内では各空白が独立した1文字トークンになるため、文字単位のラベル付け(AdaBoost 参照)は空白の前後に2つの別々の境界を付与します: 空白自体が新しいトークンの開始(ラベル B)であり、空白の直後の文字も次のトークンの開始(同じく B)です。空白は必ずちょうど1トークンであり、その直後は必ず次のトークンの開始になるため、どちらもコーパスの構成上決定的(モデルがほぼ自明な規則として学習可能)です。held-out の単語 F1 に影響するのはこのうち後者(実単語の開始境界)のみで、純粋な空白トークンはスコアリングから除外されます(評価 参照)。

学習済みモデル

korean.model

  • 学習コーパス: UD Korean-GSD(空白保持 TSV コーパス)
  • 学習オプション: --format tsv --tag-free、Averaged Perceptron を 30エポック学習し、AdaBoostのスカラー重みへ畳み込み、剪定なし (3,132特徴量)– 詳しい手順は 学習手順を参照
  • 単語 F1(held-out): 99.91%
  • 境界 F1(held-out): 99.96%

held-out 指標は、空白トークンをスコアリングから除外した上で、元の空白付きテキストに対して計算しています。

このモデルは 16 個のタグ依存特徴量テンプレートを使わずに学習しています (--tag-free、issue #183): 空白シグナルがあるため、これらは計測上 何も寄与しておらず、pointwise なモデルにすることで segment() は逐次 スコアリングパスを丸ごとスキップできます。詳細は タグなし(pointwise)モデル を参照してください。

korean_pos.model

  • アルゴリズム: 二段構成の単語分割+品詞推定(二値境界分類器 + 候補タグ語彙表付き単語単位タガー)
  • 単語 F1(held-out): 99.88%
  • タグ付き単語 F1(held-out): 93.95%
  • 注記: このモデルは korean.model同じ空白保持コーパスで学習して います(issue #198)。そのため単語 F1 は korean.model の 99.91% との差が 0.03 ポイントに留まります。#198 以前は二段構成パイプラインが空白非保持の word/POS コーパスで学習しており、実際のスペース付き入力では 94.01% でした。 プロトコルを切り替えたことで、単語 F1 は +5.9 ポイント、タグ付き単語 F1 は +10.8 ポイント向上しています。スペースは独立したトークンとして再出力され、 分類器による推測ではなく候補1つだけの語彙表エントリを通じて決定的に X と タグ付けされます。英語の正書法は空白がないと境界の手がかりがはるかに少ない ため、同じ変更が 20 ポイント以上の効果をもたらしています。詳しくは 英語を参照してください
  • 詳細: 事前学習済みモデルを参照

使用例

echo "한국어 단어 분할 테스트입니다." | litsea segment -l korean ./models/korean.model

英語

Litseaは、ラテン文字の正書法に合わせて調整した文字タイプ集合により、英語の単語分割をサポートしています。大文字と小文字、空白、アポストロフィをそれぞれ独立した種別として区別します。

文字タイプ

CodeNamePatternExamples
U大文字ラテン文字[A-ZA-Z]A, Z, T
W空白文字スペース、タブ、ノーブレークスペース , \t, U+00A0
Qアポストロフィ[\u{27}\u{2019}]',
P句読点アポストロフィを除く ASCII 句読点 + 一般句読点のダッシュ/引用符/省略記号(U+2019 を除く)+ CJK/全角., -, ", @,
A小文字ラテン文字[a-za-z]a, z
N数字[0-90-9]0, 5, 5
Oその他フォールバックCJK表意文字、ASCII外のアクセント付きラテン文字

独立した種別としての大文字

文頭の大文字、固有名詞、頭字語は英語の語境界と強く相関するため、大文字のラテン文字は小文字(「A」)と同じクラスに統合せず、独自の種別「U」として扱います。これは、他の言語が言語学的に特徴的な部分集合(例えば韓国語の助詞文字)をより広い共通クラスから切り出しているのと同じ考え方です。

独立した種別としての空白

すべての言語が共有するヘルパー関数 punct_latin_digit() は、ASCII句読点や ASCII スペース(U+0020)を分類しません – 日本語・中国語・韓国語ではどちらも "O" にフォールスルーします。これらの言語のコーパスでは、水平方向の空白それ自体が境界シグナルを持つ必要がないためです。英語は異なります: タイプテーブルにはスペース・タブ・ノーブレークスペース用の「W」が追加されています(学習コーパスに実際に出現するのはプレーンなスペースのみですが、残り2つも同じIDを共有させることで、貼り付けられた入力が "O" へフォールバックせず同じ挙動を継承します)。

独立した種別としてのアポストロフィ

アポストロフィは、短縮形や所有格を通常の句読点から区別する文字レベルのシグナルです: do + n'tGoogle + 's。ASCII アポストロフィ(U+0027)と、学習コーパスの原文によく出現するタイポグラフィ上の右シングルクォーテーションマーク(U+2019)の両方をカバーする独自の種別「Q」を割り当てています。文字レベルの特徴量テンプレートが直接キーにできるよう、Q は意図的に P から除外しています。

句読点は一律

他の3言語では @ のような ASCII 句読点は "O" にフォールスルーし、CJK/全角の句読点だけが "P" にマップされますが、英語ではアポストロフィを除く ASCII 句読点のほぼすべてを "P" として分類します。これに加えて、学習テキストの非 ASCII 版をカバーする同じ一般句読点の範囲(ダッシュ、カーリークォート、省略記号)も含まれます。これは英語固有の意図的な違いです: char_type('@') は日本語/中国語/韓国語では "O" を返しますが、英語では "P" を返します。

ハイフンは独自の種別を与えず "P" に分類しています。UD English-EWT はハイフン付きの複合語を別々のトークンとしてトークナイズするため(例: search-engine)、ゴールドスタンダード上ではハイフンは通常の区切り句読点として振る舞います。生の文字自体は文字レベル(UW*/BW*)テンプレートから引き続き参照できるため、8番目の種別コードを追加しなくてもハイフン固有の挙動は学習可能です。追加すると密な特徴量テーブルがさらに \((8/7)^3 \approx 1.49\times\) 増大してしまいます。

WC特徴量なし

英語は韓国語と同じ理由でWC(単語+文字タイプ)特徴量を使用しません: 支配的な境界シグナル(空白)がすでにほとんどの位置を解決しているため、文字/タイプ混合テンプレートを追加してもほとんど寄与しないからです。これは類推だけでなく実測でも確認されています – held-out の dev split では、38 個の基本テンプレートによる tag-free 分割モデルが Word F1 98.68% であったのに対し、WC(WC1WC4)を含む全 42 テンプレートでは 98.65% でした。つまり WC 特徴量を追加すると、モデルは改善するどころか悪化しました。

空白保持学習(Space-Preserving Training)

英語は単語の間にスペースを入れて表記され、(短縮形や一部の句読点のケースを除けば)そのスペースがほとんどの語境界を示します。韓国語と同様に、このモデルは空白保持 TSV コーパスで学習しています: トークンをタブで区切り、各スペースを独立したトークンとして保持することで、学習テキストに元の文のスペース文字が含まれ、モデルはそれを境界のコンテキストとして利用できます。コーパスは corpus_udtreebank.sh -s で生成し、特徴量は litsea extract --format tsv で抽出します。推論時に特別な処理は不要です: segment() はスペースを含むテキストをそのまま受け取り、各スペースを独立したトークンとして出力します。

複数語トークン(短縮形)。 UD English-EWT では don't のような短縮形を、間にスペースを持たない2つの単語行(don't)をカバーする範囲行(例えば ID 3-4)として表現します。corpus_udtreebank.sh -s は範囲行を特別扱いします: それ自体としてはトークンを出力せず、範囲を構成する単語間へのスペース挿入を抑制し、範囲自身の SpaceAfter アノテーションを最後の構成単語の後にのみ適用します。具体的には、“I don’t know.” という文は I don't know. というトークン列になります – これは english.model の実際の出力と一致します(下記の例を参照)。この不変条件 – 範囲を構成する単語形を連結すると範囲自身の表層形が再現される – は UD English-EWT のすべての複数語トークンで成立します。

各スペースは独立した1文字トークンであるため、文字単位のラベル付けはその前後に2つの別々の境界を付与します。これは韓国語の場合と全く同じです: これがモデルにとってほぼ自明な規則であり、held-out の単語 F1 に影響しない理由(純粋な空白トークンはスコアリングから除外されます)については韓国語の説明を参照してください。

学習済みモデル

english.model

  • 学習コーパス: UD English-EWT(空白保持 TSV コーパス)
  • 学習オプション: --format tsv --tag-free、Averaged Perceptron を 20 エポック学習(dev split での {10, 20, 30, 50} のエポックスイープにより 選定 – 品質はエポック20でピークに達し、それ以降はわずかに悪化)、 AdaBoost のスカラー重みへ畳み込み、剪定なし(4,794 特徴量)– 詳しい 手順は学習手順を参照
  • 単語 F1(held-out): 98.31%
  • 境界 F1(held-out): 99.18%
  • ファイルサイズ: 約 125 KB

このモデルは 16 個のタグ依存特徴量テンプレートを使わずに学習しています(--tag-free、issue #183)。dev split での比較により、タグ特徴量が英語にほとんど寄与しないことが確認されています(タグあり 38 テンプレートの最良値: エポック 30 で Word F1 98.71%、タグなし 38 テンプレートの最良値: エポック 20 で 98.68% – 差はわずか 0.03pt)。そのため同梱モデルは tag-free で提供され、segment() は逐次スコアリングパスを丸ごとスキップできます。詳細はタグなし(pointwise)モデルを参照してください。

held-out 指標は、空白トークンをスコアリングから除外した上で、元の空白付きテキストに対して計算しています。

english_pos.model

  • アルゴリズム: 二段構成の単語分割+品詞推定(二値境界分類器 + 候補タグ語彙表付き単語単位タガー)
  • stage-2 特徴量セット: full(dev split での fast/balanced/full の スイープにより選定 – full がタグ付き単語の精度で最良だった)、50 エポック
  • 単語 F1(held-out): 98.30%
  • タグ付き単語 F1(held-out): 90.55%
  • ファイルサイズ: 約 3.1 MB
  • 詳細: 事前学習済みモデルを参照

このモデルは english.model と同じ空白保持コーパスで学習しています(issue #198)。そのため単語 F1(98.30%)は english.model の 98.31% とほぼ一致します – 二段構成の stage-1 分類器は、専用の分割モデルと同等に英語の語境界を見つけられるようになりました。

ただし最初からそうだったわけではないため、古い数値と比較する場合はこの経緯を知っておくと役立ちます。二段構成パイプラインは当初、word/POS コーパスを空白なしで連結したもので学習しており、英語のテキストが実際に含んでいるスペースを捨てていました。そのモデルは同じ空白非保持プロトコルで 70.33%、実際のスペース付き入力では 77.55% でした – 例えば a test を1つのトークンに結合してしまう、といった具合です。学習コーパスを切り替えたことで単語 F1 で +20.8 ポイント、タグ付き単語 F1 で +20.7 ポイントを獲得し、タグ付けも約 3.6 倍高速になりました(2.05M → 7.32M chars/s)。

空白なしコーパスは、学習と推論の不一致を 2 つ同時に引き起こしていました。まず stage-1 は、英語のほぼすべての語境界を示すスペースを一度も目にしていませんでした。stage-2 のコンテキスト特徴量(L*/R*/cl*/cr*)も同様です: 推論時には単語の隣は通常スペースですが、空白なし学習では次の単語の文字でした。現在はどちらも segment --pos が計算するものと一致します。

スペースは独立したトークンとして再出力され、X としてタグ付けされます – コーパスが空白文字に候補1つだけの語彙表エントリを与えるため、packed モデルは分類器で推測するのではなく固定タグのパスを通ります(#198 以前のモデルは、同じ文の中の異なるスペースに対して PUNCT/PART/AUX を返していました)。litsea/tests/golden.rs の golden テストがこの挙動を固定しています。

使用例

echo "I don't know." | litsea segment -l english ./models/english.model
# I   do n't   know .

新しい言語の追加

Litseaの多言語フレームワークは、容易に拡張できるよう設計されています。本ガイドでは、新しい言語のサポートを追加する方法を、英語追加(issue #194)を具体例として一貫して用いながら説明します。

手順の概要

  1. Language 列挙型にバリアントを追加
  2. Display および FromStr のmatchアームを実装
  3. 文字タイプ判定関数を作成
  4. 分類関数を登録
  5. WC特徴量の有無を決定
  6. コーパスプロトコルを選択(スペース区切りまたは空白保持 TSV)
  7. 同梱分割モデルを学習(binary-perceptron 畳み込み手順)
  8. 任意で二段構成 POS モデルを学習
  9. held-out 評価用ゴールドファイルを追加
  10. テストを追加

手順1: Language にバリアントを追加

litsea/src/language.rs で、Language 列挙型に新しいバリアントを追加します。

#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum Language {
    #[default]
    Japanese,
    Chinese,
    Korean,
    English,
    Thai,       // ← new language
}
}

この列挙型に #[non_exhaustive] が付いているのは、まさに新しい言語の追加が想定されているためです。したがってバリアントの追加は、下流クレートにとって破壊的変更にはなりません。

手順2: Display と FromStr を実装

新しい言語のmatchアームを追加します。

#![allow(unused)]
fn main() {
// In Display impl
Language::Thai => write!(f, "thai"),

// In FromStr impl
"thai" | "th" => Ok(Language::Thai),
}

あわせて language.rsParseLanguageError のメッセージも更新してください。このメッセージはサポート言語を列挙しており(Supported: japanese (ja), chinese (zh), korean (ko), english (en))、ユニットテストで固定されているため、メッセージとテストの両方に新しい言語を含める必要があります。

手順3: 文字タイプ判定関数を作成

新しい言語の文字を種別 ID(type id)に分類する関数を定義します。ID は言語の順序付き type_codes() テーブル(手順 4)へのインデックスです: 共通クラスは固定インデックス(“O” = 0、“P” = 1、“A” = 2、“N” = 3)を占め、言語固有クラスは 4 から続きます。分類は文字範囲に対する match 式で直接行います(正規表現は使いません)。最初にマッチしたアームが種別を決定します。

#![allow(unused)]
fn main() {
fn thai_char_type_id(c: char) -> u8 {
    match c {
        // タイ文字の子音・順行母音 (U+0E01-U+0E3A)
        '\u{0E01}'..='\u{0E3A}' => 4, // "T"
        // タイ文字の母音・声調記号 (U+0E40-U+0E4E)
        '\u{0E40}'..='\u{0E4E}' => 5, // "V"
        // タイ数字 (U+0E50-U+0E59)
        '\u{0E50}'..='\u{0E59}' => DIGIT_TYPE_ID, // "N"
        // 共通クラス: "P"(句読点)、"A"(ラテン文字)、"N"(数字)
        _ => punct_latin_digit(c).unwrap_or(OTHER_TYPE_ID),
    }
}
}

英語の english_char_type_id は、同じパターンをラテン文字言語に適用した、リポジトリ内に実在する例です: 「U」(大文字)、「W」(空白)、「Q」(アポストロフィ)を専用クラスとして追加するだけでなく、共通クラス「P」を ASCII 句読点まで拡張しています(他の言語では ASCII 句読点は「O」のままです)– 言語の分類関数は、新しいクラスを punct_latin_digit() の後ろに追加するだけでなく、その前段に追加のロジックを自由に重ねることができます。

文字タイプ設計のヒント

  • 語境界パターンと相関する言語学的に異なるカテゴリを特定する
  • 順序は重要 – 最初にマッチしたものが優先されるため、より具体的なパターンを汎用的なパターンの前に配置する
  • 高頻度の機能語を別のタイプとして検討する(中国語の「F」のように)。あるいは、スペース区切りの言語であれば、実際に境界と相関する句読点・大文字小文字・発音区別符号の違いを検討する(英語の「U」/「W」/「Q」のように)
  • 単純な範囲だけでは足りない場合はアーム本体内の追加ロジックを使用する(韓国語が받침の有無で音節を分割するためにコードポイント判定を使用しているように)
  • 共通の「P」/「A」/「N」クラスには、共有ヘルパー punct_latin_digit() を再利用する
  • コード集合は prefix-free に保つ – どのコードも他のコードのプレフィックスであってはならない(韓国語の SN/SF が成立するのは S 単独がコードでないためであり、そのため裸の "S" は韓国語に限らずすべての言語についてユニットテストで拒否されます)。モデルローダは packed 特徴キーへのコンパイル時に連結された種別コードを左から右へデコードするため、prefix-free 性がデコードの一意性を保証します
  • 種別テーブルは最低 7 個のコードが必要です。 共有テストコンテキスト(packed_model.rsctx_for)が codes.len() >= 7 をアサートしており、英語の 7 コードのテーブルが現状の最小値です。上限は固定されていませんが、密な特徴量テーブル(BC/UC/TC/BQ/TQ)はおおよそ type_count^2 から type_count^3 に比例して大きくなるため、テーブルを大きくしすぎると分類の粒度と引き換えにモデルサイズとロード時間が増加します

手順4: 種別コードテーブルと分類関数を登録

言語の順序付きコードテーブルを Language::type_codes() に(インデックス = 種別 ID、共通コードが先頭)、ディスパッチアームを Language::char_type_id() に追加します。char_type() 自体はこの 2 つから導出されるため、文字列コードと数値 ID が乖離することはありません。

#![allow(unused)]
fn main() {
pub(crate) fn type_codes(self) -> &'static [&'static str] {
    match self {
        // ...
        Language::Thai => &["O", "P", "A", "N", "T", "V"],    // ← new
    }
}

pub(crate) fn char_type_id(self, c: char) -> u8 {
    match self {
        // ...
        Language::Thai => thai_char_type_id(c),    // ← new
    }
}
}

手順5: WC特徴量の有無を決定

特徴テンプレートは packed_model.rsTEMPLATES)に一度だけ定義されており、templates_for() が末尾の WC1WC4(文字/種別混合テンプレート)を言語が使用するかどうかを決定します。

#![allow(unused)]
fn main() {
pub(crate) fn templates_for(language: Language) -> &'static [Template] {
    match language {
        Language::Japanese | Language::Chinese => &TEMPLATES[..],
        Language::Korean | Language::English => &TEMPLATES[..BASE_TEMPLATE_COUNT], // 38 個の基本テンプレート
    }
}
}

この match 式は意図的にワイルドカードアームを持たない網羅的(exhaustive)な matchにしてあります – Thai を追加してもどちらのアームにも加えなければコンパイルエラーになり、暗黙のデフォルトにはなりません。これは意図的な設計です: 以前のバージョンのこの match には _ => &TEMPLATES[..BASE_TEMPLATE_COUNT] というフォールバックがあり、新しい言語は誰も意図的に決めないまま 38 テンプレート(WCなし)構成になってしまっていました。WC の判断は明示的に行い、推測ではなく計測で裏付けてください: held-out の dev split で tag-free モデルを WC ありとなしの両方で学習し、単語 F1 を比較します(英語での比較は英語に記載されており、WC は「役に立たない」どころか実測で「悪化させる」ことが確認されました – WCなし 98.68% に対し WCあり 98.65%)。出発点となるヒューリスティックとして: 対象言語の文字タイプに十分な多様性があり WC 特徴量が有益になりそうであれば含める、韓国語や英語の「SN」/「A」や空白が支配的なような低エントロピーなタイプ体系であれば除外する – ただし、どちらを選ぶ場合も同梱モデルにコミットする前に数値で検証してください。

手順6: コーパスプロトコルを選択

Litseaは2種類のコーパスプロトコルをサポートしており、どちらを選ぶかは対象言語がスペース区切りで表記されるかどうかで決まります:

  • スペース区切り(デフォルト): 単語を1個のスペースでつなぎ、1行1文とする形式。スペースなしで表記される言語(日本語、中国語)や、スペースが境界シグナルを持たない場合に使用します。
  • 空白保持 TSV--format tsv、issue #152): トークンをタブで区切り、トークン自体がリテラルなスペース " " になり得る形式で、元の空白がそのまま学習用の一級特徴量として残ります。スペース自体が最も強い境界シグナルとなる言語(韓国語、英語)に使用します。

対象言語が単語間にスペースを入れて表記される場合(英語のように。韓国語の語節間の慣習とは異なりますが、根底にある理由は同じです)は、空白保持プロトコルを使用します:

conllu_file=$(bash scripts/download_udtreebank.sh -l en -o /tmp)
bash scripts/corpus_udtreebank.sh -s "$conllu_file" corpus.tsv
litsea extract -l english --format tsv --tag-free corpus.tsv features.txt

元のツリーバンクに複数語トークン(短縮形、接語)が含まれる場合は、コーパスを信頼する前に corpus_udtreebank.sh -s がそれらを正しく扱えているか確認してください。 UD CoNLL-U では、英語の don't のような短縮形を、間にスペースを持たない2つの単語行(don't)をカバーする範囲行(例: 3-4 don't)として表現します。corpus_udtreebank.sh -s は範囲行を特別扱いします: それ自体としてはトークンを出力せず、範囲を構成する単語間へのスペース挿入を抑制し、範囲自身の SpaceAfter アノテーションは最後の構成単語の後にのみ適用します。新しいツリーバンクについて確認すべき不変条件は次のとおりです: 範囲を構成する単語形を連結すると、範囲自身の表層形が再現されなければならない。 これは UD English-EWT のすべての複数語トークンで成立していました(学習前にコーパス全体を比較するスクリプトで検証済み)。対象のツリーバンクで成立しない場合、安全なフォールバックは、構成単語を展開する代わりに範囲自身の表層形を単一トークンとして出力することです。もう1つの独立したチェックとして、各文の TSV トークン(スペーストークンを含む)を連結して文を再構成し、その結果を CoNLL-U ファイルのその文に対応する # text = メタデータ行と diff してください – これにより、構成単語の連結チェックだけでは見逃すスペースのバグを検出できます。corpus_udtreebank.sh 自体を変更した場合は、既存の空白保持ゴールドファイル(例: resources/eval/korean_gsd_test.tsv)も再生成し、コミット済みのバージョンと diff してください – 変更が加法的であれば、バイト単位で同一になるはずです。

手順7: 同梱分割モデルを学習

同梱の分割モデルは、プレーンな litsea train -t/-i(AdaBoost ブースティング)では学習していません。2クラスの Averaged Perceptron として学習し、無損失で AdaBoost モデル形式へ畳み込んでいます – 導出の全体と正確な5ステップの手順(抽出 → 境界ラベル 1/-1B/O にリマップ → train --perceptronscripts/collapse_binary_perceptron.py → 任意で剪定)は事前学習済みモデル: 学習手順を参照してください。空白保持プロトコルの言語の場合:

litsea extract -l english --format tsv --tag-free corpus.tsv features.txt
sed -i 's/^1\t/B\t/; s/^-1\t/O\t/' features.txt
litsea train --perceptron --num-epochs 20 features.txt perceptron.model
scripts/collapse_binary_perceptron.py perceptron.model models/english.model

エポック数や tag-free/WC の判断を推測で決めないでください – held-out のdev split(最後に一度だけ触れるべき test split には触れない)でいくつかのエポック数のスイープを行い、それぞれの最良エポック数で tag-free とタグありの比較、および(手順5に従って)WC ありとなしの比較を行ってください。例えば英語のスイープでは、品質は20エポックでピークに達し、それ以降はわずかに悪化することがわかりました – 一発だけの低エポック実行ではモデルの実際の品質を過小評価してしまい、一発だけの高エポック実行では、すでに通り過ぎた収束点ではなく過学習が天井であるかのように見えてしまいます。

手順8: 任意で二段構成 POS モデルを学習

言語に UPOS タグ付きデータが利用可能な場合は、追加で二段構成モデルを学習できます:

bash scripts/corpus_udtreebank.sh -p "$conllu_file" pos_corpus.txt
litsea extract --pos -l english --stage2-features full pos_corpus.txt pos_features
litsea train --pos --num-epochs 50 pos_features models/english_pos.model

上記のエポックスイープと同様に、dev split で --stage2-featuresfast/balanced/full)をスイープしてください。このパスを使う同梱3言語は、それぞれ異なる勝者を選んでいます(stage-2 特徴量セットの選び方を参照)。

スペース区切りの言語では、二段構成モデルも空白保持コーパスで学習してください(issue #198)。コーパスは corpus_udtreebank.sh -p -s で生成し、--pos --format tsv で抽出します:

bash scripts/corpus_udtreebank.sh -p -s "$conllu_file" pos_corpus.tsv
litsea extract --pos --format tsv -l english --stage2-features full pos_corpus.tsv pos_features

これは見た目以上に大きな影響があります。空白なしで連結したコーパスで学習すると、単語 F1 は韓国語で約 5.9 ポイント、英語で約 20.8 ポイント低下します。学習と推論の不一致が同時に 2 つ生じるためです: stage-1 はほとんどの語境界を示すスペースを一度も目にせず、stage-2 のコンテキスト特徴量(L*/R*/cl*/cr*)は学習時と推論時とで異なる隣接要素を見ることになります。空白トークンには stage-2 の学習行が与えられません(スペース付きコーパスでは行の約 43% を占め、退化した 1 クラスの学習になってしまうため)が、語彙表エントリは与えられます。これにより候補が1つだけになり、packed モデルの固定タグのパスを通じて決定的にタグ付けされます – トークンの約 43% で分類器をスキップできるため、スループット面でも有利です。

スペースを使わずに表記される言語(日本語、中国語)では、素の -p コーパスを使用してください: 保持すべき空白がなく、スペース区切りの形式がすでに実際の入力と一致しています。

手順9: held-out 評価用ゴールドファイルを追加

ツリーバンクのtest split(dev split のスイープが完了した後にのみ触れる)から held-out ゴールドデータを生成し、既存の命名規則(分割用は <language>_<treebank>_test.{txt,tsv}、POS 用は <language>_<treebank>_test_pos.txt)に従って resources/eval/ 配下に追加します:

bash scripts/corpus_udtreebank.sh -s "$conllu_file_test" resources/eval/english_ewt_test.tsv
bash scripts/corpus_udtreebank.sh -p "$conllu_file_test" resources/eval/english_ewt_test_pos.txt
litsea evaluate -l english --format tsv models/english.model resources/eval/english_ewt_test.tsv
litsea evaluate -l english --pos models/english_pos.model resources/eval/english_ewt_test_pos.txt

resources/eval/README.md を新しいファイルとその出所/ライセンス(UD ツリーバンクは通常 CC BY-SA 4.0 であり、リポジトリの他の部分の MIT/Apache-2.0 ライセンスとは異なります)で更新してください。モデルのドキュメント(新しい数値が必要なドキュメントページの一覧は手順10を参照)には、litsea train の in-sample の出力ではなく、litsea evaluate から得た held-out の数値を記録してください。

手順10: テストとドキュメントを追加

この手順はスコープを狭く見積もりがちです: 新しい言語は language.rs/segmenter.rs だけでなく、もっと多くのテストファイルに影響します。以下のチェックリストに沿って確認してください。

コードテスト:

  • litsea/src/language.rs: ALL_LANGUAGES(配列サイズを増やす)、test_language_from_strtest_parse_language_error_message(新しい完全なエラー文字列)、test_language_display、および全ての種別コードと共通クラス・「O」のケースをいくつかカバーする新しい test_<language>_char_types
  • litsea/src/packed_model.rs: test_templates_for_language_gating(新しい言語のテンプレート数をアサート)、および test_pack_parse_roundtrip_unique_and_injectivetest_dense_index_consistent_with_key_decode にあるハードコードされた2つの言語列挙配列
  • litsea/src/segmenter.rs: 新しい test_char_type_<language>(既存の言語ごとのテストを踏襲)
  • litsea/src/word_features.rs: ラウンドトリップのサンプルリストにケースを追加(低コストで、型コードのエンコーディングのバグを早期に検出できる)
  • litsea-cli/src/main.rs: --language のヘルプ文字列3箇所

モデルに依存するテスト(先に学習済みモデルが必要):

  • litsea/tests/golden.rs: 新しい golden_segment_<language>(POS モデルも学習した場合は golden_segment_with_pos_<language>_two_stage も)。理想化した出力ではなく、モデルの実際の出力を固定してください – まず実際の出力を println! で出力するテストを書き、その出力をアサーションにコピーしてから、デバッグ出力を削除します。直後にサボタージュ検証を行ってください: 期待値の1つを一時的に間違った値に変更し、テストが RED になることを確認してから、正しい値に戻します。作成時に一度も失敗しなかった golden テストは、何かを守っていることを証明していません
  • litsea/src/segmenter.rs: packed スコアラーと文字列キーの参照実装を比較する差分テスト(test_segment_differential_<language>_model)、およびモデルが tag-free の場合は segment_into のタイリング/パリティのケース
  • litsea/benches/bench.rs: 4つの言語ごとのタプルリスト(bench_segment_shortbench_external_corpus の2つのケースリスト、bench_segment_into)すべてに新しい言語を追加します。これには resources/ 配下にコーパスファイル(既存の言語ごとのコーパスと同程度のサイズのパブリックドメインのテキスト)が必要です
  • litsea-cli/tests/cli.rs: CLI の出力をエンドツーエンドで固定する分割のスモークテストを最低1つ

検証コマンド(言語の対応が完了したとみなす前に、すべて実行してください):

cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo bench -- external_corpus   # 新しいベンチケースが実行できることを確認し、剪定が必要か判断する
markdownlint-cli2 "docs/src/**/*.md"
markdownlint-cli2 "docs/ja/src/**/*.md"
mdbook build docs
mdbook build docs/ja

ドキュメント(本プロジェクトのドキュメント方針に従い、英語のソースを先に、その後 docs/ja/src/ 配下の日本語ミラーを更新します):

  • 新しい language-support/<language>.md ページ(英語または韓国語をテンプレートとして使用)と docs/src/SUMMARY.md のエントリ(および docs/ja/src/SUMMARY.md のミラー)
  • language-support/overview.mdalgorithm/character-type-classification.md – どちらも言語ごとのテーブル/セクションを拡張する必要があります
  • pre-trained-models.md – 同梱モデルごとのモデルカードに加え、該当する場合は「タグなし(pointwise)モデル」と「二段構成の品詞推定モデル」の比較表
  • ルートの README.mdlitsea/src/lib.rs のクレートレベルドキュメント
  • スイープが完了したとみなす前に grep -rln "<既存の言語名>" docs/src docs/ja/src を実行し、見つかった全ファイルを確認してください – 言語を列挙しているドキュメントは見落としやすいです(本プロジェクト自身の経験: issue #165 はこの確認を一度スキップしてしまい、14 箇所の古い記述が後続の PR でのクリーンアップ待ちになりました)

ライブラリ API 概要

litsea クレートは、単語分割、モデル学習、特徴量抽出のための Rust API を提供します。

インストール

[dependencies]
litsea = "0.13.0"

ローカルファイルからのモデル読み込みは同期 API(load_model_from_path)で行えるため、tokio などの非同期ランタイムは不要です。HTTP(S) からのリモートモデル取得など async API(load_model)を使う場合のみ、非同期ランタイムを追加してください(例えば TwoStageLearner::load_model も常に同じ非同期経路でモデル URI を解決するため、これに該当します)。

モジュール構成

graph LR
    A["litsea::segmenter"] --- B["Segmenter"]
    C["litsea::adaboost"] --- D["AdaBoost"]
    E["litsea::language"] --- F["Language"]
    G["litsea::extractor"] --- H["Extractor"]
    I["litsea::trainer"] --- J["Trainer, PerceptronTrainer, TwoStageTrainer, TwoStageMetrics"]
    K["litsea::error"] --- L["LitseaError, Result"]
    M["litsea::perceptron"] --- N["AveragedPerceptron"]
    O["litsea::upos"] --- P["Upos, SegmentLabel"]
    Q["litsea::metrics"] --- R["BinaryMetrics, MulticlassMetrics"]
    S["litsea::evaluation"] --- T["PosMetrics, SegmentationMetrics"]
    U["litsea::two_stage"] --- V["ModelKind, TwoStageFeatureSet, TwoStageLearner"]
モジュール主要な型用途
litsea::segmenterSegmenter, SegmentBuffer単語分割(所有出力またはアロケーションフリー出力)、二段構成の品詞推定付き分割
litsea::adaboostAdaBoost二値分類、モデルの入出力
litsea::perceptronAveragedPerceptron多クラス分類(二段構成の学習)、モデルの入出力
litsea::uposUpos, SegmentLabelUPOS 品詞タグ、セグメントラベル
litsea::languageLanguage言語定義、文字分類
litsea::extractorExtractorコーパスからの特徴量抽出
litsea::trainerTrainer, PerceptronTrainer, TwoStageTrainer, TwoStageMetrics学習パイプラインの制御
litsea::errorLitseaError, Resultエラー型と Result エイリアス
litsea::metricsBinaryMetrics, MulticlassMetrics学習結果の評価指標(in-sample)
litsea::evaluationPosMetrics, SegmentationMetricsgold コーパスに対する held-out 評価
litsea::two_stageModelKind, TwoStageFeatureSet, TwoStageLearner二段構成モデルのコンテナとモデル種別の判定
litsea::model_ioread_model_bytesモデル URI(パス・file://http(s)://)をバイト列へ解決する

主要な型はすべてクレートルートから再エクスポートされているため、use litsea::Segmenter;use litsea::segmenter::Segmenter; の短縮形として使えます。

各学習器は自身で URI を解決するため、model_io::read_model_bytes を直接使う場面はほとんどありません。これが公開されているのは、学習器を選ぶ前にモデルの中身を確認する必要がある呼び出し側のためです。litsea-binding-core はこれでバイト列を 1 度読み、ModelKind::detect で種別を判定し、同じバイト列を load_model_from_reader に渡すことで、リモートモデルを 2 回ダウンロードすることを避けています。

クイックスタート

use std::path::Path;

use litsea::adaboost::AdaBoost;
use litsea::language::Language;
use litsea::segmenter::Segmenter;

fn main() -> litsea::Result<()> {
    let mut learner = AdaBoost::new(0.01, 100);
    learner.load_model_from_path(Path::new("./models/RWCP.model"))?;

    let segmenter = Segmenter::with_learner(Language::Japanese, learner);
    let tokens = segmenter.segment("これはテストです。");

    assert_eq!(tokens, vec!["これ", "は", "テスト", "です", "。"]);
    Ok(())
}

クイックスタート(品詞推定)

use std::path::Path;

use litsea::language::Language;
use litsea::segmenter::Segmenter;
use litsea::two_stage::TwoStageLearner;

fn main() -> litsea::Result<()> {
    let mut learner = TwoStageLearner::new();
    learner.load_model_from_path(Path::new("./models/japanese_pos.model"))?;

    let segmenter = Segmenter::with_two_stage_learner(Language::Japanese, learner);
    let tokens = segmenter.segment_with_pos("これはテストです。")?;

    for (word, pos) in &tokens {
        print!("{}/{} ", word, pos);
    }
    println!();

    Ok(())
}

完全な API ドキュメントは docs.rs/litsea で参照できます。

Segmenter

Segmenter 構造体は、単語分割のための主要なインターフェースです。

定義

#![allow(unused)]
fn main() {
pub struct Segmenter {
    // private: language: Language,
    // private: learner: AdaBoost,
    // private: two_stage: Option<PackedTwoStageModel>(コンパイル済み stage-2 モデル)
    // internal: packed cache (see below)
}
}

フィールドは非公開です。アクセサメソッド language()learner()learner_mut() を使ってアクセスしてください。

これらに加えて、構造体は packed も保持しています。これは学習器の重みを segment() が採点に使う整数インデックスのテーブル群へコンパイルした、遅延再構築されるキャッシュです(予測パイプラインを参照)。これは内部実装の詳細でありアクセサはなく、学習器が変更されると自動的に無効化されます。two_stagewith_two_stage_learner(後述)が設定するコンパイル済み stage-2 タグ付けモデルを保持します。二段構成モデルから作成された Segmenter でなければ None です。キャッシュとは異なり、保持された学習器から導出されるものではありません — 生の stage-2 パーツはコンパイル後に破棄され、このフィールドへの変更経路は存在しません。

コンストラクタ

Segmenter::new

#![allow(unused)]
fn main() {
pub fn new(language: Language) -> Self
}

デフォルト(未学習)の AdaBoost 学習器を持つ Segmenter を作成します。学習や特徴量抽出に適しています。モデルが読み込まれるか学習データが追加されるまでは、segment は文字ごとに1単語を返します。二段構成モデルは未設定のままで、segment_with_posErr(LitseaError::PosLearnerNotSet) を返します — 単語分割と品詞タグ付けを行うには with_two_stage_learner を使用してください。

Segmenter::with_learner

#![allow(unused)]
fn main() {
pub fn with_learner(language: Language, learner: AdaBoost) -> Self
}

指定した学習器を使って Segmenter を作成します。通常は学習済みモデルを読み込んだ学習器を渡します。

#![allow(unused)]
fn main() {
use litsea::language::Language;
use litsea::segmenter::Segmenter;

// With a pre-trained model
let segmenter = Segmenter::with_learner(Language::Japanese, learner);

// Without a model (for training or feature extraction)
let segmenter = Segmenter::new(Language::Japanese);
}

メソッド

segment

#![allow(unused)]
fn main() {
pub fn segment(&self, sentence: &str) -> Vec<String>
}

文を単語に分割します。空の入力に対しては空のベクターを返します。

#![allow(unused)]
fn main() {
let tokens = segmenter.segment("これはテストです。");
// ["これ", "は", "テスト", "です", "。"]
}

内部的には segment_into の薄いラッパーで、 呼び出しごとに新しいバッファを使い、各 range を所有 String として 実体化します — 採点実装は 1 つだけです。

segment_into / SegmentBuffer

#![allow(unused)]
fn main() {
pub struct SegmentBuffer { /* 内部のスクラッチ + 出力ストレージ */ }

impl SegmentBuffer {
    pub fn new() -> Self
}

impl Segmenter {
    pub fn segment_into<'b>(
        &self,
        sentence: &str,
        buf: &'b mut SegmentBuffer,
    ) -> &'b [(usize, usize)]
}
}

segment のアロケーションフリー版です(issue #184)。返される各 (start, end) ペアは sentence へのバイト範囲(&sentence[start..end] がトークン)で、出現順に並び、文を過不足なく敷き詰めます。バッファは 呼び出しごとに必要なアロケーション(コンテキスト配列・スコアバッファ・ タグスクラッチ・出力 range)をすべて所有するため、バッチ処理で 1 つの バッファを使い回すと、定常状態では分割が一切アロケートしなくなります。 空の入力には空のスライスを返します。

公開スループット水準ではこれが効きます: segment はトークンごとに String を 1 つ確保し(バッチ処理では毎秒数百万個)、さらに呼び出し ごとのスクラッチも確保します — 計測ではバッチプロファイルの約 1/4 が アロケータ系でした。バッファは借用を持たないプレーンなデータなので、 文・モデル・言語をまたいで再利用できます。並列処理ではスレッドごとに 1 つのバッファを使ってください。

#![allow(unused)]
fn main() {
use litsea::segmenter::{SegmentBuffer, Segmenter};

let mut buf = SegmentBuffer::new();
for line in lines {
    for &(start, end) in segmenter.segment_into(line, &mut buf) {
        let token: &str = &line[start..end];
        // アロケーションなしでトークンを参照・出力
    }
}
}

char_type

#![allow(unused)]
fn main() {
pub fn char_type(&self, c: char) -> &'static str
}

文字を言語固有の文字種コードに分類します(Language::char_type に委譲します)。

#![allow(unused)]
fn main() {
let segmenter = Segmenter::new(Language::Japanese);
assert_eq!(segmenter.char_type('あ'), "I");  // Hiragana
assert_eq!(segmenter.char_type('漢'), "H");  // Kanji
assert_eq!(segmenter.char_type('A'), "A");   // ASCII
}

add_corpus

#![allow(unused)]
fn main() {
pub fn add_corpus(&mut self, corpus: &str)
}

スペース区切りのコーパスを処理し、内部の AdaBoost 学習器にインスタンスを追加します。

#![allow(unused)]
fn main() {
let mut segmenter = Segmenter::new(Language::Japanese);
segmenter.add_corpus("テスト です");
}

add_corpus_with_writer

#![allow(unused)]
fn main() {
pub fn add_corpus_with_writer<F>(&self, corpus: &str, writer: F)
where
    F: FnMut(HashSet<String>, i8),
}

コーパスを処理し、各文字位置の特徴量セットとラベルをコールバックに渡します。

#![allow(unused)]
fn main() {
segmenter.add_corpus_with_writer("テスト です", |attrs, label| {
    println!("Features: {:?}, Label: {}", attrs, label);
});
}

add_corpus_tsv / add_corpus_tsv_with_writer

#![allow(unused)]
fn main() {
pub fn add_corpus_tsv(&mut self, corpus: &str)
pub fn add_corpus_tsv_with_writer<F>(&self, corpus: &str, writer: F)
where
    F: FnMut(HashSet<String>, i8),
}

add_corpus / add_corpus_with_writer のタブ区切りバリアントです。トークンはタブ文字で区切られ、トークンとして空白文字そのもの(" ")を含められます。これにより学習テキスト内に元の文の空白が保持され、モデルは空白文字を境界のコンテキストとして学習できます(韓国語モデルおよび英語モデルで使用。issue #152)。

#![allow(unused)]
fn main() {
let mut segmenter = Segmenter::new(Language::Korean);
segmenter.add_corpus_tsv("나는\t \t고양이");
}

アクセサ

#![allow(unused)]
fn main() {
pub fn language(&self) -> Language
pub fn learner(&self) -> &AdaBoost
pub fn learner_mut(&mut self) -> &mut AdaBoost
}

Segmenter の言語と内部の学習器(二段構成の Segmenter では stage-1 境界分類器)へのアクセスを提供します。

文字位置ごとの特徴量抽出(韓国語/英語では38個、日本語・中国語では42個の特徴量)は内部実装の詳細です。以前の get_attributes メソッドは非公開になりました。

POS-Mode API

Segmenter は、二段構成モデル(issue #147)を使った 単語分割と品詞タグ付け もサポートしています。

with_two_stage_learner

#![allow(unused)]
fn main() {
pub fn with_two_stage_learner(language: Language, learner: TwoStageLearner) -> Self
}

二段構成モデル(litsea-two-stage v1 ファイルを読み込んだ TwoStageLearner)を持つ Segmenter を作成します。モデルの stage-1 境界分類器はそのまま Segmenter の AdaBoost 経路の学習器になり(segment は自然に動作します)、segment_with_pos は 分割された各単語を候補タグ語彙表(lexicon)でタグ付けします — 単一候補・優勢候補の 表層は分類器を完全にスキップし、曖昧な表層は候補マスク付き argmax、未知の表層は 全クラスの argmax を stage-2 の単語単位タガーが決定します。 二段構成形式についてはモデルファイル形式を 参照してください。

segment_with_pos

#![allow(unused)]
fn main() {
pub fn segment_with_pos(&self, sentence: &str) -> Result<Vec<(String, Upos)>>
}

stage-1 境界分類器で文を単語に分割し(segment と全く同じ)、二段構成のタグ付け経路で各単語に UPOS タグを付与します。空の文に対しては、空のベクターを持つ Ok を返します。

エラー: 二段構成学習器が設定されていない場合は LitseaError::PosLearnerNotSet を返します — まず with_two_stage_learner() で Segmenter を作成してください。

#![allow(unused)]
fn main() {
use std::path::Path;

use litsea::language::Language;
use litsea::segmenter::Segmenter;
use litsea::two_stage::TwoStageLearner;

let mut learner = TwoStageLearner::new();
learner.load_model_from_path(Path::new("./models/japanese_pos.model"))?;

let segmenter = Segmenter::with_two_stage_learner(Language::Japanese, learner);
let tokens = segmenter.segment_with_pos("これはテストです。")?;
// [("これ", Upos::PRON), ("は", Upos::ADP), ("テスト", Upos::NOUN),
//  ("です", Upos::AUX), ("。", Upos::PUNCT)]
}

add_corpus_with_pos_writer

#![allow(unused)]
fn main() {
pub fn add_corpus_with_pos_writer<F>(&self, corpus: &str, writer: F)
where
    F: FnMut(HashSet<String>, SegmentLabel)
}

POS タグ付きコーパス(word/POS word/POS ...)の文字レベル学習用特徴量を、最初の位置を含めて、Segmenter を変更することなくカスタムライターへストリーミングします。これは Extractor::extract_two_stage が stage-1 境界特徴量を構築する際の基盤となっています。

Extractor

Extractor 構造体は、モデル学習用にコーパスファイルから特徴量を抽出します。

定義

#![allow(unused)]
fn main() {
pub struct Extractor {
    segmenter: Segmenter,
}
}

コンストラクタ

Extractor::new

#![allow(unused)]
fn main() {
pub fn new(language: Language) -> Self
}

指定した言語に対応する新しい Extractor を作成します。内部的に、学習済みモデルを持たない Segmenter を作成します。ExtractorDefault も実装しており、Extractor::new(Language::Japanese) と等価です。

#![allow(unused)]
fn main() {
use litsea::extractor::Extractor;
use litsea::language::Language;

let extractor = Extractor::new(Language::Japanese);
}

抽出メソッドは &self を取るため、束縛を可変(mut)にする必要はありません。

メソッド

extract

#![allow(unused)]
fn main() {
pub fn extract(
    &self,
    corpus_path: &Path,
    features_path: &Path,
) -> litsea::Result<()>
}

コーパスファイル(スペース区切りの単語、1行1文)を読み込み、抽出した特徴量を出力ファイルに書き込みます。

#![allow(unused)]
fn main() {
use std::path::Path;

extractor.extract(
    Path::new("./corpus.txt"),
    Path::new("./features.txt"),
)?;
}

パイプライン

flowchart LR
    A["corpus.txt<br/>(space-separated words)"] --> B["Extractor::extract()"]
    B --> C["features.txt<br/>(label + features per position)"]

Extractor は以下の処理を行います:

  1. コーパスファイルから各行を読み込む
  2. Segmenter::add_corpus_with_writer() を呼び出して各行を処理する
  3. 各文字位置のラベルと特徴量セットを出力ファイルに書き込む

extract_tsv

#![allow(unused)]
fn main() {
pub fn extract_tsv(
    &self,
    corpus_path: &Path,
    features_path: &Path,
) -> litsea::Result<()>
}

タブ区切りのコーパスファイル(トークンをタブで区切り、1行1文。トークンとして空白文字そのもの(" ")を含められます)を読み込み、抽出した特徴量を書き込みます。保持された空白により、モデルは空白文字を境界のコンテキストとして学習できます — 韓国語モデルおよび英語モデルの学習に使用されています(issue #152)。出力形式は extract と同一です。

#![allow(unused)]
fn main() {
use std::path::Path;

extractor.extract_tsv(
    Path::new("./ko_corpus.tsv"),
    Path::new("./ko_features.txt"),
)?;
}

extract_tag_free / extract_tsv_tag_free

#![allow(unused)]
fn main() {
pub fn extract_tag_free(
    &self,
    corpus_path: &Path,
    features_path: &Path,
) -> litsea::Result<()>

pub fn extract_tsv_tag_free(
    &self,
    corpus_path: &Path,
    features_path: &Path,
) -> litsea::Result<()>
}

extract / extract_tsv のタグなし版です(issue #183): 入力・出力形式は 同一ですが、16 個のタグ依存テンプレート(UP*/BP*/UQ*/BQ*/TQ*。 直前の境界判定結果を参照する)を全行から除外します。この特徴量で学習した モデルは pointwise になり、segment() は逐次スコアリングパスを丸ごと スキップします。同梱の korean.model/english.model はこの方法で学習されています。 言語別の品質・速度トレードオフの実測値は タグなし(pointwise)モデル を参照してください。CLI の extract --tag-free の実体です。

extract_two_stage

#![allow(unused)]
fn main() {
pub fn extract_two_stage(
    &self,
    corpus_path: &Path,
    output_prefix: &Path,
    feature_set: TwoStageFeatureSet,
) -> litsea::Result<()>
}

POS タグ付きコーパス(word/POS word/POS ...、1行1文、POS タグは UPOS タグセット)を1パスで読み込み、二段構成モデルの学習に使う TwoStageTrainer 用の3ファイルを output_prefix から書き出します。

  • {output_prefix}.stage1 – 境界特徴量(label\tfeature1\t...、ラベルは B または O)。通常の抽出と同じ文字レベルの特徴量テンプレートを使用し、先頭位置を含む全位置で出力
  • {output_prefix}.stage2 – 単語単位の特徴量(label\tfeature1\t...、ラベルは UPOS タグ)。feature_set で選択したテンプレートを使用(詳細は下記の TwoStageFeatureSet
  • {output_prefix}.lexicon – 候補タグ語彙表(surface\tTAG:count[,TAG:count...]、出現頻度の高い順)

TwoStageTrainer::new は同じプレフィックスから同じ3つのパスを読み込みます。

#![allow(unused)]
fn main() {
use std::path::Path;

use litsea::TwoStageFeatureSet;

extractor.extract_two_stage(
    Path::new("./pos_corpus.txt"),
    Path::new("./pos_features"),
    TwoStageFeatureSet::Fast,
)?;
}

in-memory での抽出

extract* の各メソッドには *_to_writer の対応版があり、コーパスを文字列で受け取り、特徴量行を任意の Write へ書き出します。ファイルシステムのない環境(WebAssembly)や、コーパスが既にメモリ上にある場合のためのものです。出力はパス版とバイト単位で一致します。

パス版in-memory 版
extract(corpus_path, features_path)extract_to_writer(corpus, writer)
extract_tsvextract_tsv_to_writer
extract_tag_freeextract_tag_free_to_writer
extract_tsv_tag_freeextract_tsv_tag_free_to_writer
extract_two_stage(corpus_path, prefix, feature_set)extract_two_stage_to_writers(corpus, stage1, stage2, lexicon, feature_set)
extract_two_stage_tsvextract_two_stage_tsv_to_writers
#![allow(unused)]
fn main() {
use litsea::{Extractor, Language};

let extractor = Extractor::new(Language::Japanese);
let corpus = "これ は テスト です 。\n";

let mut features = Vec::new();
extractor.extract_to_writer(corpus, &mut features)?;
}

二段構成版は、パス版が {prefix}.stage1.stage2.lexicon に書き出す 3 つの出力をそのまま writer へ書きます。

#![allow(unused)]
fn main() {
let (mut stage1, mut stage2, mut lexicon) = (Vec::new(), Vec::new(), Vec::new());
extractor.extract_two_stage_to_writers(
    corpus,
    &mut stage1,
    &mut stage2,
    &mut lexicon,
    TwoStageFeatureSet::Fast,
)?;
}

結果は TwoStageTrainer::from_features に渡せます。

パス版は wasm32-unknown-unknown では利用できません(ファイルシステムが無いため)。*_to_writer 版はすべてのターゲットで利用できます。

TwoStageFeatureSet

#![allow(unused)]
fn main() {
pub enum TwoStageFeatureSet {
    Full,
    Balanced,
    #[default]
    Fast,
}
}

extract_two_stage が書き出す stage-2 の単語単位テンプレートを選択します(テンプレートの全カタログは単語単位の特徴量テンプレートを参照)。タグ付け品質とスループットのトレードオフになります:

  • Full – すべての単語テンプレート(品質重視)
  • BalancedFast のテンプレートに加えて、先頭/末尾文字そのものと単語の文字種文字列
  • Fast(既定) – 実測に基づく最小構成: 表層、単語長、先頭/末尾文字種、隣接文脈文字とその文字種、2文字の接頭辞/接尾辞

Display(小文字: "full""balanced""fast")と FromStr(不正な文字列には ParseTwoStageFeatureSetError を返す)も実装しています – これは --stage2-features CLI フラグが受け付けるのと同じ名前です。特徴量の抽出を参照してください。

Trainer

Trainer 構造体は、モデル学習パイプライン全体を制御します。

定義

#![allow(unused)]
fn main() {
pub struct Trainer {
    learner: AdaBoost,
}
}

コンストラクタ

Trainer::new

#![allow(unused)]
fn main() {
pub fn new(
    threshold: f64,
    num_iterations: usize,
    features_path: &Path,
) -> litsea::Result<Self>
}

Trainer を作成し、特徴量ファイルから初期化します。内部で AdaBoost::initialize_features()AdaBoost::initialize_instances() を呼び出します。

#![allow(unused)]
fn main() {
use std::path::Path;
use litsea::trainer::Trainer;

let mut trainer = Trainer::new(
    0.0001,                          // threshold
    20000,                           // max iterations
    Path::new("./features.txt"),     // features file
)?;
}

メソッド

load_model

#![allow(unused)]
fn main() {
pub async fn load_model(&mut self, uri: &str) -> litsea::Result<()>
}

再学習用に既存のモデルを読み込みます。ファイルパス、file://、および(remote_model フィーチャー有効時)http://https:// URI に対応しています。

Trainer::new の後に呼び出すと、読み込んだ重みは特徴名をキーとして、初期化済みの学習データにマージされます。そのため、特徴量インデックスを壊すことなく、既存モデルから増分学習を開始できます。

#![allow(unused)]
fn main() {
trainer.load_model("./models/japanese.model").await?;
}

train

#![allow(unused)]
fn main() {
pub fn train(
    &mut self,
    running: &AtomicBool,
    model_path: &Path,
) -> litsea::Result<BinaryMetrics>
}

モデルを学習し、指定したパスに保存します。評価メトリクスを返します。

running フラグにより、学習の途中停止が可能です。false に設定すると学習を早期終了します。

#![allow(unused)]
fn main() {
use std::sync::atomic::AtomicBool;
use std::path::Path;

let running = AtomicBool::new(true);
let metrics = trainer.train(&running, Path::new("./model.model"))?;

println!("Accuracy: {:.2}%", metrics.accuracy);
}

学習の完全な例

use std::sync::atomic::AtomicBool;
use std::path::Path;

use litsea::trainer::Trainer;

#[tokio::main]
async fn main() -> litsea::Result<()> {
    let mut trainer = Trainer::new(
        0.0001,
        20000,
        Path::new("./features.txt"),
    )?;

    // Optionally resume from an existing model
    // trainer.load_model("./models/japanese.model").await?;

    let running = AtomicBool::new(true);
    let metrics = trainer.train(&running, Path::new("./model.model"))?;

    println!("Accuracy:  {:.2}%", metrics.accuracy);
    println!("Precision: {:.2}%", metrics.precision);
    println!("Recall:    {:.2}%", metrics.recall);

    Ok(())
}

PerceptronTrainer

PerceptronTrainerTrainer の汎用 Averaged Perceptron 版です。特徴量ファイルから、不透明な文字列ラベルに対する多クラスの Averaged Perceptron を学習します(litsea train --perceptron)。主な用途は、畳み込みレシピ(事前学習済みモデルを参照)が同梱の AdaBoost 形式分割モデルへ変換する、2 クラス(B/O)境界パーセプトロンの学習です。

PerceptronTrainer::new

#![allow(unused)]
fn main() {
pub fn new(num_epochs: usize, features_path: &Path) -> litsea::Result<Self>
}

特徴量ファイル(各行が label\tfeature1\tfeature2\t... の形式で、ラベルは不透明な文字列。例: 境界ラベル B/O)を読み込み、学習インスタンスを登録します。

PerceptronTrainer::load_model

#![allow(unused)]
fn main() {
pub async fn load_model(&mut self, model_uri: &str) -> litsea::Result<()>
}

既存のパーセプトロンモデルを読み込み、増分学習を行います。学習データからすでに登録済みのクラスは、モデルのクラスとマージされます。

PerceptronTrainer::train

#![allow(unused)]
fn main() {
pub fn train(
    &mut self,
    running: &AtomicBool,
    model_path: &Path,
) -> litsea::Result<MulticlassMetrics>
}

設定されたエポック数だけ学習を行い、モデルを保存して、多クラス評価メトリクス(正解率、マクロ平均適合率、マクロ平均再現率)を返します。running フラグは Trainer::train と同様に、学習の途中停止を可能にします。

use std::sync::atomic::AtomicBool;
use std::path::Path;

use litsea::trainer::PerceptronTrainer;

#[tokio::main]
async fn main() -> litsea::Result<()> {
    let mut trainer = PerceptronTrainer::new(10, Path::new("./features.txt"))?;
    let running = AtomicBool::new(true);
    let metrics = trainer.train(&running, Path::new("./perceptron.model"))?;
    println!("Accuracy: {:.2}%", metrics.accuracy);
    Ok(())
}

TwoStageTrainer

TwoStageTrainer二段構成モデル (issue #147)を学習します: 二値の境界分類器(stage 1)と単語単位の マルチクラスタガー(stage 2)を、いずれも Averaged Perceptron として 学習し、候補タグ語彙表とともに単一の litsea-two-stage v1 ファイルに 組み立てます。学習後、stage 1 は既存の AdaBoost 形式のスカラー特徴量重みに 畳み込まれます(品質を損なわない変換 – 導出はこのモジュールのソース ドキュメントを参照)。これによりランタイムは通常の segment() モデルと 全く同じ方法で採点します。TwoStageTrainerTwoStageMetrics はどちらも クレートのルートから litsea::TwoStageTrainer / litsea::TwoStageMetrics として再エクスポートされています。

TwoStageTrainer::new

#![allow(unused)]
fn main() {
pub fn new(
    num_epochs: usize,
    dominance: f64,
    features_prefix: &Path,
) -> litsea::Result<Self>
}

Extractor::extract_two_stage が書き出す 3 つのファイルを features_prefix から読み込みます({prefix}.stage1{prefix}.stage2{prefix}.lexicon)。両ステージ分の学習インスタンスを登録します。

dominance は、組み立て後のモデルにおける分類器スキップの閾値です: 既知の単語のうち最頻タグが学習時の出現のこの割合以上を占めるものは、 stage-2 分類器を呼ばずにタグ付けされます。値は (0.5, 1.0] の範囲内で なければならず、new() の時点で即座に検証されます。そのため範囲外の値は 学習が始まる前に失敗し、学習後に失敗することはありません。

#![allow(unused)]
fn main() {
use std::path::Path;
use litsea::trainer::TwoStageTrainer;

let trainer = TwoStageTrainer::new(
    50,                            // num_epochs(両ステージ共通)
    0.99,                          // dominance
    Path::new("./features"),       // 特徴量プレフィックス
)?;
}

TwoStageTrainer::train

#![allow(unused)]
fn main() {
pub fn train(
    mut self,
    running: &AtomicBool,
    model_path: &Path,
) -> litsea::Result<TwoStageMetrics>
}

Trainer::trainPerceptronTrainer::train と異なり、このメソッドは self を 値として受け取ります(Trainer を消費します)。両ステージを Averaged Perceptron として num_epochs エポック分学習し、stage 1 を AdaBoost の 重みへ畳み込み、語彙表とともに 2 つのステージを litsea-two-stage v1 モデルへ組み立てて model_path に保存し、両ステージの in-sample メトリクスを返します。running フラグは他の Trainer と同様に、学習の 途中停止を可能にします。

use std::sync::atomic::AtomicBool;
use std::path::Path;

use litsea::trainer::TwoStageTrainer;

#[tokio::main]
async fn main() -> litsea::Result<()> {
    let trainer = TwoStageTrainer::new(50, 0.99, Path::new("./features"))?;
    let running = AtomicBool::new(true);
    let metrics = trainer.train(&running, Path::new("./model.model"))?;

    println!("Stage 1: {:.2}%, Stage 2: {:.2}%", metrics.stage1.accuracy, metrics.stage2.accuracy);

    Ok(())
}

TwoStageMetrics

#![allow(unused)]
fn main() {
pub struct TwoStageMetrics {
    pub stage1: MulticlassMetrics,
    pub stage2: MulticlassMetrics,
}
}

TwoStageTrainer::train の実行結果である in-sample メトリクスです。 stage1 は境界分類器の 2 クラス(B/O)に対するメトリクス、stage2 は 単語単位のタガーの UPOS タグクラスに対するメトリクスです。どちらのフィールドも MulticlassMetrics 型で、PerceptronTrainer::train(上記)が返すものと 同じ型であり、正解率とマクロ平均の適合率・再現率を保持します。

in-memory での学習

各トレーナには、パス版のコンストラクタと train に対応する in-memory 版があります。これによりファイルシステムなしでパイプライン全体を実行できます(パス版がコンパイル対象外になる wasm32-unknown-unknown など)。

パス版in-memory 版
Trainer::new(threshold, iterations, features_path)Trainer::from_features(threshold, iterations, features)
PerceptronTrainer::new(epochs, features_path)PerceptronTrainer::from_features(epochs, features)
TwoStageTrainer::new(epochs, dominance, prefix)TwoStageTrainer::from_features(epochs, dominance, stage1, stage2, lexicon)
train(running, model_path)train_to_writer(running, writer)
load_model(uri).awaitload_model_from_reader(reader)
#![allow(unused)]
fn main() {
use litsea::{Extractor, Language, Trainer};
use std::sync::atomic::AtomicBool;

let corpus = "これ は テスト です 。\n";

let mut features = Vec::new();
Extractor::new(Language::Japanese).extract_to_writer(corpus, &mut features)?;
let features = String::from_utf8(features).expect("features are UTF-8");

let mut model = Vec::new();
let metrics = Trainer::from_features(0.01, 10_000, &features)?
    .train_to_writer(&AtomicBool::new(true), &mut model)?;
}

特徴量を reader ではなく &str で受け取るのは、AdaBoost が特徴量を 2 回走査するためです(1 回目で特徴量の語彙を構築し、2 回目でそれに対するインスタンスを構築する)。

両経路はバイト単位で同一のモデルを生成し、それをクレートのテストが検証しています。

分割モデルを直接学習する

AdaBoost のパイプラインでは、特徴量ファイルを経由せずに学習器の API だけで完結します。

#![allow(unused)]
fn main() {
use litsea::{AdaBoost, Language, Segmenter};

let mut learner = AdaBoost::new(0.01, 10_000);
let segmenter = Segmenter::new(Language::Japanese);
segmenter.add_corpus_with_writer(corpus, |attrs, label| learner.add_instance(attrs, label));
learner.train(&AtomicBool::new(true));
learner.save_model_to_writer(&mut model)?;
}

新規の学習器であればこれは等価です。ただし読み込み済みモデルからの追加学習では等価になりませんTrainer の 2 パス経路は各インスタンスのブースティング重みを既存モデルから算出しますが、add_instance はすべて 1.0 から始めます。

再現性

学習は入力だけの関数です。同じ特徴量から 2 回学習すれば同じモデルになります。これは AveragedPerceptron::add_instance が特徴量をソートして保持するためです。HashSet の反復順は集合ごとに異なり、パーセプトロンの更新は順序に依存するため、これ以前は同一プロセス内の 2 回の学習が一致しないことがありました。

AdaBoost

AdaBoost 構造体は、単語境界検出のための二値分類を実装しています。

定義

#![allow(unused)]
fn main() {
pub struct AdaBoost {
    // private: threshold: f64, num_iterations: usize
    // (read via threshold() / num_iterations())
    // internal fields: model weights, features, instances, etc.
}
}

コンストラクタ

AdaBoost::new

#![allow(unused)]
fn main() {
pub fn new(threshold: f64, num_iterations: usize) -> Self
}

指定したハイパーパラメータで新しい AdaBoost インスタンスを作成します。

#![allow(unused)]
fn main() {
use litsea::adaboost::AdaBoost;

let mut learner = AdaBoost::new(0.01, 100);
}

AdaBoost::default

AdaBoostDefault も実装しており、AdaBoost::new(0.01, 100) と等価です – これはライブラリと CLI 全体で使われるデフォルトのハイパーパラメータです。

アクセサ

threshold

#![allow(unused)]
fn main() {
pub fn threshold(&self) -> f64
}

この学習器の作成時に指定した早期停止の閾値を返します。

num_iterations

#![allow(unused)]
fn main() {
pub fn num_iterations(&self) -> usize
}

ブースティング反復の最大回数を返します。

モデルの読み込み

load_model_from_path

#![allow(unused)]
fn main() {
pub fn load_model_from_path(&mut self, path: &Path) -> litsea::Result<()>
}

ローカルファイルからモデルの重みを同期的に読み込みます。不正な形式のファイル(空、bias 行の欠落、bias 行や特徴量の重複、非有限の重み)は LitseaError::InvalidData で拒否されます。ローカルファイルにはこのメソッドが推奨されます – 非同期ランタイムは不要です。

#![allow(unused)]
fn main() {
use std::path::Path;

learner.load_model_from_path(Path::new("./models/japanese.model"))?;
}

load_model_from_reader

#![allow(unused)]
fn main() {
pub fn load_model_from_reader<R: BufRead>(&mut self, reader: R) -> litsea::Result<()>
}

メモリ上のバッファや既に開いているファイルなど、任意の BufRead ソースからモデルの重みを読み込みます。

load_model

#![allow(unused)]
fn main() {
pub async fn load_model(&mut self, uri: &str) -> litsea::Result<()>
}

URI からモデルの重みを読み込みます。以下の形式に対応しています:

  • ローカルファイルパス: ./models/japanese.model
  • File URI: file:///path/to/model
  • HTTP: http://example.com/modelremote_model フィーチャーが必要)
  • HTTPS: https://example.com/modelremote_model フィーチャーが必要)
#![allow(unused)]
fn main() {
learner.load_model("https://example.com/model").await?;
}

save_model

#![allow(unused)]
fn main() {
pub fn save_model(&self, filename: &Path) -> litsea::Result<()>
}

モデルの重みをファイルに保存します。モデルが空の場合はエラーを返します。

save_model_to_writer

#![allow(unused)]
fn main() {
pub fn save_model_to_writer<W: Write>(&self, writer: &mut W) -> litsea::Result<()>
}

save_model と同じテキスト形式で、任意のライターへモデルを書き込みます。save_model はこのメソッドに処理を委譲しています。ファイルパスを経由せずにモデルをより大きなファイルの一部として埋め込めるよう公開されています – 二段構成モデル形式は、これを使って stage-1 の AdaBoost モデルを直接埋め込んでいます。ライターはフラッシュされません。モデルが空の場合はエラーを返します。

学習メソッド

initialize_features

#![allow(unused)]
fn main() {
pub fn initialize_features(&mut self, filename: &Path) -> litsea::Result<()>
}

特徴量ファイルを読み込み、特徴量インデックスを構築します。initialize_instances の前に呼び出す必要があります。

initialize_instances

#![allow(unused)]
fn main() {
pub fn initialize_instances(&mut self, filename: &Path) -> litsea::Result<()>
}

同じ特徴量ファイルを読み込み、ラベル付きインスタンスとその重みを初期化します。

train

#![allow(unused)]
fn main() {
pub fn train(&mut self, running: &AtomicBool)
}

AdaBoost の学習ループを実行します。runningfalse に設定すると早期終了します。

add_instance

#![allow(unused)]
fn main() {
pub fn add_instance(&mut self, attributes: HashSet<String>, label: i8)
}

特徴量セットとラベルを持つ単一の学習インスタンスを追加します。

予測

predict

#![allow(unused)]
fn main() {
pub fn predict(&self, attributes: &HashSet<String>) -> i8
}

与えられた特徴量セットに対してラベルを予測します。+1(境界)または -1(非境界)を返します。

#![allow(unused)]
fn main() {
use std::collections::HashSet;

let mut attrs = HashSet::new();
attrs.insert("UW4:は".to_string());
attrs.insert("UC4:I".to_string());
// ... more features

let label = learner.predict(&attrs);
// label == 1 (boundary) or -1 (non-boundary)
}

bias

#![allow(unused)]
fn main() {
pub fn bias(&self) -> f64
}

バイアス項を返します: -sum(all model weights) / 2.0。この値はキャッシュされ、重みを変更するすべての経路で同期されるため、この呼び出しは O(1) です。

評価

metrics

#![allow(unused)]
fn main() {
pub fn metrics(&self) -> BinaryMetrics
}

学習データに対する評価メトリクスを算出します。

BinaryMetrics

litsea::metrics で定義されています(litsea::BinaryMetrics としても再エクスポートされます):

#![allow(unused)]
fn main() {
pub struct BinaryMetrics {
    pub accuracy: f64,          // Accuracy in percentage
    pub precision: f64,         // Precision in percentage
    pub recall: f64,            // Recall in percentage
    pub num_instances: usize,
    pub true_positives: usize,
    pub false_positives: usize,
    pub false_negatives: usize,
    pub true_negatives: usize,
}
}

Averaged Perceptron

AveragedPerceptron 構造体は、不透明な文字列ラベルに対する多クラス分類を実装しています。二段構成品詞推定アーキテクチャの両ステージと、同梱分割モデルの畳み込みレシピ(litsea train --perceptron)の学習側の学習器です。

定義

#![allow(unused)]
fn main() {
pub struct AveragedPerceptron {
    // internal fields: slots (feature -> per-class weights + averaging state), step, classes, instances
}
}

コンストラクタ

AveragedPerceptron::new

#![allow(unused)]
fn main() {
pub fn new() -> Self
}

新しい Averaged Perceptron インスタンスを作成します。

#![allow(unused)]
fn main() {
use litsea::perceptron::AveragedPerceptron;

let mut learner = AveragedPerceptron::new();
}

インスタンスの追加

add_instance

#![allow(unused)]
fn main() {
pub fn add_instance(&mut self, features: HashSet<String>, label: String)
}

特徴量セットとラベルを持つ学習インスタンスを追加します。未知のクラスは自動的に登録されます。

#![allow(unused)]
fn main() {
use std::collections::HashSet;
use litsea::perceptron::AveragedPerceptron;

let mut learner = AveragedPerceptron::new();
let mut feats = HashSet::new();
feats.insert("UW4:猫".to_string());
feats.insert("UC4:H".to_string());
learner.add_instance(feats, "B-NOUN".to_string());
}

学習

train

#![allow(unused)]
fn main() {
pub fn train(&mut self, num_epochs: usize, running: &AtomicBool)
}

指定されたエポック数でモデルを学習します。runningfalse に設定すると早期終了します。学習終了時に重みの平均化が自動的に行われます。

#![allow(unused)]
fn main() {
use std::sync::atomic::AtomicBool;

let running = AtomicBool::new(true);
learner.train(10, &running);
}

予測

predict

#![allow(unused)]
fn main() {
pub fn predict(&self, features: &HashSet<String>) -> String
}

与えられた特徴量セットに対してラベルを予測します。各クラスのスコアを計算し、最大スコアのクラス名を返します。クラスが未登録の場合は空文字列を返します。

#![allow(unused)]
fn main() {
use std::collections::HashSet;

let mut attrs = HashSet::new();
attrs.insert("UW4:は".to_string());
attrs.insert("UC4:I".to_string());
// ... more features

let label = learner.predict(&attrs);
// label == "B-ADP", "O", etc.
}

アクセサ

classes

#![allow(unused)]
fn main() {
pub fn classes(&self) -> &[String]
}

登録されているクラス名を、格納順(重みベクトルのインデックスや predict の argmax タイブレーク〔先勝ちの strictly-greater〕で使われる並び)で返します。クラスが未登録の場合は空です。二段構成モデルへの畳み込み手順(事前学習済みモデルを参照)や、packed 二段構成ランタイムで使用されています。

モデルの入出力

save_model

#![allow(unused)]
fn main() {
pub fn save_model(&self, path: &Path) -> litsea::Result<()>
}

モデルをファイルに保存します。モデルが空の場合はエラーを返します。

save_model_to_writer

#![allow(unused)]
fn main() {
pub fn save_model_to_writer<W: Write>(&self, writer: &mut W) -> litsea::Result<()>
}

save_model と同じテキスト形式で、任意のライターへモデルを書き込みます。save_model はこのメソッドに処理を委譲しています。ファイルパスを経由せずにモデルをより大きなファイルの一部として埋め込めるよう公開されています – 二段構成モデル形式は、これを使って stage-2 の単語タガーを直接埋め込んでいます。ライターはフラッシュされません。クラスが未登録の場合(空のモデル)はエラーを返します。

load_model_from_path

#![allow(unused)]
fn main() {
pub fn load_model_from_path(&mut self, path: &Path) -> litsea::Result<()>
}

ローカルファイルからモデルの重みを同期的に読み込みます。ローカルファイルにはこのメソッドが推奨されます – 非同期ランタイムは不要です。

#![allow(unused)]
fn main() {
use std::path::Path;

learner.load_model_from_path(Path::new("./perceptron.model"))?;
}

load_model_from_reader

#![allow(unused)]
fn main() {
pub fn load_model_from_reader<R: BufRead>(&mut self, reader: R) -> litsea::Result<()>
}

メモリ上のバッファや既に開いているファイルなど、任意の BufRead ソースからモデルの重みを読み込みます。

load_model

#![allow(unused)]
fn main() {
pub async fn load_model(&mut self, uri: &str) -> litsea::Result<()>
}

URI からモデルを読み込みます。以下の形式に対応しています:

  • ローカルファイルパス: ./perceptron.model
  • File URI: file:///path/to/model
  • HTTP: http://example.com/modelremote_model フィーチャーが必要)
  • HTTPS: https://example.com/modelremote_model フィーチャーが必要)
#![allow(unused)]
fn main() {
learner.load_model("https://example.com/models/perceptron.model").await?;
}

評価

metrics

#![allow(unused)]
fn main() {
pub fn metrics(&self) -> MulticlassMetrics
}

学習データに対する評価メトリクスを算出します。

MulticlassMetrics

litsea::metrics で定義されています(litsea::MulticlassMetrics としても再エクスポートされます):

#![allow(unused)]
fn main() {
pub struct MulticlassMetrics {
    pub accuracy: f64,                            // Overall accuracy in percentage
    pub macro_precision: f64,                     // Macro-averaged precision in percentage
    pub macro_recall: f64,                        // Macro-averaged recall in percentage
    pub num_instances: usize,                     // Number of instances
    pub correct_per_class: HashMap<String, usize>,   // Correct count per class
    pub predicted_per_class: HashMap<String, usize>,  // Predicted count per class
    pub gold_per_class: HashMap<String, usize>,       // Gold label count per class
}
}

UPOS

upos モジュールは、品詞タグ付けに使用する Universal POS (UPOS) タグセットと分割ラベル型を定義します。

Upos

定義

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Upos {
    ADJ,    // Adjective
    ADP,    // Adposition
    ADV,    // Adverb
    AUX,    // Auxiliary
    CCONJ,  // Coordinating conjunction
    DET,    // Determiner
    INTJ,   // Interjection
    NOUN,   // Noun
    NUM,    // Numeral
    PART,   // Particle
    PRON,   // Pronoun
    PROPN,  // Proper noun
    PUNCT,  // Punctuation
    SCONJ,  // Subordinating conjunction
    SYM,    // Symbol
    VERB,   // Verb
    X,      // Other
}
}

Litsea は Universal Dependencies プロジェクトの全 17 UPOS タグをサポートしています:

タグ説明例(日本語)
ADJ形容詞いい, 大きい
ADP接置詞は, が, を, に
ADV副詞とても, まだ
AUX助動詞です, ます, た
CCONJ等位接続詞と, や
DET限定詞この, その
INTJ間投詞ああ, はい
NOUN名詞天気, 本
NUM数詞一, 二, 100
PART助詞・小辞ね, よ
PRON代名詞これ, それ
PROPN固有名詞東京, 太郎
PUNCT句読点。, 、
SCONJ従属接続詞ので, から
SYM記号%, $
VERB動詞読む, 書く
Xその他(未分類トークン)

定数

Upos::ALL

#![allow(unused)]
fn main() {
pub const ALL: [Upos; 17]
}

全 17 品詞の配列を返します。

トレイト実装

  • Display: "NOUN", "VERB" などの文字列に変換
  • FromStr: 文字列から Upos にパース。不正な文字列には ParseUposError を返す
#![allow(unused)]
fn main() {
use litsea::upos::Upos;

let pos: Upos = "NOUN".parse().unwrap();
assert_eq!(pos.to_string(), "NOUN");
}

ParseUposError

ParseUposError(クレートルートから litsea::ParseUposError として再エクスポート)は、文字列が有効な UPOS タグでない場合に返されます。input() アクセサはパースに失敗した文字列を返し、エラーメッセージは Unknown UPOS tag: '<input>' となります。

SegmentLabel

定義

SegmentLabel 型は単語境界検出と品詞タグ付けを組み合わせます。各文字位置に 18 ラベルのいずれかが割り当てられます:

  • B(Upos)(17 ラベル): 指定された UPOS タグを持つ単語境界(例: B-NOUN, B-VERB
  • O(1 ラベル): 非境界(現在の単語の継続)
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SegmentLabel {
    B(Upos),  // Start of a word (boundary). Carries POS information.
    O,        // Continuation of a word (non-boundary).
}
}
#![allow(unused)]
fn main() {
use litsea::upos::SegmentLabel;

// Segment labels for "今日は" (kyou wa)
// 今 → B-NOUN  (start of "今日", tagged as NOUN)
// 日 → O       (continuation of "今日")
// は → B-ADP   (start of "は", tagged as ADP)
}

メソッド

all_labels

#![allow(unused)]
fn main() {
pub fn all_labels() -> Vec<SegmentLabel>
}

全 18 個の SegmentLabel 値(文字列ではありません)のベクタを返します: 17 個の B(Upos) ラベルの後に O が続きます。

is_boundary

#![allow(unused)]
fn main() {
pub fn is_boundary(&self) -> bool
}

境界ラベル(B-*)かどうかを返します。

pos

#![allow(unused)]
fn main() {
pub fn pos(&self) -> Option<Upos>
}

品詞タグを返します。非境界(O)の場合は None

トレイト実装

  • Display: "B-NOUN", "O" などの文字列に変換
  • FromStr: 文字列から SegmentLabel にパース。不正な文字列には ParseSegmentLabelError を返す
#![allow(unused)]
fn main() {
use litsea::upos::{SegmentLabel, Upos};

let label: SegmentLabel = "B-NOUN".parse().unwrap();
assert!(label.is_boundary());
assert_eq!(label.pos(), Some(Upos::NOUN));

let label_o: SegmentLabel = "O".parse().unwrap();
assert!(!label_o.is_boundary());
assert_eq!(label_o.pos(), None);
}

ParseSegmentLabelError

ParseSegmentLabelError(クレートルートから litsea::ParseSegmentLabelError として再エクスポート)は、文字列が有効なセグメントラベルでない場合に返されます。2 つのバリアントがあります:

  • InvalidFormat – 文字列が O でも B-<UPOS> 形式でもない(メッセージ: Invalid segment label: '<input>'. Expected 'O' or 'B-<UPOS>'
  • InvalidPosB- プレフィックスはあるが POS 部分のパースに失敗した(内部の ParseUposError をラップする)

Language

Language 列挙型は、文字種分類を含む言語固有の動作を定義します。

Language 列挙型

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Language {
    #[default]
    Japanese,
    Chinese,
    Korean,
    English,
}
}

この列挙型に #[non_exhaustive] が付いているのは、新しい言語が破壊的変更なしに追加されることを想定しているためです。したがって、外部クレートで Language に対する match 式を書く場合はワイルドカードアーム(_ => ...)が必要です。

トレイト

  • DefaultLanguage::Japanese を返す
  • Display – 小文字の名前を返す("japanese""chinese""korean""english"
  • FromStr – 完全名または ISO 639-1 コードから解析(大文字・小文字を区別しない)

パース

#![allow(unused)]
fn main() {
use litsea::language::Language;

// Full names
let ja: Language = "japanese".parse().unwrap();
let zh: Language = "chinese".parse().unwrap();
let ko: Language = "korean".parse().unwrap();
let en: Language = "english".parse().unwrap();

// ISO 639-1 codes
let ja: Language = "ja".parse().unwrap();
let zh: Language = "zh".parse().unwrap();
let ko: Language = "ko".parse().unwrap();
let en: Language = "en".parse().unwrap();

// Case-insensitive
let ko: Language = "KOREAN".parse().unwrap();

// Invalid
assert!("french".parse::<Language>().is_err());
}

char_type

#![allow(unused)]
fn main() {
pub fn char_type(&self, c: char) -> &'static str
}

文字をその言語固有の文字種コードに分類します。どのクラスにも属さない文字には "O"(その他)を返します。

分類は文字範囲に対する直接の match で行われます – アロケーション不要、O(1) で、正規表現は使用しません。

#![allow(unused)]
fn main() {
use litsea::language::Language;

let lang = Language::Japanese;
assert_eq!(lang.char_type('あ'), "I");
assert_eq!(lang.char_type('漢'), "H");
assert_eq!(lang.char_type('@'), "O");
}

内部的には、char_type は言語ごとの非公開関数(japanese_char_type_idchinese_char_type_idkorean_char_type_idenglish_char_type_id)が返す数値の type id に対するテーブル参照になっており、文字列コードと数値 id が食い違うことはありません。全言語に共通のクラス – "P"(句読点)、"A"(ラテン文字)、"N"(数字) – は、言語固有のクラスの後にチェックされる共通ヘルパーで処理されます(英語では "P" を ASCII の句読点全般まで広げています。詳細は英語を参照)。

ParseLanguageError

文字列からの Language のパースに失敗すると ParseLanguageError が返されます。この型はクレートルートから再エクスポートされています(litsea::ParseLanguageError):

#![allow(unused)]
fn main() {
use litsea::language::{Language, ParseLanguageError};

let err: ParseLanguageError = "french".parse::<Language>().unwrap_err();
assert_eq!(err.input(), "french");
}
  • input() – パースに失敗した文字列を返す
  • エラーメッセージにはサポートされている言語が列挙されます: Unsupported language: 'french'. Supported: japanese (ja), chinese (zh), korean (ko), english (en)

評価

セグメンテーションおよび品詞タグ付けの held-out 品質評価 (litsea::evaluation)です。 litsea evaluate サブコマンドの背後にある ライブラリ API です。evaluate_poswith_two_stage_learner で作成した Segmenter(二段構成タグ付けを参照)を segment_with_pos を通じて評価します。

メトリクス型

#![allow(unused)]
fn main() {
pub struct SegmentationMetrics {
    pub word_precision: f64,     // %
    pub word_recall: f64,        // %
    pub word_f1: f64,            // %
    pub boundary_precision: f64, // %
    pub boundary_recall: f64,    // %
    pub boundary_f1: f64,        // %
    pub sentences: usize,
    pub gold_words: usize,
    pub predicted_words: usize,
}

pub struct PosMetrics {
    pub segmentation: SegmentationMetrics,
    pub tagged_precision: f64, // %: span and tag both match
    pub tagged_recall: f64,    // %
    pub tagged_f1: f64,        // %
}
}

いずれの型もクレートルートで再エクスポートされています。トークンは、 ゴールドトークンを連結した文字列上の文字オフセットスパンの完全一致で 対応付けます。空白のみのトークンはスコア計算から除外されます(韓国語/英語の 空白保持プロトコル。空白を使わずに表記される言語では no-op です)。

関数

evaluate_segmentation

#![allow(unused)]
fn main() {
pub fn evaluate_segmentation<I, S>(segmenter: &Segmenter, gold: I) -> SegmentationMetrics
where
    I: IntoIterator<Item = Vec<S>>,
    S: Into<String>,
}

各ゴールド文のトークンを連結したものを [Segmenter::segment] で分割し、 その結果をスコアリングします。空の文はスキップされます。

evaluate_pos

#![allow(unused)]
fn main() {
pub fn evaluate_pos<I, S>(segmenter: &Segmenter, gold: I) -> litsea::Result<PosMetrics>
where
    I: IntoIterator<Item = Vec<(S, Upos)>>,
    S: Into<String>,
}

evaluate_segmentation と同様ですが、[Segmenter::segment_with_pos] を 実行し、タグ付き単語も追加でスコアリングします。Segmenter に POS 学習器も 二段構成学習器も設定されていない場合は LitseaError::PosLearnerNotSet を 返します。

ゴールド行パーサ

#![allow(unused)]
fn main() {
pub fn parse_gold_line(line: &str, tsv: bool) -> Vec<String>
pub fn parse_gold_pos_line(line: &str, tsv: bool) -> Vec<(String, Upos)>
}

どちらもスペースで分割します(tsv = true の場合はタブで分割し、 トークンとして空白文字そのもの(" ")を含められます — スペース区切りで 書かれる言語で重要になる理由は英語を 参照)。parse_gold_pos_line はさらに各トークンを最後の / で分割し (学習パイプラインと同じルール)、タグが欠落しているか解析できない場合は Upos::X を既定値とします。tsv = true の場合、空白リテラルトークンには /POS サフィックスがなく常に Upos::X になりますが、これは無害です – 空白トークンはタグ付きスコアリングから内容によって除外され、割り当てられた タグでは判定しません。これは二段構成 POS モデルの実運用精度を、スペース 区切りで書かれる言語で測定するための仕組みです(issue #196)。韓国語・英語の 二段構成モデルは現在この空白保持コーパスで学習しているため(issue #198)、 これらの言語ではこの形式が学習時と実際の入力の双方に一致するプロトコルに なっています。

使用例

#![allow(unused)]
fn main() {
use litsea::adaboost::AdaBoost;
use litsea::evaluation::{evaluate_segmentation, parse_gold_line};
use litsea::language::Language;
use litsea::segmenter::Segmenter;

let mut learner = AdaBoost::new(0.01, 100);
learner.load_model_from_path(std::path::Path::new("./models/japanese.model"))?;
let segmenter = Segmenter::with_learner(Language::Japanese, learner);

let gold = std::fs::read_to_string("./resources/eval/japanese_gsd_test.txt")?;
let sentences = gold.lines().map(|l| parse_gold_line(l, false));
let metrics = evaluate_segmentation(&segmenter, sentences);
println!("word F1: {:.2}%", metrics.word_f1);
Ok::<(), Box<dyn std::error::Error>>(())
}

二段構成モデル(Two-Stage Model)

two_stage モジュールは litsea-two-stage v1 モデルコンテナ (TwoStageLearner)、stage-2 特徴量セットの選択肢(TwoStageFeatureSet)、 モデル種別の判定器(ModelKind)を定義します。 アーキテクチャと計測済みの品質・速度の数値は 二段構成タグ付け を、 モデルの新規学習は TwoStageTrainer を参照してください。

TwoStageLearner

#![allow(unused)]
fn main() {
pub struct TwoStageLearner {
    // private: stage1: AdaBoost,
    // private: stage2: AveragedPerceptron,
    // private: lexicon: HashMap<String, Vec<(Upos, u32)>>,
    // private: dominance: f64,
}
}

二段構成モデルの 3 パーツを保持します: stage-1 境界分類器 (スカラー重み、AdaBoost 形式)、候補タグ語彙表、stage-2 単語レベル タガー(AveragedPerceptron)。構築・シリアライズの規約は AdaBoost / AveragedPerceptron と同じです。

コンストラクタ

#![allow(unused)]
fn main() {
pub fn new() -> Self
pub fn from_parts(
    stage1: AdaBoost,
    stage2: AveragedPerceptron,
    lexicon: impl IntoIterator<Item = (String, Vec<(Upos, u32)>)>,
    dominance: f64,
) -> Result<Self>
}

new は空の learner を作成します(使用前に load_model* 系メソッドで 埋める必要があります)。from_parts はパーツから learner を構築し、 組み合わせを検証します: dominance(0.5, 1.0] の範囲内、 stage-2 の各クラス名は有効な Upos タグである必要があり、 各語彙エントリは非空のサーフェス(タブ・改行を含まない)、 正のカウントを持つ非空のタグリスト、重複タグ・重複サーフェスが 無いことが要求されます。語彙エントリは入力順に関わらず正規順序 (カウント降順、同数はタグ名昇順)に正規化されます。

モデルの入出力

#![allow(unused)]
fn main() {
pub fn save_model(&self, path: &Path) -> Result<()>
pub fn save_model_to_writer<W: Write>(&self, writer: &mut W) -> Result<()>
pub async fn load_model(&mut self, uri: &str) -> Result<()>
pub fn load_model_from_path(&mut self, path: &Path) -> Result<()>
pub fn load_model_from_reader<R: BufRead>(&mut self, reader: R) -> Result<()>
}

AdaBoost/AveragedPerceptron と同じ規約です: save_model/load_model はファイルパス、または(load_model のみ)file:///http(s):// の URI に対応します(後者は remote_model フィーチャが必要)。 *_to_writer/*_from_reader 系は任意の writer/reader に対応します。 空の learner(語彙エントリが無い、またはどちらかの内部 learner が 未学習)の保存は LitseaError::InvalidInput を返します。 読み込みはファイル構造全体を検証します(オンディスクのレイアウトは モデルファイル形式 を参照)。 不正な内容は LitseaError::InvalidData で拒否され、読み込みエラー時に learner は変更されません。

#![allow(unused)]
fn main() {
use std::path::Path;

use litsea::two_stage::TwoStageLearner;

let mut learner = TwoStageLearner::new();
learner.load_model_from_path(Path::new("./models/japanese_pos.model"))?;
}

アクセサ

#![allow(unused)]
fn main() {
pub fn stage1(&self) -> &AdaBoost
pub fn stage2(&self) -> &AveragedPerceptron
pub fn dominance(&self) -> f64
pub fn lexicon_len(&self) -> usize
pub fn lexicon_entry(&self, surface: &str) -> Option<&[(Upos, u32)]>
}

dominance は分類器スキップの閾値です: 推論時、あるサーフェスの 最頻タグが学習時出現の少なくともこの割合を占める場合、stage-2 分類器を一切呼び出さずにタグ付けします。lexicon_entry は 学習中に観測されたサーフェスの候補タグを頻度降順で返し、 一度も観測されていないサーフェスには None を返します。

実際に推論を実行するには、TwoStageLearner を直接呼び出すのではなく Segmenter::with_two_stage_learner 経由で Segmenter にインストールしてください——segmenter がこれを 高速ルックアップ用の packed スコアリングテーブルへコンパイルします。

TwoStageFeatureSet

#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum TwoStageFeatureSet {
    Full,
    Balanced,
    #[default]
    Fast,
}
}

Extractor::extract_two_stage が書き出す 23 個の単語レベル stage-2 テンプレート(特徴量抽出 参照)のうちどれを使うかを選択します。Fast(既定値)は計測済みの 最小セット——サーフェス、単語長、先頭/末尾文字タイプ、隣接文脈文字+ タイプ、2文字プレフィックス/サフィックス。Balanced はこれに 先頭/末尾文字そのものと単語タイプコード文字列を追加します。Full は 全テンプレートを含みます。分割精度は 3 セットとも同一です(stage-1 が 決定するため)——変わるのはタグ付け精度とスループットのみです。 3 セットの相対的な順序(正確な数値ではありません——この型自身の rustdoc に記載の数値は、同梱モデルとは異なるエポック数で計測した 初期プロトタイプのものです)についてはこの型自身の rustdoc を、 同梱モデルの現在の実測値は 学習済みモデル を参照してください。

FromStr(大文字小文字を区別しない: "full", "balanced", "fast")と Display(小文字)を実装しています。#[non_exhaustive] が付与されて おり、外部の match 式にはワイルドカードアームが必要です。

ModelKind

#![allow(unused)]
fn main() {
pub enum ModelKind {
    AdaBoost,
    AveragedPerceptron,
    TwoStage,
}
}

ModelKind::detect(content: &str) -> ModelKind はモデルファイルの 1行目を調べて形式を判別します——これは判別のためのヒューリスティックで あり完全な検証ではありません。AdaBoost はプレーンな分割モデル形式 (畳み込み済みの二段構成 stage 1 の形式でもあります)、 AveragedPerceptron はスタンドアロンのパーセプトロンファイル (train --perceptron の出力であり、[stage2] セクションのペイロード 形式。これは削除された joint POS モデル形式であり、POS モデルとしては 読み込めません)、TwoStagelitsea-two-stage コンテナです。

種類違いのファイルには TwoStageLearner のローダーが正確なエラーを 返します: スタンドアロンの Averaged Perceptron ファイルを指定すると “joint POS models are no longer supported — retrain with litsea train --pos” で失敗し、それ以外の非二段構成の内容はマジック行の欠落 エラーで失敗します。

CLIリファレンス概要

litsea CLIは、単語分割、モデル学習、テキスト処理のためのコマンドを提供します。

CLI バイナリはライブラリの remote_model フィーチャーを有効にしてビルドされているため、http(s):// のモデル URI を追加設定なしでそのまま利用できます。一方、litsea ライブラリ自体はデフォルトフィーチャーにリモート読み込みを含みません。

使い方

litsea <COMMAND> [OPTIONS] [ARGS]

コマンド一覧

CommandDescription
extract学習用にコーパスから特徴量を抽出
train単語分割モデルを学習
segment学習済みモデルを使用してテキストを単語に分割
evaluateheld-out のゴールドコーパスに対してモデルを評価

グローバルオプション

OptionDescription
-h, --helpヘルプ情報を表示
-V, --versionバージョン番号を表示

一般的なワークフロー

AdaBoost ワークフロー(単語分割のみ)

flowchart LR
    A["1. scripts/download_udtreebank.sh"] --> B["2. scripts/corpus_udtreebank.sh"]
    B --> C["3. litsea extract"]
    C --> D["4. litsea train"]
    D --> E["5. litsea segment"]
  1. UD Treebank をダウンロードする: conllu_file=$(bash scripts/download_udtreebank.sh -l ja -o /tmp)
  2. コーパスを準備する: bash scripts/corpus_udtreebank.sh "$conllu_file" corpus.txt
  3. 特徴量を抽出する: litsea extract -l japanese corpus.txt features.txt
  4. モデルを学習する: litsea train -t 0.0001 -i 20000 features.txt model.model
  5. テキストを分割する: echo "text" | litsea segment -l japanese model.model

二段構成ワークフロー(品詞推定付き単語分割)

flowchart LR
    A["1. scripts/download_udtreebank.sh"] --> B["2. scripts/corpus_udtreebank.sh -p"]
    B --> C["3. litsea extract --pos"]
    C --> D["4. litsea train --pos"]
    D --> E["5. litsea segment --pos"]
  1. UD Treebank をダウンロードする: conllu_file=$(bash scripts/download_udtreebank.sh -l ja -o /tmp)
  2. 品詞付きコーパスを準備する: bash scripts/corpus_udtreebank.sh -p "$conllu_file" pos_corpus.txt
  3. 二段構成の特徴量を抽出する: litsea extract --pos -l japanese pos_corpus.txt features_prefix
  4. 二段構成モデルを学習する: litsea train --pos --num-epochs 50 features_prefix model.model
  5. 品詞推定付き分割: echo "text" | litsea segment --pos -l japanese model.model

アーキテクチャについては 二段構成タグ付けを、フラグの 完全なリファレンスは モデルの学習trainを参照してください。

extract

モデル学習用にコーパスファイルから特徴量を抽出します。

使い方

litsea extract [OPTIONS] <CORPUS_FILE> <FEATURES_FILE>

引数

ArgumentDescription
CORPUS_FILE入力コーパスファイルのパス(単語をスペースで区切り、1行に1文)
FEATURES_FILE出力特徴量ファイルのパス

オプション

OptionDefaultDescription
-l, --language <LANGUAGE>japanese文字タイプ分類に使用する言語。指定可能な値: japanese / ja, chinese / zh, korean / ko, english / en
--format <FORMAT>spaceコーパスの形式: space(スペース区切りの単語)または tsv(タブ区切りのトークン。トークンは空白文字そのものでもよく、元の空白を保持できます)。--pos と併用でき(issue #198)、空白保持の word/POS コーパスから二段構成の特徴量を抽出できます
--posoff二段構成の学習用特徴量を抽出します。入力には品詞付きコーパスが必要です
--stage2-features <SET>fast--pos 用の stage-2 単語特徴セット: full(品質最優先)、balancedfast(速度最優先)
--tag-freeオフ16 個のタグ依存特徴量テンプレート(UP*/BP*/UQ*/BQ*/TQ*)を除外し、学習されるモデルを pointwise にして segment() の逐次スコアリングパスをスキップ可能にする(issue #183。同梱の korean.model/english.model で使用 – 言語別の品質・速度トレードオフはタグなし(pointwise)モデルを参照)。--format tsv と併用可。--pos とは併用不可

コーパスの形式

入力コーパスは、単語をスペースで区切り、1行に1文とする形式です。

Litsea は TinySegmenter を 参考 に 開発 さ れ た 。
Rust で 実装 さ れ た コンパクト な 単語 分割 ソフトウェア です 。

TSV コーパス形式(--format tsv

--format tsv を指定すると、トークンはタブ文字で区切られ、トークンとして空白文字そのもの(" ")を含められます。これにより学習テキスト内に元の文の空白が保持されます。空白がほとんどの語境界を示す韓国語や英語のような言語では、これが不可欠です(韓国語および英語を参照)。このようなコーパスは UD Treebank から corpus_udtreebank.sh -s で生成できます:

litsea extract -l korean --format tsv ./ko_corpus.tsv ./ko_features.txt
litsea extract -l english --format tsv --tag-free ./en_corpus.tsv ./en_features.txt

出力形式

特徴量ファイルには、文字位置ごとに1行が含まれます。コーパス行 これ は テスト です 。 に対する最初の2行は次のとおりです。

-1	BC1:OI	BC2:II	BC3:II	BP1:UU	BP2:UU	BQ1:UOI	BQ2:UII	BQ3:UOI	BQ4:UII	BW1:B1こ	BW2:これ	...
1	BC1:II	BC2:II	BC3:IK	BP1:UU	BP2:UO	BQ1:UII	BQ2:UII	BQ3:OII	BQ4:OII	BW1:これ	BW2:れは	...
  • 1 = 語境界
  • -1 = 非境界
  • 特徴量はアルファベット順にソートされてタブ区切りで書き出されます。そのため各行はテンプレート定義順ではなく BC1: 特徴量から始まります

使用例

# Japanese
litsea extract -l japanese ./corpus.txt ./features.txt

# Chinese
litsea extract -l zh ./corpus_zh.txt ./features_zh.txt

# Korean
litsea extract -l ko ./corpus_ko.txt ./features_ko.txt

# English
litsea extract -l en ./corpus_en.txt ./features_en.txt

成功時のstderr出力:

Feature extraction completed successfully.

二段構成の特徴量抽出

--pos フラグを指定すると、extract は通常の単語区切りコーパスの代わりに 品詞付きコーパス を入力として受け取ります。各行には、単語/品詞 の形式で UPOS タグが付与された単語が含まれます。

品詞付きコーパスの形式

これ/PRON は/ADP テスト/NOUN です/AUX 。/PUNCT
今日/NOUN は/ADP いい/ADJ 天気/NOUN です/AUX ね/PART 。/PUNCT

extract --pos は、 二段構成アーキテクチャ 向けに FEATURES_FILE をプレフィックスとした3つのファイルを書き出します。

ファイル内容
{FEATURES_FILE}.stage1境界特徴量。文字位置ごとに1行、ラベルは B または O(通常の抽出と同じ文字レベルの特徴量テンプレート。先頭を含む全位置で出力)
{FEATURES_FILE}.stage2単語単位の特徴量。単語ごとに1行、ラベルは UPOS タグ。書き出すテンプレートは --stage2-features で制御
{FEATURES_FILE}.lexicon候補タグ語彙表: surface\tTAG:count[,TAG:count...](出現頻度の降順)

同じプレフィックスを litsea train --pos に渡します:

litsea extract --pos -l japanese ./pos_corpus.txt ./pos_features
# ./pos_features.stage1, .stage2, .lexicon を書き出す

空白保持の品詞付きコーパス(--pos --format tsv

スペース区切りの言語では、--pos--format tsv を併用します(issue #198)。 このときコーパスは 単語/品詞 トークンのタブ区切りリストとなり、トークンとして /品詞 サフィックスを持たない空白文字そのものを含められます。これは corpus_udtreebank.sh -p -s が出力する形式です:

I/PRON	 	do/AUX	n't/PART	 	know/VERB	./PUNCT
bash scripts/corpus_udtreebank.sh -p -s "$conllu_file" ./pos_corpus.tsv
litsea extract --pos --format tsv -l english --stage2-features full ./pos_corpus.tsv ./pos_features

同梱の korean_pos.modelenglish_pos.model はこの方法で学習しています。 代わりに空白なしのコーパスで学習すると、held-out の単語 F1 は韓国語で約 5.9 ポイント、英語で約 20.8 ポイント低下します。stage-1 がほとんどの語境界を示す スペースを一度も目にせず、stage-2 のコンテキスト特徴量も推論時とは異なる隣接 要素を見ることになるためです。

空白トークンには stage-2 の学習行が与えられません – スペース付きコーパスでは トークンの約 43% を占め、退化した 1 クラス(X)の学習になってしまうためです – が、語彙表エントリは与えられます。これにより候補が1つだけになり、モデルの 固定タグのパスを通じて決定的にタグ付けされ、分類器を完全にスキップできます。

日本語と中国語では素の --pos を使用してください: これらのテキストにはスペースが なく、保持すべき空白が存在しません。

train

AdaBoostを使用して単語分割モデルを学習します。

使い方

litsea train [OPTIONS] <FEATURES_FILE> <MODEL_FILE>

引数

ArgumentDescription
FEATURES_FILE入力特徴量ファイルのパス(extract の出力)
MODEL_FILE出力モデルファイルのパス

オプション

OptionDefaultDescription
-t, --threshold <THRESHOLD>0.01早期停止のための弱分類器精度の閾値。値を小さくするとより多くの反復が可能になる
-i, --num-iterations <NUM_ITERATIONS>100ブースティング反復の最大回数
-m, --load-model-uri <LOAD_MODEL_URI>None学習を再開するための既存モデルのURI(ファイルパスまたはHTTP/HTTPS URL)
--perceptronoff不透明な文字列ラベルに対する汎用の Averaged Perceptron を学習する(同梱分割モデルの畳み込みレシピの学習ステップ)
--num-epochs <NUM_EPOCHS>10学習エポック数(--perceptron モードおよび --pos モード)
--posoff代わりに二段構成モデルを学習する。{FEATURES_FILE}.stage1/.stage2/.lexiconextract --pos の出力)を読み込む。--perceptron および -m/--load-model-uri(増分学習は非対応)とは併用できない
--dominance <DOMINANCE>0.99--pos 用の分類器スキップ閾値、範囲は (0.5, 1.0]。既知の単語のうち最頻タグが学習時の出現のこの割合以上を占めるものは、stage-2 分類器を呼ばずにタグ付けされる

出力

学習メトリクスはstderrに出力されます。

メトリクスは学習データに対して計算されます。反復回数が十分であれば、モデルは学習コーパスにほぼ完全に適合できてしまうため、現実的な品質を見積もるにはホールドアウトされたテキストで評価してください。

Result Metrics:
  Accuracy: 100.00% ( 1075868 / 1075869 )
  Precision: 100.00% ( 161283 / 161284 )
  Recall: 100.00% ( 161283 / 161283 )
  Confusion Matrix:
    True Positives: 161283
    False Positives: 1
    False Negatives: 0
    True Negatives: 914585

Ctrl+C のハンドリング

学習は優雅な中断をサポートしています。

  • 1回目のCtrl+C: 学習を停止し、現在の状態でモデルを保存する
  • 2回目のCtrl+C: 保存せずに即座に終了する

これにより、長時間の学習セッションを進捗を失うことなく停止できます。

使用例

基本的な学習:

litsea train -t 0.0001 -i 20000 ./features.txt ./models/my_model.model

これは通常の AdaBoost 学習の一般的な例です。同梱の japanese.modelchinese.modelkorean.modelenglish.model は別の手順を使っています – 学習手順を参照してください。

高精度な学習(低い閾値、多い反復回数):

litsea train -t 0.001 -i 5000 ./features.txt ./model.model

既存モデルからの再学習:

litsea train -t 0.0001 -i 20000 -m ./models/my_model.model \
    ./new_features.txt ./models/my_model_v2.model

ハイパーパラメータの調整

Parameter値を小さくした場合の効果値を大きくした場合の効果
threshold反復回数が増加、精度が向上する可能性あり、学習時間が長くなる反復回数が減少、学習が高速化、アンダーフィットの可能性あり
num_iterationsブースティングラウンドが減少、モデルが小さくなる、アンダーフィットの可能性ありラウンドが増加、モデルが大きくなる、精度が向上する可能性あり

汎用パーセプトロンの学習(--perceptron

--perceptron フラグを指定すると、AdaBoost の代わりに Averaged Perceptron アルゴリズムを使用します。ラベルは不透明な文字列として 扱われるため、このモードは label\tfeature\t... 形式の特徴量ファイルから 任意の多クラス分類器を学習できます。主な用途は、同梱分割モデルの 畳み込みレシピにおける 2 クラス(B/O)境界パーセプトロンの学習です (学習手順を参照)。

使い方

litsea train --perceptron [OPTIONS] <FEATURES_FILE> <MODEL_FILE>

パーセプトロン学習固有のオプション

OptionDefaultDescription
--perceptronoff汎用パーセプトロン学習モードを有効にする
--num-epochs <NUM_EPOCHS>10学習エポック数

使用例

# 2 クラスの境界パーセプトロンを学習(畳み込みレシピのステップ 3)
litsea train --perceptron --num-epochs 50 ./features.txt ./perceptron.model

出力

学習メトリクスはstderrに出力されます(マクロ平均の適合率・再現率)。

Result Metrics (Perceptron):
  Accuracy: 98.23% ( 277213 )
  Macro Precision: 96.82%
  Macro Recall: 93.30%

Ctrl+C のハンドリング

AdaBoost と同様に、パーセプトロンの学習も優雅な中断をサポートしています。1回目の Ctrl+C で学習を停止し、現在の状態でモデルを保存します。

パーセプトロンのハイパーパラメータ

Parameter値を小さくした場合の効果値を大きくした場合の効果
num_epochs学習が高速化、アンダーフィットの可能性あり精度が向上、学習時間が長くなる、オーバーフィットの可能性あり

二段構成モデルの学習

--pos を指定すると、 二段構成モデル を構築します: 二値の境界分類器(stage 1)と単語単位の品詞タガー(stage 2)を、 候補タグ語彙表とともに単一の litsea-two-stage v1 ファイルに組み立てます。 両ステージとも --num-epochs エポック分 Averaged Perceptron として学習し、 その後 stage 1 は既存の AdaBoost 形式のスカラー重みに畳み込まれます (品質を損なわない変換 — 導出は litsea::trainer のモジュールドキュメントを参照)。 これによりランタイムは通常の segment() モデルと全く同じ方法で採点します。

使い方

litsea train --pos [OPTIONS] <FEATURES_PREFIX> <MODEL_FILE>

FEATURES_PREFIXextract --pos に渡したものと同じプレフィックスです。

litsea extract --pos -l japanese ./pos_corpus.txt ./pos_features
litsea train --pos --num-epochs 50 ./pos_features ./models/japanese_pos.model

出力

Result Metrics (Two-Stage):
  Stage 1 (boundary) Accuracy: 99.86% ( 277213 )
  Stage 1 Macro Precision: 99.85%
  Stage 1 Macro Recall: 99.86%
  Stage 2 (tagging) Accuracy: 99.09% ( 168333 )
  Stage 2 Macro Precision: 98.96%
  Stage 2 Macro Recall: 98.77%

他のモードと同様、これらは in-sample のメトリクスです。現実的な品質を見積もるには litsea evaluate --pos でホールドアウトされたテキストを評価してください。

segment

学習済みモデルを使用してテキストを単語に分割します。

使い方

echo "text" | litsea segment [OPTIONS] <MODEL_URI>

引数

ArgumentDescription
MODEL_URI学習済みモデルファイルのパスまたはURL。サポート形式: ローカルファイルパス, file://, http://, https://

オプション

OptionDefaultDescription
-l, --language <LANGUAGE>japanese文字タイプ分類に使用する言語。指定可能な値: japanese / ja, chinese / zh, korean / ko, english / en
--posoff品詞推定付き分割を有効にします。二段構成モデル(train --pos)が必要です
--threads <N>1バッチ分割のワーカースレッド数(issue #185)。既定値では従来どおりのシングルスレッド動作。N > 1 では入力行を並列に分割しつつ入力順で出力するため、出力はどちらでもバイト単位で同一です(--pos の有無を問わず使用可)。大きな入力の実時間はコア数に応じて短縮されますが、1 行あたりのレイテンシは変わりません

入力 / 出力

  • 入力: stdinから読み取り、1行に1文。空行はスキップされます。
  • 出力: stdoutに書き込み、スペース区切りのトークン、入力行ごとに1行。
  • パイプライン: 後段の処理がパイプを早期に閉じた場合(例: litsea segment model | head -1)、コマンドは正常終了します(終了コード0)。 そのため segment はシェルのパイプライン内で問題なく連携できます。

使用例

日本語:

echo "LitseaはTinySegmenterを参考に開発された。" \
  | litsea segment -l japanese ./models/RWCP.model
Litsea は TinySegmenter を 参考 に 開発 さ れ た 。

中国語:

echo "中文分词测试。" | litsea segment -l chinese ./models/chinese.model

韓国語:

echo "한국어 단어 분할 테스트입니다." \
  | litsea segment -l korean ./models/korean.model

英語:

echo "I don't know." | litsea segment -l english ./models/english.model

ファイルの処理:

cat input.txt | litsea segment -l japanese ./models/japanese.model > output.txt

URLからモデルを読み込み:

echo "テスト文です。" \
  | litsea segment -l japanese https://example.com/models/japanese.model

品詞推定付き分割(--pos

--pos フラグを指定すると、train --pos で作成した 二段構成モデルを 使って単語分割と品詞推定を一緒に行います。--pos に他の種類のモデルを 渡すと正確なエラーで失敗します(スタンドアロンの Averaged Perceptron ファイル — 削除された joint POS モデル形式 — は、train --pos での 再学習を促すヒント付きで拒否されます)。

使い方

echo "text" | litsea segment --pos [OPTIONS] <MODEL_URI>

出力形式

各単語が 単語/品詞 の形式で出力されます。品詞は UPOS タグセットに準拠します。

echo "今日はいい天気ですね。" \
  | litsea segment --pos -l japanese ./models/japanese_pos.model
今日/NOUN は/ADP いい/ADJ 天気/NOUN です/AUX ね/PART 。/PUNCT

ファイルの処理

cat input.txt | litsea segment --pos -l japanese ./models/japanese_pos.model > output.txt

バッチ分割の並列化(--threads

文どうしは独立なので、エンジンを変更することなくバッチスループットを コア数でスケールできます: 入力行をチャンク単位で読み込み、各チャンクを ワーカーへ分配し(各ワーカーは自分専用の再利用バッファを保持)、出力は 厳密に入力順で書き出します。

litsea segment --threads 8 -l japanese ./models/japanese.model < corpus.txt > segmented.txt

--threads 1(既定値)は従来の逐次ループそのものを使います。 なお cargo bench -- external_corpus はシングルスレッドのエンジン 計測のままです — CLI レベルのスレッドスケーリングとエンジンスループットは 別の数値であり、直接比較しないでください (ベンチマークを参照)。

注意事項

  • --language フラグは、モデルが学習された言語と一致する必要があります
  • CLIは非同期のURI APIを通じてモデルを読み込み、TLS(rustls)を使用したHTTP/HTTPSをサポートしています。ライブラリには同期的なローカル読み込み(load_model_from_path)も用意されています
  • モデルURIはファイルパスに限定されません – 有効なURLであれば使用可能です
  • --pos を使用する場合、モデルは train --pos で学習した二段構成モデルである必要があります

evaluate

学習済みモデルを held-out のゴールドコーパスに対して評価し、品質メトリクスを 出力します。train が出力する in-sample 指標とは異なり、モデルが学習で一度も 見ていないテキストに対する品質を測定します。

ゴールドコーパスとは、正解が書かれたテキストファイルです: 1 行 1 文で、 人手アノテーションにより正しいトークンへ分割済みのもの(学習用コーパスと同じ ファイル形式 — 空白区切り・タブ区切り、--pos 時は word/POS)。「ゴールド」は モデル出力の採点基準となる正解(ゴールドスタンダード)を指します。意味のある held-out 評価にするには、モデルの学習に使っていない文で構成されている必要が あります — 同梱の resources/eval/ は UD GSD の test 分割で、同梱モデルは train 分割で学習されているため、この条件を満たします。

使い方

litsea evaluate [OPTIONS] <MODEL_URI> <GOLD_FILE>

引数

ArgumentDescription
MODEL_URI学習済みモデルファイルのパスまたはURL。サポート形式: ローカルファイルパス, file://, http://, https://
GOLD_FILEゴールドコーパスのパス(1行1文)

オプション

OptionDefaultDescription
-l, --language <LANGUAGE>japaneseモデルとゴールドコーパスの言語。指定可能な値: japanese / ja, chinese / zh, korean / ko, english / en
--posoff単語分割と品詞推定を同時に評価します。二段構成モデル(train --pos)が必要です。ゴールド形式は下記の --format と組み合わせて選択します
--format <FORMAT>spaceゴールドコーパスの形式。--pos なしの場合: space(スペース区切りトークン)または tsv(タブ区切りトークン。韓国語/英語の空白保持コーパスのように、トークンとして空白文字そのものを含められます)。--pos ありの場合: space"word/POS word/POS ..."(二段構成の学習コーパス形式、無空白)を、tsv はタブ区切りの "word/POS" トークン(トークンは空白文字も可)を選択します(issue #196/#198。韓国語・英語の二段構成モデルが現在学習に使っている空白保持形式であり、これらの言語では学習時と実際の入力の双方に一致するプロトコルです)

メトリクス

各文について、次の 2 つのトークン列を比較します:

  • ゴールドトークン – ゴールドコーパスに記録された基準の分割。人手で アノテーションされた正解(ここでは UD GSD ツリーバンク test 分割の トークン分割)です。評価対象の文テキストは、これらを連結して復元します
  • 予測トークン – 復元した文テキストを segment--pos 時は segment --pos)に与えたときにモデルが出力する分割。ユーザーが推論時に 得るものと完全に同じです

予測トークンとゴールドトークンは、復元した文上の文字オフセットスパンの 完全一致で対応付けます。空白のみのトークンはスコア計算から除外されるため、 韓国語/英語の空白保持プロトコルが数値を押し上げることはありません。

メトリクス測るもの低い場合の意味
Word Precision(単語適合率)予測した単語のうち、ゴールドの単語と完全一致(両端が正しい)した割合余計な単語が多い: 過分割、または誤った結合
Word Recall(単語再現率)ゴールドの単語のうち、完全一致で復元できた割合取りこぼしたゴールド単語が多い
Word F1(単語 F1)単語適合率と再現率の調和平均分割品質の総合指標
Boundary Precision(境界適合率)予測した単語開始位置のうち、ゴールドの境界だった割合誤った境界が多い(過分割)
Boundary Recall(境界再現率)ゴールドの単語開始位置のうち、検出できた割合見逃した境界が多い(分割不足)
Boundary F1(境界 F1)境界適合率と再現率の調和平均境界判定の総合指標
Tagged Word Precision / Recall / F1(--pos単語メトリクスと同様だが、予測 POS タグの一致も必要スパンは正しいがタグが誤っている

単語は両端の境界がともに正しい場合のみ正解と数えるため、単語メトリクスは 常に境界メトリクスと同等以上に厳しくなります — 境界が 1 つずれるだけで、その 両側の 2 単語が不正解になります。Sentences は評価対象(非空)のゴールド文数です。

使用例

同梱のゴールドデータ(resources/eval/、UD GSD テスト分割から変換)を使って、 ドキュメントに記載の held-out の数値を再現します:

litsea evaluate -l japanese models/japanese.model resources/eval/japanese_gsd_test.txt
litsea evaluate -l korean --format tsv models/korean.model resources/eval/korean_gsd_test.tsv
litsea evaluate -l chinese models/chinese.model resources/eval/chinese_gsd_test.txt
litsea evaluate -l english --format tsv models/english.model resources/eval/english_ewt_test.tsv
litsea evaluate --pos -l japanese models/japanese_pos.model resources/eval/japanese_gsd_test_pos.txt

韓国語・英語では、--pos 単体は #198 以前に二段構成モデルが学習に使っていた 空白非保持プロトコルを再現します。現在のモデルの品質を測るには、 --pos --format tsv*_pos_spaced.tsv のゴールドに対して実行してください — 学習時のプロトコルと実際のスペース付き入力の双方に一致します。2 つの数値は 互いに比較できません (事前学習済みモデル 参照):

litsea evaluate --pos -l english models/english_pos.model resources/eval/english_ewt_test_pos.txt
litsea evaluate --pos --format tsv -l english models/english_pos.model resources/eval/english_ewt_test_pos_spaced.tsv

出力:

Evaluation Metrics:
  Sentences: 543
  Word Precision: 96.73%
  Word Recall: 96.66%
  Word F1: 96.70%
  Boundary Precision: 98.63%
  Boundary Recall: 98.56%
  Boundary F1: 98.59%

言語バインディング

Litsea は Rust のライブラリですが、他の言語からも利用できます。バインディングは本リポジトリのワークスペースメンバーとして管理されており、litsea 本体と同じバージョンでリリースされます。

クレート一覧

クレート対象ランタイムFFI スタック状態
litsea-binding-core(共通・FFI 非依存)提供済み
litsea-pythonPython 3.10+PyO3 + maturin提供済み
litsea-nodejsNode.js 20+napi-rs提供済み
litsea-phpPHP 8.1+ext-php-rs提供済み
litsea-rubyRuby 3.1+magnus + rb-sys提供済み
litsea-wasmブラウザ / Denowasm-bindgen提供済み

設計方針

以下はすべてのバインディングに共通します。

モデルは同梱しない

バインディングのパッケージにはコードのみを含めます。モデルは呼び出し側がバイト列・ファイルパス・file:// パス・http(s):// URL のいずれかで渡します。同梱モデルは 1 つあたり 84KB〜8MB あり、4 言語分を埋め込むと wheel や npm パッケージが 20MB を超えてしまうためです。外部に置くことで、バインディングを再公開せずにモデルだけを更新できるという利点もあります。

モデルの入手方法は事前学習済みモデルを参照してください。

モデル種別は宣言ではなく判定する

CLI では、分割モデルと二段構成 POS モデルのどちらを読み込むかを --pos で指定する必要があります。バインディングではこれが不要です。モデルのバイト列を 1 度だけ読んで種別を判定するため、has_pos は「呼び出し側が正しく指定すべき情報」ではなく「読み込んだモデルの性質」になります。

キャンセルは明示的に行う

学習は長時間かかることがあり、litsea のトレーナは running フラグが倒されると早期終了します。CLI はこのフラグを Ctrl-C ハンドラから操作していますが、ライブラリが同じことをしてはいけません。シグナルハンドラはプロセス全体で 1 つしか登録できず、ホスト言語が既に握っているのが普通だからです。そのためバインディングは、呼び出し側が操作するキャンセルトークンを提供します。

共通ロジックは 1 つのクレートに集約する

FFI に依存しない処理(言語名のパース、モデル読み込みと種別判定、トークンのバイトオフセット、学習の呼び出し、エラー分類)はすべて litsea-binding-core に置きます。各バインディングは、その API をホスト言語の型と例外モデルに写像するだけです。

litsea-binding-core

litsea-binding-core は、各言語バインディングが共有する FFI 非依存のロジックをまとめたクレートです。依存先は litsea(およびネイティブターゲットではブロッキング用の Tokio)のみで、PyO3・napi・ext-php-rs・magnus・wasm-bindgen には一切依存しません。そのため、ホスト言語のツールチェーンなしでユニットテストできます。

インストール

[dependencies]
litsea-binding-core = "0.13.0"

モジュール構成

graph LR
    A["litsea_binding_core::segmenter"] --- B["CoreSegmenter"]
    C["litsea_binding_core::model"] --- D["build_segmenter, read_model_file, read_model_uri"]
    E["litsea_binding_core::token"] --- F["TokenView"]
    G["litsea_binding_core::language"] --- H["parse_language, SUPPORTED_LANGUAGES"]
    I["litsea_binding_core::trainer"] --- J["CoreExtractor, CoreTrainer, CorePerceptronTrainer, CoreTwoStageTrainer"]
    K["litsea_binding_core::cancel"] --- L["CancelToken"]
    M["litsea_binding_core::error"] --- N["CoreError, ErrorKind, CoreResult"]
    O["litsea_binding_core::runtime"] --- P["block_on"]
モジュール主な型役割
segmenterCoreSegmenter分割と POS タグ付け(単発・バッチ)。バッファを再利用する
modelBuiltSegmenter, build_segmenterモデルの読み込みと種別判定
tokenTokenView表層形・バイトオフセット・UPOS タグ(任意)を持つトークン
languageSUPPORTED_LANGUAGES, parse_language言語名のパースと列挙
trainerCoreExtractor, CoreTrainer, CorePerceptronTrainer, CoreTwoStageTrainer特徴量抽出と学習(ネイティブターゲットのみ)
cancelCancelToken学習の協調的キャンセル
errorCoreError, ErrorKind, CoreResult各言語の例外へ写像するためのエラー分類
runtimeblock_on同期ホストから非同期モデルローダを呼ぶ(ネイティブターゲットのみ)

分割

#![allow(unused)]
fn main() {
use litsea::Language;
use litsea_binding_core::CoreSegmenter;

let segmenter = CoreSegmenter::from_path(Language::Japanese, "models/japanese.model".as_ref())?;

assert_eq!(
    segmenter.segment("これはテストです。"),
    vec!["これ", "は", "テスト", "です", "。"]
);
}

空白区切りの言語では空白自体が 1 トークンとして返るため、トークンを連結すると入力が正確に復元されます。例えば korean.model"안녕하세요 반갑습니다"["안녕하세요", " ", "반갑습니다"] に分割します。

CoreSegmenterArc<Segmenter>Mutex<SegmentBuffer> を保持します。SegmenterSend + Sync であり、モデル読み込み済みのインスタンスは packed テーブルが構築済みのため、並行した segment 呼び出しは内部の read ロックしか取りません。ミューテックスが保護するのはスクラッチバッファだけです。したがって 1 つのインスタンスをスレッド間で共有し、継続的に再利用できます(各バインディングはそのように使います)。

メソッド戻り値
segment(text)Vec<String>
segment_batch(texts)Vec<Vec<String>>(バッファを 1 つ再利用)
segment_tokens(text)バイトオフセット付き Vec<TokenView>pos は未設定)
segment_with_pos(text)バイトオフセットと UPOS タグ付き CoreResult<Vec<TokenView>>
segment_with_pos_batch(texts)CoreResult<Vec<Vec<TokenView>>>

バイトオフセットは厳密です。トークンは入力を隙間も重複もなく覆うため、すべてのトークンについて &text[token.byte_start..token.byte_end] == token.surface が成り立ちます。空白を保持する韓国語・英語でも同様です。

なお segment_with_pos_batchsegment_batch のようにアロケーションを償却できません。litseasegment_with_pos のバッファ再利用版が存在しないためです。

モデルの読み込み

コンストラクタ利用可否
CoreSegmenter::from_bytes(language, bytes)wasm32 を含むすべての環境
CoreSegmenter::from_path(language, path)ネイティブターゲット
CoreSegmenter::from_uri(language, uri).awaitすべての環境(http(s)://remote_model feature が必要)
CoreSegmenter::from_uri_blocking(language, uri)ネイティブターゲット

いずれも build_segmenter を経由し、モデルファイル自身から何を構築するかを決定します。

判定された種別結果
二段構成モデル(litsea-two-stage v1POS 対応セグメンタ、has_pos() == true
AdaBoost 形式モデル分割専用セグメンタ、has_pos() == false
joint POS モデル(旧形式)joint モデルが削除済みであることを説明する ErrorKind::Model エラー

バイト列を 1 度読んでから分岐するため、リモートモデルのダウンロードは 1 回で済みます。

エラー

CoreErrorErrorKind とメッセージを持ちます。種別は安定した文字列で、ホスト言語へそのまま公開することを想定しています。

種別as_str()発生条件
InvalidArgumentinvalid_argument未知の言語名、未知の feature set、使用済みトレーナ
Modelmodelダウンロード失敗、または種別の異なるモデル
Ioioファイルの読み書き失敗
Parseparseモデルまたは学習データの形式不正
Unsupportedunsupportedこのビルドでは利用できないスキームや操作
PosUnavailablepos_unavailable分割専用モデルに対して POS タグ付けを要求した
Runtimeruntime上記以外

この一覧は remote_model feature の有無で変化しないため、バインディング側の例外階層は固定できます。

学習

ネイティブターゲットのみで利用できます。特徴量抽出と学習はファイル入出力を前提としているためです。

#![allow(unused)]
fn main() {
use litsea::Language;
use litsea_binding_core::{CancelToken, CoreExtractor, CoreTrainer, CorpusFormat};

CoreExtractor::new(Language::Japanese).extract(
    "corpus.txt".as_ref(),
    "features.txt".as_ref(),
    CorpusFormat::PlainText,
    false, // tag_free
)?;

let metrics = CoreTrainer::new(0.01, 10_000, "features.txt".as_ref())?
    .train(&CancelToken::new(), "japanese.model".as_ref())?;
println!("accuracy: {:.2}%", metrics.accuracy);
}

CoreTwoStageTrainer は CLI の train --pos に対応します。litseaTwoStageTrainer::train がトレーナを消費する(stage 1 は AdaBoost モデルへ collapse されるため、その場での再学習ができない)ため、このトレーナは 1 度しか使えません。2 回目の呼び出しは InvalidArgument エラーを返し、is_available() で状態を確認できます。

キャンセルの挙動

キャンセルは協調的であり、エラーではありません

  • トレーナは次のチェックポイントで停止し、
  • 部分的に学習されたモデルは指定パスへ保存され、
  • メトリクスが通常どおり返されます。

チェックは AdaBoost 学習ではブースティング反復ごと、パーセプトロン学習ではエポックごと・インスタンスごとに行われるため、パーセプトロン学習の方が反応がはるかに速くなります。CancelToken のクローンは同じフラグを共有するので、バックグラウンドスレッドに渡したトークンから、別スレッドが実行中の学習を停止できます。

プラットフォームサポート

wasm32-unknown-unknown では trainerruntimeread_model_fileCoreSegmenter::from_path はコンパイル対象から外れます。wasm32 にはファイルシステムもブロッキングランタイムも存在しないためです。WASM から使う場合は、JavaScript 側でモデルのバイト列を取得し CoreSegmenter::from_bytes に渡してください。

Feature

Feature既定効果
remote_model無効litsea/remote_model を有効化し、http(s):// のモデル URI を解決できるようにする

Python

litsea-pythonPyO3maturin を用いて Litsea を Python 3.10 以降へ公開するバインディングです。PyPI では litsea という名前で配布します。

インストール

pip install litsea

wheel は安定 ABI(abi3-py310)でビルドされるため、プラットフォームごとに 1 つの wheel がサポート対象の全 Python バージョンをカバーします。

モデルの入手

パッケージにモデルは含まれません。models/ ディレクトリから取得してパスを渡してください(事前学習済みモデルを参照)。

モデルの種別を指定するフラグはありません。ファイル自身が種別を持っており、読み込んだモデルで何ができるかは has_pos が示します。

分割

from litsea import Language, Segmenter

seg = Segmenter.open(Language.JAPANESE, "models/japanese.model")

seg.segment("これはテストです。")
# ['これ', 'は', 'テスト', 'です', '。']

Language を受け取る箇所では言語名も使えます。Segmenter.open("ja", ...)Segmenter.open("japanese", ...) は等価です。

空白区切りの言語では空白自体が 1 トークンとして返るため、トークンを連結すると常に入力が復元されます。

Segmenter.open("ko", "models/korean.model").segment("안녕하세요 반갑습니다")
# ['안녕하세요', ' ', '반갑습니다']

POS タグ付け

seg = Segmenter.open(Language.JAPANESE, "models/japanese_pos.model")

for token in seg.segment_with_pos("これはテストです。"):
    print(token.surface, token.pos.name, token.start, token.end)
# これ PRON 0 6
# は ADP 6 9
# テスト NOUN 9 18
# です AUX 18 24
# 。 PUNCT 24 27

startend は入力に対するバイトオフセットです。text.encode()[token.start:token.end].decode() で表層形が得られます。分割専用モデルに対して segment_with_pos を呼ぶと PosUnavailableError が送出されます。

API

呼び出し戻り値
Segmenter.open(language, path)ファイルから読み込んだセグメンタ
Segmenter.from_bytes(language, data)バイト列から読み込んだセグメンタ
Segmenter.from_uri(language, uri)パス・file://http(s):// から読み込んだセグメンタ
segment(text)list[str]
segment_batch(texts)list[list[str]]
segment_tokens(text)バイトオフセット付き list[Token]
segment_with_pos(text)タグとオフセット付き list[Token]
segment_with_pos_batch(texts)list[list[Token]]
Extractor(language).extract(...)特徴量ファイルを書き出す
Extractor(language).extract_two_stage(...).stage1 / .stage2 / .lexicon を書き出す
Trainer(threshold, iterations, features).train(model, cancel=None)BinaryMetrics
PerceptronTrainer(epochs, features).train(model, cancel=None)MulticlassMetrics
TwoStageTrainer(epochs, prefix, dominance=0.99).train(model, cancel=None)TwoStageMetrics

LanguageUposenum.Enum のサブクラスではなく PyO3 のクラスです。メンバーはクラス属性なので、列挙には for x in Language ではなく Language.all() / Upos.all() を使ってください。

学習

from litsea import Extractor, Language, Trainer

Extractor(Language.JAPANESE).extract("corpus.txt", "features.txt")
metrics = Trainer(0.01, 10_000, "features.txt").train("japanese.model")
print(f"accuracy: {metrics.accuracy:.2f}%")

TwoStageTrainer は 1 度しか実行できません。学習時に stage 1 が AdaBoost モデルへ collapse され、トレーナが消費されるためです。再利用可能かどうかは available が示し、2 回目の train()InvalidArgumentError を送出します。

キャンセル

学習中は GIL が解放されるため、別スレッドから停止できます。

import threading
from litsea import CancelToken, Trainer

cancel = CancelToken()
threading.Timer(60.0, cancel.cancel).start()
metrics = Trainer(0.01, 100_000, "features.txt").train("japanese.model", cancel=cancel)

キャンセルはエラーではありません。次のチェックポイントで停止し、部分的に学習されたモデルを保存してメトリクスを返します。バインディングはシグナルハンドラを登録しないため、Ctrl-C の扱いはアプリケーション側のままです。

エラー

すべての例外は LitseaError を継承します。

例外発生条件
InvalidArgumentError未知の言語名、未知の feature set、使用済みトレーナ
ModelErrorダウンロード失敗、または旧 joint POS モデル
IoErrorファイルの読み書き失敗
ParseErrorモデルまたは学習データの形式不正
UnsupportedErrorこのビルドでは利用できないスキームや操作
PosUnavailableError分割専用モデルに対する POS タグ付けの要求

スレッドと GIL

Segmenter はイミュータブルで、スレッド間で共有できます。segment_batchsegment_with_pos_batchextract・各 train は GIL を解放します。

単文の segment / segment_with_pos は GIL を保持します。GIL を解放するには入力文字列を所有する必要があり(PyO3 の Ungil 境界により、GIL 解放中に Python 所有のメモリへ触れられないため)、そのコピーのコストが 1 文の分割コストを上回るからです。大量処理にはバッチ版を使ってください。

開発

make setup-venv            # venv 作成と開発ツールのインストール
make test-litsea-python    # cargo test + maturin develop + pytest
make lint-litsea-python    # clippy + ruff
make build-litsea-python   # リリース wheel を litsea-python/dist へ出力

パリティテストは litsea CLI をビルドして、その出力とバインディングの出力を突き合わせます。何が正しいかを決めるのはハードコードした期待値ではなく参照実装です。

Node.js

litsea-nodejsnapi-rs を用いて Litsea を Node.js 20 以降へ公開するバインディングです。npm では litsea として配布し、Linux・macOS・Windows の x64 / arm64 向けにビルド済みバイナリを提供します。

ブラウザ向けには litsea-wasm を使ってください。

インストール

npm install litsea

モデルの入手

パッケージにモデルは含まれません。models/ から取得してパスを渡してください(事前学習済みモデルを参照)。モデルファイル自身が種別を持つため、フラグの指定は不要で、読み込んだモデルで何ができるかは hasPos が示します。

分割

import { Segmenter } from 'litsea'

const seg = Segmenter.open('japanese', 'models/japanese.model')

seg.segment('これはテストです。')
// [ 'これ', 'は', 'テスト', 'です', '。' ]

言語名と ISO 639-1 コードのどちらも使えます('ja' / 'japanese')。

空白区切りの言語では空白自体が 1 トークンとして返るため、トークンを連結すると常に入力が復元されます。

Segmenter.open('ko', 'models/korean.model').segment('안녕하세요 반갑습니다')
// [ '안녕하세요', ' ', '반갑습니다' ]

POS タグ付け

const seg = Segmenter.open('japanese', 'models/japanese_pos.model')

seg.segmentWithPos('これはテストです。')
// [ { surface: 'これ', start: 0, end: 6, pos: 'PRON' },
//   { surface: 'は', start: 6, end: 9, pos: 'ADP' },
//   { surface: 'テスト', start: 9, end: 18, pos: 'NOUN' },
//   { surface: 'です', start: 18, end: 24, pos: 'AUX' },
//   { surface: '。', start: 24, end: 27, pos: 'PUNCT' } ]

startendバイトオフセットです。JavaScript の文字列インデックスは UTF-16 コードユニットなので、切り出しには Buffer を使ってください。

Buffer.from(text).subarray(token.start, token.end).toString()   // === token.surface

タグ付けを行わない segmentTokens のトークンでは posundefined になります。

API

呼び出し戻り値
Segmenter.open(language, path)セグメンタ(同期)
Segmenter.fromBytes(language, buffer)セグメンタ(同期)
Segmenter.fromUri(language, uri)Promise<Segmenter>(イベントループ外でダウンロード)
segment(text)string[]
segmentBatch(texts)string[][]
segmentTokens(text)バイトオフセット付き Token[]
segmentWithPos(text)タグとオフセット付き Token[]
segmentWithPosBatch(texts)Token[][]
new Extractor(language).extract(...)Promise<void>
new Extractor(language).extractTwoStage(...)Promise<void>
new Trainer(threshold, iterations, features).train(model, cancel?)Promise<BinaryMetrics>
new PerceptronTrainer(epochs, features).train(model, cancel?)Promise<MulticlassMetrics>
new TwoStageTrainer(epochs, prefix, dominance?).train(model, cancel?)Promise<TwoStageMetrics>

型定義は napi-rs が生成し、index.d.ts として同梱されます。

非同期設計

モデルのダウンロード・特徴量抽出・学習はいずれも Promise を返し、libuv のスレッドプールで実行されるため、イベントループは止まりません。これがキャンセルを有効にしている理由でもあります。

import { CancelToken, Trainer } from 'litsea'

const cancel = new CancelToken()
setTimeout(() => cancel.cancel(), 60_000)

const metrics = await new Trainer(0.01, 100_000, 'features.txt').train('japanese.model', cancel)

キャンセルはエラーではありません。次のチェックポイントで停止し、部分的に学習されたモデルを保存してメトリクスで resolve します。バインディングはシグナルハンドラを登録しません。

分割自体は同期処理です。Promise のコストの方が処理そのものより大きいためです。

TwoStageTrainer は 1 度しか使えません。学習時に stage 1 が AdaBoost モデルへ collapse され、トレーナが消費されるためです。状態は available が示し、2 回目の train() は reject されます。

エラー

すべてのエラーは、他のバインディングと同じ種別を表す code を持ちます。reject された Promise も throw されたエラーと同じ code を持ちます。

err.code発生条件
invalid_argument未知の言語名、未知の feature set、使用済みトレーナ
modelダウンロード失敗、または旧 joint POS モデル
ioファイルの読み書き失敗
parseモデルまたは学習データの形式不正
unsupportedこのビルドでは利用できないスキームや操作
pos_unavailable分割専用モデルに対する POS タグ付けの要求

napi::Status が閉じた enum のため、code は 2 つの経路で JavaScript に渡ります。同期呼び出しは napi の文字列ステータス付きエラーを使い、非同期呼び出しは Task::reject で JavaScript の Error オブジェクトを再構築してプロパティを reject 後も保持させています。

開発

make test-litsea-nodejs    # cargo test + napi build + node --test
make lint-litsea-nodejs    # clippy
make build-litsea-nodejs   # リリースビルド

index.jsindex.d.tsnapi build が生成したものをコミットしており、CI で再生成して内容が古い場合は失敗させます。パリティテストは litsea CLI をビルドし、その出力とバインディングの出力を突き合わせます。

PHP

litsea-phpext-php-rs を用いて Litsea を PHP 8.1 以降へ公開するバインディングです。Packagist では litsea/litsea として配布します。

インストール

PHP 拡張は特定の PHP ABI 向けにビルドされた共有オブジェクトです。PyPI や npm と異なりビルド済みパッケージの配布はなく、自分でビルドして有効化します。

cargo build --release -p litsea-php
php -d extension=/path/to/target/release/liblitsea_php.so your-script.php

常時読み込む場合は php.iniextension=/path/to/liblitsea_php.so を追記します。ビルドには Rust ツールチェーンと libclang が必要です。

モデルの入手

拡張にモデルは含まれません。models/ から取得してパスを渡してください(事前学習済みモデルを参照)。モデル自身が種別を持つため、フラグの指定は不要で、読み込んだモデルで何ができるかは hasPos() が示します。

分割

use Litsea\Segmenter;

$seg = Segmenter::open('japanese', 'models/japanese.model');

$seg->segment('これはテストです。');
// ['これ', 'は', 'テスト', 'です', '。']

言語名と ISO 639-1 コードのどちらも使えます('ja' / 'japanese')。

空白区切りの言語では空白自体が 1 トークンとして返るため、トークンを連結すると常に入力が復元されます。

Segmenter::open('ko', 'models/korean.model')->segment('안녕하세요 반갑습니다');
// ['안녕하세요', ' ', '반갑습니다']

POS タグ付け

$seg = Segmenter::open('japanese', 'models/japanese_pos.model');

foreach ($seg->segmentWithPos('これはテストです。') as $token) {
    printf("%s\t%s\t[%d:%d]\n", $token->surface, $token->pos, $token->start, $token->end);
}
// これ    PRON    [0:6]
// は      ADP     [6:9]
// テスト  NOUN    [9:18]
// です    AUX     [18:24]
// 。      PUNCT   [24:27]

startend はバイトオフセットで、PHP の文字列はバイト列です。そのため substr($text, $token->start, $token->end - $token->start) がそのまま表層形を返します(JavaScript のようなエンコーディングを意識した切り出しは不要です)。

API

呼び出し戻り値
Segmenter::open($language, $path)セグメンタ
Segmenter::fromBytes($language, $contents)セグメンタ
Segmenter::fromUri($language, $uri)セグメンタ(ダウンロードはブロッキング)
segment($text)string[]
segmentBatch($texts)string[][]
segmentTokens($text)バイトオフセット付き Token[]
segmentWithPos($text)タグとオフセット付き Token[]
segmentWithPosBatch($texts)Token[][]
(new Extractor($language))->extract(...)void
(new Extractor($language))->extractTwoStage(...)void
(new Trainer($threshold, $iterations, $features))->train($model, $cancel?)BinaryMetrics
(new PerceptronTrainer($epochs, $features))->train($model, $cancel?)MulticlassMetrics
(new TwoStageTrainer($epochs, $prefix, $dominance?))->train($model, $cancel?)TwoStageMetrics

ext-php-rs はメソッドとプロパティを camelCase に変換するため、PHP 側では segmentWithPos()hasPos()$metrics->numInstances となります。

キャンセルは呼び出し前のみ有効

ここが他のバインディングと唯一異なる点です。これはバインディング側の不足ではなく、ホスト言語の性質です。

Python バインディングは GIL を解放し、Node.js バインディングは学習をワーカースレッドで実行するため、どちらも実行中の学習を停止できます。PHP のリクエストはシングルスレッドであり、pcntl のシグナルハンドラはブロッキング中のネイティブ呼び出しを中断できないため、train() の実行中に PHP のコードは一切動きません。したがって CancelToken は、呼び出し前にキャンセルした場合のみ効果があります。

$cancel = new Litsea\CancelToken();
$cancel->cancel();

$metrics = (new Litsea\Trainer(0.01, 100000, 'features.txt'))->train('japanese.model', $cancel);

キャンセルはエラーではありません。次のチェックポイントで停止し、部分的に学習されたモデルを保存してメトリクスを返します。

すべての処理がブロッキングであるため、学習は Web リクエストではなく CLI SAPI から実行してください。

エラー

すべての例外は Litsea\LitseaException を継承するため、1 つの catch で捕捉できます(Python バインディングと同じ階層です)。

例外発生条件
Litsea\InvalidArgumentException未知の言語名、未知の feature set、使用済みトレーナ
Litsea\ModelExceptionダウンロード失敗、または旧 joint POS モデル
Litsea\IoExceptionファイルの読み書き失敗
Litsea\ParseExceptionモデルまたは学習データの形式不正
Litsea\UnsupportedExceptionこのビルドでは利用できないスキームや操作
Litsea\PosUnavailableException分割専用モデルに対する POS タグ付けの要求

開発

make test-litsea-php    # cargo test + 拡張のビルド + PHPUnit
make lint-litsea-php    # clippy
make build-litsea-php   # リリースビルド

パリティテストは litsea CLI をビルドし、その出力とバインディングの出力を突き合わせます。

Ruby

litsea-rubymagnus と rb-sys を用いて Litsea を Ruby 3.1 以降へ公開するバインディングです。RubyGems では litsea として配布します。

インストール

gem install litsea

gem はソース配布で、インストール時に拡張をコンパイルするため Rust ツールチェーンが必要です。

モデルの入手

gem にモデルは含まれません。models/ から取得してパスを渡してください(事前学習済みモデルを参照)。モデル自身が種別を持つため、フラグの指定は不要で、読み込んだモデルで何ができるかは has_pos? が示します。

分割

require "litsea"

seg = Litsea::Segmenter.open(:japanese, "models/japanese.model")

seg.segment("これはテストです。")
# => ["これ", "は", "テスト", "です", "。"]

言語は Symbol でも String でも指定でき、ISO 639-1 コードも使えます(:ja / "japanese")。

空白区切りの言語では空白自体が 1 トークンとして返るため、トークンを連結すると常に入力が復元されます。

Litsea::Segmenter.open(:korean, "models/korean.model").segment("안녕하세요 반갑습니다")
# => ["안녕하세요", " ", "반갑습니다"]

POS タグ付け

seg = Litsea::Segmenter.open(:japanese, "models/japanese_pos.model")

seg.segment_with_pos("これはテストです。").each do |token|
  puts "#{token.surface}\t#{token.pos}\t[#{token.start}..#{token.end}]"
end
# これ    PRON    [0..6]
# は      ADP     [6..9]
# テスト  NOUN    [9..18]
# です    AUX     [18..24]
# 。      PUNCT   [24..27]

startendバイトオフセットです。Ruby の String#[] は文字単位なので、切り出しには byteslice を使ってください。

text.byteslice(token.start, token.end - token.start)   # == token.surface

API

呼び出し戻り値
Litsea::Segmenter.open(language, path)セグメンタ
Litsea::Segmenter.from_bytes(language, data)セグメンタ(バイナリ String も可)
Litsea::Segmenter.from_uri(language, uri)セグメンタ
#segment(text)Array<String>
#segment_batch(texts)Array<Array<String>>
#segment_tokens(text)バイトオフセット付き Array<Litsea::Token>
#segment_with_pos(text)タグとオフセット付き Array<Litsea::Token>
#segment_with_pos_batch(texts)Array<Array<Litsea::Token>>
Litsea::Extractor.new(language)#extract(...)nil
Litsea::Extractor.new(language)#extract_two_stage(...)nil
Litsea::Trainer.new(threshold, iterations, features)#train(model, cancel:)BinaryMetrics
Litsea::PerceptronTrainer.new(epochs, features)#train(model, cancel:)MulticlassMetrics
Litsea::TwoStageTrainer.new(epochs, prefix, dominance:)#train(model, cancel:)TwoStageMetrics

GVL の解放

モデルの読み込み・特徴量抽出・学習といった時間のかかる処理は、GVL(Global VM Lock)を解放した状態で実行されます。そのため他の Ruby スレッドが動き続け、キャンセルが意味を持ちます。

cancel = Litsea::CancelToken.new
Thread.new { sleep 60; cancel.cancel }

metrics = Litsea::Trainer.new(0.01, 100_000, "features.txt").train("japanese.model", cancel: cancel)

キャンセルはエラーではありません。次のチェックポイントで停止し、部分的に学習されたモデルを保存してメトリクスを返します。バインディングはシグナルハンドラを登録しません。

rb_thread_call_without_gvl は magnus も rb-sys もラップしていません(magnus は未バインドの C 関数一覧に挙げており、この関数は rb-sys が生成するバインディングの対象外ヘッダで宣言されています)。そのため本バインディングは src/gvl.rs で自ら宣言し、パニックが C フレームを越えて巻き戻らないよう extern "C" のトランポリンで捕捉しています。この主張は 2 つのテストで担保しています。1 つは学習中に別の Ruby スレッドが動き続けること、もう 1 つは別スレッドからのキャンセルが学習ウィンドウ内に収まることです。GVL 解放を外すと両方とも失敗します。

1 文の分割は GVL を保持したままです。解放のコストの方が処理そのものより大きいためです。

TwoStageTrainer は 1 度しか使えません。学習時に stage 1 が AdaBoost モデルへ collapse され、トレーナが消費されるためです。状態は available? が示し、2 回目の train は例外を発生させます。

エラー

すべてのエラーは Litsea::Error を継承するため、1 つの rescue で捕捉できます(Python・PHP バインディングと同じ階層です)。

エラー発生条件
Litsea::InvalidArgumentError未知の言語名、未知の feature set、使用済みトレーナ
Litsea::ModelErrorダウンロード失敗、または旧 joint POS モデル
Litsea::IoErrorファイルの読み書き失敗
Litsea::ParseErrorモデルまたは学習データの形式不正
Litsea::UnsupportedErrorこのビルドでは利用できないスキームや操作
Litsea::PosUnavailableError分割専用モデルに対する POS タグ付けの要求

開発

make test-litsea-ruby    # cargo test + rake compile + rake test
make lint-litsea-ruby    # clippy + rubocop
make build-litsea-ruby   # リリースビルド

有効な Ruby で bundle が使える必要があります。バージョン管理ツールの shim が存在していても、選択中のインタプリタに bundler が無い場合があるため、Makefile がその旨を検出して案内します。パリティテストは litsea CLI をビルドし、その出力とバインディングの出力を突き合わせます。

WebAssembly

litsea-wasmwasm-bindgen を用いて、ブラウザ・Deno・各種バンドラで Litsea を動かすバインディングです。npm では litsea-wasm として配布します。

Node.js ではネイティブバインディングの litsea を使ってください。速度が上で、モデルの学習にも対応しています。

インストール

npm install litsea-wasm

モジュールサイズは 178KB(gzip 82KB)(リリースビルドでの実測値)です。モデルは別途ダウンロードします。

使い方

import init, { Segmenter } from 'litsea-wasm'

await init()

const bytes = new Uint8Array(await (await fetch('/models/japanese.model')).arrayBuffer())
const seg = Segmenter.fromBytes('japanese', bytes)

seg.segment('これはテストです。')
// [ 'これ', 'は', 'テスト', 'です', '。' ]

seg.free()

言語名と ISO 639-1 コードのどちらも使えます。モデルファイル自身が種別を持つため、読み込んだモデルで何ができるかは hasPos が示します。

POS タグ付け

seg.segmentWithPos('これはテストです。')
// [ Token { surface: 'これ', pos: 'PRON', start: 0, end: 6 }, ... ]

startend は UTF-8 でのバイトオフセットです。JavaScript の文字列インデックスは UTF-16 コードユニットなので、切り出しには TextEncoder / TextDecoder を使ってください。

const bytes = new TextEncoder().encode(text)
new TextDecoder().decode(bytes.subarray(token.start, token.end))   // === token.surface

ホストの制約で提供しない機能

5 つのバインディングの中で最も制約が強く、いずれも推測ではなく実測に基づいて判断しています。

無い機能理由
fromUricargo check --target wasm32-unknown-unknown --features remote_model が失敗する。reqwest の wasm バックエンドには connect_timeout が無く、litsea::model_io はそれを設定しているため。代わりにページ側で fetch する(キャッシュ・CORS・進捗の制御もページ側に残る)
学習技術的な制約ではなく方針判断。下記を参照
CancelToken学習が無い以上、キャンセル対象が存在しない

学習を提供しない理由

#218litsea にファイルシステム非依存の extract/train API が入り、wasm32 でもコンパイルできるため、このバインディングでも学習を公開すること自体は可能です。それでも提供しないのは次の 2 点によります(#221)。

  • ブラウザは学習を行う場所として適さない。 タブがコーパス・そこから抽出した特徴量(コーパスよりかなり大きい)・モデルを同時に保持することになります。API の形を決めるにはまずそのメモリ実測が必要で、実用的なコーパス規模では成立しないという結論も十分あり得ます。
  • 参照実装も持たない。 lindera-pythonlindera-nodejslindera-phplindera-ruby はいずれもトレーナを備えますが、lindera-wasm は備えていません。litsea のバインディング群もこの形に揃えています。

学習は CLI かネイティブバインディングで行い、生成したモデルをここで読み込んでください。

メモリ

Segmenter はコンパイル済みモデル(POS モデルなら数 MB)を保持し、WebAssembly のオブジェクトは GC されません。不要になったら free() を呼んでください。

モデルのキャッシュ

モデルは 84KB〜8MB あり、訪問者ごとにネットワークを通ります。これはネイティブ版には無い、このバインディング固有のコストです。補助モジュールを同梱しています。

import { fetchModel, clearModelCache } from 'litsea-wasm/js/cache.js'

const bytes = await fetchModel('/models/japanese.model')

URL をキーに Cache Storage へ保存し、利用できない環境(非セキュアコンテキストなど)では通常の fetch にフォールバックするため、呼び出し側での分岐は不要です。wasm モジュール外の素の JavaScript なので、使わないページには一切コストがかかりません。

エラー

すべてのエラーは Node.js バインディングと同じ code を持ち、2 つの JavaScript バインディングで扱いが揃います。

err.code発生条件
invalid_argument未知の言語名
model旧 joint POS モデル
parseモデルの形式不正、または UTF-8 でない
pos_unavailable分割専用モデルに対する POS タグ付けの要求

開発

make test-litsea-wasm    # cargo test + ヘッドレスブラウザテスト
make lint-litsea-wasm    # wasm32 での clippy
make build-litsea-wasm   # wasm-pack build --target web

ブラウザテストはプロセスを起動できないため、tests/generate_fixtures.sh が先に litsea CLI を実行して出力を書き出し、テストはそれとの一致を検証します。他のバインディングと同じく、正解は常に参照実装が決めます。

ブラウザは make test-litsea-wasm WASM_BROWSER=chrome で切り替えられます。全テストが成功した直後に PermissionDenied で失敗する場合、PATH 上の geckodriver が snap 制約下にあります(テスト自体は実行済みです)。

トレーニングガイド

このガイドでは、Litsea で独自の単語分割モデルと品詞推定モデルを学習する手順を説明します。

両方のワークフローとも、データソースとして Universal Dependencies (UD) Treebanks を使用します。

単語分割(AdaBoost)

  1. UD Treebank をダウンロードしてコーパスを準備: conllu_file=$(bash scripts/download_udtreebank.sh -l ja -o /tmp) && bash scripts/corpus_udtreebank.sh "$conllu_file" corpus.txt
  2. コーパスから特徴量を抽出する
  3. AdaBoost でモデルを訓練する

品詞推定(二段構成)

  1. UD Treebank をダウンロードして品詞付きコーパスを準備: conllu_file=$(bash scripts/download_udtreebank.sh -l ja -o /tmp) && bash scripts/corpus_udtreebank.sh -p "$conllu_file" pos_corpus.txt
  2. 二段構成の特徴量を抽出: litsea extract --pos -l japanese pos_corpus.txt features
  3. 二段構成の POS モデルを訓練: litsea train --pos --num-epochs 50 features model.model

言語ごとの違い

パイプライン(準備 → 抽出 → 学習)とスクリプトは 4 言語で共通です。 言語固有なのは次の 2 点だけです:

  1. extract-l フラグ — 言語別の文字種分類を選択します(日本語 8 種、 中国語 9 種、韓国語 10 種、英語 7 種。韓国語と英語は WC 特徴を使いません — 言語サポート概要を参照)。このためモデルは 言語専用になります
  2. 韓国語と英語は空白保持 TSV コーパス形式を使用 — どちらも単語間に空白を 持つ表記で、空白が最も強い境界シグナルです。そのためそれぞれのコーパスは 空白をトークンとして保持します(corpus_udtreebank.sh -s + litsea extract --format tsv)。日本語・中国語は空白を使わない表記のため、 従来の空白区切り形式を使います
# Japanese / Chinese: space-separated corpus
bash scripts/corpus_udtreebank.sh "$conllu_file" corpus.txt
litsea extract -l japanese corpus.txt features.txt

# Korean / English: space-preserving TSV corpus
bash scripts/corpus_udtreebank.sh -s "$conllu_file" corpus.tsv
litsea extract -l korean --format tsv corpus.tsv features.txt

train ステップのコマンドの形は 4 言語とも同一ですが、実際に使う ハイパーパラメータは異なります。litsea train でゼロから通常の AdaBoost モデルを学習する場合、-t 0.0001 -i 20000モデルの学習を参照)は 良い出発点ですが、これは同梱の japanese/chinese/korean/english モデルが 使っている値ではありません。それらのモデルは言語ごとに異なるエポック数と 剪定を伴う別の手順で学習されています。実際の手順は 学習手順を参照してください。

その他のトピック

コーパスの準備

良質な学習コーパスは、モデルの精度にとって不可欠です。このガイドでは、Universal Dependencies (UD) Treebanks を使用したコーパスの準備方法を説明します。

データソース: UD Treebanks

Litsea は単語分割と品詞推定の両方のデータソースとして UD Treebanks を使用します。UD Treebanks は、多くの言語に対して CoNLL-U 形式の高品質な手動アノテーション済みデータを提供しています。

利用可能な Treebanks

言語ツリーバンクリポジトリ
日本語UD Japanese-GSDUD_Japanese-GSD
中国語UD Chinese-GSDUD_Chinese-GSD
韓国語UD Korean-GSDUD_Korean-GSD
英語UD English-EWTUD_English-EWT

ステップ 1: UD Treebank のダウンロード

scripts/download_udtreebank.sh を使用して UD Treebank をダウンロードします。スクリプトは学習用 CoNLL-U ファイルのパスを標準出力に出力します:

conllu_file=$(bash scripts/download_udtreebank.sh -l ja -o /tmp)

対応言語: ja(日本語、デフォルト)、ko(韓国語)、zh(中国語)、en(英語)。-o で出力ディレクトリを指定できます(デフォルト: カレントディレクトリ)。

単語分割用コーパス

単語分割(AdaBoost)用のコーパスは以下の条件を満たすプレーンテキストファイルである必要があります:

  • 1行1文
  • 単語をスペースで区切る
太郎 は 走っ た 。
Litsea は コンパクト な 単語 分割 ソフトウェア です 。

CoNLL-U から単語分割用コーパスに変換

scripts/corpus_udtreebank.sh を使用して、CoNLL-U ファイルからスペース区切りの単語を抽出します:

conllu_file=$(bash scripts/download_udtreebank.sh -l ja -o /tmp)
bash scripts/corpus_udtreebank.sh "$conllu_file" corpus.txt

これにより、CoNLL-U データがスペース区切りの単語(1行1文)に変換されます。

空白保持 TSV コーパス(韓国語、英語)

上記のスペース区切り形式では、元の文の空白情報は失われます(学習時には単語が空白なしで連結されます)。韓国語と英語ではこれにより最も強力な境界シグナル(韓国語の語節間の空白、英語の単語間の空白)が失われてしまうため、代わりに -s フラグを使用します。このフラグは、(ツリーバンクの SpaceAfter アノテーションから復元した)元の各空白を独立したトークンとして保持する タブ区切りコーパス を出力します:

conllu_file=$(bash scripts/download_udtreebank.sh -l ko -o /tmp)
bash scripts/corpus_udtreebank.sh -s "$conllu_file" ko_corpus.tsv

conllu_file=$(bash scripts/download_udtreebank.sh -l en -o /tmp)
bash scripts/corpus_udtreebank.sh -s "$conllu_file" en_corpus.tsv

英語に特有の点として、-s は複合語トークン(don't のような短縮形。CoNLL-U では 2 つの単語行にまたがる範囲行として表現されます)も処理します: 範囲に属する単語同士は間に空白トークンを挟まずに連結され、範囲自体の SpaceAfter アノテーションは最後の構成単語の後に適用されます。詳細は英語を参照してください。

TSV コーパスからの特徴量抽出には litsea extract --format tsv を使用します。-s-p と併用でき、単語/品詞 トークンの空白保持 TSV を出力します – これがスペース区切りの言語における二段構成 POS の学習コーパスであり(issue #198)、litsea extract --pos --format tsv で読み込みます。

品詞推定用コーパス

品詞推定(Averaged Perceptron)を行う場合、各単語に品詞タグを付与した形式のコーパスを使用します。

品詞付きコーパスの形式

1行1文で、各単語を 単語/品詞 の形式でスペース区切りに記述します:

これ/PRON は/ADP テスト/NOUN です/AUX 。/PUNCT
Litsea/PROPN は/ADP 単語/NOUN 分割/NOUN ソフトウェア/NOUN です/AUX 。/PUNCT

品詞タグは Universal POS (UPOS) タグセットに準拠し、17カテゴリで構成されます: ADJ, ADP, ADV, AUX, CCONJ, DET, INTJ, NOUN, NUM, PART, PRON, PROPN, PUNCT, SCONJ, SYM, VERB, X。

CoNLL-U から品詞付きコーパスに変換

scripts/corpus_udtreebank.sh-p オプションを指定して、CoNLL-U ファイルを 単語/品詞 形式に変換します:

conllu_file=$(bash scripts/download_udtreebank.sh -l ja -o /tmp)
bash scripts/corpus_udtreebank.sh -p "$conllu_file" pos_corpus.txt

複合語トークンや空ノードは変換時に自動的に処理されます。

コーパスの自動作成

Litsea には、UD Treebank のダウンロードと変換を自動化するヘルパースクリプトが scripts/ ディレクトリに用意されています:

  • scripts/download_udtreebank.sh – UD Treebank をダウンロードし、学習用 CoNLL-U ファイルのパスを出力します
  • scripts/corpus_udtreebank.sh – CoNLL-U ファイルを Litsea のコーパス形式に変換します
# UD Treebank をダウンロードして CoNLL-U ファイルのパスを取得
conllu_file=$(bash scripts/download_udtreebank.sh -l ja -o /tmp)

# 単語分割用コーパスを生成
bash scripts/corpus_udtreebank.sh "$conllu_file" corpus.txt

# 品詞付きコーパスを生成
bash scripts/corpus_udtreebank.sh -p "$conllu_file" pos_corpus.txt

download_udtreebank.sh の対応言語: ja(日本語、デフォルト)、ko(韓国語)、zh(中国語)、en(英語)。

Wikipedia ダンプからのコーパス作成

大規模な学習データが必要な場合、Wikipedia の全文ダンプから scripts/corpus_wikidump.sh を使ってコーパスを作成できます。このスクリプトは wicket でプレーンテキストを抽出し、文章のみをフィルタリングした後、lindera でトークナイズします。

使い方

# 日本語(デフォルト)
bash scripts/corpus_wikidump.sh jawiki-latest-pages-articles.xml.bz2 corpus_ja.txt

# 韓国語
bash scripts/corpus_wikidump.sh -l ko kowiki-latest-pages-articles.xml.bz2 corpus_ko.txt

# 中国語
bash scripts/corpus_wikidump.sh -l zh zhwiki-latest-pages-articles.xml.bz2 corpus_zh.txt

オプション

オプション説明デフォルト
-l lang言語コード: ja, ko, zhja
-n max_lines処理する最大文数(0 = 無制限)100000

文章フィルタリング

スクリプトは以下の2つのフィルタを適用し、整形された文章のみを抽出します:

  1. 文末記号 – 行が .!? で終わる必要があります。これにより、セクション見出し(例: 「参考文献」)、箇条書き項目、メタデータが除外されます。
  2. 最小文字数 – 行は20文字以上である必要があります。これにより、短い断片や孤立したラベルが除外されます。

トークナイザー辞書

言語辞書トークンフィルタ
日本語 (ja)embedded://unidicjapanese_compound_word(数詞複合語)
韓国語 (ko)embedded://ko-dicなし
中国語 (zh)embedded://cc-cedictなし

コーパスサイズのガイドライン

推奨されるコーパスサイズは用途によって異なります:

サイズ(文数)用途
~10,000プロトタイピングや動作確認の最小限
50,000 – 100,000モデル学習の実用的な範囲
100,000 – 500,000高品質でロバストなモデル
無制限最大精度を目指す場合は全量使用

corpus_wikidump.sh のデフォルト max_lines=100000 は、実用〜高品質の範囲を想定しています。

コーパスの品質に関するヒント

  • 多様性 – さまざまな分野のテキストを含める(ニュース、文学、ウェブなど)
  • データ量 – 推奨サイズはコーパスサイズのガイドラインを参照
  • 一貫性 – コーパス全体で一貫したトークン化を確保する
  • 重複排除 – 偏りを避けるために重複文を除去する
  • クリーニング – HTML タグ、特殊なフォーマット、非テキストコンテンツを除去する

特徴量の抽出

コーパスの準備ができたら、次のステップはモデル学習用の特徴量を抽出することです。

コマンド

litsea extract -l <LANGUAGE> <CORPUS_FILE> <FEATURES_FILE>

使用例

litsea extract -l japanese ./corpus.txt ./features.txt

出力:

Feature extraction completed successfully.

内部処理の仕組み

flowchart TD
    A["Read corpus line by line"] --> B["Split line into words"]
    B --> C["Build chars, types, and tags arrays"]
    C --> D["For each character position"]
    D --> E["Extract 38-42 features"]
    E --> F["Write label + features to file"]
  1. Extractor がコーパスの各行を読み込む
  2. 各文に対して、文字配列・文字種配列・タグ配列を持つ Segmenter コンテキストを作成する
  3. 各文字位置(先頭を除く)について特徴量を抽出し、正しいラベルとともに書き込む。二段構成の stage-1 パイプラインでは先頭位置も出力され、最初の単語の境界判定が学習データに含まれるようになっている

特徴量ファイルの形式

各行は1つの文字位置を表します。コーパス行 これ は テスト です 。 に対する最初の2行は次のとおりです:

-1	BC1:OI	BC2:II	BC3:II	BP1:UU	BP2:UU	BQ1:UOI	BQ2:UII	BQ3:UOI	BQ4:UII	...
1	BC1:II	BC2:II	BC3:IK	BP1:UU	BP2:UO	BQ1:UII	BQ2:UII	BQ3:OII	BQ4:OII	...
  • 最初の列: ラベル(1 = 境界、-1 = 非境界)
  • 残りの列: 特徴量。アルファベット順にソートされてタブ区切りで書き出される(そのため各行は BC1: 特徴量から始まる)

空白保持(TSV)コーパス形式

文の元の空白を保持したコーパス — 韓国語モデルおよび英語モデルの学習に使用 (単語間の空白がこれらの言語における最も強力な境界シグナルであるため。詳細は 韓国語 および 英語 を参照) — から抽出するには、既定のスペース区切り形式の代わりに --format tsv を 指定します:

litsea extract --format tsv -l korean ./ko_corpus.tsv ./ko_features.txt
litsea extract --format tsv --tag-free -l english ./en_corpus.tsv ./en_features.txt

入力はタブ区切りのコーパス(1行1文、トークンをタブで区切る)で、トークンとして 空白文字そのもの(" ")を含められます。出力される特徴量ファイルの形式は既定の extract と同一で、コーパスの解析方法のみが異なります。--format tsv--pos とも併用できます(issue #198。後述の二段構成の特徴量抽出を参照)。

二段構成の特徴量抽出

二段構成の品詞タグ付け(issue #147)用には、 --pos を使用します:

litsea extract --pos [--stage2-features full|balanced|fast] <CORPUS_FILE> <FEATURES_PREFIX>

使用例

litsea extract --pos -l japanese ./pos_corpus.txt ./pos_features

--pos は POS タグ付きコーパス(word/POS word/POS ...)を 読み込み、コーパスを1パスで処理し、<FEATURES_PREFIX> から1ファイルではなく 3ファイルを書き出します:

ファイル内容
<FEATURES_PREFIX>.stage1境界特徴量(ラベルは B または O)。通常の抽出と同じ文字レベルテンプレートを、先頭を含む全位置で出力
<FEATURES_PREFIX>.stage2単語単位の特徴量(ラベルは UPOS タグ)。--stage2-features で選択したテンプレート
<FEATURES_PREFIX>.lexicon候補タグ語彙表(surface\tTAG:count[,TAG:count...]、出現頻度の高い順)

litsea train --pos は同じプレフィックスから3ファイルすべてを読み込みます。 コーパスが corpus_udtreebank.sh -p -s の出力する空白保持の word/POS TSV である場合は --format tsv を併用してください(issue #198)– 同梱の韓国語・英語の二段構成モデルが学習に使っているプロトコルです。

--stage2-features の選び方

--stage2-features<FEATURES_PREFIX>.stage2 に書き出す stage-2 の単語単位 テンプレート(単語単位の特徴量テンプレート(二段構成)を参照)を選択し、 タグ付け品質とスループットをトレードオフします:

テンプレートトレードオフ
full全23個の単語テンプレート最も高精度、最も低速
balancedfull のサブセット中間的な構成
fast(既定)最小のサブセット最速、それでいて競争力のある品質

この既定値の背後にある品質・スループットの実測比較については、 stage-2 特徴量セットの選び方を参照してください。

litsea extract --pos --stage2-features balanced -l chinese ./pos_corpus.txt ./pos_features

ファイルサイズの目安

特徴量ファイルは、各文字位置が38-42個の特徴量文字列を生成するため、コーパスよりも大幅に大きくなります。1 MB のコーパスに対して、特徴量ファイルはおよそ 50-100 MB になることが見込まれます。

モデルの学習

特徴量の抽出が完了したら、AdaBoost を使用してモデルを学習します。

コマンド

litsea train [OPTIONS] <FEATURES_FILE> <MODEL_FILE>

基本的な使用例

litsea train -t 0.0001 -i 20000 ./features.txt ./models/my_model.model

これは通常の AdaBoost 学習の一般的な例です。同梱の japanese.modelchinese.modelkorean.modelenglish.modelこの方法では作られていません – これらのファイルの実際の学習手順は 学習手順を参照してください。

学習プロセス

flowchart TD
    A["Initialize features<br/>(read feature names)"] --> B["Initialize instances<br/>(read labels + features)"]
    B --> C["AdaBoost training loop"]
    C --> D{"Converged or<br/>max iterations?"}
    D -->|No| C
    D -->|Yes| E["Save model"]
    E --> F["Output metrics"]
  1. 特徴量の初期化 – 特徴量ファイルを読み込み、特徴量インデックスを構築する
  2. インスタンスの初期化 – 再度読み込み、ラベル付きインスタンスと初期重みをロードする
  3. 学習ループ – 最適な特徴量を反復的に選択し、モデルの重みを更新し、インスタンスの重みを調整する
  4. モデルの保存 – 非ゼロの特徴量の重みをモデルファイルに書き込む
  5. メトリクスの出力 – 正解率、適合率、再現率、混同行列を表示する

ハイパーパラメータ

パラメータフラグデフォルト値ガイダンス
閾値-t0.010.0001 から開始することを推奨。値を低くすると早期停止が遅くなるが、学習時間も増加する
反復回数-i10020000 から開始することを推奨。AdaBoost は 1 反復につき特徴を 1 つ選択するため、この値がモデルの特徴数の上限になる。デフォルト値では非常に小さいモデルになり、held-out 精度が大幅に低くなる

: これらはゼロから通常の AdaBoost モデルを学習する際の一般的な 出発点です。同梱の japanese.modelchinese.modelkorean.modelenglish.model は別の手順(2 クラスの Averaged Perceptron を AdaBoost の重みへ畳み込み、 言語ごとに異なるエポック数と剪定を適用)で作られています – これらの ファイルが実際にどう作られているかは、事前学習済みモデルの 学習手順を参照してください。

出力の解釈

メトリクスは学習データに対して計算されます。反復回数が十分であれば、モデルは学習コーパスにほぼ完全に適合できてしまうため、現実的な品質を見積もるにはホールドアウトされたテキストで評価してください。以下の数値は train の出力形式を示す代表例であり、同梱の japanese.model の実際の学習ログではありません。

Result Metrics:
  Accuracy: 100.00% ( 1075868 / 1075869 )
  Precision: 100.00% ( 161283 / 161284 )
  Recall: 100.00% ( 161283 / 161283 )
  Confusion Matrix:
    True Positives: 161283
    False Positives: 1
    False Negatives: 0
    True Negatives: 914585
  • Accuracy(正解率) – 正しい予測の割合(境界と非境界の両方を含む)
  • Precision(適合率) – 境界と予測されたもののうち、実際に正しかった割合
  • Recall(再現率) – 実際の境界のうち、検出できた割合
  • True Positives(真陽性) – 正しく予測された境界
  • False Positives(偽陽性) – 境界がないのに境界と予測されたもの
  • False Negatives(偽陰性) – 見逃された実際の境界
  • True Negatives(真陰性) – 正しく予測された非境界

途中停止

学習中に Ctrl+C を1回押すと、現在の状態でモデルを保存して停止します。Ctrl+C を2回押すと、保存せずに即時終了します。

汎用パーセプトロンの学習

同梱分割モデルの畳み込みレシピ(学習手順を 参照)には、--perceptron フラグを使用します。label\tfeature\t... 形式の 特徴量ファイルから、不透明な文字列ラベルに対する多クラスの Averaged Perceptron を学習します。

パーセプトロン学習コマンド

litsea train --perceptron --num-epochs 50 <FEATURES_FILE> <MODEL_FILE>

パーセプトロン学習の出力

Result Metrics (Perceptron):
  Accuracy: 98.23% ( 277213 )
  Macro Precision: 96.82%
  Macro Recall: 93.30%
  • Accuracy(正解率) – 全クラスにわたる正しい予測の割合
  • Macro Precision(マクロ適合率) – 全クラスの適合率の平均
  • Macro Recall(マクロ再現率) – 全クラスの再現率の平均

パーセプトロン学習中に Ctrl+C を1回押すと、現在の状態でモデルを保存して停止します。Ctrl+C を2回押すと、保存せずに即時終了します。

二段構成モデルの学習

品詞推定には、--pos フラグを使用します。 二段構成モデル(issue #147)を学習します: 二値の境界分類器(stage 1)と単語単位のタガー(stage 2)を、候補タグ語彙表と ともに単一の litsea-two-stage v1 ファイルに組み立てます。アーキテクチャと 実測の品質・速度の数値については 二段構成タグ付けを参照してください。

二段構成学習コマンド

litsea extract --pos <CORPUS_FILE> <FEATURES_PREFIX>
litsea train --pos --num-epochs 50 <FEATURES_PREFIX> <MODEL_FILE>

extract --posword/POS コーパスを読み込み、 FEATURES_PREFIX から 3 つのファイルを書き出します。train --pos は同じプレフィックスからそれらを読み込みます。

二段構成学習の使用例

litsea extract --pos -l japanese ./pos_corpus.txt ./pos_features
litsea train --pos --num-epochs 50 ./pos_features ./models/japanese_pos.model

二段構成学習のハイパーパラメータ

パラメータフラグデフォルト値ガイダンス
エポック数--num-epochs10同梱モデル作成時のエポックスイープ(方法論についての注記を参照)で、分割品質が既定値を大きく超えて向上し続け 50 付近でプラトーに達すると判明しました – 同梱モデルは 10 ではなく 50 を使用しています
Dominance--dominance0.99分類器スキップの閾値、範囲は (0.5, 1.0]: 既知の単語のうち最頻タグが学習時の出現のこの割合以上を占めるものは、stage-2 分類器を呼ばずにタグ付けされます。値を小さくするとより頻繁に分類器をスキップします(高速だが語彙表への依存度が上がる)。既定値は同梱モデルと同じです
stage-2 特徴量セットextract --pos--stage2-featuresfastfullbalancedfast特徴量の抽出特徴量セットの選び方を参照

二段構成学習の出力

Result Metrics (Two-Stage):
  Stage 1 (boundary) Accuracy: 99.86% ( 277213 )
  Stage 1 Macro Precision: 99.85%
  Stage 1 Macro Recall: 99.86%
  Stage 2 (tagging) Accuracy: 99.09% ( 168333 )
  Stage 2 Macro Precision: 98.96%
  Stage 2 Macro Recall: 98.77%

他のモードと同様、これらは in-sample のメトリクスです。 現実的な品質を見積もるには litsea evaluate --pos でホールドアウトされた テキストを評価してください。

二段構成学習の途中停止

二段構成学習中に Ctrl+C を1回押すと、現在の状態でモデルを保存して停止します。Ctrl+C を2回押すと、保存せずに即時終了します。

モデルの評価

モデルの品質を理解することは、良好な分割結果を得るために不可欠です。

メトリクス

train コマンドは学習後に3つの主要なメトリクスを出力します。これらは in-sample 指標(学習データ自身で測った値)であり、未知のテキストに対する 性能を過大評価します。実際の品質を知るには、必ず学習に使用していない held-out コーパスで評価してください(後述のベンチマークを参照)。

Accuracy(正解率)

Accuracy = (TP + TN) / Total Instances

すべての文字位置のうち、正しく分類された割合(境界と非境界の両方を含む)です。モデル品質の最も広範な指標です。

Precision(適合率)

Precision = TP / (TP + FP)

モデルが予測した境界のうち、正しかった割合です。高い適合率は、誤った境界(過分割)が少ないことを意味します。

Recall(再現率)

Recall = TP / (TP + FN)

実際の境界のうち、モデルが検出した割合です。高い再現率は、見逃された境界(不足分割)が少ないことを意味します。

混同行列

境界と予測 (+1)非境界と予測 (-1)
実際の境界True Positive (TP)False Negative (FN)
実際の非境界False Positive (FP)True Negative (TN)

事前学習済みモデルのベンチマーク

同梱の japanese.modelchinese.modelkorean.modelenglish.model は、 通常の AdaBoost -t/-i 学習ではなく binary-perceptron 畳み込み手順で 学習しています – 正確な手順は学習手順を参照してください。 いずれも学習コーパスの held-out テスト分割で評価しています。単語 F1 は 単語の完全一致、境界 F1 は個々の境界判定のスコアです。

モデル単語 F1境界 F1学習コーパス
japanese.model96.70%98.59%UD Japanese-GSD
korean.model99.91%99.96%UD Korean-GSD
chinese.model90.69%95.64%UD Chinese-GSD
english.model98.31%99.18%UD English-EWT

韓国語と英語は、元の空白を保持したテキスト(空白保持 TSV コーパス。空白 トークンは F1 の計算から除外)で学習・評価しています。この 2 言語では空白が ほとんどの語境界を示すため、空白を使わずに表記される日本語・中国語に比べて タスクが容易になります — このスコアは言語間で直接比較できません。韓国語の ほぼ決定論的な 99.91% と、それより低い英語の 98.31% はどちらもこの同じ 空白保持プロトコルによるものですが、両者の差は空白があってもなお英語に残る 曖昧性(短縮形、ハイフン付き複合語、省略語)に由来します。

ベンチマークの再現

上の表のすべての数値は、同梱のゴールドデータ(resources/eval/、UD GSD の test 分割から変換。同梱モデルは train 分割で学習しているため held-out に あたります)を使って、それぞれ 1 コマンドで再現できます:

litsea evaluate -l japanese models/japanese.model resources/eval/japanese_gsd_test.txt
litsea evaluate -l korean --format tsv models/korean.model resources/eval/korean_gsd_test.tsv
litsea evaluate -l chinese models/chinese.model resources/eval/chinese_gsd_test.txt
litsea evaluate -l english --format tsv models/english.model resources/eval/english_ewt_test.tsv

コマンドリファレンスは evaluate を参照して ください。POS モデルは --pos で評価し、その held-out の数値は 事前学習済みモデルに記載しています。

どのゴールドファイルを使うかはモデルの学習方法によって決まり、2 つの グループで異なります:

# 日本語・中国語: 実際のテキストにスペースがないため、スペース区切りの
# `word/POS` ゴールドがそのまま実運用のプロトコルになります。
litsea evaluate --pos -l japanese models/japanese_pos.model resources/eval/japanese_gsd_test_pos.txt

# 韓国語・英語: 空白保持コーパスで学習しているため(issue #198)、
# --format tsv で空白保持の POS ゴールドに対して評価します。
litsea evaluate --pos --format tsv -l korean models/korean_pos.model resources/eval/korean_gsd_test_pos_spaced.tsv
litsea evaluate --pos --format tsv -l english models/english_pos.model resources/eval/english_ewt_test_pos_spaced.tsv

韓国語・英語向けの *_test_pos.txt ファイル(空白トークンなし)も引き続き 同梱しています。これらは #198 以前にこれらのモデルが学習していた空白非保持 プロトコルを測定するものなので、以前公開していた数値を再現する用途にのみ 有用です – 現在の品質を測るには、上記の *_pos_spaced.tsv ゴールドを 使用してください。こちらはモデルの学習方法と、segment --pos が実際に 受け取る入力の双方に一致します。

モデル品質の改善

精度が不十分な場合は、以下を検討してください:

  1. より多くの学習データ – より大規模で多様なコーパスを用意する
  2. 閾値を下げる-t 0.0001 を試して、より多くのブースティング反復を許可する
  3. 反復回数を増やす-i 20000 以上を試す。AdaBoost は 1 反復につき弱学習器 (特徴)を 1 つ選択するため、反復回数がモデルの特徴数の上限になります。CLI の デフォルト(-i 100)では非常に小さいモデルになり、held-out 精度が大幅に 低くなります
  4. コーパスの品質向上 – 一貫したトークン化とクリーンなテキストを確保する
  5. 再学習 – 既存のモデルから開始し、追加データで学習する(モデルの再学習を参照)

上記の閾値・反復回数のチューニングは、通常の AdaBoost 学習 (--perceptron/--pos を付けない litsea train)に適用されるものです。 同梱モデル自身が通常の AdaBoost に対して達成している +5〜13pt の held-out 品質向上は、-t/-i のチューニングによるものではなく、2 クラスの Averaged Perceptron を学習してから無損失に AdaBoost の重みへ畳み込む手順に よるものです。段階的な改善ではなく同梱モデルと同水準の品質を目指す場合は、 学習手順のレシピを参照してください。

モデルの再学習

既存のモデルに新しいデータで学習を再開することで、モデルを改善できます。

コマンド

litsea train -t 0.0001 -i 20000 -m <EXISTING_MODEL> <NEW_FEATURES_FILE> <OUTPUT_MODEL>

使用例

# 新しいコーパスから特徴量を抽出
litsea extract -l japanese ./new_corpus.txt ./new_features.txt

# 既存モデルから再学習
litsea train -t 0.0001 -i 20000 \
    -m ./models/my_model.model \
    ./new_features.txt \
    ./models/my_model_v2.model

仕組み

flowchart LR
    A["Existing model<br/>(weights)"] --> C["Trainer"]
    B["New features"] --> C
    C --> D["Retrained model<br/>(updated weights)"]
  1. Trainer が新しい特徴量ファイルから特徴量とインスタンスを初期化する
  2. -m オプションで既存のモデルの重みを読み込む
  3. 読み込まれた重みを出発点として学習を継続する
  4. 新しいモデルは、学習済みのパターンをすべて引き継ぎつつ、新しいデータで改良される

ユースケース

  • ドメイン適応 – 汎用モデルをドメイン固有のテキスト(医療、法律など)でファインチューニングする
  • 段階的な改善 – ゼロから再学習せずに、より多くの学習データを追加する
  • エラー修正 – 現在のモデルが誤りを犯す例を使って学習する

注意事項

  • 出力モデルのパスは入力モデルと同じパスを指定できます(上書き)
  • -m フラグはファイルパス、file://http://https:// URI に対応しています
  • 再学習は既存の重みから開始するため、必要な反復回数が少なくなる場合があります
  • -m は通常の AdaBoost 学習でのみ利用できます。train --pos-m/--load-model-uri対応していません – 二段構成モデルの増分学習は サポートされていないため、更新したい場合は train --pos で ゼロから再学習する必要があります
  • 同梱の japanese.modelchinese.modelkorean.modelenglish.model はこの -m を使うレシピでは作られていません – これらは 学習手順で説明している perceptron 畳み込み手順で学習されています。これらの上に -m で さらに増分 AdaBoost 学習を行うと、2 つの手法が混ざってしまいます。 同梱モデルを更新したい場合は、その手順でゼロから再学習してください

モデルファイル形式

Litsea のモデルは、シンプルなプレーンテキストファイルとして保存されます。

形式の仕様

<feature_name>\t<weight>
<feature_name>\t<weight>
...
<bias>
  • 最終行を除く各行は、タブ文字で区切られた特徴量名重みを含む
  • 重みがゼロの特徴量は、ファイルをコンパクトに保つために省略される
  • 最終行はバイアス項を単一の数値として含む

BC1:IK	0.3456
BC2:KI	-0.1234
UW4:は	0.5678
UC4:I	0.2345
...
-0.0891

バイアスの復元

モデルの読み込み時に、バイアスは以下の式で復元されます:

bias_bucket_weight = -bias_value * 2 - sum(feature_weights_before_the_bias_line)

save_model で書き出されたファイルは常にバイアス行を最終行に置くため、この値は すべての特徴量の重みの合計と等しくなります。レガシーモデル(例: RWCP.model)は バイアス行をファイルの途中に置いており、それ以降の重み行も受け入れられます。バイアス バケットはバイアス行より前の重みから計算され、従来のローダーの挙動と一致します。

バリデーション

ローダーは、不正な形式のファイルを黙って読み込まず、明示的なエラーとして拒否します:

  • 空のファイル
  • バイアス行を持たないファイル(ダウンロードの途中切断やコピーの中断で 典型的に発生する症状)
  • バイアス行が 2 行以上あるファイル
  • 重複した特徴量行
  • **非有限(non-finite)**な重みまたはバイアス値(NaNinf-inf)。これらは 放置するとすべてのスコア比較を汚染してしまいます

Averaged Perceptron モデルのローダーも同様に、クラス数ヘッダーを検証し、非有限な 重みを拒否します。

予測時:

bias = -sum(all_model_weights) / 2.0    (cached; read once per sentence)
score = bias + sum(model[feature] for feature in input_attributes)

ディスク上の形式は文字列キーのまま変わっていませんが、セグメンタは文字列に対して 直接スコアリングしません: ロード時に各特徴量行がパースされ、ホットループ用の packed u64 整数キーへコンパイルされます (予測パイプライン を参照)。セグメンタの言語では生成し得ない特徴量(例: 他言語の文字種コード)は このコンパイルで無視されます – 従来も入力属性にマッチし得なかったものと完全に 同じ扱いです。一方、バイアスは常にファイル内のすべての重みから計算されます。

二段構成モデル形式(litsea-two-stage v1

二段構成モデルは、stage-1 の境界分類器、候補タグ語彙表(lexicon)、stage-2 の 単語単位タガーを、マジック行とセクションマーカーを持つ 1 つのプレーンテキスト ファイルにまとめたものです。セクションの順序は固定です:

litsea-two-stage v1
[params]
dominance\t0.99
[stage1]
<AdaBoost モデル形式: "feature\tweight" 行 + バイアス行 1 行>
[lexicon]
<surface>\t<TAG>:<count>[,<TAG>:<count>...]
[stage2]
<Averaged Perceptron モデル形式: クラス数、クラス名、重み>
  • [stage1][stage2] セクションには、上で説明した既存の形式がそのまま 埋め込まれ、既存のローダーで解析されます。
  • [lexicon] の各行は、単語表層を学習コーパスで観測された UPOS タグと出現数に 対応付けます。出現数の多い順(同数はタグ名の昇順)に並びます。表層はタブと 改行を除く任意の文字を含むことができ、trim されないため、空白トークンも 表現できます。
  • [params] セクションは省略可能です。唯一のキー dominance は分類器スキップの 閾値で、範囲は (0.5, 1.0] です: 既知の表層のうち最頻タグが学習時の出現の この割合以上を占めるものは、stage-2 分類器を呼ばずにタグ付けされます。 セクションが無い場合の既定値は 0.99 です。
  • stage-2 のクラス名は有効な UPOS タグでなければなりません。すべての重み行と lexicon 行がタブを含むことと合わせて、内容行がセクションマーカーと衝突しない ことが保証されます。

この形式は純粋に追加的です: マジック行は AdaBoost の weight/bias 行としても Perceptron のクラス数としても解釈できないため、既存のローダーは二段構成ファイルを 明示的なエラーで拒否し、既存のモデルファイルはこれまでどおり読み込めます。 将来の形式改訂では別のマジック行(例: litsea-two-stage v2)を使用し、v1 の ローダーはそれを未対応バージョンとして拒否します。ローダーはセクションの順序、 上記の lexicon 規則、パラメータの範囲を検証し、エラーにはセクション名を付けて 報告します(例: [stage2] section: ...)。

ファイルサイズ

モデルファイルのサイズは、モデルの種類と言語によって大きく異なります:

モデルサイズ特徴量
japanese.model約 1.1 MBUD Japanese-GSD
chinese.model約 2.0 MBUD Chinese-GSD
korean.model約 86 KBUD Korean-GSD
english.model約 125 KBUD English-EWT
RWCP.model約 22 KBオリジナルの TinySegmenter
JEITA_Genpaku_ChaSen_IPAdic.model約 16 KBJEITA コーパス
japanese_pos.model約 5.4 MBUD Japanese-GSD(二段構成)
chinese_pos.model約 8.0 MBUD Chinese-GSD(二段構成)
korean_pos.model約 5.0 MBUD Korean-GSD(二段構成)
english_pos.model約 3.6 MBUD English-EWT(二段構成)

RWCP.modelJEITA_Genpaku_ChaSen_IPAdic.model は本当にキロバイト級で、アプリケーションへの直接埋め込みや最小限のオーバーヘッドでの HTTP 配信に最も適しています。再学習された japanese.modelchinese.modelkorean.modelenglish.model事前学習済みモデルを参照)は、その一部のコンパクトさを引き換えに大幅な品質向上を得ています: キロバイト級ではなく約 86 KB〜2.0 MB になりましたが、それでも二段構成モデル(*_pos.model)の数メガバイトに比べれば小さく、後者はクラスごと・ステージごとの重みを保持するためサイズが大きくなります。

互換性

  • モデルファイルはエンコーディング非依存です(特徴量名はそのまま保存されます)
  • 形式は、通常の学習ワークフローにおいては決定的です: save_model は学習器の特徴量順で特徴量を書き出します。特徴量ファイルから初期化された学習器やディスクから読み込まれた学習器では、この順序は(BTreeMap により)ソートされています。一方、add_instance() のみで構築された学習器は挿入順で書き出します
  • モデルは前方互換性があります。入力に含まれるがモデルにない新しい特徴量は、予測時に単純に無視されます

リモートモデルの読み込み

Litsea は、ローカルファイルに加えて HTTP/HTTPS URL からのモデル読み込みに対応しています。

対応する URI スキーム

スキーム説明
(なし)./model.modelローカルファイルパス(デフォルト)
file://file:///path/to/model明示的な File URI
http://http://example.com/modelHTTP URL
https://https://example.com/modelHTTPS URL

CLI での使用

echo "テスト" | litsea segment -l japanese https://example.com/japanese.model

ライブラリでの使用

#![allow(unused)]
fn main() {
let mut learner = AdaBoost::new(0.01, 100);

// Local file
learner.load_model_from_path(Path::new("./models/japanese.model"))?; // local, synchronous

// HTTP URL
learner.load_model("https://example.com/models/japanese.model").await?;
}

フィーチャーの有効化

0.6.0 以降、remote_model フィーチャーはオプトインです(ライブラリのデフォルトはローカル読み込みのみとし、依存関係のツリーをコンパクトに保っています)。CLI ではこのフィーチャーが有効化されているため、litsea segment https://... はそのまま動作します。ライブラリ利用者は以下が必要です:

litsea = { version = "0.13.0", features = ["remote_model"] }

実装の詳細

  • HTTP クライアント: reqwest + rustls(OpenSSL 依存なし)
  • カスタム User-Agent: Litsea/<version>
  • load_model メソッドが**非同期(async)**なのは、HTTP 読み込みに非同期ランタイムが必要なため
  • CLI では tokio が非同期ランタイムを提供

制限とエラー処理

  • 接続タイムアウト: 10 秒、リクエスト全体のタイムアウト: 60 秒 – 応答が止まったサーバーによってモデル読み込みが無期限にブロックされることはありません
  • モデルサイズの上限: 256 MiB。Content-Length がこれより大きい場合は本文を 読み込む前に拒否され、実際の本文サイズが上限を超えた場合も拒否されます
  • 不完全なダウンロード: サーバーが Content-Length を送信している場合、 受信した本文がそれより短ければ不完全なダウンロードとして報告されます
  • 2xx 以外のレスポンスは、HTTP ステータスとともにダウンロードエラーとして報告されます
  • モデルパーサーはさらに、末尾が欠けたファイルも拒否します(バイアス行を持たない モデルは読み込みに失敗します。モデルファイル形式 を参照)

WASM に関する注意事項

wasm32 ターゲットは、remote_model フィーチャーを無効にした状態でのみ CI で チェックされています (cargo check -p litsea --target wasm32-unknown-unknown --no-default-features):

  • wasm32 では HTTP/HTTPS の読み込みは現在サポートされていません – HTTP クライアントの構成が reqwest の ClientBuilder::connect_timeouttimeout を使用しており、reqwest の WASM クライアントがこれらを提供していない ため、remote_model フィーチャーはこのターゲットではビルドできません
  • ローカルファイルパスと file:// スキームも非対応です – ファイルシステムへのアクセスが利用できないため、read_file_bytesUnsupported エラーを返します

したがって wasm32 では、ホスト環境から供給されたモデルのバイト列を load_model_from_reader に渡すなど、別の手段でモデルを与える必要があります。

ベンチマーク

Litsea には、パフォーマンス測定のための Criterion ベンチマークスイートが含まれています。

ベンチマークの実行

cargo bench --bench bench

または Makefile を使用:

make bench

ベンチマークスイート

ベンチマークは litsea/benches/bench.rs で定義されています:

ベンチマーク説明
segment_short/adaboost/{japanese,chinese,korean,english}短い文の分割(AdaBoost)
segment_short/averaged_perceptron/{japanese,chinese,korean,english}短い文の分割+品詞付与
segment_long_japanese/{adaboost,averaged_perceptron}坊っちゃん全文の処理(約 300 KB)
external_corpus/*tokenizer-speed-bench と同一のコーパススループット計測(後述)
char_type_hiragana文字種分類
add_corpus学習用コーパスの取り込み
predict_adaboost単一の AdaBoost 予測

モデルは load_model_from_path で同期的に読み込まれます。ベンチマークに非同期ランタイムは関与しません。

コーパススループット(external_corpus

external_corpus グループは、外部の tokenizer-speed-bench にある litsea の 7 ベンチをリポジトリ内で再現します(下記の english/english-two-stage ケースはこの外部ハーネスにはまだ対応するものがありません)。これにより、 スループットの回帰を cargo bench だけで検出できます:

cargo bench --bench bench -- external_corpus
ベンチ IDモデルコーパス
japanesejapanese.modelwagahaiwa_nekodearu.txt
japanese-rwcpRWCP.modelwagahaiwa_nekodearu.txt
japanese-two-stagejapanese_pos.modelwagahaiwa_nekodearu.txt
koreankorean.modelmujeong.txt
korean-two-stagekorean_pos.modelmujeong.txt
chinesechinese.modelrulin_waishi.txt
chinese-two-stagechinese_pos.modelrulin_waishi.txt
englishenglish.modelpride_and_prejudice.txt
english-two-stageenglish_pos.modelpride_and_prejudice.txt

*-two-stage ベンチは二段構成アーキテクチャ (#147/#169)と合わせて追加したもので、上記の元々の tokenizer-speed-bench を再現する 7 ベンチには含まれません。

1 イテレーションでコーパス全行を分割し(外部ベンチと同様、行のフィルタなし)、 グループの Throughput::Elements にコーパスの改行を除く文字数を設定しているため、 Criterion の elem/s 表示がそのまま chars/sec として読めます。

コーパスは resources/ に外部ベンチとバイト同一で同梱しています:

コーパスサイズ出典
wagahaiwa_nekodearu.txt約 1.1 MB吾輩は猫である(夏目漱石)、青空文庫、パブリックドメイン
mujeong.txt約 786 KB무정(李光洙、1917)、ko.wikisource、パブリックドメイン — 分かち書きされた現代表記の韓国語で、空白対応 korean.model の想定入力
rulin_waishi.txt約 985 KB儒林外史(呉敬梓)、zh.wikisource、パブリックドメイン — UD Chinese-GSD と同じ繁体字
pride_and_prejudice.txt約 688 KBPride and Prejudice(Jane Austen)、Project Gutenberg eBook #1342、パブリックドメイン — 自然な分かち書きの英語で、空白対応 english.model の想定入力

数値は公表されている tokenizer-speed-bench の値と比較可能ですが、方法論の違いに より完全には一致しません: Criterion はプロセス内のウォームアップ + サンプリング (外部ベンチは 101 回のプロセスインターリーブ実行)であり、cargo bench は litsea の チューニング済み release プロファイル(thin LTO、codegen-units=1)を継承します (外部ベンチのクレートはデフォルトの release プロファイル)。

API 比較(segment_into

segment_into グループは、所有出力の segment() API とバッファ再利用の segment_into() API(issue #184)を、external_corpus と同じ 4 つの 分割コーパス(同じ行単位ワークロード、Throughput::Elements による chars/sec 表示)でペア比較します:

ベンチ IDAPI
japanese-strings / korean-strings / chinese-strings / english-stringssegment()(トークンごとに String 1 つ、呼び出しごとに新規スクラッチ)
japanese-ranges / korean-ranges / chinese-ranges / english-ranges1 つの SegmentBuffer を再利用する segment_into()

同一 run 内で同じ言語の 2 つの ID を比較してください: その差が、バッファ 再利用 API が取り除く呼び出しごとのアロケーションコストです。採点処理は 同一です(segment()segment_into() のラッパー)。

cargo bench -- segment_into

エンジンの数値と CLI の数値

本章の計測はすべてシングルスレッドのエンジンスループットです。 CLI の segment --threads N(issue #185)はこれに加えてプロセスレベルで バッチの実時間をコア数にスケールさせますが、この 2 種類の数値は比較 できません — --threads 8 の実時間はエンジンの高速化ではなく、エンジンの chars/sec は CLI のスレッドスケーリングについて何も語りません。CLI レベルの スケーリングを報告する際は、スレッド数を明記し、後述のペア計測の規律で 測定してください。

実行間のばらつき

本ドキュメントに掲載している数値(二段構成タグ付け事前学習済みモデルページのスループット数値を含む)は、 専用のアイドルなベンチマーク用ハードウェアではなく、本プロジェクトの開発機で 計測しています。同一ビルドで external_corpus を 3 回連続実行したところ、 個々のベンチ ID で 10〜20% の振れ幅が見られました – 1 回の実行を精密な数値と 読むには大きすぎる幅です。ページ内で範囲や「N 回計測」の注記がある場合は この振れ幅をそのまま反映したものであり、単一の数値のみが示されている場合も 概ね同程度の誤差があるものとして扱ってください。(別の実行で計測した過去の 公表値と比較するのではなく)同一実行内で2 つのモデルを比較すると、 両方が同じマシン状態を経験するため、この振れ幅の大半が相殺されます。

HTML レポート

Criterion は、統計情報と比較グラフを含む詳細な HTML レポートを以下の場所に生成します:

target/criterion/report/index.html

ベンチマーク実行後にこのファイルをブラウザで開くと、以下を確認できます:

  • 信頼区間付きの反復時間
  • スループット測定
  • 前回実行との比較(自動回帰検出)

リリースプロファイル

cargo bench はリリースプロファイルを継承します。このプロファイルでは thin LTO と単一のコード生成ユニット(single codegen unit)が有効になっています(ワークスペースの Cargo.toml を参照)。そのため、ベンチマーク結果はリリースバイナリが実際に使用する最適化済みの構成を反映しています。単なる cargo build(開発プロファイル)は大幅に低速であり、代表的な数値にはなりません。

結果の解釈

パフォーマンスに影響する主な要因:

  • 分割処理は入力長に対して線形(O(n))
  • 文字種分類は文字範囲に対する match で直接行われる(数ナノ秒、セットアップコストなし)
  • 各位置での予測は特徴量の数に依存(38-42個、定数)
  • モデル読み込み時間はモデルファイルサイズに比例

事前学習済みモデル

Litsea は models/ ディレクトリに複数の事前学習済みモデルを同梱しています。

モデルの入手

モデルはリポジトリの models/ ディレクトリにあります。加えて、各リリースでは 8 つの言語モデルを個別のアセットとして添付しています。

https://github.com/mosuka/litsea/releases/download/<tag>/japanese.model
https://github.com/mosuka/litsea/releases/download/<tag>/japanese_pos.model
...

これらの URL はリリースごとに固定で、必要な言語だけを取得できます(8 個すべてで 24MB に対し、1 つあたり 84KB〜8MB)。これにより次の 2 つが可能になります。

  • remote_model feature を有効にすれば、CLI やライブラリが URL を直接受け取れます: litsea segment -l japanese https://github.com/mosuka/litsea/releases/download/<tag>/japanese.model
  • fromUri を持たない WebAssembly バインディングでも、ページ側で fetch してバイト列を Segmenter.fromBytes に渡せます

main ではなくリリースタグを指定しておくと、デプロイ先のモデル重みが固定されます。モデルはリリース間で再学習されることがあり、held-out スコアもそれに伴って変わります。

モデルカタログ

単語分割モデルは、学習コーパスの held-out テスト分割(学習に使用していない文)で 評価しています。単語 F1(Word F1) は単語の完全一致、境界 F1(Boundary F1) は個々の境界判定のスコアです。なお train コマンドが出力するのは学習データ自身で 測った in-sample 指標であり、ここに示す held-out の値より高くなる点に注意して ください。

アルゴリズムについての注記: japanese.modelchinese.modelkorean.modelenglish.model は 2 クラス(境界/非境界)の Averaged Perceptron として学習した後、 スカラーの特徴量重みへ畳み込んでいます(issue #165)。ファイル自体は従来どおり プレーンな AdaBoost テキスト形式のままで、Segmenter::with_learner / AdaBoost::load_model_from_path は無変更で動作します。この畳み込みは 無損失な変換であり(導出は scripts/collapse_binary_perceptron.py の docstring を参照)近似ではありません: この方法で学習した perceptron は、 同じコーパス・同じテンプレートで AdaBoost の presence-stump 弱学習器より 大幅に高い held-out 品質に達します。代わりにモデルファイルは大きくなり (非ゼロ重みを持つ特徴量が増えるため)、学習手順も通常の train ではなく train --perceptron を経由します(詳細は下記の学習手順を参照)。

japanese.model

プロパティ
言語日本語
学習コーパスUD Japanese-GSD
エポック数50
剪定後の特徴量数|重み| 上位 40,000
単語 F1(held-out)96.70%
境界 F1(held-out)98.59%
ファイルサイズ約 1.1 MB

korean.model

プロパティ
言語韓国語
学習コーパスUD Korean-GSD(空白保持 TSV コーパス)
エポック数30
特徴量テンプレートタグなし(pointwise、issue #183)
剪定後の特徴量数剪定なし(3,132 特徴量)
単語 F1(held-out)99.91%
境界 F1(held-out)99.96%
ファイルサイズ約 86 KB

韓国語モデルは、元の語節(어절)間の空白を保持したテキストで学習・評価して います(各空白は独立したトークンとして扱い、空白トークンは F1 の計算から 除外します)。韓国語では空白がほとんどの語境界を示すため、学習時に空白を 参照できるモデルは UD Korean-GSD の基準をほぼ決定的に解決できます – これが 韓国語の特徴量数・ファイルサイズが小さいままである理由でもあります(モデルが 学習すべき曖昧性がほとんど残っていないため)。日本語と 中国語は空白を使わずに表記されるため、プロトコルは従来のままです。

さらに韓国語モデルは 16 個のタグ依存特徴量テンプレートUP*/BP*/UQ*/BQ*/TQ*。直前 1〜3 文字の境界判定結果を参照する)を 使わずに学習しています: 空白シグナルがあるため、これらのテンプレートは 計測上何も寄与していませんでした(タグなし 99.91% vs タグあり 99.90%、 特徴量は約 22% 減)。タグ依存特徴を持たないモデルは pointwise – 各位置の判定が入力テキストのみに依存する – ため、segment() は逐次 スコアリングパスを丸ごとスキップします(issue #183)。他言語での トレードオフは後述のタグなし(pointwise)モデル を参照してください。

english.model

プロパティ
言語英語
学習コーパスUD English-EWT(空白保持 TSV コーパス)
エポック数20
特徴量テンプレートタグなし(pointwise、issue #183)、WC 特徴量なし
剪定後の特徴量数剪定なし(4,794 特徴量)
単語 F1(held-out)98.31%
境界 F1(held-out)99.18%
ファイルサイズ約 125 KB

korean.model と同様に english.model も、元の空白を保持したテキストで 学習・評価しています(各空白は独立したトークンとして扱い、F1 の計算からは 除外します)。空白保持の学習プロトコル、複数語トークン(短縮形)の扱い、 20 エポックとタグなし/WC 特徴量なしの構成を選んだエポックスイープについては English を参照してください。英語には短縮形 (contraction)、ハイフンでつながる複合語、“U.S.” のような略語といった 境界の曖昧性が残っているため、同じ空白保持レシピを共有していても、 held-out の単語 F1 は韓国語のほぼ決定的な 99.91% を下回ります。

chinese.model

プロパティ
言語中国語(簡体字・繁体字)
学習コーパスUD Chinese-GSD
エポック数100
剪定後の特徴量数|重み| 上位 70,000
単語 F1(held-out)90.69%
境界 F1(held-out)95.64%
ファイルサイズ約 2.0 MB

RWCP.model

プロパティ
言語日本語
ソースオリジナルの TinySegmenter から抽出
ライセンスBSD 3-Clause (Taku Kudo)
ファイルサイズ約 22 KB

JEITA_Genpaku_ChaSen_IPAdic.model

プロパティ
言語日本語
学習コーパスJEITA プロジェクト 杉田玄白コーパス
トークナイザChaSen with IPAdic
ファイルサイズ約 16 KB

学習手順

RWCP.modelJEITA_Genpaku_ChaSen_IPAdic.model はレガシー・互換用モデルで、 従来どおりに学習(または取得)しています – 通常の AdaBoost 手順は モデルの学習を参照してください。 japanese.modelchinese.modelkorean.model は binary-perceptron 畳み込み手順 (issue #165)で再学習しています。エンジンの変更は不要ですが、通常の litsea train に加えて数ステップが必要です:

# 1. プレーンな境界特徴量を抽出(従来と同じステップ)。--tag-free を付けると
#    16 個のタグ依存テンプレートを除外して pointwise モデルを学習できる
#    (korean.model で使用。次節を参照)。
litsea extract -l <language> [韓国語なら --format tsv] [--tag-free] <corpus> <features.txt>

# 2. 境界ラベル 1/-1 を B/O にリマップする。これは見た目の問題ではなく
#    正しさのために必須: perceptron 自身のタイブレーク(クラスインデックスが
#    小さい方が勝つ)を、AdaBoost の「score >= 0.0 は境界を優先する」という
#    規約と一致させるためのもの。"1"/"-1" のまま学習すると、タイの解決方向が
#    黙って逆転してしまう。
sed -i 's/^1\t/B\t/; s/^-1\t/O\t/' <features.txt>

# 3. 2 クラスの Averaged Perceptron として学習する。--perceptron は
#    汎用のトレーナー(PerceptronTrainer はラベルを不透明な文字列として
#    扱う)。
litsea train --perceptron --num-epochs <N> <features.txt> <perceptron.model>

# 4. プレーンな AdaBoost モデル形式へ畳み込む(無損失 -- 導出はスクリプトの
#    docstring を参照)。
scripts/collapse_binary_perceptron.py <perceptron.model> <collapsed.model>

# 5. 任意: 特徴量数の増加が `cargo bench -- external_corpus` のスループットを
#    許容範囲を超えて悪化させる場合、上位 N 特徴量に剪定し held-out 品質と
#    速度の両方を再確認する。
scripts/prune_adaboost_model.py <collapsed.model> <pruned.model> <n>

エポック数と剪定閾値は固定値ではなく言語ごとのチューニング項目です – 上記の同梱モデルを選んだのと同じように、エポックスイープと品質・スループットの トレードオフスイープから決めてください(スイープの全データは issue を参照)。 大まかな傾向として、品質は少数のエポックを大きく超えて向上し続け、最終的には プラトーに達するか(日本語は約 50 エポックを超えると軽度のオーバーフィットが 見られます)、単一の「正しい」エポック数があるわけではありません。剪定による 品質劣化は言語固有の崖に達するまで緩やかに進む傾向があるため、数値を推測するの ではなく、cargo bench のスループットが回復し始める付近の剪定レベルを いくつか試してください。

タグなし(pointwise)モデル

境界特徴量テンプレートのうち 16 個(UP*/BP*/UQ*/BQ*/TQ*)は、 モデル自身が直前 1〜3 文字で下した境界判定の結果を参照します。これは 各判定を直前の判定に連鎖させるため、segment() のスコアリングを厳密に 逐次的なパスへ縛り付けます。これらを使わずに学習したモデル (litsea extract --tag-free)は pointwise – 各位置が入力テキスト のみに依存する – になり、segment() はモデルのロード時にこれを検出して 逐次パスを丸ごとスキップします(issue #183)。

タグ特徴量の価値は言語によって大きく異なります(すべて UD GSD テスト 分割上の、収束確認済みエポックスイープによる計測値。issue #183):

言語単語 F1(タグあり)単語 F1(タグなし)スループット変化
韓国語99.90%99.91%高速化(逐次パスをスキップ)
英語98.71%98.68%*高速化(逐次パスをスキップ)
日本語96.70%96.33%実測 end-to-end 約 +45〜50%
中国語90.69%90.18%実測 end-to-end 約 +12%

* 英語の dev split での比較(タグあり 98.71% vs タグなし 98.68%、差は 0.03pt)はどちらを選んでも大差ない水準であり、同梱モデルは韓国語と同じ スループット上の理由からタグなしで出荷しています。上記の english.model の held-out テスト分割の数値(98.31%)はスイープの最後に一度だけ計測した ものであり、この dev split の対比とは直接比較できません。

語節間の空白シグナルがある韓国語ではタグ特徴量は何も寄与しないため、 korean.model はタグなしで同梱しています(サイズも約 22% 減)。英語も 同じ理由(空白シグナルが支配的であるため)でほぼ同様の状況にあります。 日本語・中国語ではまだ単語 F1 で 0.37〜0.51pt の価値があるため、 同梱モデルはタグ特徴量を保持しています – 品質がデフォルトです。 速度を優先するワークロードでは、上記の手順に extract ステップの --tag-free を加えて再学習してください。スループットの数値は本 プロジェクトの開発マシンでベンチマークの ペア計測方法論により測定したものなので、持ち越せるのは絶対値ではなく 比率と考えてください。

二段構成の品詞推定モデル

二段構成アーキテクチャ(issue #147)は、 文字位置ごとに全 UPOS クラスを採点する代わりに、二値の境界分類器で分割し、 確定した各単語を候補タグ語彙表と単語単位のタガーでタグ付けします。

held-out 行は UD GSD/EWT テスト分割に対して litsea evaluate --pos で測定した 単語 / タグ付き単語 F1 です(モデルの評価を 参照)。日本語と中国語は空白を使わずに表記されるため、コーパスと実際の入力は 同一のものです。韓国語と英語はスペース区切りであり、空白保持コーパス (--format tsv、issue #198)で学習・評価しているため、以下の数値も同様に 実運用の数値です。 「stage-2 特徴量セット」は単語単位テンプレートの選択 (fastbalancedfull特徴量の抽出を参照) で、二段構成タグ付けの 実測トレードオフから言語ごとに同梱モデル用に選定しています。スループットは ベンチマークページと同じコーパスに対する cargo bench -- external_corpus によるもので、本プロジェクトの開発機で 計測しています(専用のアイドルハードウェアではありません – そのページの方法論の注記を参照)。

エポック数についての注記: 二段構成の同梱にあたって行ったエポックスイープ (10〜150 エポック)では、stage 1 の分割品質は特に 10 エポックを大きく 超えて向上し続け、50 エポック付近でプラトーに達することが判明しました – 以下の同梱二段構成モデルは、このスイープから得た 50 エポックを使用して います。再学習の際、一発の低エポック実行ではこのアーキテクチャの到達可能な 品質を過小評価することになります (方法論についての注記を参照)。

japanese_pos.model

プロパティ
言語日本語
学習コーパスUD Japanese-GSD(7,050 文)
エポック数50
stage-2 特徴量セットfast
単語 F1(held-out)96.78%
タグ付き単語 F1(held-out)92.95%
スループット4.38M chars/s
ファイルサイズ約 5.4 MB

chinese_pos.model

プロパティ
言語中国語(簡体字・繁体字)
学習コーパスUD Chinese-GSD(3,997 文)
エポック数50
stage-2 特徴量セットbalanced
単語 F1(held-out)90.82%
タグ付き単語 F1(held-out)82.29%
スループット3.38M chars/s
ファイルサイズ約 8.0 MB

korean_pos.model

プロパティ
言語韓国語
学習コーパスUD Korean-GSD(4,400 文、空白保持 TSV プロトコル)
エポック数20
stage-2 特徴量セットfull
単語 F1(held-out)99.88%
タグ付き単語 F1(held-out)93.95%
スループット4.21M chars/s
ファイルサイズ約 4.0 MB

韓国語のスループットのプロファイルは語彙表に起因します: held-out テキストの 34.5% が未知語(学習時に未出現の表層)で、未知語は常に stage 2 の全クラスフォールバックを払うことになり、安価な dominance スキップや候補マスクの経路を使えません。そのため日本語・中国語より 多くの割合の韓国語の単語が stage 2 のフルコストを負担します。

韓国語のプロトコルについての注記: korean_pos.model は空白保持 TSV コーパス(issue #198)で学習しており、これは korean.model が使うのと 同じプロトコルです。そのため単語 F1(99.88%)は korean.model の 99.91% と直接比較でき、二段構成の stage 1 分類器はいまや専用の分割モデルと ほぼ同等に達しています。issue #198 までは空白非保持の word/POS コーパスで学習しており、実際のスペース付き入力に対するスコアは 94.01% でした。学習プロトコルの切り替えにより 単語 F1 +5.9 ポイント、 タグ付き単語 F1 +10.8 ポイント向上しています。再学習に伴い、stage-2 特徴量セットは balanced から full へ、エポック数は 50 から 20 へ 変更しました。いずれも新しいコーパス上の dev split スイープから 選び直したものです。

english_pos.model

プロパティ
言語英語
学習コーパスUD English-EWT(12,544 文、空白保持 TSV プロトコル)
エポック数50
stage-2 特徴量セットfull
単語 F1(held-out)98.30%
タグ付き単語 F1(held-out)90.55%
スループット7.32M chars/s
ファイルサイズ約 3.1 MB

英語のプロトコルについての注記: english_pos.model は空白保持 TSV コーパス(issue #198)で学習しているため、単語 F1(98.30%)は english.model の 98.31% と直接比較でき、二段構成の stage 1 分類器は いまや専用の分割モデルとほぼ同等に達しています。

これはモデルカタログの中で最大の品質変化です。issue #198 までは、 二段構成パイプラインはコーパスを空白なしで連結したテキストで学習して おり、英語のテキストが実際に含んでいる空白を捨てていました。その結果、 モデルのスコアは空白非保持プロトコルで 70.33%、実際のスペース付き 入力で 77.55% でした。入力が本来持っている空白どおりに学習すること で、単語 F1 +20.8 ポイント、タグ付き単語 F1 +20.7 ポイント向上し、 推論も約 3.6 倍高速になりました(2.05M -> 7.32M chars/s): 空白が候補 1 個の語彙表エントリを持つようになったため、トークンの約 43% が stage 2 の分類器ではなく packed モデルの固定タグ経路でタグ付けされます。

この変更は 2 つの異なる学習と推論のミスマッチを同時に解消しました。 stage 1 は、英語のほぼすべての語境界を示す空白文字を一度も見て いませんでした。stage 2 の文脈特徴量(L*/R*/cl*/cr*)も影響を 受けており、推論時に単語の隣は通常空白であるのに対し、空白非保持の 学習では次の単語の文字になっていました。どちらも現在は segment --pos が実際に計算するものと一致しています。

使用方法

echo "これはテストです。" | litsea segment --pos -l japanese models/japanese_pos.model

出力:

これ/PRON は/ADP テスト/NOUN です/AUX 。/PUNCT

モデルの選択

  • 日本語には、最高精度を求める場合は japanese.model を、オリジナルの TinySegmenter との互換性を重視する場合は RWCP.model を使用
  • 中国語には chinese.model を使用
  • 韓国語には korean.model を使用
  • 英語には english.model を使用
  • 品詞推定には二段構成モデル(japanese_pos.modelchinese_pos.modelkorean_pos.modelenglish_pos.model)を segment --pos / evaluate --pos とともに使用してください (アーキテクチャと実測値は 二段構成タグ付けを参照。英語については 特に、english_pos.model の分割品質に頼る前に上記のプロトコルについての 注記を必ず読んでください)。
  • ドメイン固有の用途には、独自モデルの学習または既存モデルの再学習を検討

サンプルデータ

resources/ ディレクトリには以下も含まれています:

  • bocchan.txt – 坊っちゃん(夏目漱石)、約 307 KB。segment_long_japanese ベンチマークと差分テストに使用。
  • wagahaiwa_nekodearu.txt – 吾輩は猫である(夏目漱石)、約 1.1 MB、青空文庫。
  • mujeong.txt – 무정(李光洙、1917)、約 786 KB、ko.wikisource。
  • rulin_waishi.txt – 儒林外史(呉敬梓)、約 985 KB、zh.wikisource。
  • pride_and_prejudice.txt – Pride and Prejudice(Jane Austen)、約 688 KB、Project Gutenberg eBook #1342(ヘッダー・フッター・挿絵キャプションを除去し、1 行 1 段落に整形)。

wagahaiwa_nekodearu.txtmujeong.txtrulin_waishi.txt の 3 つは外部の tokenizer-speed-bench のコーパスとバイト同一で、external_corpus ベンチマークグループが使用します (ベンチマークを参照)。pride_and_prejudice.txt は同じベンチマークグループの英語のケースに使われますが、この外部ハーネス側 にはまだ対応するコーパスがありません。いずれもパブリックドメインです。

ライセンス

Litsea はデュアルライセンスで配布されています。

MIT License

Litsea のメインコードベースは MIT License の下でライセンスされています:

MIT License

Copyright (c) 2025 Minoru OSUKA
Copyright (c) 2022 ICHINOSE Shogo

BSD 3-Clause License

Taku Kudo 氏が開発したオリジナルのコード(TinySegmenter)は BSD 3-Clause License の下でライセンスされています:

Copyright (c) 2008, Taku Kudo
All rights reserved.

ライセンス全文

完全なライセンス文はリポジトリ内の LICENSE ファイルで確認できます。