2018年5月6日星期日

合成逼真图像,试试港中大&英特尔的半参数方法 | CVPR 2018 oral

Root 编译整理量子位 出品 | 公众号 QbitAI

你可能不相信,上面这张图是合成的。

CG要达到这样真实的效果,目前主流的做法是先手动建模,把物体的表面结构搭建出来,然后再贴图、定材质、上灯光,最后渲染。

深度神经网络的出现,给CG带来一道曙光。

根据大致的草图框架(也称语义布局法),深度神经网络现在可以直接合成真实效果的图片。

不过,主流图像合成所用的模型大多是参数模型(parametric models)。这种模型,所有和逼真外观有关的数据,都会体现在深度神经网络的权重里。

不过,这与人类画画的方式不太一样。

我们在画画的时候,不是完全凭记忆复刻现实的。而是把外界真实的物体当作一个参考,然后细节上微调,进行再创作。

参数模型的优点是具有高度的表现力(highly expressive),可进行端对端训练。而非参数模型(nonparametric models)的优点,是可以在测试时提取大型的真实图片数据集里的素材。

为了集结这两种模型的优势,香港中文大学联合英特尔视觉计算实验室共同研究出了一种半参数模型,简称为SIMS,相关工作论文Semi-parametric Image Synthesis已被CVPR 2018接收为口头汇报。

他们工作的思路是:

1)先用大型真实图像数据集先训练非参数模型,相当于获得了一个合成素材库。

2)然后基于语义布局(Semantic layout),把这些素材填充进去,就像一张图被分割成好几个版块之后,再往上打补丁充实细节。

接缝的地方,深度网络会自行融合,并计算好版块之间物体的空间关系,进一步加强视觉的真实效果。

实验结果非常不错。

在Cityspaces、NYU、ADE20K等数据集上训练得到的效果,真实程度比去年8月量子位报道过的合成方法提高了不少。

对比上下图,你会发现,SIMS合成的图在清晰度上,光线折射关系上,都有出色的表现。物体融合的时候也不会发生扭曲。

语义布局合成法,也正是本论文的两位作者——英特尔实验室视觉组主管Vladlen Koltun与的陈启峰提出的。

换句话说,这篇论文是在陈启峰和Koltun之前工作的基础上,作出了进一步的优化。

图片的合成流水线

首先,给一个草图。告诉模型,你想合成的图片布局是什么样的。就像下图最左上的小图那样。

另外,也要砌一个素材库。

巧妇难为无米之炊。模型并不能自己瞎开脑洞编造合成用的素材。得"吃"大量的真实图片之后建一个记忆库(External Memory Bank)。

这两步完成后,模型就根据草图切割的形状,提取出记忆库里能对上号的素材,比如说路边的建筑啊,停放好的车辆,以及树啥的。如(b)图所示。

合适的素材拎出来,Transformation网络负责微调,使得各版块的素材之间二维融合的效果比较好,不至于看起来很突兀。

最后,Ordering网络计算出这些板块的空间位置,给予适当的光影关系,合成一幅逼真的图片。

OMT

这篇论文的一作和导师都很有来头。

先来介绍一下一作,齐晓娟。

齐晓娟,香港中文大学计算机科学与工程系4年级博士生,本科就读于上海交大电子科学与技术专业。

目前研究方向主要是计算机视觉,深度网络和医学影像分析。目前攻克的课题集中在语义分割,3D场景理解和图像合成上。

据GitHub上的资料介绍,她已有三篇文章被CVPR2018收录。

除了半参数图像合成这篇,另外两篇分别是GeoNet: Geometric Neural Network for Joint Depth and Surface Normal EstimationReferring Image Segmentation via Recurrent Refinement Networks

齐晓娟曾在英特尔视觉计算实验室(Intel Visual Computing Lab)实习过半年,师从Vladlen Koltun,研究课题就是图像合成。Vladlen Koltun也是本篇论文的作者之一。

导师贾佳亚博士,不仅是香港中文大学计算机科学与工程系的终生教授,还是腾讯优图实验室的杰出科学家。

贾佳亚教授于去年5月加入腾讯优图实验室。随后组建团队,打造出了好几个产品应用。那个刷爆了票圈的军装照,还有"一键卸妆"应用,都出自他们团队。

在贾佳亚教授的带领下,腾讯优图实验室在ICCV 2017顶会上取得了十分漂亮的成绩单,共有12篇论文入围。今年的CVPR,贾佳亚教授团队一共中了6篇论文。

二作陈启峰的经历也十分传奇。

曾放弃清华保送的资格。本科就读于香港科技大学,并获取2011年的ACM国际大学生程序设计竞赛金牌。本科毕业后,一举拿下九所名校全额奖学金offer,最后他选择了斯坦福。现在英特尔实验室任研究人员。

最后,附code:https://ift.tt/2HTtwGF

以及论文:https://ift.tt/2I0O8c4

欢迎大家关注我们的专栏:量子位 - 知乎专栏

诚挚招聘

量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复"招聘"两个字。

量子位 QbitAI · 头条号签约作者

վ'ᴗ' ի 追踪AI技术和产品新动态



via 量子位 - 知乎专栏 https://ift.tt/2Ij7Kew
RSS Feed

RSS5

IFTTT

合成逼真图像,试试港中大&英特尔的半参数方法 | CVPR 2018 oral

Root 编译整理量子位 出品 | 公众号 QbitAI

你可能不相信,上面这张图是合成的。

CG要达到这样真实的效果,目前主流的做法是先手动建模,把物体的表面结构搭建出来,然后再贴图、定材质、上灯光,最后渲染。

深度神经网络的出现,给CG带来一道曙光。

根据大致的草图框架(也称语义布局法),深度神经网络现在可以直接合成真实效果的图片。

不过,主流图像合成所用的模型大多是参数模型(parametric models)。这种模型,所有和逼真外观有关的数据,都会体现在深度神经网络的权重里。

不过,这与人类画画的方式不太一样。

我们在画画的时候,不是完全凭记忆复刻现实的。而是把外界真实的物体当作一个参考,然后细节上微调,进行再创作。

参数模型的优点是具有高度的表现力(highly expressive),可进行端对端训练。而非参数模型(nonparametric models)的优点,是可以在测试时提取大型的真实图片数据集里的素材。

为了集结这两种模型的优势,香港中文大学联合英特尔视觉计算实验室共同研究出了一种半参数模型,简称为SIMS,相关工作论文Semi-parametric Image Synthesis已被CVPR 2018接收为口头汇报。

他们工作的思路是:

1)先用大型真实图像数据集先训练非参数模型,相当于获得了一个合成素材库。

2)然后基于语义布局(Semantic layout),把这些素材填充进去,就像一张图被分割成好几个版块之后,再往上打补丁充实细节。

接缝的地方,深度网络会自行融合,并计算好版块之间物体的空间关系,进一步加强视觉的真实效果。

实验结果非常不错。

在Cityspaces、NYU、ADE20K等数据集上训练得到的效果,真实程度比去年8月量子位报道过的合成方法提高了不少。

对比上下图,你会发现,SIMS合成的图在清晰度上,光线折射关系上,都有出色的表现。物体融合的时候也不会发生扭曲。

语义布局合成法,也正是本论文的两位作者——英特尔实验室视觉组主管Vladlen Koltun与的陈启峰提出的。

换句话说,这篇论文是在陈启峰和Koltun之前工作的基础上,作出了进一步的优化。

图片的合成流水线

首先,给一个草图。告诉模型,你想合成的图片布局是什么样的。就像下图最左上的小图那样。

另外,也要砌一个素材库。

巧妇难为无米之炊。模型并不能自己瞎开脑洞编造合成用的素材。得"吃"大量的真实图片之后建一个记忆库(External Memory Bank)。

这两步完成后,模型就根据草图切割的形状,提取出记忆库里能对上号的素材,比如说路边的建筑啊,停放好的车辆,以及树啥的。如(b)图所示。

