20253431 2025-2026-2 《Python程序设计》实验四报告

20253431 2025-2026-2 《Python程序设计》实验四报告

课程:《Python程序设计》

班级: 2534

姓名: 吕俊孜

学号:20253431

实验教师:王志强

实验日期:2026年5月25日

必修/选修: 公选课

1.实验内容

Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。

2. 实验过程及结果

2.1 实验分析

本来是写了一个关于word文档格式判断的小工具,想着有时候还能用到,但是后来发现好像并不切题......就很坏了

ps:如果您乐意看看,这里有链接

所以在思考了很久以后,我决定写一个简单的贪吃蛇小游戏!

但是光有这么个小游戏有什么意思,现在可是大ai时代,那我就打算训练一个简单的ai来玩这个小游戏,感觉有种电子宠物的乐趣了对吧

2.2 实验设计

我们不难想到,要完成这样一个实验,在代码阶段,我们只需要写一个简单的贪吃蛇和一个ai训练模型,接下来就只需要等ai训练完成就好了。

所以我们的目标就一目了然了。

经过查阅资料,使用DQN作为ai的模型似乎是不错的选择,由于在这方面网上有一些先例,所以我们就可以借鉴一下前人的思路

以下是对DQN的介绍,如果您了解DQN,那么请移步2.3

DQN(Deep Q-Network)大概就是用一个函数Q(s,a)来反馈自己当前的动作有多好(奖励),通过一个很复杂的公式然后慢慢优化自己的算法

有没有感觉很像在训狗?狗狗乖的时候就给他点吃的(奖励),让他能优化自己的动作

由于我们的状态变量维度比较多,所以我们需要创建一个神经网络来近似Q函数

我们会把拿到奖励的经历放在一起,训练时抽取,来让他反复学习为什么这里得到了奖励

我们复制一份神经网络,这一份当做目标网络,当一个"靶子",让神经网络有一个训练的目标

接下来我们进行一步误差处理:

Loss = Q(s, a) - (r + γ × max Q_target(s', a'))²

这一步反正就是在这里,我也不知道为什么是这样的公式,反正就是处理误差的一步

这样就可以进行迭代了,很简单吧(?)

总之接下来是我们具体的操作了

2.3 实验过程

2.3.1 贪吃蛇

贪吃蛇作为一个老牌游戏,写起来虽然不简单,但也不是特别复杂

我们创建一个snake_game.py文件,准备开始写代码

首先,为了让我们最终能看得到ai训练的结果,我们一定需要可视化,那么我们选用最简单的tk来完成

由于贪吃蛇的代码不是我们的重点,在此我大概介绍一下写的过程:

框架 :首先我们要先想好加入的库,因为是一个比较简单的游戏,我只选择了pygame(提供游戏基础),random(随机位置生成食物)和sys(提供退出键)这三个库

然后我们设定常量:其中包括屏幕设置,颜色,方向和游戏速度,这些都是我们写在函数之前的

接下来就该重头了,函数部分:

函数 :首先要明确,我们不把函数直接写在文件中,而是选择创建一个类,将其他函数写为这个类的方法,这样会让后续ai训练的代码能直接创建实例来调用我们写的游戏函数,非常方便

其次,我们要写的函数大概有:

初始化函数init

重置游戏函数reset

生成食物函数(顺便附带游戏结算)generate_food

改变方向函数change_direction

执行游戏函数step

绘制地图,蛇等等可视化元素的函数(好多好多)

渲染函数render

游玩函数human_play

具体代码请点击这里~

ps:这部分代码有ai生成的部分

2.3.2 训练ai

接下来才是本次实验的重头戏:训练ai的代码:

2.3.2.1 导入模块

我们导入了一堆模块,抛去我们常用的pygame,random,os,numpy,datetime,csv以外,还有:

collections.deque------双端队列,可以自动淘汰新数据,用来充当经验池

matplotlib.pyplot------用于绘图,生成训练曲线(这个是问了ai,说这样结果会比较直观)

torch.PyTorch------深度学习框架,提供张量运算和自动求导

ps:张量就是三维及以上的数字容器

snake_game.py------引入游戏的代码,方便让ai直接进行游玩

这一系列模块,让我们能够实现ai的训练

2.3.2.2 配置参数

接下来,我们要配置一系列参数,包括:

python 复制代码
STATE_DIM = 11          # 状态维度
ACTION_DIM = 4          # 动作维度
MEMORY_SIZE = 50000     # 经验池大小
BATCH_SIZE = 64         # 批次大小
GAMMA = 0.9             # 折扣因子
EPSILON_START = 1.0     # 初始探索率
EPSILON_END = 0.01      # 最终探索率
EPSILON_DECAY = 200000  # 探索率衰减步数
LEARNING_RATE = 0.0003  # 学习率
TARGET_UPDATE = 2000    # 目标网络更新步数
TRAIN_STEPS = 500000    # 总训练步数
SAVE_INTERVAL = 50000   # 模型保存间隔
FRAME_SKIP = 2          # 帧跳过
LOG_INTERVAL = 100      # 数据记录间隔

STATE_DIM = 11

我们设计了十一个状态维度,分别是:

食物与蛇头的相对位置,2个维度

各个方向是否有危险,4个维度

当前移动方向,4个维度

蛇身长度占最大长度的比例,1个维度

这十一个状态维度将作为神经网络的输入

ACTION_DIM = 4

动作维度作为神经网络的输出,反映了ai模型对于当前状况的反应,分别对应了四个方向

MEMORY_SIZE = 50000

经验池大小设定为50000条数据,超出时自动删去最旧的

BATCH_SIZE = 64

每一批次随机抽取64条经验

GAMMA = 0.9

折扣因子决定了ai模型更关注短期还是长期,我们设定为0.9,让ai模型较少地考虑长期奖励。这是因为贪吃蛇本身是一个较为看重短期奖励的游戏,如果是棋牌博弈类的游戏,则更看重长期的奖励

类似于:我们在玩贪吃蛇的时候,不需要考虑一百步后会发生什么,但是下棋、打牌的时候就需要更长远的考虑

而我们并没有一杆子把长期奖励的权重拉的太低,太低的话会导致ai模型完全靠当前的反应操作,而没有了我们想要的"智能"效果

EPSILON_START

开始时 100% 随机探索,让ai模型尝试各种动作,快速积累各种经验

EPSILON_END

最终只剩 1% 随机动作,其余靠网络决策

EPSILON_DECAY

在 20 万步内从 1.0 线性衰减到 0.01

LEARNING_RATE

学习率 0.0003,较小值让训练更稳定

TARGET_UPDATE

每 2000 步同步一次目标网络

TRAIN_STEPS

总共训练 50 万步

FRAME_SKIP

每 2 帧执行一次动作,加快训练

2.3.2.3 创建DQN网络

我们定义了一个简单的神经网络,作为我们的策略网络和目标网络:

python 复制代码
class DQN(nn.Module):
    def __init__(self, state_dim=11, action_dim=4):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, action_dim)
        )

我们给定的参数中,state_dim=11,action_dim=4,分别用来接收参数和输出结果

在每一个神经元中,他会经过一系列计算,然后给出结果,多个神经元构成的神经网络结构中,中间的叫做隐藏层,我们设定为128+128个神经元

所以我们每给出一个十一维的参数,都会经历:

被11个神经元接收->穿过128层隐藏层->再穿过128层隐藏层->反馈到4个用于输出的神经元,并给出相应的结果

2.3.2.4 创建经验回放池
python 复制代码
class ReplayBuffer:
    def __init__(self, capacity=MEMORY_SIZE):
        self.buffer = deque(maxlen=capacity)

    def push(self, state, action, reward, next_state, done):
        self.buffer.append((state, action, reward, next_state, done))

    def sample(self, batch_size):
        batch = random.sample(self.buffer, batch_size)
        states, actions, rewards, next_states, dones = zip(*batch)
        return (np.array(states), np.array(actions), np.array(rewards),
                np.array(next_states), np.array(dones))

    def __len__(self):
        return len(self.buffer)

经验回放池是DQN算法的一大创新,解决了很多老方法的问题,他的运行逻辑大概是:

存入经验(push)->随机采样(sample)->查看大小(len)

存入经验:

游戏每进行一步,我们就把当前步的状态(state,包含了我们创建的11维参数),动作(action,当前ai模型的决策),奖励(reward,评判自己这一步做的怎么样),下一步的状态(next_state),是否结束(done)这五个参数打包为一个元组存入经验池

随机采样:

我们随机取batch_size(我们设定为64)条数据,然后通过zip函数转置成五个数组

查看大小:

看是否超过设定的上限(我们设定为50000条),超过时删除最老的,加入最新的

2.3.2.5 奖励函数

奖励函数是告诉AI"做得好不好"的关键,我们设计了多种的奖励:

事件 奖励值 说明
吃到食物 +10 主要目标
死亡 -10 惩罚
靠近食物 +0.05 引导方向
远离食物 -0.02 避免乱走
存活 +0.01 鼓励活久一点

我们不止有吃到食物和死亡的奖励,还有对于当前位置,方向和存活时间的奖励,用这样多方面的奖励,我们能得到一个更"聪明"的ai模型

2.3.2.6 动作选择

我们使用ε-greedy策略来选择动作:

python 复制代码
def select_action(self, state):
    if random.random() < self.epsilon:
        return random.randrange(4)  # 探索
    with torch.no_grad():
        q_values = self.policy_net(state)
        return q_values.argmax().item()  # 利用

一开始ε=1.0,AI完全随机探索,像个婴儿到处乱撞。随着训练进行,ε逐渐衰减到0.01,AI越来越依赖自己的判断。这个过程叫"从探索到利用",就像人从尝试到熟练。

2.3.2.7 训练函数

经过了这么长一大段的类和函数的定义,我们终于迎来了真正的训练函数

在DQNTrainer类下,我们定义了一个叫做train的函数,这是我们真正的训练函数

其伪代码大概为:

text 复制代码
for step in range(总步数):
    获取当前状态
    选择动作并执行
    得到奖励和下一状态
    存储经验
    从经验池随机采样一批训练
    每隔N步同步目标网络
    如果游戏结束,重置

这里面最关键的一步是训练更新:

python 复制代码
current_q = policy_net(states).gather(1, actions)
next_q = target_net(next_states).max(1, keepdim=True)[0]
target_q = rewards + GAMMA * next_q * (1 - dones)
loss = MSELoss(current_q, target_q)

简单来说,就是让策略网络预测的Q值,去逼近目标网络算出来的"正确答案"。目标网络每隔2000步才更新一次,相当于给了AI一个稳定的"靶子",不会因为自己一边学一边改目标而震荡。

2.3.3 实验结果

在经过一段时间python对cpu的猛攻后,我们就可以得到一个大概能玩一玩游戏的ai模型
训练曲线图,蓝色散点为单回合分数,红色曲线为100回合平均分 如图所示,训练曲线呈现明显的上升趋势。

前期(0-10万步):平均分接近 0,AI 处于随机探索阶段

中期(10-30万步):平均分开始缓慢上升,AI 逐渐学会朝食物移动

后期(30-50万步):平均分稳定在4~5分左右,最高分可达到10+分

曲线震荡属于正常现象,因为 ε-greedy 策略保留了 1% 的随机探索

不过在后续的演示环节中,实测下来几次基本上ai都能达到20+分数,甚至有时候能到30+,不知道为什么曲线好像有点问题,不过至少说明我们的训练还是很有效的

演示效果:

补充:项目代码在这里~

3. 实验过程中遇到的问题和解决过程

问题一:在ai演示的时候,我们发现,ai时常会有吃了一些食物后就会开始原地打转

解决:我初步怀疑是由于奖励机制中存在的"存活时长"让ai有了类似"只要活下去就会拿到更多奖励"的判断,所以我尝试过删去存活时长的奖励,但是实际跑下来发现好像还不如有这个奖励的时候,所以最后选择保留了下来

问题二:对于训练完成的ai,我差不多测试了十几次,但是没有出现过能过关的情况

解决:很简单,摆了。因为这个游戏设定的有点难了,过关情况设定为"铺满整个画面",但是要铺满25*25的画面太难了,不仅需要有足够的反应能力,同时也要有超越常人的规划能力,而在我们的实验中,我们并没有为ai模型设定过高的长远期望,所以ai模型确实很难达到这个目标。而且我们的训练步数为50万,没有更多次的训练,想要达成一个高目标很难。这个实验的目的只是大概尝试以DQN为基础,训练一个ai模型出来,并非是要专门训练出一个专精于这一个拙劣的贪吃蛇游戏的大师ai模型,那也没什么意义,所以我们只在短时间内训练ai,并且能明显看到成效,就已经达到我们的实验目标了

其他(感悟、思考等)

课程总结

好的,以下是一段更充实、篇幅稍长的课程总结:

通过这段时间的Python课程学习,我从一个对编程仅有模糊概念的初学者,逐步掌握了Python的基础语法,包括变量、数据类型、条件判断、循环以及函数的定义与调用。同时,课程重点讲解了列表这一常用数据结构,我学会了如何创建、索引、切片以及使用列表推导式等高效操作方法。在此基础上,课程进一步拓展到网络连接与爬虫编程,我了解了HTTP协议的基本原理,学会了使用requests库发送网络请求,并通过BeautifulSoup等工具解析HTML网页,从而提取出所需的文本、链接或图片信息。通过实际编写爬虫程序,我不仅加深了对基础语法的运用能力,也体会到Python在自动化数据采集方面的便捷与强大。当然,我也认识到爬虫过程中需要遵守网站协议、合理设置请求频率,避免对目标服务器造成负担。总体而言,这门课程让我从写简单脚本到能够独立完成一个小型爬虫项目,收获颇丰。未来我会继续深入学习反爬机制、数据清洗等内容,进一步提升自己的编程实践能力。

参考资料