どうせ和布蕪るなら、続き (jumandic 相手に貧乏性、の軽くな説明2)

これの続き。

例によって今回も、興味ある人は mecab_userdic_from_jumandic-20191118.tar.bz2 を見ながら読んでな。「個人的に楽しめたこと」というテーマの話。


まぁやってる本人はいつだって楽しんでやっているわけなんだけれども。

今回のやつの場合は、とりわけ楽しくやれたのが、class _kvar_simplifier。ちらと説明したとおり、最後まで整理しきらないままお披露目してるんで、こいつに関しても実は「名前が相応しくない」。書き始めたときに想定していた「アプローチ」から段々精度よくなってきて、「はじめに考えたことより遥かに多くの責務を担えるようになっちゃった」のに当初想定のまんまの名前、なのね。

class _kvar_simplifier が結果的に今のお披露目時点で何をするようになったか、については、実際に動かしてもらったほうがわかるかもしんない。jumandic エントリの「迷惑っぷりな分類」をしてる。のだけれど、これってのは要するに「覆水を盆に還す」処理で、言い方を変えれば「機械生成のための最小単位(とテンプレート)復元」。おそらく jumandic はこういうことをしてるんだと思うのよね:

1 # -*- coding: utf-8 -*-
2 words = [
3     [("因", "いん"), ("果", "が"), ("応", "おう"), ("報", "ほう")],
4     # ...
5     ]
6 for w in words:
7     for i in range(2**len(w)):
8         print("".join([w[j][2**j & i == 0] for j in range(len(w))]))
 1 いんがおうほう
 2 因がおうほう
 3 いん果おうほう
 4 因果おうほう
 5 いんが応ほう
 6 因が応ほう
 7 いん果応ほう
 8 因果応ほう
 9 いんがおう報
10 因がおう報
11 いん果おう報
12 因果おう報
13 いんが応報
14 因が応報
15 いん果応報
16 因果応報

これの、「words」をエントリから復元出来さえすれば、「wordsを使って何をしたのか」が復元できるであろう、てことね。「何を」は無論「ひらがな化」「部分ひらがな化」てことがまず第一で、あと、これは実現も大変だったし、実際結構処理が繊細(で、もろい)んだけれども「送り仮名違い」ね。この「変種の意味(意図)」を復元してやろう、ってこと。

class _kvar_simplifier だけでも既に大作で、ここに貼り付けて説明すると結構鬱陶しいので、コードについては添付を直接読んでもらうとして。これを使っている箇所を単純化するとこんな具合:

単純化、というよりは、実際にテスト用に使ってたコード。
 1 # -*- coding: utf-8 -*-
 2 import re
 3 import sys
 4 from collections import defaultdict
 5 import logging
 6 import _kvss  # _kvar_simplifier だけ抜き出したモジュール。
 7 import _pats  # テストしたいヤツだけ抜き出したヤツ。
 8 
 9 if __name__ == '__main__':
