MediaPipe文章比較入門:TextEmbedderとコサイン類似度
文章を数字の一覧へ変換する「埋め込み」を使うと、異なる表現の文章を数値で比較できます。ここでは愛猫についての自作英文と、別の話題の英文を使い、TextEmbedderで本物のベクトルを作ってコサイン類似度を求めます。数式を実装する前に、入力・ベクトル・比較結果のつながりを画面で確認しましょう。
1. この記事のゴールと使用する版
3つの英文を同じモデルでベクトルへ変換し、A対A・A対B・A対Cの類似度を計算できること。類似度を正解率と取り違えず、モデルと入力言語を揃える必要を説明できることを目指します。
前提はHTMLの要素、JavaScriptの変数・関数・イベント、async/awaitの基本です。モデルを自分で学習させる知識は不要です。CDN版のSDKを使用するため、このサンプルへMediaPipeをnpmで追加インストールする必要はありません。SDKとWASMは同じパッケージ・同じ版に固定します。
教材の構成:@mediapipe/tasks-text 1.1.0。モデルはUniversal Sentence Encoder、float32、モデル版1。量子化出力は使わずquantize:falseです。最新SDK版は2026年10月12日に公式npmで確認しました。
このサイトのAI画像ツールで使うSDKと配信方法、旧Solutionsとの違いは概要記事の現在のサイト構成に整理しています。音声・テキスト編は追加の学習教材です。今回のモデルは公式ストレージの版が分かるURLを指定し、コードの先頭で確認できます。
2. サンプルを試し、自分のPCで起動する
VS Codeでフォルダーを開く方法や、app.jsの編集・保存が初めてなら、導入記事のエディター準備と「保存 → ブラウザ更新 → 実行」の手順を先に確認してください。下では、このサンプルの起動に必要な操作を示します。
A・B・Cの英文をそのまま残し、「3つの英文を比較する」を押します。A対Aと、猫についてのA対B、別の話題のA対Cを見ます。次にBをAと全く同じ文へ変えて再実行します。数値とJSON内のベクトルの要素数・先頭8値が実際の推論から返ることを確認してください。

