ColaPrince commited on
Commit
5628414
·
verified ·
1 Parent(s): 059675e

Upload LLM_new.py

Browse files
Files changed (1) hide show
  1. LLM_new.py +243 -0
LLM_new.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import AutoModelForCausalLM, AutoTokenizer
2
+ from typing import Dict
3
+ from typing import Dict
4
+ import torch
5
+ from pathlib import Path
6
+ import numpy as np
7
+ import re
8
+ from Model import OmniPathWithInterTaskAttention
9
+
10
+ def build_prompt(pred_names: Dict[str, str], pred_scores: Dict[str, float]) -> str:
11
+ """
12
+ 根据分类结果,构建一个稳定、具医学上下文的提示,用于LLM生成简洁英文描述。
13
+ """
14
+ def get_pred(task_name):
15
+ name = pred_names.get(task_name, "N/A")
16
+ score = pred_scores.get(task_name, 0.0)
17
+ return f"{name} (confidence: {score:.1%})"
18
+
19
+ cancer_type = get_pred('cancer_type')
20
+ pathologic_stage = get_pred('pathologic_stage')
21
+ clinical_stage = get_pred('clinical_stage')
22
+ histological_type = get_pred('histological_type')
23
+
24
+ # 加强语义上下文
25
+ prompt = (
26
+ "You are a professional medical report generator. "
27
+ "Based on the patient's pathological classification and diagnostic model results, "
28
+ f"the cancer_type is {cancer_type}, "
29
+ f"pathologic_stage is {pathologic_stage}, "
30
+ f"clinical_stage is {clinical_stage}, "
31
+ f"and histological_type is {histological_type}. "
32
+ "Please write a concise English summary describing the diagnosis, staging interpretation, and general clinical implications as a short paragraph. "
33
+ "Avoid placeholders and avoid repeating words."
34
+ )
35
+ return prompt
36
+ # prepare the model input
37
+
38
+ def build_description(predictions: Dict[str, str], confidences: Dict[str, float],
39
+ model_name: str = "Qwen/Qwen3-0.6B", max_new_tokens: int = 32768) -> Dict[str, str]:
40
+ """
41
+ Compose the prompt and obtain a concise description using a model when available,
42
+ otherwise fall back to a deterministic template. Output length is controlled by max_new_tokens.
43
+ """
44
+ prompt = build_prompt(predictions, confidences)
45
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
46
+ model = AutoModelForCausalLM.from_pretrained(
47
+ model_name,
48
+ torch_dtype="auto",
49
+ device_map="auto"
50
+ )
51
+ messages = [
52
+ {"role": "user", "content": prompt}
53
+ ]
54
+ text = tokenizer.apply_chat_template(
55
+ messages,
56
+ tokenize=False,
57
+ add_generation_prompt=True,
58
+ enable_thinking=False # Switches between thinking and non-thinking modes. Default is True.
59
+ )
60
+ model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
61
+ generated_ids = model.generate(
62
+ **model_inputs,
63
+ max_new_tokens=32768
64
+ )
65
+ output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist()
66
+ try:
67
+ # rindex finding 151668 (</think>)
68
+ index = len(output_ids) - output_ids[::-1].index(151668)
69
+ except ValueError:
70
+ index = 0
71
+ thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n")
72
+ content = tokenizer.decode(output_ids[index:], skip_special_tokens=True).strip("\n")
73
+
74
+ print("thinking content:", thinking_content)
75
+ print("content:", content)
76
+
77
+ return {"prompt": prompt, "description": content}
78
+
79
+ @torch.no_grad()
80
+ def llm_single_patient_test(config, device):
81
+ """
82
+ 使用已训练好的 checkpoint,对单个小NPY患者进行推理测试(不依赖大NPY的数据集/数据加载器)。
83
+
84
+ 要求 config 提供:
85
+ - npy_path: 小NPY文件路径(二维矩阵,形状 [num_tiles, feature_dim])
86
+ - checkpoint_path: 训练得到的 best_model.pth 路径(包含 label_mappings 与模型权重)
87
+
88
+ 返回: {
89
+ 'short_id': str,
90
+ 'pred_names': Dict[task, class_name],
91
+ 'pred_scores': Dict[task, float],
92
+ 'raw_logits': Dict[task, torch.Tensor]
93
+ }
94
+ """
95
+ # 1) 读取小NPY(直接在此函数内完成)
96
+ npy_path = config['npy_path'] if isinstance(config, dict) else getattr(config, 'npy_path', None)
97
+ if not npy_path:
98
+ raise ValueError("config 中未提供 'npy_path'")
99
+ p = Path(npy_path)
100
+ if not p.exists():
101
+ raise FileNotFoundError(f"找不到npy文件: {npy_path}")
102
+ # 提取短ID(文件名前12位 TCGA-XX-XXXX)
103
+ m = re.search(r'(TCGA-[A-Z0-9]{2}-[A-Z0-9]{4})', p.name.upper())
104
+ short_id = m.group(1) if m else p.stem[:12]
105
+ arr = np.load(str(p), allow_pickle=False)
106
+ if not isinstance(arr, np.ndarray) or arr.ndim != 2:
107
+ raise ValueError(
108
+ f"npy 内容必须是二维特征矩阵 (tiles, dim),实际: type={type(arr)}, shape={getattr(arr, 'shape', None)}"
109
+ )
110
+ features = torch.from_numpy(arr).float() # [N, D]
111
+
112
+ # 2) 读取checkpoint
113
+ ckpt_path = config['checkpoint_path'] if isinstance(config, dict) else getattr(config, 'checkpoint_path', None)
114
+ if not ckpt_path:
115
+ raise ValueError("config 中未提供 'checkpoint_path'")
116
+ ckpt = torch.load(ckpt_path, map_location=device)
117
+
118
+ # 3) 构建模型(用checkpoint内保存的label_mappings与config参数)
119
+ label_mappings = ckpt.get('label_mappings', None)
120
+ if not label_mappings:
121
+ raise ValueError("checkpoint 中缺少 label_mappings,无法构建模型")
122
+
123
+ ck_cfg = ckpt.get('config', {}) if isinstance(ckpt.get('config', {}), dict) else {}
124
+ feature_dim = int(features.shape[1])
125
+ hidden_dim = int(ck_cfg.get('hidden_dim', 256))
126
+ dropout = float(ck_cfg.get('dropout', 0.3)) if 'dropout' in ck_cfg else 0.3
127
+ use_inter_task_attention = bool(ck_cfg.get('use_inter_task_attention', True))
128
+ inter_task_heads = int(ck_cfg.get('inter_task_heads', 4))
129
+
130
+ model = OmniPathWithInterTaskAttention(
131
+ label_mappings=label_mappings,
132
+ feature_dim=feature_dim,
133
+ hidden_dim=hidden_dim,
134
+ dropout=dropout,
135
+ use_inter_task_attention=use_inter_task_attention,
136
+ inter_task_heads=inter_task_heads
137
+ ).to(device)
138
+ model.load_state_dict(ckpt['model_state_dict'], strict=False)
139
+ model.eval()
140
+
141
+ # 4) 前向推理
142
+ feat_batch = features.unsqueeze(0).to(device) # [1, N, D]
143
+ outputs = model(feat_batch) # {task: [1, num_classes]}
144
+
145
+ # 5) 解码到类别名称与置信度
146
+ pred_names, pred_scores, raw_logits = {}, {}, {} # 为每个任务分别存放类别名、置信度和原始logits
147
+ for task_name, logits in outputs.items(): # 遍历各任务的输出(形如 [1, num_classes] 的logits)
148
+ probs = torch.softmax(logits[0], dim=-1) # 对单样本的logits做softmax,得到每个类别的概率分布
149
+ idx = int(torch.argmax(probs).item()) # 取概率最大的类别索引,作为预测类别
150
+ # 映射 idx -> class name
151
+ classes = label_mappings[task_name]['classes'] # 读取该任务的类别名称列表
152
+ class_name = classes[idx] if 0 <= idx < len(classes) else str(idx) # 将索引安全映射为类别名(越界则用字符串索引)
153
+ pred_names[task_name] = class_name # 记录该任务的预测类别名
154
+ pred_scores[task_name] = float(probs[idx].item()) # 记录该任务的预测置信度(最大概率)
155
+ raw_logits[task_name] = logits[0].detach().cpu() # 保存原始logits(去梯度并搬到CPU,便于后续分析/可视化)
156
+
157
+ return {
158
+ 'short_id': short_id,
159
+ 'pred_names': pred_names,
160
+ 'pred_scores': pred_scores,
161
+ 'raw_logits': raw_logits
162
+ }
163
+
164
+
165
+ def main():
166
+ # 设备
167
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
168
+ print(f"🖥️ 使用设备: {device}")
169
+
170
+ # 配置(请修改为你的实际路径)
171
+ config = {
172
+ # 必填:小NPY路径(二维矩阵)
173
+ 'npy_path': 'TCGA-56-8304-01Z-00-DX1.F7A7975D-C8AB-49C0-B9CD-18CFD01A0655.svs.npy',
174
+ # 必填:训练阶段保存的最佳checkpoint
175
+ 'checkpoint_path': 'best_model.pth',
176
+ # 可选:文本描述生成器配置
177
+ 'model_name': "Qwen/Qwen3-0.6B",
178
+ 'max_new_tokens': 32768,
179
+ }
180
+
181
+ # 基础检查
182
+ if not Path(config['npy_path']).exists():
183
+ raise FileNotFoundError(f"npy_path 不存在: {config['npy_path']}")
184
+ if not Path(config['checkpoint_path']).exists():
185
+ raise FileNotFoundError(f"checkpoint_path 不存在: {config['checkpoint_path']}")
186
+
187
+ # 1) 单病人推理
188
+ result = llm_single_patient_test(config, device)
189
+ short_id = result['short_id']
190
+ pred_names = result['pred_names']
191
+ pred_scores = result['pred_scores']
192
+
193
+ print("\n预测结果(按任务):")
194
+ for task, name in pred_names.items():
195
+ print(f"- {task}: {name} (conf {pred_scores.get(task, 0.0):.3f})")
196
+
197
+ # 2) 生成文字描述
198
+ desc = build_description(
199
+ predictions=pred_names,
200
+ confidences=pred_scores,
201
+ model_name=config.get('model_name', "Qwen/Qwen3-0.6B"),
202
+ max_new_tokens=int(config.get('max_new_tokens', 32768))
203
+ )
204
+
205
+ print("\n=== 自动生成的英文描述 ===")
206
+ print(desc['description'])
207
+
208
+ # 2.1 保存完整结果到文本文件
209
+ full_txt_path = f"{short_id}_description.txt"
210
+ try:
211
+ with open(full_txt_path, 'w', encoding='utf-8') as f:
212
+ f.write("=== PROMPT ===\n")
213
+ f.write(desc['prompt'])
214
+ f.write("\n\n=== DESCRIPTION (full paragraph) ===\n")
215
+ f.write(desc['description'])
216
+ print(f"\n📝 Full text saved to: {full_txt_path}")
217
+ except Exception as e:
218
+ print(f"Could not save description to file: {e}")
219
+
220
+ # === 3) 可视化显示 ===
221
+ import textwrap
222
+
223
+ # 3.1 控制台彩色输出
224
+ print("\n" + "="*80)
225
+ print("\033[1;34m🧠 PROMPT:\033[0m\n")
226
+ wrapped_prompt = textwrap.fill(desc['prompt'], width=100)
227
+ print(f"\033[0;37m{wrapped_prompt}\033[0m")
228
+
229
+ print("\n\033[1;32m💬 DESCRIPTION:\033[0m\n")
230
+ wrapped_desc = textwrap.fill(desc['description'], width=100)
231
+ print(f"\033[0;37m{wrapped_desc}\033[0m")
232
+ print("="*80 + "\n")
233
+
234
+ return {
235
+ 'short_id': short_id,
236
+ 'predictions': pred_names,
237
+ 'confidences': pred_scores,
238
+ 'description': desc['description'],
239
+ 'prompt': desc['prompt'],
240
+ }
241
+
242
+ if __name__ == "__main__":
243
+ main()