一、项目简介

本文详细介绍了RNN和BRNN的结构,训练时的反向传播,梯度消失和爆炸的原理以及它们的优缺点。包含了一个使用vanilla RNN,LSTM,BiLSTM,GRU和BiGRU模型分别分类的谣言检测项目帮助理解

二、RNN

2.1 RNN简介

RNN全称为循环神经网络,是一种不同于一般前馈神经网络的特殊神经网络,其旨在处理时间序列数据,假定当前时间步是由先前时间步和当前输入决定的。

RNN维护一个状态变量,用于捕获序列数据中存在的各种模式,因此,它们能够对序列数据建模。并且随时间共享参数集,这也是RNN能学习序列每一时刻模式的主要原因之一。

2.2 RNN的结构

假设已有数据如下所示
x = { x 1 , x 2 , . . . , x T } y = { y 1 , y 2 , . . . , y T } x=\{x_1,x_2,...,x_T \}\\y=\{y_1,y_2,...,y_T \} x={x1,x2,...,xT}y={y1,y2,...,yT}


假定使用函数逼近器f表示如下两个关系,其中θ与φ表示参数集, h t h_t ht是当前状态, h t − 1 h_{t-1} ht1是前一状态
h t = f 1 ( x t , h t − 1 ; θ ) y t = f 2 ( h t ; ϕ ) h_t=f_1(x_t,h_{t-1};\theta)\\y_t=f_2(h_t;\phi) ht=f1(xt,ht1;θ)yt=f2(ht;ϕ)


我们可以将 f 1 , f 2 f_1,f_2 f1,f2的复合看做产生x,y的真正模型的近似,于是得到公式
y t = f 2 ( f 1 ( x t , h t − 1 ; θ ) ; ϕ ) y_t=f_2(f_1(x_t,h_{t-1};\theta);\phi) yt=f2(f1(xt,ht1;θ);ϕ)


例如 y 3 y_3 y3可以表示为如下公式和图
y 3 = f 2 ( f 1 ( x 3 , f 2 ( f 1 ( x 2 , f 2 ( f 1 ( x 1 , h 0 ; θ ) ; ϕ ) ; θ ) ; ϕ ) ; θ ) ; ϕ ) y_3=f_2(f_1(x_3,f_2(f_1(x_2,f_2(f_1(x_1,h_0;\theta);\phi);\theta);\phi);\theta);\phi) y3=f2(f1(x3,f2(f1(x2,f2(f1(x1,h0;θ);ϕ);θ);ϕ);θ);ϕ)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xKiVaBjw-1646533125155)(attachment:27d351db-6e0b-4505-b339-9cd75539683d.png)]


通过此可以推出采用循环连接的RNN单步计算
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aeCXTAfe-1646533125156)(attachment:348b14ba-f48e-4674-a16a-f64970968f9b.png)]

2.3 RNN的技术描述

神经网络通常由一组权重和偏置以及一些激活函数组成,所以上述的 h t h_t ht y t y_t yt两个公式可以写成如下形式(形式不唯一,可根据不同任务调整)
h t = t a n h ( U x t + W h t − 1 ) y = s o f t m a x ( V h t ) h_t=tanh(Ux_t+Wh_{t-1})\\y=softmax(Vh_t) ht=tanh(Uxt+Wht1)y=softmax(Vht)

这里U,W,V是不同的权重矩阵,tanh和softmax是不同的激活函数

2.4 RNN的反向传播

训练RNN需要用到基于时间的反向传播(BPTT),在此我们假设预测误差 E E E,权重矩阵 w w w,样本 x x x的正确标签 l l l,样本 x x x的预测标签 y y y,损失函数 L L L为均方差。

  • 举例:以普通链式法则求 ∂ E ∂ w 3 \frac {\partial E}{\partial w_3} w3E


∂ E ∂ w 3 = ∂ L ∂ y ∂ y ∂ h ∂ h ∂ w 3 \frac {\partial E}{\partial w_3}={\frac {\partial L}{\partial y}}{\frac {\partial y}{\partial h}}{\frac {\partial h}{\partial w_3}} w3E=yLhyw3h

可以变形为


∂ E ∂ w 3 = ∂ ( y − l ) 2 ∂ y ∂ w 2 h ∂ h ( ∂ ( w 1 x ) ∂ w 3 + ∂ ( w 3 h ) ∂ w 3 ) \frac {\partial E}{\partial w_3}={\frac {\partial (y-l)^2}{\partial y}}{\frac {\partial w_2h}{\partial h}}({\frac {\partial (w_1x)}{\partial w_3}+\frac {\partial (w_3h)}{\partial w_3})} w3E=y(yl)2hw2h(w3(w1x)+w3(w3h))


∂ ( w 3 h ) ∂ w 3 \frac {\partial (w_3h)}{\partial w_3} w3(w3h)这一项会产生问题,因为 h h h是个递归变量且依赖 w 3 w_3 w3,最终会产生无限项。若要解决这一问题,可以将输入序列随时间展开,为每个输入x,创建RNN的副本,并分别计算每个副本的导数,并通过计算梯度的总和将它们回滚,以计算需要更新的权重大小。我们接下来将讨论细节。

  • 举例:用基于时间的反向传播(BPTT)求 ∂ E ∂ w 3 \frac {\partial E}{\partial w_3} w3E


计算时考虑完整的输入序列,也就是说要创建4个RNN副本计算直到第四个时间步的所有时间步之和,于是我们可以得到以下结果


