extern crate pyo3;
extern crate regex;
use pyo3::prelude::*;
use pyo3::types::PyModule;
use std::collections::HashSet;
pub mod classifier;
pub mod crf;
pub mod fasttext;
pub mod featurizers;
pub mod lr;
pub mod preprocessor;
pub mod svm;
pub mod text;
use crf::model::CRFModel;
use crf::serialization::{CRFFormat, ModelLoader, ModelSaver};
use crf::tagger::CRFTagger;
use crf::trainer::{CRFTrainer as RustCRFTrainer, LossFunction, TrainerConfig, TrainingInstance};
use lr::model::LRModel;
use lr::predictor::LRClassifier;
use lr::serialization::{LRFormat, LRModelLoader, LRModelSaver};
use lr::trainer::{
LRTrainer as RustLRTrainer, TrainerConfig as LRTrainerConfig,
TrainingInstance as LRTrainingInstance,
};
use text::tfidf::{TfIdfConfig, TfIdfVectorizer};
use svm::LinearSVC;
use classifier::{Label, Sentence, TextClassifier};
use preprocessor::TextPreprocessor;
#[pyclass]
pub struct CRFFeaturizer {
pub object: featurizers::CRFFeaturizer,
}
#[pymethods]
impl CRFFeaturizer {
#[new]
pub fn new(feature_configs: Vec<String>, dictionary: HashSet<String>) -> PyResult<Self> {
Ok(CRFFeaturizer {
object: featurizers::CRFFeaturizer::new(feature_configs, dictionary),
})
}
pub fn process(
self_: PyRef<Self>,
sentences: Vec<Vec<Vec<String>>>,
) -> PyResult<Vec<Vec<Vec<String>>>> {
let output = self_.object.process(sentences);
Ok(output)
}
}
#[pyclass(name = "CRFModel")]
pub struct PyCRFModel {
model: CRFModel,
}
#[pymethods]
impl PyCRFModel {
#[new]
pub fn new() -> PyResult<Self> {
Ok(Self {
model: CRFModel::new(),
})
}
#[staticmethod]
pub fn with_labels(labels: Vec<String>) -> PyResult<Self> {
Ok(Self {
model: CRFModel::with_labels(labels),
})
}
#[getter]
pub fn num_labels(&self) -> usize {
self.model.num_labels
}
#[getter]
pub fn num_attributes(&self) -> usize {
self.model.num_attributes
}
pub fn num_state_features(&self) -> usize {
self.model.num_state_features()
}
pub fn num_transition_features(&self) -> usize {
self.model.num_transition_features()
}
pub fn get_labels(&self) -> Vec<String> {
self.model.labels.labels().to_vec()
}
pub fn save(&self, path: String) -> PyResult<()> {
let saver = ModelSaver::new();
saver
.save(&self.model, path, CRFFormat::CRFsuite)
.map_err(pyo3::exceptions::PyIOError::new_err)
}
#[staticmethod]
pub fn load(path: String) -> PyResult<Self> {
let loader = ModelLoader::new();
let model = loader
.load(path, CRFFormat::Auto)
.map_err(pyo3::exceptions::PyIOError::new_err)?;
Ok(Self { model })
}
pub fn l2_norm_squared(&self) -> f64 {
self.model.l2_norm_squared()
}
pub fn l1_norm(&self) -> f64 {
self.model.l1_norm()
}
fn __repr__(&self) -> String {
format!(
"CRFModel(num_labels={}, num_attributes={}, state_features={}, transition_features={})",
self.model.num_labels,
self.model.num_attributes,
self.model.num_state_features(),
self.model.num_transition_features()
)
}
}
#[pyclass(name = "CRFTagger")]
pub struct PyCRFTagger {
tagger: CRFTagger,
}
#[pymethods]
impl PyCRFTagger {
#[new]
pub fn new() -> PyResult<Self> {
Ok(Self {
tagger: CRFTagger::new(),
})
}
#[staticmethod]
pub fn from_model(model: &PyCRFModel) -> PyResult<Self> {
Ok(Self {
tagger: CRFTagger::from_model(model.model.clone()),
})
}
pub fn load(&mut self, path: String) -> PyResult<()> {
self.tagger
.load(path)
.map_err(pyo3::exceptions::PyIOError::new_err)
}
pub fn tag(&self, features: Vec<Vec<String>>) -> Vec<String> {
self.tagger.tag(&features)
}
pub fn tag_with_score(&self, features: Vec<Vec<String>>) -> (Vec<String>, f64) {
let result = self.tagger.tag_with_score(&features);
let labels: Vec<String> = result
.labels
.iter()
.map(|&id| {
self.tagger
.model()
.id_to_label(id)
.unwrap_or("O")
.to_string()
})
.collect();
(labels, result.score)
}
pub fn marginals(&self, features: Vec<Vec<String>>) -> Vec<Vec<f64>> {
self.tagger.compute_marginals(&features)
}
pub fn num_labels(&self) -> usize {
self.tagger.num_labels()
}
pub fn labels(&self) -> Vec<String> {
self.tagger.labels()
}
fn __repr__(&self) -> String {
format!("CRFTagger(num_labels={})", self.tagger.num_labels())
}
}
#[pyclass(name = "CRFTrainer")]
pub struct PyCRFTrainer {
trainer: RustCRFTrainer,
}
#[pymethods]
impl PyCRFTrainer {
#[new]
#[pyo3(signature = (loss_function="lbfgs", l1_penalty=0.0, l2_penalty=0.01, learning_rate=0.1, max_iterations=100, averaging=true, verbose=1))]
pub fn new(
loss_function: &str,
l1_penalty: f64,
l2_penalty: f64,
learning_rate: f64,
max_iterations: usize,
averaging: bool,
verbose: u8,
) -> PyResult<Self> {
let loss = match loss_function {
"lbfgs"
| "LBFGS"
| "l-bfgs"
| "L-BFGS"
| "nll"
| "NLL"
| "negative_log_likelihood"
| "sgd"
| "SGD" => LossFunction::LBFGS {
l1_penalty,
l2_penalty,
},
"perceptron" | "Perceptron" | "structured_perceptron" => {
LossFunction::StructuredPerceptron { learning_rate }
}
_ => {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"Unknown loss function: {}. Use 'lbfgs' (recommended) or 'perceptron'",
loss_function
)));
}
};
let config = TrainerConfig {
loss_function: loss,
max_iterations,
epsilon: 1e-5,
averaging,
verbose: verbose as i32,
};
Ok(Self {
trainer: RustCRFTrainer::with_config(config),
})
}
pub fn set_l1_penalty(&mut self, penalty: f64) {
self.trainer.set_l1_penalty(penalty);
}
pub fn set_l2_penalty(&mut self, penalty: f64) {
self.trainer.set_l2_penalty(penalty);
}
pub fn set_max_iterations(&mut self, max_iter: usize) {
self.trainer.set_max_iterations(max_iter);
}
pub fn train(&mut self, x: Vec<Vec<Vec<String>>>, y: Vec<Vec<String>>) -> PyResult<PyCRFModel> {
if x.len() != y.len() {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"X and y must have the same length: {} vs {}",
x.len(),
y.len()
)));
}
let data: Vec<TrainingInstance> = x
.into_iter()
.zip(y)
.map(|(features, labels)| TrainingInstance::new(features, labels))
.collect();
let model = self.trainer.train(&data);
Ok(PyCRFModel { model })
}
pub fn get_model(&self) -> PyCRFModel {
PyCRFModel {
model: self.trainer.get_model().clone(),
}
}
fn __repr__(&self) -> String {
"CRFTrainer()".to_string()
}
}
#[pyclass(name = "LRModel")]
pub struct PyLRModel {
model: LRModel,
}
#[pymethods]
impl PyLRModel {
#[new]
pub fn new() -> PyResult<Self> {
Ok(Self {
model: LRModel::new(),
})
}
#[staticmethod]
pub fn with_classes(classes: Vec<String>) -> PyResult<Self> {
Ok(Self {
model: LRModel::with_classes(classes),
})
}
#[getter]
pub fn num_classes(&self) -> usize {
self.model.num_classes
}
#[getter]
pub fn num_features(&self) -> usize {
self.model.num_features
}
pub fn num_weights(&self) -> usize {
self.model.num_weights()
}
pub fn get_classes(&self) -> Vec<String> {
self.model.get_classes()
}
pub fn save(&self, path: String) -> PyResult<()> {
let saver = LRModelSaver::new();
saver
.save(&self.model, path, LRFormat::Native)
.map_err(pyo3::exceptions::PyIOError::new_err)
}
#[staticmethod]
pub fn load(path: String) -> PyResult<Self> {
let loader = LRModelLoader::new();
let model = loader
.load(path, LRFormat::Auto)
.map_err(pyo3::exceptions::PyIOError::new_err)?;
Ok(Self { model })
}
pub fn l2_norm_squared(&self) -> f64 {
self.model.l2_norm_squared()
}
pub fn l1_norm(&self) -> f64 {
self.model.l1_norm()
}
fn __repr__(&self) -> String {
format!(
"LRModel(num_classes={}, num_features={}, num_weights={})",
self.model.num_classes,
self.model.num_features,
self.model.num_weights()
)
}
}
#[pyclass(name = "LRClassifier")]
pub struct PyLRClassifier {
classifier: LRClassifier,
}
#[pymethods]
impl PyLRClassifier {
#[new]
pub fn new() -> PyResult<Self> {
Ok(Self {
classifier: LRClassifier::new(),
})
}
#[staticmethod]
pub fn from_model(model: &PyLRModel) -> PyResult<Self> {
Ok(Self {
classifier: LRClassifier::from_model(model.model.clone()),
})
}
#[staticmethod]
pub fn load(path: String) -> PyResult<Self> {
let classifier = LRClassifier::load(path).map_err(pyo3::exceptions::PyIOError::new_err)?;
Ok(Self { classifier })
}
pub fn predict(&self, features: Vec<String>) -> String {
self.classifier.predict(&features)
}
pub fn predict_with_prob(&self, features: Vec<String>) -> (String, f64) {
self.classifier.predict_with_prob(&features)
}
pub fn predict_proba(&self, features: Vec<String>) -> Vec<(String, f64)> {
self.classifier.predict_proba(&features)
}
pub fn predict_top_k(&self, features: Vec<String>, k: usize) -> Vec<(String, f64)> {
self.classifier.predict_top_k(&features, k)
}
pub fn num_classes(&self) -> usize {
self.classifier.num_classes()
}
pub fn classes(&self) -> Vec<String> {
self.classifier.classes()
}
fn __repr__(&self) -> String {
format!(
"LRClassifier(num_classes={})",
self.classifier.num_classes()
)
}
}
#[pyclass(name = "LRTrainer")]
pub struct PyLRTrainer {
trainer: RustLRTrainer,
}
#[pymethods]
impl PyLRTrainer {
#[new]
#[pyo3(signature = (l1_penalty=0.0, l2_penalty=0.01, learning_rate=0.1, max_epochs=100, batch_size=1, tol=1e-4, verbose=1))]
pub fn new(
l1_penalty: f64,
l2_penalty: f64,
learning_rate: f64,
max_epochs: usize,
batch_size: usize,
tol: f64,
verbose: u8,
) -> PyResult<Self> {
let config = LRTrainerConfig {
l1_penalty,
l2_penalty,
learning_rate,
max_epochs,
batch_size: batch_size.max(1),
tol,
verbose,
};
Ok(Self {
trainer: RustLRTrainer::with_config(config),
})
}
pub fn set_l1_penalty(&mut self, penalty: f64) {
self.trainer.set_l1_penalty(penalty);
}
pub fn set_l2_penalty(&mut self, penalty: f64) {
self.trainer.set_l2_penalty(penalty);
}
pub fn set_learning_rate(&mut self, lr: f64) {
self.trainer.set_learning_rate(lr);
}
pub fn set_max_epochs(&mut self, epochs: usize) {
self.trainer.set_max_epochs(epochs);
}
pub fn set_batch_size(&mut self, size: usize) {
self.trainer.set_batch_size(size);
}
pub fn train(&mut self, x: Vec<Vec<String>>, y: Vec<String>) -> PyResult<PyLRModel> {
if x.len() != y.len() {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"X and y must have the same length: {} vs {}",
x.len(),
y.len()
)));
}
let data: Vec<LRTrainingInstance> = x
.into_iter()
.zip(y)
.map(|(features, label)| LRTrainingInstance::new(features, label))
.collect();
let model = self.trainer.train(&data);
Ok(PyLRModel { model })
}
pub fn get_model(&self) -> PyLRModel {
PyLRModel {
model: self.trainer.get_model().clone(),
}
}
fn __repr__(&self) -> String {
"LRTrainer()".to_string()
}
}
#[pyclass(name = "TfIdfVectorizer")]
pub struct PyTfIdfVectorizer {
vectorizer: TfIdfVectorizer,
}
#[pymethods]
impl PyTfIdfVectorizer {
#[new]
#[pyo3(signature = (min_df=1, max_df=1.0, max_features=0, sublinear_tf=false, lowercase=true, ngram_range=(1, 1), min_token_length=2, norm=true))]
#[allow(clippy::too_many_arguments)]
pub fn new(
min_df: usize,
max_df: f64,
max_features: usize,
sublinear_tf: bool,
lowercase: bool,
ngram_range: (usize, usize),
min_token_length: usize,
norm: bool,
) -> PyResult<Self> {
let config = TfIdfConfig {
min_df,
max_df,
max_features,
sublinear_tf,
lowercase,
ngram_range,
min_token_length,
norm,
};
Ok(Self {
vectorizer: TfIdfVectorizer::with_config(config),
})
}
pub fn fit(&mut self, documents: Vec<String>) {
self.vectorizer.fit(&documents);
}
pub fn transform(&self, document: &str) -> Vec<(u32, f64)> {
self.vectorizer.transform(document)
}
pub fn transform_dense(&self, document: &str) -> Vec<f64> {
self.vectorizer.transform_dense(document)
}
pub fn transform_to_features(&self, document: &str) -> Vec<String> {
self.vectorizer.transform_to_features(document)
}
pub fn fit_transform(&mut self, documents: Vec<String>) -> Vec<Vec<(u32, f64)>> {
self.vectorizer.fit_transform(&documents)
}
#[getter]
pub fn vocab_size(&self) -> usize {
self.vectorizer.vocab_size()
}
#[getter]
pub fn n_docs(&self) -> usize {
self.vectorizer.n_docs()
}
pub fn is_fitted(&self) -> bool {
self.vectorizer.is_fitted()
}
pub fn get_feature_names(&self) -> Vec<String> {
self.vectorizer.get_feature_names()
}
pub fn get_idf(&self) -> Vec<f64> {
self.vectorizer.idf_values().to_vec()
}
pub fn top_features_by_idf(&self, n: usize) -> Vec<(String, f64)> {
self.vectorizer.top_features_by_idf(n)
}
pub fn get_index(&self, word: &str) -> Option<u32> {
self.vectorizer.get_index(word)
}
pub fn get_word(&self, index: u32) -> Option<String> {
self.vectorizer.get_word(index).map(|s| s.to_string())
}
pub fn save(&self, path: String) -> PyResult<()> {
let data = bincode::serialize(&self.vectorizer).map_err(|e| {
pyo3::exceptions::PyIOError::new_err(format!("Serialization error: {}", e))
})?;
std::fs::write(&path, data)
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("Write error: {}", e)))?;
Ok(())
}
#[staticmethod]
pub fn load(path: String) -> PyResult<Self> {
let data = std::fs::read(&path)
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("Read error: {}", e)))?;
let vectorizer: TfIdfVectorizer = bincode::deserialize(&data).map_err(|e| {
pyo3::exceptions::PyIOError::new_err(format!("Deserialization error: {}", e))
})?;
Ok(Self { vectorizer })
}
fn __repr__(&self) -> String {
if self.vectorizer.is_fitted() {
format!(
"TfIdfVectorizer(vocab_size={}, n_docs={})",
self.vectorizer.vocab_size(),
self.vectorizer.n_docs()
)
} else {
"TfIdfVectorizer(not fitted)".to_string()
}
}
}
#[pymodule]
fn underthesea_core(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {
m.add_class::<CRFFeaturizer>()?;
m.add_class::<PyCRFModel>()?;
m.add_class::<PyCRFTagger>()?;
m.add_class::<PyCRFTrainer>()?;
m.add_class::<PyLRModel>()?;
m.add_class::<PyLRClassifier>()?;
m.add_class::<PyLRTrainer>()?;
m.add_class::<PyTfIdfVectorizer>()?;
m.add_class::<LinearSVC>()?;
m.add_class::<TextClassifier>()?;
m.add_class::<Label>()?;
m.add_class::<Sentence>()?;
m.add_class::<TextPreprocessor>()?;
m.add_class::<PyFastText>()?;
Ok(())
}
#[pyclass(name = "FastText")]
pub struct PyFastText {
model: fasttext::FastTextModel,
}
#[pymethods]
impl PyFastText {
#[staticmethod]
pub fn load(path: &str) -> PyResult<Self> {
let model =
fasttext::FastTextModel::load(path).map_err(pyo3::exceptions::PyIOError::new_err)?;
Ok(Self { model })
}
#[pyo3(signature = (text, k=1))]
pub fn predict(&self, text: &str, k: usize) -> Vec<(String, f32)> {
self.model.predict(text, k)
}
pub fn get_labels(&self) -> Vec<String> {
self.model.get_labels()
}
pub fn get_hidden(&self, text: &str) -> Vec<f32> {
self.model.get_hidden(text)
}
pub fn get_features(&self, text: &str) -> Vec<i32> {
self.model.get_features(text)
}
#[getter]
pub fn dim(&self) -> i32 {
self.model.dim()
}
#[getter]
pub fn nwords(&self) -> i32 {
self.model.nwords()
}
#[getter]
pub fn nlabels(&self) -> i32 {
self.model.nlabels()
}
}