项目总结(一)__百度AI studio比赛--汽车大师问答摘要与推理_知了爱啃代码的博客-程序员信息网

技术标签: tensorflow  AI练手系列  nlp  深度学习  自然语言处理  神经网络  

项目地址:百度AI studio比赛–汽车大师问答摘要与推理

项目背景描述

项目是百度AI studio比赛,题目为“汽车大师问答摘要与推理”。

要求大家使用汽车大师提供的11万条(技师与用户的多轮对话与诊断建议报告数据)建立模型,模型需基于对话文本、用户问题、车型与车系,输出包含摘要与推断的报告文本,综合考验模型的归纳总结与推断能力。该解决方案可以节省大量人工时间,提高用户获取回答和解决方案的效率。

数据集说明

对于每个用户问题"QID",有对应文本形式的文本集合 D = “Brand”, “Collection”, “Problem”, “Conversation”,要求阅读理解系统自动对D进行分析,输出相应的报告文本"Report",其中包含摘要与推理。目标是"Report"可以正确、完整、简洁、清晰、连贯地对D中的信息作归纳总结与推理。

训练:所提供的训练集(82943条记录)建立模型,基于汽车品牌、车系、问题内容与问答对话的文本,输出建议报告文本

输出结果:对所提供的测试集(20000条记录)使用训练好的模型,输出建议报告的结果文件,通过最终测评得到评价分数。

数据集概览

import pandas as pd
train_path = "E:/课程/07 名企班必修课1-导师制名企实训班自然语言处理方向 004期/Lesson1-项目导论与中文词向量实践/数据集/AutoMaster_TrainSet.csv"
test_path = "E:/课程/07 名企班必修课1-导师制名企实训班自然语言处理方向 004期/Lesson1-项目导论与中文词向量实践/数据集/AutoMaster_TrainSet.csv"
df = pd.read_csv(train_path, encoding='utf-8')
df.head()

原始数据展示:

image-20210304155927892

大体流程

超参数定义->设置使用GPU->训练->验证->测试->发现问题->改进问题

超参数定义

代码详见/bin/main.py

关键参数简要说明:

max_enc_len = 200 #encoder输入最大长度
max_dec_len = 40 #decoder输入最大长度
batch_size = 32
beam_size = 3
vocab_size = 30000
embed_size = 256
enc_units = 256
dec_units = 256
attn_units = 256
learning_rate = 0.001
steps_per_epoch = 1300
max_steps = 10000
epochs = 20
dropout_rate = 0.1

设置使用GPU

#设置使用GPU还是CPU
gpus = tf.config.experimental.list_physical_devices(device_type='GPU')

#若使用GPU,对使用哪一块GPU进行设置
if gpus:
    tf.config.experimental.set_visible_devices(devices=gpus[0],device_type='GPU')

数据预处理

数据读取与构建词向量,代码详见preprocess.py、data_reader.py、build_w2v.py。

image-20210304160948545

数据分批与数据规范化

image-20210304161926269

模型构建-Seq2Seq

定义Encoder与Decoder模块,及中间的Attention部分。

Encoder部分:

embedding维度256维

self.embedding = tf.keras.layers.Embedding(vocab_size,                                                      					embedding_dim,
                                             weights=[embedding_matrix],
                                             trainable=False)

网络采用双向GRU作为编码器(由两个单向的GRU拼接而来)。

self.gru = tf.keras.layers.GRU(self.enc_units,
                                       return_sequences=True,
                                       return_state=True,
                                       recurrent_initializer='glorot_uniform')
self.bigru = tf.keras.layers.Bidirectional(self.gru, merge_mode='concat')

同时隐藏态也需要拼接:

#把隐藏层划分成几个子张量,分成2份,在第二个维度上进行切分
hidden = tf.split(hidden, num_or_size_splits=2, axis=1)      
output, forward_state, backward_state = self.bigru(x, initial_state=hidden)
state = tf.concat([forward_state, backward_state], axis=1)

详细代码见rnn_encoder.py。

Attention部分:

Score计算采用感知机的方式,再将Score经过一个Softmax归一化得到Attention_Weight。

# Calculate v^T tanh(W_h h_i + W_s s_t + b_attn)
score = self.V(tf.nn.tanh(self.W1(enc_output) + self.W2(hidden_with_time_axis)))
attn_dist = tf.nn.softmax(score,axis=1)#shape= (16,200,1)

然后reduce_sum得到context_vector

context_vector = tf.reduce_sum(attn_dist * enc_output, axis=1)

详细代码见rnn_decoder.py。

Decoder部分:

embedding维度256维:

#定义Embedding层,加载预训练的词向量
self.embedding = tf.keras.layers.Embedding(vocab_size,
                                           embedding_dim,
                                           weights=[embedding_matrix],
                                           trainable=False)

网络采用单向GRU作为编码器:

self.gru = tf.keras.layers.GRU(self.dec_units,
                               return_sequences=True,
                               return_state=True,
                               recurrent_initializer='glorot_uniform')

GRU的输入是由输入x和context_vector组成:

x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)

GRU输出后需要维度变化然后再过一个FC层输出概率分布:

output, state = self.gru(x)
output = tf.reshape(output, (-1, output.shape[2]))#(batch_size,hidden_size)
out = self.fc(output)#(batch_size,hidden_size)

代码在rnn_encoder.py中。

整体模型定义上还会有一些细节,例如Tearcher的使用等等,整体模型定义代码如下:

import tensorflow as tf
from seq2seq_tf2.encoders import rnn_encoder
from seq2seq_tf2.decoders import rnn_decoder
from utils.data_utils import load_word2vec
import time


class SequenceToSequence(tf.keras.Model):
    def __init__(self, params):
        super(SequenceToSequence, self).__init__()
        #读取word2vec.txt、vocab.txt返回词向量矩阵,矩阵大小vacab_size*embed_size,每一行都是行索引编号对应word的词向量
        self.embedding_matrix = load_word2vec(params)
        self.params = params
        self.encoder = rnn_encoder.Encoder(params["vocab_size"],
                                           params["embed_size"],
                                           params["enc_units"],
                                           params["batch_size"],
                                           self.embedding_matrix)
        self.attention = rnn_decoder.BahdanauAttention(params["attn_units"])
        self.decoder = rnn_decoder.Decoder(params["vocab_size"],
                                           params["embed_size"],
                                           params["dec_units"],
                                           params["batch_size"],
                                           self.embedding_matrix)

    def call_encoder(self, enc_inp):
        enc_hidden = self.encoder.initialize_hidden_state()#[batch_size,enc_units]
        enc_output, enc_hidden = self.encoder(enc_inp, enc_hidden)       
        return enc_output, enc_hidden
    
    def call(self, enc_output, dec_inp, dec_hidden, dec_tar):
        predictions = []
        attentions = []
        #初始化Attention,主要是初始化隐藏层和内部各个参数,隐藏层维度先初始化与encoder一致
        context_vector, _ = self.attention(dec_hidden,  # shape=(16, 300)
                                           enc_output) # shape=(16, 200, 300)
        for t in range(dec_tar.shape[1]): # 40(dec_maxlen)
            # Teachering Forcing
            x_input = dec_inp[:,t]
            _, pred, dec_hidden = self.decoder(tf.expand_dims(x_input,1),
                                                dec_hidden,
                                                enc_output,
                                                context_vector)
            #此处的attention就是第二次进Attention的值u,其实就是正常开始训练之后的值,不同于初始化的值
            context_vector, attn_dist = self.attention(dec_hidden, enc_output)
            predictions.append(pred)
            attentions.append(attn_dist)

        return tf.stack(predictions, 1), dec_hidden

模型训练

优化器选用Adam:

optimizer = tf.keras.optimizers.Adam(name='Adam', learning_rate=params["learning_rate"])

损失函数为交叉熵函数:

loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False, reduction='none')
# 定义损失函数
def loss_function(real, pred):
    mask = tf.math.logical_not(tf.math.equal(real, 1))
    dec_lens = tf.reduce_sum(tf.cast(mask, dtype=tf.float32), axis=-1)

    loss_ = loss_object(real, pred)
    mask = tf.cast(mask, dtype=loss_.dtype)
    loss_ *= mask
    loss_ = tf.reduce_sum(loss_, axis=-1)/dec_lens
    return tf.reduce_mean(loss_)

定义训练的每一步:

#dec_tar是标签值,dec_inp是正常输入值
def train_step(enc_inp, dec_tar, dec_inp):
    with tf.GradientTape() as tape:
        #初始化encoder状态,一堆0
        enc_output, enc_hidden = model.call_encoder(enc_inp)

        #初始化decoder状态等于encoder状态
        dec_hidden = enc_hidden

        # start index
        #得到50步堆接起来的概率分布值,传入loss计算函数
        #将encoder后的输出以及dec的输入和初始状态的隐藏层状态传入seq2seq模型,得到预测值
        pred, _ = model(enc_output,  # shape=(16, 200, 300)
                        dec_inp,  # shape=(16, 40)
                        dec_hidden,  # shape=(16, 300)
                        dec_tar)  # shape=(16, 40) 
        loss = loss_function(dec_tar, pred)
        #反向梯度更新
        variables = model.encoder.trainable_variables + model.attention.trainable_variables + model.decoder.trainable_variables
        gradients = tape.gradient(loss, variables)
        optimizer.apply_gradients(zip(gradients, variables))
        return loss

