分享一次专业领域词汇的无监督挖掘
By 苏剑林 | 2019-04-10 | 85204位读者 |去年 Data Fountain 曾举办了一个“电力专业领域词汇挖掘”的比赛,该比赛有意思的地方在于它是一个“无监督”的比赛,也就是说它考验的是从大量的语料中无监督挖掘专业词汇的能力。
这个显然确实是工业界比较有价值的一个能力,又想着我之前也在无监督新词发现中做过一定的研究,加之“无监督比赛”的新颖性,所以当时毫不犹豫地参加了,然而最终排名并不靠前~
不管怎样,还是分享一下我自己的做法,这是一个真正意义上的无监督做法,也许会对部分读者有些参考价值。
基准对比 #
首先,新词发现部分,用到了我自己写的库nlp zero,基本思路是先分别对“比赛所给语料”、“自己爬的一部分百科百科语料”做新词发现,然后两者进行对比,就能找到一批“比赛所给语料”的特征词。
参考的源码是:
from nlp_zero import *
import re
import pandas as pd
import pymongo
import logging
logging.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 l
class 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) * 0
words = WORDS + total_zeros
other_words = OTHER_WORDS + total_zeros
total = words + other_words
alpha = 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 Word2Vec
word_size = 100
word2vec = 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 np
from multiprocessing.dummy import Queue
def 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(w) >= 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'作者']\
and not (u'博士' in w or u'硕士' in w or u'研究生' in w)\
and not (len(set(w)) == 1 and len(w) > 1)\
and not (w[0] in u'一二三四五六七八九十' and len(w) == 2)\
and re.findall(u'[^一七厂月二夕气产兰丫田洲户尹尸甲乙日卜几口工旧门目曰石闷匕勺]', w)\
and not u'进一步' in w:
return True
else:
return False
至此,我们就可以完整地执行这个算法了:
# 种子词,在第一步得到的词表中的前面部分挑一挑即可,不需要特别准
start_words = [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'传输', u'检修', u'模拟', u'高频', u'测量', u'样本', u'高级工程师', u'变换', u'试样', u'试验研究', u'平均值', u'向量', u'特征值', u'导体', u'电晕', u'磁通', u'千伏', u'切换', u'响应', u'效率']
cluster_words = find_words(start_words, min_sim=0.6, alpha=0.35)
result2 = result[cluster_words].sort_values(ascending=False)
idxs = [i for i in result2.index if is_good(i)]
pd.Series([i for i in idxs if len(i) > 2][:10000]).to_csv('result_1_2.csv', encoding='utf-8', header=None, index=None)
最终结果(部分):
变压器
发电机
变电站
过电压
可靠性
控制器
断路器
分布式
输电线路
数学模型
滤波器
电容器
故障诊断
神经网络
直流电压
等离子体
联络线
传感器
汽轮机
晶闸管
电动机
约束条件
数据库
可行性
持续时间
整流器
稳定性
调节器
电磁场
后记感想 #
本文的算法在榜上的成绩大约是0.22左右,封榜时排在100名左右,榜首已经是0.49了,所以从成绩来看其实没什么值得炫耀的。不过当时听说不少人拿现成的专业词典去做字标注,所以当时就没做下去了。要是真的那样子的话,我觉得就很没意思了...
总之,本文算是提供了一个无监督抽取专业词的实现模版,如果读者觉得有可取之处,大方取之即可;如果觉得一无是处,敬请无视它~
转载到请包括本文地址:https://kexue.fm/archives/6540
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Apr. 10, 2019). 《分享一次专业领域词汇的无监督挖掘 》[Blog post]. Retrieved from https://kexue.fm/archives/6540
@online{kexuefm-6540,
title={分享一次专业领域词汇的无监督挖掘},
author={苏剑林},
year={2019},
month={Apr},
url={\url{https://kexue.fm/archives/6540}},
}
June 23rd, 2021
苏神,请教一下。为什么先做词表再分词呢,我再自己的数据上跑的结果感觉词表比较粗糙,还是说必须要写许多规则进行筛选呢。感谢
没有词表如何分词?这篇文章就是希望从零开始的啊,不能用现成词表。