如何使用 PyTorch 和 EfficientNet 创建一个 AI 游戏外挂🔥

2025-04-10T14:20:06+08:00 | 10分钟阅读 | 更新于 2025-04-10T14:20:06+08:00

Macro Zhao

推荐超级课程:

@TOC

机器人玩电脑游戏

简介

你是否刚刚接触人工智能领域,并想找一个有趣的入门项目?那就来吧!这篇博客将指导你使用 PyTorch 和 EfficientNet 创建一个可以玩 Chrome 小恐龙游戏的 AI 模型。 开发 ChatGPT 的 OpenAI 组织实际上是从构建可以玩 Atari 游戏的 AI 模型开始的。这个项目被称为 Atari AI,是深度强化学习的首次演示之一,为人工智能领域的许多后续进展铺平了道路。因此,构建一个可以玩 Chrome 小恐龙游戏的 AI 模型实际上是使用游戏测试和开发 AI 算法的悠久传统的一部分。 Chrome 小恐龙游戏是一款简单却令人上瘾的游戏,已经吸引了全球数百万玩家的喜爱。游戏的目标是控制一只恐龙,帮助它尽可能远地奔跑,而不会被障碍物撞到。借助 AI,我们可以创建一个可以学习如何玩游戏的模型,并打破我们的高分记录。 本教程适用于任何对构建可以玩游戏的 AI 模型感兴趣的人。即使你对 AI 或深度学习的概念还很陌生,本教程也是一个很好的起点。 我们将使用 PyTorch,一个流行的深度学习框架,以及 EfficientNet,一种最先进的神经网络架构,来训练一个模型,分析游戏屏幕并根据它看到的内容做出决策。我们将从获取必要的数据开始,然后对其进行处理,最后训练模型。到本教程结束时,您将对深度学习以及如何训练您自己的 AI 模型有更好的理解。

通用工作流程

机器学习工作流程

设置 AI 模型有 6 个主要步骤:

  1. 获取数据
  2. 处理数据
  3. 创建模型
  4. 训练模型
  5. 测试模型
  6. 从模型中进行推理

步骤 0:设置项目

  • 安装 Anaconda:从 此处 下载并安装适用于您操作系统的 Anaconda 发行版。

  • 创建一个新的项目文件夹。我们将其命名为“dino”。在这个文件夹中打开 VS Code 并打开终端。

  • 创建一个新的 conda 环境:在 Anaconda Prompt 或您的终端中,通过运行以下命令创建一个新的 conda 环境:

    conda create --name myenv python=3.10
    

这将创建一个名为 myenv 的新环境,其中安装了 Python 3.10。

  • 激活环境:创建环境后,使用以下命令激活它:

    conda activate myenv
    
  • 安装 PyTorch:使用以下命令安装具有 CUDA 支持(用于 GPU 加速)的 PyTorch 库:

    conda install pytorch torchvision torchaudio cudatoolkit=11.1 -c pytorch -c conda-forge
    

    此命令安装了 PyTorch、TorchVision 和 TorchAudio 以及 CUDA 工具包版本 11.1。您可以根据需要更改 CUDA 工具包的版本。

  • 测试安装:要验证 PyTorch 安装,请在您的 conda 环境中运行以下命令以启动 Python 解释器:

    python
    

    然后,导入 PyTorch 库并打印其版本如下:

    import torch
    print(torch.__version__)
    

    这应该打印出您环境中安装的 PyTorch 版本号。

步骤 1:获取数据

我们将通过在人类玩家玩游戏时拍摄游戏屏幕的快照来获取数据。captures.py 处理此事。

import cv2
from PIL import ImageGrab
import numpy as np
import keyboard
import os
from datetime import datetime
current_key = ""
buffer = []
# 检查是否存在名为 'captures' 的文件夹。如果不存在,则创建它。
if not os.path.exists("captures"):
    os.mkdir("captures")
def keyboardCallBack(key: keyboard.KeyboardEvent):
    '''
    当键盘事件发生时,此函数被调用。它将按下的键存储在缓冲区中并进行排序。
    ### 参数 : 
    `key (KeyboardEvent)`
    ### 返回 : 
    `None`
    ### 示例 : 
    `keyboardCallBack(key)`
    '''
    global current_key
    if key.event_type == "down" and key.name not in buffer:
        buffer.append(key.name)
    if key.event_type == "up":
        buffer.remove(key.name)
    buffer.sort()  # 将按下的键按升序排列
    current_key = " ".join(buffer)
