2018年05月30日

言語処理100本ノックでPython入門 #47 - 機能動詞構文のマイニング



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

■問題
47. 機能動詞構文のマイニング
動詞のヲ格にサ変接続名詞が入っている場合のみに着目したい.46のプログラムを以下の仕様を満たすように改変せよ.
  • 「サ変接続名詞+を(助詞)」で構成される文節が動詞に係る場合のみを対象とする 
  • 述語は「サ変接続名詞+を+動詞の基本形」とし,文節中に複数の動詞があるときは,最左の動詞を用いる 
  • 述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる 
  • 述語に係る文節が複数ある場合は,すべての項をスペース区切りで並べる(助詞の並び順と揃えよ)
例えば「別段くるにも及ばんさと、主人は手紙に返事をする。」という文から,以下の出力が得られるはずである.
返事をする      と に は        及ばんさと 手紙に 主人は
このプログラムの出力をファイルに保存し,以下の事項をUNIXコマンドを用いて確認せよ. コーパス中で頻出する述語(サ変接続名詞+を+動詞) コーパス中で頻出する述語と助詞パターン

■ 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, '')

class Pair:
    def __init__(self, particle, paragraph):
        self.particle = particle
        self.paragraph = paragraph

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 findVerbs(sentence):
    for chunk in sentence:
        for m in reversed(chunk.morphs):
            if m.pos == '動詞':
                yield m, chunk.number
                break

def findParticles(sentence, chunkNo):
    for chunk in sentence:
        if chunk.dst == chunkNo:
            nextMorph = Morph('', '', '', '')
            for m in reversed(chunk.morphs):
                if nextMorph.pos == '助詞':
                    yield m, nextMorph, chunk.concatMorphs()
                    break
                nextMorph = m

def enumPattern(article):
    for sentence in article:
        for v, num in findVerbs(sentence):
            pairlist = []
            obj = ''
            for part1, part2, para in findParticles(sentence, num):
                if part1.pos == '名詞' and part1.pos1 == 'サ変接続' and part2.surface == 'を':
                    obj = part1.surface + part2.surface
                else:
                    pairlist.append(Pair(part2.surface, para))
            if pairlist and obj != '':
                yield obj+v.base, sorted(pairlist, key=lambda x: x.particle)

def main():
    article = analyze()
    with open('result47.txt', 'w', encoding='utf8') as w:
        for v, pairList in enumPattern(article):
            w.write('{}\t{}\t{}\n'.format(v, ' '.join([x.particle for x in pairList]), \
                ' '.join([x.paragraph for x in pairList])))

if __name__ == '__main__':
    main()

ソースはGitHubでも公開しています。


■簡単な説明

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

findVerbs
前のプログラムと同じで変更ありません。


findParticles
動詞を含むChunkNoが渡ってくるので、その動詞にかかる助詞、およびその前の単語、そしてその助詞を含む分節を列挙しています。例えば以下のようなものを列挙します。
見当	が	見当が
だけ	は	いた事だけは
決心	を	決心を
降参	を	半分降参を
ここでは、助詞が「を」で無いものも抜き出しています。

enumPattern

findParticlesで得られた結果の中から、「サ変接続名詞 + を」の場合は、「サ変接続名詞 + を + 動詞」を組み立て、それ以外は、「述語に係る助詞」「 述語に係る文節」をクラスPairとしてまとめてリストに入れています。タプルでも良いかなと思ったのですが、名前がついていたほうがわかりやすいだろうと思ってクラスを定義しました。Pairという名前がちょっとイケてないですが... 

文節は助詞の並び順と揃えよ とのことなので、このPairのリストを並び替えしています。
結果、以下のような結果を列挙しています。
昼寝をする	(が,彼が)
迫害を加える	(て,追い廻して)
生活をする	(が,我等猫族が) (を, 愛を)

main
main関数では、enumPatternが列挙した結果を指定された書式でファイルに出力しています。
久しぶりにリスト内包表記を使いました。map使うという手もありますが、 リスト内包表記のほうがlambdaキーワードが無い分わかりやすいかな。

■ 結果

