第19天:NLP实战(三)——用CNN实现微博谣言检测

时间:2022-07-24
本文章向大家介绍第19天:NLP实战(三)——用CNN实现微博谣言检测,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

  接着上次的项目,主要是为了熟悉我们对NLP知识的实际应用,接着上次对深度学习中的DNN的简单应用相信大家对深度学习的相关知识以及相应的实现流程有了一个初步的了解,今天接着上次的项目,通过用CNN对微博谣言检测进行实现。很明显这是个二分类的问题,因此,我们可以用到朴素贝叶斯或者逻辑回归以及支持向量机都可以解决这个问题,另外在深度学习中,我们可以用CNN-Text或者RNN以及LSTM等模型最好,之所以本次用到CNN就是通过本次项目介绍让大家对CNN有一个更深层次的了解。接下来,我们详细给大家介绍项目。

任务介绍

  人们常说“流言止于智者”,要想不被网上的流言和谣言盅惑、伤害,首先需要对其进行科学甄别,而时下人工智能正在尝试担任这一角色。那么,在打假一线AI技术如何做到去伪存真?传统的谣言检测模型一般根据谣言的内容、用户属性、传播方式人工地构造特征,而人工构建特征存在考虑片面、浪费人力等现象。本次实践使用基于卷积神经网络(CNN)的谣言检测模型,将文本中的谣言事件向量化,通过循环神经网络的学习训练来挖掘表示文本深层的特征,避免了特征构建的问题,并能发现那些不容易被人发现的特征,从而产生更好的效果。

数据集介绍

  本次实践所使用的数据[验证码:u0is]是从新浪微博不实信息举报平台抓取的中文谣言数据,数据集中共包含1538条谣言和1849条非谣言。如下图所示,每条数据均为json格式,其中text字段代表微博原文的文字内容。这里需要说明的是,本人在自己百度网盘上传了一份数据集,大家可以下载使用,具体链接,前面介绍了,另外如果感觉数据集不够庞大,可以从中文谣言数据中下载更多的数据集,我上传的那份数据集也是来自这里。接下来我们大概看看数据集的构成。其中的文件夹如图所示:

  每个文件夹里又有很多新闻文本。

  每个文本又是json格式,具体内容如下:

  实验环境搭建以及实验平台前几篇文章均介绍过,只要是跑了上一篇文章的这次实验环境不用动,要是没跑的可以去跑一遍上一个项目:NLP实战(二)——用DNN实现手势识别,当然也可以按照前面介绍搭建好环境可以直接跑本项目。

项目实现

1、数据预处理

  首先导入必要的第三方库:

import zipfile
import os
import io
import random
import json
import matplotlib.pyplot as plt
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D, Linear, Embedding
from paddle.fluid.dygraph.base import to_variable

  接下来就是导入数据的操作,具体步骤如下:

  • (1)解压数据,读取并解析数据,生成all_data.txt
  • (2)生成数据字典,即dict.txt
  • (3)生成数据列表,并进行训练集与验证集的划分,train_list.txt 、eval_list.txt
  • (4)定义训练数据集提供器train_reader和验证数据集提供器eval_reader
#解压原始数据集,将Rumor_Dataset.zip解压至data目录下
src_path="/home/aistudio/data/data36807/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))

#全部数据进行乱序后写入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)

#在生成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) 
print('all_data.txt已生成')

具体结果如下:

  接下来就是生成数据字典。

# 生成数据字典
def create_dict(data_path, dict_path):
    with open(dict_path, 'w') as f:
        f.seek(0)
        f.truncate() 

    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("数据字典生成完成!",'t','字典长度为:',len(dict_list))

  我们可以查看一下dict_txt的内容

  接下来就是数据列表的生成

# 创建序列化表示的数据,并按照一定比例划分训练数据与验证数据
def create_data_list(data_list_path):
    
    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:
            title = line.split('t')[-1].replace('n', '')
            lab = line.split('t')[0]
            t_ids = ""
            if i % 8 == 0:
                for s in title:
                    temp = str(dict_txt[s])
                    t_ids = t_ids + temp + ','
                t_ids = t_ids[:-1] + 't' + lab + 'n'
                f_eval.write(t_ids)
            else:
                for s in title:
                    temp = str(dict_txt[s])
                    t_ids = t_ids + temp + ','
                t_ids = t_ids[:-1] + 't' + lab + 'n'
                f_train.write(t_ids)
            i += 1
        
    print("数据列表生成完成!")

  我们可以查看一下数据列表的内容

  我们生成数据列表

