本文概述:复现知乎-KG开源项目集中的BERT-NER-pytorch项目之后,进行的一些学习记录,对同样刚入行的小白来说有参考意义。

资料:关于BERT模型中的transformer介绍,必须分享的是Jay Alammar的动画图,看完后我捶胸顿首的为什么没有早早看到这样的国外佳作?


一、准备工作:

1、数据集

数据集的组织方式、处理方式都是深度领域的重头戏,可以说一个算法工程师80%的时间都在和数据打交道。现在我们来介绍一下数据集:

  • 获取方式:直接用google扩展gitzip从git直接下载的,

生数据只有3项,是
image.png
其中train集里有45000个句子,test集是3442,我们需要人为划分出val集。

  • 生数据形式-类似下面这个样子,我们来看一下msra_train_bio的前17行(一共176042行):
中    B-ORG
共    I-ORG
中    I-ORG
央    I-ORG
致    O
中    B-ORG
国    I-ORG
致    I-ORG
公    I-ORG
党    I-ORG
十    I-ORG
一    I-ORG
大    I-ORG
的    O
贺    O
词    O
各    O
  • tags(只有三种实体:机构,人,位置):
O
B-ORG
I-PER
B-PER
I-LOC
I-ORG
B-LOC

ps:可以看到,采用的是BIO标注法,我们当然可以修改!

  • 待会儿要划分数据集为(3个目录):
DatasetNumber
training set42000
validation set3000
test set3442

得到三个目录?。

  • 数据处理后的形式(各取前两条):

sentences.txt文件:

如 何 解 决 足 球 界 长 期 存 在 的 诸 多 矛 盾 , 重 振 昔 日 津 门 足 球 的 雄 风 , 成 为 天 津 足 坛 上 下 内 外 到 处 议 论 的 话 题 。
该 县 一 手 抓 农 业 技 术 推 广 , 一 手 抓 农 民 科 技 教 育 和 农 技 水 平 的 提 高 。
而 创 新 的 关 键 就 是 知 识 和 信 息 的 生 产 、 传 播 、 使 用 。

相应的,tags.txt文件:

O O O O O O O O O O O O O O O O O O O O O B-LOC I-LOC O O O O O O O O B-LOC I-LOC O O O O O O O O O O O O O O
O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O
O O O O O O O O O O O O O O O O O O O O O O O

2、准备模型和训练好的模型参数

在实验中尝试过多次之后,发现模型参数获取其实也不难,
作者复现代码的时候,没有直接可获取的pt下的模型参数,现在就是一个参数的事。
train.py代码中,创建model时候有这么一句:

model = BertForTokenClassification.from_pretrained()

我们点进去查看from_pretrained()方法,在pytorch_pretrained_bert目录下的modeling.py文件中。
可以通过路径名或者url来获取模型和参数(后来训练中有一点小插曲,我就舍弃了作者提供的,自己从里面下载了压缩包,后面会提到):

PRETRAINED_MODEL_ARCHIVE_MAP = {
    'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased.tar.gz",
    'bert-large-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased.tar.gz",
    'bert-base-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-cased.tar.gz",
    'bert-large-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased.tar.gz",
    'bert-base-multilingual-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-uncased.tar.gz",
    'bert-base-multilingual-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-cased.tar.gz",
    'bert-base-chinese': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese.tar.gz",
}

ps:先获取tf的模型参数再转换为pt,费了我好大劲,直接从Transformer库下载了一个convert_tf_checkpoint_to_pytorch.py文件到tf模型参数目录下,操作了一番得到了pt的.bin文件,好在现在一句话的事。

3、train.py文件解读

数据集的处理和超参数的设置简单,不涉及到我们文章的核心,这里就不做过多记录,想了解则去看github项目,传送门
这个文件放的东西比较多,有
①parse,params设置,logger日志
②dataloader,model,optimizer和train_and_evaluate

  • 参数parse(这个参数解析是运行.py文件时候的参数)
parser = argparse.ArgumentParser()
parser.add_argument('--data_dir', default='NER-BERT-pytorch-data-msra', help="Directory containing the dataset")
parser.add_argument('--bert_model_dir', default='pt_things', help="Directory containing the BERT model in PyTorch")
……

之后在main中,记录参数到内存:
args = parser.parse_args()
在接下来的使用中通过args.param来获取参数,

  • logger日志

相关的处理放在了utils.py中,在train.py中直接:

#创建
utils.set_logger(os.path.join(args.model_dir, 'train.log'))
# 需要记录的时候:
logging.info("device: {}, n_gpu: {}, 16-bits training: {}".format(params.device, params.n_gpu, args.fp16))
  • model

简单,两句话:

model = BertForTokenClassification.from_pretrained(args.bert_model_dir, num_labels=len(params.tag2idx))
model.to(params.device)

里边包括模型的加载和参数的加载,在训练时我们在看到modelconfig之后,还会看到两句提示:

Weights of BertForTokenClassification not initialized from pretrained model: ['classifier.weight', 'classifier.bias']
Weights from pretrained model not used in BertForTokenClassification: ['cls.predictions.bias', 'cls.predictions.transform.dense.weight', 'cls.predictions.transform.dense.bias', 'cls.predictions.decoder.weight', 'cls.seq_relationship.weight', 'cls.seq_relationship.bias', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.transform.LayerNorm.bias']

(开始我还以为这是模型参数导入失败,就去查找解决办法。

我还一度以为pytorch_model.bin文件中的参数和模型不匹配,重新从源码提供的链接下载了压缩包,但还有这两句话。

结果在Google上一个论坛发现这两句话是成功调用参数的意思。)
解谜:这个model里边包括了embedding和bert NER两个部分,其中模型的参数应该是embedding部分的,而具体的NER任务应该使用我们自己的数据来训练!
读model源码:各层详细解释
查看model(BertForTokenClassification):
有三层:
①BertModel
BertEmbeddings:里边含有多层

    (position_embeddings): Embedding(512, 768)
    (token_type_embeddings): Embedding(2, 768)
    (LayerNorm): BertLayerNorm()
    (dropout): Dropout(p=0.1, inplace=False)
BertEncoder:里边有12层encoder,每一个encoder都是一个BertLayer:
    (attention): BertAttention  #重中之重,有机会一定要刷
    (intermediate): BertIntermediate
    (output): BertOutput
BertPooler

②DropOut
③Linear

  • optimizer

full_finetuning

4、单机多卡并行和fp16

  • 多卡

要把模型和数据都分配到多卡;
①指定虚拟gpu:
os.environ["CUDA_VISIBLE_DEVICES"] = '1,2,3,0'
代表虚拟地址对应的物理地址为1,2,3,0(当时因为师兄的主gpu是物理0号卡,所以避开这个剩余显存相对较小的卡)
image.png
ps:截图时师兄已经不再用了。

②在准备模型和数据之前,放这一句:
params.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
估计'cuda'后面的部分是作者不想看到报错……
注意:如果要写'cuda:[num]',请写前面指定gpu时候的第一个,在这里就是1!

③随机种子分配到各个gpu:

# 设置可重复试验的随机种子
random.seed(args.seed)
torch.manual_seed(args.seed)    #给cpu设置
if params.n_gpu > 0:
    torch.cuda.manual_seed_all(args.seed)  # set random seed for all GPUs

ps:如果单卡,只是去掉all;

④给model分配多卡:

model = BertForTokenClassification.from_pretrained(args.bert_model_dir, num_labels=len(params.tag2idx))
model.to(params.device)
if params.n_gpu > 1 and args.multi_gpu:
    model = torch.nn.DataParallel(model)

⑤最后,给数据分配多卡:

# Initialize the DataLoader
data_loader = DataLoader(args.data_dir, args.bert_model_dir, params, token_pad_idx=0)
# Load training data and test data
train_data = data_loader.load_data('train')
val_data = data_loader.load_data('val')
……
在train()函数之前:
# data iterator for training
train_data_iterator = data_loader.data_iterator(train_data, shuffle=True)
# Train for one epoch on training set
train(model, train_data_iterator, optimizer, scheduler, params)

在data_iterator()中设置的给每个batch分配的卡:

# shift tensors to GPU if available
batch_data, batch_tags = batch_data.to(self.device), batch_tags.to(self.device)
yield batch_data, batch_tags

这里的self.device是类接受的参数。

  • fp16

参考:一篇讲fp16加速原理的CSDN
fp16使用2字节编码存储
优点:内存占用少(主)+ 加速计算
缺点:加法操作容易上下溢出
(有机会可以专门实验一下)

5、进度条工具

此处计算出每一个epoch下计算1400个batch,
所以把进度条放到每一个epoch中:

t = trange(params.train_steps)
for i in t:
    # fetch the next training batch
 batch_data, batch_tags = next(data_iterator)
 ……
 loss = model(~)
 ……
 t.set_postfix(loss='{:05.3f}'.format(loss_avg()))

结果展示:
image.png

6、评价指标库metrics

可以使用的结果评价指标(冰山一角):

from metrics import f1_score
from metrics import accuracy_score
from metrics import classification_report

模板:

metrics = {}
f1 = f1_score(true_tags, pred_tags)
accuracy=accuracy_score(true_tags, pred_tags)
metrics['loss'] = loss_avg()
metrics['f1'] = f1
metrics['accuracy']=accuracy
metrics_str = "; ".join("{}: {:05.2f}".format(k, v) for k, v in metrics.items())
logging.info("- {} metrics: ".format(mark) + metrics_str)

结果展示:
image.png

如何区分精确率和召回率?
image.png

7、多次实验存在的问题

我的f1分数一直都在50以下,但是精确率一直都在97%附近,最近因为要肝链接抽取,所以这部分先放一下把。回头补全


略多
1 声望0 粉丝

可傻了!