前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TF2下变分自编码的N种写法

TF2下变分自编码的N种写法

作者头像
代码医生工作室
发布2020-03-12 15:10:24
9260
发布2020-03-12 15:10:24
举报
文章被收录于专栏:相约机器人

在开篇之前,请允许我吐槽几段文字,发泄一下TF的不便之处。如果对这部分内容不敢兴趣请直接看正文内容。

【吐槽部分】:

在TF升级到2.x之后,带给读者更多的编码方式,同时也带给读者更多的坑。使得本来比较难用的TensorFlow变得又灵活,又难用。这使得好多TensorFlow下的深度用户,一夜之间对该框架感到陌生。

能把一个通用框架改到让原有用户都陌生的地步,这也是需要功底的。在1.x时代,一个模型的只有一种写法,规则晦涩很容易出错。在2.x时代,一个模型会变成有N种写法,而且每种写法的规则更加晦涩,写起模型来,出错率成指数增长。这使得开发人员一般会用30%的时间来实现逻辑,70%的时间来处理各种框架运行时所遇到的软件问题。大大降低了开发效率。

在《深度学习之TensorFlow:工程化项目实战》一书中,介绍了TF框架中不下于10种的子开发框架。每种都自立门户,互不兼容。随着TF2.x的到来,砍掉了好多子框架。这使得原本小范围内互不兼容的场面变成整体版本间的绝对不兼容,可以说是将不兼容属性发挥到了极致。

不过透过TF2.x的自杀式改革背后,可以看出,其希望扭转这一困境的决心。在TF2.x中,主推了2个子框架,keras与原生的动态图框架。大概这将会是TF2.x未来的使用趋势。

然而,即便是这两个子框架,自由组合起来,也可以实现n中开发方式。对用户来说,还是一样的灵活、坑多。本文就TF2.x在这两个框架下的开发,做一个系统的介绍。我们尽量不发散太多的开发方法。只针对最主流、最常用的开发方式进行介绍。也希望读者可以真正精通掌握其中的一个开发方法,至少在开发过程中,可以少一些调试框架的时间。

【正文部分】:

在《深度学习之TensorFlow:入门、原理与进阶实战》一书中,第10章介绍过变分自编码以及其在TF1.x下静态图模式的代码实现。该模型的结构相对来讲较为奇特,选用其作为例子讲解,可以触碰到更多开发中遇到的特殊情况。

为了将主流的TF2.x开发模式讲透,这里选用了与书中一样的模型和MNIST数据集。使用tf.Keras接口进行搭建模型,使用keras和动态图两种方式进行训练模型。

在学习本文之前,请先熟悉一下书中的变分自编码介绍。我们以前发表过的一篇文章<TensorFlow 2.0中实现自动编码器>

1 基础的Keras写法

先来看看最基础的keras写法

1.1 模型结构

解码器与编码器的结构代码如下:

代码语言:javascript
复制
 batch_size = 100
 original_dim = 784   #28*28
 latent_dim = 2
 intermediate_dim = 256
 nb_epoch = 50
 
 
 class Encoder(tf.keras.Model):  # 编码器
     def __init__(self ,intermediate_dim,latent_dim, **kwargs):
         super(Encoder, self).__init__(**kwargs)
         self.hidden_layer = Dense(units=intermediate_dim, activation=tf.nn.relu)
         self.z_mean = Dense(units=latent_dim)
         self.z_log_var = Dense(units=latent_dim)

     def call(self, x):
         activation = self.hidden_layer(x)
         z_mean = self.z_mean(activation)
         z_log_var= self.z_log_var(activation)
         return z_mean,z_log_var

 class Decoder(tf.keras.Model):  # 解码器
     def __init__(self ,intermediate_dim,original_dim,**kwargs):
         super(Decoder, self).__init__(**kwargs)
         self.hidden_layer = Dense(units=intermediate_dim, activation=tf.nn.relu)
         self.output_layer  = Dense(units=original_dim, activation='sigmoid')
     def call(self, z):
         activation = self.hidden_layer(z)
         output_layer = self.output_layer(activation)
         return output_layer
 def samplingfun(z_mean, z_log_var): #采样函数
     epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim),
                                     mean=0., stddev=1.0)
     return z_mean + K.exp(z_log_var / 2) * epsilon
 def sampling(args):
     z_mean, z_log_var = args
     return samplingfun(z_mean, z_log_var)
     

模型很简单只有全连接神经网络。

1.2 组合模型

定义采样器,并将编码器和解码器组合起来,形成变分自编码模型.

