用飞桨检测谣言,新技能get!

2020-03-04 10:03:50 浏览数 (1)

【飞桨开发者说】文瑞洁,中科院信工所工程师,主要研究领域:深度学习、自然语言处理。

本实验代码已在AI Studio公开,访问链接进入:

https://aistudio.baidu.com/aistudio/projectdetail/263255

社交媒体的发展在加速信息传播的同时,也带来了虚假谣言信息的泛滥,往往会引发诸多不安定因素,并对经济和社会产生巨大的影响。

2016年美国总统大选期间,受访选民平均每人每天接触到4篇虚假新闻,虚假新闻被认为影响了2016年美国大选和英国脱欧的投票结果;近期,在新型冠状病毒感染的肺炎疫情防控的关键期,在全国人民都为疫情揪心时,网上各种有关疫情防控的谣言接连不断,从“广州公交线路因新型冠状病毒肺炎疫情停运”到“北京市为防控疫情采取封城措施”,从“钟南山院士被感染”到“10万人感染肺炎”等等,这些不切实际的谣言,“操纵”了舆论感情,误导了公众的判断,更影响了社会稳定

人们常说“流言止于智者”,要想不被网上的流言和谣言盅惑、伤害,首先需要对其进行科学甄别,而时下人工智能正在尝试担任这一角色。那么,在打假一线AI技术如何做到去伪存真?

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

如何基于飞桨实现谣言检测

飞桨是以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心框架、基础模型库、端到端开发套件、工具组件和服务平台于一体,2016 年正式开源,是全面开源开放、技术领先、功能完备的产业级深度学习平台。

下面我将为大家展示如何用飞桨API 编程并搭建一个循环神经网络(Recurrent Neural Network,RNN),进行谣言检测。主要分为五个步骤,数据准备、模型配置、模型训练、模型评估以及最后使用训练好的模型进行预测

本实践代码运行的环境配置如下:Python版本为3.7,PaddlePaddle版本为1.6.2,操作系统为Windows 64位操作系统。

01

步骤1:数据准备

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

更多数据集介绍请参考:

https://github.com/thunlp/Chinese_Rumor_Dataset

首先对数据集进行预处理,从所有json文件(包括谣言和非谣言)中解析得到text字段,即微博文本。经过处理后的数据文件为all_data.txt,如下图所示,第一维表示每条数据所对应的标签,0、1分别代表谣言、非谣言,第二维代表的是从原始json数据中提取的text字段,即微博文本。

接下来,生成字典序列,即词与实数的对应关系。

代码语言:javascript复制
#生成数据字典
def create_dict(data_path, dict_path):
    dict_set = set()
    # 读取全部数据
    with open(data_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
    for line in lines:
        content = line.split('t')[-1].replace('n', '')
        for s in content:
            dict_set.add(s)
    # 把元组转换成字典,一个字对应一个数字
    dict_list = []
    i = 0
    for s in dict_set:
        dict_list.append([s, i])
        i  = 1
    # 添加未知字符
    dict_txt = dict(dict_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())

生成的字典文件为dict.txt,如下图所示:

接下来根据数据列表all_data.txt和字典列表dict.txt来生成最终参与网络训练的序列化数据,并按照一定比例划分训练集与验证集。

代码语言:javascript复制
# 创建序列化表示的数据,并按照一定比例划分训练数据与验证数据
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])

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

    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:
            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("数据列表生成完成!")

划分后的训练集和验证集文件分别为train_list.txt和eval_list.txt,如下图所示:

了解了数据的基本信息之后,我们需要有一个用于获取谣言数据的数据提供器,在这里我们定义为data_reader(),它的作用就是提供文本以及文本对应的标签。

代码语言:javascript复制
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)

paddle.reader.xmap_readers()是paddle提供的一个方法,功能是多线程下,使用自定义映射器 reader 返回样本到输出队列(详细介绍可在以下链接查看:

https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/io_cn/xmap_readers_cn.html#xmap-readers)

有了数据提供器data_reader()后,我们就可以很简洁的得到用于训练的数据提供器和用于验证的数据提供提供器了,BATCH_SIZE是一个批次的大小,在这里我们设定为128。

代码语言:javascript复制
# 获取训练数据读取器和测试数据读取器
BATCH_SIZE = 128
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)

02

步骤2:模型配置

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

(1) 模型定义

我们知道,如下图所示,前馈神经网络,比如多层感知机和卷积神经网络,就像一个复杂的函数,理论上可以完成从确定形式的输入到确定形式的输出的任何映射。然而,前馈神经网络只能完成信息的单向传递,这一特性虽然使得模型容易训练,但也在某种程度上限制了模型的能力。

在实际任务中,经常会出现模型的输入不仅与当前时刻的输入有关,还与过去某个时刻的状态有关。这就需要一种能力更强的模型——循环神经网络(Recurrent Neural Network,RNN)。如下图所示,循环神经网络通过使用带自反馈(隐藏层)的神经元,能够处理任意长度的序列。循环神经网络比前馈神经网络更加符合生物神经网络的结构。已经被广泛应用在语音识别、图像处理、语言模型以及自然语言生成等任务上。

假设现在有个更为复杂的任务,考虑到下面这句话“I grewup in France… I speak fluent French.”,现在需要语言模型通过现有以前的文字信息预测该句话的最后一个字。通过以前文字语境可以预测出最后一个字是某种语言,但是要猜测出French,要根据之前的France语境。这样的任务,有用信息与需要进行处理信息的地方之间的距离较远,这样容易导致RNN不能学习到有用的信息,最终推导的任务可能失败。

