记录一下,很久之前看的论文-基于RNN来从微博中检测谣言及其代码复现。

1 引言

现有传统谣言检测模型使用经典的机器学习算法,这些算法利用了根据帖子的内容、用户特征和扩散模式手工制作的各种特征,或者简单地利用使用正则表达式表达的模式来发现推特中的谣言(规则加词典)。

特征工程是至关重要的,但手工特征工程是繁琐复杂、有偏见和耗时费力的。例如,图1中的两个时间序列图描述了典型的谣言信号的浅层模式。虽然它们可以表明谣言和非谣言事件的时间特征(微博文本中关键词的时序变化),但这两种情况之间的差异对于特征工程来说既不明确,也不明显。

  另一方面,深度神经网络在许多机器学习问题上已经显示出了明显的优势。本文利用了循环神经网络RNN来进行有效的谣言检测。RNN适用于处理社交媒体中的文本(retweet)流的序列性质 。这是因为RNN可以捕获谣言传播的动态时序特性。

本文提出基于RNN的方法,将谣言检测视为一个序列分类问题。具体地,本文将社会上下文信息(源微博的转帖文本或相关帖子文本)建模为可变长度的时间序列,然后用RNN来学习捕获微博相关帖子的上下文特征随时间的变化。

2 模型

2.1 问题描述

  • 基于事件的谣言检测(单个微博帖子都很短,上下文非常有限。Claim通常与一些与Claim相关的帖子有关)

  • 事件集E={ E_i }, E_i= { (m_i,j,t_i,j) },事件Ei由时间戳 ti,j内的帖子 mi,j组成

  • 任务是判断每一个Event是谣言还是不是谣言

2.2 数据预处理-构造可变长度时间序列 

  将输入的序列中的post进行划分,从而将处理后的序列长度限定在在一定范围。