训练并保存中间结果:

    best_loss = 20
    epochs = params['epochs']
    for epoch in range(epochs):
        t0 = time.time()
        step = 0
        total_loss = 0
        #分批次训练
        for batch in dataset:
            loss = train_step(batch[0]["enc_input"], 
                              batch[1]["dec_target"], 
                              batch[1]["dec_input"])
            step += 1
            total_loss += loss
            if step % 100 == 0:#100
                print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1, step, total_loss / step))
        #存模型数据,每一步存一下
        if epoch % 1 == 0: 
            if total_loss / step < best_loss:
                best_loss = total_loss / step
                ckpt_save_path = ckpt_manager.save()
                print('Saving checkpoint for epoch {} at {} ,best loss {}'.format(epoch + 1, ckpt_save_path, best_loss))
                print('Epoch {} Loss {:.4f}'.format(epoch + 1, total_loss / step))
                print('Time taken for 1 epoch {} sec\n'.format(time.time() - t0))

相关代码主要见train_helper.py和train_eval_test.py。

测试

测试的主要流程与训练相似,可以选择greedy_decode和beam_decode两种方式:

def test(params):
    assert params["mode"].lower() == "test", "change training mode to 'test' or 'eval'"
    # assert params["beam_size"] == params["batch_size"], "Beam size must be equal to batch_size, change the params"

    print("Building the model ...")
    model = SequenceToSequence(params)

    print("Creating the vocab ...")
    vocab = Vocab(params["vocab_path"], params["vocab_size"])

    print("Creating the batcher ...")
    b = batcher(vocab, params)

    print("Creating the checkpoint manager")
    checkpoint_dir = "{}/checkpoint".format(params["seq2seq_model_dir"])
    ckpt = tf.train.Checkpoint(SequenceToSequence=model)
    ckpt_manager = tf.train.CheckpointManager(ckpt, checkpoint_dir, max_to_keep=5)

    # path = params["model_path"] if params["model_path"] else ckpt_manager.latest_checkpoint
    # path = ckpt_manager.latest_checkpoint
    ckpt.restore(ckpt_manager.latest_checkpoint)
    print("Model restored")
    # for batch in b:
    #     yield batch_greedy_decode(model, batch, vocab, params)
    #if params['greedy_decode']:
        # params['batch_size'] = 512
    predict_result(model, params, vocab, params['test_save_dir'])

def predict_result(model, params, vocab, result_save_path):
    dataset = batcher(vocab, params)
    # 预测结果
    if params['greedy_decode']:
        results = greedy_decode(model, dataset, vocab, params)
    elif params['beam_search_decode']:
        results = beam_decode(model, dataset, vocab, params)
    results = list(map(lambda x: x.replace(" ",""), results))
    #print("results2",results)#64个元素,每个元素都是大小为dec_max_len的序列
    # 保存结果
    save_predict_result(results, params)

    return results

具体实现方法可见test_helper.py。

验证

评价指标Rouge-L

def evaluate(params):
    gen = test(params)
    reals = []
    preds = []
    with tqdm(total=params["max_num_to_eval"], position=0, leave=True) as pbar:
        for i in range(params["max_num_to_eval"]):
            trial = next(gen)
            reals.append(trial.real_abstract)
            preds.append(trial.abstract)
            pbar.update(1)
    r = Rouge()
    scores = r.get_scores(preds, reals, avg=True)
    print("\n\n")
    pprint.pprint(scores)

其他优化

  • 使用PGN网络解决OOV问题
  • 采用惩罚机制解决重复词语的问题

最终效果result

待完成

需要去看一下冠军团队的实现思路,学习好的经验与方法。此处待补充。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/rongsenmeng2835/article/details/114381629

智能推荐

unity, Awake的调用时机_weixin_33924770的博客-程序员信息网

Awake是在setActive(true)时才会被调用,不过如果再setActive(false)然后重新setActive(true)的话,Awake就不会再被调用了,也就是说Awake能保证仅被调用一次。跟c++里构造函数的区别就是并不是Instantiate时候就开始调,不过可以凑合着当构造函数用。...

Idea Git 强制推送到远程分支 Idea Git Force push-程序员信息网

Idea Git 强制推送到远程分支 Idea Git Force push步骤是: push的时候,点击下拉 --- 选择: Force Push 即可

Ext JS - renderer 函数中各参数含义_exjjs renderer参数_csdn丨你的笑忘书的博客-程序员信息网

