标签存档: 算法

[学习笔记] Logistic 回归

本文为 Stanford 的机器学习公开课第三周的笔记,不过和上课内容的细节有些出入。

Logistic 回归解决的并不是回归问题,而是分类问题,即目标变量(target variable)的值是离散而非连续的。这时目标变量也可称为标签(label)。如果仍用线性回归硬搞,得到的结果会非常不靠谱。

我们先考虑简单的情况:数据点只有 0 和 1 两个标签(binary classification),即 \( y \in \{0,1\} \),且大致上是线性可分的(linearly separable)。如图:

那么现在问题就是要找到一个 \(\theta\),使得直线 \(\theta^{\rm T} x = 0\) [1] 能够将上面图中的 positive 和 negative 两类数据点“分开”;这样的一条直线称为决策边界(decision boundary)。——但具体什么叫“分开”?或者说,如果已知 \(\theta\),对于一个标签未知的数据点 \(x\),怎么判断它是 positive 还是 negative?

直观上看,在给定 \(\theta\) 和 \(x\) 的情况下,\(y\) 应该满足一个 0-1 分布,即
$$ y|x;\theta \sim Bernoulli\big(\phi(\theta^{\rm T} x)\big) $$
其中 \( \phi(\theta^{\rm T} x) = P(y = 1|x;\theta) \) 应该满足:

  1.  \( 0 \leq \phi(\theta^{\rm T} x) \leq 1 \)
  2. 若 \(\theta^{\rm T} x > 0\),我们可以认为 \( y=1 \) 的可能性相对更大,即 \( \phi(\theta^{\rm T} x) > 0.5 \),且如果数据点离边界越远,即 \(\theta^{\rm T} x\) 越大, \(\phi(\theta^{\rm T} x)\) 也应该越大。
  3. 反之,若 \(\theta^{\rm T} x < 0\),则 \( \phi(\theta^{\rm T} x) < 0.5 \),且随 \(\theta^{\rm T} x\) 减小而减小。
  4. 当 \(\theta^{\rm T} x = 0\) 时,点落在决策边界上,我们无从判断它的标签 \(y\),因此 \( \phi(\theta^{\rm T} x) = 0.5 \)。

那 \(\phi\) 应该取什么样的函数呢?我们知道 logistic 函数恰巧满足上述条件,不妨就取它(“logistic 回归”也因此得名)[2]。即:
$$ \phi(z) = \frac{1}{1+\exp(-z)} $$
logistic 函数的图像是:

可以直观地看到它确实是满足我们要求的。这样我们就得到了:
$$ P(y = 1|x;\theta) = \phi(\theta^{\rm T} x) = \frac{1}{1+\exp(-\theta^{\rm T} x)} $$
我们记 \( h_\theta(x) = P(y = 1|x;\theta) \),表示这是我们希望预测的量,也就是模型的假设(hypothesis) [3]。在实际分类应用中,\(h_\theta(x) > 0.5\) 时我们可以给出判断 \(y = 1\) ,\(h_\theta(x) < 0.5\) 时 \(y = 0\) ,\(h_\theta(x) = 0.5\) 的话就蒙一个吧。

接下来 \(\theta\) 该怎么求呢?根据log极大似然法,可知 \(\theta\) 的最优值就是 \(\arg\max_\theta L(\theta)\),其中
$$
\begin{eqnarray}
L(\theta) &=& \log \prod_i P(y^{(i)}|x^{(i)};\theta) \\
&=& \log \prod_i h_\theta(x^{(i)})^{y^{(i)}} \big(1-h_\theta(x^{(i)})\big)^{1-y^{(i)}} \\
&=& \sum_i y^{(i)} \log h_\theta(x^{(i)}) + (1-y^{(i)}) \log h_\theta(1-x^{(i)})
\end{eqnarray}
$$
其中第二步有点小 tricky。注意到 \( h_\theta(x)^y \big(1-h_\theta(x)\big)^{1-y} \) 在 \(y=0\) 时值为 \(h_\theta(x)^y\),在 \(y=1\) 时值为 \(\big(1-h_\theta(x)\big)^{1-y}\)。这样就把 \(y\) 两种取值的情况合并写在一个式子里了。

接下来我们可以求出梯度 \(\nabla_\theta L(\theta)\)。如果我们像上篇笔记中一样定义:
$$
X := \begin{pmatrix} (x^{(1)})^{\rm T} \\ (x^{(2)})^{\rm T} \\ \vdots \\ (x^{(m)})^{\rm T} \end{pmatrix} \quad
\vec{y} := \begin{pmatrix} y^{(1)} \\ y^{(2)} \\ \vdots \\ y^{(m)} \end{pmatrix} \quad
\vec{h_\theta} := \begin{pmatrix} h_\theta(x^{(1)}) \\ h_\theta(x^{(2)}) \\ \vdots \\ h_\theta(x^{(m)}) \end{pmatrix}
$$
可以推导得到:
$$ \nabla_\theta L(\theta) = \frac{1}{m} X^{\rm T} (\vec{y} – \vec{h_\theta}) $$
接下来,根据梯度上升算法 [4],我们就可以迭代计算下式来逼近 \(\arg\max_\theta L(\theta)\):
$$
\begin{eqnarray}
\theta_{t+1} &:=& \theta_t + \alpha \nabla_\theta L(\theta) \\
&=& \theta_t – \frac{\alpha}{m} X^{\rm T} (\vec{h_\theta} – \vec{y})
\end{eqnarray}
$$
注意到这个更新式的形式和之前线性规划+最小二乘法+梯度下降得到是一样的,只是 \(\vec{h_\theta}\) 变了。

以上就是用梯度上升做 Logistic 回归的算法了。课上还谈到了另外 3 个问题:

  1. 如果要把 Logistic 回归推广到多个分类的情况,可以利用一种叫“1 vs all”的方法。具体地说,即每次取一个分类为 positive,其它分类都为 negative。这样针对每一个分类都学习得到一个 Logistic 回归模型。要判断一个新数据点的标签,用每一个模型都测一下,取对应 \( h_\theta(x) \) 最大的分类为预测结果。
  2. 如果数据点不是线性可分的,类似线性回归到多项式回归(polynomial regression)的推广,可以将同一属性次数不同的项看成是彼此独立的,再和普通 Logistic 回归问题一样处理。
  3. 属性过多可能会造成模型过分复杂,导致过拟合问题。关于 overfit 和 underfit 的问题可以参见这篇文章的 3~6 段,这位学长讲得非常深入浅出了。课上提到的一种解决方法叫 regularization,即对 \(\theta\) 中希望加以限制的各项 \(\theta_i\),在 \(L(\theta)\) 后加上 \(-\lambda \theta_i^2 \),使这些值过大时“惩罚”目标函数。这里 \(\lambda > 0\) 的取值也要注意,如果取得太小解决不了过拟合问题,但取得太大又会造成 underfit。

