| import math |
| from typing import Sequence |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from huggingface_hub import hf_hub_download |
| from safetensors.torch import load_model |
|
|
|
|
| class DreaMS: |
| download_repository = 'aryan-f/DreaMS' |
| downloadable = 'embedding_model.safetensors' |
|
|
| def __init__(self, device=None): |
| self.device = torch.device(device or ('cuda' if torch.cuda.is_available() else 'cpu')) |
| device = self.device.type if self.device.index is None else f'{self.device.type}:{self.device.index}' |
| network = DreaMSNet() |
| network = network.to(device) |
| hf = hf_hub_download(repo_id=self.download_repository, filename=self.downloadable, dry_run=True) |
| if not hf.is_cached: |
| print('Fetching weights from 🤗Hugging Face... This make take a while.') |
| filepath = hf_hub_download(repo_id=self.download_repository, filename=self.downloadable) |
| print(f'Loading "{self.download_repository}" weights from "{filepath}" [{device}]') |
| load_model(network, filepath, device=device, strict=True) |
| self.network = network.eval() |
|
|
| def embed(self, spectra, batch_size: int = 128): |
| embeddings = [] |
| with torch.inference_mode(): |
| for start in range(0, len(spectra), batch_size): |
| spectra = [self.preprocess(spectra[index]) for index in range(start, min(start + batch_size, len(spectra)))] |
| spectra = torch.stack(spectra) |
| embeds = self.network(spectra) |
| embeds = embeds[:, 0, :] |
| embeddings.append(embeds) |
| embeddings = torch.cat(embeddings) |
| return embeddings |
|
|
| def preprocess(self, spectrum): |
| positions, values = spectrum |
| spectrum = torch.zeros((self.network.top_n, 2), dtype=torch.float32, device=self.device) |
| positions = torch.as_tensor(positions, dtype=torch.float32, device=self.device) |
| values = torch.as_tensor(values, dtype=torch.float32, device=self.device) |
| order = torch.argsort(values, descending=True)[:self.network.top_n] |
| positions = positions[order] |
| values = values[order] |
| spectrum[:len(order), 0] = positions |
| spectrum[:len(order), 1] = values |
| return spectrum |
|
|
|
|
| |
|
|
|
|
| class DreaMSNet(nn.Module): |
|
|
| def __init__(self, fourier_strategy='lin_float_int', fourier_num_freqs=None, fourier_trainable=False, max_tbxic_stdev=0.0001, max_mz=1000, d_fourier=980, dropout=0.1, no_ffs_bias=False, ff_fourier_depth=5, ff_fourier_d=512, d_peak=44, ff_peak_depth=1, d_model=1024, ff_out_depth=1, hot_mz_bin_size=0.05, focal_loss_gamma=5.0, n_layers=7, pre_norm=True, scnorm=False, n_heads=8, att_dropout=0.1, no_transformer_bias=True, attn_mech='dot-product', d_graphormer_params=0, ff_dropout=0.1, residual_dropout=0.1, top_n=100): |
| super().__init__() |
| self.max_mz = max_mz |
| self.d_model = d_model |
| token_dim = 2 |
| self.top_n = top_n |
| self.fourier_enc = FourierFeatures( |
| strategy=fourier_strategy, num_freqs=fourier_num_freqs, trainable=fourier_trainable, |
| x_min=max_tbxic_stdev, x_max=max_mz, |
| ) |
| self.ff_fourier = FeedForward( |
| in_dim=self.fourier_enc.num_features(), out_dim=d_fourier, |
| bias=not no_ffs_bias, dropout=dropout, |
| depth=ff_fourier_depth, hidden_dim=ff_fourier_d, |
| ) |
| self.ff_peak = FeedForward( |
| in_dim=token_dim, hidden_dim=d_peak, out_dim=d_peak, |
| depth=ff_peak_depth, |
| dropout=dropout, bias=not no_ffs_bias, |
| ) |
| self.transformer_encoder = TransformerEncoder( |
| n_layers, pre_norm, d_model, scnorm, n_heads, att_dropout, |
| no_transformer_bias, attn_mech, d_graphormer_params, ff_dropout, |
| residual_dropout, |
| ) |
| self.ff_out = FeedForward( |
| in_dim=d_model, hidden_dim=d_model, depth=ff_out_depth, act_last=False, |
| out_dim=int(max_mz / hot_mz_bin_size), |
| dropout=dropout, bias=True, |
| ) |
| self.ro_out = nn.Linear(2 * self.d_model, 1, bias=False) |
|
|
| def forward(self, spec): |
| |
| padding_mask = spec[:, :, 0] == 0 |
|
|
| |
| peak_embeds = self.ff_peak(self.__normalize_spec(spec)) |
|
|
| |
| fourier_features = self.ff_fourier(self.fourier_enc(spec[..., [0]])) |
| spec = torch.cat([peak_embeds, fourier_features], dim=-1) |
|
|
| graphormer_dists = fourier_features.unsqueeze(2) - fourier_features.unsqueeze(1) |
|
|
| |
| spec = self.transformer_encoder(spec, padding_mask, graphormer_dists) |
|
|
| return spec |
|
|
| def __normalize_spec(self, spec): |
| """ |
| Normalizes raw m/z values. Notice, that it is not in dataset `__getitem__ `because raw m/z values are still needed |
| for Fourier features. Intensities are supposed to be normalized in `__getitem__`. |
| """ |
| return spec / torch.tensor([self.max_mz, 1.], device=spec.device, dtype=spec.dtype) |
|
|
| @staticmethod |
| def to_classes(vals: torch.Tensor, max_val: float, bin_size: float, special_vals=(), return_num_classes=False): |
| special_masks = [vals == v for v in special_vals] |
| num_classes = int(max_val / bin_size) |
| classes = torch.round(vals / bin_size).long() |
| classes = classes.clamp(max=num_classes - 1) |
| for i, m in enumerate(special_masks): |
| classes[m] = num_classes + i |
| if return_num_classes: |
| return classes, num_classes + len(special_vals) |
| return classes |
|
|
|
|
| class TransformerEncoder(nn.Module): |
|
|
| def __init__(self, n_layers, pre_norm, d_model, scnorm, n_heads, att_dropout, no_transformer_bias, attn_mech, d_graphormer_params, ff_dropout, residual_dropout): |
| super(TransformerEncoder, self).__init__() |
| self.residual_dropout = residual_dropout |
| self.n_layers = n_layers |
| self.pre_norm = pre_norm |
|
|
| self.atts = nn.ModuleList( |
| [MultiheadAttention(d_model, n_heads, att_dropout, no_transformer_bias, attn_mech, d_graphormer_params) for |
| _ in range(self.n_layers)]) |
| self.ffs = nn.ModuleList( |
| [TokenWiseFeedForward(ff_dropout, d_model, no_transformer_bias) for _ in range(self.n_layers)]) |
|
|
| num_scales = self.n_layers * 2 + 1 if self.pre_norm else self.n_layers * 2 |
| if scnorm: |
| self.scales = nn.ModuleList([ScaleNorm(d_model ** 0.5) for _ in range(num_scales)]) |
| else: |
| self.scales = nn.ModuleList([nn.LayerNorm(d_model) for _ in range(num_scales)]) |
|
|
| def forward(self, src_inputs, src_mask, graphormer_dists=None): |
| pre_norm = self.pre_norm |
| post_norm = not pre_norm |
|
|
| x = F.dropout(src_inputs, p=self.residual_dropout, training=self.training) |
| for i in range(self.n_layers): |
| att = self.atts[i] |
| ff = self.ffs[i] |
| att_scale = self.scales[2 * i] |
| ff_scale = self.scales[2 * i + 1] |
|
|
| residual = x |
| x = att_scale(x) if pre_norm else x |
| x, _ = att(q=x, k=x, v=x, mask=src_mask, graphormer_dists=graphormer_dists) |
| x = residual + F.dropout(x, p=self.residual_dropout, training=self.training) |
| x = att_scale(x) if post_norm else x |
|
|
| residual = x |
| x = ff_scale(x) if pre_norm else x |
| x = ff(x) |
| x = residual + F.dropout(x, p=self.residual_dropout, training=self.training) |
| x = ff_scale(x) if post_norm else x |
|
|
| x = self.scales[-1](x) if pre_norm else x |
| return x |
|
|
|
|
| class MultiheadAttention(nn.Module): |
|
|
| def __init__(self, d_model, n_heads, att_dropout, no_transformer_bias, attn_mech, d_graphormer_params): |
| super(MultiheadAttention, self).__init__() |
| self.d_model = d_model |
| self.n_heads = n_heads |
| self.dropout = att_dropout |
| self.use_transformer_bias = not no_transformer_bias |
| self.attn_mech = attn_mech |
| self.d_graphormer_params = d_graphormer_params |
|
|
| if self.d_model % self.n_heads != 0: |
| raise ValueError('Required: d_model % n_heads == 0.') |
|
|
| self.head_dim = self.d_model // self.n_heads |
| self.scale = self.head_dim ** -0.5 |
|
|
| |
| self.weights = torch.nn.Parameter(torch.Tensor(4 * self.d_model, self.d_model)) |
| if self.use_transformer_bias: |
| self.biases = torch.nn.Parameter(torch.Tensor(4 * self.d_model)) |
|
|
| if self.d_graphormer_params: |
| self.lin_graphormer = nn.Linear(self.d_graphormer_params, self.n_heads, bias=False) |
|
|
| |
| |
| |
| |
| mean = 0 |
| std = (2 / (5 * self.d_model)) ** 0.5 |
| nn.init.normal_(self.weights, mean=mean, std=std) |
| if self.use_transformer_bias: |
| nn.init.constant_(self.biases, 0.) |
|
|
| if self.attn_mech == 'additive_v': |
| self.additive_v = torch.nn.Parameter(torch.Tensor(self.n_heads, self.head_dim)) |
| nn.init.normal_(self.additive_v, mean=mean, std=std) |
|
|
| def forward(self, q, k, v, mask, graphormer_dists=None, do_proj_qkv=True): |
| bs, n, d = q.size() |
|
|
| def _split_heads(tensor): |
| bsz, length, d_model = tensor.size() |
| return tensor.reshape(bsz, length, self.n_heads, self.head_dim).transpose(1, 2) |
|
|
| if do_proj_qkv: |
| q, k, v = self.proj_qkv(q, k, v) |
|
|
| q = _split_heads(q) |
| k = _split_heads(k) |
| v = _split_heads(v) |
|
|
| if self.attn_mech == 'dot-product': |
| att_weights = torch.einsum('bhnd,bhdm->bhnm', q, k.transpose(-2, -1)) |
| elif self.attn_mech == 'additive_v' or self.attn_mech == 'additive_fixed': |
| att_weights = (q.unsqueeze(-2) - k.unsqueeze(-3)) |
| if self.attn_mech == 'additive_v': |
| att_weights = (att_weights * self.additive_v.unsqueeze(0).unsqueeze(2).unsqueeze(3)) |
| att_weights = att_weights.sum(dim=-1) |
| else: |
| raise NotImplementedError(f'"{self.attn_mech}" attention mechanism is not implemented.') |
| att_weights = att_weights * self.scale |
|
|
| if graphormer_dists is not None: |
| if self.d_graphormer_params: |
| |
| att_bias = self.lin_graphormer(graphormer_dists).permute(0, 3, 1, 2) |
| else: |
| |
| att_bias = graphormer_dists.sum(dim=-1).unsqueeze(1) |
| att_weights = att_weights + att_bias |
|
|
| if mask is not None: |
| att_weights.masked_fill_(mask.unsqueeze(1).unsqueeze(-1), -1e9) |
|
|
| att_weights = F.softmax(att_weights, dim=-1) |
| att_weights = F.dropout(att_weights, p=self.dropout, training=self.training) |
| _att_weights = att_weights.reshape(-1, n, n) |
| output = torch.bmm(_att_weights, v.reshape(bs * self.n_heads, -1, self.head_dim)) |
| output = output.reshape(bs, self.n_heads, n, self.head_dim).transpose(1, 2).reshape(bs, n, -1) |
| output = self.proj_o(output) |
|
|
| return output, att_weights |
|
|
| def proj_qkv(self, q, k, v): |
| qkv_same = q.data_ptr() == k.data_ptr() == v.data_ptr() |
| kv_same = k.data_ptr() == v.data_ptr() |
|
|
| if qkv_same: |
| q, k, v = self._proj(q, end=3 * self.d_model).chunk(3, dim=-1) |
| elif kv_same: |
| q = self._proj(q, end=self.d_model) |
| k, v = self._proj(k, start=self.d_model, end=3 * self.d_model).chunk(2, dim=-1) |
| else: |
| q = self.proj_q(q) |
| k = self.proj_k(k) |
| v = self.proj_v(v) |
|
|
| return q, k, v |
|
|
| def _proj(self, x, start=0, end=None): |
| weight = self.weights[start:end, :] |
| bias = None if not self.use_transformer_bias else self.biases[start:end] |
| return F.linear(x, weight=weight, bias=bias) |
|
|
| def proj_q(self, q): |
| return self._proj(q, end=self.d_model) |
|
|
| def proj_k(self, k): |
| return self._proj(k, start=self.d_model, end=2 * self.d_model) |
|
|
| def proj_v(self, v): |
| return self._proj(v, start=2 * self.d_model, end=3 * self.d_model) |
|
|
| def proj_o(self, x): |
| return self._proj(x, start=3 * self.d_model) |
|
|
|
|
| class FourierFeatures(nn.Module): |
|
|
| def __init__(self, strategy, x_min, x_max, trainable=True, funcs='both', sigma=10, num_freqs=512): |
| assert strategy in {'random', 'voronov_et_al', 'lin_float_int'} |
| assert funcs in {'both', 'sin', 'cos'} |
| assert x_min < 1 |
|
|
| super().__init__() |
| self.funcs = funcs |
| self.strategy = strategy |
| self.trainable = trainable |
| self.num_freqs = num_freqs |
|
|
| if strategy == 'random': |
| self.b = torch.randn(num_freqs) * sigma |
| if self.strategy == 'voronov_et_al': |
| self.b = torch.tensor( |
| [1 / (x_min * (x_max / x_min) ** (2 * i / (num_freqs - 2))) for i in range(1, num_freqs)], |
| ) |
| elif self.strategy == 'lin_float_int': |
| self.b = torch.tensor( |
| [1 / (x_min * i) for i in range(2, math.ceil(1 / x_min), 2)] + |
| [1 / (1 * i) for i in range(2, math.ceil(x_max), 1)], |
| ) |
| self.b = self.b.unsqueeze(0) |
|
|
| self.b = nn.Parameter(self.b, requires_grad=self.trainable) |
| self.register_parameter('Fourier frequencies', self.b) |
|
|
| def forward(self, x): |
| x = 2 * torch.pi * x @ self.b |
| if self.funcs == 'both': |
| x = torch.cat((torch.cos(x), torch.sin(x)), dim=-1) |
| elif self.funcs == 'cos': |
| x = torch.cos(x) |
| elif self.funcs == 'sin': |
| x = torch.sin(x) |
| return x |
|
|
| def num_features(self): |
| return self.b.shape[1] if self.funcs != 'both' else 2 * self.b.shape[1] |
|
|
|
|
| class FeedForward(nn.Module): |
|
|
| def __init__(self, in_dim, out_dim, hidden_dim: int, depth=None, act_last=True, act=nn.ReLU, bias=True, dropout=0.0): |
| super().__init__() |
|
|
| if isinstance(hidden_dim, int): |
| assert depth is not None |
| hidden_dim = [hidden_dim] * depth |
| elif hidden_dim == 'interpolated': |
| assert depth is not None |
| hidden_dim = self.interpolate_interval(a=in_dim, b=out_dim, n=depth - 1, only_inter=True, rounded=True) |
| elif isinstance(hidden_dim, Sequence): |
| depth = len(hidden_dim) |
| else: |
| raise ValueError |
|
|
| self.ff = nn.ModuleList([]) |
| for l in range(depth): |
| d1 = hidden_dim[l - 1] if l != 0 else in_dim |
| d2 = hidden_dim[l] if l != depth - 1 else out_dim |
| self.ff.append(nn.Linear(d1, d2, bias=bias)) |
| if l != depth - 1: |
| self.ff.append(nn.Dropout(p=dropout)) |
| if l != depth - 1 or act_last: |
| self.ff.append(act()) |
| self.ff = nn.Sequential(*self.ff) |
|
|
| def forward(self, x): |
| return self.ff(x) |
|
|
| @staticmethod |
| def interpolate_interval(a, b, n, only_inter=False, rounded=False): |
| x_min, x_max = min(a, b), max(a, b) |
| res = [x_min + i * (x_max - x_min) / (n + 1) for i in |
| range(1 if only_inter else 0, n + 1 if only_inter else n + 2)] |
| if x_max == a: |
| res.reverse() |
| if rounded: |
| res = [round(x) for x in res] |
| return res |
|
|
|
|
| class TokenWiseFeedForward(nn.Module): |
|
|
| def __init__(self, ff_dropout, d_model, no_transformer_bias): |
| super(TokenWiseFeedForward, self).__init__() |
| self.dropout = ff_dropout |
| self.d_model = d_model |
| self.ff_dim = 4 * d_model |
| self.use_transformer_bias = not no_transformer_bias |
|
|
| self.in_proj = nn.Linear(self.d_model, self.ff_dim, bias=self.use_transformer_bias) |
| self.out_proj = nn.Linear(self.ff_dim, self.d_model, bias=self.use_transformer_bias) |
|
|
| |
| mean = 0 |
| std = (2 / (self.ff_dim + self.d_model)) ** 0.5 |
| nn.init.normal_(self.in_proj.weight, mean=mean, std=std) |
| nn.init.normal_(self.out_proj.weight, mean=mean, std=std) |
| if self.use_transformer_bias: |
| nn.init.constant_(self.in_proj.bias, 0.) |
| nn.init.constant_(self.out_proj.bias, 0.) |
|
|
| def forward(self, x): |
| |
| |
| y = F.relu(self.in_proj(x)) |
| y = F.dropout(y, p=self.dropout, training=self.training) |
| return self.out_proj(y) |
|
|
|
|
| class ScaleNorm(nn.Module): |
|
|
| def __init__(self, scale, eps=1e-5): |
| super(ScaleNorm, self).__init__() |
| self.scale = torch.nn.Parameter(torch.tensor(scale)) |
| self.eps = eps |
|
|
| def forward(self, x): |
| norm = self.scale / torch.norm(x, dim=-1, keepdim=True).clamp(min=self.eps) |
| return x * norm |
|
|