统计分词/无字典分词学习(2):n-gram词频统计

          我们现在面对的是“wheninthecourseofhumaneventsitbecomesnecessary”这样一堆语料,要获取词典,怎么办?

第一步肯定是找到所有可能是词的片段了,常用的方法就是n-gram切分了,如假设词的最大长度是3,则句子“abcd”的n-gram切分就是:

1-gram切分:a b c d

2-gram切分:ab bc cd

3-gram切分:abc bcd

         这些切分包含了句子序列中所有可能的长度小于等于3的片段,也就是说,所有的可能成为“单词”的片段,都在这些ngram切分片段里面了,我们后续的工作就是从这些候选片段中过滤出“单词”即可。

          这些候选片段的一个基本特征现在就是词频了,就是出现的个数,这个显然是一个片段能够成为词的一个基本特征,如果一个片段出现的次数非常少,我们认为它很难构成词。

        因此,为了从这些未标注的生语料中构造一个词典,我们首先需要构造所有的n-gram片段,然后统计片段的出现次数,然后选择一个阈值,把低频的片段干掉。这里就是一个简单的词频统计问题,用python的dict统计即可。但由于语料规模一般都非常大,因此,这里将代码写成hadoop streaming的形式,既可以本地运行,也可以放在hadoop运行。

     首先是mapper的代码(ngram_count_mapper.py),其功能就是切分出所有可能的ngram片段即可:

#!/usr/bin/env python
#coding=utf-8
import sys
import os
#获得ngram切分

g_contain_se = 0#是否加入开始和结束字符
g_max_word_length = 12 #最大的切分片段的长度

#输入 line  一个句子
#输入 word_length  词的长度
#coding=utf-8
import sys
import os
#获得ngram切分

g_contain_se = 0#是否加入开始和结束字符
g_max_word_length = 12 #最大的切分片段的长度,也就是最大的词长度

#输入 line  一个句子
#输入 word_length  词的长度
#输出 ngram 切分
#返回值 0,正确,其它错误
def build_ngram_word(line, word_length):
    #进行ngram交叉切分
    if g_contain_se == 1: #判断是否加入开始和结束标志,开关控制
        letter_list = ["<"] #句子开始标记
        letter_list.extend(line) #加入所有的字
        letter_list.append(">") #句子结束标记
    else:
        letter_list = line

    for i in range(0,len(letter_list)-word_length+1):
        word = "".join(letter_list[i:i+word_length])
        print word + "\t" + "1"

#构建所有长度的切分片段
def build_all_ngram_word(line):
    for word_length in range(1,g_max_word_length+2):
        build_ngram_word(line, word_length)

for line in sys.stdin:
    line = line.strip()
    build_all_ngram_word(line)

        然后是reducer代码,就是mapper处理之后的数据,是按照key排序的数据,key相同的数据都放在连续的行上,如果不适用python hadoop的api,也是简单的利用这个特性,当key变化的时候,就处理上一个key的所有数据即可,然后更新当前处理的key。

reducer的代码(ngram_count_reducer.py):

#!/usr/bin/env python
#coding=utf-8
import sys
import os
#ngram统计,reducer部分

current_word = None
current_count = 0

for line in sys.stdin:
    line = line.strip()
    (word,count) = line.split("\t")
    count = int(count)
    if current_word == word:#如果词没有变,则累加
        current_count = current_count + count
    else:#如果词发生变化了,则输出
        if current_word != None: 
            #如果读的是第一行,和 None不一致,但不输出,不是第一行才输出
            #输出上一个词
            print current_word + "\t" + str(current_count)
        #初始化current_word,current_count为当前词
        current_word = word
        current_count = count

#最后一行处理,如果是和前面一行的词,后续没有词了,则前面没有输出
print current_word + "\t" + str(current_count)
  然后就可以运行了,先用一个小数据测试下:

数据简单写几行,看看处理结果是否正确即可,相当于单元测试:

测试数据(test1.dat)

12345
abcde
1abc2

然后运行本地测试,写一个脚本(local_run.sh)

#!/bin/sh
test_data=test1.dat
mapper=./ngram_count_mapper.py
reducer=./ngram_count_reducer.py

cat ${test_data}|${mapper}|sort -k1,1|${reducer}

跑hadoop的脚本也基本类似,首先要把测试数据test1.dat放到你的hadoop文件中,然后执行即可,这里数据路径要写成你自己的:)

hadoop 运行脚本(hadoop_run.sh)

#!/bin/sh
hadoop fs -rmr mariswang/output
hadoop org.apache.hadoop.streaming.HadoopStreaming \
-input mariswang/data/test1.dat \
-output mariswang/output \
-mapper "ngram_count_mapper.py" \
-reducer "ngram_count_reducer.py" \
-file ngram_count_mapper.py \
-file ngram_count_reducer.py \
-jobconf mapred.reduce.tasks=1\
-jobconf mapred.job.name="mariswang_count"
#get data
hadoop fs -get mariswang/output/part-00000 ./

测试数据没问题了,就可以跑上一篇文章中的正式数据了。

跑出来的数据有18G,大概13亿个候选词,显然要处理这些词还是太多了,我们就大概选一个词频阈值,大于这个阈值的才进行进一步的处理,这选择40做为阈值。

过滤之后的词大概有1千万+,大概只有原来的1%。我们重点关注头部的词条,从词频最高到低对这些片段就行排序,以10万个为间隔,看这些区间内的片段属于标准词典的比例,具体如下图所示:


可以看到,词频最高的10万个片段中,有个将近27%的都是词典中的词,而第二个区间内,有10%的片段是词,而到了第10个区间,就只有不到3%的片段是词典中的词了,因此,词频是区分一个ngram片段是否是词的非常好的特征。

而具体到词频最高的10个片段中,频度最高的top1万的片段中,有67%都是词,后续的1万个词中,有40%。

词典里面总共有30万+个词,ngram切分的频度最高的前30万个片段中包含其中4万个词,钱100万个片段,包括其中17万个词。

如果我们只关注最常用的3万个词,也就是词频最高的前3万个词,则可以看到,ngram切分频度最高的前30万个片段中,包括其中1.8万个词,前100万个片段,包括2.2万个。更进一步的,只看最常用的1万个词,前30万个词,包括其中8100个词,前100万个词,包括9400个词。

因此我们后续只需要关注前100万个频率最高的ngram片段即可,因为选择更多的片段,可以小幅度增加覆盖率,但准确率明显会下降很多,会对后续的方法造成很大的困难,因此,在第一步,就为后续的步骤做好数据过滤关。

现在我们有100万个ngram片段,后续主要就是从中找出是词的片段。


你可能感兴趣的:(分词)