代码语言:javascript
复制
 encoder = Encoder(intermediate_dim,latent_dim)
 decoder = Decoder(intermediate_dim,original_dim)

 inputs = Input(batch_shape=(batch_size, original_dim))
 z_mean,z_log_var = encoder(inputs)

 z= samplingfun(z_mean, z_log_var)  
 y_pred = decoder(z)

 autoencoder = Model(inputs, y_pred, name='autoencoder')
 autoencoder.summary()

定义Input是Keras标准的使用技巧.详细介绍可以参考《深度学习之TensorFlow:工程化项目实战》一书第6章

1.3 坑1 :keras自定义模型的默认输入

如果在TF1.x中代码第1.2小节第7行会有问题,它是一个函数不能充当一个层.必须将其封装成层才行.

在TF2.x中,代码第1.2小节第7行是没问题的.但是也不正规,如果运行两次(将第1.2小节第7行代码重复一下),则会报以下错误:

这是个很难查出原因的错误.一个隐形的坑.所以最好还是用Lambda封装成一个层来使用,封装后,运行2次将不会报错.

【坑】:在使用Lambda时,被封装的函数必须只能有一个参数. 比较下面两种写法,

正确的:

代码语言:javascript
复制
z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])

错误的:

代码语言:javascript
复制
z = Lambda(samplingfun, output_shape=(latent_dim,))(z_mean, z_log_var)

错误的写法,会报如下错误:

大概意思是只向samplingfun传入了一个参数. Samplingfun没有收到z_log_var

1.4 损失函数和编译模型

用二进制交叉熵做重建损失,在配合KL散度损失,具体代码如下:

代码语言:javascript
复制
 def vae_loss(x, x_decoded_mean):
   xent_loss = original_dim * metrics.binary_crossentropy(x, x_decoded_mean)
     kl_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
         return xent_loss + kl_loss
 #编译模型
 autoencoder.compile(optimizer='rmsprop', loss=vae_loss)

其中重建损失也可以用MSE来代替。例如53行可以写成:

代码语言:javascript
复制
xent_loss = 0.5 * K.sum(K.square(x_decoded_mean - x), axis=-1)

模型必须编译才能使用

1.5 载入数据集

载入MNIST数据集

代码语言:javascript
复制
 (x_train, y_train), (x_test, y_test) = mnist.load_data(path='mnist.pkl.gz')

 x_train = x_train.astype('float32') / 255.
 x_test = x_test.astype('float32') / 255.
 x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:])))
 x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:])))

1.6 训练模型

使用keras原生的fit来训练模型.

代码语言:javascript
复制
 autoencoder.fit(x_train, x_train,
         shuffle=True,
         epochs=5,
         verbose=2,
         batch_size=batch_size,
         validation_data=(x_test, x_test))

fit是keras中非常常用的训练框架.详细介绍可以参考书中内容,这里不再展开.

1.7 使用模型

使用Model可以将任意张量组成模型下面第1行组成了一个输入是inputs输出是z_mean的模型,用该模型输出数据集中的解码均值,并显示出来.

代码语言:javascript
复制
 modencoder = Model(inputs, z_mean)

 print(modencoder.layers[1])    #Encoder

 x_test_encoded = modencoder.predict(x_test, batch_size=batch_size)
 plt.figure(figsize=(6, 6))
 plt.scatter(x_test_encoded[:, 0], x_test_encoded[:, 1], c=y_test)
 plt.colorbar()
 plt.show()

输出如下:

2 无监督训练中,没有标签的代码如何编写

在1中,介绍的训练方式是典形的有标签训练.即,在训练模型时,输入了2个样本,都是x_train.(代码第1.6小节第1行)

还可以稍加修改,在fit时不传入标签y,这样可以提升运算效率.具体做法如下:

2.1 修改组合模型

将1.2中组合模型和1.4的损失函数代码修改如下:

代码语言:javascript
复制
encoder = Encoder(intermediate_dim,latent_dim)
decoder = Decoder(intermediate_dim,original_dim)
 
inputs = Input(batch_shape=(batch_size, original_dim))
z_mean,z_log_var = encoder(inputs)
z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])
y_pred = decoder(z)
autoencoder = Model(inputs, y_pred, name='autoencoder')
 
# 重构loss
xent_loss = original_dim * metrics.binary_crossentropy(inputs, y_pred)
# KL loss
kl_loss = - 0.5 * K.sum(1 + z_log_var -
                        K.square(z_mean) - K.exp(z_log_var), axis=-1)