data_root_path = "/home/aistudio/data/"  #写自己的路径
data_path = os.path.join(data_root_path, 'all_data.txt')
dict_path = os.path.join(data_root_path, "dict.txt")
# 创建数据字典
create_dict(data_path, dict_path)

#先清空已有txt文件
with open(os.path.join(data_root_path, 'train_list.txt'), 'w', encoding='utf-8') as f_eval:
        f_eval.seek(0)
        f_eval.truncate()
        
with open(os.path.join(data_root_path, 'eval_list.txt'), 'w', encoding='utf-8') as f_train:
        f_train.seek(0)
        f_train.truncate()

# 创建数据列表
create_data_list(data_root_path)

  我们可以留意一下数据列表生成的长度。

  我们可以看看数据列表的具体内容

  最后就是定义数据读取器

def data_reader(file_path, phrase, shuffle=False):
    all_data = []
    with io.open(file_path, "r", encoding='utf8') as fin:
        for line in fin:
            cols = line.strip().split("t")
            if len(cols) != 2:
                continue
            label = int(cols[1])
            
            wids = cols[0].split(",")
            all_data.append((wids, label))

    if shuffle:
        if phrase == "train":
            random.shuffle(all_data)

    def reader():
        for doc, label in all_data:
            yield doc, label
    return reader
class SentaProcessor(object):
    def __init__(self, data_dir,):
        self.data_dir = data_dir
        
    def get_train_data(self, data_dir, shuffle):
        return data_reader((self.data_dir + "train_list.txt"), 
                            "train", shuffle)

    def get_eval_data(self, data_dir, shuffle):
        return data_reader((self.data_dir + "eval_list.txt"), 
                            "eval", shuffle)

    def data_generator(self, batch_size, phase='train', shuffle=True):
        if phase == "train":
            return paddle.batch(
                self.get_train_data(self.data_dir, shuffle),
                batch_size,
                drop_last=True)
        elif phase == "eval":
            return paddle.batch(
                self.get_eval_data(self.data_dir, shuffle),
                batch_size,
                drop_last=True)
        else:
            raise ValueError(
                "Unknown phase, which should be in ['train', 'eval']")

  总之在数据处理这一块需要我们注意的是一共生成以下的几个文件。

2、配置网络

  接下来就是构建以及配置卷积神经网络(Convolutional Neural Networks, CNN),开篇也说了,其实这里有很多模型的选择,之所以选择CNN是因为让我们熟悉CNN的相关实现。 输入词向量序列,产生一个特征图(feature map),对特征图采用时间维度上的最大池化(max pooling over time)操作得到此卷积核对应的整句话的特征,最后,将所有卷积核得到的特征拼接起来即为文本的定长向量表示,对于文本分类问题,将其连接至softmax即构建出完整的模型。在实际应用中,我们会使用多个卷积核来处理句子,窗口大小相同的卷积核堆叠起来形成一个矩阵,这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的卷积核来处理句子。具体的流程如下:

  首先我们构建单层CNN神经网络。

#单层
class SimpleConvPool(fluid.dygraph.Layer):
    def __init__(self,
                 num_channels, # 通道数
                 num_filters,  # 卷积核数量
                 filter_size,  # 卷积核大小
                 batch_size=None): # 16
        super(SimpleConvPool, self).__init__()
        self.batch_size = batch_size
        self._conv2d = Conv2D(num_channels = num_channels,
            num_filters = num_filters,
            filter_size = filter_size,
            act='tanh')
        self._pool2d = fluid.dygraph.Pool2D(
            pool_size = (150 - filter_size[0]+1,1),
            pool_type = 'max',
            pool_stride=1
        )

    def forward(self, inputs):
        # print('SimpleConvPool_inputs数据纬度',inputs.shape) # [16, 1, 148, 128]
        x = self._conv2d(inputs)
        x = self._pool2d(x)
        x = fluid.layers.reshape(x, shape=[self.batch_size, -1])
        return x


