0%

An awesome paper introduction will catch one's eyes and it is benefit to organize the rest of paper.

Do a simple introduction of what I did and the things I did will improve which related domain's work.

For example, the section of introduction can splice into 4 paragraphs:

First paragraph introduce meaning of paper and some related works (as a short literature review);

Second paragraph present my innovation of this paper, such as which newest methods I have using, or which feature no one used;

Third paragraph do a summary for previous;

Fourth paragraph display a summary of results, showing that this paper got an 'state-of-the-art' result comparing to baseline.

Fifth paragraph educe the rest of paper. A canonical 'fifth paragraph' as follow:

The rest of this paper is organized as follows. Section 2 gives an overview of the theory behind tree edit distance algorithms, the basis of our work. Section 3 presents our improved tree structure analysis algorithm, while Section 4 shows the application of this algorithm in the various tasks that comprise our approach. Experimental results demonstrating the effectiveness of our approach are in Section 5. Section 6 discusses related work. Finally, conclusions and directions for future work can be found in Section 7.

Previous paragraph cites: Reis D C, Golgher P B, Silva A S, et al. Automatic web news extraction using tree edit distance[C]// International Conference on World Wide Web. ACM, 2004:502-511.

upload successful

文件越小,意味着下载速度就越快。因此在向客户端发送资源文件前,使文件变得更小是件有益的事情。

其实,精简与压缩资源文件不仅是一件很棒的事情,同时也是每一位现代开发者应该尽量去做的事情。但是,用于精简的工具通常无法做到完美精简;用于压缩的压缩器效果好坏会取决于用于压缩的数据。下面介绍一些小技巧与方法,用于调整这些工具,使其达到最好的工作状态。

准备工作

我们将以一个简单的 SVG 文件为例:

upload successful

这个<svg>图像的内容为一个 10x10 像素的区域(viewBox),其中包含了两个 6x6 的正方形(<rect>)。原始文件大小为 176 字节,经过 gzip 压缩过后大小为 138 字节。

当然这个图像并没有什么艺术感,但它足以满足这篇文章想要表达的意思,并且防止这篇文章变成长篇大论。

第 0 步:Svgo

运行 svgo image.svg 直接进行压缩。

upload successful

(为了便于阅读,为其添加了回车与缩进)

可以明显地看到,rect 被替换成了 pathpath 路径形状由它的 d 属性定义,后面的一串命令类似于 canvas 的 draw 函数,控制一支虚拟的笔移动进行绘画。命令可以是绝对位移(移动 x,y),也可以是相对位移(向某方向移动 x,y)。请仔细观察其中的一条路径:

M 0 0:路径起点为坐标(0, 0) h 6:水平向右移动 6 px v 6:垂直向下移动 6 px H 0:水平移动至 x = 0 z:闭合路径 — 移回路径的起点

这个路径画出的正方形是多么的精确!而且它比 rect 元素更加的紧凑。

另外,#f00 被改成了 red,这儿也少了一个字节!

现在文件大小为 135 字节,gzip 压缩过后为 126 字节。

第 1 步:进行整体缩放

你可能已经注意到了,两个路径中的所有坐标均为偶数。我们是否可以把它们都除以 2 呢?

upload successful

图像和之前看起来是一样的,但它缩小了两倍。因此,我们可以对 viewBox 进行缩放,使图像与之前一样大。

upload successful

现在文件大小为 133 字节,gzip 压缩过后为 124 字节。

第 2 步:使用非闭合路径

回过头来看路径。两个路径中的最后一个命令都是 z,也就是“闭合路径”。但路径在填充的时候会被隐式地闭合,因此我们可以删除这些命令。

upload successful

又少了 2 字节,现在文件大小为 131 字节,gzip 压缩过后为 122 字节。从常识上说,原始字节数越少,能压缩的大小也越小。而现在我们已经在 svgo 之后节省了 4 个 gzip 字节了。

你可能会想:为什么 svgo 不自动进行这些优化呢?原因是缩放图像与删除尾部的 z 命令是不安全的。请看下面的例子:

upload successful

这是一些有 stroke(路径宽度)的图形。从左至右分别为:原始图形、不闭合的情况、不闭合且进行缩放的情况。

线宽完全混乱了。庆幸的是,我们知道自己不需要使用线宽。但是 Svgo 并不知道这个情况,因此它必须要保证图形的安全,避免不安全的变换。

现在看起来不能从代码中删除任何东西了。XML 语法是严格的,现在所有的属性都是必须的,并且它们的值不能不加引号。

你以为结束了?并不,这仅仅是个开始。

第 3 步:减少出现的字母

现在,让我来介绍一个非常方便的工具:gzthermal。它可以分析需要进行 gzip 压缩的文件,并对进行编码的原始字节进行着色。更好压缩的字节是绿色,不好压缩的数据是红色,简单明了。

upload successful

请再次关注 d 属性,尤其是被标成红色的 M 命令值得注意。我们不能删除它,但我们可以用相对位移 m2 2 来代替它。

初始的“指针”位置为坐标轴原点(0, 0),因此移动(2, 2)和从原点移动(2, 2)是同一个意思。让我们试试:

upload successful
upload successful

原始文件依然是 131 字节,但是经过 gzip 压缩过后大小仅有 121 字节了。发生了什么?答案是……

哈夫曼树(Huffman Trees)

Gzip 使用的是 DEFLATE 压缩算法,而 DEFLATE 算法是以哈夫曼树为基础构建的。

哈夫曼编码的核心思想就是使用更少的比特对出现次数更多的符号进行编码,反之亦然,出现次数很少的符号需要占用更多的比特。

没错,这儿说的是比特不是字节。DEFATE 算法会将一字节的字符视为一系列的比特,无论一字节包含 7、9、100 个比特,DEFLATE 算法都能一视同仁。

以字符串“Test”为例,根据它出现的字母来进行编码: 00 T 01 e 10 s 11 t

对每个符号都进行过编码的字符串“Test”可以表示为:00011011,总共占 8 比特。

然后我们把它开头的“T”改成小写“test”,再试一次: 0 t 10 e 11 s

字母 t 出现了更多的次数,它的编码也变得更短,仅为 1 比特。这个字符串经过编码后为 010110,仅为 6 比特!


在我们的 SVG 中的 M 字母也一样。在将其变为小写之后,整个编码中都不包含大写的 M 了,可以将它从树上移除,因此平均编码长度可以更短。

当你编写对 gzip 友好的代码时,应该更多地使用那些使用频率较高的字符。即使你不能将代码长度减短,但它经过压缩后消耗的比特数也会变少。

第 4 步:回退引用(backreferences)

DEFLATE 算法还有一个特性:回退引用。某些编码点不会直接进行编码,而是告诉解码器复制一些最近解码的字节。

因此,它不需要对原始字节一次又一次地进行编码,而是可以直接引用: 向前返回 n 个字节,复制 m 个字节 例如:

Hey diddle diddle, the cat and the fiddle.

Hey diddle**<7,7>**, the cat and**<12,5>**f**<24,5>**.

巧妙的是,gzthermal 还有一种只显示回退引用的特殊模式。 gzthermal -z 会显示以下图像:

upload successful

普通文本字节为橙色,可回退引用的字节为蓝色。下面的动画更直观:

除了 fill 值、m 命令和最后的 H 命令外,第二条路径几乎全部都使用了回退引用。对于 fill 和 m 我们无能为力,因为第二个方块的确有着不同的颜色和位置。

但是它们的形状是一样的,并且我们现在对 gzip 有了更加清晰的认识。因此,我们可以将绝对位移命令 H0H2 都替换为相对位移命令:h-3

upload successful
upload successful

现在,两个分开的回退引用合为了一个,文件大小为 133 字节,gzip 后的大小为 119 字节。虽然我们在压缩前增加了 2 个字节,但 gzip 的结果又减少了 2 个字节!

