use std::collections::HashMap;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
use crate::embedding::{SentenceEmbedding, WordEmbedding};
use crate::error::{Error, Result};
pub struct Word2VecEmbedding {
vocab: Vec<String>,
index: HashMap<String, usize>,
vectors: Vec<f32>,
dim: usize,
}
impl Word2VecEmbedding {
pub fn from_file(path: &Path) -> Result<Self> {
Self::from_binary(path).or_else(|_| Self::from_text(path))
}
pub fn from_binary(path: &Path) -> Result<Self> {
let f = std::fs::File::open(path)?;
let mut reader = BufReader::new(f);
let mut header = String::new();
reader.read_line(&mut header)?;
let (num_words, dim) = parse_header(&header)?;
let mut vocab = Vec::with_capacity(num_words);
let mut vectors = Vec::with_capacity(num_words * dim);
let mut byte_buf = [0u8; 1];
for _ in 0..num_words {
let mut word_bytes = Vec::new();
loop {
reader.read_exact(&mut byte_buf)?;
match byte_buf[0] {
b' ' => break,
b'\n' if word_bytes.is_empty() => {} b => word_bytes.push(b),
}
}
let word = String::from_utf8_lossy(&word_bytes).into_owned();
let mut raw = vec![0u8; dim * 4];
reader.read_exact(&mut raw)?;
let mut row: Vec<f32> = raw
.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect();
l2_normalize(&mut row);
vocab.push(word);
vectors.extend_from_slice(&row);
}
Ok(Self::build(vocab, vectors, dim))
}
pub fn from_text(path: &Path) -> Result<Self> {
let f = std::fs::File::open(path)?;
let reader = BufReader::new(f);
let mut lines = reader.lines();
let header = lines
.next()
.ok_or_else(|| Error::Parse("embedding file is empty".to_string()))??;
let (num_words, dim) = parse_header(&header)?;
let mut vocab = Vec::with_capacity(num_words);
let mut vectors = Vec::with_capacity(num_words * dim);
for line in lines {
let line = line?;
let line = line.trim();
if line.is_empty() {
continue;
}
let mut parts = line.splitn(dim + 2, ' ');
let word = match parts.next() {
Some(w) => w.to_string(),
None => continue,
};
let mut row = Vec::with_capacity(dim);
for val in parts {
if let Ok(v) = val.parse::<f32>() {
row.push(v);
}
}
if row.len() < dim {
continue; }
row.truncate(dim);
l2_normalize(&mut row);
vocab.push(word);
vectors.extend_from_slice(&row);
}
Ok(Self::build(vocab, vectors, dim))
}
pub fn get_vocabs(&self) -> &[String] {
&self.vocab
}
fn build(vocab: Vec<String>, vectors: Vec<f32>, dim: usize) -> Self {
let index: HashMap<String, usize> =
vocab.iter().enumerate().map(|(i, w)| (w.clone(), i)).collect();
Self { vocab, index, vectors, dim }
}
#[inline]
fn row(&self, idx: usize) -> &[f32] {
&self.vectors[idx * self.dim..(idx + 1) * self.dim]
}
}
impl WordEmbedding for Word2VecEmbedding {
fn dim(&self) -> usize {
self.dim
}
fn embed_word(&self, word: &str) -> Option<Vec<f32>> {
let &idx = self.index.get(word)?;
Some(self.row(idx).to_vec())
}
fn most_similar(&self, word: &str, n: usize) -> Vec<(String, f32)> {
let &query_idx = match self.index.get(word) {
Some(i) => i,
None => return vec![],
};
let query = self.row(query_idx);
let mut scores: Vec<(usize, f32)> = (0..self.vocab.len())
.filter(|&i| i != query_idx)
.map(|i| (i, dot(query, self.row(i))))
.collect();
scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scores.truncate(n);
scores
.into_iter()
.map(|(i, score)| (self.vocab[i].clone(), score))
.collect()
}
fn load(path: &Path) -> Result<Self> {
Self::from_file(path)
}
}
pub struct AvgSentEmbedding<W: WordEmbedding> {
word_model: W,
}
impl<W: WordEmbedding> AvgSentEmbedding<W> {
pub fn new(word_model: W) -> Self {
Self { word_model }
}
pub fn word_model(&self) -> &W {
&self.word_model
}
}
impl<W: WordEmbedding> SentenceEmbedding for AvgSentEmbedding<W> {
fn dim(&self) -> usize {
self.word_model.dim()
}
fn embed(&self, tokens: &[&str]) -> Result<Vec<f32>> {
let mut avg = vec![0.0f32; self.dim()];
let mut count = 0usize;
for &tok in tokens {
if let Some(v) = self.word_model.embed_word(tok) {
for (a, b) in avg.iter_mut().zip(v.iter()) {
*a += b;
}
count += 1;
}
}
if count > 0 {
let n = count as f32;
for a in &mut avg {
*a /= n;
}
}
Ok(avg)
}
fn load(path: &Path) -> Result<Self>
where
Self: Sized,
{
Ok(Self::new(W::load(path)?))
}
}
fn parse_header(header: &str) -> Result<(usize, usize)> {
let mut parts = header.trim().split_whitespace();
let nw = parts
.next()
.ok_or_else(|| Error::Parse("missing num_words in header".to_string()))?
.parse::<usize>()
.map_err(|e| Error::Parse(format!("invalid num_words: {e}")))?;
let nd = parts
.next()
.ok_or_else(|| Error::Parse("missing num_dims in header".to_string()))?
.parse::<usize>()
.map_err(|e| Error::Parse(format!("invalid num_dims: {e}")))?;
Ok((nw, nd))
}
fn l2_normalize(v: &mut [f32]) {
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-12 {
for x in v.iter_mut() {
*x /= norm;
}
}
}
fn dot(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedding::WordEmbedding;
fn make_model() -> Word2VecEmbedding {
let content = "3 3\nکتاب 1.0 0.0 0.0\nدفتر 0.9 0.1 0.0\nپنجره 0.0 0.0 1.0\n";
let dir = std::env::temp_dir();
let path = dir.join("rustam_test_w2v.vec");
std::fs::write(&path, content).unwrap();
Word2VecEmbedding::from_text(&path).unwrap()
}
#[test]
fn embed_word_known() {
let m = make_model();
assert!(m.embed_word("کتاب").is_some());
}
#[test]
fn embed_word_oov() {
let m = make_model();
assert!(m.embed_word("xyz").is_none());
}
#[test]
fn most_similar_top1() {
let m = make_model();
let sim = m.most_similar("کتاب", 1);
assert_eq!(sim.len(), 1);
assert_eq!(sim[0].0, "دفتر");
}
#[test]
fn doesnt_match_odd_word() {
let m = make_model();
let odd = m.doesnt_match(&["کتاب", "دفتر", "پنجره"]);
assert_eq!(odd, Some("پنجره".to_string()));
}
#[test]
fn avg_sent_embedding() {
use super::AvgSentEmbedding;
use crate::embedding::SentenceEmbedding;
let m = make_model();
let sent = AvgSentEmbedding::new(m);
let v = sent.embed(&["کتاب", "دفتر"]).unwrap();
assert_eq!(v.len(), 3);
assert!(v[0] > 0.0); }
#[test]
fn avg_sent_all_oov() {
use super::AvgSentEmbedding;
use crate::embedding::SentenceEmbedding;
let m = make_model();
let sent = AvgSentEmbedding::new(m);
let v = sent.embed(&["xyz", "abc"]).unwrap();
assert!(v.iter().all(|&x| x == 0.0)); }
}