之前在一篇文章中已经讲了Deep Dream的基本原理,为了能够更清晰地理解Deep Dream,我们有必要了解一下其是如何实现的。Google官方有一个实现的版本,是使用Caffe来实现的,我们这篇文章参考Google的官方实现和XavierLinNow的实现来讲一下Deep Dream的代码。
原理回顾
简单回顾一下Deep Dream的原理,对于一个预训练的网络,其对特定的分类任务有良好的效果,我们希望知道它到底学到了什么,如果要直接理解神经元提取的特征是很困难的,这时我们将一些与任务无关的图片输入,希望通过网络对其提取特征,然后反向传播的时候不再更新网络的参数,而是更新图片中的像素点,不断地迭代让网络越来越相信这张图片属于分类任务中的某一类。
这就是Deep Dream最基本的原理,看上去是非常简单的,但是实际中运用中要应用一些训练技巧来达到更好的效果。
代码实现
首先我们需要一个预训练的网络,同时我们还需要对预训练网络中的foward
进行调整,因为每次我们并不是对整个网络进行前向传播,我们希望得到网络的中间输出结果,而且我们希望我们能够很自由的控制这个变量。在PyTorch中这个实现是很简单的,我们使用预训练的50层的resnet,首先我们查看一下PyTorch对于resnet50的官方实现,我们只需要在这个官方实现的基础上修改一下就可以了,就像下面这样。
class CustomResNet(models.resnet.ResNet):
def forward(self, x, end_layer):
"""
end_layer range from 1 to 4
"""
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
layers = [self.layer1, self.layer2, self.layer3, self.layer4]
for i in range(end_layer):
x = layers[i](x)
return x
def resnet50(pretrained=False, **kwargs):
model = CustomResNet(Bottleneck, [3, 4, 6, 3], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))
return model
我们定义了我们自己的ResNet模块,这个模块是继承于PyTorch中的ResNet模块,然后我们不需要改变初始化,让它去继承ResNet的初始化,只需要重新定义forward
就可以,其中我们加入了结束层,也就是我们希望网络输出第几层的结果。
定义好了预训练的网络,接下来我们就可以开始训练模型,再次说明,网络中的参数不发生改变,更新的是图片中的像素点。
对于分类问题,我们使用交叉熵作为损失函数,那么在Deep Dream中我们使用什么作为损失函数呢?非常简单,之前我们讲过Deep Dream希望能够在迭代中不断地让网络更加确定这个图片属于某一类,所以损失函数就是网络结束层输出的特征向量的L2范数,我们希望最大化L2范数来使得图片经过网络之后提取的特征更像网络希望提取的特征。
看着可能有点难以理解,举个例子,一朵云提取特征之后有一点点像狗的图片提取的特征,因为网络的参数不会改变,所以图片如果不改变,那么再次输入网络之后得到的特征还是跟之前一样,只是有一点点像狗的图片提取出来的特征。Deep Dream希望通过反向传播更新图片的像素点,使得提取出来的特征更大,也就是使得提取出来的特征跟狗的图片提取出来的特征更像,那么用L2范数作为损失函数,网络不断更新图片的像素点来最大化L2范数,最终使得提取的特征越来越大,我们将多次更新之后的图片输出,也就得到了Deep Dream效果之后的图片。
为了得到更好的图片,我们需要应用一些小的技巧,否则得到的图片可能会存在很多噪声,或者需要很长的时间才能达到满意的效果。
对于输入的图片,我们需要对其做一些随机抖动,怎么去理解呢?看看示例代码就知道了。
shift_x, shift_y = np.random.randint(-max_jitter, max_jitter + 1, 2)
img = np.roll(np.roll(img, shift_x, -1), shift_y, -2)
max_jitter是一个整数,表示抖动的范围,随机从中取出两个整数表示x轴和y轴的抖动程度,然后np.roll表示对数组沿着一个维度进行平移,上面的代码首先对图片的第三个维度进行平移shift_x
,然后对第二个维度进行平移shift_y
。
L2范数的计算公式是$L = x_1^2 + x_2^2 + \cdots + x_n^2$,里面每个x表示特征向量中的元素,它们都是图片输入的函数,所以$\frac{\partial L}{\partial p} = 2 \sum_{i=1}^{n} x_i \frac{\partial x_i}{\partial p}$,前面的系数2是常数,所以可以去掉,这样我们就得到了反向传播时候的剃度的计算方法。而上一篇文章我们讲了PyTorch中backward的使用,所以我们可以通过下面的方式得到所有待更新的像素点的剃度。
act_value = model.forward(img_variable, end_layer)
act_value.backward(act_value.data)
act_value.backward(act_value.data)
就是上面L2范数反向传播的公式。
对于得到的剃度,我们需要对学习率进行一些限制,首先将所有参数的剃度的绝对值求个平均,然后用学习率除以这个均值,这样就得到了实际用的学习率。
ratio = np.abs(img_variable.grad.data.cpu().numpy()).mean()
learning_rate_use = learning_rate / ratio
img_variable.data.add_(img_variable.grad.data * learning_rate_use)
最后我们还需要使用一个小技巧,就是使用多尺度的图片进行计算,如果一直使用原始的图片,可能收敛速度会比较慢,所以我们先将图片缩小进行更新,然后在放大进行更新。
for i in range(octave_n - 1):
octaves.append(
nd.zoom(
octaves[-1], (1, 1, 1.0 / octave_scale, 1.0 / octave_scale),
order=1))
octave_n
表示多少张小图片,然后使用scipy.ndimage.zoom
进行图片的放缩,每次放缩的比例是1/octave_scale
,这样我们就可以图片大小递减的小图片,然后从小到大依次对图片的像素点进行更新,最后得到Deep Dream的图片。
我们对一张云的图片做Deep Dream,这张图片原始是下面这样。
然后经过Deep Dream的效果之后变成了这样,可以看到图片中多了一些狗头,还有一些眼睛,中间左边有一个明显的塔。
除此之外,我们还可以控制Deep Dream中的梦境,也就是说我们可以控制图片中出现的东西。要实现其实很简单,之前我们最大化特征向量的L2范数,这导致了图片中出现了各种各样的图片,所以要实现梦境控制,我们修改一下我们的目标函数就可以了。
首先需要输入一张图片作为梦境的控制图片,将控制图片通过网络前向传播得到其特征向量,然后将原始图片输入网络也得到特征向量,这两个特征向量的大小不同,所以先将它们重新排列成新的矩阵,然后做矩阵乘法,最后选择矩阵乘法里面最大的下标,将这些下标对应的原始图片的特征向量提取出来作为新的特征向量就可以了,这些特征向量被认为是最匹配控制图片特征向量的,这样就可以得到控制的梦境,具体可以看看下面的代码实现。
def objective_guide(dst, guide_features):
x = dst.data[0].cpu().numpy().copy()
y = guide_features.data[0].cpu().numpy()
ch, w, h = x.shape
x = x.reshape(ch,-1)
y = y.reshape(ch,-1)
A = x.T.dot(y) # compute the matrix of dot-products with guide features
result = y[:,A.argmax(1)] # select ones that match best
result = torch.Tensor(np.array([result.reshape(ch, w, h)], dtype=np.float)).cuda()
return result
我们输入的控制图片是下面这张小猫的图片
最后通过Deep Dream我们能够得到下面这张图片,可以看到图片中有一些猫的头,猫的眼睛和猫的鼻子,是不是很神奇呢?
本文参考Google官方实现和XavierLinNow的代码
本文代码已经上传到了github上
欢迎查看我的知乎专栏,深度炼丹
欢迎访问我的博客