我们只需要关心压缩后的大小即可:在传送资源时,客户端 99.9% 用的是 gzip 或者 brotli。顺带说一下 brotli。

Brotli 压缩算法

Brotli 是于 2015 年推出的用于替换浏览器中 gzip(源自 1992)的算法。不过它与 gzip 在很多方面都有相似之处:它也是基于哈夫曼编码与回退引用的原理,因此我们前面为 gzip 所做的调整都可以同样利于 Brotli。最后让我们用 Brotli 应用于前面的所有步骤:

原始文件大小:106 字节 在第 0 步之后(svgo):104 字节 在第 1 步之后(viewBox):105 字节 在第 2 步之后(使用非闭合路径):113 字节 在第 3 步之后(小写 m):116 字节 在第 4 步之后(相关命令):102 字节

如你所见,最终的文件比 svgo 后的更小。这可以说明,之前我们为 gzip 做的酷炫的工作同样适用于 Brotli。

但是,中间步骤的文件大小却是混乱的,Brotli 压缩后的文件变得更大了。毕竟,Brotli 并不是 gzip,它是一种单独的新算法。尽管与 gzip 有一些相似之处,但仍有所不同。

其中最大的不同是,Brotli 内置了预定义字典,在编码时使用它进行上下文启发。此外,Brotli 的最小回退引用大小为 2 字节(gzip 仅能创建 3 字节及以上的回退引用)。

可以说,Brotli 比 gzip 更加难以预测。我很想解释一下是什么导致了“压缩退化”,可惜 Brotli 并没有类似于 gzip 的 gzthermal 和 defdb 之类的工具。我只能靠它的规范 以及试错的方法来进行调试。

试错法

让我们再试一次。这次将改变 fill 属性内的颜色。显然 red#f00 更短,但也许 Brotli 会用更长的回退引用进行压缩。

upload successful

gzip 压缩过后大小为 120 字节,Brotli 压缩过后为 100 字节。gzip 流长了 1 字节,Brotli 流短了 2 字节。

此时,它在 Brotli 中表现更好,在 gzip 中表现更差。我觉得,这完全无碍!因为我们几乎不可能一次性将数据针对所有压缩器进行优化,并得到最佳结果。解决压缩器问题就像转一个糟糕的魔方,只能尽量优化。

总结

上面描述的所有的调整方法都不仅限于 SVG 压缩为 gzip 的情景。

以下是一些可以帮助你写出更具备压缩性能的代码的准则:

  1. 压缩更小的源数据可能会得到更小的压缩数据。
  2. 不同的字符越少就意味着熵越少。而熵越小,压缩效果就越好。
  3. 频繁出现的字符会以更小的字节被压缩。删除不常见字符以及使常见字符更常见可以提高压缩效率。
  4. 长段重复的代码可以被压缩成几个字节。DRY(“不要重复自己”原则)不一定在任何情况下都是最好的选择,有时候重复自己反而能得到更好的结果。
  5. 有些时候更大的源数据反而可以得到更小的压缩数据。减少熵可以让压缩器更好地移除冗余的信息。

你可以在 此 GitHub repo 中找到以上所有资源、压缩过的图片以及其它资料。

希望你喜欢这篇文章。下次我们将讨论如何压缩普通 JavaScript 代码与 Webpack bundle 中的 JavaScript 代码。

本文发布于掘金 https://juejin.im/post/5a30a7fdf265da4309452517

1
cat a.txt

123@123 123

234@234 234

1
awk '{print("curl -X POST -d \"email="$1"\"  -d \"password="$2"\" --user abc:123456 http://baidu.com")}' a.txt

curl -X POST -d "email=123" -d "password=123 123" --user abc:123456 http://baidu.com

curl -X POST -d "email=234" -d "password=234 234" --user abc:123456 http://baidu.com

把上面的print改成system就能执行命令。

读取行的默认分割符为空格,可用 -F "#" 进行修改。可用管道输出到其它bash命令执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELETE consum_record
FROM
consum_record,
(
SELECT
min(id) id,
user_id,
monetary,
consume_time
FROM
consum_record
GROUP BY
user_id,
monetary,
consume_time
HAVING
count(*) > 1
) t2
WHERE
consum_record.user_id = t2.user_id
and consum_record.monetary = t2.monetary
and consum_record.consume_time = t2.consume_time
AND consum_record.id > t2.id;

抄自https://www.cxiansheng.cn/server/348

upload successful Express 基于 Node.js,是一款用于构建 Web 服务的优秀框架。它很容易上手,且得益于其中间件的概念,可以很方便地进行配置与拓展。尽管现在有各种各样的用于创建 Web 应用的框架,但我的第一选择始终是 Express。然而,直接使用 Express 不能完全遵循安全性的最佳实践。因此我们需要使用类似 helmet 的模块来改善应用的安全性。

部署

在开始之前,请确认你已经安装好了 Node.js 以及 npm(或 yarn)。你可以在 Node.js 官网下载以及查看安装指南

我们将以一个新的工程为例,不过你也可以将这些功能应用于现有的工程中。

在命令行中运行以下命令创建一个新的工程:

1
2
3
mkdir secure-express-demo
cd secure-express-demo
npm init -y

运行以下命令安装 Express 模块:

1
npm install express --save

secure-express-demo 目录下创建一个名为 index.js 的文件,加入以下代码:

1
2
3
4
5
6
7
8
9
10
11
const express = require('express');
const PORT = process.env.PORT || 3000;
const app = express();

app.get('/', (req, res) => {
res.send(`<h1>Hello World</h1>`);
});

app.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}`);
});

保存文件,试运行看看它是否能正常工作。运行以下命令启动服务:

1
node index.js

访问 http://localhost:3000,你应该可以看到 Hello World

upload successful

检查 Headers

upload successful

现在让我们通过增加与删除一些 HTTP headers 来改善应用安全性。你可以用一些工具来检查它的 headers,例如使用 curl 运行以下命令:

1
curl http://localhost:3000 --include

--include 标志可以让其输出 response 的 HTTP headers。如果你没有安装 curl,也可以用你最常用浏览器开发者工具的 network 面板代替。

你可以看到在收到的 response 中包含的以下 HTTP headers:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 20
ETag: W/"14-SsoazAISF4H46953FT6rSL7/tvU"
Date: Wed, 01 Nov 2017 13:36:10 GMT
Connection: keep-alive

一般来说,由 X- 开头的 header 是非标准头部。请注意那个 X-Powered-By 的 header,它会暴露你使用的框架。对于攻击者来说,这可以降低攻击成本,因为他们只专注攻击此框架的已知漏洞即可。

戴上头盔(helmet)

upload successful

来看看如果我们使用 helmet 会发生什么。运行以下命令安装 helmet

1
npm install helmet --save

helmet 中间件加入你的应用中。对 index.js 进行如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express');
const helmet = require('helmet');
const PORT = process.env.PORT || 3000;
const app = express();

app.use(helmet());

app.get('/', (req, res) => {
res.send(`<h1>Hello World</h1>`);
});

app.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}`);
});

这样就使用了 helmet 的默认配置。接下来看看它做了什么事情。重启服务,再次通过以下命令检查 HTTP headers:

1
curl http://localhost:3000 --inspect

新的 headers 会类似于下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Type: text/html; charset=utf-8
Content-Length: 20
ETag: W/"14-SsoazAISF4H46953FT6rSL7/tvU"
Date: Wed, 01 Nov 2017 13:50:42 GMT
Connection: keep-alive

首先值得庆祝的是 X-Powered-By header 不见了。但现在又多了好些新的 header,它们是做什么的呢?

X-DNS-Prefetch-Control

