wangleiofficial commited on
Commit
3a16f3d
·
verified ·
1 Parent(s): f63f35b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +222 -313
app.py CHANGED
@@ -5,36 +5,32 @@ import torch.nn.functional as F
5
  import gradio as gr
6
  import matplotlib.pyplot as plt
7
  import matplotlib.patches as patches
8
- import matplotlib.patheffects as path_effects
9
  import numpy as np
10
  from io import BytesIO
11
  from PIL import Image
12
  from transformers import AutoTokenizer, AutoModel
13
 
14
  # ==========================
15
- # 0. 环境初始化与设置
16
  # ==========================
17
- # 强制使用非交互式后端,防止 matplotlib 在服务器上报错
18
- plt.switch_backend('Agg')
19
 
20
  os.environ["HF_HOME"] = "/tmp/hf_cache"
21
  os.environ["TRANSFORMERS_CACHE"] = "/tmp/hf_cache"
22
  os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
23
 
24
- # 清理缓存以防止空间溢出
25
  for path in ["/tmp/hf_cache", os.path.expanduser("~/.cache/huggingface")]:
26
  shutil.rmtree(path, ignore_errors=True)
27
  os.makedirs(path, exist_ok=True)
28
 
29
  # ==========================
30
- # 1. 模型架构定义
31
  # ==========================
32
  class AttentionPooling(nn.Module):
33
- """Attention Pooling Layer"""
34
  def __init__(self, d_model):
35
  super().__init__()
36
  self.attention_net = nn.Linear(d_model, 1)
37
-
38
  def forward(self, x, mask):
39
  attn_logits = self.attention_net(x).squeeze(2)
40
  attn_logits.masked_fill_(mask == 0, -float('inf'))
@@ -42,68 +38,42 @@ class AttentionPooling(nn.Module):
42
  return torch.bmm(attn_weights.unsqueeze(1), x).squeeze(1)
43
 
44
  class ProtDualBranchEnhancedClassifier(nn.Module):
45
- """Enhanced dual-branch model architecture"""
46
  def __init__(self, d_model, projection_dim, num_classes, dropout, kernel_size):
47
  super().__init__()
48
- # Branch 1: Global Semantic (CLS token)
49
  self.cls_projector = nn.Linear(d_model, projection_dim)
50
-
51
- # Branch 2: Local Structural (Token embeddings + CNN)
52
  self.token_refiner = nn.Sequential(
53
- nn.Conv1d(d_model, d_model, kernel_size, padding='same'),
54
- nn.ReLU()
55
  )
56
  self.attention_pooling = AttentionPooling(d_model)
57
  self.tok_projector = nn.Linear(d_model, projection_dim)
58
-
59
- # Fusion Gate
60
  fused_dim = projection_dim * 2
61
- self.gate = nn.Sequential(
62
- nn.Linear(fused_dim, fused_dim),
63
- nn.Sigmoid()
64
- )
65
-
66
- # Classifier Head
67
  self.classifier_head = nn.Sequential(
68
- nn.LayerNorm(fused_dim),
69
- nn.Linear(fused_dim, fused_dim * 2),
70
- nn.ReLU(),
71
- nn.Dropout(dropout),
72
- nn.Linear(fused_dim * 2, num_classes)
73
  )
74
-
75
  def forward(self, cls_embedding, token_embeddings, mask):
76
- # Process Branch 1
77
  z_cls = self.cls_projector(cls_embedding)
78
-
79
- # Process Branch 2
80
  tok_emb_permuted = token_embeddings.permute(0, 2, 1)
81
  refined_tok_emb = self.token_refiner(tok_emb_permuted).permute(0, 2, 1)
82
  z_tok_pooled = self.attention_pooling(refined_tok_emb, mask)
83
  z_tok = self.tok_projector(z_tok_pooled)
84
-
85
- # Fusion
86
  z_fused_concat = torch.cat([z_cls, z_tok], dim=1)
87
  gate_values = self.gate(z_fused_concat)
88
  z_fused_gated = z_fused_concat * gate_values
89
-
90
  return self.classifier_head(z_fused_gated)
91
 
92
  # ==========================
93
- # 2. 加载模型与资源
94
  # ==========================
95
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
96
  PLM_MODEL_NAME = "facebook/esm2_t30_150M_UR50D"
97
  CLASSIFIER_PATH = "best_model_esm2_t30_150M_UR50D.pth"
98
  LABEL_MAP_PATH = "label_map.json"
99
 
100
- # --- 检查文件 ---
101
- if not os.path.exists(LABEL_MAP_PATH):
102
- raise FileNotFoundError(f"Error: Missing '{LABEL_MAP_PATH}'. Please upload it to your Space.")
103
- if not os.path.exists(CLASSIFIER_PATH):
104
- raise FileNotFoundError(f"Error: Could not find '{CLASSIFIER_PATH}'. Please upload your trained .pth file.")
105
 
106
- # --- 加载 Label Map ---
107
  with open(LABEL_MAP_PATH, 'r') as f:
108
  label_to_idx = json.load(f)
109
  idx_to_label = {v: k for k, v in label_to_idx.items()}
@@ -111,42 +81,40 @@ with open(LABEL_MAP_PATH, 'r') as f:
111
  NUM_CLASSES = len(idx_to_label)
112
  D_MODEL = 640
113
 
114
- print(f"🔹 Loading ESM-2 Model ({PLM_MODEL_NAME})...")
115
  tokenizer = AutoTokenizer.from_pretrained(PLM_MODEL_NAME)
116
- plm_model = AutoModel.from_pretrained(PLM_MODEL_NAME).to(DEVICE)
117
- plm_model.eval()
118
-
119
- print("🔹 Loading Custom Classifier...")
120
- classifier = ProtDualBranchEnhancedClassifier(
121
- d_model=D_MODEL, projection_dim=32, num_classes=NUM_CLASSES,
122
- dropout=0.3, kernel_size=3
123
- ).to(DEVICE)
124
-
125
  classifier.load_state_dict(torch.load(CLASSIFIER_PATH, map_location=DEVICE))
