PyTorch搭建Bert对IMDB数据集进行情感分析(文本分类)

前言

关于数据集的介绍可以参考前面的文章:
PyTorch搭建LSTM对IMDB数据集进行情感分析(详细的数据分析与处理过程)

1. 数据处理

def load_data(args, path, tokenizer):
    classes = ['pos', 'neg']

    def process(flag):
        tokens = []
        labels = []
        seqs = []
        for label in classes:
            files = os.listdir(os.path.join(path, flag, label))
            # 去除标点符号
            r = '[’!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~\n。!,]+'
            cnt = 0
            for file in tqdm(files):
                cnt += 1
                if flag == 'train' and cnt == 2000:
                    break
                if flag == 'test' and cnt == 1000:
                    break
                with open(os.path.join(path, flag, label, file), 'r', encoding='utf8') as rf:
                    temp = rf.read().replace('\n', '')
                    temp = temp.replace('<br /><br />', ' ')
                    temp = re.sub(r, '', temp)
                    # token
                    token = tokenizer(temp, padding='max_length', max_length=args.seq_len,
                                      truncation=True, return_tensors='pt')
                    tokens.append(token)
                    cur_label = 1 if label == 'pos' else 0
                    seqs.append((token, cur_label))

        return seqs

    seq = process('train')
    _seq = process('test')
    seq.extend(_seq)
    # shuffle
    random.seed(42)
    random.shuffle(seq)

    Dtr = seq[:int(len(seq) * 0.6)]
    Dtr = MyDataset(Dtr)
    Dtr = DataLoader(dataset=Dtr, batch_size=args.batch_size, shuffle=True, num_workers=0, drop_last=False)

    Val = seq[int(len(seq) * 0.6):int(len(seq) * 0.8)]
    Val = MyDataset(Val)
    Val = DataLoader(dataset=Val, batch_size=args.batch_size, shuffle=True, num_workers=0, drop_last=False)

    Dte = seq[int(len(seq) * 0.8):]
    Dte = MyDataset(Dte)
    Dte = DataLoader(dataset=Dte, batch_size=args.batch_size, shuffle=True, num_workers=0, drop_last=False)

    return Dtr, Val, Dte

由于Bert参数量巨大,训练十分缓慢,因此这里只取一部分数据(一共6000条),打乱后按622的比例划分训练集、验证集以及测试集。

观察上述代码可以发现,针对每一个需要分类的文本,我们做了如下操作:

temp = rf.read().replace('\n', '')
temp = temp.replace('<br /><br />', ' ')
temp = re.sub(r, '', temp)
# token
token = tokenizer(temp, padding='max_length', max_length=args.seq_len,
                  truncation=True, return_tensors='pt')

即使用BertTokenizer将文本token化,得到input_idsattention_mask,关于token化网上有很多资料,这里不再详细解释。

将文本token化后得到的input_idsattention_mask将作为Bert模型的输入。

2. Bert

模型搭建如下:

class Bert_Classifier(nn.Module):
    def __init__(self, args):
        super(Bert_Classifier, self).__init__()
        self.config = BertConfig.from_pretrained("bert-base-uncased")
        # 
        self.config.hidden_size = args.hidden_size
        self.config.num_hidden_layers = 1
        self.config.num_attention_heads = 4
        # 
        self.bert = BertModel(config=self.config)
        self.dropout = nn.Dropout(p=0.1)
        self.fc = nn.Linear(args.hidden_size, 2)

    def forward(self, input_id, mask):
        h, output = self.bert(input_ids=input_id, attention_mask=mask, return_dict=False)
        output = self.dropout(output)
        output = self.fc(output)

        return output

由于预训练的Bert有上亿参数,虽说效果比较好,但训练时间比较缓慢,因此这里没有利用预训练模型进行fine tune,而是直接初始化一个未被训练的Bert

self.bert = BertModel(config=self.config)

其中config为参数配置列表,这里用了预训练模型时的config

self.config = BertConfig.from_pretrained("bert-base-uncased")
print(self.config)

输出如下:

BertConfig {
  "architectures": [
    "BertForMaskedLM"
  ],
  "attention_probs_dropout_prob": 0.1,
  "classifier_dropout": null,
  "gradient_checkpointing": false,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "layer_norm_eps": 1e-12,
  "max_position_embeddings": 512,
  "model_type": "bert",
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "pad_token_id": 0,
  "position_embedding_type": "absolute",
  "transformers_version": "4.21.0",
  "type_vocab_size": 2,
  "use_cache": true,
  "vocab_size": 30522
}

主要做了以下几点改变:

self.config.hidden_size = args.hidden_size
self.config.num_hidden_layers = 1
self.config.num_attention_heads = 4
  1. hidden_size表示隐藏层神经元数,默认为768,这里改成了128,这样最后得到的是每个位置处大小为128的输出。
  2. num_hidden_layers表示Transformer encoder中的隐藏层数,默认为12,这里为了降低参数数量,改成1。
  3. num_attention_heads表示多头注意力中的头数,默认为12,这里改成4。

观察forward过程:

def forward(self, input_id, mask):
    h, output = self.bert(input_ids=input_id, attention_mask=mask, return_dict=False)
    output = self.dropout(output)
    output = self.fc(output)

    return output

Bert需要input_idsattention_mask作为输入,最后得到每一个token的embedding。对于文本分类任务,我们一般只关注[CLS]的embedding,也就是output(池化后的输出)。

3. 模型训练

