LatentSpaceResearch commited on
Commit
62ca7c7
·
verified ·
1 Parent(s): e1eae9e

Upload predict.py

Browse files
Files changed (1) hide show
  1. predict.py +162 -0
predict.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Viro-BERT: Standalone CLI Inference Script for Viral Pathogen Classification
4
+ Compatible with Python 3.8+, PyTorch, and R (via reticulate or system calls).
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import argparse
11
+ import torch
12
+ import torch.nn as nn
13
+ from Bio import SeqIO
14
+ from transformers import DistilBertConfig, DistilBertModel
15
+
16
+
17
+ class BiologicalDistilBERT(nn.Module):
18
+ def __init__(self, vocab_size, num_classes):
19
+ super(BiologicalDistilBERT, self).__init__()
20
+ config = DistilBertConfig(
21
+ vocab_size=vocab_size,
22
+ max_position_embeddings=512,
23
+ n_layers=6,
24
+ n_heads=12,
25
+ dim=768,
26
+ dropout=0.1,
27
+ output_hidden_states=False
28
+ )
29
+ self.bert = DistilBertModel(config)
30
+ self.classifier = nn.Sequential(
31
+ nn.Linear(768, 768),
32
+ nn.ReLU(),
33
+ nn.Dropout(0.1),
34
+ nn.Linear(768, num_classes)
35
+ )
36
+
37
+ def forward(self, input_ids):
38
+ attention_mask = (input_ids != 0).long()
39
+ output = self.bert(input_ids, attention_mask=attention_mask)[0]
40
+ cls_output = output[:, 0, :]
41
+ return self.classifier(cls_output)
42
+
43
+
44
+ def tokenize_sequence(seq, vocab, k=3, stride=3):
45
+ tokens = [vocab.get("[CLS]", 2)]
46
+ for i in range(0, len(seq) - k + 1, stride):
47
+ kmer = seq[i : i + k]
48
+ tokens.append(vocab.get(kmer, vocab.get("[UNK]", 1)))
49
+ tokens.append(vocab.get("[SEP]", 3))
50
+ return tokens
51
+
52
+
53
+ def create_chunks(tokens, chunk_len=512, overlap=50):
54
+ stride = chunk_len - overlap
55
+ chunks = []
56
+ num_tokens = len(tokens)
57
+
58
+ if num_tokens <= chunk_len:
59
+ padded = tokens + [0] * (chunk_len - num_tokens)
60
+ chunks.append(padded)
61
+ else:
62
+ for i in range(0, num_tokens, stride):
63
+ chunk = tokens[i : i + chunk_len]
64
+ if len(chunk) > 10:
65
+ if len(chunk) < chunk_len:
66
+ chunk = chunk + [0] * (chunk_len - len(chunk))
67
+ chunks.append(chunk)
68
+
69
+ return chunks
70
+
71
+
72
+ def classify_fasta(fasta_path, weights_path, config_path, device=None, top_k=3):
73
+ if device is None:
74
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
75
+
76
+ # Load configuration
77
+ if not os.path.exists(config_path):
78
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
79
+ with open(config_path, "r", encoding="utf-8") as f:
80
+ config_data = json.load(f)
81
+
82
+ vocab = config_data["vocab"]
83
+ id_to_label = {int(k): v for k, v in config_data["id_to_label"].items()}
84
+ num_classes = config_data["num_classes"]
85
+
86
+ # Initialize and load model
87
+ model = BiologicalDistilBERT(vocab_size=len(vocab), num_classes=num_classes)
88
+ if not os.path.exists(weights_path):
89
+ raise FileNotFoundError(f"Weights file not found: {weights_path}")
90
+
91
+ state_dict = torch.load(weights_path, map_location=device)
92
+ # Clean keys if saved with torch.compile prefix
93
+ cleaned_state_dict = {k.replace("_orig_mod.", ""): v for k, v in state_dict.items()}
94
+ model.load_state_dict(cleaned_state_dict)
95
+ model.to(device)
96
+ model.eval()
97
+
98
+ # Parse FASTA sequences
99
+ records = list(SeqIO.parse(fasta_path, "fasta"))
100
+ if not records:
101
+ raise ValueError(f"No valid FASTA records found in: {fasta_path}")
102
+
103
+ print("=" * 65)
104
+ print(f"Viro-BERT Classification | Device: {device} | Sequences: {len(records)}")
105
+ print("=" * 65)
106
+
107
+ for record in records:
108
+ seq = str(record.seq).upper()
109
+ tokens = tokenize_sequence(seq, vocab, k=config_data.get("k_mer", 3), stride=config_data.get("stride", 3))
110
+ chunks = create_chunks(tokens, chunk_len=config_data.get("chunk_len", 512), overlap=config_data.get("overlap", 50))
111
+
112
+ if not chunks:
113
+ print(f"[!] Sequence {record.id} too short to extract tokens. Skipping.")
114
+ continue
115
+
116
+ input_tensor = torch.tensor(chunks, dtype=torch.long).to(device)
117
+
118
+ use_amp = (device.type == "cuda")
119
+ with torch.no_grad():
120
+ if use_amp:
121
+ with torch.autocast(device_type="cuda", dtype=torch.float16):
122
+ logits = model(input_tensor)
123
+ else:
124
+ logits = model(input_tensor)
125
+
126
+ probs = torch.softmax(logits, dim=1)
127
+ # Late Fusion Consensus (Mean Pooling over chunks)
128
+ consensus_probs = torch.mean(probs, dim=0).cpu()
129
+
130
+ top_probs, top_indices = torch.topk(consensus_probs, k=min(top_k, num_classes))
131
+ winner_idx = top_indices[0].item()
132
+ winner_label = id_to_label[winner_idx]
133
+ winner_conf = top_probs[0].item() * 100
134
+
135
+ print(f"\nSequence ID : {record.id}")
136
+ print(f"Length (bp) : {len(seq)} bp ({len(chunks)} chunks processed)")
137
+ print(f"Prediction : {winner_label} ({winner_conf:.2f}% confidence)")
138
+ print("Top Candidates:")
139
+ for rank, (p, idx) in enumerate(zip(top_probs, top_indices), start=1):
140
+ print(f" {rank}. {id_to_label[idx.item()]:<10} : {p.item()*100:.2f}%")
141
+ print("-" * 65)
142
+
143
+
144
+ def main():
145
+ parser = argparse.ArgumentParser(
146
+ description="Viro-BERT: Whole-Genome Attention for Viral Pathogen Classification",
147
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
148
+ )
149
+ parser.add_argument("--fasta", type=str, required=True, help="Path to input FASTA file containing viral sequences")
150
+ parser.add_argument("--weights", type=str, default="sota_chunk_model.pth", help="Path to model weights (.pth)")
151
+ parser.add_argument("--config", type=str, default="config.json", help="Path to config.json containing vocab & label maps")
152
+ parser.add_argument("--top_k", type=int, default=3, help="Number of top candidates to report")
153
+ parser.add_argument("--cpu", action="store_true", help="Force CPU inference even if CUDA is available")
154
+
155
+ args = parser.parse_args()
156
+
157
+ device = torch.device("cpu") if args.cpu else torch.device("cuda" if torch.cuda.is_available() else "cpu")
158
+ classify_fasta(args.fasta, args.weights, args.config, device=device, top_k=args.top_k)
159
+
160
+
161
+ if __name__ == "__main__":
162
+ main()