论文阅读及代码学习-Directed Acyclic Graph Network for Conversational Emotion Recognition
目录
一、前言
二、数据集介绍
三、模型介绍
3.1、问题定义
3.2、从对话构建有向无环图
四、实验结果
主要针对模型和数据集方面进行了介绍,略过了实验结果部分。
一、前言
文章通过将传统的基于图形的神经网络模型与基于递归的神经模型进行结合,提出了一种通过有向无环图将对话进行编码的新思路,这种新型的编码方式能够更好地结合long-term对话信息以及相邻上下文的信息,并在四个baseline上取得了很好的效果。
基于图的神经网络模型只会从一个固定长度的window内获取对话信息,这种模型的缺点就是会损失掉一些稍远距离的对话和序列信息;基于递归的神经模型通过用编码的方式来获取一定时间步内的对话和序列信息,然而,这种模型往往只使用最近的语句中相对有限的信息来更新语句的状态,这使得它们很难获得令人满意的性能。
使用有向无环图结构的好处是建模过程可以根据真实对话发生的顺序来进行,即一个句子的节点只能获取到过去发生的对话信息而无法获取到还未发生的对话信息,同时也无法包含指向自己的一条边。
二、数据集介绍
文章的实验部分主要使用了四个数据集,IEMOCAP、MELD、DailyDialog以及DailyDialog,文章只使用了多模态数据集中的文字数据来进行实验,这几个数据集都经过了预训练语言模型roberta进行特征提取,以IEMOCAP做例子,在pycharm的debug中看一下其中数据是如何表示的:
IEMOCAP定义了六种情绪标签与302条对话人物信息:

下图中的一个d是一轮对话的多个句子集合,每个句子都包含五条信息,分别是句子内容、说话人信息、句子情绪标签、一个文章没有用到的feature信息以及cls,文章使用cls的pool embedding来当作句子的特征表示。

三、模型介绍
3.1、问题定义
在DAG-ERC模型中,作者将一个对话定义为一个句子的序列,表示为
,每一个句子由许多个单词表示,作者定义了
用于表示每一个句子
的情绪预测标签,说话者的身份使用
来进行表示,文章的主要问题可以定义为通过一系列的对话上下文以及说话者的身份来确定当前对话的情感标签分类。
3.2、从对话构建有向无环图
具体的构图伪代码如下图所示,文章提出的有向无环图的节点由对话中的一条句子构成,即下图中的
;图的边代表从节点
到节点
的信息propagate,即下图中的
;
是图中边上包含的信息,0表示两个连接的句子是由两个人说的,而1则表示两个句子是由同一个人说出的,除此之外还有一个超参数
,该参数用于设置最长的来自同一个说话人的信息步长。

通过伪代码可知针对节点
,只可能有从
之前的节点指来的连边,这保证了图的有向无环特性。同时每识别到
,即收到来自同一人说的句子信息时记录次数加一,到达最大信息步长时跳出循环,文章认为
已经包含了remote信息,因而不需要继续考虑该节点之前的连边信息。图在代码中的表示主要是依靠一个邻接矩阵与一个mask矩阵(用于表示上述连边的R信息)。
def get_adj_v1(self, speakers, max_dialog_len):
'''
get adj matrix
:param speakers: (B, N) 每一个句子都有一个speaker信息,所以是N
:param max_dialog_len:
:return:
adj: (B, N, N) adj[:,i,:] means the direct predecessors of node i
'''
adj = []
for speaker in speakers:
a = torch.zeros(max_dialog_len, max_dialog_len)
for i, s in enumerate(speaker):
cnt = 0
for j in range(i - 1, -1, -1): # 保证图有向无环
a[i, j] = 1
if speaker[j] == s:
cnt += 1
if cnt == self.args.windowp: # windowp代表伪代码中的w
break
adj.append(a)
return torch.stack(adj)
def get_s_mask(self, speakers, max_dialog_len):
'''
:param speakers:
:param max_dialog_len:
:return:
s_mask: (B, N, N) s_mask[:,i,:] means the speaker informations for predecessors of node i, where 1 denotes the same speaker, 0 denotes the different speaker
s_mask_onehot (B, N, N, 2) onehot emcoding of s_mask
'''
s_mask = []
s_mask_onehot = [] #s_mask的onehot表示
for speaker in speakers: #每一个speaker是一轮对话的说话者身份信息序列
s = torch.zeros(max_dialog_len, max_dialog_len, dtype = torch.long)
s_onehot = torch.zeros(max_dialog_len, max_dialog_len, 2)
for i in range(len(speaker)):
for j in range(len(speaker)):
if speaker[i] == speaker[j]:
s[i,j] = 1
s_onehot[i,j,1] = 1
else:
s_onehot[i,j,0] = 1
s_mask.append(s)
s_mask_onehot.append(s_onehot)
return torch.stack(s_mask), torch.stack(s_mask_onehot)
3.3、DAG-ERC网络构建
文章的总体网络结构如下图所示,DAGERC层的输入是经过特征提取后的句子节点和记录图信息的邻接矩阵与s_mask矩阵,在经过了L层的DAGERC网络后,最后的输出经过一个FFN输出最后的情绪标签分类,下面重点介绍DAG-ERC网络结构。

