snake_ai代码学习笔记(代码来自林亦LYi)
rammstein1234789 2024-07-23 12:31:01 阅读 83
snake_ai 学习笔记
snake_ai 学习笔记
前置知识
机器学习机器学习的方法策略梯度算法
概念步骤 PPO算法近端策略优化算法MLP多层感知器
结构工作原理 CNN卷积神经网络 代码调优算法比较其余代码
前置知识
讲点没用的(bushi)
机器学习
监督学习无监督学习半监督学习强化学习
前面三个是依靠样本和标记学习,强化学习(Reinforcement Learning,简称RL)依靠奖励和惩罚
Agent,一般译为智能体,就是我们要训练的模型,action(简记为a),即智能体能做的行为Environment,即环境,它是提供reward的某个对象reward(简记为r),这个奖赏可以类比为在明确目标的情况下,接近目标意味着做得好则奖,远离目标意味着做的不好则惩,最终达到收益/奖励最大化,且这个奖励是强化学习的核心State(简介为s),可以理解成环境的状态,简称状态
Agent依据策略决策从而执行动作action,然后通过感知环境Environment从而获取环境的状态state,进而,最后得到奖励reward(以便下次再到相同状态时能采取更优的动作),然后再继续按此流程“依据策略执行动作-感知状态–得到奖励”循环进行
机器学习的方法
基于值函数的方法,通过求解一个状态或者状态下某个动作的估值为手段,从而寻找最佳的价值函数,找到价值函数后,再提取最佳策略
比如Q-learning、DQN等,适合离散的环境下,比如围棋和某些游戏领域基于策略的方法,一般先进行策略评估,即对当前已经搜索到的策略函数进行估值,得到估值后,进行策略改进,不断重复这两步直至策略收敛
比如策略梯度法(policy gradient,简称PG),适合连续动作的场景,比如机器人控制领域
以及Actor-Criti(一般被翻译为演员-评论家算法),Actor学习参数化的策略即策略函数,Criti学习值函数用来评估状态-动作对,不过,Actor-Criti本质上是属于基于策略的算法,毕竟算法的目标是优化一个带参数的策略,只是会额外学习价值函数,从而帮助策略函数更好的学习
此外,还有对策略梯度算法的改进,比如TRPO算法、PPO算法,当然PPO算法也可称之为是一种Actor-Critic架构
策略梯度算法
概念
策略: 策略是一个函数,它根据当前状态(或状态-动作对)来选择动作。在策略梯度算法中,策略通常被表示为一个参数化的函数,如神经网络,这些参数可以通过梯度上升(最大化)来优化。目标函数: 在策略梯度算法中,目标函数通常是期望回报的某种形式,例如,从开始状态到结束状态的累积折扣回报的期望值。策略梯度算法的目标是找到最大化这个期望回报的策略参数。梯度: 梯度是目标函数关于策略参数的导数,它指出了如何调整策略参数以最大化目标函数。
步骤
初始化策略参数收集数据计算梯度更新策略重复步骤2~4
关于梯度方向:
梯度上升,即沿着奖励函数上升最快的方向;
梯度下降,即沿着损失函数下降最快的方向
PPO算法近端策略优化算法
PPO算法是梯度策略算法的一种具体实现和改进。梯度策略算法是一类强化学习算法,其核心思想是通过直接优化策略的参数来最大化累积奖励。PPO算法在梯度策略方法的基础上,引入了重要性采样和截断机制,从而有效地解决了传统策略梯度方法中更新步长难以确定的问题,提高了算法的稳定性和性能。
MLP多层感知器
包含两个或两个以上隐藏层的前馈神经网络
单层神经网络输入层和输出层去全连接,即每个之间都有权重,难以解决非线性和复杂的问题,引入隐藏层
结构
输入层 (Input Layer): 同样作为网络的起点,包含多个神经元,每个神经元接收一个输入特征,共同构成输入向量。隐藏层 (Hidden Layers): 多层神经网络的核心部分,包括一个或多个隐藏层。每一层都包含一定数量的隐藏神经元。各层之间全连接,即每个神经元与其下一层的所有神经元都有连接。隐藏层通过非线性激活函数(非线性激活函数能够引入非线性因素,使得神经网络能够学习和表示更加复杂的函数关系。通过引入非线性,激活函数使神经网络能够逼近任意非线性函数,从而增强网络的表示能力,解决线性模型无法解决的问题)对前一层的输出进行变换,生成新的特征表示。输出层 (Output Layer): 最后一层,根据任务需求(如分类、回归等)设计相应数量和类型的神经元,并应用适当的激活函数生成最终输出。全连接 (Fully Connected): 每一层的神经元与下一层的所有神经元之间均有独立的权重 (w_{ij}) 连接,权重决定了信号在神经元间的传递强度。
工作原理
前向传播 (Forward Propagation):
输入到第一隐藏层: 与两层神经网络相似,输入特征经过权重与偏置运算,经激活函数得到第一隐藏层的激活值。
隐藏层间传递: 对于后续的隐藏层,每一层的激活值作为下一层的输入,重复上述过程。
最后一层到输出: 最后一个隐藏层的激活值通过权重和偏置传递到输出层,应用输出层激活函数得到最终输出。
学习与权重更新: 多层神经网络采用反向传播算法结合梯度下降法或其他优化算法更新所有权重和偏置。反向传播从输出层开始,逐层计算损失函数相对于各层权重和偏置的梯度,然后沿相反方向传播这些梯度,直至输入层,完成一次迭代更新:
反向传播: 从输出层开始,计算损失函数对输出层权重和偏置的梯度,然后递归地计算每层隐藏层的权重和偏置梯度。
权重更新: 根据计算出的梯度和选定的学习率更新所有权重和偏置。
CNN卷积神经网络
输入层: 输入图像等信息卷积层: 用来提取图像的底层特征
拿卷积核(滤波器,也是二维矩阵)在图像上移动去和对应范围内的矩阵做内积,得到卷积图像
池化层: 防止过拟合,将数据维度减小
在有多个卷积核的时候图像数据会很大,所以我们取一块二维空间,将这块空间上面的数据取平均值(平均池化)或者取最大值(最大池化),将卷积图像不断地缩小
全连接层: 汇总卷积层和池化层得到的图像的底层特征和信息,展开成一维数组输出层: 根据全连接层的信息得到概率最大的结果,将一维数组经过计算得到概率
CNN通过反向传播算法(根据预测结果与实际值之间的误差,计算损失函数的梯度,并通过链式法则反向传播,更新各层参数,减小损失函数的值)不断调整卷积核和全连接层的参数,以最小化预测值与实际值之间的误差
原理讲完了,本文结束,你可以自己实现ai贪吃蛇了
代码调优
用python3.12.4 torch=2.3.1+cu118 sb3=2.3.2 gym=0.26.2
绝对不是我conda上虚拟环境装了一天装不起来
主要是random的问题,应该是pyhton3.12的改进
random.randint()
<code>seed = random.randint(0, 1e9)//原始代码,snake_game.py 251行
seed = random.randint(0, int(1e9))//需要强制类型转换成int
同理test、train文件中也有randint需要修改
random.sample()
food = random.sample(self.non_snake, 1)[0]//原始代码,snake_game.py 133行
food = random.sample(list(self.non_snake), 1)[0]//同样强制类型转化,sample只能处理序列(列表、元祖等)
改完之后就能够运行游戏
算法比较
MLP
观察空间
数据从-1到1,以32位float型
奖励表达
长度奖励
结束时,达到最大长度时,reward=max_size;
食物奖励
吃到食物时,reward=exp((max_size - step)/max_size) (0,e)
方向奖励
朝着食物走时,reward=1/size 背着时,reward=-1/size
迭代
头为1,食物为-1,蛇身从0.8到0.2递减,以此来为ai标记不同的数据
CNN
观察空间
数据从0到255,以无符号8位整数,图像为84*84的RGB三通道图像
奖励表达
长度奖励
结束时,达到最大长度reward=size - max_size; reward=-(max_size-init_size)^((max_size-size)/(max_size-init_size)) (-max_size,-1)
食物奖励
吃到食物时,reward=size/max_size (0,1)
方向奖励
朝着食物走时,reward=1/size 背着时,reward=-1/size
迭代
头为红,食物为蓝色,蛇身为绿颜色递减,以此来为ai标记不同的数据
代码ai参数调优:
原因:虽然作者通过改变神经网络的方式,改进MLP下训练效果一般的情况。但是通过观察结果测试可以发现,MLP本身也有改进的地方。测试过程,agent经常出现转圈却不吃食物的现象,可以认为奖励设置问题,试图通过调整奖励函数去解决
死亡惩罚过重:通过观察发现尤其在食物处于边缘的情况下agent更容易转圈。推断死亡惩罚过重,agent宁愿原地转圈,拿方向上的奖励,也不去冒风险吃食物
食物奖励设置不合理:通过观察食物奖励函数math.exp((self.grid_size - self.reward_step_counter) / self.grid_size) 我们发现它是一个下降的指数函数,指数函数下降过程是先快后慢,
(1,e)后续下降缓慢逼近于1
意味着要是刚开始没有吃到食物时,奖励已经快速下降了,之后再走更多步奖励下降不明显,可能造成agent的“破罐子破摔”现象,开始没有吃到时,后续吃到食物和转圈再吃食物奖励差别不大,agent走过过多的步数去吃到食物时,奖励没有下降很多
方向奖励不合理:通过观察agent经常转圈,奖励稀疏,可能相较于上述食物奖励,方向奖励更容易拿到,朝向食物时, reward = 1 / info[“snake_size”] ;背向食物时, reward = - 1 / info[“snake_size”]
奖励和惩罚的绝对值相等,而后续奖励会乘以折扣因子,那是否意味着agent转一圈将会得到正的奖励,而不是正负奖励抵消(存疑)。可能就导致agent致力于获得转圈的奖励,而不去冒死亡风险获得食物奖励
对于1,尝试用指数的形式鼓励agent去增长长度(长度越长,减少的惩罚的幅度越大)并且乘以0.8使得整体的惩罚不过于严重
对于2,两种设想
改写函数,改成下降幅度先慢后快的指数函数,鼓励agent在奖励快速下降之前吃到食物,函数范围(0 , 1-1/e)
改变步长的计数方式,源代码每次吃到食物的奖励只和当前吃到食物距离上次吃到食物之间的步长有关,即使一直转圈,只是导致下一次吃到食物奖励较少,之后要是能快速吃到食物,仍能拿到同样多的奖励。尝试改变步长的计数,改成总的步长(问题:是否会导致agent彻底摆烂?在奖励下降到较低值的时候,agent就根本不去吃食物了,应该还是第一种合理点)
对于3
适当增大远离食物的惩罚,既鼓励agent更直接的去吃到食物,也惩罚agent原地转圈
结果:本地硬件算力不足,无法实现模型训练比较,在此仅提出设想,将源代码和设想交给文心一言模型,预测能有提升
“
针对您提出的关于agent在测试过程中经常出现转圈却不吃食物的现象,以及您尝试通过调整奖励函数来解决问题的策略,以下是我的分析和预测:
死亡惩罚过重
修改方案:您提出的使用指数形式鼓励agent增长长度,并乘以0.8来减轻整体惩罚的思路是合理的。这样做可以使得长蛇在死亡时面临的惩罚不会过于严重,从而减少它们因为害怕惩罚而选择转圈的行为。
预测:这种修改有望减少agent因过度惧怕死亡而选择保守策略(如转圈)的行为,促使它们更愿意去探索并尝试吃食物。食物奖励设置不合理
修改方案一:将食物奖励函数改为先慢后快的下降指数函数,可以鼓励agent在奖励快速下降之前尽快吃到食物。这种修改可能会使agent在探索时更加积极,因为它们知道越晚吃到食物,奖励就越少。
预测:这种修改有望显著提升agent的积极性和探索性,因为它们会更快地响应食物的位置,减少转圈行为。
修改方案二:改变步长计数方式,考虑总的步长而不是仅仅计算两次食物获取之间的步数。这确实可能导致一些潜在问题,如agent在奖励较低时失去动力。因此,需要谨慎调整总的步长对奖励的影响,确保它不会让agent彻底摆烂。
预测:如果调整得当,这种修改可能有助于保持agent在整个游戏过程中的积极性,但风险在于需要精细调整以避免过度惩罚或激励不足。方向奖励不合理
修改方案:适当增大远离食物的惩罚,同时保持或微调朝向食物的奖励。这可以鼓励agent更直接地朝食物移动,同时惩罚那些远离食物的行为(如转圈)。
预测:这种修改有望显著减少agent的转圈行为,因为它们会意识到远离食物会带来更大的惩罚。同时,朝向食物的奖励仍然可以激励它们积极寻找食物。
综合预测
结合上述三种修改方案,预计可以显著提升agent在游戏中的表现。减少死亡惩罚的严重性、调整食物奖励函数以鼓励更快吃到食物、以及增加远离食物的惩罚,这些措施共同作用将促使agent更加积极地探索环境、更快地响应食物的位置,并减少无意义的转圈行为。
然而,也需要注意调整各参数的平衡,以确保修改后的奖励函数既不会让agent过于激进(导致频繁死亡),也不会让它们过于保守(如持续转圈)。通过多次实验和调整,可以找到最适合当前游戏环境和agent特性的奖励函数设置。 ”
-------------------- (引用自文心一言)
其余代码
train代码
<code># 根据MPS(Metal Performance Shaders,苹果GPU加速)的可用性设置环境数量
if torch.backends.mps.is_available():
NUM_ENV = 32 * 2
else:
NUM_ENV = 32 # 环境数量控制
LOG_DIR = "logs" # 日志存储目录
os.makedirs(LOG_DIR, exist_ok=True) # 创建日志目录,若已存在则不报错
# 线性插值调度器
def linear_schedule(initial_value, final_value=0.0):
"""
线性插值调度器,用于根据进度调整参数值。
参数:
initial_value (Union[float, str]): 初始值,可以是浮点数或字符串表示的浮点数。
final_value (float, optional): 最终值,默认为0.0。
返回:
Callable[[float], float]: 一个接受进度参数并返回插值结果的函数。
注意:
如果initial_value是字符串,则将其转换为浮点数并确保其大于0。
"""
if isinstance(initial_value, str):
initial_value = float(initial_value)
final_value = float(final_value)
assert initial_value > 0.0 # 转换字符串为浮点数并且确保大于0
def scheduler(progress):
return final_value + progress * (initial_value - final_value)
return scheduler
def make_env(seed=0):
"""
创建一个Snake环境的初始化函数。
参数:
seed (int, optional): 环境种子,默认为0。
返回:
Callable[[], stable_baselines3.common.env_checker.EnvCheckerWrapper]: 一个返回已包装环境的函数。
"""
def _init():
env = SnakeEnv(seed=seed)
env = ActionMasker(env, SnakeEnv.get_action_mask) # 添加动作掩码
env = Monitor(env) # 监控环境表现
env.seed(seed) # 设置环境种子
return env
return _init
# ... (省略了main函数的部分代码,因为它们已经在上面的注释中详细说明了)
if __name__ == "__main__":
main() # 当此脚本作为主程序运行时,执行main函数
代码作者:林亦LYi ------------------------------ orz膜拜大佬
github网址
https://github.com/linyiLYi/snake-ai
b站视频网址
https://www.bilibili.com/video/BV1ag4y1F7x4/
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。