Janomeで親父ギャグメーカーを作る【Python, 自然言語処理】
目次
Janomeで親父ギャグメーカーを作る
人間の話す言語はなんというのでしょうか? これは「自然言語」と呼ばれています。
この自然言語をプログラム的に解析する処理を「自然言語処理」と言います。
自然言語処理には色々な工程がありますが、その中に「形態素解析」という工程があります。
Pythonにはこの形態素解析を行うライブラリ「Janome」があります。
今回はこのJanomeを使って日本語の文章から親父ギャグのリストを表示するというスクリプトを作ってみました。
精度的にはけっこうゆるい作りになってますがあらかじめご了承ください。
今回はこの親父ギャグメーカーについて↓を見ていきます。
- 形態素解析とは?
- Janomeとは?
- 親父ギャグの定義
- 親父ギャグメーカーを作る
- 親父ギャグメーカーを使う
形態素解析とは?
自然言語処理の工程の1つである「形態素解析」とはいったいどんな処理なのでしょうか?
これは日本語の文章を単語のリストに分割する処理です。
自然言語処理では基本となる単位が文章の「単語」です。
この単語を基本的に1単位として解析します。
そのため文章を単語のリストに変換するというのは、さまざまな工程で必要となる工程になります。
たとえば英語は、単語がスペースで区切られています。そのためスペースでチョップすれば文章を単語のリストに変換することが出来ます。
これは↓のような文章を
I have a pen
↓のような単語のリストに変換するということです。
I / have / a / pen
英語はこのように簡単に単語に変換できますが、日本語の文章の場合はそうもいきません。
たとえば「太郎は学校に行った」という文章の単語はスペースなどで区切られていないため、これを単語のリストに変換するには別の方法が必要です。
形態素解析はこのような日本語の文章を辞書を使って解析します。
単語と辞書を照らし合わせながら解析して行って、単語を保存していきます。
こうすることで例えば↓のような日本語の文章も
太郎は学校に行った
↓のように単語のリストに変換することが出来ます。
太郎 / は / 学校 / に / 行った
Janomeとは?
この形態素解析を行えるPythonのライブラリが「Janome(ジャノメ)」です。
形態素解析を行うライブラリには他にも「MeCab(メカブ)」などが有名です。
JanomeはMeCabの辞書を使っています。
速度的にはJanomeはMeCabに劣りますが、MeCabと同精度の解析を行えると言われています。
そのため自然言語処理の学習用途などで最近人気が高まってきているライブラリです。
Janomeを使うにはpipなどで環境にJanome
をインストールしておく必要があります。
インストールは↓のように行います。
> pip install Janome
親父ギャグの定義
今回スクリプトで扱う親父ギャグの定義ですが、↓のように定義したいと思います。
- Aの単語の末尾の読み方が、Bの単語の先頭の読み方に一致していたら親父ギャグ
これはつまり、「階段」という単語があるとします。
この単語の読み方は「カイダン」です。
もう1つの単語は「ダンス」です。
「ダンス」の先頭の「ダン」は「階段」の末尾の「ダン」に含まれています。
そのため「階段」と「ダンス」は親父ギャグの関係と言えます。
これを親父ギャグ表記で表すと「カイダンス」という表記になります。
今回のスクリプトではこのような出力を親父ギャグと言い張りたいと思います。
リンゴリラとかね
親父ギャグというか語呂合わせ?
親父ギャグの世界は広い
高度な親父ギャグについては今回のスクリプトでは出力できません。
たとえば「トンネルで豚が寝てるよ → 豚寝る」など、こういった高度な表現はいまのところ不可能です。
これを実現するにはおそらく機械学習が必要と思われます。
親父ギャグメーカーを作る
それでは親父ギャグメーカーを作ります。
コード全文は↓になります。
# coding: utf-8 from janome.analyzer import Analyzer from janome.tokenfilter import POSKeepFilter def is_in(toks, tok): """ toksの中にtokが含まれていたらTrue, でなければFalse """ for t in toks: if t.surface == tok.surface: return True return False def unique(toks): """ toksから重複したトークンを除外する """ dst = [] for tok in toks: if not is_in(dst, tok): dst.append(tok) return dst def analyze(text): """ textを解析してトークン列にする """ token_filters = [POSKeepFilter(['名詞'])] a = Analyzer(token_filters=token_filters) toks = a.analyze(text) return unique(toks) def find_matchs(toks, tok, min_match_len=0): """ tokにマッチするトークンをtoksから探す """ dst = [] for t in toks: if id(tok) == id(t): continue if tok.reading == t.reading: continue i = len(tok.reading) - 1 found = False while i >= 0: if tok.reading[i] == t.reading[0]: found = True break i -= 1 if not found: continue j = 0 while i + j < len(tok.reading) and j < len(t.reading): if tok.reading[i + j] != t.reading[j]: break j += 1 if j < min_match_len: continue if i + j >= len(tok.reading): dst.append(t) return dst def build(toks, min_reading=0, min_match_len=0): """ データを構築する @param {int} min_reading 読み方の最小文字数 @param {int} min_match_len マッチする最小文字数 """ data = [] for tok in toks: m = {'tok': None, 'matchs': None} if len(tok.reading) <= min_reading: continue matchs = find_matchs(toks, tok, min_match_len) if not len(matchs): continue m['tok'] = tok m['matchs'] = matchs data.append(m) return data def show(tok, match): """ トークンとマッチしたトークンを親父ギャグで表示する """ i = len(tok.reading) - 1 while i >= 0: if tok.reading[i] == match.reading[0]: break i -= 1 j = 0 while j < i: print(tok.reading[j], end='') j += 1 print(match.reading) def show_all(data): """ すべてのデータを表示する """ for m in data: tok = m['tok'] matchs = m['matchs'] print(tok.surface) for match in matchs: show(tok, match) print() def main(): text = ''' 吾輩は猫である。名前はまだ無い。 どこで生れたかとんと見当がつかぬ。...(省略) ''' toks = list(analyze(text)) data = build(toks, min_reading=1, min_match_len=2) show_all(data) main()
コードの解説は↓になります。
スポンサーリンク
必要モジュールのインポート
今回はJanomeのAnalyzer
とPOSKeepFilter
のみを使います。
↓のようにインポートします。
from janome.analyzer import Analyzer from janome.tokenfilter import POSKeepFilter
Analyzer
は形態素解析を行うパーサーです。
POSKeepFilter
はAnalyzer
に指定できるフィルターです。これに名詞などを指定すると、名詞のみを抽出するAnalyzer
を作ることが出来ます。
main関数
スクリプトはmain関数から始まります。
def main(): text = ''' 吾輩は猫である。名前はまだ無い。 どこで生れたかとんと見当がつかぬ。...(省略) ''' toks = list(analyze(text)) data = build(toks, min_reading=1, min_match_len=2) show_all(data)
今回のスクリプトは日本語の文章を元に親父ギャグの一覧を生成します。
その元になる日本語の文章はtext
変数に保存しておきます。
今回は夏目漱石の「吾輩は猫である」を使っていますが、↑のコードでは省略しています。
text
をanalyze()
関数に渡して形態素解析を行いトークン列に変換します。
そしてbuild()
関数にトークン列を渡して出力データをビルドします。
最後にshow_all()
関数でデータを表示して終わりです。
analyze関数
analyze()
関数では引数のtext
を形態素解析してトークン列に変換します。
def analyze(text): """ textを解析してトークン列にする """ token_filters = [POSKeepFilter(['名詞'])] a = Analyzer(token_filters=token_filters) toks = a.analyze(text) return unique(toks)
token_filters
にリストを保存しておきます。このリストにはPOSKeepFilter
のオブジェクトを入れています。
POSKeepFilter()
の引数にはリストで文字列の名詞
を入れておきます。こうすると名詞のみを抽出するフィルターになります。
あとはこのtoken_filters
をAnalyzer()
に渡し、Analyzer
をオブジェクトにします。
このオブジェクトのanalyze()
メソッドにtext
を渡し、形態素解析を行います。
そして最後に形態素解析で得たトークン列をunique()
関数に渡し、重複したトークンを除去します。
重複したトークンが除かれたトークン列をreturn
で返して終わりです。
unique関数
unique()
関数は引数toks
から重複したトークンを取り除きます。
内部ではis_in()
関数を使っていて、これがFalse
になるトークンをdst
リストに追加していきます。
def unique(toks): """ toksから重複したトークンを除外する """ dst = [] for tok in toks: if not is_in(dst, tok): dst.append(tok) return dst
is_in関数
is_in()
関数は引数toks
の中に引数tok
が含まれていたらTrue
を返し、含まれていなければFalse
を返します。
トークンが含まれているかどうかの判定にはトークンの持つsurface
属性を使います。
surface
は表層形と言う元の文章そのままの表記の文字列です。
def is_in(toks, tok): """ toksの中にtokが含まれていたらTrue, でなければFalse """ for t in toks: if t.surface == tok.surface: return True return False
build関数
build
関数は引数toks
から表示データを構築します。
def build(toks, min_reading=0, min_match_len=0): """ データを構築する @param {int} min_reading 読み方の最小文字数 @param {int} min_match_len マッチする最小文字数 """ data = [] for tok in toks: m = {'tok': None, 'matchs': None} if len(tok.reading) <= min_reading: continue matchs = find_matchs(toks, tok, min_match_len) if not len(matchs): continue m['tok'] = tok m['matchs'] = matchs data.append(m) return data
min_reading
引数は読み方、つまりトークンのreading
の長さの最小値です。
この値を大きく設定するとreading
が長いトークンが保存されます。
min_match_len
引数はマッチする最小文字数です。
この値が大きいほど、親父ギャグのレベル(?)が高くなります。
build
関数内ではトークン列を回して1つずつ処理します。
内部ではfind_matchs()
関数を使って、走査中のトークンにマッチするトークン列を抽出しています。
結果はtok
とmatchs
に保存され、それが辞書としてdata
に追加されます。
find_matchs関数
find_matchs関数は引数tok
にマッチするトークンを引数toks
から抽出します。
def find_matchs(toks, tok, min_match_len=0): """ tokにマッチするトークンをtoksから探す """ dst = [] for t in toks: if id(tok) == id(t): continue if tok.reading == t.reading: continue i = len(tok.reading) - 1 found = False while i >= 0: if tok.reading[i] == t.reading[0]: found = True break i -= 1 if not found: continue j = 0 while i + j < len(tok.reading) and j < len(t.reading): if tok.reading[i + j] != t.reading[j]: break j += 1 if j < min_match_len: continue if i + j >= len(tok.reading): dst.append(t) return dst
マッチの条件は親父ギャグの定義によるものです。
↑の処理では最初に走査中のトークン(t
)のreading
の先頭文字がtok
にマッチするか調べています。
tok
のreading
を末尾からチェックして、1文字マッチしていたらfound
フラグをTrue
にします。
found
がTrue
だったらtok.reading
の末尾とt.reading
の先頭部分を比較します。
比較した結果(比較した長さ)がtok.reading
の長さ以上だったらマッチしたと見なしてdst
に走査中のトークンt
を追加します。
この関数がこのスクリプトの中核部分です。
ちょっと複雑になってしまいました。書きようによってはもっとシンプルになるかもしれません。
show_all関数
show_all
関数は表示データを表示します。
データ内の辞書(m
)をfor
文で走査して、それをprint()
やshow()
関数などに渡します。
def show_all(data): """ すべてのデータを表示する """ for m in data: tok = m['tok'] matchs = m['matchs'] print(tok.surface) for match in matchs: show(tok, match) print()
show関数
show
関数は引数tok
とmatch
を使って親父ギャグ表記でトークンを表示します。
これはたとえば「階段」と「ダンス」というトークンであれば「カイダンス」と表示するというものです。
def show(tok, match): """ トークンとマッチしたトークンを親父ギャグで表示する """ i = len(tok.reading) - 1 while i >= 0: if tok.reading[i] == match.reading[0]: break i -= 1 j = 0 while j < i: print(tok.reading[j], end='') j += 1 print(match.reading)
親父ギャグメーカーを使う
今回作った親父ギャグメーカーを実行すると↓のような出力になります。
(出力結果はtext
の値によって変わります)
名前 ナマエ 見当 ケントウジ 所 トコロ いた事 イタコト 記憶 キオク そう ソウショク ソウグウ 感じ カンジン 一 イチバン イチジュ 装飾 ソウショクモツ 薬缶 ヤカンジ ヤカンジン 煙 ケムリヤリ 運転 ウンテン たくさん タクサン タクサン 兄弟 キョウダイドコロ 上今 カミイマ 笹原 ササハラ 向う ムコウ 我慢 ガマンナカ 一樹 イチジュウ 今日 キョウダイ 方 ホウモン 運 ウンテン おさん オサン オサン 台所 ダイドコロ 遍 ヘンポウ この間 コノカンジ コノカンジン 御台 ミダイドコロ 鼻 ハナシ
「兄弟」が「台所」とくっついて「キョウダイドコロ」という親父ギャグになっているのがわかります。
ほかにも「我慢」と「真ん中」がくっついて「ガマンナカ」という意味不明な親父ギャグになっているのもわかります。
入力を工夫したり、名詞のみの抽出をやめて範囲をもっと広くすればもっとバリエーションのある出力が得られるかと思います。
おわりに
今回は形態素解析ライブラリであるJanomeを使って親父ギャグメーカーを作ってみました。
このように形態素解析を行うと日本語の文章を使ったプログラムを作ることが出来ます。
自然言語処理はおもしろい
スポンサーリンク