言語に関する本[2]が面白かったので, 自分でも実験してみました. 言語の記憶性を数学的に検証します. 実データを処理するのは面倒なので, 本来の言語に近いモデルを用います.
生存時間解析などで用いられる2つの分布, 指数分布とWeibull分布を導入します.
指数分布\(\mathrm{Exp}(\lambda)\)の確率密度関数は次式である.
\begin{equation} p(x) = \frac{1}{\lambda}\mathrm{exp}\left( -\frac{x}{\lambda}\right) \end{equation}\(\mathrm{P}\)は確率測度とします. 指数分布に従う確率変数\(X\)について, 次の条件付き確率を調べます.
\begin{equation} \mathrm{P}(X\gt s+t\mid X\gt s) = \frac{\mathrm{P}(X\gt s+t, X\gt s)}{\mathrm{P}(X\gt s)} = \mathrm{exp}\left(-\frac{t}{\lambda}\right) = \mathrm{P}(X\gt t) \end{equation}例えば, \(X\)が故障までにかかる時間だとすれば, \(s\)まで故障せず, さらに時間\(t\)だけ持ち堪える確率は, \(s\)に依存しません. この, 過去の履歴に依らない性質を無記憶性と呼びます. この性質から, 指数分布はランダムな現象のモデル化に適しているとされます. 逆に, 右辺の超過確率を計算して指数関数の形になれば, ランダムな現象だと判定できそうです.
以下のWeibull分布は, 指数分布とは異なり, 無記憶性を示しません. もちろんパラメータの値に依りますが...
Weibull分布\(\mathrm{Weibull}(\eta,\lambda)\)の確率密度関数は次式である.
\begin{equation} p(x) = \frac{\eta}{\lambda}\left(\frac{x}{\lambda}\right)^{\eta-1}\mathrm{exp}\left\{ -\left(\frac{x}{\lambda}\right)^{\eta}\right\} \end{equation}ここから自然言語の話に移ります. 実データを処理するのは面倒なので, 本来の言語に近いデータを人工的に作ります. [2]によると, Barabási-Albertネットワーク上のランダムウォークから生成されるデータが自然言語に割と近いらしいです. まずは, Barabási-Albertネットワークの説明から入ります.
Barabási-Albertネットワーク(以下, BAネットワーク)は, 少数のノードが多くのリンクを持ち, 多くのノードが少数のリンクを持つ複雑ネットワークです. "rich gets richer"をモデル化するのに役立ちます. 自然言語の場合, 少数の単語のみが頻出することが分かっています(Zipfの法則). 言語のモデル化にBAネットワークは適しているでしょう.
BAネットワークの作り方を説明します. まず, 最初にノードがいくつか存在するとします. これらのノードに新規のノードを繋げます. 新規のノードからは, \(m\)本のリンクが出ており, 既存の, どのノードに繋ぐかは確率的に決めます. 既に持っているリンク数に比例する確率で繋げるノードを選択します. この操作を繰り返します.
ネットワーク作成の様子を, アニメーションで示します. \(m=1\)の場合をノード数が10に達するまで反復しました. 最初に出現したノードほど, 多くのリンクを持っています.
先ほど作成したBAネットワーク上でランダムウォークさせます. 言語の例で言うと, 各ノードは単語に対応します. 歩く主体がノードに達するごとに, そのノードに対応する単語が発生すると考えます. ノードに移動する確率は, 移動先のリンク数に比例する確率で選択します. ここでは, 50回ほど歩かせます. つまり, 50単語からなる文書が作成されるイメージです. アニメーションを以下に示します. 星マークが歩く主体です.
以下, BAネットワーク上のランダムウォークから発生させた人工データを本物の単語データと思って話を進めます. [2]に従って, 10万ノードのBAネットワーク上で100万回ランダムウォークさせます. このデータを用いて, 文書中の単語の出現がランダムと言えるかどうかを検証します.
1番目のノードに対応する単語(以下, 単語1)は頻出と期待できます. この単語1に注目して解析します. まず, 単語1の出現具合を数量化します. 単語1の出現間隔は, 出現具合の指標として, 1つの自然な選択です. この出現間隔の分布を調べてみましょう.
データがランダムに発生しているかどうかは, 指数分布に従うかどうかで峻別できるのでした. 単語1の出現間隔に対して, 間隔の頻度から確率を計算し, 超過確率を計算してランダム性を調べます. 計算結果を下図にプロットしました. 横軸は出現間隔の長さ, 縦軸はその出現間隔よりも長い間隔で単語1が現れる確率を表します. 擬似的なデータが青線です. オレンジの線は, 単語列をシャッフルしたものです. シャッフルした列は単語1の出現間隔がランダムであると期待できます.
オレンジのシャッフル列はほぼ直線です. 縦軸は対数スケールですから, おおよそ指数的に減衰する形です. 超過確率が指数関数となるので, シャッフル列の出現間隔の分布は指数分布と考えられます. 一方, 青の単語列は, 指数分布からの乖離を示します. 実はこの青のカーブはWeibull分布であることが知られています. ここまでの話をまとめれば, シャッフル列は無記憶性を示し, 単語列は記憶を保持します.
我々の用いる自然言語には, このように記憶が関わっており, 偶然性のみでは説明できません. 系列の過去の履歴にも依存し, 長い間隔を隔てて単語が再び出現する可能性が十分あります. 今回は擬似的なデータを用いましたが, 実際の言語データでも成り立つはずです(未確認).
#mathematics
using LinearAlgebra
using SparseArrays
#statistics
using Random
using StatsBase
using Statistics
using Distributions
#dataframe
using DataFrames
#visualize
using Plots
pyplot()
#macros
using ProgressMeter
using UnPack
#normalize
function normalize(vec)
S = sum(vec)
if S>0
return vec/sum(vec)
else
vec[1] = 1
return vec
end
end
#initialize the graph
function initialize!(adj,m)
for j in 1:m
for k in j+1:m
adj[k,j] = 1
adj[j,k] = 1
end
end
end
#choose m nodes
function choose_nodes(adj,m)
probs = normalize(sum(adj,dims=2)[:])
return rand(Categorical(probs),m)
end
#update adjacency matrix
function update_adj!(adj,nodes,v)
adj[nodes,v] = ones(Int64,m)
adj[v,nodes] = ones(Int64,m)
end
#V nodes each of which has m edges
function BAmodel(m,V)
adj = spzeros(Int64,V,V)
nodes = zeros(Int64,m)
initialize!(adj,m)
@showprogress for v in m+1:V
nodes = choose_nodes(adj,m)
update_adj!(adj,nodes,v)
end
return adj
end
#choose the next node
function walk(current_node,adj,edges)
nodes = adj[:,current_node].nzind
probs = normalize(edges[nodes])
idx = rand(Categorical(probs))
return nodes[idx]
end
#random walk
function myRW(adj,m,V,N)
edges = sum(adj,dims=2)[:]
current_node = 1#rand(1:V)
path = zeros(Int64,N)
path[1] = current_node
@showprogress for n in 2:N
current_node = walk(current_node,adj,edges)
path[n] = current_node
end
return path
end
#set the random seed
Random.seed!(42)
#create the BA network
m = 1
V = Int(1e1)
adj = BAmodel(m,V)
#animation
θ = 2π/V
ts = 0:0.1:2π+0.1
coss = [cos(i*θ) for i in 0:V-1]
sins = [sin(i*θ) for i in 0:V-1]
plot(cos.(ts),sin.(ts),xlim=(-1.2,1.2),ylim=(-1.2,1.2),label=false,ls=:dot,aspect_ratio=:equal,title="BA network")
plot!(coss[1:1],sins[1:1],st=:scatter,color=:skyblue,markersize=10,label=false)
anim = @animate for v in 2:V
plot!(coss[1:v],sins[1:v],st=:scatter,color=:skyblue,markersize=10,label=false)
to_v = adj[v,1:v].nzind[1]
plot!([coss[v],coss[to_v]],[sins[v],sins[to_v]],color=:blue,label=false)
end
gif(anim,"figs-NLPSD/anim1.gif",fps=1)
#set the random seed
Random.seed!(42)
#create the BA network
m = 1
V = Int(1e1)
adj = BAmodel(m,V)
#BA network
θ = 2π/V
ts = 0:0.1:2π+0.1
coss = [cos(i*θ) for i in 0:V-1]
sins = [sin(i*θ) for i in 0:V-1]
ba_plot = plot(cos.(ts),sin.(ts),xlim=(-1.2,1.2),ylim=(-1.2,1.2),label=false,ls=:dot,aspect_ratio=:equal,title="RW on BA network")
plot!(coss,sins,st=:scatter,color=:skyblue,markersize=10,label=false)
for v in 2:V
to_v = adj[v,1:v].nzind[1]
plot!([coss[v],coss[to_v]],[sins[v],sins[to_v]],color=:blue,label=false)
end
#random walk on BA network
N = 50
history = myRW(adj,m,V,N)
anim = @animate for n in 1:N
plot(ba_plot)
plot!([coss[history[n]],],[sins[history[n]],],st=:scatter,color="red",markersize=20,markershape=:star,label=false)
end
gif(anim,"figs-NLPSD/anim2.gif",fps=1)
#set the random seed
Random.seed!(42)
#create the BA network
m = 1
V = Int(1e5)
adj = BAmodel(m,V)
#create word sequence
N = Int(1e6)
W = myRW(adj,m,V,N)
n_unique = length(unique(W))
#data
data = (W=W,V=V,N=N,n_unique=n_unique)
println("The number of vocabrary = $(V). ")
println("The document has $(N) words in total. ")
println("The document has $(n_unique) unique words in total. ")
#word dataframe
word_df(W) = DataFrame(word_id=W)
#word frequency dataframe(sorted)
function word_freq_df(df)
word_cnt_df = combine(nrow,groupby(df,:word_id))
return sort(word_cnt_df,:nrow,rev=:true)
end
#unify rare wards lowest 1/Ψ
function unify_rare_words(data,my_word_freq_df,Ψ)
@unpack W,N,V,n_unique = data
Wrare = zeros(Int64,N)
rare_word_id = maximum(W)+1
sorted_freqs = sort(my_word_freq_df,:nrow)
rare_words = sorted_freqs[cumsum(sorted_freqs.nrow) .< Int(floor(N/Ψ)),:].word_id
for n in 1:N
if W[n] in rare_words
Wrare[n] = rare_word_id
end
end
return Wrare, rare_word_id
end
#interval sequence for a specific word
function Qv_df(W,v)
is_v_idx = SparseVector(W.==v).nzind
Qv = is_v_idx[begin+1:end] - is_v_idx[begin:end-1]
df = DataFrame(Qv=Qv)
freq_df = combine(nrow,groupby(df,:Qv))
return df, sort(freq_df,:nrow)
end
#cumulative distribution lower than q
cumv(q,Qv_freq_df) = sum(Qv_freq_df[Qv_freq_df.Qv.<=q,:].nrow)
function cumvvec(qs,W,v)
freq_v = sum(W.==v)
n_qs = length(qs)
cums = zeros(n_qs)
_, Qv_freq_df = Qv_df(W,v)
@showprogress for j in 1:n_qs
cums[j] = cumv(qs[j],Qv_freq_df)
end
return cums/(freq_v-1)
end
#frequency
my_word_df = word_df(W)
my_word_freq_df = word_freq_df(my_word_df)
qs = 1:60
#visualize (puseudo words data)
excess_probs = 1 .- cumvvec(qs,W,1)
fig = plot(qs,excess_probs,xlabel="interval",ylabel="prob",yscale=:log10,color=:blue,
marker=:circle,markerstrokewidth=0,label="pseudo words")
#visualize (shuffle words)
shuffled_W = shuffle(W)
excess_probs = 1 .- cumvvec(qs,shuffled_W,1)
plot!(qs,excess_probs,xlabel="interval",ylabel="prob",yscale=:log10,
marker=:circle,markerstrokewidth=0,label="shuffled")
savefig(fig,"figs-NLPSD/fig.png")