使用Bert+BiLSTM+CRF训练 NER任务

使用的数据集在这里E-Commercial NER Dataset / 电商NER数据集_数据集-阿里云天池

针对面向电商的命名实体识别研究,我们通过爬取搜集了淘宝商品文本的标题,并标注了4大类,9小类的实体类别。具体类型及实体数量如下

针对面向电商的命名实体识别研究,我们通过爬取搜集了淘宝商品文本的标题,并标注了4大类,9小类的实体类别。具体类型及实体数量如下:

每个文件数据格式相同,都为根据BI schema标注的NER数据

模型使用BERT + BiLSTM + CRF 该结构通过结合强大的预训练语言表示、序列建模能力和标签依赖建模,能够有效地提升命名实体识别任务的准确性和鲁棒性。这种组合在实际应用中已经显示出优越的性能,尤其是在复杂的语言理解任务中。

BERT + BiLSTM + CRF 是一种常用于命名实体识别(NER)任务的深度学习模型结构。下面是对这个模型结构的详细解释,以及它在 NER 任务中的好处。

模型结构

BERT(Bidirectional Encoder Representations from Transformers)
  • 预训练模型:BERT 是一种基于 Transformer 的预训练语言模型,能够捕捉到上下文信息。它通过双向编码器考虑上下文中的所有单词,从而生成每个单词的上下文嵌入。
  • 词向量表示:在 NER 任务中,BERT 可以生成高质量的词嵌入,帮助模型更好地理解文本中的语义。
BiLSTM(Bidirectional Long Short-Term Memory)
  • 序列建模:BiLSTM 是一种改进的 LSTM(长短期记忆网络),能够同时考虑序列的前向和后向信息。通过双向处理,BiLSTM 可以更好地捕捉上下文依赖关系。
  • 特征提取:在 NER 任务中,BiLSTM 能够从 BERT 输出的嵌入中提取序列特征,并对它们进行时间序列建模。
CRF(Conditional Random Field)
  • 序列标注:CRF 是一种用于序列标注的概率模型,可以考虑标签之间的依赖关系。它在输出层对 BiLSTM 的结果进行后处理,以确保输出标签满足特定的序列约束。
  • 提高准确性:CRF 能够通过学习标签之间的关系(例如,实体标签之间的相互影响)来增强模型的预测能力。

应用在 NER 任务中的好处

1. 上下文理解
  • BERT 的强大能力:BERT 提供丰富的上下文信息,能够更好地理解文本中的语义,从而提高对实体的识别能力。
2. 序列建模
  • BiLSTM 的双向特性:通过双向处理,BiLSTM 能够捕捉到前后文的依赖关系,改进对实体边界的识别。
3. 标签依赖建模
  • CRF 的序列约束:CRF 考虑了输出标签的依赖关系,能够避免产生不合理的标签序列(例如,"B-PER" 后面直接跟 "B-LOC")。
4. 提高性能
  • 综合优势:结合 BERT、BiLSTM 和 CRF 的优点,可以显著提高 NER 任务的性能,尤其是在处理复杂的实体关系和多样的语境时。

模型model.py内容

# -*- coding: utf-8 -*-
import torch
import torch.nn as nn
from transformers import BertModel
from TorchCRF import CRF


class Bert_BiLSTM_CRF(nn.Module):

    def __init__(self, tag_to_ix, embedding_dim=768, hidden_dim=256):
        super(Bert_BiLSTM_CRF, self).__init__()
        self.tag_to_ix = tag_to_ix
        self.tagset_size = len(tag_to_ix)
        self.hidden_dim = hidden_dim
        self.embedding_dim = embedding_dim

        self.bert = BertModel.from_pretrained('bert-base-chinese')
        self.lstm = nn.LSTM(input_size=embedding_dim,
                            hidden_size=hidden_dim // 2,
                            num_layers=2,
                            bidirectional=True,
                            batch_first=True)
        self.dropout = nn.Dropout(p=0.1)
        self.linear = nn.Linear(hidden_dim, self.tagset_size)
        self.crf = CRF(self.tagset_size)

    def _get_features(self, sentence):
        with torch.no_grad():
            outputs = self.bert(sentence)
        embeds = outputs.last_hidden_state
        enc, _ = self.lstm(embeds)
        enc = self.dropout(enc)
        feats = self.linear(enc)
        return feats

    def forward(self, sentence, tags, mask, is_test=False):
        emissions = self._get_features(sentence)
        if not is_test:  # 训练阶段,返回loss
            loss = -self.crf(emissions, tags, mask)
            return loss.mean()
        else:
            return self.crf.viterbi_decode(emissions, mask)

