0%

NIPS 2000 经典论文 非负矩阵分解算法 翻译

摘要

非负矩阵分解(NMF)是一种可以有效处理多变量数据的方法。本文介绍、分析了两种不同的 NMF 算法,这两种算法仅在更新规则(update rule)中使用的乘性因子(multiplicative factor)有所区别。其中一种可以对传统的最小二乘误差进行最小化(minimize),而另一种可以对广义 Kullback-Leibler 散度(KL 散度)进行最小化。可以使用与证明最大化期望算法收敛性类似的辅助函数来证明这两种算法的单调收敛性。这两种算法均可理解为用斜向最陡下降法(diagonally rescaled gradient descent)对因子进行最优化,以保证算法收敛。

简介

PCA、矢量量化(Vector Quantization)等无监督学习算法可以理解为在不同约束条件下对数据矩阵进行分解。根据其约束的不同,分解所得的因子的会表现出大相径庭的性质。比如,PCA 仅使用了弱正交约束,从而得到非常分散的表示,对这些表示使用消去法来产生多样性;矢量量化使用一种严格的全局最优型约束,最终会得到互斥的数据聚类原型。

我们之前已经证明过,在矩阵分解用于学习数据的部分表示中,非负性(non-negative)是一种非常有用的约束。学习得到的非负基向量是分散的,但仍可通过稀疏的组合,在重建时得到效果良好的表达向量。在本文中,我们详细分析了这两种用于在数据中学习最优的非负因子的数值算法。

非负矩阵分解

下面我们正式开始分析如何用算法解决以下问题:

在非负矩阵分解(NMF)中,给定非负矩阵V,找到非负矩阵因子W和H,使得:

\[(1): V\approx WH \]

NMF 可以应用下面的方法用于对多变量数据进行统计分析。给定一组多变量的 n 维数据向量,其向量位于一个 \(n\times x\) 矩阵 V 的列中(m 表示数据集中的示例数)。然后将此矩阵近似分解为 \(n\times r\) 的 W 矩阵与 \(r\times m\)的 H 矩阵。通常 r 要小于 n 或 m,以使 W 和 H 小于原始矩阵 V。最终得到的是原始数据矩阵的压缩形态。

公式(1)中约等于的意义在于它可以将公式逐列用 \(v\approx Wh\) 来表示,其中 v 和 h 是矩阵 V 和矩阵 H 的对应的列。也就是说,每个数据向量 v 近似地由矩阵 W 的各列线性组合而成,同时用 h 的分量进行加权。因此可以被认为 W 包含了对 V 中的数据的线性近似优化的基向量。由于要使用少量的基向量来表示大量的数据向量,因此只有在基向量发现数据中的潜在结构时才能实现较好的近似。

本文不会涉及关于 NMF 的应用,而会侧重于在技术方面探讨非负矩阵分解的技术。当然,已经有许多其它的矩阵分解方式在数值线性代数中得到了广泛的研究,但是以前的大多数工作都不适用于非负性约束情况。

在此,我们讨论了基于迭代更新 W 和 H 的两种 NMF 算法。由于这两种算法易于实现,同时能保证其收敛性,因此它们在现实情况中非常实用。其他算法可能在总计算时间方面更有效率,但是更难实现,并且很难推广到不同的代价函数(cost function)。因子与我们类似的算法,已经被用于对发射断层扫描和天文图像进行反卷积(deconvolution)。

在我们算法的每次迭代中,会用当前值乘某些取决于公式(1)中的“近似程度”的因数,来找到 W 或 H 的新值。我们可以证明“近似程度”会随着不断应用这些乘法更新规则而单调减小。这正意味着更新规则的重复迭代可以保证矩阵分解算法收敛到局部最优。

代价函数

为了找到$ VWH $的近似解,我们首先需要定义一个代价函数,用以量化近似的程度。可以使用两个非负矩阵 A 和 B 的距离来构造此代价函数。一种使用的距离度量方法为:计算 A 和 B 之间的欧几里得距离(Euclidean distance)的平方值。

\[(2): ||A-B||^2 = \sum_{ij}(A_{ij} - B_{ij})^2 \]

此公式下界为 0,仅当 A=B 时距离消失。

另一种实用的度量方式为:

\[(3): D(A||B) = \sum_{ij}(A_{ij} \log{\frac{A_{ij}}{B_{ij}}} - A_{ij}+B_{ij})\]

与欧几里得距离相同,它的下界也为 0,且在 A=B 时距离消失。但它不能被称为“距离”,因为这个式子在 A 与 B 中并不对称,因此我们将其称为 A 对于 B 的“散度”(divergence)。它可以归纳为 KL 散度或者相对熵,当 \(\sum_{ij}A_{ij}=\sum_{ij}B_{ij}=1\) 时,A 与 B 可以看做是标准化的概率分布。

现在,我们可以按照以下两种公式来将 NMF 化为最优化问题:

最优化问题1:在约束条件 \(W, H \geq 0\) 下,以 W 和 H 作为参数,最小化 \(||V - WH||^2\)

最优化问题2:在约束条件 \(W, H \geq 0\) 下,以 W 和 H 作为参数,最小化 \(D(V||WH)\)

虽然方程 \(||V - WH||^2\)\(D(V||WH)\) 在只考虑 W 或 H 之一时为凸,但在同时考虑 WH 两个变量时不为凸。因此,寻找一种可以找到全局最小值的算法去解决以上两个最优化问题是不切实际的。但是,还有许多数值优化方法可以用于寻找局部最小值。

虽然梯度下降法(Gradient descent)的收敛速度很慢,但它的实现最为简单。其它方法(比如共轭梯度法)可以更快地收敛(至少在局部最小值附近会更快),但是它们比梯度下降更复杂。此外,梯度下降方法的收敛对步长的选择非常敏感,这对于大规模应用十分不利。

乘法更新规则

我们发现在解决上述两个最优化问题时,在速度与实现难度中权衡,“乘法更新规则”是一种综合性能很好方法。

定理1:欧几里得距离 \(||V-WH||\) 在下面的更新规则中呈非增:

\[(4): H_{a\mu} \leftarrow H_{a\mu}\frac{(W^T V)_{a\mu}}{(W^T W H)_{a\mu}}\]

\[(4): W_{ia} \leftarrow W_{ia}\frac{(V H^T)_{ia}}{(W H H^T)_{ia}}\]

在上述更新规则中,W 与 H 在距离公式的驻点上时,欧几里得距离将固定不动。

定理2:散度 \(D(V|WH)\) 在下面的更新规则中呈非增:

\[(5): H_{a\mu} \leftarrow H_{a\mu}\frac{\frac{\sum_{i}W_{ia}V_{i\mu}}{WH_{i\mu}}}{\sum_k W_{ka}}\]

\[(5): W_{ia} \leftarrow W_{ia}\frac{\frac{\sum_{\mu}H_{a\mu}V_{i\mu}}{WH_{i\mu}}}{\sum_v H_{av}}\]

在上述更新规则中,W 和 H 在散度公式的驻点上时,散度将不再更新。

上述定理的证明将在后面给出。我们可以发现,每次更新都是乘以一个因子。特别地,当V = WH时,可以直观地看出这个乘数因子是一样的,当更新规则固定时,才会得到完美的分解。

乘法与加法更新规则

可以将乘法更新与梯度下降更新进行对比。特别的,对 H 进行更新以减小平方距离可以记为:

\[ (6):H_{a\mu} \leftarrow H_{a\mu} + \eta_{a\mu}[(W^TV)_{a\mu} - (W^T WH)_{a\mu}] \]

如果将 \(\eta_{a\mu}\) 设置为较小的正数,上式就和常规的梯度下降等价。只要数字充分小,就能在更新时减小 \(||V-WH||\)

如果我们按照斜向最陡调整变量,并设置:

\[ (7): \eta_{a\mu}=\frac{H_{a\mu}}{(W^TWH)_{a\mu}}\]

就能得到定理 1 中给出的 H 更新规则。注意,该调整可得出乘性因子(分母中的梯度的正分量和因子的分子中的负分量的绝对值)。

对于散度公式,我们按照下述公式调整斜向最陡梯度下降:

\[ (8): H_{a\mu} \leftarrow H_{a\mu} + \eta_{a\mu}[\sum_{i}W_{ia}\frac{V_{i\mu}}{(WH)_{i\mu}}-\sum_{i}W_{ia}]\]

同样的,如果将 \(\eta_{a\mu}\) 设置为较小的正数,上式就和常规的梯度下降等价。只要数字充分小,就能在更新时减小 \(D(V||WH)\)。如果设置:

\[ (9): \eta_{a\mu} = \frac{H_{a\mu}}{\sum_i W_{ia}}\]

那么就能得到定理 2 中给出的 H 更新规则。该调整可得出乘性因子(分母中的梯度的正分量和因子的分子中的负分量的绝对值)。

由于我们对 \(\eta_{a\mu}\) 的取值并不够小,看起来不能保证这种调整过后的梯度下降的代价函数减小。不过让人惊讶的是,如下节所示,上述假设是事实。

收敛证明

我们将使用一个类似于 EM 算法的辅助函数来证明定理 1 与定理 2。

定义 1\(G(h,h')\)\(F(h)\) 的辅助函数,满足以下条件成立:

\[(10): G(h,h')\geq F(h), G(h,h)=F(h) \]

根据下面的引理,此辅助函数是一个有用的概念。(在图1中的插图也显示了这一点)

引理 1:如果 G 为辅助函数,则 F 在下述更新时为非增:

\[(11): h^{t+1} = \arg\min_{h}G(h,h^t)\]

证明\(F(h^{t+1}) \leq G(h^{t+1}, h^t) \leq G(h^t,h^t) = F(h^t)\)

请注意,只有在\(h^t\)\(G(h,h^t)\)的全局最小值时满足\(F(h^{t+1})=F(h^t)\)。如果 F 的导数存在,且在\(h^{t}\)的邻域连续,也就是说$F(h^t) = 0 $。因此通过公式11反复更新,我们就能得到目标函数收敛的局部最小值 \(h_{min} = \arg\min_h F(h)\)

\[(12): F(h_{min}) \leq ... F(h^{t+1})\leq F(h^t) ... \leq F(h_2) \leq F(h_1) \leq F(h_0)\]

-w663

下面,我们证明如何为\(||V-WH||\)\(D(V,WH)\)定义适当的辅助函数\(G(h,h^t)\)。定理 1 与定理 2 可以直接遵循公式 11 的更新规则。

引理 2:如果\(K(h^t)\)为对角矩阵,

\[(13): K_{ab}(h^t) = \delta_{ab}\frac{W^T Wh^t}{h^t_a}\]

\[(14): G(h,h^t)=F(h^t)+(h-h^t)^T \nabla F(h^t) + \frac{1}{2}(h-h^t)^T K(h^t)(h-h^t)\]

\[(15): F(h)=\frac{1}{2} \sum_i(v_i- \sum_a W_{ia} h_a)^2\]

的辅助函数。

证明:因为显然 \(G(h,h)=F(h)\),因此只需证明 \(G(h,h') \geq F(h)\)。为了证明此不等式,需要将

\[(16): F(h) = F(h^t) + (h-h^t)^T \nabla F(h^t) + \frac{1}{2}(h-h^t)^T(W^TW)(h-h^t)\]

与公式 14 进行对比,发现 \(G(h,h') \geq F(h)\) 等价于

\[(17): 0 \leq (h-h^t)^T[K(h^t) - W^TW](h-h^t)\]

为证明半正定情况,考虑矩阵:

\[(18): M_{ab}(h^t)=h_a^t(K(h^t)-W^TW)_{ab}h_b^t\]

仅是\(K-W^TW\)的调整形式。因此,仅当 M 符合下列公式时,\(K-W^TW\)具有半正定性:

\[(19):v^T Mv = \sum_{ab}v_a M_{ab} v_b \\ (20):=\sum_{ab}h^t_a(W^TW)_{ab}h^t_bv_a^2-v_ah^t_a(W^TW)_{ab}h_b^tv_b \\ (21):=\sum_{ab}(W^TW)_{ab}h_a^th_b^t[\frac{1}{2}v_a^2 + \frac{1}{2}v_b^2 - v_av_b] \\ (22):=\frac{1}{2}\sum_{ab}(W^TW)_{ab}h_a^th_b^t(v_a-v_b)^2 \\ (23):\geq 0\]

现在,我们可以证明定理 1 的收敛性。

定理 1 证明:使用公式14的结果替换公式11中的\(G(h,h^t)\),得到更新规则:

\[(24): h^{t+1}=h^t - K(h^t)^{-1} \nabla F(h^t)\]

因为公式14为辅助函数,根据引理1,F 在更新规则中为非增。将上式完整的写下来,可以得到:

\[(25): h^{t+1}_a= h^{t}_a \frac{(W^Tv)_a}{(W^TWh^t)_a}\]

反转引理 1 与引理 2 中 W 和 H 的角色,F 可以以类似的方法证明在 W 的更新规则下为非增。

接下来,我们为散度代价方程寻找辅助函数。

引理 3:定义

\[(26): G(h,h^t)=\sum_i(v_i \log{v_i} - v_i) + \sum_{ia} W_{ia}h_a \\ (27):-\sum_{ia}v_i\frac{W_{ia}h^t_a}{\sum_b W_{ib} h^t_b} (\log{W_{ia} h_a - \log{\frac{W_{ia}h^t_a}{\sum_b W_{ib} h^t_b}}})\]

\[(28): F(h)=\sum_i v_i \log(\frac{v_i}{\sum_a W_{ia} h_a})- v_i + \sum_a W_{ia} h_a \]

的辅助函数。

证明:因为显然 \(G(h,h)=F(h)\),因此只需证明 \(G(h,h') \geq F(h)\)。我们通过对数函数的凸性来推导此不等式:

\[(29): -\log \sum_a W_{ia} h_a \leq -\sum_a a_a \log \frac{ W_{ia} h_a}{a_a} \]

上式对所有的联合求合数 \(a_a\) 均成立。设

\[(30): a_a =\frac{ W_{ia} h_a}{\sum_b W_{ib} h_b} \]

可以得到:

\[(31): -\log \sum_a W_{ia} h_a \leq - \sum_a \frac{ W_{ia} h_a}{\sum_b W_{ib} h_b} (\log W_{ia} h_a - \log\frac{ W_{ia} h_a}{\sum_b W_{ib} h_b} ) \]

上面的不等式遵循 \(G(h,h') \geq F(h)\)

定理 2 的证明遵循引理 1 及其应用:

定理 2 证明:要令 \(G(h,h^t)\) 最小化,则需要将梯度设为 0 来求出 h:

\[(32): \frac{dG(h,h^t)}{dh_a} = - \sum_i v_i \frac{ W_{ia} h_a^t}{\sum_b W_{ib} h_b^t} \frac{1}{h_a} + \sum_i W_{ia} = 0\]

因此,公式 11 采取的更新规则应当如下所示:

\[ (33): h_a^{t+1} = \frac{h_a^t}{\sum_b W_{kb}} \sum_i \frac{v_i}{\sum_b W_{ib}h_b^t} W_{ia} \]

因为 G 为辅助函数,公式 28 中的 F 在更新规则中为非增。用矩阵形式重写上述公式,发现与 公式 5 的更新规则等价。反转 W 和 H 的角色,可以以类似的方法证明 F 在 W 的更新规则下为非增。

讨论

我们已经证明了在公式 4 与公式 5 中应用更新规则,可以找到问题 1 与问题 2 的局部最优解。借助定义合适的辅助函数证明了函数的收敛性。我们将把这些证明推广到更复杂的约束条件下去。更新规则在计算上非常容易实现,有望进行广泛的应用。

感谢贝尔实验室的支持,以及 Carlos Brody, Ken Clarkson, Corinna Cortes, Roland Freund, Linda Kaufman, Yann Le Cun, Sam Roweis, Larry Saul, Margaret Wright 的帮助与讨论。

概述

提取关键词将得到一系列的词库,要将这些词应用于文本筛查、分类则必须要对这些词进行拆分、组合,形成特定的规则,再应用这些规则对文本进行匹配,得到某文本所对应的模式。

要生成规则,即需要从系列关键词通过某些算法找出在不同样本文本下关键词的共有特征(或规律),再对多种特征(或规律)进行组合得到某种策略,最终通过策略对目标文本进行判定。因此关键词规则生成可以总结为以下几个主要步骤:

  1. 关键词特征或规律查找
  2. 策略生成
  3. 策略优化与化简

对于关键词特征与规律的查找,可以视为机器学习中的分类问题的反向过程。分类问题简要来说即给定标注样本,通过特征训练,得到一个可适用于同类样本的分类策略。常用的分类方法包括决策树、SVM、神经网络等方法。

决策树算法是常用的数据分类算法。决策树算法具有以下优点:

  1. 决策树算法中学习简单的决策规则建立决策树模型的过程非常容易理解
  2. 决策树模型可以可视化,非常直观
  3. 应用范围广,可用于分类和回归,而且非常容易做多类别的分类
  4. 能够处理数值型和连续的样本特征

由于它生成分类策略的可解释性,以及树状结构的可编辑性,在本步骤中可选用决策树算法作为样本分类策略。

下图为对某样本文本进行决策树(CART)生成的分类器策略可视化图像:

-w889

可以从图片中看出,决策树算法根据样本标注训练而成了对应的基于关键词的文本分类器。此分类器策略为清晰的树状结构,每个节点代表一个判断条件,每个树杈代表一个条件分支。为了得到在特定计算性能高、可解释性更好的策略,需要对其进行进一步的策略挖掘及精简。

由于决策树得到的策略为树状结构,因此在这种结构上的进一步策略挖掘可选用盲目搜索算法;又由于此模型为树状结构,最终分类落于叶子节点中,故采用深度优先搜索(DFS)对此树的路径进行检索,进一步优化策略。

在决策树得到的规则通过深度优先搜索之后,会得到一系列的组别,每个组别中包含各个关键词的“与”和“或”两种逻辑关系。在目标文本中使用时,即可通过这些逻辑关系的匹配找出对应的模式。

但由于决策树得到的策略叶子节点通常较多(如上图所示),通过深度优先搜索得到的策略也非常繁杂,最终生成得到的规则也会非常复杂,因此还可以进行进一步的策略化简。

在此步骤中,实质上是对多个逻辑代数运算及逻辑代数复合运算组合而成的逻辑函数进行化简。逻辑函数的化简方法一般有如下三种:

  1. 公式化简法
  2. 卡诺图法
  3. Q-M 法

由于在此步骤实际应用时逻辑规则较为繁杂,且“与”—“或”逻辑对较多,比起公式化简法或卡诺图法,Q-M 算法在选取出可以覆盖逻辑规则真值的最小质蕴涵项时更能发挥优势。

最终,通过决策树算法生成树、对决策树进行深度优先搜索得到包含“与”、“或”的分类逻辑、对分类逻辑应用 Q-M 算法,最终得到了包含“与”、“或”、“非”三种逻辑关系的可解释性强、精简的规则。

决策树

决策树是一个树结构,其每个非叶节点表示一个特征属性上的测试,每个分支代表这个特征属性在某个值域上的输出,而每个叶节点存放一个类别。运用决策树进行决策的过程就是从根节点开始,测试待分类项中相应的特征属性,并按照其值选择输出分支,直到到达叶子节点,将叶子节点存放的类别作为决策结果。

决策树算法是一种逼近离散函数值的方法。它是一种典型的分类方法,首先对数据进行处理,利用归纳算法生成可读的规则和决策树,然后使用决策对新数据进行分析。本质上决策树算法是通过一系列规则对数据进行分类的过程。

决策树算法构造决策树来发现数据中蕴涵的分类规则.如何构造精度高、规模小的决策树是决策树算法的核心内容。决策树构造可以分两步进行:

第一步,决策树的生成:由训练样本集生成决策树的过程。一般情况下,训练样本数据集是根据实际需要有历史的、有一定综合程度的,用于数据分析处理的数据集。

第二步,决策树的剪枝:决策树的剪枝是对上一阶段生成的决策树进行检验、校正和修下的过程,主要是用新的样本数据集(称为测试数据集)中的数据校验决策树生成过程中产生的初步规则,将那些影响预衡准确性的分枝剪除。

在第一步决策树的生成中,需要确定树杈的分裂属性(即在多个自变量中,优先选择哪个自变量进行分叉)。而采用何种计算方式选择树杈决定了决策树算法的类型,典型的分裂属性的选择的方法有 ID3 算法、C4.5 算法、CART 算法三种,三种决策树算法选择树杈的方式是不一样的。

ID3 算法

ID3 算法是目前决策树算法中较有影响力的算法,于 1986 年由 Quinlan 提出。该算法只是一个启发式算法。ID3 算法的核心是判断测试哪个属性为最佳的分类属性。ID3 算法选择分裂后信息增益最大的属性进行分裂,以信息增益度量属性选择。ID3 算法中常用到的两个概念是熵和信息增益。

熵是刻画任意样本例集的纯度,如果目标属性具有 m 个不同的值,那么 D 相对于 m 这个状态的分类的熵定义为:

\[ \text{inf}\ o(D) = -\sum^m_{i=1}p_i \log_2 (P_i) \]

其中 Pi 表示 Pi 是 m 类别的比例。

一个属性的信息增益就是由于使用这个属性分割样例而导致的期望熵降低,更精确来讲,一个属性A相对样本例集合S的信息增益 Gain(S,A) 被定义为:

\[ gain(A) = info(D) - infoA(D) \]

A 对 D 划分的期望信息为:

\[ \text{inf}\ o_A(D) = \sum^v_{j=1} \frac{|D_j|}{|D|} \text{inf}\ o(D_j) \]

ID3 算法不足之处是只能处理离散型数据,信息增益的选择分裂属性的方式会偏向选择具有大量值得属性。

C4.5 算法

ID3 算法在实际应用中存在一些问题,于是 Quilan 在保留 ID3 算法优点基础上提出了 C4.5 算法,C4.5 算法只是 ID3 算法的改进算法。C4.5 算法采用最大信息增益率的属性被选为分裂属性。C4.5 算法中用到了“分裂信息”这一概念,该概念可以表示为:

\[ split\_ \text{inf}\ o_A(D) = -\sum^v_{j=1} \frac{|D_j|}{|D|} \log_2\frac{|D_j|}{|D|} \]

信息增益率的定义是:

\[ gain\_ratio(A) = \frac{gainA}{split\_\text{inf}\ o(A)} \]

C4.5 算法是对 ID3 算法的一种改进,改进后可以计算连续型属性的值。对于连续型属性的值,只需将连续型变量由小到大递增排序,取相邻连个值的中点作为分裂点,然后按照离散型变量计算信息增益的方法计算信息增益,取其中最大的信息增益作为最终的分裂点。

C4.5 算法继承了 ID3 算法的优点,并在以下几方面对 ID3 算法进行了改进:

  • 用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足;

  • 在树构造过程中进行剪枝;

  • 能够完成对连续属性的离散化处理;能够对不完整的数据进行处理。

CART 算法

CART 算法选择分裂属性的方式首先要计算不纯度,然后利用不纯度计算 Gini 指标,然后计算有效子集的不纯度和 Gini 指标,选择最小的 Gini 指标作为分裂属性。

\[ Gini(D) = 1 - \sum^n_{i=0}(\frac{Di}{D})^2 \]

\[ Gini(D|A) = \sum^n_{i=0} \frac{Di}{D} Gini(D i)\]

由于 CART 算法在处理离散数据上具有优势,因此对于关键词规则的生成可选用此方法生成决策树。

深度优先搜索算法

搜索算法可分为盲目搜索算法和启发式搜索算法两种,二者的区别在于:启发式搜索算法的每一步都试图向着目标状态方向进行搜索,而盲目搜索算法则是每一步按照固定的规则进行搜索,然后判断是否达到目标状态。相比之下,启发式搜索算法克服了盲目搜索算法的盲目性问题。

虽然启发式搜索算法可以解决盲目搜索算法的盲搜索问题,但是在实际问题求解中,缺少一些必要的信息来构建启发式搜索算法,此时采用盲目搜索算法仍然是解决问题的有效手段。盲目搜索算法有两种:一种按照广度优先展开搜索树的搜索算法,叫广度优先搜索法;另一种则是按照深度优先展开搜索的搜索算法,叫深度优先搜索法算法。

深度优先搜索算法是优先扩展尚未扩展的且具有最大深度的节点;广度优先搜索法是在扩展完第 K 层的节点后才扩展 K+1 层的节点。在此应用深度优先搜索算法。

假设初始状态是图中所有顶点未曾被访问,从图中某个顶点 i 出发,访问此顶点,然后依次从 i 的未被访问的邻接点出发深度优先遍历图,直至图中所有和 i 有路径的顶点都被访问为止。若此时图中尚有顶点未被访问,则另选图中未曾访问的顶点为起始点,重复上述过程,直至图中所有顶点都被访问为止。在节点遍历过程中,应该注意节点的回溯搜索以及避免节点被重复搜索。

在搜索过程中,如何避免被搜过的节点不被重复搜索以及节点的回溯是主要难点。根据关联矩阵以及深度优先搜索算法,从节点 i 搜索出节点 j,如果节点 j 已经被搜索过,那么修改关联矩阵中对应节点 i 和节点 j 的元素为 0,并返回节点 i 重新搜索与之相联的另一节点。当节点 j 是该条树枝的最后一个节点时,修改关联矩阵中相应的元素,并且返回节点 i 重新搜索与之相连的另一树枝。依此类推,直至遍历所有节点,也就是关联矩阵的所有元素都变为 0 时,停止搜索。在遍历各节点的同时,根据节点的先后顺序以及树枝集合,合理地安排各节点坐标。其搜索过程逻辑如下图所示:

-w241

Q-M 算法

在得到只包含“与”、“或”的逻辑函数之后,可对逻辑进行更进一步的简化与合并,并加上“非”逻辑来使得逻辑函数更具解释性。

下图为一个简单逻辑函数的卡诺图,101 项被覆盖了 3 次,明显可对函数进行化简。

-w386

Q-M 算法是由 Quine 和 Mccluskey 提出的一种系统列表化简法。这种化简方法和卡诺图化简法的基本思想大致相同, 是通过找出函数 F 的全部质蕴涵项、必要质蕴涵项以及最简质蕴涵项集来求得最简表达式。

下面为蕴含项与质蕴涵项的定义:

蕴涵项:在函数的“与”—“或”表达式中,每个“与”项被称为该函数的蕴涵项。显然在函数卡诺图中,任何一个 1 方格所对应的最小项或者卡诺圈中的 \(2^n\) 个 1 方格所对应的“与”项都是函数的蕴涵项。

质蕴涵项:若函数的一个蕴涵项不是该函数中其它蕴涵项的子集,则此蕴涵项称为质蕴涵项,简称为质项。显然在函数卡诺图中,按照最小项合并规律,如果某个卡诺圈不可能被其它更大的卡诺圈包含,那么该卡诺圈所对应的“与”项为质蕴涵项。

必要质蕴涵项:若函数的一个质蕴涵项所包含的某一个最小项不被函数的其它任何质蕴涵项包含,则此质蕴涵项被称为必要质蕴涵项,简称为必要质项。在函数卡诺图中,若某个卡诺圈包含了不可能被任何其它卡诺圈包含的 1 方格,那么该卡诺圈所对应的“与”项为必要质蕴涵项。

一般的化简步骤是:

第一步:将函数表示成“最小项之和”形式,并用二进制码表示每—个最小项。

第二步:找出函数的全部质蕴涵项。先将 n 个变量函数中的相邻最小项合并,消去相异的—个变量,得到 (n-1) 个变量的“与”项,再将相邻的 (n-1) 个变量的“与”项合并,消去相异的变量,得到 (n-2) 个变量的“与”项。依此类推,直到不能再合并为止。所得到的全部不能再合并的“与”项(包括不能合并的最小项),即所要求的全部质蕴涵项。

第三步:找出函数的必要质蕴涵项。

第四步:找出函数的最小覆盖。

初学者的并行编程指南

upload successful

在参加 Kaggle 的 Understanding the Amazon from Space 比赛时,我试图对自己代码的各个部分进行加速。速度在 Kaggle 比赛中至关重要。高排名常常需要尝试数百种模型结构与超参组合,能在一个持续一分钟的 epoch 中省出 10 秒都是一个巨大的胜利。

让我吃惊的是,数据处理是最大的瓶颈。我用了 Numpy 的矩阵旋转、矩阵翻转、缩放及裁切等操作,在 CPU 上进行运算。Numpy 和 Pytorch 的 DataLoader 在某些情况中使用了并行处理。我同时会运行 3 到 5 个实验,每个实验都各自进行数据处理。但这种处理方式看起来效率不高,我希望知道我是否能用并行处理来加快所有实验的运行速度。

Read more »

本文将与你一同探索 PhpFastCache 库,来为你的 PHP 应用实现缓存功能。通过缓存功能,能够提升网站的整体性能与页面加载速度。

什么是 PhpFastCache?

PhpFastCache 是一个能让你轻松在 PHP 应用中实现缓存功能的库。它的功能强大,且简单易用,提供了一些 API 以无痛实现缓存策略。

PhpFastCache 不是一个纯粹的传统文件系统式缓存。它支持各种各样的文件适配器(Files Adapter),可以让你选择 Memcache、Redis、MongoDB、CouchDB 等高性能的后端服务。

让我们先总览一遍最流行的适配器:

  • 文件系统
  • Memcache、Redis 和 APC
  • CouchDB 和 MongoDB
  • Zend Disk Cache 和 Zend Memory Cache

如果你用的文件适配器不在上面的列表中,也可以简单地开发一个自定义驱动,插入到系统中,同样也能高效地运行。

除了基本功能外,PhpFastCache 还提供了事件机制,可以让你对预定义好的事件进行响应。例如,当某个事物从缓存中被删除时,你可以接收到这个事件,并去刷新或删除相关的数据。

在下面的章节中,我们将通过一些示例来了解如何安装及配置 PhpFastCache。

安装与配置

在本节中,我们将了解如何安装及配置 PhpFastCache。下面是几种将它集成进项目的方法。

如果你嫌麻烦,仅准备下载这个库的 .zip 或者 .tar.gz 文件,可以去官方网站直接下载。

或者你也可以用 Composer 包的方式来安装它。这种方式更好,因为在之后的维护和升级时会更方便。如果你还没有安装 Composer,需要先去安装它。

当你安装好 Composer 之后,可以用以下命令下载 PhpFastCache:

1
$composer require phpfastcache/phpfastcache

命令完成后,你会得到一个 vendor 目录,在此目录中包括了全部 PhpFastCache 所需的文件。另外,如果你缺失了 PhpFastCache 依赖的库或插件,Composer 会提醒你先去安装依赖。

你需要找到 composer.json 文件,它类似于下面这样:

1
2
3
4
5
{
"require": {
"phpfastcache/phpfastcache": "^6.1"
}
}

无论你通过什么方式来安装的 PhpFastCache,都要在应用中 include autoload.php 文件。

如果你用的是基于 Composer 的工作流,autoload.php 文件会在 vendor 目录中。

1
2
// Include composer autoloader
require '{YOUR_APP_PATH}/vendor/autoload.php';

另外,如果你是直接下载的 .zip.tar.gzautoload.php 的路径会在 src/autoload.php

1
2
// Include autoloader
require '{YOUR_APP_PATH}/src/autoload.php';

只要完成上面的操作,就能开始进行缓存,享受 PhpFastCache 带来的好处了。在下一章节中,我们将以一个简单的示例来介绍如何在你的应用中使用 PhpFastCache。

示例

前面我提到过,PhpFastCache 支持多种文件适配器进行缓存。在本节中,我会以文件系统和 Redis 这两种文件适配器为例进行介绍。

使用文件适配器进行缓存

创建 file_cache_example.php 文件并写入下面的代码。在此我假设你使用的是 Composer workflow,因此 vendor 目录会与 file_cache_example.php 文件同级。如果你是手动安装的 PhpFastCache,需要根据实际情况修改文件结构。

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
41
<?php
/**
* file_cache_example.php
*
* Demonstrates usage of phpFastCache with "file system" adapter
*/

// Include composer autoloader
require __DIR__ . '/vendor/autoload.php';

use phpFastCache\CacheManager;

// Init default configuration for "files" adapter
CacheManager::setDefaultConfig([
"path" => __DIR__ . "/cache"
]);

// Get instance of files cache
$objFilesCache = CacheManager::getInstance('files');

$key = "welcome_message";

// Try to fetch cached item with "welcome_message" key
$CachedString = $objFilesCache->getItem($key);

if (is_null($CachedString->get()))
{
// The cached entry doesn't exist
$numberOfSeconds = 60;
$CachedString->set("This website uses PhpFastCache!")->expiresAfter($numberOfSeconds);
$objFilesCache->save($CachedString);

echo "Not in cache yet, we set it in cache and try to get it from cache!</br>";
echo "The value of welcome_message:" . $CachedString->get();
}
else
{
// The cached entry exists
echo "Already in cache!</br>";
echo "The value of welcome_message:" . $CachedString->get();
}

现在,我们一块一块地来理解代码。首先看到的是将 autoload.php 文件引入,然后导入要用到的 namespace:

1
2
3
4
// Include composer autoloader
require __DIR__ . '/vendor/autoload.php';

use phpFastCache\CacheManager;

当你使用文件缓存的时候,最好提供一个目录路径来存放缓存系统生成的文件。下面的代码就是做的这件事:

1
2
3
4
// Init default configuration for "files" adapter
CacheManager::setDefaultConfig([
"path" => __DIR__ . "/cache"
]);

当然,你需要确保 cache 目录存在且 web server 有写入权限。

接下来,我们将缓存对象实例化,用 welcome_message 加载对应的缓存对象。

1
2
3
4
5
6
7
// Get instance of files cache
$objFilesCache = CacheManager::getInstance('files');

$key = "welcome_message";

// Try to fetch cached item with "welcome_message" key
$CachedString = $objFilesCache->getItem($key);

如果缓存中不存在此对象,就将它以 60s 过期时间加入缓存,并从缓存中读取与展示它。如果它存在于缓存中,则直接获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (is_null($CachedString->get()))
{
// The cached entry doesn't exist
$numberOfSeconds = 60;
$CachedString->set("This website uses PhpFastCache!")->expiresAfter($numberOfSeconds);
$objFilesCache->save($CachedString);

echo "Not in cache yet, we set it in cache and try to get it from cache!</br>";
echo "The value of welcome_message:" . $CachedString->get();
}
else
{
// The cached entry exists
echo "Already in cache!</br>";
echo "The value of welcome_message:" . $CachedString->get();
}

非常容易上手对吧!你可以试着自己去运行一下这个程序来查看结果。

当你第一次运行这个程序时,应该会看到以下输出:

1
2
Not in cache yet, we set it in cache and try to get it from cache!
The value of welcome_message: This website uses PhpFastCache!

之后再运行的时候,输出会是这样:

1
2
Already in cache!
The value of welcome_message: This website uses PhpFastCache!

现在就能随手实现文件系统缓存了。在下一章节中,我们将模仿这个例子来使用 Redis Adapter 实现缓存。

使用 Redis Adapter 进行缓存

假定你在阅读本节前已经安装好了 Redis 服务,并让它运行在 6379 默认端口上。

下面进行配置。创建 redis_cache_example.php 文件并写入以下代码:

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
41
42
43
44
<?php
/**
* redis_cache_example.php
*
* Demonstrates usage of phpFastCache with "redis" adapter
*
* Make sure php-redis extension is installed along with Redis server.
*/

// Include composer autoloader
require __DIR__ . '/vendor/autoload.php';

use phpFastCache\CacheManager;

// Init default configuration for "redis" adapter
CacheManager::setDefaultConfig([
"host" => '127.0.0.1',
"port" => 6379
]);

// Get instance of files cache
$objRedisCache = CacheManager::getInstance('redis');

$key = "welcome_message";

// Try to fetch cached item with "welcome_message" key
$CachedString = $objRedisCache->getItem($key);

if (is_null($CachedString->get()))
{
// The cached entry doesn't exist
$numberOfSeconds = 60;
$CachedString->set("This website uses PhpFastCache!")->expiresAfter($numberOfSeconds);
$objRedisCache->save($CachedString);

echo "Not in cache yet, we set it in cache and try to get it from cache!</br>";
echo "The value of welcome_message:" . $CachedString->get();
}
else
{
// The cached entry exists
echo "Already in cache!</br>";
echo "The value of welcome_message:" . $CachedString->get();
}

如你所见,除了初始化 Redis 适配器的配置一段之外,这个文件与之前基本一样。

1
2
3
4
5
// Init default configuration for "redis" adapter
CacheManager::setDefaultConfig([
"host" => '127.0.0.1',
"port" => 6379
]);

当然如果你要在非本机运行 Redis 服务,需要根据需求修改 host 与 port 的设置。

运行 redis_cache_example.php 文件来查看它的工作原理。你也可以在 Redis CLI 中查看输出。

1
2
127.0.0.1:6379> KEYS *
1) "welcome_message"

以上就是使用 Redis 适配器的全部内容。你可以去多试试其它不同的适配器和配置!

总结

本文简单介绍了 PhpFastCache 这个 PHP 中非常热门的库。在文章前半部分,我们讨论了它的基本知识以及安装和配置。在文章后半部分,我们通过几个例子来详细演示了前面提到的概念。

希望你喜欢这篇文章,并将 PhpFastCache 集成到你即将开发的项目中。随时欢迎提问和讨论!

掘金:https://juejin.im/post/5b54d01be51d4517c5649965

无论是教导新手还是资深 Python 程序员,我都发现 很多 Python 程序员没有充分利用多重赋值这一特性

多重赋值(也经常被称为元组解包或者可迭代对象解包)能让你在一行代码内同时对多个变量进行赋值。这种特性在学习时看起来很简单,但在真正需要使用时再去回想它可能会比较麻烦。

在本文中,将介绍什么是多重赋值,举一些常用的多重赋值的样例,并了解一些较少用、常被忽视的多重赋值用法。

请注意在本文中会用到 f-strings 这种 Python 3.6 以上版本才有的特性,如果你的 Python 版本较老,可以使用字符串的 format 方法来代替这种特性。

多重赋值的实现原理

在本文中,我将使用“多重赋值”、“元组解包”、“迭代对象解包”等不同的词,但他们其实表示的是同一个东西。

Python 的多重赋值如下所示:

1
>>> x, y = 10, 20

在这儿我们将 x 设为了 10y 设为了 20

从更底层的角度看,我们其实是创建了一个 10, 20 的元组,然后遍历这个元组,将拿到的两个数字按照顺序分别赋给 xy

写成下面这种语法应该更容易理解:

1
>>> (x, y) = (10, 20)

在 Python 中,元组周围的括号是可以忽略的,因此在“多重赋值”(写成上面这种元组形式的语法)时也可以省去。下面几行代码都是等价的:

1
2
3
4
>>> x, y = 10, 20
>>> x, y = (10, 20)
>>> (x, y) = 10, 20
>>> (x, y) = (10, 20)

多重赋值常被直接称为“元组解包”,因为它在大多数情况下都是用于元组。但其实我们可以用除了元组之外的任何可迭代对象进行多重赋值。下面是使用列表(list)的结果:

1
2
3
4
5
>>> x, y = [10, 20]
>>> x
10
>>> y
20

下面是使用字符串(string)的结果:

1
2
3
4
5
>>> x, y = 'hi'
>>> x
'h'
>>> y
'i'

任何可以用于循环的东西都能和元组解包、多重赋值一样被“解开”。

下面是另一个可以证明多重赋值能用于任何数量、任何变量(甚至是我们自己创建的对象)的例子:

1
2
3
4
5
6
7
>>> point = 10, 20, 30
>>> x, y, z = point
>>> print(x, y, z)
10 20 30
>>> (x, y, z) = (z, y, x)
>>> print(x, y, z)
30 20 10

请注意,在上面例子中的最后一行我们仅交换了变量的名称。多重赋值可以让我们轻松地实现这种情形。

下面我们将讨论如何使用多重赋值。

在 for 循环中解包

你会经常在 for 循环中看到多重赋值。下面举例说明:

先创建一个字典(dict):

1
>>> person_dictionary = {'name': "Trey", 'company': "Truthful Technology LLC"}

下面这种循环遍历字典的方法比较少见:

1
2
for item in person_dictionary.items():
print(f"Key {item[0]} has value {item[1]}")

但你会经常看到 Python 程序员通过多重赋值来这么写:

1
2
for key, value in person_dictionary.items():
print(f"Key {key} has value {value}")

当你在 for 循环中写 for X in Y 时,其实是告诉 Python 在循环的每次遍历时都对 X 做一次赋值。与用 = 符号赋值一样,这儿也可以使用多重赋值。

这种写法:

1
2
for key, value in person_dictionary.items():
print(f"Key {key} has value {value}")

在本质上与这种写法是一致的:

1
2
3
for item in person_dictionary.items():
key, value = item
print(f"Key {key} has value {value}")

与前一个例子相比,我们其实就是去掉了一个没有必要的额外赋值。

因此,多重赋值在用于将字典元素解包为键值对时十分有用。此外,它还在其它地方也可以使用:

在内置函数 enumerate 的值拆分成对时,也是多重赋值的一个很有用的场景:

1
2
for i, line in enumerate(my_file):
print(f"Line {i}: {line}")

还有 zip 函数:

1
2
for color, ratio in zip(colors, ratios):
print(f"It's {ratio*100}% {color}.")
1
2
for (product, price, color) in zip(products, prices, colors):
print(f"{product} is {color} and costs ${price:.2f}")

如果你还对 enumeratezip 不熟悉,请参阅作者之前的文章 looping with indexes in Python

有些 Python 新手经常在 for 循环中看到多重赋值,然后就认为它只能与循环一起使用。但其实,多重赋值不仅可以用在循环赋值时,还可以用在其它任何需要赋值的地方。

替代硬编码索引

很少有人在代码中对索引进行硬编码(比如 point[0]items[1]vals[-1]):

1
print(f"The first item is {items[0]} and the last item is {items[-1]}")

当你在 Python 代码中看到有硬编码索引时,一般都可以设法使用多重赋值来让你的代码更具可读性

下面是一些使用了硬编码索引的代码:

1
2
3
4
def reformat_date(mdy_date_string):
"""Reformat MM/DD/YYYY string into YYYY-MM-DD string."""
date = mdy_date_string.split('/')
return f"{date[2]}-{date[0]}-{date[1]}"

我们可以通过多重赋值,分别对月、天、年三个变量进行赋值,让代码更具可读性:

1
2
3
4
def reformat_date(mdy_date_string):
"""Reformat MM/DD/YYYY string into YYYY-MM-DD string."""
month, day, year = mdy_date_string.split('/')
return f"{year}-{month}-{day}"

因此当你准备对索引进行硬编码时,请停下来想一想是不是应该用多重赋值来改善代码的可读性。

多重赋值是十分严格的

在我们对可迭代对象进行解包时,多重赋值的条件是非常严格的。

如果将一个较大的可迭代对象解包到一组数量更小的对象中,会报下面的错误:

1
2
3
4
>>> x, y = (10, 20, 30)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

如果将一个较小的可迭代对象解包到一组数量更多的对象中,会报下面的错误:

1
2
3
4
>>> x, y, z = (10, 20)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)

这种严格的限制其实很棒,如果我们在处理元素时出现了非预期的对象数量,多重赋值会直接报错,这样我们就能发现一些还没有被发现的 bug。

举个例子。假设我们有一个简单的命令行程序,通过原始的方式接受参数,如下所示:

1
2
3
4
5
import sys

new_file = sys.argv[1]
old_file = sys.argv[2]
print(f"Copying {new_file} to {old_file}")

这个程序希望接受两个参数,如下所示:

1
2
$ my_program.py file1.txt file2.txt
Copying file1.txt to file2.txt

但如果在运行程序时输入了三个参数,也不会有任何报错:

1
2
$ my_program.py file1.txt file2.txt file3.txt
Copying file1.txt to file2.txt

由于我们没有验证接收到的参数是否为 2 个,因此不会报错。

如果使用多重赋值来代替硬编码索引,在赋值时将会验证程序是否真的接收到了期望个数的参数:

1
2
3
4
import sys

_, new_file, old_file = sys.argv
print(f"Copying {new_file} to {old_file}")

注意: 我们用了一个名为 _ 的变量,意思是我们不想关注 sys.argv[0](对应的是我们程序的名称)。用 _ 对象来忽略不需关注的变量是一种常用的语法。

替代数组拆分

根据上文,我们一直多重赋值可以用来代替硬编码的索引,并且它严格的条件可以确保我们处理的元组或可迭代对象的大小是正确的。

此外,多重赋值还能用于代替硬编码的数组拆分。

“拆分”是一种手动将 list 或其它序列中的部分元素取出的方法、

下面是一种用数字索引进行“硬编码”拆分的方法:

1
2
3
all_after_first = items[1:]
all_but_last_two = items[:-2]
items_with_ends_removed = items[1:-1]

当你在拆分时发现没有在拆分索引中用到变量,那么就能用多重赋值来替代它。为了实现多重赋值拆分数组,我们将用到一个之前没提过的特性:* 符号。

* 符号于 Python 3 中加入了多重赋值的语法中,它可以让我们在解包时拿到“剩余”的元素:

1
2
3
4
5
6
>>> numbers = [1, 2, 3, 4, 5, 6]
>>> first, *rest = numbers
>>> rest
[2, 3, 4, 5, 6]
>>> first
1

因此,* 可以让我们在取数组末尾时替换硬编码拆分。

下面两行是等价的:

1
2
>>> beginning, last = numbers[:-1], numbers[-1]
>>> *beginning, last = numbers

下面两行也是等价的:

1
2
>>> head, middle, tail = numbers[0], numbers[1:-1], numbers[-1]
>>> head, *middle, tail = numbers

有了 * 和多重赋值之后,你可以替换一切类似于下面这样的代码:

1
main(sys.argv[0], sys.argv[1:])

可以写成下面这种更具自描述性的代码:

1
2
program_name, *arguments = sys.argv
main(program_name, arguments)

总之,如果你写了硬编码的拆分代码,请考虑一下你可以用多重赋值来让这些拆分的逻辑更加清晰。

深度解包

这个特性是 Python 程序员长期以来经常忽略的一个东西。它虽然不如我之前提到的几种多重复值用法常用,但是当你用到它的时候会深刻体会到它的好处。

在前文,我们已经看到多重赋值用于解包元组或者其它的可迭代对象,但还没看过它更进一步地进行深度解包。

下面例子中的多重赋值是浅度的,因为它只进行了一层的解包:

1
2
3
4
5
>>> color, point = ("red", (1, 2, 3))
>>> color
'red'
>>> point
(1, 2, 3)

而下面这种多重赋值可以认为是深度的,因为它将 point 元组也进一步解包成了 xyz 变量:

1
2
3
4
5
6
7
>>> color, (x, y, z) = ("red", (1, 2, 3))
>>> color
'red'
>>> x
1
>>> y
2

上面的例子可能比较让人迷惑,所以我们在赋值语句两端加上括号来让这个例子更加明了:

1
>>> (color, (x, y, z)) = ("red", (1, 2, 3))

可以看到在第一层解包时得到了两个对象,但是这个语句将第二个对象再次解包,得到了另外的三个对象。然后将第一个对象及新解出的三个对象赋值给了新的对象(colorxyz)。

下面以这两个 list 为例:

1
2
start_points = [(1, 2), (3, 4), (5, 6)]
end_points = [(-1, -2), (-3, 4), (-6, -5)]

下面的代码是举例用浅层解包来处理上面的两个 list:

1
2
3
for start, end in zip(start_points, end_points):
if start[0] == -end[0] and start[1] == -end[1]:
print(f"Point {start[0]},{start[1]} was negated.")

下面用深度解包来做同样的事情:

1
2
3
for (x1, y1), (x2, y2) in zip(start_points, end_points):
if x1 == -x2 and y1 == -y2:
print(f"Point {x1},{y1} was negated.")

请注意在第二个例子中,在处理对象时,对象的类型明显更加清晰易懂。深度解包让我们可以明显的看到,在每次循环中我们都会收到两个二元组。

深度解包通常会在每次得到多个元素的嵌套循环中使用。例如,你能在同时使用 enumeratezip 时应用深度多重赋值:

1
2
3
4
items = [1, 2, 3, 4, 2, 1]
for i, (first, last) in enumerate(zip(items, reversed(items))):
if first != last:
raise ValueError(f"Item {i} doesn't match: {first} != {last}")

前面我提到过多重赋值对于可迭代对象的大小以及解包的大小是非常严格的,在复读解包中我们也可以利用这点严格控制可迭代对象的大小

这么写可以正常运行:

1
2
3
>>> points = ((1, 2), (-1, -2))
>>> points[0][0] == -points[1][0] and points[0][1] == -point[1][1]
True

这种看起来 bug 的代码也能正常运行:

1
2
3
>>> points = ((1, 2, 4), (-1, -2, 3), (6, 4, 5))
>>> points[0][0] == -points[1][0] and points[0][1] == -point[1][1]
True

这种写法也能运行:

1
2
3
4
>>> points = ((1, 2), (-1, -2))
>>> (x1, y1), (x2, y2) = points
>>> x1 == -x2 and y1 == -y2
True

但是这样不行:

1
2
3
4
5
>>> points = ((1, 2, 4), (-1, -2, 3), (6, 4, 5))
>>> (x1, y1), (x2, y2) = points
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

在给变量多重赋值时我们其实也对可迭代对象做了一次特殊的断言(assert)。因此多重赋值既能让别人更容易理清你的代码(因为有着更好的代码可读性),也能让电脑更好地理解你的代码(因为对代码进行了确认保证了正确性)。

使用 list 类型语法

在前文我提到的多重赋值都用的是元组类型的语法(tuple-like),但其实多重赋值可以用于任何可迭代对象。而这种类似元组的语法也使得多重赋值常被称为“元组解包”。而更准确地来说,多重赋值应该叫做“可迭代对象解包”。

前文中我还没有提到过,多重赋值可以写成 list 类型的语法(list-like)。

下面是一个应用 list 语法的最简单多重赋值示例:

1
2
3
>>> [x, y, z] = 1, 2, 3
>>> x
1

这种写法看起来很奇怪。为什么在元组语法之外还要允许这种 list 语法呢?

我也很少使用这种特性,但它在一些特殊情况下能让代码更加简洁

举例,假设我有下面这种代码:

1
2
def most_common(items):
return Counter(items).most_common(1)[0][0]

我们用心良苦的同事决定用深度多重赋值将代码重构成下面这样:

1
2
3
def most_common(items):
(value, times_seen), = Counter(items).most_common(1)
return value

看到赋值语句左侧的最后一个逗号了吗?很容易会将它漏掉,而且这个逗号让代码看起来不伦不类。这个逗号在这段代码中是做什么事的呢?

此处的尾部逗号其实是构造了一个单元素的元组,然后对此处进行深度解包。

可以将上面的代码换种写法:

1
2
3
def most_common(items):
((value, times_seen),) = Counter(items).most_common(1)
return value

这种写法让深度解包的语法更加明显了。但我更喜欢下面这种写法:

1
2
3
def most_common(items):
[(value, times_seen)] = Counter(items).most_common(1)
return value

赋值中的 list 语法让它更加的清晰,可以明确看出我们将一个单元素可迭代对象进行了解包,并将单元素又解包并赋值给 valuetimes_seen 对象。

当我看到这种代码时,可以非常确定我们解包的是一个单元组 list(事实上代码做的也正是这个)。我们在此处用了 collections 模组中的 Counter 对象。Counter 对象的 most_common 方法可以让我们指定返回 list 的长度。在此处我们将 list 限制为仅返回一个元素。

当你在解包有很多的值的结构(比如说 list)或者有确定个数值的结构(比如说元组)时,可以考虑用 list 语法来对这些类似 list 的结构进行解包,这样能让代码更加具有“语法正确性”。

如果你乐意,还可以用对类 list 结构使用 list 语法解包时应用一些 list 的语法(常见的例子为在多重赋值时使用 * 符号):

1
>>> [first, *rest] = numbers

我自己其实不常用这种写法,因为我没有这个习惯。但如果你觉得这种写法有用,可以考虑在你自己的代码中用上它。

结论:当你在代码中用多重赋值时,可以考虑在何时的时候用 list 语法来让你的代码更具自解释性并更加简洁。这有时也能提升代码的可读性。

不要忘记这些多重赋值的用法

多重赋值可以提高代码的可读性与正确性。它能使你代码更具自描述性,同时也可以对正在进行解包的可迭代对象的大小进行隐式断言。

据我观察,人们经常忘记多重赋值可以替换硬编码索引,以及替换硬编码拆分(用 * 语法)。深度多重赋值,以及同时使用元组语法和 list 语法也常被忽视。

认清并记住所有多重赋值的用例是很麻烦的。请随意使用本文作为你使用多重赋值的参考指南。

juejin: https://juejin.im/post/5b3c2cf1e51d451925625b94

在本文中,我们将了解 ECMAScript 6 中引入的生成器(Generator)。先看一看它究竟是什么,然后用几个示例来说明它的用法。

什么是 JavaScript 生成器?

生成器是一种可以用来控制迭代器(iterator)的函数,它可以随时暂停,并可以在任意时候恢复。

上面的描述没法说明什么,让我们来看一些例子,解释什么是生成器,以及生成器与 for 循环之类的迭代器有什么区别。

下面是一个 for 循环的例子,它会在执行后立刻返回一些值。这段代码其实就是简单地生成了 0-5 这些数字。

1
2
3
4
for (let i = 0; i < 5; i += 1) {
console.log(i);
}
// 它将会立刻返回 0 -> 1 -> 2 -> 3 -> 4

现在看看生成器函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function * generatorForLoop(num) {
for (let i = 0; i < num; i += 1) {
yield console.log(i);
}
}

const genForLoop = generatorForLoop(5);

genForLoop.next(); // 首先 console.log - 0
genForLoop.next(); // 1
genForLoop.next(); // 2
genForLoop.next(); // 3
genForLoop.next(); // 4

它做了什么?它实际上只是对上面例子中的 for 循环做了一点改动,但产生了很大的变化。这种变化是由生成器最重要的特性造成的 —— 只有在需要的时候它才会产生下一个值,而不会一次性产生所有的值。在某些情景下,这种特性十分方便。

生成器语法

如何定义一个生成器函数呢?下面列出了各种可行的定义方法,不过万变不离其宗的是在函数关键词后加上一个星号。

1
2
3
4
5
6
7
8
9
10
11
function * generator () {}
function* generator () {}
function *generator () {}

let generator = function * () {}
let generator = function* () {}
let generator = function *() {}

let generator = *() => {} // SyntaxError
let generator = ()* => {} // SyntaxError
let generator = (*) => {} // SyntaxError

如上面的例子所示,我们并不能使用箭头函数来创建一个生成器。

下面将生成器作为方法(method)来创建。定义方法与定义函数的方式是一样的。

1
2
3
4
5
6
7
8
9
class MyClass {
*generator() {}
* generator() {}
}

const obj = {
*generator() {}
* generator() {}
}

yield

现在让我们一起看看新的关键词 yield。它有些类似 return,但又不完全相同。return 会在完成函数调用后简单地将值返回,在 return 语句之后你无法进行任何操作。

1
2
3
4
5
6
7
8
9
function withReturn(a) {
let b = 5;
return a + b;
b = 6; // 不可能重新定义 b 了
return a * b; // 这儿新的值没可能返回了
}

withReturn(6); // 11
withReturn(6); // 11

yield 的工作方式却不同。

1
2
3
4
5
6
7
8
9
10
11
12

function * withYield(a) {
let b = 5;
yield a + b;
b = 6; // 在第一次调用后仍可以重新定义变量
yield a * b;
}

const calcSix = withYield(6);

calcSix.next().value; // 11
calcSix.next().value; // 36

yield 返回的值只会返回一次,当你再次调用同一个函数的时候,它会执行至下一个 yield 语句处(译者注:前面的 yield 不再返回东西了)。

在生成器中,我们通常会在输出时得到一个对象。这个对象有两个属性:valuedone。如你所想,value 为返回值,done 则会显示生成器是否完成了它的工作。

1
2
3
4
5
6
7
8
9
function * generator() {
yield 5;
}

const gen = generator();

gen.next(); // {value: 5, done: false}
gen.next(); // {value: undefined, done: true}
gen.next(); // {value: undefined, done: true} - 之后的任何调用都会返回相同的结果

在生成器中,不仅可以使用 yield,也可以使用 return 来返回同样的对象。但是,在函数执行到第一个 return 语句的时候,生成器将结束它的工作。

1
2
3
4
5
6
7
8
9
10
11
function * generator() {
yield 1;
return 2;
yield 3; // 到不了这个 yield
}

const gen = generator();

gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: true}
gen.next(); // {value: undefined, done: true}

yield 委托迭代

带星号的 yield 可以将它的工作委托给另一个生成器。通过这种方式,你就能将多个生成器连接在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function * anotherGenerator(i) {
yield i + 1;
yield i + 2;
yield i + 3;
}

function * generator(i) {
yield* anotherGenerator(i);
}

var gen = generator(1);

gen.next().value; // 2
gen.next().value; // 3
gen.next().value; // 4

在开始下一节前,我们先观察一个第一眼看上去比较奇特的行为。

下面是正常的代码,不会报出任何错误,这表明 yield 可以在 next() 方法调用后返回传递的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function * generator(arr) {
for (const i in arr) {
yield i;
yield yield;
yield(yield);
}
}

const gen = generator([0,1]);

gen.next(); // {value: "0", done: false}
gen.next('A'); // {value: undefined, done: false}
gen.next('A'); // {value: "A", done: false}
gen.next('A'); // {value: undefined, done: false}
gen.next('A'); // {value: "A", done: false}
gen.next(); // {value: "1", done: false}
gen.next('B'); // {value: undefined, done: false}
gen.next('B'); // {value: "B", done: false}
gen.next('B'); // {value: undefined, done: false}
gen.next('B'); // {value: "B", done: false}
gen.next(); // {value: undefined, done: true}

在这个例子中,你可以看到 yield 默认是 undefined,但如果我们在调用 yield 时传递了任何值,它就会返回我们传入的值。我们将很快利用这个特性。

初始化与方法

生成器是可以被复用的,但是你需要对它们进行初始化。还好初始化的方法十分简单。

1
2
3
4
5
6
7
8
9
function * generator(arg = 'Nothing') {
yield arg;
}

const gen0 = generator(); // OK
const gen1 = generator('Hello'); // OK
const gen2 = new generator(); // 不 OK

generator().next(); // 可以运行,但每次都会从头开始运行

如上所示,gen0gen1 不会互相影响,gen2 完全不会运行(会报错)。因此初始化对于保证程序流程的状态是十分重要的。

下面让我们一起看看生成器给我们提供的方法。

next() 方法

1
2
3
4
5
6
7
8
9
10
11
12
function * generator() {
yield 1;
yield 2;
yield 3;
}

const gen = generator();

gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: false}
gen.next(); // {value: undefined, done: true} 之后所有的 next 调用都会返回同样的输出

这是最常用的方法。它每次被调用时都会返回下一个对象。在生成器工作结束时,next() 会将 done 属性设为 truevalue 属性设为 undefined

我们不仅可以用 next() 来迭代生成器,还可以用 for of 循环来一次得到生成器所有的值(而不是对象)。

1
2
3
4
5
6
7
8
9
10
11
12
function * generator(arr) {
for (const el in arr)
yield el;
}

const gen = generator([0, 1, 2]);

for (const g of gen) {
console.log(g); // 0 -> 1 -> 2
}

gen.next(); // {value: undefined, done: true}

但它不适用于 for in 循环,并且不能直接用数字下标来访问属性:generator[0] = undefined

return() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
function * generator() {
yield 1;
yield 2;
yield 3;
}

const gen = generator();

gen.return(); // {value: undefined, done: true}
gen.return('Heeyyaa'); // {value: "Heeyyaa", done: true}

gen.next(); // {value: undefined, done: true} - 在 return() 之后的所有 next() 调用都会返回相同的输出

return() 将会忽略生成器中的任何代码。它会根据传值设定 value,并将 done 设为 true。任何在 return() 之后进行的 next() 调用都会返回 done 属性为 true 的对象。

throw() 方法

1
2
3
4
5
6
7
8
9
10
function * generator() {
yield 1;
yield 2;
yield 3;
}

const gen = generator();

gen.throw('Something bad'); // 会报错 Error Uncaught Something bad
gen.next(); // {value: undefined, done: true}

throw() 做的事非常简单 —— 就是抛出错误。我们可以用 try-catch 来处理。

自定义方法的实现

由于我们无法直接访问 Generator 的 constructor,因此如何增加新的方法需要另外说明。下面是我的方法,你也可以用不同的方式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function * generator() {
yield 1;
}

generator.prototype.__proto__; // Generator {constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}

// 由于 Generator 不是一个全局变量,因此我们只能这么写:
generator.prototype.__proto__.math = function(e = 0) {
return e * Math.PI;
}

generator.prototype.__proto__; // Generator {math: ƒ, constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, …}

const gen = generator();
gen.math(1); // 3.141592653589793

生成器的用途

在前面,我们用了已知迭代次数的生成器。但如果我们不知道要迭代多少次会怎么样呢?为了解决这个问题,需要在生成器函数中创建一个无限循环。下面以一个会返回随机数的函数为例进行演示:

1
2
3
4
5
6
7
8
function * randomFrom(...arr) {
while (true)
yield arr[Math.floor(Math.random() * arr.length)];
}

const getRandom = randomFrom(1, 2, 5, 9, 4);

getRandom.next().value; // 返回随机的一个数

这是个简单的例子。下面来举一些更复杂的函数为例,我们要写一个节流(throttle)函数。如果你还不知道节流函数是什么,请参阅这篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function * throttle(func, time) {
let timerID = null;
function throttled(arg) {
clearTimeout(timerID);
timerID = setTimeout(func.bind(window, arg), time);
}
while (true)
throttled(yield);
}

const thr = throttle(console.log, 1000);

thr.next(); // {value: undefined, done: false}
thr.next('hello'); // 返回 {value: undefined, done: false} ,然后 1 秒后输出 'hello'

还有没有更好的利用生成器的例子呢?如果你了解递归,那你肯定听过斐波那契数列。通常我们是用递归来解决这个问题的,但有了生成器后,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function * fibonacci(seed1, seed2) {
while (true) {
yield (() => {
seed2 = seed2 + seed1;
seed1 = seed2 - seed1;
return seed2;
})();
}
}

const fib = fibonacci(0, 1);
fib.next(); // {value: 1, done: false}
fib.next(); // {value: 2, done: false}
fib.next(); // {value: 3, done: false}
fib.next(); // {value: 5, done: false}
fib.next(); // {value: 8, done: false}

不再需要递归了!我们可以在需要的时候获得数列中的下一个数字。

将生成器用在 HTML 上

既然是讨论 JavaScript,那显然要用生成器来操作下 HTML。

假设有一些 HTML 块需要处理,可以使用生成器来轻松实现。(当然除了生成器之外还有很多方法可以做到)

我们只需要少许代码就能完成此需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const strings = document.querySelectorAll('.string');
const btn = document.querySelector('#btn');
const className = 'darker';

function * addClassToEach(elements, className) {
for (const el of Array.from(elements))
yield el.classList.add(className);
}

const addClassToStrings = addClassToEach(strings, className);

btn.addEventListener('click', (el) => {
if (addClassToStrings.next().done)
el.target.classList.add(className);
});

仅有 5 行逻辑代码。

总结

还有更多使用生成器的方法。例如,在进行异步操作或者按需循环时生成器也非常有用。

我希望这篇文章能帮你更好地理解 JavaScript 生成器。

掘金地址:https://juejin.im/post/5b14faf2f265da6e4d5af3b9

<!DOCTYPE html> HMLT5 于 2014 年 10 月由万维网联盟(W3C)发布,旨在通过改进 HTML 语言来支持最新的多媒体设备,在保证计算机与设备(如 Web 浏览器,解析器等)可解析的前提下增强对人类的可读性。

upload successful

我可以确定你们都已经在使用 HTML5 做网页了,并且会使用一些常见的标签,如 <header><section><article><footer> 等等,除此之外,还有一些不常用的标签是有助于正确使用 HTML5 的语义化开发。

在此我将其中一些最重要的标签列出来,希望能帮助你遵循 HTML5 语义进行开发。

Read more »

使用 service workers API 可以让你直接由 Node.js 应用向 Chrome 浏览器发送推送通知。web-push npm 模组可以让你免去 PubNub 之类的中间商,直接推送消息。本文将在前端使用原生 JavaScript,在后端使用 Express 框架,通过一个“Hello, World”级别的样例来带你了解如何进行 web 推送通知。最终的效果如下图所示。本项目的全部源码可在 GitHub 查阅。

upload successful

鉴权及配置服务端

要设置 web 推送,必须先创建 VAPID keys。VAPID keys 用于识别是谁发送了推送消息。npm 的 web-push 模组能够帮助你生成 VAPID keys,下面我们将安装 web-push 及其依赖,并使用 web-push generate-vapid-keys 来创建 VAPID keys。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ npm install express@4.16.3 web-push@3.3.0 body-parser@1.18.2 express-static@1.2.5
+ express@4.16.3
+ web-push@3.3.0
+ body-parser@1.18.2
+ express-static@1.2.5
added 62 packages in 1.42s
$
$ ./node_modules/.bin/web-push generate-vapid-keys
=======================================
Public Key:
BOynOrGhgkj8Bfk4hsFENAQYbnqqLSigUUkCNaBsAmNuH6U9EWywR1JIdxBVQOPDbIuTaj0tVAQbczNLkC5zftw
Private Key:
<OMITTED>
=======================================
$

如果你需要支持低版本浏览器,那么还要获取 GCM API key,但在桌面版 Chrome 63 或更高版本中不需要它。

下面创建 index.js 文件,其中包含你的服务。你需要使用 require() 导入 web-push 模组,并配置刚才的 VAPID keys。配置相当简单,将 VAPID keys 存放在 PUBLIC_VAPID_KEYPRIVATE_VAPID_KEY 环境变量中即可。

1
2
3
4
5
const webpush = require('web-push');
const publicVapidKey = process.env.PUBLIC_VAPID_KEY;
const privateVapidKey = process.env.PRIVATE_VAPID_KEY;
// 此处换成你自己的邮箱
webpush.setVapidDetails('mailto:val@karpov.io', publicVapidKey, privateVapidKey);

下一步,为 Express 应用添加一个名为 /subscribe 的端点。浏览器中的 JavaScript 将会发送一个 body 中包含 PushSubscription 对象的 HTTP 请求。为了用 webpush.sendNotification() 发送推送通知,你需要获取 PushSubscription 对象。

1
2
3
4
5
6
7
8
9
10
11
const app = express();
app.use(require('body-parser').json());
app.post('/subscribe', (req, res) => {
const subscription = req.body;
res.status(201).json({});
const payload = JSON.stringify({ title: 'test' });
console.log(subscription);
webpush.sendNotification(subscription, payload).catch(error => {
console.error(error.stack);
});
});

以上就是服务端需要做的全部配置。你可以在 GitHub 查阅完整代码。现在,我们就要创建客户端 client.js 与一个 service worker —— worker.js 了。

客户端与 Service Worker

首先,使用 express-static npm 模组对 Express 应用进行配置,为客户端部署静态资源,将静态资源部署在服务的最顶级目录下。 需要注意的是要在处理 /subscribe 路由之后再调用这个 app.use(),否则 Express 将不会根据你的配置处理路由,而是会去查找 subscribe.html 文件。

1
app.use(require('express-static')('./'));

接着,创建 index.html 文件,这个文件将部署为你的应用的入口。文件中仅有的关键之处就是 <script> 标签,它将加载客户端 JavaScript 代码;其余部分都无关紧要。

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Push Demo</title>
<script type="application/javascript" src="/client.js"></script>
</head>

<body>
Service Worker Demo
</body>
</html>

现在你的入口做好了。创建一个名为 client.js 的文件。这个文件 将告知浏览器初始化你的 service worker 并向 /subscribe 发送 HTTP 请求。由于支持 service workers 的浏览器也应该能支持 async 与 await,因此上述示例中使用了 async/await

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
// 这里写死的 public key 要换成你自己的。
const publicVapidKey = 'BOynOrGhgkj8Bfk4hsFENAQYbnqqLSigUUkCNaBsAmNuH6U9EWywR1JIdxBVQOPDbIuTaj0tVAQbczNLkC5zftw';
if ('serviceWorker' in navigator) {
console.log('Registering service worker');
run().catch(error => console.error(error));
}
async function run() {
console.log('Registering service worker');
const registration = await navigator.serviceWorker.
register('/worker.js', {scope: '/'});
console.log('Registered service worker');
console.log('Registering push');
const subscription = await registration.pushManager.
subscribe({
userVisibleOnly: true,
// `urlBase64ToUint8Array()` 函数与以下网址中的描述一致
// https://www.npmjs.com/package/web-push#using-vapid-key-for-applicationserverkey
applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
});
console.log('Registered push');
console.log('Sending push');
await fetch('/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'content-type': 'application/json'
}
});
console.log('Sent push');
}

最后,你需要实现 client.js 所加载的 worker.js 文件。 这个文件是 service worker 逻辑所在之处。当订阅者接受到一个推送消息时,service worker 将收到一个 'push' 事件

1
2
3
4
5
6
7
8
9
console.log('Loaded service worker!');
self.addEventListener('push', ev => {
const data = ev.data.json();
console.log('Got push', data);
self.registration.showNotification(data.title, {
body: 'Hello, World!',
icon: 'http://mongoosejs.com/docs/images/mongoose5_62x30_transparent.png'
});
});

好了!配置正确的环境变量并启动你的服务:

1
$ env PUBLIC_VAPID_KEY='OMITTED' env PRIVATE_VAPID_KEY='OMITTED' node .

在 Chrome 中访问 http://localhost:3000,你应该可以看到下面的推送通知!

upload successful

这种通知不仅在 Chrome 中可用,在 Firefox 也可以用同样的代码实现。

upload successful

最后

Web 推送只是 service workers 带来的诸多好处的其中一种。 通过一个 npm 模组,你就能给大多数现代浏览器推送通知。下次你要为你的 web 应用增加推送通知功能的时候,记得用 service workers 哦!

摘要

在许多自然语言处理应用中,从长文本序列中学习其潜在的语义表示是重要的第一步。RNN 已经成为了这个任务的基石。然而,随着文本长度的增加,RNN 的解码重建(decoding、reconstraction)的质量也随之降低。因此,我们提出了一个 seq2seq、由纯粹的卷积与反卷积结构构成的自动编码框架,它没有上述 RNN 的问题,同时计算效率也很高。另外,此方法简单易行,可以在许多应用中作为一块积木使用。我们可以证明,此框架在重构长文本任务中的效果比 RNN 更好。同时,在半监督文本分类及文本摘要任务中的评价指标表明此框架能更好地利用无标注长文本数据。

1 简介

学习句子或多个句子组成的段落是自然语言处理的一项重要任务,它常常是实现其它任务(如情感分析、机器翻译、对话系统、文本摘要等)的基础。从数据中学习句子的表示使用的是编码器 - 解码器结构。在标准的自动编码步骤中,首先由输入序列的 embedding 编码为向量表示,再解码到原始域重建输入序列。近期在 RNN 中,LSTM 及其变体在许多依赖句子表示的学习任务中取得了巨大的成功。

基于 RNN 的方法通常将句子递归建模为含有隐藏单元的马尔可夫过程,其中每个输入句子的单词都是由前一个单词及隐藏单元的状态生成的,通过 emission 与 transition 运算建模为神经网络。原则上,输入序列的神经元表示应当包含了足够的输入序列结构的信息,才能在之后通过解码恢复原句。然而由于 RNN 的递归性质,基于 RNN 将句子完全表示为向量存在着挑战。典型的问题是,RNN 在训练时是基于之前的置信单词的状态来生成序列中的单词,而不是由编码后的向量表示解码整个句子。现在已经证明这种强制性的策略(teacher forcing training)是有必要的,因为它可以让 RNN 的输出接近于置信序列。然而,由于在重建序列时 RNN 允许解码器使用置信信息,削弱了编码器独立生成向量表示的能力,导致编码后的表示没能携带足够的信息来引导解码器在没有其它指引的情况下进行解码。为了解决这个问题,[19] 提出了一种在训练过程中的采样方法,从同时使用潜在表示与置信信号进行学习逐渐转换为仅用编码的潜在表示进行学习。但 [20] 表明,这种计划采样本质上是一种不一致训练策略,在实际使用中会产生不稳定的结果,最终导致训练无法收敛。

在推理时,在遇到无法使用置信信号的句子时,仅能通过前一个单词及表示向量的状态来生成新的单词。此时,解码的错误随着序列长度的增加而成比例增加。这也意味着,在处理句子时,一旦出现一个错误,后续生成的句子将错的更离谱。这种现象是 [19] 中的 exposure bias 产生的。

我们提出了一种简单而强大的纯卷积结构,用于学习句子的表示。比较方便的是,由于这个结构中不含 RNN,因此 teacher forcing training 与 exposure bias 的问题自然就不存在了。这种方法使用 CNN 作为编码器,deCNN 作为解码器。我们认为,这种结构通过多层 CNN 迫使潜在表示从整个句子中提取信息,由此在不适用 RNN 解码器的情况下实现较高的重建质量。这种多层 CNN 能够让表示向量从整个句子中抽取信息而无需考虑句子长度,这也使它可以应用于长句子或段落相关任务。此外,由于这种结构不涉及递归编码及解码,因此可以使用图形处理单元(GPU)的特定卷积原语进行高效并行化,与 RNN 模型相比显著减少了计算成本。

2 自动编码卷积文本模型

2.1 卷积编码器

\(w^t\) 表示给定句子中的第 t 个词,将每个词 \(w^t\) 进行 embedding,映射为 k 维的词向量 \(x_t = W_e[w^t]\),其中 $ W_e ^{k V} $ 为一个已经学习好的 Word embedding 矩阵,V 为单词数量;用 $ W_e[v]$ 来表示 \(W_e\) 的第 v 列。\(W_e\) 中的所有列都经过 l2-norm 处理,例如 $ ||W_e[v]||_2 = 1, v $。在经过 embedding 之后,一个长度为 T 的句子(经过 padding)可以在将 embedding 进行 concat 后表示为 \(X \in \mathbb{R}^{k \times T}\);其中 \(x_t\) 为 X 的第 t 列。

对于句子做编码,我们采用了类似 [24] 中的 CNN 结构,这个结构最初是为做图像处理任务设计的。这个 CNN 结构包含 L 个层(L - 1 个卷积层,第 L 层为全连接层),此结构最后可以将一个输入句子转化为一个定长的表示向量 h。层 \(l \in \{1, ... ,L\}\) 由学习到的滤波器 \(p_l\) 组成。对于第一层中的第 i 个滤波器来说,相当于对 X 进行一个步长(stride)为 \(r^{(1)}\)、滤波器为 \(W_c^{(i,1)} \in \mathbb{R}^{k \times h}\) 的卷积运算(式中的 h 代表卷积滤波器的大小)。这一系列操作会生成一个潜在特征映射:\(c^{(i,1)}=\gamma(X * W_c^{(i,1)} + b^{(i,1)}) \in \mathbb{R}^{(T-h)/r^{(1)}+1}\),其中 \(\gamma(.)\) 为非线性激活函数,\(b^{(i,1)} \in \mathbb{R}^{(T-h)/r^{(1)}+1}\) ,* 符号代表了卷积操作。在我们的实验中,\(\gamma(.)\) 为 ReLU。需要注意的是,最初的 embedding 维数 k 在经过第一层卷积层后就发生了变化,\(c^{(i,1)} \in \mathbb{R}^{(T-h)/r^{(1)}+1}\) for i = 1,...,p1。在第一层将 p1 滤波器得到的结果进行拼接,就得到了一个特征映射,\(C^{(1)}= [c^{(1,1)}... c^{(p1,1)}] \in \mathbb{R}^{p_1 \times [(T-h)/r^{(1)}+1]}\)

在第一个卷积层之后,我们对得到的特征映射 \(C^{(1)}\) 使用同样的滤波器大小 h 进行卷积操作,并在这 L - 1 层中不断重复此操作。每次操作都能将空间维数降低为 \(T^{(l+1)}=[(T^{(l)} - h)/r^{(l)} + 1]\)\(r^{(l)}\) 为步长,\(T^{(l)}\) 为空间维数大小,l 为第 l 层,[] 为向下取整函数)。在最后一层 L 中,得到了特征映射 \(C^{(L-1)}\),将其送入全连接层中制造潜在表示向量 h。在实现时,我们直接用了一个滤波器大小等于 \(T^{(L-1)}\)(不考虑 h)的卷积层,它就相当于一个全连接层。这个实现上的 trick 在 [24] 中有所使用。这个最后一层将所有的空间坐标 \(T^{(L-1)}\) 汇聚成为标量特征,使用滤波器 \(\{W_c^{(i,l)}\}\) for i=1,...,p1 and l=1,...,L 将句子的子结构依次封装为了向量表示。其中 \(W_c^{(i,l)}\) 表示层 l 的滤波器 i。这也意味着提取出来的特征的维度是固定的,与输入句子的长度无关。

upload successful

图1,卷积自动编码结构。编码器:将输入序列展开为 embedding 矩阵 X,接着通过多个卷积层编码器压缩为表示向量 h,在最后一层中将向量折叠去除空间维度。解码器:将表示向量 h 送入多个反卷积解码器,以 X 为目标,使用余弦相似度交叉熵损失函数重建 \(\hat{X}\)

在最后一层有 \(p_L\) 个滤波器,将会构造出 \(p_L\) 维的表示向量。对于输入句子来说,也记为 \(h=C^{(L)}\)。例如,在图一中,编码器由 L=3 个层构成,句子长度为 T=60,embedding 维数为 k=300,不同层的步长 \(\{ r^{(1)},r^{(2)},r^{(3)} \} = \{2,2,1\}\) ,滤波器大小 \(h = \{5,5,12\}\),滤波器数量 \(\{p_1,p_2,p_3 \}=\{300,600,500\}\)。中间层得到的特征映射 \(C^{(1)}\)\(C^{(2)}\) 的大小分别为 \(\{28 \times 300,12 \times 600\}\)。最后的特征映射大小为 1 x 500,对应的正是潜在表示向量 h。

从概念上看,较低层中的滤波器捕捉的是原始的句子信息(h-gram,类似于图像处理中的边缘信息),而叫高层中的滤波器捕捉的是更复杂的语言特征,比如语义和句法结构等(类似于图像处理中的图像元素)。这种自底向上的结构通过分层堆叠文本片段(h-gram)作为构建表示向量 h 的积木。这种方法在思路上类似于通过 concrete syntax trees [26] 对语言的语法结构进行建模。不过,我们没有事先去指定某种语法结构(比如英语),而是通过多层 CNN 网络提取此结构。

2.2 反卷积解码器

我们按照一定步长应用卷积的变体 - 反卷积操作(比如 convolutional transpose),用于解码表示向量 h,将其还原至原来的文本域。随着反卷积的不断进行,向量的空间高度也不断增加,如图1所示,和之前描述的卷积操作刚好相反。空间维数首先展开至与卷积层的 L-1 层相同,接着逐渐展开为 \(T^{(l+1)} = (T^{(l)}-1)*r^{(l)} + h\) for l=1,...直到第 L 个反卷积层(此层与卷积编码器的输入层相对应)。第 L 反卷积层的输出目标是重建 Word embedding 矩阵 \(\hat{X}\)。与 \(W_e\) 一样,\(\hat{X}\) 的每一列都经由 l2-norm 处理。

\(\hat{w}^t\) 来表示重建后句子 \(\hat{s}\) 中的第 t 个单词,\(\hat{w}^t\) 为 v 的概率可表示为:

\[p(\hat{w}^t = v) = \frac{\exp[\tau^{-1}D_{cos}(\hat{x}^t,W_e[v])]}{\sum_{v'\in V}\exp[\tau^{-1}D_{cos}(\hat{x}^t,W_e[v'])]}\]

式中,\(D_{cos}(x,y)\) 代表余弦相似度,计算方法为 \(\frac{<{x,y}>}{||x||||y||}\)\(W_e[v]\)\(W_e\) 的第 v 列,\(\hat{x}^t\)\(\hat{X}\) 的第 t 列,\(\tau\) 是一个正数,我们将其定义为 temperature parameter [27]。此参数类似于 Dirichlet 分布的浓度参数,控制着概率向量 \([p(\hat{w}^t = 1)...p(\hat{w}^t = V)]\) 的扩散,较大的 \(\tau\) 值会鼓励概率均匀地分布,较小的 \(\tau\) 值会鼓励概率稀疏并集中。在实验中,我们设定 \(\tau = 0.01\)。此外在我们的实验中,余弦相似度可直接由经由 l2-norm 的 \(W_e\)\(\hat{X}\) 内积得到。

2.3 模型学习

上述卷积自动编码器的目标可以记为所有句子(\(s \in D\))的词级别的对数似然:

\[\iota^{\alpha e} = \sum_{d \in D} \sum_t \log p(\hat{w}^t_d = w^t_d)\]

上述式子中,D 表示句子的集合。为了简单起见,使用随机梯度下降对式中的最大对数似然进行优化。实现相关的细节将在实验一节中详细描述。请注意,上述工作与之前的相关工作有两处不同:i) [22,28] 中使用了池化与上池化(pooling,unpooling)操作,而我们用了卷积与反卷积;ii) 更重要的是,[22,28] 没有像我们前面的步骤一样使用余弦相似度来重建句子,而是使用了基于 RNN 的解码器。我们将在第 3 节中更详细地讨论相关工作。在早期的实验(论文中未写出)中,我们使用了池化与上池化,但是没有观察到显著的性能提升;而卷积/反卷积运算则在内存占用方面更具效率。与标准的 LSTM RNN 自动编码器进行对比,两者参数数量大致相同,但我们的结构在单片 NVIDIA TITAN X GPU 上计算速度相当快(详见实验一节)。原因是 CNN 通过 cuDNN 原语处理有着很高的并行效率。

反卷积解码器与 RNN 解码器的对比: 这种结构可以视为 NLP 模型的一种补充结构。与标准的基于 LSTM 的解码器相反,反卷积与 RNN 的区别在于它有着不严格的序列依赖性。具体来说,RNN 生成一个单词需要一个隐藏单元的向量,以递归的方式在整句中顺序地积累信息(长效信息主要依赖于向下权重);而反卷积解码器,解码时的生成仅依赖一个封装了整个句子信息的表示向量,没有指定顺序结构。因此,对于语言生成任务,RNN 解码器会比反卷积解码器生成相关性更好的文本;与之相反,反卷积解码器在计算长句、远距离依赖的情况效果更好,因此在分类特征提取与文本摘要任务中更加有用。

2.4 半监督分类与摘要

识别相关主题或情绪以及从用户生成的内容(如博客、产品评论等)生成摘要最近获得了大量的关注[1, 3, 4, 30, 31, 13, 11]。在大多数实际情况下,未标注数据非常丰富,但实际上很少能充分发挥这些未标注数据的潜力。以此为契机,我们希望能补充一些稀缺但更有价值的标注数据,以提高监督学习模型的泛化能力。无论是标注数据还是未标注数据,上述模型可以通过提取未标注数据,学习它们的潜在表示,捕获其语义信息。这可以将任务分为两步,在监督训练之前进行上述步骤。近期,应用这种思路的基于 RNN 的方法已经被广泛使用,并在许多任务中获得了 state-of-the-art 的效果 [1, 3, 4, 30, 31]。此外,还可以构建一个分类器与自动编码解码器联合的分类模型,对潜在向量表示 h 进行分类,详见 [32, 33]。

又比如,在产品评论中,每个评论可能包含了数百个词,这对基于 RNN 的序列编码器来说有着一定的困难。因为 RNN 需要在文本中滑动并抽取信息,这会导致信息的丢失,在长句子中更为严重[34]。另外,在训练过程中解码用到了置信的真实信息,这可能导致无法完全保留来自输入文本的所有信息,而这些丢失的信息对于重建句子、分类、摘要来说是至关重要的。

我们考虑将这个卷积自动编码解码结构应用于长句、段落的半监督学习任务中。我们将半监督问题视作 [35] 中的多任务学习问题,同时训练序列编码器与监督模型,而不是像 [1, 3] 中那样预训练无监督模型。理论上来说,这种联合训练方法得到的段落 embedding 向量会保持重建与分类的能力。

3 相关工作

之前的工作 [22, 28, 21, 38, 39] 已经考虑了将 CNN 用作为一些 NLP 任务的编码器了。一般来说,基于 CNN 的编码器结构会使用一个单一的卷积层再加上池化层。这种结构在给定卷积滤波窗口大小为 h 时,实质上就是做了一个识别特定 h-gram 结构的检测器。从原理看,我们这种架构中的深层结构可以让高层学习到更复杂的语言特征。

4 实验

实验结果图:

5 结论

我们提出了一种只使用卷积与反卷积运算的用于文本模型的通用结构。这种结构没有进行序列条件生成,因此避免了 teacher forcing training 与 exposure bias 问题。这种方法可以将段落完全压缩至潜在表示向量中,此向量也能解压缩重建原始输入序列。总的来说,这种方法实现了高质量的长段落重建,并优于现有的拼写矫正算法、半监督序列分类算法、文本摘要算法,且减少了计算成本。

引用

[1] Andrew M Dai and Quoc V Le. Semi-supervised sequence learning. In NIPS, 2015.

[2] Quoc Le and Tomas Mikolov. Distributed representations of sentences and documents. In ICML, 2014.

[3] Rie Johnson and Tong Zhang. Supervised and Semi-Supervised Text Categorization using LSTM for Region Embeddings. arXiv, February 2016.

[4] Takeru Miyato, Andrew M Dai, and Ian Goodfellow. Adversarial Training Methods for Semi-Supervised Text Classification. In ICLR, May 2017.

[5] Dzmitry Bahdanau, Kyunghyun Cho, and Yoshua Bengio. Neural Machine Translation by Jointly Learning to Align and Translate. In ICLR, 2015.

[6] Kyunghyun Cho, Bart Van Merriënboer, Caglar Gulcehre, Dzmitry Bahdanau, Fethi Bougares, Holger Schwenk, and Yoshua Bengio. Learning phrase representations using rnn encoder-decoder for statistical machine translation. In EMNLP, 2014.

[7] Fandong Meng, Zhengdong Lu, Mingxuan Wang, Hang Li, Wenbin Jiang, and Qun Liu. Encoding source language with convolutional neural network for machine translation. In ACL, 2015.

[8] Tsung-Hsien Wen, Milica Gasic, Nikola Mrksic, Pei-Hao Su, David Vandyke, and Steve Young. Semantically conditioned lstm-based natural language generation for spoken dialogue systems. arXiv, 2015.

[9] Jiwei Li, Will Monroe, Alan Ritter, Michel Galley, Jianfeng Gao, and Dan Jurafsky. Deep reinforcement learning for dialogue generation. arXiv, 2016.

[10] Jiwei Li, Will Monroe, Tianlin Shi, Alan Ritter, and Dan Jurafsky. Adversarial learning for neural dialogue generation. arXiv:1701.06547, 2017.

[11] Ramesh Nallapati, Bowen Zhou, Cicero Nogueira dos santos, Caglar Gulcehre, and Bing Xiang. Abstractive Text Summarization Using Sequence-to-Sequence RNNs and Beyond. In CoNLL, 2016.

[12] Shashi Narayan, Nikos Papasarantopoulos, Mirella Lapata, and Shay B Cohen. Neural Extractive Summarization with Side Information. arXiv, April 2017.

[13] Alexander M Rush, Sumit Chopra, and Jason Weston. A Neural Attention Model for Abstractive Sentence Summarization. In EMNLP, 2015.

[14] Ilya Sutskever, Oriol Vinyals, and Quoc V Le. Sequence to sequence learning with neural networks. In NIPS, 2014.

[15] Tomas Mikolov, Martin Karafiát, Lukas Burget, Jan Cernock`y, and Sanjeev Khudanpur. Recurrent neural network based language model. In INTERSPEECH, 2010.

[16] Sepp Hochreiter and Jürgen Schmidhuber. Long short-term memory. In Neural computation, 1997.

[17] Junyoung Chung, Caglar Gulcehre, KyungHyun Cho, and Yoshua Bengio. Empirical evaluation of gated recurrent neural networks on sequence modeling. arXiv, 2014.

[18] Ronald J Williams and David Zipser. A learning algorithm for continually running fully recurrent neural networks. Neural computation, 1(2):270–280, 1989.

[19] Samy Bengio, Oriol Vinyals, Navdeep Jaitly, and Noam Shazeer. Scheduled sampling for sequence prediction with recurrent neural networks. In NIPS, 2015.

[20] Ferenc Huszár. How (not) to train your generative model: Scheduled sampling, likelihood, adversary? arXiv, 2015.

[21] Nal Kalchbrenner, Edward Grefenstette, and Phil Blunsom. A convolutional neural network for modelling sentences. In ACL, 2014.

[22] Yoon Kim. Convolutional neural networks for sentence classification. In EMNLP, 2014.

[23] Ishaan Gulrajani, Kundan Kumar, Faruk Ahmed, Adrien Ali Taiga, Francesco Visin, David Vazquez, and Aaron Courville. Pixelvae: A latent variable model for natural images. arXiv, 2016.

[24] Alec Radford, Luke Metz, and Soumith Chintala. Unsupervised representation learning with deep convolutional generative adversarial networks. arXiv, 2015.

[25] Vinod Nair and Geoffrey E Hinton. Rectified linear units improve restricted boltzmann machines. In ICML, pages 807–814, 2010.

[26] Ian Chiswell and Wilfrid Hodges. Mathematical logic, volume 3. OUP Oxford, 2007.

[27] Emil Julius Gumbel and Julius Lieblein. Statistical theory of extreme values and some practical applications: a series of lectures. 1954.

[28] Ronan Collobert, Jason Weston, Léon Bottou, Michael Karlen, Koray Kavukcuoglu, and Pavel Kuksa. Natural language processing (almost) from scratch. In JMLR, 2011.

[29] Sharan Chetlur, Cliff Woolley, Philippe Vandermersch, Jonathan Cohen, John Tran, Bryan Catanzaro, and Evan Shelhamer. cudnn: Efficient primitives for deep learning. arXiv, 2014.

[30] Zichao Yang, Diyi Yang, Chris Dyer, Xiaodong He, Alex Smola, and Eduard Hovy. Hierarchical attention networks for document classification. In NAACL, 2016.

[31] Adji B Dieng, Chong Wang, Jianfeng Gao, and John Paisley. TopicRNN: A Recurrent Neural Network with Long-Range Semantic Dependency. In ICLR, 2016.

[32] Diederik P Kingma, Shakir Mohamed, Danilo Jimenez Rezende, and Max Welling. Semi-supervised learning with deep generative models. In NIPS, 2014.

[33] Yunchen Pu, Zhe Gan, Ricardo Henao, Xin Yuan, Chunyuan Li, Andrew Stevens, and Lawrence Carin.

Variational autoencoder for deep learning of images, labels and captions. In NIPS, 2016.

[34] Sepp Hochreiter, Yoshua Bengio, Paolo Frasconi, and Jürgen Schmidhuber. Gradient flow in recurrent nets: the difficulty of learning long-term dependencies, 2001.

[35] Richard Socher, Jeffrey Pennington, Eric H Huang, Andrew Y Ng, and Christopher D Manning. Semisupervised recursive autoencoders for predicting sentiment distributions. In EMNLP. Association for Computational Linguistics, 2011.

[36] Samuel R Bowman, Luke Vilnis, Oriol Vinyals, Andrew M Dai, Rafal Jozefowicz, and Samy Bengio. Generating sentences from a continuous space. arXiv, 2015.

[37] Zichao Yang, Zhiting Hu, Ruslan Salakhutdinov, and Taylor Berg-Kirkpatrick. Improved Variational Autoencoders for Text Modeling using Dilated Convolutions. arXiv, February 2017.

[38] Baotian Hu, Zhengdong Lu, Hang Li, and Qingcai Chen. Convolutional neural network architectures for matching natural language sentences. In NIPS, 2014.

[39] Rie Johnson and Tong Zhang. Effective use of word order for text categorization with convolutional neural networks. In NAACL HLT, 2015.

[40] Jost Tobias Springenberg, Alexey Dosovitskiy, Thomas Brox, and Martin Riedmiller. Striving for simplicity: The all convolutional net. arXiv, 2014.

[41] Karen Simonyan and Andrew Zisserman. Very deep convolutional networks for large-scale image recognition. In ICLR, 2015.

[42] Stanislau Semeniuta, Aliaksei Severyn, and Erhardt Barth. A Hybrid Convolutional Variational Autoencoder for Text Generation. arXiv, February 2017.

[43] Nal Kalchbrenner, Lasse Espeholt, Karen Simonyan, Aaron van den Oord, Alex Graves, and Koray Kavukcuoglu. Neural machine translation in linear time. arXiv, 2016.

[44] Yann N Dauphin, Angela Fan, Michael Auli, and David Grangier. Language Modeling with Gated Convolutional Networks. arXiv, December 2016.

[45] J. Gehring, M. Auli, D. Grangier, D. Yarats, and Y. N. Dauphin. Convolutional Sequence to Sequence Learning. arXiv, May 2017.

[46] Aaron van den Oord, Nal Kalchbrenner, Lasse Espeholt, Oriol Vinyals, Alex Graves, et al. Conditional image generation with pixelcnn decoders. In NIPS, pages 4790–4798, 2016.

[47] Jiwei Li, Minh-Thang Luong, and Dan Jurafsky. A hierarchical neural autoencoder for paragraphs and documents. In ACL, 2015.

[48] Tomas Mikolov, Ilya Sutskever, Kai Chen, Greg S Corrado, and Jeff Dean. Distributed representations of words and phrases and their compositionality. In NIPS, 2013.

[49] Kam-Fai Wong, Mingli Wu, and Wenjie Li. Extractive summarization using supervised and semi-supervised learning. In ICCL. Association for Computational Linguistics, 2008.

[50] Chin-Yew Lin. Rouge: A package for automatic evaluation of summaries. In ACL workshop, 2004.

[51] Kishore Papineni, Salim Roukos, Todd Ward, and Wei-Jing Zhu. Bleu: a method for automatic evaluation of machine translation. In ACL. Association for Computational Linguistics, 2002.

[52] Xiang Zhang, Junbo Zhao, and Yann LeCun. Character-level convolutional networks for text classification. In NIPS, pages 649–657, 2015.

[53] Dzmitry Bahdanau, Philemon Brakel, Kelvin Xu, Anirudh Goyal, Ryan Lowe, Joelle Pineau, Aaron Courville, and Yoshua Bengio. An actor-critic algorithm for sequence prediction. arXiv, 2016.

[54] JP Woodard and JT Nelson. An information theoretic measure of speech recognition performance. In Workshop on standardisation for speech I/O, 1982.

[55] Diederik Kingma and Jimmy Ba. Adam: A method for stochastic optimization. In ICLR, 2015.

[56] Xavier Glorot and Yoshua Bengio. Understanding the difficulty of training deep feedforward neural networks. In AISTATS, 2010.

小组使用了 GitLab,在某项目中需要利用 CI 进行远程部署,但生产服务器是另一台远程服务器,而 CI Runner 的运行环境是 Docker。

由于 ssh 的安全措施,在建立 ssh 连接时不允许自动化输入密码,必须在出现密码框后手动输入,因此在 CI 流程中无法实现;经过了解,一款名为 sshpass 的工具可以满足此需求;

在本机测试时,使用

1
brew install sshpass
提示
1
We won't add sshpass because it makes it too easy for novice SSH users to ruin SSH's security.
可知此工具的确是破坏了 ssh 的安全性。

但在内部机器的 CI Runner Docker 中,不考虑这个问题。Gitlab CI Runner 默认使用的是 alpine:latest,需要事先安装依赖及 sshpass,因此在 before_script 中加上 apk add --update --no-cache openssh sshpass 安装此工具。

在后续的 job/stage 中,加入远程执行命令脚本:

1
sshpass -p 'password' ssh -T -o StrictHostKeyChecking=no username@192.168.xxx.xxx 'sh /home/remote/script.sh'

即可绕过手动输入密码步骤,在 CI 中运行远程 sh 脚本。

在联网环境慎用,防止泄密