合适的素材拎出来,Transformation网络负责微调,使得各版块的素材之间二维融合的效果比较好,不至于看起来很突兀。

最后,Ordering网络计算出这些板块的空间位置,给予适当的光影关系,合成一幅逼真的图片。

OMT

这篇论文的一作和导师都很有来头。

先来介绍一下一作,齐晓娟。

齐晓娟,香港中文大学计算机科学与工程系4年级博士生,本科就读于上海交大电子科学与技术专业。

目前研究方向主要是计算机视觉,深度网络和医学影像分析。目前攻克的课题集中在语义分割,3D场景理解和图像合成上。

据GitHub上的资料介绍,她已有三篇文章被CVPR2018收录。

除了半参数图像合成这篇,另外两篇分别是GeoNet: Geometric Neural Network for Joint Depth and Surface Normal EstimationReferring Image Segmentation via Recurrent Refinement Networks

齐晓娟曾在英特尔视觉计算实验室(Intel Visual Computing Lab)实习过半年,师从Vladlen Koltun,研究课题就是图像合成。Vladlen Koltun也是本篇论文的作者之一。

导师贾佳亚博士,不仅是香港中文大学计算机科学与工程系的终生教授,还是腾讯优图实验室的杰出科学家。

贾佳亚教授于去年5月加入腾讯优图实验室。随后组建团队,打造出了好几个产品应用。那个刷爆了票圈的军装照,还有"一键卸妆"应用,都出自他们团队。

在贾佳亚教授的带领下,腾讯优图实验室在ICCV 2017顶会上取得了十分漂亮的成绩单,共有12篇论文入围。今年的CVPR,贾佳亚教授团队一共中了6篇论文。

二作陈启峰的经历也十分传奇。

曾放弃清华保送的资格。本科就读于香港科技大学,并获取2011年的ACM国际大学生程序设计竞赛金牌。本科毕业后,一举拿下九所名校全额奖学金offer,最后他选择了斯坦福。现在英特尔实验室任研究人员。

最后,附code:https://ift.tt/2HTtwGF

以及论文:https://ift.tt/2I0O8c4

欢迎大家关注我们的专栏:量子位 - 知乎专栏

诚挚招聘

量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复"招聘"两个字。

量子位 QbitAI · 头条号签约作者

վ'ᴗ' ի 追踪AI技术和产品新动态



via 量子位 - 知乎专栏 https://ift.tt/2Ij7Kew
RSS Feed

RSS5

IFTTT

2018年5月5日星期六

机器学习时代的哈希算法,将如何更高效地索引数据

哈希算法一直是索引中最为经典的方法,它们能高效地储存与检索数据。但在去年 12 月,Jeff Dean 与 MIT 等研究者将索引视为模型,探索了深度学习模型学习的索引优于传统索引结构的条件。本文首先将介绍什么是索引以及哈希算法,并描述在机器学习与深度学习时代中,如何将索引视为模型学习比哈希算法更高效的表征。

2017 年 12 月,谷歌和麻省理工学院的研究人员发表了一篇研究论文 The Case for Learned Index Structures,该论文介绍了他们在「学习型索引结构」方面做出的探索。这些研究非常令人兴奋,正如作者在摘要中所述:

「[…] 我们相信通过可学习的模型取代数据管理系统核心组件的想法对未来的系统设计有着深远的影响,而我们这项工作对于未来的发展仅仅是惊鸿一瞥。」

事实上,谷歌和麻省理工学院研究人员提出的这项研究工作可以同索引世界中最为经典有效的 B-Tree 和哈希图(Hash Map)相匹敌。工程界对机器学习的未来早已有了许多的观点,因此这篇研究论文已经在 Hacker News,Reddit 乃至世界上所有的工业论坛中获得了广泛的关注和讨论。

新的研究是重新审视一个领域基础的绝佳机会,而且像索引等基础性并且已经获得足够研究的技术很少会有机会取得更大的突破。本文作为哈希表的入门性介绍,简要介绍了影响其快慢的主要因素,并为应用于索引构建技术的机器学习概念提供了直观性理解。

针对谷歌/麻省理工学院合作发表的工作,Peter Bailis 和斯坦福大学的研究团队回顾了索引构建的基础知识,并劝诫我们不要扔掉经典的算法书。Bailis 和他在斯坦福大学的团队重新构建了可学习型索引策略,并且通过使用名为 Cuckoo Hashing 的经典哈希表策略,在不使用任何机器学习技术的条件下取得了相似的结果。

在另一个对谷歌/麻省理工学院合作发表的工作的回应中,Thomas Neumann 描述了另一种实现与学习型索引策略相似性能的方式,这种方式仍然使用了经过长久测试和深入理解的 B-Tree。当然,这些讨论、对比实验和对进一步研究的要求,正是谷歌/麻省理工学院团队为之激动的理由,他们在论文中写道:

「需要特别强调的是:我们并不是要呼吁完全用学习型索引结构来替代传统的索引结构。相反的是,我们勾画出了一个全新的索引构建方法,它可以弥补现有工作的不足,甚至可以说是在已经有数十年历史的研究领域中打开了一个全新的研究方向。」

那么,究竟是什么引起了人们如此的关注?哈希图和 B-Trees(多路搜索树)是否注定要被新技术所淘汰?机器是否即将重写算法教科书?如果机器学习策略真的比我们所知道和喜爱的通用索引策略更好,那么它对计算机世界又意味着什么呢?学习型索引在什么情况下会超越旧的索引方式呢?

为了解决这些问题,我们需要理解什么索引,它们解决了什么问题以及是什么决定了不同索引之间的优劣差异。

什么是索引

索引的核心是要提高信息查询和检索的便捷性。早在计算机发明之前,人类就一直在对事物进行索引。当我们使用整齐的文件柜时,我们使用的是一个索引系统。全卷百科全书可被视为一种索引策略,杂货店里的标签过道也是一种索引。当我们有许多东西并且需要在集合中找到或识别特定的物品时,索引可以让我们查询的过程变得更加高效便捷。

Zenodotus,亚历山大大图书馆的第一任馆员,负责组织管理图书馆庞大的馆藏。他设计的系统包括按照流派将书籍分组放入房间,并按字母顺序放置书本。他的同行 Callimachus 走得更远,引入了一个名为 pinakes 的中央目录,它允许图书管理员查找作者,并确定该作者的每本书在图书馆中的位置。包括 1876 年被发明的杜威十进制系统(Dewey Decimal System)在内,许许多多的图书馆索引构建方式相继被发明出来。

在亚历山大图书馆,索引被用于将一段信息(书或作者的名字)映射到图书馆内的物理位置。尽管我们的计算机是数字设备,但计算机中的任何特定数据实际上都驻留在至少一个物理位置。无论是文本、最近的信用卡交易记录还是视频,数据都存在于计算机上的某个物理磁盘位置。

在 RAM 和固态硬盘驱动器中,数据作为电压存储在一系列晶体管中。在较老的旋转硬盘驱动器中,数据以磁盘格式存储在磁盘的特定圆弧上。当我们将计算机中的信息编入索引时,我们创建了一些算法,将部分数据映射到计算机中的物理位置。我们称这个地址为地址。在计算机中,被索引的信息全部都是以比特形式存在的数据,索引用于将这些数据映射到它们的地址。

数据库是索引编制的典型用例。数据库旨在保存大量信息,并且一般来说,我们希望高效地检索这些信息。搜索引擎的核心是对互联网上可用信息的庞大索引,哈希表、二叉搜索树、字典树、B-树和布隆过滤器都是索引的形式。

很容易想象在亚历山大图书馆的迷宫大厅找到具体书籍会有多难,但我们不应该理所当然地认为人类产生数据的大小呈指数级增长。互联网上人们可以获取到的数据量远远超过任何时代的任何单个图书馆的容量,而谷歌的目标是将所有数据都编入索引。人类为索引创造了许多策略,我们在本文讨论了历史上最多产的数据结构之一,并且恰好是一种索引结构的哈希表。

什么是哈希表

