slontが2016年2月14日に投稿(2016年12月27日更新)

はじめに

CaboCha美味しいよCaboCha


前回のCaboCha

インストールまで。

[Python]MacでCaboCha(MeCab)を動かしてみた


今回のCaboCha

さっそくCaboChaを使って色々試してみました。しかし、まず始めにどうやっていじれば良いのかわからなかったため、適当にサイトを巡って、それらを参考にPythonでコードを組んでみました。



やりたいことは、ある程度まとまった文節で区切って、どの単語がどの単語に係っているかという、まあ係り受け解析の基本の基本を出力することでした。そのため、品詞に着目して、主語と述語の関係を記述する必要があります。



しかしながら、そんなところからゼロから自分で書くのは骨が折れるので、とりあえずTwitterのBotを作っている人のところに行けば何かあるかと思って、次のページに辿り着きました。



日本語係り受け解析器 CaboCha Ruby 拡張の基本的な使い方とちょっとした応用



これは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のクラス拡張は何か良い案がありませんかね?

↑気に入ったらシェアしてね↑
プロフィール
slont

slont

元金融エンジニア。メイン言語はJava, HTML, JavaScript, Python, Kotlinあたり。SECCONやCTF、NLP、機械学習に興味あり。金融日記購読4年。巷で話題の変態紳士。美女ソムリエ始めてました。 お仕事の依頼はTwitterからお願いします。

comments powered by Disqus
Back to top