0%

upload successful

本文将尽量解释清楚 JavaScript 中最基础的部分之一:执行上下文(execution context)。如果你经常使用 JS 框架,那理解 this 更是锦上添花。但如果你想更加认真地对待编程的话,理解上下文无疑是非常重要的。

我们可以像平常说话一样来使用 this。例如:我会说“我妈很不爽,这(this)太糟糕了”,而不会说“我妈很不爽,我妈很不爽这件事太糟糕了”。理解了 this 的上下文,才会理解我们为什么觉得很糟糕。

现在试着把这个例子与编程语言联系起来。在 Javascript 中,我们将 this 作为一个快捷方式,一个引用。它指向其所在上下文的某个对象或变量。

现在这么说可能会让人不解,不过很快你就能理解它们了。

全局上下文

如果你和某人聊天,在刚开始对话、没有做介绍、没有任何上下文时,他对你说:“这(this)太糟糕了”,你会怎么想?大多数情况人们会试图将“这(this)”与周围的事物、最近发生的事情联系起来。

对于浏览器来说也是如此。成千上万的开发者在没有上下文的情况下使用了 this。我们可怜的浏览器只能将 this 指向一个全局对象(大多数情况下是 window)。

1
2
3
4
5
var a = 15;
console.log(this.a);
// => 15
console.log(window.a);
// => 15

[以上代码需在浏览器中执行]

函数外部的任何地方都为全局上下文,this 始终指向全局上下文(window 对象)。

函数上下文

以真实世界来类比,函数上下文可以看成句子的上下文。“我妈很不爽,这(this)很不妙。”我们都知道这句话中的 this 是什么意思。其它句子中同样可以使用 this,但是由于其处于所处上下文不同因而意思全然不同。例如,“风暴来袭,这(this)太糟糕了。”

JavaScript 的上下文与对象有关,它取决于函数被执行时所在的对象。因此 this 会指向被执行函数所在的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a = 20;

function gx () {
return this;
}

function fx () {
return this.a;
}

function fy () {
return window.a;
}

console.log(gx() === window);
// => True
console.log(fx());
// => 20
console.log(fy());
// => 20

this 由函数被调用的方式决定。如你所见,上面的所有函数都是在全局上下文中被调用。

1
2
3
4
5
6
7
8
9
var o = {
prop: 37,
f: function() {
return this.prop;
}
};

console.log(o.f());
// => 37

当一个函数是作为某个对象的方法被调用时,它的 this 指向的就是这个方法所在的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function fx () {
return this;
}

var obj = {
method: function () {
return this;
}
};

var x_obj = {
y_obj: {
method: function () {
return this;
}
}
};

console.log(fx() === window);
// => True — 我们仍处于全局上下文中。
console.log(obj.method() === window);
// => False — 函数作为一个对象的方法被调用。
console.log(obj.method() === obj);
// => True — 函数作为一个对象的方法被调用。
console.log(x_obj.y_obj.method() === x_obj)
// => False — 函数作为 y_obj 对象的方法被调用,因此 `this` 指向的是 y_obj 的上下文。

例 4

1
2
3
4
5
6
7
function f2 () {
'use strict';
return this;
}

console.log(f2() === undefined);
// => True

在严格模式下,全局作用域的函数在全局作用域被调用时,thisundefined

例 5

1
2
3
4
5
6
7
8
9
10
11
12
function fx () {
return this;
}

var obj = {
method: fx
};

console.log(obj.method() === window);
// => False
console.log(obj.method() === obj);
// => True

与前面的例子一样,无论函数是如何被定义的,在这儿它都是作为一个对象方法被调用。

例 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
method: function () {
return this;
}
};

var sec_obj = {
method: obj.method
};

console.log(sec_obj.method() === obj);
// => False
console.log(sec_obj.method() === sec_obj);
// => True

this 是动态的,它可以由一个对象指向另一个对象。

例 7

1
2
3
4
5
6
7
8
9
10
11
var shop = {
fruit: "Apple",
sellMe: function() {
console.log("this ", this.fruit);
// => this Apple
console.log("shop ", shop.fruit);
// => shop Apple
}
}

shop.sellMe()

我们既能通过 shop 对象也能通过 this 来访问 fruit 属性。

例 8

1
2
3
4
5
6
7
8
9
10
var Foo = function () {
this.bar = "baz";
};

var foo = new Foo();

console.log(foo.bar);
// => baz
console.log(window.bar);
// => undefined

现在情况不同了。new 操作符创建了一个对象的实例。因此函数的上下文设置为这个被创建的对象实例。

Call、apply、bind

依旧以真实世界举例:“这(this)太糟糕了,因为我妈开始不爽了。”

这三个方法可以让我们在任何期许的上下文中执行函数。让我们举几个例子看看它们的用法:

例 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var bar = "xo xo";

var foo = {
bar: "lorem ipsum"
};

function test () {
return this.bar;
}

console.log(test());
// => xo xo — 我们在全局上下文中调用了 test 函数。
console.log(test.call(foo));
// => lorem ipsum — 通过使用 `call`,我们在 foo 对象的上下文中调用了 test 函数。
console.log(test.apply(foo));
// => lorem ipsum — 通过使用 `apply`,我们在 foo 对象的上下文中调用了 test 函数。

这两种方法都能让你在任何需要的上下文中执行函数。

apply 可以让你在调用函数时将参数以不定长数组的形式传入,而 call 则需要你明确参数。

例 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = 5;

function test () {
return this.a;
}

var bound = test.bind(document);

console.log(bound());
// => undefined — 在 document 对象中没有 a 这个变量。
console.log(bound.call(window));
// => undefined — 在 document 对象中没有 a 这个变量。在这个情况中,call 不能改变上下文。

var sec_bound = test.bind({a: 15})

console.log(sec_bound())
// => 15 — 我们创建了一个新对象 {a:15},并在此上下文中调用了 test 函数。

bind 方法返回的函数的下上文会被永久改变。 在使用 bind 之后,其上下文就固定了,无论你再使用 call、apply 或者 bind 都无法再改变其上下文。

箭头函数(ES6)

箭头函数是 ES6 中的一个新语法。它是一个非常方便的工具,不过你需要知道,在箭头函数中的上下文与普通函数中的上下文的定义是不同的。让我们举例看看。

例 1

1
2
3
var foo = (() => this);
console.log(foo() === window);
// => True

当我们使用箭头函数时,this 会保留其封闭范围的上下文。

例 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {method: () => this};

var sec_obj = {
method: function() {
return this;
}
};

console.log(obj.method() === obj);
// => False
console.log(obj.method() === window);
// => True
console.log(sec_obj.method() === sec_obj);
// => True

请注意箭头函数与普通函数的不同点。在这个例子中使用箭头函数时,我们仍然处于 window 上下文中。 我们可以这么看:

x => this.y equals function (x) { return this.y }.bind(this)

可以将箭头函数看做其始终 bind 了函数外层上下文的 this,因此不能将它作为构造函数使用。下面的例子也说明了其不同之处。

例 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = "global";

var obj = {
method: function () {
return {
a: "inside method",
normal: function() {
return this.a;
},
arrowFunction: () => this.a
};
},
a: "inside obj"
};

console.log(obj.method().normal());
// => inside method
console.log(obj.method().arrowFunction());
// => inside obj

当你了解了函数中动态(dynamic) this 与词法(lexical)this ,在定义新函数的时候请三思。如果函数将作为一个方法被调用,那么使用动态 this;如果它作为一个子程序(subroutine)被调用,则使用词法 this

译注:了解动态作用域与词法作用域可阅读此文章

相关阅读

本文发布于掘金 https://juejin.im/post/59e066d551882578c3411908

Kejriwal M, Szekely P. Information Extraction in Illicit Web Domains[J]. 2017.

论文信息

论文作者为南加大维特比学院的 Mayank Kejriwal 和 Pedro Szekely,发表于 WWW 2017。

论文概述

建立了稳定可靠的信息提取系统(Information Extraction System),用以从非法领域的网页中抽取有用的实体信息。将建立的模型应用于真实数据集得到的结果比 baseline(设定为 CRF)的 F-Score 高了 18%。

论文提纲

  1. 论文简介
  2. 描述了一些 IE system 的相关工作
  3. 详细描述了此文章所使用的模型
  4. 实验评估
  5. 总结工作

一、论文简介

作者首先简介了构建领域知识图谱(knowledge graph)所需要做的工作:

  1. 领域发现。来源可以为爬虫或领域本体库。 > 爬虫部分引用了 S. Chakrabarti. Mining the Web: Discovering knowledge from hypertext data. Elsevier, 2002. > 领域本体库部分引用了 A. Zouaq and R. Nkambou. A survey of domain ontology engineering: methods and tools. In Advances in intelligent tutoring systems, pages 103–119. Springer, 2010.

  2. 有了数据源后,使用 IE system 抽取相关结构化数据。作者简述了基于统计学习的信息抽取方法:使用 CRF 序列标注、以及在数据量大的情况下使用深层神经网络,目的为抽取命名实体与关系(extraction of name entities and relationships)。 > 神经网络部分引用 R. Collobert and J. Weston. A unified architecture for natural language processing: Deep neural networks with multitask learning. In Proceedings of the 25th international conference on Machine learning, pages 160–167. ACM, 2008.

    当 IE system 在跨领域 Web 数据源(cross-domain Web source,以维基百科为例)和传统领域(以生物学为例)表现优秀时,在一些“动态领域”(dynamic domain)中表现一般。这些领域包括:news feed、自媒体、广告、在线市场以及一些非法领域(如人口贩卖 human trafficking)等。

  3. 非法领域的信息抽取之所以难做是因为在非法网站中常常会对信息进行混淆、非随机地对一些常用词进行错误拼写、OOV(out of vocabulary,非登录词)及生僻词高频出现、有时候还非随机使用Unicode 字符,且相关网页中正文分布稀疏、网页结构各异。与传统领域中(如聊天记录、Twitter 等)规律的信息不同,这些信息正文在非法网站中基本上是独一无二的。此论文仅讨论人口贩卖领域,不过在另一些在暗网中存在 Web 服务的非法领域(如武器贩卖、恐怖袭击、假货等)也可以适用。

  4. 接着举了两个典型例子说明上面的情况: eg1. Hey gentleman im neWYOrk and i’m looking for generous... eg2. AVAILABLE NOW! ?? - (4 two 4) six 5 two - 0 9 three 1 - 21

  5. 因此传统领域 IE system 的包装归纳学习系统(wrapper induction systems)不能在这些领域中正常工作,只能将数据给调查员和领域专家进行分析。 作者归纳:此论文分析了传统的 IE system 在动态的、非法的领域中的不适用性,因此提出了一种不依赖于传统信息提取系统、可在小样本 Web 数据集上正常运行的方法。

  6. 简介这种方法。此方法包含了两个步骤:

  1. 第一步,使用召回率很高的识别器(用于识别地址、年龄等)为所有页面做候选标注(candidate annotations)。如下图所示
upload successful
  1. 第二步,使用一种无特征的监督学习算法,基于随机映射学习单词表示的意思。用此算法对上面的候选标注进行二分类,分为正确与不正确。
  1. 简述贡献:创建了一种轻量级的、无特征的信息提取系统,可以适用于各种各样的非法领域。且这种方法很容易实现,无需大范围地调参,效果随数据集增大而增强,适用部署于流数据。且此方法在刚开始做领域发现的小数据集上也表现良好,在遇到超大 web 数据集时依然稳定。
  2. 简述 baseline,基于 CRF 的最新的 Stanford Named Entity Resolution system,包含关于人口贩卖的预训练数据。

二、相关工作

Open IE > N. Kushmerick. Wrapper induction for information extraction. PhD thesis, University of Washington, 1997. > M. Banko, M. J. Cafarella, S. Soderland,M. Broadhead, and O. Etzioni. Open information extraction from the web. In IJCAI, volume 7, pages 2670–2676, 2007. ADRMine > B. Han, P. Cook, and T. Baldwin. Text-based twitter user geolocation prediction. Journal of Artificial Intelligence Research, 49:451–500, 2014. > A. Nikfarjam, A. Sarker, K. OaˆA ̆Z ́Connor, R. Ginn, and G. Gonzalez. Pharmacovigilance from social media: mining adverse drug reaction mentions using sequence labeling with word embedding cluster features. Journal of the American Medical Informatics Association, page ocu041, 2015.

