PHP
litsea-php は ext-php-rs を用いて Litsea を PHP 8.1 以降へ公開するバインディングです。Packagist に mosuka/litsea として公開され、PIE でインストールします。拡張は litsea という名前で登録されます。
インストール
PHP 拡張は特定の PHP ABI 向けにビルドされた共有オブジェクトです。PyPI や npm と異なりビルド済みパッケージの配布はなく、PIE か自分の手で、使っている PHP に合わせてビルドします。
pie install mosuka/litsea
PIE 1.4 以降は、PHP 8.1〜8.5(NTS / ZTS)× Linux x86_64 / arm64(glibc 2.35 以上)または Apple シリコンの macOS 向けのビルド済み litsea.so を GitHub Release からダウンロードし、拡張ディレクトリへ配置して有効化します。Rust ツールチェーンは不要です(--with-php-config=... で別の PHP を選べます)。それ以外の組み合わせ(Alpine などの musl、古い glibc、Intel Mac、PIE 1.4 未満)ではソースビルドにフォールバックし、Rust ツールチェーン、libclang、PIE のビルドツール(autoconf、libtool、make)が必要になります。Windows は未対応です。Composer 自体は php-ext 型のパッケージを無視するため、composer require mosuka/litsea ではインストールできません。
PIE を使わない場合は自分でビルドして読み込みます。
cargo build --release -p litsea-php
php -d extension=/path/to/target/release/liblitsea_php.so your-script.php
常時読み込む場合は php.ini に extension=/path/to/liblitsea_php.so を追記します。どちらの方法でも php -m には litsea と表示されます。
静的解析向けのスタブ
litsea-php/stubs/litsea.stubs.php は、すべてのクラスと関数を IDE・PHPStan(stubFiles)・Psalm(stubs)向けに宣言したファイルです。ビルド済みの拡張から生成し、テストスイートが検証するため、Rust のソースと食い違うことはありません。解析ツールからこのファイルのコピーを指してください。実行時に読み込んではいけません(拡張がすでにクラスを宣言しています)。
モデルの入手
拡張にモデルは含まれません。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]
start と end はバイトオフセットで、PHP の文字列はバイト列です。そのため substr($text, $token->start, $token->end - $token->start) がそのまま表層形を返します(JavaScript のようなエンコーディングを意識した切り出しは不要です)。
ユーザー辞書
すべてのコンストラクタは、省略可能な末尾の引数を 2 つ受け取ります(#263)。$user_words は、現れた場所で必ず 1 トークンとして出力する語の配列、$user_words_gate は、モデルの margin が十分小さい場所にだけ語を適用する flip ratio(0.0〜1.0)です。どちらも既定値は null で、名前付き引数でも渡せます。
$seg = Segmenter::open('japanese', 'models/japanese.model', ['雪だるま', '新幹線']);
$seg->segment('雪だるまを作る');
// ['雪だるま', 'を', '作る'] (辞書なしでは ['雪', 'だる', 'ま', 'を', '作る'])
Segmenter::open('japanese', 'models/japanese.model', user_words: ['雪だるま'], user_words_gate: 0.5);
登録語には両端に境界が置かれ、内側には置かれません。重なった出現箇所は leftmost-longest で解決し、1 文字の語は無視します。POS モデルでは、登録語も他の語と同じようにタグ付けされます(japanese_pos.model では 雪だるま/NOUN)。照合は文脈を見ないため、短い断片を語として登録すると、それを含む他の語をすべて壊します。gate を 0.0 にすると、モデルがすでに同意している場所だけに語を適用します。0.0〜1.0 の外の gate は Litsea\InvalidArgumentException を投げます。意味論の全体は Segmenter を参照してください。
API
| 呼び出し | 戻り値 |
|---|---|
Segmenter::open($language, $path, $user_words?, $user_words_gate?) | セグメンタ |
Segmenter::fromBytes($language, $contents, $user_words?, $user_words_gate?) | セグメンタ |
Segmenter::fromUri($language, $uri, $user_words?, $user_words_gate?) | セグメンタ(ダウンロードはブロッキング) |
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、使用済みトレーナ、0.0〜1.0 の外の $user_words_gate |
Litsea\ModelException | ダウンロード失敗、または旧 joint POS モデル |
Litsea\IoException | ファイルの読み書き失敗 |
Litsea\ParseException | モデルまたは学習データの形式不正 |
Litsea\UnsupportedException | このビルドでは利用できないスキームや操作 |
Litsea\PosUnavailableException | 分割専用モデルに対する POS タグ付けの要求 |
開発
make test-litsea-php # cargo test + 拡張のビルド + PHPUnit
make test-litsea-php-pie # PIE が実行する phpize / configure / make の経路
make stubs-litsea-php # litsea-php/stubs/litsea.stubs.php を再生成
make lint-litsea-php # clippy
make build-litsea-php # リリースビルド
パリティテストは litsea CLI をビルドし、その出力とバインディングの出力を突き合わせます。
composer.json はリポジトリのルートにあります。Packagist がルートのマニフェストしか読まないためで、その php-ext.build-path が PIE を litsea-php/ へ戻し、config.m4 と Makefile.frag が PIE の phpize / configure / make の流れを cargo build --release -p litsea-php に変換します。