仮眠プログラマーのつぶやき

自分がプログラムやっていて、思いついたことをつぶやいていきます。

自作ゲームやツール、ソースなどを公開しております。
①ポンコツ自動車シュライシュラー
DOWNLOAD
②流体力学ソース付き
汚いほうDOWNLOAD
綺麗なほうDOWNLOAD
③ミニスタヲズ
DOWNLOAD
④地下鉄でGO
DOWNLOAD
⑤ババドン
DOWNLOAD
⑥圧縮拳(ツール)
DOWNLOAD
⑦複写拳
DOWNLOAD
⑧布シミュレーション
DOWNLOAD
⑨minecraft巨大電卓地形データ
DOWNLOAD
⑩フリュードランダー
デジゲー博頒布α版
DOWNLOAD
⑪パズドラルート解析GPGPU版
DOWNLOAD
⑫ゲーム「流体de月面着陸」
DOWNLOAD

UnityのComputeShader関連のエラー Failed to present D3D11 swapchain due to device reset/removed.について

Failed to present D3D11 swapchain due to device reset/removed.

というエラーメッセージについて、個人的にハマったこと、解決したことをまとめようと思います。
結論から言うとこういうことです。

環境:Windows 11、Unity 2021.2.7f1、GPUはGeForce MX150(mem 2GB)
・Windowsで描画担当のGPUが高負荷で数秒応答しなくなった時、ドライバが強制終了する仕様がある
・Unityのプログラムで初期地形生成にGPUのComputeShaderを使っていた。低スペックGPUだと3-5秒くらいかかるので、タイムアウト時間によりOSがドライバをリセット、Unityで上記エラーが発生

さて、これはバグでもなくUnity側の不具合でもなく、WindowsOSの仕様が引き起こしたことなのでバグとは呼ばず「エラー」と言うこととします。
では時系列順に起こったことを書いていきます。

Unity Editorでエラー&強制終了

ある日からUnity Editorで再生ボタンを押すとこのようにエラーマークが出てEditorごと落ちるようになった。
100%落ちるわけではなく20%くらいの確率で普通にうまくプログラムが起動する。
また、別の高スペックなほうのPCでやると一切エラーマークは発生せず、ちゃんと起動する。
e1

Waiting for Unity's code to finish executing

続いてエラーが出るときにEditor Windowの後ろのほうに出ているメッセージになにかヒントがないか調べてみた。
"EnterPlayMode"や"Waiting for Unity's code to finish executing"などで検索するも有益な情報なし。
そもそも今回のエラーと関係あるのか確証もなかった。

e2

Buildしたらどうなるか

Editor上で実行するときだけ起こる問題なのかと思い、今度はBuildでexeを生成してから実行してみた。
すると100%の確率で強制終了。画像のようなエラーマークがでる。
別のPCでやるとちゃんと起動できる。なのでPCのスペックに依存したエラーかなとあたりがつく
e3

クラッシュログを見る

Editorでエラー終了するときにどうもクラッシュログが生成されていることに気づく。
クラッシュログをみるとGPUのメモリ確保(TextureやComputeBuffer)に全て失敗しているようだった。
これでGPU関連のプログラムでなにかまずいことをしているかもと絞り込んでいろいろ試行錯誤開始。
crashlog

エラーが発生しない状況を突き止める

その結果、地形生成部分のGPUの計算負荷(ComputeShaderを使っている)を軽くするとエラーが発生しないことがわかった。具体的には1616×1616マスの地形生成を208×208に減らした。
なので今後は1616×1616のうち208×208の地形生成を最初に行い、数秒後に残りの部分の地形生成を行いエラーが発生するか確かめてみることにした。

GPU Timeoutの原因がexpensive workloads

そうしたところエラー発生。ついに有益なメッセージが得られた。
Failed to present D3D11 swapchain due to device reset/removed.
This error can happen if you draw or dispatch very expensive workloads to the GPU,
which can cause Windows to detect a GPU Timeout and reset the device.
~~~~~~~~~

e4

要約するとGPU負荷がすごすぎてGPUタイムアウト、Windowsがデバイスをリセットした、ということのよう。
これはWindows上でCUDAで遊んだり機械学習してる人なら見慣れている光景かもしれないが、レジストリのTdrDelayを書き換えれば解決するやつ。

TdrDelayを書き換え解決

リンクのやつを参考にTdrDelayを60秒にして再起動をかける。

振り返り

振り返ってみると、ある日からエラーが発生するようになったのはWindows 11にアップデートしたからでした。もともとWindows10で作業していたときは別件ですでに上記のレジストリをいじっていたのでエラーは発生しなかったのだと思われます。これがOSアップデートで戻され、タイムアウト時間が数秒程度になっていたのでしょう。
Unity側からしてみると、プログラム実行中にドライバが突然リセットされるので何が起こったかわかるはずもありません。最初まともなエラーメッセージがでなかったのも無理はありません。

多分、WindowsOSがGPUドライバを強制リセットすることがあるということを知らないとなかなか解決には至らなかったかと思うので今回記事にさせていただきました。

ウマ娘のキャラをディープラーニングで分類して出現確率を検証してみた

最初はリセマラ自動化
で遊んでいたのですが、だんだんサポートカードの出現確率が本当に正しいのか気になり、それを検証してみようと思いました。
出現確率を検証するには、ガチャ結果の画像から各キャラの識別を確実にしなければなりません。
そこでディープラーニングの勉強も兼ねてやってみたところ、分類性能がとても良かったので記事にしてみました。

ディープラーニングで分類

スクショの切り取り

今や調べればいくらでもディープラーニングのやり方自体は出てくるので、ここではディープラーニングで使う教師データをどう揃えていくかについて細かく書いていこうと思います。

まずは上の記事にあるよう、NoxPlayerなどから画面のスクショを撮れるようにがんばります。

そしてこんな感じ↓のスクショを集めまくります。

1

ここでは全キャラの全サポートの画像を集めたいので、すべてで132種類になります。(同じキャラでもR,SR,SSRがあったりする)
スクショだけでも500枚とか集めないとなかなか132種類集まりません。
集めたらここからキャラのいる位置を適切に切り取ります。

スクショ画像が540×960とすると切り取る位置は↓
aaaa


です。
切り取る大きさはすべて64*64ピクセルです。

切り取る位置の左上座標は
x=92,253,415,173,335,92,251,409,166,327
y=130,130,130,309,309,487,487,487,663,663
としてます。
ここの数値はすべて手動で調整しました。

これでこんな感じの画像が手に入りました。
tt

ちなみに切り取り位置は微妙に調整不足で、
同じキャラ画像でも10連の1番目に出たか10番目に出たかで上下左右7,8ピクセルくらいずれてたのが取得されてしまっています。

例:スイープトウショウSRで見ると右目が見切れていたり顎が見切れていたり・・・
zz4167

zz2589

zz2461


一応、切り取りを行ったプログラムを公開します。
triming.exeとソースコードtriming.hsp
hsp切り取り

triming.exeの動作としては同じディレクトリにあるフォルダ(複数も可)の中を全部検索し、pngファイルを読み込んで上記位置を切り取って
「gr」という名前のフォルダに結果を連番で保存していく、というものです。
なお「gr」フォルダは上記検索からは除外されています。
※画像にはないですが、同じディレクトリにhspcv.dllがないとエラー38になります。

最初の1枚を手動で分類

さて
tt
このように各キャラの画像がある程度集まってきたので、ここからは手動で各画像の分類を行っていきます。
しかしディープラーニングに学習させるには何万枚という教師データが必要になることが予想され、それを全部手動でやるのは非常に骨が折れます。
ひとまず、ライスシャワーRはコレ、というように最初の1枚だけは手動で分類し、あとはプログラムで見つけてもらうことにしましょう。

aaaa_20210809_145946

最初の1枚を手動で分類しました。

一致する可能性の高い画像を集める

最初の1枚をベースにして、同じと思われる画像をなんとか集めたいです。
先程のスイープトウショウSRの例を見るとわかると思いますが、完全一致の画像を集めるのだけでは不十分です。
1ドット~10ドットずれている画像がほとんどなのでそれを考慮した計算が必要になりそうです。
さらにドットずれを完全に合わせたとしても、RGB単位で完全一致するとも限りません。

zz1281353

zz1281001

例えばエアグルーヴSRのこの画像を見ると、SR特有の星のキラキラエフェクトが入った画像も混じっていることがわかります。
なので、「1ドットでも色が一致しなければダメ」は厳しすぎて、ある程度の誤差は許容しないといけないことがわかります。

これをもとに下記のようなアルゴリズムを考えました。(テンプレートマッチングみたいな感じです)

・ベース画像(64*64)を読み込み、中心の32*32範囲を切り取る。
・まだ分類してない画像(64*64)を読み込み、左上(0,0)~右下(32,32)まで1ドットずつスライドさせながら、先程の32*32の一致度合いを求める。
・132種類のベース画像全部で試し、差分スコアの一番低いのが同じ画像と思われるとする。
・一致度合いの計算は32*32領域を重ねたとき、各画素RGBごとに差をとった絶対値の合計

これで一応はいいのですが、未分類画像の中に、そもそもどのキャラにも分類されないゴミ画像が混じっていることもあり、それはそれで除外したいので
さらに下記条件を考えました。

・132種類のベース画像全部で試し、差分スコアの一番低いのを決めたあと、そのスコアが一定値を下回らなければゴミ画像


