PythonのCaboChaの使い方まとめ【自然言語処理, 構文解析】

107, 2020-11-11

目次

構文解析器CaboCha

自然言語処理とは人間の話す言語を解析するプログラミングの分野です。
自然言語処理は↓の工程に分かれています。

  • 形態素解析

  • 構文解析

  • 意味解析

  • 文脈解析

形態素解析というのは日本語を単語のリストに変換する処理です。
そして構文解析とは、それらの単語がどの単語に係(かか)っているかを解析する処理です。
係っているかどうかの関係を「係り受け」の関係といいます。

↑の工程の内、形態素解析から構文解析までをやってくれるライブラリに「CaboCha」というライブラリがあります。
この記事ではPythonによるCaboChaの簡単な使い方をまとめています。

記事執筆に当たってPython版のCaboChaのライブラリ・ドキュメントを探したのですが、見当たりませんでした。
ですのでこの記事はエスパー記事です。ネットの情報をたよりに構成していますので、あらかじめご了承ください。

CaboChaでHello, World!

CaboChaをPythonで利用するにはまずCaboChaをインポートします。
それから構文解析を行うためのパーサーをオブジェクトにしておきます。
これはCaboCha.Parserクラスから作れます。

CaboCha.ParserにはparseToString()メソッドがあります。
これに解析させたい日本語の文章を渡すと、解析が行われ、結果が文字列として返ってきます。

# 普通の出力
import CaboCha

cp = CaboCha.Parser()
sentence = '猫は道路を渡る犬を見た。'
print(cp.parseToString(sentence))

↑のコードの実行結果は↓のようになります。

  猫は-------D
  道路を-D   |
      渡る-D |
        犬を-D
        見た。
EOS

Dという記号に注目してください。
↑の結果を見ると、「猫は」は、「見た」につながってるのがわかります。
また「犬を」も「見た」につながっています。
構文解析の結果、単語間の係り受けが生成されていることがわかります。

詳細な係り受け関係の出力

先の例では人間が見やすいフォーマットで係り受け関係が出力されました。
より詳細な情報を出力したい場合は、まずParserのメソッドparse()を使って構文木を生成します。
それから構文木TreeのメソッドtoString()に情報の出力方法を指定して呼び出します。

# プログラム的な出力
import CaboCha

cp = CaboCha.Parser()
sentence = '猫は道路を渡る犬を見た。'
tree = cp.parse(sentence)
print(tree.toString(CaboCha.FORMAT_LATTICE))

tree.toString(CaboCha.FORMAT_LATTICE)とやっています。
toString()の引数にCaboCha.FORMAT_LATTICEを指定すると↓のような出力になります。
ちなみにFORMAT_TREEにすると先ほどのように人間が見やすいフォーマットになります。

* 0 4D 0/1 -2.106167
猫      名詞,一般,*,*,*,*,猫,ネコ,ネコ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
* 1 2D 0/1 2.469759
道路    名詞,一般,*,*,*,*,道路,ドウロ,ドーロ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
* 2 3D 0/0 1.242908
渡る    動詞,自立,*,*,五段・ラ行,基本形,渡る,ワタル,ワタル
* 3 4D 0/1 -2.106167
犬      名詞,一般,*,*,*,*,犬,イヌ,イヌ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
* 4 -1D 0/1 0.000000
見      動詞,自立,*,*,一段,連用形,見る,ミ,ミ
た      助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。      記号,句点,*,*,*,*,。,。,。
EOS

解析結果は、2段にわかれていて、1段目(*で始まる行)は左から順に、

  • 文節番号

  • かかり先の文節番号

  • 主辞/機能語の位置

  • 係り関係のスコア

になっています。
*以降の行は左から順に、

  • 表層形(元の文章上の表記)

  • 品詞

  • 品詞細分類1

  • 品詞細分類2

  • 品詞細分類3

  • 活用形

  • 活用型

  • 原形

  • 読み

  • 発音

です。

