epsilon-compositionality

Code for our paper Measuring Idiomaticity in Text Embedding Models with 𝜀-compositionality
git clone https://esimon.eu/repos/epsilon-compositionality.git
Log | Files | Refs | Submodules | README

commit 9a230d17128ab1459afa66e3381b29102ee3170b
parent 5eff8e8f5cc73ca14364ae980b875f0c43d43d51
Author: Sondre Wold <sondrewold1@gmail.com>
Date:   Wed,  3 Sep 2025 08:43:29 +0200

plotting, random experiments, t-test

Diffstat:
Mdebug/plot_sims.py | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Arandom_experiment.py | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
At_test.py | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 255 insertions(+), 2 deletions(-)

diff --git a/debug/plot_sims.py b/debug/plot_sims.py @@ -1,3 +1,7 @@ +import sys + +parent_dir = ".." +sys.path.append(parent_dir) import argparse import pathlib import pickle @@ -20,10 +24,25 @@ from get_epsilon import get_dataset_and_model_name def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Gather epsilon values") parser.add_argument("-points_path", type=pathlib.Path) + parser.add_argument("-random_path", type=pathlib.Path) parser.add_argument("-result_path", type=pathlib.Path) return parser.parse_args() +def annotate_means(ax, data, positions): + for i, (d, pos) in enumerate(zip(data, positions)): + mean_val = numpy.mean(d) + ax.text( + pos, + mean_val + 0.02 * (ax.get_ylim()[1] - ax.get_ylim()[0]), # Check this LLM spagetthi + f"{mean_val:.3f}", + ha="center", + va="bottom", + fontsize=8, + bbox=dict(facecolor="white", alpha=0.7, edgecolor="none") + ) + + def main() -> None: args = parse_args() points_folder: str = args.points_path @@ -36,10 +55,15 @@ def main() -> None: "context_sim": [], } + chosen_dataset = "ncip-non" + + for raw_file in raw_files: with open(raw_file, "rb") as file: points: list[Point] = pickle.load(file) dataset_name, model_name = get_dataset_and_model_name(raw_file.name) + if dataset_name != chosen_dataset: + continue word_sims = [ ( @@ -58,11 +82,27 @@ def main() -> None: for point in points ] - final_result["dataset"].append(dataset_name) + print(model_name, dataset_name) + #final_result["dataset"].append(dataset_name) final_result["model"].append(model_name) final_result["phrase_sim"].append(word_sims) final_result["context_sim"].append(context_sims) + + + + + # Sort the "model" list alphabetically + sorted_models = sorted(final_result["model"]) + + # Create a mapping from model name to its sorted index + model_order = {model: idx for idx, model in enumerate(sorted_models)} + + # Sort all other lists in the dict based on the order of "model" + final_result = { + key: sorted(value, key=lambda x: model_order[final_result["model"][value.index(x)]]) + for key, value in final_result.items() + } fig, axs = plt.subplots(nrows=2, ncols=2, figsize=(10, 4)) axs[0, 0].violinplot( final_result["phrase_sim"], @@ -72,6 +112,11 @@ def main() -> None: [y + 1 for y in range(len(final_result["phrase_sim"]))], labels=final_result["model"], ) + annotate_means( + axs[0, 0], + final_result["phrase_sim"], + [y + 1 for y in range(len(final_result["phrase_sim"]))] + ) axs[1, 0].violinplot( final_result["context_sim"], [y + 1 for y in range(len(final_result["context_sim"]))], @@ -80,8 +125,58 @@ def main() -> None: [y + 1 for y in range(len(final_result["context_sim"]))], labels=final_result["model"], ) - plt.show() + annotate_means( + axs[1, 0], + final_result["context_sim"], + [y + 1 for y in range(len(final_result["context_sim"]))] + ) + axs[0, 0].set_title(f"{chosen_dataset} phrase similarity") + axs[0, 1].set_title(f"Random phrase similarity") + axs[1, 0].set_title(f"{chosen_dataset} context similarity") + axs[1, 1].set_title(f"Random context similarity") + + if args.random_path is not None: + with open(args.random_path, "rb") as f: + random_result = pickle.load(f) + sorted_models = sorted(random_result["model"]) + + # Create a mapping from model name to its sorted index + model_order = {model: idx for idx, model in enumerate(sorted_models)} + + # Sort all other lists in the dict based on the order of "model" + random_result = { + key: sorted(value, key=lambda x: model_order[random_result["model"][value.index(x)]]) + for key, value in random_result.items() + } + + axs[0, 1].violinplot( + random_result["phrase_sim"], + [y + 1 for y in range(len(random_result["phrase_sim"]))], + ) + axs[0, 1].set_xticks( + [y + 1 for y in range(len(random_result["phrase_sim"]))], + labels=random_result["model"], + ) + annotate_means( + axs[0, 1], + random_result["phrase_sim"], + [y + 1 for y in range(len(random_result["phrase_sim"]))] + ) + axs[1, 1].violinplot( + random_result["context_sim"], + [y + 1 for y in range(len(random_result["context_sim"]))], + ) + axs[1, 1].set_xticks( + [y + 1 for y in range(len(random_result["context_sim"]))], + labels=random_result["model"], + ) + annotate_means( + axs[1, 1], + random_result["context_sim"], + [y + 1 for y in range(len(random_result["context_sim"]))] + ) + plt.show() if __name__ == "__main__": main() diff --git a/random_experiment.py b/random_experiment.py @@ -0,0 +1,89 @@ +from __future__ import annotations +from typing import NamedTuple, Any + +import pandas +import pathlib +import argparse + +import random + +import numpy + +from abc import ABC, abstractmethod +import torch +import pickle +import tqdm + +from experiment import ModelFactory + +from transformers import AutoTokenizer, AutoModel +from sentence_transformers import SentenceTransformer + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Experiment") + parser.add_argument("-data_path", type=pathlib.Path) + parser.add_argument("-result_path", type=pathlib.Path) + parser.add_argument("-n", type=int, default=100, help="The number of samples to calculate for") + parser.add_argument( + "-experiment_type", type=str, choices=["sick", "ncip", "ncip-non"] + ) + parser.add_argument( + "-model", + type=str, + choices=["BERT-cls", "BERT-mean", "S-BERT", "S-GPT", "E5", "QWEN"], + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + sep = "," + df: pandas.DataFrame = pandas.read_csv(args.data_path, sep=sep) + print(f"Similarity between random items, n_samples={args.n}") + + sentences: list[str] = df["original sentence"].values.tolist() + phrases: list[str] = df["synonym both"].values.tolist() + + choices = ["BERT-cls", "BERT-mean", "S-BERT", "E5", "QWEN"] + models = [args.model] if args.model is not None else choices + result = { + "model": [], + "phrase_sim": [], + "context_sim": [] + } + + for model_type in models: + model = ModelFactory.get(model_type) + context_sims: list[float] = [] + phrase_sims: list[float] = [] + for _ in tqdm.tqdm(range(0, args.n)): + phrase_a = random.choice(phrases) + phrase_b = random.choice(phrases) + context_a = random.choice(sentences) + context_b = random.choice(sentences) + + if (phrase_a == phrase_b) or (context_a == context_b): + continue + + phrase_a_embedded = model.embed(phrase_a) + phrase_b_embedded = model.embed(phrase_b) + + context_a_embedded = model.embed(context_a) + context_b_embedded = model.embed(context_b) + + context_sims.append(model.calculate_similarity(context_a_embedded, context_b_embedded)) + phrase_sims.append(model.calculate_similarity(phrase_a_embedded, phrase_b_embedded)) + result["model"].append(model_type) + result["phrase_sim"].append(phrase_sims) + result["context_sim"].append(context_sims) + + print(f"Model: {model}") + print("Random context sims", numpy.mean(context_sims)) + print("Random phrase cosine similarity", numpy.mean(phrase_sims)) + + with open(args.result_path, "wb") as f: + pickle.dump(result, f) + + +if __name__ == "__main__": + main() diff --git a/t_test.py b/t_test.py @@ -0,0 +1,69 @@ +import sys +import pickle +import numpy +from torch import equal, histogram +from experiment import Point +import matplotlib.pyplot as plt + +import scipy.stats as stats + +from typing import Any +import json + +import pandas + +import argparse +import pathlib + +import warnings + +from typing import NamedTuple + +warnings.filterwarnings("ignore") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Calculate the Welsch T-Test") + parser.add_argument("-epsilons_path", type=pathlib.Path) + parser.add_argument("-result_path", type=pathlib.Path) + return parser.parse_args() + + +class ResultPoint(): + def __init__(self, model_name: str = "", idiomatic_result: list[float] = [], non_idiomatic_result: list[float] = []): + self.model_name: str = model_name + self.idiomatic_result: list[float] = idiomatic_result + self.non_idiomatic_result: list[float] = non_idiomatic_result + + +def main() -> None: + args = parse_args() + with open(args.epsilons_path, "r") as f: + loaded_dict = json.load(f) + + model_results: dict[str, ResultPoint] = {} + for i in range(0, len(loaded_dict["dataset"])): + dataset_name = loaded_dict["dataset"][i] + model_name = loaded_dict["model"][i] + is_idiomatic = True if dataset_name == "ncip" else False + if model_name not in model_results: + model_results[model_name] = ResultPoint(model_name=model_name) + if is_idiomatic: + model_results[model_name].idiomatic_result = loaded_dict["original"][i] + else: + model_results[model_name].non_idiomatic_result = loaded_dict["original"][i] + + for model, res in model_results.items(): + idiom_variance = numpy.var(res.idiomatic_result) + non_idiom_variance = numpy.var(res.non_idiomatic_result) + # null hypothesis: the two groups are from the same means, p_value < 0.05 -> the two groups are different. + t_test_statistics = stats.ttest_ind(res.idiomatic_result, res.non_idiomatic_result, equal_var=False, alternative="greater").pvalue + t_rel_statistics = stats.ttest_rel(res.idiomatic_result[:250], res.non_idiomatic_result[:250]).pvalue + is_signficant: bool = t_test_statistics < 0.05 + #print(f"Model name: {model}, Idiom Variance: {idiom_variance}, Non-Idiom Variance: {non_idiom_variance}, p_value={t_test_statistics}, is_significant: {is_signficant}") + print(f"Model name: {model}, Idiom Variance: {idiom_variance}, Non-Idiom Variance: {non_idiom_variance}, p_value={t_rel_statistics}, is_significant: {is_signficant}") + + + +if __name__ == "__main__": + main()