10     logging.basicConfig(stream=sys.stderr, level=logging.INFO)
11 
12     for pt in _pats.patterns:
13         keys, vals = pt
14         dh, yomi = keys.split(",")
15         kvss = _kvss._kvar_simplifier(dh, yomi)
16         for v in vals.split(","):
17             kvss.update(v)
18         kvss.end_update()
19         #
20         tmp = defaultdict(list)
21         for kn, y in sorted(kvss._minidic):
22             tmp[y].append(kn)
23 
24         # 上の説明で「words の復元」といってるヤツのダンプ:
25         pp = "\n".join(["    {!r}: {!r},".format(
26             k, list(sorted(v))) for k, v in sorted(tmp.items())])
27         logging.info("\n%s", pp)
28 
29         # 以下はそれを使った「判定結果」のダンプ。
30         logging.info("%r", kvss._has_dkanji)
31         for i, v in enumerate(vals.split(",")):
32             #logging.error((i, v, kvss._surfs._items))
33             logging.info((dh, v, kvss.classified(v)))
34             #logging.info((dh, v, kvss.simplify(dh), kvss.simplify(v)))
上で _pats 言ってるやつの抜粋
 1 # -*- coding: utf-8 -*-
 2 patterns = [
 3     (
 4         "ありったけ,アリタケ",
 5         "ありたけ,あり丈,有りたけ,有り丈"
 6     ),
 7     (
 8         "ありったけ,アリッタケ",
 9         "ありったけ,ありっ丈,有りったけ,有りっ丈"
10     ),
11     (
12         "いまいましい,イマイマシイ",
13         "いまいましい,イマイマしい"
14     ),
15     (
16         "うつ伏せ,ウツブセ",
17         "うつ伏せ,うつぶせ,俯せ"
18     ),
19     (
20         "おじけ付く,オジケヅク",
21         "おじけ付く,おじけづく,おじ気づく,おじ気付く,怖けづく,怖け付く,怖じけづく,怖じけ付く,怖じ気づく,怖じ気付く,怖気づく,怖気付く"
22     ),
23     # ...
24     (
25         "五ヶ瀬,ゴカセ",
26         "五ヶ瀬"
27     ),
28     (
29         "仮初め,カリソメ",
30         "仮初め,かりそめ,かり初,かり初め,仮そめ,仮初,苟且"
31     ),
32     (
33         "仰る,オッシャル",
34         "仰る,おっしゃる,仰有る"
35     ),
36     (
37         "余所行き,ヨソユキ",
38         "よそゆき,他所ゆき,余所ゆき"
39     ),
40     # ...
41     (
42         "龍ケ崎,リュウガサキ",
43         "龍ケ崎"
44     ),
45 ]

