2018年06月10日

言語処理100本ノックでPython入門 #50 - 英文テキスト 分区切り



本日から、言語処理100本ノック 2015の第6章に入ります。

まずは、問題50を解きます。

■ 問題
第6章: 英語テキストの処理
英語のテキスト(nlp.txt)に対して,以下の処理を実行せよ.
50. 文区切り
(. or ; or : or ? or !) → 空白文字 → 英大文字というパターンを文の区切りと見なし,入力された文書を1行1文の形式で出力せよ.

■ Pythonのコード
import re

def enumSentence():
    with open('nlp.txt', 'r', encoding='utf8') as fin:
        for line in fin:
            nl = re.sub(r'(\.|;|:|\?|!)(\s+)([A-Z])', r'\1\n\3', line)
            ss = re.split(r'\n', nl)
            for s in filter(lambda w: len(w) > 0, ss):
                yield s

def main():
    with open('result501.txt', 'w', encoding='utf8') as w:
        for sentence in enumSentence():
            w.write(sentence + '\n')

if __name__ == '__main__':
    main()

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

■ ちょっと説明

matchで1文を抜き出そうとしたのですが、文の途中に.がある場合や、引用符の中に、(. or ; or : or ? or !) → 空白文字 → 英大文字 という パターンがある場合に正規表現がうまく書けませんでした。

それなので、考えるのやめて安易な方法でやることにしました。

sub関数で文の区切りの空白を\nに置き換えて、\nでsplitして、1行を取り出すという戦術。

でも、この時、subじゃなくて、reaplceと書いてうまくいかずに、なぜ?って思ってしまいました。 substitute の略だと思うけど、どうも忘れてしまいます。 グループ化した文字列を参照するときは、\1, \2, \3 など使います。

それと、正規表現書く時は、rで始める文字列にしたほうが使いやすいですね。 


それと久しぶりにfilter使ってみました。たまに使わないと忘れてしまいそうです。
for s in filter(lambda w: len(w) > 0, ss):
    yield s
ちなみに、入力ファイルの先頭行の
Natural language processing
は、(. or ; or : or ? or !)で終わってないけど、これも文として扱うことにします。


■結果

先頭部分だけ載せます。画面だと途中で改行されてますが、ご容赦を。

Natural language processing
From Wikipedia, the free encyclopedia
Natural language processing (NLP) is a field of computer science, artificial intelligence, and linguistics concerned with the interactions between computers and human (natural) languages.
As such, NLP is related to the area of humani-computer interaction.
Many challenges in NLP involve natural language understanding, that is, enabling computers to derive meaning from human or natural language input, and others involve natural language generation.
History
The history of NLP generally starts in the 1950s, although work can be found from earlier periods.
In 1950, Alan Turing published an article titled "Computing Machinery and Intelligence" which proposed what is now called the Turing test as a criterion of intelligence.
The Georgetown experiment in 1954 involved fully automatic translation of more than sixty Russian sentences into English.
The authors claimed that within three or five years, machine translation would be a solved problem.
...
  

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

2018年06月07日

言語処理100本ノックでPython入門 #49 - 名詞間の係り受けパスの抽出



今日は、言語処理100本ノック 2015の問題49です。
「第5章:係り受け解析」最後の問題になりました。

まだ、Pythonの文法で知らないことも結構あると思いますが、それなりのコードは書けるようになったかなと思います。やりたいアルゴリズムを実装する上で、困ることはほとんどなくなったように思います。あとはいろんなライブラリの使い方を覚えて行ければ...

■ 問題
49. 名詞間の係り受けパスの抽出
文中のすべての名詞句のペアを結ぶ最短係り受けパスを抽出せよ.ただし,名詞句ペアの文節番号が iとj(i<j)のとき,係り受けパスは以下の仕様を満たすものとする.
  • 問題48と同様に,パスは開始文節から終了文節に至るまでの各文節の表現(表層形の形態素列)を"->"で連結して表現する 文節
  • iとjに含まれる名詞句はそれぞれ,XとYに置換する
