MorphFoundryを自前で形態素解析ライブラリに育てたらかなり大きな記録になってしまった(midori227_1)
「ブラウザで自作の形態素解析エンジンを動かしたい」。その欲張りな実験をまとめたのがこの記録です。設計・実装・辞書変換・Worker連携・ベンチマークまでを一気通貫で残しました。
TL;DR(要約)
- 公開している成果物は3点。
morphfoundry.js(最小構成のViterbi風解析コア、Viterbiアルゴリズム:最適経路を探索するアルゴリズム)、unidic-converter.js(UniDic CSV→JSON変換ツール)、demo9-realdata-csv-worker.js(301件のCSVをWorker(Web Worker、バックグラウンド処理)で解析するサンプル)。
- MorphFoundryはブラウザ/Node(Node.js、サーバー側のJavaScript実行環境)/Worker(Web Worker、バックグラウンド処理)共通で動き、UniDic(形態素解析用の辞書)の軽量辞書とフル辞書を切り替えながら未知語ペナルティやプリプロセッサをチューニングできる。
- 記事後半には、辞書生成・Workerハンドシェイク・ベンチマークなどのデモをまとめて掲載。迷ったらリンクを踏めばその場で確認できる。
- 読了目安は15〜20分。急ぎの場合は「クイックスタート」→「UniDicをMorphFoundry形式に変換する」→「実データ(301件のCSV)を丸ごと解析する」の3章を追えば、最低限の動作確認まで到達できる。
読む前に
- 対象読者: 既存ライブラリをカスタマイズしたいエンジニア / Viterbi風アルゴリズム(最適経路を探索するアルゴリズム)を手元で分解したい人 / Web教材として形態素解析を配布したいクリエイター
- この記事で分かること: MorphFoundryの構成・UniDic(形態素解析用の辞書)辞書のJSON(JavaScript Object Notation、データ交換形式)化手順・Worker(Web Worker、バックグラウンド処理)/CLI(コマンドラインインターフェース)連携方法・速度チューニングのポイント
- あると安心な前提知識: 形態素解析の流れ / JavaScriptによる非同期処理 / Web Worker(バックグラウンド処理)の基本
記事の歩き方
- 3分で動かしたい → 「クイックスタート(3ステップ)」へ
- MorphFoundryの全体像を把握したい → 「MorphFoundryを構成する3ピース」「開発ロードマップ」
- 仕組みを深掘りしたい → 「形態素解析エンジンをどう設計したか」「Viterbiラティスを丸裸にする」
- 実データで試したい → 「UniDicをMorphFoundry形式に変換する」「実データ(301件のCSV)を丸ごと解析する」
- 運用と拡張のポイント → 「ユーザー辞書で専門語をサポート」「WorkerとCLIのハンドシェイク」「ベンチマークで実力を測る」
クイックスタート(3ステップで動作確認)
1. 辞書を生成する(軽量約26万件)
node articles/midori227_1/unidic-converter.js ./articles/midori227_1/lex.csv ./articles/midori227_1/dictionaries/unidic-lite.json 263040
2. 解析器を初期化して実行する(ブラウザ例)
<script type="module">
import { createMorphFoundry } from './articles/midori227_1/morphfoundry.js';
const analyzer = await createMorphFoundry({
defaultDictionaries: ['./articles/midori227_1/dictionaries/unidic-lite.json']
});
console.log(analyzer.analyze('形態素解析を試す'));
// Node/CLI 例は本文の「README/コマンド例」を参照
</script>
3. CSV実データを解析する(demo9)
- articles/midori227_1/demo9-realdata-csv-analyzer.html をブラウザで開く
- 自動で demo9-realdata-csv-worker.js が起動し、301件の説明文を解析・進捗表示・上位名詞を集計
ダウンロードと資料リンク
配布物と補助資料はここからまとめて取得できます。
---
MorphFoundryを構成する3ピース
> ここでわかること: プロジェクトを支える3つのモジュールと、デモやデータの役割。
- Core (
morphfoundry.js): 辞書ロード → ラティス構築 → バックトラックを行う最小構成の解析エンジン。MorphFoundry作成関数で非同期初期化に対応。
- Dictionary Tooling (
unidic-converter.js & dictionaries/): UniDic(形態素解析用の辞書)CSVをJSON(JavaScript Object Notation、データ交換形式)へ変換するCLI(コマンドラインインターフェース)と、軽量版/フル版の辞書サンプルを同梱。
- Playground(demo1〜demo9)と実験データ: 解析アルゴリズムの可視化、Worker(Web Worker、バックグラウンド処理)ハンドシェイク、CSV(Comma-Separated Values、カンマ区切り値)解析など開発時に使った検証UI(ユーザーインターフェース)と、説明文301件入りの
data.csv。
まずは雰囲気を掴むために、基本トークナイザーのデモを触ってみてください。
開発ロードマップ(ざっくり5段階)
> ここでわかること: どの順番で要件を固め、実装と検証を進めたか。
1. 要件定義と検証実験(demo1, demo3)
辞書のMap化や未知語処理を小さなデモで検証。
2. アルゴリズムの骨格づくり(morphfoundry.js)
ラティス構築・バックトラック・辞書追加API(アプリケーション・プログラミング・インターフェース)の最小セットを実装。
3. 辞書変換パイプラインの確立(unidic-converter.js + demo4)
UniDic(形態素解析用の辞書)CSVをJSON(JavaScript Object Notation、データ交換形式)化し、ブラウザでも扱えるデータ構造を設計。
4. 最適化と運用シナリオの検証(demo2, demo5, demo6, demo7)
プリプロセッサの効果測定、ユーザー辞書、Worker連携、ベンチマークを順に評価。
5. 実データでの総合テスト(demo9 + data.csv)
301件のCSVで辞書サイズのボトルネックを測定し、軽量版/フル版の使い分け方針にたどり着く。
既存ライブラリを流用する案も検討しましたが、辞書差し替え・CLI対応・未知語処理の柔軟性を確保するため、自作へと舵を切りました。
---
形態素解析エンジンをどう設計したか
> ここでわかること: 候補探索・未知語処理・ラティス構築の方針と、実装時の注意点。
MorphFoundry のコアは、UniDic(形態素解析用の辞書)をMap構造(連想配列)に載せ替えた候補探索と、シンプルなViterbi風ラティス(最適経路を探索するアルゴリズム)です。処理ステージを分割し、最小構成で動かすことを目指しました。
1. 形態素候補の探索戦略
- 辞書の前処理:
Map<表層形, 候補配列> に再編成し、配列走査O(n)をキー探索O(1)へ。候補には原形・読み・品詞・コストを格納して即利用。
- 未知語の扱い: 漢字・仮名・英数字でルールを分岐。未知語ペナルティ(既定9000)は利用シーンに合わせて調整可能。
- Viterbi風ラティス(最適経路を探索するアルゴリズム): 位置ごとに候補ノードを持ち、前ノードとの接続コストを計算。接続コストで滑らかさを制御し、バックトラックで最小経路を復元。
2. 実装上の指針
- プリプロセッサの差し込み:
preprocessors 配列で外部正規化関数を逐次適用。効果はベンチマークで即確認できる。
- デバッグ補助:
enableDebug で最終系列を console.table 表示。コスト分布を追いやすくした。
- API(アプリケーション・プログラミング・インターフェース)設計: MorphFoundry作成関数が辞書ロード済みのインスタンスを返し、解析関数は同期実行。ブラウザ/Node(Node.js、サーバー側のJavaScript実行環境)/Worker(Web Worker、バックグラウンド処理)を問わず共通I/F(インターフェース)。
最初は辞書を配列のまま走査し、1,000語でも応答が重くなりました。Map構造へ切り替えたことで速度が回復。未知語処理も、文字種ごとに推定品詞を変えるルールを入れてようやく落ち着きました。
---
解析パイプラインを段階的に
> ここでわかること: 処理順序を可視化しながら、辞書・プリプロセッサ・未知語コストをどう切り替えるか。
コアが動いた後は、応答性と精度を両立するための処理順序を検証しました。demo2-actual-pipeline-builder.html ではプリプロセッサや未知語コスト、辞書切り替えを同じUIで試せます。
- 文章正規化 → ラティス構築 → バックトラックを1ステップずつ確認
- プリプロセッサを外部関数として差し込み、用途ごとに組み替え可能
- 辞書ロード結果をバッジ表示し、ユニーク語数とエントリ数で状態を把握
パイプラインを構築して解析 を押すと、適用したプリプロセッサと最終ノードコストがまとめて表示されます。
パイプライン構築デモを新しいタブで開く
辞書サイズの壁を越えた経緯
当初は全デモで unidic-full.json(876,802件 / 約152MB)を読み込んでいました。ブラウザがフリーズし、Worker(Web Worker、バックグラウンド処理)に逃がしてもメモリを食い尽くす。demo9 を何度も書き直しても改善せず、計測すると辞書ロードだけで数秒〜十数秒かかっていました。
最終的には lex.csv から約3割(263,040件)を抜き出した unidic-lite.json(約46MB)を用意し、ブラウザ/Worker系のデモは軽量版を既定にしました。フル版はNode(Node.js、サーバー側のJavaScript実行環境)やバッチ解析向けに確保しています。
- フル辞書 (
unidic-full.json): 876,802件 / 約152MB / ブラウザ解析は数十秒に達することも
- 軽量辞書 (
unidic-lite.json): 263,040件 / 約46MB / 数秒〜十数秒で応答
プリプロセッサの掛けすぎもボトルネックでした。1000件×60文字のテキストではMorphFoundry単体で約420ms、プリプロセッサを2つ挟むと640msまで悪化。デモで数字を可視化することで、必要な処理だけに絞れるようになりました。
---
Viterbiラティスを丸裸にする
> ここでわかること: 解析ステップを可視化し、未知語ペナルティの影響を把握する方法。
demo3-code-explain-viterbi.html では、ラティス上の候補とコストをスライダーで追えます。未知語がどこで生まれたか、ペナルティがどの程度効いているかを即確認可能です。
カタカナ語がすべて未知語扱いになりコストが跳ね上がった際は、このデモで原因が一目瞭然でした。ルールを調整し、カタカナとアルファベットは名詞扱いで許容する方針に変更しています。
---
辞書をロードする流れ
> ここでわかること: fetch → JSON parse → 検証 → Map化 → メタ情報登録の正しい段取り。
処理手順を少しでも崩すと例外祭りになるので、時間軸で追えるデモを用意しました。
fetchが失敗した場合はログを出し、ユーザーが辞書URLを差し替えられるようにしています。タイムライン上のカードをクリックすると、該当処理のソース断片と実行ログが表示され、巨大辞書でも安心してトラブルシュートできます。
---
UniDicをMorphFoundry形式に変換する
> ここでわかること: UniDic(CSV版)を取得し、用途に合わせてJSON辞書を作る手順。
1. UniDic 公式ページ で現代書き言葉 UniDic(形態素解析用の辞書、CSV版)をダウンロード。最新は unidic-2024.4.17.zip。
2. 解凍すると lex.csv, matrix.def, feature.def, license/ などが展開される。lex.csv は200MB超なのでストレージ余裕に注意。
3. unidic-converter.js を使って JSON(JavaScript Object Notation、データ交換形式)を生成。第3引数で取り込み件数を調整できます。
# 軽量辞書(約26万件)
node unidic-converter.js ./lex.csv ./dictionaries/unidic-lite.json 263040
# 全件辞書(約87万件)
node unidic-converter.js ./lex.csv ./dictionaries/unidic-full.json
- input: UniDic(形態素解析用の辞書)の
lex.csv
- output:
unidic-lite.json(軽量版)または unidic-full.json(全件版)。どちらも label / version / entryCount を含むメタ情報付き。
- オプション: 第3引数で取り込み件数を任意に設定。ブラウザは軽量版、バッチ解析は全件版と使い分ける。
生成したJSON(JavaScript Object Notation、データ交換形式)は defaultDictionaries にそのまま渡せます。Node.js(サーバー側のJavaScript実行環境)ではファイルパス、ブラウザでは相対パスを指定してください。
node --input-type=module -e "const { createMorphFoundry, analyzeToTable } = await import('./articles/midori227_1/morphfoundry.js'); const analyzer = await createMorphFoundry({ defaultDictionaries: ['./articles/midori227_1/dictionaries/unidic-lite.json'] }); console.log(await analyzeToTable(analyzer, '形態素解析の結果を表示する。'));"
entryCount が 263,040 なら軽量版、876,802 なら全件版がロードできたサインです。辞書を配布する際はApache 2.0のライセンス文書とREADMEを必ず添付しましょう。
---
実データ(301件のCSV)を丸ごと解析する
> ここでわかること: 旧サイトのアーカイブ301件を解析し、ブラウザで耐久テストする流れ。
data.csv(CSV:Comma-Separated Values、カンマ区切り値)はカテゴリ・記事パス・公開日・説明文・動画パスを含む実データです。説明文をMorphFoundryに流し込み、名詞の出現頻度を集計します。
- 説明文(4列目)を解析し、名詞の上位20件をランキング化
- UniDic軽量辞書を使い、ブラウザでも数秒で解析
- 進捗ログには辞書ロード状況や上位語の抜粋をリアルタイム表示
301件の説明文を解析するデモを新しいタブで開く
ボタンひとつでカテゴリ数や日付レンジも算出するため、どの時期にどんなテーマが多かったかを俯瞰できます。CSVを差し替えれば、自分のアーカイブにもそのまま応用できます。
---
ユーザー辞書で専門語をサポート
> ここでわかること: addCustomEntry を使い、専門語をリアルタイムで追加する手順。
操作の流れは次の通りです。
1. 初期解析パネルで未知語フラグが立っている語を確認する。
2. 表層形・読み・品詞・コストを入力し、custom 辞書に追加して再解析する。
3. 比較パネルで追加前後の状態(未知語→既知語の切り替わり)を見比べる。
フォームに表層形・読み・品詞・コストを入力して反映すると、custom 辞書に追加され未知語ラベルが既知語へ切り替わります。ログとバッジで状態を可視化し、安心して辞書を拡張できるようにしました。大量追加はJSONで、細かな調整はUIで行う二段構えです。
---
WorkerとCLIのハンドシェイク
> ここでわかること: Web Workerが辞書ロード完了までどう待機し、CLIと同じロジックを共有するか。
初期は辞書ロード前に analyze を送ってしまい、常に未知語扱いになる失敗がありました。タイムライン化してみると、最初に listDictionary を投げるべきだったと分かったエピソードです。
バックグラウンド処理の実装例
demo9 では Worker(Web Worker、バックグラウンド処理)側で辞書ロードから解析までを担当し、メインスレッドは postMessage(メッセージ送信)/message(メッセージ受信)で進捗と結果を受け取ります。25件ごとに進捗を送ることで、ブラウザが固まっていないかをユーザーが判断できます。
<!-- analyzer 側 -->
worker.postMessage({ type: 'analyze', dataset: payload });
worker.addEventListener('message', (event) => {
const { type, current, total, tokens } = event.data;
if (type === 'progress') {
// ステータス更新
} else if (type === 'result') {
// トップ20の描画
}
});
// demo9-realdata-csv-worker.js
const analyzer = await createMorphFoundry({ defaultDictionaries: ['./dictionaries/unidic-lite.json'] });
for (let index = 0; index < dataset.length; index++) {
if (index % 25 === 0) {
self.postMessage({ type: 'progress', current: index, total: dataset.length });
}
// トークン解析 → 集計
}
self.postMessage({ type: 'result', tokens: tokenArray, totalTokens: tokenArray.length });
CLI(コマンドラインインターフェース)では同じAPI(アプリケーション・プログラミング・インターフェース)を同期呼び出しで利用できるため、ブラウザとNode(Node.js、サーバー側のJavaScript実行環境)でロジックを共有しやすくなっています。
---
ベンチマークで実力を測る
> ここでわかること: プリプロセッサや既存ライブラリとの比較による速度の見積もり。
教育向けの小規模検証ではMorphFoundry単体が既存ブラウザライブラリより15〜20%ほど速いケースがありました。一方でプリプロセッサを重ねるとすぐに逆転します。数字で可視化することで、必要な処理だけに集中できます。
---
開発フローの全体像
> ここでわかること: 調査から配布までのタスクと、関連資料への導線。
調査→実装→辞書生成→検証→配布の全工程をカード化しました。ダウンロードリンクもここに集約しています。軽量検証には unidic-lite.json、バッチ解析やNodeでは unidic-full.json を使うのが基本方針です。
---
コアとなるコード断片
> ここでわかること: ラティス構築の中心ロジックと未知語処理の入口。
// new_toppage/articles/midori227_1/morphfoundry.js(抜粋)
_buildLattice(text) {
const nodes = [];
nodes[0] = [{ cost: 0, index: -1, token: null }];
for (let position = 0; position < text.length; position++) {
if (!nodes[position]) continue;
const candidates = this._lookupDictionaries(text, position);
if (candidates.length === 0) {
candidates.push(this._createUnknownNode(text[position]));
}
for (const candidate of candidates) {
const endIndex = position + candidate.surface.length;
const entryCost = candidate.cost + this.connectionCost;
for (const prev of nodes[position]) {
const totalCost = prev.cost + entryCost;
const cell = { cost: totalCost, index: position, token: candidate, prev };
nodes[endIndex] = nodes[endIndex] || [];
const best = nodes[endIndex].find(item => item.token === candidate && item.index === position);
if (!best || best.cost > cell.cost) {
nodes[endIndex].push(cell);
}
}
}
}
return nodes;
}
未知語を生成する this._createUnknownNode では文字種ごとに推定品詞を割り当て、ペナルティを調整できるようにしています。学習用途でも改造しやすい構造です。
---
次の一歩
> ここでわかること: 実際に触るときのチェックリストと追加デモ。
実践時は次の3点を意識してください。
- 辞書をロードしたら
listDictionaries() で状態確認
- 未知語コスト・プリプロセッサは用途に応じてチューニング
- ユーザー辞書をテストに組み込み、追加語が意図通り解析されるか継続的に検証
MorphFoundryは教育向けに軽く作っていますが、UniDicを取り込むことで最新語彙にも対応できます。本番志向の辞書や大規模データへ置き換える足場としても使えるはずです。
---
まとめ
- Viterbi風の最小構成(最適経路を探索するアルゴリズム)でブラウザとNode.js(サーバー側のJavaScript実行環境)双方に対応した形態素解析ライブラリを自作
- UniDic(形態素解析用の辞書、Apache 2.0ライセンス)をJSON(JavaScript Object Notation、データ交換形式)化するスクリプトを用意し、軽量版とフル版を切り替えられるように整備
- ユーザー辞書やプリプロセッサを切り替えながら、Worker(Web Worker、バックグラウンド処理)連携・ベンチマーク・実データ解析まで検証
- ライブラリ本体・辞書変換ツール・辞書サンプル・README・デモを一括配布できる状態に整理
---
ライセンスとデータ提供元への配慮
- MorphFoundry 本体: Apache License 2.0
- UniDic 現代書き言葉(CSV版): 国立国語研究所が Apache License 2.0 で公開
- unidic-lite.json / unidic-full.json は lex.csv を unidic-converter.js でJSON化した派生データ。再配布時はライセンス文書の同梱とクレジット表記を必須とする。
- 教育・研究目的での解析・デモ公開は Apache 2.0 の範囲内。商用利用や大規模配布では公式ライセンスを改めて確認することを推奨。
国立国語研究所の利用規約に従い、READMEやダウンロード導線にもライセンス注意書きを記載しています。lex.csv を扱う際は必ず公式ページを確認してください。
---
さらに深く学ぶには
最後まで読んでくださり、ありがとうございました。辞書と未知語に悩む誰かが、MorphFoundryで一歩前に進めますように。検索好きな写真屋より。