├── ttlm_logo.png ├── src └── TruthTorchLM │ ├── long_form_generation │ ├── utils │ │ ├── __init__.py │ │ ├── dataset_utils.py │ │ └── safe_utils.py │ ├── evaluators │ │ └── __init__.py │ ├── __init__.py │ ├── claim_check_methods │ │ ├── __init__.py │ │ └── claim_check_method.py │ ├── decomposition_methods │ │ ├── __init__.py │ │ ├── decomposition_method.py │ │ ├── unstructured_decomposition_api.py │ │ ├── structured_decomposition_api.py │ │ ├── structured_decomposition_local.py │ │ └── unstructured_decomposition_local.py │ ├── templates.py │ └── generation.py │ ├── utils │ ├── __init__.py │ ├── calibration_utils.py │ ├── google_search_utils.py │ └── dataset_utils.py │ ├── scoring_methods │ ├── __init__.py │ ├── log_prob_scoring.py │ ├── length_normalized_scoring.py │ └── scoring_method.py │ ├── normalizers │ ├── __init__.py │ ├── standard_normalizer.py │ ├── minmax_normalizer.py │ ├── isotonic_regression.py │ ├── sigmoid_normalizer.py │ └── normalizer.py │ ├── environment.py │ ├── evaluators │ ├── __init__.py │ ├── correctness_evaluator.py │ ├── exact_match.py │ ├── bleu.py │ ├── rouge.py │ ├── eval_truth_method.py │ └── model_judge.py │ ├── error_handler.py │ ├── __init__.py │ ├── availability.py │ ├── truth_methods │ ├── __init__.py │ ├── attention_score.py │ ├── confidence.py │ ├── mini_check_method.py │ ├── inside.py │ ├── entropy.py │ ├── tokenSAR.py │ ├── truth_method.py │ ├── num_semantic_set_uncertainty.py │ ├── sum_eigen_uncertainty.py │ ├── matrix_degree_uncertainty.py │ ├── eccentricity_uncertainty.py │ ├── matrix_degree_confidence.py │ ├── eccentricity_confidence.py │ ├── verbalized_confidence.py │ ├── semantic_entropy.py │ ├── sentSAR.py │ ├── context_check.py │ ├── kernel_language_entropy.py │ ├── google_search_check.py │ ├── p_true.py │ └── SAR.py │ └── calibration.py ├── requirements.txt ├── LICENSE ├── setup.py ├── .github └── workflows │ └── python-publish.yml └── .gitignore /ttlm_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ybakman/TruthTorchLM/HEAD/ttlm_logo.png -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .dataset_utils import * 2 | from .eval_utils import * 3 | from .safe_utils import * 4 | -------------------------------------------------------------------------------- /src/TruthTorchLM/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .common_utils import * 2 | from .google_search_utils import * 3 | from .dataset_utils import * 4 | from .eval_utils import * 5 | from .calibration_utils import * 6 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/evaluators/__init__.py: -------------------------------------------------------------------------------- 1 | from .eval_claim import ClaimEvaluator 2 | from .long_gen_eval import evaluate_truth_method_long_form 3 | 4 | __all__ = ["ClaimEvaluator", "evaluate_truth_method_long_form"] 5 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/__init__.py: -------------------------------------------------------------------------------- 1 | from .generation import long_form_generation_with_truth_value 2 | from .decomposition_methods import * 3 | from .claim_check_methods import * 4 | from .evaluators import * 5 | from .utils import * 6 | from .templates import * 7 | -------------------------------------------------------------------------------- /src/TruthTorchLM/scoring_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .length_normalized_scoring import LengthNormalizedScoring 2 | from .log_prob_scoring import LogProbScoring 3 | from .scoring_method import ScoringMethod 4 | 5 | __all__ = ["LengthNormalizedScoring", "LogProbScoring", "ScoringMethod"] 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | evaluate 3 | instructor 4 | litellm 5 | nest_asyncio 6 | numpy 7 | outlines 8 | pandas 9 | pydantic 10 | PyYAML 11 | Requests 12 | scikit_learn 13 | scipy 14 | sentence_transformers 15 | termcolor 16 | torch 17 | tqdm 18 | transformers 19 | absl-py 20 | nltk 21 | rouge_score 22 | sentencepiece 23 | accelerate>=0.26.0 24 | -------------------------------------------------------------------------------- /src/TruthTorchLM/scoring_methods/log_prob_scoring.py: -------------------------------------------------------------------------------- 1 | from .scoring_method import ScoringMethod 2 | 3 | 4 | class LogProbScoring(ScoringMethod): 5 | def __init__(self): 6 | super().__init__() 7 | 8 | def __call__(self, logprobs: list[float]) -> float: 9 | return sum(logprobs) 10 | 11 | def __str__(self): 12 | return "Log Prob Scoring" 13 | -------------------------------------------------------------------------------- /src/TruthTorchLM/scoring_methods/length_normalized_scoring.py: -------------------------------------------------------------------------------- 1 | from .scoring_method import ScoringMethod 2 | 3 | 4 | class LengthNormalizedScoring(ScoringMethod): 5 | def __init__(self): 6 | super().__init__() 7 | 8 | def __call__(self, logprobs: list[float]) -> float: 9 | return sum(logprobs) / len(logprobs) 10 | 11 | def __str__(self): 12 | return "Length Normalized Scoring" 13 | -------------------------------------------------------------------------------- /src/TruthTorchLM/normalizers/__init__.py: -------------------------------------------------------------------------------- 1 | from .normalizer import Normalizer 2 | from .sigmoid_normalizer import SigmoidNormalizer 3 | from .minmax_normalizer import MinMaxNormalizer 4 | from .isotonic_regression import IsotonicRegression 5 | from .standard_normalizer import StandardNormalizer 6 | 7 | __all__ = [ 8 | "Normalizer", 9 | "SigmoidNormalizer", 10 | "MinMaxNormalizer", 11 | "IsotonicRegression", 12 | "StandardNormalizer", 13 | ] 14 | -------------------------------------------------------------------------------- /src/TruthTorchLM/environment.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | 5 | def get_cache_dir(): 6 | cache_dir = os.getenv("TTLM_HOME", str( 7 | Path.home() / ".cache" / "TruthTorchLM")) 8 | os.makedirs(cache_dir, exist_ok=True) 9 | return cache_dir 10 | 11 | 12 | def set_cache_dir(new_dir): 13 | os.environ["TTLM_HOME"] = new_dir 14 | os.makedirs(new_dir, exist_ok=True) 15 | # print(f"Cache directory set to: {new_dir}") 16 | -------------------------------------------------------------------------------- /src/TruthTorchLM/evaluators/__init__.py: -------------------------------------------------------------------------------- 1 | from .correctness_evaluator import CorrectnessEvaluator 2 | from .rouge import ROUGE 3 | from .bleu import BLEU 4 | from .model_judge import ModelJudge 5 | from .eval_truth_method import evaluate_truth_method, get_metric_scores 6 | from .exact_match import ExactMatch 7 | 8 | __all__ = [ 9 | "CorrectnessEvaluator", 10 | "ROUGE", 11 | "BLEU", 12 | "ExactMatch", 13 | "evaluate_truth_method", 14 | "ModelJudge", 15 | "get_metric_scores", 16 | ] 17 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/claim_check_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .claim_check_method import ClaimCheckMethod 2 | from .question_answer_generation import QuestionAnswerGeneration 3 | from .question_generation import QuestionGeneration 4 | from .answer_claim_entailment import AnswerClaimEntailment 5 | from .naive_application import NaiveApplication 6 | 7 | 8 | __all__ = [ 9 | "ClaimCheckMethod", 10 | "QuestionAnswerGeneration", 11 | "QuestionGeneration", 12 | "AnswerClaimEntailment", 13 | "NaiveApplication", 14 | ] 15 | -------------------------------------------------------------------------------- /src/TruthTorchLM/normalizers/standard_normalizer.py: -------------------------------------------------------------------------------- 1 | from .normalizer import Normalizer 2 | from sklearn import preprocessing 3 | 4 | 5 | class StandardNormalizer(Normalizer): 6 | 7 | def __init__(self): 8 | self.standard_scaler = preprocessing.StandardScaler() 9 | self.standard_scaler.fit([[0], [1]]) # dummy fit 10 | 11 | def fit(self, generation_performance_scores: list, truth_values: list): 12 | self.standard_scaler.fit(truth_values.reshape(-1, 1)) 13 | 14 | def __call__(self, truth_value): 15 | return self.standard_scaler.transform([[truth_value]])[0][0] 16 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/decomposition_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .decomposition_method import DecompositionMethod 2 | from .unstructured_decomposition_api import UnstructuredDecompositionAPI 3 | from .unstructured_decomposition_local import UnstructuredDecompositionLocal 4 | from .structured_decomposition_api import StructuredDecompositionAPI 5 | from .structured_decomposition_local import StructuredDecompositionLocal 6 | 7 | 8 | __all__ = [ 9 | "UnstructuredDecompositionAPI", 10 | "UnstructuredDecompositionLocal", 11 | "StructuredDecompositionAPI", 12 | "StructuredDecompositionLocal", 13 | ] 14 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/decomposition_methods/decomposition_method.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class DecompositionMethod(ABC): 5 | def __init__(self): 6 | 7 | pass 8 | 9 | def __call__(self, input_text) -> list[str]: 10 | 11 | return self.decompose_facts(input_text) 12 | 13 | @abstractmethod 14 | def decompose_facts(self, input_text: str) -> list[str]: 15 | raise NotImplementedError("Subclasses must implement this method") 16 | 17 | @abstractmethod 18 | def __str__(self): 19 | raise NotImplementedError("Subclasses must implement this method") 20 | -------------------------------------------------------------------------------- /src/TruthTorchLM/evaluators/correctness_evaluator.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class CorrectnessEvaluator(ABC): 5 | 6 | def __init__(self): 7 | pass 8 | 9 | @abstractmethod 10 | def __call__( 11 | self, 12 | question_text: str, 13 | generated_text: str, 14 | ground_truth_text: list[str], 15 | context: str = "", 16 | seed: int = None, 17 | ) -> int: 18 | raise NotImplementedError("Subclasses must implement this method") 19 | 20 | @abstractmethod 21 | def __str__(self): 22 | raise NotImplementedError("Subclasses must implement this method") 23 | -------------------------------------------------------------------------------- /src/TruthTorchLM/normalizers/minmax_normalizer.py: -------------------------------------------------------------------------------- 1 | from .normalizer import Normalizer 2 | from sklearn import preprocessing 3 | 4 | 5 | class MinMaxNormalizer(Normalizer): 6 | 7 | def __init__(self, float_range=(0, 1)): 8 | self.min_max_scaler = preprocessing.MinMaxScaler( 9 | feature_range=float_range) 10 | self.min_max_scaler.fit([[0], [1]]) # dummy fit 11 | 12 | def fit(self, generation_performance_scores: list, truth_values: list): 13 | self.min_max_scaler.fit(truth_values.reshape(-1, 1)) 14 | 15 | def __call__(self, truth_value): 16 | return self.min_max_scaler.transform([[truth_value]])[0][0] 17 | -------------------------------------------------------------------------------- /src/TruthTorchLM/evaluators/exact_match.py: -------------------------------------------------------------------------------- 1 | from .correctness_evaluator import CorrectnessEvaluator 2 | 3 | class ExactMatch(CorrectnessEvaluator): 4 | def __init__(self): 5 | super().__init__() 6 | 7 | def __call__( 8 | self, 9 | question_text: str, 10 | generated_text: str, 11 | ground_truths: list[str], 12 | context: str = "", 13 | seed: int = None, 14 | ) -> bool: 15 | for i in range(len(ground_truths)): 16 | matched = generated_text.strip().lower() == ground_truths.strip().lower() 17 | if matched: 18 | return 1 19 | return 0 20 | 21 | def __str__(self): 22 | return "Exact Match" -------------------------------------------------------------------------------- /src/TruthTorchLM/scoring_methods/scoring_method.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class ScoringMethod(ABC): 5 | # Scoring methods should be implemented as subclasses of this class 6 | # forward method should be implemented in subclasses 7 | 8 | def __init__(self): 9 | pass 10 | 11 | @abstractmethod 12 | # tokens: list of tokens in the generated text 13 | # logprobs: list of log probabilities of each token in the generated text 14 | # returns a float score 15 | def __call__(self, logprobs: list[float]) -> float: 16 | raise NotImplementedError("Subclasses must implement this method") 17 | 18 | @abstractmethod 19 | def __str__(self): 20 | raise NotImplementedError("Subclasses must implement this method") 21 | -------------------------------------------------------------------------------- /src/TruthTorchLM/normalizers/isotonic_regression.py: -------------------------------------------------------------------------------- 1 | from .normalizer import Normalizer 2 | import sklearn.isotonic as isotonic 3 | 4 | 5 | class IsotonicRegression( 6 | Normalizer 7 | ): # see: https://scikit-learn.org/stable/modules/isotonic.html 8 | 9 | def __init__(self, y_min: float = 0.0, y_max: float = 1.0): 10 | self.iso_reg = isotonic.IsotonicRegression( 11 | y_min=y_min, y_max=y_max, out_of_bounds="clip" 12 | ) 13 | self.iso_reg.fit([0.5], [1]) # dummy fit 14 | 15 | def fit(self, generation_performance_scores: list, truth_values: list): 16 | self.iso_reg.fit(truth_values, generation_performance_scores) 17 | print( 18 | f"Calibrated with the following parameters: {self.iso_reg.get_params()}") 19 | 20 | def __call__(self, truth_value): 21 | return self.iso_reg.predict([truth_value])[0] 22 | -------------------------------------------------------------------------------- /src/TruthTorchLM/error_handler.py: -------------------------------------------------------------------------------- 1 | def handle_logprobs_error(func): 2 | """A decorator that catches 'logprobs' errors and returns 3 | a custom message or behavior.""" 4 | 5 | def wrapper(*args, **kwargs): 6 | try: 7 | return func(*args, **kwargs) 8 | except (KeyError, AttributeError) as e: 9 | # Check if 'logprobs' is involved in the error 10 | if "logprobs" in str(e): 11 | # Here you decide what to do instead of crashing 12 | # e.g. return a custom string, raise a custom exception, log an error, etc. 13 | raise ValueError( 14 | "This API model does not provide logprobs and cannot be used with truth methods that require logprobs. Please try a different API model or different truth method." 15 | ) 16 | 17 | raise e 18 | 19 | return wrapper 20 | -------------------------------------------------------------------------------- /src/TruthTorchLM/evaluators/bleu.py: -------------------------------------------------------------------------------- 1 | from .correctness_evaluator import CorrectnessEvaluator 2 | import evaluate 3 | 4 | 5 | class BLEU(CorrectnessEvaluator): 6 | def __init__(self, threshold: float = 0.5): 7 | super().__init__() 8 | self.threshold = threshold 9 | self.bleu = evaluate.load("bleu") 10 | 11 | def __call__( 12 | self, 13 | question_text: str, 14 | generated_text: str, 15 | ground_truths: list[str], 16 | context: str = "", 17 | seed: int = None, 18 | ) -> bool: 19 | for i in range(len(ground_truths)): 20 | bleu_results = self.bleu.compute( 21 | predictions=[generated_text], references=[ground_truths[i]] 22 | ) 23 | if bleu_results["bleu"] > self.threshold: 24 | return 1 25 | return 0 26 | 27 | def __str__(self): 28 | return f"BLEU with threshold {self.threshold}" 29 | -------------------------------------------------------------------------------- /src/TruthTorchLM/evaluators/rouge.py: -------------------------------------------------------------------------------- 1 | from .correctness_evaluator import CorrectnessEvaluator 2 | import evaluate 3 | 4 | 5 | class ROUGE(CorrectnessEvaluator): 6 | def __init__(self, threshold: float = 0.5, rouge_type: str = "rougeL"): 7 | super().__init__() 8 | self.threshold = threshold 9 | self.rouge = evaluate.load("rouge") 10 | self.rouge_type = rouge_type 11 | 12 | def __call__( 13 | self, 14 | question_text: str, 15 | generated_text: str, 16 | ground_truths: list[str], 17 | context: str = "", 18 | seed: int = None, 19 | ) -> bool: 20 | for i in range(len(ground_truths)): 21 | rouge_results = self.rouge.compute( 22 | predictions=[generated_text], references=[ground_truths[i]] 23 | ) 24 | if rouge_results[self.rouge_type] > self.threshold: 25 | return 1 26 | return 0 27 | 28 | def __str__(self): 29 | return f"ROUGE with threshold {self.threshold} and type {self.rouge_type}" 30 | -------------------------------------------------------------------------------- /src/TruthTorchLM/__init__.py: -------------------------------------------------------------------------------- 1 | from TruthTorchLM import utils # TODO do we really need to import this? 2 | from TruthTorchLM import long_form_generation 3 | from TruthTorchLM import normalizers 4 | from .environment import * 5 | from .availability import AVAILABLE_DATASETS, AVAILABLE_EVALUATION_METRICS 6 | from .templates import * 7 | from .evaluators import evaluate_truth_method 8 | from TruthTorchLM import evaluators 9 | from .calibration import calibrate_truth_method 10 | from .generation import generate_with_truth_value 11 | from TruthTorchLM import truth_methods 12 | from TruthTorchLM import scoring_methods 13 | from .truth_methods.truth_method import TruthMethod 14 | import warnings 15 | 16 | warnings.filterwarnings( 17 | "ignore", 18 | message=".*parse_raw.*deprecated.*", 19 | category=DeprecationWarning 20 | ) 21 | 22 | warnings.filterwarnings( 23 | "ignore", 24 | message=".*load_str_bytes.*deprecated.*", 25 | category=DeprecationWarning 26 | ) 27 | 28 | 29 | # Suppress specific warnings in the library 30 | #warnings.filterwarnings("once") 31 | 32 | 33 | # __all__ = ['generate_with_truth_value'] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yavuz Faruk Bakman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/TruthTorchLM/normalizers/sigmoid_normalizer.py: -------------------------------------------------------------------------------- 1 | from .normalizer import Normalizer 2 | from TruthTorchLM.utils.calibration_utils import f1_picker 3 | from TruthTorchLM.utils.common_utils import sigmoid_normalization 4 | import numpy as np 5 | 6 | 7 | class SigmoidNormalizer(Normalizer): 8 | 9 | def __init__( 10 | self, threshold: float = 0.0, std: float = 1.0, threshold_picker=f1_picker 11 | ): 12 | self.threshold_picker = threshold_picker 13 | self.threshold = threshold 14 | self.std = std 15 | 16 | def fit(self, generation_performance_scores: list, truth_values: list): 17 | self.threshold = self.threshold_picker( 18 | generation_performance_scores, truth_values 19 | ) 20 | self.std = np.std(truth_values) 21 | print( 22 | "Calibrated with the following parameters: threshold =", 23 | self.threshold, 24 | "std =", 25 | self.std, 26 | ) 27 | 28 | def __call__(self, truth_value): 29 | return sigmoid_normalization(truth_value, self.threshold, self.std) 30 | 31 | def set_threshold(self, threshold): 32 | self.threshold = threshold 33 | 34 | def set_std(self, std): 35 | self.std = std 36 | 37 | def get_threshold(self): 38 | return self.threshold 39 | 40 | def get_std(self): 41 | return self.std 42 | -------------------------------------------------------------------------------- /src/TruthTorchLM/availability.py: -------------------------------------------------------------------------------- 1 | # AVAILABLE_API_MODELS = ['gpt-4o', 'gpt-4o-2024-05-13', 'gpt-4o-2024-08-06', 'chatgpt-4o-latest', 'gpt-4o-mini', 'gpt-4o-mini-2024-07-18', 2 | # 'gpt-4-turbo','gpt-4-turbo-2024-04-09', 'gpt-4-turbo-preview', 'gpt-4-0125-preview', 'gpt-4-1106-preview', 'gpt-4', 3 | # 'gpt-4-0613', 'gpt-4-0314', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo', 'gpt-3.5-turbo-1106', 'gpt-3.5-turbo-instruct', 4 | # 'claude-3-5-sonnet-20240620','claude-3-haiku-20240307', 'claude-3-opus-20240229', 'claude-3-5-sonnet-20240620', 'claude-3-sonnet-20240229',] 5 | 6 | # PROB_AVAILABLE_API_MODELS = ['gpt-4o', 'gpt-4o-2024-05-13', 'gpt-4o-2024-08-06', 'chatgpt-4o-latest', 'gpt-4o-mini', 'gpt-4o-mini-2024-07-18', 7 | # 'gpt-4-turbo','gpt-4-turbo-2024-04-09', 'gpt-4-turbo-preview', 'gpt-4-0125-preview', 'gpt-4-1106-preview', 'gpt-4', 8 | # 'gpt-4-0613', 'gpt-4-0314', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo', 'gpt-3.5-turbo-1106', 'gpt-3.5-turbo-instruct'] 9 | 10 | # ACTIVATION_AVAILABLE_API_MODELS = [] 11 | 12 | AVAILABLE_DATASETS = ["trivia_qa", "gsm8k", 13 | "natural_qa", "pop_qa", "simple_qa", "wikipedia_factual", "narrative_qa", "web_questions"] 14 | LONG_FORM_AVAILABLE_DATASETS = ["longfact_concepts", "longfact_objects"] 15 | 16 | AVAILABLE_EVALUATION_METRICS = [ 17 | "auroc", 18 | "auprc", 19 | "auarc", 20 | "accuracy", 21 | "f1", 22 | "precision", 23 | "recall", 24 | "prr", 25 | ] 26 | -------------------------------------------------------------------------------- /src/TruthTorchLM/normalizers/normalizer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import numpy as np 3 | 4 | 5 | class Normalizer(ABC): 6 | 7 | def __init__(self): 8 | pass 9 | 10 | def calibrate( 11 | self, generation_performance_scores: list, truth_values: list 12 | ): # -1 means invalid performance score, remove nan values from truth_values 13 | pos_inf_replacement = 1e10 14 | neg_inf_replacement = -1e10 15 | generation_performance_scores = np.array(generation_performance_scores) 16 | truth_values = np.array(truth_values) 17 | truth_values = truth_values[generation_performance_scores != -1] 18 | generation_performance_scores = generation_performance_scores[ 19 | generation_performance_scores != -1 20 | ] 21 | truth_values[np.isnan(truth_values)] = 0.0 22 | truth_values[truth_values == np.inf] = pos_inf_replacement 23 | truth_values[truth_values == -np.inf] = neg_inf_replacement 24 | 25 | self.fit( 26 | generation_performance_scores=generation_performance_scores, 27 | truth_values=truth_values, 28 | ) 29 | 30 | def __call__(self, truth_value: float): 31 | raise NotImplementedError("Subclasses must implement this method") 32 | 33 | @abstractmethod 34 | def fit(self, generation_performance_scores: list, truth_values: list): 35 | raise NotImplementedError("Subclasses must implement this method") 36 | 37 | # @abstractmethod 38 | # def transform(self, truth_values:list): 39 | # raise NotImplementedError("Subclasses must implement this method") 40 | 41 | def __str__(self): 42 | return f"{self.__class__.__name__} with {str(self.__dict__)}" 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | requirements = ["aiohttp", 4 | "evaluate", 5 | "instructor", 6 | "litellm", 7 | "nest_asyncio", 8 | "numpy", 9 | "outlines", 10 | "pandas", 11 | "pydantic", 12 | "PyYAML", 13 | "Requests", 14 | "scikit_learn", 15 | "scipy", 16 | "sentence_transformers", 17 | "termcolor", 18 | "torch", 19 | "tqdm", 20 | "transformers", 21 | "absl-py", 22 | "nltk", 23 | "rouge_score", 24 | "wandb", 25 | "sentencepiece", 26 | "accelerate>=0.26.0"] 27 | 28 | 29 | setup( 30 | name="TruthTorchLM", # Your package name 31 | version="0.1.17", # Package version 32 | author="Yavuz Faruk Bakman", 33 | author_email="ybakman@usc.edu", 34 | description="TruthTorchLM is an open-source library designed to assess truthfulness in language models' outputs. The library integrates state-of-the-art methods, offers comprehensive benchmarking tools across various tasks, and enables seamless integration with popular frameworks like Huggingface and LiteLLM.", 35 | long_description=open("README.md").read(), 36 | long_description_content_type="text/markdown", 37 | package_dir={"": "src"}, # Maps the base package directory 38 | # Automatically find and include all packages 39 | packages=find_packages(where="src"), 40 | install_requires=requirements, # List of dependencies 41 | python_requires=">=3.10", # Minimum Python version 42 | ) 43 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .truth_method import TruthMethod 2 | from .entropy import Entropy 3 | from .confidence import Confidence 4 | from .semantic_entropy import SemanticEntropy 5 | from .google_search_check import GoogleSearchCheck 6 | from .p_true import PTrue 7 | from .eccentricity_uncertainty import EccentricityUncertainty 8 | from .matrix_degree_uncertainty import MatrixDegreeUncertainty 9 | from .num_semantic_set_uncertainty import NumSemanticSetUncertainty 10 | from .sum_eigen_uncertainty import SumEigenUncertainty 11 | from .self_detection import SelfDetection 12 | from .inside import Inside 13 | from .sentSAR import SentSAR 14 | from .tokenSAR import TokenSAR 15 | from .lars import LARS 16 | from .kernel_language_entropy import KernelLanguageEntropy 17 | from .cross_examination import CrossExamination 18 | from .saplma import SAPLMA 19 | from .verbalized_confidence import VerbalizedConfidence 20 | from .eccentricity_confidence import EccentricityConfidence 21 | from .matrix_degree_confidence import MatrixDegreeConfidence 22 | from .attention_score import AttentionScore 23 | from .mars import MARS 24 | from .multi_llm_collab import MultiLLMCollab 25 | from .directional_entailment_graph import DirectionalEntailmentGraph 26 | from .SAR import SAR 27 | from .mini_check_method import MiniCheckMethod 28 | from .context_check import ContextCheck 29 | 30 | __all__ = [ 31 | "Entropy", 32 | "Confidence", 33 | "TruthMethod", 34 | "SemanticEntropy", 35 | "PTrue", 36 | "Inside", 37 | "SentSAR", 38 | "GoogleSearchCheck", 39 | "EccentricityUncertainty", 40 | "MatrixDegreeUncertainty", 41 | "NumSemanticSetUncertainty", 42 | "SumEigenUncertainty", 43 | "SelfDetection", 44 | "TokenSAR", 45 | "LARS", 46 | "KernelLanguageEntropy", 47 | "CrossExamination", 48 | "SAPLMA", 49 | "VerbalizedConfidence", 50 | "EccentricityConfidence", 51 | "MatrixDegreeConfidence", 52 | "AttentionScore", 53 | "MARS", 54 | "MultiLLMCollab", 55 | "DirectionalEntailmentGraph", 56 | "SAR", 57 | "MiniCheckMethod", 58 | "ContextCheck", 59 | ] 60 | -------------------------------------------------------------------------------- /src/TruthTorchLM/calibration.py: -------------------------------------------------------------------------------- 1 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 2 | from typing import Union 3 | from TruthTorchLM.truth_methods import TruthMethod 4 | from TruthTorchLM.evaluators import CorrectnessEvaluator, ExactMatch 5 | from TruthTorchLM.templates import DEFAULT_SYSTEM_BENCHMARK_PROMPT, DEFAULT_USER_PROMPT 6 | from TruthTorchLM.utils.dataset_utils import get_dataset 7 | from TruthTorchLM.utils.eval_utils import run_over_dataset 8 | import numpy as np 9 | 10 | 11 | def calibrate_truth_method( 12 | dataset: Union[str, list], 13 | model: Union[str, PreTrainedModel], 14 | truth_methods: list[TruthMethod], 15 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 16 | correctness_evaluator: CorrectnessEvaluator = ExactMatch(), 17 | size_of_data: float = 1.0, 18 | previous_context: list = [ 19 | {"role": "system", "content": DEFAULT_SYSTEM_BENCHMARK_PROMPT} 20 | ], 21 | user_prompt: str = DEFAULT_USER_PROMPT, 22 | seed: int = 0, 23 | return_method_details: bool = False, 24 | split="train", 25 | **kwargs, 26 | ): 27 | 28 | dataset = get_dataset( 29 | dataset, size_of_data=size_of_data, seed=seed, split=split) 30 | 31 | output_dict = run_over_dataset( 32 | dataset, 33 | model, 34 | truth_methods, 35 | tokenizer=tokenizer, 36 | correctness_evaluator=correctness_evaluator, 37 | previous_context=previous_context, 38 | user_prompt=user_prompt, 39 | seed=seed, 40 | return_method_details=return_method_details, 41 | **kwargs, 42 | ) 43 | 44 | for i, truth_method in enumerate(truth_methods): 45 | truth_values = output_dict[f"truth_method_{i}"]["truth_values"] 46 | truth_values = np.array(truth_values) 47 | truth_values[np.isnan(truth_values)] = 0 48 | correctness = output_dict["generations_correctness"] 49 | # if generation_correctness is -1, it means that the model didn't attempt to generate an answer, remove those from the evaluation 50 | truth_method.normalizer.calibrate( 51 | generation_performance_scores=correctness, truth_values=truth_values 52 | ) 53 | return output_dict 54 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package to PyPI when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | release-build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.x" 28 | 29 | - name: Build release distributions 30 | run: | 31 | # NOTE: put your own distribution build steps here. 32 | python -m pip install build 33 | python -m build 34 | 35 | - name: Upload distributions 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: release-dists 39 | path: dist/ 40 | 41 | pypi-publish: 42 | runs-on: ubuntu-latest 43 | needs: 44 | - release-build 45 | permissions: 46 | # IMPORTANT: this permission is mandatory for trusted publishing 47 | id-token: write 48 | 49 | # Dedicated environments with protections for publishing are strongly recommended. 50 | # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules 51 | environment: 52 | name: pypi 53 | # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: 54 | # url: https://pypi.org/p/YOURPROJECT 55 | # 56 | # ALTERNATIVE: if your GitHub Release name is the PyPI project version string 57 | # ALTERNATIVE: exactly, uncomment the following line instead: 58 | # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} 59 | 60 | steps: 61 | - name: Retrieve release distributions 62 | uses: actions/download-artifact@v4 63 | with: 64 | name: release-dists 65 | path: dist/ 66 | 67 | - name: Publish release distributions to PyPI 68 | uses: pypa/gh-action-pypi-publish@release/v1 69 | with: 70 | packages-dir: dist/ 71 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/attention_score.py: -------------------------------------------------------------------------------- 1 | from .truth_method import TruthMethod 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | 5 | import torch 6 | import numpy as np 7 | 8 | 9 | class AttentionScore(TruthMethod): 10 | def __init__(self, layer_index: int = -1): # normalization, 11 | super().__init__() 12 | self.layer_index = layer_index 13 | 14 | def forward_hf_local( 15 | self, 16 | model: PreTrainedModel, 17 | input_text: str, 18 | generated_text: str, 19 | question: str, 20 | all_ids: Union[list, torch.Tensor], 21 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 22 | generation_seed=None, 23 | sampled_generations_dict: dict = None, 24 | messages: list = [], 25 | context: str = "", 26 | **kwargs 27 | ): 28 | 29 | model_output = all_ids.to(model.device) 30 | with torch.no_grad(): 31 | output = model(model_output, output_attentions=True) 32 | target_attention = output.attentions[self.layer_index].cpu() 33 | del output 34 | scores = [] 35 | # for each head 36 | for head_index in range(target_attention.shape[1]): 37 | attention = target_attention[0][ 38 | head_index 39 | ] # this values are after softmax 40 | diag_entries = torch.diagonal(attention) 41 | log_diag_entries = torch.log(diag_entries) 42 | score = log_diag_entries.sum().item() 43 | score = score / len(diag_entries) 44 | scores.append(score) 45 | result = np.mean(scores) 46 | 47 | return { 48 | "truth_value": result, 49 | "attention_score": result, 50 | } # we shouldn't return generated text. remove it from the output format 51 | 52 | def forward_api( 53 | self, 54 | model: str, 55 | messages: list, 56 | generated_text: str, 57 | question: str, 58 | generation_seed=None, 59 | sampled_generations_dict: dict = None, 60 | logprobs: list = None, 61 | generated_tokens: list = None, 62 | context: str = "", 63 | **kwargs 64 | ): 65 | 66 | raise ValueError( 67 | "Attention Score method cannot be used with black-box API models since it requires access to activations." 68 | ) 69 | 70 | return { 71 | "truth_value": 0 72 | } # this output format should be same for all truth methods 73 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/decomposition_methods/unstructured_decomposition_api.py: -------------------------------------------------------------------------------- 1 | from .decomposition_method import DecompositionMethod 2 | from ..templates import UNSTRUCTURED_DECOMPOSITION_INSTRUCTION 3 | 4 | from copy import deepcopy 5 | from typing import Callable 6 | from litellm import completion 7 | 8 | 9 | def default_output_parser(text: str): 10 | claims = text.split("\n-") 11 | claims = [claim.strip() for claim in claims if claim.strip()] 12 | return claims 13 | 14 | 15 | class UnstructuredDecompositionAPI(DecompositionMethod): 16 | def __init__( 17 | self, 18 | model: str, 19 | instruction: list = UNSTRUCTURED_DECOMPOSITION_INSTRUCTION, 20 | decomposition_depth: int = 1, 21 | output_parser: Callable[[str], list[str]] = default_output_parser, 22 | split_by_paragraphs=True, 23 | **kwargs 24 | ): 25 | super().__init__() 26 | 27 | self.model = model 28 | self.instruction = instruction 29 | self.decomposition_depth = decomposition_depth 30 | self.split_by_paragraphs = split_by_paragraphs 31 | self.output_parser = output_parser 32 | self.kwargs = kwargs 33 | 34 | if "seed" not in kwargs: 35 | self.kwargs["seed"] = 42 36 | 37 | def decompose_facts(self, input_text: str): 38 | 39 | messages = deepcopy(self.instruction) 40 | for item in messages: 41 | item["content"] = item["content"].format(TEXT=input_text) 42 | 43 | response = completion( 44 | model=self.model, messages=messages, **self.kwargs) 45 | generated_text = "\n" + response.choices[0].message["content"] 46 | claims = self.output_parser(generated_text) 47 | 48 | return claims 49 | 50 | def __call__(self, input_text) -> list[str]: 51 | 52 | if self.split_by_paragraphs: 53 | paragraphs = [ 54 | paragraph.strip() 55 | for paragraph in input_text.split("\n") 56 | if paragraph.strip() 57 | ] 58 | else: 59 | paragraphs = [input_text] 60 | all_claims = [] 61 | for paragraph in paragraphs: 62 | claims = self.decompose_facts(paragraph) 63 | for _ in range(self.decomposition_depth - 1): 64 | temp_claims = [] 65 | for claim in claims: 66 | temp_claims.extend(self.decompose_facts(claim)) 67 | claims = temp_claims 68 | all_claims.extend(claims) 69 | return all_claims 70 | 71 | def __str__(self): 72 | return ( 73 | "Decomposition by using LLMs method with " 74 | + self.model 75 | + " model. Chat template is:\n" 76 | + str(self.instruction) 77 | + "\n Sentence seperator is: " 78 | + self.sentence_seperator 79 | ) 80 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/confidence.py: -------------------------------------------------------------------------------- 1 | from .truth_method import TruthMethod 2 | from TruthTorchLM.scoring_methods import ScoringMethod, LengthNormalizedScoring 3 | from typing import Union 4 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 5 | import torch 6 | from TruthTorchLM.error_handler import handle_logprobs_error 7 | 8 | 9 | class Confidence(TruthMethod): 10 | 11 | REQUIRES_LOGPROBS = True 12 | 13 | def __init__( 14 | self, scoring_function: ScoringMethod = LengthNormalizedScoring() 15 | ): # normalization, 16 | super().__init__() 17 | self.scoring_function = scoring_function 18 | 19 | def forward_hf_local( 20 | self, 21 | model: PreTrainedModel, 22 | input_text: str, 23 | generated_text: str, 24 | question: str, 25 | all_ids: Union[list, torch.Tensor], 26 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 27 | generation_seed=None, 28 | sampled_generations_dict: dict = None, 29 | messages: list = [], 30 | context: str = "", 31 | **kwargs 32 | ): 33 | 34 | input_ids = tokenizer.encode( 35 | input_text, return_tensors="pt").to(model.device) 36 | model_output = all_ids.to(model.device) 37 | 38 | with torch.no_grad(): 39 | outputs = model(model_output) 40 | logits = outputs.logits # Logits for each token in the input 41 | 42 | # Calculate probabilities from logits 43 | logprobs = torch.log_softmax( 44 | logits, dim=-1) # logprobs for each token 45 | logprobs = logprobs[ 46 | 0, len(input_ids[0]) - 1: -1, : 47 | ] # logprobs for each token in the generated text 48 | logprobs = torch.gather( 49 | logprobs, dim=1, index=model_output[0][len(input_ids[0]):].view(-1, 1) 50 | ) # logprobs for each token in the generated text 51 | logprobs = logprobs.view(-1).tolist() # convert to list 52 | 53 | score = self.scoring_function(logprobs) 54 | score = score 55 | generated_text = generated_text 56 | 57 | return { 58 | "truth_value": score, 59 | "generated_text": generated_text, 60 | } # we shouldn't return generated text. remove it from the output format 61 | 62 | @handle_logprobs_error 63 | def forward_api( 64 | self, 65 | model: str, 66 | messages: list, 67 | generated_text: str, 68 | question: str, 69 | generation_seed=None, 70 | sampled_generations_dict: dict = None, 71 | logprobs: list = None, 72 | generated_tokens: list = None, 73 | context: str = "", 74 | **kwargs 75 | ): 76 | 77 | score = self.scoring_function(logprobs) 78 | 79 | return { 80 | "truth_value": score, 81 | "generated_text": generated_text, 82 | } # we shouldn't return generated text. remove it from the output format 83 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/decomposition_methods/structured_decomposition_api.py: -------------------------------------------------------------------------------- 1 | from .decomposition_method import DecompositionMethod 2 | from ..templates import DECOMPOSITION_INSTRUCTION, DECOMPOSITION_INSTRUCTION_GRANULAR 3 | 4 | import instructor 5 | from copy import deepcopy 6 | from litellm import completion 7 | from pydantic import BaseModel 8 | 9 | 10 | class Claims(BaseModel): 11 | claims: list[str] 12 | 13 | 14 | class StructuredDecompositionAPI(DecompositionMethod): 15 | def __init__( 16 | self, 17 | model: str, 18 | instruction: list = DECOMPOSITION_INSTRUCTION, 19 | instruction_for_granular: list = DECOMPOSITION_INSTRUCTION_GRANULAR, 20 | decomposition_depth: int = 1, 21 | split_by_paragraphs=True, 22 | **kwargs 23 | ): 24 | super().__init__() 25 | 26 | self.model = model 27 | self.instruction = instruction 28 | self.instruction_for_granular = instruction_for_granular 29 | self.decomposition_depth = decomposition_depth 30 | self.split_by_paragraphs = split_by_paragraphs 31 | self.kwargs = kwargs 32 | self.client = instructor.from_litellm(completion) 33 | 34 | if "seed" not in kwargs: 35 | self.kwargs["seed"] = 42 36 | 37 | def decompose_facts(self, input_text: str, level: int = 1): 38 | 39 | messages = deepcopy( 40 | self.instruction if level == 1 else self.instruction_for_granular 41 | ) 42 | for item in messages: 43 | item["content"] = item["content"].format(TEXT=input_text) 44 | 45 | resp = self.client.chat.completions.create( 46 | model=self.model, messages=messages, response_model=Claims, **self.kwargs 47 | ) 48 | return resp.claims 49 | 50 | def __call__(self, input_text) -> list[str]: 51 | 52 | if self.split_by_paragraphs: 53 | paragraphs = [ 54 | paragraph.strip() 55 | for paragraph in input_text.split("\n") 56 | if paragraph.strip() 57 | ] 58 | else: 59 | paragraphs = [input_text] 60 | all_claims = [] 61 | for paragraph in paragraphs: 62 | claims = self.decompose_facts(paragraph, level=1) 63 | for d in range(1, self.decomposition_depth): 64 | temp_claims = [] 65 | for claim in claims: 66 | temp_claims.extend( 67 | self.decompose_facts(claim, level=d + 1)) 68 | claims = temp_claims 69 | all_claims.extend(claims) 70 | return all_claims 71 | 72 | def __str__(self): 73 | return ( 74 | "Decomposition by using LLMs with API calls.\nModel: " 75 | + self.model 76 | + "\nOutput structure is enforced with 'instructor' library.\nInstruction for the first level of decomposition is:\n" 77 | + str(self.instruction) 78 | + "\nInstruction for the granular decomposition (level 2 and higher) is:\n" 79 | + str(self.instruction_for_granular) 80 | ) 81 | -------------------------------------------------------------------------------- /src/TruthTorchLM/evaluators/eval_truth_method.py: -------------------------------------------------------------------------------- 1 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 2 | from typing import Union 3 | from TruthTorchLM.truth_methods import TruthMethod 4 | from .correctness_evaluator import CorrectnessEvaluator 5 | from .exact_match import ExactMatch 6 | from TruthTorchLM.availability import AVAILABLE_EVALUATION_METRICS 7 | from TruthTorchLM.templates import DEFAULT_SYSTEM_BENCHMARK_PROMPT, DEFAULT_USER_PROMPT 8 | from TruthTorchLM.utils.dataset_utils import get_dataset 9 | from TruthTorchLM.utils.eval_utils import metric_score, run_over_dataset 10 | import wandb 11 | 12 | 13 | def evaluate_truth_method( 14 | dataset: Union[str, list], 15 | model: Union[str, PreTrainedModel], 16 | truth_methods: list[TruthMethod], 17 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 18 | eval_metrics: list[str] = ["auroc"], 19 | correctness_evaluator: CorrectnessEvaluator = ExactMatch(), 20 | size_of_data=1.0, 21 | previous_context: list = [ 22 | {"role": "system", "content": DEFAULT_SYSTEM_BENCHMARK_PROMPT} 23 | ], 24 | user_prompt: str = DEFAULT_USER_PROMPT, 25 | seed: int = 0, 26 | return_method_details: bool = False, 27 | batch_generation=True, 28 | add_generation_prompt=True, 29 | continue_final_message=False, 30 | split="test", 31 | **kwargs, 32 | ): 33 | 34 | dataset = get_dataset( 35 | dataset, size_of_data=size_of_data, seed=seed, split=split) 36 | 37 | for eval_metric in eval_metrics: 38 | if eval_metric not in AVAILABLE_EVALUATION_METRICS: 39 | raise ValueError( 40 | f"Evaluation metric {eval_metric} is not available. Available evaluation metrics are: {AVAILABLE_EVALUATION_METRICS}" 41 | ) 42 | 43 | output_dict = run_over_dataset( 44 | dataset, 45 | model, 46 | truth_methods, 47 | tokenizer=tokenizer, 48 | correctness_evaluator=correctness_evaluator, 49 | previous_context=previous_context, 50 | user_prompt=user_prompt, 51 | seed=seed, 52 | return_method_details=return_method_details, 53 | batch_generation=batch_generation, 54 | add_generation_prompt=add_generation_prompt, 55 | continue_final_message=continue_final_message, 56 | **kwargs, 57 | ) 58 | 59 | eval_list = get_metric_scores( 60 | output_dict=output_dict, eval_metrics=eval_metrics, seed=seed 61 | ) 62 | 63 | return {"eval_list": eval_list, "output_dict": output_dict} 64 | 65 | 66 | def get_metric_scores(output_dict: dict, eval_metrics: list[str], seed: int = 0): 67 | truth_methods = output_dict["truth_methods"] 68 | eval_list = [] 69 | for i in range(len(truth_methods)): 70 | eval_dict = metric_score( 71 | eval_metrics, 72 | output_dict["generations_correctness"], 73 | output_dict[f"truth_method_{i}"]["truth_values"], 74 | output_dict[f"truth_method_{i}"]["normalized_truth_values"], 75 | seed=seed, 76 | ) 77 | eval_list.append(eval_dict) 78 | return eval_list 79 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/claim_check_methods/claim_check_method.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import random 3 | from typing import Union 4 | from abc import ABC, abstractmethod 5 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 6 | 7 | 8 | class ClaimCheckMethod(ABC): 9 | def __init__(self): 10 | pass 11 | 12 | def __call__( 13 | self, 14 | model: Union[PreTrainedModel, str], 15 | input_text: str = "", 16 | generated_text: str = "", 17 | question: str = "", 18 | claim: str = "", 19 | text_so_far: str = "", 20 | all_ids: Union[list, torch.Tensor] = None, 21 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 22 | generation_seed=None, 23 | messages: list = [], 24 | context:str = "", 25 | **kwargs 26 | ) -> dict: 27 | if generation_seed is not None: 28 | torch.manual_seed(generation_seed) 29 | random.seed(generation_seed) 30 | if isinstance(model, str): 31 | output_dict = self.check_claim_api( 32 | model=model, 33 | messages=messages, 34 | generated_text=generated_text, 35 | question=question, 36 | claim=claim, 37 | text_so_far=text_so_far, 38 | generation_seed=generation_seed, 39 | context=context, 40 | **kwargs 41 | ) 42 | else: 43 | output_dict = self.check_claim_local( 44 | model=model, 45 | input_text=input_text, 46 | generated_text=generated_text, 47 | question=question, 48 | claim=claim, 49 | text_so_far=text_so_far, 50 | all_ids=all_ids, 51 | tokenizer=tokenizer, 52 | generation_seed=generation_seed, 53 | messages=messages, 54 | context=context, 55 | **kwargs 56 | ) 57 | 58 | return output_dict 59 | 60 | @abstractmethod 61 | def check_claim_local( 62 | self, 63 | model: PreTrainedModel, 64 | input_text: str, 65 | generated_text: str, 66 | question: str, 67 | claim: str, 68 | text_so_far: str, 69 | all_ids: Union[list, torch.Tensor], 70 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 71 | generation_seed=None, 72 | messages: list = [], 73 | context:str = "", 74 | **kwargs 75 | ) -> dict: 76 | raise NotImplementedError("Subclasses must implement this method") 77 | 78 | @abstractmethod 79 | def check_claim_api( 80 | self, 81 | model: str, 82 | messages: list, 83 | generated_text: str, 84 | question: str, 85 | claim: str, 86 | text_so_far: str, 87 | generation_seed=None, 88 | context:str = "", 89 | **kwargs 90 | ) -> dict: 91 | raise NotImplementedError("Subclasses must implement this method") 92 | 93 | @abstractmethod 94 | def __str__(self): 95 | raise NotImplementedError("Subclasses must implement this method") 96 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/mini_check_method.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | import copy 4 | from .truth_method import TruthMethod 5 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast, AutoModelForCausalLM, AutoTokenizer 6 | import contextlib 7 | import io 8 | 9 | 10 | # paper link: https://arxiv.org/abs/2404.10774 11 | # hugginface link: https://huggingface.co/bespokelabs/Bespoke-MiniCheck-7B 12 | 13 | """ 14 | Input: Question (question_context), Answer (generated_text), Document (context) 15 | Output: Support score in [0, 1] 16 | """ 17 | 18 | 19 | class MiniCheckMethod(TruthMethod): 20 | #REQUIRES_NORMALIZATION = True 21 | 22 | def __init__(self, minicheck_model:str = 'flan-t5-large'): 23 | super().__init__() 24 | try: 25 | from minicheck.minicheck import MiniCheck 26 | except ImportError: 27 | raise ImportError("minicheck is not installed. Please install it using 'pip install minicheck[llm]@git+https://github.com/Liyan06/MiniCheck.git@main' ") 28 | if minicheck_model not in ['roberta-large', 'deberta-v3-large', 'flan-t5-large', 'Bespoke-MiniCheck-7B']: 29 | raise ValueError("Available Minicheck models are one of: 'roberta-large', 'deberta-v3-large', 'flan-t5-large', 'Bespoke-MiniCheck-7B'") 30 | else: 31 | self.minicheck_model = MiniCheck(model_name=minicheck_model) 32 | 33 | 34 | 35 | 36 | def forward_hf_local( 37 | self, 38 | model: PreTrainedModel, 39 | input_text: str, 40 | generated_text: str, 41 | question: str, 42 | all_ids: Union[list, torch.Tensor], 43 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 44 | generation_seed=None, 45 | sampled_generations_dict: dict = None, 46 | messages: list = [], 47 | context: str = "", 48 | **kwargs, 49 | ): 50 | if context == "": 51 | raise ValueError("Context is required for MiniCheck method") 52 | truth_score, truth_label = self.minicheck(context, generated_text) 53 | return {'truth_value': truth_score, 'binary_prediction': truth_label} 54 | 55 | 56 | def forward_api( 57 | self, 58 | model: str, 59 | messages: list, 60 | generated_text: str, 61 | question: str, 62 | generation_seed=None, 63 | sampled_generations_dict: dict = None, 64 | logprobs: list = None, 65 | generated_tokens: list = None, 66 | context: str = "", 67 | **kwargs, 68 | ): 69 | if context == "": 70 | raise ValueError("Context is required for MiniCheck method") 71 | 72 | truth_score, truth_label = self.minicheck(context, generated_text) 73 | return {'truth_value': truth_score, 'truth_label': truth_label} 74 | 75 | 76 | def minicheck(self, context, generated_text): 77 | # Loading Minicheck model 78 | with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): 79 | pred_label, raw_prob, _, _ = self.minicheck_model.score(docs=[context], claims=[generated_text]) 80 | return raw_prob[0], pred_label[0] -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/decomposition_methods/structured_decomposition_local.py: -------------------------------------------------------------------------------- 1 | from ..templates import DECOMPOSITION_INSTRUCTION, DECOMPOSITION_INSTRUCTION_GRANULAR 2 | from .decomposition_method import DecompositionMethod 3 | from TruthTorchLM.utils.common_utils import fix_tokenizer_chat 4 | 5 | import outlines 6 | from typing import Union 7 | from copy import deepcopy 8 | from pydantic import BaseModel 9 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 10 | 11 | 12 | class Claims(BaseModel): 13 | claims: list[str] 14 | 15 | 16 | class StructuredDecompositionLocal(DecompositionMethod): 17 | def __init__( 18 | self, 19 | model: PreTrainedModel, 20 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], 21 | instruction: list = DECOMPOSITION_INSTRUCTION, 22 | instruction_for_granular: list = DECOMPOSITION_INSTRUCTION_GRANULAR, 23 | decomposition_depth: int = 1, 24 | seed: int = 715, 25 | split_by_paragraphs=True, 26 | ): 27 | super().__init__() 28 | 29 | outlines.disable_cache() 30 | outlines_model = outlines.models.Transformers(model, tokenizer) 31 | self.generator = outlines.generate.json(outlines_model, Claims) 32 | self.tokenizer = tokenizer 33 | self.instruction = instruction 34 | self.instruction_for_granular = instruction_for_granular 35 | self.decomposition_depth = decomposition_depth 36 | self.split_by_paragraphs = split_by_paragraphs 37 | self.seed = seed 38 | self.model_name = model.name_or_path 39 | 40 | def decompose_facts(self, input_text: str, level: int = 1): 41 | 42 | messages = deepcopy( 43 | self.instruction if level == 1 else self.instruction_for_granular 44 | ) 45 | 46 | for item in messages: 47 | item["content"] = item["content"].format(TEXT=input_text) 48 | self.tokenizer, messages = fix_tokenizer_chat(self.tokenizer, messages) 49 | text = self.tokenizer.apply_chat_template( 50 | messages, 51 | tokenize=False, 52 | add_generation_prompt=True, 53 | continue_final_message=False, 54 | ) 55 | resp = self.generator(text, seed=self.seed) 56 | 57 | return resp.claims 58 | 59 | def __call__(self, input_text) -> list[str]: 60 | 61 | if self.split_by_paragraphs: 62 | paragraphs = [ 63 | paragraph.strip() 64 | for paragraph in input_text.split("\n") 65 | if paragraph.strip() 66 | ] 67 | else: 68 | paragraphs = [input_text] 69 | all_claims = [] 70 | for paragraph in paragraphs: 71 | claims = self.decompose_facts(paragraph, level=1) 72 | for d in range(1, self.decomposition_depth): 73 | temp_claims = [] 74 | for claim in claims: 75 | temp_claims.extend( 76 | self.decompose_facts(claim, level=d + 1)) 77 | claims = temp_claims 78 | all_claims.extend(claims) 79 | return all_claims 80 | 81 | def __str__(self): 82 | return ( 83 | "Decomposition by using LLMs.\nModel: " 84 | + self.model_name 85 | + "\nOutput structure is enforced with 'outlines' library.\nInstruction for the first level of decomposition is:\n" 86 | + str(self.instruction) 87 | + "\nInstruction for the granular decomposition (level 2 and higher) is:\n" 88 | + str(self.instruction_for_granular) 89 | ) 90 | -------------------------------------------------------------------------------- /src/TruthTorchLM/evaluators/model_judge.py: -------------------------------------------------------------------------------- 1 | from .correctness_evaluator import CorrectnessEvaluator 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | from litellm import completion 5 | import random 6 | import torch 7 | from TruthTorchLM.templates import DEFAULT_JUDGE_PROMPT, DEFAULT_JUDGE_SYSTEM_PROMPT 8 | 9 | 10 | class ModelJudge(CorrectnessEvaluator): 11 | def __init__( 12 | self, 13 | model: Union[PreTrainedModel, str], 14 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 15 | prompt: str = DEFAULT_JUDGE_PROMPT, 16 | system_prompt: str = DEFAULT_JUDGE_SYSTEM_PROMPT, 17 | num_retries: int = 1, 18 | ) -> None: 19 | super().__init__() 20 | self.model = model 21 | self.tokenizer = tokenizer 22 | self.prompt = prompt 23 | self.system_prompt = system_prompt 24 | self.num_retries = num_retries 25 | 26 | def __call__( 27 | self, 28 | question_text: str, 29 | generated_text: str, 30 | ground_truths: list[str], 31 | context: str = "", 32 | seed: int = None, 33 | ) -> bool: 34 | if seed == None: 35 | seed = random.randint(0, 1000000) 36 | model_generation = generated_text 37 | if self.prompt.find("context") == -1: 38 | chat = [ 39 | {"role": "system", "content": self.system_prompt}, 40 | { 41 | "role": "user", 42 | "content": self.prompt.format( 43 | question=question_text, 44 | ground_truths=", ".join(ground_truths), 45 | answer=generated_text, 46 | ), 47 | }, 48 | ] 49 | else: 50 | chat = [ 51 | {"role": "system", "content": self.system_prompt}, 52 | { 53 | "role": "user", 54 | "content": self.prompt.format( 55 | question=question_text, 56 | ground_truths=", ".join(ground_truths), 57 | answer=generated_text, 58 | context=context, 59 | ), 60 | }, 61 | ] 62 | 63 | if type(self.model) == str: 64 | response = completion( 65 | model=self.model, messages=chat, seed=seed, num_retries=self.num_retries 66 | ) 67 | generated_text = response.choices[0].message["content"] 68 | else: 69 | torch.manual_seed(seed) 70 | random.seed(seed) 71 | text = self.tokenizer.apply_chat_template(chat, tokenize=False) 72 | input_ids = self.tokenizer.encode(text, return_tensors="pt").to( 73 | self.model.device 74 | ) 75 | model_output = self.model.generate(input_ids) 76 | tokens = model_output[0][len(input_ids[0]):] 77 | generated_text = self.tokenizer.decode( 78 | tokens, skip_special_tokens=False) 79 | if "incorrect" in generated_text.lower(): 80 | return 0 81 | elif "correct" in generated_text.lower(): 82 | return 1 83 | elif "not_attempted" in generated_text.lower(): 84 | return -1 85 | else: 86 | # output warning 87 | print( 88 | "The output of the judge model is not in the expected format. Not attempted will be returned." 89 | ) 90 | return -1 91 | 92 | def __str__(self): 93 | return f"ROUGE with threshold {self.threshold} and type {self.rouge_type}" 94 | -------------------------------------------------------------------------------- /src/TruthTorchLM/utils/calibration_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.metrics import precision_recall_curve 3 | 4 | 5 | def f1_picker(correctness: list, truth_values: list): 6 | truth_values = np.array(truth_values) 7 | truth_values = np.nan_to_num(truth_values) # Replace NaN with 0 8 | precisions, recalls, thresholds = precision_recall_curve( 9 | correctness, truth_values) 10 | f1_scores = 2 * (precisions * recalls) / (precisions + recalls) 11 | # Remove NaN values and find the index of the highest F1 score 12 | f1_scores = np.nan_to_num(f1_scores) # Replace NaN with 0 13 | max_index = np.argmax(f1_scores) 14 | 15 | # The thresholds array is of length len(precisions) - 1, so we use max_index-1 to get the corresponding threshold 16 | threshold = thresholds[max_index - 1] if max_index > 0 else thresholds[0] 17 | return threshold 18 | 19 | 20 | def precision_picker(correctness: list, truth_values: list, precision: float): 21 | precisions, recalls, thresholds = precision_recall_curve( 22 | correctness, truth_values) 23 | # Find the index of the smallest precision that is greater than or equal to the target precision 24 | index = np.where(precisions >= precision)[0][0] 25 | # Since precisions is always one element longer than thresholds, we need to adjust the index 26 | threshold = thresholds[index - 1] if index > 0 else thresholds[0] 27 | return threshold 28 | 29 | 30 | def recall_picker(correctness: list, truth_values: list, recall: float): 31 | precisions, recalls, thresholds = precision_recall_curve( 32 | correctness, truth_values) 33 | # Find the index of the smallest recall that is greater than or equal to the target recall 34 | index = np.where(recalls >= recall)[0][0] 35 | # Since recalls is always one element longer than thresholds, we need to adjust the index 36 | threshold = thresholds[index - 1] if index > 0 else thresholds[0] 37 | return threshold 38 | 39 | 40 | def accuracy_picker(correctness: list, truth_values: list): 41 | accuracies = [ 42 | np.mean((truth_values > threshold) == correctness) 43 | for threshold in np.unique(truth_values) 44 | ] 45 | threshold = np.unique(truth_values)[np.argmax(accuracies)] 46 | return threshold 47 | 48 | 49 | # def find_threshold_std(correctness: list, truth_values: list, precision: float = -1, recall: float = -1): 50 | # std = np.std(truth_values) 51 | # precisions, recalls, thresholds = precision_recall_curve(correctness, truth_values) 52 | # if precision != -1: 53 | # # Find the index of the smallest precision that is greater than or equal to the target precision 54 | # index = np.where(precisions >= precision)[0][0] 55 | # # Since precisions is always one element longer than thresholds, we need to adjust the index 56 | # threshold = thresholds[index - 1] if index > 0 else thresholds[0] 57 | # elif recall != -1: 58 | # # Find the index of the smallest recall that is greater than or equal to the target recall 59 | # index = np.where(recalls >= recall)[0][0] 60 | # # Since recalls is always one element longer than thresholds, we need to adjust the index 61 | # threshold = thresholds[index - 1] if index > 0 else thresholds[0] 62 | # else: 63 | # # Calculate F1 scores for each threshold 64 | # f1_scores = 2 * (precisions * recalls) / (precisions + recalls) 65 | 66 | # # Remove NaN values and find the index of the highest F1 score 67 | # f1_scores = np.nan_to_num(f1_scores) # Replace NaN with 0 68 | # max_index = np.argmax(f1_scores) 69 | 70 | # # The thresholds array is of length len(precisions) - 1, so we use max_index-1 to get the corresponding threshold 71 | # threshold = thresholds[max_index - 1] if max_index > 0 else thresholds[0] 72 | 73 | # return threshold, std 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # macOS files 156 | .DS_Store 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ 164 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/inside.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | 4 | from .truth_method import TruthMethod 5 | from ..generation import sample_generations_hf_local 6 | 7 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 8 | 9 | 10 | class Inside(TruthMethod): 11 | REQUIRES_SAMPLED_TEXT = True 12 | REQUIRES_SAMPLED_ACTIVATIONS = True 13 | 14 | def __init__( 15 | self, 16 | number_of_generations: int = 10, 17 | alpha: float = 0.001, 18 | batch_generation=True, 19 | ): 20 | super().__init__() 21 | self.number_of_generations = number_of_generations 22 | self.alpha = alpha 23 | self.batch_generation = batch_generation 24 | 25 | def forward_hf_local( 26 | self, 27 | model: PreTrainedModel, 28 | input_text: str, 29 | generated_text: str, 30 | question: str, 31 | all_ids: Union[list, torch.Tensor], 32 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 33 | generation_seed=None, 34 | sampled_generations_dict: dict = None, 35 | messages: list = [], 36 | context: str = "", 37 | **kwargs 38 | ): 39 | 40 | if sampled_generations_dict is None: 41 | sampled_generations_dict = sample_generations_hf_local( 42 | model=model, 43 | input_text=input_text, 44 | tokenizer=tokenizer, 45 | generation_seed=generation_seed, 46 | number_of_generations=self.number_of_generations, 47 | return_text=True, 48 | return_activations=True, 49 | **kwargs 50 | ) 51 | 52 | generated_texts = sampled_generations_dict["generated_texts"][ 53 | : self.number_of_generations 54 | ] 55 | sentence_embeddings = torch.stack( 56 | [ 57 | hidden_states[-1][int(len(hidden_states[-1]) / 2)][0] 58 | for hidden_states in sampled_generations_dict["activations"][ 59 | : self.number_of_generations 60 | ] 61 | ] 62 | ) # TODO: check this part is correct or not 63 | 64 | hidden_dim = sentence_embeddings.shape[-1] 65 | centering_matrix = torch.eye(hidden_dim) - ( 66 | torch.ones((hidden_dim, hidden_dim)) / hidden_dim 67 | ) 68 | 69 | covariance = ( 70 | sentence_embeddings 71 | @ centering_matrix.to(sentence_embeddings.dtype) 72 | @ sentence_embeddings.T 73 | ) 74 | regularized_covarience = ( 75 | covariance + torch.eye(self.number_of_generations) * self.alpha 76 | ) 77 | eigenvalues, _ = torch.linalg.eig(regularized_covarience) 78 | 79 | eigenvalues = eigenvalues.real 80 | 81 | eigen_score = -torch.mean(torch.log(eigenvalues)).cpu().item() 82 | return { 83 | "truth_value": eigen_score, 84 | "generated_texts_for_inside": generated_texts, 85 | } # this output format should be same for all truth methods 86 | 87 | def forward_api( 88 | self, 89 | model: str, 90 | messages: list, 91 | generated_text: str, 92 | question: str, 93 | generation_seed=None, 94 | sampled_generations_dict: dict = None, 95 | logprobs: list = None, 96 | generated_tokens: list = None, 97 | context: str = "", 98 | **kwargs 99 | ): 100 | raise ValueError( 101 | "Inside method cannot be used with black-box API models since it requires access to activations." 102 | ) 103 | 104 | return { 105 | "truth_value": 0, 106 | "generated_texts_for_inside": [], 107 | } # this output format should be same for all truth methods 108 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/decomposition_methods/unstructured_decomposition_local.py: -------------------------------------------------------------------------------- 1 | from .decomposition_method import DecompositionMethod 2 | from ..templates import UNSTRUCTURED_DECOMPOSITION_INSTRUCTION 3 | from TruthTorchLM.utils.common_utils import generate, fix_tokenizer_chat 4 | 5 | from copy import deepcopy 6 | from typing import Union, Callable 7 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 8 | 9 | 10 | def default_output_parser(text: str): 11 | claims = text.split("\n•") 12 | claims = [claim.strip() for claim in claims if claim.strip()] 13 | return claims 14 | 15 | 16 | class UnstructuredDecompositionLocal(DecompositionMethod): 17 | def __init__( 18 | self, 19 | model: PreTrainedModel, 20 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], 21 | instruction: list = UNSTRUCTURED_DECOMPOSITION_INSTRUCTION, 22 | decomposition_depth: int = 1, 23 | output_parser: Callable[[str], list[str]] = default_output_parser, 24 | split_by_paragraphs=True, 25 | **kwargs 26 | ): 27 | super().__init__() 28 | 29 | self.model = model 30 | self.tokenizer = tokenizer 31 | self.instruction = instruction 32 | self.output_parser = output_parser 33 | self.decomposition_depth = decomposition_depth 34 | self.split_by_paragraphs = split_by_paragraphs 35 | 36 | default_kwargs = {"top_p": 1, "do_sample": False, "temperature": None} 37 | default_kwargs.update(kwargs) 38 | 39 | eos_token_id = default_kwargs.pop("eos_token_id", None) 40 | if eos_token_id is None: 41 | eos_token_id = model.config.eos_token_id 42 | default_kwargs["eos_token_id"] = eos_token_id 43 | 44 | pad_token_id = default_kwargs.pop("pad_token_id", None) 45 | if pad_token_id is None: 46 | if type(eos_token_id) == list: 47 | pad_token_id = eos_token_id[0] 48 | else: 49 | pad_token_id = eos_token_id 50 | default_kwargs["pad_token_id"] = pad_token_id 51 | self.kwargs = default_kwargs 52 | 53 | def decompose_facts(self, input_text: str): 54 | 55 | messages = deepcopy(self.instruction) 56 | for item in messages: 57 | item["content"] = item["content"].format(TEXT=input_text) 58 | self.tokenizer, messages = fix_tokenizer_chat(self.tokenizer, messages) 59 | text = self.tokenizer.apply_chat_template( 60 | messages, 61 | tokenize=False, 62 | add_generation_prompt=True, 63 | continue_final_message=False, 64 | ) 65 | generated_output = generate( 66 | text, self.model, self.tokenizer, **self.kwargs) 67 | generated_text = "\n" + \ 68 | generated_output["generated_text_skip_specials"].strip() 69 | claims = self.output_parser(generated_text) 70 | 71 | return claims 72 | 73 | def __call__(self, input_text) -> list[str]: 74 | 75 | if self.split_by_paragraphs: 76 | paragraphs = [ 77 | paragraph.strip() 78 | for paragraph in input_text.split("\n") 79 | if paragraph.strip() 80 | ] 81 | else: 82 | paragraphs = [input_text] 83 | all_claims = [] 84 | for paragraph in paragraphs: 85 | claims = self.decompose_facts(paragraph) 86 | for _ in range(self.decomposition_depth - 1): 87 | temp_claims = [] 88 | for claim in claims: 89 | temp_claims.extend(self.decompose_facts(claim)) 90 | claims = temp_claims 91 | all_claims.extend(claims) 92 | return all_claims 93 | 94 | def __str__(self): 95 | return ( 96 | "Decomposition by using LLMs method with " 97 | + self.model 98 | + " model. Chat template is:\n" 99 | + str(self.instruction) 100 | + "\n Sentence seperator is: " 101 | + self.sentence_seperator 102 | ) 103 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/entropy.py: -------------------------------------------------------------------------------- 1 | from .truth_method import TruthMethod 2 | from TruthTorchLM.scoring_methods import ScoringMethod, LengthNormalizedScoring 3 | from typing import Union 4 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 5 | import torch 6 | import numpy as np 7 | from ..generation import sample_generations_hf_local, sample_generations_api 8 | from TruthTorchLM.error_handler import handle_logprobs_error 9 | 10 | 11 | class Entropy(TruthMethod): 12 | 13 | REQUIRES_SAMPLED_TEXT = True 14 | REQUIRES_SAMPLED_LOGPROBS = True 15 | 16 | def __init__( 17 | self, 18 | scoring_function: ScoringMethod = LengthNormalizedScoring(), 19 | number_of_generations: int = 5, 20 | batch_generation=True, 21 | ): # normalization, 22 | super().__init__() 23 | self.scoring_function = scoring_function 24 | self.number_of_generations = number_of_generations 25 | self.batch_generation = batch_generation 26 | 27 | def _entropy( 28 | self, generated_texts: list[str], question: str, scores: list[float] 29 | ): 30 | entropy = -np.sum(scores) / len(scores) 31 | return { 32 | "truth_value": -entropy, 33 | "entropy": entropy, 34 | "score_for_each_generation": scores, 35 | "generated_texts": generated_texts, 36 | } 37 | 38 | def forward_hf_local( 39 | self, 40 | model: PreTrainedModel, 41 | input_text: str, 42 | generated_text: str, 43 | question: str, 44 | all_ids: Union[list, torch.Tensor], 45 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 46 | generation_seed=None, 47 | sampled_generations_dict: dict = None, 48 | messages: list = [], 49 | context: str = "", 50 | **kwargs 51 | ): 52 | 53 | if sampled_generations_dict is None: 54 | sampled_generations_dict = sample_generations_hf_local( 55 | model=model, 56 | input_text=input_text, 57 | tokenizer=tokenizer, 58 | generation_seed=generation_seed, 59 | number_of_generations=self.number_of_generations, 60 | return_text=True, 61 | return_logprobs=True, 62 | batch_generation=self.batch_generation, 63 | **kwargs 64 | ) 65 | 66 | scores = [] 67 | generated_texts = sampled_generations_dict["generated_texts"][ 68 | : self.number_of_generations 69 | ] 70 | 71 | for i in range(self.number_of_generations): 72 | score = self.scoring_function( 73 | sampled_generations_dict["logprobs"][i]) 74 | scores.append(score) # scores are in log scale 75 | 76 | return self._entropy(generated_texts, question, scores) 77 | 78 | @handle_logprobs_error 79 | def forward_api( 80 | self, 81 | model: str, 82 | messages: list, 83 | generated_text: str, 84 | question: str, 85 | generation_seed=None, 86 | sampled_generations_dict: dict = None, 87 | logprobs: list = None, 88 | generated_tokens: list = None, 89 | context: str = "", 90 | **kwargs 91 | ): 92 | 93 | if sampled_generations_dict is None: 94 | sampled_generations_dict = sample_generations_api( 95 | model=model, 96 | messages=messages, 97 | generation_seed=generation_seed, 98 | number_of_generations=self.number_of_generations, 99 | return_text=True, 100 | return_logprobs=True, 101 | **kwargs 102 | ) 103 | 104 | generated_texts = sampled_generations_dict["generated_texts"][ 105 | : self.number_of_generations 106 | ] 107 | 108 | scores = [] 109 | for i in range(self.number_of_generations): 110 | score = self.scoring_function( 111 | sampled_generations_dict["logprobs"][i]) 112 | scores.append(score) # scores are in log scale 113 | 114 | return self._entropy(generated_texts, question, scores) 115 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/utils/dataset_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import random 4 | import requests 5 | from typing import Union 6 | 7 | from TruthTorchLM.availability import LONG_FORM_AVAILABLE_DATASETS 8 | from TruthTorchLM.environment import get_cache_dir 9 | 10 | 11 | def get_dataset(dataset: Union[str, list], size_of_data: int, seed: int = 0): 12 | if type(dataset) != str: 13 | assert len(dataset) > 0, "Dataset list is empty." 14 | assert "question" in dataset[0], "Dataset should have 'question' key." 15 | if "statements" in dataset[0]: 16 | assert ( 17 | "statement_correctness" in dataset[0] 18 | ), "Statement correctness scores is missing." 19 | if "statement_correctness" in dataset[0]: 20 | assert "statements" in dataset[0], "Statements are missing." 21 | return dataset 22 | 23 | if dataset not in LONG_FORM_AVAILABLE_DATASETS: 24 | raise ValueError( 25 | f"Dataset is not available. Available datasets are: {LONG_FORM_AVAILABLE_DATASETS}" 26 | ) 27 | 28 | print("Loading dataset... Size of data:", size_of_data) 29 | 30 | if dataset == "longfact_concepts": 31 | dataset = get_longfact( 32 | branch="longfact-concepts_gpt4_01-10-2024", 33 | size_of_data=size_of_data, 34 | seed=seed, 35 | ) 36 | elif dataset == "longfact_objects": 37 | dataset = get_longfact( 38 | branch="longfact-objects_gpt4_01-12-2024", 39 | size_of_data=size_of_data, 40 | seed=seed, 41 | ) 42 | return dataset 43 | 44 | 45 | def get_longfact(branch: str, size_of_data: int = 100, seed: int = 0): 46 | # Download data 47 | download_github_folder( 48 | "google-deepmind", 49 | "long-form-factuality", 50 | "main", 51 | f"longfact/{branch}_noduplicates", 52 | f"{get_cache_dir()}/datasets/{branch}", 53 | None, 54 | ) 55 | # Load data 56 | questions = [] 57 | for file_name in os.listdir(f"{get_cache_dir()}/datasets/{branch}"): 58 | with open(f"{get_cache_dir()}/datasets/{branch}/" + file_name, "r") as file: 59 | for line in file: 60 | questions.append({"question": json.loads(line)["prompt"], "context":""}) 61 | 62 | if size_of_data < len(questions): 63 | random.seed(seed) 64 | return random.sample(questions, size_of_data) 65 | return questions 66 | 67 | 68 | def download_file(file_info, local_file_path, headers): 69 | """Download a single file from GitHub.""" 70 | download_url = file_info["download_url"] 71 | response = requests.get(download_url, headers=headers) 72 | if response.status_code == 200: 73 | with open(local_file_path, "wb") as f: 74 | f.write(response.content) 75 | # print(f"Downloaded: {file_info['path']}") 76 | else: 77 | print( 78 | f"Failed to download {file_info['path']}: {response.status_code}") 79 | 80 | 81 | def download_contents(owner, repo, branch, path, local_path, headers): 82 | """Recursively download contents from a GitHub folder.""" 83 | api_url = ( 84 | f"https://api.github.com/repos/{owner}/{repo}/contents/{path}?ref={branch}" 85 | ) 86 | response = requests.get(api_url, headers=headers) 87 | if response.status_code == 200: 88 | items = response.json() 89 | if isinstance(items, dict) and items.get("type") == "file": 90 | # It's a single file 91 | download_file(items, local_path, headers) 92 | else: 93 | # It's a directory 94 | if not os.path.exists(local_path): 95 | os.makedirs(local_path) 96 | for item in items: 97 | item_path = item["path"] 98 | item_type = item["type"] 99 | item_local_path = os.path.join(local_path, item["name"]) 100 | if item_type == "file": 101 | download_file(item, item_local_path, headers) 102 | elif item_type == "dir": 103 | download_contents( 104 | owner, repo, branch, item_path, item_local_path, headers 105 | ) 106 | else: 107 | print(f"Failed to get contents of {path}: {response.status_code}") 108 | 109 | 110 | def download_github_folder(owner, repo, branch, folder_path, local_dir, token=None): 111 | headers = {"Authorization": f"token {token}"} if token else {} 112 | if not os.path.exists(local_dir): 113 | os.makedirs(local_dir) 114 | download_contents(owner, repo, branch, folder_path, local_dir, headers) 115 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/tokenSAR.py: -------------------------------------------------------------------------------- 1 | from .truth_method import TruthMethod 2 | 3 | from typing import Union 4 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 5 | from sentence_transformers import CrossEncoder 6 | import torch 7 | import numpy as np 8 | from TruthTorchLM.error_handler import handle_logprobs_error 9 | 10 | 11 | class TokenSAR(TruthMethod): 12 | 13 | REQUIRES_LOGPROBS = True 14 | 15 | def __init__( 16 | self, 17 | tokenizer: PreTrainedTokenizer = None, 18 | similarity_model=None, 19 | similarity_model_device="cuda", 20 | ): # normalization, 21 | super().__init__() 22 | self.tokenizer = tokenizer 23 | if similarity_model is None: 24 | self.similarity_model = CrossEncoder( 25 | "cross-encoder/stsb-roberta-large", device=similarity_model_device 26 | ) 27 | else: 28 | self.similarity_model = similarity_model 29 | 30 | def forward_hf_local( 31 | self, 32 | model: PreTrainedModel, 33 | input_text: str, 34 | generated_text: str, 35 | question: str, 36 | all_ids: Union[list, torch.Tensor], 37 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 38 | generation_seed=None, 39 | sampled_generations_dict: dict = None, 40 | messages: list = [], 41 | context: str = "", 42 | **kwargs 43 | ): 44 | 45 | input_ids = tokenizer.encode( 46 | input_text, return_tensors="pt").to(model.device) 47 | model_output = all_ids 48 | tokens = model_output[0][len(input_ids[0]):] 49 | tokens_text = [tokenizer.decode(token) for token in tokens] 50 | 51 | with torch.no_grad(): 52 | outputs = model(model_output) 53 | logits = outputs.logits # Logits for each token in the input 54 | 55 | # Calculate probabilities from logits 56 | logprobs = torch.log_softmax( 57 | logits, dim=-1) # logprobs for each token 58 | logprobs = logprobs[ 59 | 0, len(input_ids[0]) - 1: -1, : 60 | ] # logprobs for each token in the generated text 61 | logprobs = torch.gather( 62 | logprobs, dim=1, index=model_output[0][len(input_ids[0]):].view(-1, 1) 63 | ) # logprobs for each token in the generated text 64 | logprobs = logprobs.view(-1).tolist() # convert to list 65 | 66 | importance_vector = [] 67 | tokens = tokens.view(-1).tolist() 68 | for i in range(len(tokens)): 69 | removed_answer_ids = tokens[:i] + tokens[i + 1:] 70 | removed_answer = tokenizer.decode( 71 | removed_answer_ids, skip_special_tokens=True 72 | ) 73 | score = self.similarity_model.predict( 74 | [ 75 | ( 76 | question + " " + removed_answer, 77 | question + " " + generated_text, 78 | ) 79 | ] 80 | ) 81 | score = 1 - score[0] 82 | importance_vector.append(score) 83 | 84 | importance_vector = importance_vector / np.sum(importance_vector) 85 | score = np.dot(importance_vector, logprobs) 86 | 87 | return { 88 | "truth_value": score, 89 | "generated_text": generated_text, 90 | } # we shouldn't return generated text. remove it from the output format 91 | 92 | @handle_logprobs_error 93 | def forward_api( 94 | self, 95 | model: str, 96 | messages: list, 97 | generated_text: str, 98 | question: str, 99 | generation_seed=None, 100 | sampled_generations_dict: dict = None, 101 | logprobs: list = None, 102 | generated_tokens: list = None, 103 | context: str = "", 104 | **kwargs 105 | ): 106 | importance_vector = [] 107 | for i in range(len(generated_tokens)): 108 | removed_answer = "".join(generated_tokens[:i]) + "".join( 109 | generated_tokens[i + 1:] 110 | ) 111 | score = self.similarity_model.predict( 112 | [ 113 | ( 114 | question + " " + removed_answer, 115 | question + " " + generated_text, 116 | ) 117 | ] 118 | ) 119 | score = 1 - score[0] 120 | importance_vector.append(score) 121 | 122 | importance_vector = importance_vector / np.sum(importance_vector) 123 | score = np.dot(importance_vector, logprobs) 124 | 125 | return { 126 | "truth_value": score, 127 | "generated_text": generated_text, 128 | } # we shouldn't return generated text. remove it from the output format 129 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/truth_method.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import random 3 | import numpy as np 4 | from typing import Union 5 | from abc import ABC, abstractmethod 6 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 7 | from TruthTorchLM.utils.common_utils import fix_tokenizer_chat 8 | from TruthTorchLM.normalizers import Normalizer, SigmoidNormalizer 9 | import litellm 10 | 11 | litellm.drop_params = True 12 | 13 | 14 | def sigmoid_normalization(x: float, threshold: float = 0.0, std: float = 1.0): 15 | z = (x - threshold) / std 16 | if z >= 0: 17 | # For positive z, compute sigmoid as 1 / (1 + exp(-z)) directly 18 | return 1 / (1 + np.exp(-z)) 19 | else: 20 | # For negative z, to avoid overflow, use the identity: sigmoid(z) = exp(z) / (1 + exp(z)) 21 | return np.exp(z) / (1 + np.exp(z)) 22 | 23 | 24 | class TruthMethod(ABC): 25 | 26 | REQUIRES_SAMPLED_TEXT = False 27 | REQUIRES_SAMPLED_LOGITS = False 28 | REQUIRES_SAMPLED_LOGPROBS = False 29 | REQUIRES_SAMPLED_ATTENTIONS = False 30 | REQUIRES_SAMPLED_ACTIVATIONS = False 31 | REQUIRES_NORMALIZATION = True 32 | REQUIRES_LOGPROBS = False 33 | 34 | def __init__(self): 35 | self.normalizer = SigmoidNormalizer( 36 | threshold=0, std=1.0 37 | ) # default dummy normalizer 38 | 39 | def __call__( 40 | self, 41 | model: Union[PreTrainedModel, str], 42 | input_text: str = "", 43 | generated_text: str = "", 44 | question: str = "", 45 | all_ids: Union[list, torch.Tensor] = None, 46 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 47 | generation_seed=None, 48 | sampled_generations_dict: dict = None, 49 | messages: list = [], 50 | logprobs: list = None, 51 | generated_tokens: list = None, 52 | context: str = "", 53 | **kwargs, 54 | ): 55 | if generation_seed is not None: 56 | torch.manual_seed(generation_seed) 57 | random.seed(generation_seed) 58 | if isinstance(model, str): 59 | output_dict = self.forward_api( 60 | model=model, 61 | messages=messages, 62 | generated_text=generated_text, 63 | question=question, 64 | generation_seed=generation_seed, 65 | sampled_generations_dict=sampled_generations_dict, 66 | logprobs=logprobs, 67 | generated_tokens=generated_tokens, 68 | context=context, 69 | **kwargs, 70 | ) 71 | else: 72 | tokenizer, messages = fix_tokenizer_chat(tokenizer, messages) 73 | output_dict = self.forward_hf_local( 74 | model=model, 75 | input_text=input_text, 76 | generated_text=generated_text, 77 | question=question, 78 | all_ids=all_ids, 79 | tokenizer=tokenizer, 80 | generation_seed=generation_seed, 81 | sampled_generations_dict=sampled_generations_dict, 82 | messages=messages, 83 | context=context, 84 | **kwargs, 85 | ) 86 | 87 | if self.REQUIRES_NORMALIZATION: 88 | output_dict["normalized_truth_value"] = float(self.normalizer( 89 | output_dict["truth_value"] 90 | )) 91 | else: 92 | output_dict["normalized_truth_value"] = float(output_dict["truth_value"]) 93 | return output_dict 94 | 95 | @abstractmethod 96 | def forward_hf_local( 97 | self, 98 | model: PreTrainedModel, 99 | input_text: str, 100 | generated_text: str, 101 | question: str, 102 | all_ids: Union[list, torch.Tensor], 103 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 104 | generation_seed=None, 105 | sampled_generations_dict: dict = None, 106 | messages: list = [], 107 | context: str = "", 108 | **kwargs, 109 | ): 110 | raise NotImplementedError("Subclasses must implement this method") 111 | 112 | @abstractmethod 113 | def forward_api( 114 | self, 115 | model: str, 116 | messages: list, 117 | generated_text: str, 118 | question: str, 119 | generation_seed=None, 120 | sampled_generations_dict: dict = None, 121 | logprobs: list = None, 122 | generated_tokens: list = None, 123 | context: str = "", 124 | **kwargs, 125 | ): 126 | raise NotImplementedError("Subclasses must implement this method") 127 | 128 | def set_normalizer(self, normalizer: Normalizer): 129 | self.normalizer = normalizer 130 | 131 | def get_normalizer(self): 132 | return self.normalizer 133 | 134 | def __str__(self): 135 | # search over all attributes and print them 136 | return f"{self.__class__.__name__} with {str(self.__dict__)}" 137 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/num_semantic_set_uncertainty.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | from transformers import DebertaForSequenceClassification, DebertaTokenizer 5 | from TruthTorchLM.utils import calculate_U_num_set 6 | from .truth_method import TruthMethod 7 | from ..generation import sample_generations_hf_local, sample_generations_api 8 | 9 | 10 | class NumSemanticSetUncertainty(TruthMethod): 11 | 12 | REQUIRES_SAMPLED_TEXT = True 13 | 14 | def __init__( 15 | self, 16 | method_for_similarity: str = "semantic", 17 | number_of_generations=5, 18 | model_for_entailment: PreTrainedModel = None, 19 | tokenizer_for_entailment: PreTrainedTokenizer = None, 20 | entailment_model_device="cuda", 21 | batch_generation=True, 22 | ): 23 | super().__init__() 24 | 25 | if ( 26 | model_for_entailment is None or tokenizer_for_entailment is None 27 | ) and method_for_similarity == "semantic": 28 | model_for_entailment = DebertaForSequenceClassification.from_pretrained( 29 | "microsoft/deberta-large-mnli" 30 | ).to(entailment_model_device) 31 | tokenizer_for_entailment = DebertaTokenizer.from_pretrained( 32 | "microsoft/deberta-large-mnli" 33 | ) 34 | 35 | if method_for_similarity not in ["semantic", "jaccard"]: 36 | raise ValueError( 37 | "method_for_similarity should be either semantic or jaccard. Please refer to https://arxiv.org/pdf/2305.19187 for more information." 38 | ) 39 | 40 | self.model_for_entailment = None 41 | self.tokenizer_for_entailment = None 42 | 43 | if method_for_similarity == "semantic": 44 | print( 45 | 'There are 2 methods for similarity: semantic similarity and jaccard score. The default method is semantic similarity. If you want to use jaccard score, please set method_for_similarity="jaccard". Please refer to https://arxiv.org/pdf/2305.19187 for more information.' 46 | ) 47 | self.tokenizer_for_entailment = tokenizer_for_entailment 48 | self.model_for_entailment = model_for_entailment 49 | 50 | self.number_of_generations = number_of_generations 51 | self.method_for_similarity = method_for_similarity # jaccard or semantic 52 | self.batch_generation = batch_generation 53 | 54 | def _num_semantic_set_uncertainty( 55 | self, sampled_generations_dict: dict, question: str 56 | ): 57 | generated_texts = sampled_generations_dict["generated_texts"][ 58 | : self.number_of_generations 59 | ] 60 | output_dict = {} 61 | output = calculate_U_num_set( 62 | generated_texts, 63 | question, 64 | method_for_similarity=self.method_for_similarity, 65 | model_for_entailment=self.model_for_entailment, 66 | tokenizer_for_entailment=self.tokenizer_for_entailment, 67 | ) 68 | output_dict["U_num_set"] = output 69 | output_dict["generated_texts"] = generated_texts 70 | output_dict["truth_value"] = -output 71 | return output_dict 72 | 73 | def forward_hf_local( 74 | self, 75 | model: PreTrainedModel, 76 | input_text: str, 77 | generated_text: str, 78 | question: str, 79 | all_ids: Union[list, torch.Tensor], 80 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 81 | generation_seed=None, 82 | sampled_generations_dict: dict = None, 83 | messages: list = [], 84 | context: str = "", 85 | **kwargs 86 | ): 87 | 88 | if sampled_generations_dict is None: 89 | sampled_generations_dict = sample_generations_hf_local( 90 | model=model, 91 | input_text=input_text, 92 | tokenizer=tokenizer, 93 | generation_seed=generation_seed, 94 | number_of_generations=self.number_of_generations, 95 | return_text=True, 96 | batch_generation=self.batch_generation, 97 | **kwargs 98 | ) 99 | 100 | return self._num_semantic_set_uncertainty( 101 | sampled_generations_dict, question 102 | ) 103 | 104 | def forward_api( 105 | self, 106 | model: str, 107 | messages: list, 108 | generated_text: str, 109 | question: str, 110 | generation_seed=None, 111 | sampled_generations_dict: dict = None, 112 | logprobs: list = None, 113 | generated_tokens: list = None, 114 | context: str = "", 115 | **kwargs 116 | ): 117 | 118 | if sampled_generations_dict is None: 119 | sampled_generations_dict = sample_generations_api( 120 | model=model, 121 | messages=messages, 122 | generation_seed=generation_seed, 123 | number_of_generations=self.number_of_generations, 124 | return_text=True, 125 | **kwargs 126 | ) 127 | 128 | return self._num_semantic_set_uncertainty( 129 | sampled_generations_dict, question 130 | ) 131 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/sum_eigen_uncertainty.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | from transformers import DebertaForSequenceClassification, DebertaTokenizer 5 | from TruthTorchLM.utils import calculate_U_eigv 6 | from ..generation import sample_generations_hf_local, sample_generations_api 7 | from .truth_method import TruthMethod 8 | 9 | 10 | class SumEigenUncertainty(TruthMethod): 11 | 12 | REQUIRES_SAMPLED_TEXT = True 13 | 14 | def __init__( 15 | self, 16 | method_for_similarity: str = "semantic", 17 | number_of_generations=5, 18 | model_for_entailment: PreTrainedModel = None, 19 | tokenizer_for_entailment: PreTrainedTokenizer = None, 20 | temperature=3.0, 21 | entailment_model_device="cuda", 22 | batch_generation=True, 23 | ): 24 | super().__init__() 25 | 26 | if ( 27 | model_for_entailment is None or tokenizer_for_entailment is None 28 | ) and method_for_similarity == "semantic": 29 | model_for_entailment = DebertaForSequenceClassification.from_pretrained( 30 | "microsoft/deberta-large-mnli" 31 | ).to(entailment_model_device) 32 | tokenizer_for_entailment = DebertaTokenizer.from_pretrained( 33 | "microsoft/deberta-large-mnli" 34 | ) 35 | 36 | if method_for_similarity not in ["semantic", "jaccard"]: 37 | raise ValueError( 38 | "method_for_similarity should be either semantic or jaccard. Please refer to https://arxiv.org/pdf/2305.19187 for more information." 39 | ) 40 | 41 | self.model_for_entailment = None 42 | self.tokenizer_for_entailment = None 43 | 44 | if method_for_similarity == "semantic": 45 | print( 46 | 'There are 2 methods for similarity: semantic similarity and jaccard score. The default method is semantic similarity. If you want to use jaccard score, please set method_for_similarity="jaccard". Please refer to https://arxiv.org/pdf/2305.19187 for more information.' 47 | ) 48 | self.tokenizer_for_entailment = tokenizer_for_entailment 49 | self.model_for_entailment = model_for_entailment 50 | 51 | self.number_of_generations = number_of_generations 52 | self.method_for_similarity = method_for_similarity # jaccard or semantic 53 | self.temperature = temperature # temperature for NLI model 54 | self.batch_generation = batch_generation 55 | 56 | def _sum_eigen_uncertainty(self, sampled_generations_dict, question): 57 | generated_texts = sampled_generations_dict["generated_texts"][ 58 | : self.number_of_generations 59 | ] 60 | 61 | output_dict = {} 62 | output = calculate_U_eigv( 63 | generated_texts, 64 | question, 65 | method_for_similarity=self.method_for_similarity, 66 | temperature=self.temperature, 67 | model_for_entailment=self.model_for_entailment, 68 | tokenizer_for_entailment=self.tokenizer_for_entailment, 69 | ) 70 | output_dict["U_eigv"] = output 71 | output_dict["generated_texts"] = generated_texts 72 | output_dict["truth_value"] = -output 73 | return output_dict 74 | 75 | def forward_hf_local( 76 | self, 77 | model: PreTrainedModel, 78 | input_text: str, 79 | generated_text: str, 80 | question: str, 81 | all_ids: Union[list, torch.Tensor], 82 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 83 | generation_seed=None, 84 | sampled_generations_dict: dict = None, 85 | messages: list = [], 86 | context: str = "", 87 | **kwargs 88 | ): 89 | if sampled_generations_dict is None: 90 | sampled_generations_dict = sample_generations_hf_local( 91 | model=model, 92 | input_text=input_text, 93 | tokenizer=tokenizer, 94 | generation_seed=generation_seed, 95 | number_of_generations=self.number_of_generations, 96 | return_text=True, 97 | batch_generation=self.batch_generation, 98 | **kwargs 99 | ) 100 | 101 | return self._sum_eigen_uncertainty(sampled_generations_dict, question) 102 | 103 | def forward_api( 104 | self, 105 | model: str, 106 | messages: list, 107 | generated_text: str, 108 | question: str, 109 | generation_seed=None, 110 | sampled_generations_dict: dict = None, 111 | logprobs: list = None, 112 | generated_tokens: list = None, 113 | context: str = "", 114 | **kwargs 115 | ): 116 | 117 | if sampled_generations_dict is None: 118 | sampled_generations_dict = sample_generations_api( 119 | model=model, 120 | messages=messages, 121 | generation_seed=generation_seed, 122 | number_of_generations=self.number_of_generations, 123 | return_text=True, 124 | **kwargs 125 | ) 126 | 127 | return self._sum_eigen_uncertainty(sampled_generations_dict, question) 128 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/matrix_degree_uncertainty.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | from transformers import DebertaForSequenceClassification, DebertaTokenizer 5 | from TruthTorchLM.utils import calculate_U_deg 6 | from .truth_method import TruthMethod 7 | from ..generation import sample_generations_hf_local, sample_generations_api 8 | 9 | 10 | class MatrixDegreeUncertainty(TruthMethod): 11 | 12 | REQUIRES_SAMPLED_TEXT = True 13 | 14 | def __init__( 15 | self, 16 | method_for_similarity: str = "semantic", 17 | number_of_generations=5, 18 | model_for_entailment: PreTrainedModel = None, 19 | tokenizer_for_entailment: PreTrainedTokenizer = None, 20 | temperature=3.0, 21 | entailment_model_device="cuda", 22 | batch_generation=True, 23 | ): 24 | 25 | super().__init__() 26 | 27 | if ( 28 | model_for_entailment is None or tokenizer_for_entailment is None 29 | ) and method_for_similarity == "semantic": 30 | model_for_entailment = DebertaForSequenceClassification.from_pretrained( 31 | "microsoft/deberta-large-mnli" 32 | ).to(entailment_model_device) 33 | tokenizer_for_entailment = DebertaTokenizer.from_pretrained( 34 | "microsoft/deberta-large-mnli" 35 | ) 36 | 37 | if method_for_similarity not in ["semantic", "jaccard"]: 38 | raise ValueError( 39 | "method_for_similarity should be either semantic or jaccard. Please refer to https://arxiv.org/pdf/2305.19187 for more information." 40 | ) 41 | 42 | self.model_for_entailment = None 43 | self.tokenizer_for_entailment = None 44 | 45 | if method_for_similarity == "semantic": 46 | print( 47 | 'There are 2 methods for similarity: semantic similarity and jaccard score. The default method is semantic similarity. If you want to use jaccard score, please set method_for_similarity="jaccard". Please refer to https://arxiv.org/pdf/2305.19187 for more information.' 48 | ) 49 | self.tokenizer_for_entailment = tokenizer_for_entailment 50 | self.model_for_entailment = model_for_entailment 51 | 52 | self.number_of_generations = number_of_generations 53 | self.method_for_similarity = method_for_similarity # jaccard or semantic 54 | self.temperature = temperature # temperature for NLI model 55 | self.batch_generation = batch_generation 56 | 57 | def forward_hf_local( 58 | self, 59 | model: PreTrainedModel, 60 | input_text: str, 61 | generated_text: str, 62 | question: str, 63 | all_ids: Union[list, torch.Tensor], 64 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 65 | generation_seed=None, 66 | sampled_generations_dict: dict = None, 67 | messages: list = [], 68 | context: str = "", 69 | **kwargs 70 | ): 71 | 72 | if sampled_generations_dict is None: 73 | sampled_generations_dict = sample_generations_hf_local( 74 | model=model, 75 | input_text=input_text, 76 | tokenizer=tokenizer, 77 | generation_seed=generation_seed, 78 | number_of_generations=self.number_of_generations, 79 | return_text=True, 80 | batch_generation=self.batch_generation, 81 | **kwargs 82 | ) 83 | 84 | output_dict = self._matrix_degree_uncertainty( 85 | sampled_generations_dict, question 86 | ) 87 | return output_dict 88 | 89 | def forward_api( 90 | self, 91 | model: str, 92 | messages: list, 93 | generated_text: str, 94 | question: str, 95 | generation_seed=None, 96 | sampled_generations_dict: dict = None, 97 | logprobs: list = None, 98 | generated_tokens: list = None, 99 | context: str = "", 100 | **kwargs 101 | ): 102 | 103 | if sampled_generations_dict is None: 104 | sampled_generations_dict = sample_generations_api( 105 | model=model, 106 | messages=messages, 107 | generation_seed=generation_seed, 108 | number_of_generations=self.number_of_generations, 109 | return_text=True, 110 | **kwargs 111 | ) 112 | 113 | output_dict = self._matrix_degree_uncertainty( 114 | sampled_generations_dict, question 115 | ) 116 | return output_dict 117 | 118 | def _matrix_degree_uncertainty( 119 | self, sampled_generations_dict: dict, question: str 120 | ): 121 | generated_texts = sampled_generations_dict["generated_texts"][ 122 | : self.number_of_generations 123 | ] 124 | 125 | output_dict = {} 126 | 127 | output = calculate_U_deg( 128 | generated_texts, 129 | question, 130 | method_for_similarity=self.method_for_similarity, 131 | temperature=self.temperature, 132 | model_for_entailment=self.model_for_entailment, 133 | tokenizer_for_entailment=self.tokenizer_for_entailment, 134 | ) 135 | output_dict["U_deg"] = output 136 | output_dict["generated_texts"] = generated_texts 137 | output_dict["truth_value"] = -output 138 | 139 | return output_dict 140 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/eccentricity_uncertainty.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | from transformers import DebertaForSequenceClassification, DebertaTokenizer 5 | from TruthTorchLM.utils import calculate_U_ecc 6 | from .truth_method import TruthMethod 7 | from ..generation import sample_generations_hf_local, sample_generations_api 8 | 9 | 10 | class EccentricityUncertainty(TruthMethod): 11 | 12 | REQUIRES_SAMPLED_TEXT = True 13 | 14 | def __init__( 15 | self, 16 | method_for_similarity: str = "semantic", 17 | number_of_generations=5, 18 | model_for_entailment: PreTrainedModel = None, 19 | tokenizer_for_entailment: PreTrainedTokenizer = None, 20 | temperature=3.0, 21 | eigen_threshold=0.9, 22 | entailment_model_device="cuda", 23 | batch_generation=True, 24 | ): 25 | super().__init__() 26 | 27 | if ( 28 | model_for_entailment is None or tokenizer_for_entailment is None 29 | ) and method_for_similarity == "semantic": 30 | model_for_entailment = DebertaForSequenceClassification.from_pretrained( 31 | "microsoft/deberta-large-mnli" 32 | ).to(entailment_model_device) 33 | tokenizer_for_entailment = DebertaTokenizer.from_pretrained( 34 | "microsoft/deberta-large-mnli" 35 | ) 36 | 37 | if method_for_similarity not in ["semantic", "jaccard"]: 38 | raise ValueError( 39 | "method_for_similarity should be either semantic or jaccard. Please refer to https://arxiv.org/pdf/2305.19187 for more information." 40 | ) 41 | 42 | self.model_for_entailment = None 43 | self.tokenizer_for_entailment = None 44 | 45 | if method_for_similarity == "semantic": 46 | print( 47 | 'There are 2 methods for similarity: semantic similarity and jaccard score. The default method is semantic similarity. If you want to use jaccard score, please set method_for_similarity="jaccard". Please refer to https://arxiv.org/pdf/2305.19187 for more information.' 48 | ) 49 | self.tokenizer_for_entailment = tokenizer_for_entailment 50 | self.model_for_entailment = model_for_entailment 51 | 52 | self.number_of_generations = number_of_generations 53 | self.method_for_similarity = method_for_similarity # jaccard or semantic 54 | self.eigen_threshold = eigen_threshold 55 | self.temperature = temperature # temperature for NLI model 56 | self.batch_generation = batch_generation 57 | 58 | def _eccentricity_uncertainty( 59 | self, sampled_generations_dict: dict, question: str 60 | ): 61 | generated_texts = sampled_generations_dict["generated_texts"][ 62 | : self.number_of_generations 63 | ] 64 | output_dict = {} 65 | output = calculate_U_ecc( 66 | generated_texts, 67 | question, 68 | method_for_similarity=self.method_for_similarity, 69 | temperature=self.temperature, 70 | eigen_threshold=self.eigen_threshold, 71 | model_for_entailment=self.model_for_entailment, 72 | tokenizer_for_entailment=self.tokenizer_for_entailment, 73 | ) 74 | output_dict["U_ecc"] = output 75 | output_dict["generated_texts"] = generated_texts 76 | output_dict["truth_value"] = -output 77 | return output_dict 78 | 79 | def forward_hf_local( 80 | self, 81 | model: PreTrainedModel, 82 | input_text: str, 83 | generated_text: str, 84 | question: str, 85 | all_ids: Union[list, torch.Tensor], 86 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 87 | generation_seed=None, 88 | sampled_generations_dict: dict = None, 89 | messages: list = [], 90 | context: str = "", 91 | **kwargs 92 | ): 93 | 94 | if sampled_generations_dict is None: 95 | sampled_generations_dict = sample_generations_hf_local( 96 | model=model, 97 | input_text=input_text, 98 | tokenizer=tokenizer, 99 | generation_seed=generation_seed, 100 | number_of_generations=self.number_of_generations, 101 | return_text=True, 102 | return_logprobs=False, 103 | batch_generation=self.batch_generation, 104 | **kwargs 105 | ) 106 | 107 | return self._eccentricity_uncertainty( 108 | sampled_generations_dict, question 109 | ) 110 | 111 | def forward_api( 112 | self, 113 | model: str, 114 | messages: list, 115 | generated_text: str, 116 | question: str, 117 | generation_seed=None, 118 | sampled_generations_dict: dict = None, 119 | logprobs: list = None, 120 | generated_tokens: list = None, 121 | context: str = "", 122 | **kwargs 123 | ): 124 | 125 | if sampled_generations_dict is None: 126 | sampled_generations_dict = sample_generations_api( 127 | model=model, 128 | messages=messages, 129 | generation_seed=generation_seed, 130 | number_of_generations=self.number_of_generations, 131 | return_text=True, 132 | **kwargs 133 | ) 134 | 135 | return self._eccentricity_uncertainty( 136 | sampled_generations_dict, question 137 | ) 138 | -------------------------------------------------------------------------------- /src/TruthTorchLM/utils/google_search_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import aiohttp 4 | import nest_asyncio 5 | 6 | 7 | class GoogleSerperAPIWrapper: 8 | """Wrapper around the Serper.dev Google Search API. 9 | You can create a free API key at https://serper.dev. 10 | To use, you should have the environment variable ``SERPER_API_KEY`` 11 | set with your API key, or pass `serper_api_key` as a named parameter 12 | to the constructor. 13 | Example: 14 | .. code-block:: python 15 | from langchain import GoogleSerperAPIWrapper 16 | google_serper = GoogleSerperAPIWrapper() 17 | """ 18 | 19 | def __init__(self, snippet_cnt=10, location: str = "us", language="en") -> None: 20 | self.k = snippet_cnt 21 | self.gl = location 22 | self.hl = language 23 | self.serper_api_key = os.environ.get("SERPER_API_KEY", None) 24 | assert ( 25 | self.serper_api_key is not None 26 | ), "Please set the SERPER_API_KEY environment variable." 27 | assert ( 28 | self.serper_api_key != "" 29 | ), "Please set the SERPER_API_KEY environment variable." 30 | nest_asyncio.apply() # to allow asyncio.run in jupyter notebook 31 | 32 | async def _google_serper_search_results( 33 | self, session, search_term: str, gl: str, hl: str 34 | ) -> dict: 35 | headers = { 36 | "X-API-KEY": self.serper_api_key or "", 37 | "Content-Type": "application/json", 38 | } 39 | params = {"q": search_term, "gl": gl, "hl": hl} 40 | async with session.post( 41 | "https://google.serper.dev/search", 42 | headers=headers, 43 | params=params, 44 | raise_for_status=True, 45 | ) as response: 46 | return await response.json() 47 | 48 | def _parse_results(self, results): 49 | snippets = [] 50 | try: 51 | if results.get("answerBox"): 52 | answer_box = results.get("answerBox", {}) 53 | if answer_box.get("answer"): 54 | element = {"content": answer_box.get( 55 | "answer"), "source": "None"} 56 | return [element] 57 | elif answer_box.get("snippet"): 58 | element = { 59 | "content": answer_box.get("snippet").replace("\n", " "), 60 | "source": "None", 61 | } 62 | return [element] 63 | elif answer_box.get("snippetHighlighted"): 64 | element = { 65 | "content": answer_box.get("snippetHighlighted"), 66 | "source": "None", 67 | } 68 | return [element] 69 | 70 | if results.get("knowledgeGraph"): 71 | kg = results.get("knowledgeGraph", {}) 72 | title = kg.get("title") 73 | entity_type = kg.get("type") 74 | if entity_type: 75 | element = {"content": f"{title}: {entity_type}", 76 | "source": "None"} 77 | snippets.append(element) 78 | description = kg.get("description") 79 | if description: 80 | element = {"content": description, "source": "None"} 81 | snippets.append(element) 82 | for attribute, value in kg.get("attributes", {}).items(): 83 | element = {"content": f"{attribute}: {value}", 84 | "source": "None"} 85 | snippets.append(element) 86 | 87 | for result in results["organic"][: self.k]: 88 | if "snippet" in result: 89 | element = { 90 | "content": result["snippet"], "source": result["link"]} 91 | snippets.append(element) 92 | for attribute, value in result.get("attributes", {}).items(): 93 | element = { 94 | "content": f"{attribute}: {value}", 95 | "source": result["link"], 96 | } 97 | snippets.append(element) 98 | 99 | if len(snippets) == 0: 100 | element = { 101 | "content": "No good Google Search Result was found", 102 | "source": "None", 103 | } 104 | return [element] 105 | except: 106 | element = { 107 | "content": "No good Google Search Result was found", 108 | "source": "None", 109 | } 110 | return [element] 111 | 112 | # keep only the first k snippets 113 | snippets = snippets[: int(self.k / 2)] 114 | 115 | return snippets 116 | 117 | async def parallel_searches(self, search_queries, gl, hl): 118 | async with aiohttp.ClientSession() as session: 119 | tasks = [ 120 | self._google_serper_search_results(session, query, gl, hl) 121 | for query in search_queries 122 | ] 123 | search_results = await asyncio.gather(*tasks, return_exceptions=True) 124 | return search_results 125 | 126 | def run(self, queries: list[str]) -> list[list]: 127 | """Run query through GoogleSearch and parse result.""" 128 | results = asyncio.run(self.parallel_searches( 129 | queries, gl=self.gl, hl=self.hl)) 130 | snippets_list = [] 131 | for i in range(len(results)): 132 | snippets_list.append(self._parse_results(results[i])) 133 | return snippets_list 134 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/matrix_degree_confidence.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | from transformers import DebertaForSequenceClassification, DebertaTokenizer 5 | from TruthTorchLM.utils import calculate_C_deg 6 | from .truth_method import TruthMethod 7 | from ..generation import sample_generations_hf_local, sample_generations_api 8 | 9 | 10 | class MatrixDegreeConfidence(TruthMethod): 11 | 12 | REQUIRES_SAMPLED_TEXT = True 13 | 14 | def __init__( 15 | self, 16 | method_for_similarity: str = "semantic", 17 | number_of_generations=5, 18 | model_for_entailment: PreTrainedModel = None, 19 | tokenizer_for_entailment: PreTrainedTokenizer = None, 20 | temperature=3.0, 21 | entailment_model_device="cuda", 22 | batch_generation=True, 23 | ): 24 | 25 | super().__init__() 26 | 27 | if ( 28 | model_for_entailment is None or tokenizer_for_entailment is None 29 | ) and method_for_similarity == "semantic": 30 | model_for_entailment = DebertaForSequenceClassification.from_pretrained( 31 | "microsoft/deberta-large-mnli" 32 | ).to(entailment_model_device) 33 | tokenizer_for_entailment = DebertaTokenizer.from_pretrained( 34 | "microsoft/deberta-large-mnli" 35 | ) 36 | 37 | if method_for_similarity not in ["semantic", "jaccard"]: 38 | raise ValueError( 39 | "method_for_similarity should be either semantic or jaccard. Please refer to https://arxiv.org/pdf/2305.19187 for more information." 40 | ) 41 | 42 | self.model_for_entailment = None 43 | self.tokenizer_for_entailment = None 44 | 45 | if method_for_similarity == "semantic": 46 | print( 47 | 'There are 2 methods for similarity: semantic similarity and jaccard score. The default method is semantic similarity. If you want to use jaccard score, please set method_for_similarity="jaccard". Please refer to https://arxiv.org/pdf/2305.19187 for more information.' 48 | ) 49 | self.tokenizer_for_entailment = tokenizer_for_entailment 50 | self.model_for_entailment = model_for_entailment 51 | 52 | self.number_of_generations = number_of_generations 53 | self.method_for_similarity = method_for_similarity # jaccard or semantic 54 | self.temperature = temperature # temperature for NLI model 55 | self.batch_generation = batch_generation 56 | 57 | def forward_hf_local( 58 | self, 59 | model: PreTrainedModel, 60 | input_text: str, 61 | generated_text: str, 62 | question: str, 63 | all_ids: Union[list, torch.Tensor], 64 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 65 | generation_seed=None, 66 | sampled_generations_dict: dict = None, 67 | messages: list = [], 68 | context: str = "", 69 | **kwargs 70 | ): 71 | 72 | if sampled_generations_dict is None: 73 | sampled_generations_dict = sample_generations_hf_local( 74 | model=model, 75 | input_text=input_text, 76 | tokenizer=tokenizer, 77 | generation_seed=generation_seed, 78 | number_of_generations=self.number_of_generations - 1, 79 | return_text=True, 80 | batch_generation=self.batch_generation, 81 | **kwargs 82 | ) 83 | 84 | generated_texts = sampled_generations_dict["generated_texts"][ 85 | : self.number_of_generations - 1 86 | ] 87 | 88 | generated_texts.append(generated_text) 89 | 90 | output_dict = self._matrix_degree_confidence( 91 | generated_texts, question, index=len(generated_texts) - 1 92 | ) 93 | return output_dict 94 | 95 | def forward_api( 96 | self, 97 | model: str, 98 | messages: list, 99 | generated_text: str, 100 | question: str, 101 | generation_seed=None, 102 | sampled_generations_dict: dict = None, 103 | logprobs: list = None, 104 | generated_tokens: list = None, 105 | context: str = "", 106 | **kwargs 107 | ): 108 | 109 | if sampled_generations_dict is None: 110 | sampled_generations_dict = sample_generations_api( 111 | model=model, 112 | messages=messages, 113 | generation_seed=generation_seed, 114 | number_of_generations=self.number_of_generations - 1, 115 | return_text=True, 116 | **kwargs 117 | ) 118 | 119 | generated_texts = sampled_generations_dict["generated_texts"][ 120 | : self.number_of_generations - 1 121 | ] 122 | generated_texts.append(generated_text) 123 | 124 | output_dict = self._matrix_degree_confidence( 125 | generated_texts, question, index=len(generated_texts) - 1 126 | ) 127 | return output_dict 128 | 129 | def _matrix_degree_confidence( 130 | self, generated_texts: list, question: str, index=-1 131 | ): 132 | 133 | output_dict = {} 134 | output = calculate_C_deg( 135 | generated_texts, 136 | question, 137 | index=index, 138 | method_for_similarity=self.method_for_similarity, 139 | temperature=self.temperature, 140 | model_for_entailment=self.model_for_entailment, 141 | tokenizer_for_entailment=self.tokenizer_for_entailment, 142 | ) 143 | output_dict["C_deg"] = output 144 | output_dict["generated_texts"] = generated_texts 145 | output_dict["truth_value"] = output 146 | 147 | return output_dict 148 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/eccentricity_confidence.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | from transformers import DebertaForSequenceClassification, DebertaTokenizer 5 | from TruthTorchLM.utils import calculate_C_ecc 6 | from .truth_method import TruthMethod 7 | from ..generation import sample_generations_hf_local, sample_generations_api 8 | 9 | 10 | class EccentricityConfidence(TruthMethod): 11 | 12 | REQUIRES_SAMPLED_TEXT = True 13 | 14 | def __init__( 15 | self, 16 | method_for_similarity: str = "semantic", 17 | number_of_generations=5, 18 | model_for_entailment: PreTrainedModel = None, 19 | tokenizer_for_entailment: PreTrainedTokenizer = None, 20 | temperature=3.0, 21 | eigen_threshold=0.9, 22 | entailment_model_device="cuda", 23 | batch_generation=True, 24 | ): 25 | super().__init__() 26 | 27 | if ( 28 | model_for_entailment is None or tokenizer_for_entailment is None 29 | ) and method_for_similarity == "semantic": 30 | model_for_entailment = DebertaForSequenceClassification.from_pretrained( 31 | "microsoft/deberta-large-mnli" 32 | ).to(entailment_model_device) 33 | tokenizer_for_entailment = DebertaTokenizer.from_pretrained( 34 | "microsoft/deberta-large-mnli" 35 | ) 36 | 37 | if method_for_similarity not in ["semantic", "jaccard"]: 38 | raise ValueError( 39 | "method_for_similarity should be either semantic or jaccard. Please refer to https://arxiv.org/pdf/2305.19187 for more information." 40 | ) 41 | 42 | self.model_for_entailment = None 43 | self.tokenizer_for_entailment = None 44 | 45 | if method_for_similarity == "semantic": 46 | print( 47 | 'There are 2 methods for similarity: semantic similarity and jaccard score. The default method is semantic similarity. If you want to use jaccard score, please set method_for_similarity="jaccard". Please refer to https://arxiv.org/pdf/2305.19187 for more information.' 48 | ) 49 | self.tokenizer_for_entailment = tokenizer_for_entailment 50 | self.model_for_entailment = model_for_entailment 51 | 52 | self.number_of_generations = number_of_generations 53 | self.method_for_similarity = method_for_similarity # jaccard or semantic 54 | self.eigen_threshold = eigen_threshold 55 | self.temperature = temperature # temperature for NLI model 56 | self.batch_generation = batch_generation 57 | 58 | def _eccentricity_confidence( 59 | self, generated_texts: list, question: str, index=-1 60 | ): 61 | output_dict = {} 62 | output = calculate_C_ecc( 63 | generated_texts, 64 | question, 65 | method_for_similarity=self.method_for_similarity, 66 | temperature=self.temperature, 67 | eigen_threshold=self.eigen_threshold, 68 | model_for_entailment=self.model_for_entailment, 69 | tokenizer_for_entailment=self.tokenizer_for_entailment, 70 | ) 71 | output_dict["C_ecc"] = output 72 | output_dict["generated_texts"] = generated_texts 73 | output_dict["truth_value"] = -output 74 | return output_dict 75 | 76 | def forward_hf_local( 77 | self, 78 | model: PreTrainedModel, 79 | input_text: str, 80 | generated_text: str, 81 | question: str, 82 | all_ids: Union[list, torch.Tensor], 83 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 84 | generation_seed=None, 85 | sampled_generations_dict: dict = None, 86 | messages: list = [], 87 | context: str = "", 88 | **kwargs 89 | ): 90 | 91 | if sampled_generations_dict is None: 92 | sampled_generations_dict = sample_generations_hf_local( 93 | model=model, 94 | input_text=input_text, 95 | tokenizer=tokenizer, 96 | generation_seed=generation_seed, 97 | number_of_generations=self.number_of_generations - 1, 98 | return_text=True, 99 | batch_generation=self.batch_generation, 100 | **kwargs 101 | ) 102 | 103 | generated_texts = sampled_generations_dict["generated_texts"][ 104 | : self.number_of_generations - 1 105 | ] 106 | generated_texts.append(generated_text) 107 | 108 | return self._eccentricity_confidence( 109 | generated_texts, question, index=len(generated_texts) - 1 110 | ) 111 | 112 | def forward_api( 113 | self, 114 | model: str, 115 | messages: list, 116 | generated_text: str, 117 | question: str, 118 | generation_seed=None, 119 | sampled_generations_dict: dict = None, 120 | logprobs: list = None, 121 | generated_tokens: list = None, 122 | context: str = "", 123 | **kwargs 124 | ): 125 | 126 | if sampled_generations_dict is None: 127 | sampled_generations_dict = sample_generations_api( 128 | model=model, 129 | messages=messages, 130 | generation_seed=generation_seed, 131 | number_of_generations=self.number_of_generations - 1, 132 | return_text=True, 133 | **kwargs 134 | ) 135 | 136 | generated_texts = sampled_generations_dict["generated_texts"][ 137 | : self.number_of_generations - 1 138 | ] 139 | generated_texts.append(generated_text) 140 | 141 | return self._eccentricity_confidence( 142 | generated_texts, question, index=len(generated_texts) - 1 143 | ) 144 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/verbalized_confidence.py: -------------------------------------------------------------------------------- 1 | from .truth_method import TruthMethod 2 | from TruthTorchLM.utils import fix_tokenizer_chat 3 | from typing import Union 4 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 5 | from TruthTorchLM.utils.common_utils import generate 6 | from ..generation import sample_generations_api 7 | from TruthTorchLM.templates import VC_SYSTEM_PROMPT, VC_USER_PROMPT, VC_USER_PROMPT_WITH_CONTEXT 8 | import torch 9 | import copy 10 | 11 | 12 | 13 | 14 | 15 | class VerbalizedConfidence(TruthMethod): 16 | 17 | def __init__( 18 | self, 19 | system_prompt: str = VC_SYSTEM_PROMPT, 20 | user_prompt: str = VC_USER_PROMPT, 21 | with_context: bool = False, 22 | max_new_tokens=1024, 23 | **generation_kwargs 24 | ): 25 | super().__init__() 26 | self.system_prompt = system_prompt 27 | self.user_prompt = user_prompt 28 | self.max_new_tokens = max_new_tokens 29 | self.generation_kwargs = generation_kwargs 30 | self.with_context = with_context 31 | 32 | if with_context and '{context}' not in self.user_prompt:#check if the prompt has a context field 33 | print("Context field is required in user prompt for with_context=True, swithing to the default user prompt with context") 34 | self.user_prompt = VC_USER_PROMPT_WITH_CONTEXT 35 | 36 | def extract_confidence(self, confidence_text): 37 | """Extracts the confidence value from the confidence text. The text may include non-numeric characters.""" 38 | confidence_text = confidence_text.strip() 39 | confidence_text = "".join( 40 | [c for c in confidence_text if c.isdigit() or c == "."] 41 | ) 42 | try: 43 | return float(confidence_text) 44 | except: 45 | return 0.0 46 | 47 | def forward_hf_local( 48 | self, 49 | model: PreTrainedModel, 50 | input_text: str, 51 | generated_text: str, 52 | question: str, 53 | all_ids: Union[list, torch.Tensor], 54 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 55 | generation_seed=None, 56 | sampled_generations_dict: dict = None, 57 | messages: list = [], 58 | context: str = "", 59 | **kwargs 60 | ): 61 | if self.with_context == False: 62 | chat = [ 63 | {"role": "system", "content": self.system_prompt}, 64 | { 65 | "role": "user", 66 | "content": self.user_prompt.format( 67 | question=question, generated_text=generated_text 68 | ), 69 | }, 70 | ] 71 | else: 72 | chat = [ 73 | {"role": "system", "content": self.system_prompt}, 74 | { 75 | "role": "user", 76 | "content": self.user_prompt.format( 77 | question=question, generated_text=generated_text, context=context 78 | ), 79 | }, 80 | ] 81 | 82 | tokenizer, chat = fix_tokenizer_chat( 83 | tokenizer, chat 84 | ) # in case some tokenizers don't have chat template and don't support system prompt 85 | prompt = tokenizer.apply_chat_template( 86 | chat, 87 | tokenize=False, 88 | add_generation_prompt=True, 89 | continue_final_message=False, 90 | ) 91 | 92 | kwargs = copy.deepcopy(kwargs) 93 | kwargs.pop("do_sample", None) 94 | generation_dict = generate( 95 | prompt, 96 | model, 97 | tokenizer, 98 | do_sample=False, 99 | max_new_tokens=self.max_new_tokens, 100 | **self.generation_kwargs 101 | ) 102 | confidence_text = generation_dict["generated_text_skip_specials"] 103 | confidence = self.extract_confidence(confidence_text) 104 | confidence = confidence / 100.0 105 | 106 | return {"truth_value": confidence, "confidence_text": confidence_text} 107 | 108 | def forward_api( 109 | self, 110 | model: str, 111 | messages: list, 112 | generated_text: str, 113 | question: str, 114 | generation_seed=None, 115 | sampled_generations_dict: dict = None, 116 | logprobs: list = None, 117 | generated_tokens: list = None, 118 | context: str = "", 119 | **kwargs 120 | ): 121 | if self.with_context == False: 122 | chat = [ 123 | {"role": "system", "content": self.system_prompt}, 124 | { 125 | "role": "user", 126 | "content": self.user_prompt.format( 127 | question=question, generated_text=generated_text 128 | ), 129 | }, 130 | ] 131 | else: 132 | chat = [ 133 | {"role": "system", "content": self.system_prompt}, 134 | { 135 | "role": "user", 136 | "content": self.user_prompt.format( 137 | question=question, generated_text=generated_text, context=context 138 | ), 139 | }, 140 | ] 141 | 142 | sampled_generations_dict = sample_generations_api( 143 | model=model, 144 | messages=chat, 145 | generation_seed=generation_seed, 146 | number_of_generations=1, 147 | return_text=True, 148 | **kwargs 149 | ) 150 | confidence_text = sampled_generations_dict["generated_texts"][0] 151 | confidence = self.extract_confidence(confidence_text) 152 | confidence = confidence / 100.0 153 | return {"truth_value": confidence, "confidence_text": confidence_text} 154 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/semantic_entropy.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import ( 4 | PreTrainedModel, 5 | PreTrainedTokenizer, 6 | PreTrainedTokenizerFast, 7 | DebertaForSequenceClassification, 8 | DebertaTokenizer, 9 | ) 10 | from TruthTorchLM.utils import bidirectional_entailment_clustering 11 | from .truth_method import TruthMethod 12 | from TruthTorchLM.scoring_methods import ScoringMethod, LengthNormalizedScoring 13 | from ..generation import sample_generations_hf_local, sample_generations_api 14 | 15 | 16 | def calculate_total_log(generated_outputs: list[str, float], clusters: list[set[str]]): 17 | total_output_for_log = 0 18 | for i, cluster in enumerate(clusters): 19 | score_list = [] 20 | for elem in cluster: 21 | for output in generated_outputs: 22 | if elem == output[0]: 23 | score_list.append(output[1]) 24 | total_output_for_log -= torch.logsumexp( 25 | torch.tensor(score_list), dim=0).item() 26 | return total_output_for_log / len(clusters) 27 | 28 | 29 | class SemanticEntropy(TruthMethod): 30 | REQUIRES_SAMPLED_TEXT = True 31 | REQUIRES_SAMPLED_LOGPROBS = True 32 | 33 | def __init__( 34 | self, 35 | scoring_function: ScoringMethod = LengthNormalizedScoring(), 36 | number_of_generations=5, 37 | model_for_entailment: PreTrainedModel = None, 38 | tokenizer_for_entailment: PreTrainedTokenizer = None, 39 | entailment_model_device="cuda", 40 | batch_generation=True, 41 | ): # normalization 42 | super().__init__() 43 | 44 | if model_for_entailment is None or tokenizer_for_entailment is None: 45 | model_for_entailment = DebertaForSequenceClassification.from_pretrained( 46 | "microsoft/deberta-large-mnli" 47 | ).to(entailment_model_device) 48 | tokenizer_for_entailment = DebertaTokenizer.from_pretrained( 49 | "microsoft/deberta-large-mnli" 50 | ) 51 | 52 | self.model_for_entailment = model_for_entailment 53 | self.tokenizer_for_entailment = tokenizer_for_entailment 54 | self.scoring_function = scoring_function 55 | self.number_of_generations = number_of_generations 56 | self.batch_generation = batch_generation 57 | 58 | def _semantic_entropy( 59 | self, 60 | generated_texts: list[str], 61 | question: str, 62 | scores: list[float], 63 | generated_outputs: list, 64 | ): 65 | 66 | clusters = bidirectional_entailment_clustering( 67 | self.model_for_entailment, 68 | self.tokenizer_for_entailment, 69 | question, 70 | generated_texts, 71 | ) 72 | total_output_for_log = calculate_total_log(generated_outputs, clusters) 73 | 74 | return { 75 | "truth_value": -total_output_for_log, 76 | "semantic_entropy": total_output_for_log, 77 | "score_for_each_generation": scores, 78 | "generated_texts": generated_texts, 79 | "clusters": clusters, 80 | } 81 | 82 | def forward_hf_local( 83 | self, 84 | model: PreTrainedModel, 85 | input_text: str, 86 | generated_text: str, 87 | question: str, 88 | all_ids: Union[list, torch.Tensor], 89 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 90 | generation_seed=None, 91 | sampled_generations_dict: dict = None, 92 | messages: list = [], 93 | context: str = "", 94 | **kwargs 95 | ): 96 | if sampled_generations_dict is None: 97 | sampled_generations_dict = sample_generations_hf_local( 98 | model=model, 99 | input_text=input_text, 100 | tokenizer=tokenizer, 101 | generation_seed=generation_seed, 102 | number_of_generations=self.number_of_generations, 103 | return_text=True, 104 | return_logprobs=True, 105 | batch_generation=self.batch_generation, 106 | **kwargs 107 | ) 108 | 109 | generated_texts = sampled_generations_dict["generated_texts"][ 110 | : self.number_of_generations 111 | ] 112 | generated_outputs = [] 113 | scores = [] 114 | 115 | for i in range(self.number_of_generations): 116 | text = generated_texts[i] 117 | score = self.scoring_function( 118 | sampled_generations_dict["logprobs"][i]) 119 | scores.append(score) # scores are in log scale 120 | generated_outputs.append((text, score)) 121 | 122 | return self._semantic_entropy( 123 | generated_texts, question, scores, generated_outputs 124 | ) 125 | 126 | def forward_api( 127 | self, 128 | model: str, 129 | messages: list, 130 | generated_text: str, 131 | question: str, 132 | generation_seed=None, 133 | sampled_generations_dict: dict = None, 134 | logprobs: list = None, 135 | generated_tokens: list = None, 136 | context: str = "", 137 | **kwargs 138 | ): 139 | if sampled_generations_dict is None: 140 | sampled_generations_dict = sample_generations_api( 141 | model=model, 142 | messages=messages, 143 | generation_seed=generation_seed, 144 | number_of_generations=self.number_of_generations, 145 | return_text=True, 146 | return_logprobs=True, 147 | **kwargs 148 | ) 149 | 150 | generated_texts = sampled_generations_dict["generated_texts"][ 151 | : self.number_of_generations 152 | ] 153 | generated_outputs = [] 154 | scores = [] 155 | 156 | for i in range(self.number_of_generations): 157 | text = generated_texts[i] 158 | score = self.scoring_function( 159 | sampled_generations_dict["logprobs"][i]) 160 | scores.append(score) # scores are in log scale 161 | generated_outputs.append((text, score)) 162 | 163 | return self._semantic_entropy( 164 | generated_texts, question, scores, generated_outputs 165 | ) 166 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/sentSAR.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 4 | from .truth_method import TruthMethod 5 | from TruthTorchLM.scoring_methods import ScoringMethod, LengthNormalizedScoring 6 | from ..generation import sample_generations_hf_local, sample_generations_api 7 | from TruthTorchLM.error_handler import handle_logprobs_error 8 | from sentence_transformers.cross_encoder import CrossEncoder 9 | 10 | 11 | class SentSAR(TruthMethod): 12 | 13 | REQUIRES_SAMPLED_TEXT = True 14 | REQUIRES_SAMPLED_LOGPROBS = True 15 | 16 | def __init__( 17 | self, 18 | scoring_function: ScoringMethod = LengthNormalizedScoring(), 19 | number_of_generations=5, 20 | t=0.001, 21 | model_for_similarity=None, 22 | similarity_model_device="cuda", 23 | batch_generation=True, 24 | ): # normalization 25 | super().__init__() 26 | if model_for_similarity is None: 27 | self.model_for_similarity = CrossEncoder( 28 | "cross-encoder/stsb-roberta-large", 29 | num_labels=1, 30 | device=similarity_model_device, 31 | ) 32 | else: 33 | self.model_for_similarity = model_for_similarity 34 | 35 | self.scoring_function = scoring_function 36 | self.number_of_generations = number_of_generations 37 | self.t = t 38 | self.batch_generation = batch_generation 39 | 40 | def _sentsar( 41 | self, 42 | generated_texts: list[str], 43 | question: str, 44 | scores: list[float], 45 | sampled_generations_dict: dict, 46 | ): 47 | 48 | similarities = {} 49 | for i in range(len(generated_texts)): 50 | similarities[i] = [] 51 | 52 | for i in range(len(generated_texts)): 53 | for j in range(i + 1, len(generated_texts)): 54 | gen_i = question + generated_texts[i] 55 | gen_j = question + generated_texts[j] 56 | similarity_i_j = self.model_for_similarity.predict( 57 | [gen_i, gen_j]) 58 | similarities[i].append(similarity_i_j) 59 | similarities[j].append(similarity_i_j) 60 | 61 | probs = torch.exp(torch.tensor(scores)) 62 | assert len(probs) == len(similarities) 63 | 64 | sentence_scores = [] 65 | for idx, prob in enumerate(probs): 66 | w_ent = -torch.log( 67 | prob 68 | + ( 69 | (torch.tensor(similarities[idx]) / self.t) 70 | * torch.cat([probs[:idx], probs[idx + 1:]]) 71 | ).sum() 72 | ) 73 | sentence_scores.append(w_ent) 74 | sentence_scores = torch.tensor(sentence_scores) 75 | 76 | entropy = ( 77 | torch.sum(sentence_scores, dim=0) / 78 | torch.tensor(sentence_scores.shape[0]) 79 | ).item() 80 | return { 81 | "truth_value": -entropy, 82 | "sentSAR": entropy, 83 | "score_for_each_generation": scores, 84 | "generated_texts": generated_texts, 85 | "similarities": similarities, 86 | } 87 | 88 | def forward_hf_local( 89 | self, 90 | model: PreTrainedModel, 91 | input_text: str, 92 | generated_text: str, 93 | question: str, 94 | all_ids: Union[list, torch.Tensor], 95 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 96 | generation_seed=None, 97 | sampled_generations_dict: dict = None, 98 | messages: list = [], 99 | context: str = "", 100 | **kwargs 101 | ): 102 | 103 | if sampled_generations_dict is None: 104 | sampled_generations_dict = sample_generations_hf_local( 105 | model=model, 106 | input_text=input_text, 107 | tokenizer=tokenizer, 108 | generation_seed=generation_seed, 109 | number_of_generations=self.number_of_generations, 110 | return_text=True, 111 | return_logprobs=True, 112 | batch_generation=self.batch_generation, 113 | **kwargs 114 | ) 115 | 116 | generated_texts = sampled_generations_dict["generated_texts"][ 117 | : self.number_of_generations 118 | ] 119 | 120 | scores = [] 121 | for i in range(self.number_of_generations): 122 | tokens_text = [ 123 | tokenizer.decode([token]) 124 | for token in sampled_generations_dict["tokens"][i] 125 | ] 126 | score = self.scoring_function( 127 | sampled_generations_dict["logprobs"][i]) 128 | scores.append(score) # scores are in log scale 129 | 130 | return self._sentsar( 131 | generated_texts, question, scores, sampled_generations_dict 132 | ) 133 | 134 | @handle_logprobs_error 135 | def forward_api( 136 | self, 137 | model: str, 138 | messages: list, 139 | generated_text: str, 140 | question: str, 141 | generation_seed=None, 142 | sampled_generations_dict: dict = None, 143 | logprobs: list = None, 144 | generated_tokens: list = None, 145 | context: str = "", 146 | **kwargs 147 | ): 148 | 149 | if sampled_generations_dict is None: 150 | sampled_generations_dict = sample_generations_api( 151 | model=model, 152 | messages=messages, 153 | generation_seed=generation_seed, 154 | number_of_generations=self.number_of_generations, 155 | return_text=True, 156 | return_logprobs=True, 157 | **kwargs 158 | ) 159 | 160 | generated_texts = sampled_generations_dict["generated_texts"][ 161 | : self.number_of_generations 162 | ] 163 | 164 | scores = [] 165 | for i in range(self.number_of_generations): 166 | score = self.scoring_function( 167 | sampled_generations_dict["logprobs"][i]) 168 | scores.append(score) # scores are in log scale 169 | 170 | return self._sentsar( 171 | generated_texts, question, scores, sampled_generations_dict 172 | ) 173 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/context_check.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | import copy 4 | from .truth_method import TruthMethod 5 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast, AutoModelForCausalLM, AutoTokenizer 6 | from litellm import completion 7 | 8 | # Inspired by Minicheck - paper link: https://arxiv.org/abs/2404.10774 9 | 10 | """ 11 | Input: Question (question_context), Answer (generated_text), Document (context) 12 | Output: Support score in [0, 1] 13 | """ 14 | 15 | DOCUMENT = "Document: " 16 | CLAIM = "Claim: " 17 | JUDGE_PROMPT = """You are an expert in fact-checking and entailment classification. 18 | Given a background document and a claim, your task is to assess how well the claim is supported by the document. 19 | 20 | ### Task: 21 | 1. Read the provided **document** carefully. 22 | 2. Analyze the **claim** and compare it with the document to determine the level of support. 23 | 3. Assign a **truth score** between 0 and 1, where: 24 | - **0** means the document **does not support** the claim. 25 | - **1** means the document **fully supports** the claim. 26 | - Any value in between reflects partial support. 27 | 28 | ### Important: 29 | - Your response **must be a single number between 0 and 1**. 30 | - Do **not** include any explanation or additional text. 31 | - Only return the numerical score as plain text. 32 | """ 33 | 34 | 35 | class ContextCheck(TruthMethod): 36 | REQUIRES_SAMPLED_TEXT = False 37 | REQUIRES_SAMPLED_LOGPROBS = False 38 | REQUIRES_NORMALIZATION = False 39 | 40 | def __init__(self, check_model:Union[PreTrainedModel, str], 41 | check_tokenizer:Union[PreTrainedTokenizer, PreTrainedTokenizerFast]=None, temperature=1.0, **generation_kwargs): 42 | super().__init__() 43 | self.check_model = check_model 44 | if not isinstance(self.check_model, str) and check_tokenizer is None: 45 | raise ValueError("tokenizer of the model must be provided with check_tokenizer argument.") 46 | self.check_tokenizer = check_tokenizer 47 | self.temperature = temperature 48 | self.generation_kwargs = generation_kwargs 49 | 50 | def extract_number(self, text): 51 | """Extracts the number from the text. The text may include non-numeric characters.""" 52 | text = text.strip() 53 | text = "".join( 54 | [c for c in text if c.isdigit() or c == "."] 55 | ) 56 | try: 57 | return float(text) 58 | except: 59 | return 0.0 60 | 61 | def forward_hf_local( 62 | self, 63 | model: PreTrainedModel, 64 | input_text: str, 65 | generated_text: str, 66 | question: str, 67 | all_ids: Union[list, torch.Tensor], 68 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 69 | generation_seed=None, 70 | sampled_generations_dict: dict = None, 71 | messages: list = [], 72 | context: str = "", 73 | **kwargs, 74 | ): 75 | 76 | if isinstance(self.check_model, str): 77 | truth_score, truth_label = self.api_context_check(context, generated_text, self.check_model) 78 | else: 79 | truth_score, truth_label = self.hf_local_context_check(context, generated_text, self.check_model, self.check_tokenizer) 80 | 81 | return {'truth_value': truth_score, 'binary_prediction': truth_label} 82 | 83 | 84 | def forward_api( 85 | self, 86 | model: str, 87 | messages: list, 88 | generated_text: str, 89 | question: str, 90 | generation_seed=None, 91 | sampled_generations_dict: dict = None, 92 | logprobs: list = None, 93 | generated_tokens: list = None, 94 | context: str = "", 95 | **kwargs, 96 | ): 97 | if isinstance(self.check_model, str): 98 | truth_score, truth_label = self.api_context_check(context, generated_text, self.check_model) 99 | else: 100 | truth_score, truth_label = self.hf_local_context_check(context, generated_text, self.check_model, self.check_tokenizer) 101 | 102 | return {'truth_value': truth_score, 'binary_prediction': truth_label} 103 | 104 | 105 | 106 | def hf_local_context_check(self, context, generated_text, model, tokenizer): 107 | judge_content = JUDGE_PROMPT 108 | judge_content += DOCUMENT.replace('', context) 109 | judge_content += CLAIM.replace('', generated_text) 110 | judge_prompt = [{"role": "user", "content": judge_content}] 111 | 112 | text = tokenizer.apply_chat_template(judge_prompt, tokenize=False) 113 | inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True) 114 | input_ids = inputs['input_ids'].to(model.device) 115 | attention_mask = inputs['attention_mask'].to(model.device) 116 | model_output = model.generate( 117 | input_ids=input_ids, 118 | attention_mask=attention_mask, 119 | temperature=self.temperature, 120 | **self.generation_kwargs 121 | ) 122 | tokens = model_output[0][len(input_ids[0]):] 123 | truth_score = tokenizer.decode(tokens, skip_special_tokens=False) 124 | try: 125 | truth_score = self.extract_number(truth_score) 126 | truth_label = 1 if truth_score >= 0.5 else 0 127 | except ValueError: 128 | print(f"Expected a numerical response between 0 and 1, but got: {truth_score}. Returning 0.5 as default.") 129 | truth_score = 0.5 130 | truth_label = 0 131 | return truth_score, truth_label 132 | 133 | def api_context_check(self, context, generated_text, model): 134 | judge_content = JUDGE_PROMPT 135 | judge_content += DOCUMENT.replace('', context) 136 | judge_content += CLAIM.replace('', generated_text) 137 | judge_prompt = [{"role": "user", "content": judge_content}] 138 | 139 | truth_score = completion(model = model, messages = judge_prompt, temperature=self.temperature, **self.generation_kwargs) 140 | truth_score = truth_score.choices[0].message.content 141 | try: 142 | truth_score = self.extract_number(truth_score) 143 | truth_label = 1 if truth_score >= 0.5 else 0 144 | except ValueError: 145 | print(f"Expected a numerical response between 0 and 1, but got: {truth_score}. Returning 0.5 as default.") 146 | truth_score = 0.5 147 | truth_label = 0 148 | return truth_score, truth_label -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/kernel_language_entropy.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Union 3 | from transformers import ( 4 | PreTrainedModel, 5 | PreTrainedTokenizer, 6 | PreTrainedTokenizerFast, 7 | DebertaForSequenceClassification, 8 | DebertaTokenizer, 9 | ) 10 | from TruthTorchLM.utils import ( 11 | calculate_affinity_matrix, 12 | calculate_laplacian, 13 | create_kernel, 14 | calculate_VNE, 15 | ) 16 | from .truth_method import TruthMethod 17 | from ..generation import sample_generations_hf_local, sample_generations_api 18 | 19 | 20 | class KernelLanguageEntropy(TruthMethod): 21 | REQUIRES_SAMPLED_TEXT = True 22 | REQUIRES_SAMPLED_LOGPROBS = False 23 | 24 | def __init__( 25 | self, 26 | number_of_generations=5, 27 | model_for_entailment: PreTrainedModel = None, 28 | tokenizer_for_entailment: PreTrainedTokenizer = None, 29 | entailment_model_device="cuda", 30 | kernel_type: str = "heat", 31 | normalize_laplacian: bool = False, 32 | temperature=0.3, 33 | smoothness=1.0, 34 | scale=1.0, 35 | ): 36 | super().__init__() 37 | 38 | if model_for_entailment is None or tokenizer_for_entailment is None: 39 | model_for_entailment = DebertaForSequenceClassification.from_pretrained( 40 | "microsoft/deberta-large-mnli" 41 | ).to(entailment_model_device) 42 | tokenizer_for_entailment = DebertaTokenizer.from_pretrained( 43 | "microsoft/deberta-large-mnli" 44 | ) 45 | 46 | self.model_for_entailment = model_for_entailment 47 | self.tokenizer_for_entailment = tokenizer_for_entailment 48 | self.number_of_generations = number_of_generations 49 | self.kernel_type = kernel_type 50 | self.normalize_laplacian = normalize_laplacian 51 | self.temperature = temperature 52 | self.smoothness = smoothness 53 | self.scale = scale 54 | 55 | def forward_hf_local( 56 | self, 57 | model: PreTrainedModel, 58 | input_text: str, 59 | generated_text: str, 60 | question: str, 61 | all_ids: Union[list, torch.Tensor], 62 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 63 | generation_seed=None, 64 | sampled_generations_dict: dict = None, 65 | messages: list = [], 66 | context: str = "", 67 | **kwargs 68 | ): 69 | 70 | if sampled_generations_dict is None: 71 | sampled_generations_dict = sample_generations_hf_local( 72 | model=model, 73 | input_text=input_text, 74 | tokenizer=tokenizer, 75 | generation_seed=generation_seed, 76 | number_of_generations=self.number_of_generations, 77 | return_text=True, 78 | return_logprobs=False, 79 | **kwargs 80 | ) 81 | 82 | generated_texts = sampled_generations_dict["generated_texts"][ 83 | : self.number_of_generations 84 | ] 85 | semantic_graph, kernel, kernel_entropy = self.calculate_kernel_language_entropy( 86 | texts=generated_texts, 87 | context=question, 88 | method_for_similarity="kernel", 89 | model_for_entailment=self.model_for_entailment, 90 | tokenizer_for_entailment=self.tokenizer_for_entailment, 91 | normalize_laplacian=self.normalize_laplacian, 92 | kernel_type=self.kernel_type, 93 | temperature=self.temperature, 94 | smoothness=self.smoothness, 95 | scale=self.scale, 96 | ) 97 | 98 | return { 99 | "truth_value": -kernel_entropy, 100 | "generated_texts": generated_texts, 101 | "kernel": kernel, 102 | "semantic_graph": semantic_graph, 103 | } 104 | 105 | def forward_api( 106 | self, 107 | model: str, 108 | messages: list, 109 | generated_text: str, 110 | question: str, 111 | generation_seed=None, 112 | sampled_generations_dict: dict = None, 113 | logprobs: list = None, 114 | generated_tokens: list = None, 115 | context: str = "", 116 | **kwargs 117 | ): 118 | 119 | if sampled_generations_dict is None: 120 | sampled_generations_dict = sample_generations_api( 121 | model=model, 122 | messages=messages, 123 | generation_seed=generation_seed, 124 | number_of_generations=self.number_of_generations, 125 | return_text=True, 126 | return_logprobs=False, 127 | **kwargs 128 | ) 129 | 130 | generated_texts = sampled_generations_dict["generated_texts"][ 131 | : self.number_of_generations 132 | ] 133 | 134 | # # KLE part 135 | semantic_graph, kernel, kernel_entropy = self.calculate_kernel_language_entropy( 136 | texts=generated_texts, 137 | context=question, 138 | method_for_similarity="kernel", 139 | model_for_entailment=self.model_for_entailment, 140 | tokenizer_for_entailment=self.tokenizer_for_entailment, 141 | normalize_laplacian=self.normalize_laplacian, 142 | kernel_type=self.kernel_type, 143 | temperature=self.temperature, 144 | smoothness=self.smoothness, 145 | scale=self.scale, 146 | ) 147 | 148 | return { 149 | "truth_value": -kernel_entropy, 150 | "generated_texts": generated_texts, 151 | "kernel": kernel, 152 | "semantic_graph": semantic_graph, 153 | } 154 | 155 | def calculate_kernel_language_entropy( 156 | self, 157 | texts: list[str], 158 | context: str, 159 | method_for_similarity: str, 160 | model_for_entailment: PreTrainedModel, 161 | tokenizer_for_entailment: PreTrainedTokenizer, 162 | normalize_laplacian: bool, 163 | kernel_type: str, 164 | temperature: float, 165 | smoothness: float, 166 | scale: float, 167 | ): 168 | semantic_graph = calculate_affinity_matrix( 169 | texts=texts, 170 | context=context, 171 | method_for_similarity=method_for_similarity, 172 | model_for_entailment=model_for_entailment, 173 | tokenizer_for_entailment=tokenizer_for_entailment, 174 | ) 175 | graph_laplacian = calculate_laplacian( 176 | graph=semantic_graph, normalize=normalize_laplacian 177 | ) 178 | kernel = create_kernel( 179 | laplacian=graph_laplacian, 180 | kernel_type=kernel_type, 181 | temperature=temperature, 182 | smoothness=smoothness, 183 | scale=scale, 184 | ) 185 | kernel_entropy = calculate_VNE(kernel) 186 | return semantic_graph, kernel, kernel_entropy 187 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/utils/safe_utils.py: -------------------------------------------------------------------------------- 1 | """Code taken from SAFE https://github.com/google-deepmind/long-form-factuality/blob/main/eval/safe/query_serper.py and https://github.com/google-deepmind/long-form-factuality/blob/main/common/utils.py and adapted to TruthTorchLM""" 2 | 3 | # Copyright 2024 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | """Class for querying the Google Serper API.""" 17 | 18 | import time 19 | import random 20 | from typing import Any, Optional, Literal, Union 21 | import requests 22 | import os 23 | import re 24 | import termcolor 25 | 26 | 27 | _SERPER_URL = "https://google.serper.dev" 28 | NO_RESULT_MSG = "No good Google Search result was found" 29 | 30 | 31 | def strip_string(s: str) -> str: 32 | """Strips a string of newlines and spaces.""" 33 | return s.strip(" \n") 34 | 35 | 36 | def clear_line() -> None: 37 | """Clears the current line.""" 38 | print(" " * os.get_terminal_size().columns, end="\r") 39 | 40 | 41 | def print_color(message: str, color: str) -> None: 42 | """Prints a message with a color.""" 43 | termcolor.cprint(message, color) 44 | 45 | 46 | def maybe_print_error( 47 | message: Union[str, Exception, None], 48 | additional_info: str = "", 49 | verbose: bool = False, 50 | ) -> None: 51 | """Prints the error message with additional info if flag is True.""" 52 | if not strip_string(str(message)): 53 | return 54 | 55 | error = type(message).__name__ if isinstance( 56 | message, Exception) else "ERROR" 57 | message = f"{error}: {str(message)}" 58 | message += f"\n{additional_info}" if verbose else "" 59 | clear_line() 60 | print_color(message, color="red") 61 | 62 | 63 | def extract_first_code_block(input_string: str, ignore_language: bool = False) -> str: 64 | """Extracts the contents of a string between the first code block (```).""" 65 | if ignore_language: 66 | pattern = re.compile(r"```(?:\w+\n)?(.*?)```", re.DOTALL) 67 | else: 68 | pattern = re.compile(r"```(.*?)```", re.DOTALL) 69 | 70 | match = pattern.search(input_string) 71 | return strip_string(match.group(1)) if match else "" 72 | 73 | 74 | def extract_first_square_brackets(input_string: str) -> str: 75 | """Extracts the contents of the FIRST string between square brackets.""" 76 | raw_result = re.findall(r"\[.*?\]", input_string, flags=re.DOTALL) 77 | 78 | if raw_result: 79 | return raw_result[0][1:-1] 80 | else: 81 | return "" 82 | 83 | 84 | def maybe_print_error( 85 | message: Union[str, Exception, None], 86 | additional_info: str = "", 87 | verbose: bool = False, 88 | ) -> None: 89 | """Prints the error message with additional info if flag is True.""" 90 | if not strip_string(str(message)): 91 | return 92 | 93 | error = type(message).__name__ if isinstance( 94 | message, Exception) else "ERROR" 95 | message = f"{error}: {str(message)}" 96 | message += f"\n{additional_info}" if verbose else "" 97 | clear_line() 98 | print_color(message, color="red") 99 | 100 | 101 | class SerperAPI: 102 | """Class for querying the Google Serper API.""" 103 | 104 | def __init__( 105 | self, 106 | gl: str = "us", 107 | hl: str = "en", 108 | k: int = 1, 109 | tbs: Optional[str] = None, 110 | search_type: Literal["news", "search", "places", "images"] = "search", 111 | ): 112 | self.serper_api_key = os.environ.get("SERPER_API_KEY", None) 113 | self.gl = gl 114 | self.hl = hl 115 | self.k = k 116 | self.tbs = tbs 117 | self.search_type = search_type 118 | self.result_key_for_type = { 119 | "news": "news", 120 | "places": "places", 121 | "images": "images", 122 | "search": "organic", 123 | } 124 | 125 | def run(self, query: str, **kwargs: Any) -> str: 126 | """Run query through GoogleSearch and parse result.""" 127 | assert self.serper_api_key, "Missing serper_api_key." 128 | results = self._google_serper_api_results( 129 | query, 130 | gl=self.gl, 131 | hl=self.hl, 132 | num=self.k, 133 | tbs=self.tbs, 134 | search_type=self.search_type, 135 | **kwargs, 136 | ) 137 | return self._parse_results(results) 138 | 139 | def _google_serper_api_results( 140 | self, 141 | search_term: str, 142 | search_type: str = "search", 143 | max_retries: int = 20, 144 | **kwargs: Any, 145 | ) -> dict[Any, Any]: 146 | """Run query through Google Serper.""" 147 | headers = { 148 | "X-API-KEY": self.serper_api_key or "", 149 | "Content-Type": "application/json", 150 | } 151 | params = { 152 | "q": search_term, 153 | "gl": "us", 154 | "hl": "en", 155 | **{key: value for key, value in kwargs.items() if value is not None}, 156 | } 157 | response, num_fails, sleep_time = None, 0, 0 158 | 159 | while not response and num_fails < max_retries: 160 | try: 161 | response = requests.post( 162 | f"{_SERPER_URL}/{search_type}", headers=headers, params=params 163 | ) 164 | # print(response) 165 | except AssertionError as e: 166 | raise e 167 | except Exception: # pylint: disable=broad-exception-caught 168 | response = None 169 | num_fails += 1 170 | sleep_time = min(sleep_time * 2, 600) 171 | sleep_time = random.uniform( 172 | 1, 10) if not sleep_time else sleep_time 173 | time.sleep(sleep_time) 174 | 175 | if not response: 176 | raise ValueError("Failed to get result from Google Serper API") 177 | 178 | response.raise_for_status() 179 | search_results = response.json() 180 | return search_results 181 | 182 | def _parse_snippets(self, results: dict[Any, Any]) -> list[str]: 183 | """Parse results.""" 184 | snippets = [] 185 | 186 | if results.get("answerBox"): 187 | answer_box = results.get("answerBox", {}) 188 | answer = answer_box.get("answer") 189 | snippet = answer_box.get("snippet") 190 | snippet_highlighted = answer_box.get("snippetHighlighted") 191 | 192 | if answer and isinstance(answer, str): 193 | snippets.append(answer) 194 | if snippet and isinstance(snippet, str): 195 | snippets.append(snippet.replace("\n", " ")) 196 | if snippet_highlighted: 197 | snippets.append(snippet_highlighted) 198 | 199 | if results.get("knowledgeGraph"): 200 | kg = results.get("knowledgeGraph", {}) 201 | title = kg.get("title") 202 | entity_type = kg.get("type") 203 | description = kg.get("description") 204 | 205 | if entity_type: 206 | snippets.append(f"{title}: {entity_type}.") 207 | 208 | if description: 209 | snippets.append(description) 210 | 211 | for attribute, value in kg.get("attributes", {}).items(): 212 | snippets.append(f"{title} {attribute}: {value}.") 213 | 214 | result_key = self.result_key_for_type[self.search_type] 215 | 216 | if result_key in results: 217 | for result in results[result_key][: self.k]: 218 | if "snippet" in result: 219 | snippets.append(result["snippet"]) 220 | 221 | for attribute, value in result.get("attributes", {}).items(): 222 | snippets.append(f"{attribute}: {value}.") 223 | 224 | if not snippets: 225 | return [NO_RESULT_MSG] 226 | 227 | return snippets 228 | 229 | def _parse_results(self, results: dict[Any, Any]) -> str: 230 | return " ".join(self._parse_snippets(results)) 231 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/google_search_check.py: -------------------------------------------------------------------------------- 1 | from .truth_method import TruthMethod 2 | from TruthTorchLM.utils import fix_tokenizer_chat 3 | from TruthTorchLM.utils.google_search_utils import GoogleSerperAPIWrapper 4 | from litellm import completion 5 | from typing import Union 6 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 7 | from TruthTorchLM.templates import ( 8 | GOOGLE_CHECK_QUERY_SYSTEM_PROMPT, 9 | GOOGLE_CHECK_QUERY_USER_PROMPT, 10 | GOOGLE_CHECK_VERIFICATION_SYSTEM_PROMPT, 11 | GOOGLE_CHECK_VERIFICATION_USER_PROMPT, 12 | ) 13 | 14 | from pydantic import BaseModel 15 | import instructor 16 | import outlines 17 | import torch 18 | import copy 19 | 20 | 21 | class Verification(BaseModel): 22 | reasoning: str 23 | error: Union[None, str] 24 | correction: str 25 | factuality: bool 26 | 27 | 28 | class Response(BaseModel): 29 | response: list[str] 30 | 31 | 32 | class GoogleSearchCheck(TruthMethod): 33 | REQUIRES_NORMALIZATION = False 34 | 35 | def __init__( 36 | self, 37 | number_of_snippets: int = 10, 38 | location: str = "us", 39 | language: str = "en", 40 | check_query_system_prompt: str = GOOGLE_CHECK_QUERY_SYSTEM_PROMPT, 41 | check_query_user_prompt: str = GOOGLE_CHECK_QUERY_USER_PROMPT, 42 | check_verification_system_prompt: str = GOOGLE_CHECK_VERIFICATION_SYSTEM_PROMPT, 43 | check_verification_user_prompt: str = GOOGLE_CHECK_VERIFICATION_USER_PROMPT, 44 | max_new_tokens=1024, 45 | temperature=1.0, 46 | top_k=50, 47 | num_beams=1, 48 | **generation_kwargs 49 | ) -> None: 50 | super().__init__() 51 | self.number_of_snippets = number_of_snippets 52 | self.location = location 53 | self.language = language 54 | self.google_serper = GoogleSerperAPIWrapper( 55 | snippet_cnt=self.number_of_snippets, 56 | location=self.location, 57 | language=self.language, 58 | ) 59 | self.check_query_system_prompt = check_query_system_prompt 60 | self.check_query_user_prompt = check_query_user_prompt 61 | self.check_verification_system_prompt = check_verification_system_prompt 62 | self.check_verification_user_prompt = check_verification_user_prompt 63 | self.max_new_tokens = max_new_tokens 64 | self.temperature = temperature 65 | self.top_k = top_k 66 | self.num_beams = num_beams 67 | self.generation_kwargs = generation_kwargs 68 | 69 | outlines.disable_cache() 70 | 71 | def get_evidences(self, query_list: list): 72 | if len(query_list) > 0: 73 | # search the queries 74 | search_results = self.google_serper.run(query_list) 75 | evidences = [ 76 | [output["content"] for output in search_result] 77 | for search_result in search_results 78 | ] 79 | else: 80 | evidences = [] 81 | print("The model did not generate any queries.") 82 | return evidences 83 | 84 | def _google_search_check( 85 | self, verification_dict: str, evidences: list, queries: list[str] 86 | ): 87 | 88 | if type(verification_dict.factuality) != bool: 89 | print("The model did not return a boolean value for factuality.") 90 | return { 91 | "truth_value": 0.5, 92 | "normalized_truth_value": 0.5, 93 | "evidences": evidences, 94 | "queries": queries, 95 | "evidences": evidences, 96 | "verification": verification_dict, 97 | } 98 | else: 99 | try: 100 | if verification_dict.factuality == True: 101 | truth_value = 1.0 102 | 103 | else: 104 | truth_value = 0.0 105 | 106 | except: 107 | truth_value = 0.5 108 | 109 | print("The model did not produce a valid verification.") 110 | 111 | return { 112 | "truth_value": truth_value, 113 | "evidences": evidences, 114 | "queries": queries, 115 | "evidences": evidences, 116 | "verification": verification_dict, 117 | } 118 | 119 | def forward_hf_local( 120 | self, 121 | model: PreTrainedModel, 122 | input_text: str, 123 | generated_text: str, 124 | question: str, 125 | all_ids: Union[list, torch.Tensor], 126 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 127 | generation_seed=None, 128 | sampled_generations_dict: dict = None, 129 | messages: list = [], 130 | context: str = "", 131 | **kwargs 132 | ): 133 | 134 | outlines_model = outlines.models.Transformers(model, tokenizer) 135 | outlines_generator = outlines.generate.json(outlines_model, Response) 136 | 137 | kwargs = copy.deepcopy(kwargs) 138 | # first we need to generate search queries 139 | 140 | chat = [ 141 | {"role": "system", "content": self.check_query_system_prompt}, 142 | { 143 | "role": "user", 144 | "content": self.check_query_user_prompt.format( 145 | question=question, input=generated_text 146 | ), 147 | }, 148 | ] 149 | 150 | tokenizer, chat = fix_tokenizer_chat(tokenizer, chat) 151 | prompt = tokenizer.apply_chat_template(chat, tokenize=False) 152 | query = outlines_generator(prompt, seed=generation_seed).response 153 | 154 | evidences = self.get_evidences(query) 155 | 156 | # Ask model to verify the claim 157 | outlines_generator = outlines.generate.json( 158 | outlines_model, Verification) 159 | 160 | chat = [ 161 | {"role": "system", "content": self.check_verification_system_prompt}, 162 | { 163 | "role": "user", 164 | "content": self.check_verification_user_prompt.format( 165 | question=question, 166 | claim=generated_text, 167 | evidence=evidences, 168 | ), 169 | }, 170 | ] 171 | tokenizer, chat = fix_tokenizer_chat(tokenizer, chat) 172 | 173 | prompt = tokenizer.apply_chat_template(chat, tokenize=False) 174 | verification = outlines_generator(prompt, seed=generation_seed) 175 | 176 | return self._google_search_check(verification, evidences, query) 177 | 178 | def forward_api( 179 | self, 180 | model: str, 181 | messages: list, 182 | generated_text: str, 183 | question: str, 184 | generation_seed=None, 185 | sampled_generations_dict: dict = None, 186 | logprobs: list = None, 187 | generated_tokens: list = None, 188 | context: str = "", 189 | **kwargs 190 | ): 191 | kwargs = copy.deepcopy(kwargs) 192 | 193 | # cerare instructor client object for structured llm output 194 | client = instructor.from_litellm(completion) 195 | 196 | # first we need to generate search queries 197 | chat = [ 198 | {"role": "system", "content": GOOGLE_CHECK_QUERY_SYSTEM_PROMPT}, 199 | { 200 | "role": "user", 201 | "content": GOOGLE_CHECK_QUERY_USER_PROMPT.format( 202 | question=question, input=generated_text 203 | ), 204 | }, 205 | ] 206 | 207 | query = client.chat.completions.create( 208 | model=model, 209 | messages=chat, 210 | response_model=Response, 211 | ).response 212 | 213 | evidences = self.get_evidences(query) 214 | 215 | # Ask model to verify the claim 216 | chat = [ 217 | {"role": "system", "content": GOOGLE_CHECK_VERIFICATION_SYSTEM_PROMPT}, 218 | { 219 | "role": "user", 220 | "content": GOOGLE_CHECK_VERIFICATION_USER_PROMPT.format( 221 | question=question, 222 | claim=generated_text, 223 | evidence=evidences, 224 | ), 225 | }, 226 | ] 227 | verification = client.chat.completions.create( 228 | model=model, 229 | messages=chat, 230 | response_model=Verification, 231 | ) 232 | 233 | return self._google_search_check(verification, evidences, query) 234 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/p_true.py: -------------------------------------------------------------------------------- 1 | from .truth_method import TruthMethod 2 | from TruthTorchLM.utils import find_token_indices, fix_tokenizer_chat 3 | from typing import Union 4 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 5 | from TruthTorchLM.templates import ( 6 | PTRUE_SYSTEM_PROMPT, 7 | PTRUE_USER_PROMPT, 8 | PTRUE_MODEL_OUTPUT, 9 | PTRUE_USER_PROMPT_WITH_CONTEXT, 10 | ) 11 | from ..generation import sample_generations_hf_local, sample_generations_api 12 | 13 | import torch 14 | import numpy as np 15 | 16 | 17 | class PTrue(TruthMethod): 18 | 19 | REQUIRES_SAMPLED_TEXT = True 20 | 21 | def __init__( 22 | self, 23 | number_of_ideas: int = 5, 24 | system_prompt: str = PTRUE_SYSTEM_PROMPT, 25 | user_prompt: str = PTRUE_USER_PROMPT, 26 | model_output: str = PTRUE_MODEL_OUTPUT, 27 | batch_generation=True, 28 | with_context: bool = False, 29 | ): 30 | super().__init__() 31 | self.number_of_ideas = number_of_ideas 32 | self.system_prompt = system_prompt 33 | self.user_prompt = user_prompt 34 | self.model_output = model_output 35 | self.batch_generation = batch_generation 36 | self.with_context = with_context 37 | if with_context and '{context}' not in self.user_prompt:#check if the prompt has a context field 38 | print("Context field is required in user prompt for with_context=True, swithing to the default user prompt with context") 39 | self.user_prompt = PTRUE_USER_PROMPT_WITH_CONTEXT 40 | 41 | def forward_hf_local( 42 | self, 43 | model: PreTrainedModel, 44 | input_text: str, 45 | generated_text: str, 46 | question: str, 47 | all_ids: Union[list, torch.Tensor], 48 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 49 | generation_seed=None, 50 | sampled_generations_dict: dict = None, 51 | messages: list = [], 52 | context: str = "", 53 | **kwargs 54 | ): 55 | 56 | if sampled_generations_dict is None: 57 | sampled_generations_dict = sample_generations_hf_local( 58 | model=model, 59 | input_text=input_text, 60 | tokenizer=tokenizer, 61 | generation_seed=generation_seed, 62 | number_of_generations=self.number_of_ideas, 63 | return_text=True, 64 | batch_generation=self.batch_generation, 65 | **kwargs 66 | ) 67 | 68 | generated_text = tokenizer.decode( 69 | tokenizer.encode( 70 | generated_text, return_tensors="pt").view(-1).tolist(), 71 | skip_special_tokens=True, 72 | ) # remove special tokens 73 | ideas = sampled_generations_dict["generated_texts"][: self.number_of_ideas] 74 | ideas = "\n".join(ideas) 75 | 76 | if self.with_context == False: 77 | chat = [ 78 | {"role": "system", "content": self.system_prompt}, 79 | { 80 | "role": "user", 81 | "content": self.user_prompt.format( 82 | question=question, 83 | ideas=ideas, 84 | generated_text=generated_text, 85 | ), 86 | }, 87 | {"role": "assistant", "content": self.model_output}, 88 | ] 89 | else: 90 | chat = [ 91 | {"role": "system", "content": self.system_prompt}, 92 | { 93 | "role": "user", 94 | "content": self.user_prompt.format( 95 | question=question, 96 | ideas=ideas, 97 | generated_text=generated_text, 98 | context=context, 99 | ), 100 | }, 101 | {"role": "assistant", "content": self.model_output}, 102 | ] 103 | tokenizer, chat = fix_tokenizer_chat( 104 | tokenizer, chat 105 | ) # in case some tokenizers don't have chat template and don't support system prompt 106 | 107 | prompt = tokenizer.apply_chat_template(chat, tokenize=False) 108 | prompt_tokens = tokenizer.encode( 109 | prompt, return_tensors="pt").to(model.device) 110 | with torch.no_grad(): 111 | outputs = model(prompt_tokens) 112 | logits = outputs.logits # Logits for each token in the input 113 | 114 | logprobs = torch.log_softmax(logits, dim=-1) # logprobs for each token 115 | # logprobs for each token except the last one 116 | logprobs = logprobs[0, :-1, :] 117 | logprobs = torch.gather( 118 | logprobs, dim=1, index=prompt_tokens[0][1:].view(-1, 1) 119 | ) # logprobs for each token in the generated text 120 | logprobs = logprobs.view(-1).tolist() # convert to list 121 | 122 | # write a function to find the probability of token 'true' in the logprobs 123 | indices, texts = find_token_indices( 124 | prompt_tokens[0][1:], tokenizer, "true") 125 | 126 | loss_true = 0 127 | for index in indices[-1]: # only look at the last occurence of the word true 128 | loss_true += logprobs[index] 129 | 130 | loss_true = loss_true / len(indices[-1]) # length normalization 131 | prob_true = np.exp(loss_true).item() 132 | 133 | return { 134 | "truth_value": prob_true, 135 | "p_true": prob_true, 136 | "generated_ideas": ideas, 137 | } # this output format should be same for all truth methods 138 | 139 | def forward_api( 140 | self, 141 | model: str, 142 | messages: list, 143 | generated_text: str, 144 | question: str, 145 | generation_seed=None, 146 | sampled_generations_dict: dict = None, 147 | logprobs: list = None, 148 | generated_tokens: list = None, 149 | context: str = "", 150 | **kwargs 151 | ): 152 | # make sampling for the ideas 153 | if sampled_generations_dict is None: 154 | sampled_generations_dict = sample_generations_api( 155 | model=model, 156 | messages=messages, 157 | generation_seed=generation_seed, 158 | number_of_generations=self.number_of_ideas, 159 | return_text=True, 160 | **kwargs 161 | ) 162 | 163 | ideas = sampled_generations_dict["generated_texts"][: self.number_of_ideas] 164 | ideas = "\n".join(ideas) 165 | 166 | if self.with_context == False: 167 | chat = [ 168 | {"role": "system", "content": self.system_prompt}, 169 | { 170 | "role": "user", 171 | "content": self.user_prompt.format( 172 | question=question, 173 | ideas=ideas, 174 | generated_text=generated_text, 175 | ), 176 | }, 177 | ] 178 | else: 179 | chat = [ 180 | {"role": "system", "content": self.system_prompt}, 181 | { 182 | "role": "user", 183 | "content": self.user_prompt.format( 184 | question=question, 185 | ideas=ideas, 186 | generated_text=generated_text, 187 | context=context, 188 | ), 189 | }, 190 | ] 191 | 192 | sampled_generations_dict = sample_generations_api( 193 | model=model, 194 | messages=chat, 195 | generation_seed=generation_seed, 196 | number_of_generations=1, 197 | return_text=True, 198 | return_logprobs=True, 199 | temperature=0.0, 200 | ) 201 | logprobs = sampled_generations_dict["logprobs"][0] 202 | tokens = sampled_generations_dict["tokens"][0] 203 | 204 | for i, token in enumerate(tokens): 205 | if "true" in token.lower(): 206 | prob = np.exp(logprobs[i]).item() 207 | return {"truth_value": prob, "p_true": prob, "generated_ideas": ideas} 208 | if "false" in token.lower(): 209 | prob = 1 - np.exp(logprobs[i]).item() 210 | return {"truth_value": prob, "p_true": prob, "generated_ideas": ideas} 211 | 212 | return {"truth_value": 0.5, "p_true": 0.5, "generated_ideas": ideas} 213 | -------------------------------------------------------------------------------- /src/TruthTorchLM/truth_methods/SAR.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | from typing import Union 4 | 5 | from .truth_method import TruthMethod 6 | from ..generation import sample_generations_hf_local, sample_generations_api 7 | from TruthTorchLM.error_handler import handle_logprobs_error 8 | 9 | from sentence_transformers.cross_encoder import CrossEncoder 10 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 11 | 12 | 13 | class SAR(TruthMethod): 14 | 15 | REQUIRES_SAMPLED_TEXT = True 16 | REQUIRES_SAMPLED_LOGPROBS = True 17 | 18 | def __init__( 19 | self, 20 | number_of_generations=5, 21 | t=0.001, 22 | model_for_similarity=None, 23 | similarity_model_device="cuda", 24 | batch_generation=True, 25 | ): # normalization 26 | super().__init__() 27 | if model_for_similarity is None: 28 | self.model_for_similarity = CrossEncoder( 29 | "cross-encoder/stsb-roberta-large", 30 | num_labels=1, 31 | device=similarity_model_device, 32 | ) 33 | else: 34 | self.model_for_similarity = model_for_similarity 35 | 36 | self.number_of_generations = number_of_generations 37 | self.t = t 38 | self.batch_generation = batch_generation 39 | 40 | def _sentsar( 41 | self, 42 | generated_texts: list[str], 43 | question: str, 44 | scores: list[float], 45 | sampled_generations_dict: dict, 46 | ): 47 | 48 | similarities = {} 49 | for i in range(len(generated_texts)): 50 | similarities[i] = [] 51 | 52 | for i in range(len(generated_texts)): 53 | for j in range(i + 1, len(generated_texts)): 54 | gen_i = question + generated_texts[i] 55 | gen_j = question + generated_texts[j] 56 | similarity_i_j = self.model_for_similarity.predict( 57 | [gen_i, gen_j]) 58 | similarities[i].append(similarity_i_j) 59 | similarities[j].append(similarity_i_j) 60 | 61 | probs = torch.exp(torch.tensor(scores)) 62 | assert len(probs) == len(similarities) 63 | 64 | sentence_scores = [] 65 | for idx, prob in enumerate(probs): 66 | w_ent = -torch.log( 67 | prob 68 | + ( 69 | (torch.tensor(similarities[idx]) / self.t) 70 | * torch.cat([probs[:idx], probs[idx + 1:]]) 71 | ).sum() 72 | ) 73 | sentence_scores.append(w_ent) 74 | sentence_scores = torch.tensor(sentence_scores) 75 | 76 | entropy = ( 77 | torch.sum(sentence_scores, dim=0) / 78 | torch.tensor(sentence_scores.shape[0]) 79 | ).item() 80 | return { 81 | "truth_value": -entropy, 82 | "SAR": entropy, 83 | "score_for_each_generation": scores, 84 | "generated_texts": generated_texts, 85 | "similarities": similarities, 86 | } 87 | 88 | def _tokensar_local( 89 | self, 90 | question: str, 91 | generated_text: str, 92 | tokens: list[int], 93 | logprobs: list[float], 94 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], 95 | ): 96 | importance_vector = [] 97 | for i in range(len(tokens)): 98 | removed_answer_ids = tokens[:i] + tokens[i + 1:] 99 | removed_answer = tokenizer.decode( 100 | removed_answer_ids, skip_special_tokens=True 101 | ) 102 | score = self.model_for_similarity.predict( 103 | [ 104 | ( 105 | question + " " + removed_answer, 106 | question + " " + generated_text, 107 | ) 108 | ] 109 | ) 110 | score = 1 - score[0] 111 | importance_vector.append(score) 112 | 113 | importance_vector = importance_vector / np.sum(importance_vector) 114 | return np.dot(importance_vector, logprobs) 115 | 116 | def forward_hf_local( 117 | self, 118 | model: PreTrainedModel, 119 | input_text: str, 120 | generated_text: str, 121 | question: str, 122 | all_ids: Union[list, torch.Tensor], 123 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 124 | generation_seed=None, 125 | sampled_generations_dict: dict = None, 126 | messages: list = [], 127 | context: str = "", 128 | **kwargs 129 | ): 130 | 131 | if sampled_generations_dict is None: 132 | sampled_generations_dict = sample_generations_hf_local( 133 | model=model, 134 | input_text=input_text, 135 | tokenizer=tokenizer, 136 | generation_seed=generation_seed, 137 | number_of_generations=self.number_of_generations, 138 | return_text=True, 139 | return_logprobs=True, 140 | batch_generation=self.batch_generation, 141 | **kwargs 142 | ) 143 | 144 | generated_texts = sampled_generations_dict["generated_texts"][ 145 | : self.number_of_generations 146 | ] 147 | generated_tokens = sampled_generations_dict["tokens"][ 148 | : self.number_of_generations 149 | ] 150 | logprobs = sampled_generations_dict["logprobs"][: self.number_of_generations] 151 | 152 | scores = [] 153 | for i in range(self.number_of_generations): 154 | score = self._tokensar_local( 155 | question, 156 | generated_texts[i], 157 | generated_tokens[i], 158 | logprobs[i], 159 | tokenizer, 160 | ) 161 | scores.append(score) # scores are in log scale 162 | 163 | return self._sentsar( 164 | generated_texts, question, scores, sampled_generations_dict 165 | ) 166 | 167 | def _tokensar_api( 168 | self, 169 | question: str, 170 | generated_text: str, 171 | tokens: list[str], 172 | logprobs: list[float], 173 | ): 174 | importance_vector = [] 175 | for i in range(len(tokens)): 176 | removed_answer = "".join(tokens[:i]) + "".join(tokens[i + 1:]) 177 | score = self.model_for_similarity.predict( 178 | [ 179 | ( 180 | question + " " + removed_answer, 181 | question + " " + generated_text, 182 | ) 183 | ] 184 | ) 185 | score = 1 - score[0] 186 | importance_vector.append(score) 187 | 188 | importance_vector = importance_vector / np.sum(importance_vector) 189 | return np.dot(importance_vector, logprobs) 190 | 191 | @handle_logprobs_error 192 | def forward_api( 193 | self, 194 | model: str, 195 | messages: list, 196 | generated_text: str, 197 | question: str, 198 | generation_seed=None, 199 | sampled_generations_dict: dict = None, 200 | logprobs: list = None, 201 | generated_tokens: list = None, 202 | context: str = "", 203 | **kwargs 204 | ): 205 | 206 | if sampled_generations_dict is None: 207 | sampled_generations_dict = sample_generations_api( 208 | model=model, 209 | messages=messages, 210 | generation_seed=generation_seed, 211 | number_of_generations=self.number_of_generations, 212 | return_text=True, 213 | return_logprobs=True, 214 | **kwargs 215 | ) 216 | 217 | generated_texts = sampled_generations_dict["generated_texts"][ 218 | : self.number_of_generations 219 | ] 220 | generated_tokens = sampled_generations_dict["tokens"][ 221 | : self.number_of_generations 222 | ] 223 | logprobs = sampled_generations_dict["logprobs"][: self.number_of_generations] 224 | 225 | scores = [] 226 | for i in range(self.number_of_generations): 227 | score = self._tokensar_api( 228 | question, generated_texts[i], generated_tokens[i], logprobs[i] 229 | ) 230 | scores.append(score) # scores are in log scale 231 | 232 | return self._sentsar( 233 | generated_texts, question, scores, sampled_generations_dict 234 | ) 235 | -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/templates.py: -------------------------------------------------------------------------------- 1 | # For claim check methods 2 | 3 | QUESTION_GENERATION_INSTRUCTION = [ 4 | { 5 | "role": "system", 6 | "content": """You are an expert assistant skilled at generating focused and contextually relevant questions from claims. \ 7 | Your task is to create a question such that the answer would align closely with the provided claim. To ensure \ 8 | the question is precise and relevant, consider the context provided by the original question. Study the examples \ 9 | below from a variety of topics and follow the same pattern. 10 | 11 | Original Question: What themes are commonly explored in 20th-century dystopian literature? 12 | Claim: George Orwell's novel 1984 explores the theme of government surveillance. 13 | Question: What theme does George Orwell's novel 1984 explore? 14 | 15 | Original Question: What themes are commonly explored in 20th-century dystopian literature? 16 | Claim: George Orwell's novel 1984 portrays a totalitarian regime that monitors every aspect of citizens' lives. 17 | Question: How does George Orwell's novel 1984 reflect the theme of totalitarian control, as commonly explored in 20th-century dystopian literature? 18 | 19 | Original Question: What themes are commonly explored in 20th-century dystopian literature? 20 | Claim: The novel 1984 is written by George Orwell. 21 | Question: Who has written the novel 1984? 22 | 23 | Original Question: How has artificial intelligence influenced industries in the 21st century? 24 | Claim: Artificial intelligence enables better decision-making through data analysis. 25 | Question: How does artificial intelligence enhance the decision-making process in modern businesses? 26 | 27 | Original Question: What factors contributed to the Great Depression, and how did governments respond? 28 | Claim: Stock market speculation contributed to the Great Depression. 29 | Question: Did stock market speculation contribute to the Great Depression? 30 | 31 | Original Question: Who is Abraham Lincoln? 32 | Claim: Abraham Lincoln is best known for leading the country through the Civil War. 33 | Question: What is Abraham Lincoln's most significant historical contribution? 34 | 35 | Original Question: Who is Abraham Lincoln? 36 | Claim: Abraham Lincoln served from 1861 to 1865 as the president of the US. 37 | Question: When did Abraham Lincoln serve as the president of the United States? 38 | 39 | Now, follow the pattern demonstrated in the examples to generate a question for the given claim,\ 40 | without adding explanations, introductions, or conversational responses.""", 41 | }, 42 | { 43 | "role": "user", 44 | "content": """Original question: {question} 45 | Claim: {claim} 46 | Question: """, 47 | }, 48 | ] 49 | 50 | ANSWER_GENERATION_INSTRUCTION = [ 51 | { 52 | "role": "system", 53 | "content": """You are a helpful assistant. Give a single claim answer to given question. \ 54 | Don\‘t provide any additional information. Just answer the question with a brief \ 55 | sentence in a single claim.""", 56 | }, 57 | {"role": "user", "content": "{question}"}, 58 | ] 59 | 60 | # ANSWER_GENERATION_INSTRUCTION = [{"role": "system", "content": 'You are a helpful assistant. Give short and precise answers.'}, 61 | # {"role": "user", "content": "{question}"},] 62 | 63 | # For decomposition methods 64 | 65 | DECOMPOSITION_INSTRUCTION = [ 66 | { 67 | "role": "system", 68 | "content": """You are a helpful assistant. List the specific factual claims included in the given input as a python list. \ 69 | Be complete and do not leave any factual claims out. Provide each factual claim as a separate sentence in a list, \ 70 | without adding explanations, introductions, or conversational responses. Each sentence must be standalone, \ 71 | containing all necessary details to be understood independently of the original text and other sentences. \ 72 | This includes using full identifiers for any people, places, or objects mentioned, instead of pronouns or partial names. \ 73 | If there is a single factual claim in the input, just provide one sentence. 74 | 75 | Examples: 76 | 77 | Paragraph: Mount Everest is the tallest mountain in the world, standing at 8,848 meters above sea level. \ 78 | It is located in the Himalayas on the border between Nepal and the Tibet Autonomous Region of China. \ 79 | The first successful ascent of Mount Everest was achieved in 1953 by Sir Edmund Hillary and Tenzing Norgay. \ 80 | I hope you found these facts interesting! Do you have any specific questions or would you like to know more about the Mount Everest? 81 | Claims: 82 | ['Mount Everest is the tallest mountain in the world.', 83 | 'Mount Everest stands at 8,848 meters above sea level.', 84 | 'Mount Everest is located in the Himalayas.', 85 | 'Mount Everest is on the border between Nepal and the Tibet Autonomous Region of China.', 86 | 'The first successful ascent of Mount Everest was achieved in 1953.', 87 | 'Sir Edmund Hillary and Tenzing Norgay achieved the first successful ascent of Mount Everest.'] 88 | 89 | Paragraph: Medical ethics are also evolving to address issues related to genetic testing, privacy concerns, \ 90 | and the ethical implications of personalized medicine, highlighting the importance of maintaining patient autonomy, \ 91 | informed consent, and confidentiality in the era of advanced health technologies. 92 | Claims: 93 | ['Medical ethics are evolving to address issues related to genetic testing.', 94 | 'Medical ethics are evolving to address privacy concerns.', 95 | 'Medical ethics are evolving to address the ethical implications of personalized medicine.', 96 | 'Maintaining patient autonomy is important in the era of advanced health technologies.', 97 | 'Informed consent is important in the era of advanced health technologies.', 98 | 'Confidentiality is important in the era of advanced health technologies.'] 99 | 100 | For the new sample, simply list the factual claim in seperate sentences as a python list, \ 101 | without adding explanations, introductions, or conversational responses.""", 102 | }, 103 | { 104 | "role": "user", 105 | "content": """Paragraph: {TEXT} 106 | Claims:""", 107 | }, 108 | ] 109 | 110 | DECOMPOSITION_INSTRUCTION_GRANULAR = [ 111 | { 112 | "role": "system", 113 | "content": """You are a helpful assistant. List the specific factual claims included in the given input as a python list. \ 114 | Be complete and do not leave any factual claims out. Provide each factual claim as a separate sentence in a list, \ 115 | without adding explanations, introductions, or conversational responses. Each sentence must be standalone, \ 116 | containing all necessary details to be understood independently of the original text. \ 117 | This includes using full identifiers for any people, places, or objects mentioned, instead of pronouns or partial names. \ 118 | If there is a single factual claim in the input, just provide the sentence itself. If there is no factual claim in the input, \ 119 | provide an empty list. 120 | 121 | Examples: 122 | 123 | Input: Mount Everest is the tallest mountain in the world, standing at 8,848 meters above sea level. 124 | Claims: 125 | ['Mount Everest is the tallest mountain in the world.', 126 | 'Mount Everest stands at 8,848 meters above sea level.'] 127 | 128 | Input: Medical ethics are also evolving to address issues related to genetic testing, privacy concerns, \ 129 | and the ethical implications of personalized medicine. 130 | Claims: 131 | ['Medical ethics are evolving to address issues related to genetic testing.', 132 | 'Medical ethics are evolving to address privacy concerns.', 133 | 'Medical ethics are evolving to address the ethical implications of personalized medicine.'] 134 | 135 | Input: Abraham Lincoln was the 16th president of the United States. 136 | Claims: 137 | ['Abraham Lincoln was the 16th president of the United States.'] 138 | 139 | Input: I hope you found these facts interesting! Is there anything else you would like to know? 140 | Claims: 141 | [] 142 | 143 | For the new input, simply list the factual claim in seperate sentences as a python list, \ 144 | without adding explanations, introductions, or conversational responses.""", 145 | }, 146 | { 147 | "role": "user", 148 | "content": """Input: {TEXT} 149 | Claims:""", 150 | }, 151 | ] 152 | 153 | UNSTRUCTURED_DECOMPOSITION_INSTRUCTION = [ 154 | { 155 | "role": "system", 156 | "content": """You are a helpful assistant. List the specific factual claims included in the given input. \ 157 | Be complete and do not leave any factual claims out. Provide each factual claim as a separate sentence \ 158 | in a separate bullet point, without adding explanations, introductions, or conversational responses. \ 159 | Each sentence must be standalone, containing all necessary details to be understood independently of the \ 160 | original text and other sentences. This includes using full identifiers for any people, places, or objects \ 161 | mentioned, instead of pronouns or partial names. If there is a single factual claim in the input, \ 162 | just provide one sentence.""", 163 | }, 164 | {"role": "user", "content": """{TEXT}"""}, 165 | ] 166 | -------------------------------------------------------------------------------- /src/TruthTorchLM/utils/dataset_utils.py: -------------------------------------------------------------------------------- 1 | from datasets import load_dataset 2 | from TruthTorchLM.availability import AVAILABLE_DATASETS 3 | from typing import Union 4 | from tqdm import tqdm 5 | 6 | 7 | def get_dataset( 8 | dataset: Union[str, list], size_of_data: float = 1.0, seed: int = 0, split="test" 9 | ): 10 | if type(dataset) != str: 11 | if len(dataset) == 0: 12 | raise ValueError("Dataset list is empty.") 13 | if ( 14 | "question" not in dataset[0].keys() 15 | or "ground_truths" not in dataset[0].keys() 16 | ): 17 | raise ValueError( 18 | "Dataset should have 'question' and 'ground_truths' keys.") 19 | return dataset 20 | 21 | if dataset not in AVAILABLE_DATASETS: 22 | raise ValueError( 23 | f"Dataset is not available. Available datasets are: {AVAILABLE_DATASETS}" 24 | ) 25 | 26 | print( 27 | "Loading dataset from Huggingface Datasets, split:", 28 | split, 29 | "fraction of data:", 30 | size_of_data, 31 | ) 32 | 33 | if dataset == "trivia_qa": 34 | dataset = get_trivia_qa( 35 | size_of_data=size_of_data, seed=seed, split=split) 36 | elif dataset == "gsm8k": 37 | dataset = get_gsm8k(size_of_data=size_of_data, seed=seed, split=split) 38 | elif dataset == "natural_qa": 39 | dataset = get_natural_qa( 40 | size_of_data=size_of_data, seed=seed, split=split) 41 | elif dataset == "pop_qa": 42 | dataset = get_pop_qa(size_of_data=size_of_data, seed=seed, split=split) 43 | elif dataset == "simple_qa": 44 | dataset = get_simple_qa( 45 | size_of_data=size_of_data, seed=seed, split=split) 46 | elif dataset == "wikipedia_factual": 47 | dataset = get_wikipedia_factual( 48 | size_of_data=size_of_data, seed=seed, split=split) 49 | elif dataset == "narrative_qa": 50 | dataset = get_narrative_qa( 51 | size_of_data=size_of_data, seed=seed, split=split) 52 | elif dataset == "web_questions": 53 | dataset = get_web_questions( 54 | size_of_data=size_of_data, seed=seed, split=split) 55 | 56 | return dataset 57 | 58 | 59 | def get_trivia_qa(size_of_data: float = 1.0, seed: int = 0, split="test"): 60 | 61 | if split == "test": 62 | raw_dataset = load_dataset( 63 | "trivia_qa", "rc.nocontext", split="validation") 64 | elif split == "train": 65 | raw_dataset = load_dataset("trivia_qa", "rc.nocontext", split="train") 66 | else: 67 | raise ValueError("Split should be either 'test' or 'train'.") 68 | 69 | if size_of_data != 1.0 or type(size_of_data) != float: 70 | raw_dataset = raw_dataset.train_test_split(train_size=size_of_data, seed=seed)[ 71 | "train" 72 | ] 73 | dataset = [] 74 | answers = raw_dataset["answer"] 75 | questions = raw_dataset["question"] 76 | for i in tqdm(range(len(raw_dataset))): 77 | ground_truths = answers[i]["aliases"] 78 | dataset.append( 79 | {"context": "", "question": questions[i], "ground_truths": ground_truths}) 80 | 81 | return dataset 82 | 83 | 84 | def get_gsm8k(size_of_data: float = 1.0, seed: int = 0, split="test"): 85 | if split == "test": 86 | raw_dataset = load_dataset("openai/gsm8k", "main", split="test") 87 | elif split == "train": 88 | raw_dataset = load_dataset("openai/gsm8k", "main", split="train") 89 | else: 90 | raise ValueError("Split should be either 'test' or 'train'.") 91 | if size_of_data != 1.0 or type(size_of_data) != float: 92 | raw_dataset = raw_dataset.train_test_split(train_size=size_of_data, seed=seed)[ 93 | "train" 94 | ] 95 | dataset = [] 96 | answers = raw_dataset["answer"] 97 | questions = raw_dataset["question"] 98 | for i in tqdm(range(len(raw_dataset))): 99 | answer = answers[i].split("####")[1].strip() 100 | dataset.append({"context": "", "question": questions[i], "ground_truths": [answer]}) 101 | 102 | return dataset 103 | 104 | 105 | def get_natural_qa(size_of_data: float = 1.0, seed: int = 0, split="test"): 106 | if split == "test": 107 | raw_dataset = load_dataset( 108 | "google-research-datasets/nq_open", split="validation" 109 | ) 110 | elif split == "train": 111 | raw_dataset = load_dataset( 112 | "google-research-datasets/nq_open", split="train") 113 | else: 114 | raise ValueError("Split should be either 'test' or 'train'.") 115 | if size_of_data != 1.0 or type(size_of_data) != float: 116 | raw_dataset = raw_dataset.train_test_split(train_size=size_of_data, seed=seed)[ 117 | "train" 118 | ] 119 | dataset = [] 120 | questions = raw_dataset["question"] 121 | answers = raw_dataset["answer"] 122 | for i in tqdm(range(len(raw_dataset))): 123 | dataset.append({"context": "", "question": questions[i], "ground_truths": answers[i]}) 124 | 125 | return dataset 126 | 127 | 128 | def get_pop_qa(size_of_data: float = 1.0, seed: int = 0, split="test"): 129 | if split == "test": 130 | raw_dataset = load_dataset("akariasai/PopQA", split="test") 131 | elif split == "train": 132 | raw_dataset = load_dataset("akariasai/PopQA", split="test") 133 | print("Train split is not available for PopQA. Using test split instead.") 134 | else: 135 | raise ValueError("Split should be either 'test' or 'train'.") 136 | if size_of_data != 1.0 or type(size_of_data) != float: 137 | raw_dataset = raw_dataset.train_test_split(train_size=size_of_data, seed=seed)[ 138 | "train" 139 | ] 140 | dataset = [] 141 | questions = raw_dataset["question"] 142 | answers = raw_dataset["possible_answers"] 143 | for i in tqdm(range(len(raw_dataset))): 144 | dataset.append( 145 | {"context": "", "question": questions[i], "ground_truths": [answers[i]]}) 146 | 147 | return dataset 148 | 149 | 150 | def get_simple_qa(size_of_data: float = 1.0, seed: int = 0, split="test"): 151 | if split == "test": 152 | raw_dataset = load_dataset("basicv8vc/SimpleQA", split="test") 153 | elif split == "train": 154 | raw_dataset = load_dataset("basicv8vc/SimpleQA", split="test") 155 | print("Train split is not available for PopQA. Using test split instead.") 156 | else: 157 | raise ValueError("Split should be either 'test' or 'train'.") 158 | if size_of_data != 1.0 or type(size_of_data) != float: 159 | raw_dataset = raw_dataset.train_test_split(train_size=size_of_data, seed=seed)[ 160 | "train" 161 | ] 162 | dataset = [] 163 | questions = raw_dataset["problem"] 164 | answers = raw_dataset["answer"] 165 | for i in tqdm(range(len(raw_dataset))): 166 | dataset.append( 167 | {"context": "", "question": questions[i], "ground_truths": [answers[i]]}) 168 | 169 | return dataset 170 | 171 | 172 | def get_wikipedia_factual(size_of_data: float = 1.0, seed: int = 0, split='train'): 173 | raw_dataset = load_dataset("achorn123/wikipedia_factual_dataset_500", split='train') 174 | 175 | if size_of_data != 1.0 or type(size_of_data) != float: 176 | raw_dataset = raw_dataset.train_test_split(train_size=size_of_data, seed=seed)['train'] 177 | 178 | dataset = [] 179 | for data in tqdm(raw_dataset, desc="Processing Wikipedia factual 500"): 180 | context = data["context"].strip() 181 | question = data["question"].strip() 182 | answer = data["answer"].strip() 183 | dataset.append({ 184 | 'context': context, 185 | 'question': question, 186 | 'ground_truths': [answer] 187 | }) 188 | 189 | return dataset 190 | 191 | 192 | def get_narrative_qa(size_of_data: float = 1.0, seed: int = 0, split='test'): 193 | raw_dataset = load_dataset("deepmind/narrativeqa", split=split) 194 | 195 | if size_of_data != 1.0 or type(size_of_data) != float: 196 | raw_dataset = raw_dataset.train_test_split(train_size=size_of_data, seed=seed)['train'] 197 | 198 | dataset = [] 199 | for data in tqdm(raw_dataset, desc="Processing NarrativeQA"): 200 | context = data["document"]["text"].strip() 201 | question = data["question"]["text"].strip() 202 | answers = [] 203 | for answer in data["answers"]: 204 | answers.append(answer["text"].strip()) 205 | 206 | dataset.append({ 207 | "context": context, 208 | "question": question, 209 | "ground_truths": answers 210 | }) 211 | 212 | return dataset 213 | 214 | def get_web_questions(size_of_data: float = 1.0, seed: int = 0, split="test"): 215 | if split == "test": 216 | raw_dataset = load_dataset("stanfordnlp/web_questions", split="test") 217 | elif split == "train": 218 | raw_dataset = load_dataset("stanfordnlp/web_questions", split="train") 219 | else: 220 | raise ValueError("Split should be either 'test' or 'train'.") 221 | if size_of_data != 1.0 or type(size_of_data) != float: 222 | raw_dataset = raw_dataset.train_test_split(train_size=size_of_data, seed=seed)[ 223 | "train" 224 | ] 225 | dataset = [] 226 | questions = raw_dataset["question"] 227 | answers = raw_dataset["answers"] 228 | for i in tqdm(range(len(raw_dataset))): 229 | dataset.append( 230 | {"context": "", "question": questions[i], "ground_truths": answers[i]}) 231 | 232 | return dataset -------------------------------------------------------------------------------- /src/TruthTorchLM/long_form_generation/generation.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import random 3 | from typing import Union 4 | from litellm import completion 5 | from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast 6 | 7 | from TruthTorchLM.long_form_generation.decomposition_methods.decomposition_method import ( 8 | DecompositionMethod, 9 | ) 10 | from TruthTorchLM.long_form_generation.claim_check_methods.claim_check_method import ( 11 | ClaimCheckMethod, 12 | ) 13 | from TruthTorchLM.utils.common_utils import generate, fix_tokenizer_chat 14 | from TruthTorchLM.error_handler import handle_logprobs_error 15 | 16 | 17 | def long_form_generation_with_truth_value( 18 | model: PreTrainedModel, 19 | messages: list, 20 | question: str = None, 21 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 22 | decomp_method: DecompositionMethod = None, 23 | claim_check_methods: list[ClaimCheckMethod] = None, 24 | generation_seed=None, 25 | add_generation_prompt=True, 26 | continue_final_message=False, 27 | context:str="", 28 | **kwargs 29 | ) -> dict: 30 | if type(model) == str: 31 | return long_form_generation_with_truth_value_api( 32 | model=model, 33 | messages=messages, 34 | question=question, 35 | decomp_method=decomp_method, 36 | claim_check_methods=claim_check_methods, 37 | generation_seed=generation_seed, 38 | context=context, 39 | **kwargs 40 | ) 41 | else: 42 | return long_form_generation_with_truth_value_hf_local( 43 | model=model, 44 | messages=messages, 45 | question=question, 46 | decomp_method=decomp_method, 47 | claim_check_methods=claim_check_methods, 48 | tokenizer=tokenizer, 49 | generation_seed=generation_seed, 50 | add_generation_prompt=add_generation_prompt, 51 | continue_final_message=continue_final_message, 52 | context=context, 53 | **kwargs 54 | ) 55 | 56 | 57 | # add cleaning function for the generated text 58 | def long_form_generation_with_truth_value_hf_local( 59 | model: PreTrainedModel, 60 | messages: list, 61 | question: str = None, 62 | tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast] = None, 63 | decomp_method: DecompositionMethod = None, 64 | claim_check_methods: list[ClaimCheckMethod] = None, 65 | generation_seed=None, 66 | add_generation_prompt=True, 67 | continue_final_message=False, 68 | context:str="", 69 | **kwargs 70 | ) -> dict: 71 | 72 | if question == None: 73 | question = "" 74 | # search over last user message if exists 75 | for message in messages[::-1]: 76 | if message["role"] == "user": 77 | question = message["content"] 78 | break 79 | 80 | eos_token_id = kwargs.pop("eos_token_id", None) 81 | if eos_token_id is None: 82 | eos_token_id = model.config.eos_token_id 83 | kwargs["eos_token_id"] = eos_token_id 84 | 85 | pad_token_id = kwargs.pop("pad_token_id", None) 86 | if pad_token_id is None: 87 | if type(eos_token_id) == list: 88 | pad_token_id = eos_token_id[0] 89 | else: 90 | pad_token_id = eos_token_id 91 | kwargs["pad_token_id"] = pad_token_id 92 | 93 | # adjust seeds 94 | if generation_seed is not None: 95 | torch.manual_seed(generation_seed) 96 | random.seed(generation_seed) 97 | 98 | # Generate the main output 99 | tokenizer, messages = fix_tokenizer_chat(tokenizer, messages) 100 | text = tokenizer.apply_chat_template( 101 | messages, 102 | tokenize=False, 103 | add_generation_prompt=add_generation_prompt, 104 | continue_final_message=continue_final_message, 105 | ) 106 | generated_output = generate(text, model, tokenizer, **kwargs) 107 | generated_text = generated_output["generated_text_skip_specials"] 108 | model_output = generated_output["all_ids"] 109 | del generated_output 110 | 111 | # Factual Decomposition 112 | print("Decomposing the generated text...") 113 | claims = decomp_method(generated_text) 114 | 115 | # Get truth score for each claim. 116 | normalized_truth_values = [] 117 | unnormalized_truth_values = [] 118 | method_spec_outputs = [] 119 | for claim_check_method in claim_check_methods: 120 | print("Applying claim check method ", 121 | claim_check_method.__class__.__name__) 122 | stmt_normalized_truth_values = [] 123 | stmt_unnormalized_truth_values = [] 124 | stmt_method_spec_outputs = [] 125 | for sidx, claim in enumerate(claims): 126 | text_so_far = " ".join(claims[:sidx]) if sidx > 0 else None 127 | truth_values = claim_check_method( 128 | model=model, 129 | input_text=text, 130 | generated_text=generated_text, 131 | question=question, 132 | claim=claim, 133 | text_so_far=text_so_far, 134 | all_ids=model_output, 135 | tokenizer=tokenizer, 136 | generation_seed=generation_seed, 137 | messages=messages, 138 | context=context, 139 | **kwargs 140 | ) 141 | 142 | stmt_normalized_truth_values.append( 143 | truth_values["normalized_truth_values"]) 144 | stmt_unnormalized_truth_values.append(truth_values["truth_values"]) 145 | stmt_method_spec_outputs.append(truth_values) 146 | normalized_truth_values.append(stmt_normalized_truth_values) 147 | unnormalized_truth_values.append(stmt_unnormalized_truth_values) 148 | method_spec_outputs.append( 149 | { 150 | "claim_check_method_name": claim_check_method.__class__.__name__, 151 | "details": stmt_method_spec_outputs, 152 | } 153 | ) 154 | 155 | # Create TruthObject 156 | truth_dict = { 157 | "generated_text": generated_text, 158 | "claims": claims, 159 | "normalized_truth_values": normalized_truth_values, 160 | "unnormalized_truth_values": unnormalized_truth_values, 161 | "claim_check_method_details": method_spec_outputs, 162 | } 163 | 164 | # Return TruthObject 165 | return truth_dict 166 | 167 | 168 | # for api-based models, we should write a wrapper function to handle exceptions during the api call 169 | @handle_logprobs_error 170 | def long_form_generation_with_truth_value_api( 171 | model: str, 172 | messages: list, 173 | question: str = None, 174 | decomp_method: DecompositionMethod = None, 175 | claim_check_methods: list[ClaimCheckMethod] = None, 176 | generation_seed=None, 177 | context:str="", 178 | **kwargs 179 | ) -> dict: 180 | 181 | if question == None: 182 | question = "" 183 | # search over last user message if exists 184 | for message in messages[::-1]: 185 | if message["role"] == "user": 186 | question = message["content"] 187 | break 188 | 189 | # adjust seeds 190 | if generation_seed is not None: 191 | random.seed(generation_seed) 192 | 193 | seed = kwargs.pop("seed", None) 194 | if seed == None: 195 | seed = random.randint(0, 1000000) 196 | kwargs["seed"] = seed # a random seed is generated if seed is not specified 197 | 198 | # Generate the main output 199 | response = completion(model=model, messages=messages, **kwargs) 200 | generated_text = response.choices[0].message["content"] 201 | 202 | # Factual Decomposition 203 | print("Decomposing the generated text...") 204 | claims = decomp_method(generated_text) 205 | 206 | # Get truth score for each claim. 207 | normalized_truth_values = [] 208 | unnormalized_truth_values = [] 209 | method_spec_outputs = [] 210 | for claim_check_method in claim_check_methods: 211 | print("Applying claim check method ", 212 | claim_check_method.__class__.__name__) 213 | stmt_normalized_truth_values = [] 214 | stmt_unnormalized_truth_values = [] 215 | stmt_method_spec_outputs = [] 216 | for sidx, claim in enumerate(claims): 217 | text_so_far = " ".join(claims[:sidx]) if sidx > 0 else None 218 | truth_values = claim_check_method( 219 | model=model, 220 | messages=messages, 221 | generated_text=generated_text, 222 | question=question, 223 | claim=claim, 224 | text_so_far=text_so_far, 225 | generation_seed=generation_seed, 226 | context=context, 227 | **kwargs 228 | ) 229 | stmt_normalized_truth_values.append( 230 | truth_values["normalized_truth_values"]) 231 | stmt_unnormalized_truth_values.append(truth_values["truth_values"]) 232 | stmt_method_spec_outputs.append(truth_values) 233 | normalized_truth_values.append(stmt_normalized_truth_values) 234 | unnormalized_truth_values.append(stmt_unnormalized_truth_values) 235 | method_spec_outputs.append( 236 | { 237 | "claim_check_method_name": claim_check_method.__class__.__name__, 238 | "details": stmt_method_spec_outputs, 239 | } 240 | ) 241 | 242 | # Create TruthObject 243 | truth_dict = { 244 | "generated_text": generated_text, 245 | "claims": claims, 246 | "normalized_truth_values": normalized_truth_values, 247 | "unnormalized_truth_values": unnormalized_truth_values, 248 | "claim_check_method_details": method_spec_outputs, 249 | } 250 | 251 | # Return TruthObject 252 | return truth_dict 253 | --------------------------------------------------------------------------------