文章目录
- 引言
- PixelCNN论文简读
- 模型介绍
- 自回归模型
- PixelCNN
- 模型结构
- 基础知识回顾
- 代码实现
- PixelConvLayer
- 具体运行过程
- 卷积模块
- 整体网络结构
- 模型执行效果
- 问题解决
- 训练好的模型在生成新的图片时,为什么要逐个元素进行生成?
- 掩码卷积仅仅是考虑了一部分的数据,并没有考虑当前像素之前所有的像素?
- 总结
- 参考连接
引言
-
在上一篇就是介绍了矢量量化变分模型的具体实现,就是一个编码器和解码器,只能生成和原来图片一样的图片,没啥意义。这里需要生成一个新的码字序列,解码器能够接受这部分数据,然后解码成对应的新的图片。
-
作者使用PixelCNN去训练这些码本
- pixelCNN的论文链接
- 代码样例
-
PixelCNN是一个自回归模型,根据已有的序列生成下一个位置的值。在这个任务里,就是生辰新的码字序列,然后使用训练好的解码器生成对应的新的图片。
-
注意:
- 这里的代码是局部代码,是vqvae项目中的一部分,是生成新的索引序列,并不是原来的图片 ,最后还是需要一个解码器来将索引序列还原成对应的图片。
- 项目地址,vqvae生成手写字符的地址
PixelCNN论文简读
- Abstract
- 这篇文章主要讲的是根据PixelCNN按照条件生成图片。这个模型可以根据向量,描述性的符号或者标记,甚至是别的网络模型生成的潜在embedding来生成新的图片。这个模型可以根据ImageNet中数据的类别标记,生成逼真并且完全不同的图片。如果输入模型的是一个其他卷积网络生成的embedding,并且这个embedding是表示人脸的,他会自动生成具有不同面部表情、姿势和光照条件的,同一个人的不同的各种新的画像。PixelCNN在图片自编码器中,也可以当作一个强大的图片解码器使用。
- Introduction
- 图片生成应用广泛,常见的比如说降噪、去模糊、修复、超分辨和着色都是使用图片生成技术,他们都需要根据不完整的图片或者带有噪声的图片来生成原始的图片。
- 这篇文章改良PixelRNN中的卷积变量实现的,基本的思路是使用自回归链接逐个像素对图片进行建模,将联合图像分布,分解成条件概率分布的乘积。原文中主要有两个变种,分别是PixelRNN,是使用LSTM进行预测,PixelCNN是使用卷积进行预测。
- 我们还引入了门控PixelCNN(条件PixelCNN)的条件变体,它允许我们在给定潜在向量嵌入的情况下对自然图像的复杂条件分布进行建模。我们表明,通过简单地对类的 one-hot 编码进行条件调节,可以使用单个条件 PixelCNN 模型从狗、草坪扇和珊瑚礁等不同类别的图像生成图像。类似地,可以使用捕获图像高级信息的嵌入来生成具有各种具有相似特征的图像。这让我们深入了解嵌入中编码的不变性——例如,我们可以根据单个图像生成同一个人的不同姿势。相同的框架也可用于分析和解释深度神经网络中的不同层和激活。
模型介绍
- 本来是想读论文的,一看已经是2016年的文章了,网上已经有很多相关的介绍了。
- PixelCNN是通过链式法则,将概率密度转成一系列条件概率之积,然后通过网络来估计条件分布。
自回归模型
-
自回归创建一个显式密度模型,该模型学习训练数据的最大似然。但是处理多个维度\特征的数据时,需要完整如下的步骤
- 首先,输入空间需要为其特征确定顺序,对于图像而言,通过像素的空间位置,来定义顺序,左侧像素在右侧像素之前,顶部像素在底部像素之前等。
- 其次,自回归模型需要将特征的联合分布p(x)转换为条件分布的乘积,以对数据观察中的特征的联合分布进行精确建模。
- 给定先前特征的值,自回归模型使用每一个特征的条件定义联合分布
-
图像中某一个像素具有特定强度值的概率由先前像素的值确定
-
图像的概率(所有像素的联合分布)是其所有像素的概率的组合
-
因此,自回归模型使用链式法则,将数据样本x的可能性分解成一维分布的乘积,将联合建模问题变成了序列问题,学习了在给定所有先前像素的情况下,预测下一个像素的过程。
PixelCNN
- PixelCNN是生成神经网络,一次生成一个像素,并且使用之前已经训练好的像素去生成下一个像素,类似于根据一个序列,去预测下一个像素点的值。如下图,是使用蓝色的像素块,来预测红色的像素块。
- 因为常规的卷积是计算周围所有的像素点,和PixelCNN的预测方式不同,所以这里出现了掩码卷积,来控制没有被预测的像素点。主要有两类掩码卷积类型,分别如下
- Mask Type A仅仅是用于第一层卷积,中间值被屏蔽,因为没有预测过
- Mask Type B可以用于出第一层以外的所有的层卷积,除了第一层,后续每一层的自身值都是被预测过的。
模型结构
- 第一层使用77的A类型掩码卷积模块,然后使用15个残差模块。每一個残拆模块的构成图如下,使用33的B类型的掩码卷积模块,首尾两个1*1的卷积模块。
-
经历过残差序列模块的处理之后,经过两个带有relu的1*1的卷积层,然后经过softmax进行输出,预测像素所有可能的预测值。模型的输出是具有与输入图像大小相同的格式乘以可能值的数量。
基础知识回顾
- 极大似然:利用已知样本结果信息,反推最具有可能导致当前样本结果的模型参数值。相当于数据的样本是已经知道的,对应的模型分布类型不知道,缺的是对应类型的模型参数,所以是根据样本估计模型参数。
- 概率和统计
- 概率:已知模型和参数,推断数据的分布。
- 统计:已知数据的具体内容,推断具体模型和参数。
- 联合概率和条件概率
- 联合概率:同一个时间,两个独立事件同时发生的可能性,事件相交的概率
- 条件概率:P(B|A)事件B在事件A发生的情况下,B发生的概率。两个相依事件的联合概率变为P(A 和B)= P(A)P(B | A)
- 贝叶斯公式
- 后验概率等于先验概率乘以似然比
- P(H|E)=P(H)P(E|H)/P(E)
- 先验概率和后颜概率
- 后验概率:所有证据被采用之后,事件发生的概率
- 先验概率:未采用证据之前,事件发生的概率,经验性概率
- 后验概率= (可能性*先验)/证据
- 最大似然估计
- 根据样本,也就是数据,来估计模型的参数,使得似然函数的值最大
代码实现
PixelConvLayer
- 这部分就是掩码卷积,通过之前已经预测的点的概率,来预测当前的点。在原来的卷积上,增加了一个掩码的部分。
- 主要是两类,差别在于A类中心点的坐标并没有标记,B类的中心点已经被预测过的,具体看上面的图。
# 掩码卷积
class PixelConvLayer(Layer):"""掩码卷积层,分别是两种类型,A类和B类"""def __init__(self,mask_type,**kwargs):super().__init__()self.mask_type = mask_typeself.conv = Conv2D(**kwargs)self.mask = Nonedef build(self,input_shape):# 创建二维卷积层,并初始化对应的卷积核权重参数self.conv.build(input_shape)kernel_shape = self.conv.kernel.get_shape()# 生成同等大小的掩码层,用来抑制没有预测的卷积层self.mask = np.zeros(shape = kernel_shape)self.mask[:kernel_shape[0] // 2,...] = 1.0self.mask[kernel_shape[0] // 2,:kernel_shape[1] // 2,...] = 1.0if self.mask_type == "B":self.mask[kernel_shape[0] // 2,kernel_shape[1]//2,...] = 1.0def call(self,inputs):# 根据掩码层,来修改更正卷积层的结果self.conv.kernel.assign(self.conv.kernel * self.mask)return self.conv(inputs)
具体运行过程
- 这里用一个33的掩码卷积作为样例,对一个88的图片,进行A类型的掩码卷积,然后使用的padding模式是same.这里并不是完全的使用之前所有的已经预测过的标记点,进行预测当前的标记点,是根据卷积核中的已经预测过的点进行预测,具体过程如下:
- 第一次预测红框中心的点
- 后续,第一个位置已经被预测过了,就会考虑到第一个点的预测结果
- 以此类推,不断向后迭代
- 最多的情况下,就是考虑了四个点的情况
- 代码主要有两部分构成,分别是掩码和前向传播的实际计算
# 掩码卷积
class PixelConvLayer(Layer):"""掩码卷积层,分别是两种类型,A类和B类"""def __init__(self,mask_type,**kwargs):super().__init__()self.mask_type = mask_typeself.conv = Conv2D(**kwargs)self.mask = Nonedef build(self,input_shape):# 创建二维卷积层self.conv.build(input_shape)kernel_shape = self.conv.kernel.get_shape()# 将卷积层改成,掩码卷积,这里是生成掩码层self.mask = np.zeros(shape = kernel_shape)self.mask[:kernel_shape[0] // 2,...] = 1.0self.mask[kernel_shape[0] // 2,:kernel_shape[1] // 2,...] = 1.0if self.mask_type == "B":self.mask[kernel_shape[0] // 2,kernel_shape[1]//2,...] = 1.0def call(self,inputs):self.conv.kernel.assign(self.conv.kernel * self.mask)return self.conv(inputs)
卷积模块
- 这部分就是在原来的基础上,使用残差网络将B类的掩码卷积套在残差模块中,具体形成如下
- 首尾分别是两个1*1的卷积
- 中间是3*3的B类掩码卷积
class ResidualBlock(Layer):"""基于掩码卷积层的残差模块"""def __init__(self,filters,**kwargs):super().__init__()# 第一个卷积模块self.conv1 = Conv2D(filters = filters,kernel_size = 1,activation="relu")# 中间的B类掩码卷积,进行概率估计self.pixel_conv = PixelConvLayer(mask_type="B",kernel_size = 3,activation = "relu",padding = "same",filters = filters // 2)# 最后一个卷积模块self.conv2 = Conv2D(filters = filters,kernel_size = 1,activation="relu")def call(self,inputs):# 前向传播中,数据传播的方式x = self.conv1(inputs)x = self.pixel_conv(x)x = self.conv2(x)# 残差模块中的直连res = add([inputs,x])return res
整体网络结构
- 具体网络结构如下图,但是教程的代码实现起来最后做了修改,我是觉得如果使用11的掩码卷积其实和常规的11的卷积是一个效果
class PixelCNN:"""PixelCNN:像素卷积模型功能:用于生成新的码本序列"""def __init__(self,input_shape,num_residual_blocks,num_pixelcnn_layers,num_embeddings):self.input_shape = input_shapeself.num_residual_blocks = num_residual_blocksself.num_pixelcnn_layers = num_pixelcnn_layersself.num_embeddings = num_embeddings# 定义不同的层self.model = None# 构建模型self._build()def _build(self):"""网络结构为:Aconv3*3,residual815,Rconv,Rconv,Softmax:return:"""PixelCNN_Input = Input(shape = self.input_shape,name = "PixelCNN input")x = PixelConvLayer(mask_type="A",filters = 128,kernel_size = 7,strides = 1,activation = "relu",padding = "same")(PixelCNN_Input)# 添加卷积模块for i in range(self.num_residual_blocks):x = ResidualBlock(filters = 128)(x)# 添加后续的像素卷积层for i in range(self.num_pixelcnn_layers):x = PixelConvLayer(mask_type="B",filters = 128,kernel_size = 1,strides = 1,activation = "relu",padding = "valid")(x)out = Conv2D(filters=1,kernel_size=1,strides=1,activation="sigmoid",padding="valid")(x)self.model = Model(PixelCNN_Input,out)def summary(self):self.model.summary()def compile(self, learning_rate=0.0001):""" 指定损失函数和优化器,并对模型进行优化 """optimizer = Adam(learning_rate=learning_rate)self.model.compile(optimizer=optimizer,loss="binary_crossentropy",)# 3.3 增加训练函数def train(self, x_train, batch_size, num_epochs):self.model.fit(x_train,x_train,batch_size=batch_size,epochs=num_epochs,shuffle=True)
模型执行效果
- 这里生成的模型比较简单,没有按照他的弄了几层。
问题解决
训练好的模型在生成新的图片时,为什么要逐个元素进行生成?
- 模型在训练的过程中,会逐个预测每一个像素的像素值的概率分布,然后计算预测结果和真实值之间的差异,也就是损失函数,然后使用反向传播来更新模型的参数,使得模型预测的每一个像素的概率分布能够更加接近真实的像素值。对于这个手写数字生成的模型而言,是使用交叉熵损失函数来当作损失函数。如下图
- 所以,训练过程中,使用多重掩码卷积逐个扫描每一个区域,预测出一个和输入图像同样大小的结果,然后计算输出图像和输入图像的像素误差。在生成过程中,一开始给的序列是空白序列,所有的像素值都是无效的,如果像训练过程一样,直接生成完整的子图,那么后续像素所参考的像素就没有任何意义,参考价值就不大。生成的图片如下图
- 而逐个元素进行生成确认,即迭代生成每一个元素,并将当前元素加入到序列中,重新进行预测。后续生成的序列会和之前的生成的像素值相关,也就是后续的像素值,有之前的像素值决定。如下图。
- 生成的效果明显就要好很多,如下图。
- 说白了,在训练过程中,用的是极大似然估计,调整模型的参数,使得模型的效果尽可能地好,预测的结果尽可能地贴近实际的图片,这里还没有用到具体的条件概率,然后在预测的过程中,迭代逐个像素进行预测,就用到了条件概率,由之前预测的像素值,来决定之后的像素值。
掩码卷积仅仅是考虑了一部分的数据,并没有考虑当前像素之前所有的像素?
-
问题:我们知道,PixelCNN的原理是将联合分布拆解成多个条件概率的分布,也就是说预测每一个像素需要考虑之前每一个值,但是掩码卷积仅仅是使用了卷积核之内的数据,并没有考虑到每一个数据。
-
PixelCNN使用了掩码卷积(masked convolution)来限制卷积核只能看到先前生成的像素值,从而确保每个像素的生成仅依赖于其前面的像素。确实并不能考虑到之前所有的像素点,只能考虑到当前像素点的感受野中的元素,还是条件概率,但是并不是之前所有的像素点。
-
具体可以看这个图片,五角星的那个像素点,确实只能考虑圆圈圈出来的点,计算他们的条件概率,并不能考虑到没有圈处来的其他的点,但这是只有一层卷积,如果有多层卷积,感受野会更大,考虑的也会更加全面。
- 如果是二层卷积,可以看到他的感受野更大了,所以能够考虑到的像素点越多。
- 通过使用掩码卷积,PixelCNN可以将联合分布拆解成多个条件概率的分布,从而将生成过程转化为逐个像素的条件生成。在预测每个像素的时候,模型只考虑了之前已经生成的像素值,并没有考虑到后面的像素值。
- 掩码卷积保证了模型的生成过程是自回归的,能够逐步地生成图像,并且可以捕捉到像素之间的条件依赖关系。每个像素的生成都受限于其前面已经生成的像素,确保了生成的图像具有一致性和合理性。
- 需要注意的是,掩码卷积仅在训练过程中使用,用于限制卷积核的感受野。在生成过程中,模型可以自由地逐个像素生成,而不受掩码卷积的限制。
- 因为生成图片的过程中,是迭代预测生成了每一个像素,每一个像素都要做一次完整的预测迭代,所以是不受掩码卷积的限制。
- 总结起来,PixelCNN使用掩码卷积将联合分布拆解成条件概率分布,并通过逐个像素的条件生成来保持自回归性质。掩码卷积限制了卷积核只能看到先前生成的像素值,从而确保每个像素的生成仅依赖于其前面的像素。
总结
- 一开始这里弄了很久都没有弄懂,尤其是条件概率那里,我知道他是考虑了之前所有的像素点,也理解掩码卷积,但是就是不理解为什么生成的时候要逐个元素进行预测,预测那么多次,现在理解了。通过做实验可以知道,这样的效果更好,而且最终生成的像素也完全不受感受野的大小,完全考虑到了所有的确定的像素点。
- 同时这样给出了一个思路,对于第一个像素点,随即指定不同的初值,可以生成不同风格,不同类型的数据。同时可以适当减少预测的类别,来提高准确率,但是要对数据尽心有效的分割,能够分成不同的部分。
- 这一章拖了蛮久的,终于写完了,找机会录一个视频,发一下。代码还是要好好写一下。
参考连接
- 参考并引用下述文章的内容
- 生成模型之PixelCNN
- 什么是PixelCNN
- 自回归模型–PixelCNN