使用预训练语言模型作帖子分类

发布时间 2023-12-07 09:21:17作者: idazhi

​ 预训练语言模型PLMs或PTMs应用广泛且效果良好。有的文章中把自然语言处理中的预训练语言模型的发展划分为4个时代:词入时代,上下文嵌入(Context Word Embedding)时代、预训练语言模型时代、改进型和领域定制型时代。

为什么需要预训练

​ 模型通常需要非常大的参数量,但并不是所有任务都有足够多的有标记的数据去训练这样复杂的模型,训练数据少可能导致模型出现过拟合,就是模型误以为少量数据特有的某些不重要的特征是关键的通用的特征。过拟合会导致模型在实际使用中表现不佳。

​ 可以先使用一些通用的数据对模型进行预训练,让模型学习一些这个领域通用的东西,然后使用较少量最终要解决的问题的数据做最终的训练。

​ 先在海量语料上对模型进行预训练,常用的语料有维基百科、新闻文章等,而且常常使用无监督学习的方法。因为很多自然语言处理任务的语料人工标注成本很高,但是有大量不带标注的语料可以用于无监督学习。

在海量语料上进行模型预训练的好处有:

(1)可以让模型学习到这个语言中的通用的知识。
(2)避免训练数据量过少造成的过拟合
(3)使用预训练参数是一种初始化模型参数的方法。

预训练模型的工作方式

​ 预训练模型有两个步骤,第一是使用海量的带有标记的通用数据训练模型,称为预训练:第二则是使用具体任务的数据,在预训练得到的模型结构和参数的基础上对模型做进一步的训练,这个过程中模型可能会学到一些新的参数,也可能会对预训阶段中的参数做一些修改,使模型更适应当前的任务,称为Fine-tuning。

​ 对于预训练模型来说,预训练阶段完成的任务被称为预训练任务,而 Fine-tuning 阶段完成的任务和实际要做的具体任务被称为下游任务(downstream tasks)。

​ Fine-tuning阶段往往会采用较小的学习率

ELMo 模型,ELMo 不仅是能提供 D 到词向量的词表的模型,而且是由 Embedding 层和个双向LSTM构成的语言模型。

​ 下游任务使用ELMo模型的时候,不仅会使用Embedding层的预训练参数,还会使用LSTM道型的结构和参数,这样当词序列通过 Embedding 层时得到词向量,再经过LSTM 模型则能够结合上下文信息。

ELMo

​ ELMo模型是一个双向的语言模型,而且ELMo模型通过双向LSTM模型输出包含上下文信息的词向量,可以根据语境自动调整具体词对应的向量。

特点

​ ELMo是来自语言模型的、结合上下文信息的词嵌入

GPT

​ GPT即Generative Pre-Training,意为生成式预训练

特点

​ GPT采用Transformer结构取代LSTM,有更高的效率。可以在更大的数据集进行更多训练。GPT 采用半监督学习的训练方案,即先进行无监督的预训练,然后在有标记的数据上进行有监督的Fine-tuningo

下游任务

​ 对于分类任务可以直接在 Transformer 结构后面添加线性层。推理任务可以在前提文本和楼断文本中间添加分隔符,再按顺序输入模型。文本相似度任务可以在要比较的两个文本中间添加分隔符,并按不同顺序输入模型。多选任务则把题目和多个回答分别用分隔符分开,分多次输入模型。

预训练过程

​ 预训练使用BooksCorpus语料,包含从互联网上获取的7000多本书籍数据,这些书涉及多个不同主题。这个数据集已经无法公开获取,但是可以通过代码自动下载和处理。预训练过程训练100个轮次。

GPT-2和GPT-3

​ GPT-2采用无监督学习,用于训练的数据集是 WebText,该数据集包含几百万网页数据。GPT-2的参数量也很大最大规模的GPT-2有超过15亿个参数。

​ GPT-3参数量高达1750亿个。

BERT

​ 使用 Transformer结的双向编码器表示

​ BERT模型与GPT模型一样都使用Transformer 结构,但BERT使用的是双向Transformer结构,可以同时结合上下文的信息,另外,GPT是Transformer Decoder模型,BERT则是Transformer Encoder。

Hugging Face Transformers

​ Hugging Face Transformers是 Hugging Face 开发的自然语言处理算法库,包含多种先进的使用Transformer 结构的自然语言处理模型,并提供预训练权重

​ Hugging Face Transformers 把它包含的模型分为自回归模型(autoregressive modelARmodel)、自编码模型(Autoencoding model)、Seq2seq 模型、多模态模型(Multimodalmodel)和 Retrieval-based model。

​ 自回归模型包括前面介绍的GPT和GPT-2模型等。自回归模型通过上文预测下一个单词,所以它是单向的语言模型。

​ 自编码模型包括前面介绍的BERT、ALBERT、ROBERTa等模型。自编码器模型的训练目标是忽略输入的噪声从而还原原始输入。自编码模型把输入的一个序列转化为另一个序列。

​ Seq2seq模型

​ 多模态模型指融合多种信息形式的模型, MMBT

使用Transformers

使用HuggingFaceTransformers解决自然语言处理问题主要分为以下几个步骤:预处理数据、定义模型、加载预训练模型、模型调优。模型调优有时可以省略。

下载预训练模型

调用 Tokenizer 或者模型的 from_pretrained 方法时可以自动下载模型。代码如下。采用from_pretrained 方法也可以通过指定本地路径从文件加载模型

from transformers import AutoTokenizer
tokenizer =AutoTokenizer.from_pretrained('bert-base-uncased')#bert-base-chinese

Tokenizer

Tokenizer用于把输入的句子分解为模型词表中的token。

result=tokenizer("并广泛动员社会各方面的力量")
print(result)

-----
{'input_ids': [101, 100, 1842, 100, 100, 100, 1924, 1763, 100, 1863, 1976, 1916, 1778, 100, 102], 
 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

原输入有13个字,但得到的 input ids有15个ID,这是因为tokenizer 默认自动添加了特殊符号。不同预训练模型的特殊字符的ID可能不同

可通过allspecialids 方法查看特殊字符ID。

print(tokenizer.all_special_ids)

----
[100, 102, 0, 101, 103]

通过allspecial tokens方法查看所有特殊字符

print(tokenizer.all_special_tokens)

----
['[UNK]', '[SEP]', '[PAD]', '[CLS]', '[MASK]']

BERT的使用

一般可以根据不同的任务选择具体的模型,如文本分类、下一句预测、文本序列标注等都有对应的类,它们都继承于同一个基类,并可以使用相同的预训练参数,但是输入和输出由于#务不同而有所不同。

Hugging Face Transformers 提供针对不同任务的多种模型使构建模型解决具体问题变得很带单。很多情况下甚至无须自定义模型类,而可以直接使用 Hugging Face Transformers 提供的类创建对象。

BertForMaskedLM

BertForMaskedLM 是 BERT 的预训练任务之一,实现了 Masked Language Model.

from transformers import BertTokenizer, BertForMaskedLM
import torch
tokenizer =BertTokenizer.from_pretrained('bert-base-uncased')#bert-base-chinese
model =BertForMaskedLM.from_pretrained('bert-base-uncased')#bert-base-chinese
inputs=tokenizer(["并广泛动员社会[MASK]方面的力量"],return_tensors="pt")
labels= tokenizer(["并广泛动员社会各方面的力量"],return_tensors="pt")["input_ids"]

outputs=model(**inputs,labels=labels)
loss = outputs.loss
logits =outputs.logits
print(loss,logits.shape)
print(inputs['input_ids'])
print(labels)


-----
tensor(3.4853, grad_fn=<NllLossBackward0>) torch.Size([1, 15, 30522])
tensor([[ 101,  100, 1842,  100,  100,  100, 1924, 1763,  103, 1863, 1976, 1916,1778,  100,  102]])
tensor([[ 101,  100, 1842,  100,  100,  100, 1924, 1763,  100, 1863, 1976, 1916,1778,  100,  102]])
BertForNextSentencePrediction

用于预测下一个句子的 BERT,预测下一个句子也是BERT的预训练任务之一。

from transformers import BertTokenizer, BertForNextSentencePrediction
import torch
tokenizer =BertTokenizer.from_pretrained('bert-base-uncased')#bert-base-chinese
model = BertForNextSentencePrediction.from_pretrained('bert-base-uncased')
prompt="在我的后园,可以看见墙外有两株树,"
next_sentence_1="一株是枣树,还有一株也是枣树"
next_sentence_2="一九二四年九月十五日"
encoding = tokenizer(prompt, next_sentence_1, return_tensors='pt')
outputs = model(**{k: v.unsqueeze(0) for k,v in encoding.items()}, labels=labels)
loss =outputs.loss
logits= outputs.logits
print(logits)

BertForTokenClassification

BertForTokenClassification 是用于标记序列中的元素的BERT模型,会给序列中每个元素输出一个标签。

from transformers import BertTokenizer, BertForTokenClassification
import torch
tokenizer =BertTokenizer.from_pretrained('bert-base-uncased')#bert-base-chinese
model = BertForTokenClassification.from_pretrained('bert-base-uncased')

inputs=tokenizer("一九二四年九月十五日",return_tensors="pt")
labels = torch.tensor([0,1,1,1,1,0,1,0,1,1,0,0]).unsqueeze(0)

outputs=model(**inputs,labels=labels)
loss = outputs.loss
logits =outputs.logits
print(logits)

-----
tensor([[[-0.4444,  0.2574],
         [ 0.6357,  0.1568],
         [ 0.5538, -0.1078],
         [ 0.6453, -0.0598],
         [ 0.6642,  0.0519],
         [ 0.7227, -0.0997],
         [ 0.6060, -0.0166],
         [ 0.7038,  0.1973],
         [ 0.6122,  0.0787],
         [ 0.6021,  0.2544],
         [ 0.6061, -0.1438],
         [ 0.1394, -0.7285]]], grad_fn=<ViewBackward0>)

BertForQuestionAnswering

BertForQuestionAnswering是用于完成问答问题的BERT模型

from transformers import BertTokenizer, BertForQuestionAnswering
import torch
tokenizer =BertTokenizer.from_pretrained('bert-base-uncased')#bert-base-chinese
model = BertForQuestionAnswering.from_pretrained('bert-base-uncased')
question,text = "在我的后园,可以看见墙外有两株树,一株是枣树,另一株是什么树?","也是枣树。"
inputs = tokenizer(question, text,return_tensors='pt')
outputs = model(**inputs)
loss = outputs.loss
start_scores = outputs.start_logits
end_scores = outputs.end_logits

其他开源中文预训练模型

目前 Hugging Face Transformers 只提供 BERT的中文版本,但该模型只有 base规模。

TAL-EduBERT

Albert


实践 使用 Hugging Face Transformers 中的 BERT 做帖子标题分类

# 定义两个列表分别存储两个板块的帖子数据
# academy_titles 考研考博 job_titles 招聘信息

# 定义两个list分别存放两个板块的帖子数据
academy_titles = []
job_titles = []
with open('E:/nlp/dataSet/academy_titles.txt', encoding='utf8') as f:
    for l in f:  # 按行读取文件
        academy_titles.append(l.strip( ))  # strip 方法用于去掉行尾空格
with open('E:/nlp/dataSet/job_titles.txt', encoding='utf8') as f:
    for l in f:  # 按行读取文件
        job_titles.append(l.strip())  # strip 方法用于去掉行尾空格
# 合并两个列表的label
data_list = []
for title in academy_titles:
    data_list.append([title, 0])

for title in job_titles:
    data_list.append([title, 1])
# 计算标题的最大长度
max_length = 0
for case in data_list:
    max_length = max(max_length, len(case[0])+2)
print(max_length)
# 切分训练集和评估集
from sklearn.model_selection import train_test_split
train_list, dev_list = train_test_split(data_list,test_size=0.3,random_state=15,shuffle=True)
# 导入包和设置参数
import os
import time
import random
import torch
import torch.nn.functional as F
from torch import nn
from tqdm import tqdm
import random

from transformers import get_linear_schedule_with_warmup, AdamW
from transformers import BertTokenizer, BertForSequenceClassification

if torch.cuda.is_available():
    device = torch.device("cuda")
else:
    device = torch.device("cpu")
max_train_epochs = 6
warmup_proportion = 0.05
gradient_accumulation_steps = 2
train_batch_size = 16
valid_batch_size = train_batch_size
test_batch_size = train_batch_size
data_workers= 2

learning_rate=1e-6
weight_decay=0.01
max_grad_norm=1.0
cur_time = time.strftime("%Y-%m-%d_%H:%M:%S") # 当前日期时间字符串
# 定义dataset和dataloader
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
class MyDataSet(torch.utils.data.Dataset):
    def __init__(self, examples):
        self.examples = examples

    def __len__(self):
        return len(self.examples)

    def __getitem__(self, index):
        example = self.examples[index]
        title = example[0]
        label = example[1]
        r = tokenizer.encode_plus(title, max_length=max_length, padding="max_length")
        return title, label, index#, r['token_type_ids'], label, index

def the_collate_fn(batch):
    r = tokenizer([b[0] for b in batch], padding=True)
    input_ids = torch.LongTensor(r['input_ids'])
    attention_mask = torch.LongTensor(r['attention_mask'])
    label = torch.LongTensor([b[1] for b in batch])
    indexs = [b[2] for b in batch]
    return input_ids, attention_mask, label, indexs #, token_type_ids

train_dataset = MyDataSet(train_list)
train_data_loader = torch.utils.data.DataLoader(
    train_dataset,
    batch_size=train_batch_size,
    shuffle = True,
    num_workers=data_workers,
    collate_fn=the_collate_fn,
)

dev_dataset = MyDataSet(dev_list)
dev_data_loader = torch.utils.data.DataLoader(
    dev_dataset,
    batch_size=train_batch_size,
    shuffle = False,
    num_workers=data_workers,
    collate_fn=the_collate_fn,
)
# 定义评估函数 
def get_score():
    y_true = []
    y_pred = []
    for step, batch in enumerate(tqdm(dev_data_loader)):
        model.eval()
        with torch.no_grad():
            input_ids, attention_mask = (b.to(device) for b in batch[:2])
        y_true += batch[2].numpy().tolist()
        logist = model(input_ids, attention_mask)[0]
        result = torch.argmax(logist, 1).cpu().numpy().tolist()
        y_pred += result
    correct = 0
    for i in range(len(y_true)):
        if y_true[i] == y_pred[i]:
            correct += 1
    accuracy = correct / len(y_pred)
    
    return accuracy
# 定义模型
# 可以直接使用BertForSequenceClassification而无需重新定义模型,并使用adamw优化器
model = BertForSequenceClassification.from_pretrained('bert-base-uncased')
model.to(device)

t_total = len(train_data_loader) // gradient_accumulation_steps * max_train_epochs + 1
num_warmup_steps = int(warmup_proportion * t_total)
print('warmup steps : %d' % num_warmup_steps)
no_decay = ['bias', 'LayerNorm.weight'] # no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
param_optimizer = list(model.named_parameters())
optimizer_grouped_parameters = [
    {'params':[p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],'weight_decay': weight_decay},
    {'params':[p for n, p in param_optimizer if any(nd in n for nd in no_decay)],'weight_decay': 0.0}
]
optimizer = AdamW(optimizer_grouped_parameters, lr=learning_rate, correct_bias=False)
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=num_warmup_steps, num_training_steps=t_total)

