どうせ和布蕪るなら、続き (コストの自動推測とモデル、ほか)

そこそこめんどいのかと思ってたら全然そうでもなかったので。

ひとつ前のでちょっと誤解してて。

それと「コスト」についてだけれど、原則として「似たものと近い値を探して埋め、トライアンドエラーで頑張って調整する」か、mecab-cost-train でトレーニングするかなんだけれど、後者を採るのであれば、ある程度自動化も出来るのかもしれなくて、これについては、さすがに「おれのためだけ」にすると勿体無いので、やる気になったらここに追記の形で書く可能性はある。

と書いたけれど、mecab-cost-train するのは別に不可欠というわけではなくて、誰かが作ったモデルファイルさえ渡せば自動推測自体はやってくれるのだね。(同じく単語の追加方法に書いてある。mecab-ipadicのモデルファイルが手に入る。)

ついでにいえば「文脈ID」も埋めてくれる機能があった。だから私のスクリプトが頑張る理由が少し薄れてる。まぁこれはいいか。

見出しにした本題の前に。これも誤解起因なんだけれど、私のスクリプトは「ユーザ辞書作成専用」にすべきだった。mecab-dict-index が役割過多というか。「辞書作成以外のこと」がいくつか出来てしまうので、ただのラッパーにしてしまうと非常にわかりにくいものになってしまう。ので、スクリプトの名前も「my-mecab-userdict-build」にしようと思う。

そんなわけで、ひとまずこうなった:

my-mecab-userdict-build.py
  1 # -*- coding: utf-8 -*-
  2 from __future__ import unicode_literals
  3 # TODO: すまん、python 2.7 対応が出来てない。
  4 
  5 import io
  6 import os
  7 import csv
  8 import re
  9 import subprocess
 10 import tempfile
 11 
 12 
 13 class _BaseMeCabDictInputBuilder(object):
 14     fieldnames_all = [
 15         "表層形",
 16         "左文脈ID", "右文脈ID",
 17         "コスト",
 18         "品詞", "品詞細分類1", "品詞細分類2", "品詞細分類3",
 19         "活用型1", "活用形2",
 20         "原形", "読み", "発音"
 21     ]
 22     _defaults_tobe_blank = ("コスト",)
 23 
 24     def __init__(self, args):
 25         self._args = args
 26         self._mecab_official_formatted0 = os.path.join(
 27             tempfile.gettempdir(), "_my_mecab_dict0_.csv")
 28         self._mecab_official_formatted1 = os.path.join(
 29             tempfile.gettempdir(), "_my_mecab_dict1_.csv")
 30         self._mecab_model_reenc = os.path.join(
 31             tempfile.gettempdir(), "_my_mecab_model_.model")
 32         #
 33         csvinput = io.open(
 34             args.input_csv, encoding=args.dictionary_charset).read()
 35         header, _, body = csvinput.partition("\n")
 36         self._fieldnames = re.split(r"\s*,\s*", header)
 37         self._extra_fieldnames = [
 38             fn for fn in self._fieldnames
 39             if fn not in self.fieldnames_all]
 40         self._reader = csv.DictReader(
 41             io.StringIO(body),
 42             fieldnames=self._fieldnames)
 43 
 44     def _default(self, k, line):
 45         # should be overidden
 46         return "*"
 47 
 48     def _fallback(self, k, line):
 49         if k in line:
 50             return line[k]
 51         if k in self._defaults_tobe_blank:
 52             return ""
 53         if k == "読み" and "発音" in line:
 54             return line["発音"]
 55         elif k == "発音" and "読み" in line:
 56             return line["読み"]
 57         return self._default(k, line)
 58 
 59     def _lines(self):
 60         for line in self._reader:
 61             yield [
 62                 line.get(k, self._fallback(k, line))
 63                 for k in (self.fieldnames_all + self._extra_fieldnames)]
 64 
 65     def _write_inputcsv(self, outname):
 66         # TODO: エスケープ必要だったりする?
 67         content = ("\n".join(
 68                 [",".join(line) for line in self._lines()
 69                  ]) + "\n").encode("utf-8")
 70         with io.open(outname, "wb") as fo:
 71             fo.write(content)
 72 
 73     def __enter__(self):
 74         return self
 75 
 76     def __exit__(self, exc, value, tb):
 77         for fn in (
 78             self._mecab_official_formatted0,
 79             self._mecab_official_formatted1,
 80             self._mecab_model_reenc,):
 81             if os.path.exists(fn):
 82                 try:
 83                     os.remove(fn)
 84                 except Exception:
 85                     pass  # no problem.
 86 
 87     def compile(self):
 88         # TODO: MECAB_HOME なんて標準があるのか未確認。
 89         _mecab_home = os.environ.get(
 90             "MECAB_HOME", "c:/Program Files (x86)/MeCab")
 91         _dict_index_bin = os.path.join(
 92             _mecab_home, "bin", "mecab-dict-index")
 93         # TODO: 「ipadic」以外の置き場もあるんではないかと。
 94         _sysdict_dir = os.path.join(_mecab_home, "dic", "ipadic")
 95 
 96         self._write_inputcsv(self._mecab_official_formatted0)
 97         #
 98         dictname = self._args.output_dictionary_name
 99         if not dictname:
