1. 先把这场比赛在考什么搞明白1.1 拆开标题里的三个词我报名这次暑假赛之前先花了一个晚上把标题里的三个词拆开看开悟、AIArena、暑假赛。这三个词各自代表一种约束理解错了努力方向就会偏。开悟是平台属性意味着它带着明显的教学和引导色彩——官方一般会给任务说明、基线代码、数据说明文档甚至给一些预备知识清单。这一点跟纯粹的工业比赛不一样工业比赛常常只给你一堆数据和一句预测目标值剩下全靠自己猜。有引导的好处是上手快坏处是很多人会直接复制官方基线然后原地不动最后卡在一个不上不下的分数上。AIArena是竞技属性核心就是排行榜。排行榜意味着你的模型要在一个你看不见的测试集上跑出分数也意味着你没法靠我本地验证集99%来自我安慰。这个属性直接决定了后面所有的技术选择必须防过拟合必须做稳定的验证切分必须留出提交次数。暑假赛是时间属性。周期短、集中、连续通常两三周到一个多月。这个约束最容易被新手忽略但它其实决定了你的策略应该是快速迭代多个可复现的实验而不是死磕一个模型调到底。把这三层想明白之后我给自己定了一个原则先保证有一个能稳定提交的基线再谈提分。这个原则在后面救了我至少三次。1.2 我的三条线打法基线线、提分线、认知线参加这类比赛最容易出现的问题是东一榔头西一棒子——今天看到别人用卷积网络提了分赶紧换明天看到有人用数据增强又提了分又赶紧加。结果是实验记录一团乱最后连哪个改动有效都说不清。我给自己划了三条并行的线。基线线的任务只有一个跑通。数据能读进来、模型能训起来、结果能提交上去、榜单上有个分数。哪怕这个分数很低只要流程是通的基线线的任务就算完成。我在这次比赛里用的基线是一个两层全连接网络验证集准确率比瞎猜好不了多少但它让我在开赛第二天就完成了第一次提交。提分线的任务是找性价比最高的那几刀。注意是性价比最高不是效果最好。有些改动能提0.5个点但要重训八小时有些改动能提0.3个点只要改三行代码后者明显更值得先做。我给自己定了个粗略的优先级数据层面的改动 训练策略层面的改动 模型结构层面的改动 集成层面的改动。认知线最容易被忽略但对我个人价值最大。我每天收工前会花十分钟写一句复盘今天试了什么、结果如何、为什么。这个习惯看着很土但在比赛后半程当我需要对十几个实验做取舍的时候这份记录就是唯一可靠的东西。1.3 四十天的时间节奏怎么排时间分配上我踩过坑。第一次参赛的时候我前十天全在学习理论等到要写代码了发现手生又花五天重新捡起来。这次我换了个更实用的排法把学习和动手交替起来。阶段大致天数主要目标交付物热身期第1-5天环境跑通、看完任务说明、跑通官方基线一次成功提交基线期第6-12天搭自己的训练脚本固定验证集可复现的训练代码提分期第13-28天数据增强、结构调整、调参实验记录表冲刺期第29-38天模型融合、TTA、结果稳定化最终提交方案复盘期第39-40天整理代码与笔记可复用的项目模板这张表真正有用的地方不是天数而是每个阶段都必须有明确交付物。没有交付物的阶段时间一定会被浪费掉。我个人最看重的是基线期因为一个可复现的训练脚本价值远大于一次偶然的高分。2. 赛前准备环境、数学、资料三件事2.1 环境配置的核心是先算清楚显存深度学习环境这件事新手最容易陷入的误区是先把框架装上再说。我的建议是先反推手头有多少算力再决定用什么框架、什么模型规模。显存估算有个粗略但好用的公式。以训练为例显存占用大致等于参数量 × 4字节权重 参数量 × 4字节梯度 参数量 × 8字节Adam的两个动量 激活值也就是说用Adam训练一个1000万参数的小模型光优化器相关就要占掉大约160MB加上激活值往往要到几百MB。这只是保守下限实际能不能跑起来还要看batch size和输入分辨率。我一般会先把batch size设成8或者16试一次跑得动再往上加。环境创建我习惯用conda隔离得干净出问题直接删掉重建conda create -n kaiwu python3.10 -y conda activate kaiwu pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install numpy pandas matplotlib scikit-learn tqdm tensorboard这里有两个细节值得说。第一Python版本不要追最新3.10左右是最稳的区间太新的版本经常遇到某个依赖还没出对应轮子编译到一半报错。第二框架和驱动版本要对应我遇到过驱动版本偏低导致装完框架torch.cuda.is_available()返回False的情况查了两个小时才发现问题在驱动。装完之后一定要写三行验证代码import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU only)这三行的意义在于在写任何模型代码之前先把是环境问题还是代码问题这个变量排除掉。后面调参调到怀疑人生的时候你会感谢自己做了这一步。至于用本地机器还是用平台提供的在线环境我的判断标准很简单如果数据集规模在几个GB以内、模型参数量在千万级以下本地跑就够了如果数据量很大、或者本地显卡显存不足就直接用平台环境别在环境上耗时间。比赛期间时间是最贵的资源。2.2 数学部分只需要一个最小必要集很多人一提到深度学习的数学就打退堂鼓其实打比赛用到的数学远比想象中少。我自己总结的最小必要集只有三块泛化误差、反向传播、偏差方差。先说自己感受最深的泛化误差。训练集上的误差叫经验误差测试集上的误差叫泛化误差两者之间的差距叫泛化间隙。比赛里所有的技术动作本质上都是在做同一件事让泛化间隙变小。数据增强是让模型见过更多变化、正则化是限制模型复杂度、早停是防止模型在训练集上跑太远。想明白这一层就不会再盲目堆技巧了。反向传播我一开始是被推导过程吓住的后来发现抓住一句话就够了它是链式法则的高效实现。前向传播算出预测值和损失反向就是把损失对每一层参数的偏导从后往前一层层乘回来。真正需要理解的不是公式本身而是它的两个推论一是靠近输入的层梯度容易变小这就是深层网络难训的根源二是学习率太大时梯度更新会震荡因为乘出来的梯度链被放大了。偏差方差这一对概念解释了很多现象。模型太小、容量不足偏差高表现是训练集和验证集都差这叫欠拟合模型太大、数据太少方差高表现是训练集很好但验证集差这叫过拟合。这两个诊断结论直接决定你下一步该加参数还是该加正则判断错了会浪费好几天。我在热身期就遇到过这个问题验证集准确率一直卡在六成左右我第一反应是模型不够强加了一层又一层结果毫无起色。后来把训练集准确率打出来一看训练集也只有六成多——这明显是欠拟合该做的是把学习率调大、把训练轮数加够而不是加层数。2.3 资料清单和它们的正确打开方式资料这块我的态度是不要贪多但要知道每份资料是干什么用的。吴恩达的课程适合打地基讲得慢但清楚尤其是关于正则化、优化算法、诊断模型的几节属于看一遍就能用上的内容。缺点是作业偏简单光看完课不动手写代码时还是会卡。《深度学习入门基于Python的理论与实现》这类书价值在于从零实现一遍前向和反向。很多人觉得有框架了为什么还要手写我的实际体会是手写一遍之后你对张量形状不对这类报错的敏感度会提升一个档次因为你知道每一步的形状是怎么来的。邱锡鹏那本《神经网络与深度学习》更偏理论体系适合在遇到为什么这么做的问题时去查不适合从头到尾啃。动手深度学习那本更偏工程实践代码多适合边看边敲。至于MATLAB那边的神经网络工具箱我个人的用法是拿它做快速验证。工具箱里拖几个模块就能搭出一个前馈网络用来验证一个想法值不值得投入时间比写Python快得多。真要打比赛长期还是得回到代码里因为可定制性差太多了。工业视觉方向还有专门的深度学习工具比如Halcon的深度学习模块它的优势是训练和部署一体化做缺陷检测这类落地项目非常省事。但比赛阶段用它意义不大因为比赛的评测接口是固定的。我把它列进学习清单是为将来做工程项目留的后路。提示资料看得再多也不如把一个数据集从头到尾跑三遍。每跑一遍你会注意到之前完全没留意的问题。3. 网络结构选型全连接、卷积、循环到底怎么用3.1 前馈网络和BP最容易被跳过的那个基线前馈神经网络也就是常说的多层感知机结构简单到一句话能说完输入层、若干隐藏层、输出层层与层之间全连接。它的训练靠的就是反向传播加梯度下降。我建议每个人都用MLP先跑一个基线理由有两层。第一层是工程上的MLP的代码最短出问题的概率最低跑通它意味着数据管道、训练循环、验证逻辑、提交格式都没问题了。第二层是认知上的MLP的参数量你算得清楚因此你知道模型容量到底有多大。举个具体例子。输入是28×28的灰度图展平后是784维接一个256维隐藏层再输出10类第一层参数784 × 256 256 200,960第二层参数256 × 10 10 2,570合计约20.4万一个20万参数的小网络在几万条数据上的表现基本就是能跑但不惊艳。这个认知很重要因为它给了你一个参照系后面换成卷积网络后参数量降到了几万效果反而更好你就能直观理解参数量不等于能力。用MLP有几个实操注意点输入一定要归一化。像素值从0-255压到0-1损失曲线会平顺很多不归一化很容易出现第一轮损失就炸掉的情况。权重初始化别用全零。全零会导致同一层的神经元梯度完全相同网络退化成只有几个有效单元。用默认的Kaiming或者Xavier初始化就行。随机种子要固定。同一份代码两次跑出不同结果排查起来非常痛苦。3.2 图像任务里为什么卷积网络能压住全连接图像处理为什么用CNN不用前馈网络这个问题我一开始的答案很敷衍——书上都说CNN好。后来自己算了一遍参数量才算真的理解。还是28×28的输入。如果用全连接第一层每个神经元都要连784个输入256个神经元就是20万个参数。而一个3×3的卷积核即使输出32个通道参数量也只有3 × 3 × 1 × 32 32 320个。差了三个数量级。但参数量少只是表象真正的差别在归纳偏置。图像有两个天然性质局部相关和平移不变。相邻像素关系密切一个特征出现在左上角还是右下角本质是同一个特征。卷积通过局部连接和权重共享把这两条先验直接写进了结构里。全连接网络没有这个先验它必须从数据里学出左上角的角和右下角的角是同一回事这需要大量样本。我用一张表概括一下两者的差别维度前馈网络MLP卷积网络CNN参数规模随输入尺寸平方增长与输入尺寸基本无关空间结构展平后丢失邻接关系保留二维邻接关系平移不变性需要从数据中学结构自带小数据集表现容易过拟合相对更稳适合的场景表格数据、低维特征图像、语音谱、时序信号卷积网络的经典入门结构是LeNet-5这类小网络两个卷积层加池化再接全连接分类头。它的层数很浅但在手写数字这种任务上表现相当好。我建议第一次搭CNN就从这种结构开始别一上来就堆ResNet因为你还没建立每加一层会发生什么的直觉。搭CNN时我踩过的坑集中在形状上卷积分辨率和通道数要一起看。经过几次下采样后特征图会变得很小这时候再加池化可能直接就变成1×1了。全连接层之前的展平维度一定要算准。我最常犯的错就是这里解决办法是在展平前打印一次形状。批归一化的位置。一般放在卷积之后、激活之前用起来收敛会明显更快。顺便说一下ViT和EfficientNetV2这类相对新的结构。ViT把图像切成patch当作序列处理在数据量大的时候表现优异但数据量小的时候往往不如CNN因为它缺少卷积的那套先验需要更多数据来补。EfficientNetV2则在精度和速度之间做了比较好的平衡做图像分类时可以直接拿来当骨干网。我的建议是比赛初期先用自己搭的小CNN建立基线中期再考虑换骨干网因为换骨干会引入一堆新的超参需要重新调。3.3 序列任务里的循环网络和它的替代选项如果任务涉及序列——文本、时序信号、音频帧——那单靠卷积就不太够了需要能记住上文的结构。循环神经网络的核心思想是维护一个隐藏状态每一步都把它和当前输入一起送进网络更新后再传给下一步。它在结构上天然适合变长序列。但基础的RNN有个硬伤梯度消失。序列一长反向传播时梯度连乘很多次很快就趋近于零模型学不到长距离依赖。LSTM通过门控机制缓解了这个问题遗忘门决定丢掉多少旧信息输入门决定写入多少新信息输出门决定暴露多少给下一步。这套机制让LSTM在长序列上稳定很多代价是参数量翻了几倍训练也更慢。现在的实际选择通常是这样序列不长、追求速度一维卷积就能做得不错把序列当一维图像处理。序列较长、需要长距离依赖LSTM或者GRU。数据量大、有充足算力注意力机制那一类结构效果通常更好但对数据和算力要求更高。还有一个方向值得一提就是把图结构引进来。如果数据本身带有关系结构比如节点之间的连接关系图神经网络会比单纯堆卷积或循环层更合适。我这次比赛没用到但在看别人的方案时注意到处理关系型数据的队伍里用图结构的不少。这条线可以作为暑假之后的延伸方向慢慢啃。3.4 选型拿不准时看这张表实战中我基本是靠这张表做第一轮决策的能省掉大量犹豫时间任务形态首选结构备选主要风险表格/低维特征MLP梯度提升树特征未归一化导致不收敛二维图像分类小CNN预训练骨干网输入尺寸与下采样不匹配长序列建模LSTM/GRU一维卷积序列过长导致训练慢关系型数据图结构网络手工特征MLP图构造方式不对白做数据极少预训练微调强增强小模型直接训练必然过拟合这张表的用法是先定方向再调细节。方向错了调参调到天亮也没用。4. 一次完整实操从读数据到提交榜单4.1 数据检查先看再动手我现在养成了一个习惯拿到数据的前半小时不写任何模型代码只看数据。这一步看起来浪费时间实际上是最省时间的一步。要看的东西包括类别分布是否均衡、图像尺寸是否统一、有没有损坏文件、标签有没有明显错误。我这次就发现训练集里有几十张图尺寸和其他样本不一样如果直接送进网络会因为维度不匹配直接报错排查起来又是半小时。提前发现统一resize就解决了。数据划分上我的做法是先用固定随机种子切出验证集一般取训练数据的15%到20%。如果类别不均衡就分层抽样保证验证集的类别分布和训练集一致。这个细节很重要因为验证集分布偏了你会得到错误的反馈信号据此做的所有调参决策都是错的。import numpy as np from sklearn.model_selection import train_test_split SEED 42 X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.15, random_stateSEED, stratifyy )4.2 一个可以反复复用的训练脚本骨架比赛期间我的经验是训练脚本要写得笨一点但必须稳定。花哨的写法在调参阶段会变成负担。下面这个骨架我用了整个暑假改动很少。import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from tqdm import tqdm def set_seed(seed42): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0.0, 0, 0 for x, y in tqdm(loader, leaveFalse): x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() total_loss loss.item() * x.size(0) correct (out.argmax(1) y).sum().item() total x.size(0) return total_loss / total, correct / total torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total 0.0, 0, 0 for x, y in loader: x, y x.to(device), y.to(device) out model(x) loss criterion(out, y) total_loss loss.item() * x.size(0) correct (out.argmax(1) y).sum().item() total x.size(0) return total_loss / total, correct / total有了这两个函数主循环就只剩十几行每轮训练、每轮验证、记录最好的那一版权重。关键点在于保存验证集最优权重而不是保存最后一轮的。训练轮数给多了以后模型一定会在某个点之后开始过拟合保存最后一轮等于主动放弃了前面最好的状态。我这次比赛里验证集最优出现在第23轮而我一共训了40轮如果不做这个保存分数会低一截。4.3 超参数的取值逻辑不是玄学超参调参我以前是靠感觉后来发现其实有比较清晰的逻辑链条。学习率是最关键的一个。它太大损失会震荡甚至发散太小收敛慢到让人绝望。我的做法是先做一次粗略的扫描取值在1e-1到1e-4之间跨数量级试几个看哪一段损失下降最快再在这个区间内细化。经验值是配合Adam1e-3是个很稳的起点用SGD的话1e-2左右更常见。批大小跟学习率是联动的。批大小翻倍学习率大致也可以翻倍这个缩放关系在多数任务上成立。批大小大的好处是训练稳定、吞吐高坏处是泛化性有时会略差。小批大小噪声大一些反而常常能带来更好的泛化。训练轮数不要靠猜通过观察验证集曲线来定。我一般会先设一个偏大的值比如50轮然后看验证集在第几轮开始不再提升。参数常见起点表现不对时的信号调整方向学习率1e-3Adam损失震荡或长时间不降先降一个数量级批大小32/64显存不足或梯度噪声太大结合学习率一起调权重衰减1e-4训练准确率远高于验证准确率适当加大训练轮数30-50验证损失开始回升加早停数据增强强度中等训练准确率上不去减弱增强早停这个技巧值得单独说。它的实现很简单记录验证集最优分数如果连续N轮没有提升就停掉训练。它同时解决了两个问题——省时间和防过拟合。我在比赛后期把早停的容忍轮数设成7轮效果比较稳。4.4 提分手段的性价比排序比赛到中期你会发现可选的提分手段一大堆这时候排序就变得非常重要。我的排序大致是这样从最推荐到最不推荐第一档数据增强和验证集质量。这两件事几乎零成本收益却很直接。图像任务里随机裁剪、水平翻转、轻微色彩抖动就能带来一两个点的提升。验证集切分做得干净你的所有后续判断才有依据。第二档训练策略优化。学习率调度比如余弦退火、warmup、标签平滑这些改动通常只要几行代码。第三档换更强的模型结构。比如从小CNN换到预训练骨干网。收益可能不小但代价是重新调参而且推理时间会变长。第四档测试时增强和模型融合。也就是TTA和多模型投票。这类手段能提分但成本高——TTA会让推理时间翻几倍多模型融合需要训好几个模型训练时间成本翻倍。第五档伪标签、自训练这类半监督手段。这类方法有一定门槛做不好反而掉分建议等前面的都做扎实了再碰。注意永远不要同时改两个变量。一次只改一个跑完看结果记录再动下一步。同时改两个你永远不知道是哪个起了作用。这是我在比赛早期浪费最多时间的地方。4.5 提交记录表和复盘排行榜上的分数是最诚实的反馈但如果不做记录你很快就会忘记第7次提交那个0.83是怎么来的。我用的表格很简单但收益极大序号改动内容验证集分数榜单分数结论1基线MLP0.710.70跑通流程2换小CNN0.850.84结构有效3加随机裁剪0.870.86增强有效4学习率改1e-30.880.87小幅提升5换骨干网0.900.89明显提升6加TTA0.910.91提升有限但值得这张表最重要的作用是防止重复劳动。人很容易在焦虑的时候反复试已经试过的东西尤其是隔了几天之后。5. 踩坑实录和排查速查表5.1 损失不下降按这个顺序查这是我整个暑假遇到最多的问题后来我固定了一套排查顺序基本能在半小时内定位。先看数据。把一批样本可视化出来确认图像内容和标签是对应的。我就遇到过标签整体错位一位的情况这个错很隐蔽因为模型依然能训出一个看似合理的数字只是分数上不去。确认输入归一化。灰度图像除以255、彩色图像按通道做标准化。归一化没做的时候损失经常在第一轮就变成nan。把学习率降一个数量级。如果损失从稳定变成下降说明学习率太大。检查标签编码。分类任务里类别标签必须是0到N-1的整数损失函数要匹配。用交叉熵却传了one-hot标签会得到一个很小但几乎不变的损失值。用一个极小的子集过拟合。拿20条数据训100轮如果模型不能在训练集上接近100%准确率那说明代码有结构性问题跟超参无关。这招非常好用能在几分钟内区分模型有问题和数据太难。检查是否忘了optimizer.zero_grad()。忘了清零梯度会导致梯度累加训练直接崩掉。检查验证逻辑和训练逻辑是否一致。忘记model.eval()会让批归一化和dropout在验证时行为错误验证分数会莫名其妙地低。5.2 过拟合和欠拟合看两条曲线就够了这个问题我用一张对比讲清楚。训练集和验证集两条损失曲线是诊断模型状态最直接的工具。训练损失下降、验证损失也下降且两者差距小——健康状态继续训。 训练损失一直降、验证损失先降后升——典型过拟合。处理方式加数据增强、加权重衰减、加dropout、减小模型、加早停。 训练损失和验证损失都很高、都降得很慢——欠拟合。处理方式加大模型容量、提高学习率、延长训练、减少正则强度。我这次比赛中期就卡在第二种状态上。验证集分数到了一个点就不动了训练集却还在往上走。当时的应对是加了两项增强随机旋转和随机擦除验证集分数又上去了一点。这个经验说明过拟合不是模型的问题是数据多样性的问题从数据侧解决往往比从模型侧解决更有效。5.3 环境和显存上的坑都是可以提前规避的**显存不足OOM**是高频问题。我的排查顺序是先看是不是batch size太大减半试试再看是不是验证阶段忘了用torch.no_grad()验证时也在建计算图显存会涨得很快最后看是不是数据加载器积压把num_workers从0开始试。训练速度慢有时候不是显卡的问题而是数据加载成了瓶颈。判断方法很简单看GPU利用率如果长期低于50%瓶颈大概率在数据侧。解决办法是把数据预处理提前做一次存成numpy数组或者缓存文件训练时直接读。随机种子不固定导致结果不可复现这个坑的代价是巨大的。我现在的做法是在脚本入口处就把torch、numpy、random三个种子全设一遍并且在训练日志里把种子值打出来。版本不匹配造成的报错往往很迷惑。我遇到过某个依赖版本升级之后加载模型权重报键名不匹配的情况。现在我的习惯是环境跑通之后马上导出依赖清单后面所有实验都在同一个环境里跑。pip freeze requirements_lock.txt这份锁定的清单在换机器或者重装环境的时候能省下大量时间。5.4 报错速查表现象大概率原因处理方式损失变成nan学习率过大 / 未归一化 / 除零降学习率检查数据范围维度不匹配报错卷积后展平维度算错展平前打印形状验证分数异常低未调用eval模式验证前加model.eval()GPU用不上驱动或框架版本不匹配重建环境跑三行验证代码训练速度波动大数据加载慢预处理缓存调整num_workers多次运行结果不同随机种子未固定统一设置torch/numpy/random种子提交格式报错列名或行数不符对照样例文件逐列核对这张表里的每一条我都至少踩过一次。尤其是最后一条提交格式错误在比赛最后阶段是致命的因为提交次数往往有限。我的做法是第一次提交前先写一个校验脚本检查行数、列名、取值范围是否与样例一致。这个脚本写一次后面每次提交前跑一遍能避免绝大部分低级失误。5.5 比赛节奏和心态上的两个坑第一个坑是过早追求高分。开赛第一周就想冲榜结果基础没打牢后面反复返工。我现在更认同的顺序是流程正确优先于分数分数稳定优先于分数最高。第二个坑是最后阶段大改方案。我在比赛倒数第二天动过一次模型结构结果验证集分数上去了提交之后榜单分数反而掉了。原因很可能是验证集和测试集分布有差异改动引入了对验证集的过拟合。这个教训让我定了一条规矩最后三天只做能大幅提升稳定性的改动比如固定种子多次运行取平均不做结构性的探索。如果提交次数有限我的分配策略是前期用掉三分之一做探索中期用掉三分之一做验证最后留三分之一做保守提交。很多队伍最后发现没有提交次数了只能眼睁睁看着排名掉下去。最后再分享一个小技巧。整个比赛期间我把所有实验目录都按日期改动分数命名比如0820_cnn_crop_0887。听起来是很土的习惯但当你需要对十几个实验做取舍、或者想回头复现某个两周前的实验时这套命名法就是救命的。至于这个暑假真正学到了什么我的答案不是某一项技术而是一套把想法快速变成可验证实验的工作流先固定变量一次只改一个记录结果定期复盘。这套流程放在这次比赛里能提分放在任何需要试错的事情上也一样管用。顺便提一句这次比赛带出来的代码模板我做了整理打算拿它去跑几个自己感兴趣的小项目——复现一个数字识别的demo试试把序列模型迁移到传感器数据上看看换个数据集之后哪些超参需要重调。这些在比赛期间没时间做的事正好留到开学后慢慢折腾。