Datawhale--AI夏令营学习笔记(二)--NLP方向

在上一个笔记中,使用的是文本特征提取方法+传统机器学习算法进行文本分类任务,现在我将使用预训练模型Bert来处理该任务。

1.导入前置依赖

#导入前置依赖
import os
import pandas as pd
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
# 用于加载bert模型的分词器
from transformers import AutoTokenizer
# 用于加载bert模型
from transformers import BertModel
from pathlib import Path

2.设置全局配置

# 设置全局配置
batch_size = 16
# 文本的最大长度
text_max_length = 128
# 总训练的epochs数
epochs = 100
# 学习率
lr = 3e-5
# 取多少训练集的数据作为验证集
validation_ratio = 0.1
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 每多少步,打印一次loss
log_per_step = 50

# # 数据集所在位置
dataset_dir = Path("dataset/train.csv")
os.makedirs(dataset_dir) if not os.path.exists(dataset_dir) else ''

# # 模型存储路径
model_dir = Path("save_model/bert_checkpoints")
# 如果模型目录不存在,则创建一个
os.makedirs(model_dir) if not os.path.exists(model_dir) else ''

3.数据预处理

pd_train_data = pd.read_csv('dataset/train.csv')
pd_train_data['title'] = pd_train_data['title'].fillna('')
pd_train_data['abstract'] = pd_train_data['abstract'].fillna('')

test_data = pd.read_csv('dataset/test.csv')
test_data['title'] = test_data['title'].fillna('')
test_data['abstract'] = test_data['abstract'].fillna('')
pd_train_data['text'] = pd_train_data['title'].fillna('') + ' ' + pd_train_data['author'].fillna('') + ' ' + pd_train_data['abstract'].fillna('') + ' ' + pd_train_data['Keywords'].fillna('')
test_data['text'] = test_data['title'].fillna('') + ' ' + test_data['author'].fillna('') + ' ' + test_data['abstract'].fillna('') + ' ' + pd_train_data['Keywords'].fillna('')

# 从训练集中随机采样测试集
validation_data = pd_train_data.sample(frac=validation_ratio)
train_data = pd_train_data[~pd_train_data.index.isin(validation_data.index)]

4.构建训练所需的dataloader与dataset

# 构建Dataset
class MyDataset(Dataset):

    def __init__(self, mode='train'):
        super(MyDataset, self).__init__()
        self.mode = mode
        # 拿到对应的数据
        if mode == 'train':
            self.dataset = train_data
        elif mode == 'validation':
            self.dataset = validation_data
        elif mode == 'test':
            # 如果是测试模式,则返回内容和uuid。拿uuid做target主要是方便后面写入结果。
            self.dataset = test_data
        else:
            raise Exception("Unknown mode {}".format(mode))

    def __getitem__(self, index):
        # 取第index条
        data = self.dataset.iloc[index]
        # 取其内容
        text = data['text']
        # 根据状态返回内容
        if self.mode == 'test':
            # 如果是test,将uuid做为target
            label = data['uuid']
        else:
            label = data['label']
        # 返回内容和label
        return text, label

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

train_dataset = MyDataset('train')
validation_dataset = MyDataset('validation')
#获取Bert预训练模型
tokenizer = AutoTokenizer.from_pretrained("model/bert-base-uncased")  # 获取到模型的分词器

此处我是将模型下载到本地再进行调用,模型下载地址为https://huggingface.co/bert-base-uncased

#接着构造我们的Dataloader。
#我们需要定义一下collate_fn,在其中完成对句子进行编码、填充、组装batch等动作:
def collate_fn(batch):
    """
    将一个batch的文本句子转成tensor,并组成batch。
    :param batch: 一个batch的句子,例如: [('推文', target), ('推文', target), ...]
    :return: 处理后的结果,例如:
             src: {'input_ids': tensor([[ 101, ..., 102, 0, 0, ...], ...]), 'attention_mask': tensor([[1, ..., 1, 0, ...], ...])}
             target:[1, 1, 0, ...]
    """
    text, label = zip(*batch)
    text, label = list(text), list(label)

    # src是要送给bert的,所以不需要特殊处理,直接用tokenizer的结果即可
    # padding='max_length' 不够长度的进行填充
    # truncation=True 长度过长的进行裁剪
    src = tokenizer(text, padding='max_length', max_length=text_max_length, return_tensors='pt', truncation=True)

    return src, torch.LongTensor(label)


train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
validation_loader = DataLoader(validation_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)

5.定义预测模型

#定义预测模型,该模型由bert模型加上最后的预测层组成
class MyModel(nn.Module):

    def __init__(self):
        super(MyModel, self).__init__()

        # 加载bert模型
        self.bert = BertModel.from_pretrained('model/bert-base-uncased')

        # 最后的预测层
        self.predictor = nn.Sequential(
            nn.Linear(768, 256),
            nn.ReLU(),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )

    def forward(self, src):
        """
        :param src: 分词后的推文数据
        """

        # 将src直接序列解包传入bert,因为bert和tokenizer是一套的,所以可以这么做。
        # 得到encoder的输出,用最前面[CLS]的输出作为最终线性层的输入
        # outputs = self.bert(**src).last_hidden_state[:, 0, :]
        outputs = self.bert(**src)
        last_hidden_state = outputs[0]
        # 使用线性层来做最终的预测
        return self.predictor(last_hidden_state[:, 0, :])

model = MyModel()
model = model.to(device)

6.定义出损失函数和优化器

criteria = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
# 由于inputs是字典类型的,定义一个辅助函数帮助to(device)
def to_device(dict_tensors):
    result_tensors = {}
    for key, value in dict_tensors.items():
        result_tensors[key] = value.to(device)
    return result_tensors

