作者苏剑林
广州火焰信息技术有限公司
研究方向丨NLP,神经网络
个人主页丨kexue.fm
去年DataFountain曾经举办过一个有趣的是,这场比赛是一场无监督的比赛,也就是说,它测试了从大量语料中挖掘专业词汇的能力。
https://www.datafountain.cn/competitions/320/details
这确实是界确实是一种有价值的能力,我认为我以前在没有监督的新词发现中做过一些研究,加上没有监督的竞争的新颖性,所以我毫不犹豫地参与了,但最终的排名并不高。
无论如何,分享自己的做法是一种真正意义上的无监督做法,可能对一些读者有一定的参考价值。
基准对比
首先,新词发现部分,用到了我自己写的库 NLP Zero,基本思路是先分别对“比赛所给语料”、“自己爬的一部分百科百科语料”做新词发现,然后两者进行对比,就能找到一批“比赛所给语料”的特征词。
https://kexue.fm/archives/5597
参考的源码是:
from nlp_zero import *import reimport pandas as pdimport pymongoimport logginglogging.basicConfig(level = logging.INFO, format = '%(asctime)s - %(name)s - %(message)s')class D: # 读取比赛方所给语料 def __iter__(self): with open('data.txt') as f: for l in f: l = l.strip().decode('utf-8') l = re.sub(u'[^\u4e00-\u9fa5]+', ' ', l) yield lclass DO: # 读取自己的语料(相当于平行语料) def __iter__(self): db = pymongo.MongoClient().baike.items for i in db.find().limit(300000): l = i['content'] l = re.sub(u'[^\u4e00-\u9fa5]+', ' ', l) yield l# 在比赛方语料中做新词发现f = Word_Finder(min_proba=1e-6, min_pmi=0.5)f.train(D()) # 统计互信息f.find(D()) # 构建词库# 导出词表words = pd.Series(f.words).sort_values(ascending=False)# 在自己的语料中做新词发现fo = Word_Finder(min_proba=1e-6, min_pmi=0.5)fo.train(DO()) # 统计互信息fo.find(DO()) # 构建词库# 导出词表other_words = pd.Series(fo.words).sort_values(ascending=False)other_words = other_words / other_words.sum() * words.sum() # 总词频归一化(这样才便于对比)"""对比两份语料词频,得到特征词。对比指标是(比赛方语料的词频 + alpha)/(自己语料的词频 + beta);alpha和beta的计算参考自 http://www.matrix67.com/blog/archives/5044"""WORDS = words.copy()OTHER_WORDS = other_words.copy()total_zeros = (WORDS + OTHER_WORDS).fillna(0) * 0words = WORDS + total_zerosother_words = OTHER_WORDS + total_zerostotal = words + other_wordsalpha = words.sum() / total.sum()result = (words + total.mean() * alpha) / (total + total.mean())result = result.sort_values(ascending=False)idxs = [i for i in result.index if len(i) >= 2] # 排除掉单字词# 导出csv格式pd.Series(idxs[:20000]).to_csv('result_1.csv', encoding='utf-8', header=None, index=None)
语义筛选
注意到,按照上述方法导出来的词表,顶多算是“语料特征词”,但是还不完全是“电力专业领域词汇”。如果着眼于电力词汇,那么需要对词表进行语义上的筛选。
我的做法是:用导出来的词表对比赛语料进行分词,然后训练一个 Word2Vec 模型,根据 Word2Vec 得到的词向量来对词进行聚类。
首先是训练 Word2Vec:
# nlp zero提供了良好的封装,可以直到导出一个分词器,词表是新词发现得到的词表。tokenizer = f.export_tokenizer()class DW: def __iter__(self): for l in D(): yield tokenizer.tokenize(l, combine_Aa123=False)from gensim.models import Word2Vecword_size = 100word2vec = Word2Vec(DW(), size=word_size, min_count=2, sg=1, negative=10)
然后是聚类,不过这不是严格意义上的聚类,而是根据我们自己跳出来的若干个种子词,然后找到一批相似词来。算法是用相似的传递性(有点类似基于连通性的聚类算法),即 A 和 B 相似,B 和 C也相似,那么 A、B、C 就聚为一类(哪怕A、C从指标上看是不相似的)。
当然,这样传递下去很可能把整个词表都遍历了,所以要逐步加强对相似的限制。比如 A 是种子词,B、C 都不是种子词,A、B 的相似度为 0.6 就定义它为相似,B、C 的相似度要大于 0.7 才能认为它们相似,不然这样一级级地传递下去,后面的词就会离种子词的语义越来越远。
聚类算法如下:
import numpy as npfrom multiprocessing.dummy import Queuedef most_similar(word, center_vec=None, neg_vec=None): """根据给定词、中心向量和负向量找最相近的词 """ vec = word2vec[word] + center_vec - neg_vec return word2vec.similar_by_vector(vec, topn=200)def find_words(start_words, center_words=None, neg_words=None, min_sim=0.6, max_sim=1., alpha=0.25): if center_words == None and neg_words == None: min_sim = max(min_sim, 0.6) center_vec, neg_vec = np.zeros([word_size]), np.zeros([word_size]) if center_words: # 中心向量是所有种子词向量的平均 _ = 0 for w in center_words: if w in word2vec.wv.vocab: center_vec += word2vec[w] _ += 1 if _ > 0: center_vec /= _ if neg_words: # 负向量是所有负种子词向量的平均(本文没有用到它) _ = 0 for w in neg_words: if w in word2vec.wv.vocab: neg_vec += word2vec[w] _ += 1 if _ > 0: neg_vec /= _ queue_count = 1 task_count = 0 cluster = [] queue = Queue() # 建立队列 for w in start_words: queue.put((0, w)) if w not in cluster: cluster.append(w) while not queue.empty(): idx, word = queue.get() queue_count -= 1 task_count += 1 sims = most_similar(word, center_vec, neg_vec) min_sim_ = min_sim + (max_sim-min_sim) * (1-np.exp(-alpha*idx)) if task_count % 10 == 0: log = '%s in cluster, %s in queue, %s tasks done, %s min_sim'%(len(cluster), queue_count, task_count, min_sim_) print log for i,j in sims: if j >= min_sim_: if i not in cluster and is_good(i): # is_good是人工写的过滤规则 queue.put((idx+1, i)) if i not in cluster and is_good(i): cluster.append(i) queue_count += 1 return cluster
规则过滤
总的来说,无监督算法始终是难以做到完美的,在工程上,常见的方法就是人工观察结果然后手写一些规则来处理。在这个任务中,由于前面是纯无监督的,哪怕进行了语义聚类,还是会出来一些非电力专业词汇(比如“麦克斯韦方程”),甚至还保留一些“非词”,所以我写了一通规则来过滤(写得有点丑):
def is_good(w): if re.findall(u'[\u4e00-\u9fa5]', w) \ and len(i) >= 2\ and not re.findall(u'[较很越增]|[多少大小长短高低好差]', w)\ and not u'的' in w\ and not u'了' in w\ and not u'这' in w\ and not u'那' in w\ and not u'到' in w\ and not w[-1] in u'为一人给内中后省市局院上所在有与及厂稿下厅部商者从奖出'\ and not w[0] in u'每各该个被其从与及当为'\ and not w[-2:] in [u'问题', u'市场', u'邮件', u'合约', u'假设', u'编号', u'预算', u'施加', u'战略', u'状况', u'工作', u'考核', u'评估', u'需求', u'沟通', u'阶段', u'账号', u'意识', u'价值', u'事故', u'竞争', u'交易', u'趋势', u'主任', u'价格', u'门户', u'治区', u'培养', u'职责', u'社会', u'主义', u'办法', u'干部', u'员会', u'商务', u'发展', u'原因', u'情况', u'国家', u'园区', u'伙伴', u'对手', u'目标', u'委员', u'人员', u'如下', u'况下', u'见图', u'全国', u'创新', u'共享', u'资讯', u'队伍', u'农村', u'贡献', u'争力', u'地区', u'客户', u'领域', u'查询', u'应用', u'可以', u'运营', u'成员', u'书记', u'附近', u'结果', u'经理', u'学位', u'经营', u'思想', u'监管', u'能力', u'责任', u'意见', u'精神', u'讲话', u'营销', u'业务', u'总裁', u'见表', u'电力', u'主编', u'作者', u'专辑', u'学报', u'创建', u'支持', u'资助', u'规划', u'计划', u'资金', u'代表', u'部门', u'版社', u'表明', u'证明', u'专家', u'教授', u'教师', u'基金', u'如图', u'位于', u'从事', u'公司', u'企业', u'专业', u'思路', u'集团', u'建设', u'管理', u'水平', u'领导', u'体系', u'政务', u'单位', u'部分', u'董事', u'院士', u'经济', u'意义', u'内部', u'项目', u'建设', u'服务', u'总部', u'管理', u'讨论', u'改进', u'文献']\ and not w[:2] in [u'考虑', u'图中', u'每个', u'出席', u'一个', u'随着', u'不会', u'本次', u'产生', u'查询', u'是否', u'作者']\