统计下数据集中出现的多少种类型的标注

# 统计标签类型的Python脚本

def count_labels(file_path):
    labels = set()  # 使用集合来存储唯一标签
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file:
            parts = line.strip().split()  # 按空格分割每一行
            if len(parts) > 1:  # 确保这一行有标签
                label = parts[-1]  # 标签通常在最后一列
                labels.add(label)  # 将标签添加到集合中
    return labels

# 指定文件路径
file_path = 'dataset/train.txt'
unique_labels = count_labels(file_path)

# 输出结果
print(f"总共有 {len(unique_labels)} 种类型的标签:")
for label in unique_labels:
    print(label)

该数据集里共出现了 9 种类型的标签:

I-HCCX
I-HPPX
B-HCCX
B-XH
I-XH
I-MISC
O
B-HPPX
B-MISC

标注遵循了一种常见的命名实体识别标注方案:

  • B- 表示实体的开始。
  • I- 表示实体的内部部分。
  • O 表示非实体部分。
  • 实体类别(如 HCCX、HPPX、XH、MISC)根据具体任务和数据集定义。例如HCCX为产品类型,HPPX是品牌名称,XH 是产品型号

下面开始编写训练需要工具

# -*- coding: utf-8 -*-
'''
@Author: Chenrui
@Date: 2021-9-30
@Description: This file is for implementing Dataset. 
@All Right Reserve
'''

import torch
from torch.utils.data import Dataset
from transformers import BertTokenizer

bert_model = 'bert-base-chinese'
tokenizer = BertTokenizer.from_pretrained(bert_model)
'''
<PAD>: 用于填充序列,确保输入长度一致。
[CLS]: 表示序列的开始,常用于分类任务。
[SEP]: 用于分隔不同的输入序列,帮助模型理解输入的结构。
'''
labels = ['B-HCCX',
              'I-HPPX',
              'I-HCCX',
              'I-MISC',
              'B-XH',
              'O',
              'I-XH',
              'B-MISC',
              'B-HPPX']

VOCAB = ('<PAD>', '[CLS]', '[SEP]')

# 将 VOCAB 转换为列表以便修改
vocab_list = list(VOCAB)

# 添加 labels 到 vocab_list
vocab_list.extend(labels)

# 将列表转换回元组
VOCAB = tuple(vocab_list)

tag2idx = {tag: idx for idx, tag in enumerate(VOCAB)}
idx2tag = {idx: tag for idx, tag in enumerate(VOCAB)}
MAX_LEN = 256 - 2


class NerDataset(Dataset):
    ''' Generate our dataset '''

    def __init__(self, f_path):
        self.sents = []
        self.tags_li = []

        with open(f_path, 'r', encoding='utf-8') as f:
            lines = [line.split('\n')[0] for line in f.readlines() if len(line.strip()) != 0]

        tags = [line.split(' ')[1] for line in lines]
        words = [line.split(' ')[0] for line in lines]

        word, tag = [], []
        for char, t in zip(words, tags):
            if char != '。':
                word.append(char)
                tag.append(t)
            else:
                if len(word) > MAX_LEN:
                    self.sents.append(['[CLS]'] + word[:MAX_LEN] + ['[SEP]'])
                    self.tags_li.append(['[CLS]'] + tag[:MAX_LEN] + ['[SEP]'])
                else:
                    self.sents.append(['[CLS]'] + word + ['[SEP]'])
                    self.tags_li.append(['[CLS]'] + tag + ['[SEP]'])
                word, tag = [], []

    def __getitem__(self, idx):
        words, tags = self.sents[idx], self.tags_li[idx]
        token_ids = tokenizer.convert_tokens_to_ids(words)
        label_ids = [tag2idx[tag] for tag in tags]
        seqlen = len(label_ids)
        return token_ids, label_ids, seqlen

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


def PadBatch(batch):
    maxlen = 512
    token_tensors = torch.LongTensor([i[0] + [0] * (maxlen - len(i[0])) for i in batch])
    label_tensors = torch.LongTensor([i[1] + [0] * (maxlen - len(i[1])) for i in batch])
    mask = (token_tensors > 0)
    return token_tensors, label_tensors, mask