这个 header 对增加安全性并没有太大作用。它的值为 off 时,将关闭浏览器对页面中 URL 的 DNS 预读取。DNS 预读取可以提高你的网站的性能,根据 MDN 描述,它可以增加 5% 或更高的图片加载速度。不过开启这项功能也可能会使用户在多次访问同一个网页时缓存出现问题。 > 译注:缓存问题未查到资料,如果您了解这块请留言

它的默认值是 off,如果你希望通过它提升性能,可以在调用 helmet() 时传入 { dnsPrefetchControl: { allow: true }} 开启 DNS 预读取。

X-Frame-Options

X-Frame-Options 可以让你控制页面是否能在 <frame/><iframe/> 或者 <object/> 之类的页框内加载。除非你的确需要通过这些方式来打开页面,否则请通过下面的配置完全禁用它:

1
2
3
4
5
app.use(helmet({
  frameguard: {
    action: 'deny'
  }
}));

所有的现代浏览器都支持 X-Frame-Options。你也可以通过稍后将介绍的内容安全策略来控制它。

Strict-Transport-Security

它也被称为 HSTS(严格安全 HTTP 传输),用于确保在访问 HTTPS 网站时不出现协议降级(回到 HTTP)的情况。如果用户一旦访问了带有此 header 的 HTTPS 网站,浏览器就会确保将来再次访问次网站时不允许使用 HTTP 进行通信。此功能有助于防范中间人攻击。

有时,当你使用公共 WiFi 时尝试访问 https://google.com 之类的门户网页时就能看到此功能运作。WiFi 尝试将你重定向到他们的门户网站去,但你曾经通过 HTTPS 访问过 google.com,且它带有 Strict-Transport-Security 的 header,因此浏览器将阻止重定向。

你可以访问 MDN 或者 OWASP wiki 查看更多相关信息。

X-Download-Options

这个 header 仅用于保护你的应用免受老版 IE 漏洞的困扰。一般来说,如果你部署了不能被信任的 HTTP 文件用于下载,用户可以直接打开这些文件(而不需要先保存到硬盘去)并且可以直接在你 app 的上下文中执行。这个 header 可以确保用户在访问这种文件前必须将其下载到本地,这样就能防止这些文件在你 app 的上下文中执行了。

你可以访问 helmet 文档MSDN 博文查看更多相关信息。

X-Content-Type-Options

一些浏览器不使用服务器发送的 Content-Type 来判断文件类型,而使用“MIME 嗅探”,根据文件内容来判断内容类型并基于此执行文件。

假设你在网页中提供了一个上传图片的途径,但攻击者上传了一些内容为 HTML 代码的图片文件,如果浏览器使用 MIME 嗅探则会将其作为 HTML 代码执行,攻击者就能执行成功的 XSS 攻击了。

通过设置 header 为 nosniff 可以禁用这种 MIME 嗅探。

X-XSS-Protection

此 header 能在用户浏览器中开启基本的 XSS 防御。它不能避免一切 XSS 攻击,但它可以防范基本的 XSS。例如,如果浏览器检测到查询字符串中包含类似 <script> 标签之类的内容,则会阻止这种疑似 XSS 攻击代码的执行。这个 header 可以设置三种不同的值:011; mode=block。如果你想了解更多关于如何选择模式的知识,请查看 X-XSS-Protection 及其潜在危害 一文。

升级你的 helmet

以上只是 helmet 提供的默认设置。除此之外,它还可以让你设置 Expect-CTPublic-Key-PinsCache-ControlReferrer-Policy 之类的 header。你可以在 helmet 文档 中查找更多相关配置。

保护你的网页免受非预期内容的侵害

upload successful

跨站脚本执行对于 web 应用来说是无法根绝的威胁。如果攻击者可以在你的应用中注入并运行代码,其后果对于你和你的用户来说可能是一场噩梦。有一种能试图阻止在你网页中运行非预期代码的方案:CSP(内容安全策略)。

CSP 允许你设定一组规则,以定义你的页面能够加载资源的来源。任何违反规则的资源都会被浏览器自动阻止。

你可以通过修改 Content-Security-Policy HTTP header 来指定规则,或者你不能改 header 时也可以使用 meta 标签来设定。

这个 header 类似于这样:

1
2
3
4
5
6
7
8
Content-Security-Policy: default-src 'none';
    script-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==';
    style-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==';
    img-src 'self';
    font-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==' fonts.gstatic.com;
    object-src 'none';
    block-all-mixed-content;
    frame-ancestors 'none';

在这个例子中,你可以看到我们只允许从自己的域名或者 Google Fonts 的 fonts.gstatic.com 来获取字体;只允许加载本域名下的图片;只允许加载不指定来源,但必须包含指定 nonce 值的脚本及样式文件。这个 nonce 值需要用下面这样的方式指定:

1
2
<script src="myscript.js" nonce="XQY ZwBUm/WV9iQ3PwARLw=="></script>
<link rel="stylesheet" href="mystyles.css" nonce="XQY ZwBUm/WV9iQ3PwARLw==" />

当浏览器收到 HTML 时,为了安全起见它会清除所有的 nonce 值,其它的脚本无法得到这个值,也就无法添加进网页中了。

你还可以禁止所有在 HTTPS 页面中包含的 HTTP 混合内容和所有 <object /> 元素,以及通过设置 default-srcnone 来禁用一切不为图片、样式表以及脚本的内容。此外,你还可以通过 frame-ancestors 来禁用 iframe。

你可以自己手动去编写这些 header,不过走运的是 Express 中已经有了许多现成的 CSP 解决方案。helmet 支持 CSP,但 nonce 需要你自己去生成。我个人为此使用了一个名为 express-csp-header 的模块。

安装及运行 express-csp-header

1
npm install express-csp-header --save

为你的 index.js 添加并修改以下内容,启用 CSP:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
const express = require('express');
const helmet = require('helmet');
const csp = require('express-csp-header');

const PORT = process.env.PORT || 3000;
const app = express();

const cspMiddleware = csp({
  policies: {
    'default-src': [csp.NONE],
    'script-src': [csp.NONCE],
    'style-src': [csp.NONCE],
    'img-src': [csp.SELF],
    'font-src': [csp.NONCE, 'fonts.gstatic.com'],
    'object-src': [csp.NONE],
    'block-all-mixed-content': true,
    'frame-ancestors': [csp.NONE]
  }
});

app.use(helmet());
app.use(cspMiddleware);

app.get('/', (req, res) => {
  res.send(`
    <h1>Hello World</h1>
    <style nonce=${req.nonce}>
      .blue { background: cornflowerblue; color: white; }
    </style>
    <p class="blue">This should have a blue background because of the loaded styles</p>
    <style>
      .red { background: maroon; color: white; }
    </style>
    <p class="red">This should not have a red background, the styles are not loaded because of the missing nonce.</p>
  `);
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});

重启服务,访问 http://localhost:3000,可以看到一个带有蓝色背景的段落,因为相关的样式成功被加载了。而另一个段落没有样式,因为其样式缺少了 nonce 值。

upload successful

CSP header 还可以设定报告违规的 URL,你还可以在严格启用 CSP 之前仅开启报告模式,收集相关数据。

你可以在 MDN CSP 介绍查看更多信息,并浏览 “Can I Use” 网站查看 CSP 兼容性。大多数主流浏览器都支持这项特性。

总结

upload successful

可惜的是,在安全性方面不存在所谓的万能方案,新的漏洞层出不穷。但是,你可以很轻松地在你的 web 应用中设置这些 HTTP header,显著地提升你应用的安全性,何乐而不为呢?如果你想了解更多有关 HTTP header 提高安全性的最佳实践,请浏览 securityheaders.io

如果你想了解更多 web 安全方面的最佳实践,请访问 Open Web Applications Security Project(OWASP),它涵盖了广泛的主题及有用的资源。

