T 沙龙 2018 年 1 月上海第 10 次线下活动总结

技术编辑:冬瓜 & EyreFree

运营编辑:千叶知风

零. 前言

作为 2018 年的开箱活动,我们为大家带来了 AI 与算法专场。过去的一年里 AI 有多火热,想必大家都非常清楚,所以虽然是 Swift 沙龙,但我们仍然特意为大家准备了这么一场以 AI 为主题的分享活动。

下面是我们沙龙技术编辑童鞋对这三个 Topic 的一些总结:

一. 自适应学习 - 机器学习在开心词场中的应用

分享嘉宾 - 沪江网数据挖掘总监王新义

王老师现任沪江网数据挖掘总监,主要负责数据挖掘和机器学习技术在互联网教育中的应用,包括用户画像、推荐系统、知识图谱、自适应学习系统等。加入沪江前积累了 6 年以大数据和数据挖掘为核心的行业数据应用经验、流程及知识。熟悉 Hadoop 架构,对数据挖掘、机器学习/深度学习和大数据方向应用有较丰富的应用经验和技术积累。

王老师带来的一套“自适应学习”的方案。首先向我们展示了机器学习在沪江的业务各个层级中的运用情况,并用以下结构图进行总概。

自适应词汇量测试

为了照顾在场多数的前端(iOS)工程师,王老师具体介绍了一个场景,即 自适应词汇量测试。首先先分析了现在市面上的背单词软件的词汇量分析功能,其原理一般分为两种:

  • 静态试卷:特点:1. 每个学生所做的题目相同;2. 在会做的容易题和不会做的难题上浪费较多时间,影响用户体验。
  • 动态交互式测试:特点:1. 每个学生所做的题目不相同;2. 下一道题目根据历史做题反馈动态改变;3. 算法可以聚焦于算法不确定的题目请学生回答,而避免在肯定会做和肯定不会做的题目浪费太多时间。

显然,其 动态交互式测试 法师优于原始的静态试卷法,于是我们引入一种评估流程:算法 → 单词 → 用户作答。这里需要套用一种算法,根据用户的做题记录评估其能力后反馈出一个新的单词让用户来作答,然后根据作答情况,继续通过算法来评估能力,并持续这个循环。再一定量的循环后,我们并可以预估用户的词汇量。这个词汇量的关键是用户的 能力情况,而这个值也是与单词难度可比。

Item Response Theory - IRT 教学模型

为了评估用户的能力情况并使其量化,我们引入了 IRT 模型,来解决问题。IRT 是用来分析考试成绩或问卷调查数据的数学模型,这些模型的目标是来确定其潜在特征(Latent Trait)是否可以通过测试题反映出来,以及测试题和被测者之间的互动关系,下面给出其数学模型。wikipedia

\begin{aligned} P(\theta)=c+(1-c)\int_{-\infty}^{a(\theta-b)}\frac{e^{-t^2}}{2}\sqrt{2\pi}dt \end{aligned}

这个 3 参数Normal-ogive 模型称为 3PN。为了计算方便,可以用 3PL 模型来替代,运用在数值处理:

\begin{aligned} P(\theta)=c+\frac{1-c}{1+e^{-Da(\theta-b)}} \end{aligned}

其中,D 为常数 1.7。a 叫做区分度参数(Item Discrimination),a / 4 的值是该曲线拐点处的斜率,即斜率最大值。b 叫做难度参数(Item Difficulty),也是在图像上最陡处所对应的 \theta 值。当 b 增加时,曲线会向右移动,即使此时 \theta 保持不变,但是答题的正确率下降,说明题目的难度增加,反之降低。c 一般称作猜测参数(Guessing Parameter),当用户的能力值很低时,但他做对题目的概率 c ,即为猜测的能力。

当前场景下,我们套用 IRT 模型即可转化表达:

\begin{aligned} P(A|b, c)=\frac{1}{1+e^{-1.4 * (c - b)}} \end{aligned}

其中 A 为答对题目事件,B 和 C 分别表示单词难度和人的能力量化值。为了评估人的能力,我们引入极大似然估计

知识追踪 DKT

自适应学习是现在教育科技领域谈得比较多的一个概念,它的核心问题可以用一句话概括,即 通过个性化的学习路径规划,提高学生的学习效率。为了对学习序列建模,并评估学生各个时刻的能力,沪江引入了 Deep Knowledge Tracing(DKT)模型,这个模型是由 Stanford 大学的 Piech Chris 等人在 NIPS 2015发表的,其本质是一个 Seq2Seq 的 RNN 模型。

以下是该模型的结构图:

x 代表问题是否作对的编码,y 代表对于所有问题做对的概率,h 代表学生隐含的知识水平。再来看该模型的层次结构:

假设题库中总共有 4 道题,则输出层的数量为 4,对应各题回答正确的概率。另外我们已知这些题目的结果数目,所以输入层的节点数就可以用 x_{n}=y_{n}\times a_{n} 来求得。进而,我们将输入层全部链接到 RNN 的隐层,接着建立隐层到输出层的全连接,最后使用 Sigmoid 函数作为激活函数,就完成了基础的 DKT 模型。在 PPT 中展示了很多训练集的训练结果,这里是 连接

