文本情感分类(四):更好的损失函数
By 苏剑林 | 2017-03-30 | 122709位读者 |文本情感分类其实就是一个二分类问题,事实上,对于分类模型,都会存在这样一个毛病:优化目标跟考核指标不一致。通常来说,对于分类(包括多分类),我们都会采用交叉熵作为损失函数,它的来源就是最大似然估计(参考《梯度下降和EM算法:系出同源,一脉相承》)。但是,我们最后的评估目标,并非要看交叉熵有多小,而是看模型的准确率。一般来说,交叉熵很小,准确率也会很高,但这个关系并非必然的。
要平均,不一定要拔尖 #
一个更通俗的例子是:一个数学老师,在努力提高同学们的平均分,但期末考核的指标却是及格率(60分及格)。假如平均分是100分(也就意味着所有同学都考到了100分),那么自然及格率是100%,这是最理想的。但现实不一定这么美好,平均分越高,只要平均分还没有达到100,那么及格率却不一定越高,比如两个人分别考40和90,那么平均分就是65,及格率只有50%;如果两个人的成绩都是60,平均分就是60,及格率却有100%。这也就是说,平均分可以作为一个目标,但这个目标并不直接跟考核目标挂钩。
那么,为了提升最后的考核目标,这个老师应该怎么做呢?很显然,首先看看所有学生中,哪些同学已经及格了,及格的同学先不管他们,而针对不及格的同学进行补课加强,这样一来,原则上来说有很多不及格的同学都能考上60分了,也有可能一些本来及格的同学考不够60分了,但这个过程可以迭代,最终使得大家都在60分以上,当然,最终的平均分不一定很高,但没办法,谁叫考核目标是及格率呢?
更好的更新方案 #
对于二分类模型,我们总希望模型能够给正样本输出1,负样本输出0,但限于模型的拟合能力等问题,一般来说做不到这一点。而事实上在预测中,我们也是认为大于0.5的就是正样本了,小于0.5的就是负样本。这样就意味着,我们可以“有选择”地更新模型,比如,设定一个阈值为0.6,那么模型对某个正样本的输出大于0.6,我就不根据这个样本来更新模型了,模型对某个负样本的输出小于0.4,我也不根据这个样本来更新模型了,只有在0.4~0.6之间的,才让模型更新,这时候模型会更“集中精力”去关心那些“模凌两可”的样本,从而使得分类效果更好,这跟传统的SVM思想是一致的。
不仅如此,这样的做法理论上还能防止过拟合,因为它防止了模型专门挑那些容易拟合的样本来“拼命”拟合(使得损失函数下降),这就好比老师只关心优生,希望优生能从80分提高到90分,而不想办法提高差生的成绩,这显然不是一个好老师。
修正的交叉熵损失 #
怎样才能达到我们上面说的目的呢?很简单,调整损失函数即可,这里主要借鉴了hinge loss和triplet loss的思想。一般常用的交叉熵损失函数是:
$$L_{old} = -\sum_y y_{true} \log y_{pred}$$
选定一个阈值$m=0.6$,这个阈值原则上大于0.5均可。引入单位阶跃函数$\theta(x)$:
$$\theta(x) = \left\{\begin{aligned}&1, x > 0\\
&\frac{1}{2}, x = 0\\
&0, x < 0\end{aligned}\right.$$
那么,考虑新的损失函数:
$$L_{new} = -\sum_y \lambda(y_{true}, y_{pred}) y_{true}\log y_{pred}$$
其中
$$\lambda(y_{true}, y_{pred}) = 1-\theta(y_{true}-m)\theta(y_{pred}-m)-\theta(1-m-y_{true})\theta(1-m-y_{pred})$$
$L_{new}$就是在交叉熵的基础上加入了修正项$\lambda(y_{true}, y_{pred})$,这一项意味着什么呢?当进入一个正样本时,那么$y_{true}=1$,显然
$$\lambda(1, y_{pred})=1-\theta(y_{pred}-m)$$
这时候,要是$y_{pred} > m$,那么$\lambda(1, y_{pred})=0$,这时候交叉熵自动为0(达到最小值),反之,$y_{pred} < m$则有$\lambda(1, y_{pred})=1$,这时候保持交叉熵,也就是说,正样本如果输出已经大于$m$了,那就不更新了(因为达到了最小值,可以认为最小值梯度是0),小于$m$才继续更新;类似地可以分析负样本的情形,结论是负样本如果输出已经小于$1-m$了,那就不更新了,大于$1-m$才继续更新。
这样一来,只要将原始的交叉熵损失,换成修正的交叉熵$L_{new}$,就可以达到我们开始设计的目的了。
基于IMDB的实验测试 #
理论看上去很美好,实测效果是不是如想象一样呢?马上来实验。
为了使得结果更具有对比性,这次我选择了文本情感分类中的一个标准任务:IMDB电影评论的分类来进行评测,使用的工具是Keras最新版(2.0)。大部分的代码在Keras的examples中都可以找到,LSTM和CNN等等都有。
首先是LSTM的:
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import Embedding, LSTM, Dense
from keras.datasets import imdb
from keras import backend as K
margin = 0.6
theta = lambda t: (K.sign(t)+1.)/2.
max_features = 20000
maxlen = 80
batch_size = 32
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
model = Sequential()
model.add(Embedding(max_features, 128))
model.add(LSTM(128, dropout=0.2, recurrent_dropout=0.2))
model.add(Dense(1, activation='sigmoid'))
def loss(y_true, y_pred):
return - (1 - theta(y_true - margin) * theta(y_pred - margin)
- theta(1 - margin - y_true) * theta(1 - margin - y_pred)
) * (y_true * K.log(y_pred + 1e-8) + (1 - y_true) * K.log(1 - y_pred + 1e-8))
model.compile(loss=loss,
optimizer='adam',
metrics=['accuracy'])
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=15,
validation_data=(x_test, y_test))
代码基本上照搬官方的,不解释~运行结束后,模型得到了99.01%的训练准确率和82.26%的测试准确率,如果把loss直接改为binary_crossentropy(其它都不改变),那么得到99.56%的训练准确率和81.02%的测试准确率,说明新的loss函数确实有助于防止过拟合,提升准确率。当然,这个测试可能会有随机误差,但多次平均的结果仍然显示,新的loss函数大概能带来0.5%~1%的准确率提升(当然,仅仅稍微改变了loss函数,你不能指望有跨越性的提升。)。
再看看CNN的:
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import Embedding, Dense, Dropout, Activation
from keras.layers import Conv1D, GlobalMaxPooling1D
from keras.datasets import imdb
from keras import backend as K
margin = 0.6
theta = lambda t: (K.sign(t)+1.)/2.
max_features = 5000
maxlen = 400
batch_size = 32
embedding_dims = 50
filters = 250
kernel_size = 3
hidden_dims = 250
epochs = 10
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
model = Sequential()
model.add(Embedding(max_features,
embedding_dims,
input_length=maxlen))
model.add(Dropout(0.2))
model.add(Conv1D(filters,
kernel_size,
padding='valid',
activation='relu',
strides=1))
model.add(GlobalMaxPooling1D())
model.add(Dense(hidden_dims))
model.add(Dropout(0.2))
model.add(Activation('relu'))
model.add(Dense(1))
model.add(Activation('sigmoid'))
def loss(y_true, y_pred):
return - (1 - theta(y_true - margin) * theta(y_pred - margin)
- theta(1 - margin - y_true) * theta(1 - margin - y_pred)
) * (y_true * K.log(y_pred + 1e-8) + (1 - y_true) * K.log(1 - y_pred + 1e-8))
model.compile(loss=loss,
optimizer='adam',
metrics=['accuracy'])
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(x_test, y_test))
运行结束后,模型得到了98.66%的训练准确率和88.24%的测试准确率,纯粹的binary_crossentropy的结果为98.90%的训练准确率和88.14%的测试准确率,两个结果基本一致,在波动范围内。但是,在训练过程中,用新loss函数的测试结果,一直保持在88.2%左右不变,而使用交叉熵时,一会跳到89%,一会跳到87%,一会又跳回88%,也就是说,虽然最终大家的准确率一致,但用交叉熵的时候,波动更大,我们有理由相信,新loss函数训练出来的模型,泛化能力更加好。
简而言之 #
本文主要借鉴了hinge loss和triplet loss的思想,对二分类所用的交叉熵损失做了一个调整,使得它更加有效地去拟合预测错误的样本,实验也表明某种意义上新的损失函数确实能带来一点小提升。
另外,这种思想事实上还可以用于多分类甚至回归问题,这里就不详细举例了,只能是遇到分析再跟大家分享了。
转载到请包括本文地址:https://kexue.fm/archives/4293
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Mar. 30, 2017). 《文本情感分类(四):更好的损失函数 》[Blog post]. Retrieved from https://kexue.fm/archives/4293
@online{kexuefm-4293,
title={文本情感分类(四):更好的损失函数},
author={苏剑林},
year={2017},
month={Mar},
url={\url{https://kexue.fm/archives/4293}},
}
April 5th, 2017
一直在看苏神的文章,今天特意注册帐号来支持下,赞。
August 20th, 2017
苏神你好,文中的“一般来说,交叉熵很小,准确率也会很高,但这个关系并非必然的。”我有点不太明白,我的理解是 根据损失函数的定义,交叉熵变小,损失函数变小,准确率肯定变高,后边的实验中之所以测试准确率波动大,是由于adam算法逼近时交叉熵波动大,损失函数波动大。采用这种调整的交叉熵的好处是可以平稳的进行逼近,但不会改变交叉熵与准确率的关系。不知道我的理解是否正确?
不正确。
考虑二分类,并假设只有两个样本,标签分别是0、1。第一种情况,预测值分别是0.48、0.51,那么交叉熵是-log(1-0.48)-log(0.51)=1.407,这时候正确率为100%(负样本预测值小于0.5就算正确,正样本预测值大于0.5就算正确);第二种情况,预测值分别是0.51、1,这时候交叉熵为-log(1-0.51)-log(1)=0.713,小于第一种情况,但正确率只有50%
September 2nd, 2017
苏神,请问下这个思想有没有caffe实现的code?
我不会caffe,但理论上任意框架实现起来都很容易呀
我太菜了,您有没有推导多类情况下的公式?
这已经是多类下的公式了呀! ytrue是one-hot编码里对应位置的1,其他都是0,这个公式不用改就是对应多分类交叉熵的
感谢帮忙解答。
哈哈哈,3年后得到了回复。最近,文本分类老是遇到某类精确率要高的需求。一直在改loss权重。把苏神loss有关文章都看了。没想到17年苏神就在研究这些了
其实改来改去,最终可能也没啥提升,归根结底,还是只能靠更多数据来解决。
算是说了句大实话...
March 9th, 2018
您好,想问一下,基于交叉熵修改后的loss_new和直接用hinge_loss相比有什么优势呢?按照我的理解,对于容易过拟合的样本,直接用hinge loss效果会更好。
对于情感分类问题或者一般的文本分类问题,可能没什么差别,甚至可能hinge loss更好。
loss_new可能的好处是,当预测值没有达到要求时,等价于交叉熵,交叉熵的收敛速度比hinge loss快,而收敛速度对于复杂问题(比如序列标注)是比较重要的。
本文的升级版本是focal loss,很多时候focal loss更好。
显示的梯度计算和实验都表明,hinge loss收敛速度比交叉熵快,楼主这里的表达是不是不太精准;不过focal loss确实更好,对于不同任务可能效果不同。
June 6th, 2018
苏先生,请问一下。
用于多分类,直接将你写的loss_new 拿来用,可以不?谢谢
思想可以用,但是loss_new不能用,因为loss_new只是二分类的
December 17th, 2018
苏神,为啥我用keras的IMDB的效果为CNN>CNN_LSTM>LSTM,用中文数据跑也一样,不应该是LSTM效果好于CNN么
“不应该是LSTM效果好于CNN么”
哪条法律规定的“应该”?
文本分类CNN好太正常了,而且可能是最好的baseline。
June 4th, 2020
你好,我想问一下,一个batch中最后会对loss取均值,然后反向传播,这样取均值之后,没法具体到对应样本,前面所做的操作不是白做了吗?
不明白你想表达什么。你不是先算好每个样本的loss,然后处理好每个样本的loss,最后才取平均的么?
嗯嗯 是我理解错了
June 4th, 2020
麻烦再问一下 x=0时 theta(x)=0.5是出于什么考虑呢?
这是$\theta(x)$的通用定义,这里只是为了跟标准保持一致罢了。事实上,从浮点数的角度来看,出现严格等于0的概率为0,所以在原点处的定义理论上并不重要。
好的 明白了 非常感谢
July 15th, 2020
感觉kaiming的focal loss应该是这里改进loss的平滑版本, 只不过focal loss已经是18年的产物了, 笔者, 赞
是的,跟focal loss差不多思想吧。
February 1st, 2022
[...]Text Emotion Classification (IV): Better loss function[...]