Strange results for cosine similarity / semantic search

#1
by karmiq - opened

I am trying to use the model in a semantic search scenario. In my experiments where embeddings are indexed into Elasticsearch and searched with a knn query, the results are mostly not relevant.

I have created an isolated demonstration which computes cosine distance between a query and a document:

import sentence_transformers

model = sentence_transformers.SentenceTransformer("Seznam/simcse-dist-mpnet-paracrawl-cs-en")

embeddings = [
  model.encode("Čím se zabývá fyzika?"),

  model.encode("Fyzika (z řeckého φυσικός (fysikos): přírodní, ze základu φύσις (fysis): příroda, přirozenost, archaicky též silozpyt) je exaktní vědní obor, který zkoumá zákonitosti přírodních jevů. Popisuje vlastnosti a projevy hmoty, antihmoty, vakua, přírodních sil, světla i neviditelného záření, tepla, zvuku atd. Vztahy mezi těmito objekty fyzika obvykle vyjadřuje matematickými prostředky. Mnoho poznatků fyziky je úspěšně aplikováno v praxi, což významně přispívá k rozvoji civilizace."),
]

sentence_transformers.util.cos_sim(embeddings[0], embeddings[1]).tolist()

# => [[0.12196816504001617]]

The first input is the query, the second input is a small passage from Wikipedia. The result is a really low similarity, 0.12.

When I try to compute similarity across a larger dataset, like here: karmiq/wikipedia-embeddings-cs-seznam-mpnet (expand "Use sentence_transformers.util.semantic_search"), the results are also mostly irrelevant. For the query Čím se zabývá fyzika?, I'm getting results like this:

# [0.58] Dynamika Fyzikální zákony [Newtonovy pohybové zákony]
# [0.53] Teorie množin [Ordinální číslo]
# [0.52] Fyzika částic Fyzikální jevy [Rozpad částice]
# [0.52] Druhy vlaků [Násled]
# [0.51] Zkratky [PKP]
# ...

Note that for eg. the intfloat/multilingual-e5-small I'm getting much better results for a corresponding dataset: karmiq/wikipedia-embeddings-cs-e5-small:

# [0.90] Fyzika částic ( též částicová fyzika ) je oblast fyziky, která se zabývá částicemi. V širším smyslu… [Fyzika částic]
# [0.89] Fyzika ( z řeckého φυσικός ( fysikos ): přírodní, ze základu φύσις ( fysis ): příroda, archaicky… [Fyzika]
# [0.89] Molekulová fyzika ( též molekulární fyzika ) je část fyziky, která zkoumá látky na úrovni atomů a… [Molekulová fyzika]
# [0.88] Jaderná fyzika ( též fyzika atomového jádra nebo nukleonika ) je část fyziky, která se zabývá… [Jaderná fyzika]
# [0.88] Atomová, molekulová a optická fyzika ( AMO ) se zabývá studiem interakcí mezi hmotou a hmotou,… [Atomová, molekulová a optická fyzika]
# ...

Do you have any idea what I might be doing wrong here? Do you have any pointers how to use the model for semantic search?

Seznam.cz org

Hi @karmiq ,
I am not very familiar with sentence_transformers library, but when I run the code using standard transformers library as in the How to use Section in https://huggingface.co/Seznam/dist-mpnet-paracrawl-cs-en such as:

import torch
from transformers import AutoModel, AutoTokenizer

model_name = "Seznam/simcse-dist-mpnet-paracrawl-cs-en"  # Hugging Face link
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

def compute_similarity(text1, text2):
    # Tokenize the input texts
    batch_dict = tokenizer([text1, text2], max_length=512, padding=True, truncation=True, return_tensors='pt')

    outputs = model(**batch_dict)
    embeddings = outputs.last_hidden_state[:, 0]  # Extract CLS token embeddings

    similarity = torch.nn.functional.cosine_similarity(embeddings[0], embeddings[1], dim=0)
    return similarity.item()

query = "Čím se zabývá fyzika?"
candidates = [
    "Fyzika (z řeckého φυσικός (fysikos): přírodní, ze základu φύσις (fysis): příroda, přirozenost, archaicky též silozpyt) je exaktní vědní obor, který zkoumá zákonitosti přírodních jevů. Popisuje vlastnosti a projevy hmoty, antihmoty, vakua, přírodních sil, světla i neviditelného záření, tepla, zvuku atd. Vztahy mezi těmito objekty fyzika obvykle vyjadřuje matematickými prostředky. Mnoho poznatků fyziky je úspěšně aplikováno v praxi, což významně přispívá k rozvoji civilizace.",
    "Dynamika Fyzikální zákony [Newtonovy pohybové zákony]",
    "Teorie množin [Ordinální číslo]",
    "Zkratky [PKP]"
]