126
  classifier.eval()
127
- print("✅ All Models Loaded Successfully.")
128
 
129
  # ==========================
130
- # 3. 高保真绘图引擎 (Visualization Engine)
131
  # ==========================
132
- def draw_prokaryotic_cell_detailed(target_class):
133
  """
134
- Generate a high-fidelity, scientific illustration of a Gram-negative bacterium
135
- with the predicted subcellular location highlighted.
136
  """
137
  target = target_class.lower() if target_class else ""
138
 
139
- # --- 精细配色板 (Scientific Palette) ---
 
140
  c = {
141
- 'bg': '#ffffff',
142
- 'om_out': '#475569', 'om_in': '#94a3b8', 'om_fill': '#e2e8f0',
143
- 'cw_mesh': '#64748b', 'cw_bg': '#f1f5f9',
144
- 'im_out': '#64748b', 'im_in': '#cbd5e1', 'im_fill': '#f8fafc',
145
- 'cyto_bg': '#ffffff', 'ribo': '#e2e8f0', 'dna': '#cbd5e1',
146
- 'hl_core': '#ef4444', 'hl_glow1': '#fca5a5', 'hl_glow2': '#fee2e2'
 
 
 
 
147
  }
148
 
149
- # --- 匹配逻辑 ---
150
  is_secreted = "extracellular" in target or "secreted" in target
151
  is_om = "outer membrane" in target
152
  is_peri = "periplasm" in target
@@ -154,117 +122,127 @@ def draw_prokaryotic_cell_detailed(target_class):
154
  is_im = "plasma membrane" in target or "inner membrane" in target
155
  is_cyto = "cytoplasm" in target or "cytosol" in target
156
 
157
- # 创建高分辨率画布
158
- fig, ax = plt.subplots(figsize=(8, 5), dpi=150)
159
- ax.set_xlim(-1, 11)
160
- ax.set_ylim(-0.5, 6.5)
161
  ax.axis('off')
162
 
163
- # --- 辅助绘图函数:拟态3D胶囊 ---
164
- def draw_3d_capsule(x, y, w, h, fill_c, edge_c_out, edge_c_in, z, is_hl=False, hl_style='rim'):
165
- r = h / 2
166
- # 基础填充
167
- base = patches.FancyBboxPatch((x, y), w-h, h, boxstyle=f"round,pad=0,rounding_size={r}", fc=fill_c, ec='none', zorder=z)
168
- ax.add_patch(base)
169
 
170
- # 内圈描边 (模拟厚度/立体感)
171
- inner_border = patches.FancyBboxPatch((x+0.08, y+0.08), w-h-0.16, h-0.16, boxstyle=f"round,pad=0,rounding_size={r-0.08}", fc='none', ec=edge_c_in, lw=1.5, zorder=z+0.1)
172
- ax.add_patch(inner_border)
173
-
174
- # 外圈描边
175
- outer_border = patches.FancyBboxPatch((x, y), w-h, h, boxstyle=f"round,pad=0,rounding_size={r}", fc='none', ec=edge_c_out, lw=2, zorder=z+0.2)
176
- ax.add_patch(outer_border)
177
-
178
- # 高亮发光逻辑
179
- if is_hl:
180
- if hl_style == 'rim': # 膜结构边缘发光
181
- for lw, alpha, color in [(6, 0.3, c['hl_glow2']), (4, 0.5, c['hl_glow1']), (2.5, 0.8, c['hl_core'])]:
182
- hl = patches.FancyBboxPatch((x, y), w-h, h, boxstyle=f"round,pad=0,rounding_size={r}", fc='none', ec=color, lw=lw, alpha=alpha, zorder=z+0.5)
183
- ax.add_patch(hl)
184
- elif hl_style == 'fill': # 区域填充发光
185
- hl_fill = patches.FancyBboxPatch((x, y), w-h, h, boxstyle=f"round,pad=0,rounding_size={r}", fc=c['hl_glow2'], ec=c['hl_core'], lw=2, alpha=0.8, zorder=z+0.5)
186
- ax.add_patch(hl_fill)
187
-
188
- # --- 1. 绘制层级结构 (从外向内) ---
189
-
190
- # Layer 1: Outer Membrane (外膜)
191
- draw_3d_capsule(0, 0, 10, 6, c['om_fill'], c['om_out'], c['om_in'], z=1, is_hl=is_om, hl_style='rim')
192
-
193
- # Layer 2: Cell Wall Space (细胞壁区域)
194
- cw_base = patches.FancyBboxPatch((0.5, 0.5), 9-5, 5, boxstyle=f"round,pad=0,rounding_size={2.5}", fc=c['cw_bg'], ec='none', zorder=2)
195
- ax.add_patch(cw_base)
 
 
 
 
 
 
 
 
 
 
 
196
 
