赞
踩
本文以电商领域的商品名称为语料进行实验,来寻找未登录词。
首先,将json格式的数据,提取其goods_name
列,写入到txt文件中。
import pandas as pd """ 将数据中的goods_name列与search_value列进行去重后再分别写入到txt """ class DataConvert(object): def __init__(self, file_input_name, file_corpus, file_searchValue): self.file_input_name = file_input_name self.file_corpus = file_corpus self.file_searchValue = file_searchValue def run(self): # 读取原始数据 # lines=True:文件的每一行为一个完整的字典,默认是一个列表中包含很多字典 input_file = pd.read_json(self.file_input_name, lines=True) # 选定需要操作的两列,并进行去重 goods_names = input_file.loc[:, 'goods_name'].dropna().drop_duplicates().tolist() search_values = input_file.loc[:, 'search_value'].dropna().drop_duplicates().tolist() # 将这两列写入到输出文件中 with open(self.file_corpus, "w", encoding="utf-8") as f1: for goods_name in goods_names: try: f1.write(goods_name) f1.write("\n") except: print(goods_name) f1.close() with open(self.file_searchValue, "w", encoding="utf-8") as f2: for search_value in search_values: f2.write(search_value) f2.write("\n") f2.close()
得到的file_corpus格式如下:
统计语料库中出现单字,双字的频率,前后链接的字相关信息;
#!usr/bin/env python # -*- coding:utf-8 -*- """ 统计语料库中出现单字,双字的频率,前后链接的字相关信息; """ import re import codecs import json import os # \u4E00-\u9FD5表示所有汉字 # a-zA-Z0-9表示26个英文字母与数字 # -+#&\._/ \(\) \~\'表示常用符号 # \u03bc\u3001 \uff08\uff09 \u2019:表示特殊字符,一些程式码 re_han = re.compile("([\u4E00-\u9FD5a-zA-Z0-9-+#&\._/\u03bc\u3001\(\)\uff08\uff09\~\'\u2019]+)", re.U) class Finding(object): def __init__(self, file_corpus, file_count, count): self.file_corpus = file_corpus self.file_count = file_count self.count = count def split_text(self, sentence): """ 找到每个商品名称中符合正则表达式的子串,以列表形式返回 :param sentence: :return: """ # re.findall():在字符串中找到正则表达式所匹配的所有子串,并返回一个列表 seglist = re_han.findall(sentence) return seglist def count_word(self, seglist, k): """ 遍历每个子串,返回窗口对应的词与前后各一个词 :param seglist: 商品名称的子串列表 :param k: 窗口大小 :return: """ for words in seglist: ln = len(words) i = 0 j = 0 if words: while 1: j = i + k if j <= ln: word = words[i:j] if i == 0: lword = 'S' else: lword = words[i - 1:i] if j == ln: rword = 'E' else: rword = words[j:j + 1] i += 1 yield word, lword, rword else: break def find_word(self): """ :return: """ # 读取语料数据 input_data = codecs.open(self.file_corpus, 'r', encoding='utf-8') dataset = {} # enumerate将可迭代对象转化为索引与数据的格式,起始下标为1 for lineno, line in enumerate(input_data, 1): try: line = line.strip() # 找到每个商品名称中符合正则表达式的子串,以列表形式返回 seglist = self.split_text(line) # count_word:遍历每个子串,返回窗口对应的词与前后各一个词 # 遍历这三个词,[[], {}, {}]分别记录窗口对应的词出现的个数,前一个词及出现个数,后一个词及出现个数 for w, lw, rw in self.count_word(seglist, self.count): if w not in dataset: dataset.setdefault(w, [[], {}, {}]) dataset[w][0] = 1 else: dataset[w][0] += 1 if lw: dataset[w][1][lw] = dataset[w][1].get(lw, 0) + 1 if rw: dataset[w][2][rw] = dataset[w][2].get(rw, 0) + 1 except: pass self.write_data(dataset) def write_data(self, dataset): """ 将统计结果写入到字典中 :param dataset: :return: """ output_data = codecs.open(self.file_count, 'w', encoding='utf-8') for word in dataset: output_data.write(word + '\t' + json.dumps(dataset[word], ensure_ascii=False, sort_keys=False) + '\n') output_data.close()
这一步会生成两个文件,分别是
count_one.txt
count_two.txt
文件。
对统计出的单字和双字的结果,使用互信熵,选择大于阈值K=的词加入词库,作为初始词库。在计算机领域,更常用的是点间互信息,点间互信息计算了两个具体事件之间的互信息。 点间互信息的定义如下:
本文操作时,选择以e为底。
#!usr/bin/env python # -*- coding:utf-8 -*- """ 对统计出的单字和双字的结果,使用互信熵,选择大于阈值K=的词加入词库,作为初始词库; """ import codecs import json import math def load_data(file_count_one): """ 加载单字文件:{one_word,[[], {}, {}]},并返回总词数与单字字典:{one_word:one_word_freq} :param file_count_one:文件名 :return: """ count_one_data = codecs.open(file_count_one, 'r', encoding='utf-8') count_one_param = {} N = 0 # 遍历每一行,返回总词数与单字字典:{one_word:one_word_freq} for line in count_one_data.readlines(): line = line.strip() line = line.split('\t') try: word = line[0] value = json.loads(line[1]) N += value[0] count_one_param[word] = int(value[0]) except: pass count_one_data.close() return N, count_one_param def select(file_count_one, file_count_two, file_dict, K=10.8): """ 遍历每一行,利用互信息熵计算每个词的成词概率PMI,利用阈值K来筛选一部分词,将结果保存到file_dict字典中 :param file_count_one: 窗口为1的文件 :param file_count_two: 窗口为2的文件 :param file_dict: 字典文件 :param K: 稳定词的阈值 :return: """ count_two_data = codecs.open(file_count_two, 'r', encoding='utf-8') # 总词数与单字字典 N, count_one_param = load_data(file_count_one) count_two_param = {} # 遍历每一行,利用互信息熵计算每个词的成词概率PMI for line in count_two_data.readlines(): line = line.strip() line = line.split('\t') try: word = line[0] value = json.loads(line[1]) # 双字出现的词频 / 总的字数 P_w = 1.0 * value[0] / N # 双字的第一个字出现的词频 / 总的字数 P_w1 = 1.0 * count_one_param.get(word[0], 1) / N # 双字的第二个字出现的词频 / 总的字数 P_w2 = 1.0 * count_one_param.get(word[1], 1) / N # 计算点间互信息PMI的计算公式,两个字的成词概率越大,则PMI值越大;如果两个子不相关,则PMI=0 mi = math.log(P_w / (P_w1 * P_w2)) count_two_param[word] = mi except: pass select_two_param = [] for w in count_two_param: mi = count_two_param[w] if mi > K: select_two_param.append(w) with codecs.open(file_dict, 'a', encoding='utf-8') as f: for w in select_two_param: f.write(w + '\t' + 'org' + '\n') count_two_data.close()
这一步会生成初始词库文件dict.txt
,后续发现的新词也会追加到这个文件中,构成新的词库文件。
这里我从业务端获得了几万条初始手工添加的词。考虑到初始已经有许多词,所以,互信息熵的阈值K
设置的可以大一点。如果没有的话,K
会对最后结果起决定作用。所使用的材料是长文本还是短文本也会对K
有影响。可以尝试初始设置为K=8
来运行程序。
有了初始词库,使用正向最大匹配,对语料库进行切分,对切分出来的字串按频率排序输出并记下数量seg_num
。
""" 有了初始词库,使用正向最大匹配,对语料库进行切分,对切分出来的字串按频率排序输出并记下数量seg_num """ from __future__ import unicode_literals import codecs import re # 匹配所有汉字与规定符号 re_han_cut = re.compile("([\u4E00-\u9FD5a-zA-Z0-9-+#&\._/\u03bc\u3001\(\)\uff08\uff09\~\'\u2019]+)", re.U) # 匹配所有汉字 re_han = re.compile("([\u4E00-\u9FD5]+)", re.U) class Cuting(object): def __init__(self, file_corpus, file_dict, file_segment): self.file_corpus = file_corpus self.file_dict = file_dict self.file_segment = file_segment self.wdict = {} self.get_dict() def get_dict(self): """ 遍历初始字典文件,初始化wdict字典 => {one_word:[many two world]} :return: """ f = codecs.open(self.file_dict, 'r', encoding='utf-8') # 遍历每一行数据 for lineno, line in enumerate(f, 1): line = line.strip() line = line.split('\t') w = line[0] if w: if w[0] in self.wdict: value = self.wdict[w[0]] value.append(w) self.wdict[w[0]] = value else: self.wdict[w[0]] = [w] def fmm(self, sentence): """ 将子串中在wdict中two_word保存到result表中 :param sentence: 字符子串 :return: """ N = len(sentence) k = 0 result = [] while k < N: w = sentence[k] maxlen = 1 # 如果这个字在初始化字典wdict中 if w in self.wdict: # 初始化字典中的value,是一个列表,里面有许多上一步找到的two_word words = self.wdict[w] t = '' for item in words: itemlen = len(item) if sentence[k:k + itemlen] == item and itemlen >= maxlen: t = item maxlen = itemlen if t and t not in result: result.append(t) k = k + maxlen return result def judge(self, words): """ 判断这个子串是否只有汉字 :param words: :return: """ flag = False n = len(''.join(re_han.findall(words))) if n == len(words): flag = True return flag def cut(self, sentence): """ :param sentence: :return: """ buf = [] # 在商品名称中找到正则表达式所匹配的所有子串,并返回一个列表 blocks = re_han_cut.findall(sentence) # 遍历每一个子串 for blk in blocks: if blk: # 将子串中在wdict中的two_word返回 fm = self.fmm(blk) if fm: try: # 如果返回值不为空,则将这些词以“|”进行拼接,并以此构建正则表达式 re_split = re.compile('|'.join(fm)) # split方法按照能够匹配的子串将字符串分割后返回列表 for s in re_split.split(blk): # 如果这个子串只有汉字,则将该子串添加到列表中,最终返回该列表 if s and self.judge(s): buf.append(s) except: pass return buf def find(self): """ :return: """ input_data = codecs.open(self.file_corpus, 'r', encoding='utf-8') output_data = codecs.open(self.file_segment, 'w', encoding='utf-8') dataset = {} # 遍历每一个语料文件 for lineno, line in enumerate(input_data, 1): line = line.strip() # 遍历基于正则切割的字符串列表 for w in self.cut(line): if len(w) >= 2: dataset[w] = dataset.get(w, 0) + 1 # 基于词频进行排序 data_two = sorted(dataset.items(), key=lambda d: d[1], reverse=True) seg_num = len(data_two) for key in data_two: output_data.write(key[0] + '\t' + str(key[1]) + '\n') print('Having segment %d words' % seg_num) input_data.close() output_data.close() return seg_num
这一步会生成片段语料文件file_segment.txt
片段语料文件会根据初始词库的迭代更新而变化。
对切分产生的字串按频率排序,前H=2000的字串进行搜索引擎(百度)。若字串是“百度百科”收录词条,将该字串作为词加入词库;或者在搜索页面的文本中出现的次数超过60,也将该字串作为词加入词库;
#!usr/bin/env python # -*- coding:utf-8 -*- """ 对切分产生的字串按频率排序,前H=2000的字串进行搜索引擎(百度), 若字串是“百度百科”收录词条,将该字串作为词加入词库, 或者在搜索页面的文本中出现的次数超过60,也将该字串作为词加入词库; """ import requests from lxml import etree import codecs import re def search(file_segment, file_dict, H, R, iternum): # headers,从网站的检查中获取 headers = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate, sdch, b', 'Accept-Language': 'zh-CN,zh;q=0.8', 'Cache-Control': 'max-age=0', 'Connection': 'keep-alive', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.' } # 加载切分出来的子符串 input_data = codecs.open(file_segment, 'r', encoding='utf-8') read_data = input_data.readlines() N = len(read_data) if H > N: H = N output_data = codecs.open(file_dict, 'a', encoding='utf-8') n = 0 m = 1 # 遍历切分出的子符串 for line in read_data[:H]: line = line.rstrip() line = line.split('\t') # 字符串 word = line[0] try: # 访问百度百科词条 urlbase = 'https://www.baidu.com/s?wd=' + word dom = requests.get(urlbase, headers=headers) ct = dom.text # 在搜索页面的文本中出现的次数 num = ct.count(word) html = dom.content selector = etree.HTML(html) flag = False # 若字串是“百度百科”收录词条,将该字串作为词加入词库 if selector.xpath('//h3[@class="t c-gap-bottom-small"]'): ct = ''.join(selector.xpath('//h3[@class="t c-gap-bottom-small"]//text()')) lable = re.findall(u'(.*)_百度百科', ct) for w in lable: w = w.strip() if w == word: flag = True if flag: output_data.write(word + '\titer_' + str(iternum) + '\n') n += 1 # 在搜索页面的文本中出现的次数超过阈值R=60,也将该字串作为词加入词库 else: if num >= R: output_data.write(word + '\titer_' + str(iternum) + '\n') n += 1 m += 1 if m % 100 == 0: print('having crawl %dth word\n' % m) except: pass print('Having add %d words to file_dict at iter_%d' % (n, iternum)) input_data.close() output_data.close() return n
这一步会将发现的新词添加到dict.txt
文件中。
更新词库后,重复step3,step4进行迭代。当searh_num=0时,结束迭代;当seg_num小于设定的Y=3000,进行最后一次step4,并H设定为H=seg_num,执行完后结束迭代,最后词库就是本程序所找的词。
#!usr/bin/env python # -*- coding:utf-8 -*- """ 算法步骤: 1.统计语料库中出现单字,双字的频率,前后链接的字相关信息; 2.对统计出的单字和双字的结果,使用互信熵,选择大于阈值K=的词加入词库,作为初始词库; 3.有了初始词库,使用正向最大匹配,对语料库进行切分,对切分出来的字串按频率排序输出并记下数量seg_num 4.对切分产生的字串按频率排序,前H=5000的字串进行搜索引擎(百度), 若字串是“百度百科”收录词条,将该字串作为词加入词库, 或者在搜索页面的文本中出现的次数超过60,也将该字串作为词加入词库; 5.更新词库后,重复step3,step4进行迭代,,当searh_num=0时,结束迭代; 当seg_num小于设定的Y=1000,进行最后一次step4,并H设定为H=seg_num,执行完后结束迭代, 最后词库就是本程序所找的词 """ from __future__ import absolute_import __version__ = '1.0' __license__ = 'MIT' import os import logging import time import codecs import sys from module.corpus_count import * from module.corpus_segment import * from module.select_model import * from module.words_search import * # 获取当前路径 medfw_path = os.getcwd() file_corpus = medfw_path + '/data_org/file_corpus.txt' file_dict = medfw_path + '/data_org/dict.txt' file_count_one = medfw_path + '/data_org/count_one.txt' file_count_two = medfw_path + '/data_org/count_two.txt' file_segment = medfw_path + '/data_org/file_segment.txt' # 日志设置 log_console = logging.StreamHandler(sys.stderr) default_logger = logging.getLogger(__name__) default_logger.setLevel(logging.DEBUG) default_logger.addHandler(log_console) def setLogLevel(log_level): global logger default_logger.setLevel(log_level) class MedFW(object): def __init__(self, K=10, H=2000, R=60, Y=5000): self.K = K # 互信息熵的阈值 self.H = H # 取file_segment.txt前多少个 self.R = R # 片段单词在搜索引擎出现的阈值 self.Y = Y # 迭代结束结束条件参数 self.seg_num = 0 # 片段语料库的数量 self.search_num = 0 # 搜索引擎向 file_dict 添加单词的数量 # step1: 统计语料库中出现单字,双字的频率,前后链接的字相关信息; def medfw_s1(self): for i in range(1, 3): if i == 1: file_count = file_count_one else: file_count = file_count_two default_logger.debug("Counting courpus to get %s...\n" % (file_count)) t1 = time.time() cc = Finding(file_corpus, file_count, i) cc.find_word() default_logger.debug("Getting %s cost %.3f seconds...\n" % (file_count, time.time() - t1)) # step2: 对统计出的单字和双字的结果,使用互信熵,选择大于阈值K=的词加入词库,作为初始词库; def medfw_s2(self): default_logger.debug("Select stable words and generate initial vocabulary... \n") select(file_count_one, file_count_two, file_dict, self.K) # step3: 有了初始词库,使用正向最大匹配,对语料库进行切分,对切分出来的字串按频率排序输出并记下数量seg_num def medfw_s3(self): t1 = time.time() sc = Cuting(file_corpus, file_dict, file_segment) self.seg_num = sc.find() default_logger.debug("Segment corpuscost %.3f seconds...\n" % (time.time() - t1)) # step4:对片段语料中的单词使用搜索引擎进行搜索 def medfw_s4(self, H, R, iternum): t1 = time.time() self.search_num = search(file_segment, file_dict, H, R, iternum) default_logger.debug("Select words cost %.3f seconds...\n" % (time.time() - t1)) # 主程序 def medfw(self): # default_logger.debug("Starting to find words and do step1...\n" ) print('-----------------------------------') print('step1:count corpus') self.medfw_s1() print('-----------------------------------') print('step2:select stable words and generate initial vocabulary') self.medfw_s2() print('-----------------------------------') print('step3:use initial vocabulary to segment corpus') self.medfw_s3() print('-----------------------------------') print('step4:use search engine to select words of segment corpus') self.medfw_s4(H=self.H, R=self.R, iternum=0) print('-----------------------------------') print('step5:cycling iteration') iter_num = 1 while True: if self.search_num: default_logger.debug("Itering %d...\n" % (iter_num)) t1 = time.time() self.medfw_s3() print("---------------------- seg_num:%s -----------------------" % self.seg_num) if self.seg_num <= self.Y: self.H = self.seg_num self.medfw_s4(H=self.H, R=self.R, iternum=iter_num) default_logger.debug("Ending the iteration ...\n") break else: self.medfw_s4(H=self.H, R=self.R, iternum=iter_num) iter_num += 1 default_logger.debug("Itering %d cost %.3f seconds...\n " % ((iter_num - 1), time.time() - t1)) else: break with codecs.open(file_dict, 'r', encoding='utf-8') as f: total_num = len(f.readlines()) print('Having succcessfuly find %d words from corpus ' % total_num) if __name__ == '__main__': md = MedFW(K=10, H=3000, R=50, Y=3000) md.medfw()
这一步会多轮迭代寻找新词。
实践表明:这种思路获得的结果有一定用处,获得的结果需要人工来甄别。缺点的话也很明显,比如一个长的品牌名或者商品名,如果其中几个连续的词的词频很高,并且本身也能成词,就会将这个品牌名或者商品名切散!还有待继续研究!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。