Files changed (1) hide show
  1. app.py +211 -0
app.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import numpy as np
4
+ from pathlib import Path
5
+ import re
6
+ from Model import OmniPathWithInterTaskAttention
7
+ from transformers import AutoModelForCausalLM, AutoTokenizer
8
+ import tempfile
9
+ import os
10
+
11
+ # 设备设置
12
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
13
+ print(f"使用设备: {device}")
14
+
15
+ # 预加载模型(避免重复加载)
16
+ @torch.no_grad()
17
+ def load_models():
18
+ """预加载必要的模型"""
19
+ # 1. 加载分类模型
20
+ ckpt_path = "best_model.pth"
21
+ if not Path(ckpt_path).exists():
22
+ raise FileNotFoundError(f"找不到模型文件: {ckpt_path}")
23
+
24
+ ckpt = torch.load(ckpt_path, map_location=device)
25
+ label_mappings = ckpt.get('label_mappings', None)
26
+ if not label_mappings:
27
+ raise ValueError("checkpoint 中缺少 label_mappings")
28
+
29
+ ck_cfg = ckpt.get('config', {})
30
+ feature_dim = 512 # 根据你的实际特征维度调整
31
+ hidden_dim = int(ck_cfg.get('hidden_dim', 256))
32
+ dropout = float(ck_cfg.get('dropout', 0.3))
33
+ use_inter_task_attention = bool(ck_cfg.get('use_inter_task_attention', True))
34
+ inter_task_heads = int(ck_cfg.get('inter_task_heads', 4))
35
+
36
+ classification_model = OmniPathWithInterTaskAttention(
37
+ label_mappings=label_mappings,
38
+ feature_dim=feature_dim,
39
+ hidden_dim=hidden_dim,
40
+ dropout=dropout,
41
+ use_inter_task_attention=use_inter_task_attention,
42
+ inter_task_heads=inter_task_heads
43
+ ).to(device)
44
+ classification_model.load_state_dict(ckpt['model_state_dict'], strict=False)
45
+ classification_model.eval()
46
+
47
+ # 2. 加载文本生成模型
48
+ llm_model_name = "Qwen/Qwen3-0.6B"
49
+ tokenizer = AutoTokenizer.from_pretrained(llm_model_name)
50
+ llm_model = AutoModelForCausalLM.from_pretrained(
51
+ llm_model_name,
52
+ torch_dtype="auto",
53
+ device_map="auto"
54
+ )
55
+
56
+ return classification_model, llm_model, tokenizer, label_mappings
57
+
58
+ # 预加载模型
59
+ classification_model, llm_model, tokenizer, label_mappings = load_models()
60
+
61
+ def build_prompt(pred_names, pred_scores):
62
+ """构建提示词"""
63
+ def get_pred(task_name):
64
+ name = pred_names.get(task_name, "N/A")
65
+ score = pred_scores.get(task_name, 0.0)
66
+ return f"{name} (confidence: {score:.1%})"
67
+
68
+ cancer_type = get_pred('cancer_type')
69
+ pathologic_stage = get_pred('pathologic_stage')
70
+ clinical_stage = get_pred('clinical_stage')
71
+ histological_type = get_pred('histological_type')
72
+
73
+ prompt = (
74
+ "You are a professional medical report generator. "
75
+ "Based on the patient's pathological classification and diagnostic model results, "
76
+ f"the cancer_type is {cancer_type}, "
77
+ f"pathologic_stage is {pathologic_stage}, "
78
+ f"clinical_stage is {clinical_stage}, "
79
+ f"and histological_type is {histological_type}. "
80
+ "Please write a concise English summary describing the diagnosis, staging interpretation, and general clinical implications as a short paragraph. "
81
+ "Avoid placeholders and avoid repeating words."
82
+ )
83
+ return prompt
84
+
85
+ def generate_description(predictions, confidences):
86
+ """生成描述"""
87
+ prompt = build_prompt(predictions, confidences)
88
+
89
+ messages = [{"role": "user", "content": prompt}]
90
+ text = tokenizer.apply_chat_template(
91
+ messages,
92
+ tokenize=False,
93
+ add_generation_prompt=True,
94
+ enable_thinking=False
95
+ )
96
+
97
+ model_inputs = tokenizer([text], return_tensors="pt").to(llm_model.device)
98
+ generated_ids = llm_model.generate(
99
+ **model_inputs,
100
+ max_new_tokens=500, # 减少token数量以加快生成速度
101
+ do_sample=True,
102
+ temperature=0.7,
103
+ )
104
+
105
+ output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist()
106
+ try:
107
+ index = len(output_ids) - output_ids[::-1].index(151668)
108
+ except ValueError:
109
+ index = 0
110
+
111
+ content = tokenizer.decode(output_ids[index:], skip_special_tokens=True).strip("\n")
112
+ return {"prompt": prompt, "description": content}
113
+
114
+ def process_npy_file(npy_file):
115
+ """处理上传的NPY文件"""
116
+ if npy_file is None:
117
+ return "请先上传NPY文件", "", ""
118
+
119
+ try:
120
+ # 读取NPY文件
121
+ arr = np.load(npy_file.name, allow_pickle=False)
122
+ if not isinstance(arr, np.ndarray) or arr.ndim != 2:
123
+ return "错误: NPY文件必须是二维特征矩阵", "", ""
124
+
125
+ features = torch.from_numpy(arr).float()
126
+
127
+ # 提取短ID
128
+ p = Path(npy_file.name)
129
+ m = re.search(r'(TCGA-[A-Z0-9]{2}-[A-Z0-9]{4})', p.name.upper())
130
+ short_id = m.group(1) if m else p.stem[:12]
131
+
132
+ # 推理
133
+ feat_batch = features.unsqueeze(0).to(device)
134
+ outputs = classification_model(feat_batch)
135
+
136
+ # 解码结果
137
+ pred_names, pred_scores = {}, {}
138
+ for task_name, logits in outputs.items():
139
+ probs = torch.softmax(logits[0], dim=-1)
140
+ idx = int(torch.argmax(probs).item())
141
+ classes = label_mappings[task_name]['classes']
142
+ class_name = classes[idx] if 0 <= idx < len(classes) else str(idx)
143
+ pred_names[task_name] = class_name
144
+ pred_scores[task_name] = float(probs[idx].item())
145
+
146
+ # 生成描述
147
+ desc_result = generate_description(pred_names, pred_scores)
148
+
149
+ # 格式化结果显示
150
+ results_text = f"患者ID: {short_id}\n\n预测结果:\n"
151
+ for task, name in pred_names.items():
152
+ results_text += f"- {task}: {name} (置信度: {pred_scores.get(task, 0.0):.3f})\n"
153
+
154
+ return results_text, desc_result['prompt'], desc_result['description']
155
+
156
+ except Exception as e:
157
+ return f"处理过程中出现错误: {str(e)}", "", ""
158
+
159
+ # 创建Gradio界面
160
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
161
+ gr.Markdown("""
162
+ # 🏥 医学病理诊断分析系统
163
+
164
+ 上传病理特征NPY文件,获取AI辅助诊断结果和医学描述。
165
+ """)
166
+
167
+ with gr.Row():
168
+ with gr.Column():
169
+ file_input = gr.File(
170
+ label="上传NPY特征文件",
171
+ file_types=[".npy"],
172
+ type="filepath"
173
+ )
174
+ analyze_btn = gr.Button("开始分析", variant="primary")
175
+
176
+ with gr.Column():
177
+ results_output = gr.Textbox(
178
+ label="分析结果",
179
+ lines=10,
180
+ max_lines=20
181
+ )
182
+
183
+ with gr.Row():
184
+ prompt_output = gr.Textbox(
185
+ label="提示词 (Prompt)",
186
+ lines=4,
187
+ max_lines=6
188
+ )
189
+
190
+ with gr.Row():
191
+ description_output = gr.Textbox(
192
+ label="AI生成描述",
193
+ lines=6,
194
+ max_lines=10
195
+ )
196
+
197
+ # 示例文件
198
+ gr.Examples(
199
+ examples=[["example.npy"]], # 你需要提供一个示例NPY文件
200
+ inputs=file_input,
201
+ label="点击使用示例文件"
202
+ )
203
+
204
+ analyze_btn.click(
205
+ fn=process_npy_file,
206
+ inputs=file_input,
207
+ outputs=[results_output, prompt_output, description_output]
208
+ )
209
+
210
+ if __name__ == "__main__":
211
+ demo.launch(share=True)