実は進化している!ローカルで動くembeddingモデルたち
要約
- 日本語オンリーならruri v3 (わずか37mでOpenAIのtext-embedding-large-v3超え)
- もしかしたら日英だったらベターかも
- 多言語+コードならgranite-embedding
はじめに
LLMの普及からはや数年、175Bとかいう途方もないパラメータで動いていたLLMもいつの間にか4bに収まるようになり、スマホやPCで簡単に動かせるようになりました(現在だとQwen3-4b-thinking-2507などはかなり高性能です)。
一方、embeddingモデルはといえば、OpenAIはtext-embeddings-3-small/largeを公開してからというもの新規モデルをリリースしていません。geminiはちょくちょくリリースされているようですが、そこまで話題に上がっていません。embeddingモデルの進化は飽和してしまったのでしょうか?
実は、オープンウェイトの世界ではembeddingモデルはめちゃくちゃ進化しています。今回は、いつの間にかリリースされていたオープンウェイトのembeddingモデルたちを、特徴とともに2つほど紹介したいと思います。
そもそもembeddingモデルとは
RAG(Retrieval Augmented Generation)などにおいてユーザーの入力と関連したドキュメントを探す必要があります。ドキュメントを探すためにはユーザーの入力とデータベース中のドキュメントの類似度を評価し、関連しているかどうかを判定する必要があるわけです。embeddingモデルは与えられたドキュメントをembeddingと呼ばれるベクトルに変換することができる言語モデルです。
具体的には
- ドキュメントをembeddingモデルでベクトルに変換しDBに保存
- 検索クエリをembeddingモデルでベクトルに変換
- コサイン類似度により検索クエリとDB中の各ドキュメントのベクトルの類似度を求める
- 類似度が近い順にN件の文書を返却する
という流れで検索クエリに関連したドキュメントを探索します。
この時、embeddingモデルが適切に学習されていないと、クエリに関連したドキュメントを発見できなかったり、逆にクエリと無関係なドキュメントの類似度を高いと判定してしまうので、その性能はRAGそのもの完成度と直結するわけです(実務分野では古典的なキーワード検索と組み合わせたり、rerankerと呼ばれる遅い代わりに精度が高いモデルで再評価することで精度を向上させているそうです)。
選定基準
ローカルでembeddingモデルを導入するモチベーションとして考えられるのは
- APIコストを抑える
- 機密情報をAPIに投げたくない
あたりでしょう。逆にデメリットとしては - パラメータ数によっては速度が遅くなる
- メモリを消費する
あたりが考えられます。そこで、今回の記事では - パラメータ数が200M以下(≒8ビット量子化で200MB以内に収まる)
を目安としてモデルを選定しています。
ruri-v3
ruriは名古屋大学のチームが中心となって開発しているembeddingモデルのシリーズです。ruri-v3の最大の特徴はmodernBERTという技術をベースとしていることです。modernBERTというのはBERTをベースに近年のLLM技術の進歩をフィードバックしたモデル群のことで、同サイズのBERTよりも高性能を発揮できます。
ruri-v3はSB Intuitionsのmodernbert-jaというモデルをベースにしています。
ruri-v3で特に驚異的なのはruri-v3-30m
です。このモデルはJMTEBベンチマークにおいてパラメータ数わずか37MでOpenAIのtext-embedding-v3-large
を上回る性能を発揮しています。
JMTEBベンチマークの比較
レポジトリのREADME.mdより引用(一部改変):
Model | #Param. | Avg. | Retrieval | STS | Classfification | Reranking | Clustering | PairClassification |
---|---|---|---|---|---|---|---|---|
Ruri-v3-30m | 37M | 74.51 | 78.08 | 82.48 | 74.80 | 93.00 | 52.12 | 62.40 |
Ruri-v3-70m | 70M | 75.48 | 79.96 | 79.82 | 76.97 | 93.27 | 52.70 | 61.75 |
Ruri-v3-130m | 132M | 76.55 | 81.89 | 79.25 | 77.16 | 93.31 | 55.36 | 62.26 |
Ruri-v3-310m | 315M | 77.24 | 81.89 | 81.22 | 78.66 | 93.43 | 55.69 | 62.60 |
OpenAI/text-embedding-ada-002 | - | 69.48 | 64.38 | 79.02 | 69.75 | 93.04 | 48.30 | 62.40 |
OpenAI/text-embedding-3-small | - | 70.86 | 66.39 | 79.46 | 73.06 | 92.92 | 51.06 | 62.27 |
OpenAI/text-embedding-3-large | - | 73.97 | 74.48 | 82.52 | 77.58 | 93.58 | 53.32 | 62.35 |
また、下記の記事の測定においても、30Mで日本語検定でOpenAIのtext-emebdding-3-largeとほぼ互角、70Mなら英語とのクロスリンガルではintfloat/multilingual-e5-largeと互角、日本語性能だとme5-large,te3-largeをともに上回っています。
37Mというパラメータ数はfull precision(32bit)で150MB弱、8bitまで落とせば37MBで動きます。もはやChromeのタブよりも少ないです。当然ながらモデルサイズが小さい=高速でもあるので、CPUでも爆速です。検索対象を日本語のテキストに限定できるなら、ruri-v3以外を選ぶ理由が思いつかないです(API経由とマルチモーダルに未対応なくらい?)。
ruriは現状リリースされているのがtransformers想定のモデルのみでggufモデルはありません(そもそもggufがmodernBERTに対応していないかも)。量子化モデルも今までほとんど存在していませんでした(そもそも量子化するまでもなく動くだろうけども)。
というわけで、今回の記事執筆を機にONNXモデルを作成させていただきました。
transformer.jsにも対応させたので、ブラウザ/Node.js/bunなどサーバーでもクライアントでも好きな場所で試してみてください。もちろんONNXなのでsentencepieceなどを駆使すればc++でも動かせると思います。コード例
拙作のveqliteを使っています。
コード例
導入:
npm add veqlite @huggingface/transformers
コード:
import { VeqliteDB, HFLocalEmbeddingModel } from "veqlite";
// Simple example of using veqlite
async function main() {
// Initialize the embedding model
const embeddingModel = await HFLocalEmbeddingModel.init(
"sirasagi62/ruri-v3-30m-ONNX",
256,
"q8"
);
// Create RAG database instance
const rag = new VeqliteDB(embeddingModel, {
// Use in-memory database
dbPath: ":memory:",
embeddingDim: 256
});
// Add some documents
await rag.insertChunk({
content: "TypeScriptは型があるJavaScriptのスーパーセットです。",
filepath: "typescript-intro"
});
await rag.insertChunk({
content: "名古屋大学は名古屋にある国立大学です。",
filepath: "rag-intro"
});
await rag.insertChunk({
content: "研一くんは東北大学のゆるキャラです。",
filepath: "rag-intro"
});
await rag.insertChunk({
content: "東北大学は仙台にある国立大学です。",
filepath: "rag-intro"
});
await rag.insertChunk({
content: "Veqliteはsqliteをvector-dbとして扱えるようにするTypescriptライブラリです。",
filepath: "veqlite-intro"
});
const query = "What is TypeScript?"
console.log(`Query: ${query}`)
// Query the system
const results = await rag.searchSimilar(query);
results.forEach(r => {
console.log(`${r.content}: ${r.distance}`)
})
// Close the database
rag.close();
}
main().catch(console.error);
出力結果(0に近いほど類似度が高い):
Query: 東北大学とはなんですか?
東北大学は仙台にある国立大学です。: 0.042655885219573975
研一くんは東北大学のゆるキャラです。: 0.11519418656826019
名古屋大学は名古屋にある国立大学です。: 0.15166956186294556
Veqliteはsqliteをvector-dbとして扱えるようにするTypescriptライブラリです。: 0.19668039679527283
TypeScriptは型があるJavaScriptのスーパーセットです。: 0.21806864440441132
granite-embedding-107m-multilingual
ruriと並んで過小評価されていると個人的に感じているモデルです。このモデルの凄さはコードが正確に扱える、ということです。IBMのgraniteは早期からcode特化モデルをリリースするなどコーディングに力を入れているようです。そして、このgranite-embedding-107m-multilingualにはオープンウェイトのembeddingモデルとしてほとんど唯一無二ともいえる特徴を有しています。それが日本語でのコード検索が可能であるという点です。
一般に、embeddingモデルはコード関連のデータセットで学習させることが少ないため、コード検索となると性能を途端に発揮できなくことがほとんどです。一部でコード検索に特化したモデルも作成されてはいるのですが、英語でしか学習されておらず、日本語での性能というのは期待出来ません。ところが、このgranite-embeddingは日本語性能が高く、コードでも学習されているため、現状、ほぼ唯一の日本語でコード検索を行えるオープンウェイトモデルとなっています。
こちらの記事でいくつかのモデルと比較したところ、jina-embeddings-v2-base-code、nomic-embed-text-v1.5といったコード特化のモデルやメジャーな多言語embeddingモデルであるmultilingual-e5を上回る性能で日本語でコード検索を行えました。パラメータ数も107Mであり、地味にmulitilingual-e5を下回ります。多言語+コードというタスクであれば、検討に値すると思います。
granite-embeddingについてもtransformer.js対応のONNXがほとんど見られなかったため、作成しました。コード検索を日本語で試す際にはぜひご利用ください。
コード例
先ほどと同じくveqliteと@huggingface/transformersを使います。
コード例と実行結果
import { VeqliteDB, HFLocalEmbeddingModel } from "veqlite";
// Simple example of using veqlite
async function main() {
// Initialize the embedding model
const embeddingModel = await HFLocalEmbeddingModel.init(
"sirasagi62/granite-embedding-107m-multilingual-ONNX",
384,
"q8"
);
// Create RAG database instance
const rag = new VeqliteDB(embeddingModel, {
// Use in-memory database
embeddingDim: 384,
dbPath: ":memory:"
});
// Add some documents
await rag.insertChunk({
content: "TypeScript is a typed superset of JavaScript",
filepath: "typescript-intro"
});
await rag.insertChunk({
content: "RAG stands for Retrieval Augmented Generation",
filepath: "rag-intro"
});
await rag.insertChunk({
content: "Minirag is a simple RAG implementation in TypeScript",
filepath: "veqlite-intro"
});
const query = "What is RAG?"
console.log(`Query: ${query}`)
// Query the system
const results = await rag.searchSimilar(query);
results.forEach(r => {
console.log(`${r.content}: ${r.distance}`)
})
// Close the database
rag.close();
}
main().catch(console.error);
まとめ
以上のように、現在では日本語に対応したembeddingモデルで魅力的なモデルが存在しています。embeddingの生成はテキスト生成よりも計算負荷が少ないため、ローカルでも気軽に動かすことができます。特にruri-v3は30m/70mというPCで余裕で動くサイズでありながら性能でOpenAIのモデルに匹敵し、十分な競争力を持ちます。RAGなどを構築する際はぜひ、これらのローカルモデルも検討してみてください。
Discussion