初看起来,哈希表是基于哈希函数的简单数据结构,我们有许多种行为不同并且被用于不同目的的哈希函数。在接下来的部分中,我们将只描述哈希表中使用的哈希函数,而不对加密哈希函数、校验和或任何其他类型的哈希函数展开讨论。

哈希函数接受一些输入值(例如数字或文本)并返回一个整数,我们称之为哈希码或哈希值。对于任何给定相同的输入,哈希码总是相同的,这意味着哈希函数必须是确定性的。

在构建哈希表时,我们首先为哈希表分配一些空间(在内存或磁盘中),我们可以视为创建一个任意大小的新数组。如果我们有很多数据,我们可能会使用较大的数组,如果我们只有少量数据,则可以使用更小的数组。任何时候我们想索引一个单独的数据,就需要创建一个键值对,其中键(Key)是关于数据的一些标识信息,而值(Value)是数据本身。

我们需要将值插入哈希表中,将数据的键发送给哈希函数哈希函数返回一个整数(哈希码),我们使用这个整数(以数组的大小为模)作为我们数组中数值的存储索引。如果我们想从哈希表中检索值,我们只需重新计算键中的哈希码并从数组中的该位置获取数据,这个位置就是我们数据的物理地址。

在使用杜威十进制系统的图书馆中,「键」是书本所属的一系列分类,「值」是书本身。「哈希码」是我们使用杜威十进制过程创建的数值,例如一本解析几何编码得到了 516.3 的「哈希码」,自然科学是 500、数学是 510、几何是 516、解析几何是 516.3。在这种方式下,杜威十进制系统可以被视为书籍的哈希函数,然后这些书将被放在与其哈希值对应的一组书架上,并按作者的字母顺序排列在书架内。

我们的比喻不是特别地完美,与杜威十进制数字不同,哈希表中用于索引的哈希值通常不会提供信息——在完美的比喻中,图书馆目录将包含每本书基于某一条相关信息的确切位置(可能是其标题,也许是作者的姓氏,也许是它的 ISBN 号码......),但除非所有具有相同键的书籍被放在同一个书架上,并且我们可以使用键在库目录中查找到书架编号,否则书籍的分组或排序就是没有意义的。

从根本上来说,这个简单的过程全都是由哈希表来完成的。然而,为了确保哈希索引的正确性和效率,我们又在此基础上构建了许多复杂的部分。

基于哈希索引的性能考量

哈希表中复杂性和优化的主要来源是哈希冲突(hash collisions)问题。当两个或更多个键产生相同的哈希码时会发生冲突。考虑如下的一个简单的哈希函数,我们假定其中的键为整数:

function hashFunction(key) {   return (key * 13) % sizeOfArray;  }

虽然任何唯一的整数在乘以 13 时会产生唯一的结果,但由于鸽巢原理(Pigeonhole principle),我们无法在每个桶只放一个物品的情况下将 6 个物品放入 5 个桶中,最终的哈希码仍然会重复出现。因为我们的存储量是有限的,所以我们不得不使用哈希值来对数组的大小取模,因此我们总会遇到冲突。

我们马上会讨论处理这些不可避免的冲突时所采用的常用策略,但首先应该注意的是,哈希函数的选择可以增加或减少冲突的几率。想象一下,我们总共有 16 个存储位置,且必须从如下两个函数中选择一个作为哈希函数

function hash_a(key) {   return (13 * key) % 16; }  function hash_b(key){   return (4 * key) % 16; }

在这种情况下,如果我们要对数字 0-32 进行哈希,hash_b 会产生 28 个冲突或重叠。7 个冲突分别产生于哈希值 0、4、8 和 12(前四个插入不发生冲突,但是后面的每个插入都会发生冲突)。然而,hash_a 会平均分散冲突,每个索引冲突 1 次,总共碰撞 16 次。这是因为在 hash_b 中,4 是哈希表大小 16 的一个因子,因为我们在 hash_a 中选择了一个素数,除非我们的表大小是 13 的倍数,我们不会遇到选择 hash_b 时的分组问题。

我们可以运行下面的脚本来观察这一过程:

function hash_a(key) {   return (13 * key) % 16; }  function hash_b(key){   return (4 * key) % 16; }  let table_a = Array(16).fill(0); let table_b = Array(16).fill(0);  for(let i = 0; i < 32; i++) {   let hash_code_a = hash_a(i);   let hash_code_b = hash_b(i);    table_a[hash_code_a] += 1;   table_b[hash_code_b] += 1; }  console.log(table_a); // [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2] console.log(table_b); // [8,0,0,0,8,0,0,0,8,0,0,0,8,0,0,0]

好的哈希函数可以在表中更均匀地分布哈希码间的冲突。

这种哈希策略,将输入的键乘以素数是一种非常常见的做法。质数减少了输出哈希码与数组大小共有一个公因式的可能性,从而减少了碰撞发生的可能。由于哈希表已经存在了相当长的一段时间,因此有很多不同种类的优秀哈希函数可供选择。

乘法移位哈希(Multiply-shift hashing)与 素数取模策略类似,但避免了相对昂贵的模运算,有利于快速进行移位操作。MurmurHash 和 Tabulation Hashing 是乘法移位哈希函数家族的强力替代品。对这些哈希函数进行的基准测试包括检查它们的计算速度,生成的哈希码的分布以及它们处理不同类型数据(例如除整数以外的字符串和浮点数)的灵活性。

如果我们选择一个好的哈希函数,我们可以降低冲突率并且仍然保持较高的计算速度。不幸的是,无论我们选择什么哈希函数,冲突总是难以避免的,决定如何处理冲突将对我们哈希表的整体性能产生重大影响。碰撞处理的两个常用策略是链接(Chaining)和线性探测(Linear Probing)。

链接简单易用,我们不是在哈希表的每个索引处存储每个条目,而是存储链表的头部指针。当一个条目通过我们的哈希函数与一个已经填充的索引相冲突时,我们将它添加为链表中的最后一个元素。查找不再是严格的「常数项时间」,因为我们必须遍历链表来查找特定项目。如果我们的哈希函数存在很多冲突,我们将会有很长的链。此外,由于对于长链的查找,哈希表的性能会随着时间的推移而降低。

链接:重复的冲突会创建更长的链接列表,但不会占用数组的其它索引。

线性探测在概念上很简单,但实现起来还是很麻烦的。在线性探测中,我们仍然为每个元素在哈希表保留一个索引。当索引 i 发生冲突时,我们检查索引 i + 1 是否为空,如果是,我们将数据存储在那里,如果 i + 1 也有一个元素,我们检查 i + 2,然后 i + 3 等,直到找到一个空插槽。只要我们找到一个空插槽,我们就将该值插入。相似地,我们可能无法实现常数级时间复杂度的查找,并且如果在一个索引中遇到多个冲突,那么我们最终将不得不搜索一系列长序列,然后才能找到要查找的条目。更重要的是,每当冲突发生时,后续发生冲突的几率都会增加。因为与链接不同,每个传入的项目最终会都占据一个新的索引。

线性探测:给定与上面链接图像相同的数据和哈希函数,我们得到一个新的结果。导致冲突的元素(红色)现在驻留在同一个数组中,并从冲突索引开始按顺序占据索引。

可能听起来链接是更好的选择,但线性探测往往被认为具有更好的性能特征。大多数情况下,这是由于链表的缓存利用率较差以及使用数组有利于提高缓存利用率。简答来说,检查链表中的所有链接比检查相同大小数组的所有索引要慢得多。这是因为每个数组中的索引在物理上相邻,而在链表中,每个新节点在创建时都会被赋予一个位置。这个新节点不一定与链表中的相邻节点在物理上相邻。其结果是,在链表中,列表顺序中「彼此相邻」的节点在 RAM 芯片内的物理位置上并不实际相邻。由于 CPU 高速缓存的工作原理,访问相邻内存位置的速度很快,而随机访问内存位置的速度则要慢得多。

机器学习基础