在此之前还没有工作对无特征、低监督的 web 非法领域信息提取进行研究。

三、方法

总体的架构如上图 Figure 1 所示。模型有两个输入端,一个是包含有关兴趣领域的 Web 页面集,一个是有着高召回率的识别器。在此论文中假定模型初始化时的数据集很小,接着会有很多的数据不断地加入到数据集中(即模仿流数据条件)。首先将给定一个初始的数据集,其中已经人工标注好了 10-100 条数据的属性(假定为城市、姓名、年龄),模型将根据此数据集在没有做特征工程的情况下学习出一个信息提取系统。 > 此处数据条数有疑问

需要注意的是需要分析的 Web 页面大多是多领域结合,因此在处理页面时不仅需要不断将新的页面加入数据集中,还要由初始数据集进行概念漂移(concept drift)以适应各种新的情况。

1 预处理过程

爬取相关网页,使用 RTE(Readability Text Extractor)对 HTML 文本进行正文提取。对 RTE 进行调参,将其调至高召回率。由于 Web 网页的结构多样,因此正文中可能会存在许多无关内容(包括一些无用的数字及 Unicode 字符等等)。RTE 最终会返回一组字符串集,字符串集中包含以句子为单位的内容。 > RTE 给了网址:https://www.readability.com/developers/api

接下来使用 NLTK 对 RTE 返回内容的每个句子分别进行分词。

2 词向量表示

接下来作者使用 CRF 序列标注模型对已标注数据进行学习(详细内容在后面)。但是由于数据集实在太少,CRF 的效果并不好。 为了避免 CRF 出现的状态,又为了避免工作量极大的页面标注,作者使用了非监督算法,以低维空间来表示页面中所有词的全集。在此使用了嵌入算法(embedding algorithms),作者在此提及了 Word2Vec 和 Bollegala 的算法和一种更简单的算法(random indexing)。 > Word2Vec 引用了 T. Mikolov, I. Sutskever, K. Chen, G. S. Corrado, and J. Dean. Distributed representations of words and phrases and their compositionality. In Advances in neural information processing systems, pages 3111–3119, 2013. > Bollegala 算法引用了 D. Bollegala, T. Maehara, and K.-i. Kawarabayashi. Embedding semantic relations into word representations. arXiv preprint arXiv:1505.00161, 2015. > random indexing 引用了 M. Sahlgren. An introduction to random indexing. In Methods and applications of semantic indexing workshop at the 7th international conference on terminology and knowledge engineering, TKE, volume 5, 2005. 最终作者选用了“最简单”的 RI(random indexing)算法。此算法可以保证向量在表示时,即使是在低维空间中点与点间依然能够保持足够的距离。RI 算法最开始本来是为了增量降维(incremental dimensionality reduction)设计的。RI 算法定义如下:

\(d \in \mathbb{Z}^+\) -> 向量维数 \(r \in [0,1]\) -> 定义为 +1 或 -1 的概率

对于一个词向量(这里应该叫做 token 向量,因为在这个情景中 nltk 并不能很好地进行正确分词)来说,随机选择 \([d * r]\) 个维度设定为 +1,随机选择 \([d * r]\) 个维度设定为 -1,其余 \(d - 2 [d * r]\) 个维度设定为 0。

由于在此场景下数据均为噪音很多的流数据,因此只考虑较短的上下文范围。由此确定 RI 算法的滑动窗口大小。滑动窗口定义如下:

给定一组数量为 \(|t|\) 的单位元素 \(t\),以及一个窗口基准位置 \(0 < i < |t|\),则可以定义一个滑动窗口 \((u, v)\)。滑动窗口内的所有元素记为 \(S\),将基准位置的特征词挖去:\(S - t[i]\),得到滑动窗口全集。滑动窗口的位置坐标为 \([max(i-u,1),min(i+v,|t|)]\)

upload successful

上图为使用 RI 算法,设定滑动窗口大小设定为 (2,2) 时产生的 Token 向量。

作者对于原始的 RI 算法进行了一些改进,改动内容如下:

原始 RI 算法在求某特征词上下文向量时没有进行任何权重计算,直接对上下文非特征词的词向量进行了求和。也就是说,对于单词 w 来说,其表示向量进行了 \(\vec{w}_{i+1} = \vec{w}_i + \vec{a}\) 计算。但是对于非法领域来说,在此情景中会出现大量独特的 token ,包括且不仅限于罕见单词、Unicode 符号、HTML tag、数字序列(如电话号码)等。这些 token 可能在文本全集中仅仅会出现一次或少数几次,因此它们产生的表示向量也会很少出现。作者为了避免这种情况的出现,预先定义了一些“高权重单位”(compound unit)

为了定义这种高权重单位的值,作者对于上下文中 token 可能出现的一些“罕见”情况进行了定义,定义如下:

Name 名称 描述
High-idf-units 低词频单元 单元内的单词词频低于设定值\(\theta\)(默认为 1%)
Pure-num-units 纯数字单元 只包含数字的单元
Alpha-num-units 字母-数字混合单元 至少包含 1 个数字与 1 个字母的单元
Pure-punct-units 纯符号单元 只包含标点符号的单元
Alpha-punct-units 字母-符号混合单元 至少包含 1 个字母与 1 个符号的单元
Nonascii-unicode-units 非 ASCII 字符单元 只包含非 ASCII 字符的单元

以上 6 种“罕见”的单元就是“高权重单位”。

作者在这儿举了个例子:有个单词为“rare”它在文中出现的词频低于某个设定值(如 1%),它根据上表可以定义为 High-idf-units。

作者对 RI 算法根据上面的定义进行了一定修改:RI 算法在计算上下文向量时,仅使用上面 6 中高权重单元的表示向量进行计算。也就是说,每个 token 最终都可以根据上面 6 中高权重单元的向量的线性计算得到自己的独特向量 \(\vec{w}_C\)

个人认为作者这样做丢失了很多可能会有用的信息,但是这样可以在本来整体信息就不多的非法领域网页 Web 正文中最大可能地提取高敏感信息。如果之后对这种方法进行改进的话,可以在这块找出更多高敏感信息的模式特征,或者使用改造过的 word2vec 等其它算法进行尝试。

3 应用高召回率识别器

此处的“识别器”指的是对单一属性进行识别的函数。作者在此给出了相关定义:

对于某个属性 A,它的识别器 \(R_A\) 为一个函数,该函数接受一组 token,该组数据记为 t。给定两个输入值 i 与 j(\(j \geq i\)),分别代表 t 中的两个位置。如果 t 的 t[i]:t[j] 子集为 A 属性的 instance 则返回 True,否则返回 False。

值得注意的是在上面的定义中,识别器并不能识别出 token 中所隐藏的 instance。在平常的 IE system 中所使用的识别器一般都是 recall 和 precision 都相对较高的。在现在这个情境中,识别器不再追求纯粹的准确率(评判标准为 recall 与 precision),而是尽量少丢失一些信息。因此作者选用了有着高 recall 的识别器对文本进行识别标注,在后面一步再去使用监督学习分类来改进这一步产生的候选标注的 precision。

upload successful
4 使用监督学习根据上下文进行分类

在此之前,已经通过高 recall 的识别器将大多数有可能正确的有意义实体给识别出来,因此需要应用分类器对前一步骤得到的各个实体进行分类以提高最终结果精确度。

由识别器得到一系列的标注(标注可以为单独的 token,也可以是一组连续的 token),将各个标注所在的滑动窗口 (u,v)-context 取出其上下文,求其向量表示,上下文向量的无权重和(unweighted sum)来表示这些标注的上下文向量。

接着,对这些向量应用 l2 正则,使用监督学习分类器(如随机森林)对其进行分类。

四、实验评估

1 数据集与评价标准
upload successful

表 2:数据集用量

