Download predict.py from viro-bert/Viro-BERT: direct link, hf CLI and curl.
- Browser
- Download file 6.3 kB
-
https://huggingface.co/viro-bert/Viro-BERT/resolve/main/predict.py
- Command line
-
hf download hf://viro-bert/Viro-BERT/predict.py
-
curl -L -o predict.py https://huggingface.co/viro-bert/Viro-BERT/resolve/main/predict.py
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() |