ベイズ推論モデル

Rによる能動的推論モデル入門

専修大学 国里愛彦

本日の内容

  • ベイズ推論モデル
  • 能動的推論モデル
  • Rによる能動的推論モデルの実装

本スライドに関して,間違っている・間違ってそう・ご不明な内容がありましたら,国里愛彦の研究室メールフォームからお知らせください(共同研究もお待ちしています)

ベイズ推論モデル

ベイズ推論モデルとは

  • ベイズ推論モデルは,ベイズの定理を用いて,エージェントが世界について知る過程をモデル化する。
  • 具体的には,ベイズの定理を用いて,事前信念をデータの情報(尤度)で更新して事後信念を得るプロセスをモデル化する。
  • 計算論的精神医学で用いられるベイズ推論モデル:パラメータ化信念更新モデル,カルマンフィルターモデル,階層ガウシアンフィルター,能動的推論モデル

ベイズ推論モデルの世界観

  • 感覚入力oの背後には,直接観測できない外部状態 \(s^*\) がある(外部状態 \(s^*\) から感覚入力oが生成されるプロセスは生成過程\(p(s^*, o)\)と呼ぶ)。
  • 外部状態 \(s^*\) は直接観測できないので,入ってきた感覚入力oから外部状態 \(s^*\) を推論する。

ベイズ推論モデルの世界観

  • 外部状態 \(s^*\) の推論で,外部状態についての信念sと感覚入力oに関する同時確率分布p(s,o)を用いる(生成モデル)。
  • 感覚入力oから外部状態 \(s^*\) についての信念sをベイズ更新することが知覚
  • 信念の更新だけでなく,行動選択aによって外界の状態にも働きかける

生成モデル

  • 生成モデルは,エージェントが外界の生成過程を近似したもの
  • 生成モデルがあれば,エージェントの知覚や行動についてシミュレーションできる。
  • 認知課題などによって行動データがあれば,生成モデルを用いてパラメータ推定を行うこともできる(能動的推論では限定的な実践)。
  • 生成モデルは同時確率分布p(s, o)に限定されることもあるが,広くデータを生成するモデルという意味で用いることもある。

ベイズの定理

\[ p(s|o) = \frac{p(o|s)p(s)}{p(o)} \]

  • [右辺の分子]ある信念sの事前確率(p(s))をある信念sの下で感覚入力oが観測される確率(尤度, p(o|s))に掛け合わせることで,観測に伴う世界の状態について信念を更新している。

状態空間モデル

  • ベイズ推論モデルは,状態空間モデルで整理して理解するとわかりやすい。
  • 状態空間モデルは,観測によって状態変数を更新することで時間的変化を説明するモデル
  • ベイズ推論モデルでは,観測を通して信念が更新されるプロセスをベイズ更新でモデル化するが,これは状態空間モデルで扱う状態変化のモデル化に使える。

→状態空間モデルの枠組みで各種ベイズ推論モデルを位置づけて理解する。

状態空間モデル

  • 2腕バンディットでは,各スロットの報酬確率が状態である。エージェントは信念を観測によってベイズ更新することで,状態を推測する。
  • 状態の変化について記述するモデル(状態方程式)と状態から観測の出力を記述するモデル(観測方程式)がある。

カルマンフィルター

  • カルマンフィルターは,時間的に変化するシステムの挙動を説明するモデルであり,1時点前の状態sの推定値と現在の観測oを用いて,現在と1時点後の状態sを推定する。
  • 状態sを観測oによってベイズ更新するが,正規分布を用いてベイズ更新を行う(平均と分散についてベイズ更新する)。

カルマンフィルター

  • スロットのポイントに応じてベイズ更新がなされ,状態(正規分布の平均と分散)が変化する。
  • カルマンフィルター(後述するHGF)では,状態を x ,観測を y などで表現するが,状態はs,観測はoで統一する(aはsに基づく反応)。

階層ガウシアンフィルター

  • 階層ガウシアンフィルターは,カルマンフィルターの状態を階層的に拡張したモデル。
  • カルマンフィルターは状態sが1つの階層からなるモデルだが,階層ガウシアンフィルターは状態sが複数の層から構成される階層化されたモデル。
  • 階層ガウシアンフィルターも状態空間モデルの枠組みで理解できる。

階層ガウシアンフィルター

  • 状態が3層からなる階層ガウシアンフィルター
  • 高次の状態が下位の状態の分散に影響を与えるボラティリティ・カップリングを用いることが多い(s3はs2の分散に影響する,つまりs2の変動性を制御する)。

→変動性を明示的に扱う

階層ガウシアンフィルター

  • 階層ガウシアンフィルターに反応モデル(ソフトマックス関数, 線形回帰)を追加することで,反応aのシミュレーションができる。
  • 行動データがあればパラメータ推定も可能(MatlabのTAPAS, PythonのPyHGFなど)

能動的推論モデル

自由エネルギー原理

  • 自由エネルギー原理(free-energy principle: FEP)は,フリストン博士によって提唱された,ベイズ推論モデルをベースにした脳と心の統一的理論
  • 能動的推論は,自由エネルギー原理に基づいて,知覚や行動を説明する枠組み
  • 自由エネルギー原理という名前が難しそうだが,カルマンフィルターや階層ガウシアンフィルターの拡張として理解できる。

自由エネルギー原理

  • FEPでは,感覚入力oから外部状態 \(s^*\) の推測において,その感覚入力oの得られにくさであるサプライザル(\(-log p(o)\))が重要
  • サプライザルが小さいほど,その生成モデルの予測性能が良い(ただ,その計算では可能性のある状態を全て考慮するため計算不可能なことも)

変分自由エネルギー\(\textbf{F}\)

  • サプライザルの計算が難しいので,変分ベイズ法による近似を用いる。
  • 変分ベイズ法では,扱いやすい形の近似事後分布\(q(s)\)をおいて,サプライザルの代わりにその上限を提供する変分自由エネルギー\(\textbf{F}\)を計算する。
  • \(\textbf{F}\)は以下の式で計算され,近似事後分布\(q(s)\)と生成モデル\(p(s, o)\)があれば計算ができる。

\[ F = \int dx q(s)log \frac{q(s)}{p(s,o)} \]

変分自由エネルギー \(\textbf{F}\)

  • 変分自由エネルギーFは,以下のように表現できる。

\[ F = D_{KL}[q(s)\parallel p(s|o)]- \log p(o) \]

  • 第1項は,カルバック・ライブラーダイバージェンスであり,確率分布\(q\)\(p\)の差異を表し,ゼロ以上の値を取る(\(q\)\(p\)が同じならゼロ)。第2項はサプライザル。

\(D_{KL}\)はゼロ以上なので,\(\textbf{F}\)がサプライザルの上限を提供。\(\textbf{F}\)を最小化する\(q\)の探索を通してサプライザルを最小化(推論問題を\(\textbf{F}\)を最小化する最適化問題へ)

変分自由エネルギー\(\textbf{F}\)の最小化

  • \(\textbf{F}\)の最小化は,状態が階層化された神経回路で実現
  • 上の階層から下へ予測が送られ,感覚入力oと比較され,予測誤差が生じる。
  • 予測誤差最小化のため,下の階層から上に向けて予測誤差を伝達して状態を更新

→階層的神経回路による予測誤差最小化で,\(\textbf{F}\)を最小化

期待自由エネルギー\(\textbf{G}\)

  • 行動選択するには,未来に生じることを考慮する必要があるが,変分自由エネルギーは未来に生じることを考慮できない。

→変分自由エネルギー\(\textbf{F}\)ではなく期待自由エネルギー\(\textbf{G}\)を計算する

  • 期待自由エネルギー\(\textbf{G}\)では,可能性のある行為の系列であるポリシー(\(\pi\))に対して生じるであろう未来のデータを生成モデルから予測して計算に用いる。
  • エージェントは期待自由エネルギー\(\textbf{G}\)が最も小さいポリシーを選択することで\(\textbf{G}\)を最小化する。

期待自由エネルギー\(\textbf{G}\)

