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

CLUENER 细粒度命名实体识别

一、任务说明:

  1. 最开始是参考知乎文章《用BERT做NER?教你用PyTorch轻松入门Roberta!》,github项目地址:《hemingkx/CLUENER2020》
  2. 任务介绍:本任务是中文语言理解测评基准(CLUE)任务之一:《CLUE Fine-Grain NER》
  3. 数据来源:本数据是在清华大学开源的文本分类数据集THUCTC基础上,选出部分数据进行细粒度命名实体标注,原数据来源于Sina News RSS.
  4. 平台github任务详情:《CLUENER 细粒度命名实体识别》
  5. CLUE命名实体任务排行榜
  6. BERT-base-X部分的代码编写思路参考 lemonhu
  7. 参考文章《中文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+CRF	bert-base-chinese   Roberta+Softmax	  Roberta+CRF	   Roberta+BiLSTM+CRF
overall	 70/67	       78.82               75.90	        80.4/79.3           79.64

可见,Roberta+lstm和Roberta模型差别不大。
官方处理方法:softmax、crf和span,模型本体和运行代码见:CLUENER2020/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/CLUENER2020》

  1. 定义config文件:
import os
import torch

data_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 tuning
full_fine_tuning = True

# hyper-parameter
learning_rate = 3e-5
weight_decay = 0.01
clip_grad = 5

batch_size = 32
epoch_num = 50
min_epoch_num = 5
patience = 0.0002
patience_num = 10

gpu = '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())}
  1. 导入config包,然后进行json文件处理。
import os
import json
import logging
import numpy as np
import pandas as pd
import config

from sklearn.model_selection import train_test_split
from 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:
      return

  word_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,则返回None
      label_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_name
              if start_index == end_index:
                labels[start_index]='S-'+ key
              else:
                labels[start_index]='B-'+ key
                labels[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))
  1. 依次处理三个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
#补个随机frac
train_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 nums
    
train_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	                                                               labels
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,...
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 Dataset
from 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 1000
train_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
#最大句子长度50
train_df['text_len'] = train_df['words'].apply(lambda x: len(x))
print(train_df['text_len'].describe())
Populating the interactive namespace from numpy and matplotlib
count    10748.000000
mean        37.380350
std         10.709827
min          2.000000
25%         32.000000
50%         41.000000
75%         46.000000
max         50.000000
Name: 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.0
mean        52.0
std          0.0
min         52.0
25%         52.0
50%         52.0
75%         52.0
max         52.0
Name: label_len, dtype: float64

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

batch_size=32

#划分训练验证集
from sklearn.model_selection import train_test_split
from datasets import Dataset
from torch.nn.utils.rnn import pad_sequence


train_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,TensorDataset
import torch
class XFeiDataset(Dataset):
  def __init__(self,encodings,labels):
    self.encodings=encodings
    self.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 item
  
  def __len__(self):
    return len(self.labels)

#def collate_fn

train_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,TensorDataset

train_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 BertModel
from torch.nn.utils.rnn import pad_sequence
#初始化bert模型
from transformers import BertConfig
import torch.nn as nn
from torch.nn import LSTM
from torch.nn import functional as F 
#from torchcrf import CRF

num_labels=31
dropout=0.1

class Bert_LSTM(nn.Module):
  def __init__(self):
    super(Bert_LSTM,self).__init__()
    self.num_labels=num_labels
    self.dropout=nn.Dropout(dropout)
    self.bert=BertModel.from_pretrained(config.roberta_model)
    for param in self.bert.parameters():
      param.requires_grad=True
    self.classifier=nn.Linear(1024,self.num_labels)
    #self.crf=CRF(num_labels,batch_first=True)
    from torch.nn import functional as F

    self.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_output
    last_hidden_state=output.last_hidden_state

    if 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=10
lr=3e-5

from transformers import AdamW,get_scheduler

train_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 time
import numpy as np
from sklearn.metrics import f1_score,precision_score,recall_score,accuracy_score
from torch.nn import functional as F
#加载进度条
from tqdm.auto import tqdm
num_training_steps=train_steps_per_epoch*epoch

progress_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.0
    for 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)


      #计算输出和loss
      logits=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的acc
    total_eval_accuracy=0
    total_eval_loss=0
    
    for 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_accuracy
      torch.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 torch
from torch import tensor
a=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 Dataset
val_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 1000
val_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	                     labels	unpad_labels
0	[,,,,,,,,,,,,,,, ...	[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_labels
0	[,,,,,,,,,,,,,,, ...	[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还要练啊)

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

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

    • 虽然最后没折腾出来,但是理解了三个函数。中间直接用一个batch的数组做的
    • 测试直接用别人的代码跑就好。
  3. 最后是pytorch的dataset和dataloader不熟悉,结果吃了大亏。

    • dataset一开始读取错误,之前每个标签是一个值,token分类是一个列表。照抄dataset类,list无法转成int。也不熟悉dataset类,对于最后装进dataset的数据,一直不知道怎么取值。(其实就是字典,一开始囫囵吞枣)
    • list应该是可以直接转为tensor
    • labels变长必须处理。单纯的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再读取,已经是字符串了。
  • 3
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

神洛华

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

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

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

打赏作者

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

抵扣说明:

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

余额充值