先頭の30個を載せます。
決心をする	と	こうと
返報をやる	んで	偸んで
昼寝をする	が	彼が
迫害を加える	て	追い廻して
生活をする	が を	我等猫族が 愛を
投書をする	て へ	やって ほととぎすへ
話をいる	に	時に
昼寝をいる	て	出て
欠伸をする	から て て	なったから して 押し出して
報道をする	に	耳に
御馳走をてる	と	見ると
雑談をいる	ながら は	寝転びながら 黒は
呼吸を飲み込む	から	なってから
思案を定める	と は	若くはないと 吾輩は
御馳走をあるく	って て	猟って なって
放蕩をする	が	ものだからが
放蕩をする	も	云うよりも
写生をいる	に従って	忠告に従って
写生をする	から	しから
対話を聞く	で	椽側で
降参をする	と は	相違ないなと 主人は
苦心をする	から	さっきから
勉強をいる	たり て と	開いたり 行列して 見ると
存在をられる	から まで	世間から 今まで
談話を聞く	が	牡蠣的主人が
往来を通る	と	見ると
決心をする	が	考え込んでいたが
間食をする	で	忍んで
我儘をする	が	他人が
返事をする	と	利かないのだよと
  

Posted by gushwell at 22:00Comments(0)Python

2018年05月27日

言語処理100本ノックでPython入門 #46 - 動詞の格フレーム情報



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

■ 問題
46. 動詞の格フレーム情報の抽出
45のプログラムを改変し,述語と格パターンに続けて項(述語に係っている文節そのもの)をタブ区切り形式で出力せよ.45の仕様に加えて,以下の仕様を満たすようにせよ.
  • 項は述語に係っている文節の単語列とする(末尾の助詞を取り除く必要はない) 
  • 述語に係る文節が複数あるときは,助詞と同一の基準・順序でスペース区切りで並べる 
「吾輩はここで始めて人間というものを見た」という例文(neko.txt.cabochaの8文目)を考える. この文は「始める」と「見る」の2つの動詞を含み,「始める」に係る文節は「ここで」,「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は,次のような出力になるはずである.
始める  で      ここで
見る    は を   吾輩は ものを


■ 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

def findVerbs(sentence):
    for chunk in sentence:
        for m in chunk.morphs:
            if m.pos == '動詞':
                yield m, chunk.number
                break

def findParticles(sentence, chunkNo):
    for chunk in sentence:
        if chunk.dst == chunkNo:
            for m in reversed(chunk.morphs):
                if m.pos == '助詞':
                    yield m, chunk.concatMorphs()
                    break

def enumPattern(article):
    for sentence in article:
        for v, num in findVerbs(sentence):
            particlesList = []
            paragraphList = []
            for part, para in findParticles(sentence, num):
                particlesList.append(part.surface)
                paragraphList.append(para)
            if particlesList:
                yield v.base, sorted(particlesList, key=lambda x: x), \
                              sorted(paragraphList, key=lambda x: x)

def main():
    article = analyze()
    with open('result46.txt', 'w', encoding='utf8') as w:
        for v, particles, paragraphs in enumPattern(article):
            w.write('{}\t{}\t{}\n'.format(v, ' '.join(particles), ' '.join(paragraphs)))

if __name__ == '__main__':
    main() 

ソースコードは、GitHubで公開しています。

■ 簡単なコードの説明

前回から変更したのは、findParticles関数、enumPattern関数、main関数の3つです。 なお、今回も新しく仕入れた情報は無かったです。

findParticles関数では、タプルを返すように変更します。返すのは、助詞とその助詞を含むchunkです。

enumPattern関数では、findParticles関数から返される情報を使って、項(述語に係っている文節そのもの)のリスト(paragraphList)も返すようにします。 つまり3つの要素を持ったタプルを返しています。

main関数では、enumPatternが返す項も含めてファイルに出力するようにします。 

■ 結果

出力結果の先頭部分だけを掲載します。

生れる	で	どこで
つく	か が	生れたか 見当が
泣く	で	所で
する	て は	いた事だけは 泣いて
始める	で	ここで
見る	は を	ものを 吾輩は
聞く	で	あとで
捕える	を	我々を
煮る	て	捕えて
食う	て	煮て
思う	から	なかったから
載せる	に	掌に
持ち上げる	て と	スーと 載せられて
ある	が	感じが
落ちつく	で	上で
見る	て を	落ちついて 顔を
見る	の	ものの
思う	と	ものだと
残る	が でも	今でも 感じが
する	をもって	第一毛をもって
する	が	顔が
逢う	も	猫にも
出会う	も	一度も
する	が	真中が
吹く	から を	ぷうぷうと煙を 中から
弱る	て	咽せぽくて
飲む	の	人間の
知る	は	事は
坐る	で に	心持に 裏で
おる	て	坐って
  
