2018年05月20日

言語処理100本ノックでPython入門 #44 - Gpaphvizとpydotで有向グラフを可視化


今日は、言語処理100本ノック 2015の問題44です。

■ 問題
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

44. 係り受け木の可視化
与えられた文の係り受け木を有向グラフとして可視化せよ.可視化には,係り受け木をDOT言語に変換し,Graphvizを用いるとよい.また,Pythonから有向グラフを直接的に可視化するには,pydotを使うとよい.

■ Graphvizのインストール

Graphviz を用いると良い、とのことなので、以下のコマンドでGraphvizをインストール.

brew install graphviz

すると、linkも必要だよといったメッセージが出てきたので、さらに以下のコマンドを投入。

brew link graphviz

■ pydot_ngのインストール

このGraphvizをPythonから使うには、pydotを使うようなので調べたら、Python3用には、互換性のある pydot_ngを使うのが良いみたい。

 で、
conda install pydot_ng
とやってみたのですが、パッケージが見つからないと出ます。
pydot_ngじゃなくて、pydot-ngらしいです。

conda install pydot-ng
でも、上のコマンドを実行したら、今度は、python2.7にダウングレードするようなそんなメッセージが出てきたので、キャンセル。

どうも、pydot-ngは、python3.4が必要のようです。僕の環境は、Python3.6です。しかたがないので、python3.4の環境を作成。
conda create --name py34 python=3.4

source activate py34

これで、pydot-ngを再度インストール。
 
conda install pydot-ng

その後、Visual Studio Code for Mac で Shift+Command+P で「Python: インタープリターを選択」を呼び出し、py34の環境を選択。

これで、Pythonのコードで、
import pydot_ng as pydot
すれば、pydotが使えるようになります。
 

■ pydotを使って可視化

問題文に「係り受け木をDOT言語に変換し」とあるので、最初、DOT言語(text)に変換する以下のようなコードを書きました。
def toDot(sentence):
    dot = 'digraph mygraph  {\n'
    for chunk in sentence:
        if chunk.done:    # chunkにはdoneメンバーを追加
            continue
        s = ''
        cur = chunk
        while cur.dst >= 0:
            s += cur.concatMorphs()
            s += ' -> '
            cur.done = True
            cur = sentence[cur.dst]
        if s != '':
            s = '    ' + s + cur.concatMorphs() + ';\n'
            dot += s
    dot += '}\n'
    return dot

これで、以下のような文字列が得られます。
digraph mygraph  {
    何でも -> 薄暗い -> 所で -> 泣いて -> 記憶している;
    じめじめした -> 所で -> 泣いて -> 記憶している;
    ニャーニャー -> 泣いて -> 記憶している;
    いた事だけは -> 記憶している;
}
DOT言語って初めて知ったので、これで本当に合っているのか分からないですが...

とりあえず、このDOT言語をpydotに食わせてみて、ダメだったら、toDot関数を直せばいいやと思ったのですが、どうやって、pydotにこのDOT言語を食わせたらよいかがわからないです。
googleで検索してみましたが、サンプルが見つかりません。 pydotにはそんな機能はもともと無いのかもしれません。

しかたがないので、Generating Graph Visualizations with pydot and Graphviz のコードを参考にして、Egdeオブジェクトを使って有向グラフを作成することにします。
def toDot(sentence):
    edges = []
    for chunk in sentence:
        if chunk.dst >= 0:
            edges.append((chunk.concatMorphs(), sentence[chunk.dst].concatMorphs()))
    return edges
まあ、DOT言語に変換する必要がないので、このほうがプログラム的にも簡単でした。 


■ Graphvizを呼び出す

pydotを使い、Graphvizを呼び出すには以下のようなコードを書きます。 これも先ほどのリンク先のページに出ていたコードを参考にしています。


    graph = pydot.Dot(graph_type='digraph')
    graph.set_node_defaults(fontname='Meiryo UI', fontsize='10')

    article = analyze()
    for s, t in toDot(article[4]):
        graph.add_edge(pydot.Edge(s, t))
    graph.write_png('result44.png')

2行目はフォントを変更するためのコードです。

■ Pythonのコード

できあがったPythonのコードです。Morphクラス、Chunkクラスは変更なし、analyze関数も変更なしでしす。
import re
import os
import functools
import pydot_ng as pydot

class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def toList(self):
        return [self.surface, self.base, self.pos, self.pos1]