简单了解机器学习思想

王老师在讲述完机器学习在沪江业务中的运用后,又简明地介绍了什么是机器学习的思想。他在现场出了这么一个题目

求出满足这个式子的所有可能性:xxx * x - xxxx = 0。并且其中每个 x 代表一位数,切满足这八位数分别是 1 ~ 8。例如有这么一组满足的算式 453 * 6 - 2718 = 0

用传统的计算机算法思维来解决这个问题,我们直接使用 全排列 对每一种情况进行枚举即可实现,全排列算法复杂度为 O(n!)

# 全排列解法 1:使用多层嵌套遍历实现
for i in range(1,9):
    for j in range(1,9):
        for k in range(1,9):
            for m in range(1,9):
                ss=str(i)+str(j)+str(k)+str(m)+str((i*100+j*10+k)*m)
                if len(ss)==8 and len(set(list(ss)))==8 and  '0' not in ss and '9' not in ss :
                    print(ss[0:3]+'*'+ss[3]+'='+ss[4:])
                    
# 全排列解法 2:itertools 包中已经有 permutations 全排列方法
from itertools import permutations
print([''.join(x[:3])+'*'+''.join(x[3])+'='+''.join(x[4:]) for x in list(permutations('12345678', 8)) if int(''.join(x[:3]))*int(x[3])==int(''.join(x[4:]))])

for x in list(permutations('12345678',8)):
    if int(''.join(x[:3]))*int(x[3])==int(''.join(x[4:])):
        print(x)
        print(''.join(x[:3])+'*'+''.join(x[3])+'-'+''.join(x[4:])+'='+str(int(''.join(x[:3]))*int(x[3])-int(''.join(x[4:]))))
复制代码

如果我们从机器学习的角度来考虑整个问题,首先需要定义一个损失函数,用来评估模型的预测值和真实值的不一致程度,它是一个非负实值函数,如果损失函数越小,则模型的鲁棒性越好。这里我们的损失函数直接用 xxx * x - xxxx 的绝对值即可描述。

第二,梯度下降函数。由于没有一个可靠的下降方式,所以这里我们采用 SGD 随机梯度下降方式,在取值范围内做随机取值来实现梯度下降。

x=[1,2,3,4,5,6,7,8]
ss=train(x)
ss
x

#%%
import random
def lose(x):   # 损失函数
    return abs((int(x[0])*100+int(x[1])*10+int(x[2]))*int(x[3])-(int(x[4])*1000+int(x[5])*100+int(x[6])*10+int(x[7])))

def  swap(x,i,j):  #
    xx=x.copy()
    tmp=xx[i]
    xx[i]=xx[j]
    xx[j]=tmp
    return xx

def sgd(x):  # 梯度下降
    data_dict={}
    for i in range(0,7):
        for j in range(i+1,8):
            x_tmp=swap(x,i,j)
            lose_v_tmp=lose(x_tmp)
            data_dict[''.join(x_tmp)]=lose_v_tmp
    dd=sorted(data_dict.items(),key=lambda item:item[1])
    #print(dd[1][0])
    return list(dd[random.randint(0, 10)][0])
 
def train(x):
    while lose(x)>0 :
        x=sgd(x)
        print(x,'lose value:',lose(x))
    return x

x=['1','2','3','4','5','6','7','8']
ss=train(x)
print()
print(ss)
复制代码

在随机梯度中输出的损失值,会发现会有抖动,不会按照正确的方向持续下降,这个波动特点会使得优化的方向从当前具备极小值点跳跃到另一个更好的局部极小值点,对于非凸函数,最终收敛域一个较好的局部极值点,甚至是全局极值点。

在 SGD 中,由于波动,因此迭代次数很多,收敛速度慢。不过最终会和全量梯度下降算法一样,具有相同的收敛性,即凸函数收敛于全局极值点,非凸损失函数收敛于局部极值点。

为了优化其收敛性,王老师还提供了一种 差分进化算法(Differential Evolution) 方案,详细代码转至 Github 进行学习,这里不做赘述。


二. 初探地图类 App 后端那些事:空间索引算法

分享嘉宾 - 冰霜

冰霜,了解 iOS 开发,略懂 JavaScript, Go, C, C++。GitHub:@halfrost,微博:@halfrost

第二位是 iOS 圈内大家耳熟能详的冰霜,本名于德志,目前在饿了么从事 Go 后端开发。之前他一直致力于钻研 iOS 端开发技巧,编写了大量技术文章,涉猎 Go、JavaScript、iOS 开发等多个技术领域,名副其实的全栈工程师。这是他的第一次公开技术分享,他结合自身的工作实践,为我们全面细致地介绍了空间索引算法在地图类 App 中的使用。

冰霜分四段给大家进行了讲解:

1. 高效的多维空间点索引算法 — Geohash

1.1 Geohash 算法简介

Geohash 是一种地理编码,由 Gustavo Niemeyer 发明的。它是一种分级的数据结构,把空间划分为网格。Geohash 属于空间填充曲线中的 Z 阶曲线(Z-order curve)的实际应用。

何为 Z 阶曲线?

上图就是 Z 阶曲线。这个曲线比较简单,生成它也比较容易,只需要把每个 Z 首尾相连即可。

