那些年使用BERT模型踩的坑~~

Preface:fine-tuning到底是啥玩意儿?

预训练模型层上添加新的网络层,然后预训练层和新网络层联合训练。
文本分类的例子最典型了,最后加一个Dense层,把输出维度降至类别数,再进行sigmoid或softmax。
比如命名实体识别,在外面添加BiLSTM+CRF层,就成了BERT+BiLSTM+CRF模型。
这个例子可能不太典型,因为还是加了繁重的网络结构。
多分类和多标签分类时,只需要用到以下四个文件。
├── tokenization.py # 所需文件四:用于文本预处理(分字)的文件
├── modeling.py # 所需文件一:BERT的网络模型文件
├── optimization.py # 所需要文件二:优化器文件
├── run_classifier.py # 所需文件三:模型的fine-tuning文件
需要修改的文件:run_classifier.py。
按模型的输入格式处理数据和导入数据,在预训练层加下游任务,修改评估函数输出结果等。

└── chinese_L-12_H-768_A-12
    ├── bert_config.json                     # BERT 的配置文件
    ├── bert_model.ckpt.data-00000-of-00001  # 预训练的模型
    ├── bert_model.ckpt.index
    ├── bert_model.ckpt.meta                
    └── vocab.txt                            # BERT字粒度的词表

#词汇表 vocab.txt 解读
词汇表中共有21128行,编号从0开始,编号1-99的为unused字符串。[CLS]在102行,id为101;[SEP]在103行,id为102。

├── bert                                        # 需要用到的 BERT 文件
│   ├── __init__.py
│   ├── modeling.py
│   ├── optimization.py
│   ├── run_classifier.py
│   └── tokenization.py
├── config.py                                   # 参数配置
├── data_processor.py                           # 对样本做拆字
├── **metrics.py                                  # 自定义 f1,precision和recall**
├── pretrained_model                            # 预训练的中文BERT模型
│   └── chinese_L-12_H-768_A-12
│       ├── bert_config.json
│       ├── bert_model.ckpt.data-00000-of-00001
│       ├── bert_model.ckpt.index
│       ├── bert_model.ckpt.meta
│       └── vocab.txt
├── run_classifier.py                           # 定义下游任务并做训练、验证和预测脚本
├── run.sh                                      # 执行的shell脚本
└── run_test.py                                 # 模型测试脚本
#**修改模型**,在预训练层外面增加一个dense层,做softmax/sigmoid变换
#**create_model这个函数是最关键的**
def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
                 labels, num_labels, use_one_hot_embeddings):
#**修改模型评估函数**,增加F1值、precision和recall 
def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate,
                     num_train_steps, num_warmup_steps, use_tpu,
                     use_one_hot_embeddings):

**
前方高能,避免踩大坑:
一是对于多分类任务,不能根据值大于0.5这个判断条件,来得到索引,而是根据是否为最大值。
值大于0.5,来得到索引,适用于二分类和多标签分类

predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)

二是用 np.argmax() 来得到索引,而不能用 tf.argmax(),否则会报错(好像也行!?)。
报错的意思是:计算图已经构建结束了,不能再调整计算图。
我的理解是,这个probabilities已经是numpy的格式,而不是tensorflow的格式,如果再用 tensorflow 的函数,那就会调整计算图。这是不允许的。

一、多标签多分类 VS 多分类任务

**
针对多标签多分类任务(Multi-label classification task),微调模型时的最后一层全连接层输出需要使用的是sigmoid转换。

而对于多分类任务(Multi-class classification task),则只需要进行softmax变换即可。
softmax适用于多分类问题中对每一个类别的概率判断。
小结:
如果模型输出为非互斥类别,且可以同时选择多个类别,则采用Sigmoid函数计算该网络的原始输出值。
如果模型输出为互斥类别,且只能选择一个类别,则采用Softmax函数计算该网络的原始输出值。

二、如何让BERT模型输出precision、recall、F1-score等指标

1、对于多分类任务:修改run_classifier.py中的代码如下

