最近刚好在看一些自然语言处理方面的东西,写的一些代码中也用到了jieba这个库,感觉从效果上来说还是可以的。就顺便把分词这一块的代码也给看了(关键词抽取部分的代码已经在之前的博客中提过了),接下来跟大家分享下其中的一些方法。
首先是入口函数:
re_han_default = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._]+)", re.U)
re_skip_default = re.compile("(\r\n|\s)", re.U)
re_han_cut_all = re.compile("([\u4E00-\u9FD5]+)", re.U)
re_skip_cut_all = re.compile("[^a-zA-Z0-9+#\n]", re.U)
def cut(self, sentence, cut_all=False, HMM=True):
'''
The main function that segments an entire sentence that contains
Chinese characters into seperated words.
Parameter:
- sentence: The str(unicode) to be segmented.
- cut_all: Model type. True for full pattern, False for accurate pattern.
- HMM: Whether to use the Hidden Markov Model.
'''
sentence = strdecode(sentence)
if cut_all:
re_han = re_han_cut_all
re_skip = re_skip_cut_all
else:
re_han = re_han_default # 所用的非空白字符都会被匹配
re_skip = re_skip_default # 所有的空白字符都会被跳过
if cut_all:
cut_block = self.__cut_all
elif HMM:
cut_block = self.__cut_DAG
else:
cut_block = self.__cut_DAG_NO_HMM
blocks = re_han.split(sentence)
for blk in blocks:
if not blk:
continue
if re_han.match(blk):
for word in cut_block(blk):
yield word # 使用yield关键字,将函数变为一个生成器
else:
tmp = re_skip.split(blk)
for x in tmp:
if re_skip.match(x):
yield x
elif not cut_all:
for xx in x:
yield xx
else:
yield x
至于什么叫做全模式,看完代码,你自然能够明白。
# 用于生成self.FREQ,参数f表示字典的路径
def gen_pfdict(self, f):
lfreq = {}
ltotal = 0 # 所有频率之和
f_name = resolve_filename(f)
for lineno, line in enumerate(f, 1):
try:
line = line.strip().decode('utf-8')
word, freq = line.split(' ')[:2] # word表示字典中的词,freq表示频率
freq = int(freq)
lfreq[word] = freq
ltotal += freq
for ch in xrange(len(word)):
wfrag = word[:ch + 1] # 将字典中一个多个字组成的词进行拆解,试图发现新词。但感觉这一项不是很有必要,因为如果这个词之后会出现
# 就没有必要加;如果之后没有出现,在产生结果时,等于0这一项也会被过滤掉
if wfrag not in lfreq:
lfreq[wfrag] = 0
except ValueError:
raise ValueError(
'invalid dictionary entry in %s at Line %s: %s' % (f_name, lineno, line))
f.close()
return lfreq, ltotal
def get_DAG(self, sentence):
self.check_initialized() #主要用于载入字典和申请cache,与分词任务本身并没有特别大的联系,所以就不列出其代码了
DAG = {}
N = len(sentence)
for k in xrange(N):
tmplist = [] # 存储的应该是从当前字开始,到下标位置为止,都是常见的词(这里常见的词应该是从字典中而来的)
i = k
frag = sentence[k]
while i < N and frag in self.FREQ:
if self.FREQ[frag]: # 如果是出现在词典中的词,且出现的次数大于1,就认为它是常见的
tmplist.append(i)
i += 1
frag = sentence[k:i + 1] # 从当前位置开始,不断地向后扩大遍历词的范围
if not tmplist:
tmplist.append(k)
DAG[k] = tmplist
return DAG
# 直接根据get_DAG函数的输出结果,输出词
def __cut_all(self, sentence):
dag = self.get_DAG(sentence)
old_j = -1 # 感觉这个变量并没有什么意义
for k, L in iteritems(dag):
if len(L) == 1 and k > old_j:
yield sentence[k:L[0] + 1]
old_j = L[0]
else:
for j in L:
if j > k:
yield sentence[k:j + 1]
old_j = j
全模式的就是指从当前字开始,列出之后所有可能的分词组合。对于『我来到北京清华大学』这一段文本,使用全模式分词之后的结果是:我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学。可以看到在『清』字这里,产生了两种结果。
def calc(self, sentence, DAG, route):
N = len(sentence)
route[N] = (0, 0)
logtotal = log(self.total)
for idx in xrange(N - 1, -1, -1):
# 这里max函数的参数是元组,比较的标准是元组中的第一个元素
# 但是还不知道第一个元素的值的数学意义,我猜应该是从该字开始之后的各种组合中,可能性最大的那一个
route[idx] = max((log(self.FREQ.get(sentence[idx:x + 1]) or 1) - logtotal + route[x + 1][0], x) for x in DAG[idx])
def __cut_DAG_NO_HMM(self, sentence):
DAG = self.get_DAG(sentence)
route = {} #表示词的边界
self.calc(sentence, DAG, route)
x = 0
N = len(sentence)
buf = ''
while x < N:
y = route[x][1] + 1
l_word = sentence[x:y]
if re_eng.match(l_word) and len(l_word) == 1: # 如果l_word是单个的字母或是数字,则希望将它与下一个词连起来
buf += l_word
x = y
else:
if buf:
yield buf
buf = ''
yield l_word
x = y
if buf:
yield buf
buf = ''
它是将分词过程看作字的分类问题。在以往的分词方法中,无论是基于规则的方法还是基于统计的方法,一般都依赖于一个事先编制的词表,自动分词过程就是通过查词表作出词语切分的决策。与词相反,由字构词的分词方法认为每个字在构造一个特定的词语时都占据这一个确定的构词位置。假如规定每个字只有4个词位:词首(B)、词中(M),词尾(E)和单独成词(S)。分词结果表示成字标注形式之后,分词问题就变成了序列标注问题。
这里所说的『字』不仅限于汉字,也可以指标点符号、外文字母、注音标号和阿拉伯数字等任何可能出现在汉语文本中的文字符号,所有这些字符都是由字构成词的基本单元。
由字构词的分词技术的重要优势在于,它能够平衡地看待词表词和未登录词的识别问题,文本中的词表词和未登录词都用统一的字标注过程来实现,分词过程成为字重组的简单过程。在学习的架构上,既可以不必专门强调词表词信息,也不用专门设计特定的未登录词识别模块,因此,大大简化了分词系统的设计。
对于代码中所涉及到的隐马尔可夫模型,以及维特比算法,这里就不详细说明了,可以到网上找相应的博客来看下。
def __cut_DAG(self, sentence):
DAG = self.get_DAG(sentence)
route = {}
self.calc(sentence, DAG, route)
x = 0
buf = ''
N = len(sentence)
while x < N:
y = route[x][1] + 1
l_word = sentence[x:y]
if y - x == 1: # 不想产生只有一个字的词
buf += l_word
else:
if buf:
if len(buf) == 1:
yield buf
buf = ''
else:
if not self.FREQ.get(buf): # 如果当前分词的结果不在字典当中,或者其出现次数小于1(因为词典中出现了一些新词),需要进行再次分割
recognized = finalseg.cut(buf)
for t in recognized:
yield t
else:
for elem in buf:
yield elem
buf = ''
yield l_word
x = y
if buf:
if len(buf) == 1:
yield buf
elif not self.FREQ.get(buf):
recognized = finalseg.cut(buf)
for t in recognized:
yield t
else:
for elem in buf:
yield elem
MIN_FLOAT = -3.14e100
PROB_START_P = "prob_start.p"
PROB_TRANS_P = "prob_trans.p"
PROB_EMIT_P = "prob_emit.p"
# 每一个标记之前的标记会是什么
PrevStatus = {
'B': 'ES', # 词首
'M': 'MB', # 词中
'S': 'SE', # 单独成词
'E': 'BM' # 词尾
}
def load_model():
# 分别代表马尔可夫链的开始状态的分布,状态转移概率,以及输出的产生概率
start_p = pickle.load(get_module_res("finalseg", PROB_START_P))
trans_p = pickle.load(get_module_res("finalseg", PROB_TRANS_P))
emit_p = pickle.load(get_module_res("finalseg", PROB_EMIT_P))
return start_p, trans_p, emit_p
if sys.platform.startswith("java"):
start_P, trans_P, emit_P = load_model()
else:
from .prob_start import P as start_P
from .prob_trans import P as trans_P
from .prob_emit import P as emit_P
def viterbi(obs, states, start_p, trans_p, emit_p):
V = [{}] # tabular,表示维特比变量,下标表示时间
path = {} # 记录从当前状态回退回去最有可能的路径
for y in states: # init
V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT) # 应该是乘号吧,第二项表示由状态y产生第一个字的可能性
path[y] = [y]
for t in xrange(1, len(obs)):
V.append({})
newpath = {}
# 对当前字的每一个状态的可能性都进行了计算
# 包括了,如果是当前状态,之前最可能的状态是什么
for y in states:
em_p = emit_p[y].get(obs[t], MIN_FLOAT)
(prob, state) = max(
[(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]]) # 计算这一步中最有可能的状态
V[t][y] = prob
newpath[y] = path[state] + [y]
path = newpath
# 每一段文字结束时的状态只能是e或s
(prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
return (prob, path[state])
# 使用的是基于字构成的分词方法
def __cut(sentence):
global emit_P
prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P)
begin, nexti = 0, 0 # nexti这个变量应该是为了避免B之后没有e的情况,因为在产生标注的时候并不是成对产生的
# print pos_list, sentence
for i, char in enumerate(sentence):
pos = pos_list[i]
if pos == 'B':
begin = i
elif pos == 'E':
yield sentence[begin:i + 1] # 构成了一个完整的词就输出
nexti = i + 1
elif pos == 'S':
yield char # 单独成词
nexti = i + 1
if nexti < len(sentence):
yield sentence[nexti:]
re_han = re.compile("([\u4E00-\u9FD5]+)")
re_skip = re.compile("(\d+\.\d+|[a-zA-Z0-9]+)")
def cut(sentence):
sentence = strdecode(sentence)
blocks = re_han.split(sentence)
for blk in blocks:
if re_han.match(blk):
for word in __cut(blk):
yield word
else:
tmp = re_skip.split(blk)
for x in tmp:
if x:
yield x