gaiaskyの技術メモ

ベイズ、機械学習などのデータサイエンスがテーマ。言語はpythonで。

アイドルの人気投票における多項ロジスティック回帰の適用例

以前、アヒル本の多項ロジスティック回帰についてpymc3版を紹介した。

gaiasky.hatenablog.com

今回は、多項ロジスティック回帰の例として、「μ's とAqours の人気の差」を題材とした記事があったので、これを紹介したいと思う。 これらの記事ではモデルはStanで実装されていたので、これをpymc3でトレースしてみることにする。

http://mikuhatsune.hatenadiary.com/entry/20170320/1490011326

http://abrahamcow.hatenablog.com/entry/2017/06/24/154902

データ生成のモデルは以下の通り
  • 各9人が所属する2つのグループがある。
    • メンバーの効果(人気)を b_i、μ'sグループの効果(人気)を gとする。
    • メンバー iが所属するグループを G_iとする。 G_i=1はμ's、 G_i=0はAqousに所属。
  • 各メンバーの投票数 v_i, i=1, \dots 18とする。
    • 得票数は多項分布に従う。 v_i \sim Multinomial(p_i)
    •  p_i = softmax(\alpha_i)
    •  \alpha_i = g * G_i + b_i
まずはデータの作成
dat_str = """name vote group
矢澤にこ 384 muse
西木野真姫 384 muse
南ことり 370 muse
東條希 336 muse
綾瀬絵里 283 muse
園田海未 263 muse
小泉花陽 251 muse
星空凛 226 muse
津島善子 190 aqours
黒澤ルビィ 171 aqours
国木田花丸 155 aqours
渡辺曜 141 aqours
高坂穂乃果 128 muse
松浦果南 110 aqours
黒澤ダイヤ 103 aqours
桜内梨子 93 aqours
小原鞠莉 80 aqours
高海千歌 74 aqours"""
df = pd.read_csv(io.StringIO(dat_str), sep=' ')

名前とグループをコード化する。

name_cate = pd.Categorical(df.name)
group_cate = pd.Categorical(df.group)
df['name_code'] = name_cate.codes
df['group_code'] = group_cate.codes
df.sort_values('name_code', inplace=True) # name_code順に並び替える 
df.head()
name vote group name_code group_code
2 南ことり 370 muse 0 1
10 国木田花丸 155 aqours 1 0
5 園田海未 263 muse 2 1
16 小原鞠莉 80 aqours 3 0
6 小泉花陽 251 muse 4 1
plt.figure(figsize=(9,6))
sns.barplot(x='vote', y='name', data=df, hue='group', orient='h')

f:id:gaiasky:20190908195638p:plain

pymc3によるモデルの実装とMCMC実行
with pm.Model() as model:
    # alpha[i] = g*G[i] + b[i] 
    # 各個人の効果
    b_mu = pm.Normal('b_mu', mu=0, sd=5)
    b_sd = pm.HalfCauchy('b_sd', beta=5)
    b = pm.Normal('b', mu=b_mu, sd=b_sd, shape=len(df)-1)
    b_f = pm.Deterministic('b_f', tt.concatenate([tt.zeros(1), b])) # 一人目の効果を0で固定
    # グループ効果
    g = pm.Normal('g', mu=0, sd=10)
    # 合計効果
    alpha = pm.Deterministic('alpha', g*df.group_code+b_f[df.name_code])
    # 
    p = pm.Deterministic('p', tt.nnet.softmax(alpha))
    y = pm.Multinomial('y', df.vote.sum(), p, observed=df.vote)
with model:
    trace = pm.sample(5000)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [g, b, b_sd, b_mu]
Sampling 2 chains: 100%|██████████| 11000/11000 [01:05<00:00, 168.16draws/s]
The number of effective samples is smaller than 25% for some parameters.
pm.traceplot(trace, varnames=['g','b','p'])

f:id:gaiasky:20190908195834p:plain

推定結果の抽出