Ext JS - JavaScript framework for web apps | Sencha.comExt JS 4.2 官方文档地址 http://docs-devel.sencha.com/extjs/4.2.2/#!/api 搜索renderer即可查询API,Ext.grid.column.ColumnView.rendererrenderer : Function

Visual Studio 添加外部.cpp/.h文件_vs添加.h文件_LaconicRazor的博客-程序员信息网

1.右键“源文件”,点击“添加”--&gt;“现有项”2.选择需要添加的文件,点击添加3.右键项目名称,点击“属性”4.选择“C/C++”下的“常规”5.点击“附加包含目录下”下的编辑,完成文件路径添加...

java下载多图压缩成包_爱吃榴莲酥的姑娘的博客-程序员信息网

@RequestMapping(value = "/download") public void download(HttpServletRequest request,HttpServletResponse response) throws Exception { PageData pd = this.getPageData(); pd = printm...

面试了一个 46 岁的程序员,我思绪万千_程序员生活志的博客-程序员信息网

转载提示:这是一篇旧文最近一直忙于面试,人事推给了我一份简历,职位是算法工程师,年龄是 46 岁,我揉了揉眼睛后再看看,确实是 46 岁。抱着忐忑的心,我电话面试一番后,还是不觉得他和我们的团队很适合。人都会有同理心,尤其是这么大岁数的程序员还是为了生计来找工作,心还是会隐隐触痛。年龄是多数程序员的天敌,之前没有概念因为生活中样本较少,现在来了一个鲜明的例子,并且还需要自己亲手关闭一扇...

随便推点

S3C2440移植linux3.4.2内核之修改分区以及制作根文件系统_s3c2440嵌入式linux内核移植怎么创建根目录文件系统_嵌入式与Linux那些事的博客-程序员信息网

启动内核有8个分区,而我们的uboot只有4个分区。0x00000000-0x00040000 : "bootloader" //存放uboot0x00040000-0x00060000 : "params" //存放环境变量0x00060000-0x00260000 : "kernel" //存放内核0...

论文解读:Cycle ISP Real Image Restoration via Improved Data Synthesis_Matrix_11的博客-程序员信息网

Cycle ISP: Real Image Restoration via Improved Data Synthesis谷歌去年发表了一篇文章:Unprocessing Images for Learned Raw Denoising,是关于如何构造逼近真实的数据来进行降噪的,在去年的文章里,研究者们主要是模拟了 ISP 中从 RAW 图到 sRGB 的过程,然后将 ISP 的过程逆转过来,从 sRGB 到 RAW,然后再在 RAW 域上添加噪声,从而构造出符合真实场景的噪声数据。今年谷歌又发表了一篇

easyUI中droppable,draggable用法列子修改_droppable中放置区拖拽禁用_疯子和傻子的爱情故事的博客-程序员信息网

在项目中要用到拖动效果,之前写了一点不够完善,今天做了一点优化,在以前的基础上可以相互拖动了,改动的地方不大,但为了不影响之前的效果,我还是重新记录一下.HTML和我之前记录的文章一样,我就不重复了。。。以下是js:var targetFlag=false;//判断从框中拖出去是否拖到了另一个框 $('.items li').draggable({ prox

ASP.NET 中的 Async/Await 简介_weixin_34124651的博客-程序员信息网

本文转载自MSDN作者:Stephen Cleary原文地址:https://msdn.microsoft.com/en-us/magazine/dn802603.aspx大多数有关 async/await 的在线资源假定您正在开发客户端应用程序,但在服务器上有 async 的位置吗?可以非常肯定地回答“有”。本文是对 ASP.NET 上异步请求的概念性概述,并提供了对最佳在线资源的引用...

HDU5126 stars(CDQ分治)_weixin_30247307的博客-程序员信息网

传送门大意:向三维空间中加点,询问一个三维区间中点的个数。解题思路:带修改CDQ,将修改和询问一起插入CDQ分治询问。(询问可以由8个前缀和加减操作实现)其中第一层CDQ维护x有序。第二层CDQ维护y有序并且将z离线处理完更新答案。注意要将原数组的辅助数组推入第二层CDQ否则会将顺序毁坏。代码: 1 #include&lt;cstdio&gt; ...

ios程序猿攻城策略_weixin_30315435的博客-程序员信息网

这也是一个面向对象的问题,作为实例对象的你需要一步一步去攻城拔寨。待各处插满了自己的旗帜,回过头来,你会感谢一路走来的自己!一、入门 面向对象编程基础(类和继承,变量和方法的作用域,MVC基本概念,分类)方法和函数(消息,类定义和属性,与C/C++的混合编程)内存管理(strong/weak, ARC自动引用计数,自动释放对象)代码设计模式(Block块语句,Target/a...

推荐文章

热门文章

相关标签