| """ |
| Mutation operators for biological language models. |
| Adds controlled noise and structural perturbations to model weights. |
| """ |
|
|
| import os |
| import torch |
| import numpy as np |
| import logging |
| from typing import Optional |
| from transformers import AutoModelForMaskedLM |
| from safetensors.torch import save_file, load_file |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class BioMutator: |
| """Base mutation operator.""" |
| |
| def mutate(self, model_path: str, save_path: str) -> str: |
| """Apply mutation to a model and save result.""" |
| raise NotImplementedError |
|
|
|
|
| class GaussianNoiseMutator(BioMutator): |
| """Add Gaussian noise to model weights.""" |
| |
| def __init__(self, std: float = 0.01, layer_filter: Optional[str] = None): |
| self.std = std |
| self.layer_filter = layer_filter |
| |
| def mutate(self, model_path: str, save_path: str) -> str: |
| logger.info(f"Applying Gaussian noise mutation (std={self.std}) to {model_path}") |
| |
| try: |
| state = load_file(os.path.join(model_path, "model.safetensors")) |
| except: |
| state = torch.load( |
| os.path.join(model_path, "pytorch_model.bin"), |
| map_location="cpu", |
| ) |
| |
| mutated = {} |
| for key, tensor in state.items(): |
| if self.layer_filter is not None and self.layer_filter not in key: |
| mutated[key] = tensor.clone() |
| continue |
| |
| |
| noise = torch.randn_like(tensor) * self.std |
| mutated[key] = tensor + noise |
| |
| |
| os.makedirs(save_path, exist_ok=True) |
| save_file(mutated, os.path.join(save_path, "model.safetensors")) |
| |
| |
| self._copy_auxiliary_files(model_path, save_path) |
| |
| logger.info(f"Mutated model saved to {save_path}") |
| return save_path |
| |
| def _copy_auxiliary_files(self, source_path: str, dest_path: str) -> None: |
| import shutil |
| |
| for fname in ["config.json", "tokenizer.json", "tokenizer_config.json", "vocab.txt"]: |
| src = os.path.join(source_path, fname) |
| if os.path.exists(src): |
| shutil.copy2(src, os.path.join(dest_path, fname)) |
|
|
|
|
| class LayerScaleMutator(BioMutator): |
| """Scale specific layers by a random factor.""" |
| |
| def __init__(self, scale_range: tuple = (0.9, 1.1), target_layers: Optional[list] = None): |
| self.scale_range = scale_range |
| self.target_layers = target_layers |
| |
| def mutate(self, model_path: str, save_path: str) -> str: |
| logger.info(f"Applying layer scale mutation to {model_path}") |
| |
| try: |
| state = load_file(os.path.join(model_path, "model.safetensors")) |
| except: |
| state = torch.load( |
| os.path.join(model_path, "pytorch_model.bin"), |
| map_location="cpu", |
| ) |
| |
| mutated = {} |
| for key, tensor in state.items(): |
| |
| should_scale = False |
| if self.target_layers: |
| for layer_name in self.target_layers: |
| if layer_name in key: |
| should_scale = True |
| break |
| else: |
| |
| should_scale = np.random.random() < 0.1 |
| |
| if should_scale: |
| scale = np.random.uniform(*self.scale_range) |
| mutated[key] = tensor * scale |
| logger.debug(f"Scaled {key} by {scale:.4f}") |
| else: |
| mutated[key] = tensor.clone() |
| |
| os.makedirs(save_path, exist_ok=True) |
| save_file(mutated, os.path.join(save_path, "model.safetensors")) |
| |
| self._copy_auxiliary_files(model_path, save_path) |
| |
| logger.info(f"Layer-scaled model saved to {save_path}") |
| return save_path |
| |
| def _copy_auxiliary_files(self, source_path: str, dest_path: str) -> None: |
| import shutil |
| |
| for fname in ["config.json", "tokenizer.json", "tokenizer_config.json", "vocab.txt"]: |
| src = os.path.join(source_path, fname) |
| if os.path.exists(src): |
| shutil.copy2(src, os.path.join(dest_path, fname)) |
|
|
|
|
| class DropoutMutator(BioMutator): |
| """Randomly zero out a fraction of weights (structural pruning).""" |
| |
| def __init__(self, dropout_rate: float = 0.01): |
| self.dropout_rate = dropout_rate |
| |
| def mutate(self, model_path: str, save_path: str) -> str: |
| logger.info(f"Applying dropout mutation (rate={self.dropout_rate}) to {model_path}") |
| |
| try: |
| state = load_file(os.path.join(model_path, "model.safetensors")) |
| except: |
| state = torch.load( |
| os.path.join(model_path, "pytorch_model.bin"), |
| map_location="cpu", |
| ) |
| |
| mutated = {} |
| for key, tensor in state.items(): |
| mask = torch.rand_like(tensor) > self.dropout_rate |
| mutated[key] = tensor * mask |
| |
| os.makedirs(save_path, exist_ok=True) |
| save_file(mutated, os.path.join(save_path, "model.safetensors")) |
| |
| self._copy_auxiliary_files(model_path, save_path) |
| |
| logger.info(f"Dropout-mutated model saved to {save_path}") |
| return save_path |
| |
| def _copy_auxiliary_files(self, source_path: str, dest_path: str) -> None: |
| import shutil |
| |
| for fname in ["config.json", "tokenizer.json", "tokenizer_config.json", "vocab.txt"]: |
| src = os.path.join(source_path, fname) |
| if os.path.exists(src): |
| shutil.copy2(src, os.path.join(dest_path, fname)) |
|
|