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.
- pepe/__init__.py +41 -0
- pepe/__main__.py +23 -0
- pepe/embedders/__init__.py +0 -0
- pepe/embedders/base_embedder.py +797 -0
- pepe/embedders/custom_embedder.py +647 -0
- pepe/embedders/esm_embedder.py +164 -0
- pepe/embedders/huggingface_embedder.py +200 -0
- pepe/model_selecter.py +99 -0
- pepe/parse_arguments.py +167 -0
- pepe/utils.py +905 -0
- pepe_cli-1.0.0.dist-info/METADATA +137 -0
- pepe_cli-1.0.0.dist-info/RECORD +18 -0
- pepe_cli-1.0.0.dist-info/WHEEL +5 -0
- pepe_cli-1.0.0.dist-info/entry_points.txt +3 -0
- pepe_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- pepe_cli-1.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_run.py +43 -0
|
@@ -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
|