Press "Enter" to skip to content

让Keras更酷一些!Keras模型杂谈(二)

沿着之前的 《让Keras更酷一些!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,指的是衡量模型性能的一些指标,比如正确率、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} – \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, **kwargs):
        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):
        if not self.layer.built:
            input_shape = K.int_shape(inputs)
            self.layer.build(input_shape)
            self.layer.built = True
        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也许不是最好的深度学习框架,但应该是最优雅的框架(封装),而且很可能没有之一。

 

DL苦短,我用Keras~

 


转载到请包括本文地址:
https://kexue.fm/archives/6311

 

如果您还有什幺疑惑或建议,欢迎在下方评论区继续讨论。

 

如果您觉得本文还不错,欢迎/本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!

 

如果您需要引用本文,请参考:

 

苏剑林. (2019, Jan 27). 《 让Keras更酷一些!Keras模型杂谈(二) 》[Blog post]. Retrieved from https://kexue.fm/archives/6311

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注