This guide covers training FastText-style subword embeddings with libgrammstein.
Subword embeddings learn distributed representations of words:
- Vocabulary building - Count word frequencies, filter rare words
- Skip-gram training - Predict context words from center word
- Negative sampling - Efficient approximation of softmax
- Subword integration - Update character n-gram embeddings
use libgrammstein::embedding::EmbeddingTrainerBuilder;
use libgrammstein::corpus::PlaintextReader;
// Load corpus
let reader = PlaintextReader::from_file("corpus.txt")?;
// Train embeddings
let model = EmbeddingTrainerBuilder::new()
.dim(100)
.window_size(5)
.min_count(5)
.epochs(5)
.train(&reader)?;
// Save model
model.save("embeddings.bin")?;Controls the size of word vectors:
| Dimension | Quality | Memory | Speed |
|---|---|---|---|
| 50 | Low | Small | Fast |
| 100 | Medium | Medium | Medium |
| 300 | High | Large | Slow |
Recommendation: 100 for small corpora, 300 for large corpora.
.dim(100) // 100-dimensional vectorsWords considered as context:
.window_size(5) // 5 words on each sideLarger windows capture broader semantic relationships.
Filter rare words from vocabulary:
.min_count(5) // Words appearing < 5 times are ignoredHigher values reduce vocabulary size and training time.
Number of passes over the corpus:
.epochs(5) // 5 passesMore epochs generally improve quality but increase training time.
Number of negative samples per positive sample:
.neg_samples(5) // 5 negative samplesMore samples improve quality but slow training.
Initial learning rate (decays linearly):
.learning_rate(0.05) // Default: 0.05Higher rates train faster but may be unstable.
let model = EmbeddingTrainerBuilder::new()
.dim(100) // Embedding dimension
.window_size(5) // Context window size
.min_count(5) // Minimum word frequency
.neg_samples(5) // Negative samples
.epochs(5) // Training epochs
.learning_rate(0.05) // Initial learning rate
.batch_size(10000) // Parallel batch size
.train(&reader)?;For each word in the corpus:
- Select center word with position
t - Sample context window randomly from
[1, window_size] - For each context word at position
t ± offset:- Compute dot product with center word
- Apply sigmoid to get probability
- Compute gradient for positive sample
- Sample negative words not in context
- Compute gradients for negative samples
- Update embeddings for center, context, and subwords
Subwords are character n-grams:
"hello" → ["<he", "hel", "ell", "llo", "lo>", "<hel", "hell", ...]
Configuration:
// In EmbeddingConfig
min_subword_len: 3, // Minimum n-gram length
max_subword_len: 6, // Maximum n-gram length
bucket_count: 2_000_000, // Hash bucketsFor out-of-vocabulary words, the model:
- Extracts character n-grams
- Hashes each to a bucket
- Averages the subword embeddings
This provides reasonable vectors for unseen words.
use crossbeam_channel::bounded;
use std::thread;
let (tx, rx) = bounded(100);
thread::spawn(move || {
while let Ok(progress) = rx.recv() {
println!(
"Epoch {}/{} | Words: {}/{} | LR: {:.6}",
progress.epoch,
total_epochs,
progress.words_processed,
progress.total_words,
progress.learning_rate
);
}
});
let trainer = EmbeddingTrainer::new(config);
let model = trainer.train_with_progress(&reader, tx)?;// Check similar words
let similar = model.most_similar("king", 10);
for (word, score) in similar {
println!("{}: {:.4}", word, score);
}
// Expected output for well-trained model:
// queen: 0.8234
// prince: 0.7891
// monarch: 0.7654// Test: king - man + woman ≈ queen
let results = model.analogy("man", "king", "woman", 5);
for (word, score) in results {
println!("{}: {:.4}", word, score);
}Use standard benchmarks:
fn evaluate_similarity(
model: &SubwordEmbedding,
word_pairs: &[(String, String, f32)], // (word1, word2, human_score)
) -> f64 {
let mut predicted = Vec::new();
let mut actual = Vec::new();
for (w1, w2, score) in word_pairs {
if model.contains(w1) && model.contains(w2) {
predicted.push(model.similarity(w1, w2) as f64);
actual.push(*score as f64);
}
}
// Compute Spearman correlation
spearman_correlation(&predicted, &actual)
}| Component | Size Formula |
|---|---|
| Word embeddings | vocab_size × dim × 4 bytes |
| Subword embeddings | bucket_count × dim × 4 bytes |
| Vocabulary | ~vocab_size × 20 bytes |
Example for dim=100, vocab=100k, buckets=2M:
- Word: 100k × 100 × 4 = 40 MB
- Subword: 2M × 100 × 4 = 800 MB
- Total: ~850 MB
-
Lower dimension:
.dim(50) // Half the memory
-
Fewer buckets:
// In EmbeddingConfig bucket_count: 500_000, // 25% of default
-
Higher min_count:
.min_count(10) // Smaller vocabulary
| Corpus Size | Dimension | Epochs |
|---|---|---|
| < 1M words | 50-100 | 10-20 |
| 1-10M words | 100 | 5-10 |
| 10-100M words | 100-200 | 3-5 |
| > 100M words | 200-300 | 1-3 |
- Similar words should have high cosine similarity
- Analogies should work (king - man + woman ≈ queen)
- OOV words should have reasonable neighbors
-
Poor quality embeddings
- Increase epochs
- Increase corpus size
- Lower learning rate
-
Training too slow
- Decrease epochs
- Increase batch_size
- Reduce dim
-
Out of memory
- Reduce bucket_count
- Reduce dim
- Increase min_count
Load and extend pre-trained models:
// Load pre-trained
let mut model = SubwordEmbedding::load("pretrained.bin")?;
// Use for downstream tasks
let vec = model.word_vector("hello");
// Find similar words
let similar = model.most_similar("computer", 10);# Train embeddings
grammstein train embedding corpus.txt embeddings.bin \
--dim 100 \
--window 5 \
--min-count 5 \
--epochs 5
# With checkpoints
grammstein train embedding large-corpus.txt embeddings.bin \
--dim 300 \
--epochs 10 \
--checkpoint ./checkpointsuse libgrammstein::embedding::{SubwordEmbedding, EmbeddingTrainerBuilder};
use libgrammstein::corpus::{WikipediaReader, WikipediaConfig};
fn main() -> libgrammstein::Result<()> {
// Load Wikipedia
let config = WikipediaConfig {
max_articles: Some(100_000),
..Default::default()
};
let reader = WikipediaReader::from_dump_with_config("enwiki.xml.bz2", config)?;
// Train
println!("Training embeddings...");
let model = EmbeddingTrainerBuilder::new()
.dim(100)
.window_size(5)
.min_count(5)
.epochs(5)
.train(&reader)?;
println!("Vocabulary: {} words", model.vocab_size());
println!("Dimension: {}", model.dim());
// Evaluate
println!("\nSimilar to 'king':");
for (word, score) in model.most_similar("king", 5) {
println!(" {}: {:.4}", word, score);
}
println!("\nAnalogy: man:king :: woman:?");
for (word, score) in model.analogy("man", "king", "woman", 3) {
println!(" {}: {:.4}", word, score);
}
// Save
model.save("wikipedia-embeddings.bin")?;
println!("\nModel saved");
Ok(())
}- SubwordEmbedding API - Complete API reference
- Hybrid Models - Combining with n-grams
- Hyperparameters - Tuning guide