class CNN(fluid.dygraph.Layer):
    def __init__(self):
        super(CNN, self).__init__()
        self.dict_dim = train_parameters["vocab_size"]
        self.emb_dim = 128   #emb纬度
        self.hid_dim = [32]  #卷积核数量
        self.fc_hid_dim = 96  #fc参数纬度
        self.class_dim = 2    #分类数
        self.channels = 1     #输入通道数
        self.win_size = [[3, 128]]  # 卷积核尺寸
        self.batch_size = train_parameters["batch_size"] 
        self.seq_len = train_parameters["padding_size"]
        self.embedding = Embedding( 
            size=[self.dict_dim + 1, self.emb_dim],
            dtype='float32', 
            is_sparse=False)
        self._simple_conv_pool_1 = SimpleConvPool(
            self.channels,
            self.hid_dim[0],
            self.win_size[0],
            batch_size=self.batch_size)
        self._fc1 = Linear(input_dim = self.hid_dim[0],
                            output_dim = self.fc_hid_dim,
                            act="tanh")
        self._fc_prediction = Linear(input_dim = self.fc_hid_dim,
                                    output_dim = self.class_dim,
                                    act="softmax")

    def forward(self, inputs, label=None):

        emb = self.embedding(inputs) # [2400, 128]
        # print('CNN_emb',emb.shape)  
        emb = fluid.layers.reshape(   # [16, 1, 150, 128]
            emb, shape=[-1, self.channels , self.seq_len, self.emb_dim])
        # print('CNN_emb',emb.shape)
        conv_3 = self._simple_conv_pool_1(emb)
        fc_1 = self._fc1(conv_3)
        prediction = self._fc_prediction(fc_1)
        if label is not None:
            acc = fluid.layers.accuracy(prediction, label=label)
            return prediction, acc
        else:
            return prediction

  接下来就是参数的配置,不过为了在模型训练过程中更直观的查看我们训练的准确率,我们首先利用python的matplotlib.pyplt函数实现一个可视化图,具体的实现如下:

def draw_train_process(iters, train_loss, train_accs):
    title="training loss/training accs"
    plt.title(title, fontsize=24)
    plt.xlabel("iter", fontsize=14)
    plt.ylabel("loss/acc", fontsize=14)
    plt.plot(iters, train_loss, color='red', label='training loss')
    plt.plot(iters, train_accs, color='green', label='training accs')
    plt.legend()
    plt.grid()
    plt.show()

  然后就是参数的配置了

