用Python构建AI驱动的石头剪刀布游戏:从数据收集到模型部署全流程

石头剪刀布这个看似简单的游戏,实际上蕴含着丰富的决策模式和人类行为规律。作为一名长期研究游戏AI的开发者,我发现用机器学习预测玩家出拳模式远比随机选择有趣得多。本文将带你从零开始,构建一个能够学习并预测人类玩家行为的AI对手,而不仅仅是随机出拳的电脑程序。

这个项目非常适合想要入门机器学习应用的Python开发者。你不需要深厚的数学背景,我们将使用TensorFlow的Keras API来简化模型构建过程。整个流程包括数据收集、模型训练、评估优化以及最终集成到游戏逻辑中,完整呈现一个机器学习项目的生命周期。

1. 环境准备与基础游戏搭建

在开始机器学习部分之前,我们需要先建立一个基础的游戏框架。这个框架将作为我们收集训练数据和最终集成AI模型的基础。

import random
from collections import defaultdict

# 游戏选项和胜负规则
GESTURES = ['石头', '剪刀', '布']
WIN_RULES = {
    '石头': '剪刀',
    '剪刀': '布',
    '布': '石头'
}

class RockPaperScissors:
    def __init__(self):
        self.history = []
        self.score = {'player': 0, 'ai': 0}
        
    def play_round(self, player_choice):
        # 验证输入
        if player_choice not in GESTURES:
            raise ValueError("无效的选择")
            
        # AI随机选择(后续将替换为模型预测)
        ai_choice = random.choice(GESTURES)
        
        # 记录当前回合
        self.history.append({
            'player': player_choice,
            'ai': ai_choice,
            'timestamp': time.time()
        })
        
        # 判断胜负
        if WIN_RULES[player_choice] == ai_choice:
            result = 'win'
            self.score['player'] += 1
        elif WIN_RULES[ai_choice] == player_choice:
            result = 'lose'
            self.score['ai'] += 1
        else:
            result = 'draw'
            
        return ai_choice, result

这个基础实现包含了游戏的核心逻辑,但关键点在于我们添加了历史记录功能,这将为后续的机器学习模型提供训练数据。每次玩家和AI的出拳选择都会被记录下来,包括时间戳(这对分析玩家行为模式很有帮助)。

2. 数据收集与特征工程

要训练一个能够预测玩家行为的模型,我们需要收集足够多的游戏数据。在实际应用中,你可以:

  1. 让多个测试者玩这个游戏并记录他们的选择
  2. 收集在线石头剪刀布游戏的数据(如果有API接口)
  3. 使用已有的公开数据集
import pandas as pd
import numpy as np

def prepare_dataset(history, window_size=3):
    """将游戏历史转换为机器学习友好的格式"""
    data = []
    targets = []
    
    for i in range(window_size, len(history)):
        # 获取滑动窗口内的历史记录
        window = history[i-window_size:i]
        
        # 提取特征
        features = {
            'player_prev_1': window[-1]['player'],
            'player_prev_2': window[-2]['player'],
            'player_prev_3': window[-3]['player'],
            'ai_prev_1': window[-1]['ai'],
            'ai_prev_2': window[-2]['ai'],
            'ai_prev_3': window[-3]['ai'],
            'time_since_last': window[-1]['timestamp'] - window[-2]['timestamp']
        }
        
        # 当前回合的玩家选择作为目标
        target = history[i]['player']
        
        data.append(features)
        targets.append(target)
    
    return pd.DataFrame(data), pd.Series(targets)

这个特征工程函数采用了滑动窗口方法,使用过去3个回合的数据来预测玩家下一回合的选择。我们考虑了:

  • 玩家之前的出拳序列
  • AI之前的出拳序列
  • 玩家做出决策的时间间隔(快速决策可能暗示某种模式)

重要提示:在实际项目中,你应该收集至少1000轮以上的游戏数据才能训练出有效的模型。小样本数据可能导致模型过拟合或表现不佳。

3. 构建预测模型

现在我们来构建一个能够学习玩家行为模式的神经网络模型。我们将使用TensorFlow的Keras API来简化这个过程。

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.utils import to_categorical
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split

