6. act_FUN

Note

act_FUNはエージェントの更新に関わる関数で、rABMの中核をなすコンポーネントです。

この章では、はじめにact_FUNの基本的な作り方を復習します。次に、関数の引数を変化させられるように設定する二つの方法について学びます。最後に更新を行ううえで速度を上げるためのいくつかのアイデアについて紹介します。

6.1 act_FUNの作り方

act_FUNは、(1)Stateの更新に関わる関数、(2)更新を行うエージェントの選択に関する関数の二つに区分して考えることができます。

(1) Stateの更新に関わる関数

act_FUNのもっとも基本的な使い方は、エージェントの状態を表現するStateフィールドの更新を行うことです。以下の例は、エージェントの所持金額が各回ごとに1ずつ増加する更新を行う関数の作成例です。

# ライブラリを読み込む
library(rABM)
# エージェントの所持金を示すmoneyフィールドを作成し、Gameオブジェクトに格納する

# 5人のエージェントのそれぞれの所持金
money <- 1:5

# Gameオブジェクトに格納する
G <- Game(State(money))

# 状態をプリントする
G
<Game>
$money(state)
[1] 1 2 3 4 5

------------------- 
time         : 1 
n of logs    : 0 
n of notes   : 0 
n of fields  : 1 
 state       : money
------------------- 
# エージェントの所持金を更新する
get_money <- function(){self$money <- self$money + 1}

# Gに格納する
add_field(G, Act(get_money))

(2) 作成の際のアドバイス

前章で紹介したように実際の作成においては、先にStateのみを作成したGオブジェクトを作成し、それをselfに付け替えたうえで、確認しながら関数を作成するほうが行いやすいです。

# いったんStateだけのGameオブジェクトを作成する
money <- 1:5
G <- Game(State(money))

# Gをselfとして代入する
self <- G

# act_FUNを作成する
get_money <- function(){self$money <- self$money + 1}

# Gに格納する
add_field(G, Act(get_money))

(3) act_FUNのテスト

act_FUNは、通常通り呼び出すことで関数を実行します。ただし関数名()のように()をつけることで実行できることに注意してください。こちらを用いて、きちんと関数が機能しているのかを確認することができます。

# 現状の所持金を確認
G$money
[1] 1 2 3 4 5
# 更新し、所持金を確認
G$get_money()

# 更新後の所持金を確認
G$money
[1] 2 3 4 5 6

(4) エージェントの選択に関わる関数

上記の例ではすべてのエージェントの所持金が更新されていましたが、もしも特定のエージェントのみを選択したい場合にはどうしたらよいでしょうか。この場合には、更新対象のエージェントを選択する別の関数を用意し、選択されたエージェントのインデックスをStateに格納したうえで、更新の際にそのStateを参照するということが考えられます。

# エージェントの初期所持金
money <- 1:5

# 更新対象のエージェントを保存するstate
selected_agent <- c()

# 更新対象のエージェントをランダムに2つ選択
select_agent <- function(){sample(1:5, 2)}

# 対象のエージェントの所持金を更新
add_money <- function(){
  self$money[self$selected_agent] <- self$money[self$selected_agent] + 1}

# すべてのフィールドを格納
G <- Game(State(money), State(selected_agent), Act(select_agent), Act(add_money))

6.2 引数を変えられるようにする

ABMにおいては、更新のルールを変更することによって、シミュレーションの結果がどのように変化するのかを検証する作業がしばしば行われます。このような機能をact_FUNに持たせるには、大きく二つの方法があります。すなわち(1)引数がほかのフィールドを参照するように設計する、(2)run_Gameの実行時に引数の指定を変えるというものです。以下順番に説明します。

(1) 引数がほかのフィールドを参照するように設計する

以下の例では、加えられる金額bが、別のフィールドbetaを参照するように設計することで、betaを変えることとadd_moneyの引数が変わることが連動するように設計しています。

# 初期所持金
money <- 1:5

# 加える額の引数
beta <- 1

# お金を加える関数
add_money <- function(beta = self$beta){self$money <- self$money + beta}

# Gameに格納する
G <- Game(State(money), State(beta), Act(add_money))

ここで、いったんbetaの値をデフォルトの1のままいったん回してみましょう。

G$add_money()

# 資金の状態を確認
G$money
[1] 2 3 4 5 6

次に、$betaの値を2に変え、更新時には+2がされるように設定を変えます。

# betaを2に変える
G$beta <- 2

# 新しいbetaの値でadd_moneyを実行する
G$add_money()

# 金額を確認する
G$money
[1] 4 5 6 7 8

(2) 引数をあらかじめ設定し、run_Gameのplanで指定する

次の例は、add_moneybetaという引数を設定するところまでは同じですが、betaの値をrun_Gameの実行時に、直接planの中で指定する方法です。