100             dictname, _ = os.path.splitext(
101                 os.path.basename(self._args.input_csv))
102             dictname += ".dic"
103         #
104         cmdl = [
105             _dict_index_bin,
106             "-d", _sysdict_dir,
107             "-f", "utf-8",
108             "-t", self._args.charset,
109             ]
110         in4mdi = self._mecab_official_formatted0
111         if self._args.model:
112             # ワタシのスクリプトでは「コストを自動推測せよ」と等価。
113             # この場合、mecab-dict-index を -a 付きで呼び出して
114             # 穴埋めしてもらった変換済みを先に作る。
115             # なお、
116             # 1. モデルファイルと辞書ファイルのエンコーディングが違うと
117             #    拒絶される
118             # 2. モデルファイルは「charset: euc-jp」のように自己主張する
119             #    ので、実のエンコーディングを変えるだけではダメ。この
120             #    主張もセットで変えなければならない。
121             # TODO: model はテキストのタイプとバイナリタイプがあるらしい。
122             model_path = self._args.model
123             model_enc = re.search(
124                 b'charset: (.*)',
125                 io.open(self._args.model, "rb").read(90)).group(1).decode()
126             if self._args.charset != model_enc:
127                 model = io.open(self._args.model, encoding=model_enc).read()
128                 with io.open(self._mecab_model_reenc, "wb") as fo:
129                     fo.write(
130                         re.sub(re.escape(r"charset: {}".format(model_enc)),
131                                "charset: {}".format(self._args.charset),
132                                model).encode(self._args.charset))
133                 model_path = self._mecab_model_reenc
134             subprocess.check_call(cmdl + [
135                     "-m", model_path,
136                     "-a",
137                     "-u", self._mecab_official_formatted1,
138                     self._mecab_official_formatted0])
139             in4mdi = self._mecab_official_formatted1
140         cmdl.extend([
141                 "-u", dictname,
142                 in4mdi])
143         #
144         subprocess.check_call(cmdl)
145 
146 
147 class _NounName(_BaseMeCabDictInputBuilder):
148     _defaults_fixed = {
149         "品詞": "名詞",
150         "品詞細分類1": "固有名詞",
151         "品詞細分類2": "人名",
152         }
153     def __init__(self, args):
154         _BaseMeCabDictInputBuilder.__init__(self, args)
155 
156     def _default(self, k, line):
157         if k in self._defaults_fixed:
158             return self._defaults_fixed[k]
159         if k in ("左文脈ID", "右文脈ID"):
160             return {
161                 "一般": "1289",
162                 "姓": "1290",
163                 "名": "1291",
164                 }.get(line.get("品詞細分類3", ""))
165         if k == "原形":
166             return line.get("表層形", "")
167         return "*"
168 
169 
170 #
171 if __name__ == '__main__':
172     import argparse
173 
174     parser = argparse.ArgumentParser()
175     parser.add_argument("input_csv")
176 
177     # my-mecab-dict-index specific
178     parser.add_argument("--output-dictionary-name", default="")
179     parser.add_argument(
180         "-f", "--dictionary-charset",
181         help="assume charset of input CSVs as ENC",
182         default="utf-8")
183     # mecab-dict-index
184     parser.add_argument(
185         "-c", "--charset",
186         help="make charset of binary dictionary ENC", default="utf-8")
187     # 「モデルファイルを指定すること」と「コストの推測」は本来の
188     # mecab-dict-index では各々独立の指定だが、「ユーザ辞書作成」
189     # という目的に絞った場合は「コストの推測をさせたいのでモデル
190     # ファイルを指定する」と結びつく。ゆえ、ワタシのスクリプト
191     # では「-m、-a」を一体で考えている。
192     parser.add_argument(
193         "-m", "--model",
194         help="use FILE as model file.")
195     #
196     args = parser.parse_args()
197     #
198     #
199     with _NounName(args) as b:
200         b.compile()