vae_loss = K.mean(xent_loss + kl_loss)
 
autoencoder.add_loss(vae_loss)
autoencoder.compile(optimizer='rmsprop')

可以看到,直接将1.4节的损失函数展开,生成了张量vae_loss.同时,利用模型的add_loss方法,将张量损失加入进去.

在编译模型时,可以不需要再指定损失了.

2.2 坑2:向模型中加入损失张量

最常见的坑,就是使用1.4节的方法,将张量损失编译到模型里.写法如下:

代码语言:javascript
复制
autoencoder.compile(optimizer='rmsprop', loss=vae_loss)

这时,报的错误绝对会使你一脸懵.错误是这样的:

【坑】:所以,记住张量损失一定要用模型的add_loss方法进行添加

2.3 坑3: 模型与训练不匹配

代码改到这里,并没有完事.因为我们将模型的loss计算中的标签输入去掉了.而fit的时候,还会输入标签.这时如果直接运行会报如下错误:

同样,这个错误也很难看出问题所在.直接修改fit函数,将输入标签去掉即可.代码1.6小节改成如下:

代码语言:javascript
复制
autoencoder.fit(x_train, validation_split=0.05, epochs=5, batch_size=batch_size)

再次运行,即可通过.

3 张量损失封装成损失函数

其实2节所介绍的方法,也可以再次封装成损失函数来进行执行.具体做法如下.

3.1 将张量损失封装成函数

在2.1小节代码后面添加如下代码:

代码语言:javascript
复制
def vae_lossfun(x, loss):
    return loss
lossautoencoder = Model(inputs, vae_loss, name='lossautoencoder')
lossautoencoder.compile(optimizer='rmsprop', loss=vae_lossfun)

该代码,重新又建立一个模型lossautoencoder,该模型的输出就是张量损失vae_loss.同时又建立一个损失函数,在输入损失时,将模型的输出透传出来即可.

在训练时可以直接使用1.6小节的fit方法即可

3.2 坑4:损失函数的参数固定

一定要注意,损失函数的参数是固定的(第一个是标签,第二个是预测值).如果将vae_lossfun,的参数改变,

def vae_lossfun(loss):

运行时,将会出现错误.

当然,这个也可以自定义.再未来的新书<机器视觉之TensorFlow2:入门原理与应用实战>里,会更加全面的展开介绍.

3.3 总结

这种模型的输出就是损失值的情况非常常见.例如最大化互信息模型(DIM)就是这种.2节和3节提供了2个这种模型的训练方法,都可以使用.

4 使用动态图训练

前面的1,2,3节都是使用keras的方式来训练模型.这种方法看是方便,但不适合模型的调试环节.尤其当训练种出现了None,更是一头雾水.虽然keras有单步训练的方式,但是仍不够灵活,为了适应训练过程中,各种情况的调试,最好还是使用底层的动态图训练模型.

4.1 修改训练方式

将1.6的代码改成如下:

代码语言:javascript
复制
optimizer = Adam(lr=0.001)#定义优化器
training_dataset = tf.data.Dataset.from_tensor_slices(  #定义数据集
x_train).batch(batch_size)
nb_epoch = 5
for epoch in range(nb_epoch):  # 按照指定迭代次数进行训练
    for dataone in training_dataset:  # 遍历数据集
        img = np.reshape(dataone, (batch_size, -1))
        with tf.GradientTape() as tape:
            
            z_mean,z_log_var = encoder(img)
            z= samplingfun(z_mean, z_log_var)  #ok
            x_decoded_mean = decoder(z)
 
            xent_loss = K.sum(K.square(x_decoded_mean - img), axis=-1)
            kl_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
 
            thisloss = K.mean(xent_loss)*0.5+K.mean(kl_loss)
 
            gradients = tape.gradient(thisloss,
 encoder.trainable_variables+decoder.trainable_variables)
            gradient_variables = zip(gradients,
 encoder.trainable_variables+decoder.trainable_variables)
            optimizer.apply_gradients(gradient_variables)

在动态图中训练需要自己定义数据集.还好,tf中数据集接口比较方便.一行代码就可以搞定. 《深度学习之TensorFlow:工程化项目实战》一书中数据集的介绍花了很大篇幅,相信读者这部分本领应该可以过关.

【坑】:即便是tf2.x其数据集接口的处理函数仍然是静态图,这个给调试过程带来很大不便.解决方法就是将其内部的调用再转成动态图.具体见书中介绍,这里不再展开.

