粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

ベイズの定理を用いてカルマンゲインを導出する(行列版)

Background

カルマンフィルタにはたくさんの導出がありますが, 自分的には大まかに以下の2つのパターンを把握していれば実用上は十分だと考えています。

(なお超初心者の場合はとりあえずコードを書いて動かして見た方が理解しやすいと思います。)

  1. 制御工学のオブザーバから入る方法
  2. ベイズの定理(ベイズフィルタ)から導く方法

オブザーバから導出するパターン

前者の例として,足立先生の「カルマンフィルタの基礎」をおすすめします。

カルマンフィルタの基礎

カルマンフィルタの基礎

ネタバレすると,証明は線形の(濾波型)オブザーバを設計し,ガウス分布で表される誤差が線形に伝搬する性質を用いて推定後の共分散を最小化するオブザーバゲインKを求める(平方完成)となっています。

似たアプローチとして超平面に垂線をおろす図形的なアプローチが合った気がしますが忘れてしまいました… 思い出したら書きます。

ベイズの定理から導くパターン

こちらは「確率ロボティクス」の書籍を見ると詳しくわかると思います。

以下のベイズの定理と,


p(x|z) = \dfrac{p(z|x)p(x)}{p(z)}

ガウス分布


f(x)=\frac{1}{(\sqrt{2 \pi})^{n} \sqrt{|\Sigma|}} \exp \left(-\frac{1}{2}(x-\mu)^{\mathrm{T}} \Sigma^{-1}(x-\mu)\right)

の式から導出できます。

ssk0109.hatenablog.com

ベイズの定理から多変数カルマンフィルタのゲインを導出

確率ロボティクスに倣って導出します。 基本的な流れは先程のブログとほぼ同じです。書きやすいように表記を少しいじっていますが,恐らく意味を大きく損なわないと思います。

前提

とりあえずよく見る線形状態方程式を仮定します。


x_{n+1}=Ax_{n}+Bu_{n}+\omega \\
y_{n} = Cx_{n}+\nu

 \omega,\nuはそれぞれシステムノイズと観測ノイズでその分散は, R,Qとします。

予測

予測にはベイズの定理を使いません。

nステップでの推定値の分布の平均を \mu_n 分散 P_nとすると, 予測値の分布の平均 \hat \mu_nと分散 \hat P_nは,


\hat P_n = P_{n|n-1} = A P_{n-1} A^\top + R\\ 
\hat \mu_n = \mu_{n|n-1} = A \mu_{n-1} + B u_n

となります。

計測更新

ベイズの定理から予測における推定値 \hat xとセンサ出力 yの分布からセンサ情報を考慮したときの推定値の分布 p(x|y)がわかります。


p(x|y) = \dfrac{p(y|\hat x)p(\hat x)}{p(y)} = \eta p(y|\hat x)p(\hat x)



ここで, \hat xは平均 \hat \mu_nと分散 \hat P_nガウス分布なので,


p(\hat x)=\lambda_1 \exp \left(-\frac{1}{2}(x_n-\hat \mu_n)^\top \hat P_n^{-1}(x_n-\hat \mu_n)\right)

また,観測の分布は


p(y|\hat x)=\lambda_2 \exp \left(-\frac{1}{2}(C x_n- z_t)^\top Q^{-1}(C x_n- z_t)\right)

と表せます。



したがって,ベイズの定理を用いてこれらをかけ合わせた場合の推定値の確率分布は


p(x|y) = \eta \lambda_1 \lambda_2 \exp \left(-\frac{1}{2}(C x_n- y_n)^\top Q^{-1}(C x_n- y_n) - \frac{1}{2}(x_n-\hat \mu_n)^\top \hat P_n^{-1}(x_n-\hat \mu_n) \right)

となります。

このexpの肩が極小値になる時がガウス分布の極大,すなわち分布の平均値,つまり最尤値になるので,


J = \frac{1}{2} \left( (C x_n- y_n)^\top Q^{-1}(C x_n- y_n) + (x_n-\hat \mu_n)^\top \hat P_n^{-1}(x_n-\hat \mu_n) \right)

が極大となる x_nを求めます。


一回微分して=0の条件から,


\dfrac{\partial J}{\partial x_n} \times 2 = C^\top (Q^{-1}+ Q^{-1} {}^\top) (C x_n- y_n) + (\hat P_n^{-1}+ \hat P_n^{-1} {}^\top ) (x_n - \hat \mu_n) = 0

よって共分散行列は対称行列より


C^\top Q^{-1}(C x_n- y_n) + \hat P_n^{-1}(x_n-\hat \mu_n) = 0

したがって推定値の平均は,


x_n = (C^\top Q^{-1}C +\hat P_n^{-1})^{-1} (C^\top Q^{-1}y_n+\hat P_n^{-1}\hat \mu_n)



また,分散はJの二階微分から明らかで,


P_n =  C^\top Q^{-1}C +\hat P_n^{-1}

となります。

式変形によるカルマンゲインの導出

先程の展開からカルマンゲイン K


x_n = \hat x_n + K (y_n - C  \hat x_n)

を満たすので,係数比較から以下のように導けます。


K =  (C^\top Q^{-1}C +\hat P_n^{-1})^{-1} C^\top Q^{-1}



見慣れた形と違いますね?この式に右から (C\hat P_n C^\top +Q)(C\hat P_n C^\top +Q)^{-1}をかけて整理することでよく見るカルマンゲインの式が得られます。


K =  (C^\top Q^{-1}C +\hat P_n^{-1})^{-1} C^\top Q^{-1}(C\hat P_n C^\top +Q)(C\hat P_n C^\top +Q)^{-1} \\
= (C^\top Q^{-1}C +\hat P_n^{-1})^{-1}(C^\top Q^{-1}C\hat P_n C^\top + C^top) (C\hat P_n C^\top +Q)^{-1}\\
= (C^\top Q^{-1}C +\hat P_n^{-1})^{-1}(C^\top Q^{-1}C + \hat P_n^{-1}) \hat P_n C^\top(C\hat P_n C^\top +Q)^{-1}\\
= \hat P_n C^\top(C\hat P_n C^\top +Q)^{-1}

長くなりましたが,これが確率ロボティクスの教科書に倣ったベイズの定理からのカルマンゲインの導出になります。

教科書のほうが丁寧なのでそちらを参照していただければと思いますが,研修中や電車の中など時間のあるときに自分で1から計算することで理解がより深まるのではないかと思います。

余談

はてなTex記法

はてなブログTex記法を書こうとすると通常の書き方から大きく変えることが多かったため,回避策として以下のブログを参考にしました。

7shi.hateblo.jp

親知らずなど抜歯後にも噛まずに食べられるもののおすすめリスト

先週末に下の埋没した親知らずを手術によって取り出しました。 はっきりいって人生1,2を争う痛さです。フガフガ。


親知らず抜歯後の食事

親知らずの抜歯,痛いですよね。

www.oho-dent.com

親知らず抜歯後の食事を妨げる要因として以下の2つが考えられます。

  • 顎の痛みがひどくて噛んだり飲んだりが難しい
  • 出血によって食べ物が血の味がする

無論,抜いた歯の状況(上顎か下顎か,埋没しているかどうか)によって抜歯後の状況は変わってきます。 一応参考までに載せます。

上顎・非埋没歯

上顎の非埋没歯の抜歯はあまり痛みを感じず,すぐに抜くことができます。

痛みも一時的で出血も1日程度で止むため2,3日すれば通常復帰できます。

私は抜いた日の晩にはラーメンを食べてました。


下顎・埋没歯

下顎は太い神経が近いため痛みがひどくなりやすく,埋没してる場合は切開なども入るので最悪です。3日間は顔がパンパンに腫れ,痛み止めとともに暮らしました。


母曰く出産の6割程度の痛みだそうです。お母さんたちすご…


ここで最大の問題となるのは抜歯後には痛みなどで噛んで食事をとることができないということです。

特に私の場合,縫合した部分が上の歯に引っかかって少なくとも抜糸までの14日間噛めない生活が続きそうです。


流動食できちんと栄養をとるのは大変!

本来の流動食の定義と少しずれますが,ここでは噛まずに食べれるものを流動食と呼称します。

流動食生活7日目を終えた感想としては,きちんとカロリーを摂取するのがとても大変!ということです。

1日に必要なカロリー・栄養分

1日に最低限必要なカロリーを「代謝で使われる分」だと仮定します。

以下のサイトで説明されていますが標準体型の男性では1500kcal前後のようです。 女性や小柄な方は1200kcalでも良さそうですが,とにかくそれくらい必要なのです。

e-keisan.com

