Keras中自定义复杂的loss函数
By 苏剑林 | 2017-07-22 | 429834位读者 |Keras是一个搭积木式的深度学习框架,用它可以很方便且直观地搭建一些常见的深度学习模型。在tensorflow出来之前,Keras就已经几乎是当时最火的深度学习框架,以theano为后端,而如今Keras已经同时支持四种后端:theano、tensorflow、cntk、mxnet(前三种官方支持,mxnet还没整合到官方中),由此可见Keras的魅力。
Keras是很方便,然而这种方便不是没有代价的,最为人诟病之一的缺点就是灵活性较低,难以搭建一些复杂的模型。的确,Keras确实不是很适合搭建复杂的模型,但并非没有可能,而是搭建太复杂的模型所用的代码量,跟直接用tensorflow写也差不了多少。但不管怎么说,Keras其友好、方便的特性(比如那可爱的训练进度条),使得我们总有使用它的场景。这样,如何更灵活地定制Keras模型,就成为一个值得研究的课题了。这篇文章我们来关心自定义loss。
输入-输出设计 #
Keras的模型是函数式的,即有输入,也有输出,而loss即为预测值与真实值的某种误差函数。Keras本身也自带了很多loss函数,如mse、交叉熵等,直接调用即可。而要自定义loss,最自然的方法就是仿照Keras自带的loss进行改写。
比如,我们做分类问题时,经常用的就是softmax输出,然后用交叉熵作为loss。然而这种做法也有不少缺点,其中之一就是分类太自信,哪怕输入噪音,分类的结果也几乎是非1即0,这通常会导致过拟合的风险,还会使得我们在实际应用中没法很好地确定置信区间、设置阈值。因此很多时候我们也会想办法使得分类别太自信,而修改loss也是手段之一。
如果不修改loss,我们就是使用交叉熵去拟合一个one hot的分布。交叉熵的公式是
$$S(q|p)=-\sum_i q_i \log p_i$$
其中$p_i$是预测的分布,而$q_i$是真实的分布,比如输出为$[z_1,z_2,z_3]$,目标为$[1,0,0]$,那么
$$loss = -\log \Big(e^{z_1}/Z\Big),\, Z=e^{z_1}+e^{z_2}+e^{z_3}$$
只要$z_1$已经是$[z_1,z_2,z_3]$的最大值,那么我们总可以“变本加厉”——通过增大训练步数,使得$z_1,z_2,z_3$增加足够大的比例(等价地,即增大向量$[z_1,z_2,z_3]$的模长),从而$e^{z_1}/Z$足够接近1(等价地,loss足够接近0)。这就是通常softmax过于自信的来源:只要盲目增大模长,就可以降低loss,训练器肯定是很乐意了,这代价太低了。为了使得分类不至于太自信,一个方案就是不要单纯地去拟合one hot分布,分一点力气去拟合一下均匀分布,即改为新loss:
$$loss = -(1-\varepsilon)\log \Big(e^{z_1}/Z\Big)-\varepsilon\sum_{i=1}^n \frac{1}{3}\log \Big(e^{z_i}/Z\Big),\, Z=e^{z_1}+e^{z_2}+e^{z_3}$$
这样,盲目地增大比例使得$e^{z_1}/Z$接近于1,就不再是最优解了,从而可以缓解softmax过于自信的情况,不少情况下,这种策略还可以增加测试准确率(防止过拟合)。
那么,在Keras中应该怎么写呢?其实挺简单的:
from keras.layers import Input,Embedding,LSTM,Dense
from keras.models import Model
from keras import backend as K
word_size = 128
nb_features = 10000
nb_classes = 10
encode_size = 64
input = Input(shape=(None,))
embedded = Embedding(nb_features,word_size)(input)
encoder = LSTM(encode_size)(embedded)
predict = Dense(nb_classes, activation='softmax')(encoder)
def mycrossentropy(y_true, y_pred, e=0.1):
loss1 = K.categorical_crossentropy(y_true, y_pred)
loss2 = K.categorical_crossentropy(K.ones_like(y_pred)/nb_classes, y_pred)
return (1-e)*loss1 + e*loss2
model = Model(inputs=input, outputs=predict)
model.compile(optimizer='adam', loss=mycrossentropy)
也就是自定义一个输入为y_pred,y_true的loss函数,放进模型compile即可。这里的mycrossentropy,第一项就是普通的交叉熵,第二项中,先通过K.ones_like(y_pred)/nb_classes构造了一个均匀分布,然后算y_pred与均匀分布的交叉熵。就这么简单~
并不仅仅是输入输出那么简单 #
前面已经说了,Keras的模型有固定的输入和输出,并且loss即为预测值与真实值的某种误差函数,然而,很多模型并非这样的,比如问答模型与triplet loss。
这个的问题是指有固定的答案库的FAQ形式的问答。一种常见的做问答模型的方法就是:先分别将答案和问题都encode成为一个同样长度的向量,然后比较它们的cos值,cos越大就越匹配。这种做法很容易理解,是一个比较通用的框架,比如这里的问题和答案都不需要一定是问题,图片也行,反正只不过是encode的方法不一样,最终只要能encode出一个向量来即可。但是怎么训练呢?我们当然希望正确答案的cos值越大越好,错误答案的\cos值越小越好,但是这不是必要的,合理的要求应该是:正确答案的cos值比所有错误答案的\cos值都要大,大多少无所谓,一丁点都行。因此,这就导致了triplet loss:
$$loss = \max\Big(0, m+\cos(q,A_{\text{wrong}})-\cos(q,A_{\text{right}})\Big)$$
其中$m$是一个大于零的正数。
怎么理解这个loss呢?要注意我们要最小化loss,所以只看$m+\cos(q,A_{\text{wrong}})-\cos(q,A_{\text{right}})$这部分,我们知道目的是拉大正确与错误答案的差距,但是,一旦$\cos(q,A_{\text{right}})-\cos(q,A_{\text{wrong}}) > m$,也就是差距大于$m$时,由于$\max$的存在,loss就等于0,这时候就自动达到最小值,就不会优化它了。所以,triplet loss的思想就是:只希望正确比错误答案的差距大一点(并不是越大越好),超过$m$就别管它了,集中精力关心那些还没有拉开的样本吧!
我们已经有问题和正确答案,错误答案只要随机挑就行,所以这样训练样本是很容易构造的。不过Keras中怎么实现triplet loss呢?看上去是一个单输入、双输出的模型,但并不是那么简单,Keras中的双输出模型,只能给每个输出分别设置一个loss,然后加权求和,但这里不能简单表示成两项的加权求和。那应该要怎么搭建这样的模型呢?下面是一个例子:
from keras.layers import Input,Embedding,LSTM,Dense,Lambda
from keras.layers.merge import dot
from keras.models import Model
from keras import backend as K
word_size = 128
nb_features = 10000
nb_classes = 10
encode_size = 64
margin = 0.1
embedding = Embedding(nb_features,word_size)
lstm_encoder = LSTM(encode_size)
def encode(input):
return lstm_encoder(embedding(input))
q_input = Input(shape=(None,))
a_right = Input(shape=(None,))
a_wrong = Input(shape=(None,))
q_encoded = encode(q_input)
a_right_encoded = encode(a_right)
a_wrong_encoded = encode(a_wrong)
q_encoded = Dense(encode_size)(q_encoded) #一般的做法是,直接讲问题和答案用同样的方法encode成向量后直接匹配,但我认为这是不合理的,我认为至少经过某个变换。
right_cos = dot([q_encoded,a_right_encoded], -1, normalize=True)
wrong_cos = dot([q_encoded,a_wrong_encoded], -1, normalize=True)
loss = Lambda(lambda x: K.relu(margin+x[0]-x[1]))([wrong_cos,right_cos])
model_train = Model(inputs=[q_input,a_right,a_wrong], outputs=loss)
model_q_encoder = Model(inputs=q_input, outputs=q_encoded)
model_a_encoder = Model(inputs=a_right, outputs=a_right_encoded)
model_train.compile(optimizer='adam', loss=lambda y_true,y_pred: y_pred)
model_q_encoder.compile(optimizer='adam', loss='mse')
model_a_encoder.compile(optimizer='adam', loss='mse')
model_train.fit([q,a1,a2], y, epochs=10)
#其中q,a1,a2分别是问题、正确答案、错误答案的batch,y是任意形状为(len(q),1)的矩阵
如果第一次看不懂,那么请反复阅读几次,这个代码包含了Keras中实现最一般模型的思路:把目标当成一个输入,构成多输入模型,把loss写成一个层,作为最后的输出,搭建模型的时候,就只需要将模型的output定义为loss,而compile的时候,直接将loss设置为y_pred(因为模型的输出就是loss,所以y_pred就是loss),无视y_true,训练的时候,y_true随便扔一个符合形状的数组进去就行了。最后我们得到的是问题和答案的编码器,也就是问题和答案都分别编码出一个向量来,我们只需要比较$\cos$,就可以选择最优答案了。
Embedding层的妙用 #
在读这一段之前,请读者务必确定自己对Embedding层有清晰的认识,如果还没有,请移步阅读《词向量与Embedding究竟是怎么回事?》。这里需要反复强调的是,虽然词向量叫Word Embedding,但是,Embedding层不是词向量,跟词向量没有半毛钱关系!!!不要有“怎么就跟词向量扯上关系了”这样的傻问题,Embedding层从来就没有跟词向量有过任何直接联系(只不过在训练词向量时可以用它)。对于Embedding层,你可以有两种理解:1、是one hot输入的全连接层的加速版本,也就是说,它就是一个以one hot为输入的Dense层,数学上完全等价;2、它就是一个矩阵查找操作,输入一个整数,输出对应下标的向量,只不过这个矩阵是可训练的。(你看,哪里跟词向量有联系了?)
这部分我们来关心center loss。前面已经说了,做分类时,一般是softmax+交叉熵做,用矩阵的写法,softmax就是
$$\text{softmax}\Big(\boldsymbol{W}\boldsymbol{x}+\boldsymbol{b}\Big)$$
其中$\boldsymbol{x}$可以理解为提取的特征,而$\boldsymbol{W},\boldsymbol{b}$是最后的全连接层的权重,整个模型是一起训练的。问题是,这样的方案所训练出来的特征模型$\boldsymbol{x}$,具有怎样的形态呢?
有一些情况下,我们更关心特征$\boldsymbol{x}$而不是最后的分类结果,比如人脸识别场景,假如我们有10万个不同的人的人脸数据库,每个人有若干张照片,那么我们就可以训练一个10万分类模型,对于给定的照片,我们可以判断它是10万个中的哪一个。但这仅仅是训练场景,那么怎么应用呢?到了具体的应用环境,比如一个公司内部,可能有只有几百人;在公共安全检测场景,可能有数百万人,所以前面做好的10万分类模型基本上是没有意义的,但是在这个模型softmax之前的特征,也就是前一段所说的$\boldsymbol{x}$,可能还是很有意义的。如果对于同一个人(也就是同一类),$\boldsymbol{x}$基本一样,那么实际应用中,我们就可以把训练好的模型当作特征提取工具,然后把提取出来的特征直接用KNN(最邻近距离)来做就行了。
设想很美好,但事实很残酷,直接训练softmax的话,事实上得到的特征不一定具有聚类特性,相反,它们会尽量布满整个空间(没有给其他人留出位置,参考center loss的相关论文和文章,比如这篇。)。那么,怎样训练才使得结果有聚类特性呢?center loss使用了一种简单粗暴但是却很有效的方案——加聚类惩罚项。完整地写出来,就是
$$loss = - \log\frac{e^{\boldsymbol{W}_y^{\top}\boldsymbol{x}+b_y}}{\sum\limits_i e^{\boldsymbol{W}_i^{\top}\boldsymbol{x}+b_i}} + \lambda \Big\Vert \boldsymbol{x}-\boldsymbol{c}_y \Big\Vert^2$$
其中$y$对应着正确的类别。可以看到,第一项就是普通的softmax交叉熵,第二项就是额外的惩罚项,它给每个类定义了可训练的中心$\boldsymbol{c}$,要求每个类要跟各自的中心靠得很近。所以,总的来说,第一项负责拉开不同类之间的距离,第二项负责缩小同一类之间的距离。
那么,Keras中要怎么实现这个方案?关键是,怎么存放聚类中心?答案就是Embedding层!这部分的开头已经提示了,Embedding就是一个待训练的矩阵罢了,正好可以存放聚类中心参数。于是,模仿第二部分的写法,就得到
from keras.layers import Input,Conv2D, MaxPooling2D,Flatten,Dense,Embedding,Lambda
from keras.models import Model
from keras import backend as K
nb_classes = 100
feature_size = 32
input_image = Input(shape=(224,224,3))
cnn = Conv2D(10, (2,2))(input_image)
cnn = MaxPooling2D((2,2))(cnn)
cnn = Flatten()(cnn)
feature = Dense(feature_size, activation='relu')(cnn)
predict = Dense(nb_classes, activation='softmax', name='softmax')(feature) #至此,得到一个常规的softmax分类模型
input_target = Input(shape=(1,))
centers = Embedding(nb_classes, feature_size)(input_target) #Embedding层用来存放中心
l2_loss = Lambda(lambda x: K.sum(K.square(x[0]-x[1][:,0]), 1, keepdims=True), name='l2_loss')([feature,centers])
model_train = Model(inputs=[input_image,input_target], outputs=[predict,l2_loss])
model_train.compile(optimizer='adam', loss=['sparse_categorical_crossentropy',lambda y_true,y_pred: y_pred], loss_weights=[1.,0.2], metrics={'softmax':'accuracy'})
model_predict = Model(inputs=input_image, outputs=predict)
model_predict.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model_train.fit([train_images,train_targets], [train_targets,random_y], epochs=10)
#TIPS:这里用的是sparse交叉熵,这样我们直接输入整数的类别编号作为目标,而不用转成one hot形式。所以Embedding层的输入,跟softmax的目标,都是train_targets,都是类别编号,而random_y是任意形状为(len(train_images),1)的矩阵。
读者可能有疑问,为什么不像第二部分的triplet loss模型那样,将整体的loss写成一个单一的输出,然后搭建模型,而是要像目前这样变成双输出呢?
事实上,Keras爱好者钟情于Keras,其中一个很重要的原因就是它的进度条——能够实时显示训练loss、训练准确率。如果像第二部分那样写,那么就不能设置metrics参数,那么训练过程中就不能显示准确率了,这不能说是一个小遗憾。而目前这样写,我们就依然能够在训练过程中看到训练准确率,还能分别看到交叉熵loss、l2_loss、总的loss分别是多少,非常舒服。
Keras就是这么好玩 #
有了以上三个案例,读者应该对Keras搭建复杂模型的步骤心中有数了,应当说,也是比较简单灵活的。Keras确实有它不够灵活的地方,但也没有网上评论的那么无能。总的来说,Keras是能够满足大多数人快速实验深度学习模型的需求的。如果你还在纠结深度学习框架的选择,那么请选择Keras吧——当你真正觉得Keras不能满足你的需求时,你已经有能力驾驭任何框架了,也就没有这个纠结了。
转载到请包括本文地址:https://kexue.fm/archives/4493
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jul. 22, 2017). 《Keras中自定义复杂的loss函数 》[Blog post]. Retrieved from https://kexue.fm/archives/4493
@online{kexuefm-4493,
title={Keras中自定义复杂的loss函数},
author={苏剑林},
year={2017},
month={Jul},
url={\url{https://kexue.fm/archives/4493}},
}
October 4th, 2019
请教博主,我在用LeNet训练cifar10的时候将model.compile()中的loss由原来的loss='sparse_categorical_crossentropy'改为下面的函数,为什么效果差别很大,改了之后的model训练虽然loss一直下降,但是accuracy一直停滞不前
def mycrossentropy(y_true, y_pred):
return K.categorical_crossentropy(y_true, y_pred)
如果是内置的loss,模型会自定识别是哪种accuracy计算函数。但是你这属于自定义loss,所以需要自己指定或者重写accuracy计算函数,你这种情况下要用sparse_categorical_accuracy(https://keras.io/metrics/#sparse_categorical_accuracy)
October 10th, 2019
博主您好!
请教您一个自定义loss函数的问题,我现在有一个模型的输出为:
model = Model(inputs=[refer_audio_input, clean_audio_input, noisy_audio_input],
outputs=[clean_audio_feature, masked_spectrogram])
clean_audio_feature和masked_spectrogram的维度均为batch_size*freq_len*time_len
我的loss是想要定义一个比较复杂的函数,类似|a^0.3|-|b^0.3|的平方和+const*|a|^0.3-|b|^0.3的平方和
因此我在定义loss函数时,设置get_loss(y_true, y_pred, c=0.113),y_true按照您说的设置的没啥用的矩阵。在我的预想中,clean_audio_feature, masked_spectrogram=y_pred[0],y_pred[1]然后按照上面的式子进行操作就行了,但是y_pred[1]却什么操作都进行不了。我在尝试中如果return K.mean(y_pred[0])可以进行训练,但是return K.mean(y_pred[1])就会报错。请问这种情况有办法进行操作吗?还是keras并不支持拆包
Keras的设计是:有多少个输出就有多少个loss,每个loss是独立的,不能交互,要交互的话放到keras层里边去,或者是用add_loss方法:
https://kexue.fm/archives/6311#%E5%8F%AF%E4%BB%A5%E4%B8%8D%E8%A6%81%E8%BE%93%E5%87%BA
December 23rd, 2019
博主您好!
感谢您的博文,读过之后有启发。
我想请问,交叉熵改进之后的loss:不要单纯地去拟合one hot分布,分一点力气去拟合一下均匀分布。——这里拟合均匀分布的道理是什么呢?我的意思是:softmax过于自信这个解释ok,但是为什么部分拟合均匀分布会有更好的效果呢?
您看我是否理解错误呢?假设ground truth = [1, 0, 0];Softmax = [0.8, 0.1, 0.1]。Softmax只是让 0.8 -> 1。而引入均匀分布,同时考虑 0.8 -> 1, 0.1 -> 0。请问我理解的有偏差吗?
还是,请问这个做法有相应的论文发表吗?我还想知道,修改了loss之后对抗过拟合的效果到底能有多好。
感激不尽!
理解还是有点偏差。加上均匀分布后,事实上就相当于将ground truth从[1, 0, 0]变成了[0.9, 0.05, 0.05]而已。
March 31st, 2020
博主,您好,很感谢您详细的总结,现在有个问题请教,自定义了loss函数,出现了如下问题:ValueError: An operation has `None` for gradient. Please make sure that all of your ops have a gradient defined (i.e. are differentiable). Common ops without gradient: K.argmax, K.round, K.eval.
您有遇到过吗,是什么问题呢?
错误信息已经把原因说清楚了
June 14th, 2020
苏神,您好!谢谢您的分享,对我产生了很积极的作用,但是我注意到一点点小小的问题,不知道是不是我理解错误,还希望您指正。
第一个代码部分的两个参数:
loss2 = K.categorical_crossentropy(K.ones_like(y_pred)/nb_classes, y_pred),
是不是应该换成:
loss2 = K.categorical_crossentropy(y_true, K.ones_like(y_pred)/nb_classes).
确实是你理解上的错误,只要稍微思考一下就能知道后一种写法必然是错的。
y_true跟参数没关系,相当于常数,K.ones_like(y_pred)/nb_classes也是常数,所以 K.categorical_crossentropy(y_true, K.ones_like(y_pred)/nb_classes)也是常数,添加一个常数到loss里边去,为了什么?
谢谢回复,理解了...我错误地将K.ones_like(y_pred)/nb_classes看成是与y_pred相关的变量,即y_pred的均值,忽略了one_like()的作用。
感谢解答。
June 17th, 2020
苏神您好!
我有两个小问题咨询您一下:
我看了center loss的原文,原文是不停的通过mini-batch的中的同类别feature去更新center,您给的代码我理解是通过设定Embedding去学习这个Center,这样二者会差距大吗?
还有一个问题,我在做无关样本剔除的工作,发现通过center loss效果有提升,我认为是聚类效应有助于后续的one-class classifier学习到规律,我看了您提到的那个博客,他有一句“該画风的满足的条件就一个,那10W海洋球兄弟不允许吃胖。这样剩下来的空间就能够给他们每人一个大院子,这样一旦有新的朋友(49亿多个兄弟)来玩耍的时候,不至于撞得他们老妈都分不清楚是谁(至少从概率上极大的降低了)”,这有什么理论依据吗?您怎么看?
先行感谢!
1、差距大不大我也不知道,不过目前的方式感觉也work得还可以吧~
2、感觉上是三角不等式来保证吧:假如$\boldsymbol{x}, \boldsymbol{y}$是两个同类样本,$\boldsymbol{c}$是类中心,那么
$$\Vert \boldsymbol{x} - \boldsymbol{y}\Vert = \Vert \boldsymbol{x} - \boldsymbol{c} + \boldsymbol{c} - \boldsymbol{y}\Vert \leq \Vert \boldsymbol{x} - \boldsymbol{c}\Vert + \Vert\boldsymbol{c} - \boldsymbol{y}\Vert$$
只要$\Vert \boldsymbol{x} - \boldsymbol{c}\Vert, \Vert\boldsymbol{c} - \boldsymbol{y}\Vert$都足够小,就可以保证$\Vert \boldsymbol{x} - \boldsymbol{y}\Vert$足够小,也就是类内差距尽可能小。
感谢回复,我再细品一下,感谢!
July 10th, 2020
感谢分享,
July 28th, 2020
苏神您好!
想请问下在您的center loss 代码中,evaluate或者fit过程,进度条会给出多个loss和acc的值,但是通过predict得到的分类标签,是通过哪个损失函数优化后得到的呢
所有损失一起优化出来的~
好的,感谢
July 28th, 2020
苏神您好:
以下是center loss 在其他数据集跑的一个epoch 结果:
Epoch 89/100
30000/30000 [==============================] - 5s 163us/step - loss: 0.5554 - dense_2_loss: 0.2586 - center_loss_layer_1_loss: 0.5935 - dense_2_acc: 0.9980 - center_loss_layer_1_acc: 0.6441 - val_loss: 1.4146 - val_dense_2_loss: 0.4038 - val_center_loss_layer_1_loss: 2.0217 - val_dense_2_acc: 0.9160 - val_center_loss_layer_1_acc: 0.3472
我可能还是不太理解,多个损失函数的具体怎么得出结果的。三个损失函数可以理解为:总损失、交叉熵损失、center loss,那么对应的两个精度:“val_dense_2_acc”,“val_center_loss_layer_1_acc”都是有意义的么,是在两个分损失函数下得到预测还是什么?最后我发现,model.predict 后的结果算出来的准确率和“val_dense_2_acc”是一样的。更有些糊涂,望指教,感谢。
只看val_dense_2_acc就好了。
November 6th, 2020
博主您好,我需要对两类不同样本采用两种不同的损失函数,请问我该怎么办呢。
我现在是所有样本都用了同一个损失,代码如下:
model.compile(
optimizer=Adam(lr=1e-3, beta_1=0.9,
loss=['mae', myCrossEntropy],
loss_weights = LOSS_WEIGHTS,
metrics={'g_module': 'mae', 'd_softmax': 'accuracy'}
)
我要问的问题是对于交叉熵损失,我的正负两种样本想用不同的两种改进后的交叉熵损失函数,不是关于网络另一个输出MAE损失的问题。
大神求助~
另外,我知道pytorch可以比较方便地实现该功能,Keras是否也提供了同样的功能呢?
就是所有样本x_train,每个epoch乱序,根据我设定的一个阈值,从含义上分为两类,一类为可信度较大的,一类为可信度较小的,正负两类样本。
参考sigmoid交叉熵的实现:- y_true * log y_pred - (1 - y_true) * log(1 - y_pred)
其中y_true是一个0/1向量。
感谢大佬,不过我想表达的是所有样本x_train从含义上分为两类,但是对于网络来说是相同的一类都装在x_train_batch一个个batch地送入网络中,这个潜在的含义我在网络外将其划分开将索引保存在列表,但并没有将这个装索引的列表传入到网络内,此时损失函数如何访问网络外面的索引来得知x_train_batch的潜在含义分组呢。我知道对于pytorch这是很容易访问以及实现的,而我的代码是用Keras写的,想知道Keras是否提供了能够实时与网络网络外的值交互的方法
你多加一个Input层,将你这个“外部”索引传入到模型不就行了?