Theme NexT works best with JavaScript enabled
0%

深度学习模型编写范式

^ _ ^

本文基于代码基于pytorch库.

Step1: 读取配置

template

1
2
3
4
5
6
7
8
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("-t", "--task_name", default=None, type=str, required=True,
help="The name of the task to train selected in the list: ")
parser.add_argument('--markup', default='bios', type=str, choices=['bios', 'bio'])
parser.add_argument("--do_train", action="store_true", help="Whether to run training.")
args = parser.parse_args()

以上参数均可通过shell脚本进行传入

Step2: 构建一个日志记录器

日志记录器最好可以同时写控制台和文件

definition template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# common.py
import logging

# global variable
logger = logging.getLogger()

def init_logger(log_file=None, log_file_level=logging.NOTSET):
'''
Example:
>>> init_logger(log_file)
>>> logger.info("abc'")
'''
if isinstance(log_file,Path):
log_file = str(log_file)
log_format = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
datefmt='%m/%d/%Y %H:%M:%S')

logger = logging.getLogger()
logger.setLevel(logging.INFO)
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_format)
logger.handlers = [console_handler]
if log_file and log_file != '':
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(log_file_level)
# file_handler.setFormatter(log_format)
logger.addHandler(file_handler)
return logger

use template

1
2
3
4
5
from common import logger
import time

time_ = time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime())
init_logger(log_file=args.output_dir + f'/{args.model_type}-{args.task_name}-{time_}.log')

Step3: GPU设置

Setup CUDA, GPU & distrubuted training

基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 为保证多次训练结果一致, 需要设置确定的随机数种子
def seed_everything(seed=1029):
'''
设置整个开发环境的seed
:param seed:
:param device:
:return:
'''
random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
# some cudnn methods can be random even after fixing the seed
# unless you tell it to be deterministic
torch.backends.cudnn.deterministic = True

seed_everything(args.seed)

# cpu or gpu
# local_rank 表示用于训练的gpu
if args.local_rank == -1 or args.no_cuda:
device = torch.device("cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
else:
torch.cuda.set_device(args.local_rank)
device = torch.device("cuda", args.local_rank)

拓展 – 分布式GPU

可以使用 torch.distributed 包进行单机多卡的GPU分布式训练.

1
2
3
4
5
# 几个常见函数, 具体使用方法没有弄懂, 仅作罗列
torch.distributed.init_process_group(backend="nccl")
torch.distributed.barrier()
torch.distributed.get_rank()
torch.distributed.get_world_size()

Step4: 数据准备

一般而言, 不同的数据集需要的处理方法是不同的, 因此可以通过实现不同的Processor类来分别完成各个数据集的处理工作.

processor类调用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# processor.py
# global
ner_processors = {
"cner": CnerProcessor,
'cluener':CluenerProcessor
}

# main.py
from processor import ner_processor as processors
args.task_name = args.task_name.lower()
if args.task_name not in processors:
raise ValueError("Task not found: %s" % (args.task_name))
processor = processors[args.task_name]()
label_list = processor.get_labels()
args.id2label = {i: label for i, label in enumerate(label_list)}
args.label2id = {label: i for i, label in enumerate(label_list)}

processor类的编写

基类编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class DataProcessor(object):
"""Base class for data converters for ner task data sets."""
# 这里的 InputExample 在不同的数据集上也是不同的
def get_train_examples(self, data_dir):
"""Gets a collection of `InputExample`s for the train set."""
raise NotImplementedError()

def get_dev_examples(self, data_dir):
"""Gets a collection of `InputExample`s for the dev set."""
raise NotImplementedError()

def get_labels(self):
"""Gets the list of labels for this data set."""
raise NotImplementedError()

@classmethod
def _read_text(self,input_file):
lines = []
with open(input_file,'r') as f:
words = []
labels = []
for line in f:
if line.startswith("-DOCSTART-") or line == "" or line == "\n":
if words:
lines.append({"words":words,"labels":labels})
words = []
labels = []
else:
splits = line.split(" ")
words.append(splits[0])
if len(splits) > 1:
labels.append(splits[-1].replace("\n", ""))
else:
# Examples could have no label for mode = "test"
labels.append("O")
if words:
lines.append({"words":words,"labels":labels})
return lines

@classmethod
def _read_json(self,input_file):
lines = []
with open(input_file,'r') as f:
for line in f:
line = json.loads(line.strip())
text = line['text']
label_entities = line.get('label',None)
words = list(text)
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)
lines.append({"words": words, "labels": labels})
return lines