∂ E ∂ w 3 = ∑ j = 1 3 ∂ L ∂ y 4 ∂ y 4 ∂ h 4 ∂ h 4 ∂ h j ∂ h j ∂ w 3 \frac {\partial E}{\partial w_3}=\sum_{j=1}^3{\frac {\partial L}{\partial y_4}}{\frac {\partial y_4}{\partial h_4}}{\frac {\partial h_4}{\partial h_j}}{\frac {\partial h_j}{\partial w_3}} w3E=j=13y4Lh4y4hjh4w3hj

其中的 ∂ h t ∂ h j \frac {\partial h_t}{\partial h_j} hjht需要 t − j + 1 t-j+1 tj+1个副本。然后将副本汇总到单个RNN,求所有先前时间步长的梯度得到一个梯度,最后更新RNN。

然而随着时间步数的增加,这会使得计算变得复杂。所以我们可以使用BPTT的近似,即截断的基于时间的反向传播(TBPTT)。在TBPTT中,我们仅仅计算固定数量的时间步的梯度,即导数只算到 t − T t-T tT,不算到最开始

  • 举例:用截断的基于时间的反向传播(TBPTT)求 ∂ E ∂ w 3 \frac {\partial E}{\partial w_3} w3E


∂ E ∂ w 3 = ∑ j = t − T t − 1 ∂ L ∂ y t ∂ y t ∂ h t ∂ h t ∂ h j ∂ h j ∂ w 3 \frac {\partial E}{\partial w_3}=\sum_{j=t-T}^{t-1}{\frac {\partial L}{\partial y_t}}{\frac {\partial y_t}{\partial h_t}}{\frac {\partial h_t}{\partial h_j}}{\frac {\partial h_j}{\partial w_3}} w3E=j=tTt1ytLhtythjhtw3hj

这样随着序列的增长,我们依旧向后计算固定数量的导数,不会使计算成本变得过大。

2.5 RNN的梯度消失与爆炸

在真实的任务训练过程中,RNN存在一个明显的缺陷,那就是当阅读很长的序列时,网络内部的信息会逐渐变得越来越复杂,以至于超过网络的记忆能力,使得最终的输出信息变得混乱无用。

假设标准RNN隐藏状态计算公式为,激活函数 σ \sigma σ是sigmoid , W W W是权重矩阵

h t = σ ( W x x t + W h h t − 1 ) h_t=\sigma(W_xx_t+W_hh_{t-1}) ht=σ(Wxxt+Whht1)

为了简化计算,忽略与当前输入相关的项,重点放在循环部分
h t = σ ( W h h t − 1 ) h_t=\sigma(W_hh_{t-1}) ht=σ(Whht1)

结合链式法则对ht求偏导得到
∂ h t ∂ h t − k = ∏ i = 0 k − 1 W h σ ( W h h t − k + i ) ( 1 − σ ( W h h t − k + i ) ) \frac{\partial h_t}{\partial h_{t-k}}= \prod_{i=0}^{k-1} W_h \sigma(W_hh_{t-k+i})(1-\sigma(W_hh_{t-k+i})) htkht=i=0k1Whσ(Whhtk+i)(1σ(Whhtk+i))

提取出k个权重矩阵
∂ h t ∂ h t − k = W h k ∏ i = 0 k − 1 σ ( W h h t − k + i ) ( 1 − σ ( W h h t − k + i ) ) \frac{\partial h_t}{\partial h_{t-k}}= W_h^k \prod_{i=0}^{k-1} \sigma(W_hh_{t-k+i})(1-\sigma(W_hh_{t-k+i})) htkht=Whki=0k1σ(Whhtk+i)(1σ(Whhtk+i))
通过公式可以总结出一下特点

  • W h h t − k + i < < 0 W_hh_{t-k+i}<<0 Whhtk+i<<0 k k k个连乘会导致 $ \frac{\partial h_t}{\partial h_{t-k}}$ 接近 0,此时发生梯度消失
  • 当$W_hh_{t-k+i}>>0 时,\sigma(W_hh_{t-k+i})接近1,1-\sigma(W_hh_{t-k+i})接近0 $ 接近 0, k k k个连乘导致 $ \frac{\partial h_t}{\partial h_{t-k}}$ 接近 0,此时发生梯度消失
  • 即使当 W h h t − k + i = 0 W_hh_{t-k+i}=0 Whhtk+i=0 时,梯度是sigmoid激活的最大值(0.25) k k k个连乘也会导致$ \frac{\partial h_t}{\partial h_{t-k}}$ 接近 0,此时发生梯度消失
  • W h k W_h^k Whk初始化不良过小或者过大时,也会产生梯度消失或者爆炸。


前三条需要通过调整RNN结构(LSTM,GRU)来避免,最后一条可以通过调整初始化或者梯度裁剪来规避

2.6 RNN的应用

  • 一对一RNN:适用于每个输入都有输出的问题,例如场景分类,需要对图像中每个像素进行标注。
  • 一对多RNN:适用于接收一个输入得到一个输出序列的问题,例如生成图像标题,对于给定的图像可以生成一个文本序列。
  • 多对一RNN:适用于输入一个序列,得到一个输出的问题,例如文本分类任务,输入一个句子或者一篇文章判断其类别。
  • 多对多RNN:适用于输入一个任意长度序列得到一个任意长度序列的任务,例如机器翻译任务,输入中文文本序列,输入出英文文本序列。

三、BRNN

3.1 BRNN简介

