疫情微博情绪识别挑战赛

疫情微博情绪识别挑战赛

举办方:科大讯飞xDatawhale

赛事地址:疫情微博情绪识别挑战赛-点击直达

赛事背景

疫情发生对人们生活生产的方方面面产生了重要影响,并引发了国内舆论的广泛关注,众多网民也参与到了疫情相关话题的讨论中。大众日常的情绪波动在疫情期间会放大,并寻求在自媒体和社交媒体上发布和评论。

为了掌握真实社会舆论情况,科学高效地做好防控宣传和舆情引导工作,针对疫情相关话题开展网民情绪识别是重要任务。本次我们重点关注微博平台上的用户情绪,希望各位选手能搭建自然语言处理模型,对疫情下微博文本的情绪进行识别。

赛事任务

本次赛题需要选手对微博文本进行情绪分类,分为正向情绪和负面情绪。数据样例如下:

在这里插入图片描述

评审规则

  1. 数据说明

赛题数据由训练集和测试集组成,训练集数据集读取代码:

import pandas as pd

pd.read_csv('train.csv',sep='\t')
  1. 评估指标

本次竞赛的评价标准采用准确率指标,最高分为1。
计算方法参考地址:https://scikit-learn.org/stable/modules/generated/sklearn.metrics.accuracy_score.html

评估代码参考:

import sklearn.metrics import accuracy_score

y_pred = [0,2,1,3]
y_true = [0,1,2,3]
accuracy_score(y_pred,y_true)
  1. 评测及排行

1、赛事提供下载数据,选手在本地进行算法调试,在比赛页面提交结果。

2、每支团队每天最多提交3次。

3、排行按照得分从高到低排序,排行榜将选择团队的历史最优成绩进行排名。

作品提交要求

文件格式:预测结果文件按照csv格式提交

文件大小:无要求

提交次数限制:每支队伍每天最多3次

预测结果文件详细说明:

  1. 以csv格式提交,编码为UTF-8,第一行为表头;

  2. 标签顺序需要与测试集文本保持一致;

  3. 提交前请确保预测结果的格式与sample_submit.csv中的格式一致。具体格式如:

label
1
1
1
1

赛程安排

正式赛:6月24日——7月23日

初赛截止成绩以团队在初赛时间段内最优成绩为准,具体排名可见初赛榜单。

初赛作品提交截止日期为7月23日17:00;正式赛名次将于结束后15天内公布。

长期赛:7月24日——10月24日

正式赛结束后,将转变为长期赛,供开发者学习实践。本阶段提交后,系统会根据成绩持续更新长期赛榜单,但该阶段榜单不再进行奖励。

Baseline思路

情感分析是一个经典的文本分类任务,初始Baseline采用预训练模型+微调下游任务的方式搭建

通过两种策略优化Baseline方法得到一个强基线的Baseline方案

策略一:Mutli-dropout

策略二:比较不同的特征池化方案,选取更合适的特征池化方法

先使用参数少的小模型(erbie-3.0-nano)得到初步的最优组合方案,再更换参数大的(erbie-3.0-base)模型结合最优策略得到较强的单模结果。

Baseline 效果

由于提交次数宝贵,因此仅提交了其中三份结果进行验证

一是小模型上验证效果最好(0.963)的单模结果

二是小模型上多模型融合的结果

三是切换为大模型(ernie-3.0-base-zh)的单模效果

模型线下验证线上提交
ernie-3.0-nano-zh + cls0.962000-
ernie-3.0-nano-zh + max0.961833-
ernie-3.0-nano-zh + mean0.962167-
ernie-3.0-nano-zh + dym0.962333-
ernie-3.0-nano-zh + dym + mutlidropout0.9630000.9655
ernie-3.0-nano-zh + mean + mutlidropout0.962833-
ernie-3.0-nano-zh + cls + mutlidropout0.962667-
ernie-3.0-nano-zh + max + mutlidropout0.962667-
ernie-3.0-nano模型融合(voting)-0.9663
ernie-3.0-base-zh + dym + mutlidropout0.971000.9735

从结果上看:

  • Mutlidropout策略十分有效,在不同池化策略的基础上添加Mutlidropout验证效果均有明显涨分
  • 嵌入策略上动态加权池化方法效果最优,其次是平均池化策略
  • 基于Voting的模型融合策略也可以提升模型的性能
  • 更换base版本的大模型后,通过两个策略的加持,线上成绩到达0.9735,靠单模成绩上排行第三,