197
- # 细胞壁网格纹理 (Hatch)
198
- cw_mesh_color = c['hl_core'] if is_cw else c['cw_mesh']
199
- cw_mesh_lw = 1.5 if is_cw else 0.5
200
- cw_texture = patches.FancyBboxPatch((0.5, 0.5), 9-5, 5, boxstyle=f"round,pad=0,rounding_size={2.5}", fc='none', ec=cw_mesh_color, lw=cw_mesh_lw, hatch='XXXX', alpha=0.3 if not is_cw else 0.6, zorder=2.1)
201
- ax.add_patch(cw_texture)
202
-
203
- # 如果是周质 (Periplasm),在细胞壁区域加一层发光填充
204
- if is_peri:
205
- draw_3d_capsule(0.4, 0.4, 9.2, 5.2, 'none', 'none', 'none', z=2.2, is_hl=True, hl_style='fill')
206
-
207
- # Layer 3: Inner Membrane (内膜)
208
- draw_3d_capsule(1.0, 1.0, 8, 4, c['im_fill'], c['im_out'], c['im_in'], z=3, is_hl=is_im, hl_style='rim')
209
-
210
- # Layer 4: Cytoplasm (胞质)
211
- cyto_fill_c = c['hl_glow2'] if is_cyto else c['cyto_bg']
212
- cyto_ec = c['hl_core'] if is_cyto else 'none'
213
- draw_3d_capsule(1.3, 1.3, 7.4, 3.4, cyto_fill_c, cyto_ec, 'none', z=4, is_hl=is_cyto, hl_style='fill')
214
-
215
- # --- 2. 填充细节内容 ---
216
-
217
- # Ribosomes (核糖体颗粒)
218
- rng = np.random.default_rng(101)
219
- x_ribo = rng.uniform(2.0, 8.0, 400)
220
- y_ribo = rng.uniform(1.8, 4.2, 400)
221
- # 简单的椭圆过滤,只保留胶囊内部的点
222
- mask = ((x_ribo - 5)**2 / 3**2 + (y_ribo - 3)**2 / 1.2**2) < 1
223
- ax.scatter(x_ribo[mask], y_ribo[mask], s=3, c=c['ribo'], alpha=0.6, zorder=4.1)
224
-
225
- # Nucleoid DNA (拟核 DNA 缠绕线)
226
- t = np.linspace(0, 20, 500)
227
- x_dna = 5 + 2.0 * np.cos(t) * np.sin(t*0.3) * np.cos(t*0.1)
228
- y_dna = 3 + 0.8 * np.sin(t) * np.cos(t*0.2)
229
- # 使用 PathEffects 增加投影立体感
230
- dna_line = ax.plot(x_dna, y_dna, color=c['dna'], alpha=0.8, linewidth=2, zorder=4.2)[0]
231
- dna_line.set_path_effects([
232
- path_effects.SimpleLineShadow(offset=(1, -1), shadow_color=c['im_fill'], alpha=0.5, linewidth=2.5),
233
- path_effects.Normal()
234
- ])
235
-
236
- # Secreted Proteins (分泌蛋白)
237
  if is_secreted:
238
- for i in range(5):
239
- sx, sy = rng.uniform(0, 10), rng.uniform(5.8, 6.5)
240
- ax.scatter(sx, sy, s=50, c=c['hl_core'], edgecolors='white', zorder=10)
241
- # 运动尾迹
242
- ax.plot([sx, sx-0.5], [sy-0.1, sy-0.6], color=c['hl_core'], lw=1, alpha=0.6, zorder=9)
243
- ax.text(5, 6.2, "Secreted / Extracellular", ha='center', va='bottom', color=c['hl_core'], fontweight='bold', fontsize=10)
244
-
245
- # --- 3. 图例与标注 ---
246
- legend_labels = [
247
- ("Outer Membrane", is_om),
248
- ("Cell Wall (Peptidoglycan)", is_cw),
249
- ("Periplasm", is_peri),
250
- ("Inner Membrane", is_im),
251
- ("Cytoplasm & Nucleoid", is_cyto)
 
 
 
 
 
252
  ]
253
 
254
- ly = 5.5
255
- for text, active in legend_labels:
256
- lc = c['hl_core'] if active else c['im_out']
257
- lw = 'bold' if active else 'normal'
258
- ax.text(10.5, ly, text, ha='right', va='center', fontsize=9, color=lc, fontweight=lw)
259
  if active:
260
- ax.scatter(10.7, ly, s=30, c=c['hl_core'], zorder=10, clip_on=False)
261
- ly -= 0.5
262
-
263
- # 底部标题
264
- pred_text = f"Predicted Localization: {target_class}" if target_class else "Prokaryotic Cell Structure"
265
- ax.text(5, -0.3, pred_text, ha='center', va='top', fontsize=11, fontweight='bold', color="#334155")
266
-
267
- # 输出图片
 
 
 
 
 
268
  buf = BytesIO()
269
  plt.savefig(buf, format='png', bbox_inches='tight', transparent=True, dpi=150)
270
  buf.seek(0)
@@ -273,204 +251,135 @@ def draw_prokaryotic_cell_detailed(target_class):
273
  return img
274
 
275
  # ==========================
276
- # 4. 预测主逻辑
277
  # ==========================
278
  def predict(sequence_input):
279
  if not sequence_input or sequence_input.isspace():
280
  raise gr.Error("Please input a protein sequence.")
281
-
282
- # 清洗 FASTA 头和非氨基酸字符
283
  seq = "".join(sequence_input.split('\n')[1:]) if sequence_input.startswith('>') else sequence_input
284
- seq = re.sub(r'[^A-Z]', '', seq.upper())
285
-
286
- if not seq:
287
- raise gr.Error("Invalid Amino Acid Sequence. Please check your input.")
288
-
289
- # 截断过长序列以防止 OOM
290
- if len(seq) > 1024:
291
- seq = seq[:1024]
292
 
293
  with torch.no_grad():
294
  inputs = tokenizer(seq, return_tensors="pt", truncation=True, max_length=1024).to(DEVICE)
295
  outputs = plm_model(**inputs)
296
-
297
- # 提取特征
298
- hidden_states = outputs.last_hidden_state
299
- cls_embedding = hidden_states[:, 0, :]
300
- token_embeddings = hidden_states[:, 1:-1, :]
301
- token_mask = inputs['attention_mask'][:, 1:-1]
302
-
303
- # 前向传播
304
- logits = classifier(cls_embedding, token_embeddings, token_mask)
305
  probs = F.softmax(logits, dim=1)[0]
306
 
307
- # 结果处理
308
  top_prob, top_idx = torch.max(probs, dim=0)
309
  top_label = idx_to_label[top_idx.item()]
310
-
311
  confidences = {idx_to_label[i]: float(p) for i, p in enumerate(probs)}
312
 
