13 Zipf の法則を再発見する

Python 配布プリント (11) by 水谷正大
http://www.ic.daito.ac.jp/~mizutani/python/index.html
[重要な前提]
スクリプトは文字符号化 UTF-8 で保存されているとする。
13 Zipf の法則を再発見する
指定した分析対象とするテキストファイルの単語頻度辞書(出現数が載った単語辞書)を作成し、その頻度
(出現数)をキーに降順 (大きな順から 小さい順に並べること) でソートした結果を求めて、Zipf の法則*1 が
成立しているかを確認してみたい。
対象とするテキストファイル document.txt をコマンドで指定して、テキスト内に登場した単語の頻度を
降順 (大きい順から小さい順) に並べ替えて
単語、順位, 頻度
または
単語, log 順位, log 頻度
を標準出力 (モニタ) に出力するスクリプト zipf_law.py を書いてみよう。これは次のように使う。
% python zipf_law.py document.txt
13.1 分析対象となるテキストとするための事前作業
事前作業に十全な注意を払い、分析テキストとして採用し得るための元テキストファイルを入念に準備す
る。注意深い作業を怠って得られた結果は誰からも信用もされない。唯一にして最も重要な作業工程になる。
“ALICE’S ADVENTURES IN WONDERLAND” のテキスト
http://www.gutenberg.org/cache/epub/19033/pg19033.txt をすべてダウンロードして保存した上で、明ら
かに英文本文とは無関係なテキストを削除して、改めて alice_adventures.txt として保存する。
この utf-8 テキストの中身を検討してみると、あらかじめ除外すべきテキストがあることに気づく。タイ
トルの ALICE’S ADVENTURES IN WONDERLAND 直下の ‘I–DOWN THE RABBIT-HOLE’ ま
でのクレジット などのテキストは全て不要である。また、テキスト内にある原作にあった挿絵位置を示す
‘‘[llustration]’’の行は全て不要である。さらに␣␣␣␣␣␣␣* を繰り返すテキストの区切り行全ても不要であ
る。また、原作が終わった以降にある “End of the Project Gutenberg EBook of Alice in Wonderland, by
Lewis Carrol” を含む全ては不要で削除する。また他にも、分析対象となるテキストを丁寧に閲覧して、原作
テキストであるために削除すべき内容があればその全てを削除する。ここではこうして得られた原作内容を
alice_adventures.txt として保存しておく。
13.2 リストの並び替え
リスト list を昇順 (increasing oreder: 小さい順に) 並び替えたリストを返すには関数 sorted を使って
次のように書く。
sorted(list)
あるいはまた、降順 (decreasing order: 大きな順に) 並べ替えたリストを返すには次のように書く。
sorted(list, reverse = True
*1
Zipf の法則についての信頼できる記述は「言語情報処理」
(岩波口座 言語の科学 9, 1998)や「計量情報学」景浦峡(丸善、2000)
にある。
1
関数 sorted() は list 自身には影響を及ぼさない。
>>> list = [11, 3, 4, 7, 9, 7, 3]
>>> sorted(list)
[3, 3, 4, 7, 7, 9, 11]
>>> sorted(list, reverse = True)
[11, 9, 7, 7, 4, 3, 3]
>>> list
[11, 3, 4, 7, 9, 7, 3]
一方、リストメソッド .sort() を使って対象となるリスト list の内容自体を並べ替えることもできる。
>>> list.sort()
>>> list
[3, 3, 4, 7, 7, 9, 11]
>>> list.sort(reverse = True)
>>> list
[11, 9, 7, 7, 4, 3, 3]
リストの並び替え関数 sorted() を使うか、リストメソッド .sotr() のいずれを使うかは処理目的によって
適宜使いわける。リスト list の要素がタプル
(item0 , item1 , . . . , itemm − 1 )
で与えられているとき、キー itemk で昇順に 並べ替えたリストを得たいときには、次のように λ-関数*2 を
使う。
sorted(list, key = lambda x: x[k])
たとえば、2 タプルを要素とするリストでは次のようになる。
>>> sorted(list, key = lambda x: x[0])
[(’a’, 9), (’a’, 3), (’b’, 4), (’c’, 2), (’e’, 5)]
>>> list =[(’a’, 9), (’c’, 2), (’e’, 5), (’b’, 4), (’a’, 3)]
>>> sorted(list, key = lambda x: x[1])
[(’c’, 2), (’a’, 3), (’b’, 4), (’e’, 5), (’a’, 9)]
13.3 登場単語頻度辞書を並び替える
単語:頻度 を要素とする辞書として、単語頻度(出現数)辞書 word_frequency_dict について、その ‘頻
度’(タプルの第 1 番目) をキーとして降順に並べ替えるには、次のような方法が考えられる。
集合である辞書要素のタプルのいずれかを指定して並べ萎えると、直目した値だけが取り出される。
>>> word_frequency_dict = {’orange’:21, ’banana’:13, ’lemon’:9, ’melon’:35, ’grape’:5}
>>> sorted(word_frequency_dict, reverse = True, key = lambda x: x[1])
[’orange’, ’grape’, ’melon’, ’lemon’, ’banana’]
その ‘頻度’(タプルの第 1 番目) をキーとして辞書要素を降順で並べるには、メソッド .items() を使って辞
書 をリスト化しておく。
>>> sorted(word_frequency_dict.items(), reverse = True, key = lambda x: x[1])
*2
λ-関数は、Church の計算可能性理論の研究において 1936 年に λ-定義可能性として導入された。計算可能である帰納関数に一致
する。
2
[(’melon’, 35), (’orange’, 21), (’banana’, 13), (’lemon’, 9), (’grape’, 5)]
以 上 の こ と か ら 、今 回 作 成 す る zipf_law.py は 、前 回 の プ リ ン ト で 作 成 し た ス ク リ プ ト
make_dictionary.py の ほ と ん ど を 踏 襲 す る 。差 異 は main 部 だ け で 、次 の よ う に な る 。た だ し 、数
学関数モジュール math をインポートしておく。
作成した辞書 word_frequency_dict をリスト化して、各要素であるタプル (word, freq) のうち ‘頻度’
freq(タプルの第 1 番目) をキーとして並べ替えた各要素(2 タプルである)を for 文で取り出すときに
for word, freq in (word, freq) を要素とする順序データ
として、2 タプルの要素 word と freq を取り出すことができることに注意する。for 文で繰り返されている内
容は、降順の並べ替え順位 order と単語 word および出現数 freq を指定した書式に従って、カンマ区切り
(,) の CSV 形式 (Comma-Separated Values) で標準出力に書き出している。
ここでは、まずテキスト内で使われている単語数(重複有り)のリスト word_list の長さ(単語総数)を
プリントしている。後で、単語の出現確率を求めるために、この総数を使う。
zipf law.py
#------
main script
-------
import math
fh = open(sys.argv[1], ’r’)
word_list = []
line = fh.readline()
while line:
wlist = modify_line(line).split()
word_list.extend(wlist)
line = fh.readline()
fh.close()
total_word_num = len(word_list)
print "Total Number of words =", total_word_num
word_frequency_dict = make_word_frequency_dictionary(word_list)
# 単語出現数で降順ソート、その順位と単語出現数を cvs 形式で出力
print "Sort by freq(Number of appearances) of words"
print "word, log(order), log(freq)"
order = 1
for word, freq in sorted(word_frequency_dict.items(), reverse=True, key = lambda x: x[1]):
#
print "%s, %d, %d" % (word, order, freq)
print "%s, %.12.10f, %1f" % (word, math.log(order), math.log(freq) )
order += 1
print において、文字列として出力する際に、上の例のように、%s は文字列を、%d は整数を、%f は浮動小
数で置き換えられる書式指定である。
モジュール math をインポートしているので、上の例では、単語に引き続いて、降順(大きい順)の順位と
その出現数の対数を関数 math.log() を使って求めて書き出している。
14 標準出力のリダイレクト
標準出力装置(モニター)への出力は、コマンドラインで記号 > を使って容易にテキストファイルとして保
存することができる。次の例は出力結果をテキストファイル alice_adventures.csv に書き出す。
3
図1
ファイル ‘Alice Adventure in Wonderland.csv’ を Excel で開いてグラフ化表示してみる。
図2
“Alice Adventure in Wonderland” に登場する英単語の順位とその出現数の関係。横軸は log 順位、
縦軸は log 出現数。この両対数プロット点は概ね傾きが負の直線に載っていることが観察できる。
出力のファイルへのリダイレクト
$ python zipf_law.py alice_adventures.txt > alice_adventures.csv
このファイル alice_adventures.csv を Microsoft Excel で開いた様子が図 1 である。単語の降順の順位
の対数と出現数の対数を図 2 に示した。
14.1 Zipf の法則を求めて
以上で、1 つのある程度の規模(∼150KB)の英文テキスト T について、その単語の出現回数 y と出現順位
x とが指数 γT > 0 を持つベキ法則 (powerlaw) に準じていることが予想される足がかりを得た。
y ∼ bT x−γT ,
γT > 0, x > 0
定数 bT はテキスト T で決まる定数である。これの意味することは次のようである:他の複数の作品
T1 , T2 , . . . , Tm についても、それぞれベキ指数 γ1 , γ2 , . . . , γm が存在して同じような関係式
y ∼ bT1 x−γT1 ,
y ∼ bT2 x−γT3 , . . . ,
y ∼ bTm x−γTm ,
4
が成立する、といっているに過ぎない。
では、これらのベキ数 γk , k = 1, . . . , m はテキストごとに異なった値 {γk } になっているのか、それとも英
文テキスト T − k に依らずに普遍定数 γ = γk なのだろうか?テキスト毎に登場単語とその出現数は当然それ
ぞれに異なっているため、この問いは大変興味深い。
このために、いままでのように単語 wordi の出現数 fi でなく、その単語の出現確率 pi
pi =
wordi の出現数
総単語数
をもとめて、log 順位 と log 出現確率 を計算しよう。順位と出現数のデータから順位と出現確率を見いだすな
ど、異なるデータ群を比較可能とする操作を規格化 (normalize) という(目的に応じて、規格化の作業はその
都度異なる)
。規格化データを標準出力装置に出力するために、スクリプト zipf_law.py の main 部後半を次
のように書き直しておく。
zipf law.py の修正箇所
# 単語頻度で降順ソート、その単語出現確率 word_prob を求めて cvs 形式で出力
print "Sort by word probablility"
print "word, log(order), log(prob)"
order = 1
for word, freq in sorted(word_frequency_dict.items(), reverse=True, key = lambda x: x[1]):
word_prob = float(freq) / total_word_num
print "%s, %18.16f, %12.10f" % (word, math.log(order), math.log(word_prob) )
order += 1
この修正したスクリプトを使って再度書き出した alice_adventures.csv に加えて、Free ebooks by
Project Gutenberg(https://www.gutenberg.org) から Charles Dickens の”Oliver Twist”(900KB) の本
文だけを慎重に取り出して保存した oliver_twist.txt を使って同様に書き出した oliver_twist.csv の
2 つ規格化したデータを使って、そのプロットを重ねたものが図 3 である。2 作品の単語出現確率に関しての
ベキ指数が概ね同じであることがわかる。
図3
“Alice Adventure in Wonderland”(L.Carroll) と “Oliver Twist”(C. Dickens) に登場する単語
の順位とその出現確率の関係。横軸は log 順位、縦軸は log 出現数。2 作品の両対数プロットは概ね同じ傾
きを持つことがわかる。
[課題] 登場する単語頻度を計算して標準出力に結果を書き出すスクリプト zipf_law.py を書きなさい。これ
を使って、Free ebooks by Project Gutenberg(https://www.gutenberg.org) から、ここで取り上げたテ
キスト以外の大規模なローマ字テキストを 3 つ以上選んで、CSV ファイルに書き出しなさい。書き出した全
内容は報告しなくてよいが、それぞれについてのテキストの
5
• 正確な作品名称と作者、発表年
• テキスト内容だけのバイト数
• テキストで使われた総単語数
• 上位 20 位までの最頻出単語とその登場回数の一覧
を報告し、それぞれのテキストの単語の順位とその出現確率の関係を示すプロットグラフを含めること。ただ
し、必ずグラフには、図 3 のようにグラフタイトル、軸目盛り、軸ラベル(説明)を必ず記載すること(これ
らが一つでも欠損しているグラフは無意味でしかない)。
テキストごとのグラフの傾きや差異などついて考察して Zipf の法則の成立について検討し、非当事者でも
わかように明快なレポートとして報告しなさい。
必ず A4 版でページ数を付けて左隅み上をホッチキスで留めること。
[研究] 対象とするテキストを UTF-8 で保存しておく意義は大変大きい。Free ebooks by Project Gutenberg
には、Saint Augustine の「告白:Confessiones」や Julius Caesar の「ガリア戦記:De Bello Gallico」などの
ラテン語著作*3 はもちろん、アクセント記号を有するフランス語やドイツ語、イタリア語・スペイン語も多数
収録されている。スクリプト zipf_law.py はこれらのテキストの単語頻度も調べることができる。言語の違
いによって Zipf の法則の指数は違ってくるのだろうか。石川啄木の Romaji Diary のような日本語のローマ
字表記ではどうだろうか。
*3
ハーバード大学の The Digital Loeb Classical Library は古代ギリシャとローマ時代のラテン文献がほぼ網羅されている。
6