原文:Super Machine Learning Revision Notes
译者:飞龙
本文旨在概述:
- 机器学习中的基本概念(例如,梯度下降,反向传播等)
- 不同的算法和各种流行的模型
- 一些实用技巧和示例是从我自己的实践和一些在线课程(如DeepLearningAI )中学习的。
如果您是正在学习机器学习的学生,希望本文可以帮助您缩短复习时间并给您带来有益的启发。 如果您不是学生,希望本文在您不记得某些模型或算法时会有所帮助。
此外,您也可以将其视为“快速检查指南”。 请随意使用Ctrl + F
搜索您感兴趣的任何关键字。
任何意见和建议都非常欢迎!
名称 | 函数 | 导数 |
---|---|---|
Sigmoid | ![]() ![]() |
![]() ![]() |
tanh | ![]() ![]() |
![]() ![]() |
如果为![]() ![]() |
||
ReLU | ![]() ![]() |
如果 ![]() ![]() |
如果 ![]() ![]() |
||
如果 ![]() ![]() |
||
LReLU | ![]() ![]() |
如果 ![]() ![]() |
如果 ![]() ![]() |
梯度下降是找到目标函数(例如损失函数)的局部最小值的一种迭代方法。
Repeat{ W := W - learning_rate * dJ(W)/dW}
通常,我们用
问题: 为什么在最小化损失函数时减去梯度而不加梯度?
答案:
当
但是有时,梯度下降法可能会遇到局部最优问题。
该计算图示例是从DeepLearningAI 的第一门课程中学到的。
假设我们有 3 个可学习的参数
计算每个节点的梯度很容易,如下所示。 (提示:实际上,如果您正在实现自己的算法,则可以在正向过程中计算梯度以节省计算资源和训练时间。因此,在进行反向传播时,无需再次计算每个节点的梯度 。)
现在,我们可以通过简单地组合节点梯度来计算每个参数的梯度:
Repeat{ W := W - (lambda/m) * W - learning_rate * dJ(W)/dW}
如果我们有一个非常深的神经网络并且未正确初始化权重,则可能会遇到梯度消失或爆炸的问题。 (有关参数初始化的更多详细信息:参数初始化)
为了解释什么是消失或爆炸梯度问题,将以一个简单的深度神经网络架构为例。 (同样,很棒的例子来自在线课程DeepLearningAI )
神经网络具有
因为权重值
这些消失/爆炸的梯度会使训练变得非常困难。 因此,仔细初始化深度神经网络的权重很重要。
如果我们拥有庞大的训练数据集,那么在单个周期训练模型将花费很长时间。 对于我们而言,跟踪训练过程将非常困难。 在小批量梯度下降中,基于当前批量中的训练示例计算成本和梯度。
小批量的过程如下:
For t= (1, ... , #Batches):
Do forward propagation on the t-th batch examples;
Compute the cost on the t-th batch examples;
Do backward propagation on the t-th batch examples to compute gradients and update parameters.
在训练过程中,当我们不应用小批量梯度下降时,与使用小批量训练模型相比,成本趋势更加平滑。
当批量大小为 1 时,称为随机梯度下降。
批量大小:
1)如果大小为
2)如果大小为 1,则称为随机梯度下降。
实际上,大小是在 1 到 M 之间选择的。当
动量的超参数是
原始的
“校正”是指数加权平均值中的“偏差校正” 的概念。 该校正可以使平均值的计算更加准确。
学习率
如果在训练期间固定学习率,则损失/成本可能会波动,如下图所示。 寻找一种使学习速率具有适应性的方法可能是一个好主意。
根据周期数降低学习率是一种直接的方法。 以下是速率衰减公式。
周期 | ![]() ![]() |
---|---|
1 | 0.1 |
2 | 0.67 |
3 | 0.5 |
4 | 0.4 |
5 | … |
当然,还有其他一些学习率衰减方法。
其他方法 | 公式 |
---|---|
指数衰减 | ![]() ![]() |
周期相关 | ![]() ![]() |
小批量相关 | ![]() ![]() |
离散阶梯 | ![]() |
手动衰减 | 逐日手动或逐小时降低学习率等。 |
使用批量规范化可以加快训练速度。
步骤如下。
在测试时,我们没有实例来计算
在这种情况下,最好使用跨小批量的指数加权平均值来估计
可学习的参数 |
---|
![]() ![]() |
超参数 |
---|
学习率![]() ![]() |
迭代次数 |
隐藏层数![]() ![]() |
每一层的隐藏单元的数量 |
选择激活函数 |
动量参数 |
小批量 |
正则化参数 |
(注意:实际上,机器学习框架(例如 tensorflow,chainer 等)已经提供了强大的参数初始化功能。)
例如,当我们初始化参数
W = numpy.random.randn(shape) * 0.01
这样做的原因是,如果您使用的是 Sigmoid 且初始参数较大,则梯度将非常小。
同样,我们将使用伪代码来显示各种初始化方法的工作方式。 我们的想法是,如果隐藏单元的数量较大,我们更愿意为参数分配较小的值,以防止训练阶段的消失或爆炸。下图可能会为您提供一些了解该想法的见解。
基于上述思想,我们可以使用与隐藏单元数有关的项对权重进行设置。
W = numpy.random.randn(shape) * numpy.sqrt(1/n[l-1])
如果您的激活函数是
调整超参数时,有必要尝试各种可能的值。 如果计算资源足够,最简单的方法是训练具有各种参数值的并行模型。 但是,最有可能的资源非常稀少。 在这种情况下,我们只能照顾一个模型,并在不同周期尝试不同的值。
除了上述方面,如何明智地选择超参数值也很重要。
如您所知,神经网络架构中有各种超参数:学习率
Ng 推荐以下超参数优先级:
优先级 | 超参数 |
---|---|
1 | 学习率![]() ![]() |
2 | ![]() ![]() ![]() ![]() ![]() ![]() |
2 | 隐藏单元数 |
2 | 批量大小 |
3 | 层数 |
3 | 学习率衰减数 |
例如,如果层数的范围是 2-6,我们可以统一尝试使用 2、3、4、5、6 来训练模型。 同样,对于 50-100 的隐藏单元,在这种比例下选择值是一个很好的策略。
例:
您可能已经意识到,对于所有参数而言,均匀采样通常不是一个好主意。
例如,让我们说学习率
下表可能有助于您更好地了解该策略。
![]() ![]() |
0.9 | 0.99 | 0.999 |
![]() ![]() |
0.1 | 0.01 | 0.001 |
![]() ![]() |
-1 | -2 | -3 |
例:
正则化是防止机器学习出现过拟合问题的一种方法。 附加的正则项将添加到损失函数中。
在新的损失函数中,
对于逻辑回归模型,
对于具有多层(例如
为了直观地了解 Dropout,Dropout 正则化的目的是使受监督的模型更加健壮。 在训练短语中,激活函数的某些输出值将被忽略。 因此,在进行预测时,模型将不依赖任何一项特征。
在 Dropout 正则化中,超参数“保持概率”描述了激活隐藏单元的几率。 因此,如果隐藏层具有
如上所示,丢弃了第二层的 2 个单元。 因此,第三层的线性组合值(即
注意:在测试时进行预测时,不需要进行 Dropout 正则化。
给定实例的特征向量
(图片从维基百科下载)
最小化成本函数实际上是最大化数据的似然。
softmax 回归将 logistic 回归(二元分类)概括为多个类(多类分类)。
如上图所示,它是 3 类分类神经网络。 在最后一层,使用 softmax 激活函数。 输出是每个类别的概率。
softmax 激活如下。
如果我们有大量的训练数据或者我们的神经网络很大,那么训练这样的模型会很费时(例如几天或几周)。 幸运的是,有一些模型已发布并公开可用。 通常,这些模型是在大量数据上训练的。
迁移学习的思想是,我们可以下载这些经过预先训练的模型,并根据自己的问题调整模型,如下所示。
如果我们有很多数据,我们可以重新训练整个神经网络。 另一方面,如果我们的训练小,则可以重新训练最后几层或最后几层(例如,最后两层)。
在哪种情况下我们可以使用迁移学习?
假设:
预先训练的模型用于任务 A,而我们自己的模型用于任务 B。
- 这两个任务应具有相同的输入格式
- 对于任务 A,我们有很多训练数据。 但是对于任务 B,数据的大小要小得多
- 从任务 A 中学到的低级特征可能有助于训练任务 B 的模型。
在分类任务中,通常每个实例只有一个正确的标签,如下所示。 第 i 个实例仅对应于第二类。
但是,在多任务学习中,一个实例可能具有多个标签。
在任务中,损失函数为:
多任务学习提示:
- 多任务学习模型可以共享较低级别的特征
- 我们可以尝试一个足够大的神经网络以在所有任务上正常工作
- 在训练集中,每个任务的实例数量相似
例如,我们有一个
而且,我们可以同时具有多个滤波器,如下所示。
同样,如果输入是一个 3 维的体积,我们也可以使用 3D 滤波器。 在此滤波器中,有 27 个可学习的参数。
滤波器的想法是,如果它在输入的一部分中有用,那么也许对输入的另一部分也有用。 而且,卷积层输出值的每个输出值仅取决于少量的输入。
步幅描述了滤波器的步长。 它将影响输出大小。
应当注意,一些输入元素被忽略。 这个问题可以通过填充来解决。
如上所述,有效卷积是我们不使用填充时的卷积。
相同卷积是我们可以使用填充通过填充零来扩展原始输入,以便输出大小与输入大小相同。
例如,输入大小为
stride = 1
和padding = 1
,我们可以获得与输入相同大小的输出。
通常,如果滤波器大小为f * f
,输入为n * n
,步幅为s
,则最终输出大小为:
实际上,我们还在卷积层上应用了激活函数,例如 Relu 激活函数。
至于参数的数量,对于一个滤波器,总共有 27 个(滤波器的参数)+1(偏置)= 28 个参数。
如果不使用 1X1 转换层,则计算成本存在问题:
使用 1X1 转换层,参数数量大大减少:
池化层(例如最大池化层或平均池化层)可以被认为是一种特殊的滤波器。
最大池化层返回滤波器当前覆盖区域的最大值。 同样,平均池层将返回该区域中所有数字的平均值。
注意:在池化层中,没有可学习的参数。
(模型中有约 60k 参数)
(模型中有约 60m 的参数;使用 Relu 激活函数;)
(模型中有大约 138m 的参数;所有滤波器中
损失函数:
首先,使用训练集来训练分类器。 然后将其逐步应用于目标图片:
问题是计算成本(按顺序计算)。 为了解决这个问题,我们可以使用滑动窗口的卷积实现(即将最后的完全连接层变成卷积层)。
使用卷积实现,我们不需要按顺序计算结果。 现在我们可以一次计算结果。
实际上,在某些图片中,只有几个窗口具有我们感兴趣的对象。在区域提议(R-CNN)方法中,我们仅在提出的区域上运行分类器。
R-CNN :
- 使用一些算法来提出区域
- 一次对这些提出的区域进行分类
- 预测标签和边界框
Fast R-CNN :
- 使用聚类方法提出区域
- 使用滑动窗口的卷积实现对提出的区域进行分类
- 预测标签和边界框
另一种更快的 R-CNN 使用卷积网络来提出区域。
每张图片均分为多个单元。
对于每个单元格:
按照惯例,通常将 0.5 用作阈值,以判断预测的边界框是否正确。 例如,如果 IOU 大于 0.5,则可以说该预测是正确的答案。
该算法可以找到对同一物体的多次检测。 例如,在上图中,它为猫找到 2 个边界框,为狗找到 3 个边界框。 非最大抑制算法可确保每个对象仅被检测一次。
步骤:
2)对于任何剩下的框:
b)在最后一步中,将所有带有
先前的方法只能在一个单元格中检测到一个对象。 但是在某些情况下,一个单元中有多个对象。 为了解决这个问题,我们可以定义不同形状的边界框。
因此,训练图像中的每个对象都分配给:
做出预测:
在这种情况下,一次性学习就是:从一个例子中学习以再次认识这个人。
如果我们相信编码函数
学习:
学习这些参数,以便:
这三张图片是:
- 锚图片
- 正图片:锚图片中同一个人的另一张图片
- 负图片:锚图片中另一张不同人的图片。
但是,仅学习上述损失函数将存在问题。 该损失函数可能导致学习
要对其进行重组:
汇总损失函数:
选择 A,P,N 的三元组:
在训练期间,如果随机选择 A,P,N,则很容易满足
我们应该选择难以训练的三元组。
当使用困难三元组进行训练时,梯度下降过程必须做一些工作以尝试将这些量推离。
我们可以学习一个 Sigmoid 二元分类函数:
我们还可以使用其他变量,例如卡方相似度:
内容图像来自电影 Bolt。
样式图像是“百马图”的一部分,这是中国最著名的古代绘画之一。
https://deepart.io 支持生成的图像。
内容成本函数,
1)使用隐藏层(不太深也不太浅)
2)
3)
2)将图片样式定义为跨通道激活之间的相关性
矩阵
对于样式图片:
对于生成的图像:
您也可以考虑合并不同层的样式损失。
在此图中,红色参数是可学习的变量
矩阵由
在该模型中,可以像其他参数(即
该模型的总体思想是在给定上下文的情况下预测目标单词。 在上图中,上下文是最后 4 个单词(即 a,玻璃,of,橙色),目标单词是“ to”。
另外,有多种方法可以定义目标词的上下文,例如:
句子:
I want a glass of orange juice to go along with my cereal.
在此词嵌入学习模型中,上下文是从句子中随机选择的词。 目标是用上下文词的窗口随机拾取的词。
例如:
让我们说上下文词为orange
,我们可能会得到以下训练示例。
模型:
softmax 函数定义为:
使用 softmax 函数的问题是分母的计算成本太大,因为我们的词汇量可能很大。 为了减少计算量,负采样是不错的解决方案。
句子:
I want a glass of orange juice to go along with my cereal.
给定一对单词(即上下文单词和另一个单词)和标签(即第二个单词是否为目标单词)。 如下图所示,(orange
)是一个正例,因为单词juice
是橙色的真正目标单词。 由于所有其他单词都是从词典中随机选择的,因此这些单词被视为错误的目标单词。 因此,这些对是负例(如果偶然将真实的目标单词选作负例,也可以)。
至于每个上下文词的负面词数,如果数据集很小,则为
我们仅训练 softmax 函数的
如何选择负例? :
如果使用第一个样本分布,则可能总是选择诸如the, of
等之类的词。但是,如果使用第三个分布,则所选词将是非代表性的。 因此,第二分布可以被认为是用于采样的更好的分布。 这种分布在第一个和第三个之间。
如果我们检查
预训练双向语言模型
正向语言模型:给定
反向语言模型:类似地,
双向语言模型:它结合了正向和反向语言模型。 共同最大化正向和后向的似然:
LSTM 用于建模前向和后向语言模型。
就输入嵌入而言,我们可以只初始化这些嵌入或使用预先训练的嵌入。 对于 ELMo,通过使用字符嵌入和卷积层,会更加复杂,如下所示。
训练了语言模型之后,我们可以得到句子中单词的 ELMo 嵌入:
在 ELMo 中,
参考:
[2] http://jalammar.github.io/illustrated-bert/
[3] https://www.mihaileric.com/posts/deep-contextualized-word-representations-elmo/
任务是将一个序列转换为另一个序列。 这两个序列可以具有不同的长度。
使用序列对模型进行序列化在机器翻译中很流行。 如图所示,翻译是逐个标记地生成的。 问题之一是如何挑选最可能的整个句子? 贪婪搜索不起作用(即在每个步骤中选择最佳单词)。 集束搜索是一种更好的解决方案。
让我们假设集束搜索宽度为 3。因此,在每一步中,我们只保留了前 3 个最佳预测序列。
例如(如上图所示),
- 在第 1 步中,我们保留
in, June, September
- 在第 2 步中,我们保留以下顺序:
(in, September), (June is), (June visits)
- …
至于集束搜索宽度,如果我们有一个较大的宽度,我们可以获得更好的结果,但是这会使模型变慢。 另一方面,如果宽度较小,则模型会更快,但可能会损害其性能。 集束搜索宽度是一个超参数,最佳值可能是领域特定的。
翻译模型的学习将最大化:
在对数空间中,即:
上述目标函数的问题在于对数空间中的分数为 始终为负,因此使用此函数将使模型偏向一个很短的句子。 我们不希望翻译实际上太短。
在调整模型的参数时,我们需要确定它们的优先级(即,更应该归咎于 RNN 或集束搜索部分)。 (通常增加集束搜索宽度不会损害性能)。
示例
从开发集中选择一个句子并检查我们的模型:
句子:Jane visite l’Afrique en septembre.
来自人类的翻译:Jane visits Africa in September.
(
算法的输出(我们的模型):Jane visited Africa last September.
(
为了弄清楚应该归咎于哪个,我们需要根据 RNN 神经网络计算并比较
RNN 预测
通过在开发集中的多个实例上重复上述错误分析过程,我们可以得到下表:
根据该表,我们可以找出是由于集束搜索/ RNN。
如果大多数错误是由于集束搜索造成的,请尝试增加集束搜索宽度。 否则,我们可能会尝试使 RNN 更深入/添加正则化/获取更多训练数据/尝试不同的架构。
如果一个句子有多个出色的答案/推荐,我们可以使用 Bleu 得分来衡量模型的准确性。
示例(二元组的 Bleu 得分):
法语: Le chat est sur le tapis.
参考 1:The cat is on the mat.
参考 2:There is a cat on the mat.
我们模型的输出:The cat the cat on the cat.
计数是输出中出现的当前二元组的数量。 截断计数是二元组出现在参考 1 或参考 2 中的最大次数。
然后可以将二元组的 Bleu 分数计算为:
上面的等式可以用来计算 unigram,bigram 或 any-gram Bleu 分数。
合并的 Bleu 分数合并了不同 Gram 的分数。
简短的惩罚会惩罚简短的翻译。 (我们不希望翻译得太短,因为简短的翻译会带来很高的精度。
RNN(例如 lstm)的一个问题是很难记住超长句子。 模型翻译质量将随着原始句子长度的增加而降低。
有多种计算注意力的方法。 一种方法是:
在这种方法中,我们使用小型神经网络将之前和当前的信息映射到注意力权重。
已经证明注意力模型可以很好地工作,例如归一化。
架构:
详细信息:
输入嵌入:
模型的输入嵌入是单词嵌入及其每个单词的位置编码的总和。 例如,对于输入句子
解码器
- 顶部编码器的输出将转换为注意力向量
和 。 这些用于多头注意力子层(也称为编解码器注意力)。 注意力向量可以帮助解码器专注于输入句子的有用位置。 - 掩码的自注意只允许专注于输出句子的较早位置。 因此,通过在 softmax 步骤之前将它们设置为-inf。
- “多头注意”层类似于编码器中的“自注意”层,除了:
参考: https://jalammar.github.io/illustrated-transformer/
BERT 是通过堆叠转换器编码器构建的。
对未标记的大文本进行预训练(预测被掩盖的单词)
“带掩码的语言模型会随机掩盖输入中的某些标记,目的是为了 仅根据上下文来预测被屏蔽单词的原始词汇 ID。” [2]
使用受监督的训练对特定任务(例如, 分类任务,NER 等
BERT 论文的下图显示了如何将模型用于不同的任务。
如果特定任务不是分类任务,则可以忽略[CLS]。
参考:
[1] http://jalammar.github.io/illustrated-bert/
[2] Devlin, J., Chang, M.W., Lee, K. and Toutanova, K., 2018. Bert: Pre-training of deep bidirectional transformers for language understanding. arXiv preprint arXiv:1810.04805.
- 通常,我们将数据集的 70% 用作训练数据,将 30%用作测试集; 或 60%(训练)/ 20%(开发)/ 20%(测试)。 但是,如果我们有一个大数据集,则可以将大多数实例用作训练数据(例如 1,000,000, 98%),并使开发和测试集的大小相等(例如 10,000( 1% 用于开发),测试集使用 10,000( 1%)。 由于我们的数据集很大,因此开发和测试集中的 10,000 个示例绰绰有余。
- 确保开发和测试集来自同一分布
我们可能遇到的另一种情况:
1)我们要为特定域构建系统,但是在该域中我们只有几个标记数据集(例如 10,000)
2)我们 可以从类似的任务中获得更大的数据集(例如 200,000 个实例)。
在这种情况下,如何构建训练,开发和测试集?
最简单的方法是将两个数据集组合在一起并对其进行随机排序。 然后,我们可以将合并的数据集分为三个部分(训练,开发和测试集)。 但是,这不是一个好主意。 因为我们的目标是为我们自己的特定领域构建系统。 将没有来自我们自己域的一些实例添加到开发/测试数据集中以评估我们的系统是没有意义的。
合理的方法是:
1)将所有更容易使用的实例(例如 200,000)添加到训练集中
2)从特定域数据集中选择一些实例并将它们添加到训练集中
3)将我们自己领域的其余实例分为开发和测试集
对于分类任务,人类分类误差应该在 0%左右。 监督模型在训练和开发集上的各种可能的表现分析如下所示。
人为错误 | 0.9% | 0.9% | 0.9% | 0.9% |
训练集错误 | 1% | 15% | 15% | 0.5% |
测试集错误 | 11% | 16% | 30% | 1% |
评价 | 过拟合 | 欠拟合 | 欠拟合 | 良好 |
高方差 | 高偏差 | 高偏差和方差 | 低偏差和方差 |
解决方案:
您可能已经注意到,在上表中,人为水平的误差设置为 0.9%,如果人为水平的表现不同但训练/开发误差相同,该怎么办?
人为错误 | 1% | 7.5% |
训练集错误 | 8% | 8% |
测试集错误 | 10% | 10% |
评价 | 高偏差 | 高方差 |
尽管模型误差相同,但在左图中人为误差为 1%时,我们有高偏差问题,而在右图中有高方差问题。
至于模型的性能,有时它可能比人类的模型更好。 但是只要模型的性能不如人类,我们就可以:
1)从人类获得更多的标记数据
2)从手动误差分析中获得见解
3)从偏差/方差分析中获得见解
当我们为自己的特定领域构建系统时,针对我们自己的问题,我们只有几个带标签的实例(例如 10,000 个)。 但是我们很容易从另一个类似的领域中收集很多实例(例如 200,000 个)。 此外,大量容易获得的实例可能有助于训练一个好的模型。 数据集可能看起来像这样:
但是在这种情况下,训练集的数据分布与开发/测试集不同。 这可能会导致副作用-数据不匹配问题。
为了检查我们是否存在数据不匹配问题,我们应该随机选择训练集的一个子集作为名为训练-开发数据集的验证集。 该集合具有相同的训练集分布,但不会用于训练。
人为误差 | 0% | 0% | 0% | 0% |
训练误差 | 1% | 1% | 10% | 10% |
训练-开发误差 | 9% | 1.5% | 11% | 11% |
开发误差 | 10% | 10% | 12% | 20% |
问题 | 高方差 | 数据不匹配 | 高偏差 | 高偏差+数据不匹配 |
总结一下:
首先,进行手动错误分析以尝试了解我们的训练集和开发/测试集之间的区别。
其次,根据分析结果,我们可以尝试使训练实例与开发/测试实例更相似。 我们还可以尝试收集更多与开发/测试集的数据分布相似的训练数据。
我们有一个包含
使用输入标准化可以使训练更快。
假设输入是二维
如果我们不仅关心模型的表现(例如准确性,F 分数等),还关心运行时间,则可以设计一个数字评估指标来评估我们的模型。
另外,我们还可以指定可以接受的最大运行时间:
进行错误分析对于确定改善模型性能的后续步骤的优先级非常有帮助。
例如,为了找出模型为什么错误标记某些实例的原因,我们可以从开发集中获取大约 100 个错误标记的示例并进行错误分析(手动逐个检查)。
图片 | 狗 | 猫 | 模糊 | 评价 |
---|---|---|---|---|
1 | ![]() ![]() |
|||
2 | ![]() ![]() |
|||
3 | ![]() ![]() |
![]() ![]() |
||
… | … | … | … | … |
百分比 | 8% | 43% | 61% |
通过手动检查这些标签错误的实例,我们可以估计错误的出处。 例如,在上述表格中,我们发现 61%的图像模糊,因此在下一步中,我们可以集中精力改善模糊图像的识别性能。
有时,我们的数据集很嘈杂。 换句话说,数据集中存在一些不正确的标签。 同样,我们可以从开发/测试集中选取约 100 个实例,然后手动逐个检查它们。
例如,当前开发/测试集上的模型错误为 10%。 然后,我们手动检查从开发/测试集中随机选择的 100 个实例。
图片 | 标签不正确 |
---|---|
1 个 | |
2 | ![]() ![]() |
3 | |
4 | |
5 | ![]() ![]() |
… | … |
百分比 | 6% |
假设,最后我们发现 6%实例的标签错误。 基于此,我们可以猜测由于标签错误而导致的
因此,如果我们下一步专注于纠正标签,那么可能不是一个好主意。