\[ G(\pi) = - \mathbb{E}_{q(\tilde{s},\tilde{o}|\pi)}[D_{KL}[q(\tilde{s}|\tilde{o},\pi) \parallel q(\tilde{s}|\pi)]]- \mathbb{E}_{q(\tilde{o}|\pi)}[\log p(\tilde{o}|C)] \]

  • 第1項は新しい情報を求める価値(認識的価値)になる(KLダイバージェンスが大きいほどGは小さい)。未来の観測による状態の不確実性の減少の程度になる。
  • 第2項の\(\textbf{C}\)は選好であり,好ましい観測を求める価値になる(実利的価値)。エージェントにとって好ましい観測は\(\textbf{G}\)が小さくなる。
  • 期待自由エネルギーは認識的価値と実利的価値とのバランスで定まり,探索-利用のバランスを扱っている。

部分観測マルコフ決定過程(POMDP)

  • 期待自由エネルギーを用いて行動選択することを能動的推論と呼ぶ。能動的推論では,期待自由エネルギーが最も小さくなるポリシーが高い確率で選ばれる。
  • 能動的推論では,観測が離散変数の場合は,部分観測マルコフ決定過程(partially observable Markov decision process: POMDP)を用いる。
  • POMDPは部分観測とあるように,状態を直接観測できない状況で推測する際の枠組み

因子グラフ

  • 能動的推論のPOMDPモデルは,因子グラフ形式で表現されることが多い。
  • 因子グラフでは,状態sや観測oなどの変数は円,状態間や状態と観測の間のような変数間の関係は四角で表現する。
  • 四角を因子と呼び,因子は尤度を表すものと事前分布を作るものに分けられる。

4つの因子(\(\textbf{ABCD}\)

  • A:状態から観測が得られる確率(尤度) \(P(o_τ|s_τ )\)
  • B:状態間の遷移行列\(P(s_{τ+1} |s_τ,π))\)
  • C:観測データの事前信念
  • D:初期状態の事前分布\(P(s_1 )\)
  • τ:内部的な時刻

4つの因子(\(\textbf{ABCD}\)

  • ポリシー (\(\pi\))は,\(\sigma(-G)\) に従い選択される(\(\sigma\) はソフトマックス関数)
  • ポリシーに対する期待自由エネルギー\(\textbf{G}\)が小さいほど選択確率が大きくなる
  • 状態空間モデルとして理解できる(因子の追加やポリシーが状態遷移に影響するなどが違う)

\(\textbf{A}\)\(\textbf{C}\)行列を用いたGの計算

  • POMDPモデルにおいて,期待自由エネルギー(\(\textbf{G}\))は,以下のように,状態\(\textbf{s}\),観測\(\textbf{o}\), 尤度\(\textbf{A}\) ,観測データの事前信念\(\textbf{C}\) から計算ができる。

\[ \begin{align} \textbf{G}_{\pi} = \textbf{H} \cdot \textbf{s}_{\pi \tau} + \textbf{o}_{\pi \tau} \cdot \varsigma_{\pi \tau} \\ \varsigma_{\pi \tau} = \log \textbf{o}_{\pi \tau} - \log \textbf{C}_{\tau} \\ \textbf{H} = -diag(\textbf{A}\cdot \log\textbf{A}) \end{align} \]

  • 因子グラフの◯から□を挟んで別の◯への矢印は,変数に因子(確率分布)を乗ずることで別の変数に変換する計算を意味する

\(\textbf{A}\)\(\textbf{B}\)行列での\(\textbf{F}\)の計算

  • 変分自由エネルギーは以下のように,状態s, アウトカムo, 尤度\(\textbf{A}\) ,遷移行列\(\textbf{B}\) から計算ができる。

\[ \begin{align} F = \pi \cdot \textbf{F} \\ \textbf{F}_{\pi} = \Sigma_{\tau}\textbf{F}_{\pi \tau} \\ \textbf{F}_{\pi \tau} = \textbf{s}_{\pi \tau} \cdot (\log \textbf{s}_{\pi \tau} - \log \textbf{A}\cdot o_{\tau} - \log \textbf{B}_{\pi \tau}\textbf{s}_{\pi \tau-1}) \end{align} \]

  • \(\textbf{G}\)\(\textbf{F}\)の計算で,初期状態の事前分布\(\textbf{D}\) が含まれていないが,\(\textbf{D}\)は状態の最初の値になるので,実装において必要となる。

能動的推論モデルの実装

  • 尤度(\(\textbf{A}\)),遷移確率 (\(\textbf{B}\)),観測データの事前信念 (\(\textbf{C}\)),初期状態に関する事前信念 (\(\textbf{D}\))を設定すれば,期待自由エネルギーを最小化するように振る舞う能動的推論エージェントを作ることができる。
  • MATLAB上で動作する脳機能画像解析用ソフトであるSPM12 に同封されているspm_MDP_VB_X.mを使う際もこれら\(\textbf{ABCD}\)を設定する。

\(\textbf{R}\)による能動的推論モデルの実装

なぜ\(\textbf{R}\)

  • 能動的推論に関しては,Matlabで動作するSPM,Pythonパッケージのpymdp,JuliaパッケージのActiveInference.jlなどがある。
  • パッケージ化されたものは便利な機能も多いが,能動的推論を学ぶ上ではブラックボックス化してしまいがちかも?
  • 非エンジニア向けのプログラミング言語であるRで動かして学ぶこともチュートリアルとしては意味がある。

\(\textbf{R}\)のはじめかた

  • ローカルで使う場合は,Rをダウンロード&インストール,RStudioのダウンロード&インストールをしてください(「R Rstudio インストール」で検索すると解説しているサイトがたくさんでてくる)。
  • RやRStudioに慣れている方は,ローカルで実行ください。
  • RやRStudioに慣れていない方は,次スライドで説明するGoogle Colabを利用ください。

Google Colabの使い方(1)

  • Google Colabを使う場合は,このチュートリアル用のリンクにアクセスして,「ファイル」→「ドライブにコピーを保存」をクリックしてください。

Google Colabの使い方(2)

チュートリアル1

チュートリアル1について

  • pymdpが公開している能動的推論のチュートリアルTutorial 1: Active inference from scratchを参考にして,能動的推論モデルをRで実装する。
  • チュートリアル1では,グリッドワールド環境において,能動的推論エージェントをRでゼロから構築する。
  • このチュートリアルを通して,能動的推論における\(\textbf{ABCD}\)行列の設定とエージェントのシミュレーション方法がつかめる。

\(\textbf{R}\)パッケージ

  • 以下のRパッケージを用いる(library()でパッケージ読み込み)
library(tidyverse)
library(R6)
library(gridExtra)
  • tidyverse : データハンドリング&プロット用
  • R6 : Rでのオブジェクト指向プログラミング用
  • gridExtra : 複数の図を1つに並べる用

グリッドワールドについて

  • 能動的推論の例として,3行3列のマス目からなる「グリッドワールド」を用いる。
  • グリッドワールドでは,グリッド(格子)で囲われたマスを移動していく(移動は,隣り合った上下左右のマス)
  • グリッドで作られたワールドの中を移動する能動的推論エージェントの生成モデル \(P(o,s)\) を記述する前に,グリッドワールドについて可視化する。

グリッドを作成し位置を示す関数

  • グリッドをつくるcreate_grid_locations関数とグリッドをプロットするplot_grid関数を以下のコードのように書く(これらの関数はキレイにプロットしているだけなので,実装は理解しなくても大丈夫)。
# expand.gridを使ってグリッド位置を作る関数
create_grid_locations <- function() {
  grid <- expand.grid(y = 1:3,x = 1:3)
  grid_locations <- Map(c, grid$x, grid$y)
  return(grid_locations)
}

# グリッドをプロットする関数
plot_grid <- function(grid_locations, num_x = 3, num_y = 3) {
  # 場所を用意
  grid_heatmap <- matrix(0, nrow = num_y, ncol = num_x)
  counter <- 1
  for (i in 1:num_y) {
    for (j in 1:num_x) {
      grid_heatmap[i, j] <- counter
      counter <- counter + 1
    }
  }
  # データフレームに変換
  plot_data <- data.frame(
    y = rep(1:num_y, num_x),
    x = rep(1:num_x, each = num_y),
    value = as.vector(grid_heatmap)
  )
  # ヒートマップの色の設定
  custom_colors <- scale_fill_gradient(low = "#E8F4D9",high = "#1A365D")
  # ヒートマップの作成
  ggplot(plot_data, aes(x = x, y = y, fill = value)) +
    geom_tile() +
    geom_text(aes(label = sprintf("%.0f", value)), 
              size = 6,
              color = ifelse(plot_data$value > 5, "white", "black")) +
    custom_colors +
    scale_y_reverse() +
    theme_minimal(base_size = 15) +
    theme(legend.position = "none",
          axis.title.x = element_blank(),
          axis.title.y = element_blank()) +
    coord_fixed()
}



#

グリッドワールドのプロット

  • 作った関数を用いてグリッドワールドをプロットする(プロットした図は次スライド)
grid_locations <- create_grid_locations()
plot_grid(grid_locations)

グリッドワールドのプロット

  • グリッドに1から9の番号を振っており,エージェントは,このグリッド上の番号を移動する。
  • 1から9へ移動する場合,上下左右移動して,1→2→3→6→9のような感じで移動する。

生成モデルを作る: \(\textbf{ABCD}\)行列の用意

  • 能動的推論エージェントの生成モデルを作ろう!
  • 能動的推論における生成モデルの作成では,\(\textbf{ABCD}\)行列(配列)を指定する。

(1) 尤度(\(\textbf{A}\)

  • A行列では状態sの下での観測oの確率 \(P(o\mid s)\) を扱う(つまり尤度)
  • 尤度(\(\textbf{A}\))では,どの状態の場合にそれぞれの観測がどのくらいの確率なのかを表している。
  • 生成過程の構造を示しているが,エージェントの持っている世界のモデルなので,合っていることも間違っていることもある。

状態と観測の数

  • グリッドワールドには9つの場所があるので,状態も観測もそれぞれ9つになる。
# 隠れ状態と観測の次元数を格納する変数を作成
n_states <- length(grid_locations)
n_observations <- length(grid_locations)

print(paste("隠れ状態の次元:", n_states,
            ",観測の次元:", n_observations))
[1] "隠れ状態の次元: 9 ,観測の次元: 9"

\(\textbf{A}\)行列を格納する場所を用意

  • 状態の数と観測の数を用いて,\(\textbf{A}\)行列を格納する場所を作る。
  • まだ中身は分からないので,行列の要素はゼロをいれる。
A <- matrix(0, nrow = n_states, ncol = n_observations)

状態と観測が完全一致する場合

  • 状態と観測が完全に一致する場合の尤度行列を作る(diag()で,行列の対角成分が1になるように設定)
diag(A) <- 1.0
A
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
 [1,]    1    0    0    0    0    0    0    0    0
 [2,]    0    1    0    0    0    0    0    0    0
 [3,]    0    0    1    0    0    0    0    0    0
 [4,]    0    0    0    1    0    0    0    0    0
 [5,]    0    0    0    0    1    0    0    0    0
 [6,]    0    0    0    0    0    1    0    0    0
 [7,]    0    0    0    0    0    0    1    0    0
 [8,]    0    0    0    0    0    0    0    1    0
 [9,]    0    0    0    0    0    0    0    0    1

\(\textbf{A}\)行列を確認する関数

  • \(\textbf{A}\)(尤度)行列を確認するためのplot_likelihood関数を以下のように準備する(きれいなプロットしているだけなので,関数の実装は理解しなくても大丈夫)。
# 2次元尤度行列をヒートマップとしてプロットする関数
plot_likelihood <- function(matrix, xlabels = 1:9, ylabels = 1:9, title_str = "Likelihood distribution (A)") {
  # 列方向の和が1かどうかを確認
  if (!all(near(colSums(matrix), 1.0))) {
    stop("Distribution not column-normalized! Please normalize (ensure colSums(matrix) == 1.0 for all columns)")
  }
  
  # 行列をデータフレームに変換
  df <- as.data.frame(matrix)
  colnames(df) <- xlabels
  rownames(df) <- ylabels
  df <- df %>% 
    mutate(y = factor(rownames(df), levels = rev(rownames(df)))) %>% 
    pivot_longer(cols = -y, names_to = "x", values_to = "value")
  
  # ggplot2でヒートマップを作成
  ggplot(df, aes(x = x, y = y, fill = value)) +
    geom_tile() +
    scale_fill_gradient(low = "black", high = "white", limits = c(0, 1)) +
    labs(title = title_str, x = "隠れ状態の位置", y = "観測の位置") +
    theme_minimal() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1))
}


