03中文分词

出自——火哥

目录

1. 为什么要分词

1.1 中文分词的目的

1.2 英文天然分词,比中文更适合做相似度比对?

1.3 分词是越细越好吗?

2. 怎么分词

2.1 字典匹配

2.1.1前缀树

2.1.2字典匹配案例

2.1.3 字典匹配实现

2.2概率语言模型

2.2.1 概率语言模型的理论知识

2.2.2 概率语言模型的案例

2.3 viterbi算法

2.3.1 viterbi算法解决什么问题

2.3.2 viterbi算法在分词中的应用

2.4 马尔可夫模型

2.4.1 马尔科夫模型能解决什么问题 

2.4.2 马尔科夫的局限性——不能解决什么问题

2.5隐马尔可夫模型

2.5.1 HMM参数的计算

2.5.2 HMM解决不容易切分的词 

3. jieba分词——常用的中文分词工具

4.分词实战 

4.1 批量分词

4.2 增量分词


1. 为什么要分词

1.1 中文分词的目的

让机器更好的“理解”文章。

1.2 英文天然分词,比中文更适合做相似度比对?

No,英文中每个单词包含很多不同的意思。

1.3 分词是越细越好吗?

视情况而定

搜索——>越细越好——>因为其比较注重召回

推荐——>粗一点好——>因为其更注重精准度(精准推荐),粒度粗有利于“保留语义”

2. 怎么分词

 

里面涉及的一些算法:

容易切分的:用字典匹配,动态规划,vterbi算法

不容易切分的:隐马尔可夫模型

2.1 字典匹配

2.1.1前缀树

如下图所示,一共包含了8个键的trie结构:"A", "to", "tea", "ted", "ten", "i", "in", "inn".

详情见:https://zh.wikipedia.org/wiki/Trie

2.1.2字典匹配案例

词库为:北京,北京大学,大学生,生活,学生,中心,活动

需要切分的句子:北京大学生活动中心

(1) 将词库存到前缀树中:

(2) 正向匹配

计算词库中单词的最大长度maxLen。

“北京大学生活动中心”正向匹配的分词结果为:[北京大学,生活,动,中心]

(3) 反向匹配

正向匹配和反向匹配的原理相同,唯一不同的是,正向匹配是从左到右依次滑窗,反向匹配是从右向左依次滑窗。

“北京大学生活动中心”反向匹配的分词结果为:[北京,大学生,活动,中心]

2.1.3 字典匹配实现

下面是一个反向最大匹配的例子

import sys

WordDic = {}
MaxWordLen = 1

def LoadLexicon(lexiconFile):
    global MaxWordLen
    infile = open(lexiconFile, 'r', encoding='gb2312')
    s = infile.readline().strip()
    while len(s) > 0:
        #s = s.decode("gb2312")
        WordDic[s] = 1
        if len(s) > MaxWordLen:
            MaxWordLen = len(s)
        s = infile.readline().strip()
    infile.close()

def BMM(s):
    global MaxWordLen
    wordlist = []
    i = len(s)
    while i > 0:
        start = i - MaxWordLen
        if start < 0:
            start = 0
        while start < i:
            tmpWord = s[start:i]
            if tmpWord in WordDic:
                wordlist.insert(0, tmpWord)
                break
            else:
                start += 1
        if start >= i:
            wordlist.insert(0, s[i-1:i])
            start = i - 1
        i = start
    return wordlist

def PrintSegResult(wordlist):
    print("After word-seg:")
    for i in range(len(wordlist)-1):
        print(wordlist[i])
    print(wordlist[len(wordlist)-1])

LoadLexicon("./lexicon.txt")

# inputStr = u"南京市长江大桥"
inputStr = u"北京大学生活动中心"

wordlist = BMM(inputStr)
PrintSegResult(wordlist)

其中BMM为Back Max Match,./lexicon.txt 为词库,比如:

正向匹配代码实现,待续:

Q:正向/反向匹配效果不好?

———主要是因为词库(lexicon)不好,词库准全的问题;

