Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,491 @@
//! BERT task-specific heads
//!
//! This module implements various task-specific heads for BERT models:
//! - Masked Language Modeling (MLM) head
//! - Sequence classification head
//! - Token classification head (NER, POS tagging)
//! - Question answering head (SQuAD-style)
use crate::{Result, TransformerError};
use rtx_tensor::{Device, Tensor};
use super::config::BertConfig;
use super::feedforward::BertIntermediate;
use super::layer_norm::BertLayerNorm;
/// BERT masked language modeling head
#[derive(Debug)]
pub struct BertMLMHead {
pub hidden_size: usize,
pub vocab_size: usize,
/// Dense layer for hidden state transformation: [`hidden_size`, `hidden_size`]
pub dense: Option<Tensor>,
/// Dense layer bias: [`hidden_size`]
pub dense_bias: Option<Tensor>,
/// Layer normalization for predictions
pub layer_norm: Option<BertLayerNorm>,
/// Decoder weights for vocabulary projection: [`hidden_size`, `vocab_size`]
pub decoder: Option<Tensor>,
/// Decoder bias: [`vocab_size`]
pub decoder_bias: Option<Tensor>,
}
impl BertMLMHead {
/// Create new MLM head
pub fn new(config: &BertConfig, device: &Device) -> Result<Self> {
// Dense layer for hidden state transformation
let init_std = (2.0 / (config.hidden_size + config.hidden_size) as f32).sqrt();
let dense = Tensor::randn(&[config.hidden_size, config.hidden_size], device)?
.mul_scalar(init_std)?;
let dense_bias = Tensor::zeros([config.hidden_size], device)?;
// Layer normalization
let layer_norm = BertLayerNorm::new(config.hidden_size, config.layer_norm_eps, device)?;
// Decoder (vocabulary projection) layer
let decoder_init_std = (2.0 / (config.hidden_size + config.vocab_size) as f32).sqrt();
let decoder = Tensor::randn(&[config.hidden_size, config.vocab_size], device)?
.mul_scalar(decoder_init_std)?;
let decoder_bias = Tensor::zeros([config.vocab_size], device)?;
Ok(Self {
hidden_size: config.hidden_size,
vocab_size: config.vocab_size,
dense: Some(dense),
dense_bias: Some(dense_bias),
layer_norm: Some(layer_norm),
decoder: Some(decoder),
decoder_bias: Some(decoder_bias),
})
}
/// Forward pass of MLM head
pub fn forward(&self, hidden_states: &Tensor) -> Result<Tensor> {
let dense_weight = self
.dense
.as_ref()
.ok_or_else(|| TransformerError::architecture("MLM dense layer not initialized"))?;
let dense_bias = self
.dense_bias
.as_ref()
.ok_or_else(|| TransformerError::architecture("MLM dense bias not initialized"))?;
let layer_norm = self
.layer_norm
.as_ref()
.ok_or_else(|| TransformerError::architecture("MLM layer norm not initialized"))?;
let decoder_weight = self
.decoder
.as_ref()
.ok_or_else(|| TransformerError::architecture("MLM decoder not initialized"))?;
let decoder_bias = self
.decoder_bias
.as_ref()
.ok_or_else(|| TransformerError::architecture("MLM decoder bias not initialized"))?;
// Transform hidden states: hidden_states @ dense + dense_bias
let transformed = hidden_states
.matmul(dense_weight)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
let transformed = transformed
.add(dense_bias)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
// Apply GELU activation
let activated = BertIntermediate::gelu_activation(&transformed)?;
// Apply layer normalization
let normalized = layer_norm.forward(&activated)?;
// Project to vocabulary: normalized @ decoder + decoder_bias
let logits = normalized
.matmul(decoder_weight)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
let logits = logits
.add(decoder_bias)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
Ok(logits)
}
/// Compute masked language modeling loss (cross-entropy)
pub fn compute_loss(&self, logits: &Tensor, labels: &Tensor) -> Result<Tensor> {
// logits shape: [batch_size, seq_len, vocab_size]
// labels shape: [batch_size, seq_len]
let batch_size = logits.shape().dims()[0];
let seq_len = logits.shape().dims()[1];
let vocab_size = logits.shape().dims()[2];
// Flatten logits and labels for cross-entropy computation
let logits_flat = logits.reshape([batch_size * seq_len, vocab_size])?;
let labels_flat = labels.reshape([batch_size * seq_len])?;
// Compute softmax probabilities
let logits_data = logits_flat.to_cpu()?;
let labels_data = labels_flat.to_cpu()?;
let mut total_loss = 0.0;
let mut valid_tokens = 0;
// Compute cross-entropy loss for each position
for i in 0..(batch_size * seq_len) {
let label = labels_data[i] as i32;
// Skip positions with -100 label (mask token)
if label == -100 {
continue;
}
valid_tokens += 1;
// Extract logits for this position
let start_idx = i * vocab_size;
let position_logits: Vec<f32> = logits_data[start_idx..start_idx + vocab_size].to_vec();
// Compute softmax for numerical stability
let max_logit = position_logits
.iter()
.fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let exp_logits: Vec<f32> = position_logits
.iter()
.map(|&x| (x - max_logit).exp())
.collect();
let sum_exp: f32 = exp_logits.iter().sum();
// Cross-entropy loss: -log(p_true)
if label >= 0 && (label as usize) < vocab_size {
let prob = exp_logits[label as usize] / sum_exp;
total_loss += -prob.ln();
}
}
// Average loss over valid tokens
let average_loss = if valid_tokens > 0 {
total_loss / valid_tokens as f32
} else {
0.0
};
Tensor::scalar(average_loss, rtx_tensor::DType::F32, logits.device())
.map_err(|e| TransformerError::tensor_op(e.to_string()))
}
}
/// BERT sequence classification head
#[derive(Debug)]
pub struct BertClassificationHead {
pub hidden_size: usize,
pub num_labels: usize,
pub dropout_prob: f64,
/// Classifier dense layer: [`hidden_size`, `num_labels`]
pub classifier: Option<Tensor>,
/// Classifier bias: [`num_labels`]
pub bias: Option<Tensor>,
}
impl BertClassificationHead {
/// Create new classification head
pub fn new(
hidden_size: usize,
num_labels: usize,
dropout_prob: f64,
device: &Device,
) -> Result<Self> {
// Initialize classifier layer with Xavier initialization
let init_std = (2.0 / (hidden_size + num_labels) as f32).sqrt();
let classifier = Tensor::randn(&[hidden_size, num_labels], device)?.mul_scalar(init_std)?;
let bias = Tensor::zeros([num_labels], device)?;
Ok(Self {
hidden_size,
num_labels,
dropout_prob,
classifier: Some(classifier),
bias: Some(bias),
})
}
/// Forward pass of classification head
pub fn forward(&self, pooled_output: &Tensor) -> Result<Tensor> {
let classifier_weight = self
.classifier
.as_ref()
.ok_or_else(|| TransformerError::architecture("Classifier not initialized"))?;
let bias = self
.bias
.as_ref()
.ok_or_else(|| TransformerError::architecture("Classifier bias not initialized"))?;
// Apply dropout (simplified - in practice would check training mode)
// For now, skip dropout during inference
// Apply classifier: pooled_output @ classifier + bias
let logits = pooled_output
.matmul(classifier_weight)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
let logits = logits
.add(bias)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
Ok(logits)
}
/// Compute cross-entropy loss for classification
pub fn compute_loss(&self, logits: &Tensor, labels: &Tensor) -> Result<Tensor> {
// logits shape: [batch_size, num_labels]
// labels shape: [batch_size]
let batch_size = logits.shape().dims()[0];
let num_labels = logits.shape().dims()[1];
let logits_data = logits.to_cpu()?;
let labels_data = labels.to_cpu()?;
let mut total_loss = 0.0;
// Compute cross-entropy loss for each sample
for i in 0..batch_size {
let label = labels_data[i] as usize;
if label >= num_labels {
return Err(TransformerError::invalid_input(format!(
"Label {label} exceeds number of classes {num_labels}"
)));
}
// Extract logits for this sample
let start_idx = i * num_labels;
let sample_logits: Vec<f32> = logits_data[start_idx..start_idx + num_labels].to_vec();
// Compute softmax for numerical stability
let max_logit = sample_logits
.iter()
.fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let exp_logits: Vec<f32> = sample_logits
.iter()
.map(|&x| (x - max_logit).exp())
.collect();
let sum_exp: f32 = exp_logits.iter().sum();
// Cross-entropy loss: -log(p_true)
let prob = exp_logits[label] / sum_exp;
total_loss += -prob.ln();
}
// Average loss over batch
let average_loss = total_loss / batch_size as f32;
Tensor::scalar(average_loss, rtx_tensor::DType::F32, logits.device())
.map_err(|e| TransformerError::tensor_op(e.to_string()))
}
}
/// BERT token classification head
#[derive(Debug)]
pub struct BertTokenClassificationHead {
pub hidden_size: usize,
pub num_labels: usize,
pub dropout_prob: f64,
/// Token classifier dense layer: [`hidden_size`, `num_labels`]
pub classifier: Option<Tensor>,
/// Classifier bias: [`num_labels`]
pub bias: Option<Tensor>,
}
impl BertTokenClassificationHead {
/// Create new token classification head
pub fn new(
hidden_size: usize,
num_labels: usize,
dropout_prob: f64,
device: &Device,
) -> Result<Self> {
// Initialize classifier layer with Xavier initialization
let init_std = (2.0 / (hidden_size + num_labels) as f32).sqrt();
let classifier = Tensor::randn(&[hidden_size, num_labels], device)?.mul_scalar(init_std)?;
let bias = Tensor::zeros([num_labels], device)?;
Ok(Self {
hidden_size,
num_labels,
dropout_prob,
classifier: Some(classifier),
bias: Some(bias),
})
}
/// Forward pass of token classification head
pub fn forward(&self, sequence_output: &Tensor) -> Result<Tensor> {
let classifier_weight = self
.classifier
.as_ref()
.ok_or_else(|| TransformerError::architecture("Token classifier not initialized"))?;
let bias = self.bias.as_ref().ok_or_else(|| {
TransformerError::architecture("Token classifier bias not initialized")
})?;
// Apply dropout (simplified - in practice would check training mode)
// For now, skip dropout during inference
// Apply classifier: sequence_output @ classifier + bias
// sequence_output shape: [batch_size, seq_len, hidden_size]
// classifier shape: [hidden_size, num_labels]
// output shape: [batch_size, seq_len, num_labels]
let logits = sequence_output
.matmul(classifier_weight)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
let logits = logits
.add(bias)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
Ok(logits)
}
/// Compute token-level cross-entropy loss
pub fn compute_loss(
&self,
logits: &Tensor,
labels: &Tensor,
attention_mask: Option<&Tensor>,
) -> Result<Tensor> {
// logits shape: [batch_size, seq_len, num_labels]
// labels shape: [batch_size, seq_len]
// attention_mask shape: [batch_size, seq_len] (optional)
let batch_size = logits.shape().dims()[0];
let seq_len = logits.shape().dims()[1];
let num_labels = logits.shape().dims()[2];
let logits_data = logits.to_cpu()?;
let labels_data = labels.to_cpu()?;
let mask_data = if let Some(mask) = attention_mask {
Some(mask.to_cpu()?)
} else {
None
};
let mut total_loss = 0.0;
let mut valid_tokens = 0;
// Compute cross-entropy loss for each token position
for batch_idx in 0..batch_size {
for seq_idx in 0..seq_len {
let label = labels_data[batch_idx * seq_len + seq_idx] as i32;
// Skip positions with -100 label (ignored tokens)
if label == -100 {
continue;
}
// Skip positions that are masked out
if let Some(ref mask) = mask_data
&& mask[batch_idx * seq_len + seq_idx] == 0.0 {
continue;
}
valid_tokens += 1;
// Extract logits for this position
let start_idx = (batch_idx * seq_len + seq_idx) * num_labels;
let position_logits: Vec<f32> =
logits_data[start_idx..start_idx + num_labels].to_vec();
// Compute softmax for numerical stability
let max_logit = position_logits
.iter()
.fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let exp_logits: Vec<f32> = position_logits
.iter()
.map(|&x| (x - max_logit).exp())
.collect();
let sum_exp: f32 = exp_logits.iter().sum();
// Cross-entropy loss: -log(p_true)
if label >= 0 && (label as usize) < num_labels {
let prob = exp_logits[label as usize] / sum_exp;
total_loss += -prob.ln();
}
}
}
// Average loss over valid tokens
let average_loss = if valid_tokens > 0 {
total_loss / valid_tokens as f32
} else {
0.0
};
Tensor::scalar(average_loss, rtx_tensor::DType::F32, logits.device())
.map_err(|e| TransformerError::tensor_op(e.to_string()))
}
}
/// BERT question answering head for span prediction
#[derive(Debug)]
pub struct BertQAHead {
pub hidden_size: usize,
/// QA classifier: [`hidden_size`, 2] for start/end logits
pub qa_outputs: Option<Tensor>,
/// QA bias: [2]
pub bias: Option<Tensor>,
}
impl BertQAHead {
/// Create new question answering head
pub fn new(hidden_size: usize, device: &Device) -> Result<Self> {
// Initialize QA classifier for start/end positions
let init_std = (2.0 / (hidden_size + 2) as f32).sqrt();
let qa_outputs = Tensor::randn(&[hidden_size, 2], device)?.mul_scalar(init_std)?;
let bias = Tensor::zeros([2], device)?;
Ok(Self {
hidden_size,
qa_outputs: Some(qa_outputs),
bias: Some(bias),
})
}
/// Forward pass to predict start and end logits
pub fn forward(&self, sequence_output: &Tensor) -> Result<(Tensor, Tensor)> {
let qa_weight = self
.qa_outputs
.as_ref()
.ok_or_else(|| TransformerError::architecture("QA outputs not initialized"))?;
let bias = self
.bias
.as_ref()
.ok_or_else(|| TransformerError::architecture("QA bias not initialized"))?;
// Apply QA classifier: sequence_output @ qa_outputs + bias
// sequence_output shape: [batch_size, seq_len, hidden_size]
// qa_outputs shape: [hidden_size, 2]
// output shape: [batch_size, seq_len, 2]
let logits = sequence_output
.matmul(qa_weight)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
let logits = logits
.add(bias)
.map_err(|e| TransformerError::tensor_op(e.to_string()))?;
// Split into start and end logits
let batch_size = logits.shape().dims()[0];
let seq_len = logits.shape().dims()[1];
let logits_data = logits.to_cpu()?;
let mut start_data = Vec::with_capacity(batch_size * seq_len);
let mut end_data = Vec::with_capacity(batch_size * seq_len);
for batch_idx in 0..batch_size {
for seq_idx in 0..seq_len {
let base_idx = (batch_idx * seq_len + seq_idx) * 2;
start_data.push(logits_data[base_idx]);
end_data.push(logits_data[base_idx + 1]);
}
}
let start_logits = Tensor::from_data(start_data, [batch_size, seq_len], logits.device())?;
let end_logits = Tensor::from_data(end_data, [batch_size, seq_len], logits.device())?;
Ok((start_logits, end_logits))
}
}