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 14724cd7122d09ad2bf1e2a7cc1ace7f81862c2d
parent c82e284c00e85275181f14259dda076fd844b902
Author: Sondre Wold <sondrewold1@gmail.com>
Date:   Thu, 25 Sep 2025 09:15:58 +0200

Refactor and clean up the code

Diffstat:
Ddebug/analyze_embeddings.py | 114-------------------------------------------------------------------------------
Ddebug/plot_sims.py | 182-------------------------------------------------------------------------------
Mexperiment.py | 509++++---------------------------------------------------------------------------
Mget_epsilon.py | 71++++++++++++++++-------------------------------------------------------
Amodels.py | 429+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrandom_experiment.py | 15++++++++-------
Mstatistical_testing.py | 58+++++++++++++++++++++++++++++++++++++++++++++-------------
7 files changed, 522 insertions(+), 856 deletions(-)

diff --git a/debug/analyze_embeddings.py b/debug/analyze_embeddings.py @@ -1,114 +0,0 @@ -import argparse -import pathlib -import pickle - -from numpy import mean -from experiment import Point - -import pandas as pd - -from typing import Any - -import torch -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("-result_path", type=pathlib.Path) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - points_folder: str = args.points_path - raw_files = list(pathlib.Path(points_folder).glob("*.pkl")) - - final_result: dict[str, list[Any]] = { - "dataset": [], - "model": [], - "a_norm": [], - "b_norm": [], - "ac_norm": [], - "bc_norm": [], - "a_mean": [], - "b_mean": [], - "ac_mean": [], - "bc_mean": [], - "distance": [], - } - - 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) - a_norms: list[float] = [] - b_norms: list[float] = [] - ac_norms: list[float] = [] - bc_norms: list[float] = [] - a_means: list[torch.Tensor] = [] - b_means: list[torch.Tensor] = [] - ac_means: list[torch.Tensor] = [] - bc_means: list[torch.Tensor] = [] - for point in points: - a_norms.append( - torch.linalg.norm(torch.Tensor(point.phrase_a_embedded)).item() - ) - b_norms.append( - torch.linalg.norm(torch.Tensor(point.phrase_b_embedded)).item() - ) - ac_norms.append( - torch.linalg.norm(torch.Tensor(point.context_ac_embedded)).item() - ) - bc_norms.append( - torch.linalg.norm(torch.Tensor(point.context_bc_embedded)).item() - ) - a_means.append(torch.Tensor(point.phrase_a_embedded)) - b_means.append(torch.Tensor(point.phrase_b_embedded)) - ac_means.append(torch.Tensor(point.context_ac_embedded)) - bc_means.append(torch.Tensor(point.context_bc_embedded)) - - final_result["dataset"].append(dataset_name) - final_result["model"].append(model_name) - final_result["a_norm"].append(mean(a_norms)) - final_result["b_norm"].append(mean(b_norms)) - final_result["ac_norm"].append(mean(ac_norms)) - final_result["bc_norm"].append(mean(bc_norms)) - - final_result["a_mean"].append( - torch.mean(torch.stack(a_means), dim=-1).norm().item() - ) - final_result["b_mean"].append( - torch.mean(torch.stack(b_means), dim=-1).norm().item() - ) - final_result["ac_mean"].append( - torch.mean(torch.stack(ac_means), dim=-1).norm().item() - ) - final_result["bc_mean"].append( - torch.mean(torch.stack(bc_means), dim=-1).norm().item() - ) - - final_result["distance"].append( - ( - ( - torch.mean(torch.stack(ac_means), dim=-1) - + torch.mean(torch.stack(bc_means), dim=-1) - ) - / 2 - - ( - torch.mean(torch.stack(a_means), dim=-1) - + torch.mean(torch.stack(b_means), dim=-1) - ) - / 2 - ) - .norm() - .item() - ) - - df = pd.DataFrame.from_dict(final_result) - print(df) - - -if __name__ == "__main__": - main() diff --git a/debug/plot_sims.py b/debug/plot_sims.py @@ -1,182 +0,0 @@ -import sys - -parent_dir = ".." -sys.path.append(parent_dir) -import argparse -import pathlib -import pickle - -import numpy - -from numpy import mean -from experiment import Point - -import matplotlib.pyplot as plt - -import pandas as pd - -from typing import Any - -import torch -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 - raw_files = list(pathlib.Path(points_folder).glob("*.pkl")) - - final_result: dict[str, list[Any]] = { - "dataset": [], - "model": [], - "phrase_sim": [], - "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 = [ - ( - point.phrase_similarity - if isinstance(point.phrase_similarity, float) - else point.phrase_similarity.item() - ) - for point in points - ] - context_sims = [ - ( - point.contextualized_similarity - if isinstance(point.contextualized_similarity, float) - else point.contextualized_similarity.item() - ) - for point in points - ] - - 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"], - [y + 1 for y in range(len(final_result["phrase_sim"]))], - ) - axs[0, 0].set_xticks( - [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"]))], - ) - axs[1, 0].set_xticks( - [y + 1 for y in range(len(final_result["context_sim"]))], - labels=final_result["model"], - ) - 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/experiment.py b/experiment.py @@ -14,9 +14,7 @@ import tqdm from transformers import AutoTokenizer, AutoModel from sentence_transformers import SentenceTransformer - - -DEVICE = "cuda" if torch.cuda.is_available() else "cpu" +from models import ModelFactory, Model def parse_args() -> argparse.Namespace: @@ -27,11 +25,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "-experiment_type", type=str, choices=["sick", "ncip", "ncip-non"] ) - parser.add_argument( - "-model", - type=str, - choices=list(ModelFactory._factory.keys()) - ) + parser.add_argument("-model", type=str, choices=list(ModelFactory._factory.keys())) return parser.parse_args() @@ -55,435 +49,18 @@ class Point(NamedTuple): comp_class: str -class ResultObject(NamedTuple): - model_name: str - dataset_name: str - points: list[Point] - - class SentencePairs(NamedTuple): sentence_A: list[str] sentence_B: list[str] classes: list[str] -def normalize_v2(): - """ - For a context c and a substring x (to be replaced), replace x with n candidates, embed them, calculate the mean the std.dev. - Subtract the mean and divide by the std.dev (gaussian assumption) (mu(ac) - (mean(c with x)) / std.dev - - Sample x from a list of random words. - """ - pass - - -class Model(ABC): - - @abstractmethod - def embed(self, string: str) -> torch.Tensor: - pass - - def normalize( - self, - composed: torch.Tensor, - original_phrase_removed: torch.Tensor, - lambda_value: float, - ): - return composed - (original_phrase_removed * lambda_value) - - @abstractmethod - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - pass - - -class ModelFactory: - - _factory: dict[str, type[Model]] = {} - - def __init__(self, name: str): - self._name = name - - def __call__(self, model: type[Model]) -> type[Model]: - self._factory[self._name] = model - return model - - @classmethod - def get(cls: type[ModelFactory], name: str, *args: Any, **kwargs: Any) -> Model: - return cls._factory[name](*args, **kwargs) - - -# Generated -def mean_pool_last_four_layers(string: str, tokenizer, encoder) -> torch.Tensor: - inputs = tokenizer(string.lower(), return_tensors="pt") - - # Get all hidden states (set output_hidden_states=True) - outputs = encoder(**inputs, output_hidden_states=True) - - # Get the last four hidden states (layers) - # hidden_states is a tuple of length num_layers+1 (includes embedding layer) - # We take layers [-4:] to get the last four layers (excluding embedding layer) - last_four_layers = outputs.hidden_states[-4:] - - # Stack the last four layers and average them - stacked_layers = torch.stack(last_four_layers, dim=0) # Shape: (4, batch_size, seq_len, hidden_size) - averaged_layers = torch.mean(stacked_layers, dim=0) # Shape: (batch_size, seq_len, hidden_size) - - attention_mask = inputs["attention_mask"] - - # Expand attention mask to match token embeddings dimensions - attention_mask_expanded = ( - attention_mask.unsqueeze(-1).expand(averaged_layers.size()).float() - ) - - # Apply mask to token embeddings (set padding tokens to 0) - masked_embeddings = averaged_layers * attention_mask_expanded - - # Sum embeddings and divide by number of non-padding tokens - summed_embeddings = torch.sum(masked_embeddings, dim=1) # Shape: (batch_size, hidden_size) - summed_mask = torch.clamp(attention_mask_expanded.sum(dim=1), min=1e-9) # Avoid division by zero - - # Create sentence representation by mean pooling - mean_pooled = summed_embeddings / summed_mask - - return mean_pooled - -# Generated -def mean_pool(string: str, tokenizer, encoder) -> torch.Tensor: - inputs = tokenizer(string.lower(), return_tensors="pt") - outputs = encoder(**inputs) - - token_embeddings = outputs.last_hidden_state - attention_mask = inputs["attention_mask"] - - # Expand attention mask to match token embeddings dimensions - attention_mask_expanded = ( - attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() - ) - - # Apply mask to token embeddings (set padding tokens to 0) - masked_embeddings = token_embeddings * attention_mask_expanded - - # Sum embeddings and divide by number of non-padding tokens - summed_embeddings = torch.sum( - masked_embeddings, dim=1 - ) # Shape: (batch_size, hidden_size) - summed_mask = torch.clamp( - attention_mask_expanded.sum(dim=1), min=1e-9 - ) # Avoid division by zero - - # Create sentence representation by mean pooling - mean_pooled = summed_embeddings / summed_mask - - return mean_pooled - - -# Generated -def cls_sep_pool(string: str, tokenizer, encoder) -> torch.Tensor: - inputs = tokenizer(string.lower(), return_tensors="pt") - outputs = encoder(**inputs) - token_embeddings = outputs.last_hidden_state - attention_mask = inputs["attention_mask"] - input_ids = inputs["input_ids"] - - batch_size, seq_len, hidden_size = token_embeddings.shape - - # Initialize result tensor - cls_sep_embeddings = torch.zeros(batch_size, hidden_size) - - for batch_idx in range(batch_size): - # Find the sequence length (excluding padding) - valid_length = attention_mask[batch_idx].sum().item() - - # CLS token is always at position 0 - cls_embedding = token_embeddings[batch_idx, 0] - - # Find SEP token position by looking for the SEP token ID - # SEP token is the last non-padding token before padding starts - sep_token_id = tokenizer.sep_token_id - sep_position = None - - # Search for SEP token in the valid sequence - for pos in range(valid_length): - if input_ids[batch_idx, pos] == sep_token_id: - sep_position = pos - break - - # If no SEP token found, use the last valid token - if sep_position is None: - raise Exception("No SEP token found") - - sep_embedding = token_embeddings[batch_idx, sep_position] - - # Sum CLS and SEP embeddings - cls_sep_embeddings[batch_idx] = cls_embedding + sep_embedding - - return cls_sep_embeddings - - -@ModelFactory("BERT-cls") -class BERT_cls(Model): - - def __init__(self) -> None: - self.model_name: str = "bert-base-uncased" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return self.encoder( - self.tokenizer(string.lower(), return_tensors="pt")["input_ids"] - ).last_hidden_state[:, 0, :] - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - -@ModelFactory("ModernBERT-cls") -class ModernBERT_cls(Model): - - def __init__(self) -> None: - self.model_name: str = "answerdotai/ModernBERT-base" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return self.encoder( - self.tokenizer(string.lower(), return_tensors="pt")["input_ids"] - ).last_hidden_state[:, 0, :] - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - -@ModelFactory("ModernBERT-mean") -class ModernBERT_mean(Model): - - def __init__(self) -> None: - self.model_name: str = "answerdotai/ModernBERT-base" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return mean_pool_last_four_layers(string=string, tokenizer=self.tokenizer, encoder=self.encoder) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - -@ModelFactory("ModernBERT-clssep") -class ModernBERT_clssep(Model): - - def __init__(self) -> None: - self.model_name: str = "answerdotai/ModernBERT-base" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return cls_sep_pool(string=string, tokenizer=self.tokenizer, encoder=self.encoder) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - -@ModelFactory("DeBERTa-cls") -class DeBERTa_cls(Model): - - def __init__(self) -> None: - self.model_name: str = "microsoft/deberta-v3-base" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return self.encoder( - self.tokenizer(string.lower(), return_tensors="pt")["input_ids"] - ).last_hidden_state[:, 0, :] - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - -@ModelFactory("DeBERTa-mean") -class DeBERTa_mean(Model): - - def __init__(self) -> None: - self.model_name: str = "microsoft/deberta-v3-base" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return mean_pool_last_four_layers(string=string, tokenizer=self.tokenizer, encoder=self.encoder) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - -@ModelFactory("DeBERTa-clssep") -class DeBERTa_clssep(Model): - - def __init__(self) -> None: - self.model_name: str = "microsoft/deberta-v3-base" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return cls_sep_pool(string=string, tokenizer=self.tokenizer, encoder=self.encoder) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - - -@ModelFactory("BERT-mean") -class BERT_mean(Model): - - def __init__(self) -> None: - self.model_name: str = "bert-base-uncased" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return mean_pool_last_four_layers(string=string, tokenizer=self.tokenizer, encoder=self.encoder) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - -@ModelFactory("BERT-clssep") -class BERT_clssep(Model): - - def __init__(self) -> None: - self.model_name: str = "bert-base-uncased" - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self.encoder = AutoModel.from_pretrained(self.model_name) - self.encoder.eval() - - @torch.no_grad() - def embed(self, string: str) -> torch.Tensor: - return cls_sep_pool(string=string, tokenizer=self.tokenizer, encoder=self.encoder) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return torch.nn.functional.cosine_similarity( - a, b - ).item() # dist(\mu(a), \mu(b)) - - -@ModelFactory("S-BERT") -class S_BERT(Model): - - def __init__(self): - self.encoder = SentenceTransformer("all-MiniLM-L6-v2", device=DEVICE) - - def embed(self, string: str) -> torch.Tensor: - return self.encoder.encode([string]) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return self.encoder.similarity(a, b).item() - - -@ModelFactory("S-GPT") -class S_GPT(Model): - - def __init__(self): - self.encoder = SentenceTransformer( - "Muennighoff/SGPT-125M-weightedmean-nli-bitfit", device=DEVICE - ) - - def embed(self, string: str) -> torch.Tensor: - return self.encoder.encode([string]) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return self.encoder.similarity(a, b).item() - - -@ModelFactory("E5") -class E5(Model): - - def __init__(self): - self.encoder = SentenceTransformer( - "intfloat/multilingual-e5-large-instruct", device=DEVICE - ) - self.task = "Given a sentence, encode its semantic properties" - - def get_detailed_instruct(self, query: str) -> str: - return f"Instruct: {self.task}\nQuery: {query}" - - def embed(self, string: str) -> torch.Tensor: - prepared_string = self.get_detailed_instruct(string) - return self.encoder.encode([prepared_string]) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return self.encoder.similarity(a, b).item() - - -@ModelFactory("QWEN") -class QWEN(Model): - - def __init__(self): - self.encoder = SentenceTransformer("Qwen/Qwen3-Embedding-0.6B", device=DEVICE) - self.task = "Given a sentence, encode its semantic properties" - - def get_detailed_instruct(self, query: str) -> str: - return f"Instruct: {self.task}\nQuery: {query}" - - def embed(self, string: str) -> torch.Tensor: - prepared_string = self.get_detailed_instruct(string) - return self.encoder.encode([prepared_string]) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return self.encoder.similarity(a, b).item() - - -@ModelFactory("UAE") -class UAE(Model): - - def __init__(self): - self.encoder = SentenceTransformer("WhereIsAI/UAE-Large-V1", device=DEVICE) - - def embed(self, string: str) -> torch.Tensor: - return self.encoder.encode(string) - - def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: - return self.encoder.similarity(a, b).item() - - - def find_diff(a: str, b: str) -> list[str] | None: """ Finds the longest continuous substring where A differs from B and B differs from A. Subtracts the longest common prefix and suffix and takes whats left. Not the smartest algorithm, perhaps. """ - if a == b: # Should not happen for SICK or NCIP, but you never know. + if a == b: # Should not happen, but you never know.... return None try: @@ -535,7 +112,6 @@ class Experiment: self.model: Model = model self.sentence_pairs = sentence_pairs self.normalization_value: float = normalization_value - self.diff_errors: int = 0 self.embed_errors: int = 0 self.points: list[Point] = [] self.result_path: pathlib.Path = result_path @@ -545,12 +121,13 @@ class Experiment: self.phrase_b_length: list[int] = [] def run_experiment(self) -> None: - for i, (sentence_a, sentence_b, cls) in enumerate(tqdm.tqdm(zip(*self.sentence_pairs))): + for i, (sentence_a, sentence_b, cls) in enumerate( + tqdm.tqdm(zip(*self.sentence_pairs)) + ): diffs: list[str] | None = find_diff(sentence_a, sentence_b) if not diffs: - self.diff_errors += 1 - print(f"Error at index {i}, sentence: {sentence_a}") - continue + # We need exact matching of the pairs, so abort mission here. + raise Exception(f"Error at index {i}, sentence: {sentence_a}") phrase_a: str phrase_b: str @@ -580,23 +157,6 @@ class Experiment: self.embed_errors += 1 continue - if self.normalization_value > 0.0: - a_removed: str = sentence_a.replace(phrase_a, "") - b_removed: str = sentence_b.replace(phrase_b, "") - try: - a_removed_embedded = self.model.embed(a_removed) - b_removed_embedded = self.model.embed(b_removed) - except EmbedError as e: - self.embed_errors += 1 - continue - - a_in_context_embedded = self.model.normalize( - a_in_context_embedded, a_removed_embedded, self.normalization_value - ) - b_in_context_embedded = self.model.normalize( - b_in_context_embedded, b_removed_embedded, self.normalization_value - ) - context_similarity: float = self.model.calculate_similarity( a_in_context_embedded, b_in_context_embedded ) @@ -613,7 +173,7 @@ class Experiment: phrase_b_embedded=phrase_b_embedded, context_ac_embedded=a_in_context_embedded, context_bc_embedded=b_in_context_embedded, - comp_class=cls + comp_class=cls, ) ) @@ -622,21 +182,10 @@ class Experiment: with open(self.result_path.with_suffix(".pkl"), "wb") as file: pickle.dump(self.points, file) - def save_results_2(self, dataset): - pathlib.Path(self.result_path).parent.mkdir(parents=True, exist_ok=True) - result = ResultObject(model_name=self.model.name, dataset_name=dataset, points=self.points) - with open(self.result_path.with_suffix(".pkl"), "wb") as file: - pickle.dump(self.points, result) - - - def main() -> None: - args = parse_args() - sep = "\t" - if args.experiment_type in ["ncip", "ncip-non"]: - sep = "," - + args: argparse.Namespace = parse_args() + sep: str = "," df: pandas.DataFrame = pandas.read_csv(args.data_path, sep=sep) if args.experiment_type == "ncip": @@ -650,39 +199,33 @@ def main() -> None: synonym_sentence: list[str] = df[ "synonym both" ].values.tolist() # Both the modifier and the noun are swapped with synonyms from word net. - + sentence_A: list[str] = original_sentence sentence_B: list[str] = synonym_sentence - # Validate the find_diff function somewhat - for i, (sA, sB, goldA) in enumerate(zip(sentence_A, sentence_B, original_idiom)): + # Validate the find_diff function somewhat. + # The find_diff between sentA and sentB should return the origin MWE. + for i, (sA, sB, goldA) in enumerate( + zip(sentence_A, sentence_B, original_idiom) + ): res = find_diff(sA, sB) if res is not None: diffa, diffb = res if diffa != goldA: - print(i, sA, sB, goldA, diffa, diffb) - exit() + raise Exception(i, sA, sB, goldA, diffa, diffb) elif args.experiment_type == "ncip-non": # Here we measure the difference between two non-idiomatic phrases in context. sentence_A = df["synonym both"].values.tolist() sentence_B = df["both synonym alt1_1"].values.tolist() - - elif args.experiment_type == "sick": - lexA = df.loc[df["sentence_A_expRule"] == "S2_lex"] - lexB = df.loc[df["sentence_B_expRule"] == "S2_lex"] - lex = pandas.concat([lexA, lexB]) - sentence_A = lex["sentence_A"].values.tolist() - sentence_B = lex["sentence_B"].values.tolist() - else: raise ValueError("Undefined dataset type...") - clss: list[str] = df[ - "class" - ].values.tolist() # The comp. classes + clss: list[str] = df["class"].values.tolist() # The comp. classes - sentence_pairs = SentencePairs(sentence_A=sentence_A, sentence_B=sentence_B, classes=clss) + sentence_pairs = SentencePairs( + sentence_A=sentence_A, sentence_B=sentence_B, classes=clss + ) models = ( [args.model] if args.model is not None else list(ModelFactory._factory.keys()) @@ -691,8 +234,7 @@ def main() -> None: for model_type in models: model = ModelFactory.get(model_type) result_path = ( - args.result_path - / f"{args.experiment_type}_{model_type}_lambda{int(args.normalization_value * 10)}" + args.result_path / f"{args.experiment_type}_{model_type}" ) # shitty logging of lambda parameter, need to fix experiment = Experiment( sentence_pairs=sentence_pairs, @@ -702,9 +244,7 @@ def main() -> None: ) print(f"Running experiment for model: {model_type}.") experiment.run_experiment() - print( - f"Diff errors: {experiment.diff_errors}, Embedding errors: {experiment.embed_errors}" - ) + print(f"Embedding errors: {experiment.embed_errors}") print(f"Saving experiment to: {str(result_path)}") sent_a_mean = numpy.mean(experiment.sentence_a_length) @@ -726,7 +266,6 @@ def main() -> None: print(descriptive_statistics) experiment.save_results() - #experiment.save_results_2(dataset=args.experiment_type) if __name__ == "__main__": diff --git a/get_epsilon.py b/get_epsilon.py @@ -9,7 +9,6 @@ import matplotlib.pyplot as plt from typing import Any import json - import pandas import argparse @@ -24,12 +23,6 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Gather epsilon values") parser.add_argument("-points_path", type=pathlib.Path) parser.add_argument("-result_path", type=pathlib.Path) - parser.add_argument( - "-percentile", - type=float, - default=None, - help="The percentile for the epsilon value", - ) return parser.parse_args() @@ -40,23 +33,6 @@ def get_dataset_and_model_name(filename: str) -> tuple[str, str]: return dataset_name, model_name -def get_simscore( - ac_emb: list[torch.Tensor], bc_emb: list[torch.Tensor] - ) -> list[float]: - res: list[float] = [] - for ac, bc in zip(ac_emb, bc_emb): - ac = ac.view(1, -1) - bc = bc.view(1, -1) - try: - x = float(torch.nn.functional.cosine_similarity(ac, bc)) - except: - print(ac.shape) - print(bc.shape) - exit() - res.append(x) - return res - - def get_epsilons( word_sims: numpy.ndarray, context_sims: numpy.ndarray ) -> dict[str, list[float]]: @@ -90,14 +66,16 @@ def main() -> None: "original": [], "divisive": [], "additive": [], - "simscores": [], "classes": [], } + 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) - print(model_name) + dataset_name, model_name = get_dataset_and_model_name( + raw_file.with_suffix("").name + ) + print(f"Getting epsilons for: {model_name}") n: int = len(points) word_sims: numpy.ndarray = numpy.array( [point.phrase_similarity for point in points] @@ -106,37 +84,20 @@ def main() -> None: [point.contextualized_similarity for point in points] ) classes = [point.comp_class for point in points] - ac_embedded: list[torch.Tensor] = [torch.Tensor(point.context_ac_embedded) for point in points] - bc_embedded: list[torch.Tensor] = [torch.Tensor(point.context_bc_embedded) for point in points] + ac_embedded: list[torch.Tensor] = [ + torch.Tensor(point.context_ac_embedded) for point in points + ] + bc_embedded: list[torch.Tensor] = [ + torch.Tensor(point.context_bc_embedded) for point in points + ] tmp_result: dict[str, list[float]] = get_epsilons(word_sims, context_sims) - simscores: list[float] = get_simscore(ac_embedded, bc_embedded) final_result["dataset"].append(dataset_name) final_result["model"].append(model_name) - final_result["simscores"].append(simscores) - if ( - args.percentile is not None - ): # Save the percentile epsilons to disk as a csv - final_original: Any = float( - numpy.percentile(tmp_result["original"], args.percentile) - ) - final_division: Any = float( - numpy.percentile(tmp_result["divisive"], args.percentile) - ) - final_additive: Any = float( - numpy.percentile(tmp_result["additive"], args.percentile) - ) - else: - final_original = tmp_result["original"] - final_division = tmp_result["divisive"] - final_additive = tmp_result["additive"] - - final_result["original"].append(final_original) - final_result["divisive"].append(final_division) - final_result["additive"].append(final_additive) - final_result["classes"] = classes - - # df: pandas.DataFrame = pandas.DataFrame.from_dict(final_result) - # df.to_csv(args.result_path, sep=",", index=False) + final_result["original"].append(tmp_result["original"]) + final_result["divisive"].append(tmp_result["divisive"]) + final_result["additive"].append(tmp_result["additive"]) + final_result["classes"].append(classes) + with open(args.result_path, "w") as f: json.dump(final_result, f) diff --git a/models.py b/models.py @@ -0,0 +1,429 @@ +from __future__ import annotations +from typing import NamedTuple, Any +from abc import ABC, abstractmethod +from abc import ABC, abstractmethod +from transformers import AutoTokenizer, AutoModel +from sentence_transformers import SentenceTransformer +import torch + +DEVICE = "cuda" if torch.cuda.is_available() else "cpu" + + +class Model(ABC): + + @abstractmethod + def embed(self, string: str) -> torch.Tensor: + pass + + def normalize( + self, + composed: torch.Tensor, + original_phrase_removed: torch.Tensor, + lambda_value: float, + ): + return composed - (original_phrase_removed * lambda_value) + + @abstractmethod + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + pass + + +class ModelFactory: + + _factory: dict[str, type[Model]] = {} + + def __init__(self, name: str): + self._name = name + + def __call__(self, model: type[Model]) -> type[Model]: + self._factory[self._name] = model + return model + + @classmethod + def get(cls: type[ModelFactory], name: str, *args: Any, **kwargs: Any) -> Model: + return cls._factory[name](*args, **kwargs) + + +# Generated +def mean_pool_last_four_layers(string: str, tokenizer, encoder) -> torch.Tensor: + inputs = tokenizer(string.lower(), return_tensors="pt") + + # Get all hidden states (set output_hidden_states=True) + outputs = encoder(**inputs, output_hidden_states=True) + + # Get the last four hidden states (layers) + # hidden_states is a tuple of length num_layers+1 (includes embedding layer) + # We take layers [-4:] to get the last four layers (excluding embedding layer) + last_four_layers = outputs.hidden_states[-4:] + + # Stack the last four layers and average them + stacked_layers = torch.stack( + last_four_layers, dim=0 + ) # Shape: (4, batch_size, seq_len, hidden_size) + averaged_layers = torch.mean( + stacked_layers, dim=0 + ) # Shape: (batch_size, seq_len, hidden_size) + + attention_mask = inputs["attention_mask"] + + # Expand attention mask to match token embeddings dimensions + attention_mask_expanded = ( + attention_mask.unsqueeze(-1).expand(averaged_layers.size()).float() + ) + + # Apply mask to token embeddings (set padding tokens to 0) + masked_embeddings = averaged_layers * attention_mask_expanded + + # Sum embeddings and divide by number of non-padding tokens + summed_embeddings = torch.sum( + masked_embeddings, dim=1 + ) # Shape: (batch_size, hidden_size) + summed_mask = torch.clamp( + attention_mask_expanded.sum(dim=1), min=1e-9 + ) # Avoid division by zero + + # Create sentence representation by mean pooling + mean_pooled = summed_embeddings / summed_mask + + return mean_pooled + + +# Generated +def mean_pool(string: str, tokenizer, encoder) -> torch.Tensor: + inputs = tokenizer(string.lower(), return_tensors="pt") + outputs = encoder(**inputs) + + token_embeddings = outputs.last_hidden_state + attention_mask = inputs["attention_mask"] + + # Expand attention mask to match token embeddings dimensions + attention_mask_expanded = ( + attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + ) + + # Apply mask to token embeddings (set padding tokens to 0) + masked_embeddings = token_embeddings * attention_mask_expanded + + # Sum embeddings and divide by number of non-padding tokens + summed_embeddings = torch.sum( + masked_embeddings, dim=1 + ) # Shape: (batch_size, hidden_size) + summed_mask = torch.clamp( + attention_mask_expanded.sum(dim=1), min=1e-9 + ) # Avoid division by zero + + # Create sentence representation by mean pooling + mean_pooled = summed_embeddings / summed_mask + + return mean_pooled + + +# Generated +def cls_sep_pool(string: str, tokenizer, encoder) -> torch.Tensor: + inputs = tokenizer(string.lower(), return_tensors="pt") + outputs = encoder(**inputs) + token_embeddings = outputs.last_hidden_state + attention_mask = inputs["attention_mask"] + input_ids = inputs["input_ids"] + + batch_size, seq_len, hidden_size = token_embeddings.shape + + # Initialize result tensor + cls_sep_embeddings = torch.zeros(batch_size, hidden_size) + + for batch_idx in range(batch_size): + # Find the sequence length (excluding padding) + valid_length = attention_mask[batch_idx].sum().item() + + # CLS token is always at position 0 + cls_embedding = token_embeddings[batch_idx, 0] + + # Find SEP token position by looking for the SEP token ID + # SEP token is the last non-padding token before padding starts + sep_token_id = tokenizer.sep_token_id + sep_position = None + + # Search for SEP token in the valid sequence + for pos in range(valid_length): + if input_ids[batch_idx, pos] == sep_token_id: + sep_position = pos + break + + # If no SEP token found, use the last valid token + if sep_position is None: + raise Exception("No SEP token found") + + sep_embedding = token_embeddings[batch_idx, sep_position] + + # Sum CLS and SEP embeddings + cls_sep_embeddings[batch_idx] = cls_embedding + sep_embedding + + return cls_sep_embeddings + + +@ModelFactory("BERT-cls") +class BERT_cls(Model): + + def __init__(self) -> None: + self.model_name: str = "bert-base-uncased" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return self.encoder( + self.tokenizer(string.lower(), return_tensors="pt")["input_ids"] + ).last_hidden_state[:, 0, :] + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("ModernBERT-cls") +class ModernBERT_cls(Model): + + def __init__(self) -> None: + self.model_name: str = "answerdotai/ModernBERT-base" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return self.encoder( + self.tokenizer(string.lower(), return_tensors="pt")["input_ids"] + ).last_hidden_state[:, 0, :] + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("ModernBERT-mean") +class ModernBERT_mean(Model): + + def __init__(self) -> None: + self.model_name: str = "answerdotai/ModernBERT-base" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return mean_pool_last_four_layers( + string=string, tokenizer=self.tokenizer, encoder=self.encoder + ) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("ModernBERT-clssep") +class ModernBERT_clssep(Model): + + def __init__(self) -> None: + self.model_name: str = "answerdotai/ModernBERT-base" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return cls_sep_pool( + string=string, tokenizer=self.tokenizer, encoder=self.encoder + ) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("DeBERTa-cls") +class DeBERTa_cls(Model): + + def __init__(self) -> None: + self.model_name: str = "microsoft/deberta-v3-base" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return self.encoder( + self.tokenizer(string.lower(), return_tensors="pt")["input_ids"] + ).last_hidden_state[:, 0, :] + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("DeBERTa-mean") +class DeBERTa_mean(Model): + + def __init__(self) -> None: + self.model_name: str = "microsoft/deberta-v3-base" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return mean_pool_last_four_layers( + string=string, tokenizer=self.tokenizer, encoder=self.encoder + ) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("DeBERTa-clssep") +class DeBERTa_clssep(Model): + + def __init__(self) -> None: + self.model_name: str = "microsoft/deberta-v3-base" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return cls_sep_pool( + string=string, tokenizer=self.tokenizer, encoder=self.encoder + ) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("BERT-mean") +class BERT_mean(Model): + + def __init__(self) -> None: + self.model_name: str = "bert-base-uncased" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return mean_pool_last_four_layers( + string=string, tokenizer=self.tokenizer, encoder=self.encoder + ) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("BERT-clssep") +class BERT_clssep(Model): + + def __init__(self) -> None: + self.model_name: str = "bert-base-uncased" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.encoder = AutoModel.from_pretrained(self.model_name) + self.encoder.eval() + + @torch.no_grad() + def embed(self, string: str) -> torch.Tensor: + return cls_sep_pool( + string=string, tokenizer=self.tokenizer, encoder=self.encoder + ) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return torch.nn.functional.cosine_similarity( + a, b + ).item() # dist(\mu(a), \mu(b)) + + +@ModelFactory("S-BERT") +class S_BERT(Model): + + def __init__(self): + self.encoder = SentenceTransformer("all-MiniLM-L6-v2", device=DEVICE) + + def embed(self, string: str) -> torch.Tensor: + return self.encoder.encode([string]) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return self.encoder.similarity(a, b).item() + + +@ModelFactory("S-GPT") +class S_GPT(Model): + + def __init__(self): + self.encoder = SentenceTransformer( + "Muennighoff/SGPT-125M-weightedmean-nli-bitfit", device=DEVICE + ) + + def embed(self, string: str) -> torch.Tensor: + return self.encoder.encode([string]) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return self.encoder.similarity(a, b).item() + + +@ModelFactory("E5") +class E5(Model): + + def __init__(self): + self.encoder = SentenceTransformer( + "intfloat/multilingual-e5-large-instruct", device=DEVICE + ) + self.task = "Given a sentence, encode its semantic properties" + + def get_detailed_instruct(self, query: str) -> str: + return f"Instruct: {self.task}\nQuery: {query}" + + def embed(self, string: str) -> torch.Tensor: + prepared_string = self.get_detailed_instruct(string) + return self.encoder.encode([prepared_string]) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return self.encoder.similarity(a, b).item() + + +@ModelFactory("QWEN") +class QWEN(Model): + + def __init__(self): + self.encoder = SentenceTransformer("Qwen/Qwen3-Embedding-0.6B", device=DEVICE) + self.task = "Given a sentence, encode its semantic properties" + + def get_detailed_instruct(self, query: str) -> str: + return f"Instruct: {self.task}\nQuery: {query}" + + def embed(self, string: str) -> torch.Tensor: + prepared_string = self.get_detailed_instruct(string) + return self.encoder.encode([prepared_string]) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return self.encoder.similarity(a, b).item() + + +@ModelFactory("UAE") +class UAE(Model): + + def __init__(self): + self.encoder = SentenceTransformer("WhereIsAI/UAE-Large-V1", device=DEVICE) + + def embed(self, string: str) -> torch.Tensor: + return self.encoder.encode(string) + + def calculate_similarity(self, a: torch.Tensor, b: torch.Tensor) -> float: + return self.encoder.similarity(a, b).item() diff --git a/random_experiment.py b/random_experiment.py @@ -23,10 +23,7 @@ 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("-n", type=int, default=1000, help="The number of samples to calculate for") parser.add_argument( "-model", type=str, @@ -41,14 +38,17 @@ def main() -> None: 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["compound"].values.tolist() + phrases: list[str] = df["compound noun"].values.tolist() models = [args.model] if args.model is not None else ModelFactory._factory.keys() result: dict[str, list] = { "model": [], "phrase_sim": [], - "context_sim": [] + "context_sim": [], + "phrase_sim_avg": [], + "context_sim_avg": [] } for model_type in models: @@ -74,10 +74,11 @@ def main() -> None: phrase_sim = model.calculate_similarity(phrase_a_embedded, phrase_b_embedded) phrase_sims.append(phrase_sim) - print(f"{context_sim}") result["model"].append(model_type) result["phrase_sim"].append(phrase_sims) result["context_sim"].append(context_sims) + result["phrase_sim_avg"].append(numpy.mean(phrase_sims)) + result["context_sim_avg"].append(numpy.mean(context_sims)) print(f"Model: {model}") print("Random context sims", numpy.mean(context_sims)) diff --git a/statistical_testing.py b/statistical_testing.py @@ -26,12 +26,30 @@ 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) - parser.add_argument("-simscore", action="store_true", help="Use the score method based on the He et al paper") + parser.add_argument( + "-epsilon_type", + type=str, + choices=["original", "divisive", "additive"], + default="original", + help="Use the score method based on the He et al paper", + ) + parser.add_argument( + "-class_type", + type=str, + nargs="*", + choices=["C", "NC", "PC", "NCPC"], + help="The compositionality class to look at", + ) return parser.parse_args() -class ResultPoint(): - def __init__(self, model_name: str = "", idiomatic_result: list[float] = [], non_idiomatic_result: list[float] = []): +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 @@ -62,27 +80,41 @@ def main() -> None: model_name = loaded_dict["model"][i] print(f"{model_name}, {dataset_name}, {len(loaded_dict["original"][i])}") is_idiomatic = True if dataset_name == "ncip" else False - key = "simscore" if args.simscore else "original" + key = args.epsilon_type if model_name not in model_results: model_results[model_name] = ResultPoint(model_name=model_name) + + result_list: list[float] = [] + # Filter the type of samples we look for (C, NC, PC) + for item, c in zip(loaded_dict[key][i], loaded_dict["classes"][i]): + if c in args.class_type: + result_list.append(item) + if is_idiomatic: - model_results[model_name].idiomatic_result = loaded_dict[key][i] + model_results[model_name].idiomatic_result = result_list else: - model_results[model_name].non_idiomatic_result = loaded_dict[key][i] + model_results[model_name].non_idiomatic_result = result_list for model, res in model_results.items(): idiom_variance = numpy.var(res.idiomatic_result) non_idiom_variance = numpy.var(res.non_idiomatic_result) idiom_is_normal = stats.normaltest(res.idiomatic_result).pvalue > 0.05 non_idiom_is_normal = stats.normaltest(res.non_idiomatic_result).pvalue > 0.05 - coxon = stats.wilcoxon(res.idiomatic_result, res.non_idiomatic_result) # Dependence, not normally distributed, unequal sample size - U1 = coxon.statistic - U2 = len(res.idiomatic_result) * len(res.non_idiomatic_result) - U1 # as per the documentaiton notes on the scipy function - cl_effect_size = U1 / (len(res.idiomatic_result) * len(res.non_idiomatic_result)) # as per Wikipedia on Mann Whitney U - r_effect_size = ((2 * U1) / (len(res.idiomatic_result) * len(res.non_idiomatic_result))) - 1 # as per Wikipedia on Mann Whitney U + coxon = stats.wilcoxon( + res.idiomatic_result, res.non_idiomatic_result, alternative="greater" + ) + T = coxon.statistic + # As per the Wikipedia page of the Wilxocon signed-rank test. T divided by the sum of ranks. + rank_sums = sum([i for i in range(1, len(res.idiomatic_result) + 1)]) + r_effect_size = T / rank_sums is_signficant: bool = coxon.pvalue < 0.05 - hodges_lehman = hodges_lehmann_2sample(res.idiomatic_result, res.non_idiomatic_result) # To gauge the practical difference between the two groups, as suggested by the Wikipedia page on Mann Whitney U - print(f"######### \n Model name: {model} \n Idiom Variance (n={len(res.idiomatic_result)}): {idiom_variance}, Idiom is normally distributed: {idiom_is_normal}, Non-Idiom Variance (n={len(res.non_idiomatic_result)}): {non_idiom_variance}, Non-idiom is normal distributed: {non_idiom_is_normal} \n Hodges-Lehman Estimator: {hodges_lehman} \n Two-sided Wilcoxon signed-rank test: p_value={coxon.pvalue}, is_significant: {is_signficant} \n Effect size (Common Language F-test): {cl_effect_size} \n Effect size (Rank-biserial correlation): {r_effect_size}") + hodges_lehman = hodges_lehmann_2sample( + res.idiomatic_result, res.non_idiomatic_result + ) # To gauge the practical difference between the two groups, as suggested by the Wikipedia page on Mann Whitney U + + print( + f"######### \n Model name: {model} \n Idiom Variance (n={len(res.idiomatic_result)}): {idiom_variance}, Idiom is normally distributed: {idiom_is_normal}, Non-Idiom Variance (n={len(res.non_idiomatic_result)}): {non_idiom_variance}, Non-idiom is normal distributed: {non_idiom_is_normal} \n Hodges-Lehman Estimator: {hodges_lehman} \n Two-sided Wilcoxon signed-rank test: p_value={coxon.pvalue}, is_significant: {is_signficant} \n Effect size (Rank-biserial correlation): {r_effect_size}" + ) if __name__ == "__main__":