Tutorial (Japanese Named Entity Recognition)
Train a Japanese NER model for KWDLC
This tutorial provides an example of training a Japanese NER model by using Kyoto University Web Document Leads Corpus(KWDLC).
Download the dataset
KWDLC is distributed on GitHub (https://github.com/ku-nlp/KWDLC). Clone the repository to the working directory. This tutorial uses version 1.1.1 of KWDLC.
git clone --depth 1 --branch v1.1.1 https://github.com/ku-nlp/KWDLC
Install python libraries
Before we get started, please run the following command to install the libraries used in this tutorial.
pip install nagisa
pip install seqeval
Preprocess the dataset
First, convert the downloaded data to the input data format for nagisa. The input data format of the train/dev/test files is the tsv format. The Each line is word and tag and one line is represented by word \t tag. Note that you put EOS between sentences.
In the KNP format files of KWDLC (KWDLC/knp/), a named entity is annotated
with a tag such as <NE:ORGANIZATION:京都大学> on a basic phrase line (a line starting with +).
The code below converts these annotations into word-level tags in the IOB2 format (B-TYPE, I-TYPE and O).
It also splits the corpus into the train/dev/test sets by using the official split of the document IDs (KWDLC/id/split_for_pas/).
After running the code, kwdlc.train, kwdlc.dev and kwdlc.test are output to the working directory.
python tutorial_preprocess_kwdlc.py
1import os
2import re
3
4
5# e.g., <NE:ORGANIZATION:京都大学>
6NE_PATTERN = re.compile(r"<NE:([A-Z]+):([^>]+)>")
7
8
9def find_ne_span(words, ne, phrase_start, phrase_end):
10 # An NE tag is attached to the basic phrase that contains the last word of the NE,
11 # so search for the words that end in the basic phrase and match the NE string.
12 for end in range(phrase_start + 1, phrase_end + 1):
13 for start in range(end):
14 if "".join(words[start:end]) == ne:
15 return start, end
16 return None
17
18
19def convert_to_iob2_tags(words, phrase_starts, nes):
20 tags = ["O"] * len(words)
21 phrase_ends = phrase_starts[1:] + [len(words)]
22
23 for netype, ne, phrase_id in nes:
24 span = find_ne_span(words, ne, phrase_starts[phrase_id], phrase_ends[phrase_id])
25
26 # Skip an NE that is a part of a word (e.g., 英 in 英語).
27 if span is None:
28 continue
29
30 start, end = span
31 tags[start] = "B-"+netype
32 for i in range(start + 1, end):
33 tags[i] = "I-"+netype
34
35 return tags
36
37
38def load_kwdlc(filename):
39 data = []
40
41 words = []
42 phrase_starts = []
43 nes = []
44
45 with open(filename, "r", encoding="utf-8") as f:
46 for line in f:
47 line = line.rstrip("\n")
48
49 if line.startswith("+ "):
50 # A basic phrase line may have an NE tag.
51 phrase_starts.append(len(words))
52 match = NE_PATTERN.search(line)
53 if match:
54 netype, ne = match.groups()
55 nes.append([netype, ne, len(phrase_starts) - 1])
56
57 elif line.startswith("# ") or line.startswith("* "):
58 continue
59
60 elif line == "EOS":
61 tags = convert_to_iob2_tags(words, phrase_starts, nes)
62 data.append([words, tags])
63
64 # reset
65 words = []
66 phrase_starts = []
67 nes = []
68
69 else:
70 # The first field of a morpheme line is the surface form.
71 surface = line.split(" ")[0]
72 words.append(surface)
73
74 return data
75
76
77def write_file(filename, data):
78 with open(filename, "w", encoding="utf-8") as f:
79 for words, tags in data:
80 for word, tag in zip(words, tags):
81 f.write("\t".join([word, tag])+"\n")
82 f.write("EOS\n")
83
84
85def main():
86 kwdlc_dir = "./KWDLC"
87
88 for split in ["train", "dev", "test"]:
89 # Use the official train/dev/test split of the documents.
90 fn_id = os.path.join(kwdlc_dir, "id", "split_for_pas", split+".id")
91 with open(fn_id, "r", encoding="utf-8") as f:
92 doc_ids = [line.strip() for line in f if line.strip()]
93
94 data = []
95 for doc_id in doc_ids:
96 # e.g., ./KWDLC/knp/w201106-00000/w201106-0000060050.knp
97 fn_knp = os.path.join(kwdlc_dir, "knp", doc_id[:13], doc_id+".knp")
98 data += load_kwdlc(fn_knp)
99
100 fn_out = "kwdlc."+split
101 write_file(fn_out, data)
102
103
104if __name__ == "__main__":
105 main()
This is an example of the converted data.
フランコ B-PERSON
・ I-PERSON
モスキーノ I-PERSON
は O
1950 B-DATE
年 I-DATE
イタリア B-LOCATION
に O
生まれる O
。 O
EOS
Train a model
Next, you train a NER model by using the nagisa.fit() function.
After finish training the model, save the three model files (kwdlc_ner_model.vocabs, kwdlc_ner_model.params, kwdlc_ner_model.hp) in the current directory.
python tutorial_train_kwdlc.py
1import nagisa
2
3
4def main():
5 fn_in_train = "kwdlc.train"
6 fn_in_dev = "kwdlc.dev"
7 fn_in_test = "kwdlc.test"
8
9 # start training
10 fn_out_model = "kwdlc_ner_model"
11 nagisa.fit(
12 train_file=fn_in_train,
13 dev_file=fn_in_dev,
14 test_file=fn_in_test,
15 model_name=fn_out_model
16 )
17
18
19if __name__ == "__main__":
20 main()
This is a log of the training process.
[nagisa] LAYERS: 1
[nagisa] THRESHOLD: 2
[nagisa] DECAY: 1
[nagisa] EPOCH: 10
[nagisa] WINDOW_SIZE: 3
[nagisa] DIM_UNI: 32
[nagisa] DIM_BI: 16
[nagisa] DIM_WORD: 16
[nagisa] DIM_CTYPE: 8
[nagisa] DIM_TAGEMB: 16
[nagisa] DIM_HIDDEN: 100
[nagisa] LEARNING_RATE: 0.1
[nagisa] DROPOUT_RATE: 0.3
[nagisa] SEED: 1234
[nagisa] TRAINSET: kwdlc.train
[nagisa] TESTSET: kwdlc.test
[nagisa] DEVSET: kwdlc.dev
[nagisa] DICTIONARY: None
[nagisa] EMBEDDING: None
[nagisa] HYPERPARAMS: kwdlc_ner_model.hp
[nagisa] MODEL: kwdlc_ner_model.params
[nagisa] VOCAB: kwdlc_ner_model.vocabs
[nagisa] EPOCH_MODEL: kwdlc_ner_model_epoch.params
[nagisa] NUM_TRAIN: 12271
[nagisa] NUM_TEST: 2195
[nagisa] NUM_DEV: 1585
[nagisa] VOCAB_SIZE_UNI: 2295
[nagisa] VOCAB_SIZE_BI: 26344
[nagisa] VOCAB_SIZE_WORD: 9748
[nagisa] VOCAB_SIZE_POSTAG: 20
Epoch LR Loss Time_m DevWS_f1 DevPOS_f1 TestWS_f1 TestPOS_f1
1 0.100 6.536 1.596 93.42 88.54 95.56 92.06
2 0.100 3.711 1.610 94.39 89.64 96.08 92.70
3 0.100 2.912 1.615 94.62 90.26 96.46 93.52
4 0.100 2.431 1.676 94.79 90.80 96.47 93.66
5 0.100 2.081 1.692 94.83 90.77 96.52 93.82
6 0.100 1.768 1.672 95.10 91.14 96.51 93.68
7 0.100 1.589 1.892 95.18 91.31 96.58 93.90
8 0.050 1.399 1.758 95.00 90.95 96.58 93.90
9 0.050 1.014 1.869 95.45 91.41 96.70 93.98
10 0.025 0.865 1.591 95.26 91.22 96.70 93.98
Predict
You can build the tagger only by loading the three trained model files (kwdlc_ner_model.vocabs, kwdlc_ner_model.params, kwdlc_ner_model.hp) to set arguments in nagisa.Tagger().
python tutorial_predict_kwdlc.py
1import nagisa
2
3
4def main():
5 # build the tagger for kwdlc
6 ner_tagger = nagisa.Tagger(vocabs='kwdlc_ner_model.vocabs',
7 params='kwdlc_ner_model.params',
8 hp='kwdlc_ner_model.hp')
9
10 # predict
11 text = "鈴木花子さんは2020年に京都大学を卒業しました。"
12 tokens = ner_tagger.tagging(text)
13 print(tokens)
14 #> 鈴木/B-PERSON 花子/I-PERSON さん/O は/O 2020/B-DATE 年/I-DATE に/O 京都/B-ORGANIZATION 大学/I-ORGANIZATION を/O 卒業/O し/O ました/O 。/O
15
16
17if __name__ == "__main__":
18 main()
Error analysis
By checking tag-level accuracy/entity-level micro-f1/classification_report, you can see what the model is wrong with.
python tutorial_error_analysis_kwdlc.py
1import nagisa
2
3from seqeval.metrics import f1_score
4from seqeval.metrics import accuracy_score
5from seqeval.metrics import classification_report
6
7
8def main():
9 # load the testset
10 test_X, test_Y = nagisa.utils.load_file("kwdlc.test")
11
12 # build the tagger for kwdlc
13 ner_tagger = nagisa.Tagger(vocabs='kwdlc_ner_model.vocabs',
14 params='kwdlc_ner_model.params',
15 hp='kwdlc_ner_model.hp')
16
17 # predict
18 true_Y = []
19 pred_Y = []
20 for words, true_y in zip(test_X, test_Y):
21 pred_y= ner_tagger.decode(words)
22
23 _pred_y = []
24 _true_y = []
25 for word, pred, true in zip(words, pred_y, true_y):
26 _pred_y.append(pred)
27 _true_y.append(true)
28 true_Y.append(_true_y)
29 pred_Y.append(_pred_y)
30
31 # evaluate
32 accuracy = accuracy_score(true_Y, pred_Y)
33 print("accuracy: {}".format(accuracy))
34 f1 = f1_score(true_Y, pred_Y)
35 print("micro-f1: {}".format(f1))
36 report = classification_report(true_Y, pred_Y)
37 print(report)
38
39
40if __name__ == "__main__":
41 main()
42
This is the result of the error analysis on the test set.
accuracy: 0.9671861495999331
micro-f1: 0.5798764342453663
precision recall f1-score support
ARTIFACT 0.24 0.27 0.26 114
DATE 0.79 0.79 0.79 245
LOCATION 0.69 0.69 0.69 399
MONEY 0.90 0.90 0.90 20
OPTIONAL 0.25 0.09 0.13 35
ORGANIZATION 0.33 0.40 0.36 168
PERCENT 0.75 0.75 0.75 8
PERSON 0.50 0.53 0.51 118
TIME 0.25 0.12 0.17 16
micro avg 0.57 0.59 0.58 1123
macro avg 0.52 0.50 0.51 1123
weighted avg 0.58 0.59 0.58 1123