总结:

  1. 使用了两种有效的策略(Mutlidropout和动态池化策略)获得一个强基线的baseline,希望对还未提升到0.972分数以上的小伙伴一些启发,基于这个强基线的baseline是可以冲击到0.973等更高的分数。

  2. Baseline项目使用ernie-3.0的nano模型仅72MB,micro和nano版本不超过100MB,对资源要求友好,在当前超参数配置下(最大截断长度200,训练批次大小64)显存占用不到5GB,训练3轮5.4万条样本仅需11分钟左右,取得线上0.9655(Rank35 时间:2022-07-09)

  3. 当更换参数量更大的Base模型后,相同配置下显存占用19GB左右,训练时间提升到30分钟。更换Base后的强基线单模线下得到0.9735,进入前五梯队(Rank3 时间:2022-07-09)

在这里插入图片描述

后续优化推荐

  • 使用FGM等对抗训练提升模型的鲁棒性
  • 使用EMA增加模型在测试集上的健壮性
  • 融合不同模型,采用不同的模型融合策略
# 将paddlenlp更新至最新版本
!pip install -U paddlenlp
# emoji转换成文字
!pip install emojiswitch
# 测试 emojiswitch 效果
import emojiswitch
emojiswitch.demojize('心中千万只🐑🐑🐑呼啸而过',delimiters=("",""), lang="zh")
'心中千万只母羊母羊母羊呼啸而过'
import os
import json
import random
import time
import numpy as np
import pandas as pd
from tqdm import tqdm
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import paddle
import paddlenlp
import paddle.nn.functional as F
from functools import partial
from paddlenlp.data import Stack, Dict, Pad
from paddlenlp.datasets import load_dataset
import paddle.nn as nn
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import pearsonr
from paddlenlp.transformers.auto.tokenizer import AutoTokenizer
from paddlenlp.transformers.auto.modeling import AutoModelForSequenceClassification

seed = 12345
def set_seed(seed):
    paddle.seed(seed)
    random.seed(seed)
    np.random.seed(seed)
set_seed(seed)
# 使用ernie-3.0的mini模型,仅109MB左右,micro和nano版本不超过100MB,对资源要求友好
# 在当前配置显存占用不到5GB,训练3轮6万条样本需要20分钟左右
# 当更换base模型会有明显增益,但显存开销和训练时长会大大增加
# ernie-1.0-base-zh / ernie-2.0-base-zh / ernie-2.0-large-zh 
# ernie-3.0-base-zh / ernie-3.0-xbase-zh / ernie-3.0-medium-zh / ernie-3.0-mini-zh / ernie-3.0-micro-zh / ernie-3.0-nano-zh

# 超参数
MODEL_NAME = 'ernie-3.0-base-zh'
# 设置最大阶段长度 和 batch_size
max_seq_length = 200
train_batch_size = 64
valid_batch_size = 64
test_batch_size = 16
# 训练过程中的最大学习率
learning_rate = 2e-5
# 训练轮次
epochs = 3
# 学习率预热比例
warmup_proportion = 0.1
# 权重衰减系数,类似模型正则项策略,避免模型过拟合
weight_decay = 0.01
max_grad_norm = 1.0
# 训练结束后,存储模型参数
save_dir_curr = "checkpoint/{}-model".format(MODEL_NAME.replace('/','-'))
# 记录训练epoch、损失等值
loggiing_print = 50
loggiing_eval = 200
# 提交文件名称
sumbit_name = "work/sumbit.csv"
model_logging_dir = 'work/model_logging.csv'
# 是否开启 mutli-dropout
enable_mdrop = True
enable_adversarial = False
layer_mode = 'dym' # cls / mean / max / dym

1 数据读取和EDA

1.1 读取数据并统一格式

train = pd.read_csv("data/data155996/train.csv",sep='\t')
test = pd.read_csv("data/data155996/test.csv",sep='\t')

print("train size: {} \ntest size {}".format(len(train),len(test)))
train size: 60000 
test size 10000
import re
def clean_str(text):
    text = emojiswitch.demojize(text,delimiters=("",""), lang="zh") # Emoji转文字
    return text.strip()