実際に動かすとこんな具合:

  1 INFO:root:
  2     ((0, 'ア'),): ['有'],
  3     ((2, 'タ'), (3, 'ケ')): ['丈'],
  4 INFO:root:''
  5 INFO:root:('ありったけ', 'ありたけ', ([], []))
  6 INFO:root:('ありったけ', 'あり丈', ([], ['漢字表記']))
  7 INFO:root:('ありったけ', '有りたけ', ([], ['漢字表記']))
  8 INFO:root:('ありったけ', '有り丈', ([], ['漢字表記']))
  9 INFO:root:
 10     ((0, 'ア'),): ['有'],
 11     ((3, 'タ'), (4, 'ケ')): ['丈'],
 12 INFO:root:''
 13 INFO:root:('ありったけ', 'ありったけ', ([], []))
 14 INFO:root:('ありったけ', 'ありっ丈', ([], ['漢字表記']))
 15 INFO:root:('ありったけ', '有りったけ', ([], ['漢字表記']))
 16 INFO:root:('ありったけ', '有りっ丈', ([], ['漢字表記']))
 17 INFO:root:
 18 
 19 INFO:root:''
 20 INFO:root:('いまいましい', 'いまいましい', (['ひらがな'], []))
 21 INFO:root:('いまいましい', 'イマイマしい', (['カタカナ'], []))
 22 INFO:root:
 23     ((0, 'ウ'), (1, 'ツ'), (2, 'ブ')): ['ウツ伏', '俯'],
 24     ((2, 'ブ'),): ['伏'],
 25 INFO:root:'K'
 26 INFO:root:('うつ伏せ', 'うつ伏せ', ([], []))
 27 INFO:root:('うつ伏せ', 'うつぶせ', ([], ['全平仮名化']))
 28 INFO:root:('うつ伏せ', '俯せ', ([], []))
 29 INFO:root:
 30     ((0, 'オ'),): ['怖'],
 31     ((2, 'ケ'),): ['気'],
 32     ((2, 'ケ'), (3, 'ヅ')): ['気付'],
 33     ((3, 'ヅ'),): ['付'],
 34 INFO:root:''
 35 INFO:root:('おじけ付く', 'おじけ付く', ([], []))
 36 INFO:root:('おじけ付く', 'おじけづく', ([], ['全平仮名化']))
 37 INFO:root:('おじけ付く', 'おじ気づく', ([], []))
 38 INFO:root:('おじけ付く', 'おじ気付く', ([], []))
 39 INFO:root:('おじけ付く', '怖けづく', ([], ['送り仮名']))
 40 INFO:root:('おじけ付く', '怖け付く', ([], ['送り仮名']))
 41 INFO:root:('おじけ付く', '怖じけづく', ([], []))
 42 INFO:root:('おじけ付く', '怖じけ付く', ([], []))
 43 INFO:root:('おじけ付く', '怖じ気づく', ([], []))
 44 INFO:root:('おじけ付く', '怖じ気付く', ([], []))
 45 INFO:root:('おじけ付く', '怖気づく', ([], ['送り仮名']))
 46 INFO:root:('おじけ付く', '怖気付く', ([], ['送り仮名']))
 47 INFO:root:
 48 
 49 INFO:root:''
 50 INFO:root:('せる', 'せる', ([], []))
 51 INFO:root:
 52 
 53 INFO:root:''
 54 INFO:root:('そこら', 'そこいら', (['長促拗音等'], []))
 55 INFO:root:
 56 
 57 INFO:root:''
 58 INFO:root:('やっぱり', 'やっぱり', ([], []))
 59 INFO:root:
 60 
 61 INFO:root:''
 62 INFO:root:('アイスホッケー', 'アイスホッケ', (['長促拗音等'], []))
 63 INFO:root:
 64    ... (snip) ...
 65 INFO:root:
 66     ((0, 'コ'), (1, 'ザ'), (2, 'ト')): ['小里', '阜'],
 67     ((0, 'コ'), (1, 'ザ'), (2, 'ト'), (3, 'ヘ'), (4, 'ン')): ['小里偏', '阜偏'],
 68     ((3, 'ヘ'), (4, 'ン')): ['偏'],
 69 INFO:root:'K'
 70 INFO:root:('阜偏', '阜偏', ([], []))
 71 INFO:root:('阜偏', 'こざとへん', ([], ['全平仮名化']))
 72 INFO:root:('阜偏', 'こざと偏', ([], ['仮名化']))
 73 INFO:root:('阜偏', '小里へん', ([], ['仮名化']))
 74 INFO:root:('阜偏', '小里偏', ([], []))
 75 INFO:root:('阜偏', '阜へん', ([], ['仮名化']))
 76 INFO:root:
 77     ((0, 'ア'),): ['阿'],
 78     ((0, 'ア'), (1, 'バ')): ['阿婆'],
 79     ((0, 'ア'), (1, 'バ'), (2, 'ズ')): ['阿婆擦'],
 80     ((1, 'バ'),): ['婆'],
 81     ((1, 'バ'), (2, 'ズ')): ['婆擦'],
 82     ((2, 'ズ'),): ['擦'],
 83 INFO:root:''
 84 INFO:root:('阿婆擦れ', '阿婆擦れ', ([], []))
 85 INFO:root:('阿婆擦れ', 'あばずれ', ([], ['全平仮名化']))
 86 INFO:root:('阿婆擦れ', 'あば擦', ([], ['送り仮名', '仮名化']))
 87 INFO:root:('阿婆擦れ', 'あば擦れ', ([], ['仮名化']))
 88 INFO:root:('阿婆擦れ', 'あ婆ずれ', ([], ['仮名化']))
 89 INFO:root:('阿婆擦れ', 'あ婆擦', ([], ['送り仮名', '仮名化']))
 90 INFO:root:('阿婆擦れ', 'あ婆擦れ', ([], ['仮名化']))
 91 INFO:root:('阿婆擦れ', '阿ばずれ', ([], ['仮名化']))
 92 INFO:root:('阿婆擦れ', '阿ば擦', ([], ['送り仮名', '仮名化']))
 93 INFO:root:('阿婆擦れ', '阿ば擦れ', ([], ['仮名化']))
 94 INFO:root:('阿婆擦れ', '阿婆ずれ', ([], ['仮名化']))
 95 INFO:root:('阿婆擦れ', '阿婆擦', ([], ['送り仮名']))
 96 INFO:root:
 97     ((0, 'カ'), (1, 'ク')): ['隠'],
 98     ((4, 'ボ'),): ['坊'],
 99 INFO:root:''