Z 阶曲线同样可以扩展到三维空间。只要 Z 形状足够小并且足够密,也能填满整个三维空间。

Geohash 有一个和 Z 阶曲线相关的性质,那就是一个点附近的地方(但不绝对) hash 字符串总是有公共前缀,并且公共前缀的长度越长,这两个点距离越近。

由于这个特性,Geohash 就常常被用来作为唯一标识符。用在数据库里面可用 Geohash 来表示一个点。Geohash 这个公共前缀的特性就可以用来快速的进行邻近点的搜索。越接近的点通常和目标点的 Geohash 字符串公共前缀越长(但是这不一定,也有特殊情况,下面举例会说明)

Geohash 也有几种编码形式,常见的有 2 种,base 32 和 base 36。

Decimal0123456789101112131415
Base 320123456789bcdefg
Decimal16171819202122232425262728293031
Base 32hjkmnpqrstuvwxyz

base 36 的版本对大小写敏感,用了 36 个字符,“23456789bBCdDFgGhHjJKlLMnNPqQrRtTVWX”。

Decimal0123456789101112131415161718
Base 3623456789bBCdDFgGhHj
Decimal1920212223242526272829303132333435
Base 36JKILMnNPqQrRtTVWX

1.2 Geohash 具体实现

接下来用 Go 实现一下 Geohash 算法:


package geohash

import (
	"bytes"
)

const (
	BASE32                = "0123456789bcdefghjkmnpqrstuvwxyz"
	MAX_LATITUDE  float64 = 90
	MIN_LATITUDE  float64 = -90
	MAX_LONGITUDE float64 = 180
	MIN_LONGITUDE float64 = -180
)

var (
	bits   = []int{16, 8, 4, 2, 1}
	base32 = []byte(BASE32)
)

type Box struct {
	MinLat, MaxLat float64 // 纬度
	MinLng, MaxLng float64 // 经度
}

func (this *Box) Width() float64 {
	return this.MaxLng - this.MinLng
}

func (this *Box) Height() float64 {
	return this.MaxLat - this.MinLat
}

// 输入值:纬度,经度,精度(geohash的长度)
// 返回geohash, 以及该点所在的区域
func Encode(latitude, longitude float64, precision int) (string, *Box) {
	var geohash bytes.Buffer
	var minLat, maxLat float64 = MIN_LATITUDE, MAX_LATITUDE
	var minLng, maxLng float64 = MIN_LONGITUDE, MAX_LONGITUDE
	var mid float64 = 0

	bit, ch, length, isEven := 0, 0, 0, true
	for length < precision {
		if isEven {
			if mid = (minLng + maxLng) / 2; mid < longitude {
				ch |= bits[bit]
				minLng = mid
			} else {
				maxLng = mid
			}
		} else {
			if mid = (minLat + maxLat) / 2; mid < latitude {
				ch |= bits[bit]
				minLat = mid
			} else {
				maxLat = mid
			}
		}

		isEven = !isEven
		if bit < 4 {
			bit++
		} else {
			geohash.WriteByte(base32[ch])
			length, bit, ch = length+1, 0, 0
		}
	}

	b := &Box{
		MinLat: minLat,
		MaxLat: maxLat,
		MinLng: minLng,
		MaxLng: maxLng,
	}

	return geohash.String(), b
}

复制代码

1.3 Geohash 的优缺点

Geohash 的优点很明显,它利用 Z 阶曲线进行编码。而 Z 阶曲线可以将二维或者多维空间里的所有点都转换成一维曲线。在数学上成为分形维。并且 Z 阶曲线还具有局部保序性。

Z 阶曲线通过交织点的坐标值的二进制表示来简单地计算多维度中的点的z值。一旦将数据被加到该排序中,任何一维数据结构,例如二叉搜索树,B树,跳跃表或(具有低有效位被截断)哈希表 都可以用来处理数据。通过 Z 阶曲线所得到的顺序可以等同地被描述为从四叉树的深度优先遍历得到的顺序。

这也是 Geohash 的另外一个优点,搜索查找邻近点比较快。

Geohash 的缺点之一也来自 Z 阶曲线。

Z 阶曲线有一个比较严重的问题,虽然有局部保序性,但是它也有突变性。在每个 Z 字母的拐角,都有可能出现顺序的突变。

看上图中标注出来的蓝色的点点。每两个点虽然是相邻的,但是距离相隔很远。看右下角的图,两个数值邻近红色的点两者距离几乎达到了整个正方形的边长。两个数值邻近绿色的点也达到了正方形的一半的长度。

Geohash 的另外一个缺点是,如果选择不好合适的网格大小,判断邻近点可能会比较麻烦。

看上图,如果选择 Geohash 字符串为6的话,就是蓝色的大格子。红星是美罗城,紫色的圆点是搜索出来的目标点。如果用 Geohash 算法查询的话,距离比较近的可能是 wtw37p,wtw37r,wtw37w,wtw37m。但是其实距离最近的点就在 wtw37q。如果选择这么大的网格,就需要再查找周围的8个格子。

如果选择 Geohash 字符串为7的话,那变成黄色的小格子。这样距离红星星最近的点就只有一个了。就是 wtw37qw。

