学习时间:2022.04.26~2022.04.30
文章目录
7. 基于PyTorch的BERT应用实践
本节着重于将BERT模型应用到具体的实践当中,因此未来有很多可以优化的地方,比如自己重写dataset和dataloader方法,这样对于pytorch应该能有更好地、更灵活的运用。
7.1 工具选取
使用PyTorch框架应用Bert,一般是去Hugging Face (一个社区,有很多人会把训练好了的模型放在上面,可以支持PyTorch和Tenserflow)上,把Bert模型下载下来,然后再应用。
在应用具体模型之前,需要先安装一下Hugging Face提供的transformers库,因为它上面的模型都是基于这个transformers库来编写的,即使下载了模型也要通过这个库来使用。(这个是必要的)
pip install transformers
此外,数据处理部分,如果不想自己重写dataset和dataloader方法的话,Hugging Face也提供了datasets库,可以通过
pip install datasets
来进行安装和使用。(这个是可选的)
这两个库的使用方法,可以直接去官网查看使用文档,我觉得还是相对来说比较详细了(见Transformers和Datasets)。
此外,自己这次使用的数据集也是来自Kaggle:Quora Insincere Questions Classification | Kaggle。
7.2 文本预处理
这一部分主要参考了Kaggle上大神的代码,然后汇总了一下装进一个函数里了。以下简单举例:
- 将一些表情符号、html网址、email ids、urls全都清除:
defclean_data(data):
punct_tag = re.compile(r'[^\w\s]')
data = punct_tag.sub(r'', data)
html_tag = re.compile(r'<.*?>')
data = html_tag.sub(r'', data)
url_clean = re.compile(r"https://\S+|www\.\S+")
data = url_clean.sub(r'', data)
emoji_clean = re.compile("["u"\U0001F600-\U0001F64F"# emoticonsu"\U0001F300-\U0001F5FF"# symbols & pictographsu"\U0001F680-\U0001F6FF"# transport & map symbolsu"\U0001F1E0-\U0001F1FF"# flags (iOS)u"\U00002702-\U000027B0"u"\U000024C2-\U0001F251""]+", flags=re.UNICODE)
data = emoji_clean.sub(r'', data)
url_clean = re.compile(r"https://\S+|www\.\S+")
data = url_clean.sub(r'', data)return data
- 清除所有格:
defstrip_possessives(text):
text = text.replace("'s",'')
text = text.replace('’s','')
text = text.replace("\'s",'')
text = text.replace("\’s",'')return text
- 将数字替换成##:
defclean_numbers(x):
x = re.sub("[0-9]{5,}",'#####', x)
x = re.sub("[0-9]{4}",'####', x)
x = re.sub("[0-9]{3}",'###', x)
x = re.sub("[0-9]{2}",'##', x)return x
……
最后通过一个函数全部调用:
deftexts_preprogress(df):# 应用之前所有的预处理步骤
df = df.apply(lambda x: clean_data(x))
df = df.apply(lambda x: expand_contractions(x))
df = df.apply(lambda x: replace_typical_misspell(x))
df = df.apply(lambda x: strip_possessives(x))
df = df.apply(lambda x: replace_multi_exclamation_mark(x))
df = df.apply(lambda x: clean_text(x))
df = df.apply(lambda x: change_stopwords(x))
df = df.apply(lambda x: clean_numbers(x))return df
7.3 使用BERT模型
整个使用分为5部分:数据输入及应用预处理、提取词向量、构建网络模型(嵌入BERT)、参数准备和模型训练。
个人本次使用的是
bert-base-uncased
模型。
7.3.1 数据输入及应用预处理
# 确定设备
device = torch.device("cuda"if torch.cuda.is_available()else"cpu")print(device)# 导入文件并进行文本预处理
train_df = pd.read_csv('train.csv', nrows=540)
train_df['question_text']= texts_preprogress(train_df['question_text'])
eval_df = pd.read_csv('train.csv', nrows=675)[540:675]
eval_df['question_text']= texts_preprogress(eval_df['question_text'])
7.3.2 提取词向量
⭐这里值得特别特别特别提一下的是:
tokenizer
函数中的
padding
参数。
padding=True
等价于
padding='max_length'
,只对于句子对任务起作用,会一样自动补全到batch中的最长长度;但是!!!对于单句的任务,两者并不等价!!!这也是我一开始设置了
padding=True
,然后指定
max_length=72
,最后输出的句子却依然长短不一,然后输不进网络的原因。
所以对于单句任务,一定要先指定
padding='max_length'
,然后再设置
max_length=
,这样才会真正补全。
# 导入分词器
tokenizer = AutoTokenizer.from_pretrained('D:/Py-project/models/huggingface/bert-base-uncased/')# 定义token函数deftokenize_function(examples):return tokenizer(examples['question_text'], padding='max_length', max_length=72, truncation=True)# padding='max_length'填充批处理中较短的序列以匹配最长的序列(太坑了!!!!);truncation=True将序列截断为模型接受的最大长度# 定义处理标签及id列的函数defbatch_label(df):
df = df.drop('qid', axis=1)
dataset = Dataset.from_pandas(df)for k in dataset.column_names:if k =='target':
dataset = dataset.rename_column(k,'labels')
inputs = dataset.map(tokenize_function, batched=True)# 分词并输出词向量;batched=True开启批次输入
inputs.set_format(type='torch')# 词向量转为tensor
inputs = inputs.remove_columns('question_text')# 删除文本列,因为模型不接受原始文本作为输入
dataloader = DataLoader(inputs, shuffle=True, batch_size=128)# 创建成DataLoaderreturn dataloader
train_dl = batch_label(train_df)
eval_dl = batch_label(eval_df)
7.3.3 网络建模
其实Bert也有直接提供一个训练好的bertclassification模型,但只能直接调用,不太好嵌入到网络,所以这里只用了bert模型,然后嵌入到了网络里面。
classmy_bert(nn.Module):def__init__(self):super(my_bert, self).__init__()# Bert模型需要嵌入到网络中
self.bert = BertModel.from_pretrained("D:/Py-project/models/huggingface/bert-base-uncased")# 将Bert模型的参数设置为可以更新for param in self.bert.parameters():
param.requires_grad =True
self.linear = torch.nn.Linear(768,2)
self.dropout = torch.nn.Dropout(0.5)defforward(self, x):
input_ids = x['input_ids'].to(device)
token_type_ids = x['token_type_ids'].to(device)
attention_mask = x['attention_mask'].to(device)
output = self.bert(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
output = output['pooler_output']# bert的输出有4个,这个任务仅需要1个
output = self.linear(output)
output = self.dropout(output)
output = torch.sigmoid(output)return output
7.3.4 参数准备
# 设置随机数种子,保证结果可复现
seed =42if device =='cuda':
torch.cuda.manual_seed(seed)else:
torch.manual_seed(seed)# 实例化模型
model = my_bert()
model.to(device)# 设置参数
lr =2e-5
epoch =2
show_step =1
optimizer = optim.AdamW(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
7.3.5 模型训练
这里只涉及到一个小问题:如果数据集太大了,基本都需要转成dataloader后再分批输入网络进行训练和验证。
for i inrange(epoch):
model.train()
losses =[]
accuracy =[]
start_time = time.time()for batch in tqdm(train_dl, desc=f'第{i+1}/{epoch}次迭代进度', ncols=100):
pred = model(batch)# 正向传播
label = batch['labels'].to(device)
loss = criterion(pred, label)# 计算损失函数# 存入准确率和loss
losses.append(loss.item())
pred_labels = torch.argmax(pred, dim=1)
acc = torch.sum(pred_labels == label).item()/len(pred_labels)
accuracy.append(acc)
optimizer.zero_grad()# 优化器的梯度清零
loss.backward()# 反向传播
optimizer.step()# 参数更新# 测试集评估if i % show_step ==0:# 控制输出间隔
model.eval()
ev_losses =[]
ev_acc =[]with torch.no_grad():for batch in eval_dl:
ev_pred = model(batch)
ev_label = batch['labels'].to(device)
ev_loss = criterion(ev_pred, ev_label)# 存入准确率和loss
ev_losses.append(ev_loss.item())
pred_labels = torch.argmax(ev_pred, dim=1)
acc = torch.sum(pred_labels == ev_label).item()/len(pred_labels)
ev_acc.append(acc)
elapsed_time = time.time()- start_time
print("\nEpoch: {}/{}: ".format(i+1, epoch),"Accuracy: {:.6f}; ".format(np.mean(accuracy)),"Val Accuracy: {:.6f}; ".format(np.mean(ev_acc)),"Loss: {:.6f}; ".format(np.mean(losses)),"Val Loss: {:.6f}; ".format(np.mean(ev_losses)),'Time: {:.2f}s'.format(elapsed_time))
以上就是本人的bert第一次尝试的大部分内容了,之后应该会去试试ALBERT和RoBERTa,然后也会尝试自己重写下datasets和dataloader。未来继续加油!
版权归原作者 新四石路打卤面 所有, 如有侵权,请联系我们删除。