zoukankan      html  css  js  c++  java
  • NLP(二十八):BertForSequenceClassification进行文本分类,基于transformers

    上一篇文章中,我详细讲解了 BertModel

    在今天这篇文章,我会使用 BertForSequenceClassification,在自己的训练集上训练情感分类模型。

    数据集来源于 https://github.com/bojone/bert4keras/tree/master/examples/datasets

    是一个中文的情感二分类数据集。

    而词汇表 vocab.txt 来自于哈工大的中文预训练语言模型 BERT-wwm, Chinese

    地址:https://github.com/ymcui/Chinese-BERT-wwm#%E4%B8%AD%E6%96%87%E6%A8%A1%E5%9E%8B%E4%B8%8B%E8%BD%BD

    以 PyTorch 版BERT-wwm, Chinese为例,下载完毕后对zip文件进行解压得到:

     
     
    1
    2
    3
    4
    chinese-bert_chinese_wwm_pytorch.zip
    |- chinese_wwm_pytorch.bin # 模型权重
    |- bert_config.json # 模型参数
    |- vocab.txt # 词表

    我们前面提到,BertForSequenceClassification 是在 BertModel 的基础上,添加了一个线性层 + 激活函数,用于分类。而 Huggingface 提供的预训练模型 bert-base-uncased 只包含 BertModel 的权重,不包括线性层 + 激活函数的权重。在下面,我们会使用model = BertForSequenceClassification.from_pretrained("bert-base-uncased", config=config)来加载模型,那么线性层 + 激活函数的权重就会随机初始化。我们的目的,就是通过微调,学习到线性层 + 激活函数的权重。

    我们这里预训练模型使用 Huggingface 的 bert-base-uncased,不使用哈工大模型的权重,因为我们是想要在 bert-base-uncased 的基础上进行微调。因此只使用其中的 vocab.txt

    我把数据、词汇表(vocab.txt)以及代码,放到了 github 上:https://github.com/zhangxiann/BertPractice

    下面开始讲解代码。

    导入库

     
     
    1
    2
    3
    4
    5
    6
    7
    8
    import torch.nn as nn
    from transformers import AdamW
    from torch.utils.data import Dataset
    import pandas as pd
    import torch
    from transformers import BertConfig, BertForSequenceClassification
    from transformers import BertTokenizer
    from torch.utils.data import DataLoader

    参数设置

     
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 超参数
    hidden_dropout_prob = 0.3
    num_labels = 2
    learning_rate = 1e-5
    weight_decay = 1e-2
    epochs = 2
    batch_size = 16
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    # 文件路径
    data_path = ".\\sentiment\\"
    vocab_file = data_path+"vocab.txt" # 词汇表
    train_data = data_path + "sentiment.train.data" # 训练数据集
    valid_data = data_path + "sentiment.valid.data" # 验证数据集

    定义 Dataset,加载数据

    在 Dataset 的 __getitem__() 函数里,根据 idx 分别找到 text 和 label,最后返回一个 dict。

    DataLoader 的 batch_size 设置为 16。

     
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class SentimentDataset(Dataset):
    def __init__(self, path_to_file):
    self.dataset = pd.read_csv(path_to_file, sep="\t", names=["text", "label"])
    def __len__(self):
    return len(self.dataset)
    def __getitem__(self, idx):
    # 根据 idx 分别找到 text 和 label
    text = self.dataset.loc[idx, "text"]
    label = self.dataset.loc[idx, "label"]
    sample = {"text": text, "label": label}
    # 返回一个 dict
    return sample

    # 加载训练集
    sentiment_train_set = SentimentDataset(data_path + "sentiment.train.data")
    sentiment_train_loader = DataLoader(sentiment_train_set, batch_size=batch_size, shuffle=True, num_workers=0)
    # 加载验证集
    sentiment_valid_set = SentimentDataset(data_path + "sentiment.valid.data")
    sentiment_valid_loader = DataLoader(sentiment_valid_set, batch_size=batch_size, shuffle=False, num_workers=0)

    定义 Tokenizer 和 Model

    这里定义了 BertConfig,使用了上面定义的一些超参数:如类别数量,hidden_dropout_prob 等。

    预训练模型选择 bert-base-uncased

     
     
    1
    2
    3
    4
    5
    6
    7
    8
    # 定义 tokenizer,传入词汇表
    tokenizer = BertTokenizer(data_path+vocab_file)


    # 加载模型
    config = BertConfig.from_pretrained("bert-base-uncased", num_labels=num_labels, hidden_dropout_prob=hidden_dropout_prob)
    model = BertForSequenceClassification.from_pretrained("bert-base-uncased", config=config)
    model.to(device)

    定义损失函数和优化器

    其中bias 和 LayerNorm 的权重不使用 weight_decay。这是根据 https://huggingface.co/transformers/training.html 来设置的,暂未查到这么做的原因。如果你知道原因,欢迎留言告诉我。

     
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 定义优化器和损失函数
    # Prepare optimizer and schedule (linear warmup and decay)
    # 设置 bias 和 LayerNorm.weight 不使用 weight_decay
    no_decay = ['bias', 'LayerNorm.weight']
    optimizer_grouped_parameters = [
    {'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], 'weight_decay': weight_decay},
    {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
    ]

    #optimizer = AdamW(model.parameters(), lr=learning_rate)
    optimizer = AdamW(optimizer_grouped_parameters, lr=learning_rate)
    criterion = nn.CrossEntropyLoss()

    定义训练和验证的函数

    首先从 dataloader获取到 text 和 label。

    然后通过

     
     
    1
    tokenized_text = tokenizer(text, max_length=100, add_special_tokens=True, truncation=True, padding=True, return_tensors="pt")

    获得 tokenized_text,包括 input_ids, token_type_ids, attention_mask

    max_length=100 表示最大长度为 100,配合 truncation=True,表示超过 100 则截断。

    padding=True 表示长度小于 100,则补全到 100。

     
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70

    # 定义训练的函数
    def train(model, dataloader, optimizer, criterion, device):
    model.train()
    epoch_loss = 0
    epoch_acc = 0
    for i, batch in enumerate(dataloader):
    # 标签形状为 (batch_size, 1)
    label = batch["label"]
    text = batch["text"]

    # tokenized_text 包括 input_ids, token_type_ids, attention_mask
    tokenized_text = tokenizer(text, max_length=100, add_special_tokens=True, truncation=True, padding=True, return_tensors="pt")
    tokenized_text = tokenized_text.to(device)
    # 梯度清零
    optimizer.zero_grad()

    #output: (loss), logits, (hidden_states), (attentions)
    output = model(**tokenized_text, labels=label)

    # y_pred_prob = logits : [batch_size, num_labels]
    y_pred_prob = output[1]
    y_pred_label = y_pred_prob.argmax(dim=1)

    # 计算loss
    # 这个 loss 和 output[0] 是一样的
    loss = criterion(y_pred_prob.view(-1, 2), label.view(-1))

    # 计算acc
    acc = ((y_pred_label == label.view(-1)).sum()).item()

    # 反向传播
    loss.backward()
    optimizer.step()

    # epoch 中的 loss 和 acc 累加
    # loss 每次是一个 batch 的平均 loss
    epoch_loss += loss.item()
    # acc 是一个 batch 的 acc 总和
    epoch_acc += acc
    if i % 200 == 0:
    print("current loss:", epoch_loss / (i+1), "\t", "current acc:", epoch_acc / ((i+1)*len(label)))

    # len(dataloader) 表示有多少个 batch,len(dataloader.dataset.dataset) 表示样本数量
    return epoch_loss / len(dataloader), epoch_acc / len(dataloader.dataset.dataset)

    def evaluate(model, iterator, device):
    model.eval()
    epoch_loss = 0
    epoch_acc = 0
    with torch.no_grad():
    for _, batch in enumerate(iterator):
    label = batch["label"]
    text = batch["text"]
    tokenized_text = tokenizer(text, max_length=100, add_special_tokens=True, truncation=True, padding=True,
    return_tensors="pt")
    tokenized_text = tokenized_text.to(device)

    output = model(**tokenized_text, labels=label)
    y_pred_label = output[1].argmax(dim=1)
    loss = output[0]
    acc = ((y_pred_label == label.view(-1)).sum()).item()
    # epoch 中的 loss 和 acc 累加
    # loss 每次是一个 batch 的平均 loss
    epoch_loss += loss.item()
    # acc 是一个 batch 的 acc 总和
    epoch_acc += acc

    # len(dataloader) 表示有多少个 batch,len(dataloader.dataset.dataset) 表示样本数量
    return epoch_loss / len(iterator), epoch_acc / len(iterator.dataset.dataset)

    开始训练和验证

     
     
    1
    2
    3
    4
    5
    6
    # 开始训练和验证
    for i in range(epochs):
    train_loss, train_acc = train(model, sentiment_train_loader, optimizer, criterion, device)
    print("train loss: ", train_loss, "\t", "train acc:", train_acc)
    valid_loss, valid_acc = evaluate(model, sentiment_valid_loader, criterion, device)
    print("valid loss: ", valid_loss, "\t", "valid acc:", valid_acc)

    参考

    https://www.cnblogs.com/dogecheng/p/11911909.html

     转自https://blog.zhangxiann.com/202008222159/

  • 相关阅读:
    [BZOJ 3774]最优选择
    [HDU 6598]Harmonious Army
    [SP2063]MPIGS-Sell Pigs
    [CF103E]Buying Sets
    [LOJ 6058]百步穿杨
    [CQOI2014]危桥
    李宏毅机器学习课程笔记-3.梯度下降精讲
    李宏毅机器学习课程笔记-2.5线性回归Python实战
    李宏毅机器学习课程笔记-2.4交叉验证
    李宏毅机器学习课程笔记-2.3欠拟合与过拟合
  • 原文地址:https://www.cnblogs.com/zhangxianrong/p/15066981.html
Copyright © 2011-2022 走看看