条件随机场属于统计机器学习方法,所以需要概率论、随机过程等前置知识才能更好地理解它。我之前在数学专栏中总结了相关大学概率论的内容。下面我搜集了一些随机过程的内容。
二、前置知识
1. 随机过程
随机过程就是一族随机变量 X ( t ) , t ∈ T {X(t), t \in T} X(t),t∈T, 其中 t 是参数, 它属于某个指标集 T, T 称为参数集。一般,t表示时间。当T={0,1,2,…}是也称随机过程为随机序列。
随机过程在时刻 t 取的值称作是过程所处的状态, 状态的全体称为状态空间.
根据 T 及状态空间的不同我们可以对过程进行分类. 依照状态空间可分为连续状态和离散状态;依参数集 T, 当 T 为有限集或可数集则称之为离散参数过程, 否则称为连续参数过程。
2. 随机场(Random field)
在物理学和数学中,随机场是任意域上的随机函数(通常是多维空间,例如 R n {\displaystyle \mathbb {R} ^{n}} Rn)。根据现代定义,随机场是随机过程的概括,其中基础参数不再需要是实数或整数值“时间”,而是可以取多维向量或某个流形上的点的值。
这里一篇论文里面的定义
总的来讲就是说随机过程是关于时间t的一组随机变量(变量与变量之间有时序的关系),而随机场可以是关于多维空间的(也就是说随机变量和变量之间不仅仅只有时序的关系,比如下面的四个随机变量就既有x轴上的关系,也有y轴上的关系)。
比如,假设有四个随机变量, X 1 , X 2 , X 3 , 和 X 4 {\displaystyle X_{1}}, {\displaystyle X_{2}}, {\displaystyle X_{3}}, 和 {\displaystyle X_{4}} X1,X2,X3,和X4,分别位于 (0,0)、(0,2)、(2,2) 和 (2,0) 处的二维网格中。假设每个随机变量可以取值 -1 或 1,并且每个随机变量取值的概率取决于其直接相邻的邻居。这是离散随机场的一个简单示例。
随机场的例子有很多,但也许最容易看出来的是海平面的例子。事实上,这个例子很好地介绍了随机场理论的很多基本概念。海洋表面的高度显然是时间和空间的函数,因此我们通过将其表示为
f
(
t
,
x
)
,
t
∈
[
0
,
∞
)
≡
R
+
,
x
∈
R
2
f(t, x), \quad t \in[0, \infty) \equiv \mathbb{R}_{+}, \quad x \in \mathbb{R}^{2}
f(t,x),t∈[0,∞)≡R+,x∈R2
其实这里容易混淆的就是随机函数跟随机过程中随机变量的关系。以参数t为例,随机函数的输出为 X t X_t Xt,那为什么叫做随机场呢?就是说这个随机函数输出 X t X_t Xt不是固定的,而是随机的(准确来说是服从一个概率分布的),因此随机函数的输出本身是一个随机变量,当我们研究随机函数的多组输出的时候,这其实就恰好对应上面随机过程提到的一组随机变量。比如以海平面高度为例,即使参数(t,x)相同,就是说每年的某一天的某个地方的海平面高度随机的,而不是固定的。
3. 马尔可夫随机场
马尔可夫随机场就是满足马尔可夫性质的随机场。
马尔可夫性质:
可见,马尔可夫随机场的参数一般只有一个,即时间t,也就是说一般研究的都是马尔可夫随机过程。
4. 条件随机场
条件随机场 (condítional random field) 是给定随机变量X 条件下,随机变量Y的马尔可夫随机场。可以这样理解,条件随机场是条件分布和马尔可夫随机场的结合体。之前的马尔可夫随机场是没有限定条件的,现在是限定条件的。
下面我们将从自然语言处理的角度解释条件随机场。
二、自然语言处理中的条件随机场
原文地址:https://www.jianshu.com/p/55755fc649b1
这是我看的讲的易懂的一篇CRF文章,供大家学习,当然,这里需要先了解HMM模型,最好把李航老师的《统计机器学习》里面的HMM和CRF的相关内容先过一遍(没办法,追求真理的道路是曲折的):
理解条件随机场最好的办法就是用一个现实的例子来说明它。但是目前中文的条件随机场文章鲜有这样干的,可能写文章的人都是大牛,不屑于举例子吧。于是乎,我翻译了这篇文章。希望对其他伙伴有所帮助。
原文在这里[http://blog.echen.me/2012/01/03/introduction-to-conditional-random-fields/]
想直接看英文的朋友可以直接点进去了。我在翻译时并没有拘泥于原文,许多地方都加入了自己的理解,用学术点的话说就是意译。(画外音:装什么装,快点开始吧。)好的,下面开始翻译!
假设你有许多小明同学一天内不同时段的照片,从小明提裤子起床到脱裤子睡觉各个时间段都有(小明是照片控!)。现在的任务是对这些照片进行分类。比如有的照片是吃饭,那就给它打上吃饭的标签;有的照片是跑步时拍的,那就打上跑步的标签;有的照片是开会时拍的,那就打上开会的标签。问题来了,你准备怎么干?
一个简单直观的办法就是,不管这些照片之间的时间顺序,想办法训练出一个多元分类器。就是用一些打好标签的照片作为训练数据,训练出一个模型,直接根据照片的特征来分类。例如,如果照片是早上6:00拍的,且画面是黑暗的,那就给它打上睡觉的标签;如果照片上有车,那就给它打上开车的标签。
这样可行吗?乍一看可以!但实际上,由于我们忽略了这些照片之间的时间顺序这一重要信息,我们的分类器会有缺陷的。举个例子,假如有一张小明闭着嘴的照片,怎么分类?显然难以直接判断,需要参考闭嘴之前的照片,如果之前的照片显示小明在吃饭,那这个闭嘴的照片很可能是小明在咀嚼食物准备下咽,可以给它打上吃饭的标签;如果之前的照片显示小明在唱歌,那这个闭嘴的照片很可能是小明唱歌瞬间的抓拍,可以给它打上唱歌的标签。
所以,为了让我们的分类器能够有更好的表现,在为一张照片分类时,我们必须将与它相邻的照片的标签信息考虑进来。这——就是条件随机场(CRF)大显身手的地方!
1. 从例子说起——词性标注问题
啥是词性标注问题?
非常简单的,就是给一个句子中的每个单词注明词性。比如这句话:“Bob drank coffee at Starbucks”,注明每个单词的词性后是这样的:“Bob (名词) drank(动词) coffee(名词) at(介词) Starbucks(名词)”。
下面,就用条件随机场来解决这个问题。
以上面的话为例,有5个单词,我们将:(名词,动词,名词,介词,名词)作为一个标注序列,称为l,可选的标注序列有很多种,比如l还可以是这样:(名词,动词,动词,介词,名词),我们要在这么多的可选标注序列中,挑选出一个最靠谱的作为我们对这句话的标注。
怎么判断一个标注序列靠谱不靠谱呢?
就我们上面展示的两个标注序列来说,第二个显然不如第一个靠谱,因为它把第二、第三个单词都标注成了动词,动词后面接动词,这在一个句子中通常是说不通的。
假如我们给每一个标注序列打分,打分越高代表这个标注序列越靠谱,我们至少可以说,凡是标注中出现了动词后面还是动词的标注序列,要给它负分!!
上面所说的动词后面还是动词就是一个特征函数,我们可以定义一个特征函数集合,用这个特征函数集合来为一个标注序列打分,并据此选出最靠谱的标注序列。也就是说,每一个特征函数都可以用来为一个标注序列评分,把集合中所有特征函数对同一个标注序列的评分综合起来,就是这个标注序列最终的评分值。
2. 定义CRF中的特征函数
现在,我们正式地定义一下什么是CRF中的特征函数,所谓特征函数,就是这样的函数,它接受四个参数:
句子s(就是我们要标注词性的句子)
i,用来表示句子s中第i个单词
l
i
l_i
li,表示要评分的标注序列给第i个单词标注的词性
l
i
−
1
l_{i-1}
li−1,表示要评分的标注序列给第i-1个单词标注的词性
它的输出值是0或者1,0表示要评分的标注序列不符合这个特征,1表示要评分的标注序列符合这个特征。
Note:这里,我们的特征函数仅仅依靠当前单词的标签和它前面的单词的标签对标注序列进行评判,这样建立的CRF也叫作线性链CRF,这是CRF中的一种简单情况。为简单起见,本文中我们仅考虑线性链CRF。
3. 从特征函数到概率
定义好一组特征函数后,我们要给每个特征函数
f
j
f_j
fj赋予一个权重
λ
j
λ_j
λj。现在,只要有一个句子s,有一个标注序列l,我们就可以利用前面定义的特征函数集来对l评分。
score
(
l
∣
s
)
=
∑
j
=
1
m
∑
i
=
1
n
λ
j
f
j
(
s
,
i
,
l
i
,
l
i
−
1
)
\operatorname{score}(l \mid s)=\sum_{j=1}^{m} \sum_{i=1}^{n} \lambda_{j} f_{j}\left(s, i, l_{i}, l_{i-1}\right)
score(l∣s)=j=1∑mi=1∑nλjfj(s,i,li,li−1)
上式中有两个求和,外面的求和用来求每一个特征函数
f
j
f_j
fj评分值的和,里面的求和用来求句子中每个位置的单词的的特征值的和。
对这个分数进行指数化和标准化,我们就可以得到标注序列l的概率值p(l|s),如下所示:
p
(
l
∣
s
)
=
exp
[
score
(
l
∣
s
)
]
∑
l
′
exp
[
score
(
l
′
∣
s
)
]
=
exp
[
∑
j
=
1
m
∑
i
=
1
n
λ
j
f
j
(
s
,
i
,
l
i
,
l
i
−
1
)
]
∑
l
′
exp
[
∑
j
=
1
m
∑
i
=
1
n
λ
j
f
j
(
s
,
i
,
l
i
′
,
l
i
−
1
′
)
]
p(l \mid s)=\frac{\exp [\operatorname{score}(l \mid s)]}{\sum_{l^{\prime}} \exp \left[\operatorname{score}\left(l^{\prime} \mid s\right)\right]}=\frac{\exp \left[\sum_{j=1}^{m} \sum_{i=1}^{n} \lambda_{j} f_{j}\left(s, i, l_{i}, l_{i-1}\right)\right]}{\sum_{l^{\prime}} \exp \left[\sum_{j=1}^{m} \sum_{i=1}^{n} \lambda_{j} f_{j}\left(s, i, l_{i}^{\prime}, l_{i-1}^{\prime}\right)\right]}
p(l∣s)=∑l′exp[score(l′∣s)]exp[score(l∣s)]=∑l′exp[∑j=1m∑i=1nλjfj(s,i,li′,li−1′)]exp[∑j=1m∑i=1nλjfj(s,i,li,li−1)]
几个特征函数的例子
前面我们已经举过特征函数的例子,下面我们再看几个具体的例子,帮助增强大家的感性认识。
当
l
i
l_i
li是“副词”并且第i个单词以“ly”结尾时,我们就让
f
1
=
1
f_1 = 1
f1=1,其他情况
f
1
f_1
f1为0。不难想到,
f
1
f_1
f1特征函数的权重
λ
1
\lambda_1
λ1应当是正的。而且
λ
1
\lambda_1
λ1越大,表示我们越倾向于采用那些把以“ly”结尾的单词标注为“副词”的标注序列
f
1
(
s
,
i
,
l
i
,
l
i
−
1
)
=
1
f_{1}\left(s, i, l_{i}, l_{i-1}\right)=1
f1(s,i,li,li−1)=1
如果i=1,
l
i
l_i
li=动词,并且句子s是以“?”结尾时,
f
2
=
1
f_2=1
f2=1,其他情况
f
2
=
0
f_2=0
f2=0。同样,
λ
2
\lambda_2
λ2应当是正的,并且
λ
2
\lambda_2
λ2越大,表示我们越倾向于采用那些把问句的第一个单词标注为“动词”的标注序列。
f
2
(
s
,
i
,
l
i
,
l
i
−
1
)
=
1
f_{2}\left(s, i, l_{i}, l_{i-1}\right)=1
f2(s,i,li,li−1)=1
当
l
i
−
1
l_{i-1}
li−1是介词,
l
i
l_i
li是名词时,
f
3
f_3
f3 = 1,其他情况
f
3
=
0
f_3=0
f3=0。
λ
3
\lambda_3
λ3也应当是正的,并且
λ
3
\lambda_3
λ3越大,说明我们越认为介词后面应当跟一个名词。
f
3
(
s
,
i
,
l
i
,
l
i
−
1
)
=
1
f_{3}\left(s, i, l_{i}, l_{i-1}\right)=1
f3(s,i,li,li−1)=1
如果
l
i
l_i
li和
l
i
−
1
l_i-1
li−1都是介词,那么
f
4
f_4
f4等于1,其他情况
f
4
f_4
f4=0。这里,我们应当可以想到
λ
4
\lambda_4
λ4是负的,并且
λ
4
\lambda_4
λ4的绝对值越大,表示我们越不认可介词后面还是介词的标注序列。
f
4
(
s
,
i
,
l
i
,
l
i
−
1
)
=
1
f_{4}\left(s, i, l_{i}, l_{i-1}\right)=1
f4(s,i,li,li−1)=1
好了,一个条件随机场就这样建立起来了,让我们总结一下:
为了建一个条件随机场,我们首先要定义一个特征函数集,每个特征函数都以整个句子s,当前位置i,位置i和i-1的标签为输入。然后为每一个特征函数赋予一个权重,然后针对每一个标注序列l,对所有的特征函数加权求和,必要的话,可以把求和的值转化为一个概率值。
CRF与逻辑回归的比较
观察公式:
p
(
l
∣
s
)
=
exp
[
score
(
l
∣
s
)
]
∑
l
′
exp
[
score
(
l
′
∣
s
)
]
=
exp
[
∑
j
=
1
m
∑
i
=
1
n
λ
j
f
j
(
s
,
i
,
l
i
,
l
i
−
1
)
]
∑
l
′
exp
[
∑
j
=
1
m
∑
i
=
1
n
λ
j
f
j
(
s
,
i
,
l
i
′
,
l
i
−
1
′
)
]
p(l \mid s)=\frac{\exp [\operatorname{score}(l \mid s)]}{\sum_{l^{\prime}} \exp \left[\operatorname{score}\left(l^{\prime} \mid s\right)\right]}=\frac{\exp \left[\sum_{j=1}^{m} \sum_{i=1}^{n} \lambda_{j} f_{j}\left(s, i, l_{i}, l_{i-1}\right)\right]}{\sum_{l^{\prime}} \exp \left[\sum_{j=1}^{m} \sum_{i=1}^{n} \lambda_{j} f_{j}\left(s, i, l_{i}^{\prime}, l_{i-1}^{\prime}\right)\right]}
p(l∣s)=∑l′exp[score(l′∣s)]exp[score(l∣s)]=∑l′exp[∑j=1m∑i=1nλjfj(s,i,li′,li−1′)]exp[∑j=1m∑i=1nλjfj(s,i,li,li−1)]
是不是有点逻辑回归的味道?
事实上,条件随机场是逻辑回归的序列化版本。逻辑回归是用于分类的对数线性模型,条件随机场是用于序列化标注的对数线性模型。
CRF与HMM的比较
对于词性标注问题,HMM模型也可以解决。HMM的思路是用生成办法,就是说,在已知要标注的句子s的情况下,去判断生成标注序列l的概率,如下所示:
p
(
l
,
s
)
=
p
(
l
1
)
∏
i
p
(
l
i
∣
l
i
−
1
)
p
(
w
i
∣
l
i
)
p(l, s)=p\left(l_{1}\right) \prod_{i} p\left(l_{i} \mid l_{i-1}\right) p\left(w_{i} \mid l_{i}\right)
p(l,s)=p(l1)i∏p(li∣li−1)p(wi∣li)
这里:
p
(
l
i
∣
l
i
−
1
)
p(l_i|l_{i-1})
p(li∣li−1)是转移概率,比如,
l
i
−
1
l_i-1
li−1是介词,
l
i
l_i
li是名词,此时的p表示介词后面的词是名词的概率。
p
(
w
i
∣
l
i
)
p(w_i|l_i)
p(wi∣li)表示发射概率(emission probability),比如
l
i
l_i
li是名词,
w
i
w_i
wi是单词“ball”,此时的p表示在是名词的状态下,是单词“ball”的概率。
那么,HMM和CRF怎么比较呢?
答案是:CRF比HMM要强大的多,它可以解决所有HMM能够解决的问题,并且还可以解决许多HMM解决不了的问题。事实上,我们可以对
上面的HMM模型取对数,就变成下面这样:
log
p
(
l
,
s
)
=
log
p
(
l
0
)
+
∑
i
log
p
(
l
i
∣
l
i
−
1
)
+
∑
i
log
p
(
w
i
∣
l
i
)
\log p(l, s)=\log p\left(l_{0}\right)+\sum_{i} \log p\left(l_{i} \mid l_{i-1}\right)+\sum_{i} \log p\left(w_{i} \mid l_{i}\right)
logp(l,s)=logp(l0)+i∑logp(li∣li−1)+i∑logp(wi∣li)
我们把这个式子与CRF的式子进行比较:
score
(
l
∣
s
)
=
∑
j
=
1
m
∑
i
=
1
n
λ
j
f
j
(
s
,
i
,
l
i
,
l
i
−
1
)
\operatorname{score}(l \mid s)=\sum_{j=1}^{m} \sum_{i=1}^{n} \lambda_{j} f_{j}\left(s, i, l_{i}, l_{i-1}\right)
score(l∣s)=j=1∑mi=1∑nλjfj(s,i,li,li−1)
不难发现,如果我们把第一个HMM式子中的log形式的概率看做是第二个CRF式子中的特征函数的权重的话,我们会发现,CRF和HMM具有相同的形式。
换句话说,我们可以构造一个CRF,使它与HMM的对数形式相同。怎么构造呢?
对于HMM中的每一个转移概率
p
(
l
i
=
y
∣
l
i
−
1
=
x
)
p(l_i=y|l_{i-1}=x)
p(li=y∣li−1=x),我们可以定义这样的一个特征函数:
f
x
,
y
(
s
,
i
,
l
i
,
l
i
−
1
)
=
1
f_{x, y}\left(s, i, l_{i}, l_{i-1}\right)=1
fx,y(s,i,li,li−1)=1
该特征函数仅当
l
i
=
y
,
l
i
−
1
=
x
l_i = y,l_{i-1}=x
li=y,li−1=x时才等于1。这个特征函数的权重如下:
w
x
,
y
=
log
p
(
l
i
=
y
∣
l
i
−
1
=
x
)
w_{x, y}=\log p\left(l_{i}=y \mid l_{i-1}=x\right)
wx,y=logp(li=y∣li−1=x)
同样的,对于HMM中的每一个发射概率,我们也都可以定义相应的特征函数,并让该特征函数的权重等于HMM中的log形式的发射概率。
用这些形式的特征函数和相应的权重计算出来的
p
(
l
∣
s
)
p(l|s)
p(l∣s)和对数形式的HMM模型几乎是一样的!
用一句话来说明HMM和CRF的关系就是这样:
每一个HMM模型都等价于某个CRF!
每一个HMM模型都等价于某个CRF!
每一个HMM模型都等价于某个CRF!
但是,CRF要比HMM更加强大,原因主要有两点:
CRF可以定义数量更多,种类更丰富的特征函数。HMM模型具有天然具有局部性,就是说,在HMM模型中,当前的单词只依赖于当前的标签,当前的标签只依赖于前一个标签。这样的局部性限制了HMM只能定义相应类型的特征函数,我们在上面也看到了。但是CRF却可以着眼于整个句子s定义更具有全局性的特征函数,如这个特征函数:
f
2
(
s
,
i
,
l
i
,
l
i
−
1
)
=
1
f_{2}\left(s, i, l_{i}, l_{i-1}\right)=1
f2(s,i,li,li−1)=1
如果
i
=
1
,
l
i
=
动
词
i=1,l_i=动词
i=1,li=动词,并且句子s是以“?”结尾时,
f
2
=
1
f_2=1
f2=1,其他情况
f
2
=
0
f_2=0
f2=0。
CRF可以使用任意的权重将对数HMM模型看做CRF时,特征函数的权重由于是log形式的概率,所以都是小于等于0的,而且概率还要满足相应的限制,如
0
<
=
p
(
w
i
∣
l
i
)
<
=
1
,
∑
w
p
(
w
i
=
w
∣
l
1
)
=
1
0<=p\left(w_{i} \mid l_{i}\right)<=1, \sum_{w} p\left(w_{i}=w \mid l_{1}\right)=1
0<=p(wi∣li)<=1,w∑p(wi=w∣l1)=1
但在CRF中,每个特征函数的权重可以是任意值,没有这些限制。
值得注意的是:
尽管milter指出CRF可以定义数量更多,种类更丰富的特征函数。但根据笔者对标准CRF的代码实现的观察,CRF跟HMM一样,其实也只有两种类型的特征,即转移特征和状态特征,其定义如下所示
三、条件随机场的参数化形式
经过一中的例子,相信大家可以很好地理解在序列标注中条件随机场它的思想和建模方式。下面简单介绍下它的参数化形式。
一般在自然语言处理领域的条件随机场(CRF)都是线性链条件随机场(Linear chain CRF),线性链条件随机场满足一阶马尔可夫性,定义见下。
在词性标注问题中,观测变量(X,这里观测变量即为句子中的词)和隐变量Y(或称状态变量,这里为词性),这两个变量是一一对应的,我们称他们拥有相同的图结构(如图11.5所示),可以以下示例图表示。
这里为了更一般化的表示,我们使用X表示观测变量,x表示其某一个具体取值,Y表示标记序列变量,y表示其某一个具体取值。即用X代替第一节使用的符号s,使用Y代替第一节使用的符号l。
1.转移特征
我们定义 t k ( y i − 1 , y i , x , i ) t_k(y_{i-1},y_i,x,i) tk(yi−1,yi,x,i)为第k个转移特征,它表示在i-1位置上的某个标注转移到i位置上的另一个标注。举例,在某个正确的词性标注中, y i − 1 = N ( 名 词 ) , y i = V ( 动 词 ) y_{i-1}=N(名词),y_i=V(动词) yi−1=N(名词),yi=V(动词)。容易知道,如果序列标注中共有m中标注(e.g. m种词性),那么位置i上的转移特征的可能取值会有 m 2 m^2 m2种转移特征。
2.状态特征
定义 s l ( y i , x , i ) s_l(y_i,x,i) sl(yi,x,i)为第l个状态特征,他表示在i位置上序列标注 y i y_i yi为某个标注,如名词。容易知道,如果序列标注中共有m中标注(e.g. m种词性),那么那么位置i上的转移特征的可能取值会有 m m m种状态特征。
3. 参数化形式
设P(Y|X)为线性链条件随机场,则在随机变量X的取值为x的条件西,随机变量Y取值为y的条件概率具有以下形式:
P
(
y
∣
x
)
=
1
Z
(
x
)
exp
(
∑
i
,
k
λ
k
t
k
(
y
i
−
1
,
y
i
,
x
,
i
)
+
∑
i
,
I
μ
l
s
l
(
y
i
,
x
,
i
)
)
P(y \mid x)=\frac{1}{Z(x)} \exp \left(\sum_{i, k} \lambda_{k} t_{k}\left(y_{i-1}, y_{i}, x, i\right)+\sum_{i, I} \mu_{l} s_{l}\left(y_{i}, x, i\right)\right)
P(y∣x)=Z(x)1exp⎝⎛i,k∑λktk(yi−1,yi,x,i)+i,I∑μlsl(yi,x,i)⎠⎞
其中Z(x)是规范化因子,它为
Z
(
x
)
=
∑
y
exp
(
∑
i
,
k
λ
k
t
k
(
y
i
−
1
,
y
i
,
x
,
i
)
+
∑
i
,
l
μ
l
s
l
(
y
i
,
x
,
i
)
)
Z(x)=\sum_{y} \exp \left(\sum_{i, k} \lambda_{k} t_{k}\left(y_{i-1}, y_{i}, x, i\right)+\sum_{i, l} \mu_{l} s_{l}\left(y_{i}, x, i\right)\right)
Z(x)=y∑exp⎝⎛i,k∑λktk(yi−1,yi,x,i)+i,l∑μlsl(yi,x,i)⎠⎞
在上面的式子中, λ k \lambda_k λk和 u l u_l ul是两种特征函数对应的权值。 t s t_s ts和 s l s_l sl特征函数,通常取值为1或0,当满足特征条件时取值为1,否则为0。
条件随机场完全由特征函数的权值 λ k \lambda_k λk和 u l u_l ul觉得,而且它跟逻辑回归一样,是对数线性模型;跟逻辑回归一样,它也是一种判别式模型。
进一步,如果我们不区分转移特征和状态特征,统一用 f k f_k fk表示,那么条件随机场可以表示为
P
(
y
∣
x
)
=
1
Z
(
x
)
exp
∑
k
=
1
K
w
k
f
k
(
y
,
x
)
Z
(
x
)
=
∑
y
exp
∑
k
=
1
K
w
k
f
k
(
y
,
x
)
\begin{aligned} P(y \mid x) &=\frac{1}{Z(x)} \exp \sum_{k=1}^{K} w_{k} f_{k}(y, x) \\ Z(x) &=\sum_{y} \exp \sum_{k=1}^{K} w_{k} f_{k}(y, x) \end{aligned}
P(y∣x)Z(x)=Z(x)1expk=1∑Kwkfk(y,x)=y∑expk=1∑Kwkfk(y,x)
其中,
f
k
(
y
,
x
)
=
∑
i
=
1
n
f
k
(
y
i
−
1
,
y
i
,
x
,
i
)
,
k
=
1
,
2
,
⋯
,
K
f_{k}(y, x)=\sum_{i=1}^{n} f_{k}\left(y_{i-1}, y_{i}, x, i\right), \quad k=1,2, \cdots, K
fk(y,x)=i=1∑nfk(yi−1,yi,x,i),k=1,2,⋯,K
这是因为式表示注意到条件随机场同一特征在各个位置都有定义,可以对同个特征在各个位置求和 将局部特征函数转化个全局特征函数,这样就可以将矗件随机场写成权值向量和特征向量的内积形式 即条件随机场的简化形式
三、条件随机场的求解
对于一个句子x,我们希望找到得分最高的词性标注,即
y
^
=
arg max
y
p
(
y
∣
x
)
=
arg max
y
exp
∑
k
=
1
K
w
k
f
k
(
y
,
x
)
∑
y
exp
∑
k
=
1
K
w
k
f
k
(
y
,
x
)
=
arg max
y
exp
∑
k
=
1
K
w
k
f
k
(
y
,
x
)
\hat y = \argmax_y p(y|x)=\argmax_y \dfrac{\exp \sum_{k=1}^{K} w_{k} f_{k}(y, x)}{\sum_{y} \exp \sum_{k=1}^{K} w_{k} f_{k}(y, x)}=\argmax_y {\exp \sum_{k=1}^{K} w_{k} f_{k}(y, x)}
y^=yargmaxp(y∣x)=yargmax∑yexp∑k=1Kwkfk(y,x)exp∑k=1Kwkfk(y,x)=yargmaxexpk=1∑Kwkfk(y,x)
其中
f
k
(
Y
,
x
)
f_k(Y,x)
fk(Y,x)表示第k个特征,
w
k
w_k
wk对应它的权值。这需要我们计算Y的每一个取值,并取概率最大者作为输出。因此,这引出了CRF关键问题——预测问题(也称解码问题)。
另外,我们需要根据训练数据学习最佳的特征权重w,这称为学习问题。
所以跟HMM一样,CRF也有三个基本问题(过程),即概率计算问题,学习问题,和解码问题。(建议先回顾下HMM的三个问题,推荐查看李航老师的《统计机器学习》)。
1. 条件随机场的概率计算问题
概率计算问题目的是用更高效的算法计算概率,通常可用于学习问题的求解。
这里先引入条件随机场的矩阵形式,我们使用一个矩阵 M i ( x ) M_i(x) Mi(x)表示对于观测序列x的第i个位置的一个m阶矩阵,矩阵的每一个元素 a j k a_{jk} ajk表示 y i − 1 , y i y_{i-1},y_{i} yi−1,yi分别取所有可能取值中(共有m个)第j个和第k个取值的概率(得分)。
M i ( x ) = [ M i ( y i − 1 , y i ∣ x ) ] M i ( y i − 1 , y i ∣ x ) = exp ( W i ( y i − 1 , y i ∣ x ) ) W i ( y i − 1 , y i ∣ x ) = ∑ k = 1 K w k f k ( y i − 1 , y i , x , i ) \begin{gathered} M_{i}(x)=\left[M_{i}\left(y_{i-1}, y_{i} \mid x\right)\right] \\ M_{i}\left(y_{i-1}, y_{i} \mid x\right)=\exp \left(W_{i}\left(y_{i-1}, y_{i} \mid x\right)\right) \\ W_{i}\left(y_{i-1}, y_{i} \mid x\right)=\sum_{k=1}^{K} w_{k} f_{k}\left(y_{i-1}, y_{i}, x, i\right) \end{gathered} Mi(x)=[Mi(yi−1,yi∣x)]Mi(yi−1,yi∣x)=exp(Wi(yi−1,yi∣x))Wi(yi−1,yi∣x)=k=1∑Kwkfk(yi−1,yi,x,i)
因此,对于某一个标记序列y,它的概率可以通过n+1个矩阵的乘积来表示,即
P
w
(
y
∣
x
)
=
1
Z
w
(
x
)
∏
i
=
1
n
+
1
M
i
(
y
i
−
1
,
y
i
∣
x
)
P_{w}(y \mid x)=\frac{1}{Z_{w}(x)} \prod_{i=1}^{n+1} M_{i}\left(y_{i-1}, y_{i} \mid x\right)
Pw(y∣x)=Zw(x)1i=1∏n+1Mi(yi−1,yi∣x)
Z
w
(
x
)
Z_w(x)
Zw(x)是规范化因子,是以start为起点,stop为终点的所有路径
y
1
y
2
.
.
.
y
n
y_1y_2...y_n
y1y2...yn的非规范化概率之和。
前向-后向算法
条件随机场的概率计算问题是指
给定输入序列x和输出序列y,计算条件概率
P
(
Y
i
=
y
i
∣
x
)
,
P
(
Y
i
−
1
=
y
i
−
1
,
Y
i
=
y
i
∣
x
)
P\left(Y_{i}=y_{i} \mid x\right), P\left(Y_{i-1}=y_{i-1}, Y_{i}=y_{i} \mid x\right)
P(Yi=yi∣x),P(Yi−1=yi−1,Yi=yi∣x)和它们相应的数学期望、规范化因子Z(x)的问题
这跟HMM的概率计算由很大的不同
但是它们都可以通过前向后向算法来高效地进行计算(该算法利用了动态规划的思想,因为欲求 P ( Y i = y i ∣ x ) P\left(Y_{i}=y_{i} \mid x\right) P(Yi=yi∣x), 那么根据全概率公式,它需要枚举除了位置i之外所有标注的可能取值情况并求和,而这里面有很多情况的部分标注是相同的,因此就可以利用先保存局部结果来高效计算的算法——前向后向算法)。
首先
摘取自李航《统计机器学习》
然后就有
P
(
Y
i
=
y
i
∣
x
)
=
α
i
T
(
y
i
∣
x
)
β
i
(
y
i
∣
x
)
Z
(
x
)
P
(
Y
i
−
1
=
y
i
−
1
,
Y
i
=
y
i
∣
x
)
=
α
i
−
1
T
(
y
i
−
1
∣
x
)
M
i
(
y
i
−
1
,
y
i
∣
x
)
β
i
(
y
i
∣
x
)
Z
(
x
)
\begin{gathered} P\left(Y_{i}=y_{i} \mid x\right)=\frac{\alpha_{i}^{\mathrm{T}}\left(y_{i} \mid x\right) \beta_{i}\left(y_{i} \mid x\right)}{Z(x)} \\ P\left(Y_{i-1}=y_{i-1}, Y_{i}=y_{i} \mid x\right)=\frac{\alpha_{i-1}^{\mathrm{T}}\left(y_{i-1} \mid x\right) M_{i}\left(y_{i-1}, y_{i} \mid x\right) \beta_{i}\left(y_{i} \mid x\right)}{Z(x)} \end{gathered}
P(Yi=yi∣x)=Z(x)αiT(yi∣x)βi(yi∣x)P(Yi−1=yi−1,Yi=yi∣x)=Z(x)αi−1T(yi−1∣x)Mi(yi−1,yi∣x)βi(yi∣x)
从笔者对BERT-CRF代码和书本的研读中,我发现真正可以用于学习问题的概率计算其实只有规范化因子Z(x)的计算
,而对于
P
(
Y
i
=
y
i
∣
x
)
,
P
(
Y
i
−
1
=
y
i
−
1
,
Y
i
=
y
i
∣
x
)
P\left(Y_{i}=y_{i} \mid x\right), P\left(Y_{i-1}=y_{i-1}, Y_{i}=y_{i} \mid x\right)
P(Yi=yi∣x),P(Yi−1=yi−1,Yi=yi∣x)的计算,笔者并没有发现它们在后面两个问题中的应用(若发现,欢迎批评指正)。
1. 条件随机场的学习问题
学习问题其实就是给定训练数据集估计条件随机场模型参数 w w w的问题,.具体的优化实现算法有改进的迭代尺度法 IIS 、梯度下降法以及拟牛顿法。
在深度学习时代,我们通常使用梯度下降法来寻得最优的w。下面将结合CRF的代码实现进行介绍(如没有接触过NER及相关深度学习内容的同学可参考跳过这部分,查看李航老师的《统计机器学习》对该部分的介绍,或者先学习对应的先验知识)。
使用梯度下降法优化模型参数w的具体做法就是前向传播+反向传播。以下代码实现为hanlp对CRF的开源实现。
1. 前向传播
前向传播的任务是,给定CRF模型,计算该模型预测真实标注y的概率 P ( y ∣ x ) P(y|x) P(y∣x), y y y是真实序列。
该模型的目标函数为
m
i
n
−
l
o
g
P
(
y
∣
x
)
min -log P(y|x)
min−logP(y∣x)
− l o g P ( y ∣ x ) -log P(y|x) −logP(y∣x)其实就是前向传播所得到的最终结果并返回,下面看看它的具体计算:
# 首先是初始化函数(构造函数),定义类的属性
class CRF(nn.Module):
def __init__(self, num_tags: int, batch_first: bool = False) -> None:
if num_tags <= 0:
raise ValueError(f'invalid number of tags: {num_tags}')
super().__init__()
# num tags就是标注种类的 数量
self.num_tags = num_tags
# batch_first只对输入数据格式,不影响原理理解
self.batch_first = batch_first
# start_transitions 标注序列均以<start>符号开始,这个转移矩阵表示<start>符号转移到
# num_tags 种标注的概率(是一种学习的权重,可看成与<start>有关的转移特征的权值)
self.start_transitions = nn.Parameter(torch.empty(num_tags))
# 同样的end_transitions表示num_tags 种标注转移到<end>符号的概率,
# 可看成与<end>有关的转移特征的权值
self.end_transitions = nn.Parameter(torch.empty(num_tags))
# 同样,transitions表示n种标注两两转换的的概率,转移特征的权值
self.transitions = nn.Parameter(torch.empty(num_tags, num_tags))
self.reset_parameters()
这里定义了CRF的转移特征的权值,那状态特征的权值呢?它其实就是forward方法(见下)中的emissions(又称发射概率),是有外部传入CRF的,它的计算如下(注意这个计算并不属于CRF类):
class BertCrfForNer(BertPreTrainedModel):
def __init__(self, config):
super(BertCrfForNer, self).__init__(config)
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
self.crf = CRF(num_tags=config.num_labels, batch_first=True)
self.init_weights()
def forward(self, input_ids, token_type_ids=None, attention_mask=None,labels=None):
#1. 用编码器(比如LSTM,BERT,或者是word2vec)来得到输入句子x的编码表示(一个向量,设维度为m)
outputs =self.bert(input_ids = input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids)
sequence_output = outputs[0]
sequence_output = self.dropout(sequence_output)
#2. 然后将向量输入到一个线性层即可得到每个字所有可能的标注的概率(m->n,n表示所有可能标注的数量)
logits = self.classifier(sequence_output)
outputs = (logits,)
# 3.接着将该概率logits视为CRF的对应状态特征的权重又称发射概率),输入到CRF中
if labels is not None:#对应训练阶段
loss = self.crf(emissions = logits, tags=labels, mask=attention_mask)
outputs =(-1*loss,)+outputs
else:#对应预测阶段
outputs = self.crf.decode(emissions = logits)
return outputs # (loss), scores
当外部函数将emissions赋予CRF之后,它就可以计算 − l o g P ( y ∣ x ) -log P(y|x) −logP(y∣x)了,它通过以下的forward函数进行计算。
class CRF(nn.Module):
def forward(self, emissions: torch.Tensor,
tags: torch.LongTensor,
mask: Optional[torch.ByteTensor] = None,
reduction: str = 'mean') -> torch.Tensor:
# 这个函数其实就是前向传播
... 此处省略部分内容
# shape: (batch_size,)
numerator = self._compute_score(emissions, tags, mask)#解释及实现见下
# shape: (batch_size,)
denominator = self._compute_normalizer(emissions, mask)#解释及实现见下
# shape: (batch_size,)
llh = numerator - denominator
...
return llh.sum() / mask.float().sum()
回忆 P ( y ∣ x ) = 1 Z ( x ) exp ∑ k = 1 K w k f k ( y , x ) P(y \mid x) =\frac{1}{Z(x)} \exp \sum_{k=1}^{K} w_{k} f_{k}(y, x) P(y∣x)=Z(x)1exp∑k=1Kwkfk(y,x),其中_compute_score函数计算的是 − ∑ k = 1 K w k f k ( y , x ) -\sum_{k=1}^{K} w_{k} f_{k}(y, x) −∑k=1Kwkfk(y,x)(取负log),而_compute_normalizer函数计算的就是 − l o g Z ( x ) -logZ(x) −logZ(x), 最终把两者相减得到的llh就是 − l o g P ( y ∣ x ) -log P(y|x) −logP(y∣x)。
_compute_score函数和_compute_normalizer函数的实现见下,其中_compute_normalizer函数中用到的就是前面介绍的前向后向算法。
def _compute_score(self, emissions: torch.Tensor,
tags: torch.LongTensor,
mask: torch.ByteTensor) -> torch.Tensor:
# emissions: (seq_length, batch_size, num_tags)
# tags: (seq_length, batch_size)
# mask: (seq_length, batch_size)
seq_length, batch_size = tags.shape
mask = mask.float()
# Start transition score and first emission
# shape: (batch_size,)
score = self.start_transitions[tags[0]]
score += emissions[0, torch.arange(batch_size), tags[0]]
for i in range(1, seq_length):
# Transition score to next tag, only added if next timestep is valid (mask == 1)
# shape: (batch_size,)
score += self.transitions[tags[i - 1], tags[i]] * mask[i]
# Emission score for next tag, only added if next timestep is valid (mask == 1)
# shape: (batch_size,)
score += emissions[i, torch.arange(batch_size), tags[i]] * mask[i]
# End transition score
# shape: (batch_size,)
seq_ends = mask.long().sum(dim=0) - 1
# shape: (batch_size,)
last_tags = tags[seq_ends, torch.arange(batch_size)]
# shape: (batch_size,)
score += self.end_transitions[last_tags]
return score
def _compute_normalizer(self, emissions: torch.Tensor,
mask: torch.ByteTensor) -> torch.Tensor:
# emissions: (seq_length, batch_size, num_tags)
# mask: (seq_length, batch_size)
seq_length = emissions.size(0)
# Start transition score and first emission; score has size of
# (batch_size, num_tags) where for each batch, the j-th column stores
# the score that the first timestep has tag j
# shape: (batch_size, num_tags)
score = self.start_transitions + emissions[0]
for i in range(1, seq_length):
# Broadcast score for every possible next tag
# shape: (batch_size, num_tags, 1)
broadcast_score = score.unsqueeze(2)
# Broadcast emission score for every possible current tag
# shape: (batch_size, 1, num_tags)
broadcast_emissions = emissions[i].unsqueeze(1)
# Compute the score tensor of size (batch_size, num_tags, num_tags) where
# for each sample, entry at row i and column j stores the sum of scores of all
# possible tag sequences so far that end with transitioning from tag i to tag j
# and emitting
# shape: (batch_size, num_tags, num_tags)
next_score = broadcast_score + self.transitions + broadcast_emissions
# Sum over all possible current tags, but we're in score space, so a sum
# becomes a log-sum-exp: for each sample, entry i stores the sum of scores of
# all possible tag sequences so far, that end in tag i
# shape: (batch_size, num_tags)
next_score = torch.logsumexp(next_score, dim=1)
# Set score to the next score if this timestep is valid (mask == 1)
# shape: (batch_size, num_tags)
score = torch.where(mask[i].unsqueeze(1), next_score, score)
# End transition score
# shape: (batch_size, num_tags)
score += self.end_transitions
# Sum (log-sum-exp) over all possible tags
# shape: (batch_size,)
return torch.logsumexp(score, dim=1)
最后,我们利用梯度下降法联合优化BERT的参数和CRF的参数(具体推导过于复杂,目前的深度学习框架可自动实现,略),个人认为只要熟悉简单的梯度下降就可以很好的理解复杂网络的梯度下降大致是怎样的了。
1. 条件随机场的解码问题(也称预测问题)
条件随机场的预测问题是给定条件随机场 P(Y|X) 和输入序列〈观测序列) X , 求条件概率最大的输出序列(标记序列) 即对观测序列进行标注,条件随机场的预测算法是著名的维特比算法
在上面BertCrfForNer的类中的一行代码就调用了CRF的解码函数。
outputs = self.crf.decode(emissions = logits)
在解码时,我们希望求得概率P(y|x)最大的y, 如下
下图词性标注的例子展示了维特比解码的过程
维特比解码和贪心解码的区别
1)贪心解码只会选择转移概率最大的单步路径,而维特比解码会选择当前步以前和当前步的联合概率最大的路径
2)比如在第二步,CRF会计算第一步取各个状态的概率+转移到要/v的概率的最大者最为要/v单元格的该v。
3)维特比事实上也是一种动态规划,据保存前面可能的路径的概率以加快计算性能,而且它是全局最优的。
代码实现
def _viterbi_decode(self, emissions: torch.FloatTensor,
mask: torch.ByteTensor,
pad_tag: Optional[int] = None) -> List[List[int]]:
# emissions: (seq_length, batch_size, num_tags)
# mask: (seq_length, batch_size)
# return: (batch_size, seq_length)
if pad_tag is None:
pad_tag = 0
device = emissions.device
seq_length, batch_size = mask.shape
# Start transition and first emission
# shape: (batch_size, num_tags)
score = self.start_transitions + emissions[0]
history_idx = torch.zeros((seq_length, batch_size, self.num_tags),
dtype=torch.long, device=device)
oor_idx = torch.zeros((batch_size, self.num_tags),
dtype=torch.long, device=device)
oor_tag = torch.full((seq_length, batch_size), pad_tag,
dtype=torch.long, device=device)
# - score is a tensor of size (batch_size, num_tags) where for every batch,
# value at column j stores the score of the best tag sequence so far that ends
# with tag j
# - history_idx saves where the best tags candidate transitioned from; this is used
# when we trace back the best tag sequence
# - oor_idx saves the best tags candidate transitioned from at the positions
# where mask is 0, i.e. out of range (oor)
# Viterbi algorithm recursive case: we compute the score of the best tag sequence
# for every possible next tag
for i in range(1, seq_length):
# Broadcast viterbi score for every possible next tag
# shape: (batch_size, num_tags, 1)
broadcast_score = score.unsqueeze(2)
# Broadcast emission score for every possible current tag
# shape: (batch_size, 1, num_tags)
broadcast_emission = emissions[i].unsqueeze(1)
# Compute the score tensor of size (batch_size, num_tags, num_tags) where
# for each sample, entry at row i and column j stores the score of the best
# tag sequence so far that ends with transitioning from tag i to tag j and emitting
# shape: (batch_size, num_tags, num_tags)
next_score = broadcast_score + self.transitions + broadcast_emission
# Find the maximum score over all possible current tag
# shape: (batch_size, num_tags)
next_score, indices = next_score.max(dim=1)
# Set score to the next score if this timestep is valid (mask == 1)
# and save the index that produces the next score
# shape: (batch_size, num_tags)
score = torch.where(mask[i].unsqueeze(-1), next_score, score)
indices = torch.where(mask[i].unsqueeze(-1), indices, oor_idx)
history_idx[i - 1] = indices
# End transition score
# shape: (batch_size, num_tags)
end_score = score + self.end_transitions
_, end_tag = end_score.max(dim=1)
# shape: (batch_size,)
seq_ends = mask.long().sum(dim=0) - 1
# insert the best tag at each sequence end (last position with mask == 1)
history_idx = history_idx.transpose(1, 0).contiguous()
history_idx.scatter_(1, seq_ends.view(-1, 1, 1).expand(-1, 1, self.num_tags),
end_tag.view(-1, 1, 1).expand(-1, 1, self.num_tags))
history_idx = history_idx.transpose(1, 0).contiguous()
# The most probable path for each sequence
best_tags_arr = torch.zeros((seq_length, batch_size),
dtype=torch.long, device=device)
best_tags = torch.zeros(batch_size, 1, dtype=torch.long, device=device)
for idx in range(seq_length - 1, -1, -1):
best_tags = torch.gather(history_idx[idx], 1, best_tags)
best_tags_arr[idx] = best_tags.data.view(batch_size)
return torch.where(mask, best_tags_arr, oor_tag).transpose(0, 1)
参考资料
【1】https://www.jianshu.com/p/55755fc649b1
【2】李航《统计机器学习》
【3】hanlp. https://hanlp.hankcs.com/docs/index.html