yamakiy’s tech blog

技術系の記事をメインにやります。

言語処理100本ノック(第4章)

yamakiy.hatenadiary.jp github.com

いよいよ4章
mecabは仕事でも使ったことがあるから早めに終わらせたい

第4章: 形態素解析

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

なお,問題37, 38, 39はmatplotlibもしくはGnuplotを用いるとよい.

mecabの使い方はここ

最初にneko.txt.mecabを作成する

#!/usr/bin/python3.6
import MeCab

if __name__ == "__main__":
    m = MeCab.Tagger()
    with open("./files/neko.txt", "r") as f:
        with open("./files/neko.txt.mecab", "w") as f2:
            f2.write(m.parse(f.read()))

neko.txt.mecab

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

~~~ 中略 ~~~

られ    動詞,接尾,*,*,一段,未然形,られる,ラレ,ラレ
ぬ      助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ
。      記号,句点,*,*,*,*,。,。,。
南無阿弥陀仏    名詞,一般,*,*,*,*,南無阿弥陀仏,ナムアミダブツ,ナムアミダブツ
南無阿弥陀仏    名詞,一般,*,*,*,*,南無阿弥陀仏,ナムアミダブツ,ナムアミダブツ
。      記号,句点,*,*,*,*,。,。,。
ありがたい      形容詞,自立,*,*,形容詞・アウオ段,基本形,ありがたい,アリガタイ,アリガタイ
ありがたい      形容詞,自立,*,*,形容詞・アウオ段,基本形,ありがたい,アリガタイ,アリガタイ
。      記号,句点,*,*,*,*,。,。,。

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

形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素マッピング型)のリストとして表現せよ.第4章の残りの問題では,ここで作ったプログラムを活用せよ.

公式より、以下のMeCabは以下のフォーマットで解析を行っている
表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音

import re
import pickle
from pprint import pprint

class NekoMecab(object):
    model = []

    def __init__(self):
       self.load_model()

    # 形態素解析結果の読み込みを行う。 無ければ作成
    def load_model(self):
        if os.path.exists("./files/neko.model.pickle"):
            with open("./files/neko.model.pickle", "rb") as f:
                self.model = pickle.load(f)
        else:
            self.save_model()

    # 形態素解析結果の作成
    def save_model(self):
        with open("./files/neko.txt.mecab", "r") as f:
            for line in f:
                # 終了文字EOSが現れたら終了
                if line == "EOS\n":
                    break
                vals = re.split(r"\t|,", line)
                self.model.append({
                    "surface": vals[0], "base": vals[7], "pos": vals[1], "pos1": vals[2]
                })
        with open("./files/neko.model.pickle", "wb") as f:
            pickle.dump(self.model, f)

if __name__ == "__main__":
    nm = NekoMecab()
    pprint(nm.model)

"""
[{'base': '一', 'pos': '名詞', 'pos1': '数', 'surface': '一'},
 {'base': '\u3000', 'pos': '記号', 'pos1': '空白', 'surface': '\u3000'},
 {'base': '吾輩', 'pos': '名詞', 'pos1': '代名詞', 'surface': '吾輩'},
 {'base': 'は', 'pos': '助詞', 'pos1': '係助詞', 'surface': 'は'},
 {'base': '猫', 'pos': '名詞', 'pos1': '一般', 'surface': '猫'},
:
"""

以降も利用するとのことで、生成にも時間がかかるためpickleで保管

31. 動詞

動詞の表層形をすべて抽出せよ.

import re
import pickle
from pprint import pprint

class NekoMecab(object):
    model = []

    def __init__(self):
       self.load_model()

    # 形態素解析結果の読み込みを行う。 無ければ作成
    def load_model(self):
        if os.path.exists("./files/neko.model.pickle"):
            with open("./files/neko.model.pickle", "rb") as f:
                self.model = pickle.load(f)
        else:
            self.save_model()

    # 形態素解析結果の作成
    def save_model(self):
        with open("./files/neko.txt.mecab", "r") as f:
            for line in f:
                # 終了文字EOSが現れたら終了
                if line == "EOS\n":
                    break
                vals = re.split(r"\t|,", line)
                self.model.append({
                    "surface": vals[0], "base": vals[7], "pos": vals[1], "pos1": vals[2]
                })
        with open("./files/neko.model.pickle", "wb") as f:
            pickle.dump(self.model, f)

    def filter_pos(self, pos):
        self.model = [i for i in self.model if pos in i["pos"]]
        return self

if __name__ == "__main__":
    nm = NekoMecab()
    pprint(nm.filter_pos("動詞").model)

"""
[{'base': '生れる', 'pos': '動詞', 'pos1': '自立'},
 {'base': 'つく', 'pos': '動詞', 'pos1': '自立'},
 {'base': 'する', 'pos': '動詞', 'pos1': '自立'},
 {'base': '泣く', 'pos': '動詞', 'pos1': '自立'},
 {'base': 'する', 'pos': '動詞', 'pos1': '自立'}

"""

32. 動詞の原形

動詞の原形をすべて抽出せよ.

import re
import pickle
from pprint import pprint

class NekoMecab(object):
    model = []

    def __init__(self):
       self.load_model()

    # 形態素解析結果の読み込みを行う。 無ければ作成
    def load_model(self):
        if os.path.exists("./files/neko.model.pickle"):
            with open("./files/neko.model.pickle", "rb") as f:
                self.model = pickle.load(f)
        else:
            self.save_model()

    # 形態素解析結果の作成
    def save_model(self):
        with open("./files/neko.txt.mecab", "r") as f:
            for line in f:
                # 終了文字EOSが現れたら終了
                if line == "EOS\n":
                    break
                vals = re.split(r"\t|,", line)
                self.model.append({
                    "surface": vals[0], "base": vals[7], "pos": vals[1], "pos1": vals[2]
                })
        with open("./files/neko.model.pickle", "wb") as f:
            pickle.dump(self.model, f)

    def filter_pos(self, pos):
        self.model = [i for i in self.model if pos = i["pos"]]
        return self

    def get_surface(self):
        return [i["surface"] for i in self.model]

if __name__ == "__main__":
    nm = NekoMecab()
    pprint(nm.filter_pos("動詞").get_surface())

"""
['で',
 'ある',
 '生れ',
 'た',
 'つか',
 'ぬ',
 'し',

"""

33. サ変名詞

サ変接続の名詞をすべて抽出せよ.

import re
import pickle
from pprint import pprint

class NekoMecab(object):
    model = []

    def __init__(self):
       self.load_model()

    # 形態素解析結果の読み込みを行う。 無ければ作成
    def load_model(self):
        if os.path.exists("./files/neko.model.pickle"):
            with open("./files/neko.model.pickle", "rb") as f:
                self.model = pickle.load(f)
        else:
            self.save_model()

    # 形態素解析結果の作成
    def save_model(self):
        with open("./files/neko.txt.mecab", "r") as f:
            for line in f:
                # 終了文字EOSが現れたら終了
                if line == "EOS\n":
                    break
                vals = re.split(r"\t|,", line)
                self.model.append({
                    "surface": vals[0], "base": vals[7], "pos": vals[1], "pos1": vals[2]
                })
        with open("./files/neko.model.pickle", "wb") as f:
            pickle.dump(self.model, f)

    def filter(self, **kwargs):
        for key in kwargs:
            self.model = [i for i in self.model if kwargs[key] == i[key]]
        return self

    def get_surface(self):
        return [i["surface"] for i in self.model]

if __name__ == "__main__":
    nm = NekoMecab()
    pprint(nm.filter(pos="名詞", pos1="サ変接続").get_surface())

"""
['見当',
 '記憶',
 '話',
 '装飾',
 '突起',
 '運転',

"""

ちょっとリファクタ

34. 「AのB」

2つの名詞が「の」で連結されている名詞句を抽出せよ.

import re
import pickle
from pprint import pprint

class NekoMecab(object):
    model = []

    def __init__(self):
       self.load_model()

    # 形態素解析結果の読み込みを行う。 無ければ作成
    def load_model(self):
        if os.path.exists("./files/neko.model.pickle"):
            with open("./files/neko.model.pickle", "rb") as f:
                self.model = pickle.load(f)
        else:
            self.save_model()

    # 形態素解析結果の作成
    def save_model(self):
        with open("./files/neko.txt.mecab", "r") as f:
            for line in f:
                # 終了文字EOSが現れたら終了
                if line == "EOS\n":
                    break
                vals = re.split(r"\t|,", line)
                self.model.append({
                    "surface": vals[0], "base": vals[7], "pos": vals[1], "pos1": vals[2]
                })
        with open("./files/neko.model.pickle", "wb") as f:
            pickle.dump(self.model, f)

    # 2つの名詞が「の」で連結されている名詞句
    def get_A_no_B(self):
        res = []
        for i in range(1, len(self.model) - 1):
            if self.model[i-1]["pos"] == self.model[i+1]["pos"] == "名詞" and self.model[i]["surface"] == "の":
                res.append(self.model[i-1]["surface"] + self.model[i]["surface"] + self.model[i+1]["surface"])
        return res

if __name__ == "__main__":
    nm = NekoMecab()
    pprint(nm.get_A_no_B())

"""
['彼の掌',
 '掌の上',
 '書生の顔',
 'はずの顔',
 '顔の真中',

"""

35. 名詞の連接

名詞の連接(連続して出現する名詞)を最長一致で抽出せよ.

import re
import pickle
from pprint import pprint

class NekoMecab(object):
    model = []

    def __init__(self):
       self.load_model()

    # 形態素解析結果の読み込みを行う。 無ければ作成
    def load_model(self):
        if os.path.exists("./files/neko.model.pickle"):
            with open("./files/neko.model.pickle", "rb") as f:
                self.model = pickle.load(f)
        else:
            self.save_model()

    # 形態素解析結果の作成
    def save_model(self):
        with open("./files/neko.txt.mecab", "r") as f:
            for line in f:
                # 終了文字EOSが現れたら終了
                if line == "EOS\n":
                    break
                vals = re.split(r"\t|,", line)
                self.model.append({
                    "surface": vals[0], "base": vals[7], "pos": vals[1], "pos1": vals[2]
                })
        with open("./files/neko.model.pickle", "wb") as f:
            pickle.dump(self.model, f)

    # 連続した名詞句を取得
    def get_continuous_nouns(self):
        res = []
        nonus = []
        for line in self.model:
            if line["pos"] == "名詞":
                nonus.append(line["surface"])
            elif len(nonus) > 1:
                res.append("".join(nonus))
                nonus = []
        # 最後の連続した名詞が上記のforから漏れるからここで処理
        if len(nonus) > 1:
            res.append("".join(nonus))
        return res

if __name__ == "__main__":
    nm = NekoMecab()
    pprint(nm.get_continuous_nouns())

"""
['一吾輩',
 '猫名前',
 'どこ見当',
 '何所',
 'ニャーニャーいた事',

"""

36. 単語の出現頻度

文章中に出現する単語とその出現頻度を求め,出現頻度の高い順に並べよ.

import collections
import re
import pickle
from pprint import pprint

class NekoMecab(object):
    model = []

    def __init__(self):
       self.load_model()

    # 形態素解析結果の読み込みを行う。 無ければ作成
    def load_model(self):
        if os.path.exists("./files/neko.model.pickle"):
            with open("./files/neko.model.pickle", "rb") as f:
                self.model = pickle.load(f)
        else:
            self.save_model()

    # 形態素解析結果の作成
    def save_model(self):
        with open("./files/neko.txt.mecab", "r") as f:
            for line in f:
                # 終了文字EOSが現れたら終了
                if line == "EOS\n":
                    break
                vals = re.split(r"\t|,", line)
                self.model.append({
                    "surface": vals[0], "base": vals[7], "pos": vals[1], "pos1": vals[2]
                })
        with open("./files/neko.model.pickle", "wb") as f:
            pickle.dump(self.model, f)

    def get_word_frequency(self):
        res = []
        surfaces = self.get_arg("surface")
        return collections.Counter(surfaces)

if __name__ == "__main__":
    nm = NekoMecab()
    pprint(nm.get_word_frequency()

"""
Counter({'の': 9194,
         '。': 7486,
         'て': 6873,
         '、': 6772,
         'は': 6422,
         'に': 6268,
"""

37. 頻度上位10語

出現頻度が高い10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.

グラフとのことなので、4章冒頭でも紹介されていたmatplotlibを入れる

pip3 install matplotlib

matplotlibはそのままだと日本語が使えないため、ipaからttcファイルを落としてくる

wget https://ipafont.ipa.go.jp/old/ipafont/IPAMTTC00303.php -O ipamttc.zip
unzip ipamttc.zip

グラフをファイルに出力

import collections
import os
import re
import pickle
from pprint import pprint
from matplotlib import pyplot
from matplotlib.font_manager import FontProperties

class NekoMecab(object):
    model = []

    def __init__(self):
       self.load_model()

    # 形態素解析結果の読み込みを行う。 無ければ作成
    def load_model(self):
        if os.path.exists("./files/neko.model.pickle"):
            with open("./files/neko.model.pickle", "rb") as f:
                self.model = pickle.load(f)
        else:
            self.save_model()

    # 形態素解析結果の作成
    def save_model(self):
        with open("./files/neko.txt.mecab", "r") as f:
            for line in f:
                # 終了文字EOSが現れたら終了
                if line == "EOS\n":
                    break
                vals = re.split(r"\t|,", line)
                self.model.append({
                    "surface": vals[0], "base": vals[7], "pos": vals[1], "pos1": vals[2]
                })
        with open("./files/neko.model.pickle", "wb") as f:
            pickle.dump(self.model, f)

    def get_word_frequency(self):
        res = []
        surfaces = self.get_arg("surface")
        return collections.Counter(surfaces)

    def output_frequency_rank_img(self, rank_num):
        word_frequency = self.get_word_frequency()
        top10 = word_frequency.most_common(rank_num)
        height = [i[1] for i in top]
        xtick = [i[0] for i in top]

        fp = FontProperties(fname='./IPAMTTC00303/ipam.ttc', size=14)
        pyplot.bar(range(rank_num), height)
        pyplot.xticks(range(rank_num), xtick, fontproperties=fp)
        pyplot.title("頻度上位10語", fontproperties=fp)
        pyplot.xlabel("単語", fontproperties=fp)
        pyplot.ylabel("出現回数", fontproperties=fp)
        pyplot.savefig(f"./files/img/frequency_rank_top{rank_num}.png")

if __name__ == "__main__":
    nm = NekoMecab()
    nm.output_frequency_rank_img(10)

f:id:yamakiy:20181104225613p:plain

38. ヒストグラム

単語の出現頻度のヒストグラム(横軸に出現頻度,縦軸に出現頻度をとる単語の種類数を棒グラフで表したもの)を描け.

def output_frequency_histogram_img(self):
        word_frequency = self.get_word_frequency()
        top = word_frequency.most_common()
        datas = [i[1] for i in top]

        fp = FontProperties(fname='./IPAMTTC00303/ipam.ttc', size=14)
        # 出現頻度の差が大きいため、上限30で表示を打ち切る
        pyplot.hist(datas, bins=30, range=(1, 30))
        pyplot.title("単語の出現頻度ヒストグラム", fontproperties=fp)
        pyplot.xlabel("出現頻度", fontproperties=fp)
        pyplot.ylabel("単語の種類", fontproperties=fp)
        pyplot.savefig(f"./files/img/frequency_hist.png")

if __name__ == "__main__":
    nm = NekoMecab()
    nm.output_frequency_histogram_img()

f:id:yamakiy:20181105000058p:plain

全部表示しようとすると、出現頻度の多い単語は重なりが少なく
見えづらいため上限30まででグラフを出力。

39. Zipfの法則

単語の出現頻度順位を横軸,その出現頻度を縦軸として,両対数グラフをプロットせよ.

def output_frequency_zipf_img(self):
        word_frequency = self.get_word_frequency()
        top = word_frequency.most_common()
        datas = [i[1] for i in top]

        fp = FontProperties(fname='./IPAMTTC00303/ipam.ttc', size=14)
        pyplot.scatter(range(len(datas)), datas)
        pyplot.xlim(1, len(datas) + 1)
        pyplot.title("zipf", fontproperties=fp)
        pyplot.xlabel("単語の出現頻度順", fontproperties=fp)
        pyplot.ylabel("出現頻度", fontproperties=fp)
        pyplot.xscale("log")
        pyplot.yscale("log")
        pyplot.savefig(f"./files/img/frequency_zipf.png")

if __name__ == "__main__":
    nm = NekoMecab()
    nm.output_frequency_zipf_img()

f:id:yamakiy:20181105002622p:plain

思ったより時間かかってしまった・・・
11月4日までに終わらせるつもりがぎりぎり間に合わず

matoplotlibの使い方が身についてきた気がする