p.s. 今天早睡的目标又达成失败了……………………


脚注:
  1. [1] 和上节线性规划中一样,每个 \(x^{(i)}\) 是一个 \(n+1\) 维向量,为书写简便我们在 \(n\) 个属性前再加一维 \(x^{(i)}_0=1\)。
  2. [2] 这里说“不妨”似乎有点太随意了。事实上,如果假设在给定 \(x\) 的情况下 \(y\) 服从 0-1 分布,那么\(\phi\) 取 logistic 函数实际上可以由广义线性模式(Generalized linear model)的假设推导得出。
  3. [3] 注意到和上节课讲的线性回归一样,我们要预测的 \(h_\theta(x)\) 其实都是期望 \(E_{y|x;\theta}\)。
  4. [4] 由于这里要求的是 \(L(\theta)\) 的最大值,因此是梯度上升而非梯度下降。课程视频中取要最小化的目标函数\(J(\theta)=-L(\theta)\),因此是梯度下降。实际是一回事。

[学习笔记] 线性回归(及梯度下降)

这个做的是 Stanford 的机器学习公开课的笔记。前两课很简单,吴大牛讲得又极清楚,我很多地方就简单记了。

线性回归解决的是一个回归问题(怎么像废话),即要预测的目标变量(target variable)是连续值。我们有一个大小为 \(m\) 的训练集,其中数据点 \( x^{(i)} \) 分别对应目标变量 \( y^{(i)} \) \( (1 \leq i \leq m) \);每个 \(x^{(i)}\) 是一个 \(n+1\) 维向量,\(n\) 为一个数据点的属性(feature)数。(为后面书写简便我们在 \(n\) 个属性前再加一维 \( x_0^{(i)} = 1 \),即 \( x^{(i)} = (1, x_1^{(i)}, x_2^{(i)}, …, x_n^{(i)})^{\rm T} \)。)给定这样一个训练集,我们希望知道对于某个训练集外的 \( x \),它对应的 \( y \) 是什么。

线性回归的假设(hypothesis)是,数据点的属性 \(x\) 和对应的目标变量 \(y\) 是线性关系,可以用一条直线 \(  h_\theta(x) = \theta^{\rm T} x \) 来拟合 [1],就像下面这样(图片盗自维基):

其中 \( \theta \) 也是一个 \(n+1\) 维向量,就是我们想拟合的参数,因为假如能求出 \( \theta \),对于一个训练集外的点 \( x \) 我们就能预测它对应的 \( y = \theta^{\rm T} x \) 了。

为了评价我们估计的 \( \theta \) 的优劣,我们需要引入一个目标函数/费用函数(cost function)。根据最小二乘法,我们定义下面的 \( J(\theta) \) 为我们需要最小化的目标函数:
$$
J(\theta) = \frac{1}{2m} \sum_i^m (h_\theta(x) – y^{(i)})^2
$$
其中 \( 1/2m \) 这个常系数并非必须,只是为了计算便利加上去的。我们要求的就是 \( \arg\min_\theta J(\theta) \)。

不过在继续之前我们先把 \( J(\theta) \) 的记法再进一步 vectorize 一下。我们定义一个 \( m \times n \) 的设计矩阵 \( X \),其第 \(i\) 行为 \( (x^{(i)})^{\rm T} \),即
$$
X := \begin{pmatrix} (x^{(1)})^{\rm T} \\ (x^{(2)})^{\rm T} \\ \vdots \\ (x^{(m)})^{\rm T} \end{pmatrix}
= \begin{pmatrix} 1 & x_1^{(1)} & x_2^{(1)} & … & x_n^{(1)} \\
1 & x_1^{(2)} & x_2^{(2)} & … & x_n^{(2)} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
1 & x_1^{(m)} & x_2^{(m)} & … & x_n^{(m)} \end{pmatrix}
$$
我们再定义
$$
\begin{eqnarray}
&&\vec{y} := (y^{(1)},y^{(2)},…,y^{(m)})^{\rm T} \\
&&\vec{h_\theta} := \big( h_\theta(x^{(1)}),h_\theta(x^{(2)}),…,h_\theta(x^{(m)}) \big)^{\rm T} = X\theta
\end{eqnarray}
$$
不难推出 \( J(\theta) \) 可以写成下面的形式:
$$
J(\theta) = \frac{1}{2m} (\vec{h_\theta} – \vec{y})^{\rm T} (\vec{h_\theta} – \vec{y})
$$
我们还可以接着求出 \( J(\theta) \) 的梯度 \( \nabla_\theta J(\theta) \) [2]
$$
\nabla_\theta J(\theta) = \frac{1}{m} X^{\rm T} (\vec{h_\theta} – \vec{y}) = \frac{1}{m} X^{\rm T} (X\theta – \vec{y})
$$

我们知道函数在极值处梯度为零,因此第一种解法就是直接求 \( \nabla_\theta J(\theta) = 0 \)。这样可以得到一个解析解:
$$
\theta = (X^{\rm T} X)^{-1} X^{\rm T} \vec{y}
$$
注意其中 \( X^{\rm T} X \) 是一个 \( n \times n \) 的矩阵,而矩阵求逆的的复杂度一般是 \( O(n^3) \)。所以这种解法在 \(n\) 比较小的时候非常高效,但在 \(n\) 较大时就瞎了。

我们还知道 \( J(\theta) \) 上某一点的梯度向量指向 \( J(\theta) \) 增长最快的方向,长度为这个增长的变化率。因此第二种解法就是让 \( \theta \) 的值向着 \( -\nabla_\theta J(\theta) \) 的方向,即 \( J(\theta) \) 减小最快的方向迭代变化,这样就可以逼近 \( J(\theta) \) 的极小值了。因此我们可以得到下面被称为梯度下降的算法:

Repeat until converge {$$
\begin{eqnarray}
\theta_{t+1} &:=& \theta_t – \alpha \nabla_\theta J(\theta) \\
&=& \theta_t – \frac{\alpha}{m} X^{\rm T} (\vec{h_\theta} – \vec{y})
\end{eqnarray}
$$}

[3] 其中 \(\alpha\) 称为学习速率(learning rate),意思就是……学习的速率= = 这个参数控制着每次迭代 \(\theta\) 值变化的保守或激进程度;如果太大,一次迭代对 \(\theta\) 改变过大,导致“冲得太猛”越过了极值点没法收敛;如果太小,一次迭代对 \(\theta\) 改变太少,算法收敛太慢。决定 \( \alpha \) 的方法是……呃,就是试,“0.01 太小,0.1 太大,0.03 有点小,咦 0.06 正好”这样。

