🚚 Move files and traits around, extract stuff out of train.rs

main
Shad Amethyst 2 years ago
parent b4a97694a6
commit 920bca4a48

@ -11,7 +11,7 @@ use neuramethyst::prelude::*;
use rand::Rng;
fn main() {
let mut network = neura_network![
let mut network = neura_sequential![
neura_layer!("dense", 2, 8; Relu, NeuraL1(0.001)),
neura_layer!("dropout", 0.25),
neura_layer!("dense", 2; Linear, NeuraL1(0.001)),

@ -5,7 +5,7 @@ use neuramethyst::derivable::loss::Euclidean;
use neuramethyst::prelude::*;
fn main() {
let mut network = neura_network![
let mut network = neura_sequential![
neura_layer!("dense", 2, 4; Relu),
neura_layer!("dense", 3; Relu),
neura_layer!("dense", 1; Relu)

@ -1,8 +1,7 @@
use super::NeuraLayer;
use super::{NeuraLayer, NeuraTrainableLayer};
use crate::{
algebra::NeuraVectorSpace,
derivable::NeuraDerivable,
train::NeuraTrainableLayer,
utils::{multiply_matrix_transpose_vector, multiply_matrix_vector, reverse_dot_product},
};

@ -1,8 +1,6 @@
use rand::Rng;
use crate::train::NeuraTrainableLayer;
use super::NeuraLayer;
use super::{NeuraLayer, NeuraTrainableLayer};
#[derive(Clone, Debug)]
pub struct NeuraDropoutLayer<const LENGTH: usize, R: Rng> {

@ -0,0 +1,39 @@
use super::{NeuraLayer, NeuraTrainableLayer};
/// Represents a layer that has been locked:
/// it won't be modified during training and its weight won't be stored
#[derive(Clone, Debug, PartialEq)]
pub struct NeuraLockLayer<L: NeuraLayer>(pub L);
impl<L: NeuraLayer> NeuraLayer for NeuraLockLayer<L> {
type Input = L::Input;
type Output = L::Output;
fn eval(&self, input: &Self::Input) -> Self::Output {
self.0.eval(input)
}
}
impl<L: NeuraLayer + NeuraTrainableLayer> NeuraTrainableLayer for NeuraLockLayer<L> {
type Delta = ();
#[inline(always)]
fn backpropagate(
&self,
input: &Self::Input,
epsilon: Self::Output,
) -> (Self::Input, Self::Delta) {
(self.0.backpropagate(input, epsilon).0, ())
}
#[inline(always)]
fn regularize(&self) -> Self::Delta {
()
}
#[inline(always)]
fn apply_gradient(&mut self, _gradient: &Self::Delta) {
// Noop
}
}

@ -10,6 +10,11 @@ pub use softmax::NeuraSoftmaxLayer;
mod one_hot;
pub use one_hot::NeuraOneHotLayer;
mod lock;
pub use lock::NeuraLockLayer;
use crate::algebra::NeuraVectorSpace;
pub trait NeuraLayer {
type Input;
type Output;
@ -17,6 +22,42 @@ pub trait NeuraLayer {
fn eval(&self, input: &Self::Input) -> Self::Output;
}
pub trait NeuraTrainableLayer: NeuraLayer {
/// The representation of the layer gradient as a vector space
type Delta: NeuraVectorSpace;
/// Computes the backpropagation term and the derivative of the internal weights,
/// using the `input` vector outputted by the previous layer and the backpropagation term `epsilon` of the next layer.
///
/// Note: we introduce the term `epsilon`, which together with the activation of the current function can be used to compute `delta_l`:
/// ```no_rust
/// f_l'(a_l) * epsilon_l = delta_l
/// ```
///
/// The function should then return a pair `(epsilon_{l-1}, δW_l)`,
/// with `epsilon_{l-1}` being multiplied by `f_{l-1}'(activation)` by the next layer to obtain `delta_{l-1}`.
/// Using this intermediate value for `delta` allows us to isolate it computation to the respective layers.
fn backpropagate(
&self,
input: &Self::Input,
epsilon: Self::Output,
) -> (Self::Input, Self::Delta);
/// Computes the regularization
fn regularize(&self) -> Self::Delta;
/// Applies `δW_l` to the weights of the layer
fn apply_gradient(&mut self, gradient: &Self::Delta);
/// Called before an iteration begins, to allow the layer to set itself up for training.
#[inline(always)]
fn prepare_epoch(&mut self) {}
/// Called at the end of training, to allow the layer to clean itself up
#[inline(always)]
fn cleanup(&mut self) {}
}
#[macro_export]
macro_rules! neura_layer {
( "dense", $( $shape:expr ),*; $activation:expr ) => {
@ -53,4 +94,8 @@ macro_rules! neura_layer {
( "one_hot" ) => {
$crate::layer::NeuraOneHotLayer as $crate::layer::NeuraOneHotLayer<2, _>
};
( "lock", $layer:expr ) => {
$crate::layer::NeuraLockLayer($layer)
};
}

@ -1,6 +1,4 @@
use crate::train::NeuraTrainableLayer;
use super::NeuraLayer;
use super::{NeuraLayer, NeuraTrainableLayer};
/// A special layer that allows you to split a vector into one-hot vectors
#[derive(Debug, Clone, PartialEq)]

@ -1,6 +1,6 @@
use crate::{train::NeuraTrainableLayer, utils::multiply_vectors_pointwise};
use crate::utils::multiply_vectors_pointwise;
use super::NeuraLayer;
use super::{NeuraLayer, NeuraTrainableLayer};
#[non_exhaustive]
#[derive(Clone, Debug)]

@ -15,11 +15,11 @@ pub use utils::{argmax, one_hot};
pub mod prelude {
// Macros
pub use crate::{neura_layer, neura_network};
pub use crate::{neura_layer, neura_sequential};
// Structs and traits
pub use crate::layer::{NeuraDenseLayer, NeuraDropoutLayer, NeuraLayer};
pub use crate::network::{NeuraNetwork, NeuraNetworkTail};
pub use crate::network::sequential::{NeuraSequential, NeuraSequentialTail};
pub use crate::train::{NeuraBackprop, NeuraBatchedTrainer};
pub use crate::utils::cycle_shuffling;
}

@ -0,0 +1,26 @@
use crate::{algebra::NeuraVectorSpace, derivable::NeuraLoss, layer::NeuraLayer};
pub mod sequential;
pub trait NeuraTrainableNetwork: NeuraLayer {
type Delta: NeuraVectorSpace;
fn apply_gradient(&mut self, gradient: &Self::Delta);
/// Should implement the backpropagation algorithm, see `NeuraTrainableLayer::backpropagate` for more information.
fn backpropagate<Loss: NeuraLoss<Input = Self::Output>>(
&self,
input: &Self::Input,
target: &Loss::Target,
loss: Loss,
) -> (Self::Input, Self::Delta);
/// Should return the regularization gradient
fn regularize(&self) -> Self::Delta;
/// Called before an iteration begins, to allow the network to set itself up for training.
fn prepare_epoch(&mut self);
/// Called at the end of training, to allow the network to clean itself up
fn cleanup(&mut self);
}

@ -1,16 +1,26 @@
use crate::{
derivable::NeuraLoss,
layer::NeuraLayer,
train::{NeuraTrainable, NeuraTrainableLayer},
layer::{NeuraLayer, NeuraTrainableLayer},
};
use super::NeuraTrainableNetwork;
#[derive(Clone, Debug)]
pub struct NeuraNetwork<Layer: NeuraLayer, ChildNetwork> {
pub struct NeuraSequential<Layer: NeuraLayer, ChildNetwork> {
pub layer: Layer,
pub child_network: ChildNetwork,
}
impl<Layer: NeuraLayer, ChildNetwork> NeuraNetwork<Layer, ChildNetwork> {
/// Operations on the tail end of a sequential network
pub trait NeuraSequentialTail {
type TailTrimmed;
type TailPushed<T: NeuraLayer>;
fn trim_tail(self) -> Self::TailTrimmed;
fn push_tail<T: NeuraLayer>(self, layer: T) -> Self::TailPushed<T>;
}
impl<Layer: NeuraLayer, ChildNetwork> NeuraSequential<Layer, ChildNetwork> {
pub fn new(layer: Layer, child_network: ChildNetwork) -> Self {
Self {
layer,
@ -29,36 +39,27 @@ impl<Layer: NeuraLayer, ChildNetwork> NeuraNetwork<Layer, ChildNetwork> {
self.child_network
}
pub fn push_front<T: NeuraLayer>(self, layer: T) -> NeuraNetwork<T, Self> {
NeuraNetwork {
pub fn push_front<T: NeuraLayer>(self, layer: T) -> NeuraSequential<T, Self> {
NeuraSequential {
layer: layer,
child_network: self,
}
}
}
/// Operations on the tail end of the network
pub trait NeuraNetworkTail {
type TailTrimmed;
type TailPushed<T: NeuraLayer>;
fn trim_tail(self) -> Self::TailTrimmed;
fn push_tail<T: NeuraLayer>(self, layer: T) -> Self::TailPushed<T>;
}
// Trimming the last layer returns an empty network
impl<Layer: NeuraLayer> NeuraNetworkTail for NeuraNetwork<Layer, ()> {
impl<Layer: NeuraLayer> NeuraSequentialTail for NeuraSequential<Layer, ()> {
type TailTrimmed = ();
type TailPushed<T: NeuraLayer> = NeuraNetwork<Layer, NeuraNetwork<T, ()>>;
type TailPushed<T: NeuraLayer> = NeuraSequential<Layer, NeuraSequential<T, ()>>;
fn trim_tail(self) -> Self::TailTrimmed {
()
}
fn push_tail<T: NeuraLayer>(self, layer: T) -> Self::TailPushed<T> {
NeuraNetwork {
NeuraSequential {
layer: self.layer,
child_network: NeuraNetwork {
child_network: NeuraSequential {
layer,
child_network: (),
},
@ -67,29 +68,29 @@ impl<Layer: NeuraLayer> NeuraNetworkTail for NeuraNetwork<Layer, ()> {
}
// Trimming another layer returns a network which calls trim recursively
impl<Layer: NeuraLayer, ChildNetwork: NeuraNetworkTail> NeuraNetworkTail
for NeuraNetwork<Layer, ChildNetwork>
impl<Layer: NeuraLayer, ChildNetwork: NeuraSequentialTail> NeuraSequentialTail
for NeuraSequential<Layer, ChildNetwork>
{
type TailTrimmed = NeuraNetwork<Layer, <ChildNetwork as NeuraNetworkTail>::TailTrimmed>;
type TailTrimmed = NeuraSequential<Layer, <ChildNetwork as NeuraSequentialTail>::TailTrimmed>;
type TailPushed<T: NeuraLayer> =
NeuraNetwork<Layer, <ChildNetwork as NeuraNetworkTail>::TailPushed<T>>;
NeuraSequential<Layer, <ChildNetwork as NeuraSequentialTail>::TailPushed<T>>;
fn trim_tail(self) -> Self::TailTrimmed {
NeuraNetwork {
NeuraSequential {
layer: self.layer,
child_network: self.child_network.trim_tail(),
}
}
fn push_tail<T: NeuraLayer>(self, layer: T) -> Self::TailPushed<T> {
NeuraNetwork {
NeuraSequential {
layer: self.layer,
child_network: self.child_network.push_tail(layer),
}
}
}
impl<Layer: NeuraLayer> NeuraLayer for NeuraNetwork<Layer, ()> {
impl<Layer: NeuraLayer> NeuraLayer for NeuraSequential<Layer, ()> {
type Input = Layer::Input;
type Output = Layer::Output;
@ -99,7 +100,7 @@ impl<Layer: NeuraLayer> NeuraLayer for NeuraNetwork<Layer, ()> {
}
impl<Layer: NeuraLayer, ChildNetwork: NeuraLayer<Input = Layer::Output>> NeuraLayer
for NeuraNetwork<Layer, ChildNetwork>
for NeuraSequential<Layer, ChildNetwork>
{
type Input = Layer::Input;
@ -110,7 +111,7 @@ impl<Layer: NeuraLayer, ChildNetwork: NeuraLayer<Input = Layer::Output>> NeuraLa
}
}
impl<Layer: NeuraTrainableLayer> NeuraTrainable for NeuraNetwork<Layer, ()> {
impl<Layer: NeuraTrainableLayer> NeuraTrainableNetwork for NeuraSequential<Layer, ()> {
type Delta = Layer::Delta;
fn apply_gradient(&mut self, gradient: &Self::Delta) {
@ -141,8 +142,8 @@ impl<Layer: NeuraTrainableLayer> NeuraTrainable for NeuraNetwork<Layer, ()> {
}
}
impl<Layer: NeuraTrainableLayer, ChildNetwork: NeuraTrainable<Input = Layer::Output>> NeuraTrainable
for NeuraNetwork<Layer, ChildNetwork>
impl<Layer: NeuraTrainableLayer, ChildNetwork: NeuraTrainableNetwork<Input = Layer::Output>>
NeuraTrainableNetwork for NeuraSequential<Layer, ChildNetwork>
{
type Delta = (Layer::Delta, ChildNetwork::Delta);
@ -182,7 +183,7 @@ impl<Layer: NeuraTrainableLayer, ChildNetwork: NeuraTrainable<Input = Layer::Out
}
}
impl<Layer: NeuraLayer> From<Layer> for NeuraNetwork<Layer, ()> {
impl<Layer: NeuraLayer> From<Layer> for NeuraSequential<Layer, ()> {
fn from(layer: Layer) -> Self {
Self {
layer,
@ -191,18 +192,20 @@ impl<Layer: NeuraLayer> From<Layer> for NeuraNetwork<Layer, ()> {
}
}
/// An utility to recursively create a NeuraSequential network, while writing it in a declarative and linear fashion.
/// Note that this can quickly create big and unwieldly types.
#[macro_export]
macro_rules! neura_network {
macro_rules! neura_sequential {
[] => {
()
};
[ $layer:expr $(,)? ] => {
$crate::network::NeuraNetwork::from($layer)
$crate::network::sequential::NeuraSequential::from($layer)
};
[ $first:expr, $($rest:expr),+ $(,)? ] => {
$crate::network::NeuraNetwork::new_match_output($first, neura_network![$($rest),+])
$crate::network::sequential::NeuraSequential::new_match_output($first, neura_sequential![$($rest),+])
};
}
@ -218,22 +221,22 @@ mod test {
fn test_neura_network_macro() {
let mut rng = rand::thread_rng();
let _ = neura_network![
let _ = neura_sequential![
NeuraDenseLayer::from_rng(&mut rng, Relu, NeuraL0) as NeuraDenseLayer<_, _, 8, 16>,
NeuraDenseLayer::from_rng(&mut rng, Relu, NeuraL0) as NeuraDenseLayer<_, _, _, 12>,
NeuraDenseLayer::from_rng(&mut rng, Relu, NeuraL0) as NeuraDenseLayer<_, _, _, 2>
];
let _ = neura_network![
let _ = neura_sequential![
NeuraDenseLayer::from_rng(&mut rng, Relu, NeuraL0) as NeuraDenseLayer<_, _, 8, 16>,
];
let _ = neura_network![
let _ = neura_sequential![
NeuraDenseLayer::from_rng(&mut rng, Relu, NeuraL0) as NeuraDenseLayer<_, _, 8, 16>,
NeuraDenseLayer::from_rng(&mut rng, Relu, NeuraL0) as NeuraDenseLayer<_, _, _, 12>,
];
let _ = neura_network![
let _ = neura_sequential![
neura_layer!("dense", 8, 16; Relu),
neura_layer!("dense", 12; Relu),
neura_layer!("dense", 2; Relu)

@ -1,84 +1,30 @@
use crate::{
algebra::NeuraVectorSpace, derivable::NeuraLoss, layer::NeuraLayer, network::NeuraNetwork,
algebra::NeuraVectorSpace,
derivable::NeuraLoss,
layer::NeuraLayer,
network::{sequential::NeuraSequential, NeuraTrainableNetwork},
};
// TODO: move this trait to layer/mod.rs
pub trait NeuraTrainableLayer: NeuraLayer {
type Delta: NeuraVectorSpace;
/// Computes the backpropagation term and the derivative of the internal weights,
/// using the `input` vector outputted by the previous layer and the backpropagation term `epsilon` of the next layer.
///
/// Note: we introduce the term `epsilon`, which together with the activation of the current function can be used to compute `delta_l`:
/// ```no_rust
/// f_l'(a_l) * epsilon_l = delta_l
/// ```
///
/// The function should then return a pair `(epsilon_{l-1}, δW_l)`,
/// with `epsilon_{l-1}` being multiplied by `f_{l-1}'(activation)` by the next layer to obtain `delta_{l-1}`.
/// Using this intermediate value for `delta` allows us to isolate it computation to the respective layers.
fn backpropagate(
&self,
input: &Self::Input,
epsilon: Self::Output,
) -> (Self::Input, Self::Delta);
/// Computes the regularization
fn regularize(&self) -> Self::Delta;
/// Applies `δW_l` to the weights of the layer
fn apply_gradient(&mut self, gradient: &Self::Delta);
/// Called before an iteration begins, to allow the layer to set itself up for training.
#[inline(always)]
fn prepare_epoch(&mut self) {}
/// Called at the end of training, to allow the layer to clean itself up
#[inline(always)]
fn cleanup(&mut self) {}
}
pub trait NeuraTrainable: NeuraLayer {
type Delta: NeuraVectorSpace;
fn apply_gradient(&mut self, gradient: &Self::Delta);
/// Should implement the backpropagation algorithm, see `NeuraTrainableLayer::backpropagate` for more information.
fn backpropagate<Loss: NeuraLoss<Input = Self::Output>>(
&self,
input: &Self::Input,
target: &Loss::Target,
loss: Loss,
) -> (Self::Input, Self::Delta);
/// Should return the regularization gradient
fn regularize(&self) -> Self::Delta;
/// Called before an iteration begins, to allow the network to set itself up for training.
fn prepare_epoch(&mut self);
/// Called at the end of training, to allow the network to clean itself up
fn cleanup(&mut self);
}
pub trait NeuraGradientSolver<Output, Target = Output> {
fn get_gradient<Layer: NeuraLayer, ChildNetwork>(
&self,
trainable: &NeuraNetwork<Layer, ChildNetwork>,
trainable: &NeuraSequential<Layer, ChildNetwork>,
input: &Layer::Input,
target: &Target,
) -> <NeuraNetwork<Layer, ChildNetwork> as NeuraTrainable>::Delta
) -> <NeuraSequential<Layer, ChildNetwork> as NeuraTrainableNetwork>::Delta
where
NeuraNetwork<Layer, ChildNetwork>: NeuraTrainable<Input = Layer::Input, Output = Output>;
NeuraSequential<Layer, ChildNetwork>:
NeuraTrainableNetwork<Input = Layer::Input, Output = Output>;
fn score<Layer: NeuraLayer, ChildNetwork>(
&self,
trainable: &NeuraNetwork<Layer, ChildNetwork>,
trainable: &NeuraSequential<Layer, ChildNetwork>,
input: &Layer::Input,
target: &Target,
) -> f64
where
NeuraNetwork<Layer, ChildNetwork>: NeuraTrainable<Input = Layer::Input, Output = Output>;
NeuraSequential<Layer, ChildNetwork>:
NeuraTrainableNetwork<Input = Layer::Input, Output = Output>;
}
#[non_exhaustive]
@ -97,24 +43,26 @@ impl<const N: usize, Loss: NeuraLoss<Input = [f64; N]> + Clone>
{
fn get_gradient<Layer: NeuraLayer, ChildNetwork>(
&self,
trainable: &NeuraNetwork<Layer, ChildNetwork>,
trainable: &NeuraSequential<Layer, ChildNetwork>,
input: &Layer::Input,
target: &Loss::Target,
) -> <NeuraNetwork<Layer, ChildNetwork> as NeuraTrainable>::Delta
) -> <NeuraSequential<Layer, ChildNetwork> as NeuraTrainableNetwork>::Delta
where
NeuraNetwork<Layer, ChildNetwork>: NeuraTrainable<Input = Layer::Input, Output = [f64; N]>,
NeuraSequential<Layer, ChildNetwork>:
NeuraTrainableNetwork<Input = Layer::Input, Output = [f64; N]>,
{
trainable.backpropagate(input, target, self.loss.clone()).1
}
fn score<Layer: NeuraLayer, ChildNetwork>(
&self,
trainable: &NeuraNetwork<Layer, ChildNetwork>,
trainable: &NeuraSequential<Layer, ChildNetwork>,
input: &Layer::Input,
target: &Loss::Target,
) -> f64
where
NeuraNetwork<Layer, ChildNetwork>: NeuraTrainable<Input = Layer::Input, Output = [f64; N]>,
NeuraSequential<Layer, ChildNetwork>:
NeuraTrainableNetwork<Input = Layer::Input, Output = [f64; N]>,
{
let output = trainable.eval(&input);
self.loss.eval(target, &output)
@ -183,11 +131,12 @@ impl NeuraBatchedTrainer {
>(
&self,
gradient_solver: GradientSolver,
network: &mut NeuraNetwork<Layer, ChildNetwork>,
network: &mut NeuraSequential<Layer, ChildNetwork>,
inputs: Inputs,
test_inputs: &[(Layer::Input, Target)],
) where
NeuraNetwork<Layer, ChildNetwork>: NeuraTrainable<Input = Layer::Input, Output = Output>,
NeuraSequential<Layer, ChildNetwork>:
NeuraTrainableNetwork<Input = Layer::Input, Output = Output>,
Layer::Input: Clone,
{
let mut iter = inputs.into_iter();
@ -197,10 +146,10 @@ impl NeuraBatchedTrainer {
// Contains `momentum_factor * factor * gradient_sum_previous_iter`
let mut previous_gradient_sum =
<NeuraNetwork<Layer, ChildNetwork> as NeuraTrainable>::Delta::zero();
<NeuraSequential<Layer, ChildNetwork> as NeuraTrainableNetwork>::Delta::zero();
'd: for iteration in 0..self.iterations {
let mut gradient_sum =
<NeuraNetwork<Layer, ChildNetwork> as NeuraTrainable>::Delta::zero();
<NeuraSequential<Layer, ChildNetwork> as NeuraTrainableNetwork>::Delta::zero();
network.prepare_epoch();
for _ in 0..self.batch_size {
@ -249,16 +198,18 @@ mod test {
assert_approx,
derivable::{activation::Linear, loss::Euclidean, regularize::NeuraL0},
layer::NeuraDenseLayer,
network::NeuraNetworkTail,
neura_network,
network::sequential::NeuraSequentialTail,
neura_sequential,
};
#[test]
fn test_backpropagation_simple() {
for wa in [0.0, 0.25, 0.5, 1.0] {
for wb in [0.0, 0.25, 0.5, 1.0] {
let network =
NeuraNetwork::new(NeuraDenseLayer::new([[wa, wb]], [0.0], Linear, NeuraL0), ());
let network = NeuraSequential::new(
NeuraDenseLayer::new([[wa, wb]], [0.0], Linear, NeuraL0),
(),
);
let gradient =
NeuraBackprop::new(Euclidean).get_gradient(&network, &[1.0, 1.0], &[0.0]);
@ -274,7 +225,7 @@ mod test {
fn test_backpropagation_complex() {
const EPSILON: f64 = 0.00001;
// Test that we get the same values as https://hmkcode.com/ai/backpropagation-step-by-step/
let network = neura_network![
let network = neura_sequential![
NeuraDenseLayer::new([[0.11, 0.21], [0.12, 0.08]], [0.0; 2], Linear, NeuraL0),
NeuraDenseLayer::new([[0.14, 0.15]], [0.0], Linear, NeuraL0)
];

Loading…
Cancel
Save