推荐评论展示(基于预训练Bert的文本分类)

推荐评论展示指的是从众多用户评论中选出一个作为店铺的推荐理由,以希望更多的人点开这个店铺。

这看着像是推荐系统,因为要把合适的评论推荐给用户看嘛,比如用户A对环境要求高,如果推荐理由是“环境好”的话,A就会点进去,而用户B更加关注好不好吃,只要好吃,环境好不好无所谓,那么推荐理由是“某某食品好吃到爆”的话,B更有可能点进去。也就是说,同样一家店铺,不同人看到的推荐理由是不一样的,不知道现在的美团、大众点评等有木有这么做~

言归正传,本次任务是一个典型的短文本(最长20个字)二分类问题,用预训练的bert做。另外推荐一个论文How to Fine-Tune BERT for Text Classification?这篇论文中关于Bert做文本分类(长文本或者短文本)部分的方法,都是一些非常直观的想法,我曾在一个长文本分类的任务中用过,亲测有效~

目录

 

一、题目描述

1.1 背景描述

1.2 数据集

1.3 评测指标

二、解题思路

2.1 ML/DL的前提假设

2.2 主要思路

2.3 进一步的改进

三、动手实践

四、全部代码

一、题目描述

1.1 背景描述

本次推荐评论展示任务的目标是从真实的用户评论中,挖掘合适作为推荐理由的短句。点评软件展示的推荐理由具有长度限制,而真实用户评论语言通顺、信息完整。综合来说,两者都具有用户情感的正负向,但是展示推荐理由的内容相关性高于评论,需要较强的文本吸引力。一些真实的推荐理由如下图所示:

                                                    推荐理由

1.2 数据集

训练集:16000条,正负样本比约为1:2,示例如下

                                            

测试集:4189条,示例如下

                                            

数据集获取链接数据下载地址

1.3 评测指标

      AUC

二、解题思路

2.1 ML/DL的前提假设

不管是机器学习还是深度学习,都基于一个前提“训练集和测试集独立同分布”,只有满足这个前提,模型的表现才会好。这里简单的看一下文本的长度,如果训练集都是短文本,测试集是长文本的话,想来模型不会表现太好~

train['length'] = train['content'].apply(lambda row:len(row))
test['length'] = test['content'].apply(lambda row:len(row))

        

可以看出,就文本长度而言,训练集和测试集是同分布的,且label为0和label为1的长度差不太多,将文本长度作为特征对分类的作用不大。

2.2 主要思路

文本分类有很多种方法,有众多的机器学习方法、fasttext、textcnn、基于RNN的……,但在bert面前,这些方法就如小巫见大巫,且bert天生就适合做分类任务。既然是刷分题,那我就不客气了,bert走起~(此处必须感谢下实验室的V100,伯禹也是提供了GPU环境的,不过还是线下用的爽一点~)

既然bert天生就适合做分类任务,那就把用bert好了,官方做法是取[CLS]对应的hidden经过一个全连接层来得到分类结果。这里为了充分利用这个时间步的信息,把bert最后一层取出来,然后进行一些简单的操作,如下

                    Bert——>全局平均池化——>全局最大池化——>[CLS]与序列其他位置的注意力得分

Keras实现如下

from keras_bert import load_trained_model_from_checkpoint, Tokenizer
from keras_self_attention import SeqSelfAttention

def build_bert(nclass, selfloss, lr, is_train):
    """
    nclass:output层的节点数
    lr:学习率
    selfloss:损失函数
    is_train:是否微调bert
    """
    bert_model = load_trained_model_from_checkpoint(config_path, checkpoint_path, seq_len=None)

    for l in bert_model.layers:
        l.trainable = is_train

    x1_in = Input(shape=(None,))
    x2_in = Input(shape=(None,))

    x = bert_model([x1_in, x2_in])
    x = Lambda(lambda x: x[:, :])(x)

    avg_pool_3 = GlobalAveragePooling1D()(x)
    max_pool_3 = GlobalMaxPooling1D()(x)
    attention_3 = SeqSelfAttention(attention_activation='softmax')(x)
    attention_3 = Lambda(lambda x: x[:, 0])(attention_3)

    x = keras.layers.concatenate([avg_pool_3, max_pool_3, attention_3])
    p = Dense(nclass, activation='sigmoid')(x)

    model = Model([x1_in, x2_in], p)
    model.compile(loss=selfloss,
                  optimizer=Adam(lr),
                  metrics=['acc'])
    print(model.summary())
    return model

 