#

\(\textbf{A}\)行列を確認

  • \(\textbf{A}\)(尤度)行列を確認(0は黒,1に近づくほど白)
plot_likelihood(A, title_str = "A行列")

\(\textbf{A}\)行列に不確実性がある場合

  • 尤度に不確実性があることも(状態と観測に確率的な関係があることが多い)
  • 位置1は位置2と位置4が隣り合っているので,状態が1の時に観測が1と2と4で混同するかも?

位置1,2,4に不確実性がある場合

  • 状態の位置が1の際に,観測の位置が1,2,4の確率が1/3の場合の設定を以下のようにする(A_noisyと名前を変えた)。最後にプロットして確認(図は次スライド)
A_noisy <- A
# 隠れ状態の位置が1の時に,観測の位置が1の場合P(o=1|s=1) 
A_noisy[1, 1] <- 1 / 3.0 
# 隠れ状態の位置が1の時に,観測の位置が2の場合P(o=2|s=1) 
A_noisy[2, 1] <- 1 / 3.0
# 隠れ状態の位置が1の時に,観測の位置が1の場合P(o=4|s=1) 
A_noisy[4, 1] <- 1 / 3.0

plot_likelihood(A_noisy, title_str = "(1,1) が不鮮明になるように修正したA行列")

位置1,2,4に不確実性がある場合

  • 状態が1の時に(1列目),観測が1,2,4の場合は色が薄くなっている(1/3)

\(\textbf{A}\)行列の設定

  • \(\textbf{A}\)(尤度)行列の設定は,エージェントの世界モデルを設定しているともいえる。
  • これが生成過程を表現できていることもあれば,不正確だったりズレていることもある。
  • そのような設定をしてシミュレーションをすることでエージェントの理解が深まることも。
  • 今回は状態と観測が一致するような設定だが,状態と観測が確率的な関係の設定もある。

(2) 遷移行列(\(\textbf{B}\))

  • \(\textbf{B}\)行列( \(P(s_{t}\mid s_{t-1}, a_{t-1})\) )は,時間経過と行為に伴う状態間の遷移を扱う(つまり状態遷移行列)
  • 時刻 \(t-1\)の状態が,どのように時刻 \(t\) の状態をもたらすかに関する行列であり,今回の状態はグリッドワールド上の位置なので,1つ前の時点の位置から次の時点の位置への遷移になる。
  • 遷移行列は行動 \(a_t\)によって条件付けられ,グリッドワールド内で,“UP”, “DOWN”, “LEFT”, “RIGHT”, “STAY”の5つのアクション(a)によって,1つ前の時点の位置( \(s_{t-1}\) )から次の時点の位置( \(s_{t}\) )の遷移行列を用意する。

遷移行列を用意する関数

  • create_B_matrix関数で\(\textbf{B}\)行列を作成する。
  • \(\textbf{B}\)行列は,時点tの状態数(9)×時点t-1の状態数(9)×アクション数(5)からなる配列(アクションごとに時点tの状態×時点t-1の状態の行列を用意)
