2021SC@SDUSC
上一篇博客我们分析了utils.py中初步提取关键短语的函数,最后介绍了混淆矩阵的概念,接下来将会用到混淆矩阵的指标计算precision和recall及F1分数,再复习一下,recall就是分类后的正例占样本中标记为正例的所以样本的比例,precision就是实际正例占分类结果为正例的样本的比例。
本篇博客我们将首先分析utils.py中的get_fscore函数,再分析pke中的有监督的神经网络为基础的seq2seq模型,其实现了输入文本的序列化。(结合之前我们分析的vocabulary类)
一、get_fscore函数的分析
这里通过计算我们得到的关键短语与数据集的label比对后的得分来衡量关键词抽取的结果。注意这里的recall和precision的计算方法,将在后面详细介绍。我们依旧是一段一段地分析代码。
def get_fscore(pred,label):
new_pred=[]
for e in pred:
e = e.replace('-',' ')
c=''
e = e.split(' ') # 再将关键短语分割成列表
# 跳过长度大于4的关键短语
if len(e)>4:
continue
for q in e:
c = c + ' ' + porter_stemmer.stem(q) # 用porter_stemmer规格化q
if c.strip() not in new_pred:
new_pred.append(c.strip())
这里首先是初始化new_pred,然后对于我们提取出的关键短语的列表,再次以空格进行分割,过滤掉程度大于4的关键短语,如原来关键短语的列表里有['keywords list' , 'this is the first keyword'] 等关键短语,过滤,用空格进行分割后为[['keyword' , 'list'] , ['this' , 'is' , 'the' , 'first' , 'keyword']]然后['this' , 'is' , 'the' , 'first' , 'keyword']长度大于4会被过滤掉,之后再对分割后的每个词用porter_stemmer算法进行一定变形规格化,再加上词间空格组合成新的关键短语。最后将关键短语首末的空格删去,若最后得到的关键短语没有出现在new_pred这个list中,则new_pred加入该短语。(即new_pred不含重复的关键短语)
tmp=[]
for e in new_pred: # 遍历new_pred中的关键短语
flg=0
for w in tmp: # 遍历tmp中的关键短语
if w in e: # 判断关键短语是否有包含关系
flg=1
break
if flg==0:
tmp.append(e) # tmp中加入关键短语
new_pred = tmp
这里先初始化flg=0,将tmp集合中加入new_pred中的关键短语,下一次循环中,遍历tmp中的元素,这里也就是刚加入tmp中的关键短语,如果tmp中的元素是这一轮的e(也就是new_pred下一个关键短语)的其中一部分,就令flg=1并退出里层循环,同时也不将new_pred的新的词加入tmp中,最后令处理完的tmp为new_pred。
这里举个例子,第一轮外层循环中,从new_pred取出了第一个关键短语‘first candidate',并将其加入tmp中,第二轮循环,会对tmp中元素进行遍历,也就是会得到‘first candidate',判断这个关键短语是不是被包含在这一轮new_pred的关键短语e中,假设这一轮的e为'my first candidta',就会令flg=1(之后不会将该短语加入tmp中),同时跳出对tmp的遍历,因为随着tmp中关键短语的不断加入,需要对tmp中的每个词都进行判断,是否与当前处理的new_pred中关键短语e有包含关系,若一个短语出现包含关系,则不在tmp中重复加入e,且不再继续遍历tmp中后面的关键短语。
new_pred = new_pred[:min(10,len(new_pred))] #取new_pred中10和new_pred的长度较小者
pred=new_pred
precision=0
recall=0
for e in label:
if e in pred: # 判断pred中是否有label
recall += 1
precision += 1
if precision==0:
return 0
precision /= len(pred) # 计算精确度
recall /= len(label) # 计算召回率
return 2*precision*recall/(precision+recall) # 计算F1分数
处理好抽取出的关键短语列表后,就可以进行相关分数的计算了,这里的new_pred就是处理好的关键短语列表,label是真正的关键短语标签列表,在数据集中以keyword的标注出现。这里的方法是遍历标签集合中的标签,若标签在pred中出现过,则precision和recall的记录数字暂时+1,
precision的计算为precision计数/pred长度,也就是表明了在pred的关键短语列表中,预测的结果究竟有多少与label相符合,以此表明精确度。
recall的计算为recall计数/label长度,也就是表明了,在label集合中,pred得到了其中的多少关键短语(正确结果),以此表明召回率,要注意这里precision和recall的计算与分类问题理解的不同之处。
二、对于kp20k_training.json数据集的处理
由于本项目的代码是main_code,并不是完整的代码,我们在分析代码时,发现对预料库的处理部分的document.npy文件并为给出,查阅资料后得知该文件是对预料库做过一定处理保存好的文件,是numpy形式,内容形如['one two three.' , 'one one one one.' , 'three three.']每个元素为一个string,而我们下载的数据集kp20k_training为json格式,需要进行一定的数据转换才能实验论文代码的复现。本部分的代码由笔者自己编写,旨在处理训练的数据集。
首先是引入pandas和numpy,读入kp20k_training.json训练集。展示读入结果的第一行数据。
import pandas as pd
import numpy as np
train = pd.read_table('kp20k_training.json',header = None)
train.iloc[1,0]
运行结果如下:
可以看到,数据集的第一行由{}括起,分为"abstract","keyword","title"三部分。 这里的keyword为
"keyword": "sigma delta modulators;analog-to-digital converters (adcs);multistage (mash);multibit quantizer;dynamic range improvement"
可以看到是关键短语的形式,而不是关键词,也就是该文本段的label。abstract就是文本段部分,我们需要从中提取关键短语。
keywordlist = [] # 所有keyword
abstractlist = [] # 所有abstract
titlelist = [] # 所有title
for i in range(train.shape[0]):
# 找到abstract的索引
a = train.iloc[i,0].find('abstract') z
starta = a+len('abstract')+4
# 找到keyword的索引
b = train.iloc[i,0].find('keyword')
enda = b-4
startb = b+len('keyword')+4
# 找到title的索引
c = train.iloc[i,0].find('title')
startc = c+len('title')+4
endb = c-4
endc = -3
abstractlist.append((train.iloc[1,0])[starta:enda])
keywordlist.append((train.iloc[1,0])[startb:endb])
titlelist.append((train.iloc[1,0])[startc:endc])
keywordlist
读入数据集后,只有一列,我们需要将abstract、keyword、title分开,并将abstract整合到一起作为语料集输入。这里先初始化了存放所有abstract、keyword、title的list,然后对数据集的每一列进行处理,首先是找到每一行中的abstract、keyword、title的索引,由此对字符串进行切割,切割出abstract、keyword、title三部分,加入对应的list中,最后以keyword为例进行展示。
运行结果如下:
得到了数据集中所有行的keyword的list。接下来需要将获得的三个list作为新的列插入原dataframe中。
train.insert(0,'abstract',abstractlist)
train.insert(1,'keyword',keywordlist)
train.insert(2,'title',titlelist)
train
插入后的结果如下,该数据集有53万条数据。
至此,我们完成了对于语料集中每一条数据的abstract、keyword和title的分隔。
对于由语料集的数据生成document.npy见以下代码,我们得到的abstract列的每个值都写入kp20k.txt中,然后进行分句,注意这里还不用分成token,分成token的工作是在utils.py的extract_candidate函数中完成。
f = open('kp20k.txt', 'r', encoding='gbk')
e = f.readline()
while e:
sentences = nltk.sent_tokenize(e) # 调用nltk提供的函数完成分句
e = f.readline()
print(sentences)
a = numpy.array(sentences) # list转为numpy
np.save("document.npy", a) # save得到document.npy文件
运行结果如下,可以看到,每个元素为分句后的一句话。
最后,我们得到了document.npy,extract函数部分的运行就可以成功加载,作为extract函数的input。我们可以通过extract函数,得到sliver.npy,得到评分比较高的关键短语,作为深度模型的训练输入。
if __name__ == '__main__':
Extract(list(np.load('doucument.npy', allow_pickle=True)))
三、pke中seq2seq.py的分析
接下来我们分析对seq2seq中inputsequence的实现,我们将文本分句分词以及词性标注后还不能作为seq2seq模型的输入,该模型需要一个数值的序列化输入,这里就需要用到之前写的vocabulary类建立词典,句首句末有特殊标记表示,未知单词用<UNK>标识。用word2idx方法得到单词的索引形成sequence,用idx2word方法从索引得到单词,从sequence再得到word。
from __future__ import absolute_import
from __future__ import print_function
from pke.supervised.api import SupervisedLoadFile
class Seq2Seq(SupervisedLoadFile):
def __init__(self):
super(Seq2Seq, self).__init__()
# 输入序列
self.sequence = []
# vocabulary的初始化
self.vocabulary = ['<SOS>', '<EOS>', '<UNK>']
def candidate_selection(self):
pass
def candidate_weighting(self):
pass
首先是重写了初始化函数,初始化了输入序列为空,以及vocabulary为 ['<SOS>', '<EOS>', '<UNK>']
def document_to_ix(self):
# 输入文件转换为ix
self.sequence.append(self.vocabulary.index('<SOS>'))
# 找到<SOS>的索引并加入sequence中,sequence以<SOS>开始
for i, sentence in enumerate(self.sentences):
for word in sentence.stems:
try:
self.sequence.append(self.vocabulary.index(word))
# 找到单词的索引并加入sequence中
except ValueError:
self.sequence.append(self.vocabulary.index('<UNK>'))
# 出错时找到<UNK>的索引并加入sequence中
# 找到<EOS>的索引并加入sequence中,sequence以<EOS>结束
self.sequence.append(self.vocabulary.index('<EOS>'))
这个函数是将输出文件转为ix,首先是调用vocabulary的index函数,找到<SOS>在词典中的索引,然后加入sequence序列中,对于输入的文本sentences,用enumerate迭代,取出每个单词,然后调用index函数找到该单词的索引,得到索引后加入sequence输入序列中。如果word单词在vocabulary中没有出现过,会报ValueError,这时候,找到<UNK>的索引加入输入的sequence序列中,最后处理完了sentences,找到<EOS>的索引加入sequence序列中,至此完成了输入文本token的序列化。
该序列化的过程结合了之前我们分析的07--create_vocabulary.py的分析中vocabulary类的创建,实现序列化后,才能作为seq2seq模型的输入,之后得到输出序列后,再通过vocabulary的index2word方法将outputsequence转为文本token的形式。
下一篇博客中,我们将分析pke中的基于统计的tf-idf的实现,该方法用于我们给candidate keyword的评分排序,得到sliver label。