313
- # 生成可视化图
314
- cell_diagram = draw_prokaryotic_cell_detailed(top_label)
315
-
316
  return confidences, cell_diagram
317
 
318
  # ==========================
319
- # 5. UI 界面构建 (Modern Scientific Theme)
320
  # ==========================
321
- flagship_css = """
322
- @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;700&display=swap');
 
 
323
 
324
- body { font-family: 'IBM Plex Sans', sans-serif !important; background-color: #f8fafc; }
325
-
326
- /* 顶部 Header 样式 */
327
  .header-box {
328
- background: linear-gradient(145deg, #1e293b, #334155);
329
- color: white;
330
- padding: 2.5rem;
331
- border-radius: 16px;
332
  margin-bottom: 2rem;
333
- box-shadow: 0 10px 25px -5px rgba(30, 41, 59, 0.3);
334
- border: 1px solid rgba(255,255,255,0.1);
335
  }
336
- .header-title { font-size: 2.4rem; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 0.5rem; }
337
- .header-subtitle { opacity: 0.85; margin-bottom: 15px; font-size: 1.1rem; font-weight: 300; }
338
 
339
- /* 徽章样式 */
340
- .badge-container { display: flex; gap: 10px; flex-wrap: wrap; }
341
  .badge {
342
- background: rgba(255,255,255,0.1);
343
- padding: 6px 14px;
344
- border-radius: 99px;
345
- font-size: 0.85rem;
346
  font-weight: 500;
347
- border: 1px solid rgba(255,255,255,0.2);
348
- backdrop-filter: blur(4px);
349
- }
350
-
351
- /* 内容卡片样式 */
352
- .content-box {
353
- background: white;
354
- padding: 2rem;
355
- border-radius: 16px;
356
  border: 1px solid #e2e8f0;
357
- box-shadow: 0 4px 6px -1px rgba(0,0,0,0.03), 0 2px 4px -1px rgba(0,0,0,0.02);
358
- height: 100%;
359
  }
360
 
361
- /* 图例说明框 */
362
- .legend-box {
363
- margin-top:15px;
364
- padding:12px;
365
- background:#f1f5f9;
366
- border-radius:8px;
367
- font-size:0.85rem;
368
- color:#475569;
369
- border-left: 4px solid #64748b;
370
  }
 
 
 
371
  """
372
 
373
- theme = gr.themes.Soft(
374
- primary_hue="slate",
375
- font=[gr.themes.GoogleFont("IBM Plex Sans"), "ui-sans-serif", "system-ui"]
 
 
 
 
 
376
  )
377
 
378
- with gr.Blocks(theme=theme, css=flagship_css, title="LocPred-Prok High-Fidelity") as app:
379
 
380
- # --- Header Area ---
381
  with gr.Column(elem_classes="header-box"):
382
  gr.HTML("""
383
  <div class="header-title">LocPred-Prok</div>
384
- <div class="header-subtitle">High-Fidelity Prokaryotic Subcellular Localization Prediction</div>
385
- <div class="badge-container">
386
- <span class="badge">🧬 ESM-2 Enhanced</span>
387
- <span class="badge">🔬 Scientific Visualization</span>
388
- <span class="badge">🎯 Gram-negative Model</span>
 
 
389
  </div>
390
  """)
391
 
392
- # --- Main Tab Area ---
393
  with gr.Tabs():
394
-
395
- # === Tab 1: Analysis ===
396
- with gr.TabItem("🚀 Analysis", id="predict"):
397
  with gr.Row():
398
-
399
- # Left Column: Input
400
- with gr.Column(scale=5, elem_classes="content-box"):
401
- gr.Markdown("### 📥 Protein Sequence Input")
402
- sequence_input = gr.Textbox(
403
- lines=12,
404
- placeholder=">Paste FASTA sequence here (e.g., >Protein ...)",
405
- show_label=False
406
- )
407
 
408
  with gr.Row():
409
- clear_btn = gr.ClearButton(sequence_input, value="Clear Input")
410
- submit_btn = gr.Button("Run Prediction & Visualization", variant="primary", scale=2)
411
 
412
- gr.Markdown("#### 💡 Load Example Data")
413
  gr.Examples(
414
  examples=[
415
- [">Outer Membrane Protein (OmpA)\nAPKNTWYTGAKLGWSQYHDTGFINNNGPTHENQLGAGAFGGYQVNPYVGFEMGYDWLGRMPYKGSVENGAYKAQGVQLTAKLGYPITDDLDIYTRLGGMVWRADTKSNVYGKNHDTGVSPVFAGGVEYAITPEIATRLEYQWTNNIGDAHTIGTRPDNGMLSLGVSYRFGQGEAAPVVAPAPAPAPEVQTKHFTLKSDVLFNFNKATLKPEGQAALDQLYSQLSNLDPKDGSVVVLGYTDRIGSDAYNQGLSERRAQSVVDYLISKGIPADKISARGMGESNPVTGNTCDNVKQRAALIDCLAPDRRVEIEVKGIKDVVTQPQA"],
416
- [">Cell Wall Binding Protein\nMKFKLTAGCLAVAGVLLASSFGADAEIVVNAIYDQVARTEDGVYTQGQLTGRRIELLNKLGIEPEDSLASTVIHEFVARVGDDHGIETIIDEFYRQHPSASL"],
417
- [">Cytoplasmic Ribosomal Protein\nARYLGPKLKLSRREGTDLFLKSGVRAIDTKCKIEQAPGQHGARKPRLSDYGVQLREKQKVRRIYGVLERQFRNYYKEAARLKGNTGENLLALLEGRLDNVVYRMGFG"]
418
  ],
419
- inputs=sequence_input,
420
- label=None
421
  )
422
 
423
- # Right Column: Output
424
- with gr.Column(scale=7, elem_classes="content-box"):
425
- gr.Markdown("### 🔬 High-Fidelity Localization Map")
426
 
