はじめに
こんにちは、事業会社で働いているデータサイエンティストです。
今回の記事は
の続きで、ディリクレ過程レジーム埋め込みモデル(Dirichlet Process Regime Embedding、DPRE)モデルの事後分布のAPIをDockerを利用して作成してみます。
技術の説明に入る前に、まずは政治学や経済学、法学、社会学の人にとって馴染みのないAPIとDocker(コンテナ化)の技術を利用する必要性から説明します。
この記事で利用するコードはgithubで公開しました。
また、ディレクトリ構成はこちらです:
> fs::dir_tree()
.
├── README.md
├── docker
│ ├── Dockerfile.plumber
│ └── necessary_packages.R
├── docker-compose.yml
├── dpre_plumber_api.Rproj
├── estimation
│ ├── estimation.R
│ ├── senate_dpre
│ └── senate_dpre.stan
├── parameters
│ ├── cos_sim_dpre_df.obj
│ └── eta_dpre_df.obj
└── plumber
├── plumber.R
└── run_plumber.R
API化の必要性
最近の計量社会科学、特にベイズ機械学習関連のモデルでは、学術的に意義のあるQOI(Quantities of Interest)を大量に抽出してくれます。例えば、経済学の場合は大量の商品のスルツキー行列、政治学の場合は私が今まで書いた記事で提案したような、大量のイデオロギー比較などがあります。
大量のQOIの抽出自体は(偽陽性を避けるために事前分布の設定や正則化などの工夫が必要ですが)嬉しいことですが、巨大な結果ファイルの整形は面倒です。同じ言語だったらまだ良いですが、例えばPythonのpyroで構築した国際法学モデルのパラメータを普段Rしか使わない人が抽出・整形するのは困難で、途中でモデル開発者が想定したロジックの再現が失敗する可能性もあります。
そこで、APIを利用すれば、モデル開発者が定義した方式でリクエストを飛ばせば、誰がどんな言語を利用しても、モデルに同じ指示をすることができ、そして同じ結果が得られます。
少し話が脱線しますが、ビジネス側の方にとって、LookMLでビジネスロジックを統一することに似ている、と説明した方がわかりやすいかもしれません。
Dockerの必要性
Dockerはすごく難しいですが、Dockerのおかげで、プログラムが動作する環境(ハードウェア、通信系は除く)の同一性を完全に担保でき、高い移植性を実現できます。要するに、クラウドなど、より多くの人がアクセスでき、かつ計算資源の拡充が容易なところに、自分が今まで手元のPCでやっていたことを簡単に再現できます。
私もDockerの初心者で、ChatGPTなしではDockerが書けないですが、Dockerはただの処理指示書だと理解してください(情報工学の専門家の皆さん許してください)。
台湾人なので、外国人が長期滞在で日本に入国した後やらないといけないことをDocker風に表現すると
RUN 空港で在留カードをもらう
RUN 役所で住民登録する
RUN 携帯の契約をする
RUN 銀行口座を作る
になります。何をするかの指示を書くだけです。実は割と単純で怖くないです。
API定義
さて、APIの定義ですが、今回はコサイン類似度と$\eta$の事後分布を抽出するAPIを作ります。両方とも上院議員名を入力とし、コサイン類似度の方はさらに上位何件を持ってくるかを指定する数字も受け取ります。
plumber API構築
では早速APIを構築しましょう!
plumberの書き方の詳細はこちらの記事で確認してください。
library(plumber)
#* @apiTitle DPRE API
#* @apiDescription DPREモデルの事後分布からQOIを抽出するAPI
#* コサイン類似度を抽出する
#* @param senator 対象の上院議員
#* @param top_num 抽出データ数
#* @get /cos_sim
function(senator, top_num) {
cos_sim_dpre_df |>
dplyr::filter(.data$senator_1 == .env$senator) |>
dplyr::select(senator_2, mean, q5, q95) |>
dplyr::arrange(-mean) |>
head(as.integer(top_num))
}
#* ETAの事後分布を抽出する
#* @param senator 対象の上院議員
#* @get /eta
function(senator) {
eta_dpre_df |>
dplyr::filter(.data$senator == .env$senator) |>
dplyr::select(senator, latent_group_id, mean, q5, q95)
}
割と単純な処理なんですが、今回の記事はplumberとDockerの利用方法がメインなので、裏のRの処理の難易度はあえて抑えています。
実行時は、こちらのRのスクリプトで必要なデータを読み込ませた後にAPIを起動させる作業手続きを指定します:
library(plumber)
cos_sim_dpre_df <- readRDS("parameters/cos_sim_dpre_df.obj")
eta_dpre_df <- readRDS("parameters/eta_dpre_df.obj")
plumb("plumber/plumber.R")$
run(host = "0.0.0.0", port = 8000)
Dockerfile構築
次はDockerfileを構築します。
これはChatGPTと壁打ちしながら書きました。生成AIに感謝です!
Dockerfileは要するに、仮想PC内に指示を出す手順書だけです。
「ここのファイルをここに持ってきてー」「フォルダ作ってー」「これをダウンロードしておいてー」だけです。
実際はこんな感じです:
FROM rstudio/plumber
WORKDIR /api
RUN mkdir parameters
RUN mkdir plumber
COPY parameters/* parameters/
COPY plumber/* plumber/
RUN apt-get update && apt-get install -y \
libssl-dev \
libcurl4-openssl-dev
# Install R packages
RUN install2.r dplyr
ENTRYPOINT ["Rscript", "plumber/run_plumber.R"]
何をしているのかというと、
- FROM:イメージ(他の人が作ってくれた便利な箱みたいなもの)を指定する
- WORKDIR:作業ディレクトリを指定する
- RUN:コマンドの実行。ほぼlinuxコマンドの前にRUNをつけるだけです
- COPY:必要なファイルをCOPY [本物の手元のPC内の場所] [仮想PC内の場所]で指定します
- ENTRYPOINT:仮想PC起動時に行われる処理の指示
です。
日本語に訳すと
rstudio/plumberからイメージを持ってきて
/apiという場所で作業するよ
parametersフォルダを作って
plumberフォルダを作って
手元のPCのparametersフォルダ配下の前ファイルをコピーして仮想PCのparametersフォルダに入れて
手元のPCのplumberフォルダ配下の前ファイルをコピーして仮想PCのplumberフォルダに入れて
linux側で必要なパッケージを入れて
dplyrをRにインストールして
起動されたら、仮想PCのplumber/run_plumber.RファイルをRで実行して
になります。
docker-compose.yml構築
docker-compose.ymlとは、簡単にいうとDockerの細かい環境設定を指示するファイルです。
version: '3.8'
services:
dpre_api:
build:
context: .
dockerfile: docker/Dockerfile.plumber # Specify the custom Dockerfile name
container_name: dpre_api # Specify the custom container name
ports:
- '8000:8000'
これも基本的にはChatGPTと壁打ちしながらやれば書けるようになります。
ちなみに、ChatGPTにコードの質問をする際は、必ず理由を聞きましょう。ハルシネーションの心配ももちろんありますが、理由を知ることで、コードの理解が深まり、いずれはChatGPTに依存しない、もしくは他人に教えられるほどの知識を得ることができます。
結果確認
まず、Rstudioのterminalで、
docker-compose build
を入れて実行します。
そしたら仮想PC内で指定された処理が実行されます。
実行が終わったら
docker-compose up
を実行して、このような結果が出てきます。
[+] Running 1/0
✔ Container dpre_api Created 0.0s
Attaching to dpre_api
dpre_api | Running plumber API at http://0.0.0.0:8000
dpre_api | Running swagger Docs at http://127.0.0.1:8000/__docs__/
これで手元のPCのchromeやR、Pythonなどからアクセスできるようになりました。
chromeでアクセスする
実際にhttp://127.0.0.1:8000/__docs__/ を実行したら
の画面が出てきます。
これはPythonのFastAPIなども利用しているAPI開発者向けツールSwaggerです。
念の為の強調なんですが、今何が起きているのかというと、私のPCは実は今仮想PCという別のPCとやりとりしています。たまたま(?)両方とも私のPC内に存在しているだけです。なので、普段直接plumber::pr_run()を実行するときの状況とは異なります。
ここで、実際にオバマさんとコサイン類似度が近い議員を抽出してみましょう:
念の為手元のRでも(そう、この二つのRは一応別PCに存在している扱いになっています)実行してみよう:
> cos_sim_dpre_df |> dplyr::filter(senator_1 == "OBAMA (D IL)") |> dplyr::select(senator_2, mean, q5, q95) |>
dplyr::arrange(-mean)
# A tibble: 102 × 4
senator_2 mean q5 q95
<chr> <dbl> <dbl> <dbl>
1 OBAMA (D IL) 1 1 1
2 MIKULSKI (D MD) 0.993 0.987 0.997
3 DODD (D CT) 0.992 0.985 0.997
4 MENENDEZ (D NJ) 0.991 0.984 0.997
5 REED (D RI) 0.990 0.983 0.996
6 LEVIN (D MI) 0.989 0.981 0.995
7 DURBIN (D IL) 0.989 0.981 0.996
8 KERRY (D MA) 0.989 0.980 0.995
9 CANTWELL (D WA) 0.989 0.981 0.995
10 SARBANES (D MD) 0.988 0.979 0.995
# ℹ 92 more rows
一致していますね!
$\eta$の方も確認すると、APIでは
の結果を返却していますが、R側は
> eta_dpre_df |> dplyr::filter(senator == "NELSON (D NE)")
# A tibble: 5 × 11
variable mean median sd mad q5 q95 senator_id latent_group_id senator party_id
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <int> <chr> <dbl>
1 eta[77,1] 0.904 0.926 0.0942 0.0762 7.34e-1 0.996 77 1 NELSON… 2
2 eta[77,2] 0.0843 0.0632 0.0776 0.0724 1.39e-3 0.237 77 2 NELSON… 2
3 eta[77,3] 0.00852 0.000258 0.0538 0.000378 5.42e-7 0.0247 77 3 NELSON… 2
4 eta[77,4] 0.00174 0.0000202 0.0148 0.0000298 5.89e-9 0.00452 77 4 NELSON… 2
5 eta[77,5] 0.00148 0.00000853 0.0234 0.0000126 3.26e-9 0.00267 77 5 NELSON… 2
結果は一緒ですね!
RからAPIを叩く
では、RからAPIを叩きましょう。うるさいかもしれませんが、今の状況を繰り返し説明すると、私の手元のPCのRが、仮想PCという別のPCに存在しているRのAPIに対して、リクエストを出します。なので、これが成功したら、この仮想PCの構築方法(Dockerfileなど)をクラウドにアップロードすれば、リンクを変えるだけで世界中の人からのアクセスを受け付けることが可能になります。
早速確認しましょう。APIを叩くためのURLはSwaggerのRequest URLから確認できます:
> httr::GET("http://127.0.0.1:8000/cos_sim?senator=OBAMA%20%28D%20IL%29&top_num=3") |> httr::content()
[[1]]
[[1]]$senator_2
[1] "OBAMA (D IL)"
[[1]]$mean
[1] 1
[[1]]$q5
[1] 1
[[1]]$q95
[1] 1
[[2]]
[[2]]$senator_2
[1] "MIKULSKI (D MD)"
[[2]]$mean
[1] 0.9928
[[2]]$q5
[1] 0.9868
[[2]]$q95
[1] 0.9971
[[3]]
[[3]]$senator_2
[1] "DODD (D CT)"
[[3]]$mean
[1] 0.992
[[3]]$q5
[1] 0.9853
[[3]]$q95
[1] 0.9969
問題なく結果を再現しました!
結論
このように、Dockerとplumberを組み合わせれば、あなたの研究結果をインターネットの世界の共通言語で幅広いユーザーに提供できます。これで、モデルから得られた示唆がより多くの人に理解され、より大きいインパクトを発揮できると思います。
ぜひDockerとplumberを活用してください!