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的分布。交叉熵的公式是
其中pipi是预测的分布,而qiqi是真实的分布,比如输出为[z1,z2,z3][z1,z2,z3],目标为[1,0,0][1,0,0],那么
ֻҪz1z1已经是[z1,z2,z3][z1,z2,z3]的最大值,那么我们总可以“变本加厉”――通过增大训练参数,使得z1,z2,z3z1,z2,z3增加足够大的比例(等价地,即增大向量[z1,z2,z3][z1,z2,z3]的模长),从而ez1/Zez1/Z足够接近1(等价地,loss足够接近0)。这就是通常softmax过于自信的来源:只要盲目增大模长,就可以降低loss,训练器肯定是很乐意了,这代价太低了。为了使得分类不至于太自信,一个方案就是不要单纯地去拟合one hot分布,分一点力气去拟合一下均匀分布,即改为新loss:
这样,盲目地增大比例使得ez1/Zez1/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)(encoder) def mycrossentropy(y_true, y_pred, e=0.1): return (1-e)*K.categorical_crossentropy(y_pred,y_true) + e*K.categorical_crossentropy(y_pred, K.ones_like(y_pred)/nb_classes) 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:
其中mm是一个大于零的正数。
怎么理解这个loss呢?要注意我们要最小化loss,所以只看m+cos(q,Awrong)cos(q,Aright)这部分,我们知道目的是拉大正确与错误答案的差距,但是,一旦cos(q,Aright)cos(q,Awrong)>m,也就是差距大于mm时,由于maxmax的存在,loss就等于0,这时候就自动达到最小值,就不会优化它了。所以,triplet loss的思想就是:只希望正确比错误答案的差距大一点(并不是越大越好),超过mm就别管它了,集中精力关心那些还没有拉开的样本吧!
我们已经有问题和正确答案,错误答案只要随机挑就行,所以这样训练样本是很容易构造的。不过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