BRNN意为双向循环神经网络,是一种常见的提升RNN预测质量的方法。之前介绍RNN时假设了当前时间步是由前面的较早时间步和当前输入决定的。可是当输入序列是文本时句子当前时间步也可能是由后面的时间步决定的,例如:

  • 我晚上不能喝 ___ ,因为要开车回家。


当预测是从头开始读句子,预测出可乐,雪碧,果汁,酒 都合理。因为没有足够的文本信息来帮助预测

  • 我晚上不能喝____


而预测时同时从前到后以及从后向前读句子,则能较大概率的预测出 ,因为上下文信息(或许还要结合一些知识库的支持)足够我们进行正确预测。

  • 我晚上不能喝___ ,
  • ___ ,因为要开车回家。

3.2 BRNN结构

其结构和RNN非常相似,就是两个反向的RNN的叠加, H t H_t Ht是隐藏状态,由 H t → H_t^{\rightarrow} Ht正向隐藏状态和 H t ← H_t^{\leftarrow} Ht反向隐藏状态组成,x为输入,O为输出,W为权重矩阵,b为偏置矩阵。
H t → = σ ( X t W x h ( f ) + H t − 1 → W h h ( f ) + b h ( f ) ) H_t^{\rightarrow}=\sigma (X_tW_{xh}^{(f)}+H_{t-1}^{\rightarrow}W_{hh}^{(f)}+b_h^{(f)}) Ht=σ(XtWxh(f)+Ht1Whh(f)+bh(f))
H t ← = σ ( X t W x h ( b ) + H t − 1 ← W h h ( b ) + b h ( b ) ) H_t^{\leftarrow}=\sigma (X_tW_{xh}^{(b)}+H_{t-1}^{\leftarrow}W_{hh}^{(b)}+b_h^{(b)}) Ht=σ(XtWxh(b)+Ht1Whh(b)+bh(b))
H t = ( H t → , H t ← ) H_t=(H_t^{\rightarrow}, H_t^{\leftarrow}) Ht=(Ht,Ht)
O t = H t W h q + b q O_t=H_tW_{hq}+b_q Ot=HtWhq+bq

两个方向的隐藏层的连接方式是 concat 拼接起来,蕴含了两个方向的信息 ,根据上图可以想成一个信息往上传,一个信息往下传。不同方向上的隐藏单元个数也可以不同。其反向传播方式和RNN类似,同时这也带来了RNN的一些容易梯度爆炸和梯度消失的缺点,在此不再赘述。

四、RNN以及其变体在项目中的运用

4.1.项目介绍

本项目介绍如何从零开始完成一个谣言检测任务。通过引入微博谣言数据集,基于Paddle框架使用RNN,lstm,bilstm,gru,bigru完成谣言文本的判断。

4.1.1 什么是谣言检测任务

传统的谣言检测模型一般根据谣言的内容、用户属性、传播方式人工地构造特征,而人工构建特征存在考虑片面、浪费人力等现象。本次实践使用基于循环神经网络(RNN)的谣言检测模型,将文本中的谣言事件向量化,通过循环神经网络的学习训练来挖掘表示文本深层的特征,避免了特征构建的问题,并能发现那些不容易被人发现的特征,从而产生更好的效果。

4.2、数据集

4.2.1 数据集概览

本次实践所使用的数据是从新浪微博不实信息举报平台抓取的中文谣言数据,数据集中共包含1538条谣言和1849条非谣言。如下图所示,每条数据均为json格式,其中text字段代表微博原文的文字内容。

更多数据集介绍请参考https://github.com/thunlp/Chinese_Rumor_Dataset。

4.2.2 数据集处理

(1)解压数据,读取并解析数据,生成all_data.txt

(2)生成数据字典,即dict.txt

(3)生成数据列表,并进行训练集与验证集的划分,train_list.txt 、eval_list.txt

(4)定义训练数据集提供器train_reader和验证数据集提供器eval_reader

#解压原始数据集,将Rumor_Dataset.zip解压至data目录下
import zipfile
import os
import random
from PIL import Image
from PIL import ImageEnhance
import json
 
src_path="/home/aistudio/data/data20519/Rumor_Dataset.zip"
target_path="/home/aistudio/data/Chinese_Rumor_Dataset-master"
if(not os.path.isdir(target_path)):
    z = zipfile.ZipFile(src_path, 'r')
    z.extractall(path=target_path)
    z.close()
#分别为谣言数据、非谣言数据、全部数据的文件路径
rumor_class_dirs = os.listdir(target_path+"/Chinese_Rumor_Dataset-master/CED_Dataset/rumor-repost/")
non_rumor_class_dirs = os.listdir(target_path+"/Chinese_Rumor_Dataset-master/CED_Dataset/non-rumor-repost/")
original_microblog = target_path+"/Chinese_Rumor_Dataset-master/CED_Dataset/original-microblog/"

#谣言标签为0,非谣言标签为1
rumor_label="0"
non_rumor_label="1"

#分别统计谣言数据与非谣言数据的总数
rumor_num = 0
non_rumor_num = 0

all_rumor_list = []
all_non_rumor_list = []


#解析谣言数据
for rumor_class_dir in rumor_class_dirs: 
    if(rumor_class_dir != '.DS_Store'):
        #遍历谣言数据,并解析
        with open(original_microblog + rumor_class_dir, 'r') as f:
	        rumor_content = f.read()
        rumor_dict = json.loads(rumor_content)
        all_rumor_list.append(rumor_label+"\t"+rumor_dict["text"]+"\n")
        rumor_num +=1