如果网格大小,精度选择的不好,那么查询最近点还需要再次查询周围 8 个点。

2. 空间填充曲线

在数学分析中,有这样一个难题:能否用一条无限长的线,穿过任意维度空间里面的所有点?经过人们的不断探索,除了上面的 Z 阶曲线外,还发现下列曲线都能实现这一需求:

2.1 Peano 曲线

2.2 龙曲线

2.3 高斯帕曲线

2.4 Koch 曲线

2.5 摩尔定律曲线

2.6 谢尔宾斯基曲线 & 奥斯古德曲线

2.7 希尔伯特曲线

3. 多维空间点索引算法之二 — Google S²

在介绍这个重量级算法之前,先解释一些这个算法的名字由来。S2其实是来自几何数学中的一个数学符号 S²,它表示的是单位球。S2 这个库其实是被设计用来解决球面上各种几何问题的。值得提的一点是,除去 golang 官方 repo 里面的 geo/s2 完成度目前只有40%,其他语言,Java,C++,Python 的 S2 实现都完成100%了。本此分享的讲解以 Go 的这个版本为主。

接下来就看看怎么用 S2 来解决多维空间点索引的问题的。

3.1 球面坐标转换

按照之前我们处理多维空间的思路,先考虑如何降维,再考虑如何分形。

众所周知,地球是近似一个球体。球体是一个三维的,如何把三维降成一维呢?大致原理如下图所示:

再进一步,我们可以和球面上的经纬度联系起来,于是地球上任意的一个经纬度的点,就可以转换成 f(x,y,z):

3.2 球面变平面

接下来一步 S2 把球面碾成平面。怎么做的呢?首先在地球外面套了一个外切的正方体,如下图:

从球心向外切正方体6个面分别投影。S2 是把球面上所有的点都投影到外切正方体的6个面上。这里简单的画了一个投影图,上图左边的是投影到正方体一个面的示意图,实际上影响到的球面是右边那张图:

投影到正方体以后,我们就可以把这个正方体展开了:

3.3 球面矩形投影修正

上一步我们把球面上的球面矩形投影到正方形的某个面上,形成的形状类似于矩形,但是由于球面上角度的不同,最终会导致即使是投影到同一个面上,每个矩形的面积也不大相同:

上图就表示出了球面上个一个球面矩形投影到正方形一个面上的情况。

经过实际计算发现,最大的面积和最小的面积相差5.2倍。见上图左边。相同的弧度区间,在不同的纬度上投影到正方形上的面积不同。

现在就需要修正各个投影出来形状的面积。如何选取合适的映射修正函数就成了关键。目标是能达到上图右边的样子,让各个矩形的面积尽量相同。

这块转换的代码在 C++ 的版本里面才有详细的解释,在 Go 的版本里面只一笔带过了。害笔者懵逼了好久。

面积比率边比率对角线比率ToPointRawToPointFromPoint
线性变换5.2002.1172.9590.0200.0870.085
tan()变换1.4141.4141.7040.2370.2990.258
二次变换2.0821.8021.9320.0330.0960.108

线性变换是最快的变换,但是变换比最小。tan() 变换可以使每个投影以后的矩形的面积更加一致,最大和最小的矩形比例仅仅只差0.414。可以说非常接近了。但是 tan() 函数的调用时间非常长。如果把所有点都按照这种方式计算的话,性能将会降低3倍。

最后谷歌选择的是二次变换,这是一个近似切线的投影曲线。它的计算速度远远快于 tan() ,大概是 tan() 计算的3倍速度。生成的投影以后的矩形大小也类似。不过最大的矩形和最小的矩形相比依旧有2.082的比率。

经过修正变换以后,u,v都变换成了s,t。值域也发生了变化。u,v的值域是[-1,1],变换以后,是s,t的值域是[0,1]。

至此,小结一下,球面上的点S(lat,lng) -> f(x,y,z) -> g(face,u,v) -> h(face,s,t)。目前总共转换了4步,球面经纬度坐标转换成球面xyz坐标,再转换成外切正方体投影面上的坐标,最后变换成修正后的坐标。

3.4 点与坐标轴点相互转换

在 S2 算法中,默认划分 Cell 的等级是30,也就是说把一个正方形划分为 2^30 * 2^30个小的正方形。

那么上一步的s,t映射到这个正方形上面来,对应该如何转换呢?

s,t的值域是[0,1],现在值域要扩大到[0,2^30^-1]。

3.5 坐标轴点与希尔伯特曲线 Cell ID 相互转换

最后一步,如何把 i,j 和希尔伯特曲线上的点关联起来呢?在变换之前,先来解释一下定义的一些变量。

posToIJ 代表的是一个矩阵,里面记录了一些单元希尔伯特曲线的位置信息。

把 posToIJ 数组里面的信息用图表示出来,如下图:

同理,把 ijToPos 数组里面的信息用图表示出来,如下图:

posToOrientation 数组里面装了4个数字,分别是1,0,0,3。 lookupIJ 和 lookupPos 分别是两个容量为1024的数组。这里面分别对应的就是希尔伯特曲线 ID 转换成坐标轴 IJ 的转换表,和坐标轴 IJ 转换成希尔伯特曲线 ID 的转换表。

