2022搜狐校园 情感分析 × 推荐排序 算法大赛 baseline

本文涉及的产品
NLP 自学习平台,3个模型定制额度 1个月
NLP自然语言处理_高级版,每接口累计50万次
NLP自然语言处理_基础版,每接口每天50万次
简介: 2022搜狐校园 情感分析 × 推荐排序 算法大赛 baseline

67.png


比赛链接:https://www.biendata.xyz/competition/sohu_2022/


完整代码 可关注ChallengeHub 回复“搜狐”即可获取


赛题背景


在工业界,推荐算法和自然语言处理是结合非常紧密的两个技术环节。

本次大赛我们推出创新赛制——NLP 和推荐算法双赛道:探究文本情感对推荐转化的影响。情感分析是NLP领域的经典任务,本次赛事在经典任务上再度加码,研究文本对指定对象的情感极性及色彩强度,难度升级,挑战加倍。

同时拥有将算法成果研究落地实际场景的绝佳机会,接触在校园难以体验到的工业实践,体验与用户博弈的真实推荐场景。


比赛任务


比赛分为两部分:

  • 第一部分:面向实体对象的文本描述情感极性及色彩强度分析。情感极性和强度分为五种情况:极正向、正向、中立、负向、极负向。选手需要针对给定的每一个实体对象,从文本描述的角度,分析出对该实体的情感极性和强度。
  • 第二部分:利用给出的用户文章点击序列数据及用户相关特征,结合第一部分做出的情感分析模型,对给定的文章做出是否会形成点击转化的预测判别。用户点击序列中涉及的文章,及待预测的文章,我们都会给出其详细内容。


任务1:面向实体对象的文本情感分类


第一部分:面向实体对象的文本描述情感极性及色彩强度分析。情感极性和强度分为五种情况:极正向、正向、中立、负向、极负向。选手需要针对给定的每一个实体对象,从文本描述的角度,分析出对该实体的情感极性和强度。


数据加载

train_file = 'data/Sohu2022_data/nlp_data/train.txt'
test_file = 'data/Sohu2022_data/nlp_data/test.txt'
sub_file = 'data/submission/section1.txt'
train = pd.read_json(train_file, lines=True)
test = pd.read_json(test_file, lines=True)
sub= pd.read_table(sub_file)


69.png


文本长度统计


train['text_len'].quantile([0.5,0.8,0.9,0.96])
0.50    426.0
0.80    479.0
0.90    515.0
0.96    562.0
Name: text_len, dtype: float64


大部分文本长度在562以内,在迭代过程中发现,输入到模型的文本越完整效果越好,所以可以尝试文档级的模型,比如ernie-doc或者xlnet等。


70.png


实体情感标签统计


sns.countplot(sentiment_df.sentiment)
plt.xlabel('sentiment value count')


71.png


可以看出中性情感占到了绝大部分,极端情感最少。因为数据量比较大,大家可以使用一些采样策略:

  • 中立情感负采样 ,但是有过拟合风险
  • 保证情感比例采样:加快模型迭代速度
  • 对同一个样本的重复情感可以负采样,ent1和ent2:1   text|ent1+ent2


数据预处理



同一样本的标签有多个,然后按照多个实体情感对样本进行复制,得到每个文本以及标签,处理代码如下:

lst_col = 'sentiment'
train = pd.DataFrame({
    col: np.repeat(train[col].values, train[lst_col].str.len())
    for col in train.columns.difference([lst_col])
}).assign(**{lst_col: np.concatenate(train[lst_col].values)})[train.columns.tolist()]


72.png


由于文本过长,对于资源紧缺的同学可以尝试文本滑窗构造、关键句筛选等方式,参考处理代码如下:

def get_text_label(row):
    content = row['content']
    # print(row['sentiment'].keys())
    entity, sentiment = list(row['sentiment'].keys())[0], list(row['sentiment'].values())[0]
    # 构造文本方式1:
    # line_split = split_sentences(content)
    # contain_indexs=[index for index,sent in enumerate(line_split) if entity in sent] # 1 和6
    # tmp=[]
    # for index in contain_indexs:
    #     if index==0:
    #         tmp.extend(line_split[index:index+2])
    #     elif index==len(line_split)-1:
    #         tmp.extend(line_split[index-2:])
    #     else:
    #         tmp.extend(line_split[index-2:index+1])
    # # print(tmp)
    # text = ','.join(tmp)
    ## textrank 构造文本
    # 160-200 0.633
    return entity, content, sentiment


模型定义