427
- # 动态生成的细胞图
428
- output_image = gr.Image(
429
- label="Visualization",
430
- show_label=False,
431
- show_download_button=True,
432
- interactive=False,
433
- type="pil",
434
- height=450
435
- )
436
 
437
- gr.Markdown("### 📊 Prediction Confidence")
438
- # 概率分布条 (显示所有类别)
439
  output_label = gr.Label(num_top_classes=NUM_CLASSES, show_label=False)
440
 
441
- gr.HTML("""
442
- <div class="legend-box">
443
- <b>Legend Note:</b> This diagram illustrates a generic <b>Gram-negative bacterium</b> cross-section.
444
- The peptidoglycan cell wall is depicted by the hatched mesh layer between the inner and outer membranes.
445
- The nucleoid DNA is shown as tangled strands within the dense cytoplasm.
446
- </div>
447
- """)
448
-
449
- # === Tab 2: About ===
450
- with gr.TabItem("📖 Methodology"):
451
- gr.Markdown("""
452
- ### About LocPred-Prok
453
- LocPred-Prok is a deep learning tool designed for precise prokaryotic protein subcellular localization.
454
-
455
- **Architecture:**
456
- * **Backbone:** ESM-2 (150M Parameters)
457
- * **Classifier:** Dual-Branch Architecture (Global Semantics + Local Motifs)
458
-
459
- **Visualization:**
460
- The generated diagram dynamically renders a Gram-negative bacterial cell model using `matplotlib`, applying shading and texture effects to highlight the predicted localization site.
461
- """)
462
-
463
- # --- Event Handling ---
464
- submit_btn.click(
465
- fn=predict,
466
- inputs=sequence_input,
467
- outputs=[output_label, output_image]
468
- )
469
- clear_btn.click(
470
- lambda: [None, None],
471
- outputs=[output_label, output_image]
472
- )
473
-
474
- # 启动应用
475
- if __name__ == "__main__":
476
- app.launch()
 
5
  import gradio as gr
6
  import matplotlib.pyplot as plt
7
  import matplotlib.patches as patches
8
+ import matplotlib.path as mpath
9
  import numpy as np
10
  from io import BytesIO
11
  from PIL import Image
12
  from transformers import AutoTokenizer, AutoModel
13
 
14
  # ==========================
15
+ # 0. 环境初始化
16
  # ==========================
17
+ plt.switch_backend('Agg') # 关键:防止服务器绘图报错
 
18
 
19
  os.environ["HF_HOME"] = "/tmp/hf_cache"
20
  os.environ["TRANSFORMERS_CACHE"] = "/tmp/hf_cache"
21
  os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
22
 
 
23
  for path in ["/tmp/hf_cache", os.path.expanduser("~/.cache/huggingface")]:
24
  shutil.rmtree(path, ignore_errors=True)
25
  os.makedirs(path, exist_ok=True)
26
 
27
  # ==========================
28
+ # 1. 模型定义 (保持不变)
29
  # ==========================
30
  class AttentionPooling(nn.Module):
 
31
  def __init__(self, d_model):
32
  super().__init__()
33
  self.attention_net = nn.Linear(d_model, 1)
 
34
  def forward(self, x, mask):
35
  attn_logits = self.attention_net(x).squeeze(2)
36
  attn_logits.masked_fill_(mask == 0, -float('inf'))
 
38
  return torch.bmm(attn_weights.unsqueeze(1), x).squeeze(1)
39
 
40
  class ProtDualBranchEnhancedClassifier(nn.Module):
 
41
  def __init__(self, d_model, projection_dim, num_classes, dropout, kernel_size):
42
  super().__init__()
 
43
  self.cls_projector = nn.Linear(d_model, projection_dim)
 
 
44
  self.token_refiner = nn.Sequential(
45
+ nn.Conv1d(d_model, d_model, kernel_size, padding='same'), nn.ReLU()
 
46
  )
47
  self.attention_pooling = AttentionPooling(d_model)
48
  self.tok_projector = nn.Linear(d_model, projection_dim)
 
 
49
  fused_dim = projection_dim * 2
50
+ self.gate = nn.Sequential(nn.Linear(fused_dim, fused_dim), nn.Sigmoid())
 
 
 
 
 
51
  self.classifier_head = nn.Sequential(
52
+ nn.LayerNorm(fused_dim), nn.Linear(fused_dim, fused_dim * 2),
53
+ nn.ReLU(), nn.Dropout(dropout), nn.Linear(fused_dim * 2, num_classes)
 
 
 
54
  )
 
55
  def forward(self, cls_embedding, token_embeddings, mask):
 
56
  z_cls = self.cls_projector(cls_embedding)
 
 
57
  tok_emb_permuted = token_embeddings.permute(0, 2, 1)
58
  refined_tok_emb = self.token_refiner(tok_emb_permuted).permute(0, 2, 1)
59
  z_tok_pooled = self.attention_pooling(refined_tok_emb, mask)
60
  z_tok = self.tok_projector(z_tok_pooled)
 
 
61
  z_fused_concat = torch.cat([z_cls, z_tok], dim=1)
62
  gate_values = self.gate(z_fused_concat)
63
  z_fused_gated = z_fused_concat * gate_values
 
64
  return self.classifier_head(z_fused_gated)
65
 
66
  # ==========================
67
+ # 2. 加载模型
68
  # ==========================
69
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
70
  PLM_MODEL_NAME = "facebook/esm2_t30_150M_UR50D"
71
  CLASSIFIER_PATH = "best_model_esm2_t30_150M_UR50D.pth"
72
  LABEL_MAP_PATH = "label_map.json"
73
 
74
+ if not os.path.exists(LABEL_MAP_PATH): raise FileNotFoundError(f"Missing {LABEL_MAP_PATH}")
75
+ if not os.path.exists(CLASSIFIER_PATH): raise FileNotFoundError(f"Missing {CLASSIFIER_PATH}")
 
 
 
76
 
 
77
  with open(LABEL_MAP_PATH, 'r') as f:
78
  label_to_idx = json.load(f)
79
  idx_to_label = {v: k for k, v in label_to_idx.items()}
 
81
  NUM_CLASSES = len(idx_to_label)
82
  D_MODEL = 640
83
 
84
+ print("🔹 Loading models...")
85
  tokenizer = AutoTokenizer.from_pretrained(PLM_MODEL_NAME)
86
+ plm_model = AutoModel.from_pretrained(PLM_MODEL_NAME).to(DEVICE).eval()
87
+ classifier = ProtDualBranchEnhancedClassifier(D_MODEL, 32, NUM_CLASSES, 0.3, 3).to(DEVICE)
 
 
 
 
 
 
 
88
  classifier.load_state_dict(torch.load(CLASSIFIER_PATH, map_location=DEVICE))
89
  classifier.eval()
90
+ print("✅ Ready.")
91
 
92
  # ==========================
93
+ # 3. UniProt 风格绘图引擎 (Schematic Vector Style)
94
  # ==========================
95
+ def draw_uniprot_style_cell(target_class):
96
  """
97
+ 绘制类似 UniProt/SwissBioPics 的扁平化矢量风格细胞图。
98
+ 特点:清晰的黑色描边,柔和的填充色,双层膜结构。
99
  """
100
  target = target_class.lower() if target_class else ""
101
 
102
+ # === UniProt/SwissBioPics 配色板 ===
103
+ # 柔和、低饱和度背景,高饱和度高亮
104
  c = {
105
+ 'stroke': '#263238', # 深灰/黑色描边 (清晰感来源)
106
+ 'cyto_bg': '#FFF9C4', # 极淡的黄色 (胞质)
107
+ 'peri_bg': '#E1F5FE', # 极淡的蓝色 (周质)
108
+ 'ext_bg': '#ffffff', # 纯白背景
109
+ 'mem_line': '#546E7A', # 膜的线条颜色
110
+ 'cw_pattern': '#90A4AE', # 细胞壁颜色
111
+ 'dna': '#B0BEC5', # DNA 灰色
112
+ 'ribo': '#CFD8DC', # 核糖体 浅灰
113
+ 'active': '#D32F2F', # UniProt 经典红 (高亮)
114
+ 'active_fill': '#FFCDD2' # 红色填充
115
  }
116
 
117
+ # 状态判断
118
  is_secreted = "extracellular" in target or "secreted" in target
119
  is_om = "outer membrane" in target
120
  is_peri = "periplasm" in target
 
122
  is_im = "plasma membrane" in target or "inner membrane" in target
123
  is_cyto = "cytoplasm" in target or "cytosol" in target
124
 
125
+ # 创建画布 (宽高比 16:9)
126
+ fig, ax = plt.subplots(figsize=(8, 4.5), dpi=150)
127
+ ax.set_xlim(0, 10)
128
+ ax.set_ylim(0, 6)
129
  ax.axis('off')
130
 
131
+ # === 绘图辅助函数 ===
132
+ def draw_membrane_layer(x, y, w, h, z, is_active):
133
+ """绘制双层膜结构 (两条平行线)"""
134
+ color = c['active'] if is_active else c['stroke']
135
+ lw = 2.5 if is_active else 1.2
 
136
 
137
+ # 外层线
138
+ p1 = patches.FancyBboxPatch((x, y), w-h, h, boxstyle=f"round,pad=0,rounding_size={h/2}",
139
+ fc="none", ec=color, lw=lw, zorder=z)
140
+ ax.add_patch(p1)
141
+ # 内层线 (稍微小一圈,模拟双层膜厚度)
142
+ gap = 0.15
143
+ p2 = patches.FancyBboxPatch((x+gap, y+gap), w-h-2*gap, h-2*gap, boxstyle=f"round,pad=0,rounding_size={(h-2*gap)/2}",
144
+ fc="none", ec=color, lw=lw, zorder=z)
145
+ ax.add_patch(p2)
146
+
147
+ # === 1. 背景层 (Backgrounds) ===
148
+
149
+ # 外膜区域背景 (Periplasm starts here)
150
+ # 整个胶囊的底色设为周质颜色 (Light Blue)
151
+ base_capsule = patches.FancyBboxPatch((1, 1), 8, 4, boxstyle="round,pad=0,rounding_size=2",
152
+ fc=c['peri_bg'], ec="none", zorder=1)
153
+ ax.add_patch(base_capsule)
154
+
155
+ # 内膜区域背景 (Cytoplasm starts here)
156
+ # 内部胶囊底色设为胞质颜色 (Light Yellow)
157
+ cyto_capsule = patches.FancyBboxPatch((1.6, 1.6), 6.8, 2.8, boxstyle="round,pad=0,rounding_size=1.4",
158
+ fc=c['cyto_bg'], ec="none", zorder=2)
159
+ ax.add_patch(cyto_capsule)
160
+
161
+ # === 2. 结构层 (Structures with Strokes) ===
162
+
163
+ # A. Outer Membrane (外膜) - 双层线
164
+ draw_membrane_layer(1, 1, 8+2, 4, z=5, is_active=is_om)
165
+
166
+ # B. Cell Wall (细胞壁) - 网格纹理层
167
+ # 在周质层中间画一层
168
+ cw_color = c['active'] if is_cw else c['cw_pattern']
169
+ cw_lw = 2 if is_cw else 0.5
170
+ cw_alpha = 1.0 if is_cw else 0.4
171
+ cw_patch = patches.FancyBboxPatch((1.3, 1.3), 7.4, 3.4, boxstyle="round,pad=0,rounding_size=1.7",
172
+ fc="none", ec=cw_color, lw=cw_lw, linestyle=(0, (5, 5)), zorder=4)
173
+ ax.add_patch(cw_patch)
174
 
