N-gram 分词

概述

本课程作业主要借助python工具,实现了N-gram分词中的Unigram和Bigram分词器,并将前向最大切词FMM和后向最大切词的结果作为Baseline,对比分析N-gram分词器在词语切分正确率、词义消歧和新词识别等方面的优势。 github地址

数据说明

本实验使用的语料是人民日报1998年中文标注的语料库,19484条。在处理过程中,按照训练集 : 测试集 = 9 : 1的比例进行随机划分。 数据预处理包括:去词性、去文本行标识(19980101-01-001-001)、词典统计、标点统计等。

主要流程
  1. 文本预处理,分为:语料随机切分、去词性、统计词典等
  2. 使用前向FMM和后向BMM最大切分,对测试语料进行切分,统计准确率、召回率和F1值
  3. 统计训练语料词典概率,用Unigram模型对待切分文本采用递归的思想,进行最大概率切分,统计准确率、召回率和F1值
  4. 统计Bigram词典概率,用Bigram模型对文本进行切分方案概率计算,选取概率最大切分,统计准确率、召回率和F1值
  5. 对比分析不同切词方案对文本歧义和未登录词的处理效果。

算法描述

1. 文本预处理
"""语料的随机切分,默认按照9 : 1的比例切分训练集合测试集"""
def splitCorpus(train=0.9, fileName='199801.txt'):
train_file = open('train.txt', 'wb')
test_file = open('test.txt', 'wb')
with open(fileName, 'rb') as f:
  for line in f:
      if random() <= train:
          train_file.write(line)
      else:
          test_file.write(line)
train_file.close()
test_file.close()
print('successfully to split corpus by train = %f test = %f' %
    (train, 1 - train))

"""统计语料词典"""
def toWordSet(file_name='train.txt', is_save=False, save_file='wordSet.pkl'):
# 获取词典
word_dict = defaultdict(float)
with open(file_name, 'rb') as f:
    for line in f:
        content = line.decode('gbk').strip().split()
        # 去掉第一个词“19980101-01-001-001/m”
        for word in content[1:]:
            word_dict[word.split(u'/')[0]] += 1
if is_save:
    # 保存wordSet以复用
    joblib.dump(word_dict, save_file)
print("successfully get word dictionary!")
print("the total number of words is:{0}".format(len(word_dict.keys())))
return word_dict
2. FMM和BMM
  • 前向最大切词,是以可变滑动窗口对文本进行顺序取词,若改词在词典中存在,则进行一次切分;否则,缩小窗口大小,继续取词与词典库进行搜索,知道窗口词长为1。后向切词原理相似,只不过是从后面开始进行窗口滑动。
def forwardMaxCut(ustring, word_set, word_max_len=5):
"""
前向最大切词
:param ustring: 待切词文本
:param word_set: 词典
:param word_max_len: 最大词长
:return: 词列表
"""
wordList = []
if not ustring:
return wordList
while ustring:
sentence_len = len(ustring)
if sentence_len < word_max_len:
    word_max_len = sentence_len
for i in range(word_max_len, 0, -1):
    if ustring[:i] in word_set or i == 1:
        wordList.append(ustring[:i])
        ustring = ustring[i:]
        break
    else:
        i -= 1
return wordList
  • 运行结果:

    前向分词结果:
    successfully to split corpus by train = 0.900000 test = 0.100000
    the total number of punction is:47
    the total number of words is:53198
    召回率为:0.9466013860392212
    准确率为:0.9154134377927275
    F值为:0.9307462195496794
    
    后向分词结果:
    successfully to split corpus by train = 0.900000 test = 0.100000
    the total number of punction is:47
    the total number of words is:53767
    召回率为:0.950686195146746
    准确率为:0.92130516483316
    F值为:0.9357651113664159
    
  • 由于每次运行,都会对语料进行随机切分,因此运行结果中的词典大小有出入。

  1. Unigram分词
  • Unigram切词的计算公式如下:
  • 基本思路: 首先统计出训练集词典中各个词的频率,用来表示公式中的wi;然后,对待切分文本的进行某种策略的切分,递归选择切分概率最大的子切分序列,最后回溯得到最大概率切分。
  • 举个栗子:
例句S:我是北京大学的一名研究生
# S的切分可以拆成两步
P(S) = P(我)*P(是北京大学的一名研究生)
#同时后面的子句,继续可以拆成:
P(S) = P(我)*P(是北京大学的一名研究生) = P(我)*P(是)*p(北京大学的一名研究生)
#这里有个问题,我们是如何知道应该拆成“我”和“是”两个词,而不是“我是”一个词呢
#上面计算最大概率,是递归调用的,假设我们开始有两种切分
[我,是北京大学的一名研究生]
[我是,北京大学的一名研究生]
#计算组合概率
P1 = P(我)*P(是北京大学的一名研究生)
P2 = P(我是)*P(北京大学的一名研究生)
#我们会发现
P1 > P2
#对于后面的任何子句,我们都采用无脑切分,即设置最大词长,这里假设为3,可得到以下切分:
[我,是北京大学的一名研究生]
[我是,北京大学的一名研究生]
[我是北,京大学的一名研究生]
#然后分别递归计算
#为了满足性能的要求,避免重复计算,我们采用将间接计算的子序列的组合概率,都存储起来
#每次计算新的子序列时,先查看子序列的切分组合中,是否包含已经计算过的子子序列,包含,则直接复用
  • 平滑,对于词典中搜索不到的词,需要做一定的平滑处理,常用的平滑方法原理见这里,本课程实验支持加1平滑、WItten-Bell平滑方法,默认采用的是Wittten-Bell平滑方法。以下为计算最大切分概率程序:
def maxP(self, sentence):
  '''
  计算最大切分方案
  :param sentence: 待切分句子
  :return:
  '''
  # 遍历所有切分组合中,找出最大概率切分
  if len(sentence) <= 1:
      return self.DICT.getPValue(self, sentence)
  # 判断切词方向:backward 或 forward
  sentence_split_words = [self.backwardSplitSentence(
      sentence), self.forwardSplitSentence(sentence)][self.split_way != 'back']
  # 记录最大概率值
  max_p_value = 0
  # 储存最大概率下的切分组合
  word_pairs = []
  # 组合概率值
  word_p = 0
  for pair in sentence_split_words:
      p1, p2 = 0, 0
      if pair[0] in self.value_dict:
          p1 = self.value_dict[pair[0]]
      else:
          p1 = self.maxP(pair[0])
      if pair[1] in self.value_dict:
          p2 = self.value_dict[pair[1]]
      else:
          p2 = self.maxP(pair[1])
      word_p = p1 * p2
      if max_p_value < word_p:
          max_p_value = word_p
          word_pairs = pair
  # 在词典中查询当前句对应的频率,不存在时,返回 1/N
  sentence_p_value = self.DICT.getPValue(self, sentence)
  # 不切分概率最大时,更新各值
  if sentence_p_value > max_p_value and self.DICT.inDict(self, sentence):
      self.value_dict[sentence] = sentence_p_value
      self.seg_dict[sentence] = sentence
      return sentence_p_value
  # 某种切分组合概率最大时,更新sentence对应概率,避免后续切分重复计算
  else:
      self.value_dict[sentence] = max_p_value
      self.seg_dict[sentence] = word_pairs
      return max_p_value
  • 运行结果:
successfully to split corpus by train = 0.900000 test = 0.100000
the total number of words is:53705
the total number of punction is:47
召回率为:0.9614382160763091
准确率为:0.9319770859102912
F值为:0.9464784466054017
3. Bigram分词
  • Bigram切词的计算公式如下:
  • 基本思路: 首先统计出训练集词典中各个Bigram的频率,如[我|是]、[我|来自],用来表示公式中的[wi|wi-1];然后,对待切分文本给出所有的切分方案,计算切分概率最大的切分序列。
  • 举个栗子:
例句S: 这几块地面积还真不小。
#对S进行切分,获得所有切分方案
S1 = ['这', '几', '块', '地', '面', '积', '还', '真', '不', '小']
S2 = ['这', '几', '块', '地', '面', '积', '还', '真', '不小']
S3 = ['这', '几', '块', '地', '面积', '还', '真', '不', '小']
S4 = ['这', '几', '块', '地', '面积', '还', '真', '不小']
S5 = ['这', '几', '块', '地面', '积', '还', '真', '不小']
#利用Bigram公式,计算所有的方案的切分概率,为了避免出现float下溢出,采用log求和
P(S1) = -64.745
P(S2) = -63.894
P(S3) = -55.041
P(S4) = -54.190
P(S5) = -58.190
P(S4) > P(S3)>P(S5)>P(S2)>P(S1)
#不难发现,上述例句对于机器是一个歧义句,S4和S5两种切分都可以
#但是根据语境,S4是正确的
  • 运行结果:
successfully to split corpus by train = 0.900000 test = 0.100000
the total number of words is:53260
The total number of bigram is : 403121.
successfully witten-Bell smoothing! smooth_value:1.3372788850370981e-05
the total number of punction is:47
召回率为:0.962036929819092
准确率为:0.9401303935308096
F值为:0.950957517059212
4. 结果分析
  • 对比指标
指标 FMM BMM Unigram Bigram
准确率 91.54% 92.13% 93.20% 94.01%
召回率 94.66% 95.07% 96.14% 96.20%
F1值 93.07% 93.58% 94.64% 95.10%
  • 根据上表可知:分词效果最好的是Bigram,最差的是FMM。 因为FMM只考虑了前向顺序词是否在字典中出现,而Bigram除了考虑词典中是否包含此词,同时也考虑了邻接词对分词的选择的影响。 在处理歧义上,Bigram具有较好的效果,能基本实现消除歧义,但是消除歧义的效果受文本训练大小的影响。 在处理未登录词上,这里仅仅是对未登录词切分为单个字,因此在未登录词的处理上还要进一步的研究讨论。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容