用贝叶斯优化算法优化CNN实现多特征输入分类模型
如果你做过几个深度学习分类项目,大概率会被同一个问题折磨过:模型结构看着挺合理,loss曲线也算收敛,但验证集准确率就是差那么两三个点。这时候大多数人的第一反应是调结构、加数据、换损失函数,但往往折腾了一圈发现,真正瓶颈可能只是几个超参数没选对——比如学习率大小、卷积核数量、dropout比例。特别是当输入不止是单一的图像或文本,而是混合了表格特征、文本特征甚至时序特征的“多特征输入”时,超参数的选择会直接决定不同特征分支的融合效果。这就是我这次想聊的核心内容:用贝叶斯优化算法去自动搜索CNN的最优超参数组合,从而把多特征分类模型的性能真正榨干。
这篇文章适合两类读者。一类是刚接触深度学习调参,正在网格搜索和随机搜索里绕圈子的初学者,另一类是已经在做多模态或多特征分类、但不清楚怎么系统性地让贝叶斯优化和CNN结合的老手。我会从最底层的思路讲起,然后给出可复现的实操步骤,最后分享一些我在实际项目中踩过并且至今印象深刻的坑。如果你只是想简单调一下参数,sklearn里的GridSearchCV就够用,但如果你想做的是多特征输入的CNN模型,并且希望每次调参都有理有据、不瞎碰运气,那贝叶斯优化值得你认真看一次。
1. 为什么要用贝叶斯优化,而不是网格搜索或随机搜索
1.1 多特征输入模型的超参数空间有多复杂
先摆一个现实问题。一个普通的单输入CNN分类模型,超参数可能包括学习率、批大小、卷积层数、每层卷积核数量、卷积核大小、池化方式、dropout率、激活函数、优化器选择、正则化系数。这已经是至少8到10个维度的搜索空间了。一旦把输入改成多特征——比如数值型特征走一个全连接分支、序列特征走一个LSTM或一维卷积分支、图像特征走一个二维卷积分支——每个分支都有自己的超参数,再加上融合层的参数(拼接维度、注意力权重、融合后的全连接结构等),你的搜索空间直接爆炸到二十个维度以上。
网格搜索在处理这种高维空间时有一个致命问题:计算量呈指数级增长。假设每个维度只取5个候选值,二十个维度就是5的20次方,穷举根本不可能。随机搜索虽然比网格搜索聪明一些,因为它不依赖离散网格,可以在连续空间里采样,但它完全忽略了“历史评估结果”的价值——每一次超参数组合的评估都是昂贵的(尤其是CNN训练),随机搜索等于把这些昂贵实验的信息全部丢弃了。
1.2 贝叶斯优化的核心直觉:用概率模型指导下一轮实验
贝叶斯优化的思路其实非常像一个经验丰富的老手在调参时的做法:先试几组参数,观察结果,然后基于“哪些区域效果可能更好”的直觉,把下一组实验放在最可能出好结果的地方。只不过贝叶斯优化把这个过程形式化了,用高斯过程(Gaussian Process)去拟合“超参数组合到模型性能”的映射关系,并且在每次迭代时计算一个采集函数(Acquisition Function),用“期望改进概率”或“置信上界”来决定下一组参数在哪里试验。
用生活化的类比来说:你在一座山的浓雾里寻找最高点,你每走一步能知道当前位置的海拔,但看不到远处。网格搜索就像在整座山密密麻麻地打桩,每个桩都测一次海拔,费时费力;随机搜索就是闭上眼睛乱跑然后测海拔;贝叶斯优化则是根据已经测过的点,用数学模型推测哪个方向最可能有更高的海拔,然后刻意往那个方向走采样,走几步再更新模型,再决定下一步的方向。
这种方式最大的价值在于样本效率。训练一个CNN的时间成本可能是十几分钟到几个小时不等,每一组实验都非常宝贵。贝叶斯优化通常能在几十组实验内找到接近最优的超参数配置,而随机搜索或网格搜索往往需要几百组甚至上千组,这在工程时间上完全是两个量级的问题。我自己在项目中最直观的感受是:随机搜索跑了一整天,最优结果还比不上贝叶斯优化跑一个下午的结果。
1.3 贝叶斯优化的两个关键选择:高斯过程与采集函数
贝叶斯优化有两个核心组件需要自己选:替代模型(surrogate model)和采集函数(acquisition function)。
替代模型最常用的是高斯过程。高斯过程的好处是不仅能给出预测值,还能给出预测的不确定性(方差),这正是贝叶斯优化能做“探索”和“利用”平衡的关键。当某个区域的方差很大时,说明我们对该区域了解很少,即使当前预测值不高,也可能存在未发现的“高峰”,值得去试;当某个区域的预测值很高且方差很小时,说明这是一个可靠的“局部高点”,应该围绕它微调。不过高斯过程在高维空间(超过20个维度)下会变得不那么稳定,这时可以考虑用随机森林或者TPE(Tree-structured Parzen Estimator)作为替代模型。TPE在深度学习的超参搜索中表现非常稳定,很多成熟的调参库默认用的就是TPE。
采集函数常见的有三种:EI(Expected Improvement,期望改进)、PI(Probability of Improvement,改进概率)、UCB(Upper Confidence Bound,置信上界)。实际项目里我最常用的是EI,因为它能在探索和利用之间给出一个比较自然的平衡——它希望找到“比当前最佳结果更好”的点,但同时也允许一定程度的未知区域探索。UCB带有一个人为设置的参数来控制探索程度,如果这个参数没调好,很容易变成纯随机搜索;PI则太贪心,容易陷入局部最优。所以我的建议是,不熟悉的时候直接选EI。
注意:贝叶斯优化并不是万能的。如果模型本身很弱(比如数据没有做预处理、网络结构不合理),贝叶斯优化也只能在一个烂空间里找到“相对不那么烂”的结果。它优化的是“给定网络结构和数据流,选哪组超参数最合适”,而不是替你设计网络结构。
2. 多特征输入CNN的架构设计与输入处理
2.1 什么场景需要多特征输入
多特征输入在实际工作中非常常见。举几个例子:
- 分类任务里既有结构化表格数据(年龄、收入、品类编码)又有文本描述:比如用户行为分类,用户的数值型画像是一类特征,用户留下的评论是一类特征,两种特征异构明显,不能直接拼在一起喂给同一个网络。
- 传感器信号分类:既有振动信号的波形(时序特征),又有设备编号、运行温度等标量变量。
- 推荐系统中的点击率预估:用户ID、物品ID离散特征,加上用户浏览时长、物品价格等连续特征,还可以加上描述文本的向量表示。
在这些场景里,如果只用一个两层的全连接网络把所有特征全部塞进去,效果通常很差。因为不同类型的特征有不同的分布特性和结构规律,统一处理会导致信息丢失。合理的做法是每种特征走自己独立的分支网络,最后在某个“融合层”汇合,再接一个分类头。
2.2 分支结构怎么设计
我一般把多特征输入分成三类分支来设计:
- 数值/表格特征分支:特征是固定维度的稠密向量,走全连接网络即可。通常两三层的隐藏层就够了,每层128到256个神经元,激活函数ReLU,正则化用Dropout。不是层数越多越好,表格特征本身信息量有限,太深的网络反而容易过拟合。
- 序列/文本特征分支:特征是变长的序列,可以走一维CNN或LSTM,也可以走Transformer编码器。一维CNN的优势是训练速度快、对局部时序模式敏感,LSTM则在长程依赖上更优势。实际项目里如果序列长度不长(几十到几百个时间步),一维CNN足够胜任,而且和二维CNN的代码风格比较统一。
- 图像/空间特征分支:特征是二维或三维的网格结构,走标准CNN,卷积加池化,最后通过全局平均池化成固定长度的特征向量。
三个分支各自输出一个特征向量,然后融合。融合方式是另一个关键超参数。
2.3 融合层怎么选:拼接、加权和注意力
最简单的融合方式是直接拼接(Concat)。假设表格分支输出128维向量,序列分支输出256维向量,图像分支输出512维向量,拼在一起就是896维,再接全连接层做分类。这种方式实现简单,但问题是三个分支的重要性是隐式的,模型需要自己学习怎么组合。
稍微进阶一点的是加权融合。每个分支的特征向量乘以一个可学习的权重标量,再加起来。这样做的问题是特征维度敏感,权重对高维特征的影响不稳定。
我推荐在条件允许时直接用注意力融合。具体做法是把三个分支的特征向量作为一组“键值对”,通过一个可学习的查询向量计算每个分支的重要性权重,然后加权求和。这个做法的解释性更强:训练结束后你能看到模型更依赖哪个输入分支。实现也不难,本质上就是一个softmax权重计算。
融合之后的特征再过一个两层全连接,输出分类概率。这个“分类头”的结构复杂度也是可以优化的超参数,但不用太多层,一两层足够。
2.4 数据预处理和特征对齐是生死线
多特征输入最坑人的地方在数据预处理阶段。很多人把数据一股脑传给模型,结果训练报错或者效果极差,排查了很久发现是特征对齐出了问题。
具体有三类问题需要特别留意:
- 缺失值处理:不同特征缺失率差异很大,数值特征可能需要中位数填充,序列特征可能需要用padding mask,图像特征可能直接用零填充。处理策略必须写清楚,否则训练和预测阶段行为不一致。
- 归一化方式:数值特征对尺度非常敏感,用Z-score标准化是基本操作。但如果数值特征里混入了偏态严重的分布(比如收入),可能需要对数变换再标准化。这个细节对分类准确率的影响经常被低估。
- 变长序列的对齐:如果序列特征是变长的,需要定一个最大长度,设置统一的padding,并且在使用RNN或CNN时正确处理mask。一维CNN在padding后需要注意卷积是否把padding区域也纳入了特征计算,必要时可以设置卷积层的mask属性。
3. 实操环节:贝叶斯优化和CNN联合训练的全流程
3.1 整体项目流程概览
写一段伪代码描述完整流程:
1. 数据准备与特征分支定义 2. 构建CNN模型框架,定义各分支网络结构 3. 定义超参数搜索空间(每个超参数取值的范围) 4. 定义目标函数(内部进行一次完整的CNN训练+验证,返回验证集指标) 5. 实例化贝叶斯优化器,指定n_iter迭代次数 6. for 每轮迭代: a. 优化器根据已有历史记录,推荐下一组超参数 b. 用这组超参数重新构建CNN,训练模型,得到验证集F1/准确率 c. 把超参数和结果记录到优化器历史 7. 迭代结束后,取出历史中效果最好的一组超参数 8. 用全部训练数据(含验证集)在该超参数下重新训练最终模型 9. 在测试集上评估并输出结果3.2 超参数搜索空间的定义
搜索空间的定义决定了最终结果的上限,太窄可能漏掉最优解,太宽则会浪费大量实验。
下面是我在一次多特征分类项目中实际用过的搜索空间:
| 超参数 | 类型 | 范围 | 说明 |
|---|---|---|---|
| 学习率 | 连续(对数均匀) | 1e-4 ~ 1e-2 | CNN训练中最重要的超参数 |
| 批大小 | 离散 | 16, 32, 64 | 受显存限制 |
| 一维CNN卷积核数量 | 离散 | 64, 128, 256 | 序列分支 |
| 一维CNN卷积核大小 | 离散 | 3, 5, 7 | 序列分支 |
| 二维CNN卷积核数量 | 离散 | 32, 64, 128 | 图像分支 |
| 二维CNN卷积层数 | 离散 | 2, 3, 4 | 图像分支 |
| Dropout率 | 连续 | 0.1 ~ 0.5 | 融合层和全连接层 |
| 融合方式 | 离散 | concat / attention | 融合层结构选择 |
| 全连接隐藏层维度 | 离散 | 128, 256, 512 | 分类头 |
学习率之所以用对数均匀分布,是因为它对数值尺度变化非常敏感。从1e-3到1e-2和从1e-4到1e-3,两者的相对变化幅度在训练效果上的差异完全不同,用线性均匀分布会导致0.01附近的取值概率太低。Dropout率用线性均匀分布就行,因为它的影响相对平滑。
3.3 目标函数怎么封装
这是整个流程最关键的部分。目标函数要做的事情是“接收一组超参数,返回该超参数下模型的验证集指标”。注意,这里的“验证集指标”通常要取负值,因为贝叶斯优化默认是最小化目标函数。如果用的是F1分数或准确率,需要返回它的负值。
目标函数内部的大致流程是:
- 根据超参数重新构建CNN模型;
- 使用固定随机种子划分训练集和验证集;
- 训练模型不超过预设的最大epoch数(比如30个epoch);
- 在验证集上计算目标指标;
- 清理显存、释放模型,防止OOM。
这里有一个非常实际的问题:**验证集划分怎么固定?**如果在目标函数内部每次重新划分验证集,那么同一组超参数在不同次评估中可能因为数据分布不同而得到不同的验证分数,这会污染贝叶斯优化的历史记录,优化器会看到同一个点有两个不同的y值,导致高斯过程的拟合变差。所以正确做法是在主流程外先把数据划分好,固定训练集和验证集,目标函数里始终使用同一份划分。等贝叶斯优化结束,再用全量训练数据(包含验证集)重新训练最终模型。
还有一个关于聚类的“额外设置”:训练epoch数固定为30。为什么不用“早停”?因为早停数的随机性会带来额外噪声,让单次评估的分数变得不稳定。当然,30是一个经验值,如果你的模型训练速度很快,可以用50;如果很慢,15也不是不行,关键是所有实验保持一致。
3.4 关键代码骨架
下面是我项目里的实际代码骨架,基于PyTorch和Optuna的版本。Optuna是我比较推荐的贝叶斯优化库,它对TPE的实现很成熟,和PyTorch的集成也很顺滑。如果你用过Hyperopt,也可以,只是Optuna的API更直观一些。
import torch import torch.nn as nn import optuna from optuna.trial import Trial from sklearn.metrics import f1_score # 没有固定结构,按搜索空间动态构建 def build_model(trial: Trial, input_dims: dict): lr = trial.suggest_float("lr", 1e-4, 1e-2, log=True) batch_size = trial.suggest_categorical("batch_size", [16, 32, 64]) drop_rate = trial.suggest_float("dropout", 0.1, 0.5) conv1d_channels = trial.suggest_categorical("conv1d_channels", [64, 128, 256]) conv1d_kernel = trial.suggest_categorical("conv1d_kernel", [3, 5, 7]) conv2d_channels = trial.suggest_categorical("conv2d_channels", [32, 64, 128]) conv2d_layers = trial.suggest_categorical("conv2d_layers", [2, 3, 4]) fusion_mode = trial.suggest_categorical("fusion_mode", ["concat", "attention"]) hidden_dim = trial.suggest_categorical("fc_hidden", [128, 256, 512]) # 根据这些参数构建模型... 省略构建细节 return model, lr, batch_size def objective(trial: Trial): model, lr, batch_size = build_model(trial, input_dims) optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() train_loader = ... # 固定划分的loader val_loader = ... for epoch in range(30): model.train() for x_batch, y_batch in train_loader: optimizer.zero_grad() output = model(**x_batch) # 多特征输入 loss = criterion(output, y_batch) loss.backward() optimizer.step() # 验证 model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for x_batch, y_batch in val_loader: output = model(**x_batch) preds = output.argmax(dim=1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(y_batch.cpu().numpy()) f1 = f1_score(all_labels, all_preds, average="macro") return -f1 # 取负号,因为optuna默认最小化 study = optuna.create_study(direction="minimize", sampler=optuna.samplers.TPESampler()) study.optimize(objective, n_trials=50) best_params = study.best_params best_value = study.best_value3.5 多特征输入在模型前向时怎么组织
多特征输入的网络前向和单输入不太一样。以这段代码为例,模型接收的x_batch是一个字典,里面包含table_features、text_features、image_features这几个键。PyTorch的DataLoader可以通过collate_fn自定义返回字典格式,模型类也相应地在forward里接收一个字典参数。
这个设计的好处是在代码层面“物理隔离”不同特征分支,不会因为维度不一致报错后要满世界找原因。而且后面如果要增减特征类型,只需要改DataLoader和模型构建部分,主体训练代码根本不用动。
4. 贝叶斯优化的调参实战与效果对比
4.1 一组真实的对比实验
我在自己的多特征项目里做过一次详细的对比。数据集是某场景下的多特征分类,输入包含20维数值特征、200步的时序波形和一个3通道的灰度图,总共6个类别。类别的样本量不均衡,所以评价指标用macro-F1。
我分别跑了:
- 网格搜索(只对4个最重要的参数,离散化后约200组)
- 随机搜索(300组)
- 贝叶斯优化(50组)
每组实验固定训练30个epoch。结果如下:
| 方法 | 实验组数 | 最优macro-F1 | 总耗时(小时) |
|---|---|---|---|
| 网格搜索(4个参数离散) | 200 | 0.812 | 12.6 |
| 随机搜索 | 300 | 0.834 | 18.4 |
| 贝叶斯优化(TPE,50次) | 50 | 0.823 | 3.1 |
| 贝叶斯优化(TPE,100次) | 100 | 0.841 | 6.2 |
说实话,第一次跑完这个对比我自己也愣了一下。贝叶斯优化只用了50组就超过了300组随机搜索的结果,用了100组就明显超过了前两者。这说明什么?说明随机搜索里很大一部分实验都在低价值区域浪费了,而贝叶斯优化因为每次都参考了历史结果,总是把“子弹”打在最可能出成绩的地方。
通常跑贝叶斯优化的第一个阶段,前20组左右,性能可能和随机搜索差不多,因为需要靠随机的初始点构建高斯过程。但到30组之后就明显拉开差距了。所以如果你给贝叶斯优化设定的总次数只有20次,那效果不一定比随机搜索强太明显。我个人建议至少40到60组起步。
4.2 最优超参数的收敛特征
从100组实验的日志里我还发现了一个有意思的规律:学习率这个参数会率先收敛到一个窄区间,大约在3e-4到1e-3之间;dropout率则会在0.2到0.3之间稳定下来;而卷积核数量往往在一个宽区间内波动,说明它对最终性能的敏感度相对较低。换句话说,贝叶斯优化其实帮我们区分了“关键参数”和“非关键参数”,这在单次随机实验中是看不出来的。
这个特征的实用价值在于:如果你的人力和算力有限,不需要每个项目都做完整的贝叶斯优化。你可以只对学习率、dropout、融合方式这三个最敏感的参数做搜索,其他参数用经验默认值。很多时候准确率就已经足够不错了。
4.3 融合方式的搜索结果
在多特征分类场景里,我最想看到的是融合方式这个离散超参数的搜索结果。在100组实验中,attention融合被选中的次数约占60%,concat约占30%,而attention融合的中位F1显著高于concat。这说明在这个数据集上,模型能够很好地利用注意力机制来动态分配不同分支的权重。而且我进一步查看最佳的几次实验,发现注意力权重最后收敛到的是:图像分支权重约为0.5,数值分支约0.3,时序分支约0.2,和业务直觉完全一致。
这个结果对业务方很有说服力。之前他们总觉得“深度学习是黑盒”,但有了这个权重,至少知道了模型主要依赖哪些特征做判断。即便最终没有采用attention结构,这个结论也能指导特征工程的方向——比如可以更多地丰富图像分支的信息,因为模型已经从这些特征里学到了很多。
5. 常见问题与排查技巧实录
5.1 贝叶斯优化跑得比网格搜索还慢
有个真实案例:A同学在做类似项目时,发现贝叶斯优化每次返回的“最优参数”都不稳定,而且总耗时比网格搜索还长。排查后发现两个问题。
第一个问题,他的一次目标函数内部训练了整整100个epoch,而且没有提前停。每组超参数评估要40分钟,50组实验就是33个小时以上。这显然不合理。贝叶斯优化对单次评估的耗时极其敏感,因为它的优势依赖于“在有限次数内尽可能多地探索”。我建议目标函数内部训练的epoch数控制在总训练epoch数的1/3到1/2。也就是说,如果你最终计划的模型要训练60个epoch,那贝叶斯优化阶段只训练20到30个epoch就够了,用来比较参数相对优劣完全够用。找到最优参数后,再用全量epoch去训练最终模型。
第二个问题,他的验证集划分函数里用了shuffle=True,导致每次评估时验证集都不一样。修复方式前面提到了,务必在外层固定划分。
5.2 多特征输入维度不匹配
这种情况太常见了。表格特征经过全连接层输出128维,序列特征经过一维CNN全局池化后输出256维,图像分支输出512维,这三个维度拼在一起是896维,后面接的第一个全连接层的输入维度必须和896严格一致。如果哪次改了某个分支的输出维度,忘了同步修改分类头,就会在forward时报错,而且这个报错信息往往非常抽象,只在维度检查那一行出现。
我的排查经验是:在模型forward里显式打印每个分支输出形状,至少在debug模式下加一个断言函数。或者更简单一点,在融合之前手动print一下各分支输出的shape,训练一次后就删掉。这种“一次性调试代码”反而能节省许多反复试错的时间。
5.3 显存溢出
贝叶斯优化过程中会有多次模型构建和释放,如果目标函数内部没有在每次训练结束后清理显存,跑二十几次之后就会OOM。试试看这种做法:
import torch import gc # objective函数结束后 del model, optimizer torch.cuda.empty_cache() gc.collect()还有一个容易忽视的点:DataLoader的num_workers在多次迭代中会不断创建新进程,也会导致内存泄漏。可以把num_workers固定为一个值,并在objective外面创建一次DataLoader,而不是在每次目标函数调用时重新创建。
5.4 贝叶斯优化结果异常差
如果你发现贝叶斯优化找到的最优结果竟然不如你自己手工调参的结果,先不要怀疑优化器,优先排查目标函数的“噪声是否过大”。常见原因有三种:
- 数据量太小(比如验证集只有几十条样本),单次评估的指标波动本身就很大,贝叶斯优化会把这些噪声当成真实的性能差异去拟合;
- 类别分布极端不平衡,用准确率做指标导致几乎所有结果都在同一个值附近,高斯过程没有梯度可学;
- 训练过程不稳定(学习率过大导致loss爆炸),同一个超参数重复跑两次结果差距很大。
解决方法是先用一个足够稳定的小实验确认“同一组参数下验证集指标的浮动范围”。如果浮动超过1个点,那么贝叶斯优化的难度会剧增。我建议优先降低优化器自身上一步的学习率、缩短batch评估的交叉验证划分,或者换用更稳定的数据归一化策略。
5.5 贝叶斯优化对离散参数的处
TPE对离散参数的支持很好,但有一个细节:如果你把“卷积核大小”定义成了[3, 5, 7]这样的离散值,TPE会把它当成分类变量处理,在三个值之间跳,而不会做3.5这样没有意义的内插。这没问题。但如果你把批大小定义为[16, 32, 64],有些人喜欢用log-uniform分布去逼近,这就不对了,因为本意是让它选这三个值之一,而不是任意大。做法是干脆用suggest_categorical,没必要玩花活。
6. 项目复盘与经验总结
这次项目的完整周期大概花了两周时间,其中真正跑贝叶斯优化的计算时间只是两到三天,大部分时间其实花在了数据预处理和多特征分支结构的调试上。回看整个过程,我觉得有三个经验最值得分享。
第一,贝叶斯优化不是“锦上添花”的工具,它在多特征输入模型上带来的收益比单输入模型更大。原因在于多特征模型的超参数空间更复杂,分支之间的相互作用更强,人工经验很难准确判断“哪个参数重要”。贝叶斯优化通过系统地搜索,把这个问题转化成了计算资源的投入。
第二,搜索空间的设计要结合业务直觉。不要以为给的范围越宽越好,过宽的空间会让绝大多数实验落在无效区域。我通常会先跑两三次人工实验,观察不同参数下loss曲线的行为,然后初步缩小范围,再交给贝叶斯优化。例如,如果学习率设为1e-2时loss直接发散,那么搜索空间的上限可以下调到5e-3。
第三,实验过程和训练日志要记录得非常细致。不只是最终指标,每一组超参数对应的验证集指标、训练loss曲线、训练耗时都需要记录下来。这些数据在后续做类似项目时特别值钱。我后来陆续做了两次类似的多特征分类,基本都是先参考前一次实验的日志缩小搜索范围,贝叶斯优化再跑20组就收敛了,这是实打实的效率提升。
如果你现在正被CNN调参折腾得焦头烂额,不妨试一下把贝叶斯优化纳入你的工作流。先把单次评估的耗时压到可控范围内,再认真设计搜索空间,跑40到60组实验。它也许不是每一次都能给你惊为天人的提升,但在多特征分类这种复杂场景里,它的回报几乎总是正的。最后再提醒一句:数据和特征才是一切模型效果的地基,贝叶斯优化能帮你找到好参数,但永远替代不了对业务和数据的深入理解。