175
+ # 如果是周质高亮,填充浅红色
176
+ if is_peri:
177
+ peri_hl = patches.FancyBboxPatch((1.1, 1.1), 7.8, 3.8, boxstyle="round,pad=0,rounding_size=1.9",
178
+ fc=c['active_fill'], ec="none", alpha=0.5, zorder=1.5)
179
+ ax.add_patch(peri_hl)
180
+
181
+ # C. Inner Membrane (内膜) - 双层线
182
+ draw_membrane_layer(1.6, 1.6, 6.8, 2.8, z=6, is_active=is_im)
183
+
184
+ # D. Cytoplasm (胞质) - 如果高亮则填充红
185
+ if is_cyto:
186
+ cyto_hl = patches.FancyBboxPatch((1.7, 1.7), 6.6, 2.6, boxstyle="round,pad=0,rounding_size=1.3",
187
+ fc=c['active_fill'], ec="none", zorder=2.1)
188
+ ax.add_patch(cyto_hl)
189
+
190
+ # === 3. 内容细节 (Details) ===
191
+
192
+ # DNA (拟核) - 清晰的线条,不再是模糊的
193
+ t = np.linspace(0, 15, 300)
194
+ x_dna = 5 + 2.0 * np.cos(t) * np.sin(t*0.5)
195
+ y_dna = 3 + 0.6 * np.sin(t)
196
+ ax.plot(x_dna, y_dna, color=c['dna'], lw=2, zorder=3, solid_capstyle='round')
197
+
198
+ # Ribosomes (核糖体) - 规则的小圆圈,而不是随机噪点
199
+ # 使用规律的分布让画面更整洁
200
+ rng = np.random.default_rng(42)
201
+ for _ in range(30):
202
+ rx, ry = rng.uniform(2.5, 7.5), rng.uniform(2.2, 3.8)
203
+ # 避开 DNA 区域 (简单判断)
204
+ if not (4 < rx < 6 and 2.5 < ry < 3.5):
205
+ circle = patches.Circle((rx, ry), radius=0.06, fc=c['ribo'], ec="none", zorder=3)
206
+ ax.add_patch(circle)
207
+
208
+ # Secreted Proteins (胞外)
 
 
 
 
 
 
209
  if is_secreted:
210
+ # 画几个 UniProt 风格的小球+箭头
211
+ ax.text(5, 5.5, "Secreted", ha='center', va='bottom', color=c['active'], fontweight='bold', fontsize=11)
212
+ for i in range(3):
213
+ sx = 4 + i
214
+ sy = 5.2
215
+ # 箭头
216
+ ax.arrow(sx, 4.8, 0, 0.4, head_width=0.15, head_length=0.15, fc=c['active'], ec=c['active'], width=0.04, zorder=10)
217
+ # 蛋白球
218
+ circle = patches.Circle((sx, 4.7), radius=0.15, fc=c['active'], ec=c['stroke'], zorder=10)
219
+ ax.add_patch(circle)
220
+
221
+ # === 4. 标注 (Annotations) ===
222
+ # 使用简单的线条指引
223
+ labels = [
224
+ ("Outer Membrane", 1, 5, is_om),
225
+ ("Periplasm", 1, 4.2, is_peri),
226
+ ("Cell Wall", 1, 3.6, is_cw),
227
+ ("Inner Membrane", 1, 3.0, is_im),
228
+ ("Cytoplasm", 1, 2.0, is_cyto)
229
  ]
230
 
231
+ for txt, x_anchor, y_anchor, active in labels:
 
 
 
 
232
  if active:
233
+ # 激活状态:红色加粗字体 + 指示线
234
+ ax.text(0.2, y_anchor, txt, ha='left', va='center', color=c['active'], fontweight='bold', fontsize=10)
235
+ # 画一条线连到细胞对应位置
236
+ con_x = 1.0 # 胶囊左边缘附近
237
+ ax.plot([0.8, con_x], [y_anchor, 3], color=c['active'], lw=1, ls='dotted', alpha=0.5)
238
+ else:
239
+ # 非激活状态:灰色小字
240
+ ax.text(0.2, y_anchor, txt, ha='left', va='center', color='#90A4AE', fontsize=8)
241
+
242
+ # 标题
243
+ ax.text(5, 0.2, f"Localization: {target_class}", ha='center', va='bottom', fontsize=11, fontweight='bold', color=c['stroke'])
244
+
245
+ # 保存
246
  buf = BytesIO()
247
  plt.savefig(buf, format='png', bbox_inches='tight', transparent=True, dpi=150)
248
  buf.seek(0)
 
251
  return img
252
 
253
  # ==========================
254
+ # 4. 预测逻辑
255
  # ==========================
256
  def predict(sequence_input):
257
  if not sequence_input or sequence_input.isspace():
258
  raise gr.Error("Please input a protein sequence.")
 
 
259
  seq = "".join(sequence_input.split('\n')[1:]) if sequence_input.startswith('>') else sequence_input
260
+ seq = re.sub(r'[^A-Z]', '', seq.upper())[:1024]
261
+ if not seq: raise gr.Error("Invalid Amino Acid Sequence.")
 
 
 
 
 
 
262
 
263
  with torch.no_grad():
264
  inputs = tokenizer(seq, return_tensors="pt", truncation=True, max_length=1024).to(DEVICE)
265
  outputs = plm_model(**inputs)
266
+ logits = classifier(outputs.last_hidden_state[:, 0, :], outputs.last_hidden_state[:, 1:-1, :], inputs['attention_mask'][:, 1:-1])
 
 
 
 
 
 
 
 
267
  probs = F.softmax(logits, dim=1)[0]
268
 
 
269
  top_prob, top_idx = torch.max(probs, dim=0)
270
  top_label = idx_to_label[top_idx.item()]
 
271
  confidences = {idx_to_label[i]: float(p) for i, p in enumerate(probs)}
272
 
273
+ cell_diagram = draw_uniprot_style_cell(top_label)
 
 
274
  return confidences, cell_diagram
275
 
276
  # ==========================
