Python是人工智能编程的首选语言,至少当作者在键盘上敲下这行话时,这是事实。作为一本Python基础入门性质的教科书,本书无法就Python在人工智能与深度学习中的应用展开深入讨论。要理解深度学习的内部细节,需要复杂的数学知识。不过作为应用层面的开发者,读者或者不需要理解深度学习复杂的数学细节,简单借助于开源的工具包和模型,也可以享受到人工智能的益处。本章通过图像风格迁移这个示例,让读者尝尝人工智能的味道。

版权声明

本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。

本文不可以以纸质出版为目的进行改编、摘抄。

22. 人工智能的味道 - 图像风格迁移

22.1 图像风格迁移

1559818000723

将上图中左上角图像的“风格”提取出来,应用到左下角的“原图”上,生成右方的图像,就是图像风格迁移的研究内容。 艺术风格是什么,即便在艺术家的视角看,也是见人见智的。显然不太可能从数学上准确地定义并求出图像的风格。到底怎么把一个缺乏明确数学定义的概念变成可以执行的程序,是困扰图像风格迁移的研究者的主要问题。

在将人工神经网络和深度学习应用于图像风格迁移之前,人们的主要思路就是分析某一种风格的图像,比如把毕加索画的全部画作进行统计分析,如小波分析,通过建立统计模型,然后迭代改变要做迁移的图像,让它更好地符合统计模型。这种方法有一个很大的缺点:一个程序只能做某一种风格或场景的图像风格迁移。

2015年,Gatsys等人在论文A Neural Algorithm for Artistic Style中首次将人工神经网络用于图像风格迁移,斯坦福大学的李飞飞和她的学生Justin Johnson等人于2016年进一步改进了算法,提高了图像风格迁移的速度。本章示例程序中图像风格迁移就是以Justin Johnson等人训练的深度神经网络模型为基础的。

22.2 深度神经网络

人工智能是一个研究了很多年的课题。它有众多的学派,其中符号学派将机器学习看作逆向演绎,联结学派对大脑进行逆向分析和模拟,进化学派在计算机上模拟生物进化,贝叶斯学派认为学习是一种概率推理形式,理论根基在于统计学。其中,联结学派的主要工具就是人工神经网络,人工神经网络模拟了大脑神经元和神经元间突触连接的结构和工作方式。而深度神经网络又是人工神经网络的一种。

1559827722696

上图展示了一个深度神经网络(Deep Neural Network) - DNN的示意图。图中的节点称之为神经元,神经元之间的连线模拟了人类大脑中脑细胞之间的突触连接。可以简单地认为,每个神经元具备计算或者是信号处理的功能,它通过突触从别的神经元获取输入,经过计算/处理后再通过突触向其它神经元输出信号。我们已知,大脑细胞之间的每个突触连接,其离子通道的导电能力是有差异的。与之对应,神经网络神经元之间的连接权重参数用来表征该连接的重要程度。

神经元的层次,数量,神经元之间的连接关系可以称为神经网络的结构。突触连接的权重、偏倚等信息则称为神经网络的参数。为满足特定任务的需要,比如在各种图片或者视频中识别出猫,工程师会设计特定的神经网络结构,并将大量经过标注的资料图片(图片中有猫的位置被人工标记)输入神经网络的输入层,通过神经网络的计算,从输出层获得识别结果,然后根据识别错误反向修改神经网络内部的参数,经过多次迭代后,神经网络的内部参数被修正到即便对于标注资料外的图片,神经网络也能识别出猫的程度。这一过程称为神经网络的训练。训练的结果称为模型,它包括了神经网络的结构以及内部参数。

猫长什么模样,在数学上很难精确定义。我们可以认为,上述神经网络的训练过程有点类似于人类学习的过程,经过训练的神经网络模型包括了“何种模式的图像是猫”的知识。这种知识虽然说不清道不明,但在实践中却非常有效。

同理,将梵高的星空等相似风格的画作作为数据集,也可以对深度神经网络进行训练,训练所得的模型包含了这类画作的”风格是什么“以及”如何把另一幅图片变成这种风格”的知识。本章的示例程序就是借助于Justin Johnson等人训练好的图像风格迁移网络模型,对图片进行风格应用的。在调入模型,创建好深度神经网络后,将图片”输入“给图像风格迁移网络,该网络会进行一系列的迭代计算,其输出就是应用了对应风格的被修改过的图片。

22.3 程序解读

本章的示例程序包含在目录C22_StyleTransfer当中,主程序为StyleTransfer.py。子目录images用于存储风格迁移的输入图片,models子目录用于存储风格图片及对应的图像风格迁移网络模型文件。其中,模型文件需要读者自行下载,详情请见Readme.txt。

