AI関連技術

Pythonとティーチャブルマシンで作る!カメラで人間のじゃんけんを認識してAIと対戦する方法(初心者OK)

Pythonとティーチャブルマシンで作る!カメラで人間のじゃんけんを認識してAIと対戦する方法

今日は、とあるセミナーで好評だったネタを紹介します。

Pythonとティーチャブルマシンを使って、カメラで人間のじゃんけんの手を認識し、AIと対戦する方法を初心者向けに解説します。

AIとのじゃんけん対戦と聞くと難しそうに思えるかもしれませんが、安心してください。

この記事では、プログラミング初心者でもできるように丁寧に解説していきます

プログラミングの基礎知識がない方でも楽しみながら学べる内容ですので、一緒にAIの楽しい世界を体験してみましょう!

開発環境

はじめに、このプロジェクトに取り組むために必要なツールと環境設定について説明します。

Pythonのバージョン

まず、Pythonのバージョンですが、動画内では3.11を使用しています。

実際はPython3.7以上であれば問題ありません。

統合開発環境(IDE)

次に、統合開発環境(IDE)ですが、私はPyCharmというアプリケーションの無料で使えるコミュニティ版を使用して解説しています。

PyCharmはとてもおすすめです。

詳しくはこちらの記事を参考にしてください。

しかし、他の環境(例えばVisual Studio CodeやJupyter Notebook、メモ帳など)を使っても問題ありません。

重要なのは、Pythonコードを書き、実行できる環境があることです。

使用するライブラリ、フレームワーク

最後に、必要なライブラリ関係についてです。

ライブラリとは、Pythonのコードを書く際に特定の機能を簡単に実装できるようにするための拡張機能のようなものです。

このプロジェクトではOpenCVとTensorFlowをインストールします。

実は、numpyも使用するのですが、上記をインストールすると依存関係でインストールされるので個別にはインストール不要です。

ターミナルやコマンドプロンプトでインストールする場合はこちらのコマンドを使ってください。

pip install opencv-python
pip install tensorflow

*似たような名前のものが多いので、注意してください。

初心者の方から、コマンドプロンプトを使ったインストールについて質問が多いので以下に参考記事を載せておきます。

この記事の内容で使用するライブラリの目的は、画像処理にはOpenCVを、機械学習のモデルにはTensorFlow(正確にはその一部であるKeras)とnumpyを使用します。

今回私が使用した主なライブラリのバージョンを記載しておきます。

tensorflow:2.15.0
opencv:4.9.0.80
numpy:1.26.4
scikit-learn:1.41.1

全く同じではなくても動作しますので、厳密に合わせる必要はありません。

なお、動画内でも説明している通り、エラーが出る場合には特定のバージョンのライブラリをインストールする必要があります。

その場合、以下のようにバージョンを指定したインストールが必要です。

pip install tensorflow==2.4.0

*PyCharmではとても簡単にバージョンを変更できます。

講座の全体像

この記事の内容は、以下の動画を一部再編集してテキスト化したものです。

全部で3つの動画に分かれています。

ステップ1:じゃんけんゲームの基礎

*文字が小さくて見づらいと思うので、最大化するかYouTubeでご覧ください。

ステップ2:ティーチャブルマシンを使って人間の手を画像で認識する方法

*文字が小さくて見づらいと思うので、最大化するかYouTubeでご覧ください。

ステップ3:コンピュータの手をAIで強くする方法

詳しい解説を聞きながら一緒に進めたいという方は、ぜひ動画をご覧ください。

ただし、Pythonの基礎については動画内で触れていませんので、心配な方は以下記事内の解説をご覧になってから動画を視聴されると分かりやすいと思います。

また、動画内のスクリプトの全文はこの記事内に記載しておりますので、必要に応じてお使いください。

Pythonの基礎知識

本講座内で使用しているPythonに関する最低限の基礎知識を解説しておきます。

具体的に、以下の点について知っておくと理解が深まります。

・print文

・intへの型変換

・inputモジュールの使い方

・randomモジュールのrandintの基礎

・リストの基礎

・if/elif/elseによる条件分岐

・while Trueの基本

*不要な方は飛ばしてください。

print文

print文は、メッセージや変数の内容を画面に表示するために使用されます。

これはPythonプログラミングで最も基本的な機能の一つです。

print("Hello, World!")  # 画面に"Hello, World!"と表示します

*実行結果を表示したり、ユーザーに情報を伝えるために使用します。

今回のじゃんけんゲームでは、プレイヤーとコンピュータが何を出したか?勝敗結果などを出力するために使用しています。

inputメソッドの使い方

input()メソッドは、ユーザーからの入力を受け取るために使用されます。

入力された内容は、数値であっても文字列として扱われます。(これ重要!)

user_input = input("あなたの名前は何ですか? ")
print("こんにちは、" + user_input + "さん!")

*inputメソッドは、処理が正しく動いているか?を確認するのに重宝します。

今回のじゃんけんゲームでは、最初の段階でユーザーにじゃんけんの手を入力してもらうために使います。

後半では、じゃんけんの手を画像認識するので使いません。

intへの型変換

Pythonでは、変数の型を変更することができます。

文字列を整数(int)に変換するには、int()関数を使用します。