DataProcessor类中主要针对两种ner输入文件格式进行解析, 返回类型均为 dict_list.
txt样板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
吴 B-NAME
重 I-NAME
阳 E-NAME
, O
中 B-CONT
国 I-CONT
国 I-CONT
籍 E-CONT

历 O
任 O
公 B-ORG
司 E-ORG
副 B-TITLE
总 M-TITLE
经 M-TITLE
理 E-TITLE

json样板

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

以Cner数据集为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class InputExample(object):
"""A single training/test example for ner task"""
def __init__(self, guid, text_a, labels):
"""Constructs a InputExample.
Args:
guid: Unique id for the example.
text_a: list. The words of the sequence.
labels: (Optional) list. The labels for each word of the sequence. This should be
specified for train and dev examples, but not for test examples.
"""
self.guid = guid
self.text_a = text_a
self.labels = labels

def __repr__(self):
return str(self.to_json_string())
def to_dict(self):
"""Serializes this instance to a Python dictionary."""
output = copy.deepcopy(self.__dict__)
return output
def to_json_string(self):
"""Serializes this instance to a JSON string."""
return json.dumps(self.to_dict(), indent=2, sort_keys=True) + "\n"

class CnerProcessor(DataProcessor):
"""Processor for the chinese ner data set."""

def __init__(self, element_dict_file):
'''load label dict
element_dict format: {"Name": "姓名", "Age": "年龄"}
'''
assert os.path.existed(element_dict_file)
with open(element_dict_file, 'r', encoding='utf-8') as f:
self.element_dict = json.load(f)

def get_train_examples(self, data_path):
"""See base class."""
assert os.path.existed(data_path)
return self._create_examples(self._read_text(data_path), "train")