これらを実装したのがコチラ(3232check.exe)になります。

これで、こんな感じに未分類の画像をある程度の精度で仕分けすることができました。
分類後


そしてゴミとして認識され、未分類フォルダに残ったファイルはこんな感じでした。

amari


これを見るに、SSRのエフェクトで光の帯ががっつりかかってしまったものや、そもそもガチャ結果画像じゃないものなどが残っていることがわかります。

しかし一見普通のスイープトウショウSSRやウイニングチケットSSR画像も残っているように見え、分類精度としてはもうちょっと上を目指したいところです。

3232check.exeの使い方

少し話はもどって3232check.exeの使い方です。
3232tukaikata

このように上記リンクからDLしてきて
・3232check.exe
・HSPCL64.dll
をおきます。

・「gr」フォルダには64*64の未分類画像がzz?????.pngという名前で入っている状態にしてください。
この?????には0埋めしてない数字で0~999999までの数字としてください。
・「アイネスフウジン」「アグネスタキオン」・・・のフォルダ内には「R」「SR」「SSR」などフォルダがあり、そのフォルダ内にベース画像(64*64)が1枚ある状態にしておいてください。
SR,SSRがないキャラは「R」というフォルダだけ作っていれば良いです。

ディープラーニング開始

これで各サポカの画像がかなり揃った状態になったと思います。
レア度の高いSSRでも最低10枚くらいあればディープラーニング開始できると思われます。おそらくその頃にはRやSRは500~1000枚くらい集まっていることでしょう。

まだ足りなければ「スクショの切り取り」に戻って素材を集めましょう。


ディープラーニング自体はかの有名な
ゼロから作るDeep Learning
の本のコードをそのまま使っていきます。
本ではMNISTの分類問題を扱っていますが、ここではそのままウマ娘に適応します。

具体的にはMNISTは28*28のグレースケール画像だが、ウマ娘では64*64のカラー画像であり、入力要素数は64*64*3=12288に増えるということになります。

出力層のノード数も10から132に増えますが、ここではゴミ画像の検出も行いたいので出力は133ということになります。

中間層のノード数は1024、384程度に適当に設定しておきます。

肝心のディープラーニング自体のソースコードはここでは載せません・・・(本とほぼ同じなので)

これで先程まで分類できていなかった画像を分類していきます。

無題2

画面上がディープラーニングで分類した画像、下は教師用のデータです。
面白いことに、ゴールドシップRのぼやけた画像も正しく分類することができました!

教師データには1枚もぼかし画像はなかったはずなのに、しっかり認識しているあたりディープラーニングのすごさが伺えます。

他のキャラの画像でも似たような現象があり、ぼやけた画像もかなりの精度で分類できていました。
しかし髪の色が似ている別のキャラに間違って分類されていたこともあったり、そこはまだ人の目でチェックしていく必要がありそうという印象でした。

もちろんぼかしが入ってない画像に関しては完璧に分類できていました。


ここで一応、再現性があるよう、どのようなディープラーニングを行ったかについてまとめておきます。
・入力層12288→中間層1024→中間層384→出力層133
・画像色情報は0.0~1.0の範囲におさまるよう正規化
・活性化関数はReLUを使用
初期値はHeの初期値を使用 (最初これを使用していなくて分類精度が著しく悪く挫折しかけた)
・ミニバッチのバッチサイズは256
・Batch NormalizationやDropoutは使用してない
・学習の計算打ち切り目安は、損失関数の評価値 loss<0.01~0.02


悪意のあるデータ(強烈なぼかしが入っている、画像の大半が白飛びしてる等)を取り除くと、
教師:テスト=0.8 : 0.2程度に分けて、しっかり認識精度100%を達成できました。

ディープラーニングの結果をすべて確認して分類しなおす

先程は悪意のあるデータと書きましたが、やはり↓このような微妙な画像は間違って分類されていたりということがおきていました。

無題1

ウイニングチケットSSRに分類された画像です。正しいのは水色っぽいほうですが、スイープトウショウSSRも混ざってしまっています。(この時はイベントでスイープトウショウSSRとウイニングチケットSSRが大量に出現していた)

実際光のエフェクトのかかり具合が似ていて、これは間違えてもしょうがないという印象です。

残念ながら、まだ教師データが不足していると言えそうです。
間違って分類された画像を1枚1枚正しいところにうつしかえ、再度ディープラーニングで学習→未分類画像を分類→目で見てチェック・・・を繰り返します。

正直ここが一番時間がかかりました。
ただ繰り返していくうちにどんどん精度が良くなっていくのを感じたので、単純作業の割には面白かったかなと思います。

重複ファイル削除

少し話が逸れますが、ここまで大量の画像を扱っていると、中身が全く同じ画像というのがでてきます。学習させるのに全く同じ画像は必要ないので重複ファイルを削除したくなります。

しかし巷にあるフリーソフトの重複ファイル削除ソフトはいまいち性能が悪く、結局自分で作ったほうがはやかったです。

重複pngを自動削除する.exeとソースコード

同じディレクトリにあるpngファイルで、完全一致のものを1つだけ残してあとは削除するソフトです。

1つのフォルダで完結すればいいのですが、今回は全キャラ全種類合計132のフォルダがあるので、一つ一つフォルダにいれて実行するのはまた骨の折れる作業です。
ということで
HSPで2階層先のフォルダにexeをコピーして自動実行するスクリプトを書いて使いました。

pngファイルを全部削除

いろいろ実験しているうちに、フォルダ内にあるpngファイルを全部削除したいということがあります。もちろんエクスプローラーでファイルを選択して削除やフォルダごと削除すればいいのですが、5万ファイルや10万ファイルという多くのファイルを扱うと、なぜかエクスプローラーがフリーズしたり削除に時間がかかったり(具体的には99%で止まる)します。

これも地味にストレスなのでpngだけ削除するプログラムを作成しました。

同じ階層のpngを全部削除.exeとソースコード

ついに人間の目でも判別がつかなくなった

教師データを増やしディープラーニングの精度をどんどん上げていった結果、ついに人間の目でも判別がつかないデータだけが残りました。

これはディープラーニングでゴミ画像、つまりどのキャラにも分類されないと判定された画像です。

gomi


さすがにこれは分からない!!!

頑張って目をこらせばスーパークリークやキタサンブラックが見つかりますが、そこまで求めているわけではないというか・・


prpth


この頃には教師データが32万ファイルを超えており、合計3.7GBになっていました。
重複ファイルを除いてこれなので、実際は120万ファイルぐらいはあったと思います。



本来の目的を思い出すと、ガチャ結果の画像で正確にキャラを判定し、出現確率を求めたいというのがありました。
ガチャ結果のスクショのタイミングが悪いと、上の画像のように真っ白のSSRがキャプチャされることがあり、そういう場合は再度タイミングをずらしてスクショを撮れば良いということになります。

つまり今回のディープラーニングに求めるのは、白みがかった画像をきちんとゴミ画像と判定してくれることです。

人間の目でも判別がつかないので、キャラ分別の精度を追求するのはここまでにして、最終目的を果たしていくことにしましょう。

出現確率の検証

出現数のカウント

今までNoxPlayerを操作させるコード(Python)でガチャ結果のスクショを撮っていました。
そこにディープラーニングの推論部分を実装し、スクショ保存部分をキャラ推論に書き換えます。
10キャラともかなり高い精度(>0.92)で分類できたら確定とし、1キャラでも精度が低かったりやゴミ画像に分類されたらタイミングをずらして再度スクショを撮って推論をやり直します。

実行時の様子
jikkouji


これは冒頭でも紹介した
https://noitalog.tokyo/python-umamusume/

ここで配布されてるコードを一部改変したものになります。


続いてアプリで公開されている提供割合↓は小数点第四位で四捨五入されているようなので、手計算で正確な値を導き出しておきます。
0730_1600

2021/8/9時点

スイープトウショウSSR:0.750%
ウイニングチケットSSR:0.750%
その他SSR(34種類):0.04411765%
SR(29種類):0.62068966%
R(67種類):1.17910448%



プログラムで各サポートカードの出現数をカウントし、確率を求めました。

10連のうち1-9枚目(R=79%,SR=18%,SSR=3%)

合計190836枚