这里的话由于设计比较复杂的数学计算,就不进一步展开了,有兴趣的同学可以参考冰霜大佬的 高效的多维空间点索引算法 — Geohash 和 Google S2 一文。

3.6 S2 Cell ID 数据结构

最后需要来谈谈 S2 Cell ID 数据结构,这个数据结构直接关系到不同 Level 对应精度的问题。

由上一章我们知道,由于投影的原因,所以导致投影之后的面积依旧有大小差别。这里推算的公式比较复杂,就不证明了,具体的可以看文档。这就是最大最小面积和平均面积的倍数关系:

levelmin areamax areaaverage areaunitsRandom cell 1 (UK) min edge lengthRandom cell 1 (UK) max edge lengthRandom cell 2 (US) min edge lengthRandom cell 2 (US) max edge lengthNumber of cells
0085011012.1985011012.1985011012.19km27842 km7842 km7842 km7842 km6
0121252753.0521252753.0521252753.05km23921 km5004 km3921 km5004 km24
024919708.236026521.165313188.26km21825 km2489 km1825 km2489 km96
031055377.481646455.501328297.07km2840 km1167 km1130 km1310 km384
04231564.06413918.15332074.27km2432 km609 km579 km636 km1536
0553798.67104297.9183018.57km2210 km298 km287 km315 km6K
0612948.8126113.3020754.64km2108 km151 km143 km156 km24K
073175.446529.095188.66km254 km76 km72 km78 km98K
08786.201632.451297.17km227 km38 km36 km39 km393K
09195.59408.12324.29km214 km19 km18 km20 km1573K
1048.78102.0381.07km27 km9 km9 km10 km6M
1112.1825.5120.27km23 km5 km4 km5 km25M
123.046.385.07km21699 m2 km2 km2 km100M
130.761.591.27km2850 m1185 m1123 m1225 m402M
140.190.400.32km2425 m593 m562 m613 m1610M
1547520.3099638.9379172.67m2212 m296 m281 m306 m6B
1611880.0824909.7319793.17m2106 m148 m140 m153 m25B
172970.026227.434948.29m253 m74 m70 m77 m103B
18742.501556.861237.07m227 m37 m35 m38 m412B
19185.63389.21309.27m213 m19 m18 m19 m1649B
2046.4197.3077.32m27 m9 m9 m10 m7T
2111.6024.3319.33m23 m5 m4 m5 m26T
222.906.084.83m2166 cm2 m2 m2 m105T
230.731.521.21m283 cm116 cm110 cm120 cm422T
240.180.380.30m241 cm58 cm55 cm60 cm1689T
25453.19950.23755.05cm221 cm29 cm27 cm30 cm7e15
26113.30237.56188.76cm210 cm14 cm14 cm15 cm27e15
2728.3259.3947.19cm25 cm7 cm7 cm7 cm108e15
287.0814.8511.80cm22 cm4 cm3 cm4 cm432e15
291.773.712.95cm212 mm18 mm17 mm18 mm1729e15
300.440.930.74cm26 mm9 mm8 mm9 mm7e18

3.7 S2 与 Geohash 对比

Geohash 有12级,从5000km 到 3.7cm。中间每一级的变化比较大。有时候可能选择上一级会大很多,选择下一级又会小一些。比如选择字符串长度为4,它对应的 cell 宽度是39.1km,需求可能是50km,那么选择字符串长度为5,对应的 cell 宽度就变成了156km,瞬间又大了3倍了。这种情况选择多长的 Geohash 字符串就比较难选。选择不好,每次判断可能就还需要取出周围的8个格子再次进行判断。Geohash 需要 12 bytes 存储。

S2 有30级,从 0.7cm² 到 85,000,000km² 。中间每一级的变化都比较平缓,接近于4次方的曲线。所以选择精度不会出现 Geohash 选择困难的问题。S2 的存储只需要一个 uint64 即可存下。

S2 库里面不仅仅有地理编码,还有其他很多几何计算相关的库。地理编码只是其中的一小部分。本文没有介绍到的 S2 的实现还有很多很多,各种向量计算,面积计算,多边形覆盖,距离问题,球面球体上的问题,它都有实现。

S2 还能解决多边形覆盖的问题。比如给定一个城市,求一个多边形刚刚好覆盖住这个城市。

用相同的 Cell 也可以达到相同的目的,上图就是用相同 Level 的 Cell 覆盖了整个圣保罗城市。

这些都是 Geohash 做不到的。多边形覆盖利用的是近似的算法,虽然不是严格意义上的最优解,但是实践中效果特别好。

3.8 S2 的应用

S2 主要能用在以下 8 个地方:

  • 涉及到角度,间隔,纬度经度点,单位矢量等的表示,以及对这些类型的各种操作。
  • 单位球体上的几何形状,如球冠(“圆盘”),纬度 - 经度矩形,折线和多边形。
  • 支持点,折线和多边形的任意集合的强大的构造操作(例如联合)和布尔谓词(例如,包含)。
  • 对点,折线和多边形的集合进行快速的内存索引。
  • 针对测量距离和查找附近物体的算法。
  • 用于捕捉和简化几何的稳健算法(该算法具有精度和拓扑保证)。
  • 用于测试几何对象之间关系的有效且精确的数学谓词的集合。
  • 支持空间索引,包括将区域近似为离散“S2单元”的集合。此功能可以轻松构建大型分布式空间索引。
  • 最后一点空间索引相信在工业生产中使用的非常广泛。