また,係り受けパスの形状は,以下の2通りが考えられる.
  • 文節iから構文木の根に至る経路上に文節jが存在する場合: 文節iiから文節jjのパスを表示
  • 上記以外で,文節iiと文節jjから構文木の根に至る経路上で共通の文節kkで交わる場合: 文節iiから文節kkに至る直前のパスと文節jjから文節kに至る直前までのパス,文kkの内容を"|"で連結して表示
例えば,「吾輩はここで始めて人間というものを見た。」という文(neko.txt.cabochaの8文目)から,次のような出力が得られるはずである.
Xは | Yで -> 始めて -> 人間という -> ものを | 見た
Xは | Yという -> ものを | 見た
Xは | Yを | 見た
Xで -> 始めて -> Y
Xで -> 始めて -> 人間という -> Y
Xという -> Y

■ 新たに得たPythonの知識

Python はメソッドのオーバーロードが使えない。

intの最大値
import sys
sys.maxsize

深いコピーができる
import copy

copy.deepcopy(x)

■ 簡単な説明

Chunkクラスには、hasNounメソッドを追加。

analyze関数は今回は変更。 EOSを明確に示すために、sentenseの最後には、Chunk(sys.maxsize, -1) を追加しました。これがEOSを意味します。
 sentence.append(Chunk(sys.maxsize, -1))
これに伴い、これまでは、dst:-1の時は、次のChunkはないことを示していましたが、最後のChunkのdstは、EOSを表すChunkへの番号をセットするようにしました。
    if destNo == -1:
        destNo = num + 1
各関数の動作については、コードにコメントを付けたのでそれを読んでください。
なお、主要な変数は以下の通りです。
  • 変数ciは、文節i(Chunkクラス) を表す
  • 変数cjは、文節j(Chunkクラス) を表す
  • 変数ckは、文節k(Chunkクラス) を表す
  • 変数kは、文節kのインデックスを表す (sentenceのインデックス) 

ただ、あまり効率の良いコードとは言えないです。改良の余地hじゃありますが、まあ良しとします。


■ Pythonのコード
import re
import sys
import copy
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 hasNoun(self):
        for m in self.morphs:
            if m.pos == '名詞':
                return True
        return False

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

# 文章をsentenceに分割する。ひとつのsentenceは、Chunk(文節)の配列からなる
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'))
                if destNo == -1:
                    destNo = num + 1
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    sentence.append(Chunk(sys.maxsize, -1))
                    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)のペアを列挙する
def enumPairs(sentence):
    count = len(sentence)
    for i in range(count):
        for j in range(i+1, count):
            if sentence[i].hasNoun() and sentence[j].hasNoun():
                yield sentence[i], sentence[j]

# ciからのパスの中にcjが含まれているかを調べる
def containsOther(ci, cj, sentence):
    curr = ci
    while curr.dst >= 0:
        if curr == cj:
            return True
        curr = sentence[curr.dst]
    return False

# ciからのパスとcjからのパスが出会う番号を返す
def connectedPoint(ci, cj, sentence):
    k = ci.dst
    while k >= 0:
        curr = cj
        while curr.dst >= 0:
            if curr.number == k:
                return k
            curr = sentence[curr.dst]
        k = sentence[k].dst
    return -1

# chunkの名詞の部分を to で指定した値に変更(オリジナルは変更しない)
def replaceTo(to, chunk):
    dup = copy.deepcopy(chunk)
    for m in dup.morphs:
        if m.pos == '名詞':
            m.surface = to
            break
    return dup

# 'Xで -> 始めて -> 人間という -> Y' というパス文字列を生成
def makePath1(ci, cj, sentence):
    path = []
    curr = replaceTo('X', ci)
    while curr.number < cj.number:
        path.append(curr.concatMorphs())
        curr = sentence[curr.dst]
    path.append('Y')
    return '{}'.format(' -> '.join(path))