训练的主程序

# -*- coding: utf-8 -*-
'''
@Author: Chenrui
@Date: 2023-9-30
@Description: This file is for training, validating and testing.
@All Right Reserve
'''

import argparse
import warnings

import numpy as np
import torch
from sklearn import metrics
from torch.utils import data
from transformers import AdamW, get_linear_schedule_with_warmup

from models import Bert_BiLSTM_CRF
from utils import NerDataset, PadBatch, tag2idx, idx2tag, labels

warnings.filterwarnings("ignore", category=DeprecationWarning)
# os.environ['CUDA_VISIBLE_DEVICES'] = '0'





if __name__ == "__main__":

    best_model = None
    _best_val_loss = 1e18
    _best_val_acc = 1e-18

    parser = argparse.ArgumentParser()
    parser.add_argument("--batch_size", type=int, default=64)
    parser.add_argument("--lr", type=float, default=0.001)
    parser.add_argument("--n_epochs", type=int, default=40)
    parser.add_argument("--trainset", type=str, default="./dataset/train.txt")
    parser.add_argument("--validset", type=str, default="./dataset/dev.txt")
    parser.add_argument("--testset", type=str, default="./dataset/test.txt")

    ner = parser.parse_args()
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model = Bert_BiLSTM_CRF(tag2idx).to(device)

    print('Initial model Done.')
    train_dataset = NerDataset(ner.trainset)
    eval_dataset = NerDataset(ner.validset)
    test_dataset = NerDataset(ner.testset)
    print('Load Data Done.')
    '''
    torch.utils.data.DataLoader 是 PyTorch 中用于加载数据的一个强大工具。它的主要作用是将数据集分成小批量(batches),
    并提供迭代器以便在训练模型时方便地访问数据。以下是 DataLoader 的一些主要功能和使用方法。
    '''
    train_iter = data.DataLoader(dataset=train_dataset,
                                 batch_size=ner.batch_size,
                                 shuffle=True,
                                 num_workers=4,
                                 collate_fn=PadBatch)

    eval_iter = data.DataLoader(dataset=eval_dataset,
                                batch_size=(ner.batch_size) // 2,
                                shuffle=False,
                                num_workers=4,
                                collate_fn=PadBatch)

    test_iter = data.DataLoader(dataset=test_dataset,
                                batch_size=(ner.batch_size) // 2,
                                shuffle=False,
                                num_workers=4,
                                collate_fn=PadBatch)

    # optimizer = optim.Adam(self.model.parameters(), lr=ner.lr, weight_decay=0.01)
    optimizer = AdamW(model.parameters(), lr=ner.lr, eps=1e-6)

    # Warmup
    len_dataset = len(train_dataset)
    epoch = ner.n_epochs
    batch_size = ner.batch_size
    total_steps = (len_dataset // batch_size) * epoch if len_dataset % batch_size == 0 \
        else (len_dataset // batch_size + 1) * epoch

    warm_up_ratio = 0.1  # Define 10% steps

    '''
    get_linear_schedule_with_warmup 是 Hugging Face 的 transformers 库中提供的一个函数,用于创建一个学习率调度器。
    它的主要作用是在训练过程中动态调整学习率,具体通过以下几个步骤实现:
    主要功能
    学习率预热(Warmup):
    在训练的初始阶段,学习率从 0 逐渐增加到设定的最大学习率。这有助于防止模型在训练初期出现较大的更新,从而导致不稳定。
    线性衰减(Linear Decay):
    在预热阶段结束后,学习率会线性地递减到 0。这种衰减方式通常能有效地提高模型的收敛性。
    '''
    scheduler = get_linear_schedule_with_warmup(optimizer=optimizer,
                                                num_warmup_steps=warm_up_ratio * total_steps,
                                                num_training_steps=total_steps)

    print('Start Train...,')
    for epoch in range(1, ner.n_epochs + 1):

        train(epoch, model, train_iter, optimizer, scheduler, device)
        candidate_model, loss, acc = validate(epoch, model, eval_iter, device)

        if loss < _best_val_loss and acc > _best_val_acc:
            best_model = candidate_model
            _best_val_loss = loss
            _best_val_acc = acc

        print("=============================================")

    y_test, y_pred = test(best_model, test_iter, device)
    print(metrics.classification_report(y_test, y_pred, labels=labels, digits=3))

    # 保存模型
    torch.save(model.state_dict(), 'ner_task_model.pth')

导入部分工具的用途

  • argparse:用于解析命令行参数。
  • warnings:用于控制警告信息。
  • numpytorch:用于数值计算和深度学习。
  • sklearn.metrics:用于评估模型性能。
  • transformers:提供 AdamW 优化器和学习率调度器

train训练函数,功能训练模型并输出损失。

def train(e, model, iterator, optimizer, scheduler, device):
    model.train()
    losses = 0.0
    step = 0
    for i, batch in enumerate(iterator):
        step += 1
        x, y, z = batch
        x = x.to(device)
        y = y.to(device)
        z = z.to(device)

        loss = model(x, y, z)
        losses += loss.item()
        loss.backward()
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad()

    print("Epoch: {}, Loss:{:.4f}".format(e, losses / step))
  • e: 当前的训练轮数。
  • model: NER 模型。
  • iterator: 数据加载器。
  • optimizer: 优化器。
  • scheduler: 学习率调度器。
  • device: 设备(CPU 或 GPU)

**validate函数,**验证集上评估模型性能,计算损失和准确率,并返回模型、损失和准确率。

def validate(e, model, iterator, device):
    # 设置模型为评估模式
    model.eval()
    Y, Y_hat = [], []
    losses = 0
    step = 0
    with torch.no_grad():
        for i, batch in enumerate(iterator):
            step += 1

            x, y, z = batch
            x = x.to(device)
            y = y.to(device)
            z = z.to(device)

            y_hat = model(x, y, z, is_test=True)

            loss = model(x, y, z)
            losses += loss.item()

            for j in y_hat:
                Y_hat.extend(j)
            mask = (z == 1)
            y_orig = torch.masked_select(y, mask)
            Y.append(y_orig.cpu())

    Y = torch.cat(Y, dim=0).numpy()
    Y_hat = np.array(Y_hat)
    acc = (Y_hat == Y).mean() * 100

    print("Epoch: {}, Val Loss:{:.4f}, Val Acc:{:.3f}%".format(e, losses / step, acc))
    return model, losses / step, acc

**test函数,**在测试集上评估模型性能,返回真实标签和预测标签

def test(model, iterator, device):
    model.eval()
    Y, Y_hat = [], []
    with torch.no_grad():
        for i, batch in enumerate(iterator):
            x, y, z = batch
            x = x.to(device)
            z = z.to(device)
            y_hat = model(x, y, z, is_test=True)
            # Save prediction
            for j in y_hat:
                Y_hat.extend(j)
            # Save labels
            mask = (z == 1).cpu()
            y_orig = torch.masked_select(y, mask)
            Y.append(y_orig)

    Y = torch.cat(Y, dim=0).numpy()
    y_true = [idx2tag[i] for i in Y]
    y_pred = [idx2tag[i] for i in Y_hat]

    return y_true, y_pred

上面代码实现了一个完整的 NER 训练、验证和测试流程,使用了 BERT、BiLSTM 和 CRF 模型结构。通过 argparse 解析输入参数,使用 PyTorch 处理数据和模型训练,使用 DataLoader 创建迭代器以便于批量处理数据,置 AdamW 优化器和学习率调度器,进行多个训练轮次,每个轮次调用训练和验证函数,最终评估模型性能并保存最佳模型。这是一个典型的深度学习项目框架,适用于序列标注任务。

运行训练

python main.py --n_epochs 1000

查看到准确率稍低, 先不管了把模型文件下载下来本地先试试识别提取能力

下载保存的模型文件

测试该模型代码

import os

import torch
from torch.utils.data import Dataset

from models import Bert_BiLSTM_CRF
from utils import tokenizer, tag2idx, MAX_LEN, idx2tag

device = 'cuda' if torch.cuda.is_available() else 'cpu'


class SequenceLabelingDataset(Dataset):
    def __init__(self, lines):
        self.lines = lines
        self.sents, word = [], []

        words = [line[0] for line in lines]
        for char in words:
            if char != '。':
                word.append(char)

            else:
                if len(word) > MAX_LEN:
                    self.sents.append(['[CLS]'] + word[:MAX_LEN] + ['[SEP]'])
                else:
                    self.sents.append(['[CLS]'] + word + ['[SEP]'])
                word = []

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

    def item(self):
        words = self.sents[0]
        token_ids = tokenizer.convert_tokens_to_ids(words)
        seqlen = len(token_ids)
        return token_ids, None, seqlen

    def get_token_ids(self, batch):
        maxlen = 512
        token_tensors = torch.LongTensor([i[0] + [0] * (maxlen - len(i[0])) for i in batch])
        label_tensors = None
        mask = (token_tensors > 0)
        return token_tensors, label_tensors, mask


# 示例数据
data_string = '2005复刻正版戏曲光盘1dvd视频碟片河南曲剧优秀剧目集锦。'


print('data_string', data_string)
data = [(char,) for char in data_string]
print('data', data)

# 创建数据集和 DataLoader
dataset = SequenceLabelingDataset(data)

current_directory = os.getcwd()
model_path = os.path.join(current_directory, 'model', 'ner_task_model.pth')

model = Bert_BiLSTM_CRF(tag2idx).to(device)
model.load_state_dict(torch.load(model_path, map_location=device))
model.eval()


def predict(model, batch, device):
    x, y, z = batch[0]

    maxlen = 512
    token_tensors = torch.LongTensor([i[0] + [0] * (maxlen - len(i[0])) for i in batch])
    mask = (token_tensors > 0)

    predict = model(token_tensors, None, mask, is_test=True)  # 使用生成的 mask
    return predict


try:
    batch = [dataset.item()]
    predict = predict(model, batch, device)
    print('predict', predict)
    label_ids = [idx2tag[tag] for tag in predict[0]]
    print('label_ids', label_ids)
except Exception as e:
    print('predict throw exception', e)

# 提取实体
entities = {
    'HCCX': [],
    'HPPX': [],
    'MISC': [],
    'XH': [],
    'O': []
}

# 当前实体存储
current_entities = {
    'HCCX': [],
    'HPPX': [],
    'MISC': [],
    'XH': [],
    'O': []
}

# 按照标签提取实体
for i, label in enumerate(label_ids):
    word = data_string[i] if i < len(data_string) else ''

    if 'B-' in label:
        entity_type = label[2:]  # 获取实体类型
        current_entities[entity_type].append(word)
    elif 'I-' in label:
        entity_type = label[2:]  # 获取实体类型
        if current_entities[entity_type]:  # 确保当前实体存在
            current_entities[entity_type].append(word)
    else:
        # 处理当前实体
        for entity_type in current_entities:
            if current_entities[entity_type]:
                entities[entity_type].append("".join(current_entities[entity_type]))
                current_entities[entity_type] = []

# 处理最后一个实体
for entity_type in current_entities:
    if current_entities[entity_type]:
        entities[entity_type].append("".join(current_entities[entity_type]))

# 输出结果
for entity_type, entity_list in entities.items():
    print(f"提取的{entity_type}实体:", entity_list)

返回结果

实体识别出来一些

现在NER任务基本都是大模型去做,相比大语言模型小模型越来越势微越来越不受重视,前公司在知识图谱上搞得globalpointer 任务做NER得产品已经彻底放弃,以至于我们还有些用该产品得客户已经无法得到支持了,但作为NER 入门得例子还是值得拿出来写写

相关推荐
迅易科技28 分钟前
借助腾讯云质检平台的新范式,做工业制造企业质检的“AI慧眼”
人工智能·视觉检测·制造
古希腊掌管学习的神1 小时前
[机器学习]XGBoost(3)——确定树的结构
人工智能·机器学习
ZHOU_WUYI2 小时前
4.metagpt中的软件公司智能体 (ProjectManager 角色)
人工智能·metagpt
靴子学长3 小时前
基于字节大模型的论文翻译(含免费源码)
人工智能·深度学习·nlp
AI_NEW_COME3 小时前
知识库管理系统可扩展性深度测评
人工智能
海棠AI实验室4 小时前
AI的进阶之路:从机器学习到深度学习的演变(一)
人工智能·深度学习·机器学习
hunteritself4 小时前
AI Weekly『12月16-22日』:OpenAI公布o3,谷歌发布首个推理模型,GitHub Copilot免费版上线!
人工智能·gpt·chatgpt·github·openai·copilot
IT古董5 小时前
【机器学习】机器学习的基本分类-强化学习-策略梯度(Policy Gradient,PG)
人工智能·机器学习·分类
centurysee5 小时前
【最佳实践】Anthropic:Agentic系统实践案例
人工智能
mahuifa5 小时前
混合开发环境---使用编程AI辅助开发Qt
人工智能·vscode·qt·qtcreator·编程ai