作者描述,数据集来自于 DARPA MEMEX 项目。(网址:http://www.darpa.mil/program/memex)

upload successful

表 3:5 种类别的标准数据集

baseline 与本文的分类器都将对上表包含的 5 种类型的数据进行分类标注。

本文的标准数据集来自于数据集。对数据集应用高 recall 识别器,再对识别器得到的结果进行人工标注。

2 系统描述

根据上文所述,本文构建的 Information Extraction System 对于每种属性都由两个部分组成:高 recall 识别器与用于裁剪标记的分类器。作者实验室为此构建了 4 中高 recall 的识别器,分别为:

GeoNames-Cities GeoNames-States RegEx-Ages Dictionary-Names

其中前两种识别器的数据集来自于http://www.geonames.org/,在此处引用了 M. Wick and C. Boutreux. Geonames. GeoNames Geographical Database, 2011。

对于年龄识别,开发了 https://github.com/usc-isi-i2/dig-age-extractor > 阅读代码发现原理特别简单,单纯的匹配数字

对于姓名属性,作者收集了各种语言的姓名于数据集中,数据集地址: https://github.com/usc-isi-i2/dig-dictionaries/tree/master/person-names

3 baseline

作者使用 Stanford Named Entity Recognition system(NER) 作为其 baseline。 > 此处引用 J. R. Finkel, T. Grenager, and C. Manning. Incorporating non-local information into information extraction systems by gibbs sampling. In Proceedings of the 43rd Annual Meeting on Association for Computational Linguistics, pages 363–370. Association for Computational Linguistics, 2005.

并使用为其使用了一个预训练数据集(脚标 12)。对于新数据集使用 baseline 模型进行重训练,随机采样 30% 与 70% 数据集作为训练集,其余数据作为测试集。baseline 的设置如下表所示:

upload successful 表 4:斯坦福 NER 在模型重训练时的参数设置

4 相关参数

词向量表示中,使用 100 维,低词频词的词频比例阈值设为 0.01。(这些参数作者说是根据之前的相关论文设定的,但是在这儿没有引用具体论文)。滑动窗口大小设置为 (2,2),作者在此描述尝试使用了更大的滑动窗口但是没有很好的效果。

分类器采用随机森林分类,树数量为 10,变量重要性度量方式为 Gini 指数法,找出最佳的 20 个特征进行分类,使用 ANOVA(方差分析)作为评估函数。

评估指标:准确率、回归率与 F1 指标

实验环境:iMac,4GHz Intel core i7,32 GB RAM,Scikit-learn v0.18

5 实验结果

直接贴论文中实验结果的指标表格:

upload successful

表 5:baseline 与此系统在训练 30% 数据时的 PRF 指标

upload successful

表 6:baseline 与此系统在训练 70% 数据时的 PRF 指标

upload successful

表 7:训练 30% 数据时,此系统在不同全集中的 F1 指标

upload successful

表 8:训练 70% 数据时,此系统在不同全集中的 F1 指标

upload successful

图 5:训练 30% 数据时,在姓名属性识别中,使用不同特征数量得到的 PRF 指标变化折线图

upload successful

表 9:系统识别城市名称的一些样例

6 讨论

(作者”解释“了前面我关于为什么不使用 word2vec 的疑惑)作者表示,他也不清楚为啥不能使用 word2vec 之类更具适应性的算法来代替 Random Indexing 算法。不过表 7 与表 8 可以看到 Random Indexing 算法在不断加入更多网站的情况下表现仍然稳定。

upload successful

表 10:在一万数据集与全集中 Random Indexing 算法找到的相似语义样例

由表 10 可以看到 Random Indexing 算法在不同数据集大小下表现的鲁棒性,给出的相似语义词依然较为准确。

upload successful

图 6:城市名称上下文分类时的可视化图(不同的颜色表示在标准数据集中的标签)

使用 t-SNE 工具对数据进行可视化,得到上图。 > t-SNE 工具引用了 L. v. d. Maaten and G. Hinton. Visualizing data using t-sne. Journal of Machine Learning Research, 9(Nov):2579–2605, 2008.

由图可见,分类器对城市名称正负例分类效果良好。另外可以观察到图中有许多点各自又组成了一些”子聚类“(sub-cluster),这些点来自于上下文比较相似的数据。

最后,作者表示他们在后来的工作中对一些不常见的属性(例如一些领域特定属性)进行了测试,仍然得到了相似的性能。因此这种方法在各种情况都适用。

总结

作者提出了一种轻量、特征不可知的信息提取方式,适用于非法 Web 领域。这种方法基于初始本体集中构建的向量表示,与使用高 recall 的分类器结合,得到了良好的结果。实验结果表明这种方法相对于其 baseline(CRF)有明显的提升。论文中的各种代码都在 github 上进行了公开。

Uber 工程师们一直致力于开发各种新技术,以让客户得到有效、无缝的用户体验。现在,他们正在加大对人工智能、机器学习领域的投入来实现这个愿景。在 Uber,工程师们开发出了一个名为“米开朗基罗”(Michelangelo)的机器学习平台,它是一个内部的“MLaaS”(机器学习即服务)平台,用以降低机器学习开发的门槛,并能根据不同的商业需求对 AI 进行拓展与缩放,就有如客户使用 Uber 打车一样方便。

米开朗基罗平台可以让公司内部团队无缝构建、部署与运行 Uber 规模的机器学习解决方案。它旨在覆盖全部的端到端机器学习工作流,包括:数据管理、训练模型、评估模型、部署模型、进行预测、预测监控。此系统不仅支持传统的机器学习模型,还支持时间序列预测以及深度学习。

米开朗基罗在 Uber 投产约一年时间,已经成为了 Uber 工程师、数据科学家真正意义上的“平台”,现在有数十个团队在此平台上构建、部署模型。实际上,米开朗基罗平台现在部署于多个 Uber 数据中心并使用专用硬件,用于为公司内最高负载的在线服务提供预测功能。

本文将介绍米开朗基罗以及其产品用例,并简单通过这个强大的 MLaaS 系统介绍整个机器学习工作流。

米开朗基罗背后的动机

在米开朗基罗平台出现前,Uber 的工程师和数据科学家们在构建、部署一些公司需要,并且能根据实际操作进行规模拓展的机器学习模型时,遇到了很多挑战。那时他们试图使用各种各样的工具来创建预测模型(如 R 语言、scikit-learn、自定义算法等),此时工程团队会构建一些一次性的系统以使用这些模型进行预测。因此,在 Uber 内能够在短时间内使用各种开源工具构建出框架的数据科学家与工程师少之又少,限制了机器学习在公司内的应用。

具体来说,那时没有建立一个可靠、统一、pipeline 可复用的系统用于创建、管理、训练、预测规模化数据。因此在那时,不会有人做出数据科学家的台式机跑不了的模型,也没有一个规范的结果存储方式,要将几个实验结果进行对比也是相当困难的事情。更重要的是,那时没有一种将模型部署到生产环境的确定方法。因此,大多数情况下都是相关的工程团队不得不为手中的项目开发定制的服务容器。这时,他们注意到了这些迹象符合由 Scully 等人记录的机器学习的反模式一文的描述。

米开朗基罗旨在将整个团队的工作流程和工具标准化,通过端对端系统让整个公司的用户都能轻松构建、运行大型机器学习系统。但是工程师们的目标不仅限于解决这些直观的问题,更是要创立一个能与业务共同发展的体系。

当工程师们于 2015 年年中开始构建米开朗基罗系统时,他们也开始解决一些规模化模型训练以及一些将模型部署于生产环境容器的问题。接着,他们专注于构建能够更好进行管理、共享特征 pipeline 的系统。而最近,他们的重心转移到了开发者生产效率 — 如何加速从想法到产品模型的实现以及接下来的快速迭代。

下一节将通过一个样例来介绍如何使用米开朗基罗构建、部署一个模型,用于解决 Uber 的某种特定问题。虽然下面重点讲的是 UberEATS 中的具体用例,但是这个平台也管理着公司里其他针对多种预测用例的类似模型。

用例:UberEATS 送餐到家时间预估模型

UberEATS 在米开朗基罗中有数个模型在运行,包括送餐到达时间预测、搜索排行、搜索自动完成、餐厅排行等。送餐到达时间预测模型能够预测准备膳食、送餐以及送餐过程中的各个阶段所需的时间。

upload successful

图 1:UberEATS app 提供了估测外卖送达时间的功能,此功能由基于米开朗基罗构建的机器学习模型驱动。

预测外卖的送达时间(ETD)并不是一件简单的事情。当 UberEATS 用户下单时,订单将被送到餐厅进行处理。餐厅需要确认订单,根据订单的复杂度以及餐厅的繁忙程度准备餐品,这一步自然要花费一些时间。在餐品快要准备完毕的时候,Uber 外卖员出发去取餐。接着,外卖员需要开车到达餐厅、找到停车场、进餐厅取餐、回到车里、开车前往客户家(这个步骤耗时取决于路线、交通等因素)、找到车位、走到客户家门口,最终完成交货。UberEATS 的目标就是预测这个复杂的多阶段过程的总时间,并在各个步骤重新计算 ETD。

在米开朗基罗平台上,UberEATS 数据科学家们使用了 GBDT(梯度提升决策树)回归模型来预测这种端到端的送达时间。此模型使用的特征包括请求信息(例如时间、送餐地点)、历史特征(例如餐厅在过去 7 天中的平均餐食准备时间)、以及近似实时特征(例如最近一小时的平均餐食准备时间)。此模型部署于 Uber 数据中心的米开朗基罗平台提供的容器中,通过 UberEATS 微服务提供网络调用。预测结果将在餐食准备及送达前展示给客户。

系统架构

米开朗基罗系统由一些开源系统和内置组件组成。主要使用的开源组件有 HDFSSparkSamzaCassandraMLLibXGBoostTensorFlow。在条件允许的前提下,开发团队更倾向于使用一些成熟的开源系统,并会进行 fork、定制化,如果有需求的话也会对其进行贡献。如果找不到合适的开源解决方案,他们也会自己构建一些系统。

米开朗基罗系统建立与 Uber 的数据及计算基础设施之上,它们提供了一个“数据湖”,其中包含了 Uber 所有的事务和日志数据。由 Kafka 对 Uber 的所有服务日志进行采集汇总,使用 Cassandra 集群管理的 Samza 流计算引擎以及 Uber 内部服务进行计算与部署。

在下一节中将以 UberEATS 的 ETD 模型为例,简单介绍系统的各个层次,说明米开朗基罗的技术细节。

机器学习工作流

在 Uber,大多数的机器学习用例(包括一些正在做的工作,例如分类、回归以及时间序列预测等)都有着一套同样的工作流程。这种工作流程可以与具体实现分离,因此很容易进行拓展以支持新的算法和框架(例如最新的深度学习框架)。它还适用于各种不同预测用例的部署模式(如在线部署与离线部署,在车辆中使用与在手机中使用)。

米开朗基罗专门设计提供可拓展、可靠、可重用、易用的自动化工具,用于解决下面 6 步工作流:

  1. 管理数据
  2. 训练模型
  3. 评估模型
  4. 部署模型
  5. 预测结果
  6. 预测监控

下面将详细介绍米开朗基罗的架构是如何促进工作流中的各个步骤的。

管理数据

找出良好的特征经常是是机器学习最难的部分,工程师们也发现整个机器学习解决方案中最费时费力的部分就是构建及管理数据管道。

因此,平台应提供一套标准工具以构建数据管道,生成特征,对数据集进行标记(用于训练及再训练),以及提供无标记特征数据用以预测,这些工具需要与公司的数据湖、数据中心以及公司的在线数据服务系统进行深度的整合。构建出来的数据管道必须具有可缩放性以及足够的性能,能够监控数据流以及数据质量,为各种在线/离线训练与预测都提供全面的支持。这些工具还应该能通过团队共享的方式生成特征,以减少重复工作并提高数据质量。此外,这些工具应当提供强有力的保护措施,鼓励用户去采用最好的方式使用工具(例如,保证在训练时和预测时都采用同一批次生成的数据)。

米开朗基罗的数据管理组件分为在线管道和离线管道。目前,离线管道主要用于为批量模型训练以及批量预测作业提供数据;在线管道主要为在线、低时延预测作业提供数据(以及之后会为在线学习系统提供支持)。

此外,工程师们还为数据管理层新加了一个特征存储系统,可以让各个团队共享、发现高质量的数据特征以解决他们的机器学习问题。工程师们发现,Uber 的许多模型都是用了类似或相同的特征,而在不同组织的团队以及团队里的不同项目中共享特征是一件很有价值的事情。

upload successful

图 2:数据预处理管道将数据存入特征库以及训练数据仓库中。

离线部署

Uber 的事务与日志数据会“流入”一个 HDFS 数据湖中,可以使用 Spark 和 Hive SQL 的计算作业轻松调用这些数据。平台提供了容器与计划任务两种方式运行常规作业,用于计算项目内部的私有特征或将其发布至特征存储库(见后文)与其他团队共享。当计划任务运行批量作业或通过别的方式触发批量作业时,作业将被整合传入数据质量监控工具,此工具能够快速回溯找出问题出在 pipeine 中的位置,判明是本地代码的问题还是上游代码的问题导致的数据错误。

在线部署

在线部署的模型将无法访问 HDFS 存储的数据,因此,一些需要在 Uber 生产服务的支撑数据库中读取的特征很难直接用于这种在线模型(例如,无法直接查询 UberEATS 的订单服务去计算某餐厅某特定时间段平均膳食准备时间)。因此,工程师们将在线模型需要的特征预计算并存储在 Cassandra 中,线上模型可以低延迟读取这些数据。

在线部署支持两种计算系统:批量预计算与近实时计算,详情如下:

  • 批量预计算。这个系统会定期进行大批量计算,并将 HDFS 中的特征历史记录加载进 Cassandra 数据库中。这样做虽然很简单粗暴,但是如果需要的特征对实时性要求不高(比如允许隔几小时更新一次),那么效果还是很好的。这个系统还能保证在批处理管道中用于训练和服务的数据是同批次的。UberEATS 就采用了这个系统处理一部分特征,如“餐厅过去七天的膳食平均准备时间”。
  • 近实时计算。这个系统会将相关指标发布至 Kafka 系统,接着运行 Samza 流计算作业以低时延生成所有特征。接着这些特征将直接存入 Cassandra 数据库用于提供服务,并同时备份至 HDFS 用于之后的训练作业。和批量预计算系统一样,这样做同样能保证提供服务和进行训练的数据为同一批次。为了避免这个系统的冷启动,工程师们还专门为这个系统制作了一个工具,用于“回填”数据与基于历史记录运行批处理生成训练数据。UberEATS 就使用了这种近实时计算 pipeline 来得到如“餐厅过去一小时的膳食平均准备时间”之类的特征。

共享特征库

工程师们发现建立一个集中的特征库是很有用的,这样 Uber 的各个团队可以使用其他团队创建和管理的可靠的特征,且特征可以被分享。从大方向上看,它做到了以下两件事情:

  1. 它可以让用户轻松地将自己构建的特征存入共享特征库中(只需要增加少许元数据,如添加者、描述、SLA 等),另外它也能让一些特定项目使用的特征以私有形式存储。
  2. 只要特征存入了特征库,那之后再用它就十分简单了。无论是在线模型还是离线模型,都只要简单地在模型配置中写上特征的名称就行了。系统将会从 HDFS 取出正确的数据,进行处理后返回相应的特征集既可以用于模型训练,也可以用于批量预测或者从 Cassandra 取值做在线预测。

目前,Uber 的特征库中有大约 10000 个特征用于加速机器学习工程的构建,公司的各个团队还在不断向其中增加新的特征。特征库中的特征每天都会进行自动计算与更新。

未来,工程师们打算构建自动化系统,以进行特征库搜索并找出解决给定预测问题的最有用的特征。

用于特征选择及转换的领域特定语言(DSL)。

由数据 pipeline 生成的特征与客户端服务传来的特征经常不符合模型需要的数据格式,而且这些数据时常会缺失一些值,需要对其进行填充;有时候,模型可能只需要传入的特征的一个子集;还有的时候,将传入的时间戳转换为 小时/天 或者 天/周 会在模型中起到更好的效果;另外,还可能需要对特征值进行归一化(例如减去平均值再除以标准差)。

为了解决这些问题,工程师们为建模人员创造了一种 DSL(领域特定语言),用于选择、转换、组合那些用于训练或用于预测的特征。这种 DSL 为 Scala 的子集,是一种纯函数式语言,包含了一套常用的函数集,工程师们还为这种 DSL 增加了自定义函数的功能。这些函数能够从正确的地方取出特征(离线模型从数据 pipeline 取特征值,在线模型从客户请求取特征值,或是直接从特征库中取出特征)。

此外,DSL 表达式是模型配置的一部分,在训练时取特征的 DSL 与在与测试时用的 DSL 需要保持一致,以确保任何时候传入模型的特征集的一致性。

训练模型

目前平台支持离线、大规模分布式训练,包括决策树、线性模型、逻辑模型、无监督模型(k-means)、时间序列模型以及深度神经网络。工程师们将定期根据用户的需求增加一些由 Uber AI 实验室新开发的模型。此外,用户也可以自己提供模型类型,包括自定义训练、评价以及提供服务的代码。分布式模型训练系统可以规模化处理数十亿的样本数据,也可以处理一些小数据集进行快速迭代。

一个模型的配置包括模型类型、超参、数据源、特征 DSL,以及计算资源需求(需要的机器数量、内存用量、是否使用 GPU 等)。这些信息将用于配置运行在 YARNMesos 集群上的训练作业。

在模型训练完毕之后,系统会将其计算得到的性能指标(例如 ROC 曲线和 PR 曲线)进行组合,得到一个模型评价报告。在训练结束时,系统会将原始配置、学习到的参数以及评价包括存回模型库,用于分析与部署。

除了训练单个模型之外,米开朗基罗系统还支持对分块模型等各种模型进行超参搜索。以分块模型为例,以分块模型为例,系统会根据用户配置自动对训练数据进行分块,对每个分块训练一个模型;在有需要的时候再将各个分块模型合并到父模型中(例如,先对每个城市数据进行训练,如果无法得到准确的市级模型时再将其合并为国家级模型)。

训练作业可以通过 Web UI 或者 API 进行配置与管理(通常使用 Jupyter notebook)。大多数团队都使用 API 以及流程管理工具来对他们的模型进行定期重训练。

upload successful

图 3:模型训练作业使用特征库与数据训练仓库中的数据集来训练模型,接着将模型存入模型库中。

评估模型

训练模型可以看成是一种寻找最佳特征、算法、超参以针对问题建立最佳模型的探索过程。在得到用例的理想模型前,训练数百种模型而一无所获也是常有的事。虽然这些失败的模型最终不能用于生产,但它们可以指导工程师们更好地进行模型配置,从而获得更好的性能。追踪这些训练过的模型(例如谁、何时训练了它们,用了什么数据集、什么超参等),对它们的性能进行评估、互相对比,可以为平台带来更多的价值与机会。不过要处理如此之多的模型,也是一个极大的挑战。

米开朗基罗平台中训练的每个模型都需要将以下信息作为版本对象存储在 Cassandra 的模型库中:

  • 谁训练的模型。
  • 训练模型的开始时间与结束时间。
  • 模型的全部配置(包括用了什么特征、超参的设置等)。
  • 引用训练集和测试集。
  • 描述每个特征的重要性。
  • 模型准确性评价方法。
  • 模型每个类型的标准评价表或图(例如 ROC 曲线图、PR 曲线图,以及二分类的混淆矩阵等)。
  • 模型所有学习到的参数。
  • 模型可视化摘要统计。

用户可以通过 Web UI 或者使用 API 轻松获取这些数据,用以检查单个模型的详细情况或者对多个模型进行比较。

模型准确率报告

回归模型的准确率报告会展示标准的准确率指标与图表;分类模型的准确率报告则会展示不同的分类集合,如图 4 图 5 所示:

upload successful

图 4:回归模型的报告展示了与回归相关的性能指标。

upload successful

图 5:二分类模型报告展示了分类相关的性能指标。

可视化决策树

决策树作为一种重要的模型类型,工程师们为其提供了可视化工具,以帮助建模者更好地理解模型的行为原理,并在建模者需要时帮助其进行调试。例如在一个决策树模型中,用户可以浏览每个树分支,看到其对于整体模型的重要程度、决策分割点、每个特征对于某个特定分支的权重,以及每个分支上的数据分布等变量。用户可以输入一个特征值,可视化组件将会遍历整个决策树的触发路径、每个树的预测、整个模型的预测,将数据展示成类似下图的样子:

upload successful

图 6:使用强大的树可视化组件查看树模型。

特征报告

米开朗基罗提供了特征报告,在报告中使用局部依赖图以及混合直方图展示了各个特征对于模型的重要性。选中两个特征可以让用户看到它们之间相互的局部依赖图表,如下所示:

upload successful

图 7:在特征报告中可以看到的特征、对模型的重要性以及不同特征间的相关性。

部署模型

米开朗基罗支持使用 UI 或 API 端对端管理模型的部署。一个模型可以有下面三种部署方式:

  1. 离线部署。模型将部署于离线容器中,使用 Spark 作业,根据需求或计划任务进行批量预测。
  2. 在线部署。模型将部署于在线预测服务集群(集群通常为使用负载均衡部署的数百台机器),客户端可以通过网络 RPC 调用发起单个或批量的预测请求。
  3. 部署为库。工程师们希望能在服务容器中运行模型。可以将其整合为一个库,也可以通过 Java API 进行调用(在下图中没有展示此类型,不过这种方式与在线部署比较类似)。
upload successful

图 8:模型库中的模型部署于在线及离线容器中用于提供服务。

上面所有情况中,所需要的模型组件(包括元数据文件、模型参数文件以及编译好的 DSL)都将被打包为 ZIP 文件,使用 Uber 的标准代码部署架构将其复制到 Uber 数据中心的相关数据上。预测服务容器将会从磁盘自动加载新模型,并自动开始处理预测请求。

许多团队都自己写了自动化脚本,使用米开朗基罗 API 进行一般模型的定期再训练及部署。例如 UberEATS 的送餐时间预测模型就由数据科学家和工程师通过 Web UI 控制进行训练与部署。

预测结果

一旦模型部署于服务容器并加载成功,它就可以开始用于对数据管道传来的特征数据或用户端发来的数据进行预测。原始特征将通过编译好的 DSL 传递,如有需要也可以对 DSL 进行修改以改进原始特征,或者从特征存储库中拉取一些额外的特征。最终构造出的特征向量会传递给模型进行评分。如果模型为在线模型,预测结果将通过网络传回给客户端。如果模型为离线模型,预测结果将被写回 Hive,之后可以通过下游的批处理作业或者直接使用 SQL 查询传递给用户,如下所示:

upload successful

图 9:在线预测服务及离线预测服务使用一组特征向量生成预测结果。

引用模型

在米开朗基罗平台中可以同时向服务容器部署多个模型。这也使得从旧模型向新模型进行无痛迁移以及对模型进行 A/B 测试成为可能。在服务中,可以由模型的 UUID 以及一个在部署时可指定的 tag(或者别名)识别不同的模型。以一个在线模型为例,客户端服务会将特征向量与需要使用的模型 UUID 或者 tag 同时发送给服务容器;如果使用的是 tag,服务容器会使用此 tag 对应的最新部署的模型进行预测。如果使用的是多个模型,所有对应的模型都将对各批次的数据进行预测,并将 UUID 和 tag 与预测结果一同传回,方便客户端进行筛选过滤。

如果在部署一个新模型替换旧模型时用了相同的事物(例如用了一些同样的特征),用户可以为新模型设置和旧模型一样的 tag,这样容器就会立即开始使用新模型。这可以让用户只需要更新模型,而不用去修改他们的客户端代码。用户也可以通过设置 UUID 来部署新模型,再将客户端或中间件配置中旧模型的 UUID 换成新的,逐步将流量切换到新模型去。

如果需要对模型进行 A/B 测试,用户可以通过 UUID 或者 tag 轻松地部署竞争模型,再使用客户端服务中的 Uber 实验框架将部分流量导至各个模型,再对性能指标进行评估。

规模缩放与时延

由于机器学习模型是无状态的,且不需要共享任何东西,因此,无论是在线模式还是离线模式下对它们进行规模缩放都是一件轻而易举的事情。如果是在线模型,工程师可以简单地给预测服务集群增加机器,使用负载均衡器分摊负载。如果是离线预测,工程师可以给 Spark 设置更多的 executor,让 Spark 进行并行管理。

在线服务的延迟取决于模型的类型与复杂度以及是否使用从 Cassandra 特征库中取出的特征。在模型不需要从 Cassandra 取特征的情况下,P95 测试延迟小于 5 毫秒。在需要从 Cassandra 取特征时,P95 测试延迟仍小于 10 毫秒。目前用量最大的模型每秒能提供超过 250000 次预测。

预测监控

当模型训练完成并完成评价之后,使用的数据都将是历史数据。监控模型的预测情况,是确保其在未来正常工作的重要保障。工程师需要确保数据管道传入的是正确的数据,并且生产环境没有发生变化,这样模型才能够进行准确的预测。

为了解决这个问题,米开朗基罗系统会自动记录并将部分预测结果加入到数据 pipeline 的标签中去,有了这些信息,就能得到持续的、实时的模型精确度指标。在回归模型中,会将 R^2/决定系数均方根对数误差(RMSLE)、均方根误差(RMSE)以及平均绝对值误差发布至 Uber 的实时监控系统中,用户可以分析指标与时间关系的图标,并设置阈值告警:

upload successful

图 10:对预测结果进行采样,与观测结果进行比较得到模型准确指标。

管理层、API、Web UI

米开朗基罗系统的最后一个重要部分就是 API 层了,它也是整个系统的大脑。API 层包含一个管理应用,提供了 Web UI 以及网络 API 两种访问方式,并与 Uber 的监控、报警系统相结合。同时该层还包含了用于管理批量数据管道、训练作业、批量预测作业、模型批量部署以及在线容器的工作流系统。

米开朗基罗的用户可以通过 Web UI、REST API 以及监控、管理工具直接与这些组件进行交互。

米开朗基罗平台之后的构建工作

工程师们打算在接下来几个月继续扩展与加强现有的系统,以支持不断增长的用户和 Uber 的业务。随着米开朗基罗平台各个层次的不断成熟,他们计划开发更高层的工具与服务,以推动机器学习在公司内部的发展,更好地支持商业业务:

  • AutoML。这是将会成为一个自动搜寻与发现模型配置的系统(包括算法、特征集、超参值等),可以为给定问题找到表现最佳的模型。该系统还会自动构建数据管道,根据模型的需要生成特征与标签。目前工程师团队已经通过特征库、统一的离线在线数据管道、超参搜索特征解决了此系统的一大部分问题。AutoML 系统可以加快数据科学的早期工作,数据科学家们只需要指定一组标签和一个目标函数,接着就能高枕无忧地使用 Uber 的数据找到解决问题的最佳模型了。这个系统的最终目标就是构建更智能的工具,简化数据科学家们的工作,从而提高生产力。
  • 模型可视化。对于机器学习,尤其是深度学习,理解与调试模型现在变得越来越重要。虽然工程师们已经首先为树状模型提供了可视化工具,但是还需要做更多的工作,帮助数据科学家理解、debug、调整他们的模型,得到真正令人信服的结果。
  • 在线学习。Uber 的机器学习模型大多数直接受到 Uber 产品的实时影响。这也意味着这些模型需要能够在复杂、不断变化的真实世界中运行。为了保证模型在变化环境中的准确性,这些模型需要随着环境一同进化;现在,各个团队会在米开朗基罗平台上定期对模型进行重训练。一个完整的平台式解决方案应该让用户能够轻松地对模型进行升级、快速训练及评价,有着更精细的监控及报警系统。虽然这将是一个很大的工程,但是早前的研究结果表明,构建完成在线学习系统可能会带来巨大的收益。
  • 分布式深度学习。越来越多的 Uber 机器学习系统开始使用深度学习实现。定义与迭代深度学习模型的工作流与标准的工作流有着很大的区别,因此需要平台对其进行额外的支持。深度学习需要处理更大的数据集,需要不同的硬件支持(例如 GPU),因此它更需要分布式学习的支持,以及与更具弹性的资源管理堆栈进行紧密结合。

如果你对挑战规模化机器学习有兴趣,欢迎申请Uber 机器学习平台团队

作者简介:Jeremy Hermann 是 Uber 机器学习平台团队的工程经理,Mike Del Balso 是 Uber 机器学习平台团队的产品经理。

发布于掘金 https://juejin.im/post/59c8b4d56fb9a00a4843b2a6

使用 python re 处理数据时,程序提示“unbalanced parenthesis”,中文意思即为“不平衡的括号”。查看代码发现,在定义正则时使用了这样的做法:

1
re.compile(r"123" + str + "123")

然后排查发现数据中有几个例外的 str 含有括号,这些括号没有经过处理就直接传入了正则表达式中,变成了类似

1
r"123this is str)123"

的数据,造成错误。

翻阅 python re 文档发现,可以使用 re.escape 对字符串进行正则转义。如

1
this is str)

