此文为学习何晗老师《自然语言处理入门》笔记
由于词语级别的模型天然缺乏OOV召回能力,我们需要更细粒度的模型。
比词语更细的颗粒就是字符,如果字符级模型能够掌握汉字组词的规律,那么它就能够由字构词、动态的识别新词汇,而不局限于词典。
序列标注问题
序列标注(tagging)指的是给定一个序列,找出序列中每个元素对应的标签的问题。
其中是所有可能的取值集合,称为标注集*(tagset)。
最简单的奇偶标注
求解序列标注问题的模型一般称为序列标注器(tagger)。
通常模型从一个标注数据集中学习相关知识后再进行预测。
在NLP问题中,通常是字符或词语,而则是待预测的组词角色或词性等标签。
序列标注与中文分词
比如下面,{B,M,E,S}为最流行的标注集,其中:
- B: Begin 词首
- E: End 词尾
- M: Middle 词中
- S: Single 单字成词
标注后,按照最近的两个BE标签对应区间合并为一个词,S标签对应单字词语,按顺序输出即为分词过程。
序列标注与词性标注
序列标注与命名实体识别
所谓命名实体就是现实存在的实体,比如人名、地名和机构名。命名实体是OOV的主要组成部分,往往也是最令人关注的成分。
隐马尔科夫模型
隐马尔科夫模型(Hidden Markov Model, HMM)是描述两个时序序列联合分布的概率模型:
- 观测序列(Observation Sequence):序列外界可见
- 状态序列(State Sequence):序列外界不可见
从马尔科夫假设到隐马尔科夫模型
- 马尔科夫假设:每个事件发生的概率只取决于前一个事件
- 马尔科夫链:将满足该假设的连续多个事件串联在一起构成
在NLP语境下,可以将事件具象为单词,于是马尔科夫模型就具象为二元语法模型。
根据联合概率分布特性
假定先有状态,后有观测,取决于两个序列的可见与否。
状态与观测之间的依赖关系确定以后,隐马尔科夫模型利用三个要素来模拟时序序列的发生过程:
- 初始状态概率向量(下图中)
- 状态转移概率矩阵(下图中)
- 发射概率矩阵(也称观测概率矩阵,下图中)
初始概率向量、状态转移概率矩阵和发射概率矩阵被称为隐马尔科夫模型的三元组:
?\lambda =(\pi, A, B)?
初始状态概率向量
系统启动时进入的第一状态称为初始状态。
假设有种取值,即,那么就是一个独立的离散型随机变量,由描述。 其中初始概率向量:
比如中文分词问题采用标注集时:
则
状态转移概率矩阵
根据马尔科夫假设,时的状态取决于时的状态。在种状态下,从状态转移到状态的概率就构成了一个的状态转移概率矩阵: 其中下标分别表示状态转移到状态概率。
发射概率矩阵
根据马尔科夫假设,当前观测仅取决于当前状态。
假设观测一共有种可能的取值,即。由于共有种,所以这些参数向量构成了的发射概率矩阵:
示例
下图中,
为健康、发烧,为正常、体寒、头晕可观测矩阵
隐马尔科夫模型三个基本用法
- 样本生成问题:给定模型,生成满足模型约束的样本,即一系列观测序列及对应的状态序列
- 模型训练问题:给定训练集,估计模型参数
- 序列预测问题:已知模型参数,给定观测序列,求最可能的状态序列
马尔科夫模型样本生成
隐马尔科夫模型训练
在监督学习中,我们利用极大似然法来估计隐马尔科夫模型参数。
归一化函数及转为为函数,方便后期计算时,使用加法代替乘法。
/**
* 频次向量归一化为概率分布
* 并将概率转换为log,方便后期将乘法改为加法
*
* @param freq 输入的一维度矩阵(1 X n)
*/
void normalizeAndToLog(float[] freq){
// 累计一行的SUM
float sum = MathUtility.sum(freq);
for (int i = 0; i < freq.length; i++) {
// 归一化
freq[i] /= sum;
// 使用log函数
freq[i] = Math.log(freq[i])
}
}
初始化状态概率向量的估计
初始状态可以看作是状态转移的一种特例,即是由BOS转移而来。 统计的所有取值的频次记作向量
由初始状态到的概率向量:
| \ | ||||
|---|---|---|---|---|
/**
* 估计初始状态概率向量
*
* @param samples 训练样本集
* 是包含多个2*N的矩阵样本数据,每个矩阵第0行为观测值,第1行为状态值
* @param max_state 状态的最大下标,等于N-1
*/
void estimateStartProbability(Collection<int[][]> samples, int max_state) {
// 初始状态概率向量
float[] start_probability = new float[max_state + 1];
for (int[][] sample : samples) {
// 只取第一个状态进行累加
int s = sample[1][0];
++start_probability[s];
}
// 归一化 log化
normalizeAndToLog(start_probability);
}
转移概率矩阵的估计
记样本序列在时刻处于状态,时刻转移到状态。
统计这样的转移频次计入矩阵元素,根据极大似然估计,从转移到的转移概率可估计为矩阵第行频次的归一化:
有前一状态转移到当前状态,矩阵如下:
| \ | ||||
|---|---|---|---|---|
/**
* 估计转移概率矩阵
*
* @param samples 训练样本集
* 是包含多个2*N的矩阵样本数据,每个矩阵第0行为观测值,第1行为状态值
* @param max_state 状态的最大下标,等于N-1
*/
void estimateTransitionProbability(Collection<int[][] samples, int max_state) {
// 状态转移概率矩阵
float[][] transition_probability = new float[max_state + 1][max_state + 1];
for (int[][] sample: samples) {
// 处理一个序列
int prev_s = sample[1][0];
for (int i = 1; i < sample[0].length; i++) {
// 获取当前状态
int s = sample[1][i];
// 累计状态转移矩阵的对应值
++transition_probability[prev_s][s];
// 转移到下一个状态
prev_s = s;
}
}
for (int i = 0; i < transition_probality.length; i++) {
// 归一化每一个状态(一行数据)
normalizeAndToLog(transition_probability[i])
}
}
发射概率矩阵的估计
统计样本中状态为且观测为的频次,计入矩阵元素,则状态发射观测的概率估计为:
由当前状态发射到的矩阵:
| \ | ||||
|---|---|---|---|---|
/**
* 估计状态发射概率
*
* @param samples 训练样本集
* 是包含多个2*N的矩阵样本数据,每个矩阵第0行为观测值,第1行为状态值
* @param max_state 状态的最大下标,等于N-1
* @param max_obser 观测的最大下标
*/
void estimateEmissionProbability(Collection<int[][]> samples, int max_state, int max_obser) {
float[][] emission_probability = new float[max_state + 1][max_obser + 1];
for (int[][] sample : samples) {
// 计算每一个序列
for (int i = 0; i < sample[0].length; i++) {
int o = sample[0][i];
int s = sample[1][i];
++emission_probability[s][o];
}
}
for (int i = 0; i < emission_probability.length; i++) {
normalizeAndToLog(emission_probability[i]);
}
}
隐马尔科夫模型的预测
预测:给定观测序列,求解最可能的状态序列极其概率。
比如一个一个病人,他最近三天的感受是:正常、体寒、头晕,请预测他这三天最可能的健康状态和相应的概率。
概率计算的前向算法
给定观测序列和一个状态序列,计算两者的联合概率,以及搜索其中的最大概率。
那么给定长为的状态序列,对应的概率就是:
由以上公式,得到显隐状态序列的联合概率:
将其中的每个对应上实际发生的序列,就能带入中相应元素,从而计算出任意序列的概率了。
搜索状态序列的维比特算法
要搜索上式最大概率所对应的状态,可以将每个状态作为有向图中的一个节点:
- 节点间的距离由转移概率决定
- 节点本身的花费由发射概率决定
那么所有备选状态构成一幅有向无环图,待求概率最大的状态序列就是求取图中最长路径,此时的搜索算法称为维比特算法(如红色为示意找到的最佳路径):
- 暴力搜索:枚举每个时刻的种备选状态,相邻两个时刻之间的状态由组合,则个时刻的复杂度是
维比特算法
可以论证,最优解情况下仅依赖于。定义二维数据表示时刻以结尾的所有局部路径最大概率,从递推到,每次递推都是在上一次的N条局部路径中挑选,所以复杂度。为了追踪最优解,还需要记录每个状态的前驱:定义另外一个二维数组,同下标定义,存储局部最优路径状态的前驱状态。
计算过程:
- 初始化:时初始最优路径的备选由个状态组成,它们的前驱为空:
- 递推:根据转移概率矩阵和发射概率计算花费,找出新的局部最优解,更新两个数组
- 终止:找出最终时刻数组中的最大概率,以及相应的结尾状态
- 回溯:根据前驱数组回溯前驱状态,取得最优路径下标,其中
path路径矩阵,保存每个状态的前驱状态,如上面的维比特算法示意,对应前驱路径矩阵如下:
| \ | ||||
|---|---|---|---|---|
每个状态的最大分数表(仅仅示例)
| \ | ||||
|---|---|---|---|---|
代码
/*
* 预测(维比特算法)
*
* @param observeration 观测序列
* @param state 预测状态序列
* @return 概率的对象,可利用Math.exp(maxScore)还原log函数
*/
float predict(int[] observeration, int[] state) {
// 序列长度
final int T = observation.length;
// 状态种类
final int max_s = start_probability.length;
float[] score = new float[max_s];
// 如上面的二维表格
// path[t][s]: 第t个状态在为s时,存储前一个状态
int[][] path = new int[T][max_s];
// 第一个时刻,使用初始概率乘以发射概率,遍历状态
for (int cur_s = 0; cur_s < max_s; ++cur_s) {
// 由于初始概率与发射概率都已经log化过,此处的加法,就是乘法
score[cur_s] = start_probability[cur_s] + emission_probability[cur_s][observation[0]];
}
// 第二个时刻,需考虑上一时刻的概、转移概率以及发射概率
float[] pre = new float[max_s];
// 遍历观察序列
for (int t = 1; t < T; t++) {
// swap(now, pre)
float[] temp = pre;
// 保存前一状态的分数
pre = score;
score = temp;
// 遍历状态
for (int s = 0; s < max_s; s++) {
score[s] = Integer.MIN_VALUE;
// 计算转移概率
for (int f = 0; f < max_s; f++) {
// log化后的加法相当于乘法
// 计算路径长度:前驱状态f转移到当前状态s
float p = pre[f] + transition_probability[f][s] + emission_probability[s][observation[t]];
// 取路径最大的,记录其前驱状态f
if (p > socre[s]) {
score[s] = p;
path[t][s] = f;
}
}
}
}
float max_score = Integer.MIN_VALUE;
int best_s = 0;
// 计算最终分数最高的最终状态
for (int s = 0; s < max_s; s++) {
if (score[s] > max_score) {
max_score = score[s];
best_s = s;
}
}
// 回溯前向路径矩阵
for (int t = path.length - 1; t >= 0; --t) {
// 由最终最优状态向前回溯状态
state[t] = best_s;
// 取得最大score的pre状态,进行回溯
best_s = path[t][best_s];
}
return max_score;
}
隐马尔科夫模型应用于中文分词
如果将观测换成字符,状态换成{B, M, E, S},我们就能应用隐马尔科夫模型驱动中文分词。
然而隐马尔科夫模型用于中文分词的效果并不理想。 事实上,隐马尔科夫模型假设人们说的话仅仅取决于一个隐藏的{B, M, E, S}序列,这个假设太单纯,不符合语言规律。语言不是由那么简单的标签序列生成,语言含有更多特征,而隐马尔科夫模型没有捕捉到。
隐马尔科夫模型能捕捉到仅限两种特征:
- 前一个标签是什么
- 当前字符是什么 朴素的隐马尔科夫模型并不适合分词,需要更高级的模型。
特征提取
劳动者 的 合法权益 又 如何 保障 ?
protected List<String[]> convertToSequence(Sentence sentence) {
List<String[]> charList = new LinkedList<String[]>();
// 遍历每个单词
for (Word w : sentence.toSimpleWordList()) {
String word = CharTable.convert(w.value);
if (word.length() == 1) {
// 词:标签 第0行是词,第1行是标签
charList.add(new String[]{word, "S"});
} else {
charList.add(new String[]{word.substring(0, 1), "B"});
for (int i = 1; i < word.length() - 1; ++i) {
charList.add(new String[]{word.substring(i, i + 1), "M"});
}
charList.add(new String[]{word.substring(word.length() - 1), "E"});
}
}
return charList;
}
训练
- 特性转换
// sentenceList为特征提取里面提到的
List<int[][]> sampleList = new ArrayList<int[][]>(sequenceList.size());
for (List<String[]> sequence : sequenceList)
{
int[][] sample = new int[2][sequence.size()];
int i = 0;
for (String[] os : sequence)
{
// 显性序列 字符
sample[0][i] = vocabulary.idOf(os[0]);
assert sample[0][i] != -1;
// 隐形状态 标签{B, M, E, S}
sample[1][i] = tagSet.add(os[1]);
++i;
}
sampleList.add(sample);
}
model.train(sampleList);
训练及预测同上面的HMM训练过程