#解析非谣言数据
for non_rumor_class_dir in non_rumor_class_dirs: 
    if(non_rumor_class_dir != '.DS_Store'):
        with open(original_microblog + non_rumor_class_dir, 'r') as f2:
	        non_rumor_content = f2.read()
        non_rumor_dict = json.loads(non_rumor_content)
        all_non_rumor_list.append(non_rumor_label+"\t"+non_rumor_dict["text"]+"\n")
        non_rumor_num +=1
        
print("谣言数据总量为:"+str(rumor_num))
print("非谣言数据总量为:"+str(non_rumor_num))

谣言数据总量为:1538
非谣言数据总量为:1849
#全部数据进行乱序后写入all_data.txt

data_list_path="/home/aistudio/data/"
all_data_path=data_list_path + "all_data.txt"

all_data_list = all_rumor_list + all_non_rumor_list # 正负样本列表相加

random.shuffle(all_data_list)  # 打乱list

#在生成all_data.txt之前,首先将其清空
with open(all_data_path, 'w') as f:
    f.seek(0)
    f.truncate() 
    
with open(all_data_path, 'a') as f:
    for data in all_data_list: # 按行写入,一行一个样本
        f.write(data)

# with open(all_data_path,'r',encoding='UTF-8') as f:
#     print(f.read())

# 导入必要的包
%matplotlib inline
import os
from multiprocessing import cpu_count
import numpy as np
import shutil
import paddle
import paddle.fluid as fluid
from PIL import Image
import matplotlib.pyplot as plt