对于线性回归和梯度下降,课上还谈到了另外两个话题。一是属性缩放(feature scaling)。如果不同属性数量级差得太大,会严重影响梯度下降的性能。这时可以对分别对每项属性做类似“减去均值,除以标准差”这样的缩放。二是多项式回归(polynomial regression)。属性值和目标变量值之间的关系可能不是线性的,而是多项式的关系,比如 \( y = \theta_0 + \theta_1 x + \theta_2 x^2 \) 这样。但由于这个式子对于 \( \theta \) 仍然是线性的,所以用最小二乘法时可以直接把 \(x\)、\(x^2\) 看成是彼此独立的不同的属性,然后按上面的方法如法炮制。至于多项式回归时属性的次数怎么取,应该会在讲 bias-variance trade-off 的时候讲吧,我到时候再写好了。


脚注:
  1. [1] 这里讲得有点 sloppy 了(不过你能明白我的意思就行=.=)。更严格一点说,应该是训练集中的数据满足:
    $$
    y^{(i)} = \theta^{\rm T} x^{(i)} + \epsilon^{(i)}
    $$
    其中 \( \epsilon^{(i)} \) 表示误差;各 \( \epsilon^{(i)} \) 满足均值为零,方差相同且互不相关(参见高斯-马尔科夫定理)。

    顺带一提,我们如果进一步假设 \( \epsilon^{(i)} \sim N(0,\sigma^2) \),有 \( y^{(i)}|x^{(i)};\theta \sim N(\theta^{\rm T} x^{(i)},\sigma^2) \),则这个模型成为广义线性模式(Generalized linear model)的一个特例。我们用极大log似然法
    $$ \arg\max_\theta L(\theta) = \arg\max_\theta \sum_i \log(P(y^{(i)}|x^{(i)};\theta)) $$
    可以推出和下面提到的最小二乘法一样的形式。——如果你不知道我上面在说什么,忽略吧,不碍得的。

  2. [2] 推导其实挺麻烦的。教授在课上没讲,我也懒得敲了=.=
  3. [3] 注意这里每次迭代的运算都会考虑整个训练集,这称为批量梯度下降(batch gradient descent),当训练集很大时这可能导致很大的开销。我们也可以每次迭代只依次选取训练集中的一个实例 \(x^{(i)}\),更新
    $$
    \theta_{t+1} := \theta_{t} – \alpha (\theta^{\rm T} x^{(i)} – y^{(i)}) x^{(i)}
    $$
    这个叫增量梯度下降(incremental gradient descent)。好处在于每次迭代开销小,因而收敛速度较快,在处理大数据集时可能很有用。但坏处在于这算法可能永远都收敛不到极小值,而是一直围着极小值转——不过在实际应用中这个也能接受了。

[学习笔记] Probabilistic latent semantic analysis (pLSA)

Probabilistic latent semantic analysis (概率潜在语义分析,pLSA) 是一种 Topic model,在99年被 Thomas Hofmann 提出。它和随后提出的 LDA 使得 Topic Model 成为了研究热点,其后的模型大都是建立在二者的基础上的。

我们有时会希望在数量庞大的文档库中自动地发现某些结构。比如我们希望在文档库发现若干个“主题”,并将每个主题用关键词的形式表现出来。我们还希望知道每篇文章中各个主题占得比重如何,并据此判断两篇文章的相关程度。而 pLSA 就能完成这样的任务。

我之前取了 Wikinews 中的 1000 篇新闻,试着用 pLSA 在其中发现 K=15 个主题。比如一篇关于 Wikileaks 的阿萨奇被保释消息的新闻,算法以 100% 的概率把它分给了主题 9,其关键词为:

media phone hacking wikileaks assange
australian stated information investigation murdoch

可以看到这个主题的发现还是非常靠谱的。又比如这条中国人民的老朋友威胁要大打打核战争的新闻。算法把它以 97.7% 的概率分给了主题 3,2.3% 的概率分给了主题 7。主题 3 的关键词是:

south north court china military
death tornado service million storm

主题 7 的关键词是:

nuclear plant power japan million
carbon radiation china water minister

可以看到这条新闻和主题 3 中的“南北”、“军事”、“中国”、“死亡”这些信息联系在一起,和主题 7 中的“核”、“中国”联系在一起。应该是因为我的数据集中与北朝鲜核问题相关的新闻只有不到 10 条,而 10 个主题的划分并不够细致,所以关于“朝核问题”或者“核武器”的这样的主题并没能被分离出来。但可以看到即使是这样结果也是很 make sense 的。

那我们就来看看 pLSA 模型是怎么回事吧。和很多模型一样,pLSA 遵从 bag-of-words 假设,即只考虑一篇文档中单词出现的次数,忽略单词的先后次序关系,且每个单词的出现都是彼此独立的。这样一来,我们观察到的其实就是每个单词 \(w \in W\) 在每篇文档 \(d \in D\) 中出现的次数 \(n(w,d)\)。pLSA 还假设对于每对出现的 \((d,w)\) 都对应着一个表示“主题”的隐藏变量 \(z \in Z\)。pLSA 是一个生成模型,它假设 \(d\)、\(w\) 和 \(z\) 之间的关系用贝叶斯网络表示是这样的(从 (Blei et al., 2003) 偷的图):

实心的节点 \(d\) 和 \(w\) 表示我们能观察到的文档和单词,空心的 \(z\) 表示我们观察不到的隐藏变量,用来表示隐含的主题。图中用了所谓的“盘子记法”,即用方框表示随机变量的重复。这里方框右下角的字母 \(M\) 和 \(N\) 分别表示有 \(M\) 篇文档,第 \(j\) 篇文档有 \(N_j\) 个单词。每条有向边表示随机变量间的依赖关系。也就是说,pLSA 假设每对 \((d,w)\) 都是由下面的过程产生的:

  1. 以 \(P(d)\) 的先验概率选择一篇文档 \(d\)
  2. 选定 \(d\) 后,以 \(P(z|d)\) 的概率选中主题 \(z\)
  3. 选中主题 \(z\) 后,以 \(P(w|z)\) 的概率选中单词 \(w\)