可以转成

1
this\ is\ str\)

这样传入正则就不会出现问题了。

以后写正则如果还要传字符串进去一定要细心这类问题

Google 的 Search Console 小组最近向所有站长发了一封 email,警告 Google Chrome 将从 10 月起,在包含表单但没有使用安全措施的网站中显示警告信息。

下图为我收件箱里的通知:

upload successful

如果你的网站还不支持 HTTPS,那这个通知就直接与你相关。即使你的网站并没有用到表单,也应当早日将网站迁移为 HTTPS。因为现在这项措施只不过是 Google“标识非安全网站”策略的第一步。他们在消息中明确表示:

这个新的警告仅仅是将所有通过 HTTP 提供服务的页面标记为“不安全”的长期计划的一部分。

upload successful

问题在于:安装 SSL 证书、将网站 URL 从 HTTP 转换为 HTTPS、以及将所有链接和图像链接等都换成 HTTPS 并不是一项简单的任务。谁会为了自己的个人网站去费时费钱呢?

我使用 GitHub Pages 免费托管了一系列的网站和项目,其中的一部分还使用了自定义域名。因此,我想看看我能否快速、低成本地将这些网站从 HTTP 迁移为 HTTPS。最后我找到了一种相对简单且低成本的方案,希望能够帮助到你们。下面让我们来探究一下这种方法吧。