该程序需要用到opencv-python以及matplotlib库。opencv是著名的C++语言编写的计算机视觉及图像处理工具包,本程序中,主要用它来读取、转换图片以及进行图像风格迁移网络的计算。matplotlib在本程序中用于交互和图片显示。

22.3.1 程序的使用

先下载并将模型文件(扩展名为.t7)存入models子目录。在Visual Source Code中打开StyleTransfer目录,打开StyleTransfer.py并运行即可。按上下方向键可以切换图片,按左右方向键则可以切换风格模型。如本章开始的样图所示,左上角是风格图片,左下角是原图,右边则是风格迁移的结果图片。由于风格迁移网络的计算量很大,所以切换图片或者风格时反应较慢。

22.3.2 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
class App:
def __init__(self):
self.idxImage = 0 #当前被风格迁移的图片在images列表中的下标
self.idxModel = 0 #当前使用的模型文件在models列表中的下标
self.images = glob.glob("images/*.jpg")
self.paintings,self.models = [],[]
t = glob.glob("models/*.jpg")
for x in t:
m = x[:-3] + "t7" #模型文件的扩展名为.t7, 基本名与对应的风格图片相同
if os.path.exists(m): #仅在风格图片及模型文件同时存在时,将二者加入列表
self.paintings.append(x)
self.models.append(m)

为了避免过多的全部变量污染名字空间,作者把相关信息组织在App类型中。

数据成员
- images: 包含images子目录内全部jpg图片文件路径的列表,形如[‘images\1.jpg’, ‘images\2.jpg’, ‘images\3.jpg’]。
- paintings: 包含models子目录风格图片路径的列表,形如[‘models\candy.jpg’, ‘models\composition_vii.jpg’,…]。
- models: 包含models子目录内模型文件路径的列表,形如[‘models\candy.t7’, ‘models\composition_vii.t7’,…]。 paintings与models列表内的风格图片与模型是一一对应的,且文件基本名相同,仅扩展名不同。
- idxImage: 当前被风络迁移的内容图片在images列表中的下标。
- idxModel: 当前使用的模型文件在models列表中的下标。

