Modeling Conversation Structure and Temporal Dynamics for Jointly Predicting Rumor Stance and Veracity(ACL-19)

LeonYi發表於2022-07-09

  記錄一下,論文建模對話結構和時序動態來聯合預測謠言立場和真實性及其程式碼復現。

1 引言

  之前的研究發現,公眾對謠言訊息的立場是識別流行的謠言的關鍵訊號,這也能表明它們的真實性。因此,對謠言的立場分類被視為謠言真實性預測的重要前置步驟,特別是在推特對話的背景下。

1.1 建模推特對話結構

  一些先進的謠言立場分類方法試圖模擬Twitter對話的序列屬性或時序屬性。在本文中,我們提出了一個基於結構屬性的新視角:通過聚合相鄰推文的資訊來學習推文表示。

  直觀地看,一條推文在對話束中的鄰居比更遠的鄰居更有資訊,因為它們的對話關係更接近,它們的立場表達有助於中心推文的立場進行分類。例如,在圖1中,推文“1”、“4”和“5”是推文“2”的一跳鄰居,它們對預測“2”立場的影響較大 兩跳鄰居“3”)。本文用圖卷積網路(GCN)來在潛空間中聯合表示推文內容和對話結構,它旨在通過聚合其鄰居來學習每條推文的姿態特徵的特性。本文基於訊息傳遞的方法來利用對話中的內在結構以學習推文表示。

1.2 編碼立場時序動態

  此外,為了確定人們反應的立場,另一個挑戰是我們如何利用公眾的立場來準確地預測謠言的真實性。本文觀察到,公眾立場的時序動態可以表明謠言的真實性。圖2分別顯示了討論真實謠言、虛假謠言和不真實謠言的推文的立場分佈 。

  正如我們所看到的,支援的立場主導了傳播的開始階段。然而,隨著時間的推移,否認針對虛假謠言的推文的比例顯著增加。與此同時,詢問針對尚未驗證謠言的推文的比例也顯示出上升趨勢。基於這一觀察結果,本文進一步提出用RNN來建模立場演化的時序動態,捕獲立場中包含立場特徵的關鍵訊號,以進行有效的真實性預測。

1.3 聯合立場分類和真實性預測

  此外,現有的方法大多分別處理立場分類和真實性預測,這一方式是次優的,它限制了模型的泛化。如前所示,這兩個任務是密切相關的,其中立場分類可以提供指示性線索,以促進真實性預測。因此,聯合建模這兩個任務可以更好地利用它們之間的相互關係(互相幫助)

1.4  Hierarchical multi-task learning framework for jointly Predicting rumor Stance and Veracity

  基於以上考慮,本文提出了一種層次化的多工學習框架來聯合預測謠言的立場和真實性,從而深度整合了謠言的立場分類任務和真實性預測任務

  本文框架的底部元件通過基於聚合的結構建模,對討論謠言的對話中的推文的立場進行了分類,並設計了一種為會話結構定製的新的圖卷積操作。最上面的元件通過利用立場演化的時間動態來預測謠言的真實性,同時考慮到內容特徵和由底部元件學習到的立場特徵。兩個元件被聯合訓練,以利用兩個任務之間的相互關係來學習更強大的特徵表示。

2 模型

2.1 問題描述

  考慮一個推特對話束$C$,它由一個源推文$t_1$和一些回覆推文${ \{ t_2,t_3,\dots,t_{|C|} \}}$直接或間接響應$t_1$,此外每個推文$t_1 (i \in [1,|C|])$表達了它對謠言的立場。對話束C是一個樹狀結構,其中源tweet $t_1$是根節點,而推文之間的回覆關係構成了edges。

  本文主要關注兩個任務:第一個任務是謠言立場分類,其目的是確定$C$中每條推文的立場,屬於$\{supporting, denying, querying, commenting\}$ 第二個任務是預測謠言的真實性,目的是確定謠言的真實性,屬於$\{true, f alse, unverif ied\}$。

2.2 Hierarchical-PSV

  本文出了一個層次的多工學習框架來聯合預測謠言的立場和真實性(Hierarchical-PSV),圖3說明了其整體架構。

  其底部的元件是對會話結構中的tweet的立場進行分類(節點分類任務),它通過使用定製的圖卷積(Conversational-GCN)對會話結構進行編碼來學習立場特徵。最重要的部分是預測謠言的真實性,它考慮了從底部的部分學習到的特徵,並用一個迴圈神經網路(Stance-Aware RNN)來建模立場演化的時序動態。

