我们现在面对的是“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的代码(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 运行脚本(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片段,后续主要就是从中找出是词的片段。