cotlab 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cotlab/__init__.py +3 -0
- cotlab/analyse_experiments.py +392 -0
- cotlab/analysis/__init__.py +11 -0
- cotlab/analysis/cot_parser.py +243 -0
- cotlab/analysis/faithfulness_metrics.py +192 -0
- cotlab/backends/__init__.py +16 -0
- cotlab/backends/base.py +78 -0
- cotlab/backends/transformers_backend.py +335 -0
- cotlab/backends/vllm_backend.py +227 -0
- cotlab/cli.py +83 -0
- cotlab/core/__init__.py +34 -0
- cotlab/core/base.py +749 -0
- cotlab/core/config.py +90 -0
- cotlab/core/registry.py +68 -0
- cotlab/datasets/__init__.py +45 -0
- cotlab/datasets/loaders.py +1889 -0
- cotlab/experiment/__init__.py +315 -0
- cotlab/experiments/__init__.py +43 -0
- cotlab/experiments/activation_compare.py +290 -0
- cotlab/experiments/activation_patching.py +1050 -0
- cotlab/experiments/attention_analysis.py +885 -0
- cotlab/experiments/classification.py +235 -0
- cotlab/experiments/composite_shift_detector.py +524 -0
- cotlab/experiments/cot_ablation.py +277 -0
- cotlab/experiments/cot_faithfulness.py +187 -0
- cotlab/experiments/cot_heads.py +208 -0
- cotlab/experiments/full_layer_cot.py +232 -0
- cotlab/experiments/full_layer_patching.py +225 -0
- cotlab/experiments/h_neuron_analysis.py +712 -0
- cotlab/experiments/logit_lens.py +439 -0
- cotlab/experiments/multi_head_cot.py +220 -0
- cotlab/experiments/multi_head_patching.py +229 -0
- cotlab/experiments/probing_classifier.py +402 -0
- cotlab/experiments/residual_norm_ood.py +413 -0
- cotlab/experiments/sae_feature_analysis.py +673 -0
- cotlab/experiments/steering_vectors.py +223 -0
- cotlab/experiments/sycophancy_heads.py +224 -0
- cotlab/logging/__init__.py +5 -0
- cotlab/logging/json_logger.py +161 -0
- cotlab/main.py +317 -0
- cotlab/patching/__init__.py +24 -0
- cotlab/patching/cache.py +141 -0
- cotlab/patching/hooks.py +558 -0
- cotlab/patching/interventions.py +86 -0
- cotlab/patching/patcher.py +439 -0
- cotlab/patching/sae.py +181 -0
- cotlab/prompts/__init__.py +43 -0
- cotlab/prompts/cardiology.py +378 -0
- cotlab/prompts/histopathology.py +265 -0
- cotlab/prompts/length_matched_strategies.py +157 -0
- cotlab/prompts/mcq.py +193 -0
- cotlab/prompts/neurology.py +353 -0
- cotlab/prompts/oncology.py +367 -0
- cotlab/prompts/plab.py +162 -0
- cotlab/prompts/pubhealthbench.py +82 -0
- cotlab/prompts/pubmedqa.py +173 -0
- cotlab/prompts/radiology.py +414 -0
- cotlab/prompts/strategies.py +939 -0
- cotlab/prompts/tcga.py +168 -0
- cotlab/runner.py +204 -0
- cotlab-0.8.0.dist-info/METADATA +166 -0
- cotlab-0.8.0.dist-info/RECORD +65 -0
- cotlab-0.8.0.dist-info/WHEEL +4 -0
- cotlab-0.8.0.dist-info/entry_points.txt +3 -0
- cotlab-0.8.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Length-matched prompt strategies for controlled comparison.
|
|
2
|
+
|
|
3
|
+
These strategies produce prompts with identical token counts,
|
|
4
|
+
eliminating prompt length as a confounding variable in
|
|
5
|
+
mechanistic analysis experiments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..core.base import BasePromptStrategy
|
|
11
|
+
from ..core.registry import Registry
|
|
12
|
+
|
|
13
|
+
# Target prompt template (longest version - others will pad to match)
|
|
14
|
+
# We use a standardized template where only the "instruction" varies
|
|
15
|
+
# but total token count stays the same via padding.
|
|
16
|
+
|
|
17
|
+
STANDARD_QUESTION_TEMPLATE = """You are a medical expert. Consider this clinical case carefully.
|
|
18
|
+
|
|
19
|
+
{instruction}
|
|
20
|
+
|
|
21
|
+
Case: {question}
|
|
22
|
+
|
|
23
|
+
{padding}Provide your analysis:"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@Registry.register_prompt("contrarian_matched")
|
|
27
|
+
class ContrarianMatchedStrategy(BasePromptStrategy):
|
|
28
|
+
"""
|
|
29
|
+
Length-matched contrarian strategy.
|
|
30
|
+
|
|
31
|
+
Matches token count with other strategies by using standardized template.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
INSTRUCTION = """Play devil's advocate and argue against the obvious answer.
|
|
35
|
+
State the obvious diagnosis, then explain why it might be WRONG.
|
|
36
|
+
Consider alternative explanations that could fit the symptoms."""
|
|
37
|
+
|
|
38
|
+
PADDING = "" # No padding needed - this is the longest
|
|
39
|
+
|
|
40
|
+
def __init__(self, name: str = "contrarian_matched", **kwargs):
|
|
41
|
+
self._name = name
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
return self._name
|
|
46
|
+
|
|
47
|
+
def build_prompt(self, input_data: Dict[str, Any]) -> str:
|
|
48
|
+
question = input_data.get("question", input_data.get("text", ""))
|
|
49
|
+
return STANDARD_QUESTION_TEMPLATE.format(
|
|
50
|
+
instruction=self.INSTRUCTION, question=question, padding=self.PADDING
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def parse_response(self, response: str) -> Dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"answer": response.strip(),
|
|
56
|
+
"reasoning": response,
|
|
57
|
+
"raw": response,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def get_system_message(self) -> Optional[str]:
|
|
61
|
+
return None # System message in prompt for consistency
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@Registry.register_prompt("cot_matched")
|
|
65
|
+
class ChainOfThoughtMatchedStrategy(BasePromptStrategy):
|
|
66
|
+
"""
|
|
67
|
+
Length-matched chain-of-thought strategy.
|
|
68
|
+
|
|
69
|
+
Matches token count with contrarian via padding.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
INSTRUCTION = """Think through this problem step by step.
|
|
73
|
+
Reason carefully before giving your final answer.
|
|
74
|
+
Show your complete reasoning process."""
|
|
75
|
+
|
|
76
|
+
# Padding to match contrarian's longer instruction
|
|
77
|
+
PADDING = "Take your time. "
|
|
78
|
+
|
|
79
|
+
def __init__(self, name: str = "cot_matched", **kwargs):
|
|
80
|
+
self._name = name
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def name(self) -> str:
|
|
84
|
+
return self._name
|
|
85
|
+
|
|
86
|
+
def build_prompt(self, input_data: Dict[str, Any]) -> str:
|
|
87
|
+
question = input_data.get("question", input_data.get("text", ""))
|
|
88
|
+
return STANDARD_QUESTION_TEMPLATE.format(
|
|
89
|
+
instruction=self.INSTRUCTION, question=question, padding=self.PADDING
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def parse_response(self, response: str) -> Dict[str, Any]:
|
|
93
|
+
return {
|
|
94
|
+
"answer": response.strip(),
|
|
95
|
+
"reasoning": response,
|
|
96
|
+
"raw": response,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def get_system_message(self) -> Optional[str]:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@Registry.register_prompt("direct_matched")
|
|
104
|
+
class DirectAnswerMatchedStrategy(BasePromptStrategy):
|
|
105
|
+
"""
|
|
106
|
+
Length-matched direct answer strategy.
|
|
107
|
+
|
|
108
|
+
Matches token count with contrarian via padding.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
INSTRUCTION = """Give only the final diagnosis.
|
|
112
|
+
Do not explain your reasoning or show your work.
|
|
113
|
+
Answer with just the diagnosis name."""
|
|
114
|
+
|
|
115
|
+
# Padding to match contrarian's longer instruction
|
|
116
|
+
PADDING = "Be concise and direct. "
|
|
117
|
+
|
|
118
|
+
def __init__(self, name: str = "direct_matched", **kwargs):
|
|
119
|
+
self._name = name
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def name(self) -> str:
|
|
123
|
+
return self._name
|
|
124
|
+
|
|
125
|
+
def build_prompt(self, input_data: Dict[str, Any]) -> str:
|
|
126
|
+
question = input_data.get("question", input_data.get("text", ""))
|
|
127
|
+
return STANDARD_QUESTION_TEMPLATE.format(
|
|
128
|
+
instruction=self.INSTRUCTION, question=question, padding=self.PADDING
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def parse_response(self, response: str) -> Dict[str, Any]:
|
|
132
|
+
return {
|
|
133
|
+
"answer": response.strip(),
|
|
134
|
+
"reasoning": None,
|
|
135
|
+
"raw": response,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def get_system_message(self) -> Optional[str]:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Utility function to verify token counts
|
|
143
|
+
def verify_token_counts(tokenizer, question: str = "Patient has chest pain.") -> Dict[str, int]:
|
|
144
|
+
"""Verify that all matched strategies produce same token count."""
|
|
145
|
+
strategies = [
|
|
146
|
+
ContrarianMatchedStrategy(),
|
|
147
|
+
ChainOfThoughtMatchedStrategy(),
|
|
148
|
+
DirectAnswerMatchedStrategy(),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
counts = {}
|
|
152
|
+
for strategy in strategies:
|
|
153
|
+
prompt = strategy.build_prompt({"question": question})
|
|
154
|
+
tokens = tokenizer(prompt, return_tensors="pt")
|
|
155
|
+
counts[strategy.name] = tokens["input_ids"].shape[1]
|
|
156
|
+
|
|
157
|
+
return counts
|
cotlab/prompts/mcq.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""MCQ (Multiple Choice Question) prompt strategy for medical benchmarks.
|
|
2
|
+
|
|
3
|
+
This is a generic prompt strategy for MCQ-style datasets like MedQA, MMLU, MedMCQA.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from ..core.base import BasePromptStrategy, StructuredOutputMixin
|
|
9
|
+
from ..core.registry import Registry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@Registry.register_prompt("mcq")
|
|
13
|
+
class MCQPromptStrategy(BasePromptStrategy, StructuredOutputMixin):
|
|
14
|
+
"""Generic MCQ prompt strategy for medical question answering."""
|
|
15
|
+
|
|
16
|
+
SYSTEM_ROLE = """You are an expert medical professional taking a medical licensing examination.
|
|
17
|
+
You must carefully analyze each question and select the single best answer from the options provided.
|
|
18
|
+
Think through your reasoning step by step before selecting your final answer."""
|
|
19
|
+
|
|
20
|
+
SYSTEM_ROLE_CONTRARIAN = """You are a skeptical medical expert reviewing a licensing question.
|
|
21
|
+
Critically analyze the scenario. Actively look for distractors and "red herrings" that might lead to a common trap answer.
|
|
22
|
+
Consider why the "obvious" choice might be wrong and justify your final selection with rigorous evidence."""
|
|
23
|
+
|
|
24
|
+
PROMPT_TEMPLATE = """## Medical Question
|
|
25
|
+
|
|
26
|
+
{question_text}
|
|
27
|
+
|
|
28
|
+
## Instructions
|
|
29
|
+
|
|
30
|
+
Analyze this medical question carefully. Consider:
|
|
31
|
+
1. Key clinical findings and patient presentation
|
|
32
|
+
2. Relevant pathophysiology and mechanisms
|
|
33
|
+
3. Standard diagnostic and treatment guidelines
|
|
34
|
+
4. Why each option is correct or incorrect
|
|
35
|
+
|
|
36
|
+
After your analysis, select the single best answer from the provided options.
|
|
37
|
+
|
|
38
|
+
{format_instructions}"""
|
|
39
|
+
|
|
40
|
+
FEW_SHOT_EXAMPLES = [
|
|
41
|
+
{
|
|
42
|
+
"question": """A 45-year-old woman presents with fatigue, weight gain, and cold intolerance. Laboratory studies show elevated TSH and low free T4. Which of the following is the most likely diagnosis?
|
|
43
|
+
|
|
44
|
+
A) Graves' disease
|
|
45
|
+
B) Hashimoto's thyroiditis
|
|
46
|
+
C) Thyroid adenoma
|
|
47
|
+
D) Subacute thyroiditis""",
|
|
48
|
+
"reasoning": "The patient presents with classic symptoms of hypothyroidism: fatigue, weight gain, and cold intolerance. The lab findings confirm primary hypothyroidism with elevated TSH (the pituitary is trying to stimulate the thyroid) and low free T4 (the thyroid is not producing enough hormone). Hashimoto's thyroiditis is the most common cause of primary hypothyroidism in iodine-sufficient areas. Graves' disease causes hyperthyroidism. Thyroid adenoma typically presents as a nodule and doesn't usually cause hypothyroidism. Subacute thyroiditis can cause transient hypothyroidism but typically presents with pain and follows a viral illness.",
|
|
49
|
+
"answer": "B",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"question": """A 60-year-old man with a history of smoking presents with hemoptysis and a 3 cm lung mass on chest CT. Biopsy shows small round blue cells. Which of the following is the most appropriate next step?
|
|
53
|
+
|
|
54
|
+
A) Surgical resection
|
|
55
|
+
B) Radiation therapy alone
|
|
56
|
+
C) Chemotherapy with cisplatin and etoposide
|
|
57
|
+
D) Observation with repeat imaging in 3 months""",
|
|
58
|
+
"reasoning": "The biopsy showing small round blue cells in a smoker with a lung mass is diagnostic of small cell lung cancer (SCLC). SCLC is highly aggressive but initially chemosensitive. Unlike non-small cell lung cancer, surgical resection is not typically indicated for SCLC because it has usually metastasized by the time of diagnosis. The standard first-line treatment is combination chemotherapy with cisplatin (or carboplatin) and etoposide, often combined with radiation for limited-stage disease. Observation would be inappropriate given the aggressive nature of this malignancy.",
|
|
59
|
+
"answer": "C",
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
name: str = "mcq",
|
|
66
|
+
few_shot: bool = True,
|
|
67
|
+
output_format: str = "json",
|
|
68
|
+
answer_first: bool = False,
|
|
69
|
+
contrarian: bool = False,
|
|
70
|
+
cot_trigger: str = "Let's think through this step by step:",
|
|
71
|
+
**kwargs,
|
|
72
|
+
):
|
|
73
|
+
self._name = name
|
|
74
|
+
self.few_shot = few_shot
|
|
75
|
+
self.output_format = output_format
|
|
76
|
+
self.answer_first = answer_first
|
|
77
|
+
self.contrarian = contrarian
|
|
78
|
+
self.cot_trigger = cot_trigger
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def name(self) -> str:
|
|
82
|
+
return self._name
|
|
83
|
+
|
|
84
|
+
def get_system_prompt(self) -> str:
|
|
85
|
+
return self.SYSTEM_ROLE_CONTRARIAN if self.contrarian else self.SYSTEM_ROLE
|
|
86
|
+
|
|
87
|
+
def build_prompt(
|
|
88
|
+
self,
|
|
89
|
+
inputs: Dict[str, Any],
|
|
90
|
+
few_shot: Optional[bool] = None,
|
|
91
|
+
**kwargs,
|
|
92
|
+
) -> str:
|
|
93
|
+
use_few_shot = few_shot if few_shot is not None else self.few_shot
|
|
94
|
+
|
|
95
|
+
# Get the question text (already formatted with options by the dataset loader)
|
|
96
|
+
question_text = inputs.get("text", "")
|
|
97
|
+
|
|
98
|
+
# Build format instructions based on output format
|
|
99
|
+
format_instructions = self._get_format_instructions()
|
|
100
|
+
|
|
101
|
+
# Build few-shot examples if enabled
|
|
102
|
+
examples_str = ""
|
|
103
|
+
if use_few_shot:
|
|
104
|
+
examples_str = self._build_few_shot_examples()
|
|
105
|
+
|
|
106
|
+
# Construct the prompt
|
|
107
|
+
prompt = self.PROMPT_TEMPLATE.format(
|
|
108
|
+
question_text=question_text,
|
|
109
|
+
format_instructions=format_instructions,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if examples_str:
|
|
113
|
+
prompt = f"## Examples\n\n{examples_str}\n\n{prompt}"
|
|
114
|
+
|
|
115
|
+
if self.cot_trigger:
|
|
116
|
+
prompt += f"\n\n{self.cot_trigger}"
|
|
117
|
+
|
|
118
|
+
return prompt
|
|
119
|
+
|
|
120
|
+
def _build_few_shot_examples(self) -> str:
|
|
121
|
+
examples = []
|
|
122
|
+
for i, ex in enumerate(self.FEW_SHOT_EXAMPLES, 1):
|
|
123
|
+
if self.answer_first:
|
|
124
|
+
example = f"### Example {i}\n\n{ex['question']}\n\n**Answer:** {ex['answer']}\n\n**Reasoning:** {ex['reasoning']}"
|
|
125
|
+
else:
|
|
126
|
+
example = f"### Example {i}\n\n{ex['question']}\n\n**Reasoning:** {ex['reasoning']}\n\n**Answer:** {ex['answer']}"
|
|
127
|
+
examples.append(example)
|
|
128
|
+
return "\n\n".join(examples)
|
|
129
|
+
|
|
130
|
+
def _get_format_instructions(self) -> str:
|
|
131
|
+
if self.output_format == "json":
|
|
132
|
+
if self.answer_first:
|
|
133
|
+
return """Respond with a JSON object in this exact format:
|
|
134
|
+
```json
|
|
135
|
+
{"answer": "X", "reasoning": "Your step-by-step explanation"}
|
|
136
|
+
```
|
|
137
|
+
Where X is the letter (e.g., A, B, C, D, ...) of the correct answer."""
|
|
138
|
+
else:
|
|
139
|
+
return """Respond with a JSON object in this exact format:
|
|
140
|
+
```json
|
|
141
|
+
{"reasoning": "Your step-by-step explanation", "answer": "X"}
|
|
142
|
+
```
|
|
143
|
+
Where X is the letter (e.g., A, B, C, D, ...) of the correct answer."""
|
|
144
|
+
else:
|
|
145
|
+
if self.answer_first:
|
|
146
|
+
return "State your final answer as a single letter, followed by your reasoning."
|
|
147
|
+
else:
|
|
148
|
+
return "Provide your reasoning, then state your final answer as a single letter."
|
|
149
|
+
|
|
150
|
+
def parse_response(self, response: str) -> Dict[str, Any]:
|
|
151
|
+
"""Parse model response to extract answer and reasoning."""
|
|
152
|
+
import json
|
|
153
|
+
import re
|
|
154
|
+
|
|
155
|
+
result = {"answer": None, "reasoning": None, "raw_response": response}
|
|
156
|
+
|
|
157
|
+
# Try JSON parsing first
|
|
158
|
+
try:
|
|
159
|
+
# Find JSON block
|
|
160
|
+
json_match = re.search(r"\{[^{}]*\}", response, re.DOTALL)
|
|
161
|
+
if json_match:
|
|
162
|
+
parsed = json.loads(json_match.group())
|
|
163
|
+
result["answer"] = parsed.get("answer", "").upper().strip()
|
|
164
|
+
result["reasoning"] = parsed.get("reasoning", "")
|
|
165
|
+
return result
|
|
166
|
+
except json.JSONDecodeError:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
# Fallback: Look for answer pattern
|
|
170
|
+
answer_patterns = [
|
|
171
|
+
r"(?:final\s+answer|answer|selection|choice)\s*[:\-]\s*([A-Z])",
|
|
172
|
+
r"(?:final\s+answer|answer)\s*(?:is|would be)\s*([A-Z])",
|
|
173
|
+
r"(?:^|\n)\s*([A-Z])\s*(?:\)|\.|\:)",
|
|
174
|
+
r"\*\*([A-Z])\*\*",
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
for pattern in answer_patterns:
|
|
178
|
+
match = re.search(pattern, response, re.IGNORECASE)
|
|
179
|
+
if match:
|
|
180
|
+
result["answer"] = match.group(1).upper()
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
# If still no answer, look for lone letter
|
|
184
|
+
if not result["answer"]:
|
|
185
|
+
letters = re.findall(r"\b([A-Z])\b", response)
|
|
186
|
+
if letters:
|
|
187
|
+
result["answer"] = letters[-1].upper() # Take last mentioned
|
|
188
|
+
|
|
189
|
+
result["reasoning"] = response
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
def get_prediction_field(self) -> str:
|
|
193
|
+
return "answer"
|