DeepDream:使用深度学习再造毕加索抽象风格艺术画

时间:2024-04-04 12:14:44

毕加索是近代最成功的艺术家,是抽象画派的开山师祖,而且凭借那些惊悚的抽象线条创造出来的画作非常挣钱。毕加索这种抽象创造能力能不能用计算机实现呢,随着深度学习的进一步发展,答案是肯定的。

DeepDream:使用深度学习再造毕加索抽象风格艺术画

抽象艺术能给人带来特殊的体验在于一种名为Pareidolia的心理效应,它让我们从混乱无序的信号刺激中查找模式或规律,这是进化带给人的生存优势。之所以产生这种效应,是因为大脑的颞叶皮层存在一个区域叫梭状回,该区域的神经元能从混乱随机的现象中抽取规律,倘若我们能把这些神经元的识别功能转换成算法应用到计算机视觉上,那么我们就有可能像毕加索那样,使用计算机绘制出让促动人内心的抽象画。

我们前面章节介绍过用于图像识别的网络层VGG16,它通过读取图片内容,计算图片特征来识别图片。在抽象画中,画面的像素组合所形成的图案能刺激人大脑,让人从混乱的点线中寻求模式或规律,从而形成一系列独特的感受,试想如果我们把大量的抽象画当做训练数据输入到VGG16等相关网络,当网络读取这些图片后,是否能识别出抽象画的潜在规律,然后使用这些规律去画画,从而产生也能让我们看到毕加索抽象画时那种感觉的作品呢?我们前面章节使用的是VGG16网络层来识别图片,当时我们提到过还有很多功能类似于VGG16的网络层,这次我们使用google开发的卷积网络inception_v3来读取具有抽象性质的图片,看看它能从中获取什么信息。我们看下面这幅画:

DeepDream:使用深度学习再造毕加索抽象风格艺术画

上图其实是普通的花儿照片,问题在于花朵的图像比较特别,它能让你误以为是蝴蝶,有时你又能从中读出一幅扭曲的人脸,于是这些花朵就就被了“抽象性”,我们先加载网络层,然后使用它来检测这幅图像对应什么物体,相关代码如下:

#加载intception卷积网络层
from keras.applications import inception_v3
from keras import backend as K
from keras.preprocessing import image

K.set_learning_phase(0)
model = inception_v3.InceptionV3(weights='imagenet', include_top = True)

运行上面代码后,程序会把inceptionV3网络层的参数从网上读下来,并构建相应的网络层模型,接着我们对图片进行预处理,以便图片能读入inceptionV3进行识别,相关代码如下:

def preprocess_image(img_path):
    img = image.load_img(img_path)
    img = image.img_to_array(img)
    img = np.expand_dims(img, axis = 0)
    img = inception_v3.preprocess_input(img)
    return img

inceptionV3网络层像VGG16那样,能识别1000多种常见物体,我们将图片输入网络,看看它决定图片中的东西是什么:

import keras.applications.imagenet_utils as utils
img = preprocess_image('flower.png')
preds = model.predict(img)
for n, label , prob in utils.decode_predictions(preds)[0]:
    print(label, prob)

下面是网络预测的结果:
DeepDream:使用深度学习再造毕加索抽象风格艺术画
首先它认为是蜜蜂,小黄花确实和黄色蜜蜂很像,sulphur_butterfly是一种黄色小蝴蝶,cabbage_butterfly是一种白色小蝴蝶,从上图我们看到,花朵有黄有白,因此网络读取不同部分后给出了不同结果。网络对图片的识别与人脑识别的原理类似,它抓取不同的色块,然后从这些色块的图案中抽取出模式,并将模式与其他有类似模式的物体做比较,如果相似度高的话,那么就认为这个色块对应的就是该物体。

如果我们能把这种人脑识别机制转换为算法,那么计算机就能构造出具有抽象化性质的作品,其本质无法就是让点,线,颜色以某种模式分布,当人脑看到这些分布后,自动抽取出模式即可。抽象画相比于传统画作,最大的区别在于它往往没有确切的形象或是要表达的意象,传统画作比拼的是具体和生动,但缺点在于它极大的约束了人的想象力,例如达芬奇的《蒙娜丽莎》你看了之后,很难引发出对男人或动物的想象,抽象画由于没有具体指向,因此观察者没有先入为主的约束,于是在线条,色块,或者是构图形态的刺激下引发无限的发散联想。有些人对线条敏感,有些人对色块敏感,有些人对构图敏感,那么如何判别某个人的敏感源呢,办法也不难,我们分别对三种元素做微小改变,例如小小修改线条的长短或粗细,微微更改色块的颜色或亮度,轻轻的挪移一下构图,看哪一种微小改变对观察者的刺激最大,我们就能确定哪种元素是观察者的敏感源,这种想法就是我们下面要介绍算法deep dream的基本原理。

我们使用下面代码看看inceptionV3网络层结构:

print(model.summary())

得到结果如下图:

DeepDream:使用深度学习再造毕加索抽象风格艺术画

我们看到这个网络模型异常庞大,单卷积网络就达到94层,全网络总共有2400万个参数!前面我们在讲解卷积网络时提到过,网络层越低,它从图片中抓取的信息就越容易理解,在前面章节我们把底层网络抓取的信息绘制出来时,我们可以看到低层次的网络抓取了图片中物体的边缘信息,如果图片里是动物,那么他们的眼睛,鼻子都信息会被抓取,但我们试图将高层次网络抓取的信息绘制出来时,发现画不出来,因为高层网络抽取的是图片中物体抽象信息,基本上无法使用形象化的方法表现出来,正所谓”道可道,非常道“。

虽然无法直观的将高层级网络识别的信息绘制出来,但可以通过”旁敲侧击"的方式推测高层级网络抓取的信息特点,我们前面讲过的如何识别抽象画中那些元素对人产生刺激的方法就可以应用到这里,例如我们想直观的感受上图所示最后一层卷积网络activation_94,我们可以更改输入图像中像素点的值,使得最后一层卷积层的到最大的‘刺激’,这里我们需要精确的定义何为‘刺激’,我们构造一个数学函数,函数的输入就是activation_94网络层输出的值,函数的输出就定义为网络层接收到的‘刺激’,要想增强activation_94网络层受到的刺激,我们就得调整输入图片每个像素点的值,使得函数的输出值最大,问题是如何调整呢?

假设用于定义网络层的刺激的函数为Stimulate(activation_94),最后一层网络层的输出结果显然要取决于输入图像,神经网络inceptionV3从输入层读入图片,经过中间网络层的计算最后抵达最后一层,最后一层再经过一次运算后输出结果,也就是说最后一层的输出与输入图像间存在对应关系,我们把这个对应关系定义为activation_94 = inceptionV3(image),其中image对应输入图片的像素点,inceptionV3对应网络对图片的计算过程,于是上面函数转换为Stimulate(inceptionV3(image)),我们目的是希望activation_94网络层受到的刺激最大化,也就是说我们希望通过调整输入图片的像素点,使得Stimulate函数输出的结果增大。这个调整方式我们到现在应该很熟悉了,那就是对每个像素点求偏导,前面我们在训练网络时,希望调整网路里神经元间的链路参数,使得损失函数结果变小,也是就在损失函数的基础上对链路参数求偏导,然后链路参数按照偏导数指向的方向进行反向调节。

我们现在的希望是增大Stimulate(inceptionV3(image))输出结果,于是我们就可以针对image里的每个像素点求偏导,然后按照偏导数指向的方向调整每个像素点的值,这种调整反复进行一定的次数后,所得的图像就是具有抽象或玄幻风格的图像,我们看个具体例子,下面是一张输入图片:

DeepDream:使用深度学习再造毕加索抽象风格艺术画

左边对应的是输入图片,右边对应的是输出图片,图片经过调整后出现了抽象画线条,从而使得图片多了几分玄幻色彩,接下来我们就用代码将算法实现出来,顺便说一下该算法是由google工程师提出的,他们把该算法命名为deep dream,我们先看看算法的流程图:

DeepDream:使用深度学习再造毕加索抽象风格艺术画

根据上图流程所示,我们先对图片连续做三次等比例缩小,该比例是1.4,之所以要缩小,图片缩小是为了让图片的像素点调整后所得结果图案能显示得更加平滑。缩小三次后,我们把图片每个像素点当做参数,对他们求偏导,这样我们就可以知道如何调整图片像素点能够对给定网络层的输出产生最大化的刺激,用数学公式表示就是:

DeepDream:使用深度学习再造毕加索抽象风格艺术画

其中Stimulate就是我们定义在网络层activation_94输出结果的刺激,inceptionV3表示输入图片与网络层activation_94输出结果间的函数关系,上面求偏导数时,使用了链式求导法则,接下来我们结合代码实现,就能对算法加深理解:

#定义要刺激的网络层
def get_layer_to_stimulate(model, layer_num):
    #选中的网络层名字
    layer =  "activation_" + str(layer_num)
    activation = model.get_layer(layer).output
    return activation

def define_stimulation(activation):
    '''
    假设网络层的输出是x1,x2...xn
    刺激函数的定义为(x1^2 + x2^2 + .... xn^2) / (x1 * x2 .... *xn)
    '''
    #先把对应网络层的输出元素相乘
    scaling = K.prod(K.cast(K.shape(activation), 'float'))
    #2:-2作用是排除图像边界点对应的网络层输出
    stimulate = K.sum(K.square(activation[:, 2:-2, 2:-2, :])) / scaling
    return stimulate