例えばウィダーゼリーやカロリーメイトのゼリー飲料などは100-200kcal程度しかありません。 1日に7~8個飲む必要がありますが,体感なかなかそこまでの量を飲めないと思います。

ゼリーだけで食事をするのは栄養価意外の面でも少しキツイです。(ただ,飲み物を飲んで200kcal取れるのは破格です。)

大塚製薬 カロリーメイト ゼリー アップル味 215g×24袋

大塚製薬 カロリーメイト ゼリー アップル味 215g×24袋

  • 発売日: 2016/05/23
  • メディア: 食品&飲料

なお,筆者は決して少食ではなく二郎系ラーメンが好物でヤサイマシマシを食える程度には胃の容量がある方で,単に食事を全てゼリー飲料に代替するのは結構辛いということだと思います。


流動食 Level1:完全な液体

  • ○:術後すぐに飲める
  • ○:飲みやすい
  • △:味が薄いと血がにじむ
  • ✖:カロリーが取りづらい

牛乳,飲むヨーグルト

最初に手を出したのが牛乳や飲むヨーグルトです。 どちらも,カロリーは60kcal/100ml 程度です。

  • 飲むヨーグルトは甘いので牛乳で割ってラッシー風にすると飲みやすい
  • 牛乳はプレーンなのでミロやココアなど味変がしやすい
  • 味が濃く,出血時も血の味を感じにくい
  • 牛乳だけで最低限の栄養をとろうとすると2L飲まなければ行けない

飲みやすいですがカロリーをとるのが大変です。 この他にもプロテイン飲料や豆乳もこのカテゴリに入ります。

コーンスープ(うらごし)

塩気に飢えていたときに飲んだので非常に美味しかったと印象に残っています。

カップスープや味噌汁も飲みましたが,具材が極力ないものを選ぶ必要があったり飲んでもたいしてカロリーを稼げないものが多かった中でコーンスープは重宝しました。

  • 液体系でカロリーがそこそこある(85kcal/dL)
  • 飲みやすく食事感もある
  • 一週間くらい飲み続けていると流石に飽きる

これは主戦力足り得るので常備しました。

栄養ドリンク・野菜ジュースなど

  • ビタミンなどを摂取するのには良い
  • 甘すぎるのでたくさん飲めない場合が多い
  • 味が濃いので出血してても美味しく飲める
  • 患部にしみる場合があり注意が必要

ジューサーがある人は生搾りのジュースを作っても良いと思います。

これも貴重な栄養源なので常備しました。

カロリーメイト ドリンク

常備その3。

  • 量あたりのカロリーが高く飲みやすい(牛乳の約二倍の効率)
  • ちょっと高い

朝食やちょっとしたエネルギー不足を補うにはもってこいでした。やカ神。


流動食 Level2:ゼリー状の食事

  • ○:食べやすく,一部食べごたえもある
  • ○:栄養価もそこそこ
  • ✖:高いことが多い

ゼリー飲料・ゼリー

ゼリー飲料は飲みやすく,カロリーもそれなりに取れるので最初の方はよくのんでました。

しかし想像以上にゼリーというのは飲みづらいものです。 牛乳を1L飲むのはそう苦ではないですが,ゼリーを1L飲むのはまぁまぁ大変です。

大塚製薬 カロリーメイト ゼリー アップル味 215g×24袋

大塚製薬 カロリーメイト ゼリー アップル味 215g×24袋

  • 発売日: 2016/05/23
  • メディア: 食品&飲料

  • 簡単に食べれる
  • ちょっと高め
  • たくさん食べるのに向いていない

豆腐料理

食べやすさ,味付けの多彩さ,食べごたえ全て◎の無敵超人,それが豆腐料理です。

私が食べてたスンドゥブは200kcal前後ですが,そこに生卵を割り入れることで栄養的にも幾分マシなものができます。

S&B 菜館 スンドゥブチゲの素 辛口 300g×5個

S&B 菜館 スンドゥブチゲの素 辛口 300g×5個

  • 発売日: 2015/08/03
  • メディア: 食品&飲料

  • 食べやすさ,味付けの多彩さ,食べごたえ全て◎
  • これに頼りすぎるかもしれない

甘酒

実家では風邪ひきの時に甘酒に卵を入れて煮込んだものをよく食べてました。 安い缶のやつではダメで,でろっとした濃いものを使ってください。うまいです。高いけど。

  • うまい
  • 高い


流動食? Level3:食事を刻む

  • 汁物の食事などをぐでぐでにするか,刻むなどして食べる

この段階まで来ると流動食?って感じですが食べた中ではワンタンが食べやすかったのでおすすめです。

イメージ画像がないので以下載せますが私が食べたのはチルドのワンタンです。

ワンタン しょうゆ味 32g×12個

ワンタン しょうゆ味 32g×12個

  • メディア: 食品&飲料

まとめ

手間を掛けないなら牛乳系と豆腐料理で凌ぐのが楽そうです。

スムージーやミキサーがあればもうちょっと幅が出て良いかもしれません。

短期決戦における1日の流動食メニュー例

現時点では以下のように落ち着きつつあります。

  • 牛乳系:速攻で飲める優秀なやつ
  • 野菜ジュース系:ビタミン担当
  • スンドゥブ+生卵:栄養補給
  • カロリーメイト(ジュース):足りないカロリーを調整

体調について

1日の摂取カロリーが少ないのでなんかやたら眠くなったり,一気に3キロ位痩せたりしました。俺の筋肉を返せ 多分致命的には悪くなってないはず。

ダイエットにも効果的でしょうが身体を壊しては元も子もないので気をつけて。

https://www.buzzfeed.com/jp/carolinekee/liquid-diets-cleanses-help-lose-weight-1

余談:ディスカバリーチャンネル

関係ないけど飢えといえばエドスタフォードのサバイバルを思い出します。 面白いので是非見てください。

youtu.be

番外編:失敗

お粥など

風邪ひきの定番といえばお粥です。 なんどかチャレンジしましたが,

  • 食えることは食える
  • ✖:血の味がつきやすくそうなると非常にまずい

ということで以後作るのはやめました。

ちねりうどん?

もう少しマシな選択肢として,うどんを細かくしたものをグデグデになるまで煮込むというのがあります。

  • めんつゆなどの濃い味付けで血の味が気になりにくい
  • それでもあんまり量は食べられなかった(~4日目の感想)

8日目とかならもっとガンガン食べれるのかな?

焼き鳥(しかも鶏皮)

途中で油分のカロリーが高いことに気づいて買いましたが,ただでさえ噛むのが大変な鶏皮をどうして噛まずに食えると思ったのか…

判断力の低下には気をつけなくてはいけません。もったいないので包丁で微塵にして飲みました。 ミキサーがあれば手っ取り早くカロリーを摂取できるかもしれませんね。

味は保証しませんが。

ポテチ

ポテチもカロリー面につられて買いました。 堅揚げポテトなんかは一袋500kcalを越す夢のカロリーダイナマイトなので粉状にして摂取するのを検討しましたが口の中をパサパサするので食べにくさもダイナマイト級で断念しました。

唯一味はポテチだったので味だけ見ればかなり良い体験でした。飲み込むまでが食事なんですけどね。

vscode上でマークダウンを使ってスライドを作る。(Marp, Reveal.js)

スライドをいちいちGUIで作るのはだるい,時々よくあることだと思います。

Markdown形式のメモをそのままスライドにする(実際はちょっといじるんだけど)手法について個人的な設定をメモ・紹介します。

Abstract

  • マークダウン形式で簡単に発表資料を作る。
  • 2020年5月現在,Marpがオススメできる。

本記事における環境

ossyaritoori.hatenablog.com

手法紹介・総評・雑感

Markdownでスライドのようなものを作成する手法にはいくつかあります。 例えば以下の記事でもいくつか紹介されてますね。

qiita.com

今回は手元のvscode上で書いてその場でPreviewしたいので以下のものを試しました。

  • Reveal.js:動きがかっこ良く,発表向け。スライドはカスタマイズしないと大味な感じ。
  • Marp:静的なスライド作成。設定がデフォルトでもそこそこ見やすい。

f:id:ossyaritoori:20200520121123g:plain
VScode上でPreviewしたReveal.jsのスライド例。動きの制御も記述できる上に,発表者用ツールも作成可能。

f:id:ossyaritoori:20200520121329p:plain
Marpで作成したスライドのPreview。ほぼデフォルトのものを使用。比率やスライドのカラーリング,図の配置など比較的設定しやすい。

ということでReveal.jsよりも今回はMarpを重点において紹介します。

Marp書き方

Marpの基礎的な文法について述べます。