而我们感兴趣的正是其中的 \(P(z|d)\) 和 \(P(w|z)\):利用前者我们可以知道每篇文章中各主题所占的比重,利用后者我们则能知道各单词在各主题中出现的概率,从而进一步找出各主题的“关键词”。记 \(\theta = (P(z|d), P(w|z))\),表示我们希望估计的模型参数。当然 \(\theta\) 不仅仅代表两个数,而是对于每对 \( (w^{(j)}, z^{(k)}) \) 和 \( (d^{(i)}, z^{(k)}) \) ,我们都要希望知道 \( P(z^{(k)}|d^{(i)}) \) 和 \( P(w^{(j)}|z^{(k)}) \) 的值。也就是说,模型中共有 \( |Z| \cdot |D| + |W| \cdot |Z| \) 个参数。我们还知道:
$$
\begin{eqnarray}
P(d,w) &=& P(d)P(w|d) \\
P(w|d) &=& \sum_z P(w|z)P(z|d)
\end{eqnarray}
$$
根据最大log似然估计法,我们要求的就是
$$
\begin{eqnarray}
\arg\max_\theta L(\theta)
&=& \arg\max_\theta \sum_{d,w} n(d,w)\log P(d,w;\theta) \\
&=& \arg\max_\theta \sum_{d,w} n(d,w)\log P(w|d;\theta)P(d) \\
&=& \arg\max_\theta \left\{ \sum_{d,w} n(d,w)\log P(w|d;\theta) + \sum_{d,w} n(d,w)\log P(d) \right\}
\end{eqnarray}
$$
这里由于 \( \sum_{d,w} n(d,w)\log P(d) \) 这一项与 \(\theta\) 无关,在 \(\arg\max_\theta\) 中可以被直接扔掉。[1] 因此
$$
\begin{eqnarray}
\arg\max_\theta L(\theta)
&=& \arg\max_\theta \sum_{d,w} n(d,w)\log P(w|d;\theta) \\
&=& \arg\max_\theta \sum_{d,w} n(d,w)\log \sum_z P(w|z)P(z|d)
\end{eqnarray}
$$

这里出现了 \( \log \) 套 \( \sum \) 的形式,导致很难直接拿它做最大似然。但假如能观察到 \(z\),问题就很简单了。于是我们想到根据 EM 算法(参见我的上篇笔记),可以用下式迭代逼近 \( \arg\max_\theta L(\theta) \):
$$ \arg\max_\theta Q_t(\theta) = \arg\max_\theta \sum_{d,w} n(d,w) E_{z|d,w;\theta_t}[\log P(w|d;\theta)] $$
其中
$$
\begin{eqnarray}
E_{z|d,w;\theta_t}[\log P(w|d;\theta)]
&=& \sum_z P(z|d,w;\theta_t) \log P(w|d;\theta) \\
&=& \sum_z P(z|d,w;\theta_t) [\log P(w|z) + \log P(z|d)] \\
\end{eqnarray}
$$
在 E-step 中,我们需要求出 \( Q_t(\theta) \) 中除 \(\theta\) 外的其它未知量,也就是说对于每组 \( (d^{(i)}, w^{(j)}, z^{(k)}) \) 我们都需要求出 \( P(z^{(k)}|d^{(i)},w^{(j)};\theta_t) \)。根据贝叶斯定理,我们知道:
$$
P(z^{(k)}|d^{(i)},w^{(j)};\theta_t) = \frac{P_t(z^{(k)}|d^{(i)})P_t(w^{(j)}|z^{(k)})} {\sum_z P_t(z|d^{(i)})P_t(w^{(j)}|z)}
$$
而 \( P_t(z|d) \) 和 \( P_t(w|z) \) 就是上轮迭代求出的 \( \theta_t \)。这样就完成了 E-step。

接下来 M-step 就是要求 \( \arg\max_\theta Q_t(\theta) \) 了。利用基本的微积分工具 [2],可以分别对每对 \( (w^{(j)}, z^{(k)}) \) 和 \( (d^{(i)}, z^{(k)}) \) 求出:
$$
\begin{eqnarray}
P_{t+1}(w^{(j)}|z^{(k)}) = \frac {\sum_d n(d,w^{(j)})P(z^{(k)}|d,w^{(j)};\theta_t)} {\sum_{d,w} n(d,w)P(z^{(k)}|d,w;\theta_t)} \\
P_{t+1}(z^{(k)}|d^{(i)}) = \frac {\sum_w n(d^{(i)},w)P(z^{(k)}|d^{(i)},w;\theta_t)} {\sum_{w,z} n(d,w)P(z|d^{(i)},w;\theta_t)}
\end{eqnarray}
$$
以上就是 pLSA 算法了。最后贴个我用 MATLAB 写的实现 [3]

function [p_w_z, p_z_d, Lt] = pLSA(n_dw, n_z, iter_num)
% PLSA	Fit a pLSA model on given data
%       in which n_dw(d,w) is the number of occurrence of word w
%		in document d, d, n_z is the number of topics to be discovered
%		

% pre-allocate space
[n_d, n_w] = size(n_dw); % max indices of d and w
p_z_d = rand(n_z, n_d); % p(z|d)
p_w_z = rand(n_w, n_z); % p(w|z)
n_p_z_dw = cell(n_z, 1); % n(d,w) * p(z|d,w)
for z = 1:n_z
    n_p_z_dw{z} = sprand(n_dw);
end

p_dw = sprand(n_dw); % p(d,w)
Lt = []; % log-likelihood
for i = 1:iter_num
    %disp('E-step');
    for d = 1:n_d
        for w = find(n_dw(d,:))
            for z = 1:n_z
                n_p_z_dw{z}(d,w) = p_z_d(z,d) * p_w_z(w,z) * ...
					n_dw(d,w) / p_dw(d, w);
            end
        end
    end

    %disp('M-step');
    %disp('update p(z|d)')
    concat = cat(2, n_p_z_dw{:}); % make n_p_z_dw{:}(d,:)) possible
    for d = 1:n_d
        for z = 1:n_z
            p_z_d(z,d) = sum(n_p_z_dw{z}(d,:));
        end
        p_z_d(:,d) = p_z_d(:,d) / sum(concat(d,:));
    end

    %disp('update p(w|z)')
    for z = 1:n_z
        for w = 1:n_w
            p_w_z(w,z) = sum(n_p_z_dw{z}(:,w));
        end
        p_w_z(:,z) = p_w_z(:,z) / sum(n_p_z_dw{z}(:));
    end

    % update p(d,w) and calculate likelihood
    L = 0;
    for d = 1:n_d
        for w = find(n_dw(d,:))
            p_dw(d,w) = 0;
            for z = 1:n_z
                p_dw(d,w) = p_dw(d,w) + p_w_z(w,z) * p_z_d(z,d);
            end
            L = L + n_dw(d,w) * log(p_dw(d, w));
        end
    end

    Lt = [Lt; L];
    %plot(Lt); ylim([2*median(Lt)-L-0.1 L+(L-median(Lt))/2+0.1]);
    %drawnow; pause(0.1)
end

end

第一次拿 Mablab 写程序,比较丑……[4] 下图是 Log 似然度随迭代收敛的情况。可以看到收敛速度还是相对较快的。而且由于是 EM 算法的缘故,Log 似然度确实是单调上升的.

最后,pLSA 的问题是在文档的层面上没有一个概率模型,每篇文档的 P(d|z) 都是需要拟合的模型参数。这就导致参数的数目会随文档数目线性增长、不能处理训练集外的文档这样的问题。所以02年 David Blei、Andrew Ng(就是正在 ml-class.org 里上公开课的那位) 和 Michael Jordan 又提出了一个更为简洁的模型:LDA。有时间的话下次再写了。

参考文献