class LastHiddenModel(nn.Module):
    def __init__(self, model_name, n_classes):
        super().__init__()
        config = AutoConfig.from_pretrained(model_name)
        self.model = AutoModel.from_pretrained(model_name, config=config)
        self.linear = nn.Linear(config.hidden_size, n_classes)
    def forward(self, input_ids, attention_mask, token_type_ids):
        outputs = self.model(input_ids, attention_mask, token_type_ids)# last_hidden_state和pooler out
        last_hidden_state = outputs[0] # 所有字符最后一层hidden state # 32 400 768 ,但是PAD PAD
        input_mask_expanded = attention_mask.unsqueeze(-1).expand(last_hidden_state.size()).float()
        sum_embeddings = torch.sum(last_hidden_state * input_mask_expanded, 1)
        sum_mask = input_mask_expanded.sum(1)
        sum_mask = torch.clamp(sum_mask, min=1e-9)
        mean_embeddings = sum_embeddings / sum_mask
        logits = self.linear(mean_embeddings)
        return logits


扩展思路:


  • 长文本处理:模型输入/模型预测:TTA
  • doc级文本模型:longformer

(xlnet) https://huggingface.co/hfl/chinese-xlnet-base
(longformer_zh) https://huggingface.co/ValkyriaLenneth/longformer_zh
(longformer-chinese-base-4096) https://huggingface.co/schen/longformer-chinese-base-4096
transform-xl:


  • 轻量级模型:LSTM、GRU/Transformer等网络 600 word 300
  • 选择使用不同预训练模型进行微调,chinese-roberta-wwm/nezha/xlnet/ernie/ernie-gram,其中ernie或者ernie-gram效果可能会好些
  • 预训练模型输出的利用:CLS/PoolerOut/LastHiddenState/+(Bi)LSTM/LastFourConcat/etc...
  • 训练优化:对抗训练(FGM/PGD/AWP)/EMA/MultiDropout/Rdrop

import torch.nn.functional as F
# define your task model, which outputs the classifier logits
model = TaskModel()
def compute_kl_loss(self, p, q, pad_mask=None):
    p_loss = F.kl_div(F.log_softmax(p, dim=-1), F.softmax(q, dim=-1), reduction='none')
    q_loss = F.kl_div(F.log_softmax(q, dim=-1), F.softmax(p, dim=-1), reduction='none')
    # pad_mask is for seq-level tasks
    if pad_mask is not None:
        p_loss.masked_fill_(pad_mask, 0.)
        q_loss.masked_fill_(pad_mask, 0.)
    # You can choose whether to use function "sum" and "mean" depending on your task
    p_loss = p_loss.sum()
    q_loss = q_loss.sum()
    loss = (p_loss + q_loss) / 2
    return loss
# keep dropout and forward twice
logits = model(x)
logits2 = model(x)
# cross entropy loss for classifier
ce_loss = 0.5 * (cross_entropy_loss(logits, label) + cross_entropy_loss(logits2, label))
kl_loss = compute_kl_loss(logits, logits2)
# carefully choose hyper-parameters
loss = ce_loss + α * kl_loss


改进1 Last 4 Layers Concatenating

class LastFourModel(nn.Module):
    def __init__(self):
        super().__init__()
        config = AutoConfig.from_pretrained(PRE_TRAINED_MODEL_NAME)
        config.update({'output_hidden_states':True})
        self.model = AutoModel.from_pretrained(PRE_TRAINED_MODEL_NAME, config=config)
        self.linear = nn.Linear(4*768, n_classes)
    def forward(self, input_ids, attention_mask):
        outputs = self.model(input_ids, attention_mask)
        all_hidden_states = torch.stack(outputs[2])
        concatenate_pooling = torch.cat(
            (all_hidden_states[-1], all_hidden_states[-2], all_hidden_states[-3], all_hidden_states[-4]), -1
        )
        concatenate_pooling = concatenate_pooling[:,0]
        output = self.linear(concatenate_pooling)
        return soutput


改进2 模型层间差分学习率

对不同的网络层数使用不同的学习率,这样可以防止过拟合,有利于加速学习。

def get_parameters(model, model_init_lr, multiplier, classifier_lr):
    parameters = []
    lr = model_init_lr
    for layer in range(12,-1,-1):
        layer_params = {
            'params': [p for n,p in model.named_parameters() if f'encoder.layer.{layer}.' in n],
            'lr': lr
        }
        parameters.append(layer_params)
        lr *= multiplier
    classifier_params = {
        'params': [p for n,p in model.named_parameters() if 'layer_norm' in n or 'linear' in n 
                   or 'pooling' in n],
        'lr': classifier_lr
    }
    parameters.append(classifier_params)
    return parameters
parameters=get_parameters(model,2e-5,0.95, 1e-4)
optimizer=AdamW(parameters)


  • BERT长文本处理:《CogLTX: Applying BERT to Long Texts》

https://github.com/Sleepychord/CogLTX

COGLTX采用的策略是将每个子句从原句中移除判断其是否是必不可少的(t是一个阈值):


72.png