number_str = "5"
number_int = int(number_str)  # 文字列"5"を整数5に変換
print(number_int)

*見た目は「数字」でも、内部的(データ型)に「文字」と扱われている場合に「整数」へ変換する必要があります。

今回のじゃんけんゲームでは、inputメソッドで入力された「文字列」である「012」という数字を「数値」として「012」に変換するために使用しています。

randomモジュールのrandintの基礎

randomモジュールのrandint()関数は、指定された範囲の整数の中からランダムに数値を選びます。

import random

random_number = random.randint(1, 10)  # 1から10の範囲でランダムな数値を生成
print(random_number)

*randomモジュールは標準モジュールなので、インストール不要で利用できます。

今回のじゃんけんゲームでは、最初の段階でコンピュータの手を乱数で生成するために使用しています。

リストの基礎

リストは角括弧([])を使って定義され、その中に任意の数の要素をコンマで区切って含めることができます。

リストの各要素には、インデックス(0から始まる番号)を使ってアクセスします。

詳しくはこちらの記事でも解説しています。

以下に使用例を紹介しておきます。

# リストの作成
fruits = ["apple", "banana", "cherry"]

# リストの要素にアクセス
print(fruits[0])  # 出力: apple
print(fruits)  # 出力: banana
print(fruits)  # 出力: cherry

*リストを作成し、それを表示する方法を示しています。

詳しい解説は、この記事では省略します。

if/elif/elseによる条件分岐

条件によって、処理を切り替えるためにif/elif/elseを使用します。

詳しくはこちらの記事でも解説しています。

以下に使用例を紹介しておきます。

number = 5

if number > 10:
    print("数値は10より大きいです。")
elif number < 10:
    print("数値は10より小さいです。")
else:
    print("数値は10です。")

*この場合、numberは5なので「数値は10より小さいです」となります。

詳しい解説は、この記事では省略します。

while Trueの基本

while Trueループは、特定の条件が満たされるまで無限にループする構造です。

詳しくはこちらの記事でも解説しています。

while True:
    user_input = input("終了するには'exit'と入力してください: ")
    if user_input == "exit":
        break
    print("あなたの入力: " + user_input)

*ループを抜けるためには、breakステートメントを使用します。

breakとcontinueの違いがわかりづらいという方は多いので、不安な方はこちらの記事も参考にしてください。

本編でも、初心者の方にはこの部分が少しわかりづらいかも知れません。

詳しい解説は、この記事では省略します。

ティーチャブルマシンとは?

ティーチャブルマシンは、機械学習のモデルを簡単に作成できるツールです。

詳しくはこちらの記事(動画)でも紹介しています。

プログラミングの知識がなくても、ノーコードでモデルをトレーニングすることができます。

今回はこのツールを使用し、じゃんけんの手の画像を学習させ、それを認識できるAIモデルを作成しています。

本当に、プログラミング初心者でもできるので安心してください。

AIじゃんけんゲームを実現する3ステップ

ここから本題です。

今回は3つのステップに分けて解説しています。

ステップ1:じゃんけんゲームの基礎

Pythonで基本的なじゃんけんゲームを作成することから始めましょう。

このステップでは、ユーザーからじゃんけんの手(グー、チョキ、パー)を入力として受け取り、コンピュータがランダムに手を選ぶ簡単なプログラムを作成します。

ただ、最初から全て完成するより、段階的に作成する方が理解が深まるので、以下の3段階で紹介します。

動画でご覧になる方はこちらからどうぞ!

その1

ユーザーは「グーチョキパー」のように「文字」で入力します。

コンピュータは「グー」に固定しています。

if文を使い、「引き分け」「勝ち」「負け」を判定しています。

print('じゃんけんゲームスタート!')
print('あなたの番です')
player = input('「グー、チョキ、パー」を入力してください:')
computer = 'グー'

print('========================')
print('あなたは ' + player + ' を出しました')
print('コンピュータは ' + computer + 'を出しました')

# じゃんけんの判定
if player == computer:
    print('引き分け')
# プレイヤーが勝つパターン
elif (player == 'グー' and computer == 'チョキ') or \
        (player == 'チョキ' and computer == 'パー') or \
        (player == 'パー' and computer == 'グー'):
    print('あなたの勝ち')
else:
    print('あなたの負け')

*最もシンプルなコードですが、冗長的であまり良いコードとは言えません。

その2

じゃんけんの手をリストに定義し、インデックス番号で呼び出すように修正します。

hands = ['グー', 'チョキ', 'パー']

print('じゃんけんゲームスタート!')
print('あなたの番です')
player = int(input('何を出しますか?数字を入力してください(0:グー、1:チョキ、2:パー):'))
player_hand = hands[player]
computer = hands[0] # グーを呼び出す

print('========================')
print('あなたは ' + player_hand + ' を出しました')
print('コンピュータは ' + computer + 'を出しました')

# じゃんけんの判定
if player_hand == computer:
    print('引き分け')
# プレイヤーが勝つパターン
elif (player_hand == 'グー' and computer == 'チョキ') or \
        (player_hand == 'チョキ' and computer == 'パー') or \
        (player_hand == 'パー' and computer == 'グー'):
    print('あなたの勝ち')
else:
    print('あなたの負け')