# アクション定義
actions <- c("UP", "DOWN", "LEFT", "RIGHT", "STAY")
# B行列作成関数
create_B_matrix <- function() {
  num_states <- length(grid_locations)
  num_actions <- length(actions)
  # 3次元配列の初期化
  B <- array(0, dim = c(num_states, num_states, num_actions))
  # 各アクションと状態に対する遷移を計算
  for (action_id in seq_along(actions)) {
    action_label <- actions[action_id]
    for (curr_state in seq_along(grid_locations)) {
      # 現在の位置を取得
      grid_location <- grid_locations[[curr_state]]
      y <- grid_location[1]
      x <- grid_location[2]
      # アクションに基づいて次の位置を計算
      if (action_label == "UP") {
        next_y <- ifelse(y > 1, y - 1 ,y)
        next_x <- x
      } else if (action_label == "DOWN") {
        next_y <- ifelse(y < 3, y + 1, y)
        next_x <- x
      } else if (action_label == "LEFT") {
        next_x <- ifelse(x > 1, x - 1, x)
        next_y <- y
      } else if (action_label == "RIGHT") {
        next_x <- ifelse(x < 3, x + 1, x)
        next_y <- y
      } else if (action_label == "STAY") {
        next_x <- x
        next_y <- y
      }
      # 新しい位置を作成
      new_location <- c(next_y, next_x)
      # 新しい位置のインデックスを検索
      next_state <- which(sapply(grid_locations, function(loc) 
        all(loc == new_location)))
      # 遷移確率を設定
      B[next_state, curr_state, action_id] <- 1.0
    }
  }
  return(B)
}




#

\(\textbf{B}\)行列の作成と確認

  • \(\textbf{B}\)行列を作り,”UP”を選択した場合,4→1, 5→2, 6→3, 7→4, 8→5, 9→6となる

\(\textbf{B}\)行列の作成と確認

  • ”UP”を選択した場合の,時点tの状態×時点t-1の状態の行列を確認すると(B[,,1]),列に対して行が1つ上に上がっている(4→1, 5→2, 6→3, 7→4, 8→5, 9→6)
# B行列の作成と表示
B <- create_B_matrix()
B[,,1]
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
 [1,]    1    0    0    1    0    0    0    0    0
 [2,]    0    1    0    0    1    0    0    0    0
 [3,]    0    0    1    0    0    1    0    0    0
 [4,]    0    0    0    0    0    0    1    0    0
 [5,]    0    0    0    0    0    0    0    1    0
 [6,]    0    0    0    0    0    0    0    0    1
 [7,]    0    0    0    0    0    0    0    0    0
 [8,]    0    0    0    0    0    0    0    0    0
 [9,]    0    0    0    0    0    0    0    0    0

グリッド上の位置を表示する関数

  • グリッド上のどこの位置にいるかを表示するplot_point_on_grid関数,インデックスを1それ以外を0とするベクトルを出すonehot関数を作成する(この実装はあまり深く理解しなくても良い)
plot_point_on_grid <- function(state_vector, grid_locations) {
  # 状態のベクトルから状態のインデックスを取得
  state_index <- which(state_vector == 1)
  # グリッドの位置からxとyの値を抽出
  coords <- grid_locations[[state_index]]
  x <- coords[1]
  y <- coords[2]
  # 空のグリッドを用意してグリッドの位置に値をいれる
  grid_heatmap <- matrix(0, nrow = 3, ncol = 3)
  grid_heatmap[y, x] <- 1.0
  # プロット用のデータ用意
  plot_data <- data.frame(
    y = rep(1:nrow(grid_heatmap), each = ncol(grid_heatmap)),
    x = rep(1:ncol(grid_heatmap), times = nrow(grid_heatmap)),
    value = as.vector(grid_heatmap),
    index = 1:9
  )
  ggplot(plot_data, aes(x = x, y = y, fill = value)) +
    geom_tile() +
    geom_text(aes(label = sprintf("%.0f", index)), 
              size = 6,
              color = "white") +
    scale_fill_gradient(low = "black", high = "grey", limits = c(0, 1)) +
    theme_minimal() +
    theme(legend.position = "none") +
    scale_y_reverse() +
    theme(axis.title.x = element_blank(),
          axis.title.y = element_blank()) +
    coord_fixed()
}

onehot <- function(index, length) {
  vec <- numeric(length)
  vec[index] <- 1
  return(vec)
}



#

位置についての信念をプロットする関数

  • グリッドワールドのどこにあるのか(位置)についての信念をプロットするplot_beliefs関数を作成する(この実装はあまり理解しなくても良い)
plot_beliefs <- function(belief_dist, title_str = "") {
  # プロット用データの用意
  plot_data <- data.frame(
    x = 1:length(belief_dist),
    probability = belief_dist
  )
  ggplot(plot_data, aes(x = x, y = probability)) +
    geom_bar(stat = "identity", fill = "red") +
    scale_x_continuous(breaks = 1:(length(belief_dist))) +
    scale_y_continuous(limits = c(0, 1)) +
    labs(title = title_str,x = "State",y = "Probability")
}

初期位置の設定

  • グリッドワールド上の4の位置を初期位置とする
# 初期位置を指定して,インデックスを取得
starting_location <- list(c(2, 1))
state_index <- which(sapply(grid_locations, function(loc) all(loc == starting_location[[1]])))
# 初期位置をプロットする
n_states <- length(grid_locations)
starting_state <- onehot(state_index, n_states) 
plot_point_on_grid(starting_state, grid_locations)

初期位置の信念をプロット

  • 初期位置の情報(starting_state)を入れて信念をプロットする
plot_beliefs(starting_state, "Categorical distribution over the starting state")

グリッドワールドを動き回る

  • アクションしてグリッドワールドの中を動いてみましょう!
  • アクションは,“UP”, “DOWN”, “LEFT”, “RIGHT”, “STAY”の5つ

「RIGHT」のインデックスを取得

  • 今回は”RIGHT”を選択(つまり右に移動)しましょう
  • ”RIGHT”のインデックスを取得する(whichを使います)。
# アクションの用意とRIGHTのインデックスを取得
actions <- c("UP", "DOWN", "LEFT", "RIGHT", "STAY")
right_action_idx <- which(actions == "RIGHT")

「RIGHT」の遷移行列から次の状態を計算

  • 取得した”RIGHT”のインデックスを用いて,\(\textbf{B}\)行列のスライスである”RIGHT”アクションの遷移行列を取得
  • ”RIGHT”アクションの遷移行列と初期位置のベクトルから,行列ベクトル積を計算し(\(\textbf{B}\)行列*初期ベクトル),次の状態を取得して,プロットする(図は次スライド)
# 次の状態ベクトルを生成して,グリッドワールドにプロット
next_state <- B[, , right_action_idx] %*% starting_state
plot_point_on_grid(next_state, grid_locations)

「RIGHT」を選択の結果

  • 初期位置(4)を「RIGHT」の遷移行列で移動させて5に

\(\textbf{B}\)行列の設定

  • \(\textbf{B}\)行列( \(P(s_{t}\mid s_{t-1}, a_{t-1})\) )は,時間経過と行為に伴う隠れ状態間の遷移行列であり,アクションによって条件づけられる
  • 上記から,移動には状態遷移行列の\(\textbf{B}\)が使われていること,アクションの選択がその状態遷移行列に作用していることがわかる

(3) 観測の事前信念(\(\textbf{C}\))

  • \(\textbf{C}\)は観測の事前信念であり,生成モデルの特定の観測値に対する事前の選好を確率で表現したもの( \(\tilde{P}(o)\) )。
  • 例えば,エージェントが位置9が望ましいと信じている場合,以下のように位置9(Y=3, X=3)に事前選好を持っているように設定できる(図は次スライド)
# Cを格納する空のベクトルを作成
C <- numeric(n_observations)
# 望ましい報酬のある位置(Y=3,X=3)とそのインデックス(9)を用意
desired_location <- c(3,3)
desired_location_index <- which(sapply(grid_locations, function(x) all(x == desired_location)))
# 望ましい位置の選好を100%(1.0)に設定
C[desired_location_index] <- 1.0
# 事前選好の分布を表示
plot_beliefs(C, title_str = "Preferences over observations")