train['text'] = train['text'].apply(lambda x: clean_str(x))
test['text'] = test['text'].apply(lambda x: clean_str(x))
# 处理后的数据一览
train.head(3)
textlabel
0这是在向世界上所有的母亲宣战![怒] //@子小亻青loukas妈:听说后一直不想看,耐不住...0
1和少奶奶@5棒冰 一起收拾完衣柜,就躺在听她给我讲#步步惊心#的情感纠葛「四和八喜欢她」,「...0
2两个教堂竟然都拍出星轨了……城市的光污染都很严重才对啊,这不科学……[泪]//@简白: 想去...0

1.2 简易数据分析

# 绘制讯飞数据集的文本长度分
train['len'] = [len(i) for i in train["text"]]
test['len'] = [len(i) for i in test["text"]]
print(train['len'].quantile(0.995))
plt.title("text length")
sns.distplot(train['len'],bins=10,color='r')
sns.distplot(test['len'],bins=10,color='g')
plt.show()
179.0

在这里插入图片描述

# 查看标签label分布
print(train['label'].value_counts())
plt.title("label distribution")
sns.countplot(y='label',data=train)
0    31962
1    28038
Name: label, dtype: int64





<matplotlib.axes._subplots.AxesSubplot at 0x7fca648b7f10>

在这里插入图片描述

1.3 结论

  • 文本长度多数在200以下
  • 数据标签分布相对平衡

2 数据处理

# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
# 创建数据迭代器iter
def read(df,istrain=True):
    if istrain:
        for _,data in df.iterrows():
            yield {
                "words":data['text'],
                "labels":data['label']
                }
    else:
        for _,data in df.iterrows():
            yield {
                "words":data['text'],
                }

# 将生成器传入load_dataset
train,valid = train_test_split(train,test_size=0.1,random_state=seed)
train_ds = load_dataset(read, df=train, lazy=False)
valid_ds = load_dataset(read, df=valid, lazy=False)

# 查看数据
for idx in range(1,3):
    print(train_ds[idx])
    print("==="*30)
{'words': '有财真是六千一斤 //@不二小姐yqw: 5000到6000左右吧看样子这条鱼大概有六斤左右[晕] //@云中飞花拳秀腿:应该在2000多吧 //@林广杰-青蛙王子: 回复 @不二小姐yqw:[鼓掌]好样儿的知道多少钱一斤吗 //@不二小姐yqw: 野生大黄鱼吧[思考][思考][思考][思考]', 'labels': 0}
==========================================================================================
{'words': '特喜欢这家酒店!@Oliver郝焕: 离南非哪个城市近?下次要去感受一下。//@Eileenzy: 面朝大海,春暖花开!鲸鱼在眼前[哈哈]@南非精品酒店 @新非洲 @wendy的旅程 这么美的地方,一定要组织更多人去呀@Oliver郝焕 @天蝎座的回忆 @南非航空公司', 'labels': 1}
==========================================================================================
# 编码
def convert_example(example, tokenizer, max_seq_len=512, mode='train'):
    # 调用tokenizer的数据处理方法把文本转为id
    tokenized_input = tokenizer(example['words'],is_split_into_words=True,max_seq_len=max_seq_len)
    if mode == "test":
        return tokenized_input
    # 把意图标签转为数字id
    tokenized_input['labels'] = [example['labels']]
    return tokenized_input # 字典形式,包含input_ids、token_type_ids、labels

train_trans_func = partial(
        convert_example,
        tokenizer=tokenizer,
        mode='train',
        max_seq_len=max_seq_length)

valid_trans_func = partial(
        convert_example,
        tokenizer=tokenizer,
        mode='dev',
        max_seq_len=max_seq_length)

# 映射编码
train_ds.map(train_trans_func, lazy=False)
valid_ds.map(valid_trans_func, lazy=False)

# 初始化BatchSampler
np.random.seed(seed)
train_batch_sampler = paddle.io.BatchSampler(train_ds, batch_size=train_batch_size, shuffle=True)
valid_batch_sampler = paddle.io.BatchSampler(valid_ds, batch_size=valid_batch_size, shuffle=False)

# 定义batchify_fn
batchify_fn = lambda samples, fn = Dict({
    "input_ids": Pad(axis=0, pad_val=tokenizer.pad_token_id), 
    "token_type_ids": Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
    "labels": Stack(dtype="int32"),
}): fn(samples)