为了理解机器学习是如何重建哈希表(和其他索引)的关键特征的,有必要快速重新审视一下统计模型的主要思想。在统计学中,模型是可以接受一些向量为输入并返回标签(分类模型)或数据值(回归模型)的函数。输入向量包含所有数据点的相关信息,输出的标签或数据是模型的预测值。

在预测高校学生能否进入哈佛学习的模型中,输入向量可能包含学生的 GPA、SAT 成绩、参加的课外俱乐部的数量以及其他与学术成就相关的值;输出的标签可以是 True 或 False(可以进入哈佛或不可以进入哈佛)。

在预测抵押贷款违约率的模型中,输入向量可能包含信用值、信用卡账户数量、逾期付款频率、年收入以及与申请抵押人财务状况相关的其他值,该模型会返回一个 0 到 1 范围内的数字代表违约的可能性。

一般而言,可以用机器学习建立统计学模型。机器学习从业者将大量数据和机器学习算法相结合,在数据集上运行算法得到的结果是训练好的模型。机器学习的核心在于创造可以自动从原始数据中建立准确模型的算法,该算法无需人工帮助机器「理解」这些数据实际上表示什么。这与其他形式的人工智能,如人类广泛考察数据、告诉计算机这些数据的意义(如定义启发式)以及定义计算机如何使用这些数据(如使用极小极大算法或 A* 寻路算法)是不同的。尽管在实践中,机器学习常常和经典的非学习技术相结合;AI 智能体一般会同时使用学习和非学习策略以完成目标。

以著名的 AI 象棋棋手「深蓝」和最近广受关注的 AI 围棋棋手「AlphaGo」为例。深蓝是完全的非学习 AI;程序员和象棋专家合作为深蓝创建了一个函数,该函数以棋局状态为输入(所有棋子的位置以及棋手的回合),返回的值与该位置有多「好」相关。深蓝从不「学习」任何东西——人类棋手编写了机器的评估函数。深蓝的主要特征是树搜索算法,该算法计算了所有可能的落子处、以及对手对这些落子方法可能做出的回应以及未来可能会产生的移动。

AlphaGo 树搜索的可视化。(来源:https://blogs.loc.gov/maps/category/game-theory/)

 AlphaGo 执行的也是树搜索。与深蓝的相似之处在于,AlphaGo 预测了可能的每一步之后的好几步,而不同点在于 AlphaGo 在没有围棋专家明确指导的情况下建立了其自己的评估函数。在这种情况中,评估函数是一个训练好的模型。AlphaGo 的机器学习算法将棋盘状态(每一个位置是黑子、白子还是没有棋子)作为输入向量,标签表示了哪一方的棋手(白棋或黑棋)会赢。使用这些信息,机器学习算法在数十万种游戏中都可以确定该如何评估当前状态。AlphaGo 通过观察数以百万的例子教会自己在哪落子赢得游戏的可能性更高。

将模型作为索引,背离 ML 范式

谷歌的研究人员首先在他们的文章中提出索引是模型的前提,或者说至少可以将机器学习模型当索引用。理由是:模型是接受一些输入,然后返回一个标签的机器;如果输入是关键词,而标签是模型对内存地址的判断,模型就可以当索引用。尽管听起来很清晰明了,但是索引的问题似乎不能完美契合于机器学习。在一些领域中,谷歌团队已经离开了机器学习范式以实现他们自己的目标。

一般情况下,机器学习模型是在已知数据上训练,目标是评估没见到的数据。若我们对数据进行索引,就没法接受评估值。索引唯一的用处在于得到内存中一些数据的确切定位。一个箱子外的神经网络(或其他机器学习器)无法精确到这种程度。谷歌通过在训练过程中追踪最大(正)和最小(负)误差以解决这个问题。将这些值作为边界,ML 索引可以在界限内进行搜索,找到元素的确切位置。

另一个出发点是机器学习从业者一般都要小心避免他们的模型对训练数据「过拟合」;一个「过拟合」模型会在训练数据上产生很高的预测准确率,但在训练集以外的数据上表现得很糟糕。另一方面,从定义上讲,索引是过度拟合的。训练集是被索引过的数据,这也使其成为测试集。由于查找必须发生在索引的实际数据上,在这种机器学习的应用上更容易遇到过拟合的问题。同时,如果模型已经对现有数据过拟合了,再向索引添加项目可能会在预测时造成可怕的错误;正如文章中所述:

「在这个模型的普遍性和『最后一英里』的表现间发生了有趣的取舍;可以这么说,『最后一英里』的预测结果越好,模型的过拟合就越严重,就越不适用于新的数据项目。」

最后,一般而言,模型的训练过程是整个过程中最昂贵的部分。不幸的是,在广泛的数据库应用程序(和其他索引应用程序)中,将数据添加到索引中是很常见的。该团队坦言这方面的限制:

「迄今为止,我们的结果都将注意力集中在只读存储区数据库系统的索引结构上。正如我们已经指出的那样,目前的设计,即使没有任何重大的修改,也已经可以替换现在的数据库中的索引结构了——前者的索引结构可能每天只更新一次,而后者则需要在合并 SSTable 的过程中批量创建 B-树。」——(SSTable 是谷歌的「BigTable」的重要元件)

学习哈希

本文研究了使用机器学习模型代替标准哈希函数的可能性。研究人员们想要了解的问题之一是:了解数据的分布可以帮助我们更好地创建索引吗?用我们之前讨论过的传统的策略(移位乘法、Murmur Hash、素数乘法……),完全忽略了数据的分布。每一个传入的项目都被视为独立的值,而非作为具有数值属性的较大数据集的一部分考虑的。结果是,即使在很多先进的哈希表中,也存在大量空间浪费的问题。

实现哈希表的内存利用率只有约 50%,这意味着哈希表占用了数据存储实际所需空间的两倍。也就是说,当我们存储与数组中存储数量一样多的项时,有一半的地址是空的。用机器学习模型替换标准哈希表中的哈希函数,研究人员发现浪费的空间显著减少了。

这并非特别令人意外的结果:通过在输入数据上进行训练,学习到的哈希函数可以在一些空间更均匀地分布数值,因为 ML 模型已经知道了数据的分布!这是一种强有力的、可以显著减少基于哈希的索引所需存储量的方式。相应的也有一些限制:ML 模型比我们上面所述的标准哈希函数计算得要慢一些,而且需要训练,但标准哈希函数不需要。

也许可以将基于哈希函数的 ML 用于关键在于有效的内存使用的情况,但是在这些情况中计算能力不再是瓶颈。谷歌和麻省理工的研究团队认为数据库就是一个很好的例子,因为索引在很昂贵的过程中每天重建一次;使用更多的计算时间以达到显著的节省内存的目的,这对许多数据库环境而言都是一场胜利。

但在此还是要有一个超展开,接下来进入布谷鸟哈希。

布谷鸟哈希(Cuckoo Hashing)

布谷鸟哈希发明于 2001 年,以布谷鸟类的名字命名。布谷鸟哈希是用链接技术和线性探测处理哈希冲突的替代(而非哈希函数的替代)。该策略一如其名,因为某些布谷鸟种类中,准备产卵的雌鸟会找到一个有主的巢,并将巢里的蛋挪出去,然后把自己的蛋下在里面。在布谷鸟哈希中,传入的数据会窃取旧数据的地址,就像布谷鸟会窃取别的鸟类的巢一样。

工作原理如下:当创建哈希表时将表分为两个空间,将这两个空间分别称为主地址空间和次地址空间。之后,分别初始化两个哈希函数,每一个函数分配给一个地址空间。这些哈希函数有可能非常相似——例如它们可能都属于「素数乘法」,其中每个哈希函数都会使用不同的素数。将这两个函数称为主哈希函数和次哈希函数

最初,插入布谷鸟哈希只会利用主哈希函数和主地址空间。当哈希冲突发生时,新数据会驱逐旧数据,然后用次哈希函数对旧数据进行哈希,并将其放入次地址空间中。

用于哈希冲突的布谷鸟哈希:黄色数据驱逐绿色数据,绿色数据在第二地址空间找到了新家(在次要空间顶部索引的淡绿色圆点)。

如果该次要地址已经被占用,则会再一次进行驱逐,在次要空间的数据会被发送回主要地址空间。因为有可能造成无限循环的驱逐,一般而言会设置一个每次插入驱逐的阈值;如果驱逐次数到达阈值,就会重建哈希表,重建过程可能包括为该表分配更多空间或是选择新的哈希函数

二次驱逐(Double eviction):传入的黄色圆点驱逐了绿色的,绿色的驱逐红色的,红色圆点在主地址空间找到了归宿(淡红色圆点)。

众所周知,这种策略在内存受限的情况下是非常有效的。所谓「二次幂(power of two choices)」就允许布谷鸟哈希即便在高利用率的情况下也有很稳定的表现(这在链接技术或线性探索中是不会出现的)。

Bails 和他在斯坦福的研究团队发现,只要进行适当优化,布谷鸟哈希可以非常快,即使在利用率高达 99% 的情况下,也能有不错的表现(http://dawn.cs.stanford.edu/2018/01/11/index-baselines/)。从本质上讲,布谷鸟哈希通过利用二次幂可以在无需昂贵训练阶段的情况下实现「机器学习」的高度利用。

索引的下一步是什么?

最终,每个人都对学习索引结构的潜力感到兴奋。随着更多 ML 工具的广泛应用,以及像 TPU 这样硬件的进步使得机器学习工作负载更快,索引可以从机器学习策略中受益良多。与此同时,像布谷鸟哈希(cuckoo hashing)这样漂亮的算法提醒我们,机器学习并不是万能的。融合了具有令人难以置信的力量的机器学习技术和像「二次幂」这样的旧理论的工作将继续推动计算机效率和能力的界限。

看起来,索引的基本原理不会一夜之间就被机器学习策略替代,但是自微调索引的想法是强大而令人兴奋的概念。随着我们越来越善用机器学习,以及在提升计算机处理机器学习工作负载的效率上做出的不断的努力,利用这些优势的新想法肯定会找到其主流用途。下一代 DynamoDB 或 Cassandra 可能也会很好地利用机器学习策略;日后 PostgreSQL 或 MySQL 的应用也会采用这样的策略。最终,这都取决于未来研究所能取得的成就,这些成就会继续建立在最先进的非线性策略和「AI 革命」的尖端技术上。

出于必要性的考虑,本文简化了许多细节。想要了解更多细节的读者请参阅:


原文链接:https://blog.bradfieldcs.com/an-introduction-to-hashing-in-the-era-of-machine-learning-6039394549b0

]]> 原文: https://ift.tt/2JTOmSK
RSS Feed