観測の事前信念(\(\textbf{C}\))の確認

  • 位置9に観測の事前信念が高くなっている

(4) 初期状態に関する事前信念(\(\textbf{D}\))

  • エージェントがもっている初期状態(位置)に関する事前信念\(\textbf{D}\)( \(P(s)\) )を用意する。
  • 今回は,位置1を100%(1.0)とするような初期状態における事前信念を設定する(図は次ページ)
# 初期状態に関する事前信念
D <- onehot(1, n_states)
# 初期状態に関する事前信念をプロット
plot_beliefs(D, title_str = "Prior beliefs over states")

初期状態に関する事前信念の確認

  • 初期状態の事前信念は,位置1が高くなっている。

能動的推論エージェントを作る

  • 状態と観測についてのモデル(\(\textbf{A}\)),行為に伴う状態の遷移ルール(\(\textbf{B}\)),観測に関する事前の選好(\(\textbf{C}\)),初期状態についての信念(\(\textbf{D}\))を設定した。
  • \(\textbf{ABCD}\)の設定に対して,変分自由エネルギーと期待自由エネルギーを最小化するという動作原理を組み込むことで,能動的推論エージェントが完成する。

能動的推論の手順

  • 能動的推論では,以下を行って知覚と行動を行う
  1. 隠れている状態には,状態sとポリシー \(\pi\) の2種類がある。
  2. 変分自由エネルギーFを最小化することでsの事後分布を推定する(知覚)
  3. 期待自由エネルギーGを最小化することで \(\pi\) の事後分布を推定する(行動)

隠れ状態の推論

  • エージェントは観測に応じて状態の推論をする。状態の推論は,変分自由エネルギーを最小化する最適な近似事後分布 \(q(s_t)\) を探すことによってなされる。
  • グリッドワールドのある位置からある位置への移動は時間が離散的なので,生成モデルにPOMDPが使える。
  • POMDPでは,状態sは以下のシンプルな更新則で推測できる( \(\sigma\) はsoftmax関数)

\[ q(s_t) = \sigma\left(\ln \mathbf{A}[o,:] + \ln\mathbf{B}[:,:,a] \cdot q(s_{t-1})\right) \]

状態推定に必要な関数の準備

  • 数値的に安定して動作する対数関数とsoftmax関数,状態を推定するinfer_states関数を準備する。
# 対数関数
log_stable <- function(arr) {
  # 非常に小さい値を避けるための下限値とその場合の置き換え
  MIN_VALUE <- 1e-16
  arr[arr < MIN_VALUE] <- MIN_VALUE
  return(log(arr))
}

# softmax関数
softmax <- function(arr) {
  # オーバーフローを防ぐため,最大値を引く
  arr_shifted <- arr - max(arr)
  exp_arr <- exp(arr_shifted)
  return(exp_arr / sum(exp_arr))
}

# 状態推論関数
infer_states <- function(observation_index, A, prior) {
  # 尤度の対数を計算(上の式の第1項)
  log_likelihood <- log_stable(A[observation_index, ])
  # 事前分布の対数を計算(上の式の第2項(Bとsの積は別に計算))
  log_prior <- log_stable(prior)
  # softmaxを適用して事後分布を計算
  qs <- softmax(log_likelihood + log_prior)
  return(qs)
}

#

状態の推論:現在位置

  • 1つの時点における,状態の推論を試してみる
  • まずは,位置5にいるという信念からスタートし,1つ前に「上」に移動したとする(つまり現在は位置2)
# 過去の状態に関する信念を作成(位置5,つまり(2,2))
qs_past <- onehot(5, n_states)
# 1タイムステップ前に「上」に移動したことを示す
last_action <- "UP"
# 「上」移動のアクションインデックスを取得
action_id <- which(actions == last_action)

状態の推論:状態の事前分布

  • 以下の式で,過去の事後分布と過去のアクションを使って現在の状態の事前分布を取得する

\[ P(s_t) = \mathbf{E}_{q(s_{t-1})}[P(s_t | s_{t-1}, a_{t-1})] \]

  • \(\textbf{B}\)行列と過去の事後分布(qs_past)を使って計算
# アクションと過去の事後分布を使用して事前分布を計算
prior <- B[, , action_id] %*% qs_past

状態の推論:状態の事後分布

  • 現在の事前分布を計算した上で,観測する(位置は2)
  • 観測と \(\mathbf{A}\) 行列と事前分布を使用して,infer_states()関数(観測による状態の更新)を実行する。
  • 更新した状態の信念をプロット(図は次スライド)
# 観測値のインデックスを設定
observation_index <- 2
# 状態推論を実行
qs_new <- infer_states(observation_index, A, prior)
# 隠れ状態に関する信念をプロット
plot_beliefs(qs_new, title_str = "Beliefs about hidden states")

状態の事後分布の確認

  • 事前分布と観測が一致しているので,位置2の信念が100%になる。

観測と事前分布が矛盾する場合

  • 事前分布は位置2だが観測が位置3の場合は,位置2と位置3に半分(50%)ずつ信念が割り振られる。
observation_index <- 3
qs_new <- infer_states(observation_index, A, prior)
plot_beliefs(qs_new)

行為の選択

  • 状態sの推論ができるようになったので,今度は行為の選択( \(\pi\) )を扱う。
  • 行為の選択にあたっては,期待自由エネルギー \(\textbf{G}\)を計算する。

\(\textbf{G}\)の計算に必要な関数の準備

  • 期待される状態 \(Q(s_{t+1}|a_t)\),期待される観測 \(Q(o_{t+1}|a_t)\),尤度 \(P(o|s)\) のエントロピー( \(\mathbf{H}\left[\mathbf{A}\right]\)),期待される観測と事前選好 \(\mathbf{C}\) とのKLダイバージェンスを計算する関数を用意する。
# 期待される状態を計算する関数
get_expected_states <- function(B, qs_current, action) {
  # 特定のアクションが与えられた時の1ステップ先の期待される状態を計算
  qs_a <- B[, , action] %*% qs_current
  return(qs_a)
}

# 期待される観測を計算する関数
get_expected_observations <- function(A, qs_a) {
  # 特定のアクションが与えられた時の1ステップ先の期待される観測を計算
  qo_a <- A %*% qs_a
  return(qo_a)
}

# エントロピーHを計算する関数
entropy <- function(A) {
  H_A <- -colSums(A * log_stable(A))
  return(H_A)
}

# KLダイバージェンスを計算する関数
kl_divergence <- function(qo_a, C) {
  # 2つの1次元カテゴリカル分布間のKullback-Leiblerダイバージェンスを計算
  return(sum((log_stable(qo_a) - log_stable(C)) * qo_a))
}



#

関数の説明

  • get_expected_states():期待される状態 \(Q(s_{t+1}|a_t)\) を,特定の行為に対する状態遷移行列*現在の状態で計算
  • get_expected_observations():期待される観測 \(Q(o_{t+1}|a_t)\) を,尤度*現在の状態で計算
  • entropy():尤度 \(P(o|s)\) のエントロピー( \(\mathbf{H}\left[\mathbf{A}\right]\) )を, \(-diag(\textbf{A}\cdot ln \textbf{A})\) で計算
  • kl_divergence():期待される観測と事前選好 \(\mathbf{C}\) とのKLダイバージェンスを, \((ln\textbf{o}_{\pi} - ln\textbf{C})\cdot \textbf{o}_{\pi}\) で計算

\(\textbf{G}\)の計算:開始位置

  • 開始状態の位置5(2,2)にいると想像する(生成過程の話で,世界の真の状態)
# 位置5のインデックスを取得してベクトル化してプロット
state_idx <- which(sapply(grid_locations, function(x) all(x == c(2,2))))
state_vector <- onehot(state_idx, n_states)
plot_point_on_grid(state_vector, grid_locations)

\(\textbf{G}\)の計算:現在の信念

  • エージェントが位置について正しい信念(位置5)を持って始めると仮定する