4. 实际应用

最后,关于索引算法的应用场景,给出了一系列展示图片:

另关于本次分享的内容,可参考冰霜 GitHub 的空间搜索系列博文:github.com/halfrost/Ha…


三. 机器学习介绍

分享嘉宾 - 梅元刚

第三位分享嘉宾是来自金山云的梅元刚。中科院硕士,擅长图像处理和机器学习,三星 Gear 360 全景视频作者,金山云美颜作者,金山云 AI 画质增强核心作者。由于之前种种原因接触了 Swift 并接触了 iOS 开发,本场沙龙,他以移动开发者的角度,带着我们简单了解了机器学习。

梅老师主要分享了一些他在机器学习过程中的积累,对入门非常有帮助。

机器学习的基本分类

监督学习就是标明一些数据是对的,另一些数据是错的,然后让程序预测,新的数据是对的还是错的。所以说,有监督学习,必须是有标签的。

无监督学习,顾名思义,就是不对数据进行标明,让机器自动去判断,哪些数据比较像,归到一类等等。

强化学习或者叫做加强学习,是指什么呢?在前边的监督学习中,机器每次做出预测,都会知道结果对不对,但是在这里却不行,每次做出预测不会得到对或者不对的结果,只会收到看似没有半毛钱关系的反馈。所以强化学习不是依赖数据的标签进行学习,而是依赖自己积累的反馈。强化学习适合学习交互过程,比如下围棋(AlphaGo的成功就是强化学习的力量)。

卷积神经网络

如下图所示,展示了一个33的卷积核在55的图像上做卷积的过程。每个卷积都是一种特征提取方式,就像一个筛子,将图像中符合条件(激活值越大越符合条件)的部分筛选出来。不同的卷积核能够提取到图像中的不同特征,这里有 在线 demo

深度学习图像应用

我在此不是要说明作者是怎么做到这些的,而是让大家看一些令人难以置信的结果。

艺术类

TYLE2PAINTS:强大的为线稿上色的 AI

推荐理由:新一代的强大线稿上色 AI,可根据用户上传的自定义色彩给线稿进行上色。项目提供了在线使用网站,十分方便使用。

CycleGAN:生成对抗网络图像处理工具

这个工具功能十分强大,不仅可将绘画作品“还原”成照片(可理解为是一个 “反滤镜”),还能将夏天转换成冬天,或将普通的马转化成斑马。

make ASCII Art by Deep Learning

识别类

maskrcnn

使用迁移学习做动物脸部识别

牛其实不愿意看到人类的,他们会视人类为捕食者,因此养牛场的工作人员会给牛群带来紧张情绪。那么我们就把农场的管理交给人工智能吧。

人工智能通过农场的摄像装置获得牛脸以及身体状况的照片,进而通过深度学习对牛的情绪和健康状况进行分析,然后帮助农场主判断出那些牛生病了,生了什么病,那些牛没有吃饱,甚至那些牛到了发情期。除了摄像装置对牛进行“牛脸”识别,还可以配合上可穿戴的智能设备,这会让农场主更好的管理农场。这些数据上传到云服务器上,用自己开发的算法通过机器学习让这些海量的原始数据变成直观的图表和信息发送到客户那里。这些信息包括奶牛的健康分析、发情期探测和预测、喂养状况、位置服务等。

图像增强类

DeblurGAN

图像超分辨率

RAISR 这项技术能利用机器学习,把低分辨率图片转为高分辨率图片。它的效果能达到甚至超过现在的超分辨率解决方案,同时速度提升大约 10 至 100 倍,且能够在普通的移动设备上运行。

Face2Face:扮演特朗普

斯坦福大学的一个小组做了一款名为 Face2Face 的应用,这套系统能够利用人脸捕捉,让你在视频里实时扮演另一个人,简单来讲,就是可以把你的面部表情实时移植到视频里正在发表演讲的美国总统身上。

增强学习

MIT最新课程 ——九小时速成深度学习&自动驾驶汽车

a minitaur duck

OpenAI 所训练的一款人工智能算法在著名的电子竞技游戏 Dota2 国际邀请赛 The International 中,参与了 1V1 比赛环节,并压倒性的击败了顶级电子竞技选手 Dendi。

反复攻破和修补自己的防火墙

Google 大脑的研究团队创建了两个深度学习网络用于安全工具开发,他们让其中一个不断创造自己的加密算法,然后让另一个网络去尽力攻破它。在两套系统的反复缠斗后,第一个系统已经能生成非常优秀的安全加密算法。

AI可以自己编程

DeepCoder 使用了一种叫做程序合成(Program Synthesis)的技术,其运行原理与程序员所做的事情差不多,就是从存在的软件中获取已知的代码段,并将它们拼接到一起执行新的程序。只要赋予 DeepCoder 中每个片段对应的输入和输出,程序就可以“学习”到哪些代码是我们所需要的。

iOS实现

