Skip to content

Instantly share code, notes, and snippets.

@45deg
Last active July 2, 2026 15:59
Show Gist options
  • Select an option

  • Save 45deg/436ae740673b892a1c20dcd96bd21844 to your computer and use it in GitHub Desktop.

Select an option

Save 45deg/436ae740673b892a1c20dcd96bd21844 to your computer and use it in GitHub Desktop.
title: "ブラウザ完結型・日本語句読点復元モデルの設計と軽量化技術"
description: "キャラクターレベルTransformerの構築、ONNX変換、およびINT8動的量子化によるモデル軽量化を行い、ブラウザ上でのセキュアかつ低遅延な句読点復元推論を実現するプロセス。"
blocks:
- type: card
title: "1. 開発背景と音声文字起こしの課題"
icon: "mic"
color: "context"
body:
- type: markdown
text: |
音声認識技術の進歩により、会議やインタビューの文字起こしは容易になりました。しかし、音声認識の出力は句読点や改行が失われやすく、人間が読むには文頭・文末の判別が困難で非効率的です。
この文章構造の復元において、大規模言語モデル(LLM)のAPIを利用する方法もありますが、実用上いくつかの課題があります。
- type: list
variant: "definitions"
items:
- term: "意図しない言い換えの発生"
body: "LLMは入力された文章を「要約」や「補完」してしまい、元の発話テキストをそのまま維持できない場合があります。"
- term: "情報漏洩リスク"
body: "社外秘の会議や個人情報を含む音声を、外部サーバーのAPIへ送信することにはセキュリティ上の懸念があります。"
- term: "通信遅延とAPIコスト"
body: "長文を処理する際の通信待ち時間(レイテンシ)と、利用トークン量に応じたAPIコストが発生し続けます。"
- type: card
title: "2. 設計目標とタスク定義"
icon: "target"
color: "concept"
body:
- type: layout
variant: "side_by_side"
size: [1.2, 1]
columns:
-
- type: markdown
text: |
本プロジェクトでは、「元の発話文を完全に維持し、かつサーバーを介さずブラウザローカルで安全かつ瞬時に句読点と改行を挿入する」ことを目標としました。
このため、テキストの「生成」ではなく**「各文字境界の分類問題」**としてタスクをシンプルに再定義しました。
- **入力**: 既存の「、」「。」「?」や改行、不要な空白などをすべて取り除いた、平坦な文字の並び。
- **出力**: 元の文字列の順序を完全に保子、予測された文字境界に「、」「。」および改行コードのみを挿入したテキスト。
-
- type: list
title: "出力ヘッドの分離設計"
variant: "definitions"
items:
- term: "句読点ヘッド"
body: "各文字の直後に「なし(NONE)」「読点(、)」「句点(。)」のいずれを入れるかを予測する3クラス分類。"
- term: "改行ヘッド"
body: "句読点予測とは独立して、各文字の直後で改行を入れるべきかを判定する二値分類(0〜1の確率)。"
- type: card
title: "3. なぜ「キャラクターレベル(文字単位)」なのか"
icon: "network"
color: "term"
body: |
日本語のNLP(自然言語処理)では、形態素解析を用いて単語に分割してから処理することが一般的です。しかし、本モデルでは一文字単位(文字トークン)で処理するアプローチを採用しました。
- **境界の曖昧性の回避**: 句読点が存在しない話し言葉テキストは、単語の区切り自体が曖昧になり、単語分割器がエラーを起こしやすくなります。
- **未知語への強さ**: 固有名詞や新語、話し言葉特有の表現、誤字脱字が含まれていても、文字レベルであれば「辞書にない単語(Out-of-Vocabulary)」として処理が破綻することがありません。
- **語彙サイズの圧縮**: 単語単位では数万〜数十万の語彙テーブルが必要ですが、文字単位であれば約8,300文字でカバーでき、モデルの省メモリ化に貢献します。
- type: card
title: "4. モデル構造:自己注意機構とSwiGLUの採用"
icon: "brain"
color: "procedure"
body:
- type: layout
variant: "side_by_side"
size: [1.3, 1]
columns:
-
- type: markdown
text: |
本モデルは、4層のTransformer Encoderを核とする軽量ニューラルネットワークです。
- **自己注意機構 (Self-Attention)**:
文中のすべての文字の関係性を並列で計算します。文頭から文末まで同時に文脈を見渡すことができるため、「〜ですが」のような接続表現や「〜します」といった語尾の特徴から適切な挿入境界を予測します。
- **埋め込み層**:
文字トークン埋め込み $E(t_i)$ に、最大192文字の絶対位置エンコーディング $P(i)$ を加算。
$$x_i = E(t_i) + P(i)$$
- **SwiGLU FFNの採用**:
近年の大規模言語モデル(LLaMAなど)でも採用されている活性化関数を導入しました。
$$\text{SwiGLU}(x) = (\text{SiLU}(x W_1) \otimes x W_2) W_3$$
従来のGELU FFNに比べてパラメータあたりの表現能力が優れており、モデル層数を増やさずに予測精度を高めることが可能です。
-
- type: diagram
format: "mermaid"
width: 100
body: |
flowchart TD
Input["入力: 文字ID列"] --> Embed["Token + Position Embedding"]
Embed --> Norm["LayerNorm + Dropout"]
Norm --> Enc["Transformer Encoder (4 Layers)"]
Enc --> LN["Final LayerNorm"]
LN --> PHead["Punctuation Head"]
LN --> NHead["Newline Head"]
PHead --> POutput["NONE / 、 / 。"]
NHead --> NOutput["改行確率"]
style Input fill:#e0f2fe,stroke:#0284c7,stroke-width:1px,color:#0369a1
style Embed fill:#e0f2fe,stroke:#0284c7,stroke-width:1px,color:#0369a1
style Norm fill:#f3f4f6,stroke:#4b5563,stroke-width:1px,color:#374151
style Enc fill:#faf5ff,stroke:#7c3aed,stroke-width:2px,color:#6d28d9
style LN fill:#f3f4f6,stroke:#4b5563,stroke-width:1px,color:#374151
style PHead fill:#ecfdf5,stroke:#059669,stroke-width:1px,color:#047857
style NHead fill:#ecfdf5,stroke:#059669,stroke-width:1px,color:#047857
style POutput fill:#fff1f2,stroke:#e11d48,stroke-width:1px,color:#be123c
style NOutput fill:#fff1f2,stroke:#e11d48,stroke-width:1px,color:#be123c
caption: "共通のエンコーダーから2つの予測ヘッドへと分岐するデュアルヘッド構造"
- type: card
title: "5. 学習データ:LLM-jp Corpus v4 の選定と混合"
icon: "database"
color: "context"
body:
- type: layout
variant: "side_by_side"
size: [1, 1]
columns:
-
- type: markdown
text: |
学習データには、大規模言語モデルの事前学習用コーパスである **LLM-jp Corpus v4** の日本語サブコーパスを利用しました。
音声文字起こしに近い話し言葉や、多様な表記スタイルをカバーするため、独自の比率でデータを混合しています。
- **ja_fineweb-2 (40%)**: Webサイト由来のカジュアルな表現や多様な言い回し。
- **ja_wiki (Wikipedia; 30%)**: 正確な固有名詞、論理的で端正な表記構造。
- **ja_kokkai_giji (国会会議録; 20%)**: 口語でのやり取り、発話特有の長文パターン。
- **ja_aozorabunko (青空文庫; 10%)**: 文末表現や丁寧な句読点の打ち方。
-
- type: diagram
format: "vega_lite"
width: 100
body:
mark:
type: arc
innerRadius: 45
data:
values:
- corpus: "ja_fineweb-2 (Web)"
ratio: 40
- corpus: "ja_wiki (百科事典)"
ratio: 30
- corpus: "ja_kokkai_giji (発話)"
ratio: 20
- corpus: "ja_aozorabunko (文学)"
ratio: 10
encoding:
theta:
field: "ratio"
type: "quantitative"
color:
field: "corpus"
type: "nominal"
tooltip:
- field: "corpus"
type: "nominal"
- field: "ratio"
type: "quantitative"
view:
stroke: null
caption: "学習時における日本語サブコーパスの混合比"
- type: card
title: "6. クラス不均衡問題とエポック毎の学習推移"
icon: "trending-up"
color: "important"
body:
- type: layout
variant: "side_by_side"
size: [1.2, 1]
columns:
-
- type: diagram
format: "vega_lite"
width: 100
body:
data:
values:
- epoch: 1
metric: "読点F1"
value: 0.5096
- epoch: 2
metric: "読点F1"
value: 0.5481
- epoch: 3
metric: "読点F1"
value: 0.5803
- epoch: 4
metric: "読点F1"
value: 0.5981
- epoch: 5
metric: "読点F1"
value: 0.6117
- epoch: 1
metric: "句点F1"
value: 0.6356
- epoch: 2
metric: "句点F1"
value: 0.6923
- epoch: 3
metric: "句点F1"
value: 0.7286
- epoch: 4
metric: "句点F1"
value: 0.7436
- epoch: 5
metric: "句点F1"
value: 0.7602
- epoch: 1
metric: "改行F1"
value: 0.4579
- epoch: 2
metric: "改行F1"
value: 0.5227
- epoch: 3
metric: "改行F1"
value: 0.5294
- epoch: 4
metric: "改行F1"
value: 0.5588
- epoch: 5
metric: "改行F1"
value: 0.5668
mark:
type: line
point: true
encoding:
x:
field: "epoch"
type: "ordinal"
y:
field: "value"
type: "quantitative"
scale:
domain: [0.4, 0.8]
color:
field: "metric"
type: "nominal"
width: 340
height: 200
caption: "エポックごとの各指標(F1値)の推移"
-
- type: markdown
text: |
**クラス不均衡(少数ラベル)への対策**
文字境界の9割以上は「記号なし(NONE)」です。「、」や「。」は極めてまれにしか出現しないため、単純に学習させるとモデルがすべてNONEと予測するようになってしまいます。
これを防ぐため、クロスエントロピー損失関数において、句読点と改行の正例クラスに重みを付けてインバランスを解消しました。
**指標の分析**:
- **句点 F1 (0.7602)**: 文末は「〜です」「〜ます」などの定型パターンが多く学習しやすいため、高スコアを獲得。
- **読点 F1 (0.6117)**: 挿入位置に個人差や文体の依存度が高く、最も難度の高いタスクとなりました。
- **改行 F1 (0.5668)**: 文脈や発話リズムに依存するため、安定化に時間を要しました。
- type: card
title: "7. ONNXフォーマットへのエクスポート"
icon: "package"
color: "procedure"
body: |
PythonやPyTorchによる開発環境は重厚であり、そのままブラウザ上で動かすことはできません。そこで、学習済みのモデルを共通モデル形式である **ONNX (Open Neural Network Exchange; opset 17)** へ書き出しました。
- **静的グラフ化**: 実行モデルを数式演算の静的なつながり(計算グラフ)としてエクスポートすることで、Pythonのコード実行系に依存せず、V8などのJavaScript実行エンジンから呼び出せるようにしました。
- **推論用マルチ出力ラッパー**: 句読点用と改行用の2つのヘッドの演算を一度に並列実行して結果を返すよう、ONNXモデルの出力を一つの実行ユニットにラッピングしました。
- **前処理メタデータの分離**: 文字とIDをひも付ける語彙テーブル、しきい値やスライディングウィンドウサイズなどの推論パラメータを「メタデータJSON」として別途切り出しました。これにより、Python側とブラウザ側の前処理の整合性を完全に維持しています。
- type: card
title: "8. INT8動的量子化によるモデル容量と実行速度の最適化"
icon: "scale"
color: "supplement"
body:
- type: layout
variant: "side_by_side"
size: [1.2, 1]
columns:
-
- type: markdown
text: |
Webアプリケーションでモデルを配信するためには、ファイルサイズの軽量化と、CPU上での実行性能の向上が不可欠です。本プロジェクトでは **動的量子化 (Dynamic Quantization)** を適用しました。
$$q = \text{round}\left(\frac{r}{S}\right) + Z$$
- **値の変換**: 浮動小数点数(FP32、32ビット)で表現されている重みを、情報をある程度保ちながら8ビット整数(INT8)へ丸め込みました。
- **サイズの圧縮**: 学習済みのPyTorchチェックポイント(119 MiB)から、ONNX変換と量子化により **10 MiB** へと約12分の1に圧縮。一般的なブラウザでも高速ダウンロード可能なサイズに収めました。
- **演算の高速化**: INT8への量子化により、一般的なCPUに備わっている整数並列演算命令(WASM SIMDなど)を活用した、より単純で高速な行列計算が実行可能になりました。
-
- type: diagram
format: "vega_lite"
width: 100
body:
data:
values:
- stage: "Checkpoint"
size: 119
- stage: "FP32 ONNX"
size: 33.8
- stage: "INT8 ONNX"
size: 10
mark: "bar"
encoding:
x:
field: "stage"
type: "nominal"
sort: null
axis:
title: "開発フェーズ"
labelAngle: 0
y:
field: "size"
type: "quantitative"
axis:
title: "ファイルサイズ (MiB)"
tooltip:
- field: "stage"
type: "nominal"
- field: "size"
type: "quantitative"
width: 250
height: 140
caption: "各開発フェーズにおけるファイルサイズの圧縮推移"
- type: card
title: "9. フロントエンドでの長文処理:スライディングウィンドウ"
icon: "monitor"
color: "procedure"
body:
- type: layout
variant: "side_by_side"
size: [1.2, 1]
columns:
-
- type: markdown
text: |
開発したTransformerモデルが一度に処理できるテキスト長は最大192文字です。しかし実業務での議事録や書き起こしテキストは数千文字以上に及びます。
これを処理するため、**スライディングウィンドウ方式**を実装しました。
- **境界情報の連続性の維持**:
長文を単に192文字で細切れにすると、分割した端の文字では前後の文脈が不足し、句読点予測のブレや誤判定が多発します。
そこで、隣り合うウィンドウ同士を**75%(144文字)重複(ストライド)**させながら切り出して推論を実行します。
- **予測結果の重み付き平均**:
同じ文字に対して複数回の予測結果(重なり部分)が生じるため、これらを単純平均するのではなく、各ウィンドウの**「中心に近い予測ほど信頼し、端に近い予測は重みを小さくする」**加重平均処理(Center Weighting)を適用してマージします。
$$\text{logit}_{\text{final}}(x) = \frac{\sum_w W_w(x) \cdot \text{logit}_w(x)}{\sum_w W_w(x)}$$
この工夫により、ウィンドウの継ぎ目における予測の不連続性を解消しました。
-
- type: diagram
format: "mermaid"
width: 100
body: |
flowchart TD
A["入力テキスト"] --> B["前処理: 記号・空白除去"]
B --> C["192文字の重複ウィンドウに分割"]
C --> D["WASM上のONNX Runtime推論"]
D --> E["Center Weighting加重平均統合"]
E --> F["判定(マージン 0.75 / 閾値 0.40)"]
F --> G["結果のテキスト結合出力"]
style A fill:#fdf2f8,stroke:#db2777,stroke-width:1px,color:#9d174d
style G fill:#fdf2f8,stroke:#db2777,stroke-width:1px,color:#9d174d
style B fill:#f0fdf4,stroke:#16a34a,stroke-width:1px,color:#15803d
style C fill:#f0fdf4,stroke:#16a34a,stroke-width:1px,color:#15803d
style E fill:#f0fdf4,stroke:#16a34a,stroke-width:1px,color:#15803d
style F fill:#f0fdf4,stroke:#16a34a,stroke-width:1px,color:#15803d
style D fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1d4ed8
caption: "スライディングウィンドウと統合推論の全体処理フロー"
- type: card
title: "10. まとめと得られた知見"
icon: "wrench"
color: "note"
body:
- type: list
title: "本プロジェクトから得られた知見"
variant: "definitions"
items:
- term: "タスク再定義による軽量化"
body: "「文章の再生成」ではなく「文字境界への挿入判定(分類タスク)」に特化させたため、10 MiBの超軽量・高性能なエッジ推論が実現しました。"
- term: "ローカル実行とプライバシー"
body: "サーバー通信が一切不要なクライアント完結型にしたことで、機密文書や会議録を完全に保護できる実用的なセキュリティ体制が担保されました。"
- term: "一貫した前処理設計"
body: "前処理ルールやしきい値をメタデータとしてモデルと分離して管理する手法は、学習時と推論環境の不一致を防ぐ上で有効に機能しました。"
@45deg

45deg commented Jul 2, 2026

Copy link
Copy Markdown
Author
Screenshot 2026-07-02 at 23-20-08 LLMjp Corpus Viewer Screenshot 2026-07-02 at 23-19-47 LLMjp Corpus Viewer Screenshot 2026-07-02 at 23-19-03 LLMjp Corpus Viewer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment