この記事は拙著100本ノックでPythonに入門の続編です。100本ノック第4章の解説をやってきます。
まずは形態素解析器MeCabをインストールしてneko.txtをダウンロードし、形態素解析して中身を確認してみましょう。
$ mecab < neko.txt > neko.txt.mecab
青空文庫の「吾輩は猫である」です。
MeCabのデフォルト辞書の体系は学校文法に似ていますが、形容動詞は名詞+助動詞、サ変動詞は名詞+動詞になるなど、学校文法における単語より少し細かい分割になります。出力フォーマットはおそらく次のようになっています。
表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音形
また、文はEOS
で区切られています。ちなみに、多くの形態素解析器は全角文字を前提としているので、Webテキストなどを解析する際は半角文字を置換しておくのが良いでしょう。
itertools
せっかくなので、itertoolsモジュールを知っておきましょう。便利なイテレータを作るメソッドが揃っています。
islice()
2章でも登場しましたね。islice(iterable, start, stop, step)
でイテレータのスライスができます。step
省略時は1、さらにstart
を省略するとstart=0
になります。stop=None
とすると終端までという意味になります。とても便利なので是非使いましょう。
groupby()
Unixコマンドのuniq
的なことができます。
from itertools import groupby
a = [1,1,1,0,0,1]
for k, g in groupby(a):
print(k, list(g))
1 [1, 1, 1]
0 [0, 0]
1 [1]
このように第1引数にiterable
を渡すと、その要素の値と、そのグループを返すイテレータのペアを作ってくれます。sort
と同様に、第2引数にkey
を指定することもできます。よく使うのはoperator.itemgetter
ですね(2章でソート HOW TOを読んだ人は知っていることでしょう)。また、ブール値を返すラムダ式を使ったりもします。。
from operator import itemgetter
a = [(3, 0), (4, 0), (2, 1)]
for k, g in groupby(a, key=itemgetter(1)):
print(k, list(g))
0 [(3, 0), (4, 0)]
1 [(2, 1)]
chain.from_iterable()
2次配列を1次元にflatten
できます。
from itertools import chain
a = [[1,2], [3,4], [5,6]
print(list(chain.from_iterable(a))
[1, 2, 3, 4, 5, 6]
zip_longest()
組み込み関数のzip()
は短いイテラブルに合わせますが、長いイテラブルに合わせてほしいときはこれを使います。普通に使うとNone
で穴埋めされますが、第2引数 fillvalue
で穴埋めに使う値を指定できます。
product()
直積を計算します。他にもpermutations()
やcombinations()
は自分で実装すると辛いので一応知っておいて損はないと思います。
これらはitertools
のごく一部なので、興味のある人はドキュメントを読みましょう。
30. 形態素解析結果の読み込み
形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素(マッピング型)のリストとして表現せよ.第4章の残りの問題では,ここで作ったプログラムを活用せよ.
オブジェクト指向な言語をやった人であればクラスを使いたいころですが、それは次の章までお預けです。また2章同様Pandasを使うこともできますが、問題文の意図から離れそうなのでやめておきます。
以下、解答例です。
import argparse
from itertools import groupby, islice
from pprint import pprint
import sys
def main():
parser = argparse.ArgumentParser()
parser.add_argument('num', type=int)
args = parser.parse_args()
for sent_lis in islice(read_mecab(sys.stdin), args.num-1, args.num):
pprint(sent_lis)
def read_mecab(fi):
for is_eos, sentence in groupby(fi, lambda line: line.startswith('EOS')):
if not is_eos:
yield list(map(line2dic, sentence))
def line2dic(line):
surface, info = line.rstrip().split('\t')
col = info.split(',')
dic = {'surface': surface,
'pos': col[0],
'pos1': col[1],
'base': col[6]}
return dic
if __name__ == '__main__':
main()
$ python q30.py 2 < neko.txt.mecab
[{'base': '\u3000', 'pos': '記号', 'pos1': '空白', 'surface': '\u3000'},
{'base': '吾輩', 'pos': '名詞', 'pos1': '代名詞', 'surface': '吾輩'},
{'base': 'は', 'pos': '助詞', 'pos1': '係助詞', 'surface': 'は'},
{'base': '猫', 'pos': '名詞', 'pos1': '一般', 'surface': '猫'},
{'base': 'だ', 'pos': '助動詞', 'pos1': '*', 'surface': 'で'},
{'base': 'ある', 'pos': '助動詞', 'pos1': '*', 'surface': 'ある'},
{'base': '。', 'pos': '記号', 'pos1': '句点', 'surface': '。'}]
main()
では出力を絞ってます。pprint.pprint()
はprint()
の代わりに使うと、改行とかを整えて出力(プリティープリント)してくれます。
この手の形式はgroupby()
のkey
に、行がEOS
かどうかを返す関数を渡せばスマートに書けます。yield
は2章でやりましたね。
問題なのはlist(map())
です。map(func, iterable)
は関数func
をiterable
の各要素に適用し、イテレータを返す、という動作をします。結果は[line2sic(x) for x in sentence]
と同じなのですが、どうもPythonは自作関数をfor
文中で呼び出すのが遅いようなのでこのような記法を採用してみました(参考)。
31. 動詞
動詞の表層形をすべて抽出せよ.
以下、解答例です。
from itertools import islice
import sys
from q30 import read_mecab
def main():
for sent_lis in islice(read_mecab(sys.stdin), 5):
for word in filter(lambda x: x['pos'] == '動詞', sent_lis):
print(word['surface'])
if __name__ == '__main__':
main()
$ python q31.py < neko.txt.mecab
生れ
つか
し
泣い
し
いる
先頭N文をとってくるようにしてます。argpase
も面倒臭がって省略しています。
他に特に言うことが無かったので強引にfilter()
を使ってみました。これは(x for x in iterable if condition(x))
と等価で、条件にマッチした要素だけを返します。正直普通にif
文使えば十分なので出番は少ないですね(というかこの場合filter()
使うと遅い気がします)。
32. 動詞の原形
動詞の原形をすべて抽出せよ.
ほぼ31と同じなので省略。
33.「AのB」
2つの名詞が「の」で連結されている名詞句を抽出せよ.
ごり押しでできてしまうので何も言うことはありません。以下、解答例です。
from itertools import islice
import sys
from q30 import read_mecab
def main():
for sent_lis in islice(read_mecab(sys.stdin), 20):
for i in range(len(sent_lis) - 2):
if (sent_lis[i+1]['base'] == 'の' and sent_lis[i]['pos'] == '名詞'
and sent_lis[i+2]['pos'] == '名詞'):
print(''.join(x['surface'] for x in sent_lis[i: i+3]))
if __name__ == '__main__':
main()
$ python q33.py < neko.txt.mecab
彼の掌
掌の上
書生の顔
はずの顔
顔の真中
穴の中
書生の掌
掌の裏
Pythonで行継続は\
と前言いましたが、括弧の内側でも自由に改行できます。それを言いたいがためだけに無駄にif文の条件式を()
で囲っています。
34. 名詞の連接
名詞の連接(連続して出現する名詞)を最長一致で抽出せよ.
品詞でgroupby()
すれば良いです。以下、解答例です。
import sys
from itertools import groupby, islice
from q30 import read_mecab
def main():
for sent_lis in islice(read_mecab(sys.stdin), 20):
for key, group in groupby(sent_lis, lambda word: word['pos']):
if key == '名詞':
words = [word['surface'] for word in group]
if len(words) > 1:
print(''.join(words))
if __name__ == '__main__':
main()
$ python q34.py < neko.txt.mecab
人間中
一番獰悪
時妙
一毛
その後猫
一度
ぷうぷうと煙
35. 単語の出現頻度
文章中に出現する単語とその出現頻度を求め,出現頻度の高い順に並べよ.
collections.Counter
を使うだけです。以下、解答例です。
import sys
from collections import Counter
from pprint import pprint
from q30 import read_mecab
def get_freq():
word_freq = Counter(word['surface'] for sent_lis in read_mecab(sys.stdin)
for word in sent_lis)
return word_freq.most_common(10)
if __name__ == '__main__':
pprint(get_freq())
$ python q35.py < neko.txt.mecab
[('の', 9194),
('。', 7486),
('て', 6868),
('、', 6772),
('は', 6420),
('に', 6243),
('を', 6071),
('と', 5508),
('が', 5337),
('た', 3988)]
matplotlib
さて、次の問題でグラフを書くのでいよいよこいつの出番です。pip install
しておきましょう。「Pythonに入門」という記事で外部モジュールの解説はあまりしたくないですし、こいつを真面目に解説すると本が1冊書けてしまいます。まずこのQiita記事を読んでmatplotlibの階層構造を把握しましょう。とてもゴチャゴチャしてますよね。その辺を適当に設定してくれるのがPyplot
です。今回は細かい見た目の調整はしないのでこちらの方法でやります。簡単な例を紹介します。
import matplotlib.pyplot as plt
# グラフの種類とデータを指定
plt.plot([1, 2, 3, 4], [1, 4, 9, 16])
# なんやかんや設定する
plt.title('example')
plt.ylabel('some numbers')
# 描画
plt.show()
インポート文の意味は「matplotlib
モジュールのサブモジュールpyplot
をplt
という名前でインポート」という意味になります。
まずはグラフの種類を決めます。折れ線グラフであればplt.plot()
, 横棒グラフであればplt.barh()
, 縦棒グラフであればplt.bar()
といった具合です(イテラブルはpyplot内部でnumpy
配列に勝手に変換されるので、numpy
配列を渡す方が本当は良いかもしれません)。
次になんやかんやの見た目を設定します。plt.yticks()
はy座標とそこに添える文字を設定できます。plt.xlim()
を使うとx軸の最大・最小値を設定できます。plt.yscale("log")
とするとy軸が対数スケールになります。
最後に描画です。私はJupyterを使ってるのでplt.show()
としています。スクリプト実行している人はplt.savefig(filename)
を使ってファイル出力する方が良いと思います。
正直この方法はMATLABライクでありPythonicでは無いですが、簡単ではありますね。
matplotlibで日本語を表示させるには
デフォルトのフォントが日本語非対応なので、豆腐化してしまいます。そのためグラフに日本語を表示させるには日本語対応のフォントを設定すれば良いのですが、お使いの環境によっては日本語フォントが見当たらなかったり、そもそも入ってないのでインストールが必要だったりして、大変です。japanize-matplotlibで楽をさせていただきます。
36. 頻度上位10語
出現頻度が高い10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.
from collections import Counter
from q30 import read_mecab
import matplotlib.pyplot as plt
import japanize_matplotlib
word_freq = Counter(word['base'] for sent_lis in read_mecab(open('neko.txt.mecab'))
for word in sent_lis)
word, count = zip(*word_freq.most_common(10))
len_word = range(len(word))
plt.barh(len_word, count, align='center')
plt.yticks(len_word, word)
plt.xlabel('frequency')
plt.ylabel('word')
plt.title('36. 頻度上位10語')
plt.show()
zip()
の中の*
は何だ?という話ですが、ここでやりたいのは転置です。すなわち、[[a,b],[c,d]]
のようなデータを[[a,c],[b,d]]
のように変形することです。そこでその転置が一番簡単にできるのがzip(*seq)
という書き方です。これはzip(seq[0], seq[1], ...)
と等価です(引数リストのアンパック)。zip([a,b], [c,d])
は[(a,c),(b,d)]
ですよね?さらにアンパック代入を使えば一気に別々の変数に代入できます。
(これでようやく第1章のngram関数の別解の説明が終了しました)
単語頻度で上位を占める語は機能語(助詞、句読点)であることをおさえておきましょう。
37. 「猫」と共起頻度の高い上位10語
「猫」とよく共起する(共起頻度が高い)10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.
word_freq = Counter()
for sent_lis in read_mecab(open('neko.txt.mecab')):
for word in sent_lis:
if word['surface'] == '猫':
word_freq.update(x['base'] for x in sent_lis if x['surface'] != '猫')
break
words, count = zip(*word_freq.most_common(10))
len_word = range(len(words))
plt.barh(len_word, count, align='center')
plt.yticks(len_word, words)
plt.xlabel('frequency')
plt.ylabel('word')
plt.title('37. 「猫」と共起頻度の高い上位10語')
plt.show()
Counter.update(iterable)
でCounter
オブジェクトは更新できます。しかし自立語に絞らないと全く面白くない結果になりますね。
38. ヒストグラム
単語の出現頻度のヒストグラム(横軸に出現頻度,縦軸に出現頻度をとる単語の種類数を棒グラフで表したもの)を描け.
word_freq = Counter(word['base'] for sent_lis in read_mecab(open('neko.txt.mecab'))
for word in sent_lis)
data = Counter(count for count in word_freq.values())
x, y = data.keys(), data.values()
plt.bar(x, y)
plt.title("38. ヒストグラム")
plt.xlabel("frequency")
plt.ylabel("number of the words")
plt.xlim(1, 30)
plt.show()
dict.keys()
で全てのkeyを、dict.values()
で全てのvalueを取得できます。
単語の種類数で見ると、多くの語は低頻度であることがわかります。なんとなく反比例しているようにも見えます。深層学習ではこうした事情で低頻度語処理も重要になってきます。
39. Zipfの法則
単語の出現頻度順位を横軸,その出現頻度を縦軸として,両対数グラフをプロットせよ.
word_freq = Counter(word['base'] for sent_lis in read_mecab(open('neko.txt.mecab'))
for word in sent_lis)
_, count = zip(*word_freq.most_common())
plt.plot(range(1, len(count)+1), count)
plt.yscale("log")
plt.xscale("log")
plt.title("39. Zipfの法則")
plt.xlabel("log(rank)")
plt.ylabel("log(frequency)")
plt.show()
両対数グラフの傾きが大体-1になってますね。これはfreq ∝ rank^(-1)
という意味になります。38番の結果と関連していると思われます。Zipfの法則の詳細はググりましょう。
まとめ
itertools
-
map()
,filter()
matplotlib
- 転置
-
dict.keys()
,dict.values()
次は5章
いよいよクラスを使います。Pythonの入門としては次で最終回ですかね。
(5/16) 書きました → https://qiita.com/hi-asano/items/5e18e3a5a711a752ad99