def build_model(input_shape, num_classes):
    """构建预测玩家选择的神经网络模型"""
    model = Sequential([
        Dense(64, activation='relu', input_shape=input_shape),
        Dropout(0.3),
        Dense(32, activation='relu'),
        Dropout(0.2),
        Dense(num_classes, activation='softmax')
    ])
    
    model.compile(
        optimizer='adam',
        loss='categorical_crossentropy',
        metrics=['accuracy']
    )
    
    return model

# 数据预处理
def preprocess_data(df, target_series):
    # 将类别特征转换为数值
    encoder = LabelEncoder()
    encoded_features = df.apply(encoder.fit_transform)
    
    # 对目标变量进行one-hot编码
    encoded_target = encoder.transform(target_series)
    one_hot_target = to_categorical(encoded_target)
    
    # 划分训练集和测试集
    X_train, X_test, y_train, y_test = train_test_split(
        encoded_features.values, 
        one_hot_target,
        test_size=0.2,
        random_state=42
    )
    
    return X_train, X_test, y_train, y_test, encoder

这个模型结构相对简单,但对于石头剪刀布这样的离散选择问题已经足够。关键点在于:

  • 使用ReLU激活函数的全连接层
  • 添加Dropout层防止过拟合
  • 输出层使用softmax激活函数,输出每个选择的概率
  • 对类别特征进行标签编码,对目标变量进行one-hot编码

4. 模型训练与评估

有了数据和模型架构,现在我们可以开始训练过程了。

def train_and_evaluate(model, X_train, X_test, y_train, y_test):
    """训练模型并评估性能"""
    history = model.fit(
        X_train, y_train,
        epochs=50,
        batch_size=32,
        validation_split=0.1,
        verbose=1
    )
    
    # 评估测试集性能
    test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
    print(f"测试集准确率: {test_acc:.2f}")
    
    return model, history

# 使用示例
# 假设df和targets是从prepare_dataset获得的
X_train, X_test, y_train, y_test, encoder = preprocess_data(df, targets)
model = build_model((X_train.shape[1],), len(encoder.classes_))
trained_model, training_history = train_and_evaluate(model, X_train, X_test, y_train, y_test)

训练过程中,你应该关注以下指标:

  1. 训练集和验证集的准确率
  2. 训练集和验证集的损失值
  3. 模型在测试集上的表现

模型性能分析表

指标 期望值 说明
训练准确率 >65% 低于随机猜测(33%)说明模型有问题
验证准确率 接近训练准确率 差距过大可能过拟合
测试准确率 >60% 实际应用中可接受的水平

如果模型表现不佳,可以尝试:

  • 增加训练数据量
  • 调整模型架构(更多层或神经元)
  • 调整超参数(学习率、批次大小等)
  • 改进特征工程(添加更多有意义的特征)

5. 集成AI到游戏逻辑

现在我们已经有了一个训练好的模型,可以将其集成到原始游戏中,替换掉随机选择的AI。

class AIPlayer:
    def __init__(self, model, encoder, window_size=3):
        self.model = model
        self.encoder = encoder
        self.window_size = window_size
        self.memory = []
        
    def record_round(self, player_choice, ai_choice):
        """记录当前回合结果"""
        self.memory.append({
            'player': player_choice,
            'ai': ai_choice,
            'timestamp': time.time()
        })
        
        # 保持内存不超过窗口大小
        if len(self.memory) > self.window_size:
            self.memory.pop(0)
    
    def predict(self):
        """预测玩家下一回合的选择"""
        if len(self.memory) < self.window_size:
            return random.choice(GESTURES)
            
        # 准备输入数据
        features, _ = prepare_dataset(self.memory, self.window_size)
        encoded_features = features.apply(self.encoder.transform)
        
        # 预测概率
        proba = self.model.predict(encoded_features.values[-1:])[0]
        
        # 选择最佳应对策略
        predicted_index = np.argmax(proba)
        predicted_gesture = self.encoder.inverse_transform([predicted_index])[0]
        
        # 根据预测选择能够击败玩家的手势
        return WIN_RULES[predicted_gesture]

这个AIPlayer类会:

  1. 记住最近几轮的游戏历史
  2. 使用训练好的模型预测玩家下一回合最可能的选择
  3. 根据预测选择能够击败玩家的手势

将AI集成到主游戏类中:

class RockPaperScissorsAI(RockPaperScissors):
    def __init__(self, model, encoder):
        super().__init__()
        self.ai_player = AIPlayer(model, encoder)
        
    def play_round(self, player_choice):
        # 验证输入
        if player_choice not in GESTURES:
            raise ValueError("无效的选择")
            
        # AI预测并选择
        ai_choice = self.ai_player.predict()
        
        # 记录当前回合
        self.history.append({
            'player': player_choice,
            'ai': ai_choice,
            'timestamp': time.time()
        })
        
        # 更新AI记忆
        self.ai_player.record_round(player_choice, ai_choice)
        
        # 判断胜负
        if WIN_RULES[player_choice] == ai_choice:
            result = 'win'
            self.score['player'] += 1
        elif WIN_RULES[ai_choice] == player_choice:
            result = 'lose'
            self.score['ai'] += 1
        else:
            result = 'draw'
            
        return ai_choice, result

6. 高级优化策略

基础版本的AI已经可以工作,但我们可以进一步优化它的表现:

6.1 混合策略AI

单纯的预测模型可能会被玩家发现模式并利用。我们可以结合多种策略:

class HybridAIPlayer(AIPlayer):
    def __init__(self, model, encoder, strategies=None):
        super().__init__(model, encoder)
        self.strategies = strategies or [
            ('predictive', 0.7),  # 70%时间使用预测模型
            ('random', 0.2),      # 20%时间随机选择
            ('counter', 0.1)      # 10%时间针对玩家最常见选择
        ]
        self.player_pattern = defaultdict(int)
        
    def predict(self):
        # 更新玩家模式统计
        if self.memory:
            last_player_choice = self.memory[-1]['player']
            self.player_pattern[last_player_choice] += 1
            
        # 选择策略
        strategy = np.random.choice(
            [s[0] for s in self.strategies],
            p=[s[1] for s in self.strategies]
        )
        
        if strategy == 'predictive' and len(self.memory) >= self.window_size:
            return super().predict()
        elif strategy == 'random':
            return random.choice(GESTURES)
        else:  # counter strategy
            if not self.player_pattern:
                return random.choice(GESTURES)
            most_common = max(self.player_pattern.items(), key=lambda x: x[1])[0]
            return WIN_RULES[most_common]

6.2 实时学习

我们可以让模型在游戏过程中继续学习玩家的行为模式:

class OnlineLearningAIPlayer(AIPlayer):
    def __init__(self, model, encoder, window_size=3, learning_rate=0.1):
        super().__init__(model, encoder, window_size)
        self.learning_rate = learning_rate
        self.online_model = clone_model(model)
        self.online_model.set_weights(model.get_weights())
        
    def update_model(self, player_choice):
        if len(self.memory) < self.window_size:
            return
            
        # 准备数据
        features, _ = prepare_dataset(self.memory, self.window_size)
        encoded_features = features.apply(self.encoder.transform)
        target = self.encoder.transform([player_choice])
        one_hot_target = to_categorical(target, num_classes=len(self.encoder.classes_))
        
        # 小批次在线学习
        self.online_model.train_on_batch(
            encoded_features.values[-1:],
            one_hot_target
        )
        
    def predict(self):
        if len(self.memory) < self.window_size:
            return random.choice(GESTURES)
            
        # 使用在线学习后的模型预测
        features, _ = prepare_dataset(self.memory, self.window_size)
        encoded_features = features.apply(self.encoder.transform)
        
        proba = self.online_model.predict(encoded_features.values[-1:])[0]
        predicted_index = np.argmax(proba)
        predicted_gesture = self.encoder.inverse_transform([predicted_index])[0]
        
        return WIN_RULES[predicted_gesture]

6.3 心理模型集成

人类玩家往往会表现出一些心理模式,我们可以将这些模式编码到AI中:

class PsychologicalAIPlayer(AIPlayer):
    def __init__(self, model, encoder, window_size=3):
        super().__init__(model, encoder, window_size)
        self.psychological_factors = {
            'win_stay': 0.6,    # 赢了后保持同样选择的概率
            'lose_shift': 0.75,  # 输了后改变选择的概率
            'draw_shift': 0.5    # 平局后改变选择的概率
        }
        
    def predict(self):
        if len(self.memory) < self.window_size:
            return random.choice(GESTURES)
            
        # 考虑最近一轮的结果
        last_round = self.memory[-1]
        last_player_choice = last_round['player']
        last_result = (
            'win' if WIN_RULES[last_player_choice] == last_round['ai'] else
            'lose' if WIN_RULES[last_round['ai']] == last_player_choice else
            'draw'
        )
        
        # 根据心理模型调整预测
        base_prediction = super().predict()
        
        if last_result == 'win' and random.random() < self.psychological_factors['win_stay']:
            return WIN_RULES[last_player_choice]
        elif last_result == 'lose' and random.random() < self.psychological_factors['lose_shift']:
            # 玩家可能会改变选择,预测一个不同的选择
            options = [g for g in GESTURES if g != last_player_choice]
            return WIN_RULES[random.choice(options)]
        elif last_result == 'draw' and random.random() < self.psychological_factors['draw_shift']:
            # 玩家可能会改变选择
            options = [g for g in GESTURES if g != last_player_choice]
            return WIN_RULES[random.choice(options)]
            
        return base_prediction

7. 部署与用户交互

最后,我们需要创建一个用户友好的界面来与AI对战:

def main():
    # 加载预训练模型和编码器
    model = load_model('rps_predictor.h5')
    encoder = joblib.load('encoder.pkl')
    
    # 初始化游戏
    game = RockPaperScissorsAI(model, encoder)
    
    print("欢迎来到AI石头剪刀布游戏!")
    print("输入'石头'、'剪刀'或'布'开始游戏,输入'退出'结束游戏")
    
    while True:
        player_input = input("你的选择是? ").strip()
        
        if player_input == '退出':
            print("游戏结束!")
            print(f"最终比分 - 玩家: {game.score['player']} AI: {game.score['ai']}")
            break
            
        if player_input not in GESTURES:
            print("无效输入,请重新尝试")
            continue
            
        ai_choice, result = game.play_round(player_input)
        
        print(f"AI选择了: {ai_choice}")
        if result == 'win':
            print("你赢了这一轮!")
        elif result == 'lose':
            print("AI赢了这一轮!")
        else:
            print("平局!")
            
        print(f"当前比分 - 玩家: {game.score['player']} AI: {game.score['ai']}")
        print()

if __name__ == "__main__":
    main()

这个交互式程序允许玩家:

  1. 选择石头、剪刀或布
  2. 查看AI的选择和每轮结果
  3. 跟踪累计比分
  4. 随时退出游戏

8. 进一步改进方向

虽然我们的AI已经相当强大,但仍有改进空间:

  1. 多模态输入:除了选择历史,还可以考虑玩家输入时间、游戏节奏等因素
  2. 强化学习:使用RL算法让AI自我对弈学习最优策略
  3. 个性化模型:为每个玩家建立单独的模型档案,适应其独特玩法
  4. 在线学习:在游戏过程中持续更新模型权重
  5. 情感分析:结合自然语言处理分析玩家聊天内容(如果有)来调整策略
# 示例:强化学习环境设置
class RPSEnvironment:
    def __init__(self):
        self.action_space = ['石头', '剪刀', '布']
        self.observation_space = ...  # 定义状态空间
        
    def reset(self):
        """重置环境状态"""
        self.state = ...  # 初始状态
        return self.state
        
    def step(self, action):
        """执行一个动作并返回新状态、奖励和是否结束"""
        # 模拟玩家反应(可以用预训练模型)
        player_action = ...  
        
        # 计算奖励
        reward = self._calculate_reward(action, player_action)
        
        # 更新状态
        self.state = ...  
        
        done = False  # 是否结束一局游戏
        
        return self.state, reward, done
        
    def _calculate_reward(self, ai_action, player_action):
        """根据游戏结果计算奖励"""
        if WIN_RULES[ai_action] == player_action:
            return 1   # 赢
        elif WIN_RULES[player_action] == ai_action:
            return -1  # 输
        return 0       # 平

在实际项目中,我发现玩家行为模式会随着时间变化。一个实用的技巧是定期重新训练模型,或者在检测到玩家胜率异常时触发模型更新。另一个有趣的发现是,加入少量随机性实际上能让AI显得更"人性化",完全理性的AI反而容易被玩家识破模式。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