まずはvscode-marp拡張機能からインストールしてきます。

スライドを構成する最小要素は

---
marp: true
---

# ページ1
`---`でスライド追加

---

# ページ2

です。ここから設定を追加していきます。

現時点での個人設定例

  • themeをgaiaに
  • headerとfooterを追加
  • 16:9に
  • ページナンバーをつける
  • 図を中央に表示
---
marp: true
theme: gaia
header: ''
footer: '@ossyaritoori'

size: 16:9
page_number: true
paginate: true
---

<!-- コメント:画像を中央に配置する centerコマンドを有効に -->
<style>
img[alt~="center"] {
  display: block;
  margin: 0 auto;
}
</style>

一部,個別に見ていきましょう。

スライドのテーマ

デフォルトではスライドのテーマは以下の3つのようです。

  • default: 白色中央寄せ
  • gaia: クリーム色タイトル上部固定(一番使いやすい)
  • uncover: 書式なし

また,サブテーマというものがあり,各ページごとに設定できます。 gaiaの場合はデフォルト含めて4種類あります。

f:id:ossyaritoori:20200520124159p:plain
gaiaのサブテーマ。上からinvert,gaia,lead。

書き方は以下の通り。全体をいじるか,1スライド毎に設定もできます。

<!-- 全体を色反転 -->
<!-- class : invert -->


<!-- 1スライドを色反転 -->
<!-- _class : invert -->

個人的に使えると思ったのは,タイトルを色反転かつ中心揃えにすることで,以下のように書けます。

<!-- タイトル用書式:色反転+中央寄せ -->
<!-- _class : lead invert-->

ページ番号

古い記述ではpage_numberを用いていますが,2020年5月のv0.13.0では, paginate: trueのみでページ番号をFooterの位置に出現させられます。

全体のページ数をカウントして表示するのも欲しいですね。

図を中央に配置する

  • デフォルトでは図は左に配置されます。
  • 改行なしで並べると図は下面を揃えて左右に並びます。

テンプレにある書き方をすることで図を左右中央揃えに制御できます。上下も中央揃えにしたいですね。

図の貼り付け・サイズ調整

図はMarkdown,htmlの両方の記法が使えます。

縦・横を指定して画像を貼る

他の文献では![倍率%](画像url)で画像のサイズ調整ができたようですが,手元のv0.13.0ではまだできません。 代わりに,![w:300](画像URL)と書いています。

元のサイズとの比を見るより横幅などで指定した方がよく見えます(縦指定でも可)。

背景として画像を貼る

また,![bg w:300](画像URL)のようにbgを入れることにより背景画像としても指定できます。 これはデフォルトで中央揃えです。

f:id:ossyaritoori:20200529180349p:plain
画面を左右に分割したときのレイアウト。

また背景と文章を分割することも出来,以下のように書きます。

# 背景 右50%領域に
![bg right:50% cover](sky.jpg)
  • cover(default): 縦か横に合わせる
  • contain(fit):はみ出ないように合わせる
  • auto:オリジナルサイズで貼る

おそらくよくつかうのはcoverかcontainだと思います。

数式

数式はKatex形式をサポートしているようです。 今の所特に語ることはないですが,項目として残しておきます。

サンプルスライド(自分テンプレート)

---
marp: true
theme: gaia
header: 'header text'
footer: '@ossyaritoori'

size: 16:9
paginate: true
---


<!-- コメント:画像を中央に配置する centerコマンドを有効に -->
<style>
img[alt~="center"] {
  display: block;
  margin: 0 auto;
}
</style>

<!-- タイトル用書式:色反転+中央寄せ -->
<!-- _class : lead invert-->


# タイトルスライド

- 日付:名前
- 概要:
    - Issue1
    - Issue2

--- 

## 普通のスライド
- Problem1
- Answer1

こうなります。

f:id:ossyaritoori:20200521191618p:plain
普通に使えそう。使います。

Marpの参考資料

  • 基礎的な文法(vscode版に準拠しておらず,動かないコードもある)

qiita.com

  • Advancedな内容

www.slideshare.net

  • 自分でMarpのThemeをカスタマイズする

blog.okazuki.jp

  • Marpの開発に関わる話

tech.speee.jp

余談:GIF画像をキャプチャするソフト

Reveal.jsの紹介に使った,スクリーンをGIFでキャプチャするソフトです。 結構便利。

www.screentogif.com

オンラインで友人とボードゲームが遊べるボードゲームアリーナをやってみた所感

ボードゲームアリーナとは

という自宅待機民にうってつけのサービスです。 一部人気ゲーム(カルカソンヌ等)は有料ですが,無料のものだけでも十分に楽しむことができると思います。

ボードゲームはセットアップや片付けが大変なので非常に便利です。

f:id:ossyaritoori:20200502205707p:plain
プレミアムでしか卓を建てられないゲーム一覧

同じ様な紹介記事を書いているかたがこちら。

thecongress.hatenablog.com

環境と登録方法

PC版はインストールも不要で非常に便利です。Google連携が楽かなと思います。

  • 動作環境:ブラウザ上で動く(スマホアプリがβリリース中)
  • GoogleFacebookアカウントと連携,もしくはメールアドレスで登録
  • usernameとパスワード決めておしまい!

boardgamearena.com

友達と遊ぶまでに必要なステップ

  1. 友人にアカウントを作ってもらう
  2. 「username」を検索してフレンド追加

でOKです。

ボードゲームアリーナを友達とプレイする方法! - せいたかのっぽの備忘録

(補足)画面共有をすれば1人分のアカウントでもできるかもしれませんが

「今すぐプレイ」から以下のように追加していきます。

f:id:ossyaritoori:20200502213910p:plain
画面上部にこの様なウィンドゥが現れます。

ゲーム画面に移行後,相手が「承認」してくれるのを待ちます。

ゲームを受ける人は,画面上に出てくるポップアップから承認をします。

友人と遊ぶ際の通話環境

プレミアム会員になるとビデオやチャットを使えますが,普通に外部のサービスで通話すればいいです。

使ったことがあるのは以下の通りです。

  • LINE:手軽,大体の人が持っている。
  • Skype:年代によっては結構な人が持っている。
  • Zoom:画面共有がルール解説に非常に便利。近年普及したので持っている人は多いかもしれない。
  • discord:音声通話の品質が高い。あまり使ってる人がいない?

2人ゲーム初プレイ所感

ここから本題ですがいくつかのゲームを2人でやったのでその感想についてレビューしていきます。

なお,筆者のボードゲーム経験は中程度(~30種類程度)である程度ボードゲーム慣れしている側の意見です。

  • 運要素がある程度強いゲームの方が盛り上がりやすい印象
  • ガチガチの戦略ゲームはよほど雑談ができる人じゃないと厳しいかも?

こいこい

  • プレイ時間:30分~(ルールを熟知してるなら半分以下でできる)
  • 難易度:☆

花札のプレイ方法の一種。ルールは任天堂準拠らしいです。

www.nintendo.co.jp

  • 動きと基本的な役を覚えれば良いので簡単です。
  • ただ,ゲームとしては単調寄り。こいこいの駆け引きで頑張って盛り上げてください。

タルギ

  • プレイ時間:60分~
  • 難易度:☆☆☆

商人になって「資源」を集めつつ,得点になる「部族カード」を購入していくゲーム。

f:id:ossyaritoori:20200502224712p:plain
タルギのプレイ画面。中央の3*3のサプライから資源や勝利点を入手していき点数を競うゲーム。

  • ボードゲームによくある設定なので,中級者的にはプレイの難易度は比較的低い
  • 各カードの効果や最終得点のボーナス点など勝敗を左右する要素が非常に多く,最適な立ち回りを考え出すと結構難しくなる
  • どのカードがサプライに置かれるか,山札をめくって入手するなどのランダム要素もある程度あり,やってて楽しかった

それはオレの牧場だ!(Battle Sheep)

  • プレイ時間:~20分
  • 難易度:☆

16匹いる羊を分割していって領地を広げる陣取りゲーム。

f:id:ossyaritoori:20200502233430p:plain

  • 囲碁っぽい。
  • 簡単だけど戦略性が高い。

クアルト

  • プレイ時間:~10分
  • 難易度:☆

相手にコマを選ばせる,複雑化した四目並べ

f:id:ossyaritoori:20200502233700p:plain
4×4のマスで同じ高さ,色,形,穴のある無しでビンゴを作らせないように駒を選ぶ

  • 色,形状などの複数の条件を気にする四目並べ
  • 簡単だけど戦略性が高いっぽい。

キャントストップ

  • プレイ時間:~20分
  • 難易度:☆

