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