中文多模态情感分析任务的研究计划
下面各个"流程概括表"是对后面紧接着代码的流程概括,这样做为了节省看代码或者看大段文字说明的时间。这个ipynb文件并不能运行,我在此文件中将所有关键的部分,按合适的顺序做了如下全面的分析。并在最后结合热点提出了模型改进计划。
代码来源/CH-SIMS数据集:https://github.com/thuiar/ch-sims-v2
目录
mi模型训练过程与结果
项目地址:https://github.com/thuiar/ch-sims-v2?tab=readme-ov-file
一、参考论文的主要工作概述
论文《Make Acoustic and Visual Cues Matter: CH-SIMS v2.0 Dataset and AV-Mixup Consistent Module》的主要贡献可以进一步细化为:
-
CH-SIMS v2.0数据集的构建与发布:创建了一个多模态情感分析数据集,这些片段经过精心挑选和注释,以突出非语言的情感线索。数据集不仅包括有监督的数据,还有大量未标记的原始视频片段,以丰富情感分析的非语言上下文。
-
AV-MC框架的提出:提出了一个新颖的多模态学习框架,即Acoustic Visual Mixup Consistent (AV-MC),该框架通过模态混合策略,增强了模型对非语言情感线索的感知能力,尤其是在半监督学习的背景下。
-
多模态情感分析的性能提升:通过在新构建的数据集上进行广泛实验,证明了AV-MC框架在多模态情感分析任务上的有效性,特别是在提高对弱情感的识别能力方面。
二、参考论文的项目代码的全面分析
2.1 参考论文的数据集信息与读取
CH-SIMS v2.0数据集是多模态情感分析领域的一个扩展和增强型数据集,其详细描述如下:
-
数据集规模与构成:数据集包含4402个有监督实例和10161个无监督实例。有监督实例具有细粒度的单模态和多模态情感标签,而无监督实例则提供了丰富的声学和视觉情感上下文,以突出非语言线索。
-
情感标签:数据集中的情感标签采用多级划分,包括强负面(-1/-0.8)、弱负面(-0.6/-0.4/-0.2)、中性(0)、弱正面(0.2/0.4/0.6)和强正面(0.8/1.0)。这种细粒度的标签划分有助于更精确地评估模型对不同情感强度的识别能力。
-
符号含义:在数据集中,不同的符号代表了不同的含义。例如,"M"代表声学模态的标签,"V"代表视觉模态的标签,"A"代表文本模态的标签,而"T"代表多模态的标签。这些符号在数据集中用于区分不同模态的情感标签。
-
标注顺序与策略:为了保证标注的准确性和减少模态间的相互影响,数据集采用了严格的模态隔离策略进行标注。标注顺序遵循文本、声学、视觉和多模态的顺序,确保每种模态的标注都能独立反映其情感内容。
(1)数据信息
(2)数据读取
在load_data.py中,数据读取是通过MMDataset类实现的,这个类继承自PyTorch的Dataset类,用于封装多模态数据集的加载和预处理逻辑。以下是对数据读取部分的流程概括:
# data/load_data.py
import os
import logging
import pickle # 用于序列化和反序列化Python对象
import numpy as np # 用于科学计算的库
import torch # PyTorch库,提供张量计算和深度学习功能
import torch.nn.functional as F # PyTorch中的函数库,包含常用的神经网络操作
from torch.utils.data import Dataset, DataLoader # PyTorch的数据处理模块
__all__ = ['MMDataLoader'] # 指定模块对外暴露的API
logger = logging.getLogger('MSA') # 获取一个名为 'MSA' 的日志记录器
class MMDataset(Dataset):
def __init__(self, args, mode='train'):
self.mode = mode # 数据集的模式,例如训练、验证或测试
self.args = args # 传递的参数
DATA_MAP = {
'sims3l': self.__init_sims, # 数据集名称与初始化函数的映射
}
DATA_MAP[args.datasetName]() # 根据数据集名称调用相应的初始化函数
def __init_sims(self):
with open(self.args.dataPath, 'rb') as f:
data = pickle.load(f) # 从文件中加载数据
# 控制有监督数据的数量
if self.args.supvised_nums != 2722:
if self.mode == 'train':
temp_data = {}
temp_data[self.mode] = {}
for key in data[self.mode].keys():
temp_data[self.mode][key] = data[self.mode][key][-self.args.supvised_nums:]
data[self.mode] = temp_data[self.mode]
if self.mode == 'valid':
temp_data = {}
temp_data[self.mode] = {}
for key in data[self.mode].keys():
p = int(self.args.supvised_nums / 2)
temp_data[self.mode][key] = data[self.mode][key][-p:]
data[self.mode] = temp_data[self.mode]
if self.mode == 'train_mix':
temp_data = {}
temp_data[self.mode] = {}
for key in data[self.mode].keys():
data_sup = data[self.mode][key][2722 - self.args.supvised_nums:2722]
data_unsup = data[self.mode][key][2723:]
temp_data[self.mode][key] = np.concatenate((data_sup, data_unsup), axis=0)
data[self.mode] = temp_data[self.mode]
# 原始视频数据
# if not self.mode == 'train_mix':
# self.rawText = data[self.mode]['raw_text']
if self.args.use_bert:
self.text = data[self.mode]['text_bert'].astype(np.float32) # 使用BERT编码的文本数据
else:
self.text = data[self.mode]['text'].astype(np.float32) # 使用常规编码的文本数据
self.audio = data[self.mode]['audio'].astype(np.float32) # 音频数据
self.vision = data[self.mode]['vision'].astype(np.float32) # 视觉数据
self.ids = data[self.mode]['id'] # 样本ID
self.audio_lengths = data[self.mode]['audio_lengths'] # 音频长度
self.vision_lengths = data[self.mode]['vision_lengths'] # 视觉长度
# 标签
self.labels = {
'M': data[self.mode][self.args.train_mode + '_labels'].astype(np.float32) # 主标签
}
if self.args.datasetName == 'sims3l':
for m in "TAV":
self.labels[m] = data[self.mode][self.args.train_mode + '_labels_' + m] # 各模态标签
logger.info(f"{self.mode} samples: {self.labels['M'].shape}") # 记录样本数量
if self.mode == 'train_mix':
self.mask = data[self.mode]['mask'] # 掩码
# 清理无效数据
self.audio[self.audio == -np.inf] = 0
self.vision[self.vision == -np.inf] = 0
# 均值特征
if self.args.need_normalized:
self.__normalize()
def __normalize(self):
self.vision_temp = []
self.audio_temp = []
for vi in range(len(self.vision_lengths)):
self.vision_temp.append(np.mean(self.vision[vi][:self.vision_lengths[vi]], axis=0)) # 计算视觉特征均值
for ai in range(len(self.audio_lengths)):
self.audio_temp.append(np.mean(self.audio[ai][:self.audio_lengths[ai]], axis=0)) # 计算音频特征均值
self.vision = np.array(self.vision_temp)
self.audio = np.array(self.audio_temp)
def __len__(self):
return len(self.labels['M']) # 返回数据集的样本数量
def get_seq_len(self):
if self.args.use_bert:
return (self.text.shape[2], self.audio.shape[1], self.vision.shape[1]) # 返回各模态的序列长度
else:
return (self.text.shape[1], self.audio.shape[1], self.vision.shape[1])
def __getitem__(self, index):
sample = {
'index': index,
# 'raw_text': self.rawText[index] if self.mode != 'train_mix' else [],
'text': torch.Tensor(self.text[index]), # 文本数据
'audio': torch.Tensor(self.audio[index]), # 音频数据
'vision': torch.Tensor(self.vision[index]), # 视觉数据
'id': self.ids[index], # 样本ID
'labels': {k: torch.Tensor(v[index].reshape(-1)) for k, v in self.labels.items()}, # 标签
'audio_lengths': self.audio_lengths[index], # 音频长度
'vision_lengths': self.vision_lengths[index], # 视觉长度
'mask': self.mask[index] if self.mode == 'train_mix' else [], # 掩码
}
return sample # 返回样本
def MMDataLoader(args):
datasets = {
'train': MMDataset(args, mode='train'),
'train_mix': MMDataset(args, mode='train_mix'),
'valid': MMDataset(args, mode='valid'),
'test': MMDataset(args, mode='test'),
}
if 'seq_lens' in args:
args.seq_lens = datasets['train'].get_seq_len() # 获取序列长度
dataLoader = {
ds: DataLoader(datasets[ds],
batch_size=args.batch_size, # 批次大小
# num_workers=args.num_workers,
num_workers=4, # 使用的工作线程数
shuffle=True) # 是否打乱数据
for ds in datasets.keys()
}
return dataLoader # 返回数据加载器
2.2 参考论文的AV-MC框架介绍
AV-MC框架,即Acoustic Visual Mixup Consistent(听觉视觉混合一致)框架,是论文《Make Acoustic and Visual Cues Matter: CH-SIMS v2.0 Dataset and AV-Mixup Consistent Module》中提出的核心贡献之一。以下是结合论文实际内容对AV-MC框架的详细介绍和改进重述:
(1)特征提取
AV-MC框架首先依赖于有效的特征提取机制。论文中使用了多种模态的特征提取方法:
- 文本特征:使用BertTextEncoder类,该类基于BERT模型架构,通过预训练的权重对输入文本进行编码,输出文本的特征表示。
- 声学特征:通过AVsubNet类实现,该网络利用线性层和双向LSTM层来处理音频序列数据。LSTM层的设计允许网络学习音频信号中的时间依赖关系,从而提取出音频特征。
- 视觉特征:同样使用AVsubNet类,但是这里处理的是视频数据。视频特征提取网络能够从视频帧中提取视觉特征,理解视频中的运动和外观信息。
# models/subNets/BertTextEncoder.py
import os
import sys
import collections
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import BertTokenizer, BertModel
__all__ = ['BertTextEncoder']
# 定义一个名为BertTextEncoder的类,用于文本编码
class BertTextEncoder(nn.Module):
def __init__(self, language='en', use_finetune=False):
"""
language: en / cn
"""
super(BertTextEncoder, self).__init__()
assert language in ['en', 'cn'] # 确保语言参数是'en'或'cn'
tokenizer_class = BertTokenizer
model_class = BertModel
# 根据选择的语言,加载对应的BERT模型和分词器
if language == 'en':
self.tokenizer = tokenizer_class.from_pretrained('bert-base-uncased', do_lower_case=True) # 加载英文的BERT分词器
self.model = model_class.from_pretrained('bert-base-uncased') # 加载英文的BERT模型
elif language == 'cn':
self.tokenizer = tokenizer_class.from_pretrained('bert-base-chinese') # 加载中文的BERT分词器
self.model = model_class.from_pretrained('bert-base-chinese') # 加载中文的BERT模型
self.use_finetune = use_finetune # 是否使用微调的标志
def get_tokenizer(self):
return self.tokenizer # 返回分词器实例
def from_text(self, text):
"""
text: raw data
"""
input_ids = self.get_id(text) # 将文本转化为输入ID
with torch.no_grad():
last_hidden_states = self.model(input_ids)[0] # 获取模型的最后一层隐藏状态
return last_hidden_states.squeeze() # 去掉多余的维度,返回最后的隐藏状态
def forward(self, text):
# 将输入的文本拆分成input_ids, input_mask和segment_ids
input_ids, input_mask, segment_ids = text[:, 0, :].long(), text[:, 1, :].float(), text[:, 2, :].long()
# 根据use_finetune标志决定是否在训练模式下运行
if self.use_finetune:
last_hidden_states = self.model(input_ids=input_ids,attention_mask=input_mask,token_type_ids=segment_ids)[0] # 获取模型的最后一层隐藏状态
else:
with torch.no_grad():
last_hidden_states = self.model(input_ids=input_ids,attention_mask=input_mask,token_type_ids=segment_ids)[0] # 获取模型的最后一层隐藏状态
return last_hidden_states # 返回最后的隐藏状态
if __name__ == "__main__":
bert_normal = BertTextEncoder() # 创建一个BertTextEncoder实例
# models/multiTask/V1_Semi.py中AVsubNet的定义
...
class AVsubNet(nn.Module):
def __init__(self, in_size, hidden_size, dropout, bidirectional):
super(AVsubNet, self).__init__()
# 定义预融合子网络
self.liner = nn.Linear(in_size, hidden_size) # 线性层
self.dropout = nn.Dropout(dropout) # dropout层
self.rnn1 = nn.LSTM(hidden_size, hidden_size, bidirectional=bidirectional) # 双向LSTM层1
self.rnn2 = nn.LSTM(2*hidden_size, hidden_size, bidirectional=bidirectional) # 双向LSTM层2
self.layer_norm = nn.LayerNorm((2*hidden_size,)) # 层归一化
def forward(self, sequence, lengths):
lengths = lengths.squeeze().int().detach().cpu().view(-1) # 处理长度信息
batch_size = sequence.shape[0] # 获取批次大小
sequence = self.dropout(self.liner(sequence)) # 线性变换和dropout
packed_sequence = pack_padded_sequence(sequence, lengths, batch_first=True, enforce_sorted=False) # 打包序列
packed_h1, (final_h1, _) = self.rnn1(packed_sequence) # LSTM层1
padded_h1, _ = pad_packed_sequence(packed_h1) # 解包序列
padded_h1 = padded_h1.permute(1, 0, 2) # 维度变换
normed_h1 = self.layer_norm(padded_h1) # 层归一化
packed_normed_h1 = pack_padded_sequence(normed_h1, lengths, batch_first=True, enforce_sorted=False) # 打包序列
_, (final_h2, _) = self.rnn2(packed_normed_h1) # LSTM层2
utterance = torch.cat((final_h1, final_h2), dim=2).permute(1, 0, 2).contiguous().view(batch_size, -1) # 拼接最终隐藏状态
return utterance # 返回语句表示
...
(2)数据对齐
AlignSubNet
的作用是对多模态输入数据(文本、音频、视频)进行对齐。由于不同模态的输入可能具有不同的序列长度,需要对它们进行对齐,以便后续处理可以在相同的时间步长上进行操作。
-
AlignSubNet
:提供了三种对齐方式,在AMIO.py第29行显示用使用的是avg_pool。 -
AMIO
:通过MODEL_MAP
字典映射模型名称到具体的模型类,可以选择不同的对齐方式。
对齐方式的选择通过AlignSubNet
类的mode
参数实现,允许根据不同的多模态任务和数据特性选择和调整对齐策略。
# models/subNets/AlignNets.py
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
from torch.nn.parameter import Parameter
from torch.nn.init import xavier_uniform, xavier_normal, orthogonal
__all__ = ['AlignSubNet'] # 定义所有导出的模块
# 定义CTCModule类,继承自nn.Module
class CTCModule(nn.Module):
def __init__(self, in_dim, out_seq_len):
'''
该模块用于执行从A(例如音频)到B(例如文本)的对齐。
:param in_dim: 输入模态A的维度
:param out_seq_len: 输出模态B的序列长度
来源: https://github.com/yaohungt/Multimodal-Transformer
'''
super(CTCModule, self).__init__() # 调用父类的构造函数
# 使用LSTM预测从A到B的位置
self.pred_output_position_inclu_blank = nn.LSTM(in_dim, out_seq_len + 1, num_layers=2,
batch_first=True) # 1表示空白
self.out_seq_len = out_seq_len # 输出序列长度
self.softmax = nn.Softmax(dim=2) # 定义softmax激活函数
def forward(self, x):
'''
:param x: 输入,形状为[batch_size x in_seq_len x in_dim]
'''
# 注意,索引0表示空白
pred_output_position_inclu_blank, _ = self.pred_output_position_inclu_blank(x)
prob_pred_output_position_inclu_blank = self.softmax(
pred_output_position_inclu_blank) # 形状为 [batch_size x in_seq_len x out_seq_len+1]
prob_pred_output_position = prob_pred_output_position_inclu_blank[:, :,
1:] # 形状为 [batch_size x in_seq_len x out_seq_len]
prob_pred_output_position = prob_pred_output_position.transpose(1,
2) # 转置为 [batch_size x out_seq_len x in_seq_len]
pseudo_aligned_out = torch.bmm(prob_pred_output_position, x) # 计算对齐后的输出 [batch_size x out_seq_len x in_dim]
# pseudo_aligned_out 被视为与B对齐的A
return pseudo_aligned_out # 返回对齐后的输出
# 定义AlignSubNet类,继承自nn.Module
class AlignSubNet(nn.Module):
def __init__(self, args, mode):
"""
mode: 对齐方式
avg_pool, ctc, conv1d
"""
super(AlignSubNet, self).__init__() # 调用父类的构造函数
assert mode in ['avg_pool', 'ctc', 'conv1d'] # 确保对齐方式在可选范围内
in_dim_t, in_dim_a, in_dim_v = args.feature_dims # 获取特征维度
seq_len_t, seq_len_a, seq_len_v = args.seq_lens # 获取序列长度
self.dst_len = seq_len_t # 目标序列长度
self.mode = mode # 对齐方式
self.ALIGN_WAY = {
'avg_pool': self.__avg_pool,
'ctc': self.__ctc,
'conv1d': self.__conv1d
} # 定义对齐方式的映射
if mode == 'conv1d':
self.conv1d_T = nn.Conv1d(seq_len_t, self.dst_len, kernel_size=1, bias=False) # 定义卷积层用于文本对齐
self.conv1d_A = nn.Conv1d(seq_len_a, self.dst_len, kernel_size=1, bias=False) # 定义卷积层用于音频对齐
self.conv1d_V = nn.Conv1d(seq_len_v, self.dst_len, kernel_size=1, bias=False) # 定义卷积层用于视频对齐
elif mode == 'ctc':
self.ctc_t = CTCModule(in_dim_t, self.dst_len) # 定义CTC模块用于文本对齐
self.ctc_a = CTCModule(in_dim_a, self.dst_len) # 定义CTC模块用于音频对齐
self.ctc_v = CTCModule(in_dim_v, self.dst_len) # 定义CTC模块用于视频对齐
def get_seq_len(self):
return self.dst_len # 返回目标序列长度
def __ctc(self, text_x, audio_x, video_x):
text_x = self.ctc_t(text_x) if text_x.size(1) != self.dst_len else text_x # 如果文本序列长度不等于目标长度,则进行对齐
audio_x = self.ctc_a(audio_x) if audio_x.size(1) != self.dst_len else audio_x # 如果音频序列长度不等于目标长度,则进行对齐
video_x = self.ctc_v(video_x) if video_x.size(1) != self.dst_len else video_x # 如果视频序列长度不等于目标长度,则进行对齐
return text_x, audio_x, video_x # 返回对齐后的序列
def __avg_pool(self, text_x, audio_x, video_x):
def align(x):
raw_seq_len = x.size(1) # 获取原始序列长度
if raw_seq_len == self.dst_len:
return x # 如果原始序列长度等于目标长度,直接返回
if raw_seq_len // self.dst_len == raw_seq_len / self.dst_len:
pad_len = 0
pool_size = raw_seq_len // self.dst_len
else:
pad_len = self.dst_len - raw_seq_len % self.dst_len
pool_size = raw_seq_len // self.dst_len + 1
pad_x = x[:, -1, :].unsqueeze(1).expand([x.size(0), pad_len, x.size(-1)]) # 填充序列
x = torch.cat([x, pad_x], dim=1).view(x.size(0), pool_size, self.dst_len, -1)
x = x.mean(dim=1) # 进行平均池化
return x # 返回对齐后的序列
text_x = align(text_x) # 对齐文本序列
audio_x = align(audio_x) # 对齐音频序列
video_x = align(video_x) # 对齐视频序列
return text_x, audio_x, video_x # 返回对齐后的序列
def __conv1d(self, text_x, audio_x, video_x):
text_x = self.conv1d_T(text_x) if text_x.size(1) != self.dst_len else text_x # 如果文本序列长度不等于目标长度,则进行卷积对齐
audio_x = self.conv1d_A(audio_x) if audio_x.size(1) != self.dst_len else audio_x # 如果音频序列长度不等于目标长度,则进行卷积对齐
video_x = self.conv1d_V(video_x) if video_x.size(1) != self.dst_len else video_x # 如果视频序列长度不等于目标长度,则进行卷积对齐
return text_x, audio_x, video_x # 返回对齐后的序列
def forward(self, text_x, audio_x, video_x):
# 如果输入序列已经对齐,直接返回
if text_x.size(1) == audio_x.size(1) == video_x.size(1):
return text_x, audio_x, video_x
return self.ALIGN_WAY[self.mode](text_x, audio_x, video_x) # 否则根据指定的对齐方式进行对齐并返回对齐后的序列
# models/AMIO.py
"""
AIO -- All Model in One
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
from torch.nn.parameter import Parameter
from torch.nn.init import xavier_uniform, xavier_normal, orthogonal
from models.subNets.AlignNets import AlignSubNet
from models.multiTask import *
__all__ = ['AMIO'] # 定义所有导出的模块
MODEL_MAP = {
# 多任务模型映射
'v1': V1,
'v1_semi': V1_Semi,
}
# 定义AMIO类,继承自nn.Module
class AMIO(nn.Module):
def __init__(self, args):
super(AMIO, self).__init__() # 调用父类的构造函数
self.need_model_aligned = args.need_model_aligned # 获取是否需要对齐网络的参数
# 如果需要对齐网络
if(self.need_model_aligned):
self.alignNet = AlignSubNet(args, 'avg_pool') # 初始化对齐子网络
if 'seq_lens' in args.keys():
args.seq_lens = self.alignNet.get_seq_len() # 获取对齐后的序列长度
lastModel = MODEL_MAP[args.modelName] # 根据模型名称从映射中获取模型类
self.Model = lastModel(args) # 初始化模型
# 定义前向传播方法
def forward(self, text_x, audio_x, video_x):
# 如果需要对齐网络,先对齐输入
if(self.need_model_aligned):
text_x, audio_x, video_x = self.alignNet(text_x, audio_x, video_x)
return self.Model(text_x, audio_x, video_x) # 将对齐后的输入传入模型并返回输出
(3)半监督多模态模型建立/多模态特征融合
AV-MC框架的模型架构主要由以下几个部分组成:
-
子网络(SubNet):用于处理视频和音频数据的独立网络,包括批归一化层、dropout层以及多个线性层和ReLU激活函数。在代码中,
SubNet
类定义了这些组件,并通过forward
方法实现数据的前向传播。 -
音频-视频子网络(AVsubNet):专门用于音频和视频数据的子网络,包括线性层、dropout层、双向LSTM层以及层归一化。
AVsubNet
类在代码中实现了这一架构,并通过处理序列数据和长度信息来进行特征提取。 -
重建网络(Reconstruction Network):用于学习输入特征的表示,并通过dropout层和线性层实现特征的重建。
Reconsitution
类在代码中定义了重建网络的结构。 -
半监督多模态融合模型(V1_Semi):
-
特征提取/文本编码/音视频子网络,已在前面介绍,在此处进行调用
-
特征提取:在forward方法中,文本特征通过BertTextEncoder进行编码,然后通过线性层变换。音频和视频特征则通过AVsubNet进行处理,得到每个模态的特征表示。
-
分类层:对融合后的特征进行分类,使用多个线性层和ReLU激活函数,最终通过sigmoid函数输出分类结果。分类层在代码中通过
post_text_layer_3
、post_audio_layer_3
和post_video_layer_3
实现。 -
融合层(Fusion Layer):将文本、音频和视频的特征进行融合,使用torch.cat拼接文本、音频和视频特征,来实现多模态特征的整合。在代码中,通过
post_fusion_layer_1
、post_fusion_layer_2
和post_fusion_layer_3
实现特征的融合。 -
输出调整:通过参数调整输出范围,以适应特定任务的需求。在代码中,
output_range
和output_shift
参数用于调整最终的输出值。
- 模型输出(forward):forward方法最终返回一个包含不同模态特征、分类结果和融合结果的字典。
# models/multiTask/V1_Semi.py
from __future__ import print_function # 兼容Python 2.x中的print函数
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable # 自动求导
from torch.nn.parameter import Parameter
from torch.nn.init import xavier_uniform, xavier_normal, orthogonal
from models.subNets.BertTextEncoder import BertTextEncoder # BertTextEncoder预训练模型
import numpy as np
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pad_packed_sequence
from models.subNets.transformers_encoder.transformer import TransformerEncoder
__all__ = ['V1_Semi'] # 指定模块对外暴露的API
class SubNet(nn.Module):
'''
TFN中用于视频和音频的子网络,在融合前阶段使用
'''
def __init__(self, in_size, hidden_size, dropout):
'''
Args:
in_size: 输入维度
hidden_size: 隐藏层维度
dropout: dropout概率
Output:
(在forward函数中返回)形状为(batch_size, hidden_size)的张量
'''
super(SubNet, self).__init__()
self.norm = nn.BatchNorm1d(in_size) # 批归一化层
self.drop = nn.Dropout(p=dropout) # dropout层
self.linear_1 = nn.Linear(in_size, hidden_size) # 线性层1
self.linear_2 = nn.Linear(hidden_size, hidden_size) # 线性层2
self.linear_3 = nn.Linear(hidden_size, hidden_size) # 线性层3
def forward(self, x):
'''
Args:
x: 形状为(batch_size, in_size)的张量
'''
normed = self.norm(x) # 批归一化
dropped = self.drop(normed) # dropout
y_1 = F.relu(self.linear_1(dropped)) # 线性变换和ReLU激活
y_2 = F.relu(self.linear_2(y_1)) # 线性变换和ReLU激活
y_3 = F.relu(self.linear_3(y_2)) # 线性变换和ReLU激活
return y_3 # 返回结果
class AVsubNet(nn.Module):
def __init__(self, in_size, hidden_size, dropout, bidirectional):
super(AVsubNet, self).__init__()
# 定义预融合子网络
self.liner = nn.Linear(in_size, hidden_size) # 线性层
self.dropout = nn.Dropout(dropout) # dropout层
self.rnn1 = nn.LSTM(hidden_size, hidden_size, bidirectional=bidirectional) # 双向LSTM层1
self.rnn2 = nn.LSTM(2*hidden_size, hidden_size, bidirectional=bidirectional) # 双向LSTM层2
self.layer_norm = nn.LayerNorm((2*hidden_size,)) # 层归一化
def forward(self, sequence, lengths):
lengths = lengths.squeeze().int().detach().cpu().view(-1) # 处理长度信息
batch_size = sequence.shape[0] # 获取批次大小
sequence = self.dropout(self.liner(sequence)) # 线性变换和dropout
packed_sequence = pack_padded_sequence(sequence, lengths, batch_first=True, enforce_sorted=False) # 打包序列
packed_h1, (final_h1, _) = self.rnn1(packed_sequence) # LSTM层1
padded_h1, _ = pad_packed_sequence(packed_h1) # 解包序列
padded_h1 = padded_h1.permute(1, 0, 2) # 维度变换
normed_h1 = self.layer_norm(padded_h1) # 层归一化
packed_normed_h1 = pack_padded_sequence(normed_h1, lengths, batch_first=True, enforce_sorted=False) # 打包序列
_, (final_h2, _) = self.rnn2(packed_normed_h1) # LSTM层2
utterance = torch.cat((final_h1, final_h2), dim=2).permute(1, 0, 2).contiguous().view(batch_size, -1) # 拼接最终隐藏状态
return utterance # 返回语句表示
class Reconsitution(nn.Module):
"""效仿ARGF模型"""
def __init__(self, args, input_dim, output_dim):
super(Reconsitution, self).__init__()
self.rec_dropout = nn.Dropout(args.rec_dropout) # dropout层
self.post_layer_1_rec = nn.Linear(input_dim, input_dim) # 线性层1
self.post_layer_2_rec = nn.Linear(input_dim, output_dim) # 线性层2
# self.tanh = nn.Tanh()
def forward(self, input_feature):
input_feature = self.rec_dropout(input_feature) # dropout
input_feature1 = F.relu(self.post_layer_1_rec(input_feature)) # 线性变换和ReLU激活
input_feature2 = self.post_layer_2_rec(input_feature1) # 线性变换
return input_feature2 # 返回结果
class V1_Semi(nn.Module):
def __init__(self, args):
super(V1_Semi, self).__init__()
# 按顺序指定音频、视频和文本的维度
self.text_in, self.audio_in, self.video_in = args.feature_dims
self.text_hidden, self.audio_hidden, self.video_hidden = args.hidden_dims
self.audio_prob, self.video_prob, self.text_prob = args.dropouts
self.post_text_prob, self.post_audio_prob, self.post_video_prob, self.post_fusion_prob = args.post_dropouts
self.post_fusion_dim = args.post_fusion_dim
self.post_text_dim = args.post_text_dim
self.post_audio_dim = args.post_audio_dim
self.post_video_dim = args.post_video_dim
self.text_model = BertTextEncoder(language=args.language, use_finetune=args.use_bert_finetune) # 文本编码器
self.tliner = nn.Linear(self.text_in, self.text_hidden) # 文本线性层
self.audio_model = AVsubNet(self.audio_in, self.audio_hidden, self.audio_prob, bidirectional=True) # 音频子网络
self.video_model = AVsubNet(self.video_in, self.video_hidden, self.video_prob, bidirectional=True) # 视频子网络
# self.audio_model = SubNet(self.audio_in, self.audio_hidden, self.audio_prob)
# self.video_model = SubNet(self.video_in, self.video_hidden, self.video_prob)
# 定义文本分类层
self.post_text_dropout = nn.Dropout(p=self.post_text_prob)
self.post_text_layer_1 = nn.Linear(self.text_hidden, self.post_text_dim)
self.post_text_layer_2 = nn.Linear(self.post_text_dim, self.post_text_dim)
self.post_text_layer_3 = nn.Linear(self.post_text_dim, 1)
# 定义音频分类层
self.post_audio_dropout = nn.Dropout(p=self.post_audio_prob)
self.post_audio_layer_1 = nn.Linear(4 * self.audio_hidden, self.post_audio_dim)
self.post_audio_layer_2 = nn.Linear(self.post_audio_dim, self.post_audio_dim)
self.post_audio_layer_3 = nn.Linear(self.post_audio_dim, 1)
# 定义视频分类层
self.post_video_dropout = nn.Dropout(p=self.post_video_prob)
self.post_video_layer_1 = nn.Linear(4 * self.video_hidden, self.post_video_dim)
self.post_video_layer_2 = nn.Linear(self.post_video_dim, self.post_video_dim)
self.post_video_layer_3 = nn.Linear(self.post_video_dim, 1)
# transformer融合层
self.post_fusion_dropout = nn.Dropout(p=self.post_fusion_prob)
self.post_fusion_layer_1 = nn.Linear(self.post_text_dim + self.post_audio_dim + self.post_video_dim, self.post_fusion_dim)
self.post_fusion_layer_2 = nn.Linear(self.post_fusion_dim, self.post_fusion_dim)
self.post_fusion_layer_3 = nn.Linear(self.post_fusion_dim, 1)
# 重建网络
self.t_rec = Reconsitution(args, self.post_text_dim, self.text_in)
self.a_rec = Reconsitution(args, self.post_audio_dim, self.audio_in)
self.v_rec = Reconsitution(args, self.post_video_dim, self.video_in)
self.output_range = Parameter(torch.FloatTensor([6]), requires_grad=False)
self.output_shift = Parameter(torch.FloatTensor([-3]), requires_grad=False)
def extract_features_eazy(self, audio, audio_lengths, vision, vision_lengths):
vision_temp = []
audio_temp = []
for vi in range(len(vision_lengths)):
vision_temp.append(torch.mean(vision[vi][:vision_lengths[vi]], axis=0)) # 计算视频特征均值
for ai in range(len(audio_lengths)):
audio_temp.append(torch.mean(audio[ai][:audio_lengths[ai]], axis=0)) # 计算音频特征均值
vision_utt = torch.stack(vision_temp) # 堆叠视频特征
audio_utt = torch.stack(audio_temp) # 堆叠音频特征
return audio_utt, vision_utt # 返回音频和视频特征
def forward(self, text_x, audio_x, video_x):
text_x , flag = text_x # 获取文本输入和标志
batch_size = text_x.shape[0] # 获取批次大小
# utterance_audio_raw, utterance_video_raw = self.extract_features_eazy(audio_x, a_len, video_x, v_len)
global text_h
global audio_h
global video_h
if flag == 'train':
# data_pre
audio_x, a_len = audio_x # 获取音频输入和长度
video_x, v_len = video_x # 获取视频输入和长度
text_x = self.text_model(text_x)[:,0,:] # 文本编码
text_h = self.tliner(text_x) # 文本线性变换
audio_h = self.audio_model(audio_x, a_len) # 音频子网络
video_h = self.video_model(video_x, v_len) # 视频子网络
# audio_h = self.audio_model(audio_x.squeeze(1))
# video_h = self.video_model(video_x.squeeze(1))
if flag == 'mix_train':
text_h = text_x # 直接使用输入的文本特征
audio_h = audio_x # 直接使用输入的音频特征
video_h = video_x # 直接使用输入的视频特征
# 文本处理
x_t1 = self.post_text_dropout(text_h)
x_t2 = F.relu(self.post_text_layer_1(x_t1), inplace=True)
x_t3 = F.relu(self.post_text_layer_2(x_t2), inplace=True)
output_text = self.post_text_layer_3(x_t3)
# 音频处理
x_a1 = self.post_audio_dropout(audio_h)
x_a2 = F.relu(self.post_audio_layer_1(x_a1), inplace=True)
x_a3 = F.relu(self.post_audio_layer_2(x_a2), inplace=True)
output_audio = self.post_audio_layer_3(x_a3)
# 视频处理
x_v1 = self.post_video_dropout(video_h)
x_v2 = F.relu(self.post_video_layer_1(x_v1), inplace=True)
x_v3 = F.relu(self.post_video_layer_2(x_v2), inplace=True)
output_video = self.post_video_layer_3(x_v3)
# 融合处理
fusion_data = torch.cat([x_t2, x_a2, x_v2], dim=1) # 拼接文本、音频和视频特征
# fusion_data = fusion_f.unsqueeze(0)
# fusion_data = self.fusion_trans(fusion_data).squeeze()
fusion_data = self.post_fusion_dropout(fusion_data) # dropout
fusion_data = self.post_fusion_layer_1(fusion_data) # 线性变换
fusion_data = self.post_fusion_layer_2(fusion_data) # 线性变换
fusion_data = self.post_fusion_layer_3(fusion_data) # 线性变换
output_fusion = torch.sigmoid(fusion_data) # sigmoid激活
output_fusion = output_fusion * self.output_range + self.output_shift # 调整输出范围
x_t2_rec = self.t_rec(x_t2) # 文本特征重建
x_a2_rec = self.a_rec(x_a2) # 音频特征重建
x_v2_rec = self.v_rec(x_v2) # 视频特征重建
res = {
'Feature_t': text_h, # 文本特征
'Feature_a': audio_h, # 音频特征
'Feature_v': video_h, # 视频特征
# 'Feature_f': [fusion_tensor3, x_m_rec],
'M': output_fusion, # 融合输出
'T': output_text, # 文本输出
'A': output_audio, # 音频输出
'V': output_video # 视频输出
}
return res # 返回结果
(4)V1_Semi模型训练
V1_Semi
模型训练是一个结合了有监督和无监督学习策略的过程,以下是模型训练的各个阶段和关键步骤的流程概览。
- 在config_regression.py中,ConfigRegression类定义了用于V1_Semi模型的配置参数,这里半监督训练使用的sims3l的参数设置。以下是其内容概述:
# trains/multiTask/V1_Semi.py
import os
import time
import logging
import argparse
import numpy as np
from glob import glob
from tqdm import tqdm
import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
from utils.functions import dict_to_str
from utils.metricsTop import MetricsTop
from torch.autograd import Variable
logger = logging.getLogger('MSA')
# 定义一个V1_Semi类,用于多模态情感分析
class V1_Semi():
def __init__(self, args):
assert args.datasetName == 'sims3l' # 断言数据集名称为'sims3l'
self.args = args
self.args.tasks = "MTAV" # 定义任务类型
# 根据训练模式选择损失函数
self.criterion = nn.L1Loss() if args.train_mode == 'regression' else nn.CrossEntropyLoss()
self.recloss = nn.MSELoss() # 定义重构损失函数
self.metrics = MetricsTop(args.train_mode).getMetics(args.datasetName) # 获取评估指标
# 定义训练方法
def do_train(self, model, dataloader):
# 定义不同参数组
bert_no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
bert_params = list(model.Model.text_model.named_parameters())
audio_params = list(model.Model.audio_model.named_parameters())
video_params = list(model.Model.video_model.named_parameters())
bert_params_decay = [p for n, p in bert_params if not any(nd in n for nd in bert_no_decay)]
bert_params_no_decay = [p for n, p in bert_params if any(nd in n for nd in bert_no_decay)]
audio_params = [p for n, p in audio_params]
video_params = [p for n, p in video_params]
model_params_other = [p for n, p in list(model.Model.named_parameters()) if 'text_model' not in n and \
'audio_model' not in n and 'video_model' not in n]
optimizer_grouped_parameters = [
{'params': bert_params_decay, 'weight_decay': self.args.weight_decay_bert,
'lr': self.args.learning_rate_bert},
{'params': bert_params_no_decay, 'weight_decay': 0.0, 'lr': self.args.learning_rate_bert},
{'params': audio_params, 'weight_decay': self.args.weight_decay_audio, 'lr': self.args.learning_rate_audio},
{'params': video_params, 'weight_decay': self.args.weight_decay_video, 'lr': self.args.learning_rate_video},
{'params': model_params_other, 'weight_decay': self.args.weight_decay_other,
'lr': self.args.learning_rate_other}
]
optimizer = optim.Adam(optimizer_grouped_parameters) # 定义优化器
# 初始化训练结果
epochs, best_epoch = 0, 0
min_or_max = 'min' if self.args.KeyEval in ['Loss'] else 'max'
best_valid = 1e8 if min_or_max == 'min' else 0
# 循环直到早停
while True:
epochs += 1
# 训练
y_pred = {'M': [], 'T': [], 'A': [], 'V': []}
y_true = {'M': [], 'T': [], 'A': [], 'V': []}
losses = []
model.train()
train_loss = 0.0
with tqdm(dataloader['train_mix']) as td:
for batch_data in td:
vision = batch_data['vision'].to(self.args.device)
vision_lengths = batch_data['vision_lengths'].to(self.args.device)
audio = batch_data['audio'].to(self.args.device)
audio_lengths = batch_data['audio_lengths'].to(self.args.device)
text = batch_data['text'].to(self.args.device)
mask = batch_data['mask']
labels = batch_data['labels']
# 清除梯度
optimizer.zero_grad()
flag = 'train'
# 前向传播
outputs = model((text, flag), (audio, audio_lengths), (vision, vision_lengths))
# 计算损失
loss = 0
# 1. 监督损失
labels_true = {}
outputs_true = {}
for k in labels.keys():
labels[k] = labels[k].to(self.args.device).view(-1, 1)
mask_index = torch.where(mask == 1)
labels_true[k] = labels[k][mask_index]
outputs_true[k] = outputs[k][mask_index]
for m in self.args.tasks:
if mask.sum() > 0:
loss += eval('self.args.' + m) * self.criterion(outputs_true[m], labels_true[m])
# 2. 非监督损失
flag = 'mix_train'
# text_utt = outputs['Feature_t']
# pret = outputs['T']
audio_utt = outputs['Feature_a']
prea = outputs['A']
video_utt = outputs['Feature_v']
prev = outputs['V']
loss_V_mix = 0.0
video_utt, video_utt_chaotic, video_utt_mix, y, y2, ymix, lam = mixup_data(video_utt, prev)
x_v1 = model.Model.post_video_dropout(video_utt_mix)
x_v2 = F.relu(model.Model.post_video_layer_1(x_v1), inplace=True)
x_v3 = F.relu(model.Model.post_video_layer_2(x_v2), inplace=True)
output_video = model.Model.post_video_layer_3(x_v3)
loss_V_mix += self.args.V * self.criterion(output_video, ymix)
loss_A_mix = 0.0
audio_utt, audio_utt_chaotic, audio_utt_mix, y, y2, ymix, lam = mixup_data(audio_utt, prea)
x_a1 = model.Model.post_audio_dropout(audio_utt_mix)
x_a2 = F.relu(model.Model.post_audio_layer_1(x_a1), inplace=True)
x_a3 = F.relu(model.Model.post_audio_layer_2(x_a2), inplace=True)
output_audio = model.Model.post_audio_layer_3(x_a3)
loss_A_mix += self.args.A * self.criterion(output_audio, ymix)
# 反向传播
loss += loss_A_mix
loss += loss_V_mix
if mask.sum() > 0:
loss.backward()
train_loss += loss.item()
# 更新参数
optimizer.step()
# 存储结果
for m in self.args.tasks:
y_pred[m].append(outputs_true[m].cpu())
y_true[m].append(labels_true['M'].cpu())
with tqdm(dataloader['train']) as td:
for batch_data in td:
vision = batch_data['vision'].to(self.args.device)
vision_lengths = batch_data['vision_lengths'].to(self.args.device)
audio = batch_data['audio'].to(self.args.device)
audio_lengths = batch_data['audio_lengths'].to(self.args.device)
text = batch_data['text'].to(self.args.device)
labels = batch_data['labels']
for k in labels.keys():
if self.args.train_mode == 'classification':
labels[k] = labels[k].to(self.args.device).view(-1).long()
else:
labels[k] = labels[k].to(self.args.device).view(-1, 1)
# 清除梯度
optimizer.zero_grad()
flag = 'train'
# 前向传播
outputs = model((text, flag), (audio, audio_lengths), (vision, vision_lengths))
# 计算损失
loss = 0.0
for m in self.args.tasks:
loss += eval('self.args.' + m) * self.criterion(outputs[m], labels[m])
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 存储结果
train_loss += loss.item()
for m in self.args.tasks:
y_pred[m].append(outputs[m].cpu())
y_true[m].append(labels['M'].cpu())
train_loss = train_loss / len(dataloader['train_mix'])
logger.info("TRAIN-(%s) (%d/%d/%d)>> loss: %.4f " % (self.args.modelName, \
epochs - best_epoch, epochs, self.args.cur_time,
train_loss))
for m in self.args.tasks:
pred, true = torch.cat(y_pred[m]), torch.cat(y_true[m])
train_results = self.metrics(pred, true)
logger.info('%s: >> ' % (m) + dict_to_str(train_results))
# 验证
val_results = self.do_test(model, dataloader['valid'], mode="VAL")
cur_valid = val_results[self.args.KeyEval]
# 保存最佳模型
isBetter = cur_valid <= (best_valid - 1e-6) if min_or_max == 'min' else cur_valid >= (best_valid + 1e-6)
# 保存最佳模型
if isBetter:
best_valid, best_epoch = cur_valid, epochs
# 保存模型
torch.save(model.cpu().state_dict(), self.args.model_save_path)
model.to(self.args.device)
# 早停
if epochs - best_epoch >= self.args.early_stop:
return
# 定义测试方法
def do_test(self, model, dataloader, mode="VAL"):
model.eval()
y_pred = {'M': [], 'T': [], 'A': [], 'V': []}
y_true = {'M': [], 'T': [], 'A': [], 'V': []}
eval_loss = 0.0
with torch.no_grad():
with tqdm(dataloader) as td:
for batch_data in td:
vision = batch_data['vision'].to(self.args.device)
vision_lengths = batch_data['vision_lengths'].to(self.args.device)
audio = batch_data['audio'].to(self.args.device)
audio_lengths = batch_data['audio_lengths'].to(self.args.device)
text = batch_data['text'].to(self.args.device)
labels = batch_data['labels']
for k in labels.keys():
if self.args.train_mode == 'classification':
labels[k] = labels[k].to(self.args.device).view(-1).long()
else:
labels[k] = labels[k].to(self.args.device).view(-1, 1)
flag = 'train'
outputs = model((text, flag), (audio, audio_lengths), (vision, vision_lengths))
loss = 0.0
for m in self.args.tasks:
loss += eval('self.args.' + m) * self.criterion(outputs[m], labels[m])
eval_loss += loss.item()
for m in self.args.tasks:
y_pred[m].append(outputs[m].cpu())
y_true[m].append(labels['M'].cpu())
eval_loss = round(eval_loss / len(dataloader), 4)
logger.info(mode + "-(%s)" % self.args.modelName + " >> loss: %.4f " % eval_loss)
eval_results = {}
for m in self.args.tasks:
pred, true = torch.cat(y_pred[m]), torch.cat(y_true[m])
results = self.metrics(pred, true)
logger.info('%s: >> ' % (m) + dict_to_str(results))
eval_results[m] = results
eval_results = eval_results[self.args.tasks[0]]
eval_results['Loss'] = eval_loss
return eval_results
# 混合数据的方法
def mixup_data(x, y, alpha=1.0):
'''返回混合输入、目标对和lambda'''
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1
batch_size = x.size()[0]
index = torch.randperm(batch_size)
x2 = x[index, :]
y2 = y[index]
xmix = lam * x + (1 - lam) * x2
ymix = lam * y + (1 - lam) * y2
y, y2 = y, y[index]
return x, x2, xmix, y, y2, ymix, lam
def mixup_data_no_grad(x, y, y_m, alpha=1.0, use_cuda=True):
'''返回混合输入、目标对和lambda'''
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1
batch_size = x.size()[0]
index = torch.randperm(batch_size)
mixed_x = lam * x + (1 - lam) * x[index, :]
y_a, y_b = y, y[index]
y_m_a, y_m_b = y_m, y_m[index]
return mixed_x, y_a, y_b, y_m_a, y_m_b, lam
def mixup_criterion(criterion, pred, y_a, y_b, lam):
return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)
# config/config_regression.py
import os
import argparse
from utils.functions import Storage # 从utils.functions模块导入Storage类
# 定义一个名为ConfigRegression的配置类
class ConfigRegression():
def __init__(self, args):
# 定义模型超参数的映射关系
HYPER_MODEL_MAP = {
# 多任务模型
'v1': self.__V1,
'v1_semi': self.__V1_Semi,
}
# 定义数据集超参数的映射关系
HYPER_DATASET_MAP = self.__datasetCommonParams()
# 标准化模型名称和数据集名称
model_name = str.lower(args.modelName)
dataset_name = str.lower(args.datasetName)
# 加载参数
commonArgs = HYPER_MODEL_MAP[model_name]()['commonParas']
dataArgs = HYPER_DATASET_MAP[dataset_name]
# 根据是否需要对齐数据选择对应的数据集参数
dataArgs = dataArgs['aligned'] if (commonArgs['need_data_aligned'] and 'aligned' in dataArgs) else dataArgs['unaligned']
# 整合所有参数
self.args = Storage(dict(vars(args),
**dataArgs,
**commonArgs,
**HYPER_MODEL_MAP[model_name]()['datasetParas'][dataset_name],
))
# 定义数据集的公共参数
def __datasetCommonParams(self):
root_dataset_dir = "data" # 数据集的根目录
tmp = {
'sims3l':{
'aligned': {
'dataPath': os.path.join(root_dataset_dir,'CHSims_aligned2.pkl'), # 对齐数据集的路径
# 数据集形状参数 (batch_size, seq_lens, feature_dim)
'seq_lens': (50, 925, 232), # 文本、音频和视频的序列长度
'feature_dims': (768, 25, 177), # 文本、音频和视频的特征维度
'train_samples': 2722, # 训练样本数量
'train_mix_samples': 12000, # 混合训练样本数量
'num_classes': 3, # 类别数量
'language': 'cn', # 数据集语言
'KeyEval': 'Loss', # 评价指标
},
'unaligned': {
'dataPath': os.path.join(root_dataset_dir, 'train_mix.pkl'), # 未对齐数据集的路径
'seq_lens': (50, 925, 232), # 文本、音频和视频的序列长度
'feature_dims': (768, 25, 177), # 文本、音频和视频的特征维度
'train_samples': 2722, # 训练样本数量
'train_mix_samples': 12000, # 混合训练样本数量
'num_classes': 3, # 类别数量
'language': 'cn', # 数据集语言
'KeyEval': 'Loss', # 评价指标
}
},
}
return tmp
# 定义V1模型的超参数
def __V1(self):
tmp = {
'commonParas':{
'need_data_aligned': False, # 是否需要数据对齐
'need_model_aligned': False, # 是否需要模型对齐
'need_normalized': True, # 是否需要归一化
'use_bert':True, # 是否使用BERT
'use_bert_finetune': False, # 是否对BERT进行微调
'early_stop': 8 # 早停步数
},
# 数据集参数
'datasetParas':{
'sims3l':{
'hidden_dims': (64, 16, 16), # 隐藏层维度
'post_text_dim': 32, # 文本后处理维度
'post_audio_dim': 32, # 音频后处理维度
'post_video_dim': 64, # 视频后处理维度
'post_fusion_out': 16, # 后融合输出维度
'dropouts': (0.1,0.1,0.1), # Dropout率
'post_dropouts': (0.3,0.3,0.3,0.3), # 后处理Dropout率
'batch_size': 32, # 批处理大小
'M': 1.0, # 模态权重
'T': 0.2, # 文本权重
'A': 0.8, # 音频权重
'V': 0.4, # 视频权重
'learning_rate_bert': 5e-4, # BERT学习率
'learning_rate_audio': 5e-4, # 音频学习率
'learning_rate_video': 1e-3, # 视频学习率
'learning_rate_other': 5e-4, # 其他部分学习率
'weight_decay_bert': 1e-4, # BERT权重衰减
'weight_decay_audio': 0, # 音频权重衰减
'weight_decay_video': 0, # 视频权重衰减
'weight_decay_other': 5e-4, # 其他部分权重衰减
}
},
}
return tmp
# 定义V1_Semi模型的超参数
def __V1_Semi(self):
tmp = {
'commonParas':{
'need_data_aligned': False, # 是否需要数据对齐
'need_model_aligned': False, # 是否需要模型对齐
'need_sampling': False, # 是否需要采样
'need_sampling_fix': False, # 是否需要固定采样
'need_normalized': False, # 是否需要归一化
'use_bert':True, # 是否使用BERT
'use_bert_finetune': False, # 是否对BERT进行微调
'early_stop': 8 # 早停步数
},
# 数据集参数
'datasetParas':{
'sims3l':{
'batch_size': 64, # 批处理大小
'learning_rate_bert': 5e-4, # BERT学习率
'learning_rate_audio': 1e-3, # 音频学习率
'learning_rate_video': 1e-3, # 视频学习率
'learning_rate_other': 5e-4, # 其他部分学习率
'hidden_dims': (64, 32, 32), # 隐藏层维度
'post_fusion_dim': 32, # 后融合维度
'post_text_dim': 16, # 文本后处理维度
'post_audio_dim': 8, # 音频后处理维度
'post_video_dim': 64, # 视频后处理维度
'dropouts': (0.1,0.1,0.1), # Dropout率
'post_dropouts': (0.4,0.4,0.4,0.4), # 后处理Dropout率
'M': 0.8, # 模态权重
'T': 0.6, # 文本权重
'A': 0.2, # 音频权重
'V': 0.8, # 视频权重
'weight_decay_bert': 1e-5, # BERT权重衰减
'weight_decay_audio': 0, # 音频权重衰减
'weight_decay_video': 1e-5, # 视频权重衰减
'weight_decay_other': 1e-5, # 其他部分权重衰减
}
},
}
return tmp
# 获取配置参数
def get_config(self):
return self.args # 返回整合后的配置参数
(5)V1_Semi模型训练过程与结果
- 训练之前刚花了三个晚上给数据集处理好并把代码调通,然后在终于6.20号晚上终于可以正常训练了,结果一直训练到第二天21号上午12点,共计14h,期间恍然发现自己没有修改过训练配置,以至于24G显存只利用了不到5G。
- 训练结果看起来正常,但与论文作者在论文中写的实验结果,有着1%左右的差距。推测有三种可能性:
- ①我在按照作者的建议对有/无监督数据处理进行连接的时候,采用了和作者不同的方式;
- ②论文作者本身的随机种子设置的就无法保证实验结果的完全可重复性;
- ③下载的源代码中的参数并未指定的和作者在做实验时的一致。
作者的实验结果,主要看标黑的“AV-MC(Semi)”行:
我的实验结果缩略图:
我的实验结果具体评估数值:
很奇怪,跟作者的有些差距,明明没怎么改过源代码。。。?
项目结构
三、模型改进计划!
目前来看,比较容易下手且可预测精度会提高的改进有:特征提取增强、分类层改进。前者比较现有的可供替换模块资源和教程较多,本人对模块的替换和插入较为熟悉;后者因为原模型特征处理和分类都用了线性层,而最近新出的KAN可以很好的代替原有的线性连接,可预见的会有精度的提升。缺陷是会增大参数量,模型测试调优训练的时间会不可避免的变长。
比较亮眼更有研究意义但是上手难度更大的改进点有:数据对齐改进、融合策略优化。这两个领域具有较高研究价值,但相应的难度较大,需要对现有模型架构有深入的理解,且涉及到复杂的数学推导和编程技巧,同时各平台讲解等资源比较少。可以使用的时间对齐技术比如有动态时间弯曲(DTW)或者利用注意力机制进行软对齐,可以使模型更加灵活地处理不同长度的输入序列。可以改进的融合策略比如有Cross-Modal Interaction或基于Transformer的特征融合技术,都可以增强模态间的交互,还能利用自注意力机制捕捉全局依赖关系,从而提升模型对复杂场景的理解能力,对于本中文多模态情感分析任务是十分适合的。
至于其他的方面的改进点,只能说综合研究价值和可预见的结果来说,优先级不高。所以我的策略采取迭代的方法进行探索,从容易上手的模型改进开始,边跑实验试试精度提升如何,并在此过程中加深理解,再逐步挑战更具研究价值的改进点。