テキストエンベディングとは?
テキストエンベディングとは、文章をコンピュータが処理しやすい数値ベクトル(N次元配列)で表現することを指します。
例えば、「昨日の夜ご飯はラーメンを食べた」と「近所のラーメン屋が新しくオープンした」という文章は、どちらもラーメンについて書かれているので、テキストエンベディングを用いると、これらの文章は意味的に近いと判断されます。
人間は、文章を読んで意味を理解できますが、コンピュータはそのままでは理解することができません。そこで、テキストエンベディング技術を用いて、文章を数値ベクトルに変換することで、コンピュータによる計算や分析を可能にします。
テキストエンベディングモデルは、word2vecやBERT、GPTなど様々な種類が存在し、それぞれ異なる特徴を持っています。これらのモデルに共通して言えることは、エンベディング処理によって、比較対象の次元数を統一できる点です。次元数が揃うことで、比較対象の類似度を容易に計算できるようになります。類似度の計算には、コサイン類似度などが一般的に用いられます。コサイン類似度とは、2つのベクトルの角度に基づいて類似度を計算する方法です。類似度として-1から1の間の数値が出力され、1に近いほど類似度が高いと言えます。
また、テキストエンベディングによって高次元の文章データが低次元のベクトルに変換される事で、計算コストが下がる事や、高次元データを可視化して分析する事が容易になるメリットもあります。
ブログ更新時の差分チェック
このブログはGitHub Actionsで入稿データ以外の資材をS3にアップしています。また、前回のビルドから記事数・タグ数・カテゴリ数に差分が生じた場合にCodebuildのビルド(next build
)が開始されるようにしています。
差分チェック時に参照する実際のjsonは以下です。
{
"lastBuildDate": "2025-03-09T00:00:00.000Z",
"totalPublished": {
"articles": 10,
"categories": 4,
"tags": 7,
}
}
記事数・カテゴリ数・タグ数はmicrocms-js-sdk
で取得します。記事数を取得するサンプルコードは以下です。
import { createClient } from "microcms-js-sdk";
export const client = createClient({
apiKey: "API_KEY",
serviceDomain: "SERVICE_DOMAIN",
});
export const fetchArticlesCount = async (fields: string, filters: string): Promise<number> => {
const data = await client.get<{
totalCount: number;
}>({
endpoint: "articles",
queries: {
fields: fields,
filters: filters,
},
});
return data.totalCount;
};
テキストエンベディング・コサイン類似度の計算
記事の関連性を正しく評価するために、buildSpec.ymlに、全記事のテキストエンベディングおよびコサイン類似度の計算を行うプログラムを実行するコマンドを記載します。
テキストエンベディングにはGemini APIのtext-embedding-004モデルを利用しました(ドキュメント)。
サンプルコードは以下です。
import { GenerativeModel, GoogleGenerativeAI } from "@google/generative-ai";
export async function main() {
const genAI = new GoogleGenerativeAI("API_KEY");
const model = genAI.getGenerativeModel({ model: "text-embedding-004"});
vecA = (await model.embedContent("昨日の夜ご飯はラーメンを食べた")).embedding.values;
vecB = (await model.embedContent("近所のラーメン屋が新しくオープンした")).embedding.values;
// コサイン類似度を計算
const score = cosineSimilarity(vecA, vecB);
// コサイン類似度を計算する関数
function cosineSimilarity(vectorA: number[], vectorB: number[]): number {
const dotProduct = vectorA.reduce((sum, a, idx) => sum + a * vectorB[idx], 0);
const magnitudeA = Math.sqrt(vectorA.reduce((sum, a) => sum + a * a, 0));
const magnitudeB = Math.sqrt(vectorB.reduce((sum, b) => sum + b * b, 0));
return dotProduct / (magnitudeA * magnitudeB);
}
}
if(import.meta.url == `file://${process.argv[1]}`) {
main()
.then(() => {
console.log("success");
process.exit(0);
})
.catch((err) =>
console.error(err);
process.exit(1);
});
}
実際のコードでは、全ての記事の組み合わせについて、コサイン類似度を計算しました。
- nCr = n! / (r! * (n-r)!)
n: 総記事数
r: 2
またモデルに与える文章ですが、microCMSのAPIはリッチテキストで出力されるためHTMLタグが含まれてしまっているので、精度低下を防ぐためにも省いておきます(日本語はアルファベットに比べてもトークン数が多いので、1リクエストにおけるトークン数を節約することは重要です)。
感想
類似度の高い記事を上位3件表示する機能を実装してみましたが、いくつかの課題が見えてきました。
まず、総記事数が少ない場合、類似度が低い記事でも上位に表示されてしまう点です。閾値を設けて足切りすることも考えられますが、どの程度のスコアを類似度が高いと判断すべきか、明確な基準を見つけるのが難しいと感じています。
また、記事全文をモデルに読み込ませるアプローチが最適なのか、別の方法を検討する必要があるのか悩ましいという点です。
このように、いくつかの課題は残りましたが、Gemini APIを利用することで、数時間で実装を完了できたのは大きな収穫でした。スムーズに開発を進めることができ、非常に楽しい週末を過ごせました!