名前 出現回数
アイネスフウジン_R 2274
アイネスフウジン_SSR 86
アグネスタキオン_R 2217
アグネスタキオン_SR 1186
アグネスデジタル_R 2179
アグネスデジタル_SR 1137
イクノディクタス_R 2307
イクノディクタス_SR 1109
ウイニングチケット_R 2113
ウイニングチケット_SSR 86
ウイニングチケット_SSR2 1424
ウオッカ_R 2253
ウオッカ_SSR 72
エアグルーヴ_R 2225
エアグルーヴ_SR 1223
エアシャカール_R 2225
エアシャカール_SSR 72
エイシンフラッシュ_R 2213
エイシンフラッシュ_SR 1175
エルコンドルパサー_R 2235
エルコンドルパサー_SSR 86
オグリキャップ_R 2266
オグリキャップ_SSR 95
カワカミプリンセス_R 2190
カワカミプリンセス_SSR 97
キタサンブラック_R 2211
キタサンブラック_SSR 97
キングヘイロー_R 2268
キングヘイロー_SR 1232
キングヘイロー_SSR 77
グラスワンダー_R 2355
グラスワンダー_SSR 69
ゴールドシチー_R 2254
ゴールドシチー_SSR 77
ゴールドシップ_R 2282
ゴールドシップ_SSR 96
サイレンススズカ_R 2304
サイレンススズカ_SSR 105
サクラチヨノオー_R 2235
サクラチヨノオー_SSR 87
サクラバクシンオー_R 2194
サクラバクシンオー_SSR 80
サトノダイヤモンド_R 2226
サトノダイヤモンド_SSR 85
シンコウウインディ_R 2238
シンコウウインディ_SR 1244
シンボリルドルフ_R 2239
シーキングザパール_R 2203
シーキングザパール_SR 1191
スイープトウショウ_R 2275
スイープトウショウ_SR 1189
スイープトウショウ_SSR 1482
スペシャルウィーク_R 2219
スペシャルウィーク_SSR 103
スマートファルコン_R 2255
スマートファルコン_SSR 97
スーパークリーク_R 2295
スーパークリーク_SSR 83
セイウンスカイ_R 2259
セイウンスカイ_SSR 103
セイウンスカイ_SSR2 63
ゼンノロブロイ_R 2204
ゼンノロブロイ_SR 1160
タイキシャトル_R 2223
タマモクロス_R 2308
タマモクロス_SSR 95
ダイタクヘリオス_R 2219
ダイタクヘリオス_SR 1164
ダイワスカーレット_R 2284
ダイワスカーレット_SR 1221
ツインターボ_R 2251
ツインターボ_SSR 85
テイエムオペラオー_R 2214
トウカイテイオー_R 2278
トウカイテイオー_SSR 92
ナイスネイチャ_R 2272
ナイスネイチャ_SR 1253
ナリタタイシン_R 2258
ナリタタイシン_SR 1233
ナリタブライアン_R 2276
ニシノフラワー_R 2317
ニシノフラワー_SR 1213
ニシノフラワー_SSR 73
ハルウララ_R 2251
バンブーメモリー_R 2256
バンブーメモリー_SSR 95
ヒシアケボノ_R 2254
ヒシアケボノ_SSR 91
ヒシアマゾン_R 2233
ヒシアマゾン_SR 1241
ビコーペガサス_R 2205
ビコーペガサス_SSR 76
ビワハヤヒデ_R 2238
ビワハヤヒデ_SR 1176
ファインモーション_R 2256
ファインモーション_SSR 90
フジキセキ_R 2298
フジキセキ_SR 1230
マチカネタンホイザ_R 2222
マチカネフクキタル_R 2215
マチカネフクキタル_SR 1190
マヤノトップガン_R 2240
マヤノトップガン_SR 1209
マルゼンスキー_R 2185
マンハッタンカフェ_R 2222
マンハッタンカフェ_SR 1133
マーベラスサンデー_R 2274
マーベラスサンデー_SR 1149
ミホノブルボン_R 2267
ミホノブルボン_SR 1193
メイショウドトウ_R 2189
メイショウドトウ_SR 1192
メジロアルダン_R 2236
メジロアルダン_SR 1248
メジロドーベル_R 2252
メジロドーベル_SR 1197
メジロパーマー_R 2227
メジロパーマー_SSR 75
メジロマックイーン_R 2260
メジロライアン_R 2190
メジロライアン_SR 1209
メジロライアン_SSR 92
ヤエノムテキ_R 2300
ヤエノムテキ_SSR 80
ユキノビジン_R 2248
ユキノビジン_SR 1230
ユキノビジン_SSR 81
ライスシャワー_R 2242
桐生院葵_R 2207
桐生院葵_SR 1089
駿川たづな_R 2298
駿川たづな_SSR 95

今後もサンプル増える予定

10連のうち10枚目(SR=97%,SSR=3%)

●●●●●●●●●●●サンプルが少ないのでまだ●●●●●●●●●

統計的に有意差を見る

確率と統計、たまにガチャの話[qiita]
こちらを参考に、各サポートカード出現確率が本当に正しいのか検証しましょう。

ここでは両側検定をしようと思います。
「俺はキタサンブラックSSRがほしいのに全然でない!たづなSSRはいらないほど出てるのに!」みたいなヤツの仮説を棄却するには、SSR自体の出現確率が0.044%より有意に低くも高くもないことを示さないといけないからです。

また有意水準は0.05とします。

今回のキタサンブラックSSRの出現確率について考えると、190836回施行のうち97回出現が確認されたので次のようになります。

x

公式の出現確率は
y

なので
z


有意水準は0.05なので棄却域は -1.96 <= Z <= 1.96 の外ということになり、今回の結果では、「キタサンブラックSSRの出現確率は0.0441176%である」という説が覆る可能性は低そうです。

全サポートカードのZ値


計算例にそって全サポートカードのZ値を算出しました。