# 初始化DataLoader
train_data_loader = paddle.io.DataLoader(
    dataset=train_ds,
    batch_sampler=train_batch_sampler,
    collate_fn=batchify_fn,
    return_list=True)
valid_data_loader = paddle.io.DataLoader(
    dataset=valid_ds,
    batch_sampler=valid_batch_sampler,
    collate_fn=batchify_fn,
    return_list=True)

3 模型搭建

3.1 基础的分类模型

预训练模型 + 微调下游任务的全连接层(dropout + linear)

from paddlenlp.transformers.ernie.modeling import ErniePretrainedModel

# 原始的基于Ernie的分类模型
class EmotionErnieModel(ErniePretrainedModel):
    def __init__(self, ernie, num_classes=1, dropout=None):
        super().__init__()
        # 预训练模型
        self.ernie = ernie
        self.num_classes = num_classes
        self.dropout = nn.Dropout(self.ernie.config['hidden_dropout_prob'])
        self.classifier = nn.Linear(self.ernie.config['hidden_size'],self.num_classes)
        self.apply(self.init_weights)

    def forward(self,input_ids,token_type_ids=None):
        sequence_output , _ = self.ernie(input_ids,token_type_ids=token_type_ids)
        sequence_output = sequence_output.mean(axis=1)
        sequence_output = self.dropout(sequence_output) 
        logits = self.classifier(sequence_output)
        return logits 

3.2 模型改进方向1 — Multi-Sample Dropout

参考资料:Multi-Sample Dropout

在这里插入图片描述

Multi-Sample Dropout是对Dropout的一种改进,改进后的结果是加快了训练收敛速度和提高了泛化能力。

具体代码如下:

# 增加MultiDropout-Ernie的分类模型
class Mdrop(nn.Layer):
    def __init__(self):
        super(Mdrop,self).__init__()
        self.dropout_0 = nn.Dropout(p=0)
        self.dropout_1 = nn.Dropout(p=0.1)
        self.dropout_2 = nn.Dropout(p=0.2)
        self.dropout_3 = nn.Dropout(p=0.3)
        self.dropout_4 = nn.Dropout(p=0.4)
    def forward(self,x):
        output_0 = self.dropout_0(x)
        output_1 = self.dropout_1(x)
        output_2 = self.dropout_2(x)
        output_3 = self.dropout_3(x)
        output_4 = self.dropout_4(x)
        return [output_0,output_1,output_2,output_3,output_4]
class EmotionMDropErnieModel(ErniePretrainedModel):
    def __init__(self, ernie, num_classes=1, dropout=None):
        super().__init__()
        # 预训练模型
        self.ernie = ernie
        self.num_classes = num_classes
        # 设置mutlidropout
        self.dropout = Mdrop()
        self.classifier = nn.Linear(self.ernie.config['hidden_size'],self.num_classes)
        self.apply(self.init_weights)

    def forward(self,input_ids,token_type_ids=None):
        sequence_output , _ = self.ernie(input_ids,token_type_ids=token_type_ids)
        sequence_output = sequence_output.mean(axis=1)
        sequence_output = self.dropout(sequence_output)
        # 将mutlidropout进行pooling
        sequence_output = paddle.mean(paddle.stack(sequence_output,axis=0),axis=0) 
        logits = self.classifier(sequence_output)
        return logits 

3.3 模型改进方向2 — 不同的特征提取方式

本次比赛是了解和学习BERT预训练模型非常好的途径,下面的资料详细的结构了BERT

参考资料:英文版 - The Illustrated BERT, ELMo, and co. (How NLP Cracked Transfer Learning)

参考资料:机器之心翻译版 - 图解当前最强语言模型BERT:NLP是如何攻克迁移学习的?

在这里插入图片描述

其中,基于微调的方式,使得我们习惯了在Transformer之后附加一个额外的输出层,用于下游任务或模型的后半部分,将预训练的语言模型的最后一层的表征作为默认输入。

上述图片中,作者通过将不同的向量组合作为输入特征,输入到一个用于命名实体识别任务的BiLSTM中,并观察所得到的F1分数,来测试不同单词嵌入策略的效果。

结果显示,串联最后四层的产生了最好的结果。

由于ernie模型的源码中无法获取全部12层的特征,所以本次代码中尝试4种词嵌入策略,分别为:

  • CLS Last Hidden Layer
  • Mean Pooling Last Hidden Layer
  • Max Pooling Last Hidden Layer
  • Dynamic Pooling Last Hidden Layer

