目录
1 本文算法
1.1 算法概述或框架图
本次实验主要可以分为三大部分:标注序列、模型训练用于预测标签以及维特比求解最优路径。下面将依次对这三大部分进行概述。
1.1.1 标注序列
这一部分主要就是给句子中的每个字进行标注,具体字标注的方为法有好几种,其中最常见的为4标注和6标注。
本次实验采用MSR语料库进行,MSR数据集是人民日报标注语料库是在得到人民日报社新闻信息中心许可的条件下,以1998年和2014人民日报语料为对象,由北京大学计算语言学研究所和富士通研究开发中心有限公司共同制作的标注语料库。我们首先对语料库进行预处理,这次的分词仅需要词语切分部分的信息,并不需要词性标注信息,因此对原语料库的分词信息进行读取并进行标注,将每个中文字标注为4种类型,分别为S(0)、B(1)、M(2)、E(3),其中S(Single)代表单个字,B(Begin)代表词的第一个字(词的开始),M(Middle)代表词的中间部分字,E(End)代表词的最后一个字(词的结束)。
标注实例如下:广/b 外/e 是/s 一/b 所/e 人/b 才/m 济/m 济/e 的/s 大/b 学/e。理论上来说标注越多会越精细,而效果也会越来越好,但标注太多可能存在时间精力的问题,导致样本不足或样本质量不高等问题。
1.1.2 模型训练
这里我们用到了双向长短期记忆模型(双向LSTM),双向LSTM是基于LSTM进行改进的,而LSTM是递归神经网络的一种变形。前向LSTM与后向LSTM组合构成双向LSTM。如果单独对字特征向量进行其他深度模型的训练的话,无法考虑到字词在句子中的顺序,而LSTM可以捕捉到较长距离的字词依赖关系,并且通过训练可以学到记忆哪些信息和遗忘哪些信息。但如何只是单向LSTM的话存在一个问题:无法从后往前编码信息,而现实生活中的句子前后往往都是有联系的,所以此时选择双向LSTM能够更好的捕捉语义之间的关系,使得后续的标注效果更好。
在分词中,LSTM可以根据输入序列输出一个序列,这个序列考虑了上下文的联系,因此,可以给每个输出序列接一个softmax分类器,来预测每个标签的概率。基于这个序列到序列的思路,我们就可以直接预测句子的标签。BLSTM具体工作实现可以参考图1。
1.1.3 维特比算法求解最优路径
维特比算法是一个特殊但应用最广的动态规划算法,利用动态规划,可以解决任何一个图中的最短路径问题。而维特比算法是针对一个特殊的图——篱笆网络的有向图的最短路径问题而提出的。所以此时正好能够利用维特比算法来帮助我们找到句子标注的最优组合。
1.2 算法各模块流程图等或公式文字描述等
该小结以“我喜欢广州市”为例,并试图采用绘图的方式,来介绍双向LSTM和维特比算法。具体内容如下:
双向LSTM:
如上图所示,训练后的双向LSTM在给句子中的某一个字,如“欢”字进行预测的时候,不仅会结合前面的“我”和“喜”,而且也会结合“广”、“州”和“市”,最终再输出标注结果,且采用了softmax,所以输出结果中的每个类别的值为对应可能预测的概率值,
维特比算法:
如上图所示,当双向LSTM对句子中的每个字进行标注后,每个字对应的能够得到一个长度为4的向量,又由于使用了最终分类时使用到了softmax,因此向量中的每个值分别对应类别s、b、m和e的概率值大小,即概率值越大,对应为该类别的可能性更大。此时最终的标注结果的可能结果有4的6次方那么多,且句子越长,对应的可能结果越多,此时若采用穷举的方法,那么算法的时间复杂度会非常的大。因此我们采用了维特比的动态规划的思想,最终得到一个最优解。
此时结果为:我/s 喜/b 欢/e 广/b 州/m 市/e
1.3 算法细节
维特比算法求解最优解的具体实现过程:
要想找到从开头的“我”到结尾的“市”的最优路径,我们可以先从左往右一列一列地来看。首先是第一列,我们不能武断地说开头第一列哪一个必定是全局最优路径中的起点,目前为止任何一个都有可能是全局最短路径的备选项,所以我们接着往右看,到了第二列“喜”,依次从“喜”对应的s、b、m、e开始遍历,首先是s:
如上图所示,经过“喜”s的只有4条路径,我们通过简单的运算就可以知道b->s是其中最优的路径,而其他三条都比b->s要差,因此绝不可能是目标答案,可以大胆的删掉了,删掉了不可能是答案的路径,就是维特比算法的重点,因为后面我们再也不用考虑这些被删掉的路径了。现在经过“喜”s的所有路径只剩一条路径了,如下图所示:
同理,经过“喜”b、“喜”m和“喜”e也可以得到对应的最优子路径。而且对应到每一列都是得到4个候选解。以此类推,到最终的“市”这一列,便能得到4个最优候选解,再从其中选出最优的解即可。
完整代码如下:
import tensorflow as tf
from tensorflow.python.keras.models import Model, load_model
from tensorflow.python.keras.layers import Input, Dense, Dropout, LSTM, Embedding, TimeDistributed, Bidirectional
from tensorflow.python.keras.utils import np_utils
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_recall_fscore_support
from sklearn.metrics import accuracy_score
import numpy as np
import re
# 模型参数设置
embedding_size = 128
maxlen = 32
hidden_size = 64
batch_size = 64
epochs = 25
def load_data(path):
data = open(path, encoding='utf-8').read().rstrip('\n')
data = re.split('[,。?!、\n]', data)
X_data = []
Y_data = []
for sentence in data:
sentence = sentence.split(" ")
X = []
y = []
try:
for s in sentence:
s = s.strip()
if len(s) == 0:
continue
elif len(s) == 1:
X.append(char2id[s])
y.append(tags['s'])
elif len(s) > 1:
X.append(char2id[s[0]])
y.append(tags['b'])
for i in range(1, len(s) - 1):
X.append(char2id[s[i]])
y.append(tags['m'])
X.append(char2id[s[-1]])
y.append(tags['e'])
if len(X) > maxlen:
X = X[:maxlen]
y = y[:maxlen]
else:
for i in range(maxlen - len(X)):
X.append(0)
y.append(tags['x'])
except:
continue
else:
if len(X) > 0:
X_data.append(X)
Y_data.append(y)
X_data = np.array(X_data)
Y_data = np_utils.to_categorical(Y_data, 5)
return X_data, Y_data
def calc_test_result(result, test_label):
true_label = []
predicted_label = []
for i in range(result.shape[0]):
for j in range(result.shape[1]):
if np.argmax(test_label[i, j]) != 4:
true_label.append(np.argmax(test_label[i, j]))
predicted_label.append(np.argmax(result[i, j]))
print("Confusion Matrix :")
print(confusion_matrix(true_label, predicted_label))
print("Classification Report :")
print(classification_report(true_label, predicted_label, digits=2))
print("Accuracy ", accuracy_score(true_label, predicted_label))
print("Macro Classification Report :")
print(precision_recall_fscore_support(true_label, predicted_label, average='macro'))
print("Weighted Classification Report :")
print(precision_recall_fscore_support(true_label, predicted_label, average='weighted'))
def viterbi(nodes):
trans = {'be': 0.5, 'bm': 0.5, 'eb': 0.5, 'es': 0.5, 'me': 0.5, 'mm': 0.5, 'sb': 0.5, 'ss': 0.5}
paths = {'b': nodes[0]['b'], 's': nodes[0]['s']} # 第一层,只有两个节点
for l in range(1, len(nodes)): # 后面的每一层
paths_ = paths.copy() # 先保存上一层的路径
paths = {}
for i in nodes[1].keys(): # i为本层节链接点
# 对于本层节点,找出最短路径。
nows = {}
# 上一层的每个结点到本层节点的连接
for j in paths_.keys(): # j为上层节点
if j[-1] + i in trans.keys(): # 若转移概率不为0
nows[j + i] = paths_[j] + nodes[l][i] + trans[j[-1] + i]
nows = sorted(nows.items(), key=lambda x: x[1], reverse=True)
paths[nows[0][0]] = nows[0][1]
paths = sorted(paths.items(), key=lambda x: x[1], reverse=True)
return paths[0][0]
def cut_words(data):
data = re.split('[,。!?、\n]', data)
sens = []
Xs = []
for sentence in data:
sen = []
X = []
sentence = list(sentence)
for s in sentence:
s = s.strip()
if not s == '' and s in char2id:
sen.append(s)
X.append(char2id[s])
if len(X) > maxlen:
sen = sen[:maxlen]
X = X[:maxlen]
else:
for i in range(maxlen - len(X)):
X.append(0)
if len(sen) > 0:
Xs.append(X)
sens.append(sen)
Xs = np.array(Xs)
ys = model.predict(Xs)
results = ''
results2 = ''
for i in range(ys.shape[0]):
nodes = [dict(zip(['s', 'b', 'm', 'e'], d[:4])) for d in ys[i]]
ts = viterbi(nodes)
for x in range(len(sens[i])):
if ts[x] in ['s', 'e']:
results += sens[i][x] + '/'
else:
results += sens[i][x]
# if ys[i][x].tolist().index(max(ys[i][x])) == 0 or ys[i][x].tolist().index(max(ys[i][x])) == 3:
# results2 += sens[i][x] + '/'
# else:
# results2 += sens[i][x]
# print(results[:-1])
# print(results2[:-1])
return results[:-1]
if __name__ == '__main__':
# print(cut_words('今天又是美好的一天!'))
# print(cut_words('我喜欢你'))
vocab = open("data/msr_training_words.utf8", encoding='utf-8').read().rstrip('\n').split('\n')
vocab = list(''.join(vocab))
stat = {}
for v in vocab:
stat[v] = stat.get(v, 0) + 1
stat = sorted(stat.items(), key=lambda x: x[1], reverse=True) # 按出现字数降序排列
vocab = [s[0] for s in stat]
char2id = {w: c + 1 for c, w in enumerate(vocab)}
id2char = {c + 1: w for c, w in enumerate(vocab)}
tags = {'s': 0, 'b': 1, 'm': 2, 'e': 3, 'x': 4}
# 模型训练
X_train, y_train = load_data('data/msr_training.utf8')
X_test, y_test = load_data('data/msr_test_gold.utf8')
X = Input(shape=[maxlen, ], dtype='int32', name='input')
embedding = Embedding(input_dim=len(vocab) + 1, output_dim=embedding_size, input_length=maxlen, mask_zero=True)(X)
blstm = Bidirectional(LSTM(hidden_size, return_sequences=True), merge_mode='concat')(embedding)
blstm = Dropout(0.6)(blstm)
output = TimeDistributed(Dense(5, activation='softmax'))(blstm)
model = Model(X, output)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
model.fit(X_train, y_train, batch_size=batch_size, epochs=epochs)
# 模型保存
print('模型保存')
model.save('model_blstm.h5')
# 模型调用
print('模型调用')
model = load_model('model_blstm.h5')
result = model.predict(X_test)
calc_test_result(result, y_test)
print(cut_words('实现祖国的完全统一是海内外全体中国人的共同心愿'))
print(cut_words('他从马上下来'))
print(cut_words('中文分词在中文信息处理中是最最基础的,无论机器翻译亦或信息检索还是其他相关应用,如果涉及中文,都离不开中文分词,因此中文分词具有极高的地位。'))
2 实验结果或系统展示
为了进一步检验BLSTM的中文分词效果,我们还提供了LSTM中文分词和jieba中文分词进行对比。
LSTM:实现/祖国/的/完全/统一/是/海内外/全体/中国/人/的/共同/心愿
BLSTM:实现/祖国/的/完全/统一/是/海内外/全体/中国人/的/共同/心愿
JIEBA:实现/祖国/的/完全/统一/是/海内外/全体/中国/人/的/共同/心愿
此时的BLSTM分词结果好于LSTM分词以及JIEBA分词。
LSTM:他/从/马上/下来
BLSTM:他/从/马上/下来
JIEBA:他/从/马上/下来
此时三种分词方法的分词结果相同。
LSTM:中文分词/在/中文/信息/处理/中/是/最最/基础/的/无论/机器/翻译/亦/或/信息/检索/还是/其他/相关/应用/如果/涉及/中文/都/离不开/中文/分词/因此/中文分词/具有/极高/的/地
BLSTM:中文/分词/在/中文/信息处理/中/是/最最/基础/的/无论/机器/翻译/亦/或/信息/检索/还是/其他/相关/应用/如果/涉及/中文/都/离不开/中文/分词/因此/中文/分词/具有/极高/的/地位
JIEBA:中文/分词/在/中文信息处理/中是/最最/基础/的/无论/机器翻译/亦/或/信息检索/还是/其他/相关/应用/如果/涉及/中文/都/离不开/中文/分词/因此/中文/分词/具有/极高/的/地位/
此时分词的效果:JIEBA > BLSTM > LSTM。
LSTM模型效果如下:
BLSTM模型效果如下:
通过对比我们不难发现,LSTM的准确率为95.5%,效果要远高于LSTM,而这也正好符合了我们最开始所提到的双向LSTM能够更好的捕捉语义之间的关系,使得后续的标注效果更好。
3 讨论和分析
3.1 实例分析
以“我喜欢你”这句话为例:
首先我们将该句以字进行拆分,分为“我”、“喜”、“欢”和“你”四个字,在事先构建好的dict词典中,将这四个由字符串转为数值,并构建为一个向量,且为方便后续模型预测的进行,需将长度不足事先设定好的maxlen值的向量进行填充,之后放进BLSTM模型进行预测,此时得到一个矩阵,如下所示:
[[[9.9999571e-01 3.9801721e-06 4.1145225e-09 4.0362377e-07 5.0128282e-20]
[2.1982818e-05 9.9997699e-01 1.0702739e-06 1.3490954e-08 2.3963344e-21]
[1.6898995e-05 4.3487813e-07 2.6678084e-04 9.9971586e-01 1.6420975e-21]
[9.9862480e-01 1.3330624e-03 1.8524337e-05 2.3612887e-05 4.2626622e-17]
[3.0711657e-01 2.8803250e-01 1.3073629e-01 2.7393839e-01 1.7633705e-04]
…
[3.0711657e-01 2.8803250e-01 1.3073629e-01 2.7393839e-01 1.7633705e-04]
[3.0711657e-01 2.8803250e-01 1.3073629e-01 2.7393839e-01 1.7633705e-04]
[3.0711657e-01 2.8803250e-01 1.3073629e-01 2.7393839e-01 1.7633705e-04]
]]
此时我们就得到了每个字对应的句向量,即每个标签可能的概率值。如下:
“我”:{‘s’: 0.9999957, ‘b’: 3.980172e-06, ‘m’: 4.1145225e-09, ‘e’: 4.0362377e-07}
“喜”:{‘s’: 2.1982818e-05, ‘b’: 0.999977, ‘m’: 1.0702739e-06, ‘e’: 1.3490954e-08}
“欢”:{‘s’: 1.6898995e-05, ‘b’: 4.3487813e-07, ‘m’: 0.00026678084, ‘e’: 0.99971586}
“你”:{‘s’: 0.9986248, ‘b’: 0.0013330624, ‘m’: 1.8524337e-05, ‘e’: 2.3612887e-05}
之后,我们再利用上述提到的维特比算法的动态规划的思想,求解出最优路径,即:s -> b -> e -> s。所以最后,我们只需要判断最优路径中哪个为s或者e,在其后面做分割即可。
最终结果为:我/ 喜欢/ 你。
3.2 讨论分析
其实本次实验我们存在一个问题,即转移矩阵中的转移概率是我们自行定义的值,与现实生活中的概率值存在较大误差,而这也会进一步导致最终利用维特比算法求解最优路径的方法没有办法达到更为理想的效果。
同时,BLSTM模型在解决分词这个标注问题上表现比较不错,在比较小的语料库上能够达到这样一个勉强能用的效果,甚至在个别句子上优于Jieba,但缺点也很明显,首先需要大量的标注好的分词训练数据集,更多的数据量基本意味着更好的分词效果,其次本模型对于新词没有很好的适应能力,对于新词汇的分词会出现困难,并且没有对专有名词进行收录,对于生僻的人名也没有办法很好的处理。
4 结论
采用BLSTM+维特比算法那进行中文分词的效果已经是比较不错的了,并且在某些情况甚至优于jieba分词,但是仍然存在缺陷。首先模型训练需要由大量的数据进行训练,且对数据的质量要求较高;其次是该模型对于新词没有很好的适应能力,究其根本也是深度学习本身存在的一些问题,导致了对于某些情况下的分词效果不佳。而除了本文提及的方法外,还有很多效果不错且较为成熟的方法。如HMM、CRF等。事实上,不管是基于规则还是基于HMM、CRF或者是深度学习的方法,在分词效果的具体任务中的差距不是特别明显。在实际运用中,比较多是基于词典的方式进行分词,然后再用统计分词方法进行辅助。不仅能保证分词的准确率,也能对未登录词和歧义词有较好的识别。Jieba分词就是基于规则和基于统计两类方法,且运行速度即分词效果往往由于基于长短期记忆模型的中文分词效果。
结束语
整理不易,希望大家能够关注点赞。
另外,对上述内容或者代码有疑问或者需求的,可以评论留言告诉我。
推荐关注的专栏
👨👩👦👦 机器学习:分享机器学习理论基础和常用模型讲解
👨👩👦👦 数据分析:分享数据分析实战项目和常用技能整理
关注我,了解更多相关知识!
CSDN@报告,今天也有好好学习