#![allow(clippy::missing_docs_in_private_items, clippy::print_stdout, clippy::unwrap_used)]
use std::{fs::File, io::prelude::*, path::Path, time::Instant};
use esopt::*;
use ofnn::{Float, *};
use rand::prelude::*;
const BATCHSIZE: usize = 32;
const NOISE_STD: Float = 0.025; const POPULATION: usize = 500;
fn main() {
std::fs::create_dir_all("./model").ok();
let mut model = Sequential::load("./model/cifar10.nn").unwrap_or_else(|_| {
let mut model = Sequential::new(3072);
model
.add_layer_dense(384, Initializer::Const(0.0)) .add_layer(Layer::SELU)
.add_layer_dense(10, Initializer::Const(0.0)) .add_layer(Layer::SoftMax);
model
});
let eval = CIFAR10Evaluator::new(model.clone(), "cifar-10-binary/", true);
let mut opt = Lookahead::<RAdam>::load("./model/optimizer.json")
.unwrap_or_else(|_| Lookahead::new(RAdam::new()));
opt.set_k(10);
opt.get_opt_mut().set_lr(0.005).set_lambda(0.0); let iterations = opt.get_t();
let mut opt = ES::new(opt, eval);
opt.set_params(model.get_params()).set_std(NOISE_STD).set_samples(POPULATION);
println!("Initial results on test set:");
let mut tester = CIFAR10Evaluator::new(model.clone(), "cifar-10-binary/", false); tester.print_metrics();
println!("Beginning training..");
println!();
let time = Instant::now();
for i in 0..10 {
let n = 100;
let res = opt.optimize_ranked_par(n);
model.set_params(opt.get_params());
model.save("./model/cifar10.nn").ok();
opt.get_opt().save("./model/optimizer.json").ok();
println!("After {} iteratios:", iterations + (i + 1) * n);
println!("Score: {}", res.0);
println!("Gradnorm: {}", res.1);
println!();
}
let elapsed = time.elapsed();
let sec = (elapsed.as_secs() as f64) + (f64::from(elapsed.subsec_nanos()) / 1_000_000_000.0);
println!("Time: {} min {:.3} s", (sec / 60.0).floor(), sec % 60.0);
println!();
model.set_params(opt.get_params());
model.save("./model/cifar10.nn").ok();
opt.get_opt().save("./model/optimizer.json").ok();
println!("Final results on test data:");
tester.set_model(model.clone());
tester.print_metrics();
}
fn image_normalize(x: u8) -> Float {
(Float::from(x) / 255.0 - 0.5) * 2.0 * 1.6 }
#[allow(clippy::type_complexity)]
fn load_cifar10(filename: &Path) -> std::io::Result<(Vec<Vec<Float>>, Vec<Vec<Float>>)> {
let mut file = File::open(filename)?;
let mut x = Vec::new();
let mut y = Vec::new();
let mut buffer = [0_u8; 3073];
for _i in 0..10000 {
file.read_exact(&mut buffer)?;
y.push(to_categorical(10, buffer[0]));
let data: Vec<Float> = buffer[1..].iter().copied().map(image_normalize).collect();
x.push(data);
}
Ok((x, y))
}
fn to_categorical(classes: u8, label: u8) -> Vec<Float> {
let mut vec = vec![0.0; classes as usize];
vec[label as usize] = 1.0;
vec
}
fn argmax(vec: &[Float]) -> usize {
let mut argmax = 0;
let mut max = std::f64::NEG_INFINITY as Float;
for (i, val) in vec.iter().enumerate() {
if *val >= max {
max = *val;
argmax = i;
}
}
argmax
}
#[derive(Clone)]
struct CIFAR10Evaluator {
model: Sequential,
data: (Vec<Vec<Float>>, Vec<Vec<Float>>),
seed: u64,
}
impl CIFAR10Evaluator {
pub fn new(model: Sequential, folder: &str, train: bool) -> CIFAR10Evaluator {
let path = Path::new(folder);
let mut data;
if train {
data = load_cifar10(&path.join("data_batch_1.bin")).unwrap();
let mut tmp = load_cifar10(&path.join("data_batch_2.bin")).unwrap();
data.0.append(&mut tmp.0);
data.1.append(&mut tmp.1);
let mut tmp = load_cifar10(&path.join("data_batch_3.bin")).unwrap();
data.0.append(&mut tmp.0);
data.1.append(&mut tmp.1);
let mut tmp = load_cifar10(&path.join("data_batch_4.bin")).unwrap();
data.0.append(&mut tmp.0);
data.1.append(&mut tmp.1);
let mut tmp = load_cifar10(&path.join("data_batch_5.bin")).unwrap();
data.0.append(&mut tmp.0);
data.1.append(&mut tmp.1);
} else {
data = load_cifar10(&path.join("test_batch.bin")).unwrap();
}
let seed = thread_rng().next_u64() % (std::u64::MAX - 50000); CIFAR10Evaluator { model, data, seed }
}
pub fn set_model(&mut self, model: Sequential) {
self.model = model;
}
pub fn print_metrics(&mut self) {
let pred = self.model.predict(&self.data.0);
let loss = losses::categorical_crossentropy(&pred, &self.data.1);
let mut acc = 0.0;
for (p, t) in pred.iter().zip(self.data.1.iter()) {
let select = argmax(p);
if (t[select] - 1.0).abs() < Float::EPSILON {
acc += 1.0;
}
}
acc *= 100.0 / pred.len() as Float;
println!("Loss: {}", loss);
println!("Accuracy: {:6.3}%", acc);
}
}
impl Evaluator for CIFAR10Evaluator {
fn eval_train(&self, params: &[Float], index: usize) -> Float {
let mut local = self.model.clone();
local.set_params(params);
let mut rng = SmallRng::seed_from_u64(self.seed + index as u64);
let start = rng.gen::<usize>() % (self.data.0.len() - BATCHSIZE); let end = start + BATCHSIZE;
let pred = local.predict(&self.data.0[start..end]);
let loss = -losses::categorical_crossentropy(&pred, &self.data.1[start..end]);
let reg =
-0.1 * params.iter().fold(0.0, |acc, e| acc + e.abs().sqrt()) / (params.len() as Float); loss + reg
}
fn eval_test(&self, params: &[Float]) -> Float {
let mut local = self.model.clone();
local.set_params(params);
let pred = local.predict(&self.data.0);
let loss = losses::categorical_crossentropy(&pred, &self.data.1);
let mut acc = 0.0;
for (p, t) in pred.iter().zip(self.data.1.iter()) {
let select = argmax(p);
if (t[select] - 1.0).abs() < Float::EPSILON {
acc += 1.0;
}
}
acc *= 100.0 / pred.len() as Float;
acc.round() + loss / 10.0
}
}