使用一个参数layer_mode选择不同的词嵌入策略cls、max、mean、dym 分别表示上述策略

# 不同嵌入策略的分类模型
class EmotionLayerModel(ErniePretrainedModel):
    def __init__(self, ernie, num_classes=1, dropout=None):
        super().__init__()
        # 预训练模型
        self.ernie = ernie
        self.num_classes = num_classes
        self.dropout = nn.Dropout(self.ernie.config['hidden_dropout_prob'])
        self.classifier = nn.Linear(self.ernie.config['hidden_size'],self.num_classes)
        self.dym_pool = nn.Linear(self.ernie.config['hidden_size'],1)
        self.apply(self.init_weights)

    def dym_pooling(self, avpooled_out, maxpooled_out):
        pooled_output = [avpooled_out, maxpooled_out]
        pool_logits = []
        for i, layer in enumerate(pooled_output):
            pool_logits.append(self.dym_pool(layer))
        pool_logits = paddle.concat(pool_logits, axis=-1)
        pool_dist = paddle.nn.functional.softmax(pool_logits)
        pooled_out = paddle.concat([paddle.unsqueeze(x, 2) for x in pooled_output], axis=2)
        pooled_out = paddle.unsqueeze(pooled_out, 1)
        pool_dist = paddle.unsqueeze(pool_dist, 2)
        pool_dist = paddle.unsqueeze(pool_dist, 1)
        pooled_output = paddle.matmul(pooled_out, pool_dist)
        pooled_output = paddle.squeeze(pooled_output)
        return pooled_output

    def forward(self,input_ids,token_type_ids=None):
        sequence_output , pooled_output = self.ernie(input_ids,token_type_ids=token_type_ids)
         # 选择嵌入策略
        if layer_mode == "mean":
            output = sequence_output.mean(axis=1)
        elif layer_mode == "max":
            output = sequence_output.max(axis=1)
        elif layer_mode == "dym":
            mean_output = sequence_output.mean(axis=1)
            max_output = sequence_output.max(axis=1)
            output = self.dym_pooling(mean_output,max_output)
        else:
            # 默认使用cls
            output = pooled_output
        output = self.dropout(output) 
        logits = self.classifier(output)
        return logits 

3.4 改进后的分类模型

为了提高代码复用性,创建一个新的model

使用参数enable_mdrop控制mutlidropout的开启和关闭

使用参数layer_mode控制得到文本特征的方式

使用下面的model作为后续的baseline模型

# 改进后的模型
class EmotionModel(ErniePretrainedModel):
    def __init__(self, ernie, num_classes=1, dropout=None):
        super().__init__()
        # 预训练模型
        self.ernie = ernie
        self.num_classes = num_classes
        if enable_mdrop:
            self.dropout = Mdrop()
        else:
            self.dropout = nn.Dropout(self.ernie.config['hidden_dropout_prob'])
        self.classifier = nn.Linear(self.ernie.config['hidden_size'],self.num_classes)
        self.dym_pool = nn.Linear(self.ernie.config['hidden_size'],1)
        self.apply(self.init_weights)

    def dym_pooling(self, avpooled_out, maxpooled_out):
        pooled_output = [avpooled_out, maxpooled_out]
        pool_logits = []
        for i, layer in enumerate(pooled_output):
            pool_logits.append(self.dym_pool(layer))
        pool_logits = paddle.concat(pool_logits, axis=-1)
        pool_dist = paddle.nn.functional.softmax(pool_logits)
        pooled_out = paddle.concat([paddle.unsqueeze(x, 2) for x in pooled_output], axis=2)
        pooled_out = paddle.unsqueeze(pooled_out, 1)
        pool_dist = paddle.unsqueeze(pool_dist, 2)
        pool_dist = paddle.unsqueeze(pool_dist, 1)
        pooled_output = paddle.matmul(pooled_out, pool_dist)
        pooled_output = paddle.squeeze(pooled_output)
        return pooled_output

    def forward(self,input_ids,token_type_ids=None):
        sequence_output , pooled_output = self.ernie(input_ids,token_type_ids=token_type_ids)
        # 选择嵌入策略
        if layer_mode == "mean":
            output = sequence_output.mean(axis=1)
        elif layer_mode == "max":
            output = sequence_output.max(axis=1)
        elif layer_mode == "dym":
            mean_output = sequence_output.mean(axis=1)
            max_output = sequence_output.max(axis=1)
            output = self.dym_pooling(mean_output,max_output)
        else:
            # 默认使用cls
            output = pooled_output
        # 选择dropout
        output = self.dropout(output)
        if enable_mdrop:
            output = paddle.mean(paddle.stack(output,axis=0),axis=0) 
        # 下游任务
        logits = self.classifier(output)
        return logits 