名前 出現回数 出現確率 p Z
アイネスフウジン_R 2274 0.01191599 0.011791045 0.50565195
アイネスフウジン_SSR 86 0.00045065 0.000441176 0.197048502
アグネスタキオン_R 2217 0.01161730 0.011791045 -0.703119424
アグネスタキオン_SR 1186 0.00621476 0.006206897 0.043739669
アグネスデジタル_R 2179 0.01141818 0.011791045 -1.508967006
アグネスデジタル_SR 1137 0.00595800 0.006206897 -1.384432891
イクノディクタス_R 2307 0.01208891 0.011791045 1.205466955
イクノディクタス_SR 1109 0.00581127 0.006206897 -2.200531497
ウイニングチケット_R 2113 0.01107233 0.011791045 -2.908597017
ウイニングチケット_SSR 86 0.00045065 0.000441176 0.197048502
ウイニングチケット_SSR2 1424 0.00746190 0.0075 -0.192889403
ウオッカ_R 2253 0.01180595 0.011791045 0.060315128
ウオッカ_SSR 72 0.00037729 0.000441176 -1.329067456
エアグルーヴ_R 2225 0.01165923 0.011791045 -0.533467301
エアグルーヴ_SR 1223 0.00640864 0.006206897 1.122155684
エアシャカール_R 2225 0.01165923 0.011791045 -0.533467301
エアシャカール_SSR 72 0.00037729 0.000441176 -1.329067456
エイシンフラッシュ_R 2213 0.01159634 0.011791045 -0.787945485
エイシンフラッシュ_SR 1175 0.00615712 0.006206897 -0.276870497
エルコンドルパサー_R 2235 0.01171163 0.011791045 -0.321402148
エルコンドルパサー_SSR 86 0.00045065 0.000441176 0.197048502
オグリキャップ_R 2266 0.01187407 0.011791045 0.335999827
オグリキャップ_SSR 95 0.00049781 0.000441176 1.178123046
カワカミプリンセス_R 2190 0.01147582 0.011791045 -1.275695337
カワカミプリンセス_SSR 97 0.00050829 0.000441176 1.396139611
キタサンブラック_R 2211 0.01158586 0.011791045 -0.830358516
キタサンブラック_SSR 97 0.00050829 0.000441176 1.396139611
キングヘイロー_R 2268 0.01188455 0.011791045 0.378412858
キングヘイロー_SR 1232 0.00645580 0.006206897 1.384473093
キングヘイロー_SSR 77 0.00040349 0.000441176 -0.784026043
グラスワンダー_R 2355 0.01234044 0.011791045 2.223379691
グラスワンダー_SSR 69 0.00036157 0.000441176 -1.656092304
ゴールドシチー_R 2254 0.01181119 0.011791045 0.081521643
ゴールドシチー_SSR 77 0.00040349 0.000441176 -0.784026043
ゴールドシップ_R 2282 0.01195791 0.011791045 0.675304072
ゴールドシップ_SSR 96 0.00050305 0.000441176 1.287131329
サイレンススズカ_R 2304 0.01207319 0.011791045 1.141847409
サイレンススズカ_SSR 105 0.00055021 0.000441176 2.268205873
サクラチヨノオー_R 2235 0.01171163 0.011791045 -0.321402148
サクラチヨノオー_SSR 87 0.00045589 0.000441176 0.306056784
サクラバクシンオー_R 2194 0.01149678 0.011791045 -1.190869276
サクラバクシンオー_SSR 80 0.00041921 0.000441176 -0.457001195
サトノダイヤモンド_R 2226 0.01166447 0.011791045 -0.512260786
サトノダイヤモンド_SSR 85 0.00044541 0.000441176 0.088040219
シンコウウインディ_R 2238 0.01172735 0.011791045 -0.257782602
シンコウウインディ_SR 1244 0.00651869 0.006206897 1.734229638
シンボリルドルフ_R 2239 0.01173259 0.011791045 -0.236576087
シーキングザパール_R 2203 0.01154394 0.011791045 -1.000010638
シーキングザパール_SR 1191 0.00624096 0.006206897 0.189471563
スイープトウショウ_R 2275 0.01192123 0.011791045 0.526858465
スイープトウショウ_SR 1189 0.00623048 0.006206897 0.131178805
スイープトウショウ_SSR 1482 0.00776583 0.0075 1.345980662
スペシャルウィーク_R 2219 0.01162779 0.011791045 -0.660706393
スペシャルウィーク_SSR 103 0.00053973 0.000441176 2.050189308
スマートファルコン_R 2255 0.01181643 0.011791045 0.102728158
スマートファルコン_SSR 97 0.00050829 0.000441176 1.396139611
スーパークリーク_R 2295 0.01202603 0.011791045 0.950988771
スーパークリーク_SSR 83 0.00043493 0.000441176 -0.129976346
セイウンスカイ_R 2259 0.01183739 0.011791045 0.18755422
セイウンスカイ_SSR 103 0.00053973 0.000441176 2.050189308
セイウンスカイ_SSR2 63 0.00033013 0.000441176 -2.310142
ゼンノロブロイ_R 2204 0.01154918 0.011791045 -0.978804123
ゼンノロブロイ_SR 1160 0.00607852 0.006206897 -0.714066179
タイキシャトル_R 2223 0.01164875 0.011791045 -0.575880332
タマモクロス_R 2308 0.01209415 0.011791045 1.22667347
タマモクロス_SSR 95 0.00049781 0.000441176 1.178123046
ダイタクヘリオス_R 2219 0.01162779 0.011791045 -0.660706393
ダイタクヘリオス_SR 1164 0.00609948 0.006206897 -0.597480664
ダイワスカーレット_R 2284 0.01196839 0.011791045 0.717717103
ダイワスカーレット_SR 1221 0.00639816 0.006206897 1.063862926
ツインターボ_R 2251 0.01179547 0.011791045 0.017902097
ツインターボ_SSR 85 0.00044541 0.000441176 0.088040219
テイエムオペラオー_R 2214 0.01160158 0.011791045 -0.76673897
トウカイテイオー_R 2278 0.01193695 0.011791045 0.590478011
トウカイテイオー_SSR 92 0.00048209 0.000441176 0.851098198
ナイスネイチャ_R 2272 0.01190551 0.011791045 0.463238919
ナイスネイチャ_SR 1253 0.00656585 0.006206897 1.996547047
ナリタタイシン_R 2258 0.01183215 0.011791045 0.166347704
ナリタタイシン_SR 1233 0.00646105 0.006206897 1.413619472
ナリタブライアン_R 2276 0.01192647 0.011791045 0.54806498
ニシノフラワー_R 2317 0.01214132 0.011791045 1.417532108
ニシノフラワー_SR 1213 0.00635624 0.006206897 0.830691896
ニシノフラワー_SSR 73 0.00038253 0.000441176 -1.220059173
ハルウララ_R 2251 0.01179547 0.011791045 0.017902097
バンブーメモリー_R 2256 0.01182167 0.011791045 0.123934674
バンブーメモリー_SSR 95 0.00049781 0.000441176 1.178123046
ヒシアケボノ_R 2254 0.01181119 0.011791045 0.081521643
ヒシアケボノ_SSR 91 0.00047685 0.000441176 0.742089915
ヒシアマゾン_R 2233 0.01170115 0.011791045 -0.363815179
ヒシアマゾン_SR 1241 0.00650297 0.006206897 1.646790502
ビコーペガサス_R 2205 0.01155442 0.011791045 -0.957597608
ビコーペガサス_SSR 76 0.00039825 0.000441176 -0.893034325
ビワハヤヒデ_R 2238 0.01172735 0.011791045 -0.257782602
ビワハヤヒデ_SR 1176 0.00616236 0.006206897 -0.247724119
ファインモーション_R 2256 0.01182167 0.011791045 0.123934674
ファインモーション_SSR 90 0.00047161 0.000441176 0.633081632
フジキセキ_R 2298 0.01204175 0.011791045 1.014608317
フジキセキ_SR 1230 0.00644532 0.006206897 1.326180335
マチカネタンホイザ_R 2222 0.01164351 0.011791045 -0.597086847
マチカネフクキタル_R 2215 0.01160682 0.011791045 -0.745532454
マチカネフクキタル_SR 1190 0.00623572 0.006206897 0.160325184
マヤノトップガン_R 2240 0.01173783 0.011791045 -0.215369571
マヤノトップガン_SR 1209 0.00633528 0.006206897 0.714106381
マルゼンスキー_R 2185 0.01144962 0.011791045 -1.381727914
マンハッタンカフェ_R 2222 0.01164351 0.011791045 -0.597086847
マンハッタンカフェ_SR 1133 0.00593703 0.006206897 -1.501018406
マーベラスサンデー_R 2274 0.01191599 0.011791045 0.50565195
マーベラスサンデー_SR 1149 0.00602088 0.006206897 -1.034676346
ミホノブルボン_R 2267 0.01187931 0.011791045 0.357206342
ミホノブルボン_SR 1193 0.00625144 0.006206897 0.247764321
メイショウドトウ_R 2189 0.01147058 0.011791045 -1.296901853
メイショウドトウ_SR 1192 0.00624620 0.006206897 0.218617942
メジロアルダン_R 2236 0.01171687 0.011791045 -0.300195633
メジロアルダン_SR 1248 0.00653965 0.006206897 1.850815154
メジロドーベル_R 2252 0.01180071 0.011791045 0.039108612
メジロドーベル_SR 1197 0.00627240 0.006206897 0.364349836
メジロパーマー_R 2227 0.01166971 0.011791045 -0.491054271
メジロパーマー_SSR 75 0.00039301 0.000441176 -1.002042608
メジロマックイーン_R 2260 0.01184263 0.011791045 0.208760735
メジロライアン_R 2190 0.01147582 0.011791045 -1.275695337
メジロライアン_SR 1209 0.00633528 0.006206897 0.714106381
メジロライアン_SSR 92 0.00048209 0.000441176 0.851098198
ヤエノムテキ_R 2300 0.01205223 0.011791045 1.057021348
ヤエノムテキ_SSR 80 0.00041921 0.000441176 -0.457001195
ユキノビジン_R 2248 0.01177975 0.011791045 -0.045717449
ユキノビジン_SR 1230 0.00644532 0.006206897 1.326180335
ユキノビジン_SSR 81 0.00042445 0.000441176 -0.347992912
ライスシャワー_R 2242 0.01174831 0.011791045 -0.172956541
桐生院葵_R 2207 0.01156490 0.011791045 -0.915184577
桐生院葵_SR 1089 0.00570647 0.006206897 -2.783459073
駿川たづな_R 2298 0.01204175 0.011791045 1.014608317
駿川たづな_SSR 95 0.00049781 0.000441176 1.178123046

今回サポートカード全種類に対して検定を行っておりますが、実はこれには多重検定の考えを持ち込んだほうがいいかもしれません。
https://www.jstage.jst.go.jp/article/kagakutoseibutsu/51/7/51_483/_pdf
↑にも少し書いてありますが、有意水準0.05で検定を行うと平均値の差は、20回に1回は実際に差がなくとも有意差があると判定されてしまってもおかしくないです。

このような多重性の問題を解決するために有意水準を厳しくとろうと思います。
Bonferroni法でやるとすると

0.05/132=0.00037878787878・・・

を新たな有意水準として設定します。

Zの棄却域ボーダーは±1.96から±3.555になります。

ウイニングチケットRは Z=-2.9086 とかなり小さい値ですが、Bonferroni法で考えるとこれも有意とはとらないことになります。(まぁ仮に有意でもRが少ないのは良いこと)

結論


アプリ ウマ娘プリティーダービーのサポートカードガチャの出現確率は、公式発表されているものと異なる可能性はかなり低い。

今後もサンプル数増やす予定


最後に、Bonferroni法はやや厳しめの方法であるということを記しておきます。
有意水準を厳しく取ると、本当はCygamesが不正しているのにそれを見逃してしまう可能性があります。
しかしこれはサンプル数を増やすことで解決できます。

Bonferroni法以外の方法を使っても良いのですが(面倒ですし)、今の所サンプル数を増やすのが最も簡単そうなので、もっとサンプル数が増えた段階で最終結論を書こうと思っています。

【Switchモンハン自動操作】Arduino Uno R3+Win10

目的

モンハンライズが発売され1カ月近く経ちますが、今回はモンハンのあるルーチンの自動化をやろうと思います。
・オトモの交易船アイテム回収
・オトモ隠密隊アイテム回収
・溶岩洞上位の探索クエ一部採掘(ポイント稼ぎ)

dekirukoto


これらの作業を自動化することにより、ハチミツやマンドラゴラ、魚類などの素材、隠密隊で手に入るモンスター等の素材も手にはいります。
さらにオトモの経験値やカムラポイントも若干ですが増えることが期待できます。

前提知識

メモ帳

プログラミングができる必要はありませんが、メモ帳でコード内の数値をいじったりコピペなどのスキルは必要です。
メモ帳の開き方すら分からないという方は難易度的にこの記事は厳しいかもしれません・・・

コマンドプロンプト、(WSL)

コマンドプロンプト、WSLを使ったことがあるorなんとなく聞いたことがあるなら尚よいです。
コマンドプロンプトはPCからArduinoにプログラムを書き込む際に必要になります。
またArduinoにプログラムを書き込む前の段階で「make」コマンドを使う必要があり、私はWSLでやっちゃっていますが、「make」コマンドを使うだけであれば他にも方法があり各々調べてやりやすい方法をとると良いです。

Arduino

電子工作ができる必要はありませんが、Arduinoがどういう製品なのかは少し知っておく必要があります。
Arduinoには10個以上のpinがついており、これらの動作をプログラム可能。例えば「1秒後にpin10を5Vにして2秒後にpin9を0Vにする」というような動作をさせることができます。

なのでpinにLEDをつないでLチカさせたり、モーターを制御させたりして遊ぶことができます。それが普通の使い方。
457j853e56h

今回SwitchにこのArduino Unoをコントローラーとして認識させて自動操作するのですが、これはやや普通の使い方から外れるので、ちょっと面倒なことをしないといけません。

