深度神经网络是如何成为图像大师的?
深度学习为何如此有效仍是个谜。
在本文中,我们将尝试用神经网络为我们绘制抽象的图像,然后对这些图像进行解释,以便对神秘面纱下发生的事情有更好的理解。
看完这篇文章,你将学会生成一些图像,如下所示。
(所有内容都少于100行PyTorch 代码。随附的Jupyter notebook:https://github.com/paraschopra/abstract-art-neural-network)
这幅图像是如何生成的?
这张图片是由一个简单的架构—复合模式生成网络(CPPN)生成的。
(你可以通过http://blog.otoro.net/2015/06/19/neural-network-generative-art/这篇文章来了解。)
文章中,作者通过用JavaScript编写的神经网络生成抽象图像。而本文用PyTorch实现了它们。
通过神经网络生成图像的一种方法是让它们一次性输出完整的图像,比如说,下面的被称为“生成器”的神经网络将随机噪声作为输入,并在输出层中生成整个图像(以及宽度*高度)。
与输出整个图像不同,CPPN在给定位置(作为输入)输出像素的颜色。
忽略上面图像中的z和r,注意网络正在获取像素的x,y坐标,并输出该像素应该是什么颜色(用c表示)。这种网络的PyTorch模型如下所示:
class NN(nn.Module):
def __init__(self):
super(NN, self).__init__()
self.layers = nn.Sequential(nn.Linear(2, 16, bias=True),
nn.Tanh(),
nn.Linear(16, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 3, bias=False),
nn.Sigmoid())
def forward(self, x):
return self.layers(x)
请注意,它接受2个输入,并有3个输出(像素的RGB值)。生成整个图像的方法是输入所需图像(特定大小)的所有X、Y位置,并将这些X、Y位置的颜色设置为网络输出的颜色。
神经网络实验
也许,当你尝试运行上面的神经网络时,你会生成下面的图像:
也许你会充满疑问:为什么不管你提供的是什么x,y位置,网络输出的都是灰色?理想情况下,这不应该发生,因为这个网络如此的深。更改输入值就应更改输出值。
每次神经网络初始化时,由于参数(权重和偏差)的随机初始化,它都有可能生成一个全新的图像。但往往即使经过几次尝试,你从神经网络中得到的都是这种一片灰色。为什么?
有人可能会说,是所用的特定激活函数-tanh 的问题。可能后续层中的多个tanh序列在输出层(代表灰色)将所有输入数字压缩到接近0.5。然而,本文最开始推荐的那篇文章中也使用了tanh。我们所做的只是将博客中用JavaScript编写的神经网络转换成PyTorch而没有做任何更改。
根源问题在哪里?
当一个新的神经网络被初始化时,PyTorch是如何初始化权重的?根据用户论坛,他们用从-1/sqrt(N)到+1/sqrt(N)随机抽取的数字初始化权重。其中n是一个层中传入连接的数量。因此,如果对于隐藏层N=16,权重将会被初始化为-1/4 到 +1/4之间。所以,我们可以做如下猜测:假设产生一片灰色的原因是因为权重的范围很小,而且变化不大。
如果网络中的所有权重都在-1/4到+1/4之间,当乘以任何输入并加在一起时,可能会发生类似中心极限定理的效果。
中心极限定理(CLT)证明:在某些情况下,添加独立随机变量,即使原始变量本身不是正态分布,它们适当归一化的和也趋向于正态分布(非正式地称为“钟形曲线”)。
回想如何计算后续层上的值。
在我们的例子中,第一个输入层有2个值(x,y),第二个隐藏层有16个神经元。所以,第二层上的每个神经元得到2个值乘以权重,这些权重值在-1/4 到 +1/4之间。这些值被求和,然后在它从激活函数tanh开始后,成为要传递到第三层的新值。
现在,从第二层开始,有16个输入要传递到第三层的16个神经元中的每一个。假设这些值中的每一个都用z表示,那么第三层中每个神经元的值是:
这是我们的另一个猜测。由于权重的方差较小(-1/4到+1/4),z的值(即输入x,y乘以权重,然后通过tanh函数)也不会变化太多(因此会相似)。所以这个方程可以看作:
对于每个神经元,从-0.25到+0.25的16个权重之和的最可能值是零。即使在第一层,和不接近零,网络的八层给了上述方程足够的机会使最终产生接近零的值。因此,不管输入值(x,y)如何,进入激活函数的总值(权重乘以输入的综合)总是接近零值,tanh映射为零(因此,所有后续层中的值保持为零)。
x轴是tanh的输入,y轴是输出。请注意,0映射到0。
灰色的原因是什么?这是因为s形函数(最后一层的激活功能)将这个输入值取零,并映射到0.5(表示灰色,0表示黑色,1表示白色)。
注意s形函数如何将输入值0映射到0.5。
如何修复一片灰色?
由于根源是权重的微小变化,我们下一步要做的就是增加它。更改默认的初始化函数,将权重从-100分配到+100(而不是-1/4到+1/4)。现在运行神经网络,我们可以得到:
哇!一片灰色现在是一些颜色的斑点。
现在有了一些进展。我们的假设是正确的。但是生成的图像仍然没有太多的结构。这太简单了。
这个神经网络在表面下所做的就是将输入与权重相乘,推动它们通过tanh,最后通过s形函数输出颜色。既然我们固定了权重,那么可以修改输入以使输出图像更有趣吗?当然。
请注意,上面的图像是在输入x,y作为原始像素坐标时生成的,这些坐标从0,0开始到128,128结束(这是图像的大小)。这意味着我们的网络从来没有一个负数作为输入,而且由于这些数字很大(比如x,y可以是100,100),tanh函数要么得到一个很大的数字(它被压缩到+1),要么得到一个很小的数字(它被压扁到-1)。这就是我们看到原色的简单组合的原因(例如,0,1,1的R,G,B输出代表你在上图中看到的青色)。
如何使图像变得有趣?
就像在文章最开始提到的那篇文章中一样,我们将x和y标准化。因此,我们不输入x,而是输入(x/image_size)-0.5。这意味着x和y的值范围为-0.5到+0.5(不考虑图像大小)。这样就得到了以下图像:
还是有一些进展的!
有趣的是,在前一幅图像中,线条一直向右下角增长(因为x,y值在增加)。这里,因为x,y值是标准化的并且现在包含负数,所以这些线向外均匀地增长。
然而,图像仍然不够漂亮。
如何使图像更有趣一些?
如果你仔细观察,你会发现在图像的中间,似乎有比边缘更多的结构。这是数学之神给我们的暗示,我们应该放大那里去寻找美。
有三种放大图像中心的方法:
· 生成一幅大图像。由于像素坐标是标准化的,我们可以简单地运行神经网络来生成更大的图像。然后,我们可以通过图像编辑工具放大中间部分,看看我们发现了什么。
· 将x和y输入乘以少量(缩放因子),这将有效地实现与前一种方法相同的结果(并避免我们在其他无趣区域进行浪费的计算)。
· 由于输出是由输入乘以权重决定的,因此我们也可以通过将权重值从-100、+100减少到+3、-3等其他值来进行缩放而不是减少输入值(同时记住不要过度减少。还记得如果权重在-0.25到+0.25范围内就会出现一片灰色吗?)。
当我们采用第二种方法将x和y乘以0.01时,得到了:
当采用第三种方法并将权重初始化为介于-3和+3之间时,这是我们得到的图像:
你的思维打开了吗?
更多的实验
将权重初始化更改为正态分布(平均值为0,标准差为1),并生成多个图像(下图是从随机初始化开始的)。
当移除所有隐藏层(仅输入到输出映射)时:
0个隐藏层
当仅保留一个隐藏层(而不是默认的8个隐藏层)时:
1个隐藏层
当把隐藏层的数量加倍到16层时:
16个隐藏层,每层有16个神经元
正如你所能想象的,随着增加隐藏层的数量,图像变得越来越复杂。如果不是将层加倍,而是将层的数量保持不变(8),但是将每层神经元的数量加倍(从16到32),会发生什么?我们得到的是:
8个隐藏层,每层32个神经元
请注意,尽管在上述两种情况下,网络中的权重总数是相似的,但具有两个层的网络比每层双倍神经元的网络更像素化。像素表示,在这些区域中,函数变化剧烈,因此如果我们进一步缩放,会发现更多的结构。而对于层数不变但每层神经元数量加倍的网络,其功能相当平滑,因此“可缩放性”较小。
当然,所有这些都是深度使神经网络更具表现力的另一种说法。
计算函数的复杂度随深度呈指数增长。
这正是我们所看到的。一般的逼近定理认为,理论上,一个足够大的神经网络,即使有一个隐藏层,也可以表示任何函数。但在实践中,网络越深,输入到输出映射就越复杂。
毫无意义但很有趣的实验
如果我们把每层神经元的数量从8个增加到128个(数量级的增加)。
神经-波洛克!
如果我们从每一个隐藏层128个神经元开始,然后像下面这样逐渐地在后续层将它们减半。
self.layers = nn.Sequential(nn.Linear(2, hidden_n, bias=True),
nn.Tanh(),
nn.Linear(128, 64, bias=False),
nn.Tanh(),
nn.Linear(64, 32, bias=False),
nn.Tanh(),
nn.Linear(32, 16, bias=False),
nn.Tanh(),
nn.Linear(16, 8, bias=False),
nn.Tanh(),
nn.Linear(8, 4, bias=False),
nn.Tanh(),
nn.Linear(4, 3, bias=False),
nn.Sigmoid())
我们得到的是:
这个看起来比其他的更“自然”。
有很多实验可以做并且得到有趣的图像,你可以尝试更多的架构、激活和层次。