对 GitHub Pages 强制启用 HTTPS

托管在 GitHub Pages 上的网站可以通过设置很方便地启用 HTTPS。进入项目设置页面,勾上“Enforce HTTPS”即可。

upload successful

但我们仍然需要 SSL

第一步十分的简单,但它并不符合 Google 对安全网站定义的要求。我们启用了 HTTPS 设置,但是没有为使用自定义域名的网站安装、提供 SSL 证书。直接使用 GitHub Pages 提供的网址的站点已经完全符合要求了,但是使用自定义域名的站点必须要进行一些额外的步骤,让其在域名的层面上使用安全证书。

还有个问题,SSL 证书虽然并不贵,但也需要花一笔钱,在你尽可能希望降低成本时可不想为此增加花费。所以得找个办法解决这个问题。

我们可以通过 CDN 免费试用 SSL!

在这儿就不得不提 Cloudflare 了。Cloudflare 是一个内容分发网络(CDN)提供商,同时它也提供分布式域名服务,这也意味着我们可以利用他们的网络来设置 HTTPS。使用这个服务真正的好处在于他们提供了免费的方案,让这一切成为可能。

另外,值得一提的是在 CSS-Tricks 论坛里也有许多帖子描述了使用 CDN 的好处。虽然这篇文章中主要探讨的是安全性问题,但其实 CDN 除了能帮你使用 HTTPS 之外,还是降低服务器负载、提升网站性能的绝佳方式。

在下文中,我将简述我使用 Cloudflare 连接 Github Pages 的步骤。如果你还没有 Cloudflare 账号,你可以点击这儿注册账号再跟着步骤操作。

第一步:选择“+ Add Site”选项

首先,我们需要告诉 Cloudflare 我们使用的域名。Cloudflare 将会扫描 DNS 记录,以验证域名是否存在,并检查域名的公开信息。

upload successful

第二步:查看 DNS 记录

Cloudflare 扫描 DNS 记录后会将结果展示出来供你查看。如果 Cloudflare 认为这些信息符合要求,就会在“Status”列中显示一个橙色的云的图标。你需要检查这份报告,确认其中的信息与你在域名注册商中留的信息相符,如果没问题的话,点击“Continue”按钮继续。

upload successful

第三步:获取免费方案

Cloudflare 会询问你需要哪种级别的服务。瞧~你可以在这儿选择“免费”选项。

upload successful

第四步:更新域名解析服务器(NS 服务器)

这一步中,Cloudflare 给我们提供了其服务器地址,我们要做的就是将这个地址粘贴到自己的域名注册商中的 DNS 设置里。

upload successful

这一步其实并不困难,但你可能会有些疑惑。你的域名注册商可能会提供这一步的操作指南。例如点此查看 GoDaddy 的指南,了解如何通过他们的服务更新域名解析服务器。

完成这一步之后,你的域名将会很快被映射到 Cloudflare 的服务器上,这些服务器将成为域名与 Github Pages 之间的中间层。不过,这一步需要耗费一些时间,Cloudflare 可能需要 24 小时来处理这个请求。

如果你没有用主域名,而是用了子域名来使用 GitHub Pages,则需要额外进行一步操作。打开你的 GitHub Pages 设置页面,在 DNS 设置中添加一条 CNAME 记录,设置它指向 <your-username>.github.io,其中 <your-username> 是你的 Github 账号。此外,你需要在 GitHub 项目的根目录下添加一个文件名为 CNAME 的无后缀名文本文档,其内容为你的域名。

下面的屏幕截图为在 Cloudflare 设置中将 GitHub Pages 子域名添加为 CNAME 记录的例子:

upload successful

第五步:在 Cloudflare 中启用 HTTPS

现在,我们从技术上说已经为 GitHub Pages 启用了 HTTPS,但是我们还需要在 Cloudflare 中做同样的事。Cloudflare 把这个功能称为“Crypto”,不仅强制开启了 HTTPS,还提供了我们梦寐以求的 SSL 证书。现在先让我们为 HTTPS 启用 Crypto,之后的步骤中我们会获取到证书的。

upload successful

开启“Always use HTTPS”选项:

upload successful

此时,任何来自浏览器的 HTTP 请求都会被切换成更安全的 HTTPS。我们离“取悦” Google Chrome 又进了一步。

第六步:使用 CDN

我们现在正在用 CDN 来获取 SSL 证书,所以我们还可以利用它的性能优势来得到更多的好处。我们可以通过自动压缩文件、延长浏览器缓存过期时间来提升网站性能。

选择“Speed”选项,允许 Cloudflare 自动压缩网站资源:

upload successful

我们还可以通过设置浏览器缓存过期时间来最大化地提升性能:

upload successful

将过期时间设置为比默认选项更长,可以让浏览器在访问网站时不再需要每次都去请求那些没有变更过的网站资源。这将让访客在一个月内再次访问你的网站时节省额外的下载量。

第七步:使用安全的外部资源

如果你的网站还使用了一些外部资源(我们很多人都这么做),那么还需要确保这些外部资源是安全的。例如,如果你使用了一个 Javascript 框架,但没有使用 HTTPS 源,那么 Google Chrome 将会认为其降低了我们网站的安全性,因此我们需要对其进行改进。

如果你使用的外部资源不提供 HTTPS 源,那么你可以考虑自己对其进行托管。反正我们现在已经有了 CDN,做托管服务的负载并不成问题。

第八步:激活 SSL

已经做到这一步啦!我们已经在 GitHub Pages 设置中开启了 HTTPS,现在还缺少自定义域名与 GitHub Pages 的连接证书。Cloudflare 提供了免费的 SSL 证书,我们可以在网站中使用它。

打开 Cloudflare 的 Crypto 设置页面,确认 SSL 证书处于激活状态:

upload successful

如果证书处于激活状态,在主菜单中切换到“Page Rules”页面,选择“Create Page Rule”选项:

upload successful

然后点击“Add a Setting”,选择“Always use HTTPS”选项:

upload successful

点击“Save and Deply”,恭喜你!现在,我们拥有了一个在 Google Chrome 眼中完全安全的网站,并且在迁移的过程中我们并不需要接触、修改很多代码。

总结

Google 这样推进 HTTPS 意味着前端开发者们在开发自己的网站、公司网站、客户网站的时候需要优先考虑 SSL 支持。这一举措将会促使我们将站点向 HTTPS 迁移。而使用 CDN 可以让我们使用免费的 SSL 并提升网站性能,如此超值的事何乐而不为呢?

你记录过迁移到 HTTPS 的经历吗?在评论里留言你的迁移方法,让我们相互对比吧。

享受你的既安全又快速的网站吧!

发布于掘金 https://juejin.im/post/59b129365188253da63829ad

upload successful 位于希腊爱琴海伊莫洛维里的一个 Airbnb 民宿的美好风景

简介

数据产品一直是 Airbnb 服务的重要组成部分,不过我们很早就意识到开发一款数据产品的成本是很高的。例如,个性化搜索排序可以让客户更容易发现中意的房屋,智能定价可以让房东设定更具竞争力的价格。然而,需要许多数据科学家和工程师付出许多时间和精力才能做出这些产品。

最近,Airbnb 机器学习的基础架构进行了改进,使得部署新的机器学习模型到生产环境中的成本降低了许多。例如,我们的 ML Infra 团队构建了一个通用功能库,这个库让用户可以在他们的模型中应用更多高质量、经过筛选、可复用的特征。数据科学家们也开始将一些自动化机器学习工具纳入他们的工作流中,以加快模型选择的速度以及提高性能标准。此外,ML Infra 还创建了一个新的框架,可以自动将 Jupyter notebook 转换成 Airflow pipeline 能接受的格式。

在本文中,我将介绍这些工具是如何协同运作来加快建模速度,从而降低开发 LTV 模型(预测 Airbnb 民宿价格)总体成本的。

什么是 LTV?

LTV 全称 Customer Lifetime Value,意为“客户终身价值”,是电子商务、市场公司中很流行的一种概念。它定义了在未来一个时间段内用户预期为公司带来的收益,通常以美元为单位。

在一些例如 Spotify 或者 Netflix 之类的电子商务公司里,LTV 通常用于制定产品定价(例如订阅费等)。而在 Airbnb 之类的市场公司里,知晓用户的 LTV 将有助于我们更有效地分配营销渠道的预算,更明确地根据关键字做在线营销报价,以及做更好的类目细分。

我们可以根据过去的数据来计算历史值,当然也可以进一步使用机器学习来预测新登记房屋的 LTV。

LTV 模型的机器学习工作流

数据科学家们通常比较熟悉和机器学习任务相关的东西,例如特征工程、原型制作、模型选择等。然而,要将一个模型原型投入生产环境中需要的是一系列数据工程技术,他们可能对此不太熟练。

upload successful

不过幸运的是,我们有相关的机器学习工具,可以将具体的生产部署工作流从机器学习模型的分析建立中分离出来。如果没有这些神奇的工具,我们就无法轻松地将模型应用于生产环境。下面将通过 4 个主题来分别介绍我们的工作流以及各自用到的工具:

  • 特征工程:定义相关特征
  • 原型设计与训练:训练一个模型原型
  • 模型选择与验证:选择模型以及调参
  • 生产部署:将选择好的模型原型投入生产环境使用

特征工程

使用工具:Airbnb 内部特征库 — Zipline

任何监督学习项目的第一步都是去找到会影响到结果的相关特征,这一个过程被称为特征工程。例如在预测 LTV 时,特征可以是某个房源房屋在接下来 180 天内的可使用天数所占百分比,或者也可以是其与同市场其它房屋定价的差异。