*現在、コンピュータは「グー」を固定しています。

その3

これまで、コンピュータは「グー」を固定していますが、これを「グー、チョキ、パー」からランダムで選択するよう修正します。

import random

hands = ['グー', 'チョキ', 'パー']

print('じゃんけんゲームスタート!')
print('あなたの番です')
player = int(input('何を出しますか?数字を入力してください(0:グー、1:チョキ、2:パー):'))
player_hand = hands[player]
computer_hand = hands[random.randint(0, 2)]

print('========================')
print('あなたは ' + player_hand + ' を出しました')
print('コンピュータは ' + computer_hand + 'を出しました')

# じゃんけんの判定
if player_hand == computer_hand:
    print('引き分け')
# プレイヤーが勝つパターン
elif (player_hand == 'グー' and computer_hand == 'チョキ') or \
        (player_hand == 'チョキ' and computer_hand == 'パー') or \
        (player_hand == 'パー' and computer_hand == 'グー'):
    print('あなたの勝ち')
else:
    print('あなたの負け')

*これで完成です。

しかし、この状態はまだいくつか問題があります。

  • ・ユーザーが0〜2以外を入力するとエラーになる
  • ・ユーザーが数値ではなく「グー」のように文字を入れるとエラーになる
  • ・ゲームは1回で終わってしまう

*このような問題は、次のステップ2で改善します。

ステップ2:ティーチャブルマシンを使って人間の手を画像で認識する方法

ここまで、プレイヤーのじゃんけんの手はinputメソッドによって0から2の数字を入力していました。

次のステップでは、人間の手をカメラで認識して「グー」「チョキ」「パー」を判定することができます。

先ほど紹介した「ティーチャブルマシン」というものを利用して、学習モデルを生成します。

サイトはこちら
https://teachablemachine.withgoogle.com/

ティーチャブルマシンの使い方は以下の記事でも解説しています。

今回のプロジェクトにおいて、じゃんけんの手を学習してPythonのスクリプトにする部分の解説は以下の動画を参照してください。

ティーチャブルマシンの基本コード

以下に、ティーチャブルマシンでトレーニング後に公開されているコードスニペットを掲載しておきます。

これを実行すると、認識したものをprint文でコンソールに出力し続けるだけです。

from keras.models import load_model  # TensorFlow is required for Keras to work
import cv2  # Install opencv-python
import numpy as np

# Disable scientific notation for clarity
np.set_printoptions(suppress=True)

# Load the model
model = load_model("keras_Model.h5", compile=False)

# Load the labels
class_names = open("labels.txt", "r").readlines()

# CAMERA can be 0 or 1 based on default camera of your computer
camera = cv2.VideoCapture(0)

while True:
    # ウェブカメラの画像を取得します。
    ret, image = camera.read()

    # 元の画像を(高さ224ピクセル、幅224ピクセル)にリサイズします
    image = cv2.resize(image, (224, 224), interpolation=cv2.INTER_AREA)

    # 画像をウィンドウに表示します
    cv2.imshow("Webcam Image", image)

    # 画像をnumpy配列に変換し、モデルの入力形状に合わせて変形します。
    image = np.asarray(image, dtype=np.float32).reshape(1, 224, 224, 3)

    # 画像配列を正規化します
    image = (image / 127.5) - 1

    # モデルで予測します
    prediction = model.predict(image)
    index = np.argmax(prediction)
    class_name = class_names[index]
    confidence_score = prediction[0][index]

    # 予測結果と信頼度スコアを表示します
    print("Class:", class_name, end="")
    print("Confidence Score:", str(np.round(confidence_score * 100))[:-2], "%")

    # キーボード入力を待ちます。
    keyboard_input = cv2.waitKey(1)

    # 27はキーボードのescキーのASCIIコードです。
    if keyboard_input == 27:
        break

camera.release()
cv2.destroyAllWindows()

*コメントは分かりやすいように日本語に翻訳済みです。原本は英語です。

実行するとこのように表示され、認識結果がコンソールに表示されます。

camera = cv2.VideoCapture(0)

*この部分の(かっこ)内は0ならインカメラ、1なら外付けUSBカメラを利用できます。

ただ、このままじゃんけんゲームと組み合わせても、不要な部分が邪魔をします。

必要な部分を組み合わせ、一部修正が必要です。

ティーチャブルマシンの学習モデルとじゃんけんゲームの基本コードを組み合わせる

ステップ1で作成したじゃんけんゲームの基本コードと、ティーチャブルマシンのサイトで公開されている基本コードを組み合わせます。

その解説はブログでは行なっていません。詳しくは動画をご覧ください。

完成したコードは以下の通りです。

from keras.models import load_model
import cv2
import numpy as np
import random

np.set_printoptions(suppress=True)
model = load_model("keras_Model.h5", compile=False)
class_names = open("labels.txt", "r").readlines()

camera = cv2.VideoCapture(0)

hands = ['グー', 'チョキ', 'パー']

# ここからゲームを開始する
print('じゃんけんゲームスタート!')

