Pretrained language modelsを理解する
はじめに
少し前に「A Review of the Neural History of Natural Language Processing」というまとめ記事を見ました。
blog.aylien.com
自然言語処理の発展の歴史みたいなもので、近年のブレイクスルーをまとめてくれています。
2018年の注目はELMo
に代表される「Pretrained language models」の発展であり、これによりSOTAを達成したとの論文が挙がっています。
ELMoはこちらで分かりやすく解説されています。
kamujun.hatenablog.com
ここでは、自分なりに「Pretrained language models」をまとめようと思います。
Language Model(言語モデル)
言語モデルは文章(単語の並び)に関する確率分布を評価するものです。
ある単語の並びが自然であればその文章の確率は高くなり、不自然であれば低くなります。
言語モデルが学習するのは「自然な単語の並び」です。
数式を使って言語モデルを書いてみます。ここでは、という個の単語からなる文章について考えます。このとき、という並びで単語が出現する確率は同時確率で表されます。これは条件付き確率で書くと、
となります。
これは後述する言語モデルと区別するため、「forward LM」(前向き言語モデル)と呼ばれることが多いです。
最近の言語モデルでは、前向き・後ろ向きの両方の言語モデルを利用することが一般的です。
後ろ向き言語モデルは「backward LM」と呼ばれ、下記のような式で表されます。
イメージとしてはこんな感じです。
2つ合わせて「biLMs」(双方向言語モデル)と呼びます。
「are」という単語の出現確率の計算に「前の単語の並び」だけではなく、「後ろの単語の並び」も利用します。
双方向の並びを学習することで、中間層の重みが言語モデルを最適化するように調整されます。
最終的にはその重みを結合させます。
※上の2つはThe Bidirectional Language Model – Motoki Wu – Mediumより引用
「word2vec
」などの事前学習で獲得した分散表現を入力層の初期パラメータとすることで自然言語処理のタスクの精度が向上することが知られています。
ただし、入力層に渡される文章の分散表現は単語を順番に並べただけであり、当然ですが文脈情報は含まれていません。
一方、双方向の言語モデルで学習された分散表現の場合、文脈情報が考慮されております。
このあたりが「文脈情報を考慮した分散表現」としてSOTAに貢献しているのかなとも思いました。
ELMo
ELMoで学習した単語の分散表現は以下のように目的のタスクに結合させます。
※http://ai2-website.s3.amazonaws.com/publications/semi-supervised-sequence.pdfより引用
通常の単語の分散表現は、文章中の単語を分散表現に変換したものの集まりです。
一方、biLMsの分散表現は、文章を言語モデルに通して得られたものです。
上記の例だと、単語「York」の分散表現は、forward LMを通して、「New」の単語ベクトルの情報も含んでいます。また、backward LMを通して、「is located ...」の単語ベクトルの情報も含んでいます。
Pytorch:テキストの前処理(torchtext)③
はじめに
torchtextの使い方メモ第三弾。
前回の内容は下記参照
kento1109.hatenablog.com
今回の内容は1つだけ。
POSやNERなどのTaggingを考える場合、どのようにtorchtextで読み込めばよいか。
前回まではtorchtextでデータをファイルから読む際、想定されているのは(感情分析等の場合)以下のような形式だった。
I love this moive,1
ただし、学習の目的がTaggingの場合、入力データのフォーマットがそもそも異なる。
NERの場合、だいたいこんな感じだ。
Tom B-PER
is O
my O
friend O
New B-LOC
York I-LOC
is O
always O
crowded O
行ごとに単語が並んでおり、それぞれにタグが付与されている。
文章の区切りは、CoNLLのデータセットの場合、空行で行われる。
こんなフォーマットの場合、どのように処理すればよいだろう。
結論から言うと、SequenceTaggingDataset
を使う。以上だ。
SequenceTaggingDataset
先ほどのデータの場合、以下のようにして読み込む。
from torchtext import data, datasets TEXT = data.Field() LABEL = data.Field() train = datasets.SequenceTaggingDataset( path='test.tsv', fields=[('text', TEXT), ('label', LABEL)])
すると、以下のようにセンテンス単位となる。
print(vars(train[0])) # {'text': ['Tom', 'is', 'my', 'friend'], 'label': ['B-PER', 'O', 'O', 'O']}
必要に応じて
TEXT = data.Field(init_token='<bos>', eos_token='<eos>') LABEL = data.Field(init_token='<bos>', eos_token='<eos>')
とすると、文の先頭と末尾に記号をつけることが出来る。
センテンスの単位であるが、github.com
にあるSequenceTaggingDataset
クラスをみれば分かるが、行が空白の場合、それまでの単語・ラベルをセンテンスとしてまとめる処理をしている。(31行目あたり)
datasets
さえ作れれば、後の処理は前回と同じ。
前回と同じ書き方で辞書も作れる。
TEXT.build_vocab(train) vocab = TEXT.vocab vocab.itos ['<unk>', '<pad>', 'is', 'New', 'Tom', 'York', 'always', 'crowded', 'friend', 'my']
大した内容ではないが、少し嵌ったのでメモした。
Neo4jを触ってみる
はじめに
自然言語処理を勉強していると、オントロジーやシソーラスってのが出てくる。
オントロジーやシソーラスを勉強すると、グラフ理論ってのが出てくる。
グラフ理論を勉強すると、グラフDBとして有名な「Neo4j」が出てくる。
ということで、今回はNeo4jの基本的なことを勉強したのでまとめようと思う。
以下の内容は公式サイトのドキュメントより引用したものが、ほとんどなので詳しく知りたい場合はそっちを見るのが良い。
neo4j.com
Property Graph Model
Neo4jは、Property Graph Modelと呼ばれている。これは、各ノードがプロパティを持ち、ノード情報を格納するモデル。
グラフ構造の基本形はこんな感じ。
各構成要素を簡単に説明をする。
ノード
しばしば「エンティティ」を表現される。
1つの実体を表す構成単位みたいなもの。
プロパティ付きで単一をノードを作成するには、
CREATE (n {name: {key1:value1, key2:value2, ...}})
と書く。
関係
ノード間をつなぐ要素。関係を定義してこそのグラフDB。
Neo4jは有向グラフなので、ノードの向きも合わせて定義する必要がある。
ノード間の関係を定義するには、
CREATE (identifier)-[:name]->(identifier)
と書く。
プロパティ(属性)
ノードや関係などが持つ実際の値。
ノードの例だと、name
やborn
が各プロパティ。関係の場合、roles
がプロパティ。
ラベル
グラフ構造体と呼ばれる複数のノードを1つにまとめるために使われる。オントロジーで言うところの「概念」みたいなもので、 世界(ノード)をラベルを付けて、概念化する。
ノードの図で言うと、上のノードには「Person」ラベルがついており、下のラベルには「Movie」ラベルがついていることが分かる。オントロジー上ではPersonやMovieはあくまで概念に過ぎないが、それをインスタンス化してあげると、各エンティティ(ノード)が出来あがるようなイメージ。
ノード・ラベル・プロパティの関係を図示するとこんな感じ。
(Cypher Query Language(QL)-初級編 #neo4j - クリエーションライン株式会社
より引用)
識別子
ノードや関係性、処理結果などの値を示す構造体。
上の構成要素と異なり、処理中に用いるものに過ぎないが、知っておかないとラベルとの違いに少し混乱する。
こんな感じでノードを表してみる。(emil
が識別子)
emil:Person {name:'Emil'}
そうすると、emil
がPerson {name:'Emil'}というノード情報を保持した構造体として記憶される。
一般的な使用方法としては、ノードと関係を作成するような例。
CREATE (TheMatrix:Movie {title:'The Matrix', released:1999})
CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})
CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix)
作成されたノードに識別子の情報は一切持たないので、そのノードを使って後の文で処理しないのであれば、名前にこだわる必要は全くない。
どういうことかと言うと、
CREATE (TheMatrix:Movie {title:'The Matrix', released:1999})
CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})
を宣言した後で別のクエリで
CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix)
としても上の識別子のノードではなく、新しいノードが作られてしまう。
最低限こんなこととクエリの書き方さえ覚えればOKかと思う。
Stan:NMF
はじめに
今回はNMF(Non-negative Matrix Factorization)について勉強する。
日本語では「非負値行列因子分解」と言い、線形次元手法の1つ。
その名前の通り「観測データの非負性」を仮定したモデル。
「非負性」の制約を設けることで、これに適用可能な観測データはより良い表現ができるらしい。
推薦アルゴリズムなどでも使われている。
詳しいことは省略して実装するうえで大事なところを押さえていく。MCMCではなく、最適化により求める方法は「非負値行列因子分解 - NTTコミュニケーション科学基礎研究所」がとても丁寧だった。
尚、今回の記事のいくつかは「ベイズ推論による機械学習入門 」を参考とした。
NMF
まず、観測データ(各要素が非負値の行列)行列を以下のように表す。
尚、・
は観測データに出現しない潜在変数である。
を要素ごとに書くと次のようになる。
例えば、行列がユーザー×アイテムのレーティング値の場合、はユーザー×潜在変数(ユーザーの嗜好)を表し、行列は潜在変数×アイテム(アイテムの特徴)のようなものを表している。
この場合、ユーザーのアイテムへの評価は、ユーザーの嗜好ベクトルとアイテムの特徴ベクトルの内積で決定される。2つのベクトルの方向が近い(ユーザーの嗜好とアイテムの特徴が近い)ほど、レーティング値は大きくなるようになる。
モデル
今回は観測データの要素が非負の整数を仮定したモデルを考える。
非負の整数なので、データ生成にはポアソン分布が使える。
各要素は以下のように表せたので、
これをそのままポアソン分布にあてはめる。
今回はこれが尤度となる。(計算では対数を取る。)
NMFで目指すのは、をで近似させること。
各要素とが近似できているかどうかは「尤度」で測ることができる。
ポアソン分布の場合、観測値が平均値と一致する値が尤度最大となる値である。この場合、である。観測値が平均から離れるほど尤度は小さくなる
簡単であるが、ポアソン分布での尤度を比べてみた。
dpois(1,1,log=TRUE) [1] -1 dpois(1,5,log=TRUE) [1] -3.390562
観測値と平均値が近いほど、尤度が大きいことが分かる。
NMFではポアソン分布の尤度を大きくすることが、をさせることと同じ意味なのである。
行列全体の尤度は以下のように各要素の積で書ける。
の各要素は、ガンマ分布を取っておくのが良いと思われる。
コード
コードは以下の通り。
(以前、ネットに落ちていたのを少し改良した。)
data{ int<lower=1> D; // row dimension of input int<lower=1> N; // column dimension of input int<lower=1> M; // hidden dimension int X[D,N]; // original data real<lower=0> alpha; //hyperparameter real<lower=0> beta; //hyperparameter } parameters{ matrix<lower=0>[D, M] W; matrix<lower=0>[M, N] H; } model{ //estimate W matrix dist for(d in 1:D){ for(m in 1:M){ W[d,m] ~ gamma(alpha,beta); } } //estimate H matrix dist for(m in 1:M){ for(n in 1:N){ H[m,n] ~ gamma(alpha,beta); } } for(d in 1:D){ for(n in 1:N){ target += poisson_lpmf(X[d,n] | sum(W[d,:])*sum(H[:,n])); } } }
以下のようにしてキックする。
stan.dat <- list(D=nrow(data),D=ncol(data),M=2,X=data,alpha=0.1,beta=0.1) stan.fit <- stan("NMF.stan", data=stan.dat)
ちなみにデータはこんな感じ。
data item1 item2 item3 item4 item5 item6 item7 item8 item9 item10 1 0 2 4 1 0 0 0 1 2 5 2 0 2 3 0 0 0 1 2 2 3 3 1 0 1 4 3 0 1 1 1 0 4 0 0 1 4 5 1 0 1 2 1 5 1 1 5 4 1 1 1 0 1 5
収束はするがデータが小さいので面白い発見は出来ない・・
今回は観測データが非負の整数を取ったが、取る範囲が広いほど推論が難しい気がする。観測データが二値でベルヌーイ分布でやってみるのも面白いかもしれない。
Pytorch:ライブラリの誤差関数の構造を理解する
はじめに
今まで当たり前のように誤差関数を使っていた。
既に用意されたものであればそれで問題ない。
しかし、誤差関数を自作したいと思った場合、
ライブラリの誤差関数の構造を理解している必要がある。
そんなわけでライブラリの誤差関数について調べたのでメモ。
簡単な復習
簡単に使い方を復習する。
ライブラリの誤差関数を利用する場合、以下のような使い方をする。
import torch import torch.nn as nn import torch.nn.functional as F net = Net() outputs = net(inputs) criterion = nn.MSELoss() loss = criterion(outputs, targets) loss.backward()
ネットワーク
今回はシンプルな以下のネットワークを考える。
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.fc1 = nn.Linear(32, 16) self.fc2 = nn.Linear(16, 1) def forward(self, x): x = F.relu(self.fc1(x)) x = self.fc2(x) return x
パラメータは以下の通り
params = list(net.parameters()) print(params[0].size()) torch.Size([16, 32]) print(params[1].size()) torch.Size([16]) print(params[1]) Parameter containing: tensor([ 0.0982, 0.0495, 0.1656, -0.1646, 0.1014, -0.0163, -0.0873, 0.0418, -0.0404, 0.1556, -0.1247, 0.0236, -0.0651, 0.0960, 0.1342, 0.1203])
入力
以下のように入力データを作成する。
torch.manual_seed(1) inputs = torch.randn(10, 32) targets = torch.randn(10) targets = targets.view(1, -1) print(targets) tensor([[ 0.1924, 0.7161, -0.8120, -1.4617, 0.2328, 0.1896, -0.2204, 0.1491, 0.0100, -0.1243]])
出力
出力結果は以下の通り
outputs = net(inputs) outputs = outputs.view(1, -1) print(outputs) tensor([[-0.2504, 0.0378, 0.1485, 0.1909, -0.1019, 0.2417, 0.1653, 0.1359, -0.1187, 0.0876]])
誤差計算
まず、誤差関数のインスタンスを生成する。
criterion = nn.MSELoss()
次に出力結果と真の値を誤差関数の入力として、誤差を求める。
outputs = outputs.view(1, -1) loss = criterion(outputs, targets) print(loss) tensor(0.4635)
Backprop
求めた誤差からパラメータの勾配を計算する。
net.zero_grad() print(net.fc2.weight.grad) > None loss.backward() print(net.fc2.weight.grad) tensor([[ 0.0611, 0.0976, -0.2338, 0.1976, 0.0020, -0.0788, -0.1145, -0.1976, 0.0699, -0.3314, 0.2379, -0.0534, 0.0729, -0.0008, -0.2456, -0.2657]])
はじめは勾配がNone
となっているが、loss.backward()
により、勾配ベクトルが計算されていることが分かる。
パラメータ更新(最適化)
今回は確率的勾配降下法(SGD)を用いた。
learning_rate = 0.01 for f in net.parameters(): f.data.sub_(f.grad.data * learning_rate)
sub_
でその値から引数の値を引き算する。
更新後の誤差を確認する。
outputs = net(inputs) outputs = outputs.view(1, -1) loss = criterion(outputs, targets) print(loss) tensor(0.3956)
誤差が減少していることが確認できた。
実際は多くの最適化関数も用意されているのでそちらを利用する方が良い。
import torch.optim as optim optimizer = optim.SGD(net.parameters(), lr=0.01) optimizer.step()
参考:
Neural Networks — PyTorch Tutorials 0.4.1 documentation
基本的なことを復習したところで、もう少し深く理解する。
nn.MSELoss()
ソースを覗いてみる。
torch.nn.modules.loss — PyTorch master documentation
class MSELoss(_Loss): def __init__(self, size_average=None, reduce=None, reduction='elementwise_mean'): super(MSELoss, self).__init__(size_average, reduce, reduction) def forward(self, input, target): return F.mse_loss(input, target, reduction=self.reduction)
※長かったのでコメントは省略
実装部分は意外とシンプルでコンストラクタとforward関数のみのクラス。
他の誤差関数も同じようなクラス構造。
F.mse_loss
自体が実際に平均二乗誤差を計算している関数。
このクラスが継承している_Loss
とは何か。
class _Loss(Module): def __init__(self, size_average=None, reduce=None, reduction='elementwise_mean'): super(_Loss, self).__init__() if size_average is not None or reduce is not None: self.reduction = _Reduction.legacy_get_string(size_average, reduce) else: self.reduction = reduction
このクラス自体はModule
を継承している。
てな感じでみていくと誤差関数のクラスは以下の構造でOKと思われる。
class LossFunction(nn.Module): def __init__(self): super(LossFunction, self).__init__() def forward(self, inputs, targets): loss = function(inputs, targets) return loss
ただし、必ずクラス構造である必要はなく、誤差関数だけでも良い。
実際に以下でも正しくパラメータ更新ができた。
loss = F.mse_loss(outputs, targets)
Stan:TwoCountryQuiz②
はじめに
前回は「コワイ本」のTwoCountryQuizのタスクに取り組んだ。
今回はこの続きについてまとめる。
ラベルスイッチング
前回のモデルは事後分布が二峰性をもつような分布となっていた。
この原因として「ラベルスイッチング」が原因だと述べた。
混合モデルを考える場合、これはやはり避けて通れない。
はじめに「何故そう考えたか」について尤度計算を交えて説明する。
まず、今回のデータ(TwoCountryQuiz)は以下のような行列だった。
A | B | ... | H | |
人1 | 1 | 0 | ... | 1 |
人2 | 1 | 0 | ... | 1 |
... | ... | ... | ... | ... |
人8 | 0 | 1 | ... | 0 |
そして、
- 人1が「タイ人かどうか」
- 問題Aが「タイの歴史に関するものかどうか」
を推論することが今回の目的だった。
これは以下の推論でも構わない。
そして、当たり前だがモデルはユーザーがどっちを想定しているかなど知らない。
この推論が同じ結果になることを尤度計算により確認する。
全体尤度の計算は以下の通り。
対数を取ると以下の通り。
log_sum_exp
を使うと以下の通り。
※各式の意味に関しては前回参照
実際に人1の問題Aに関する尤度を計算する。
尚、計算時点でのパラメータは以下で与えられたとする。
計算結果は以下の通り。
他のパラメータを固定したもとでに更新すると尤度はどう変化するか確認する。
計算結果は以下の通り。
尤度が小さくなることが分かる。
これは、「各人がタイ人(モルドバ人)であるとき、問題がモルドバ(タイ)の歴史」としたの場合の尤度に近い(確率なので同じではない)ので尤度が小さくなる。
では、この条件下でに更新すると尤度はどう変化するか。
最初のパラメータでの尤度と等しくなる。
つまり、に関係なく、とでの尤度は等しくなるというわけである。
そんな訳で尤度を大きくするパラメータは、でもでも[z_1]によっては尤度が同じということになり、事後分布が二峰性を持つことになるのである。
パラメータの制約
では、どうすればよいか。
問題はでも尤度が同じになるということだった。では、が取れる範囲を制約してしまおうというのが1つの対処法である。
例えば、の制約を設けることで、その制約下で尤度を大きくするためのを探索する。
実装方法としては以下のような書き方が考えられる。
parameters { real<lower=0.5, upper=1> init_x_prob; real<lower=0, upper=1> other_x_prob[nx-1]; } transformed parameters { real x_prob[nx]; x_prob[1] = init_x_prob; for(i in 1:nx-1) x_prob[i+1] = other_x_prob[i]; }
これでの乱数生成をに制限する。
※ベクトルの1要素のみに制約を設定する方法は既にグーグルフォーラムで挙がっていたので参考とさせて頂いた。
Google グループ
モデリング
全体の
Stan
は以下の通り。の制約以外はほとんど前と同じ。
data{ int<lower=1> nx; // num person int<lower=1> nz; // num question int<lower=0, upper=1> k[nx, nz]; // person * question } parameters { real<lower=0.5, upper=1> init_x_prob; real<lower=0, upper=1> other_x_prob[nx-1]; real<lower=0, upper=1> z_prob[nz]; real<lower=0, upper=1> alpha; real<lower=0, upper=alpha> beta; } transformed parameters { real x_prob[nx]; x_prob[1] = init_x_prob; for(i in 1:nx-1) x_prob[i+1] = other_x_prob[i]; } model { vector[4] lp; //for(i in 1:nx) // x_prob[i] ~ beta(a,b);; // prior //for(j in 1:nz) // z_prob[j] ~ beta(a,b);; // prior for(i in 1:nx){ for(j in 1:nz){ lp[1] = (log1m(x_prob[i]) + log1m(z_prob[j]) + bernoulli_lpmf(k[i, j]| alpha)); lp[2] = (log(x_prob[i]) + log(z_prob[j]) + bernoulli_lpmf(k[i, j]| alpha)); lp[3] = (log1m(x_prob[i]) + log(z_prob[j]) + bernoulli_lpmf(k[i, j]| beta)); lp[4] = (log(x_prob[i]) + log1m(z_prob[j]) + bernoulli_lpmf(k[i, j]| beta)); target += log_sum_exp(lp); } } }
結果の確認
実行結果は以下の通り。
stan.fit <- stan("TwoCountryQuiz.stan",data=stan.dat,pars=c('x_prob','z_prob'))
summary(stan.fit) $summary mean se_mean sd 2.5% 25% 50% x_prob[1] 0.8257743 0.004583606 0.1372088 0.528647083 0.73226802 0.8620697 x_prob[2] 0.7505101 0.013206512 0.2454967 0.077761085 0.65885906 0.8371625 x_prob[3] 0.2848253 0.011180096 0.2422784 0.008530586 0.09599669 0.2132672 x_prob[4] 0.3344115 0.008289728 0.2467132 0.017356223 0.13546493 0.2774034 x_prob[5] 0.7564982 0.013374523 0.2441466 0.090157994 0.66361202 0.8418284 x_prob[6] 0.7106300 0.011092027 0.2467630 0.087634316 0.57762401 0.7808384 x_prob[7] 0.2776645 0.010963704 0.2489371 0.007548964 0.08553814 0.1958664 x_prob[8] 0.2905203 0.010156586 0.2449326 0.007862108 0.09827746 0.2208900 z_prob[1] 0.7189481 0.010696188 0.2444943 0.085165215 0.60045169 0.7911544 z_prob[2] 0.2712665 0.010862294 0.2398265 0.009096772 0.08682635 0.1975436 z_prob[3] 0.3250752 0.007364386 0.2474678 0.015272428 0.12510105 0.2692455 z_prob[4] 0.7139130 0.011395878 0.2432047 0.099609097 0.59076957 0.7835717 z_prob[5] 0.7556119 0.012485880 0.2441034 0.091712754 0.65056501 0.8450558 z_prob[6] 0.2471072 0.012882019 0.2460234 0.004999654 0.06493085 0.1560054 z_prob[7] 0.2848787 0.012258488 0.2448348 0.010895594 0.09539916 0.2123684 z_prob[8] 0.7588505 0.012929736 0.2370986 0.105125385 0.66719031 0.8418040 lp__ -76.6538982 0.173507422 4.0323283 -85.464497249 -79.14733785 -76.2325885 75% 97.5% n_eff Rhat x_prob[1] 0.9405903 0.9950119 896.0852 1.002287 x_prob[2] 0.9309936 0.9919926 345.5532 1.005609 x_prob[3] 0.4083455 0.9058809 469.6110 1.002421 x_prob[4] 0.4877216 0.9151902 885.7355 1.002222 x_prob[5] 0.9374089 0.9953916 333.2304 1.006969 x_prob[6] 0.9068108 0.9915599 494.9237 1.002667 x_prob[7] 0.4008561 0.9103599 515.5425 1.004395 x_prob[8] 0.4202415 0.9004982 581.5642 1.004779 z_prob[1] 0.9100841 0.9919731 522.4915 1.004377 z_prob[2] 0.3818967 0.9177776 487.4739 1.005363 z_prob[3] 0.4726530 0.9227644 1129.1828 1.001313 z_prob[4] 0.9048383 0.9913114 455.4579 1.002746 z_prob[5] 0.9364634 0.9945814 382.2165 1.003059 z_prob[6] 0.3527158 0.9079530 364.7412 1.005107 z_prob[7] 0.4027377 0.9158467 398.9079 1.005976 z_prob[8] 0.9324511 0.9944236 336.2627 1.007949 lp__ -73.6854277 -70.1485710 540.1018 1.002221
収束判定は問題なさそう。
確率も前回のような0.5周辺ではなく、きっちり分かれている。
事後分布
前回、二峰性だった事後分布を確認する。
stan_dens(stan.fit,pars ="x_prob[1]",separate_chains = TRUE)
単峰性の分布であることが確認できる。chainによるバラツキもあまりない。裾野が長いのが気になるが、これはサンプル数の問題と思われる。(後で検証してみる。)
一応、制約を加えなかったパラメータも確認する。
stan_dens(stan.fit,pars ="x_prob[3]",separate_chains = TRUE)
二峰性は見られないことが分かる。
トレース結果
stan_trace(stan.fit,pars = "x_prob[1]")
制約を加えたこともあり、制約範囲内で動いている。(非常に分散が大きいのが少し気にがるが・・)
同様に制約を加えなかったパラメータも確認する。
stan_trace(stan.fit,pars = "x_prob[3]")
分散は大きいがあたりで多くサンプリングされていることが分かる。
サンプル数を増やしてモデリング
最後にサンプル数を24人に増やして再度モデリングを行った。
(データセットは単純に8人分のデータ×3しただけ)
結果は以下の通り。
summary(stan.fit) $summary mean se_mean sd 2.5% 25% x_prob[1] 0.88909723 0.0015264536 0.09654140 6.379189e-01 0.84088896 x_prob[2] 0.88306846 0.0017073929 0.10798501 5.979277e-01 0.83358725 x_prob[3] 0.17779696 0.0022253553 0.14074382 6.284815e-03 0.06514292 x_prob[4] 0.24431969 0.0026401563 0.16697814 1.462065e-02 0.11160276 x_prob[5] 0.88339388 0.0016822210 0.10639300 5.915050e-01 0.83695690 x_prob[6] 0.82474879 0.0021329165 0.13489748 4.874794e-01 0.74488734 x_prob[7] 0.15902617 0.0020105498 0.12715834 5.048530e-03 0.05834003 x_prob[8] 0.17922045 0.0021341862 0.13497779 7.067403e-03 0.07218645 x_prob[9] 0.88388361 0.0016258287 0.10282643 6.155001e-01 0.83528646 x_prob[10] 0.88482958 0.0016178430 0.10232138 6.219439e-01 0.83460776 x_prob[11] 0.17376721 0.0021239378 0.13432962 7.741369e-03 0.07019395 x_prob[12] 0.24658736 0.0025807395 0.16322030 1.360339e-02 0.11582604 x_prob[13] 0.88225775 0.0016771932 0.10607501 6.072548e-01 0.82842426 x_prob[14] 0.82616041 0.0021704359 0.13727042 4.905692e-01 0.74875851 x_prob[15] 0.15963763 0.0021007873 0.13286546 4.750483e-03 0.05315915 x_prob[16] 0.18142896 0.0021570449 0.13642350 6.887512e-03 0.07567072 x_prob[17] 0.88654126 0.0016228331 0.10263698 6.202509e-01 0.83865202 x_prob[18] 0.88467146 0.0016392687 0.10367646 6.029201e-01 0.83315147 x_prob[19] 0.17220191 0.0021321826 0.13485107 6.665817e-03 0.06455919 x_prob[20] 0.24572632 0.0026661863 0.16862443 1.241102e-02 0.11079253 x_prob[21] 0.88259919 0.0016423417 0.10387081 6.201909e-01 0.83208995 x_prob[22] 0.82613928 0.0022048355 0.13944604 4.796073e-01 0.75000947 x_prob[23] 0.15867396 0.0020519952 0.12977957 5.023390e-03 0.05646634 x_prob[24] 0.18281938 0.0021713075 0.13732554 9.281820e-03 0.07586258 z_prob[1] 0.90059791 0.0013291991 0.08406593 6.808949e-01 0.85748083 z_prob[2] 0.08281926 0.0011276441 0.07131847 2.591367e-03 0.02921736 z_prob[3] 0.16589135 0.0018243489 0.11538195 6.090128e-03 0.07509763 z_prob[4] 0.89537937 0.0013233079 0.08369334 6.885263e-01 0.85128247 z_prob[5] 0.94851652 0.0007727115 0.04887056 8.209348e-01 0.92862688 z_prob[6] 0.05198457 0.0007983830 0.05049418 1.322733e-03 0.01514927 z_prob[7] 0.09923228 0.0012919329 0.08170901 3.422783e-03 0.03655729 z_prob[8] 0.94831005 0.0008012587 0.05067605 8.110328e-01 0.92770249 lp__ -165.12638755 0.1230833587 4.69380930 -1.754599e+02 -167.91580233 50% 75% 97.5% n_eff Rhat x_prob[1] 0.91570868 0.9632312 0.9965173 4000.000 1.0000139 x_prob[2] 0.91501176 0.9641354 0.9965150 4000.000 0.9997085 x_prob[3] 0.14653449 0.2576420 0.5169342 4000.000 0.9991991 x_prob[4] 0.21428473 0.3508085 0.6205980 4000.000 0.9994822 x_prob[5] 0.91291980 0.9619288 0.9971203 4000.000 0.9994962 x_prob[6] 0.85376265 0.9305850 0.9914166 4000.000 0.9991922 x_prob[7] 0.12972880 0.2282888 0.4739765 4000.000 0.9999588 x_prob[8] 0.15058849 0.2521316 0.5022369 4000.000 0.9998293 x_prob[9] 0.91202499 0.9622890 0.9963306 4000.000 0.9999095 x_prob[10] 0.91357950 0.9628045 0.9967515 4000.000 0.9998023 x_prob[11] 0.14187091 0.2502796 0.4986213 4000.000 0.9994550 x_prob[12] 0.22103501 0.3514679 0.6198173 4000.000 1.0002860 x_prob[13] 0.91473044 0.9630547 0.9967746 4000.000 0.9994394 x_prob[14] 0.85865190 0.9357102 0.9948808 4000.000 0.9998928 x_prob[15] 0.12518733 0.2320583 0.4899430 4000.000 0.9995863 x_prob[16] 0.15374736 0.2578792 0.5127041 4000.000 0.9997273 x_prob[17] 0.91694762 0.9645227 0.9965024 4000.000 0.9995645 x_prob[18] 0.91524153 0.9621940 0.9968299 4000.000 0.9993828 x_prob[19] 0.14001510 0.2524526 0.4938968 4000.000 0.9995974 x_prob[20] 0.21958370 0.3541058 0.6335430 4000.000 0.9998727 x_prob[21] 0.91241451 0.9615494 0.9964914 4000.000 0.9999107 x_prob[22] 0.85743842 0.9350120 0.9944499 4000.000 0.9998915 x_prob[23] 0.12682041 0.2299418 0.4853042 4000.000 1.0002858 x_prob[24] 0.15102426 0.2611727 0.5188826 4000.000 0.9996171 z_prob[1] 0.92263662 0.9645727 0.9961818 4000.000 0.9993239 z_prob[2] 0.06412635 0.1186724 0.2634773 4000.000 0.9995061 z_prob[3] 0.14696709 0.2362540 0.4324484 4000.000 0.9992923 z_prob[4] 0.91543158 0.9591549 0.9955493 4000.000 0.9997276 z_prob[5] 0.96217694 0.9843390 0.9986652 4000.000 0.9999034 z_prob[6] 0.03690516 0.0717256 0.1881421 4000.000 1.0008297 z_prob[7] 0.07866694 0.1412252 0.2983229 4000.000 0.9998847 z_prob[8] 0.96319514 0.9854857 0.9990697 4000.000 0.9995410 lp__ -164.78851211 -161.7427068 -157.0022084 1454.294 1.0035093
全体的に分散が小さくなっていることが分かる。
事後分布
サンプル数を増やしたことで裾野が短くなっていることが分かる。
トレース結果
トレース結果からも分散が小さくなっていることが分かる。
さいごに
TwoCountryQuizを取り組んだが、実力不足もあり時間がかかってしまった。しかし、ラベルスイッチングやパラメータの制約に関して理解が深まったので良かった。
次はTwoCountryQuizを応用した別のタスクをを検討したい。
Stan:TwoCountryQuiz①
はじめに
前回は「コワイ本」のTwentyQuestionsを取り組んだ。
kento1109.hatenablog.com
今回はそれをもう少し難しくした「TwoCountryQuiz」に取り組む。
TwentyQuestionsのコードを少し変えるだけと思っていたが、実はとても難しく嵌りどころがたくさんだった・・
それはともかく、さっそく問題を見ていく。
TwoCountryQuiz
問題の設定としては以下の通り(本文より引用)
あるグループの人々が歴史クイズを出されて、各人の各回答は正答または誤答として得点化される。人々の一部はタイ人、一部はモルドバ人である。問題の一部はタイの歴史についてのものであり、モルドバ人よりもタイ人が答えを知ってそうな問題である。残りの問題はモルドバの歴史についてのものであり、タイ人よりもモルドバ人が答えを知っていそうな問題である。
ここでは、誰がタイ人で誰がモルドバ人なのか分からず、問題の内容も分からない。データからどの人が同じ国から来た人なのか、どの問題が彼らの国と関係するのかを推定してほしい。
ちなみにモルドバ人はルーマニア語を話すラテン系の民族集団らしい。
モルドバ人 - Wikipedia
モデルの作成
ここでは2種類の回答を仮定する。
人の国籍が問題の起源と合致する場合、回答率は高い確率で正しい。(タイ人のタイの歴史の問題に対する正答率)
一方、ある人が他の国について尋ねられた場合には、回答は低い確率で正しい。(タイ人のモルドバの歴史の問題に対する正答率)
このアイデアをグラフィカルモデルにすると以下となる。
この場合、はそれぞれ国のインデックスを示す。
今回、は以下のように定義する。
正答率はある人が自身の歴史の問題に対する正答率、正答率はある人が他国の歴史の問題に対する正答率。仮定よりの制約を設ける。
離散パラメータの対応
WinBUGS
の場合、このグラフィカルモデルで問題ない。ただし、そのままStan
で扱うことは出来ない。パラメータは(各国を示すインデックス)が離散値だからである。離散パラメータに対応する最もシンプルな方法は「周辺化除去(marginalization )」である。詳しくは以前に書いた。
どう実装するべきかと調べていたら、TwoCountryQuizってどうやってStan
で書くのかっていうディスカッションがあった。
groups.google.com
今回の場合は各離散パラメータの値に応じて確率分布が異なる(と)ので、周辺化除去ができない。なので、パラメータを確率変数として定義するのが妥当なアイデアみたいだ。は番目の人がタイ人(モルドバ人)である確率、は問題がタイ(モルドバ)の歴史に関する問題の確率を表す。
これでStan
での実装が可能となる。
モデリング
Googleでのディスカッションのアイデアに基づき以下のように実装した。
data{ int<lower=1> nx; // num person int<lower=1> nz; // num question int<lower=0, upper=1> k[nx, nz]; // person * question real<lower=0> a; // hyper parameter real<lower=0> b; // hyper parameter } parameters { real<lower=0, upper=1> x_prob[nx]; real<lower=0, upper=1> z_prob[nz]; real<lower=0, upper=1> alpha; real<lower=0, upper=alpha> beta; } model { vector[4] lp; for(i in 1:nx) x_prob[i] ~ beta(a,b);; // prior for(j in 1:nz) z_prob[j] ~ beta(a,b);; // prior for(i in 1:nx){ for(j in 1:nz){ lp[1] = (log1m(x_prob[i]) + log1m(z_prob[j]) + bernoulli_lpmf(k[i, j]| alpha)); lp[2] = (log(x_prob[i]) + log(z_prob[j]) + bernoulli_lpmf(k[i, j]| alpha)); lp[3] = (log1m(x_prob[i]) + log(z_prob[j]) + bernoulli_lpmf(k[i, j]| beta)); lp[4] = (log(x_prob[i]) + log1m(z_prob[j]) + bernoulli_lpmf(k[i, j]| beta)); target += log_sum_exp(lp); } } }
※log1m(a)
はを効率的に計算するための関数
ポイントは尤度計算の部分。
今回は人と問題の組み合わせが2×2なので4つの式に基づき尤度が計算される。
k[i, j] |
タイの歴史 | モルドバの歴史 |
タイ人 | lp[1] |
lp[3] |
モルドバ人 | lp[4] |
lp[2] |
組み合わせの計算
なぜこのような尤度計算になるのか簡単な例で考える。
あるサイコロの出目の確率は全て「1/6」のとき、「6」が出る確率は当然「1/6」である。
次にサイコロが2つの場合を考える。
Aのサイコロの「6」が出る確率はであり、Bのサイコロの「6」が出る確率とする。また、Aを投げる確率がであるとする。
このとき、「6」が出る確率は以下のような計算となる。
このとき、AとBを投げる確率は排反事象であり、計算は「AまたはB」の組み合わせの確率(足し算)で求める。
これにもう一つ条件を加える。
投げ手(ホスト・ゲスト)によってサイコロの出目が変わるとする。ホストが投げる確率を、サイコロと投げ手によるサイコロの「6」が出る確率は以下とする。
ホスト | ゲスト | |
A | ||
B |
このとき、「6」が出る確率は以下のような計算となる。
これの対数を取ると今回のような計算になることが分かる。
推論結果
以下のようにしてデータを渡す。
尚、実験データは以下に作った。
github.com
TwoCoutryQuiz <- read.csv("TwoCountryQuiz.txt") stan.dat <- list(nx=nrow(TwoCoutryQuiz),nz=ncol(TwoCoutryQuiz),k=TwoCoutryQuiz,a=1.0,b=1.0) stan.fit <- stan("TwoCountryQuiz.stan",data=stan.dat)
推論結果は以下の通り。
summary(stan.fit) $summary mean se_mean sd 2.5% 25% 50% x_prob[1] 0.53029713 0.036775653 0.3541604 0.011127718 0.15609154 0.5985944 x_prob[2] 0.52931333 0.037102322 0.3561591 0.010755941 0.15249905 0.5860287 x_prob[3] 0.47212875 0.030683095 0.3283253 0.011333054 0.15645041 0.4296219 x_prob[4] 0.47621507 0.025471547 0.3018463 0.019701274 0.19461178 0.4615459 x_prob[5] 0.53508411 0.037486953 0.3561731 0.013228531 0.16077255 0.6045322 x_prob[6] 0.52638124 0.031305999 0.3259938 0.022406221 0.20644299 0.5571316 x_prob[7] 0.46897241 0.033495749 0.3356998 0.015722103 0.14365877 0.4235867 x_prob[8] 0.47378394 0.030727943 0.3240059 0.013129741 0.16318500 0.4420749 z_prob[1] 0.52560515 0.031547300 0.3286305 0.018388960 0.20320846 0.5550213 z_prob[2] 0.47342530 0.032388415 0.3323511 0.010483992 0.15403045 0.4412354 z_prob[3] 0.48138335 0.024288168 0.2972166 0.021458533 0.21662389 0.4679963 z_prob[4] 0.52477091 0.030564488 0.3238213 0.021646336 0.20497649 0.5546996 z_prob[5] 0.53043538 0.037612554 0.3594749 0.009185296 0.14711397 0.5980563 z_prob[6] 0.46748793 0.036646445 0.3567124 0.008275019 0.11662093 0.4036751 z_prob[7] 0.46928308 0.031075766 0.3271401 0.012422863 0.15836508 0.4300338 z_prob[8] 0.53520960 0.036506380 0.3559883 0.011017414 0.15899693 0.6020018 alpha 0.84288128 0.004903208 0.1328105 0.495493896 0.77739007 0.8791112 beta 0.08643854 0.003764911 0.1007688 0.001575219 0.01844138 0.0486498 lp__ -76.06556122 0.129462953 3.7709042 -84.645570937 -78.28974038 -75.5811353 75% 97.5% n_eff Rhat x_prob[1] 0.8788707 0.9907077 92.74258 1.059581 x_prob[2] 0.8841432 0.9894480 92.14802 1.062257 x_prob[3] 0.7910437 0.9855612 114.50130 1.044380 x_prob[4] 0.7592288 0.9719845 140.43037 1.040285 x_prob[5] 0.8874325 0.9919339 90.27383 1.065438 x_prob[6] 0.8409497 0.9847431 108.43353 1.052919 x_prob[7] 0.8063384 0.9830522 100.44369 1.057658 x_prob[8] 0.7888879 0.9829815 111.18311 1.049953 z_prob[1] 0.8405482 0.9845766 108.51542 1.050264 z_prob[2] 0.8033599 0.9829672 105.29670 1.055173 z_prob[3] 0.7437687 0.9731136 149.74652 1.035735 z_prob[4] 0.8337083 0.9829047 112.24744 1.046241 z_prob[5] 0.8862753 0.9926954 91.34220 1.062050 z_prob[6] 0.8405933 0.9903413 94.74860 1.060763 z_prob[7] 0.7935304 0.9819733 110.82146 1.054304 z_prob[8] 0.8860739 0.9926517 95.08980 1.058714 alpha 0.9451407 0.9944410 733.67550 1.002910 beta 0.1123582 0.3918710 716.37944 1.002677 lp__ -73.4013764 -69.9297720 848.39868 1.002594
これを見る限りRHat
は問題なさそうである。
ただし、n_eff
が小さく見直しの余地あり。
事後分布とサンプルのトレースを確認すればモデルが良くないことが分かる。
事後分布
stan_dens(stan.fit,pars ="x_prob[1]",separate_chains = TRUE)
ここから分かる問題は
- 明らかに分布が多峰性となっている
- chainによる分布のバラつきが大きい。
ということである。
ちなみに理想的な事後分布はこんな感じ。
トレース結果
stan_trace(stan.fit,pars = "x_prob[1]")
理想としてはサンプリングがどこかの値付近でバラついてほしい。
しかし、取り得る範囲0~1の間を広く動いている。
(その結果、平均が0.5あたりになるのであろう。)
どう考えてもうまくいっていない・・
なぜこのような問題が起きたのか。
原因はおそらく次の2つだと考えている。
- データ数の不足
- ラベルスイッチング
次回、この問題に対する対処方法を考えたい。