Python强化学习实战:从零构建多智能体对战模拟器 最近在技术社区里一个名为“甜瓜世界战争1战争起源”的项目悄然走红。乍一看这个标题你可能会以为这是某个独立游戏或者网络模组但点进去才发现它其实是一个基于Python和强化学习框架的AI对战模拟器。这并非一个简单的游戏Demo而是一个探讨多智能体协作与对抗、策略演化以及复杂系统涌现行为的绝佳实验场。对于开发者而言这个项目的价值在于它提供了一个低成本、高灵活度的沙盒环境让你可以亲手搭建、训练并观察一群“甜瓜”智能体如何在设定的规则下为了资源、领土或生存而“开战”。这背后涉及到的技术栈包括环境建模、智能体决策逻辑、奖励函数设计、多进程/分布式训练等正是当前AI研究与应用的热点。本文将带你深入“甜瓜世界战争1”的核心不仅会拆解其技术架构和实现原理更会提供一份从零开始的完整实践指南。无论你是想学习强化学习实战还是对多智能体系统MAS感兴趣亦或是单纯想找一个有趣的项目来练手Python这篇文章都将为你提供清晰的路径和可运行的代码。1. 这个项目真正解决了什么问题在深入代码之前我们必须先理解我们为什么要折腾一群“甜瓜”打架这背后对应着哪些真实的技术挑战传统的单智能体强化学习如玩Atari游戏、下围棋的AlphaGo已经取得了巨大成功但其设定相对理想化一个智能体在一个明确规则的环境中追求单一目标。然而现实世界是多主体、动态交互、目标冲突的。例如自动驾驶你的车需要与道路上其他数十辆由人或AI驾驶的车辆互动。电商定价你的定价策略会直接影响竞争对手的策略从而改变整个市场。网络资源分配多个用户或服务竞争有限的带宽和计算资源。“甜瓜世界战争1”这类项目正是为了模拟和研究这类复杂互动场景而生的轻量级平台。它抽象并简化了现实问题让你可以专注于核心挑战环境建模如何用代码定义一个“世界”包含地图、资源、物理规则移动、碰撞、攻击智能体设计每个甜瓜的“大脑”是什么是简单的规则脚本还是一个可训练的神经网络它如何感知环境看到周围的敌人和资源奖励机制如何定义“好”与“坏”是消灭敌人得分还是占领地盘得分或是存活时间越长越好奖励函数的设计直接决定了智能体最终会演化出何种行为。多智能体交互智能体之间的行为会相互影响。一个甜瓜的攻击会导致另一个甜瓜的防御或逃跑这种动态博弈是研究的重点。训练效率模拟成千上万场战争来训练智能体需要高效的仿真和并行计算能力。通过动手实现这个项目你将能切身体会到上述挑战并学习到解决它们的实用工具和方法如PyGame用于可视化Ray或RLlib用于分布式训练PyTorch/TensorFlow用于构建智能体网络。2. 核心概念与项目架构在开始搭建环境之前我们需要厘清几个关键概念和项目的整体架构。2.1 核心概念解析环境 (Environment)即“甜瓜世界”。它是一个动态系统定义了状态空间所有甜瓜的位置、血量、资源分布等、动作空间移动、攻击、采集等以及状态转移规则执行某个动作后世界会如何变化。环境还负责在每一步给每个智能体提供奖励。智能体 (Agent)即“甜瓜”。它是环境的参与者拥有自己的策略。策略是一个函数输入是当前环境的状态或观察值输出是下一步要执行的动作。智能体的目标是最大化其长期累积奖励。观察 (Observation)智能体对环境的“所见”。在全信息博弈中智能体可能知道全局状态但在更真实的设定下每个智能体只能看到周围有限范围内的信息局部观察。动作 (Action)智能体可以执行的操作。通常是离散的如上、下、左、右、攻击或连续的向某个方向移动一定距离。奖励 (Reward)环境反馈给智能体的标量信号用于评价上一个动作的好坏。它是引导智能体学习的“胡萝卜”。策略 (Policy)智能体的决策逻辑。可以是查找表、if-else规则也可以是深度神经网络。2.2 项目技术栈与架构一个典型的“甜瓜世界战争”项目可能包含以下层次仿真核心层使用纯Python或NumPy实现世界的物理逻辑、碰撞检测、状态更新。这是项目的引擎。可视化层使用PyGame、Pyglet或matplotlib的动画功能将世界的状态实时渲染成图像便于我们直观观察。智能体层规则智能体用于测试环境和作为初代对手。逻辑写在固定的函数里。学习智能体基于强化学习算法如DQN, PPO, A3C的智能体。这部分会用到PyTorch或TensorFlow来构建和训练神经网络策略。训练框架层如果进行大规模训练可能会集成Ray RLlib、Stable-Baselines3或Tianshou等强化学习库它们提供了成熟的算法实现、并行采样和训练管线。评估与分析层记录训练曲线、保存模型快照、可视化智能体的行为策略等。本实践指南将聚焦于构建一个简化但完整的版本包含仿真核心、可视化以及规则智能体并为接入学习智能体打下坚实基础。3. 环境准备与前置条件我们将使用Python作为开发语言。请确保你的系统满足以下条件操作系统Windows 10/11, macOS, 或 Linux (Ubuntu 20.04 推荐)。本文示例在Windows和Ubuntu上均测试通过。Python版本Python 3.8 或 3.9。这是大多数科学计算和深度学习库兼容性最好的版本。不推荐使用Python 3.10可能遇到一些库的依赖问题。包管理工具使用pip即可。强烈建议使用虚拟环境venv或conda来隔离项目依赖。3.1 创建虚拟环境与安装依赖打开你的终端或命令提示符执行以下步骤# 1. 创建项目目录并进入 mkdir melon-world-war cd melon-world-war # 2. 创建Python虚拟环境 (以venv为例) python -m venv venv # 3. 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/macOS: source venv/bin/activate # 激活后命令行提示符前通常会出现 (venv) 字样 # 4. 安装核心依赖 pip install pygame numpy matplotlibpygame用于2D图形渲染和事件处理是我们可视化世界的窗口。numpy用于高效的数值计算处理智能体位置、向量运算等。matplotlib用于后期绘制训练曲线和数据分析。如果你的网络环境导致pip安装缓慢可以考虑使用国内镜像源例如pip install pygame numpy matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple4. 构建甜瓜世界仿真核心我们首先构建一个没有AI、只有基本规则的世界。创建一个名为world.py的文件。4.1 定义世界常量与智能体类# world.py import numpy as np import pygame from enum import Enum from typing import List, Tuple, Optional class Action(Enum): 智能体可执行的基本动作 UP 0 DOWN 1 LEFT 2 RIGHT 3 ATTACK 4 NONE 5 class MelonAgent: 甜瓜智能体类 def __init__(self, agent_id: int, team: int, x: float, y: float): self.id agent_id self.team team # 0代表红队1代表蓝队 self.x x self.y y self.health 100.0 # 生命值 self.attack_range 30.0 # 攻击范围 self.attack_damage 20.0 # 攻击伤害 self.speed 2.0 # 移动速度 self.color (255, 0, 0) if team 0 else (0, 0, 255) # 红队红色蓝队蓝色 self.last_action Action.NONE def get_observation(self, world_width: int, world_height: int) - np.ndarray: 获取智能体的局部观察简化版只返回自身状态 # 更复杂的版本可以返回视野内的敌人、队友、资源信息 return np.array([self.x / world_width, self.y / world_height, self.health / 100.0]) def take_action(self, action: Action, world: MelonWorld) - Tuple[float, bool]: 执行动作并返回奖励是否存活 简化逻辑移动无奖励攻击命中得奖励被击败得负奖励。 reward 0.0 alive True if action Action.UP: self.y max(0, self.y - self.speed) elif action Action.DOWN: self.y min(world.height - 1, self.y self.speed) elif action Action.LEFT: self.x max(0, self.x - self.speed) elif action Action.RIGHT: self.x min(world.width - 1, self.x self.speed) elif action Action.ATTACK: # 寻找攻击范围内的最近敌人 target self._find_nearest_enemy(world.agents) if target is not None: target.health - self.attack_damage reward 10.0 # 命中奖励 if target.health 0: reward 50.0 # 击败奖励 world.remove_agent(target.id) # Action.NONE 什么都不做 self.last_action action # 检查自己是否存活简化暂无环境伤害 alive self.health 0 return reward, alive def _find_nearest_enemy(self, agents: List[MelonAgent]) - Optional[MelonAgent]: 寻找距离最近的敌对智能体 min_dist float(inf) target None for agent in agents: if agent.team ! self.team and agent.health 0: dist np.sqrt((self.x - agent.x)**2 (self.y - agent.y)**2) if dist self.attack_range and dist min_dist: min_dist dist target agent return target4.2 定义世界主类继续在world.py中添加class MelonWorld: 甜瓜世界主类管理所有智能体和环境状态 def __init__(self, width: int 800, height: int 600, n_agents_per_team: int 5): self.width width self.height height self.agents {} self.agent_counter 0 self._init_agents(n_agents_per_team) def _init_agents(self, n_per_team: int): 初始化智能体两队分别置于左右两侧 for team in [0, 1]: for i in range(n_per_team): # 红队在左侧蓝队在右侧 x 50 i * 30 if team 0 else self.width - 100 - i * 30 y self.height // 2 (i - n_per_team//2) * 40 agent_id self.agent_counter self.agents[agent_id] MelonAgent(agent_id, team, x, y) self.agent_counter 1 def remove_agent(self, agent_id: int): 从世界中移除一个智能体 if agent_id in self.agents: del self.agents[agent_id] def step(self, actions: dict) - dict: 世界前进一步。 :param actions: 字典key为agent_idvalue为Action枚举 :return: 字典包含每个存活智能体的观察、奖励、是否存活标志 rewards {} observations {} dones {} # 先收集所有智能体的动作结果避免在迭代中修改字典 results [] for agent_id, action in actions.items(): if agent_id in self.agents: agent self.agents[agent_id] reward, alive agent.take_action(action, self) results.append((agent_id, agent.get_observation(self.width, self.height), reward, alive)) # 更新返回信息并移除死亡的智能体 dead_agents [] for agent_id, obs, reward, alive in results: observations[agent_id] obs rewards[agent_id] reward dones[agent_id] not alive if not alive: dead_agents.append(agent_id) for dead_id in dead_agents: self.remove_agent(dead_id) return { observations: observations, rewards: rewards, dones: dones } def get_state_for_rendering(self): 获取用于渲染的世界状态 return [agent for agent in self.agents.values()]5. 可视化与主循环现在我们创建主程序文件main.py它将世界和可视化连接起来并实现一个简单的规则智能体逻辑。5.1 实现规则智能体策略# main.py import pygame import sys import random from world import MelonWorld, MelonAgent, Action class RuleBasedAgent: 一个基于简单规则的智能体策略 def __init__(self, agent_id: int): self.id agent_id def choose_action(self, world: MelonWorld, agent: MelonAgent) - Action: 规则 1. 如果生命值低随机移动模拟逃跑。 2. 否则寻找最近敌人。 3. 如果敌人在攻击范围内攻击。 4. 否则向敌人移动。 if agent.health 30: # 生命值低随机移动 return random.choice([Action.UP, Action.DOWN, Action.LEFT, Action.RIGHT]) # 寻找最近敌人 nearest_enemy None min_dist float(inf) for other in world.agents.values(): if other.team ! agent.team and other.health 0: dist ((agent.x - other.x)**2 (agent.y - other.y)**2)**0.5 if dist min_dist: min_dist dist nearest_enemy other if nearest_enemy is None: return Action.NONE if min_dist agent.attack_range: return Action.ATTACK else: # 向敌人方向移动 dx nearest_enemy.x - agent.x dy nearest_enemy.y - agent.y # 选择主要移动方向 if abs(dx) abs(dy): return Action.RIGHT if dx 0 else Action.LEFT else: return Action.DOWN if dy 0 else Action.UP5.2 创建Pygame主循环继续在main.py中添加def main(): # 初始化世界 world MelonWorld(width800, height600, n_agents_per_team5) # 初始化Pygame pygame.init() screen pygame.display.set_mode((world.width, world.height)) pygame.display.set_caption(甜瓜世界战争1战争起源 - 规则智能体演示) clock pygame.time.Clock() font pygame.font.SysFont(None, 24) # 为每个智能体创建一个规则策略对象 policies {} for agent_id in world.agents.keys(): policies[agent_id] RuleBasedAgent(agent_id) running True episode_step 0 max_steps 1000 # 最大模拟步数 while running and episode_step max_steps: # 处理退出事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 1. 每个智能体根据策略选择动作 actions {} for agent_id, policy in policies.items(): if agent_id in world.agents: agent world.agents[agent_id] actions[agent_id] policy.choose_action(world, agent) # 2. 世界执行一步 step_result world.step(actions) # 3. 渲染 screen.fill((240, 240, 220)) # 浅米色背景 # 绘制智能体 for agent in world.get_state_for_rendering(): # 绘制身体圆形 pygame.draw.circle(screen, agent.color, (int(agent.x), int(agent.y)), 15) # 绘制生命条 health_width 30 health_height 5 health_x int(agent.x - health_width / 2) health_y int(agent.y - 25) # 背景条灰色 pygame.draw.rect(screen, (100, 100, 100), (health_x, health_y, health_width, health_height)) # 前景条绿色到红色 health_ratio agent.health / 100.0 health_color (int(255 * (1 - health_ratio)), int(255 * health_ratio), 0) current_width int(health_width * health_ratio) pygame.draw.rect(screen, health_color, (health_x, health_y, current_width, health_height)) # 绘制团队标识 team_text font.render(fT{agent.team}, True, (255, 255, 255)) screen.blit(team_text, (int(agent.x - 5), int(agent.y - 10))) # 显示统计信息 red_count sum(1 for a in world.agents.values() if a.team 0) blue_count sum(1 for a in world.agents.values() if a.team 1) stats_text font.render(f步数: {episode_step} | 红队: {red_count} | 蓝队: {blue_count}, True, (0, 0, 0)) screen.blit(stats_text, (10, 10)) pygame.display.flip() clock.tick(30) # 30 FPS episode_step 1 # 检查是否一方全灭 if red_count 0 or blue_count 0: winner 蓝队 if red_count 0 else 红队 print(f战争结束获胜方{winner} 总步数{episode_step}) # 可以在这里暂停或重置 # running False pygame.quit() sys.exit() if __name__ __main__: main()6. 运行与效果验证现在让我们运行这个初步的“甜瓜世界”。保存文件确保你的项目目录下有world.py和main.py两个文件。运行程序在激活的虚拟环境中运行以下命令python main.py预期效果会弹出一个800x600的窗口。你会看到红色和蓝色两群“甜瓜”分别位于屏幕左右两侧。它们会根据我们编写的简单规则RuleBasedAgent开始行动寻找最近的敌人靠近并攻击。生命值低的甜瓜会尝试随机移动“逃跑”。观察控制台当一方被完全消灭时控制台会打印出获胜方和总步数。如何判断运行成功窗口正常弹出无报错。甜瓜们能正常移动并随着攻击生命条减少。被击败的甜瓜会从屏幕上消失。最终控制台输出胜利信息。如果运行失败第一步应该看哪里检查Python和Pygame版本确保在虚拟环境中且版本兼容。检查文件路径和导入确保main.py和world.py在同一目录且main.py中正确from world import ...。查看命令行错误信息Python的错误回溯Traceback会明确指出问题所在通常是语法错误、缩进问题或未定义的变量。7. 从规则到学习接入强化学习智能体有了可运行的世界和可视化我们现在可以引入真正的“大脑”——可学习的智能体。我们将使用一个流行的强化学习库Stable-Baselines3来演示。这需要额外安装。7.1 安装强化学习库pip install stable-baselines3[extra] torch7.2 将世界包装成Gymnasium标准环境强化学习库通常要求环境遵循gymnasium原OpenAI Gym接口。我们需要创建一个适配器。新建文件gym_wrapper.py。# gym_wrapper.py import gymnasium as gym from gymnasium import spaces import numpy as np from world import MelonWorld, Action class MelonWorldEnv(gym.Env): 将甜瓜世界包装成Gymnasium环境单智能体版本简化示例 metadata {render_modes: [human, rgb_array], render_fps: 30} def __init__(self, render_modeNone): super().__init__() self.world MelonWorld(n_agents_per_team1) # 简化单智能体对战一个规则对手 self.render_mode render_mode self.current_agent_id 0 # 控制我们学习的智能体假设是红队第一个 # 定义动作空间5个离散动作上下左右攻击 self.action_space spaces.Discrete(5) # 对应Action枚举的0-4 # 定义观察空间智能体的归一化位置和生命值 self.observation_space spaces.Box( low0.0, high1.0, shape(3,), dtypenp.float32 ) # 如果需要渲染初始化pygame这里简化实际可复用main.py的逻辑 self.screen None if render_mode human: import pygame pygame.init() self.screen pygame.display.set_mode((self.world.width, self.world.height)) pygame.display.set_caption(MelonWorld RL) def reset(self, seedNone, optionsNone): 重置环境到初始状态 super().reset(seedseed) self.world MelonWorld(n_agents_per_team1) self.current_agent_id 0 agent self.world.agents.get(self.current_agent_id) if agent is None: # 如果智能体已不存在理论上不会发生在reset时重新获取一个 self.current_agent_id list(self.world.agents.keys())[0] agent self.world.agents[self.current_agent_id] obs agent.get_observation(self.world.width, self.world.height) info {} return obs.astype(np.float32), info def step(self, action): 执行一个动作 # 将离散动作ID转换为Action枚举 action_enum list(Action)[action] # 为所有智能体生成动作我们的智能体用学习到的动作对手用规则 actions {} for aid, agent in self.world.agents.items(): if aid self.current_agent_id: actions[aid] action_enum else: # 对手使用一个非常简单的规则随机移动或攻击 from main import RuleBasedAgent # 简单起见这里直接调用 # 注意实际项目应避免循环导入这里仅为演示 # 更好的做法是将规则逻辑独立出来 if agent.health 0: # 简单随机策略 import random actions[aid] random.choice([Action.UP, Action.DOWN, Action.LEFT, Action.RIGHT, Action.ATTACK]) else: actions[aid] Action.NONE # 世界前进一步 step_result self.world.step(actions) # 提取我们智能体的信息 obs step_result[observations].get(self.current_agent_id, np.zeros(3)) reward step_result[rewards].get(self.current_agent_id, 0.0) terminated step_result[dones].get(self.current_agent_id, False) truncated False # 可以设置最大步数截断 # 检查是否所有敌人都被消灭额外奖励 enemies_alive any(a.team ! self.world.agents[self.current_agent_id].team and a.health 0 for a in self.world.agents.values()) if not enemies_alive: reward 100.0 terminated True info {} return obs.astype(np.float32), reward, terminated, truncated, info def render(self): 渲染环境简化版 if self.render_mode ! human: return # 这里可以复用main.py的渲染逻辑为简洁省略 pass def close(self): if self.screen is not None: import pygame pygame.quit()7.3 训练一个PPO智能体创建一个新的训练脚本train.py。# train.py from stable_baselines3 import PPO from stable_baselines3.common.env_util import make_vec_env from gym_wrapper import MelonWorldEnv import os # 创建并行化环境加速采样 env make_vec_env(MelonWorldEnv, n_envs4) # 创建PPO模型 model PPO( MlpPolicy, # 使用多层感知机策略 env, verbose1, # 打印训练日志 tensorboard_log./ppo_melon_tensorboard/, # 可选使用TensorBoard记录 learning_rate3e-4, n_steps2048, # 每次更新前收集的步数 batch_size64, n_epochs10, # 每次更新时优化epoch数 gamma0.99, # 折扣因子 gae_lambda0.95, clip_range0.2, ent_coef0.01, # 鼓励探索 ) # 开始训练 print(开始训练PPO智能体...) model.learn(total_timesteps100000) # 训练10万步 # 保存模型 os.makedirs(./models, exist_okTrue) model.save(./models/ppo_melon_agent) print(模型已保存至 ./models/ppo_melon_agent.zip) # 关闭环境 env.close()运行这个脚本你将看到控制台输出训练进度。训练完成后会生成一个模型文件。7.4 加载并测试训练好的智能体创建测试脚本test_trained.py。# test_trained.py from stable_baselines3 import PPO from gym_wrapper import MelonWorldEnv import pygame # 加载训练好的模型 model PPO.load(./models/ppo_melon_agent) # 创建用于可视化的环境 env MelonWorldEnv(render_modehuman) obs, info env.reset() running True clock pygame.time.Clock() while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 使用模型预测动作 action, _states model.predict(obs, deterministicTrue) # 执行动作 obs, reward, terminated, truncated, info env.step(action) # 简单渲染这里需要你实现env.render()来更新画面 # env.render() # 为了演示我们直接打印信息 print(f动作: {action}, 奖励: {reward:.2f}, 终止: {terminated}) if terminated or truncated: print(回合结束) obs, info env.reset() clock.tick(10) # 控制循环速度 env.close()8. 常见问题与排查思路在实践过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案运行python main.py时报ModuleNotFoundError: No module named pygamePygame未安装或未安装在当前虚拟环境。在终端输入pip list检查是否有pygame。确认终端前缀是否为(venv)。在激活的虚拟环境中执行pip install pygame。窗口一闪而过或立即关闭。主循环可能因为错误提前退出或者max_steps设置过小。在while running循环开始和结束处添加print语句查看循环执行情况。检查是否有未捕获的异常。增加max_steps或在循环结束前添加input(“按回车键退出...”)进行调试。智能体不动或者行为异常如一直朝一个方向走。规则智能体逻辑 (RuleBasedAgent.choose_action) 有bug或者动作映射错误。在choose_action方法中添加打印输出nearest_enemy的位置和计算出的动作。检查Action枚举值与实际移动方向的对应关系。仔细调试规则逻辑确保dx, dy计算正确动作选择符合预期。安装stable-baselines3时失败提示与torch版本冲突。PyTorch 与当前 Python 或 CUDA 版本不兼容。查看错误详情通常会提示需要哪个版本的torch。访问 PyTorch官网 获取正确的安装命令。根据你的系统环境使用官网推荐的pip命令安装兼容的 PyTorch然后再安装stable-baselines3。训练时奖励不上升智能体学不会。奖励函数设计不合理、环境太难、超参数不当或训练步数不足。使用 TensorBoard (tensorboard --logdir ./ppo_melon_tensorboard/) 查看奖励曲线。在环境中打印每一步的奖励观察是否稀疏。调整奖励函数如增加探索奖励、塑形奖励。简化环境如减少敌人。调整 PPO 超参数如learning_rate,gamma。增加total_timesteps。多智能体训练时环境不稳定或速度慢。智能体数量多Python 循环效率低或者未使用并行化。使用性能分析工具如cProfile找出瓶颈。使用numpy向量化操作替代循环。考虑使用Ray RLlib等专为多智能体设计的框架进行分布式训练。9. 最佳实践与项目扩展方向当你成功运行基础版本后可以考虑以下方向深化项目这能让你更深入地理解多智能体系统改进观察空间让智能体能看到周围一定半径内的敌人、队友和资源位置而不是仅感知自身状态。这更接近真实场景。设计更复杂的规则智能体实现不同的战术流派如激进进攻型、保守防御型、游击型作为学习智能体的多样化对手。引入资源与经济系统在地图上随机生成“食物”或“矿石”智能体采集后可以回复生命或增强攻击力。这增加了策略维度。实现真正的多智能体学习使用Ray RLlib的MultiAgentEnv接口让红蓝两队的所有智能体都具备学习能力研究合作与竞争的涌现行为。使用自博弈 (Self-Play)让学习智能体与自己历史版本的策略对战这是训练出强大AI的经典方法如AlphaGo。添加通信机制允许同队智能体之间传递简单的信息如“发现敌人位置”、“请求支援”研究协作通信的演化。优化性能对于大规模智能体如100vs100纯Python循环会成为瓶颈。可以考虑用numba加速关键计算或用Cython/Rust重写仿真核心。完善可视化与分析工具除了实时渲染增加战后复盘功能可视化智能体的移动轨迹、攻击决策热力图等便于分析策略。“甜瓜世界战争1”是一个起点而不是终点。它的价值不在于游戏本身有多复杂而在于它为你提供了一个高度可控、可扩展的实验平台。你可以像搭积木一样不断加入新的机制、新的算法去验证你对智能体行为、机器学习乃至复杂系统的种种猜想。从运行规则智能体对战到成功训练出一个能击败规则对手的强化学习智能体这个过程本身就是理解现代AI研发流程的一次绝佳实践。建议你将代码托管到GitHub记录每次实验的改动和结果这不仅能巩固所学也能成为你技术履历中一个生动有趣的项目。