Source code for nlp_shap.value.tfidf

"""Frozen-corpus TF-IDF cosine value function."""

import math
from collections import Counter
from collections.abc import Sequence

from ..domain.generation import GenerationRecord
from ..protocols.backend import GenerationResult
from ._metrics import cosine_similarity
from ._token_hash import TokenHasher


[docs] class TfIdfCosineValue: """Score generations with cosine similarity of frozen TF-IDF vectors.""" def __init__(self) -> None: self._hasher = TokenHasher() self._idf: dict[int, float] | None = None self._fitted = False @property def name(self) -> str: """Return the registered value-function identifier.""" return "tfidf_cosine"
[docs] def fit(self, corpus: Sequence[GenerationRecord]) -> None: """Freeze inverse document frequency weights from ``corpus``.""" if not corpus: msg = "corpus must contain at least one generation" raise ValueError(msg) documents = [self._hasher.document_token_ids(record) for record in corpus] document_count = len(documents) document_frequency: Counter[int] = Counter() for document in documents: document_frequency.update(set(document)) self._idf = { term: math.log((document_count + 1) / (frequency + 1)) + 1.0 for term, frequency in document_frequency.items() } self._fitted = True
[docs] def score(self, base: GenerationResult, candidate: GenerationResult) -> float: """Return TF-IDF cosine similarity between ``candidate`` and ``base``.""" if not self._fitted or self._idf is None: msg = "TfIdfCosineValue.fit must be called before scoring" raise RuntimeError(msg) base_record = _require_generation_record(base) candidate_record = _require_generation_record(candidate) base_vector = self._tfidf_vector(self._hasher.document_token_ids(base_record)) candidate_vector = self._tfidf_vector( self._hasher.document_token_ids(candidate_record) ) base_dense, candidate_dense = _dict_to_dense(base_vector, candidate_vector) return cosine_similarity(base_dense, candidate_dense)
def _tfidf_vector(self, token_ids: Sequence[int]) -> dict[int, float]: idf = self._idf if idf is None: msg = "TF-IDF weights are not initialized" raise RuntimeError(msg) term_frequency = Counter(token_ids) if not term_frequency: return {} max_frequency = max(term_frequency.values()) vector: dict[int, float] = {} for term, count in term_frequency.items(): tf = count / max_frequency vector[term] = tf * idf.get(term, 1.0) return vector
def _require_generation_record(generation: GenerationResult) -> GenerationRecord: if not isinstance(generation, GenerationRecord): msg = "TfIdfCosineValue requires GenerationRecord token rows" raise TypeError(msg) return generation def _dict_to_dense( left: dict[int, float], right: dict[int, float], ) -> tuple[list[float], list[float]]: vocabulary = sorted(set(left) | set(right)) left_dense = [left.get(term, 0.0) for term in vocabulary] right_dense = [right.get(term, 0.0) for term in vocabulary] return left_dense, right_dense