glob.glob(“images/*.jpg”) 遍历images子目录,找出其中的全部jpg文件,生成一个包含全部jpg文件路径字符串的列表。上述图像风格迁移网络的模型文件的扩展名为.t7,这是PyTorch的数据保存格式。而PyTorch是Facebook的深度学习开源工具包,具体一点可以把PyTorch看做加入了GPU 支持的numpy,同时也可以看成一个拥有自动求导功能的强大的深度神经网络。

22.3.3 风格迁移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import cv2 as cv    #导入opencv库
class App:
...
def styleTransfer(self):
net = cv.dnn.readNetFromTorch(self.models[self.idxModel]) #cv2.dnn_Net
net.setPreferableBackend(cv.dnn.DNN_BACKEND_OPENCV) #设置计算后台类型
inImg = cv.imread(self.images[self.idxImage]) #读取输入的内容图片
inp = cv.dnn.blobFromImage(inImg, 1.0, (inImg.shape[1],inImg.shape[0]),
(103.939, 116.779, 123.68), swapRB=False, crop=False)
net.setInput(inp)
outImg = net.forward()
outImg = outImg.reshape(3, outImg.shape[2], outImg.shape[3])
outImg[0] += 103.939
outImg[1] += 116.779
outImg[2] += 123.68
outImg /= 255
outImg = outImg.transpose(1, 2, 0)
return inImg, outImg

类App的styleTransfer()函数应用当前的图像风格迁移网络模型将当前内容图片风格迁移成输出图片。函数返回的inImg即为输入的内容图片,outImg则为输出图片,两者都是numpy三维数组。

读者可能会感到诧异,如此复杂的功能居然只有这么少的代码。是的,当你站在前人的肩膀上,只是调库(调用别人开发好的库)时,就是这么简单。

代码说明
- net = cv.dnn.readNetFromTorch() - 加载Torch格式的配置网络及其参数。执行后,net的类型为cv2.dnn_net,是一个深度神经网络。
- cv.imread()函数用于读取图像文件,其返回一个numpy的三维数组,其维度依次为图像的像素高,图像的像素宽,像素颜色通道数(通常为3)。
- 接下来,blobFromImage将inImg进行转换,变成深度神经网络所接受的数据格式;net.setupInput(inp)则把转换后的数据设定为神经网络的输入; net.forward()则应用神经网络进行计算,并在其输出层得到输出图像outImg;outImg也是一个numpy多维数组,其维度与数据格式与常规的图像有所不同;所以后续代码对其进行了一些格式变换。
- 最后,函数返回inImg-输入图像, outImg-输出图像两个用于表示图像的多维数组。

22.3.4 图像显示与刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
class App:
...
def refresh(self,fig,axStyle,axIn,axOut):
print("Style tranfering...")
self.idxImage = self.idxImage % len(self.images)
self.idxModel = self.idxModel % len(self.models)
inImg, outImg = self.styleTransfer()
print("Rendering...")
styleImg = cv.imread(self.paintings[self.idxModel])
axStyle.imshow(cv.cvtColor(styleImg,cv.COLOR_BGR2RGB))
axIn.imshow(cv.cvtColor(inImg, cv.COLOR_BGR2RGB))
axOut.imshow(cv.cvtColor(outImg, cv.COLOR_BGR2RGB))
fig.canvas.draw()

函数refresh()负责对当前内容图像进行风格迁移运算,并将风格风像,内容图像,迁移后的输出图像显示在对应的子图(Axes)中。其中,axStyle用于显示风格图像,axIn显示内容图像,axOut用于显示输出图像。

self.idxImage和self.idxModel分别对len(self.images)和len(self.models)进行求模,目的是防止列表的越界访问(后续代码中,切换图片和风格时没有进行越界限制)。

axStyle/axIn/axOut.imshow()负责将图像显示在子图中。请注意,在显示之前,还应用cv.cvtColor()函数进行了一次图像格式转换,这是因为OpenCV中的图像其颜色通道顺序为BGR,即三维数组第三维中下标0对应蓝色,下标1对应绿色,下标2对应红色;而Matplotlib中,图像的颜色通道顺序为RGB,为了能正确显示,交换一下颜色通道顺序。

1
2
3
4
5
6
7
8
9
10
11
...
fig = plt.figure(figsize=(12,6))
axStyle = plt.subplot(231)
axIn = plt.subplot(234)
axOut = plt.subplot2grid((2,3),(0,1),rowspan=2,colspan=2)
plt.subplots_adjust(0,0,1,1,0,0) #设置子图间的间距为0
for ax in (axStyle,axIn,axOut):
ax.set_axis_off() #子图不显示坐标轴
app.refresh(fig,axStyle,axIn,axOut) #刷新显示
fig.canvas.mpl_connect('key_release_event',on_key_release) #连接键盘事件响应函数
plt.show()

我们在matplotlib中创建1个图和3个子图。plt.subplot(231)将图的可视区域等分成2行3列,然后在第1个区域创建一个子图。同理,plt.subplot(234)将图的可视区域等分成2行3列,然后在第4个区域(即第2行的第1个区域)创建子图。上述第1个区域,第4个区域是从1开始计数的。

plt.subplot2grid()也是将图的可视区域分成2行3列,然后在第0行的第1列开始创建子图,子图横向占2列,纵向占两行。这里的第0行,第1列从0开始计数,显然,这里的计数方式跟subplot()函数不同。读者不必为此感到困惑,这没有为什么。作者将此差异视作matplotlib设计者犯下的一个小错误,如果全部保持相同的计数规则,对于使用者而言,可能可容易理解一些。

fig.canvas.connect()函数连接键盘松开事件至on_key_release()函数, 当读者松开(release)键盘按键时,on_key_release()函数将被调用执行。

22.3.5 交互响应

1
2
3
4
5
6
7
8
9
10
11
12
def on_key_release(event):
if event.key == 'up':
app.idxImage-=1
elif event.key == 'down':
app.idxImage+=1
elif event.key == 'left':
app.idxModel-=1
elif event.key == 'right':
app.idxModel+=1
else:
return
app.refresh(fig,axStyle,axIn,axOut)

on_key_release()函数响应按键松开事件。如果是上-up, 下-down键,修改idxImage切换内容图片;如果是左(left),右(right)键,修改idxModel切换图像风格迁移网络模型。最后,调用app.refresh()函数进行风格迁移和界面刷新。

22.4 小结

下图是重庆大学虎溪校区图书馆及其在云湖上的倒影经过风格迁移后的效果。

1559813765575

现实中很多类似的计算问题,人类无法对相关概念进行准确的数学定义。比如:什么是图像的风格? 何种形式的图像模式是一只猫? 在乳腺X片里,什么样的图像可能是恶性的结节? 在这种情况下,使用经过标注的数据集来训练神经网络,经过训练的神经网络从数据集中学习了相关的知识。这些知识包含在神经网络的内部参数中。即便这种知识的表达和真正的数学含义有些说不清,道不明,但我们仍然可以应用这些经过训练的神经网络来解决各种现实问题。


本文内容节选自作者编著的《Python编程基础及应用》(高等教育出版社)一书。

Python编程基础及应用

免费随书B站MOOC: