20大风控文本分类算法之6-基于BERT的文本分类实战

文摘   科学   2024-03-29 01:53   浙江  

大家好,我是小伍哥,很久没更新内容风控相关的内容了,今天在写一篇。文本数据的处理,对于一个风控策略或者算法,我觉得是必须要掌握的技能,有人说,我的风控并不涉及到文本?我觉得这片面了,在非内容风控领域,文本知识也是非常有用的。

用户昵称、地址啥的,这种绝大部分风控场景都能遇到,如果要做互联网风控,对文本的处理基本上是必不可少的技能。20大风控文本分类算法 系列,已经写了5篇,介绍了风控场景下文本分类的基本方法,对抗文本变异,包括传统的词袋模型、循环神经网络,也有常用于计算机视觉任务的卷积神经网络,以及 RNN + CNN,试验完一遍,基本能搞定大部分的文本分类以及文本变异对抗问题。

今天是第 6 讲,主要讲BERT进行文本特征的应用。使用BERT模型,绕不开HuggingFace提供的Transformers生态。HuggingFace提供了各类BERT的API(transformers库)、训练好的模型(HuggingFace Hub)还有数据集(datasets)。Transformers库提供了很多简单易用的接口,使开发者能够使用和训练各种预训练的NLP模型,例如BERT、GPT、RoBERTa等。这些模型在各种NLP任务上表现出色,包括文本分类、命名实体识别、情感分析等。

我们的目标是创建一个模型,该模型以句子为输入(就如上述数据集中的评论),输出为1(句子带有积极情感)或者0(句子带有消极情感)。如下图所示:

以下是HuggingFace目前提供的类列表

BertModel 
BertForPreTraining 
BertForMaskedLM 
BertForNextSentencePrediction(下句预测) 
BertForSequenceClassification(我们本次使用的分类模型) BertForTokenClassification 
BertForQuestionAnswering

一、模型安装

直接用pip就可以安装transformers了。
pip install transformers


二、模型探索

1、模型加载

下面两行代码会创建 BertTokenizer,并将所需的词表加载进来。首次使用这个模型时,transformers 会帮我们将模型从HuggingFace Hub下载到本地。
import torchimport randomfrom transformers     import BertTokenizer, BertForSequenceClassification, AdamWfrom torch.utils.data import DataLoader, Dataset, random_splitimport pandas as pdfrom tqdm import tqdm
加载BERT 和 分词器
# 加载BERT 和 分词器tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')model     = BertForSequenceClassification.from_pretrained('bert-base-chinese'


查看模型结构
# 获取字典的长度dictionary = tokenizer.get_vocab()len(dictionary)
# 查看模型结构print(model)
可以看到,词典大小是21128
打印分词器,查看分词器的基本信息,可以看到,词表大小21128,支持最大输入长度512,未知字符用[UNK]替代,等等信息。
print(tokenizer)BertTokenizer(name_or_path='bert-base-chinese', vocab_size=21128,               model_max_length=512, is_fast=False, padding_side='right',               truncation_side='right',               special_tokens={'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'},               clean_up_tokenization_spaces=True             )

2、句子分词

查看分词结果
# 切词结果token = tokenizer.tokenize('小伍哥真帅') print(token)['小', '伍', '哥', '真', '帅']

3、词转换成ID

Bert的切词器,附带Bert的字典,因为对词向量化需要先找到字典中对应的序号,才能找到它的词向量。
# 将词转换为对应字典的idindexes = tokenizer.convert_tokens_to_ids(token) print(indexes) # 输出id[2207, 824, 1520, 4696, 2358]
# 将id转换为对应字典的词tokens = tokenizer.convert_ids_to_tokens(indexes)print(tokens) # 输出词
['小', '伍', '哥', '真', '帅']
# 用tokenizer.encode编码,会自动加上[CLS]和[SEP]input_encoded = tokenizer.encode('小伍哥真帅')input_encoded[101, 2207, 824, 1520, 4696, 2358, 102]
#用tokenizer.decode接码还原tokenizer.decode(input_encoded)'[CLS] 小 伍 哥 真 帅 [SEP]'
也可以用直接用初始化的tokenizer进行分词,包含的信息更多。
input_encoded = tokenizer('小伍哥真帅')print(input_encoded){'input_ids': [101, 2207, 824, 1520, 4696, 2358, 102],  'token_type_ids': [0, 0, 0, 0, 0, 0, 0],  'attention_mask': [1, 1, 1, 1, 1, 1, 1]}
'input_ids':编码转换后的tokens
'token_type_ids':句子对编号,我们这里只有一个句子,所以是0
'attention_mask':句子是否进行了填充
1)input_ids
可以看到,我们输入一句话,得到的一个Python字典。其中,input_ids最容易理解,它表示的是句子中的每个Token在词表中的索引数字。词表(Vocabulary)是一个Token到索引数字的映射。
我们可以使用decode()方法,将索引数字转换为Token。
tokenizer.decode(input_encoded["input_ids"])'[CLS] 小 伍 哥 真 帅 [SEP]'
可以看到,BertTokenizer在还原文本是,自动给文本加上了[CLS][SEP]这两个符号,分别对应在词表中的索引数字为101和102。decode()之后,也将这两个符号反向解析出来了。
2)token_type_ids
token_type_ids主要用于句子对,比如下面的例子,两个句子通过[SEP]分割,0表示Token对应的input_ids属于第一个句子,1表示Token对应的input_ids属于第二个句子。token_type_ids:[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
input_encoded = tokenizer('小伍哥真帅','小伍哥正真有钱')print(input_encoded){'input_ids': [101, 2207, 824, 1520, 4696, 2358, 102, 2207, 824, 1520, 3633, 4696, 3300, 7178, 102],  'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],  'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
3)attention_mask
在一个分类任务重,句子不一样长,多个句子组成一个Batch训练时,需要保证每个句子都一样长,所以需要进行填充,但是填充的不进行训练,需要标记出来,attention_mask就起了至关重要的作用。比如下面的0就是表示填充的
tensor([[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
input_encoded = tokenizer(['小伍哥真帅','小伍哥正真的太有钱'], padding=True, return_tensors="pt")input_encoded{'input_ids':  tensor([[ 101, 2207,  824, 1520, 4696, 2358,  102,    0,    0,    0,    0],        [ 101, 2207,  824, 1520, 3633, 4696, 4638, 1922, 3300, 7178,  102]]),  'token_type_ids':  tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]),  'attention_mask':  tensor([[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
padding=True表示短句子的结尾会被填充[PAD]符号,return_tensors="pt"表示返回PyTorch格式的Tensor。attention_mask告诉模型,哪些Token需要被模型关注而加入到模型训练中,哪些Token是被填充进去的无意义的符号,模型无需关注。

4、提取流程

大的流程是把文本通过BERT,转换成句子向量,然后进行分类任务。
转换的过程:分词,词转换成数字ID,数字ID输入BERT模型
经过模型的训练和学习,提取最后一层的句子向量 [CLS]

5、词向量对比

我们可以看看BERT出来的词向量
import torchfrom transformers import BertTokenizer, BertModel
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')model = BertModel.from_pretrained("bert-base-chinese")
input_ids1 = torch.tensor(tokenizer.encode("大米粥")).unsqueeze(0) # Batch size 1input_ids2 = torch.tensor(tokenizer.encode("小米手机")).unsqueeze(0) # Batch size 1
outputs1 = model(input_ids1)outputs2 = model(input_ids2)print(outputs1[0][0][2]) # 大米粥中的 米print(outputs2[0][0][2]) # 小米手机中的 米
看的出来,同一个词,在不同的句子中词向量是不同的,因此bert能够很好的解决一次多义的现象,这便是它的魅力所在,word2vec词向量就解决不了这个问题。

五、BERT分类实战

1、数据读取

我做了两份数据测试,一个是weibo_senti_100k数据集,做情感分类的,准确率非常高,随便训练就0.98的准确率了。一个是之前的弹幕数据,准确率相对比较低。看了下原因,主要是很多乱码数据,无法编码导,还有就是黑灰产的文本存在反常表达,用常规预料无法学习,所以可以考虑单独训练或者用各种简单的模型。两份数据我都放网盘了,大家后台回复【BERT】领取,可以分别做实验下。
# 读取训练数据集#df = pd.read_csv("weibo_senti_100k.csv")df = pd.read_csv("text_all.csv")
# 随机打乱数据行df = df.sample(frac=1).reset_index(drop=True)

# 微博数据集 查看数据df.head() label review0 1 @?子的歌 的老爸吹瓶吹得好欢乐啊![笑哈哈]//@微美食厦门: #2013微美食厦门专属达...1 0 被shootme鄙视了【failed starting server. please che...2 0 本来不想评论1:5的,因为实在是已经习惯了!但今天中午吃面条时,边上一老大妈居然都在聊:“哎...3 0 俭姐肿么办//@果粒橙_cyx:美的你,非要上帅哥的车,把成绩都影响了[抓狂]4 0 请大家帮忙 //@莎墨:还是@小影客 发现告诉我们的[晕] //@双鱼座的KIKI:今天早上...
# 弹幕数据集 查看数据 label review0 1 2591 1478寂寞美女口..1 0 加速器免费,加速部分要收费2 0 开抽vx了,小姐姐ᚠ3 1 恭 CC-名字-看拼ᚰ4 1 __𝙝𝖈593ᚰ盘片5 0 刚才的双飞呢6 0 吼哦哦哦7 1 染口口 29521 917838 0 人家直接无视我9  0  小哥哥好嫩
2、数据预处理
为了快速训练展示,我们只加载了1500条数据,全量训练非常慢,我的mac电脑跑一晚上都出不来,这个模型参数太大,CPU干不动。
对文本进行编码,使用transformers对文本进行转换,这里使用的是bert-base-chinese模型,所以加载的Tokenizer也要对应。模型如果直接下载不了,可以离线加载,我都打包放网盘了。后台回复【BERT】领取
# 读取训练数据集 两个数据都可以测试下df = pd.read_csv("weibo_senti_100k.csv") df = pd.read_csv("text_all.csv")

# 加载模型和分词器tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')model = BertForSequenceClassification.from_pretrained('bert-base-chinese')

# 如果下不了 也可以离线加载模型# model_path = '/Users/wuzhengxiang/Documents/BERT/bert-base-chinese'# tokenizer = BertTokenizer.from_pretrained(model_path)# model = BertForSequenceClassification.from_pretrained(model_path)

104种语言的模型:bert-base-multilingual-cased,在弹幕数据集上,这个0.95左右准确率
中文训练的模型  :bert-base-chinese,在弹幕数据集上,这个0.932左右准确率
大家都可以测试下。bert-base-chinese可以看看分词效果
token = tokenizer.tokenize("佳喂:sx111505可越ne")token#['佳', '喂', ':', 's', '##x', '##11', '##150', '##5', '可', '越', 'ne']
数据处理,方便模型批量处理
# 设置随机种子以确保可重复性random.seed(42)# 随机打乱数据行df = df.sample(frac=1).reset_index(drop=True)
# 数据集中1为正样本或者黑样本,0为负样本或者白样本class SentimentDataset(Dataset): def __init__(self, dataframe, tokenizer, max_length=128): self.dataframe = dataframe self.tokenizer = tokenizer self.max_length = max_length
def __len__(self): return len(self.dataframe)
def __getitem__(self, idx): text = self.dataframe.iloc[idx]['review'] label = self.dataframe.iloc[idx]['label'] encoding = self.tokenizer(text, padding='max_length', truncation=True, max_length=self.max_length, return_tensors='pt') return { 'input_ids': encoding['input_ids'].flatten(), 'attention_mask': encoding['attention_mask'].flatten(), 'labels': torch.tensor(label, dtype=torch.long) }
# 创建数据集对象dataset = SentimentDataset(df[:1500], tokenizer)

3、训练集、验证集划分

# 创建数据集对象dataset = SentimentDataset(df[:1500], tokenizer)
# 划分训练集和验证集train_size = int(0.8 * len(dataset))val_size = len(dataset) - train_sizetrain_dataset, val_dataset = random_split(dataset, [train_size, val_size])
# 创建数据加载器,batch_size定义每次训练的数据量train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)val_loader = DataLoader(val_dataset, batch_size=8, shuffle=False)

4、参数设置

# 设置训练参数optimizer = AdamW(model.parameters(), lr=5e-5)device = torch.device("cuda" if torch.cuda.is_available() else "cpu")model.to(device)

5、模型训练

# 训练模型model.train()for epoch in range(3):  # 3个epoch作为示例    for batch in tqdm(train_loader, desc="Epoch {}".format(epoch + 1)):        input_ids = batch['input_ids'].to(device)        attention_mask = batch['attention_mask'].to(device)        labels = batch['labels'].to(device)        # 正向传播        optimizer.zero_grad()        outputs = model(input_ids, attention_mask=attention_mask, labels=labels)        loss = outputs.loss        # 反向梯度信息        loss.backward()        # 参数更新        optimizer.step()

6、模型评估

# 评估模型model.eval()total_eval_accuracy = 0for batch in tqdm(val_loader, desc="Evaluating"):    input_ids = batch['input_ids'].to(device)    attention_mask = batch['attention_mask'].to(device)    labels = batch['labels'].to(device)
with torch.no_grad(): outputs = model(input_ids, attention_mask=attention_mask) logits = outputs.logits preds = torch.argmax(logits, dim=1) accuracy = (preds == labels).float().mean() total_eval_accuracy += accuracy.item()
average_eval_accuracy = total_eval_accuracy / len(val_loader)print("Validation Accuracy:", average_eval_accuracy)

7、模型预测

# 使用微调后的模型进行预测def predict_sentiment(sentence):    inputs = tokenizer(sentence, padding='max_length', truncation=True, max_length=128, return_tensors='pt').to(device)    with torch.no_grad():        outputs = model(**inputs)    logits = outputs.logits    probs = torch.softmax(logits, dim=1)    positive_prob = probs[0][1].item()  # 1表示正面    negtived_prob = probs[0][0].item()  # 0表示负面    print("黑样本概率:", positive_prob,"白样本概率:", negtived_prob)
# 找几个案例测试下predict_sentiment("资源口扣12 5656 8844") 黑样本概率: 0.9945248365402222 白样本概率: 0.005475129000842571
# 买课加我微信 wuzhx2014predict_sentiment("买课加我微信 wuzhx2014")黑样本概率: 0.9529826641082764 白样本概率: 0.04701732099056244


往期精彩:

[课程]万物皆网络-风控中的网络挖掘方法

风控中的复杂网络-学习路径图

信用卡欺诈孤立森林实战案例分析,最佳参数选择、可视化等

风控策略的自动化生成-利用决策树分分钟生成上千条策略

SynchroTrap-基于松散行为相似度的欺诈账户检测算法


长按关注本号             长按加我进群
      

小伍哥聊风控
风控策略&算法,内容风控、复杂网络挖掘、图神经网络、异常检测、策略自动化、黑产挖掘、反欺诈、反作弊等
 最新文章