# 現在のqs_currentを真の開始状態と同じにする
qs_current <- state_vector
# 現在位置の信念をプロット
plot_beliefs(qs_current, title_str = "どの位置にいると信じているか?")

\(\textbf{G}\)の計算:\(\textbf{C}\)の設定

  • エージェントは位置6(開始位置の右)にいることを好むとする(\(\mathbf{C}\) を位置6が高い確率になるよう設定)
# 位置(2,3)に対する選好を作成
desired_idx <- which(sapply(grid_locations, function(x) all(x == c(2,3))))
# 選好ベクトルCを作成
C <- onehot(desired_idx, n_observations)
# 選好をプロット
plot_beliefs(C, title_str = "Preferences")

\(\textbf{G}\)の計算:“LEFT”と”RIGHT

  • 現在の位置は5なので,取りうる行為は5種類(上下左右と留まる)だが,単純化して”LEFT”または”RIGHT”の2つの期待自由エネルギー \(\mathbf{G}\) を計算する
  • 2つのアクションのインデックスを確認
# 左右の移動に対応するアクションインデックスを取得
left_idx <- which(actions == "LEFT")
right_idx <- which(actions == "RIGHT")
# インデックスを表示
cat('LEFTのインデックスは:', left_idx,
    ',RGHITのインデックスは:', right_idx, '\n')
LEFTのインデックスは: 3 ,RGHITのインデックスは: 4 

\(\textbf{G}\)の計算:“LEFT”での状態の事後分布

  • \(\textbf{G}\)の計算にあたり,“LEFT”を選択した際に期待される状態 \(Q(s_{t+1}|a_t)\) を計算(位置4)
# 2つのアクションの期待自由エネルギーを格納する場所を用意
G <- numeric(2)
# 左に移動する場合の期待される状態を計算
qs_a_left <- get_expected_states(B, qs_current, left_idx)
qs_a_left
      [,1]
 [1,]    0
 [2,]    0
 [3,]    0
 [4,]    1
 [5,]    0
 [6,]    0
 [7,]    0
 [8,]    0
 [9,]    0

\(\textbf{G}\)の計算:“LEFT”での期待される観測

  • 期待される観測 \(Q(o_{t+1}|a_t)\) を計算する。
  • 状態と観測が完全に一致しているので,位置4になる。
# 左に移動する場合の期待される観測を計算
qo_a_left <- get_expected_observations(A, qs_a_left)
qo_a_left
      [,1]
 [1,]    0
 [2,]    0
 [3,]    0
 [4,]    1
 [5,]    0
 [6,]    0
 [7,]    0
 [8,]    0
 [9,]    0

\(\textbf{G}\)の計算:尤度のエントロピー

  • 尤度 \(P(o|s)\) のエントロピー( \(\mathbf{H}\left[\mathbf{A}\right]\) )を計算する。
  • 尤度は対角成分だけのため,すべて0になった。
# 尤度のエントロピーHを計算
H_A <- entropy(A)
H_A
[1] 0 0 0 0 0 0 0 0 0

\(\textbf{G}\)の計算:\(\textbf{G}\)の第1項の計算

\[ G(\pi) = - \mathbb{E}_{q(\tilde{s},\tilde{o}|\pi)}[D_{KL}[q(\tilde{s}|\tilde{o},\pi) \parallel q(\tilde{s}|\pi)]]- \mathbb{E}_{q(\tilde{o}|\pi)}[\log p(\tilde{o}|C)] \\ = \mathbb{E}_{q(\tilde{s},\tilde{o}|\pi)}[H[p(\tilde{o}|\tilde{s})]]-D_{KL}[q(\tilde{o}|\pi) \parallel p(\tilde{o}|C)] \]

  • 上の2行目の式の第1項を計算する。尤度のエントロピー\(\textbf{H}\)は計算しているので,以下のように状態と掛け合わせて合計する(曖昧さの期待値の計算)。
  • 状態と観測が一致しているので,ゼロになる。
predicted_uncertainty_left <- sum(H_A * qs_a_left)
predicted_uncertainty_left
[1] 0

\(\textbf{G}\)の計算:\(\textbf{G}\)の第2項の計算

\[ G(\pi) = - \mathbb{E}_{q(\tilde{s},\tilde{o}|\pi)}[D_{KL}[q(\tilde{s}|\tilde{o},\pi) \parallel q(\tilde{s}|\pi)]]- \mathbb{E}_{q(\tilde{o}|\pi)}[\log p(\tilde{o}|C)] \\ = \mathbb{E}_{q(\tilde{s},\tilde{o}|\pi)}[H[p(\tilde{o}|\tilde{s})]]-D_{KL}[q(\tilde{o}|\pi) \parallel p(\tilde{o}|C)] \]

  • 上の2行目の式の第2項を計算する。具体的には \((ln\textbf{o}_{\pi} - ln\textbf{C})\cdot \textbf{o}_{\pi}\) を計算(リスクの計算)
  • 36.84…になり,期待される観測と事前選好 \(\mathbf{C}\) との間になんらかの距離がある。
predicted_divergence_left <- kl_divergence(qo_a_left, C)
predicted_divergence_left
[1] 36.84136

\(\textbf{G}\)の計算:“LEFT”の\(\textbf{G}\)の計算

\[ G(\pi) = - \mathbb{E}_{q(\tilde{s},\tilde{o}|\pi)}[D_{KL}[q(\tilde{s}|\tilde{o},\pi) \parallel q(\tilde{s}|\pi)]]- \mathbb{E}_{q(\tilde{o}|\pi)}[\log p(\tilde{o}|C)] \\ = \mathbb{E}_{q(\tilde{s},\tilde{o}|\pi)}[H[p(\tilde{o}|\tilde{s})]]-D_{KL}[q(\tilde{o}|\pi) \parallel p(\tilde{o}|C)] \]

  • 2行目の式の第1項(predicted_uncertainty_left)と第2項(predicted_divergence_left)が計算できたので,2つを足して,期待自由エネルギー\(\textbf{G}\)を計算する。
  • 曖昧さの期待値(predicted_uncertainty_left)はゼロのため,リスクのみが\(\textbf{G}\)に反映される。
# 左に移動する場合のGを計算
G[1] <- predicted_uncertainty_left + predicted_divergence_left
G[1]
[1] 36.84136

\(\textbf{G}\)の計算:“RIGHT”の\(\textbf{G}\)の計算

  • “RIGHT”の選択も同様なので,一気に計算
  • 曖昧さの期待値は”LEFT”と同じくゼロだが,リスクは”LEFT”とは違いゼロになる(右に移動すると好ましい位置6になる)。結果,期待自由エネルギーもゼロに
# 右に移動する場合の期待される状態を計算
qs_a_right <- get_expected_states(B, qs_current, right_idx)
# 右に移動する場合期待される観測を計算
qo_a_right <- get_expected_observations(A, qs_a_right)
# 曖昧さの期待値
predicted_uncertainty_right <- sum(H_A * qs_a_right)
# リスク
predicted_divergence_right <- kl_divergence(qo_a_right, C)
# 右に移動する場合のGを計算
G[2] <- predicted_uncertainty_right + predicted_divergence_right
cat('曖昧さの期待値:', predicted_uncertainty_right, ',リスク', predicted_divergence_right, ',期待自由エネルギーG', G[2])
曖昧さの期待値: 0 ,リスク 0 ,期待自由エネルギーG 0

\(\textbf{G}\)の計算:2つの\(\textbf{G}\)の比較

  • 2つの行為の期待自由エネルギー\(\textbf{G}\)を並べると,“RIGHT”選択の期待自由エネルギー\(\textbf{G}\)の方が小さい
# 計算した期待自由エネルギーを表示
cat('左に移動することの期待自由エネルギーG:', G[1],
    ',右に移動することの期待自由エネルギーG', G[2], '\n\n')
左に移動することの期待自由エネルギーG: 36.84136 ,右に移動することの期待自由エネルギーG 0 

