クソ診断紹介4「関東地方ダーツの旅」

この記事は、クソ診断 Advent Calendar 2019 の23日目の記事です。

1. はじめに

皆さんこんばんは。4週間にわたり続けたクソ診断紹介も最終回となってしまいました。今回は、「旅に出たいものの行先が思いつかない」といったときに使える(?)診断「関東地方ダーツの旅」を紹介したいと思います。
この診断では、関東地方全域から1つの地点をランダムに抽選し、その場所を示すGoogle Mapのリンクが表示されます。
診断テキストには、東京駅を基準とした東西方向および南北方向の距離が表示されるので、大まかな場所の予想を立てた後でGoogle Mapを確認してみるのも面白いかと思います。
まだこの診断を回したことがない方は、さっそく以下のリンクから挑戦してみましょう。

shindanmaker.com

行き先のない旅

皆様は、旅行を思い立ったとき、事前にどれだけ下調べを行うでしょうか。
旅行の目的が明確な場合、その達成のために観光地の営業時間や交通機関の時刻表を調べるといった事前計画は重要です。一方、綿密に計画を立てた旅行は、計画を消化する「流れ作業」に終始する傾向にあることも否めません。
旅に出たいものの具体的な行き先は思いつかない、または事前計画を立てず感性の赴くまま自由に行動したい場合、行き先がランダムに決定された方がかえって好都合なことも多いように思えます。

行き先を定めない旅行に対する需要を汲み取ってか、数多くの旅行会社が、出発当日まで行き先が明かされない「ミステリーツアー」を企画しています。また、日本航空では、4つの行き先候補地が提示された状態で特典航空券を申し込み、後日最終的な行き先がランダムに決まる「どこかにマイル」サービスを提供しています。

ご予約の際に、4つの行き先候補地をJALからご提案。そのなかからお申し込みから3日以内に、決定した行き先をお知らせします。いつもとは違うワクワクする旅の出会いや、偶然が生み出す発見の旅をお楽しみください。

どこかにマイル - JALマイレージバンク

テレビ番組内での旅行企画

いくつかのバラエティ番組では、サイコロやダーツを使い、行き先を偶然性に任せて決める旅行企画が度々行われ、人気を博しています。

  • 「サイコロの旅」(水曜どうでしょう)
    各サイコロの目に交通機関と行き先の組み合わせが割り当てられます。乗り継ぎのたび、サイコロを振って出た目に対応する交通機関への乗車を繰り返すことで、スタートの東京からゴールの札幌を目指す企画です。

  • 「日本列島ダーツの旅」(1億人の大質問!?笑ってコラえて!)
    区画ごとに分けられた日本地図にダーツの矢を投げ、当たった区画内の町村を実際に訪問する企画です。 区画の多くは南北約30km、東西約40kmの長方形となっている為、比較的行先の融通が利きやすそうにみえます。

「関東地方ダーツの旅」は、この「日本列島ダーツの旅」から着想を得て作成した診断となりますが、以下の点において着想元の企画とは異なります。

  • 対象範囲が関東地方に限定されること
  • 行先が区画ではなく、ピンポイントで指定されること

2. 「ダーツの的」を作る

この章では、「ダーツの的」に相当するデータの作成方法について、試行錯誤の過程を踏まえながら説明していきます。
より良い診断結果を得るため、以下の3点を目標として設定しました。

  • 目標1: 結果が他ユーザと被らないようパターン数を十分多くすること
  • 目標2: 他地方や海域を出力しないこと
  • 目標3: 可能な限り多くのエリアを結果候補に含めること

さて、診断メーカーのシステムでは、リストに以下の厳しい制約条件が課せられています。

  • リストの数は最大10個
  • 各リストに入れられる最大の値の数は999個

果たして、制約条件の範囲内で目標1~目標3を全て満たすような「ダーツの的」を作成することはできるのでしょうか。

試行錯誤の過程

以降、X および Y はそれぞれ診断結果の東西方向および南北方向の座標を表すものとします。

手法1: リストに格納した X および Y をそのまま結果表示に用いる

関東地方の内部の点をリストに格納するだけの最もシンプルな方法です。しかしこれでは、リストの制約条件より結果パターンの最大数は9990通りにしかならず、結果が他ユーザと容易に被ってしまいます

手法2: X および Y に値域を設定し、その範囲内で独立に抽選を行う

このとき、診断結果の座標が取りうる範囲の軌跡は長方形または正方形となります。

また、結果パターン数は(Xがとりうる値の場合の数) × (Yがとりうる値の場合の数)となり、手法1と比較して格段に結果パターン数を増加させることができます。

例えば、あるリストにkm単位での座標の整数部分(-100km~+99km, 1km間隔)を、別のリストに小数部分(5m~995m, 10m間隔)を格納し抽選に利用すると、XおよびYがとりうる値の場合の数はそれぞれ2万通りとなるため、約4億通りの結果パターンを得ることができます。これだけの結果パターンがあれば、診断結果が他ユーザと被る可能性を心配する必要はないでしょう。

診断作成当時(2016/09/24)はこの手法が採用されており、東京駅を中心とした一辺200kmの正方形の内部を抽選対象領域としていました。しかし、この手法は高確率で関東地方以外の場所が抽選されるという問題を抱えていました。

診断作成当時の抽選対象領域と関東地方の位置関係について、図1に示します。 抽選対象領域40,000km^{2}16,483km^{2}は関東地方外(山梨県静岡県、または海域)となっており、抽選結果が関東地方内に収まる確率は58.8%ほどしかありませんでした。

また、北関東3県の一部地域は東京駅から北に100km以上離れていることもあり、関東地方32,420km^{2}のうち抽選対象領域がカバーする面積は23,517km^{2}と、割合にして72.5%に過ぎませんでした。

f:id:yryrrrrryryr:20191223234220p:plain:w400
図1 手法2における抽選対象領域と関東地方との位置関係

なお、関東地方外が抽選対象となる問題については、診断結果の座標が取りうる範囲の軌跡が関東地方の領域内に収まるよう X および Y の値域を調整することで解決が可能です。しかし、単一の正方形領域のみで抽選対象領域を表現しようとした場合、「抽選結果が関東地方内に収まる確率」と「抽選対象領域がカバーする面積の割合」はトレードオフの関係となってしまいます。

手法3: 合同な正方形を、関東地方の領域内に敷き詰める

本手法では、抽選対象領域を複数の正方形領域に拡張することを考えます。具体的には、図2のように合同な正方形(以下、グリッド)を関東地方の領域内にすき間なく敷き詰めていきます。このとき、抽選対象領域は関東地方の領域の内部にあるため、出力される抽選結果は関東地方の領域内であることが保証されます

f:id:yryrrrrryryr:20191224013043p:plain:w400
図2 2kmグリッドで構成された抽選対象領域

なお、グリッドの作成には、QGISというオープンソースの地理情報空間システムソフトウェアを使用しました。
https://www.qgis.org/ja/site/about/index.html

手法3における抽選方法の概要について、図3に示します。 グリッドの絶対位置→グリッド内の相対位置 の順番で抽選が行われます。

f:id:yryrrrrryryr:20191224010153p:plain:w400
図3 手法3における抽選方法の概要

最初に、グリッドリストからグリッドを1つ抽選し、その中心座標(X_0,Y_0)を取得します。

次に、グリッド内の位置を抽選するため、(-1,1)の範囲をとる一様乱数リストを作成し、そこから2つの独立した値(p,q)を抽選します。

グリッドの辺長をLとしたとき、求める(X,Y)は、

 \large X = X_0 + p×\frac{L}{2}

 \large Y = Y_0 + q×\frac{L}{2}

となります。

しかし、リストの制約条件から、最大でも8,991個のグリッドデータ(乱数抽選用リストを除いたリスト9個分)しか格納できないことに注意が必要です。

関東地方の面積は 32,420km^{2}、また格納可能なグリッドデータの総面積は8,991L^{2} なので、関東地方全体をグリッドデータで表現するためにはおおむねL≧1.9kmに設定する必要があります。

例えば、図2のように2kmグリッドで抽選対象領域を構成した場合、グリッドデータの数は7,621個、総面積は30,484km^{2}となります。抽選対象領域の関東地方面積カバー率は94.0%と、手法2と比較して大幅に改善しているものの、他地方との境界線付近および沿岸部での取りこぼしが多いことが課題として残ります。

手法4: 異なる大きさの正方形を、関東地方の領域内に敷き詰める(最終的に採用)

手法3では、格納可能なグリッドデータ数の制約から、グリッドの辺長Lを大きく設定せざるを得ないことが問題となりました。手法4では、複数のサイズのグリッドを用意し、内陸部には大きいグリッドを、沿岸部や他地方との境界では小さなグリッドを使うことで、グリッドデータ数を節約しつつ面積カバー率の改善を目指します。

今回は、辺長がそれぞれ5.4km, 1.8km, 0.9km, 0.6km, 0.3kmである5種類のグリッドを用意しました。図4に、グリッドの作成順序を伊豆大島を例に示します。大きいグリッドから優先的に領域を埋めていき、なお空いている部分に対してより小さなグリッドが使われるようにしています。また、作成した関東地方(島しょ部除く)全域のグリッドマップを図5に示します。最も大きな5.4kmグリッドが面積のほとんどを占めており、辺長が1.8km以下のグリッドは沿岸部や他地方との境界に対し補助的に使われていることがわかると思います。

