赞
踩
(1)Baseline的选择
我选择的是Datawhale闫强“致Great Bert方案”进行的迭代优化,其中是使用加载预训练模型,进行微调。用到了差分学习率和AdamW 优化器。这些点这对于Bert的微调有巨大的作用。还有更多Bert微调技巧来自于论文《How to Fine-Tune BERT for Text Classification?》论文中提到的微调策略有
- 处理长文本 我们知道BERT 的最大序列长度为 512,BERT 应用于文本分类的第一个问题是如何处理长度大于 512 的文本。本文尝试了以下方式处理长文章。
- 截断法 文章的关键信息位于开头和结尾。 我们可以使用三种不同的截断文本方法来执行 BERT 微调。
- 只截断头部
- 只截断尾部
- 头尾截断
- 层级法 输入的文本首先被分成k = L/510个片段,喂入 BERT 以获得 k 个文本片段的表示向量。 每个分数的表示是最后一层的 [CLS] 标记的隐藏状态,然后我们使用均值池化、最大池化和自注意力来组合所有分数的表示。
- 不同层的特征 BERT 的每一层都捕获输入文本的不同特征。 改论文研究了来自不同层的特征的有效性,可以看到:最后一层表征效果最好;最后4层进行max-pooling效果最好
- 灾难性遗忘 Catastrophic forgetting (灾难性遗忘)通常是迁移学习中的常见诟病,这意味着在学习新知识的过程中预先训练的知识会被遗忘。 因此,本文还研究了 BERT 是否存在灾难性遗忘问题。 我们用不同的学习率对 BERT 进行了微调,发现需要较低的学习率,例如 2e-5,才能使 BERT 克服灾难性遗忘问题。 在 4e-4 的较大学习率下,训练集无法收敛。所以当预训练模型失效不能够收敛的时候多检查下超参数是否设置有问题。
- Layer-wise Decreasing Layer Rate 逐层降低学习率 下表 显示了不同基础学习率和衰减因子在 IMDb 数据集上的性能。 我们发现为下层分配较低的学习率对微调 BERT 是有效的,比较合适的设置是 ξ=0.95 和 lr=2.0e-5
(2)方案的改进
通过以下方案,将单模型Bert从0.79逐步提升到0.8246,最终通过模型融合得到最高得分0.8304
Bert的数据预处理,只做了所有字母转换为小写和词性还原,词性还原比如working还原为work,采用nltk.stem.WordNetLemmatizer.lemmatize(word)工具包。其他的预处理,则是适得其反,过多的预处理,会降低模型精度,因为通过查阅资料,Bert预训练的模型,在训练当初就是使用原生数据训练的,为了达到更好的fine tune微调效果,在使用预训练模型的数据尽量与训练模型时候的数据形式保持统一。
选择的是5折2epoch训练,3090的显卡,5W的数据,batch—size为10,bert_base都得训练7个小时左右,bert_large 10个小时左右,Roberta_large 14个小时左右。更别说后期通过数据增强和加入伪标签总训练数据近16W的时候,训练时间加倍。一训练就是一整天。所以选择5折2epoch是迫不得已,通过观察发现,训练完模型都没有过拟合,完全可以加大训练深度,设4epoch模型精度会更高。
Bert_ base模型最小,有444M,8G的显存的勉强能跑起来,模型精度在bert中最低,历史最高得分0.8162。bert_large模型规模中等,1G以上的,11G的显存都无法跑起来。模型精度中等,历史最高得分0.8171。Roberta_large规模比Bert_large还要大,需要的显存更大。模型精度最高历史最高得分0.8246,通过其他Baseline的启发,在bert后加一层Bi-LSTM能提高千分点。也有方案加一层Attention的,但是我们没有取得成功。
第一种方式,将原始训练集5W的数据全部增强,第二种方式也将原始5W数据进行了增强。在传统 深度学习模型TextCNN、FastText方案中,这两种增强并没有带来增益,但是在Bert中,两种数据增强方式,都带来了增益。
这两种方式我们尝试了三种组合。带来的增益幅度也是不一样的
(1)原始数据5W+第一种数据增强5W:提升0.05+
(2)原始数据5W+第二种数据增强5W:提升0.05+
(3)原始数据5W+第一种数据增强5W+第二种数据增强5W:提升0.1+
将测试集放入各个模型中预测,得到多个模型的预测结果,选择各个模型都投票的数据,给测试集做伪标签,即获得Top K 的高质量标签,再将这些伪标签和对应的文本数据加入训练集中重新从零训练模型。直接将线上提升0.1+个点,反复重复以上操作,还能得到更高准确率,直到不再提升。该方式在传统DL 方案中能提升0.3个点,绝对的大杀器。
注意图片中的第二个Model和第一个Model不是同一个,第二个Model,是重新训练出来的
注意本次任务中通过对比实验,Batch_size选择的是10 ,对应模型初始学习率2e-5,全连接层的初始学习率是1e-4。如果显存够大,可以加倍,但是学习率也需要加倍。实验过程中,batch-size为4时,模型精度更高,但是太费时,我们只能舍弃精度,加快训练。
文本的最大长度,通过实验对比,选择过100、200、300,发现300的时候模型精度最高
混合精度训练是在尽可能减少精度损失的情况下利用半精度浮点数加速训练。它使用FP16即半精度浮点数存储权重和梯度。在减少占用内存的同时起到了加速训练的效果。
Stacking 是将验证集和测试集同时放入多个模型中预测,各自得到预测矩阵,再将每个模型的预测矩阵水平合并得到新的矩阵,则将验证集合并的预测矩阵作为基模型的训练集,去训练基模型,基模型此任务中选择的LogisticRegression 。用测试集合并的预测矩阵放入基模型预测作为最终的预测值。
Voting 在分类任务中是常用的融合技巧,以下一张图,一目了然,每一列表示一个模型的预测结果,每行去计数,每行出现次数最高的数即是该行的最终值。
但是我们的最终融合方式并不是简单的等权融合,而是变形的加权融合。是把个模型的历史的高分结果,进行了按比例融合,选择了3个bert_base、4个roberta_large、3个bert_large、一个robert_base、1个机器学习LGB、1个textcnn、1个fasttext。如下图所示
(1)加载包
import pandas as pd from nltk.stem import WordNetLemmatizer from tqdm import tqdm import re import nltk tqdm.pandas() import re import os import pickle train = pd.read_csv('./data/train_data.csv',sep="\t") test = pd.read_csv('./data/test.csv', sep='\t') def preprocess_text(document): stemmer = WordNetLemmatizer() text = str(document) # 替换换行符 text = text.replace("\n", ' ') # 用单个空格替换多个空格 text = re.sub(r'\s+', ' ', text, flags=re.I) # 转换为小写 text = text.lower() # 词形还原 tokens = text.split() tokens = [stemmer.lemmatize(word) for word in tokens] preprocessed_text = ' '.join(tokens) return preprocessed_text train["title"] = train["title"].progress_apply(lambda x: preprocess_text(x)) train["abstract"] = train["abstract"].progress_apply(lambda x: preprocess_text(x)) test["title"] = test["title"].progress_apply(lambda x: preprocess_text(x)) test["abstract"] = test["abstract"].progress_apply(lambda x: preprocess_text(x))
(2)训练集预处理
train_text_list = [] for index, row in train.iterrows(): title = row["title"] abstract = row["abstract"] text = "[CLS] " + title + " [SEP] " + abstract + " [SEP]" train_text_list.append(text) train['text'] = train_text_list label_path = "data/pseudo_data/label_id2cate.pkl" #将标签进行转换 label_id2cate = dict(enumerate(train.categories.unique())) label_cate2id = {value: key for key, value in label_id2cate.items()} with open(label_path, 'wb') as f: pickle.dump(label_id2cate, f, pickle.HIGHEST_PROTOCOL) train['label'] = train['categories'].map(label_cate2id) train_data = pd.DataFrame(columns=['text','label']) train_data['text'] = train['text'] train_data['label'] = train['label'] train_data.to_csv('data/train_clean_data.csv', sep='\t',index=False)
(3)测试集预处理
test_text_list = []
for index, row in test.iterrows():
title = row["title"]
abstract = row["abstract"]
text = "[CLS] " + title + " [SEP] " + abstract + " [SEP]"
test_text_list.append(text)
test['text'] = test_text_list
pseudo_label = pd.read_csv("./submit_voting-8-9-2.csv")
test['label'] = pseudo_label['categories'].map(label_cate2id)
# 去除换行符
test_data = test.drop(['paperid', 'title', 'abstract'], axis=1, inplace=False)
test_data.to_csv('data/test_clean_data.csv', sep='\t', index=False)
bert_base、bert_large、roberta_large代码都是一样的,唯一的不同在于使用预训练模型和对应分词器的选择不同。在以下代码会注释说明
(1)加载包
# 导入transformers import transformers from transformers import AutoConfig,AutoModel,AutoTokenizer,logging from torch.utils.data import RandomSampler,Dataset, DataLoader from transformers import BertModel, BertTokenizer, BertConfig, AdamW, get_linear_schedule_with_warmup from transformers import RobertaTokenizer, RobertaModel # 导入torch import torch import torch.nn as nn import torch.nn.functional as F # 常用包 import re import os import numpy as np import pandas as pd from tqdm import tqdm import pickle import os import torch import torch.nn as nn from torch.utils import data from sklearn.utils import resample from sklearn.metrics import accuracy_score # 全局变量 os.environ["TOKENIZERS_PARALLELISM"] = "false" RANDOM_SEED = 42 np.random.seed(RANDOM_SEED) torch.manual_seed(RANDOM_SEED) device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
(2)模型结构
cache_dir = '/media/mgege007/winType/cache' class PaperClassifier(nn.Module): def __init__(self): n_classes = 39 super(PaperClassifier, self).__init__() # 如果是Bert_large,此处是 # PRE_TRAINED_MODEL_NAME = "bert-large-uncased" # self.robert = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME) # 如果是bert_base,此处是 PRE_TRAINED_MODEL_NAME = "bert-base-uncased" self.robert = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME) # 如果是roberta_large,此处是 # PRE_TRAINED_MODEL_NAME = 'roberta-large' # self.robert = RobertaModel.from_pretrained(PRE_TRAINED_MODEL_NAME) self.bilstm = nn.LSTM(input_size=self.robert.config.hidden_size, hidden_size=self.robert.config.hidden_size, batch_first=True, bidirectional=True) self.drop = nn.Dropout(p=0.3) self.out = nn.Linear(self.robert.config.hidden_size * 2, n_classes) def forward(self, input_ids, attention_mask): last_hidden_out, pooled_output = self.robert( # 只要了句子级表示? _:[10, 300, 768] [16, 768] input_ids=input_ids, attention_mask=attention_mask) # [16, 300]300是句子长度 last_hidden_out = self.drop(last_hidden_out) output_hidden, _ = self.bilstm(last_hidden_out) # [10, 300, 768] output = self.drop(output_hidden) # dropout output = output.mean(dim=1) return self.out(output)
(3)读取数据
def data_process():
train = pd.read_csv('data/train_clean_data.csv', sep='\t')
test = pd.read_csv('data/test_clean_data.csv', sep='\t')
label_path = "data/label_id2cate.pkl"# label编码所需要的字典
if os.path.exists(label_path):
with open(label_path, 'rb') as f:
label_id2cate = pickle.load(f)
return train, test, label_id2cate
(4)封装数据集
class PaperDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, item): """ item 为数据索引,迭代取第item条数据 """ text = str(self.texts[item]) label = self.labels[item] encoding = self.tokenizer.encode_plus( # 等价于tokenizer.tokenize() + tokenizer.convert_tokens_to_ids() text, add_special_tokens=True, max_length=self.max_len, truncation=True, return_token_type_ids=True, pad_to_max_length=True, return_attention_mask=True, return_tensors='pt', ) return { 'texts': text, 'input_ids': encoding['input_ids'].flatten(), 'attention_mask': encoding['attention_mask'].flatten(), 'labels': torch.tensor(label, dtype=torch.long) }
def create_data_loader(df, tokenizer, max_len, batch_size,sampler): ds = PaperDataset( # dataset texts=df['text'].values, labels=df['label'].values, tokenizer=tokenizer, max_len=max_len ) return DataLoader( ds, batch_size=batch_size, sampler = sampler, num_workers=4, # 多线程 pin_memory=True # 页锁定内存 ) def create_test_loader(df, tokenizer, max_len, batch_size): ds = PaperDataset( # dataset texts=df['text'].values, labels=df['label'].values, tokenizer=tokenizer, max_len=max_len ) return DataLoader( ds, batch_size=batch_size, num_workers=4,#多线程 pin_memory=True # 页锁定内存 )
(5)定义1个epoch训练
def train_epoch(model, data_loader, loss_fn, optimizer, device, scheduler): print("start training!") model = model.train() losses = [] pred_ls = [] label_ls = [] accumulate_step = 10 i = 0 for d in tqdm(data_loader): input_ids = d["input_ids"].to(device) attention_mask = d["attention_mask"].to(device) targets = d["labels"].to(device) outputs = model( input_ids=input_ids, attention_mask=attention_mask ) _, preds = torch.max(outputs, dim=1) loss = loss_fn(outputs, targets) losses.append(loss.item()) loss = loss/accumulate_step loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) if (i+1)%accumulate_step ==0: optimizer.step() scheduler.step() optimizer.zero_grad() i+=1 label_ls.extend(d["labels"]) pred_ls.extend(preds.tolist()) correct_predictions = accuracy_score(label_ls, pred_ls) return correct_predictions, np.mean(losses) # 验证 def eval_model(model, data_loader, loss_fn, device): model = model.eval() # 验证预测模式 losses = [] pred_ls = [] label_ls = [] with torch.no_grad(): for d in data_loader: input_ids = d["input_ids"].to(device) attention_mask = d["attention_mask"].to(device) targets = d["labels"].to(device) outputs = model( input_ids=input_ids, attention_mask=attention_mask ) _, preds = torch.max(outputs, dim=1) loss = loss_fn(outputs, targets) y_pred_arr = outputs.data.cpu().numpy() losses.append(loss.item()) pred_ls.extend(preds.tolist()) label_ls.extend(d["labels"]) correct_predictions = accuracy_score(label_ls, pred_ls) return correct_predictions, np.mean(losses)
(6)预测结果
def model_predictions(model, data_loader, device):
model = model.eval()
result = []
with torch.no_grad():
for d in data_loader:
input_ids = d["input_ids"].to(device)
attention_mask = d["attention_mask"].to(device)
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask
)
y_pred = outputs.data.cpu().numpy()
result.extend(y_pred)
return result
(7)K折划分数据
# K折数据划分 def load_data_kfold(dataset,BATCH_SIZE,MAX_LEN, k, n): print("Cross validation第{}折正在划分数据集".format(n+1)) l = len(dataset) print(l) shuffle_dataset = True random_seed = 42 # fixed random seed indices = list(range(l)) if shuffle_dataset: np.random.seed(random_seed) np.random.shuffle(indices) # shuffle # Collect indexes of samples for validation set. val_indices = indices[int(l / k) * n:int(l / k) * (n + 1)] train_indices = list(set(indices).difference(set(val_indices))) train_sampler = data.SubsetRandomSampler(train_indices) # build Sampler valid_sampler = data.SubsetRandomSampler(val_indices) tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') train_data_loader = create_data_loader(dataset, tokenizer, MAX_LEN, BATCH_SIZE,train_sampler) val_data_loader = create_data_loader(dataset, tokenizer, MAX_LEN, BATCH_SIZE,valid_sampler) print("划分完成") return train_data_loader, val_data_loader
(8)模型层间差分学习率
# 差分学习率的设置 def get_parameters(model, model_init_lr, multiplier, classifier_lr): parameters = [] lr = model_init_lr for layer in range(12, -1, -1): # 遍历模型的每一层,bert_large和robert_large参数选择24,bert_base选择12 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 # 每一层的学习率*0.95的衰减因子 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
注意:因为bert_large和robert_large是24层网络结构,参数选择24,bert_base是12层网络结构选择12
(9)训练过程
#训练模型 def train_start(EPOCHS,MAX_LEN,BATCH_SIZE,train, test_data_loader, label_id2cate): #模型定义 model = PaperClassifier() model = model.to(device) #普通学习率 k_fold = 5 predict_all = np.zeros([10000,39])#存储测试集的 预测结果 for n in range(k_fold): train_data_loader, val_data_loader = load_data_kfold(train, BATCH_SIZE,MAX_LEN, k_fold, n) #使用差分学习率 parameters = get_parameters(model, 2e-5, 0.95, 1e-4) optimizer = AdamW(parameters) total_steps = len(train_data_loader) * EPOCHS scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=0, num_training_steps=total_steps ) loss_fn = nn.CrossEntropyLoss().to(device) best_accuracy = 0 for epoch in range(EPOCHS): print(f'Epoch {epoch + 1}/{EPOCHS}') print('-' * 10) train_acc, train_loss = train_epoch( model, train_data_loader, loss_fn, optimizer, device, scheduler ) print(f'Train loss {train_loss} accuracy {train_acc}') val_acc, val_loss = eval_model( model, val_data_loader, loss_fn, device) val_x.extend(val_x_epoch) val_y.extend(val_y_epoch) print(f'Val loss {val_loss} accuracy {val_acc}') if val_acc > best_accuracy: torch.save(model.state_dict(), 'model/best_model_state_large_ensemble.bin') best_accuracy = val_acc #进行预测 y_pred = model_predictions(model, test_data_loader, device) predict_all += np.array(y_pred) # 预测矩阵取平均 predictions = predict_all/k_fold # 存储预测矩阵 np.save("submit_arr.npy", predictions) pred = np.argmax(predictions, axis=1) print("计算完成") model_name = "Bert_large_cross" sub = pd.read_csv('data/sample_submit.csv') # 生成提交文件 sub['categories'] = list(pred) sub['categories'] = sub['categories'].map(label_id2cate) sub.to_csv('submit/submit_{}.csv'.format(model_name), index=False)
(9)主函数
# 加载数据集 train, test, label_id2cate = data_process() EPOCHS = 2 MAX_LEN = 300 BATCH_SIZE = 10 # bert_base和bert_large,对应各自的分词器 # PRE_TRAINED_MODEL_NAME = "bert-large-uncased" PRE_TRAINED_MODEL_NAME = "bert-base-uncased" tokenizer = BertTokenizer.from_pretrained(PRE_TRAINED_MODEL_NAME) # roberta_large 选择以下分词器 # PRE_TRAINED_MODEL_NAME = 'roberta-large/' # tokenizer = RobertaTokenizer.from_pretrained(PRE_TRAINED_MODEL_NAME) # 封装测试集。不封装训练集,因为要先K折划分后再实现封装 test_data_loader = create_test_loader(test, tokenizer, MAX_LEN, BATCH_SIZE) # 开始训练 train_start(EPOCHS,MAX_LEN,BATCH_SIZE,train, test_data_loader, label_id2cate)
继续预训练自己的数据集,在任务内的训练方式,理论上是可以提升模型的精度,但是我们尝试过训练。设置60epoch,训练5W的数据,训练了20多个小时,只有0.70+的精度,并没有带来提升,没有时间进一步分析就放弃了该方案,费时不讨好。
import warnings import pandas as pd from transformers import (AutoModelForMaskedLM, AutoTokenizer, LineByLineTextDataset, DataCollatorForLanguageModeling, Trainer, TrainingArguments) warnings.filterwarnings('ignore') train_data = pd.read_csv('data/train/train.csv', sep='\t') test_data = pd.read_csv('data/test/test.csv', sep='\t') train_data['text'] = train_data['title'] + '.' + train_data['abstract'] test_data['text'] = test_data['title'] + '.' + test_data['abstract'] data = pd.concat([train_data, test_data]) data['text'] = data['text'].apply(lambda x: x.replace('\n', '')) text = '\n'.join(data.text.tolist()) with open('text.txt', 'w') as f: f.write(text) model_name = 'roberta-base' model = AutoModelForMaskedLM.from_pretrained(model_name) tokenizer = AutoTokenizer.from_pretrained(model_name) tokenizer.save_pretrained('./paper_roberta_base') train_dataset = LineByLineTextDataset( tokenizer=tokenizer, file_path="text.txt", # mention train text file here block_size=256) valid_dataset = LineByLineTextDataset( tokenizer=tokenizer, file_path="text.txt", # mention valid text file here block_size=256) data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.15) training_args = TrainingArguments( output_dir="./paper_roberta_base_chk", # select model path for checkpoint overwrite_output_dir=True, num_train_epochs=5,# 轮数不要设置的太大 per_device_train_batch_size=16, per_device_eval_batch_size=16, gradient_accumulation_steps=2, evaluation_strategy='steps', save_total_limit=2, eval_steps=200, metric_for_best_model='eval_loss', greater_is_better=False, load_best_model_at_end=True, prediction_loss_only=True, report_to="none") trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=valid_dataset) trainer.train() trainer.save_model(f'./paper_roberta_base')
通过梯度缓存和累积的方式,用时间来换取模型精度,最终训练效果等效于更大的batch size。因此,只要你跑得起batch_size=1,只要你愿意花n倍的时间,就可以跑出n倍的batch size。
def train_epoch(model, data_loader, loss_fn, optimizer, device, scheduler, n_examples): print("start training!") model = model.train() losses = [] correct_predictions = 0 step = 0 pred_ls = [] label_ls = [] # 累计次数 accumulation_steps = 4 i= 0 for d in tqdm(data_loader): input_ids = d["input_ids"].to(device) attention_mask = d["attention_mask"].to(device) targets = d["labels"].to(device) outputs = model( input_ids=input_ids, attention_mask=attention_mask ) _, preds = torch.max(outputs, dim=1) loss = loss_fn(outputs, targets) loss = loss / accumulation_steps # 梯度累积 losses.append(loss.item()) loss.backward() if (i+1) % accumulation_steps == 0: # Wait for several backward steps optimizer.step() # Now we can do an optimizer step nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) scheduler.step() model.zero_grad() # Reset gradients tensors label_ls.extend(d["labels"]) pred_ls.extend(preds.tolist()) i+=1 correct_predictions = accuracy_score(label_ls, pred_ls) return correct_predictions, np.mean(losses)
BERT推出这一年来,除了XLNet,其他的改进都没带来太多惊喜,无非是越堆越大的模型和数据,以及动辄1024块TPU,让工程师们不知道如何落地。今天要介绍的ELECTRA是我在ICLR盲审中淘到的宝贝(9月25日已截稿),也是BERT推出以来我见过最赞的改进,通过类似GAN的结构和新的预训练任务,在更少的参数量和数据下,不仅吊打BERT,而且仅用1/4的算力就达到了当时SOTA模型RoBERTa的效果。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。