while True:
    print('カメラにじゃんけんの手を写し、Enterキーを押してください')

    while True:
        # ウェブカメラの画像を取得します。
        ret, image = camera.read()
        if not ret:
            print('カメラから画像を取得できませんでした')
            continue

        # 元の画像を(高さ224ピクセル、幅224ピクセル)にリサイズします
        image = cv2.resize(image, (224, 224), interpolation=cv2.INTER_AREA)

        # 画像をウィンドウに表示します
        cv2.imshow("Webcam Image", image)

        # エンターキーを押したらループを抜ける
        if cv2.waitKey(1) & 0xFF == 13:
            break
        # エスケープキーを押したらループを抜ける
        if cv2.waitKey(1) & 0xFF == 27:
            break

    # 画像をnumpy配列に変換し、モデルの入力形状に合わせて変形します。
    image = np.asarray(image, dtype=np.float32).reshape(1, 224, 224, 3)

    # 画像配列を正規化します
    image = (image / 127.5) - 1

    # モデルで予測します
    prediction = model.predict(image)
    index = np.argmax(prediction)

    # プレイヤーの手を確定する
    player_hand = hands[index]
    computer_hand = hands[random.randint(0, 2)]

    print('========================')
    print('あなたは ' + player_hand + ' を出しました')
    print('コンピュータは ' + computer_hand + 'を出しました')

    # じゃんけんの判定
    if player_hand == computer_hand:
        print('引き分け')
    # プレイヤーが勝つパターン
    elif (player_hand == 'グー' and computer_hand == 'チョキ') or \
            (player_hand == 'チョキ' and computer_hand == 'パー') or \
            (player_hand == 'パー' and computer_hand == 'グー'):
        print('あなたの勝ち')
    else:
        print('あなたの負け')

camera.release()
cv2.destroyAllWindows()

*実行すると時間がかかるので、焦らず待ちましょう。

このスクリプトでは、カメラにじゃんけんの手を映してEnterを押すと認識した手を使ってじゃんけんを行います。

また、ゲームは一度ではなく繰り返されるように修正しています。

ESCキーを押すと、ゲームを終了することができます。

うまく認識しない時の注意点

ティーチャブルマシンを使って学習モデルを作成する際、カメラの背景によっては正しく認識しないことがあります。

また、実行時にエラーが出る場合の原因はいくつかありますが、私がセミナーで多く散見した事例は以下の通りです。

・urllib3のバージョンが2系ではなく1系にしたらエラーが解消された

・ブラウザでティーチャブルマシンのサイトを開いたままだとカメラが使われているので、Pythonのスクリプトでカメラが使えない状態となっていた

そのような注意点も動画内で詳しく解説しています。

なお、今回は私が実際に手を使って撮影したじゃんけんの学習モデル.h5ファイルとラベルファイルをこちらに置いておきます。

*ファイル名をクリックすると、Googleドライブ上のファイルをダウンロードできます。

label.txt

keras_model.h5

もし、上手く行かないときはこちらを組み合わせると間違いなく動作します。

それでも動かないという場合、他の問題が考えられます。

ステップ3:コンピュータの手をAIで強くする方法

最後に、コンピュータのじゃんけんを強くします。

じゃんけんが強いとは、どういうことか?といえば、「できるだけ多く勝つ」ということですね。

では、じゃんけんの攻略法って何だと思いますか?

この記事では、一般的なじゃんけんの攻略法に基づいて解説します。

AIによるゲームの攻略

初めに、この記事の主題でもある「AIによってじゃんけんの手を強くする」という点について説明します。

一般的なAIでは、「機械学習」や「深層学習」という用語が一般的です。

AIに関する解説や定義についてはこの記事を参考にしてください。

囲碁や将棋、オセロといった「ゲーム」に用いられるAI技術には「強化学習」というものがあります。

しかし、この「強化学習」を用いた「強いじゃんけんAI」を実現しようとすると、少々複雑で難しくなります。

そこで、今回は強化学習を用いる方法は使いません

ただ、せっかくAIについての記事を書いているので強化学習についても解説だけ入れておきます。

興味があればご一読ください。

強化学習とは?

強化学習では、エージェント(この場合はコンピュータ)が環境(じゃんけんゲーム)とのやりとり(ゲーム)をしながら、勝利(報酬)を最大化する行動を学習することを目的とします。

具体的には、コンピュータはじゃんけんでの各ラウンドの結果から学び、どの手(グー、チョキ、パー)を出すと勝率が高くなるかを理解するようになります。

これにより、コンピュータは単なるランダム選択ではなく、戦略的な手を選択するようになります。

強化学習には、Q学習というアルゴリズムがあるのですが、今回はそのQ学習をじゃんけんように簡素化したモデルを使用します。

Q学習は、状態と行動のペアに対する価値を推定することができるアルゴリズムです。

例えば、自分が「グー」で相手が「チョキ」だったとき、勝利します。この場合、「正の報酬」を得ることができます。

反対に、自分が「パー」で相手が「チョキ」だったとき、敗北します。この場合、「負の報酬」が与えられます。

このような繰り返しの中で、少しでも「正の報酬」つまり「勝利」を得るための手を選択します。

Q学習では、勝負を繰り返しながらその結果を記録し、各状態でどの手を選択すべきかを決定します。

じゃんけんのようなシンプルなゲームでは、比較的シンプルなQ学習アルゴリズムが効果的です。

ただし、今回はこの強化学習ではない、もっと簡単な処理を紹介します。