f:id:yryrrrrryryr:20191224025539p:plain:w400
図4 グリッドの作成順序

f:id:yryrrrrryryr:20191222224611p:plain:w400
図5 関東地方(島しょ部除く)全域のグリッドマップ

以下に、グリッドサイズ別のグリッド数、総面積、および抽選対象領域の累積面積カバー率を示します。大きなグリッドと小さなグリッドを併用することにより、面積カバー率を98.85%まで改善することができました。

辺長(km) グリッド当り面積(km^{2}) グリッド数 総面積(km^{2}) 累積カバー率
5.4 29.16 961 28,023 86.44%
1.8 3.24 799 2,589 94.42%
0.9 0.81 990 802 96.90%
0.6 0.36 904 325 97.90%
0.3 0.09 3439 310 98.85%

また、このときのグリッドデータの総数は7,093個であり、グリッドサイズ抽選用リスト、乱数抽選用リストを除いた使用可能なリスト8個以内にデータを収めることが可能です。

3. 診断メーカーに実装してみる

抽選方法の概要

この診断における抽選方法の概要を図6に示します。グリッドサイズ→グリッドの絶対位置→グリッド内の相対位置 の順番で抽選を行います。

f:id:yryrrrrryryr:20191222201139p:plain:w400
図6 本診断における抽選方法の概要

また、各リストはそれぞれ以下の用途で使われています。

リスト名 リストの説明
[LIST1] グリッドサイズの抽選用
[LIST2] グリッド内の相対位置の抽選用
[LIST3][LIST10] グリッドの絶対位置の抽選用

Step 1 : グリッドサイズの抽選

Step 1では、5種類のサイズのグリッドの中からどれを選ぶか抽選します。[LIST3][LIST10]には、グリッドサイズにより異なる番号のリストにグリッドの絶対位置が格納されています。[LIST1]は、[LIST3][LIST10]のうち1つだけを参照し、グリッドサイズの抽選を行うための「くじ」の役割を果たします。

ここで、各リストの値の数は999以下に制限されているため、0.3kmグリッドは[LIST3][LIST6]に4分割して格納しました。
また、各場所が選択される確率を一様にするため、リストの抽選割合は、各リスト中に含まれるグリッドの総面積に比例させる必要があります。そこで、[LIST1]中の出現回数は、[LIST3][LIST6] (0.3kmグリッド) を各2回とし、残りは面積占有率に比例するよう設定しました。

リスト番号 辺長(km) グリッド数 面積占有率 [LIST1]中の出現回数
[LIST3] 0.3 860 0.24% 2
[LIST4] 0.3 860 0.24% 2
[LIST5] 0.3 860 0.24% 2
[LIST6] 0.3 859 0.24% 2
[LIST7] 0.6 904 1.02% 8
[LIST8] 0.9 990 2.50% 21
[LIST9] 1.8 799 8.08% 67
[LIST10] 5.4 799 87.44% 724

Step 2 : 具体的なグリッドの抽選

[LIST3][LIST10]の値には、各グリッドの中心位置における「東京駅からの東西および南北方向距離(それぞれ東と北が正方向)」、「経度緯度」、「経度1度あたり距離」が格納されています。

項目 値の例
東京駅からの東西方向距離 X_0 (km) -122.177
東京駅からの南北方向距離 Y_0 (km) 91.006
経度 λ_0 (deg) 138.40186
緯度 φ_0 (deg) 36.49298
経度1度あたり距離 (km/deg) 89.236

Step 3 : グリッド内の位置の抽選

最後に、グリッド内での東西方向の相対位置( \large p×\frac{L}{2})、および南北方向の相対位置( \large q×\frac{L}{2})を求めるため、pおよびqについてそれぞれ独立した抽選を行います。

[LIST2]には、pおよびqに代入する為の一様乱数が500個(-0.998から+0.998まで0.004刻み)格納されています。

東京駅からの東西方向距離 X および 南北方向距離 Y は、それぞれ以下の式にて計算されます。

 \large X = X_0 + p×\frac{L}{2}

 \large Y = Y_0 + q×\frac{L}{2}

このとき、抽選されうる場所同士の間隔は、最も小さい0.3kmグリッドで 0.6m、最も大きい5.4kmグリッドでも 10.8m となり、地理的に密な抽選結果を得ることができます。

また、Google Mapへのリンク生成時に用いられる 緯度 φ および 経度 λ は、それぞれ以下の式にて計算されます。

 \large φ = φ_0 + q×\frac{L}{2×111}

 \large λ = λ_0 + p×\frac{L}{2×111×cosφ}

診断結果例

2019/12/22に診断を回してみた結果、埼玉県秩父市の山奥を提示されました。
最寄駅である三峰口駅からは30kmほど離れた場所にあり、公共交通機関のみを用いて到達するのは極めて困難な場所といえるでしょう。

赤きちさんは、東京駅から西に88 km、北に31.53 km 離れたこの場所に行ってらっしゃい!
www.google.co.jp/maps/place/35.96117,138.78988

4. おわりに

今回は、「結果パターン数を十分多くする」「他地方や海域を出力しない」「可能な限り多くのエリアを結果候補に含める」の3点を目標とし、関東地方全域から1つの地点をランダムに抽選する診断を作りました。結果として、他地方や海域を出力させることなく、関東地方の98.85%のエリアを抽選対象領域に含めた診断を作ることができました。

この診断は一見すると実用的な部類に入りそうですが、作者としてはこの診断は手間をかけただけのクソ診断に過ぎないという認識でいます。診断結果例でも示したとおり、公共交通機関で到達困難な場所が診断結果となることも非常に多く、安易な「実用」は危険を伴います。

この診断で選ばれた場所に「実際に行ってみた」といったコメント等は歓迎しますが、私有地や立入禁止区域に入ることは現に謹んでください。また、遭難等のトラブルに巻き込まれないよう、身の安全を優先して行動してください。当然ですが、湖沼や河川等が診断結果に表示された場合でも、決して入水しようと思わないでください

以上にて私のクソ診断紹介を終了したいと思います。ここまで読んでくださった皆様に、深く感謝申し上げます。

(参考1) 島しょ部の出現確率について

グリッドデータには、伊豆諸島および小笠原諸島のものも含まれています。これら島しょ部の面積の合計は359.10km^{2}であり、関東地方の面積の約1.11%を占めています。ただし、島しょ部は単位面積あたりの沿岸距離が長くなり、グリッドによる面積カバー率が相対的に低下することから、診断メーカー上での出現確率は約1.02%と若干低くなっています。

地域 実際の面積(km^{2}) カバー率 グリッドの総面積(km^{2})
関東全域 32,420 98.85% 32,048
島しょ部 359.1 91.10% 327.2

また、診断上での最南端は「南硫黄島」であり、診断メーカー上での出現確率は約0.007%となっています。(日本最南端の沖ノ鳥島は本診断では残念ながら出現しません)
暇があったら是非離島が出るまで診断を回し続けてみてください。

(参考2) 本診断の姉妹版

(参考3) ソースコード

診断に使用した基本診断テキストおよび各リストをgithubに公開したので、興味のある人はご覧ください。 github.com

クソ診断紹介3「下半身ジャンケン」

この記事は、クソ診断 Advent Calendar 2019 の16日目の記事です。

1. はじめに

皆さんこんばんは。クソ診断紹介3回目にあたる今日は、ジャンケンに第4の選択肢「下半身」を加えた「下半身ジャンケン」について紹介していきたいと思います。
まだこの診断を回したことがない人は、さっそく以下のリンクから不審者と下半身ジャンケンをプレイしてみましょう。
shindanmaker.com

いろいろな変則ジャンケン

幼少期の頃、変則的なルールのジャンケンを1度は遊んだことがあるかと思います。例えば、グーで勝てば3段、チョキで勝てば6段、パーで勝てば6段進み、階段の頂点を目指すゲーム(通称「グリコ」)は変則ジャンケンの1つにあたります。

また世界各国では、出す手の選択肢が4種類以上のジャンケンが存在します。
これについては、(英語版ではなくなぜか)フランス語版のWikipediaに情報が充実しているので、リンクを掲載しておきます。

Pierre-papier-ciseaux — Wikipédia

例えばフランスでは、Pierre(石)、Ciseaux(はさみ)、Feuille(紙)、Puits(井戸)の4種類の手を用いるジャンケンのルールがあるようです。

さらに、変則ジャンケンは数学の確率の大学入試問題として頻繁に出題されるだけでなく、小論文としての出題例もあります。2018年には、早稲田大学スポーツ科学部の入学試験において、ジャンケンに4種類目の選択肢を加えた新しいゲームを考案させる問題が出題され、大きな話題となりました。

じゃんけんの選択肢「グー」「チョキ」「パー」に、「キュー」という選択肢も加えた新しいゲームを考案しなさい。解答は、新ゲームの目的およびルールを説明するとともに、その新ゲームの魅力あるいは難点も含めて、601字以上1000字以内で論じなさい。

「下半身ジャンケン」誕生の経緯

さて、私の趣味の1つに、「日本不審者情報センター」に掲載される不審者情報の確認があります。

上記リンクを見ると、人をターゲットとした事案のうち最も多いものは「下半身露出」で、1日に数件~十数件もの事案が報告されていることが分かると思います。twitterで毎日たくさんの下半身露出情報に触れるようになった私は、2019年4月9日、ついに下半身ジャンケンのルールを思いつき、次のようなツイートをしてしまいます。