机器知心

IFTTT

如何利用散点图矩阵进行数据可视化

本文介绍了如何在 Python 中利用散点图矩阵(Pairs Plots)进行数据可视化。

如何快速构建强大的探索性数据分析可视化

当你得到一个很不错的干净数据集时,下一步就是探索性数据分析(Exploratory Data Analysis,EDA)。EDA 可以帮助发现数据想告诉我们什么,可用于寻找模式、关系或者异常来指导我们后续的分析。尽管在 EDA 中有很多种可以使用的方法,但是其中最有效的启动工具之一就是散点图矩阵(pairs plot,也叫做 scatterplot matrix)。散点图矩阵允许同时看到多个单独变量的分布和它们两两之间的关系。散点图矩阵是为后续分析识别趋势的很棒方法,幸运的是,用 Python 实现也是相当简单的。

本文,我们将介绍如何使用 Seaborn 可视化库(https://seaborn.pydata.org/)在 Python 中启动和运行散点图矩阵。我们将看到如何为快速检查数据而创建默认散点图矩阵,以及如何为了更深入的分析定制可视化方案。

代码地址:https://github.com/WillKoehrsen/Data-Analysis/blob/master/pairplots/Pair%20Plots.ipynb

我们将探索一个现实世界数据集,它由国家级的社会经济数据组成,这些数据都是 Gapminder 收集的。

Seaborn 中的散点图矩阵

我们需要先了解一下数据,以便开始后续的进展。我们可以 pandas 数据帧的形式加载这些社会经济数据,然后我们会看到下面这些列:

每一行代表一个国家一年的观察数据,列代表变量(这种格式的数据被称作整洁数据,tidy data),其中有两个类别列(国家和洲)和四个数值列。这些列简单易懂:life_exp 是出生时的预期寿命,以年为单位,popis 是人口数量,gdp_per_cap 是人均 GDP(以国际元)为单位。

seaborn 中的默认散点图矩阵仅仅画出数值列,尽管我们随后也会使用类别变量来着色。创建默认的散点图矩阵很简单:我们加载 seaborn 库,然后调用 pairplot 函数,向它传递我们的数据帧即可:

# Seaborn visualization library import seaborn as sns # Create the default pairplot sns.pairplot(df) 

我仍旧大为吃惊,一行简单的代码就能够让我们得到整个图。散点图矩阵会构建两种基本图形:直方图和散点图。位于对角线位置的直方图让我们看到了每一个变量的分布,而对角线上下的散点图则展示了变量两两之间的关系。例如,第二行最左侧的散点图展示了 life_exp 和 year 之间的关系。

默认的散点图矩阵通常能够提供有价值的洞见。我们可以看到 life-exp 和 gdp_per_cap 是正相关的,这表明较高收入国家的国民要活得更久一些(尽管这并不能表明二者存在因果关系)。令人欣慰的是,这也显示出世界范围内的人口寿命随着时间逐渐增长。我们可以从直方图中了解到人口和 GDP 变量呈严重右偏态分布。为了在以后的图中更好地展示这些变量,我们可以通过对列数值取对数来进行列变换:

# Take the log of population and gdp_per_capita df['log_pop'] = np.log10(df['pop']) df['log_gdp_per_cap'] = np.log10(df['gdp_per_cap'])  # Drop the non-transformed columns df = df.drop(columns = ['pop', 'gdp_per_cap'])

尽管这一张图在分析中就很有用,然而我们发现基于类别变量(例如洲)对图进行着色能够让它更有价值。这在 seaborn 中也是极其简单的。我们唯一要做的就是在调用 sns.pairplot 函数的时候使用关键词 hue。

sns.pairplot(df, hue = 'continent')

现在我们发现大洋洲和欧洲趋向于拥有最高的期望寿命,而亚洲拥有最多的人口量。注意我们对人口和 GDP 的对数变换使得这些变量呈正态分布,这提供了一个关于这些变量更加全面的表征。

这张图具有更多的信息,但是还存在一些问题:正如对角线上看到的一样,我认为堆叠的直方图可解释性不是很好。展示来自多类别的单变量分布的一个更好方法就是密度图(density plot)。我们可以通过调用函数将直方图变成密度图。向散点图输入一些关键词,改变点的透明度、大小和边缘颜色。

# Create a pair plot colored by continent with a density plot of the # diagonal and format the scatter plots. sns.pairplot(df, hue = 'continent', diag_kind = 'kde',              plot_kws = {'alpha': 0.6, 's': 80, 'edgecolor': 'k'},              size = 4)

对角线上的密度图使得对比洲之间的分布相对于堆叠的直方图更加容易。改变散点图的透明度增加了图的可读性,因为这些图存在相当多的重叠(ovelapping)。

现在是默认散点图矩阵的最后一个例子。为减少复杂度,我们仅画出 2000 年以后的数据。我们仍旧把洲着色,但是不画出「年」这一列。为了限制画出的列的数量,我们给函数传递了一个 vars 列表。为了更好的阐明这个图,我们还加上了标题。

# Plot colored by continent for years 2000-2007 sns.pairplot(df[df['year'] >= 2000],               vars = ['life_exp', 'log_pop', 'log_gdp_per_cap'],               hue = 'continent', diag_kind = 'kde',               plot_kws = {'alpha': 0.6, 's': 80, 'edgecolor': 'k'},              size = 4); # Title  plt.suptitle('Pair Plot of Socioeconomic Data for 2000-2007',               size = 28);


现在开始变得相当好看了!如果继续建模,我们可能会使用这些图中的信息指导我们的选择。例如,我们知道 log_gdp_per_cap 与 life_exp 是成正相关的,所以我们会创建一个线性模型来量化这种关系。本文主要集中在画图上面,如果希望更多地探索数据,我们可以使用 PairGrid 类定制散点图。

使用 PairGrid 的定制化

与 sns.pairplot 函数相反,sns.PairGrid 是一个类,这意味着它不能自动填充图。我们创建一个类实例,然后为网格的不同部分匹配特定的函数。为了给数据创建 PairGrid 实例,我们使用了以下的代码,这也限制了我们所展示的变量:

# Create an instance of the PairGrid class. grid = sns.PairGrid(data= df_log[df_log['year'] == 2007],                     vars = ['life_exp', 'log_pop',                      'log_gdp_per_cap'], size = 4)

如果我们要显示内容的话,则会得到一个空图,因为我们还没有为网格部分匹配任何函数。一个 PairGrid 需要填充三个网格部分:上三角、下三角和对角线。为了给这些部分匹配图,我们使用在这一部分使用 grid.map 方法。例如,为了给上三角匹配一个散点图,我们使用:

# Map a scatter plot to the upper triangle grid = grid.map_upper(plt.scatter, color = 'darkred')

map_upper 方法采用任意接受两个变量数组的函数(例如 plt.scatter),以及相关的关键词(例如 color)。map_lower 方法几乎与其相同,但是它填充的是网格的下三角。map_diag 与这两者稍有不同,因为它采用接受单个数组的函数(回想一下,对角线只显示单个变量)。一个例子是 plt.hist,我们使用它来填充对角线部分:

# Map a histogram to the diagonal grid = grid.map_diag(plt.hist, bins = 10, color = 'darkred',                       edgecolor = 'k') # Map a density plot to the lower triangle grid = grid.map_lower(sns.kdeplot, cmap = 'Reds')

在这个例子中,我们在下三角中使用二维核密度估计(即密度图)。将上面的内容合在一起,这段代码就会给出下图:

当我们想要创建自定义函数将不同的信息匹配到该图时,使用 PairGrid 类的实际好处就会显露出来。例如,我可能希望在散点图上增加两个变量的皮尔逊相关系数。为了做到这一点,我会写一个使用两个数组的函数,用它来计算统计数据,然后画在图上。下面的代码展示的就是如何做到这件事(来源:https://stackoverflow.com/questions/30942577/seaborn-correlation-coefficient-on-pairgrid)。

# Function to calculate correlation coefficient between two arrays def corr(x, y, **kwargs):      # Calculate the value     coef = np.corrcoef(x, y)[0][1]     # Make the label     label = r'$\rho$ = ' + str(round(coef, 2))      # Add the label to the plot     ax = plt.gca()     ax.annotate(label, xy = (0.2, 0.95), size = 20, xycoords = ax.transAxes)  # Create a pair grid instance grid = sns.PairGrid(data= df[df['year'] == 2007],                     vars = ['life_exp', 'log_pop', 'log_gdp_per_cap'], size = 4)  # Map the plots to the locations grid = grid.map_upper(plt.scatter, color = 'darkred') grid = grid.map_upper(corr) grid = grid.map_lower(sns.kdeplot, cmap = 'Reds') grid = grid.map_diag(plt.hist, bins = 10, edgecolor =  'k', color = 'darkred');

我们的新函数被匹配到上三角中了,因为我们需要两个数组来计算相关系数(还要注意到,我们可以将多个函数匹配到网格部分中)。这样就得到了下图:

现在相关系数已经出现在上面的散点图上了。这是一个比较直接的例子,但是我们可以使用 PairGrid 映射任何一个我们想要映射到图上的函数。我们可以按照需要增加相关的信息,这可以帮助我们解决如何写这个函数的问题!最后一个例子,下图对角线上展示了总结统计信息:

虽然还需要一些整理,但是它展示了一个通用的思想:除了使用库中现有的函数将数据映射到图上,例如 matplotlib,我们可以写自己的函数来展示自定义信息。

总结

散点图矩阵(pairs plots)是一款强大的工具,可以快速探索数据集中的分布和关系。为了让散点图矩阵可定制、可扩展,Seaborn 通过 Pair Grid 类提供了一个简单的默认方法。在数据分析项目中,大部分的价值通常不是来自于酷炫的机器学习,而是来自对数据的直接可视化。散点图矩阵给我们提供了对数据的概览,是数据分析项目很棒的起点。

原文链接:https://towardsdatascience.com/visualizing-data-with-pair-plots-in-python-f228cf529166

]]> 原文: https://ift.tt/2IkVXfV
RSS Feed

