2018年06月21日

言語処理100本ノックでPython入門 #53 - Stanford Core NLPの出力から単語抽出

今日は、言語処理100本ノック 2015の第6章・問題53です。

■ 問題 
53. Tokenization
Stanford Core NLPを用い,入力テキストの解析結果をXML形式で得よ.また,このXMLファイルを読み込み,入力テキストを1行1単語の形式で出力せよ.

■ Stanford Core NLPをダウンロード


以下のURLからStanford Core NLPをダウンロードします。

https://stanfordnlp.github.io/CoreNLP/


これを、corenlpというフォルダに配置します。

■ Stanford Core NLPを実行

以下のようなコマンドを投入し、Stanford Core NLPを起動します。
java -cp "/Users/xxxxxxx/corenlp/*" -Xmx3g edu.stanford.nlp.pipeline.StanfordCoreNLP -annotators tokenize,ssplit,pos,lemma,ner -file nlp.txt

これで、nlp.txt.xmlファイルが作成されます。

ちなみに、Javaのバージョンは、java version "1.8.0_101" です。

でも、作成したnlp.txt.xml見ると文の判断が正しくないので、入力ファイルを手で修正。
入力ファイルは、基本1行1文なのだけれど、Stanford Core NLPは、ピリオドが行の単位と認識するようなので、タイトル行の最後にもピリオドを付加しています。4か所くらいあったかな。

得られたXMLファイルの先頭部分を載せておきます。
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="CoreNLP-to-HTML.xsl" type="text/xsl"?>
<root>
  <document>
    <docId>nlp.txt</docId>
    <sentences>
      <sentence id="1">
        <tokens>
          <token id="1">
            <word>Natural</word>
            <lemma>natural</lemma>
            <CharacterOffsetBegin>0</CharacterOffsetBegin>
            <CharacterOffsetEnd>7</CharacterOffsetEnd>
            <POS>JJ</POS>
            <NER>O</NER>
            <Speaker>PER0</Speaker>
          </token>
          <token id="2">
            <word>language</word>
            <lemma>language</lemma>
            <CharacterOffsetBegin>8</CharacterOffsetBegin>
            <CharacterOffsetEnd>16</CharacterOffsetEnd>
            <POS>NN</POS>
            <NER>O</NER>
            <Speaker>PER0</Speaker>
          </token>
          <token id="3">
            <word>processing</word>
            <lemma>processing</lemma>
            <CharacterOffsetBegin>17</CharacterOffsetBegin>
            <CharacterOffsetEnd>27</CharacterOffsetEnd>
            <POS>NN</POS>
            <NER>O</NER>
            <Speaker>PER0</Speaker>
          </token>

■ XMLファイルを操作する

XMLファイルを読み込むには、xml.etreeライブラリを使います。
from xml.etree import ElementTree
次に、入力ファイルを指定し、nlp.txt.xmlをパースします。
xdoc = ElementTree.parse('nlp.txt.xml')
これで、読み込んだ結果がtree構造として返されます。

続いて、ルートを取得します。
root = xdoc.getroot()
このrootを使い、必要な要素を取り出していきます。
sentences = root.find('document/sentences')
で、rootの直下から、パスを指定してXML elementを取得。

さらに、
for e in sentences.findall('sentence/tokens/token/word'):
        yield e
で、word要素をすべて取り出します。 実際のテキストは、
e.text
で取得できます。


■ Pythonのコード

作成したPythonのコードです。
from xml.etree import ElementTree

def getWords():
    xdoc = ElementTree.parse('nlp.txt.xml')
    root = xdoc.getroot()
    sentences = root.find('document/sentences')
    for e in sentences.findall('sentence/tokens/token/word'):
        yield e


def main():
    with open('result53.txt', 'w', encoding='utf8') as w:
        for word in getWords():
            w.write(f'{word.text}\n')
       
if __name__ == '__main__':
    main()


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

■ 結果

結果の先頭部分を載せます。
ピリオドやカンマも抽出しています。本来は、これらは除外すべきかもしれませんが良しとします。
Natural
language
processing
.
From
Wikipedia
,
the
free
encyclopedia
.
Natural
language
processing
-LRB-
NLP
-RRB-
is
a
field
of
computer
science
,
artificial
intelligence
,
and
linguistics
concerned
  

Posted by gushwell at 07:40Comments(0)

2018年06月17日

言語処理100本ノックでPython入門 #52 - nltkでステミング

今日は、言語処理100本ノック 2015の第6章・問題52を解きます。

■ 問題
52. ステミング
51の出力を入力として受け取り,Porterのステミングアルゴリズムを適用し,単語と語幹をタブ区切り形式で出力せよ. Pythonでは,Porterのステミングアルゴリズムの実装としてstemmingモジュールを利用するとよい.