keyboard.hook(callback=keyboardCallBack)
i = 0
while (not keyboard.is_pressed("esc")):
    # 捕获图像并将其保存到 'captures' 文件夹中,文件名包含时间和日期以及按下的键
    image = cv2.cvtColor(np.array(ImageGrab.grab(
        bbox=(620, 220, 1280, 360))), cv2.COLOR_RGB2BGR)
    # 如果按下了键,将按下的键嵌入文件名
    if len(buffer) != 0:
        cv2.imwrite("captures/" + str(datetime.now()).replace("-", "_").replace(":",
                    "_").replace(" ", "_")+" " + current_key + ".png", image)
    # 如果没有按下键,将 'n' 嵌入文件名
    else:
        cv2.imwrite("captures/" + str(datetime.now()).replace("-",
                    "_").replace(":", "_").replace(" ", "_") + " n" + ".png", image)
    i = i+1

此代码捕获屏幕截图并将它们作为 PNG 文件保存到指定的目录中。屏幕截图使用 Python 库 PIL(Python Imaging Library)和 OpenCV 捕获,并使用 ImageGrab 模块捕获。屏幕截图的坐标使用 ImageGrab.grab 中的 bbox(边界框)参数指定。您可能需要根据您的显示缩放调整 bbox 值。 该程序还使用键盘库捕获键盘事件。当按下键时,键的名称被附加到缓冲区中。当键被释放时,键的名称从缓冲区中删除。缓冲区的当前状态被保存为变量 current_key 中的字符串。 该程序将每个屏幕截图保存为包含 i(整数,在 while 循环的每次迭代中递增)和缓冲区当前状态(保存在 current_key 中)的文件名。如果在捕获屏幕截图时没有按下任何键,则文件名包含“n”而不是键名。 此代码可以用作构建图像数据集的起点,用于机器学习项目,例如目标识别或图像分类。通过捕获图像并将其与相应的按键进行标记,可以创建一个数据集,可用于训练机器学习模型以识别图像并预测相应的按键。 前往 https://chromedino.com/ 并开始玩游戏,同时脚本将捕获图像并将其保存到“captures”文件夹中。 运行此 Python 文件并开始玩游戏。至少玩 20 次不同的运行以获得良好的数据集。 捕获的图像应该如下所示。

在捕获所有图像后,“captures”文件夹应该如下所示。您可以再次运行脚本并添加更多训练数据。

步骤 2:处理数据

接下来,我们需要一个脚本来处理我们捕获的图像,并将它们转换为我们的模型可以理解的训练集。创建一个新的 process.py 文件。

import pandas as pd
import matplotlib.pyplot as plt
import os
import csv
labels = []
dir = 'captures'  # 从该目录获取捕获的图像
# 获取目录中每个图像的标签
for f in os.listdir(dir):
    key = f.rsplit('.', 1)[0].rsplit(" ", 1)[1]
    if key == "n":
        labels.append({'file_name': f, 'class': 0})
    elif key == "space":
        labels.append({'file_name': f, 'class': 1})
field_names = ['file_name', 'class']
# 将标签写入 csv 文件
with open('labels_dino.csv', 'w') as csvfile:
    writer = csv.DictWriter(csvfile, fieldnames=field_names)
    writer.writeheader()
    writer.writerows(labels)

在此代码片段中,我们正在为目录中捕获的图像生成标签,然后将它们写入 CSV 文件。 首先,我们定义一个目录 dir,其中包含捕获的图像。然后,我们使用 os.listdir() 方法迭代目录中的每个文件。 对于每个文件,我们使用字符串操作从文件名中提取类标签。如果文件名以“n”结尾,我们分配标签 0,否则如果它以“space”结尾,我们分配标签 1。 然后,我们将标签存储在字典列表中,每个字典包含单个图像的文件名和类标签。 最后,我们使用 csv 模块将标签写入名为 labels_dino.csv 的 CSV 文件。我们定义 CSV 文件的字段名称,并使用 DictWriter 方法将标签写入文件。我们首先写入包含字段名称的标题行,然后使用 writerows 方法将目录中每个图像的标签写入文件。 这是 labels_dino csv 文件应该的样子。

步骤 3:创建模型

Ahhh…. 人工智能中的乐趣部分…制作模型。但等等,在创建模型之前,我们需要采取一些步骤。