今回のじゃんけん攻略法

さて、強化学習ではない方法で、できるだけ分かりやすい処理はないか?を考えました。

じゃんけんについてウェブで検索したとき、「日本じゃんけん協会」というサイトを見つけました。

日本じゃんけん協会のサイト

コチラのサイトでは、勝利の法則10か条というものが紹介されています。

例えば、

初心者は最初にグーを出す

という法則があります。

また、

あいこのときは、「あいこの手」に「負ける手」を次に出すと勝ちやすい

という法則もあります。

こちらで紹介されている法則が「絶対に正しい」という訳ではないということはあらかじめご了承ください。

(人によって、「最初はパーを出す」という人も多いようですし。)

今回は、この「日本じゃんけん協会」の「勝利の法則10か条」を要件にしてコンピュータの手を判定する処理を追加します。

ただし、このうち

  • 5番「拳読」
  • 6番「宣言する」
  • 8番「上級者にはグー」
  • 9番「先見せ」

については「人間同士のじゃんけんによる心理戦」でのみ有効となるため、今回は使用しません。

また、その他の項目でも「人間対人間」で有効なものは使わないことにしています。

これは「AI」なのか?

ここまでの説明で、「これってAIって言わないんじゃないの?」と思われたかもしれません。

「AIって、もっとこう、頭のよさそうな?人間の思考をプログラムにしたものでしょ?」

そう言われることは良くあります。

実は、AIと呼ばれる技術は1950年頃から存在しており、当初は推論や探索でカンタンなパズルやルールを解くものを「AI」と呼んでいました。

いわゆる「第一次AIブーム」の頃の話です。

つまり、現代でイメージされるAIは先進的なものかもしれませんが、広義ではこれも立派なAIと言えます。

機械学習や深層学習といった最新のAI技術ではなく、この程度のAIでもコンピュータのじゃんけんを強くする処理を考えてみましょう。

勝利の法則10か条を処理にする

それでは、具体的に「勝利の法則10か条」をどのようにスクリプト(処理)にするのか?を考えましょう。

10か条を全てこの記事で紹介することはしませんので、詳しくは日本じゃんけん協会のサイトをご覧ください。

一つ例を出すと、

初心者は最初にグーを出す

2回目はチョキを出す

という法則があります。

これは、コンピュータの手を決める際に「そのじゃんけんが何回目か?」によって処理を変えることができます。

今回は、ゲームの記録をリストに残してその記録からコンピュータが手を出します。

例えばこのようなリストを用意します。

previous_rounds = [] 

そのじゃんけんが1回目なのか?2回目なのか?という基準は「このリストに記録があるかどうか?」という条件なので、

# 1回目はパーを出す(法則①に基づく)
if len(previous_rounds) == 0:
    computer_hand = 'パー'
elif len(previous_rounds) == 1:
    # 2回目はチョキを出す(法則②に基づく)
     computer_hand = 'チョキ'
else:
    # 3回目以降は前の手によって変わる

*len関数によってリスト内に値がいくつあるのか?を確認している

というように分岐させることができます。

3回目以降については、前の手によって更に処理が分岐されますが、今回は省略します。

基本のジャンケンスクリプトに勝利の法則を組み合わせる

まずは、ステップ1で作成したinputメソッドでじゃんけんする「基本スクリプト」に先ほどの「勝利の法則10か条」を組み合わせます。

その後、ステップ2で追加したティーチャブルマシンで人間の手を判定するスクリプトと組み合わせたものも完成品を紹介します。

import random

hands = ['グー', 'チョキ', 'パー']

print('じゃんけんゲームスタート!')
print('あなたの番です')
player = int(input('何を出しますか?数字を入力してください(0:グー、1:チョキ、2:パー):'))
player_hand = hands[player]
computer_hand = hands[random.randint(0, 2)]

print('========================')
print('あなたは ' + player_hand + ' を出しました')
print('コンピュータは ' + computer_hand + 'を出しました')

# じゃんけんの判定
if player_hand == computer_hand:
    print('引き分け')
# プレイヤーが勝つパターン
elif (player_hand == 'グー' and computer_hand == 'チョキ') or \
        (player_hand == 'チョキ' and computer_hand == 'パー') or \
        (player_hand == 'パー' and computer_hand == 'グー'):
    print('あなたの勝ち')
else:
    print('あなたの負け')

(再掲)これをベースにします。

組み合わせたものが以下の通りです。

import random

hands = ['グー', 'チョキ', 'パー']
computer_hand_counter = {'グー': 0, 'チョキ': 0, 'パー': 0}
previous_rounds = []

def decide_computer_hand(previous_rounds):
    global computer_hand_counter
    hand = None

    if not previous_rounds:
        hand = 'パー'
    else:
        last_round = previous_rounds[-1]
        last_player_hand = last_round[0]
        last_computer_hand = last_round

        if last_computer_hand == last_player_hand:
            if last_player_hand == 'グー':
                hand = 'チョキ'
            elif last_player_hand == 'チョキ':
                hand = 'チョキ'
            else:
                hand = 'グー'
        else:
            if last_computer_hand == 'チョキ':
                hand = 'チョキ'
            else:
                hand = 'パー'

    if computer_hand_counter[hand] >= 2:
        hand = hands[random.randint(0, 2)]
        computer_hand_counter = {key: 0 for key in computer_hand_counter}
    else:
        computer_hand_counter[hand] += 1

    return hand