アクションの事後分布の計算

  • \(Q(a_t) = \sigma(-\mathbf{G})\)を使って,アクションの事後分布を計算する(\(\sigma\)はsoftmax関数)
  • 右に移動する確率のほうが高くなる。期待自由エネルギー\(\textbf{G}\)が小さい方の行為が選ばれる確率が高い(\(\textbf{G}\)に従って行動することが\(\textbf{G}\)の最小化につながる)
# アクションの確率を計算
Q_a <- softmax(-G)

# 各アクションの確率を表示
cat('左に移動する確率:', Q_a[1], 
    ',右に移動する確率:', Q_a[2], '\n')
左に移動する確率: 1e-16 ,右に移動する確率: 1 

能動的推論の流れ

  • 能動的推論では以下を繰り返して\(\textbf{F}\)\(\textbf{G}\)を最小化する
  1. 環境から観測 \(o_t\) をサンプリング
  2. 状態を推論する(変分自由エネルギー最小化を通じて \(q(s)\) を最小化)
  3. 期待自由エネルギー \(\mathbf{G}\) を計算
  4. アクションの事後分布 \(\sigma(-\mathbf{G})\) からアクションをサンプリング
  5. サンプリングされたアクション \(a_t\) で環境(生成過程)に働きかけ,ステップ1に戻る

Rでオブジェクト指向プログラミング

  • Pythonのクラスのようなオブジェクト指向プログラミングをRで行う場合はR6パッケージを使う。R6クラスを定義するには,R6Class() 関数を使用する。
MyClass <- R6Class("MyClass",
  public = list(
    name = NULL,
    age = NULL,
    initialize = function(name, age) {
      self$name <- name
      self$age <- age
    },
    greet = function() {
      print(paste("Hello, my name is", self$name, "and I am", self$age, "years old."))
    }
  )
)

R6クラスの設定:フィールド

  • publicリストに,フィールドとメソッドを定義(クラスの外からアクセスさせないprivateリストもある)
  • フィールドは変数の宣言になり,以下ではnameやage
MyClass <- R6Class("MyClass",
  public = list(
    name = NULL,
    age = NULL,
    initialize = function(name, age) {
      self$name <- name
      self$age <- age
    },
    greet = function() {
      print(paste("Hello, my name is", self$name, "and I am", self$age, "years old."))
    }
  )
)

R6クラスの設定:初期化メソッド

  • initializeメソッドは,クラスのインスタンスが作成されるときに実行される関数。ここでは,インスタンス作成時に入力されるname, ageを受け取って,フィールドに格納(その際にself$XXXを使う)。
MyClass <- R6Class("MyClass",
  public = list(
    name = NULL,
    age = NULL,
    initialize = function(name, age) {
      self$name <- name
      self$age <- age
    },
    greet = function() {
      print(paste("Hello, my name is", self$name, "and I am", self$age, "years old."))
    }
  )
)

R6クラスの設定:自作メソッド

  • greetのように,関数を設定しておくと,インスタンスで使えるようになる。
MyClass <- R6Class("MyClass",
  public = list(
    name = NULL,
    age = NULL,
    initialize = function(name, age) {
      self$name <- name
      self$age <- age
    },
    greet = function() {
      print(paste("Hello, my name is", self$name, "and I am", self$age, "years old."))
    }
  )
)

インスタンスの作成

  • クラスのインスタンスを作成するには、オブジェクト名$new() メソッドを使用する。
my_object <- MyClass$new("John", 30)

メソッドの呼び出し

  • クラスのメソッドを呼び出すには、オブジェクト名$関数名() を使用する。
my_object$greet()
[1] "Hello, my name is John and I am 30 years old."

能動的推論エージェントを作成

  • R6を使って能動的推論エージェントのクラスを作成
  • フィールドの設定をしてからメソッドを設定。initializeメソッドで\(\textbf{ABCD}\)行列をいれる。infer_statesで状態の推定,calc_Gで期待自由エネルギーの計算,sample_actionでアクションのサンプリング
AIagent <- R6Class("AIagent",
  public = list(
    # フィールドの定義
    A = NULL,
    B = NULL,
    C = NULL,
    D = NULL,
    actions = NULL,
    prior = NULL,
    qs = NULL,
    G = NULL,
    # 初期化メソッド
    initialize = function(A, B, C, D,actions) {
      self$A = A
      self$B = B
      self$C = C
      self$prior = D
      self$actions = actions
    },
    # 状態の推定
    infer_states = function(obs_index) {
      # 尤度の対数を計算(上の式の第1項)
      log_likelihood <- log_stable(self$A[obs_index, ])
      # 事前分布の対数を計算(上の式の第2項(Bとsの積は別に計算))
      log_prior <- log_stable(self$prior)
      # softmaxを適用して事後分布を計算
      self$qs <- softmax(log_likelihood + log_prior)
      return(self$qs)
    },
    # 期待自由エネルギーGの計算
    calc_G = function() {
      # 尤度 P(o|s) のエントロピーHを計算
      H_A <- -colSums(self$A * log_stable(self$A))
      # 各アクションについて以下繰り返し
      for(action_i in seq_along(self$actions)) {
        # 現在のアクションにおける期待される状態を計算
        qs_a <- self$B[, , action_i] %*% self$qs
        # 現在のアクションにおける期待される観測を計算
        qo_a <- self$A %*% qs_a
        # 曖昧さの期待値を計算
        pred_uncertainty <- sum(H_A * qs_a)
        # リスクを計算
        pred_div <- sum((log_stable(qo_a) - log_stable(self$C)) * qo_a)
        # これらを合計して期待自由エネルギーGを得る
        self$G[action_i] <- pred_uncertainty + pred_div
      }
      return(self$G)
    },
    # アクションの事後分布の計算とサンプリング
    sample_action = function(){
      # アクションの事後分布を計算
      Q_a <- softmax(-self$G)
      # アクションの確率分布からアクションをサンプリング
      chosen_action <- sample(length(Q_a), size = 1, prob = Q_a)
      # 次のタイムステップの推論のための事前分布を計算
      self$prior <- self$B[, , chosen_action] %*% self$qs
      return(chosen_action)
    }
  )
)






#

グリッドワールド環境を作成

  • グリッドワールド環境(生成過程)のクラスを設定(生成過程と生成モデルは別,現実の生成過程の方が複雑)
  • 能動的推論エージェントの選択に応じて状態を変化させ,エージェントが移動できる環境を設定(エージェントの選択に対して観測を返すstepメソッドを用意)
# グリッドワールドの環境クラスを定義
GridWorldEnv <- R6Class("GridWorldEnv",
  public = list(
    # フィールドの定義
    init_state = NULL,
    current_state = NULL,
    # 初期化メソッド
    initialize = function(starting_state = c(1,1)) {
      self$init_state <- starting_state
      self$current_state <- self$init_state
      cat('Starting state is', paste(starting_state, collapse=","), '\n')
    },
    # 1ステップ進めるメソッド
    step = function(action_label) {
      Y <- self$current_state[1]
      X <- self$current_state[2]
      if(action_label == "UP") {
        Y_new <- if(Y > 1) Y - 1 else Y
        X_new <- X
      } else if(action_label == "DOWN") {
        Y_new <- if(Y < 3) Y + 1 else Y
        X_new <- X
      } else if(action_label == "LEFT") {
        Y_new <- Y
        X_new <- if(X > 1) X - 1 else X
      } else if(action_label == "RIGHT") {
        Y_new <- Y
        X_new <- if(X < 3) X + 1 else X
      } else if(action_label == "STAY") {
        Y_new <- Y
        X_new <- X
      }
      # 新しいグリッド位置を保存
      self$current_state <- c(Y_new, X_new)
      # エージェントは常に自分がいるグリッド位置を直接観測する
      obs <- self$current_state
      return(obs)
    },
    # 環境をリセットするメソッド
    reset = function() {
      self$current_state <- self$init_state
      cat('Re-initialized location to', paste(self$init_state, collapse=","), '\n')
      obs <- self$current_state
      cat('..and sampled observation', paste(obs, collapse=","), '\n')
      return(obs)
    }
  )
)






#