Posted by gushwell at 22:30Comments(0)Python

2018年05月23日

言語処理100本ノックでPython入門 #45 - 動詞の格パターンの抽出



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

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

45. 動詞の格パターンの抽出
今回用いている文章をコーパスと見なし,日本語の述語が取りうる格を調査したい. 動詞を述語,動詞に係っている文節の助詞を格と考え,述語と格をタブ区切り形式で出力せよ. ただし,出力は以下の仕様を満たすようにせよ.
  • 動詞を含む文節において,最左の動詞の基本形を述語とする 
  • 述語に係る助詞を格とする 
  • 述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる 
「吾輩はここで始めて人間というものを見た」という例文(neko.txt.cabochaの8文目)を考える. この文は「始める」と「見る」の2つの動詞を含み,「始める」に係る文節は「ここで」,「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は,次のような出力になるはずである.
始める  で
見る    は を
このプログラムの出力をファイルに保存し,以下の事項をUNIXコマンドを用いて確認せよ.
  • コーパス中で頻出する述語と格パターンの組み合わせ 
  • 「する」「見る」「与える」という動詞の格パターン(コーパス中で出現頻度の高い順に並べよ)

■ 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


def findVerbs(sentence):
    for chunk in sentence:
        for m in chunk.morphs:
            if m.pos == '動詞':
                yield m, chunk.number
                break

def findParticles(sentence, chunkNo):
    for chunk in sentence:
        if chunk.dst == chunkNo:
            for m in reversed(chunk.morphs):
                if m.pos == '助詞':
                    yield m
                    break

def enumPattern(article):
    for sentence in article:
        for v, num in findVerbs(sentence):
            lst = []
            for p in sorted(findParticles(sentence, num), key=lambda x: x.surface):
                lst.append(p.surface)
            if lst:
                yield v.base, lst

def main():
    article = analyze()
    with open('result45.txt', 'w', encoding='utf8') as w:
        for v, particles in enumPattern(article):
            w.write('{}\t{}\n'.format(v, ' '.join(particles)))

if __name__ == '__main__':
    main()


ソースコードは、GitHubで公開しています。

■ 簡単なコードの説明 

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

問題文が随分と複雑になってきました。そのぶんコード量も増えています。ただし、問題を解くのに新しい知識は特になかったです。

findVerbs は、センテンスから動詞のMorph(形態素)を列挙する関数。
列挙する際は、そのMorphを含むChunk(文節)に振った番号も一緒に列挙。chunkオブジェクトには、numberを付加してあるので、それを利用しています。 

findParticlesは、sentenceとChunkの番号を引数で受け取って、動詞に係っている助詞(Morph)を列挙しています。
動詞に係る助詞が一つのChunkの中に複数ある場合を考慮して、Chunk.Morphsを逆順に操作して、最初に見つかった助詞を採用しています。 
Chunk.Morphsを逆順を求めるのに、reversed関数を使っています。 reverseという関数もあるみたいですけど、こちらはリストそのものの順序を変えてしまうので、ここでは使えないです。 

enumPattern は、findVerbs findParticles を使い、文章全体から「述語と助詞」を列挙しています。 この時、助詞が複数ある場合は、sortedを使って並び替えています。 

なお、動詞に係る助詞がない場合は、出力しないようにしています。 


■ 結果

結果の一部だけを示します。
生れる	で
つく	か が
泣く	で
する	て は
始める	で
見る	は を
聞く	で
捕える	を
煮る	て
食う	て
思う	から
載せる	に
持ち上げる	て と
ある	が
落ちつく	で
見る	て を
見る	の
思う	と
残る	が でも
する	をもって
する	が
逢う	も
出会う	も
する	が
吹く	から を
弱る	て
飲む	の
知る	は
坐る	で に
おる	て
  
Posted by gushwell at 22:00Comments(0)Python

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()

ソースコードは、GitHubで公開しています。


■ 結果 

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

result44

 
  
Posted by gushwell at 22:10Comments(0)Python

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()

ソースコードは、GitHubで公開しています。

■ 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)Python

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()

ソースコードは、GitHubで公開しています。

■ コードの解説

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)Python