LLMとVectorDBで幸せになれるこの時代に、私は、ElasticsearchでSPLADEをしたいのです。
こんにちは。
今年の8月にElasticsearch 1.5.0がリリースされました(リリースノート)。
このアップデートでは、sparse_vector
クエリが追加され、疎ベクトルを使ってsparse_vector
フィールドに対してクエリできるようになりました。
元々はtext_expansion
とweighted_tokens
というクエリがあったようなのですが、sparse_vector
はこれらを代替するものであり、これらは同時に非推奨になったようです。
今回はこの機能を使って、SPLADE[1]というモダンな検索モデルをElasticsearch上で使ってみます。
実験に使ったコードも公開していますので、適宜ご覧ください:https://github.com/argonism/splade-es
SPLADEとは?
SPLADEはBERTベースの検索モデルです。
ここでは簡単に説明して、最後にもう少し詳しい解説を付け加えています。
簡単に言うと、文書内の語の拡張と重みの計算
テキストをSPLADEに入力すると、単語とその重みの情報が疎ベクトルの形で得られます。
このとき、SPLADEはテキスト内の単語の重みをいい感じに計算してくれて、かつテキスト内にない単語の拡張もしてくれます。
例えば、「LLMとVectorDBで幸せになれるこの時代に」を入力した時に、
[1.72, 0.00, 0.00, 0.00, 0.00, 0.23, ..., 0.00, 0.33, 0.00]
のようなベクトルが得られます。
このベクトルの各次元は単語と紐づいていて、例えば1次元目は「LLM」という語と紐づいているとした時、この疎ベクトルでは「LLM」に1.72が重みとしてついていると解釈できます。
このとき、SPLADEでは元のテキストにない語についても、検索に必要な語であれば拡張してくれます。
次の例では、「AI」や「Dense」など、元のテキストにはなかったけど関連ありそうな語を拡張しています。
LLM | Vector | DB | 幸せ | 時代 | AI | Dense | この | なれる | ... | ささみ |
---|---|---|---|---|---|---|---|---|---|---|
1.7 | 1.5 | 1.3 | 0.8 | 0.75 | 0.5 | 0.45 | 0.3 | 0.1 | ... | 0.00 |
これを、検索にどう使うかと言うと、単純に文書とクエリをSPLADEを通して、それぞれ疎ベクトルにし、その内積をスコアとする形で文書のランク付けをします。
例えば、先ほどの例で、「LLMとVectorDBで幸せになれるこの時代に」が文書だったとします。
このとき、文書のベクトルとして
[1.72, 0.00, 0.00, 0.00, 0.00, 0.23, ..., 0.00, 0.33, 0.00]
が得られました。
クエリ「LLM 幸福」における上の文書のスコアを計算する時は、まずこのクエリをSPLADEに通して同様に疎ベクトルを得ます。
[0.00, 0.00, 0.00, 0.00, 0.00, 1.41, ..., 0.89, 0.10, 0.00]
そして得られた文書とクエリの疎ベクトルの内積を計算します。
[1.72, 0.00, 0.00, 0.00, 0.00, 0.23, ..., 0.00, 0.33, 0.00]
・
[0.00, 0.00, 0.00, 0.00, 0.00, 1.41, ..., 0.89, 0.10, 0.00]
= 0.3573
SPLADEでは、このようにして疎ベクトルの内積を使って文書のスコアを計算します。
SPLADE-Doc
SPLADEにはSPLADE-Docというバリエーションがあります。
これはSPLADEがわかれば至ってシンプルで、文書側だけSPLADEを通して、クエリ側はSPLADEを通さずにそのまま用いると言うものです。
クエリ時にSPLADEを通さないので、クエリのスループットが向上します。
検索性能については、これまでの研究でSPLADEよりは検索性能が劣ることがわかっているため、検索性能と速度のバランスをとったような手法になっています。
クエリ時にBERTを通す必要がないというのは、とても魅力的に見えます。
ElasticsearchでSPLADEをやってみる
SPLADEはテキストを疎ベクトルにエンコードしますが、Elasticsearch上ではsparse_vectorは単語をkey、重みをvaueとしたjsonのobjectとして表現されます。ただし、語彙数分全部載せるのではなく、0より大きい値を持った語だけ載せます
例えば、
LLM | Vector | DB | 幸せ | 時代 | AI | Dense | この | 醤油 | ... | ささみ |
---|---|---|---|---|---|---|---|---|---|---|
1.7 | 1.5 | 1.3 | 0.8 | 0.75 | 0.5 | 0.45 | 0.3 | 0.0 | ... | 0.00 |
というような疎ベクトルが得られた場合、
{
"LLM": 1.7,
"Vector": 1.5,
"DB": 1.3,
"幸せ": 0.8,
"時代": 0.75,
"AI": 0.5,
"Dense": 0.45,
"この": 0.3
}
このように表現されます。
このため、SPLADEモデルの出力を、python上では辞書型に変換します:
def dictionalize_model_outputs(
model_outputs: torch.Tensor, vocab_dict: dict[int, str]
) -> list[dict[str, float]]:
expand_terms_list: list[dict[str, float]] = []
for model_output in model_outputs:
indexes = torch.nonzero(model_output, as_tuple=False).squeeze()
expand_terms = {}
for idx in indexes:
weight = model_output[idx].item()
if weight > 0:
token = vocab_dict[int(idx)]
# Ignore dot because elasticsearch does not support it
if token == ".":
continue
expand_terms[token] = weight
expand_terms_list.append(expand_terms)
return expand_terms_list
class SpladeEncoder(object):
def __init__(self, encoder_path: str, device: str, verbose: bool = True) -> None:
self.splade = AutoModelForMaskedLM.from_pretrained(encoder_path)
self.tokenizer = AutoTokenizer.from_pretrained(encoder_path)
self.vocab_dict = {v: k for k, v in self.tokenizer.get_vocab().items()}
logger.debug("loading to device: %s", device)
self.device = device
self.splade.to(device)
def tokenize(self, texts: list[str]) -> list[list[str]]:
return [self.tokenizer.tokenize(text) for text in texts]
def _encode_texts(self, texts: list[str], batch_size: int) -> torch.Tensor:
outputs: list[torch.Tensor] = []
iterator = (
tqdm(
batched(texts, batch_size),
total=len(texts) // batch_size,
desc="Encoding texts",
)
if self.verbose
else batched(texts, batch_size)
)
for batch in iterator:
tokenized = self.tokenizer(
batch, return_tensors="pt", padding=True, truncation=True
).to(self.device)
# MLM output (input length x vocab size)
output = self.splade(**tokenized, return_dict=True).logits
# max-pooling against each token's vocab size vectors
# We handle this output as a expanded document
output, _ = torch.max(
torch.log(1 + torch.relu(output))
* tokenized["attention_mask"].unsqueeze(-1),
dim=1,
)
outputs.append(output)
return torch.cat(outputs)
def encode_texts(self, texts: list[str], batch_size: int = 32) -> torch.Tensor:
with torch.no_grad():
model_outputs = self._encode_texts(texts, batch_size=batch_size)
return model_outputs
def model_outputs_to_dict(
self, model_outputs: torch.Tensor
) -> list[dict[str, float]]:
return dictionalize_model_outputs(model_outputs, self.vocab_dict)
def encode_to_dict(
self, texts: list[str], batch_size: int = 32
) -> list[dict[str, float]]:
model_outputs = self.encode_texts(texts, batch_size=batch_size)
return self.model_outputs_to_dict(model_outputs)
SPLADEの使い方は、本家の実装(link)や、以下のaken12/splade-japanese-v3
の使用例が参考になるかと思います(link)。
今回の実験の実装の方もリンクを載せておきます:https://github.com/argonism/splade-es/blob/main/splade_es/tasks/splade.py
インデクシング
SPLADEを通して疎ベクトルを得た後に、疎ベクトルをElasticsearch(以下ES)に投げます。
まずは、次のように"type": "sparse_vector"
を指定したフィールドを用意します。
PUT my-index-000001
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "standard"
},
"sparse_vec_field": {
"type": "sparse_vector"
},
}
}
}
そして、ここに対して文書をインデックスします。
Elasticsearchに疎ベクトルをインデクシングするには、前述したような形で表現します。
POST my-index-000001/_doc
{
"title": "In this era where we can achieve happiness with LLMs and VectorDBs, I want to use Elasticsearch with SPLADE."
"sparse_vec_field": {"Elasticsearch": 1.55, "SPLADE": 2.9, "VectorDBs": 15}
}
これでインデックスできました。実際にインデックスされた文書を見てみると
{
"_index": "my-index-000001",
"_id": "TtVmppMBndQCrQ0GMNNS",
"_score": 1,
"_source": {
"title": "In this era where we can achieve happiness with LLMs and VectorDBs, I want to use Elasticsearch with SPLADE.",
"sparse_vec_field": {
"Elasticsearch": 1.55,
"SPLADE": 2.9,
"VectorDBs": 15
}
}
}
インデックスされたまんま、表示されました。
では次に、このインデックスに対して疎ベクトルで検索クエリを投げてみます。
クエリしてみる
疎ベクトルを使ってクエリするにはsparse_vector
クエリを使います。
フィールドのタイプと同じでややこしいですが、以下のようにして使います。
get /my-index-000001/_search
{
"query": {
"sparse_vector": {
"field": "sparse_vec_field",
"query_vector": { "SPLADE": 0.1, "happiness": 0.25 }
}
}
}
このようなクエリを投げると、次のような結果が返ってきます。
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.28984377,
"hits": [
{
"_index": "my-index-000002",
"_id": "TtVmppMBndQCrQ0GMNNS",
"_score": 0.28984377,
"_source": {
"title": "In this era where we can achieve happiness with LLMs and VectorDBs, I want to use Elasticsearch with SPLADE.",
"sparse_vec_field": {
"Elasticsearch": 1.55,
"SPLADE": 2.9,
"VectorDBs": 15
}
}
}
]
}
文書のスコアは0.2898...
となりました。
文書のスコアはクエリと文書それぞれの疎ベクトルもちゃんと内積になっているようです。
実際、リポジトリの方のdocsを見てみると、そのような記述がありました(link)。
非常にシンプルで、特に複雑なことは何もなかったですね。
ちなみに
sparse_vector
フィールドは複数のベクトルを持つことができるそうです。そのような場合には、各語の最大値のみがstoreされるとのこと。
ベンチマーク
ElasticsearchでSPLADEを動かしてみましたが、果たして本当にこれで正しくSPLADEをできているか確かめたいですね。
そのために、SPLADEの論文で評価に使われているデータセットを使って評価してみようと思います。
データセットにはBEIRの一部のデータセットを使います。
私用PCの性能の都合上大きいデータセットは一部省いていますが、あくまで動作確認レベルでの評価ですのでご容赦ください。
SPLADEにはオリジナル学習済みモデル(SPLADE-v3)を利用しました:https://huggingface.co/naver/splade-v3
v3は、検索モデルの訓練で良いとされる様々な手法(hard negativesや蒸留など)を取り入れて性能を向上させたモデルになっているもので、モデルの仕組みとしては無印と同じものです。
ではBEIRデータセットの一部での評価結果です。
上の行が今回のSPLADE w/ Elasticsearchで、下の(orginal)の数字はSPLADE-v3の論文から引っ張ってきたものです。
Metrics | scidocs | quora | touche | scifact | arguana | fiqa | trec-covid | nq | nfcorpus |
---|---|---|---|---|---|---|---|---|---|
ndcg@10 | 15.6 | 81.4 | 31.0 | 71.6 | 61.4 | 37.6 | 73.3 | 51.2 | 36.3 |
ndcg@10 (original) | 15.8 | 81.4 | 29.3 | 71.0 | 50.9 | 37.4 | 74.8 | 58.6 | 35.7 |
arguana 以外ではほぼ同等な結果になりました。
arguanaだけ、SPLADEの元論文と比較してかなり低くなっていますが、まだ原因がわかっていないです。
Xでarguanaでの評価時のポイントを教えてもらって、再評価し値を修正しました。逆に上がり過ぎてしまって困惑してますが、クエリを取り除いて評価した結果になります。
他のESクエリと組み合わせる
Elasticsearchを使ってSPLADEをやることの利点の一つとして、Elasticsearchのその他の機能を使えるということが挙げられます。
例えば、sparse_vector
クエリは、match
クエリなどのクエリと一緒に使うことができます。
そのため、「SPLADEでスコアづけをしながら、フレーズでマッチした文書を重みづける」というようなことも可能です。
SPLADEでは文書を疎ベクトルとして扱うため、語の位置や順番の情報は失われてしまいます。そこを、Elasticsearchの力で通常のテキストフィールドのインデクシングとmatch
クエリ等を組み合わせることで、語の隣接性を直接的にスコアに組み込むことができます。
「titleにクエリ語が入ってることは、やっぱり重視したい」というのは、実アプリケーションではよくあるのではないでしょうか。
次のクエリでは、sparse_vector
での検索した文書集合を、title
フィールドにhappiness
を含むような文書のスコアを2倍の重みをつけてランクづけします。
get /my-index-000001/_search
{
"query": {
"function_score": {
"query": {
"sparse_vector": {
"field": "sparse_vec_field",
"query_vector": { "SPLADE": 0.1, "happiness": 0.25 }
}
},
"functions": [
{
"filter": {
"multi_match": {
"query": "happiness",
"fields": ["title"]
}
},
"weight": 2
}
]
}
}
}
このように、SPLADEの検索結果をtitleブーストするというようなことも可能です。
kibanaでクエリを手軽に色々試せるのも、魅力の一つですね。
ちなみに、NFCorpusを使ってこの手法の評価もしてみましたが、わずかな性能の変化(nDCG@10で0.001くらい)がありましたが、SPLADEとBM25の差と比べると本当に僅かですね。
分析してみる
クエリごとのパフォーマンス分析
実際にクエリを見てみることで分かることも多いです。
ここでは、NFCorpus[2]の結果の中で、SPLADEでうまく行ったクエリ・うまくいかなかったクエリを見てみましょう。比較には、同じ疎ベクトルに基づいた検索モデルであるBM25を使います。
具体的には、
- BM25でうまくいかなかったけど、SPLADEでうまくいったクエリ
- BM25でうまくいったけど、SPLADEでうまくいかなかったクエリ
- どっちでもうまくいかなかったクエリ
を見てみます。
評価指標にはnDCG@10を用いています。
なお、ここで紹介するクエリの例は一部を抜粋したものです。
BM25としては、次のようなシンプルなmulti_match
クエリを使っています。
{
"query": {
"multi_match": {
"query": {{query}},
"fields": ["text", "title", "url"]
}
}
}
BM25でうまくいったけど、SPLADEでうまくいかなかったクエリ
BM25と比べて、SPLADEでうまく行ったクエリを見てみます。
Query | bm25 | splade |
---|---|---|
Harvard Physicians’ Study II | 0.61 | 0.09 |
The Actual Benefit of Diet vs. Drugs | 0.44 | 0.00 |
Diet and Cellulite | 0.37 | 0.00 |
Fish Fog | 0.29 | 0.00 |
medical ethics | 0.22 | 0.09 |
The Saturated Fat Studies: Buttering Up the Public | 0.19 | 0.00 |
mouth cancer | 0.11 | 0.00 |
一番上の例を見てみましょう。
クエリは「Harvard Physicians’ Study II」です。
この時、SPLADEでは次の文章が1位となっていました。
タイトル: A global survey of physicians' perceptions on cholesterol management: the From The Heart study.
本文:AIMS: Guidelines for cardiovascular disease (CVD) prevention cite high levels of low-density lipoprotein cholesterol (LDL-C) as a major risk factor and recommend ...(省略)
適合性:不明
そして、この文書は適合性ラベルが付いていないんですが、適合文書と見合わせてみるとおそらく適合ではなさそうに見えます。
拡張された語でマッチした語として research
や doctor
、doctors
、clinic
がありました。
さて、次にBM25で1位となっていた文書を見てみます。
タイトル:Multivitamins in the Prevention of Cancer in Men: The Physicians’ Health Study II Randomized Controlled Trial
本文:Context Multivitamin preparations are the most common dietary supplement, taken by at least one-third of all US adults.(以下略)
適合性:1
この文書は適合性ラベルが1(適合)です。
タイトルを見ると、「The Physicians’ Health Study II」とあるのがわかります。
クエリにも「Physicians’ Study II」とありましたね。そして、この論文のAuthor Affiliationsを見てみると「Harvard」の文字があります。
さらに、2位の方も見てみましょう。
タイトル :Multivitamins in the Prevention of Cardiovascular Disease in Men: The Physicians' Health Study II Randomized Controlled Trial
本文:Context Though multivitamins aim to prevent vitamin and mineral deficiency, there is a perception that multivitamins may prevent(以下略)
2位の方にも「The Physicians' Health Study II」とタイトルにあります。
というわけで、この「The Physicians' Health Study II」はおそらくシリーズというかプロジェクトみたいなものだと思われます。(関連ありそうなwebページ)
このクエリでは「Physicians’ Study II」がフレーズのようになっていて、これがそのまま出てくるような文書を探す方が有利だったわけですね。
特に、NFCorpusでは医療ドメインの文書が多いですから、Studyやphysicianはよく拡張されやすい語であったのかもしれません。その結果、余計に色んな文書とマッチしてしまいスコアを落とした可能性があります。
ちなみに、SPLADEがクエリをエンコードした時の重みは大きい順に
harvard | 2.59 |
ii | 2.56 |
study | 2.00 |
2 | 1.34 |
physician | 1.30 |
boston | 0.96 |
でした。
BM25でうまくいかなかったけど、SPLADEでうまくいったクエリ
次は、BM25ではうまくいかなかったクエリが、SPLADEではうまく行った例を見てみましょう。
Query | bm25 | splade |
---|---|---|
Too Much Iodine Can Be as Bad as Too Little | 0.00 | 0.77 |
low-carb diets | 0.09 | 0.55 |
What’s Driving America’s Obesity Problem? | 0.08 | 0.50 |
crib death | 0.00 | 0.47 |
Preventing Cataracts with Diet | 0.00 | 0.44 |
Meat & Multiple Myeloma | 0.00 | 0.41 |
Relieving Yourself of Excess Estrogen | 0.00 | 0.39 |
Apthous Ulcer Mystery Solved | 0.00 | 0.36 |
ここにはあまり出ていませんが、ざっと見て気づいたのは、質問ベースのクエリや自然分っぽいクエリが多いことです。やはり、質問系のクエリに対してはSPLADEの方が強いのかもしれません。
一番上のクエリ「Too Much Iodine Can Be as Bad as Too Little」について、SPLADEのケースを見てみましょう。
SPLADEで1位だった文書は次の文書です:
タイトル:Iodine-induced neonatal hypothyroidism secondary to maternal seaweed consumption: a common practice in some Asian cultures to promote breast milk s...
本文:Mild iodine deficiency is a recognised problem in Australia and New Zealand. However, iodine excess can cause hypothyroidism in some infants.(省略)
適合性:2
io, ##dineはクエリ文書ともスコアが高くなっていましたが、too はクエリではスコアは高くなっていますが、文書では0.638 と比較的低くなっていました。
クエリ側のエンコード結果は次の通りです:
io | ##dine | too | bad | dangerous | little | excessive | much | can | deficiency | thyroid | than | difference | dose | minimal | low | as | amount | high | mean | overdose | excess | toxic | harmful | effect | limit | very | you |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2.61 | 1.79 | 1.57 | 1.33 | 1.29 | 1.10 | 1.10 | 1.00 | 0.79 | 0.76 | 0.71 | 0.64 | 0.55 | 0.50 | 0.48 | 0.38 | 0.19 | 0.18 | 0.17 | 0.16 | 0.13 | 0.12 | 0.11 | 0.10 | 0.08 | 0.06 | 0.06 | 0.02 |
次にBM25での1位の文書を見てみましょう。
タイトル:Too much of too little: xylitol, an unusual trigger of a chronic metabolic hyperchloremic acidosis.
本文:Despite compelling statistics that show we could eliminate 80%of all heart disease and strokes,
適合性:0
「Too much」や「too little」に強く影響を受けてそうですね。
実際、スコアの内訳をkibanaで見てみると、スコアの約60% が、タイトルの「too」から来ていました。
この例は、SPLADEでの単語のweightの計算がうまく効いた例(もしくはBM25と言えるかもしれません。
どっちでもうまくいかなかったクエリ
最後に、どっちでもうまくいかなかったクエリを見てみます。
Query | bm25 | splade |
---|---|---|
Veggies vs. Cancer | 0.09 | 0.08 |
Barriers to Heart Disease Prevention | 0.05 | 0.10 |
Caloric Restriction vs. Plant-Based Diets | 0.08 | 0.07 |
EPIC Study | 0.06 | 0.06 |
halibut | 0.00 | 0.00 |
leeks | 0.00 | 0.00 |
Lindane | 0.00 | 0.00 |
lyme disease | 0.00 | 0.00 |
mesquite | 0.00 | 0.00 |
Mevacor | 0.00 | 0.00 |
myelopathy | 0.00 | 0.00 |
パッと見て、スコアが0のクエリは難しい単語が並んでいそうですね。
クエリ「mesquite」の例を見てみます。簡単にこの単語を調べてみると、アメリカの都市に「mesquite」があったり、「mesquite」という名前の植物があるようです。
SPLADEの方では、サブワードに分割されて、サブワードにヒットすることで関係なさそうな文書がヒットしていました。
BM25の方では何もヒットしていなかったので、これらの語を含むような文書はなかったんですね。
このクエリの適合文書を見ながら色々調べてみると、どうも植物のmesquiteは燻製に使われるそうで、それが食品に与える影響などを調べているようでした(知識がないのであくまで雰囲気ですが...)。
これは、spladeに追加学習すれば改善する余地がありそうなクエリ例ですね。
SPLADEがmesquiteをエンコードした時の結果は次のとおりになっていました。
##qui | ##te | me | ##s | clay | tribe | variety | rock | geology | mexico | crystal | lily | genus | indian | marsh | river | geography |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2.74 | 2.52 | 2.50 | 1.20 | 0.76 | 0.74 | 0.48 | 0.39 | 0.35 | 0.34 | 0.30 | 0.15 | 0.13 | 0.13 | 0.11 | 0.08 | 0.00 |
ここでfoodやsmokeが入っていたら、より高い性能を出せていたかもしれません。
まとめ
以上、ElasticsearchでSPLADEをやってみた記事でした。
SPLADE等の疎ベクトルベースの検索モデルの面白い点は、分析でやったように結果の解釈性が高いところです。
実際にどのような単語が重視されていたのか・拡張されたのかをみることができるのはとても良い長所だとおもいます。今回やったような分析も、kibanaを使えばやりやすいです。explain
を使えばスコアの内訳が見れますし、今回の分析でも多様しました(しかし、sparse_vectorクエリだとどの単語とマッチしたのかが重みから逆算するしかなくて面倒ですが)。
また、結果の制御がしやすいので、Elasticsearchの色んなクエリと組み合わせて微妙な調整を入れたり、既存のElasticesearchのクエリと組み合わせて使うことも簡単なのはやはり魅力的だと思います。
本当はSPLADE-Docでの結果を載せたり、今回BM25とやったような比較をSPLADE-Docでもやりたかったんですが、時間がないので、ここで一旦まとめてしまいます。
余談
8.15.0がリリースされた頃、僕はSPLADE-Docという検索手法を、Elasticsearchを使ってやる実験を個人的にしていました。8.15がリリースされるまではsparse_query
のような機能はありませんでした(rank_featureで頑張るとできないこともなかったらしいがクエリするの難しそう)が、疎ベクトルを使って検索することが難しかったので、SPLADEは諦めていました。一方で、SPLADE-Docであれば、クエリ時は通常の検索と同じようにやれば良いので、SPLADE-Docの推論結果をwhitespace tokenizerを使ってやるような形で行っていました。実際に、パフォーマンスもSPLADE-Docの論文と同等の検索性能が出ていて機能してはいたので、検索勉強会のLT等で発表でもしようかなと企んでいたところにこのアップデートが入ったので、この企みは消滅し、SPLADEをElasticsearchで使う企画に変貌しました。
SPLADEをもう少し詳しく話すと
SPLADEのベースとなっているBERTでは、語彙数次元のベクトルを使った事前学習が行われています。SPLADE等のBERTベースの疎ベクトルに基づく検索モデルは、このテキストを語彙数次元のベクトルに変換する仕組みを使っています。
元々、SPLADEより前からテキストから、BERT等のエンコーダを使って語彙数次元のベクトル生成して検索モデルはありました。
SPLADEが新しかったのは、疎ベクトルの学習にsparsityの正則化を入れたことです。
疎ベクトルの検索モデルの嬉しい点の一つとして、転置インデックスと相性が良いことが挙げられます。
転置インデックスは、ある単語を含むような文書を高速に探せるデータ構造であり、検索ではよく使われるデータ構造となっています。
転置インデックスには各単語とその単語が含まれている文書のリスト(ポスティングリスト)の情報があり、頻出する単語ほどその文書のリストは長くなります。
そして、文書のリストが長くなると、その単語での検索効率が悪くなってきます。極端な例として、全ての文書に「LLM」という単語で入っていた場合、文書リストは全ての文書の集合になってしまい、文書をうまく絞り込めず速度面で不利になります。
同様に、検索モデルによって文書が拡張されても、拡張された後の文書が巨大になってしまうと転置インデックスの文書リストも大きくなってしまい、転置インデックスの利点を活かせません。
SPLADEの前に提案されていたモデル(e.g. SparTerm)でもモデルの語彙数次元のベクトルを生成して、語の拡張と語の重みを計算することはしていました。しかし、つまり、語彙数次元のベクトルが、十分に疎になっていなかったことで、結果として文書が色んな語で拡張されてしまうことになり、上記のような文書リストが長くなってしまう問題を抱えていました。
この問題に対処するために、SPLADEは学習時に疎ベクトルが十分に疎になるような損失を入れています。
正確には、語の出現頻度はジップの法則として知られるように偏るものですから、効率の良い転置インデックスになるようにするためには、なるべく語の分布が散るようにしたいです。
文書間で同じ語について高い重みをつけていると、インデックスのバランスが偏ってしまいますので、
これを抑制するために、SPLADEでは同じ語についての重みの平均が小さくなるような損失を計算しています。
こうすることで、多くの文書が同じ語についての重みを大きくつけているようなケースで損失が大きくなります。
Reference
-
Formal, Thibault, Benjamin Piwowarski, and Stéphane Clinchant. "SPLADE: Sparse lexical and expansion model for first stage ranking." Proceedings of the 44th International ACM SIGIR Conference on Research and Development in Information Retrieval. 2021. ↩︎
-
Vera Boteva, Demian Gholipour, Artem Sokolov and Stefan Riezler
A Full-Text Learning to Rank Dataset for Medical Information Retrieval
Proceedings of the 38th European Conference on Information Retrieval (ECIR), Padova, Italy, 2016 ↩︎
Discussion