'''
参数配置
'''
train_parameters = {
    "epoch": 50,                              #训练轮次
    "batch_size": 128,                        #批次大小
    "adam":0.006,                              #学习率
    "padding_size": 150,                     #padding纬度
    "vocab_size": 4409,                     #字典大小
    "skip_steps":30,                         #每N个批次输出一次结果
    "save_steps": 60,                        #每M个批次保存一次
    "checkpoints":"data/"                    #保存路径

3、模型训练

def train():
    with fluid.dygraph.guard(place = fluid.CUDAPlace(0)): # 因为要进行很大规模的训练,因此我们用的是GPU,如果没有安装GPU的可以使用下面一句,把这句代码注释掉即可
    # with fluid.dygraph.guard(place = fluid.CPUPlace()):

        processor = SentaProcessor( data_dir="data/")
    
        train_data_generator = processor.data_generator(
            batch_size=train_parameters["batch_size"],
            phase='train',
            shuffle=True)
            
        model = CNN()
        sgd_optimizer = fluid.optimizer.Adagrad(learning_rate=train_parameters["adam"],parameter_list=model.parameters())
        steps = 0
        Iters,total_loss, total_acc = [], [], []
        for eop in range(train_parameters["epoch"]):
            for batch_id, data in enumerate(train_data_generator()):
                steps += 1
                #转换为 variable 类型
                doc = to_variable(
                    np.array([
                        np.pad(x[0][0:train_parameters["padding_size"]],  #对句子进行padding,全部填补为定长150
                              (0, train_parameters["padding_size"] - len(x[0][0:train_parameters["padding_size"]])),
                               'constant',
                              constant_values=(train_parameters["vocab_size"])) # 用 <unk> 的id 进行填补
                        for x in data
                    ]).astype('int64').reshape(-1))
                #转换为 variable 类型
                label = to_variable(
                    np.array([x[1] for x in data]).astype('int64').reshape(
                        train_parameters["batch_size"], 1))

                model.train() #使用训练模式
                prediction, acc = model(doc, label)
                loss = fluid.layers.cross_entropy(prediction, label)
                avg_loss = fluid.layers.mean(loss)
                avg_loss.backward()
                sgd_optimizer.minimize(avg_loss)
                model.clear_gradients()
                
                if steps % train_parameters["skip_steps"] == 0:
                    Iters.append(steps)
                    total_loss.append(avg_loss.numpy()[0])
                    total_acc.append(acc.numpy()[0])
                    print("eop: %d, step: %d, ave loss: %f, ave acc: %f" %
                         (eop, steps,avg_loss.numpy(),acc.numpy()))
                if steps % train_parameters["save_steps"] == 0:
                    save_path = train_parameters["checkpoints"]+"/"+"save_dir_" + str(steps)
                    print('save model to: ' + save_path)
                    fluid.dygraph.save_dygraph(model.state_dict(),
                                                   save_path)
                # break
    draw_train_process(Iters, total_loss, total_acc)

  开始训练

train()

  训练的过程以及训练的结果如下:

4、模型评估

def to_eval():
    with fluid.dygraph.guard(place = fluid.CUDAPlace(0)):
        processor = SentaProcessor(data_dir="data/") #写自己的路径

        eval_data_generator = processor.data_generator(
                batch_size=train_parameters["batch_size"],
                phase='eval',
                shuffle=False)

        model_eval = CNN() #示例化模型
        model, _ = fluid.load_dygraph("data//save_dir_180.pdparams") #写自己的路径
        model_eval.load_dict(model)

        model_eval.eval() # 切换为eval模式
        total_eval_cost, total_eval_acc = [], []
        for eval_batch_id, eval_data in enumerate(eval_data_generator()):
            eval_np_doc = np.array([np.pad(x[0][0:train_parameters["padding_size"]],
                                    (0, train_parameters["padding_size"] -len(x[0][0:train_parameters["padding_size"]])),
                                    'constant',
                                    constant_values=(train_parameters["vocab_size"]))
                            for x in eval_data
                            ]).astype('int64').reshape(-1)
            eval_label = to_variable(
                                    np.array([x[1] for x in eval_data]).astype(
                                    'int64').reshape(train_parameters["batch_size"], 1))
            eval_doc = to_variable(eval_np_doc)
            eval_prediction, eval_acc = model_eval(eval_doc, eval_label)
            loss = fluid.layers.cross_entropy(eval_prediction, eval_label)
            avg_loss = fluid.layers.mean(loss)
            total_eval_cost.append(avg_loss.numpy()[0])
            total_eval_acc.append(eval_acc.numpy()[0])

    print("Final validation result: ave loss: %f, ave acc: %f" %
        (np.mean(total_eval_cost), np.mean(total_eval_acc) ))   
to_eval()

评估准确率如下:

  结果还可以,这里说明的是,刚开始我们的模型训练评估不可能这么好,可能存在过拟合或者欠拟合的问题,不过更常见的是过拟合,这就需要我们调整我们的epoch、batchsize、激活函数的选择以及优化器、学习率等各种参数,通过不断的调试、训练最好可以得到不错的结果,但是,如果还要更好的模型效果,其实可以将CNN换为更为合适的RNN、LSTM以及bi-LSTM模型会好很多,我们下篇文章在为大家介绍。最后就是我们的模型的预测。

5、模型预测

# 获取数据
def load_data(sentence):
    # 读取数据字典
    with open('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
train_parameters["batch_size"] = 1
lab = [ '谣言', '非谣言']
 
with fluid.dygraph.guard(place = fluid.CUDAPlace(0)):
    
    data = load_data('兴仁县今天抢小孩没抢走,把孩子母亲捅了一刀,看见这车的注意了,真事,车牌号辽HFM055!!!!!赶紧散播! 都别带孩子出去瞎转悠了 尤其别让老人自己带孩子出去 太危险了 注意了!!!!辽HFM055北京现代朗动,在各学校门口抢小孩!!!110已经 证实!!全市通缉!!')
    data_np = np.array(data)
    data_np = np.array(np.pad(data_np,(0,150-len(data_np)),"constant",constant_values =train_parameters["vocab_size"])).astype('int64').reshape(-1)

    infer_np_doc = to_variable(data_np)
   
    model_infer = CNN()
    model, _ = fluid.load_dygraph("data/save_dir_900.pdparams")
    model_infer.load_dict(model)
    model_infer.eval()
    result = model_infer(infer_np_doc)
    print('预测结果为:', lab[np.argmax(result.numpy())])

我们最后查看其预测的结果:

  训练的结果还是挺满意的,到此为止,我们的本次项目实验到此结束。

总结

  本文通过一个实际问题:微博谣言检测来更好的理解深度学习CNN原理以及从数据的处理到模型的预测整个流程过了一遍,并且还详细解释了在训练过程中可能遇到的问题。这些代码均可运行,希望大家在有时间的情况下,可以动手运行一遍,感受一下CNN的原理以及在实际中是如何被应用的,另外就是感受一下一个网络的训练过程和预测过程。接下来的一篇文章会给大家介绍RNN以及LSTM在自然语言处理中的应用,练习完之后感觉收获满满。加油,希望我们都学有所获,坚持练习,我们未来可期。