グループ(μ's)効果の確認。μ'sは、投票にに対してAquosより2.3倍の影響力がある。

pm.plot_posterior(np.exp(trace.get_values('g'))) #muse効果

f:id:gaiasky:20190908200030p:plain

可視化用のコード。

# 推定結果をグラフ表示するための整形関数
def mk_data4graph(df, trace, vname, do_exp):
    skip = 20
    if do_exp:
        estimate_params = np.exp(pm.trace_to_dataframe(trace, varnames=[vname]).loc[::skip]).T
    else:
        estimate_params = pm.trace_to_dataframe(trace, varnames=[vname]).loc[::skip].T        
    estimate_params.index = name_cate.categories
    
    # 平均値が高いキャラでソートする
    estimate_params['mean'] = estimate_params.mean(axis=1)
    estimate_params_sorted = estimate_params.sort_values('mean', ascending=False)
    estimate_params_sorted = estimate_params_sorted.drop(columns='mean')
    
    # グラフ用データに変換し、グループ名を追加
    data4graph = pd.merge(pd.melt(estimate_params_sorted.T), 
                          df[['name','group']], left_on='variable', right_on='name')
    return data4graph

各メンバーの投票率の可視化。

# 各メンバーの投票率)
plt.figure(figsize=(6,9))
sns.violinplot(x='value', y='variable', data=mk_data4graph(df, trace,'p', False),
               hue='group', palette=sns.color_palette("Set1", 2), orient='h',)
plt.xlabel('投票率$p_i$')
plt.title('メンバーの投票率$p_i$の推定値')

f:id:gaiasky:20190908200439p:plain

各メンバーの人気の可視化。

# 各メンバーの人気(exp(b))
plt.figure(figsize=(6,9))
sns.violinplot(x='value', y='variable', data=mk_data4graph(df, trace,'b_f', True), 
               hue='group', palette=sns.color_palette("Set1", 2)[::-1], orient='h')
plt.xlabel('人気$\exp(b)$')
plt.title('メンバーの人気$\exp(b_i)$の推定値')

f:id:gaiasky:20190908200528p:plain

モデルを定式化でき、影響の大きさが定量的に把握できるのが、ロジスティック回帰はやはり有用だと思う。

以上

ベイズ生存時間分析(Weibull分布)

データ分析では正規分布を仮定することが多いが、生存時間分析・信頼性工学では、ワイブル分布を仮定することが多い。これはワイブル分布が、形状パラメータ・尺度パラメータによって、所謂バスタブカーブの3要素(初期故障、偶発故障、摩耗)を表現可能であるからと思う。一方、データには打ち切りが存在することが一般的で、そのために、ノンパラメトリックモデルのカプランマイヤー法で推定することが多いが、ベイズ推定を用いるとパラメトリックモデルで打ち切りデータもうまく扱えるようだ。

Bayesian Inference With Stan ~062~ | |

上記の記事はstanで実装されているため、今回はこの例題をpymcで実装してみることにした。

# データセットの読み込み。
df = load_rossi()

rossiデータの概要。

  • 再逮捕までの時間(週単位)
  • 右側打ち切り(52週)
  • 共変量あり(fin=1, 経済的支援あり)
df.head()
week arrest fin age race wexp mar paro prio
0 20 1 0 27 1 0 0 1 3
1 17 1 0 18 1 0 0 1 8
2 25 1 0 19 0 1 0 1 13
3 52 0 1 23 1 1 1 1 1
4 52 0 0 19 0 1 0 1 3
df_obs = df[df.arrest==1].week.values # 打ち切りなしのデータ
df_fin_obs = df[df.arrest==1].fin.values
df_cens = df[df.arrest==0].week.values # 右側打ち切りのデータ
df_fin_cens = df[df.arrest==0].fin.values

ワイブル分布の関数定義

# pdf
def weibProbDist(x, a, b):
    return (a / b) * (x / b) ** (a - 1) * np.exp(-(x / b) ** a)
# cdf
def weibCumDist(x, a, b):
    return 1 - np.exp(-(x / b) ** a)

ワイブル分布への当てはめ

pymc3の公式HPのExampleを参考にした。

Reparameterizing the Weibull Accelerated Failure Time Model — PyMC3 3.5 documentation

打ち切りなしデータと、打ち切りありデータのそれぞれを分けてサンプリングする。

def weibull_lccdf(x, shape, scale):
    ''' Log complementary cdf of Weibull distribution. '''
    return -(x / scale)**shape
with pm.Model() as model_1:
    # 形状パラメータ
    alpha_sd = 10.0
    alpha_raw = pm.Normal('a0', mu=0, sd=0.1)
    shape = pm.Deterministic('shape', tt.exp(alpha_sd * alpha_raw))
    # 尺度パラメータ
    mu = pm.Normal('mu', mu=0, sd=100)
    scale = pm.Deterministic('scale', tt.exp(mu / shape))

    y_obs = pm.Weibull('y_obs', alpha=shape, beta=scale, observed=df_obs)
    y_cens = pm.Potential('y_cens', weibull_lccdf(df_cens, shape, scale))
with model_1:
    trace_1 = pm.sample(5000, tune=2000, 
                        nuts_kwargs={'target_accept': 0.95}, 
                        init='adapt_diag')
Auto-assigning NUTS sampler...
Initializing NUTS using adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [mu, a0]
Sampling 2 chains: 100%|██████████| 14000/14000 [00:50<00:00, 277.78draws/s]
The number of effective samples is smaller than 25% for some parameters.
pm.traceplot(trace_1)

f:id:gaiasky:20181111232851p:plain

pm.summary(trace_1).round(2)
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
a0 0.03 0.01 0.00 0.01 0.05 1680.58 1.0
mu 6.56 0.49 0.01 5.60 7.50 1671.80 1.0
shape 1.36 0.12 0.00 1.12 1.60 1671.94 1.0
scale 126.44 14.47 0.26 101.34 156.59 2817.21 1.0

推定したパラメータによる生存時間関数をグラフ表示する。

qlist = [2.5, 50, 97.5]
shapes = pm.quantiles(trace_1.get_values('shape'), qlist=qlist)
scales = pm.quantiles(trace_1.get_values('scale'), qlist=qlist)
# 生存時間関数表示関数
def plot_survival_function(ax, x, shape, scale, color='blue', qlist=[2.5,50,97.5]):
    low=qlist[0]
    mid=qlist[1]
    hig=qlist[2]
    ax.plot(x, 1-weibCumDist(x, shape[mid], scale[mid]), color=color)
    ax.fill_between(x=x, 
                 y1=1-weibCumDist(x, shape[low], scale[low]),
                 y2=1-weibCumDist(x, shape[hig], scale[hig]),
                 alpha=0.2, color=color)
    ax.set_xlabel('週')
    ax.set_ylabel('生存率')
    return ax
x = np.linspace(0,52,100)
fig = plt.figure(figsize=(9,4))
ax = fig.add_subplot(111)
ax = plot_survival_function(ax, x, shapes, scales, qlist=qlist)

f:id:gaiasky:20181111233138p:plain

カプランマイヤー法

  • ノンパラメトリックモデルであるカプランマイヤー法による生存率推定結果と比較する。
from lifelines import KaplanMeierFitter
kmf = KaplanMeierFitter()
kmf.fit(df.week.values, df.arrest.astype(np.bool), alpha=0.975)
x = np.linspace(0,52,100)
fig = plt.figure(figsize=(9,4))
ax = fig.add_subplot(111)
ax = plot_survival_function(ax, x, shapes, scales, qlist=qlist)
kmf.plot(ax=ax)

f:id:gaiasky:20181111233210p:plain

ベイズ推定はカプランマイヤー法よりも広めに推定されている。 元の記事の推定結果よりも広いため、うまく推定できていないのかも知れない。

比例ハザードモデル

  • 形状パラメータではなく、尺度パラメータに共変量の効果を入れる模様。
with pm.Model() as model_2:
    # 形状パラメータ
    alpha_sd = 10.0
    alpha_raw = pm.Normal('a0', mu=0, sd=0.1)
    shape = pm.Deterministic('shape', tt.exp(alpha_sd * alpha_raw))
    # 尺度パラメータ
    beta = pm.Normal('beta', mu=0, sd=10**2, shape=2)
    # 経済的支援ありの場合の尺度パラメータ
    scale1 = pm.Deterministic('scale1', tt.exp(-(beta[0]+beta[1])/shape))
    # 経済的支援なしの場合の尺度パラメータ
    scale2 = pm.Deterministic('scale2', tt.exp(-(beta[0])/shape))
                              
    y_obs = pm.Weibull('y_obs', alpha=shape, beta=tt.exp(-(beta[0]+df_fin_obs*beta[1])/shape), observed=df_obs)
    y_cens = pm.Potential('y_cens', weibull_lccdf(df_cens, shape, tt.exp(-(beta[0]+df_fin_cens*beta[1])/shape)))
with model_2:
    trace_2 = pm.sample(5000, tune=2000, 
                        nuts_kwargs={'target_accept': 0.9}, 
                        init='adapt_diag')
Auto-assigning NUTS sampler...
Initializing NUTS using adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [beta, a0]
Sampling 2 chains: 100%|██████████| 14000/14000 [01:30<00:00, 154.14draws/s]
pm.summary(trace_2).round(2)
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
a0 0.03 0.01 0.00 0.01 0.05 2917.32 1.0
beta__0 -6.42 0.49 0.01 -7.38 -5.49 2859.43 1.0
beta__1 -0.37 0.19 0.00 -0.73 0.01 4608.48 1.0
shape 1.37 0.12 0.00 1.14 1.61 2877.35 1.0
scale1 146.66 21.97 0.33 108.67 190.92 4099.34 1.0
scale2 111.09 13.30 0.18 87.85 138.03 5590.24 1.0
pm.traceplot(trace_2)

f:id:gaiasky:20181111233422p:plain

shapes = pm.quantiles(trace_2.get_values('shape'), qlist=qlist)
scales1 = pm.quantiles(trace_2.get_values('scale1'), qlist=qlist)
scales2 = pm.quantiles(trace_2.get_values('scale2'), qlist=qlist)
fig = plt.figure(figsize=(9,4))
ax = fig.add_subplot(111)
plot_survival_function(ax, x, shapes, scales1)
plot_survival_function(ax, x, shapes, scales2, color='red')

f:id:gaiasky:20181111233458p:plain

Cox比例ハザードモデルとの比較

cph = lifelines.CoxPHFitter()
cph.fit(df[['week','arrest','fin']], duration_col='week', event_col='arrest', show_progress=False)
cph.print_summary()  # access the results using cph.summary
n=432, number of events=114

       coef  exp(coef)  se(coef)       z      p  lower 0.95  upper 0.95   
fin -0.3691     0.6914    0.1897 -1.9453 0.0517     -0.7409      0.0028  .
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 

Concordance = 0.546
Likelihood ratio test = 3.837 on 1 df, p=0.05013
fig = plt.figure(figsize=(9,4))
ax = fig.add_subplot(111)
plot_survival_function(ax, x, shapes, scales1)
plot_survival_function(ax, x, shapes, scales2, color='red')
cph.plot_covariate_groups('fin',[0,1], ax=ax)

f:id:gaiasky:20181111233536p:plain

こちらもやはり、ベイズ信頼区間の範囲が元の記事よりも広い。 元の記事(stan)では事前分布が明示されていないため、そこが違いを生んでいるのかも知れない。 とはいえ、pymcでの打ち切りデータを含む場合のweibull分布への当てはめ方法が分かった。

以上

アヒル本10章棋士の強さのモデル化をpymcでやってみる。

この章の例題をpymcで実装してみた。 pymcによる実装の課題は以下の通り。

  • 順序付けを行うorderedが、pymcにはない(と思われる)
  • 単純にMCMCを実行するとエラーで実行できない

1点目は、以前のドラゴンボールの戦闘力推定の実装を使った。

gaiasky.hatenablog.com

# データの読み込み
players = pd.read_csv('shogi/data-shogi-player-name.txt', sep='\t')
battles = pd.read_csv('shogi/data-shogi-player.txt', sep=',')

モデル化は、ドラゴンボールの戦闘力推定モデルとほぼ一緒。

with pm.Model() as model:
    # prior
    s_mu = pm.Normal('s_mu', mu=0, sd=100)
    mu = pm.Normal('mu', mu=0, sd=s_mu, shape=len(players))
    sigma = pm.Gamma('sigma', 10, 10, shape=len(players))

    
    loser_perf = pm.Normal('loser_perf', mu=mu[battles.Loser-1], sd=sigma[battles.Loser-1], shape=len(battles))
    winner_perf = pm.Normal('winner_perf', mu=mu[battles.Winner-1], sd=sigma[battles.Winner-1], shape=len(battles))
    
    diff = winner_perf-loser_perf
    p = pm.math.sigmoid(diff)
    z = pm.Bernoulli('z',p=p, observed=np.ones(len(battles)))

MCMCの実行では、ADVI()による初期化を行うことで、NUTSによる実行が可能となった。 (ADVIによる初期化をしないと、エラーで落ちる)

with model:
    trace = pm.sample(2000, init='advi', n_init=25000)
Auto-assigning NUTS sampler...
Initializing NUTS using advi...
Average Loss = 4,742.8: 100%|██████████| 25000/25000 [05:40<00:00, 73.47it/s]
Finished [100%]: Average Loss = 4,743.1
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [winner_perf, loser_perf, sigma, mu, s_mu]
Sampling 2 chains: 100%|██████████| 5000/5000 [44:36<00:00,  2.09draws/s]
The gelman-rubin statistic is larger than 1.05 for some parameters. This indicates slight problems during sampling.
The estimated number of effective samples is smaller than 200 for some parameters.
pm.traceplot(trace, varnames=['mu','sigma'])

f:id:gaiasky:20180914230747p:plain

推定結果を表示する際、indexを棋士名に変換する。

def rename_indexes(trace, varname, players):
    ret = pm.summary(trace, varnames=[varname])
    
    tab = {}
    for p in range(len(players)):
        tab.setdefault('%s__%d'%(varname,p), players[players.nid==p+1].kname.values[0])
    ret = ret.rename(index=tab)
    return ret

棋士の強さの平均を表示する。

ret_mu = rename_indexes(trace, 'mu', players)
ret_mu.sort_values('mean', ascending=False).head()
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
羽生善治 2.959619 0.268438 0.015966 2.412412 3.475476 31.911283 1.073706
渡辺明 2.538573 0.294463 0.017209 1.984500 3.114538 80.892714 1.030923
行方尚史 2.116030 0.313367 0.017155 1.464525 2.698630 223.610725 1.019783
豊島将之 2.067839 0.286370 0.015999 1.542303 2.653113 117.756783 1.045872
郷田真隆 1.995864 0.285740 0.016104 1.472823 2.532897 195.203563 1.026106

棋士の勝負ムラを表示する。

ret_sigma = rename_indexes(trace, 'sigma', players)
ret_sigma.sort_values('mean', ascending=False).head()

勝負ムラが大きい棋士

mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
藤森哲也 1.126341 0.387405 0.030618 0.353202 1.831846 65.198876 0.999863
森内俊之 1.125223 0.344858 0.026571 0.511552 1.838470 89.927761 1.000393
深浦康市 1.116453 0.337526 0.025087 0.499118 1.743568 112.326690 1.003547
神崎健二 1.113792 0.336298 0.024777 0.483929 1.786775 59.970475 1.000581
阿久津主税 1.113714 0.327681 0.022573 0.472958 1.722987 119.652929 1.023349

勝負ムラが小さい棋士

ret_sigma.sort_values('mean', ascending=False).tail()
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
菅井竜也 0.906430 0.282381 0.022321 0.375220 1.422869 56.414318 1.013031
先崎学 0.904325 0.279065 0.019915 0.380872 1.418062 103.896750 0.999974
飯島栄治 0.899098 0.250615 0.018413 0.408726 1.358156 72.390061 1.004667
竹内雄悟 0.892474 0.282656 0.019096 0.392110 1.476494 128.417525 1.001682
北島忠雄 0.862707 0.272092 0.018809 0.332110 1.346679 132.050279 1.002114

書籍の結果とは微妙に異なるが、近い順位が得られた。

以上

アヒル本11章ゼロ過剰ポアソン分布をpymcでやってみる。

名前のとおり、ゼロが多いポアソン分布ということで、ベルヌーイ分布とポアソン分布を組み合わせたZIP分布を使った例題。

stanではなく、pymcで実装する場合、以下が注意点として挙げられる。

  • 切片のデータを追加しておく。
  • pymcのZIP分布におけるポアソン分布のパラメータλは、リンク関数(exp)を掛けて値を入れる。 (stanの実装では、poisson(exp(x))と等価な関数でZIP関数が作られているため、リンク関数を掛けていない。 これに気づかず、中々うまく推定できなかった。)

データを読み込み、可視化する。ゼロが多いカウントデータであることが分かる。

df = pd.read_csv('./data/data-ZIP.txt')

df.Y.plot.hist()

f:id:gaiasky:20180911215640p:plain

データの整形。切片も加える。

df.Age = df.Age/10
df['Intercept'] = 1
Y = df.Y.values
X = df[['Intercept', 'Sex', 'Sake', 'Age']].values
n_shape = X.shape[1]

モデルの実装。

with pm.Model() as model:
    b1 = pm.Normal('b1', mu=0, sd=5, shape=n_shape)
    b2 = pm.Normal('b2', mu=0, sd=5, shape=n_shape)
    
    q_x = pm.math.dot(X,b1)
    q = pm.invlogit(q_x)
    lam = pm.math.exp(pm.math.dot(X,b2)) # リンク関数(exp)をかけておく
    
    y_pred = pm.ZeroInflatedPoisson('y_pred', q, lam, observed=Y)
with model:
    trace = pm.sample(2000, start=pm.find_MAP(), step=pm.NUTS())
logp = -416.06, ||grad|| = 0.11749: 100%|██████████| 65/65 [00:00<00:00, 754.49it/s]  
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [b2, b1]
Sampling 2 chains: 100%|██████████| 5000/5000 [00:27<00:00, 181.93draws/s]
pm.summary(trace)
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
b1__0 0.914821 0.674350 0.012534 -0.332461 2.305058 2608.604989 0.999940
b1__1 1.579079 0.415617 0.006595 0.779755 2.401231 4781.578735 0.999810
b1__2 3.258817 0.788188 0.014349 1.781250 4.799758 2837.311993 1.000395
b1__3 -0.356855 0.179263 0.003336 -0.716908 -0.018116 2592.259435 1.000172
b2__0 1.450101 0.134314 0.002736 1.175606 1.699740 2785.911268 1.000095
b2__1 -0.745450 0.079422 0.001370 -0.893933 -0.588722 3471.970062 0.999781
b2__2 -0.161931 0.073769 0.001336 -0.306756 -0.023078 3011.972673 0.999774
b2__3 0.198165 0.033652 0.000672 0.131085 0.262932 2823.546198 1.000473
pm.traceplot(trace)

f:id:gaiasky:20180911220116p:plain

推測したパラメータでデータを生成し、分布を確認。

pred = pm.sample_ppc(trace, samples=100, model=model)
100%|██████████| 100/100 [00:00<00:00, 682.51it/s]
plt.hist(pred['y_pred'].reshape(-1,))

f:id:gaiasky:20180911220213p:plain

実際のデータに近い分布が得られた。

以上

アヒル本10章多項ロジスティック回帰をpymcでやってみる。

8.4章のロジスティック回帰は、ある一つの項目(出席/欠席)についてのみ考えるが、多項ロジスティック回帰は複数の項目について考える。

カテゴリカル分布を使ってモデル化するのだが、各パラメータの1番目の要素を0に固定化するというのが味噌で、これをpymcで実装してみた。

# データの読み込み
df = pd.read_csv('data/data-category.txt')
# データの整形
X = df[['Age','Sex','Income']].values
Y = df.Y - 1
K = len(df.Y.unique()) # カテゴリ数

モデルの実装。

with pm.Model() as model:
    # パラメータ数は、後で0を加えるため、カテゴリ数-1になる。
    alpha = pm.Normal('alpha', mu=0, sd=100, shape=(K-1,))
    beta = pm.Normal('beta', mu=0, sd=100, shape=(3,K-1))

    # 各パラメータの1つを0に固定化する。
    alpha_f = pm.Deterministic('alpha_f', tt.concatenate([tt.zeros(1), alpha]))
    beta_f = pm.Deterministic('beta_f', tt.concatenate([tt.zeros((3,1)) , beta], axis=1))

    #
    mu = alpha_f + pm.math.dot(X, beta_f)
    theta = tt.nnet.softmax(mu)
    y = pm.Categorical('y', p=theta, observed=Y)
with model:
    start = pm.find_MAP(method='Powell')
    step = pm.NUTS()
    trace_sf = pm.sample(3000, step, start)
logp = -inf, ||grad|| = 0: 100%|██████████| 510/510 [00:00<00:00, 637.82it/s] 
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [beta, alpha]
Sampling 2 chains: 100%|██████████| 7000/7000 [03:05<00:00, 37.79draws/s]

MCMC実行結果。

pm.traceplot(trace_sf)

f:id:gaiasky:20180911214415p:plain

書籍には推定結果は載っていなかったため、データと推測結果を比較してみた。

# 推定結果の取り出し
alpha_f = np.mean(trace_sf['alpha_f'], axis=0)
beta_f = np.mean(trace_sf['beta_f'], axis=0)

各データで一番可能性が高いカテゴリを予測する。

preds = []
for uid in df.index:
    mu = alpha_f + np.dot(X[uid], beta_f)
    theta = tt.nnet.softmax(mu).eval()[0]
    argmax = np.argmax(theta)
    preds.append(argmax)

実際の結果と予測結果を比較する。0が最も多いはず。

(Y - preds).value_counts()
 0    101
 1     40
 2     32
-2     26
 4     23
-1     17
 3     16
 5     14
-4     13
-3     11
-5      7
Name: Y, dtype: int64

実際-予測=0が一番多かった。ということで、うまく推定できたと判断しよう。

以上

アヒル本8.4章ロジスティック回帰階層モデルをpymcでやってみる。

この例題は、学生の出席/欠席を、アルバイト好きか、学問への興味、天気などのデータから予測するもので、 学生差や科目差を考慮した階層モデルになっている。

これをpymcで実装してみたのだが、比較的複雑なモデルであったためか、収束が遅く、 MCMCサンプリングのパラメータ調整に骨が折れた。

# データの読み込み
df1 = pd.read_csv('data/data-attendance-4-1.txt')
df2 = pd.read_csv('data/data-attendance-4-2.txt')
# 出欠データに学生データをマージする
df = pd.merge(df2, df1, on='PersonID', how='left')
# データの整形
df1.Score = df1.Score/200
df.Weather2 = df.Weather.astype('category').cat.codes
# データ数
I = len(df) #出欠データ数 
N = len(df.PersonID.unique()) # 学生数`
C = len(df.CourseID.unique()) # 科目数

モデルの実装は以下の通り。 パラメータの事前分布も一様分布にするとMCMCの実行でエラーが発生したため、 範囲を狭めた。

with pm.Model() as model:
    # 学生
    b2 = pm.Normal('b2', mu=0, sd=5)
    b3 = pm.Normal('b3', mu=0, sd=5)
    s_p = pm.HalfCauchy('s_p', beta=5)
    x_p = pm.Normal('x_p', mu=b2*df1.A+b3*df1.Score, sd=s_p, shape=N)
    # 科目
    s_c = pm.HalfCauchy('s_c', beta=5)
    x_c = pm.Normal('x_c', mu=0, sd=s_c, shape=C)
    # 出欠
    b4 = pm.Normal('b4', mu=0, sd=5)
    x_w = pm.Deterministic('x_w', b4*df.Weather2)

    # 
    b1 = pm.Normal('b1', mu=0, sd=5)
    x = b1 + x_p[df.PersonID-1] + x_c[df.CourseID-1] + x_w
    q = pm.invlogit(x)
    
    y = pm.Bernoulli('y', q, observed=df.Y)

次はMCMCの実行なのだが、普通に実行すると、 「There were 70 divergences after tuning. Increase target_accept or reparameterize.」 と表示されたため、サンプリングのパラメータを調整した。 これでもまだ、有効サンプル数が少ないと言われているが、ひとまずこれで良しとした。

with model:
    trace = pm.sample(4000, tune=1000, nuts_kwargs=dict(target_accept=.95))
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [b1, b4, x_c, s_c, x_p, s_p, b3, b2]
Sampling 2 chains: 100%|██████████| 10000/10000 [07:30<00:00, 16.01draws/s]
The estimated number of effective samples is smaller than 200 for some parameters.
pm.summary(trace, varnames=['b1','b2','b3','b4','s_p','s_c','x_c'])
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
b1 0.891183 0.597252 0.034681 -0.294326 2.029407 249.037909 0.999991
b2 -0.799847 0.153195 0.003269 -1.097714 -0.500283 2634.308828 1.000426
b3 1.635974 0.594128 0.040074 0.494068 2.894429 159.238272 1.000977
b4 -0.392469 0.069501 0.000932 -0.522982 -0.251189 5431.473522 0.999893
s_p 0.354840 0.086419 0.002680 0.184643 0.525146 949.691355 1.003182
s_c 1.407389 0.417113 0.009598 0.742878 2.230000 1596.695327 1.000975
x_c__0 0.496923 0.528346 0.024537 -0.598436 1.513327 403.326425 1.000081
x_c__1 -1.294490 0.494636 0.025063 -2.297486 -0.304960 325.406740 1.000153
x_c__2 -0.985298 0.501408 0.024779 -2.014632 -0.015746 346.060896 1.000204
x_c__3 1.344727 0.524338 0.024928 0.220888 2.315912 376.628593 1.000045
x_c__4 -0.258379 0.500144 0.024979 -1.259820 0.730618 341.972609 1.000112
x_c__5 -1.931672 0.508829 0.025169 -2.940520 -0.919634 349.604963 0.999921
x_c__6 0.901908 0.512885 0.024878 -0.090079 1.943308 352.460165 1.000018
x_c__7 -0.536358 0.501140 0.024853 -1.523805 0.475292 348.076164 1.000092
x_c__8 1.207217 0.527864 0.025324 0.131012 2.239323 363.558944 1.000101
x_c__9 1.287300 0.530528 0.025280 0.200316 2.323838 371.970131 1.000043
pm.traceplot(trace, varnames=['b1','b2','b3','b4','s_p','s_c','x_c'])

f:id:gaiasky:20180911212239p:plain

パラメータの可視化。書籍ではviolinplotだが、ここではboxplotにした。

skip=10
estimate_params = pd.DataFrame(
    data = {'b1': trace['b1',::skip],
        'b2': trace['b2',::skip],
        'b3': trace['b3',::skip],
        'b4': trace['b4',::skip],
        's_p': trace['s_p',::skip],
        's_c': trace['s_c',::skip]
    })
sns.boxplot(x='variable', y='value', data=pd.melt(estimate_params))

f:id:gaiasky:20180911212348p:plain

大体同じような結果が得られたと思う。

以上

アヒル本8.3章非線形モデルの階層モデルをpymcでやってみる。

StanとRでベイズ統計モデリングの8.3「非線形モデルの階層モデル」をpymcで実装してみる、というお話。 この本は実践的な例題が多く、非常に参考になる。ただし、タイトルの通りだが、RとStanの実装。

  • この節の例題は、経過時間と薬の血中濃度非線形の関係でモデル化している。
  • 各患者のモデルパラメータを階層化してモデル化しており、パラメータの対数値が正規分布に従う、と仮定されている。
  • pymcでモデルを実装する際、時系列やパラメータの対数変換はどうするのか?という素朴な疑問からpymcで実装してみた。

実装してみた結果は以下。

  • 患者と時間という2軸あるので、少し分かりにくい印象。(Stanの方がコード化しやすい気がする)
  • NUTSによるMCMCはエラーで実行できなかった。代わりにADVIで推定した。得られた推定結果は書籍と大体同じであった。
df = pd.read_csv('data/data-conc-2.txt')
df.head()
PersonID Time1 Time2 Time4 Time8 Time12 Time24
0 1 2.4 5.0 7.5 11.9 12.5 12.7
1 2 1.4 3.9 4.4 7.7 6.4 8.3
2 3 5.2 9.4 19.4 20.2 22.7 24.9
3 4 6.7 12.6 19.1 23.4 25.8 26.1
4 5 0.3 4.7 7.0 10.2 12.9 14.8
N = len(df.PersonID.unique())

Times = np.array([1,2,4,8,12,24])
Times = np.array([Times]).T # 列ベクトルに変換する。
with pm.Model() as model:
    # 階層化部分
    a0 = pm.Normal('a0', mu=0, sd=10**2)
    s_a = pm.HalfCauchy('s_a', beta=10**2)
    b0 = pm.Normal('b0', mu=0, sd=10**2)
    s_b = pm.HalfCauchy('s_b', beta=10**2) 
    # 各患者のモデルパラメータ
    log_a = pm.Normal('log_a', mu=a0, sd=s_a, shape=N)
    a = pm.Deterministic('a', pm.math.exp(log_a))
    log_b = pm.Normal('log_b', mu=b0, sd=s_b, shape=N)
    b = pm.Deterministic('b', pm.math.exp(log_b))
    # 誤差
    eps = pm.Normal('eps', mu=0, sd=10**2)
    
    # 各時刻における各患者の平均(matrixになるので、少しややこしい)
    mu = pm.Deterministic('mu', a*(1-pm.math.exp(-b*Times)))
    y = pm.Normal('y', mu=mu, sd=eps, observed=df.iloc[:,1:].values.T)

パラメータの推定。MCMC(NUTS)はエラーで実行できなかった。

with model:
    inference = pm.ADVI()
    approx = pm.fit(100000, method=inference)
Average Loss = 265.72: 100%|██████████| 100000/100000 [02:42<00:00, 614.61it/s]
Finished [100%]: Average Loss = 265.69
trace = approx.sample(draws=3000)

推定結果。書籍の結果と大体同じ値が得られている。

pm.summary(trace, varnames=['a0','b0','s_a','s_b','eps'])
mean sd mc_error hpd_2.5 hpd_97.5
a0 2.859904 0.110053 0.001996 2.636606 3.068902
b0 -1.165525 0.089034 0.001613 -1.344461 -0.993055
s_a 0.420661 0.083319 0.001390 0.275964 0.590157
s_b 0.344515 0.071129 0.001492 0.210082 0.483927
eps 1.808791 0.139476 0.002373 1.540795 2.083439

グラフ化して確認。 各患者の平均値の推定値(事後分布の中央値)と、95%予測区間をグラフ化する。

times = Times.T[0]
# 事後分布の中央値を使う。
mu = pm.quantiles(trace)['mu'][50]
eps = pm.quantiles(trace)['eps'][50]

_, axes = plt.subplots(4, 4)
for (row, col), ax in np.ndenumerate(axes):
    id = row * 4+ col
    # 観測値
    ax.scatter(times, df.loc[id,'Time1':].values, color='k')
    # 平均の推定値(事後分布の中央値)
    ax.plot(times, mu[:,id], color='k')
    # 平均の推定値(事後分布の95%最高密度区間)
    y1,y2 = map(list, zip(*pm.hpd(trace, alpha=0.05)['mu'][:,id]))
    #ax.fill_between(times, y1=y1, y2=y2, alpha=0.2, color='b')
    # 平均値の推定値(事後分布の中央値)を使った場合の95%予測区間
    y1,y2 = map(list, zip(*norm.ppf(q=[0.025, 0.975], loc=np.array([mu[:,id]]).T, scale=eps)))
    ax.fill_between(times, y1=y1, y2=y2, alpha=0.2, color='r')    
    # グラフの調整
    if row < 3:
        plt.setp(ax.get_xticklabels(), visible=False)
    else:
        plt.setp(ax, xlabel='Time (hour)')
    if col > 0:
        plt.setp(ax.get_yticklabels(), visible=False)
    else:
        plt.setp(ax, ylabel='Y')    
    plt.setp(ax, title='id:%d'%(id+1), xticks=times, xlim=(0, 24), yticks=np.arange(0, 40, 10), ylim=(-3, 37))
plt.tight_layout()

f:id:gaiasky:20180905223022p:plain

グラフも大体同じ結果。

以上