def judge(player_hand, computer_hand):
    if player_hand == computer_hand:
        return 'draw'
    elif (player_hand == 'グー' and computer_hand == 'チョキ') or \
         (player_hand == 'チョキ' and computer_hand == 'パー') or \
         (player_hand == 'パー' and computer_hand == 'グー'):
        return 'win'
    else:
        return 'lose'

results = {'win': 0, 'lose': 0, 'draw': 0}

rounds = 0
while True:
    print('====================')
    print(f'第{rounds + 1}回戦')
    player = int(input('何を出しますか?数字を入力してください(0:グー、1:チョキ、2:パー):'))
    player_hand = hands[player]
    computer_hand = decide_computer_hand(previous_rounds)
    previous_rounds.append((player_hand, computer_hand))
    result = judge(player_hand, computer_hand)
    results[result] += 1

    print(f'あなたは {player_hand} を出しました')
    print(f'コンピュータは {computer_hand} を出しました')
    print(f'結果: {result}')
    print(f"現在の成績: 勝ち: {results['win']}, 負け: {results['lose']}, 引き分け: {results['draw']}")

    rounds += 1

*基本のスクリプトと比べると、関数が定義されているなど初心者には少し分かりにくい処理が含まれています。

なお、このスクリプトで行われている処理について、詳しい解説は動画で説明していますので、興味があればご視聴ください。

検証:AIで強くなったのか?

ここまで紹介した「勝利の法則」に基づくコンピュータの手によって、どれほど強くなったのか?を確認しましょう。

検証にあたり、プレイヤーの手は「ランダム」で出しています。

ステップ1の基本スクリプトで100回対戦した結果

コンピュータの成績: 勝ち: 28, 負け: 41, 引き分け: 31

コンピュータの勝率:28%(引き分けを除いた場合、40.5%)

ステップ3のAI追加スクリプトで100回対戦した結果

コンピュータの成績: 勝ち: 43, 負け: 22, 引き分け: 35

コンピュータの勝率:43%(引き分けを除いた場合、66.2%)

以上の通り、良い結果が出ました。

…というのは嘘で、実は少し不正をしていまして、「最も良い時の結果」を載せています。

実際にはステップ1の基本スクリプトでも、

基本スクリプトで100回対戦した結果(良い結果)

コンピュータの成績: 勝ち: 46, 負け: 26, 引き分け: 28

コンピュータの勝率:46%(引き分けを除いた場合、63.9%)

というように良い結果がでる場合もありました。

「むしろAIの方が弱いときあるじゃん笑」という結果もあるということです。

結論

「じゃんけんをAIで強くする」というテーマで解説してきましたが、そもそもじゃんけんは以下の性質であるということが分かりました。

  • 考えて出すより、適当に出した方が案外強い
  • 普通は100回勝負などなく、3回勝負程度では学習データを集める間もなくゲームは終わるので強くするのは難しい

上の例では「100回勝負」していますが、これを10000回にすると結果は「勝ち」「あいこ」「負け」がほぼ同じくなります。

10000回対戦した結果

コンピュータの成績: 勝ち: 3324, 負け: 3333, 引き分け: 3343

つまり、一時的に勝ちが多くなったり負けが多くなることはあるかもしれませんが、それぞれ1/3ずつに収束すると考えられます。

ということで、じゃんけんをAIで強くするのは現実的ではないのでは?というのが私の考えです。

みなさんはこの結論について、どう思われますか?

ぜひ、ご意見やアドバイスなどありましたらコメントでお知らせください。

参考:さらに改良する例

①ティーチャブルマシンの画像認識とAIじゃんけんを組み合わせる

ステップ2で作成した「プレイヤーの手をティーチャブルマシンで学習して画像認識する」処理とステップ3の「AIによるコンピュータの手の選択」を組み合わせましょう。

これについては、詳しい解説はしませんが、動作する完成品を載せておきます。

from keras.models import load_model
import cv2
import numpy as np
import random

# モデルとラベルの読み込み
model = load_model("keras_Model.h5", compile=False)
class_names = open("labels.txt", "r").readlines()

# カメラの設定
camera = cv2.VideoCapture(1)

# じゃんけんの手の選択肢
hands = ['グー', 'チョキ', 'パー']
computer_hand_counter = {'グー': 0, 'チョキ': 0, 'パー': 0}
previous_rounds = []

# 結果を記録するための辞書
results = {'win': 0, 'lose': 0, 'draw': 0}

# コンピュータの手を決める関数
def decide_computer_hand(previous_rounds):
    global computer_hand_counter
    hand = None

    if not previous_rounds:
        hand = 'パー'  # 最初の手
    else:
        last_round = previous_rounds[-1]
        last_player_hand, last_computer_hand = last_round

        if last_computer_hand == last_player_hand:
            if last_player_hand == 'グー':
                hand = 'チョキ'  # グーあいこの次はチョキ
            elif last_player_hand == 'チョキ':
                hand = 'チョキ'  # チョキあいこはチョキ
            else:
                hand = 'グー'    # パーあいこの次はグー
        else:
            if last_computer_hand == 'チョキ':
                hand = 'チョキ'  # 直前に勝った手を再び
            else:
                hand = 'パー'

    # 同じ手が3回続いた場合はランダムに手を選ぶ
    if computer_hand_counter[hand] >= 2:
        hand = hands[random.randint(0, 2)]
        computer_hand_counter = {key: 0 for key in computer_hand_counter}
    else:
        computer_hand_counter[hand] += 1

    return hand

