bachi / models /HT.py
vedmistry's picture
initial BACHI deployment
bb92a2a
Raw
History Blame Contribute Delete
16.9 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from typing import Dict, Optional, Tuple
def get_relative_position_encoding(n_steps, d_model, max_dist=10):
"""
Generates relative positional encodings, similar to Transformer-XL.
"""
vocab_size = 2 * max_dist + 1
position = torch.arange(0, vocab_size, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe = torch.zeros(vocab_size, d_model)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
range_vec = torch.arange(n_steps)
distance_mat = range_vec[None, :] - range_vec[:, None]
distance_mat_clipped = torch.clamp(distance_mat, -max_dist, max_dist)
final_mat = distance_mat_clipped + max_dist
embeddings = F.embedding(final_mat.long(), pe)
return embeddings
class RelativeMultiHeadAttention(nn.Module):
"""
Multi-Head Attention with relative positional encoding, inspired by Transformer-XL and HTv2.
"""
def __init__(self, d_model, n_heads, dropout=0.1, max_dist=10):
super().__init__()
assert d_model % n_heads == 0
self.d_model = d_model
self.n_heads = n_heads
self.d_head = d_model // n_heads
self.max_dist = max_dist
self.w_q = nn.Linear(d_model, d_model)
self.w_k = nn.Linear(d_model, d_model)
self.w_v = nn.Linear(d_model, d_model)
self.w_o = nn.Linear(d_model, d_model)
self.u_bias = nn.Parameter(torch.randn(self.n_heads, self.d_head))
self.v_bias = nn.Parameter(torch.randn(self.n_heads, self.d_head))
self.w_r = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(d_model)
def forward(self, q, k, v, pos_emb, key_padding_mask=None, attn_mask=None, values_provided=False):
batch_size, seq_len_q, _ = q.size()
seq_len_k = k.size(1)
residual = q
q = self.w_q(q).view(batch_size, seq_len_q, self.n_heads, self.d_head)
k = self.w_k(k).view(batch_size, seq_len_k, self.n_heads, self.d_head)
v_transformed = self.w_v(v).view(batch_size, seq_len_k, self.n_heads, self.d_head)
q_with_u = (q + self.u_bias).transpose(1, 2) # (B, h, T_q, d_h)
q_with_v = (q + self.v_bias).transpose(1, 2) # (B, h, T_q, d_h)
k = k.transpose(1, 2) # (B, h, T_k, d_h)
v_transformed = v_transformed.transpose(1, 2) # (B, h, T_k, d_h)
pos_emb = self.w_r(pos_emb).view(seq_len_q, seq_len_k, self.n_heads, self.d_head)
pos_emb = pos_emb.permute(2, 0, 3, 1) # (h, T_q, d_h, T_k)
# Content-based addressing
ac = torch.matmul(q_with_u, k.transpose(-2, -1)) # (B, h, T_q, T_k)
# Position-based addressing
q_for_bd = q_with_v.permute(1, 2, 0, 3) # (h, T_q, B, d_h)
bd_t = torch.matmul(q_for_bd, pos_emb) # (h, T_q, B, T_k)
bd = bd_t.permute(2, 0, 1, 3) # (B, h, T_q, T_k)
attn_score = (ac + bd) / math.sqrt(self.d_head)
# Apply key and (optional) attention masks
if key_padding_mask is not None:
attn_score = attn_score.masked_fill(key_padding_mask.unsqueeze(1).unsqueeze(2), float('-inf'))
if attn_mask is not None:
attn_score = attn_score.masked_fill(attn_mask.unsqueeze(0), float('-inf'))
# ------------------------------------------------------------------
# Guard against rows where *all* keys were masked (e.g., an attention
# block that consists purely of padding tokens). A softmax over a
# vector of \(-\infty\) would otherwise produce NaNs. We detect such
# rows and set their scores to zero, ensuring a well-defined softmax
# (which will yield a uniform distribution) before later resetting
# their weights to zero.
# ------------------------------------------------------------------
all_masked = torch.isinf(attn_score) & (attn_score < 0) # True where -inf
all_masked = all_masked.all(dim=-1, keepdim=True) # (B, h, T_q, 1)
# For rows with all_masked == True, replace -inf with 0 so that the
# subsequent softmax does not generate NaNs.
attn_score = attn_score.masked_fill(all_masked, 0.0)
attn_weights = F.softmax(attn_score, dim=-1) # (B, h, T_q, T_k)
# After the softmax, zero-out rows that correspond to fully-padded
# queries so they don’t influence the output.
attn_weights = attn_weights.masked_fill(all_masked, 0.0)
attn_weights = self.dropout(attn_weights)
attn_output = torch.matmul(attn_weights, v_transformed) # (B, h, T_q, d_h)
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len_q, self.d_model)
attn_output = self.w_o(attn_output)
attn_output = self.dropout(attn_output)
if values_provided:
output = v + attn_output
else:
output = residual + attn_output
return self.layer_norm(output), attn_weights
class IntraBlockMHA(nn.Module):
def __init__(self, d_model, n_heads, dropout, max_dist=3):
super().__init__()
self.mha = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist=max_dist)
def forward(self, x, n_blocks, key_padding_mask=None):
batch_size, seq_len, _ = x.shape
block_len = seq_len // n_blocks
x = x.reshape(batch_size * n_blocks, block_len, -1)
mask = None
if key_padding_mask is not None:
mask = key_padding_mask.reshape(batch_size * n_blocks, block_len)
pos_emb = get_relative_position_encoding(block_len, x.size(-1), self.mha.max_dist).to(x.device)
x, _ = self.mha(x, x, x, pos_emb, key_padding_mask=mask)
return x.reshape(batch_size, seq_len, -1)
class ConvFFN(nn.Module):
def __init__(self, d_model, dropout=0.1):
super().__init__()
# piano roll resolution: 4 -> 12 (3x)
# kernel size: 3 -> 9 (3x)
self.conv1 = nn.Conv1d(d_model, d_model, kernel_size=9, padding=4)
self.conv2 = nn.Conv1d(d_model, d_model, kernel_size=9, padding=4)
self.dropout = nn.Dropout(dropout)
self.norm = nn.LayerNorm(d_model)
def forward(self, x):
residual = x
x = x.transpose(1, 2)
x = F.relu(self.conv1(x))
x = self.dropout(x)
x = self.conv2(x)
x = self.dropout(x)
x = x.transpose(1, 2)
x = x + residual
return self.norm(x)
class TransformerLayer(nn.Module):
def __init__(self, d_model, n_heads, dropout, max_dist, use_conv_ffn=True):
super().__init__()
self.self_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
self.cross_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
self.pos_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
if use_conv_ffn:
self.ffn = ConvFFN(d_model, dropout)
else: # Fallback to original FFN if needed
self.ffn = nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(d_model * 4, d_model),
nn.Dropout(dropout)
)
self.norm = nn.LayerNorm(d_model)
self.use_conv_ffn = use_conv_ffn
def forward(self, dec_input, enc_output, pos_emb, dec_pos_emb,
tgt_mask=None, memory_mask=None,
tgt_key_padding_mask=None, memory_key_padding_mask=None):
# Self-attention
dec_output, _ = self.self_attn(dec_input, dec_input, dec_input, pos_emb, key_padding_mask=tgt_key_padding_mask)
# Positional Attention
dec_output = self.pos_attn(dec_pos_emb.unsqueeze(0).repeat(dec_input.size(0), 1, 1),
dec_pos_emb.unsqueeze(0).repeat(dec_input.size(0), 1, 1),
dec_output, pos_emb=pos_emb,
key_padding_mask=tgt_key_padding_mask, values_provided=True)[0]
# Cross-attention
dec_output, _ = self.cross_attn(dec_output, enc_output, enc_output, pos_emb, key_padding_mask=memory_key_padding_mask)
# FFN
if self.use_conv_ffn:
dec_output = self.ffn(dec_output)
else:
dec_output = self.norm(dec_output + self.ffn(dec_output))
return dec_output
class PositionalEncoding(nn.Module):
"""Injects some information about the relative or absolute position of the tokens in the sequence."""
def __init__(self, d_model, dropout=0.1, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
"""
Args:
x: Tensor, shape [batch_size, seq_len, d_model]
"""
x = x + self.pe[:, :x.size(1), :]
return self.dropout(x)
class BinaryRound(torch.autograd.Function):
"""
Rounds a tensor whose values are in [0,1] to a tensor with values in {0, 1},
using the straight through estimator for the gradient.
"""
@staticmethod
def forward(ctx, input):
return torch.round(input).to(input.dtype)
@staticmethod
def backward(ctx, grad_output):
return grad_output
class HarmonyTransformer(nn.Module):
def __init__(self, config: Dict, vocab_sizes: Dict[str, int]):
super().__init__()
self.input_size = config['input_size']
self.d_model = config['d_model']
self.n_layers = config['n_layers']
self.n_heads = config['n_heads']
self.dropout_rate = config['dropout']
self.train_boundary = config['train_boundary']
self.slope = config.get('slope', 1.0)
self.max_len = config['n_beats'] * config['beat_resolution']
self.config = config
# Embeddings
self.enc_input_embed = nn.Linear(self.input_size, self.d_model)
self.dec_input_embed = nn.Linear(self.input_size, self.d_model)
self.pos_encoder = PositionalEncoding(self.d_model, self.dropout_rate, self.max_len)
self.register_buffer('pos_emb', get_relative_position_encoding(self.max_len, self.d_model, self.max_len - 1))
self.enc_intra_block_mha = IntraBlockMHA(self.d_model, self.n_heads, self.dropout_rate, max_dist=3)
self.dec_intra_block_mha = IntraBlockMHA(self.d_model, self.n_heads, self.dropout_rate, max_dist=3)
# Encoder
self.encoder_layers = nn.ModuleList([
RelativeMultiHeadAttention(self.d_model, self.n_heads, self.dropout_rate, self.max_len-1)
for _ in range(self.n_layers)
])
self.encoder_ffns = nn.ModuleList([ConvFFN(self.d_model, self.dropout_rate) for _ in range(self.n_layers)])
self.enc_weights = nn.Parameter(torch.zeros(self.n_layers + 1))
# Decoder
self.decoder_layers = nn.ModuleList([
TransformerLayer(self.d_model, self.n_heads, self.dropout_rate, self.max_len-1)
for _ in range(self.n_layers)
])
self.dec_weights = nn.Parameter(torch.zeros(self.n_layers + 1))
# Chord Change Prediction
self.chord_change_predictor = nn.Linear(self.d_model, 1)
# Output layers
self.root_predictor = nn.Linear(self.d_model, vocab_sizes['root'])
self.quality_predictor = nn.Linear(self.d_model, vocab_sizes['quality'])
self.bass_predictor = nn.Linear(self.d_model, vocab_sizes['bass'])
self._reset_parameters()
def _reset_parameters(self):
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
def chord_block_compression(self, hidden_states, chord_changes):
"""Compress hidden states according to chord changes."""
block_ids = torch.cumsum(chord_changes, dim=1) - chord_changes[:, 0].unsqueeze(1)
# Ensure integer dtype for one-hot encoding
block_ids = block_ids.long()
max_blocks = (torch.max(block_ids).item() + 1) if block_ids.numel() > 0 else 1
one_hot_ids = F.one_hot(block_ids, num_classes=max_blocks).float() # (B, S, M)
summed_states = torch.bmm(one_hot_ids.transpose(1, 2), hidden_states) # (B, M, H)
block_counts = one_hot_ids.sum(dim=1).unsqueeze(-1).clamp(min=1)
mean_states = summed_states / block_counts
# block_ids already of integer dtype
return mean_states, block_ids
def decode_compressed_sequences(self, compressed_sequences, block_ids):
"""Decode chord sequences according to chords_pred and block_ids."""
return torch.gather(compressed_sequences, 1, block_ids.unsqueeze(-1).expand(-1, -1, compressed_sequences.size(-1)))
def forward(self, src: torch.Tensor, src_key_padding_mask: torch.Tensor) -> Dict[str, torch.Tensor]:
"""
Args:
src: (batch_size, seq_len, input_size)
src_key_padding_mask: (batch_size, seq_len), True for valid, False for pad
Returns:
Dictionary of predictions.
"""
# --- Encoder ---
enc_output = self.pos_encoder(self.enc_input_embed(src))
# Intra-block MHA
enc_output = self.enc_intra_block_mha(enc_output, n_blocks=self.max_len//4, key_padding_mask=src_key_padding_mask)
# Main encoder layers
enc_layer_outputs = [enc_output]
for i in range(self.n_layers):
enc_output, _ = self.encoder_layers[i](enc_output, enc_output, enc_output,
self.pos_emb, key_padding_mask=src_key_padding_mask)
enc_output = self.encoder_ffns[i](enc_output)
enc_layer_outputs.append(enc_output)
# Weighted sum of encoder layers
enc_weights = F.softmax(self.enc_weights, dim=0)
enc_output = torch.stack(enc_layer_outputs, dim=-1) # (B, S, H, L+1)
enc_output = (enc_output * enc_weights).sum(dim=-1) # (B, S, H)
# --- Chord‐change prediction ---
boundary_logits = self.chord_change_predictor(enc_output) # (B, S, 1)
chord_change_prob = torch.sigmoid(self.slope * boundary_logits)
chord_change_pred = BinaryRound.apply(chord_change_prob).squeeze(-1) # (B, S) in {0,1}
# --- Decoder input embedding with regionalization ---
dec_input_embed = self.dec_input_embed(src)
dec_input_embed = F.dropout(dec_input_embed, p=self.dropout_rate, training=self.training)
dec_input_embed = self.dec_intra_block_mha(dec_input_embed, n_blocks=self.max_len // 4, key_padding_mask=src_key_padding_mask)
# Compress by predicted chord boundaries and expand back
dec_input_embed_reg, block_ids = self.chord_block_compression(dec_input_embed, chord_change_pred.long())
dec_input_embed_reg = self.decode_compressed_sequences(dec_input_embed_reg, block_ids)
# Combine embeddings
dec_input_embed = dec_input_embed + dec_input_embed_reg + enc_output
# Positional encoding
dec_input_embed = self.pos_encoder(dec_input_embed)
dec_pos_emb = self.pos_encoder.pe[:, :dec_input_embed.size(1), :].squeeze(0)
# --- Decoder layers with layer weighting ---
dec_layer_outputs = [dec_input_embed]
dec_output = dec_input_embed
for i in range(self.n_layers):
dec_output = self.decoder_layers[i](dec_output, enc_output, self.pos_emb, dec_pos_emb,
tgt_key_padding_mask=src_key_padding_mask,
memory_key_padding_mask=src_key_padding_mask)
dec_layer_outputs.append(dec_output)
dec_weights = F.softmax(self.dec_weights, dim=0)
dec_output = torch.stack(dec_layer_outputs, dim=-1) # (B, S, H, L+1)
dec_output = (dec_output * dec_weights).sum(dim=-1)
# --- Output Projections ---
root_logits = self.root_predictor(dec_output)
quality_logits = self.quality_predictor(dec_output)
bass_logits = self.bass_predictor(dec_output)
preds = {
'root': root_logits,
'quality': quality_logits,
'bass': bass_logits,
'boundary': boundary_logits
}
return preds