神经风格迁移由 Leon Gatys 等人于 2015 年夏天提出,它是指将一些艺术画(如梵高的星空)的艺术风格迁移至目标图片(如自己拍摄的照片),使你拍摄的照片拥有艺术家作品一样的风格。具体如下图:
第1关:卷积神经网络
编程要求:使用 keras 搭建出卷积神经网络模型,再对模型进行训练,并将训练好的模型保存至 ./step1/cnn.model 中。
# -*- coding: utf-8 -*-
import os
import numpy as np
import cv2
#设置图片宽高为32x32
IMAGE_SIZE = 32
#获取图像和标签
def get_img_and_label(dirname):
images = []
labels = []
sub_dirs = ['roses', 'sunflowers']
for dir in sub_dirs:
path = os.path.join(dirname, dir)
for filename in os.listdir(path):
img = cv2.imread(os.path.join(path, filename), 0)
img = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE))
images.append(img)
if dir == 'roses':
labels.append([0, 1])
else:
labels.append([1, 0])
images = np.array(images).reshape((-1, IMAGE_SIZE, IMAGE_SIZE, 1))/255.0
labels = np.array(labels).reshape((-1, 2))
return images, labels
#获取训练集数据与标签
train_images,train_labels = get_img_and_label('./step1/train')
#********* Begin *********#
#构建模型
from keras import layers
from keras import models
model = models.Sequential()
#卷积层
model.add(layers.Conv2D(32, (3,3), activation='relu', input_shape=[32, 32, 1]))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64,(3,3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64,(3,3) ,activation='relu'))
#拉直
model.add(layers.Flatten())
#全连接层
model.add(layers.Dense(64,activation='relu'))
model.add(layers.Dense(2,activation='softmax'))
#编译模型
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
#********* End *********#
#训练模型
model.fit(train_images, train_labels, epochs=5, batch_size=64,verbose=0)
#保存模型
model.save('./step1/model/cnn.model')
相关知识:
1、卷积神经网络
为什么使用卷积神经网络
卷积神经网络最早主要用来处理图像信息。如果用全连接前馈网络来处理图像时,会存在以下两个问题:
参数太多:如果输入图像大小为100 × 100 × 3。在全连接前馈网络中,第一个隐藏层的每个神经元到输入层都有 30000个相互独立的连接,每个连接都对应一个权重参数。随着隐藏层神经元数量的增多,参数的规模也会急剧增加。这会导致整个神经网络的训练效率会非常低,也很容易出现过拟合。
局部不变性特征: 自然图像中的物体都具有局部不变性特征,比如在尺度缩放、平移、旋转等操作不影响其语义信息。而全连接前馈网络很难提取这些局部不变特征,一般需要进行数据增强来提高性能。
卷积神经网络是一种具有局部连接、 权重共享等特性的深层前馈神经网络。想要识别图像中的物体,就需要提取出比较好的特征,该特征应能很好地描述想要识别的物体。所以物体的特征提取是一项非常重要的工作。而图像中物体的特征以下几种特点:
1.物体的特征可能只占图像中的一小部分。比如下图中狗的鼻子只是图像中很小的一部分。
2.同样的特征可能出现在不同图像中的不同位置,比如下图中狗的鼻子在两幅图中出现的位置不同。
3.缩放图像的大小对物体特征的影响可能不大,比如下图是缩小后的图,但依然能很清楚的辨认出狗的鼻子。
而卷积神经网络中的卷积与池化操作能够较好地抓住物体特征的以上3
种特点。
- 卷积
卷积说白了就是有一个卷积核(其实就是一个带权值的滑动窗口)在图像上从左到右,从上到下地扫描,每次扫描的时候都会将卷积核里的值所构成的矩阵与图像被卷积核覆盖的像素值矩阵做内积。整个过程如下图所示,其中黄色方框代表卷积核,绿色部分代表单通道图像,红色部分代表卷积计算后的结果,通常称为特征图:
那为什么说卷积能够提取图像中物体的特征呢?其实很好理解,上图中的卷积核中值的分布如下:
当这个卷积核卷积的时候就会在 3 行 3 列的小范围内计算出图像中几乎所有的 3 行 3 列子图像与卷积核的相似程度(也就是内积的计算结果)。相似程度越高说明该区域中的像素值与卷积核越相似。(上图的特征图中值为 4 的位置所对应到的源图像子区域中像素值的分布与卷积核值的分布最为接近)这也就说明了卷积在提取特征时能够考虑到特征可能只占图像的一小部分,以及同样的特征可能出现在不同的图像中不同的位置这两个特点。
PS:卷积核的值是怎么确定下来的?很明显是训练出来的!
- 池化
池化就是将输入图像进行缩小,减少像素信息,只保留重要信息。 池化的操作也很简单,通常情况下,池化区域是 2 行 2 列的大小,然后按一定规则转换成相应的值,例如最常用的最大池化( max pooling )。最大池化保留了每一小块内的最大值,也就是相当于保留了这一块最佳的匹配结果。举个例子,如下图中图像是 4 行 4 列的,池化区域是 2 行 2 列的,所以最终池化后的特征图是 2 行 2 列的。图像中粉色区域最大的值是 6 ,所以池化后特征图中粉色位置的值是 6 ,图像中绿色区域最大的值是 8 ,所以池化后特征图中绿色位置的值是 8 ,以此类推。
从上图可以看出,最大池化不仅仅缩小了图像的大小,减少后续卷积的计算量,而且保留了最佳的特征(如果图像是经过卷积后的特征图)。也就相当于把图缩小了,但主要特征还在,这就考虑到了缩放图像的大小对物体的特征影响可能不大的特点。
- 全连接网络
卷积与池化能够很好的提取图像中物体的特征,当提取好特征之后就可以着手开始使用全连接网络来进行分类了。全连接网络的大致结构如下:
其中输入层通常指的是对图像进行卷积,池化等计算之后并进行扁平后的特征图。隐藏层中每个方块代表一个神经元,每一个神经元可以看成是一个很简单的线性分类器和激活函数的组合。输出层中神经元的数量一般为标签类别的数量,激活函数为softmax
(因为将该图像是猫或者狗的得分进行概率化)。因此我们可以讲全连接网络理解成很多个简单的分类器的组合,来构建成一个非常强大的分类器。
- 卷积神经网络大致结构
将卷积,池化,全连接网络进行合理的组合,就能构建出属于自己的神经网络来识别图像中是猫还是狗。通常来说卷积,池化可以多叠加几层用来提取特征,然后接上一个全连接网络来进行分类。大致结构如下:
使用Keras构建卷积神经网络
Keras 遵从了极简主义,提供了一些简单易用的接口,使得我们能够很快的构建出属于自己的卷积神经网络。
构建卷积神经网络的步骤如下:
- 实例化 Sequential 对象。
- 往 Sequential 对象中添加层(卷积层,池化层,全连接层......)。
实例化 Sequential 对象很简单,代码如下:
from keras.models import Sequential
model = Sequential()
有了 Sequential 对象之后就可以 add 了, add 的对象是 Layer 对象, Layer 对象有很多种,例如 Conv2D (卷积层), MaxPooling2D (最大池化层), Flatten (扁平层), Dense (全连接层)等。如果 add 一层全连接层,代码如下:
from keras.layers import Dense
model = Sequential()
'''
units=64表示这层有64个神经元
activation='relu'表示这层的激活函数是relu
'''
model.add(Dense(units=64, activation='relu')
想要 add 一层卷积层,代码如下:
from keras.layers import Conv2D
model = Sequential()
'''
16表示该卷积层有16个卷积核
kernel_size=3表示卷积核的大小为3*3
activation='relu'表示卷积层的激活函数是relu
input_shape=[IMAGE_HEIGHT, IMAGE_WIDTH, 3]表示待卷积图像为32*32的3通道图像
'''
model.add(Conv2D(16, kernel_size=3, activation='relu', input_shape=[32, 32, 3]))
想要 add 一层最大池化层,代码如下:
from keras.layers import Conv2D
model = Sequential()
'''
pool_size=2表示池化窗口的大小为2*2
'''
model.add(MaxPooling2D(pool_size=2))
想要 add 一层扁平层,代码如下:
from keras.layers import Conv2D
model = Sequential()
'''
卷积或者池化后想要接上全连接层之前需要接入扁平层
'''
model.add(Flatten())
最后,训练模型,代码如下:
model.fit(train_images, train_labels, epochs=5, batch_size=64,verbose=0)
第2关:迁移学习
# -*- coding: utf-8 -*-
import os
import numpy as np
import cv2
#设置图片高宽为48x48
IMAGE_SIZE = 48
#获取图像和标签
def get_img_and_label(dirname):
images = []
labels = []
sub_dirs = ['roses', 'sunflowers']
for dir in sub_dirs:
path = os.path.join(dirname, dir)
for filename in os.listdir(path):
img = cv2.imread(os.path.join(path, filename), 1)
img = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE))
images.append(img)
if dir == 'roses':
labels.append([0, 1])
else:
labels.append([1, 0])
images = np.array(images).reshape((-1, IMAGE_SIZE, IMAGE_SIZE, 3))/255.0
labels = np.array(labels).reshape((-1, 2))
return images, labels
#获取训练集数据与标签
train_images,train_labels = get_img_and_label('./step2/train')
#迁移VGG19模型的卷积层,并添加一个softmax层,对softmax层进行训练
#********* Begin *********#
from keras import layers
from keras import models
from keras.applications import vgg19
model_vgg19 = vgg19.VGG19(input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3), weights='imagenet',include_top=False)
for layer in model_vgg19.layers:layer.trainable=False;
model=models.Sequential()
model.add(model_vgg19)
model.add(layers.Flatten())
model.add(layers.Dense(2,activation='softmax'))
model.compile(optimizer='adam',loss='categorical_crossentropy',metrics=['accuracy'])
#********* End *********#
#训练模型
model.fit(train_images, train_labels, epochs=5, batch_size=64,verbose=0)
#保存模型
model.save('./step2/model/vgg19.model')
相关知识:
1、迁移学习
深度学习中,最强大的理念之一就是,有的时候神经网络可以从一个任务中习得知识,并将这些知识应用到另一个独立的任务中。例如,你已经训练好一个神经网络,能够识别像猫这样的对象,然后使用那些知识,或者部分习得的知识去帮助您更好地阅读** x 射线扫描图,这就是所谓的迁移学习。迁移学习最有用的场合是,如果你尝试优化任务 B 的性能, 通常这个任务数据相对较少,例如, 在放射科中你知道很难收集很多射线扫描图来搭建一个性能良好的放射科诊断系统, 所以在这种情况下, 你可能会找一个相关但不同的任务, 如图像识别, 其中你可能用一百万张图片训练过了, 并从中学到很多低层次特征**, 所以那也许能帮助网络在放射任务上做得更好, 尽管任务没有这么多数据。 迁移学习什么时候是有意义的? 它确实可以显著提高你的学习任务的性能, 但有时候也有些场合使用迁移学习时, 任务实际上数据量比任务要少,这种情况下增益可能不多。
2、利用keras进行迁移学习
卷积神经网络中的卷积核是用来提取图像更深层次的特征的,网上已经有很多经典的卷积神经网络,如 VGG19 模型,它们已经被训练好,我们可以直接把它们迁移过来使用,这样可以省去我们大量的训练时间,对于这次任务,我们只需要迁移模型训练好的卷积层,用它们来提取图像特征,最后,再添加上 softmax 层,并对其进行训练用来分类,在 keras 中使用代码如下:
from keras import layers
from keras import models
from keras.applications import vgg19
#加载vgg19模型卷积层
model_vgg19 = vgg19.VGG19()
#将卷积层冻结不训练
for layer in model_vgg19.layers:
layer.trainable=False
model=models.Sequential()
model.add(model_vgg19)
model.add(layers.Flatten())
#添加softmax层
model.add(layers.Dense(2,activation='softmax'))
VGG19()
方法中有三个常用参数:
input_shape:填入的是输入图片的形状
weights:等于'imagenet'表示的是使用表示加载在imagenet数据库上训练的预训练权重,定义为None则不加载权重,参数随机初始化
include——top: False表示不添加全连接层,Ture表示添加全连接层
最后再将模型进行编译就可以进行训练了。
第3关:神经风格迁移
# -*- coding: utf-8 -*-
from keras import backend as K
from keras.applications import vgg19
from keras.preprocessing.image import load_img, img_to_array
import numpy as np
#预处理图片
def preprocess_image(image_path):
img = load_img(image_path, target_size=(400, 400))
img = img_to_array(img)
img = np.expand_dims(img, axis=0)
img = vgg19.preprocess_input(img)
return img
#将图像由 BGR 格式转换为 RGB 格式
def deprocess_image(x):
x[:, :, 0] = x[:, :, 0] + 103.939
x[:, :, 1] = x[:, :, 1] + 116.779
x[:, :, 2] = x[:, :, 2] + 123.68
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype('uint8')
return x
#内容损失
def content_loss(base, combination):
return K.sum(K.square(combination - base))
#gram矩阵
def gram_matrix(x):
features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
gram = K.dot(features, K.transpose(features))
return gram
#风格损失
def style_loss(style, combination):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = 400 * 400
return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))
#总变差损失
def total_variation_loss(x):
a = K.square(
x[:, :400 - 1, :400 - 1, :] -
x[:, 1:, :400 - 1, :])
b = K.square(
x[:, :400 - 1, :400 - 1, :] -
x[:, :400 - 1, 1:, :])
return K.sum(K.pow(a + b, 1.25))
#神经风格迁移
def neural_style_transfer(img, epochs, learning_rate,target_image_path,style_reference_image_path):
'''
:param img: 预处理后的图像,类型为ndarray,shape为(样本个数,图像的宽,图像的高,通道数)
:param epochs: 梯度下降的迭代次数
:param learning_rate: 梯度下降的学习率
:param target_image_path:目标图片路径
:param style_reference_image_path:风格图片路径
:return: 神经风格迁移后的图像,其中图像的维度为(样本个数,图像的高,图像的宽,通道数)
'''
#设置保存内容,风格,目标图片的占位符
target_image = K.constant(preprocess_image(target_image_path))
style_reference_image = K.constant(preprocess_image(style_reference_image_path))
combination_image = K.placeholder((1, 400, 400, 3))
#将三张图片设为一个批量
input_tensor = K.concatenate([target_image,style_reference_image,combination_image], axis=0)
#加载vgg19模型
model = vgg19.VGG19(input_tensor=input_tensor,weights='imagenet',include_top=False)
#将层的名称映射为激活张量的字典
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])
#用于计算内容损失的层
content_layer = 'block5_conv2'
#用于计算风格损失的层
style_layers = ['block1_conv1',
'block2_conv1',
'block3_conv1',
'block4_conv1',
'block5_conv1']
#********* Begin *********#
#设置损失函数
loss = K.variable(0.)
layer_features = outputs_dict[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
#获取内容损失
loss += 0.025 * content_loss(target_image_features,combination_features)
#获取每一层的风格损失
for layer_name in style_layers:
layer_features = outputs_dict[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
loss += (1. / len(style_layers)) * sl
loss += 1e-4 * total_variation_loss(combination_image)
# 计算损失的梯度的变化
grads = K.gradients(loss, combination_image)[0]
# 设置函数以检索给定输入图像的损失和梯度值。
outputs = [loss, grads]
fetch_loss_and_grads = K.function([combination_image], outputs)
#获取损失值与梯度
def eval_loss_and_grads(x):
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1]
return loss_value, grad_values
#使用梯度下降更新图片
for i in range(epochs):
loss_value, grad_values = eval_loss_and_grads(img)
img -= learning_rate * grad_values
return img
你生成的图片与正确图片很相近
#********* End *********#
相关知识:
1、神经风格迁移流程
如何实现神经风格迁移呢,具体思路是,先随机的生成一张图片,它可能是这个样子:
是一张没有任何意义的图片,全是雪花点,然后将它跟内容图片,风格图片,三张图作为输入,利用已经训练好的卷积神经网络,提取风格图片的风格,内容图片的内容,随机生成图片的内容与风格。再将内容图片的内容与随机生成图片的内容构成一个内容损失函数,风格图片的风格跟随机生成图片的风格构成一个风格损失函数,再将两个损失函数相结合,构成我们最终要优化的损失函数。优化方法为梯度下降方法。这样不断的对随机生成的图片进行更新,最终就得到了我们需要的图片,具体流程如下:
-END-