在 Airbnb 中,要做特征工程一般得从头开始写 Hive 查询语句来创建特征。但是这个工作相当无聊,而且需要花费很多时间。因为它需要一些特定的领域知识和业务逻辑,也因此这些特征 pipeline 并不容易共享或复用。为了让这项工作更具可扩展性,我们开发了 Zipline —— 一个训练特征库。它可以提供不同粒度级别(例如房主、客户、房源房屋及市场级别)的特征。

这个内部工具“多源共享”的特性让数据科学家们可以在过去的项目中找出大量高质量、经过审查的特征。如果没有找到希望提取的特征,用户也可以写一个配置文件来创建他自己需要的特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
source: {
type: hive
query:"""
SELECT
id_listing as listing
, dim_city as city
, dim_country as country
, dim_is_active as is_active
, CONCAT(ds, ' 23:59:59.999') as ts
FROM
core_data.dim_listings
WHERE
ds BETWEEN '{{ start_date }}' AND '{{ end_date }}'
"""
dependencies: [core_data.dim_listings]
is_snapshot: true
start_date: 2010-01-01
}
features: {
city: "City in which the listing is located."
country: "Country in which the listing is located."
is_active: "If the listing is active as of the date partition."
}

在构建训练集时,Zipline 将会找出训练集所需要的特征,自动的按照 key 将特征组合在一起并填充数据。在构造房源 LTV 模型时,我们使用了一些 Zipline 中已经存在的特征,还自己写了一些特征。模型总共使用了 150 多个特征,其中包括:

  • 位置:国家、市场、社区以及其它地理特征
  • 价格:过夜费、清洁费、与相似房源的价格差异
  • 可用性:可过夜的总天数,以及房主手动关闭夜间预订的占比百分数
  • 是否可预订:预订数量及过去 X 天内在夜间订房的数量
  • 质量:评价得分、评价数量、便利设施
upload successful

实例数据集

在定义好特征以及输出变量之后,就可以根据我们的历史数据来训练模型了。

原型设计与训练

使用工具:Python 机器学习库scikit-learn

以前面的训练集为例,我们在做训练前先要对数据进行一些预处理:

  • 数据插补:我们需要检查是否有数据缺失,以及它是否为随机出现的缺失。如果不是随机现象,我们需要弄清楚其根本原因;如果是随机缺失,我们需要填充空缺数据。
  • 对分类进行编码:通常来说我们不能在模型里直接使用原始的分类,因为模型并不能去拟合字符串。当分类数量比较少时,我们可以考虑使用 one-hot encoding 进行编码。如果分类数量比较多,我们就会考虑使用 ordinal encoding, 按照分类的频率计数进行编码。

在这一步中,我们还不知道最有效的一组特征是什么,因此编写可快速迭代的代码是非常重要的。如 Scikit-LearnSpark 等开源工具的 pipeline 结构对于原型构建来说是非常方便的工具。Pipeline 可以让数据科学家们设计蓝图,指定如何转换特征、训练哪一个模型。更具体来说,可以看下面我们 LTV 模型的 pipeline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
transforms = []

transforms.append(
('select_binary', ColumnSelector(features=binary))
)

transforms.append(
('numeric', ExtendedPipeline([
('select', ColumnSelector(features=numeric)),
('impute', Imputer(missing_values='NaN', strategy='mean', axis=0)),
]))
)

for field in categorical:
transforms.append(
(field, ExtendedPipeline([
('select', ColumnSelector(features=[field])),
('encode', OrdinalEncoder(min_support=10))
])
)
)

features = FeatureUnion(transforms)

在高层设计时,我们使用 pipeline 来根据特征类型(如二进制特征、分类特征、数值特征等)来指定不同特征中数据的转换方式。最后使用 FeatureUnion 简单将特征列组合起来,形成最终的训练集。

使用 pipeline 开发原型的优势在于,它可以使用 data transforms 来避免繁琐的数据转换。总的来说,这些转换是为了确保数据在训练和评估时保持一致,以避免将原型部署到生产环境时出现的数据不一致。

另外,pipeline 还可以将数据转换过程和训练模型过程分开。虽然上面代码中没有,但数据科学家可以在最后一步指定一种 estimator(估值器)来训练模型。通过尝试使用不同的估值器,数据科学家可以为模型选出一个表现最佳的估值器,减少模型的样本误差。

模型选择与验证

使用工具:各种自动机器学习框架

如上一节所述,我们需要确定候选模型中的哪个最适合投入生产。为了做这个决策,我们需要在模型的可解释性与复杂度中进行权衡。例如,稀疏线性模型的解释性很好,但它的复杂度太低了,不能很好地运作。一个足够复杂的树模型可以拟合各种非线性模式,但是它的解释性很差。这种情况也被称为偏差(Bias)和方差(Variance)的权衡

upload successful

上图引用自 James、Witten、Hastie、Tibshirani 所著《R 语言统计学习》

在保险、信用审查等应用中,需要对模型进行解释。因为对模型来说避免无意排除一些正确客户是很重要的事。不过在图像分类等应用中,模型的高性能比可解释更重要。

由于模型的选择相当耗时,我们选择采用各种自动机器学习工具来加速这个步骤。通过探索大量的模型,我们最终会找到表现最好的模型。例如,我们发现 XGBoost (XGBoost) 明显比其他基准模型(比如 mean response 模型、岭回归模型、单一决策树)的表现要好。

upload successful

上图:我们通过比较 RMSE 可以选择出表现更好的模型

鉴于我们的最初目标是预测房源价格,因此我们很舒服地在最终的生产环境中使用 XGBoost 模型,比起可解释性它更注重于模型的弹性。

生产部署

使用工具:Airbnb 自己写的 notebook 转换框架 — ML Automator

如开始所说,构建生产环境工作流和在笔记本上构建一个原型是完全不同的。例如,我们如何进行定期的重训练?我们如何有效地评估大量的实例?我们如何建立一个 pipeline 以随时监视模型性能?

在 Airbnb,我们自己开发了一个名为 ML Automator 的框架,它可以自动将 Jupyter notebook 转换为 Airflow 机器学习 pipeline。该框架专为熟悉使用 Python 开发原型,但缺乏将模型投入生产环境经验的数据科学家准备。

upload successful

ML Automator 框架概述(照片来源:Aaron Keys)

  • 首先,框架要求用户在 notebook 中指定模型的配置。该配置将告诉框架如何定位训练数据表,为训练分配多少计算资源,以及如何计算模型评价分数。
  • 另外,数据科学家需要自己写特定的 fittransform 函数。fit 函数指定如何进行训练,而 transform 函数将被 Python UDF 封装,进行分布式计算(如果有需要)。

下面的代码片段展示了我们 LTV 模型中的 fittransform 函数。fit 函数告诉框架需要训练 XGBoost 模型,同时转换器将根据我们之前定义的 pipeline 转换数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def fit(X_train, y_train):
import multiprocessing
from ml_helpers.sklearn_extensions import DenseMatrixConverter
from ml_helpers.data import split_records
from xgboost import XGBRegressor

global model

model = {}
n_subset = N_EXAMPLES
X_subset = {k: v[:n_subset] for k, v in X_train.iteritems()}
model['transformations'] = ExtendedPipeline([
('features', features),
('densify', DenseMatrixConverter()),
]).fit(X_subset)

# 并行使用转换器
Xt = model['transformations'].transform_parallel(X_train)

# 并行进行模型拟合
model['regressor'] = XGBRegressor().fit(Xt, y_train)

def transform(X):
# return dictionary
global model
Xt = model['transformations'].transform(X)
return {'score': model['regressor'].predict(Xt)}

一旦 notebook 完成,ML Automator 将会把训练好的模型包装在 Python UDF 中,并创建一个如下图所示的 Airflow pipeline。数据序列化、定期重训练、分布式评价等数据工程任务都将被载入到日常批处理作业中。因此,这个框架显著降低了数据科学家将模型投入生产的成本,就像有一位数据工程师在与科学家一起工作一样!

upload successful

我们 LTV 模型在 Airflow DAG 中的图形界面,运行于生产环境中

Note:除了模型生产化之外,还有一些其它项目(例如跟踪模型随着时间推移的性能、使用弹性计算环境建模等)我们没有在这篇文章中进行介绍。这些都是正在进行开发的热门领域。

经验与展望

过去的几个月中,我们的数据科学家们与 ML Infra 密切合作,产生了许多很好的模式和想法。我们相信这些工具将会为 Airbnb 开发机器学习模型开辟新的范例。

  • 首先,显著地降低了模型的开发成本:通过组合各种不同的独立工具的优点(Zipline 用于特征工程、Pipeline 用于模型原型设计、AutoML 用于模型选择与验证,以及最后的 ML Automator 用于模型生产化),我们大大减短了模型的开发周期。
  • 其次,notebook 的设计降低了入门门槛:还不熟悉框架的数据科学家可以立即得到大量的真实用例。在生产环境中,可以确保 notebook 是正确、自解释、最新的。这种设计模式受到了新用户的好评。
  • 因此,团队将更愿意关注机器学习产品的 idea:在本文撰写时,我们还有其它几支团队在采用类似的方法探索机器学习产品的 idea:为检查房源队列进行排序、预测房源是否会增加合伙人、自动标注低质量房源等等。

我们对这个框架和它带来的新范式的未来感到无比的兴奋。通过缩小原型与生产环境间的差距,我们可以让数据科学家和数据工程师更多去追求端到端的机器学习项目,让我们的产品做得更好。


想使用或者一起开发这些机器学习工具吗?我们正在寻找 能干的你加入我们的数据科学与分析团队


特别感谢参与这项工作的Data Science&ML Infra团队的成员:Aaron Keys, Brad Hunter, Hamel Husain, Jiaying Shi, Krishna Puttaswamy, Michael Musson, Nick Handel, Varant Zanoyan, Vaughn Quoss* 等人。另外感谢 Gary Tang, Jason Goodman, Jeff Feng, Lindsay Pettingill 给本文提的意见。*

发布于掘金 https://juejin.im/post/59acfc336fb9a0249471e47d

在我开始学习 Go 语言时已经有一些 Web 开发经验了,但是并没有直接操作 Cookie 的经验。我之前做过 Rails 开发,当我不得不需要在 Rails 中读写 Cookie 时,并不需要自己去实现各种安全措施。

瞧瞧,Rails 默认就自己完成了大多数的事情。你不需要设置任何 CSRF 策略,也无需特别去加密你的 Cookie。在新版的 Rails 中,这些事情都是它默认帮你完成的。

而使用 Go 语言开发则完全不同。在 Golang 的默认设置中,这些事都不会帮你完成。因此,当你想要开始使用 Cookie 时,了解各种安全措施、为什么要使用这些措施、以及如何将这些安全措施集成到你的应用中是非常重要的事。希望本文能帮助你做到这一点。

注意:我并不想引起关于 Go 与 Reils 两者哪种更好的论战。两者各有优点,但在本文中我希望能着重讨论 Cookie 的防护,而不是去争论 Rails 和 Go 哪个好。

在进入 Cookie 防护相关的内容前,我们必须要理解 Cookie 究竟是什么。从本质上说,Cookie 就是存储在终端用户计算机中的键值对。因此,使用 Go 创建一个 Cookie 需要做的事就是创建一个包含键名、键值的 http.Cookie 类型字段,然后调用 http.SetCookie 函数通知终端用户的浏览器设置该 Cookie。

写成代码之后,它看起来类似于这样:

1
2
3
4
5
6
7
func someHandler(w http.ResponseWriter, r *http.Request) {
c := http.Cookie{
Name: "theme",
Value: "dark",
}
http.SetCookie(w, &c)
}

http.SetCookie 函数并不会返回错误,但它可能会静默地移除无效的 Cookie,因此使用它并不是什么美好的经历。但它既然这么设计了,就请你在使用这个函数的时候一定要牢记它的特性。

虽然这好像是在代码中“设定”了一个 Cookie,但其实我们只是在我们返回 Response 时发送了一个 "Set-Cookie" 的 Header,从而定义需要设置的 Cookie。我们不会在服务器上存储 Cookie,而是依靠终端用户的计算机创建与存储 Cookie。

