失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > CLUENER 细粒度命名实体识别 附完整代码

CLUENER 细粒度命名实体识别 附完整代码

时间:2022-12-12 21:09:31

相关推荐

CLUENER 细粒度命名实体识别 附完整代码

CLUENER 细粒度命名实体识别

文章目录

CLUENER 细粒度命名实体识别一、任务说明:二、数据集介绍:2.1 数据集划分和数据内容2.2 标签类别和定义:2.3 数据分布 三、处理json文件,转成BIOS标注3.1 分词和标签预处理 四、数据预处理,装入dataloader4.1 pandas读取npz文件,将BIOS标注转成索引4.2 将labels统一填充到句子最大长度52,以便装入dataloader。4.3 划分数据集,设置batch_size,数据装入dataloader 五、定义bert模型、优化器、和训练部分5.1 定义bert模型本体5.2 定义优化器5.3 编写训练和验证循环5.4 编写predict函数5.5 用模型预测验证集结果,与原标签对比 六. 总结(pytorch还要练啊)

一、任务说明:

最开始是参考知乎文章《用BERT做NER?教你用PyTorch轻松入门Roberta!》,github项目地址:《hemingkx/CLUENER》任务介绍:本任务是中文语言理解测评基准(CLUE)任务之一:《CLUE Fine-Grain NER》。数据来源:本数据是在清华大学开源的文本分类数据集THUCTC基础上,选出部分数据进行细粒度命名实体标注,原数据来源于Sina News RSS.平台github任务详情:《CLUENER 细粒度命名实体识别》CLUE命名实体任务排行榜BERT-base-X部分的代码编写思路参考 lemonhu参考文章《中文NER任务简析与深度算法模型总结和实战展示》

二、数据集介绍:

cluener下载链接:数据下载

2.1 数据集划分和数据内容

训练集:10748验证集:1343测试集(无标签):1345原始数据存储在json文件中。文件中的每一行是一条单独的数据,一条数据包括一个原始句子以及其上的标签,具体形式如下:

{"text": "浙商银行企业信贷部叶老桂博士则从另一个角度对五道门槛进行了解读。叶老桂认为,对目前国内商业银行而言,", "label": {"name": {"叶老桂": [[9, 11]]}, "company": {"浙商银行": [[0, 3]]}}}{"text": "生生不息CSOL生化狂潮让你填弹狂扫", "label": {"game": {"CSOL": [[4, 7]]}}}

展开看就是:

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

数据字段解释:

以train.json为例,数据分为两列:text & label,其中text列代表文本,label列代表文本中出现的所有包含在10个类别中的实体。例如:

text: "北京勘察设计协会副会长兼秘书长周荫如"label: {"organization": {"北京勘察设计协会": [[0, 7]]}, "name": {"周荫如": [[15, 17]]}, "position": {"副会长": [[8, 10]], "秘书长": [[12, 14]]}}其中,organization,name,position代表实体类别,"organization": {"北京勘察设计协会": [[0, 7]]}:表示原text中,"北京勘察设计协会" 是类别为 "组织机构(organization)" 的实体, 并且start_index为0,end_index为7 (注:下标从0开始计数)"name": {"周荫如": [[15, 17]]}:表示原text中,"周荫如" 是类别为 "姓名(name)" 的实体, 并且start_index为15,end_index为17"position": {"副会长": [[8, 10]], "秘书长": [[12, 14]]}:表示原text中,"副会长" 是类别为 "职位(position)" 的实体, 并且start_index为8,end_index为10,同时,"秘书长" 也是类别为 "职位(position)" 的实体,并且start_index为12,end_index为14

2.2 标签类别和定义:

数据分为10个标签类别,分别为: 地址(address),书名(book),公司(company),游戏(game),政府(goverment),电影(movie),姓名(name),组织机构(organization),职位(position),景点(scene)

标签定义与规则:

地址(address): **省**市**区**街**号,**路,**街道,**村等(如单独出现也标记),注意:地址需要标记完全, 标记到最细。书名(book): 小说,杂志,习题集,教科书,教辅,地图册,食谱,书店里能买到的一类书籍,包含电子书。公司(company): **公司,**集团,**银行(央行,中国人民银行除外,二者属于政府机构), 如:新东方,包含新华网/中国军网等。游戏(game): 常见的游戏,注意有一些从小说,电视剧改编的游戏,要分析具体场景到底是不是游戏。政府(goverment): 包括中央行政机关和地方行政机关两级。 中央行政机关有国务院、国务院组成部门(包括各部、委员会、中国人民银行和审计署)、国务院直属机构(如海关、税务、工商、环保总局等),军队等。电影(movie): 电影,也包括拍的一些在电影院上映的纪录片,如果是根据书名改编成电影,要根据场景上下文着重区分下是电影名字还是书名。姓名(name): 一般指人名,也包括小说里面的人物,宋江,武松,郭靖,小说里面的人物绰号:及时雨,花和尚,著名人物的别称,通过这个别称能对应到某个具体人物。组织机构(organization): 篮球队,足球队,乐团,社团等,另外包含小说里面的帮派如:少林寺,丐帮,铁掌帮,武当,峨眉等。职位(position): 古时候的职称:巡抚,知州,国师等。现代的总经理,记者,总裁,艺术家,收藏家等。景点(scene): 常见旅游景点如:长沙公园,深圳动物园,海洋馆,植物园,黄河,长江等。