2.3 Conversational-GCN: Aggregation-basedStructure Modeling for Stance Prediction 

   其輸入是鄰接矩陣$\mathbf{A}$(非0項,代表節點對之間的對話、評論或轉發關係)和節點初始特徵(基於詞嵌入和BiGRU編碼得到推文文字表示)。

   Conversational-GCN相比GCN的改進就是將其原始的濾波器,即自環歸一化領接矩陣變為了二階領接矩陣且去除歸一化操作,來擴大感受野(鄰居範圍)。

  之所以這樣做,是因為本文認為:

  1)樹狀的對話結構可能非常深,這意味著在本文的例子中,GCN層的感受野受到了限制。雖然我們可以堆疊多個GCN層來擴充套件感受野,然而在處理深度的對話結構仍有困難而且會增加引數量。

  2)歸一化矩陣$\hat {\mathbf{A}}$在一定程度上削弱了其推文的重要性。

  Conversational-GCN可以表示為如下的矩陣乘法形式:

  具體來說,第一個GCN層將所有推文的內容特徵作為輸入,而最後一個GCN層的輸出表示對話中所有的推文的立場特徵$\hat {\mathbf{s}}$。

  在這裡,立場分類即節點分類任務。

2.4 Stance-Aware RNN: Temporal Dynamics Modeling for Veracity Prediction

  Stance-Aware RNN試圖建模推文立場的時序動態變化,它在每個時間步的輸入包括原始推文文字表示$\mathbf{c}$還有推文的立場特徵$\mathbf{s}_i$。經過GRU後,其所有時間步隱狀態通過最大池化,從而獲得捕獲了立場演化的全域性資訊的表示通過$\mathbf{v}$。

  在這裡,真實度預測被視為序列分類任務:

2.5 Jointly Learning Two Tasks 

  為了利用前一個任務(立場分類)和後續任務(真實性預測)之間的相互關係,本文聯合訓練了兩個元件。具體來說,這兩個任務的損失函式相加得到一個最終損失函式L,並被聯合優化以學習模型。

2.6 復現程式碼

  在實現中,由於沒有立場監督訊號,故沒有立場分類這一部分。此外,為了便於實現,在ConversationGCN處使用了常規的GCN。

# -*- coding: utf-8 -*-
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn  import GCNConv
from tqdm  import tqdm

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 ConvGCN(nn.Module):
    def __init__( self , dim_in, dim_hid, dim_out, dim_stance = 4 , dropout = 0.5 ):
        """
        Conversational-GCN

        :Params:
            T (int):              the number of splited timesteps for propagation graph
            dim_in (int):         結點的初始輸入特徵維度 k
            dim_hid (int):        預設(固定)的結點嵌入的維度(等於結點池化後的圖的嵌入維度)
            dim_out (int):        模型最終的輸出維度,用於分類
            num_layers (int):     LGAT的層數(鄰居聚集的迭代次數)
            dropout (float):
        """
        super (ConvGCN,  self ).__init__()
        self .dropout  = dropout
        # dim_hid -> embed_size
        self .word_embeddings  = nn.Parameter(nn.init.xavier_uniform_(
            torch.zeros(dim_in, dim_hid, dtype = torch. float , device = device), gain = np.sqrt( 2.0 )), requires_grad = True )

        # BiGRU for obtaining post embedding (batch_size = 1)
        bigru_num_layers  = 1
        bigru_num_directions  = 2
        bigru_hidden  = dim_hid  / / 2
        self .BiGRU  = nn.GRU(dim_hid, bigru_hidden, bigru_num_layers, bidirectional = True )
        self .H0  = torch.zeros(bigru_num_layers  * bigru_num_directions,  1 , bigru_hidden, device = device)

        # Graph Convolution
        self .conv1  = GCNConv(dim_hid, dim_hid)
        self .conv2  = GCNConv(dim_hid, dim_stance)

        # GRU for modeling the temporal dynamics
        rnn_num_layers  = 1 # rnn_hidden = dim_hid
        self .GRU  = nn.GRU(dim_hid + dim_stance, dim_hid + dim_stance, rnn_num_layers)
        self .H1  = torch.zeros(rnn_num_layers,  1 , dim_hid + dim_stance, device = device)
       
        self .MaxPooling  = GlobalMaxPool1d()
        self .prediction_layer  = nn.Linear(dim_hid + dim_stance, dim_out)
        nn.init.xavier_normal_( self .prediction_layer.weight)

    def forward( self , words_indices, edge_indices):
        features  = []
        edge_indice  = edge_indices[ 0 ].to(device)   # only one snapshot
        words_indices  = words_indices.to(device)   # get post embedding
        for i  in range (words_indices.shape[ 0 ]):
            word_indice  = torch.nonzero(words_indices[i], as_tuple = True )[ 0 ]
            # assert word_indice.shape[0] > 0, "words must large or equal to one"
            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.to(device))
            _, hn  = self .BiGRU(words.unsqueeze( 1 ),  self .H0)   # (num_layers * num_directions, batch, hidden_size)
            post_embedding  = hn.flatten().unsqueeze( 0 )
            features.append(post_embedding)

        x0  = torch.cat(features, dim = 0 )
        content  = torch.clone(x0)
        x1  = self .conv1(x0, edge_indice)
        x1  = F.relu(x1)
        x1  = F.dropout(x1,  self .dropout)
        x2  = self .conv2(x1, edge_indice)
        x2  = F.relu(x2)

        x  = torch.cat((content, x2),  1 )
        gru_output, _  = self .GRU(x.unsqueeze( 1 ),  self .H1)
        z  = self .MaxPooling(gru_output.transpose( 0 ,  2 ))
        return self .prediction_layer(z.squeeze( 1 ).transpose_( 0 ,  1 ))   # 使用BCE(不需事先計算softmax)

  資料載入

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 get_edges(data):
    """依據src_id's data,載入propagation network edges with relative index in a graph"""
    tweet_num  = len (data)
    mids  = [tweet[ "mid" ]  for tweet  in data]            # 使用"mid"才能找到所有轉發關係
    mids_id  = {mids[i]: i  for i  in range (tweet_num)}   # mid: id  from mid to index,按順序生成mid的id
    mid_edges  = [(mid, data[mids_id[mid]][ "parent" ])  for mid  in mids  if data[mids_id[mid]][ "parent" ] ! = None ]
    return [(mids_id[edge[ 0 ]], mids_id[edge[ 1 ]])  for edge  in mid_edges]


