介绍
本文尝试从几个方面来介绍提取关键词所知的技术,以及关键词提取所遇到的问题,接着介绍SIFRank-zh算法,最后穿插下个人的理解与总结。
关键词提取技术
刚开始接触这个概念的时候,网上一大堆介绍TF-IDF和TextRank算法,这俩简直已经称为了关键词提取的baseline。
关于TF-IDF,的确在许多文档中已经作为了baseline来和其他技术相对比,是一种简单易行并且效果不差的无监督技术。
TextRank具体我没看,此处略过。
关键词提取步骤
- 候选词提取
- 排序
候选词提取所遇到的问题
那么,此处引入一个问题,什么叫关键词?换句话说,什么样的词我们认为是关键词?
比如一句话:从2021年11月1日起,南京各个社区将尝试采取网格化管理,增强人民群众安全。
,不同分词器的结果如下:
lac的分词结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19[('从', 'p'),
('2021年11月1', 'TIME'),
('日起', 'q'),
(',', 'w'),
('南京', 'LOC'),
('各个', 'r'),
('社区', 'n'),
('将', 'd'),
('尝试', 'v'),
('采取', 'v'),
('网格化', 'vn'),
('管理', 'vn'),
(',', 'w'),
('增强', 'v'),
('人民群众', 'n'),
('安全', 'an'),
('。', 'w')]ltpV3版本的分词结果:
1
2
3
4['从', '2021年', '11月', '1日', '起', ',',
'南京', '各个', '社区', '将', '尝试', '采取',
'网格化', '管理', ',', '增强', '人民', '群众', '安全', '。'
]
这里我想突出一个问题,目前市面所见的所有分词器都是采用细粒度分词
,那这就导致一个最直接的问题,就是关键信息词被拆开了。
这句话本质想突出网格化管理
这个词,但是在两个分词器中都将其分成了两个词。
你可以说NER可以解决这个问题哇,从技术角度本身来讲,这的确是可行的。比如时间短语,机构短语,地址短语。
你也可以说数据标注标准不同,比如msra或者ptb数据集。
你也可以说粗粒度分词会更容易产生歧义。比如白天鹅在湖中游
等等。这条不太认同。
你也可以说粗粒度分词下如果想获取更细粒度分词则无法获得。这条我认为可有可无。
但是我想说的是,那能不能有一个分词器,从使用效果上来讲更贴切人直观感受,不需要关注底层分词和NER这两种技术的。
可惜没有!
之前我看腾讯开源了一个训练好的word2vec模型,在Simple Cases那里,简直看到了希望!!
当时就在想,怎么根据word2vec反推出一个分词?
过程就是分词和word2vec训练放到一个任务中,但是只保存分词的模型。
然后在脑里想分词实现技术有哪些,首先肯定排除掉最长距离或者最短距离分词,那么能想到的只有HMM和CRF。
不管HMM还是CRF,本质都是计算状态概率转移矩阵和发射概率矩阵,HMM多了个初始概率矩阵,这个还好。
但是想了半天,貌似都不太可行,为什么?
- 每个句子到底有多少种切分方式,就决定了有多少训练可能性,数据集无法确定。比如
商品和服务
,有['商品', '和', '服务']
以及['商品', '和服', '务']
,仅仅这个例子,就有两种可能性,如果一个句子过长的话甚者有很多中语料的话,就无法估算那个语料才是真正正确的那个。那么在无法估计正确语料这条路来讲,crf就不可行。 - 对于hmm来讲,我记得使用统计的方式来做,印象中记得有一阶词频,还有二阶词频。。。emmmm,哈哈,忘完了,不管怎么说,会形成一个基于词的有向无环图。每次分词在这个图上使用vertbi算法进行迭代获取最终分词效果。但是问题是,怎么估算这些参数并应用于分词上且将其应用到word2vec训练预料并产生最终的word2vec模型。
emmmmm,但是我觉得可以换条思路来实现,那就是用于辅助标注人员的方式来重标数据集,或者对标注人员说,我更倾向于使用粗粒度分词的。
额,说了这么些,感觉貌似又回到了起点。
但是是一劳永逸的一种方式。
候选词提取可参考的方式
上面介绍了这么多,本质来讲是想提取更粗粒度的,那么我们基于现有的条件下,可以借鉴的实现方式:
- 基于词性进行聚合。比如名词和名词聚合,动名词和名词聚合,以及一些自定义情况则进行聚合,如果聚合后在一个粗粒度分词词典中,那么权重应更高。
- 添加自定义词典。在适用于自己产品的方向上添加自定义词典,也是比较合适的实现方式。
排序
排序的本质是正确代表语句或文本的真实意图或者需要涵盖的方面点。从无监督方式出发的话,我们能够利用的特征:
- 位置信息
- 预训练模型
- 领域信息
SIFRank以及SIFRank+的实现
SIFRank的实现主要分为以下步骤:
- 初始化elmo,thunlp(添加自定义词典)
- 分词,停用词处理,提取候选关键词,通过正则表达
- 分句,按句号和长度16来分。
- 获取elmo每层的输出。
- 对每个token取elmo第一层的平均值,取代掉elmo最后一层。
- 将不同句中elmo的hidden_size进行concat到一个维度上。
- 给予不同词不同的权重,比如停用词为0,符号为0,以及处理oov情况。
- 获取整段话的每层平均向量。
- 获取候选词的每层平均向量。
- 获取每个候选词与整段话的余弦距离。排序
- 获取positional score。 (SIFRank+)
接下来会对其实现做更具体说明。
关于elmo和ELMoForManyLangs
ELMoForManyLangs,SIFRank_zh作者指出的关于:
1 | 哈工大的elmoformanylangs 0.0.3中有个较为明显的问题,当返回所有层Embeddings的时候代码写错了,当output_layer=-2时并不是返回所有层的向量,只是返回了倒数第二层的。问题讨论在这里#31 |
已经解决,故可以忽略。
测试代码以官方test.py为准。
初始化elmo,thunlp(添加自定义词典)
1 | #download from https://github.com/HIT-SCIR/ELMoForManyLangs |
1 | class SentEmbeddings(): |
1 | def get_word_weight(weightfile="", weightpara=2.7e-4): |
这地方主要干了初始化elmo和以jieba分词统计的词频为主进行获取词的权重。
- 分词使用thunlp,但是以jieba同级的词频为主,这地方不合适。
- 这样获取词的权重,也不太合适。是否可以使用tf-idf呢。
分词,停用词处理,提取候选关键词,通过正则表达
1 | zh_model = thulac.thulac(model_path=r'../auxiliary_data/thulac.models/',user_dict=r'../auxiliary_data/user_dict.txt') |
1 | class InputTextObj: |
1 | GRAMMAR_zh = """ NP: |
- 分词
- 处理停用词和特殊符号词性
- 根据目标词性通过正则匹配获取候选词
阶段小结
到这里位置作者完成了对候选词的处理,还是回到最上面讲候选词获取那里的问题,如果有一个更合适的分词器。
- 那么这里我们就可以重新获取dict.txt。
- 不需要这么复杂的正则表达式来获取候选词。
- 接下来的获取候选词词向量那里也会更适合。
另外一个方面,关于正则匹配那:
- 我们可以不局限于词性,也可以根据词的信息来合并,比如’和’,‘的’等字。
- 针对特定词进行对分词器做调整。
分句,按句号和长度16来分
1 | def get_sent_segmented(tokens): |
但是想不明白的是,干嘛不直接用一个更成熟的分句工具呢,比如ltp和hanlp中都有现成的。
这一步和获取elmo每层输出作者将其称为文档分割(document segmentation,DS)
,其作用如下:
1 | DS:通过将文档分为较短且完整的句子(如16个词左右),并行计算来加速ELMo; |
获取elmo每层的输出
注意,下面将不区分elmo和ELMoForManyLangs,如果不做说明,则统一为ELMoForManyLangs。
1 | def get_tokenized_words_embeddings(self, sents_tokened): |
elmo输出为三层结构,这个可以在ELMoForManyLangs中看到,
1 | output_layer: the target layer to output. |
比如sents_tokened长度为[44, 110],输出结果elmo_embedding为torch.Size([2, 3, 110, 1024])
。
对每个token取elmo第一层的平均值,取代掉elmo最后一层
1 | def context_embeddings_alignment(elmo_embeddings, tokens_segmented): |
这地方做了三件事情:
- 获取每个词对应的词向量,将相同词的向量append到一起。
- 求每个词的平均词向量
- 替换掉elmo最后一层的词对应的词向量
这一步作者叫做词向量对齐(embeddings alignment,EA)
,其作用如下:
1 | EA:同时利用锚点词向量对不同句子中的相同词的词向量进行对齐,来稳定同一词在相同语境下的词向量表示。 |
将不同句中elmo的hidden_size进行concat到一个维度上
1 | def splice_embeddings(elmo_embeddings,tokens_segmented): |
比如tokens_segmented
长度为[44, 110]
,elmo_embeddings
shape为:torch.Size([2, 3, 110, 1024])
。
那么new_elmo_embeddings
的shape为:torch.Size([1, 3, 154, 1024])
。
给予不同词不同的权重,比如停用词为0,符号为0,以及处理oov情况
1 | def get_weight_list(word2weight_pretrain, word2weight_finetune, tokenized_sents, lamda, database=""): |
1 | def get_oov_weight(tokenized_sents,word2weight,word,method="max_weight"): |
就是根据dict.txt计算出每个词的权重,所获取到的.
获取整段话的每层平均向量
1 | def get_weighted_average(tokenized_sents, sents_tokened_tagged,weight_list, embeddings_list, embeddings_type="elmo"): |
- 初始化一个shape为(3, 1024)的矩阵,因为elmo为3层,1024为输出的hidden_size。
- 和上面获取不同词权重结合,并且
只计算考虑的词性
。比如(n,np,ns)等。 - 将elmo每层的计算的平均结果保存。
注意这里哦,这里有两点需要注意的:
- elmo第三层不是原来第三层的结果了,是作者称为
词向量对齐
的给替代了。- 他计算的是只考虑的词性,而不是所有词的平均向量。
获取候选词的每层平均向量
1 | def get_candidate_weighted_average(tokenized_sents, weight_list, embeddings_list, start,end,embeddings_type="elmo"): |
整个计算过程和上一步基本一致。
阶段小结
到这一步作者完成了对文档向量的计算,以及候选词向量的计算。
不难看出,第一还是词权重的问题。第二是文档向量只考虑了目标词性。第三是候选词一般是复合词,那么在计算复合词权重的时候是词权重 * 词向量求平均的方式。
那如果我们有一个更合适的分词器,训练一个自己的词权重文件,以及训练一个更适合自己的elmo模型,那是否可以讲整套技术上是更为完整的。
获取每个候选词与整段话的余弦距离。排序
1 |
|
计算每个候选词与整段话的余弦距离,但是这里默认每层的权重是[0.0,1.0,0.0]
,那这就导致作者声称的词向量对齐(embeddings alignment,EA)
技术并没有用到~
1 |
|
产生的结果比如:{“技术”: [0.8432613915568461, 0.8243992563094531, 0.8120348604031394]},那么又做了一次平均。
阶段总结
- 如果我们有一个更合适的分词器,训练一个自己的词权重文件,以及训练一个更适合自己的elmo模型,那是否可以讲整套技术上是更为完整的。
- 计算候选词和整段的余弦距离时,其本质是否可以理解成是候选词和只考虑的词性组成的能代表整段文本向量进行排序呢。
- 作者声称的
词向量对齐(embeddings alignment,EA)
技术默认没有用到。
到这里作者完成了SIFRank_zh算法,的确是受限于目前的成果,整体流程也是具备参考价值的。
突出的问题,到这里为止,并没有利用到位置信息特征,比如行首或者行尾可能更具备代表含义。那么作者在此基础上,提供了SIFRank+算法。
获取positional score。 (SIFRank+)
1 |
|
我原本以为作者会用到词在原文中的索引位置,结果是候选词之间的索引位置。emmmm,但反过来想这其实也不正是原文中的相对索引位置嘛。
作者这里用到了两个技术点让我觉得很nice,一个是wnl.lemmatize
,其作用是词性还原的,对于英文来说有用。还有一个是softmax
,是用于归一化的。都能看出作者的扎实底子。
总结与思考
从作者这地方可以看出,整套流程是提取复合词的候选词,然后使用elmo计算候选词和以只考虑的目标词性所组成的句向量进行余弦计算其排序。
可提升点
在不改变整体流程和技术的方式上
- 如果使用thunlp,那么就训练一个以thunlp为结果的dict.txt,但是也可以训练一个tf-idf,个人觉得使用后者来表示词权重是更合适。
- 不局限于现有的正则表达,可以在分词的基础上增添自己的理解,另外可以训练自己ner。
- elmo越深其代表的含义更复杂,比如第一层可能代表词向量,第二层到第三层可能代表语义,句法等。作者把第三层替换掉,但是并没有使用,以及第一层,那么是否可以给予不同权重,但是这又属于超参问题,另外效果怎么样也不敢保证。
- 位置信息。根据业务来,有的业务有title和content,那么title可给予更高的权重。