はじめに
前回の記事で、青空文庫からテキストを取得して、操作できる形式に変換することができました。
せっかく、自然言語のテキストが大量に扱えるようになったので、自然言語処理関連でのいろいろを考えていきたいと思います。
今回は、自然言語の経験則であるジップの法則(Zipf’s law)についての確認を行います。
実行環境は、SageMaker Studio Labですが、他の環境でも問題なく動作します。。
使用するパッケージ
テキストの読み込みから解析までは、前回に使用したものの一部です。表示に関しては、前回がネットワーク構造の可視化だったのに対して、今回は数値データのグラフ表示ですので違うパッケージを用います。
HTTP.jl | httpアクセス | 前回と同じ |
ZipFile.jl | zipファイル操作 | 前回と同じ |
StringEncodings.jl | 文字エンコード処理 | 前回と同じ |
Awabi.jl | 形態素解析 | 前回と同じ |
Plots.jl | グラフ表示 |
ジップの法則(Zipf’s law)
簡単に言えば
「自然言語のテキストにおける単語の出現頻度は、頻度の大きい単語から順に並べた場合に、k番目の単語は1/kの頻度になっている」
というものです。
詳細については「ジップの法則」を参照してください。
実際のデータをジップの法則に当てはめてみると、出現頻度の大きい単語(kが小さい場合)の場合は、うまく一致しないこともあります。以下では、それも含めて確認します。
形態素解析器Awabiでの問題点 ← Awabi.jl 0.1.2で修正済みです!
ここに書かれている不具合は「Awabi.jl 0.1.1」の時点のものです。
すでに「Awabi.jl 0.1.2」では改修済みです。(素早い対応ありがとうございました。)
→ Awabi.jl 0.1.2 リリース
なお、下に書いてあるコードは、そのまま動作するので、そのまま残しておきます。
エラーをcatchでとらえているので、エラーが発生しなければ、通常の処理が行われます。エラーによるスキップがなくなった分だけ、頻度情報が増えますが、全体の出現頻度の分布をみているので問題ありません。
さて、複数のテキストでの検証を行っていた時に、形態素解析器Awabiでエラーが出る場合を発見してしまいました。
「吾輩は猫である」(夏目漱石)中の次の文でエラーが発生します。
「Archaiomelesidonophrunicherata と云う字だ」
Awabiの内部でエラーが出ているので、手を出せません。おそらく、同じエラーが発生する文は他にもあるだろうと予想されます。
今回は、エラーをtry/catchで受け止めて、対象となる文の形態素解析をスキップすることにします。
その文字列自体を解析しなければならない場合にはこの処置では問題ありますが、今回は全体の出現頻度の分布をみているので、許容範囲内としました。(出現頻度で言えば、誤解析によって頻度が正しく取れない場合があることの方が影響が大きいかもしれません。)
青空文庫から取得したzipファイル
以前は、圧縮されたzipファイル中には一つのテキストだけが入っているとして処理しましたが、複数のファイル(例えば、挿絵のpngファイル)が含まれるものがあり、解析に失敗することがありました。
そこで、zipファイルに格納されているファイルのうちで最初に見つかったテキストファイルを処理することで、これを回避するようにしました。
具体的には、後述するコードをご覧ください。
関数の定義
今回は複数の文書を対象とするので、各処理を関数として定義して、再度の呼び出しができるようにしておきます。
まず、テキストデータの取得です。
using HTTP
using ZipFile
using StringEncodings
# 青空文庫の小説のzipファイルのURLからデータを取得
function getaozora(url::String)
# urlからzipファイルを読み込み
dat = HTTP.get(url)
# zipファイルから圧縮前の元データを取得
r = ZipFile.Reader(IOBuffer(dat.body))
# 最初に見つかったテキストファイルを取得する
f = nothing
for file in r.files
if findlast(".txt", file.name) != nothing
f = file
break
end
end
close(r)
# 文字コードShiftJISで記述されたファイルを読み込む:CP932を指定する
lines = readlines(f, enc"CP932")
end
次に、このテキストデータから、不要な情報を除去します。奥付のマッチング文字列も少し調整しました。
# 青空文庫のテキストデータを加工して、形態素解析できるようにする
# ・注釈行・奥付やルビ・入力者注を削除
# ・句点で改行させ、不要な空白・空行を除去
function correctaozora(lines)
# 先頭の注釈行判定用
note_line = r"^--------"
is_note = false
# 末尾の奥付判定用
endmark = r"^(底本|底本の親本|翻訳の底本|初出):"
# 最終結果の格納領域
new_lines = []
for line in lines
# 不要行を削除する(new_lineへの登録をskipすることで実現)
## 先頭の注釈行の削除
if is_note
if occursin(note_line, line)
is_note = false
end
continue
else
if occursin(note_line, line)
is_note = true
continue
end
end
## 末尾の奥付を削除
if occursin(endmark, line)
break
end
# 不要行削除ここまで
# ルビ、および、入力者注の削除
line = replace(line, r"[#[^]]+]" => "")
line = replace(line, r"《[^》]+》" => "")
# 形態素解析に長文を渡したり、不要な呼び出しをしないように、文字列を調整
## 句点「。」の後で分割する
slines = split(replace(line, r"。" => "。\n"), "\n")
for ll in slines
## 行頭の空白文字列を削除
ll = replace(ll, r"^[ ]+" => "")
## 空行は削除
if length(ll) == 0
continue
end
# 処理済み文字列を格納
push!(new_lines, ll)
end
end
new_lines
end
最後に、形態素解析して、形態素の頻度情報を取得する関数です。
using Awabi
function countmorph(lines)
# 形態素解析器の設定
## Linux / Mac
#tokenizer = Tokenizer()
## Windows:
#dic = Dict("dicdir" => "C:\\Program Files (x86)\\MeCab\\dic\\ipadic")
#tokenizer = Tokenizer(dic)
## SageMaker Studio Lab
rcfile = "/home/studio-lab-user/mecab/etc/mecabrc"
tokenizer = Tokenizer(rcfile)
# 数え上げ格納領域
word_counts = Dict{String, Int}()
# 形態素解析&数え上げ
for line in lines
# 1文を形態素解析
try
tokens = tokenize(tokenizer, line)
new_tokens = []
for token in tokens
attr = split(token[2], ",")
hinsi = attr[1]
hinsi2 = attr[2]
surface = token[1] # 表記
basic = (attr[7] != "*") ? attr[7] : surface # 形態素の基本形
# 一文字のひらがな・カタカナ・長音「ー」・波線「〜」を対象外にする
if hinsi in ["名詞", "動詞", "形容詞"] && !(hinsi2 in ["接尾", "非自立"]) &&
(length(surface) > 1 || ((surface < "ぁ" || surface > "ヶ") && !(surface in ["ー", "~"])))
push!(new_tokens, basic)
end
end
# 行中の形態素の重複を除去
#unique!(new_tokens)
# 形態素数を数え上げ
for surface in new_tokens
word_counts[surface] = get(word_counts, surface, 0) + 1
end
catch
# pass
end
end
word_counts
end
グラフで比較
夏目漱石の「こころ」を対象として実施してみます。
- 作品名:こころ
- 著者:夏目漱石
- URL:https://www.aozora.gr.jp/cards/000148/files/773_ruby_5968.zip
url = "https://www.aozora.gr.jp/cards/000148/files/773_ruby_5968.zip"
lines = correctaozora(getaozora(url))
# 形態素表記から頻度を得る、辞書構造
word_counts = countmorph(lines)
# 頻度だけを、降順(大きい順)に並べた配列
ordered_count = [c for (w,c) in sort(collect(word_counts), rev = true, by =x -> x[2]) ]
上記で、変数「ordered_count」に、降順(大きい順)に頻度が格納されています。これを、グラフ表示します。グラフ表示には「Plots.jl」を使います。今回はバックエンドはデフォルト(GR)です。
頻度の変化を表現するので、基本は棒グラフを使うところですが、ジップの法則のグラフ(y = a / x)と比較したいので、折れ線グラフを使います。
まず、何も考えずに、グラフ表示してみます。
# ex1:何も考えずに全部表示
using Plots
base = ordered_count[1]
y_limit = base
p1 = plot(ordered_count, title="Zipf's law", markershape=:circle, linecolor=:blue, ylims=(0,y_limit), label="kokoro")
plot!(p1, x->base/x, linecolor=:red, label="zipf's law")
大半が頻度1になってしまっているので、ロングテイルの部分を表示する必要はなさそうです。上位50件を表示してみます。
# ex2:上位50件を表示
using Plots
base = ordered_count[1]
y_limit = base
p2 = plot(ordered_count[1:50], title="Zipf's law", markershape=:circle, linecolor=:blue, ylims=(0,y_limit), label="kokoro")
plot!(p2, x->base/x, linecolor=:red, label="zipf's law")
最大頻度を基準にしたので、グラフがうまく重なりません。頻度が大きいほどジップの法則からのずれは大きくなるので、20番目の値を使ってみます。20番目の値=基準の1/20と仮定して、グラフを調整します。
# ex3:20番目の値から基準を調整
using Plots
base = ordered_count[20] * 20
y_limit = base
p3 = plot(ordered_count[1:50], title="Zipf's law", markershape=:circle, linecolor=:blue, ylims=(0,y_limit), label="kokoro")
plot!(p3, x->base/x, linecolor=:red, label="zipf's law")
それっぽくなりました。
もう少し一致するかと思ったのですが、やはり、頻度が高いところほどずれが大きくなっています。小説でないテキスト(ニュースやブログ)で試してみると、また違った結果になるかもしれません。
まとめ
上記をまとめたipynbファイルを、下記に格納してあります。こちらには、もっと多くのテキスト(小説)での実行結果なども掲載してあります。
参考記事
- Plots.jl(GitHub)
- Plots(docs.juliplots.org)
- Plots / Supported Attributes(docs.juliaplots.org)
- ジップの法則(Wikipedia)
- 青空文庫
- JuliaでPlotsするよ(Qiita)
- Plots.jl入門(Zenn)
- Juliaで数列の計算、棒グラフを描く方法(趣味の大学数学)
- プログラミング/julia/グラフの書き方(武内修@筑波大)
- Plots/GR: グラフ package のおすすめ(Daisuke Furihata)
- Plots メモ(Takuya Miyashita)
- Juliaでグラフをプロットする-基礎編-(Programing Style)
- JuliaをインストールしてPlotsでグラフを表示する(Qiita)
- JuliaでPlotsするよ(Qiita)
コメント
修正しました
https://nakagami.blog.ss-blog.jp/2022-06-25-1
ありがとうございました。エラーが出ないことを確認し、記事を修正しました。