“让Keras更酷一些!”:随意的输出和灵活的归一化
By 苏剑林 | 2019-01-27 | 98690位读者 |继续“让Keras更酷一些!”系列,让Keras来得更有趣些吧~
这次围绕着Keras的loss、metric、权重和进度条进行展开。
可以不要输出 #
一般我们用Keras定义一个模型,是这样子的:
x_in = Input(shape=(784,))
x = x_in
x = Dense(100, activation='relu')(x)
x = Dense(10, activation='softmax')(x)
model = Model(x_in, x)
model.compile(loss='categorical_crossentropy ',
optimizer='adam',
metrics=['accuracy'])
model.fit(x_train, y_train, epochs=5)
这种模型就是普通的输入输出结构,然后loss是输出的运算。然而,对于比较复杂的模型,如自编码器、GAN、Seq2Seq,这种写法有时候不够方便,loss并不总只是输出的运算。幸好,比较新的Keras版本已经支持更加灵活的loss定义,比如我们可以这样写一个自编码器:
x_in = Input(shape=(784,))
x = x_in
x = Dense(100, activation='relu')(x)
x = Dense(784, activation='sigmoid')(x)
model = Model(x_in, x)
loss = K.mean((x - x_in)**2)
model.add_loss(loss)
model.compile(optimizer='adam')
model.fit(x_train, None, epochs=5)
上述写法的几个特点是:
1、compile
的时候并没有传入loss,而是在compile
之前通过另外的方式定义loss,然后通过add_loss
加入到模型中,这样可以随意写足够灵活的loss,比如这个loss可以跟中间层的某个输出有关、跟输入有关,等等。
2、fit
的时候,原来的目标数据,现在是None
,因为这种方式已经把所有的输入输出都通过Input
传递进来了。读者还可以看我之前写的Seq2Seq:《玩转Keras之seq2seq自动生成标题》,在那个例子中,读者能更充分地感觉到这种写法的便捷性。
更随意的metric #
另一种输出是训练过程中用来观察的metric。这里的metric,就是指衡量模型性能的一些指标,比如正确率、F1等,Keras内置了一些常见的metric。像开头例子的accuracy
一样,将这些metric的名字加入到model.compile
中,就可以在训练过程中动态地显示这些metric。
当然,你也可以参考Keras中内置的metric来自己定义新metric,但问题是在标准的metric定义方法中,metric是“输出层”与“目标值”之间的运算结果,而我们经常要在训练过程中观察一些特殊的量的变化过程,比如我想观察中间某一层的输出变化情况,这时候标准的metric定义就无法奏效了。
那可以怎么办呢?我们可以去看Keras的源码,去追溯它的metric相关的方法,最终我发现metric实际上定义在两个list
之中,通过修改这两个list
,我们可以非常灵活地显示需要观察的metric,比如
x_in = Input(shape=(784,))
x = x_in
x = Dense(100, activation='relu')(x)
x_h = x
x = Dense(10, activation='softmax')(x)
model = Model(x_in, x)
model.compile(loss='categorical_crossentropy ',
optimizer='adam',
metrics=['accuracy'])
# 重点来了
model.metrics_names.append('x_h_norm')
model.metrics_tensors.append(K.mean(K.sum(x_h**2, 1)))
model.fit(x_train, y_train, epochs=5)
上述代码展示了在训练过程中观察中间层的平均模长的变化情况。可以看到,主要涉及到两个list
:model.metrics_names
是metric的名称,是字符串列表;model.metrics_tensors
是metric的张量。只要在这里把你需要展示的量添加进去,就可以在训练过程中显示了。当然,要注意的是,一次性只能添加一个标量。
灵活的权重归一化 #
有时候我们需要把权重做一些约束,常见的是归一化,如L2范数归一化、谱归一化等,当然也可以是其他约束。
权重约束的实现方法一般有两种。第一种是事后处理,即在每一步梯度下降之后直接对权重进行硬处理,即
\begin{equation}\begin{aligned}&\boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \varepsilon\nabla_{\boldsymbol{\theta}}L(\boldsymbol{\theta})\\
&\boldsymbol{\theta}\leftarrow constraint(\boldsymbol{\theta})\end{aligned}\end{equation}
显然,这种处理方法是要被写在优化器的实现中的。而事实上Keras内置的就是这一种,使用方法很简单,只需要在添加层的时候,设置kernel_constraint
或bias_constraint
参数即可,详细请参考:https://keras.io/constraints/ 。
第二种是事前处理,我们希望对权重处理后,才代入后续的层进行运算,也就是说把约束作为模型的一部分,而不是作为优化器的一部分。Keras本身不提供这种方案的支持,但我们可以自行实现这个需求。
这时候Keras设计的精妙之处就充分体现出来了。在建立一个层对象的时候,Keras将它分为两个步骤:build
和call
,前者负责建立权重,后者负责进行运算。默认情况下,这两个部分是同时执行的,但是我们可以“移花接木”,让我们手动分步执行。
下面是利用这个思路实现的谱归一化(Spectral Normalization):
class SpectralNormalization:
"""层的一个包装,用来加上SN。
"""
def __init__(self, layer):
self.layer = layer
def spectral_norm(self, w, r=5):
w_shape = K.int_shape(w)
in_dim = np.prod(w_shape[:-1]).astype(int)
out_dim = w_shape[-1]
w = K.reshape(w, (in_dim, out_dim))
u = K.ones((1, in_dim))
for i in range(r):
v = K.l2_normalize(K.dot(u, w))
u = K.l2_normalize(K.dot(v, K.transpose(w)))
return K.sum(K.dot(K.dot(u, w), K.transpose(v)))
def spectral_normalization(self, w):
return w / self.spectral_norm(w)
def __call__(self, inputs):
with K.name_scope(self.layer.name):
if not self.layer.built:
input_shape = K.int_shape(inputs)
self.layer.build(input_shape)
self.layer.built = True
if self.layer._initial_weights is not None:
self.layer.set_weights(self.layer._initial_weights)
if not hasattr(self.layer, 'spectral_normalization'):
if hasattr(self.layer, 'kernel'):
self.layer.kernel = self.spectral_normalization(self.layer.kernel)
if hasattr(self.layer, 'gamma'):
self.layer.gamma = self.spectral_normalization(self.layer.gamma)
self.layer.spectral_normalization = True
return self.layer(inputs)
使用方法为
x = SpectralNormalization(Dense(100, activation='relu'))(x)
也就是定义完层之后加个SpectralNormalization
修改一下就行了。至于原理,我们只需要观察__call__
部分,首先新建立的层是built=False
的,然后我们自己手动执行build
方法,然后对原来的权重进行归一化,并赋值覆盖原来的权重,即self.layer.kernel = self.spectral_normalization(self.layer.kernel)
这一句。
调用Keras的进度条 #
最后顺便提一个比较有趣的玩意,就是Keras自带的进度条,早期就是Keras这个自带的进度条吸引了不少新用户。当然,现在来说进度条已经不是什么新鲜玩意了,Python下有很好用的进度条工具tqdm,很久之前就介绍过它:《两个惊艳的python库:tqdm和retry》。
当然,如果你更喜欢Keras进度条的样式,或者不想另外安装tqdm,那么也可以在自己的设计中调用Keras的进度条:
import time
from keras.utils import Progbar
pbar = Progbar(100)
for i in range(100):
pbar.update(i + 1)
time.sleep(0.1)
它会显示进度和剩余时间,如果要在进度条上更多内容,可以在update
的时候增加value
参数,比如
import time
from keras.utils import Progbar
pbar = Progbar(100)
for i in range(100):
pbar.update(i + 1, values=[('something', i - 10)])
time.sleep(0.1)
不过要注意的是,这里的value是会滑动平均的,因为这个进度条主要是Keras为metric设计的而已,如果你不想它滑动更新,那就
import time
from keras.utils import Progbar
pbar = Progbar(100, stateful_metrics=['something'])
for i in range(100):
pbar.update(i + 1, values=[('something', i - 10)])
time.sleep(0.1)
更多使用参数可以参考这里,或者参考源码。总的来说,功能远不如tqdm强,但是作为一个精致的工具,偶尔使用一下,还是个不错的选择。
折腾不息的Keras #
又分享了一些花式Keras技巧,希望对大家有帮助。灵活地用好Keras是一件颇有趣味的事情,Keras也许不是最好的深度学习框架,但应该是最优雅的框架(封装),而且很可能没有之一。
DL苦短,我用Keras~
转载到请包括本文地址:https://kexue.fm/archives/6311
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jan. 27, 2019). 《“让Keras更酷一些!”:随意的输出和灵活的归一化 》[Blog post]. Retrieved from https://kexue.fm/archives/6311
@online{kexuefm-6311,
title={“让Keras更酷一些!”:随意的输出和灵活的归一化},
author={苏剑林},
year={2019},
month={Jan},
url={\url{https://kexue.fm/archives/6311}},
}
January 28th, 2019
[...]苏剑林. (2019, Jan 27). 《 让Keras更酷一些!Keras模型杂谈(二) 》[Blog post]. Retrieved from https://kexue.fm/archives/6311[...]
January 28th, 2019
您好我想问问如何用keras 获得可以复现的结果,按照文档设置的种子,每次跑结果还是不同
没研究过,貌似有cudnn参与的地方,固定tensorflow和numpy的随机种子都不行。
部分GPU上運算不可復現,
若著重復現可用CPU版本+seeds設定
可以參考此影片:
https://www.youtube.com/watch?v=Ys8ofBeR2kA&t=2s
或是此notebook:
https://github.com/ageron/handson-ml/blob/master/extra_tensorflow_reproducibility.ipynb
February 1st, 2019
苏神,赞一个。有个问题想请教下:按照论文《Averaging Weights Leads to Wider Optima and Better Generalization》,我用keras训好几个模型,对其权重求了平均。这个模型有BN,我想用平均后的模型重新计算下每个BN层的统计量(runing mean和runing val),该怎么实现呀?我尝试了很多方法,都不行(统计量都保持不变)。谢谢啦~
“试了很多方法”包括哪些呢?“统计量都保持不变”是通过什么方法确认的?
因为我没有做过类似的事情,所以我需要一些参考信息~
March 28th, 2019
苏神,请教一下,用定义SpectralNormalization类这样的方式,就不用修改keras/engine/base_layer.py的Layer对象的add_weight方法了么?
看懂了,还是要修改,谢谢!
不需要。
April 22nd, 2019
请教一个问题,我在自定义损失函数的时候,需要把y_pred由tensor变成list再进行相关操作.如下操作有误:list(np.array(y_pred)).请问应该如何实现呢?
tensor不能变成list,请想其他办法。
感谢~
请问,自定义loss函数的时候,需要把tensor变为list进行相关操作。不能直接list(np.array(y_pred))实现。
可以利用tf.py_func(my_func,[y_true, y_pred],[tf.float32]) 来实现相关操作吗?也就是,自定义的my_func()函数中,对y_pred,和y_true由tensor变为array,进行相关操作。
不能也不应该用“把tensor变为list”这个操作。
我从来没有见过要定义什么loss还必须先转换为list的。
请好好学习keras或tf的张量化写法,不要只想着python的循环编程。
December 30th, 2019
不知道自己理解的对不对,如果拿最简单的约束权重的模为1的话,如果就采用kernel_constraint这种方式的话,是不是只有第一次算事后处理,从第二次开始可以算作事前了,因为第二次的权重已经通过第一次的事后处理变为模为1了,可以这么理解吗
事前事后不是这样区分的...
事前相当于修改的loss,所以是会改变梯度方向的..
January 10th, 2020
关于第一个,tf2真的要人命…我也想监控一个tensor然后好像api被去掉了?
最新版支持model.add_metric(metric_tensor, metric_name)的方法
September 25th, 2020
谱归一化那里求教,事后是指:计算梯度、优化参数、权重归一化。那么,事前是指“权重归一化、计算梯度、优化参数”还是“计算梯度、权重归一化、优化参数”呢?
啊。。是不是这样的:事前相当于把谱归一化作为网络的一个层,每次卷积等操作之后都把权重归一化一下,再继续后续层的操作?
“权重归一化、计算梯度、优化参数”
December 23rd, 2020
首先感谢大佬,从文章里收益良多。
请问一下,如果我的loss有两个,一个mmd_loss和中间层的输出有关,另一个还是传统的分类误差。前者可以用add_loss解决,但是后者似乎不行,因为涉及到不属于模型输入输出的y_true了。请问下这种该怎么结合呢?
传统的分类误差为什么“不属于模型输入输出的y_true”?
感谢解答,茅塞顿开。直接在模型中把y_ture放输入层就行了。之前被keras定式的fit(x_train,y_train)搞得思维僵化了。
July 27th, 2021
但是把y_true放在输入层的话,只在训练的时候有y_true,但是实际预测的时候并没有y_true啊
所以你建立另一个权重共享的模型预测不行吗~