diff --git a/README-ja.md b/README-ja.md index 974b6ad..1e8b86d 100644 --- a/README-ja.md +++ b/README-ja.md @@ -21,7 +21,7 @@ Vaporetto はトークン化モデルを生成するための方法を3つ用意 #### 配布モデルをダウンロードする -1番目は最も単純な方法で、我々によって学習されたモデルをダウンロードすることです。 +1つ目は最も単純な方法で、学習済みモデルをダウンロードすることです。 モデルファイルは[ここ](https://github.com/daac-tools/vaporetto/releases)にあります。 `bccwj-suw+unidic+tag` を選びました。 @@ -29,24 +29,24 @@ Vaporetto はトークン化モデルを生成するための方法を3つ用意 % wget https://github.com/daac-tools/vaporetto/releases/download/v0.5.0/bccwj-suw+unidic+tag.tar.xz ``` -各ファイルにはモデルファイルとライセンス条項が含まれているので、以下のようなコマンドでダウンロードしたファイルを展開する必要があります。 +各ファイルはモデルファイルとライセンス条項が含まれた圧縮ファイルなので、ダウンロードしたファイルを展開する必要があります。 ``` % tar xf ./bccwj-suw+unidic+tag.tar.xz ``` -トークン化を行うには、以下のコマンドを実行します。 +トークン化には、以下のコマンドを実行します。 ``` % echo 'ヴェネツィアはイタリアにあります。' | cargo run --release -p predict -- --model path/to/bccwj-suw+unidic+tag.model.zst ``` -以下が出力されるでしょう。 +以下が出力されます。 ``` ヴェネツィア は イタリア に あり ます 。 ``` #### KyTea のモデルを変換する -2番目の方法も単純で、 KyTea で学習されたモデルを変換することです。 +2つ目の方法も単純で、 KyTea で学習されたモデルを変換することです。 まずはじめに、好きなモデルを [KyTea Models](http://www.phontron.com/kytea/model.html) ページからダウンロードします。 `jp-0.4.7-5.mod.gz` を選びました。 @@ -54,7 +54,7 @@ Vaporetto はトークン化モデルを生成するための方法を3つ用意 % wget http://www.phontron.com/kytea/download/model/jp-0.4.7-5.mod.gz ``` -各モデルは圧縮されているので、以下のようなコマンドでダウンロードしたモデルを展開する必要があります。 +各モデルは圧縮されているので、ダウンロードしたモデルを展開する必要があります。 ``` % gunzip ./jp-0.4.7-5.mod.gz ``` @@ -64,21 +64,21 @@ KyTea のモデルを Vaporetto のモデルに変換するには、 Vaporetto % cargo run --release -p convert_kytea_model -- --model-in path/to/jp-0.4.7-5.mod --model-out path/to/jp-0.4.7-5-tokenize.model.zst ``` -これでトークン化を行えます。以下のコマンドを実行します。 +これでトークン化できます。以下のコマンドを実行します。 ``` % echo 'ヴェネツィアはイタリアにあります。' | cargo run --release -p predict -- --model path/to/jp-0.4.7-5-tokenize.model.zst ``` -以下が出力されるでしょう。 +以下が出力されます。 ``` ヴェネツィア は イタリア に あ り ま す 。 ``` #### 自分のモデルを学習する -3番目は主に研究者向けで、自分で学習コーパスを用意し、自分でトークン化モデルを学習することです。 +3つ目は主に研究者向けで、自分で学習コーパスを用意し、モデルを学習することです。 -Vaporetto は2種類のコーパス、すなわちフルアノテーションコーパスと部分アノテーションコーパスから学習することが可能です。 +Vaporetto は2種類のコーパス(フルアノテーションコーパスと部分アノテーションコーパス)から学習することが可能です。 フルアノテーションコーパスは、すべての文字境界に対してトークン境界であるかトークンの内部であるかがアノテーションされたコーパスです。 このデータは、以下に示すようにトークン境界に空白が挿入された形式です。 @@ -88,7 +88,7 @@ Vaporetto は2種類のコーパス、すなわちフルアノテーションコ 火星 猫 の 生態 の 調査 結果 ``` -一方、部分アノテーションコーパスは一部の文字境界のみに対してアノテーションされたコーパスです。 +部分アノテーションコーパスは、一部の文字境界のみに対してアノテーションされたコーパスです。 各文字境界には `|` (トークン境界)、 `-` (非トークン境界)、 ` ` (不明) のいずれかの形式でアノテーションされます。 ここに例を示します。 @@ -104,7 +104,13 @@ Vaporetto は2種類のコーパス、すなわちフルアノテーションコ `--tok` 引数ではフルアノテーションコーパスを指定し、 `--part` 引数では部分アノテーションコーパスを指定します。 `--dict` 引数によって単語辞書を指定することもできます。 -単語辞書は、1行1単語のファイルです。 +単語辞書は、1行1単語のファイルであり、必要に応じてタグを付与することもできます。 +``` +トスカーナ +パンツァーノ +灯里/名詞-固有名詞-人名-名/アカリ +形態/名詞-普通名詞-一般/ケータイ +``` 学習器は空行の入力を受け付けません。 このため、学習の前にコーパスから空行を削除してください。 diff --git a/README.md b/README.md index 864b78a..1a7c5ed 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,14 @@ To train a model, use the following command: The `--tok` argument specifies a fully annotated corpus, and the `--part` argument specifies a partially annotated corpus. You can also specify a word dictionary with the `--dict` argument. -A word dictionary is a file with words per line. +A word dictionary is a file that lists words line by line and can be tagged as needed: + +``` +トスカーナ +パンツァーノ +灯里/名詞-固有名詞-人名-名/アカリ +形態/名詞-普通名詞-一般/ケータイ +``` The trainer does not accept empty lines. Therefore, remove all empty lines from the corpus before training. diff --git a/examples/wasm/src/lib.rs b/examples/wasm/src/lib.rs index db54aa9..c1b856d 100644 --- a/examples/wasm/src/lib.rs +++ b/examples/wasm/src/lib.rs @@ -12,7 +12,7 @@ use yew::{html, Component, Context, Html}; use once_cell::sync::Lazy; use vaporetto::{CharacterType, Model, Predictor, Sentence}; use vaporetto_rules::{ - sentence_filters::{ConcatGraphemeClustersFilter, KyteaWsConstFilter, PatternMatchTagger}, + sentence_filters::{ConcatGraphemeClustersFilter, KyteaWsConstFilter}, string_filters::KyteaFullwidthFilter, SentenceFilter, StringFilter, }; @@ -20,15 +20,13 @@ use vaporetto_rules::{ use crate::text_input::TextInput; use crate::token_view::TokenView; -static PREDICTOR: Lazy<(Predictor, PatternMatchTagger)> = Lazy::new(|| { +static PREDICTOR: Lazy = Lazy::new(|| { let mut f = Cursor::new(include_bytes!("bccwj-suw+unidic+tag-huge.model.zst")); let mut decoder = ruzstd::StreamingDecoder::new(&mut f).unwrap(); let mut buff = vec![]; decoder.read_to_end(&mut buff).unwrap(); - let (model, rest) = Model::read_slice(&buff).unwrap(); - let config = bincode::config::standard(); - let word_tag_map: Vec<(String, Vec>)> = bincode::decode_from_slice(rest, config).unwrap().0; - (Predictor::new(model, true).unwrap(), PatternMatchTagger::new(word_tag_map.into_iter().collect())) + let (model, _) = Model::read_slice(&buff).unwrap(); + Predictor::new(model, true).unwrap() }); pub enum Message { @@ -84,7 +82,7 @@ impl gloo_worker::Worker for Worker { let filtered_text = pre_filter.filter(sentence_orig.as_raw_text()); sentence_filtered.update_raw(filtered_text).unwrap(); - PREDICTOR.0.predict(sentence_filtered); + PREDICTOR.predict(sentence_filtered); let wsconst_g = ConcatGraphemeClustersFilter; let wsconst_d = KyteaWsConstFilter::new(CharacterType::Digit); @@ -92,7 +90,6 @@ impl gloo_worker::Worker for Worker { wsconst_d.filter(sentence_filtered); sentence_filtered.fill_tags(); - PREDICTOR.1.filter(sentence_filtered); let n_tags = sentence_filtered.n_tags(); sentence_orig diff --git a/predict/src/main.rs b/predict/src/main.rs index 016ba46..8ac5af4 100644 --- a/predict/src/main.rs +++ b/predict/src/main.rs @@ -7,7 +7,7 @@ use std::time::Instant; use clap::Parser; use vaporetto::{CharacterType, Model, Predictor, Sentence}; use vaporetto_rules::{ - sentence_filters::{ConcatGraphemeClustersFilter, KyteaWsConstFilter, PatternMatchTagger}, + sentence_filters::{ConcatGraphemeClustersFilter, KyteaWsConstFilter}, string_filters::KyteaFullwidthFilter, SentenceFilter, StringFilter, }; @@ -92,14 +92,6 @@ fn main() -> Result<(), Box> { let mut f = zstd::Decoder::new(File::open(args.model)?)?; let model = Model::read(&mut f)?; let predictor = Predictor::new(model, args.predict_tags)?; - let word_tag_map: Vec<(String, Vec>)> = if args.predict_tags { - let config = bincode::config::standard(); - bincode::decode_from_std_read(&mut f, config).unwrap_or_else(|_| vec![]) - } else { - vec![] - }; - let pattern_match_tagger = (!word_tag_map.is_empty()) - .then(|| PatternMatchTagger::new(word_tag_map.into_iter().collect())); eprintln!("Start tokenization"); let start = Instant::now(); @@ -119,9 +111,6 @@ fn main() -> Result<(), Box> { post_filters.iter().for_each(|filter| filter.filter(&mut s)); if args.predict_tags { s.fill_tags(); - if let Some(tagger) = pattern_match_tagger.as_ref() { - tagger.filter(&mut s); - } } s.write_tokenized_text(&mut buf); writeln!(out, "{}", buf)?; @@ -142,9 +131,6 @@ fn main() -> Result<(), Box> { post_filters.iter().for_each(|filter| filter.filter(&mut s)); if args.predict_tags { s.fill_tags(); - if let Some(tagger) = pattern_match_tagger.as_ref() { - tagger.filter(&mut s); - } } s_orig.update_raw(line)?; s_orig.reset_tags(s.n_tags()); diff --git a/train/src/main.rs b/train/src/main.rs index 759ee9b..ea68e8c 100644 --- a/train/src/main.rs +++ b/train/src/main.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::fs::File; use std::io::{prelude::*, stderr, BufReader}; use std::path::PathBuf; @@ -125,42 +125,46 @@ fn main() -> Result<(), Box> { eprintln!("# of sentences: {}", train_sents.len()); } - let mut word_tag_map = BTreeMap::new(); - let mut word_buf = Sentence::default(); + let mut tag_dictionary = vec![]; + let mut dictionary = BTreeSet::new(); for path in args.dict { eprintln!("Loading {:?} ...", path); let f = File::open(path)?; let f = BufReader::new(f); - for (i, line) in f.lines().enumerate() { - if i % 100000 == 0 { - eprint!("# of words: {}\r", i); + for line in f.lines() { + if dictionary.len() % 10000 == 0 { + eprint!("# of words: {}\r", dictionary.len()); stderr().flush()?; } - let line = line?; - word_buf.update_tokenized(&line).unwrap(); - for token in word_buf.iter_tokens() { - let word = token.surface().to_string(); - let word = if args.no_norm { - word - } else { - fullwidth_filter.filter(&word) - }; - word_tag_map.entry(word).or_insert_with(|| { - token - .tags() - .iter() - .map(|tag| tag.as_ref().map(|tag| tag.to_string())) - .collect() - }); + let s = Sentence::from_tokenized(&line?)?; + let s = if args.no_norm { + s + } else { + let new_line = fullwidth_filter.filter(s.as_raw_text()); + let mut new_s = Sentence::from_raw(new_line)?; + new_s.boundaries_mut().clone_from_slice(s.boundaries()); + new_s.reset_tags(s.n_tags()); + new_s.tags_mut().clone_from_slice(s.tags()); + new_s + }; + for token in s.iter_tokens() { + dictionary.insert(token.surface().to_string()); } + tag_dictionary.push(s); } - eprintln!("# of words: {}", word_tag_map.len()); + eprintln!("# of words: {}", dictionary.len()); } - let dictionary = word_tag_map.iter().map(|(word, _)| word.clone()).collect(); + let dictionary = dictionary.into_iter().collect(); eprintln!("Extracting into features..."); let mut trainer = Trainer::new( - args.charw, args.charn, args.typew, args.typen, dictionary, args.dictn, + args.charw, + args.charn, + args.typew, + args.typen, + dictionary, + args.dictn, + &tag_dictionary, )?; for (i, s) in train_sents.iter().enumerate() { if i % 10000 == 0 { @@ -177,12 +181,6 @@ fn main() -> Result<(), Box> { let mut f = zstd::Encoder::new(File::create(args.model)?, 19)?; model.write(&mut f)?; - for tag_model in model.tag_models() { - word_tag_map.remove(tag_model.token()); - } - let word_tag_map: Vec<(String, Vec>)> = word_tag_map.into_iter().collect(); - let config = bincode::config::standard(); - bincode::encode_into_std_write(&word_tag_map, &mut f, config)?; f.finish()?; Ok(()) diff --git a/vaporetto/src/tag_trainer.rs b/vaporetto/src/tag_trainer.rs index 9297116..a6ca686 100644 --- a/vaporetto/src/tag_trainer.rs +++ b/vaporetto/src/tag_trainer.rs @@ -46,6 +46,7 @@ pub struct TagTrainer<'a> { char_ngram_size: u8, _type_window_size: u8, type_ngram_size: u8, + default_tags: HashMap<&'a str, &'a [Option>]>, // Uses BTreeMap to improve compression ratio. examples: BTreeMap<&'a str, Vec>>, } @@ -56,12 +57,14 @@ impl<'a> TagTrainer<'a> { char_ngram_size: u8, type_window_size: u8, type_ngram_size: u8, + default_tags: HashMap<&'a str, &'a [Option>]>, ) -> Self { Self { _char_window_size: char_window_size, char_ngram_size, _type_window_size: type_window_size, type_ngram_size, + default_tags, examples: BTreeMap::new(), } } @@ -295,7 +298,18 @@ impl<'a> TagTrainer<'a> { }) } - pub fn train(self, epsilon: f64, cost: f64, solver: SolverType) -> Result> { + pub fn train(mut self, epsilon: f64, cost: f64, solver: SolverType) -> Result> { + for (token, tags) in self.default_tags { + if tags.iter().any(|t| t.is_some()) && !self.examples.contains_key(token) { + self.examples.insert( + token, + vec![TagExample { + tags, + features: vec![], + }], + ); + } + } let mut tag_models = vec![]; liblinear::toggle_liblinear_stdout_output(false); let n_tokens = self.examples.len(); diff --git a/vaporetto/src/trainer.rs b/vaporetto/src/trainer.rs index 6c43ba9..ffdfe77 100644 --- a/vaporetto/src/trainer.rs +++ b/vaporetto/src/trainer.rs @@ -159,7 +159,7 @@ impl<'a> BoundaryFeature<'a> { /// } /// /// let dict: Vec = vec![]; -/// let mut trainer = Trainer::new(3, 3, 3, 3, dict, 0).unwrap(); +/// let mut trainer = Trainer::new(3, 3, 3, 3, dict, 0, &[]).unwrap(); /// for (i, s) in train_sents.iter().enumerate() { /// trainer.add_example(&s); /// } @@ -196,6 +196,8 @@ impl<'a> Trainer<'a> { /// * `dict_words` - A word dictionary. /// * `dict_word_max_len` - Dictionary words longer than this value will be grouped together, /// where the length is in characters. + /// * `tag_dictionary` - A tag dictionary. Words not included in the corpus are annotated + /// with the tag specified here. /// /// # Errors /// @@ -207,6 +209,7 @@ impl<'a> Trainer<'a> { type_ngram_size: u8, dict_words: Vec, dict_word_max_len: u8, + tag_dictionary: &'a [Sentence<'a, '_>], ) -> Result { let dict_pma = if dict_words.is_empty() { None @@ -216,6 +219,14 @@ impl<'a> Trainer<'a> { .map_err(|e| VaporettoError::invalid_argument("dict_words", e.to_string()))?, ) }; + let mut default_tags = HashMap::new(); + for s in tag_dictionary { + for token in s.iter_tokens() { + if !default_tags.contains_key(token.surface()) { + default_tags.insert(token.surface(), token.tags()); + } + } + } Ok(Self { char_window_size, char_ngram_size, @@ -232,6 +243,7 @@ impl<'a> Trainer<'a> { char_ngram_size, type_window_size, type_ngram_size, + default_tags, ), }) } @@ -480,7 +492,7 @@ mod tests { #[test] fn check_features_3322() { let s = Sentence::from_tokenized("これ は テスト です").unwrap(); - let trainer = Trainer::new(3, 3, 2, 2, vec![], 4).unwrap(); + let trainer = Trainer::new(3, 3, 2, 2, vec![], 4, &[]).unwrap(); let mut examples = vec![]; trainer.gen_features(&s, &mut examples); @@ -683,6 +695,7 @@ mod tests { 2, vec!["これ".into(), "これは".into(), "テスト".into()], 4, + &[], ) .unwrap(); let mut examples = vec![];