深度学习为何如此有效仍是个谜。

在本文中,我们将尝试用神经网络为我们绘制抽象的图像,然后对这些图像进行解释,以便对神秘面纱下发生的事情有更好的理解。

看完这篇文章,你将学会生成一些图像,如下所示。

(所有内容都少于100行PyTorch 代码。随附的Jupyter notebook:https://github.com/paraschopra/abstract-art-neural-network)

ccbc3c3c12889b99366afcc3608c39d6.png
c517769ce2e59375bfbdeef5a472ab87.png

这幅图像是如何生成的?

这张图片是由一个简单的架构—复合模式生成网络(CPPN)生成的。

(你可以通过http://blog.otoro.net/2015/06/19/neural-network-generative-art/这篇文章来了解。)

文章中,作者通过用JavaScript编写的神经网络生成抽象图像。而本文用PyTorch实现了它们。

通过神经网络生成图像的一种方法是让它们一次性输出完整的图像,比如说,下面的被称为“生成器”的神经网络将随机噪声作为输入,并在输出层中生成整个图像(以及宽度*高度)。

61a1c7da979d90c87c03de7ed3a02a9b.png

与输出整个图像不同,CPPN在给定位置(作为输入)输出像素的颜色。

3e338e45f7092c0a1a5105d1cba3f2f6.png

忽略上面图像中的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位置的颜色设置为网络输出的颜色。

c517769ce2e59375bfbdeef5a472ab87.png

神经网络实验

也许,当你尝试运行上面的神经网络时,你会生成下面的图像:

2c28d91ef66b41f99952beffeaa97c83.png

也许你会充满疑问:为什么不管你提供的是什么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)证明:在某些情况下,添加独立随机变量,即使原始变量本身不是正态分布,它们适当归一化的和也趋向于正态分布(非正式地称为“钟形曲线”)。

回想如何计算后续层上的值。

398edbd62a8c54a298f02033644b4f85.png

在我们的例子中,第一个输入层有2个值(x,y),第二个隐藏层有16个神经元。所以,第二层上的每个神经元得到2个值乘以权重,这些权重值在-1/4 到 +1/4之间。这些值被求和,然后在它从激活函数tanh开始后,成为要传递到第三层的新值。

现在,从第二层开始,有16个输入要传递到第三层的16个神经元中的每一个。假设这些值中的每一个都用z表示,那么第三层中每个神经元的值是:

c318a6936f3acb9b3c11d9d6cbb2dd2e.png

这是我们的另一个猜测。由于权重的方差较小(-1/4到+1/4),z的值(即输入x,y乘以权重,然后通过tanh函数)也不会变化太多(因此会相似)。所以这个方程可以看作:

7ec498147fa7027bf7f7ab65c8aeaacc.png

对于每个神经元,从-0.25到+0.25的16个权重之和的最可能值是零。即使在第一层,和不接近零,网络的八层给了上述方程足够的机会使最终产生接近零的值。因此,不管输入值(x,y)如何,进入激活函数的总值(权重乘以输入的综合)总是接近零值,tanh映射为零(因此,所有后续层中的值保持为零)。

cba2233c813740848129624b76a60085.png

x轴是tanh的输入,y轴是输出。请注意,0映射到0。

灰色的原因是什么?这是因为s形函数(最后一层的激活功能)将这个输入值取零,并映射到0.5(表示灰色,0表示黑色,1表示白色)。

6ab9d2ab2d9d5e3b0847ca04c52d54fe.png

注意s形函数如何将输入值0映射到0.5。

c517769ce2e59375bfbdeef5a472ab87.png

如何修复一片灰色?

由于根源是权重的微小变化,我们下一步要做的就是增加它。更改默认的初始化函数,将权重从-100分配到+100(而不是-1/4到+1/4)。现在运行神经网络,我们可以得到:

34e45e48dfa6426309f079effaef40d7.png

哇!一片灰色现在是一些颜色的斑点。

现在有了一些进展。我们的假设是正确的。但是生成的图像仍然没有太多的结构。这太简单了。

这个神经网络在表面下所做的就是将输入与权重相乘,推动它们通过tanh,最后通过s形函数输出颜色。既然我们固定了权重,那么可以修改输入以使输出图像更有趣吗?当然。

请注意,上面的图像是在输入x,y作为原始像素坐标时生成的,这些坐标从0,0开始到128,128结束(这是图像的大小)。这意味着我们的网络从来没有一个负数作为输入,而且由于这些数字很大(比如x,y可以是100,100),tanh函数要么得到一个很大的数字(它被压缩到+1),要么得到一个很小的数字(它被压扁到-1)。这就是我们看到原色的简单组合的原因(例如,0,1,1的R,G,B输出代表你在上图中看到的青色)。

c517769ce2e59375bfbdeef5a472ab87.png

如何使图像变得有趣?

就像在文章最开始提到的那篇文章中一样,我们将x和y标准化。因此,我们不输入x,而是输入(x/image_size)-0.5。这意味着x和y的值范围为-0.5到+0.5(不考虑图像大小)。这样就得到了以下图像:

95a97c77d4536e54702986a6cbfce639.png

还是有一些进展的!

有趣的是,在前一幅图像中,线条一直向右下角增长(因为x,y值在增加)。这里,因为x,y值是标准化的并且现在包含负数,所以这些线向外均匀地增长。

然而,图像仍然不够漂亮。

c517769ce2e59375bfbdeef5a472ab87.png

如何使图像更有趣一些?

如果你仔细观察,你会发现在图像的中间,似乎有比边缘更多的结构。这是数学之神给我们的暗示,我们应该放大那里去寻找美。

有三种放大图像中心的方法:

· 生成一幅大图像。由于像素坐标是标准化的,我们可以简单地运行神经网络来生成更大的图像。然后,我们可以通过图像编辑工具放大中间部分,看看我们发现了什么。

· 将x和y输入乘以少量(缩放因子),这将有效地实现与前一种方法相同的结果(并避免我们在其他无趣区域进行浪费的计算)。

· 由于输出是由输入乘以权重决定的,因此我们也可以通过将权重值从-100、+100减少到+3、-3等其他值来进行缩放而不是减少输入值(同时记住不要过度减少。还记得如果权重在-0.25到+0.25范围内就会出现一片灰色吗?)。

当我们采用第二种方法将x和y乘以0.01时,得到了:

877b4388bb0dd140021ada5fc94fe013.png

当采用第三种方法并将权重初始化为介于-3和+3之间时,这是我们得到的图像:

5d01dd0e576b5a3f425359c115162a0e.png

你的思维打开了吗?

c517769ce2e59375bfbdeef5a472ab87.png

更多的实验

将权重初始化更改为正态分布(平均值为0,标准差为1),并生成多个图像(下图是从随机初始化开始的)。

812fc3d420bed70407a512b6e5d95c15.png
6b0ab80a144b84a9a7dc9a7ca48b4553.png
b0c08040d36c335287b9ec4642377cb6.png

当移除所有隐藏层(仅输入到输出映射)时:

b9b9350d71376c84bd6e8e3ff9946d1d.png

0个隐藏层

当仅保留一个隐藏层(而不是默认的8个隐藏层)时:

0d2be9799c11e9286bb6bd9b32e173b3.png

1个隐藏层

当把隐藏层的数量加倍到16层时:

2e4ddb97f3056bb5eb5f5e62b32fab51.png

16个隐藏层,每层有16个神经元

正如你所能想象的,随着增加隐藏层的数量,图像变得越来越复杂。如果不是将层加倍,而是将层的数量保持不变(8),但是将每层神经元的数量加倍(从16到32),会发生什么?我们得到的是:

ff4632c05d53388afaa8bbf3a84e12b3.png

8个隐藏层,每层32个神经元

请注意,尽管在上述两种情况下,网络中的权重总数是相似的,但具有两个层的网络比每层双倍神经元的网络更像素化。像素表示,在这些区域中,函数变化剧烈,因此如果我们进一步缩放,会发现更多的结构。而对于层数不变但每层神经元数量加倍的网络,其功能相当平滑,因此“可缩放性”较小。

当然,所有这些都是深度使神经网络更具表现力的另一种说法。

计算函数的复杂度随深度呈指数增长。

这正是我们所看到的。一般的逼近定理认为,理论上,一个足够大的神经网络,即使有一个隐藏层,也可以表示任何函数。但在实践中,网络越深,输入到输出映射就越复杂。

c517769ce2e59375bfbdeef5a472ab87.png

毫无意义但很有趣的实验

如果我们把每层神经元的数量从8个增加到128个(数量级的增加)。

3cc6ca84cbbd82b7136d6f6f282b3abd.png

神经-波洛克!

如果我们从每一个隐藏层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())

我们得到的是:

d00917922785a3bfed6b3c9707721904.png

这个看起来比其他的更“自然”。

有很多实验可以做并且得到有趣的图像,你可以尝试更多的架构、激活和层次。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