如果你有任何问题,或有其它用于提升 Node.js web 应用的工具,请随时联系我:

本文发布于掘金 https://juejin.im/post/5a24fd8f51882509e5438247

1
grep -rl "192.168.1.1" |xargs -i sed -i 's/192.168.1.1/192.168.1.2/g' "{}"

将目录下所有文件的192.168.1.1替换成192.168.1.2

upload successful

开发者们在开发的过程中会无意地产生一些 bug。bug 越老,找到并修复它的难度就越高。在本系列的文章中,我将试着向你展示如何使用 Google Chrome 开发者工具、Chrome 插件以及 WebStorm 进行调试。

这篇文章将介绍最常用的调试工具 —— Chrome Console。请享用!

Console

打开 Chrome 开发者工具的方法:

  • 在主菜单中选择“更多工具”菜单 > 点击开发者工具。
  • 在页面任何元素上右键,选择“检查”。
  • 在 Mac 中,按下 Command+Option+I;在 Windows 与 Linux 中,按下 Ctrl+Shift+I。

请观察 Console 选项卡中的内容。

upload successful

第一行:

upload successful
  • 清空 console 控制台

top — 在默认状态下,Console 的上下文(context)为 top(顶级)。不过当你检查元素或使用 Chrome 插件上下文时,它会发生变化。 你可以在此更改 console 执行的上下文(页面的顶级 frame)。

过滤: 对控制台的输出进行过滤。你可以根据输出严重级别、正则表达式对其进行过滤,也可以在此隐藏网络连接产生的消息。

设置: Hide network — 隐藏诸如 404 之类的网络错误。 Preserve log — 控制台将会在页面刷新或者跳转时不清空记录。 Selected context only — 勾上后可以根据前面 top 选择的上下文来指定控制台的日志记录范围。 User messages only — 隐藏浏览器产生的访问异常之类的警告。 Log XMLHttpRequests — 顾名思义,记录 XMLHttpRequest 产生的信息。 Show timestamps — 在控制台中显示时间戳信息。 Autocomplete from history — Chrome 会记录你曾经输入过的命令,进行自动补全。

选择合适的 Console API

控制台会在你应用的上下文中运行你输入的 JS 代码。你可以轻松地通过控制台查看全局作用域中存储的东西,也可以直接输入并查看表达式的结果。例如:“null === 0”。

console.log — 对象引用

根据定义,console.log 将会在控制台中打印输出内容。除此之外,你还得知道,console.log 会对你展示的对象保持引用关系。请看下面的代码:

1
2
3
4
5
6
var fruits = [{one: 1}, {two: 2}, {three: 3}];
console.log('fruits before modification: ', fruits);
console.log('fruits before modification - stringed: ', JSON.stringify(fruits));
fruits.splice(1);
console.log('fruits after modification: ', fruits);
console.log('fruits after modification - stringed : ', JSON.stringify(fruits))
upload successful

当调试对象或数组时,你需要注意这点。我们可以看到 fruits 数组再被修改前包含 3 个对象,但之后发生了变化。如需要在特定时刻查看结果,可以使用 JSON.stringify 来展示信息。不过这种方法对于展示大对象来说并不方便。之后我们会介绍更好的解决方案。

console.log — 对对象属性进行排序

JavaScript 是否能保证对象属性的顺序呢?

4.3.3 Object — ECMAScript 第三版 (1999)

对象是 Object 的成员,它是一组无序属性的集合,每个属性都包含一个原始值、对象或函数。称存储在对象属性中的函数为方法。

但是…… 在 ES5 中它的定义发生了改变,属性可以有序 —— 但你还是不能确定你的对象属性是否能按顺序排列。浏览器通过各种方法实现了有序属性。在 Chrome 中运行下面的代码,可以看到令人困惑的结果:

1
2
3
4
5
6
7
var letters = {
z: 1,
t: 2,
k: 6
};
console.log('fruits', letters);
console.log('fruits - stringify', JSON.stringify(letters));
upload successful

Chrome 按照字母表的顺序对属性进行了排序。没法说我们是否应该喜欢这种排序方式,但了解这儿发生了什么总没坏处。

console.assert(expression, message)

如果 expression 表达式的结果为 falseConsole.assert 将会抛出错误。关键的是,assert 函数不会由于报错而停止评估之后的代码。它可以帮助你调试冗长棘手的代码,或者找到多次迭代后函数自身产生的错误。

1
2
3
4
5
function callAssert(a,b) {
console.assert(a === b, 'message: a !== b ***** a: ' + a +' b:' +b);
}
callAssert(5,6);
callAssert(1,1);
upload successful

console.count(label)

简而言之,它就是一个会计算相同表达式执行过多少次的 console.log。其它的都一样。

1
2
3
4
for(var i =0; i <=3; i++){
console.count(i + ' Can I go with you?');
console.count('No, no this time');
}
upload successful

如上面的例子所述,只有完全相同的表达式才会增加统计数字。

console.table()

很好用的调试函数,但即使它会提高工作效率,我也一般懒得用它…… 别像我这样,咱要保持高效。

1
2
3
4
5
6
var fruits = [
{ name: 'apple', like: true },
{ name: 'pear', like: true },
{ name: 'plum', like: false },
];
console.table(fruits);
upload successful

它非常棒。第一,你可以将所有东西都整齐地放在表格中;第二,你也会得到 console.log 的结果。它在 Chrome 中可以正常工作,但是不保证兼容所有浏览器。

1
2
3
4
5
6
var fruits = [
{ name: 'apple', like: true },
{ name: 'pear', like: true },
{ name: 'plum', like: false },
];
console.table(fruits, ['name'])
upload successful

我们可以决定是完全展示数据内容还是只展示整个对象的某几列。这个表格是可排序的 —— 点击需要排序的列的表头,即可按此列对表格进行排序。

console.group() / console.groupEnd();

这次让我们直接从代码开始介绍。运行下面的代码看看控制台是如何进行分组的。

1
2
3
4
5
6
7
8
9
10
11
12
console.log('iteration');
for(var firstLevel = 0; firstLevel<2; firstLevel++){
console.group('First level: ', firstLevel);
for(var secondLevel = 0; secondLevel<2; secondLevel++){
console.group('Second level: ', secondLevel);
for(var thirdLevel = 0; thirdLevel<2; thirdLevel++){
console.log('This is third level number: ', thirdLevel);
}
console.groupEnd();
}
console.groupEnd();
}
upload successful

它可以帮助你更好的处理数据。

console.trace();

console.trace 会将调用栈打印在控制台中。如果你正在构建库或框架时,它给出的信息将十分有用。

1
2
3
4
5
6
7
8
9
10
function func1() {
func2();
}
function func2() {
func3();
}
function func3() {
console.trace();
}
func1();
upload successful

对比 console.log 与 console.dir

1
2
console.log([1,2]);
console.dir([1,2]);
upload successful

它们的实现方式取决于浏览器。在最开始的时候,规范中建议 dir 要保持对对象的引用,而 log 不需要引用。(Log 会显示一个对象的副本)。但现在,如上图所示,log 也保持了对于对象的引用。它们展示对象的方式有所不同,但我们不再加以深究。不过 dir 在调试 HTML 对象的时候会非常有用。 > 译注:console.dir 会详细打印一个对象的所有属性与方法。

$_, $0 — $4

$_ 会返回最近执行表达式的值。 $0 — $4 — 分别作为近 5 此检查元素时对 HTML 元素的引用。

upload successful

getEventListeners(object)

返回指定 DOM 元素上注册的事件监听器。这儿还有一种更便捷的方法来设置事件监听,下次教程会介绍它。

upload successful

monitorEvents(DOMElement, [events]) / unmonitorEvents(DOMElement)