def get_static_edgeindex(edges):
    """ 獲取傳播圖的edge_indices列表,要保證得到的絕對邊索引對應結點特徵 (for second)"""
    graph  = nx.Graph()     # len(split_edges)= T,和新增的邊相關的結點就是Interacting結點
    graph.add_edges_from(edges)
    edge_index  = list (nx.adjacency_matrix(graph).nonzero())    # 結點和邊的Id以及安排了,不需再處理
    return edge_index


def load_data(ids, T, thres_num, is_dynamic = False ):
    """
    依據weibo的id,載入所有的結點特徵和傳播結構

    :params:
        weibo_id (string):    微博id
    :returns:
        graph_list: a list of Diffusion graph objects which contain elements as follows:
            1.features:       numpy ndarray          結點數N by 特徵維度k的結點初始嵌入矩陣
            2.edge_indices:  list(numpy ndarray)    對話結構的鄰接矩陣
    """
    graph_list  = []
    for weibo_id  in ids:
        txt_data  = load_sptext(weibo_id)   # 從磁碟上載入以稀疏矩陣儲存的text matrix
        data  = load_rawdata(data_path  + "Weibo/{0}.json" . format (weibo_id))
        edges  = get_edges(data)
        edge_indices  = get_static_edgeindex(edges)
        graph_list.append(DiffGraph(txt_data, edge_indices))

    return graph_list   # 直接把graph list中的DiffGraph拿來Pickle

class DiffGraph():
    def __init__( self , text_data, edge_indices):
        self .text_data  = text_data
        self .edge_indices  = [torch.tensor(edges, dtype = torch. long )  for edges  in edge_indices]

    def get_wordindices( self ):
        return torch.from_numpy( self .text_data.toarray())

  其餘的資料載入和劃分、模型初始化以及訓練和評估的程式碼可自行新增。

3 實驗

3.1 兩個任務的對比實驗

3.2 超引數實驗

  可以觀察到,Conversational-GCN在大多數深度水平上明顯優於原始-GCN和BranchLSTMBranchLSTM可能更喜歡在對話中釋出“較淺”的推文,因為他它們經常使用出現在多個分支中推文。結果表明,Conversational-GCN在識別對話中釋出的“深度”推文立場方面具有優勢。  

  當λ從0.0增加到1.0時,識別假謠言和未被驗證謠言的效能通常會提高。因此,當立場分類的監督訊號變強時,學習到的立場特徵可以產生更準確的預測謠言真實性的線索。

3.3 消融實驗

3.4 Case study

   圖6顯示了本文模型識別的一個假謠言。可以看到,回覆推文的立場呈現出一個典型的時序模式,“支援→查詢→否認”

  本文模型用RNN捕捉了這種立場演化,並正確預測了其真實性。此外,根據推文的視覺化顯示,最大池化操作在會話中捕獲了資訊豐富的推文。因此,本文的框架可以在傳播過程中注意到謠言真實性的顯著性指標,並將它們結合起來,給出正確的預測。

4 總結

  本文提出了一個聯合立場檢測和謠言分類的層次化多工學習模型來編碼帖子傳播過程中的傳播結構特徵和時序動態。

  該模型先利用雙向GRU提取帖子的文字特徵,然後引入GCN來對帖子的文字特徵以及帖子間的關係進行建模,以學習編碼了上下文資訊的帖子立場特徵,最後通過GRU捕捉帖子的立場隨時間的動態變化來檢測謠言。

  儘管本文發表於19年,其效果不弱於SOTA,這表明了基於資料探查和分析來發現問題而做出改進的有效性。當然,值得注意的是,本文提出的模型可能在某些場景下失效,即其假設不再成立的情形,例如是否有某一類謠言,公眾對其立場的變化並不代表什麼,甚至可能誤分類。

相關文章