またArduinoと言っても何種類もあります。
Arduinoシリーズ15種類の違い | まとめ比較表
結論から言うと

Arduino Leonardo

が良いです。
これを使ったやり方もたくさん記事があり、Arduino Unoを使ったやり方より簡単そうです。もしまだ購入してないならArduino Leonardoをおススメします。
私は良く調べずにArduino Unoを買ってしまったので以降はUnoでのやり方を解説します。

USB type

ケーブル類も何種類か必要なので「USBって何?」という方だとちょっときついかもしれません。USB Type-Cなどメジャーな規格は知っておくと良いです。

準備

ハードウェア

Nintendo Switch本体
PC (Windows 10)
Arduino Uno R3
USBケーブル:タイプAオス - タイプBオス
USB Type-Cハブ(なければUSBケーブル:タイプCオス - タイプBオスでも良い)
4sdfb09


ソフトウェア

AutoRaid-1.0.0aw.zip
lufa-d6a7df4f78898957fa6d5b63e62015ce763560d4.zip
dfu-programmer-win-0.7.2.zip

STEP1 ソフトウェアのDL(難易度:易)

・AutoRaid-1.0.0aw

↑ここのAutoWattからDL

・lufa

↑ここからDL(CODE→Download ZIP)

・dfu-programmer-win-0.7.2

↑ここからDL

STEP2 Arduino UnoのHID化(難易度:易)

[qiita]Arduino UNOを使った、キーボード機器製作法
ここを参考にArduino UnoをHID化します。
「ATmega16U2をHIDキーボードデバイスと認識させる方法」
の1~4までをやります。
この時点でPCにつないだArduino Unoが、「不明なデバイス」から「ATmega16U2」と認識されるようになっているはずです。
devicem
つまずきポイントは特になさそうですが、Arduino UnoをPCにつなげる際、ピンをショートさせながらつなげるので手が忙しいという問題はあります。ショートさせるのはドライバーでもシャー芯でもなんでも良いです。
終わったらいったんArduino UnoはPCからはずしておきます。

STEP3 Joystick.cとhexファイルの確認(難易度:易)

先ほどの記事だと、このあとArduino Unoをキーボードに擬態させるため専用のファームウェアをダウンロードしてきてArduinoに書き込むことをしています。
今回私たちはSwitchのコントローラーに擬態させるので、それ用のファームウェア(hexファイル)を自作していく必要があります。
結構大変そうですが、頭のイカれた変人 良い人たちが道筋を用意してくれているのでそれを活用させていただきます。

まず
こちらを参考に
AutoRaid-1.0.0aw.zipとlufa-d6a7df4f78898957fa6d5b63e62015ce763560d4.zipを解凍し、
AutoRaid-1.0.0awフォルダの中のlufaフォルダ(空になっている)に先ほど解凍したlufa-d6・・の中身をうつします。
無題


そしてAutoRaid-1.0.0awフォルダ内に
Joystick.c
Joystick.h
Joystick.hex
などのファイルがあることを確認します。

私も良く分かってはいないですが、ちょっと解説すると

Joystick.cにはポケモンの自動ワット稼ぎコードが記載されている。ここをいじれば任意の自動操作ができる。

Joystick.hにはキーコード(Aボタンは0x04とか)が記載されている。Joystick.cからinclude(参照)されている。これは基本いじらない。

Joystick.hexはJoystick.cをArduinoが読める形式にしたファイル。これを作りたい。

最初の時点で存在しているJoystick.hexはポケモン用のコードになっているので、このままではモンハン自動操作はできません。
なので
「Joystick.cを書き換えJoystick.hexを作ってArduinoに書き込む」ということが必要になってきます。

ここの難易度がやや高めですが、逆にここさえできてしまえばモンハンに限らず色んなソフトで自動操作を実現することができます。

今は私の作ったモンハン自動コードを載せておくのでそれをコピペするだけで一応モンハン自動化はできます。Joystick.cを以下のコードに置き換えてください。


/*
Nintendo Switch Fightstick - Proof-of-Concept

Based on the LUFA library's Low-Level Joystick Demo
    (C) Dean Camera
Based on the HORI's Pokken Tournament Pro Pad design
    (C) HORI

This project implements a modified version of HORI's Pokken Tournament Pro Pad
USB descriptors to allow for the creation of custom controllers for the
Nintendo Switch. This also works to a limited degree on the PS3.

Since System Update v3.0.0, the Nintendo Switch recognizes the Pokken
Tournament Pro Pad as a Pro Controller. Physical design limitations prevent
the Pokken Controller from functioning at the same level as the Pro
Controller. However, by default most of the descriptors are there, with the
exception of Home and Capture. Descriptor modification allows us to unlock
these buttons for our use.
*/

#include "Joystick.h"

typedef enum {
  L_UP,
  L_DOWN,
  L_LEFT,
  L_UPLEFT,     // UP + LEFT
  X,
  Y,
  A,
  B,
  L,
  R,
  ZR,
  ZL,
  PLUS,
  H_UP,
  H_DOWN,
  H_LEFT,
  H_RIGHT,
  NOTHING,
} Buttons_t;

typedef struct {
  Buttons_t button;
  uint16_t duration;
} command;


static const command stepSetupController[] = {
  // ここまでコントローラーを認識させるおまじない
  // Setup controller
  { NOTHING,   30 },
  { A,         39 },
  { B,         49 },
};

static const command stepGotoSyukaijoAndTankue[] = {
  //里内にて里内移動→「集会場」
  { NOTHING,    1 },
  { PLUS,      35 },
  { H_LEFT,    15 },
  { H_DOWN,    15 },
  { H_DOWN,    15 },
  { A,         15 },
  { H_UP,      15 },
  { A,         85 },
  //ミノトにあう
  { L_UPLEFT,  90 },
  { L_LEFT,    15 },
  { NOTHING,   10 },
  { A,         85 },
  //探索クエ選択
  { A,         20 },
  { H_UP,      15 },
  { A,         60 },
  { H_UP,      10 },
  { A,         10 },
  { A,         10 },
  { A,         73 },
  { ZR,        19 },
  { A,       1030 },//クエスト出発、ロード
};

static const command stepSubcamp[] = {
  //サブキャンプへ移動
  { PLUS,      35 },
  { H_RIGHT,   10 },
  { A,         20 },
  { A,         10 },
  { H_DOWN,    10 },
  { A,         10 },
  { A,        360 },
};

static const command stepBISUMASU[] = {
  //下に向かっって3秒
  { L_DOWN,   120 },
  { A,        110 },//ここに採取ポイント
};

static const command stepNENTANSEKI[] = {
  //左に向かっって8秒+上に3秒
  { L_LEFT,   270 },
  { L_UP,      90 },
  { A,          5 },//ここに採取ポイント
  { L_UP,       5 },
  { A,          5 },//ここに採取ポイント
  { L_UP,       5 },
  { A,          5 },//ここに採取ポイント
  { L_UP,       5 },
  { A,        100 },//ここに採取ポイント
};

static const command stepWait4min[] = {
  //適度に屈伸をして全くの停止時間を作らない
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        400 },
  { B,        600 },
};

static const command stepBackToHome[] = {
  { PLUS,      55 },
  { H_RIGHT,   55 },
  { H_DOWN,    10 },
  { H_DOWN,    10 },
  { H_DOWN,    10 },
  { H_DOWN,    10 },
  { A,         55 },
  { H_LEFT,    15 },
  { A,        700 },
  { A,        100 },//これで終了。クエスト帰還まち
  { A,        100 },
  { A,        100 },
  { A,        800 },
  //帰還終了
};

//隠密と交易船の受け取り
static const command stepOtomo[] = {
  { PLUS,      35 },
  { H_LEFT,    15 },
  { H_DOWN,    15 },
  { H_DOWN,    15 },
  { A,         15 },
  { H_DOWN,    15 },
  { H_DOWN,    15 },
  { H_DOWN,    15 },
  { A,         85 },//ルームサービスにあう
  { L_LEFT,    50 },
  { A,         55 },
  { H_DOWN,    15 },
  { A,         65 },
  { A,        125 },
  { H_UP,      25 },
  { A,         65 },
  { A,         29 },//今度は依頼
  { X,         20 },
  { A,         40 },
  { H_UP,      20 },
  { A,         15 },
  { A,         10 },
  { A,         10 },
  { A,         10 },
  { A,         10 },
  { A,         10 },
  { A,         10 },
  { A,         10 },
  { A,         10 },//決定
  { A,         30 },//はい
  { B,         20 },//もどる
  { H_DOWN,    20 },//交易船
  { A,         40 },
  { A,         20 },
  { H_UP,      20 },
  { A,         20 },
  { H_UP,      20 },
  { A,         20 },
  { B,         20 },
  { B,         20 },
  { B,         50 },
};


// Main entry point.
int main(void) {
  // We'll start by performing hardware and peripheral setup.
  SetupHardware();
  // We'll then enable global interrupts for our use.
  GlobalInterruptEnable();
  // Once that's done, we'll enter an infinite loop.
  for (;;)
  {
    // We need to run our task to process and deliver data for our IN and OUT endpoints.
    HID_Task();
    // We also need to run the main USB management task.
    USB_USBTask();
  }
}