在指定 DOM 元素上触发任何事件时,都可以在控制台中看到相关信息。直到取消对相应元素的监视。

upload successful

在控制台中选择元素

upload successful

在 Element 标签中按 ESC 键展开这个界面。

$ 没有另做它用的情况下:

$() — 相当于 **document.querySelector()**。它会返回匹配 CSS 选择器的第一个元素(例如 $('span') 会返回第一个 span) $$() — 相当于 **document.querySelectorAll()**。它会以数组的形式返回所有匹配 CSS 选择器的元素。

复制打印的数据

有时,当你处理数据时可能会想打个草稿,或者简单地看看两个对象是否有区别。全选之后再复制可能会很麻烦,在此介绍一种很方便的方法。

在打印出的对象上点击右键,选择 copy(复制),或选择 Store as global element(将指定元素的引用存储在全局作用域中),然后你就可以在控制台中操作刚才存储的元素啦。

控制台中的任何内容都可以通过使用 copy('object-name') 进行复制。

自定义控制台输出样式

假设你正在开发一个库,或者在为公司、团队开发一个大模块。此时在开发模式下对一些日志进行高亮处理会很舒爽。你可以试试下面的代码:

console.log('%c Truly hackers code! ', 'background: #222; color: #bada55');

upload successful

%d%i — 整型值 %f — 浮点值 %o — 可展开的 DOM 元素 %O — 可展开的 JS 对象 %c — 使用 CSS 格式化输出

以上就是本文的全部内容,但并不是 Console 这个话题的全部内容。你可以点击以下链接了解更多有关知识:

本文发布于掘金 https://juejin.im/post/5a08087f6fb9a04529363d71

本系列文章汇总

  1. RNN 循环神经网络系列 1:基本 RNN 与 CHAR-RNN
  2. RNN 循环神经网络系列 2:文本分类
  3. RNN 循环神经网络系列 3:编码、解码器
  4. RNN 循环神经网络系列 4:注意力机制
  5. RNN 循环神经网络系列 5:自定义单元

在本文中,我们将探索并尝试创建我们自己定义的 RNN 单元。不过在此之前,我们需要先仔细研究简单的 RNN,再逐步深入较为复杂的单元(如 LSTM 与 GRU)。我们会分析这些单元在 tensorflow 中的实现代码,最终参照这些代码来创建我们的自定义单元。本文将援引由 Chris Olah 所著,在 RNN、LSTM 方面非常棒的一篇文章中的图片。在此我强烈推荐你阅读这篇文章,本文中会重申其中许多相关内容,不过由于我们主要还是关注 tf 代码,所以这些内容将会较快地略过。将来当我要对 RNN 结构进行层规范化时,我还会引用本文中的代码。之后的文章可以在这儿查看。

基本 RNN:

对于传统的 RNN 来说,最大的问题就在于每个单元的重复输入都是静态的,因此我们无法充分学习到长期的依赖情况。你回想一下基本 RNN 单元,就会发现所有操作都是单一的 tanh 运算。

upload successful

对于解决短期依赖情况的问题来说,这种结构已经够用了;但如果我们希望通过有效的长期记忆来预测目标,则需要使用更稳定强大的 RNN 单元 —— LSTM。

长短期记忆网络(LSTM):

LSTM 的结构可以让我们在更多的操作中进行长期的信息控制。传统的 RNN 仅有一个输出,其既作为隐藏状态表示也作为此单元的输出端。

upload successful

这种结构缺乏对信息的控制,无法存住对许多步之后有用的信息。而 LSTM 有两种不同的输出。其中一种仍与前面的传统结构一样,既作为隐藏状态表示也作为单元输出;但 LSTM 单元还有另一种输出 - 单元状态 C。这也是 LSTM 精髓所在,让我们仔细研究它。

upload successful

遗忘门:

第一个要介绍的门就是遗忘门。这个门可以让我们选择性地传递信息以决定单元的状态。我将公式罗列在下,后面介绍其它的门时也会如此。

upload successful
upload successful

你可以参考类似 tf 的 _linear 函数来实现它。不过遗忘门的主要要点是对输入与隐藏状态前应用了 sigmoid。那么这个 sigmoid 的作用是什么?请回想一下,sigmoid 会输出在 [0, 1] 范围的值,在此我们将其应用于 [N X H] 的矩阵,因此会得到 NXH 个 sigmoid 算出的值。如果 sigmoid 得到 0 值,那么其对应的隐藏值就会失效;如果 sigmoid 得到 1 值,那么此隐藏值将会被应用在计算中。而处于 0 和 1 之间的值将会允许一部分的信息继续传递。这样就能很好地通过阻塞与选择性地传递输入单元的数据,以达到控制信息的目的。

这就是遗忘门。它是我们的单元得到最终结果前的第一个步骤。下面介绍另一个操作:输入门。

输入门:

输入门将获取我们的输入值 X 以及在前面的隐藏状态,并对它们进行两次运算。首先会通过 sigmoid 门来选择性地允许部分数据输入,接着将其与输入值的 tanh 值相乘。

upload successful

这儿的 tanh 与前面的 sigmoid 操作不同。请回忆一下,tanh 会将输入值改变为 [-1, 1] 范围内的值。它本质上通过非线性的方式改变了输入的表示。这一步与我们在基本 RNN 单元中进行的操作一致,不过在此我们将两值的乘积加上遗忘门得到的值得到本单元的状态值。

遗忘门与输入门的操作可以看做同时保存了旧状态(C_{t-1})的一部分与新变换(tanh)单元状态(C~_t)的一部分。这些权重将会通过我们数据的训练学到需要保存多少数据以及如何进行正确的变换。

输出门:

最后一个门是输出门,它利用输入值、前面的隐藏状态值以及新单元状态值来共同决定新隐藏状态的表示。

upload successful

该步骤依旧涉及到了 sigmoid,将它的值与单元状态的 tanh 值相乘以决定信息的去留。需要注意这一步的 tanh 计算与输入门的 tanh 计算不同,此步不再是神经网络的计算,而仅仅是单纯、不带任何权重地计算单元状态值的 tanh 值。这样我们就能强制单元状态矩阵 [NXH] 的值处于 [-1, 1] 的范围内。

变体

RNN 单元有许多种变体,在此再次建议去阅读 Chris Olah 的这篇博文学习更多相关知识。不过他在文中讨论的是 peehole 模型(在计算 C_{t-1} 或 C_t 时允许所有门都能观察到单元状态值)以及单元状态的 couple(更新与遗忘同时进行)。不过目前 LSTM 的竞争对手是正在被广泛使用的 GRU(Gated Recurrent Unit)。

GRU(Gated Recurrent Unit):

GRU 的主要原理是将遗忘门与输入门结合成一个更新门。

upload successful

在实际使用中,GRU 的性能与 LSTM 相当,但其计算量更小,因此它现在日益流行。

原生 Tensorflow 实现:

我们先观察一下 Tensorflow 官方对于 GRU 单元的实现代码,主要关注其函数调用方式、输入以及输出。然后我们会复制它的结构用于创建我们自己的单元。如果你对其它的单元有兴趣,可以在这儿找到它们的实现。本文将主要关注 GRU,因为它在大多数情况下性能与 LSTM 相当且复杂度更低。

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
28
29
30
class GRUCell(RNNCell):
"""Gated Recurrent Unit cell (cf. http://arxiv.org/abs/1406.1078)."""

def __init__(self, num_units, input_size=None, activation=tanh):
if input_size is not None:
logging.warn("%s: The input_size parameter is deprecated.", self)
self._num_units = num_units
self._activation = activation

@property
def state_size(self):
return self._num_units

@property
def output_size(self):
return self._num_units