100 INFO:root:('隠れん坊', 'かくれんぼ', ([], ['全平仮名化']))
101 INFO:root:('隠れん坊', '隠れんぼ', ([], ['仮名化']))
102 INFO:root:
103     ((0, 'カ'), (1, 'ク')): ['隠'],
104     ((4, 'ボ'), (5, 'ウ')): ['坊'],
105 INFO:root:''
106 INFO:root:('隠れん坊', '隠れん坊', ([], []))
107 INFO:root:('隠れん坊', 'かくれんぼう', ([], ['全平仮名化']))
108 INFO:root:('隠れん坊', 'かくれん坊', ([], ['仮名化']))
109 INFO:root:('隠れん坊', '隠れんぼう', ([], ['仮名化']))
110 INFO:root:
111     ((0, 'キ'), (1, 'リ')): ['霧'],
112     ((0, 'キ'), (1, 'リ'), (2, 'ア'), (3, 'メ')): ['霧雨'],
113     ((2, 'ア'), (3, 'メ')): ['雨'],
114 INFO:root:''
115 INFO:root:('霧雨', 'きりあめ', ([], ['全平仮名化']))
116 INFO:root:('霧雨', '霧あめ', ([], ['仮名化']))
117 INFO:root:
118     ((0, 'キ'), (1, 'リ')): ['霧'],
119     ((0, 'キ'), (1, 'リ'), (2, 'サ'), (3, 'メ')): ['霧雨'],
120     ((2, 'サ'), (3, 'メ')): ['雨'],
121 INFO:root:''
122 INFO:root:('霧雨', '霧雨', ([], []))
123 INFO:root:('霧雨', 'きりさめ', ([], ['全平仮名化']))
124 INFO:root:('霧雨', 'きり雨', ([], ['仮名化']))
125 INFO:root:
126    ... (snip) ...

jumandic には「代表表記」という考え方があって、それを使ってグルーピング出来るので、そのグループ内の小さな辞書を作っているわけなんだけれども、そのミニ辞書構築がね、それだけでも結構めんどいかったんだわ。実際コードを読んでみればいいと思う。「ばっかだなー、もっと単純に書けろ」はあるだろうけれど、ただ、考えなければいけないことが多いことだけは、じっくり鑑賞してもらえばわかってもらえると思う。そう、これね、その大変さに対して真面目に取り組んでるのがさ、なんか楽しかったのよね。これが功を奏しなかったなら楽しくない結末だったかもしれんけど、思ったより狙い通りの結果になったから尚更。

ただし。最初に予告した通り、「テクニカルな意味でのうまみ」ってのはないのよ。だからこうやってブログネタにするのには、ほとんど相応しくない。単なる「複雑なコード」でしかなくて、現実世界に対する適用ってのはこういうもんだ、って実感出来る以上のもんではない。実際これを読んでもらったとしても、「うげぇ、長いしなんかむつかしい」以上のことは感じないだろうし、Python 的なであるとかの新発見は、きっとないと思うよ。まぁあなたのレベルにもよるけれど。(汎化出来る部分も多くないしさ。)

あぁ、ただ、ちょっと「あなたにとって」面白いと感じるかもしれないことは、ないことはない。それは、「落としどころの見つけ方」。つまりは「どう妥協点を探るか」のあんばいね。まぁワタシのやることは割といつもそうではあるんだけれど、今回のネタはそれがおそらく最も顕著に現れていて、「理想論」に全然走らない。あくまでも「目の前の現実」だけに立ち向かってる。簡単に言えば「今相手にしてる jumandic 以外のことなんぞ知らん」として、あえて汎用化とは正反対の志向を「心がけている」。そこを踏まえて鑑賞すると、何か得るものがあるんではないかと思う。(これは _kvar_simplifier に限らず「スクリプト」全体で言える話。)