■ nltkライブラリのstemモジュールを使う


ステミングとは、単語の変化形の変化した部分を取り除く処理のことらしいです。

問題に、「stemmingモジュールを利用するとよい」とあるので、以下のようなコマンドを投入。
conda install stemming
でも、以下のエラーが出て見つかりません。
Solving environment: failed

PackagesNotFoundError: The following packages are not available from current channels:

  - stemming
そもそもこのstemmingライブラリは、python3.xに非対応らしいです。

python3.xで利用できるステミングモジュールはないかな、って調べたら、nltk というパッケージでstemmingができるらしいです。

僕が利用しているpythonのanacondaは、デフォルトでいろんなライブラリが入っているので、まずは、anacondaにあるかどうか調べます。
conda list
とやったら、
nltk                      3.2.2                    py36_0
と出てきました。これを使おうとおもいます。

from nltk import stem
で stem モジュールをimportします。

でも、pylintが以下のようなエラーを吐きます。
F0002:<class 'AttributeError'>: 'TreeRebuilder3k' object has no attribute 'visit_joinedstr'
どうも、pylintが古いみたいです。

無視してもいいのですが、せっかくなので、pylintを新しくします。
conda install -c anaconda pylint
無事、エラーが消えました。


では、stemを使って、Porterのステミングアルゴリズムで語幹を取り出します。
stemmer = stem.PorterStemmer()
stm = stemmer.stem(word)
とすれば、語幹が得られます。stemとは語幹という意味です。

ちなみに、
stemmer = stem.LancasterStemmer()
とすれば、LancasterStemmerという別のアルゴリズムも使えるみたいです。


■ Pythonのコード


以下、出来上がったPythonのコードを載せます。
from nltk import stem

def enumStem():
    stemmer = stem.PorterStemmer()
    #stemmer = stem.LancasterStemmer()
    with open('result51.txt', 'r', encoding='utf8') as fin:
        for line in fin:
            word = line.rstrip('\n')
            if word != '':
                yield word, stemmer.stem(word)

def main():
    with open('result52.txt', 'w', encoding='utf8') as w:
        for word, stm in enumStem():
            w.write(f'{word}\t{stm}\n')

if __name__ == '__main__':
    main()


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


そうだ、書き忘れていたけど、Pythonでも、C#の逐語的リテラル文字列のような書き方ができるようになったんですね。

f'{word}\t{stm}\n' 

便利になりました。


■ 結果
Natural	natur
language	languag
processing	process
From	from
Wikipedia	wikipedia
the	the
free	free
encyclopedia	encyclopedia
Natural	natur
language	languag
processing	process
NLP	nlp
is	is
a	a
field	field
of	of
computer	comput
science	scienc
artificial	artifici
intelligence	intellig
and	and
linguistics	linguist
concerned	concern
with	with
the	the
interactions	interact
between	between
computers	comput
and	and
human	human
... 以下省略
  
Posted by gushwell at 22:30Comments(0)

2018年06月12日

言語処理100本ノックでPython入門 #51 - 英文テキスト 単語の切り出し


今日は、言語処理100本ノック 2015の第6章・問題51を解きます。

■ 問題

51. 単語の切り出し
空白を単語の区切りとみなし,50の出力を入力として受け取り,1行1単語の形式で出力せよ.ただし,文の終端では空行を出力せよ.


■ Pythonのコード
import re

def enumWords():
    with open('result50.txt', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'[\s\.",:;()]+', line)
            for w in words:
                if re.match(r'^[a-zA-Z]', w):
                    yield w

def main():
    with open('result51.txt', 'w', encoding='utf8') as w:
        w.writelines([x + '\n' for x in enumWords()])

if __name__ == '__main__':
    main()


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


■ ちょっと説明

単純に空白を区切り文字としてプログラム書いて実行して結果をみると、出力ファイルの中に以下のような内容がありました。
In
1950,
Alan
Turing
published
an
article
titled
"Computing
Machinery
and
Intelligence"
which
...

なんか、常識的に考えておかしいです。
「空白を単語の区切り」とあるけど、, " ( ) ? などの記号をどうしたらよいかは、問題読んでもよくわからないんですよね。

ということで、単語の区切りとなりそうな空白以外の記号も区切り記号として書き直したのが上のコードです。

それと、今回初めて、writelinesメソッド使ってみました。これで一気に複数行をファイルに出力できます。 でも、引数の配列の各行の最後に改行入れないといけないのが面倒です。
with open('result51.txt', 'w', encoding='utf8') as w:
    w.writelines([x + '\n' for x in enumWords()])

■ 結果

先頭の30語だけ掲載します。
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
  
Posted by gushwell at 08:00Comments(0)

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)

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)

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)

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)

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)