このツイートに対し、想像以上にリツイートやいいねの反応を頂けたこともあり、診断メーカーで実装してみたら面白いのではないかと考えました。この6日後の2019年4月15日、下半身ジャンケンは診断メーカーとして産声を上げることになりました。

ルール

下半身ジャンケンのルールを図1に示します。

f:id:yryrrrrryryr:20191214164628p:plain:w400
図1 下半身じゃんけんの手の相関図

  • 2人で遊ぶジャンケンです。「グー」「チョキ」「パー」「下半身」の4種類の手を出すことができます。
  • 下半身はグーとパーに勝ちますが、チョキに負けます。
  • 下半身を出してチョキで負けた場合、次回以降の勝負で下半身を出す事ができません。
  • じゃんけんを6回行い、合計勝利数が多いプレイヤーが最終的な勝者となります。

各章の内容

  • 2章では、対戦者同士が最善を尽くした場合の最適戦略を探り、各手が出される割合を算出します。
  • 3章では、診断メーカーで下半身ジャンケンを実装した過程を簡単に紹介します。
  • 4章では、下半身ジャンケンに勝つための人工知能を作り、様々な戦略を持つ相手に戦いを挑みます。

おことわり

筆者はゲーム理論人工知能について詳細な知識を持っているわけではありません。
そのため、厳密な議論を省略しイメージで書いている部分も多いかと思います。
また、内容に誤りがあった場合、twitter宛リプライまたはダイレクトメールでお知らせ頂きますと大変助かります。

2. 下半身ジャンケンの最適戦略

このゲームを診断メーカー化するためには、プレイヤーが各手を出す確率を決めておく必要があります。
この章では、ミニマックス法を用いて、相手がどのような戦略を取ってきても負けにくい戦略を考え、最適戦略を取ったときに各回で出される手の割合を算出してみましょう。

ミニマックス法では、対戦相手が期待得点を最大化するような「最適戦略」を常に選ぶものと仮定します。 このとき自分が取るべき戦略は、相手の最適戦略における期待得点の最大値最小化させるものとなります。

図2に考え方の具体例を示します。
自分が戦略Aを取るとき、相手の最適戦略は「グー」「チョキ」「パー」「下半身」(いずれの手も出す可能性がある)で、このとき相手の期待得点は3点です。
自分が戦略Bを取るとき、相手の最適戦略は「下半身」のみで、このとき相手の期待得点は5点です。
このとき、相手の最適戦略における期待得点の最大値A戦略の方が低くなるため、これが自分が取るべき戦略となります。

f:id:yryrrrrryryr:20191214182013p:plain
図2 ミニマックス法の考え方の例

また、相手が複数の手をそれぞれ一定確率で出す場合、期待得点は特定の手を出す場合の最大値と最小値の範囲内に収まることから、以降相手の出す手(最大4通り)ごとの期待得点のみを考えるものとします。

各回で勝った場合+1点、負けた場合-1点が各プレイヤーに入るものとします。 このとき、利得表は以下のとおりとなります。

自分の手/相手の手 グー チョキ パー 下半身
グー 0 1 -1 -1
チョキ -1 0 1 1
パー 1 -1 0 -1
下半身 1 -1 1 0

また、下半身を出す事が可能なプレイヤーは、下半身を出せない場合と比べ戦略が広がるため、より試合を有利に進める事ができます。そこで、残回数がnのときに下半身が出せることの価値を a_n点 と定義し、自分と相手の下半身露出の可否で場合分けして戦略を考えていきます。

(a) 自分も相手も下半身が出せない場合

ただの普通のじゃんけんです。「グー」「チョキ」「パー」を等確率に出すことが自明に最適解となります。

(b) 自分の下半身だけ出せる場合

相手のみ下半身が出せない状態のため、この回で自分が「下半身」を、相手が「チョキ」を出さない限り、 次回でもa_n点分不利な状態が継続することになります。
自分が(グー,チョキ,パー,下半身)を出す確率を(p,q,r,s) と表すとき、相手の出す手と相手の期待得点の組み合わせは以下のとおりとなります。

相手の手 相手の期待得点
グー E_1=(q-r-s)-a_n
チョキ E_2=(-p+r+s)-(p+q+r)a_n
パー E_3=(p-q-s)-a_n

E=max(E_1,E_2,E_3)を最小化するように、(p,q,r,s) の組み合わせを考えていきます。

  • 相手が出す可能性がある手が2種類以下の戦略は、相手の最適戦略とはなりません(証明は省略します)。
  • 相手が3種類の手全てを出す可能性がある場合、各手を出したときの相手の期待得点は等しくなり、E_1=E_2=E_3が成立します。

このことから、以下の連立方程式を解くこととなります。

 (q-r-s)-a_n = E
 (-p+r+s)-(p+q+r)a_n = E 
 (p-q-s)-a_n = E
 p+q+r+s = 1

(p,q,r,s) は、a および E を用いて、以下のように表す事ができます。

  • 0≦a_n<1 または 1<a_nのとき
 \large p = \frac{ 3{a_n}^2 + 3a_nE+4a_n+3E-1}{3(a_n-1)}
 \large q = \frac{-2a_n-3E-1}{3(a_n-1)}
 \large r = \frac{ -3{a_n}^2 - 3a_nE-8a_n-9E-1}{3(a_n-1)}
 \large s = \frac{3(a_n+E)}{3(a_n-1)}

ここで、0≦p,q,r,s≦1の条件下でEを最小化することを考えます。これはr=0のとき成立し、以下に示す(p,q,r,s)が求める自分の戦略となります。

 \large p = \frac{2(a_n+2)}{3(a_n+3)}
 \large q = \frac{2(a_n+2)}{3(a_n+3)}
 \large r = 0
 \large s = \frac{1}{3(a_n+3)}

また、このときの相手の期待得点は  \large E = \frac{-s(1-a_n)+3a_n}{3} となります。
これは、相手だけ下半身を持っていなかった分のビハインドに等しいため、 {a_n} について以下の漸化式が成立します。
 \large -a_{n+1} = \frac{-s(1-a_n)+3a_n}{3}

  • a=1 のとき
 \large p = 1
 \large q = \frac{1-p}{2}
 \large r = 1-2p
 \large s = \frac{3p-1}{2}

このときの相手の期待得点は  E = -1 となります。

(c) 相手の下半身だけ出せる場合

このとき、自分は下半身を出せないため、常に s = 0 が成り立ちます。
相手の出す手と相手の期待得点の組み合わせは以下のとおりとなります。

相手の手 相手の期待得点
グー E_1=(q-r)+a_n
チョキ E_2=(-p+r)+a_n
パー E_3=(p-q)+a_n
下半身 E_4=(p-q+r)+a_n(p+r)

前項の結果を踏まえると、対称性より相手は「グー」「チョキ」「下半身」の3種類の手を出す可能性があり、E_1=E_2=E_4(=E)が成立します。連立方程式は、

 (q-r)+a_n = E
 (-p+r)+a_n = E 
 (p-q-r)+(p+r)a_n = E
 p+q+r = 1

となり、これを解くことで得られる(p,q,r,s)が求める自分の戦略となります。

 \large p = \frac{2(a_n+1)}{3(a_n+3)}
 \large q = \frac{4}{3(a_n+3)}
 \large r = \frac{1}{3}
 \large s = 0

ここで、相手の期待得点は  \large E = \frac{s(1-a_n)-3a_n}{3} となりますが、これは「自分の下半身だけ出せない場合」のEを-1倍したものと等しいことが確認できます。 また、 {a_n}についての漸化式も、前項と同値のものを導出することができます。  \large a_{n+1} = \frac{s(1-a_n)-3a_n}{3}

「下半身露出権」の価値

導出した漸化式を使い、「下半身露出権」が何勝分の価値を持つのか計算してみましょう。
試合終了時の下半身露出の可否は、勝敗に関係がないため、a_0 = 0が成立します。この条件下で、残回数 n に応じた 下半身露出権の価値 a_n は以下のとおりとなります。

残り回数 n 下半身露出権の価値 a_n
0 0.0000
1 0.1111
2 0.2063
3 0.2889
4 0.3609
5 0.4243
1.0000

6回勝負での1回戦では、下半身を奪うことに0.4243勝分の付加価値があるものの、残り回数が減るごとにその価値が薄れていくことが分かります。
また、残り回数が無限(n→∞)のとき、a_nは1に収束します。すなわち、チョキで下半身に勝つ価値は、他の手段で勝つ価値のちょうど2倍に相当するといえます。

(d) 自分も相手も下半身を出せる場合

最後に、自分も相手も下半身を出せる初期状態について考えます。 相手の出す手と相手の期待得点の組み合わせは以下のとおりとなります。

相手の手 相手の期待得点
グー E_1=q-r-s
チョキ E_2=-p+r+(a_n+1)s
パー E_3=p-q-s
下半身 E_4=p-(a_n+1)q+r

このとき、ゲームの対称性より、

  • 相手が出す可能性のある手 → 相手の期待得点が0
  • 相手が出さない手 → 相手の期待得点が負(あえて不利になる手は出さない)

が成立します。
「相手が全ての手を出す場合」「相手が出さない手が1つある場合」で場合分けを行ったとき、下半身露出権の価値a_nとそれに対応した最適戦略は以下のとおりとなります。(グーのみ出さない、チョキのみ出さない戦略が最適となるa_nの範囲は存在しない)