(Hofmann, 1999) Hofmann, T. 1999. Probabilistic latent semantic indexing. Proceedings of the 22nd annual international ACM SIGIR conference on Research and development in information retrieval SIGIR 99. pages, (1999), 50-57.
(Gildea & Hofmann, 1999) Gildea, D. and Hofmann, T. 1999. Topic-based language models using EM. Proceedings of the 6th European Conference on Speech (1999), 2167-2170.
(Brants, 2005) Brants, T. 2005. Test Data Likelihood for PLSA Models. Information Retrieval. (2005), 181-196.
(Blei et al., 2003) Blei, D.M. et al. 2003. Latent Dirichlet Allocation. Journal of Machine Learning Research. 3, 4-5 (2003), 993-1022.


脚注:
  1. [1] 这里 Hofmann 自己在 (Hofmann, 1999) 和 (Gildea & Hofmann, 1999) 中使用了不同的形式。本文和 (Gildea & Hofmann, 1999)、(Brants, 2005) 一样选择不去理会 \(P(d)\)。因为正如 (Brants, 2005) 中指出、(Blei et al., 2003) 及很多其它文献吐槽的那样,(Hofmann, 1999) 中的模型算出的 \(P(d)\) 实在坑爹,当 \(d\) 不在训练集中时 \(P(d)\) 就一律为0,没什么意义,还不如别估计它呢。另外,(Hofmann, 1999) 中额外引入了一个参数 \(\beta\) 来“解决”过度拟合问题,但 (Brants, 2005) 中指出这一问题实际并不存在,因此本文也对此忽略不提。
  2. [2] 具体而言,这里要求的是 \( Q_t(\theta) \) 在 \( \sum_w P(w|z) = 1 \) 和 \( \sum_z P(z|d) = 1 \) 约束条件下的极值。根据拉格朗日乘数法,解:

    $$
    \nabla_\theta \left( Q(\theta) + \sum_z \alpha_z (\sum_w P(w|z) -1) + \sum_d \beta_d (\sum_z P(z|d) -1) \right) = \mathbf{0}
    $$

  3. [3] 完整的程序和数据在这里
  4. [4] 吐槽:用 Matlab 做简单字符串处理怎么都那么恶心!长度不同的字符串竟然算是不同类型的!Cell array 怎么那么难用!

[学习笔记] Expectation-Maximization(EM) 算法


果然还是有必要保持记笔记的习惯呐。前两天实验室讲 pLSA 的推导,用到 EM 竟然完全记不清了,竟然还把 Jensen 不等式和 SVM 的 minmax=maxmin 什么的记混= = (这么一说 SVM 具体怎么回事也记不清了。。。)赶紧补一篇复习笔记。

Expectation-Maximization 算法是统计学中用来给带隐含变量的模型做最大似然(和最大后验概率)的一种方法。EM 的应用特别广泛,经典的比如做概率密度估计用的 Gaussian Mixture Model。这两天我或许还会写 pLSA 的笔记放上来,也是 EM 应用的例子。

下面我会先解释 EM 算法要解决的问题,然后直接给出算法的过程,最后再说明算法的正确性。

问题

首先我们定义要解决的问题。给定一个训练集 \(X=\{x^{(1)},…,x^{(m)}\}\),我们希望拟合包含隐含变量 \(z\) 的模型 \(P(x,z;\theta)\) 中的参数 \(\theta\)。根据模型的假设,每个我们观察到的 \(x^{(i)}\) 还对应着一个我们观察不到的隐含变量 \(z^{(i)}\),我们记 \(Z=\{z^{(1)},…,z^{(m)}\}\)。做最大对数似然就是要求 \( \theta \) 的“最优值”:$$\theta=\arg\max_\theta{L(\theta;X)}$$ 其中 $$L(\theta;X)=log{P(X;\theta)}=\log{\sum_Z P(X,Z;\theta)}$$

想用这个\(\log\)套\(\sum\)的形式直接求解 \(\theta\) 往往非常困难。而如果能观察到隐含变量 \(z\) ,求下面的似然函数的极大值会容易许多:$$L(\theta;X,Z)=\log{P(X, Z;\theta)}$$

问题是实际上我们没法观察到 \(z\) 的值,只能在给定 \(\theta\) 时求 \(z\) 的后验概率 \(P(z|x;\theta)\) [1]。EM 算法就可以帮我们打破这样的困境。

算法

下面给出 EM 算法的过程。其中\(\theta_t\) 是第 t-1 次迭代时算出的 \(\theta\) 值;\(\theta_0\) 为任意初值。

Repeat until converge {

  1. (E-step) Calculate \( P(Z|X;\theta_t) \), in order to get:
    $$
    \begin{eqnarray}
    E_{Z|X;\theta_t}[L(\theta;X,Z)] &:=& E_{Z|X;\theta_t}[\log{P(X,Z;\theta)}] \\
    &=& \sum_Z P(Z|X;\theta_t) \log{P(X,Z;\theta)}
    \end{eqnarray}
    $$
  2. (M-step) $$\theta_{t+1} := \arg\max_\theta E_{Z|X;\theta_t}[\log{P(X,Z;\theta)}]$$

}

对,就这么短。所以我总觉得称之为 algorithm 不如称之为 method 更恰当。上面的过程在收敛后就得到了我们需要的 \( \theta=\arg\max_\theta{L(\theta;X)} \) [2]

先简单说说这短短两步都做了些啥。EM 算法每次迭代都建立在上轮迭代对 \(\theta\) 的最优值的估计 \(\theta_t\) 上,利用它可以求出 \(Z\) 的后验概率 \(P(Z|X;\theta_t)\),进而求出 \(L(\theta;X,Z)\) 在分布 \(Z \sim P(Z|X;\theta)\) 上的期望 \(E_{Z|X;\theta_t}[L(\theta;X,Z)]\)。在第一节中我们提到 \( \arg\max_\theta L(\theta;X,Z) \) 在未知 \(Z\) 的情况下难以直接计算,于是 EM 算法就转而通过最大化它的期望 \(E_{Z|X;\theta_t}[L(\theta;X,Z)]\) 来逼近 \(\theta\) 的最优值,得到 \(\theta_{t+1}\) 。注意由于 \(L(\theta;X,Z)\) 的这个期望是在 \(Z\) 的一个分布上求的,这样得到的表达式就只剩下 \(\theta\) 一个未知量,因而绕过了 \(z\) 未知的问题。而 \(\theta_{t+1}\) 又可以作为下轮迭代的基础,继续向最优逼近。算法中 E-step 就是在利用 \(\theta_t\) 求期望 \(E_{Z|X;\theta_t}[L(\theta;X,Z)]\),这就是所谓“Expectation”;M-step 就是通过寻找 \(\theta_{t+1}\) 最大化这个期望来逼近 \( \theta \) 的最优值,这就叫“Maximization”。EM 算法因此得名。