def metric_fn(per_example_loss, label_ids, logits, is_real_example):
        predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)
        accuracy = tf.metrics.accuracy(
            labels=label_ids, predictions=predictions, weights=is_real_example)
        loss = tf.metrics.mean(values=per_example_loss, weights=is_real_example)
        auc = tf.metrics.auc(labels=label_ids, predictions=predictions, weights=is_real_example)
        precision = tf.metrics.precision(labels=label_ids, predictions=predictions, weights=is_real_example)
        recall = tf.metrics.recall(labels=label_ids, predictions=predictions, weights=is_real_example)
        return {
            "eval_accuracy": accuracy,
            "eval_loss": loss,
            'eval_auc': auc,
            'eval_precision': precision,
            'eval_recall': recall,
        }

2、对于多标签多分类任务:

三、除了直接基于预训练模型的获得词向量,如何基于微调训练获得词向量?

1.基于预训练模型直接获得768维的词向量
该方法简单,但是后期任务的效果很差,至少不会好!
直接基于肖涵博士的***bert-as-service***获得词向量。
准备工作:
安装bert服务端:pip install bert-serving-server
安装bert服务客户端:pip install bert-serving-client
在命令行输入: bert-serving-start -model_dir E:/chinese_L-12_H-768_A-12
-num_worker=2
(训练好的中文预模型路径,num_worker的数量表示最高处理来自2个客户端的并发请求),如果成功开启则出现以下界面。
那些年使用BERT模型踩的坑~~_第1张图片

from bert_serving.client import BertClient
bc = BertClient()
a=bc.encode(['浙江投融界科技有限公司#服务:计算机软硬件、网络信息技术的技术开发、技术咨询、技术服务、成果转让,第二类增值电信业务中的信息服务业务(仅限互联网信息服务),计算机系统集成,实业投资、投资管理、投资咨询(以上项目除证券、期>货,未经金融等监管部门批准,不得从事向公众融资存款、融资担保、代客理财等金融服务),企业管理咨询,市场营销策划,网页>设计,承接网络工程(涉及资质证凭证经营),会展服务,经济信息咨询、商务信息咨询(除中介),设计、制作国内广告;其他无>需报经审批的一切合法项目。(依法须经批准的项目,经相关部门批准后方可开展经营活动)'])
print(a)
print(len(a[0]))

具体可参考:肖涵博士Github
2.基于run_classifier.py微调模型获得768维的词向量
只需修改run_classifier.py中的fine-tuning代码,从而输出相应的经微调训练之后的词向量。
该方法稍微有点耗时耗资源,但是后期任务的效果较好,至少不会很差!

def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
                 labels, num_labels, use_one_hot_embeddings):
    """Creates a classification model."""
    model = modeling.BertModel(
        config=bert_config,
        is_training=is_training,
        input_ids=input_ids,
        input_mask=input_mask,
        token_type_ids=segment_ids,
        use_one_hot_embeddings=use_one_hot_embeddings)
        
    output_layer = model.get_pooled_output()     #分类任务的768维词向量
    hidden_size = output_layer.shape[-1].value # 768
    output_weights = tf.get_variable(
        "output_weights", [num_labels, hidden_size],
        initializer=tf.truncated_normal_initializer(stddev=0.02))
    output_bias = tf.get_variable(
        "output_bias", [num_labels], initializer=tf.zeros_initializer())
    with tf.variable_scope("loss"):
        if is_training:
            output_layer = tf.nn.dropout(output_layer, keep_prob=0.9)
        logits = tf.matmul(output_layer, output_weights, transpose_b=True)
        logits = tf.nn.bias_add(logits, output_bias)
        #probabilities是由输出向量经sigmoid变换得到的
        #多分类问题,但是每个样本只属于一个类别,softmax交叉熵算出来的是一个值
        #多分类问题,且一个样本可以同时拥有多个标签,一个样本会在每个类别上有一个交叉熵,使用tf.sigmoid(与tf.nn.sigmoid相同,但最好用tf.sigmoid)
        probabilities = tf.sigmoid(logits)
        label_ids = tf.cast(labels, tf.float32)
        per_example_loss = tf.reduce_sum(
            tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=label_ids), axis=-1)  #logits和labels必须有相同的类型和大小
        loss = tf.reduce_mean(per_example_loss)
        
#         probabilities = tf.nn.softmax(logits, axis=-1)
#         log_probs = tf.nn.log_softmax(logits, axis=-1)

#         one_hot_labels = tf.one_hot(labels, depth=num_labels, dtype=tf.float32)

#         per_example_loss = -tf.reduce_sum(one_hot_labels * log_probs, axis=-1)
#         loss = tf.reduce_mean(per_example_loss)

        return (loss, per_example_loss, logits, probabilities, output_layer)  #只需在return中增加output_layer即可。
