1.VAE和GAN
- 变分自编码器(VAE,variatinal autoencoder)
- 生成式对抗网络(GAN,generative adversarial network)
两者不仅适用于图像,还可以探索声音、音乐甚至文本的潜在空间;
- VAE非常适合用于学习具有良好结构的潜在空间,其中特定方向表示数据中有意义的变化轴;
- GAN生成的图像可能非常逼真,但它的潜在空间可能没有良好结构,也没有足够的连续型。
自编码,简单来说就是把输入数据进行一个压缩和解压缩的过程。 原来有很多 Feature,压缩成几个来代表原来的数据,解压之后恢复成原来的维度,再和原数据进行比较。它是一种非监督算法,只需要输入数据,解压缩之后的结果与原数据本身进行比较。

在实践中,这种经典的自编码器不会得到特别有用或具有良好结构的潜在空间。它们也没有对数据做多少压缩。因此,它们已经基本上过时了(Keras 0.x版本还有AutoEncoder这个层,后来直接都删了)。但是,VAE向自编码器添加了一点统计魔法,迫使其学习连续的、高度结构化的潜在空间。这使得VAE已成为图像生成的强大工具。变分编码器和自动编码器的区别就在于,传统自动编码器的隐变量z的分布是不知道的,因此我们无法采样得到新的z,也就无法通过解码器得到新的x。下面我们来变分,我们现在不要从x中直接得到z,而是得到z的均值和方差,然后再迫使它逼近正态分布的均值和方差,则网络变成下面的样子:

然而上面这个网络最大的问题是,它是断开的。前半截是从数据集估计z的分布,后半截是从一个z的样本重构输入。最关键的采样这一步,恰好不是一个我们传统意义上的操作。这个网络没法求导,因为梯度传到f(z)以后没办法往前走了。为了使得整个网络得以训练,使用一种叫reparemerization的trick,使得网络对均值和方差可导,把网络连起来。这个trick的idea见下图:

实际上,这是将原来的单输入模型改为二输入模型了。因为
服从标准正态分布,所以它乘以估计的方差加上估计的均值,效果跟上上图直接从高斯分布里抽样本结果是一样的。这样,梯度就可以通上图红线的方向回传,整个网络就变的可训练了。

VAE的工作原理:
(1)一个编码器模块将输入样本input_img转换为表示潜在空间中的两个参数z_mean和z_log_variance;
(2)我们假定潜在正态分布能够生成输入图像,并从这个分布中随机采样一个点:z=z_mean + exp(z_log_variance)*epsilon,其中epsilon是取值很小的随机张量;
(3)一个解码器模块将潜在空间的这个点映射回原始输入图像。
因为epsilon是随机的,所以这个过程可以确保,与input_img编码的潜在位置(即z-mean)靠近的每个点都能被解码为与input_img类似的图像,从而迫使潜在空间能够连续地有意义。潜在空间中任意两个相邻的点都会被解码为高度相似的图像。连续性以及潜在空间的低维度,将迫使潜在空间中的每个方向都表示数据中一个有意义的变化轴,这使得潜在空间具有非常良好的结构,因此非常适合通过概率向量来进行操作。
VAE的参数通过两个损失函数来进行训练:一个是重构损失(reconstruction loss),它迫使解码后的样本匹配初始输入;另一个是正则化损失(regularization loss),它有助于学习具有良好结构的潜在空间,并可以降低训练数据上的过拟合。
实现代码如下:
编码自编码器是更现代和有趣的一种自动编码器,它为码字施加约束,使得编码器学习到输入数据的隐变量模型。 隐变量模型是连接显变量集和隐变量集的统计模型,隐变量模型的假设是显变量是由隐变量的状态控制的,各个显变量之间条件独立。 也就是说,变分编码器不再学习一个任意的函数,而是学习你的数据概率分布的一组参数。 通过在这个概率分布中采样,你可以生成新的输入数据,即变分编码器是一个生成模型。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import keras
from keras import layers
from keras import backend as K
from keras.models import Model
from keras.layers import Input ,Dense
import numpy as np
img_shape = ( 28 , 28 , 1 )
latent_dim = 2
input_img = keras. Input (shape = img_shape)
encoded = layers.Conv2D( 32 , 3 ,padding = 'same' ,activation = 'relu' )(input_img)
encoded = layers.Conv2D( 64 , 3 ,padding = 'same' ,activation = 'relu' ,strides = ( 2 , 2 ))(encoded)
encoded = layers.Conv2D( 64 , 3 ,padding = 'same' ,activation = 'relu' )(encoded)
encoded = layers.Conv2D( 64 , 3 ,padding = 'same' ,activation = 'relu' )(encoded)
shape_before_flattening = K.int_shape(encoded)
shape_before_flattening
|
|
卷积层的输入必须是3维的(长,宽,1或者3)
keras不需要输入batch的大小,fit时候再设置
shape_before_flattening
(None, 14, 14, 64)
|
1 2 3 4 5 6 7 8 9 | encoded = layers.Flatten()(encoded)
encoded = layers.Dense( 32 ,activation = 'relu' )(encoded)
z_mean = layers.Dense(latent_dim)(encoded)
z_log_var = layers.Dense(latent_dim)(encoded)
encoder = Model(input_img,z_mean)
|
|
z_mean --->
<tf.Tensor 'dense_5/BiasAdd:0' shape=(?, 2) dtype=float32>
K.shape(z_mean) ---> <tf.Tensor 'Shape:0' shape=(2,) dtype=int32>