根据论文以及代码可知每一层DAGERC网络由一个图注意力网络(GAT)以及两层gru组成,图注意力网络的功能主要是进行节点信息的aggregation,如下图所示GAT计算的重点是共享参数W和权重系数
。

GAT的通常计算过程是首先将输入节点与其邻居上下文节点进行拼接并计算相似度系数,取得相似度系数后进行一个softmax取其归一化后的特征系数,之后再利用这个特征系数去将上下文信息加权求和后得到最终的聚合信息,这个聚合信息用于之后控制节点信息的传播。
class GAT_dialoggcn_v1(nn.Module):
'''
use linear to avoid OOM
H_i = alpha_ij(W_rH_j)
alpha_ij = attention(H_i, H_j)
'''
def __init__(self, hidden_size):
super().__init__()
self.hidden_size = hidden_size
self.linear = nn.Linear(hidden_size * 2, 1)
self.Wr0 = nn.Linear(hidden_size, hidden_size, bias = False)
self.Wr1 = nn.Linear(hidden_size, hidden_size, bias = False)
def forward(self, Q, K, V, adj, s_mask):
'''
imformation gatherer with linear attention
:param Q: (B, D) # query utterance
:param K: (B, N, D) # context
:param V: (B, N, D) # context
:param adj: (B, N) # the adj matrix of the i th node
:param s_mask: (B, N) #
:return:
'''
B = K.size()[0]
N = K.size()[1]
Q = Q.unsqueeze(1).expand(-1, N, -1) # (B, N, D);
X = torch.cat((Q,K), dim = 2) # (B, N, 2D) 将query utterance与上下文信息拼接
alpha = self.linear(X).permute(0,2,1) #(B, 1, N)
adj = adj.unsqueeze(1) # (B, 1, N)
alpha = mask_logic(alpha, adj) # (B, 1, N) # 将图的邻接矩阵信息加入到alpha
attn_weight = F.softmax(alpha, dim = 2) # (B, 1, N) # 计算出注意力系数
V0 = self.Wr0(V) # (B, N, D)
V1 = self.Wr1(V) # (B, N, D)
s_mask = s_mask.unsqueeze(2).float() # (B, N, 1)
V = V0 * s_mask + V1 * (1 - s_mask) # 此处的计算结果V=V0
attn_sum = torch.bmm(attn_weight, V).squeeze(1) # (B, D) 得到聚合后的信息即论文中的M
return attn_weight, attn_sum
def mask_logic(alpha, adj):
'''
performing mask logic with adj
:param alpha:
:param adj:
:return:
'''
# 将所有没有连边的节点之间的注意力系数置为负无穷
return alpha - (1 - adj) * 1e30
在计算得到M之后,文章通过两个输入和隐状态相反的gru来控制信息的传递,下面代码中的C代表平常的通过隐状态M来控制每个节点H[i]的信息更新,除此之外文章认为只有C并不能很好地利用上下文信息,所以添加了一个相反的gru_p,让两个结果相加来当作计算的结果H并拼接到最终结果之中,这个结果在最后的FFN之后通过argmax来获取最后的分类标签。
C = self.grus_c[l](H[l][:,i,:], M).unsqueeze(1)
P = self.grus_p[l](M, H[l][:,i,:]).unsqueeze(1)
H_temp = C+P
H1 = torch.cat((H1 , H_temp), dim = 1)
四、实验结果
略
