拼音输入法实验报告
实验基本思路
经课上了解以及翻阅资料,该问题可由隐马尔可夫求解,即隐马尔可夫的三个基本问题中的第三个,预测问题。其实也包含了第二个问题,学习问题,由于此问题已给定观测序列(因为不考虑多音字,相当于观测状态数量为1且概率为1),语料中的汉字即隐藏状态序列,因此先用极大似然估计也就是统计字频,二元的转移概率,即可得到转移概率矩阵,初始状态概率也通过统计字频求得。接下来就是预测问题,则使用维特比算法求解。
-
第一步:统计字频,得到转移概率矩阵以及初始状态概率,观测概率(不考虑多音字时认为是1即可)
-
第二步:平滑处理
-
第三步:维特比算法
1、预处理、统计字频以及二元字频
由于给定的是原始语料,含有不少符号以及数字,而且还有一二级汉字表6763个汉字中没出现的不常见汉字,因此先对这些其他汉字、字符处理,并且将语料处理成一行一行的句子。
大致看了新闻语料后发现,其中的有不少数字有不少日期,例如“在10月10日发射XXXXX”,若是只是简单的丢弃非6763个汉字中的其他内容,并且在该处断句,会造成“日”字的在初始状态出现的概率增大,觉得不合理。而且考虑到将初始概率的求解也统一到转移概率当中。
因此自定义了5种隐藏状态,分别是START,END,NUMBER,OTHER_CHINESE,OTHER_SYMBOL。那么初始状态概率则成为了P(w1|START)也记录在转移矩阵中,便于统计。
#此函数定义在src/newtrain.py中
def splitSentence(context, hz2py_dict):
...
subdata.append(START)
for c in context:
if isChinese(c):
if hz2py_dict.setdefault(c,None) != None:
subdata.append(c)
before = c
else:
subdata.append(OTHER_CHINESE)
before = OTHER_CHINESE
elif isNumber(c):
if before == NUMBER:
continue
subdata.append(NUMBER)
before = NUMBER
...
...
...
return data
以上是划分句子的部分代码(自定义的5个状态:START,在汉字表中为’s’,对应拼音为’start’,同理,END,‘e’,‘end’,NUMBER,‘n’,‘number’,OTHER_CHINESE,‘c’,‘other_chinese’,OTHER_SYMBOL,‘y’,‘other_symbol’)。
效果如下:
input:我是中国人,我25岁,在看《我和我的祖国》
output:
s我是中国人e
s我nce
sccy我和我cc国ye
统计字频以及二元字频
因为在初始字之前插入一个"s"表示开头,因此初始概率也统一到二元词频中。
2、平滑处理
按我个人的理解,应该是为了解决训练集稀疏,低概率的状态被认为不可能发生的情况。在本次实验中,使用了加一平滑以及固定lambda插值平滑。
在统计字频的时候发现,有250个字在给定的语料中从未出现过,使用了加一平滑,生成P(wi)的概率(因为bigram插值平滑要用上unigram的概率),公式如下:
P
(
w
i
)
=
C
(
w
i
)
+
1
∑
j
(
C
(
w
j
)
+
1
)
=
C
(
w
i
)
+
1
∑
j
C
(
w
j
)
+
V
P(w_i)=\frac{C(w_i) + 1}{\sum_j (C(w_j)+1)}=\frac{C(w_i)+1}{{\sum_j C(w_j)}+V}
P(wi)=∑j(C(wj)+1)C(wi)+1=∑jC(wj)+VC(wi)+1
随后运用公式生成二元模型条件概率:
P
(
w
i
∣
w
i
−
1
)
=
C
(
w
i
−
1
w
i
)
C
(
w
i
−
1
)
P(w_i\mid w_{i-1})=\frac{C(w_{i-1}w_i)}{C(w_{i-1})}
P(wi∣wi−1)=C(wi−1)C(wi−1wi)
λ
P
(
w
i
∣
w
i
−
1
)
+
(
1
−
λ
)
P
(
w
i
)
⟹
P
(
w
i
∣
w
i
−
1
)
\lambda P(w_i\mid w_{i-1})+(1-\lambda)P(w_i)\Longrightarrow P(w_i\mid w_{i-1})
λP(wi∣wi−1)+(1−λ)P(wi)⟹P(wi∣wi−1)
其实还有这样的一种情况出现,上述公式中的 C ( w i − 1 ) C(w_{i-1}) C(wi−1)为0的时候,会导致分母为0的情况,则可以取 P ( w i ∣ w i − 1 , C ( w i − 1 ) = 0 ) = P ( w i ) P(w_i\mid w_{i-1},C(w_{i-1}) = 0) = P(w_i) P(wi∣wi−1,C(wi−1)=0)=P(wi)。
不过在本次实验中,为了减少转移矩阵的大小,取
P
(
w
i
∣
w
i
−
1
,
C
(
w
i
−
1
)
=
0
)
=
1
V
P(w_i\mid w_{i-1},C(w_{i-1}) = 0) = \frac {1}{V}
P(wi∣wi−1,C(wi−1)=0)=V1,而不是为一整行每个都单独存一个不同的数,而且对于例如图中,“是”的那一整行的情况,代码是这样处理的,只对“是中”进行插值平滑处理,其他平分剩余的概率值,写成公式如下:
对于每一行:
1、若
C
(
w
i
−
1
)
=
=
0
C(w_{i-1})==0
C(wi−1)==0,则:
P
w
i
∣
w
i
−
1
,
C
(
w
i
−
1
)
=
0
=
1
V
P{w_i\mid w_{i-1},C(w_{i-1} )= 0} = \frac {1}{V}
Pwi∣wi−1,C(wi−1)=0=V1
2、若
C
(
w
i
−
1
)
!
=
0
C(w_{i-1})!=0
C(wi−1)!=0且
C
(
w
i
−
1
w
i
)
!
=
0
C(w_{i-1}w_i)!=0
C(wi−1wi)!=0,则:
P
(
w
i
∣
w
i
−
1
)
=
λ
C
(
w
i
−
1
w
i
)
C
(
w
i
−
1
)
+
(
1
−
λ
)
P
(
w
i
)
P(w_i\mid w_{i-1}) = \lambda \frac{C(w_{i-1}w_i)}{C(w_{i-1})}+(1-\lambda)P(w_i)
P(wi∣wi−1)=λC(wi−1)C(wi−1wi)+(1−λ)P(wi)
3、若
C
(
w
i
−
1
)
!
=
0
C(w_{i-1})!=0
C(wi−1)!=0且
C
(
w
i
−
1
w
i
)
=
=
0
C(w_{i-1}w_i)==0
C(wi−1wi)==0,则
P
(
w
i
∣
w
i
−
1
)
=
1
−
∑
上
一
公
式
计
算
出
的
概
率
V
−
不
为
0
的
个
数
P(w_i\mid w_{i-1}) = \frac{1- \sum 上一公式计算出的概率}{V - 不为0的个数}
P(wi∣wi−1)=V−不为0的个数1−∑上一公式计算出的概率
在代码中的实现如下:
#此函数定义在src/newtrain.py中
def createTransition(bigram_prob,unigram_prob,hz2py_dict, bigram_smooth):
transition_prob = dict()
transition_prob['default'] = {'default': math.log(1.0 / HANZI_COUNT)}#第一种情况
...
...
...
transition_prob[wi_1][wi] = math.log( bigram_smooth*math.exp(bigram_prob[wi_1][wi])\
+ (1 - bigram_smooth)*math.exp(unigram_prob[wi]))
prob -= math.exp(transition_prob[wi_1][wi]) #第二种情况
...
...
...
transition_prob[wi_1]['default'] = math.log(prob) - math.log(HANZI_COUNT - exesist)#第三种情况
return transition_prob
若是完全按照插值平滑,太多不同的值,没法压缩,生成的转移矩阵写入到文件后将会达到1.3G的大小,读文件很耗时,而且维特比算法运算时也耗时,所以对平滑处理进行了改进,完全是处于效率方面的考虑,在我的测试中似乎不仅是效率提高了,而且精准也有些提高,但这样的平滑处理有没啥影响我也不太清楚。
3、维特比算法
最后就是维特比算法,按照《统计学习方法》书中所给的伪代码自己实现了一遍,这里并没啥技术点存在,不在此赘述。有一点说一下,因为我对于每个句子都加了’s’'e’标示开头和结尾,因此初始状态概率向量就是一个除’s’状态为1其他状态都为0的向量。可以更方便一些。
实验效果和分析
使用微信群中共享的一份测试数据,该数据有8722行,共有84238个汉字,分别在不同的lambda情况共进行了15次预测,句子错误率平均在50.99%,字错误率平均值为12.85%。
./data/input_test.txt #拼音文件
./data/YUANSHI_test.txt #汉子文件
效果差与好的例子
原文 | 预测结果 |
---|---|
特朗普提名布鲁耶特出任美国能源部长 | 特朗普提名补录也特出任美国能源部长 |
虽然冲突的结果难免一拍两散 | 虽然冲突的结果难免疫排两三 |
其背后掩盖的种种荒诞与黑暗 | 其背后掩盖的重中黄石与黑暗 |
一名自杀式袭击者在阿富汗东部楠格哈尔省地区的抗议活动期间引爆炸弹 | 一名自杀式袭击者在阿富汗东部楠格哈尔省地区的抗议活动期间引爆炸弹 |
第一个例子,应该说是人名未能正确预测,这类专有名词,效果不好,或许需要基于词的模型,并且配有知识库,将达到更好的效果,但“特朗普”却正确预测了,可能是训练的语料中出现的比较多吧。
第二个例子,将“难免一拍两散”预测成了“难免疫排两三”,可能是训练用的语料中,免疫出现的比较多,而我们又是用的二元模型,只考虑前后两个字,因此对于这种情况,可能基于字的三元模型即可解决
第三个例子,我主要想说的是“石”,因为这个是多音字,而我的模型并未考虑多音字的情况,查看训练结果的时候我发现几乎所有“dan”的音的字都预测成了“石”,这个问题需要改一下观测概率,应该把多音字考虑上。
第四个例子,至于为什么可以完美预测如此长的句子,不是很清楚。
不同平滑参数 λ \lambda λ对预测效果的影响
在0.8到1的区间均分取了15个值
句子预测错误率
15个 λ \lambda λ下的预测错误率分布在[50.66%,51.60%]之间,平均值为50.99%,当 λ \lambda λ取0.843时得到了最低的句预测错误率为50.66%。
字预测错误率
15个 λ \lambda λ下的预测错误率分布在[12.79%,12.97%]之间,平均值为12.85%,当 λ \lambda λ取0.929时得到了最低的字预测错误率为12.79%。
从结果可以看出,似乎 λ \lambda λ只要在[0.8,1]之间,似乎对预测准确度影响不是很大,也可能用于测试的拼音数据比较单一,而且和训练用的语料类似,所以看不出什么影响,但网上看别人资料说在0.95的时候效果比较好。
运行效率分析
模型加载大约需要6秒,根据不同\(\lambda\)生成整个转移概率矩阵需要6秒,在这15次测试中预测用时平均为1400秒,最差一次为1500秒,也就是单个句子平均用时161毫秒,单个字平均用时16.7毫秒。维特比算法时间复杂度大概是 O ( t ∗ n 2 ) O(t*n^2) O(t∗n2),不过由于转移矩阵比较稀疏,实际应该更快一些。
收获
对隐马尔可夫更加熟悉了,其次则是使用动态规划的维特比算法,在此次试验中曾想过要如何来使用A算法,但看到树上基本都是用动态规划(算是A的一种特例),而且不知道怎么定义评价函数。或许可以用更高效些的近似算法,减少搜寻的分支。然后是在真实输入法中,应该会列出前K个候选,这样的功能也可以考虑实现。
改进方案
- 基于字的三元四元模型,但据说增加耗时的同时,效果提升得并不多,可能不太建议;
- 基于词的二元三元模型,应该可以有不错的提升,后期打算尝试;
- 另一种无脑的方法,利用比较火热的深度学习,可以比较省事又效果好。
附:
目录结构:
./src/
--yuliao/
----sina_news/
----sina_news_gbk/
--pinyinhanzibiao/
----一二级汉字表.txt
...
--model_data/
----transition_prob.json
...
--newtrain.py #预处理以及生成模型参数
--predict.py #维特比算法
--compare.py #比较不同lambda值的影响
--main.py #支持传参 main.py ./data/input.txt ./data/output.txt