其实我也是尝试了一些复杂操作的(比如后面接一个CNN或者接一层GRU),也尝试了把最后三层的特征都取出来做一些操作,但效果没有提升,但也不错。

2.3 进一步的改进

训练集中正负样本比为1:2,虽然算不上样本不平衡,但也算不上平衡=-=一般损失函数是交叉熵,但交叉熵与AUC之间并不是严格单调的关系,交叉熵的下降并不一定能带来AUC的提升,最好的方法是直接优化AUC,但AUC难以计算。

在样本平衡的时候AUC、F1、准确率(accuary)可能是差不多的,但在不平衡的时候accuary是不可以用来做评价指标的,应该用F1或者AUC来做评价指标。仔细想想,AUC其实是与Precision和Recall有关的,F1也是和Precision和Recall有关的,那我们就直接来优化F1好了,但F1也不可导啊,有办法,推荐苏剑林大佬写的函数光滑化杂谈:不可导函数的可导逼近(看了这个要考考自己能不能写出多分类的F1哦!哈哈)

直接用f1_loss做损失函数。

def f1_loss(y_true, y_pred):
    # y_true:真实标签0或者1
    # y_pred:为正类的概率

    loss = 2 * tf.reduce_sum(y_true * y_pred) / tf.reduce_sum(y_true + y_pred) + K.epsilon()

    return -loss

(代码有点tf风格哈~实在是因为自己tf写的多一点,keras不常写)

 

三、动手实践

Step 1:batch=16,交叉熵损失函数,学习率1e-5,微调bert层,即

build_bert(1, 'binary_crossentropy', 1e-5, True)

(之前做过一次长文本分类,用的同款学习率~算是祖传参数了吧emm)

Step 2:加载Step1得到的模型,固定bert层,只微调全连接层,batch依旧为16,学习率取为1e-7,即

build_bert(1, f1_loss, 1e-7, False)

(之前做的是长文本的三分类,也是样本不平衡,也用了f1_loss微调,也有效果的提升~)

(不可以直接就用f1_loss做优化,会出问题的~如果想知道为啥出问题,可以评论区留言=-=)

需要注意的是,模型参数>>数据量(16000),所以理论上一定会产生过拟合的,故采用early stopping来防止过拟合。

 

 

四、全部代码

其实这里有点模型集成的意思,用了五折交叉验证,得到五个模型,五个模型对测试集的预测取均值得到最终的预测结果。如下图

          

(盗图~在大佬推荐评论展示大作业的图上修改的=-=,水印也懒得去了,不讲究了~)

(GPU上大概运行1小时,CPU也是可以跑的,可能得四五小时吧~)

import keras
from keras.utils import to_categorical
from keras.layers import *
from keras.callbacks import *
from keras.models import Model
import keras.backend as K
from keras.optimizers import Adam
import codecs
import gc
import numpy as np
import pandas as pd
import time
import os
from keras.utils.training_utils import multi_gpu_model
import tensorflow as tf
from keras.backend.tensorflow_backend import set_session
from sklearn.model_selection import KFold
from keras_bert import load_trained_model_from_checkpoint, Tokenizer
from keras_self_attention import SeqSelfAttention
from sklearn.metrics import roc_auc_score
# 线下0.9552568091358987 batch = 16 交叉熵 1e-5  线上 0.96668
# 线下0.9603767202619631 batch = 16 在上一步基础上用f1loss 不调bert层 1e-7 线上0.97010

class OurTokenizer(Tokenizer):
    def _tokenize(self, text):
        R = []
        for c in text:
            if c in self._token_dict:
                R.append(c)
            elif self._is_space(c):
                R.append('[unused1]')  # space类用未经训练的[unused1]表示
            else:
                R.append('[UNK]')  # 剩余的字符是[UNK]
        return R


def f1_loss(y_true, y_pred):
    # y_true:真实标签0或者1
    # y_pred:为正类的概率

    loss = 2 * tf.reduce_sum(y_true * y_pred) / tf.reduce_sum(y_true + y_pred) + K.epsilon()

    return -loss


def seq_padding(X, padding=0):
    L = [len(x) for x in X]
    ML = max(L)
    return np.array([
        np.concatenate([x, [padding] * (ML - len(x))]) if len(x) < ML else x for x in X
    ])