def __call__(self, inputs, state, scope=None):
"""Gated recurrent unit (GRU) with nunits cells."""
with vs.variable_scope(scope or type(self).__name__): # "GRUCell"
with vs.variable_scope("Gates"): # Reset gate and update gate.
# We start with bias of 1.0 to not reset and not update.
r, u = array_ops.split(1, 2, _linear([inputs, state],
2 * self._num_units, True, 1.0))
r, u = sigmoid(r), sigmoid(u)
with vs.variable_scope("Candidate"):
c = self._activation(_linear([inputs, r * state],
self._num_units, True))
new_h = u * state + (1 - u) * c
return new_h, new_h

GRUCell 类由 init 函数开始执行。在 init 函数中定义了单元的数量与其使用的激活函数。其激活函数一般是 tanh,不过也可以使用 sigmoid 来使得值固定在 [0, 1] 范围内方便我们控制信息流。另外,它还有两个在调用时会返回 self._num_units 的属性。最后定义了 call 函数,它将处理输入值并得出新的隐藏值。回忆一下,GRU 没有类似 LSTM 的单元状态值。

call 中,我们首先计算 r 和 u(u 是前面图中的 z)。在这步中,我们没有单独去计算它们,而是以乘以 2 倍 num_units 的形式合并了权重,再将结果分割成两份得到它们(split(dim, num_splits, value))。然后对得到的值应用 sigmoid 激活函数,以选择性地控制信息流。接着计算 c 的值,用它计算新隐藏状态表示值。你可能发现它计算 new_h 的顺序和之前颠倒了,不过由于权重会同时进行训练,因此代码仍能正常运行。

其它的单元代码都与此代码类似,你弄明白了上面的代码就能轻松解释其它单元的代码。

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

在每周周一,超过 1 亿位 Spotify 用户会收到一份新鲜的歌曲播放列表。这个自定义列表中包含了 30 首用户从来没听过,但可能会喜欢上的歌曲。这个神奇的功能被称为“每周推荐(Discover Weekly)”。

我是 Spotify 的忠实粉丝,尤其喜欢它的每周推荐功能。因为,它让我感觉到我被重视着。它比谁都了解我的音乐品味,而且每周的推荐都刚好令我满足。如果没有它,我可能一辈子都找不到一些我非常喜欢的歌曲。

如果你苦于找不到想听的音乐,请让我隆重介绍我最好的虚拟伙伴:

upload successful

事实证明,痴迷于每周推荐的用户不仅只有我一个 —— 许多用户都为它痴狂,这足以让 Spotify 重新思考其发展重点,将更多的资源投入播放列表推荐算法中。

每周推荐功能于 2015 亮相,从那时开始,我就非常渴望了解它是如何运作的(我是他们公司的粉丝,所以常常假设自己在 Spotify 工作并研究他们的产品)。在经过三个星期的疯狂搜索之后,我得以瞟到了其帷幕后的一丝真容。

那么 Spotify 是如何做出每周为每个用户选出 30 首歌这个惊人的工作的呢?让我们先看一看其它一些音乐服务商是如何进行音乐推荐的,然后分析为什么 Spotify 做的更好。


upload successful

早在 2000 年,Songza 就开始使用人工编辑来进行在线音乐策展(curation,策划并展示)。“人工编辑”意味着需要一些”音乐专家“团队或者其它管理员手动将他们认为很好听的歌放到歌单中去。(后来 Beats Music 也实行了同样的策略)。虽然人工编辑运作的很好,但是它需要手动操作并且过于简单,无法考虑到每个听众个人音乐品味的差别

如 Songza 一样,Pandora 也是音乐策展的元老之一。它采用的方法较为先进,使用人工标注歌曲属性的方法。也就是说,有一组人在听歌之后,为每首歌选择一些描述性的词,对各个曲目进行了标注。然后,Pandora 就能利用代码简单地对标注进行筛选,得到比较类似的歌单。

与此同时,麻省理工学院媒体实验室开发出了名为”The Echo Nest“的智能音乐助手,开创了一种更加先进的个性化音乐推荐方式。The Echo Nest 使用算法分析各个音乐音频与文本的内容,使其能进行音乐识别、个性化推荐、创建歌单以及进行分析。

此外,至今依然存在的 Last.fm 采用了一种名为协同过滤的不同的方法。它可以识别用户可能喜欢的音乐。稍后会详细提到它。


以上就是其它音乐策展服务进行推荐的方法。那么 Spotify 是如何造出它们神奇的引擎,如何做出更加符合用户口味的推荐的呢?

Spotify 的 3 种推荐模型

实际上 Spotify 并没有使用某个革命性的推荐模型 —— 与此相反,他们是将一些其它服务中单一使用的最佳策略混合起来,创建了自己独特、强大的发现引擎。

Spotify 每周推荐的开发者主要采用了如下三种类型的推荐:

  1. 协同过滤模型(就是 Last.fm 最开始使用的模型),通过分析你的行为与他人的行为进行运作。
  2. 自然语言处理(NLP)模型,用于分析文本
  3. 音频 模型,用于分析原始音轨
upload successful

下面让我们深入了解上述各个推荐模型吧!


推荐模型 #1:协同过滤

upload successful

首先简述一些背景:当人们听见”协同推荐“这个词的时候,大多会想起 Netflix 这个首批采用协同过滤推荐模型的公司。他们使用用户对影片的评星来确定将什么影片推荐给其它喜好相似的用户。

当 Netflix 成功使用这种推荐方法之后,开始迅速发展。现在通常被认为是尝试使用推荐模型的鼻祖。

与 Netflix 不同,Spotify 没有让用户对音乐进行评星。他们采用的数据是隐式反馈 —— 具体来说,包括对用户听歌的流数据进行统计,以及收集一些其它的流数据,包括用户是否将歌曲保存到他们自己的歌单、在听完歌之后是否访问了歌手的主页等等。

那么什么是协同过滤,它又是如何运作的呢?这儿用下面这个简短的对话来做个简述:

upload successful

图中发生了什么?图中的两个人都有一些喜欢的歌曲 - 左边的人喜欢歌曲 P、Q、R 及 S;右边的人喜欢歌曲 Q、R、S 及 T。

协同过滤就像用这些数据说:

”Emmmmm,你们都喜欢 Q、R、S 三首歌,所以你们可能是类似的用户。所以,你应该会喜欢对方爱听而你还没听过的歌。“

也就是说,会建议右边的人去听歌曲 P 试试,建议左边的人去听听歌曲 T。这很简单吧!

但 Spotify 是如何将这种方法落到实处,用于由百万级别用户的喜好歌曲来计算百万级别用户的推荐的呢?

……应用数学矩阵,然后使用 Python 库来实现。

upload successful

在实际情况中,你在看到的这个矩阵是巨大无比的,矩阵中的每一行都代表了 Spotify 的 1.4 亿用户(如果你也用 Spotify,那你也会是这个矩阵的一行),每列代表了 Spotify 数据库中的 3000 万首歌

接着,Python 库会长时间、缓慢地对矩阵按照以下分离公式进行计算:

upload successful

在它完成计算之后,我们会得到两种向量,在这里用 X 与 Y 表示。X 是用户向量,代表了单个用户的口味;Y 是歌曲向量,代表了一首歌的属性。

upload successful

现在,我们有了 1.4 亿条用户向量以及 3000 万条歌曲向量。这些向量的内容实质上就是一堆数字,本身没有任何意义。但是对它们进行对比就能起到巨大的作用。

为了找到哪些用户和我有着最相似的口味,协同过滤会将我的向量和其它每个用户的向量进行对比,最终找到与我最相近的用户。同样的,对 Y 向量进行比较,可以找到与你正在听的歌最相近的歌。

协同过滤的效果相当不错,但 Spotify 没有满足于此,他们知道通过增加一些其它的引擎可以使得效果更好。下面让我们看看 NLP。


