Kaggle入门比赛:灾难推文的NLP 详细教程
最近对NLP挺感兴趣,打算学习一下。在这里记录一下学习过程和中途遇到的一些坑!!
ps:下文中贴出的都是一些代码块,就个人经验而言自己手敲一边发现bug并debug的过程可以大大增强对数据结构和pytorch框架的认识。
代码放在个人github上:https://github.com/JYJ0327/Kaggle-conpetition
运行环境:用jupyter notebook运行
使用pytorch框架进行训练
文章目录
1.数据预处理
1)检查数据并进行空白填充
从Kaggle网站下载灾难推文数据集后,是一个csv文件,先检查一下数据,可以看到训练集数据共7613行,5列,分别是id,location,keyword,text与target,其中,部分样本存在keyword与location缺失,对缺失部分进行填充。
#数据处理均在notebook上进行
#读取数据集
pd.set_option('display.width', 1000)#
df_train = pd.read_csv('./df_train.csv')
#对null进行填充
for df in [df_train, df_test]:
for col in ['keyword', 'location']:
df[col] = df[col].fillna(f'no_{col}')
df_train
2)数据清理
数据清理主要是使用正则表达式对文本的符号的tokenizer无法识别的字符进行清理和替换。
ps:下面的代码只展示了如何处理,大家可以将其封装成函数对原本进行处理后保存为数据集中新的一列
# Urls
tweet = re.sub(r"https?:\/\/t.co\/[A-Za-z0-9]+", "", tweet)
# Words with punctuations and special characters
punctuations = '@#!?+&*[]-%.:/();$=><|{}^' + "'`"
for p in punctuations:
tweet = tweet.replace(p, f' {p} ')
# ... and ..
tweet = tweet.replace('...', ' ... ')
if '...' not in tweet:
tweet = tweet.replace('..', ' ... ')
# Acronyms
tweet = re.sub(r"MH370", "Malaysia Airlines Flight 370", tweet)
tweet = re.sub(r"m̼sica", "music", tweet)
tweet = re.sub(r"okwx", "Oklahoma City Weather", tweet)
tweet = re.sub(r"arwx", "Arkansas Weather", tweet)
tweet = re.sub(r"gawx", "Georgia Weather", tweet)
tweet = re.sub(r"scwx", "South Carolina Weather", tweet)
tweet = re.sub(r"cawx", "California Weather", tweet)
tweet = re.sub(r"tnwx", "Tennessee Weather", tweet)
tweet = re.sub(r"azwx", "Arizona Weather", tweet)
tweet = re.sub(r"alwx", "Alabama Weather", tweet)
tweet = re.sub(r"wordpressdotcom", "wordpress", tweet)
tweet = re.sub(r"usNWSgov", "United States National Weather Service", tweet)
tweet = re.sub(r"Suruc", "Sanliurfa", tweet)
# Grouping same words without embeddings
tweet = re.sub(r"Bestnaijamade", "bestnaijamade", tweet)
tweet = re.sub(r"SOUDELOR", "Soudelor", tweet)
3)分词
简单的数据清洗之后就可以使用分词器对文本进行向量化了,只有将词转换为向量才能送入神经网络进行学习。这里推荐使用transformers库,可以很方便的使用预训练的神经网络模型与对应的分词器。
#加载分词器
model_name = 'bert-base-uncased'
tokenizer = BertTokenizer.from_pretrained(pretrained_model_name_or_path=model_name)
sentences = df_train['text_cleaned'] + df_train['keyword']
sentences = sentences.tolist()
sentences_tokenizer = tokenizer(sentences,
truncation=True,
padding=True,
max_length=128,
add_special_tokens=True)
因为后续的训练在GPU上进行,所以这里直接将数据转化为tensor形式保存。(这里建议大家讲数据预处理和训练过程分开,将预处理完的数据保存为npy格式,这样可以避免后续训练搜索参数时重复进行数据预处理,浪费时间)
sentences_tokenizer['input_ids'] = np.array(sentences_tokenizer['input_ids'])
sentences_tokenizer['attention_mask'] = np.array(sentences_tokenizer['attention_mask'])
#del sentences_tokenizer['token_type_ids']
data = {}
data['input_ids'] = torch.tensor(sentences_tokenizer['input_ids'])
data['attention_mask'] = torch.tensor(sentences_tokenizer['attention_mask'])
data['labels'] = torch.tensor(df_train['target'])
#保存数据
np.save('sentences_tokenizer.npy', data)
2.训练过程
1)读取保存好的tensor类型数据
data_tensor = np.load('sentences_tokenizer.npy',allow_pickle=True)
for key in data_tensor.item():
data_tensor.item()[key] = data_tensor.item()[key].to(devices)
2)重构本次训练的Dataset类与Module类
class DisasterDataset(Dataset):
def __init__(self,data):
self.data = data.item()
def __getitem__(self, idx):
labels = self.data['labels'][idx]
sentences = {}
sentences['input_ids'] = self.data['input_ids'][idx]
sentences['attention_mask'] = self.data['attention_mask'][idx]
return sentences, labels
def __len__(self):
return len(self.data['input_ids'])
class BertClassificationModel(nn.Module):
def __init__(self,dropout,hidden_size=768):
super(BertClassificationModel, self).__init__()
model_name = 'bert-base-uncased'
# 读取预训练模型
self.bert = BertModel.from_pretrained(pretrained_model_name_or_path=model_name)
# self.bert = BertForSequenceClassification.from_pretrained(pretrained_model_name_or_path=model_name, num_labels=2)
# for p in self.bert.parameters(): # 冻结bert参数
# p.requires_grad = False
self.dropout = nn.Dropout(dropout, inplace=True)
self.fc = nn.Linear(hidden_size,2)
def forward(self, batch_sentences): # [batch_size,1]
input_ids = batch_sentences['input_ids']
attention_mask = batch_sentences['attention_mask']
bert_out=self.bert(input_ids=input_ids,attention_mask=attention_mask) # 模型
last_hidden_state =bert_out[0] # [batch_size, sequence_length, hidden_size] # 变量
bert_cls_hidden_state=last_hidden_state[:,0,:] # 变量
self.dropout(bert_cls_hidden_state)
fc_out=self.fc(bert_cls_hidden_state) # 模型
return fc_out
ps:这里我取消了冻结预训练bert的参数,大家实验时可以冻结观察实验结果的不同。
3)搭建训练过程
def main():
batchsize = 256 # 定义每次放多少个数据参加训练
#这里大家可以根据自己显卡情况调整
Datas = DisasterDataset(data_tensor) # 加载训练集
train_data, val_data = torch.utils.data.random_split(Datas,[6144,1469])
# train_loader = torch.utils.data.DataLoader(Datas, batch_size=batchsize, shuffle=False)#遍历train_dataloader 每次返回batch_size条数据
val_loader = torch.utils.data.DataLoader(val_data, batch_size=batchsize, shuffle=False)
#在调参数可以将训练集分为训练集和验证集,最后确定参数数需要将所有训练集都用于训练
# 这里搭建训练循环,输出训练结果
epoch_num = 2
dropout = 0.3
# 初始化模型
model=BertClassificationModel(dropout=dropout)
if isinstance(model,torch.nn.DataParallel):
model = model.module
model = nn.DataParallel(model).to(devices)#这里用单机多卡并行,如果只有单卡可以不用这个
optimizer = optim.AdamW(model.parameters(), lr=2e-5)
# scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=2,gamma=0.4 )
criterion = nn.CrossEntropyLoss()
criterion.to(devices)
print("模型数据已经加载完成,现在开始模型训练。")
for epoch in tqdm(range(epoch_num)):
model.train()
acc = 0
for i, (data,labels) in enumerate(train_loader, 0):
output = model(data)
optimizer.zero_grad() # 梯度清0
loss = criterion(output, labels) # 计算误差
out = output.argmax(dim=1)
acc += (out == labels).sum().item()
loss.backward() # 反向传播
optimizer.step() # 更新参数
scheduler.step()
# 打印一下每一次数据扔进去学习的进展
# print('batch:%d loss:%.5f' % (i, loss.item()))
# 打印一下每个epoch的深度学习的进展i
train_acc = acc / 6144
print('epoch:%d acc:%.5f' % (epoch, train_acc))
# torch.save(model.state_dict(),'model.pth')#最后一次训练时记得保存模型
num = 0
model.eval() # 不启用 BatchNormalization 和 Dropout,保证BN和dropout不发生变化,主要是在测试场景下使用;
for j, (data,labels) in enumerate(test_loader, 0):
output = model(data)
out = output.argmax(dim=1)
num += (out == labels).sum().item()
val_acc = num / 1469
print('val_Accuracy:', val_acc)
3.测试并提交
1)读取处理过的测试集
ps:测试集的处理与训练集相同
data_tensor = np.load('sentences_tokenizer_test.npy',allow_pickle=True)
for key in data_tensor.item():
data_tensor.item()[key] = data_tensor.item()[key].to(devices)
2)重构训练集Dataset类
ps:因为训练集数据没有label,所以需要重新构建,这里其实也可以通过一个判断和训练集构建成同一个,但因为一开始自己学的时候没想到0.0
class DisasterDataset(Dataset):
def __init__(self,data):
self.data = data.item()
def __getitem__(self, idx):
sentences = {}
sentences['input_ids'] = self.data['input_ids'][idx]
sentences['attention_mask'] = self.data['attention_mask'][idx]
return sentences
def __len__(self):
return len(self.data['input_ids'])
3)加载训练好的模型
model = BertClassificationModel(0.3)
model = nn.DataParallel(model).to(devices)
model.load_state_dict(torch.load('model.pth'))
ps:这里因为保存模型时保存的知识模型的参数,所以需要定义一个结构相同的模型实例后,再将参数传入
4)对测试集进行推理
batchsize = 128 # 定义每次放多少个数据参加训练
Datas = DisasterDataset(data_tensor) # 加载训练集
test_loader = torch.utils.data.DataLoader(Datas, batch_size=batchsize, shuffle=False)#遍历train_dataloader 每次返回batch_size条数据
model.eval() # 不启用 BatchNormalization 和 Dropout,保证BN和dropout不发生变化,主要是在测试场景下使用;
label = []
for j, data in enumerate(test_loader, 0):
#data = data.to(devices)
output = model(data)
out = output.argmax(dim=1)
out=out.tolist()
label.extend(out)
5)保存并提交结果
label = pd.DataFrame(label)
ids = pd.read_csv('test.csv')#这里因为测试集的id不是连续的!!!所以需要读取一下
ids = ids.iloc[:,1]
label = pd.concat([ids,label],axis=1)
fieldname = ['id','target']
label.to_csv("submission.csv",index_label = fieldname)