(参考: 【Mac】日本語係り受け解析器「CaboCha」をインストールして使ってみる - bitA Tech Blog

CaboChaでよく使用するタイプ

CaboChaによる解析でよく使用するクラスは↓の通りです。

  • CaboCha.Tree

  • CaboCha.Token

  • CaboCha.Chunk

これらのタイプは↓のコードで確認することが可能です。

# タイプ
import CaboCha

cp = CaboCha.Parser()
tree = cp.parse('猫')
print(type(tree))  # <class 'CaboCha.Tree'>

token = tree.token(0)
print(type(token))  # <class 'CaboCha.Token'>
print(type(token.chunk))  # <class 'CaboCha.Chunk'>

PythonのライブラリであるCaboChaでは、バイナリのdllをバインディングしています。
そのためhelp()などでメソッドなどのコメントを確認できません。
バインディングされているメソッドなどは確認可能です。

トークン(形態素)の出力

CaboChaで解析した構文木内のトークン(形態素)を出力するには、まずParser.parse()で構文木を生成します。
それからTree.size()の数だけループを回し、Tree.token()にインデックスを渡してトークンを取得します。
取得したトークンは属性feature, surfaceなどを持っています。
featureはカンマ区切りで並べられた品詞の文字列です。
surfaceはトークンの表層形です。

# tokenの出力
import CaboCha

cp = CaboCha.Parser()
sentence = '猫は道路を渡る犬を見た。'
tree = cp.parse(sentence)

for i in range(tree.size()):
    tok = tree.token(i)
    print('品詞:', tok.feature)
    print('表層形:', tok.surface)
    print('-' * 40)

↑のコードを実行すると↓のような結果になります。

品詞: 名詞,一般,*,*,*,*,猫,ネコ,ネコ
表層形: 猫
----------------------------------------
品詞: 助詞,係助詞,*,*,*,*,は,ハ,ワ
表層形: は
----------------------------------------
品詞: 名詞,一般,*,*,*,*,道路,ドウロ,ドーロ
表層形: 道路
----------------------------------------
品詞: 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
表層形: を
----------------------------------------
品詞: 動詞,自立,*,*,五段・ラ行,基本形,渡る,ワタル,ワタル
表層形: 渡る
----------------------------------------
品詞: 名詞,一般,*,*,*,*,犬,イヌ,イヌ
表層形: 犬
----------------------------------------
品詞: 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
表層形: を
----------------------------------------
品詞: 動詞,自立,*,*,一段,連用形,見る,ミ,ミ
表層形: 見
----------------------------------------
品詞: 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
表層形: た
----------------------------------------
品詞: 記号,句点,*,*,*,*,。,。,。
表層形: 。
----------------------------------------

CaboChaは内部でMeCabを使っているので、これらの形態素の出力はMeCabによるものです(開発環境によります)。

自力で係り受け関係を出力する

CaboCha.Treeのメソッドを使って自力で係り受け関係をたどりたい場合は↓のようにします。

import CaboCha


def gen_chunks(tree):
    """
    構文木treeからチャンクの辞書を生成する
    """
    chunks = {}
    key = 0  # intにしているがこれはChunk.linkの値で辿れるようにしている

    for i in range(tree.size()):  # ツリーのサイズだけ回す
        tok = tree.token(i)  # トークンを得る
        if tok.chunk:  # トークンがチャンクを持っていたら
            chunks[key] = tok.chunk  # チャンクを辞書に追加する
            key += 1

    return chunks


def get_surface(tree, chunk):
    """
    chunkからtree内のトークンを得て、そのトークンが持つ表層形を取得する
    """
    surface = ''
    beg = chunk.token_pos  # このチャンクのツリー内のトークンの位置
    end = chunk.token_pos + chunk.token_size  # トークン列のサイズ

    for i in range(beg, end):
        token = tree.token(i)
        surface += token.surface  # 表層形の取得

    return surface


def main():
    sentence = '猫は道路を渡る犬を見た。'

    cp = CaboCha.Parser()  # パーサーを得る
    tree = cp.parse(sentence)  # 入力から構文木を生成
    print(tree.toString(CaboCha.FORMAT_TREE))  # デバッグ用

    chunks = gen_chunks(tree)  # チャンクの辞書を生成する

    for from_chunk in chunks.values():
        if from_chunk.link < 0:
            continue  # リンクのないチャンクは飛ばす

        # このチャンクの表層形を取得
        from_surface = get_surface(tree, from_chunk)

        # from_chunkがリンクしているチャンクを取得
        to_chunk = chunks[from_chunk.link]
        to_surface = get_surface(tree, to_chunk)

        # 出力
        print(from_surface, '->', to_surface)


main()

CaboChaでは係り受けの関係をCaboCha.Chunkというクラスで管理しているようです(推測ですが)。
そのため最初にCaboCha.Treeからこのチャンクを辞書に保存しておきます。

def gen_chunks(tree):
    """
    構文木treeからチャンクの辞書を生成する
    """
    chunks = {}
    key = 0  # intにしているがこれはChunk.linkの値で辿れるようにしている

    for i in range(tree.size()):  # ツリーのサイズだけ回す
        tok = tree.token(i)  # トークンを得る
        if tok.chunk:  # トークンがチャンクを持っていたら
            chunks[key] = tok.chunk  # チャンクを辞書に追加する
            key += 1

    return chunks

次にチャンクからトークンの表層形を取得できる関数get_surface()を定義します。

def get_surface(tree, chunk):
    """
    chunkからtree内のトークンを得て、そのトークンが持つ表層形を取得する
    """
    surface = ''
    beg = chunk.token_pos  # このチャンクのツリー内のトークンの位置
    end = chunk.token_pos + chunk.token_size  # トークン列のサイズ

    for i in range(beg, end):
        token = tree.token(i)
        surface += token.surface  # 表層形の取得

    return surface

チャンクはtoken_posというツリー内におけるチャンクが所属するトークンのポジションを持っています。
このポジションを参照しTree.token()に渡すことでチャンクのトークンを参照できるらしいです。

あとはこれらの関数を使ってmain()関数内で処理を実行します。

    chunks = gen_chunks(tree)  # チャンクの辞書を生成する

    for from_chunk in chunks.values():
        if from_chunk.link < 0:
            continue  # リンクのないチャンクは飛ばす

        # このチャンクの表層形を取得
        from_surface = get_surface(tree, from_chunk)

        # from_chunkがリンクしているチャンクを取得
        to_chunk = chunks[from_chunk.link]
        to_surface = get_surface(tree, to_chunk)

        # 出力
        print(from_surface, '->', to_surface)

このコードの出力は↓のような結果になります。

  猫は-------D
  道路を-D   |
      渡る-D |
        犬を-D
        見た。
EOS

猫は -> 見た。
道路を -> 渡る
渡る -> 犬を
犬を -> 見た。

単語間の係り受けの関係が出力されています。

このコードはトラフィールズさんの掲載コードをたいへん参考にさせていただきました。
(参考: Python CaboChaを用いて係り受け構造を抽出する方法 | トライフィールズ

おわりに

係り受けの関係が解析できれば自然言語処理で一歩進んだ処理が可能になります。
形態素解析に続いて抑えておきたいところです。

(^ _ ^)

構文解析でみんなハッピー

参考



この記事のアンケートを送信する