# じゃんけんの勝敗を判定する関数
def judge(player_hand, computer_hand):
    if player_hand == computer_hand:
        return 'draw'
    elif (player_hand == 'グー' and computer_hand == 'チョキ') or \
         (player_hand == 'チョキ' and computer_hand == 'パー') or \
         (player_hand == 'パー' and computer_hand == 'グー'):
        return 'win'
    else:
        return 'lose'

# じゃんけんゲームスタート
print('じゃんけんゲームスタート!')

rounds = 0
while True:
    rounds += 1
    print('====================')
    print(f'第{rounds}回戦')
    print('カメラにじゃんけんの手を写し、Enterキーを押してください')

    while True:
        ret, image = camera.read()
        if not ret:
            continue

        cv2.imshow("Webcam Image", image)
        key = cv2.waitKey(1) & 0xFF
        if key == 13:  # Enterが押されたら
            break

    # 画像からプレイヤーの手を認識
    image = cv2.resize(image, (224, 224), interpolation=cv2.INTER_AREA)
    image = np.asarray(image, dtype=np.float32).reshape(1, 224, 224, 3)
    image = (image / 127.5) - 1
    prediction = model.predict(image)
    player_hand = hands[np.argmax(prediction)]

    # コンピュータの手を決定
    computer_hand = decide_computer_hand(previous_rounds)
    previous_rounds.append((player_hand, computer_hand))

    # 勝敗の判定と表示
    result = judge(player_hand, computer_hand)
    results[result] += 1
    print(f'あなた: {player_hand}, コンピュータ: {computer_hand}')
    print(f'結果: {result}')
    print(f"現在の成績: 勝ち: {results['win']}, 負け: {results['lose']}, 引き分け: {results['draw']}")

camera.release()
cv2.destroyAllWindows()

*参考になれば嬉しいです。

実行している様子は、こちらの動画で紹介しています。

②GUIアプリにしてみた

tkinterを使い、デスクトップアプリとして利用できるようにしてみました。

実行すると、このようにウィンドウが表示されてゲームができます。

pythonじゃんけんゲームのデスクトップアプリ実行結果

実際にじゃんけんしている様子。

じゃんけんデスクトップアプリの動作確認

画像を認識した結果をイラストで表示し、コンピュータの手もイラストにして分かりやすくなっています。

ゲームを続けるボタンと終了するボタンを設置しているので、アプリとして使えるようになっていると思います。

こちらも詳しい解説はしていませんが、コピペしてできるようにコード全文を置いておきます。

import tkinter as tk
from PIL import Image, ImageTk
import cv2
import numpy as np
from keras.models import load_model

# モデルとラベルの読み込み
model = load_model("keras_Model.h5", compile=False)
class_names = open("labels.txt", "r").readlines()

# カメラの設定
camera = cv2.VideoCapture(1)

# じゃんけんの手の選択肢
hands = ['グー', 'チョキ', 'パー']
hands_images = {'グー': 'images/rock.png', 'チョキ': 'images/scissors.png', 'パー': 'images/paper.png'}
previous_rounds = []  # 前回のラウンドの結果を記録するリスト

# コンピュータの手を決定する関数
def decide_computer_hand(previous_rounds):
    # コンピュータの次の手を決定する関数
    if len(previous_rounds) == 0:
        # 最初の手としてパーを出す(要件①に基づく)
        return 'パー'
    elif len(previous_rounds) == 1:
        # 2回目はチョキを出す(要件②に基づく)
        return 'チョキ'
    else:
        # 最後のプレイヤーとコンピュータの手を取得
        last_player_hand = previous_rounds[-1][0]
        last_computer_hand = previous_rounds[-1]
        if last_player_hand == last_computer_hand:
            # 前回があいこだった場合のロジック(要件③、④に基づく)
            if last_player_hand == 'グー':
                return 'チョキ'  # グーあいこの次はチョキを出す
            elif last_player_hand == 'チョキ':
                return 'チョキ'  # チョキあいこはチョキを続ける(要件④に基づく)
            else:
                return 'グー'  # パーあいこの次はグーを出す
        else:
            # 前回が勝ち負けだった場合のロジック(要件⑦に基づく)
            if last_computer_hand == 'チョキ':
                return 'チョキ'  # 直前の勝ち手がチョキの場合、再びチョキを出す
            else:
                return 'パー'  # それ以外はパーを出す(要件⑩に基づく)

# 結果表示関数
def show_result(player_hand, computer_hand):
    player_image = Image.open(hands_images[player_hand])
    computer_image = Image.open(hands_images[computer_hand])

    # 画像をリサイズ
    player_image = player_image.resize((100, 100), Image.Resampling.LANCZOS)
    computer_image = computer_image.resize((100, 100), Image.Resampling.LANCZOS)

    player_photo = ImageTk.PhotoImage(player_image)
    computer_photo = ImageTk.PhotoImage(computer_image)

    # プレイヤーとコンピュータの手を明確に表示
    player_hand_label.config(image=player_photo, borderwidth=2, relief="solid")
    player_hand_label.image = player_photo
    computer_hand_label.config(image=computer_photo, borderwidth=2, relief="solid")
    computer_hand_label.image = computer_photo

