无监督关键短语的生成问题博客10--utils.py的分析

 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。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值