机器知心

IFTTT

从零开始,了解元学习

元学习是目前机器学习领域一个令人振奋的研究趋势,它解决的是学习如何学习的问题。

传统的机器学习研究模式是:获取特定任务的大型数据集,然后用这个数据集从头开始训练模型。很明显,这和人类利用以往经验,仅仅通过少量样本就迅速完成学习的情况相差甚远。

因为人类学习了「如何学习」。

在这篇文章中,我将从一个非常直观的元学习简介入手,从它最早的起源一直谈到如今的元学习研究现状。然后,我会从头开始,在 PyTorch 中实现一个元学习模型,同时会分享一些从该项目中学到的经验教训。

首先,什么是学习?

我们先来简单了解一下,当我们训练一个用来实现猫狗图像分类的简单神经网络时,到底发生了什么。假设我们现在有一张猫的图像,以及对应的表示「这是一只猫」的标签。为简洁起见,我做了一个简单的动画来展示训练的过程。

(图注)神经网络训练过程的单步。该网络用来实现猫狗图像分类。

反向传播是神经网络训练中很关键的一步。因为神经网络执行的计算和损失函数都是可微函数,因此我们能够求出网络中每一个参数所对应的梯度,进而减少神经网络当前给出的预测标签与真实/目标标签之间的差异(这个差异是用损失函数度量的)。在反向传播完成后,就可以使用优化器来计算模型的更新参数了。而这正是使神经网络的训练更像是一门「艺术」而不是科学的原因:因为有太多的优化器和优化设置(超参数)可供选择了。

我们把该「单个训练步」放在一张图中展示,如下所示:

现在,训练图像是一只🐈,表示图像是一只猫的标签是 🔺。最大的这些 △ 表示我们的神经网络,里面的 ■ 表示参数和梯度,标有 L 的四边形表示损失函数,标有 O 的四边形表示优化器。

完整的学习过程就是不断地重复这个优化步,直到神经网络中的参数收敛到一个不错的结果上。

上图表示神经网络的训练过程的三步,神经网络(用最大的 △ 表示)用于实现猫狗图像分类。

元学习

元学习的思想是学习「学习(训练)」过程。

元学习有好几种实现方法,不过本文谈到的两种「学习『学习』过程」的方法和上文介绍的方式很类似。

在我们的训练过程中,具体而言,可以学习到两点:

  • 神经网络的初始参数(图中的蓝色■);

  • 优化器的参数(粉色的★)。

我会介绍将这两点结合的情况,不过这里的每一点本身也非常有趣,而且可获得到简化、加速以及一些不错的理论结果。

现在,我们有两个部分需要训练:

  • 用「模型(M)」这个词来指代我们之前的神经网络,现在也可以将其理解为一个低级网络。有时,人们也会用「优化对象(optimizee)」或者「学习器(learner)」来称呼它。该模型的权重在图中用 ■ 表示。

  • 用「优化器(O)」或者「元学习器」来指代用于更新低级网络(即上述模型)权重的高级模型。优化器的权重在图中用 ★ 表示。

如何学习这些元参数?

事实上,我们可以将训练过程中的元损失的梯度反向传播到初始的模型权重和/或优化器的参数。

现在,我们有了两个嵌套的训练过程:优化器/元学习器上的元训练过程,其中(元)前向传输包含模型的多个训练步:我们之前见过的前馈、反向传播以及优化步骤。