# カメラの映像を更新する関数
def update_camera_image():
    ret, frame = camera.read()
    if ret:
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        frame = Image.fromarray(frame)
        frame = frame.resize((320, 240), Image.Resampling.LANCZOS)  # 修正箇所
        frame_photo = ImageTk.PhotoImage(frame)
        canvas.create_image(0, 0, image=frame_photo, anchor=tk.NW)
        canvas.image = frame_photo
    window.after(100, update_camera_image)


# じゃんけん実行関数
def play_janken():
    try:
        # カメラから画像を取得してプレイヤーの手を認識
        ret, image = camera.read()
        if not ret:
            raise ValueError("カメラから画像を取得できませんでした。")

        image = cv2.resize(image, (224, 224), interpolation=cv2.INTER_AREA)
        image = np.asarray(image, dtype=np.float32).reshape(1, 224, 224, 3)
        image = (image / 127.5) - 1
        prediction = model.predict(image)

        # 手が正しく検出されたかどうかを確認
        if prediction is None or len(prediction[0]) == 0:
            raise ValueError("手が認識されませんでした。")

        # 最も可能性の高い手を選択
        max_index = np.argmax(prediction)
        if max_index not in range(len(hands)):
            raise ValueError("予測された手が無効です。")

        player_hand = hands[max_index]
        computer_hand = decide_computer_hand(previous_rounds)
        show_result(player_hand, computer_hand)
        previous_rounds.append((player_hand, computer_hand))

        print("\n回数 | プレイヤー | コンピュータ")
        print("-" * 30)
        for i, round in enumerate(previous_rounds, start=1):
            print(f"{i:>3} | {round[0]:>7} | {round:>10}")

        # 勝敗の判定と表示
        if player_hand == computer_hand:
            result = "あいこ"
        elif (player_hand == "グー" and computer_hand == "チョキ") or \
                (player_hand == "チョキ" and computer_hand == "パー") or \
                (player_hand == "パー" and computer_hand == "グー"):
            result = "プレイヤーの勝ち!"
        else:
            result = "コンピュータの勝ち!"
        result_label.config(text=f"結果: {result}")

    except ValueError as e:
        error_label.config(text=str(e))

    # 再度じゃんけんをする指示を表示
    instruction_label.config(text="もう一度じゃんけんをするか、終了するか選んでください。")


# Tkinter GUI設定
window = tk.Tk()
window.title("じゃんけんゲーム")

canvas = tk.Canvas(window, width=640, height=480)
canvas.pack()

# プレイヤーとコンピュータの手のラベルとタイトル
player_title_label = tk.Label(window, text="プレイヤー", font=('Helvetica', 12))
player_title_label.pack(side=tk.LEFT, padx=10)
player_hand_label = tk.Label(window)
player_hand_label.pack(side=tk.LEFT, padx=10)

computer_title_label = tk.Label(window, text="コンピュータ", font=('Helvetica', 12))
computer_title_label.pack(side=tk.LEFT, padx=10)
computer_hand_label = tk.Label(window)
computer_hand_label.pack(side=tk.LEFT, padx=10)

instruction_label = tk.Label(window, text="手を出して、ボタンをクリックしてください", font=('Helvetica', 12))
instruction_label.pack()

play_button = tk.Button(window, text="じゃんけんをする", command=play_janken)
play_button.pack(pady=10)

result_label = tk.Label(window, font=('Helvetica', 16))
result_label.pack()

exit_button = tk.Button(window, text="ゲームを終了する", command=window.destroy)
exit_button.pack(pady=10)

# エラーメッセージ表示用のラベル
error_label = tk.Label(window, font=('Helvetica', 16), fg="red")
error_label.pack()

update_camera_image()

window.mainloop()
camera.release()

各種ライブラリのバージョンによって動作しないことがあるので、注意して下さい。

特にPillowについては10.2を使って動作確認しています。

バージョンが古いと使えないメソッドがあるので、エラーが出る場合は上記のバージョンに合わせると良いです。

まとめ

今回は、Pythonとティーチャブルマシンを使用して、カメラでじゃんけんの手を認識し、AIと対戦する方法について解説しました。

最初は難しそうに感じたかもしれませんが、一歩一歩進めていくことで、AIの基本的な仕組みやプログラミングの楽しさを実感してもらえたら嬉しいです。

これをきっかけに、さらに多くのプロジェクトに挑戦してみましょう。

今後も初心者向けにプログラミングの情報をお届けします。

Pythonの演習はこちら ▶

以下のメルマガにご登録頂くと、不定期に配信をお届けします。

ぜひ購読してください!

それでは、ステキなPythonライフを!

ABOUT ME
papa3
某教育機関で現役の講師をしながら、ブログとYouTubeでプログラミングの情報を発信しています。個人でもウェブアプリケーション開発と運営をしながら本業収入<副業を目指しています!

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です