class Chunk:
    def __init__(self, number, dst):
        self.number = number
        self.morphs = []
        self.dst = dst
        self.srcs = []

    def print(self):
        print(self.number)
        print([x.toList() for x in self.morphs])
        print(self.dst, self.srcs)
        print()

    def concatMorphs(self):
        seq = filter(lambda x: x.pos != '記号', self.morphs)
        return functools.reduce(lambda x, y: x + y.surface, seq, '')



def analyze():
    article = []
    sentence = []
    chunk = None
    with open('neko.txt.cabocha', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n| ', line)
            if line[0] == '*':
                num = int(words[1])
                destNo = int(words[2].rstrip('D'))
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    for index, c in enumerate(sentence, 0):
                        sentence[c.dst].srcs.append(index)
                    article.append(sentence)
                sentence = []
            else:
                chunk.morphs.append(Morph(
                    words[0],
                    words[7],
                    words[1],
                    words[2],
                ))
    return article

def toDot(sentence):
    edges = []
    for chunk in sentence:
        if chunk.dst >= 0:
            edges.append((chunk.concatMorphs(), sentence[chunk.dst].concatMorphs()))
    return edges

def main():
    graph = pydot.Dot(graph_type='digraph')
    graph.set_node_defaults(fontname='Meiryo UI', fontsize='10')

    article = analyze()
    for s, t in toDot(article[4]):
        graph.add_edge(pydot.Edge(s, t))
    graph.write_png('result44.png')

if __name__ == '__main__':
    main()
■ 結果 

出力したpngファイルです。

result44

   

Posted by gushwell at 22:10Comments(0)

2018年05月16日

言語処理100本ノックでPython入門 #43 - 分節の処理 & yield, any, all, slice

今日は、言語処理100本ノック 2015の問題43です。

■ 問題
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

43. 名詞を含む文節が動詞を含む文節に係るものを抽出
名詞を含む文節が,動詞を含む文節に係るとき,これらをタブ区切り形式で抽出せよ.ただし,句読点などの記号は出力しないようにせよ.

■ Pythonのコード
import re
import functools
#import itertools

class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def toList(self):
        return [self.surface, self.base, self.pos, self.pos1]

class Chunk:
    def __init__(self, number, dst):
        self.number = number
        self.morphs = []
        self.dst = dst
        self.srcs = []

    def print(self):
        print(self.number)
        print([x.toList() for x in self.morphs])
        print(self.dst, self.srcs)
        print()

    def concatMorphs(self):
        seq = filter(lambda x: x.pos != '記号', self.morphs)
        return functools.reduce(lambda x, y: x + y.surface, seq, '')

def analyze():
    article = []
    sentence = []
    chunk = None
    with open('chap05/neko.txt.cabocha', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n| ', line)
            if line[0] == '*':
                num = int(words[1])
                destNo = int(words[2].rstrip('D'))
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    for index, c in enumerate(sentence, 0):
                        sentence[c.dst].srcs.append(index)
                    article.append(sentence)
                sentence = []
            else:
                chunk.morphs.append(Morph(
                    words[0],
                    words[7],
                    words[1],
                    words[2],
                ))
    return article


# chunkの中に、posで指定した品詞が含まれているかを確かめる
def contains(chunk, pos):
    return any(m.pos == pos for m in chunk.morphs)

## 名詞を含んだ文節が、動詞を含んだ文節に係るものを抜き出す。
def extract(article):
    for sentence in article:
        for chunk in sentence:
            if chunk.dst >= 0 and contains(chunk, '名詞'):
                target = sentence[chunk.dst]
                if contains(target, '動詞'):
                    yield chunk, target

## nounとverbをタブ区切りで表示する
def printPairs(wr, noun, verb):
    s = noun.concatMorphs()
    if s != '':
        t = verb.concatMorphs()
        wr.write("{}\t{}\n".format(s, t))

def main():
    article = analyze()
    with open('result43.txt', 'w', encoding='utf8') as w:
        for noun, verb in extract(article):
            printPairs(w, noun, verb)

if __name__ == '__main__':
    main()

■ yieldとfor文

Morphクラス、Chunkクラスは変更なし、analyze関数も変更なし。

extract関数は、yieldで値を列挙しています。 pythonではジェネレータというらしいです。

extract関数とprintPair関数を定義した後、まず確認の為に先頭の20個だけを表示させようとして以下のように書きました。
for noun, verb in extract(article)[0:20]:
    printPairs(noun, verb)

でも、実行させたら、エラーになりました(T T)
TypeError: 'generator' object is not subscriptable

いったん、リストにしないとダメみたいです。(最終的には全部を表示しているのでlist()は使ってないです)
for noun, verb in list(extract(article))[0:20]:
    printPairs(noun, verb)

それと、C#のLINQみたいに、take使えればいいのになーと思いますね。

■ any関数

extract関数の中では、contains関数を呼び出しています。このcontains関数は初めは以下のように書きました。chunkの中に指定した品詞があるかどうかを調べる関数です。
def contains(chunk, pos):
    for m in chunk.morphs:
        if m.pos == pos:
            return True
    return False

C#やっている僕としてはちょっとこのコードは泥臭く感じます。C#ならば、Any使うところです。
Pythonにはany無いのかなーって調べたら、Pythonにもanyがありました。
def contains(chunk, pos):
    return any(m.pos == pos for m in chunk.morphs)

以下のようにも書けるけど、こちらはいったんリストに変換されるのであまり良くないと思います。確信は無いですが...
return any([m.pos == pos for m in chunk.morphs])

allもあるみたいです
return all(m.pos == pos for m in chunk.morphs)

■ タプルとfor文

これ書いていて、ちょっと気になったので、次のようなコードを書いてみました。
for elem in (1, 2, 3):
    print(elem)

ちゃんと繰り返されています。 あれ、よくわからなくなってきた、(1, 2, 3) ってタプルなのかな? それともシーケンス?
for elem in 1, 2, 3:
    print(elem)
上のコードでも結果は同じです。どう違うのかな?と思い、
a = (1, 2, 3)
b = 1, 2, 3
をデバッグで見てみたら、2つとも同じ構造みたいです。ということは、両方ともタプルなんんですね。

今日は、いろいろと新しいことを知ることができました。

■ islice関数

話がそれましたので、元に戻します。 anyやallがあるんなら、takeもあるんじゃね、ということで調べたら、 itertools.isliceというのがありました。
for noun, verb in itertools.islice(extract(article), 20):
と書けば、リストにしないでも、20個取り出すことができます。たぶん、こちらのほうが効率よいはずです。 でも、以下のように書けないのがとってもストレスです。
for noun, verb in extract(article).islice(20):

もちろん、最終的には、以下のように変更したので、isliceは使ってません。
for noun, verb in extract(article):
ところで、islice の 先頭のiって何?、i-sliceという意味なのかな?


itertools には、count() repeat() takewhile() dropwhile() groupby()... なんてのがあるみたいです。 必要になった時にちゃんと調べようと思います。


■ 結果

結果の先頭部分だけを示します。
どこで    生れたか
見当が    つかぬ
所で    泣いて
ニャーニャー    泣いて
いた事だけは    記憶している
吾輩は    見た
ここで    始めて
ものを    見た
あとで    聞くと
我々を    捕えて
掌に    載せられて
スーと    持ち上げられた
時    フワフワした
感じが    あったばかりである
上で    落ちついて
顔を    見たのが
ものの    見始であろう
  
Posted by gushwell at 22:30Comments(0)

2018年05月13日

言語処理100本ノックでPython入門 #42 - 係り元と係り先の文節 & 高階関数

今日は、言語処理100本ノック 2015の問題42です。

■ 問題
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

42. 係り元と係り先の文節の表示
係り元の文節と係り先の文節のテキストをタブ区切り形式ですべて抽出せよ.ただし,句読点などの記号は出力しないようにせよ.

■ Pythonのコード

まずは、最終的なPythonのコードを示します。

import re
import functools

class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def toList(self):
        return [self.surface, self.base, self.pos, self.pos1]

class Chunk:
    def __init__(self, number, dst):
        self.number = number
        self.morphs = []
        self.dst = dst
        self.srcs = []

    def print(self):
        print(self.number)
        print([x.toList() for x in self.morphs])
        print(self.dst, self.srcs)
        print()

    def concatMorphs(self):
        seq = filter(lambda x: x.pos != '記号', self.morphs)
        return functools.reduce(lambda x, y: x + y.surface, seq, '')

def analyze():
    article = []
    sentence = []
    chunk = None
    with open('neko.txt.cabocha', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n| ', line)
            if line[0] == '*':
                num = int(words[1])
                destNo = int(words[2].rstrip('D'))
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    for index, c in enumerate(sentence, 0):
                        sentence[c.dst].srcs.append(index)
                    article.append(sentence)
                sentence = []
            else:
                chunk.morphs.append(Morph(
                    words[0],
                    words[7],
                    words[1],
                    words[2],
                ))
    return article


## sentence内の係り元と係り先の対をタブ区切りで表示する
def printPairs(wr, sentence):
    for chunk in sentence:
        if chunk.dst == -1:
            continue
        s = chunk.concatMorphs()
        if s != '':
            t = sentence[chunk.dst].concatMorphs()
            wr.write("{s}\t{t}\n".format(s=s, t=t))

def main():
    article = analyze()
    with open('result42.txt', 'w', encoding='utf8') as w:
        for sentence in article:
            printPairs(w, sentence)

if __name__ == '__main__':
    main()

■ コードの解説

Morphクラス、Chunkクラス、analyzeは前回と同じまま。

新たにprintPairs関数を定義。この関数は1文の中の係り元の文節と係り先の文節のテキストをタブ区切りで表示するもの。

それと、Chunkクラスに concatMorphsメソッドを定義。これは、morphsの中のsurfaceを連結するメソッドです。 久しぶりに、formatメソッド使ってみました。
wr.write("{}\t{}\n".format(s, t))
なんと、{}の中を省略すると、引数の順番と一致するんですね。 知らなかった。 で

も、C#の挿入文字列に慣れている身としては、やはり違和感があるなー。 以下のようにC#みたいに書ければ良いんだけど。
wr.write($"{s}\t{t}")

それと、concatMorphsは、最初は以下のように書きました。ただ、ちょっと冗長です。
def concatMorphs(self):
    s = ''
    for m in self.morphs:
        if m.pos == '記号':
            continue
        s += m.surface
    return s
別の書き方がないのかなと思って調べたら、Pythonには高階関数というのがあるらしいので、使ってみました。(これが採用したコード)
import functools
    ……
    def concatMorphs(self):
        seq = filter(lambda x: x.pos != '記号', self.morphs)
        return functools.reduce(lambda x, y: x + y.surface, seq, '')

reduce関数は、C#のAggregateと同じものですね。
reduce使うには、functoolsというのをimportしなくてはいけないみたいです。はっきりいって、あまりうれしくないです。

reduce使うのやめて、以下のように書きかたも検討しましたが... せっかっくなので、reduce版を採用。
def concatMorphs(self):
    s = ''
    for m in filter(lambda x: x.pos != '記号', self.morphs):
        s += m.surface
    return s

以下のようなアロー式使えればもう少し書きやすくなるのになー。
for m in filter(x => x.pos != '記号', self.morphs):
    s += m.surface

■ 結果

出力ファイルの先頭部分だけをここに掲載
吾輩は    猫である
名前は    無い
まだ    無い
どこで    生れたか
生れたか    つかぬ
とんと    つかぬ
見当が    つかぬ
何でも    薄暗い
薄暗い    所で
じめじめした    所で
所で    泣いて
ニャーニャー    泣いて
泣いて    記憶している
いた事だけは    記憶している
吾輩は    見た
ここで    始めて
始めて    人間という
人間という    ものを
ものを    見た
しかも    種族であったそうだ
あとで    聞くと
聞くと    種族であったそうだ
それは    種族であったそうだ
書生という    人間中で
人間中で    種族であったそうだ
一番    獰悪な
獰悪な    種族であったそうだ
  
Posted by gushwell at 22:00Comments(0)

2018年05月10日

言語処理100本ノックでPython入門 #41 - 分節と係り受け

今日は、言語処理100本ノック 2015の問題41です。

■ 問題


夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

41. 係り受け解析結果の読み込み(文節・係り受け)
40に加えて,文節を表すクラスChunkを実装せよ.このクラスは形態素(Morphオブジェクト)のリスト(morphs),係り先文節インデックス番号(dst),係り元文節インデックス番号のリスト(srcs)をメンバ変数に持つこととする.さらに,入力テキストのCaboChaの解析結果を読み込み,1文をChunkオブジェクトのリストとして表現し,8文目の文節の文字列と係り先を表示せよ.第5章の残りの問題では,ここで作ったプログラムを活用せよ.


■ Pythonのコード
import re

class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def toList(self):
        return [self.surface, self.base, self.pos, self.pos1]

class Chunk:
    def __init__(self, number, dst):
        self.number = number
        self.morphs = []
        self.dst = dst
        self.srcs = []

    def print(self):
        print(self.number)
        print([x.toList() for x in self.morphs])
        print(self.dst, self.srcs)
        print()

def analyze():
    article = []
    sentence = []
    chunk = None
    with open('chap05/neko.txt.cabocha', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n| ', line)
            if line[0] == '*':
                num = int(words[1])
                destNo = int(words[2].rstrip('D'))
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    for index, c in enumerate(sentence, 0):
                        sentence[c.dst].srcs.append(index)
                    article.append(sentence)
                sentence = []
            else:
                chunk.morphs.append(Morph(
                    words[0],
                    words[7],
                    words[1],
                    words[2],
                ))
    return article

def main():
    article = analyze()
    for c in article[8]:
        c.print()

if __name__ == '__main__':
    main()

■ どう解いたか 

この問題を解くのに、文章全体をarticleという変数で表しています。article変数は、以下のような構造になっています。

  • articleは、複数のsentenceから成る -> sentenceのリスト 
  • sentenceは、複数のchunckから成る -> chunckのリスト 
  • chunckは、複数のMorhpから成る -> Morhpのリストをメンバーに持つ

Chunk.srcsの値(係り元文節インデックス番号のリスト)をどう設定するかが悩みましたが、'EOS'の行が現れたら、その直前の文 - つまりsentence変数(リスト)を走査して、各Chunkのsrcsを設定するようにしました。

問題には指定されていませんが、Chunkクラスに勝手に、
self.number = number
を追加しました。このメンバーは表示用なので、無くても問題の本質部分のanalyze関数には影響ないです。

コードの途中に出てくるNoneは、C#のnullのようなものという理解。
chunk = None

Morphクラス、Chunkクラス、analyze関数は、問題42以降使い回すことになると思います。

■ 結果

問題文が求めている表示の形式と違うような気もしますが、本質的な部分ではないと思うので、これで良しとします。

1行目が分節の番号、2行目が分節、3行目が係り先の番号と[]の中は、係り元の番号となっています。
それが、分節の数だけ続きます。最後は、係り先はないので、-1となっています。

0
[['しかし', 'しかし', '接続詞', '*']]
9 []

1
[['その', 'その', '連体詞', '*']]
2 []

2
[['当時', '当時', '名詞', '副詞可能'], ['は', 'は', '助詞', '係助詞']]
5 [1]

3
[['何', '何', '名詞', '代名詞'], ['という', 'という', '助詞', '格助詞']]
4 []

4
[['考', '考', '名詞', '一般'], ['も', 'も', '助詞', '係助詞']]
5 [3]

5
[['なかっ', 'ない', '形容詞', '自立'], ['た', 'た', '助動詞', '*'], ['から', 'から', '助詞', '接続助詞']]
9 [2, 4]

6
[['別段', '別段', '副詞', '助詞類接続']]
7 []

7
[['恐し', '恐い', '形容詞', '自立']]
9 [6]

8
[['いとも', 'いとも', '副詞', '一般']]
9 []

9
[['思わ', '思う', '動詞', '自立'], ['なかっ', 'ない', '助動詞', '*'], ['た', 'た', '助動詞', '*'], ['。', '。', '記号', '句点']]
-1 [0, 5, 7, 8, 9]
  
Posted by gushwell at 21:35Comments(0)

2018年05月06日

言語処理100本ノックでPython入門 #40 - cabochaで形態素解析 & クラスの定義


今日から言語処理100本ノック 2015の第5章に入ります。今日はその1問目の問題40です。

■ 問題
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

40. 係り受け解析結果の読み込み(形態素)
形態素を表すクラスMorphを実装せよ.このクラスは表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をメンバ変数に持つこととする.さらに,CaboChaの解析結果(neko.txt.cabocha)を読み込み,各文をMorphオブジェクトのリストとして表現し,3文目の形態素列を表示せよ.

■ CaboChaをインストール


まずは、MacにCaboChaをインストールします。
$ brew install cabocha
以下のページを参考にしました。

MeCabとCaboChaをMacに導入してPythonから使ってみる


■ CaboChaを使い、neko.txtを解析


インストールが完了したらneko.txtのあるフォルダに移動し、以下のコマンドを投入します。
$ cabocha -f1 neko.txt -o neko.txt.cabocha

cabochaには、いくつものオプションがあってどれを使ったら良いかわからなかったのですが、デフォルトの出力オプションだとプログラムで扱うのが面倒そうなので、-f1 というオプションをつけました。

これで、以下のようなneko.txt.cabochaというファイルが作成されます。
* 0 -1D 0/0 0.000000
一    名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
* 0 2D 0/0 -0.764522
     記号,空白,*,*,*,*, , , 
* 1 2D 0/1 -0.764522
吾輩    名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ
は    助詞,係助詞,*,*,*,*,は,ハ,ワ
* 2 -1D 0/2 0.000000
猫    名詞,一般,*,*,*,*,猫,ネコ,ネコ
で    助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある    助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
。    記号,句点,*,*,*,*,。,。,。
EOS
* 0 2D 0/1 -1.911675
名前    名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は    助詞,係助詞,*,*,*,*,は,ハ,ワ
* 1 2D 0/0 -1.911675
まだ    副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
* 2 -1D 0/0 0.000000
無い    形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。    記号,句点,*,*,*,*,。,。,。
EOS
…以下省略...

EOSは文の終わり、アスタリスクの行は係り受けの情報。そして形態素解析の行は、以下の項目を表しています。
表層形 (Tab区切り)
0. 品詞
1. 品詞細分類1
2. 品詞細分類2
3. 品詞細分類3
4. 活用形
5. 活用型
6. 原形
7. 読み
8. 発音
係り受けの情報については、必要になったら調べます。

■ クラスの定義

やっとクラスを定義する問題が出ました。さっそくMorphクラスを定義します。  
class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1
Pythonもclassキーワードを使ってクラスを定義します。
__init__ は、コンストラクタを表します。
第1引数のselfは必ず必要のようです。これが自身のインスタンスを示します。
これだけで、surfaceやbaseといったメンバーが定義できちゃうのは便利ですね。

メソッドも定義してみました。
def print(self):
    print([self.surface, self.base, self.pos, self.pos1])
メソッドにもselfは必要です。

なんとなく、C#の拡張メソッドみたいです。 C#のようなpublic/privateといったアクセス修飾子はないみたいです。原則公開です。

ただ、
    def __print(self):
        print([self.surface, self.base, self.pos, self.pos1])
とすると、非公開という位置づけになります。

完全な非公開じゃないけど、obj.print() では呼び出せなくなります。先頭に __が付いているから当たり前ですね。

■ インスタンスの生成

インスタンス生成するのにnewキーワードは必要ありません。以下のように書くとインスタンスが生成され、mにインスタンスが代入されます。
m = Morph(
    words[0],
    words[7],
    words[1],
    words[2],
)

■ neko.txt.cabochaの解析

neko.txt.cabochaを読み込み解析する機能は、通常の関数(analyze関数)として定義しました。ちょっと複雑になってしまったかな。

articleは、sentenceが入るリスト。sentenceはMorphが入るリストです。 analyze関数はこのarticleを返しています。

問題には3文目と書いてあります。先頭が0文目から始まると解釈して、article[3]で結果を取り出しています。


■ 出来上がったPythonのコード
import re

class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def print(self):
        print([self.surface, self.base, self.pos, self.pos1])

def analyze():
    article = []
    sentence = []
    with open('chap05/neko.txt.cabocha', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n| ', line)
            if words[0] == '*':
                continue
            elif words[0] == 'EOS':
                if sentence:
                    article.append(sentence)
                sentence = []
            else:
                sentence.append(Morph(
                    words[0],
                    words[7],
                    words[1],
                    words[2],
                ))
    return article

def main():
    article = analyze()
    for morph in article[3]:
        morph.print()

if __name__ == '__main__':
    main()

■ 結果
['\u3000', '\u3000', '記号', '空白']
['どこ', 'どこ', '名詞', '代名詞']
['で', 'で', '助詞', '格助詞']
['生れ', '生れる', '動詞', '自立']
['た', 'た', '助動詞', '*']
['か', 'か', '助詞', '副助詞/並立助詞/終助詞']
['とんと', 'とんと', '副詞', '一般']
['見当', '見当', '名詞', 'サ変接続']
['が', 'が', '助詞', '格助詞']
['つか', 'つく', '動詞', '自立']
['ぬ', 'ぬ', '助動詞', '*']
['。', '。', '記号', '句点']
  
Posted by gushwell at 21:00Comments(0)