我要强调上面这一点,因为它存在非常严重的安全隐患:我们不能控制这些数据,而终端用户的计算机(以及用户)才能控制这些数据。

当读取与写入终端用户控制的数据时,我们都需要十分谨慎地对数据进行处理。恶意用户可以删除 Cookie、修改存储在 Cookie 中的数据,甚至我们可能会遇到中间人攻击,即当用户向服务器发送数据时,另有人试图窃取 Cookie。

根据我的经验,Cookie 相关的安全性问题大致分为以下五大类。下面我们先简单地看一看,本文的剩余部分将详细讨论每个分类的细节问题与解决对策。

1. Cookie 窃取 - 攻击者会通过各种方式来试图窃取 Cookie。我们将讨论如何防范、规避这些方式,但是归根结底我们并不能完全阻止设备上的物理类接触。

2. Cookie 篡改 - Cookie 中存储的数据可以被用户有意或无意地修改。我们将讨论如何验证存储在 Cookie 中的数据确实是我们写入的合法数据

3. 数据泄露 - Cookie 存储在终端用户的计算机上,因此我们需要清楚地意识到什么数据是能存储在 Cookie 中的,什么数据是不能存储在 Cookie 中的,以防其发生数据泄露。

4. 跨站脚本攻击(XSS) - 虽然这条与 Cookie 没有直接关系,但是 XSS 攻击在攻击者能获取 Cookie 时危害更大。我们应该考虑在非必须的时候限制脚本访问 Cookie。

5. 跨站请求伪造(CSRF) - 这种攻击常常是由于使用 Cookie 存储用户登录会话造成的。因此我们将讨论在这种情景下如何防范这种攻击。

如我前面所说,在下文中我们将分别解决这些问题,让你最终能够专业地将你的 Cookie 装进保险柜。

Cookie 窃取攻击就和它字面意思一样 —— 某人窃取了正常用户的 Cookie,然后一般用来将自己伪装成那个正常用户。

Cookie 通常是被以下方式中的某种窃取:

  1. 中间人攻击,或者是类似的其它攻击方式,归纳一下就是攻击者拦截你的 Web 请求,从中窃取 Cookie。
  2. 取得硬件的访问权限。

阻止中间人攻击的终极方式就是当你的网站使用 Cookie 时,使用 SSL。使用 SSL 时,由于中间人无法对数据进行解密,因此外人基本上没可能在请求的中途获取 Cookie。

可能你会觉得“哈哈,中间人攻击不太可能…”,我建议你看看 firesheep,这个简单的工具,它足以说明在使用公共 wifi 时窃取未加密的 Cookie 是一件很轻松的事情。

如果你想确保这种事情不发生在你的用户中,请使用 SSL!试试使用 Caddy Server 进行加密吧。它经过简单的配置就能投入生产环境中。例如,你可以使用下面四行代码轻松让你的 Go 应用使用代理:

1
2
3
4
calhoun.io {
gzip
proxy / localhost:3000
}

然后 Caddy 会为你自动处理所有与 SSL 有关的事务。

防范通过访问硬件来窃取 Cookie 是十分棘手的事情。我们不能强制我们的用户使用高安全性系统,也不能逼他们为电脑设置密码,所以总会有他人坐在电脑前偷走 Cookie 的风险。此外,Cookie 也可能被病毒窃取,比如用户打开了某些钓鱼邮件时就会出现这种情况。

不过这些都容易被发现。例如,如果有人偷了你的手表,当你发现表不在手上时你立马就会注意到它被偷了。然而 Cookie 还可以被复制,这样任何人都不会意识到它已经丢了。

虽然不是万无一失,但你还是可以用一些技术来猜测 Cookie 是否被盗了。例如,你可以追踪用户的登录设备,要求他们重新输入密码。你还可以跟踪用户的 IP 地址,当其在可疑地点登录时通知用户。

所有的这些解决方案都需要后端做更多的工作来追踪数据,如果你的应用需要处理一些敏感信息、金钱,或者它的收益可观的话,请在安全方面投入更多精力。

也就是说,对于大多数只是作为过渡版本的应用来说,使用 SSL 就足够了。

请直面这种情况 —— 可能有一些混蛋突然就想看看你设的 Cookie,然后修改它的值。也可能他是出于好奇才这么做的,但是还是请你为这种可能发生的情况做好准备。

在一些情景中,我们对此并不在意。例如,我们给用户定义一种主题设置时,并不会关心用户是否改变了这个设置。当这个 Cookie 过期时,就会恢复默认的主题设置,并且如果用户设置其为另一个有效的主题时我们可以让他正常使用那个主题,这并不会对系统造成任何损失。

但是在另一些情况下,我们需要格外小心。编辑会话 Cookie 冒充另一个用户产生的危害比改个主题大得多。我们绝不想看到张三假装自己是李四。

我们将介绍两种策略来检测与防止 Cookie 被篡改。

1. 对数据进行数字签名

对数据进行数字签名,即对数据增加一个“签名”,这样能让你校验数据的可靠性。这种方法并不需要对终端用户的数据进行加密或隐藏,只要对 Cookie 增加必要的签名数据,我们就能检测到用户是否修改数据。

这种保护 Cookie 的方法原理是哈希编码 —— 我们对数据进行哈希编码,接着将数据与它的哈希编码同时存入 Cookie 中。当用户发送 Cookie 给我们时,再对数据进行哈希计算,验证此时的哈希值与原始哈希值是否匹配。

我们当然不会想看到用户也创建一个新的哈希来欺骗我们,因此你可以使用一些类似 HMAC 的哈希算法来使用秘钥对数据进行哈希编码。这样就能防范用户同时编辑数据与数字签名(即哈希值)。

JSON Web Tokens(JWT) 默认内置了数字签名功能,因此你可能对这种方法比较熟悉。

在 Go 中,可以使用类似 Gorilla 的 securecookie 之类的 package,你可以在创建 SecureCookie 时使用它来保护你的 Cookie。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 推荐使用 32 字节或 64 字节的 hashKey
// 此处为了简洁故设为了 “very-secret”
var hashKey = []byte("very-secret")
var s = securecookie.New(hashKey, nil)

func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
encoded, err := s.Encode("cookie-name", "cookie-value")
if err == nil {
cookie := &http.Cookie{
Name: "cookie-name",
Value: encoded,
Path: "/",
}
http.SetCookie(w, cookie)
fmt.Fprintln(w, encoded)
}
}

然后你可以在另一个处理 Cookie 的函数中同样使用 SecureCookie 对象来读取 Cookie。

1
2
3
4
5
6
7
8
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie("cookie-name"); err == nil {
var value string
if err = s.Decode("cookie-name", cookie.Value, &value); err == nil {
fmt.Fprintln(w, value)
}
}
}

以上样例来源于 http://www.gorillatoolkit.org/pkg/securecookie.

注意:这儿的数据并不是进行了加密,而只是进行了编码。我们会在“数据泄露”一章讨论如何对数据进行加密。

这种模式还需要注意的是,如果你使用这种方式进行身份验证,请遵循 JWT 的模式,将登录过期日期和用户数据同时进行签名。你不能只凭 Cookie 的过期日期来判断登录是否有效,因为存储在 Cookie 上的日期并未经过签名,且用户可以创建一个永不过期的新 Cookie,将原 Cookie 的内容复制进去就得到了一个永远处于登录状态的 Cookie。

2. 进行数据混淆

还有一种解决方案可以隐藏数据并防止用户造假。例如,不要这样存储 Cookie:

1
2
3
4
5
// 别这么做
http.Cookie{
Name: "user_id",
Value: "123",
}

我们可以存储一个值来映射存在数据库中的真实数据。通常使用 Session ID 或者 remember token 来作为这个值。例如我们有一个名为 remember_tokens 的表,这样存储数据:

1
2
remember_token: LAKJFD098afj0jasdf08jad08AJFs9aj2ASfd1
user_id: 123

在 Cookie 中,我们仅存储这个 remember token。如果用户想伪造 Cookie 也会无从下手。它看上去就是一堆乱码。

之后当用户要登陆我们的应用时,再根据 remember token 在数据库中查询,确定用户具体的登录状态。

为了让此措施正常工作,你需要确保你的混淆值有以下特性:

  • 能映射到用户数据(或其它资源)
  • 随机
  • 熵值高
  • 可被无效化(例如在数据库中删除、修改 token 值)

这种方法也有一个缺点,就是在用户访问每个需要校验权限的页面时都得进行数据库查询。不过这个缺点很少有人注意,而且可以通过缓存等技术来减小数据库查询的开销。这种方法的升级版就是 JWT,应用这种方法你可以随时使会话无效化。

注意:尽管目前 JWT 收到了大多数 JS 框架的追捧,但上文这种方法是我了解的最常用的身份验证策略。

数据泄露

在真正出现数据泄露前,通常需要另一种攻击向量 —— 例如 Cookie 窃取。然而还是很难去正确地判断并提防数据泄露的发生。因为仅仅是 Cookie 发生了泄露并不意味着攻击者也得到了用户的账户密码。

无论何时,都应当减少存储在 Cookie 中的敏感数据。绝不要将用户密码之类的东西存在 Cookie 中,即使密码已经经过了编码也不要这么做。这篇文章 给出了几个开发者无意间将敏感数据存储在 Cookie 或 JWT 中的实例,由于(JWT 的 payload)是 base64 编码,没有经过任何加密,因此任何人都可以对其进行解码。

出现数据泄露可是犯了大错。如果你担心你不小心存储了一些敏感数据,我建议你使用如 Gorilla 的 securecookie 之类的 package。

前面我们讨论了如何对你的 Cookie 进行数字签名,其实 securecookie 也可以用于加密与解密你的 Cookie 数据,让你的数据不能被轻易地解码并读取。

使用这个 package 进行加密,你只需要在创建 SecureCookie 实例时传入一个“块秘钥”(blockKey)即可。

1
2
3
4
var hashKey = []byte("very-secret")
// 增加这一部分进行加密
var blockKey = []byte("a-lot-secret")
var s = securecookie.New(hashKey, blockKey)

其它所有东西都和前面章节的数字签名中的样例一致。

再次提醒,你不应该在 Cookie 中存储任何敏感数据,尤其不能存储密码之类的东西。加密仅仅是一项为数据增加一部分安全性,使其成为”半敏感数据“数据的技术而已。

跨站脚本攻击(XSS)

跨站脚本(Cross-site scripting)也经常被记为 XSS,及有人试图将一些不是你写的 JavaScript 代码注入你的网站中。但由于其攻击的机理,你无法知道正在浏览器中运行的 JavaScript 代码到底是不是你的服务器提供的代码。

无论何时,你都应该尽量去阻止 XSS 攻击。在本文中我们不会深入探讨这种攻击的具体细节,但是以防万一我建议你在非必要的情况下禁止 JavaScript 访问 Cookie 的权限。在你需要这个权限的时候你可以随时开启它,所以不要让它成为你的网站安全性脆弱的理由。

在 Go 中完成这点很简单,只需要在创建 Cookie 时设置 HttpOnly 字段为 true 即可。

1
2
3
4
5
cookie := http.Cookie{
// true 表示脚本无权限,只允许 http request 使用 Cookie。
// 这与 Http 与 Https 无关。
HttpOnly: true,
}

CSRF(跨站请求伪造)

CSRF 发生的情况为某个用户访问别人的站点,但那个站点有一个能提交到你的 web 应用的表单。由于终端用户提交表单时的操作不经由脚本,因此浏览器会将此请求设为用户进行的操作,将 Cookie 附上表单数据同时发送。

乍一看似乎这没什么问题,但是如果外部网站发送一些用户不希望发送的数据时会发生什么呢?例如,badsite.com 中有个表单,会提交请求将你的 100 美元转到他们的账户中,而 chase.com 希望你在它这儿登录你的银行账户。这可能会导致在终端用户不知情的情况下钱被转走。

Cookie 不会直接导致这样的问题,不过如果你使用 Cookie 作为身份验证的依据,那你需要使用 Gorilla 的 csrf 之类的 package 来避免 CSRF 攻击。

