Skip to main content

klib/ml/base/
model.rs

1//! Model definition for the Kord model.
2
3#[cfg(feature = "ml_train")]
4use burn::train::MultiLabelClassificationOutput;
5use burn::{
6    module::Module,
7    nn::{
8        self,
9        attention::{MhaInput, MultiHeadAttention, MultiHeadAttentionConfig},
10    },
11    tensor::{backend::Backend, Tensor},
12};
13
14use super::{INPUT_SPACE_SIZE, NUM_CLASSES};
15
16#[cfg(all(feature = "ml_train", feature = "ml_target_folded_bass"))]
17use super::{PITCH_CLASS_COUNT, TARGET_FOLDED_BASS_NOTE_OFFSET, TARGET_FOLDED_BASS_OFFSET};
18
19#[cfg(feature = "ml_train")]
20use crate::ml::train::data::KordBatch;
21
22/// The Kord model.
23///
24/// This model is a transformer model that uses multi-head attention to classify notes from a frequency space.
25#[derive(Module, Debug)]
26pub struct KordModel<B: Backend> {
27    mha: MultiHeadAttention<B>,
28    norm1: nn::LayerNorm<B>,
29
30    trunk_in: nn::Linear<B>,
31    trunk_dropout: nn::Dropout,
32    trunk_out: nn::Linear<B>,
33    norm2: nn::LayerNorm<B>,
34
35    output: nn::Linear<B>,
36
37    // Store chunk dimensions for forward pass
38    num_chunks: usize,
39    chunk_size: usize,
40}
41
42impl<B: Backend> KordModel<B> {
43    /// Create the model from the given configuration.
44    pub fn new(device: &B::Device, mha_heads: usize, dropout: f64, trunk_hidden_size: usize) -> Self {
45        // Calculate chunk dimensions based on number of heads
46        // Each head gets one chunk to attend to
47        assert!(
48            INPUT_SPACE_SIZE.is_multiple_of(mha_heads),
49            "INPUT_SPACE_SIZE ({}) must be divisible by mha_heads ({})",
50            INPUT_SPACE_SIZE,
51            mha_heads
52        );
53        let num_chunks = mha_heads;
54        let chunk_size = INPUT_SPACE_SIZE / mha_heads;
55        assert!(
56            chunk_size >= mha_heads && chunk_size.is_multiple_of(mha_heads),
57            "chunk_size ({}) must be >= mha_heads ({}) and divisible by it",
58            chunk_size,
59            mha_heads
60        );
61
62        let mha = MultiHeadAttentionConfig::new(chunk_size, mha_heads).with_dropout(dropout).init::<B>(device);
63        let norm1 = nn::LayerNormConfig::new(INPUT_SPACE_SIZE).init(device);
64
65        let trunk_in = nn::LinearConfig::new(INPUT_SPACE_SIZE, trunk_hidden_size).with_bias(true).init::<B>(device);
66        let trunk_dropout = nn::DropoutConfig::new(dropout).init();
67        let trunk_out = nn::LinearConfig::new(trunk_hidden_size, INPUT_SPACE_SIZE).with_bias(true).init::<B>(device);
68        let norm2 = nn::LayerNormConfig::new(INPUT_SPACE_SIZE).init(device);
69
70        let output = nn::LinearConfig::new(INPUT_SPACE_SIZE, NUM_CLASSES).with_bias(true).init::<B>(device);
71
72        Self {
73            mha,
74            norm1,
75            trunk_in,
76            trunk_dropout,
77            trunk_out,
78            norm2,
79            output,
80            num_chunks,
81            chunk_size,
82        }
83    }
84
85    /// Applies the forward pass on the input tensor.
86    pub fn forward(&self, input: Tensor<B, 2>) -> Tensor<B, 2> {
87        use burn::tensor::activation::gelu;
88
89        let [batch_size, input_size] = input.dims();
90
91        // Reshape to multi-token sequence format for attention
92        // Split input into chunks so attention can attend between different frequency regions
93        // Each head gets its own chunk (num_chunks = mha_heads)
94        let x = input.clone().reshape([batch_size, self.num_chunks, self.chunk_size]);
95
96        // Apply multi-head attention across the chunk sequence
97        let attn_output = self.mha.forward(MhaInput::new(x.clone(), x.clone(), x));
98
99        // Flatten back to [batch_size, input_size]
100        let attn_out = attn_output.context.reshape([batch_size, input_size]);
101
102        // Post-norm after attention with residual (normalize over full input_size)
103        let x = self.norm1.forward(input + attn_out);
104
105        // Light MLP trunk with residual connection and post-norm before the final output head.
106        let trunk_in = self.trunk_in.forward(x.clone());
107        let trunk_in = gelu(trunk_in);
108        let trunk_in = self.trunk_dropout.forward(trunk_in);
109        let trunk_out = self.trunk_out.forward(trunk_in);
110        let x = self.norm2.forward(x + trunk_out);
111
112        // Final linear layer to produce logits
113        self.output.forward(x)
114    }
115
116    /// Applies the forward classification pass on the input tensor.
117    #[cfg(all(feature = "ml_train", any(feature = "ml_target_full", feature = "ml_target_folded")))]
118    pub fn forward_classification(&self, item: KordBatch<B>) -> MultiLabelClassificationOutput<B> {
119        use burn::nn::loss::BinaryCrossEntropyLossConfig;
120
121        let logits = self.forward(item.samples);
122        let targets = item.targets;
123
124        let loss = BinaryCrossEntropyLossConfig::new().with_logits(true).init(&logits.device()).forward(logits.clone(), targets.clone());
125
126        MultiLabelClassificationOutput { loss, output: logits, targets }
127    }
128
129    /// Applies the forward classification pass when the folded-bass target is enabled.
130    #[cfg(all(feature = "ml_train", feature = "ml_target_folded_bass"))]
131    pub fn forward_classification(&self, item: KordBatch<B>) -> MultiLabelClassificationOutput<B> {
132        use burn::nn::loss::{BinaryCrossEntropyLossConfig, CrossEntropyLossConfig};
133
134        let logits = self.forward(item.samples);
135        let targets = item.targets;
136        let batch = logits.dims()[0];
137
138        let device = logits.device();
139        let bce_loss = BinaryCrossEntropyLossConfig::new().with_logits(true).init(&device);
140        let ce_loss = CrossEntropyLossConfig::new().init(&device);
141
142        let bass_start = TARGET_FOLDED_BASS_OFFSET;
143        let bass_end = bass_start + PITCH_CLASS_COUNT;
144        let note_start = TARGET_FOLDED_BASS_NOTE_OFFSET;
145        let note_end = note_start + PITCH_CLASS_COUNT;
146
147        let bass_logits = logits.clone().slice([0..batch, bass_start..bass_end]);
148        let note_logits = logits.clone().slice([0..batch, note_start..note_end]);
149
150        let note_targets = targets.clone().slice([0..batch, note_start..note_end]);
151        let bass_targets_hot = targets.clone().slice([0..batch, bass_start..bass_end]);
152        let bass_targets = bass_targets_hot.argmax(1).squeeze_dim::<1>(1);
153
154        let note_loss = bce_loss.forward(note_logits, note_targets);
155        let categorical_loss = ce_loss.forward(bass_logits, bass_targets);
156        let loss = note_loss + categorical_loss;
157
158        MultiLabelClassificationOutput { loss, output: logits, targets }
159    }
160}