使用 TensorFlow with Keras,按照《Python 深度学习》(《Deep Learning with Python》)文本和序列处理那章,使用一维卷积Conv1D进行IMBD电影评论情感分类,下面是书里的代码:
结果,运行的时候,出现了训练误差和精度、验证误差和精度都保持不变的情况:
我惊呆了!训练误差一直是 7.7364,训练精度一直是0.4985,验证误差一直是7.6168,验证精度一直是 0.5062 。你说你要是上升,或者下降又上升,或者 xxxx ,怎么着都比这种固定不动的情况好理解啊,这 xxxx。
既不是过拟合,也不是性能不佳,不是学习率太大或太小,也不是网络层次复杂,更不是训练过程中 batch大小、epoch数或者validation 数据的比例。。。
最后的最后,终于让我找到了原因,并且引出了一个大问题,这波很重要~~
原因:二至交叉熵损失函数 。
注意,模型 compile 的时候,书里的代码传入的是 loss='binary_crossentropy',这是以字符串形式传入的损失函数,经过查阅文档、百度搜索、VS Code里点来点去找模块源码,我终于明白这是个在keras 的 losses 模块里定义的一个函数(它虽然与同模块下 BinaryCrossentropy 类有相同的功能——计算二值交叉熵,但是二者相对独立地存在,而字符串指定 'binary_crossentropy' 时,指的就是前者 this function,而不是后者 that class)。
而不管是身为函数的 binary_crossentropy,还是身为类的 BinaryCrossentropy (虽然它的对象可以当作正常函数来访问,因为实现了 __call__ 这个成员方法),它们使用时都会涉及一个叫做 from_logits 的关键字参数。当 from_logits = False 时,该损失函数接收概率值作为输入,也就是说接收 [0, 1] 的数,计算损失时直接带入熵的公式,而 False 也是默认参数值;当
from_logits=True 时,该损失函数接收 logit 值作为输入(即 logistic 值,亦即 ),是个 ( - INF, INF) 的值,计算损失时,可以理解为:先计算概率 ,然后再带入交叉熵公式得到结果。
那么问题来了,书中代码构建的网络,最后一层全连接层,并没有指定使用 activation 函数,也就是说,一个线性转换的结果直接输出作为预测结果,它是一个从负无穷到正无穷的数,是个logit数值,并不是个概率值,所以不能直接当成 from=False 的 binary_crossentropy 函数参数,来计算损失,因为这样会使得 log 传入负值而没有数学意义,代码层面不会报错,则不知道使用了什么机制,可能是不予考虑,也可能是别的什么,反正没有正常计算,更没有正常 backward。所以就出现了精度、误差都是固定值的情况。
=========================================================================
可怕的是,使用书中不很正确的代码,并不会每次都重现精度、误差保持不变的情况,这是因为最后一层参数初始化随机的问题。如果运气好,那么很少会出现交叉熵的 log 计算无意义的情况,即不会有对负数求 log 的情况。阴差阳错的,也会进行梯度下降,而且还真就能提高精度。
但毕竟不严谨。
现在看来,解决办法:要么设定二值交叉熵函数的 from_logits=True,要么网络最后一层添加 activation='sigmoid' 作为非线性压缩转换。
第二种办法——给网络加上最后的 sigmoid函数,肯定是对的(这应该是书中这段代码的主要错误了,加上这个就完全没问题了)。
至于第一种办法,虽然使得 loss 计算合理了,但是仍然有问题,因为 编译的时候制定了 metrics 为 'acc',即 accuracy 精度,要知道我们输出的并不是 label,而是个连续值,所以又出现过很多其他的问题,比如 loss 不断下降,但是精度却一直保持一个固定值,这种情况也是偶尔地出现,还是依靠于网络权重的初始化,运气不好就 GG。所以参数随机初始化真是个好东西,可以弥补理论上的错误,无脑地迭代、梯度下降、参数更新,最后竟然还能学到不错的结果 :)
===============================================================
我的实验怎么做的?
As mentioned above,精度、损失值固定不变是随机出现的,参数初始化的好就不会出现,为了研究这个问题,让它必然出现这种情况,我在训练开始前,使用
将最后一个全连接层的所有权重设置为 -1.0,这样,必然得到负数,必然使 log 无意义,必然出现那种情况(此时这层依然是书上的代码,没有非线性转换),然后我就换用不同的 loss 设置,一个是直接 from_logits=False 的 'binary_crossentropy',这时出现了上述的精度、误差全都不变的情况,原因很简单,数学意义错误,代码层面的捕捉异常我就没有深入考虑了;另一个是使用loss=BinaryCrossentropy(from_logits=True),这时,更有趣了,误差不断下降,但是精度像上述一样,训练时、验证时都保持一个固定值不变,原因上面也说了,网络输出的是连续值,不可能跟0 or 1作为离散标记来比对。
====================================================================
上面的都是为了满足我的好学心(好奇心)瞎做的,瞎扯的。
其实。。。
从 model 定义(架构)、optimizer的学习率、fit 时传入的超参数设置等进行考虑,逐步地(循环递归地(皮))排查,最后发现 model 的输出不太对劲,因为这是个文本分类的问题,结果输出了既不是 like 0,又不是 like 1 的输出,就知道,书里把上一章的温度检测回归模型的思路带进来了,于是:最后一个全连接(或者密集连接)层没有进行非线性的转换。
因为使用的是交叉熵损失,所以最后输出网络的时候要给个非线性变换,把输出压缩到 [0, 1]范围内,不然直接输出的数值没有意义,或者不能直接进行解释。
直接老老实实加个 activation='sigmoid'就好。
完。
抓主要矛盾,主要矛盾,主要矛盾。下次一定 :)