def train(args, Dtr, Val, path):
    device = args.device
    model = Bert_Classifier(args).to(device)
    # freeze parameters
    # unfreeze_layers = ['layer.10', 'layer.11', 'bert.poller', 'out.']
    if args.freeze:
        unfreeze_layers = ['fc.weight', 'fc.bias']
        for name, param in model.named_parameters():
            param.requires_grad = False
            for e in unfreeze_layers:
                if e in name:
                    param.requires_grad = True
                    break

    loss_function = nn.BCEWithLogitsLoss().to(device)
    if args.optimizer == 'adam':
        optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=args.lr,
                                     weight_decay=args.weight_decay)
    else:
        optimizer = torch.optim.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr=args.lr,
                                    momentum=0.9, weight_decay=args.weight_decay)
    scheduler = StepLR(optimizer, step_size=args.step_size, gamma=args.gamma)
    # training
    min_epochs = 2
    best_model = None
    min_val_loss = 5
    for epoch in tqdm(range(args.epochs)):
        train_loss = []
        for (bert_input, label) in Dtr:
            label = Variable(label.long()).to(device)
            input_ids = bert_input['input_ids'].squeeze(1).to(device)
            attention_mask = bert_input['attention_mask'].to(device)
            y_pred = model(input_ids, attention_mask)
            # one hot
            label = F.one_hot(label, 2).float()
            loss = loss_function(y_pred, label)
            train_loss.append(loss.item())
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        # validation
        val_loss = get_val_loss(args, model, Val)
        if epoch > min_epochs and val_loss < min_val_loss:
            min_val_loss = val_loss
            best_model = copy.deepcopy(model)

        print('epoch {:03d} train_loss {:.8f} val_loss {:.8f}'.format(
            epoch, np.mean(train_loss), val_loss))

        scheduler.step()
        model.train()

    state = {'models': best_model.state_dict()}
    torch.save(state, path)

需要注意的是,由于损失函数采用了BCEWithLogitsLoss,因此在模型的最后不需要加sigmoid激活函数,而是直接将logits作为输入。

由于模型训练过程是在服务器(4张RTX 3090Ti)中进行,因此batch_size(512)开的比较大。如果在其他环境中运行,可以适当减小batch_size

4. 模型测试

训练15轮,最终准确率:

acc: 0.85
很抱歉,作为AI语言模型,我并不能执行代码,但我可以提供一些实现BERT和完成IMDB数据集文本分类的建议: 1. 首先,需要下载BERT预训练模型以及IMDB数据集。可以在Hugging Face的网站上找到BERT的预训练模型,IMDB数据集可以在官网上下载。 2. 使用PyTorch中的transformers库加载BERT预训练模型,将其fine-tuning为文本分类器。可以使用BertForSequenceClassification类完成这一过程。 3. 将IMDB数据集转换为PyTorch的Dataset和DataLoader格式,以便进行训练和验证。 4. 使用AdamW优化器和学习率调度器对模型进行训练。在每个epoch结束时,计算模型在验证集上的准确率,并保存最佳的模型参数。 5. 使用训练好的模型对测试集进行预测,并计算模型在测试集上的准确率和其他评估指标。 下面是一个简单的BERT文本分类的代码示例: ``` import torch from transformers import BertForSequenceClassification, BertTokenizer from torch.utils.data import DataLoader, Dataset from sklearn.metrics import classification_report # 加载BERT预训练模型和tokenizer model = BertForSequenceClassification.from_pretrained('bert-base-uncased') tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') # 加载IMDB数据集 class IMDBDataset(Dataset): def __init__(self, file_path): self.data = [] with open(file_path, 'r') as f: for line in f: text, label = line.strip().split('\t') self.data.append((text, int(label))) def __len__(self): return len(self.data) def __getitem__(self, idx): text, label = self.data[idx] input_ids = tokenizer.encode(text, add_special_tokens=True) attention_mask = [1] * len(input_ids) return torch.tensor(input_ids), torch.tensor(attention_mask), torch.tensor(label) train_dataset = IMDBDataset('train.tsv') val_dataset = IMDBDataset('val.tsv') test_dataset = IMDBDataset('test.tsv') # 转换为DataLoader格式 train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False) test_loader = DataLoader(test_dataset, batch_size=16, shuffle=False) # 定义优化器和学习率调度器 optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1, gamma=0.1) # 训练模型 best_val_acc = 0 for epoch in range(5): model.train() for input_ids, attention_mask, labels in train_loader: optimizer.zero_grad() outputs = model(input_ids, attention_mask=attention_mask, labels=labels) loss = outputs.loss loss.backward() optimizer.step() scheduler.step() # 在验证集上评估模型 model.eval() val_preds = [] val_labels = [] with torch.no_grad(): for input_ids, attention_mask, labels in val_loader: outputs = model(input_ids, attention_mask=attention_mask) logits = outputs.logits preds = torch.argmax(logits, dim=1).tolist() val_preds.extend(preds) val_labels.extend(labels.tolist()) val_acc = sum([1 if p==l else 0 for p, l in zip(val_preds, val_labels)]) / len(val_labels) print(f'Epoch {epoch+1}, val_acc: {val_acc}') # 保存最佳模型参数 if val_acc > best_val_acc: torch.save(model.state_dict(), 'best_model.pt') best_val_acc = val_acc # 在测试集上评估模型 model.load_state_dict(torch.load('best_model.pt')) model.eval() test_preds = [] test_labels = [] with torch.no_grad(): for input_ids, attention_mask, labels in test_loader: outputs = model(input_ids, attention_mask=attention_mask) logits = outputs.logits preds = torch.argmax(logits, dim=1).tolist() test_preds.extend(preds) test_labels.extend(labels.tolist()) test_acc = sum([1 if p==l else 0 for p, l in zip(test_preds, test_labels)]) / len(test_labels) print(f'Test_acc: {test_acc}') print(classification_report(test_labels, test_preds)) ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyril_KI

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值