------
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-uncased and are newly initialized: ['classifier.bias', 'classifier.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
warmup steps : 46
def print_test(title):
    r = tokenizer([title])
    input_ids = torch.LongTensor(r['input_ids']).to(device)
    attention_mask = torch.LongTensor(r['attention_mask']).to(device)
    logist = model(input_ids, attention_mask)[0]
    result = torch.argmax(logist, 1).cpu().numpy().tolist()[0]
    result = ['考研考博', '招聘信息'][result]
    print(title, result)
def print_cases():
    print_test('考研心得')
    print_test('北大实验室博士')
    print_test('考外校博士')
    print_test('北大实验室招博士')
    print_test('工作or考研?')
    print_test('急求自然语言处理工程师')
    print_test('校招offer比较')
# 训练模型
import time
time.clock =time.perf_counter
for epoch in range(max_train_epochs):
    b_time = time.time()
    model.train()
    for step, batch in enumerate(tqdm(train_data_loader)):
        input_ids, attention_mask, label = (b.to(device) for b in batch[:-1])
        loss = model(input_ids, attention_mask, labels=label)
        loss = loss[0]
        loss.backward()
        if (step + 1) % gradient_accumulation_steps == 0:
            optimizer.step()
            scheduler.step() 
            optimizer.zero_grad()
    print('Epoch = %d Epoch Mean Loss %.4f Time %.2f min' % (epoch+1, loss.item(), (time.time() - b_time)/60))
    print(get_score())
    print_cases()