能動的推論の生成モデルの準備

  • 能動的推論エージェントのために,生成モデル(\(\textbf{ABCD}\)行列),5つのアクションを用意する
  • 好ましい位置は9,状態の初期信念\(\textbf{D}\)を6
# 尤度Aを作成(単位行列)
A <- diag(n_states)
# 状態遷移行列Bを作成
B <- create_B_matrix()
# 選好Cを作成(位置9を好む)
desired_loc_idx <- which(sapply(grid_locations, function(x) all(x == c(3,3))))
C <- onehot(desired_loc_idx, n_observations) 
# 初期信念Dを作成(位置6からスタートすると信じているとします)
start_loc_idx <- which(sapply(grid_locations, function(x) all(x == c(2,3))))
D <- onehot(start_loc_idx, n_states) 
# 可能なアクションのベクトル
actions <- c("UP", "DOWN", "LEFT", "RIGHT", "STAY")

グリッドワールド環境のインスタンス化

  • $new()メソッドを使って,グリッドワールド環境のインスタンス化をする。
  • 開始位置を6(つまり,(2,3))で初期化(つまり,エージェントは開始位置について正確な信念を持っている)
env <- GridWorldEnv$new(starting_state = c(2,3))
Starting state is 2,3 

能動的推論エージェントのインスタンス化

  • $new()メソッドを使って,能動的推論エージェントのインスタンス化をする。
  • これ以降は,agent$XXX()というやり方でメソッドを実行できる。また,メソッドを実行すれば,必要に応じてフィールドが更新されていく。
agent <- AIagent$new(A, B, C, D,actions)

能動的推論エージェントの動作確認用プロット関数

  • 能動的推論エージェントの動作確認ため,信念,グリッド位置,期待自由エネルギー,選択などをプロットするplot_ai_agent()を定義する(ただキレイなプロットを書くだけの関数なので,理解する必要はないです)
plot_ai_agent <- function(qs_current,state_vector, grid_locations, actions, chosen_action,G, t){
 p1 <- plot_beliefs(qs_current, title_str = paste("Beliefs about location at time", t))
 # グリッドの位置のプロット
 p2 <- plot_point_on_grid(state_vector, grid_locations)
 # 期待自由エネルギーと選択した行為のプロット
 chosen_data <- data.frame(
      actions = actions,
      selected_action = ifelse(seq_along(actions) == chosen_action, 1, 0),
      G = G)
 # 期待自由エネルギーのプロット
 p3 <- ggplot(chosen_data, aes(x = actions, y = G)) +
      geom_bar(stat = "identity", fill = "lightgreen") +
      theme_minimal() +
      labs(x = "Actions", y = "Expected Free Energy") +
      theme(axis.text.x = element_text(angle = 0, hjust = 0.5))
 # 選択した行為のプロット
 p4 <- ggplot(chosen_data, aes(x = actions, y = selected_action)) +
      geom_bar(stat = "identity", fill = "steelblue") +
      ylim(0, 1) +  # y軸の範囲を0から1に設定
      theme_minimal() +
      labs(x = "Actions", y = "Selected action") +
      theme(axis.text.x = element_text(angle = 0, hjust = 0.5))
    grid.arrange(p1, p2, p3, p4, nrow = 2)
}






#

能動的推論エージェントの動作確認

  • 環境の準備をし,(1)状態の推定,(2)\(\textbf{G}\)の計算,(3)行動をサンプリングする(図は次スライド)。
# 試行番号の設定,環境のリセット,観測のインデックスを取得
t <- 1
obs <- env$reset()
obs_idx <- which(sapply(grid_locations, function(x) all(x == obs)))
# 状態に関する推論を実行
qs_current <- agent$infer_states(obs_idx)
# グリッドワールド上の位置を取得
state_idx <- which(sapply(grid_locations,function(x) all(x == obs)))
state_vector <- onehot(state_idx, n_states)
# 期待自由エネルギーを計算
G <- agent$calc_G()
# アクションのサンプリングとラベル取得
chosen_action <- agent$sample_action()
action_label <- actions[chosen_action]
# プロット
plot_ai_agent(qs_current, state_vector, grid_locations, actions, chosen_action, G, t)

能動的推論エージェントの動作確認

  • 6からスタート,“DOWN”の期待自由エネルギーが低くなっている。
Re-initialized location to 2,3 
..and sampled observation 2,3 

5試行の能動的推論を実行

  • 今後は,開始位置を5(中央)にして,5試行分能動的推論を実行しましょう!(アクションのサンプリング後に環境の更新が追加)
trial_n <- 5 
env <- GridWorldEnv$new(starting_state = c(2,2))
agent <- AIagent$new(A, B, C, D,actions)
obs <- env$reset()

for (t in 1:trial_n) {
  obs_idx <- which(sapply(grid_locations, function(x) all(x == obs)))
  # 状態に関する推論を実行
  qs_current <- agent$infer_states(obs_idx)
  # グリッドワールド上の位置を取得
  state_idx <- which(sapply(grid_locations,function(x) all(x == obs)))
  state_vector <- onehot(state_idx, n_states)
  # 期待自由エネルギーを計算
  G <- agent$calc_G()
  # アクションのサンプリングとラベル取得
  chosen_action <- agent$sample_action()
  action_label <- actions[chosen_action]
  # アクションを踏まえた環境の更新
  obs <- env$step(action_label)
  # プロット
  plot_ai_agent(qs_current, state_vector, grid_locations, actions, chosen_action, G, t)
}

#

5試行の能動的推論:1試行目

5試行の能動的推論:2試行目

5試行の能動的推論:3試行目

5試行の能動的推論:4試行目

5試行の能動的推論:5試行目

プランニング

  • 開始位置を1にすると,5試行では位置9まで到達しない(env <- GridWorldEnv$new(starting_state = c(1,1))で実行すると確認できる)
  • これまで実行した設定では各行為の期待自由エネルギーGを1タイムステップ先までしか評価していない。位置9から遠い位置ではアクションごとの期待自由エネルギーに差がなく,行動選択がランダムになる。
  • プランニング,すなわち複数タイムステップを考慮したポリシーを設定すれば短い試行で位置9に到達する(実装がやや複雑なので今回は省略,詳細はpymdpのチュートリアル参照)

能動的推論モデルの学習

  • 生成モデル(\(\textbf{ABCD}\))を事前に設定してきたが,生成モデルはエージェントのモデルであって,生成過程のモデルではない。体験を通した生成モデルの変化(学習)を考える必要があることも。
  • 生成モデルの尤度(\(\textbf{A}\))と遷移行列(\(\textbf{B}\))を経験を通して変化(学習)させることができる。
  • \(\textbf{A}\)\(\textbf{B}\)などのパラメータに関する信念を設定して,その更新によって学習を実現できる。

階層的推論(深層生成モデル)

階層的推論(深層生成モデル)

  • Sandved-Smith and Hesp et al.(2011)のメタアウェアネスモデルの生成モデル
  • 上の層は時間的にゆっくり変化
  • 層が増えるほど複雑な現象のモデル化ができる

まとめ

  • 離散時間のPOMDP下での能動的推論は,生成過程(環境のモデル)と生成モデル(\(\textbf{ABCD}\)行列)を設定すれば,シミュレーションできる
  • pymdpなどのパッケージを使うと便利だが,ブラックボックス化&モデル作成の自由度が下がる可能性がある。トイモデルをスクラッチで書くのが良い?
  • 生成モデルについて,プランニング,学習,階層化などを通してより複雑な現象も扱える。

(バンディット課題も入れたかったが今回は分量的に断念…またの機会に…)

おまけ:因子グラフをRで描く

  • 能動的推論では,モデルを因子グラフを使って記載する。
  • これは,DiagrammeRを使って,grVizを用いて描くことができる(\(\textbf{D}\)->s->\(\textbf{B}\)のところの矢印の高さがどうしても揃わないけど…)
  • 興味のある方は以下を参照ください(Colabではうまく走らないのでローカルでお試しください)。

factor graph of active active inference with R