一直想要做个图像风格迁移来玩玩的,感觉还是蛮有意思的。
所谓图像风格迁移,即给定内容图片A,风格图片B,能够生成一张具有A图片内容和B图片风格的图片C。
比如说,我们可以使用梵高先生的名画《星夜》 作为风格图片,来与其他图片生成具有《星夜》风格新图片。emmm,夭寿啦,机器帮你画世界名画啦。。。
举两个生成的例子:
均使用《星夜》作为风格图片(可以替换,我以《星夜》为例):
示例1:
网络上找到的一张风景图片。
内容图片:
生成图片:
生成图片的尺寸比较小,没办法,我的显卡太差了,尺寸大一点的话显卡内存不足。
示例2:
嗷嗷嗷,狼人嚎叫~
内容图片:
生成图片:
效果还凑合吧,可以接受。
下面记录实现过程。
一.获取预训练的vgg19模型
VGG19是Google DeepMind发表在ICLR 2015上的论文《VERY DEEP CONVOLUTIONAL NETWORK SFOR LARGE-SCALE IMAGE RECOGNITION》中提出的一种DCNN结构。
众所周知,CNN在图片处理上表现良好,VGG19提出后,也被用在图像处理上。我这里要用到的VGG19模型就是在imagenet数据集上预训练的模型。
一般认为,深度卷积神经网络的训练是对数据集特征的一步步抽取的过程,从简单的特征,到复杂的特征。
训练好的模型学习到的是对图像特征的抽取方法,所以在imagenet数据集上训练好的模型理论上来说,也可以直接用于抽取其他图像的特征,这也是迁移学习的基础。自然,这样的效果往往没有在新数据上重新训练的效果好,但能够节省大量的训练时间,在特定情况下非常有用。
预训练好的VGG19模型可以从这里下载,模型大小500M+。
二.模型编写
这里的模型基本上就是VGG19模型,只是稍微做了一些修改。
我们要从预训练的模型中,获取卷积层部分的参数,用于构建我们自己的模型。VGG19中的全连接层舍弃掉,这一部分对提取图像特征基本无用。
要注意的是,我这里提取出来的VGG参数全部是作为constant(即常量)使用的,也就是说,这些参数是不会再被训练的,在反向传播的过程中也不会改变。
另外,输入层要设置为Variable,我们要训练的就是这个。最开始输入一张噪音图片,然后不断地根据内容loss和风格loss对其进行调整,直到一定次数后,该图片兼具了风格图片的风格以及内容图片的内容。当训练结束时,输入层的参数就是我们生成的图片。
附一张VGG结构图:
这个代码里主要是定义VGG,至于LOSS在训练过程中进行说明。
models.py
# -*- coding: utf-8 -*-# @Time : 18-3-23 下午12:20# @Author : AaronJny# @Email : Aaron__7@163.comimporttensorflowastfimportnumpyasnpimportsettingsimportscipy.ioimportscipy.miscclassModel(object):def__init__(self,content_path,style_path):self.content=self.loadimg(content_path)# 加载内容图片self.style=self.loadimg(style_path)# 加载风格图片self.random_img=self.get_random_img()# 生成噪音内容图片self.net=self.vggnet()# 建立vgg网络defvggnet(self):# 读取预训练的vgg模型vgg=scipy.io.loadmat(settings.VGG_MODEL_PATH)vgg_layers=vgg['layers'][0]net={}# 使用预训练的模型参数构建vgg网络的卷积层和池化层# 全连接层不需要# 注意,除了input之外,这里参数都为constant,即常量# 和平时不同,我们并不训练vgg的参数,它们保持不变# 需要进行训练的是input,它即是我们最终生成的图像net['input']=tf.Variable(np.zeros([1,settings.IMAGE_HEIGHT,settings.IMAGE_WIDTH,3]),dtype=tf.float32)# 参数对应的层数可以参考vgg模型图net['conv1_1']=self.conv_relu(net['input'],self.get_wb(vgg_layers,0))net['conv1_2']=self.conv_relu(net['conv1_1'],self.get_wb(vgg_layers,2))net['pool1']=self.pool(net['conv1_2'])net['conv2_1']=self.conv_relu(net['pool1'],self.get_wb(vgg_layers,5))net['conv2_2']=self.conv_relu(net['conv2_1'],self.get_wb(vgg_layers,7))net['pool2']=self.pool(net['conv2_2'])net['conv3_1']=self.conv_relu(net['pool2'],self.get_wb(vgg_layers,10))net['conv3_2']=self.conv_relu(net['conv3_1'],self.get_wb(vgg_layers,12))net['conv3_3']=self.conv_relu(net['conv3_2'],self.get_wb(vgg_layers,14))net['conv3_4']=self.conv_relu(net['conv3_3'],self.get_wb(vgg_layers,16))net['pool3']=self.pool(net['conv3_4'])net['conv4_1']=self.conv_relu(net['pool3'],self.get_wb(vgg_layers,19))net['conv4_2']=self.conv_relu(net['conv4_1'],self.get_wb(vgg_layers,21))net['conv4_3']=self.conv_relu(net['conv4_2'],self.get_wb(vgg_layers,23))net['conv4_4']=self.conv_relu(net['conv4_3'],self.get_wb(vgg_layers,25))net['pool4']=self.pool(net['conv4_4'])net['conv5_1']=self.conv_relu(net['pool4'],self.get_wb(vgg_layers,28))net['conv5_2']=self.conv_relu(net['conv5_1'],self.get_wb(vgg_layers,30))net['conv5_3']=self.conv_relu(net['conv5_2'],self.get_wb(vgg_layers,32))net['conv5_4']=self.conv_relu(net['conv5_3'],self.get_wb(vgg_layers,34))net['pool5']=self.pool(net['conv5_4'])returnnetdefconv_relu(self,input,wb):""" 进行先卷积、后relu的运算 :param input: 输入层 :param wb: wb[0],wb[1] == w,b :return: relu后的结果 """conv=tf.nn.conv2d(input,wb[0],strides=[1,1,1,1],padding='SAME')relu=tf.nn.relu(conv+wb[1])returnreludefpool(self,input):""" 进行max_pool操作 :param input: 输入层 :return: 池化后的结果 """returntf.nn.max_pool(input,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')defget_wb(self,layers,i):""" 从预训练好的vgg模型中读取参数 :param layers: 训练好的vgg模型 :param i: vgg指定层数 :return: 该层的w,b """w=tf.constant(layers[i][0][0][0][0][0])bias=layers[i][0][0][0][0][1]b=tf.constant(np.reshape(bias,(bias.size)))returnw,bdefget_random_img(self):""" 根据噪音和内容图片,生成一张随机图片 :return: """noise_image=np.random.uniform(-20,20,[1,settings.IMAGE_HEIGHT,settings.IMAGE_WIDTH,3])random_img=noise_image*settings.NOISE+self.content*(1-settings.NOISE)returnrandom_imgdefloadimg(self,path):""" 加载一张图片,将其转化为符合要求的格式 :param path: :return: """# 读取图片image=scipy.misc.imread(path)# 重新设定图片大小image=scipy.misc.imresize(image,[settings.IMAGE_HEIGHT,settings.IMAGE_WIDTH])# 改变数组形状,其实就是把它变成一个batch_size=1的batchimage=np.reshape(image,(1,settings.IMAGE_HEIGHT,settings.IMAGE_WIDTH,3))# 减去均值,使其数据分布接近0image=image-settings.IMAGE_MEAN_VALUEreturnimageif__name__=='__main__':Model(settings.CONTENT_IMAGE,settings.STYLE_IMAGE)
三.模型训练
这里简述一下训练的思路:
1.首先,我们使用VGG中的一些层的输出来表示图片的内容特征和风格特征。比如,我使用[‘conv4_2’,‘conv5_2’]表示内容特征,使用[‘conv1_1’,‘conv2_1’,‘conv3_1’,‘conv4_1’]表示风格特征。
# 定义计算内容损失的vgg层名称及对应权重的列表CONTENT_LOSS_LAYERS=[('conv4_2',0.5),('conv5_2',0.5)]# 定义计算风格损失的vgg层名称及对应权重的列表STYLE_LOSS_LAYERS=[('conv1_1',0.2),('conv2_1',0.2),('conv3_1',0.2),('conv4_1',0.2),('conv5_1',0.2)]
2.将内容图片输入网络,计算内容图片在网络指定层(比如[‘conv4_2’,‘conv5_2’])上的输出值。
3.计算内容损失。我们可以这样定义内容损失:内容图片在指定层上提取出的特征矩阵,与噪声图片在对应层上的特征矩阵的差值的L2范数。即求两两之间的像素差值的平方。
对应每一层的内容损失函数:
(话说为了写这个公式,我还跑去现学了latex语法= =)
其中,X是噪声图片的特征矩阵,P是内容图片的特征矩阵。M是P的长*宽,N是信道数。
最终的内容损失为,每一层的内容损失加权和,再对层数取平均。
4.将风格图片输入网络,计算风格图片在网络指定层(比如[‘conv1_1’,‘conv2_1’,‘conv3_1’,‘conv4_1’])上的输出值。
5.计算风格损失。我们使用风格图像在指定层上的特征矩阵的GRAM矩阵来衡量其风格,风格损失可以定义为风格图像和噪音图像特征矩阵的格莱姆矩阵的差值的L2范数。
对于每一层的风格损失函数:
其中M是特征矩阵的长*宽,N是特征矩阵的信道数。G为噪音图像特征的Gram矩阵,A为风格图片特征的GRAM矩阵。
最终的风格损失为,每一层的风格损失加权和,再对层数取平均。
6.最终用于训练的损失函数为内容损失和风格损失的加权和。
7.当训练开始时,我们根据内容图片和噪声,生成一张噪声图片。并将噪声图片喂给网络,计算loss,再根据loss调整噪声图片。将调整后的图片喂给网络,重新计算loss,再调整,再计算…直到达到指定迭代次数,此时,噪声图片已兼具内容图片的内容和风格图片的风格,进行保存即可。
具体代码如下:
train.py
# -*- coding: utf-8 -*-# @Time : 18-3-23 下午12:22# @Author : AaronJny# @Email : Aaron__7@163.comimporttensorflowastfimportsettingsimportmodelsimportnumpyasnpimportscipy.miscdefloss(sess,model):""" 定义模型的损失函数 :param sess: tf session :param model: 神经网络模型 :return: 内容损失和风格损失的加权和损失 """# 先计算内容损失函数# 获取定义内容损失的vgg层名称列表及权重content_layers=settings.CONTENT_LOSS_LAYERS# 将内容图片作为输入,方便后面提取内容图片在各层中的特征矩阵sess.run(tf.assign(model.net['input'],model.content))# 内容损失累加量content_loss=0.0# 逐个取出衡量内容损失的vgg层名称及对应权重forlayer_name,weightincontent_layers:# 提取内容图片在layer_name层中的特征矩阵p=sess.run(model.net[layer_name])# 提取噪音图片在layer_name层中的特征矩阵x=model.net[layer_name]# 长x宽M=p.shape[1]*p.shape[2]# 信道数N=p.shape[3]# 根据公式计算损失,并进行累加content_loss+=(1.0/(2*M*N))*tf.reduce_sum(tf.pow(p-x,2))*weight# 将损失对层数取平均content_loss/=len(content_layers)# 再计算风格损失函数style_layers=settings.STYLE_LOSS_LAYERS# 将风格图片作为输入,方便后面提取风格图片在各层中的特征矩阵sess.run(tf.assign(model.net['input'],model.style))# 风格损失累加量style_loss=0.0# 逐个取出衡量风格损失的vgg层名称及对应权重forlayer_name,weightinstyle_layers:# 提取风格图片在layer_name层中的特征矩阵a=sess.run(model.net[layer_name])# 提取噪音图片在layer_name层中的特征矩阵x=model.net[layer_name]# 长x宽M=a.shape[1]*a.shape[2]# 信道数N=a.shape[3]# 求风格图片特征的gram矩阵A=gram(a,M,N)# 求噪音图片特征的gram矩阵G=gram(x,M,N)# 根据公式计算损失,并进行累加style_loss+=(1.0/(4*M*M*N*N))*tf.reduce_sum(tf.pow(G-A,2))*weight# 将损失对层数取平均style_loss/=len(style_layers)# 将内容损失和风格损失加权求和,构成总损失函数loss=settings.ALPHA*content_loss+settings.BETA*style_lossreturnlossdefgram(x,size,deep):""" 创建给定矩阵的格莱姆矩阵,用来衡量风格 :param x:给定矩阵 :param size:矩阵的行数与列数的乘积 :param deep:矩阵信道数 :return:格莱姆矩阵 """# 改变shape为(size,deep)x=tf.reshape(x,(size,deep))# 求xTxg=tf.matmul(tf.transpose(x),x)returngdeftrain():# 创建一个模型model=models.Model(settings.CONTENT_IMAGE,settings.STYLE_IMAGE)# 创建sessionwithtf.Session()assess:# 全局初始化sess.run(tf.global_variables_initializer())# 定义损失函数cost=loss(sess,model)# 创建优化器optimizer=tf.train.AdamOptimizer(1.0).minimize(cost)# 再初始化一次(主要针对于第一次初始化后又定义的运算,不然可能会报错)sess.run(tf.global_variables_initializer())# 使用噪声图片进行训练sess.run(tf.assign(model.net['input'],model.random_img))# 迭代指定次数forstepinrange(settings.TRAIN_STEPS):# 进行一次反向传播sess.run(optimizer)# 每隔一定次数,输出一下进度,并保存当前训练结果ifstep%50==0:print'step {} is down.'.format(step)# 取出input的内容,这是生成的图片img=sess.run(model.net['input'])# 训练过程是减去均值的,这里要加上img+=settings.IMAGE_MEAN_VALUE# 这里是一个batch_size=1的batch,所以img[0]才是图片内容img=img[0]# 将像素值限定在0-255,并转为整型img=np.clip(img,0,255).astype(np.uint8)# 保存图片scipy.misc.imsave('{}-{}.jpg'.format(settings.OUTPUT_IMAGE,step),img)# 保存最终训练结果img=sess.run(model.net['input'])img+=settings.IMAGE_MEAN_VALUEimg=img[0]img=np.clip(img,0,255).astype(np.uint8)scipy.misc.imsave('{}.jpg'.format(settings.OUTPUT_IMAGE),img)if__name__=='__main__':train()
四、一些配置信息
settings.py
# -*- coding: utf-8 -*-# @Time : 18-3-23 下午12:22# @Author : AaronJny# @Email : Aaron__7@163.com# 内容图片路径CONTENT_IMAGE='images/content.jpg'# 风格图片路径STYLE_IMAGE='images/style.jpg'# 输出图片路径OUTPUT_IMAGE='output/output'# 预训练的vgg模型路径VGG_MODEL_PATH='imagenet-vgg-verydeep-19.mat'# 图片宽度IMAGE_WIDTH=450# 图片高度IMAGE_HEIGHT=300# 定义计算内容损失的vgg层名称及对应权重的列表CONTENT_LOSS_LAYERS=[('conv4_2',0.5),('conv5_2',0.5)]# 定义计算风格损失的vgg层名称及对应权重的列表STYLE_LOSS_LAYERS=[('conv1_1',0.2),('conv2_1',0.2),('conv3_1',0.2),('conv4_1',0.2),('conv5_1',0.2)]# 噪音比率NOISE=0.5# 图片RGB均值IMAGE_MEAN_VALUE=[128.0,128.0,128.0]# 内容损失权重ALPHA=1# 风格损失权重BETA=500# 训练次数TRAIN_STEPS=3000