// Configures hardware and peripherals, such as the USB peripherals.
void SetupHardware(void) {
  // We need to disable watchdog if enabled by bootloader/fuses.
  MCUSR &= ~(1 << WDRF);
  wdt_disable();

  // We need to disable clock division before initializing the USB hardware.
  clock_prescale_set(clock_div_1);
  // We can then initialize our hardware and peripherals, including the USB stack.

  #ifdef ALERT_WHEN_DONE
  // Both PORTD and PORTB will be used for the optional LED flashing and buzzer.
  #warning LED and Buzzer functionality enabled. All pins on both PORTB and \
           PORTD will toggle when printing is done.
  DDRD  = 0xFF; //Teensy uses PORTD
  PORTD =  0x0;
                  //We'll just flash all pins on both ports since the UNO R3
  DDRB  = 0xFF; //uses PORTB. Micro can use either or, but both give us 2 LEDs
  PORTB =  0x0; //The ATmega328P on the UNO will be resetting, so unplug it?
  #endif
  // The USB stack should be initialized last.
  USB_Init();
}

// Fired to indicate that the device is enumerating.
void EVENT_USB_Device_Connect(void) {
  // We can indicate that we're enumerating here (via status LEDs, sound, etc.).
}

// Fired to indicate that the device is no longer connected to a host.
void EVENT_USB_Device_Disconnect(void) {
  // We can indicate that our device is not ready (via status LEDs, sound, etc.).
}

// Fired when the host set the current configuration of the USB device after enumeration.
void EVENT_USB_Device_ConfigurationChanged(void) {
  bool ConfigSuccess = true;

  // We setup the HID report endpoints.
  ConfigSuccess &= Endpoint_ConfigureEndpoint(JOYSTICK_OUT_EPADDR, EP_TYPE_INTERRUPT, JOYSTICK_EPSIZE, 1);
  ConfigSuccess &= Endpoint_ConfigureEndpoint(JOYSTICK_IN_EPADDR, EP_TYPE_INTERRUPT, JOYSTICK_EPSIZE, 1);

  // We can read ConfigSuccess to indicate a success or failure at this point.
}

// Process control requests sent to the device from the USB host.
void EVENT_USB_Device_ControlRequest(void) {
  // We can handle two control requests: a GetReport and a SetReport.

  // Not used here, it looks like we don't receive control request from the Switch.
}

// Process and deliver data from IN and OUT endpoints.
void HID_Task(void) {
  // If the device isn't connected and properly configured, we can't do anything here.
  if (USB_DeviceState != DEVICE_STATE_Configured)
    return;

  // We'll start with the OUT endpoint.
  Endpoint_SelectEndpoint(JOYSTICK_OUT_EPADDR);
  // We'll check to see if we received something on the OUT endpoint.
  if (Endpoint_IsOUTReceived())
  {
    // If we did, and the packet has data, we'll react to it.
    if (Endpoint_IsReadWriteAllowed())
    {
      // We'll create a place to store our data received from the host.
      USB_JoystickReport_Output_t JoystickOutputData;
      // We'll then take in that data, setting it up in our storage.
      while(Endpoint_Read_Stream_LE(&JoystickOutputData, sizeof(JoystickOutputData), NULL) != ENDPOINT_RWSTREAM_NoError);
      // At this point, we can react to this data.

      // However, since we're not doing anything with this data, we abandon it.
    }
    // Regardless of whether we reacted to the data, we acknowledge an OUT packet on this endpoint.
    Endpoint_ClearOUT();
  }

  // We'll then move on to the IN endpoint.
  Endpoint_SelectEndpoint(JOYSTICK_IN_EPADDR);
  // We first check to see if the host is ready to accept data.
  if (Endpoint_IsINReady())
  {
    // We'll create an empty report.
    USB_JoystickReport_Input_t JoystickInputData;
    // We'll then populate this report with what we want to send to the host.
    GetNextReport(&JoystickInputData);
    // Once populated, we can output this data to the host. We do this by first writing the data to the control stream.
    while(Endpoint_Write_Stream_LE(&JoystickInputData, sizeof(JoystickInputData), NULL) != ENDPOINT_RWSTREAM_NoError);
    // We then send an IN packet on this endpoint.
    Endpoint_ClearIN();
  }
}


#define ECHOES 2
int echoes = 0;
USB_JoystickReport_Input_t last_report;

int report_count = 0;
int hold_LX = STICK_CENTER;
int hold_LY = STICK_CENTER;
int hold_RX = STICK_CENTER;
int hold_RY = STICK_CENTER;
int bufindex = 0;
int bufindexloop = 0;//もどった回数
int duration_count = 0;



//サブキャンプ→ビスマス→サブキャンプ→燃炭石のルーチン
void SBSNRutin(int *starti,int *endi,command *cm_tmp){
	//サブキャンプへ移動
	*starti=*endi;
	*endi+=sizeof(stepSubcamp) / sizeof(stepSubcamp[0]);
	if ((bufindex>=*starti)&(bufindex<*endi)){
		*cm_tmp=stepSubcamp[bufindex-*starti];
		return;
	}
	//ビスマス鉱石採取
	*starti=*endi;
	*endi+=sizeof(stepBISUMASU) / sizeof(stepBISUMASU[0]);
	if ((bufindex>=*starti)&(bufindex<*endi)){
		*cm_tmp=stepBISUMASU[bufindex-*starti];
		return;
	}
	//サブキャンプへ移動
	*starti=*endi;
	*endi+=sizeof(stepSubcamp) / sizeof(stepSubcamp[0]);
	if ((bufindex>=*starti)&(bufindex<*endi)){
		*cm_tmp=stepSubcamp[bufindex-*starti];
		return;
	}
	//燃炭石
	*starti=*endi;
	*endi+=sizeof(stepNENTANSEKI) / sizeof(stepNENTANSEKI[0]);
	if ((bufindex>=*starti)&(bufindex<*endi)){
		*cm_tmp=stepNENTANSEKI[bufindex-*starti];
		return;
	}
	
	return;
}

//現在のbufindexからコマンドを返す
command MainRutin()
{
	int starti=0;
	int endi=0;
	command cm_tmp;
	cm_tmp.duration=0;//0はnullという意味。
	
	//コントローラー認識ステップ
	starti=endi;
	endi+=4;
	if (bufindex<4){
		return stepSetupController[bufindex];
	}
	
	//集会場クエ受注出発
	starti=endi;
	endi+=sizeof(stepGotoSyukaijoAndTankue) / sizeof(stepGotoSyukaijoAndTankue[0]);
	if ((bufindex>=starti)&(bufindex<endi)){
		return stepGotoSyukaijoAndTankue[bufindex-starti];
	}
	
	//クエ中、炭鉱
	SBSNRutin(&starti,&endi,&cm_tmp);
	if (cm_tmp.duration!=0)return cm_tmp;
	
	//4分まつ
	starti=endi;
	endi+=sizeof(stepWait4min) / sizeof(stepWait4min[0]);
	if ((bufindex>=starti)&(bufindex<endi)){
		return stepWait4min[bufindex-starti];
	}
	
	//クエ中、炭鉱
	SBSNRutin(&starti,&endi,&cm_tmp);
	if (cm_tmp.duration!=0)return cm_tmp;
	
	//帰還
	starti=endi;
	endi+=sizeof(stepBackToHome) / sizeof(stepBackToHome[0]);
	if ((bufindex>=starti)&(bufindex<endi)){
		return stepBackToHome[bufindex-starti];
	}
	
	//帰還したら次のループへ
	if (bufindex>=endi){
		bufindex=4;
		bufindexloop++;
	}
	return stepGotoSyukaijoAndTankue[0];
}


//おとも
command OtomoRutin(){
	if (bufindex<41 +4){
		return stepOtomo[bufindex-4];
	}else{
		bufindex=4;
		bufindexloop++;
	}
	return stepGotoSyukaijoAndTankue[0];
}


void GetNextReport(USB_JoystickReport_Input_t* const ReportData) {
  // Prepare an empty report
  memset(ReportData, 0, sizeof(USB_JoystickReport_Input_t));
  ReportData->LX = STICK_CENTER;
  ReportData->LY = STICK_CENTER;
  ReportData->RX = STICK_CENTER;
  ReportData->RY = STICK_CENTER;
  ReportData->HAT = HAT_CENTER;

  //Hold stick
  ReportData->LX = hold_LX;
  ReportData->LY = hold_LY;
  ReportData->RX = hold_RX;
  ReportData->RY = hold_RY;

  // Repeat ECHOES times the last report
  if (echoes > 0)
  {
    memcpy(ReportData, &last_report, sizeof(USB_JoystickReport_Input_t));
    echoes--;
    return;
  }
  //次のコマンドをgetしてくる
  command cmd;
  
  if (bufindexloop%6==5){
	  cmd=OtomoRutin();
  }else{
	  cmd=MainRutin();
  }
  
  Buttons_t bt;//step[bufindex].button
  bt=cmd.button;
  
  switch (bt)
  {
	case L_UP:
	  ReportData->LY = STICK_MIN;
	  break;

	case L_LEFT:
	  ReportData->LX = STICK_MIN;
	  break;

	case L_DOWN:
	  ReportData->LY = STICK_MAX;
	  break;
	  
	case L_UPLEFT:
	  ReportData->LY = STICK_MIN;
	  ReportData->LX = STICK_MIN;
	  break;

	default:
	  break;
  }
	  
  if (duration_count>5)bt=NOTHING;//ボタン系は125ms以上おさないで残りは待ち
  
  switch (bt)
  {
	case A:
	  ReportData->Button |= SWITCH_A;
	  break;

	case B:
	  ReportData->Button |= SWITCH_B;
	  break;

	case X:
	  ReportData->Button |= SWITCH_X;
	  break;

	case Y:
	  ReportData->Button |= SWITCH_Y;
	  break;

	case R:
	  ReportData->Button |= SWITCH_R;
	  break;

	case ZR:
	  ReportData->Button |= SWITCH_ZR;
	  break;

	case ZL:
	  ReportData->Button |= SWITCH_ZL;
	  break;

	case PLUS:
	  ReportData->Button |= SWITCH_PLUS;
	  break;

	case H_UP:
	  ReportData->HAT = HAT_TOP;
	  break;

	case H_DOWN:
	  ReportData->HAT = HAT_BOTTOM;
	  break;
	  
	case H_LEFT:
	  ReportData->HAT = HAT_LEFT;
	  break;
	  
	case H_RIGHT:
	  ReportData->HAT = HAT_RIGHT;
	  break;
	default:
	  break;
  }

  duration_count++;
  if (duration_count > cmd.duration)
  {
    bufindex++;
    duration_count = 0;
  }

  // Prepare to echo this report
  memcpy(&last_report, ReportData, sizeof(USB_JoystickReport_Input_t));
  echoes = ECHOES;
}