这个 package 将会提供一个 CSRF token,插入你网站的每个表单中,当表单中不含 token 时,csrf package 中间件将会阻止表单的提交,使得别的网站不能欺骗用户在他们那儿向你的网站提交表单。

更多关于 CSRF 攻击的资料请参阅:

我们要讨论的最后一件事与特定的攻击无关,更像是一种指导原则。我建议在使用 Cookie 时尽量限制其权限,仅在你需要时开发相关权限。

前面讨论 XSS 时我也简单的提到过这点,但一般的观点是你需要尽可能限制对 Cookie 的访问。例如,如果你的 Web 应用没有使用子域名,那你就不应该赋予 Cookie 所有子域的权限。不过这是 Cookie 的默认值,因此其实你什么都不用做就能将 Cookie 的权限限制在某个特定域中。

但是,如果你需要与子域共享 Cookie,你可以这么做:

1
2
3
4
5
6
c := Cookie{
// 根据主机模式的默认设置,Cookie 进行的是精确域名匹配。
// 因此请仅在需要的时候开启子域名权限!
// 下面的代码可以让 Cookie 在 yoursite.com 的任何子域下工作:
Domain: "yoursite.com",
}

欲了解更多有关域的信息,请参阅 https://tools.ietf.org/html/rfc6265#section-5.1.3。你也可以在这儿阅读源码,参阅其默认设置:https://golang.org/src/net/http/cookie.go#L157.

你可以参阅 这个 stackoverflow 的问题 了解更多信息,弄明白为什么在为子域使用 Cookie 时不需要提供子域前缀.此外 Go 源码链接中也可以看到如果你提供前缀名的话会被自动去除。

除了将 Cookie 的权限限制在特定域上之外,你还可以将 Cookie 限制于某个特定的目录路径中。

1
2
3
4
5
c := Cookie{
// Defaults 设置为可访问应用的任何路径,但你也可以
// 进行如下设置将其限制在特定子目录下:
Path: "/app/",
}

还有你也可以对其设置路径前缀,例如 /blah/,你可以参阅下面这篇文章了解更多这个字段的使用方法:https://tools.ietf.org/html/rfc6265#section-5.1.4.

为什么我不使用 JWT?

就知道肯定会有人提出这个问题,下面让我简单解释一下。

可能有很多人和你说过,Cookie 的安全性与 JWT 一样。但实际上,Cookie 与 JWT 解决的并不是相同的问题。比如 JWT 可以存储在 Cookie 中,这和将其放在 Header 中的实际效果是一样的。

另外,Cookie 可用于无需验证的数据,在这种情况下了解如何增加 Cookie 的安全性也是必要的。

发布于掘金 https://juejin.im/post/59aa7a4d6fb9a0249c007e16

Version:

Ionic: 3.9.2

Cordova: 7.0.1

Using 'ionic cordova resources' will generate all-size splashs and icons for selected platforms automatically.

But this method depends on cloud service of ionic, so that when you can't connect to network, this method would be failed.

There is a awesome tool can deal with these problem: cordova-resgen.

https://github.com/helixhuang/ionic-resources

This tool base on cordova-splash and cordova-icon, using graphicsmagic to cut pictures.

Usage:

1
2
3
4
5
brew install graphicsmagick
sudo npm install cordova-resgen -g

cd your-project
cordova-resgen

upload successful

图为一位盲人正在阅读盲文(图片链接

根据世界健康组织的统计,全球约有 2.85 亿位视力障碍人士,仅美国就有 810 万网民患视力障碍。

在我们视力正常的人看来,互联网是一个充满了文字、图片、视频等事物的地方,然而对于视力障碍人士来说却并不是这样的。有一种可以读出网页中文字和元数据的工具叫做屏幕阅读器,然而这种工具的作用十分有限,仅能让人看到网页的一部分文本。虽然一些开发人员花时间去改进他们的网站,为视障人士添加图片的描述性文字,但是绝大多数程序员都不会花时间去做这件公认冗长乏味的事情。

所以,我决定做这么一个工具,来帮助视障人士通过 AI 的力量来“看”互联网。我给它起名为“Auto Alt Text”(自动 Alt 文本添加器),是一个 Chrome 拓展插件,可以让用户在图片上点击右键后得到场景描述 —— 最开始是要这么做的。

您可以观看 这个视频,了解它是如何运作的,然后 下载它并亲自试一试吧!

为什么我想做 Auto Alt Text:

我曾经是不想花时间为图片添加描述的开发者中的一员。对那时的我来说,无障碍永远是“考虑考虑”的事,直到有一天我收到了来自我的一个项目的用户的邮件。

upload successful

邮件内容如下:“你好,Abhinav,我看了你的 flask-base 项目,我觉得它非常适合我的下个工程。感谢你开发了它。不过我想让你知道,你应该为你 README 中的图片加上 alt 描述。我是盲人,用了很长一段时间才弄清楚它们的内容 :/来自某人”

在收到邮件的时候,无障碍功能的开发是放在我开发队列的最后面的,基本上它就是个“事后有空再添加”的想法而已。但是,这封邮件唤醒了我。在互联网中,有许多的人需要无障碍阅读功能来理解网站、应用、项目等事物的用途。

“现在 Web 中充满了缺失、错误或者没有替代文本的图片” —— WebAIM(犹他州立大学残疾人中心)

用 AI(人工智能)来挽救:

现在其实有一些方法来给图像加描述文字;但是,大多数方法都有一些缺点:

  1. 它们反应很慢,要很长时间才能返回描述文字。
  2. 它们是半自动化的(即需要人类手动按需标记描述文字)。
  3. 制作、维护它们需要高昂的代价。

现在,通过创建神经网络,这些问题都能得到解决。最近我接触、学习了 Tensorflow —— 一个用于机器学习开发的开源库,开始深入研究机器学习与 AI。Tensorflow 使开发人员能够构建可用于完成从对象检测到图像识别的各种任务的高鲁棒模型。

在做了一些研究之后,我找到了一篇 Vinyals 写的论文《Show and Tell: Lessons learned from the 2015 MSCOCO Image Captioning Challenge》。这些研究者们创建了一个深度神经网络,可以以语义化方式描述图片的内容。

upload successful

im2txt 的实例来自 im2txt Github Repository

im2txt 的技术细节:

这个模型的机制相当的精致,但是它基本上是一个“编码器 - 解码器”的方案。首先图片会传入一个名为 Inception v3 的卷积神经网络进行图片分类,接着编码好的图片送入 LSTM 网络中。LSTM 是一种专门用于序列模型/时间敏感信息的神经网络层。最后 LSTM 通过组合设定好的单词,形成一句描述图片内容的句子。LSTM 通过求单词集中每个单词在句子中出现的似然性,分别计算第一个词出现的概率分布、第二个词出现的概率分布……直到出现概率最大的字符为“.”,为句子加上最后的句号。

upload successful

图为此神经网络的概况(图片来自 im2txt Github repository

根据 Github 库中的说明,这个模型在 Tesla k20m GPU 上的训练时间大约为 1-2 周(在我笔记本的标准 CPU 上计算需要更多的时间)。不过值得庆幸的是,Tensorflow 社区提供了一个已经训练好的模型。

使用 box + Lamdba 解决问题:

在运行模型时,我试图使用 Bazel 来运行模型(Bazel 是一个用于将 tensorflow 模型解包成可运行脚本的工具)。但是,当命令行运行时,它需要大约 15 秒钟的时间才能从获取一张图片的结果!解决问题的唯一办法就是让 Tensorflow 的整个 Graph 都常驻内存,但是这样需要这个程序全天候运行。我计划将这个模型挂在 AWS Elasticbeanstalk 上,在这个平台上是以小时为单位为计算时间计费的,而我们要维持应用程序常驻,因此并不合适(它完全匹配了前面章节所说的图片描述软件缺点的第三条缺点)。因此,我决定使用 AWS Lambda 来完成所有工作。

Lambda 是一种无服务器计算服务,价格很低。此外,它会在计算服务激活时按秒收费。Lambda 的工作原理很简单,一旦应用收到了用户的请求,Lambda 就会将应用程序的映象激活,返回 response,然后再停止应用映象。如果收到多个并发请求,它会唤起多个实例以拓展负载。另外,如果某个小时内应用不断收到请求,它将会保持应用程序的激活状态。因此,Lambda 服务非常符合我的这个用例。

upload successful

图为 AWS API Gateway + AWS = ❤️ (图片链接)

使用 Lambda 的问题就在于,我必须要为 im2txt 模型创建一个 API。另外,Lambda 对于以功能形式加载的应用有空间限制。上传整个应用程序的 zip 包时,最终文件大小不能超过 250 MB。这个限制是一个麻烦事,因为 im2txt 的模型就已经超过 180 MB 了,再加上它运行需要的依赖文件就已经超过 350 MB 了。我尝试将程序的一部分传到 S3 服务上,然后在 Lambda 实例运行再去下载相关文件。然而,Lambda 上一个应用的总存储限制为 512 MB,而我的应用程序已经超过限制了(总共约 530 MB)。

为了减小项目的大小,我重新配置了 im2txt,只下载精简过的模型,去掉了没用的一些元数据。这样做之后,我的模型大小减小到了 120 MB。接着,我找到了一个最小依赖的 lambda-packs,不过它仅有早期版本的 python 和 tensorflow。我将 python 3.6 语法和 tensorflow 1.2 的代码进行了降级,经过痛苦的降级过程后,我最终得到了一个总大小约为 480 MB 的包,小于 512 MB 的限制。

为了保持应用的快速响应,我创建了一个 CloudWatch 函数,让 Lambda 实例保持”热“状态,使应用始终处于激活态。接着,我添加了一些函数用于处理不是 JPG 格式的图片,在最后,我做好了一个能提供服务的 API。这些精简工作让应用在大多数情况下能够于 5 秒之内返回 response。

upload successful

上图为 API 提供的图片可能内容的概率

此外,Lambda 的价格便宜的令人惊讶。以现在的情况,我可以每个月免费分析 60,952 张图片,之后的图片每张仅需 0.0001094 美元(这意味着接下来的 60,952 张图像约花费 6.67 美元)。

有关 API 的更多信息,请参考 repo:https://github.com/abhisuri97/auto-alt-text-lambda-api

剩下的工作就是将其打包为 Chrome 拓展插件,以方便用户使用。这个工作没啥挑战性(仅需要向我的 API 端点发起一个简单的 AJAX 请求即可)。

upload successful

上图为 Auto Alt Text Chrome 插件运行示例

结论:

Im2txt 模型对于人物、风景以及其它存在于 COCO 数据集中的内容表现良好。

upload successful

上图为 COCO 数据集图片分类

这个模型能够标注的内容还是有所限制;不过,它能标注的内容已经涵盖了 Facebook、Reddit 等社交媒体上的大多数图片。

但是,对于 COCO 数据集中不存在的图片内容,这个模型并不能完成标注。我曾尝试着使用 Tesseract 来解决这个问题,但是它的结果并不是很准确,而且花费的时间也太长了(超过 10 秒)。现在我正在尝试使用 Tensorflow 实现 王韬等人的论文,将其加入这个项目中。

总结:

虽然现在几乎每周都会涌现一些关于 AI 的新事物,但最重要的是退回一步,看看这些工具能在研究环境之外发挥出怎样的作用,以及这些研究能怎样帮助世界各地的人们。总而言之,我希望我能深入研究 Tensorflow 和 in2txt 模型,并将我所学知识应用于现实世界。我希望这个工具能成为帮助视障人士”看“更好的互联网的第一步。

相关链接:

  • 关注文章作者:我会在 Medium 上首发我写的文章。如果你喜欢这篇文章,欢迎关注我:)。接下来一个月,我将会在下个月发布一系列“如何使用 AI/tensorflow 解决现实世界问题”的文章。最近我还会发一些 JS 方面的教程。
  • 本文工具 Chrome 插件:下载地址
  • Auto Alt Text Lambda API:Github repository 地址

发布于掘金 https://juejin.im/post/59a51e91f265da2499603c8c