from matplotlib.font_manager import FontProperties
font = FontProperties(fname='simhei.ttf', size=16) 
# all_data_path= data_list_path + "all_data.txt"
# data_list_path="/home/aistudio/data/"
# dict_path = data_list_path + "dict.txt"
# 生成数据字典
def create_dict(data_path, dict_path):
    dict_set = set()  # 创建一个无序不重复元素集,集合
    # 读取全部数据
    with open(data_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
    # with open('data/test_lines.txt', 'w', encoding='utf-8') as f:
    #     f.write(str(lines))
        #print(lines)
    # 把数据生成一个元组
    for line in lines:
        content = line.split('\t')[-1].replace('\n', '') 
        # 以\t为分隔符,取最后一段。去掉每一行的换行符
        # print(line,content)
        for s in content:
            dict_set.add(s)
            # print(s,dict_set)  #将每一个字加入到一个无序不重复元素集中(元组)
    # 把集合转换成字典,一个字对应一个数字
    dict_list = []
    i = 0
    for s in dict_set:
        dict_list.append([s, i])
        i += 1
    # print(dict_list)
    # 添加未知字符
    dict_txt = dict(dict_list) # list转字典
    end_dict = {"<unk>": i}
    dict_txt.update(end_dict)
    # 把这些字典保存到本地中
    with open(dict_path, 'w', encoding='utf-8') as f:
        f.write(str(dict_txt))
    print("数据字典生成完成!")
    
    
# 获取字典的长度
def get_dict_len(dict_path):
    with open(dict_path, 'r', encoding='utf-8') as f:
        line = eval(f.readlines()[0])
    return len(line.keys())

create_dict('data/all_data.txt','data/dict.txt')
get_dict_len('data/dict.txt')
数据字典生成完成!





4410
# 创建序列化表示的数据,并按照一定比例划分训练数据与验证数据
def create_data_list(data_list_path):
    #在生成数据之前,首先将eval_list.txt和train_list.txt清空
    with open(os.path.join(data_list_path, 'eval_list.txt'), 'w', encoding='utf-8') as f_eval:
        f_eval.seek(0)
        f_eval.truncate()
        
    with open(os.path.join(data_list_path, 'train_list.txt'), 'w', encoding='utf-8') as f_train:
        f_train.seek(0)
        f_train.truncate() 
    
    with open(os.path.join(data_list_path, 'dict.txt'), 'r', encoding='utf-8') as f_data:
        dict_txt = eval(f_data.readlines()[0])
    # print(dict_txt)

    with open(os.path.join(data_list_path, 'all_data.txt'), 'r', encoding='utf-8') as f_data:
        lines = f_data.readlines()
    # print(lines)

    i = 0
    with open(os.path.join(data_list_path, 'eval_list.txt'), 'a', encoding='utf-8') as f_eval,open(os.path.join(data_list_path, 'train_list.txt'), 'a', encoding='utf-8') as f_train:
        for line in lines:
            # print(line)
            words = line.split('\t')[-1].replace('\n', '')
            label = line.split('\t')[0]
            labs = ""
            if i % 8 == 0:
                for s in words:
                    lab = str(dict_txt[s])
                    labs = labs + lab + ','
                labs = labs[:-1]
                labs = labs + '\t' + label + '\n'
                f_eval.write(labs)
            else:
                for s in words:
                    lab = str(dict_txt[s])
                    labs = labs + lab + ','
                labs = labs[:-1]
                labs = labs + '\t' + label + '\n'
                f_train.write(labs)
            i += 1
        
    print("数据列表生成完成!")
create_data_list('/home/aistudio/data/')
数据列表生成完成!
#dict_path为数据字典存放路径
#all_data_path= data_list_path + "all_data.txt"
#data_list_path="/home/aistudio/data/"
dict_path = data_list_path + "dict.txt"

#创建数据字典,存放位置:dict.txt。在生成之前先清空dict.txt
with open(dict_path, 'w') as f:
    f.seek(0)
    f.truncate() 
create_dict(all_data_path, dict_path)

#创建数据列表,存放位置:train_list.txt eval_list.txt
create_data_list(data_list_path)
    
数据字典生成完成!
数据列表生成完成!
def data_mapper(sample):
    data, label = sample
    data = [int(data) for data in data.split(',')]
    return data, int(label)

#定义数据读取器
def data_reader(data_path):
    def reader():
        with open(data_path, 'r') as f:
            lines = f.readlines()
            for line in lines:
                data, label = line.split('\t')
                yield data, label
    return paddle.reader.xmap_readers(data_mapper, reader, cpu_count(), 1024)
# 获取训练数据读取器和测试数据读取器,设置超参数
# data_list_path="/home/aistudio/data/"
BATCH_SIZE = 256

train_list_path = data_list_path+'train_list.txt'
eval_list_path = data_list_path+'eval_list.txt'

train_reader = paddle.batch(
		reader=data_reader(train_list_path), 
		batch_size=BATCH_SIZE)
eval_reader = paddle.batch(
		reader=data_reader(eval_list_path), 
		batch_size=BATCH_SIZE)

4.3、模型组网

数据准备的工作完成之后,接下来我们将动手来搭建一个循环神经网络,进行文本特征的提取,从而实现微博谣言检测。

4.3.1 搭建网络

4.3.1.1 rnn
def rnn_net(ipt, input_dim): #循环神经网络
    # 以数据的IDs作为输入, ipt 输入 数据集
    emb = fluid.layers.embedding(input=ipt, size=[input_dim, 128],is_sparse=True)
    drnn = fluid.layers.DynamicRNN()
    with drnn.block():
        # 将embedding标记为RNN的输入,每个时间步取句子中的一个字进行处理
        word=drnn.step_input(emb)
        # 将memory初始化为一个值为0的常量Tensor,shape=[batch_size, 200],其中batch_size由输入embedding决定
        memory = drnn.memory(shape=[200])
        hidden = fluid.layers.fc(input=[word, memory], size=200, act='relu')
        # 用hidden更新memory
        drnn.update_memory(ex_mem=memory, new_mem=hidden)
        # 将hidden标记为RNN的输出
        drnn.output(hidden)
    
    # 最大序列池操作
    fc = fluid.layers.sequence_pool(input=drnn(), pool_type='max')

    # 以softmax作为全连接的输出层,大小为2,也就是正负面
    out = fluid.layers.fc(input=fc, size=2, act='softmax')
    return out
4.3.1.2 lstm
def lstm_net(ipt, input_dim): # 长短期记忆网络
    # 以数据的IDs作为输入
    emb = fluid.layers.embedding(input=ipt, size=[input_dim, 128], is_sparse=True)
    # 第一个全连接层
    fc1 = fluid.layers.fc(input=emb, size=128)
    # 进行一个长短期记忆操作
    lstm1, _ = fluid.layers.dynamic_lstm(input=fc1, #返回:隐藏状态(hidden state),LSTM的神经元状态
                                         size=128) #size=4*hidden_size
    # 第一个最大序列池操作
    fc2 = fluid.layers.sequence_pool(input=fc1, pool_type='max')
    # 第二个最大序列池操作
    lstm2 = fluid.layers.sequence_pool(input=lstm1, pool_type='max')
    # 以softmax作为全连接的输出层,大小为2,也就是正负面
    out = fluid.layers.fc(input=[fc2, lstm2], size=2, act='softmax')
    return out
4.3.1.3 bilstm
def bilstm_net(ipt, input_dim): # 双向长短期神经网络
    # 以数据的IDs作为输入
    emb = fluid.layers.embedding(input=ipt, size=[input_dim, 128], is_sparse=True)
    # 第一个全连接层
    fc1_f = fluid.layers.fc(input=emb, size=128)
    fc1_b = fluid.layers.fc(input=emb, size=128)
    # 进行一个长短期记忆操作
    lstm1_f, _ = fluid.layers.dynamic_lstm(input=fc1_f, #返回:隐藏状态(hidden state),LSTM的神经元状态
                                         size=128) #size=4*hidden_size
    lstm1_b, _ = fluid.layers.dynamic_lstm(input=fc1_b, #返回:隐藏状态(hidden state),LSTM的神经元状态
                                        is_reverse = True,                
                                         size=128) #size=4*hidden_size                                     
    # 第一个最大序列池操作
    fc2_f = fluid.layers.sequence_pool(input=fc1_f, pool_type='max')
    fc2_b = fluid.layers.sequence_pool(input=fc1_b, pool_type='max')
    # 第二个最大序列池操作
    lstm2_f = fluid.layers.sequence_pool(input=lstm1_f, pool_type='max')
    lstm2_b = fluid.layers.sequence_pool(input=lstm1_b, pool_type='max')

    lstm2 = fluid.layers.concat(input=[lstm2_f, lstm2_b], axis=1)
    fc2 = fluid.layers.concat(input=[fc2_f, fc2_b], axis=1)
    # 以softmax作为全连接的输出层,大小为2,也就是正负面
    out = fluid.layers.fc(input=[fc2, lstm2], size=2, act='softmax')
    return out
4.3.1.4 gru
def gru_net(ipt, input_dim): # 门控循环单元
    # 以数据的IDs作为输入
    emb = fluid.layers.embedding(input=ipt, size=[input_dim, 128], is_sparse=True)
    # 第一个全连接层
    fc1 = fluid.layers.fc(input=emb, size=384)
    # 进行一个长短期记忆操作
    gru1= fluid.layers.dynamic_gru(input=fc1,size=128)
    # 第一个最大序列池操作
    fc2 = fluid.layers.sequence_pool(input=fc1, pool_type='max')
    # 第二个最大序列池操作
    gru2 = fluid.layers.sequence_pool(input=gru1, pool_type='max')
    # 以softmax作为全连接的输出层,大小为2,也就是正负面
    out = fluid.layers.fc(input=[fc2, gru2], size=2, act='softmax')
    return out
4.3.1.5 bigru
def bigru_net(ipt, input_dim): # 双向门控循环单元
    # 以数据的IDs作为输入
    emb = fluid.layers.embedding(input=ipt, size=[input_dim, 128], is_sparse=True)
    # 第一个全连接层
    fc1_f = fluid.layers.fc(input=emb, size=384)
    fc1_b = fluid.layers.fc(input=emb, size=384)
    # 进行一个长短期记忆操作
    gru1_f= fluid.layers.dynamic_gru(input=fc1_f,size=128)
    gru1_b= fluid.layers.dynamic_gru(input=fc1_b,size=128,is_reverse=True)
    
    # 第一个最大序列池操作
    fc2_f = fluid.layers.sequence_pool(input=fc1_f, pool_type='max')
    fc2_b = fluid.layers.sequence_pool(input=fc1_b, pool_type='max')
    
    # 第二个最大序列池操作
    gru2_f = fluid.layers.sequence_pool(input=gru1_f, pool_type='max')
    gru2_b = fluid.layers.sequence_pool(input=gru1_b, pool_type='max')
    
    gru2 = fluid.layers.concat(input=[gru2_f, gru2_b], axis=1)
    fc2 = fluid.layers.concat(input=[fc2_f, fc2_b], axis=1)

    # 以softmax作为全连接的输出层,大小为2,也就是正负面
    out = fluid.layers.fc(input=[fc2, gru2], size=2, act='softmax')
    return out

4.3.2 定义数据层

# 定义输入数据, lod_level不为0指定输入数据为序列数据
paddle.enable_static()
words = fluid.data(name='words', shape=[None,1], dtype='int64', lod_level=1) 
label = fluid.data(name='label', shape=[None,1], dtype='int64') 

4.3.3 获取分类器

# 获取数据字典长度
dict_dim = get_dict_len(dict_path)
# 获取分类器
# model = rnn_net(words, dict_dim) 
# model = lstm_net(words, dict_dim) 
model = bilstm_net(words, dict_dim) 
# model = gru_net(words, dict_dim) 
# model = bigru_net(words, dict_dim) 

4.3.4 定义损失函数和准确率

定义了一个损失函数之后,还有对它求平均值,因为定义的是一个Batch的损失值。

同时我们还可以定义一个准确率函数,这个可以在我们训练的时候输出分类的准确率。

# 获取损失函数和准确率
cost = fluid.layers.cross_entropy(input=model, label=label)
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=model, label=label)