步骤 3.1. 创建自定义 DinoDataset

为了创建我们的模型,我们首先需要创建一个自定义 Pytorch 数据集。我们将称此为 DinoDataset. 通过创建一个新的笔记本 train.ipynb 来开始。 让我们导入所有依赖项:

from torch.utils.data import Dataset, DataLoader
import cv2
from PIL import Image
import pandas as pd
import torch
import os
from torchvision.transforms import CenterCrop, Resize, Compose, ToTensor, Normalize
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.model_selection import train_test_split
import torchvision.models
import torch.optim as optim
from tqdm import tqdm
import gc
import numpy as np

现在让我们创建一个图像转换管道,这对于 EfficientNet v2 是必需的

transformer = Compose([
    Resize((480,480)),
    CenterCrop(480),
    Normalize(mean =[0.485, 0.456, 0.406], std =[0.229, 0.224, 0.225] )
])

所需的值在 PyTorch 文档的 EfficientNet v2. 中给出

现在让我们创建我们的 DinoDataset

class DinoDataset(Dataset):
    def __init__(self, dataframe, root_dir, transform = None):
        """
        Args:
            csv_file (string): 包含注释的 csv 文件路径。
            root_dir (string): 包含所有图像的目录。
            transform (callable, optional): 可选的转换,可以应用于样本。
        """
        self.key_frame = dataframe
        self.root_dir = root_dir
        self.transform = transform
    def __len__(self):
        return len(self.key_frame)
    
    def __getitem__(self, idx):
        if torch.is_tensor(idx):
            idx = idx.to_list()
    
        img_name = os.path.join(self.root_dir, self.key_frame.iloc[idx, 0])
        image = Image.open(img_name)
        image = ToTensor()(image)
        label = torch.tensor(self.key_frame.iloc[idx, 1])
        if self.transform: 
            image = self.transform(image)
        return image, label

这是自定义数据集类 DinoDataset 的定义,它继承自 PyTorch Dataset 类。它接受三个参数:

  • dataframe:一个 pandas 数据框,包含数据集中每个图像的文件名和标签。
  • root_dir:存储图像的根目录。
  • transform:一个可选的转换,可以应用于样本。 __len__ 方法返回数据集的长度,即图像的数量。 __getitem__ 方法负责加载图像及其对应的标签。它接受一个索引 idx 作为输入,并返回图像及其标签。图像使用 PIL.Image.open 加载,使用 ToTensor 转换为 PyTorch 张量,标签从数据框中使用 iloc 读取。如果指定了转换,则将其应用于图像。

步骤 3.2. 创建训练和测试 DataLoader

key_frame = pd.read_csv("labels.csv") # 导入包含关键帧标签的 csv 文件
train, test = train_test_split(key_frame, test_size = 0.2)  # 将数据分为训练集和测试集
train = pd.DataFrame(train) 
test = pd.DataFrame(test)
batch_size = 4
trainset = DinoDataset(root_dir = "captures", dataframe = train, transform = transformer)
trainloader = torch.utils.data.DataLoader(trainset, batch_size = batch_size) 
testset = DinoDataset(root_dir = "captures", dataframe = test, transform = transformer)
testloader = torch.utils.data.DataLoader(testset, batch_size = batch_size)

在此代码中,scikit-learn 的 train_test_split 函数用于将数据集分为训练集和测试集,测试大小为 0.2(20%)。生成的分割存储在 traintest 变量中,作为 pandas 数据框。 接下来,定义一个批处理大小为 4,并使用 DinoDataset 类创建 PyTorch DataLoader 对象用于训练和测试集。root_dir 参数设置为“captures”,这是包含捕获图像的目录,transform 参数设置为 transformer,这是之前定义的数据预处理管道。生成的 DataLoader 对象是 trainloadertestloader,它们可以在训练和测试过程中用于将数据馈送到神经网络。 如果您有高端 GPU,可以使用更高的 batch_size 值。现在让我们使用较小的批处理大小。 让我们检查 DataLoader 中一个批次中的图像。

dataiter = iter(trainloader)
images, labels = next(dataiter)
for i in range(len(images)):
        ax = plt.subplot(2, 4, i + 1)
        image = (images[i].permute(1,2,0)*255.0).cpu()
        ax.set_title(labels[i].item(), fontsize=20)  # 设置子图的标题
        ax.set_xticklabels([])   # 移除 x 轴标签
        ax.set_yticklabels([])   # 移除 y 轴标签
        plt.imshow(image)        # 绘制图像