class data_generator:
    def __init__(self, data, batch_size=8, shuffle=True):
        self.data = data
        self.batch_size = batch_size
        self.shuffle = shuffle
        self.steps = len(self.data) // self.batch_size
        if len(self.data) % self.batch_size != 0:
            self.steps += 1

    def __len__(self):
        return self.steps

    def __iter__(self):
        while True:
            idxs = list(range(len(self.data)))

            if self.shuffle:
                np.random.shuffle(idxs)

            X1, X2, Y = [], [], []
            for i in idxs:
                d = self.data[i]
                text = d[0][:maxlen]
                # indices, segments = tokenizer.encode(first='unaffable', second='钢', max_len=10)
                x1, x2 = tokenizer.encode(first=text)
                y = np.float32(d[1])
                X1.append(x1)
                X2.append(x2)
                Y.append([y])
                if len(X1) == self.batch_size or i == idxs[-1]:
                    X1 = seq_padding(X1)
                    X2 = seq_padding(X2)
                    Y = seq_padding(Y)
                    # print('Y', Y)
                    yield [X1, X2], Y[:, 0]
                    [X1, X2, Y] = [], [], []


def build_bert(nclass, selfloss, lr, is_train):
    bert_model = load_trained_model_from_checkpoint(config_path, checkpoint_path, seq_len=None)

    for l in bert_model.layers:
        l.trainable = is_train

    x1_in = Input(shape=(None,))
    x2_in = Input(shape=(None,))

    x = bert_model([x1_in, x2_in])
    x = Lambda(lambda x: x[:, :])(x)

    avg_pool_3 = GlobalAveragePooling1D()(x)
    max_pool_3 = GlobalMaxPooling1D()(x)
    # 官方文档:https://www.cnpython.com/pypi/keras-self-attention
    # 源码 https://github.com/CyberZHG/keras-self-attention/blob/master/keras_self_attention/seq_self_attention.py
    attention_3 = SeqSelfAttention(attention_activation='softmax')(x)
    attention_3 = Lambda(lambda x: x[:, 0])(attention_3)

    x = keras.layers.concatenate([avg_pool_3, max_pool_3, attention_3], name="fc")
    p = Dense(nclass, activation='sigmoid')(x)

    model = Model([x1_in, x2_in], p)
    model.compile(loss=selfloss,
                  optimizer=Adam(lr),
                  metrics=['acc'])
    print(model.summary())
    return model


def run_cv(nfold, data, data_test):
    kf = KFold(n_splits=nfold, shuffle=True, random_state=2020).split(data)
    train_model_pred = np.zeros((len(data), 1))
    test_model_pred = np.zeros((len(data_test), 1))

    lr = 1e-7  # 1e-5
    # categorical_crossentropy (可选方案:'binary_crossentropy', f1_loss)
    selfloss = f1_loss
    is_train = False  # True False

    for i, (train_fold, test_fold) in enumerate(kf):

        print('***************%d-th****************' % i)
        t = time.time()

        X_train, X_valid, = data[train_fold, :], data[test_fold, :]

        model = build_bert(1, selfloss, lr, is_train)
        early_stopping = EarlyStopping(monitor='val_acc', patience=3)
        plateau = ReduceLROnPlateau(monitor="val_acc", verbose=1, mode='max', factor=0.5, patience=2)
        checkpoint = ModelCheckpoint('/home/comment_classify/expriments/' + str(i) + '_2.hdf5', monitor='val_acc',
                                     verbose=2, save_best_only=True, mode='max', save_weights_only=False)

        batch_size = 16
        train_D = data_generator(X_train, batch_size=batch_size, shuffle=True)
        valid_D = data_generator(X_valid, batch_size=batch_size, shuffle=False)
        test_D = data_generator(data_test, batch_size=batch_size, shuffle=False)

        model.load_weights('/home/comment_classify/expriments/' + str(i) + '.hdf5')

        model.fit_generator(
            train_D.__iter__(),
            steps_per_epoch=len(train_D),
            epochs=8,
            validation_data=valid_D.__iter__(),
            validation_steps=len(valid_D),
            callbacks=[early_stopping, plateau, checkpoint],
        )

        # return model
        train_model_pred[test_fold] = model.predict_generator(valid_D.__iter__(), steps=len(valid_D), verbose=1)
        test_model_pred += model.predict_generator(test_D.__iter__(), steps=len(test_D), verbose=1)

        del model
        gc.collect()
        K.clear_session()

        print('time:', time.time()-t)

    return train_model_pred, test_model_pred