# 'Xは | Yという -> ものを | 見た' という形式のパス文字列を生成
def makePath2(ci, cj, ck, sentence):
    ci = replaceTo('X', ci)
    cj = replaceTo('Y', cj)
    p1 = ci.concatMorphs()
    list1 = []
    curr = cj
    while curr.number < ck.number:
        list1.append(curr.concatMorphs())
        curr = sentence[curr.dst]
    p2 = '{}'.format(' -> '.join(list1))
    list2 = []
    curr = ck
    while curr.dst > 0:
        list2.append(curr.concatMorphs())
        curr = sentence[curr.dst]
    p3 = '{}'.format(' -> '.join(list2))
    return '{} | {} | {}'.format(p1, p2, p3)

# 1センテンスから、パスを列挙する
def extractPaths(sentence):
    for ci, cj in enumPairs(sentence):
        if containsOther(ci, cj, sentence):
            yield makePath1(ci, cj, sentence)
        else:
            k = connectedPoint(ci, cj, sentence)
            if k > 0:
                yield makePath2(ci, cj, sentence[k], sentence)

# ファイルを読み込み、1センテンス毎に、extractPathsを呼び出し、Path(複数)を取り出し、ファイルに出力
def main():
    article = analyze()
    with open('result49.txt', 'w', encoding='utf8') as w:
        for sentence in article:
            for path in extractPaths(sentence):
                w.write(path + '\n')

if __name__ == '__main__':
    main()

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

■ 結果

先頭の10センテンスの結果を掲載します。
Xは -> Y
Xで | Yが | つかぬ
Xでも -> 薄暗い -> Y
Xでも | Y | 泣いて -> 記憶している
Xでも | Yだけは | 記憶している
Xでも -> 薄暗い -> 所で -> 泣いて -> Y
Xで | Y | 泣いて -> 記憶している
Xで | Yだけは | 記憶している
Xで -> 泣いて -> Y
X | Yだけは | 記憶している
X -> 泣いて -> Y
Xだけは -> Y
Xは | Yで -> 始めて -> 人間という -> ものを | 見た
Xは | Yという -> ものを | 見た
Xは | Yを | 見た
Xで -> 始めて -> Y
Xで -> 始めて -> 人間という -> Y
Xという -> Y
Xで | Yは | 種族であったそうだ
Xで | Yという -> 人間中で | 種族であったそうだ
Xで | Y中で | 種族であったそうだ
Xで | Y -> 獰悪な | 種族であったそうだ
Xで | Yな | 種族であったそうだ
Xで -> 聞くと -> Y
Xは | Yという -> 人間中で | 種族であったそうだ
Xは | Y中で | 種族であったそうだ
Xは | Y -> 獰悪な | 種族であったそうだ
Xは | Yな | 種族であったそうだ
Xは -> Y
Xという -> Y
Xという | Y -> 獰悪な | 種族であったそうだ
Xという | Yな | 種族であったそうだ
Xという -> 人間中で -> Y
X中で | Y -> 獰悪な | 種族であったそうだ
X中で | Yな | 種族であったそうだ
X中で -> Y
X -> Y
X -> 獰悪な -> Y
Xな -> Y
Xというのは | Yを -> 捕えて -> 煮て -> 食うという | 話である
Xというのは -> Y
Xを -> 捕えて -> 煮て -> 食うという -> Y
Xは | Yという -> 考も | なかったから -> 思わなかった
Xは | Yも | なかったから -> 思わなかった
Xという -> Y
Xの -> Y
Xの | Yと | 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
Xの -> 掌に -> 載せられて -> 持ち上げられた -> Y
Xの -> 掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> Y
Xに | Yと | 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
Xに -> 載せられて -> 持ち上げられた -> Y
Xに -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> Y
Xと -> 持ち上げられた -> Y
Xと -> 持ち上げられた -> 時 -> フワフワした -> Y
X -> フワフワした -> Y
  
Posted by gushwell at 08:00Comments(0)Python