277
+ # 5. UI 界面 (Paper-White Style)
278
  # ==========================
279
+ # 极简学术风 CSS:白底、黑字、细线
280
+ paper_css = """
281
+ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap');
282
+ body { font-family: 'Roboto', sans-serif !important; background-color: #ffffff; color: #1a1a1a; }
283
 
284
+ /* 标题区:纯白背景,底部细线 */
 
 
285
  .header-box {
286
+ background: #ffffff;
287
+ padding: 2rem 0;
288
+ border-bottom: 1px solid #e5e7eb;
 
289
  margin-bottom: 2rem;
290
+ text-align: left;
 
291
  }
292
+ .header-title { font-size: 2.5rem; font-weight: 700; color: #000000; letter-spacing: -1px; line-height: 1.1; }
293
+ .header-subtitle { font-size: 1.2rem; color: #52525b; font-weight: 300; margin-top: 10px; }
294
 
295
+ /* 标签徽章:简约线条风 */
 
296
  .badge {
297
+ display: inline-block;
298
+ padding: 4px 10px;
299
+ font-size: 0.8rem;
 
300
  font-weight: 500;
301
+ color: #0f172a;
302
+ background: #f1f5f9;
303
+ border-radius: 4px;
 
 
 
 
 
 
304
  border: 1px solid #e2e8f0;
305
+ margin-right: 8px;
 
306
  }
307
 
308
+ /* 内容卡片:无阴影,仅边框,类似论文中的 Figure */
309
+ .content-box {
310
+ background: #ffffff;
311
+ border: 1px solid #e5e7eb;
312
+ border-radius: 8px;
313
+ padding: 1.5rem;
 
 
 
314
  }
315
+
316
+ /* 按钮:学术蓝 */
317
+ button.primary { background: #2563eb !important; color: white !important; font-weight: 500; }
318
  """
319
 
320
+ theme = gr.themes.Base(
321
+ primary_hue="blue",
322
+ font=[gr.themes.GoogleFont("Roboto"), "ui-sans-serif", "system-ui"]
323
+ ).set(
324
+ body_background_fill="#ffffff",
325
+ block_background_fill="#ffffff",
326
+ block_border_width="1px",
327
+ block_label_background_fill="#ffffff"
328
  )
329
 
330
+ with gr.Blocks(theme=theme, css=paper_css, title="LocPred-Prok") as app:
331
 
332
+ # --- Header (Paper Style) ---
333
  with gr.Column(elem_classes="header-box"):
334
  gr.HTML("""
335
  <div class="header-title">LocPred-Prok</div>
336
+ <div class="header-subtitle">
337
+ Accurate prokaryotic subcellular localization using dual-branch protein language models
338
+ </div>
339
+ <div style="margin-top: 15px;">
340
+ <span class="badge">Article</span>
341
+ <span class="badge">ESM-2 Enhanced</span>
342
+ <span class="badge">Gram-negative</span>
343
  </div>
344
  """)
345
 
 
346
  with gr.Tabs():
347
+ with gr.TabItem("Prediction Interface"):
 
 
348
  with gr.Row():
349
+ # Input
350
+ with gr.Column(scale=4, elem_classes="content-box"):
351
+ gr.Markdown("### 1. Sequence Input")
352
+ gr.Markdown("<small style='color:gray'>Enter protein sequence in FASTA format.</small>")
353
+ sequence_input = gr.Textbox(lines=10, show_label=False, placeholder=">Sequence...")
 
 
 
 
354
 
355
  with gr.Row():
356
+ clear_btn = gr.ClearButton(sequence_input, value="Clear")
357
+ submit_btn = gr.Button("Analyze", variant="primary")
358
 
359
+ gr.Markdown("#### Example Data")
360
  gr.Examples(
361
  examples=[
362
+ [">Outer Membrane Protein A (OmpA)\nAPKNTWYTGAKLGWSQYHDTGFINNNGPTHENQLGAGAFGGYQVNPYVGFEMGYDWLGRMPYKGSVENGAYKAQGVQLTAKLGYPITDDLDIYTRLGGMVWRADTKSNVYGKNHDTGVSPVFAGGVEYAITPEIATRLEYQWTNNIGDAHTIGTRPDNGMLSLGVSYRFGQGEAAPVVAPAPAPAPEVQTKHFTLKSDVLFNFNKATLKPEGQAALDQLYSQLSNLDPKDGSVVVLGYTDRIGSDAYNQGLSERRAQSVVDYLISKGIPADKISARGMGESNPVTGNTCDNVKQRAALIDCLAPDRRVEIEVKGIKDVVTQPQA"],
363
+ [">Cytoplasmic Protein (Ribo)\nARYLGPKLKLSRREGTDLFLKSGVRAIDTKCKIEQAPGQHGARKPRLSDYGVQLREKQKVRRIYGVLERQFRNYYKEAARLKGNTGENLLALLEGRLDNVVYRMGFG"]
 
364
  ],
365
+ inputs=sequence_input, label=None
 
366
  )
367
 
368
+ # Output
369
+ with gr.Column(scale=6, elem_classes="content-box"):
370
+ gr.Markdown("### 2. Localization Analysis")
371
 
372
+ # 矢量风格图
373
+ output_image = gr.Image(label="Schematic Visualization", show_label=False, show_download_button=True, interactive=False, type="pil", height=350)
 
 
 
 
 
 
 
374
 
375
+ # 概率分布
376
+ gr.Markdown("#### Probability Scores")
377
  output_label = gr.Label(num_top_classes=NUM_CLASSES, show_label=False)
378
 
379
+ with gr.TabItem("About & Citation"):
380
+ gr.Markdown("### Method Description\nLocPred-Prok utilizes...")
381
+
382
+ submit_btn.click(fn=predict, inputs=sequence_input, outputs=[output_label, output_image])
383
+ clear_btn.click(lambda: [None, None], outputs=[output_label, output_image])
384
+
385
+ app.launch()