动态图的代码很有流程化,读者只需要按顺序一步一步的做皆可.在倒数后两行需要注意,得将要训练得模型权重全部放进去.

4.2 技巧:任意提取模型

使用动态图训练好的模型,本质是改变实例化模型类的对象.所以也可以再使用keras.model方法,将其任意组成子模型.

这是tf框架非常赞的地方.它可以非常方便的将子模型提取出来,并通过权重的载入载出方法将模型保存和加载,例如:

modeENCODER.save_weights('my_modeldimvae.h5')

modeENCODER.load_weights('my_modeldimvae.h5')

这种方法可以非常方便的进行模型工程化部署.

具体例子,见配套的源码文件

5 以类的方式封装模型损失函数

为了代码工整,还可以将模型的整个过程封装起来,直接输出损失函数.

5.1 封装损失函数

将整个流程封装起来,具体如下:

代码语言:javascript
复制
class VAE(tf.keras.Model):  # 提取图片特征
    def __init__(self ,intermediate_dim,original_dim,latent_dim,**kwargs):
        super(VAE, self).__init__(**kwargs)
        self.encoder = Encoder(intermediate_dim,latent_dim)
        self.decoder = Decoder(intermediate_dim,original_dim)
 
    def call(self, x):
        z_mean,z_log_var = self.encoder(x)
        z = sampling( (z_mean,z_log_var) )
        y_pred = self.decoder(z)
        xent_loss = original_dim * metrics.binary_crossentropy(x, y_pred) #ok
        kl_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)
        loss = xent_loss + kl_loss
        return loss

该类的训练方法可以参考2,3节.具体可以参考配套的源码.

5.2 更合理的类封装模式

真正使用是,常常会将特征提取部分单独分开,作为一个类.这样利于扩展.令变分自编码功能方面的部分单独成一个类只完成变分训练功能.具体如下

代码语言:javascript
复制
class Featuremodel(tf.keras.Model):  # 提取图片特征
    def __init__(self ,intermediate_dim,latent_dim, **kwargs):
        super(Featuremodel, self).__init__(**kwargs)
        self.hidden_layer = Dense(units=intermediate_dim, activation=tf.nn.relu)
 
    def call(self, x):
        activation = self.hidden_layer(x)
        return x,activation
 
class Autoencoder(tf.keras.Model):
  def __init__(self, intermediate_dim, original_dim,latent_dim):
    super(Autoencoder, self).__init__()
    self.featuremodel = Featuremodel(intermediate_dim,latent_dim)
    self.encoder = Encoder(intermediate_dim,latent_dim)
    self.decoder = Decoder(intermediate_dim,original_dim)
 
  def call(self, input_features):
    x,feature =   self.featuremodel(input_features)
    z_mean,z_log_var = self.encoder(feature)
    epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0.,
                               stddev=1.0)
    code =  z_mean + K.exp(z_log_var / 2) * epsilon
    reconstructed = self.decoder(code)
    return reconstructed,z_mean,z_log_var

Featuremodel类可以被扩展成更复杂的模型. Autoencoder则专注于变分训练.

6 配套资源下载方式

本文只是对tf2的基本使用做了简单的总结.全面系统的教程还要以书为参.另外tf2在BN的支持上也存在许多不便之处,例如,使用动态图训练时,可以为每个BN加入一个istraining参数,来控制模型是否需要更新BN中的均值和方差(因为在测试时不需要更新);如果在keras模型体系中,则通过设置模型的trainable来控制。篇幅有限,这里不再展开.读者可以自行查找相关资料,在使用时还需多加小心.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 相约机器人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 基础的Keras写法
    • 1.1 模型结构
      • 1.2 组合模型
        • 1.3 坑1 :keras自定义模型的默认输入
          • 1.4 损失函数和编译模型
            • 1.5 载入数据集
              • 1.6 训练模型
                • 1.7 使用模型
                • 2 无监督训练中,没有标签的代码如何编写
                  • 2.1 修改组合模型
                    • 2.2 坑2:向模型中加入损失张量
                      • 2.3 坑3: 模型与训练不匹配
                      • 3 张量损失封装成损失函数
                        • 3.1 将张量损失封装成函数
                          • 3.2 坑4:损失函数的参数固定
                            • 3.3 总结
                            • 4 使用动态图训练
                              • 4.1 修改训练方式
                                • 4.2 技巧:任意提取模型
                                • 5 以类的方式封装模型损失函数
                                  • 5.1 封装损失函数
                                    • 5.2 更合理的类封装模式
                                    • 6 配套资源下载方式
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档