具身智能仿真环境搭建避坑指南从PyBullet到Unity3D的实战选择当你第一次尝试为你的机器人或智能体构建一个虚拟的训练场时那种感觉就像站在一个巨大的工具墙前面对琳琅满目的扳手、螺丝刀和电钻却不知道从何下手。PyBullet、MuJoCo、Unity、Unreal Engine、Isaac Sim... 每一个名字都代表着一种可能也暗藏着无数个可能让你项目停滞数周的“坑”。具身智能的研究核心在于“身体”与环境的交互学习而仿真环境就是这个“身体”赖以生存和学习的数字孪生世界。选错了工具你可能会在物理引擎的诡异穿透、渲染的卡顿、或是资产导入的崩溃中耗尽热情。这篇文章我想和你分享的不是一份冰冷的软件功能对比表而是我亲身经历的几个项目从机械臂抓取到四足机器人复杂地形行走一路踩坑、填坑后总结出的那份关于“如何选择”的实战心得。我们将抛开那些笼统的“优缺点”深入到具体场景看看在预算有限、追求物理精度、需要高保真视觉、或是渴望快速原型的不同十字路口究竟该转向哪条路。1. 理解你的需求仿真环境选择的四个核心维度在打开任何一个软件的官网之前你需要先和自己进行一次深度对话。具身智能项目的需求千差万别盲目跟风选择“最流行”的工具往往是灾难的开始。我习惯从以下四个维度来框定需求这能帮你迅速过滤掉一大批不合适的选项。1.1 物理仿真的精度与速度鱼与熊掌的永恒博弈物理仿真是具身智能的基石。你需要问自己我的智能体需要多“真实”的物理反馈高精度动力学模拟如果你的研究聚焦于精细的力控、接触力学、或是涉及柔性体、流体的复杂交互那么物理引擎的精度就是生命线。例如训练机械臂进行精密装配或者让机器人手灵巧地操作一个易变形的物体微小的力反馈误差都可能导致策略完全失效。实时性与交互速度如果你正在进行强化学习训练需要与环境进行数百万甚至上亿次的交互那么仿真速度就至关重要。每秒能跑多少步Steps Per Second, SPS直接决定了你的算法迭代周期。一个物理精度极高但速度慢如蜗牛的仿真器对于大规模RL训练来说是致命的。这里有一个简单的决策矩阵基于我过往项目的经验需求侧重点推荐工具倾向关键考量与潜在“坑”极致物理精度预算充足MuJoCo, DrakeMuJoCo的接触模型和约束求解器在学术界备受推崇但其商业许可费用不菲。Drake在机器人动力学建模上非常严谨但学习曲线陡峭社区资源相对较少。平衡精度与速度开源优先PyBullet, Isaac SimPyBullet基于Bullet引擎开源免费在刚体模拟上表现稳健是许多RL基准环境如Gym的默认选择。但它的某些接触处理可能不够精细。NVIDIA Isaac Sim基于PhysX在GPU加速上表现出色尤其适合并行仿真但对硬件GPU有要求。需要特殊物理效果如布料、软体PyBullet, Unity (带有特定插件)PyBullet内置了基本的软体和布料模拟。Unity则需要依赖如Obi Cloth、Obi Softbody等第三方资产效果更佳但配置复杂。超大规模并行仿真数千个环境Isaac Sim, 自研基于JAX的仿真器Isaac Sim的GPU加速能力在此场景下优势明显。前沿研究中也开始流行用JAX编写可微分物理仿真实现极致的并行化和梯度计算但这需要极强的工程能力。注意没有“完美”的物理引擎。MuJoCo也可能在高速碰撞中出现穿透PyBullet的关节阻尼默认值可能需要你手动调整。选择时最好用你项目中最关键的一两个物理场景比如一个特定的抓取或行走任务编写小型测试用例在各个候选引擎中跑一跑直观感受差异。1.2 视觉渲染的真实性你需要“好看”还是“好用”视觉感知是许多具身智能任务的输入源。渲染质量直接关系到从“模拟到真实”的迁移难度。离线渲染与照片级真实感如果你要生成用于训练计算机视觉模型的大规模合成数据集或者你的算法严重依赖极其真实的纹理、光照和阴影细节例如基于视觉的精细操作那么你需要强大的渲染引擎。这通常意味着更长的渲染时间。实时渲染与效率对于强化学习训练视觉观察往往是状态的一部分。你需要的是能够以训练步调同步生成图像的渲染器速度比绝对的真实感更重要。此时轻量级渲染甚至“无头模式”Headless才是首选。一个常见的误区是盲目追求游戏级的画面。我曾在一个导航项目初期执着于使用Unreal Engine打造逼真的室内场景结果发现训练时超过80%的算力都消耗在渲染上而算法本身并不需要那么丰富的细节。后来切换到Unity的轻量渲染管线URP并将纹理和光照大幅简化训练速度提升了三倍而任务成功率并未下降。# 一个简单的对比在PyBullet中获取相机图像速度优先 import pybullet as p import pybullet_data # 连接物理服务器无GUI模式节省资源 physicsClient p.connect(p.DIRECT) # 设置相机参数 view_matrix p.computeViewMatrix([0, -1, 1], [0, 0, 0], [0, 0, 1]) proj_matrix p.computeProjectionMatrixFOV(fov60, aspect1.0, nearVal0.1, farVal10.0) # 获取图像宽度、高度、RGB、深度、分割掩码 width, height 224, 224 images p.getCameraImage(width, height, view_matrix, proj_matrix) rgb_array images[2] # RGB数据// 在Unity中你可能使用C#脚本控制一个高保真相机这涉及复杂的材质和光照计算。 // 以下仅为示意实际代码涉及相机组件、渲染纹理等。 public class HighFidelityCameraCapture : MonoBehaviour { public Camera agentCamera; public RenderTexture renderTex; void Update() { // 每一帧或每个时间步进行渲染 agentCamera.Render(); // 从RenderTexture中读取像素数据到CPU这个过程比PyBullet的接口调用开销大得多 RenderTexture.active renderTex; Texture2D tex new Texture2D(renderTex.width, renderTex.height); tex.ReadPixels(new Rect(0, 0, renderTex.width, renderTex.height), 0, 0); byte[] rgbBytes tex.EncodeToPNG(); // 或获取原始像素数据 } }关键抉择点在于你的视觉感知网络是在仿真数据上训练并直接用于仿真还是需要迁移到真实世界对于前者渲染效率优先对于后者则需要通过域随机化等技术来弥补真实感差距此时渲染的灵活性和可编程性比单纯的画质更重要。1.3 开发效率与生态不要重复造轮子时间是宝贵的科研资源。一个拥有丰富预制组件、活跃社区和良好文档的工具链能让你将精力集中在算法创新上而非环境搭建的泥潭中。预制资产与模型库你的项目是否需要常见的机器人模型如Franka Panda, UR5、家居场景或特定物体一些平台提供了开箱即用的资产库。PyBullet通过pybullet_data模块提供了一些基础机器人、物体和场景。Unity/Unreal拥有庞大的Asset Store市场可以找到几乎任何类型的3D模型但物理属性需要自己配置。NVIDIA Isaac Sim提供了丰富的机器人、传感器和虚拟环境资产且针对机器人仿真进行了优化。API设计与编程体验你更喜欢用Python进行快速实验还是愿意使用C#/C以获得更高的性能和控制力Python优先PyBullet, Gymnasium, MuJoCo (通过mujoco-py或mujoco的Python绑定) Drake。它们与主流的机器学习库PyTorch, TensorFlow, JAX集成无缝。C#/C生态Unity (C#) Unreal Engine (C/Blueprint) Isaac Sim (Python接口已很完善底层为C)。这些引擎功能强大但需要你熟悉其特定的开发模式和框架。社区与文档遇到一个诡异的物理bug时Stack Overflow或GitHub issue上是否有相关的讨论官方教程是否清晰这一点上PyBullet和Unity由于用户基数大通常更容易找到解决方案。1.4 部署与“模拟-真实”迁移的路径仿真的最终目的是服务于现实。你搭建的环境是否便于将训练好的策略部署到真实机器人上中间件与通信ROS (Robot Operating System) 是机器人领域的实际标准。仿真环境是否提供良好的ROS接口或桥接工具Gazebo与ROS集成最为紧密是传统机器人仿真的首选。Isaac Sim提供了强大的ROS2桥接功能。Unity/Unreal可以通过ROS#或ROS-TCP-Connector等插件实现通信但需要额外配置。传感器模拟的真实性仿真传感器如RGB-D相机、激光雷达、IMU的数据噪声、畸变模型是否与你的真实传感器匹配能否方便地注入各种噪声进行域随机化控制接口的一致性仿真中控制机器人的接口如位置控制、速度控制、力矩控制是否与真实机器人的驱动程序接口保持一致或易于转换避免在部署时重写大量底层代码。综合以上四个维度你可以给自己当前的项目画一个象限图。例如一个“高物理精度、中等渲染需求、Python开发、需ROS部署”的项目可能就会指向MuJoCo或Isaac Sim。而一个“中等物理精度、高渲染需求、快速原型、暂不考虑部署”的视觉导航项目Unity可能是更快乐的起点。2. 主流工具深度剖析与实战踩坑记录了解了选择维度我们来深入几个主流工具的内部结合具体案例看看它们的光环背后有哪些需要小心绕行的“坑”。2.1 PyBullet轻快灵活的“瑞士军刀”PyBullet是我个人在算法原型阶段最常使用的工具。它的核心优势是简单直接。几行代码就能启动一个物理世界加载机器人并开始交互。典型应用场景强化学习算法快速验证、多智能体仿真、不需要复杂视觉渲染的物理交互研究。让我又爱又恨的“坑”与技巧“幽灵碰撞”与穿透在高速运动或薄物体接触时Bullet引擎的离散碰撞检测DCD可能导致物体相互穿透。解决方案启用连续碰撞检测CCD。# 为高速运动的物体如机器人末端启用CCD p.setPhysicsEngineParameter(enableConeFriction1) robot_id p.loadURDF(robot.urdf) p.changeDynamics(robot_id, linkIndex, collisionMargin0.01, ccdSweptSphereRadius0.05)URDF加载的材质丢失PyBullet在加载URDF文件时通常会忽略visual标签中的材质颜色信息模型看起来是灰白的。解决方案手动指定颜色或使用更复杂的.obj/.stl网格文件。# 加载后手动改变颜色 p.changeVisualShape(robot_id, linkIndex, rgbaColor[1, 0, 0, 1]) # 红色同步与实时模式p.stepSimulation()默认在实时模式下运行模拟速度受限于真实时间。对于需要固定步长或加速仿真的RL训练必须使用p.setRealTimeSimulation(0)禁用实时模式然后手动控制步进。p.setRealTimeSimulation(0) for _ in range(num_steps): # 应用控制指令 p.setJointMotorControlArray(...) # 推进固定步长的物理模拟 p.stepSimulation() # 获取观察 obs get_observation()获取接触信息获取两个特定物体间的接触点信息有时比较麻烦。p.getContactPoints()会返回所有接触点需要自己过滤。contact_points p.getContactPoints(bodyArobot_id, bodyBobject_id, linkIndexAlink_index) if len(contact_points) 0: # 发生了接触 normal_force contact_points[0][9] # 法向力实战心得PyBullet就像一辆手动挡的越野车它能带你去很多地方但需要你自己处理很多细节。对于刚入门具身智能仿真的同学我强烈建议从PyBullet开始它能让你深刻理解物理仿真中的基本概念和常见问题。2.2 Unity打造高保真世界的“梦工厂”当你的项目需要复杂的场景、逼真的光影、或是与游戏逻辑深度结合时Unity几乎是唯一的选择。它的核心优势在于无与伦比的视觉表现力和成熟的资源工作流。典型应用场景需要高质量视觉输入的视觉导航、人机交互研究、复杂场景下的任务仿真如家庭服务机器人、以及任何对场景美观度有要求的演示或应用。从兴奋到崩溃的“坑”与爬坑指南物理引擎的“游戏性”Unity默认的PhysX引擎为了游戏流畅性做了很多优化和简化其物理精度可能不如MuJoCo或Bullet。对于精细的机器人控制可能需要调整大量参数或使用第三方插件如来自NVIDIA的更多PhysX高级功能。单位制与尺度混乱Unity中1个单位通常代表1米但导入的模型可能是在不同单位制如厘米下创建的。URDF模型导入Unity是一个经典难题关节轴、质量、惯性矩的转换很容易出错。解决方案使用成熟的转换工具或插件如URDF Importer for Unity并务必在导入后仔细检查模型的动力学行为。与机器学习工作流的集成Unity本身不是为ML设计的。你需要通过Unity ML-Agents Toolkit或自建的TCP/UDP通信层将Unity环境包装成类似Gym的接口。这个过程会引入额外的复杂性和延迟。// 一个简化的ML-Agents Agent脚本片段用于发送动作和接收观察 public class RobotAgent : Agent { public override void OnActionReceived(float[] vectorAction) { // 将动作如关节目标速度应用到机器人模型上 ApplyJointVelocities(vectorAction); } public override void CollectObservations(VectorSensor sensor) { // 收集观察值如关节角度、相机图像等放入sensor sensor.AddObservation(GetJointPositions()); sensor.AddObservation(GetCameraPixels()); } }性能开销精美的画面背后是巨大的计算开销。即使使用无头模式渲染管线的开销依然存在。你需要精心优化场景减少多边形数量、使用LOD多层次细节、烘焙光照、谨慎使用实时阴影和反射。实战心得Unity项目很容易变得“好看但难用”。我的建议是从一开始就为性能而设计。使用简单的材质限制动态光源并尽早建立与Python训练循环的稳定通信。记住你是在构建一个科学实验平台而不是一款3A游戏。Unity的强大在于其可扩展性你可以从简单的方块世界开始逐步增加视觉复杂度而不是一开始就陷入资产制作的汪洋大海。2.3 MuJoCo追求物理极致的“精密仪器”MuJoCo以其准确、稳定的物理模拟在机器人控制领域建立了声誉。它的模型描述文件MJCF非常灵活可以定义复杂的肌腱、流体等模型。典型应用场景生物力学研究、需要高精度接触和力控的机器人操作、以及众多经典的强化学习基准环境如DeepMind Control Suite。高昂代价与精细控制商业许可这是最大的门槛。MuJoCo在2021年被DeepMind开源但在此之前其高昂的授权费用让许多个人研究者望而却步。开源后情况大大改善但一些历史遗留的第三方模型或代码可能仍依赖旧版本。MJCF学习曲线MJCF是一种基于XML的建模语言功能强大但语法需要时间熟悉。定义复杂的机器人尤其是带有闭链机构或特殊约束的机器人比URDF更灵活但也更复杂。!-- 一个简单的MJCF关节定义示例 -- joint nameknee typehinge axis0 1 0 limit range-160 0 / stiffness100/stiffness !-- 刚度 -- damping10/damping !-- 阻尼 -- /joint渲染集成MuJoCo自带的渲染器mjv速度很快但视觉效果相对基础。如果需要更高质量的渲染需要集成其他渲染引擎如Unity这增加了复杂性。实战心得如果你研究的问题对物理精度有硬性要求并且预算或开源条件允许MuJoCo是值得投资的。它的模拟结果通常更可靠论文中的实验也更容易被复现。在开始正式项目前花几天时间彻底学习MJCF语法是绝对必要的。2.4 新兴力量与云端方案NVIDIA Isaac Sim与云端仿真近年来NVIDIA Isaac Sim凭借其GPU加速和对ROS2的原生支持成为了工业界和学术界的新宠。它基于Omniverse平台旨在提供端到端的机器人仿真到部署工作流。它的优势非常鲜明性能利用RTX GPU进行硬件加速的物理和渲染支持大规模并行仿真。保真度集成了高保真渲染Path Tracing和精确的传感器模型如激光雷达、超声波。工作流与ROS2无缝连接方便将仿真中的算法部署到真实的NVIDIA Jetson等边缘设备上。然而它的“坑”在于硬件依赖需要强大的NVIDIA GPU软件生态也紧密绑定NVIDIA。复杂度Omniverse平台本身是一个庞大的生态系统学习曲线不低。资源消耗运行一个高保真场景对硬件要求极高。此外云端仿真平台如AWS RoboMaker, Google Cloud Robotics也开始提供托管仿真服务。它们解决了本地硬件资源不足的问题特别适合需要超大规模并行训练的场景。但你需要考虑数据上传下载的延迟、持续使用的成本以及对云服务商的依赖。3. 构建你的第一个仿真环境一个模块化的设计框架无论选择哪个工具一个清晰、模块化的环境设计框架都能让你事半功倍也便于后续维护和扩展。下面我分享一个基于策略-环境分离思想的设计模式我用它在多个项目中都获得了不错的效果。3.1 核心架构仿真的“三层楼”我将一个仿真环境抽象为三个层次物理/渲染后端层这是地基直接与PyBullet、Unity等引擎交互。负责创建场景、加载资产、执行物理步进、渲染图像。这一层应该被封装起来向上提供统一的、引擎无关的接口。环境抽象层这是主体建筑。它基于后端层提供的原始功能构建出具体的任务环境。它定义观察空间、动作空间、奖励函数、重置条件和终止条件。这一层遵循Gymnasium等标准接口使得你的环境可以轻松被各种RL算法库使用。任务与场景管理层这是内部的装修和隔断。它管理不同的任务变体如不同的目标位置、不同的障碍物布局和场景配置。通过参数化设计你可以从一个基础环境快速衍生出多个训练或测试环境。# 一个简化的框架示例 (以PyBullet后端为例) # 文件sim_backend.py class PhysicsBackend: def __init__(self, guiFalse): self.client p.connect(p.GUI if gui else p.DIRECT) p.setGravity(0, 0, -9.8) p.setTimeStep(self.sim_timestep) def load_urdf(self, path, position): return p.loadURDF(path, basePositionposition) def step(self): p.stepSimulation() def get_camera_image(self, camera_params): # ... 返回RGB深度等 # ... 其他底层物理操作 # 文件base_env.py import gymnasium as gym class BaseRobotEnv(gym.Env): def __init__(self, backendpybullet, task_configNone): if backend pybullet: from sim_backend import PhysicsBackend self.sim PhysicsBackend(guiFalse) # ... 其他后端初始化 self.task_manager TaskManager(task_config) # 任务管理层 self._setup_scene() self.observation_space self._define_observation_space() self.action_space self._define_action_space() def reset(self, seedNone): # 重置物理世界、机器人状态、任务目标 self.sim.reset() self.robot.reset_pose() self.task_manager.reset_task() return self._get_obs(), {} def step(self, action): # 1. 应用动作到机器人 self._apply_action(action) # 2. 推进物理仿真若干步控制频率 vs 仿真频率 for _ in range(self.control_steps_per_env_step): self.sim.step() # 3. 获取新观察 obs self._get_obs() # 4. 计算奖励和终止信号 reward self.task_manager.compute_reward(obs) terminated self.task_manager.is_terminal(obs) truncated False # 或根据步数判断 info {} return obs, reward, terminated, truncated, info def _setup_scene(self): # 加载地板、墙壁、机器人、目标物体等 self.ground_id self.sim.load_urdf(plane.urdf, [0,0,0]) self.robot RobotModel(self.sim, panda.urdf, [0,0,0.5]) self.object self.sim.load_urdf(cube.urdf, [0.5, 0, 0.1]) # ... 其他必要方法3.2 关键模块实现细节观察空间构建除了机器人本体状态关节位置、速度视觉观察的处理至关重要。考虑使用多模态观察例如将RGB图像和深度图分别通过不同的CNN编码再与本体状态向量拼接。在仿真中你可以轻易获取完美的深度图和分割掩码这是相对于真实世界的巨大优势。奖励函数设计这是引导智能体学习的“指挥棒”。避免设计过于稀疏的奖励。对于抓取任务可以结合距离奖励末端与目标距离、接触奖励是否抓握、成功奖励是否到达目标位置和惩罚项关节极限、能量消耗。使用gymnasium.Wrapper可以方便地组合和修改奖励函数。域随机化这是缩小“模拟-真实”鸿沟的核心技术。在你的环境重置函数中随机化以下参数视觉外观物体纹理、颜色、光照强度和方向、相机噪声。物理参数物体质量、摩擦系数、关节阻尼、执行器延迟。场景布局障碍物位置、目标初始位置。def _randomize_domain(self): # 随机化物体外观 new_color np.random.uniform(0, 1, 3).tolist() [1] p.changeVisualShape(self.object_id, -1, rgbaColornew_color) # 随机化物理参数 new_mass np.random.uniform(0.5, 1.5) p.changeDynamics(self.object_id, -1, massnew_mass) new_friction np.random.uniform(0.3, 0.8) p.changeDynamics(self.object_id, -1, lateralFrictionnew_friction) # 随机化目标位置 new_target_pos np.random.uniform([-0.2, -0.2, 0.1], [0.2, 0.2, 0.3]) self.task_manager.set_target(new_target_pos)动作空间与控制器仿真中的动作通常是关节目标位置、速度或力矩。在底层你需要一个控制器来将高层的动作命令转化为电机信号。常用的有PD控制器、逆动力学控制器等。确保你的控制器在仿真中稳定并且其行为与真实机器人上的控制器尽可能相似。4. 性能优化与调试让仿真飞起来当你的环境搭建完毕开始大规模训练时性能瓶颈和诡异的Bug就会接踵而至。以下是一些关键的优化和调试策略。4.1 性能优化技巧仿真步长与渲染频率解耦物理模拟需要小步长如0.002秒以保证稳定但渲染图像不需要那么高频。以每秒50帧20毫秒渲染一次为例每渲染一帧可以执行10次物理步进0.002 * 10 0.02秒。无头模式与离线渲染训练时务必使用无头模式如PyBullet的DIRECT连接Unity的-batchmode。只有在调试和评估时才开启GUI。对于需要保存视频的情况可以定期开启渲染并截图而不是每一帧都渲染。简化碰撞几何视觉上精细的网格模型可能有数万个三角形用于碰撞检测会极其缓慢。为其创建一个简化的碰撞网格Collision Mesh比如用多个基本几何体盒子、球体、胶囊组合近似或者使用凸包分解。# 在PyBullet中加载URDF时可以使用简化的碰撞形状 robot_id p.loadURDF( complex_robot.urdf, useFixedBaseTrue, flagsp.URDF_USE_SIMPLE_COLLISION_FOR_NON_FIXED_LINKS # 使用简化碰撞 )并行仿真这是加速RL训练的终极武器。利用ray、multiprocessing或仿真器自带的并行接口如Isaac Sim同时运行数百个环境实例。# 使用ray并行化环境的简单示意 import ray ray.init() ray.remote class EnvWorker: def __init__(self): self.env BaseRobotEnv() def step(self, action): return self.env.step(action) def reset(self): return self.env.reset() # 创建多个环境工作者 workers [EnvWorker.remote() for _ in range(16)] # 并行收集数据 actions ... # 从策略网络得到 results ray.get([w.step.remote(a) for w, a in zip(workers, actions)])4.2 系统性调试方法仿真中的Bug往往比真实世界更隐蔽。建立一套调试流程至关重要。可视化调试充分利用仿真器的调试绘图功能。在PyBullet中可以绘制坐标系、施加的力、接触点等。# 在机器人末端绘制坐标系 p.addUserDebugLine(lineFromXYZ, lineToXYZ, lineColorRGB[1,0,0], parentObjectUniqueIdrobot_id, parentLinkIndexend_effector_link_index) # 绘制接触点 contacts p.getContactPoints(bodyA, bodyB) for contact in contacts: pos contact[5] # 接触点位置 p.addUserDebugPoints([pos], [[1,0,0]], pointSize10)“手动模式”测试在编写自动控制逻辑前先实现一个手动控制接口如用键盘控制关节。手动操作机器人完成一次任务检查物理交互、碰撞、传感器读数是否正常。这能帮你快速定位是环境问题还是控制算法问题。录制与回放实现环境状态的保存和加载功能。当出现一个罕见Bug时保存出错前的状态然后可以反复回放、单步调试定位问题根源。单元测试为你的奖励函数、终止条件、观察计算等编写单元测试。用一些预设的、边界的状态来验证这些函数是否按预期工作。搭建具身智能仿真环境是一个不断权衡和迭代的过程。没有一劳永逸的选择只有最适合当前项目阶段和目标的工具。我的习惯是在项目初期用PyBullet快速验证想法的可行性当需要更复杂的视觉或场景时转向Unity而当物理精度成为瓶颈时则会认真考虑MuJoCo或Isaac Sim。最重要的是保持环境的模块化和可替换性这样当需求变化时你才能从容地切换底层引擎而不是推倒重来。最后多看看开源社区别人是怎么做的比如DeepMind的dm_control、Facebook的habitat-sim它们的架构设计往往能给你带来很多启发。仿真世界是具身智能的沙盒在这里试错的成本远低于现实。尽情实验但也要时刻记住仿真的终极目标是让智能体在现实世界中同样游刃有余。