Port 53

明日のための技術メモ

言語処理100本ノック 2020 第4章 前半

nlp100.github.io

第4章 形態素解析の前半(30-34まで)解説書きます。
ますます言語処理っぽくなってきた。MeCabとお友達になる章。
ここだけの話、「吾輩は猫である」読んだことなかった。
ファイルの最後までスクロールして、衝撃のオチだった。

第4章 後半はこちら

目次

準備

吾輩は猫である」(夏目漱石)の全文がneko.txtで渡されるので、
コマンドラインMeCabを使って形態素解析をかけてその結果をneko.txt.mecabとして保存する。

私の環境はMacOS Catalina(10.15.4)なので、まずはMeCabのインストールをして、
Pythonから呼び出せるようにする。

$ brew install mecab
$ brew install mecab-ipadic
$ pip install mecab-python3

続いて、形態素解析をかけてその結果ファイルneko.txt.mecabを得る

$ mecab neko.txt > neko.txt.mecab

neko.txt.mecabの中身は以下の通り(結構長いので最初の方だけ。)

一   名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
  記号,空白,*,*,*,*, , , 
吾輩  名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
猫 名詞,一般,*,*,*,*,猫,ネコ,ネコ
で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある  助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
。 記号,句点,*,*,*,*,。,。,。
EOS
名前  名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
まだ  副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
無い  形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。 記号,句点,*,*,*,*,。,。,。
EOS

行ごとの出力形式は、以下の通りになっている。

表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音

詳しくは、以下のドキュメントを参照

taku910.github.io

30. 形態素解析結果の読み込み

f:id:saturn-glave:20200512230819p:plain

結果を読み込まないと何にも使えない。4章で避けて通れない問題。
MeCabの出力形式を見ると、Tabとカンマが同居しているので、pandas使いにくい。
素直にファイルオープンして、一行ずつ読み込みながら改行コードやTabを処理していく。
なお、行の終わりにEOSが混ざっているので、こいつは含めてはいけない。
行の終わりの目印として活用させてもらった。

次の問題からはこの部分を関数化して使う。

解答

# coding:utf-8
# mecab neko.txt > neko.txt.mecab
# 表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音
path = './neko.txt.mecab'
neko = []
sentence = []

with open(path) as f:
    for line in f:
        # [表層系, それ以外]として一行の中身をリスト化
        tmp_line = line.rstrip('\n').split('\t')
        # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない
        if len(tmp_line) == 1 and len(sentence) != 0:
            neko.append(sentence)
            sentence = []
        elif len(tmp_line) == 2:
            # 品詞以降の部分をカンマ区切りする
            tmp = tmp_line[1].split(',')
            morpheme = {'surface': tmp_line[0],
                        'base': tmp[6],
                        'pos': tmp[0],
                        'pos1': tmp[1]}
            sentence.append(morpheme)


print(neko)

結果は以下の通り。全部出すと長いので最後の1文だけ抜粋。
ネタバレではない(多分)

$ python CH4-30.py
 [{'surface': 'ありがたい', 'base': 'ありがたい', 'pos': '形容詞', 'pos1': '自立'}, {'surface': 'ありがたい', 'base': 'ありがたい', 'pos': '形容詞', 'pos1': '自立'}, {'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}]]

31. 動詞

f:id:saturn-glave:20200512230834p:plain

最初の問題で各単語を辞書に入れたので、品詞(pos)と表層(surface)に注目し、
二重ループで抜いていくだけ。

解答

# coding:utf-8


def neko_load():
    path = './neko.txt.mecab'
    neko = []
    sentence = []

    with open(path) as f:
        for line in f:
            # [表層系, それ以外]として一行の中身をリスト化
            tmp_line = line.rstrip('\n').split('\t')
            # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない
            if len(tmp_line) == 1 and len(sentence) != 0:
                neko.append(sentence)
                sentence = []
            elif len(tmp_line) == 2:
                # 品詞以降の部分をカンマ区切りする
                tmp = tmp_line[1].split(',')
                morpheme = {'surface': tmp_line[0],
                            'base': tmp[6],
                            'pos': tmp[0],
                            'pos1': tmp[1]}
                sentence.append(morpheme)
    return(neko)


def main():
    cat = neko_load()
    verb = []

    for line in cat:
        for word in line:
            if word['pos'] == '動詞':
                # print(word['pos'], word['surface'])
                verb.append(word['surface'])

    print(verb)


if __name__ == "__main__":
    main()

出力はこんな感じ。長いので最初の7つだけ。

$ python CH4-31.py
['生れ', 'つか', 'し', '泣い', 'し', 'いる', '始め']
 

32. 動詞の原形

f:id:saturn-glave:20200512230847p:plain

31.とほぼ一緒。今度は品詞(pos)と原型(base)に注目し、
二重ループで抜いてあげる。

解答

# coding:utf-8


def neko_load():
    path = './neko.txt.mecab'
    neko = []
    sentence = []

    with open(path) as f:
        for line in f:
            # [表層系, それ以外]として一行の中身をリスト化
            tmp_line = line.rstrip('\n').split('\t')
            # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない
            if len(tmp_line) == 1 and len(sentence) != 0:
                neko.append(sentence)
                sentence = []
            elif len(tmp_line) == 2:
                # 品詞以降の部分をカンマ区切りする
                tmp = tmp_line[1].split(',')
                morpheme = {'surface': tmp_line[0],
                            'base': tmp[6],
                            'pos': tmp[0],
                            'pos1': tmp[1]}
                sentence.append(morpheme)
    return(neko)


def main():
    cat = neko_load()
    verb = []

    for line in cat:
        for word in line:
            if word['pos'] == '動詞':
                verb.append(word['base'])

    print(verb)


if __name__ == "__main__":
    main()

出力はこんな感じ。長いのでこれも最初の7つだけ。

$ python CH4-32.py
['生れる', 'つく', 'する', '泣く', 'する', 'いる', '始める']

33. 「AのB」

f:id:saturn-glave:20200512230859p:plain

言語処理的には、俺のベーカリー のように、名詞 + 助詞(の) + 名詞 になっているものを抜き出す。
私はPython3.8.2でやっているので、満を持してセイウチ演算子を使ってみた。

ポイント

  • セイウチ演算子:=は、変数への代入→変数として使用を一気に行える
  • セイウチ演算子で代入した変数を使う時は、変数部分を必ず括弧で囲むこと
if ( n := len(list) ) == 3:

解答

セイウチ演算子を、リストの長さを取ってそれを変数に格納→使用する部分で発動した。
これ便利だわ。

# coding:utf-8


def neko_load():
    path = './neko.txt.mecab'
    neko = []
    sentence = []

    with open(path) as f:
        for line in f:
            # [表層系, それ以外]として一行の中身をリスト化
            tmp_line = line.rstrip('\n').split('\t')
            # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない
            if len(tmp_line) == 1 and len(sentence) != 0:
                neko.append(sentence)
                sentence = []
            elif len(tmp_line) == 2:
                # 品詞以降の部分をカンマ区切りする
                tmp = tmp_line[1].split(',')
                morpheme = {'surface': tmp_line[0],
                            'base': tmp[6],
                            'pos': tmp[0],
                            'pos1': tmp[1]}
                sentence.append(morpheme)
    return(neko)


def main():
    cat = neko_load()
    a_no_b = []
    # print(cat[0])
    for line in cat:
        if (n := len(line)) >= 3:
            for i in range(n - 2):
                if line[i]['pos'] == '名詞' and (line[i + 1]['pos'] == '助詞' and line[i + 1]['surface'] == 'の') and line[i + 2]['pos'] == '名詞':
                    a_no_b.append(line[i]['surface'] + 'の' + line[i + 2]['surface'])

    print(a_no_b)


if __name__ == "__main__":
    main()

出力は以下の通り。長いので最初だけ。

$ python CH4-33.py
['彼の掌', '掌の上', '書生の顔', 'はずの顔', '顔の真中', '穴の中', '書生の掌

参考記事

qiita.com

34. 名詞の連節

f:id:saturn-glave:20200512230912p:plain

連続して名詞が出てくるところを抜いてくる。名詞単体だけのところは除外 ひたすらバグを出しまくった。ちょっときつかった。

解答スクリプト

# coding:utf-8


# coding:utf-8


def neko_load():
    path = './neko.txt.mecab'
    neko = []
    sentence = []

    with open(path) as f:
        for line in f:
            # [表層系, それ以外]として一行の中身をリスト化
            tmp_line = line.rstrip('\n').split('\t')
            # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない
            if len(tmp_line) == 1 and len(sentence) != 0:
                neko.append(sentence)
                sentence = []
            elif len(tmp_line) == 2:
                # 品詞以降の部分をカンマ区切りする
                tmp = tmp_line[1].split(',')
                morpheme = {'surface': tmp_line[0],
                            'base': tmp[6],
                            'pos': tmp[0],
                            'pos1': tmp[1]}
                sentence.append(morpheme)
    return(neko)


def main():
    cat = neko_load()
    # print(cat)
    ans = []

    for line in cat:
        if (n := len(line)) >= 2:
            tmp = ''
            count = 0
            # 名詞が連続している部分を探す
            for i in range(n):
                if line[i]['pos'] == '名詞':
                    tmp += line[i]['surface']
                    count += 1
                # 名詞が2つ以上続いた時だけ答えに入れる
                elif line[i]['pos'] != '名詞' and tmp != '' and count >= 2:
                    ans.append(tmp)
                    tmp = ''
                    count = 0
                # 名詞がひとつ来てその次名詞以外だった場合はリセット
                elif line[i]['pos'] != '名詞' and tmp != '' and count == 1:
                    tmp = ''
                    count = 0

    print(ans)


if __name__ == "__main__":
    main()

出力は以下の通り。最初だけ抜粋。

 ['人間中', '一番獰悪', '時妙', '一毛', 'その後猫', '一度', 'ぷうぷうと煙'