# 获取预测程序
test_program = fluid.default_main_program().clone(for_test=True)

4.3.5 定义优化方法

# 定义优化方法
optimizer = fluid.optimizer.AdagradOptimizer(learning_rate=0.001)
opt = optimizer.minimize(avg_cost)

4.4、训练网络

4.4.1 创建Executor

# use_cuda为False,表示运算场所为CPU;use_cuda为True,表示运算场所为GPU 
use_cuda = True 
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
exe = fluid.Executor(place)  
# 进行参数初始化
exe.run(fluid.default_startup_program())
W0221 21:51:10.279976 21097 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1
W0221 21:51:10.283764 21097 device_context.cc:465] device: 0, cuDNN Version: 7.6.





[]

4.4.2 定义数据映射器

DataFeeder负责将数据提供器(train_reader,test_reader)返回的数据转成一种特殊的数据结构,使其可以输入到Executor中。

feed_list设置向模型输入的向变量表或者变量表名

# 定义数据映射器
feeder = fluid.DataFeeder(place=place, feed_list=[words, label])

4.4.3 展示模型训练曲线

all_train_iter=0
all_train_iters=[]
all_train_costs=[]
all_train_accs=[]

all_eval_iter=0
all_eval_iters=[]
all_eval_costs=[]
all_eval_accs=[]


def draw_process(title,iters,costs,accs,label_cost,lable_acc):
    plt.title(title, fontsize=24)
    plt.xlabel("iter", fontsize=20)
    plt.ylabel("cost/acc", fontsize=20)
    plt.plot(iters, costs,color='red',label=label_cost) 
    plt.plot(iters, accs,color='green',label=lable_acc) 
    plt.legend()
    plt.grid()
    plt.show()

4.4.4 训练并保存模型

Executor接收传入的program,并根据feed map(输入映射表)和fetch_list(结果获取表) 向program中添加feed operators(数据输入算子)和fetch operators(结果获取算子)。

feed map为该program提供输入数据。fetch_list提供program训练结束后用户预期的变量。

每一轮训练结束之后,再使用验证集进行验证,并求出相应的损失值Cost和准确率acc。

EPOCH_NUM=20                                      #训练轮数
model_save_dir = '/home/aistudio/work/infer_model/' #模型保存路径
# 开始训练

