Feature Loss
上周,我们把fastai发展到这样一个阶段,让GAN变得像API一样,比任何其他的库更简洁,更灵活。我也有点失望,训练要花很长时间,结果也一般。下一步是我们可以完全舍弃GAN。第一步,我们真正想做的事情,是提出更好的损失函数。我们需要一个能很好辨认出高品质图片的损失函数,能克服GAN具有的问题,或者不只是高品质图片,就是应该长成某种样子的图片。真正好用的技巧在这里,几年前的这篇论文,Perceptual Losses for Real-Time Style Transfer and Super-Resolution ,基于感知损失函数的实时风格转换和超分辨率重建,Justin Johnson和Alahi创造了他们称之为感知损失的东西。这是一篇很好的论文,但我很讨厌这个词,因为它们没有特别感知,我愿意称之为特征损失(feature loss)。所以在fastai库里,你可以看到这用feature loss来表示。
它里面一些东西是和GAN共通的,在经过generator后(他们把generator叫“image transform net”),你可以看到它有点像U-Net。他们并没有用U-Net,因为在写这个论文时,研究机器学习的人里没人知道U-Net。当然,现在我们会用U-Net。不管怎样,这是一个和U-Net比较像的东西。
在这种架构里,你有一个下采样的过程,然后有一个上采样的过程,这个下采样的过程经常被叫做encoder,就像你在代码里见过的那样,我们称之为encoder。上采样过程经常被叫做decoder。一般在生成模型里,一包括文本生成模型、神经网络翻译之类的东西,它们会被叫成encoder和decoder。
我们有了这个generator,我们还需要一个损失函数来判断“它创建的这个东西和我想要的东西长得像吗?”。他们采用的方法是,取预测值,通常我们用表示模型的预测值。我们取预测值,放进一个预训练好的ImageNet的神经网络。,
在论文发表时,他们用的预训练网络是VGG。人们仍然在用这个看起来有点老旧的模型,但人们还是倾向于继续使用他,因为它对这个过程很有效。把预测值传入VGG imageNet模型,注意是一个预训练好的imageNet网络。具体这是哪个无关紧要。
通常它的输出会告诉你这是否是生成的图片,无论内容是狗、或猫、或飞机、或消防车或别的什么东西。但在获知最后分类结果的过程中,要经过很多不同层,他们将所有层进行了颜色编码,在特征图上相同的网格大小的层,使用相同颜色的编码,所以颜色变了,网格大小就变了。这里是一个步长为2的卷积层。在VGG里他们仍然会用到一些最大池化层类似的想法。我们能做的是,不要取VGG模型的最终输出,基于生成图片的(),而是取一些中间值。让我们取某些中间层的激活作为输出(蓝色线),
可能是一个特征图,形态为256通道的28×28网格。这样的28×28尺寸的网格,大概语义上在说,在这个的28×x28的网格中,是一些看上去毛茸茸的东西,或是一些看起来有光泽的东西,或是一个圆形的东西,或是一个看上去是眼球的东西,等等。然后我们取目标值,即实际的值,传入同样的预训练VGG网络。取出相同层的激活。然后我们计算均方误差,它会指出在原图里,28×28大小特征图的网格(1,1)处,是毛茸茸的蓝色圆形。在生成的图片上,同样位置是毛茸茸的,蓝色,但不是圆形。这样的匹配还说得过去,但还需要苦苦探索眼球问题的解决。因为在这种情况下,特征图试图说明,这里有眼球,但这里却没有眼球。
所以为了改善性能,请生成更好的眼球,想法就是这样。我们将这称之为特征损失,或者按照Johnson和Alah的建议称之为感知损失。
要做到此改进,我们将用到lesson7-superres.ipynb,这次我们要做的任务和之前的一样,但这个notebook是在做GAN notebook之前做的,也就是在我有了这个想法之前,就是加入文本或任意的JPEG图片质量的想法。这里的JPEG图片质量总是60,没有加入文本,大小是96×96.在我发现crappify这个词多合适之前,我称之为resize,这是残次化处理后的图片和原图,和之前的任务类似,我要创建一个损失函数来完成这个任务,要做的第一件事就是定义一个base_loss函数,
基本上就是比较像素和特征上的差距。主要是选择有最小均方误差和L1损失,选择哪个并不重要。我更喜欢L1而非MSE,所以我选择了L1,你也可以选择mse。现在来创建一个VGG模型,只需要使用预先训练好的模型,VGG有一个属性是.features,包含了模型的卷积部分,所以这里是VGG的卷积部分。
因为我们只想要中间层的激活,然后到GPU上去验证,我们用eval模式。因为我们不打算训练它。我们将关闭requires grad,因为我们不想更新模型的权重,只是用它来计算损失,然后让我们遍历模型的所有children,找到所有的最大池化层。因为在VGG模型中,网格大小在这一层发生改变。从这张图可以看到,我们想提取特征,就在每次改变网格大小之前进行提取,
比如我们从i-1层开始提取,也就是在网格大小改变之前的层。在最大池化层之前的图层形成一个list数组,意料之中,激活函数都是ReLU。这就是我们提取特征的地方。
我们把它放进blocks,它只是一个关于ID的list。这是feature loss类,它将实现我们的想法,当我们调用feature loss类时,要先传入一些预训练模型,模型的名字是m_feat。这是包含特征的模型,我们将基于这些特征生成特征损失函数。我们继续提取所有的层,从网络的这些层里提取特征来计算损失。我们需要hook一些函数来抓取所有的中间输出,因为在pytorch里,提取中间层需要用hook机制,这个变量保存着我们抓取的输出。
现在,在前项特征损失函数中,我们要调用make_features,传入traget(实际的),
这里会调用VGG模型,遍历所有存储的激活值,抓取一份副本。
我们将调用make_feature函数,用于'target'得到'out_feat'。用于input
得到生成器in_feat
.现在,我们来计算基于像素的L1损失度,因为我们仍然需要基于像素的损失。
然后我们遍历所有层的特征,并得到它们的L1损失度。我们遍历每个layer,并在每一层的后面,
获得激活值,并计算L1损失。
最终结果存储在这个list里,
名叫feat_losses,最后将其求和作为函数返回值。我用list的原因是,这里有一个很好的回调,如果你在损失度函数里把它们放进这个叫.metrics的东西里,它会打印所有层的损失函数值,很方便。
就是这样,这就是我们的perceptual loss或者说feature loss类。
现在我们可以接着训练一个U-Net,照常传入数据和预训练好的架构。一个ResNet34网络,再传入用VGG模型得到的损失函数,这个损失函数使用我们的预训练VGG模型。这个callback_fns是我提过的LossMetrics,它可以打印出所有层的损失。这(blur、norm_type)是两个我们会在课程第二部分学习的东西,但现在我们要使用它。
这里是lr_find函数:
我创建了一个叫do_fit的函数,功能是训练一轮并保存模型,显示结果,和往常一样。
因为我们在U-Net里用了一个预训练网络,我们开始时用冻结的层作为下采样路径,训练一阵。你可以看到,我们得到的不只是简单的损失,还有基于像素的损失和每一层的特征的损失,以及我们会在第二部分学到的gram_loss。据我所知,目前没有人用过它来 实现高分辨率重建。可以看到,结果很好。
用了11分钟,比GAN快得多,并且,你看这个输出,效果已经很好了。
然后,我们解冻之前冻结的参数,再训练一会儿。
这次又好了一点儿。然后,我们把data中的size修改为size*2。同时给batch size(批次大小)减半,避免GPU内存溢出,再解冻,训练一会儿。
这次用了43分钟,结果更好了。然后解冻,再训练。
我们总共用了一个小时20分钟来训练,看看这个。它做到了。模型了解到图片中眼睛的重要性,因此它在这块付出了努力。模型知道皮毛的重要性,因此它在这块也付出了努力。模型做了一些像猫耳朵的jpeg矫饰,从这里的猫毛一团模糊,眼睛也只是模糊的浅蓝色,到最后训练完成这些部分真的都增加了质感。这只猫很明显是从一个猫爬架的顶部看过来,所以背景是模糊的。于是模型识别到,这样的东西很可能有着地毯般毛茸茸的质感,于是模型创造了一种地毯质感的材质,这真是太卓越了。
谈及太卓越了,我想说在不使用GAN的情况下,我从来没有见过这样优质的输出结果。在模型生成这些图像时,我一直都异常激动,我们训练得很快,只在一个GPU上用了一个半小时。如果你创建你自己的图像残次化函数 (crappification functions),然后训练这个模型,你可以构建出没有人做过的东西。因为据我所知没有人这样做。所以,我觉得有巨大的机会。
试想一下,现在我们能做的,不再是从low res(低分辨率)开始,实际上我还保存
了分辨率是256的图片,我们把它叫medium res,我们看下,如果我们用medium res会发生什么。
这里我们抓取了中等分辨率的数据,这是我们中等分辨率的图片:
你可以提升它吗?可以看到有很多提升的空间。比如睫毛这里,有很多高像素化的地方。这个应该是毛发的地方,但现在就是一片模糊的。看看下面这个地方。
让我运行程序,看,仔细看看,解决了。
它把一个中等分辨率的图片变成了完全清晰的东西。这些毛又出现了。但看看眼球,我们回去看下。这原来的眼球看起来就像是蓝色的东西,这里(生成的图片里)已经有正常的质感。我认为这很让人兴奋。这是我用一个半小时创建的一个模型,用的都是你们学过的知识范围内的标准模块,U-Net、预训练模型,feature loss函数,我们得到了可以把中等分辨率图片变成高分辨率图片的模型。想想能用它做什么真让人兴奋。
这里的灵感,来自于一个叫Jason Antic的项目。Jason是去年这个课程的一个学生,他辞去了每周4天的工作,每周六天学习深度学习,他做了一个结课项目,你们也应该这样。他的项目是把GANs与 特征损失结合起来。他的残次化方法是,把彩色图片变成黑白的。他调用了整个ImageNet,创建了一个黑白版的ImageNet,然后训练了一个模型来重新着色。他把这个提交到DeOldify,现在他可以把真正的19世纪的老照片变成彩色的。
他做的东西让人难以置信。看这个。这个模型认为,“噢,这可能是一个铜壶,我要把它变成铜的颜色”,“噢,这些图片在墙上,它们可能和墙的颜色不一样”,“这看起来像一个镜子,可能它可以反射出外面的东西”,“这些可能是蔬菜,让我把它们变成红色”。他做得令人惊奇。你们也可以做这个。你们可以用我们的feature loss和我们的GAN loss,把它们结合起来。非常感谢Jason,他帮我们更好地授课,我们也很荣幸通过课程能帮助他,因为他先前也没有意识到他可以用这些预训练的东西。希望在几周之内,能看到DeOldify变得更擅长还原颜色。也希望你们都能添加上其他逆残次化的方法。
如果可能的话,我喜欢每节课都展示一些新东西,因为每个学生都有机会构建先前无人触及的作品。这就是这样一种东西。有了更优质的分割结果,还有这些更简单快捷的逆残次化结果,你也可以创造出这些厉害的玩意。
提问: 能不能把U-Net和GAN通过类似的方法用到NLP上?比如,如果我想标记句子里的动词名词,创建一个很好的莎士比亚生成程序? [1:35:54]
是的,当然可以。虽然我们还不能确定是否可行。这是一个全新领域,但有很多机会。稍后我们会讲一些例子。
实际上,我曾经测试过这个。记得这个我上节课给你们看过的幻灯片里的图吗,这是一个看起来质量很差的图。我想,如果用这个模型处理它会怎样。它把这个图(左边)变成右边这样,我觉得这是一个很好的例子。
你可以看到它没有把奇怪的颜色去掉。因为我没有对掉色进行残次化。如果你想创建一个很好的图片修复程序,你需要做个高质量的残次化。