语义检索系统之排序模块:基于 ERNIE-Gram 的 Pair-wise 和基于 RocketQA 的 CrossEncoder 训练的单塔模型
语义检索系统之排序模块:基于 ERNIE-Gram 的 Pair-wise 和基于 RocketQA 的 CrossEncoder 训练的单塔模型
文本匹配任务数据每一个样本通常由两个文本组成(query,title)。类别形式为 0 或 1,0 表示 query 与 title 不匹配; 1 表示匹配。
基于单塔 Point-wise 范式的语义匹配模型 ernie_matching: 模型精度高、计算复杂度高, 适合直接进行语义匹配 2 分类的应用场景。
基于单塔 Pair-wise 范式的语义匹配模型 ernie_matching: 模型精度高、计算复杂度高, 对文本相似度大小的序关系建模能力更强,适合将相似度特征作为上层排序模块输入特征的应用场景。
基于双塔 Point-Wise 范式的语义匹配模型 这 2 种方案计算效率更高,适合对延时要求高、根据语义相似度进行粗排的应用场景。
Pointwise
:输入两个文本和一个标签,可看作为一个分类问题,即判断输入的两个文本是否匹配。Pairwise
:输入为三个文本,分别为 Query 以及对应的正样本和负样本,该训练方式考虑到了文本之间的相对顺序。单塔
:先将输入文本合并,然后输入到单一的神经网络模型。双塔
:对输入文本分别进行编码成固定长度的向量,通过文本的表示向量进行交互计算得到文本之间的关系。语义搜索系列文章全流程教学:
更多文本匹配方案参考:
1.排序模型任务简介和要求
1.1 技术方案和评估指标
技术方案
双塔模型,使用 ERNIE-Gram 预训练模型,使用 margin_ranking_loss 训练模型。
评估指标
(1)采用 AUC 指标来评估排序模型的排序效果。
效果评估先看
1.2 环境依赖和安装说明
环境依赖
python >= 3.7
paddlepaddle >= 2.3.7
paddlenlp >= 2.3
pandas >= 0.25.1
scipy >= 1.3.1
1.3 代码结构
项目代码结构及说明:
1.4 数据介绍
数据集说明
样例数据如下:
2.基于 ERNIE-Gram 模型训练
排序模型下载链接:
训练环境说明
NVIDIA Driver Version: 440.64.00
Ubuntu 16.04.6 LTS (Docker)
Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz
2.1 单机单卡训练/单机多卡训练
这里采用单机多卡方式进行训练,通过如下命令,指定 GPU 0,1,2,3 卡, 基于 ERNIE-Gram 训练模型,数据量比较大,需要 20 小时 10 分钟左右。如果采用单机单卡训练,只需要把--gpu
参数设置成单卡的卡号即可
训练的命令如下:
2.1.1 推荐系统中常用的两种优化损失函数的机器学习范式:pointwise loss 和 pairwise loss
精排简介
Learning to Rank (LTR)是一类技术方法,主要利用机器学习算法解决实际中的排序问题。传统的机器学习主要解决的问题是一个分类或者回归问题,比如对一个样本数据预测对应的类别或者预测一个数值分值。而 LTR 解决的是一个排序问题,对一个 list 的 item 进行一个排序,所以 LTR 并不太关注这个 list 的每个 item 具体得多少分值,更关注所有 item 的相对顺序。排序通常是信息检索的核心成分,所以 LTR 最常见的应用是搜索场景,对召回的 document 进行排序。
Pointwise 方法
Pointwise 方法是通过近似为回归问题解决排序问题,输入的单条样本为得分 - 文档,将每个查询 - 文档对的相关性得分作为实数分数或者序数分数,使得单个查询 - 文档对作为样本点 (Pointwise 的由来),训练排序模型。预测时候对于指定输入,给出查询 - 文档对的相关性得分。
pointwise loss :
最小化预测输出与目标值之间的平分损失,具体处理是在处理负样本时:把未观察到的实体(即 user 与 item 没有交互)当作负样本,或者从未观察到的实体中采样负样本。
Pairwise 方法
Pairwise 方法是通过近似为分类问题解决排序问题,输入的单条样本为标签 - 文档对。对于一次查询的多个结果文档,组合任意两个文档形成文档对作为输入样本。即学习一个二分类器,对输入的一对文档对 AB(Pairwise 的由来),根据 A 相关性是否比 B 好,二分类器给出分类标签 1 或 0。对所有文档对进行分类,就可以得到一组偏序关系,从而构造文档全集的排序关系。该类方法的原理是对给定的文档全集 S,降低排序中的逆序文档对的个数来降低排序错误,从而达到优化排序结果的目的。
pairwise loss :
最大化观察到的(即正样本)预测输出和未观察到的(负样本)的预测输出的边缘,表现为观察到的实体得分排名高于未观察到的实体。
2.1.2 深度学习框架中的 Ranking Loss 层
paddlepaddle
margin_ranking_loss:计算输入 input,other 和 标签 label 间的 margin rank loss 损失。更多内容进行文章跳转看 api 文档
Caffe
Constrastive Loss Layer. 限于 Pairwise Ranking Loss 计算. 例如,可以用于训练 Siamese 网络。
PyCaffe Triplet Ranking Loss Layer. 用来训练 triplet 网络,by David Lu。
PyTorch
CosineEmbeddingLoss. 使用余弦相似度的 Pairwise Loss。输入是一对二元组,标签标记它是一个正样本对还是负样本对,以及边距 margin。
MarginRankingLoss. 同上, 但使用欧拉距离。
TripletMarginLoss. 使用欧拉距离的 Triplet Loss。进入 Loss Functions 查看具体没课函数
TensorFlow
contrastive_loss. Pairwise Ranking Loss.
triplet_semihard_loss. 使用 semi-hard 负采样的 Triplet loss。
更多内容参考:
推荐系统[四]:精排-详解排序算法LTR (Learning to Rank): poitwise, pairwise, listwise相关评价指标,超详细知识指南。
推荐系统[4.1]:Ranking Loss 函数:度量学习、Siamese 和 triplet 网络、RankNet、pair-wise、List-wise loss以及在深度学习框架中loss设计
参数说明:
--
margin
, default=0.2, type=float, help="Margin for pos_score and neg_score.--
train_file
, type=str, required=True, help="The full path of train file--
test_file
, type=str, required=True, help="The full path of test file--
save_dir
, default='./checkpoint', type=str, help="The output directory where the model checkpoints will be written.--
max_seq_length
, default=128, type=int, help="The maximum total input sequence length after tokenization. Sequences longer than this will be truncated, sequences shorter will be padded.--
batch_size
, default=32, type=int, help="Batch size per GPU/CPU for training.--
learning_rate
, default=5e-5, type=float, help="The initial learning rate for Adam.--
weight_decay
, default=0.0, type=float, help="Weight decay if we apply some.--
epochs
, default=3, type=int, help="Total number of training epochs to perform.--
eval_step
, default=200, type=int, help="Step interval for evaluation.--
save_step
, default=10000, type=int, help="Step interval for saving checkpoint.--
warmup_proportion
, default=0.0, type=float, help="Linear warmup proportion over the training process.--
init_from_ckpt
, type=str, default=None, help="The path of checkpoint to be loaded.--
model_name_or_path
, default="ernie-3.0-medium-zh", help="The pretrained model used for training--
seed
, type=int, default=1000, help="Random seed for initialization.--
device
, choices=['cpu', 'gpu'], default="gpu", help="Select which device to train model, defaults to gpu.部分结果展示:
2.1.3 更多 ERNIE 3.0 模型选择
官网链接:https://github.com/PaddlePaddle/PaddleNLP/tree/develop/model_zoo/ernie-3.0
更多技术细节可以参考论文:
ERNIE-Tiny: A Progressive Distillation Framework for Pretrained Transformer Compression
下表汇总介绍了目前 PaddleNLP 支持的 ERNIE 模型对应预训练权重。
<table><thead><tr><th>Pretrained Weight</th><th>Language</th><th>Details of the model</th></tr></thead><tbody><tr><td><code>ernie-1.0-base-zh</code></td><td>Chinese</td><td>12-layer, 768-hidden, 12-heads, 108M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-1.0-base-zh-cw</code></td><td>Chinese</td><td>12-layer, 768-hidden, 12-heads, 118M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-1.0-large-zh-cw</code></td><td>Chinese</td><td>24-layer, 1024-hidden, 16-heads, 272M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-tiny</code></td><td>Chinese</td><td>3-layer, 1024-hidden, 16-heads, _M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-2.0-base-en</code></td><td>English</td><td>12-layer, 768-hidden, 12-heads, 103M parameters. Trained on lower-cased English text.</td></tr><tr><td><code>ernie-2.0-base-en-finetuned-squad</code></td><td>English</td><td>12-layer, 768-hidden, 12-heads, 110M parameters. Trained on finetuned squad text.</td></tr><tr><td><code>ernie-2.0-large-en</code></td><td>English</td><td>24-layer, 1024-hidden, 16-heads, 336M parameters. Trained on lower-cased English text.</td></tr><tr><td><code>ernie-3.0-xbase-zh</code></td><td>Chinese</td><td>20-layer, 1024-hidden, 16-heads, 296M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-3.0-base-zh</code></td><td>Chinese</td><td>12-layer, 768-hidden, 12-heads, 118M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-3.0-medium-zh</code></td><td>Chinese</td><td>6-layer, 768-hidden, 12-heads, 75M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-3.0-mini-zh</code></td><td>Chinese</td><td>6-layer, 384-hidden, 12-heads, 27M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-3.0-micro-zh</code></td><td>Chinese</td><td>4-layer, 384-hidden, 12-heads, 23M parameters. Trained on Chinese text.</td></tr><tr><td><code>ernie-3.0-nano-zh</code></td><td>Chinese</td><td>4-layer, 312-hidden, 12-heads, 18M parameters. Trained on Chinese text.</td></tr><tr><td><code>rocketqa-base-cross-encoder</code></td><td>Chinese</td><td>12-layer, 768-hidden, 12-heads, 118M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-medium-cross-encoder</code></td><td>Chinese</td><td>6-layer, 768-hidden, 12-heads, 75M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-mini-cross-encoder</code></td><td>Chinese</td><td>6-layer, 384-hidden, 12-heads, 27M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-micro-cross-encoder</code></td><td>Chinese</td><td>4-layer, 384-hidden, 12-heads, 23M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-nano-cross-encoder</code></td><td>Chinese</td><td>4-layer, 312-hidden, 12-heads, 18M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-base-query-encoder</code></td><td>Chinese</td><td>12-layer, 768-hidden, 12-heads, 118M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-base-para-encoder</code></td><td>Chinese</td><td>12-layer, 768-hidden, 12-heads, 118M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-medium-query-encoder</code></td><td>Chinese</td><td>6-layer, 768-hidden, 12-heads, 75M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-medium-para-encoder</code></td><td>Chinese</td><td>6-layer, 768-hidden, 12-heads, 75M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-mini-query-encoder</code></td><td>Chinese</td><td>6-layer, 384-hidden, 12-heads, 27M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-mini-para-encoder</code></td><td>Chinese</td><td>6-layer, 384-hidden, 12-heads, 27M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-micro-query-encoder</code></td><td>Chinese</td><td>4-layer, 384-hidden, 12-heads, 23M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-micro-para-encoder</code></td><td>Chinese</td><td>4-layer, 384-hidden, 12-heads, 23M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-nano-query-encoder</code></td><td>Chinese</td><td>4-layer, 312-hidden, 12-heads, 18M parameters. Trained on DuReader retrieval text.</td></tr><tr><td><code>rocketqa-zh-nano-para-encoder</code></td><td>Chinese</td><td>4-layer, 312-hidden, 12-heads, 18M parameters. Trained on DuReader retrieval text.</td></tr></tbody></table>
2.2 模型评估
在排序阶段使用的指标为AUC,AUC反映的是分类器对样本的排序能力,如果完全随机得对样本分类,那么AUC应该接近0.5。分类器越可能把真正的正样本排在前面,AUC越大,分类性能越好。
部分结果展示:
2.3 模型预测
准备预测数据:待预测数据为 tab 分隔的 tsv 文件,每一行为 1 个文本 Pair,和文本 pair 的语义索引相似度,部分示例如下:
部分效果展示:
2.3.1 使用 FastTokenizer 加速
FastTokenizer 是飞桨提供的速度领先的文本处理算子库,集成了 Google 于 2021 年底发布的 LinMaxMatch 算法,该算法引入 Aho-Corasick 将 WordPiece 的时间复杂度从 O(N<sup>2</sup>) 优化到 O(N),已在 Google 搜索业务中大规模上线。FastTokenizer 速度显著领先,且呈现 batch_size 越大,优势越突出。例如,设置 batch_size = 64 时,FastTokenizer 切词速度比 HuggingFace 快 28 倍。
在 ERNIE 3.0 轻量级模型裁剪、量化基础上,当设置切词线程数为 4 时,使用 FastTokenizer 在 NVIDIA Tesla T4 环境下在 IFLYTEK (长文本分类数据集,最大序列长度为 128)数据集上性能提升了 2.39 倍,相比 BERT-Base 性能提升了 7.09 倍,在 Intel(R) Xeon(R) Gold 6271C CPU @ 2.60GHz、线程数为 8 的情况下性能提升了 1.27 倍,相比 BERT-Base 性能提升了 5.13 倍。加速效果如下图所示:
使用 FastTokenizer 的方式非常简单,在安装 fast_tokenizer 包之后,仅需在 tokenizer 实例化时直接传入 use_fast=True
即可。目前已在 Linux 系统下支持 BERT、ERNIE、TinyBERT 等模型。
如需设置切词线程数,需要调用fast_tokenizer.set_thread_num
接口进行设置:
调用 from_pretrained
时只需轻松传入一个参数 use_fast=True
:
2.5 部署
2.5.1 动转静导出:首先把动态图模型转换为静态图:
2.5.2 Paddle Inference
使用 PaddleInference:
部分结果展示::
2.5.3 Paddle Serving 部署
Paddle Serving 的详细文档请参考 Pipeline_Design和Serving_Design,首先把静态图模型转换成 Serving 的格式:
参数含义说明
dirname
: 需要转换的模型文件存储路径,Program 结构文件和参数文件均保存在此目录。model_filename
: 存储需要转换的模型 Inference Program 结构的文件名称。如果设置为 None ,则使用__model__
作为默认的文件名params_filename
: 存储需要转换的模型所有参数的文件名称。当且仅当所有模型参数被保>存在一个单独的二进制文件中,它才需要被指定。如果模型参数是存储在各自分离的文件中,设置它的值为 Noneserver_path
: 转换后的模型文件和配置文件的存储路径。默认值为 serving_serverclient_path
: 转换后的客户端配置文件存储路径。默认值为 serving_clientfetch_alias_names
: 模型输出的别名设置,比如输入的 input_ids 等,都可以指定成其他名字,默认不指定feed_alias_names
: 模型输入的别名设置,比如输出 pooled_out 等,都可以重新指定成其他模型,默认不指定
这里需要注意,
dirname
参数在 paddle2.5.0 版本中 serving_io.inference_model_to_serving 算子中被移除了,目前使用 paddle2.4.2 版本即可。最后在 serving_sever 会生成 4-5 个文件
也可以运行下面的 bash 脚本:自行修改参数
Paddle Serving 的部署有两种方式,第一种方式是 Pipeline 的方式,第二种是 C++的方式,下面分别介绍这两种方式的用法:
Pipeline 方式部署
修改 config_nlp.yml 文件中 model 路径
修改 Tokenizer,web_service.py
启动 Pipeline Server:
启动客户端调用 Server。
首先修改 rpc_client.py 中需要预测的样本:
模型输出:
如果遇到结果越界等问题,请更改 paddle 版本,目前使用 paddle 2.4.0 develop 版本 【介于 2.40 2.50 之间】
C++的方式部署
启动 C++的 Serving:
遇到相关问题请参考:https://blog.csdn.net/sinat_39620217/article/details/131675175
time to cost :0.006819009780883789 seconds[0.96249247]
也可以使用 curl 方式发送 Http 请求:
3.基于 RocketQA 的 CrossEncoder 训练的单塔模型
基于 RocketQA 的 CrossEncoder(交叉编码器)训练的单塔模型,该模型用于搜索的排序阶段,对召回的结果进行重新排序的作用。
CrossEncoder 和 Pairwise 区别:
输入方式:
Pairwise 模型:接受两个文本对作为输入,通常是一个正例和一个负例。正例表示相关的文本对,负例表示不相关的文本对。
CrossEncoder 模型:接受多个文本对作为输入,可以同时处理多个文本对的相关性判断。
训练方式:
Pairwise 模型:通过训练模型来学习区分正例和负例之间的特征。模型会比较两个文本对之间的相似度或相关性,并为每个文本对产生一个得分或预测标签。
通过将文本对转化为三个样本来训练:正样本(相关的文本对),负样本(不相关的文本对),以及参考样本(用于度量两个样本之间的相关性)。这个模型的目标是训练一个二分类器,将正样本得分高于负样本。经过编码器(通常是基于深度学习的模型,如 BERT)进行编码。然后,编码后的文本会通过一个相似度计算方法(如余弦相似度或点积)生成一个相关性得分,用于判断文本对的相关性。
CrossEncoder 模型:一次性对多个文本对进行编码和判断。模型会将多个文本对作为整体输入,学习捕捉多个文本对之间的关系,并输出它们之间的相关性得分或标签。
将一对文本作为单个样本来训练,不需要额外的负样本和参考样本。这个模型的目标是训练一个多分类器,将不同的文本对分为相关的和不相关的类别。它们经过编码器进行编码,并在编码后的表示上应用一个多层感知机或其他类型的全连接网络。该网络将文本对的编码表示映射到相关性得分或概率。
处理效率:
Pairwise 模型:由于是逐对比较,处理效率相对较低。需要遍历每对文本对进行比较和预测,特别是在大规模的文本对数据集上训练和推断时,效率会较低。
CrossEncoder 模型:可以一次性处理多个文本对,因此在处理大规模文本对任务时具有较高的效率。能够进行批量处理,减少了逐对比较的时间消耗。
应用场景:
Pairwise 模型:常用于文本排序或排名任务,如搜索引擎中的搜索结果排序、推荐系统中的推荐列表排序等。
CrossEncoder 模型:适用于需要同时处理多个文本对的任务,如阅读理解中的问题-答案匹配、文本匹配中的相似性判断等。
Pairwise 模型更适用于在大规模数据集上进行训练,因为它可以从大量的正样本和负样本中学习到相关性特征。而 CrossEncoder 模型则不需要额外的负样本,因此在训练数据有限的情况下可能更容易实现。
3.1 代码结构
[literature_search_rank]数据集情况
3.2 模型训练
参数情况:
部分结果展示:
3.3 模型评估
3.4 模型预测+FastTokenizer 加速
部分结果展示:
3.5 部署
动转静导出:首先把动态图模型转换为静态图:
参数含义说明
dirname
: 需要转换的模型文件存储路径,Program 结构文件和参数文件均保存在此目录。model_filename
: 存储需要转换的模型 Inference Program 结构的文件名称。如果设置为 None ,则使用__model__
作为默认的文件名params_filename
: 存储需要转换的模型所有参数的文件名称。当且仅当所有模型参数被保>存在一个单独的二进制文件中,它才需要被指定。如果模型参数是存储在各自分离的文件中,设置它的值为 Noneserver_path
: 转换后的模型文件和配置文件的存储路径。默认值为 serving_serverclient_path
: 转换后的客户端配置文件存储路径。默认值为 serving_clientfetch_alias_names
: 模型输出的别名设置,比如输入的 input_ids 等,都可以指定成其他名字,默认不指定feed_alias_names
: 模型输入的别名设置,比如输出 pooled_out 等,都可以重新指定成其他模型,默认不指定
终端启动效果如下:
C++的方式:Client 可以使用 http 或者 rpc 两种方式参考第二章节
相关步骤即可
总结
整体 CrossEncoder 训练方式优于 pairwise,这里我就不长时间训练下去,仅简单增加训练时长进行对比验证了一下。
本项目提供了排序模块有 2 种选择:
第一种基于前沿的预训练模型 ERNIE,训练 Pair-wise 语义匹配模型;
第二种是基于 RocketQA 模型训练的 Cross Encoder 模型。
CrossEncoder 和 Pairwise 区别:
输入方式:
Pairwise 模型:接受两个文本对作为输入,通常是一个正例和一个负例。正例表示相关的文本对,负例表示不相关的文本对。
CrossEncoder 模型:接受多个文本对作为输入,可以同时处理多个文本对的相关性判断。
训练方式:
Pairwise 模型:通过训练模型来学习区分正例和负例之间的特征。模型会比较两个文本对之间的相似度或相关性,并为每个文本对产生一个得分或预测标签。
通过将文本对转化为三个样本来训练:正样本(相关的文本对),负样本(不相关的文本对),以及参考样本(用于度量两个样本之间的相关性)。这个模型的目标是训练一个二分类器,将正样本得分高于负样本。经过编码器(通常是基于深度学习的模型,如 BERT)进行编码。然后,编码后的文本会通过一个相似度计算方法(如余弦相似度或点积)生成一个相关性得分,用于判断文本对的相关性。
CrossEncoder 模型:一次性对多个文本对进行编码和判断。模型会将多个文本对作为整体输入,学习捕捉多个文本对之间的关系,并输出它们之间的相关性得分或标签。
将一对文本作为单个样本来训练,不需要额外的负样本和参考样本。这个模型的目标是训练一个多分类器,将不同的文本对分为相关的和不相关的类别。它们经过编码器进行编码,并在编码后的表示上应用一个多层感知机或其他类型的全连接网络。该网络将文本对的编码表示映射到相关性得分或概率。
处理效率:
Pairwise 模型:由于是逐对比较,处理效率相对较低。需要遍历每对文本对进行比较和预测,特别是在大规模的文本对数据集上训练和推断时,效率会较低。
CrossEncoder 模型:可以一次性处理多个文本对,因此在处理大规模文本对任务时具有较高的效率。能够进行批量处理,减少了逐对比较的时间消耗。
应用场景:
Pairwise 模型:常用于文本排序或排名任务,如搜索引擎中的搜索结果排序、推荐系统中的推荐列表排序等。
CrossEncoder 模型:适用于需要同时处理多个文本对的任务,如阅读理解中的问题-答案匹配、文本匹配中的相似性判断等。
Pairwise 模型更适用于在大规模数据集上进行训练,因为它可以从大量的正样本和负样本中学习到相关性特征,但对于噪声数据更为敏感,即一个错误的标注会导致多个 pair 对的错误。而 CrossEncoder 模型则不需要额外的负样本,因此在训练数据有限的情况下可能更容易实现。
语义搜索系列文章全流程教学:
更多文本匹配方案参考:
更多优质内容请关注公号:汀丶人工智能;会提供一些相关的资源和优质文章,免费获取阅读。
版权声明: 本文为 InfoQ 作者【汀丶人工智能】的原创文章。
原文链接:【http://xie.infoq.cn/article/eb7c3a7dd05469690304a0685】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论