for pass_id in range(EPOCH_NUM):
    # 进行训练
    print('epoch:',int(pass_id)+1)
    for batch_id, data in enumerate(train_reader()):
        train_cost, train_acc = exe.run(program=fluid.default_main_program(),
                             feed=feeder.feed(data),
                             fetch_list=[avg_cost, acc])
        all_train_iter=all_train_iter+BATCH_SIZE
        all_train_iters.append(all_train_iter)
        all_train_costs.append(train_cost[0])
        all_train_accs.append(train_acc[0])
        
        if batch_id % 10 == 0:
            print('Pass:%d, Batch:%d, Cost:%0.5f, Acc:%0.5f' % (pass_id, batch_id, train_cost[0], train_acc[0]))
    # 进行验证
    eval_costs = []
    eval_accs = []
    for batch_id, data in enumerate(eval_reader()):
        eval_cost, eval_acc = exe.run(program=test_program,
                                              feed=feeder.feed(data),
                                              fetch_list=[avg_cost, acc])
        eval_costs.append(eval_cost[0])
        eval_accs.append(eval_acc[0])
        
        all_eval_iter=all_eval_iter+BATCH_SIZE
        all_eval_iters.append(all_eval_iter)
        all_eval_costs.append(eval_cost[0])                                       
        all_eval_accs.append(eval_acc[0])      
    # 计算平均预测损失在和准确率
    eval_cost = (sum(eval_costs) / len(eval_costs))
    eval_acc = (sum(eval_accs) / len(eval_accs))
    print('Test:%d, Cost:%0.5f, ACC:%0.5f' % (pass_id, eval_cost, eval_acc))

# 保存模型
if not os.path.exists(model_save_dir): 
    os.makedirs(model_save_dir) 
fluid.io.save_inference_model(model_save_dir, 
                            feeded_var_names=[words.name], 
                            target_vars=[model], 
                            executor=exe)
print('训练模型保存完成!') 

draw_process("train",all_train_iters,all_train_costs,all_train_accs,"trainning cost","trainning acc")
draw_process("eval",all_eval_iters,all_eval_costs,all_eval_accs,"evaling cost","evaling acc")
epoch: 1
Pass:0, Batch:0, Cost:0.69911, Acc:0.47266
Pass:0, Batch:10, Cost:0.66337, Acc:0.56250
Test:0, Cost:0.66470, ACC:0.53683
epoch: 2
Pass:1, Batch:0, Cost:0.65396, Acc:0.55469
Pass:1, Batch:10, Cost:0.63739, Acc:0.62500
Test:1, Cost:0.64528, ACC:0.63030
epoch: 3
Pass:2, Batch:0, Cost:0.63112, Acc:0.70703
Pass:2, Batch:10, Cost:0.61620, Acc:0.71875
Test:2, Cost:0.62902, ACC:0.69699
epoch: 4
Pass:3, Batch:0, Cost:0.61294, Acc:0.77344
Pass:3, Batch:10, Cost:0.59836, Acc:0.76953
Test:3, Cost:0.61558, ACC:0.71670
epoch: 5
Pass:4, Batch:0, Cost:0.59646, Acc:0.78906
Pass:4, Batch:10, Cost:0.58233, Acc:0.79688
Test:4, Cost:0.60275, ACC:0.73047
epoch: 6
Pass:5, Batch:0, Cost:0.58203, Acc:0.80078
Pass:5, Batch:10, Cost:0.56806, Acc:0.81250
Test:5, Cost:0.59103, ACC:0.76581
epoch: 7
Pass:6, Batch:0, Cost:0.56877, Acc:0.81641
Pass:6, Batch:10, Cost:0.55464, Acc:0.81250
Test:6, Cost:0.57990, ACC:0.77269
epoch: 8
Pass:7, Batch:0, Cost:0.55748, Acc:0.82422
Pass:7, Batch:10, Cost:0.54018, Acc:0.83984
Test:7, Cost:0.57005, ACC:0.78162
epoch: 9
Pass:8, Batch:0, Cost:0.54539, Acc:0.83203
Pass:8, Batch:10, Cost:0.52911, Acc:0.85156
Test:8, Cost:0.56025, ACC:0.78757
epoch: 10
Pass:9, Batch:0, Cost:0.53370, Acc:0.83594
Pass:9, Batch:10, Cost:0.52003, Acc:0.85547
Test:9, Cost:0.55105, ACC:0.79846
epoch: 11
Pass:10, Batch:0, Cost:0.52343, Acc:0.84375
Pass:10, Batch:10, Cost:0.50598, Acc:0.87500
Test:10, Cost:0.54268, ACC:0.79548
epoch: 12
Pass:11, Batch:0, Cost:0.51352, Acc:0.84375
Pass:11, Batch:10, Cost:0.49801, Acc:0.87891
Test:11, Cost:0.53439, ACC:0.80134
epoch: 13
Pass:12, Batch:0, Cost:0.50305, Acc:0.84766
Pass:12, Batch:10, Cost:0.48714, Acc:0.89453
Test:12, Cost:0.52690, ACC:0.80627
epoch: 14
Pass:13, Batch:0, Cost:0.49415, Acc:0.83984
Pass:13, Batch:10, Cost:0.47674, Acc:0.89844
Test:13, Cost:0.51935, ACC:0.81017
epoch: 15
Pass:14, Batch:0, Cost:0.48502, Acc:0.84375
Pass:14, Batch:10, Cost:0.46768, Acc:0.89844
Test:14, Cost:0.51199, ACC:0.82199
epoch: 16
Pass:15, Batch:0, Cost:0.47742, Acc:0.85156
Pass:15, Batch:10, Cost:0.45791, Acc:0.89844
Test:15, Cost:0.50501, ACC:0.82394
epoch: 17
Pass:16, Batch:0, Cost:0.46781, Acc:0.85938
Pass:16, Batch:10, Cost:0.45186, Acc:0.89844
Test:16, Cost:0.49864, ACC:0.82394
epoch: 18
Pass:17, Batch:0, Cost:0.45989, Acc:0.86328
Pass:17, Batch:10, Cost:0.44218, Acc:0.89453
Test:17, Cost:0.49238, ACC:0.82292
epoch: 19
Pass:18, Batch:0, Cost:0.45258, Acc:0.85938
Pass:18, Batch:10, Cost:0.43367, Acc:0.89453
Test:18, Cost:0.48672, ACC:0.82487
epoch: 20
Pass:19, Batch:0, Cost:0.44533, Acc:0.85547
Pass:19, Batch:10, Cost:0.42680, Acc:0.89453
Test:19, Cost:0.48060, ACC:0.82096
训练模型保存完成!