if __name__ == '__main__':

    config = tf.ConfigProto()
    config.gpu_options.per_process_gpu_memory_fraction = 0.8  # 定量
    config.gpu_options.allow_growth = True  # 按需
    set_session(tf.Session(config=config))

    t = time.time()
    maxlen = 20  # 数据集中最大长度是19
    # chinese_L-12_H-768_A-12是谷歌官方预训练的中文bert,Chinese-Base版本
    config_path = '/home/chinese_L-12_H-768_A-12/bert_config.json'
    checkpoint_path = '/home/chinese_L-12_H-768_A-12/bert_model.ckpt'
    dict_path = '/home/chinese_L-12_H-768_A-12/vocab.txt'
    token_dict = {}
    with codecs.open(dict_path, 'r', 'utf8') as reader:
        for line in reader:
            token = line.strip()
            token_dict[token] = len(token_dict)

    tokenizer = OurTokenizer(token_dict)

    data_dir = '/home/comment_classify/'
    train_df = pd.read_csv(os.path.join(data_dir, 'train.csv'))
    test_df = pd.read_csv(os.path.join(data_dir, 'test.csv'))

    print(len(train_df), len(test_df))

    DATA_LIST = []
    for data_row in train_df.iloc[:].itertuples():
        DATA_LIST.append((data_row.content, data_row.label))
    DATA_LIST = np.array(DATA_LIST)

    DATA_LIST_TEST = []
    for data_row in test_df.iloc[:].itertuples():
        DATA_LIST_TEST.append((data_row.content, 0))
    DATA_LIST_TEST = np.array(DATA_LIST_TEST)

    n_cv = 5
    train_model_pred, test_model_pred = run_cv(n_cv, DATA_LIST, DATA_LIST_TEST)

    train_df['Prediction'] = train_model_pred
    test_df['Prediction'] = test_model_pred/n_cv

    train_df.to_csv(os.path.join(data_dir, 'train_submit2.csv'), index=False)

    test_df['ID'] = test_df.index
    test_df[['ID', 'Prediction']].to_csv(os.path.join(data_dir, 'submit2.csv'), index=False)

    auc = roc_auc_score(np.array(train_df['label']), np.array(train_df['Prediction']))
    print('auc', auc)

    print('time is ', time.time()-t)  # 2853s

 

以上就是本文的全部内容,若有不足之处,还请各位不吝指出。另外附上本次比赛官方要求交上去的文档推荐评论展示文档

 

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
好的,这里是一段基于BERT预训练模型进行微调的文本分类代码,使用PyTorch实现: ```python import torch import torch.nn as nn from transformers import BertTokenizer, BertForSequenceClassification # 加载预训练BERT模型和分词器 tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') model = BertForSequenceClassification.from_pretrained('bert-base-uncased') # 定义分类器,输出层为2个类别 classifier = nn.Linear(model.config.hidden_size, 2) # 将BERT模型和分类器拼接成一个完整的模型 model.classifier = classifier # 加载数据 train_dataset = ... val_dataset = ... # 定义训练器 optimizer = torch.optim.Adam(model.parameters(), lr=1e-5) criterion = nn.CrossEntropyLoss() # 开始训练 for epoch in range(num_epochs): # 训练 model.train() for inputs, labels in train_dataset: optimizer.zero_grad() outputs = model(inputs['input_ids'], attention_mask=inputs['attention_mask'], labels=labels) loss = criterion(outputs.logits, labels) loss.backward() optimizer.step() # 验证 model.eval() with torch.no_grad(): total_loss = 0.0 total_correct = 0 for inputs, labels in val_dataset: outputs = model(inputs['input_ids'], attention_mask=inputs['attention_mask'], labels=labels) total_loss += criterion(outputs.logits, labels).item() total_correct += (outputs.logits.argmax(-1) == labels).sum().item() val_loss = total_loss / len(val_dataset) val_acc = total_correct / len(val_dataset) print(f"Epoch {epoch}: Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.4f}") ``` 在这段代码中,我们首先加载了预训练BERT模型和分词器,然后定义了一个分类器,将其与BERT模型拼接在一起,得到一个完整的分类模型。接着加载了训练和验证数据,并定义了训练器。最后,进行了训练和验证,并输出了验证损失和准确率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值