def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate,
                     num_train_steps, num_warmup_steps, use_tpu,
                     use_one_hot_embeddings):
    """Returns `model_fn` closure for TPUEstimator."""

    def model_fn(features, labels, mode, params):  # pylint: disable=unused-argument
        """The `model_fn` for TPUEstimator."""

        tf.logging.info("*** Features ***")
        for name in sorted(features.keys()):
            tf.logging.info("  name = %s, shape = %s" % (name, features[name].shape))

        input_ids = features["input_ids"]
        input_mask = features["input_mask"]
        segment_ids = features["segment_ids"]
        label_ids = features["label_ids"]

        is_training = (mode == tf.estimator.ModeKeys.TRAIN)
        #此处增加一个output_layer
        (total_loss, per_example_loss, logits, probabilities, ***output_layer***) = create_model(
            bert_config, is_training, input_ids, input_mask, segment_ids, label_ids,
            num_labels, use_one_hot_embeddings)

        tvars = tf.trainable_variables()

        scaffold_fn = None
        if init_checkpoint:
            (assignment_map, initialized_variable_names
             ) = modeling.get_assignment_map_from_checkpoint(tvars, init_checkpoint)
            if use_tpu:

                def tpu_scaffold():
                    tf.train.init_from_checkpoint(init_checkpoint, assignment_map)
                    return tf.train.Scaffold()

                scaffold_fn = tpu_scaffold
            else:
                tf.train.init_from_checkpoint(init_checkpoint, assignment_map)

        tf.logging.info("**** Trainable Variables ****")
        for var in tvars:
            init_string = ""
            if var.name in initialized_variable_names:
                init_string = ", *INIT_FROM_CKPT*"
            tf.logging.info("  name = %s, shape = %s%s", var.name, var.shape,
                            init_string)

        output_spec = None
        if mode == tf.estimator.ModeKeys.TRAIN:

            train_op = optimization.create_optimizer(
                total_loss, learning_rate, num_train_steps, num_warmup_steps, use_tpu)

            output_spec = tf.contrib.tpu.TPUEstimatorSpec(
                mode=mode,
                loss=total_loss,
                train_op=train_op,
                scaffold_fn=scaffold_fn)
        elif mode == tf.estimator.ModeKeys.EVAL:

            def metric_fn(per_example_loss, label_ids, probabilities):
                predict_ids = tf.cast(probabilities > 0.5, tf.int32)
                label_ids = tf.cast(label_ids, tf.int32)
                elements_equal = tf.cast(tf.equal(predict_ids, label_ids), tf.int32)   #tf.equal():逐个元素判断是否相等
                row_predict_ids = tf.reduce_sum(elements_equal, -1)
                row_label_ids = tf.reduce_sum(tf.ones_like(label_ids), -1)
                accuracy = tf.metrics.accuracy(
                    labels=row_label_ids, predictions=row_predict_ids)
                loss = tf.metrics.mean(per_example_loss)
                
                return {
                         "eval_accuracy": accuracy,
                         "eval_loss": loss,
                         }

            eval_metrics = (metric_fn, [per_example_loss, label_ids, probabilities])
            output_spec = tf.contrib.tpu.TPUEstimatorSpec(
                mode=mode,
                loss=total_loss,
                eval_metrics=eval_metrics,
                scaffold_fn=scaffold_fn)
        else:
            output_spec = tf.contrib.tpu.TPUEstimatorSpec(
                mode=mode,
                predictions={"probabilities": probabilities, ***"output_layer":output_layer***},
                scaffold_fn=scaffold_fn)    #predictions增加"output_layer":output_layer
        return output_spec

    return model_fn

OK,微调后的词向量提取成功!

四、多标签分类任务中,阈值一般真的是设0.5吗?

阈值绝大部分确实是设置为0.5的。
但自己在实际中会遇到一些样本预测的输出向量中的所有“概率”数据都小于0.5,因此这些样本就不会有预测标签。现实中也确实存在这种问题,模型对这部分的样本的确区分不出来,也有可能是因为相应标签对应的样本数很少,在划分数据集时,shuffle之后的trainset数据集中有的标签并未抽取到,因此验证时必定有的标签得不出预测标签。

持续更新中…

你可能感兴趣的:(深度学习,Algorithms,自然语言处理,tensorflow,神经网络,深度学习)