今回のベクトルと類似度を読む
A対Aは1.00000、愛猫と日光についてのA対Bは0.96229、データベースの更新についてのA対Cは0.66536でした。この3文の組ではA対Bの方が近い結果です。0.66536だけから「関係がある」「ない」を決める共通の閾値は設けていません。
コードはembeddings[0]という返却配列の最初のオブジェクトを使い、今回のJSONではそのheadIndexが1、ベクトルの長さが100でした。配列番号0とheadIndex 0は同じ意味ではありません。先頭の生の値には1を超える値もあり、出力の正規化をこの観察だけで仮定しません。ここでは量子化していない同じモデルのベクトルをAPIで比較しています。
ZIPを展開して、HTMLを直接開かずHTTPで起動する
- ZIPを保存し、エクスプローラーの「すべて展開」で中身を展開します。ZIPの中を閲覧している状態で実行しません。
index.htmlとapp.jsが見えるフォルダーで右クリックし、ターミナルを開きます。README.txtにも手順があります。- 下の最初の2行を1行ずつ実行します。バージョンが表示されればNode.jsとnpmを利用できます。「見つからない」ならNode.js公式サイトのLTS版を導入し、ターミナルを開き直します。
- 3行目を実行します。
http-serverの初回取得には通信が必要です。サーバーが起動したらターミナルは開いたままにします。 - ブラウザのアドレス欄へ
http://localhost:8080/を入力します。サンプルが表示されたら上の操作を試します。 - 編集後は保存してブラウザを再読み込みします。終了はターミナルでCtrl+Cです。
node --version
npm.cmd --version
npx.cmd --yes http-server . -p 8080 Windows PowerShellではnpm.cmdとnpx.cmdを使うと、スクリプト実行ポリシーによるつまずきを避けやすくなります。macOS・Linuxはnpm・npxで実行します。8080が使用中なら-p 8081へ変え、ブラウザ側もhttp://localhost:8081/に合わせます。
初回のボタン操作でSDK・WASM・モデルを取得するので、しばらく待つ場合があります。HTTPサーバーはHTMLとJavaScriptを配信するためのもので、AI推論をサーバー側で行う構成ではありません。file://での直接起動は使いません。基本操作の確認はPCのブラウザで行い、スマホは画面幅の確認と実機での動作検証を区別します。
3. 埋め込みは「文章を表す数字の一覧」
モデルは文字列から、複数の数値を並べたベクトルを作ります。同じモデルが同じ方法で作ったベクトルなら、方向の近さを計算して比較できます。分類のようにpositive / negativeを直接返す処理とは異なり、カテゴリを指定しなくても文章同士を比べられます。
一つの数値だけを見て「この要素は猫の量」と解釈することはできません。意味の表現はベクトル全体で捉えます。サンプルでは、先頭8値を表示して数字の配列が返ることを確認し、全文の長いベクトルを画面へ大量に出さないようにしています。
使うモデルはUniversal Sentence Encoderです。この教材の比較は英語の短文に限定します。日本語や複数言語の検索へそのまま広げる前に、対象言語を含むモデルの適用範囲と、そのデータでの品質を確認してください。日本語のサイト上で実行できることと、日本語の意味を十分に表せることは別です。
4. 同じモデルで3つの文を変換する
初期化にはFilesetResolver.forTextTasks()とTextEmbedder.createFromOptions()を使います。embed()へ文字列を渡すと、embeddingsの一覧を含む結果が返ります。今回のモデルでは最初の埋め込みを比較します。
const embedder = await TextEmbedder.createFromOptions(files, {
baseOptions: { modelAssetPath: MODEL },
quantize: false,
});
const first = embedder.embed(textA);
const second = embedder.embed(textB);quantize:falseにしたのは、浮動小数点の配列をそのまま確認するためです。量子化した出力を使う設定もありますが、数値の形式と扱いが変わります。学習の最初は、どの形式の結果が返るかをJSONで確かめてから変更すると混乱しません。
異なるモデルで作ったベクトルを混ぜないことも大切です。要素数が偶然一致しても、数値が同じ意味の空間にあるとは限りません。保存した文ベクトルに対してモデルを更新する場合は、既存のデータも同じモデルで作り直す必要があるかを検討します。コードに固定モデルのURLを残す理由は、比較条件を揃えるためでもあります。
5. コサイン類似度は一致率ではない
MediaPipeにはTextEmbedder.cosineSimilarity()が用意されているため、ここでは数式を自分で書かずに使います。第1引数と第2引数には、embeddings[0]のオブジェクトを渡します。floatEmbeddingの配列だけを渡す呼び出しではないことに注意します。
const similarity = TextEmbedder.cosineSimilarity(
first.embeddings[0],
second.embeddings[0],
);コサイン類似度の数学上の範囲は−1から1です。同じベクトルなら、浮動小数点計算の誤差を除いて1になります。サンプルのA対Aはこの基本確認です。A対BとA対Cの値は、使用するモデルと文章から計算された結果で、予め決めた値を表示していません。
例えば0.8という値が返っても、「文章の80%が同じ」「80%の確率で正しい」という意味にはなりません。同じ話題でも反対の意見を述べる文が近いベクトルになる場合があります。検索で上位候補を出す用途と、文章の意味が完全に一致すると断定する用途は、必要な確認が異なります。
6. ベクトルを確認し、検索への応用を考える
JSONには、入力文、SDKとモデル、処理時間、3組の類似度、各ベクトルの要素数と先頭8値を載せます。これらをセットで保存すると、モデルや文章を変更した後に比較条件を振り返れます。類似度だけを書き写すと、何を比べた値かが失われます。
const vector = first.embeddings[0].floatEmbedding;
console.log("要素数", vector.length);
console.log("先頭8個", vector.slice(0, 8));例文のBをAと同じにする、猫に関する別の表現へ変える、データベースの英文に変える、と少しずつ試してください。同じ文の比較が極端に低いなら、別のモデルの結果を混ぜていないか、別の出力ヘッドを選んでいないか、実際の入力文が同じかを確認します。
Webの小さな検索機能へ応用するなら、候補文を同じモデルで一度だけ埋め込みへ変換し、検索文のベクトルとの類似度で並べる構成が考えられます。文章数が増えたときの初期化・保存・検索速度は別に設計します。このサイトへ関連記事の候補提示などを加えるアイデアにはなりますが、現在のサイトにその検索が実装済みという意味ではありません。
最新の公式WebガイドにはEmbeddingGemma向けの入力形式の説明もあります。この教材はUniversal Sentence Encoderを使い、その機能を呼びません。新しいモデルへURLだけを置き換える前に、SDKの対応、モデル形式、入力の整形、必要なメモリを個別に確認します。まず固定した小さなサンプルで仕組みを理解することが、更新時の比較にも役立ちます。
この検証は自作の英文3つと、作成環境のブラウザでの動作確認です。多言語検索や大規模な文書検索の精度・速度は測っていません。入力文章を外部へ送信する処理はありませんが、SDKとモデルを取得する通信は行います。
7. 完全コードを確認する
以下はダウンロードZIPと同じソースです。index.htmlが画面と操作ボタン、app.jsがSDKの初期化・入力の準備・推論・出力表示を担当します。コードを途中だけコピーすると必要な要素が欠けるので、最初はZIPの完全版から起動してください。慣れたら設定を1つずつ変更し、JSONと画面の変化を見ます。
index.html:画面の完全コード
<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>MediaPipeで英文の似ている度合いを比較する | 猫の手ツール</title><style>:root{font-family:system-ui,sans-serif;color:#17243c;background:#eef2f8}*{box-sizing:border-box}body{margin:0}main{max-width:1080px;margin:auto;padding:28px 20px}h1{font-size:clamp(1.7rem,5vw,2.4rem)}p{line-height:1.8}button,input,select,textarea{font:inherit}button{background:#4338ca;color:white;border:0;border-radius:8px;padding:12px 18px;cursor:pointer}button:disabled{opacity:.55;cursor:wait}label{display:block;font-weight:650;margin:12px 0 6px}textarea{width:100%;min-height:100px;padding:12px;border:1px solid #aab7ce;border-radius:8px}select,input[type=file]{max-width:100%;padding:8px}section,.box{background:white;border:1px solid #dbe1eb;border-radius:14px;padding:18px;margin:18px 0}.controls{display:flex;flex-wrap:wrap;gap:12px;align-items:center}#status{border-left:4px solid #4338ca;padding:14px;background:white;min-height:54px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}canvas,video{max-width:100%;height:auto;display:block;border-radius:8px;background:#14243b}table{border-collapse:collapse;width:100%;font-size:.9rem}th,td{text-align:left;border-bottom:1px solid #dbe1eb;padding:10px}pre{background:#12203a;color:#e0e8ff;padding:16px;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere;max-height:420px;overflow:auto}.scroll{overflow:auto}meter{width:160px;max-width:100%}a{color:#4338ca}small{color:#53637d}button:focus-visible,a:focus-visible{outline:3px solid #e99a23;outline-offset:3px}@media(max-width:640px){.grid{grid-template-columns:1fr}main{padding:18px 12px}section{padding:14px}}</style></head><body><main><p><a href="./README.txt">起動手順を開く</a></p><h1>MediaPipeで英文の似ている度合いを比較する</h1><p>Universal Sentence Encoderで英文を数値のベクトルにし、コサイン類似度を計算します。点数は一致率・正解率ではありません。</p><section><h2>実行する</h2><label for="text-a">基準となる英文 A</label><textarea id="text-a" maxlength="2000" lang="en">I love watching my cat sleep in the sunshine.</textarea><label for="text-b">比較する英文 B</label><textarea id="text-b" maxlength="2000" lang="en">My cat is resting in the warm sunlight.</textarea><label for="text-c">比較する英文 C</label><textarea id="text-c" maxlength="2000" lang="en">The database server needs a security update.</textarea><button id="run-button">3つの英文を比較する</button></section><p id="status" role="status" aria-live="polite">準備できました。ボタンを押すとSDK・モデルを読み込みます。</p><section><h2>コサイン類似度</h2><table><thead><tr><th>比較</th><th>実測スコア</th><th>ベクトルの要素数</th></tr></thead><tbody id="results-body"></tbody></table><p><small>数学上の範囲は−1〜1。比較する文・モデルによって結果が変わります。</small></p></section><section><h2>実際の出力JSON・検証条件</h2><pre id="json-output">まだ実行していません。</pre></section><p><small>推論はブラウザ内で行います。SDK・モデル取得のためCDN / Googleの公開ストレージへ接続します。入力データを送信する処理はありません。記事の検証日:2026年10月12日。</small></p></main><script type="module" src="./app.js"></script></body></html>app.js:処理の完全コード
const VERSION = '1.1.0';
const BASE = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-text@${VERSION}`;
const MODEL = 'https://storage.googleapis.com/mediapipe-models/text_embedder/universal_sentence_encoder/float32/1/universal_sentence_encoder.tflite';
const inputs = ['a', 'b', 'c'].map(key => document.querySelector(`#text-${key}`));
const button = document.querySelector('#run-button');
const status = document.querySelector('#status');
const body = document.querySelector('#results-body');
const json = document.querySelector('#json-output');
let embedder, Embedder;
button.addEventListener('click', async () => {
const texts = inputs.map(input => input.value.trim());
if (texts.some(text => !text)) { status.textContent = 'A・B・Cすべてに英文を入力してください。'; body.replaceChildren(); return; }
button.disabled = true; inputs.forEach(input => { input.disabled = true; });
try {
if (!embedder) {
status.textContent = 'テキスト用SDK・WASM・埋め込みモデルを準備しています。';
const { TextEmbedder, FilesetResolver } = await import(`${BASE}/text_bundle.mjs`); Embedder = TextEmbedder;
const files = await FilesetResolver.forTextTasks(`${BASE}/wasm`);
// 生の浮動小数点値を確認するためquantize:false。出力の正規化は仮定しません。
embedder = await TextEmbedder.createFromOptions(files, { baseOptions: { modelAssetPath: MODEL }, quantize: false });
}
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const start = performance.now(); const results = texts.map(text => embedder.embed(text));
const vectors = results.map(result => result.embeddings[0]);
const comparisons = [['A ↔ A', 0, 0], ['A ↔ B', 0, 1], ['A ↔ C', 0, 2]].map(([pair, a, b]) => ({
pair, similarity: Embedder.cosineSimilarity(vectors[a], vectors[b]), dimensions: vectors[a].floatEmbedding?.length,
}));
const elapsedMs = performance.now() - start; body.replaceChildren();
for (const result of comparisons) {
const row = body.insertRow(); row.insertCell().textContent = result.pair;
row.insertCell().textContent = result.similarity.toFixed(5); row.insertCell().textContent = String(result.dimensions);
}
json.textContent = JSON.stringify({ sdkVersion: VERSION, model: MODEL, inputTexts: texts, elapsedMs,
userAgent: navigator.userAgent, comparisons, vectorPreviews: vectors.map(vector => ({
headIndex: vector.headIndex, dimensions: vector.floatEmbedding?.length,
first8Values: vector.floatEmbedding?.slice(0, 8),
})) }, null, 2);
status.textContent = `比較完了:3文の埋め込みと類似度 ${elapsedMs.toFixed(2)} ms。類似度を正解率とは解釈しません。`;
} catch (error) { console.error(error); status.textContent = `実行できませんでした:${error.message}。HTTP起動・通信を確認してください。`; }
finally { button.disabled = false; inputs.forEach(input => { input.disabled = false; }); }
});
window.addEventListener('pagehide', () => { embedder?.close(); embedder = null; });
画面には入力文やモデルのラベルをtextContentで表示します。文字列をHTMLとして実行する必要はありません。ボタンを処理中に無効にするのは、初期化や推論を重ねて実行しないためです。推論が同期処理のタスクでは、async関数から呼んでも自動的に別スレッドにはなりません。
8. 困ったときの確認表
| 症状 | 確認 |
|---|---|
| cosineSimilarity()でエラー | 配列だけでなくembeddings[0]のオブジェクトを渡し、同じモデル・同じ要素数を確認します。 |
| 同じ話題の反対意見が似ている | 意味の完全一致判定ではありません。用途に合う例文で追加確認します。 |
| 日本語の比較が予想と違う | この教材は英文向けです。対象言語を含むモデルと実データで品質を確認します。 |
| モデル変更後に検索が不自然 | 旧モデルで作った保存ベクトルと、新モデルの出力を混ぜていないか確認します。 |
| SDK・モデルを読み込めない | HTTP起動か、通信できるか、固定URLと版が揃っているか確認します。開発者ツールのConsoleとNetworkで、最初のエラーと失敗した取得先を見ます。 |
問い合わせや自分のメモでは「動かない」だけでなく、使用OS・ブラウザ、選んだ入力、画面の状態、最初のエラー、SDK・モデルの版を残すと切り分けやすくなります。入力内容や私的なファイル名を公開する必要はありません。この記事のキャプチャは実行したサンプルの観察で、すべての端末・入力への品質を保証するものではありません。
参考資料
資料確認日:2026年10月12日。SDKとモデルは別々に更新されます。更新する際は固定した版のサンプルを残し、変更後の入力・結果・実行条件を比べてください。