WebAssembly
litsea-wasm は wasm-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 }, ... ]
start と end は UTF-8 でのバイトオフセットです。JavaScript の文字列インデックスは UTF-16 コードユニットなので、切り出しには TextEncoder / TextDecoder を使ってください。
const bytes = new TextEncoder().encode(text)
new TextDecoder().decode(bytes.subarray(token.start, token.end)) // === token.surface
ユーザー辞書
fromBytes は、Node.js バインディングと同じ名前の、省略可能な末尾の引数を 2 つ受け取ります(#263)。userWords は、現れた場所で必ず 1 トークンとして出力する語の配列、userWordsGate は、モデルの margin が十分小さい場所にだけ語を適用する flip ratio(0〜1)です。
const seg = Segmenter.fromBytes('japanese', bytes, ['雪だるま', '新幹線'])
seg.segment('雪だるまを作る')
// [ '雪だるま', 'を', '作る' ] (辞書なしでは [ '雪', 'だる', 'ま', 'を', '作る' ])
登録語には両端に境界が置かれ、内側には置かれません。重なった出現箇所は leftmost-longest で解決し、1 文字の語は無視します。照合は文脈を見ないため、短い断片を語として登録すると、それを含む他の語をすべて壊します。userWordsGate を 0 にすると、モデルがすでに同意している場所だけに語を適用します。意味論の全体は Segmenter を参照してください。
ホストの制約で提供しない機能
5 つのバインディングの中で最も制約が強く、いずれも推測ではなく実測に基づいて判断しています。
| 無い機能 | 理由 |
|---|---|
fromUri | cargo check --target wasm32-unknown-unknown --features remote_model が失敗する。reqwest の wasm バックエンドには connect_timeout が無く、litsea::model_io はそれを設定しているため。代わりにページ側で fetch する(キャッシュ・CORS・進捗の制御もページ側に残る) |
| 学習 | 技術的な制約ではなく方針判断。下記を参照 |
CancelToken | 学習が無い以上、キャンセル対象が存在しない |
学習を提供しない理由
#218 で litsea にファイルシステム非依存の extract/train API が入り、wasm32 でもコンパイルできるため、このバインディングでも学習を公開すること自体は可能です。それでも提供しないのは次の 2 点によります(#221)。
- ブラウザは学習を行う場所として適さない。 タブがコーパス・そこから抽出した特徴量(コーパスよりかなり大きい)・モデルを同時に保持することになります。API の形を決めるにはまずそのメモリ実測が必要で、実用的なコーパス規模では成立しないという結論も十分あり得ます。
- 参照実装も持たない。
lindera-python・lindera-nodejs・lindera-php・lindera-rubyはいずれもトレーナを備えますが、lindera-wasmは備えていません。litsea のバインディング群もこの形に揃えています。
学習は CLI かネイティブバインディングで行い、生成したモデルをここで読み込んでください。
メモリ
Segmenter はコンパイル済みモデル(POS モデルなら数 MB)を保持し、WebAssembly のオブジェクトは GC されません。不要になったら free() を呼んでください。
モデルのキャッシュ
モデルは 86KB〜8.3MB あり、訪問者ごとにネットワークを通ります。これはネイティブ版には無い、このバインディング固有のコストです。補助モジュールを同梱しています。
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 | 未知の言語名、0〜1 の外の userWordsGate |
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 制約下にあります(テスト自体は実行済みです)。