「ワタシ個人が楽しんだ」というだけの話なので、おぬしもみーつーかどうかは知らん。楽しんでもらえたらいいんだけどね。こういうのはほら、感性ってやつがあるから。何に興奮するのかなんて人それぞれ。性癖が人それぞれなのと一緒。まぁ楽しめる人は楽しんでくれぃ。


17:50追記:
「_kvar_simplifier」という命名にした理由がはっきりわかる「初版」を見つけた:

 1 # -*- coding: utf-8-unix -*-
 2 import re
 3 import doctest
 4 import itertools
 5 
 6 _khmap = {
 7     chr(i + ord("ぁ")): chr(i + ord("ァ"))
 8     for i in range((ord("ん") - ord("ぁ")) + 1)}
 9 def _h2k(s):
10     return "".join(
11         [_khmap.get(c, c) for c in s])
12 _hkmap = {
13     chr(i + ord("ァ")): chr(i + ord("ぁ"))
14     for i in range((ord("ン") - ord("ァ")) + 1)}
15 def _k2h(s):
16     return "".join(
17         [_hkmap.get(c, c) for c in s])
18 
19 
20 class _kvar_simplifier(object):
21     # 「阜偏/小里偏/こざと偏/阜へん/小里」のような変種群で
22     # 変種の区別をするのに使うやーつ。
23     def __init__(self, yomi):
24         self._yomi = yomi
25         self._minidic = set()
26         self._rgx = None
27 
28     def update(self, sur):
29         def _tw(s1, s2):
30             return list(itertools.takewhile(
31                 lambda x: _h2k(x[0]) == x[1], zip(s1, s2)))
32         s, y = sur, self._yomi
33         p1, p2, p3 = (s, y), ("", ""), ("", "")
34         lm = len(_tw(reversed(s), reversed(y)))
35         if lm:
36             p1, p3 = (s[:-lm], y[:-lm]), (s[-lm:], y[-lm:])
37         s, y = p1
38         fm = len(_tw(s, y))
39         p1, p2 = (s[:fm], y[:fm]), (s[fm:], y[fm:])
40         self._minidic |= set([
41                 p for p in (p1, p2, p3)
42                 if p[0] and re.search(r"[^ぁ-んァ-ンヴー・]", p[0])])
43 
44     def end_update(self):
45         a = sorted(list(self._minidic), key=lambda x: (-len(x[0]), x[0]))
46         self._rgx = re.compile(
47             r"({})".format("|".join([s[0] for s in a])))
48 
49     def simplify(self, sur):
50         class _sub(object):
51             def __init__(self):
52                 self._occ = 1
53             def __call__(self, m):
54                 res = "<rep{}>".format(self._occ)
55                 self._occ += 1
56                 return res
57         return self._rgx.sub(_sub(), sur)
58 
59 
60 if __name__ == '__main__':
61     md = _kvar_simplifier("コザトヘン")
62     md.update("阜偏")
63     md.update("阜へん")
64     md.update("小里偏")
65     md.update("小里へん")
66     md.update("こざと偏")
67     md.end_update()
68     print(md.simplify("阜偏"))
69     print(md.simplify("阜へん"))
70     print(md.simplify("こざと偏"))
71     md = _kvar_simplifier("インガオウホウ")
72     md.update("いんが応報")
73     md.update("いん果応ほう")
74     md.update("因果応報")
75     md.end_update()
76     print(md.simplify("いん果応ほう"))
77     print(md.simplify("因果応報"))

一応履歴管理はしてるんだけど、さすがにこれはコミットしたものの中にはなくて、「a.py」として一番最初の実験のがたまたま残してあった。

最新状態のものと較べて遥かに単純で「バカ」だが、はじめにワタシが何を考えたのかは、これならすぐにわかるよね。無論ミニ辞書構築も不完全で、まったく役には立たないよ。