# 创建model
label_classes = train['label'].unique()
model = EmotionModel.from_pretrained(MODEL_NAME,num_classes=len(label_classes))

4 模型配置

# 训练总步数
num_training_steps = len(train_data_loader) * epochs

# 学习率衰减策略
lr_scheduler = paddlenlp.transformers.LinearDecayWithWarmup(learning_rate, num_training_steps,warmup_proportion)

decay_params = [
    p.name for n, p in model.named_parameters()
    if not any(nd in n for nd in ["bias", "norm"])
]

# 定义优化器
optimizer = paddle.optimizer.AdamW(
    learning_rate=lr_scheduler,
    parameters=model.parameters(),
    weight_decay=weight_decay,
    apply_decay_param_fun=lambda x: x in decay_params,
    grad_clip=paddle.nn.ClipGradByGlobalNorm(max_grad_norm))
# utils - 对抗训练 FGM
class FGM(object):
    """
    Fast Gradient Method(FGM)
    针对 embedding 层梯度上升干扰的对抗训练方法
    """
    def __init__(self, model, epsilon=1., emb_name='emb'):
        # emb_name 这个参数要换成你模型中embedding 的参数名
        self.model = model
        self.epsilon = epsilon
        self.emb_name = emb_name
        self.backup = {}

    def attack(self):
        for name, param in self.model.named_parameters():
            if not param.stop_gradient and self.emb_name in name:  # 检验参数是否可训练及范围
                self.backup[name] = param.numpy()  # 备份原有参数值
                grad_tensor = paddle.to_tensor(param.grad)  # param.grad 是个 numpy 对象
                norm = paddle.norm(grad_tensor)  # norm 化
                if norm != 0:
                    r_at = self.epsilon * grad_tensor / norm
                    param.add(r_at)  # 在原有 embed 值上添加向上梯度干扰

    def restore(self):
        for name, param in self.model.named_parameters():
            if not param.stop_gradient and self.emb_name in name:
                assert name in self.backup
                param.set_value(self.backup[name])  # 将原有 embed 参数还原
        self.backup = {}

# 对抗训练
if enable_adversarial:
    adv = FGM(model=model,epsilon=1e-6,emb_name='word_embeddings')

5 模型训练

# 验证部分
@paddle.no_grad()
def evaluation(model, data_loader):
    model.eval()
    real_s = []
    pred_s = []
    for batch in data_loader:
        input_ids, token_type_ids, labels = batch
        logits = model(input_ids, token_type_ids)
        probs = F.softmax(logits,axis=1)
        pred_s.extend(probs.argmax(axis=1).numpy())
        real_s.extend(labels.reshape([-1]).numpy())
    score =  accuracy_score(y_pred=pred_s,y_true=real_s)
    return score