在 WWDC 2017 上,苹果首次公布了机器学习方面的动作。iOS 系统早已支持 Machine Learning 和 Computer Vision,但这次苹果提供了更合理,容易上手的API,让那些对基础理论知识一窍不通的门外汉也能玩转高大上的前沿科技。

此后,苹果公司宣布了可以在设备上应用机器学习的两种新技术:Core ML 和 MPS graph API(Core ML 构建于 MPS 之上,MPS 更底层)。

注意:运行 demo 需要使用 Xcode 9 和运行 iOS 11 的设备

iOS10 的 MPS 框架已实现支持 GPU 的快速 CNN 计算

支持在 iOS 设备上运行卷积神经网络(CNN)的 API 已经加入到了 iOS 10 的 MPS (MetalPerformanceShaders)框架中。我们现在可以利用 GPU 实现快速的 CNN 计算,换句话说,最先进的深度学习技术已经可以在单机上离线独立运行。

需要传入 MPSCNNConvolutionDataSource 对象。该对象负责加载权重。

我们开始写数据输入类。由于我们的层都非常相似,所以我们 DataSource 将为所有层使用相同的类 - 但是每个层都有自己的实例。代码如下所示:

class DataSourceCNN: NSObject, MPSCNNConvolutionDataSource {
    let wName: String
    let bName: String
    let kernelWidth: Int
    let kernelHeight: Int
    let inputFeatureChannels: Int
    let outputFeatureChannels: Int
    let useLeaky: Bool
    let stride:Int
    let paramA:Float
    
    var wData: Data?
    var bData: Data?
    
    init(_ wName: String, _ bName: String, _ kernelWidth: Int, _ kernelHeight: Int,
         _ inputFeatureChannels: Int, _ outputFeatureChannels: Int,
         useLeaky: Bool = true, stride: Int = 1,paramA:Float=0.0) {
        self.wName = wName
        self.bName = bName
        self.kernelWidth = kernelWidth
        self.kernelHeight = kernelHeight
        self.inputFeatureChannels = inputFeatureChannels
        self.outputFeatureChannels = outputFeatureChannels
        self.useLeaky = useLeaky
        self.stride=stride
        self.paramA=paramA
    }
    
    func descriptor() -> MPSCNNConvolutionDescriptor {
        let desc = MPSCNNConvolutionDescriptor(kernelWidth: kernelWidth,
                                               kernelHeight: kernelHeight,
                                               inputFeatureChannels: inputFeatureChannels,
                                               outputFeatureChannels: outputFeatureChannels)
        if useLeaky {
            
            desc.setNeuronType(.reLU, parameterA: 0.0, parameterB: 0.0)
            
        } else {
            desc.setNeuronType(.none, parameterA: 0.0, parameterB: 0.0)
        }
        
        desc.strideInPixelsX=stride
        desc.strideInPixelsY=stride
        
        
        return desc
    }
    
    func weights() -> UnsafeMutableRawPointer {
        let ptr=UnsafeMutableRawPointer(mutating: (wData! as NSData).bytes)
        return ptr
    }
    
    func biasTerms() -> UnsafeMutablePointer<Float>? {
        
        return nil
    }
    
    func load() -> Bool {
        if let url = Bundle.main.url(forResource: name, withExtension: "bin") {
          do {
            data = try Data(contentsOf: url)
            return true
          } catch {
            print("Error: could not load \(url): \(error)")
          }
        }
        return false
    }
        
    func purge() {
        wData = nil
        bData=nil
    }
    
    func label() -> String? {
        return wName
    }
    
    func dataType() -> MPSDataType {
        return .float32
    }
}  
复制代码

MPSCNNConvolutionDataSource 需要具有 load()purge() 函数。在这里,我们只需将上一步导出的二进制文件(例如,conv1.bin)加载到Data对象中即可。

要获取此层的权重,该 weights() 函数将返回一个指向此 Data 对象的第一个元素的指针。假设我们的层没有偏置,所以 biasTerms() 可以返回 nil.

现在,数据源被整理出来,我们可以开始构建图:

let inputImage = MPSNNImageNode(handle: nil)

let scale = MPSNNLanczosScaleNode(source: inputImage,
                 outputSize: MTLSize(width: 416, height: 416, depth: 3))

let conv1 = MPSCNNConvolutionNode(source: scale.resultImage,
                                  weights: DataSource("conv1", 3, 3, 3, 16))

let pool1 = MPSCNNPoolingMaxNode(source: conv1.resultImage, filterSize: 2)

let conv2 = MPSCNNConvolutionNode(source: pool1.resultImage,
                                  weights: DataSource("conv2", 3, 3, 16, 32))

let pool2 = MPSCNNPoolingMaxNode(source: conv2.resultImage, filterSize: 2)

// ... and so on ...

guard let graph = MPSNNGraph(device: device, 
                             resultImage: conv9.resultImage) else {
  fatalError("Error: could not initialize graph")
}
复制代码

我们首先为输入图像声明一个节点,并将一个将该输入图像缩放到 416×416。接下来每个层都使用 source 参数连接到前一个层。所以 scale 节点连接到 inputImageconv1 被连接到 scale.resultImage,等等。graph 本身是一个 MPSNNGraph 对象,并连接到网络中最后一层的输出 conv9

总结