推荐模型 #2:自然语言处理(NLP)

Spotify 采用的第二种推荐模型是自然语言处理(NLP)模型。顾名思义,这种模型的数据来源就是传统意义上的文字 —— 这些文字来源于歌曲的元数据、新闻文章、博客,以及互联网中的其它文本。

upload successful

NLP 是一种让计算机理解人类语言的能力,是一个庞大的领域。在这儿可以采用一些情感分析 API 来实现。

NLP 背后的机制已经超出了本文的讨论范围。不过我们可以这么来大致概括:Spotify 爬虫不断地查找与音乐有关的博客以及各种文本,并了解人们对特定艺术家及歌曲的看法 —— 谈到这些歌曲人们通常会用什么形容词和语言,以及会同时提到哪些其他的艺术家及歌曲。

虽然我不知道 Spotify 处理数据的细节,但我知道 the Echo Nest 是如何与他们进行协同工作的。他们会将语言处理封装为“文化向量”或者“高频短语”。每个艺术家及歌曲都有着数以千计的高频短语,且每天都在变化。每个短语都有一个权重,用于表示这个短语的重要性(大致来说,就是某人描述这个音乐时会用这个短语的概率)。

upload successful

the Echo Nest 使用的“文化向量”与“高频短语”,Brian Whitman 提供表格

接下来与协同过滤一样,NLP 模型会使用这些短语和权重为每首歌构建一个表示向量,这样就能判断两首歌是否相似了。酷不酷炫?


推荐模型 #3:原始音频模型

upload successful

在开始本章之前,你可能会问:

我们已经在前两个模型中应用了足够多的数据,为什么还需要分析音频本身呢?

首先,引入这第三个模型能使这个惊人的推荐服务的准确率得到进一步的提升。但实际上,使用这个模型还有第二种目的:与前两个模型不同,原始音频模型可以用于处理

举个例子,你的歌手朋友将他的新歌传上了 Spotify,然而他仅有 50 名听众,如果要使用协同过滤显然人数太少了。并且他还没有火起来,在互联网上任何角落都没有被提到过,因此 NLP 模型也没法为他发挥作用。不过幸运的是原始音频模型不会在乎这是新歌还是老歌,有了它的帮助,你朋友的歌就有可能和那些流行的歌一起被加入每周推荐歌单了!

接下来解释“如何”对如此抽象的原始音频进行分析。

…使用 卷积神经网络(CNN)!

卷积神经网络正是人脸识别背后使用的技术。在 Spotify 这个场景中,工程师们使用音频数据来代替像素。下面是神经网络一中结构的实例:

upload successful

这个特制的神经网络有 4 层卷积层,它们在图的左边,看起来像很厚的木板;它还有 3 层全连接层,它们在图的右边,看起来像很窄的木板。输入值是音频帧的频率的表示,在图中以光谱图的形式表示。

音频帧通过这些卷积层后,在最后一个卷积层边你可以看到一个“全局时间池化”层。这个池化层沿整个时间轴进行池化,高效地根据统计学找出在歌曲的时间序列中找到的特征。

在此之后,神经网络会输出它对一首歌的理解,其中包括各种类似时间戳、调性、风格、节奏、音量等典型特征。下图为 Daft Punk 的 “Around the World” 一曲中截取 30 秒片段的数据。

upload successful

图片版权:Tristan Jehan & David DesRoches (The Echo Nest)

最终,这些由一首歌理解到的各种关键的信息可以让 Spotify 理解不同的歌中的一些本质的相似之处,由此基于用户的听歌历史推断出此用户可能会喜欢这首新歌。


以上概况了推荐模型中的三个基本组成部分。正是由这些推荐模型组成的推荐 pipeline,最终构成了强大的每周推荐歌单功能!

upload successful

当然,这些推荐模型还与 Spotify 更大的生态系统息息相关,这个生态系统中包含了海量的数据,使用大量的 Hadoop 集群对推荐系统践行规模化运作,使得这些引擎能够在大尺度、无穷尽的互联网中顺利地分析音乐相关文章以及无比庞大的音频文件。

我希望本文的信息能满足你的好奇心(就像我的好奇心被满足了一样)。现在我正在通过我个性化的每周推荐找到我喜欢的音乐,了解以及欣赏它背后的各种机器学习知识。🎶


**资源: - From Idea to Execution: Spotify’s Discover Weekly (Chris Johnson, ex-Spotify) - Collaborative Filtering at Spotify (Erik Bernhardsson, ex-Spotify) - Recommending music on Spotify with deep learning (Sander Dieleman) -  How music recommendation works — and doesn’t work (Brian Whitman, co-founder of The Echo Nest) - Ever Wonder How Spotify Discover Weekly Works? Data Science (Galvanize) - The magic that makes Spotify’s Discover Weekly playlists so damn good (Quartz) - The Echo Nest’s Analyzer Documentation

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

使用 Keras 创建、评价深度神经网络非常的便捷,不过你需要严格地遵循几个步骤来构建模型。

在本文中我们将一步步地探索在 Keras 中创建、训练、评价深度神经网络,并了解如何使用训练好的模型进行预测。

在阅读完本文后你将了解:

  • 如何在 Keras 中定义、编译、训练以及评价一个深度神经网络。

  • 如何选择、使用默认的模型解决回归、分类预测问题。

  • 如何使用 Keras 开发并运行你的第一个多层感知机网络。

  • 2017 年 3 月更新:将示例更新至 Keras 2.0.2 / TensorFlow 1.0.1 / Theano 0.9.0。

upload successful

题图版权由 Martin Stitchener 所有。

综述

下面概括一下我们将要介绍的在 Keras 中构建神经网络模型的 5 个步骤。

  1. 定义网络。
  2. 编译网络。
  3. 训练网络。
  4. 评价网络。
  5. 进行预测。
Keras 中构建神经网络的 5 个步骤

Keras 中构建神经网络的 5 个步骤

想要了解更多使用 Python 进行深度学习的知识?

免费订阅 2 周,收取我的邮件,探索 MLP、CNN 以及 LSTM 吧!(附带样例代码)

现在点击注册还能得到免费的 PDF 版教程。

点击这里开始你的小课程吧!

第一步:定义网络

首先要做的就是定义你的神经网络。

在 Keras 中,可以通过一系列的层来定义神经网络。这些层的容器就是 Sequential 类。(译注:序贯模型)

第一步要做的就是创建 Sequential 类的实例。然后你就可以按照层的连接顺序创建你所需要的网络层了。

例如,我们可以做如下两步:

1
2
model = Sequential()
model.add(Dense(2))

此外,我们也可以通过创建一个层的数组,并将其传给 Sequential 构造器来定义模型。

1
2
layers = [Dense(2)]
model = Sequential(layers)

网络的第一层必须要定义预期输入维数。指定这个参数的方式有许多种,取决于要建造的模型种类,不过在本文的多层感知机模型中我们将通过 input_dim 属性来指定它。

例如,我们要定义一个小型的多层感知机模型,这个模型在可见层中具有 2 个输入,在隐藏层中有 5 个神经元,在输出层中有 1 个神经元。这个模型可以定义如下:

1
2
3
model = Sequential()
model.add(Dense(5, input_dim=2))
model.add(Dense(1))

你可以将这个序贯模型看成一个管道,从一头喂入数据,从另一头得到预测。

这种将通常互相连接的层分开,并作为单独的层加入模型是 Keras 中一个非常有用的概念,这样可以清晰地表明各层在数据从输入到输出的转换过程中起到的职责。例如,可以将用于将各个神经元中信号求和、转换的激活函数单独提取出来,并将这个 Activation 对象同层一样加入 Sequential 模型中。

1
2
3
4
5
model = Sequential()
model.add(Dense(5, input_dim=2))
model.add(Activation('relu'))
model.add(Dense(1))
model.add(Activation('sigmoid'))

