use crate::error::Error;
use crate::neural_network::Tensor;
use crate::neural_network::layers::TrainingParameters;
use crate::neural_network::layers::layer_weight::{EmbeddingLayerWeight, LayerWeight};
use crate::neural_network::layers::validation::validate_weight_shape;
use crate::neural_network::traits::{Layer, ParamGrad};
use crate::parallel_gates::cheap_map_parallel_threshold;
use ndarray::{Array, Array2, IxDyn};
use ndarray_rand::{RandomExt, rand_distr::Uniform};
use rayon::prelude::*;
use std::borrow::Cow;
const INIT_LIMIT: f32 = 0.05;
const TASK_ELEMENTS: usize = 16_384;
#[derive(Debug)]
pub struct Embedding {
input_dim: usize,
output_dim: usize,
embeddings: Array2<f32>,
index_cache: Option<Vec<usize>>,
input_shape: Option<Vec<usize>>,
grad_embeddings: Option<Array2<f32>>,
}
impl Embedding {
pub fn new(input_dim: usize, output_dim: usize) -> Result<Self, Error> {
if input_dim == 0 {
return Err(Error::invalid_parameter(
"input_dim",
"is 0, and the table must hold at least 1 row",
));
}
if output_dim == 0 {
return Err(Error::invalid_parameter(
"output_dim",
"is 0, and an embedding vector must hold at least 1 element",
));
}
Ok(Self {
input_dim,
output_dim,
embeddings: Self::init_table(input_dim, output_dim, None),
index_cache: None,
input_shape: None,
grad_embeddings: None,
})
}
pub fn with_random_state(mut self, random_state: u64) -> Self {
self.embeddings = Self::init_table(self.input_dim, self.output_dim, Some(random_state));
self
}
fn init_table(input_dim: usize, output_dim: usize, random_state: Option<u64>) -> Array2<f32> {
let mut rng = crate::random::make_rng(random_state);
Array::random_using(
(input_dim, output_dim),
Uniform::new(-INIT_LIMIT, INIT_LIMIT).unwrap(),
&mut rng,
)
}
pub fn set_weights(&mut self, embeddings: Array2<f32>) -> Result<(), Error> {
validate_weight_shape("embeddings", self.embeddings.shape(), embeddings.shape())?;
self.embeddings = embeddings.as_standard_layout().into_owned();
Ok(())
}
fn to_indices(&self, input: &Tensor) -> Result<Vec<usize>, Error> {
if input.ndim() == 0 {
return Err(Error::invalid_input(
"Embedding layer expects an input of rank 1 or more, got a scalar tensor",
));
}
if input.is_empty() {
return Err(Error::empty_input("input tensor"));
}
let mut indices = Vec::with_capacity(input.len());
for &value in input.iter() {
let index = value as usize;
if value.is_nan() || value <= -1.0 || index >= self.input_dim {
return Err(Error::invalid_input(format!(
"Embedding layer received the index {}, and every index must truncate into \
0..{}",
value, self.input_dim
)));
}
indices.push(index);
}
Ok(indices)
}
fn gather(&self, indices: &[usize], input_shape: &[usize]) -> Tensor {
let width = self.output_dim;
let elements = indices.len() * width;
let table = self
.embeddings
.as_slice()
.expect("the table is kept in C order");
let data = if elements >= cheap_map_parallel_threshold() {
let mut data = vec![0.0f32; elements];
let rows_per_task = (TASK_ELEMENTS / width).max(1);
data.par_chunks_mut(rows_per_task * width)
.enumerate()
.for_each(|(task, block)| {
let first_row = task * rows_per_task;
for (row, destination) in block.chunks_mut(width).enumerate() {
let index = indices[first_row + row];
destination.copy_from_slice(&table[index * width..(index + 1) * width]);
}
});
data
} else {
let mut data = Vec::with_capacity(elements);
for &index in indices {
data.extend_from_slice(&table[index * width..(index + 1) * width]);
}
data
};
let mut shape = input_shape.to_vec();
shape.push(width);
Tensor::from_shape_vec(IxDyn(&shape), data).expect("the shape matches the data")
}
}
impl Layer for Embedding {
fn forward(&mut self, input: &Tensor) -> Result<Tensor, Error> {
let indices = self.to_indices(input)?;
let output = self.gather(&indices, input.shape());
self.index_cache = Some(indices);
self.input_shape = Some(input.shape().to_vec());
Ok(output)
}
fn predict(&self, input: &Tensor) -> Result<Tensor, Error> {
let indices = self.to_indices(input)?;
Ok(self.gather(&indices, input.shape()))
}
fn backward(&mut self, grad_output: &Tensor) -> Result<Tensor, Error> {
let Self {
input_dim,
output_dim,
index_cache,
input_shape,
grad_embeddings,
..
} = self;
let (Some(indices), Some(input_shape)) = (index_cache.as_ref(), input_shape.as_ref())
else {
return Err(Error::forward_pass_not_run("Embedding"));
};
let mut expected = input_shape.clone();
expected.push(*output_dim);
if grad_output.shape() != expected.as_slice() {
return Err(Error::shape_mismatch(expected, grad_output.shape()));
}
let grad = grad_embeddings.get_or_insert_with(|| Array2::zeros((*input_dim, *output_dim)));
grad.fill(0.0);
let table = grad
.as_slice_mut()
.expect("the gradient buffer is kept in C order");
let source = grad_output.as_standard_layout();
let source = source
.as_slice()
.expect("as_standard_layout gives a C-order array");
let width = *output_dim;
for (row, &index) in source.chunks_exact(width).zip(indices) {
let slot = &mut table[index * width..(index + 1) * width];
for (accumulator, &value) in slot.iter_mut().zip(row) {
*accumulator += value;
}
}
Ok(Tensor::zeros(IxDyn(input_shape)))
}
fn layer_type(&self) -> &str {
"Embedding"
}
fn output_shape(&self) -> String {
match &self.input_shape {
Some(shape) => {
let mut axes: Vec<String> = shape[1..].iter().map(|e| e.to_string()).collect();
axes.push(self.output_dim.to_string());
format!("(None, {})", axes.join(", "))
}
None => "Unknown".to_string(),
}
}
fn param_count(&self) -> TrainingParameters {
TrainingParameters::Trainable(self.input_dim * self.output_dim)
}
fn parameters(&mut self) -> Vec<ParamGrad<'_>> {
let Self {
embeddings,
grad_embeddings,
..
} = self;
let mut params = Vec::new();
if let Some(grad) = grad_embeddings.as_ref() {
params.push(ParamGrad::weight(
embeddings
.as_slice_mut()
.expect("the table is kept in C order"),
grad.as_slice()
.expect("the gradient buffer is kept in C order"),
));
}
params
}
fn get_weights(&self) -> LayerWeight<'_> {
LayerWeight::Embedding(EmbeddingLayerWeight {
embeddings: Cow::Borrowed(&self.embeddings),
})
}
}