另外,如果数据满足独立同分布的条件,分别枚举 \(z^{(i)}\) 的值可能要比枚举整个 \(Z\) 方便些,可把 \(E_{Z|X;\theta_t}[L(\theta;X,Z)]\) 替换成:
$$
\begin{eqnarray}
\sum_i E_{z^{(i)}|x^{(i)};\theta_t}[L(\theta;x^{(i)},z^{(i)}]
&:=& \sum_i E_{z^{(i)}|x^{(i)};\theta_t}[\log{P(x^{(i)},z^{(i)};\theta)}] \\
&=& \sum_i \sum_{z^{(i)}} P(z^{(i)}|x^{(i)};\theta_t) \log{P(x^{(i)},z^{(i)};\theta)}
\end{eqnarray}
$$

原理

为什么这样 E一步,M一步,一步E,一步M,就能逼近极大似然?具体而言,为什么通过迭代计算 \(\arg\max_\theta E_{Z|X;\theta_t}[L(\theta;X,Z)]\) 可以逼近 \(\theta\) 的最优值 \(\arg\max_\theta L(\theta;X,Z)\)?我们稍后会看到,这是因为每次迭代得到的 \(\theta_{t+1}\) 一定比 \(\theta_t\) 更优,即算法是在对 \(\theta\) 的最优值做单调逼近。

不过首先让我们先抛开最大似然,考虑一个更一般的问题。假设有一个凹函数 \( F(\theta) \),我们想求 \( \arg\max_\theta F(\theta) \),但直接求很困难。不过对于任意给定的 \( \theta_t \),假设我们都能找到 \( F(\theta) \) 的一个下界函数 \( G_{\theta_t}(\theta) \),满足 \( F(\theta) \geq G_{\theta_t}(\theta) \) 且 \( F(\theta_t) = G_{\theta_t}(\theta_t) \) ——我管 \( G_{\theta_t}(\theta) \) 这样的函数叫 \( F \) 的“在 \( \theta_t \) 处相等的下界函数”。现在考虑 \( \theta_{t+1} := \arg\max_\theta G_{\theta_t}(\theta) \),它一定会满足:
$$ F(\theta_{t+1}) \geq G_{\theta_t}(\theta_{t+1}) \geq G_{\theta_t}(\theta_t) = F(\theta_t) $$
也就是说,\( \theta_{t+1} \) 一定比 \( \theta_t \) 更优。而接下来我们又可以用 \( \theta_{t+1} \) 找到一个 \( G_{\theta_{t+1}}(\theta) \),再据此算出比 \( \theta_{t+1} \) 还优的 \( \theta_{t+2} := \arg\max_\theta G_{\theta_{t+1}}(\theta) \) 。如此不断迭代,就能不断步步逼近  \( \theta \) 的最优值了。由此可见,如果对任意 \( \theta_t \) 都能找到 \(F\) 的“在 \( \theta_t \) 处相等的下界函数”\( G_{\theta_t}(\theta) \),我们就得到了一个能单调逼近 \(\theta\) 的最优值的算法:

Repeat until converge {

  1. 找到函数 \( F(\theta) \) 的“在 \( \theta_t \) 处相等的下界函数” \( G_{\theta_t}(\theta) \)
  2. \( \theta_{t+1} := \arg\max_\theta G_{\theta_t}(\theta) \)

}

上面的算法看起来和 EM 算法的每步都分别对应——事实上也正如此。下面是从 PRML 中偷的一张图改的,展示了上述逼近的过程:

现在我们回到最大似然问题 \(\theta=\arg\max_\theta{L(\theta;X)}\) 。如果我们想套用上面的算法来逼近 \( \theta \) 的这个最优解,就需要找到对于每个 \( \theta_t \),函数 \( L(\theta;X) \) 的“在 \( \theta_t \) 处相等的下界函数”。该怎么找呢?让我们从 \( L(\theta) \) 的初始形式开始推导:

$$
\begin{eqnarray}
L(\theta) &=& \log{P(X;\theta)} \\
&=& \log{\sum_Z P(X,Z;\theta)}
\end{eqnarray}
$$

又卡在这个\(\log\)套\(\sum\)的形式上了……我们说过麻烦在于观察不到 \(Z\) 的值,那不妨给它任意引入一个概率分布 \(Q(Z)\) [3],利用分子分母同乘 \(Q(Z)\) 的小 trick,得到:

$$
\begin{eqnarray}
L(\theta) &=& \log{\sum_Z P(X,Z;\theta)} \\
&=& \log{\sum_Z Q(Z) \frac{P(X,Z;\theta)}{Q(Z)}} \\
&=& \log E_{Z \sim Q}\left[ \frac{P(X,Z;\theta)}{Q(Z)} \right]
\end{eqnarray}
$$

根据 Jensen 不等式 [4],对于任意分布 \(Q\) 都有:

$$
L(\theta) = \log E_{Z \sim Q}\left[ \frac{P(X,Z;\theta)}{Q(Z)} \right] \geq E_{Z \sim Q}\left[ \log\frac{P(X,Z;\theta)}{Q(Z)} \right]
$$

且上面的不等式在 \( \frac {P(X,Z;\theta)} {Q(Z)}\) 为常数时取等号。

于是我们就得到了 \( L(\theta;X) \) 的一个下界函数。我们要想套用上面的算法,还要让这个不等式在 \( \theta_t \) 处取等号,这就这要求在 \( \theta = \theta_t \) 时 \( \frac {P(X,Z;\theta)} {Q(Z)}\) 为常数,即 \(Q(Z) \propto P(X,Z;\theta_t)\)。由于 \(Q(Z)\) 是一个概率分布,必须满足 \(\sum_z Q_i(z) = 1\),所以这样的 \(Q(Z)\) 只能是 \(Q(Z) = \frac {P(X,Z;\theta_t)} {\sum_Z P(X,Z;\theta_t)} = P(Z|X;\theta_t)\)。那我们就把 \( Q(Z) = P(Z|X;\theta_t) \) 代入上式,得到:

$$
L(\theta) \geq E_{Z|X;\theta_t}\left[ \log\frac{P(X,Z;\theta)}{P(Z|X;\theta_t)} \right]
$$

且该不等式在 \( \theta = \theta_t \) 时取到等号。那么……\( E_{Z|X;\theta_t}\left[ \log\frac{P(X,Z;\theta)}{P(Z|X;\theta_t)} \right] \) 就是 \( L(\theta;X) \) 的“在 \( \theta_t \) 处相等的下界函数”——这不就是我们要找的么!于是把它塞进本节开始得到的算法里替换“\( G_{\theta_t}(\theta) \)”就可以用啦。也就是说,迭代计算 \( \arg\max_\theta E_{Z|X;\theta_t}\left[ \log\frac{P(X,Z;\theta)}{P(Z|X;\theta_t)} \right] \) 就可以逼近 \( \theta \) 的最优值了。而由于利用 Jensen 不等式的那一步搞掉了\(\log\)套\(\sum\)的形式,它算起来往往要比直接算 \( \arg\max_\theta{L(\theta;X)} \) 容易不少。

我们还可以再做几步推导,得到一个更简单的形式:

$$\begin{eqnarray}
\theta_{t+1}
&:=& \arg\max_\theta E_{Z|X;\theta_t}\left[ \log\frac{P(X,Z;\theta)}{P(Z|X;\theta_t)} \right] \\
&=& \arg\max_\theta \sum_{Z} P(Z|X;\theta_t) \log\frac{P(X,Z;\theta)}{P(Z|X;\theta_t)} \\
&=& \arg\max_\theta \sum_{Z} [P(Z|X;\theta_t)\log P(X,Z;\theta) - P(Z|X;\theta_t) \log P(Z|X;\theta_t)] \\
&=& \arg\max_\theta \sum_{Z} P(Z|X;\theta_t)\log P(X,Z;\theta) \\
&=& \arg\max_\theta E_{Z|X;\theta_t}[\log{P(X,Z;\theta)}]
\end{eqnarray}$$

其中倒数第二步是因为 \(- P(Z|X;\theta_t) \log P(Z|X;\theta_t)]\) 这一项与 \(\theta\) 无关,所以就直接扔掉了。这样就得到了本文第二节 EM 算法中的形式——它就是这么来的。

