| from transformers import AutoModelForCausalLM, AutoTokenizer |
| from typing import Dict |
| from typing import Dict |
| import torch |
| from pathlib import Path |
| import numpy as np |
| import re |
| from Model import OmniPathWithInterTaskAttention |
|
|
| def build_prompt(pred_names: Dict[str, str], pred_scores: Dict[str, float]) -> str: |
| """ |
| 根据分类结果,构建一个稳定、具医学上下文的提示,用于LLM生成简洁英文描述。 |
| """ |
| def get_pred(task_name): |
| name = pred_names.get(task_name, "N/A") |
| score = pred_scores.get(task_name, 0.0) |
| return f"{name} (confidence: {score:.1%})" |
|
|
| cancer_type = get_pred('cancer_type') |
| pathologic_stage = get_pred('pathologic_stage') |
| clinical_stage = get_pred('clinical_stage') |
| histological_type = get_pred('histological_type') |
|
|
| |
| prompt = ( |
| "You are a professional medical report generator. " |
| "Based on the patient's pathological classification and diagnostic model results, " |
| f"the cancer_type is {cancer_type}, " |
| f"pathologic_stage is {pathologic_stage}, " |
| f"clinical_stage is {clinical_stage}, " |
| f"and histological_type is {histological_type}. " |
| "Please write a concise English summary describing the diagnosis, staging interpretation, and general clinical implications as a short paragraph. " |
| "Avoid placeholders and avoid repeating words." |
| ) |
| return prompt |
| |
|
|
| def build_description(predictions: Dict[str, str], confidences: Dict[str, float], |
| model_name: str = "Qwen/Qwen3-0.6B", max_new_tokens: int = 32768) -> Dict[str, str]: |
| """ |
| Compose the prompt and obtain a concise description using a model when available, |
| otherwise fall back to a deterministic template. Output length is controlled by max_new_tokens. |
| """ |
| prompt = build_prompt(predictions, confidences) |
| tokenizer = AutoTokenizer.from_pretrained(model_name) |
| model = AutoModelForCausalLM.from_pretrained( |
| model_name, |
| torch_dtype="auto", |
| device_map="auto" |
| ) |
| messages = [ |
| {"role": "user", "content": prompt} |
| ] |
| text = tokenizer.apply_chat_template( |
| messages, |
| tokenize=False, |
| add_generation_prompt=True, |
| enable_thinking=False |
| ) |
| model_inputs = tokenizer([text], return_tensors="pt").to(model.device) |
| generated_ids = model.generate( |
| **model_inputs, |
| max_new_tokens=32768 |
| ) |
| output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist() |
| try: |
| |
| index = len(output_ids) - output_ids[::-1].index(151668) |
| except ValueError: |
| index = 0 |
| thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n") |
| content = tokenizer.decode(output_ids[index:], skip_special_tokens=True).strip("\n") |
|
|
| print("thinking content:", thinking_content) |
| print("content:", content) |
|
|
| return {"prompt": prompt, "description": content} |
|
|
| @torch.no_grad() |
| def llm_single_patient_test(config, device): |
| """ |
| 使用已训练好的 checkpoint,对单个小NPY患者进行推理测试(不依赖大NPY的数据集/数据加载器)。 |
| |
| 要求 config 提供: |
| - npy_path: 小NPY文件路径(二维矩阵,形状 [num_tiles, feature_dim]) |
| - checkpoint_path: 训练得到的 best_model.pth 路径(包含 label_mappings 与模型权重) |
| |
| 返回: { |
| 'short_id': str, |
| 'pred_names': Dict[task, class_name], |
| 'pred_scores': Dict[task, float], |
| 'raw_logits': Dict[task, torch.Tensor] |
| } |
| """ |
| |
| npy_path = config['npy_path'] if isinstance(config, dict) else getattr(config, 'npy_path', None) |
| if not npy_path: |
| raise ValueError("config 中未提供 'npy_path'") |
| p = Path(npy_path) |
| if not p.exists(): |
| raise FileNotFoundError(f"找不到npy文件: {npy_path}") |
| |
| m = re.search(r'(TCGA-[A-Z0-9]{2}-[A-Z0-9]{4})', p.name.upper()) |
| short_id = m.group(1) if m else p.stem[:12] |
| arr = np.load(str(p), allow_pickle=False) |
| if not isinstance(arr, np.ndarray) or arr.ndim != 2: |
| raise ValueError( |
| f"npy 内容必须是二维特征矩阵 (tiles, dim),实际: type={type(arr)}, shape={getattr(arr, 'shape', None)}" |
| ) |
| features = torch.from_numpy(arr).float() |
|
|
| |
| ckpt_path = config['checkpoint_path'] if isinstance(config, dict) else getattr(config, 'checkpoint_path', None) |
| if not ckpt_path: |
| raise ValueError("config 中未提供 'checkpoint_path'") |
| ckpt = torch.load(ckpt_path, map_location=device) |
|
|
| |
| label_mappings = ckpt.get('label_mappings', None) |
| if not label_mappings: |
| raise ValueError("checkpoint 中缺少 label_mappings,无法构建模型") |
|
|
| ck_cfg = ckpt.get('config', {}) if isinstance(ckpt.get('config', {}), dict) else {} |
| feature_dim = int(features.shape[1]) |
| hidden_dim = int(ck_cfg.get('hidden_dim', 256)) |
| dropout = float(ck_cfg.get('dropout', 0.3)) if 'dropout' in ck_cfg else 0.3 |
| use_inter_task_attention = bool(ck_cfg.get('use_inter_task_attention', True)) |
| inter_task_heads = int(ck_cfg.get('inter_task_heads', 4)) |
|
|
| model = OmniPathWithInterTaskAttention( |
| label_mappings=label_mappings, |
| feature_dim=feature_dim, |
| hidden_dim=hidden_dim, |
| dropout=dropout, |
| use_inter_task_attention=use_inter_task_attention, |
| inter_task_heads=inter_task_heads |
| ).to(device) |
| model.load_state_dict(ckpt['model_state_dict'], strict=False) |
| model.eval() |
|
|
| |
| feat_batch = features.unsqueeze(0).to(device) |
| outputs = model(feat_batch) |
|
|
| |
| pred_names, pred_scores, raw_logits = {}, {}, {} |
| for task_name, logits in outputs.items(): |
| probs = torch.softmax(logits[0], dim=-1) |
| idx = int(torch.argmax(probs).item()) |
| |
| classes = label_mappings[task_name]['classes'] |
| class_name = classes[idx] if 0 <= idx < len(classes) else str(idx) |
| pred_names[task_name] = class_name |
| pred_scores[task_name] = float(probs[idx].item()) |
| raw_logits[task_name] = logits[0].detach().cpu() |
|
|
| return { |
| 'short_id': short_id, |
| 'pred_names': pred_names, |
| 'pred_scores': pred_scores, |
| 'raw_logits': raw_logits |
| } |
|
|
|
|
| def main(): |
| |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| print(f"🖥️ 使用设备: {device}") |
|
|
| |
| config = { |
| |
| 'npy_path': 'TCGA-56-8304-01Z-00-DX1.F7A7975D-C8AB-49C0-B9CD-18CFD01A0655.svs.npy', |
| |
| 'checkpoint_path': 'best_model.pth', |
| |
| 'model_name': "Qwen/Qwen3-0.6B", |
| 'max_new_tokens': 32768, |
| } |
|
|
| |
| if not Path(config['npy_path']).exists(): |
| raise FileNotFoundError(f"npy_path 不存在: {config['npy_path']}") |
| if not Path(config['checkpoint_path']).exists(): |
| raise FileNotFoundError(f"checkpoint_path 不存在: {config['checkpoint_path']}") |
|
|
| |
| result = llm_single_patient_test(config, device) |
| short_id = result['short_id'] |
| pred_names = result['pred_names'] |
| pred_scores = result['pred_scores'] |
|
|
| print("\n预测结果(按任务):") |
| for task, name in pred_names.items(): |
| print(f"- {task}: {name} (conf {pred_scores.get(task, 0.0):.3f})") |
|
|
| |
| desc = build_description( |
| predictions=pred_names, |
| confidences=pred_scores, |
| model_name=config.get('model_name', "Qwen/Qwen3-0.6B"), |
| max_new_tokens=int(config.get('max_new_tokens', 32768)) |
| ) |
|
|
| print("\n=== 自动生成的英文描述 ===") |
| print(desc['description']) |
|
|
| |
| full_txt_path = f"{short_id}_description.txt" |
| try: |
| with open(full_txt_path, 'w', encoding='utf-8') as f: |
| f.write("=== PROMPT ===\n") |
| f.write(desc['prompt']) |
| f.write("\n\n=== DESCRIPTION (full paragraph) ===\n") |
| f.write(desc['description']) |
| print(f"\n📝 Full text saved to: {full_txt_path}") |
| except Exception as e: |
| print(f"Could not save description to file: {e}") |
|
|
| |
| import textwrap |
|
|
| |
| print("\n" + "="*80) |
| print("\033[1;34m🧠 PROMPT:\033[0m\n") |
| wrapped_prompt = textwrap.fill(desc['prompt'], width=100) |
| print(f"\033[0;37m{wrapped_prompt}\033[0m") |
|
|
| print("\n\033[1;32m💬 DESCRIPTION:\033[0m\n") |
| wrapped_desc = textwrap.fill(desc['description'], width=100) |
| print(f"\033[0;37m{wrapped_desc}\033[0m") |
| print("="*80 + "\n") |
| |
| return { |
| 'short_id': short_id, |
| 'predictions': pred_names, |
| 'confidences': pred_scores, |
| 'description': desc['description'], |
| 'prompt': desc['prompt'], |
| } |
|
|
| if __name__ == "__main__": |
| main() |
|
|