|
1 2 3 4 5 6 7 | def sampling(args):
z_mean,z_log_var = args
epsilon = K.random_normal(shape = (K.shape(z_mean)[ 0 ],latent_dim),mean = 0. ,stddev = 1. )
return z_mean + K.exp(z_log_var) * epsilon
z = layers.Lambda(sampling,output_shape = (latent_dim,))([z_mean,z_log_var])
|
|
在keras中,任何对象都应该是一个层,如果代码不是内置层的一部分,
我们应该将其包装到一个Lambda层(或自定义层)中
Keras的Lambda层以一个张量函数为参数,对输入的数据按照张量函数的要求做映射。
本质上就是Keras layer中.call()的快捷方式。先定义运算逻辑
K.int_shape(z) ---> (None,2) None应该是batch_size
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | decoder_input = layers. Input (K.int_shape(z)[ 1 :])
decoded = layers.Dense(np.prod(shape_before_flattening[ 1 :]),activation = 'relu' )(decoder_input)
decoded = layers.Reshape(shape_before_flattening[ 1 :])(decoded)
decoded = layers.Conv2DTranspose( 32 , 3 ,padding = 'same' ,activation = 'relu' ,strides = ( 2 , 2 ))(decoded)
decoder_output = layers.Conv2D( 1 , 3 ,padding = 'same' ,activation = 'sigmoid' )(decoded)
decoder = Model(decoder_input,decoder_output)
z_decoded = decoder(z)
|
|
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class CustomVariationalLayer(keras.layers.Layer):
def vae_loss( self ,x,z_decoded):
x = K.flatten(x)
z_decoded = K.flatten(z_decoded)
xent_loss = keras.metrics.binary_crossentropy(x,z_decoded)
kl_loss = - 5e - 4 * K.mean( 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var),axis = - 1 )
return K.mean(xent_loss + kl_loss)
def call( self ,inputs):
x = inputs[ 0 ]
z_decoded = inputs[ 1 ]
loss = self .vae_loss(x,z_decoded)
self .add_loss(loss,inputs = inputs)
return x
y = CustomVariationalLayer()([input_img,z_decoded])
|
|
正则化损失 + 重构损失
我们一般认为采样函数的形式为loss(input,target),VAE的双重损失不符合这种形式。
因此,损失的设置方法为:编写一个自定义层,并在其内部使用内置的add_loss层方法
来创建一个你想要的损失
|
|
|
1 2 3 4 | vae = Model(input_img,y)
vae. compile (optimizer = 'rmsprop' ,loss = None )
vae.summary()
|
|