私の書いたJoystick.cのコードに関してはApache License 2.0とします。(改変、再公開などご自由にお使い下さい)

このコードでは基本的に以下の行動を繰り返します。
・①カムラの里で溶岩洞(上位)の探索クエを受注、出発
・②溶岩洞でサブキャンプ1に移動、周辺のビスマス光石や燃石炭を採取
・③5分経過したら帰還
・①~③を5回施行後、自宅のルームサービスから隠密隊のアイテム回収、隠密隊出発、交易船のアイテム回収

STEP4 makeコマンドを使えるように設定する(難易度:高)

私のやり方(WSLを使ったやり方)を書いておきます。
WSLは入っているものとします。(まだならhttps://qiita.com/matarillo/items/61a9ead4bfe2868a0b86 こういうのを参考に)

WSLを起動
ubu

すると黒い画面がでます。
ここにはlinuxコマンドを打って操作します。

まず(やらなくても良いですがパッケージ更新したければ)
sudo apt update
sudo apt upgrade
でパッケージを更新

次にgcc makeコマンドを使えるよう
sudo apt install build-essential

essensialコマンド2
としてインストール

次に
cd /mnt/z/AutoRaid-1.0.0aw

これで先ほど解凍してできたJoystick.cのある階層に現在地を移動させます。
「Joystick.cのある階層」を一字一句間違いなく指定しないとエラーがでて先に進めません。
ここがつまづきポイントになりそうですが、
例えば私なら最初にJoystick.cのあるところを開いておき、
cd説明

矢印の部分をクリック

cd説明2

こうなるのでこれをコピーすれば
Z:\AutoRaid-1.0.0aw
という文字が得られます。

Zを小文字のzにして:を消して、\(円マーク)を/(バックスラッシュ)にして
cd /mnt/z/AutoRaid-1.0.0aw
先のこのコマンドがでできたというわけですね・・・

普通Cドライブ上で作業することが多いので皆さんのほうではZはCになっているかと思います。(私はramdisk上でやってるのでZになってます)

ちゃんと階層が移動できたか確認するために

ls

と打ってみましょう。

ls
このように
Joystick.cを含んだファイルの一覧が表示されればokです。

そこまでできたら

make
とコマンドを打ちます。
すると恐らく


「致命的エラー: avr/io.h: そのようなファイルやディレクトリはありません」
「make: avr-gcc: コマンドが見つかりませんでした」
「make: *** [lufa/LUFA/Build/DMBS/DMBS/gcc.mk:167: build_begin] エラー 127」

といったようなエラーがでるので

sudo apt-get install avr-libc gcc-avr

で足りないパッケージをインストールして
再度makeを実行

すると
make後

こんなふうに大量のメッセージが表示されますが、赤い文字でerrorとか書いてなければ大丈夫!

今の操作でhexファイルが生成されたはずです。
windowsのエクスプローラーにもどり
makehex確認

こんな感じに更新日時が現在の時刻になっているはずです。
もしなっていなければJoystick.hexが生成されなかったということなので、どこかがまずかったということです。頑張ってコマンド見直すなりググって解決して下さい・・・。

ちなみに黒い画面で一回打ったコマンドは、↑↓キーを押すことで履歴をだせるので再度同じのを打たなくても済みます。

STEP5 Arduinoに書き込み(難易度:中)

これを参考にArduinoをDFUモードでPCに接続します。
このとき必ずDFUモードでつながないといけないので注意してください。

以下のサイトも参考にしつつ

まずはcmd(コマンドプロンプト)を起動します。
次にdfu-programmer.exeがある階層にcdで移動します。

cd /d Z:/dfu-programmer-win-0.7.2

例によってここも正しい階層を一字一句間違いなく指定しないとエラーで先に進めません。
cddfuprog
私の場合はZ:/dfu-programmer-win-0.7.2のフォルダの中にdfu-programmer.exeがあるのでこのようなコマンドになっています。

次に先ほど作成した
Joystick.hex
をdfu-programmer.exeと同じフォルダ内にコピーします。
hex移動

次にコマンドプロンプトで

dfu-programmer.exe atmega16u2 erase

と打って実行します。
エラーがでていたらArduinoがDFUモードでつながってないということなのでさしなおしてください。

次に
dfu-programmer.exe atmega16u2 flash Joystick.hex

でhexファイルの情報をArduinoに書き込みます。
arwrtn

こんな感じの出力になるはず。
これにて完了。ArduinoをPCから外します。

STEP6 Arduino UnoをSwitchにつなげる(難易度:易)

これは完成形を見たほうがはやいでしょう。
bg354556he564


事前準備(モンハン側)

つないだ瞬間から自動操作が始まります。
なので開始するタイミングが地味に重要で、NPCと会話中につなぐとプラスボタンを押したのにメニューが開けず、結果想定しない動作が行われることになります。画面キャプチャ&フェーズ認識をしてるわけではないので、想定しない画面が出ても順番通りにボタンが入力され続けるので注意です。

他にも自動操作前にしておく必要のある注意点があるのですが全部あげておくと

・自動操作開始のタイミングは、カムラの里内でメニュー(プラスボタン)が開ける状態であること

5

・溶岩洞のサブキャンプを解放させておくこと

サブキャンプ1だけ解放してれば多分バグりませんが検証してないです。
no title

・交易船で3枠とも依頼しておくこと

1枠でも多分バグりませんが検証してないです。
3

・隠密隊は依頼した状態にしておくこと

依頼してない状態だとアイテム回収の時点でバグります。
2

・隠密隊用のオトモアイルーを4匹以上準備しておき、オーダー画面で一番上に隠密隊用のアイルーが選べるようにしておくこと

ここは地味にAボタンが10連続で押されるところです
1


完成!


実演動画(2番目のリンク)↓


なお自動操作を終了する場合はArduinoのケーブルを引っこ抜くしかありません。
大前提ですが、自動操作は自己責任で行って下さい。

リミテーション

今回の手法にはいろいろ制限があります。
原因ははっきりわかりませんが、あまり大量のメモリを使うプログラムだと、コンパイルがうまく行ったとしてもArduinoが予期しない動作を起こす場合がありました。
例えば
  { PLUS,      55 },
  { H_RIGHT,   55 },
  { H_DOWN,    10 },
  { H_DOWN,    10 },
  { H_DOWN,    10 },
  { H_DOWN,    10 },
  { A,         55 },
  { H_LEFT,    15 },
・・・・と、このコマンド数がだいたい200を超えてくるとまともに動かないという事象が起こりました。
Arduinoの問題なのかは不明ですが、どちらにせよこのまま動作を継ぎ足ししてはすぐ限界がきてしまい、あまり複雑な行動を取らせることは無理なようです。
またキャプチャーボードで現在の画面の状況をリアルタイムで監視しているわけではないので、ラグなどの原因でボタン入力のタイミングがずれるとその後が悲惨なことになります。

さらに実際の動作確認デバッグのとき、Joystick.cのプログラムを書き換えてからArduinoに書き込みSwitchに接続、という繰り返しをするも少々だるいです。

https://www.youtube.com/watch?v=hDI7YCTNfiA
こちらを見るにキャプチャーボードでPCに画面取得→PCで操作コマンド指示出し→BluetoothでSwitchへ
という形式がより発展性がありそうと感じました。
そのうちチャレンジしてみたいと思います。

いかがでしたか?参考文献

パディングなしでバンクコンフリクトを解消する方法 CUDA 行列転置

シェアードメモリとバンク

GPUを使った行列転置でほぼ頻出話題のバンクコンフリクト(Bank Conflict)とパディング(Padding)。前提知識としてCUDA等のGPUプログラミングでシェアードメモリ(共有メモリ)を明示的に扱う際に、Bankを意識しないと遅くなっちゃいますよという話があります。
bank

シェアードメモリは4byte刻みで32個のBankに分かれています。
例えばthread 0がSMEM[0]、thread 1がSMEM[32]にアクセスするようなプログラムだと(SMEMは4byte型とします)、どちらも同じBank0にアクセスすることになるのでリプレイ(replay)が発生して遅くなります。

行列転置バンクコンフリクトありver

つまり普通にシェアードメモリを使って行列転置をしようとすると
jurai

こんな感じにShared Memoryから値を読み込む際にバンクコンフリクトが起こってしまっています。
ちなみにここではわかりやすくBankは0~3までとしています。

行列転置パディングありver

そこでパディングをいれることで参照Bankがずれるのでこれを解消できます。そこそこ有名なテクニックです。
Kepler GPUアーキテクチャとプログラム最適化
プログラムを高速化する話Ⅱ 〜GPGPU編〜

sample0

概念図
juraip

これでShared Memoryに書き込むときも読み込むときも、Bank競合が起きてないことが分かります。
でも貴重なShared Memoryに未使用領域があるのはちょっと気持ち悪くないですか。

行列転置パディングなしver

上の概念図がすでに大きなヒントでした。
sinki
これでも読み書きともバンクコンフリクトは起こってないですね。
メモリアクセスの仕方としては
code0

こんな感じにx方向のズレ幅をyごとにずらしてやればいいです。

一応コードの全貌を載せておきます。

#include<stdio.h>
#include<cuda.h>
#include<cuda_runtime.h>

// --matrix size --
const int nimax=8192,njmax=8192;
// -- the number of threads per block --
const int BS=32;
__device__ float idata[nimax][njmax],odata[nimax][njmax];

__global__ void transposeNaive(){
  int ni,nj;
  ni=blockIdx.y*blockDim.y+threadIdx.y;
  nj=blockIdx.x*blockDim.x+threadIdx.x;
  odata[nj][ni]=idata[ni][nj];
  return;
}

__global__ void transposeShared(){
  int ni,nj;
  __shared__ float tile[BS][BS];
  ni=blockIdx.y*blockDim.y+threadIdx.y;
  nj=blockIdx.x*blockDim.x+threadIdx.x;
  tile[threadIdx.y][threadIdx.x]=idata[ni][nj];
  __syncthreads();
  ni=blockIdx.x*blockDim.x+threadIdx.y;
  nj=blockIdx.y*blockDim.y+threadIdx.x;
  odata[ni][nj]=tile[threadIdx.x][threadIdx.y];
  return;
}

__global__ void transposeSharedNobankconf(){
  int ni,nj;
  __shared__ float tile[BS][BS+1];
  ni=blockIdx.y*blockDim.y+threadIdx.y;
  nj=blockIdx.x*blockDim.x+threadIdx.x;
  tile[threadIdx.y][threadIdx.x]=idata[ni][nj];
  __syncthreads();
  ni=blockIdx.x*blockDim.x+threadIdx.y;
  nj=blockIdx.y*blockDim.y+threadIdx.x;
  odata[ni][nj]=tile[threadIdx.x][threadIdx.y];
  return;
}


__global__ void transposeSharedNobankconf_nopading(){
  int ni,nj;
  __shared__ float tile[BS][BS];
  ni=blockIdx.y*blockDim.y+threadIdx.y;
  nj=blockIdx.x*blockDim.x+threadIdx.x;
  tile[threadIdx.y][(threadIdx.x+threadIdx.y)%BS]=idata[ni][nj];
  __syncthreads();
  ni=blockIdx.x*blockDim.x+threadIdx.y;
  nj=blockIdx.y*blockDim.y+threadIdx.x;
  odata[ni][nj]=tile[threadIdx.x][(threadIdx.x+threadIdx.y)%BS];
  return;
}


void transpose(float hidata[][njmax],float hodata[][njmax]){
  int ni,nj;
  for(ni=0;ni<nimax;ni++){
    for(nj=0;nj<njmax;nj++){
      hodata[nj][ni]=hidata[ni][nj];
    }
  }
  return;
}

void check_mat(float C[][njmax],float CC[][njmax]){
  int ni,nj,flg;
  flg=0;
  for(ni=0;ni<nimax;ni++){
    for(nj=0;nj<njmax;nj++){
      if(fabs(C[ni][nj]-CC[ni][nj])>1.0e-8){
        flg=1;
        printf("%d %d %lf %lf\n",ni,nj,C[ni][nj],CC[ni][nj]);
      }
    }
  }
  if(flg==1){
    printf("Calculation error.\n");
  }else{
    printf("OK.\n");
  }
  return;
}




float hidata[nimax][njmax],hodata[nimax][njmax],htdata[nimax][njmax];

int main(){
  int ni,nj;
  dim3 grid,blck;
  grid.x=nimax/BS; grid.y=njmax/BS;
  blck.x=BS; blck.y=BS;
  // -- set initial value --
  srand(248309);
  
  
  for(ni=0;ni<nimax;ni++)
  {
    for(nj=0;nj<njmax;nj++)
    {
      hidata[ni][nj]=(float)rand()*1.1;
    }
  }
  
  
  cudaMemcpyToSymbol(idata,hidata,nimax*njmax*sizeof(float));
  transpose(hidata,htdata);
  // -- 1. transpose by simple kernel --
  transposeNaive<<<grid,blck>>>();
  transposeNaive<<<grid,blck>>>();
  cudaMemcpyFromSymbol(hodata,odata,nimax*njmax*sizeof(float));
  check_mat(htdata,hodata);
  
  // -- 2. transpose with shared memory --
  transposeShared<<<grid,blck>>>();
  transposeShared<<<grid,blck>>>();
  cudaMemcpyFromSymbol(hodata,odata,nimax*njmax*sizeof(float));
  check_mat(htdata,hodata);
  
  // -- 3. transpose with shared memory and without bank conflict --
  transposeSharedNobankconf<<<grid,blck>>>();
  transposeSharedNobankconf<<<grid,blck>>>();
  cudaMemcpyFromSymbol(hodata,odata,nimax*njmax*sizeof(float));
  check_mat(htdata,hodata);

  // -- 4. transpose with shared memory and without bank conflict and nopading --
  transposeSharedNobankconf_nopading<<<grid,blck>>>();
  transposeSharedNobankconf_nopading<<<grid,blck>>>();
  cudaMemcpyFromSymbol(hodata,odata,nimax*njmax*sizeof(float));
  check_mat(htdata,hodata);
  

  cudaDeviceReset();
  
  return 0;
}


8192×8192要素の行列の転置をするコードとなっています。

速度比較


normal

1.transposeNaive:シェアードメモリすら使ってない。グローバルメモリへコアレスアクセスできてないので一番遅い
2.transposeShared:シェアードメモリ内でバンクコンフリクト発生
3.transposeNobankconf:パディング入れてるやつ
4.transposeNobankconf_nopading:パディング入れてないやつ


まずグローバルメモリにコアレスアクセスできてない1番、これは論外です。次に2,3のバンクコンフリクトのあるなし、これだけで2倍違うことがわかります。
今回考案した4番は、速度的に3番に劣ってないことがわかります。

で、これは何の役に立つんですか?

さきほど書いたよう、パディング入りは無駄なメモリ領域を確保しています。なのでOccupancyが100%になる前にシェアードメモリ容量が律速になるような問題では以下のようなことが発生します。

1Blockあたり16.25KBのシェアードメモリを使うプログラムの場合
nvvp2

1Blockあたり16.0KBのシェアードメモリを使うプログラムの場合
nvvp

このようにOccupancyが上がることが期待できます。

なお、上記の行列転置のコードのカーネルはすべてOccupancy100%です。(Shared Memoryを使い切る前にOccupancy100%になる)

CUDA Unified Memoryを扱う方法 (CとPyCUDAのコード)

最小限のコードであまり落ちてなかったので自分のメモ用としても

まずはCUDA版(nvccでコンパイルするやつ)
#include <stdio.h>
__global__ void VecAdd(float* A, float* B, float* C) {
	int id = 256 * blockIdx.x + threadIdx.x;
	C[id]=A[id]+B[id];
}

int main() {
	int N = 1024;
	// Allocate 3 arrays on GPU
	float *d_A, * d_B, * d_C;
	cudaMallocManaged(&d_A, N * sizeof(float));
	cudaMallocManaged(&d_B, N * sizeof(float));
	cudaMallocManaged(&d_C, N * sizeof(float));
	// CPU init
	for(int i=0;i<N;i++){
		d_A[i]=d_B[i]=1.0*i;
	}

	//gpu kernel
	VecAdd <<<N/256, 256>>> (d_A, d_B, d_C);
	cudaDeviceSynchronize();//wait

	for(int i = 0;i<N;i++){
		printf("%f ",d_C[i]);
	}
	
	cudaFree(d_A);
	cudaFree(d_B);
	cudaFree(d_C);
	return 0;
}

つぎにPyCUDA版(2020.1)
from pycuda.autoinit import context
import pycuda.driver as cuda
from pycuda.compiler import SourceModule
import numpy as np

mod = SourceModule("""
__global__ void doublify(float *a)
{
    a[threadIdx.x] *= 2;
}
""")
doublify = mod.get_function("doublify")

a = cuda.managed_empty(shape=12, dtype=np.float32, mem_flags=cuda.mem_attach_flags.GLOBAL)
a[:] = np.linspace(0, 11, len(a)) # Fill array on host
doublify(a, grid=(1,1), block=(len(a),1,1))
context.synchronize() # Wait for kernel completion before host access
print(a)
del a #これでガベージコレクションに削除してもらうことで、GPUメモリも解放される

ほんの19行でかけた aにCPUからもGPUからもアクセスできている。
プロフィール

toropippi

記事検索
アクセスカウンター

    QRコード
    QRコード
    • ライブドアブログ