DreaMS / model.py
aryan-f's picture
Create model.py
f629e17 verified
Raw
History Blame Contribute Delete
17.4 kB
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, :] # precursor
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
# Implementation adapted from https://github.com/pluskal-lab/DreaMS
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 # m/z and intensity
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):
# Generate padding mask
padding_mask = spec[:, :, 0] == 0
# Lift peaks to d_peak (m/z's are normalized)
peak_embeds = self.ff_peak(self.__normalize_spec(spec))
# Concatenate with fourier features (d_peak -> d_peak + d_fourier ("num_fourier_features" -> d_fourier))
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)
# Transformer encoder blocks
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) # clamp not to have a separate class for max_mz
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
# Parameters for linear projections of queries, keys, values and output
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)
# initializing
# If we do Xavier normal initialization, std = sqrt(2/(2D))
# but it's too big and causes un-stability in PostNorm,
# so we use the smaller std of feedforward module, i.e. sqrt(2/(5D))
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:
# (bs, n, n, dists_d) -> (bs, n, n, n_heads) -> (bs, n_heads, n, n) = A.shape
att_bias = self.lin_graphormer(graphormer_dists).permute(0, 3, 1, 2)
else:
# (bs, n, n, dists_d) -> (bs, 1, n, n) broadcastable with A
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): # e.g. is List or Tuple
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)
# initializing
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):
# my preliminary experiments show all RELU-variants
# work the same and slower, RELU FTW!!!
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