分类实例:https://github.com/Sleepychord/CogLTX/blob/main/run_20news.py

CogLTX : bert处理长文本代码解析


  • XLNET分类模型

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from transformers import XLNetModel
class MyXLNet(nn.Module):
    def __init__(self, num_classes=35, alpha=0.5):
        self.alpha = alpha
        super(MyXLNet, self).__init__()
        self.net = XLNetModel.from_pretrained(xlnet_cfg.xlnet_path).cuda()
        for name, param in self.net.named_parameters():
            if 'layer.11' in name or 'layer.10' in name or 'layer.9' in name or 'layer.8' in name or 'pooler.dense' in name:
                param.requires_grad = True
            else:
                param.requires_grad = False
        self.MLP = nn.Sequential(
            nn.Linear(768, num_classes, bias=True),
        ).cuda()
    def forward(self, x):
        x = x.long()
        x = self.net(x, output_all_encoded_layers=False).last_hidden_state
        x = F.dropout(x, self.alpha, training=self.training)
        x = torch.max(x, dim=1)[0]
        x = self.MLP(x)
        return torch.sigmoid(x)


  • 长文本理解模型 ERNIE-Doc


ERNIE-DOC,是一个基于Recurrence Transformers(Dai et al., 2019) 的文档级语言预训练模型。 本模型用了两种技术:回溯式feed机制和增强的循环机制,使模型 具有更长的有效上下文长度,以获取整个文档的相关信息。

https://github.com/PaddlePaddle/ERNIE

import paddle
from paddlenlp.transformers import *
tokenizer = AutoTokenizer.from_pretrained('ernie-doc-base-zh')
text = tokenizer('自然语言处理')
# 语义表示
model = AutoModel.from_pretrained('ernie-doc-base-zh')
sequence_output, pooled_output = model(input_ids=paddle.to_tensor([text['input_ids']]))
# 文本分类 & 句对匹配
model = AutoModelForSequenceClassification.from_pretrained('ernie-doc-base-zh')


任务2:文章点击预测


第二部分:利用给出的用户文章点击序列数据及用户相关特征,结合第一部分做出的情感分析模型,对给定的文章做出是否会形成点击转化的预测判别。用户点击序列中涉及的文章,及待预测的文章,我们都会给出其详细内容。


数据加载


train = pd.read_csv('data/Sohu2022_data/rec_data/train-dataset.csv')
test = pd.read_csv('data/Sohu2022_data/rec_data/test-dataset.csv')
print("train_data.shape",train.shape)
print("test_data.shape",test.shape)


73.png


训练集中每条样本包含pvId,用户id,点击序列(序列中的每次点击都包含文章id和浏览时间),用户特征(包含但不限于操作系统、浏览器、设备、运营商、省份、城市等),待预测文章id和当前时间戳,以及用户的行为(1为有点击,0为未点击)。

  • smapleId:样本的唯一id
  • label:点击标签
  • pvId:将每次曝光给用户的展示结果列表称为一个Group(每个Group都有唯一的pvId)
  • suv:用户id
  • itemId:文章id
  • userSeq:点击序列
  • logTs:当前时间戳
  • operator:操作系统
  • browserType:浏览器
  • deviceType:设备
  • osType:运营商
  • province:省份
  • city:城市


数据分析


def statics(data):
    stats = []
    for col in data.columns:
        stats.append((col, data[col].nunique(), data[col].isnull().sum() * 100 / data.shape[0],
                      data[col].value_counts(normalize=True, dropna=False).values[0] * 100, data[col].dtype))
    stats_df = pd.DataFrame(stats, columns=['Feature', 'Unique_values', 'Percentage_of_missing_values',
                                            'Percentage_of_values_in_the_biggest category', 'type'])
    stats_df.sort_values('Percentage_of_missing_values', ascending=False, inplace=True)
    return stats_df


我们使用这个函数可以直接对训练集进行初步统计分析

# 字段基本统计
stats_df=statics(train)
stats_df


74.png


标签分布如下

sns.countplot(train.label)
plt.xlabel('train label count')


75.png


初步特征工程


  • 情感特征

amount_feas = ['prob_0', 'prob_1', 'prob_2', 'prob_3','prob_4' ]
category_fea = ['id']
for f in tqdm(amount_feas, desc="amount_feas 基本聚合特征"):
    for cate in category_fea:
        if f != cate:
            rec_item_sentiment['{}_{}_medi'.format('senti', f)] = rec_item_sentiment.groupby(cate)[f].transform('median')
            rec_item_sentiment['{}_{}_mean'.format('senti', f)] = rec_item_sentiment.groupby(cate)[f].transform('mean')
            rec_item_sentiment['{}_{}_max'.format('senti', f)] = rec_item_sentiment.groupby(cate)[f].transform('max')
            rec_item_sentiment['{}_{}_min'.format('senti', f)] = rec_item_sentiment.groupby(cate)[f].transform('min')
            rec_item_sentiment['{}_{}_std'.format('senti', f)] = rec_item_sentiment.groupby(cate)[f].transform('std')