对于上述任务,解决方案就是引入长短时记忆网络。长短时记忆神经网络(Long Short-Term Memory Neural Network,LSTM)是循环神经网络的一个变体,可以有效地解决长期依赖问题/梯度消失问题。LSTM 模型的关键是引入了一组记忆单元(Memory Units),允许网络可以学习何时遗忘历史信息,何时用新信息更新记忆单元。

飞桨 API中dynamic_lstm接口已经给我们实现了 LSTM。

代码语言:javascript复制
# 定义长短期记忆网络
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, 
                                         size=128) 
    # 第一个最大序列池操作
    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

上述代码中, sequence_pool是一个用作对不等长序列进行池化的接口,它将每一个实例的全部时间步的特征进行池化。

接下来进行数据层的定义。

代码语言:javascript复制
# 定义输入数据, lod_level不为0指定输入数据为序列数据
words = fluid.data(name='words', shape=[None,1], dtype='int64', lod_level=1)
label = fluid.data(name='label', shape=[None,1], dtype='int64')

上面我们定义好了卷积神经网络结构,这里我们使用定义好的网络来获取分类器。

代码语言:javascript复制
# 获取数据字典长度
dict_dim = get_dict_len(dict_path)
# 获取分类器
model = lstm_net(words, dict_dim)  

(2)损失函数

接着是定义损失函数,这里使用的是交叉熵损失函数,该函数在分类任务上比较常用。定义了一个损失函数之后,还要对它求平均值,因为定义的是一个Batch的损失值。同时还可以定义一个准确率函数,可以在训练的时候输出分类的准确率。

代码语言:javascript复制
# 获取损失函数和准确率
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()。

代码语言:javascript复制
test_program=fluid.default_main_program().clone(for_test=True)

(3)优化方法

接着定义优化算法,这里使用的是Adam优化算法,指定学习率为0.0001。

代码语言:javascript复制
# 定义优化方法
optimizer =fluid.optimizer.AdagradOptimizer(learning_rate=0.001)
opt = optimizer.minimize(avg_cost)

用户完成网络定义后,一段Paddle程序中通常存在两个Program:

fluid.default_startup_program:定义了创建模型参数,输入输出,以及模型中可学习参数的初始化等各种操作,由框架自动生成,使用时无需显示地创建;

fluid.default_main_program :定义了神经网络模型,前向反向计算,以及优化算法对网络中可学习参数的更新,使用飞桨的核心就是构建起 default_main_program。

03

步骤3:模型训练

在上一步骤中定义好了网络模型,即构造好了两个核心Program,接下来将介绍飞桨如何使用Excutor来执行Program。首先在正式进行网络训练前,首先进行参数初始化。

代码语言:javascript复制
use_cuda = True                              
place = fluid.CUDAPlace(0) if use_cudaelse fluid.CPUPlace()#定义训练设备为CPU或GPU
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())     #执行参数初始化操作

定义好模型训练需要的Executor,在执行训练之前,需要告知网络传入的数据分为两部分,第一部分是words值,第二部分是label值:

代码语言:javascript复制
feeder = fluid.DataFeeder(place=place,feed_list=[words, label])

之后就可以进行正式的训练了,本实践中设置训练轮数50。在Executor的run方法中,feed代表以字典的形式定义了数据传入网络的顺序,feeder在上述代码中已经进行了定义, 将data[0]、data[1]分别传给words、label。fetch_list定义了网络的输出。

在每轮训练中,每100个batch,打印一次训练平均误差和准确率。每轮训练完成后,使用验证集进行一次验证。

代码语言:javascript复制
EPOCH_NUM=50
model_save_dir = '/home/aistudio/work/infer_model/'
# 开始训练

for pass_id in range(EPOCH_NUM):
    # 进行训练
    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])

        if batch_id % 100 == 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])
    # 计算平均预测损失在和准确率
    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))

每轮训练完成后,对模型进行一次保存,使用飞桨提供的fluid.io.save_inference_model()进行模型保存:

代码语言:javascript复制
model_save_dir = "/home/aistudio/work/model"
    if not os.path.exists(model_save_dir):
        os.makedirs(model_save_dir)
    # 保存训练的模型,executor 把所有相关参数保存到 dirname 中
    fluid.io.save_inference_model(model_save_dir,#保存模型的路径
                                  [words.name],    #预测需要 feed 的数据
                                  [model],    #保存预测结果的变量
                                  exe)         #executor 保存预测模型

04

步骤4:模型评估

通过观察训练过程中误差和准确率随着迭代次数的变化趋势,可以对网络训练结果进行评估。

通过上图可以观察到,在训练和验证过程中平均误差是在逐步降低的,与此同时,训练与验证的准确率逐步趋近于100%。

05

步骤5:模型预测

前面已经进行了模型训练,并保存了训练好的模型。接下来就可以使用训练好的模型进行谣言检测了。

为了进行预测,我们任意选取3个微博文本。我们把文本中的每个词对应到dict中的id。如果词典中没有这个词,则设为unknown。然后我们用create_lod_tensor来创建细节层次的张量。

代码语言:javascript复制
# 用训练好的模型进行预测并输出预测结果
# 创建执行器
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_ (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]))

预测结果如下图所示:

至此,恭喜您!已经成功使用飞桨核心框架实现了谣言检测。本实践代码已在AI Studio上公开

0 人点赞