litsea 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Cargo.toml +25 -0
- data/README.md +137 -0
- data/README_ja.md +137 -0
- data/extconf.rb +8 -0
- data/lib/litsea/version.rb +7 -0
- data/lib/litsea.rb +19 -0
- data/src/error.rs +120 -0
- data/src/gvl.rs +112 -0
- data/src/language.rs +31 -0
- data/src/lib.rs +84 -0
- data/src/metrics.rs +250 -0
- data/src/segmenter.rs +241 -0
- data/src/token.rs +135 -0
- data/src/trainer.rs +421 -0
- metadata +72 -0
data/src/token.rs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//! The token class handed to Ruby.
|
|
2
|
+
|
|
3
|
+
use litsea_binding_core::TokenView;
|
|
4
|
+
use magnus::{Module, RModule, Ruby, error::Error};
|
|
5
|
+
|
|
6
|
+
/// A segmented token.
|
|
7
|
+
///
|
|
8
|
+
/// `start` and `end` are byte offsets into the input string, so
|
|
9
|
+
/// `text.byteslice(token.start, token.end - token.start)` returns the
|
|
10
|
+
/// surface. Ruby's `String#[]` works in characters, hence `byteslice`.
|
|
11
|
+
#[magnus::wrap(class = "Litsea::Token", free_immediately, size)]
|
|
12
|
+
pub struct Token {
|
|
13
|
+
/// The token's surface form.
|
|
14
|
+
surface: String,
|
|
15
|
+
/// The UPOS tag name (for example `"NOUN"`), or `nil` when the segmenter
|
|
16
|
+
/// has no POS model.
|
|
17
|
+
pos: Option<String>,
|
|
18
|
+
/// Starting byte offset in the input string.
|
|
19
|
+
start: usize,
|
|
20
|
+
/// Ending byte offset (exclusive) in the input string.
|
|
21
|
+
end: usize,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Token {
|
|
25
|
+
/// Returns the token's surface form.
|
|
26
|
+
///
|
|
27
|
+
/// # Returns
|
|
28
|
+
/// The surface string.
|
|
29
|
+
fn surface(&self) -> String {
|
|
30
|
+
self.surface.clone()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Returns the UPOS tag name.
|
|
34
|
+
///
|
|
35
|
+
/// # Returns
|
|
36
|
+
/// The tag name, or `nil` for segmentation-only output.
|
|
37
|
+
fn pos(&self) -> Option<String> {
|
|
38
|
+
self.pos.clone()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Returns the starting byte offset.
|
|
42
|
+
///
|
|
43
|
+
/// # Returns
|
|
44
|
+
/// The offset into the input string.
|
|
45
|
+
fn start(&self) -> usize {
|
|
46
|
+
self.start
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Returns the ending byte offset (exclusive).
|
|
50
|
+
///
|
|
51
|
+
/// # Returns
|
|
52
|
+
/// The offset into the input string.
|
|
53
|
+
fn end(&self) -> usize {
|
|
54
|
+
self.end
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Returns a readable representation.
|
|
58
|
+
///
|
|
59
|
+
/// # Returns
|
|
60
|
+
/// For example `#<Litsea::Token これ/PRON [0..6]>`.
|
|
61
|
+
fn inspect(&self) -> String {
|
|
62
|
+
match &self.pos {
|
|
63
|
+
Some(pos) => {
|
|
64
|
+
format!("#<Litsea::Token {}/{} [{}..{}]>", self.surface, pos, self.start, self.end)
|
|
65
|
+
}
|
|
66
|
+
None => format!("#<Litsea::Token {} [{}..{}]>", self.surface, self.start, self.end),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl From<TokenView> for Token {
|
|
72
|
+
/// Converts a core token view into the Ruby-facing token.
|
|
73
|
+
///
|
|
74
|
+
/// # Arguments
|
|
75
|
+
/// * `view` - The token view.
|
|
76
|
+
///
|
|
77
|
+
/// # Returns
|
|
78
|
+
/// The corresponding [`Token`].
|
|
79
|
+
fn from(view: TokenView) -> Self {
|
|
80
|
+
Self {
|
|
81
|
+
surface: view.surface,
|
|
82
|
+
// The tag travels as its name, matching the other bindings'
|
|
83
|
+
// token shape.
|
|
84
|
+
pos: view.pos.map(|pos| pos.to_string()),
|
|
85
|
+
start: view.byte_start,
|
|
86
|
+
end: view.byte_end,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Defines `Litsea::Token`.
|
|
92
|
+
///
|
|
93
|
+
/// # Arguments
|
|
94
|
+
/// * `ruby` - The Ruby handle for the current thread.
|
|
95
|
+
/// * `module` - The `Litsea` module to define the class on.
|
|
96
|
+
///
|
|
97
|
+
/// # Returns
|
|
98
|
+
/// `()` on success.
|
|
99
|
+
///
|
|
100
|
+
/// # Errors
|
|
101
|
+
/// Returns a Ruby exception if the class cannot be defined.
|
|
102
|
+
pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
103
|
+
let class = module.define_class("Token", ruby.class_object())?;
|
|
104
|
+
class.define_method("surface", magnus::method!(Token::surface, 0))?;
|
|
105
|
+
class.define_method("pos", magnus::method!(Token::pos, 0))?;
|
|
106
|
+
class.define_method("start", magnus::method!(Token::start, 0))?;
|
|
107
|
+
class.define_method("end", magnus::method!(Token::end, 0))?;
|
|
108
|
+
class.define_method("inspect", magnus::method!(Token::inspect, 0))?;
|
|
109
|
+
class.define_method("to_s", magnus::method!(Token::inspect, 0))?;
|
|
110
|
+
Ok(())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[cfg(test)]
|
|
114
|
+
mod tests {
|
|
115
|
+
use litsea::Upos;
|
|
116
|
+
|
|
117
|
+
use super::*;
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn test_conversion_keeps_offsets_and_tag_name() {
|
|
121
|
+
let token = Token::from(TokenView::new("テスト", 9, 18, Some(Upos::NOUN)));
|
|
122
|
+
assert_eq!(token.surface(), "テスト");
|
|
123
|
+
assert_eq!(token.pos().as_deref(), Some("NOUN"));
|
|
124
|
+
assert_eq!(token.start(), 9);
|
|
125
|
+
assert_eq!(token.end(), 18);
|
|
126
|
+
assert_eq!(token.inspect(), "#<Litsea::Token テスト/NOUN [9..18]>");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn test_untagged_token() {
|
|
131
|
+
let token = Token::from(TokenView::new("テスト", 0, 9, None));
|
|
132
|
+
assert_eq!(token.pos(), None);
|
|
133
|
+
assert_eq!(token.inspect(), "#<Litsea::Token テスト [0..9]>");
|
|
134
|
+
}
|
|
135
|
+
}
|
data/src/trainer.rs
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
//! Feature extraction, training, and cancellation.
|
|
2
|
+
//!
|
|
3
|
+
//! Training releases the GVL (see [`crate::gvl`]), so other Ruby threads keep
|
|
4
|
+
//! running while it works - which is what makes [`CancelToken`] usable while
|
|
5
|
+
//! a run is already going, as in the Python and Node.js bindings.
|
|
6
|
+
|
|
7
|
+
use std::cell::RefCell;
|
|
8
|
+
use std::path::PathBuf;
|
|
9
|
+
|
|
10
|
+
use litsea_binding_core::{
|
|
11
|
+
CancelToken as CoreCancelToken, CoreExtractor, CorePerceptronTrainer, CoreTrainer,
|
|
12
|
+
CoreTwoStageTrainer, CorpusFormat, parse_feature_set,
|
|
13
|
+
};
|
|
14
|
+
use magnus::{
|
|
15
|
+
Module, Object, RModule, Ruby, Value, error::Error, function, method, scan_args::scan_args,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
use crate::error::map_err;
|
|
19
|
+
use crate::gvl::without_gvl;
|
|
20
|
+
use crate::language::language_from_value;
|
|
21
|
+
use crate::metrics::{RbBinaryMetrics, RbMulticlassMetrics, RbTwoStageMetrics};
|
|
22
|
+
|
|
23
|
+
/// A flag that asks a running training job to stop.
|
|
24
|
+
///
|
|
25
|
+
/// Cancelling is cooperative and is **not** an error: training stops at its
|
|
26
|
+
/// next check point, still writes the partially trained model, and returns
|
|
27
|
+
/// its metrics. Because training releases the GVL, another Ruby thread can
|
|
28
|
+
/// cancel a run that is already going.
|
|
29
|
+
#[magnus::wrap(class = "Litsea::CancelToken", free_immediately, size)]
|
|
30
|
+
pub struct CancelToken {
|
|
31
|
+
/// The wrapped token; clones share one flag.
|
|
32
|
+
inner: CoreCancelToken,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl CancelToken {
|
|
36
|
+
/// Creates a token in the "keep running" state.
|
|
37
|
+
///
|
|
38
|
+
/// # Returns
|
|
39
|
+
/// The new token.
|
|
40
|
+
fn new() -> Self {
|
|
41
|
+
Self {
|
|
42
|
+
inner: CoreCancelToken::new(),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Requests cancellation.
|
|
47
|
+
fn cancel(&self) {
|
|
48
|
+
self.inner.cancel();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Returns the token to the "keep running" state.
|
|
52
|
+
fn reset(&self) {
|
|
53
|
+
self.inner.reset();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Returns whether cancellation has been requested.
|
|
57
|
+
///
|
|
58
|
+
/// # Returns
|
|
59
|
+
/// `true` once `cancel` has been called.
|
|
60
|
+
fn is_cancelled(&self) -> bool {
|
|
61
|
+
self.inner.is_cancelled()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Returns the flag a training run should observe.
|
|
65
|
+
///
|
|
66
|
+
/// # Arguments
|
|
67
|
+
/// * `token` - The caller-supplied token, if any.
|
|
68
|
+
///
|
|
69
|
+
/// # Returns
|
|
70
|
+
/// A clone of the supplied token, or a fresh one.
|
|
71
|
+
fn resolve(token: Option<&CancelToken>) -> CoreCancelToken {
|
|
72
|
+
token.map_or_else(CoreCancelToken::new, |token| token.inner.clone())
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Reads the optional `cancel:` keyword argument.
|
|
77
|
+
///
|
|
78
|
+
/// magnus cannot express an optional keyword argument in a `method!` arity,
|
|
79
|
+
/// so the trainers take their arguments through `scan_args`.
|
|
80
|
+
///
|
|
81
|
+
/// # Arguments
|
|
82
|
+
/// * `args` - The raw Ruby arguments.
|
|
83
|
+
///
|
|
84
|
+
/// # Returns
|
|
85
|
+
/// The model path and the cancellation flag to observe.
|
|
86
|
+
///
|
|
87
|
+
/// # Errors
|
|
88
|
+
/// Returns a Ruby `ArgumentError` if the arguments do not match.
|
|
89
|
+
fn scan_train_args(args: &[magnus::Value]) -> Result<(PathBuf, CoreCancelToken), Error> {
|
|
90
|
+
let args = scan_args::<(String,), (), (), (), _, ()>(args)?;
|
|
91
|
+
let (model_path,) = args.required;
|
|
92
|
+
let kwargs = magnus::scan_args::get_kwargs::<_, (), (Option<Option<&CancelToken>>,), ()>(
|
|
93
|
+
args.keywords,
|
|
94
|
+
&[],
|
|
95
|
+
&["cancel"],
|
|
96
|
+
)?;
|
|
97
|
+
let (cancel,) = kwargs.optional;
|
|
98
|
+
|
|
99
|
+
Ok((PathBuf::from(model_path), CancelToken::resolve(cancel.flatten())))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Extracts training features from a corpus.
|
|
103
|
+
#[magnus::wrap(class = "Litsea::Extractor", free_immediately, size)]
|
|
104
|
+
pub struct Extractor {
|
|
105
|
+
/// The wrapped extractor.
|
|
106
|
+
inner: CoreExtractor,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl Extractor {
|
|
110
|
+
/// Creates an extractor for a language.
|
|
111
|
+
///
|
|
112
|
+
/// # Arguments
|
|
113
|
+
/// * `language` - A language name or ISO 639-1 code, as a String or Symbol.
|
|
114
|
+
///
|
|
115
|
+
/// # Returns
|
|
116
|
+
/// The new extractor.
|
|
117
|
+
///
|
|
118
|
+
/// # Errors
|
|
119
|
+
/// Raises `Litsea::InvalidArgumentError` for an unknown language.
|
|
120
|
+
fn new(language: Value) -> Result<Self, Error> {
|
|
121
|
+
Ok(Self {
|
|
122
|
+
inner: CoreExtractor::new(language_from_value(language)?),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Extracts boundary-classification features.
|
|
127
|
+
///
|
|
128
|
+
/// Accepts `tsv:` and `tag_free:` keyword arguments.
|
|
129
|
+
///
|
|
130
|
+
/// # Arguments
|
|
131
|
+
/// * `args` - `corpus_path`, `features_path`, and the keywords.
|
|
132
|
+
///
|
|
133
|
+
/// # Returns
|
|
134
|
+
/// `nil`; the features file is written.
|
|
135
|
+
///
|
|
136
|
+
/// # Errors
|
|
137
|
+
/// Raises `Litsea::IoError` if the corpus cannot be read or the output
|
|
138
|
+
/// cannot be written.
|
|
139
|
+
fn extract(&self, args: &[magnus::Value]) -> Result<(), Error> {
|
|
140
|
+
let args = scan_args::<(String, String), (), (), (), _, ()>(args)?;
|
|
141
|
+
let (corpus_path, features_path) = args.required;
|
|
142
|
+
let kwargs = magnus::scan_args::get_kwargs::<_, (), (Option<bool>, Option<bool>), ()>(
|
|
143
|
+
args.keywords,
|
|
144
|
+
&[],
|
|
145
|
+
&["tsv", "tag_free"],
|
|
146
|
+
)?;
|
|
147
|
+
let (tsv, tag_free) = kwargs.optional;
|
|
148
|
+
|
|
149
|
+
let extracted = without_gvl(|| {
|
|
150
|
+
self.inner.extract(
|
|
151
|
+
std::path::Path::new(&corpus_path),
|
|
152
|
+
std::path::Path::new(&features_path),
|
|
153
|
+
CorpusFormat::from_tsv_flag(tsv.unwrap_or(false)),
|
|
154
|
+
tag_free.unwrap_or(false),
|
|
155
|
+
)
|
|
156
|
+
});
|
|
157
|
+
map_err(extracted)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Extracts two-stage (segmentation + POS) features.
|
|
161
|
+
///
|
|
162
|
+
/// Writes `{output_prefix}.stage1`, `.stage2`, and `.lexicon`. Accepts
|
|
163
|
+
/// `feature_set:` and `tsv:` keyword arguments.
|
|
164
|
+
///
|
|
165
|
+
/// # Arguments
|
|
166
|
+
/// * `args` - `corpus_path`, `output_prefix`, and the keywords.
|
|
167
|
+
///
|
|
168
|
+
/// # Returns
|
|
169
|
+
/// `nil`; the three files are written.
|
|
170
|
+
///
|
|
171
|
+
/// # Errors
|
|
172
|
+
/// Raises `Litsea::InvalidArgumentError` for an unknown feature set, or
|
|
173
|
+
/// `Litsea::IoError` on I/O failure.
|
|
174
|
+
fn extract_two_stage(&self, args: &[magnus::Value]) -> Result<(), Error> {
|
|
175
|
+
let args = scan_args::<(String, String), (), (), (), _, ()>(args)?;
|
|
176
|
+
let (corpus_path, output_prefix) = args.required;
|
|
177
|
+
let kwargs = magnus::scan_args::get_kwargs::<_, (), (Option<String>, Option<bool>), ()>(
|
|
178
|
+
args.keywords,
|
|
179
|
+
&[],
|
|
180
|
+
&["feature_set", "tsv"],
|
|
181
|
+
)?;
|
|
182
|
+
let (feature_set, tsv) = kwargs.optional;
|
|
183
|
+
|
|
184
|
+
let feature_set = map_err(parse_feature_set(feature_set.as_deref().unwrap_or("fast")))?;
|
|
185
|
+
|
|
186
|
+
let extracted = without_gvl(|| {
|
|
187
|
+
self.inner.extract_two_stage(
|
|
188
|
+
std::path::Path::new(&corpus_path),
|
|
189
|
+
std::path::Path::new(&output_prefix),
|
|
190
|
+
feature_set,
|
|
191
|
+
CorpusFormat::from_tsv_flag(tsv.unwrap_or(false)),
|
|
192
|
+
)
|
|
193
|
+
});
|
|
194
|
+
map_err(extracted)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Trains a segmentation model.
|
|
199
|
+
#[magnus::wrap(class = "Litsea::Trainer", free_immediately, size)]
|
|
200
|
+
pub struct Trainer {
|
|
201
|
+
/// The wrapped trainer; `RefCell` because Ruby methods take `&self`.
|
|
202
|
+
inner: RefCell<CoreTrainer>,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
impl Trainer {
|
|
206
|
+
/// Loads a features file and prepares training.
|
|
207
|
+
///
|
|
208
|
+
/// # Arguments
|
|
209
|
+
/// * `threshold` - Early-stopping threshold for weak classifiers.
|
|
210
|
+
/// * `num_iterations` - Maximum number of boosting iterations.
|
|
211
|
+
/// * `features_path` - Path to the features file.
|
|
212
|
+
///
|
|
213
|
+
/// # Returns
|
|
214
|
+
/// The new trainer.
|
|
215
|
+
///
|
|
216
|
+
/// # Errors
|
|
217
|
+
/// Raises `Litsea::IoError` or `Litsea::ParseError` if the features file
|
|
218
|
+
/// cannot be read.
|
|
219
|
+
fn new(threshold: f64, num_iterations: usize, features_path: String) -> Result<Self, Error> {
|
|
220
|
+
let inner = map_err(CoreTrainer::new(
|
|
221
|
+
threshold,
|
|
222
|
+
num_iterations,
|
|
223
|
+
std::path::Path::new(&features_path),
|
|
224
|
+
))?;
|
|
225
|
+
Ok(Self {
|
|
226
|
+
inner: RefCell::new(inner),
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Loads an existing model to continue training from it.
|
|
231
|
+
///
|
|
232
|
+
/// # Arguments
|
|
233
|
+
/// * `model_uri` - Path, `file://` path, or `http(s)://` URL.
|
|
234
|
+
///
|
|
235
|
+
/// # Returns
|
|
236
|
+
/// `nil`; the model is merged into the learner.
|
|
237
|
+
///
|
|
238
|
+
/// # Errors
|
|
239
|
+
/// Raises `Litsea::ModelError`, `Litsea::IoError`, or
|
|
240
|
+
/// `Litsea::ParseError`.
|
|
241
|
+
fn load_model(&self, model_uri: String) -> Result<(), Error> {
|
|
242
|
+
let loaded = without_gvl(|| self.inner.borrow_mut().load_model_blocking(&model_uri));
|
|
243
|
+
map_err(loaded)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// Trains the model and writes it to `model_path`.
|
|
247
|
+
///
|
|
248
|
+
/// The GVL is released while training runs, so another Ruby thread can
|
|
249
|
+
/// cancel it through the `cancel:` token.
|
|
250
|
+
///
|
|
251
|
+
/// # Arguments
|
|
252
|
+
/// * `args` - `model_path` and an optional `cancel:` token.
|
|
253
|
+
///
|
|
254
|
+
/// # Returns
|
|
255
|
+
/// The training metrics.
|
|
256
|
+
///
|
|
257
|
+
/// # Errors
|
|
258
|
+
/// Raises `Litsea::IoError` if the model cannot be written.
|
|
259
|
+
fn train(&self, args: &[magnus::Value]) -> Result<RbBinaryMetrics, Error> {
|
|
260
|
+
let (model_path, cancel) = scan_train_args(args)?;
|
|
261
|
+
let trained = without_gvl(|| self.inner.borrow_mut().train(&cancel, &model_path));
|
|
262
|
+
Ok(RbBinaryMetrics::from(map_err(trained)?))
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Trains a label-agnostic Averaged Perceptron model.
|
|
267
|
+
#[magnus::wrap(class = "Litsea::PerceptronTrainer", free_immediately, size)]
|
|
268
|
+
pub struct PerceptronTrainer {
|
|
269
|
+
/// The wrapped trainer.
|
|
270
|
+
inner: RefCell<CorePerceptronTrainer>,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
impl PerceptronTrainer {
|
|
274
|
+
/// Loads a features file and prepares training.
|
|
275
|
+
///
|
|
276
|
+
/// # Arguments
|
|
277
|
+
/// * `num_epochs` - Number of passes over the training data.
|
|
278
|
+
/// * `features_path` - Path to the features file.
|
|
279
|
+
///
|
|
280
|
+
/// # Returns
|
|
281
|
+
/// The new trainer.
|
|
282
|
+
///
|
|
283
|
+
/// # Errors
|
|
284
|
+
/// Raises `Litsea::IoError` or `Litsea::ParseError` if the features file
|
|
285
|
+
/// cannot be read.
|
|
286
|
+
fn new(num_epochs: usize, features_path: String) -> Result<Self, Error> {
|
|
287
|
+
let inner =
|
|
288
|
+
map_err(CorePerceptronTrainer::new(num_epochs, std::path::Path::new(&features_path)))?;
|
|
289
|
+
Ok(Self {
|
|
290
|
+
inner: RefCell::new(inner),
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Trains the model and writes it to `model_path`.
|
|
295
|
+
///
|
|
296
|
+
/// # Arguments
|
|
297
|
+
/// * `args` - `model_path` and an optional `cancel:` token.
|
|
298
|
+
///
|
|
299
|
+
/// # Returns
|
|
300
|
+
/// The training metrics.
|
|
301
|
+
///
|
|
302
|
+
/// # Errors
|
|
303
|
+
/// Raises `Litsea::IoError` if the model cannot be written.
|
|
304
|
+
fn train(&self, args: &[magnus::Value]) -> Result<RbMulticlassMetrics, Error> {
|
|
305
|
+
let (model_path, cancel) = scan_train_args(args)?;
|
|
306
|
+
let trained = without_gvl(|| self.inner.borrow_mut().train(&cancel, &model_path));
|
|
307
|
+
Ok(RbMulticlassMetrics::from(map_err(trained)?))
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// Trains a two-stage segmentation + POS model.
|
|
312
|
+
///
|
|
313
|
+
/// A trainer can only be used once: training collapses stage 1 into an
|
|
314
|
+
/// AdaBoost model, which consumes it. `available?` reports the state, and a
|
|
315
|
+
/// second `train` raises.
|
|
316
|
+
#[magnus::wrap(class = "Litsea::TwoStageTrainer", free_immediately, size)]
|
|
317
|
+
pub struct TwoStageTrainer {
|
|
318
|
+
/// The wrapped trainer.
|
|
319
|
+
inner: RefCell<CoreTwoStageTrainer>,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
impl TwoStageTrainer {
|
|
323
|
+
/// Loads a two-stage features prefix and prepares training.
|
|
324
|
+
///
|
|
325
|
+
/// Accepts a `dominance:` keyword argument.
|
|
326
|
+
///
|
|
327
|
+
/// # Arguments
|
|
328
|
+
/// * `args` - `num_epochs`, `features_prefix`, and the keyword.
|
|
329
|
+
///
|
|
330
|
+
/// # Returns
|
|
331
|
+
/// The new trainer.
|
|
332
|
+
///
|
|
333
|
+
/// # Errors
|
|
334
|
+
/// Raises `Litsea::InvalidArgumentError` if `dominance` is out of range,
|
|
335
|
+
/// or `Litsea::IoError` / `Litsea::ParseError` if the feature files
|
|
336
|
+
/// cannot be read.
|
|
337
|
+
fn new(args: &[magnus::Value]) -> Result<Self, Error> {
|
|
338
|
+
let args = scan_args::<(usize, String), (), (), (), _, ()>(args)?;
|
|
339
|
+
let (num_epochs, features_prefix) = args.required;
|
|
340
|
+
let kwargs = magnus::scan_args::get_kwargs::<_, (), (Option<f64>,), ()>(
|
|
341
|
+
args.keywords,
|
|
342
|
+
&[],
|
|
343
|
+
&["dominance"],
|
|
344
|
+
)?;
|
|
345
|
+
let (dominance,) = kwargs.optional;
|
|
346
|
+
|
|
347
|
+
let inner = map_err(CoreTwoStageTrainer::new(
|
|
348
|
+
num_epochs,
|
|
349
|
+
dominance.unwrap_or(0.99),
|
|
350
|
+
std::path::Path::new(&features_prefix),
|
|
351
|
+
))?;
|
|
352
|
+
Ok(Self {
|
|
353
|
+
inner: RefCell::new(inner),
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/// Returns whether this trainer can still be used.
|
|
358
|
+
///
|
|
359
|
+
/// # Returns
|
|
360
|
+
/// `false` once `train` has run.
|
|
361
|
+
fn is_available(&self) -> bool {
|
|
362
|
+
self.inner.borrow().is_available()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// Trains both stages and writes the model.
|
|
366
|
+
///
|
|
367
|
+
/// # Arguments
|
|
368
|
+
/// * `args` - `model_path` and an optional `cancel:` token.
|
|
369
|
+
///
|
|
370
|
+
/// # Returns
|
|
371
|
+
/// The metrics of both stages.
|
|
372
|
+
///
|
|
373
|
+
/// # Errors
|
|
374
|
+
/// Raises `Litsea::InvalidArgumentError` if the trainer has already been
|
|
375
|
+
/// used, or `Litsea::IoError` if the model cannot be written.
|
|
376
|
+
fn train(&self, args: &[magnus::Value]) -> Result<RbTwoStageMetrics, Error> {
|
|
377
|
+
let (model_path, cancel) = scan_train_args(args)?;
|
|
378
|
+
let trained = without_gvl(|| self.inner.borrow_mut().train(&cancel, &model_path));
|
|
379
|
+
Ok(RbTwoStageMetrics::from(map_err(trained)?))
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// Defines the training classes.
|
|
384
|
+
///
|
|
385
|
+
/// # Arguments
|
|
386
|
+
/// * `ruby` - The Ruby handle for the current thread.
|
|
387
|
+
/// * `module` - The `Litsea` module to define the classes on.
|
|
388
|
+
///
|
|
389
|
+
/// # Returns
|
|
390
|
+
/// `()` on success.
|
|
391
|
+
///
|
|
392
|
+
/// # Errors
|
|
393
|
+
/// Returns a Ruby exception if a class cannot be defined.
|
|
394
|
+
pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
395
|
+
let cancel = module.define_class("CancelToken", ruby.class_object())?;
|
|
396
|
+
cancel.define_singleton_method("new", function!(CancelToken::new, 0))?;
|
|
397
|
+
cancel.define_method("cancel", method!(CancelToken::cancel, 0))?;
|
|
398
|
+
cancel.define_method("reset", method!(CancelToken::reset, 0))?;
|
|
399
|
+
cancel.define_method("cancelled?", method!(CancelToken::is_cancelled, 0))?;
|
|
400
|
+
|
|
401
|
+
let extractor = module.define_class("Extractor", ruby.class_object())?;
|
|
402
|
+
extractor.define_singleton_method("new", function!(Extractor::new, 1))?;
|
|
403
|
+
extractor.define_method("extract", method!(Extractor::extract, -1))?;
|
|
404
|
+
extractor.define_method("extract_two_stage", method!(Extractor::extract_two_stage, -1))?;
|
|
405
|
+
|
|
406
|
+
let trainer = module.define_class("Trainer", ruby.class_object())?;
|
|
407
|
+
trainer.define_singleton_method("new", function!(Trainer::new, 3))?;
|
|
408
|
+
trainer.define_method("load_model", method!(Trainer::load_model, 1))?;
|
|
409
|
+
trainer.define_method("train", method!(Trainer::train, -1))?;
|
|
410
|
+
|
|
411
|
+
let perceptron = module.define_class("PerceptronTrainer", ruby.class_object())?;
|
|
412
|
+
perceptron.define_singleton_method("new", function!(PerceptronTrainer::new, 2))?;
|
|
413
|
+
perceptron.define_method("train", method!(PerceptronTrainer::train, -1))?;
|
|
414
|
+
|
|
415
|
+
let two_stage = module.define_class("TwoStageTrainer", ruby.class_object())?;
|
|
416
|
+
two_stage.define_singleton_method("new", function!(TwoStageTrainer::new, -1))?;
|
|
417
|
+
two_stage.define_method("available?", method!(TwoStageTrainer::is_available, 0))?;
|
|
418
|
+
two_stage.define_method("train", method!(TwoStageTrainer::train, -1))?;
|
|
419
|
+
|
|
420
|
+
Ok(())
|
|
421
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: litsea
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.13.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Minoru Osuka
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.9'
|
|
26
|
+
description: 'Ruby binding for Litsea: word segmentation and Universal POS tagging
|
|
27
|
+
for Japanese, Chinese, Korean, and English. Models are not bundled.'
|
|
28
|
+
executables: []
|
|
29
|
+
extensions:
|
|
30
|
+
- extconf.rb
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- Cargo.toml
|
|
34
|
+
- README.md
|
|
35
|
+
- README_ja.md
|
|
36
|
+
- extconf.rb
|
|
37
|
+
- lib/litsea.rb
|
|
38
|
+
- lib/litsea/version.rb
|
|
39
|
+
- src/error.rs
|
|
40
|
+
- src/gvl.rs
|
|
41
|
+
- src/language.rs
|
|
42
|
+
- src/lib.rs
|
|
43
|
+
- src/metrics.rs
|
|
44
|
+
- src/segmenter.rs
|
|
45
|
+
- src/token.rs
|
|
46
|
+
- src/trainer.rs
|
|
47
|
+
homepage: https://github.com/mosuka/litsea
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
homepage_uri: https://github.com/mosuka/litsea
|
|
52
|
+
source_code_uri: https://github.com/mosuka/litsea
|
|
53
|
+
bug_tracker_uri: https://github.com/mosuka/litsea/issues
|
|
54
|
+
rubygems_mfa_required: 'true'
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '3.1'
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubygems_version: 3.6.9
|
|
70
|
+
specification_version: 4
|
|
71
|
+
summary: Ruby binding for Litsea, a compact word segmentation and POS tagging library
|
|
72
|
+
test_files: []
|