fast.ai 深度学习笔记(六)(3)https://developer.aliyun.com/article/1482699
人工智能伦理[35:31]
这是我们谈论最重要的部分,现在我们可以做所有这些事情,我们应该做什么,我们如何考虑?简而言之,我其实不知道。最近,你们中的许多人看到了 spaCy prodigy 公司的创始人在 Explosion AI 做了一个演讲,Matthew 和 Ines,之后我和他们一起吃饭,我们基本上整个晚上都在讨论,辩论,争论我们这样的公司正在构建可以以有害方式使用的工具,这意味着什么。他们是非常深思熟虑的人,我们,我不会说我们没有达成一致意见,我们只是无法得出结论。所以我只是列出一些问题,并指出一些研究,当我说研究时,实际上大部分文献综述和整理工作都是由 Rachel 完成的,所以谢谢 Rachel。
让我先说一下,我们构建的模型通常在某些方面相当糟糕,这些问题并不立即显现[36:52]。除非与你一起构建它们的人是各种各样的人,与你一起使用它们的人也是各种各样的人,否则你不会知道它们有多糟糕。例如,一对出色的研究人员,Timnit Gebru在微软工作,Joy Buolamwini刚从麻省理工学院获得博士学位,他们进行了一项非常有趣的研究,他们查看了一些现成的人脸识别器,其中包括来自 FACE++的一个,这是一家庞大的中国公司,IBM 的,以及微软的,他们寻找了一系列不同类型的人脸。
一般来说,微软的一个特别准确,除非人脸类型恰好是深色皮肤,突然间糟糕了 25 倍。IBM 几乎一半的时间都搞错了。对于这样一个大公司来说,发布一个对世界上大部分人来说都不起作用的产品,不仅仅是技术上的失败。这是对理解需要使用什么样的团队来创建这样的技术以及测试这样的技术,甚至对你的客户是谁的一种深刻失败。你的一些客户有深色皮肤。“我还要补充说,分类器在女性身上的表现都比在男性身上差”(Rachel)。令人震惊。有趣的是,Rachel 前几天在推特上发表了类似的言论,有人说“这是怎么回事?你在说什么?难道你不知道人们很长时间以来一直在制造汽车吗——你是在说你需要女性来制造汽车吗?”Rachel 指出——实际上是的。在汽车安全的大部分历史中,女性在汽车中的死亡风险远远高于男性,因为男性创造了看起来像男性、感觉像男性、尺寸像男性的碰撞测试假人,所以汽车安全实际上没有在女性身材上进行测试。产品管理糟糕,缺乏多样性和理解的失败在我们领域并不新鲜。
“我只是想说,这是在比较男性和女性的影响力”(Rachel)。我不知道为什么每当你在 Twitter 上说这样的话时,Rachel 都要这样说,因为每当你在 Twitter 上说这样的话时,大约有 10 个人会说“哦,你必须比较所有这些其他事情”,好像我们不知道一样。
像微软的人脸识别器或谷歌的语言翻译器这样的我们最好最著名的系统做的其他事情,你把“她是医生。他是护士。”翻译成土耳其语,非常正确——两个代词都变成了 O,因为土耳其语中没有性别代词。反过来,它会变成什么?“他是医生。她是护士。”所以我们在每天使用的工具中内置了这种偏见。而且,人们会说“哦,它只是展示了世界上的东西”,好吧,这个基本断言有很多问题,但正如你所知,机器学习算法喜欢概括。
因为他们喜欢概括,这是你们现在了解技术细节的一个很酷的事情,因为当你看到像 60%的照片中烹饪的人是女性,而他们用来构建这个模型的照片,然后你在另一组照片上运行模型时,84%被选择为烹饪的人是女性,而不是正确的 67%。这对于算法来说是一个非常可以理解的事情,因为它接受了有偏见的输入,并创造了一个更有偏见的输出,因为对于这个特定的损失函数来说,这就是它的结果。这是一种非常常见的模型放大。
这些事情很重要。它的重要性不仅仅体现在尴尬的翻译或黑人照片未被正确分类的方式上。也许也有一些胜利,比如到处可怕的监视,也许对黑人不起作用。“或者会更糟,因为这是可怕的监视,而且是彻头彻尾的种族主义和错误”(Rachel)。但让我们深入一点。尽管我们谈论人类的缺陷,但文明和社会有着长期的历史,创造了层层人类判断,希望避免最可怕的事情发生。有时候,热爱技术的公司会认为“让我们抛弃人类,用技术取代他们”,就像 Facebook 所做的那样。几年前,Facebook 真的摆脱了他们的人类编辑,当时这成为了新闻。他们被算法取代了。现在,当算法将所有内容放在你的新闻源上,而人类编辑却被排除在外时,接下来会发生什么?
接下来发生了很多事情。其中之一是缅甸发生了大规模的可怕种族灭绝。婴儿被从母亲怀里夺走,扔进火里。大规模的强奸、谋杀,整个民族被流放出境。
好吧,我不会说那是因为 Facebook 这样做的,但我要说的是,当这个可怕项目的领导接受采访时,他们经常谈论他们从 Facebook 学到的关于罗辛亚人恶劣动物行为的一切,这些行为需要被扫除。因为算法只是想要给你更多让你点击的东西。如果你被告知这些人不像你,你不认识这些坏人,这里有很多关于坏人的故事,然后你开始点击它们,然后他们会给你更多这些东西。接下来你会发现,你陷入了这个不寻常的循环。人们一直在研究这个问题,比如,我们被告知有几次人们点击我们的 fast.ai 视频,然后推荐给他们的下一个东西是来自 Alex Jones 的阴谋论视频,然后继续下去。因为人类点击那些让我们震惊、惊讶和恐惧的东西。在很多层面上,这个决定产生了不同寻常的后果,我们只是开始理解。再次强调,这并不是说这个特定的后果是因为这一个原因,但说它与此毫无关联显然是在忽视我们所拥有的所有证据和信息。
有意外的后果
关键是要考虑你正在构建什么,以及它可能如何被使用。现在有很多努力投入到人脸识别中,包括我们的课程。我们一直在花费大量时间思考如何识别东西以及它在哪里。有很多很好的理由希望在这方面做得更好,比如改善农业产量、改善医学诊断和治疗规划、改善你的乐高分类机器人系统等等。但它也被广泛用于监视、宣传和虚假信息。再次,问题是我该怎么办?我不完全知道。但至少重要的是要考虑这个问题,谈论这个问题。
失控的反馈循环
有时候你可以做一些非常好的事情。例如,meetup.com 做了一件我认为是非常好的事情的事情,他们早就意识到一个潜在的问题,即更多的男性倾向于参加他们的聚会。这导致他们的协同过滤系统,你现在熟悉正在构建的系统,向男性推荐更多技术内容。这导致更多的男性参加更多的技术内容,从而导致推荐系统向男性推荐更多技术内容。当我们将算法和人类结合在一起时,这种失控的反馈循环是非常常见的。那么 Meetup 做了什么?他们故意做出了向女性推荐更多技术内容的决定,不是因为对世界应该如何的高尚想法,而只是因为这是有道理的。失控的反馈循环是一个 bug——有女性想要参加技术聚会,但当你去参加一个技术聚会,里面全是男性,你就不去了,然后它就会向男性推荐更多,依此类推。因此,Meetup 在这里做出了一个非常强有力的产品管理决策,即不按照算法建议的做。不幸的是,这种情况很少见。大多数这种失控的反馈循环,例如在预测性警务中,算法告诉警察去哪里,很多时候是更多的黑人社区,这些社区最终会涌入更多的警察,导致更多的逮捕,这有助于告诉更多的警察去更多的黑人社区等等。
AI 中的偏见
算法偏见的问题现在非常普遍,随着算法在特定政策决策、司法决策以及日常决策中的广泛使用,这个问题变得越来越严重。其中一些问题实际上是产品管理决策中的人员在最初就应该看到的,这些问题在任何定义下都是没有意义和不合理的。例如,阿贝·龚指出的这些问题——这些问题既用于预审,即谁需要支付保释金,这些人甚至还没有被定罪,也用于判决以及谁获得假释。尽管存在所有的缺陷,这在去年被威斯康星州最高法院维持了。所以你是否必须因为支付不起保释金而留在监狱,你的刑期有多长,你在监狱里待多久取决于你父亲做了什么,你的父母是否离婚,你的朋友是谁,以及你住在哪里。现在事实证明这些算法实际上非常糟糕,最近的一些分析显示它们基本上比随机还要糟糕。但即使公司在这些统计上的相关性上很有信心,有人能想象出一个世界,在那里根据你父亲的行为来决定发生什么吗?
在基本层面上,很多事情显然是不合理的,很多事情只是以这种你可以从经验上看到的方式失败了,这种失控的反馈循环一定发生过,这种过度概括一定发生过。例如,任何领域工作的人都应该准备这些交叉表,使用这些算法。因此,对黑人和白人被告重新犯罪的可能性的预测,我们可以很简单地计算出来。那些被标记为高风险但没有再犯的人中,23.5%是白人,而非洲裔美国人大约是白人的两倍。而那些被标记为低风险但再犯的人中,白人只有非洲裔美国人的一半,而非洲裔美国人只有 28%。这就是这种情况,至少如果你正在使用我们谈论过的技术,并以任何方式将其投入生产,为其他人构建 API,为人们提供培训,或者其他什么——那么至少确保你所做的事情可以被追踪,以便人们知道发生了什么,至少他们是知情的。我认为假设人们是邪恶的,试图破坏社会是错误的。我认为我更愿意从一个假设开始,即如果人们做了愚蠢的事情,那是因为他们不知道更好的方法。所以至少确保他们有这些信息。我发现很少有机器学习从业者考虑他们的界面中应该呈现什么信息。然后我经常会和数据科学家交谈,他们会说“哦,我正在研究的东西对社会没有影响。”真的吗?有很多人认为他们正在做的事情完全毫无意义吗?来吧。人们付钱让你做这件事是有原因的。它会以某种方式影响人们。所以考虑一下这是什么。
在招聘中的责任
我知道的另一件事是很多参与其中的人都在招聘人才,如果你在招聘人才,我想你现在都非常熟悉 fast.ai 的理念,这基本上是这样一个前提,我认为人们总体上并不邪恶,我认为他们需要被告知并拥有工具。因此,我们正在尽可能地为尽可能多的人提供他们需要的工具,特别是我们正在尝试将这些工具交到更广泛人群的手中。因此,如果你参与招聘决策,也许你可以记住这种理念。如果你不仅仅是招聘更广泛的人才,而且还提拔更广泛的人才,并为更广泛的人提供适当的职业管理,除了其他任何事情,你的公司会做得更好。事实证明,更多样化的团队更有创造力,往往比不那么多样化的团队更快更好地解决问题,而且你也可能避免这些糟糕的失误,这在某种程度上对世界是有害的,而在另一层面,如果你被发现,它们可能毁掉你的公司。
IBM 和“死亡计算器”
他们也可以摧毁你,或者至少让你在历史上看起来很糟糕。举几个例子,一个是回到第二次世界大战。IBM 提供了跟踪大屠杀所需的所有基础设施。这些是他们使用的表格,它们有不同的代码 - 犹太人是 8,吉普赛人是 12,毒气室中的死亡是 6,所有这些都记录在这些打孔卡上。现在你可以去博物馆看这些打孔卡,这实际上已经被一位瑞士法官审查过,他说 IBM 的技术支持促进了纳粹分子的任务并促使他们犯下反人类罪行。回顾这些时期的历史,看看当时 IBM 的人们在想什么是很有趣的。当时人们明显在想的是展示技术优势的机会,测试他们的新系统的机会,当然还有他们赚取的巨额利润。当你做了一些事情,即使在某个时候会变成问题,即使你被告知要这样做,这对你个人来说也可能成为问题。例如,大家都记得大众柴油排放丑闻。谁是唯一一个入狱的人?那就是只是在做他的工作的工程师。如果所有这些关于实际上不要搞砸世界的东西还不足以说服你,那么它也可能毁掉你的生活。如果你做了一些事情,结果导致问题,即使有人告诉你要这样做,你绝对可能被追究刑事责任。亚历山大·科根就是那个交出剑桥分析数据的人。他是一位剑桥学者。现在是一位全球著名的剑桥学者,因为他为摧毁民主的基础做出了自己的贡献。这不是我们想要留在历史上的方式。
问题: 在你的一条推特中,你说 dropout 被专利化了[56:50]。我认为这是关于 Google 的 WaveNet 专利。这是什么意思?你能分享更多关于这个主题的见解吗?这意味着我们将来要付费使用 dropout 吗?专利持有人之一是 Geoffrey Hinton。那又怎样?这不是很棒吗?发明就是关于专利的,啦啦啦。我的答案是否定的。专利已经变得疯狂。我们每周讨论的可以被专利化的东西数量会有几十个。很容易想出一个小调整,然后如果你把它变成专利来阻止每个人在接下来的 14 年内使用那个小调整,最终我们就会面临现在的情况,所有东西都以 50 种不同的方式被专利化。然后你会遇到这些专利流氓,他们通过购买大量垃圾专利然后起诉任何无意中做了那件事的人,比如给按钮加上圆角。那么对于我们来说,深度学习中有很多东西被专利化意味着什么?我不知道。
做这个工作的主要人员之一是 Google,而且来自 Google 的人回应这个专利时倾向于认为 Google 这样做是因为他们想要防御性地拥有它,所以如果有人起诉他们,他们可以说不要起诉我们,我们会反诉你,因为我们有所有这些专利。问题是据我所知,他们还没有签署所谓的防御性专利承诺,所以基本上你可以签署一个法律约束文件,说我们的专利组合只会用于防御而不是进攻。即使你相信 Google 的所有管理层永远不会变成专利流氓,你必须记住管理层会变化。给你一个具体的例子,我知道,Google 的最近的 CFO 对 PNL 有更积极的态度,我不知道,也许她会决定他们应该开始变现他们的专利,或者也许做出那个专利的团队可能会被分拆然后卖给另一家公司,最终可能会落入私募股权手中并决定变现专利或其他。所以我认为这是一个问题。最近在法律上有一个从软件专利转向实际上没有任何法律地位的大变化,所以这些可能最终都会被驳回,但现实是,任何不是大公司的人都不太可能有财务能力来抵抗这些庞大的专利流氓。
如果你写代码,就无法避免使用专利的东西。我不会感到惊讶,如果你写的大部分代码都有专利。实际上,有趣的是,最好的做法不是研究专利,因为如果你故意侵犯,惩罚会更严重。所以最好的做法是把手放在耳朵上,唱首歌,然后继续工作。所以关于 dropout 被专利化的事情,忘记我说过的。你不知道那个。你跳过那部分。
风格迁移[1:01:28]
这非常有趣——艺术风格。我们在这里有点复古,因为这实际上是最初的艺术风格论文,后来有很多更新和很多不同的方法,我实际上认为在很多方面最初的方法是最好的。我们也会看一些更新的方法,但我实际上认为最初的方法是一个很棒的方式,即使在之后的一切发展之后。让我们来看看代码。
%matplotlib inline %reload_ext autoreload %autoreload 2from fastai.conv_learner import * from pathlib import Path from scipy import ndimage torch.cuda.set_device(3) torch.backends.cudnn.benchmark=TruePATH = Path('data/imagenet') PATH_TRN = PATH/'train'm_vgg = to_gpu(vgg16(True)).eval() set_trainable(m_vgg, False)
这里的想法是我们想要拍摄一只鸟的照片,并且我们想要创作一幅看起来像梵高画了这只鸟的画。顺便说一句,我正在做的很多事情都使用了 ImageNet。你不必为我所做的任何事情下载整个 ImageNet。在files.fast.ai/data中有一个 ImageNet 样本,它有几个 G 的数据,对我们正在做的一切来说应该足够了。如果你想要得到真正出色的结果,你可以获取 ImageNet。你可以从Kaggle下载。定位竞赛实际上包含了所有的分类数据。如果你有空间,最好拥有一份 ImageNet 的副本,因为它随时都会派上用场。
img_fn = PATH_TRN/'n01558993'/'n01558993_9684.JPEG' img = open_image(img_fn) plt.imshow(img);
所以我刚从我的 ImageNet 文件夹中拿出了这只鸟,这就是我的鸟:
sz=288trn_tfms,val_tfms = tfms_from_model(vgg16, sz) img_tfm = val_tfms(img) img_tfm.shape*(3, 288, 288)*opt_img = np.random.uniform(0, 1, size=img.shape).astype(np.float32) plt.imshow(opt_img);
我要做的是从这张图片开始:
我将尝试让它越来越像梵高画的鸟的图片。我做的方法实际上非常简单。你们都很熟悉它。我们将创建一个损失函数,我们将称之为f。损失函数将以一张图片作为输入,并输出一个值。如果图像看起来更像梵高画的鸟照片,那么这个值将更低。编写了这个损失函数之后,我们将使用 PyTorch 的梯度和优化器。梯度乘以学习率,我们不会更新任何权重,而是会更新输入图像的像素,使其更像梵高画的鸟的图片。然后我们再次通过损失函数来获取更多的梯度,一遍又一遍地进行。就是这样。所以这与我们解决每个问题的方式是相同的。你们知道我是一个只会一招的人,对吧?这是我的唯一招数。创建一个损失函数,用它来获取一些梯度,乘以学习率来更新某些东西,以前我们总是更新模型中的权重,但今天我们不会这样做。我们将更新输入图像中的像素。但实际上并没有什么不同。我们只是针对输入而不是针对权重来获取梯度。就是这样。我们快要完成了。
让我们做更多的事情。让我们在这里提到,我们的损失函数将有两个额外的输入。一个是鸟的图片。第二个是梵高的一幅艺术作品。通过将它们作为输入,这意味着我们以后可以重新运行这个函数,使其看起来像梵高画的鸟,或者像莫奈画的鸟,或者像梵高画的大型喷气式飞机等。这些将是三个输入。最初,正如我们讨论过的,我们的输入是一些随机噪音。我们从一些随机噪音开始,使用损失函数,获取梯度,使其更像梵高画的鸟,依此类推。
所以我猜我们可以简要讨论的唯一未解决的问题是我们如何计算我们的图像看起来有多像梵高画的这只鸟。让我们将其分为两部分:
内容损失:返回一个值,如果看起来更像这只鸟(不只是任何鸟,而是我们要处理的特定鸟)。
风格损失:如果图像更像 V.G.的风格,则返回一个较低的数字。
有一种非常简单的计算内容损失的方法——我们可以查看输出的像素,将它们与鸟的像素进行比较,计算均方误差,然后相加。所以如果我们这样做,我运行了一段时间。最终我们的图像会变成一只鸟的图像。你应该尝试一下。你应该尝试这个作为练习。尝试使用 PyTorch 中的优化器,从一个随机图像开始,通过使用均方误差像素损失将其转变为另一幅图像。这并不是非常令人兴奋,但这将是第一步。
问题是,即使我们已经有了我们的风格损失函数运行得很好,然后假设我们要做的是将这两者相加,然后其中一个,我们将乘以一些λ来调整风格与内容的比例。假设我们有一个风格损失并选择了一些合理的λ,如果我们使用像素级的内容损失,那么任何使其看起来更像梵高而不是完全像照片、背景、对比度、光照等的东西都会增加内容损失——这不是我们想要的。我们希望它看起来像鸟,但不是以相同的方式。它仍然会有相同位置的两只眼睛,相同的形状等等,但不是相同的表示。所以我们要做的是,这可能会让您震惊,我们要使用一个神经网络!我们将使用 VGG 神经网络,因为那是我去年使用的,我没有时间看其他东西是否有效,所以您可以在这一周自己尝试。
VGG 网络是一个接受输入并将其通过多个层的网络,我将把这些层视为卷积层,显然还有 ReLU,如果是带有批量归一化的 VGG,那么它也有批量归一化。还有一些最大池化等等,但没关系。我们可以做的是,我们可以取其中一个卷积激活,而不是比较这只鸟的像素,我们可以比较这个(由 V.G.绘制的)鸟的 VGG 层 5 激活与我们原始鸟的 VGG 层 5 激活(或第 6 层,第 7 层等)。那么为什么这样更有趣呢?首先,它不会是同一只鸟。它不会完全相同,因为我们不是在检查像素。我们在检查一些后续的激活。那么这些后续的激活包含什么?假设它经过一些最大池化后,它包含一个较小的网格——所以它对事物的位置不那么具体。而不是包含像素颜色值,它们更像是语义的东西,比如这是一种眼球,这是一种毛茸茸的,这是一种明亮的,或者这是一种反射的,或者平放的,或者其他什么。因此,我们希望通过这些层有一定程度的语义特征,如果我们得到一个与这些激活匹配的图片,那么任何匹配这些激活的图片看起来像鸟,但不是相同的鸟的表示。这就是我们要做的。这就是我们的内容损失将是什么。人们通常称之为感知损失,因为在深度学习中,您总是为您做的每件明显的事情创造一个新名称。如果您将两个激活进行比较,您正在进行感知损失。就是这样。我们的内容损失将是感知损失。然后我们将稍后进行风格损失。
让我们从尝试创建一只最初是随机噪音的鸟开始,我们将使用感知损失来创建类似鸟的东西,但不是特定的鸟。我们将从 288x288 开始。因为我们只做一只鸟,所以不会出现 GPU 内存问题。我实际上很失望地意识到我选择了一个相当小的输入图像。尝试使用更大的图像创建一个真正宏伟的作品会很有趣。另一件事要记住的是,如果您要将其投入生产,可以一次处理整个批次。有时人们会抱怨这种方法(Gatys 是主要作者)——Gatys 的风格迁移方法很慢,但我不同意它很慢。只需要几秒钟,您就可以在几秒钟内处理整个批次。
sz=288
所以我们将按照通常的做法将其通过 VGG16 模型的一些转换。记住,转换类有 dunder call 方法(__call__
),所以我们可以将其视为一个函数。如果你将一个图像传递给它,那么我们将得到转换后的图像。尽量不要将 fast.ai 和 PyTorch 基础设施视为黑盒,因为它们都设计成非常易于以解耦的方式使用。所以这个转换只是“可调用”的想法(即用括号括起来的东西)来自于 PyTorch,我们完全抄袭了这个想法。所以在 torch.vision 或 fast.ai 中,你的转换只是可调用的。整个转换流水线只是一个可调用的。
trn_tfms,val_tfms = tfms_from_model(vgg16, sz) img_tfm = val_tfms(img) img_tfm.shape ''' (3, 288, 288) '''
现在我们有了一个 3x288x288 的东西,因为 PyTorch 喜欢通道在前面。正如你所看到的,它已经被转化为一个方形,被归一化为(0,1),所有这些正常的东西。
现在我们正在创建一个随机图像。
opt_img = np.random.uniform(0, 1, size=img.shape).astype(np.float32) plt.imshow(opt_img);
这是我发现的一件事。试图将这个转化为任何东西的图片实际上非常困难。我发现实际上很难让优化器获得合理的梯度,使其有所作为。就在我以为我要在这门课上耗尽时间并真正让自己尴尬的时候,我意识到关键问题是图片不是这样的。它们更加平滑,所以我稍微模糊了一下,将其转化为以下内容:
opt_img = scipy.ndimage.filters.median_filter(opt_img, [8,8,1]) plt.imshow(opt_img);
我使用了一个中值滤波器——基本上就像一个中值池化。一旦我将其改为这样,它立即开始训练得非常好。你必须做一些微小的调整才能让这些东西工作起来,这有点疯狂,但这里有一个小调整。
所以我们从一个随机图像开始,这个图像至少有一定的平滑度。我发现我的鸟类图像的像素均值大约是这个值的一半,所以我将其除以 2,只是试图让匹配变得更容易一些(我不知道这是否重要)。将其转化为一个变量,因为这个图像,记住,我们将使用优化算法修改这些像素,所以任何涉及损失函数的东西都需要是一个变量。并且,它需要梯度,因为我们实际上是在更新图像。
opt_img = val_tfms(opt_img)/2 opt_img_v = V(opt_img[None], requires_grad=True) opt_img_v.shape*torch.Size([1, 3, 288, 288])*
所以现在我们有了一个大小为 1 的小批量,3 个通道,288x288 的随机噪声。
m_vgg = nn.Sequential(*children(m_vgg)[:37])
我们将使用,没有特定原因,VGG 的第 37 层。如果你打印出 VGG 网络(你只需输入m_vgg
并打印出来),你会看到这是中后期的层。所以我们可以只获取前 37 层并将其转化为一个顺序模型。现在我们有了一个 VGG 的子集,它将输出一些中间层的激活,这就是模型将要做的事情。所以我们可以拿到我们实际的鸟类图像,我们想创建一个大小为一的小批量。记住,如果你在 Numpy 中使用None
进行切片,也就是np.newaxis
,它会在那个点引入一个新的单位轴。这里,我想创建一个大小为 1 的轴,表示这是一个大小为一的小批量。所以就像我在这里做的一样(opt_img_v = V(opt_img[**None**], requires_grad=**True**)
)使用None
进行切片,在前面得到一个单位轴。然后我们将其转化为一个变量,这个变量不需要更新,所以我们使用VV
来表示你不需要为这个变量计算梯度。这将给我们我们的目标激活。
- 我们已经拿到了我们的鸟类图像。
- 将其转化为一个变量
- 将其通过我们的模型传递,以获取第 37 层的激活,这是我们的目标。我们希望我们的内容损失是这组激活。
- 我们将创建一个优化器(我们稍后会回到这个细节)
- 我们将进行多次迭代
- 梯度清零
- 调用一些损失函数
- 损失反向传播()
这就是高层次的版本。我一会儿会回到细节,但关键是我们传入那个随机生成的图像的损失函数——优化图像的变量。因此,我们将该图像传递给我们的损失函数,它将使用损失函数进行更新,而损失函数是通过将我们当前的优化图像通过我们的 VGG 获取中间激活,并将其与目标激活进行比较来计算均方误差损失。我们运行一堆次数,然后将其打印出来。我们有我们的鸟,但没有它的表示形式。
targ_t = m_vgg(VV(img_tfm[None])) targ_v = V(targ_t) targ_t.shape ''' torch.Size([1, 512, 18, 18]) ''' max_iter = 1000 show_iter = 100 optimizer = optim.LBFGS([opt_img_v], lr=0.5)
Broyden–Fletcher–Goldfarb–Shanno(BFGS)
这里有一些新的细节。其中一个是一个奇怪的优化器(optim.LBFGS
)。任何完成过某些数学和计算机科学课程的人进入深度学习领域都会发现我们使用像 Adam 和 SGD 这样的东西,并且总是假设该领域的人对计算机科学一无所知,立即说“你们有人尝试过使用 BFGS 吗?”实际上,我们并没有使用来训练神经网络的完全不同类型的优化算法的长期历史。当然,事实上,那些花了几十年研究神经网络的人确实对计算机科学有所了解,结果表明这些技术整体上并不工作得很好。但实际上,这对我们来说会很有效,并且这是一个很好的机会,让那些在学校没有学习过这种类型的优化算法的人了解一个有趣的算法。BFGS(四个不同人的首字母缩写),L 代表有限内存。它是一个优化器,也就是说,有一些损失函数,它将使用一些梯度(并非所有优化器都使用梯度,但我们使用的所有优化器都会)来找到一个方向,并尝试通过调整一些参数使损失函数降低。它只是一个优化器。但它是一种有趣的优化器,因为它在每一步上做的工作比我们习惯的要多一点。具体来说,它的工作方式与我们习惯的方式相同,即我们只是选择一个起点,而在这种情况下,我们选择了一个随机图像,正如你所看到的。像往常一样,我们计算梯度。但我们不仅仅是采取一步,而是实际上在找到梯度的同时,我们还尝试找到二阶导数。二阶导数表示梯度变化的速度。
梯度:函数变化的速度
二阶导数:梯度变化的速度
换句话说,它有多曲折?基本思想是,如果你知道它不太曲折,那么你可能可以跳得更远。但如果它非常曲折,那么你可能不想跳得太远。因此,在更高维度中,梯度被称为雅可比矩阵,而二阶导数被称为海森矩阵。你会经常看到这些词,但它们的意思就是这样。再次强调,数学家们也必须为每件事发明新词。他们就像深度学习研究人员一样——也许有点傲慢。使用 BFGS,我们将尝试计算二阶导数,然后我们将使用它来确定前进的方向和距离——因此,这不是对未知领域的一次疯狂跳跃。
现在的问题是,实际计算 Hessian(二阶导数)几乎肯定不是一个好主意。因为在你要前进的每个可能方向上,对于你测量梯度的每个方向,你还必须在每个方向上计算 Hessian。这变得非常庞大。所以我们不是真的计算它,我们走几步,基本上看一下梯度在每一步变化了多少,然后用那个小函数来近似 Hessian。再次强调,这似乎是一个非常明显的事情,但直到后来有人想到了,这花了相当长的时间。跟踪每一步都需要大量内存,所以别跟踪每一步,只保留最后的十步或二十步。第二部分,就是 L 到 LBFGS。有限内存的 BFGS 意味着保留最后的 10 或 20 个梯度,用它来近似曲率的量,然后用曲率和梯度来估计前进的方向和距离。在深度学习中通常不是一个好主意,有很多原因。这比 Adam 或 SGD 更新更费力,也使用更多内存,当你有一个 GPU 来存储和数亿个权重时,内存就成了一个更大的问题。但更重要的是,小批量是非常颠簸的,所以弄清楚曲率以决定到底要前进多远,有点像我们说的磨亮了粪便(是的,澳大利亚和英国的表达方式,你懂的)。有趣的是,实际上使用二阶导数信息,结果就像是一个吸引鞍点的磁铁。因此,有一些有趣的理论结果基本上说,如果使用二阶导数信息,它实际上会把你引向函数的恶劣平坦区域。所以通常不是一个好主意。
def actn_loss(x): return F.mse_loss(m_vgg(x), targ_v)*1000 def step(loss_fn): global n_iter optimizer.zero_grad() loss = loss_fn(opt_img_v) loss.backward() n_iter+=1 if n_iter%show_iter==0: print(f'Iteration: n_iter, loss: **{loss.data[0]}**') return loss
但在这种情况下,我们不是在优化权重,而是在优化像素,所以所有规则都改变了,实际上 BFGS 是有意义的。因为每次它做更多的工作,它是一种不同类型的优化器,PyTorch 中的 API 也有点不同。正如你在这里看到的,当你说optimizer.step
时,你实际上传入了损失函数。所以我们的损失函数是调用step
,传入一个特定的损失函数,即我们的激活损失(actn_loss
)。在循环内部,你不会说 step,step,step。而是看起来像这样。所以有点不同,你可以尝试重写这个来使用 SGD,它仍然会工作。只是会花更长的时间,我还没有尝试过用 SGD,我很想知道它需要多长时间。
n_iter=0 while n_iter <= max_iter: optimizer.step(partial(step,actn_loss)) ''' Iteration: n_iter, loss: 0.8466196656227112 Iteration: n_iter, loss: 0.34066855907440186 Iteration: n_iter, loss: 0.21001280844211578 Iteration: n_iter, loss: 0.15562333166599274 Iteration: n_iter, loss: 0.12673595547676086 Iteration: n_iter, loss: 0.10863320529460907 Iteration: n_iter, loss: 0.0966048613190651 Iteration: n_iter, loss: 0.08812198787927628 Iteration: n_iter, loss: 0.08170554041862488 Iteration: n_iter, loss: 0.07657770067453384 '''
所以你可以看到损失函数在下降。我们的 VGG 模型第 37 层的激活与目标激活之间的均方误差,记住目标激活是应用于我们的鸟的 VGG。明白了吗?所以现在我们有了一个内容损失。现在,关于这个内容损失,我要说的一件事是我们不知道哪一层会起到最好的作用。所以如果我们能多做一些实验就好了。现在的情况很烦人:
也许我们甚至想使用多个层。所以,与其截断我们想要的层之后的所有层,不如我们能够以某种方式抓取几个层的激活值。现在,我们已经知道一种方法可以在我们做 SSD 时做到这一点,我们实际上编写了一个具有多个输出的网络。记得吗?不同的卷积层,我们吐出了一个不同的oconv
东西?但我真的不想去添加到 torch.vision ResNet 模型中,特别是如果以后我想尝试 torch.vision VGG 模型,然后我想尝试 NASNet-A 模型,我不想去修改它们的输出。此外,我希望能够轻松地按需打开和关闭某些激活。所以我们之前简要提到过这个想法,PyTorch 有这些名为 hooks 的奇妙东西。您可以有前向钩子,让您将任何您喜欢的东西插入到计算的前向传递中,或者有后向钩子,让您将任何您喜欢的东西插入到后向传递中。所以我们将创建世界上最简单的前向钩子。
x = val_tfms.denorm(np.rollaxis(to_np(opt_img_v.data),1,4))[0] plt.figure(figsize=(7,7)) plt.imshow(x);
前向钩子[1:29:42]
这是几乎没有人知道的事情之一,因此几乎在互联网上找到的任何实现风格转移的代码都会有各种可怕的黑客,而不是使用前向钩子。但前向钩子真的很容易。
要创建一个前向钩子,只需创建一个类。该类必须有一个名为hook_fn
的东西。您的钩子函数将接收您挂钩的module
,前向传递的input
和output
,然后您可以做任何您喜欢的事情。所以我要做的就是将这个模块的输出存储在某个属性中。就是这样。所以hook_fn
实际上可以被称为您喜欢的任何东西,但“hook function”似乎是标准,因为您可以看到,在构造函数中发生的是我在某个属性中存储了m.register_forward_hook
的结果(m
将是我要挂钩的层),并传入您希望在调用模块的前向方法时调用的函数。当调用其前向方法时,它将调用self.hook_fn
,该函数将在名为features
的属性中存储输出。
class SaveFeatures(): features=None def __init__(self, m): self.hook = m.register_forward_hook(self.hook_fn) def hook_fn(self, module, input, output): self.features = output def close(self): self.hook.remove()
现在我们可以像以前一样创建一个 VGG。让我们将其设置为不可训练,这样我们就不会浪费时间和内存来计算梯度。让我们遍历并找到所有的最大池层。让我们遍历这个模块的所有子层,如果是一个最大池层,让我们输出索引减 1——这样就会给我最大池之前的层。通常,最大池或步长 2 卷积之前的层是一个非常完整的表示,因为下一层正在改变网格。所以这对我来说是一个很好的地方来获取内容损失。我们在该网格大小上拥有的最语义化、最有趣的内容。这就是为什么我要选择这些索引。
m_vgg = to_gpu(vgg16(True)).eval() set_trainable(m_vgg, False)
这些是 VGG 中每个最大池之前的最后一层的索引[1:32:30]。
block_ends = [ i-1 for i,o in enumerate(children(m_vgg)) if isinstance(o,nn.MaxPool2d) ] block_ends ''' [5, 12, 22, 32, 42] '''
我要获取32
——没有特定的原因,只是尝试其他东西。所以我要说block_ends[3]
(即 32)。children(m_vgg)[block_ends[3]]
会给我 VGG 的第 32 层作为一个模块。
sf = SaveFeatures(children(m_vgg)[block_ends[3]])
然后,如果我调用SaveFeatures
构造函数,它会执行:
self.hook = {VGG 的第 32 层}.register_forward_hook(self.hook_fn)
现在,每当我对这个 VGG 模型进行前向传递时,它都会将第 32 层的输出存储在sf.features
中。
def get_opt(): opt_img = np.random.uniform( 0, 1, size=img.shape ).astype(np.float32) opt_img = scipy.ndimage.filters.median_filter(opt_img, [8,8,1]) opt_img_v = V(val_tfms(opt_img/2)[None], requires_grad=True) return opt_img_v, optim.LBFGS([opt_img_v]) opt_img_v, optimizer = get_opt()
在这里[1:33:33],我调用了我的 VGG 网络,但我没有将其存储在任何地方。我没有说activations = m_vgg(VV(img_tfm[**None**]))
。我调用它,丢弃答案,然后抓取我们在SaveFeatures
对象中存储的特征。
m_vgg()
— 这是在 PyTorch 中进行前向路径的方法。你不会说 m_vgg.forward()
,你只是将其用作可调用。在 nn.module
上使用可调用会自动调用 forward
。这就是 PyTorch 模块的工作方式。
所以我们称之为可调用的,最终调用我们的前向钩子,前向钩子将激活存储在 sf.features
中,所以现在我们有了我们的目标变量 — 就像以前一样,但以一种更加灵活的方式。
get_opt
包含了我们之前的相同的 4 行代码[1:34:34]。它只是给我一个要优化的随机图像和一个优化器来优化该图像。
m_vgg(VV(img_tfm[None])) targ_v = V(sf.features.clone()) targ_v.shape ''' torch.Size([1, 512, 36, 36]) ''' def actn_loss2(x): m_vgg(x) out = V(sf.features) return F.mse_loss(out, targ_v)*1000
现在我可以继续做完全相同的事情。但现在我将使用不同的损失函数 actn_loss2
(激活损失 #2),它不会说 out=m_vgg
,再次,它调用 m_vgg
进行前向传递,丢弃结果,并获取 sf.features
。所以现在这是我的第 32 层激活,然后我可以在其上执行均方误差损失。你可能已经注意到,最后一个损失函数和这个都乘以了一千。为什么它们乘以一千?这就像所有试图使这个课程不正确的事情。我以前没有使用一千,它就无法训练。今天午餐时间,什么都不起作用。经过几天的尝试让这个东西工作,最终偶然注意到“天哪,损失函数的数字真的很低(如 10E-7)”,我想如果它们不那么低会怎样。所以我将它们乘以一千,然后它开始工作了。那为什么它不起作用呢?因为我们正在使用单精度浮点数,而单精度浮点数并不那么精确。特别是当你得到的梯度有点小,然后你乘以学习率可能也很小,最终得到一个很小的数字。如果它太小,它们可能会被四舍五入为零,这就是发生的事情,我的模型还没有准备好。我相信有比乘以一千更好的方法,但无论如何。它运行得很好。无论你将损失函数乘以多少,因为你关心的只是它的方向和相对大小。有趣的是,这与我们在训练 ImageNet 时所做的事情类似。我们使用了半精度浮点数,因为 Volta 张量核要求如此。如果你想要训练半精度浮点数,实际上你必须将损失函数乘以一个缩放因子。我们使用了 1024 或 512。我认为 fast.ai 现在是第一个具有所有必要技巧以在半精度浮点数中进行训练的库,因此如果你有幸拥有 Volta 或者你可以支付 AWS P3,如果你有一个学习对象,你只需说 learn.half
,它现在就会神奇地正确地训练半精度浮点数。它也内置在模型数据对象中,一切都是自动的。我相信没有其他库能做到这一点。
n_iter=0 while n_iter <= max_iter: optimizer.step(partial(step,actn_loss2)) ''' Iteration: n_iter, loss: 0.2112911492586136 Iteration: n_iter, loss: 0.0902421623468399 Iteration: n_iter, loss: 0.05904778465628624 Iteration: n_iter, loss: 0.04517251253128052 Iteration: n_iter, loss: 0.03721420466899872 Iteration: n_iter, loss: 0.03215853497385979 Iteration: n_iter, loss: 0.028526008129119873 Iteration: n_iter, loss: 0.025799645110964775 Iteration: n_iter, loss: 0.02361033484339714 Iteration: n_iter, loss: 0.021835438907146454 '''
这只是在稍早的层上做同样的事情[1:37:35]。这只是让鸟看起来更像鸟。希望你能理解,较早的层越接近像素。有更多的网格单元,每个单元更小,更小的感受野,更简单的语义特征。所以我们越早得到,它看起来就越像一只鸟。
x = val_tfms.denorm(np.rollaxis(to_np(opt_img_v.data),1,4))[0] plt.figure(figsize=(7,7)) plt.imshow(x);
sf.close()
事实上,这篇论文有一张很好的图片展示了各种不同的层,并放大到这座房子[1:38:17]。他们试图让这座房子看起来像《星夜》的图片。你可以看到后来,它变得非常混乱,而之前看起来像这座房子。所以这只是在做我们刚刚做的事情。我在我们的学习小组中注意到的一件事是,每当我告诉某人回答一个问题,每当我说去读这篇论文中有一些东西告诉你问题的答案时,总会有一种震惊的表情“读这篇论文?我?”但是说真的,论文已经做了这些实验并绘制了这些图片。论文中有很多东西。这并不意味着你必须读完论文的每一部分。但至少看看图片。所以看看 Gatys 的论文,里面有很好的图片。所以他们已经为我们做了实验,但看起来他们没有深入研究 — 他们只是得到了一些早期的结果。
风格匹配[1:39:29]
我们接下来需要做的是创建风格损失。我们已经有了损失,即它有多像鸟。现在我们需要知道它有多像这幅绘画的风格。我们将做几乎相同的事情。我们将获取某一层的激活。现在问题是,某一层的激活,假设它是一个 5x5 的层(当然没有 5x5 的层,它是 224x224,但我们假装)。这里是一些激活,我们可以获取这些激活,无论是针对我们正在优化的图像还是我们的梵高绘画。让我们看看我们的梵高绘画。这就是它 —《星夜》
style_fn = PATH/'style'/'starry_night.jpg' style_img = open_image(style_fn) style_img.shape, img.shape ''' ((1198, 1513, 3), (291, 483, 3)) ''' plt.imshow(style_img);
我从维基百科下载了这幅图像,我想知道为什么加载如此缓慢[1:40:39] — 结果,我下载的维基百科版本是 30,000 x 30,000 像素。他们有这种严肃的画廊品质存档真的很酷。我不知道这个存在。不要试图在上面运行神经网络。完全毁了我的 Jupyter 笔记本。
所以我们可以为我们的梵高图像做到这一点,也可以为我们的优化图像做到这一点。然后我们可以比较这两者,最终我们会创建一幅内容类似于绘画但并非绘画的图像 — 这不是我们想要的。我们想要的是具有相同风格但不是绘画且没有内容的东西。所以我们想要丢弃所有的空间信息。我们不想要创造出一个这里有月亮,这里有星星,这里有教堂的东西。我们不想要任何这些。那么我们如何丢弃所有的特殊信息呢?
在这种情况下,这里有 19 个面 - 19 个切片。所以让我们拿到这个顶部切片,这将是一个 5x5 矩阵。现在,让我们展平它,我们得到一个 25 个元素的长向量。一下子,我们通过展平抛弃了大部分空间信息。现在让我们拿到第二个切片(即另一个通道)并做同样的事情。所以我们有通道 1 展平和通道 2 展平,它们都有 25 个元素。现在,让我们进行点积,我们可以在 Numpy 中用 @
来做(注:这里是 Jeremy 对我的点积与矩阵乘法问题的回答)。点积将给我们一个数字。那个数字是什么?它告诉我们什么?假设激活在 VGG 网络的中间层附近,我们可能期望其中一些激活是笔触纹理有多强,一些是这个区域有多明亮,一些是这部分是房子的一部分还是圆形的一部分,或者其他部分是这幅画的哪部分有多暗。所以点积基本上是一个相关性。如果这个元素和这个元素都非常正或都非常负,它会给我们一个大结果。另外,如果它们相反,它会给一个小结果。如果它们都接近零,它不会给结果。所以基本上点积是衡量这两个东西有多相似的一个指标。所以如果通道 1 和通道 2 的激活相似,那么它基本上是在说 - 让我们举个例子[1:44:28]。比如第一个是笔触纹理有多强(C1),而另一个是笔触有多倾斜(C2)。
如果细胞(1,1)的 C1 和 C2 同时高,细胞(4,2)也是如此,那么它表明具有纹理的网格单元也倾向于具有对角线。因此,当具有纹理的网格单元也具有对角线时,点积会很高,当它们没有时,点积也不高。所以这就是 C1 @ C2
。另外,C1 @ C1
实际上是 2-范数(即 C1 的平方和)。这基本上是在说纹理通道中有多少网格单元是活跃的,以及它们有多活跃。换句话说,C1 @ C1
告诉我们纹理绘画进行了多少。而 C2 @ C2
告诉我们对角线绘画进行了多少。也许 C3 是“颜色是否明亮?”,所以 C3 @ C3
将告诉我们明亮颜色单元有多频繁。
那么我们可以创建一个包含每个点积的 19x19 矩阵[1:47:17]。就像我们讨论过的,数学家们必须给每样东西起个名字,所以这个特定的矩阵,将其展平然后进行所有点积的操作,被称为 Gram 矩阵。
我告诉你一个秘密[1:48:29]。大多数深度学习从业者要么不知道,要么不记得所有这些东西,比如如果他们曾经在大学学过 Gram 矩阵。他们可能忘记了,因为之后他们可能熬夜了。实际上的工作方式是你意识到“哦,我可以创建一个非空间表示,展示通道之间的相关性”,然后当我写论文时,我不得不去问周围的人,“这个东西有个名字吗?” 然后有人会说“这不就是 Gram 矩阵吗?” 你去查一下,确实是。所以不要认为你必须先学习所有的数学。先运用你的直觉和常识,然后再担心数学叫什么,通常是这样。有时候也会反过来,不过不是对我,因为我不擅长数学。
所以这被称为 Gram 矩阵。当然,如果你是一个真正的数学家,非常重要的是你要说得好像你一直知道这是一个 Gram 矩阵,然后你就会说,哦是的,我们只是计算 Gram 矩阵。所以 Gram 矩阵就是这种映射——对角线可能是最有趣的部分。对角线显示哪些通道最活跃,然后非对角线显示哪些通道倾向于一起出现。总的来说,如果两幅图片有相同的风格,那么我们期望某些激活层会有相似的 Gram 矩阵。因为如果我们找到了捕捉很多关于画笔笔触和颜色的东西的激活层,那么仅仅对角线(在 Gram 矩阵中)可能就足够了。这是另一个有趣的作业,如果有人想尝试的话,可以尝试使用 Gatys 的风格迁移,而不是使用 Gram 矩阵,而是只使用 Gram 矩阵的对角线。这只需要改变一行代码。但我还没有看到有人尝试过,也不知道它是否会起作用,但它可能会很好。
“好的,是的,克里斯汀,你已经尝试过了。”“我已经尝试过了,大多数时候都有效,除非你有需要两种风格出现在同一个地方的有趣图片。所以看起来像是一半是草,一半是人群,你需要这两种风格。”(克里斯汀)。很酷,你仍然会做你的作业,但克里斯汀说她会替你做。
def scale_match(src, targ): h,w,_ = img.shape sh,sw,_ = style_img.shape rat = max(h/sh,w/sw); rat res = cv2.resize(style_img, (int(sw*rat), int(sh*rat))) return res[:h,:w] style = scale_match(img, style_img) plt.imshow(style) style.shape, img.shape ''' ((291, 483, 3), (291, 483, 3)) '''
这是我们的绘画。我尝试调整绘画的大小,使其与我的鸟类图片大小相同。所以这就是所有这些在做的事情。不管我使用哪一部分,只要它有很多漂亮的风格就可以了。
我像以前一样获取了我的优化器和随机图像:
opt_img_v, optimizer = get_opt()
这一次,我为所有的block_ends
调用SaveFeatures
,这将给我一个 SaveFeatures 对象的数组——每个模块都会出现在最大池化之前的层中。因为这一次,我想玩弄不同的激活层风格,更具体地说,我想让你来玩。所以现在我有了一个完整的数组。
sfs = [SaveFeatures(children(m_vgg)[idx]) for idx in block_ends]
style_img
是我的梵高的绘画。所以我拿我的style_img
,通过我的转换来创建我的转换风格图像(style_tfm
)。
style_tfm = val_tfms(style_img)
将其转换为一个变量,通过我的 VGG 模块的前向传播,现在我可以遍历所有的 SaveFeatures 对象并获取每组特征。请注意,我调用clone
,因为以后,如果我再次调用我的 VGG 对象,它将替换这些内容。我还没有想过这是否有必要。如果你把它拿走了,那没关系。但我只是小心翼翼。现在这是每个block_end
层的激活的数组。在这里,你可以看到所有这些形状:
m_vgg(VV(style_tfm[None])) targ_styles = [V(o.features.clone()) for o in sfs] [o.shape for o in targ_styles] ''' [torch.Size([1, 64, 288, 288]), torch.Size([1, 128, 144, 144]), torch.Size([1, 256, 72, 72]), torch.Size([1, 512, 36, 36]), torch.Size([1, 512, 18, 18])] '''
你可以看到,能够快速地编写一个列表推导式在你的 Jupyter 玩耍中非常重要。因为你真的希望能够立即看到这是我的通道(64、128、256,…),以及我们期望的网格大小减半(288、144、72…),因为所有这些都出现在最大池化之前。
因此,要进行 Gram MSE 损失,它将是输入的 Gram 矩阵与目标的 Gram 矩阵的 MSE 损失。Gram 矩阵只是x
与x
转置(x.t()
)的矩阵乘积,其中 x 简单地等于我已经将批处理和通道轴全部展平的输入。我只有一个图像,所以可以忽略批处理部分——基本上是通道。然后其他所有部分(-1
),在这种情况下是高度和宽度,是另一个维度,因为现在将是通道乘以高度和宽度,然后正如我们讨论过的,我们可以将其与其转置进行矩阵乘积。为了归一化,我们将其除以元素的数量(b*c*h*w
)——如果我说input.numel
(元素的数量)会更优雅,这将是相同的事情。再次,这给我了很小的数字,所以我乘以一个大数字使其变得更合理。所以这基本上就是我的损失。
def gram(input): b,c,h,w = input.size() x = input.view(b*c, -1) return torch.mm(x, x.t())/input.numel()*1e6 def gram_mse_loss(input, target): return F.mse_loss(gram(input), gram(target))
现在我的风格损失是将我的图像优化,通过 VGG 前向传递,获取所有 SaveFeatures 对象中特征的数组,然后在每一层上调用我的 Gram MSE 损失。这将给我一个数组,然后我只需将它们相加。现在你可以用不同的权重将它们相加,你可以添加子集,或者其他。在这种情况下,我只是获取了所有的。
def style_loss(x): m_vgg(opt_img_v) outs = [V(o.features) for o in sfs] losses = [gram_mse_loss(o, s) for o,s in zip(outs, targ_styles)] return sum(losses)
像以前一样将其传递给我的优化器:
n_iter=0 while n_iter <= max_iter: optimizer.step(partial(step,style_loss)) ''' Iteration: n_iter, loss: 230718.453125 Iteration: n_iter, loss: 219493.21875 Iteration: n_iter, loss: 202618.109375 Iteration: n_iter, loss: 481.5616760253906 Iteration: n_iter, loss: 147.41177368164062 Iteration: n_iter, loss: 80.62625122070312 Iteration: n_iter, loss: 49.52326965332031 Iteration: n_iter, loss: 32.36254119873047 Iteration: n_iter, loss: 21.831811904907227 Iteration: n_iter, loss: 15.61091423034668 '''
这里有一张随机图像,风格类似于梵高,我觉得挺酷的。
x = val_tfms.denorm(np.rollaxis(to_np(opt_img_v.data),1,4))[0] plt.figure(figsize=(7,7)) plt.imshow(x);
再次,Gatys 已经为我们做好了。这里是不同层次的随机图像,风格类似于梵高。所以第一个,你可以看到,激活是简单的几何图形——一点也不有趣。后面的层次更有趣。所以我们有一种怀疑,我们可能想要主要使用后面的层次来进行风格损失,如果我们想要看起来好的话。
我添加了这个SaveFeatures.close
,它只是调用self.hook.remove()
。记住,我将 hook 存储为self.hook
,所以hook.remove()
会将其删除。最好将其删除,否则可能会一直使用内存。因此,在最后,我只需遍历每个 SaveFeatures 对象并关闭它:
for sf in sfs: sf.close()
风格转移
风格转移是将内容损失和风格损失加在一起,并加上一些权重。所以没有太多可以展示的。
获取我的优化器,获取我的图像:
opt_img_v, optimizer = get_opt()
我的综合损失是一个特定层次的 MSE 损失,我所有层次的风格损失,将风格损失相加,加到内容损失上,我正在缩放内容损失。实际上,我已经将风格损失缩放为 1E6。所以它们都被精确地缩放了。将它们加在一起。再次,你可以尝试对不同的风格损失进行加权,或者你可以删除其中一些,所以这是最简单的版本。
def comb_loss(x): m_vgg(opt_img_v) outs = [V(o.features) for o in sfs] losses = [gram_mse_loss(o, s) for o,s in zip(outs, targ_styles)] cnt_loss = F.mse_loss(outs[3], targ_vs[3])*1000000 style_loss = sum(losses) return cnt_loss + style_loss
训练它:
n_iter=0 while n_iter <= max_iter: optimizer.step(partial(step,comb_loss)) ''' Iteration: n_iter, loss: 1802.36767578125 Iteration: n_iter, loss: 1163.05908203125 Iteration: n_iter, loss: 961.6024169921875 Iteration: n_iter, loss: 853.079833984375 Iteration: n_iter, loss: 784.970458984375 Iteration: n_iter, loss: 739.18994140625 Iteration: n_iter, loss: 706.310791015625 Iteration: n_iter, loss: 681.6689453125 Iteration: n_iter, loss: 662.4088134765625 Iteration: n_iter, loss: 646.329833984375 ''' x = val_tfms.denorm(np.rollaxis(to_np(opt_img_v.data),1,4))[0] plt.figure(figsize=(9,9)) plt.imshow(x, interpolation='lanczos') plt.axis('off');
for sf in sfs: sf.close()
天啊,它看起来真的很好。所以我觉得这很棒。这里的主要要点是,如果你想用神经网络解决问题,你所要做的就是设置一个损失函数,然后优化某些东西。而损失函数是一个较低的数字是你更满意的东西。因为当你优化它时,它会使那个数字尽可能低,它会做你想要它做的事情。所以在这里,Gatys 提出了一个损失函数,当它看起来像我们想要的东西时,它会是一个较小的数字,看起来像我们想要的风格。这就是我们所要做的。
实际上,除了实现了 Gram MSE 损失,这只是 6 行代码,这就是我们的损失函数:
将其传递给我们的优化器,等大约 5 秒钟,我们就完成了。记住,我们可以一次处理一批,所以我们可以等待 5 秒钟,64 个就完成了。所以我认为这真的很有趣,自从这篇论文发表以来,它确实激发了很多有趣的工作。不过对我来说,大部分有趣的工作还没有发生,因为对我来说,有趣的工作是将人类创造力与这些工具结合起来的工作。我还没有看到可以下载或使用的工具,艺术家可以控制并可以以交互方式进行操作。与Google Magenta项目的人交谈很有趣,这是他们的创意人工智能项目,他们在音乐方面所做的一切都是关于这个的。它正在构建音乐家可以实时使用的工具。由于 Magenta 的存在,您将在音乐领域看到更多这样的东西。如果您访问他们的网站,您会看到各种按键,可以实际更改鼓点、旋律、音调等。您肯定会看到 Adobe 或 Nvidia 开始发布一些小型原型并开始这样做,但这种创意人工智能的爆发尚未发生。我认为我们已经拥有了我们所需的所有技术,但没有人将其整合到一起并说“看看我建造的东西,看看人们用我的东西建造的东西”。所以这只是一个巨大的机会领域。
所以我在课堂开始时提到的那篇论文[2:01:16] ——基本上是将美国队长的盾牌添加到任意绘画中使用了这种技术。不过,诀窍是通过一些微小的调整使粘贴的美国队长盾牌能够很好地融入其中。但那篇论文只有几天的历史,所以尝试这个项目将是一个非常有趣的项目,因为您可以使用所有这些代码。它确实利用了这种方法。然后,您可以从使内容图像类似于带有盾牌的绘画开始,然后样式图像可以是不带盾牌的绘画。这将是一个很好的开始,然后您可以看看他们在这篇论文中尝试解决的具体问题,以使其更好。但您现在可以开始。
问题:之前有很多人表达了对 Pyro 和概率编程的兴趣。所以 TensorFlow 现在有了这个 TensorFlow 概率或其他东西。有很多概率编程框架。我认为它们很有趣,但至今未经证明,因为我还没有看到任何使用概率编程系统完成的事情,而没有使用它们更好。基本前提是它允许你创建更多关于你认为世界是如何运作的模型,然后插入参数。所以 20 年前当我还在管理咨询行业工作时,我们经常使用电子表格,然后我们会使用这些蒙特卡洛模拟插件——有一个叫做 At Risk(?),一个叫做 Crystal Ball。我不知道几十年后它们是否还存在。基本上它们让你可以更改电子表格中的一个单元格,说这不是一个具体的值,而实际上代表一个具有这个均值和标准差的值分布,或者它有这个分布,然后你会点击一个按钮,电子表格会从这些分布中随机抽取一千次数字重新计算,并显示你的结果的分布,可能是利润或市场份额或其他什么。那时我们经常使用它们。显然,人们认为电子表格是做这种工作的更明显的地方,因为你可以更自然地看到所有这些,但我不知道。我们将看到。在这个阶段,我希望它能够被证明有用,因为我觉得它非常吸引人,符合我过去经常做的工作。实际上,围绕这种东西有整个实践,他们过去称之为系统动力学,这实际上是建立在这种东西之上的,但它并没有走得太远。
问题:然后有一个关于通用风格转移预训练的问题。我不认为你可以为通用风格进行预训练,但你可以为特定风格的通用照片进行预训练,这就是我们要达到的目标。尽管可能最终会成为一项作业。我还没有决定。但我会做所有的部分。
问题:请让他谈谈多 GPU。哦,是的,我还没有关于那个的幻灯片。我们马上就要谈到了。
在我们开始之前,再分享一张来自 Gatys 论文的有趣图片。他们有更多图片,只是没有适合我的幻灯片,但是有不同的卷积层用于风格。不同的风格和内容比例,这里是不同的图片。显然这不再是梵高的风格,这是一个不同的组合。所以你可以看到,如果你只做风格,你看不到任何图片。如果你做很多内容,但是使用足够低的卷积层,看起来还可以,但背景有点愚蠢。所以你可能想要在中间某个地方。所以你可以尝试一下,做一些实验,但也可以使用论文来帮助指导你。
数学
实际上,我想现在开始研究数学,下周我们将讨论多 GPU 和超分辨率,因为这是来自论文的内容,我真的希望在我们讨论完论文后,你们能阅读论文,并在论坛上提出任何不清楚的问题。但这篇论文中有一部分我想谈谈,讨论如何解释它。所以论文说,我们将得到一个输入图像x,这个小东西通常表示它是一个向量,Rachel,但这个是一个矩阵。我猜它可能是两者之一。我不知道。通常小写粗体字母表示向量,或者带有上箭头的小写字母表示向量。通常大写字母表示矩阵,或者带有两个箭头的小写字母表示矩阵。在这种情况下,我们的图像是一个矩阵。我们基本上将其视为向量,所以也许我们只是在超前一步。
所以我们有一个输入图像x,它可以通过 CNN 的特定层中的滤波器响应(即激活)进行编码。滤波器响应就是激活。希望你们都能理解。CNN 的基本功能就是生成激活层。一个层有一堆滤波器,产生一定数量的通道。今年表示第 L 层有大写 Nl个滤波器。再次强调,这里的大写字母不代表矩阵。所以我不知道,数学符号是如此不一致。所以在第 L 层有 Nl 个不同的滤波器,这意味着也有同样数量的特征图。所以确保你能看到这个字母 Nl 和这个字母是一样的。所以你必须非常小心地阅读字母并认识到它就像啪一下,这个字母和那个字母是一样的。显然,Nl 个滤波器创建了 Nl 个特征图或通道,每个尺寸为 Ml(好吧,我看到这里正在发生展开)。这就像 numpy 符号中的 M[l]。这是第l层。所以 M 是第l层。尺寸是高度乘以宽度——所以我们将其展平。所以第 l 层的响应可以存储在矩阵 F 中(现在l在顶部,出于某种原因)。这不是 f^l,这只是另一个索引。我们只是为了好玩而移动它。这里我们说它是 R 的元素——这是一个特殊的 R,表示实数 N 乘以 M(这表示它的维度是 N 乘以 M)。这非常重要,不要继续。就像 PyTorch 一样,确保你首先理解维度的秩和大小,数学也是一样。这些是你停下来思考为什么是 N 乘以 M 的地方。N 是滤波器的数量,M 是高度乘以宽度。所以你还记得我们做.view(b*c, -1)
的时候吗?这就是。所以尝试将代码映射到数学上。所以 F 是x
:
如果我对你更友好,我会使用与论文相同的字母。但我太忙于让这个该死的东西运行起来,无法仔细做到这一点。所以你可以回去将其重命名为大写 F。
所以我们将 L 移到顶部是因为我们现在要有更多的索引。在 Numpy 或 PyTorch 中,我们通过方括号索引事物,然后用逗号分隔很多东西。在数学中的方法是用小写字母围绕你的字母——到处都扔上去。所以这里,Fl是 F 的第l层,然后ij是第l层中第i个滤波器在第j位置的激活。所以位置j的大小是 M,即高度乘以宽度的大小。这是容易混淆的事情。通常你会看到一个ij,然后假设它是在图像的高度乘以宽度的位置进行索引,但实际上不是,对吧?它是在通道中对展平图像的第i个滤波器/通道的第j个位置进行索引。它甚至告诉你——它是第l层中展平图像中第j个位置的第i个滤波器/通道。所以除非你理解 F 是什么,否则你将无法进一步阅读论文。这就是为什么这些是你停下来确保你感到舒适的地方。
所以现在,内容损失,我不会花太多时间,但基本上我们只是要检查激活值与预测值的平方[2:12:03]。所以这就是我们的内容损失。风格损失将是类似的,但使用格拉姆矩阵 G:
我真的很想向你展示这个。我觉得这很棒。有时我真的喜欢数学符号中可以做的事情,它们也是你通常可以在 J 和 APL 中做的事情,这种隐式循环正在这里进行。这是在说什么呢?嗯,它在说我的层l中的格拉姆矩阵,对于一个轴上的第i个位置和另一个轴上的第j个位置等于我的 F 矩阵(所以我的展平矩阵)对于该层中的第i个通道与同一层中的第j个通道,然后我将进行求和。我们将取第k个位置并将它们相乘然后将它们全部加起来。所以这正是我们之前计算格拉姆矩阵时所做的事情。所以这里发生了很多事情,因为对我来说,这是非常巧妙的符号 —— 有三个隐式循环同时进行,加上求和中的一个显式循环,然后它们一起工作来为每一层创建这个格拉姆矩阵。所以让我们回去看看你是否能匹配这个。所以所有这一切都同时发生,这非常棒。
就是这样。所以下周,我们将看到一个非常类似的方法,基本上再次进行风格转移,但这次我们实际上会训练一个神经网络来为我们做这件事,而不是进行优化。我们还将看到你可以做同样的事情来进行超分辨率。我们还将回顾一些 SSD 的内容,以及进行一些分割。所以如果你忘记了 SSD,这周可能值得进行一点复习。好的,谢谢大家。下周见。