├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── benches └── my_benchmark.rs ├── examples ├── cifar10.rs ├── fortress.rs ├── mnist.rs ├── tictactoe.rs └── xor.rs ├── rustfmt.toml └── src ├── lib.rs ├── network ├── functional │ ├── activation_layer │ │ ├── leakyrelu.rs │ │ ├── mod.rs │ │ ├── relu.rs │ │ ├── sigmoid.rs │ │ └── softmax.rs │ ├── error │ │ ├── bce.rs │ │ ├── cce.rs │ │ ├── error_trait.rs │ │ ├── mod.rs │ │ ├── mse.rs │ │ ├── noop.rs │ │ └── rmse.rs │ ├── functional_trait.rs │ └── mod.rs ├── layer │ ├── convolution │ │ ├── conv_utils.rs │ │ ├── convolution2d.rs │ │ └── mod.rs │ ├── dense.rs │ ├── dropout.rs │ ├── flatten.rs │ ├── layer_trait.rs │ ├── mod.rs │ └── reshape.rs ├── mod.rs ├── nn.rs ├── optimizer │ ├── adagrad.rs │ ├── adam.rs │ ├── mod.rs │ ├── momentum.rs │ ├── noop.rs │ ├── optimizer_trait.rs │ └── rmsprop.rs └── tests.rs └── rl ├── agent ├── agent_trait.rs ├── ddql_agent.rs ├── dql_agent.rs ├── human_player.rs ├── mod.rs ├── ql_agent.rs ├── random_agent.rs └── results.rs ├── algorithms ├── dq_learning.rs ├── mod.rs ├── observation.rs ├── q_learning.rs ├── replay_buffer.rs └── utils.rs ├── env ├── env_trait.rs ├── fortress.rs ├── mod.rs └── tictactoe.rs ├── mod.rs └── training ├── mod.rs ├── trainer.rs └── utils.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Run fmt 20 | run: cargo fmt --all -- --check 21 | - name: Build 22 | run: cargo build --verbose 23 | - name: Run tests 24 | run: cargo test --verbose 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /data 5 | **/data 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | *.swp 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [lib] 2 | [package] 3 | name = "rust-rl" 4 | version = "0.1.0" 5 | authors = ["zuse"] 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [profile.release] 11 | debug = true 12 | 13 | [dependencies] 14 | rand = "0.8" 15 | ndarray-rand = "0.13" 16 | ndarray-stats = "0.4" 17 | 18 | [features] 19 | default = [] 20 | download = ["datasets/download"] 21 | 22 | [dependencies.datasets] 23 | git = "https://github.com/ZuseZ4/Datasets" 24 | branch = "master" 25 | default-features = false 26 | 27 | [dependencies.ndarray] 28 | version = "0.14" 29 | features = ["rayon"] 30 | 31 | [dev-dependencies] 32 | criterion = "0.3" 33 | 34 | [[bench]] 35 | name = "my_benchmark" 36 | harness = false 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust_RL 2 | 3 | This is a test repository to learn about Rust, Neural Networks and Reinforcement Learning. 4 | The Neural Network implementations have some real basic optimizations and the forward pass supports parallelization. 5 | However, it comes with some design-flawes and it is significantly limited by not supporting GPUs and not supporting any kind of autodiff. 6 | 7 | # FUNCTIONALITY 8 | 1) The following layers have been implemented: Dense, Dropout, Flatten, Reshape 9 | 2) Convolution_Layer (weight updates work, but don't stack them) 10 | 3) The following activation functions have been implemented: Softmax, Sigmoid, ReLu, LeakyReLu 11 | 4) The following loss functions have been implemented: MSE, RMSE, binary_crossentropy, categorical_crossentropy 12 | 5) The following optimizer have been implemented: SGD (default), Momentum, AdaGrad, RMSProp, Adam 13 | 6) Networks work for 1d, 2d, or 3d input. Exact input shape has to be given, following layers adjust accordingly. 14 | 7) Available Datasets: Mnist(-Fashion), Cifar10, Cifar100 15 | 8) Available Agents: Random, Q-Learning, DQN, Double-DQN 16 | 9) Available Environments: TicTacToe, Fortress (https://www.c64-wiki.com/wiki/Fortress_(SSI) 17 | 18 | # EXAMPLES 19 | MNIST (achieving ~98%) 20 | CIFAR10 (achieving ~49%) 21 | 22 | TicTacToe (results in an optimal Agent) 23 | Fortress (... at least DDQN performs significantly better than a random moving bot) 24 | 25 | 26 | # TODO 27 | 1) Add backpropagation of error to conv_layers 28 | 2) Improve design 29 | 3) Add GPU support for matrix-matrix multiplications 30 | 4) Add some autodiff support. 31 | 32 | At least the last two TODOs will probably stay for some time. 33 | -------------------------------------------------------------------------------- /benches/my_benchmark.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use hello_rust::engine::{ai_engine, random_engine}; 3 | use hello_rust::game::fortress::Game; 4 | 5 | use criterion::{criterion_group, criterion_main, Criterion}; 6 | 7 | pub fn criterion_benchmark_rand(c: &mut Criterion) { 8 | c.bench_function("random games", |b| b.iter(|| main_rand())); 9 | } 10 | 11 | pub fn criterion_benchmark_ai_rand(c: &mut Criterion) { 12 | c.bench_function("ai vs random games", |b| b.iter(|| main_ai_rand())); 13 | } 14 | 15 | pub fn criterion_benchmark_ai(c: &mut Criterion) { 16 | c.bench_function("ai vs ai games", |b| b.iter(|| main_ai())); 17 | } 18 | 19 | pub fn criterion_benchmark_train(c: &mut Criterion) { 20 | c.bench_function("train only", |b| b.iter(|| main_train())); 21 | } 22 | 23 | criterion_group!( 24 | benches, 25 | criterion_benchmark_rand, 26 | criterion_benchmark_ai_rand, 27 | criterion_benchmark_ai, 28 | criterion_benchmark_train 29 | ); 30 | criterion_main!(benches); 31 | 32 | pub fn main_rand() -> Result<(), String> { 33 | let rounds: u8 = 25; 34 | let engines: u8 = 11; 35 | let bench_games: u64 = 100; 36 | 37 | let mut game = Game::new(rounds, engines)?; 38 | game.bench(bench_games); 39 | Ok(()) 40 | } 41 | 42 | pub fn main_ai_rand() -> Result<(), String> { 43 | let rounds: u8 = 25; 44 | let engines: u8 = 21; 45 | let train_games: u64 = 50; 46 | let bench_games: u64 = 100; 47 | 48 | let mut game = Game::new(rounds, engines)?; 49 | game.train(train_games); 50 | game.bench(bench_games); 51 | Ok(()) 52 | } 53 | 54 | pub fn main_ai() -> Result<(), String> { 55 | let rounds: u8 = 25; 56 | let engines: u8 = 22; 57 | let train_games: u64 = 50; 58 | let bench_games: u64 = 100; 59 | 60 | let mut game = Game::new(rounds, engines)?; 61 | game.train(train_games); 62 | game.bench(bench_games); 63 | Ok(()) 64 | } 65 | 66 | pub fn main_train() -> Result<(), String> { 67 | let rounds: u8 = 25; 68 | let engines: u8 = 22; 69 | let train_games: u64 = 50; 70 | 71 | let mut game = Game::new(rounds, engines)?; 72 | game.train(train_games); 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /examples/cifar10.rs: -------------------------------------------------------------------------------- 1 | use datasets::cifar10; 2 | use ndarray::{Array2, Array4, Axis}; 3 | use rand::Rng; 4 | use rust_rl::network::nn::NeuralNetwork; 5 | use std::time::Instant; 6 | 7 | fn new() -> NeuralNetwork { 8 | let mut nn = NeuralNetwork::new3d((3, 32, 32), "cce".to_string(), "adam".to_string()); 9 | nn.set_batch_size(32); 10 | nn.set_learning_rate(0.1); 11 | nn.add_convolution((3, 3), 10, 1); 12 | nn.add_activation("sigmoid"); 13 | nn.add_dropout(0.4); 14 | nn.add_flatten(); 15 | nn.add_dense(10); //Dense with 10 output neuron 16 | nn.add_activation("softmax"); 17 | nn 18 | } 19 | 20 | fn test(nn: &mut NeuralNetwork, input: &Array4, feedback: &Array2) { 21 | nn.test(input.clone().into_dyn(), feedback.clone()); 22 | } 23 | 24 | fn train(nn: &mut NeuralNetwork, num: usize, input: &Array4, fb: &Array2) { 25 | let mut rng = rand::thread_rng(); 26 | for _ in 0..num { 27 | let pos = rng.gen_range(0..input.shape()[0]) as usize; 28 | let current_input = input.index_axis(Axis(0), pos).into_owned(); 29 | let current_fb = fb.index_axis(Axis(0), pos).into_owned(); 30 | nn.train3d(current_input, current_fb); 31 | } 32 | } 33 | 34 | pub fn main() { 35 | let (train_size, test_size, depth, rows, cols) = (50_000, 10_000, 3, 32, 32); 36 | 37 | #[cfg(feature = "download")] 38 | cifar10::download_and_extract(); 39 | let cifar10::Data { 40 | trn_img, 41 | trn_lbl, 42 | tst_img, 43 | tst_lbl, 44 | .. 45 | } = cifar10::new_normalized(); 46 | assert_eq!(trn_img.shape(), &[train_size, depth, rows, cols]); 47 | assert_eq!(tst_img.shape(), &[test_size, depth, rows, cols]); 48 | 49 | // Get the image of the first digit. 50 | let first_image = trn_img.index_axis(Axis(0), 0); 51 | assert_eq!(first_image.shape(), &[3, 32, 32]); 52 | 53 | let mut nn = new(); 54 | nn.print_setup(); 55 | train(&mut nn, train_size, &trn_img, &trn_lbl); 56 | let start = Instant::now(); 57 | for i in 0..10 { 58 | print!("{}: ", i + 1); 59 | test(&mut nn, &tst_img, &tst_lbl); 60 | } 61 | let stop = Instant::now(); 62 | let duration = stop.duration_since(start); 63 | println!( 64 | "Trained for {},{} seconds.", 65 | duration.as_secs(), 66 | duration.subsec_millis() 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /examples/fortress.rs: -------------------------------------------------------------------------------- 1 | use agent::*; 2 | use env::Fortress; 3 | use rust_rl::network::nn::NeuralNetwork; 4 | use rust_rl::rl::{agent, env, training}; 5 | use std::io; 6 | use training::{utils, Trainer}; 7 | 8 | fn new(learning_rate: f32, batch_size: usize) -> NeuralNetwork { 9 | let mut nn = NeuralNetwork::new3d((1, 6, 6), "mse".to_string(), "adam".to_string()); 10 | nn.set_batch_size(batch_size); 11 | nn.set_learning_rate(learning_rate); 12 | //nn.add_convolution((3, 3), 32, 0); 13 | //nn.add_activation("sigmoid"); 14 | //nn.add_convolution((3, 3), 32, 0); 15 | //nn.add_activation("sigmoid"); 16 | nn.add_flatten(); 17 | nn.add_dense(100); 18 | nn.add_activation("sigmoid"); 19 | nn.add_dense(50); 20 | nn.add_activation("sigmoid"); 21 | nn.add_dense(36); 22 | //nn.add_activation("sigmoid"); 23 | nn.print_setup(); 24 | nn 25 | } 26 | 27 | pub fn main() { 28 | let mut auto_fill = String::new(); 29 | println!("Run with default parameters? (y/n)"); 30 | io::stdin() 31 | .read_line(&mut auto_fill) 32 | .expect("Failed to read y or no!"); 33 | let auto_fill: String = auto_fill.trim().parse().expect("Please type y or no!"); 34 | 35 | let ((train_games, bench_games, iterations), rounds, agent_numbers) = match auto_fill.as_str() { 36 | "y" => ((5000, 1000, 5), 25, vec![2, 2]), 37 | "n" => ( 38 | utils::read_game_numbers(), 39 | utils::read_rounds_per_game(), 40 | utils::read_agents(2), 41 | ), 42 | _ => panic!("please only answer y or n!"), 43 | }; 44 | let agents = get_agents(agent_numbers).unwrap(); 45 | 46 | let game = Fortress::new(rounds as usize); 47 | 48 | let mut trainer = Trainer::new(Box::new(game), agents, true).unwrap(); 49 | trainer.train_bench_loops(train_games, bench_games, iterations); 50 | } 51 | 52 | fn get_agents(agent_nums: Vec) -> Result>, String> { 53 | let mut res: Vec> = vec![]; 54 | let batch_size = 16; 55 | for agent_num in agent_nums { 56 | let new_agent: Result, String> = match agent_num { 57 | 0 => Ok(Box::new(DDQLAgent::new( 58 | 1., 59 | batch_size, 60 | new(1e-4, batch_size), 61 | ))), 62 | 1 => Ok(Box::new(DQLAgent::new( 63 | 1., 64 | batch_size, 65 | new(0.001, batch_size), 66 | ))), 67 | 2 => Ok(Box::new(QLAgent::new(1., 0.1, 6 * 6))), 68 | 3 => Ok(Box::new(RandomAgent::new())), 69 | 4 => Ok(Box::new(HumanPlayer::new())), 70 | _ => Err("Only implemented agents 1-4!".to_string()), 71 | }; 72 | res.push(new_agent?); 73 | } 74 | Ok(res) 75 | } 76 | -------------------------------------------------------------------------------- /examples/mnist.rs: -------------------------------------------------------------------------------- 1 | use datasets::mnist; 2 | use ndarray::{Array2, Array4, Axis}; 3 | use rand::Rng; 4 | use rust_rl::network::nn::NeuralNetwork; 5 | use std::time::Instant; 6 | 7 | fn new() -> NeuralNetwork { 8 | let mut nn = NeuralNetwork::new3d((1, 28, 28), "cce".to_string(), "adam".to_string()); 9 | nn.set_batch_size(32); 10 | nn.set_learning_rate(1e-3); 11 | nn.add_convolution((3, 3), 10, 1); 12 | nn.add_flatten(); 13 | nn.add_activation("sigmoid"); 14 | nn.add_dropout(0.5); 15 | nn.add_dense(10); 16 | nn.add_activation("softmax"); 17 | nn 18 | } 19 | 20 | fn test(nn: &mut NeuralNetwork, input: &Array4, feedback: &Array2) { 21 | nn.test(input.clone().into_dyn(), feedback.clone()); 22 | } 23 | 24 | fn train(nn: &mut NeuralNetwork, num: usize, input: &Array4, fb: &Array2) { 25 | for _ in 0..num { 26 | let pos = rand::thread_rng().gen_range(0..input.shape()[0]) as usize; 27 | let current_input = input.index_axis(Axis(0), pos).into_owned(); 28 | let current_fb = fb.index_axis(Axis(0), pos).into_owned(); 29 | nn.train3d(current_input, current_fb); 30 | } 31 | } 32 | 33 | pub fn main() { 34 | let (train_size, test_size, rows, cols) = (60_000, 10_000, 28, 28); 35 | 36 | #[cfg(feature = "download")] 37 | mnist::download_and_extract(); 38 | let mnist::Data { 39 | trn_img, 40 | trn_lbl, 41 | tst_img, 42 | tst_lbl, 43 | .. 44 | } = mnist::new_normalized(); 45 | let trn_img = trn_img.into_shape((train_size, 1, rows, cols)).unwrap(); 46 | let tst_img = tst_img.into_shape((test_size, 1, rows, cols)).unwrap(); 47 | 48 | assert_eq!(trn_img.shape(), &[train_size, 1, rows, cols]); 49 | assert_eq!(tst_img.shape(), &[test_size, 1, rows, cols]); 50 | 51 | // Get the image of the first digit. 52 | let first_image = trn_img.index_axis(Axis(0), 0); 53 | assert_eq!(first_image.shape(), &[1, 28, 28]); 54 | 55 | let mut nn = new(); 56 | nn.print_setup(); 57 | train(&mut nn, 60_000, &trn_img, &trn_lbl); //60_000 58 | let start = Instant::now(); 59 | for i in 0..20 { 60 | print!("{}: ", i + 1); 61 | test(&mut nn, &tst_img, &tst_lbl); 62 | } 63 | let stop = Instant::now(); 64 | let duration = stop.duration_since(start); 65 | println!( 66 | "Trained for {},{} seconds.", 67 | duration.as_secs(), 68 | duration.subsec_millis() 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /examples/tictactoe.rs: -------------------------------------------------------------------------------- 1 | use agent::*; 2 | use env::TicTacToe; 3 | use rust_rl::network::nn::NeuralNetwork; 4 | use rust_rl::rl::{agent, env, training}; 5 | use std::io; 6 | use training::{utils, Trainer}; 7 | 8 | fn new(learning_rate: f32, batch_size: usize) -> NeuralNetwork { 9 | let mut nn = NeuralNetwork::new3d((1, 3, 3), "mse".to_string(), "adam".to_string()); 10 | nn.set_batch_size(batch_size); 11 | nn.set_learning_rate(learning_rate); 12 | nn.add_flatten(); 13 | nn.add_dense(100); 14 | nn.add_activation("sigmoid"); 15 | nn.add_dense(30); 16 | nn.add_activation("sigmoid"); 17 | nn.add_dense(9); 18 | //nn.add_activation("sigmoid"); 19 | nn.print_setup(); 20 | nn 21 | } 22 | 23 | pub fn main() { 24 | let mut auto_fill = String::new(); 25 | println!("Run with default parameters? (y/n)"); 26 | io::stdin() 27 | .read_line(&mut auto_fill) 28 | .expect("Failed to read y or n!"); 29 | let auto_fill: String = auto_fill.trim().parse().expect("Please type y or n!"); 30 | 31 | let ((train_games, bench_games, iterations), agent_numbers) = match auto_fill.as_str() { 32 | "y" => ((40_000, 10_000, 5), vec![0, 2]), 33 | "n" => (utils::read_game_numbers(), utils::read_agents(2)), 34 | _ => panic!("please only answer y or n!"), 35 | }; 36 | 37 | let agents = get_agents(agent_numbers).unwrap(); 38 | let game = TicTacToe::new(); 39 | 40 | let mut trainer = Trainer::new(Box::new(game), agents, true).unwrap(); 41 | trainer.train_bench_loops(train_games, bench_games, iterations); 42 | } 43 | 44 | fn get_agents(agent_nums: Vec) -> Result>, String> { 45 | let mut res: Vec> = vec![]; 46 | let batch_size = 8; 47 | for agent_num in agent_nums { 48 | let new_agent: Result, String> = match agent_num { 49 | 0 => Ok(Box::new(DDQLAgent::new( 50 | 1., 51 | batch_size, 52 | new(3e-4, batch_size), 53 | ))), 54 | 1 => Ok(Box::new(DQLAgent::new( 55 | 1., 56 | batch_size, 57 | new(0.0003, batch_size), 58 | ))), 59 | 2 => Ok(Box::new(QLAgent::new(1., 0.1, 3 * 3))), 60 | 3 => Ok(Box::new(RandomAgent::new())), 61 | 4 => Ok(Box::new(HumanPlayer::new())), 62 | _ => Err("Only implemented agents 1-4!".to_string()), 63 | }; 64 | res.push(new_agent?); 65 | } 66 | Ok(res) 67 | } 68 | -------------------------------------------------------------------------------- /examples/xor.rs: -------------------------------------------------------------------------------- 1 | use ndarray::{array, Array2}; 2 | use rand::Rng; 3 | use rust_rl::network::nn::NeuralNetwork; 4 | 5 | fn new() -> NeuralNetwork { 6 | let mut nn = NeuralNetwork::new1d(2, "none".to_string(), "adam".to_string()); 7 | nn.set_batch_size(4); 8 | nn.set_learning_rate(0.01); 9 | nn.add_dense(2); //Dense with 2 output neurons 10 | nn.add_activation("sigmoid"); 11 | nn.add_dense(2); 12 | nn.add_activation("softmax"); 13 | //nn.add_dense(1); //Dense with 1 output neuron 14 | //nn.add_activation("sigmoid"); //Sigmoid 15 | nn 16 | } 17 | 18 | fn test(nn: &mut NeuralNetwork, input: &Array2, feedback: &Array2) { 19 | for i in 0..4 { 20 | println!("input: {}, feedback: {}", input.row(i), feedback.row(i)); 21 | let pred = nn.predict1d(input.row(i).into_owned()); 22 | println!("output: {}", pred); 23 | } 24 | println!(); 25 | } 26 | 27 | fn train(nn: &mut NeuralNetwork, num_games: usize, input: &Array2, feedback: &Array2) { 28 | for _ in 0..num_games { 29 | let move_number = rand::thread_rng().gen_range(0..input.nrows()) as usize; 30 | let current_input = input.row(move_number).into_owned().clone(); 31 | let current_feedback = feedback.row(move_number).into_owned().clone(); 32 | nn.train1d(current_input, current_feedback); 33 | } 34 | } 35 | 36 | pub fn main() { 37 | let input = array![[0., 0.], [0., 1.], [1., 0.], [1., 1.]]; // XOR 38 | let feedback = array![[0., 1.], [1., 0.], [1., 0.], [0., 1.]]; //First works good with 200k examples 39 | //let feedback = array![[0.],[1.],[1.],[0.]]; //First works good with 200k examples 40 | let mut nn = new(); 41 | nn.print_setup(); 42 | 43 | for _ in 0..10 { 44 | train(&mut nn, 20_000, &input, &feedback); 45 | test(&mut nn, &input, &feedback); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | #defaults are fine 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! A crate including two submodules for neural networks and reinforcement learning. 4 | 5 | /// A submodule including everything to build a neural network. 6 | /// 7 | /// Currently multiple layers, error functions, and optimizers are provided. 8 | pub mod network; 9 | 10 | /// A submodule including everything to run reinforcement learning tasks. 11 | /// 12 | /// This submodule includes two interfaces for environments and agents. 13 | /// One environment and multiple agents are provided as example. 14 | /// A training submodule is available for convenience. 15 | pub mod rl; 16 | -------------------------------------------------------------------------------- /src/network/functional/activation_layer/leakyrelu.rs: -------------------------------------------------------------------------------- 1 | use crate::network::functional::Functional; 2 | use crate::network::layer::Layer; 3 | use ndarray::ArrayD; 4 | 5 | /// A leaky-relu layer. 6 | #[derive(Default)] 7 | pub struct LeakyReLuLayer {} 8 | 9 | impl LeakyReLuLayer { 10 | /// No parameters are possible. 11 | pub fn new() -> Self { 12 | LeakyReLuLayer {} 13 | } 14 | } 15 | 16 | impl Functional for LeakyReLuLayer {} 17 | 18 | impl Layer for LeakyReLuLayer { 19 | fn get_type(&self) -> String { 20 | "LeakyReLu Layer".to_string() 21 | } 22 | 23 | fn get_output_shape(&self, input_dim: Vec) -> Vec { 24 | input_dim 25 | } 26 | 27 | fn get_num_parameter(&self) -> usize { 28 | 0 29 | } 30 | 31 | fn predict(&self, x: ArrayD) -> ArrayD { 32 | x.mapv(|x| if x > 0. { x } else { 0.01 * x }) 33 | } 34 | 35 | fn forward(&mut self, x: ArrayD) -> ArrayD { 36 | self.predict(x) 37 | } 38 | 39 | fn backward(&mut self, feedback: ArrayD) -> ArrayD { 40 | feedback.mapv(|x| if x >= 0. { 1. } else { 0.01 }) 41 | } 42 | 43 | fn clone_box(&self) -> Box { 44 | Box::new(LeakyReLuLayer::new()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/network/functional/activation_layer/mod.rs: -------------------------------------------------------------------------------- 1 | mod leakyrelu; 2 | mod relu; 3 | mod sigmoid; 4 | mod softmax; 5 | 6 | pub use leakyrelu::LeakyReLuLayer; 7 | pub use relu::ReLuLayer; 8 | pub use sigmoid::SigmoidLayer; 9 | pub use softmax::SoftmaxLayer; 10 | -------------------------------------------------------------------------------- /src/network/functional/activation_layer/relu.rs: -------------------------------------------------------------------------------- 1 | use crate::network::functional::Functional; 2 | use crate::network::layer::Layer; 3 | use ndarray::ArrayD; 4 | 5 | /// A relu layer. 6 | #[derive(Default)] 7 | pub struct ReLuLayer {} 8 | 9 | impl ReLuLayer { 10 | /// No parameters are possible. 11 | pub fn new() -> Self { 12 | ReLuLayer {} 13 | } 14 | } 15 | 16 | impl Functional for ReLuLayer {} 17 | 18 | impl Layer for ReLuLayer { 19 | fn get_type(&self) -> String { 20 | "ReLu Layer".to_string() 21 | } 22 | 23 | fn get_output_shape(&self, input_dim: Vec) -> Vec { 24 | input_dim 25 | } 26 | 27 | fn get_num_parameter(&self) -> usize { 28 | 0 29 | } 30 | 31 | fn predict(&self, x: ArrayD) -> ArrayD { 32 | x.mapv(|x| if x > 0. { x } else { 0. }) 33 | } 34 | 35 | fn forward(&mut self, x: ArrayD) -> ArrayD { 36 | self.predict(x) 37 | } 38 | 39 | fn backward(&mut self, feedback: ArrayD) -> ArrayD { 40 | feedback.mapv(|x| if x >= 0. { 1. } else { 0. }) 41 | } 42 | 43 | fn clone_box(&self) -> Box { 44 | Box::new(ReLuLayer::new()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/network/functional/activation_layer/sigmoid.rs: -------------------------------------------------------------------------------- 1 | use crate::network::functional::Functional; 2 | use crate::network::layer::Layer; 3 | use ndarray::{Array1, ArrayD}; 4 | 5 | /// A Sigmoid layer, 6 | #[derive(Default)] 7 | pub struct SigmoidLayer { 8 | output: ArrayD, 9 | } 10 | 11 | impl SigmoidLayer { 12 | /// No parameters are possible. 13 | pub fn new() -> Self { 14 | SigmoidLayer { 15 | output: Array1::zeros(0).into_dyn(), 16 | } 17 | } 18 | } 19 | 20 | impl Functional for SigmoidLayer {} 21 | 22 | impl Layer for SigmoidLayer { 23 | fn get_type(&self) -> String { 24 | "Sigmoid Layer".to_string() 25 | } 26 | 27 | fn get_output_shape(&self, input_dim: Vec) -> Vec { 28 | input_dim 29 | } 30 | 31 | fn get_num_parameter(&self) -> usize { 32 | 0 33 | } 34 | 35 | fn predict(&self, x: ArrayD) -> ArrayD { 36 | x.mapv(|x| 1.0 / (1.0 + (-x).exp())) 37 | } 38 | 39 | fn forward(&mut self, x: ArrayD) -> ArrayD { 40 | self.output = self.predict(x); 41 | self.output.clone() 42 | } 43 | 44 | fn backward(&mut self, feedback: ArrayD) -> ArrayD { 45 | self.output.mapv(|x| x * (1.0 - x)) * feedback 46 | } 47 | 48 | fn clone_box(&self) -> Box { 49 | Box::new(SigmoidLayer::new()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/network/functional/activation_layer/softmax.rs: -------------------------------------------------------------------------------- 1 | use crate::network::functional::Functional; 2 | use crate::network::layer::Layer; 3 | use ndarray::{Array, ArrayD}; 4 | use ndarray_stats::QuantileExt; 5 | 6 | /// A softmax layer. 7 | #[derive(Default)] 8 | pub struct SoftmaxLayer { 9 | output: ArrayD, 10 | } 11 | 12 | impl SoftmaxLayer { 13 | /// No parameters are possible. 14 | pub fn new() -> Self { 15 | SoftmaxLayer { 16 | output: Array::zeros(0).into_dyn(), //will be overwritten 17 | } 18 | } 19 | } 20 | 21 | impl Functional for SoftmaxLayer {} 22 | 23 | impl Layer for SoftmaxLayer { 24 | fn get_type(&self) -> String { 25 | "Softmax Layer".to_string() 26 | } 27 | 28 | fn get_output_shape(&self, input_dim: Vec) -> Vec { 29 | input_dim 30 | } 31 | 32 | fn get_num_parameter(&self) -> usize { 33 | 0 34 | } 35 | 36 | fn predict(&self, mut x: ArrayD) -> ArrayD { 37 | if x.ndim() == 1 { 38 | predict_single(&mut x); 39 | return x; 40 | } 41 | assert_eq!(x.ndim(), 2); 42 | for single_x in x.outer_iter_mut() { 43 | predict_single(&mut single_x.into_owned()); 44 | } 45 | x 46 | } 47 | 48 | fn forward(&mut self, x: ArrayD) -> ArrayD { 49 | self.output = self.predict(x); 50 | self.output.clone() 51 | } 52 | 53 | fn backward(&mut self, feedback: ArrayD) -> ArrayD { 54 | &self.output - &feedback 55 | } 56 | 57 | fn clone_box(&self) -> Box { 58 | Box::new(SoftmaxLayer::new()) 59 | } 60 | } 61 | 62 | fn predict_single(single_x: &mut ArrayD) { 63 | let max: f32 = *single_x.max_skipnan(); 64 | single_x.mapv_inplace(|x| (x - max).exp()); 65 | let sum: f32 = single_x.iter().filter(|x| !x.is_nan()).sum::(); 66 | single_x.mapv_inplace(|x| x / sum) 67 | } 68 | -------------------------------------------------------------------------------- /src/network/functional/error/bce.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use crate::network::functional::activation_layer::SigmoidLayer; 3 | use crate::network::layer::Layer; 4 | use ndarray::{Array, ArrayD}; 5 | 6 | /// This implements the binary crossentropy. 7 | pub struct BinaryCrossEntropyError { 8 | activation_function: Box, 9 | } 10 | 11 | impl Default for BinaryCrossEntropyError { 12 | fn default() -> Self { 13 | Self::new() 14 | } 15 | } 16 | 17 | impl Clone for BinaryCrossEntropyError { 18 | fn clone(&self) -> Self { 19 | BinaryCrossEntropyError::new() 20 | } 21 | } 22 | 23 | impl BinaryCrossEntropyError { 24 | /// No parameters required. 25 | pub fn new() -> Self { 26 | BinaryCrossEntropyError { 27 | activation_function: Box::new(SigmoidLayer::new()), 28 | } 29 | } 30 | } 31 | 32 | unsafe impl Sync for BinaryCrossEntropyError {} 33 | unsafe impl Send for BinaryCrossEntropyError {} 34 | 35 | impl Error for BinaryCrossEntropyError { 36 | fn get_type(&self) -> String { 37 | format!("Binary Crossentropy") 38 | } 39 | 40 | // loss after activation function (which probably was sigmoid) 41 | fn forward(&self, input: ArrayD, target: ArrayD) -> ArrayD { 42 | -target.clone() * input.mapv(f32::ln) - (1. - target) * input.mapv(|x| (1. - x).ln()) 43 | } 44 | 45 | // deriv after activation function (which probably was sigmoid) 46 | fn backward(&self, input: ArrayD, target: ArrayD) -> ArrayD { 47 | -&target / &input + (1.0 - target) / (1.0 - input) 48 | } 49 | 50 | // takes input from last dense/conv/.. layer directly, without activation function in between 51 | //Loss(t,z) = max(z,0) - tz + log(1+ e^(-|z|)), t is label 52 | fn loss_from_logits(&self, input: ArrayD, target: ArrayD) -> ArrayD { 53 | let tmp = input.mapv(|z| 1. + (-f32::abs(z)).exp()); 54 | let loss = input.mapv(|x| f32::max(0., x)) + tmp.mapv(f32::ln) - input * target.clone(); 55 | let cost: f32 = loss.sum() / target.len() as f32; 56 | Array::from_elem(1, cost).into_dyn() 57 | } 58 | 59 | // takes input from last dense/conv/.. layer directly, without activation function in between 60 | fn deriv_from_logits(&self, input: ArrayD, target: ArrayD) -> ArrayD { 61 | self.activation_function.predict(input) - target 62 | } 63 | 64 | fn clone_box(&self) -> Box { 65 | Box::new(BinaryCrossEntropyError::new()) 66 | } 67 | } 68 | 69 | //https://gombru.github.io/2018/05/23/cross_entropy_loss/ 70 | //https://towardsdatascience.com/implementing-the-xor-gate-using-backpropagation-in-neural-networks-c1f255b4f20d 71 | 72 | //numeric stable version from here (as in keras/tf): 73 | //https://towardsdatascience.com/nothing-but-numpy-understanding-creating-binary-classification-neural-networks-with-e746423c8d5c 74 | -------------------------------------------------------------------------------- /src/network/functional/error/cce.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use ndarray::{Array, ArrayD}; 3 | 4 | use ndarray_stats::QuantileExt; 5 | 6 | /// This implements the categorical crossentropy loss. 7 | #[derive(Clone, Default)] 8 | pub struct CategoricalCrossEntropyError {} 9 | 10 | impl CategoricalCrossEntropyError { 11 | /// No parameters required. 12 | pub fn new() -> Self { 13 | CategoricalCrossEntropyError {} 14 | } 15 | 16 | fn clip_values(&self, mut arr: ArrayD) -> ArrayD { 17 | arr.mapv_inplace(|x| if x > 0.9999 { 0.9999 } else { x }); 18 | arr.mapv(|x| if x < 1e-8 { 1e-8 } else { x }) 19 | } 20 | } 21 | 22 | impl Error for CategoricalCrossEntropyError { 23 | fn get_type(&self) -> String { 24 | format!("Categorical Crossentropy") 25 | } 26 | 27 | fn forward(&self, mut output: ArrayD, target: ArrayD) -> ArrayD { 28 | output = self.clip_values(output); 29 | let loss = -(target * output.mapv(f32::ln)).sum(); 30 | Array::from_elem(1, loss).into_dyn() 31 | } 32 | 33 | fn backward(&self, output: ArrayD, target: ArrayD) -> ArrayD { 34 | -(target / self.clip_values(output)) 35 | } 36 | 37 | fn deriv_from_logits(&self, mut output: ArrayD, target: ArrayD) -> ArrayD { 38 | let max: f32 = *output.clone().max_skipnan(); 39 | output.mapv_inplace(|x| (x - max).exp()); 40 | let sum: f32 = output.iter().filter(|x| !x.is_nan()).sum::(); 41 | output.mapv_inplace(|x| x / sum); 42 | output - target 43 | } 44 | 45 | fn loss_from_logits(&self, mut output: ArrayD, target: ArrayD) -> ArrayD { 46 | // ignore nans on sum and max 47 | let max: f32 = *output.max_skipnan(); 48 | output.mapv_inplace(|x| (x - max).exp()); 49 | let sum: f32 = output.iter().filter(|x| !x.is_nan()).sum::(); 50 | output.mapv_inplace(|x| x / sum); 51 | let loss = -(target * output).iter().sum::(); 52 | Array::from_elem(1, loss).into_dyn() 53 | } 54 | 55 | fn clone_box(&self) -> Box { 56 | Box::new(self.clone()) 57 | } 58 | } 59 | 60 | //https://gombru.github.io/2018/05/23/cross_entropy_loss/ 61 | //https://towardsdatascience.com/implementing-the-xor-gate-using-backpropagation-in-neural-networks-c1f255b4f20d 62 | 63 | //https://stats.stackexchange.com/questions/235528/backpropagation-with-softmax-cross-entropy 64 | -------------------------------------------------------------------------------- /src/network/functional/error/error_trait.rs: -------------------------------------------------------------------------------- 1 | use ndarray::ArrayD; 2 | 3 | /// An interface for all relevant functions which an error function has to implement. 4 | /// 5 | /// This layer may only be called during a training run, where the label (ground trouth) is known. 6 | /// The loss/driv_from_logits functions may directly forward input/output to/from forward/backward. 7 | /// They exist in order to allow a (numerically/performancewise/...) optimized implementation in combination 8 | /// with a specific activation function as the last layer of the neural network. 9 | /// Examples are sigmoid+bce or softmax+cce. 10 | pub trait Error: Send + Sync { 11 | /// This function returns a unique string identifying the type of the error function. 12 | fn get_type(&self) -> String; 13 | 14 | /// This function is used to calculate the error based on the last layer output and the expected output. 15 | fn forward(&self, input: ArrayD, feedback: ArrayD) -> ArrayD; 16 | 17 | /// This function is used to calculate the error to update previous layers. 18 | fn backward(&self, input: ArrayD, feedback: ArrayD) -> ArrayD; 19 | 20 | /// This function takes the output of the neural network *before* the last activation function. 21 | /// It merges the functionality of the last activation function with the forward() function in an improved implementation. 22 | /// Only works for a given activation function. 23 | fn loss_from_logits(&self, input: ArrayD, feedback: ArrayD) -> ArrayD; 24 | 25 | /// Similare to the loss_from_logits() function it takes the output *before* the last activation function. 26 | /// It merges the functionality of the backward() function with the backward() function of a specific activation function. 27 | fn deriv_from_logits(&self, input: ArrayD, feedback: ArrayD) -> ArrayD; 28 | 29 | /// A function to create a boxed clone of the used Error object. 30 | fn clone_box(&self) -> Box; 31 | } 32 | -------------------------------------------------------------------------------- /src/network/functional/error/mod.rs: -------------------------------------------------------------------------------- 1 | mod bce; 2 | mod cce; 3 | mod mse; 4 | mod noop; 5 | //mod rmse; 6 | 7 | mod error_trait; 8 | 9 | pub use error_trait::Error; 10 | 11 | pub use bce::BinaryCrossEntropyError; 12 | 13 | pub use cce::CategoricalCrossEntropyError; 14 | 15 | pub use mse::MeanSquareError; 16 | 17 | pub use noop::NoopError; 18 | 19 | //probably not correct impl 20 | //pub use rmse::RootMeanSquareError; 21 | -------------------------------------------------------------------------------- /src/network/functional/error/mse.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use ndarray::{Array1, ArrayD, Ix1}; 3 | 4 | /// This function calculates the mean of squares of errors between the neural network output and the ground truth. 5 | #[derive(Clone, Default)] 6 | pub struct MeanSquareError {} 7 | 8 | impl MeanSquareError { 9 | /// No parameters required. 10 | pub fn new() -> Self { 11 | MeanSquareError {} 12 | } 13 | } 14 | 15 | impl Error for MeanSquareError { 16 | fn get_type(&self) -> String { 17 | format!("Mean Square") 18 | } 19 | 20 | fn forward(&self, output: ArrayD, target: ArrayD) -> ArrayD { 21 | let output = output.into_dimensionality::().unwrap(); 22 | let target = target.into_dimensionality::().unwrap(); 23 | let n = output.len() as f32; 24 | let err = output 25 | .iter() 26 | .zip(target.iter()) 27 | .fold(0., |err, val| err + f32::powf(val.0 - val.1, 2.)) 28 | / n; 29 | Array1::::from_elem(1, 0.5 * err).into_dyn() 30 | } 31 | 32 | fn backward(&self, output: ArrayD, target: ArrayD) -> ArrayD { 33 | let n = target.len() as f32; 34 | (output - target) / n 35 | } 36 | 37 | fn loss_from_logits(&self, output: ArrayD, target: ArrayD) -> ArrayD { 38 | self.forward(output, target) 39 | } 40 | 41 | fn deriv_from_logits(&self, output: ArrayD, target: ArrayD) -> ArrayD { 42 | self.backward(output, target) 43 | } 44 | 45 | fn clone_box(&self) -> Box { 46 | Box::new(self.clone()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/network/functional/error/noop.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use ndarray::{Array1, ArrayD}; 3 | 4 | /// This function returns 42 during the forward call and forwards the ground trouth unchanged to the previous layer. 5 | /// 6 | /// It is intended for debug purpose only. 7 | #[derive(Clone, Default)] 8 | pub struct NoopError {} 9 | 10 | impl NoopError { 11 | /// No parameters required. 12 | pub fn new() -> Self { 13 | NoopError {} 14 | } 15 | } 16 | 17 | impl Error for NoopError { 18 | fn get_type(&self) -> String { 19 | "Noop Error function".to_string() 20 | } 21 | 22 | //printing 42 as obviously useless 23 | fn forward(&self, _input: ArrayD, _target: ArrayD) -> ArrayD { 24 | Array1::from_elem(1, 42.).into_dyn() 25 | } 26 | 27 | fn backward(&self, _input: ArrayD, feedback: ArrayD) -> ArrayD { 28 | feedback 29 | } 30 | 31 | //printing 42 as obviously useless 32 | fn loss_from_logits(&self, _input: ArrayD, _feedback: ArrayD) -> ArrayD { 33 | Array1::from_elem(1, 42.).into_dyn() 34 | } 35 | 36 | fn deriv_from_logits(&self, _input: ArrayD, feedback: ArrayD) -> ArrayD { 37 | feedback 38 | } 39 | 40 | fn clone_box(&self) -> Box { 41 | Box::new(self.clone()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/network/functional/error/rmse.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use ndarray::{Array1, ArrayD, Ix1}; 3 | 4 | /// This error function works on the square-root of the mse. 5 | #[derive(Clone, Default)] 6 | pub struct RootMeanSquareError { 7 | err: f32, 8 | } 9 | 10 | impl RootMeanSquareError { 11 | /// No parameters required. 12 | pub fn new() -> Self { 13 | RootMeanSquareError { err: 0. } 14 | } 15 | } 16 | 17 | impl Error for RootMeanSquareError { 18 | fn get_type(&self) -> String { 19 | format!("Root Mean Square") 20 | } 21 | 22 | fn forward(&self, output: ArrayD, target: ArrayD) -> ArrayD { 23 | let output = output.into_dimensionality::().unwrap(); 24 | let target = target.into_dimensionality::().unwrap(); 25 | let n = output.len() as f32; 26 | let err = output 27 | .iter() 28 | .zip(target.iter()) 29 | .fold(0., |err, val| err + f32::powf(val.0 - val.1, 2.)) 30 | / (2. * n); 31 | self.err = f32::sqrt(err); 32 | Array1::::from_elem(1, self.err).into_dyn() 33 | } 34 | 35 | fn backward(&mut self, output: ArrayD, target: ArrayD) -> ArrayD { 36 | let div = 2. * target.len() as f32 * self.err; 37 | (output - target) / div 38 | } 39 | 40 | fn loss_from_logits(&mut self, output: ArrayD, target: ArrayD) -> ArrayD { 41 | self.forward(output, target) 42 | } 43 | 44 | fn deriv_from_logits(&mut self, output: ArrayD, target: ArrayD) -> ArrayD { 45 | self.backward(output, target) 46 | } 47 | 48 | fn clone_box(&self) -> Box { 49 | Box::new(self.clone()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/network/functional/functional_trait.rs: -------------------------------------------------------------------------------- 1 | use crate::network::layer::Layer; 2 | 3 | /// Layer Interface: 4 | /// All layers passed to the neural network must implement this trait 5 | /// 6 | pub trait Functional: Layer + Send + Sync {} 7 | -------------------------------------------------------------------------------- /src/network/functional/mod.rs: -------------------------------------------------------------------------------- 1 | /// This trait forces functional layers and functions to implement Send and Sync 2 | mod functional_trait; 3 | 4 | pub use functional_trait::Functional; 5 | 6 | /// This module contains the most common activation functions like sigmoid, relu, or softmax. 7 | pub mod activation_layer; 8 | 9 | /// This submodule provides various error function. 10 | /// 11 | /// Beside of the normal forward() and backward() functions some Error functions implement both functions in a *_from_logits() variant. 12 | /// They merge their own implementation with the previous activation function in a numerically more stable way. 13 | /// Better known examples are Softmax+CategoricalCrossEntropy or Sigmoid+BinaryCrossEntropy. 14 | /// When used as part of nn, the appropriate functions are automatically picked. 15 | pub mod error; 16 | -------------------------------------------------------------------------------- /src/network/layer/convolution/conv_utils.rs: -------------------------------------------------------------------------------- 1 | use ndarray::{par_azip, s, Array, Array1, Array2, Array3, ArrayD, Ix3}; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use super::*; 6 | use ndarray::{arr2, arr3}; 7 | 8 | #[test] 9 | fn test_unfold1() { 10 | let input = arr3(&[[ 11 | [1., 2., 3., 4.], 12 | [5., 6., 7., 8.], 13 | [9., 10., 11., 12.], 14 | [13., 14., 15., 16.], 15 | ]]) 16 | .into_dyn(); 17 | let output = unfold_3d_matrix(1, input, 3, true); 18 | assert_eq!( 19 | output, 20 | arr2(&[ 21 | [1., 2., 3., 5., 6., 7., 9., 10., 11.], 22 | [2., 3., 4., 6., 7., 8., 10., 11., 12.], 23 | [5., 6., 7., 9., 10., 11., 13., 14., 15.], 24 | [6., 7., 8., 10., 11., 12., 14., 15., 16.] 25 | ]) 26 | ); 27 | } 28 | #[test] 29 | fn test_unfold2() { 30 | let input = arr3(&[[ 31 | [1., 2., 3., 4.], 32 | [5., 6., 7., 8.], 33 | [9., 10., 11., 12.], 34 | [13., 14., 15., 16.], 35 | ]]) 36 | .into_dyn(); 37 | let output = unfold_3d_matrix(1, input, 2, true); 38 | assert_eq!( 39 | output, 40 | arr2(&[ 41 | [1., 2., 5., 6.], 42 | [2., 3., 6., 7.], 43 | [3., 4., 7., 8.], 44 | [5., 6., 9., 10.], 45 | [6., 7., 10., 11.], 46 | [7., 8., 11., 12.], 47 | [9., 10., 13., 14.], 48 | [10., 11., 14., 15.], 49 | [11., 12., 15., 16.] 50 | ]) 51 | ); 52 | } 53 | 54 | #[test] 55 | fn test_padding1() { 56 | let input = arr2(&[ 57 | [1., 2., 3., 4.], 58 | [5., 6., 7., 8.], 59 | [9., 10., 11., 12.], 60 | [13., 14., 15., 16.], 61 | ]) 62 | .into_dyn(); 63 | let output = add_padding(1, input); 64 | assert_eq!( 65 | output, 66 | arr2(&[ 67 | [0., 0., 0., 0., 0., 0.], 68 | [0., 1., 2., 3., 4., 0.], 69 | [0., 5., 6., 7., 8., 0.], 70 | [0., 9., 10., 11., 12., 0.], 71 | [0., 13., 14., 15., 16., 0.], 72 | [0., 0., 0., 0., 0., 0.] 73 | ]) 74 | .into_dyn() 75 | ); 76 | } 77 | #[test] 78 | fn test_padding2() { 79 | let input = arr3(&[ 80 | [[1., 2., 3.], [5., 6., 7.]], 81 | [[9., 10., 11.], [13., 14., 15.]], 82 | ]) 83 | .into_dyn(); 84 | let output = add_padding(1, input); 85 | assert_eq!( 86 | output, 87 | arr3(&[ 88 | [ 89 | [0., 0., 0., 0., 0.], 90 | [0., 1., 2., 3., 0.], 91 | [0., 5., 6., 7., 0.], 92 | [0., 0., 0., 0., 0.] 93 | ], 94 | [ 95 | [0., 0., 0., 0., 0.], 96 | [0., 9., 10., 11., 0.], 97 | [0., 13., 14., 15., 0.], 98 | [0., 0., 0., 0., 0.] 99 | ] 100 | ]) 101 | .into_dyn() 102 | ); 103 | } 104 | } 105 | 106 | /// We create a new Array of zeros with the size of the original input+padding. 107 | /// Afterwards we copy the original image over to the center of the new image. 108 | /// TODO change to full/normal/... 109 | pub fn add_padding(padding: usize, input: ArrayD) -> ArrayD { 110 | let n = input.ndim(); // 2d or 3d input? 111 | let shape: &[usize] = input.shape(); 112 | let x = shape[n - 2] + 2 * padding; // calculate the new dim with padding 113 | let y = shape[n - 1] + 2 * padding; // calculate the new dim with padding 114 | 115 | if n > 4 { 116 | unimplemented!(); 117 | } 118 | 119 | if n == 4 { 120 | let batch_size: usize = shape[0]; 121 | let channels: usize = shape[1]; 122 | let mut out: ArrayD = Array::zeros((batch_size, channels, x, y)).into_dyn(); 123 | par_azip!((mut out_entry in out.outer_iter_mut(), input_entry in input.outer_iter()) { 124 | let tmp = add_padding(padding, input_entry.into_owned()); 125 | out_entry.assign(&tmp); 126 | }); 127 | return out; 128 | } 129 | let start: isize = padding as isize; 130 | let x_stop: isize = padding as isize + shape[n - 2] as isize; 131 | let y_stop: isize = padding as isize + shape[n - 1] as isize; 132 | let mut out: ArrayD; 133 | if n == 2 { 134 | out = Array::zeros((x, y)).into_dyn(); 135 | out.slice_mut(s![start..x_stop, start..y_stop]) 136 | .assign(&input); 137 | } else { 138 | let z = shape[n - 3]; 139 | out = Array::zeros((z, x, y)).into_dyn(); 140 | out.slice_mut(s![.., start..x_stop, start..y_stop]) 141 | .assign(&input); 142 | } 143 | out 144 | } 145 | 146 | /// We shape the input into a 2d array, so we can apply our vector of (kernel)vectors with a matrix-matrix multiplication. 147 | pub fn shape_into_kernel(x: Array3) -> Array2 { 148 | let (shape_0, shape_1, shape_2) = (x.shape()[0], x.shape()[1], x.shape()[2]); 149 | x.into_shape((shape_0, shape_1 * shape_2)).unwrap() 150 | } 151 | 152 | /// For efficiency reasons we handle kernels and images in 2d, here we revert that in order to receive the expected output. 153 | pub fn fold_output(x: Array2, (num_kernels, n, m): (usize, usize, usize)) -> Array3 { 154 | // add self.batch_size as additional dim later > for batch processing? 155 | let (shape_x, shape_y) = (x.shape()[0], x.shape()[1]); 156 | let output = x.into_shape((num_kernels, n, m)); 157 | match output { 158 | Ok(v) => v, 159 | Err(_) => panic!( 160 | "Got array with shape [{},{}], but expected {}*{}*{} elements.", 161 | shape_x, shape_y, num_kernels, n, m 162 | ), 163 | } 164 | } 165 | 166 | /// checked with Rust Playground. Works for quadratic filter and arbitrary 2d/3d images 167 | /// We receive either 2d or 3d input. In order to have one function handling both cases we use ArrayD. 168 | /// We unfold each input image once into a vector of vector (say 2d Array). 169 | /// Each inner vector corresponds to a 2d or 3d subsection of the input image of the same size as a single kernel. 170 | pub fn unfold_3d_matrix( 171 | in_channels: usize, 172 | input: ArrayD, 173 | k: usize, 174 | forward: bool, 175 | ) -> Array2 { 176 | assert_eq!(input.ndim(), 3); 177 | let (len_y, len_x) = (input.shape()[1], input.shape()[2]); 178 | 179 | let mut xx = if forward { 180 | Array::zeros(((len_y - k + 1) * (len_x - k + 1), k * k * in_channels)) 181 | } else { 182 | Array::zeros(((len_y - k + 1) * (len_x - k + 1) * in_channels, k * k)) 183 | }; 184 | 185 | let mut row_num = 0; 186 | 187 | // windows is not implemented on ArrayD. Here we already know the input dimension, so we just declare it as 3d. No reshaping occurs! 188 | let x_3d: Array3 = input.into_dimensionality::().unwrap(); 189 | if forward { 190 | let windows = x_3d.windows([in_channels, k, k]); 191 | for window in windows { 192 | let unrolled: Array1 = 193 | window.into_owned().into_shape(k * k * in_channels).unwrap(); 194 | xx.row_mut(row_num).assign(&unrolled); 195 | row_num += 1; 196 | } 197 | } else { 198 | // During the backprop part we have to do some reshaping, since we store our kernels as 2d matrix but receive 3d feedback from the next layer. 199 | // In the 2d case the dimensions match, so we skipped it. 200 | let windows = x_3d.windows([1, k, k]); 201 | for window in windows { 202 | let unrolled: Array1 = window.into_owned().into_shape(k * k).unwrap(); 203 | xx.row_mut(row_num).assign(&unrolled); 204 | row_num += 1; 205 | } 206 | } 207 | xx 208 | } 209 | -------------------------------------------------------------------------------- /src/network/layer/convolution/convolution2d.rs: -------------------------------------------------------------------------------- 1 | use super::conv_utils; 2 | use crate::network::layer::Layer; 3 | use crate::network::optimizer::Optimizer; 4 | use conv_utils::*; 5 | use ndarray::{Array, Array1, Array2, Array3, ArrayD, Axis, Ix3, Ix4}; 6 | use ndarray_rand::rand_distr::Normal; //{StandardNormal,Normal}; //not getting Standardnormal to work. should be cleaner & faster 7 | use ndarray_rand::RandomExt; 8 | 9 | /// This layer implements a convolution on 2d or 3d input. 10 | pub struct ConvolutionLayer2D { 11 | batch_size: usize, 12 | kernels: Array2, 13 | in_channels: usize, 14 | bias: Array1, // one bias value per kernel 15 | padding: usize, 16 | last_input: ArrayD, 17 | filter_shape: (usize, usize), 18 | kernel_updates: Array2, 19 | bias_updates: Array1, 20 | learning_rate: f32, 21 | num_in_batch: usize, 22 | // Rust requires knowledge about obj size during compile time. Optimizers can be set/changed dynamically during runtime, so we just store a reference to the heap 23 | weight_optimizer: Box, 24 | bias_optimizer: Box, 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | use ndarray::{arr2, arr3}; 31 | 32 | #[test] 33 | fn test_shape_feedback1() { 34 | let input = arr3(&[ 35 | [[1., 2., 3.], [5., 6., 7.]], 36 | [[9., 10., 11.], [13., 14., 15.]], 37 | ]); 38 | let output = shape_into_kernel(input); 39 | assert_eq!( 40 | output, 41 | arr2(&[[1., 2., 3., 5., 6., 7.], [9., 10., 11., 13., 14., 15.]]) 42 | ); 43 | } 44 | } 45 | 46 | fn new_from_kernels( 47 | kernels: Array2, 48 | bias: Array1, 49 | weight_optimizer: Box, 50 | bias_optimizer: Box, 51 | filter_shape: (usize, usize), 52 | in_channels: usize, 53 | out_channels: usize, 54 | padding: usize, 55 | batch_size: usize, 56 | learning_rate: f32, 57 | ) -> ConvolutionLayer2D { 58 | let elements_per_kernel = filter_shape.0 * filter_shape.1 * in_channels; 59 | ConvolutionLayer2D { 60 | filter_shape, 61 | learning_rate, 62 | kernels, 63 | in_channels, 64 | padding, 65 | bias, 66 | last_input: Default::default(), 67 | kernel_updates: Array::zeros((out_channels, elements_per_kernel)), 68 | bias_updates: Array::zeros(out_channels), 69 | batch_size, 70 | num_in_batch: 0, 71 | weight_optimizer, 72 | bias_optimizer, 73 | } 74 | } 75 | 76 | impl ConvolutionLayer2D { 77 | /// This function prints the kernel values. 78 | /// 79 | /// It's main purpose is to analyze the learning success of the first convolution layer. 80 | /// Later layers might not show clear patterns. 81 | pub fn print_kernel(&self) { 82 | let n = self.kernels.nrows(); 83 | println!("printing kernels: \n"); 84 | for i in 0..n { 85 | let arr = self.kernels.index_axis(Axis(0), i); 86 | println!( 87 | "{}\n", 88 | arr.into_shape((self.in_channels, self.filter_shape.0, self.filter_shape.1)) 89 | .unwrap() 90 | ); 91 | } 92 | } 93 | 94 | /// Allows setting of hand-crafted filters. 95 | /// 2d or 3d filters have to be reshaped into 1d, so kernels.nrows() equals the amount of kernels used. 96 | pub fn set_kernels(&mut self, kernels: Array2) { 97 | self.kernels = kernels; 98 | } 99 | 100 | /// Create a new convolution layer. 101 | /// 102 | /// Currently we only accept quadratic filter_shapes. Common dimensions are (3,3) or (5,5). 103 | /// The in_channels has to be set equal to the last dimension of the input images. 104 | /// The out_channels can be set to any positive value, 16 or 32 might be enough for simple cases or to get started. 105 | /// The padding will be applied to all sites of the input. Using padding: 1 on a 28x28 image will therefore result in a 30x30 input. 106 | pub fn new( 107 | filter_shape: (usize, usize), 108 | in_channels: usize, 109 | out_channels: usize, 110 | padding: usize, 111 | batch_size: usize, 112 | learning_rate: f32, 113 | optimizer: Box, 114 | ) -> Self { 115 | assert_eq!( 116 | filter_shape.0, filter_shape.1, 117 | "currently only supporting quadratic filter!" 118 | ); 119 | assert!(filter_shape.0 >= 1, "filter_shape has to be one or greater"); 120 | assert!( 121 | in_channels >= 1, 122 | "filter depth has to be at least one (and equal to img_channels)" 123 | ); 124 | let elements_per_kernel = filter_shape.0 * filter_shape.1 * in_channels; 125 | let kernels: Array2 = Array::random( 126 | (out_channels, elements_per_kernel), 127 | Normal::new(0.0, 1.0 as f32).unwrap(), 128 | ); 129 | assert_eq!(kernels.nrows(), out_channels, "filter implementation wrong"); 130 | let bias = Array::zeros(out_channels); //http://cs231n.github.io/neural-networks-2/ 131 | let mut weight_optimizer = optimizer.clone_box(); 132 | let mut bias_optimizer = optimizer; 133 | weight_optimizer.set_input_shape(vec![out_channels, elements_per_kernel]); 134 | bias_optimizer.set_input_shape(vec![out_channels]); 135 | new_from_kernels( 136 | kernels, 137 | bias, 138 | weight_optimizer, 139 | bias_optimizer, 140 | filter_shape, 141 | in_channels, 142 | out_channels, 143 | padding, 144 | batch_size, 145 | learning_rate, 146 | ) 147 | } 148 | } 149 | 150 | impl Layer for ConvolutionLayer2D { 151 | fn get_type(&self) -> String { 152 | format!("Conv") 153 | } 154 | 155 | fn get_num_parameter(&self) -> usize { 156 | self.kernels.nrows() * self.kernels.ncols() + self.kernels.nrows() // num_kernels * size_kernels + bias 157 | } 158 | 159 | fn clone_box(&self) -> Box { 160 | let out_channels = self.kernels.nrows(); 161 | let new_layer = new_from_kernels( 162 | self.kernels.clone(), 163 | self.bias.clone(), 164 | self.weight_optimizer.clone_box(), 165 | self.bias_optimizer.clone_box(), 166 | self.filter_shape, 167 | self.in_channels, 168 | out_channels, 169 | self.padding, 170 | self.batch_size, 171 | self.learning_rate, 172 | ); 173 | Box::new(new_layer) 174 | } 175 | 176 | // returns the output shape despite of a (maybe) existing batchsize. 177 | // So those values are for a single input. 178 | fn get_output_shape(&self, input_shape: Vec) -> Vec { 179 | get_single_output_shape( 180 | input_shape, 181 | self.kernels.nrows(), 182 | self.filter_shape, 183 | self.padding, 184 | ) 185 | } 186 | 187 | fn predict(&self, input: ArrayD) -> ArrayD { 188 | if input.ndim() == 3 { 189 | let single_input = input.into_dimensionality::().unwrap(); 190 | return predict_single( 191 | single_input, 192 | &self.kernels, 193 | &self.bias, 194 | self.in_channels, 195 | self.filter_shape, 196 | self.padding, 197 | ) 198 | .into_dyn(); 199 | } 200 | assert_eq!(input.ndim(), 4); 201 | let batch_input = input.into_dimensionality::().unwrap(); 202 | let single_input_shape = batch_input.index_axis(Axis(0), 0).shape().to_vec(); 203 | let tmp_dims = self.get_output_shape(single_input_shape); 204 | let dims = vec![ 205 | batch_input.shape()[0], 206 | tmp_dims[0], 207 | tmp_dims[1], 208 | tmp_dims[2], 209 | ]; 210 | let mut res = Array::zeros(dims); 211 | for (i, single_input) in batch_input.outer_iter().enumerate() { 212 | let single_res = predict_single( 213 | single_input.into_owned(), 214 | &self.kernels, 215 | &self.bias, 216 | self.in_channels, 217 | self.filter_shape, 218 | self.padding, 219 | ); 220 | res.index_axis_mut(Axis(0), i).assign(&single_res); 221 | } 222 | res.into_dyn() 223 | } 224 | 225 | fn forward(&mut self, input: ArrayD) -> ArrayD { 226 | self.last_input = conv_utils::add_padding(self.padding, input.clone()); 227 | self.predict(input) 228 | } 229 | 230 | fn backward(&mut self, feedback: ArrayD) -> ArrayD { 231 | if feedback.ndim() == 3 { 232 | let single_feedback = feedback.into_dimensionality::().unwrap(); 233 | calc_single_weight_update( 234 | single_feedback, 235 | self.last_input.clone(), 236 | &mut self.kernel_updates, 237 | &mut self.bias_updates, 238 | &mut self.num_in_batch, 239 | self.in_channels, 240 | ); 241 | } else { 242 | assert_eq!(feedback.ndim(), 4); 243 | let batch_feedback = feedback.into_dimensionality::().unwrap(); 244 | for (single_feedback, single_input) in batch_feedback 245 | .outer_iter() 246 | .zip(self.last_input.outer_iter()) 247 | { 248 | calc_single_weight_update( 249 | single_feedback.into_owned(), 250 | single_input.into_owned(), 251 | &mut self.kernel_updates, 252 | &mut self.bias_updates, 253 | &mut self.num_in_batch, 254 | self.in_channels, 255 | ); 256 | } 257 | } 258 | 259 | // After the final update we finally apply the updates. 260 | if self.num_in_batch == self.batch_size { 261 | self.num_in_batch = 0; 262 | let weight_delta = 1. / (self.batch_size as f32) * self.kernel_updates.clone(); 263 | let bias_delta = 1. / (self.batch_size as f32) * self.bias_updates.clone(); 264 | self.kernels -= &(self.weight_optimizer.optimize2d(weight_delta) * self.learning_rate); 265 | self.bias -= &(self.bias_optimizer.optimize1d(bias_delta) * self.learning_rate); 266 | } 267 | 268 | // calc feedback for the previous layer: 269 | // use fliped kernel vectors here 270 | // https://medium.com/@pavisj/convolutions-and-backpropagations-46026a8f5d2c 271 | // return that 272 | 273 | Array::zeros(self.last_input.shape()).into_dyn() 274 | } 275 | } 276 | 277 | fn calc_single_weight_update( 278 | feedback: Array3, 279 | input: ArrayD, 280 | kernel_updates: &mut Array2, 281 | bias_updates: &mut Array1, 282 | num_in_batch: &mut usize, 283 | in_channels: usize, 284 | ) { 285 | // precalculate the weight updates for this batch_element 286 | 287 | // prepare feedback matrix 288 | // we always return a 3d output in our forward/predict function, so we will always receive a 3d feedback: 289 | let x: Array3 = feedback.into_dimensionality::().unwrap(); 290 | let feedback_as_kernel = conv_utils::shape_into_kernel(x.clone()); 291 | 292 | //prepare feedback 293 | let k: usize = (feedback_as_kernel.shape()[1] as f64).sqrt() as usize; 294 | let input_unfolded = unfold_3d_matrix(in_channels, input, k, false); 295 | 296 | //calculate kernel updates 297 | let prod = input_unfolded.dot(&feedback_as_kernel.t()).t().into_owned(); 298 | let sum: Array1 = x.sum_axis(Axis(1)).sum_axis(Axis(1)); // 3d feedback, but only 1d bias (1 single f32 bias value per kernel) 299 | 300 | // When having a batch size of 32, we are setting the weight updates once and update them 31 times. 301 | if *num_in_batch == 0 { 302 | *kernel_updates = prod; 303 | *bias_updates = sum; 304 | } else { 305 | *kernel_updates = kernel_updates.clone() + prod; 306 | *bias_updates = bias_updates.clone() + sum; 307 | } 308 | *num_in_batch += 1; 309 | } 310 | 311 | fn get_single_output_shape( 312 | input_shape: Vec, 313 | out_channels: usize, 314 | filter_shape: (usize, usize), 315 | padding: usize, 316 | ) -> Vec { 317 | let num_dim = input_shape.len(); 318 | assert!( 319 | num_dim == 3, 320 | "expected input dim 3: (in_channels, x, y), was: {}", 321 | num_dim 322 | ); 323 | let mut res = vec![out_channels, 0, 0]; 324 | res[1] = input_shape[num_dim - 2] - filter_shape.0 + 1 + 2 * padding; 325 | res[2] = input_shape[num_dim - 1] - filter_shape.1 + 1 + 2 * padding; 326 | res 327 | } 328 | 329 | fn predict_single( 330 | input: Array3, 331 | kernels: &Array2, 332 | bias: &Array1, 333 | in_channels: usize, 334 | filter_shape: (usize, usize), 335 | padding: usize, 336 | ) -> Array3 { 337 | let tmp = get_single_output_shape( 338 | input.shape().to_vec(), 339 | kernels.nrows(), 340 | filter_shape, 341 | padding, 342 | ); 343 | let (output_shape_x, output_shape_y) = (tmp[1], tmp[2]); 344 | let input = conv_utils::add_padding(padding, input.into_dyn()); 345 | 346 | // prepare input matrix 347 | let x_unfolded = unfold_3d_matrix(in_channels, input, filter_shape.0, true); 348 | 349 | // calculate convolution (=output for next layer) 350 | let prod = x_unfolded.dot(&kernels.t()) + bias; 351 | 352 | // reshape product for next layer: (num_kernels, new_x, new_y) 353 | let res = fold_output(prod, (kernels.nrows(), output_shape_x, output_shape_y)); 354 | res 355 | } 356 | -------------------------------------------------------------------------------- /src/network/layer/convolution/mod.rs: -------------------------------------------------------------------------------- 1 | //mod convolution1d; 2 | mod convolution2d; 3 | //mod convolution3d; 4 | mod conv_utils; 5 | 6 | //// This layer implements a classical convolution layer. 7 | //pub use convolution1d::ConvolutionLayer1D; 8 | /// This layer implements a classical 2d convolution layer. 9 | pub use convolution2d::ConvolutionLayer2D; 10 | //// This layer implements a classical convolution layer. 11 | //pub use convolution3d::ConvolutionLayer3D; 12 | -------------------------------------------------------------------------------- /src/network/layer/dense.rs: -------------------------------------------------------------------------------- 1 | use crate::network::layer::Layer; 2 | use crate::network::optimizer::Optimizer; 3 | use ndarray::{Array, Array1, Array2, ArrayD, Axis, Ix1, Ix2}; 4 | use ndarray_rand::rand_distr::Normal; //{StandardNormal,Normal}; //not getting Standardnormal to work. should be better & faster 5 | use ndarray_rand::RandomExt; 6 | 7 | /// A dense (also called fully connected) layer. 8 | pub struct DenseLayer { 9 | input_dim: usize, 10 | output_dim: usize, 11 | learning_rate: f32, 12 | weights: Array2, 13 | bias: Array1, 14 | net: Array2, 15 | feedback: Array2, 16 | batch_size: usize, 17 | forward_passes: usize, 18 | backward_passes: usize, 19 | weight_optimizer: Box, 20 | bias_optimizer: Box, 21 | } 22 | 23 | impl DenseLayer { 24 | /// A common constructor for a dense layer. 25 | /// 26 | /// The learning_rate is expected to be in the range [0,1]. 27 | /// A batch_size of 1 basically means that no batch processing happens. 28 | /// A batch_size of 0, a learning_rate outside of [0,1], or an input or output dimension of 0 will result in an error. 29 | /// TODO: return Result instead of Self 30 | pub fn new( 31 | input_dim: usize, 32 | output_dim: usize, 33 | batch_size: usize, 34 | learning_rate: f32, 35 | optimizer: Box, 36 | ) -> Self { 37 | //xavier init 38 | let weights: Array2 = Array::random( 39 | (output_dim, input_dim), 40 | Normal::new(0.0, 2.0 / ((output_dim + input_dim) as f32).sqrt()).unwrap(), 41 | ); 42 | let bias: Array1 = Array::zeros(output_dim); //https://cs231n.github.io/neural-networks-2/#init 43 | let mut weight_optimizer = optimizer.clone_box(); 44 | let mut bias_optimizer = optimizer; 45 | weight_optimizer.set_input_shape(vec![output_dim, input_dim]); 46 | bias_optimizer.set_input_shape(vec![output_dim]); 47 | new_from_matrices( 48 | weights, 49 | bias, 50 | input_dim, 51 | output_dim, 52 | batch_size, 53 | learning_rate, 54 | weight_optimizer, 55 | bias_optimizer, 56 | ) 57 | } 58 | 59 | fn update_weights(&mut self) { 60 | let d_w: Array2 = &self.feedback.dot(&self.net.t()) / (self.batch_size as f32); 61 | let d_b: Array1 = &self.feedback.sum_axis(Axis(1)) / (self.batch_size as f32); 62 | 63 | assert_eq!(d_w.shape(), self.weights.shape()); 64 | assert_eq!(d_b.shape(), self.bias.shape()); 65 | 66 | self.weights -= &(self.weight_optimizer.optimize2d(d_w) * self.learning_rate); 67 | self.bias -= &(self.bias_optimizer.optimize1d(d_b) * self.learning_rate); 68 | } 69 | } 70 | 71 | fn new_from_matrices( 72 | weights: Array2, 73 | bias: Array1, 74 | input_dim: usize, 75 | output_dim: usize, 76 | batch_size: usize, 77 | learning_rate: f32, 78 | weight_optimizer: Box, 79 | bias_optimizer: Box, 80 | ) -> DenseLayer { 81 | DenseLayer { 82 | input_dim, 83 | output_dim, 84 | learning_rate, 85 | weights, 86 | bias, 87 | net: Array::zeros((input_dim, batch_size)), 88 | feedback: Array::zeros((output_dim, batch_size)), 89 | batch_size, 90 | forward_passes: 0, 91 | backward_passes: 0, 92 | weight_optimizer, 93 | bias_optimizer, 94 | } 95 | } 96 | 97 | impl Layer for DenseLayer { 98 | fn get_type(&self) -> String { 99 | format!("Dense") 100 | } 101 | 102 | fn get_num_parameter(&self) -> usize { 103 | self.input_dim * self.output_dim + self.output_dim // weights + bias 104 | } 105 | 106 | fn get_output_shape(&self, _input_dim: Vec) -> Vec { 107 | vec![self.output_dim] 108 | } 109 | 110 | fn clone_box(&self) -> Box { 111 | let new_layer = new_from_matrices( 112 | self.weights.clone(), 113 | self.bias.clone(), 114 | self.input_dim, 115 | self.output_dim, 116 | self.batch_size, 117 | self.learning_rate, 118 | self.weight_optimizer.clone_box(), 119 | self.bias_optimizer.clone_box(), 120 | ); 121 | Box::new(new_layer) 122 | } 123 | 124 | fn predict(&self, x: ArrayD) -> ArrayD { 125 | // Handle 1D input 126 | if x.ndim() == 1 { 127 | let single_input: Array1 = x.into_dimensionality::().unwrap(); 128 | let res: Array1 = self.weights.dot(&single_input) + &self.bias; 129 | return res.into_dyn(); 130 | } 131 | 132 | // Handle 2D input (input-batch) 133 | assert_eq!(x.ndim(), 2, "expected a 1d or 2d input!"); 134 | let batch_input: Array2 = x.into_dimensionality::().unwrap(); 135 | let batch_size = batch_input.nrows(); 136 | let mut res = Array2::zeros((batch_size, self.output_dim)); 137 | assert_eq!(res.nrows(), batch_size); 138 | for (i, single_input) in batch_input.outer_iter().enumerate() { 139 | let single_res = &self.weights.dot(&single_input) + &self.bias; 140 | res.row_mut(i).assign(&single_res); 141 | } 142 | res.into_dyn() 143 | } 144 | 145 | fn forward(&mut self, x: ArrayD) -> ArrayD { 146 | store_input( 147 | x.clone(), 148 | &mut self.net, 149 | self.batch_size, 150 | &mut self.forward_passes, 151 | ); 152 | self.predict(x) 153 | } 154 | 155 | fn backward(&mut self, feedback: ArrayD) -> ArrayD { 156 | // store feedback gradients for batch-weightupdates 157 | store_input( 158 | feedback.clone(), 159 | &mut self.feedback, 160 | self.batch_size, 161 | &mut self.backward_passes, 162 | ); 163 | 164 | //calc derivate to backprop through layers 165 | // TODO assure which way the a.dot(b) should be calculated! 166 | let output: ArrayD; 167 | if feedback.ndim() == 1 { 168 | let single_feedback: Array1 = feedback.into_dimensionality::().unwrap(); 169 | output = single_feedback.dot(&self.weights).into_owned().into_dyn(); 170 | assert_eq!(output.shape()[0], self.input_dim); 171 | //output = self.weights.t().dot(&single_feedback).into_dyn(); 172 | } else { 173 | assert_eq!(feedback.ndim(), 2); 174 | let batch_feedback: Array2 = feedback.into_dimensionality::().unwrap(); 175 | let batch_size = batch_feedback.nrows(); 176 | let mut tmp_res = Array2::zeros((batch_size, self.input_dim)); 177 | for (i, single_feedback) in batch_feedback.outer_iter().enumerate() { 178 | //let single_grad = single_feedback.dot(&self.weights.t()); 179 | let single_grad = &self.weights.t().dot(&single_feedback); 180 | tmp_res.row_mut(i).assign(single_grad); 181 | } 182 | output = tmp_res.into_dyn(); 183 | } 184 | 185 | //update weights 186 | if self.backward_passes % self.batch_size == 0 { 187 | self.update_weights(); 188 | } 189 | 190 | output 191 | } 192 | } 193 | 194 | fn store_input( 195 | input: ArrayD, 196 | buffer: &mut Array2, 197 | batch_size: usize, 198 | start_pos: &mut usize, 199 | ) { 200 | // 1D case 201 | if input.ndim() == 1 { 202 | let single_input = input.into_dimensionality::().unwrap(); 203 | buffer.column_mut(*start_pos).assign(&single_input); 204 | *start_pos = (*start_pos + 1) % batch_size; 205 | return; 206 | } 207 | 208 | // 2D case 209 | assert_eq!(input.ndim(), 2); 210 | assert!( 211 | input.shape()[0] <= batch_size, 212 | format!( 213 | "error, failed assertion {} <= {}", 214 | input.shape()[0], 215 | batch_size 216 | ) 217 | ); // otherwise buffer overrun 218 | let batch_input = input.into_dimensionality::().unwrap(); 219 | let mut pos_in_buffer = *start_pos % batch_size; 220 | for single_input in batch_input.outer_iter() { 221 | buffer.column_mut(pos_in_buffer).assign(&single_input); 222 | pos_in_buffer = (pos_in_buffer + 1) % batch_size; 223 | } 224 | *start_pos = (*start_pos + batch_input.shape()[0]) % batch_size; 225 | } 226 | -------------------------------------------------------------------------------- /src/network/layer/dropout.rs: -------------------------------------------------------------------------------- 1 | use crate::network::layer::Layer; 2 | use ndarray::{Array, ArrayD}; 3 | use ndarray_rand::rand_distr::Binomial; 4 | use ndarray_rand::RandomExt; 5 | 6 | /// This layer implements a classical dropout layer. 7 | pub struct DropoutLayer { 8 | drop_prob: f32, 9 | dropout_matrix: ArrayD, 10 | } 11 | 12 | impl DropoutLayer { 13 | /// The dropout probability must be in the range [0,1]. 14 | /// 15 | /// A dropout probability of 1 results in setting every input value to 0. 16 | /// A dropout probability of 0 results in forwarding the input without changes. 17 | /// A dropout probability outside of [0,1] results in an error. 18 | pub fn new(drop_prob: f32) -> Self { 19 | DropoutLayer { 20 | drop_prob, 21 | dropout_matrix: Default::default(), 22 | } 23 | } 24 | } 25 | 26 | impl Layer for DropoutLayer { 27 | fn get_type(&self) -> String { 28 | let output = format!("Dropping: ~{:.2}%", self.drop_prob * 100.); 29 | output 30 | } 31 | 32 | fn get_num_parameter(&self) -> usize { 33 | 0 34 | } 35 | 36 | fn get_output_shape(&self, input_dim: Vec) -> Vec { 37 | input_dim 38 | } 39 | 40 | fn clone_box(&self) -> Box { 41 | Box::new(DropoutLayer::new(self.drop_prob)) 42 | } 43 | 44 | fn predict(&self, x: ArrayD) -> ArrayD { 45 | x 46 | } 47 | 48 | fn forward(&mut self, x: ArrayD) -> ArrayD { 49 | let weights = Array::random( 50 | x.shape(), 51 | Binomial::new(1, 1. - (self.drop_prob as f64)).unwrap(), 52 | ); 53 | // Scale output during training to handle dropped values. 54 | let weights = weights.mapv(|x| (x as f32) / (1. - self.drop_prob)); 55 | self.dropout_matrix = weights.into_dyn(); 56 | x * &self.dropout_matrix 57 | } 58 | 59 | fn backward(&mut self, feedback: ArrayD) -> ArrayD { 60 | feedback * &self.dropout_matrix 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/network/layer/flatten.rs: -------------------------------------------------------------------------------- 1 | use crate::network::layer::Layer; 2 | use ndarray::ArrayD; 3 | 4 | /// A flatten layer which turns higher dimensional input into a one dimension. 5 | /// 6 | /// A one dimensional input remains unchanged. 7 | pub struct FlattenLayer { 8 | input_ndim: usize, 9 | input_shape: Vec, 10 | batch_input_shape: Vec, 11 | num_elements: usize, 12 | } 13 | 14 | impl FlattenLayer { 15 | /// The input_shape is required for the backward pass. 16 | pub fn new(input_shape: Vec) -> Self { 17 | let num_elements = input_shape.clone().iter().product(); 18 | let mut batch_input_shape = vec![0]; 19 | batch_input_shape.extend_from_slice(&input_shape); 20 | FlattenLayer { 21 | input_ndim: input_shape.len(), 22 | input_shape, 23 | batch_input_shape, 24 | num_elements, 25 | } 26 | } 27 | } 28 | 29 | impl Layer for FlattenLayer { 30 | fn get_type(&self) -> String { 31 | "Flatten".to_string() 32 | } 33 | 34 | fn get_num_parameter(&self) -> usize { 35 | 0 36 | } 37 | 38 | fn get_output_shape(&self, input_dim: Vec) -> Vec { 39 | vec![input_dim.iter().product()] 40 | } 41 | 42 | fn clone_box(&self) -> Box { 43 | Box::new(FlattenLayer::new(self.input_shape.clone())) 44 | } 45 | 46 | fn predict(&self, x: ArrayD) -> ArrayD { 47 | if x.ndim() == self.input_ndim { 48 | return x.into_shape(self.num_elements).unwrap().into_dyn(); 49 | } 50 | let batch_size = x.shape()[0]; 51 | x.into_shape((batch_size, self.num_elements)) 52 | .unwrap() 53 | .into_dyn() 54 | } 55 | 56 | fn forward(&mut self, x: ArrayD) -> ArrayD { 57 | self.predict(x) 58 | } 59 | 60 | fn backward(&mut self, feedback: ArrayD) -> ArrayD { 61 | if feedback.ndim() == 1 { 62 | return feedback 63 | .into_shape(self.input_shape.clone()) 64 | .unwrap() 65 | .into_dyn(); 66 | } 67 | self.batch_input_shape[0] = feedback.shape()[0]; 68 | feedback 69 | .into_shape(self.batch_input_shape.clone()) 70 | .unwrap() 71 | .into_dyn() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/network/layer/layer_trait.rs: -------------------------------------------------------------------------------- 1 | use ndarray::ArrayD; 2 | 3 | /// Layer Interface: 4 | /// All layers passed to the neural network must implement this trait 5 | /// 6 | pub trait Layer: Send + Sync { 7 | /// A unique String to identify the layer type, e.g. "Dense" or "Flatten" 8 | /// 9 | fn get_type(&self) -> String; 10 | 11 | /// The number of trainable parameters in this Layer. 12 | /// Might be zero for some layers like "Flatten". 13 | /// 14 | fn get_num_parameter(&self) -> usize; 15 | 16 | /// Each layer is required to predict is output shape given the input shape. 17 | /// 18 | fn get_output_shape(&self, input_dim: Vec) -> Vec; 19 | 20 | /// This method is used for the inference part, when no training is required. 21 | /// It comes with a smaller memory footprint than the forward() method, which stores information for a following backward() call. 22 | /// 23 | fn predict(&self, input: ArrayD) -> ArrayD; // similar to forward(..), but no training is expected on this data. Interim results are therefore not stored 24 | 25 | /// This method is used for the forward pass during training time. 26 | /// 27 | fn forward(&mut self, input: ArrayD) -> ArrayD; 28 | 29 | /// This method is used for the weight updates during the training run. 30 | /// It is expected to update the weights, if any, accordingly and return the error for the previous layer. 31 | /// 32 | fn backward(&mut self, feedback: ArrayD) -> ArrayD; 33 | 34 | /// A function to create a boxed clone of the used Layer. 35 | fn clone_box(&self) -> Box; 36 | } 37 | -------------------------------------------------------------------------------- /src/network/layer/mod.rs: -------------------------------------------------------------------------------- 1 | mod convolution; 2 | mod dense; 3 | mod dropout; 4 | mod flatten; 5 | //pub mod conv_test; 6 | mod layer_trait; 7 | 8 | /// This trait defines all functions which a layer has to implement to be used as a part of the neural network. 9 | pub use layer_trait::Layer; 10 | 11 | /// This layer implements a classical convolution layer. 12 | pub use convolution::ConvolutionLayer2D; 13 | /// This layer implements a classical dense layer. 14 | pub use dense::DenseLayer; 15 | /// This layer implements a classical dropout layer. 16 | pub use dropout::DropoutLayer; 17 | /// This layer implements a classical dropout layer. 18 | pub use flatten::FlattenLayer; 19 | -------------------------------------------------------------------------------- /src/network/layer/reshape.rs: -------------------------------------------------------------------------------- 1 | use crate::network::layer::Layer; 2 | use ndarray::{Array, ArrayD, Ix1}; 3 | pub struct ReshapeLayer { 4 | input_shape: [usize;3], 5 | num_elements: usize, 6 | } 7 | 8 | impl ReshapeLayer { 9 | pub fn new(input_shape: [usize;3]) -> Self { 10 | FlattenLayer{ 11 | input_shape, 12 | num_elements: input_shape[0]*input_shape[1]*input_shape[2], 13 | } 14 | } 15 | } 16 | 17 | impl Layer for ReshapeLayer { 18 | 19 | fn get_type(&self) -> String { 20 | "Reshape".to_string() 21 | } 22 | 23 | fn get_num_parameter(&self) -> usize { 24 | 0 25 | } 26 | 27 | fn predict(&self, mut x: ArrayD) -> ArrayD { 28 | self.forward(x) 29 | } 30 | 31 | 32 | fn forward(&self, mut x: ArrayD) -> ArrayD { 33 | x.into_shape(self.num_elements).unwrap().into_dyn() 34 | } 35 | 36 | 37 | fn backward(&self, feedback: ArrayD) -> ArrayD{ 38 | feedback.into_shape(self.input_shape).unwrap().into_dyn() 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/network/mod.rs: -------------------------------------------------------------------------------- 1 | /// This submodule offers multiple layer implementation. 2 | /// 3 | /// The forward and backward functions have to accept and return data in the form ArrayD\. 4 | /// Common activation functions are bundled under activation_layer. 5 | pub mod layer; 6 | 7 | /// This submodules bundles all neural network related functionalities. 8 | /// 9 | /// A new neural network is created with new1d(..), new2d(..), or new3d(..). 10 | /// For higher-dimensional input a new() function is available which accepts arbitrary sized input. 11 | /// The input shape, error function and the optimizer are set during network creation. 12 | /// If an individual error function or optimizer should be used, they can be set (overwriting the former one) by using set_error_function() or set_optimizer(). 13 | /// Default layers can be added using convenience functions like add_dense(..) or add_convolution(..) which allow setting the main parameters. 14 | /// For a higher level of controll, or to add own layers, the store_layer(Box) function can be used to add a layer to the current network. 15 | pub mod nn; 16 | 17 | /// This submodule offers 5 of the most common optimizers. 18 | /// 19 | /// noop falls back to the default sgd. 20 | pub mod optimizer; 21 | 22 | /// This submodule offers stateless layers and functions. 23 | pub mod functional; 24 | 25 | mod tests; 26 | -------------------------------------------------------------------------------- /src/network/nn.rs: -------------------------------------------------------------------------------- 1 | use crate::network; 2 | use ndarray::par_azip; 3 | use ndarray::parallel::prelude::*; 4 | use ndarray::{Array1, Array2, Array3, ArrayD, Axis, Ix1, Ix2}; 5 | use network::functional::activation_layer::{ 6 | LeakyReLuLayer, ReLuLayer, SigmoidLayer, SoftmaxLayer, 7 | }; 8 | use network::functional::error::{ 9 | BinaryCrossEntropyError, CategoricalCrossEntropyError, Error, MeanSquareError, NoopError, 10 | }; 11 | //RootMeanSquareError, 12 | use network::layer::{ConvolutionLayer2D, DenseLayer, DropoutLayer, FlattenLayer, Layer}; 13 | use network::optimizer::*; 14 | 15 | #[derive(Clone)] 16 | enum Mode { 17 | Eval, 18 | Train, 19 | } 20 | 21 | #[derive(Clone, Default)] 22 | struct HyperParameter { 23 | batch_size: usize, 24 | learning_rate: f32, 25 | } 26 | 27 | impl HyperParameter { 28 | pub fn new() -> Self { 29 | HyperParameter { 30 | batch_size: 1, 31 | learning_rate: 0.002, //10e-4 32 | } 33 | } 34 | pub fn batch_size(&mut self, batch_size: usize) { 35 | if batch_size == 0 { 36 | eprintln!("batch size should be > 0! Doing nothing!"); 37 | return; 38 | } 39 | self.batch_size = batch_size; 40 | } 41 | pub fn get_batch_size(&self) -> usize { 42 | self.batch_size 43 | } 44 | pub fn set_learning_rate(&mut self, learning_rate: f32) { 45 | if learning_rate < 0. { 46 | eprintln!("learning rate should be >= 0! Doing nothing!"); 47 | return; 48 | } 49 | self.learning_rate = learning_rate; 50 | } 51 | pub fn get_learning_rate(&self) -> f32 { 52 | self.learning_rate 53 | } 54 | } 55 | 56 | // Refactor in NeuralNetwork::constructor and NeuralNetwork::executor? 57 | /// The main neural network class. It stores all relevant information. 58 | /// 59 | /// Especially all layers, as well as their input and output shape are stored and verified. 60 | pub struct NeuralNetwork { 61 | input_dims: Vec>, //each layer takes a 1 to 4-dim input. Store details here 62 | h_p: HyperParameter, 63 | layers: Vec>, 64 | error: String, //remove due to error_function 65 | error_function: Box, 66 | optimizer_function: Box, 67 | from_logits: bool, 68 | mode: Mode, 69 | } 70 | 71 | impl Clone for NeuralNetwork { 72 | fn clone(&self) -> NeuralNetwork { 73 | let new_layers: Vec<_> = self.layers.iter().map(|x| x.clone_box()).collect(); 74 | NeuralNetwork { 75 | input_dims: self.input_dims.clone(), 76 | h_p: self.h_p.clone(), 77 | layers: new_layers, 78 | error: self.error.clone(), 79 | error_function: self.error_function.clone_box(), 80 | optimizer_function: self.optimizer_function.clone_box(), 81 | from_logits: self.from_logits, 82 | mode: self.mode.clone(), 83 | } 84 | } 85 | } 86 | 87 | impl NeuralNetwork { 88 | fn get_activation(activation_type: String) -> Result, String> { 89 | match activation_type.as_str() { 90 | "softmax" => Ok(Box::new(SoftmaxLayer::new())), 91 | "sigmoid" => Ok(Box::new(SigmoidLayer::new())), 92 | "relu" => Ok(Box::new(ReLuLayer::new())), 93 | "leakyrelu" => Ok(Box::new(LeakyReLuLayer::new())), 94 | _ => Err(format!("Bad Activation Layer: {}", activation_type)), 95 | } 96 | } 97 | 98 | fn get_error(error_type: String) -> Result, String> { 99 | match error_type.as_str() { 100 | "mse" => Ok(Box::new(MeanSquareError::new())), 101 | //"rmse" => Ok(Box::new(RootMeanSquareError::new())), 102 | "bce" => Ok(Box::new(BinaryCrossEntropyError::new())), 103 | "cce" => Ok(Box::new(CategoricalCrossEntropyError::new())), 104 | "noop" => Ok(Box::new(NoopError::new())), 105 | _ => Err(format!("Unknown Error Function: {}", error_type)), 106 | } 107 | } 108 | 109 | fn get_optimizer(optimizer: String) -> Result, String> { 110 | match optimizer.as_str() { 111 | "adagrad" => Ok(Box::new(AdaGrad::new())), 112 | "rmsprop" => Ok(Box::new(RMSProp::new(0.9))), 113 | "momentum" => Ok(Box::new(Momentum::new(0.9))), 114 | "adam" => Ok(Box::new(Adam::new(0.9, 0.999))), 115 | "none" => Ok(Box::new(Noop::new())), 116 | _ => Err(format!("Unknown optimizer: {}", optimizer)), 117 | } 118 | } 119 | 120 | fn new(input_shape: Vec, error: String, optimizer: String) -> Self { 121 | let error_function; 122 | match NeuralNetwork::get_error(error.clone()) { 123 | Ok(error_fun) => error_function = error_fun, 124 | Err(warning) => { 125 | eprintln!("{}", warning); 126 | error_function = Box::new(NoopError::new()); 127 | } 128 | } 129 | let optimizer_function; 130 | match NeuralNetwork::get_optimizer(optimizer) { 131 | Ok(optimizer) => optimizer_function = optimizer, 132 | Err(warning) => { 133 | eprintln!("{}", warning); 134 | optimizer_function = Box::new(Noop::new()); 135 | } 136 | } 137 | 138 | NeuralNetwork { 139 | error, 140 | error_function, 141 | optimizer_function, 142 | input_dims: vec![input_shape], 143 | layers: vec![], 144 | h_p: HyperParameter::new(), 145 | from_logits: false, 146 | mode: Mode::Train, 147 | } 148 | } 149 | 150 | /// Sets network to inference mode, dropout and backpropagation/training are disabled. 151 | pub fn eval_mode(&mut self) { 152 | self.mode = Mode::Eval; 153 | } 154 | 155 | /// Sets network to train mode, additional calculations for weight updates might occur. 156 | pub fn train_mode(&mut self) { 157 | self.mode = Mode::Train; 158 | } 159 | 160 | /// A constructor for a neural network which takes 1d input. 161 | pub fn new1d(input_dim: usize, error: String, optimizer: String) -> Self { 162 | NeuralNetwork::new(vec![input_dim], error, optimizer) 163 | } 164 | /// A constructor for a neural network which takes 2d input. 165 | pub fn new2d( 166 | (input_dim1, input_dim2): (usize, usize), 167 | error: String, 168 | optimizer: String, 169 | ) -> Self { 170 | NeuralNetwork::new(vec![input_dim1, input_dim2], error, optimizer) 171 | } 172 | 173 | /// A constructor for a neural network which takes 3d input. 174 | pub fn new3d( 175 | (input_dim1, input_dim2, input_dim3): (usize, usize, usize), 176 | error: String, 177 | optimizer: String, 178 | ) -> Self { 179 | NeuralNetwork::new(vec![input_dim1, input_dim2, input_dim3], error, optimizer) 180 | } 181 | 182 | /// A setter to adjust the optimizer. 183 | /// 184 | /// By default, batch sgd is beeing used. 185 | pub fn set_optimizer(&mut self, optimizer: Box) { 186 | self.optimizer_function = optimizer; 187 | } 188 | 189 | /// A setter to adjust the error function. 190 | /// 191 | /// Should be picked accordingly to the last layer and the given task. 192 | pub fn set_error_function(&mut self, error: Box) { 193 | self.error_function = error; 194 | } 195 | 196 | /// A setter to adjust the batch size. 197 | /// 198 | /// By default a batch size of 1 is used, which is equal to no batch-processing. 199 | pub fn set_batch_size(&mut self, batch_size: usize) { 200 | self.h_p.batch_size(batch_size); 201 | } 202 | 203 | /// A getter for the batch size. 204 | pub fn get_batch_size(&self) -> usize { 205 | self.h_p.get_batch_size() 206 | } 207 | 208 | /// A setter to adjust the learning rate. 209 | /// 210 | /// By default a learning rate of 0.002 is used. 211 | pub fn set_learning_rate(&mut self, learning_rate: f32) { 212 | self.h_p.set_learning_rate(learning_rate); 213 | } 214 | 215 | /// A getter for the learning rate. 216 | pub fn get_learning_rate(&self) -> f32 { 217 | self.h_p.get_learning_rate() 218 | } 219 | 220 | /// This function appends a custom layer to the neural network. 221 | /// 222 | /// This function might also be used to add a custom activation function to the neural network. 223 | pub fn store_layer(&mut self, layer: Box) { 224 | let input_shape = self.input_dims.last().unwrap().clone(); 225 | self.input_dims.push(layer.get_output_shape(input_shape)); 226 | self.layers.push(layer); 227 | } 228 | 229 | /// This function appends one of the implemented activation function to the neural network. 230 | pub fn add_activation(&mut self, layer_kind: &str) { 231 | let new_activation = NeuralNetwork::get_activation(layer_kind.to_string()); 232 | match new_activation { 233 | Err(error) => { 234 | eprintln!("{}, doing nothing", error); 235 | return; 236 | } 237 | Ok(layer) => { 238 | self.store_layer(layer); 239 | } 240 | } 241 | match (self.error.as_str(), layer_kind) { 242 | ("bce", "sigmoid") => self.from_logits = true, 243 | ("cce", "softmax") => self.from_logits = true, 244 | _ => self.from_logits = false, 245 | } 246 | } 247 | 248 | /// This function appends a convolution layer to the neural network. 249 | /// 250 | /// Currently filter_shape.0 == filter_shape.1 is requred. 251 | /// If padding > 0 then the x (and if available y) dimension are padded with zeros at both sides. 252 | /// E.g. For padding == 1 and x=y=2 we might receive: 253 | /// 0000 254 | /// 0ab0 255 | /// 0cd0 256 | /// 0000 257 | /// Currently the backpropagation of the error in the convolution layer is not implemented. 258 | /// Adding a convolution layer therefore only works if this layer is the first layer in the neural network. 259 | pub fn add_convolution( 260 | &mut self, 261 | filter_shape: (usize, usize), 262 | filter_number: usize, 263 | padding: usize, 264 | ) { 265 | let filter_depth: usize; 266 | let input_dim = self.input_dims.last().unwrap().clone(); 267 | assert!( 268 | input_dim.len() == 2 || input_dim.len() == 3, 269 | "only implemented conv for 2d or 3d input! {}", 270 | input_dim.len() 271 | ); 272 | if input_dim.len() == 2 { 273 | filter_depth = 1; 274 | } else { 275 | filter_depth = input_dim[0]; 276 | } 277 | let conv_layer = ConvolutionLayer2D::new( 278 | filter_shape, 279 | filter_depth, 280 | filter_number, 281 | padding, 282 | self.h_p.batch_size, 283 | self.h_p.learning_rate, 284 | self.optimizer_function.clone_box(), 285 | ); 286 | self.store_layer(Box::new(conv_layer)); 287 | self.from_logits = false; 288 | } 289 | 290 | /// This function appends a dense (also called fully_conected) layer to the neural network. 291 | pub fn add_dense(&mut self, output_dim: usize) { 292 | if output_dim == 0 { 293 | eprintln!("output dimension should be > 0! Doing nothing!"); 294 | return; 295 | } 296 | let input_dims = self.input_dims.last().unwrap(); 297 | if input_dims.len() > 1 { 298 | eprintln!("Dense just accepts 1d input! Doing nothing!"); 299 | return; 300 | } 301 | let dense_layer = DenseLayer::new( 302 | input_dims[0], 303 | output_dim, 304 | self.h_p.batch_size, 305 | self.h_p.learning_rate, 306 | self.optimizer_function.clone_box(), 307 | ); 308 | self.store_layer(Box::new(dense_layer)); 309 | self.from_logits = false; 310 | } 311 | 312 | /// This function appends a dropout layer to the neural network. 313 | /// 314 | /// The dropout probability has to ly in the range [0,1], where 315 | /// 0 means that this layer just outputs zeros, 316 | /// 1 means that this layer outputs the (unchanged) input, 317 | /// any value x in between means that input elements are set to zero with a probability of x. 318 | pub fn add_dropout(&mut self, dropout_prob: f32) { 319 | if dropout_prob < 0. || dropout_prob > 1. { 320 | eprintln!("dropout probability has to be between 0. and 1."); 321 | return; 322 | } 323 | let dropout_layer = DropoutLayer::new(dropout_prob); 324 | self.store_layer(Box::new(dropout_layer)); 325 | self.from_logits = false; 326 | } 327 | 328 | /// This function appends a flatten layer to the neural network. 329 | /// 330 | /// 1d input remains unchanged. 331 | /// 2d or higher dimensional input is reshaped into a 1d array. 332 | pub fn add_flatten(&mut self) { 333 | let input_dims = self.input_dims.last().unwrap(); 334 | if input_dims.len() == 1 { 335 | eprintln!("Input dimension is already one! Doing nothing!"); 336 | return; 337 | } 338 | let flatten_layer = FlattenLayer::new(input_dims.to_vec()); 339 | self.store_layer(Box::new(flatten_layer)); 340 | self.from_logits = false; 341 | } 342 | 343 | fn print_separator(&self, separator: &str) { 344 | println!( 345 | "{}", 346 | std::iter::repeat(separator).take(70).collect::() 347 | ); 348 | } 349 | 350 | /// This function prints an overview of the current neural network. 351 | pub fn print_setup(&self) { 352 | println!( 353 | "\nModel: \"sequential\" {:>20} Input shape: {:?}", 354 | "", self.input_dims[0] 355 | ); 356 | self.print_separator("─"); 357 | println!( 358 | "{:<20} {:^20} {:>20}", 359 | "Layer (type)".to_string(), 360 | "Output Shape".to_string(), 361 | "Param #".to_string() 362 | ); 363 | self.print_separator("═"); 364 | for i in 0..self.layers.len() { 365 | let layer_type = format!("{:<20}", self.layers[i].get_type()); 366 | let layer_params = format!("{:>20}", self.layers[i].get_num_parameter()); 367 | let output_shape = format!("{:?}", &self.input_dims[i + 1]); 368 | println!("{} {:^20} {}", layer_type, output_shape, layer_params); 369 | self.print_separator("─"); 370 | } 371 | println!( 372 | "{:<35} {:>30}", 373 | "Error function:".to_string(), 374 | self.error_function.get_type() 375 | ); 376 | println!( 377 | "{:<35} {:>30}", 378 | "Optimizer:".to_string(), 379 | self.optimizer_function.get_type() 380 | ); 381 | println!( 382 | "{:<35} {:>30}", 383 | "using from_logits optimization:".to_string(), 384 | self.from_logits 385 | ); 386 | self.print_separator("─"); 387 | } 388 | 389 | /// This function handles the inference on 1d input. 390 | pub fn predict1d(&self, input: Array1) -> Array1 { 391 | self.predict(input.into_dyn()) 392 | } 393 | /// This function handles the inference on 2d input. 394 | pub fn predict2d(&self, input: Array2) -> Array1 { 395 | self.predict(input.into_dyn()) 396 | } 397 | /// This function handles the inference on 3d input. 398 | pub fn predict3d(&self, input: Array3) -> Array1 { 399 | self.predict(input.into_dyn()) 400 | } 401 | 402 | /// This function handles the inference on dynamic-dimensional input. 403 | pub fn predict(&self, mut input: ArrayD) -> Array1 { 404 | for i in 0..self.layers.len() { 405 | input = self.layers[i].predict(input); 406 | } 407 | input.into_dimensionality::().unwrap() //output should be Array1 again 408 | } 409 | 410 | /// This function handles the inference on a batch of dynamic-dimensional input. 411 | pub fn predict_batch(&self, mut input: ArrayD) -> Array2 { 412 | for i in 0..self.layers.len() { 413 | input = self.layers[i].predict(input); 414 | } 415 | input.into_dimensionality::().unwrap() 416 | } 417 | 418 | /// This function calculates the inference accuracy on a testset with given labels. 419 | pub fn test(&self, input: ArrayD, target: Array2) { 420 | let n = target.len_of(Axis(0)); 421 | let mut loss: Array1 = Array1::zeros(n); 422 | let mut correct: Array1 = Array1::ones(n); 423 | par_azip!((index i, l in &mut loss, c in &mut correct) { 424 | let current_input = input.index_axis(Axis(0), i); 425 | let current_fb = target.index_axis(Axis(0), i); 426 | let pred = self.predict(current_input.into_owned().into_dyn()); 427 | *l = self.loss_from_prediction(pred.clone(), current_fb.into_owned()); 428 | 429 | let best_guess: f32 = (pred.clone() * current_fb).sum(); 430 | 431 | let num: usize = pred.iter().filter(|&x| *x >= best_guess).count(); 432 | if num != 1 { 433 | *c = 0.; 434 | } 435 | }); 436 | let avg_loss = loss.par_iter().sum::() / (n as f32); 437 | let acc = correct.par_iter().sum::() / (n as f32); 438 | println!("avg loss: {}, percentage correct: {}", avg_loss, acc); 439 | } 440 | 441 | /// This function calculates the loss based on the neural network inference and a target label. 442 | pub fn loss_from_prediction(&self, prediction: Array1, target: Array1) -> f32 { 443 | let y = prediction.into_dyn(); 444 | let t = target.into_dyn(); 445 | let loss = self.error_function.forward(y, t); 446 | loss[0] 447 | } 448 | 449 | /// This function calculates the loss based on the original data and the target label. 450 | pub fn loss_from_input(&self, mut input: ArrayD, target: Array1) -> f32 { 451 | let n = self.layers.len(); 452 | for i in 0..(n - 1) { 453 | input = self.layers[i].predict(input); 454 | } 455 | 456 | let loss; 457 | if self.from_logits { 458 | loss = self 459 | .error_function 460 | .loss_from_logits(input, target.into_dyn()); 461 | } else { 462 | loss = self.error_function.forward(input, target.into_dyn()); 463 | }; 464 | loss[0] 465 | } 466 | 467 | /// This function handles training on a single 1d example. 468 | pub fn train1d(&mut self, input: Array1, target: Array1) { 469 | self.train(input.into_dyn(), target.into_dyn()); 470 | } 471 | /// This function handles training on a single 2d example. 472 | pub fn train2d(&mut self, input: Array2, target: Array1) { 473 | self.train(input.into_dyn(), target.into_dyn()); 474 | } 475 | /// This function handles training on a single 3d example. 476 | pub fn train3d(&mut self, input: Array3, target: Array1) -> Array1 { 477 | self.train(input.into_dyn(), target.into_dyn()) 478 | .into_dimensionality::() 479 | .unwrap() 480 | } 481 | /// This function handles training on a single dynamic-dimensional example. 482 | pub fn train(&mut self, mut input: ArrayD, target: ArrayD) -> ArrayD { 483 | //assert_eq!(input.len_of(Axis(0)), target.len()); //later when training on batches 484 | //maybe return option(accuracy,None) and add a setter to return accuracy? 485 | let n = self.layers.len(); 486 | 487 | // forward pass 488 | // handle layers 1 till pre-last 489 | for i in 0..(n - 1) { 490 | input = self.layers[i].forward(input); 491 | } 492 | let res = input.clone(); 493 | 494 | let mut feedback; 495 | // handle last layer and error function 496 | if self.from_logits { 497 | //merge last layer with error function 498 | feedback = self 499 | .error_function 500 | .deriv_from_logits(input, target.into_dyn()); 501 | // to print error function loss here: 502 | // println!("{}", self.error_function.loss_from_logits(input, target); 503 | } else { 504 | //evaluate last activation layer and error function seperately 505 | input = self.layers[n - 1].forward(input); 506 | // to print error function loss here: 507 | // println!("{}", self.error_function.loss(input, target); 508 | feedback = self.error_function.backward(input, target.into_dyn()); 509 | feedback = self.layers[n - 1].backward(feedback); 510 | } 511 | 512 | // backward pass 513 | // handle pre-last till first layer 514 | for i in (0..(n - 1)).rev() { 515 | feedback = self.layers[i].backward(feedback); 516 | } 517 | res 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/network/optimizer/adagrad.rs: -------------------------------------------------------------------------------- 1 | use super::optimizer_trait::Optimizer; 2 | use ndarray::{Array, Array1, Array2, Array3, ArrayD, Ix1, Ix2, Ix3}; 3 | 4 | /// An optimizer for more efficient weight updates. 5 | #[derive(Default, Clone)] 6 | pub struct AdaGrad { 7 | previous_sum_squared: ArrayD, 8 | } 9 | 10 | impl AdaGrad { 11 | /// No parameters available. 12 | pub fn new() -> Self { 13 | AdaGrad { 14 | previous_sum_squared: Array::zeros(0).into_dyn(), 15 | } 16 | } 17 | } 18 | 19 | impl Optimizer for AdaGrad { 20 | fn get_type(&self) -> String { 21 | format!("AdaGrad") 22 | } 23 | fn set_input_shape(&mut self, shape: Vec) { 24 | self.previous_sum_squared = Array::zeros(shape); 25 | } 26 | fn optimize(&mut self, delta_w: ArrayD) -> ArrayD { 27 | self.previous_sum_squared = 28 | self.previous_sum_squared.clone() + delta_w.mapv(|x| x.powf(2.)); 29 | delta_w / self.previous_sum_squared.mapv(f32::sqrt) 30 | } 31 | fn optimize1d(&mut self, delta_w: Array1) -> Array1 { 32 | self.optimize(delta_w.into_dyn()) 33 | .into_dimensionality::() 34 | .unwrap() 35 | } 36 | fn optimize2d(&mut self, delta_w: Array2) -> Array2 { 37 | self.optimize(delta_w.into_dyn()) 38 | .into_dimensionality::() 39 | .unwrap() 40 | } 41 | fn optimize3d(&mut self, delta_w: Array3) -> Array3 { 42 | self.optimize(delta_w.into_dyn()) 43 | .into_dimensionality::() 44 | .unwrap() 45 | } 46 | fn clone_box(&self) -> Box { 47 | Box::new(Clone::clone(self)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/network/optimizer/adam.rs: -------------------------------------------------------------------------------- 1 | use super::optimizer_trait::Optimizer; 2 | use ndarray::{Array, Array1, Array2, Array3, ArrayD, Ix1, Ix2, Ix3}; 3 | 4 | /// An optimizer for more efficient weight updates. 5 | #[derive(Clone)] 6 | pub struct Adam { 7 | previous_sum: ArrayD, 8 | previous_sum_squared: ArrayD, 9 | beta1: f32, 10 | beta2: f32, 11 | t: f32, 12 | } 13 | 14 | impl Default for Adam { 15 | fn default() -> Self { 16 | Adam::new(0.9, 0.999) 17 | } 18 | } 19 | 20 | impl Adam { 21 | /// Common values for beta1 and beta2 are 0.9 and 0.999. 22 | pub fn new(beta1: f32, beta2: f32) -> Self { 23 | Adam { 24 | previous_sum: Array::zeros(0).into_dyn(), 25 | previous_sum_squared: Array::zeros(0).into_dyn(), 26 | beta1, 27 | beta2, 28 | t: 1., 29 | } 30 | } 31 | } 32 | 33 | impl Optimizer for Adam { 34 | fn get_type(&self) -> String { 35 | format!("Adam") 36 | } 37 | fn set_input_shape(&mut self, shape: Vec) { 38 | self.previous_sum = Array::zeros(shape.clone()); 39 | self.previous_sum_squared = Array::zeros(shape); 40 | } 41 | fn optimize(&mut self, delta_w: ArrayD) -> ArrayD { 42 | self.previous_sum = &self.previous_sum * self.beta1 + delta_w.clone() * (1. - self.beta1); 43 | self.previous_sum_squared = &self.previous_sum_squared * self.beta2 44 | + delta_w.mapv(|x| f32::powf(x, 2.)) * (1. - self.beta2); 45 | let sum_bias_corrected = self.previous_sum.clone() / (1. - self.beta1.powf(self.t)); 46 | let sum_squared_bias_corrected = 47 | self.previous_sum_squared.clone() / (1. - self.beta2.powf(self.t)); 48 | self.t += 1.; 49 | sum_bias_corrected / (sum_squared_bias_corrected.mapv(f32::sqrt) + 1e-8) 50 | //self.previous_sum.clone() / (self.previous_sum_squared.mapv(f32::sqrt) + 1e-8) 51 | } 52 | fn optimize1d(&mut self, delta_w: Array1) -> Array1 { 53 | self.optimize(delta_w.into_dyn()) 54 | .into_dimensionality::() 55 | .unwrap() 56 | } 57 | fn optimize2d(&mut self, delta_w: Array2) -> Array2 { 58 | self.optimize(delta_w.into_dyn()) 59 | .into_dimensionality::() 60 | .unwrap() 61 | } 62 | fn optimize3d(&mut self, delta_w: Array3) -> Array3 { 63 | self.optimize(delta_w.into_dyn()) 64 | .into_dimensionality::() 65 | .unwrap() 66 | } 67 | fn clone_box(&self) -> Box { 68 | Box::new(Clone::clone(self)) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/network/optimizer/mod.rs: -------------------------------------------------------------------------------- 1 | mod adagrad; 2 | mod adam; 3 | mod momentum; 4 | mod noop; 5 | mod rmsprop; 6 | 7 | mod optimizer_trait; 8 | pub use optimizer_trait::Optimizer; 9 | 10 | pub use adagrad::AdaGrad; 11 | pub use adam::Adam; 12 | pub use momentum::Momentum; 13 | pub use noop::Noop; 14 | pub use rmsprop::RMSProp; 15 | -------------------------------------------------------------------------------- /src/network/optimizer/momentum.rs: -------------------------------------------------------------------------------- 1 | use super::optimizer_trait::Optimizer; 2 | use ndarray::{Array, Array1, Array2, Array3, ArrayD, Ix1, Ix2, Ix3}; 3 | 4 | /// An optimizer for more efficient weight updates. 5 | #[derive(Clone)] 6 | pub struct Momentum { 7 | previous_delta: ArrayD, 8 | decay_rate: f32, 9 | } 10 | 11 | impl Momentum { 12 | /// A basic optimization over sgd. 13 | /// A common value might be 0.9. 14 | pub fn new(decay_rate: f32) -> Self { 15 | Momentum { 16 | previous_delta: Array::zeros(0).into_dyn(), 17 | decay_rate, 18 | } 19 | } 20 | } 21 | 22 | impl Optimizer for Momentum { 23 | fn get_type(&self) -> String { 24 | format!("Momentum") 25 | } 26 | fn set_input_shape(&mut self, shape: Vec) { 27 | self.previous_delta = Array::zeros(shape); 28 | } 29 | fn optimize(&mut self, mut delta_w: ArrayD) -> ArrayD { 30 | delta_w = delta_w + &self.previous_delta * self.decay_rate; 31 | self.previous_delta = delta_w.clone(); 32 | delta_w 33 | } 34 | fn optimize1d(&mut self, delta_w: Array1) -> Array1 { 35 | self.optimize(delta_w.into_dyn()) 36 | .into_dimensionality::() 37 | .unwrap() 38 | } 39 | fn optimize2d(&mut self, delta_w: Array2) -> Array2 { 40 | self.optimize(delta_w.into_dyn()) 41 | .into_dimensionality::() 42 | .unwrap() 43 | } 44 | fn optimize3d(&mut self, delta_w: Array3) -> Array3 { 45 | self.optimize(delta_w.into_dyn()) 46 | .into_dimensionality::() 47 | .unwrap() 48 | } 49 | fn clone_box(&self) -> Box { 50 | Box::new(Clone::clone(self)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/network/optimizer/noop.rs: -------------------------------------------------------------------------------- 1 | use super::optimizer_trait::Optimizer; 2 | use ndarray::{Array1, Array2, Array3, ArrayD}; 3 | 4 | /// The basic sgd weight update procedure. 5 | #[derive(Default, Clone)] 6 | pub struct Noop {} 7 | 8 | impl Noop { 9 | /// The basic sgd weight update procedure. 10 | pub fn new() -> Self { 11 | Noop {} 12 | } 13 | } 14 | 15 | impl Optimizer for Noop { 16 | fn get_type(&self) -> String { 17 | format!("None") 18 | } 19 | fn set_input_shape(&mut self, _shape: Vec) {} 20 | fn optimize(&mut self, delta_w: ArrayD) -> ArrayD { 21 | delta_w 22 | } 23 | fn optimize1d(&mut self, delta_w: Array1) -> Array1 { 24 | delta_w 25 | } 26 | fn optimize2d(&mut self, delta_w: Array2) -> Array2 { 27 | delta_w 28 | } 29 | fn optimize3d(&mut self, delta_w: Array3) -> Array3 { 30 | delta_w 31 | } 32 | fn clone_box(&self) -> Box { 33 | Box::new(Clone::clone(self)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/network/optimizer/optimizer_trait.rs: -------------------------------------------------------------------------------- 1 | use ndarray::{Array1, Array2, Array3, ArrayD}; 2 | 3 | /// A trait defining functions to alter the weight and bias updates before they are applied. 4 | /// 5 | /// All neural network layers are expected to call the coresponding functions after calculating the deltas 6 | /// and only to apply the results of these functions to update their weights or biases. 7 | pub trait Optimizer: Send + Sync { 8 | /// 9 | fn set_input_shape(&mut self, shape: Vec); 10 | /// Returns a string identifying the specific optimizer type. Examples are "Adam", "Momentum", and "None" for basic sgd. 11 | fn get_type(&self) -> String; 12 | /// Applies the specific optimization to dynamically shaped arrays. 13 | fn optimize(&mut self, weight_update: ArrayD) -> ArrayD; 14 | /// A wrapper around optimize(). 15 | fn optimize1d(&mut self, weight_update: Array1) -> Array1; 16 | /// A wrapper around optimize() 17 | fn optimize2d(&mut self, weight_update: Array2) -> Array2; 18 | /// A wrapper around optimize() 19 | fn optimize3d(&mut self, weight_update: Array3) -> Array3; 20 | 21 | /// Allows each layer to create a copy of the given optimizer in case that he has more than one array to update. 22 | /// 23 | /// Should be called before calling any of the optimizeX functions. 24 | fn clone_box(&self) -> Box; 25 | } 26 | -------------------------------------------------------------------------------- /src/network/optimizer/rmsprop.rs: -------------------------------------------------------------------------------- 1 | use super::optimizer_trait::Optimizer; 2 | use ndarray::{Array, Array1, Array2, Array3, ArrayD, Ix1, Ix2, Ix3}; 3 | 4 | /// An optimizer for more efficient weight updates. 5 | #[derive(Clone)] 6 | pub struct RMSProp { 7 | previous_sum_squared: ArrayD, 8 | decay_rate: f32, 9 | } 10 | 11 | impl RMSProp { 12 | /// A constructor including the decay rate. A common value is 0.9. 13 | pub fn new(decay_rate: f32) -> Self { 14 | RMSProp { 15 | previous_sum_squared: Array::zeros(0).into_dyn(), 16 | decay_rate, 17 | } 18 | } 19 | } 20 | 21 | impl Optimizer for RMSProp { 22 | fn get_type(&self) -> String { 23 | format!("RMSProp") 24 | } 25 | fn set_input_shape(&mut self, shape: Vec) { 26 | self.previous_sum_squared = Array::zeros(shape); 27 | } 28 | fn optimize(&mut self, delta_w: ArrayD) -> ArrayD { 29 | self.previous_sum_squared = self.previous_sum_squared.clone() * self.decay_rate 30 | + delta_w.mapv(|x| x.powf(2.)) * (1. - self.decay_rate); 31 | delta_w / self.previous_sum_squared.mapv(f32::sqrt) 32 | } 33 | fn optimize1d(&mut self, delta_w: Array1) -> Array1 { 34 | self.optimize(delta_w.into_dyn()) 35 | .into_dimensionality::() 36 | .unwrap() 37 | } 38 | fn optimize2d(&mut self, delta_w: Array2) -> Array2 { 39 | self.optimize(delta_w.into_dyn()) 40 | .into_dimensionality::() 41 | .unwrap() 42 | } 43 | fn optimize3d(&mut self, delta_w: Array3) -> Array3 { 44 | self.optimize(delta_w.into_dyn()) 45 | .into_dimensionality::() 46 | .unwrap() 47 | } 48 | fn clone_box(&self) -> Box { 49 | Box::new(Clone::clone(self)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/network/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[allow(non_snake_case)] 3 | mod MLP { 4 | use crate::network::nn::NeuralNetwork; 5 | use datasets::mnist; 6 | use ndarray::{array, Array, Array2, Axis}; 7 | use rand::Rng; 8 | 9 | fn new(i_dim: usize, bs: usize, lr: f32) -> NeuralNetwork { 10 | let mut nn = NeuralNetwork::new1d(i_dim, "bce".to_string(), "adam".to_string()); 11 | nn.set_batch_size(bs); 12 | nn.set_learning_rate(lr); 13 | nn.add_dense(2); //Dense with 2 output neurons 14 | nn.add_activation("sigmoid"); //Sigmoid 15 | nn.add_dense(1); //Dense with 1 output neuron 16 | nn.add_activation("sigmoid"); //Sigmoid 17 | nn 18 | } 19 | 20 | fn test(nn: NeuralNetwork, input: Array2, feedback: Array2, testname: String) { 21 | let mut current_input; 22 | let mut current_feedback; 23 | for i in 0..input.nrows() { 24 | current_input = input.row(i).into_owned().clone(); 25 | current_feedback = feedback.row(i).into_owned().clone(); 26 | 27 | let prediction = nn.predict1d(current_input.clone()); 28 | //let diff = nn.loss_from_prediction(prediction.clone(), current_feedback.clone()); 29 | let diff = 30 | nn.loss_from_input(current_input.clone().into_dyn(), current_feedback.clone()); 31 | assert!( 32 | diff < 0.2, 33 | "failed learning: {}. Achieved loss: {}\n input: {} output was: {:} should {:}", 34 | testname, 35 | diff, 36 | current_input.clone(), 37 | prediction, 38 | current_feedback 39 | ); 40 | } 41 | } 42 | 43 | fn train(nn: &mut NeuralNetwork, num: usize, input: &Array2, feedback: &Array2) { 44 | let mut pos; 45 | let mut current_input; 46 | let mut current_feedback; 47 | for _ in 0..num { 48 | pos = rand::thread_rng().gen_range(0..input.nrows()) as usize; 49 | current_input = input.row(pos).into_owned().clone(); 50 | current_feedback = feedback.row(pos).into_owned().clone(); 51 | nn.train1d(current_input, current_feedback); 52 | } 53 | } 54 | 55 | #[allow(non_snake_case)] 56 | #[test] 57 | #[ignore] 58 | fn MNIST() { 59 | let (trn_size, rows, cols) = (60_000, 28, 28); 60 | 61 | // Deconstruct the returned Mnist struct. 62 | let mnist::Data { trn_img, .. } = mnist::new_normalized(); 63 | assert_eq!(trn_img.shape(), &[trn_size, rows, cols]); 64 | } 65 | 66 | #[test] 67 | fn and() { 68 | let input = array![[0., 0.], [0., 1.], [1., 0.], [1., 1.], [1., 1.], [1., 1.]]; // AND 69 | let feedback = array![[0.], [0.], [0.], [1.], [1.], [1.]]; //AND work ok with 200k examples (10 and 01 are classified correctly, but close to 0.5) 70 | let mut nn = new(2, 6, 0.1); 71 | train(&mut nn, 1_000, &input, &feedback); 72 | test(nn, input, feedback, "and".to_string()); 73 | } 74 | 75 | #[test] 76 | fn test_and_batch2_input() { 77 | let input = array![[0., 0.], [0., 1.], [1., 0.], [1., 1.], [1., 1.], [1., 1.]]; // AND 78 | let feedback = array![[0.], [0.], [0.], [1.], [1.], [1.]]; //AND work ok with 200k examples (10 and 01 are classified correctly, but close to 0.5) 79 | let mut nn = new(2, 6, 0.1); 80 | train(&mut nn, 2_000, &input, &feedback); 81 | let diff = (nn.predict_batch(input.into_dyn()) - feedback) 82 | .mapv(|x| x.abs()) 83 | .sum(); 84 | assert!(diff < 0.2, "error, diff was {}", diff); 85 | } 86 | 87 | #[test] 88 | fn train_batch2_input() { 89 | //nn: &mut NeuralNetwork, num: usize, input: &Array4, fb: &Array2) { 90 | let input = array![[0., 0.], [0., 1.], [1., 0.], [1., 1.], [1., 1.], [1., 1.]]; // AND 91 | let feedback = array![[0.], [0.], [0.], [1.], [1.], [1.]]; //AND work ok with 200k examples (10 and 01 are classified correctly, but close to 0.5) 92 | let mut nn = new(2, 6, 0.1); 93 | 94 | for _ in 0..10 { 95 | let pos1 = rand::thread_rng().gen_range(0..input.shape()[0]) as usize; 96 | let pos2 = rand::thread_rng().gen_range(0..input.shape()[0]) as usize; 97 | let current_input1 = input.index_axis(Axis(0), pos1).into_owned(); 98 | let current_input2 = input.index_axis(Axis(0), pos2).into_owned(); 99 | let current_fb1 = feedback.index_axis(Axis(0), pos1).into_owned(); 100 | let current_fb2 = feedback.index_axis(Axis(0), pos2).into_owned(); 101 | println!("cfb1: {}", current_fb1); 102 | let mut current_fb = Array::zeros((6, 1)); 103 | current_fb.index_axis_mut(Axis(0), 0).assign(¤t_fb1); 104 | current_fb.index_axis_mut(Axis(0), 1).assign(¤t_fb2); 105 | let mut current_input = Array::zeros((6, 2)); 106 | current_input 107 | .index_axis_mut(Axis(0), 0) 108 | .assign(¤t_input1); 109 | current_input 110 | .index_axis_mut(Axis(0), 1) 111 | .assign(¤t_input2); 112 | nn.train(current_input.into_dyn(), current_fb.into_dyn()); 113 | } 114 | } 115 | 116 | #[test] 117 | fn test_entire_batch_input() { 118 | //nn: &mut NeuralNetwork, num: usize, input: &Array4, fb: &Array2) { 119 | let input = array![[0., 0.], [0., 1.], [1., 0.], [1., 1.], [1., 1.], [1., 1.]]; // AND 120 | let feedback = array![[0.], [0.], [0.], [1.], [1.], [1.]]; //AND work ok with 200k examples (10 and 01 are classified correctly, but close to 0.5) 121 | let mut nn = new(2, 6, 0.1); 122 | nn.train(input.into_dyn(), feedback.into_dyn()); 123 | } 124 | 125 | #[test] 126 | fn clone_nn() { 127 | let input = array![[0., 0.], [0., 1.], [1., 0.], [1., 1.], [1., 1.], [1., 1.]]; // AND 128 | let feedback = array![[0.], [0.], [0.], [1.], [1.], [1.]]; //AND work ok with 200k examples (10 and 01 are classified correctly, but close to 0.5) 129 | let mut nn = new(2, 6, 0.1); 130 | train(&mut nn, 1_000, &input, &feedback); 131 | test( 132 | nn.clone(), 133 | input.clone(), 134 | feedback.clone(), 135 | "copy_init_failed".to_string(), 136 | ); 137 | let nn_clone = nn.clone(); 138 | test(nn_clone, input, feedback, "clone_failed".to_string()); 139 | } 140 | 141 | #[test] 142 | fn or() { 143 | let input = array![[0., 0.], [0., 0.], [0., 0.], [0., 1.], [1., 0.], [1., 1.]]; // OR 144 | let feedback = array![[0.], [0.], [0.], [1.], [1.], [1.]]; //OR works great with 200k examples 145 | let mut nn = new(2, 6, 0.1); 146 | train(&mut nn, 5000, &input, &feedback); 147 | test(nn, input, feedback, "or".to_string()); 148 | } 149 | 150 | #[test] 151 | fn not() { 152 | let input = array![[0.], [1.]]; 153 | let feedback = array![[1.], [0.]]; // NOT works great with 200k examples 154 | let mut nn = new(1, 1, 0.1); 155 | train(&mut nn, 500, &input, &feedback); 156 | test(nn, input, feedback, "not".to_string()); 157 | } 158 | 159 | #[test] 160 | fn first() { 161 | let input = array![[0., 0.], [0., 1.], [1., 0.], [1., 1.]]; // FIRST 162 | let feedback = array![[0.], [0.], [1.], [1.]]; //First works good with 200k examples 163 | let mut nn = new(2, 4, 0.1); 164 | train(&mut nn, 500, &input, &feedback); 165 | test(nn, input, feedback, "first".to_string()); 166 | } 167 | 168 | #[test] 169 | #[ignore] 170 | fn xor() { 171 | let input = array![[0., 0.], [0., 1.], [1., 0.], [1., 1.]]; 172 | let feedback = array![[0.], [1.], [1.], [0.]]; //XOR 173 | let mut nn = new(2, 4, 0.1); 174 | train(&mut nn, 30_000, &input, &feedback); 175 | test(nn, input, feedback, "xor".to_string()); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/rl/agent/agent_trait.rs: -------------------------------------------------------------------------------- 1 | use ndarray::{Array1, Array2}; 2 | 3 | /// A trait including all functions required to train them. 4 | pub trait Agent { 5 | /// Returns a simple string identifying the specific agent type. 6 | fn get_id(&self) -> String; 7 | 8 | /// Expect the agent to return a single usize value corresponding to a (legal) action he picked. 9 | /// 10 | /// The concrete encoding of actions as usize value has to be looked up in the documentation of the specific environment. 11 | /// Advanced agents shouldn't need knowledge about the used encoding. 12 | fn get_move(&mut self, env: Array2, actions: Array1, reward: f32) -> usize; 13 | 14 | /// Informs the agent that the current epoch has finished and tells him about his final result. 15 | /// 16 | /// Common values might be -1./0./1. if the agent achieved a loss/draw/win. 17 | fn finish_round(&mut self, result: i8, final_state: Array2); 18 | 19 | /// Updates the exploration rate if it lies in the range [0,1]. 20 | fn set_exploration_rate(&mut self, e: f32) -> Result<(), String>; 21 | 22 | /// Returns the current exploration rate. 23 | fn get_exploration_rate(&self) -> f32; 24 | 25 | /// Updates the learning rate if it lies in the range [0,1]. 26 | fn set_learning_rate(&mut self, e: f32) -> Result<(), String>; 27 | 28 | /// Returns the current learning rate. 29 | fn get_learning_rate(&self) -> f32; 30 | } 31 | -------------------------------------------------------------------------------- /src/rl/agent/ddql_agent.rs: -------------------------------------------------------------------------------- 1 | use super::results::RunningResults; 2 | use crate::network::nn::NeuralNetwork; 3 | use crate::rl::agent::Agent; 4 | use crate::rl::algorithms::DQlearning; 5 | use ndarray::{Array1, Array2}; 6 | 7 | /// An agent using Deep-Q-Learning, based on a small neural network. 8 | pub struct DDQLAgent { 9 | ddqlearning: DQlearning, 10 | results: RunningResults, 11 | } 12 | 13 | // based on Q-learning using a HashMap as table 14 | // 15 | impl DDQLAgent { 16 | /// A constructor including an initial exploration rate. 17 | pub fn new(exploration: f32, batch_size: usize, nn: NeuralNetwork) -> Self { 18 | DDQLAgent { 19 | ddqlearning: DQlearning::new(exploration, batch_size, nn, true), 20 | results: RunningResults::new(1000, true), 21 | } 22 | } 23 | } 24 | 25 | impl Agent for DDQLAgent { 26 | fn get_id(&self) -> String { 27 | "ddqlearning agent".to_string() 28 | } 29 | 30 | fn finish_round(&mut self, reward: i8, final_state: Array2) { 31 | self.results.add(reward.into()); 32 | self.ddqlearning.finish_round(reward.into(), final_state); 33 | } 34 | 35 | fn get_move(&mut self, board: Array2, actions: Array1, reward: f32) -> usize { 36 | self.ddqlearning.get_move(board, actions, reward) 37 | } 38 | 39 | fn get_learning_rate(&self) -> f32 { 40 | self.ddqlearning.get_learning_rate() 41 | } 42 | 43 | fn set_learning_rate(&mut self, lr: f32) -> Result<(), String> { 44 | self.ddqlearning.set_learning_rate(lr) 45 | } 46 | 47 | fn get_exploration_rate(&self) -> f32 { 48 | self.ddqlearning.get_exploration_rate() 49 | } 50 | 51 | fn set_exploration_rate(&mut self, e: f32) -> Result<(), String> { 52 | if !(0.0..=1.).contains(&e) { 53 | return Err("exploration rate must be in [0,1]!".to_string()); 54 | } 55 | self.ddqlearning.set_exploration_rate(e)?; 56 | Ok(()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/rl/agent/dql_agent.rs: -------------------------------------------------------------------------------- 1 | use super::results::RunningResults; 2 | use crate::network::nn::NeuralNetwork; 3 | use crate::rl::agent::Agent; 4 | use crate::rl::algorithms::DQlearning; 5 | use ndarray::{Array1, Array2}; 6 | 7 | /// An agent using Deep-Q-Learning, based on a small neural network. 8 | pub struct DQLAgent { 9 | dqlearning: DQlearning, 10 | results: RunningResults, 11 | } 12 | 13 | // based on Q-learning using a HashMap as table 14 | // 15 | impl DQLAgent { 16 | /// A constructor including an initial exploration rate. 17 | pub fn new(exploration: f32, batch_size: usize, nn: NeuralNetwork) -> Self { 18 | DQLAgent { 19 | results: RunningResults::new(100, true), 20 | dqlearning: DQlearning::new(exploration, batch_size, nn, false), 21 | } 22 | } 23 | } 24 | 25 | impl Agent for DQLAgent { 26 | fn get_id(&self) -> String { 27 | "dqlearning agent".to_string() 28 | } 29 | 30 | fn finish_round(&mut self, reward: i8, final_state: Array2) { 31 | self.results.add(reward.into()); 32 | self.dqlearning.finish_round(reward.into(), final_state); 33 | } 34 | 35 | fn get_move(&mut self, board: Array2, actions: Array1, reward: f32) -> usize { 36 | self.dqlearning.get_move(board, actions, reward) 37 | } 38 | 39 | fn get_learning_rate(&self) -> f32 { 40 | self.dqlearning.get_learning_rate() 41 | } 42 | 43 | fn set_learning_rate(&mut self, lr: f32) -> Result<(), String> { 44 | self.dqlearning.set_learning_rate(lr) 45 | } 46 | 47 | fn get_exploration_rate(&self) -> f32 { 48 | self.dqlearning.get_exploration_rate() 49 | } 50 | 51 | fn set_exploration_rate(&mut self, e: f32) -> Result<(), String> { 52 | if !(0.0..=1.).contains(&e) { 53 | return Err("exploration rate must be in [0,1]!".to_string()); 54 | } 55 | self.dqlearning.set_exploration_rate(e)?; 56 | Ok(()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/rl/agent/human_player.rs: -------------------------------------------------------------------------------- 1 | use crate::rl::agent::agent_trait::Agent; 2 | use ndarray::{Array1, Array2}; 3 | use std::io; 4 | 5 | /// An agent which just shows the user the current environment and lets the user decide about each action. 6 | #[derive(Default)] 7 | pub struct HumanPlayer {} 8 | 9 | impl HumanPlayer { 10 | /// No arguments required since the user has to take over the agents decisions. 11 | pub fn new() -> Self { 12 | HumanPlayer {} 13 | } 14 | } 15 | 16 | impl Agent for HumanPlayer { 17 | fn get_id(&self) -> String { 18 | "human player".to_string() 19 | } 20 | 21 | fn get_move(&mut self, board: Array2, actions: Array1, _: f32) -> usize { 22 | let (n, m) = (board.shape()[0], board.shape()[1]); 23 | for i in 0..n { 24 | for j in 0..m { 25 | print!("{} ", board[[i, j]]); 26 | } 27 | println!(); 28 | } 29 | loop { 30 | let mut next_action = String::new(); 31 | println!("please insert the number of your next action.\n It should be a number between 1 and {}", actions.len()); 32 | println!("{}", actions); 33 | io::stdin() 34 | .read_line(&mut next_action) 35 | .expect("Failed to read number of rounds"); 36 | let mut next_action: usize = next_action.trim().parse().expect("please type a number"); 37 | next_action -= 1; //from human to cs indexing. 38 | 39 | // assert choosen move exists and is legal 40 | if next_action < actions.len() && actions[next_action] { 41 | // human(non cs) friendly counting 42 | return next_action; 43 | } else { 44 | eprintln!("The selected move was illegal. Please try again.\n"); 45 | } 46 | } 47 | } 48 | 49 | fn finish_round(&mut self, _single_res: i8, _final_state: Array2) {} 50 | 51 | fn get_learning_rate(&self) -> f32 { 52 | 42. 53 | } 54 | 55 | fn set_learning_rate(&mut self, _e: f32) -> Result<(), String> { 56 | Ok(()) 57 | } 58 | 59 | fn get_exploration_rate(&self) -> f32 { 60 | 42. 61 | } 62 | 63 | fn set_exploration_rate(&mut self, _e: f32) -> Result<(), String> { 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/rl/agent/mod.rs: -------------------------------------------------------------------------------- 1 | mod agent_trait; 2 | mod results; 3 | 4 | mod ddql_agent; 5 | mod dql_agent; 6 | mod human_player; 7 | mod ql_agent; 8 | mod random_agent; 9 | 10 | pub use agent_trait::Agent; 11 | 12 | pub use ddql_agent::DDQLAgent; 13 | pub use dql_agent::DQLAgent; 14 | pub use human_player::HumanPlayer; 15 | pub use ql_agent::QLAgent; 16 | pub use random_agent::RandomAgent; 17 | -------------------------------------------------------------------------------- /src/rl/agent/ql_agent.rs: -------------------------------------------------------------------------------- 1 | use super::results::RunningResults; 2 | use crate::rl::algorithms::Qlearning; 3 | use ndarray::{Array1, Array2}; 4 | 5 | use crate::rl::agent::Agent; 6 | 7 | /// An agent working on a classical q-table. 8 | pub struct QLAgent { 9 | qlearning: Qlearning, 10 | results: RunningResults, 11 | } 12 | 13 | // based on Q-learning using a HashMap as table 14 | // 15 | impl QLAgent { 16 | /// A constructor with an initial exploration rate. 17 | pub fn new(exploration: f32, learning: f32, action_space_length: usize) -> Self { 18 | QLAgent { 19 | qlearning: Qlearning::new(exploration, learning, action_space_length), 20 | results: RunningResults::new(100, true), 21 | } 22 | } 23 | } 24 | 25 | impl Agent for QLAgent { 26 | fn get_id(&self) -> String { 27 | "qlearning agent".to_string() 28 | } 29 | 30 | fn finish_round(&mut self, reward: i8, final_state: Array2) { 31 | self.results.add(reward.into()); 32 | self.qlearning.finish_round(reward.into(), final_state); 33 | } 34 | 35 | fn get_move(&mut self, board: Array2, actions: Array1, reward: f32) -> usize { 36 | self.qlearning.get_move(board, actions, reward) 37 | } 38 | 39 | fn set_learning_rate(&mut self, lr: f32) -> Result<(), String> { 40 | if !(0.0..=1.).contains(&lr) { 41 | return Err("learning rate must be in [0,1]!".to_string()); 42 | } 43 | self.qlearning.set_learning_rate(lr)?; 44 | Ok(()) 45 | } 46 | 47 | fn get_learning_rate(&self) -> f32 { 48 | self.qlearning.get_learning_rate() 49 | } 50 | 51 | fn get_exploration_rate(&self) -> f32 { 52 | self.qlearning.get_exploration_rate() 53 | } 54 | 55 | fn set_exploration_rate(&mut self, e: f32) -> Result<(), String> { 56 | if !(0.0..=1.).contains(&e) { 57 | return Err("exploration rate must be in [0,1]!".to_string()); 58 | } 59 | self.qlearning.set_exploration_rate(e)?; 60 | Ok(()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/rl/agent/random_agent.rs: -------------------------------------------------------------------------------- 1 | use crate::rl::agent::agent_trait::Agent; 2 | use crate::rl::algorithms::utils; 3 | use ndarray::{Array1, Array2}; 4 | 5 | /// An agent who acts randomly. 6 | /// 7 | /// All input is ignored except of the vector of possible actions. 8 | /// All allowed actions are considered with an equal probability. 9 | #[derive(Default)] 10 | pub struct RandomAgent {} 11 | 12 | impl RandomAgent { 13 | /// Returns a new instance of a random acting agent. 14 | pub fn new() -> Self { 15 | RandomAgent {} 16 | } 17 | } 18 | 19 | impl Agent for RandomAgent { 20 | fn get_id(&self) -> String { 21 | "random agent".to_string() 22 | } 23 | 24 | fn get_move(&mut self, _: Array2, actions: Array1, _: f32) -> usize { 25 | utils::get_random_true_entry(actions) 26 | } 27 | 28 | fn finish_round(&mut self, _single_res: i8, _final_state: Array2) {} 29 | 30 | fn get_learning_rate(&self) -> f32 { 31 | 42. 32 | } 33 | 34 | fn set_learning_rate(&mut self, _e: f32) -> Result<(), String> { 35 | Ok(()) 36 | } 37 | 38 | fn get_exploration_rate(&self) -> f32 { 39 | 42. 40 | } 41 | 42 | fn set_exploration_rate(&mut self, _e: f32) -> Result<(), String> { 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/rl/agent/results.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default)] 2 | pub struct RunningResults { 3 | n: usize, 4 | print: bool, 5 | full: bool, 6 | current_pos: usize, 7 | won: u32, 8 | draw: u32, 9 | lost: u32, 10 | results: Vec, 11 | } 12 | 13 | impl RunningResults { 14 | pub fn new(n: usize, print: bool) -> RunningResults { 15 | RunningResults { 16 | n, 17 | print, 18 | results: vec![0; n], 19 | ..Default::default() 20 | } 21 | } 22 | 23 | pub fn add(&mut self, result: i32) { 24 | if self.full { 25 | match self.results[self.current_pos] { 26 | -1 => self.lost -= 1, 27 | 0 => self.draw -= 1, 28 | 1 => self.won -= 1, 29 | _ => panic!(), 30 | } 31 | } 32 | self.results[self.current_pos] = result; 33 | self.current_pos = (self.current_pos + 1) % self.n; 34 | if self.current_pos == 0 { 35 | self.full = true; 36 | } 37 | match result { 38 | -1 => self.lost += 1, 39 | 0 => self.draw += 1, 40 | 1 => self.won += 1, 41 | _ => panic!(), 42 | } 43 | if self.current_pos == 0 { 44 | println!( 45 | "accumulated results of last {} epochs: \t won: {} \t draw: {} \t lost: {}", 46 | self.n, self.won, self.draw, self.lost 47 | ); 48 | } 49 | } 50 | 51 | pub fn get_results(&self) -> (u32, u32, u32) { 52 | (self.won, self.draw, self.lost) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/rl/algorithms/dq_learning.rs: -------------------------------------------------------------------------------- 1 | use super::{Observation, ReplayBuffer}; 2 | use crate::network::nn::NeuralNetwork; 3 | use crate::rl::algorithms::utils; 4 | use ndarray::{par_azip, Array1, Array2, Array4}; 5 | use ndarray_stats::QuantileExt; 6 | use rand::rngs::ThreadRng; 7 | use rand::Rng; 8 | 9 | static EPSILON: f32 = 1e-4; 10 | 11 | pub struct DQlearning { 12 | nn: NeuralNetwork, 13 | use_ddqn: bool, 14 | target_nn: NeuralNetwork, 15 | target_update_counter: usize, 16 | target_update_every: usize, 17 | exploration: f32, 18 | discount_factor: f32, 19 | // last_turn: (board before last own move, allowed moves, NN output, move choosen from NN) 20 | last_turn: (Array2, Array1, Array1, usize), 21 | replay_buffer: ReplayBuffer>, 22 | rng: ThreadRng, 23 | } 24 | 25 | impl DQlearning { 26 | // TODO add mini_batch_size to bs, so that bs % mbs == 0 27 | pub fn new(exploration: f32, batch_size: usize, mut nn: NeuralNetwork, use_ddqn: bool) -> Self { 28 | if nn.get_batch_size() % batch_size != 0 { 29 | eprintln!( 30 | "not implemented yet, unsure how to store 31 | intermediate vals before weight updates" 32 | ); 33 | unimplemented!(); 34 | } 35 | nn.set_batch_size(batch_size); 36 | let target_nn = if use_ddqn { 37 | nn.clone() 38 | } else { 39 | NeuralNetwork::new1d(0, "none".to_string(), "none".to_string()) 40 | }; 41 | let discount_factor = 0.95; 42 | DQlearning { 43 | use_ddqn, 44 | target_nn, 45 | target_update_counter: 0, 46 | target_update_every: 20, // update after 5 episodes (entire games) 47 | nn, 48 | exploration, 49 | last_turn: ( 50 | Default::default(), 51 | Default::default(), 52 | Default::default(), 53 | 42, 54 | ), 55 | replay_buffer: ReplayBuffer::new(batch_size, 2_000), 56 | discount_factor, 57 | rng: rand::thread_rng(), 58 | } 59 | } 60 | 61 | pub fn get_learning_rate(&self) -> f32 { 62 | self.nn.get_learning_rate() 63 | } 64 | 65 | pub fn set_learning_rate(&mut self, lr: f32) -> Result<(), String> { 66 | if !(0.0..=1.).contains(&lr) { 67 | return Err("learning rate must be in [0,1]!".to_string()); 68 | } 69 | self.nn.set_learning_rate(lr); 70 | Ok(()) 71 | } 72 | 73 | pub fn get_exploration_rate(&self) -> f32 { 74 | self.exploration 75 | } 76 | 77 | pub fn set_exploration_rate(&mut self, e: f32) -> Result<(), String> { 78 | if !(0.0..=1.).contains(&e) { 79 | return Err("exploration rate must be in [0,1]!".to_string()); 80 | } 81 | self.exploration = e; 82 | Ok(()) 83 | } 84 | } 85 | 86 | impl DQlearning { 87 | // learn based on last action and their result 88 | pub fn finish_round(&mut self, mut final_reward: f32, s1: Array2) { 89 | if f32::abs(final_reward) < 1e-4 { 90 | final_reward = 0.5; // smaller bonus for a draw 91 | } 92 | 93 | self.replay_buffer.add_memory(Observation::new( 94 | self.last_turn.0.clone(), 95 | self.last_turn.3, 96 | s1, 97 | final_reward, 98 | true, 99 | )); 100 | self.target_update_counter += 1; 101 | self.learn(); 102 | } 103 | 104 | pub fn get_move( 105 | &mut self, 106 | board_arr: Array2, 107 | action_arr: Array1, 108 | reward: f32, 109 | ) -> usize { 110 | let actions = action_arr.mapv(|x| if x { 1. } else { 0. }); 111 | 112 | // store every interesting action, as well as every 20% of the actions with zero-reward 113 | if f32::abs(reward) > EPSILON || self.rng.gen::() < 0.2 { 114 | self.replay_buffer.add_memory(Observation::new( 115 | self.last_turn.0.clone(), 116 | self.last_turn.3, 117 | board_arr.clone(), 118 | reward, 119 | false, 120 | )); 121 | } 122 | self.learn(); 123 | 124 | let board_with_channels = board_arr 125 | .clone() 126 | .into_shape((1, board_arr.shape()[0], board_arr.shape()[1])) 127 | .unwrap(); 128 | let predicted_moves = self.nn.predict3d(board_with_channels); 129 | let legal_predicted_moves = predicted_moves.clone() * actions.clone(); 130 | let mut next_move = legal_predicted_moves.argmax().unwrap(); 131 | 132 | // shall we explore a random move? 133 | // also select random move if predicted move not allowed 134 | // (e.g. legal_predicted_moves contains only 0's). 135 | if (self.exploration > self.rng.gen()) || (!action_arr[next_move]) { 136 | next_move = utils::get_random_true_entry(action_arr); 137 | } 138 | 139 | // bookkeeping 140 | self.last_turn = (board_arr, actions, predicted_moves, next_move); 141 | 142 | //println!("action: {}, \t reward: {}", self.last_turn.3, reward); 143 | 144 | self.last_turn.3 145 | } 146 | 147 | fn learn(&mut self) { 148 | if !self.replay_buffer.is_full() { 149 | return; 150 | } 151 | let (s0_vec, actions, s1_vec, rewards, done) = self.replay_buffer.get_memories_SoA(); 152 | let s0_arr = vec_to_arr(s0_vec); 153 | let s1_arr = vec_to_arr(s1_vec); 154 | let done: Array1 = done.mapv(|x| if !x { 1. } else { 0. }); 155 | 156 | let current_q_list: Array2 = self.nn.predict_batch(s0_arr.clone().into_dyn()); 157 | let future_q_list_1: Array2 = self.nn.predict_batch(s1_arr.clone().into_dyn()); 158 | let future_q_list_2 = if self.use_ddqn { 159 | self.target_nn.predict_batch(s1_arr.into_dyn()) 160 | } else { 161 | self.nn.predict_batch(s1_arr.into_dyn()) 162 | }; 163 | 164 | let best_future_actions: Array1 = argmax(future_q_list_1); 165 | let future_rewards: Array1 = get_future_rewards(future_q_list_2, best_future_actions); 166 | 167 | // TODO done vorziehen um nur bei nicht endzuständen zu predicten 168 | let mut new_q_list: Array1 = rewards + self.discount_factor * done * future_rewards; 169 | new_q_list.mapv_inplace(|x| if x < 1. { x } else { 1. }); 170 | let targets = update_targets(current_q_list, actions, new_q_list); 171 | 172 | self.nn.train(s0_arr.into_dyn(), targets.into_dyn()); 173 | 174 | if self.use_ddqn && self.target_update_counter > self.target_update_every { 175 | self.target_nn = self.nn.clone(); 176 | // TODO improve, only copy weights later 177 | // due to optimizer´s and such stuff 178 | self.target_update_counter = 0; 179 | } 180 | } 181 | } 182 | 183 | fn get_future_rewards(rewards_2d: Array2, indices: Array1) -> Array1 { 184 | let mut best_rewards: Array1 = Array1::zeros(indices.len()); 185 | par_azip!((mut best_reward in best_rewards.outer_iter_mut(), 186 | rewards_1d in rewards_2d.outer_iter(), 187 | index in &indices) 188 | { 189 | best_reward.fill(rewards_1d[*index]); 190 | }); 191 | best_rewards 192 | } 193 | 194 | fn update_targets( 195 | mut targets: Array2, 196 | actions: Array1, 197 | rewards: Array1, 198 | ) -> Array2 { 199 | par_azip!((mut target in targets.outer_iter_mut(), 200 | action in &actions, 201 | reward in &rewards) 202 | { 203 | target[*action] = *reward; 204 | }); 205 | targets 206 | } 207 | 208 | fn vec_to_arr(input: Vec>) -> Array4 { 209 | let (bs, nrows, ncols) = (input.len(), input[0].nrows(), input[0].ncols()); 210 | let mut res = Array4::zeros((bs, 1, nrows, ncols)); 211 | par_azip!((mut out_entry in res.outer_iter_mut(), in_entry in &input) { 212 | out_entry.assign(in_entry); 213 | }); 214 | res.into_shape((bs, 1, nrows, ncols)).unwrap() 215 | } 216 | 217 | fn argmax(input: Array2) -> Array1 { 218 | let mut res: Array1 = Array1::zeros(input.nrows()); 219 | 220 | par_azip!((mut out_entry in res.outer_iter_mut(), in_entry in input.outer_iter()) { 221 | 222 | let mut argmax = (0, f32::MIN); 223 | for (i, &val) in in_entry.iter().enumerate() { 224 | if val > argmax.1 { 225 | argmax = (i, val); 226 | } 227 | } 228 | //println!("{} {} {:}\n", argmax.0, argmax.1, in_entry); 229 | out_entry.fill(argmax.0); 230 | }); 231 | 232 | res 233 | } 234 | #[cfg(test)] 235 | mod tests { 236 | use super::*; 237 | use ndarray::array; 238 | 239 | #[test] 240 | fn test_argmax() { 241 | let input: Array2 = array![[2., 1., 3.], [-0.4, -1., -2.2]]; 242 | let output: Array1 = array![2, 0]; 243 | assert_eq!(output, argmax(input)); 244 | } 245 | 246 | #[test] 247 | fn test_update_targets() { 248 | let targets: Array2 = array![[5., 2., 1.5, 4.], [3., 2.2, -1., 0.]]; 249 | let actions: Array1 = array![2, 0]; 250 | let rewards: Array1 = array![42., 0.]; 251 | let output: Array2 = array![[5., 2., 42., 4.], [0., 2.2, -1., 0.]]; 252 | assert_eq!(update_targets(targets, actions, rewards), output); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/rl/algorithms/mod.rs: -------------------------------------------------------------------------------- 1 | mod dq_learning; 2 | mod observation; 3 | mod q_learning; 4 | mod replay_buffer; 5 | pub mod utils; 6 | 7 | pub use dq_learning::DQlearning; 8 | pub use observation::Observation; 9 | pub use q_learning::Qlearning; 10 | pub use replay_buffer::ReplayBuffer; 11 | -------------------------------------------------------------------------------- /src/rl/algorithms/observation.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone)] 2 | pub struct Observation 3 | where 4 | T: std::clone::Clone, 5 | { 6 | pub s0: T, 7 | pub a: usize, 8 | pub s1: T, 9 | pub r: f32, 10 | pub d: bool, 11 | } 12 | 13 | impl Observation { 14 | pub fn new(s0: T, a: usize, s1: T, r: f32, d: bool) -> Self { 15 | Observation { s0, a, s1, r, d } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/rl/algorithms/q_learning.rs: -------------------------------------------------------------------------------- 1 | use super::{Observation, ReplayBuffer}; 2 | use crate::rl::algorithms::utils; 3 | use ndarray::{Array1, Array2}; 4 | use rand::rngs::ThreadRng; 5 | use rand::Rng; 6 | use std::collections::HashMap; 7 | 8 | #[allow(dead_code)] 9 | pub struct Qlearning { 10 | exploration: f32, 11 | learning_rate: f32, 12 | discount_factor: f32, 13 | scores: HashMap<(String, usize), f32>, // (State,Action), reward 14 | replay_buffer: ReplayBuffer, 15 | last_state: String, 16 | last_action: usize, 17 | rng: ThreadRng, 18 | action_space_length: usize, 19 | } 20 | 21 | const EPSILON: f32 = 1e-4; 22 | 23 | // based on Q-learning using a HashMap as table 24 | // 25 | impl Qlearning { 26 | pub fn new(exploration: f32, learning_rate: f32, action_space_length: usize) -> Self { 27 | let bs = 16; 28 | let discount_factor = 0.95; 29 | Qlearning { 30 | exploration, 31 | learning_rate, 32 | discount_factor, 33 | last_action: 42usize, 34 | last_state: "".to_string(), 35 | replay_buffer: ReplayBuffer::new(bs, 1000), 36 | scores: HashMap::new(), 37 | rng: rand::thread_rng(), 38 | action_space_length, 39 | } 40 | } 41 | 42 | pub fn get_learning_rate(&self) -> f32 { 43 | self.learning_rate 44 | } 45 | 46 | pub fn set_learning_rate(&mut self, lr: f32) -> Result<(), String> { 47 | if !(0.0..=1.).contains(&lr) { 48 | return Err("learning rate must be in [0,1]!".to_string()); 49 | } 50 | self.learning_rate = lr; 51 | Ok(()) 52 | } 53 | pub fn get_exploration_rate(&self) -> f32 { 54 | self.exploration 55 | } 56 | 57 | pub fn set_exploration_rate(&mut self, e: f32) -> Result<(), String> { 58 | if !(0.0..=1.).contains(&e) { 59 | return Err("exploration rate must be in [0,1]!".to_string()); 60 | } 61 | self.exploration = e; 62 | Ok(()) 63 | } 64 | } 65 | 66 | impl Qlearning { 67 | // update "table" based on last action and their result 68 | pub fn finish_round(&mut self, mut reward: f32, s1: Array2) { 69 | if (reward).abs() < EPSILON { 70 | reward = 0.5; 71 | } 72 | let s1 = s1.fold("".to_string(), |acc, x| acc + &x.to_string()); 73 | self.replay_buffer.add_memory(Observation::new( 74 | self.last_state.clone(), 75 | self.last_action, 76 | s1, 77 | reward, 78 | true, 79 | )); 80 | self.learn(); 81 | } 82 | 83 | fn max_future_q(&self, s: String) -> f32 { 84 | let mut max_val = f32::MIN; 85 | for a in 0..self.action_space_length { 86 | let key = (s.clone(), a); 87 | let new_val = if !self.scores.contains_key(&key) { 88 | 0. 89 | } 90 | // equals zero-init with RIC (see learn() regarding RIC). 91 | else { 92 | *self.scores.get(&key).expect("can't fail") 93 | }; 94 | if new_val > max_val { 95 | max_val = new_val; 96 | } 97 | } 98 | max_val 99 | } 100 | 101 | fn learn(&mut self) { 102 | if !self.replay_buffer.is_full() { 103 | return; 104 | } 105 | let mut updates: Vec<((String, usize), f32)> = Default::default(); 106 | let memories = self.replay_buffer.get_memories(); 107 | for observation in memories { 108 | let Observation { s0, a, s1, r, .. } = *observation; 109 | let key = (s0, a); 110 | let new_val = if self.scores.contains_key(&key) { 111 | let val = self.scores.get(&key).expect("can't fail"); 112 | val + self.learning_rate * (r + self.discount_factor * self.max_future_q(s1) - val) 113 | } else { 114 | r // use RIC: https://en.wikipedia.org/wiki/Q-learning#Initial_conditions_(Q0) 115 | }; 116 | updates.push((key, new_val)); 117 | } 118 | for (key, new_val) in updates { 119 | self.scores.insert(key, new_val); 120 | } 121 | } 122 | 123 | pub fn get_move( 124 | &mut self, 125 | board_arr: Array2, // TODO work on T 126 | action_arr: Array1, // TODO work on V 127 | reward: f32, 128 | ) -> usize { 129 | let board_as_string = board_arr.fold("".to_string(), |acc, x| acc + &x.to_string()); 130 | if f32::abs(reward) > EPSILON || self.rng.gen::() < 0.2 { 131 | self.replay_buffer.add_memory(Observation::new( 132 | self.last_state.clone(), 133 | self.last_action, 134 | board_as_string.clone(), 135 | reward, 136 | false, // aparently we are not done yet. 137 | )); 138 | } 139 | self.learn(); 140 | 141 | self.last_state = board_as_string; 142 | 143 | if self.exploration > rand::thread_rng().gen() { 144 | self.last_action = utils::get_random_true_entry(action_arr); 145 | } else { 146 | self.last_action = match self.get_best_move(action_arr.clone()) { 147 | Some(action) => action, 148 | None => utils::get_random_true_entry(action_arr), 149 | } 150 | } 151 | 152 | self.last_action 153 | } 154 | 155 | fn get_best_move(&mut self, actions: Array1) -> Option { 156 | // get all legal actions 157 | let mut existing_entries: Vec<(usize, f32)> = Vec::new(); // Vec<(action, val)> 158 | for move_candidate in 0..actions.len() { 159 | if !actions[move_candidate] { 160 | continue; 161 | } 162 | 163 | let score = self.scores.get(&(self.last_state.clone(), move_candidate)); 164 | if let Some(&val) = score { 165 | existing_entries.push((move_candidate, val)) 166 | } 167 | } 168 | 169 | // all state,action pairs are unknown, return any 170 | if existing_entries.is_empty() { 171 | return None; 172 | } 173 | 174 | let (mut pos, mut max_val): (usize, f32) = (0, f32::MIN); 175 | for (i, (_action, new_val)) in existing_entries.iter().enumerate() { 176 | if *new_val > max_val { 177 | pos = i; 178 | max_val = *new_val; 179 | } 180 | } 181 | 182 | Some(existing_entries[pos].0) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/rl/algorithms/replay_buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::rl::algorithms::observation::Observation; 2 | use ndarray::{Array, Array1}; 3 | use rand::Rng; 4 | 5 | /// A struct containing all relevant information. 6 | pub struct ReplayBuffer 7 | where 8 | T: std::clone::Clone, 9 | { 10 | b: usize, // Batch_size 11 | m_max: usize, // #Memories to store 12 | iter_pos: usize, // to be able to remove the oldest entries 13 | is_full: bool, // how full is the storage 14 | memories: Vec>>, // up to M observations 15 | } 16 | 17 | impl ReplayBuffer { 18 | /// Creates a new replay buffer. 19 | /// 20 | /// B is the amount of observations to be retreived by calling get_memories(). 21 | /// M is the maximum amount of observations to be stored simultaneously. 22 | pub fn new(b: usize, m: usize) -> Self { 23 | assert!(m>0,"a replay buffer without the possibility to store memories doesn't make sense. Please increase m!"); 24 | ReplayBuffer { 25 | b, 26 | m_max: m, 27 | iter_pos: 0, 28 | is_full: false, 29 | memories: Vec::with_capacity(b), 30 | } 31 | } 32 | 33 | /// Returns true if the replay buffer is filled entirely. 34 | /// 35 | /// New entries can still be added, however they will replace the oldest entry. 36 | pub fn is_full(&self) -> bool { 37 | self.is_full 38 | } 39 | 40 | /// Returns true if no entry has been stored yet. 41 | #[allow(dead_code)] 42 | pub fn is_empty(&self) -> bool { 43 | self.memories.is_empty() 44 | } 45 | 46 | /// Get a vector containing B Observations. 47 | /// 48 | /// Panics if no memory has previously been added. 49 | /// May return a single memory multiple times, mainly if only few observations have been added. 50 | pub fn get_memories(&self) -> Vec>> { 51 | assert!( 52 | self.is_full || self.iter_pos > 0, 53 | "add at least a single observation before calling get_memories()" 54 | ); 55 | let mut rng = rand::thread_rng(); 56 | let mut res: Vec>> = Vec::new(); 57 | let max = self.get_num_entries(); 58 | for _ in 0..self.b { 59 | let index = rng.gen_range(0..max); 60 | res.push(self.memories[index].clone()); 61 | } 62 | res 63 | } 64 | 65 | /// Same as get_memories(), but returns data as Struct of Arrays, instead of Array of Structs. 66 | #[allow(non_snake_case)] 67 | pub fn get_memories_SoA(&self) -> (Vec, Array1, Vec, Array1, Array1) { 68 | assert!( 69 | self.is_full || self.iter_pos > 0, 70 | "add at least a single observation before calling get_memories()" 71 | ); 72 | let max = self.get_num_entries(); 73 | let mut rewards: Array1 = Array::zeros(self.b); 74 | let mut actions: Array1 = Array::zeros(self.b); 75 | let mut done_arr: Array1 = Array::from_elem(self.b, false); 76 | let mut s0_arr = vec![]; 77 | let mut s1_arr = vec![]; 78 | let mut rng = rand::thread_rng(); 79 | for i in 0..self.b { 80 | let observation_number = rng.gen_range(0..max); 81 | let Observation { s0, a, s1, r, d } = *self.memories[observation_number].clone(); 82 | rewards[i] = r; 83 | actions[i] = a; 84 | done_arr[i] = d; 85 | s0_arr.push(s0); 86 | s1_arr.push(s1); 87 | } 88 | (s0_arr, actions, s1_arr, rewards, done_arr) 89 | } 90 | 91 | fn get_num_entries(&self) -> usize { 92 | if self.is_full { 93 | self.m_max 94 | } else { 95 | self.iter_pos 96 | } 97 | } 98 | 99 | /// Add a single memory to our replay buffer. 100 | /// 101 | /// If the maximum amount of entries is already reached, the oldest entry is replaced. 102 | /// Otherwise our new entry is appended. 103 | pub fn add_memory(&mut self, obs: Observation) { 104 | if !self.is_full { 105 | self.memories.push(Box::new(obs)); 106 | } else { 107 | self.memories[self.iter_pos] = Box::new(obs); 108 | } 109 | self.iter_pos += 1; 110 | if self.iter_pos == self.m_max { 111 | self.iter_pos = 0; 112 | self.is_full = true; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/rl/algorithms/utils.rs: -------------------------------------------------------------------------------- 1 | use ndarray::Array1; 2 | use rand::Rng; 3 | pub fn get_random_true_entry(actions: Array1) -> usize { 4 | let num_legal_actions = actions.fold(0, |sum, &val| if val { sum + 1 } else { sum }); 5 | assert!(num_legal_actions > 0, "no legal action available!"); 6 | let mut action_number = rand::thread_rng().gen_range(0..num_legal_actions) as usize; 7 | let b = action_number; 8 | 9 | let mut position = 0; 10 | while (action_number > 0) | !actions[position] { 11 | if actions[position] { 12 | action_number -= 1; 13 | } 14 | position += 1; 15 | } 16 | assert!( 17 | actions[position], 18 | "randomly picked illegal move! {:} {} {}", 19 | actions, position, b 20 | ); 21 | position 22 | } 23 | -------------------------------------------------------------------------------- /src/rl/env/env_trait.rs: -------------------------------------------------------------------------------- 1 | use ndarray::{Array1, Array2}; 2 | 3 | /// This trait defines all functions on which agents and other user might depend. 4 | pub trait Environment { 5 | /// The central function which causes the environment to pass various information to the agent. 6 | /// 7 | /// The Array2 encodes the environment (the board). 8 | /// The array1 encodes actions as true (allowed) or false (illegal). 9 | /// The third value returns a reward for the last action of the agent. 0 before the first action of the agent. 10 | /// The final bool value (done) indicates, wether it is time to reset the environment. 11 | fn step(&self) -> (Array2, Array1, f32, bool); 12 | /// Update the environment based on the action given. 13 | /// 14 | /// If the action is allowed for the currently active agent then update the environment and return true. 15 | /// Otherwise do nothing and return false. The same agent can then try a new move. 16 | fn take_action(&mut self, action: usize) -> bool; 17 | /// Shows the current envrionment state in a graphical way. 18 | /// 19 | /// The representation is environment specific and might be either by terminal, or in an extra window. 20 | fn render(&self); 21 | /// Resets the environment to the initial state. 22 | fn reset(&mut self); 23 | /// A vector with one entry for each agent, either 1 (agent won), 0 (draw), or -1 (agent lost). 24 | fn eval(&mut self) -> Vec; 25 | } 26 | -------------------------------------------------------------------------------- /src/rl/env/fortress.rs: -------------------------------------------------------------------------------- 1 | use crate::rl::env::env_trait::Environment; 2 | use ndarray::{Array, Array1, Array2}; 3 | use std::cmp::Ordering; 4 | 5 | static NEIGHBOURS_LIST: [&[usize]; 6 * 6] = [ 6 | &[1, 6], 7 | &[0, 2, 7], 8 | &[1, 3, 8], 9 | &[2, 4, 9], 10 | &[3, 5, 10], 11 | &[4, 11], 12 | &[7, 0, 12], 13 | &[6, 8, 1, 13], 14 | &[7, 9, 2, 14], 15 | &[8, 10, 3, 15], 16 | &[9, 11, 4, 16], 17 | &[10, 5, 17], 18 | &[13, 6, 18], 19 | &[12, 14, 7, 19], 20 | &[13, 15, 8, 20], 21 | &[14, 16, 9, 21], 22 | &[15, 17, 10, 22], 23 | &[16, 11, 23], 24 | &[19, 12, 24], 25 | &[18, 20, 13, 25], 26 | &[19, 21, 14, 26], 27 | &[20, 22, 15, 27], 28 | &[21, 23, 16, 28], 29 | &[22, 17, 29], 30 | &[25, 18, 30], 31 | &[24, 26, 19, 31], 32 | &[25, 27, 20, 32], 33 | &[26, 28, 21, 33], 34 | &[27, 29, 22, 34], 35 | &[28, 23, 35], 36 | &[31, 24], 37 | &[30, 32, 25], 38 | &[31, 33, 26], 39 | &[32, 34, 27], 40 | &[33, 35, 28], 41 | &[34, 29], 42 | ]; 43 | 44 | /// A struct containing all relevant information to store the current position of a single fortress game. 45 | pub struct Fortress { 46 | field: [i8; 36], 47 | flags: [i8; 36], 48 | first_player_turn: bool, 49 | rounds: usize, 50 | total_rounds: usize, 51 | first_player_moves: Array1, 52 | second_player_moves: Array1, 53 | active: bool, 54 | } 55 | 56 | impl Environment for Fortress { 57 | fn step(&self) -> (Array2, Array1, f32, bool) { 58 | if !self.active { 59 | eprintln!("Warning, calling step() after done = true!"); 60 | } 61 | 62 | // storing current position into ndarray 63 | let position = Array2::from_shape_vec((6, 6), self.field.to_vec()).unwrap(); 64 | let position = position.mapv(|x| x as f32); 65 | //TODO test removing next line 66 | // let position = position.mapv(|x| (x + 3.) / 6.); // scale to [0,1] 67 | 68 | // collecting allowed moves 69 | let moves = self.get_legal_actions(); 70 | 71 | // get rewards 72 | let reward = get_reward(self.first_player_turn, self.field, self.flags); 73 | 74 | let done = self.done(); 75 | (position, moves, reward, done) 76 | } 77 | 78 | fn reset(&mut self) { 79 | *self = Fortress::new(self.total_rounds); 80 | } 81 | 82 | fn render(&self) { 83 | let mut fst; 84 | let mut snd; 85 | let mut val; 86 | for row_num in 0..6 { 87 | println!(); 88 | let mut line = String::from(""); 89 | for x in 0..6 { 90 | fst = self.field[6 * row_num + x]; 91 | snd = self.flags[6 * row_num + x]; 92 | val = 10 * fst + snd; 93 | let update = format!(" {:^3}", &val.to_string()); 94 | line.push_str(&update); 95 | } 96 | println!("{} ", line); 97 | } 98 | println!(); 99 | } 100 | 101 | fn take_action(&mut self, pos: usize) -> bool { 102 | let player_val = if self.first_player_turn { 1 } else { -1 }; 103 | 104 | // check that field is not controlled by enemy, no enemy building on field, no own building on max lv (3) already exists 105 | if (self.first_player_turn && self.first_player_moves[pos]) 106 | || (!self.first_player_turn && self.second_player_moves[pos]) 107 | { 108 | self.store_update(pos, player_val); 109 | self.update_neighbours(pos, player_val); 110 | self.first_player_turn = !self.first_player_turn; 111 | self.rounds += 1; 112 | return true; 113 | } 114 | self.render(); 115 | let current_player = if self.first_player_turn { 1 } else { 2 }; 116 | eprintln!("WARNING ILLEGAL MOVE {} BY PLAYER {}", pos, current_player); 117 | false // move wasn't allowed, do nothing 118 | } 119 | 120 | fn eval(&mut self) -> Vec { 121 | if !self.done() { 122 | panic!("Hey, wait till the game is finished!"); 123 | } 124 | let (p1, p2) = controlled_fields(self.field, self.flags); 125 | match p1.cmp(&p2) { 126 | Ordering::Equal => vec![0, 0], 127 | Ordering::Greater => vec![1, -1], 128 | Ordering::Less => vec![-1, 1], 129 | } 130 | } 131 | } 132 | 133 | impl Fortress { 134 | /// A simple constructor which just takes the amount of moves from each player during a single game. 135 | /// 136 | /// After the given amount of rounds the player which controlls the majority of fields wins a single game. 137 | pub fn new(total_rounds: usize) -> Self { 138 | Fortress { 139 | field: [0; 36], 140 | flags: [0; 36], 141 | first_player_turn: true, 142 | rounds: 0, 143 | total_rounds, 144 | first_player_moves: Array::from_elem(36, true), 145 | second_player_moves: Array::from_elem(36, true), 146 | active: true, 147 | } 148 | } 149 | 150 | /// A getter for the amount of action each player is allowed to take before the game ends. 151 | pub fn get_total_rounds(&self) -> usize { 152 | self.total_rounds 153 | } 154 | 155 | fn done(&self) -> bool { 156 | self.rounds == self.total_rounds 157 | } 158 | 159 | fn get_legal_actions(&self) -> Array1 { 160 | if self.first_player_turn { 161 | self.first_player_moves.clone() 162 | } else { 163 | self.second_player_moves.clone() 164 | } 165 | } 166 | 167 | fn update_neighbours(&mut self, pos: usize, update_val: i8) { 168 | let neighbours: Vec = NEIGHBOURS_LIST[pos].to_vec(); 169 | for neighbour_pos in neighbours { 170 | self.flags[neighbour_pos] += update_val; 171 | if self.field[neighbour_pos] * self.flags[neighbour_pos] < 0 { 172 | //enemy neighbour building outnumbered, destroy it 173 | let val = -self.field[neighbour_pos]; 174 | self.field[neighbour_pos] = 0; 175 | self.flags[neighbour_pos] += val; 176 | self.update_neighbours(neighbour_pos, val); 177 | } 178 | } 179 | } 180 | 181 | fn store_update(&mut self, pos: usize, player_val: i8) { 182 | self.field[pos] += player_val; 183 | self.flags[pos] += player_val; 184 | if self.field[pos].abs() == 3 { 185 | // buildings on lv. 3 can't be upgraded 186 | self.second_player_moves[pos] = false; 187 | self.first_player_moves[pos] = false; 188 | return; 189 | } 190 | if player_val == 1 { 191 | self.second_player_moves[pos] = false; 192 | } else { 193 | self.first_player_moves[pos] = false; 194 | } 195 | } 196 | } 197 | 198 | // To make the game harder we only give a reward when finishing the game. 199 | // Feel free to change that behaviour 200 | fn get_reward(_first_player_turn: bool, _field: [i8; 36], _flags: [i8; 36]) -> f32 { 201 | 0. 202 | /* 203 | let controlled_fields = controlled_fields(field, flags); 204 | let mut reward = (controlled_fields.0 as i32) - (controlled_fields.1 as i32); 205 | if !first_player_turn { 206 | reward *= -1; 207 | } 208 | 0.5 / (1. + f32::exp(-1. * reward as f32)) + 0.25 // moved into [-0.5,0.5] 209 | */ 210 | } 211 | 212 | fn controlled_fields(field: [i8; 36], flags: [i8; 36]) -> (u8, u8) { 213 | let mut fields_one = 0; 214 | let mut fields_two = 0; 215 | for (&building_lv, &flags) in field.iter().zip(flags.iter()) { 216 | if building_lv > 0 || (building_lv == 0 && flags > 0) { 217 | fields_one += 1; 218 | } else if building_lv == 0 && flags == 0 { 219 | continue; 220 | } else { 221 | fields_two += 1; 222 | } 223 | } 224 | (fields_one, fields_two) 225 | } 226 | -------------------------------------------------------------------------------- /src/rl/env/mod.rs: -------------------------------------------------------------------------------- 1 | mod env_trait; 2 | 3 | mod fortress; 4 | /// An implementation of the game Fortress from Gary Allen, 1984. 5 | pub use fortress::Fortress; 6 | 7 | mod tictactoe; 8 | /// An implementation of the game TicTacToe. 9 | pub use tictactoe::TicTacToe; 10 | 11 | /// A trait defining the functions on which agents and other user depend. 12 | pub use env_trait::Environment; 13 | -------------------------------------------------------------------------------- /src/rl/env/tictactoe.rs: -------------------------------------------------------------------------------- 1 | use crate::rl::env::env_trait::Environment; 2 | use ndarray::{Array, Array1, Array2}; 3 | 4 | #[allow(clippy::unusual_byte_groupings)] 5 | static BITMASKS: [&[u16]; 9] = [ 6 | &[0b_111, 0b_100_100_100, 0b_100_010_001], 7 | &[0b_111, 0b_010_010_010], 8 | &[0b_111, 0b_001_010_100, 0b_001_001_001], 9 | &[0b_111_000, 0b_100_100_100], 10 | &[0b_111_000, 0b_100_010_001, 0b_010_010_010, 0b_001_010_100], 11 | &[0b_111_000, 0b_100_100_100], 12 | &[0b_111_000_000, 0b_001_001_001, 0b_001_010_100], 13 | &[0b_111_000_000, 0b_010_010_010], 14 | &[0b_111_000_000, 0b_100_010_001, 0b_001_001_001], 15 | ]; 16 | 17 | /// A struct containing all relevant information to store the current position of a single fortress game. 18 | pub struct TicTacToe { 19 | player1: u16, 20 | player2: u16, 21 | first_player_turn: bool, 22 | rounds: usize, 23 | total_rounds: usize, 24 | state: GameState, 25 | } 26 | 27 | #[derive(Debug, Clone, PartialEq)] 28 | enum GameState { 29 | Running, 30 | Draw, 31 | Player1won, 32 | Player2won, 33 | } 34 | 35 | impl Default for TicTacToe { 36 | fn default() -> Self { 37 | Self::new() 38 | } 39 | } 40 | 41 | impl Environment for TicTacToe { 42 | fn step(&self) -> (Array2, Array1, f32, bool) { 43 | // storing current position into ndarray 44 | let position = board_as_arr(self.player1, self.player2) 45 | .into_shape((3, 3)) 46 | .unwrap(); 47 | let position = position.mapv(|x| x as f32); 48 | 49 | // collecting allowed moves 50 | let moves = get_legal_actions(self.player1, self.player2); 51 | 52 | // get rewards 53 | let agent_num = if self.first_player_turn { 0 } else { 1 }; 54 | let reward = get_reward(&self.state, agent_num); 55 | 56 | let done = self.done(); 57 | 58 | (position, moves, reward, done) 59 | } 60 | 61 | fn reset(&mut self) { 62 | *self = TicTacToe::new(); 63 | } 64 | 65 | fn render(&self) { 66 | let res = board_as_arr(self.player1, self.player2); 67 | for i in 0..3 { 68 | println!("{} {} {}", res[3 * i + 0], res[3 * i + 1], res[3 * i + 2]); 69 | } 70 | } 71 | 72 | fn eval(&mut self) -> Vec { 73 | match self.state { 74 | GameState::Player1won => vec![1, -1], 75 | GameState::Player2won => vec![-1, 1], 76 | GameState::Draw => vec![0, 0], 77 | GameState::Running => panic!("Hey, wait till the game is finished!"), 78 | } 79 | } 80 | 81 | fn take_action(&mut self, pos: usize) -> bool { 82 | if pos > 8 { 83 | return false; 84 | } 85 | let bin_pos = 1 << pos; 86 | 87 | if (self.player1 | self.player2) & bin_pos != 0 { 88 | return false; 89 | } 90 | 91 | if self.first_player_turn { 92 | self.player1 |= bin_pos; 93 | } else { 94 | self.player2 |= bin_pos; 95 | } 96 | self.rounds += 1; 97 | 98 | self.state = check_result(self.first_player_turn, self.player1, self.player2, pos); 99 | 100 | self.first_player_turn ^= true; // next player 101 | 102 | true 103 | } 104 | } 105 | 106 | impl TicTacToe { 107 | /// A simple constructor which just takes the amount of moves from each player during a single game. 108 | /// 109 | /// If one player achieves it to put 3 pieces in a row he wins, otherwise the game ends as a draw. 110 | pub fn new() -> Self { 111 | TicTacToe { 112 | player1: 0u16, 113 | player2: 0u16, 114 | first_player_turn: true, 115 | rounds: 0, 116 | total_rounds: 9, 117 | state: GameState::Running, 118 | } 119 | } 120 | 121 | fn done(&self) -> bool { 122 | !matches!(self.state, GameState::Running) 123 | } 124 | 125 | /// A getter for the amount of action each player is allowed to take before the game ends. 126 | pub fn get_total_rounds(&self) -> usize { 127 | self.total_rounds 128 | } 129 | } 130 | 131 | fn board_as_arr(player1: u16, player2: u16) -> Array1 { 132 | let p1_at = |x: u16| -> i32 { 133 | if (player1 & 1 << x) != 0 { 134 | 1 135 | } else { 136 | 0 137 | } 138 | }; 139 | let p2_at = |x: u16| -> i32 { 140 | if (player2 & 1 << x) != 0 { 141 | 1 142 | } else { 143 | 0 144 | } 145 | }; 146 | let val_at = |x: u16| -> i32 { p1_at(x) - p2_at(x) }; 147 | let mut res = Array::zeros(9); 148 | for i in 0..9u16 { 149 | res[i as usize] = val_at(i); 150 | } 151 | res 152 | } 153 | 154 | fn get_legal_actions(player1: u16, player2: u16) -> Array1 { 155 | let bit_res = 0b_111_111_111 & (!(player1 | player2)); 156 | let mut res = Array::from_elem(9, true); 157 | for i in 0..9 { 158 | res[i] = (bit_res & 1 << i) != 0; 159 | } 160 | res 161 | } 162 | 163 | // Game fields are: 164 | // 0,1,2, 165 | // 3,4,5, 166 | // 6,7,8 167 | // pos i is encoded at bit 2^i 168 | #[test] 169 | fn test_bitmask() { 170 | let p1 = GameState::Player1won; 171 | let p2 = GameState::Player2won; 172 | let r = GameState::Running; 173 | let (fr, _mr, _lr) = (1 + 2 + 4, 8 + 16 + 32, 64 + 128 + 256); //rows 174 | let (_fc, mc, _lc) = (1 + 8 + 64, 2 + 16 + 128, 4 + 32 + 256); //columns 175 | let (tlbr, trbl) = (1 + 16 + 256, 4 + 16 + 64); //diagonals 176 | assert_eq!(p1, check_result(true, fr, 0u16, 0)); 177 | assert_eq!(p1, check_result(true, fr, 0u16, 1)); 178 | assert_eq!(p1, check_result(true, fr, 0u16, 2)); 179 | assert_eq!(r, check_result(true, fr, 0u16, 3)); 180 | assert_eq!(p1, check_result(true, mc, 0u16, 1)); 181 | assert_eq!(p1, check_result(true, mc, 0u16, 4)); 182 | assert_eq!(p1, check_result(true, mc, 0u16, 7)); 183 | assert_eq!(r, check_result(true, mc, 0u16, 5)); 184 | assert_eq!(p1, check_result(true, tlbr, 0u16, 0)); 185 | assert_eq!(p1, check_result(true, tlbr, 0u16, 4)); 186 | assert_eq!(p1, check_result(true, tlbr, 0u16, 8)); 187 | assert_eq!(r, check_result(true, tlbr, 0u16, 1)); 188 | assert_eq!(p2, check_result(false, 0u16, trbl, 2)); 189 | assert_eq!(p2, check_result(false, 0u16, trbl, 4)); 190 | assert_eq!(p2, check_result(false, 0u16, trbl, 6)); 191 | assert_eq!(r, check_result(false, 0u16, trbl, 0)); 192 | } 193 | 194 | fn check_result(first_player_turn: bool, player1: u16, player2: u16, pos: usize) -> GameState { 195 | let board = if first_player_turn { player1 } else { player2 }; 196 | 197 | for &bm in BITMASKS[pos] { 198 | assert!(bm < 512); 199 | if (board & bm) == bm { 200 | if first_player_turn { 201 | return GameState::Player1won; 202 | } else { 203 | return GameState::Player2won; 204 | }; 205 | } 206 | } 207 | 208 | if (player1 | player2) == 511 { 209 | return GameState::Draw; 210 | } 211 | GameState::Running 212 | } 213 | 214 | // For a higher complexity we give rewards only when finishing games 215 | fn get_reward(_state: &GameState, _agent_num: usize) -> f32 { 216 | 0. 217 | /* 218 | let x = if agent_num == 0 { 1. } else { -1. }; 219 | match state { 220 | GameState::Draw => 0.4, 221 | GameState::Player1won => 1. * x, 222 | GameState::Player2won => -1. * x, 223 | GameState::Running => 0., 224 | } 225 | */ 226 | } 227 | -------------------------------------------------------------------------------- /src/rl/mod.rs: -------------------------------------------------------------------------------- 1 | /// This submodule contains the concrete agent interface and multiple agent examples. 2 | pub mod agent; 3 | 4 | mod algorithms; 5 | 6 | /// A submodule containing the Environment trait which all environments should implement. 7 | /// 8 | /// An example implementation is given for the game "Fortress". 9 | pub mod env; 10 | 11 | /// This submodule offers some convenience functionality to simplify training agents. 12 | pub mod training; 13 | -------------------------------------------------------------------------------- /src/rl/training/mod.rs: -------------------------------------------------------------------------------- 1 | mod trainer; 2 | pub use trainer::Trainer; 3 | 4 | /// Some helper functions to controll training via terminal IO. 5 | pub mod utils; 6 | -------------------------------------------------------------------------------- /src/rl/training/trainer.rs: -------------------------------------------------------------------------------- 1 | use crate::rl::agent::Agent; 2 | use crate::rl::env::Environment; 3 | use ndarray::Array2; 4 | 5 | /// A trainer works on a given environment and a set of agents. 6 | pub struct Trainer { 7 | env: Box, 8 | res: Vec<(u32, u32, u32)>, 9 | agents: Vec>, 10 | learning_rates: Vec, 11 | exploration_rates: Vec, 12 | print: bool, 13 | } 14 | 15 | impl Trainer { 16 | /// We construct a Trainer by passing a single environment and one or more (possibly different) agents. 17 | pub fn new( 18 | env: Box, 19 | agents: Vec>, 20 | print: bool, 21 | ) -> Result { 22 | if agents.is_empty() { 23 | return Err("At least one agent required!".to_string()); 24 | } 25 | let (exploration_rates, learning_rates) = get_rates(&agents); 26 | Ok(Trainer { 27 | env, 28 | res: vec![(0, 0, 0); agents.len()], 29 | agents, 30 | learning_rates, 31 | exploration_rates, 32 | print, 33 | }) 34 | } 35 | 36 | /// Returns a (#won, #draw, #lost) tripple for each agent. 37 | /// 38 | /// The numbers are accumulated over all train and bench games, either since the beginning, or the last reset_results() call. 39 | pub fn get_results(&self) -> Vec<(u32, u32, u32)> { 40 | self.res.clone() 41 | } 42 | 43 | /// Resets the (#won, #draw, #lost) values for each agents to (0,0,0). 44 | pub fn reset_results(&mut self) { 45 | self.res = vec![(0, 0, 0); self.agents.len()]; 46 | } 47 | 48 | /// Returns a Vector containing the string identifier of each agent. 49 | pub fn get_agent_ids(&self) -> Vec { 50 | self.agents.iter().map(|a| a.get_id()).collect() 51 | } 52 | 53 | /// Executes n training games folowed by m bench games. 54 | /// Repeates this cycle for i iterations. 55 | pub fn train_bench_loops(&mut self, n: u64, m: u64, i: u64) { 56 | for _ in 0..i { 57 | self.train(n); 58 | let res: Vec<(u32, u32, u32)> = self.bench(m); 59 | if self.print { 60 | for (agent, result) in res.iter().enumerate() { 61 | println!( 62 | "agent{} ({}): lost: {}, draw: {}, won: {}", 63 | agent, 64 | self.get_agent_ids()[agent], 65 | result.0, 66 | result.1, 67 | result.2 68 | ); 69 | } 70 | } 71 | } 72 | } 73 | 74 | /// Executes the given amount of (independent) training games. 75 | /// 76 | /// Results are stored and agents are expected to update their internal parameters 77 | /// in order to adjust to the game and performe better on subsequent games. 78 | pub fn train(&mut self, num_games: u64) { 79 | self.play_games(num_games, true); 80 | } 81 | 82 | /// Executes the given amount of (independent) bench games. 83 | /// 84 | /// Results are stored and agents are expected to not learn based on bench games. 85 | pub fn bench(&mut self, num_games: u64) -> Vec<(u32, u32, u32)> { 86 | self.adjust_rates(1.); 87 | self.play_games(num_games, false) 88 | } 89 | 90 | fn adjust_rates(&mut self, fraction_done: f32) { 91 | for i in 0..self.agents.len() { 92 | self.agents[i] 93 | .set_exploration_rate(self.exploration_rates[i] * (1. - fraction_done)) 94 | .unwrap(); 95 | self.agents[i] 96 | .set_learning_rate(self.learning_rates[i] * (1. - fraction_done)) 97 | .unwrap(); 98 | } 99 | } 100 | 101 | fn update_results(&mut self, new_res: &[i8]) { 102 | assert_eq!( 103 | new_res.len(), 104 | self.agents.len(), 105 | "results and number of agents differ!" 106 | ); 107 | for (i, res) in new_res.iter().enumerate() { 108 | match res { 109 | -1 => self.res[i].0 += 1, 110 | 0 => self.res[i].1 += 1, 111 | 1 => self.res[i].2 += 1, 112 | _ => panic!("only allowed results are -1,0,1"), 113 | } 114 | } 115 | } 116 | 117 | fn play_games(&mut self, num_games: u64, train: bool) -> Vec<(u32, u32, u32)> { 118 | self.res = vec![(0, 0, 0); self.agents.len()]; 119 | let mut sub_epoch: u64 = (num_games / 50) as u64; 120 | if sub_epoch == 0 { 121 | sub_epoch = 1; 122 | } 123 | 124 | // TODO parallelize 125 | println!("num games: {}", num_games); 126 | for game in 0..num_games { 127 | self.env.reset(); 128 | if (game % sub_epoch) == 0 && train { 129 | self.adjust_rates(game as f32 / num_games as f32); 130 | } 131 | 132 | let final_state: Array2 = 'outer: loop { 133 | for agent in self.agents.iter_mut() { 134 | let (env, actions, reward, done) = self.env.step(); 135 | if done { 136 | break 'outer env; 137 | } 138 | let res = self.env.take_action(agent.get_move(env, actions, reward)); 139 | if !res { 140 | println!("illegal move!"); 141 | } 142 | } 143 | }; 144 | 145 | let game_res = self.env.eval(); 146 | self.update_results(&game_res); 147 | if train { 148 | for (i, agent) in self.agents.iter_mut().enumerate() { 149 | agent.finish_round(game_res[i], final_state.clone()); 150 | } 151 | } 152 | } 153 | self.res.clone() 154 | } 155 | } 156 | 157 | //fn get_rates(agents: &Vec>) -> (Vec, Vec) { 158 | fn get_rates(agents: &[Box]) -> (Vec, Vec) { 159 | let exploration_rates: Vec = agents.iter().map(|a| a.get_exploration_rate()).collect(); 160 | let learning_rates: Vec = agents.iter().map(|a| a.get_learning_rate()).collect(); 161 | 162 | (exploration_rates, learning_rates) 163 | } 164 | -------------------------------------------------------------------------------- /src/rl/training/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | /// A helper function to create agents based on terminal input. 4 | pub fn read_agents(n: usize) -> Vec { 5 | let mut agents: Vec = vec![]; 6 | 7 | println!( 8 | "\nPlease insert {} numbers, seperated by whitespace, to select the agents.", 9 | n 10 | ); 11 | println!("(0 for ddql, 1 for dql, 2 for ql, 3 for random, 4 for human)"); 12 | let stdin = io::stdin(); 13 | loop { 14 | let mut buffer = String::new(); 15 | stdin.read_line(&mut buffer).unwrap(); 16 | let nums: Vec<&str> = buffer.split(' ').collect(); 17 | if nums.len() != n { 18 | println!("Please enter exactly {} values", n); 19 | continue; 20 | } 21 | for agent_num in nums 22 | .iter() 23 | .map(|num| usize::from_str_radix(num.trim(), 10).unwrap()) 24 | { 25 | agents.push(agent_num); 26 | } 27 | break; 28 | } 29 | agents 30 | } 31 | 32 | /// Reads the amount of training- and test-games from terminal. 33 | pub fn read_game_numbers() -> (u64, u64, u64) { 34 | loop { 35 | println!("\nPlease enter #training_games #test_games #iterations, seperated by whitespace"); 36 | let stdin = io::stdin(); 37 | let mut buffer = String::new(); 38 | stdin.read_line(&mut buffer).unwrap(); 39 | let nums: Vec<&str> = buffer.split(' ').collect(); 40 | if nums.len() != 3 { 41 | println!("Please enter exactly three values"); 42 | continue; 43 | } 44 | let nums: Vec = nums 45 | .iter() 46 | .map(|num| u64::from_str_radix(num.trim(), 10).unwrap()) 47 | .collect(); 48 | return (nums[0], nums[1], nums[2]); 49 | } 50 | } 51 | 52 | /// For round based games, reads an usize value from terminal. 53 | pub fn read_rounds_per_game() -> usize { 54 | //set number of rounds to play per game 55 | let mut rounds = String::new(); 56 | println!("please insert the number of rounds per game."); 57 | io::stdin() 58 | .read_line(&mut rounds) 59 | .expect("Failed to read number of rounds"); 60 | 61 | let rounds: usize = rounds.trim().parse().expect("please type a number"); 62 | rounds 63 | } 64 | --------------------------------------------------------------------------------