#输入图片像素点
dream = model.input
#刺激第41层
activation = get_layer_to_stimulate(model, 41)
#对每个像素点求偏导,实现刺激函数最大化
stimulate = define_stimulation(activation)
grads = K.gradients(stimulate, dream)[0]
#对每个偏导数做正规化处理
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)
iterate_grad_ac_step = K.function([dream], [stimulate, grads])

上面代码定义了刺激函数,要刺激的网络层,接下来我们对图片进行缩放等相关操作:

#定义图片相关操作
import scipy
#把二维数组转换为图片格式
def deprocess_image(x):
    if K.image_data.format() == 'channel_first':
        x = x.reshape((3, x.shape[2], x.shape[3]))
        x = x.transpose((1, 2, 0))
    else:
        x = x.reshape((x.shape[1], x.shape[2], 3))
    
    x /= 2.
    x += 0.5
    x *= 255
    x = np.clip(x, 0, 255).astype('int8')
    return x

def resize_img(img ,size):
    img = np.copy(img)
    factors = (1, float(size[0]) / img.shape[1], float(size[1]) / img.shape[2], 1)
    return scipy.ndimage.zoom(img, factors, order=1)

def save_img(img, fname):
    pil_img = deprocess_image(np.copy(img))
    scipy.misc.imsave(fname, pil_img)

接着我们添加图片缩放处理流程代码:

#将图片进行4次缩放处理
num_octave = 4
#定义每次缩放的比例为1.4
octave_scale = 1.4
#每次对图片进行20次求偏导
iterations = 20
#限制刺激强度不超过20,如果刺激强度太大,产生的图片效果就好很难看
max_stimulate = 20
base_image_path = path = '/Users/chenyi/Documents/人工智能出书/数据集/第9章/9-28.jpg'
#将图片转换为二维数组
img = preprocess_image(base_image_path)
original_shape = img.shape[1:3]
successive_shapes = [original_shape]
#以比例1.4缩小图片
for i in range(1 , num_octave):
    shape = tuple(int(dim / (octave_scale ** i)) for dim in original_shape)
    successive_shapes.append(shape)
#将图片比率由小到达排列
successive_shapes = successive_shapes[::-1]
original_img = np.copy(img)
#将图片按照最小比率压缩
shrunk_original_img = resize_img(img, successive_shapes[0])
print(successive_shapes)

最后我们根据输入图片,调整图片像素点,实现指定网络层输出结果的输出刺激,然后把调整后的图片存储起来,代码如下:

#像素调整次数
MAX_ITRN = 20
#刺激最大值不超过20
MAX_STIMULATION = 20
#像素点的调整比率
learning_rate = 0.01

def gradient_ascent(x, interations, step, max_loss=None):
    '''
    通过对输入求偏导的方式求取函数最大值
    '''
    for i in range(iterations):
        loss_value, grad_values = iterate_grad_ac_step([x])
        if max_loss is not None and loss_value > max_loss:
            break
        #根据偏导数调整输入值
        x += step * grad_values
    return x

for shape in successive_shapes:
    print('Processing image shape, ', shape)
    #变换图片比率
    img = resize_img(img, shape)
    img = gradient_ascent(img, MAX_ITRN, step=learning_rate,
                         max_loss = MAX_STIMULATION)
    #把调整后的图片等比例还原
    upscaled_shrunk_original_img = resize_img(shrunk_original_img, shape)
    same_size_original = resize_img(original_img, shape)
    '''
    图片缩小后再放大回原样会失去某些像素点的信息,我们把原图片和缩小再放大回原样的图片相减
    就能得到失去的像素点值
    '''
    lost_detail = same_size_original - upscaled_shrunk_original_img
    #把失去的像素点值加回到放大图片就相当于调整后的图片与原图片的结合
    img += lost_detail
    #按照比率缩放图片
    shrunk_original_img = resize_img(original_img, shape)
    file_name = fname='dream_at_scale_' + str(shape) + '.png'
    print('save file as : ', file_name)
    save_img(img, file_name)

save_img(img, fname='final_dream.png')

在执行上面代码时,我们输入图片如下:
DeepDream:使用深度学习再造毕加索抽象风格艺术画

图片像素点经过不断调整后,形成图案如下:

DeepDream:使用深度学习再造毕加索抽象风格艺术画

我们看到图片中增加了很多动物形态的混合图案,这是因为inceptionV3网络在训练是输入了大量的动物图片,同时接受刺激的activation_41网络层,它的作用应该是对图片中“构图”信息的抽取,因此我们在调整图片像素点查看对应网络层抽取什么类型信息时,我们从调整结果看到很多动物轮廓的混合,这表明对应网络层从图片中抓取的信息是输入图案中图像对象的”构图“信息。由于我们无法直接把对应网络成输出数据通过形象化的方法展现出来,但我们通过一种侧面对应的方式同样可以掌握对应网络层在识别图案的那些内容,从而加深对网络层运行机制的理解。

更详细的讲解和代码调试演示过程,请点击链接

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
DeepDream:使用深度学习再造毕加索抽象风格艺术画