输出层激活函数的选择尤为重要,它决定了预测值的格式。

例如,以下是一些常用的预测建模问题类型,以及它们可以在输出层使用的结构和标准的激活函数:

  • 回归问题:使用线性的激活函数 “linear”,并使用与与输出数量相匹配的神经元数量。
  • 二分类问题:使用逻辑激活函数 “sigmoid”,在输出层仅设一个神经元。
  • 多分类问题:使用 Softmax 激活函数 “softmax”;假如你使用的是 one-hot 编码的输出格式的话,那么每个输出对应一个神经元。

第二步:编译网络

当我们定义好网络之后,必须要对它进行编译。

编译是一个高效的步骤。它会将我们定义的层序列通过一系列高效的矩阵转换,根据 Keras 的配置转换成能在 GPU 或 CPU 上执行的格式。

你可以将编译过程看成是对你网络的预计算。

无论是要使用优化器方案进行训练,还是从保存的文件中加载一组预训练权重,只要是在定义模型之后都需要编译,因为编译步骤会将你的网络转换为适用于你的硬件的高效结构。此外,进行预测也是如此。

编译步骤需要专门针对你的网络的训练设定一些参数,设定训练网络使用的优化算法 以及用于评价网络通过优化算法最小化结果的损失函数尤为重要。

下面的例子对定义好的用于回归问题的模型进行编译时,指定了随机梯度下降(sgd)优化算法,以及均方差(mse)算是函数。

1
model.compile(optimizer='sgd', loss='mse')

预测建模问题的种类也会限制可以使用的损失函数类型。

例如,下面是几种不同的预测建模类型对应的标准损失函数:

  • 回归问题:均方差误差 “mse”。
  • 二分类问题:对数损失(也称为交叉熵)“binary_crossentropy”。
  • 多分类问题:多类对数损失 “categorical_crossentropy”。

你可以查阅 Keras 支持的损失函数

最常用的优化算法是随机梯度下降,不过 Keras 也支持其它的一些优化算法

以下几种优化算法可能是最常用的优化算法,因为它们的性能一般都很好:

  • 随机梯度下降sgd” 需要对学习率以及动量参数进行调参。
  • ADAMadam” 需要对学习率进行调参。
  • RMSproprmsprop” 需要对学习率进行调参。

最后,你还可以指定在训练模型过程中除了损失函数值之外的特定指标。一般对于分类问题来说,最常收集的指标就是准确率。需要收集的指标由设定数组中的名称决定。

例如:

1
model.compile(optimizer='sgd', loss='mse', metrics=['accuracy'])

第三步:训练网络

在网络编译完成后,就能对它进行训练了。这个过程也可以看成是调整权重以拟合训练数据集。

训练网络需要制定训练数据,包括输入矩阵 X 以及相对应的输出 y。

在此步骤,将使用反向传播算法对网络进行训练,并使用在编译时制定的优化算法以及损失函数来进行优化。

反向传播算法需要指定训练的 Epoch(回合数、历元数)、对数据集的 exposure 数。

每个 epoch 都可以被划分成多组数据输入输出对,它们也称为 batch(批次大小)。batch 设定的数字将会定义在每个 epoch 中更新权重之前输入输出对的数量。这种做法也是一种优化效率的方式,可以确保不会同时加载过多的输入输出对到内存(显存)中。

以下是一个最简单的训练网络的例子:

1
model.compile(optimizer='sgd', loss='mse', metrics=['accuracy'])

在训练网络之后,会返回一个历史对象(History oject),其中包括了模型在训练中各项性能的摘要(包括每轮的损失函数值及在编译时制定收集的指标)。

第四步:评价网络

在网络训练完毕之后,就可以对其进行评价。

可以使用训练集的数据对网络进行评价,但这种做法得到的指标对于将网络进行预测并没有什么用。因为在训练时网络已经“看”到了这些数据。

因此我们可以使用之前没有“看”到的额外数据集来评估网络性能。这将提供网络在未来对没有见过的数据进行预测的性能时的估测。

评价模型将会评价所有测试集中的输入输出对的损失值,以及在模型编译时指定的其它指标(例如分类准确率)。本步骤将返回一组评价指标结果。

例如,一个在编译时使用准确率作为指标的模型可以在新数据集上进行评价,如下所示:

1
loss, accuracy = model.evaluate(X, y)

第五步:进行预测

最后,如果我们对训练后的模型的性能满意的话,就能用它来对新的数据做预测了。

这一步非常简单,直接在模型上调用 predict() 函数,传入一组新的输入即可。

例如:

1
predictions = model.predict(x)

预测值将以网络输出层定义的格式返回。

在回归问题中,这些由线性激活函数得到的预测值可能直接就符合问题需要的格式。

对于二分类问题,预测值可能是一组概率值,这些概率说明了数据分到第一类的可能性。可以通过四舍五入(K.round)将这些概率值转换成 0 与 1。

而对于多分类问题,得到的结果可能也是一组概率值(假设输出变量用的是 one-hot 编码方式),因此它还需要用 argmax 函数将这些概率数组转换为所需要的单一类输出。

End-to-End Worked Example

让我们用一个小例子将以上的所有内容结合起来。

我们将以 Pima Indians 糖尿病发病二分类问题为例。你可以在 UCI 机器学习仓库中下载此数据集。

该问题有 8 个输入变量,需要输出 0 或 1 的分类值。

我们将构建一个包含 8 个输入的可见层、12 个神经元的隐藏层、rectifier 激活函数、1 个神经元的输出层、sigmoid 激活函数的多层感知机神经网络。

我们将对网络进行 100 epoch 次训练,batch 大小设为 10,使用 ADAM 优化算法以及对数损失函数。

在训练之后,我们使用训练数据对模型进行评价,然后使用训练数据对模型进行单独的预测。这么做是为了方便起见,一般来说我们都会使用额外的测试数据集进行评价,用新的数据进行预测。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Keras 多层感知机神经网络样例
from keras.models import Sequential
from keras.layers import Dense
import numpy
# 加载数据
dataset = numpy.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = dataset[:,0:8]
Y = dataset[:,8]
# 1. 定义网络
model = Sequential()
model.add(Dense(12, input_dim=8, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# 2. 编译网络
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# 3. 训练网络
history = model.fit(X, Y, epochs=100, batch_size=10)
# 4. 评价网络
loss, accuracy = model.evaluate(X, Y)
print("\nLoss: %.2f, Accuracy: %.2f%%" % (loss, accuracy*100))
# 5. 进行预测
probabilities = model.predict(X)
predictions = [float(round(x)) for x in probabilities]
accuracy = numpy.mean(predictions == Y)
print("Prediction Accuracy: %.2f%%" % (accuracy*100))

运行样例,会得到以下输出:

1
2
3
4
5
6
7
8
9
...
768/768 [==============================] - 0s - loss: 0.5219 - acc: 0.7591
Epoch 99/100
768/768 [==============================] - 0s - loss: 0.5250 - acc: 0.7474
Epoch 100/100
768/768 [==============================] - 0s - loss: 0.5416 - acc: 0.7331
32/768 [>.............................] - ETA: 0s
Loss: 0.51, Accuracy: 74.87%
Prediction Accuracy: 74.87%

总结

在本文中,我们探索了使用 Keras 库进行深度学习时构建神经网络的 5 个步骤。

此外,你还学到了:

  • 如何在 Keras 中定义、编译、训练以及评价一个深度神经网络。
  • 如何选择、使用默认的模型解决回归、分类预测问题。
  • 如何使用 Keras 开发并运行你的第一个多层感知机网络。

你对 Keras 的神经网络模型还有别的问题吗?或者你对本文还有什么建议吗?请在评论中留言,我会尽力回答。

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