Viro-BERT / predict.py
LatentSpaceResearch's picture
Upload predict.py
62ca7c7 verified
Raw History Blame Contribute Delete
6.3 kB
#!/usr/bin/env python3
"""
Viro-BERT: Standalone CLI Inference Script for Viral Pathogen Classification
Compatible with Python 3.8+, PyTorch, and R (via reticulate or system calls).
"""
import os
import sys
import json
import argparse
import torch
import torch.nn as nn
from Bio import SeqIO
from transformers import DistilBertConfig, DistilBertModel
class BiologicalDistilBERT(nn.Module):
def __init__(self, vocab_size, num_classes):
super(BiologicalDistilBERT, self).__init__()
config = DistilBertConfig(
vocab_size=vocab_size,
max_position_embeddings=512,
n_layers=6,
n_heads=12,
dim=768,
dropout=0.1,
output_hidden_states=False
)
self.bert = DistilBertModel(config)
self.classifier = nn.Sequential(
nn.Linear(768, 768),
nn.ReLU(),
nn.Dropout(0.1),
nn.Linear(768, num_classes)
)
def forward(self, input_ids):
attention_mask = (input_ids != 0).long()
output = self.bert(input_ids, attention_mask=attention_mask)[0]
cls_output = output[:, 0, :]
return self.classifier(cls_output)
def tokenize_sequence(seq, vocab, k=3, stride=3):
tokens = [vocab.get("[CLS]", 2)]
for i in range(0, len(seq) - k + 1, stride):
kmer = seq[i : i + k]
tokens.append(vocab.get(kmer, vocab.get("[UNK]", 1)))
tokens.append(vocab.get("[SEP]", 3))
return tokens
def create_chunks(tokens, chunk_len=512, overlap=50):
stride = chunk_len - overlap
chunks = []
num_tokens = len(tokens)
if num_tokens <= chunk_len:
padded = tokens + [0] * (chunk_len - num_tokens)
chunks.append(padded)
else:
for i in range(0, num_tokens, stride):
chunk = tokens[i : i + chunk_len]
if len(chunk) > 10:
if len(chunk) < chunk_len:
chunk = chunk + [0] * (chunk_len - len(chunk))
chunks.append(chunk)
return chunks
def classify_fasta(fasta_path, weights_path, config_path, device=None, top_k=3):
if device is None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Load configuration
if not os.path.exists(config_path):
raise FileNotFoundError(f"Configuration file not found: {config_path}")
with open(config_path, "r", encoding="utf-8") as f:
config_data = json.load(f)
vocab = config_data["vocab"]
id_to_label = {int(k): v for k, v in config_data["id_to_label"].items()}
num_classes = config_data["num_classes"]
# Initialize and load model
model = BiologicalDistilBERT(vocab_size=len(vocab), num_classes=num_classes)
if not os.path.exists(weights_path):
raise FileNotFoundError(f"Weights file not found: {weights_path}")
state_dict = torch.load(weights_path, map_location=device)
# Clean keys if saved with torch.compile prefix
cleaned_state_dict = {k.replace("_orig_mod.", ""): v for k, v in state_dict.items()}
model.load_state_dict(cleaned_state_dict)
model.to(device)
model.eval()
# Parse FASTA sequences
records = list(SeqIO.parse(fasta_path, "fasta"))
if not records:
raise ValueError(f"No valid FASTA records found in: {fasta_path}")
print("=" * 65)
print(f"Viro-BERT Classification | Device: {device} | Sequences: {len(records)}")
print("=" * 65)
for record in records:
seq = str(record.seq).upper()
tokens = tokenize_sequence(seq, vocab, k=config_data.get("k_mer", 3), stride=config_data.get("stride", 3))
chunks = create_chunks(tokens, chunk_len=config_data.get("chunk_len", 512), overlap=config_data.get("overlap", 50))
if not chunks:
print(f"[!] Sequence {record.id} too short to extract tokens. Skipping.")
continue
input_tensor = torch.tensor(chunks, dtype=torch.long).to(device)
use_amp = (device.type == "cuda")
with torch.no_grad():
if use_amp:
with torch.autocast(device_type="cuda", dtype=torch.float16):
logits = model(input_tensor)
else:
logits = model(input_tensor)
probs = torch.softmax(logits, dim=1)
# Late Fusion Consensus (Mean Pooling over chunks)
consensus_probs = torch.mean(probs, dim=0).cpu()
top_probs, top_indices = torch.topk(consensus_probs, k=min(top_k, num_classes))
winner_idx = top_indices[0].item()
winner_label = id_to_label[winner_idx]
winner_conf = top_probs[0].item() * 100
print(f"\nSequence ID : {record.id}")
print(f"Length (bp) : {len(seq)} bp ({len(chunks)} chunks processed)")
print(f"Prediction : {winner_label} ({winner_conf:.2f}% confidence)")
print("Top Candidates:")
for rank, (p, idx) in enumerate(zip(top_probs, top_indices), start=1):
print(f" {rank}. {id_to_label[idx.item()]:<10} : {p.item()*100:.2f}%")
print("-" * 65)
def main():
parser = argparse.ArgumentParser(
description="Viro-BERT: Whole-Genome Attention for Viral Pathogen Classification",
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("--fasta", type=str, required=True, help="Path to input FASTA file containing viral sequences")
parser.add_argument("--weights", type=str, default="sota_chunk_model.pth", help="Path to model weights (.pth)")
parser.add_argument("--config", type=str, default="config.json", help="Path to config.json containing vocab & label maps")
parser.add_argument("--top_k", type=int, default=3, help="Number of top candidates to report")
parser.add_argument("--cpu", action="store_true", help="Force CPU inference even if CUDA is available")
args = parser.parse_args()
device = torch.device("cpu") if args.cpu else torch.device("cuda" if torch.cuda.is_available() else "cpu")
classify_fasta(args.fasta, args.weights, args.config, device=device, top_k=args.top_k)
if __name__ == "__main__":
main()