use std::collections::BTreeMap;
use std::str::FromStr;
use anyhow::{anyhow, Result};
use crate::feature::{ExampleGenerator, FeatureExtractor};
use crate::model::Model;
use crate::sentence::Sentence;
use crate::utils::FeatureIDManager;
#[derive(Clone, Copy, Debug)]
pub enum SolverType {
L2RegularizedLogistic = 0,
L2RegularizedL2LossSVCDual = 1,
L2RegularizedL2LossSVC = 2,
L2RegularizedL1LossSVCDual = 3,
CrammerSingerSVC = 4,
L1RegularizedL2LossSVC = 5,
L1RegularizedLogistic = 6,
L2RegularizedLogisticDual = 7,
}
impl FromStr for SolverType {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"0" => Ok(Self::L2RegularizedLogistic),
"1" => Ok(Self::L2RegularizedL2LossSVCDual),
"2" => Ok(Self::L2RegularizedL2LossSVC),
"3" => Ok(Self::L2RegularizedL1LossSVCDual),
"4" => Ok(Self::CrammerSingerSVC),
"5" => Ok(Self::L1RegularizedL2LossSVC),
"6" => Ok(Self::L1RegularizedLogistic),
"7" => Ok(Self::L2RegularizedLogisticDual),
_ => Err("Unsupported solver type."),
}
}
}
impl From<SolverType> for liblinear::SolverType {
fn from(solver: SolverType) -> Self {
match solver {
SolverType::L2RegularizedLogistic => Self::L2R_LR,
SolverType::L2RegularizedL2LossSVCDual => Self::L2R_L2LOSS_SVC_DUAL,
SolverType::L2RegularizedL2LossSVC => Self::L2R_L2LOSS_SVC,
SolverType::L2RegularizedL1LossSVCDual => Self::L2R_L1LOSS_SVC_DUAL,
SolverType::CrammerSingerSVC => Self::MCSVM_CS,
SolverType::L1RegularizedL2LossSVC => Self::L1R_L2LOSS_SVC,
SolverType::L1RegularizedLogistic => Self::L1R_LR,
SolverType::L2RegularizedLogisticDual => Self::L2R_LR_DUAL,
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "train")))]
pub struct Dataset<'a> {
dictionary: Vec<Vec<u8>>,
feature_extractor: FeatureExtractor,
example_generator: ExampleGenerator,
char_window_size: usize,
type_window_size: usize,
dict_word_max_size: usize,
fid_manager: FeatureIDManager<'a>,
xs: Vec<Vec<(u32, f64)>>,
ys: Vec<f64>,
}
impl<'a> Dataset<'a> {
pub fn new<D, P>(
char_ngram_size: usize,
char_window_size: usize,
type_ngram_size: usize,
type_window_size: usize,
dictionary: D,
dict_word_max_size: usize,
) -> Result<Self>
where
D: AsRef<[P]>,
P: AsRef<[u8]> + AsRef<str>,
{
Ok(Self {
dictionary: dictionary
.as_ref()
.iter()
.map(|word| (word.as_ref() as &[u8]).to_vec())
.collect(),
feature_extractor: FeatureExtractor::new(
char_ngram_size,
type_ngram_size,
dictionary,
dict_word_max_size,
)?,
example_generator: ExampleGenerator::new(char_window_size, type_window_size),
char_window_size,
type_window_size,
dict_word_max_size,
fid_manager: FeatureIDManager::default(),
xs: vec![],
ys: vec![],
})
}
pub fn push_sentence(&mut self, s: &'a Sentence) {
let feature_spans = self.feature_extractor.extract(s);
let examples = self.example_generator.generate(s, feature_spans, false);
for example in examples {
let mut feature_ids = BTreeMap::new();
for f in example.features {
let fid = self.fid_manager.get_id(f) + 1;
if let Some(v) = feature_ids.get_mut(&fid) {
*v += 1.0;
} else {
feature_ids.insert(fid, 1.0);
}
}
self.xs.push(feature_ids.into_iter().collect());
self.ys.push(example.label as u8 as f64);
}
}
pub fn n_features(&self) -> usize {
self.fid_manager.map.len()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "train")))]
pub struct Trainer {
epsilon: f64,
cost: f64,
bias: f64,
}
impl Trainer {
pub const fn new(epsilon: f64, cost: f64, bias: f64) -> Self {
Self {
epsilon,
cost,
bias,
}
}
pub fn train(&self, dataset: Dataset, solver: SolverType) -> Result<Model> {
let mut builder = liblinear::Builder::new();
let training_input =
liblinear::util::TrainingInput::from_sparse_features(dataset.ys, dataset.xs)
.map_err(|e| anyhow!("liblinear error: {:?}", e))?;
builder.problem().input_data(training_input).bias(self.bias);
builder
.parameters()
.solver_type(solver.into())
.stopping_criterion(self.epsilon)
.constraints_violation_cost(self.cost);
let model = builder.build_model().map_err(|e| anyhow!(e.to_string()))?;
Ok(Model::from_liblinear_model(
model,
dataset.fid_manager,
dataset.dictionary,
dataset.char_window_size,
dataset.type_window_size,
dataset.dict_word_max_size,
))
}
}