# 训练阶段
def do_train(model,data_loader):
    total_loss = 0.
    model_total_epochs = 0
    best_score = 0.9
    training_loss = 0
    # 训练
    print("train ...")
    train_time = time.time()
    valid_time = time.time()
    model.train()
    for epoch in range(0, epochs):
        preds,reals = [],[]
        for step, batch in enumerate(data_loader, start=1):
            input_ids, token_type_ids, labels = batch
            logits = model(input_ids, token_type_ids)
            loss = F.softmax_with_cross_entropy(logits,labels).mean()

            probs = F.softmax(logits,axis=1)
            preds.extend(probs.argmax(axis=1))
            reals.extend(labels.reshape([-1]))
            
            loss.backward()
            # 对抗训练
            if enable_adversarial:
                adv.attack()  # 在 embedding 上添加对抗扰动
                adv_logits = model(input_ids, token_type_ids)
                adv_loss = F.softmax_with_cross_entropy(adv_logits,labels).mean()
                adv_loss.backward()  # 反向传播,并在正常的 grad 基础上,累加对抗训练的梯度
                adv.restore()  # 恢复 embedding 参数

            total_loss +=  loss.numpy()
            optimizer.step()
            lr_scheduler.step()
            optimizer.clear_grad()
        
            model_total_epochs += 1
            if model_total_epochs % loggiing_print == 0:
                train_acc = accuracy_score(preds,reals)
                print("step: %d / %d, train acc: %.5f training loss: %.5f speed %.1f s" % (model_total_epochs, num_training_steps, train_acc, total_loss/model_total_epochs,(time.time() - train_time)))
                train_time = time.time()
            
            if model_total_epochs % loggiing_eval == 0:
                eval_score = evaluation(model, valid_data_loader)
                print("validation speed %.2f s" % (time.time() - valid_time))
                valid_time = time.time()
                if best_score  < eval_score:
                    print("eval acc: %.5f acc update %.5f ---> %.5f " % (eval_score,best_score,eval_score))
                    best_score  = eval_score
                    # 保存模型
                    os.makedirs(save_dir_curr,exist_ok=True)
                    save_param_path = os.path.join(save_dir_curr, 'model_best.pdparams')
                    paddle.save(model.state_dict(), save_param_path)
                    # 保存tokenizer
                    tokenizer.save_pretrained(save_dir_curr)
                else:
                    print("eval acc: %.5f but best acc %.5f " % (eval_score,best_score))
                model.train()
    return best_score
best_score = do_train(model,train_data_loader)

nano版本训练完3轮5.4W多条数据大约需要10分钟

base版本训练完3轮5.4W多条数据大约需要33分钟

print("best pearsonr score: %.5f" % best_score)
best pearsonr score: 0.97100
# logging part
logging_dir = 'work/sumbit'
logging_name = os.path.join(logging_dir,'run_logging.csv')
os.makedirs(logging_dir,exist_ok=True)

var = [MODEL_NAME, seed, learning_rate, max_seq_length, layer_mode, enable_mdrop, enable_adversarial, best_score]
names = ['model', 'seed', 'lr', "max_len" , 'layer_mode', 'enable_mdrop', 'enable_adversarial', 'best_score']
vars_dict = {k: v for k, v in zip(names, var)}
results = dict(**vars_dict)
keys = list(results.keys())
values = list(results.values())

if not os.path.exists(logging_name):    
    ori = []
    ori.append(values)
    logging_df = pd.DataFrame(ori, columns=keys)
    logging_df.to_csv(logging_name, index=False)
else:
    logging_df= pd.read_csv(logging_name)
    new = pd.DataFrame(results, index=[1])
    logging_df = logging_df.append(new, ignore_index=True)
    logging_df.to_csv(logging_name, index=False)  
logging_df.head(10)
modelseedlrmax_lenlayer_modeenable_mdropenable_adversarialbest_score
0ernie-3.0-nano-zh123450.00002200clsFalseFalse0.962000
1ernie-3.0-nano-zh123450.00002200maxFalseFalse0.961833
2ernie-3.0-nano-zh123450.00002200meanFalseFalse0.962167
3ernie-3.0-nano-zh123450.00002200dymFalseFalse0.962333
4ernie-3.0-nano-zh123450.00002200dymTrueFalse0.963000
5ernie-3.0-nano-zh123450.00002200meanTrueFalse0.962833
6ernie-3.0-nano-zh123450.00002200clsTrueFalse0.962667
7ernie-3.0-nano-zh123450.00002200maxTrueFalse0.962667
8ernie-3.0-base-zh123450.00002200dymTrueFalse0.971000

6 模型预测

# 相同方式构造测试集
test_ds = load_dataset(read,df=test, istrain=False, lazy=False)

test_trans_func = partial(
        convert_example,
        tokenizer=tokenizer,
        mode='test',
        max_seq_len=max_seq_length)

test_ds.map(test_trans_func, lazy=False)

test_batch_sampler = paddle.io.BatchSampler(test_ds, batch_size=test_batch_size, shuffle=False)

test_batchify_fn = lambda samples, fn = Dict({
    "input_ids": Pad(axis=0, pad_val=tokenizer.pad_token_id), 
    "token_type_ids": Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
}): fn(samples)

test_data_loader = paddle.io.DataLoader(
    dataset=test_ds,
    batch_sampler=test_batch_sampler,
    collate_fn=test_batchify_fn,
    return_list=True)

