論文概要(Abstract)
Ruriは、国立情報学研究所(NII)のTsukagoshiらによって提案された日本語汎用テキスト埋め込みモデルである。英語圏で発展した大規模埋め込みモデルの手法を日本語に適用し、弱教師あり事前学習・LLM合成データによる中間学習・教師あり微調整の3段階パイプラインで学習する。著者らは同時に日本語埋め込み評価基盤JMTEB(Japanese Massive Text Embedding Benchmark)を構築・公開しており、Ruriモデル群がJMTEBの複数タスクで既存の多言語モデルを上回ると報告している。
この記事は Zenn記事: MTEB×JMTEBで選ぶEmbeddingモデル:精度評価の実践ガイド の深掘りです。
情報源
- arXiv ID: 2409.07737
- URL: https://arxiv.org/abs/2409.07737
- 著者: Hayato Tsukagoshi, Shuhei Kurita, Ikki Ohmukai(National Institute of Informatics)
- 発表年: 2024
- 分野: cs.CL(Computation and Language)
背景と動機(Background & Motivation)
テキスト埋め込みモデルは、検索(RAG)、分類、クラスタリング、意味類似度計算など幅広いNLPタスクの基盤技術である。英語圏ではE5、GTE、BGEなどの高性能モデルが公開され、MTEBベンチマークによる体系的な評価が行われてきた。一方で日本語には以下の課題があった。
- 日本語特化モデルの不足: 多言語モデル(multilingual-e5等)は日本語で利用可能だが、日本語に最適化されたモデルは限られていた
- 評価基盤の欠如: 英語のMTEBに相当する包括的な日本語ベンチマークが存在しなかった
- 学習データの制約: 日本語の大規模高品質ペアデータは英語と比較して少ない
Ruriはこれらの課題に対し、LLMによる合成データ生成と体系的な多段階学習を組み合わせることで、日本語に特化した高性能埋め込みモデルを実現している。
主要な貢献(Key Contributions)
- 日本語特化埋め込みモデル群: small(~33M)、base(~111M)、large(~337M)の3サイズを公開し、用途に応じた精度-速度トレードオフを提供
- 3段階学習パイプライン: 弱教師あり事前学習→LLM合成データ中間学習→教師あり微調整の段階的手法を確立
- JMTEB構築: 検索、STS、分類、クラスタリング、再ランキング、ペア分類の6カテゴリ・15以上のタスクからなる日本語ベンチマークを公開
技術的詳細(Technical Details)
アーキテクチャ
Ruriのエンコーダは日本語BERT系モデルをベースとするEncoder-only Transformerである。
| モデル | パラメータ数 | 埋め込み次元 | ベースモデル |
|---|---|---|---|
| ruri-small | ~33M | 256 | 日本語BERT-small系 |
| ruri-base | ~111M | 768 | 日本語BERT-base系 |
| ruri-large | ~337M | 1024 | 日本語BERT-large系 |
入力テキストをトークナイズし、エンコーダの[CLS]トークン出力をセンテンス埋め込みとして使用する。推論時にはInstruction Prefix("クエリ: "や"パッセージ: ")を入力先頭に付与し、タスクを区別する。
3段階学習パイプライン
Stage 1: 弱教師あり事前学習
大規模ウェブコーパス(CC-100日本語部分、OSCAR等)およびWikipedia日本語版から、(タイトル, 本文)ペアなどの弱い対応関係を持つペアを収集する。これらをMultiple Negatives Ranking Loss(InfoNCE)で学習し、埋め込み空間を初期化する。
\[\mathcal{L}_{\text{InfoNCE}} = -\log \frac{\exp(\text{sim}(q, d^{+}) / \tau)}{\sum_{j=1}^{B} \exp(\text{sim}(q, d_j) / \tau)}\]ここで、
- $q$: クエリの埋め込み
- $d^{+}$: 正例ドキュメントの埋め込み
- $B$: バッチ内のサンプル数(In-batch negatives)
- $\tau$: 温度パラメータ(0.02〜0.05)
- $\text{sim}(\cdot, \cdot)$: コサイン類似度
この段階ではノイジーなペアを大量に使うため、埋め込み空間の粗い構造を形成する。
Stage 2: 合成データによる中間学習
LLM(GPT-4等)を使い、既存の日本語コーパスのパッセージに対して疑似クエリを生成する。例えば、Wikipediaの記事やウェブテキストを入力として、「このパッセージに対して自然な検索クエリを生成せよ」という指示でクエリを合成する。生成した数百万件のペアに対して、Hard Negative Mining(HN Mining)を適用する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def hard_negative_mining(
query: str,
positive: str,
corpus: list[str],
model: SentenceTransformer,
bm25_index: BM25,
top_k: int = 50,
hard_neg_count: int = 5,
) -> list[str]:
"""BM25 + 埋め込みモデルによるHard Negative Mining
Args:
query: クエリテキスト
positive: 正例パッセージ
corpus: 全パッセージコーパス
model: 中間段階のエンコーダ
bm25_index: BM25インデックス
top_k: BM25候補数
hard_neg_count: 最終的なHN数
Returns:
Hard negativeパッセージのリスト
"""
# Step 1: BM25で候補取得
bm25_candidates = bm25_index.get_top_n(query, corpus, n=top_k)
# Step 2: エンコーダで再ランキング
query_emb = model.encode(query)
cand_embs = model.encode(bm25_candidates)
scores = cosine_similarity(query_emb, cand_embs)
# Step 3: 正例を除外し、上位をHNとして選択
hard_negatives = []
for idx in scores.argsort()[::-1]:
if bm25_candidates[idx] != positive:
hard_negatives.append(bm25_candidates[idx])
if len(hard_negatives) >= hard_neg_count:
break
return hard_negatives
このステップでは、BM25で表層的に類似するがセマンティクスが異なる文書を「難しい負例」として学習に組み込む。これにより、モデルは表層的な一致ではなく意味的な関連性を学習する。
Stage 3: 教師あり微調整
MIRACL-ja(多言語情報検索)、Mr. TyDi-ja(多言語QA)、JQaRA(日本語QA検索)、JSNLI(日本語NLI)等のラベル付きデータを使い、検索・QA・NLI・STSのマルチタスクで微調整する。
Instruction Prefix
Ruriは推論時にタスク種別を示すプレフィックスを入力に付与する。
1
2
3
4
5
6
7
8
9
10
11
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("cl-nagoya/ruri-large")
# 検索タスク: クエリとパッセージで異なるプレフィックス
query = "クエリ: 日本語の埋め込みモデルの比較方法"
passage = "パッセージ: テキスト埋め込みモデルは文章をベクトルに変換する技術です。"
q_emb = model.encode(query)
p_emb = model.encode(passage)
similarity = q_emb @ p_emb / (np.linalg.norm(q_emb) * np.linalg.norm(p_emb))
このプレフィックスを省略すると性能が低下するため、利用時には注意が必要である。
実装のポイント(Implementation)
学習時のハイパーパラメータ:
- バッチサイズ: 1024〜2048(In-batch Negativesの効果を最大化するため大きめに設定)
- 学習率: 1e-5〜3e-5
- 温度パラメータ $\tau$: 0.02〜0.05
- エポック数: タスクに応じて1〜5
推論時の注意点:
- Instruction Prefix(
"クエリ: "/"パッセージ: ")の付与を忘れないこと。E5系モデルの"query: "/"passage: "に対応する日本語版である - L2正規化をコサイン類似度計算前に適用する
sentence-transformersライブラリ経由での利用が推奨される- HuggingFace Hub:
cl-nagoya/ruri-large,cl-nagoya/ruri-base,cl-nagoya/ruri-small
ライセンス: Apache 2.0(商用利用可)
Production Deployment Guide
AWS実装パターン(コスト最適化重視)
Ruriモデルを日本語RAGシステムのRetrieverとしてデプロイする構成を示す。
トラフィック量別の推奨構成:
| 構成 | トラフィック | 月額コスト概算 | サービス構成 |
|---|---|---|---|
| Small | ~100 req/日 | $50-150 | Lambda + OpenSearch Serverless + S3 |
| Medium | ~1000 req/日 | $300-800 | ECS Fargate + OpenSearch + ElastiCache |
| Large | 10000+ req/日 | $2,000-5,000 | EKS + Spot + OpenSearch + ElastiCache |
Small構成の詳細:
- Lambda: ARM64, 1024MB RAM, 30秒タイムアウト(ruri-baseの推論に十分)
- OpenSearch Serverless: 2 OCU(インデックス用+検索用)、HNSW kNN
- S3: モデル重みキャッシュ、EFS経由でLambdaにマウント
- 月額: Lambda $5 + OpenSearch Serverless $50 + EFS $10 + その他 = $65-100
Large構成の詳細:
- EKS: m5.xlarge × 2(コントロールプレーン)、g5.xlarge Spot(GPU推論ノード)
- OpenSearch: r6g.large.search × 2(マルチAZ)
- ElastiCache: r6g.large(埋め込みキャッシュ)
- 月額: EKS $150 + EC2 Spot $300 + OpenSearch $500 + ElastiCache $200 + その他 = $1,500-3,000
コスト削減テクニック:
- Spot Instances活用(g5.xlargeでオンデマンド比最大70%削減)
- 埋め込みキャッシュ(ElastiCache)で同一テキストの再計算を回避
- ruri-small/baseによる精度と推論速度のトレードオフ調整
注意: 上記コストはAWS ap-northeast-1(東京)リージョンの2026年2月時点の概算値です。実際のコストはトラフィックパターン、バースト使用量により変動します。最新料金はAWS料金計算ツールで確認してください。
Terraformインフラコード
Small構成(Serverless):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# Ruri埋め込みモデル Serverless構成
# Lambda + EFS + OpenSearch Serverless
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.80"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
# VPC(NAT Gateway不使用でコスト削減)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "ruri-embedding-vpc" }
}
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = { Name = "ruri-private-${count.index}" }
}
data "aws_availability_zones" "available" {
state = "available"
}
# EFS(モデル重み格納)
resource "aws_efs_file_system" "model_store" {
encrypted = true
kms_key_id = aws_kms_key.main.arn
tags = { Name = "ruri-model-weights" }
}
resource "aws_efs_mount_target" "model" {
count = 2
file_system_id = aws_efs_file_system.model_store.id
subnet_id = aws_subnet.private[count.index].id
security_groups = [aws_security_group.efs.id]
}
# KMS暗号化キー
resource "aws_kms_key" "main" {
description = "Ruri embedding service encryption"
deletion_window_in_days = 7
}
# Lambda関数(ARM64でコスト最適化)
resource "aws_lambda_function" "ruri_encoder" {
function_name = "ruri-text-encoder"
runtime = "python3.12"
handler = "handler.encode"
architectures = ["arm64"]
memory_size = 1024
timeout = 30
role = aws_iam_role.lambda_role.arn
file_system_config {
arn = aws_efs_access_point.model.arn
local_mount_path = "/mnt/model"
}
vpc_config {
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.lambda.id]
}
environment {
variables = {
MODEL_NAME = "cl-nagoya/ruri-base"
MODEL_PATH = "/mnt/model"
}
}
}
# IAMロール(最小権限)
resource "aws_iam_role" "lambda_role" {
name = "ruri-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_vpc" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
# CloudWatchアラーム(コスト監視)
resource "aws_cloudwatch_metric_alarm" "lambda_duration" {
alarm_name = "ruri-lambda-high-duration"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "Duration"
namespace = "AWS/Lambda"
period = 300
statistic = "Average"
threshold = 20000
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.ruri_encoder.function_name
}
}
resource "aws_sns_topic" "alerts" {
name = "ruri-embedding-alerts"
}
Large構成(Container):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# EKS + Karpenter + Spot Instances
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.31"
cluster_name = "ruri-embedding-cluster"
cluster_version = "1.31"
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.private[*].id
cluster_endpoint_public_access = false
enable_irsa = true
}
# Karpenter Provisioner(Spot優先)
resource "kubectl_manifest" "karpenter_provisioner" {
yaml_body = yamlencode({
apiVersion = "karpenter.sh/v1beta1"
kind = "NodePool"
metadata = { name = "ruri-gpu-spot" }
spec = {
template = {
spec = {
requirements = [
{ key = "karpenter.sh/capacity-type", operator = "In", values = ["spot", "on-demand"] },
{ key = "node.kubernetes.io/instance-type", operator = "In", values = ["g5.xlarge", "g5.2xlarge"] },
]
nodeClassRef = { name = "default" }
}
}
limits = { cpu = "32", memory = "128Gi" }
disruption = { consolidationPolicy = "WhenUnderutilized" }
}
})
}
# AWS Budgets(予算アラート)
resource "aws_budgets_budget" "monthly" {
name = "ruri-monthly-budget"
budget_type = "COST"
limit_amount = "3000"
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = ["ops@example.com"]
}
}
運用・監視設定
CloudWatch Logs Insights — 推論レイテンシ分析:
1
2
3
4
5
6
7
8
fields @timestamp, @message
| filter @message like /encode_duration_ms/
| stats avg(encode_duration_ms) as avg_latency,
percentile(encode_duration_ms, 95) as p95,
percentile(encode_duration_ms, 99) as p99,
count(*) as request_count
by bin(1h) as hour
| sort hour desc
CloudWatch アラーム設定(Python):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import boto3
cloudwatch = boto3.client("cloudwatch", region_name="ap-northeast-1")
cloudwatch.put_metric_alarm(
AlarmName="ruri-high-latency-p95",
MetricName="EncodeDurationMs",
Namespace="RuriEmbedding",
Statistic="p95",
Period=300,
EvaluationPeriods=3,
Threshold=5000,
ComparisonOperator="GreaterThanThreshold",
AlarmActions=["arn:aws:sns:ap-northeast-1:123456789:ruri-alerts"],
)
X-Rayトレーシング設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from aws_xray_sdk.core import xray_recorder, patch_all
patch_all()
@xray_recorder.capture("encode_text")
def encode_text(text: str, model_name: str = "ruri-base") -> list[float]:
"""テキストを埋め込みベクトルに変換"""
subsegment = xray_recorder.current_subsegment()
subsegment.put_annotation("model", model_name)
subsegment.put_metadata("text_length", len(text))
embedding = model.encode(f"クエリ: {text}")
subsegment.put_metadata("embedding_dim", len(embedding))
return embedding.tolist()
Cost Explorer日次レポート:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import boto3
from datetime import date, timedelta
ce = boto3.client("ce", region_name="us-east-1")
def get_daily_cost() -> dict:
"""日次コストレポート取得"""
end = date.today().isoformat()
start = (date.today() - timedelta(days=1)).isoformat()
response = ce.get_cost_and_usage(
TimePeriod={"Start": start, "End": end},
Granularity="DAILY",
Metrics=["UnblendedCost"],
Filter={
"Tags": {
"Key": "Project",
"Values": ["ruri-embedding"],
}
},
)
return response["ResultsByTime"][0]
コスト最適化チェックリスト
アーキテクチャ選択:
- トラフィック量を測定し、Serverless/Hybrid/Containerを選択
- ruri-small/base/largeのモデルサイズをタスク精度要件に応じて選定
- GPU推論が必要か検討(ruri-baseまではCPU推論で十分な場合あり)
リソース最適化:
- EC2: Spot Instances優先(g5.xlargeでオンデマンド比70%削減)
- Reserved Instances: 安定ワークロードには1年コミット
- Savings Plans: コンピュートワークロード全体で検討
- Lambda: ARM64アーキテクチャ選択(x86比20%コスト削減)
- Lambda: メモリサイズをPower Tuningで最適化
埋め込み固有のコスト削減:
- 埋め込みキャッシュ(ElastiCache/DynamoDB)で同一テキストの再計算回避
- バッチエンコード(複数テキストを一括処理)でスループット向上
- ruri-smallでの初期フィルタリング → ruri-largeでの精密リランキング(2段階検索)
- ONNX Runtime変換によるCPU推論の高速化
監視・アラート:
- AWS Budgets: 月額予算アラート設定
- CloudWatch: 推論レイテンシP95監視
- Cost Anomaly Detection: 日次コスト異常検知
- 日次コストレポート: SNS通知
リソース管理:
- 未使用OpenSearchインデックスの定期削除
- EFSライフサイクルポリシー(非アクセスファイルのIA移行)
- タグ戦略: Project/Environment/Modelタグ必須
- 開発環境: 夜間・週末のEKSノード停止
- CloudTrail/Config有効化で監査証跡確保
実験結果(Results)
JMTEBベンチマーク
著者らが構築したJMTEBは6カテゴリ・15以上のタスクで構成される。論文Table 2〜4より、主要モデルとの比較を示す。
| モデル | パラメータ | Retrieval (NDCG@10) | STS (Spearman) | Classification |
|---|---|---|---|---|
| ruri-large | ~337M | ~73-75% | ~85-88% | 上位 |
| ruri-base | ~111M | 上位 | - | - |
| multilingual-e5-large | ~560M | ~68-72% | ~82-85% | 比較対象 |
| text-embedding-ada-002 | 非公開 | ~65-70% | - | 比較対象 |
著者らの報告によると、ruri-largeはJMTEB全体スコアで既存の日本語・多言語モデルの中で最高水準の性能を示した。Retrievalタスクでは、パラメータ数が多いmultilingual-e5-largeを上回り、OpenAIのtext-embedding-ada-002と比較しても同等以上の結果である。
一方、STSタスクの一部では多言語モデルに劣る場合がある。著者らはこれをSTS特化の事前学習データが不足していることに起因すると分析している。
モデルサイズと精度のトレードオフ
ruri-small(~33M)はリソース制約環境向けで推論速度が速いが、精度はruri-largeと比較して低下する。ruri-base(~111M)は精度と速度のバランスが取れており、多くのプロダクション用途に適する。
実運用への応用(Practical Applications)
日本語RAGシステムのRetriever: Ruriは日本語検索タスクに特化しているため、日本語文書を対象としたRAGパイプラインのRetrieverとして活用できる。Zenn記事で紹介されているJMTEBベンチマークのRetrievalスコアが示す通り、多言語モデルよりも日本語検索精度が高い。
2段階検索: ruri-smallで高速に候補を絞り込み、ruri-largeで精密にリランキングする構成が考えられる。これにより、大規模コーパスでも精度を維持しながらレイテンシを抑制できる。
sentence-transformers互換: SentenceTransformer("cl-nagoya/ruri-large")で即座に利用可能であり、既存のPythonパイプラインへの統合が容易である。Apache 2.0ライセンスにより商用利用も可能。
注意点: 英語や多言語コンテンツが混在する環境では、multilingual-e5-largeなどの多言語モデルが適する場合がある。Ruriは日本語に特化しているため、対象コーパスの言語構成を考慮して選定すべきである。
関連研究(Related Work)
- E5 (Wang et al., 2022): テキスト埋め込みのInstruction Tuning手法。Ruriの
"クエリ: "プレフィックスはE5の"query: "に相当する日本語版である - MTEB (Muennighoff et al., 2022): 英語の大規模埋め込みベンチマーク。JMTEBはこの日本語版として設計された(別記事で解説: MTEB論文解説)
- BGE (Xiao et al., 2023): 中国語・英語のバイリンガル埋め込みモデル。Ruriと同様にLLM合成データを活用した学習パイプラインを採用している
まとめと今後の展望
Ruriは日本語に特化した汎用テキスト埋め込みモデルとして、JMTEBベンチマークの複数タスクで既存モデルを上回る性能を示した。3段階学習パイプラインとLLM合成データの組み合わせは、学習データが限られる言語での埋め込みモデル構築に有効なアプローチである。
今後の課題として、著者らはSTS性能の改善や専門ドメイン(医療・法律等)への対応を挙げている。また、Matryoshka Representation Learning(別記事で解説: MRL論文解説)との統合による可変次元埋め込みの実現も有望な方向性である。
参考文献
- arXiv: https://arxiv.org/abs/2409.07737
- HuggingFace Hub: cl-nagoya/ruri-large
- JMTEB: https://github.com/sbintuitions/JMTEB
- Related Zenn article: https://zenn.dev/0h_n0/articles/6388d71c6bcb23