每个图像顶部的数字显示在捕获该图像时按下的键。1 表示“空格”,0 表示没有按键。

步骤 3.3. 创建模型

model = torchvision.models.efficientnet_v2_s(pretrained=True)
model.classifier = torch.nn.Linear(in_features = 1280, out_features = 2)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
model.train()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

这里我们使用了 EfficientNet v2-S 预训练模型,并将其分类器层更改为具有 2 个输出特征的线性层,以匹配我们的数据集(跳或不跳)。

步骤 4:训练模型

epochs = 15  # 训练轮数
loss_container = [] # 存储每个 epoch 后的损失值
for epoch in range(epochs):  # 多次遍历数据集
    running_loss = 0.0
    for data in tqdm(trainloader, position=0, leave=True):
        # 获取输入;数据是一个包含 [输入,标签] 的列表
        inputs, labels = data
        inputs = inputs.to(device)
        labels = labels.to(device)
        # 清零参数梯度
        optimizer.zero_grad()
        # 前向传播 + 反向传播 + 优化
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    loss_container.append(running_loss)
    print(f'[{epoch + 1}] | loss: {running_loss / len(trainloader):.3f}')
    running_loss = 0.0
print('训练完成')
# 绘制损失曲线
plt.plot(np.linspace(1, epochs, epochs).astype(int), loss_container)
# 清理 GPU 内存 
gc.collect()
torch.cuda.empty_cache()

这是模型的训练循环。for 循环遍历固定数量的 epoch(在本例中为 15),在此期间,模型在数据集上训练。 内部 for 循环使用 DataLoader 对象以批处理方式加载数据集。在每次迭代中,输入和标签被加载并发送到设备(如果有 GPU,则为 GPU)。然后清零优化器的梯度,并在输入上执行前向传播。将模型的输出与标签使用交叉熵损失标准进行比较。损失通过模型反向传播,并调用优化器的步长方法来更新模型的权重。 每个 epoch 的损失被累加以获得该 epoch 的总损失。在每个 epoch 的末尾,模型在测试集上进行评估,以检查其在未见数据上的性能。 注意,tqdm 用于在训练循环中显示每个数据批次的进度条。 这是损失曲线的样子。也许我们可以继续运行训练循环更多 epoch。

我们可以使用以下代码保存我们的模型

PATH = 'efficientnet_s.pth'
torch.save(model.state_dict(), PATH)

步骤 5:测试模型性能

让我们加载一个新的 EfficientNet 模型,该模型使用我们在上一步中保存的权重。