サイコロを降ってその和を使って複数のルートからゴールを目指すすごろく?的なゲーム。

f:id:ossyaritoori:20200503001741p:plain
キャントストップのプレイ画面,4つのサイコロから2組の和を選び,該当するマスのコマを進める。

  • 運要素や駆け引き要素が強く初心者でも盛り上がりやすい
  • サイコロの出目の確率について知っているとちょっと戦略的になる?

バトルフォーヒル

  • プレイ時間:20分~
  • 難易度:☆☆☆

兵隊カードと爆撃カードを駆使して相手の陣地へと攻め込むカードゲーム調。

  • 攻撃や補給などのルールが地味に分かりづらかった
  • どういう戦略が良さそうか初プレイではわからなかった

まとめ

プレイし次第追記します。

今の所は

  • キャントストップ

が手軽で運要素で盛り上がれそうなので初心者とやる分にはおすすめです。

どうやらwikiがあるのでここも参照したほうが良いかもしれません。

w.atwiki.jp

anacondaを使った仮想環境上のjupyterがmoduleをimportできない問題の解決 / jupyter上で環境を切り替える

起こった問題

  • condaで作った仮想環境上であるパッケージ(lightgbm)をインストールした
  • コマンドライン上で起動したpython環境では動く
  • がjupyter notebook上からはそのようなmoduleはないと言われた

要は入れたはずのライブラリがnotebookからインポートできないという状況になりました。

原因

  • 仮想環境上にはjupyterを入れてなかった
  • base環境のjupyterが起動し,仮想環境上でのライブラリ追加が

解決策

したがって簡単なことですが,これだけです。

  • 仮想環境上にjupyter notebookを入れる。
conda install jupyter notebook

余談ですが,juptyerのnotebook上でも戦闘に!をつけた行はコマンドライン上の入力として実行されるので,notebook上で

!pip install ~~

と実行しても良いです。

ただし,condaはyes/noの入力が必要になるので

!echo y|conda install ~~

とすること。

注意点:condaとpipをまぜるな

anacondaとpipでは扱うパッケージの形式が異なり,混ぜると環境を壊す恐れがあります。

https://cdn.juku.st/entry_images/5368/original/3c02421dae59735fbe1a9e2ee17958227702cc65.jpg

https://insilico-notebook.com/conda-pip-install/

jupyter上で仮想環境を切り替えられるようにする

バクを調べてる時に知ったのですが,jupyer上でKernel→Change Kernelの欄から仮想環境を切り替えることができて非常に便利です。

qiita.com

手順概略

手順は,

  1. base環境で,pip install environment_kernels
  2. jupyter notebook --generate-configで.jupyter以下に設定を作成(すでに出ている場合は変更なし)
  3. 生成されるjupyter_notebook_config.pyに下記を追記
c.NotebookApp.kernel_spec_manager_class = 'environment_kernels.EnvironmentKernelSpecManager'
c.EnvironmentKernelSpecManager.env_dirs=['C:/Program Files/Anaconda3/envs/'] # envsが保存されている場所

最後に使いたい環境にjupyterを入れpip install environment_kernelsを実行で完了です。

condaとpipをまぜるなといったそばからこれですが,environment_kernelsをcondaで入れている文献を見てないので仕方なくやってます。 best solutionでないのは確か。

ハマリぽいんと

3つ目の手順でenvsが保存されている場所というのが厄介で,windows機の場合,\エスケープする必要があります。

したがって,わたしの場合は

c.EnvironmentKernelSpecManager.conda_env_dirs = [ 'C:\\Users\\username\\AppData\\Local\\conda\\conda\\envs\\' ]

としなくてはいけませんでした。あと,最後に仮想環境上でのpip installを忘れないようにというのもポイントです。

これができると,環境ごとにコマンドプロンプトを立ち上げなくていいので気持ちが楽になります。

① ikawidget2のデータを使ってSplatoon2での編成を評価 ~1.データ取得と集計編~

はじめに

発売から2年半経っていますがSplatoon2を未だにやってます。ここまで長いことやったゲームは初めてでつまり神ゲーってことです。

今回はSplatoon2の編成強い弱い問題を最近遊んだ機械学習ライブラリで解き明かせないかという企画になります。(なお著者はCS専門ではないです。)

  • 自分の戦績データから,「勝敗に関連しやすそうな要素」を抽出する
  • 編成などから「勝敗予想」モデルを作成する

というのを目標に据えてやっていきます。

概要

  • スプラトゥーン2のプレイデータをもとに勝敗の予測モデルを作成
  • 予測モデルをもとにどの要素が勝敗に寄与したかを明らかにできる
  • 直感通りキルデスが最も影響が強いと出るがルールごとに差がでる

余談

  • ①とあるが、続編は忙しくて作らなかった
  • 勝敗の鍵になる因果関係を検証するには本手法はやや不向き
  • 一個一個の要素を可視化しつつ、細かな仮説を地道に検証したほうが良さそう

記事

①ではデータの読み出しとデータ構造の解析に焦点を当てます。とりあえずデータのとり方など技術的な話をするので興味ない方は飛ばして結構です。

背景(モチベーション)

Splatoon2では敵味方4対4に分かれて試合を行うのですが,その時のブキの編成が結果を非常に左右しやすいという印象を受けます。

例えば,チャージャー,ローラー,ブラスターなどを2枚引くと塗りや打開が厳しいというのは誰しもが思っていることでしょう。

感覚的に厳しい編成かどうかはわかるのですがうまいことこれをデータで示せないか,というのがモチベーションです。

自分の戦績はikawidget2というスマホアプリから取得したものを用います。

参考情報ですが,2020/4/28時のデータはこの様になってます。

  • ウデマエ:エリア・ホコ・ヤグラ XP2000~2200,アサリ S+ 2000前後
  • 持ちブキ:チャージャー・スピナー以外全般
  • 戦績データ数:エリア: 826戦ヤグラ:845戦ホコ:432戦アサリ:339戦

です。

ikawidget2の戦績データを抽出する

このブログを参考にまずはデータをPythonで読み出します

  1. バックアップを行う → 本体に ~~.ikaxという名前のファイルが保存されるのでそれをPC上にダウンロード。
  2. ~~.ikaxの拡張子を.zipに変更して中身を開く → stats.realmというファイルが出てくる。
  3. Realm StudioをDLしてstats.realmを開き,json形式で保存する。

json形式の戦績データを読む

以下のようにして読み出します。

import json

with open("../data/stats.json","r",encoding="utf-8") as f:
    result_json=json.load(f)

試合結果はresult_json["Results"]にリストの形式で入っています。

試合の情報全てがこのResultsに入っており,

  • ガチパワー,ステージ等の情報
  • プレーヤーのギア・ブキや戦績情報
  • 勝敗

が辞書型で記録されています。

試しにkeysを見てみると,

result_json.keys()
>> dict_keys(['Boss', 'BossCount', 'Brand', 'CoopResult', 'CoopSchedule', 'CoopStage', 'CoopWeapon', 'EventType', 'Fes', 'Game', 'Gear', 'Grade', 'Player', 'PlayerType', 'Result', 'Skill', 'SkillLog', 'Skills', 'Special', 'Stage', 'SubWeapon', 'WaterLevel', 'WaveDetail', 'Weapon', 'Worker'])

となります。主に使う戦績データはResultに入ってます。

ステージ毎の勝率計算