Core ML 大大降低了开发者在苹果设备上使用机器学习技术的门槛。苹果制定了自己的模型格式,这样当前主流机器学习模型通过转换工具都能运用到 APP 当中。如果你是移动开发者,又看好机器学习,为什么不试一试呢?如果说前几年是智能机时代,有可能未来几年就是智能应用时代了。

深度学习局限性

如果问题不能纯粹地转化为一个映射问题时,深度学习的执行力就存在局限性。(由于深度学习更专注于一些基于数据驱动的映射问题,而事实上无论在视觉领域还是在人工智能领域,很多问题并不是映射问题。)

比如,深度学习解决问题时,是通过构建神经网络架构实现的,这一过程过度依赖样本。同时,我们又不清楚深度学习具体如何解决问题、如何解释解决问题的过程。因此,深度学习有一些和统计学习方法相同的顽疾:容易被一些方法陷害,比如灌入脏数据。从而使得深度学习做得不好。

例如,做一个巡逻机器人放在小区里。开始时,业主和物业觉得这个巡逻机器人有趣,但后来觉得它没用,不能解决他们的问题。他们就想这东西能干什么,然后提出了一个痛点需求:小区里面猫屎、狗屎,如果没有被及时清理,影响环境且容易被踩到。机器人能不能通过巡逻,找到狗屎,反馈给物业,让保洁快速清理掉?

以这样一个问题为例,如果我们用传统的非深度学习的方法去做的话,可能要搜集几百、几千张狗屎的照片,然后人工地去搜集它的颜色、形状、以及纹理特征,然后去调节分类器。我们过去做人脸检测、行人检测,车辆检测都是这么做的,可能需要十几年的时间才调出来一个还不错的模型。但是深度学习模型一两个月就可以解决这个问题:我们先用平台去收集上万张狗屎的照片,也许我们再花上一两个星期的时间,调调模型然后交给机器去训练就好了,大概一两个月也许就可以部署这样一个系统。

对于大数据来说这够了,但和人相比还是不够。如果一个小孩踩了一次狗屎,基本上就不会踩第二次,也就意味着他基本上用一个样本,几秒钟的时间就学完了狗屎检测的问题。

深度学习缺乏常识

比如说杯子不会悬在空中,它一定是在桌面上;狗屎一般不会在墙上,因为狗一般不会跑到墙上去拉屎,这些“常识”使得人在不需要很多样本的情况下,很精准的解决问题。比如行人检测,人不会去树上做检测,那很奇怪。因为人知道,行人一般不会在树上。比如汽车检测,人不会在天上做汽车检测。因为人知道,天上不会飞汽车。人其实都有“常识”,它使得人并不需要很多样本,就可以做很准确的判断。

深度学习需要大量数据

自动驾驶其实大家知道,真正测试一个自动驾驶系统的行为,不是靠这些 normal traffic(常规交通),而是靠什么呢?靠很多边界的case,靠很多不正常的 traffic data,比如小孩子突然走到马路,你可能一辈子很难碰到几个,但是你就是要拿这些情况去测试。但是你不可能用真实的数据,你不可能让小孩真的去横闯马路,然后去测试你这个自动驾驶的系统,所以一定是用仿真的系统,去产生的很多的这种配制,然后去训练去测试。这个是自动驾驶必须要走的路,那这个里面实际上就是用大量的数据,但是这个数据是举一反三虚拟想象出来的。所以未来的话,有可能想象的数据会填补我们对数据的缺失所带来的掣肘,然后使你实际上effectively(有效的)是用小数据,但是你从 generate 很多大量的数据,使得你这个系统能够不断的进化,去变得越来越聪明。

强化学习反馈时延很久

Alphago zero,你会发现它其实一定意义上来讲没有数据,因为它是完全从零的状态开始博弈,它完全是左右博弈,去虚拟下无数盘棋。整个这个程序是用的深度学习加强化学习,在不断的从虚拟的对决里面去学习很多的经验,最后达到一个很强大的能力,会接近棋盘真理。 zero data learning ,它是没有用任何人类历史的棋盘对决的数据,但是它又是大数据,为什么呢?因为它用很多虚拟的数据来学习,所以就是说,你就发现想象力使你在 zero data learningdata learning 之间好像有个虫洞效应。实际上它们两个之间距离是非常短的,不是我们想象的差别那么大。


四. 其他资源

当然我们也提供了 Slides 和录屏,感兴趣的同学可以通过下面的链接获取:

王新义 - 自适应学习:机器学习在开心词场中应用:录屏 & Slides

冰霜 - 初探地图类 App 后端那些事:空间索引算法:录屏 & Slides

梅元刚 - iOS AI:录屏 & Slides

五. 关于我们

T 沙龙是一个非盈利的线下沙龙组织,我们会定期举办 iOS 开发相关的线下沙龙活动,目的是促进 iOS 开发技术人员的线下面对面交流。沙龙通过采用闭门邀请制的方式,使参加者的技术水平和学习兴趣尽可能地接近,促进大家进行真正的交流。

六. 特别感谢

最后感谢本次沙龙的场地提供方 皑沐(上海)文化传媒

转载于:https://juejin.im/post/5a67e1996fb9a01c927eda39

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值