saved_model = torchvision.models.efficientnet_v2_s(pretrained=False)
saved_model.classifier = torch.nn.Linear(in_features = 1280, out_features = 2)
saved_model.load_state_dict(torch.load(PATH))
saved_model = saved_model.to(device)
saved_model.eval()
correct = 0
total = 0
with torch.no_grad():
  for data in tqdm(testloader):
    images,labels = data
    images = images.to(device)
    labels = labels.to(device)
    outputs = saved_model(images)
    predicted = torch.softmax(outputs,dim = 1).argmax(dim = 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()
print(f'\\n 在测试图像上的网络准确率: {100 * correct // total} %')

这段代码正在评估训练好的模型在测试集上的性能。 correcttotal 变量被初始化为 0,然后开始一个循环遍历测试集。testloader 用于每次加载一批图像和标签。 在循环内部,图像和标签被移动到训练过程中指定的设备(在这种情况下为 "cuda")。然后使用之前训练的模型(训练 earlier)对输入图像进行预测。 torch.softmax() 函数应用于模型输出以将其转换为概率,然后 argmax() 函数用于确定每个图像的预测类别。然后通过比较预测和真实标签来计算正确分类的图像数量。 total 变量通过当前批次的规模递增,correct 变量通过批次中正确分类的图像数量递增。 循环完成后,模型在测试集上的准确率百分比被打印到控制台。此模型的准确率为 91%,足以玩游戏。可以通过更多实验调整优化器的超参数。仍有改进的空间。在我的未来博客中,我将深入研究使用 weights and biases 工具进行超参数调整。

步骤 6:推理/玩游戏

创建一个新文件 dino.py。运行此文件,然后来到小恐龙游戏屏幕,观看您的 AI模型玩这个游戏。

import torch
from torchvision.models.efficientnet import efficientnet_v2_s                                                                                          
import keyboard
from PIL import Image, ImageGrab
import numpy as np
from torchvision.transforms import Compose, Resize, CenterCrop, ToTensor, Normalize
from tqdm import tqdm
device = "cuda" if torch.cuda.is_available() else "cpu"
model = efficientnet_v2_s()
model.classifier = torch.nn.Linear(in_features = 1280, out_features = 2)
model.load_state_dict(torch.load("models/efficientnet_s.pth"))
model.to(device)
model.eval()
transformer = Compose([
    Resize([480,480]),
    CenterCrop(480),
    Normalize(mean =[0.485, 0.456, 0.406], std =[0.229, 0.224, 0.225])
])
def generator():
    while(not keyboard.is_pressed("esc")):
      yield
for _ in tqdm(generator()):
    image = ImageGrab.grab(bbox = (620,220,1280,360)) 
    image = ToTensor()(image)
    image = image.to(device)
    image = transformer(image)
    outputs = model(image[None , ...])
    _,preds = torch.max(outputs.data, 1)
    if preds.item() == 1:
        keyboard.press_and_release("space")

代码的第一部分导入必要的库和模块,包括 torchvision 包中的 EfficientNetV2-S 模型、用于模拟键盘按键的 keyboard 库、用于图像处理的 PIL 库、用于数值操作的 numpy 库,以及 tqdm 库用于进度跟踪。 代码然后加载预训练的 EfficientNetV2-S 模型,为其添加一个新的线性分类器层,并从保存的检查点文件加载新模型的训练权重。然后将模型移动到 GPU 进行更快的处理,并将其设置为评估模式。 transformer 变量定义了一系列图像预处理步骤,这些步骤在捕获的屏幕图像被馈送到模型之前应用于它。这些步骤包括将图像缩放到一个 480x480 的正方形,裁剪到中心,并使用 ImageNet 数据集的均值和标准差归一化像素值。 generator 函数是一个简单的循环,它生成一个空值,直到按下“esc”键。 for 循环持续捕获指定边界框内的屏幕图像,使用 ImageGrab.grab() 函数。捕获的图像然后被转换为 PyTorch 张量并发送到 GPU。transformer 被应用于张量以预处理图像。最后,预处理的图像被馈送到模型以获得预测的输出概率。torch.max() 函数用于获取概率最高的类标签,如果预测的标签对应于“跳跃”动作,则调用 keyboard.press_and_release() 函数模拟空格键的按下,导致游戏中的角色跳跃。 循环继续,直到按下“esc”键,并且使用 tqdm 模块跟踪过程。现在您的模型应该能够玩恐龙游戏了。至少在鸟儿出现之前。

© 2011 - 2025 Macro Zhao的分享站

关于我

如遇到加载502错误,请尝试刷新😄

Hi,欢迎访问 Macro Zhao 的博客。Macro Zhao(或 Macro)是我在互联网上经常使用的名字。

我是一个热衷于技术探索和分享的IT工程师,在这里我会记录分享一些关于技术、工作和生活上的事情。

我的CSDN博客:
https://macro-zhao.blog.csdn.net/

欢迎你通过评论或者邮件与我交流。
Mail Me

推荐好玩(You'll Like)
  • AI 动·画
    • 这是一款有趣·免费的能让您画的画中的角色动起来的AI工具。
    • 支持几十种动作生成。
我的项目(My Projects)
  • 爱学习网

  • 小乙日语App

    • 这是一个帮助日语学习者学习日语的App。
      (当然初衷也是为了自用😄)
    • 界面干净,简洁,漂亮!
    • 其中包含 N1 + N2 的全部单词和语法。
    • 不需注册,更不需要订阅!完全免费!
  • 小乙日文阅读器

    • 词汇不够?照样能读日语名著!
    • 越读积累越多,积跬步致千里!
    • 哪里不会点哪里!妈妈再也不担心我读不了原版读物了!
赞助我(Sponsor Me)

如果你喜欢我的作品或者发现它们对你有所帮助,可以考虑给我买一杯咖啡 ☕️。这将激励我在未来创作和分享更多的项目和技术。🦾

👉 请我喝一杯咖啡

If you like my works or find them helpful, please consider buying me a cup of coffee ☕️. It inspires me to create and share more projects in the future. 🦾

👉 Buy me a coffee