以上就是 EM 了。至于独立同分布的情况推导也类似。

顺带一提,\( \arg\max_\theta E_{Z|X;\theta_t}[\log{P(X,Z;\theta)}] \) 有时也比较难算。这时我们其实可以退而求其次,不要求这个期望最大化了,只要它在 \( \theta_{t+1} \) 处的值大于在 \( \theta_t \) 处的值就行了。根据上面的推导,这样也能逼近 \( \theta \) 的最优值,只是收敛速度较慢。这就是所谓 GEM (Generalized EM) 算法了。

p.s. MathJax 很神嘛。
p.p.s. 这篇笔记竟然断断续续写写改改了两天多,我对 EM 的认识也越来越清晰。“‘教’是最好的‘学’”真是一点没错。


更新历史:
2011.10.12 重写了“原理”部分,把利用函数的“在 \(\theta_t\) 处相等的下界函数”逼近 \(\theta\) 的最优值的算法单独提到前面说,这样似乎清楚很多。
2011.10.13 修正了对在利用 Jensen 不等式的那一步要取 \( Q(Z) = P(Z|X;\theta_t) \) 的解释


脚注:
  1. [1] 一般可以利用贝叶斯定理
    $$P(z|x;\theta) = \frac{P(x|z;\theta)P(z;\theta)}{\sum_z{P(x|z;\theta)P(z;\theta)}}$$ 而 \(P(x|z;\theta)\) 和 \(P(z;\theta)\) 往往是模型假设的一部分。
  2. [2] 实际上在某些特殊情况下,\(\theta\) 还可能收敛在局部最优点或鞍点上。这时可以多跑几次算法,每次随机不同的 \(\theta_0\),最后取最好的结果。为简明起见,本文忽略这种情况。
  3. [3] \(Q(Z)\) 为概率分布,意即需满足 \(\sum_Z Q(Z) = 1\) 且 \(Q(Z) \geq 0\)
  4. [4] Jensen 不等式:
    \(f\) 为凸函数,\(X\) 为随机变量。则 $$ E[f(X)] \geq f(EX) $$ 若 \(f\) 是严格凸的,则上式取等号当前仅当 \(X\) 为常数。

    在这里 \(\log\) 函数是严格凹的,所以要把上面的不等号方向调转。

[BHOJ10235] 窗口取数

http://upload.tomtung.com/img/sliding-window.jpg

窗口(超级版)

时间限制:5000 ms 内存限制:65535 KB

描述

有一串整数在排队……

有N个整数,你有一个可以框住M个连续段整数的木框,现在你想知道,对于这个队列中任意的连续M个整数,最大和最小的整数是哪个?

例:(M大小的窗口向右滑动)

1 2 3 2 1   M=2
1 2    MAX 2  MIN 1
2 3    MAX 3  MIN 2
3 2    MAX 3  MIN 2
2 1    MAX 2  MIN 1

最后需要输出的是两行:

1 2 2 1
2 3 3 2

输入

第一行包含一个整数T,表示有T组测试数据;
以下每组测试数据格式:
第一行包含2个整数N,M表示有N个整数在排队,取连续M个整数。
第二行包含N个整数。
其中N不大于1,000,000,M不大于N。

输出

按照题目描述格式输出结果,第一行为MIN,第二行为MAX。

样例输入

1
5 2
1 2 3 2 1

样例输出

1 2 2 1
2 3 3 2

提示

注意数据规模!

问题来源

软件学院07级数据结构第二次测试

题解

很久不考虑算法问题了的说……当然也好久不写解题报告了。上面这题让我体会到久违了的思考的乐趣(i.e.我废掉好久了>_<)

最显然的解法是利用平衡树始终保持木框内M个数的有序状态,大体是O(NlogM)的复杂度。这个不用多说。

问题是:能否找到O(N)的解法?在读下去之前你可以好好想想。

我纠结了好久,终于……也没想出来- – 尽管想出很多优化,终究还是不能达到要求。网上搜了下,看到 CS大牛csdn 上有人给出了一个解法(你也可以在读完全文后再回头看这一段):

是可以到o(n);

编程之美上有一个类似的问题:”队列中取最大值操作问题”;
实际上窗口移动就相当于对队列做了一次出队与入队操作,所以lz这道题可以套用该解法;

书上是使用两个栈来模拟队列,假设为分别A,B;
1)当入队的时候,push A;
2)当出队的时候,
a)若B非空,pop B,
b)若B为空,则先将A中的元素依次pop并push到B,再pop B;
这样,就使用两个栈达到了队列的功能;

同时,对于单个栈,由于pop,push都是在栈顶进行的,所以每个栈都可以方便地维护自己的最
大值与最小值在栈内的索引;
以最大值为例;
用max_idx保存栈内最大值的索引;
另使用一个跟栈的最大长度一样的数组idx,idx[i]表示栈的索引范围在0到i-1的元素中的最
大值的索引为idx[i];
1) 当push的时候,比较栈顶元素与栈的已保存的最大值,
a) 若栈顶元素大于已保存的最大值,那么idx[top]=max_idx,max_idx=top;
b) 若栈顶元素不大于已保存的最大值,那么idx[top]=max_idx(书上是idx[top]=-1);
2) 当pop的时候,max_idx=idx[top](书上是先比较top与max_idx,若top==max_idx,则
max_idx=idx[top]);

具体到这个题目;
可以使用两个长度为m的栈来模拟窗口;
1)将前m个元素依次push到栈A;
2)移动窗口就相当于分别进行pop B 与 push A操作;
3) 窗口的最大值==max[A.items[A.max_idx]),B.items[B.max_idx])];

