use rand::Rng;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
pub const ALLOWED_CHARS: &str =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,!?:;-";
const VOCAB_SIZE: usize = ALLOWED_CHARS.len();
lazy_static::lazy_static! {
static ref CHAR_TO_INDEX: HashMap<char, usize> = {
let mut map = HashMap::new();
for (i, c) in ALLOWED_CHARS.chars().enumerate() {
map.insert(c, i);
}
map
};
static ref INDEX_TO_CHAR: Vec<char> = {
ALLOWED_CHARS.chars().collect()
};
}
const HIDDEN: usize = 128; const LR: f32 = 0.005; pub const CONTEXT_SIZE: usize = 4;
#[derive(Clone)]
pub struct NGramModel {
w1: [[f32; HIDDEN]; VOCAB_SIZE * CONTEXT_SIZE],
w2: [[f32; VOCAB_SIZE]; HIDDEN],
context_to_index: HashMap<Vec<usize>, usize>,
next_context_index: usize,
}
impl NGramModel {
pub fn new() -> Self {
let mut rng = rand::thread_rng();
let mut w1 = [[0.0; HIDDEN]; VOCAB_SIZE * CONTEXT_SIZE];
let mut w2 = [[0.0; VOCAB_SIZE]; HIDDEN];
for i in 0..VOCAB_SIZE * CONTEXT_SIZE {
for j in 0..HIDDEN {
w1[i][j] = rng.gen_range(-0.1..0.1); }
}
for i in 0..HIDDEN {
for j in 0..VOCAB_SIZE {
w2[i][j] = rng.gen_range(-0.1..0.1);
}
}
Self {
w1,
w2,
context_to_index: HashMap::new(),
next_context_index: 0,
}
}
fn get_or_create_context_index(&mut self, context: &[usize]) -> usize {
let context_vec = context.to_vec();
if let Some(&idx) = self.context_to_index.get(&context_vec) {
return idx;
}
if self.next_context_index >= VOCAB_SIZE * CONTEXT_SIZE {
let mut hash = 0;
for &c in context {
hash = (hash * 31 + c) % (VOCAB_SIZE * CONTEXT_SIZE);
}
return hash;
}
let idx = self.next_context_index;
self.context_to_index.insert(context_vec, idx);
self.next_context_index += 1;
idx
}
pub fn forward(&mut self, context: &[usize]) -> Vec<f32> {
if context.len() != CONTEXT_SIZE {
panic!("Context must contain exactly {} characters", CONTEXT_SIZE);
}
let input_idx = self.get_or_create_context_index(context);
let mut hidden = [0.0; HIDDEN];
for j in 0..HIDDEN {
hidden[j] = self.w1[input_idx][j].tanh();
}
let mut output = vec![0.0; VOCAB_SIZE];
for i in 0..VOCAB_SIZE {
for j in 0..HIDDEN {
output[i] += hidden[j] * self.w2[j][i];
}
}
softmax(&mut output);
output
}
pub fn train(&mut self, context: &[usize], target: usize) {
if context.len() != CONTEXT_SIZE {
panic!("Context must contain exactly {} characters", CONTEXT_SIZE);
}
let target_idx = target;
let input_idx = self.get_or_create_context_index(context);
let mut hidden = [0.0; HIDDEN];
for j in 0..HIDDEN {
hidden[j] = self.w1[input_idx][j].tanh();
}
let mut logits = [0.0; VOCAB_SIZE];
for i in 0..VOCAB_SIZE {
for j in 0..HIDDEN {
logits[i] += hidden[j] * self.w2[j][i];
}
}
let mut probs = logits;
softmax(&mut probs);
probs[target_idx] -= 1.0;
for j in 0..HIDDEN {
for i in 0..VOCAB_SIZE {
self.w2[j][i] -= LR * probs[i] * hidden[j];
}
}
for j in 0..HIDDEN {
let grad = (1.0 - hidden[j] * hidden[j]) * self.w2[j][target_idx];
self.w1[input_idx][j] -= LR * grad;
}
}
pub fn save(&self, filename: &str) -> std::io::Result<()> {
let mut file = File::create(filename)?;
file.write_all(&(VOCAB_SIZE as u32).to_le_bytes())?;
file.write_all(&(HIDDEN as u32).to_le_bytes())?;
file.write_all(&(CONTEXT_SIZE as u32).to_le_bytes())?;
for row in &self.w1 {
for &value in row {
file.write_all(&value.to_le_bytes())?;
}
}
for row in &self.w2 {
for &value in row {
file.write_all(&value.to_le_bytes())?;
}
}
file.write_all(&(self.context_to_index.len() as u32).to_le_bytes())?;
for (context, &index) in &self.context_to_index {
file.write_all(&(context.len() as u32).to_le_bytes())?;
for &c in context {
file.write_all(&(c as u32).to_le_bytes())?;
}
file.write_all(&(index as u32).to_le_bytes())?;
}
file.write_all(&(self.next_context_index as u32).to_le_bytes())?;
Ok(())
}
pub fn load(filename: &str) -> std::io::Result<Self> {
let mut file = File::open(filename)?;
let mut buffer = [0; 4];
file.read_exact(&mut buffer)?;
let vocab_size = u32::from_le_bytes(buffer) as usize;
file.read_exact(&mut buffer)?;
let hidden_size = u32::from_le_bytes(buffer) as usize;
file.read_exact(&mut buffer)?;
let context_size = u32::from_le_bytes(buffer) as usize;
if vocab_size != VOCAB_SIZE || hidden_size != HIDDEN || context_size != CONTEXT_SIZE {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Dimensions do not match: expected {}x{}x{}, found {}x{}x{}",
VOCAB_SIZE, HIDDEN, CONTEXT_SIZE, vocab_size, hidden_size, context_size
),
));
}
let mut model = Self::new();
for i in 0..VOCAB_SIZE * CONTEXT_SIZE {
for j in 0..HIDDEN {
file.read_exact(&mut buffer)?;
model.w1[i][j] = f32::from_le_bytes(buffer);
}
}
for i in 0..HIDDEN {
for j in 0..VOCAB_SIZE {
file.read_exact(&mut buffer)?;
model.w2[i][j] = f32::from_le_bytes(buffer);
}
}
file.read_exact(&mut buffer)?;
let num_contexts = u32::from_le_bytes(buffer) as usize;
for _ in 0..num_contexts {
file.read_exact(&mut buffer)?;
let context_len = u32::from_le_bytes(buffer) as usize;
let mut context = Vec::with_capacity(context_len);
for _ in 0..context_len {
file.read_exact(&mut buffer)?;
context.push(u32::from_le_bytes(buffer) as usize);
}
file.read_exact(&mut buffer)?;
let index = u32::from_le_bytes(buffer) as usize;
model.context_to_index.insert(context, index);
}
file.read_exact(&mut buffer)?;
model.next_context_index = u32::from_le_bytes(buffer) as usize;
Ok(model)
}
}
pub fn softmax(x: &mut [f32]) {
let max = x.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let sum: f32 = x
.iter_mut()
.map(|v| {
*v = (*v - max).exp();
*v
})
.sum();
for v in x.iter_mut() {
*v /= sum;
}
}
pub fn sample(probs: &[f32]) -> usize {
const TEMPERATURE: f32 = 0.3;
let mut adjusted_probs = Vec::with_capacity(probs.len());
for &p in probs {
adjusted_probs.push(p.powf(1.0 / TEMPERATURE));
}
let sum: f32 = adjusted_probs.iter().sum();
for p in &mut adjusted_probs {
*p /= sum;
}
let mut cumulative_sum = 0.0;
let r: f32 = rand::random();
for (i, &p) in adjusted_probs.iter().enumerate() {
cumulative_sum += p;
if r < cumulative_sum {
return i;
}
}
VOCAB_SIZE - 1
}
pub fn prepare_training_data(text: &str) -> Vec<(Vec<usize>, usize)> {
let allowed_chars: Vec<char> = ALLOWED_CHARS.chars().collect();
let filtered_text: String = text.chars().filter(|c| allowed_chars.contains(c)).collect();
let chars: Vec<usize> = filtered_text.chars().map(|c| char_to_index(c)).collect();
let mut data = Vec::new();
for i in CONTEXT_SIZE..chars.len() {
let context = chars[i - CONTEXT_SIZE..i].to_vec();
let target = chars[i];
data.push((context, target));
}
data
}
pub fn char_to_index(c: char) -> usize {
*CHAR_TO_INDEX.get(&c).unwrap_or(&0)
}
pub fn index_to_char(idx: usize) -> char {
INDEX_TO_CHAR[idx % VOCAB_SIZE]
}