a_nの範囲 最適戦略
0≦a_n<1 パーのみ出さない
a_n=1 全ての手を出す
a_n>1 下半身のみ出さない

すなわち、診断メーカー上での6回勝負ルールでは、「パーのみ出さない」戦略が最適となります。以下に、a_nの範囲に応じた最適戦略の(p,q,r,s)を示します。

  • 0≦a_n<1のとき (パーのみ出さない)
 \large p = \frac{a_n+1}{a_n+3}
 \large q = \frac{1}{a_n+3}
 \large r = 0
 \large s = \frac{1}{a_n+3}
  • a_n=1のとき (全ての手を出す)
 \large p = \frac{1+2s}{3}
 \large q = \frac{1-s}{3}
 \large r = \frac{1-4s}{3}
 \large (0≦s≦\frac{1}{4})
  • a_n>1のとき (下半身のみ出さない)
 \large p = \frac{1}{3}
 \large q = \frac{1}{3}
 \large r = \frac{1}{3}
 \large s = 0

以下に興味深い点を2つほど示します。

  • 回数が無限(a_n=1)のとき、下半身を出す確率sが異なる最適戦略が共存可能なこと。例えば、以下の2種類の戦略はともに最適戦略となり、互角に戦うことが可能です。

    • (p,q,r,s)=(40\%,30\%,20\%,10\%) の露出控えめ戦略
    • (p,q,r,s)=(50\%,25\%,0\%,25\%) の露出狂戦略
  • 下半身露出権の価値を高めすぎるルールに変更(例:試合終了時に下半身が残っていたら1勝分とカウント)し、a_nが1を超えると、リスクの高さから誰も下半身を出さなくなり普通のじゃんけんと化してしまうこと。

各回に出される手の割合

以上の結果を踏まえ、対戦者が互いに最適戦略を取る場合、各回にて出される手の割合は、図3のとおりとなります。

f:id:yryrrrrryryr:20191216035135p:plain
図3 各回にて出される手の割合

3. 診断メーカーへの実装

この章では、下半身ジャンケンを診断メーカーにどのように実装したか紹介していきます。

各リストの説明

  • [LIST1][LIST6]は、各回の「じゃんけんの手」と「勝敗判定」を抽選するためのリストです。リスト中の各値には、1回目から6回目の対戦結果がカンマ区切りで格納されています。以下は格納された値から、1回分の要素を取り出した例です。
対戦結果 格納された値の例
勝利 〇 ✊ - ✌ ×=IF(0=0,"",[LIST8_1])
引分 △ ✊ - ✊ △
敗北 × ✊ - ╰U╯ 〇=IF(0=0,"",[LIST9_1])

また、勝利の際は[LIST8]を、敗北の際は[LIST9]をそれぞれ参照する事で、勝敗数をカウントしています。

[LIST1][LIST4]は、その回に下半身の奪取がなかった場合の結果を、下半身露出の可否別に分けて格納しています。

リスト名 リストの説明
[LIST1] 自分も相手も下半身を出せる
[LIST2] 自分のみ下半身を出せる
[LIST3] 相手のみ下半身を出せる
[LIST4] 自分も相手も下半身を出せない

[LIST5][LIST6]は、その回に下半身の奪取があった場合の結果を格納しています。また、[LIST10]を参照することにより、下半身の奪取があった回数がカウントされます。

リスト名 リストの説明 演出
[LIST5] 自分相手の下半身を奪取 ★ㄘんㄘんを奪った!
[LIST6] 相手自分の下半身を奪取 ★ㄘんㄘんを奪われた…
  • [LIST7]では6回分の結果を各リストから参照することで、下半身奪取が行われるタイミングを抽選しています。 例えば、2回目で下半身を奪い、4回目で下半身を奪われるような状態遷移は、以下の順番でリストを参照することにより表現されます。
回数 状態 参照先のリスト
1回戦 自分:○/相手:○ [LIST1]
2回戦 下半身を奪った [LIST6]
3回戦 自分:○/相手:× [LIST3]
4回戦 下半身を奪われた [LIST5]
5回戦 自分:×/相手:× [LIST4]
6回戦 自分:×/相手:× [LIST4]
  • [LIST8][LIST10]は、各種カウントを行うためのリストです。定数 1 が格納されており、SUMLIST()関数を使うことで、呼び出された回数を出力することができます。
リスト名 リストの説明
[LIST8] 自分の勝利数
[LIST9] 自分の敗北数
[LIST10] 下半身の奪取があった回数

抽選方法

  • 最初に、[LIST7]を用いて、試合全体で下半身の奪取が生じるタイミングが抽選されます。
  • 次に、[LIST7]から参照される[LIST1][LIST6]内で、各回でのジャンケンの手と勝敗判定が抽選されます。

診断結果例

下半身ジャンケン!
赤きち VS 不審者
1回 〇 ✊ - ✌ ×
2回 ×╰U╯ - ✌ 〇
★ㄘんㄘんを奪われた…
3回 × ✋ - ✌ 〇
4回 × ✊ - ╰U╯ 〇
5回 × ✊ - ╰U╯ 〇
6回 〇 ✊ - ✌ ×
You Lose…(2 勝 4 敗)

なお、下半身の奪取が1度も起きなかった場合、最後に「★下半身の平和は守られた!」と表示されます。

4. 人工知能に下半身ジャンケンをさせてみた

2章で求めた「最適戦略」は、対戦者がお互い最善を尽くすと仮定し、相手の期待得点を最小化しようとする、いわば負けないための戦略でした。
本章では、人工知能の1つである強化学習を用いて、対戦相手の出した手の履歴などをもとに報酬を最大化する手を決める、すなわち勝ちにいく戦略モデルを作っていきます。

人工知能の実装

強化学習では、行動によって得られた報酬をもとに、試行錯誤を繰り返すことで報酬を最大化する戦略を求めていきます。
詳細な説明については、以下のサイトを参考にしてください。
強化学習とは?(What is Reinforcement Learning?)

今回実装したモデルでは、以下のStepを繰り返すことによりモデルを学習させていきました。

  • Step 1: 試合の各回で、行動価値(試合の展開に応じた手の優劣を判定する指標)をもとに行動を選択する
  • Step 2: 試合終了後、試合結果をもとに行動価値を更新する

ここで、行動価値 Q(s,a) は、「状態 s で、行動 a を取った場合の、現在から試合終了までに期待される総報酬」として定義され、学習前のQ(s,a) の初期値は 0 に設定されています。
また、試合中の各回での報酬は、ジャンケン勝利時に+1点、引分時に0点、敗北時に-1点とします。

状態 s、行動 a、および行動価値 Q の関係の例を 図4 に示します。

f:id:yryrrrrryryr:20191216014544p:plain
図4 状態 s 、行動 a 、および行動価値 Q の関係の例

人工知能が区別可能な状態sとして、以下の3点を設定しました。これらが全て同じ場合に、同一の状態sとして認識されることになります。

  • 試合の進行状況 (何試合目まで終了したか)
  • 下半身の状態 (自分および相手の下半身露出の可否)
  • 相手の出した各手の回数

行動a は、自分が次に出す手の候補の集合となります。すなわち、

  • 下半身露出可能な場合、「グー」「チョキ」「パー」「下半身」
  • 下半身露出不可能な場合、「グー」「チョキ」「パー」

となります。

次に、各Stepにおいて、何が行われているか具体的に見ていくことにしましょう。

Step 1: 試合の各回で、行動価値をもとに行動を選択する

本モデルでは、以下の方針で行動 a を選択することにしました。

  • 学習中では、90%の確率で行動価値 Q(s,a) を最大とする行動 a を選択し、10%の確率でランダムに行動 a を選択する(Epsilon-Greedy法)
  • 本番では、100%の確率で行動価値 Q(s,a) を最大とする行動 a を選択する

Step 2: 試合終了後、試合結果をもとに行動価値を更新する

試合で遷移した全ての状態sおよび行動aの組み合わせに対して、行動価値 Q(s,a) の更新を行ないます。

Q(s,a) ← Q(s,a) + ε(q-Q(s,a))

ここで、q は状態 s 以降に試合で獲得した総報酬です。例えば、状態 s が1回戦終了時のものの場合、q は 2回戦~6回戦における (勝利回数) - (敗北回数) となります。

また、ε は学習率と呼ばれるパラメータで、学習が進むにつれて減衰させることにより大きな値の変化が起こりくくなるよう設定するのが一般的です。
今回は、Q(s,a)の更新が n 回目のとき、ε=\frac{0.1}{\sqrt{n}} と定めました。

対戦相手が出す手の設定

