[Python] CaboChaを使って係り受け解析をしてみた
はじめに
CaboCha美味しいよCaboCha
### 前回のCaboCha インストールまで。
[Python]MacでCaboCha(MeCab)を動かしてみた
### 今回のCaboCha さっそく[CaboCha](http://taku910.github.io/cabocha/)を使って色々試してみました。しかし、まず始めにどうやっていじれば良いのかわからなかったため、適当にサイトを巡って、それらを参考にPythonでコードを組んでみました。
やりたいことは、ある程度まとまった文節で区切って、どの単語がどの単語に係っているかという、まあ係り受け解析の基本の基本を出力することでした。そのため、品詞に着目して、主語と述語の関係を記述する必要があります。
しかしながら、そんなところからゼロから自分で書くのは骨が折れるので、とりあえずTwitterのBotを作っている人のところに行けば何かあるかと思って、次のページに辿り着きました。
[日本語係り受け解析器 CaboCha Ruby 拡張の基本的な使い方とちょっとした応用](http://ultraist.hatenablog.com/entry/20111015/1318662808)
これはCaboCha with Rubyですが、内容としては私がやりたいことにぴったりだったので、ここからRubyコードを拝借して、Pythonに書き換えてみました。以下がコードです。
実装
ソースコード
# -*- coding: utf-8 -*-
import CaboCha
# オレオレTokenラッパークラス
class Token:
def __init__(self, token):
self.chunk = token.chunk
self.features = token.feature.split(',')
self.surface = token.surface
# 名詞かどうか
def is_noun(self):
return self.features[0] == '名詞'
# 動詞かどうか
def is_verb(self):
return self.features[0] == '動詞'
# 形容詞かどうか
def is_adjective(self):
return self.features[0] == '形容詞'
# サ変するかどうか
def is_sahen(self):
return self.features[4] == 'サ変・スル'
# 名詞接続かどうか (「お星様」の「お」など)
def is_noun_connection(self):
return self.features[0] == '接頭詞' and self.features[1] == '名詞接続'
# サ変接続かどうか (掃除する 洗濯する など)
def is_sahen_connection(self):
return self.features[0] == '名詞' and self.features[1] == 'サ変接続'
# 基本形へ
def get_base(self):
if 6 < len(self.features) and self.features[6] != "*":
return self.features[6]
else:
return self.surface
# オレオレChunkラッパークラス
class Chunk:
def __init__(self, chunk, tree):
self.link = chunk.link
self.tokens = [Token(tree.token(i)) for i in xrange(chunk.token_pos, chunk.token_pos + chunk.token_size)]
# 形容詞かどうか
def is_adjective(self):
return self.tokens[0].is_adjective()
# 名詞サ変接続+スルかどうか
def is_verb_sahen(self):
return (1 < len(self.tokens) and self.tokens[0].is_sahen_connection() and self.tokens[1].is_sahen())
# 名詞かどうか
def is_noun(self):
return (not self.is_verb_sahen() and (self.tokens[0].is_noun() or self.tokens[0].is_noun_connection()))
# 動詞かどうか
def is_verb(self):
return self.tokens[0].is_verb() or self.is_verb_sahen()
# 主語っぽいかどうか
def is_subject(self):
if not any([ch == self.tokens[-1].surface for ch in ['は', 'って', 'も', 'が']]):
return False
return self.is_noun() or self.is_adjective() or self.is_verb()
# 基本形へ変換
def get_base(self):
tokens = self.tokens
if self.is_noun():
# 連続する名詞、・_や名詞接続をくっつける
base = ""
for token in tokens:
if token.is_noun_connection():
base += token.get_base()
elif token.is_noun():
base += token.get_base()
elif "_" in token.surface or "・" in token.surface:
base += token.get_base()
elif 0 < len(base):
break
return base
elif self.is_verb_sahen():
ret = tokens[0].get_base() + tokens[1].get_base()
if self.is_negative(tokens):
ret += 'ない'
return ret
elif self.is_verb():
ret = tokens[0].get_base()
if self.is_negative(tokens):
ret += 'ない'
return ret
elif self.is_adjective():
ret = tokens[0].get_base()
if self.is_negative(tokens):
ret += 'ない'
return ret
else:
return ''.join([token.surface for token in tokens])
# 元の形へ変換
def get_surface(self):
tokens = self.tokens
if self.is_noun():
# 連続する名詞、・_や名詞接続をくっつける
surface = ""
for token in tokens:
if token.is_noun_connection():
surface += token.surface
elif token.is_noun():
surface += token.surface
elif "_" in token.surface or "・" in token.surface:
surface += token.surface
elif 0 < len(surface):
break
return surface
elif self.is_verb_sahen():
# 名詞サ変接続 + スル
ret = tokens[0].surface
if self.is_negative(tokens):
ret += tokens[1].surface + 'ない'
else:
ret += 'する'
return ret
elif self.is_verb():
ret = ''
if self.is_negative(tokens):
# 否定の直前までカウント
count = 0
for token in tokens:
if token.features[6] == 'ない':
break
count += 1
ret = ''.join([tokens[i].surface for i in xrange(count)]) + 'ない'
else:
ret = tokens[0].get_base()
return ret
elif self.is_adjective():
ret = ''
if self.is_negative(tokens):
# 否定の直前までカウント
count = 0
for token in tokens:
if token.features[6] == 'ない':
break
count += 1
ret = ''.join([tokens[i].surface for i in xrange(count)]) + 'ない'
else:
ret = tokens[0].get_base()
return ret
else:
return ''.join([token.surface for token in tokens])
def is_negative(self, tokens):
count = 0
for token in tokens:
if token.features[6] == 'ない':
count += 1
return count % 2 == 1
# 対象の文章
sentences = ['ソクラテスは人間です。',
'福沢諭吉は1万円札に出てる人間です。',
'僕も普通の人間。',
'鳥が空を飛んでいる。',
'馬ってたぶんうまい。',
'飛ぶのは飛行機です。',
'かわいいは正義。',
'お星様はとてもまぶしい。',
'拙者は時々切腹するでござる。',
'私は走らない。',
'今日は暑くない。',
'今日は暑くなくはない。',
'あなたは絵を描きたくないんだね。',
'あなたは絵を描きたくなくもないんだね。']
if __name__ == '__main__':
parser = CaboCha.Parser('-f1')
for sentence in sentences:
print '+', sentence
tree = parser.parse(sentence)
chunk_dic = {}
chunk_id = 0
# 全てのchunkに対して
for i in range(0, tree.size()):
token = Token(tree.token(i))
chunk = token.chunk
if chunk:
chunk_dic[chunk_id] = Chunk(chunk, tree)
chunk_id += 1
for chunk_id, chunk in chunk_dic.items():
# 接続先があるかどうか
if 0 < chunk.link:
to_chunk = chunk_dic[chunk.link]
# 主語っぽくて接続先の接続先がないチャンクを抽出
if (chunk.is_subject() and to_chunk.link < 0):
# 主語 => 述語を表示
print "- proto: {} => {}".format(chunk.get_base(), to_chunk.get_base())
print "- org: {} => {}".format(chunk.get_base(), to_chunk.get_surface())
まあ、かなりソースをパクっています笑(ultraistさんアリがとう)。ですが、Pythonistにとってはかなり見やすくなったかと思います。
ただ、元のソースでは出力が原形だったのと、否定が考慮されていないので、とりあえずそこは簡単に考慮するように改良し、出力もなるべく原文と意味が近いものになるようにしました(もちろん文章によっては上手く行きません)。
実行結果
+ ソクラテスは人間です。
- proto: ソクラテス => 人間
- org: ソクラテス => 人間
+ 福沢諭吉は1万円札に出てる人間です。
- proto: 福沢諭吉 => 人間
- org: 福沢諭吉 => 人間
+ 僕も普通の人間。
- proto: 僕 => 人間
- org: 僕 => 人間
+ 鳥が空を飛んでいる。
- proto: 鳥 => 飛ぶ
- org: 鳥 => 飛ぶ
+ 馬ってたぶんうまい。
- proto: 馬 => うまい
- org: 馬 => うまい
+ 飛ぶのは飛行機です。
- proto: 飛ぶ => 飛行機
- org: 飛ぶ => 飛行機
+ かわいいは正義。
- proto: かわいい => 正義
- org: かわいい => 正義
+ お星様はとてもまぶしい。
- proto: お星様 => まぶしい
- org: お星様 => まぶしい
+ 拙者は時々切腹するでござる。
- proto: 拙者 => 切腹する
- org: 拙者 => 切腹する
+ 私は走らない。
- proto: 私 => 走るない
- org: 私 => 走らない
+ 今日は暑くない。
- proto: 今日 => 暑いない
- org: 今日 => 暑くない
+ 今日は暑くなくはない。
- proto: 今日 => 暑い
- org: 今日 => 暑い
+ あなたは絵を描きたくないんだね。
- proto: あなた => 描くない
- org: あなた => 描きたくない
+ あなたは絵を描きたくなくもないんだね。
- proto: あなた => 描く
- org: あなた => 描く
結構良い感じじゃないですかね←
これでも例えば
+ 今日は暑くなくはないこともない。
- proto: こと => ないない
- org: こと => ない
+ 私はいちごもももも好きです。
- proto: 私 => 好き
- org: 私 => 好き
- proto: もも => 好き
- org: もも => 好き
みたいに、普通に少しめんどうな感じの文章では、期待通りにはいきません。ここら辺はまだまだ改良の余地がありますね。導入一日での進捗はこんなもんでしょう。
おわりに
CaboChaを使ってオレオレ係り受け解析を実装してみました。個人的にこれを元に色々試して、Botを作れればなあ、なんて思っています。
上記で載せたコードは僭越ながらGitHubで公開しておきますので、自由に使ってください。
知識不足なのですが、TokenとChunkのクラス拡張は何か良い案がありませんかね?