由于每个元素最多进入两栈各一次;
所以总的复杂度是o(n)的;

这个解法的确可行,但描述仍不直观。为什么两个栈暧昧地眉来眼去一番,就在O(N)时间内把问题解决了呢?褪去实现上的种种细节,从更高的角度观察这个算法,它的思路是怎么样的呢?

和冬冬讨论了半天,似乎找到了一个比较靠近此算法根本动机的理解方向。如下:

一、简化

此问题中,框的左端和右端同时在向右移动。这时,要在任意时刻用O(1)的时间获得框内最小值(最大值同理,故仅以最小值为例)并不容易。但是,如果假设这个框是可伸缩的,将其一端固定,仅移动另外一端,任意时刻能否在O(1)的时间内获得最小值呢?

稍加思考,就发现要找到这样的算法是很容易的(事实上在我最初考虑时,就是试图在这样一个算法的基础上进行优化的)。

假如这个框是左端固定,右端不断向右移动的(称之为左定右动框)。对于框内的每个数,有一个对应指针指向框内它左边(包括自身)所有数中最小者。这样,框内最小值就是框内最右端元素对应的指针所指向的元素。如在下图中,最小元素就是右端7对应指针所指向的元素6。

http://upload.tomtung.com/img/bhoj-10235_1.png

min = 6

当框的右端向右扩展时,可以通过递推获得新元素对应指针应指向的元素。以上图为例。加入2时,由7对应的指针可知原框中最小值为6,而6>2,所以新元素2对应的指针应指向自己。

http://upload.tomtung.com/img/bhoj-10235_2.png

min = 2

加入8时,由2的指针知原框中最小值为2,而2<8,所以新元素8对应指针应指向2的指针所指向的元素2。

http://upload.tomtung.com/img/bhoj-10235_3.png

min = 2

其余如法炮制。

http://upload.tomtung.com/img/bhoj-10235_4.png

min = 2

可见,在框左端固定、右端不断扩展时,任意时刻都能在常数时间内确定框内元素的最小值。(不仅如此,即使需要让右端向左收缩,也能在常数时间内,通过最右端元素指针获得得到收缩后框内所剩元素的最小值。) 整个过程演示如下(先扩张后收缩):

http://upload.tomtung.com/img/bhoj-10235_5.gif

演示中的收缩操作在当前问题中并无必要。演示出来是为了说明无论右端如何移动,只要左端不动,就可以在任意时刻立刻得到最小值。同时也是为了与下面一个演示保持一致。

如果一个框右端固定,左端收缩(称之为右定左动框),也可以在任意时刻花费常数时间获得框内的最小元素。过程和上面左右对称。具体地说,每个元素对应一个指针,指向框内它右边(包括自身)所有数中最小者。首先需要从右到左计算出各个元素对应的指针。这个过程类似于将框的左端由右向左扩展。计算完毕后就可以在收缩左端的过程中在常数时间内获得最小元素了。

演示如下(先扩张后收缩,可以看到完全和上面对称):

http://upload.tomtung.com/img/bhoj-10235_6.gif

对于当前问题,在“扩张”过程中并不需要求最小值,而仅仅需要计算出指针来为收缩过程作准备。这里在全过程中标注出最小值是为了说明任意时刻都有能力得到它,也为了与上一个演示保持一致。

上面拉拉杂杂说了一堆,只为了说明:如果框的一端固定,仅移动另外一端,则在此过程中可以仅花费常数时间获得框内的最小元素。针对左定右移、右定左移两种情况的算法是彼此对称的。

最后顺带提一点实现细节,希望不会影响你对整体的理解。上面所使用的指针也可以用索引号来代替,这里使用指针是为了显得更形象。事实上,不保存索引或指针而直接保存最小的“值”也是可行的,但是并不推荐。设想,如果每个元素不是数而是四五米长的字符串(>_<),偏序关系使用字符串长度的比较,那么如果不保存指针/索引而保存值,所带来的元素复制开销恐怕不是你想要的。

二、推广

怎样把上面只允许一端移动的解法推广到同时允许两端移动呢?

一种想法是,将以上二者合二为一。具体地说,将框内的数看成左右两部分,左边一部分看成右定左动的,右边一部分看成左定右动的。这样,在左边收缩、右边扩张的过程中,左右两部分都可以在常数时间内得到最小元素。取两个最小元素中更小者,即为整个框中的最小元素。

OK,这就是O(N)算法的基本思路了。回到原题目,下面看一个例子。

假设我们要处理的是这么一串数:

3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3

框的大小为5。

首先,把最先被框住的5个数看成被一个右定左动的框框住,这5个数右边看成是一个空的左定右动框。当然,首先需要计算出开始这5个元素对应的指针:

http://upload.tomtung.com/img/bhoj-10235_7.gif

接下来,左边的右定左动框收缩,右边的左定右动框扩张。在此过程中,框在框中的M个元素的最小者可以在常数时间内获得。

http://upload.tomtung.com/img/bhoj-10235_8.gif

到左边部分收缩至空时就没有办法继续收缩了。怎么继续这个过程呢?

解决方法是,将右端含有M个元素的左定右动框重新处理为右定左动框,并在右边再放上一个空左定右动框:

http://upload.tomtung.com/img/bhoj-10235_9.gif

接下来继续这个过程就行了。下面是全过程的演示:

http://upload.tomtung.com/img/bhoj-10235_X.gif

你可能会对每次将右边的左定右动框重新处理为左边的右定左动框(有点晕- -)时计算指针造成的额外开销存有疑虑。然而,每次重新计算时需要处理M个元素的指针,每隔M个元素才会进行一次这样的处理,N/M*M仍然为N,并不会升高复杂度的阶。换句话说,每个元素至多被重新计算两次指针,所以总体复杂度仍然为O(N)。

以上就是整个解法。

三、关于栈

我们可以把右定左动框看成是一个底在右顶在左的栈,左端向左扩张看成是向栈push元素,左端向右收缩看成是栈在pop元素。左定右动框亦然。前面提到,两种框上的操作是对称的;如果把它们都看成栈,则push和pop时的操作完全一致。这样就将两种框对称的操纵统一在了同一个数据结构上,使得实现起来更为简洁。

这就是对开始所引用那段算法描述的解释。

四、问题的扩展

更进一步,我们还可以使用类似的方法处理此问题的变种。如,对于一个队列,随意进行入队出队操作(相当于木框的宽度不再固定为M,其两端也不同时向右移动),求在任意时刻队列中的最小元素。或者,一个双端队列,在两端随意进行入队出队操作,求任意时刻队列中的最小元素。等等。

最后顺便一提,在搜索此题资料时发现这个问题似乎涉及到数据流的处理算法。这个方面我一无所知,没办法站在那样一个高度阐述,抱歉。

标签云

豆瓣