DeepDream的代码实现

之前在一篇文章中已经讲了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,这张图片原始是下面这样。

Paste_Image.png

然后经过Deep Dream的效果之后变成了这样,可以看到图片中多了一些狗头,还有一些眼睛,中间左边有一个明显的塔。

Paste_Image.png

除此之外,我们还可以控制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

我们输入的控制图片是下面这张小猫的图片

Paste_Image.png

最后通过Deep Dream我们能够得到下面这张图片,可以看到图片中有一些猫的头,猫的眼睛和猫的鼻子,是不是很神奇呢?

Paste_Image.png

本文参考Google官方实现和XavierLinNow的代码

本文代码已经上传到了github

欢迎查看我的知乎专栏,深度炼丹

欢迎访问我的博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容