pepe-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,647 @@
1
+ import logging
2
+ import torch
3
+ import torch.nn as nn
4
+ import os
5
+ import json
6
+ import sys
7
+ import pepe.utils
8
+ from pepe.embedders.base_embedder import BaseEmbedder
9
+ from transformers import AutoTokenizer
10
+
11
+ logger = logging.getLogger("src.embedders.custom_embedder")
12
+
13
+
14
+ class CustomEmbedder(BaseEmbedder):
15
+ """
16
+ CustomEmbedder for loading custom PyTorch models from .pt files.
17
+
18
+ This embedder allows loading custom trained models saved as .pt files.
19
+ It expects the model to have a specific structure and provides flexibility
20
+ for different model architectures.
21
+ """
22
+
23
+ def __init__(self, args):
24
+ super().__init__(args)
25
+
26
+ # For custom models, we expect the model_name to be a path to a .pt file
27
+ # or a directory containing model files
28
+ model_path = args.model_name
29
+
30
+ # Handle custom: prefix
31
+ if model_path.startswith("custom:"):
32
+ model_path = model_path[7:] # Remove 'custom:' prefix
33
+
34
+ if not os.path.exists(model_path):
35
+ raise FileNotFoundError(f"Custom model path not found: {model_path}")
36
+
37
+ # Store the actual model path
38
+ self.model_path = model_path
39
+ self.tokenizer_path = args.tokenizer_from
40
+
41
+ # Load sequences from FASTA file
42
+ self.sequences = pepe.utils.fasta_to_dict(args.fasta_path)
43
+ self.num_sequences = len(self.sequences)
44
+
45
+ # Initialize model and get model parameters
46
+ (
47
+ self.model,
48
+ self.tokenizer,
49
+ self.num_heads,
50
+ self.num_layers,
51
+ self.embedding_size,
52
+ ) = self._initialize_model(self.model_path, self.tokenizer_path)
53
+
54
+ # Set up tokenizer and validate tokens
55
+ self.valid_tokens = self._get_valid_tokens()
56
+ pepe.utils.check_input_tokens(
57
+ self.valid_tokens, self.sequences, self.model_name
58
+ )
59
+
60
+ # Set up special tokens
61
+ self.special_tokens = self._get_special_tokens()
62
+
63
+ # Process layers
64
+ self.layers = self._load_layers(self.layers)
65
+
66
+ # Load and tokenize data
67
+ self.data_loader, self.max_length = self._load_data(
68
+ self.sequences, self.substring_dict
69
+ )
70
+
71
+ # Initialize output objects
72
+ self._set_output_objects()
73
+
74
+ logger.info(
75
+ f"Custom embedder initialized with {self.num_layers} layers, "
76
+ f"{self.num_heads} heads, embedding size {self.embedding_size}"
77
+ )
78
+
79
+ def _initialize_model(self, model_path, tokenizer_path=None):
80
+ """Initialize the custom model from .pt file or directory."""
81
+ logger.info(f"Loading custom model from: {model_path}")
82
+
83
+ # Check if model_path is a directory or a file
84
+ if os.path.isdir(model_path):
85
+ # Directory: look for model files
86
+ config_path = os.path.join(model_path, "config.json")
87
+ model_file_path = os.path.join(model_path, "pytorch_model.pt")
88
+ if not tokenizer_path:
89
+ tokenizer_path = model_path
90
+
91
+ # Alternative model file names
92
+ if not os.path.exists(model_file_path):
93
+ model_file_path = os.path.join(model_path, "model.pt")
94
+ if not os.path.exists(model_file_path):
95
+ model_file_path = os.path.join(model_path, "model.pth")
96
+
97
+ if not os.path.exists(model_file_path):
98
+ raise FileNotFoundError(
99
+ f"No model file found in {model_path}. "
100
+ f"Expected: pytorch_model.pt, model.pt, or model.pth"
101
+ )
102
+ else:
103
+ # Single file: assume it's the model file
104
+ model_file_path = model_path
105
+ config_path = os.path.join(os.path.dirname(model_path), "config.json")
106
+ if not tokenizer_path:
107
+ tokenizer_path = os.path.dirname(model_path)
108
+
109
+ # Load model
110
+ model_data = torch.load(model_file_path, map_location="cpu")
111
+
112
+ # Handle different model saving formats
113
+ if isinstance(model_data, dict):
114
+ if "model" in model_data:
115
+ # Model saved with additional metadata
116
+ model_state_dict = model_data["model"]
117
+ config = model_data.get("config", {})
118
+ elif "state_dict" in model_data:
119
+ # Model saved with state_dict key
120
+ model_state_dict = model_data["state_dict"]
121
+ config = model_data.get("config", {})
122
+ else:
123
+ # Assume it's a state dict directly
124
+ model_state_dict = model_data
125
+ config = {}
126
+ else:
127
+ # Model saved directly
128
+ model_state_dict = (
129
+ model_data.state_dict() if hasattr(model_data, "state_dict") else None
130
+ )
131
+ config = {}
132
+
133
+ # Load config if available
134
+ if os.path.exists(config_path):
135
+ with open(config_path, "r") as f:
136
+ file_config = json.load(f)
137
+ config.update(file_config)
138
+
139
+ # Extract model parameters from config or infer from state_dict
140
+ num_layers = config.get("num_layers", self._infer_num_layers(model_state_dict))
141
+ num_heads = config.get(
142
+ "num_attention_heads", self._infer_num_heads(model_state_dict)
143
+ )
144
+ embedding_size = config.get(
145
+ "hidden_size", self._infer_embedding_size(model_state_dict)
146
+ )
147
+
148
+ # Initialize model architecture
149
+ if isinstance(model_data, torch.nn.Module):
150
+ model = model_data
151
+ else:
152
+ # Create a generic model wrapper
153
+ model = CustomModelWrapper(model_state_dict, config)
154
+
155
+ # Set model to evaluation mode
156
+ model.eval()
157
+
158
+ # Move model to appropriate device
159
+ if torch.cuda.is_available() and self.device.type == "cuda":
160
+ model = model.cuda()
161
+ logger.info("Transferred custom model to GPU")
162
+ else:
163
+ logger.info("Running custom model on CPU")
164
+
165
+ # Initialize tokenizer
166
+ tokenizer = self._initialize_tokenizer(tokenizer_path, config)
167
+
168
+ return model, tokenizer, num_heads, num_layers, embedding_size
169
+
170
+ def _initialize_tokenizer(self, tokenizer_path, config):
171
+ """Initialize tokenizer for custom model."""
172
+ try:
173
+ tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
174
+ logger.info(f"Loaded tokenizer from {tokenizer_path}")
175
+ return tokenizer
176
+ except Exception as e:
177
+ logger.warning(f"Could not load tokenizer from directory: {e}")
178
+
179
+ # Use a default protein tokenizer if no custom tokenizer found
180
+ logger.info("Using default protein tokenizer")
181
+ return DefaultProteinTokenizer()
182
+
183
+ def _infer_num_layers(self, state_dict):
184
+ """Infer number of layers from state dict."""
185
+ assert state_dict is not None, "State dict cannot be None for layer inference"
186
+
187
+ layer_keys = [k for k in state_dict.keys() if "layer" in k.lower()]
188
+ # Extract layer numbers and find the maximum
189
+ layer_numbers = []
190
+ for key in layer_keys:
191
+ parts = key.split(".")
192
+ for part in parts:
193
+ if part.isdigit():
194
+ layer_numbers.append(int(part))
195
+ return max(layer_numbers) + 1 if layer_numbers else 12
196
+
197
+ def _infer_num_heads(self, state_dict):
198
+ """Infer number of attention heads from state dict."""
199
+ assert state_dict is not None, "State dict cannot be None for head inference"
200
+
201
+ # Look for attention weight matrices
202
+ attn_keys = [
203
+ k
204
+ for k in state_dict.keys()
205
+ if "attention" in k.lower() and "weight" in k.lower()
206
+ ]
207
+ for key in attn_keys:
208
+ tensor = state_dict[key]
209
+ if len(tensor.shape) >= 2:
210
+ # Assume the second dimension might be related to heads
211
+ return tensor.shape[1] // 64 if tensor.shape[1] % 64 == 0 else 12
212
+
213
+ def _infer_embedding_size(self, state_dict):
214
+ """Infer embedding size from state dict."""
215
+ assert (
216
+ state_dict is not None
217
+ ), "State dict cannot be None for embedding size inference"
218
+
219
+ # Look for embedding layers
220
+ emb_keys = [
221
+ k
222
+ for k in state_dict.keys()
223
+ if "embed" in k.lower() and "weight" in k.lower()
224
+ ]
225
+ if emb_keys:
226
+ # Get embedding dimension
227
+ tensor = state_dict[emb_keys[0]]
228
+ return tensor.shape[-1] if len(tensor.shape) >= 2 else 768
229
+
230
+ # Look for output layers
231
+ output_keys = [
232
+ k
233
+ for k in state_dict.keys()
234
+ if "output" in k.lower() and "weight" in k.lower()
235
+ ]
236
+ if output_keys:
237
+ tensor = state_dict[output_keys[0]]
238
+ return tensor.shape[-1] if len(tensor.shape) >= 2 else 768
239
+
240
+ return 768
241
+
242
+ def _get_valid_tokens(self):
243
+ """Get valid tokens for the tokenizer."""
244
+ if hasattr(self.tokenizer, "get_vocab"):
245
+ return set(self.tokenizer.get_vocab().keys())
246
+ elif hasattr(self.tokenizer, "vocab"):
247
+ return set(self.tokenizer.vocab.keys())
248
+ else:
249
+ # Return standard amino acid tokens
250
+ return set(
251
+ [
252
+ "A",
253
+ "R",
254
+ "N",
255
+ "D",
256
+ "C",
257
+ "Q",
258
+ "E",
259
+ "G",
260
+ "H",
261
+ "I",
262
+ "L",
263
+ "K",
264
+ "M",
265
+ "F",
266
+ "P",
267
+ "S",
268
+ "T",
269
+ "W",
270
+ "Y",
271
+ "V",
272
+ ]
273
+ )
274
+
275
+ def _get_special_tokens(self):
276
+ """Get special token IDs."""
277
+ if hasattr(self.tokenizer, "all_special_ids"):
278
+ return torch.tensor(
279
+ self.tokenizer.all_special_ids, device=self.device, dtype=torch.int8
280
+ )
281
+ else:
282
+ # Default special tokens (pad, cls, sep, unk)
283
+ return torch.tensor([0, 1, 2, 3], device=self.device, dtype=torch.int8)
284
+
285
+ def _load_layers(self, layers):
286
+ """Process layer specification."""
287
+ if not layers:
288
+ layers = list(range(1, self.num_layers + 1))
289
+ return layers
290
+
291
+ # Validate layer indices
292
+ assert all(
293
+ -(self.num_layers + 1) <= i <= self.num_layers for i in layers
294
+ ), f"Layer indices must be in range [{-(self.num_layers + 1)}, {self.num_layers}]"
295
+
296
+ # Convert negative indices to positive
297
+ layers = [(i + self.num_layers + 1) % (self.num_layers + 1) for i in layers]
298
+ return layers
299
+
300
+ def _load_data(self, sequences, substring_dict=None):
301
+ """Load and tokenize sequences."""
302
+ # Create dataset
303
+ dataset = CustomDataset(
304
+ sequences,
305
+ substring_dict,
306
+ self.context,
307
+ self.tokenizer,
308
+ self.max_length,
309
+ add_special_tokens=not self.disable_special_tokens,
310
+ )
311
+
312
+ logger.info("Generating batches...")
313
+ batch_sampler = pepe.utils.TokenBudgetBatchSampler(
314
+ dataset=dataset, token_budget=self.batch_size
315
+ )
316
+
317
+ data_loader = torch.utils.data.DataLoader(
318
+ dataset, batch_sampler=batch_sampler, collate_fn=dataset.safe_collate
319
+ )
320
+
321
+ max_length = dataset.get_max_encoded_length()
322
+ logger.info("Data loaded and tokenized")
323
+
324
+ return data_loader, max_length
325
+
326
+ def _compute_outputs(
327
+ self,
328
+ model,
329
+ toks,
330
+ attention_mask,
331
+ return_embeddings,
332
+ return_contacts,
333
+ return_logits,
334
+ ):
335
+ """Compute model outputs."""
336
+ # Forward pass through the model
337
+ outputs = model(
338
+ input_ids=toks,
339
+ attention_mask=attention_mask,
340
+ output_hidden_states=return_embeddings,
341
+ output_attentions=return_contacts,
342
+ return_dict=True,
343
+ )
344
+
345
+ # Process logits
346
+ logits = None
347
+ if return_logits and hasattr(outputs, "logits"):
348
+ logits = outputs.logits.to(
349
+ dtype=self._precision_to_dtype(self.precision, "torch") # type: ignore
350
+ ).cpu()
351
+ torch.cuda.empty_cache()
352
+
353
+ # Process attention matrices
354
+ attention_matrices = None
355
+ if (
356
+ return_contacts
357
+ and hasattr(outputs, "attentions")
358
+ and outputs.attentions is not None
359
+ ):
360
+ attention_matrices = (
361
+ torch.stack(outputs.attentions)
362
+ .to(self._precision_to_dtype(self.precision, "torch")) # type: ignore
363
+ .cpu()
364
+ )
365
+ torch.cuda.empty_cache()
366
+
367
+ # Process representations
368
+ representations = None
369
+ if return_embeddings and hasattr(outputs, "hidden_states"):
370
+ representations = {
371
+ layer: outputs.hidden_states[layer - 1]
372
+ .to(self._precision_to_dtype(self.precision, "torch")) # type: ignore
373
+ .cpu()
374
+ for layer in self.layers
375
+ }
376
+ torch.cuda.empty_cache()
377
+
378
+ return logits, representations, attention_matrices
379
+
380
+
381
+ class CustomModelWrapper(torch.nn.Module):
382
+ """
383
+ Generic wrapper for custom models loaded from state dict.
384
+ """
385
+
386
+ def __init__(self, state_dict, config):
387
+ super().__init__()
388
+ self.config = config
389
+ self.state_dict_keys = list(state_dict.keys()) if state_dict else []
390
+
391
+ # Get dimensions from config
392
+ self.hidden_size = config.get("hidden_size", 768)
393
+ self.num_layers = config.get("num_layers", 12)
394
+ self.num_heads = config.get("num_attention_heads", 12)
395
+
396
+ # Load state dict if provided
397
+ if state_dict:
398
+ self.load_state_dict(state_dict, strict=False)
399
+
400
+ def forward(
401
+ self,
402
+ input_ids,
403
+ attention_mask=None,
404
+ output_hidden_states=True,
405
+ output_attentions=False,
406
+ return_dict=True,
407
+ ):
408
+ """
409
+ Forward pass - this is a placeholder implementation.
410
+ For actual custom models, this should be implemented based on the specific architecture.
411
+ """
412
+ # This is a basic implementation that returns dummy outputs
413
+ # In practice, you would implement the actual forward pass of your model
414
+
415
+ batch_size, seq_len = input_ids.shape
416
+
417
+ # Use config values instead of hardcoded defaults
418
+ hidden_size = self.hidden_size
419
+ num_layers = self.num_layers
420
+ num_heads = self.num_heads
421
+
422
+ # Create dummy outputs for demonstration
423
+ # In practice, these would be the actual model outputs
424
+ dummy_hidden_states = []
425
+ for _ in range(num_layers):
426
+ dummy_hidden_states.append(
427
+ torch.randn(batch_size, seq_len, hidden_size, device=input_ids.device)
428
+ )
429
+
430
+ dummy_attentions = []
431
+ if output_attentions:
432
+ for _ in range(num_layers):
433
+ dummy_attentions.append(
434
+ torch.randn(
435
+ batch_size, num_heads, seq_len, seq_len, device=input_ids.device
436
+ )
437
+ )
438
+
439
+ # Create output object
440
+ class ModelOutput:
441
+ def __init__(self):
442
+ self.hidden_states = (
443
+ dummy_hidden_states if output_hidden_states else None
444
+ )
445
+ self.attentions = dummy_attentions if output_attentions else None
446
+ self.logits = torch.randn(
447
+ batch_size, seq_len, 21, device=input_ids.device
448
+ ) # 21 for amino acids
449
+
450
+ return ModelOutput()
451
+
452
+
453
+ class DefaultProteinTokenizer:
454
+ """
455
+ Default tokenizer for protein sequences.
456
+ """
457
+
458
+ def __init__(self):
459
+ # Standard amino acid vocabulary
460
+ self.vocab = {
461
+ "<pad>": 0,
462
+ "<cls>": 1,
463
+ "<sep>": 2,
464
+ "<unk>": 3,
465
+ "A": 4,
466
+ "R": 5,
467
+ "N": 6,
468
+ "D": 7,
469
+ "C": 8,
470
+ "Q": 9,
471
+ "E": 10,
472
+ "G": 11,
473
+ "H": 12,
474
+ "I": 13,
475
+ "L": 14,
476
+ "K": 15,
477
+ "M": 16,
478
+ "F": 17,
479
+ "P": 18,
480
+ "S": 19,
481
+ "T": 20,
482
+ "W": 21,
483
+ "Y": 22,
484
+ "V": 23,
485
+ }
486
+ self.id_to_token = {v: k for k, v in self.vocab.items()}
487
+ self.pad_token_id = 0
488
+ self.cls_token_id = 1
489
+ self.sep_token_id = 2
490
+ self.unk_token_id = 3
491
+ self.all_special_ids = [0, 1, 2, 3]
492
+
493
+ def get_vocab(self):
494
+ return self.vocab
495
+
496
+ def __call__(
497
+ self,
498
+ text,
499
+ truncation=True,
500
+ padding="max_length",
501
+ max_length=512,
502
+ add_special_tokens=True,
503
+ return_tensors=None,
504
+ ):
505
+ """Tokenize protein sequence."""
506
+ if isinstance(text, str):
507
+ sequences = [text]
508
+ else:
509
+ sequences = text
510
+
511
+ tokenized = []
512
+ for seq in sequences:
513
+ tokens = []
514
+ if add_special_tokens:
515
+ tokens.append(self.cls_token_id)
516
+
517
+ for aa in seq:
518
+ tokens.append(self.vocab.get(aa.upper(), self.unk_token_id))
519
+
520
+ if add_special_tokens:
521
+ tokens.append(self.sep_token_id)
522
+
523
+ if truncation and len(tokens) > max_length:
524
+ tokens = tokens[:max_length]
525
+
526
+ # Create attention mask
527
+ attention_mask = [1] * len(tokens)
528
+
529
+ # Pad if necessary
530
+ if padding == "max_length":
531
+ while len(tokens) < max_length:
532
+ tokens.append(self.pad_token_id)
533
+ attention_mask.append(0)
534
+
535
+ tokenized.append({"input_ids": tokens, "attention_mask": attention_mask})
536
+
537
+ if return_tensors == "pt":
538
+ return {
539
+ "input_ids": torch.tensor([t["input_ids"] for t in tokenized]),
540
+ "attention_mask": torch.tensor(
541
+ [t["attention_mask"] for t in tokenized]
542
+ ),
543
+ }
544
+
545
+ return tokenized[0] if len(tokenized) == 1 else tokenized
546
+
547
+
548
+ class CustomDataset(pepe.utils.SequenceDictDataset):
549
+ """
550
+ Dataset class for custom embedder.
551
+ """
552
+
553
+ def __init__(
554
+ self,
555
+ sequences,
556
+ substring_dict,
557
+ context,
558
+ tokenizer,
559
+ max_length,
560
+ add_special_tokens=True,
561
+ ):
562
+ super().__init__(sequences, substring_dict, context)
563
+ self.tokenizer = tokenizer
564
+ self.max_length = max_length
565
+ self.add_special_tokens = add_special_tokens
566
+ self.pad_token_id = getattr(tokenizer, "pad_token_id", 0)
567
+
568
+ # Encode sequences
569
+ self.encoded_data = self._encode_sequences(
570
+ self.data, tokenizer, max_length, add_special_tokens
571
+ )
572
+
573
+ # Handle substring sequences if provided
574
+ if self.substring_dict:
575
+ logger.info("Tokenizing substrings...")
576
+ self.encoded_substring_data = self._encode_sequences(
577
+ self.filtered_substring_data,
578
+ tokenizer,
579
+ "max_length",
580
+ add_special_tokens=False,
581
+ )
582
+ self.substring_masks = self._get_substring_masks()
583
+
584
+ def _encode_sequences(self, data, tokenizer, max_length, add_special_tokens):
585
+ """Encode sequences using the tokenizer."""
586
+ labels, strs = zip(*data)
587
+
588
+ # Convert max_length to int if needed
589
+ if max_length == "max_length":
590
+ max_length = max(len(s) for s in strs) + (2 if add_special_tokens else 0)
591
+
592
+ encoded_sequences = []
593
+ for label, seq in zip(labels, strs):
594
+ # Tokenize the sequence
595
+ tokens = tokenizer(
596
+ seq,
597
+ truncation=True,
598
+ padding="max_length",
599
+ max_length=max_length,
600
+ add_special_tokens=add_special_tokens,
601
+ return_tensors="pt",
602
+ )
603
+
604
+ encoded_sequences.append(
605
+ (
606
+ label,
607
+ seq,
608
+ tokens["input_ids"].squeeze(0),
609
+ tokens["attention_mask"].squeeze(0),
610
+ )
611
+ )
612
+
613
+ return encoded_sequences
614
+
615
+ def get_max_encoded_length(self):
616
+ """Get maximum encoded sequence length."""
617
+ return max(len(toks) for _, _, toks, _ in self.encoded_data)
618
+
619
+ def safe_collate(self, batch):
620
+ """Collate function for data loader."""
621
+ if self.substring_dict:
622
+ labels, seqs, toks, attn_masks, substring_masks = zip(*batch)
623
+ return (
624
+ list(labels),
625
+ list(seqs),
626
+ torch.stack(toks),
627
+ torch.stack(attn_masks),
628
+ torch.stack(substring_masks),
629
+ )
630
+ else:
631
+ labels, seqs, toks, attn_masks = zip(*batch)
632
+ return (
633
+ list(labels),
634
+ list(seqs),
635
+ torch.stack(toks),
636
+ torch.stack(attn_masks),
637
+ None,
638
+ )
639
+
640
+ def __getitem__(self, idx):
641
+ """Get item from dataset."""
642
+ labels, seqs, toks, attn_mask = self.encoded_data[idx]
643
+ if self.substring_dict:
644
+ substring_masks = self.substring_masks[idx]
645
+ return labels, seqs, toks, attn_mask, substring_masks
646
+ else:
647
+ return labels, seqs, toks, attn_mask