Skip to content

Commit bbda75c

Browse files
authored
Merge pull request #198 from megagonlabs/feature/refactor_cli
Feature/refactor cli
2 parents ad46e8b + e043888 commit bbda75c

8 files changed

+882
-372
lines changed

docs/command_line_tool.md

+46-3
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ $ ginza
2323

2424
`ginzame`コマンドでオープンソース形態素解析エンジン [MeCab](https://taku910.github.io/mecab/)`mecab`コマンドに近い形式で解析結果を出力することができます。
2525
`ginzame`コマンドは形態素解析処理のみをマルチプロセスで高速に実行します。
26-
このコマンドと`mecab`の出力形式の相違点として、
27-
最終フィールド(発音)が常に`*`となることに注意して下さい
26+
このコマンドと`mecab`の出力形式の相違点として、最終フィールド(発音)が常に`*`となること、
27+
ginza の split_mode はデフォルトが `C` なので unidic 相当の単語分割を得るためには `-s A` を指定する必要があることに注意して下さい
2828
```console
2929
$ ginzame
3030
銀座でランチをご一緒しましょう。
@@ -41,6 +41,49 @@ EOS
4141

4242
```
4343

44+
## OPTIONS
45+
`ginza`コマンドでは以下のオプションを指定することができます。
46+
`ginzame`コマンドでは `--split-mode` `--hash-comment` `output-path` `--use-normalized-form` `--parallel` オプションが利用可能です。
47+
48+
- `--model-path <string>`, `-b <string>`
49+
`spacy.language.Language` 形式の学習済みモデルが保存されたディレクトリを指定します。
50+
`--ensure-model` オプションと同時に指定することはできません。
51+
- `--ensure-model <string>`, `-m <string>`
52+
ginza が公開している学習済みモデル名を指定します。`--model-path` オプションと同時に指定することはできません。次の値のいずれかを指定できます。
53+
- `ja_ginza`
54+
- `ja_ginza_electra`
55+
使用するモデルに応じて、事前に `pip install ja-ginza-electra` のようにパッケージをダウンロードする必要があります。
56+
`--model-path`, `--ensure-model` のどちらも指定されない場合には `ja_ginza_electra``ja_ginza` の順の優先度でロード可能なモデルを利用します。
57+
- `--split-mode <string>`, `-s <string>`
58+
複合名詞の分割モードを指定します。モードは [sudachi](https://github.com/WorksApplications/Sudachi#the-modes-of-splitting) に準拠し、`A``B``C`のいずれかを指定できます。デフォルト値は `C` です。
59+
`A`が分割が最も短く複合名詞が UniDic 短単位まで分割され、 `C` では固有名詞が抽出されます。`B` は二つの中間の単位に分割されます。
60+
- `--hash-comment <string>`, `-c <string>`
61+
行頭が `#` から始まる行を解析対象とするかのモードを指定します。次の値のいずれかを指定できます。
62+
- `print`
63+
解析対象とはしないが、解析結果には入力をそのまま出力します。
64+
- `skip`
65+
解析対象とせず、解析結果にも出力しません。
66+
- `analyze`
67+
`#` から始まる行についても解析を行い、結果を出力します。
68+
デフォルト値は `print` です。
69+
- `--output-path <string>`, `-o <string>`
70+
解析結果を出力するファイルのパスを指定します。指定しない場合には標準出力に解析結果が出力されます。
71+
- `--output-format <string>`, `-f <string>`
72+
[解析結果のフォーマット](#出力形式の指定)を指定します。次の値のいずれかを指定できます。
73+
- `0`, `conllu`
74+
- `1`, `cabocha`
75+
- `2`, `mecab`
76+
- `3`, `json`
77+
デフォルト値は `conllu` です。
78+
- `--require-gpu`, `-g`
79+
gpu を有効にするブールスイッチ。gpu_id=0 の gpu が利用されます。
80+
- `--use-normalized-form`, `-n`
81+
[sudachi](https://github.com/WorksApplications/Sudachi#normalized-form) による lemma の標準化を有効にするブールスイッチ。
82+
- `--disable-sentencizer`, `-d`
83+
`ja_ginza``ja_ginza_electra` モデル利用時に[disable_sentencizer](https://github.com/megagonlabs/ginza/blob/develop/ginza/disable_sentencizer.py)を有効化するブールスイッチ。
84+
- `--parallel <int>`, `-p <int>`
85+
並列実行するプロセス数を指定します。0 を指定すると cpu コア数分のプロセスを起動します。デフォルト値は1です。
86+
4487
## 出力形式の指定
4588

4689
### JSON
@@ -79,7 +122,7 @@ $ ginza -f json
79122

80123
日本語係り受け解析器 [CaboCha](https://taku910.github.io/cabocha/)`cabocha -f1`のラティス形式に近い解析結果を出力する場合は
81124
`ginza -f 1` または `ginza -f cabocha` を実行して下さい。
82-
このオプションと`cabocha -f1`の出力形式の相違点として、
125+
このオプションと`cabocha -f1`の出力形式の相違点として、
83126
スラッシュ記号`/`に続く`func_index`フィールドが常に自立語の終了位置(機能語があればその開始位置に一致)を示すこと、
84127
機能語認定基準が一部異なること、
85128
に注意して下さい。

ginza/analyzer.py

+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# coding: utf8
2+
import json
3+
import sys
4+
from typing import Iterable, Iterator, Optional, Tuple
5+
6+
import spacy
7+
from spacy.tokens import Span
8+
from spacy.language import Language
9+
from spacy.lang.ja import Japanese, JapaneseTokenizer
10+
11+
from . import set_split_mode, inflection, reading_form, ent_label_ene, ent_label_ontonotes, bunsetu_bi_label, bunsetu_position_type
12+
from .bunsetu_recognizer import bunsetu_available, bunsetu_head_list, bunsetu_phrase_span
13+
14+
15+
class Analyzer:
16+
def __init__(
17+
self,
18+
model_path: str,
19+
ensure_model: str,
20+
split_mode: str,
21+
hash_comment: str,
22+
output_format: str,
23+
require_gpu: bool,
24+
disable_sentencizer: bool,
25+
) -> None:
26+
self.model_path = model_path
27+
self.ensure_model = ensure_model
28+
self.split_mode = split_mode
29+
self.hash_comment = hash_comment
30+
self.output_format = output_format
31+
self.require_gpu = require_gpu
32+
self.disable_sentencizer = disable_sentencizer
33+
self.nlp: Optional[Language] = None
34+
35+
def set_nlp(self) -> None:
36+
if self.nlp:
37+
return
38+
39+
if self.require_gpu:
40+
spacy.require_gpu()
41+
42+
if self.output_format in ["2", "mecab"]:
43+
nlp = JapaneseTokenizer(nlp=Japanese(), split_mode=self.split_mode).tokenizer
44+
else:
45+
# Work-around for pickle error. Need to share model data.
46+
if self.model_path:
47+
nlp = spacy.load(self.model_path)
48+
elif self.ensure_model:
49+
nlp = spacy.load(self.ensure_model.replace("-", "_"))
50+
else:
51+
try:
52+
nlp = spacy.load("ja_ginza_electra")
53+
except IOError as e:
54+
try:
55+
nlp = spacy.load("ja_ginza")
56+
except IOError as e:
57+
print(
58+
'Could not find the model. You need to install "ja-ginza-electra" or "ja-ginza" by executing pip like `pip install ja-ginza-electra`.',
59+
file=sys.stderr,
60+
)
61+
raise e
62+
63+
if self.disable_sentencizer:
64+
nlp.add_pipe("disable_sentencizer", before="parser")
65+
66+
if self.split_mode:
67+
set_split_mode(nlp, self.split_mode)
68+
69+
self.nlp = nlp
70+
71+
def analyze_lines_mp(self, lines: Iterable[str]) -> Tuple[Iterable[Iterable[str]]]:
72+
self.set_nlp()
73+
return tuple(list(map(list, self.analyze_line(line))) for line in lines) # to avoid generator serialization inside of results of analyze_line
74+
75+
def analyze_line(self, line: str) -> Iterable[Iterable[str]]:
76+
return analyze(self.nlp, self.hash_comment, self.output_format, line)
77+
78+
79+
def analyze(
80+
nlp: Language, hash_comment: str, output_format: str, line: str
81+
) -> Iterable[Iterable[str]]:
82+
line = line.rstrip("\n")
83+
if line.startswith("#"):
84+
if hash_comment == "print":
85+
return ((line,),)
86+
elif hash_comment == "skip":
87+
return ((),)
88+
if line == "":
89+
return (("",),)
90+
if output_format in ["0", "conllu"]:
91+
doc = nlp(line)
92+
return [analyze_conllu(sent) for sent in doc.sents]
93+
elif output_format in ["1", "cabocha"]:
94+
doc = nlp(line)
95+
return [analyze_cabocha(sent) for sent in doc.sents]
96+
elif output_format in ["2", "mecab"]:
97+
doc = nlp.tokenize(line)
98+
return [analyze_mecab(doc)]
99+
elif output_format in ["3", "json"]:
100+
doc = nlp(line)
101+
return [analyze_json(sent) for sent in doc.sents]
102+
else:
103+
raise Exception(output_format + " is not supported")
104+
105+
106+
def analyze_json(sent: Span) -> Iterator[str]:
107+
tokens = []
108+
for token in sent:
109+
t = {
110+
"id": token.i - sent.start + 1,
111+
"orth": token.orth_,
112+
"tag": token.tag_,
113+
"pos": token.pos_,
114+
"lemma": token.lemma_,
115+
"head": token.head.i - token.i,
116+
"dep": token.dep_,
117+
"ner": "{}-{}".format(token.ent_iob_, token.ent_type_) if token.ent_type_ else token.ent_iob_,
118+
}
119+
if token.whitespace_:
120+
t["whitespace"] = token.whitespace_
121+
tokens.append(" " + json.dumps(t, ensure_ascii=False))
122+
tokens = ",\n".join(tokens)
123+
124+
yield """ {{
125+
"paragraphs": [
126+
{{
127+
"raw": "{}",
128+
"sentences": [
129+
{{
130+
"tokens": [
131+
{}
132+
]
133+
}}
134+
]
135+
}}
136+
]
137+
}}""".format(
138+
sent.text,
139+
tokens,
140+
)
141+
142+
143+
def analyze_conllu(sent: Span, print_origin=True) -> Iterator[str]:
144+
if print_origin:
145+
yield "# text = {}".format(sent.text)
146+
np_labels = [""] * len(sent)
147+
use_bunsetu = bunsetu_available(sent)
148+
if use_bunsetu:
149+
for head_i in bunsetu_head_list(sent):
150+
bunsetu_head_token = sent[head_i]
151+
phrase = bunsetu_phrase_span(bunsetu_head_token)
152+
if phrase.label_ == "NP":
153+
for idx in range(phrase.start - sent.start, phrase.end - sent.start):
154+
np_labels[idx] = "NP_B" if idx == phrase.start else "NP_I"
155+
for token, np_label in zip(sent, np_labels):
156+
yield conllu_token_line(sent, token, np_label, use_bunsetu)
157+
yield ""
158+
159+
160+
def conllu_token_line(sent, token, np_label, use_bunsetu) -> str:
161+
bunsetu_bi = bunsetu_bi_label(token) if use_bunsetu else None
162+
position_type = bunsetu_position_type(token) if use_bunsetu else None
163+
inf = inflection(token)
164+
reading = reading_form(token)
165+
ne = ent_label_ontonotes(token)
166+
ene = ent_label_ene(token)
167+
misc = "|".join(
168+
filter(
169+
lambda s: s,
170+
(
171+
"SpaceAfter=Yes" if token.whitespace_ else "SpaceAfter=No",
172+
"" if not bunsetu_bi else "BunsetuBILabel={}".format(bunsetu_bi),
173+
"" if not position_type else "BunsetuPositionType={}".format(position_type),
174+
np_label,
175+
"" if not inf else "Inf={}".format(inf),
176+
"" if not reading else "Reading={}".format(reading.replace("|", "\\|").replace("\\", "\\\\")),
177+
"" if not ne or ne == "O" else "NE={}".format(ne),
178+
"" if not ene or ene == "O" else "ENE={}".format(ene),
179+
)
180+
)
181+
)
182+
183+
return "\t".join(
184+
[
185+
str(token.i - sent.start + 1),
186+
token.orth_,
187+
token.lemma_,
188+
token.pos_,
189+
token.tag_.replace(",*", "").replace(",", "-"),
190+
"NumType=Card" if token.pos_ == "NUM" else "_",
191+
"0" if token.head.i == token.i else str(token.head.i - sent.start + 1),
192+
token.dep_.lower() if token.dep_ else "_",
193+
"_",
194+
misc if misc else "_",
195+
]
196+
)
197+
198+
199+
def analyze_cabocha(sent: Span) -> Iterable[str]:
200+
bunsetu_index_list = {}
201+
bunsetu_index = -1
202+
for token in sent:
203+
if bunsetu_bi_label(token) == "B":
204+
bunsetu_index += 1
205+
bunsetu_index_list[token.i] = bunsetu_index
206+
207+
lines = []
208+
for token in sent:
209+
if bunsetu_bi_label(token) == "B":
210+
lines.append(cabocha_bunsetu_line(sent, bunsetu_index_list, token))
211+
lines.append(cabocha_token_line(token))
212+
lines.append("EOS")
213+
lines.append("")
214+
return lines
215+
216+
217+
def cabocha_bunsetu_line(sent: Span, bunsetu_index_list, token) -> str:
218+
bunsetu_head_index = None
219+
bunsetu_dep_index = None
220+
bunsetu_func_index = None
221+
dep_type = "D"
222+
for t in token.doc[token.i : sent.end]:
223+
if bunsetu_index_list[t.i] != bunsetu_index_list[token.i]:
224+
if bunsetu_func_index is None:
225+
bunsetu_func_index = t.i - token.i
226+
break
227+
tbi = bunsetu_index_list[t.head.i]
228+
if bunsetu_index_list[t.i] != tbi:
229+
bunsetu_head_index = t.i - token.i
230+
bunsetu_dep_index = tbi
231+
if bunsetu_func_index is None and bunsetu_position_type(t) in {"FUNC", "SYN_HEAD"}:
232+
bunsetu_func_index = t.i - token.i
233+
else:
234+
if bunsetu_func_index is None:
235+
bunsetu_func_index = len(sent) - token.i
236+
if bunsetu_head_index is None:
237+
bunsetu_head_index = 0
238+
if bunsetu_dep_index is None:
239+
bunsetu_dep_index = -1
240+
return "* {} {}{} {}/{} 0.000000".format(
241+
bunsetu_index_list[token.i],
242+
bunsetu_dep_index,
243+
dep_type,
244+
bunsetu_head_index,
245+
bunsetu_func_index,
246+
)
247+
248+
249+
def cabocha_token_line(token) -> str:
250+
part_of_speech = token.tag_.replace("-", ",")
251+
part_of_speech += ",*" * (3 - part_of_speech.count(",")) + "," + inflection(token)
252+
reading = reading_form(token)
253+
return "{}\t{},{},{},{}\t{}".format(
254+
token.orth_,
255+
part_of_speech,
256+
token.lemma_,
257+
reading if reading else token.orth_,
258+
"*",
259+
"O" if token.ent_iob_ == "O" else "{}-{}".format(token.ent_iob_, token.ent_type_),
260+
)
261+
262+
263+
def analyze_mecab(sudachipy_tokens) -> Iterable[str]:
264+
return tuple(mecab_token_line(t) for t in sudachipy_tokens) + ("EOS", "")
265+
266+
267+
def mecab_token_line(token) -> str:
268+
reading = token.reading_form()
269+
return "{}\t{},{},{},{}".format(
270+
token.surface(),
271+
",".join(token.part_of_speech()),
272+
token.normalized_form(),
273+
reading if reading else token.surface(),
274+
"*",
275+
)

0 commit comments

Comments
 (0)