python3 seq2seq_convert.py 对应代码解读抽取式提取+生成式提取摘要代码解读------摘要代码解读4


查看main函数中的内容,首先进行文件的读取

if __name__ == '__main__':
    #data_extract_json = /home/xiaoguzai/代码/法研杯文本摘要/datasets/train_extract.json
    data = load_data(data_extract_json)
    data_x = np.load(data_extract_npy)
    data_seq2seq_json = data_json[:-5] + '_seq2seq.json'
    convert(data_seq2seq_json, data, data_x)
    print(u'输出路径:%s' % data_seq2seq_json)

为了明确这里的data_extract_json和data_extract_npy文件的内容,我们需要到上一个extract_model.py之中去查看data_extract_json和data_extract_npy的调用情况

回到extract_model.py之中查看

这里的

data = load_data(data_extract_json)
data_x = np.load(data_extract_npy)

调用data和data_x

再次回到seq2seq_convert.py之中查看(多折抽取代码研读)

这里的len(data)和len(data_x)的长度都为20,data_seq2seq_json = train_seq2seq.json
(所有数据的总长度即为20)
接下来进入数据转换部分

convert(data_seq2seq_json,data,data_x)

加载对应的数据
这里的data_split在snippets.py之中

def data_split(data, fold, num_folds, mode):
    """划分训练集和验证集
    """
    if mode == 'train':
        D = [d for i, d in enumerate(data) if i % num_folds != fold]
    else:
        D = [d for i, d in enumerate(data) if i % num_folds == fold]

    if isinstance(data, np.ndarray):
        return np.array(D)
    else:
        return D

也就是说每次fold_convert之后得到的data的结果相同
这里的valid_data和valid_x是每次切割出来的数值,我们查看每个fold之中调取出来的数据

for fold in range(num_folds):
	total_results.append(fold_convert(data,data_x,fold))

进入fold_convert函数之中查看调用的过程

def fold_convert(data, data_x, fold):
    """每一fold用对应的模型做数据转换
    """
    valid_data = data_split(data, fold, num_folds, 'valid')
    valid_x = data_split(data_x, fold, num_folds, 'valid')
    model.load_weights('./weights/extract_model.%s.weights' % fold)
    y_pred = model.predict(valid_x)[:, :, 0]
    results = []
    for d, yp in tqdm(zip(valid_data, y_pred), desc=u'转换中'):
        yp = yp[:len(d[0])]
        yp = np.where(yp > threshold)[0]
        source_1 = ''.join([d[0][int(i)] for i in yp])
        source_2 = ''.join([d[0][int(i)] for i in d[1]])
        result = {
    
    
            'source_1': source_1,
            'source_2': source_2,
            'target': d[2],
        }
        results.append(result)

    return results

fold = 0和fold = 1的时候提取出来的结果是不一样的,fold = 0的时候提取出来的数据
这里是同样的数据data以及data_x拆解出不同的valid_data和valid_x
在思考为什么要将同样的数据拆出两个valid_data和valid_x的时候,我们首先要思考一个问题:我们有什么数据?
文本原文,以及总结内容,这都是我们拥有的数据,除此之外我们还有一个官方指定的由原文到总结的索引内容,以及一个训练好的由原文到总结的模型。
这里我们从20条数据之中一次抽取2个,由于num_epochs = 2(作者原定义num_epochs = 15,这里跟之前抽取数据的时候训练的有关,抽取的时候训练了几个epoch这里就有几个epoch的数据),所以总共抽取2*10 = 20个数据。
这里注意一下局部变量的用法

from extract_model import *

这里将extract_model之中的整体的变量全部抽取出来了,所以extract_model之中的整个变量之中num_folds = 15
如果在函数之中定义了num_folds的对应值

for d, yp in tqdm(zip(valid_data, y_pred), desc=u'转换中'):
	  num_folds = 2
    yp = yp[:len(d[0])]
    yp = np.where(yp > threshold)[0]
    source_1 = ''.join([d[0][int(i)] for i in yp])
    source_2 = ''.join([d[0][int(i)] for i in d[1]])
    result = {
    
    
        'source_1': source_1,
        'source_2': source_2,
        'target': d[2],
    }
    results.append(result)