可将每个帖子建模作为一个输入实例,并构建一个序列长度等于帖子数的时间序列的用于RNN建模。然而,一个流行的事件可能会有成千上万个的帖子。我们只有一个输出单元(仅适用最终隐状态,有信息瓶颈问题)来指示在每个事件的最后一个时间步长中的类。通过大量的时间步长进行反向传播,而只有一个最后阶段的损失,计算代价高昂且无效的。(处理长序列时,RNN的BPTT存在的梯度消失问题会导致有偏的权重,即离Loss越远的时间步的梯度对参数的贡献越小,从而使其难以建模好长期依赖

  因此,为了妥善处理短时间内密集的帖子序列,本文将一批帖子构成一个时间间隔,并将它作为一个时间序列中的一个输入单元,然后使用RNN进行序列建模。简而言之,就是将原始的帖子序列按相对时间间隔划分成固定长度(例如k个)的子序列,其中子序列中帖子的数量不一定相同。

具体地,给定事件相关帖子的数据集,先将每条帖子视为输入实例,其序列长度等于帖子数量。进一步将帖子按照时间间隔进行批处理,视为时间序列中的单元,然后使用RNN序列进行建模,采用RNN序列的参考长度来构造时间序列。

动态时间序列算法:

    1. 将整个事件线均分为N个internal,形成初始集合U0;
    2. 遍历U0,删除没有包含帖子的internal,形成U1;
    3. 从U1中选出总时间跨度最长的连续internal,形成集合U2(找到一个最长的时间序列)
    4. 如果U2中internal的数量小于N且大于之前一轮,将internal减半,返回步骤1,继续分区(使最终internal数量接近N);
    5. 否则,返回该总时间跨度最长的连续internal集合U2。

       根据上述算法,其实现如下所示(针对常用的微博数据集,其每一个样本的原始信息存储在JSON文件中):

def load_rawdata(file_path):
""" json file, like a list of dict """
with open(file_path, encoding="utf-8") as f:
data = json.loads(f.read())
return data def GetContinueInterval(inter_index):
"""根据初步划分的间隔索引列表,得出最大连续间隔的索引"""
max_inters = []
temp_inters = [inter_index[0]]
for q in range(1, len(inter_index)):
if inter_index[q] - inter_index[q - 1] > 1:
if len(temp_inters) > len(max_inters):
max_inters = temp_inters
temp_inters = [inter_index[q]]
else:
temp_inters.append(inter_index[q]) if len(max_inters) == 0:
max_inters = temp_inters return max_inters def ConstructSeries(tweet_list, interval_num, time_interval):
"""基于相对时间间隔,按照时间戳对post序列进行划分
Params:
tweet_list (list), 由Post Index以及时间戳二元组构成的序列
interval_num (int), 依据基准序列长度N,计算出的当前序列的时间间隔数
time_interval (float), 单位时间间隔长度
Returns:
Output (list), 划分好的post batch,每一个batch包含的一个时间间隔内的post
inter_index (list), Interval的index列表
"""
# 遍历每一个间隔
tweet_index = 0
output, inter_index = [], []
start_time = tweet_list[0][1]
for inter in range(0, interval_num):
non_empty = 0
interval_post = [] # 存储当前间隔内的post
for q in range(tweet_index, len(tweet_list)):
if start_time <= tweet_list[q][1] < start_time + time_interval:
non_empty += 1
interval_post.append(tweet_list[q][0])
elif tweet_list[q][1] >= start_time + time_interval:
# 记录超出interval的tweet位置,下次可直接从此开始
tweet_index = q - 1
break if non_empty == 0:
output.append([]) # 空间隔不会记录其索引
else:
if tweet_list[-1][1] == start_time + time_interval:
interval_post.append(tweet_list[-1][0]) # add the last tweet inter_index.append(inter)
output.append(interval_post)
start_time = start_time + time_interval # 更新间隔开始时间 return output, inter_index

   以下代码为动态时间序列算法主函数,其中N为RNN的参考长度,即超参数:

def SplitSequence(weibo_id, N=50):
"""将source post对应的posts划分成不定长的post batch序列
Params:
weibo_id (str), source post对应的id,用于读取对应数据
N (int), 时间序列的基准time steps个数
Returns:
output (list), interval list, 每一个interval包含一定数量的post index
"""
# 不同时间间隔内的post数量不必相同)
path = "Weibo" + os_sep + "{}.json".format(weibo_id)
data = load_rawdata(data_path + path) # 基于weibo id加载包含转帖文本及时间戳的原始数据
tweet_list = [(idx, tweet["t"]) for idx, tweet in enumerate(data)]
total_timespan = tweet_list[-1][1] - tweet_list[0][1] # L(i)
time_interval = total_timespan / N # l k = 0
pre_max_inters = [] # U_(k_1)
while True:
# Spliting series by the current time interval
k += 1
interval_num = int(total_timespan / time_interval)
output, inter_index = ConstructSeries(tweet_list, interval_num, time_interval)
max_inters = GetContinueInterval(inter_index) # maximum continue interval index
if len(pre_max_inters) < len(max_inters) < N:
time_interval = int(time_interval * 0.5) # Shorten the intervals
pre_max_inters = max_inters
if time_interval == 0:
output = output[max_inters[0]:max_inters[-1] + 1]
break
else:
output = output[max_inters[0]:max_inters[-1] + 1]
break return output

2.3 模型结构(two-layer GRU)

    首先,将每一个post的tf-idf向量和一个词嵌入矩阵相乘,这等价于加权求和词向量。由于本文较老,词嵌入是基于监督信号从头开始学习的,而非使用word2vec或预训练的BERT。

  以下是加载数据的部分的代码。为了便于实现,这里并没有使用torch自带的dataset和dataloader,也没有没有对序列进行截断和填充。

class Data():
def __init__(self, text_data):
self.text_data = text_data def get_wordindices(self):
return [torch.from_numpy(inter_text) for inter_text in self.text_data] def load_data(ids):
""" 依据weibo的id,加载所有的结点特征
Params:
ids (list), 微博id list
Returns:
instance_list: a list of numpy ndarray, 每一个numpy ndarray是一个B by k的tf-idf矩阵
"""
instance_list = []
for weibo_id in tqdm(ids):
text_matrix = load_sptext(weibo_id).toarray() # 所有post的numpy tfidx矩阵
split_interval = SplitSequence(weibo_id)
text_data = [text_matrix[interval] for interval in split_interval]
instance_list.append(Data(text_data))
return instance_list

  模型代码:本文的模型对每一个时间间隔内的post的embedding直接使用了最大池化操作。

class GlobalMaxPool1d(nn.Module):
def __init__(self):
super(GlobalMaxPool1d, self).__init__() def forward(self, x):
return torch.max_pool1d(x, kernel_size=x.shape[2]) class GRU2_origin(nn.Module):
def __init__(self, dim_in, dim_word, dim_hid, dim_out):
"""
Detecting Rumors with Recurrent Neural Network-IJCAI16 :Params:
dim_in (int): post的初始输入特征维度 k
dim_word(int): word嵌入的维度
dim_hid (int): GRU hidden unit
dim_out (int): 模型最终的输出维度,用于分类
"""
super(GRU2_origin, self).__init__()
self.word_embeddings = nn.Parameter(nn.init.xavier_uniform_(
torch.zeros(dim_in, dim_word, dtype=torch.float, device=device), gain=np.sqrt(2.0)), requires_grad=True) # GRU for modeling the temporal dynamics
rnn_num_layers = 2
self.MaxPooling = GlobalMaxPool1d()
self.GRU = nn.GRU(dim_word, dim_hid, rnn_num_layers)
self.H0 = torch.zeros(rnn_num_layers, 1, dim_hid, device=device)
self.prediction_layer = nn.Linear(dim_hid, dim_out)
nn.init.xavier_normal_(self.prediction_layer.weight) def forward(self, text_data):
batch_posts = []
for idx in range(len(text_data)):
# words_indices is a sparse tf-idf vector with N * 5000 dimension
words_indices = text_data[idx].to(device)
tmp_posts = []
for i in range(words_indices.shape[0]):
word_indice = torch.nonzero(words_indices[i], as_tuple=True)[0]
if word_indice.shape[0] == 0:
word_indice = torch.tensor([0], dtype=torch.long).to(device) words = self.word_embeddings.index_select(0, word_indice) # select out embeddings
word_tensor = words_indices[i][word_indice].unsqueeze(dim=0) # select out weights
post_embedding = word_tensor.mm(words).squeeze(dim=1)
tmp_posts.append(post_embedding) # Interval中的post batch取平均 (矩阵乘法)
tmp_embeddings = torch.cat(tmp_posts, dim=0).unsqueeze(1)
batch_embedding = self.MaxPooling(tmp_embeddings.transpose(0, 2)) # transpose(0, 2)
batch_posts.append(batch_embedding.squeeze(1).transpose(0, 1)) x = torch.cat(batch_posts, dim=0)
gru_output, _ = self.GRU(x.unsqueeze(1), self.H0)
return self.prediction_layer(gru_output[-1]) # Using the last hidden vector of GRU

  后续的完整的数据加载、模型初始化、训练和评估,可自行添加。

3 实验

  模型训练设置:

  1. 使用TF-IDF来获取post的初始文本表示
  2. AdaGrad算法进行参数更新
  3. 根据经验,将词汇量大小为k=5000,待从头学的词嵌入维度为100,隐藏单元的尺寸为100,学习率为0.5

  实验结果:

4 总结

  这篇文章算是将深度学习用于虚假信息检测的开山之作,开始了利用深度网络来自动提取具备判别性的高阶特征的范式,后续很多文章都是在此基础上改进的。

  由于文章较老,所以在目前看,待改进的点其实挺多的。首先要注意,原始的TF-IDF特征一般不能在全局数据上提取(训练集、验证集和测试集,暂不考虑半监督的情况),相同的词的在验证集和测试集的TF-IDF特征和训练集取同样的值。而对于新出现的词,取默认值。推广到一般情况,如果提取特征时,不区分训练测试,或许使用了相应特征的对比方法取得的结果过于乐观,并不符合实际情况。

  此外,可以考虑文本特征的获取、序列的层次化建模、注意力机制、其他特征的使用(用户信息、传播结构特征)、外部知识的引入(知识图谱)、非线性传播结构的利用、多任务学习(结合立场分类)等等。

  值得注意的是,当算法实际应用时,并不是越复杂的模型的效果就越好,而且需要考虑实际的业务需求和数据。有时候,或许假设简单、模型结构简单的算法或许在大量人工特征的引入和大量数据的支持下,也能取得不错的效果,毕竟数据决定算法的上限。

Detecting Rumors from Microblogs with Recurrent Neural Networks(IJCAI-16)的更多相关文章

  1. Recurrent Neural Network(递归神经网络)

    递归神经网络(RNN),是两种人工神经网络的总称,一种是时间递归神经网络(recurrent neural network),另一种是结构递归神经网络(recursive neural network ...

  2. AntisymmetricRNN: A Dynamical System View on Recurrent Neural Networks(解析)

    原文链接:https://arxiv.org/abs/1902.09689 发表在:ICLR 2019 ------------------------------------------------ ...

  3. 循环神经网络(RNN, Recurrent Neural Networks)介绍(转载)

    循环神经网络(RNN, Recurrent Neural Networks)介绍    这篇文章很多内容是参考:http://www.wildml.com/2015/09/recurrent-neur ...

  4. Attention and Augmented Recurrent Neural Networks

    Attention and Augmented Recurrent Neural Networks CHRIS OLAHGoogle Brain SHAN CARTERGoogle Brain Sep ...

  5. cs231n spring 2017 lecture10 Recurrent Neural Networks 听课笔记

    (没太听明白,下次重新听一遍) 1. Recurrent Neural Networks

  6. 第十四章——循环神经网络(Recurrent Neural Networks)(第一部分)

    由于本章过长,分为两个部分,这是第一部分. 这几年提到RNN,一般指Recurrent Neural Networks,至于翻译成循环神经网络还是递归神经网络都可以.wiki上面把Recurrent ...

  7. 第十四章——循环神经网络(Recurrent Neural Networks)(第二部分)

    本章共两部分,这是第二部分: 第十四章--循环神经网络(Recurrent Neural Networks)(第一部分) 第十四章--循环神经网络(Recurrent Neural Networks) ...

  8. Pixel Recurrent Neural Networks翻译

    Pixel Recurrent Neural Networks 目前主要在用的文档存放: https://www.yuque.com/lart/papers/prnn github存档: https: ...

  9. 循环神经网络(Recurrent Neural Networks, RNN)介绍

    目录 1 什么是RNNs 2 RNNs能干什么 2.1 语言模型与文本生成Language Modeling and Generating Text 2.2 机器翻译Machine Translati ...

随机推荐

  1. Oracle 错误表

    ORA-00001: 违反唯一约束条件 (.) ORA-00017: 请求会话以设置跟踪事件 ORA-00018: 超出最大会话数 ORA-00019: 超出最大会话许可数 ORA-00020: 超出 ...

  2. python基础-基本数据类型(一)

    一.什么是数据类型 编程语言通过计算机的一些物理底层机制创造出不同类型的数据,用来表示现实世界中的不同信息,以便于计算机更好的存储和计算. python中常见的数据类型有: 1.数值类型 名称 描述 ...

  3. spring boot的配置文件

    1.SpringBootApplication是标志启动类,启动后可以把这个类所在的包资源发布到服务器,不用再启动tomcat 2.利用spring boot工程可以和以前一样直接在Controlll ...

  4. 《Streaming Systems》第二章: 数据处理中的 What, Where, When, How

    本章中,我们将通过对 What,Where,When,How 这 4 个问题的回答,逐步揭开流处理过程的全貌. What:计算什么结果? 也就是我们进行数据处理的目的,答案是转换(transforma ...

  5. 10个 Linux 命令,让你的操作更有效率

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 根据老九大师兄口头阐述,Linux是最适合开发的操作系统 ...

  6. acunetix_14.7安装破解

    acunetix_14.7.220401065版 本次更新增加了许多的漏洞检测,包括Spring4Shell漏洞(CVE-2022-22965) 下载地址: https://pan.baidu.com ...

  7. UDP协议、操作系统、同步与异步、阻塞与非阻塞

    UDP协议 # 客户端 import socket server = socket.socket(type=socket.SOCK_DGRAM) server.bind(('127.0.0.1', 8 ...

  8. Python-100-Days-master-第二周笔记

    python100day学习第二周 # 通过enumerate函数处理列表之后再遍历可以同时获得元素索引和值 list1 = [1, 3, 5, 7, 100] for index, elem in ...

  9. c# 读取所有磁盘的剩余空间

    介绍: 有一个控制台命令是创建指定大小的空文件,因此我想制作一个一键填充剩余磁盘空间的坑人小程序. 想要填充剩余容量,就要先获取所有本地磁盘的剩余空间,这个程序就是用来做这个的. 项目类型为c#控制台 ...

  10. 【Axure】母版引发事件

    引发事件是指你将母版中某一元件的事件从母版中提升出来,以使其在页面的级别可用. 通过引发事件,可以对在不同页面上母版实例的同一个元件设置不同的交互. 设置引发事件 打开一个母版: 选择其中一个组件: ...