def get_dev_examples(self, data_path):
"""See base class."""
assert os.path.existed(data_path)
return self._create_examples(self._read_text(data_path, "dev")

def get_test_examples(self, data_path):
"""See base class."""
assert os.path.existed(data_path)
return self._create_examples(self._read_text(data_path, "test")

def get_labels(self):
"""See base class."""
label_list = []
for elem_name in self.element_dict.keys():
label_list.append(f"B-{elem_name}")
label_list.append(f"I-{elem_name}")
label_list.append(f"S-{elem_name}")
label_list.extend(["X", "O", "[START]", "[END]"])
return label_list

# 这个函数主要是将一些与预训练模型所接受数据格式不同的数据进行转换. 比如将bioes, bios, bmos等各种各样格式转换为统一的bio格式
def _create_examples(self, lines, set_type):
"""Creates examples for the training and dev sets."""
examples = []
for (i, line) in enumerate(lines):
if i == 0:
continue
guid = "%s-%s" % (set_type, i)
text_a= line['words']
# BIOS
labels = []
for x in line['labels']:
if 'M-' in x:
labels.append(x.replace('M-','I-'))
elif 'E-' in x:
labels.append(x.replace('E-', 'I-'))
else:
labels.append(x)
examples.append(InputExample(guid=guid, text_a=text_a, labels=labels))
return examples

Step5: 加载预训练模型

加载方式

1
2
3
4
5
6
7
8
9
10
11
12
13
from transformers import BertConfig, BertTokenizer
from models.bert_for_ner import BertCrfForNer

MODEL_CLASSES = {
## bert ernie bert_wwm bert_wwwm_ext
'bert': (BertConfig, BertCrfForNer, BertTokenizer),
}

args.model_type = args.model_type.lower()
config_class, model_class, tokenizer_class = MODEL_CLASSES[args.model_type]
config = config_class.from_pretrained(args.model_name_or_path,num_labels=num_labels,)
tokenizer = tokenizer_class.from_pretrained(args.model_name_or_path, do_lower_case=args.do_lower_case,)
model = model_class.from_pretrained(args.model_name_or_path, config=config)

预训练模型可以去 hugging face 官网上进行下载. 对于基于 pytorch 的预训练模型, 主要下载 config.json(对应BertConfig类的初始化), vocab.txt(对应BertTokenize类的初始化), pytorch.bin(对应预训练模型).

很多时候, 我们会在预训练模型之后增加一些神经网络层进行微调. 比如这个例子中, BertCrfForNer 就是由程序员编写, 而不是像BertConfigBertTokenizer一样直接从transformers库中引用.

网络结构编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from .layers.crf import CRF
from transformers import BertModel, BertPreTrainedModel
import torch.nn as nn

class BertCrfForNer(BertPreTrainedModel):
def __init__(self, config):
super(BertCrfForNer, self).__init__(config)
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
self.crf = CRF(num_tags=config.num_labels, batch_first=True)
self.init_weights()

def forward(self, input_ids, token_type_ids=None, attention_mask=None,labels=None):
outputs =self.bert(input_ids = input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids)
sequence_output = outputs[0]
sequence_output = self.dropout(sequence_output)
logits = self.classifier(sequence_output)
outputs = (logits,)
if labels is not None:
loss = self.crf(emissions = logits, tags=labels, mask=attention_mask)
outputs =(-1*loss,)+outputs
return outputs # (loss), scores

其中, BertPreTrainedModel是一个抽象类, 包含_init_weights来处理参数初始化; BertModel 是一个基础的Bert网络架构, 需要通过 configs 传入一些参数. 不过这些参数不需要程序员设定, 可以直接从BertConfig中进行读取.

整个BertCrfForNer由两部分组成:

  • 第一部分相当于一个分类模型. 输入一个词, 输出是这个词被标注为各个label的概率.
  • 第二部分是CRF. 输入一段文本, 这个文本中每个词经过第一部分都会得到一个概率向量. 根据label之间的约束, 找到一条联合概率最大的路径作为输出结果, 即为输入文本对应的标注结果.

Classifier

bert –> dropout –> classifier

CRF

CRF可以继承torch.nn.Module类作为Bert层后的连接层. Bert层的输出是一个3维矩阵: $batch_size \times sequence_length \times label_nums$. 对于 sequence 中的每个词, 都会对应所有label 有一个特定的概率. 一种简单的做法是直接取概率最大的那个label作为该词的输出label. 但这种简单的方式忽略了 label 之间的依赖和约束关系. 比如 B-Product 标签后面一定不会再跟一个 B-Product. 而 CRF 层同时考虑了label之间的依赖约束关系和Bert层的到的概率矩阵.

CRF层中比较重要的几个变量是:

  • start_transitions, end_transitions: 一维向量, 长度为 label_num , 分别表示每个 label 出现在句首和句尾的概率.
  • transitions: 转移矩阵, shape 为 $label_num \times label_num$, mat[i, j]表示 label_i 后面跟 label_j 的概率.
  • emissions: 发射矩阵, shape 为 $batch_size \times sequence_length \times label_nums$, 即 Bert 层输出结果.

以上三个矩阵都不需要人为设置, 其中start_transitions, end_transitions, transitions三个参数CRF层会在模型训练的过层中自动调整, 而emissions是Bert层传入的参数, 会在Bert层进行自动调整. 因此在CRF类中也需要定义forward函数, 传入主要参数包括 emissions(Bert层输出结果), tags(正确label序列), mask(元素类型为bool的大小为$label_num \time label_num$的矩阵, mat[i, j]表示label_i的下一个label是否可以为label_j).

指定模型训练GPU

1
2
model.to(args.device)
logger.info("Training/evaluation parameters %s", args)

Step6: 加载数据

调用方式

1
train_dataset = load_and_cache_examples(args, args.task_name, tokenizer, data_type='train')

load_and_cache_examples函数

如果数据已经缓存了, 就直接从缓存文件中读取出来; 否则, 利用之前编写好的 processor 类从源数据文件中读取并转换为需要训练的数据格式, 最后将数据写入缓存文件方便下次使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def load_and_cache_examples(args, task, tokenizer, data_type='train'):
if args.local_rank not in [-1, 0] and not evaluate:
torch.distributed.barrier() # Make sure only the first process in distributed training process the dataset, and the others will use the cache
processor = processors[task]()
# Load data features from cache or dataset file
cached_features_file = os.path.join(args.data_dir, 'cached_crf-{}_{}_{}_{}'.format(
data_type,
list(filter(None, args.model_name_or_path.split('/'))).pop(),
str(args.train_max_seq_length if data_type == 'train' else args.eval_max_seq_length),
str(task)))
if os.path.exists(cached_features_file) and not args.overwrite_cache:
logger.info("Loading features from cached file %s", cached_features_file)
features = torch.load(cached_features_file)
else:
logger.info("Creating features from dataset file at %s", args.data_dir)
label_list = processor.get_labels()
if data_type == 'train':
examples = processor.get_train_examples(args.train_path)
elif data_type == 'dev':
examples = processor.get_dev_examples(args.dev_path)
else:
examples = processor.get_test_examples(args.test_path)
features = convert_examples_to_features(examples=examples,
tokenizer=tokenizer,
label_list=label_list,
max_seq_length=args.train_max_seq_length if data_type == 'train' \
else args.eval_max_seq_length,
cls_token_at_end=bool(args.model_type in ["xlnet"]),
pad_on_left=bool(args.model_type in ['xlnet']),
cls_token=tokenizer.cls_token,
cls_token_segment_id=2 if args.model_type in ["xlnet"] else 0,
sep_token=tokenizer.sep_token,
# pad on the left for xlnet
pad_token=tokenizer.convert_tokens_to_ids([tokenizer.pad_token])[0],
pad_token_segment_id=4 if args.model_type in ['xlnet'] else 0,
)

logger.info("Saving features into cached file %s", cached_features_file)
torch.save(features, cached_features_file)
# Convert to Tensors and build dataset
all_input_ids = torch.tensor([f.input_ids for f in features], dtype=torch.long)
all_input_mask = torch.tensor([f.input_mask for f in features], dtype=torch.long)
all_segment_ids = torch.tensor([f.segment_ids for f in features], dtype=torch.long)
all_label_ids = torch.tensor([f.label_ids for f in features], dtype=torch.long)
all_lens = torch.tensor([f.input_len for f in features], dtype=torch.long)
dataset = TensorDataset(all_input_ids, all_input_mask, all_segment_ids, all_lens, all_label_ids)
return dataset

在这个函数中包含的关键函数是convert_examples_to_features, 这个函数将[{words: char_list, labels: label_list}] 转换为 [{words: num_list, label: num_list}].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import logging
logger = logging.getLogger(__name__)

class InputFeatures(object):
"""A single set of features of data."""
def __init__(self, input_ids, input_mask, input_len,segment_ids, label_ids):
self.input_ids = input_ids
self.input_mask = input_mask
self.segment_ids = segment_ids
self.label_ids = label_ids
self.input_len = input_len

def __repr__(self):
return str(self.to_json_string())

def to_dict(self):
"""Serializes this instance to a Python dictionary."""
output = copy.deepcopy(self.__dict__)
return output

def to_json_string(self):
"""Serializes this instance to a JSON string."""
return json.dumps(self.to_dict(), indent=2, sort_keys=True) + "\n"

def convert_examples_to_features(examples,label_list,max_seq_length,tokenizer,
cls_token_at_end=False,cls_token="[CLS]",cls_token_segment_id=1,
sep_token="[SEP]",pad_on_left=False,pad_token=0,pad_token_segment_id=0,
sequence_a_segment_id=0,mask_padding_with_zero=True,):
""" Loads a data file into a list of `InputBatch`s
`cls_token_at_end` define the location of the CLS token:
- False (Default, BERT/XLM pattern): [CLS] + A + [SEP] + B + [SEP]
- True (XLNet/GPT pattern): A + [SEP] + B + [SEP] + [CLS]
`cls_token_segment_id` define the segment id associated to the CLS token (0 for BERT, 2 for XLNet)
"""
label_map = {label: i for i, label in enumerate(label_list)}
features = []
for (ex_index, example) in enumerate(examples):
if ex_index % 10000 == 0:
logger.info("Writing example %d of %d", ex_index, len(examples))
if isinstance(example.text_a,list):
example.text_a = " ".join(example.text_a)
tokens = tokenizer.tokenize(example.text_a)
label_ids = [label_map[x] for x in example.labels]
# Account for [CLS] and [SEP] with "- 2".
special_tokens_count = 2
if len(tokens) > max_seq_length - special_tokens_count:
tokens = tokens[: (max_seq_length - special_tokens_count)]
label_ids = label_ids[: (max_seq_length - special_tokens_count)]

# The convention in BERT is:
# (a) For sequence pairs:
# tokens: [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
# type_ids: 0 0 0 0 0 0 0 0 1 1 1 1 1 1
# (b) For single sequences:
# tokens: [CLS] the dog is hairy . [SEP]
# type_ids: 0 0 0 0 0 0 0
#
# Where "type_ids" are used to indicate whether this is the first
# sequence or the second sequence. The embedding vectors for `type=0` and
# `type=1` were learned during pre-training and are added to the wordpiece
# embedding vector (and position vector). This is not *strictly* necessary
# since the [SEP] token unambiguously separates the sequences, but it makes
# it easier for the model to learn the concept of sequences.
#
# For classification tasks, the first vector (corresponding to [CLS]) is
# used as as the "sentence vector". Note that this only makes sense because
# the entire model is fine-tuned.
tokens += [sep_token]
label_ids += [label_map['O']]
segment_ids = [sequence_a_segment_id] * len(tokens)

if cls_token_at_end:
tokens += [cls_token]
label_ids += [label_map['O']]
segment_ids += [cls_token_segment_id]
else:
tokens = [cls_token] + tokens
label_ids = [label_map['O']] + label_ids
segment_ids = [cls_token_segment_id] + segment_ids

input_ids = tokenizer.convert_tokens_to_ids(tokens)
# The mask has 1 for real tokens and 0 for padding tokens. Only real
# tokens are attended to.
input_mask = [1 if mask_padding_with_zero else 0] * len(input_ids)
input_len = len(label_ids)
# Zero-pad up to the sequence length.
padding_length = max_seq_length - len(input_ids)
if pad_on_left:
input_ids = ([pad_token] * padding_length) + input_ids
input_mask = ([0 if mask_padding_with_zero else 1] * padding_length) + input_mask
segment_ids = ([pad_token_segment_id] * padding_length) + segment_ids
label_ids = ([pad_token] * padding_length) + label_ids
else:
input_ids += [pad_token] * padding_length
input_mask += [0 if mask_padding_with_zero else 1] * padding_length
segment_ids += [pad_token_segment_id] * padding_length
label_ids += [pad_token] * padding_length

assert len(input_ids) == max_seq_length
assert len(input_mask) == max_seq_length
assert len(segment_ids) == max_seq_length
assert len(label_ids) == max_seq_length
if ex_index < 5:
logger.info("*** Example ***")
logger.info("guid: %s", example.guid)
logger.info("tokens: %s", " ".join([str(x) for x in tokens]))
logger.info("input_ids: %s", " ".join([str(x) for x in input_ids]))
logger.info("input_mask: %s", " ".join([str(x) for x in input_mask]))
logger.info("segment_ids: %s", " ".join([str(x) for x in segment_ids]))
logger.info("label_ids: %s", " ".join([str(x) for x in label_ids]))

features.append(InputFeatures(input_ids=input_ids, input_mask=input_mask,input_len = input_len,
segment_ids=segment_ids, label_ids=label_ids))
return features

Step7: 模型训练

调用方式

1
global_step, tr_loss = train(args, train_dataset, model, tokenizer)

train函数编写

1
2
def train(args, train_dataset, model, tokenizer):
...

data_loader

1
2
3
4
5
6
7
8
9
from torch.utils.data import DataLoader, RandomSampler, SequentialSampler
train_sampler = RandomSampler(train_dataset)
train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=args.train_batch_size,collate_fn=collate_fn)

if args.max_steps > 0:
t_total = args.max_steps
args.num_train_epochs = args.max_steps // (len(train_dataloader) // args.gradient_accumulation_steps) + 1
else:
t_total = len(train_dataloader) // args.gradient_accumulation_steps * args.num_train_epochs

Prepare optimizer and schedule

1.还原结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from callback.optimizater.adamw import AdamW
from callback.lr_scheduler import get_linear_schedule_with_warmup

no_decay = ["bias", "LayerNorm.weight"]
bert_param_optimizer = list(model.bert.named_parameters())
crf_param_optimizer = list(model.crf.named_parameters())
linear_param_optimizer = list(model.classifier.named_parameters())
optimizer_grouped_parameters = [
{'params': [p for n, p in bert_param_optimizer if not any(nd in n for nd in no_decay)],
'weight_decay': args.weight_decay, 'lr': args.learning_rate},
{'params': [p for n, p in bert_param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0,
'lr': args.learning_rate},

{'params': [p for n, p in crf_param_optimizer if not any(nd in n for nd in no_decay)],
'weight_decay': args.weight_decay, 'lr': args.crf_learning_rate},
{'params': [p for n, p in crf_param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0,
'lr': args.crf_learning_rate},

{'params': [p for n, p in linear_param_optimizer if not any(nd in n for nd in no_decay)],
'weight_decay': args.weight_decay, 'lr': args.crf_learning_rate},
{'params': [p for n, p in linear_param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0,
'lr': args.crf_learning_rate}
]
args.warmup_steps = int(t_total * args.warmup_proportion)
optimizer = AdamW(optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon)
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=args.warmup_steps,
num_training_steps=t_total)

2.还原optimizer和scheduler参数

1
2
3
4
5
6
# Check if saved optimizer or scheduler states exist
if os.path.isfile(os.path.join(args.model_name_or_path, "optimizer.pt")) and os.path.isfile(
os.path.join(args.model_name_or_path, "scheduler.pt")):
# Load in optimizer and scheduler states
optimizer.load_state_dict(torch.load(os.path.join(args.model_name_or_path, "optimizer.pt")))
scheduler.load_state_dict(torch.load(os.path.join(args.model_name_or_path, "scheduler.pt")))

3.还原checkpoint数据

1
2
3
4
5
6
7
8
9
10
11
12
global_step = 0
steps_trained_in_current_epoch = 0
# Check if continuing training from a checkpoint
if os.path.exists(args.model_name_or_path) and "checkpoint" in args.model_name_or_path:
# set global_step to gobal_step of last saved checkpoint from model path
global_step = int(args.model_name_or_path.split("-")[-1].split("/")[0])
epochs_trained = global_step // (len(train_dataloader) // args.gradient_accumulation_steps)
steps_trained_in_current_epoch = global_step % (len(train_dataloader) // args.gradient_accumulation_steps)
logger.info(" Continuing training from checkpoint, will skip to saved global_step")
logger.info(" Continuing training from epoch %d", epochs_trained)
logger.info(" Continuing training from global step %d", global_step)
logger.info(" Will skip the first %d steps in the first epoch", steps_trained_in_current_epoch)

训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
tr_loss, logging_loss = 0.0, 0.0
model.zero_grad()
seed_everything(args.seed) # Added here for reproductibility (even between python 2 and 3)
pbar = ProgressBar(n_total=len(train_dataloader), desc='Training', num_epochs=int(args.num_train_epochs))

for epoch in range(int(args.num_train_epochs)):
pbar.reset()
pbar.epoch_start(current_epoch=epoch)
for step, batch in enumerate(train_dataloader):
# Skip past any already trained steps if resuming training
if steps_trained_in_current_epoch > 0:
steps_trained_in_current_epoch -= 1
continue
model.train()
batch = tuple(t.to(args.device) for t in batch)
inputs = {"input_ids": batch[0], "attention_mask": batch[1], "labels": batch[3]}
if args.model_type != "distilbert":
# XLM and RoBERTa don"t use segment_ids
inputs["token_type_ids"] = (batch[2] if args.model_type in ["bert", "xlnet"] else None)
outputs = model(**inputs)
loss = outputs[0] # model outputs are always tuple in pytorch-transformers (see doc)
if args.gradient_accumulation_steps > 1:
loss = loss / args.gradient_accumulation_steps
loss.backward()

pbar(step, {'loss': loss.item()})
tr_loss += loss.item()
torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)

optimizer.step()
scheduler.step() # Update learning rate schedule
model.zero_grad()
global_step += 1

if args.local_rank in [-1, 0] and args.save_steps > 0 and global_step % args.save_steps == 0:
# Save model checkpoint
output_dir = os.path.join(args.output_dir, "checkpoint-{}".format(global_step))
if not os.path.exists(output_dir):
os.makedirs(output_dir)
model_to_save = (
model.module if hasattr(model, "module") else model
) # Take care of distributed/parallel training
model_to_save.save_pretrained(output_dir)
torch.save(args, os.path.join(output_dir, "training_args.bin"))
logger.info("Saving model checkpoint to %s", output_dir)
tokenizer.save_vocabulary(output_dir)
# torch.save(optimizer.state_dict(), os.path.join(output_dir, "optimizer.pt"))
torch.save(scheduler.state_dict(), os.path.join(output_dir, "scheduler.pt"))
logger.info("Saving optimizer and scheduler states to %s", output_dir)
logger.info("\n")
if 'cuda' in str(args.device):
torch.cuda.empty_cache()
return global_step, tr_loss / global_step

Step8: 保存最好的模型结果

1
2
3
4
5
6
7
8
9
10
11
12
13
global_step, tr_loss = train(args, train_dataset, model, tokenizer)
logger.info(" global_step = %s, average loss = %s", global_step, tr_loss)

logger.info("Saving model checkpoint to %s", args.output_dir)
# Save a trained model, configuration and tokenizer using `save_pretrained()`.
# They can then be reloaded using `from_pretrained()`
model_to_save = (
model.module if hasattr(model, "module") else model
) # Take care of distributed/parallel training
model_to_save.save_pretrained(args.output_dir)
tokenizer.save_vocabulary(args.output_dir)
# Good practice: save your training arguments together with the trained model
torch.save(args, os.path.join(args.output_dir, "training_args.bin"))

Step9: 在dev集上评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import glob

results = {}
checkpoints = [args.output_dir]
if args.eval_all_checkpoints:
checkpoints = list(
os.path.dirname(c) for c in sorted(glob.glob(args.output_dir + "/**/" + WEIGHTS_NAME, recursive=True))
)
logging.getLogger("pytorch_transformers.modeling_utils").setLevel(logging.WARN) # Reduce logging
logger.info("Evaluate the following checkpoints: %s", checkpoints)
for checkpoint in checkpoints:
global_step = checkpoint.split("-")[-1] if len(checkpoints) > 1 else ""
prefix = checkpoint.split('/')[-1] if checkpoint.find('checkpoint') != -1 else ""
model = model_class.from_pretrained(checkpoint, config=config)
model.to(args.device)
result = evaluate(args, model, tokenizer, prefix=prefix)
if global_step:
result = {"{}_{}".format(global_step, k): v for k, v in result.items()}
results.update(result)
output_eval_file = os.path.join(args.output_dir, "eval_results.txt")
with open(output_eval_file, "w") as writer:
for key in sorted(results.keys()):
writer.write("{} = {}\n".format(key, str(results[key])))

evaluate函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def evaluate(args, model, tokenizer, prefix=""):
metric = SeqEntityScore(args.id2label, markup=args.markup)
eval_output_dir = args.output_dir
if not os.path.exists(eval_output_dir) and args.local_rank in [-1, 0]:
os.makedirs(eval_output_dir)
eval_dataset = load_and_cache_examples(args, args.task_name, tokenizer, data_type='dev')
args.eval_batch_size = args.per_gpu_eval_batch_size * max(1, args.n_gpu)
# Note that DistributedSampler samples randomly
eval_sampler = SequentialSampler(eval_dataset) if args.local_rank == -1 else DistributedSampler(eval_dataset)
eval_dataloader = DataLoader(eval_dataset, sampler=eval_sampler, batch_size=args.eval_batch_size,
collate_fn=collate_fn)
# Eval!
logger.info("***** Running evaluation %s *****", prefix)
logger.info(" Num examples = %d", len(eval_dataset))
logger.info(" Batch size = %d", args.eval_batch_size)
eval_loss = 0.0
nb_eval_steps = 0
pbar = ProgressBar(n_total=len(eval_dataloader), desc="Evaluating")
if isinstance(model, nn.DataParallel):
model = model.module
for step, batch in enumerate(eval_dataloader):
model.eval()
batch = tuple(t.to(args.device) for t in batch)
with torch.no_grad():
inputs = {"input_ids": batch[0], "attention_mask": batch[1], "labels": batch[3]}
if args.model_type != "distilbert":
# XLM and RoBERTa don"t use segment_ids
inputs["token_type_ids"] = (batch[2] if args.model_type in ["bert", "xlnet"] else None)
outputs = model(**inputs)
tmp_eval_loss, logits = outputs[:2]
tags = model.crf.decode(logits, inputs['attention_mask'])
if args.n_gpu > 1:
tmp_eval_loss = tmp_eval_loss.mean() # mean() to average on multi-gpu parallel evaluating
eval_loss += tmp_eval_loss.item()
nb_eval_steps += 1
out_label_ids = inputs['labels'].cpu().numpy().tolist()
input_lens = batch[4].cpu().numpy().tolist()
tags = tags.squeeze(0).cpu().numpy().tolist()
for i, label in enumerate(out_label_ids):
temp_1 = []
temp_2 = []
for j, m in enumerate(label):
if j == 0:
continue
elif j == input_lens[i] - 1:
metric.update(pred_paths=[temp_2], label_paths=[temp_1])
break
else:
temp_1.append(args.id2label[out_label_ids[i][j]])
temp_2.append(args.id2label[tags[i][j]])
pbar(step)
logger.info("\n")
eval_loss = eval_loss / nb_eval_steps
eval_info, entity_info = metric.result()
results = {f'{key}': value for key, value in eval_info.items()}
results['loss'] = eval_loss
logger.info("***** Eval results %s *****", prefix)
info = "-".join([f' {key}: {value:.4f} ' for key, value in results.items()])
logger.info(info)
logger.info("***** Entity results %s *****", prefix)
for key in sorted(entity_info.keys()):
logger.info("******* %s results ********" % key)
info = "-".join([f' {key}: {value:.4f} ' for key, value in entity_info[key].items()])
logger.info(info)
return results

其中比较关键的两个部件是SeqEntityScore(准确率计算类)和crf.decode(解码CRF结果矩阵的到最优tag路径).

SeqEntityScore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class SeqEntityScore(object):
def __init__(self, id2label,markup='bios'):
self.id2label = id2label
self.markup = markup
self.reset()

def reset(self):
self.origins = []
self.founds = []
self.rights = []

def compute(self, origin, found, right):
recall = 0 if origin == 0 else (right / origin)
precision = 0 if found == 0 else (right / found)
f1 = 0. if recall + precision == 0 else (2 * precision * recall) / (precision + recall)
return recall, precision, f1

def result(self):
class_info = {}
origin_counter = Counter([x[0] for x in self.origins])
found_counter = Counter([x[0] for x in self.founds])
right_counter = Counter([x[0] for x in self.rights])
for type_, count in origin_counter.items():
origin = count
found = found_counter.get(type_, 0)
right = right_counter.get(type_, 0)
recall, precision, f1 = self.compute(origin, found, right)
class_info[type_] = {"acc": round(precision, 4), 'recall': round(recall, 4), 'f1': round(f1, 4)}
origin = len(self.origins)
found = len(self.founds)
right = len(self.rights)
recall, precision, f1 = self.compute(origin, found, right)
return {'acc': precision, 'recall': recall, 'f1': f1}, class_info

def update(self, label_paths, pred_paths):
'''
labels_paths: [[],[],[],....]
pred_paths: [[],[],[],.....]

:param label_paths:
:param pred_paths:
:return:
Example:
>>> labels_paths = [['O', 'O', 'O', 'B-MISC', 'I-MISC', 'I-MISC', 'O'], ['B-PER', 'I-PER', 'O']]
>>> pred_paths = [['O', 'O', 'B-MISC', 'I-MISC', 'I-MISC', 'I-MISC', 'O'], ['B-PER', 'I-PER', 'O']]
'''
for label_path, pre_path in zip(label_paths, pred_paths):
label_entities = get_entities(label_path, self.id2label,self.markup)
pre_entities = get_entities(pre_path, self.id2label,self.markup)
self.origins.extend(label_entities)
self.founds.extend(pre_entities)
self.rights.extend([pre_entity for pre_entity in pre_entities if pre_entity in label_entities])

crf.decode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def _viterbi_decode(self, emissions: torch.FloatTensor,
mask: torch.ByteTensor,
pad_tag: Optional[int] = None) -> List[List[int]]:
# emissions: (seq_length, batch_size, num_tags)
# mask: (seq_length, batch_size)
# return: (batch_size, seq_length)
if pad_tag is None:
pad_tag = 0

device = emissions.device
seq_length, batch_size = mask.shape

# Start transition and first emission
# shape: (batch_size, num_tags)
score = self.start_transitions + emissions[0]
history_idx = torch.zeros((seq_length, batch_size, self.num_tags),
dtype=torch.long, device=device)
oor_idx = torch.zeros((batch_size, self.num_tags),
dtype=torch.long, device=device)
oor_tag = torch.full((seq_length, batch_size), pad_tag,
dtype=torch.long, device=device)

# - score is a tensor of size (batch_size, num_tags) where for every batch,
# value at column j stores the score of the best tag sequence so far that ends
# with tag j
# - history_idx saves where the best tags candidate transitioned from; this is used
# when we trace back the best tag sequence
# - oor_idx saves the best tags candidate transitioned from at the positions
# where mask is 0, i.e. out of range (oor)

# Viterbi algorithm recursive case: we compute the score of the best tag sequence
# for every possible next tag
for i in range(1, seq_length):
# Broadcast viterbi score for every possible next tag
# shape: (batch_size, num_tags, 1)
broadcast_score = score.unsqueeze(2)

# Broadcast emission score for every possible current tag
# shape: (batch_size, 1, num_tags)
broadcast_emission = emissions[i].unsqueeze(1)

# Compute the score tensor of size (batch_size, num_tags, num_tags) where
# for each sample, entry at row i and column j stores the score of the best
# tag sequence so far that ends with transitioning from tag i to tag j and emitting
# shape: (batch_size, num_tags, num_tags)
next_score = broadcast_score + self.transitions + broadcast_emission

# Find the maximum score over all possible current tag
# shape: (batch_size, num_tags)
next_score, indices = next_score.max(dim=1)

# Set score to the next score if this timestep is valid (mask == 1)
# and save the index that produces the next score
# shape: (batch_size, num_tags)
score = torch.where(mask[i].unsqueeze(-1), next_score, score)
indices = torch.where(mask[i].unsqueeze(-1), indices, oor_idx)
history_idx[i - 1] = indices

# End transition score
# shape: (batch_size, num_tags)
end_score = score + self.end_transitions
_, end_tag = end_score.max(dim=1)

# shape: (batch_size,)
seq_ends = mask.long().sum(dim=0) - 1

# insert the best tag at each sequence end (last position with mask == 1)
history_idx = history_idx.transpose(1, 0).contiguous()
history_idx.scatter_(1, seq_ends.view(-1, 1, 1).expand(-1, 1, self.num_tags),
end_tag.view(-1, 1, 1).expand(-1, 1, self.num_tags))
history_idx = history_idx.transpose(1, 0).contiguous()

# The most probable path for each sequence
best_tags_arr = torch.zeros((seq_length, batch_size),
dtype=torch.long, device=device)
best_tags = torch.zeros(batch_size, 1, dtype=torch.long, device=device)
for idx in range(seq_length - 1, -1, -1):
best_tags = torch.gather(history_idx[idx], 1, best_tags)
best_tags_arr[idx] = best_tags.data.view(batch_size)

return torch.where(mask, best_tags_arr, oor_tag).transpose(0, 1)

Step9: 在test集上预测

与evaluate基本一致