则这个num_folds的作用域只在函数之内有效,在函数之外无效。

对于每次抽取出来的数据之中

def fold_convert(data, data_x, fold):
    """每一fold用对应的模型做数据转换
    """
    #print('num_fold = ')
    #print(num_folds)
    num_folds = 2
    valid_data = data_split(data, fold, num_folds, 'valid')
    valid_x = data_split(data_x, fold, num_folds, 'valid')
    model.load_weights('./weights/extract_model.%s.weights' % fold)
    y_pred = model.predict(valid_x)[:, :, 0]

    results = []
    for d, yp in tqdm(zip(valid_data, y_pred), desc=u'转换中'):
        yp = yp[:len(d[0])]
        yp = np.where(yp > threshold)[0]
        source_1 = ''.join([d[0][int(i)] for i in yp])
        source_2 = ''.join([d[0][int(i)] for i in d[1]])
        result = {
    
    
            'source_1': source_1,
            'source_2': source_2,
            'target': d[2],
        }
        results.append(result)
    print('results = ')
    print(results)
    print('----------')
    return results

这里的valid_data以及valid_x的长度均为10,总共2折,因此所有的数据为20。

接下来对这些数据进行写入文件操作操作

n = 0
while True:
    i, j = n % num_folds, n // num_folds
    try:
        d = total_results[i][j]
    except:
        break
    F.write(json.dumps(d, ensure_ascii=False) + '\n')
    n += 1

(这中间的source_2其实也不是标准的,只不过是计算相应的rouge分数得到的,我们将它视为标准答案,只不过实际模型预测出来的source_1可能与标准答案source_2有差距)
之前正是由于忘改了局部变量,导致只有4条记录信息

使用交叉验证的原因

从上面的内容来看,在调用抽取的预测模型的时候使用了交叉验证的方法,原因在于训练集和测试集的不同。
引用大佬原文的内容
我们需要将原文作为输入,通过抽取模型输出抽取摘要,然后把抽取摘要作为生成模型的输入,来输出最终摘要。但是,这有一个问题,训练的数据我们都是见过的,但我们真正预测的是未见过的数据,如果直接训练一个抽取模型,然后用该模型抽取训练集的摘要,那么很明显由于都被训练过了,抽取出来的摘要分数肯定会偏高,而新样本的效果则会偏低,造成训练预测的不一致性。
也就是说提取出来的抽取结果必须是模型预测出来的结果,不能是算出来的与最终结果相同的抽取出来的文本,其实这点内容也很好理解,因为如果使用抽取出来的文本,那文本内容都是精炼过的,而如果使用模型预测出来的结果,那结果之中既有与最终的summary内容较为相似的结果,又有中间模型多预测出来的结果,这样才能使得后面的sequence-to-sequence内容很好地进行训练。
但是,有一个问题,就是如果抽取训练集和测试集的话,肯定有问题,训练集被训练过了,抽取的效果好,测试集没被训练过,抽取的效果不好,所以这里只能够选择使用交叉验证的方法,训练一波预测一波
这里从Evaluator类别可以看出来,每一个epoch预测出来之后保留一次对应的结果

class Evaluator(keras.callbacks.Callback):
	def on_epoch_end(self,epoch,logs=None):
		optimizer.apply_ema_weights()
		model.save_weights('weights/seq2seq_model.%s.weights'%epoch)
		optimizer.reset_old_weights()

总结:

回到模型方面,我们使用的是以句为单位的序列标注模型作为抽取模型,句向量部分用“BERT+平均池化”来生成,并固定不变,标注模型主体方面则用DGCNN模型构建。
值得指出的一个细节是,在训练抽取模型的时候,我们是以0.3为阈值做EarlyStop的,但最终以0.2为阈值构建生成模型的数据,依据还是前面说的抽取模型的原则是要“求全”。

Guess you like

Origin blog.csdn.net/znevegiveup1/article/details/120896143