埋め込み:意味を数値にする
オープンソースの中国語埋め込みモデルを手元で動かして文をベクトルに変え、コサイン類似度で意味を比べます。何が得意で、何が区別できないのかを見ていきます。この先のセマンティック検索と RAG の土台です。
- 約 35 分
- 難易度:入門
- 検証:2026-09-14 bge-small-zh-v1.5、sentence-transformers
コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。
httpx の Q&A アシスタントを作っているとしましょう。ユーザーは「httpx でリクエストのタイムアウトはどう設定する?」と聞くのに、ドキュメントに書いてあるのは「timeout configuration」。「リクエストがエラーになるまでもう少し長く待たせるには?」と聞かれても、ドキュメントには「タイムアウト」という言葉すら出てきません。キーワードで検索すると、どの言い方でも該当する段落が見つかりません。
「字面」ではなく「意味」を比べる方法が必要です。それをするのが埋め込み(embedding)です。
埋め込みとは
埋め込みモデルは別の種類のモデルです。文章を生成せず、することは一つだけ。文章を入力すると、固定長の数字の並び、つまりベクトルを出力します。学習の目標は、意味の近い文章からは、近いベクトルが得られることです。
「近さ」は一つの数値で測ります。最もよく使われるのはコサイン類似度で、二つのベクトルの向きがそろっているほど 1 に近く、まったく無関係なら 0 に近くなります。2 次元の例で直感的に感じてみましょう。
import numpy as np
def cosine(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
print(cosine(np.array([1, 2]), np.array([2, 4]))) # 方向完全相同,长度不同
print(cosine(np.array([1, 2]), np.array([2, -1]))) # 互相垂直
0.9999999999999998
0.0
[1, 2] と [2, 4] は長さが違いますが、向きはまったく同じなので、類似度は 1 です(表示される 0.9999999999999998 は浮動小数点計算のわずかな誤差で、1 とみなしてかまいません)。コサイン類似度は向きだけを見て長さは見ません。これこそ望んでいる性質です。文が少し長いか短いかで、意味が変わるべきではないからです。
本物の埋め込みベクトルは数百から数千次元あって図には描けませんが、計算方法はまったく同じです。
DeepSeek には埋め込み API がない
2026 年 9 月時点で、DeepSeek の API が提供しているのは会話モデルだけで、埋め込みモデルはありません。OpenAI SDK で埋め込み API を呼んでみたところ、そのまま 404 が返ってきました。
方法は二つあります。
- 他社の埋め込み API を使う。Alibaba Cloud 百煉、智譜(Zhipu)、OpenAI などが埋め込み API を提供しており、呼び出し方は会話 API と同じく OpenAI SDK を使います(
client.embeddings.create(...))。具体的なモデル名は各社のドキュメントに従ってください。 - オープンソースの埋め込みモデルを手元で動かす。埋め込みモデルは会話モデルよりずっと小さく、普通のパソコンの CPU で動きます。
このコースでは二つ目を使います。選んだのは北京智源人工知能研究院(BAAI)がオープンソースで公開している bge-small-zh-v1.5 です。中国語向けに学習されていて、パラメータはわずか 2400 万、ダウンロードは 92MB、完全無料で、ネット接続なしで使え、資料が第三者に漏れる心配もありません。
やってみる:いくつかの文の意味を比べる
パッケージを一つ入れます。
uv add sentence-transformers
sentence-transformers は埋め込みモデルを動かすための専用ライブラリです。初回実行時に Hugging Face からモデルをダウンロードします。中国国内でダウンロードが遅い場合は、先にミラーを設定してください。
export HF_ENDPOINT=https://hf-mirror.com
そして次のスクリプトを実行します。
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
query = "httpx 怎么设置请求超时?"
sentences = [
"How do I set a timeout for requests in httpx?",
"httpx 的 timeout 参数默认是 5 秒。",
"给 httpx 客户端配置代理服务器",
"requests 库如何设置超时时间",
"今天中午吃了一碗牛肉面",
]
# normalize_embeddings=True 把每个向量的长度缩放成 1,这样点积就等于余弦相似度
vectors = model.encode([query] + sentences, normalize_embeddings=True)
print(f"每句话变成了一个 {vectors.shape[1]} 维的向量")
print(f"第一句的前 5 个数:{np.round(vectors[0][:5], 4)}")
print()
q, rest = vectors[0], vectors[1:]
scores = rest @ q
print(f"和「{query}」的相似度:")
for score, text in sorted(zip(scores, sentences), reverse=True):
print(f" {score:.3f} {text}")
実行結果です(このコードは API を一切呼ばないので、同じ数値が得られるはずです)。
每句话变成了一个 512 维的向量
第一句的前 5 个数:[-0.0232 0.0017 0.0812 0.0058 0.0116]
和「httpx 怎么设置请求超时?」的相似度:
0.777 requests 库如何设置超时时间
0.673 httpx 的 timeout 参数默认是 5 秒。
0.669 How do I set a timeout for requests in httpx?
0.659 给 httpx 客户端配置代理服务器
0.202 今天中午吃了一碗牛肉面
各文が 512 個の数値に変わりました。この数値を単独で見ても意味はなく、意味があるのはベクトル同士の類似度です。
結果から読み取れること
意味の関係する文はスコアが高く、無関係な文は低い。HTTP リクエストに関係する 4 つの文はすべて 0.65 以上で、「今天中午吃了一碗牛肉面」(今日の昼に牛肉麺を一杯食べた)はわずか 0.2 です。はっきり区別できています。
言語をまたいでもマッチする。英語の「How do I set a timeout for requests in httpx?」は中国語の質問と 1 文字も重なっていないのに、スコアは 0.669 です。これが埋め込みがキーワード検索より優れている点です。
しかし、どのライブラリかは区別できない。スコアが最も高かったのは、なんと「requests 库如何设置超时时间」(requests ライブラリでタイムアウトを設定するには)で、httpx についての 2 つの文よりも高いのです。埋め込みモデルから見ると、「○○ライブラリでタイムアウトをどう設定するか」という文型と意味が最も重要で、requests か httpx かは副次的な違いにすぎません。ところが Q&A アシスタントにとっては、それこそが最も重要な違いです。requests のドキュメントで httpx の質問に答えれば、間違いになります。
同じように、「给 httpx 客户端配置代理服务器」(httpx クライアントにプロキシサーバーを設定する)はタイムアウトではなくプロキシの話なのに 0.659 を得ていて、タイムアウトの文とわずかな差しかありません。
これは、埋め込みが「大意」をとらえるのは得意でも、固有名詞、関数名、バージョン番号のような正確なものには敏感でないことを示しています。モジュール 04 第 4 課では、埋め込みとキーワード検索を組み合わせて、この問題に取り組みます。
スコアに絶対的な意味はない。0.67 は高いのか低いのか。決まった答えはありません。埋め込みモデルによってスコアの範囲は違い、同じモデルでも分野によって違います。役に立つのは比較です。同じ質問に対して、どの段落が上位に来るか。自分のデータで検証していない限り、「類似度が 0.7 を超えたら関連あり」のようなルールをコードにハードコードしないでください。
埋め込みの用途
- セマンティック検索:すべてのドキュメントの段落のベクトルをあらかじめ計算して保存しておき、ユーザーが質問したら質問のベクトルを計算して、最も似ている段落をいくつか探します。これが RAG の中心で、モジュール 04 第 3 課でゼロから書きます。
- 重複排除:二つの文章のベクトルがほとんど同じなら、たいてい重複した内容です。
- 分類とクラスタリング:ユーザーのフィードバックや issue を意味で自動的にグループ分けします。
- レコメンド:ユーザーが見た内容と意味の近い別の内容を探します。
使うときの落とし穴
質問とドキュメントは同じモデルでなければならない。埋め込みモデルが違えばベクトルにはまったく互換性がなく、二つの異なる座標系のようなものです。埋め込みモデルを替えたら、すべてのドキュメントを計算し直す必要があります。
長さに上限がある。bge-small-zh-v1.5 が読めるのは最大 512 トークンで、超えた部分はそのまま捨てられ、しかもエラーになりません。ですから長いドキュメントは先に小さな段落に分割し、それぞれベクトルを計算します。これはモジュール 04 第 2 課の内容です。
質問に接頭辞を付ける必要があるモデルもある。埋め込みモデルの中には、クエリの前に「为这个句子生成表示以用于检索相关文章:」(この文の表現を生成して関連する記事の検索に使う:)のような決まった説明を付けると検索の効果が上がるとしているものがあります。付けるかどうか、何を付けるかはモデルの説明書を見てください。この課では簡単にするため付けていません。
練習問題
sentencesに自分で考えた文をいくつか加えてください。言い方を変えただけで意味がまったく同じもの(「httpx 请求等太久怎么办」、httpx のリクエストが長く待ちすぎるときはどうする)と、キーワードは同じだが意味が違うもの(「超时费用怎么计算」、延長料金はどう計算する)。それぞれ何位に来るでしょうか。- クエリを英語の「How to configure a proxy in httpx?」に替えて、順位がどう変わるか見てください。
- 百煉、智譜、OpenAI のキーを持っているなら、
client.embeddings.create(model=..., input=[...])でそれぞれの埋め込み API を呼び、同じ文の類似度を計算して、ローカルモデルの結果と比べてください。スコアの範囲がまったく違う可能性があるので、順位を比べれば十分です。
確認テスト
1. コサイン類似度がベクトルの向きだけを見て、長さを見ないのはなぜですか?
私たちが気にしているのは二つの文章の意味が近いかどうかで、文章の長さやその他の無関係な量ではありません。コサイン類似度は二つのベクトルの長さで割っているので、向きの情報だけが残ります。ベクトルをあらかじめ長さ 1 に正規化しておけば、内積がそのままコサイン類似度になり、計算も速くなります。
2. ユーザーは httpx について質問したのに、埋め込み検索が requests ライブラリのドキュメントを 1 位にしました。なぜでしょうか。どうすればよいですか?
埋め込みモデルがとらえるのは全体の意味で、「あるライブラリでタイムアウトをどう設定するか」という意味はとても近く、ライブラリ名は副次的な違いにすぎないので、どちらも高いスコアになります。解決策は、埋め込み検索とキーワード検索を組み合わせる(キーワード検索は「httpx」のような正確な語に敏感です)か、httpx のドキュメントの中だけを検索するよう限定することです。
3. 5000 字の記事をそのまま bge-small-zh-v1.5 に渡してベクトルを計算すると、何が起きますか?
最初の 512 トークンしか読まず、残りは切り捨てられ、しかもエラーになりません。計算されたベクトルは記事の冒頭の意味しか表しません。ですから長いドキュメントは先に小さな段落に分割し、段落ごとにベクトルを計算します。
質問と議論
このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。
質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。
議論を読み込んでいます…