最近在改进一个研究语义进行二分类的分类器,原分类器是基于textcnn的,但是效果不是特别好,于是决定使用预训练bert模型进行微调(fine-tune),中间遇到了许多预料未及的问题,但是这些问题也让我学到了许多东西
目录
对数据集的处理
模型训练
主要代码和流程
训练过程
遇到的问题
经验和所得
原数据集是.csv文件,只有content和label两个标签内容,由于是一个二分类问题,label_number = 2,处理起来也比较轻松。
原分类器是基于.csv文件处理数据的,但是在观察数据的时候发现,数据集上出现了一些格式错误,通常在csv文件中经常遇到的一个格式问题就是换行,如果换行导致格式问题就会导致数据集缺失值,尽管可以中处理数据的时候将换行导致的label缺失值删除,但是也会导致一些不太好的数据出现,比如只保留了换行前的数据,换行后的数据删除了,样本数据集不够好了。
所以我改成了使用txt文件来处理数据,但是txt文件读取后转化成Dataframe对象有点绕,参考代码如下:
import pandas as pd
path = ""
labels = []
texts = []
data_dict = {}
with open(path) as file:
for line in file:
#txt文件中label和text用\t隔开
label = line.split("\t")[0]
text = line.split("\t")[1].strip()
labels.append(label)
texts.append(text)
data["label"] = labels
data["text"] = texts
data = pd.DataFrame(data_dict)
然后就可以对数据集dropna和shuffle了,然后写入新txt文件中用作训练集
import pandas as pd
data.dropna(inplace = True)
data_shuffle = shuffle(data)
new_path = ""
with open(new_path,"a+") as file_new:
for idx,label in enumerate(data_shuffle["label"]):
file_new.write(label + "\t" + data_shuffle["text"][idx] + "\n")
训练前需要提前下载好预训练模型,我这里使用的是chinese-roberta-wwm-ext,主要流程就是从训练集读取数据,然后加载预训练模型,将数据转化为tensor格式,设置好参数就可以开始训练了
class BertClass:
def __init__(self) -> None:
self.data_path =""
bert_path = "./chinese-roberta-wwm-ext"
self.tokenizer = AutoTokenizer.from_pretrained(bert_path)
self.model = AutoModelForSequenceClassification.from_pretrained(bert_path, num_labels=2)
self.device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
self.output_dir = "./bert_model/model"
self.logs_dir = "./bert_model/logs"
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
if not os.path.exists(self.logs_dir):
os.makedirs(self.logs_dir)
self.batch_size = 16
self.num_epochs = 10
self.metric = evaluate.load("accuracy")
def load_data(self):
count = 0
with open(self.data_path) as file:
texts = []
labels = []
for row in file:
count += 1
print(count)
label = row.split("\t")[0]
content = row.split("\t")[1]
texts.append(content)
labels.append(label)
x_train, x_dev, y_train, y_dev = train_test_split(texts, labels, test_size=0.2, random_state=0)
return x_train, x_dev, y_train, y_dev
def compute_metrics(self, eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return self.metric.compute(predictions=predictions, references=labels)
def train(self):
x_train, x_dev, y_train, y_dev = self.load_data()
print("x_train number: " + str(len(x_train)))
print("y_train number: " + str(len(y_train)))
print("x_dev number: " + str(len(x_dev)))
print("y_dev number: " + str(len(y_dev)))
train_encodings = self.tokenizer(x_train, truncation=True, padding=True, max_length=512)
val_encodings = self.tokenizer(x_dev, truncation=True, padding=True, max_length=512)
self.train_dataset = MyDataset(train_encodings, y_train)
self.valid_dataset = MyDataset(val_encodings, y_dev)
training_args = TrainingArguments(
output_dir=self.output_dir,
num_train_epochs=self.num_epochs,
per_device_train_batch_size=self.batch_size,
per_device_eval_batch_size=self.batch_size,
warmup_steps=500,
weight_decay=0.001,
logging_dir=self.logs_dir,
logging_steps=20,
evaluation_strategy="epoch",
save_strategy="epoch"
)
trainer = Trainer(
model=self.model,
args=training_args,
train_dataset=self.train_dataset,
eval_dataset=self.valid_dataset,
compute_metrics=self.compute_metrics
)
trainer.train()
其中batch_size = 16,epoch = 10。
warmup_steps = 500位预热步数,weight_decay=0.001为权重下降率。
验证策略和保存策略都是每个epoch一次
将数据转化为tensor代码如下:
class MyDataset(torch.utils.data.Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
if self.labels[idx] == "label_0":
label = 0
else:
label = 1
item['labels'] = torch.tensor(label)
return item
def __len__(self):
return len(self.labels)
执行代码后就开始了训练过程,训练过程中每次读取16个数据,一共训练数据集10次,每个epoch验证和保存一次模型
在训练过程中还保存了log文件,我们可以使用TensorBoard查看我们训练过程的日志文件
我们可以在终端或者命令行进入到log文件夹目录下,然后执行命令
tensorboard --logdir=logs --port=6006
6006是默认端口号,也可以改成其他可用端口
然后就可以进入http://localhost:6006查看日志文件了,界面大概是这样的
这样我们就可以知道在训练过程中出现了哪些问题
在训练的过程中并没有报错,但是遇到了一个始料未及的情况
在通宵跑完模型后第二天满怀期待的进行测试,但是在测试集上的f1分数却只有0.16,对一部分小数据进行单独测试发现预测得分基本都在0.5左右徘徊,看起来像是权重矩阵被初始随机化了,这让我十分纳闷,翻看日志发现在训练第一个epoch时并没有什么问题,在训练第二个epoch的时候损失函数直接从0.15悬崖式上升到0.7,并且之后一直稳定在0.7附近。
我首先对数据集的缺失值进行检查,并没有发现有缺失值的情况
然后初步怀疑是不是在第一个epoch保存模型后参数被初始化然后学习率出现了问题。
我将验证和保存策略改成了
evaluation_strategy="steps",
save_strategy="steps",
save_steps=3000,
eval_steps=3000
即每3000个batch验证并保存一次,训练过程中发现保存第一次后第二次继续训练并没有出现前面的问题,大概是在0.9个epoch的时候出现异常,然后排除该情况。
然后我怀疑是不是warmup预热的问题,我将warmup取消重新训练,还是并没有发现问题。
接下来我将数据集切割,原本的数据集大约为10w条数据,比例约为1:1,我将其切割成几千条数据,比例不变,重新训练,这次训练完10个epoch都很稳定。
然后我逐步增加数据集大小,1w,2w,3w,4w都没有问题,但是跑10w条数据太久了,于是我尝试更改预训练模型,发现更改预训练模型后还是同样在0.9个epoch附近出现异常,这让我怀疑极有可能是数据集的问题。
由于我验证过缺失值的情况,所以排除数据集格式的问题,那么就是内容的问题。
我花了大量的时间简要过滤了一遍所有数据,终于找到了问题所在。。。
由于之前用.csv文件保存数据时很多数据出现了格式问题,我就改成了txt文件格式,在更改的过程中处理了缺失值问题,但是保留了很大一部分不够完善的数据,导致出现了很多垃圾数据甚至无意义数据,当数量足够大的时候就会导致模型崩溃从而发生这种情况。
我将数据量缩减到了4w条并人工过滤了一遍垃圾数据,训练后模型正常了。
经过这次遇到的问题后我更加确信了数据的重要性,数据并不是越多越好,对于一个简单的二分类问题对于Bert来说,4w条数据绰绰有余了,甚至2w条就足够了,但是数据的质量一定要十分好,而且数据量比较小的情况下也可以方便我们人工过滤数据