use crate::compat::Sequential;
use crate::optim::Adam;
use crate::{AutodiffError, Tape, Tensor, Var};
use super::train_support::{check_model_size, cross_entropy_mean, scalar_of, take_updated};
#[derive(Debug, Clone, Copy)]
pub struct TransformerClassifierConfig {
pub seq_len: usize,
pub in_features: usize,
pub embed_dim: usize,
pub num_heads: usize,
pub num_layers: usize,
pub dim_feedforward: usize,
pub mlp_hidden: usize,
pub num_classes: usize,
}
impl TransformerClassifierConfig {
pub fn cifar10(
embed_dim: usize,
num_heads: usize,
num_layers: usize,
num_classes: usize,
) -> Result<Self, AutodiffError> {
let ffn_dim = embed_dim.checked_mul(2).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"TransformerClassifierConfig::cifar10: embed_dim({embed_dim})* 2 が usize の範囲を超える"
))
})?;
Ok(TransformerClassifierConfig {
seq_len: 32,
in_features: 96,
embed_dim,
num_heads,
num_layers,
dim_feedforward: ffn_dim,
mlp_hidden: ffn_dim,
num_classes,
})
}
}
fn sinusoidal_positional_encoding(seq_len: usize, embed_dim: usize) -> Vec<f32> {
let mut out = vec![0.0f32; seq_len * embed_dim];
for pos in 0..seq_len {
for i in 0..embed_dim {
let exponent = 2.0 * (i / 2) as f32 / embed_dim as f32;
let denom = 10000f32.powf(exponent);
let angle = pos as f32 / denom;
out[pos * embed_dim + i] = if i % 2 == 0 { angle.sin() } else { angle.cos() };
}
}
out
}
pub struct TransformerClassifier {
config: TransformerClassifierConfig,
embed: Sequential,
pos_encoding: Tensor<f32>,
encoder: Sequential,
head: Sequential,
}
impl TransformerClassifier {
pub fn new(config: TransformerClassifierConfig, seed: u64) -> Result<Self, AutodiffError> {
if config.seq_len == 0
|| config.in_features == 0
|| config.embed_dim == 0
|| config.num_heads == 0
|| config.num_layers == 0
|| config.dim_feedforward == 0
|| config.mlp_hidden == 0
|| config.num_classes == 0
{
return Err(AutodiffError::InvalidArgument(
"TransformerClassifier::new: config の各フィールドはいずれも 0 より大きい必要がある"
.to_string(),
));
}
if !config.embed_dim.is_multiple_of(config.num_heads) {
return Err(AutodiffError::InvalidArgument(format!(
"TransformerClassifier::new: embed_dim({})は num_heads({})で割り切れる必要がある",
config.embed_dim, config.num_heads
)));
}
let pos_elems = config
.seq_len
.checked_mul(config.embed_dim)
.ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"TransformerClassifier::new: seq_len({})* embed_dim({})が usize の範囲を超える",
config.seq_len, config.embed_dim
))
})?;
const PER_LAYER_OVERHEAD_ELEMS: usize = 4096;
let e = config.embed_dim;
let per_layer = e
.saturating_mul(e)
.saturating_mul(4)
.saturating_add(e.saturating_mul(config.dim_feedforward).saturating_mul(2))
.saturating_add(e.saturating_mul(9))
.saturating_add(config.dim_feedforward)
.saturating_add(PER_LAYER_OVERHEAD_ELEMS);
let estimated = pos_elems
.saturating_add(config.in_features.saturating_mul(e))
.saturating_add(per_layer.saturating_mul(config.num_layers))
.saturating_add(e.saturating_mul(config.mlp_hidden))
.saturating_add(config.mlp_hidden.saturating_mul(config.num_classes));
check_model_size("TransformerClassifier::new", estimated)?;
let embed = Sequential::new().add_linear(config.in_features, config.embed_dim, seed)?;
let pos_encoding_data = sinusoidal_positional_encoding(config.seq_len, config.embed_dim);
let pos_encoding = Tensor::new(pos_encoding_data, &[config.seq_len, config.embed_dim])
.map_err(|e| {
AutodiffError::InvalidArgument(format!(
"TransformerClassifier::new: 位置符号テンソル構築に失敗: {e}"
))
})?;
let mut encoder = Sequential::new();
for i in 0..config.num_layers {
encoder = encoder.add_transformer_encoder(
config.embed_dim,
config.num_heads,
config.dim_feedforward,
seed.wrapping_add(1000 + i as u64),
)?;
}
let head = Sequential::new()
.add_linear(config.embed_dim, config.mlp_hidden, seed.wrapping_add(2000))?
.add_relu()
.add_linear(
config.mlp_hidden,
config.num_classes,
seed.wrapping_add(2001),
)?;
Ok(TransformerClassifier {
config,
embed,
pos_encoding,
encoder,
head,
})
}
pub fn forward<'t>(&self, tape: &'t Tape, x: &Var<'t>) -> Result<Var<'t>, AutodiffError> {
let shape = x.to_tensor().shape().to_vec();
if shape.len() != 3
|| shape[1] != self.config.seq_len
|| shape[2] != self.config.in_features
{
return Err(AutodiffError::InvalidArgument(format!(
"TransformerClassifier::forward: 入力 shape は [N, {}, {}] である必要がある \
(実際: {shape:?})",
self.config.seq_len, self.config.in_features
)));
}
let n = shape[0];
let flat_rows = n.checked_mul(self.config.seq_len).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"TransformerClassifier::forward: n({n})* seq_len({})が usize の範囲を超える",
self.config.seq_len
))
})?;
let flat = x.reshape(&[flat_rows, self.config.in_features])?;
let embedded = self.embed.forward(tape, &flat)?;
let embedded = embedded.reshape(&[n, self.config.seq_len, self.config.embed_dim])?;
let pos_var = tape.var(&self.pos_encoding);
let with_pos = embedded.add(&pos_var)?;
let encoded = self.encoder.forward(tape, &with_pos)?;
let pooled = encoded.mean(Some(1))?;
self.head.forward(tape, &pooled)
}
pub fn predict(&self, x: &Tensor<f32>) -> Result<Tensor<f32>, AutodiffError> {
let tape = crate::tape();
let xv = tape.var(x);
let y = self.forward(&tape, &xv)?;
Ok(y.to_tensor())
}
pub fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
let mut out: Vec<(String, &Tensor<f32>)> = self
.embed
.named_parameters()
.into_iter()
.map(|(k, v)| (format!("embed.{k}"), v))
.collect();
out.extend(
self.encoder
.named_parameters()
.into_iter()
.map(|(k, v)| (format!("encoder.{k}"), v)),
);
out.extend(
self.head
.named_parameters()
.into_iter()
.map(|(k, v)| (format!("head.{k}"), v)),
);
out
}
pub fn set_training(&mut self, training: bool) {
if training {
self.embed.train();
self.encoder.train();
self.head.train();
} else {
self.embed.eval();
self.encoder.eval();
self.head.eval();
}
}
pub fn training(&self) -> bool {
self.embed.training()
}
pub fn train_step(
&mut self,
x: &Tensor<f32>,
y: &Tensor<i32>,
opt: &mut Adam,
) -> Result<f32, AutodiffError> {
let shape = x.shape().to_vec();
if shape.len() != 3
|| shape[1] != self.config.seq_len
|| shape[2] != self.config.in_features
{
return Err(AutodiffError::InvalidArgument(format!(
"TransformerClassifier::train_step: 入力 shape は [N, {}, {}] である必要がある \
(実際: {shape:?})",
self.config.seq_len, self.config.in_features
)));
}
self.set_training(true);
let n = shape[0];
let flat_rows = n.checked_mul(self.config.seq_len).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"TransformerClassifier::train_step: n({n})* seq_len({})が usize の範囲を超える",
self.config.seq_len
))
})?;
let (loss_value, updated) = {
let tape = crate::tape();
let xv = tape.var(x);
let embed_bound = self.embed.bind(&tape);
let encoder_bound = self.encoder.bind(&tape);
let head_bound = self.head.bind(&tape);
let flat = xv.reshape(&[flat_rows, self.config.in_features])?;
let embedded = embed_bound.forward(&tape, &flat)?;
let embedded = embedded.reshape(&[n, self.config.seq_len, self.config.embed_dim])?;
let pos_var = tape.var(&self.pos_encoding);
let with_pos = embedded.add(&pos_var)?;
let encoded = encoder_bound.forward(&tape, &with_pos)?;
let pooled = encoded.mean(Some(1))?;
let logits = head_bound.forward(&tape, &pooled)?;
let loss = cross_entropy_mean(&tape, &logits, y, self.config.num_classes)?;
let loss_value = scalar_of(&loss.to_tensor())?;
let grads = tape.backward(&loss)?;
let mut param_refs: Vec<&Tensor<f32>> = self.embed.trainable_parameters();
param_refs.extend(self.encoder.trainable_parameters());
param_refs.extend(self.head.trainable_parameters());
let mut grad_refs: Vec<&Tensor<f32>> = embed_bound.trainable_grads(&grads)?;
grad_refs.extend(encoder_bound.trainable_grads(&grads)?);
grad_refs.extend(head_bound.trainable_grads(&grads)?);
if param_refs.len() != grad_refs.len() {
return Err(AutodiffError::InvalidArgument(format!(
"TransformerClassifier::train_step: パラメータ数({})と勾配数({})が不一致",
param_refs.len(),
grad_refs.len()
)));
}
let pairs: Vec<(&Tensor<f32>, &Tensor<f32>)> =
param_refs.into_iter().zip(grad_refs).collect();
let updated = opt.step(&pairs)?;
(loss_value, updated)
};
let mut idx = 0usize;
let embed_n = self.embed.trainable_parameters().len();
self.embed
.apply_parameters(take_updated(&updated, &mut idx, embed_n)?)?;
let encoder_n = self.encoder.trainable_parameters().len();
self.encoder
.apply_parameters(take_updated(&updated, &mut idx, encoder_n)?)?;
let head_n = self.head.trainable_parameters().len();
self.head
.apply_parameters(take_updated(&updated, &mut idx, head_n)?)?;
if idx != updated.len() {
return Err(AutodiffError::InvalidArgument(format!(
"TransformerClassifier::train_step: 更新後パラメータ数({})が消費数({idx})と不一致",
updated.len()
)));
}
Ok(loss_value)
}
pub fn config(&self) -> TransformerClassifierConfig {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(seq_len: usize) -> TransformerClassifierConfig {
TransformerClassifierConfig {
seq_len,
in_features: 1,
embed_dim: 1,
num_heads: 1,
num_layers: 1,
dim_feedforward: 1,
mlp_hidden: 1,
num_classes: 1,
}
}
#[test]
fn new_rejects_huge_config_without_panic() {
assert!(matches!(
TransformerClassifier::new(cfg(usize::MAX), 0),
Err(AutodiffError::InvalidArgument(_))
));
assert!(matches!(
TransformerClassifier::new(cfg(1 << 40), 0),
Err(AutodiffError::InvalidArgument(_))
));
let mut c = cfg(4);
c.num_layers = usize::MAX;
assert!(matches!(
TransformerClassifier::new(c, 0),
Err(AutodiffError::InvalidArgument(_))
));
let mut c = cfg(4);
c.num_layers = 1 << 27;
assert!(matches!(
TransformerClassifier::new(c, 0),
Err(AutodiffError::InvalidArgument(_))
));
assert!(TransformerClassifier::new(cfg(4), 0).is_ok());
}
}