# 初期所持金
money <- 1:5


# お金を加える関数
add_money <- function(beta = 1){self$money <- self$money + beta}

# Gameに格納する
G <- Game(State(money), Act(add_money))

run_Gameのプランでadd_moneyを呼び出す際に、beta = 2を引数内に記載します。

G2 <- run_Game(G = G, plan = c("add_money(beta = 2)"), times = 1, verbose = FALSE)
[stop_FUN] 
stop times at 2

The initial values at time 1 were saved.
# 中身を確認する(money = {3, 4, 4, 6, 7}になる)
G2$money
[1] 3 4 5 6 7

なぜこのような動作が起こるのかというと、run_Gameには、planで指定された引数を解釈し、当該関数の引数をあらかじめ更新する機能が備わっているからです。実際、結果が格納されたG2のadd_moneyの中身を見てみると引数のデフォルトがbeta = 2に書き換えられていることが確認できます。

G2$add_money
function (beta = 2) 
{
    self$money <- self$money + beta
}
<environment: 0x000001e3eedaae40>

6.3 速度を上げるためのアイデア

act_FUNはシミュレーション中に何度も呼ばれる関数となるため、更新の速度が速い設計にしておく方が望ましいです。究極的にはRCPPパッケージを用いたC言語で書くという方法が考えられますが、ここではもう少し簡単にできる工夫を3つ紹介したいと思います。これらはいずれも、act_FUNに特有というよりも、Rでの計算を行う場合に一般的に当てはまるものです。

(1) ベクトル計算する

Rはベクトル計算の方が、個々の計算を行うよりも圧倒的に速くなります。例えば、ここまで用いてきたadd_money関数は、ベクトルmoneyに1を足すというベクトル計算を行っています。一方、同じadd_moneyは以下のようにも書くことができますが、セルごとにループ計算を行うため速度は遅くなります。

add_money2 <- function(){
  for(i in seq_len(length(self$money))){
    self$money[i] <- self$money[i] + 1
  }
}

(2) メモリをあらかじめ確保する

もちろん、どうしてもループ処理をしなければならない場面も出てきます。そのような場合には、あらかじめ計算結果を格納する対象のオブジェクトを用意し、メモリを確保したうえでループを行うほうが、計算速度が速くなります。

以下では、xiを順に足していく単純な例で、あらかじめメモリを確保せずにループの途中で逐次的に追加していく遅い方法と、メモリを確保したうえで追加する速い方法を並べてみました。

# 遅い例
add_i_1 <- function(){
  # 空のオブジェクトaをつくる
  x <- c()
  
  # xに順にiを足す
  for(i in 1:5){
    x <- c(x, i)
  }
  
  # xを更新する
  self$x <- x
}
# 速い例
add_i_2 <- function(){
  # 空のオブジェクトを格納する最終的な長さ分先に確保しておく
  x <- rep(0, 5)
  
  # xにiを順に足す
  for(i in 1:5){
    x[i] <- i
  }
  
  # xを更新する
  self$x <- x
}

(3) キャッシュする

シミュレーションの際にはしばしば結果が同じになる計算を何度も行う場面が出てきます。このようなものについては、あらかじめ計算結果を別のフィールドに保存しておくことによって、計算をスキップし、速度を上げることができます。

以下の例は、毎回エージェントの数をlengthで計算するのではなく、settingsというフィールドに格納しておいて、そちらを参照することによって計算速度を(この例の場合にはごくわずかですが)上げています。

# money
money <- 1:5

# エージェントの数をsettingsというフィールドに格納しておく
settings <- list()
settings$n_agent <- 5

# ループの際にはこのsettings内のn_agentを参照する
add_money <- function(){
  for(i in seq_len(self$settings$n_agent)){
    self$money[i] <- self$money[i] + 1
  }
}

# Gameに格納
G <- Game(State(money), State(settings), Act(add_money))

(4) do.callでまとめて結合する

lapply関数などを用いてリスト形式のものをまとめる場合には、do.callを用いると一括して結合を行うことができます。次の例は、個々に生成されたdata.framedo.callを用いて、一括してrbindするものです。

act <- function(){
  # lapplyを用いた計算の例
  dat_list <- lapply(1:5, function(i){
    data.frame(x = i, y = i^2)
  })
  
  # do.callを用いて一括結合
  do.call(rbind, dat_list)
}

【参考】並列計算に関する注意

このほか、並列計算を用いるという手法も考えられますが、あとの章で紹介するように、run_Gameにも複数のシミュレーションを並列計算をする機能が実装されており、それぞれの機能が衝突する可能性があるため、基本的にはお勧めしません。ただし、run_Gameをあくまで単体のシミュレーションを行うことしか予定していないならば、並列計算をact_FUNに組み込むことは理論上可能です。