———用规则的方式永远也解决不了所有的问题。(那用什么来解决呢?

Q有两种切分方法,正向匹配:[北京大学,生活,动,中心],反向匹配:[北京,大学生,活动,中心], 到底用哪一个呢?机器怎么判断哪一个更好呢?

——使用概率语言模型 

2.2概率语言模型

顾名思义:找概率最大方案。

2.2.1 概率语言模型的理论知识

其中C表示句子,S表示句子的切分方案。

P(C): 句子出现的概率,往往是常数。

P(C|S):在切分方案S的条件下,得到句子C的概率。为1,把切分方案S中的词拼到一起就是句子C了。

故:

其中假设: 句子中,每个词的出现独立同分布,所以:

Q每个词出现的概率一般要怎么算呢?从哪里来呢?

Q如果一个句子很长,切分出来了很多个词,且每个词的概率又不高,P(S)很小,向下溢出或是很难比较出两个方案的优劣(因为两个方案的P(S)很小,差0.00000000001,如果精度不够,可能就会认为两个方案是一样的?)

——万能的log登场

——使用log,一举两得

      a. 防止最终结果向下溢出

      b. 乘法变加法,计算速度更快

Q:句子中的词真的是独立同分布的吗?

No,实际中,独立性假设并不成立。

一元概率语言模型:我们上述的认为一个词的出现不依赖与它前面出现的词(即句子中的词是独立同分布的),叫做一元概率模型(Unigram),该模型只考虑了切分出的词数和词频。

二元概率语言模型:假设当前单词只与前个单词有关

三元概率语言模型:假设当前单词只与前个单词有关 

 

……

N元概率语言模型:假设当前单词与前N个单词有关

2.2.2 概率语言模型的案例

接着2.1.2 的案例来说

词库为:北京,北京大学,大学生,生活,学生,中心,活动

需要切分的句子:北京大学生活动中心

现在有两种切分方案:

第一种正向匹配:[北京大学,生活,动,中心]

第二种反向匹配:[北京,大学生,活动,中心]

下面就可以用概率语言模型,来看一下,哪种方案更好:

P(S1) = P([北京大学,生活,动,中心]) = P(北京大学) * P(生活) * P(动)* P(中心)

P(S2) = P([北京,大学生,活动,中心]) = P(北京) * P(大学生) * P(活动)* P(中心)

2.3 viterbi算法

Viterbi算法是一种动态规划的算法。

想象一个乡村诊所。村民有着非常理想化的特性,要么健康要么发烧。他们只有问诊所的医生的才能知道是否发烧。 聪明的医生通过询问病人的感觉诊断他们是否发烧。村民只回答他们感觉正常、头晕或冷。

假设一个病人每天来到诊所并告诉医生他的感觉。医生相信病人的健康状况如同一个离散马尔可夫链。病人的状态有两种“健康”和“发烧”,但医生不能直接观察到,这意味着状态对他是“隐含”的。每天病人会告诉医生自己有以下几种由他的健康状态决定的感觉的一种:正常、冷或头晕。这些是观察结果。 整个系统为一个隐马尔可夫模型(HMM)。

病人连续三天看医生,医生发现第一天他感觉正常,第二天感觉冷,第三天感觉头晕。 于是医生产生了一个问题:怎样的健康状态序列最能够解释这些观察结果。

维特比算法揭示了观察结果 ['normal', 'cold', 'dizzy'] 最有可能由状态序列 ['Healthy', 'Healthy', 'Fever']产生。 换句话说,对于观察到的活动, 病人第一天感到正常,第二天感到冷时都是健康的,而第三天发烧了。

维特比算法的计算过程可以直观地由格图表示。 维特比路径本质上是穿过格式结构的最长路径。 诊所例子的格式结构如下, 黑色加粗的是维特比路径:

 参考:https://zh.wikipedia.org/wiki/%E7%BB%B4%E7%89%B9%E6%AF%94%E7%AE%97%E6%B3%95https://zh.wikipedia.org/wiki/%E7%BB%B4%E7%89%B9%E6%AF%94%E7%AE%97%E6%B3%95

2.3.1 viterbi算法解决什么问题

寻找最有可能产生观测时间序列的viterbi路径——隐含状态序列,特别是在马尔可夫信息源上下文和隐马尔可夫模型中。

用直白的话来说,就是寻找最优或概率最大的路径。

Q:遍历和 viterbi算法寻求最优路径的时间复杂度,why?viterbi算法的优点是什么? 

2.3.2 viterbi算法在分词中的应用

给定一个句子的wordGraph,寻找最优路径

(1)通过遍历寻找最优路径。

(2)通过viterbi算法寻找最优路径,在level t+1重用t的结果。

   

一起用代码来用代码切分一下“容易切分”的词吧。(即句子wordGraph中的每个词都在词表中)

整体的代码逻辑如下所示:

createLexicon.py

import sys

rawDataFile = "./RenMinData.txt"
idDataFile = "./RenMinData.id.txt"
wordDicFile = "./WordDic.rm.txt"

WordIDTable = {}

id = 1
infile = open(rawDataFile, 'r', encoding='gb2312')
s = infile.readline().strip()
while len(s) > 0:
    #s = s.decode("gb2312")
    for word in s.split(' '):
        if word not in WordIDTable:
            WordIDTable[word] = id
            id += 1
    s = infile.readline().strip()
infile.close()
print("Reading raw data file finished!")
print("Total number of words:", len(WordIDTable))

infile = open(rawDataFile, 'r', encoding='gb2312')
outfile = open(idDataFile, 'w')
s = infile.readline().strip()
while len(s) > 0:
    #s = s.decode("gb2312")
    words = s.split(' ')
    for i in range(len(words)-1):
        word = words[i]
        if word not in WordIDTable:
            print("OOV word found!")
        else:
            outfile.write(str(WordIDTable[word]))
            outfile.write(' ')
    word = words[len(words)-1]
    if word not in WordIDTable:  # 未登录词
        print("OOV word found!")
    else:
        outfile.write(str(WordIDTable[word]))
    outfile.write("\r\n")
    s = infile.readline().strip()
infile.close()
outfile.close()
print("Writing id data file finished!")

outfile = open(wordDicFile, 'w', encoding='gb2312')
for word in WordIDTable.keys():
    # outfile.write(word.encode("gb2312"))
    outfile.write(word)
    outfile.write(' ')
    outfile.write(str(WordIDTable[word]))
    outfile.write("\r\n")
outfile.close()
print("Writing word id table file finished!")

BiLMTrain.py

import sys

idDataFile = "./RenMinData.id.txt"
wordDicFile = "./WordDic.rm.txt"
biModelFile = "./BiModel.rm.txt"

WordIDTable = {}
BigramTableList = []  # 一元概率模型
UnigramCountList = []  # 二元概率模型
SmoothedProbList = []
TotalNum = 0

# load wordDicFile to WordIDTable
infile = open(wordDicFile, 'r', encoding='gb2312')
s = infile.readline().strip()
while len(s) > 0:
    #s = s.decode("gb2312")
    words = s.split(' ')
    if words[0] not in WordIDTable:
        WordIDTable[words[0]] = int(words[1])
    s = infile.readline().strip()
infile.close()
print("Reading word dic file finished!")
print("Total number of words:",len(WordIDTable))

# 初始化 BigramTableList UnigramCountList SmoothedProbList
lenWordIDTable = len(WordIDTable)
BigramTableList = [{} for _ in range(lenWordIDTable + 1)]
UnigramCountList = [0 for _ in range(lenWordIDTable + 1)] 
SmoothedProbList = [0 for _ in range(lenWordIDTable + 1)]  

infile = open(idDataFile, 'r')
s = infile.readline().strip()
while len(s) > 0:
    words = s.split(' ')
    widlist = []
    TotalNum += len(words)
    for word in words:
        widlist.append(int(word))
    for wordid in widlist:
        UnigramCountList[wordid] += 1
    for i in range(len(widlist)-1):
        tmpHT = BigramTableList[widlist[i]]
        if widlist[i+1] not in tmpHT:
            tmpHT[widlist[i+1]] = 1
        else:
            tmpHT[widlist[i+1]] += 1
    s = infile.readline().strip()
infile.close()
print("Reading id data file finished!")

#compute probabilities
for wid1 in range(1,len(WordIDTable)+1):
    SmoothedProbList[wid1] = 1/(float)(UnigramCountList[wid1] + len(WordIDTable))
    ht = BigramTableList[wid1]
    for wid2 in ht.keys():
        ht[wid2] = (float)(ht[wid2]+1) /(float)(UnigramCountList[wid1] + len(WordIDTable))
    UnigramCountList[wid1] = (float)(UnigramCountList[wid1])/(float)(TotalNum)

#save to file
outfile = open(biModelFile, 'w')
outfile.write(str(len(WordIDTable))+" "+str(TotalNum)+"\r\n")
for wid1 in range(1,len(WordIDTable)+1):
    outfile.write(str(UnigramCountList[wid1])+" ")
    outfile.write(str(SmoothedProbList[wid1]))
    ht = BigramTableList[wid1]
    for wid2 in ht.keys():
        outfile.write(" "+str(wid2)+" "+str(ht[wid2]))
    outfile.write("\r\n")
outfile.close()
print("Writing model file finished!")

ViterbiCWS.py

viterbi算法的基本逻辑

BiModel.rm.txt(只择出来和“南京市长江大桥”有关的概率)

s1:createGraph

s2:利用viterbi算法搜索得到最优路径 

# -*- coding:utf-8 -*-

import sys
import math


class Node:
    def __init__(self, word):
        self.bestScore = 0.0
        self.bestPreNode = None
        self.len = len(word)
        self.word = word


class BiLM:
    def __init__(self, WordDicFile, biLMFile):
        self.wordNum = 0
        self.wordIDTable = {}
        self.unigramProb = []
        self.bigramProb = []
        self.unknownWordProb = 1.0

        # load WordIDTable
        infile = open(WordDicFile, 'r', encoding='gb2312')
        sline = infile.readline().strip()
        self.maxWordLen = 1
        while len(sline) > 0:
            # sline = sline.decode("gb2312")
            items = sline.split(' ')
            if len(items) != 2:
                print("Lexicon format error!")
                sline = infile.readline().strip()
                continue
            self.wordIDTable[items[0]] = int(items[1])
            if len(items[0]) > self.maxWordLen:
                self.maxWordLen = len(items[0])
            sline = infile.readline().strip()
        infile.close()
        infile = open(biLMFile, 'r')
        sline = infile.readline().strip()
        items = sline.split(' ')
        if len(items) == 2:  # the first line
            self.wordNum = int(items[0])
        else:
            print("Bad format found in LM file!")
            sys.exit()
        sline = infile.readline().strip()

        # initialization unigramProb bigramProb
        # load unigramProb bigramProb
        lenWordIDTable = len(self.wordIDTable)
        self.unigramProb = [0.0 for _ in range(lenWordIDTable + 1)]
        self.bigramProb = [{} for _ in range(lenWordIDTable + 1)]

        wid = 1
        while len(sline) > 0:
            items = sline.split(' ')
            # self.unigramProb[wid] = float(items[1])
            self.unigramProb[wid] = float(items[0])
            i = 2
            while i < len(items):
                self.bigramProb[wid][int(items[i])] = float(items[i + 1])
                i += 2
            sline = infile.readline().strip()
            wid += 1
        infile.close()
        print(len(self.wordIDTable), "words loaded")

    def GetScoreBack(self, word1, word2):
        wid1 = -1
        wid2 = -1

        if (word1 is not '' and word1 not in self.wordIDTable.keys())\
                or word2 not in self.wordIDTable.keys():
            print("word1 or word2 should be in wordIDTable. word1: %s, word2: %s" % (word1, word2))
            sys.exit()
        wid2 = self.wordIDTable[word2]
        if wid2 not in self.bigramProb[wid1]:
            return self.unigramProb[wid1]
        return self.bigramProb[wid1][wid2]

    def GetScore(self, word1, word2):
        wid1 = -1
        wid2 = -1
        if word1 not in self.wordIDTable:
            return self.unknownWordProb
        wid1 = self.wordIDTable[word1]
        if word2 not in self.wordIDTable:
            return self.unigramProb[wid1]
        wid2 = self.wordIDTable[word2]
        if wid2 not in self.bigramProb[wid1]:
            return self.unigramProb[wid1]
        return self.bigramProb[wid1][wid2]


def CreateGraph(s):
    # initializatioon WordGraph
    WordGraph = [[] for _ in range(len(s) + 2)]  # +2 is start and end Node
    WordGraph[0] = [Node("")]  # start Node
    WordGraph[-1] = [Node("")]  # end Node

    # Other nodes
    for i in range(len(s)):
        j = myBiLM.maxWordLen
        if i + j > len(s):
            j = len(s) - i
        while j > 0:
            if s[i:i + j] in myBiLM.wordIDTable:
                newNode = Node(s[i:i + j])
                WordGraph[i + j].append(newNode)
            j -= 1
        if len(WordGraph[i + 1]) < 1:  # why?
            print("Unknown character found!", i, s[i])
            sys.exit()
    return WordGraph


def ViterbiSearch(WordGraph):

    for i in range(len(WordGraph) - 1):
        for curNode in WordGraph[i + 1]:
            if curNode.len == 0:
                preLevel = i
            else:
                preLevel = i + 1 - curNode.len  # the level of the previous word. eg"南","南京市"的前一个leval为0,"市长"的前一个level为2
            if preLevel < 0:
                print("running error!")
                sys.exit()
            preNode = WordGraph[preLevel][0]
            score = myBiLM.GetScore(preNode.word, curNode.word)
            score = preNode.bestScore + math.log(score)
            maxScore = score
            curNode.bestScore = score
            curNode.bestPreNode = preNode
            for j in range(1, len(WordGraph[preLevel])):
                preNode = WordGraph[preLevel][j]
                score = myBiLM.GetScore(preNode.word, curNode.word)
                score = preNode.bestScore + math.log(score)
                if score > maxScore:
                    curNode.bestScore = score
                    curNode.bestPreNode = preNode


def BackSearch(WordGraph):
    resultList = []
    curNode = WordGraph[len(WordGraph) - 1][0].bestPreNode
    while curNode.bestPreNode != None:
        resultList.insert(0, curNode.word)
        curNode = curNode.bestPreNode
    return resultList


WordDicFile = "./WordDic.rm.txt"
BiLMFile = "./BiModel.rm.txt"
myBiLM = BiLM(WordDicFile, BiLMFile)

inputStr = u"南京市长江大桥"

WordGraph = CreateGraph(inputStr)

for NodeList in WordGraph:
    for Node in NodeList:
        print("CurNode Word: ", Node.word)

ViterbiSearch(WordGraph)
resultList = BackSearch(WordGraph)
for word in resultList:
    print(word, '')

2.4 马尔可夫模型

每个状态只依赖之前有限个状态

  • N阶马尔可夫:依赖之前n个状态 <——> N元概率模型
  • 1阶马尔可夫:仅仅依赖前一个状态<——>1元概率模型

如下为1阶马尔可夫:

2.4.1 马尔科夫模型能解决什么问题 

可以解决句子生成,文章生成

比如要生成句子——今天我写了一个程序

w1=今天,w2=我,w3=写,w4=了,w5=一个,w6=程序

p(w1=今天,w2=我,w3=写,w4=了,w5=一个,w6=程序)

=p(w1=今天)p(w2=我|w1=今天)p(w3=我|w2=今天)……p(w6=程序|w5=一个)

2.4.2 马尔科夫的局限性——不能解决什么问题

马尔可夫模型不能解决双序列问题

(1)机器翻译:源语言序列 <--> 目标语言序列

      比如,中文译为英文

      

(2)语音识别:语音信号序列 <--> 文字序列

(3)词性标注:文字序列 <--> 词性序列

         写 / 一个 / 程序

        Verb / Num / Noun

(4)拼音纠错:原始文字序列 <--> 纠正过的文字序列

        自己的事情自己坐

        自己的事情自己做

Q:那怎么解决这些双序列问题?

隐马尔科夫模型。

2.5隐马尔可夫模型

Q:怎样判断我这词该分不该分呢?就是启动用HMM切分“未登录词”的条件是什么?

容易切分的词已经用字典匹配,匹配上了,剩下的就是不容易切分的词(即未登录词),把剩下的这部分词再放HMM里面看其是否需要被切分。

用一句话来描述HMM:

先完成第一状态,然后依次由当前状态生成下一状态,最后每个状态发射出一个观测值

 

2.5.1 HMM参数的计算

HMM的参数,初始概率,状态转移概率,发射概率需要计算,观测值直接观测得到。

假设中译英的场景。

Q:在中译英的场景中,谁是观测序列,谁是状态序列?

中文是观测序列,英文是状态序列。

首先找语料库(一堆中英文对照的文章)

 

2.5.2 HMM解决不容易切分的词 

HMM就是“胶水”,看哪些字可以“粘”在一起,变成词。

假设“广州塔”词表中没有,那他到底是一个词还是可以再切分出多个词呢?

其中,每个字均用<位置,词性>来表示

  • BEMS表示位置信息:B(开头)、M(中间)、E(结尾)、S(独立成词)
  • 词性:n(名词)、nr(人名)、ns(地名)、v(动词) 

中文分词词性对照表

中文分词词性对照表_kevin_darkelf的博客-CSDN博客_词性对照表

最终得到的状态序列为<B,n><M,n><E,n>,即表示“广州塔”是一个词。

最终得到的状态序列为<B,n><E,n><S,n>,即表示“广州塔”分为“广州/塔”。

对于HMM的应用,最典型的就属给定O,找到最优的S

即S,O联合概率最大的状态(S序列),即我们想要的状态(S序列)。

Q:怎样找出来最优的S呢?

S1: 首先需要得到HMM模型中的初始概率,状态转移概率,发射概率。

S2:用voterbi算法得到最优的路径S

3. jieba分词——常用的中文分词工具

jieba分词GitHub - fxsjy/jieba: 结巴中文分词

和我们上面章节讲的分词思路是一致的

4.分词实战 

4.1 批量分词

4.2 增量分词

Q:对于一个新的专有领域,怎么使用HMM来分未登录词?新的领域,每个词的状态,概率怎么得到?先想一下已有的,每个词的状态,概率,是怎么得到的?

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值