/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/matplotlib/cbook/__init__.py:2349: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
  if isinstance(obj, collections.Iterator):
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/matplotlib/cbook/__init__.py:2366: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
  return list(data) if isinstance(data, collections.MappingView) else data

4.5、模型预测

# 用训练好的模型进行预测并输出预测结果
# 创建执行器
place = fluid.CPUPlace()
infer_exe = fluid.Executor(place)
infer_exe.run(fluid.default_startup_program())

save_path = '/home/aistudio/work/infer_model/'

# 从模型中获取预测程序、输入数据名称列表、分类器
[infer_program, feeded_var_names, target_var] = fluid.io.load_inference_model(dirname=save_path, executor=infer_exe)


# 获取数据
def get_data(sentence):
    # 读取数据字典
    with open('/home/aistudio/data/dict.txt', 'r', encoding='utf-8') as f_data:
        dict_txt = eval(f_data.readlines()[0])
    dict_txt = dict(dict_txt)
    # 把字符串数据转换成列表数据
    keys = dict_txt.keys()
    data = []
    for s in sentence:
        # 判断是否存在未知字符
        if not s in keys:
            s = '<unk>'
        data.append(int(dict_txt[s]))
    return data


data = []
# 获取图片数据
data1 = get_data('兴仁县今天抢小孩没抢走,把孩子母亲捅了一刀,看见这车的注意了,真事,车牌号辽HFM055!!!!!赶紧散播! 都别带孩子出去瞎转悠了 尤其别让老人自己带孩子出去 太危险了 注意了!!!!辽HFM055北京现代朗动,在各学校门口抢小孩!!!110已经 证实!!全市通缉!!')
data2 = get_data('重庆真实新闻:2016年6月1日在重庆梁平县袁驿镇发生一起抢儿童事件,做案人三个中年男人,在三中学校到镇街上的一条小路上,把小孩直接弄晕(儿童是袁驿新幼儿园中班的一名学生),正准备带走时被家长及时发现用棒子赶走了做案人,故此获救!请各位同胞们以此引起非常重视,希望大家有爱心的人传递下')
data3 = get_data('@尾熊C 要提前预习育儿知识的话,建议看一些小巫写的书,嘻嘻')
data.append(data1)
data.append(data2)
data.append(data3)

# 获取每句话的单词数量
base_shape = [[len(c) for c in data]]

# 生成预测数据
tensor_words = fluid.create_lod_tensor(data, base_shape, place)

# 执行预测
result = exe.run(program=infer_program,
                 feed={feeded_var_names[0]: tensor_words},
                 fetch_list=target_var)

# 分类名称
names = [ '谣言', '非谣言']

# 获取结果概率最大的label
for i in range(len(data)):
    lab = np.argsort(result)[0][i][-1]
)
data.append(data2)
data.append(data3)

# 获取每句话的单词数量
base_shape = [[len(c) for c in data]]

# 生成预测数据
tensor_words = fluid.create_lod_tensor(data, base_shape, place)

# 执行预测
result = exe.run(program=infer_program,
                 feed={feeded_var_names[0]: tensor_words},
                 fetch_list=target_var)

# 分类名称
names = [ '谣言', '非谣言']

# 获取结果概率最大的label
for i in range(len(data)):
    lab = np.argsort(result)[0][i][-1]
    print('预测结果标签为:%d, 分类为:%s, 概率为:%f' % (lab, names[lab], result[0][i][lab]))
预测结果标签为:0, 分类为:谣言, 概率为:0.722868
预测结果标签为:0, 分类为:谣言, 概率为:0.754371
预测结果标签为:1, 分类为:非谣言, 概率为:0.577661

五、总结

  • 展示的项目使用了RNN,LSTM,BiLSTM,GRU和BiGRU分别进行谣言分类

  • 相比与vanilla RNN(原始RNN),其变体(LSTM,GRU)在实际项目中运用比较广泛

  • 双向循环神经网络虽然可以更全面的结合文本信息进行预测,但是带来了更多参数,减慢了模型训练速度。

  • 采用其他网络的谣言检测任务可以在本项目的基础上修改实现

引用


1.(澳)图珊·加内格达拉著;马恩驰,陆健译. TensorFlow自然语言处理[M]. 北京:机械工业出版社, 2019.06.


2. 深度循环神经网络和双向循环神经网络学习笔记


3.基于PaddlePaddle的微博谣言检测

作者简介

作者 : f_g

研究方向 :多模态,假新闻检测

深度学习初学者,希望可以互相学习共同进步

我在AI Studio上获得白银等级,点亮2个徽章,来互关呀~ https://aistudio.baidu.com/aistudio/personalcenter/thirdview/788652

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