参考ブログでは,戦績からfor文を用いてガチエリアの戦績を集計しています。 試しに苦手なガチヤグラ'game': `を集計してみました。


補足:

  • ルール情報は'game'にある。ヤグラはtower_controlgachi,エリアはsplat_zonesgachietc...
  • ブキ・ギア・ステージ等は番号で管理されており,IDと名前の対応はWeapon・Gear・Stageのkeys下に保管されている
yagura_result={}

for item in result_json["Result"]:
    if item["udemae"]<10 and item["game"]!="tower_controlgachi": #S以上の戦績に限定&ガチヤグラ以外のルールを排除
        continue
    for i in result_json["Stage"]:
        if i["ID"]==item["stage"]:
            stage_name=i["name"]

    # 勝ちと負けをステージごとにカウント
    if not stage_name in yagura_result.keys():
        yagura_result[stage_name]={"win":0,"lose":0}
    if item["win"]:
        yagura_result[stage_name]["win"]+=1
    else:
        yagura_result[stage_name]["lose"]+=1

# 勝率高い順にソート        
for key in sorted(yagura_result.keys(),key = lambda x:yagura_result[x]["win"]/(yagura_result[x]["win"] +yagura_result[x]["lose"]),reverse=True):
    print(key,yagura_result[key]["win"]/(yagura_result[key]["win"] +yagura_result[key]["lose"]))

結果は

デボン海洋博物館 0.6153846153846154
(中略)
モズク農園 0.41346153846153844

となりました。この時点での情報はikawidgetでも見れますね。

プレーヤー情報の取得

ここから勝ち負けを評価するためにプレーヤーの情報を抽出します。

プレーヤー情報はResults以下の myMembers(味方),otherMember(敵),player(自分自身)の3つのkeyに保存されています。使いそうなのは

  • kii / allKill: キル数 / アシスト込のキル
  • assist: アシスト数
  • death: デス数
  • paintpoint: 塗りポイント
  • special: スペシャル回数
  • weapon: ブキ

くらいでしょうか。

偽相関が出そうですが,以下の情報も使えそうです。

  • elapsed time:試合時間
  • gachiEstimatePower / gachiEstimateXPower :ガチパワー / Xパワー (Player以下のudemaeIsXの変数フラグにより切り替え)

味方ブキ毎の勝率計算

例としてステージ毎の集計と似たような感じで味方のブキ毎の勝率を計算することができます。

#エリアで勝ったときの味方のブキを集める
win_weapon_ally = {}

# プレイヤー情報の入った辞書データから勝敗を抽出する関数
def addwin(win_weapon,result_json,item,win):
    w_name = "null"
    for i in result_json["Weapon"]:
        if i["ID"]==item:
            w_name=i["name"]
    if not w_name in win_weapon.keys():
        win_weapon[w_name]={"win":0,"lose":0}
    if win:
        win_weapon[w_name]["win"] +=1
    else:
        win_weapon[w_name]["lose"] +=1

# 試合結果から結果を抜き出すループ
for item in result_json["Result"]:
    #if item["udemae"]<10 and item["game"]!="tower_controlgachi": #S以上の戦績に限定&ガチヤグラ以外のルールを排除
    if item["udemae"]<10 and item["game"]!="splat_zonesgachi": #S以上の戦績に限定&ガチヤグラ以外のルールを排除
        continue
    for ally in item["myMembers"]:
        if item["win"]:
            addwin(win_weapon_ally,result_json,ally["weapon"],1)
        else:
            addwin(win_weapon_ally,result_json,ally["weapon"],0)

結果がこれ。(長文注意)

出会ったことの少ないマイナーなブキが上位に来てますが50戦以上データがあるなかで勝率が良かったのはなんとボールドマーカー7(約62%)でした。 最下位はソイチューバー2種。かわいそう...

H3リールガンチェリー 1.0 2
ヒーロースピナー レプリカ 0.8888888888888888 9
14式竹筒銃・丙 0.8181818181818182 11
スプラローラーコラボ 0.7619047619047619 21
スパイガジェット 0.7368421052631579 19
スパイガジェットベッチュー 0.7368421052631579 19
スクリュースロッシャーネオ 0.7 10
プロモデラーRG 0.6666666666666666 12
スプラスコープベッチュー 0.6666666666666666 21
ノーチラス79 0.6666666666666666 15
ノヴァブラスターネオ 0.6666666666666666 3
ロングブラスターカスタム 0.6666666666666666 3
スプラマニューバーベッチュー 0.6410256410256411 39
ホクサイ 0.6363636363636364 22
ホットブラスターカスタム 0.6285714285714286 35
L3リールガンベッチュー 0.625 8
リッター4Kカスタム 0.625 8
ジェットスイーパーカスタム 0.6222222222222222 45
ボールドマーカー7 0.6197183098591549 71
ホクサイベッチュー 0.6176470588235294 34
カーボンローラー 0.6153846153846154 13
プライムシューター 0.6111111111111112 36
スプラマニューバー 0.6086956521739131 23
プロモデラーPG 0.6071428571428571 28
.52ガロン 0.6 10
ヴァリアブルローラー 0.6 5
パーマネント・パブロ 0.6 10
クラッシュブラスターネオ 0.5897435897435898 78
ヒッセン・ヒュー 0.5882352941176471 51
ジェットスイーパー 0.5882352941176471 68
クーゲルシュライバー 0.5882352941176471 17
ケルビン525デコ 0.5869565217391305 46
ヒッセン 0.5853658536585366 41
ヒーローローラー レプリカ 0.5842696629213483 89
ヒーローチャージャー レプリカ 0.5833333333333334 60
ヒーローブラシ レプリカ 0.5833333333333334 12
バケットスロッシャーソーダ 0.5833333333333334 24
ダイナモローラーベッチュー 0.5789473684210527 133
ボールドマーカーネオ 0.5740740740740741 54
ノヴァブラスター 0.5737704918032787 61
スプラスコープコラボ 0.5714285714285714 28
Rブラスターエリートデコ 0.5714285714285714 7
デュアルスイーパー 0.5645161290322581 62
L3リールガン 0.5625 32
スパッタリークリア 0.5609756097560976 123
スプラローラー 0.5598290598290598 234
.96ガロンデコ 0.5555555555555556 36
.96ガロン 0.5555555555555556 27
バレルスピナーデコ 0.5555555555555556 27
ケルビン525ベッチュー 0.5555555555555556 9
ロングブラスター 0.55 60
ボトルガイザーフォイル 0.55 40
プライムシューターコラボ 0.5454545454545454 44
ヒーローマニューバー レプリカ 0.5454545454545454 11
ハイドラントカスタム 0.5443786982248521 169
オーバーフロッシャーデコ 0.5384615384615384 39
H3リールガンD 0.5384615384615384 26
スプラスピナーベッチュー 0.5384615384615384 13
リッター4K 0.5368421052631579 95
オクタシューター レプリカ 0.5344827586206896 58
もみじシューター 0.5333333333333333 75
デュアルスイーパーカスタム 0.5329512893982808 349
オーバーフロッシャー 0.5307692307692308 130
スプラスピナー 0.5277777777777778 36
パラシェルターソレーラ 0.5263157894736842 19
4Kスコープ 0.5263157894736842 95
14式竹筒銃・甲 0.5254237288135594 59
スクイックリンα 0.5217391304347826 23
バレルスピナーリミックス 0.5190839694656488 131
スプラシューターコラボ 0.5181818181818182 220
N-ZAP89 0.5163398692810458 153
プライムシューターベッチュー 0.5150684931506849 365
ボールドマーカー 0.5142857142857142 140
クアッドホッパーブラック 0.5135135135135135 74
ホクサイ・ヒュー 0.5128205128205128 39
N-ZAP85 0.5114285714285715 350
スプラシューターベッチュー 0.5032258064516129 155
わかばシューター 0.5023041474654378 217
ラピッドブラスターベッチュー 0.5 70
ノーチラス47 0.5 24
ハイドラント 0.5 20
スパイガジェットソレーラ 0.5 6
ヴァリアブルローラーフォイル 0.5 48
バケットスロッシャー 0.5 8
スクリュースロッシャー 0.5 6
ヒーロースロッシャー レプリカ 0.5 2
ラピッドブラスターデコ 0.5 14
ケルビン525 0.5 6
スプラスピナーコラボ 0.5 10
シャープマーカーネオ 0.49382716049382713 81
パブロ 0.49019607843137253 51
キャンピングシェルターカーモ 0.4864864864864865 37
L3リールガンD 0.4827586206896552 58
スプラチャージャー 0.4803921568627451 102
クーゲルシュライバー・ヒュー 0.47706422018348627 109
エクスプロッシャーカスタム 0.47297297297297297 74
スパッタリー・ヒュー 0.4722222222222222 72
スプラシューター 0.4714285714285714 70
ラピッドブラスター 0.46153846153846156 13
Rブラスターエリート 0.46153846153846156 13
スプラマニューバーコラボ 0.4578313253012048 83
スプラチャージャーコラボ 0.45454545454545453 44
クラッシュブラスター 0.45454545454545453 22
ヒーローブラスター レプリカ 0.45454545454545453 11
シャープマーカー 0.45454545454545453 33
スクリュースロッシャーベッチュー 0.45098039215686275 102
ロングブラスターネクロ 0.45098039215686275 51
.52ガロンベッチュー 0.4482758620689655 29
おちばシューター 0.4444444444444444 63
スプラローラーベッチュー 0.44 25
カーボンローラーデコ 0.44 50
ダイナモローラーテスラ 0.4318181818181818 44
.52ガロンデコ 0.42857142857142855 7
スクイックリンβ 0.42857142857142855 21
スプラチャージャーベッチュー 0.42857142857142855 35
クアッドホッパーホワイト 0.42857142857142855 7
ダイナモローラー 0.42857142857142855 14
パブロ・ヒュー 0.425 40
ホットブラスター 0.4166666666666667 24
スプラスコープ 0.4084507042253521 71
H3リールガン 0.4 5
4Kスコープカスタム 0.4 5
スパッタリー 0.39285714285714285 28
ヒーローシューター レプリカ 0.38461538461538464 13
プロモデラーMG 0.3793103448275862 29
パラシェルター 0.375 24
スクイックリンγ 0.375 16
N-ZAP83 0.36 25
ノヴァブラスターベッチュー 0.34782608695652173 46
エクスプロッシャー 0.3333333333333333 36
バケットスロッシャーデコ 0.3333333333333333 21
ボトルガイザー 0.3333333333333333 3
14式竹筒銃・乙 0.3333333333333333 9
バレルスピナー 0.29411764705882354 17
キャンピングシェルターソレーラ 0.2857142857142857 7
ヒーローシェルター レプリカ 0.2 10
キャンピングシェルター 0.14285714285714285 7
ソイチューバー 0.14285714285714285 7
ソイチューバーカスタム 0.0 2

この結果は私の持ちブキや立ち回りとの相性という話なので一般的ではないですが,傾向を見る上では参考になりそうです。

より大規模にはika.statsというサイトがあります。

結構ちゃんと分析されてるようなので私の出る幕があるかどうかは微妙です。まぁしばらくは車輪を再発明していきましょう。

github.com

解析に使うデータを整形する

今回の生データはjsonで記述されていますが,これは今後の解析を考える上ではあまり使いやすいものとは言えません。

Kaggleで遊んだときのようなcsvデータのような形式で試合結果を保存するのが良いでしょう。

ossyaritoori.hatenablog.com

行列形式で1行に試合結果を入れるとすると,ひとまず列に必要そうな要素としては

  • ステージ
  • 自分のブキ
  • 自分のキル・デス
  • 自チームの合計キル・デス , 相手チームの合計キル・デス
  • 自チームの合計SP回数, 相手チームの合計SP回数
  • 自チームの合計塗りポイント,相手チームの合計塗りポイント(以後”相手~”の部分省略)
  • 自チームのブキごとの編成数, 相手~
  • 勝ち負け

あたりでしょうか。 Resultsからこれを抽出する関数を書きましょう。

使用するのはPandasのDataFrameとします。

結果からスコアを抜き出すテスト

試しにいくつかの要素をピックアップしてpandasのdataframeに変換してみましょう。ブキの分類はちょっと考察がいるので後回しにすることにします。

import pandas as pd
import numpy as np

# 一時保存用
dfarray = []

# For ループを回してデータをArray状にする
for item in result_json["Result"]:
    # 1. game information
    game = item["game"]
    # 2. gachi power if X or not
    gachipower = item["gachiEstimateXPower"] if item["udemaeIsX"] else item["gachiEstimatePower"] 
    # 3. extract elapsed time
    battletime = item["elapsedTime"]
    # 4. stage
    stage = item["stage"]
    # 5, Mypower
    mypower = item["xPower"]
    # 6. My data
    mykill = item["player"]["kill"]
    mydeath = item["player"]["death"]    
    mysp = item["player"]["special"]
    myweapon = item["player"]["weapon"]
    mypaintpt = item["player"]["paintPoint"]
    # 7. Our data
    ourkill = mykill
    ourdeath = mydeath
    oursp = mysp
    ourweapon = [myweapon]
    ourpaintpt = mypaintpt
    for ally in item["myMembers"]:
        ourkill += ally["kill"]
        ourdeath += ally["death"]    
        oursp += ally["special"]
        ourweapon.append(ally["weapon"])
        ourpaintpt += ally["paintPoint"]
    # 8. Enemy data
    theirkill = 0
    theirdeath = 0
    theirsp = 0
    theirweapon = []
    theirpaintpt = 0
    for ene in item["otherMembers"]:
        theirkill += ene["kill"]
        theirdeath += ene["death"]    
        theirsp += ene["special"]
        theirweapon.append(ene["weapon"])
        theirpaintpt += ene["paintPoint"]
    # 9. Results
    win = 1 if item["win"] else 0
    
    dfarray.append([game,gachipower,battletime,stage,mypower,mykill,mydeath,mysp,mypaintpt,ourkill,ourdeath,oursp,ourpaintpt,theirkill,theirdeath,theirsp,theirpaintpt,win])

column_name = ["game","gachipower","battletime","stage","mypower","mykill","mydeath","mysp","mypaintpt","ourkill","ourdeath","oursp","ourpaintpt","theirkill","theirdeath","theirsp","theirpaintpt","win"]

# テスト用のデータ格納
testdata=pd.DataFrame(data=dfarray,columns=column_name)

抜き出したのはこんな感じ。昔のデータがあるのでS+時代のデータから始まってるのはご愛嬌。

テスト解析:sklearnの決定木でガチエリアの集計

以前やった決定木から試していきましょう。(Localの環境構築を忘れていたのでsklearnから始めます。)

ガチエリアのX帯の戦績に着目してデータを抜き出します。

areadata = testdata[testdata["game"]=="splat_zonesgachi" ]
areaXdata = areadata[areadata["mypower"]>0]
areaXdata = areaXdata.drop(["game","stage","battletime"],axis=1)

整形後のガチエリアX帯のデータ

さて,以前のコードを流用して簡単に分析ごっこをしてみましょう。

KaggleのTitanicデータに対してsklearnの決定木を試してみる - 粗大メモ置き場

まずはモデルを作成。

import sklearn.tree as tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

clf = tree.DecisionTreeClassifier(max_depth=10)

そして交差検証で学習です。

# 訓練と結果に分ける
X_train = areaXdata.drop(["win"],axis=1)
y_train = areaXdata["win"]


# 5分割交差検証を指定し、インスタンス化
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=0)

# スコアとモデルを格納するリスト
score_list = []
models = []

# 各分割ごとに評価
for fold_, (train_index, valid_index) in enumerate(kf.split(X_train, y_train)):    
    print(f'fold{fold_ + 1} start')
    train_x = X_train.iloc[train_index]
    valid_x = X_train.iloc[valid_index]
    train_y = y_train.iloc[train_index]
    valid_y = y_train.iloc[valid_index]
    
    ## 分割データで学習・予測・評価
    clf = tree.DecisionTreeClassifier(max_depth=10)
    model = clf.fit(train_x, train_y)
    
    # データを用いて予測,記録
    predicted = model.predict(valid_x)
    score_list.append(accuracy_score(predicted,valid_y))
    models.append(model)
print(score_list, '平均score', round(np.mean(score_list), 3))

結果は以下の通りで,85%の精度で結果を推定できました。

[0.9099099099099099, 0.8018018018018018, 0.8198198198198198, 0.8918918918918919, 0.8090909090909091] 平均score 0.847

ここでこの木における重要度,すなわちどの要素が結果を大きく左右したかについて見てみます。

import matplotlib.pyplot as plt

importances = np.zeros(model.feature_importances_.size)

for model in models:
    importances += model.feature_importances_

fig, ax = plt.subplots()
plt.grid()
ax.bar(train_x.columns,importances)
fig.autofmt_xdate() # make space for and rotate the x-axis tick labels
plt.show()

エリアの勝敗への各変数の影響力:敵味方のキルデスの他に自身のデスなども比較的重要と出ました。


データ数と解析手法がおもちゃなのでこのまま信用することはできませんが,言われてみればちょっと納得できるような結果になったのではないでしょうか。

これを拡張して編成を評価できるようにしていきたいですね。

決定木による検証2:他のルールでのキル・デスの関係性

同様の解析をルール毎にかけてみました。

ここから,ホコ・ヤグラ・アサリはオブジェクトの動きに依存するためエリアのようにリザルトだけを見ても勝敗を予想しづらいという仮説が建てられます。

これらのルールそれぞれでの各変数の影響力を比較してみましょう。xticksという機能を使って折れ線グラフのx軸を各変数名に変えています。

fig, ax = plt.subplots()
plt.grid()
xrange = range(len(area_importances))
ax.plot(xrange,area_importances,marker = 'o',label="area")
ax.plot(xrange,yagura_importances, marker='x',label="yagura")
ax.plot(xrange,hoko_importances,marker = 's',label="hoko")
ax.plot(xrange,asari_importances, marker='*',label="asari")
plt.legend()
plt.xticks(xrange,train_x.columns)
fig.autofmt_xdate() # make space for and rotate the x-axis tick labels
plt.show()

各ルール毎の変数の重要度の比較:エリアでは味方のキルが重要であることがわかります。また,ヤグラでは敵のキルと試合結果に大きく相関があります。


深さ10,5回交差検定の決定木による解析結果まとめ

  • エリアでは味方のキル数が試合結果に大きく影響する
  • ヤグラでは敵のデス数が試合結果に大きく影響する
  • アサリ以外では味方のキル・デスと敵のキルデスの占める割合が大きい
  • アサリは他に比べて塗りポイントの重要性が高い?

アサリは特に複雑性の高いルールですのでちょっとなんとも言えないですが,言われてみればそこそこ的を射ているのではないでしょうか。

まとめ

長くなりましたが,第一回としては

  • ikawidget2のデータをrealm→json→pandas(csv)形式に変換
  • データ内容,解析の手法・方向性やテスト

について書きました。

  • 戦績のリザルトからある程度勝敗について予測できるのは直感通り
  • どのスコアがどの程度影響するか可視化できるのは面白い

戦績データをためてて解析してみてほしいという人は一報ください。

補足:LightGBMを用いた際のリザルト重要度比較

今度はKaggleでよく使われるLightGBMの決定木を用いて重要度を比較してみましょう。()内はsklearnからの増分です。

  • ガチエリア:平均86.8%の的中率(+2%)
  • ガチヤグラ:平均81.5%の的中率(+5%)
  • ガチホコ:平均75.9%の的中率(+5%)
  • ガチアサリ:平均70.7%の的中率(+7%)

このときの各変数の重要度の比較は,以下のようになりました。

LightGBMで学習したアンサンブル木での重要度:味方のキルデスの重要度が高いのはそのままですが,敵のキルデスよりもこちらの塗りや自分のデスなどに比重が多くなる場合もあります。

LightGBMの決定木による解析結果まとめ

  • 全般的に味方のキル・デス数が試合結果に大きく影響する
  • SPの回数はあまり勝敗を決しない
  • ヤグラ以外ではデスの方がキルよりも試合への影響が大きい
  • アサリは他に比べて塗りポイントの重要性が高い

Lightgbmを使った際のコード

途中まで一緒なのでKfoldで交差検証するところからメモしました。

コード(クリックして展開)

コードの書き方は主に2つあります。 LightgbmのTrain APIを使う場合とScikitlearn APIを使う場合の違いのようです。

【Python】Kaggleで引っ張りだこ!lightgbmの2種類の使い方!Training APIとScikit-learn API!【lightgbm】│SKJブログ

  • Train関数を使う場合
# スコアとモデルを格納するリスト
score_list1 = []
models1 = []
test_predicts1 = []

# 各分割ごとに評価
for fold_, (train_index, valid_index) in enumerate(kf5.split(X_train, y_train)):    
    print(f'fold{fold_ + 1} start')
    train_x = X_train.iloc[train_index]
    valid_x = X_train.iloc[valid_index]
    train_y = y_train.iloc[train_index]
    valid_y = y_train.iloc[valid_index]
    # parameter
    params = {
    'task': 'train',
    'boosting_type': 'gbdt',
    'objective': 'binary',
    'metric': 'binary_logloss',
    'num_leaves': 64,
    'min_data_in_leaf': 20,
    'max_depth': 7,
    'verbose': 0,
    }
    # create dataset
    lgb_train = lgb.Dataset(train_x,  label=train_y)
    lgb_test = lgb.Dataset(valid_x, label=valid_y)
    # train
    model = lgb.train(params, lgb_train, 10000, valid_sets=lgb_test, early_stopping_rounds=50, verbose_eval=1000)

    # データを用いて予測,記録
    predicted = model.predict(valid_x, num_iteration=model.best_iteration)  # この時点でpredictedは小数
    score_list1.append(accuracy_score(predicted.round(0),valid_y))
    models1.append(model)
  • fit 関数を用いる場合
# LightGBMの分類器をインスタンス化
gbm = lgb.LGBMClassifier(objective='binary')

# スコアとモデルを格納するリスト
score_list2 = []
models2 = []
test_predicts2 = []

# 各分割ごとに評価
for fold_, (train_index, valid_index) in enumerate(kf5.split(X_train, y_train)):    
    print(f'fold{fold_ + 1} start')
    train_x = X_train.iloc[train_index]
    valid_x = X_train.iloc[valid_index]
    train_y = y_train.iloc[train_index]
    valid_y = y_train.iloc[valid_index]
    
    ## 分割データで学習・予測・評価
    gbm.fit(train_x, train_y, eval_set = [(valid_x, valid_y)],
            early_stopping_rounds=20,  # 20回連続でlossが下がらなかったら終了
            verbose=10  # 10round毎に、lossを表示
    ) ;
    # データを用いて予測,記録
    predicted = gbm.predict(valid_x, num_iteration=gbm.best_iteration_) 
    score_list2.append(accuracy_score(predicted,valid_y))
    models2.append(gbm)

ちなみにあまり大きな差は出ません。前者の方は出力が確率になるので解析のときに少しやりやすいかもしれませんね。

TODO

ブキの分類

流石にブキの種類が多いため,そのままブキ名をデータに加えても有意義な解析ができないと思います。 したがって種類ごとに分類します。

  • 公式の分類通りに分類
  • 射程毎に分類
  • 塗りの強さ毎に分類

最も方針が立てやすいのは公式の分類でしょう。ika statsでも似たような表があります。

ika stats様から引用

塗りの評価はika statsさんが出してくれているのでそれを流用するのも良いかもしれません。

ブキ種ごとのkill/deathの重さを可視化

kill/deathと試合結果を2次元Plotで可視化することでどのブキのどの立回りが強いなどの情報を直感的に見ることができます。 (試合時間で正規化していないので今見るともっと良いグラフがかけそうですね。)

ブキ全体では勝ち越す≒グラフ内で暖色になるには1より多いキルレ(kill/death)が必要になることがわかります。

ブキ全体のキルデスと勝敗の比較図。赤いほど勝率が高い。

しかし、例えば前線特攻ブキであるボールド系と最後衛であるリッターではこの勝敗の傾向はだいぶ違います。

ボールドマーカーのキルデスと勝敗の可視化。

リッターのキルデスと勝敗の可視化

ボールドはキル数に試合結果が相関しているのに対し、リッターはそこまで相関していないように読み取れます。
真面目に考察するなら超短射程のボールドが無双するときは盤面が取れてるに違いがないので相関は当然強いでしょうし、リッターの抜きはどちらかというとタイミングが重要なので数が勝敗とそこまで結びつかない、ということだと思われます。無論、リッターが死なない≒リッターに圧がかかるほど相手が攻めれてないなのでリッターのデスが少ないときは全般的に勝率が高いです。

このようにブキ種ごとにどのような傾向があるのかきちんと個別に可視化してから仮説を立てることでより深い洞察につながるのですが、面倒になってやめてしまいました。誰かにやって欲しいですね。

Kaggle-titanicコンペのデータを用いてRandomForest,SVM,LightGBMを試す

前回のつづきです。

(注)本記事はコンペ目的ではなく,ライブラリの使用感を確かめているところで個人メモの範疇です。

ossyaritoori.hatenablog.com

前処理やデータなど

KaggleのTitanicを使っています。 前処理は前記事を参照してください。

RandomForest

例のごとくscikit learnのライブラリを用いていきます。

scikit-learn.org

前回の木構造を複数学習して多数決をとるような手法です。

Boostingなどのキーワードは調べておいても良いでしょう。

コード

デフォルトでは100本の木でアンサンブルしようとするので10本程度に収まるようにさせました。深さ4を上限としています。

import sklearn.tree as tree
from sklearn.ensemble import RandomForestClassifier

# RandomForest 10個のモデルの重ね合わせ
clf = RandomForestClassifier(max_depth=4,random_state=0,n_estimators=10)

ここから交差検証に入ります。5分割くらいでも良かったかも。

# 3分割交差検証を指定し、インスタンス化
from sklearn.model_selection import KFold
kf = KFold(n_splits=3, shuffle=True, random_state=0)

# スコアとモデルを格納するリスト
score_list = []
models = []

# 各分割ごとに評価
for fold_, (train_index, valid_index) in enumerate(kf.split(X_train, y_train)):    
    print(f'fold{fold_ + 1} start')
    train_x = X_train.iloc[train_index]
    valid_x = X_train.iloc[valid_index]
    train_y = y_train.iloc[train_index]
    valid_y = y_train.iloc[valid_index]
    
    ## 分割データで学習・予測・評価
    model = clf.fit(train_x, train_y)
    
    # データを用いて予測,記録
    predicted = model.predict(valid_x)
    score_list.append(accuracy_score(predicted,valid_y))
    models.append(model)
print(score_list, '平均score', round(np.mean(score_list), 3))

このように交差検証法で多数決をとることでデータの偏りの影響を低減できます。(ランダムフォレストの多数決とは別の多数決,ややこしいですね。)

もちろん図示も可能ですが,複数の木の多数決構造をとっているので可視化をする時はそのうちの一つに絞ります。 model.estimators_にリストで入ってるので適当な番号のを覗いてみましょう。

図は前回を参照にしてください。

重要度は全部の木について計算してくれているのかmodelから直接とることができます。

# bar plot
fig, ax = plt.subplots()
plt.grid()
ax.bar(train_x.columns,model.feature_importances_)
fig.autofmt_xdate() # make space for and rotate the x-axis tick labels
plt.show()

f:id:ossyaritoori:20200421010909p:plain
RandomForestで見た変数重要度。決定木と比べて Fareに重点が置かれています。

何本アンサンブルすればいいの?

木を増やせば汎化性能は上がりそうですが,計算量の問題もあります。 モデルの複雑性が増すと過学習しないかなという懸念もあります。

結構議論されているようですが,いくつか見て以下の意見を参考にしました。

How to determine the number of trees to be generated in Random Forest algorithm?

f:id:ossyaritoori:20200421011153p:plain
要約:原則木が多いほど性能が上がるが,上がり幅が小さくなっていく。個々のケースでは木が少ない場合が良い結果を出す可能性はあるかもね。10,30,100あたりで決めたら?

10本 RF 交差検証:0.808 本番:0.77990
30本 RF 交差検証:0.805 本番:0.76555
100本 RF 交差検証:0.805 本番:0.78468

確かに,若干は上がるようです。この辺は試行錯誤する必要があるので後で説明するgrid searchをすると良いでしょう。

SVMサポートベクターマシン

木構造とは異なりますが,SVMのような分類手法も有名です。

簡単に言うとデータ群を最適に分割する超平面を求める手法です。

www.slideshare.net

  • カーネルトリック:平面で分割とは言ってもデータの変数をかさ増しすることで非線形な分割も可能になります。例えば元の変数がx,yなら,x^2, xy ,y^2も変数に加えた空間で分割すれば元の空間では直線ではなくなります。ポイントは内積計算が楽になるようにこのかさ増しを工夫することです。

sklearnではデフォルトでRBFカーネルというものが使われています。

参考:

[python 機械学習初心者向け] scikit-learnでSVMを簡単に実装する - Qiita

カーネル近似(クラス分類)【Pythonとscikit-learnで機械学習:第2回】

コード

from sklearn.svm import SVC

clf = SVC(kernel='rbf', C=1, gamma=0.1)


# 3分割交差検証を指定し、インスタンス化
from sklearn.model_selection import KFold
kf = KFold(n_splits=3, shuffle=True, random_state=0)

# スコアとモデルを格納するリスト
score_list = []
models = []

# 各分割ごとに評価
for fold_, (train_index, valid_index) in enumerate(kf.split(X_train, y_train)):    
    print(f'fold{fold_ + 1} start')
    train_x = X_train.iloc[train_index]
    valid_x = X_train.iloc[valid_index]
    train_y = y_train.iloc[train_index]
    valid_y = y_train.iloc[valid_index]
    
    ## 分割データで学習・予測・評価
    model = clf.fit(train_x, train_y)
    
    # データを用いて予測,記録
    predicted = model.predict(valid_x)
    score_list.append(accuracy_score(predicted,valid_y))
    models.append(model)
print(score_list, '平均score', round(np.mean(score_list), 3))

結果は[0.7104377104377104, 0.6868686868686869, 0.6666666666666666] 平均score 0.688と少し低めに出てしまいました。

ハイパーパラメータ調整の指針

以下のサイトから引用します。

scikit-learn.org

upura.hatenablog.com

  • γ:「一つの訓練データが与える影響の範囲」を意味します。小さいほど「遠く」、大きいほど「近く」まで影響します。
  • C:分類ミスに対するペナルティと分離平面からの距離のトレードオフです。大きいほど誤りに厳しく,データすれすれに線をひきます。したがって小さいほど大雑把でシンプルな分類になりやすいようです。

実際のシチュエーションでは複数のハイパーパラメータを試して最も良さそうな組み合わせをチョイスする必要があります。 for文でも書けますが,sklearnにはgrid_searchという関数があります。

バージョンによって関数名が変わるのですが,Ver1.0では以下のように書きます。

from sklearn.model_selection import GridSearchCV

# グリッドサーチを用いたオートチューニング
parameters = [{'kernel':['rbf'], 'C':np.logspace(-4, 4, 9), 'gamma':np.logspace(-4, 4, 9)}] # RBF kernel でグリッドサーチ

# 交差検定5回でSVCを用いてやる
clf = GridSearchCV(cv=5,estimator=SVC(), param_grid=parameters, n_jobs = -1)
out = clf.fit(X_train, y_train)

print(out.best_estimator_)

このbest estimatorが最も成績の良かった学習器です。

SVC(C=10000.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0,
    decision_function_shape='ovr', degree=3, gamma=0.0001, kernel='rbf',
    max_iter=-1, probability=False, random_state=None, shrinking=True,
    tol=0.001, verbose=False)

このときの精度は0.8417508417508418と前回より随分高くなりました。

LightGBM

LightGBMは決定木のアルゴリズムを用いた機械学習フレームワークです。以下のサイトが勉強になります。

www.codexa.net

コード

使い方はsklearnのものと非常によく似ています。

import lightgbm as lgb

# LightGBMの分類器をインスタンス化
gbm = lgb.LGBMClassifier(objective='binary')

# trainとvalidを指定し学習
gbm.fit(train_x, train_y, eval_set = [(valid_x, valid_y)],
        early_stopping_rounds=20,  # 20回連続でlossが下がらなかったら終了
        verbose=10  # 10round毎に、lossを表示
) ;

一応推論をしてみると,score 81.48 %と出ます。

# valid_xについて推論
oof = gbm.predict(valid_x, num_iteration=gbm.best_iteration_)  # oofはout of fold
print('score', round(accuracy_score(valid_y, oof)*100,2), '%')  # 正解率の表示

図示

こちらはデフォルトでそこそこ見やすい図が出ます。

lgb.create_tree_digraph(gbm)

f:id:ossyaritoori:20200428003643p:plain
一部分のみ。

重要度は以下のように簡単にplotできます。

lgb.plot_importance(gbm, figsize=(12, 6))

f:id:ossyaritoori:20200428004244p:plain
LightGBMでの変数重要性:船賃と年齢が圧倒的で,性差は低めに見積もられていることがわかります。

提出に用いた交差検証 LightGBM

こちらも交差検証を用いて複数のモデルの多数決をとります。

# 5分割交差検証を指定し、インスタンス化
from sklearn.model_selection import KFold
kf5 = KFold(n_splits=5, shuffle=True, random_state=0)

# スコアとモデルを格納するリスト
score_list = []
models = []
test_predicts = []

# 各分割ごとに評価
for fold_, (train_index, valid_index) in enumerate(kf5.split(X_train, y_train)):    
    print(f'fold{fold_ + 1} start')
    train_x = X_train.iloc[train_index]
    valid_x = X_train.iloc[valid_index]
    train_y = y_train.iloc[train_index]
    valid_y = y_train.iloc[valid_index]
    
    ## 分割データで学習・予測・評価
    gbm.fit(train_x, train_y, eval_set = [(valid_x, valid_y)],
            early_stopping_rounds=20,  # 20回連続でlossが下がらなかったら終了
            verbose=10  # 10round毎に、lossを表示
    ) ;
    # データを用いて予測,記録
    test_predicts.append(gbm.predict(test, num_iteration=gbm.best_iteration_) )
    predicted = gbm.predict(valid_x, num_iteration=gbm.best_iteration_) 
    score_list.append(accuracy_score(predicted,valid_y))
    
print(score_list, '平均score', round(np.mean(score_list), 3))
[0.8491620111731844, 0.8089887640449438, 0.8314606741573034, 0.8033707865168539, 0.8089887640449438] 平均score 0.82

コンペ結果

気休めですが,LightBGMが僅差でランダムフォレストを上回っていました。

f:id:ossyaritoori:20200428004543p:plain
そこまで各手法で劇的に差が出るわけではありませんね。

他にも手法があるので,概略と動かし方をわかっていれば今後の解析の時に役立ちそうですね。

scikit-learn.org

[https://scikit-learn.org/stable/images/sphx_glr_plot_classifier_comparison_001.png:image=https://scikit-learn.org/stable/images/sphx_glr_plot_classifier_comparison_001.png]