for candidate in candidates:
    print(candidate)
    print(compute_similarity(query, candidate))

I get more reasonable results:

Fyzika (z řeckého φυσικός (fysikos): přírodní, ze základu φύσις (fysis): příroda, přirozenost, archaicky též silozpyt) je exaktní vědní obor, který zkoumá zákonitosti přírodních jevů. Popisuje vlastnosti a projevy hmoty, antihmoty, vakua, přírodních sil, světla i neviditelného záření, tepla, zvuku atd. Vztahy mezi těmito objekty fyzika obvykle vyjadřuje matematickými prostředky. Mnoho poznatků fyziky je úspěšně aplikováno v praxi, což významně přispívá k rozvoji civilizace.
0.6310943365097046
Dynamika Fyzikální zákony [Newtonovy pohybové zákony]
0.5915541052818298
Teorie množin [Ordinální číslo]
0.2819131910800934
Zkratky [PKP]
0.06636099517345428

One of the issues of using sentence_transformers might be that it loads tokenizer badly. Could you please check what is the output of the tokenizer that sentence_transformers use for:

text1 = "Čím se zabývá fyzika?"
text2 = "Fyzika (z řeckého φυσικός (fysikos): přírodní, ze základu φύσις (fysis): příroda, přirozenost, archaicky též silozpyt) je exaktní vědní obor, který zkoumá zákonitosti přírodních jevů. Popisuje vlastnosti a projevy hmoty, antihmoty, vakua, přírodních sil, světla i neviditelného záření, tepla, zvuku atd. Vztahy mezi těmito objekty fyzika obvykle vyjadřuje matematickými prostředky. Mnoho poznatků fyziky je úspěšně aplikováno v praxi, což významně přispívá k rozvoji civilizace."

batch_dict = tokenizer([text1, text2], max_length=512, padding=True, truncation=True, return_tensors='pt')
print(batch_dict)

I get:

{'input_ids': tensor([[  101, 32693,  7367, 38819, 52191,  1029,   102,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0],
        [  101, 52191,  1006,  1062, 56142,  6806,   100,  1006, 32156,  5332,
         15710,  1007,  1024, 33750,  1010, 27838, 43953,   100,  1006, 32156,
          6190,  1007,  1024, 38851,  1010, 34709, 30668,  1010, 19261,  4801,
         34610, 31327, 18153,  7685,  2102,  1007, 15333,  4654,  4817, 30626,
         40107,  2078, 30553, 35854,  1010, 30720, 53612, 55693, 31895, 40157,
         51460,  1012, 38936, 33927,  1037, 43187, 40597,  1010,  3424, 54689,
          3723,  1010, 42287,  2050,  1010, 40157, 31327,  1010, 34445,  1045,
         45819, 39346, 40510,  1010, 36955,  1010, 41493, 32582,  1012, 35065,
         30826, 38256, 37260, 52191, 34135, 45606, 35707, 33354, 33633,  1012,
         31962, 48899, 51842, 15333, 37805, 31745, 44512,  1058, 34958,  1010,
         31434, 42682, 43056,  1047, 41032, 48519,  1012,   102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
Seznam.cz org
edited Feb 28

@karmiq
hey - direct loading using the SentenceTransformer class does not work because the sentence model is created with the MEAN pooling by default. Our models work with CLS pooling.
You can load it like this:

import sentence_transformers
from sentence_transformers.models import Transformer, Pooling

embedding_model = Transformer("Seznam/simcse-dist-mpnet-paracrawl-cs-en")
pooling = Pooling(word_embedding_dimension=embedding_model.get_word_embedding_dimension(), pooling_mode="cls")
model = sentence_transformers.SentenceTransformer(modules=[embedding_model, pooling])

Thanks for the replies, @arahusky and @nekoboost !

The tokenizer issue is a good hunch, this is the output I'm seeing:

texts = [
  "Čím se zabývá fyzika?",

  "Fyzika (z řeckého φυσικός (fysikos): přírodní, ze základu φύσις (fysis): příroda, přirozenost, archaicky též silozpyt) je exaktní vědní obor, který zkoumá zákonitosti přírodních jevů. Popisuje vlastnosti a projevy hmoty, antihmoty, vakua, přírodních sil, světla i neviditelného záření, tepla, zvuku atd. Vztahy mezi těmito objekty fyzika obvykle vyjadřuje matematickými prostředky. Mnoho poznatků fyziky je úspěšně aplikováno v praxi, což významně přispívá k rozvoji civilizace.",
]

for text in texts:
  print(model.tokenizer(text, padding=True, truncation=True, max_length=512, return_tensors='pt'))
  print('-'*80)

# {'input_ids': tensor([[  101, 32693,  7367, 38819, 52191,  1029,   102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1]])}
# --------------------------------------------------------------------------------
# {'input_ids': tensor([[  101, 52191,  1006,  1062, 56142,  6806,   100,  1006, 32156,  5332,
#         15710,  1007,  1024, 33750,  1010, 27838, 43953,   100,  1006, 32156,
#          6190,  1007,  1024, 38851,  1010, 34709, 30668,  1010, 19261,  4801,
#         34610, 31327, 18153,  7685,  2102,  1007, 15333,  4654,  4817, 30626,
#         40107,  2078, 30553, 35854,  1010, 30720, 53612, 55693, 31895, 40157,
#         51460,  1012, 38936, 33927,  1037, 43187, 40597,  1010,  3424, 54689,
#          3723,  1010, 42287,  2050,  1010, 40157, 31327,  1010, 34445,  1045,
#         45819, 39346, 40510,  1010, 36955,  1010, 41493, 32582,  1012, 35065,
#         30826, 38256, 37260, 52191, 34135, 45606, 35707, 33354, 33633,  1012,
#         31962, 48899, 51842, 15333, 37805, 31745, 44512,  1058, 34958,  1010,
#         31434, 42682, 43056,  1047, 41032, 48519,  1012,   102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
#         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
#         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
#         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
#         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
# --------------------------------------------------------------------------------

At least in the first item, it doesn't look like it's returning the padding tokens, otherwise it returns the same tokens, at least in my limited understanding.

@nekoboost , thanks for the clarification! When I adjust the isolated example to use the code you've provided, the score does improve significantly:

texts = [
  "Čím se zabývá fyzika?",

  "Fyzika (z řeckého φυσικός (fysikos): přírodní, ze základu φύσις (fysis): příroda, přirozenost, archaicky též silozpyt) je exaktní vědní obor, který zkoumá zákonitosti přírodních jevů. Popisuje vlastnosti a projevy hmoty, antihmoty, vakua, přírodních sil, světla i neviditelného záření, tepla, zvuku atd. Vztahy mezi těmito objekty fyzika obvykle vyjadřuje matematickými prostředky. Mnoho poznatků fyziky je úspěšně aplikováno v praxi, což významně přispívá k rozvoji civilizace.",
]

import sentence_transformers
from sentence_transformers.models import Transformer, Pooling

embedding_model = Transformer("Seznam/simcse-dist-mpnet-paracrawl-cs-en")
pooling = Pooling(word_embedding_dimension=embedding_model.get_word_embedding_dimension(), pooling_mode="cls")
model = sentence_transformers.SentenceTransformer(modules=[embedding_model, pooling])

embeddings = [ model.encode(text) for text in texts]

sentence_transformers.util.cos_sim(embeddings[0], embeddings[1]).tolist()

# [[0.6310944557189941]]

When I run the example I have in the README, on the whole Wikipedia dataset, I'm still getting irrelevant results — that's expected, though, due to the incorrect initialization of the model when computing the corpus embeddings. I'll regenerate the embeddings with the new code and will report the results.

I've regenerated the embeddings using the new code from @nekoboost (649bf8f), and the example from the README now works much better:

# ...
query = "Čím se zabývá fyzika?"
# ...
[0.72] Molekulová fyzika ( též molekulární fyzika ) je část fyziky, která zkoumá látky na úrovni atomů a… [Molekulová fyzika]
[0.70] Fyzika ( z řeckého φυσικός ( fysikos ): přírodní, ze základu φύσις ( fysis ): příroda, archaicky… [Fyzika]
[0.69] Experimentální fyzika je spolu s teoretickou fyzikou jednou ze dvou hlavních metod zkoumání fyziky.… [Experimentální fyzika]
[0.69] Mechanika je obor fyziky, který se zabývá mechanickým pohybem, tedy přemísťováním těles v prostoru… [Mechanika]
[0.69] Matematická fyzika je vědecká disciplína zabývající se aplikací matematiky k řešení fyzikálních… [Matematická fyzika]
[0.68] Fyzika částic Fyzikální jevy [Rozpad částice]
[0.68] Teoretická fyzika se snaží racionálně, často pomocí matematických vztahů, vysvětlit fyzikální jevy… [Teoretická fyzika]
[0.68] Fyzika částic ( též částicová fyzika ) je oblast fyziky, která se zabývá částicemi. V širším smyslu… [Fyzika částic]
[0.66] Fyzika pevných látek je část fyziky, která studuje makroskopické vlastnosti pevných látek, přičemž… [Fyzika pevných látek]
[0.66] Klasická fyzika je označení pro starší fyzikální teorie, zejména ty popsané mezi koncem 17. a… [Klasická fyzika]

I'll try the model in a demo application I have.

I've plugged the model into a demo application I have (semantic search with Elasticsearch, code here, demo here), and the results are indeed much better than previously, where they have been almost 100% not relevant.

For a query Co je to staroměstský orloj?, I'm getting these results with size=10:

* Oldřišov
* Označování ulic a veřejných prostranství
* Spálov (okres Nový Jičín)
* Bedřichov (Oskava)
* Osiky
* Nesuchyně
* Zvole (okres Šumperk)
* Oldřiš
* Pohořelice
* Lukovany

It seems to return geographically related concepts, but almost all results are unrelated to the specific query, and it's not able to match the best hit (page Staroměstský orloj).

With the intfloat/multilingual-e5-small model, I'm getting these results:

* Staroměstský orloj
* Orloj
* Pokojový orloj Jana Maška
* Pohádkový orloj v Ostravě
* Jakub Čech (hodinář)
* Slovenský orloj
* Mistr Hanuš
* Olomoucký orloj
* Chmelový orloj
* Hvězdný čas

If I try a query with "vocabulary mismatch" problem, velké staré hodiny pro turisty v Praze, I'm getting results like this:

* Buquoyský palác (Staré Město)
* Seznam představitelů Starého Města pražského
* Pomník mistra Jana Husa
* Palác pánů z Kunštátu a Poděbrad
* Krocínova kašna
* Dům U Vejvodů
* Komořany (zámek)
* Lidový dům (Hybernská)
* Královská cesta
* Dům U Voříkovských

The e5-small model struggles with this query as well, where eg. the e5-large model seems to get it quite right:

* Veřejné hodiny v Praze
* Hodiny na České
* Staroměstský orloj
* Patrik Pařízek
* Gnómón
* Brněnský orloj
* Pražský poledník
* Přesýpací hodiny
* Kinského zahrada
* Pražský metronom

The good news is that it seems like the huggingface/text-embeddings-inference project produces the same results, even though it loads the model with the Rust-based Candle library. The only important thing is to pass the cls pooling setting, as you have suggested:

text-embeddings-router --model-id Seznam/simcse-dist-mpnet-paracrawl-cs-en --pooling cls
Seznam.cz org

Hi, iam getting different scores - make sure you do NOT use any prefixes (e.g. "query {query}"), also check that the cosine sim is actually calculated, not the Euclidean or dot product

Co je to staroměstský orloj?
    Oldřišov                similarity: 0.5156916975975037
    Staroměstský orloj  	similarity: 0.855506181716919

@nekoboost , thanks! Could you please share the code you've used?

I've double-checked in the code that the prefixes are not used for this model (they should apply only to the E5 models), and will run some manual testing (fresh embedding, than similarity matches) on my end.

Seznam.cz org

Your code above produces the same results c :

texts = [
  "Co je to staroměstský orloj?",
  "Staroměstský orloj",
]

import sentence_transformers
from sentence_transformers.models import Transformer, Pooling

embedding_model = Transformer("Seznam/simcse-dist-mpnet-paracrawl-cs-en")
pooling = Pooling(word_embedding_dimension=embedding_model.get_word_embedding_dimension(), pooling_mode="cls")
model = sentence_transformers.SentenceTransformer(modules=[embedding_model, pooling])

embeddings = [ model.encode(text) for text in texts]

sentence_transformers.util.cos_sim(embeddings[0], embeddings[1]).tolist()

[[0.855506181716919]]

This comment has been hidden

Sign up or log in to comment