现在我们来看看元训练的步骤:

元训练步(训练优化器 O)包含 3 个模型(M)的训练步。

在这里,元训练过程中的单个步骤是横向表示的。它包含模型训练过程中的两个步骤(在元前馈和元反向传播的方格中纵向表示),模型的训练过程和我们之前看到的训练过程完全一样。

可以看到,元前向传输的输入是在模型训练过程中依次使用的一列样本/标签(或一列批次)。

元训练步中的输入是一列样本(🐈、🐕)及其对应的标签(🔺、🔻)。

我们应该如何使用元损失来训练元学习器呢?在训练模型时,我们可以直接将模型的预测和目标标签做比较,得到误差值。

在训练元学习器时,我们可以用元损失来度量元学习器在目标任务——训练模型——上的表现。

一个可行的方法是在一些训练数据上计算模型的损失:损失越低,模型就越好。最后,我们可以计算出元损失,或者直接将模型训练过程中已经计算得到的损失结合在一起(例如,把它们直接加起来)。

我们还需要一个元优化器来更新优化器的权重,在这里,问题就变得很「meta」了:我们可以用另一个元学习器来优化当前的元学习器……不过最终,我们需要人为选择一个优化器,例如 SGD 或者 ADAM(不能像「turtles all the way down」一样(注:turtles all the way down 这里大概是说,「不能一个模型套一个模型,这样无限的套下去」)。

这里给出一些备注,它们对于我们现在要讨论的实现而言非常重要:

  • 二阶导数:将元损失通过模型的梯度进行反向传播时,需要计算导数的导数,也就是二阶导数(在最后一个动画中的元反向传播部分,这是用绿色的 ▲ 穿过绿色的 ■ 来表示的)。我们可以使用 TensorFlow 或 PyTorch 等现代框架来计算二阶导数,不过在实践中,我们通常不考虑二阶导数,而只是通过模型权重进行反向传播(元反向传播图中的黄色 ■),以降低复杂度。

  • 坐标共享:如今,深度学习模型中的参数数量非常多(在 NLP 任务中,很容易就有将近 3000 万 ~2亿个参数)。当前的 GPU 内存无法将这么多参数作为单独输入传输给优化器。我们经常采用的方法是「坐标共享」(coordinate sharing),这表示我们为一个参数设计一个优化器,然后将其复制到所有的参数上(具体而言,将它的权重沿着模型参数的输入维度进行共享)。在这个方法中,元学习器的参数数量和模型中的参数数量之间并没有函数关系。如果元学习器是一个记忆网络,如 RNN,我们依然可以令模型中的每个参数都具有单独的隐藏状态,以保留每个参数的单独变化情况。

在 PyTorch 中实现元学习

我们来尝试写些代码,看看真实情况如何吧。

现在我们有了一个模型,它包含一个我们想要进行训练的权重集合,我们将使用该集合解决这两项任务:

  • 在元前馈步骤中:我们使用这个模型计算(损失函数的)梯度,并作为优化器的输入来更新模型参数;

  • 在元反向传播步骤中:我们使用这个模型作为反向传播优化器参数梯度(从元损失中计算得到)的路径。

在 PyTorch 中完成这个任务最简单的方法是:使用两个一样的模块来表示模型,每个任务一个。我们把存储元前馈步骤中使用的模型梯度的模块称为前向模型(forward model),把元反向传播步骤中将参数存储为反向传播优化器梯度的连续路径的模块称为后向模型(backward model)。

两个模块之间会使用共享的 Tensor,以防止重复占用内存(Tensor 是内存中真正有意义的部分);但同时,也会保留各自的 Variable,以明确区分模型的梯度和元学习器的梯度。

PyTorch 中的一个简单元学习器类

在 PyTorch 中共享张量非常直接:只需要更新 Variable 类中的指针,让它们指向相同的 Tensor 就可以了。但如果模型已经是内存优化模型,例如 AWD-LSTM 或 AWD-QRNN 这类共享 Tensors(输入和输出嵌入)的算法时,我们就会遇到问难。这时,我们在更新两个模块中的模型参数时,需要很小心,以确保我们保留的指针是正确的。

在这里给出一个实现方法:设置一个简单的辅助程序来完成遍历参数的任务,并返回更新 Parameter 指针(而不只是 Tensor)所需的全部信息,并保持共享参数同步。

以下是一个实现函数:

def get_params(module, memo=None, pointers=None):     """ Returns an iterator over PyTorch module parameters that allows to update parameters         (and not only the data).     ! Side effect: update shared parameters to point to the first yield instance         (i.e. you can update shared parameters and keep them shared)     Yields:         (Module, string, Parameter): Tuple containing the parameter's module, name and pointer     """     if memo is None:         memo = set()         pointers = {}     for name, p in module._parameters.items():         if p not in memo:             memo.add(p)             pointers[p] = (module, name)             yield module, name, p         elif p is not None:             prev_module, prev_name = pointers[p]             module._parameters[name] = prev_module._parameters[prev_name] # update shared parameter pointer     for child_module in module.children():         for m, n, p in get_params(child_module, memo, pointers):             yield m, n, p 

通过这个函数,我们可以嵌入任何模型,并且很整洁地遍历元学习器的模型参数。

现在,我们来写一个简单的元学习器类。我们的优化器是一个模块:在前馈阶段,它可以将前向模型(及其梯度)和后向模型作为输入接受,并遍历它们的参数来更新后向模型中的参数,同时允许元梯度反向传播(通过更新 Parameter 指针,而不仅仅是 Tensor 指针)。

class MetaLearner(nn.Module):     """ Bare Meta-learner class         Should be added: intialization, hidden states, more control over everything     """     def __init__(self, model):         super(MetaLearner, self).__init__()         self.weights = Parameter(torch.Tensor(1, 2))      def forward(self, forward_model, backward_model):         """ Forward optimizer with a simple linear neural net         Inputs:             forward_model: PyTorch module with parameters gradient populated             backward_model: PyTorch module identical to forward_model (but without gradients)               updated at the Parameter level to keep track of the computation graph for meta-backward pass         """         f_model_iter = get_params(forward_model)         b_model_iter = get_params(backward_model)         for f_param_tuple, b_param_tuple in zip(f_model_iter, b_model_iter): # loop over parameters             # Prepare the inputs, we detach the inputs to avoid computing 2nd derivatives (re-pack in new Variable)             (module_f, name_f, param_f) = f_param_tuple             (module_b, name_b, param_b) = b_param_tuple             inputs = Variable(torch.stack([param_f.grad.data, param_f.data], dim=-1))             # Optimization step: compute new model parameters, here we apply a simple linear function             dW = F.linear(inputs, self.weights).squeeze()             param_b = param_b + dW             # Update backward_model (meta-gradients can flow) and forward_model (no need for meta-gradients).             module_b._parameters[name_b] = param_b             param_f.data = param_b.data 

这样一来,我们就可以像在第一部分中看到的那样来训练优化器了。以下是一个简单的要点示例,展示了前文描述的元训练过程:

def train(forward_model, backward_model, optimizer, meta_optimizer, train_data, meta_epochs):   """ Train a meta-learner   Inputs:     forward_model, backward_model: Two identical PyTorch modules (can have shared Tensors)     optimizer: a neural net to be used as optimizer (an instance of the MetaLearner class)     meta_optimizer: an optimizer for the optimizer neural net, e.g. ADAM     train_data: an iterator over an epoch of training data     meta_epochs: meta-training steps   To be added: intialization, early stopping, checkpointing, more control over everything   """   for meta_epoch in range(meta_epochs): # Meta-training loop (train the optimizer)     optimizer.zero_grad()     losses = []     for inputs, labels in train_data:   # Meta-forward pass (train the model)       forward_model.zero_grad()         # Forward pass       inputs = Variable(inputs)       labels = Variable(labels)       output = forward_model(inputs)       loss = loss_func(output, labels)  # Compute loss       losses.append(loss)       loss.backward()                   # Backward pass to add gradients to the forward_model       optimizer(forward_model,          # Optimizer step (update the models)                 backward_model)     meta_loss = sum(losses)             # Compute a simple meta-loss     meta_loss.backward()                # Meta-backward pass     meta_optimizer.step()               # Meta-optimizer step 

避免内存爆炸——隐藏状态记忆

有时,我们想要学习一个可在非常庞大的(可能有几千万个参数的)模型上运行的优化器;同时,我们还希望可以在大量步骤上实现元训练,以得到优质梯度;就像我们在论文《Meta-Learning a Dynamical Language Model》中所实现的那样。

在实践中,这意味着,我们想要在元前馈中包含一个很长的训练过程,以及很多时间步;同时我们还需要将每一步的参数(黄色■)和梯度(绿色■)保存在内存中,这些参数和梯度会在元反向传播中使用到。

我们如何在不让 GPU 内存爆炸的情况下做到这一点呢?

一个办法是,使用梯度检查点(gradient checkpointing)来用内存换取计算,这个方法也叫「隐藏状态记忆」(Hidden State Memorization)。在我们的案例中,梯度检查点表示,将我们连续计算的元前馈和元反向传播切分成片段。

来自 Open AI 的 Yaroslav Bulatov 有一篇很好的介绍梯度检查点的文章,如果你感兴趣,可以了解一下:

Fitting larger networks into memory(https://medium.com/@yaroslavvb/fitting-larger-networks-into-memory-583e3c758ff9)

这篇文章非常长,所以我没有给出一个完整的梯度检查点代码示例,建议大家使用已经很完善的 TSHadley 的 PyTorch 实现,以及当前还在开发的梯度检查点的 PyTorch 本地实现。

元学习中的其他方法

元学习中还有另外两个很有前景的研究方向,但本文没有时间来讨论了。在这里我给出一些提示,这样,当你知道了它们大致的原理后,就可以自己查阅相关资料了:

  • 循环神经网络:我们之前给出了神经网络的标准训练过程。还有一个方法:将连续的任务作为一个输入序列,然后建立一个循环模型,并用它提取、构建一个可用于新任务的序列表征。在这种方法中,对于某个带有记忆或注意力的循环神经网络,我们通常只使用一个训练过程。这个方法的效果也很不错,尤其是当你设计出适合任务的嵌入时。最近的这篇 SNAIL 论文是一个很好的例子:A Simple Neural Attentive Meta-Learner(https://openreview.net/forum?id=B1DmUzWAW)。

  • 强化学习:优化器在元前馈过程中完成的计算和循环神经网络的计算过程很类似:在输入序列(学习过程中模型的权重序列和梯度序列)上重复使用相同的参数。在真实场景下,这表示我们会遇到循环神经网络经常遇到的一个问题:一旦模型出错,就很难返回安全路径,因为我们并没有训练模型从训练误差中恢复的能力;同时,当遇到一个比元学习过程中使用的序列更长的序列时,模型难以泛化。为了解决这些问题,我们可以求助于强化学习方法,让模型学习一个和当前训练状态相关的动作策略。

自然语言处理中的元学习

元学习和用于自然语言处理(NLP)的神经网络模型(如循环神经网络)之间有一个非常有趣的相似之处。在上一段中,我们曾提到:

用于优化神经网络模型的元学习器的行为和循环神经网络类似。

和 RNN 类似,元学习器会提取一系列模型训练过程中的参数和梯度作为输入序列,并根据这个输入序列计算得到一个输出序列(更新后的模型参数序列)。

我们的论文《Meta-Learning a Dynamical Language Model》中详细论述了该相似性,并研究了将元学习器用于神经网络语言模型中,以实现中期记忆:经过学习,元学习器能够在标准 RNN(如 LSTM)的权重中,编码中期记忆(除了短期记忆在 LSTM 隐藏状态中的传统编码方式以外)。

我们的元学习语言模型由 3 层记忆层级组成,自下而上分别是:标准 LSTM、用于更新 LSTM 权重以存储中期记忆的元学习器,以及一个长期静态记忆。

我们发现,元学习语言模型可以通过训练来编码最近输入的记忆,就像一篇维基百科文章的开始部分对预测文章的结尾部分非常有帮助一样。

上图中的曲线展示了在给定一篇维基百科文章开始部分的情况下(A, …, H 是连续的维基百科文章),模型预测文章词汇的效果。单词颜色表示的意思相同:蓝色表示更好,红色表示更差。当模型在阅读一篇文章时,它从文章的开始部分进行学习,读到结尾部分的时候,它的预测效果也变得更好了(更多细节,请阅读我们的论文)。

以上是我对元学习的介绍,希望对大家有所帮助!

参考文献

1. ^ (https://ift.tt/2JX88Nb) As such, meta-learning can be seen as a generalization of「transfer learning」and is related to the techniques for fine-tuning model on a task as well as techniques for hyper-parameters optimization. There was an interesting workshop on meta-learning (https://ift.tt/2BE2zjc) at NIPS 2017 last December.

2. ^ (https://ift.tt/2JWPd5a) Of course in a real training we would be using a mini-batch of examples.

3. ^ (https://ift.tt/2IiaEjG) More precisely:「most of」these operations are differentiable.

4. ^ (https://ift.tt/2JUB9ce) Good blog posts introducing the relevant literature are the BAIR posts: Learning to learn (https://ift.tt/2tCp4zP) by Chelsea Finn and Learning to Optimize with Reinforcement Learning (https://ift.tt/2w4AP8f) by Ke Li.

5. ^ (https://ift.tt/2rnJtut) Good examples of learning the model initial parameters are Model-Agnostic Meta-Learning (https://ift.tt/2tCRqtI) of UC Berkeley and its recent developments (https://ift.tt/2IfP7br) as well as the Reptile algorithm (https://ift.tt/2Fxz7QR) of OpenAI. A good example of learning the optimizer』s parameters is the Learning to learn by gradient descent by gradient descent (https://ift.tt/1XqcyQy) paper of DeepMind. A paper combining the two is the work Optimization as a Model for Few-Shot Learning (https://ift.tt/2tCHuAF) by Sachin Ravi and Hugo Larochelle. An nice and very recent overview can be found in Learning Unsupervised Learning Rules (https://ift.tt/2IpgY5k).

6. ^ (https://ift.tt/2JVpY2V) Similarly to the way we back propagate through time in an unrolled recurrent network.

7. ^ (https://ift.tt/2IhUmr3) Initially described in DeepMind』s Learning to learn by gradient descent by gradient descent (https://ift.tt/1XqcyQy) paper.

8. ^ (https://ift.tt/2JSbcde) We are using coordinate-sharing in our meta-learner as mentioned earlier. In practice, it means we simply iterate over the model parameters and apply our optimizer broadcasted on each parameters (no need to flatten and gather parameters like in L-BFGS for instance).

9. ^ (https://ift.tt/2rnJxdH) There is a surprising under-statement of how important back-propagating over very long sequence can be to get good results. The recent paper An Analysis of Neural Language Modeling at Multiple Scales (https://ift.tt/2INDbv1) from Salesforce research is a good pointer in that direction.

10. ^ (https://ift.tt/2rmWUdW) Gradient checkpointing is described for example in Memory-Efficient Backpropagation Through Time (https://ift.tt/2prug8P) and the nice blog post (https://ift.tt/2EJ3zDA) of Yaroslav Bulatov.


原文链接:https://ift.tt/2uHd46b

]]> 原文: https://ift.tt/2IhUrLn
RSS Feed

机器知心

IFTTT

M2 模型杀回 Coding 和 Agent 领域,MiniMax 想要「普惠智能」-InfoQ每周精要No.900

「每周精要」 NO. 900 2025/11/01 头条 HEADLINE M2 模型杀回 Coding 和 Agent 领域,MiniMax 想要「普惠智能」 精选 SELECTED a16z 将 3000 万开发者标价 3 万亿 网友:几个初创公司 + 大模型就...