冒頭コメントの通り、前回のも今回のも Python 2.7 で動かない、すまん。

その「コストの自動推測」以外については:

  • 「ユーザ設定フィールド」も使えるようにしといた
  • インターフェイスを mecab-dict-index に少し似せた

ことだけ、かな、確か。コードは少し整理はしてあるけど本質的にはそれだけ。

 1 [me@host: ~]$ cat Noun.name.anime.csv
 2 表層形, 品詞細分類3, 原形, 読み, ジャンル
 3 黒沢ともよ,一般,黒沢ともよ,クロサワトモヨ,声優
 4 犬吠埼樹,一般,犬吠埼樹,イヌボエザキイツキ,アニメキャラ
 5 [me@host: ~]$ # 
 6 [me@host: ~]$ # my-mecab-userdict-build.py にマジックナンバー付いてて
 7 [me@host: ~]$ # としてなおかつパスが通ってるとして…
 8 [me@host: ~]$ my-mecab-userdict-build.py Noun.name.anime.csv -m=mecab-ipadic-2.7.0-20070801.model
 9 C:\Users\HHSPRI~1\AppData\Local\Temp\_my_mecab_model_.model is not a binary model. reopen it as text mode...
10 reading C:\Users\HHSPRI~1\AppData\Local\Temp\_my_mecab_dict0_.csv ... 
11 done!
12 reading C:\Users\HHSPRI~1\AppData\Local\Temp\_my_mecab_dict1_.csv ... 2
13 emitting double-array: 100% |###########################################| 
14 
15 done!
16 [me@host: ~]$ # mecab にパスが通ってるとして…
17 [me@host: ~]$ cat some.txt
18 犬吠埼樹はアニメのキャラ。
19 [me@host: ~]$ mecab < some.txt
20 犬吠埼樹	名詞,固有名詞,人名,一般,*,*,犬吠埼樹,イヌボエザキイツキ,イヌボエザキイツキ,アニメキャラ
21 は	助詞,係助詞,*,*,*,*,は,ハ,ワ
22 アニメ	名詞,一般,*,*,*,*,アニメ,アニメ,アニメ
23 の	助詞,連体化,*,*,*,*,の,ノ,ノ
24 キャラ	名詞,一般,*,*,*,*,キャラ,キャラ,キャラ
25 。	記号,句点,*,*,*,*,。,。,。
26 EOS

ユーザ設定フィールドは使うならちゃんと「設計」して使ったほうがいいかな。入力によってフィールドの意味を変えちゃうんでは管理できなくなるだろう。


前回の『あとは…「人名以外」のためのものがあって、してそれのファクトリがいて、それをコマンドラインオプションで指定出来て…で、まぁまぁ完全なものにはなるんじゃないかな。「コンパイルまでなら」。』はそのままで、新たに『穴埋めした csv も吐き出せた方がいいかしら?』が増えた。特にコスト推測の結果を知りたいというのはありそう。(今のスクリプトは作ったら消してる。)

トレーニングしてモデルを作成するネタも一緒にやろうかと思ったんだけど、コードが既に長くなってることもあって、一緒に書くと煩わしい(読みにくい)ので、これについてはここではやめとく。


追記: あかん。「トレーニング」は簡単に手を出せるシロモノではなかった。
see オリジナル辞書/コーパスからのパラメータ推定. いずれは遊んでみようかとも思うが、すぐではない。