|
1 2 3 4 5 6 7 8 9 10 11 12 13 | from keras.datasets import mnist
(x_train,_),(x_test,y_test) = mnist.load_data()
x_train = x_train[: 600 ]
x_test = x_test[: 100 ]
x_train = x_train.astype( 'float32' ) / 255.
print ( 'x_train.shape' ,x_train.shape)
x_train = x_train.reshape(x_train.shape + ( 1 ,))
print ( 'x_train.shape' ,x_train.shape)
x_test = x_test.astype( 'float32' ) / 255.
print ( 'x_test.shape' ,x_test.shape)
x_test = x_test.reshape(x_test.shape + ( 1 ,))
print ( 'x_test.shape' ,x_test.shape)
|
|
x_train.shape (600, 28, 28)
x_train.shape (600, 28, 28, 1)
x_test.shape (100, 28, 28)
x_test.shape (100, 28, 28, 1)
|
1 2 3 4 5 6 | vae.fit(x_train, None ,
shuffle = True ,
epochs = 1 ,
batch_size = 100 ,
validation_data = (x_test, None )
)
|
|
一旦训练好了这样的模型,我们就可以使用decoder网络将任意潜在空间向量
转换为图像
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import matplotlib.pyplot as plt
from scipy.stats import norm
batch_size = 100
n = 15
digit_size = 28
figure = np.zeros((digit_size * n,digit_size * n))
grid_x = norm.ppf(np.linspace( 0.05 , 0.95 ,n))
grid_y = norm.ppf(np.linspace( 0.05 , 0.95 ,n))
print (grid_x)
print (grid_y)
for i,yi in enumerate (grid_x):
for j,xi in enumerate (grid_y):
z_sample = np.array([[xi,yi]])
z_sample = np.tile(z_sample,batch_size).reshape(batch_size, 2 )
x_decoded = decoder.predict(z_sample,batch_size = batch_size)
digit = x_decoded[ 0 ].reshape(digit_size,digit_size)
figure[i * digit_size:(i + 1 ) * digit_size,j * digit_size:(j + 1 ) * digit_size] = digit
plt.figure(figsize = ( 10 , 10 ))
plt.imshow(figure,cmap = 'Greys_r' )
plt.show()
|
|
因为训练时候就用了600个数据,所以效果很差....电脑实在带不动,┭┮﹏┭┮
以后有服务器再试试,7777777
|
小结: 用深度学习进行图像生成,就是通过对潜在空间进行学习来实现的,这个潜在空间能够捕捉到关于图像数据集的统计信息。 通过对潜在空间中的点进行采样和编码,我们可以生成前所未见的图像。
网上的代码大部分都是关于mnist数据集的,直接load_dataset就完事了,我找到了名人头像的数据集celebrity_data,用这个数据集做vae更有趣一点。
1 2 3 4 5 6 7 8 9 10 | import keras
from keras import layers
from keras import backend as K
from keras.models import Model
import numpy as np
import skimage
import glob
from skimage import io
import os
import imageio
|
|
- skimage即是Scikit-Image。基于python脚本语言开发的数字图片处理包,比如PIL,Pillow, opencv, scikit-image等。
- PIL和Pillow只提供最基础的数字图像处理,功能有限;opencv实际上是一个c++库,只是提供了python接口,更新速度非常慢。
- scikit-image是基于scipy的一款图像处理包,它将图片作为numpy数组进行处理,正好与matlab一样,
- 因此,我们最终选择scikit-image进行数字图像处理。
|
1 2 3 4 5 6 | train_imgs = glob.glob( './celebrity_data/train/*.jpg' )
np.random.shuffle(train_imgs)
test_imgs = glob.glob( './celebrity_data/test/*.jpg' )
np.random.shuffle(train_imgs)
nxf_image = io.imread(test_imgs[ 0 ])
|
|
Image读出来的是PIL的类型,而skimage.io读出来的数据是numpy格式的
1 2 3 4 5 6 7 | import Image as img
import os
from matplotlib import pyplot as plot
from skimage import io,transform
img_file1 = img. open ( './CXR_png/MCUCXR_0042_0.png' )
img_file2 = io.imread( './CXR_png/MCUCXR_0042_0.png' )
|
输出可以看出Img读图片的大小是图片的(width, height);而skimage的是(height,width, channel)
|
1 2 3 4 5 | height,width = imageio.imread(train_imgs[ 0 ]).shape[: 2 ]
center_height = int ((height - width) / 2 )
img_xdim = 218
img_ydim = 178
z_dim = 512
|
|
训练集里面的图片都是218*178*3的,训练的时候我也没有改大小,直接放进去训练的 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def imread(f):
x = imageio.imread(f)
x = x[center_height:center_height + width,:]
x = skimage.transform.resize(x,(img_xdim,img_ydim),mode = 'constant' )
return x.astype(np.float32) / 255 * 2 - 1
def train_data_generator(batch_size = 32 ):
X = []
while True :
np.random.shuffle(train_imgs)
for f in train_imgs:
X.append(imread(f))
if len (X) = = batch_size:
X = np.array(X)
yield X, None
X = []
|
|
train_data_generator是训练集图片生成器,每次生成一个图片
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | img_shape = (img_xdim,img_ydim, 3 )
latent_dim = 2
input_img = keras. Input (shape = img_shape)
encoded = layers.Conv2D( 32 , 3 ,padding = 'same' ,activation = 'relu' )(input_img)
encoded = layers.Conv2D( 64 , 3 ,padding = 'same' ,activation = 'relu' ,strides = ( 2 , 2 ))(encoded)
encoded = layers.Conv2D( 64 , 3 ,padding = 'same' ,activation = 'relu' )(encoded)
encoded = layers.Conv2D( 64 , 3 ,padding = 'same' ,activation = 'relu' )(encoded)
shape_before_flattening = K.int_shape(encoded)
encoded = layers.Flatten()(encoded)
encoded = layers.Dense( 32 ,activation = 'relu' )(encoded)
z_mean = layers.Dense(latent_dim)(encoded)
z_log_var = layers.Dense(latent_dim)(encoded)
encoder = Model(input_img,z_mean)
|
|
这部分和上面基于minist数据集的encoder部分一样 |
1 2 3 4 5 | nxf_image = nxf_image.reshape(( 1 ,) + nxf_image.shape)
nxf_image_encoder = encoder.predict(nxf_image)
print ( 'nxf_image_encoder' ,nxf_image_encoder)
|
|
这里是我在测试encoder,随机输入一张图片,输出了二维的一个值,一个是均值,一个是方差,encoder没有编译,
也没有fit,就相当于将多维图片降维成二维的一组
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | def sampling(args):
z_mean,z_log_var = args
epsilon = K.random_normal(shape = (K.shape(z_mean)[ 0 ],latent_dim),mean = 0. ,stddev = 1. )
return z_mean + K.exp(z_log_var) * epsilon
z = layers.Lambda(sampling,output_shape = (latent_dim,))([z_mean,z_log_var])
decoder_input = layers. Input (K.int_shape(z)[ 1 :])
decoded = layers.Dense(np.prod(shape_before_flattening[ 1 :]),activation = 'relu' )(decoder_input)
decoded = layers.Reshape(shape_before_flattening[ 1 :])(decoded)
decoded = layers.Conv2DTranspose( 32 , 3 ,padding = 'same' ,activation = 'relu' ,strides = ( 2 , 2 ))(decoded)
decoder_output = layers.Conv2D( 3 , 3 ,padding = 'same' ,activation = 'sigmoid' )(decoded)
decoder = Model(decoder_input,decoder_output)
z_decoded = decoder(z)
|
|
这部分也是一样的,解码操作,随机生成一个点(均值,方差)放入decoder中,看看生成的图片能不能和原来的图片一样 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class CustomVariationalLayer (keras.layers.Layer):
def vae_loss( self , x, z_decoded):
x = K.flatten (x)
z_decoded = K.flatten (z_decoded)
xent_loss = keras.metrics.binary_crossentropy (x, z_decoded)
kl_loss = - 5e - 4 * K.mean ( 1 + z_log_var - K.square (z_mean) - K.exp (z_log_var), axis = - 1 )
return K.mean (xent_loss + kl_loss)
def call( self , inputs):
x = inputs[ 0 ]
z_decoded = inputs[ 1 ]
loss = self .vae_loss (x, z_decoded)
self .add_loss(loss, inputs = inputs)
return x
y = CustomVariationalLayer() ([input_img, z_decoded])
vae = Model(input_img, y)
vae. compile (optimizer = 'rmsprop' , loss = None )
|
|
VAE的两个损失,由于keras自带的损失函数没有同时有正则损失和重构损失,所以需要自定义一个损失层,
使用call函数来定义该损失层的功能
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | def sample(path):
figure_nxf = np.array(nxf_image_encoder)
nxf_recon = decoder.predict(figure_nxf)[ 0 ]
imageio.imwrite(path,nxf_recon)
from keras.callbacks import Callback
class Evaluate(Callback):
def __init__( self ):
import os
self .lowest = 1e10
self .losses = []
if not os.path.exists( 'samples' ):
os.mkdir( 'samples' )
def on_epoch_end( self , epoch, logs = None ):
path = 'samples/test_%s.png' % epoch
sample(path)
self .losses.append((epoch, logs[ 'loss' ]))
if logs[ 'loss' ] < = self .lowest:
self .lowest = logs[ 'loss' ]
encoder.save_weights( './best_encoder.weights' )
evaluator = Evaluate()
vae.fit_generator(train_data_generator(),
epochs = 1 ,
steps_per_epoch = 1 ,
callbacks = [evaluator])
|
|
sample函数,我就随机输入两个值(encoder的输出值),看看能不能生成一个相似的图片 |
参考文献:
【1】Keras示例程序解析(4):变分编码器VAE
【2】变分自编码器(Variational Autoencoder, VAE)通俗教程
【3】变分自编码器VAE:一步到位的聚类方案
【4】如何使用变分自编码器VAE生成动漫人物形象
【5】vae 名人数据集的使用
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!