特定の相手1人と連続して対戦を行う場合、相手の出す手の癖を見抜き、勝つための戦略を立てることは比較的容易であり、人工知能を使うほどではありません。
そこで今回は、1試合ごとに出す手の比率が異なる対戦相手と交代する状況を想定します。
試合が始まるごとに、相手が出す手の比率を次のように決定します。
まず、互いに独立な[0,1]の一様乱数 (p',q',r',s') を生成し、出す手の割合の合計が1になるよう正規化します。このとき、相手が各手を出す確率(p,q,r,s)は以下のとおりとなります。

  • 相手が下半身露出可能なとき
 \large p=\frac{p'}{p'+q'+r'+s'}
 \large q=\frac{q'}{p'+q'+r'+s'}
 \large r=\frac{r'}{p'+q'+r'+s'}
 \large s=\frac{s'}{p'+q'+r'+s'}
  • 相手が下半身露出不可能なとき
 \large p=\frac{p'}{p'+q'+r'}
 \large q=\frac{q'}{p'+q'+r'}
 \large r=\frac{r'}{p'+q'+r'}
 \large s=0

以降、上記戦略をもつ対戦相手の集団を「烏合の衆」と呼ぶことにします。 今回は、人工知能に「烏合の衆」と100万試合の下半身ジャンケンを行わせることで行動価値を学習させました。

試合その1: 「人工知能」VS「烏合の衆」

先ほど学習に用いた「烏合の衆」を相手として、下半身ジャンケンの本番試合を1万回行いました。
なお人工知能は、本番ではランダムな手は一切出さず、行動価値が最大となる手のみ出す戦略を取ります。

試合結果は以下のとおりです。勝利率は69.27%と、敗北率の17.54%に対し4倍近い好成績を叩き出しました。
相手の出す手の割合に偏りがある場合、わずか6回勝負であっても、相手の手の履歴から勝つ戦略を立てることが可能なことがわかります。

勝利率 引分率 敗北率 平均勝敗数差
69.27% 13.19% 17.54% 1.783

人工知能の学習結果

さて、各状態において人工知能が導きだした最適手が何であるか、行動価値を通して具体的に確認していきましょう。
まず、1回戦における各手の行動価値を以下に示します。「チョキ」の行動価値が最大値を示しているため、本番試合においては、人工知能は1回戦で必ず「チョキ」を出していたことになります。また、その場合に期待される試合中の勝敗数差は1.678と推定されていたことがわかります。

出す手 グー チョキ パー 下半身
行動価値 1.137 1.678 1.172 1.563

次に、2回戦における行動価値を図5-1および図5-2に示します。「相手が出した初手」「下半身露出の可否」が状態として保存されているため、合計6通りの状態に応じた行動価値が計算されています。
両者とも下半身が残っている場合、

  • 相手の初手が「グー」または「パー」の場合 → 下半身
  • 相手の初手が「チョキ」または「下半身」の場合 → チョキ

の行動価値が最も高くなる計算結果となりました。
この結果は、「チョキ」を出しやすい相手に対しては下半身を出すのは危険であり、「下半身」を出しやすい相手に対してはチョキで下半身を奪取するメリットが多いと考えることで説明がつきます。

f:id:yryrrrrryryr:20191216231950p:plain
図5-1 2回戦における行動価値(2人とも下半身露出可能な場合)

f:id:yryrrrrryryr:20191216232018p:plain
図5-2 2回戦における行動価値(1人のみ下半身露出可能な場合)

試合その2: 「人工知能」VS「最適戦略者」

最後に、2章で求めた「最適戦略」を取る相手と、下半身ジャンケンの試合を1万回行った結果を以下に示します。

勝利率 引分率 敗北率 平均勝敗数差
41.15% 17.28% 41.57% -0.027

勝利率と敗北率がほぼ等しく、ほぼ互角の結果となりました。「最適戦略」を知っている相手に対しては、偶然の範囲を超えて勝ち越すことは難しいようです。

5. おわりに

本記事では、私が考案した変則ジャンケンの「下半身ジャンケン」について、お互いが最善を尽くした場合の最適戦略を導出しました。また強化学習を使った人工知能を作り対戦させる試みでは、「烏合の衆」相手に対しては好成績を収めた一方で、「最適戦略者」相手にはほぼ互角の成績となりました。肝心の診断メーカーの実装方法の記述が手薄になってしまった事についてはどうかご容赦ください。
4週にわかるクソ診断紹介も、いよいよ次で最後となります。最終回で紹介する診断は、関東地方内の任意の場所に等確率で飛ばされる「関東地方ダーツの旅」となります。作成の過程で使用したGIS(地理情報システム)などの話を盛り込めればと考えております。
分量多めの記事となってしまいましたが、最後までお読みいただきありがとうございました。

(参考1) いろいろな確率

ここでは、対戦者同士が2章の最適戦略を選択した場合、下半身ジャンケン中に様々な事象がどれくらいの確率で出現するのかをみていきます。

各回における下半身の状態の出現割合

図6に、各回終了時における下半身露出可能なプレイヤーの人数の出現割合を示します。
両者ともに下半身を残したまま平和的に終戦する可能性は27.7%となります。一方で、試合中に両者ともに下半身を奪われる結末も23.9%の確率で発生します。

f:id:yryrrrrryryr:20191216232110p:plain
図6 各回における下半身の状態の出現割合

勝利回数および敗北回数の出現割合

図7に、1試合での (勝利回数)-(敗北回数) の出現割合を示します。
勝ち越す/負け越す確率はそれぞれ41.04%ずつ、引分となる確率は17.92%となります。また、全勝/全敗する確率はそれぞれ0.37%ほどしかありません。
また図8に、勝利回数および敗北回数の組み合わせの出現割合を示します。
出現する可能性の最も高い組み合わせは2勝2敗の10.91%です。一方、出現確率が最低となる組み合わせは0勝0敗の0.15%ですが、実際に6回連続で引分となるような診断結果ツイートがこれまでに複数確認されています。

f:id:yryrrrrryryr:20191219084259p:plain
図7 勝敗数差の出現割合

f:id:yryrrrrryryr:20191219084954p:plain
図8 勝数および敗数の組み合わせの出現割合

試合途中で同点の場合に下半身露出権の有無が試合結果に及ぼす影響

図9は、試合途中で同点かつ、自分のみ下半身露出権を持つ場合、どの程度試合が有利になるのか示したものです。
基本的に早い段階で下半身露出権を独占した方が、勝利率が高まることがわかります。
例外として4回戦(残り2戦)で同点の場合のみ、引分に終わる確率が高くなる影響で勝利率、敗北率ともに圧縮されます。

f:id:yryrrrrryryr:20191216232203p:plain
図9 試合途中で同点の場合に下半身露出権の有無が試合結果に及ぼす影響

(参考2) 作成した他のちんぽ系診断

(参考3) ソースコード

診断に使用した基本診断テキストおよび各リストをgithubに公開したので、興味のある人はご覧ください。 github.com

クソ診断紹介2「あなたに必要な3つのもの」

この記事は、クソ診断 Advent Calendar 2019 の9日目の記事です。

1. はじめに

診断作成の経緯

twitterのタイムラインで、格子上に並んだ文字の中から言葉を探す心理テストを時折見かけることがあります。見つけた言葉が表すものは、手に入れたいものであったり、人生で重要なものであったりとツイートにより異なるものの、「最初に見つけた3つの言葉」という条件はほぼ共通しているようです。
さて、本診断を作成したのは2019年の元日のことでした。日本人の運気に対する関心が特に高まるこの年末年始の時期、多数の占いや心理テストに関するツイートが投稿されました。「最初に見つけた3つの言葉」についてもいくつかの投稿があり、特に以下のツイートは多数のRTを集めました。

一方、作成に手間がかかるなどの理由からか、「最初に見つけた3つの言葉」についてはあまり多くのバリエーションが作成されていないように見えました。これを毎日遊べるコンテンツにする事が、診断作成の動機でした。

診断の概要

カタカナ80文字の中に、縦または横に3~4文字のキーワードが隠れています。
その中で最初に見つけた3つが、あなたに今必要なものを表しています。 shindanmaker.com

この診断の抽選の仕組みを図1に示します。どの位置に何文字の単語を入れるかを決めるパズルと、パズルに入れる文字を決めるキーワードの2種類のリストで構成されています。これらの抽選結果を組み合わせることで、診断結果が生成されます。

f:id:yryrrrrryryr:20191209040548p:plain:w400
図1 診断の抽選の仕組み

この記事では、パズルおよびキーワードの各リストの作成方法を示した後、実装方法について診断メーカーの制約条件を踏まえながら解説していきます。

2. リストの中身を作る

パズルを作成する

どの位置に何文字の単語を入れるか決定するアルゴリズムは、診断メーカーの機能だけで作成するには少々複雑です。
そのため、パズル部分は診断メーカー外であらかじめ候補を作成しておき、診断メーカーでは候補の中から抽選を行うような仕組みとしました。
パズルの作成方法を図2に示します。行数と列数が決められた格子を、ピースで隙間なく埋めていくことを考えます。

f:id:yryrrrrryryr:20191209051654p:plain:w400
図2 パズルの作成方法

なお、ピースはキーワードの文字数読む方向に応じて、以下の6種類があります。

  • 1文字
  • 横2文字
  • 横3文字
  • 横4文字
  • 縦3文字
  • 縦4文字

2文字以上のピース(5種類)からランダムに1種類を選択し、既存のピースと重ならないように配置する動作を、格子の80%以上が埋まるまで繰り返します。最後に残った格子には、1文字のピースを配置します。

疑似コードは以下のとおりとなります。

ピースが配置された文字数 = 0
基準文字数 = 格子の総文字数 × 80%
while( ピースが配置された文字数 < 基準文字数 ):
    ピースの種類をランダムに選択
    ピースの位置をランダムに選択
    if(新しいピースが既存のピースと重ならない):
       新しいピースを格子に配置する
       ピースが配置された文字数 += 新しいピースの文字数
残った格子に1文字のピースを配置する

キーワードを選ぶ

キーワードの選定については、『現代日本語書き言葉均衡コーパス』短単位語彙表 ver.1.0を参考にしました。 pj.ninjal.ac.jp

  • 3文字、4文字のリスト 「名詞-普通名詞」の中から、各200単語ずつを選定しました。

  • 2文字のリスト 任意の1文字を2つ並べた場合、偶然に意味のある単語となる可能性が高く、ゲームとしての難易度を大きく下げる要因になり得ます。
    (例えば、アから始まる2文字の名詞だけでも、「愛(アイ)」「青(アオ)」「赤(アカ)」「秋(アキ)」等多数存在)
    そこで、難易度調整を目的として、2文字のリストには語彙表に掲載されていない、すなわち単語として意味を成さない文字列を選定しました。

3. 実装してみる

リストの構成

リストの構成は以下のとおりとなります。

リスト番号 リストの説明
[LIST1] キーワード(1文字)
[LIST2] キーワード(縦2文字,不使用)
[LIST3] キーワード(縦3文字)
[LIST4] キーワード(縦4文字)
[LIST5] キーワード(横2文字)
[LIST6] キーワード(横3文字)
[LIST7] キーワード(横4文字)
[LIST8] パズル(左半分)
[LIST9] パズル(右半分)

制約条件を考慮して実装する

ツイート文字数の制約

Twitter上でツイート可能な文字数の制約から、診断結果本文に使える文字数は全角換算で120文字以下となります。制約条件の詳細については前回のクソ診断紹介記事をご覧ください。

クソ診断紹介1「ちんぽ揃えゲーム」 - にゃーん

(なお、この節における「文字」および「文字数」は、全角換算のものを示すこととします)

格子の行数をR、列数をCとすると、消費される文字数は以下の通りとなります。

  • 文字に R×C 文字
  • 改行に 0.5(R-1) 文字

今回は、格子サイズを行数 R=8 、列数 C=10 と設定したため、格子部分で83.5文字が消費されることになります。

残りの36.5文字で、文字の羅列が何を意味するかツイート冒頭に説明を加えます。

最初に見つけた3つの言葉が、[USER]に今必要なものです。(改行2回)

ここで、[USER]には診断トップ画面で入力された名前が入ります。
[USER]を除いた文字数は24.5文字のため、[USER]が12文字以内であれば、診断結果が途切れることなくツイートされます。

リストの値の文字数の制約

診断メーカーのシステムの都合上、リストの各値の文字数は、300文字以内に制限されております。
(なお、この節における「文字」および「文字数」は、全角半角を問わず1文字としてカウントしたものを示すものとします)
パズルのリストでは、用いたピースのリストを順番に参照していきます。その際、[LIST]表記も全て文字数としてカウントされます。また、パズルのリストは行ごとにカンマで区切られるため、縦のピースのリストは分断され、文字数と同じ回数だけ参照する必要があります(図3)。

f:id:yryrrrrryryr:20191209071230p:plain:w400
図3 ピースの方向によるリスト参照回数の違い

ピースの文字数を n としたとき、リスト参照時に消費される文字数は以下のとおりとなります。

ピースの方向 リストの枝番が1桁の場合 リストの枝番が2桁の場合
11n 12n
9 10

なお、同一のピースのリスト(文字数と方向が等しい)から10種類以上のキーワードを参照する事は少ないため、以降リストの枝番は常に1桁と仮定して話を進めます。
1文字のピースの占める割合が20%で、残りの5種類のピースが等確率で選ばれると仮定したとき、 RC 列の格子のパズルにおけるピースごとの期待出現数および期待リスト消費文字数は以下のとおりとなります。

ピースの種類 消費文字数/ピース 期待出現数 期待リスト消費文字数
1文字 9 0.20RC 1.80RC
縦3文字 33 0.05RC 1.65RC
縦4文字 44 0.05RC 2.20RC
横2文字 9 0.05RC 0.45RC
横3文字 9 0.05RC 0.45RC
横4文字 9 0.05RC 0.45RC

よって、RC 列の格子のパズルのリストを記述するのに必要な文字数は、期待リスト消費文字数の総和 7RC 文字に、行ごとのカンマ区切り 0.5(R-1) 文字を加えたものとなります。

リストの各値の文字数の制約より、

7RC + 0.5(R-1) ≦ 300

が満たされる必要がありますが、作成予定の8行10列の格子では期待リスト消費文字数が563.5文字となり、制約条件を大幅に超過してしまいます。
そのため、格子を左右で2分割する方針を取りました。具体的には、左半分と右半分でそれぞれ8行5列のパズルを抽選し、最終的な結果表示時に結合するようにしました。この場合、期待リスト消費文字数は283.5文字で制約条件を満たすようになります。なお、ランダムに選択されたピースが縦方向のものに偏り、制約条件を満たさなくなるパズルは用いない事としました。

格子を左右で2分割した事により、横方向のピースが5列目~6列目を跨ぐことができないのは大きなデメリットですが、パズルの組み合わせ総数の増加につながっている側面もあります。
(今回は左用のパズルを50個、右用のパズルを50個作成したため、パズルの組み合わせ総数は2,500個)

4. おわりに

ほぼ1年前に作成したクソ診断を思い出したらブログを執筆していたら、夜を明かしてしまいました。何という贅沢な時間の使い方でしょう。 今週の診断はクソ要素薄めの診断紹介になってしまい申し訳ありません。とはいえ、キーワードに用いる単語のユーモア次第では、クソ診断に化けるポテンシャルは秘めているかもしれません。 来週は、グー、チョキ、パーの他に下半身が出せる「下半身ジャンケン」を紹介したいと思います。それではまたお会いしましょう。

(参考) ソースコード

診断に使用した基本診断テキストおよび[LIST8]、[LIST9] (パズル部分)をgithubに公開しています。 github.com

クソ診断紹介1「ちんぽ揃えゲーム」

この記事は、クソ診断 Advent Calendar 2019 の2日目の記事です。

1. はじめに

ごあいさつ

皆様はじめまして。赤きちと申します。twitterで主に女子中学生をしています。 昨年に引き続き、「クソ診断 Advent Calendar 2019」が作成されたとの事なので、 4回にわたり過去に作成したクソ診断(診断メーカー)を紹介していきたいと思います。

ちんぽ揃えゲームの概要

「ち」「ん」「ぽ」の3種類の文字から等確率に1文字を選び、 左から右に順番に並べる操作を、末尾3文字が「ちんぽ」となるまで繰り返します。 5文字以内に「ちんぽ」が揃うと、何かが起こるそうです(?) shindanmaker.com

本記事の主な内容

  • 2章「確率について考える」 では、「ちんぽ」の文字列が揃うまでの過程から確率漸化式を立て、文字数の確率分布を調べる方法について解説しています。

  • 3章「実装してみる」 では、診断メーカーの制約条件を乗り越えて実装する工夫の過程を綴っています。末尾の「(参考)診断メーカーの仕様」を適宜参照しながら読むと理解が深まるかもしれません。

  • 4章「Twitterの診断結果を分析する」 では、ツイートされた全診断結果(約3000件)を取得し、ツイートが特定の診断結果に偏っていないか統計検定を行った結果が書かれています。

先行プログラムの紹介

2013年には、たろいも氏により「おちんぽ」の文字列が出るまで「お」「ち」「ん」「ぽ」の4文字を表示し続ける「おちんぽ表示プログラム」が作成されています。 musicstd.nobody.jp 正直なところ、「ちんぽ揃えゲーム」は完全に二番煎じなのです…

2. 確率について考える

確率漸化式をつくる

「ちんぽ」が揃うまでの過程を分かりやすくするため、文字列の状態にADという名前を付けておきます。

状態名 意味
A 末尾3文字が「ちんぽ」
B 末尾2文字が「ちん」
C 末尾1文字が「ち」
D ACのいずれにも当てはまらない

空の文字列(状態 D に相当)に対し、下に示す操作を n 回行います。操作終了後に文字列の状態が A, B, C, D となる確率を、それぞれ a_{n}, b_{n}, c_{n}, d_{n} と表すものとします。

「ち」「ん」「ぽ」の3種類の文字から等確率で1文字を選び、文字列の末尾に加える。
ただし、末尾3文字が「ちんぽ」(状態 A )の場合は、何も行わない

このとき、ある状態から操作を1回行ったときに各状態に遷移する確率は、 図1 のように表せます。

f:id:yryrrrrryryr:20191201001452p:plain:w400
図1 状態遷移図

また、a_{n} ~ d_{n} (n\geq0)は、以下の漸化式で表すことができます。


\begin{pmatrix} a_{0} \\ b_{0} \\ c_{0} \\ d_{0} \end{pmatrix}
= \begin{pmatrix} 0 \\ 0 \\ 0 \\ 1 \end{pmatrix},
\begin{pmatrix} a_{n+1} \\ b_{n+1} \\ c_{n+1} \\ d_{n+1} \end{pmatrix}
=\dfrac {1}{3}
\begin{pmatrix}
3 \quad 1 \quad 0 \quad 0 \\
0 \quad 0 \quad 1 \quad 0 \\
0 \quad 1 \quad 1 \quad 1 \\
0 \quad 1 \quad 1 \quad 2
\end{pmatrix}
\begin{pmatrix} a_{n} \\ b_{n} \\ c_{n} \\ d_{n} \end{pmatrix}

この漸化式を用いて、n 回以内の操作で「ちんぽ」が揃う確率は a_{n}、ちょうどn 回の操作で「ちんぽ」が揃う確率は (a_{n}-a_{n-1})と表すことができます。

文字数の分布をみる

「ちんぽ」が揃ったときの文字数の頻度および累積確率分布を 図2 に示します。 中央値は20文字で、20文字以内に半分以上の確率(51.96%)で「ちんぽ」が揃うことになります。

f:id:yryrrrrryryr:20191201020028p:plain:w400
図2 文字数の頻度および累積確率分布

また、期待値の27文字はちょうど3の3乗と綺麗な数字になりますが、「ちんぽ」以外の並びでは、(直観に反して)揃うまでの文字数の期待値が異なることがあります。3文字の全ての組み合わせ27通りで期待値を平均すると、29文字となります。

文字列パターン 組み合わせ数 期待値
ちんぽ 6 27
ちちん 6 27
んちち 6 27
ちんち 6 30
ちちち 3 39

3. 実装してみる

状態遷移を再現する

文字列の状態ごとに抽選に使うリストを作成し、次回の抽選に使うリストを再帰的に参照することを考えます。

  • 状態 D のリスト
抽選結果 次回の抽選に使うリスト
状態 C
状態 D
状態 D


  • 状態 C のリスト (末尾1文字が「ち」)
抽選結果 次回の抽選に使うリスト
状態 C
状態 B
状態 D


  • 状態 B のリスト (末尾2文字が「ちん」)
抽選結果 次回の抽選に使うリスト
状態 C
状態 D
なし(完成!)

上の表を見ると、抽選結果次第では、次回の抽選に使うリストが今回と同一になる事がわかります。
しかし、診断メーカーの仕様上、リストからは「異なるリスト」の抽選結果しか参照する事ができません。 ここで、以下のように抽選回数の奇偶によりリストを分ける工夫を行います。こうすることにより、同一の状態に遷移する場合でも、次回必ず異なるリストを使用するため、診断メーカー上でも正しくリスト間の移動を行うことが可能になります。

リスト名 リストを使用する条件
[LIST1] 状態 D かつ 奇数回目の抽選
[LIST2] 状態 D かつ 偶数回目の抽選
[LIST3] 状態 C かつ 奇数回目の抽選
[LIST4] 状態 C かつ 偶数回目の抽選
[LIST5] 状態 B かつ 奇数回目の抽選
[LIST6] 状態 B かつ 偶数回目の抽選

f:id:yryrrrrryryr:20191201040344p:plain:w400
図3 抽選回数の奇偶によるリストの分割

抽選確率の精度を高める

抽選されたリストが再び使われた場合、無限ループに陥り、診断結果が出力されなくなります。そのため、一度抽選されたリストは、次回以降の抽選で使われないように設定する必要があります。
一方で、1度使用したリストの値を削除するとその後の抽選確率が変化してしまいます。この影響を最小限に抑えるためには、

  • リストの値の個数を可能な限り増やす
  • リストが再帰的に参照される回数を可能な限り減らす

ことが有効です。
今回は、後者の「リストが再帰的に参照される回数を可能な限り減らす」方針で、1回のリスト参照で抽選する文字数を増やしてみましょう。

「ち」「ん」「ぽ」の3種類の文字を一列に n 個並べたときの場合の数は、 3^n 通りです。 ここで、各リストの値の数は999個以下である必要があり、この制約条件を満たす最大の n は6となります。

1回のリスト参照で6文字抽選する場合、抽選前の状態に応じた、抽選後の状態の場合の数は以下のとおりとなります。

抽選後 状態D 状態C 状態B 状態A (完成時の文字数)
6 5 4 3 2 1
抽選前 状態 D 331 216 75 26 27 27 27 0 0
状態 C 291 190 66 23 24 27 27 81 0
状態 B 216 141 49 17 18 18 27 0 243

文字数をカウントする

診断メーカーには、文字数をカウントする関数こそ用意されていないものの、リストから参照された数値の合計を計算する SUMLIST 関数が実装されています。この関数では、実際に結果に表示されなくとも、関数内で使用されたリストの数値は合計対象に含まれます。
そこで、[LIST1][LIST6]の文字列値の後ろに、条件に関わらず何も表示しないIF関数を挿入し、条件文中で文字数と等しい数値が格納されたリストを参照しています。
リスト数には限りがあるため、[LIST8] および [LIST9] に 6 を、[LIST10] に 1 をそれぞれ格納し、2文字~5文字の抽選結果については、枝番を変えて[LIST10]を複数回参照することで文字数をカウントさせています。

(例1) 6文字の抽選結果の場合

ちちちちんぽ=IF([LIST8_1]=0,"","")

(例2) 3文字の抽選結果の場合

ちんぽ=IF(=CALC([LIST10_1]+[LIST10_2]+[LIST10_3])=0,"","")

特殊演出を表示させる

少ない文字数で「ちんぽ」を出せた場合に、評価およびアスキーアートを表示させています。
なお、複数行にわたるアスキーアートは診断結果基本テキストに直接書き込むことはできないので、 [LIST7] 内にアスキーアートを格納しています。

  • 3文字~5文字→ EXCELLENT評価 (出現確率:11.11%)
\( ^ω^)/ EXCELLENT!!
  \  \
   \   γ∩ミ
    ⊂:: ::⊃)
      /乂∪彡\
  • 6文字~10文字→ GREAT評価 (出現確率:16.48%)
(/^ω^)/ 〇U〇 GREAT!

4. Twitterの診断結果を分析する

診断結果を取得する

作成した診断メーカーが診断結果ツイートを通して広く拡散し、沢山の人に遊ばれると嬉しいものです。診断メーカーのアドレスを検索窓に入力して、個々のツイートや診断結果に続く一言コメントを見るのが趣味の一つになっているといっても良いでしょう。
さて、診断結果ツイートが数百、数千と蓄積していくと、個々の診断結果だけでなく、「気に入った結果のときだけ診断結果をツイートしていないだろうか?」といった疑問に対する統計的な答えを知りたくなることがあります。
このような欲望に応えてくれるAPIが、twitter開発者アカウントを登録すると使用可能になる"Search API Full-Archive"です。これを用いると、Twitterサービス開始以来の全ツイートを対象としたツイートの検索および取得ができます。
なお、このサービスには無料版(Sandbox) と 有料版(Premium) があります。有料版の料金は最低でも月額$99 からと、趣味用途に用いるには高額ですが、無料版でも月間50回まで、1回あたり最大100ツイートの検索および取得が可能です。

サービスの詳細は以下のリンクを参考にしてください。 https://developer.twitter.com/en/account/subscriptions/search-fullarchive

さて今回は、無料版のAPIを用いて、診断作成日(2019/01/02)以降で、「ちんぽ揃えゲーム」のアドレス(https://shindanmaker.com/855159)を含む診断結果を取得してみました。取得したツイート数は下記のとおりで、無料版の範囲内に収まりながらも統計的な考察をするには十分なサンプルが集まったかと思います。

  • 取得に要した検索回数: 33回 / 上限50回
  • 取得できたツイート数(RTを除く): 3,031ツイート

また、診断メーカーサイト上での診断人数カウントは 10,922人 (2019/11/30 16:00現在) ですので、診断ページにアクセスした人のうち3割弱(27.8%)が診断結果をツイートしていたことも分かりました。

診断結果ツイートの偏りを調べる

この節では、EXCELLENT評価(3文字~5文字) および GREAT評価(6文字~10文字) が出現する確率が、理論的に期待される確率と有意差があるか調べることを目標に、データ処理や検定を行っていきます。

正規表現による検索

次に、正規表現を用いて、ちんぽ揃えに成功したときの文字列を抽出しました。

  • 文頭が「ち」「ん」「ぽ」のみで構成された文字列で、その後に左括弧が続くものを検索します。
 ^[ちんぽ]+\(
  • また、最後の1文字でちんぽ揃えに成功した場合として、文頭が「ち」「ん」「ぽ」のみで構成された文字列で、その後に"ちんぽ…"が続くものを検索しました。
^[ちんぽ]+ちんぽ

事後確率の計算

「特殊演出を表示させる」の節で、EXCELLENT評価 と GREAT評価 が出現する確率はそれぞれ 11.11% と 16.48% となる事を紹介しました。しかし、正規表現で抽出したツイート中での各評価の出現確率は、先述した確率よりも高くなります。
これは、「ちんぽ」を揃えるまでにツイートが省略されるほど文字数の多い診断結果は、正規表現にマッチしなくなり、サンプルに含まれなくなるからです。

2019年11月6日以降の診断メーカーのシステムでは、診断結果本文に最大120文字が使用可能です。本診断では、120文字から以下の10文字を除いた110文字が、「ちんぽ」を揃えるためのボーダー文字数になります。

  • ハッシュタグ「 #ちんぽ揃えゲーム」(全角9文字分、半角スペース含む)
  • 途中省略が発生した場合、省略記号の「…」(全角1文字分)

さて、正規表現で抽出したツイート中での各評価の出現確率は、「110文字以内にちんぽ揃えに成功した」という情報が加わったときの事後確率と考えることができます。110文字以内に「ちんぽ」が揃う確率が98.80%である事を考慮すると、求める確率は以下の通りとなります。

評価 事前確率 事後確率
EXCELLENT 11.11% 11.25%
GREAT 16.48% 16.68%

ツイートの取捨選択

上記で求めた出現確率は、「110文字以内にちんぽ揃えに成功した」ツイートが母集団であるときに成り立つものです。何らかの理由でちんぽ揃えの成功条件となるボーダー文字数が異なるツイートが含まれる場合、以下のように取捨選択の処理を行いました。

  • 成功条件のボーダー文字数が110文字を超えるツイート → 111文字以上でちんぽ揃えに成功したツイートのみを除外

これは、本診断の診断作成日(2019年1月2日)から現在までの間に、診断メーカーの仕様変更(2019年11月6日)が行われたことに起因します。
仕様変更以前は、診断結果に「#shindanmaker」タグが付与されなかったぶん診断結果に使用可能な文字数が現在よりも多く、成功条件のボーダーもその分緩くなっていました。現在のボーダー文字数に基準を合わせるため、111文字以上でちんぽ揃えに成功したツイートは集計の対象外としました。


  • 成功条件のボーダー文字数が110文字未満のツイート → 全て除外

失われた分布(実際には110文字以内でちんぽ揃えに成功していたにも関わらず抽出不能なもの)は、どんなに頑張っても復元することはできません。よって、診断結果本文に使用可能な文字数を減少させうる条件が含まれるツイートは、文字数の結果によらず全て集計の対象外としました。
具体的には、文頭に「ち」「ん」「ぽ」以外の文字が含まれるツイートを除外しました。対象となったツイートのほとんどは、他者への@リプライとして診断結果をツイートしたものでした。

以上の手順で、合計2,964件のサンプルを抽出し、ちんぽが揃うまでの文字数を集計しました。結果は図4のとおりです。

f:id:yryrrrrryryr:20191202032132p:plain:w400
図4 文字数の期待値および実測値の出現回数分布

二項検定の実施

最後に、「各評価のツイート中の出現率は、診断結果での理論的な出現率と等しい」と帰無仮説を設定し、有意水準 α=0.05 の両側二項検定を行いました。

評価 期待値 実測値 Z統計量 p値
EXCELLENT 333.32 561 13.24 <0.001
GREAT 494.42 526 1.56 0.060

結果として、EXCELLENT評価は、理論的な期待値よりも有意に多く出現している(1.68倍)が、GREAT評価は、理論的な期待値と有意差があるとはいえない事が分かりました。

診断結果に偏りが見られる原因については、

  • 良い結果が出た場合のみツイートをしている
  • 良い結果が出るまで名前を変えつつ何度も診断してからツイートしている
  • 良い結果に改ざんしてツイートをしている

などが考えられます。3番目のような不正行為はやめましょうね!

5. おわりに

本記事では、作成した「ちんぽ揃えゲーム」について、確率的な考察や、実装方法の解説、診断結果の統計分析などを雑多に解説してみました。 私がブログを開設して初めて作った記事という事もあり、想像以上に筆の進みが遅く、まとまった量の文章を書くには普段からの鍛錬が必要であることを痛感させられました。

最後に、診断メーカーを経由せずに「ちんぽ揃えゲーム」をプレイする方法を紹介したいと思います。

シェル芸bot(@minyoruminyon)にフォローバックされる必要がありますが、上記ツイートをコピーしてツイートすると、引用リプライで結果が返ってきます。
シェルスクリプトであれば1行で書けてしまうこのゲームを、診断メーカーで実装しようとすると何十倍もの労力がかかるのは事実です。しかし、何万人あるいは何十万人に診断を楽しんでもらえるのは、抜群に利用者の多い診断メーカーというプラットフォームがあってこその事でしょう。
皆様の診断結果ツイートを励みにして、今後も時間を見つけながら新しい診断メーカーを作っていきたいと思います。

(参考1) 診断メーカーの仕様

診断メーカーの基本データは、「診断結果基本テキスト」と「リスト」で構成されます。

  • 診断結果基本テキスト

    • 診断をした際に「診断結果」として表示される文章のベースとなる部分
    • 全てのリストから抽選結果を呼び出し可能
    • 改行記号[BR]を直接的に使用できない
  • リスト

    • 抽選結果の候補となる「値」を格納する部分
    • 異なるリストから抽選結果を呼び出し可能
    • 改行記号[BR]の使用が可能

リストの呼び出し方法

[LISTx\_y, z] (y, zは省略可能)

記号 意味
x リストの番号
y 枝番 (枝番が異なると、異なる抽選結果が得られる)
z 値の要素番号 (値がカンマで区切られている場合のみ有効)

制約条件

リスト上の制約

作成可能なリストの上限数は10個です。また、リストごとに以下の制約があります。

対象 上限値
値の数 999
各値の文字数※1 300
値の合計バイト数※2 65535

※1 全角文字、半角文字を問わず1文字として計算
※2 全角文字は3バイト、半角文字は1バイトとして計算

診断結果本文の自動カット

ツイートボタン経由のツイート、および「コピペ用(140文字)」の診断結果では、文字数が長い診断結果の全角換算文字数が139文字以下になるように末尾が省略されます。

  • 診断結果本文に使える全角換算文字数は120文字以下 (ツイートの際、#shindanmaker タグおよび診断メーカーへのリンクが自動で付与されるため)
  • 120文字を超過した場合、119文字目までが有効、以降は「…」として省略される
  • 自分でハッシュタグを設定した場合、その分だけ本文に使える文字数が減る (診断結果の後にタグが付与されるため)

関数の仕様

SUMLIST(x_{1},x_{2},...x_{n}) 関数

  • リストから参照された数値の合計を計算する
  • x_{n} は、カウント対象とするリストの番号
  • [LISTx\_y]について、リスト番号 x と枝番 y が同一のものが複数回参照された場合、1回のみカウント

(参考2) 「ちんぽ」が揃う確率の一般項を求める

ゲームのルールのシンプルさとは裏腹に、「ちんぽ」が揃う確率の一般項はやや複雑なものとなります。

ここでは、n 回以内の操作で「ちんぽ」が揃う確率 a_{n} の一般項について、簡単な導出過程を含め紹介していきます。

まず、ちょうど (n+3) 回の操作で「ちんぽ」が揃う必要十分条件は以下の通りです。

  • n 回目の操作終了時に、まだ「ちんぽ」が揃っていない
  • (n+1)〜(n+3) 回目の操作で、「ちんぽ」の 3 文字が続く

このことは、式(1)のように漸化式で表すことができます。

a_{n+3}-a_{n+2}=\dfrac{1}{27} (1-a_{n}) \tag{1}


(1)を整理し、式(2)のように変形します。式(2)の形は一般に「定数係数線形漸化式」と呼ばれます。

27a_{n+3}-27a_{n+2}+a_{n} = 1 \tag{2}


漸化式を満たす具体的な解の 1 つを「特解」、(漸化式の右辺) =0 としたときの一般解を「斉次解」としたとき、一般解は「特解」と「斉次解」の和として求めることができます。


まず、a_{n} = 1 (定数数列) は、式(2)を満たす解の 1 つであるため、式(2)の特解となります。 次に、斉次解を求めるため、式(2)の右辺を 0 とした式(3)の一般解を求めていきましょう。

27a_{n+3}-27a_{n+2}+a_{n} = 0 \tag{3}


(3)中の a_{n+k}x^{k} に置き換えた特性方程式(4)を考えます。

27x^{3}-27x^{2}+1=0  \tag{4}


なお、式(4)は下表のとおり、異なる3つの実数解 p_{1},p_{2},p_{3} をもちます。

記号 近似値
p_{1} \dfrac{1+2cos(\dfrac{7\pi}{9})}{3} -0.177363
p_{2} \dfrac{1+2cos(\dfrac{5\pi}{9})}{3} 0.217568
p_{3} \dfrac{1+2cos(\dfrac{\pi}{9})}{3} 0.959800


(4)の解のうち 1 つを p とします。

a_{n} = C \cdot p^{n-1} とおき、式(3)に代入することで、式(5)を得ます。

 C \cdot p^{n-1}  (27 p^{3} - 27 p^{2} + 1) = 0 \tag{5}


p は 式(4)の解であることから (27 p^{3} - 27 p^{2} + 1) = 0 となり、式(5)C の値によらず恒等的に成立します。

p_{1},p_{2},p_{3} について同様のことがいえるため、C_{1}〜C_{3}を任意定数として、式(2)の斉次解は式(6)のように書き表すことができます。

a_{n} = C_{1} \cdot p_{1}^{n-1} + C_{2} \cdot p_{2}^{n-1} + C_{3} \cdot p_{3}^{n-1} \tag{6}


なお、C_{1}〜C_{3}は、式(6)a_{1}=0, a_{2}=0, a_{3}=\dfrac{1}{27}を代入し連立方程式を解くことで、下表のとおり求まります。

記号 近似値
C_{1} \dfrac {2\sqrt{93}}{27} cos(\theta+\dfrac{\pi}{3})-\dfrac{1}{3} 0.0124236
C_{2} \dfrac {2\sqrt{93}}{27} cos(\theta-\dfrac{\pi}{3})-\dfrac{1}{3} 0.0351339
C_{3} \dfrac {2\sqrt{93}}{27} cos(\theta-\pi)-\dfrac{1}{3} -1.04756

ここで、 \theta は定数で、 \theta = \dfrac{1}{3}tan^{-1}(\dfrac{19}{199\sqrt{3}}) です。


以上より、特解と斉次解を足し合わせた式(7)が求める一般項となります。

a_{n} = C_{1} \cdot p_{1}^{n-1} + C_{2} \cdot p_{2}^{n-1} + C_{3} \cdot p_{3}^{n-1} + 1 \tag{7}

(参考3) ソースコード

診断に使用した基本診断テキストおよび各リストをgithubに公開したので、興味のある人はご覧ください。 github.com