# 预测阶段
def do_sample_predict(model,data_loader):
    model.eval()
    preds = []
    for batch in data_loader:
        input_ids, token_type_ids= batch
        logits = model(input_ids, token_type_ids)
        probs = F.softmax(logits,axis=1)
        preds.extend(probs.argmax(axis=1).numpy())
    return preds

# 读取最佳模型
state_dict = paddle.load(os.path.join(save_dir_curr,'model_best.pdparams'))
model.load_dict(state_dict)

# 预测
print("predict start ...")
pred_score = do_sample_predict(model,test_data_loader)
print("predict end ...")
predict start ...
predict end ...

7 生成提交文件

# 根据运行日志生成编号,对应日志编号的生成提交文件名称
# 例如sumbit_emtion1.csv 就代表日志index为1的提交结果文件
result_record = len(logging_df) - 1
sumbit = pd.DataFrame([],columns=['label'])
sumbit["label"] = pred_score
sumbit.to_csv("work/sumbit/sumbit_emtion{}.csv".format(result_record),index=False)

8 模型融合-Voting硬投票

对8份提交文件使用最简单的模型融合操作,使用Voting也能够投票的方式,利用“和而不同”的方式集成不同结果

paddle.mode()可以沿着可选的axis查找对应轴上的众数和结果所在的索引信息

使用paddle.mode可以快速进行voting操作

参考资料:paddle.mode()文档

# 示例代码
import paddle

tensor = paddle.to_tensor([[1,2,3],[2,2,3],[2,2,1],[2,3,3]],dtype=paddle.int32)
res = paddle.mode(tensor,axis=0)
voting_values = res[0].numpy() # 众数结果
voting_indexs = res[1].numpy() # 众数索引
print(voting_values)
# 输出结果
[2 2 3]
enable_voting = True
if enable_voting:
    # 读取多个模型的结果文件
    voting_file = [i for i in os.listdir('work/sumbit') if 'sumbit' in i]
    print(voting_file)
    # 创建新的提交文件
    ensemble_sumbit = pd.DataFrame([],columns=['label'])
    # 使用列表保存每一个提交文件的结果
    concat_pred = []
    for sumbit_dir in voting_file:
        pred = pd.read_csv(os.path.join('work/sumbit',sumbit_dir))
        concat_pred.append(pred['label'].tolist())
    # 调用paddle.mode快速得到voting结果
    ensemble_sumbit['label'] = paddle.mode(paddle.to_tensor(concat_pred),axis=0)[0].numpy()
    # 保存voting结果
    ensemble_sumbit.to_csv("work/voting_results.csv",index=False)
    print(f"已生成voting文件 work/voting_results.csv")
['sumbit_emtion5.csv', 'sumbit_emtion4.csv', 'sumbit_emtion7.csv', 'sumbit_emtion8.csv', 'sumbit_emtion0.csv', 'sumbit_emtion3.csv', 'sumbit_emtion2.csv', 'sumbit_emtion1.csv', 'sumbit_emtion6.csv']
已生成voting文件 work/voting_results.csv

总结

模型线下验证线上提交
ernie-3.0-nano-zh + cls0.962000-
ernie-3.0-nano-zh + max0.961833-
ernie-3.0-nano-zh + mean0.962167-
ernie-3.0-nano-zh + dym0.962333-
ernie-3.0-nano-zh + dym + mutlidropout0.9630000.9655
ernie-3.0-nano-zh + mean + mutlidropout0.962833-
ernie-3.0-nano-zh + cls + mutlidropout0.962667-
ernie-3.0-nano-zh + max + mutlidropout0.962667-
ernie-3.0-nano模型融合(voting)-0.9663
ernie-3.0-base-zh + dym + mutlidropout0.971000.9735

提交ernie-3.0-base-zh + dym + mutlidropout的预测文件线上取得0.9735的成绩,进入前五梯队(Rank3 时间:2022-07-09)

在本项目中使用了两种有效的策略(Mutlidropout和动态池化策略)获得一个强基线的baseline,,完成了模型训练、结果提交全流程,包括数据准备、模型训练及保存和预测步骤,并完成了预测结果提交。希望本Baseline能够参加比赛的队伍带来思路,希望榜前的大佬多多分享比赛思路!

如果项目中有任何问题,欢迎在评论区留言交流,共同进步!

此项目仅为搬运,原作链接:https://aistudio.baidu.com/aistudio/projectdetail/4305294

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