赞
踩
CCF大数据与计算智能大赛(CCF Big Data & Computing Intelligence Contest,简称CCF BDCI)由中国计算机学会于2013年创办。大赛由国家自然科学基金委员会指导,是大数据与人工智能领域的算法、应用和系统大型挑战赛事。大赛面向重点行业和应用领域征集需求,以前沿技术与行业应用问题为导向,以促进行业发展及产业升级为目标,以众智、众包的方式,汇聚海内外产学研用多方智慧,为社会发现和培养了大量高质量数据人才。 大赛迄今已成功举办八届,累计吸引全球1500余所高校、1800家企事业单位及80余所科研机构的12万余人参与,已成为中国大数据与人工智能领域最具影响力的活动之一,是中国大数据综合赛事第一品牌。 2021年第九届大赛以“数引创新,竞促汇智”为主题,立足余杭、面向全球,于9月至12月举办。大赛将致力于解决来自政府、企业真实场景中的痛点、难点问题,邀请全球优秀团队参与数据资源开发利用,广泛征集信息技术应用解决方案。
比赛的地址为https://www.datafountain.cn/competitions/518
本赛题提供一部分电影剧本作为训练集,训练集数据已由人工进行标注,参赛队伍需要对剧本场景中每句对白和动作描述中涉及到的每个角色的情感从多个维度进行分析和识别。该任务的主要困难和挑战包括:1)剧本的行文风格和通常的新闻类语料差别较大,更加口语化;2)剧本中角色情感不仅仅取决于当前的文本,对前文语义可能有深度依赖。
from tqdm import tqdm import pandas as pd import os from functools import partial import numpy as np import time # 导入paddle库 import paddle import paddle.nn.functional as F import paddle.nn as nn from paddle.io import DataLoader from paddle.dataset.common import md5file # 导入paddlenlp的库 import paddlenlp as ppnlp from paddlenlp.transformers import LinearDecayWithWarmup from paddlenlp.metrics import ChunkEvaluator from paddlenlp.transformers import BertTokenizer,BertPretrainedModel from paddlenlp.data import Stack, Tuple, Pad, Dict from paddlenlp.datasets import DatasetBuilder,get_path_from_url # 导入所需要的py包 from paddle.io import Dataset
!unzip -o data/data110628/剧本角色情感识别.zip -d data
# 将文件“data/data110628/剧本角色情感识别.zip”解压缩到-d data文件地址里,如有相同的文件存在,要求unzip命令覆盖原先的文件
with open('data/train_dataset_v2.tsv', 'r', encoding='utf-8') as handler: # “.tsv”文件可以当作利用\t分割的txt文本 lines = handler.read().split('\n')[1:-1] # 读取舍掉头和尾两行的42790数据,按照‘\n’进行分割,存到lines,维度(42790,) data = list() for line in tqdm(lines): # tqdm 是一个快速,可扩展的 Python 进度条,可以在 Python 长循环中添加一个进度提示信息,用户只需要封装任意的迭代器 sp = line.split('\t') # 将42790条文本数据,按'\t'进行分割,返回数组sp if len(sp) != 4: # 判断是否符合数据规范 print("ERROR:", sp) continue data.append(sp) # 将符合数据规范的数据存入data train = pd.DataFrame(data) # 将data,维度(42790, 4),从list转换成DataFrame类型 train.columns = ['id', 'content', 'character', 'emotions']# 替换、更新数据列名 test = pd.read_csv('data/test_dataset.tsv', sep='\t') # 读取测试集 submit = pd.read_csv('data/submit_example.tsv', sep='\t') # 读取提交样例 train = train[train['emotions'] != ''] # 将没有情感倾向标签的数据筛掉,维度变成(36782, 4)
训练集数据也可以直接用pandas读取,但训练集数据是否有残缺不清楚,所以采用python文件逐条读取判断,确保训练集数据的准确性
train['text'] = train[ 'content'].astype(str) +'角色: ' + train['character'].astype(str) test['text'] = test['content'].astype(str) + ' 角色: ' + test['character'].astype(str) # 将训练集和测试集中的'content'和'character'两列特征合并成为新特征'text' train['emotions'] = train['emotions'].apply(lambda x: [int(_i) for _i in x.split(',')]) # 将训练集所有数据的'emotions'这一列,用匿名函数按照‘,’split分割后转为int,再返回数组 train[['love', 'joy', 'fright', 'anger', 'fear', 'sorrow']] = train['emotions'].values.tolist() # 将Series类型转成list类型,再为train新添加的'love', 'joy', 'fright', 'anger', 'fear', 'sorrow'六列赋值 test[['love', 'joy', 'fright', 'anger', 'fear', 'sorrow']] =[0,0,0,0,0,0] # python向量的广播规则 train.to_csv('data/train.csv',columns=['id', 'content', 'character','text','love', 'joy', 'fright', 'anger', 'fear', 'sorrow'], sep='\t', index=False) # '.to_csv',当sep = '\t',不是默认的‘,’或者不写时,都会将所有列合并到一列中存储为csv文件,在读取时,需要加入sep = ‘\t’才可 test.to_csv('data/test.csv',columns=['id', 'content', 'character','text','love', 'joy', 'fright', 'anger', 'fear', 'sorrow'], sep='\t', index=False) #同理
注意数据的读取和sep的应用
target_cols=['love', 'joy', 'fright', 'anger', 'fear', 'sorrow']
# roberta
PRE_TRAINED_MODEL_NAME='roberta-wwm-ext'
# 加载预训练模型的分词器
tokenizer = ppnlp.transformers.RobertaTokenizer.from_pretrained(PRE_TRAINED_MODEL_NAME)
# 加载预训练模型
base_model =
ppnlp.transformers.RobertaModel.from_pretrained(PRE_TRAINED_MODEL_NAME)
class RoleDataset(Dataset): # 定义了一个名为RoleDataset的自定义数据集类,并继承自Dataset类。 def __init__(self, mode='train',trans_func=None): #定义RoleDataset类的初始化方法,其中mode参数用于指定是训练数据还是测试数据,trans_func参数用于指定数据预处理函数。 super(RoleDataset, self).__init__() if mode == 'train': self.data = pd.read_csv('data/train.csv',sep='\t') else: self.data = pd.read_csv('data/test.csv',sep='\t') self.texts=self.data['text'].tolist() self.labels=self.data[target_cols].to_dict('records') # 将数据集中的标签数据转换为字典格式。 self.trans_func=trans_func # 将数据预处理函数保存到trans_func属性中。 def __getitem__(self, index): #定义数据集类的__getitem__方法,该方法根据给定的索引返回单个样本。 text=str(self.texts[index]) label=self.labels[index] sample = { 'text': text } for label_col in target_cols: sample[label_col] =label[label_col] # 创建一个字典对象,存入所有的‘text’和‘label’ sample=self.trans_func(sample) # 使用预处理函数trans_func处理样本字典 return sample def __len__(self): # 定义数据集类的__len__方法,该方法返回数据集的长度。 return len(self.texts) # 转换成id的函数 def convert_example(example, tokenizer, max_seq_length=512, is_test=False): # print(example) sample={} # 使用BERT Tokenizer对输入文本进行编码并生成对应的token ID,类型ID和注意力掩码,并将它们保存在样本字典中。 encoded_inputs = tokenizer(text=example["text"], max_seq_len=max_seq_length) sample['input_ids'] = encoded_inputs["input_ids"] sample['token_type_ids'] = encoded_inputs["token_type_ids"] # 将样本的情感标签转换成np.array格式,将所有信息存储到一个新的字典sample中 sample['love'] = np.array(example["love"], dtype="float32") sample['joy'] = np.array(example["joy"], dtype="float32") sample['anger'] = np.array(example["anger"], dtype="float32") sample['fright'] = np.array(example["fright"], dtype="float32") sample['fear'] = np.array(example["fear"], dtype="float32") sample['sorrow'] = np.array(example["sorrow"], dtype="float32") return sample max_seq_length=128 trans_func = partial( convert_example, tokenizer=tokenizer, max_seq_length=max_seq_length) # 使用了Python标准库中的partial函数,将convert_example函数的tokenizer和max_seq_length参数进行固定,并生成一个新的函数对象trans_func。这样,在使用trans_func函数时就无需传递这两个参数,只需要传递样本信息即可,这样可以简化代码的编写。其中,tokenizer是对文本进行编码的Tokenizer实例对象,max_seq_length是编码后的文本序列的最大长度。 train_ds=RoleDataset('train',trans_func) test_ds=RoleDataset('test',trans_func)
epochs=3
weight_decay=0.0 # 权重衰减系数
data_path='data'
warmup_proportion=0.0
init_from_ckpt=None
batch_size=64
learning_rate=5e-5
# 创建一个数据加载器(DataLoader),用于从数据集(dataset)中加载数据并组成一个个批次进行训练或预测 def create_dataloader(dataset, mode='train', batch_size=1, batchify_fn=None): shuffle = True if mode == 'train' else False if mode == 'train': batch_sampler = paddle.io.DistributedBatchSampler( dataset, batch_size=batch_size, shuffle=shuffle) else: batch_sampler = paddle.io.BatchSampler( dataset, batch_size=batch_size, shuffle=shuffle) # 这个if-else语句根据数据集模式创建不同的批次采样器。如果数据集模式是“train”,则使用paddle.io.DistributedBatchSampler创建一个分布式批次采样器,否则使用paddle.io.BatchSampler创建一个普通的批次采样器。 return paddle.io.DataLoader( dataset=dataset, batch_sampler=batch_sampler, collate_fn=batchify_fn, return_list=True) # 建一个数据加载器,用于从数据集(dataset)中加载数据并组成批次进行训练或预测。 # dataset:要加载的数据集。 # batch_sampler:批次采样器,用于指定每个批次包含的样本索引。 # collate_fn:批量处理函数,用于将每个批次的样本组合成一个张量或一个字典。 # return_list:指定是否将每个批次的数据转换为列表返回。如果为 True,则返回一个列表,列表中的每个元素表示一个批次;如果为 False,则返回一个迭代器。
通常情况下,我们在训练集上使用分布式批次采样器,而在测试集上使用普通的批次采样器,主要是因为两个数据集的性质不同。
在训练集上,我们通常会进行数据增强、随机翻转等操作,这样可以增加模型的泛化能力,提高模型在新数据上的表现。因此,我们需要使用分布式批次采样器来保证每个进程看到的数据都不同,从而避免模型在重复数据上过拟合。
而在测试集上,我们需要对模型进行验证,计算模型的精度、召回率等指标。因此,我们不需要进行数据增强等操作,而是希望能够遍历整个测试集,并对每个样本进行预测,计算出最终的指标。此时,使用普通的批次采样器即可,因为我们需要遍历整个数据集,并且不需要保证每个进程看到的数据都不同。
此外,分布式批次采样器需要额外的计算资源和网络通信开销,而普通的批次采样器则不需要。因此,使用分布式批次采样器会增加训练的时间和成本,而在测试集上使用普通的批次采样器可以节省计算资源和时间。
def collate_func(batch_data): # 获取batch数据的大小 batch_size = len(batch_data) # 如果batch_size为0,则返回一个空字典 if batch_size == 0: return {} input_ids_list, attention_mask_list = [], [] love_list,joy_list,anger_list=[],[],[] fright_list,fear_list,sorrow_list=[],[],[] # 遍历batch数据,将每一个数据,转换成tensor的形式 for instance in batch_data: input_ids_temp = instance["input_ids"] attention_mask_temp = instance["token_type_ids"] love=instance['love'] joy=instance['joy'] anger=instance['anger'] fright= instance['fright'] fear=instance['fear'] sorrow=instance['sorrow'] input_ids_list.append(paddle.to_tensor(input_ids_temp, dtype="int64")) attention_mask_list.append(paddle.to_tensor(attention_mask_temp, dtype="int64")) love_list.append(love) joy_list.append(joy) anger_list.append(anger) fright_list.append(fright) fear_list.append(fear) sorrow_list.append(sorrow) # 对一个batch内的数据,进行padding return {"input_ids": Pad(pad_val=0, axis=0)(input_ids_list), "token_type_ids": Pad(pad_val=0, axis=0)(attention_mask_list), "love": Stack(dtype="int64")(love_list), "joy": Stack(dtype="int64")(joy_list), "anger": Stack(dtype="int64")(anger_list), "fright": Stack(dtype="int64")(fright_list), "fear": Stack(dtype="int64")(fear_list), "sorrow": Stack(dtype="int64")(sorrow_list), } # 其中,Pad和Stack都是paddle提供的函数,分别用于在指定轴上对数据进行padding和拼接。axis=0表示在第0个轴上进行padding或拼接,pad_val=0表示使用0进行padding。dtype参数指定输出的数据类型,int64表示输出的数据类型为64位整型。
train_data_loader = create_dataloader(
train_ds,
mode='train',
batch_size=batch_size,
batchify_fn=collate_func) # 表示用于batch数据的函数,这里传入了之前定义的collate_func函数。
class EmotionClassifier(nn.Layer): def __init__(self, bert,n_classes): super(EmotionClassifier, self).__init__() self.bert = bert self.out_love = nn.Linear(self.bert.config["hidden_size"], n_classes) self.out_joy = nn.Linear(self.bert.config["hidden_size"], n_classes) self.out_fright = nn.Linear(self.bert.config["hidden_size"], n_classes) self.out_anger = nn.Linear(self.bert.config["hidden_size"], n_classes) self.out_fear = nn.Linear(self.bert.config["hidden_size"], n_classes) self.out_sorrow = nn.Linear(self.bert.config["hidden_size"], n_classes) # 这个类的构造函数,接收两个参数:bert和n_classes。bert是一个预训练的Bert模型,n_classes表示情感分类任务中的分类数。在构造函数内,首先调用父类nn.Layer的构造函数,然后定义了一个属性self.bert,用于保存传入的bert模型。接下来,用nn.Linear定义了六个线性层,分别是对六种情感的分类,输入维度是bert.config["hidden_size"],即Bert模型的隐藏层的维度,输出维度是n_classes。 def forward(self, input_ids, token_type_ids): _, pooled_output = self.bert( input_ids=input_ids, token_type_ids=token_type_ids ) love = self.out_love(pooled_output) joy = self.out_joy(pooled_output) fright = self.out_fright(pooled_output) anger = self.out_anger(pooled_output) fear = self.out_fear(pooled_output) sorrow = self.out_sorrow(pooled_output) return { 'love': love, 'joy': joy, 'fright': fright, 'anger': anger, 'fear': fear, 'sorrow': sorrow, } # 接收两个参数input_ids和token_type_ids。在函数内,首先调用bert模型对输入进行处理,得到模型的输出pooled_output。然后分别将pooled_output输入到六个线性层中,得到对六种情感的分类结果。最后将这六个分类结果以字典的形式返回。 class_names=[1] model = EmotionClassifier(base_model,4)# 创建了一个EmotionClassifier的实例model
num_train_epochs=3 num_training_steps = len(train_data_loader) * num_train_epochs # 表示训练总步数,通过将训练轮数和训练数据集大小相乘得到。 # 定义 learning_rate_scheduler,负责在训练过程中对 lr 进行调度 lr_scheduler = LinearDecayWithWarmup(learning_rate, num_training_steps, 0.0) # 一个包含需要应用权重衰减的参数名的列表,其中排除了所有偏置和 LayerNorm 参数。 decay_params = [ p.name for n, p in model.named_parameters() if not any(nd in n for nd in ["bias", "norm"]) ] # 定义了AdamW优化器,它采用了权重衰减,学习率由lr_scheduler提供。 optimizer = paddle.optimizer.AdamW( learning_rate=lr_scheduler, parameters=model.parameters(), weight_decay=0.0, apply_decay_param_fun=lambda x: x in decay_params) # 交叉熵损失 criterion = paddle.nn.loss.CrossEntropyLoss() # 评估的时候采用准确率指标 metric = paddle.metric.Accuracy()
def do_train( model, data_loader, criterion, optimizer, scheduler, metric ): model.train() # 表示将模型设置为训练模式 global_step = 0 # 表示全局训练步数,用于控制训练过程中一些指标的输出频率。 tic_train = time.time() # 表示开始训练的时间点,用于计算训练速度。 log_steps=100 # 表示训练过程中每隔多少步输出一次训练指标。 for epoch in range(num_train_epochs): losses = [] for step,sample in enumerate(data_loader): # print(sample) # 表示从样本中获取 input_ids 和 token_type_ids。 input_ids = sample["input_ids"] token_type_ids = sample["token_type_ids"] # 表示使用模型进行前向计算,得到预测结果。 outputs = model(input_ids=input_ids, token_type_ids=token_type_ids) # print(outputs) # 多任务学习,分别计算不同标签分类器的损失 loss_love = criterion(outputs['love'], sample['love']) loss_joy = criterion(outputs['joy'], sample['joy']) loss_fright = criterion(outputs['fright'], sample['fright']) loss_anger = criterion(outputs['anger'], sample['anger']) loss_fear = criterion(outputs['fear'], sample['fear']) loss_sorrow = criterion(outputs['sorrow'], sample['sorrow']) loss = loss_love + loss_joy + loss_fright + loss_anger + loss_fear + loss_sorrow # 所有损失求和 for label_col in target_cols: # correct = metric.compute(outputs[label_col], sample[label_col]) # 方法会比较两者的值并返回一个布尔值表示是否预测正确。接下来的一行代码metric.update(correct)用于更新metric对象的状态,将当前的准确率结果加入到其内部的缓存中。 metric.update(correct) acc = metric.accumulate() # 最后,代码计算出当前所有样本的平均准确率,存入acc变量中。这里用到了metric.accumulate()方法,它会返回之前所有的准确率的平均值。 # 损失值存储 losses.append(loss.numpy()) loss.backward() # 反向传播 # nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) global_step += 1 # 每间隔 log_steps 输出训练指标 if global_step % log_steps == 0: print("global step %d, epoch: %d, batch: %d, loss: %.5f, accuracy: %.5f, speed: %.2f step/s" % (global_step, epoch, step, loss, acc, log_steps / (time.time() - tic_train))) optimizer.step() # 根据优化算法更新模型参数 scheduler.step() # 更新学习率 optimizer.clear_grad() # 清空梯度 metric.reset() # metric对象的状态重置为初始状态,以便在下一轮epoch中重新统计模型性能。 return np.mean(losses) # 代码计算出所有batch的平均loss do_train(model,train_data_loader,criterion,optimizer,lr_scheduler,metric)
from collections import defaultdict test_data_loader = create_dataloader( test_ds, mode='test', batch_size=batch_size, batchify_fn=collate_func) test_pred = defaultdict(list) # 保存每个目标列的预测结果 for step, batch in tqdm(enumerate(test_data_loader)): b_input_ids = batch['input_ids'] token_type_ids = batch['token_type_ids'] logits = model(input_ids=b_input_ids, token_type_ids=token_type_ids) # logits对象是一个字典类型,包含了每个目标列的预测结果。 for col in target_cols: out2 = paddle.argmax(logits[col], axis=1) test_pred[col].append(out2.numpy()) # 按照情感标签存储预测标签 print(test_pred) # 字典维度(6个索引,21376) print(logits) break
def predict(model,test_data_loader): val_loss = 0 test_pred = defaultdict(list) model.eval() for step, batch in tqdm(enumerate(test_data_loader)): b_input_ids = batch['input_ids'] token_type_ids = batch['token_type_ids'] with paddle.no_grad(): logits = model(input_ids=b_input_ids, token_type_ids=token_type_ids) for col in target_cols: print(col,logits[col]) out2 = paddle.argmax(logits[col], axis=1) # 求出当前col输出下对应的情感级数 print(out2) test_pred[col].extend(out2.numpy().tolist()) # 将每个col下所有的情感级数按照批次存放到test_pred里面 print(test_pred[col]) return test_pred submit = pd.read_csv('data/submit_example.tsv', sep='\t') test_pred = predict(model,test_data_loader)
label_preds = [] # test_preds,字典,6个索引,每个都有21376个情感级数 for col in target_cols: preds = test_pred[col] label_preds.append(preds) print(len(label_preds),len(label_preds[0])) # (6,21376) sub = submit.copy() # print(sub) sub['emotion'] = np.stack(label_preds, axis=1).tolist() # 按照行合并到一行 # (21376,) print(sub['emotion'].shape) # (21376,),每个元素皆是数组,存入到提交样例中 print(sub["emotion"][0]) sub['emotion'] = sub['emotion'].apply(lambda x: ','.join([str(i) for i in x])) # 将数组转成str类型 print(sub["emotion"][0]) sub.to_csv('baseline_{}.tsv'.format(PRE_TRAINED_MODEL_NAME), sep='\t', index=False) # 生成可提交文件 sub.head()
macbert_data=pd.read_csv('baseline_macbert-base-chinese.tsv',sep='\t')、
# 模型一 计算出的结果数据
macbert_result=macbert_data['emotion'].tolist()
# 将情感值转变成列表存储
macbert_data.head()
macbert_large_data=pd.read_csv('baseline_macbert-large-chinese.tsv',sep='\t')
# 模型二
macbert_large_result=macbert_large_data['emotion'].tolist()
macbert_large_data.head()
roberta_data=pd.read_csv('baseline_roberta-wwm-ext.tsv',sep='\t')
# 模型三
roberta_result=roberta_data['emotion'].tolist()
roberta_data.head()
from collections import Counter def get_counts(list_x): count = Counter(list_x).most_common(1) # most_common 方法取出出现次数最多的元素和它的出现次数。 # Counter('abracadabra').most_common(3) #[('a', 5), ('r', 2), ('b', 2)] return count[0] merge_result=[] # most_common 方法取出出现次数最多的元素和它的出现次数。 result_analyse=[] # 如果count不等于0和3,就将其加入到result_analyse列表中。 for i in range(len(macbert_result)): x1_arr=macbert_result[i].split(',') x2_arr=macbert_large_result[i].split(',') x3_arr=roberta_result[i].split(',') result=[] for x1,x2,x3 in zip(x1_arr,x2_arr,x3_arr): list_x=[] list_x.append(int(x1)) list_x.append(int(x2)) list_x.append(int(x3)) key,count=get_counts(list_x) result.append(key) if(count!=0 and count !=3): result_analyse.append([i,count]) # 将三个列表中对应位置的元素转化为整型后分别存储在变量 x1_arr、x2_arr 和 x3_arr 中。接下来,对于 x1_arr、x2_arr 和 x3_arr 中相应位置的元素,构造一个新的列表 list_x 并调用 get_counts 函数,得到出现次数最多的元素和它出现的次数,将这个元素存储在 result 列表中。如果该元素出现的次数不止一种,且不同时出现,则将这个情况的下标和出现的次数存储在 result_analyse 中。最后将 result 添加到 merge_result 列表中。 merge_result.append(result) print(result_analyse[:10])
sub_merge = submit.copy()
sub_merge['emotion'] =merge_result # 使用投票后的答案
sub_merge['emotion'] = sub_merge['emotion'].apply(lambda x: ','.join([str(i) for i in x]))
sub_merge.to_csv('baseline_merge.tsv', sep='\t', index=False)
# 生成可提交文件
sub_merge.head()
1.数据增强: 中文数据增强工具、回译等
2.尝试不同的预训练模型、调参优化等。
3.5fodls交叉验证、多模型结果融合等
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。