7.定义验证方法

#定义一个验证方法,获取到验证集的精准率和loss
def validate():
    model.eval()
    total_loss = 0.
    total_correct = 0
    for inputs, targets in validation_loader:
        inputs, targets = to_device(inputs), targets.to(device)
        outputs = model(inputs)
        loss = criteria(outputs.view(-1), targets.float())
        total_loss += float(loss)

        correct_num = (((outputs >= 0.5).float() * 1).flatten() == targets).sum()
        total_correct += correct_num

    return total_correct / len(validation_dataset), total_loss / len(validation_dataset)

8.模型训练

# 首先将模型调成训练模式
model.train()

# 清空一下cuda缓存
if torch.cuda.is_available():
    torch.cuda.empty_cache()

# 定义几个变量,帮助打印loss
total_loss = 0.
# 记录步数
step = 0

# 记录在验证集上最好的准确率
best_accuracy = 0

# 开始训练
for epoch in range(epochs):
    model.train()
    for i, (inputs, targets) in enumerate(train_loader):
        # 从batch中拿到训练数据
        inputs, targets = to_device(inputs), targets.to(device)
        # 传入模型进行前向传递
        outputs = model(inputs)
        # 计算损失
        loss = criteria(outputs.view(-1), targets.float())
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

        total_loss += float(loss)
        step += 1

        if step % log_per_step == 0:
            print("Epoch {}/{}, Step: {}/{}, total loss:{:.4f}".format(epoch+1, epochs, i, len(train_loader), total_loss))
            total_loss = 0

        del inputs, targets

    # 一个epoch后,使用过验证集进行验证
    accuracy, validation_loss = validate()
    print("Epoch {}, accuracy: {:.4f}, validation loss: {:.4f}".format(epoch+1, accuracy, validation_loss))

    # 保存最好的模型
    if accuracy > best_accuracy:
        torch.save(model.state_dict(), model_dir / f"bert_best_model.pt")
        best_accuracy = accuracy

9.模型预测

#加载最好的模型,然后进行测试集的预测
model.load_state_dict(torch.load(model_dir / f"bert_best_model.pt"))
model = model.eval()

test_dataset = MyDataset('test')
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)

results = []
for inputs, ids in test_loader:
    outputs = model(inputs.to(device))
    outputs = (outputs >= 0.5).int().flatten().tolist()
    ids = ids.tolist()
    results = results + [(id, result) for result, id in zip(outputs, ids)]

10.结果保存

test_label = [pair[1] for pair in results]
test_data['label'] = test_label
test_data[['uuid', 'Keywords', 'label']].to_csv('result/submit_task.csv', index=None)

至此,整个代码结束,将结果提交到平台进行评测,得分为0.99301


然后我还想补充一下在学习笔记(一)中用到的CountVectorizer和没有用到的TfidfVectorizer

接下来我将用一段代码来演示一下这两种向量表示

from sklearn.feature_extraction.text import CountVectorizer

texts = ["dog cat fish", "dog cat cat", "fish bird", "bird"]
cv = CountVectorizer()  # 创建词袋数据结构
cv_fit = cv.fit_transform(texts)
# 上述代码等价于下面两行
# cv.fit(texts)
# cv_fit = cv.transform(texts)

print(cv.get_feature_names())  # ['bird', 'cat', 'dog', 'fish'],输出词袋中的所有特征词列表
print(cv.vocabulary_)  # {'dog': 2, 'cat': 1, 'fish': 3, 'bird': 0},输出每个特征词对应的索引,构成的词汇表
print(cv_fit)  # 用稀疏矩阵表示了每个文本中各个特征词的词频统计结果

第三个输出为:
(0, 2) 1
(0, 1) 1
(0, 3) 1
(1, 2) 1
(1, 1) 2
(2, 3) 1
(2, 0) 1
(3, 0) 1
数据分别表示:(句子索引,词在词典中对应的value) 该词在此句中出现的频次
注意: 词袋的索引顺序是按照特征词在词汇表中出现的先后顺序确定的,并且是固定的。

from sklearn.feature_extraction.text import TfidfVectorizer

tv = TfidfVectorizer()
tv_fit = tv.fit_transform(texts)
print(tv.get_feature_names())  # ['bird', 'cat', 'dog', 'fish']
print(tv.vocabulary_)  # {'dog': 2, 'cat': 1, 'fish': 3, 'bird': 0}
print(tv_fit)

第三个输出为:
(0, 3) 0.5773502691896257
(0, 1) 0.5773502691896257
(0, 2) 0.5773502691896257
(1, 1) 0.8944271909999159
(1, 2) 0.4472135954999579
(2, 0) 0.7071067811865475
(2, 3) 0.7071067811865475
(3, 0) 1.0
这与上面CountVectorizer结果中不同之处只有第三个输出中括号后的值
在CountVectorizer中,这个值是单词在句中出现的频次
而在TfidfVectorizer中,这个值代表了词频(TF)和逆文档频率(IDF)两个因素,
我以(0, 3) 0.5773502691896257为例,这是计算第一个文本中dog的tf-idf,计算公式如下:
T F − I D F ( d o g ) = T F ( d o g ) ∗ I D F ( d o g ) TF-IDF(dog)=TF(dog) * IDF(dog) TFIDF(dog)=TF(dog)IDF(dog)
= ( 1 / 3 ) ∗ lg ⁡ ( 4 / 2 ) =(1/3) * \lg (4/2) =(1/3)lg(4/2)
其中,TF(dog)表示"dog"在第0个文本样本中的词频(出现次数除以文本总词数),IDF(dog)表示"dog"的逆文档频率(文档总数除以包含"dog"的文档数的对数)。

你可能感兴趣的:(人工智能,自然语言处理,学习)