类别特征count特征

# count特征
for col in tqdm(sparse_features):
    data[col + '_count'] = data.groupby(col)['sampleId'].transform('count')
    dense_features.append(col + '_count')


用户特征:

# count特征
for col in tqdm(['pvId','itemId' ]):
    data[f'group_suv_{col}_nunique'] = \
        data[['suv', col]].groupby('suv')[col].transform('nunique')
    dense_features.append(f'group_suv_{col}_nunique')


物料特征

# pvId nunique特征
select_cols = ['suv', 'itemId']
for col in tqdm(select_cols):
    data[f'group_pvId_{col}_nunique'] = \
        data[['pvId', col]].groupby('pvId')[col].transform('nunique')
    dense_features.append(f'group_pvId_{col}_nunique')      
# itemId nunique特征
select_cols = ['pvId', 'suv', 'operator', 'browserType', 
             'deviceType', 'osType', 'province', 'city']
for col in tqdm(select_cols):
    data[f'group_itemId_{col}_nunique'] = \
        data[['itemId', col]].groupby('itemId')[col].transform('nunique')
    dense_features.append(f'group_itemId_{col}_nunique')


NN模型-DeepFM



基于deepctr实现DeepFM训练

train_model_input = {name: train[name] for name in feature_names}
valid_model_input = {name: valid[name] for name in feature_names}
test_model_input = {name: test[name] for name in feature_names}
model = DeepFM(linear_feature_columns, dnn_feature_columns, task='binary')
model.compile("adam", "binary_crossentropy", metrics=['binary_crossentropy', 'accuracy'])
history = model.fit(train_model_input, train[target].values,
                    batch_size=1024, epochs=3, verbose=1, 
                    validation_data=(valid_model_input, valid[target].values))
pred_ans = model.predict(valid_model_input, batch_size=1024)
print("valid AUC", round(roc_auc_score(valid[target].values, pred_ans), 4))
pred_ans = model.predict(test_model_input, batch_size=1024)


树模型-Catboost


train_model = CatBoostClassifier(iterations=15000, depth=5, learning_rate=0.05, loss_function='Logloss',
                                     logging_level='Verbose', eval_metric='AUC', task_type="GPU", devices='0:1')
train_model.fit(train_dataset, eval_set=eval_dataset, early_stopping_rounds=30, verbose=40)


特征工程思路扩展


  • 高阶特征:类别特征组合、高阶聚合特征,比例特征
  • 点击序列统计特征:当前用户|全局: item 众数当做类别特征;统计量 count或者nunique
  • 序列 Embedding特征:word2vec,tfidf(词袋)+SVD、graph embedding(deepwalk)
  • 点击转化率特征:itemid、pvId,类别组合 ..(提分) Kfold
相关文章
|
3月前
|
算法
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
|
10天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
51 8
|
10天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
41 7
|
1月前
|
搜索推荐 Shell
解析排序算法:十大排序方法的工作原理与性能比较
解析排序算法:十大排序方法的工作原理与性能比较
51 9
|
1月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
23 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
1月前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
31 0
|
1月前
|
存储 算法 搜索推荐
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
72 0
|
3月前
|
搜索推荐 算法 Java
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
该博客文章通过UML类图和Java源码示例,展示了如何使用适配器模式将QuickSort类和BinarySearch类的排序和查找功能适配到DataOperation接口中,实现算法的解耦和复用。
40 1
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
|
3月前
|
算法 搜索推荐 Java
算法实战:手写归并排序,让复杂排序变简单!
归并排序是一种基于“分治法”的经典算法,通过递归分割和合并数组,实现O(n log n)的高效排序。本文将通过Java手写代码,详细讲解归并排序的原理及实现,帮助你快速掌握这一实用算法。
41 0
|
3月前
|
算法 关系型数据库 MySQL
揭秘MySQL中的版本号排序:这个超级算法将颠覆你的排序世界!
【8月更文挑战第8天】在软件开发与数据管理中,正确排序版本号对软件更新及数据分析至关重要。因MySQL默认按字符串排序版本号,可能出现'1.20.0'在'1.10.0'之前的不合理情况。解决办法是将版本号各部分转换为整数后排序。例如,使用`SUBSTRING_INDEX`和`CAST`函数从`software`表的`version`字段提取并转换版本号,再按这些整数排序。这种方法可确保版本号按逻辑正确排序,适用于'major.minor.patch'格式的版本号。对于更复杂格式,需调整处理逻辑。掌握此技巧可有效应对版本号排序需求。
189 3