2018年06月04日

言語処理100本ノックでPython入門 #48 - 名詞から根へのパスの抽出



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

■ 問題
48. 名詞から根へのパスの抽出
文中のすべての名詞を含む文節に対し,その文節から構文木の根に至るパスを抽出せよ. ただし,構文木上のパスは以下の仕様を満たすものとする.
  • 各文節は(表層形の)形態素列で表現する 
  • パスの開始文節から終了文節に至るまで,各文節の表現を"->"で連結する 
「吾輩はここで始めて人間というものを見た」という文(neko.txt.cabochaの8文目)から,次のような出力が得られるはずである.
吾輩は -> 見た
ここで -> 始めて -> 人間という -> ものを -> 見た
人間という -> ものを -> 見た
ものを -> 見た

■ 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 findNouns(sentence):
    for chunk in sentence:
        for m in chunk.morphs:
            if m.pos == '名詞':
                yield chunk
                break

def makePath(sentence, chunk):
    curr = sentence[chunk.number]
    path = []
    while curr.dst >= 0:
        path.append(curr.concatMorphs())
        curr = sentence[curr.dst]
    path.append(curr.concatMorphs())
    return path

def enumPath(article):
    for sentence in article:
        for chunk in findNouns(sentence):
            path = makePath(sentence, chunk)
            if len(path) >= 2:
                yield path

def main():
    article = analyze()
    with open('result48.txt', 'w', encoding='utf8') as w:
        for path in enumPath(article):
            w.write('{}\n'.format(' -> '.join(path)))

if __name__ == '__main__':
    main()

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

■ コードの簡単な説明


Morphクラス、Chunkクラス、analyze関数は変更ありません。
問題47は、結構複雑なコードで書いているうちに自分が何をやりたかったのか判らなくなることがありましたが、 この問題48は、問題47に比べて複雑さもそれほどではなかったです。

enumPath関数
analyze関数で得たarticleから、名詞を含む文節に対し,その文節から構文木の根に至るパスを抽出する関数

findNouns関数
enumPathの下請け関数
1つの文から、名詞を含む文節(Chunk)を列挙する。 makePath関数

enumPathの下請け関数
findNounsで得た文節から終了文節に至るまでのパスを作成するする関数

main関数
enumPathで得たパスを-> で連結しファイルに出力する


■ ひとり言

そういえば、初めて while 文を使ったかも。
makePathの中で使っています。while文は他の言語にもあるので、何も難しいことはないですね。

各文節を"->"で連結するのに、joinを使っていますが、いまだに、以下の書き方に慣れないです。
' -> '.join(path)

■ 結果
吾輩は -> 猫である
名前は -> 無い
どこで -> 生れたか -> つかぬ
見当が -> つかぬ
何でも -> 薄暗い -> 所で -> 泣いて -> 記憶している
所で -> 泣いて -> 記憶している
ニャーニャー -> 泣いて -> 記憶している
いた事だけは -> 記憶している
吾輩は -> 見た
ここで -> 始めて -> 人間という -> ものを -> 見た
人間という -> ものを -> 見た
ものを -> 見た
あとで -> 聞くと -> 種族であったそうだ
それは -> 種族であったそうだ
書生という -> 人間中で -> 種族であったそうだ
人間中で -> 種族であったそうだ
一番 -> 獰悪な -> 種族であったそうだ
獰悪な -> 種族であったそうだ
書生というのは -> 話である
我々を -> 捕えて -> 煮て -> 食うという -> 話である
当時は -> なかったから -> 思わなかった
何という -> 考も -> なかったから -> 思わなかった
考も -> なかったから -> 思わなかった
彼の -> 掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
スーと -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
時 -> フワフワした -> 感じが -> あったばかりである
感じが -> あったばかりである
掌の -> 上で -> 落ちついて -> 見たのが -> 人間という -> ものの -> 見始であろう
  
Posted by gushwell at 07:30Comments(0)Python

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