2.3 数据分布

训练集:10748 验证集:1343

按照不同标签类别统计,训练集数据分布如下(注:一条数据中出现的所有实体都进行标注,如果一条数据出现两个地址(address)实体,那么统计地址(address)类别数据的时候,算两条数据):

【训练集】标签数据分布如下:

地址(address):2829书名(book):1131公司(company):2897游戏(game):2325政府(government):1797电影(movie):1109姓名(name):3661组织机构(organization):3075职位(position):3052景点(scene):1462【验证集】标签数据分布如下:地址(address):364书名(book):152公司(company):366游戏(game):287政府(government):244电影(movie):150姓名(name):451组织机构(organization):344职位(position):425景点(scene):199

平台测试结果:

Roberta指的chinese_roberta_wwm_large模型。(roberta-wwm-large-ext)

模型 BiLSTM+CRFbert-base-chinese Roberta+Softmax Roberta+CRF Roberta+BiLSTM+CRFoverall 70/67 78.8275.90 80.4/79.3 79.64

可见,Roberta+lstm和Roberta模型差别不大。

官方处理方法:softmax、crf和span,模型本体和运行代码见:CLUENER/pytorch_version/models/albert_for_ner.py | run_ner_crf.py。

为什么使用CRF提升这么大呢? softmax最终分类,只能通过输入判断输出,但是 CRF 可以通过学习转移矩阵,看前后的输出来判断当前的输出。这样就能学到一些规律(比如“O 后面不能直接接 I”“B-brand 后面不可能接 I-color”),这些规律在有时会起到至关重要的作用。

例如下面的例子,A 是没加 CRF 的输出结果,B 是加了 CRF 的输出结果,一看就懂不细说了

三、处理json文件,转成BIOS标注

本文选取Roberta+lstm+lstm,标注方法选择BIOS。

“B”:(实体开始的token)前缀

“I” :(实体中间的token)前缀

“O”:无特别实体(no special entity)

“S”: 即Single,“S-X”表示该字单独标记为X标签

另外还有BIO、BIOE(“E-X”表示该字是标签X的词片段末尾的终止字)等。

3.1 分词和标签预处理

NER作为序列标注任务,输出需要确定实体边界和类型。如果预先进行了分词处理,由于分词工具原本就无法保证绝对正确的分词方案,势必会产生错误的分词结果,而这将进一步影响序列标注结果。因此,我们不进行分词,在字层面进行BIOS标注。

我们采用BIOS标注对原始标签进行转换。范例:

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

转换结果为:

['B-company', 'I-company', 'I-company', 'I-company', 'O', 'O', 'O', 'O', 'O', 'B-name', 'I-name', 'I-name', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'B-name', 'I-name', 'I-name', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']

这部分处理代码参考《hemingkx/CLUENER》

定义config文件:

import osimport torchdata_dir='./data/'train_dir=data_dir+'train.npz'test_dir=data_dir+'test.npz'files=['train','test']bert_model='bert-base-chinese'roberta_model='hfl/chinese-roberta-wwm-ext-large'case_dir=os.getcwd()+'/case/bad_case.txt'# 训练集、验证集划分比例dev_split_size=0.1# 是否加载训练好的NER模型load_before = False# 是否对整个BERT进行fine tuningfull_fine_tuning = True# hyper-parameterlearning_rate = 3e-5weight_decay = 0.01clip_grad = 5batch_size = 32epoch_num = 50min_epoch_num = 5patience = 0.0002patience_num = 10gpu = '1'if gpu != '':device = torch.device(f"cuda:{gpu}")else:device = torch.device("cpu")labels = ['address', 'book', 'company', 'game', 'government','movie', 'name', 'organization', 'position', 'scene']label2id = {"O": 0,"B-address": 1,"B-book": 2,"B-company": 3,'B-game': 4,'B-government': 5,'B-movie': 6,'B-name': 7,'B-organization': 8,'B-position': 9,'B-scene': 10,"I-address": 11,"I-book": 12,"I-company": 13,'I-game': 14,'I-government': 15,'I-movie': 16,'I-name': 17,'I-organization': 18,'I-position': 19,'I-scene': 20,"S-address": 21,"S-book": 22,"S-company": 23,'S-game': 24,'S-government': 25,'S-movie': 26,'S-name': 27,'S-organization': 28,'S-position': 29,'S-scene': 30}id2label = {_id: _label for _label, _id in list(label2id.items())}

导入config包,然后进行json文件处理。

import osimport jsonimport loggingimport numpy as npimport pandas as pdimport configfrom sklearn.model_selection import train_test_splitfrom torch.utils.data import DataLoader

# 定义process函数,将原始标签替换为BIOS标签,并与token(字)一一对应# 处理完的word_list BIOS_label_list存储为npz格式def process(mode):data_dir='./data/'input_dir=data_dir+str(mode)+'.json'output_dir=data_dir+str(mode)+'.npz'if os.path.exists(output_dir) is True:returnword_list=[]label_list=[]with open(input_dir,'r',encoding='utf-8') as f:# 先读取到内存中,然后逐行处理for line in f.readlines():# loads():用于处理内存中的json对象,strip去除可能存在的空格json_line = json.loads(line.strip())text=json_line['text']words=list(text)# 如果没有label,则返回Nonelabel_entities = json_line.get('label', None)labels=['O']*len(words)if label_entities is not None:for key, value in label_entities.items():for sub_name, sub_index in value.items():for start_index, end_index in sub_index:assert ''.join(words[start_index:end_index+1]) == sub_nameif start_index == end_index:labels[start_index]='S-'+ keyelse:labels[start_index]='B-'+ keylabels[start_index+1:end_index+1]=['I-'+key]*(len(sub_name)-1)word_list.append(words)label_list.append(labels)# 保存成二进制文件np.savez_compressed(output_dir,words=word_list,labels=label_list)logging.info("--------{} data process DONE!--------".format(mode))

依次处理三个json文件,得到三个处理好的npz文件。

#处理训练集和验证集数据mode1,mode2,mode3='train','dev','test'train_data=process(mode1)#45min处理时间dev_data=process(mode2)test_data=process(mode3)

四、数据预处理,装入dataloader

4.1 pandas读取npz文件,将BIOS标注转成索引

#加载处理完的npz数据集#不加allow_pickle=True会报错Object arrays cannot be loaded when allow_pickle=False,numpy新版本中默认为False。train_data=np.load('./data/train.npz',allow_pickle=True)val_data=np.load('./data/dev.npz',allow_pickle=True)test_data=np.load('./data/test.npz',allow_pickle=True)

#转换为dataframe格式import pandas as pd#补个随机fractrain_df=pd.concat([pd.DataFrame(train_data['words'],columns=['words']),pd.DataFrame(train_data['labels'],columns=['labels'])],axis=1)val_df=pd.concat([pd.DataFrame(val_data['words'],columns=['words']),pd.DataFrame(val_data['labels'],columns=['labels'])],axis=1)test_df=pd.concat([pd.DataFrame(test_data['words'],columns=['words']),pd.DataFrame(test_data['labels'],columns=['labels'])],axis=1)#将训练验证集的BIOS标签转换为数字索引,此时word和labels已经对齐了def trans(labels):labels=list(labels)nums=[]for label in labels:nums.append(config.label2id[label])return numstrain_df['labels']=train_df['labels'].map(lambda x: trans(x))val_df['labels']=val_df['labels'].map(lambda x: trans(x))test_df['labels']=test_df['labels'].map(lambda x: trans(x))val_df

words labels0[彭, 小, 军, 认, 为, ,, 国, 内, 银, 行, 现, 在, 走, 的, 是, ...[7, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...1[温, 格, 的, 球, 队, 终, 于, 又, 踢, 了, 一, 场, 经, 典, 的, ...[7, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...2[突, 袭, 黑, 暗, 雅, 典, 娜, 》, 中, R, i, d, d, i, c, ...[4, 14, 14, 14, 14, 14, 14, 14, 0, 7, 17, 17, ...3[郑, 阿, 姨, 就, 赶, 到, 文, 汇, 路, 排, 队, 拿, 钱, ,, 希, ...[0, 0, 0, 0, 0, 0, 1, 11, 11, 0, 0, 0, 0, 0, 0...4[我, 想, 站, 在, 雪, 山, 脚, 下, 你, 会, 被, 那, 巍, 峨, 的, ...[0, 0, 0, 0, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0............1338[在, 这, 个, 非, 常, 喜, 庆, 的, 日, 子, 里, ,, 我, 们, 首, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...1339[姜, 哲, 中, :, 公, 共, 之, 敌, 1, -, 1, 》, 、, 《, 神, ...[6, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16...1340[目, 前, ,, 日, 本, 松, 山, 海, 上, 保, 安, 部, 正, 在, 就, ...[0, 0, 0, 5, 15, 15, 15, 15, 15, 15, 15, 15, 0...1341[也, 就, 是, 说, 英, 国, 人, 在, 世, 博, 会, 上, 的, 英, 国, ...[0, 0, 0, 0, 0, 0, 0, 0, 10, 20, 20, 0, 0, 0, ...1342[另, 外, 意, 大, 利, 的, P, l, a, y, G, e, n, e, r, ...[0, 0, 0, 0, 0, 0, 2, 12, 12, 12, 12, 12, 12, ...1343 rows × 2 columns

4.2 将labels统一填充到句子最大长度52,以便装入dataloader。

关于labels的长度不一致,也可以在整理函数里面写填充。一开始没写出来,后来pandas已经处理完了可以跑了,就没有再试。应该是涉及到pytorch的张量运算。

from datasets import Datasetfrom transformers import AutoTokenizer#这里一定要选AutoTokenizer,如果是BertTokenizer,会提示bertbase没有word_ids方法。结果没用到trains_ds=Dataset.from_pandas(train_df)val_ds=Dataset.from_pandas(val_df)test_ds=Dataset.from_pandas(test_df)tokenizer=AutoTokenizer.from_pretrained(config.roberta_model,do_lower_case=True)#tokenized_inputs=tokenizer(trains_ds["words"],padding=True,truncation=True,is_split_into_words=True)为啥这种是错的tokenized_trains_ds=trains_ds.map(lambda examples:tokenizer(examples['words'],is_split_into_words=True,truncation=True,padding=True),batched=True)tokenized_val_ds=val_ds.map(lambda examples:tokenizer(examples['words'],is_split_into_words=True,truncation=True,padding=True),batched=True)tokenized_test_ds=test_ds.map(lambda examples:tokenizer(examples['words'],is_split_into_words=True,truncation=True,padding=True),batched=True)

#在编码之后的datasets里面操作,得到的结果无法写入datasets,所以只好写到pandas文件里。#将labels用-100来填充到和input_ids一样长(最长句子52,所以其实全部都填充到52)def padding(data):pad_labels=[]for ds in data:labels=ds['labels'] mask=ds['attention_mask']label_ids=[-100]pad_length=len(mask)label_length=len(labels)label_ids=label_ids+labels+[-100]*(pad_length-label_length-1)pad_labels.append(label_ids)return pad_labels#tokenized_trains_ds["pad_labels"]=pad_labels# Column 2 named labels expected length 10748 but got length 1000train_df['pad_labels']=padding(tokenized_trains_ds)#val_df['pad_labels']=padding(tokenized_val_ds)test_df['pad_labels']=padding(tokenized_test_ds)test_df

测试训练集句子长度

%pylab inline#最大句子长度50train_df['text_len'] = train_df['words'].apply(lambda x: len(x))print(train_df['text_len'].describe())

Populating the interactive namespace from numpy and matplotlibcount 10748.000000mean 37.380350std 10.709827min2.00000025% 32.00000050% 41.00000075% 46.000000max 50.000000Name: text_len, dtype: float64

#每个句子都被pad到52的长度train_df['label_len'] = train_df['pad_labels'].apply(lambda x: len(x))print(train_df['label_len'].describe())count 10748.0mean 52.0std0.0min 52.025% 52.050% 52.075% 52.0max 52.0Name: label_len, dtype: float64

4.3 划分数据集,设置batch_size,数据装入dataloader

batch_size=32#划分训练验证集from sklearn.model_selection import train_test_splitfrom datasets import Datasetfrom torch.nn.utils.rnn import pad_sequencetrain_data,val_data,train_label,val_label=train_test_split(train_df['words'].iloc[:], train_df['pad_labels'].iloc[:],test_size=0.15,shuffle=True)test_data,test_label=(test_df['words'].iloc[:],test_df['pad_labels'].iloc[:])validation_data,validation_label=(val_df['words'].iloc[:],val_df['pad_labels'].iloc[:])#stratify=train_df['label'].iloc[:]报错:The least populated class in y has only 1 member,which is too few.#The minimum number of groups for any class cannot be less than 2.估计是样本太少,分层抽取不可行。#数据预处理tokenizer=AutoTokenizer.from_pretrained(config.roberta_model,do_lower_case=True)train_encoding=tokenizer(list(train_data),is_split_into_words=True,truncation=True,padding=True,return_tensors='pt')#训练集中划分的训练集val_encoding=tokenizer(list(val_data),is_split_into_words=True,truncation=True,padding=True,return_tensors='pt')#训练集中划分的验证集test_encoding=tokenizer(list(test_data),is_split_into_words=True,truncation=True,padding=True,return_tensors='pt')#测试集validation_econding=tokenizer(list(validation_data),is_split_into_words=True,truncation=True,padding=True,return_tensors='pt')#原本的验证集

中间test_loader数据被我照抄的时候shuffle,结果预测结果完全不对,坑。

#加载到datalodar并预处理#数据集读取from torch.utils.data import Dataset, DataLoader,TensorDatasetimport torchclass XFeiDataset(Dataset):def __init__(self,encodings,labels):self.encodings=encodingsself.labels=labels# 读取单个样本def __getitem__(self,idx):item={key:torch.tensor(val[idx]) for key,val in self.encodings.items()}item['pad_labels']=torch.tensor((self.labels[idx]))item['mask']=(item['pad_labels']!=-100)return itemdef __len__(self):return len(self.labels)#def collate_fntrain_dataset=XFeiDataset(train_encoding,list(train_label))val_dataset=XFeiDataset(val_encoding,list(val_label))test_dataset=XFeiDataset(test_encoding,list(test_label))validation_dataset=XFeiDataset(validation_econding,list(validation_label))from torch.utils.data import Dataset,DataLoader,TensorDatasettrain_loader=DataLoader(train_dataset,batch_size=batch_size,shuffle=True)val_loader=DataLoader(val_dataset,batch_size=batch_size,shuffle=True)test_loader=DataLoader(test_dataset,batch_size=batch_size,shuffle=False)#test数据不能shuffle啊,真坑死我了validation_loader=DataLoader(validation_dataset,batch_size=batch_size,shuffle=False)#test数据不能shuffle啊,真坑死我了

可以选取数据打印看看

for examples in validation_loader:print(examples,len(examples))#输出是一个五元字典,mask矩阵可以用来帅选有效的词

五、定义bert模型、优化器、和训练部分

5.1 定义bert模型本体

本来想用bert+lstm+crf来做。结果装的crf一直import不了,用softmax做的。crf还没试

from transformers import BertModelfrom torch.nn.utils.rnn import pad_sequence#初始化bert模型from transformers import BertConfigimport torch.nn as nnfrom torch.nn import LSTMfrom torch.nn import functional as F #from torchcrf import CRFnum_labels=31dropout=0.1class Bert_LSTM(nn.Module):def __init__(self):super(Bert_LSTM,self).__init__()self.num_labels=num_labelsself.dropout=nn.Dropout(dropout)self.bert=BertModel.from_pretrained(config.roberta_model)for param in self.bert.parameters():param.requires_grad=Trueself.classifier=nn.Linear(1024,self.num_labels)#self.crf=CRF(num_labels,batch_first=True)from torch.nn import functional as Fself.bilstm=nn.LSTM(input_size=1024, hidden_size=512, batch_first=True,num_layers=2,dropout=0.5, bidirectional=True)def forward(self,batch_seqs,batch_seq_masks,batch_seq_segments):output=self.bert(input_ids=batch_seqs,attention_mask=batch_seq_masks,token_type_ids=batch_seq_segments)#pooler_output=output.pooler_outputlast_hidden_state=output.last_hidden_stateif model.train():last_hidden_state=self.dropout(last_hidden_state)#只有这种写法不会报错,如果是sequence_output,pooler_output=self.bert(**kwags)这种,sequence_output会报错str没有xxx属性。#貌似是bert输出有很多,直接用output.last_hidden_state来调用结果(估计是版本问题,坑),关键是输出要打印出来lstm_output,(hn,cn)=self.bilstm(last_hidden_state)#output为输出序列的隐藏层,hn为最后一个时刻的隐藏层,cn为最后一个时刻的隐藏细胞if model.train():lstm_output=self.dropout(lstm_output)# 得到判别值logits=self.classifier(lstm_output)log_probs = F.log_softmax(logits,dim=-1)return log_probs

加载模型

model=Bert_LSTM()#model.load_state_dict(torch.load("best_bert_model_3epoch"))device=torch.device("cuda" if torch.cuda.is_available() else "cpu")model.to(device)

5.2 定义优化器

epoch=10lr=3e-5from transformers import AdamW,get_schedulertrain_steps_per_epoch=len(train_loader)num_training_steps=train_steps_per_epoch*epoch#定义各模块参数bert_parameters=list(model.bert.named_parameters())lstm_parameters=list(model.bilstm.named_parameters())classifier_parameters=list(model.classifier.named_parameters())no_decay=['bias','LayerNorm.weight']#bert模型、lstm模型、nn.linear的学习率分离,后两个是bert的3倍optimizer_grouped_parameters=[{'params':[p for n,p in bert_parameters if not any(nd in n for nd in no_decay)],'lr':lr,'weight_decay':0.01},{'params':[p for n,p in bert_parameters if any(nd in n for nd in no_decay)],'lr':lr,'weight_decay':0.0},{'params':[p for n,p in lstm_parameters if not any(nd in n for nd in no_decay)],'lr':lr*3,'weight_decay':0.01},{'params':[p for n,p in lstm_parameters if any(nd in n for nd in no_decay)],'lr':lr*3,'weight_decay': 0.0},{'params':[p for n,p in classifier_parameters if not any(nd in n for nd in no_decay)],'lr':lr*3,'weight_decay':0.01},{'params':[p for n,p in classifier_parameters if any(nd in n for nd in no_decay)],'lr':lr*3,'weight_decay':0.0}]optimizer=AdamW(optimizer_grouped_parameters,lr=lr,eps=1e-8)lr_scheduler=get_scheduler("linear",optimizer=optimizer,num_warmup_steps=0,num_training_steps=num_training_steps)

5.3 编写训练和验证循环

import timeimport numpy as npfrom sklearn.metrics import f1_score,precision_score,recall_score,accuracy_scorefrom torch.nn import functional as F#加载进度条from tqdm.auto import tqdmnum_training_steps=train_steps_per_epoch*epochprogress_bar=tqdm(range(num_training_steps))def train_and_eval(epoch):best_acc=0.0#criterion=nn.CrossEntropyLoss()criterion=nn.NLLLoss()#不带softmax的损失函数for i in range(epoch):"""训练模型"""start=time.time()model.train()print("***** Running training epoch {} *****".format(i+1))train_loss_sum=0.0for idx,batch in enumerate(train_loader):input_ids=batch['input_ids'].to(device)attention_mask=batch['attention_mask'].to(device)token_type_ids=batch['token_type_ids'].to(device)pad_labels=batch['pad_labels'].to(device)mask=batch['mask'].to(device)#计算输出和losslogits=model(input_ids,attention_mask,token_type_ids)loss=criterion(logits[mask],pad_labels[mask])loss.backward()optimizer.step()lr_scheduler.step()optimizer.zero_grad() progress_bar.update(1)train_loss_sum+=loss.item()if (idx+1)%(len(train_loader)//5)==0: # 只打印五次结果print("Epoch {:03d} | Step {:04d}/{:04d} | Loss {:.4f} | Time {:.4f} | Learning rate = {} \n".format(i+1,idx+1,len(train_loader),train_loss_sum/(idx+1),time.time()-start,optimizer.state_dict()['param_groups'][0]['lr']))#验证模型model.eval()total=0#每个batch要预测的token总数acc=0#每个batch的acctotal_eval_accuracy=0total_eval_loss=0for batch in val_loader:with torch.no_grad():#只有这一块是不需要求导的input_ids=batch['input_ids'].to(device)attention_mask=batch['attention_mask'].to(device)token_type_ids=batch['token_type_ids'].to(device)pad_labels=batch['pad_labels'].to(device)mask=batch['mask'].to(device)logits=model(input_ids,attention_mask,token_type_ids)#logits[mask]从句子矩阵变被拉平,且只含有真实token的logtis。和bertfortoken分类任务头的view效果是一样的。loss=criterion(logits[mask],pad_labels[mask])#只计算没有mask的部分单词的loss和准确率total_eval_loss+=loss.item()acc+=(logits.argmax(dim=-1)==pad_labels)[mask].sum().item()#只计算没有mask的单词的准确率total+=mask.sum().item()total_eval_accuracy=acc/total#avg_val_accuracy=total_eval_accuracy/len(val_loader)if total_eval_accuracy>best_acc:best_acc=total_eval_accuracytorch.save(model.state_dict(),"after_test_bert_lstm_softmax_model")print("val_accuracy:%.4f" % (total_eval_accuracy))print("Average val loss: %.4f"%(total_eval_loss))print("time costed={}s \n".format(round(time.time()-start,5)))print("-------------------------------")

开始训练

train_and_eval(epoch)

验证集准确率0.934,老感觉哪里不对,是不是准确率没有舍掉pad部分,但是不应该啊。

5.4 编写predict函数

#编写predict函数def predict(model,data_loader):#参数名为data时加载训练好的模型来预测报错,原模型不报错model.eval()test_pred = []for batch in data_loader:with torch.no_grad():input_ids=batch['input_ids'].to(device)attention_mask=batch['attention_mask'].to(device)token_type_ids=batch['token_type_ids'].to(device)mask=batch['mask'].to(device)logits=model(input_ids,attention_mask,token_type_ids)pad_logits=logits[mask]y_pred=torch.argmax(logits,dim=-1).detach().cpu().numpy()#为啥最后拉平的又变回矩阵了,看不懂啊test_pred.extend(y_pred)return test_pred

试了一下,用mask矩阵,由于过滤之后句子长度不一致,二维token矩阵会被拉成一维,不知道为啥最后预测的结果还可以是长52的labels矩阵。这一点没想明白。

import torchfrom torch import tensora=torch.randn(2,4)b=tensor([[False,True,True,False],[True,False,True,True]])c=a[b]print(a,a.shape)print(c,c.shape)tensor([[-0.2196, 0.1262, -0.6929, -1.7824],[-0.4014, -0.5301, -0.6155, 0.6116]]) torch.Size([2, 4])tensor([ 0.1262, -0.6929, -0.4014, -0.6155, 0.6116]) torch.Size([5])

5.5 用模型预测验证集结果,与原标签对比

#用trainer预测验证集结果并保存#torch.save(model.state_dict(),"best_lstm_whole_4epoch")#model.load_state_dict(torch.load("after_test_bert_lstm_softmax_model"))#model.to(device)predictions=predict(model,validation_loader)val_df['pre_labels']=pd.Series(predictions)from datasets import Datasetval_datasets=Dataset.from_pandas(val_df)#将预测的结果直接加到val_df。如果存入csv读取出来再加入,读取的labels数据就是文本数据,坑了好久才发现。而且还有换行符,醉了

懒得写pandas列间运算,这里抄前面datasets的填充处理,将填充的无效部分labels去掉

def unpadding(data):unpad_labels=[]for ds in data:#直接这样迭代读取报错。pandas数据不能这样读取每一行。datasets是dict格式,可以用datasets['train'][0]这样的方式读取pad_labels=ds['pre_labels'] #这里是pre_labels,又他妈写错了words=ds['words']length=len(words)label_ids=pad_labels[1:(length+1)]unpad_labels.append(label_ids)return unpad_labels#tokenized_trains_ds["pad_labels"]=pad_labels# Column 2 named labels expected length 10748 but got length 1000val_df['unpad_labels']=unpadding(val_datasets)val_df

val_df.drop(columns=(['pad_labels','pre_labels',]),inplace=True)val_df.to_csv('bert_lstm_validation_1113.csv')val_df

words labelsunpad_labels0[彭, 小, 军, 认, 为, ,, 国, 内, 银, 行, 现, 在, 走, 的, 是, ...[7, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...[7, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...1[温, 格, 的, 球, 队, 终, 于, 又, 踢, 了, 一, 场, 经, 典, 的, ...[7, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...[7, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...2[突, 袭, 黑, 暗, 雅, 典, 娜, 》, 中, R, i, d, d, i, c, ...[4, 14, 14, 14, 14, 14, 14, 14, 0, 7, 17, 17, ...[4, 14, 14, 14, 14, 14, 14, 14, 0, 7, 17, 17, ...3[郑, 阿, 姨, 就, 赶, 到, 文, 汇, 路, 排, 队, 拿, 钱, ,, 希, ...[0, 0, 0, 0, 0, 0, 1, 11, 11, 0, 0, 0, 0, 0, 0...[0, 0, 0, 0, 0, 0, 1, 11, 11, 0, 0, 0, 0, 0, 0...4[我, 想, 站, 在, 雪, 山, 脚, 下, 你, 会, 被, 那, 巍, 峨, 的, ...[0, 0, 0, 0, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0...[0, 0, 0, 0, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0...............1338[在, 这, 个, 非, 常, 喜, 庆, 的, 日, 子, 里, ,, 我, 们, 首, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...1339[姜, 哲, 中, :, 公, 共, 之, 敌, 1, -, 1, 》, 、, 《, 神, ...[6, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16...[7, 17, 17, 0, 6, 16, 16, 16, 16, 16, 16, 16, ...1340[目, 前, ,, 日, 本, 松, 山, 海, 上, 保, 安, 部, 正, 在, 就, ...[0, 0, 0, 5, 15, 15, 15, 15, 15, 15, 15, 15, 0...[0, 0, 0, 5, 15, 15, 15, 15, 15, 15, 15, 15, 0...1341[也, 就, 是, 说, 英, 国, 人, 在, 世, 博, 会, 上, 的, 英, 国, ...[0, 0, 0, 0, 0, 0, 0, 0, 10, 20, 20, 0, 0, 0, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 20...1342[另, 外, 意, 大, 利, 的, P, l, a, y, G, e, n, e, r, ...[0, 0, 0, 0, 0, 0, 2, 12, 12, 12, 12, 12, 12, ...[0, 0, 1, 0, 0, 0, 2, 12, 12, 12, 12, 12, 12, ...1343 rows × 3 columns

5.6 生成test数据集结果

test_predictions=predict(model,test_loader)test_df['pre_labels']=pd.Series(test_predictions)test_datasets=Dataset.from_pandas(test_df)test_df['unpad_labels']=unpadding(test_datasets)test_df.drop(columns=(['labels','pad_labels','pre_labels']),inplace=True)test_df

words labels unpad_labels0[彭, 小, 军, 认, 为, ,, 国, 内, 银, 行, 现, 在, 走, 的, 是, ...[7, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...[7, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...1[温, 格, 的, 球, 队, 终, 于, 又, 踢, 了, 一, 场, 经, 典, 的, ...[7, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...[7, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...2[突, 袭, 黑, 暗, 雅, 典, 娜, 》, 中, R, i, d, d, i, c, ...[4, 14, 14, 14, 14, 14, 14, 14, 0, 7, 17, 17, ...[4, 14, 14, 14, 14, 14, 14, 14, 0, 7, 17, 17, ...3[郑, 阿, 姨, 就, 赶, 到, 文, 汇, 路, 排, 队, 拿, 钱, ,, 希, ...[0, 0, 0, 0, 0, 0, 1, 11, 11, 0, 0, 0, 0, 0, 0...[0, 0, 0, 0, 0, 0, 1, 11, 11, 0, 0, 0, 0, 0, 0...4[我, 想, 站, 在, 雪, 山, 脚, 下, 你, 会, 被, 那, 巍, 峨, 的, ...[0, 0, 0, 0, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0...[0, 0, 0, 0, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0...............1338[在, 这, 个, 非, 常, 喜, 庆, 的, 日, 子, 里, ,, 我, 们, 首, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...1339[姜, 哲, 中, :, 公, 共, 之, 敌, 1, -, 1, 》, 、, 《, 神, ...[6, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16...[7, 17, 17, 0, 6, 16, 16, 16, 16, 16, 16, 16, ...1340[目, 前, ,, 日, 本, 松, 山, 海, 上, 保, 安, 部, 正, 在, 就, ...[0, 0, 0, 5, 15, 15, 15, 15, 15, 15, 15, 15, 0...[0, 0, 0, 5, 15, 15, 15, 15, 15, 15, 15, 15, 0...1341[也, 就, 是, 说, 英, 国, 人, 在, 世, 博, 会, 上, 的, 英, 国, ...[0, 0, 0, 0, 0, 0, 0, 0, 10, 20, 20, 0, 0, 0, ...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 20...1342[另, 外, 意, 大, 利, 的, P, l, a, y, G, e, n, e, r, ...[0, 0, 0, 0, 0, 0, 2, 12, 12, 12, 12, 12, 12, ...[0, 0, 1, 0, 0, 0, 2, 12, 12, 12, 12, 12, 12, ...1343 rows × 3 columns

最后部分索引转成BIOS再转成json文件还没有做。那个平台登录不上去,无法提交结果。

六. 总结(pytorch还要练啊)

一开始纠结于bert的特殊字符处有没有输出最终的词向量,lstm的处理中有没有特殊字符eos等等。打印输出就折腾了好久,最终是读取一个batch数据进行测试。并对比bert模型和bertfortoken分类模型的输出,后者就是过了一个nn.linear层。

总感觉特殊字符输出的bert词向量,输入lstm后对最终结果有影响。上次做句子分类没有处理。这次准备bert输出的last_hidden_state,根据实际tokens去掉特殊字符处的值。然后变长序列进过pad_sentences后,再经过pack_padded_sequence打包压缩,去掉pad_sentences函数pad的字符。最后pad_packed_sequence将序列恢复到原来的长度。

虽然最后没折腾出来,但是理解了三个函数。中间直接用一个batch的数组做的测试直接用别人的代码跑就好。

最后是pytorch的dataset和dataloader不熟悉,结果吃了大亏。

dataset一开始读取错误,之前每个标签是一个值,token分类是一个列表。照抄dataset类,list无法转成int。也不熟悉dataset类,对于最后装进dataset的数据,一直不知道怎么取值。(其实就是字典,一开始囫囵吞枣)list应该是可以直接转为tensorlabels变长必须处理。单纯的LSTM是句子索引和label一起pad_sentences。我的是encoding等长,labels变长要pad到一样长度。bertfortoken的整理函数太复杂,也用不了。4.2词性标注教程的word_ids只是将cls和sep替换成-100,无济于事。bertfortoken任务头是将一个batch的数据拉平成一个超长序列的句子,用torch.where根据attention_mask==1来取值,去掉pad部分,感觉也用不了。因为句子词向量还要输入下一个模型一开始在dataloader整理函数里写pad太麻烦,后来才意识到是对张量处理不熟。最后是直接在datasets里面处理,将labels填充为一样长的pad_labels。(datasets不能新增列,只能输入padans,最后装入dataloader)自己偷懒预,测的结果存入csv再读取,已经是字符串了。

如果觉得《CLUENER 细粒度命名实体识别 附完整代码》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。