Bert+LSTM+CRF命名实体识别pytorch代码详解

发布时间:2023-10-24 10:30

Bert+LSTM+CRF命名实体识别

从0开始解析源代码。

  1. 理解原代码的逻辑,具体了解为什么使用预训练的bert,bert有什么作用,网络的搭建是怎么样的,训练过程是怎么训练的,输出是什么

  2. 调试运行源代码

NER目标

NER是named entity recognized的简写,对人名地名机构名日期时间专有名词等进行识别。

结果输出标注方法

采用细粒度标注,就是对于每一个词都给一个标签,其中连续的词可能是一个标签,与原始数据集的结构不同,需要对数据进行处理,转化成对应的细粒度标注形式。

数据集形式修改

形式:

{
	"text": "浙商银行企业信贷部叶老桂博士则从另一个角度对五道门槛进行了解读。叶老桂认为,对目前国内商业银行而言,",
	"label": {
		"name": {
			"叶老桂": [
				[9, 11],
				[32, 34]
			]
		},
		"company": {
			"浙商银行": [
				[0, 3]
			]
		}
	}
}

修改后数据集对应格式:

sentence: ['温', '格', '的', '球', '队', '终', '于', '又', '踢', '了', '一', '场', '经', '典', '的', '比', '赛', ',', '2', '比', '1', '战', '胜', '曼', '联', '之', '后', '枪', '手', '仍', '然', '留', '在', '了', '夺', '冠', '集', '团', '之', '内', ',']
label: ['B-name', 'I-name', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'B-organization', 'I-organization', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']

数据预处理

对于一个句子不进行分词,原因是NER为序列标注任务,需要确定边界,分词后就可能产生错误的分词结果影响效果(B-x,I-x这种连续性,分词后会影响元意思表达)。

def preprocess(self, mode):
        """
        params:
            words:将json文件每一行中的文本分离出来,存储为words列表
            labels:标记文本对应的标签,存储为labels
        examples:
            words示例:['生', '生', '不', '息', 'C', 'S', 'O', 'L']
            labels示例:['O', 'O', 'O', 'O', 'B-game', 'I-game', 'I-game', 'I-game']
        """
np.savez_compressed(output_dir, words=word_list, labels=label_list)

保存的文件也还是一句是一句的,所以后续处理中只有CLS,不需要终止符。

数据集分集与分batch
def dev_split(dataset_dir):
    """split dev set"""
    data = np.load(dataset_dir, allow_pickle=True)#加载npz文件
    words = data["words"]
    labels = data["labels"]
    x_train, x_dev, y_train, y_dev = train_test_split(words, labels, test_size=config.dev_split_size, random_state=0)
    return x_train, x_dev, y_train, y_dev

调用train_test_split实现分train和dev的数据集。

将数据转化形式,用idx表示,构造NERDataset类表示使用数据集
    def __init__(self, words, labels, config, word_pad_idx=0, label_pad_idx=-1):
        self.tokenizer = BertTokenizer.from_pretrained(config.bert_model, do_lower_case=True)#调用预训练模型
        self.label2id = config.label2id#字典                                                 
        self.id2label = {_id: _label for _label, _id in list(config.label2id.items())}##字典
        self.dataset = self.preprocess(words, labels)#数据集预处理
        self.word_pad_idx = word_pad_idx
        self.label_pad_idx = label_pad_idx
        self.device = config.device

    def preprocess(self, origin_sentences, origin_labels):
        """
        Maps tokens and tags to their indices and stores them in the dict data.
        examples: 
            word:['[CLS]', '浙', '商', '银', '行', '企', '业', '信', '贷', '部']
            sentence:([101, 3851, 1555, 7213, 6121, 821, 689, 928, 6587, 6956],
                        array([ 1,  2,  3,  4,  5,  6,  7,  8,  9]))
            label:[3, 13, 13, 13, 0, 0, 0, 0, 0]
        """
        data = []
        sentences = []
        labels = []
        # eg. i am cutting tokenize: cutting->[cut,'##ing']自动修改形式变成单数或者恢复原型
        for line in origin_sentences:
            # replace each token by its index
            # we can not use encode_plus because our sentences are aligned to labels in list type
            words = []
            word_lens = []
            for token in line:
                words.append(self.tokenizer.tokenize(token))
                word_lens.append(len(token))#如果含有英文会出现上面的情况,中文没有分词一般是1
                #>> [1]*9
            # 变成单个字的列表,开头加上[CLS]
            words = ['[CLS]'] + [item for token in words for item in token]
            token_start_idxs = 1 + np.cumsum([0] + word_lens[:-1])# np.array:[1,2,3]  自动广播机制 每个+1  a[1,2,3] a[:-1]->[1,2] 求出每个词在没加【cls】的句首字母idx
            # 这里计数tokens在words中的索引,第一个起始位置+1(加了cls)了,所以每一个+1
            sentences.append((self.tokenizer.convert_tokens_to_ids(words), token_start_idxs))
            #单词转化成idx,直接调用函数即可
        for tag in origin_labels:
            label_id = [self.label2id.get(t) for t in tag] #单个句子的tag idx
            labels.append(label_id)
        for sentence, label in zip(sentences, labels):
            data.append((sentence, label))#句子编码、token在words中的位置、对应的label(一个token可能占用多个word(cutting->cut+ing)
        return data

preprocess处理token和word,记录每个token在word中的起始位置用于后续的对齐,对于每个单词进行tokennize(中文无变化,英文可能会有,但数据处理过程中将单词分成字母,所以无影响),然后在句首加上开始字符,因为生成第一个单词也需要概率因此句首不能省略,然后就是将字符转化成idx存储,tag也转化成idx;

类中的功能函数
def __getitem__(self, idx):#class使用索引
    """sample data to get batch"""
    word = self.dataset[idx][0]
    label = self.dataset[idx][1]
    return [word, label]
def __len__(self):#class 使用长度
    """get dataset size"""
    return len(self.dataset)

可以索引访问与访问长度。

encode_plus可以直接编码,但这里不能使用:align限制

因为单词要和标签对应,直接tokennize后编码,不能确定与标签的对应关系;

tokennize()

对于英文一个token通过tokennize会得到多个word:cutting->cut+##ing;

np.cumsum(a)累计计数
[1,1,1]--->[1,2,3]

模型架构

首先要明确,是继承bert基类,然后自定义forward函数就建好网络了,基本结构试:

class Module(nn.Module):
    def __init__(self):
        super(Module, self).__init__()
        # ......
       
    def forward(self, x):
        # ......
        return x
data = .....  #输入数据
# 实例化一个对象
module = Module()
# 前向传播
module(data)  
# 而不是使用下面的
# module.forward(data)  
关于forward的解释

nn.module中实现时就在call函数中定义了调用forward,然后传参就自动调用了。

定义__call__方法的类可以当作函数调用,具体参考Python的面向对象编程。也就是说,当把定义的网络模型model当作函数调用的时候就自动调用定义的网络模型的forward方法。nn.Module 的__call__方法部分源码如下所示:

def __call__(self, *input, **kwargs):
result = self.forward(*input, **kwargs)
BERT模式:选择对应,在代码的不同部分都有切换(model.eval();model.train())
  • train
  • eval
  • predict

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号