colab上基於tensorflow2的BERT中文文本多分類finetuning


整體背景

本文實現了在colab環境下基於tf-nightly-gpu的BERT中文多分類,如果你在現階段有實現類似的功能的需求,相信這篇文章會給你帶來一些幫助。

准備工作

1.環境:

硬件環境:

直接使用谷歌提供的免費訓練環境colab,選擇GPU

軟件環境:

tensorflow:tensorflow2.1.0版本對BERT的支持有些問題,現象是可以訓練但預測時無法正常加載模型(稍后代碼里會詳述),因此改為選擇tf-nightly(主線版本隨時會有新變更,如果擔心有影響可以選擇打tag的dev版本,如筆者驗證2.2.0.dev20200218是可以的)

預訓練模型:基於tf2.x的中文預訓練模型(https://tfhub.dev/tensorflow/bert_zh_L-12_H-768_A-12/1)【注:谷歌基於tf1.x版本公布過一個中文預訓練模型(BERT-base,Chinese),根據注釋其在tf2.0環境下應該無法使用】

分類代碼邏輯:谷歌已經幫我們准備好了基於BERT的分類主程序,其不隨tensorflow一起提供,而是單獨放在了tensorflow/models下面。models目前也分為發布版和主線版本,本文為了保證分類代碼邏輯的穩定,選擇發布版models v2.1.0【注:models主線版本近期(2020年2月)有頻繁變更,可以看到的是正在對BERT分類邏輯做小規模的重構優化,所以當models 2.2.0的時候,可能分類邏輯會有較大變化】

適配代碼:谷歌提供的代碼肯定不能直接滿足我們自己的需求,所以要對代碼進行修改調整,本文也提供了基於models2.1.0版本修改出來的BERT分類適配代碼

2.訓練數據:

本文從百度百科上爬取了10w左右的詞條,按照人物(human)、自然(nature)、地點(poi)、組織(org)、生活(life)等等標注成大概十幾個類別。把每個百科詞條中的信息按照標題、概述、正文、infobox等分別提取出來並進行簡單的清洗操作后,直接連接到一起形成一個長文本。再與百度百科id以及分類標簽按照"id\tinfo\tlabel"的格式組織成3列,形成類似於這種格式的訓練數據:

bkid    info    label

---------------

1         xxxx    life

2        xxxx    human

……

實際數據看起來是這樣的:

 數據准備好后重新洗牌並按照70%:20%:10%的比例拆分成訓練集、驗證集和測試集。

代碼適配

bert相關代碼都在tensorflow/models下面,將models 2.1.0壓縮包下載下來后后,BERT分類代碼位於:models2.1.0/official/nlp/bert目錄下

復制代碼
$ tree .
.
|-- bert_cloud_tpu.md
|-- classifier_data_lib.py # 分類數據方法庫
|-- common_flags.py # 通用命令行參數
|-- create_finetuning_data.py # 生成tfrecord格式的微調數據(依賴classifier_data_lib.py)
|-- create_pretraining_data.py # 生成預訓練數據(只有重新預訓練整個模型時才使用,本次不用)
|-- custom_metrics.py # 自定義文件,用於計算自定義metrics
|-- do_pred_data.py # 自定義文件,用於生成預測數據
|-- do_predict.py # 自定義文件,用於模型訓練完成后執行預測
|-- export_tfhub.py
|-- export_tfhub_test.py
|-- __init__.py
|-- input_pipeline.py # 用於加載tfrecord格式的訓練數據
|-- model_saving_utils.py # 用於模型保存
|-- README.md
|-- run_classifier.py # finetune分類主程序
|-- run_pretraining.py # 預訓練主程序(本次不用)
|-- run_squad.py # squad主程序(本次不用)
|-- squad_lib.py
|-- squad_lib_sp.py
|-- tf1_checkpoint_converter_lib.py
|-- tf2_albert_encoder_checkpoint_converter.py
|-- tf2_encoder_checkpoint_converter.py
|-- tokenization.py # 分詞器
|-- tokenization_test.py
`-- utils.py #自定義文件,用於繪制自定義metrics圖表

0 directories, 25 files
復制代碼

主要修改點有二:

1. 添加自定義的數據處理器及處理邏輯

2. 原finetune代碼只有訓練集和驗證集的代碼流程,本文在整個訓練結束后添加了測試集的相關流程。

3. 原代碼只系統了訓練邏輯,本文添加了訓練完成之后的預測邏輯(暫未包括生產環境部署)。

下面對主要修改點進行說明:

1.數據格式轉換邏輯:

tensorflow推薦使用tfrecord格式的數據,因此將數據集轉換成tfrecord格式並保存下來,便於后續重復使用

文件1:create_finetuning_data.py 

a. 添加新的數據處理器:

 1 def generate_classifier_dataset():
 2   """Generates classifier dataset and returns input meta data."""
 3   assert FLAGS.input_data_dir and FLAGS.classification_task_name
 4 
 5   processors = { 
 6       "cola": classifier_data_lib.ColaProcessor,
 7       "mnli": classifier_data_lib.MnliProcessor,
 8       "mrpc": classifier_data_lib.MrpcProcessor,
 9       "qnli": classifier_data_lib.QnliProcessor,
10       "sst-2": classifier_data_lib.SstProcessor,
11       "xnli": classifier_data_lib.XnliProcessor,
12       "bdbk": classifier_data_lib.BdbkProcessor, # 添加新的數據處理器
13   }
14 ......

 

文件2:classifier_data_lib.py

a. 添加新Processor的處理邏輯:

谷歌預設的數據處理器以及后續的分類邏輯中,都只使用了訓練集和驗證集,本文在此基礎上,增加了測試集和預測集的數據處理和使用

 1 class BdbkProcessor(DataProcessor):
 2     """Processor for bdbk data set."""
 3 
 4     def get_train_examples(self, data_dir):
 5         """See base class."""
 6         return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
 7 
 8     def get_dev_examples(self, data_dir):
 9         """See base class."""
10         return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
11 
12     def get_test_examples(self, data_dir):
13         """See base class."""
14         return self._create_examples(self._read_tsv(os.path.join(data_dir, "test.tsv")), "test")
15 
16     def get_predict_examples(self, data_dir):
17         return self._create_examples(self._read_tsv(os.path.join(data_dir, "predict.tsv")), "predict")
18 
19     def get_labels(self):
20         """See base class."""
21         return [
22             'human',
23             'poi',
24             'nature',
25             ... 此處根據你的需要,定義自己的類別標簽
26         ]
27     @staticmethod
28     def get_processor_name():
29         """See base class."""
30         return "BDBK"
31 
32     def _create_examples(self, lines, set_type):
33         """Creates examples for the training and dev sets."""
34         examples = []
35         for (i, line) in enumerate(lines):
36             guid = "%s-%s" % (set_type, i)
37             if i == 0: # 如果你的數據有標題行,就加上這兩句代碼跳過
38                 continue
39             text_a = self.process_text_fn(line[1]) # line[1]列是文本
40             label = self.process_text_fn(line[2]) if len(line) >= 3 else "na" # line[2]列是標簽 41             examples.append(InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
42         return examples

 

2.訓練、驗證及測試邏輯:

對tfrecord數據讀取、模型訓練結果存儲等功能進行調整

文件1:run_classifier.py

run_classifier是分類主程序入口,涵蓋了預訓練模型加載、分類模型構建及編譯、輸入數據讀取、模型訓練和評估、產出訓練結果等全過程。

其中分類模型構建環節中沒有添加其他分類網絡,即直接用BERT預訓練模型的輸出對接dropout(抑制過擬合)層后,直接接fc層產出各個類別的概率。此處也可以自定義其他網絡結構,比如BERT的輸出對接一個TextCNN,然后再接fc層(這種網絡結構也可以理解成BERT作為word embedding層,TextCNN作為分類模型)。本文中采用第一種默認結構,即BERT+dropout+fc,網絡模型結構如下:

a. 添加一些epoch結尾的評估metrics,便於評估訓練效果

 1     eval_data_list = list(evaluation_dataset.as_numpy_iterator())
 2     custom_metric = custom_metrics.Metrics(labels_list, valid_data=eval_data_list[0])
 3 
 4     if custom_callbacks is not None:
 5       custom_callbacks += [custom_metric, summary_callback, checkpoint_callback]
 6     else:
 7       custom_callbacks = [custom_metric, summary_callback, checkpoint_callback]
 8 
 9     history = bert_model.fit(
10         x=training_dataset,
11         validation_data=evaluation_dataset,
12         steps_per_epoch=steps_per_epoch,
13         epochs=epochs,
14         validation_steps=eval_steps,
15         callbacks=custom_callbacks)
16 
17     return bert_model, history, custom_metric

文件2:model_saving_utils.py

a. 在export_bert_model()最后添加訓練結果保存邏輯

    # tf.train.Checkpoint API was used via custom training loop logic.
    else:
      checkpoint = tf.train.Checkpoint(model=model)

      # Restores the model from latest checkpoint.
      latest_checkpoint_file = tf.train.latest_checkpoint(checkpoint_dir)
      assert latest_checkpoint_file
      logging.info('Checkpoint file %s found and restoring from '
                   'checkpoint', latest_checkpoint_file)
      checkpoint.restore(
          latest_checkpoint_file).assert_existing_objects_matched()

  model.save(model_export_path, include_optimizer=True, overwrite=True, save_format='h5') # 添加訓練結果保存 return

b. 新增custom_metrics.py,用於在on_epoch_end中計算自定義metrics

from sklearn.metrics import f1_score, recall_score, precision_score, classification_report
from tensorflow.keras.callbacks import Callback
import numpy as np

class Metrics(Callback):
    def __init__(self, labels_list, valid_data):
        super(Metrics, self).__init__()
        self.validation_data = valid_data
        self.val_f1s = []
        self.val_recalls = []
        self.val_precisions = []
        self.reports = []
        self.labels_list = labels_list

    def on_epoch_end(self, epoch, logs=None):
        logs = logs or {}
        val_predict = np.argmax(self.model.predict(self.validation_data[0]), -1) 
        val_targ = self.validation_data[1]
        if len(val_targ.shape) == 2 and val_targ.shape[1] != 1:
            val_targ = np.argmax(val_targ, -1) 

        # 分別計算macro f1, recall, precision
        _val_precision = precision_score(val_targ, val_predict, average='macro')
        self.val_precisions.append(_val_precision)
        logs['val_precision'] = _val_precision


        _val_recall = recall_score(val_targ, val_predict, average='macro')
        self.val_recalls.append(_val_recall)
        logs['val_recall'] = _val_recall

        _val_f1 = f1_score(val_targ, val_predict, average='macro')
        self.val_f1s.append(_val_f1)
        logs['val_f1'] = _val_f1
        remain_list = []
        val_union = np.union1d(val_targ, val_predict)
        for i, v in enumerate(self.labels_list):
            if i in val_union:
                remain_list.append(v)
        # 一並計算三個指標
        report = classification_report(val_targ,
                                       val_predict,
                                       target_names=remain_list,
                                       output_dict=True)

    def get(self, metrics, of_class):
        return [report[str(of_class)][metrics] for report in self.reports]

c. 新增utils.py,用於繪制訓練結果圖

#-*- encoding:utf-8 -*-
import pickle
import matplotlib.pyplot as plt 
import matplotlib.style as style

class MetricPlot(object):
    def __init__(self, file_pickle_path):
        self.file_pickle = file_pickle_path

    def draw_all_curves(self):
        with open(self.file_pickle + '/metric/metric.pickle', 'rb') as f:
            metric_data = pickle.load(f)
            reports = metric_data['reports']
            style.use("bmh")

            class_list = []
            for report in reports:
                class_list.extend(list(report.keys()))
            class_list = list(set(class_list))
            class_list.remove('accuracy')
            class_list.remove('macro avg')
            class_list.remove('weighted avg')

            # 每個類別分別打印precision、recall、f1
            class_num = len(class_list)
            # 每個類別分別打印precision、recall、f1
            class_num = len(class_list)
            plt.figure(figsize=(8, class_num * 4))
            for i, v in enumerate(class_list):
                plt.subplot(class_num, 1, i + 1)
                for m in {'precision', 'recall', 'f1-score'}:
                    plt.plot([report[v][m] for report in reports],
                           label='Class {0} {1}'.format(v, m))
                    plt.legend(loc='lower right')
                    plt.ylabel('Class {}'.format(v))
                    plt.title('Class {} Curves'.format(v))
            plt.show()

    def draw_metric_curves(self):
        with open(self.file_pickle + '/metric/metric.pickle', 'rb') as f:
            metric_data = pickle.load(f)
            f1 = metric_data['f1']
            recall = metric_data['recall']
            precision = metric_data['precision']

            epochs = len(f1)

            style.use("bmh")
            plt.figure(figsize=(8, 12))

            plt.subplot(3, 1, 1)
            plt.plot(range(1, epochs+1), f1, label='Val F1')
            plt.legend(loc='lower right')
            plt.ylabel('F1')
            plt.title('Validation F1 Curve')

            plt.subplot(3, 1, 2)
            plt.plot(range(1, epochs+1), recall, label='Val Recall')
            plt.legend(loc='lower right')
            plt.ylabel('Recall')
            plt.title('Validation Recall Curve')

            plt.subplot(3, 1, 3)
            plt.plot(range(1, epochs+1), precision, label='Val Precision')
            plt.legend(loc='lower right')
            plt.ylabel('Precision')
            plt.title('Validation Precision Curve')
            plt.xlabel('epoch')
            plt.show()
    def draw_history_curves(self):
        """Plot the learning curves of loss and macro f1 score 
        for the training and validation datasets.

        Args:
            history: history callback of fitting a tensorflow keras model 
        """
        with open(self.file_pickle + '/history/hist.pickle', 'rb') as f:
            history = pickle.load(f)

            loss = history['loss']
            val_loss = history['val_loss']
            accuracy = history['test_accuracy']
            val_accuracy = history['val_test_accuracy']

            epochs = len(loss)

            style.use("bmh")
            plt.figure(figsize=(8, 8)) 

            plt.subplot(2, 1, 1)
            plt.plot(range(1, epochs+1), loss, label='Training Loss')
            plt.plot(range(1, epochs+1), val_loss, label='Validation Loss')
            plt.legend(loc='upper right')
            plt.plot(range(1, epochs+1), val_loss, label='Validation Loss')
            plt.legend(loc='upper right')
            plt.ylabel('Loss')
            plt.title('Training and Validation Loss')

            plt.subplot(2, 1, 2)
            plt.plot(range(1, epochs+1), accuracy, label='Training Accuracy')
            plt.plot(range(1, epochs+1), val_accuracy, label='Validation Accuracy')
            plt.legend(loc='lower right')
            plt.ylabel('Accuracy')
            plt.title('Training and Validation Accuracy')
            plt.show()

主要的代碼邏輯修改介紹完畢,下面介紹在colab上操作的流程。

三、colab操作:

1.環境部署:

# 安裝tf-nightly-gpu,目前master各版本應該均可用(至少2.2.0.dev20200218親測可用)
# 如選擇發布版2.1.0,在預測階段(重新load_model)會觸發tf底層的一個bug(https://github.com/tensorflow/neural-structured-learning/issues/41),導致加載模型失敗
!pip install tf-nightly-gpu
# 下載並解壓tensorflow models2.1.0
!wget https://github.com/tensorflow/models/archive/v2.1.0.tar.gz
!tar xzf v2.1.0.tar.gz && rm v2.1.0.tar.gz
# 安裝models需要的依賴
!pip install -r /content/models-2.1.0/official/requirements.txt

2.將修改代碼上傳到colab,並放置到對應的目錄

# 下載數據集合和修改適配的文件
%rm -rf bert_multiclass_zh_model2-1-0/
%rm -rf process_dir
# 此處將修改好的代碼傳到colab上
<下載代碼>
%mv /content/bert_multiclass_zh_model2-1-0/code/utils.py /content/ %mv /content/bert_multiclass_zh_model2-1-0/code/*.py /content/models-2.1.0/official/nlp/bert/ %mv /content/bert_multiclass_zh_model2-1-0/process_dir /content/

3. 為了對數據有更直觀的了解,可以用下面代碼對數據的分布進行觀察

 1 import pandas as pd
 2 import matplotlib.pyplot as plt
 3 import matplotlib.style as style
 4 import seaborn as sns
 5 
 6 style.use("fivethirtyeight")
 7 plt.figure(figsize=(8, 12))
 8 
 9 show_data_dict = {
10     'train': pd.read_csv("/content/process_dir/train_data/train.tsv", "\t"), 
11     'dev': pd.read_csv("/content/process_dir/train_data/dev.tsv", "\t")
12 }
13 
14 # Get label frequencies in descending order
15 length = len(show_data_dict)
16 cnt = 0
17 for k, v in show_data_dict.items():
18     cnt += 1
19     plt.subplot(length, 1, cnt)
20     label_freq = v['label'].apply(lambda s: str(s)).value_counts().sort_values(ascending=False)
21     # Bar plot
22     sns.barplot(y=label_freq.index, x=label_freq.values, order=label_freq.index)
23     plt.title("{} label frequency".format(k), fontsize=14)
24     plt.xlabel("")
25     plt.xticks(fontsize=12)
26     plt.yticks(fontsize=12)
27 plt.show()

執行后可以看到數據分布情況:

 

 

 4.生成tfrecord格式的數據:

 1 !python ./models/official/nlp/bert/create_finetuning_data.py \
 2     --input_data_dir=${BASE_DIR}/train_data \
 3     --vocab_file=${BASE_DIR}/hub/vocab.txt \
 4     --train_data_output_path=${BASE_DIR}/train_data/${TASK_NAME}_train.tf_record \
 5     --eval_data_output_path=${BASE_DIR}/train_data/${TASK_NAME}_eval.tf_record \
 6     --test_data_output_path=${BASE_DIR}/train_data/${TASK_NAME}_test.tf_record \
 7     --meta_data_file_path=${BASE_DIR}/train_data/${TASK_NAME}_meta_data \
 8     --fine_tuning_task_type=classification \
 9     --max_seq_length=128 \
10     --classification_task_name=${TASK_NAME}

6. 啟動訓練:

 1 !python ./models/official/nlp/bert/run_classifier.py \
 2     --mode='train_and_eval' \
 3     --input_meta_data_path=${BASE_DIR}/train_data/${TASK_NAME}_meta_data \
 4     --train_data_path=${BASE_DIR}/train_data/${TASK_NAME}_train.tf_record \
 5     --eval_data_path=${BASE_DIR}/train_data/${TASK_NAME}_eval.tf_record \
 6     --test_data_path=${BASE_DIR}/train_data/${TASK_NAME}_test.tf_record \
 7     --bert_config_file=${BASE_DIR}/hub/bert_config.json \
 8     --train_batch_size=64 \
 9     --eval_batch_size=128 \
10     --test_batch_size=256 \
11     --steps_per_loop=1 \
12     --learning_rate=2e-5 \
13     --num_train_epochs=2 \
14     --model_dir=${BASE_DIR}/output \
15     --hub_module_url=https://tfhub.dev/tensorflow/bert_zh_L-12_H-768_A-12/1 \
16     --use_keras_compile_fit=True \
17     --distribution_strategy=mirrored \
18     --num_gpus=1 \
19     --save_history_path=${BASE_DIR}/history/hist.pickle \
20     --save_metric_path=${BASE_DIR}/metric/metric.pickle \
21     --model_export_path=${BASE_DIR}/export_model/save_model.h5 \
22     --test_result_dir=${BASE_DIR}/test_result/test_ret

因為在run_classifiert.py中model.fit()之前增加了一行model.summary(),所以訓練開始前的日志中可見訓練圖層次結構等信息

整個模型參數都設置為是trainable的(默認值),筆者嘗試過修改代碼來freeze BERT自帶的102267649個參數,但訓練10個epoch后,accuracy仍達不到90%以上,感覺效果不是很好。具體原因還未深究。

7. 查看訓練結果:

日志中可以查看訓練中,每個batch結束后的訓練loss、accuracy;每個epoch結束后的val loss、val accuracy;以及整個訓練結束后的test accuracy

INFO:tensorflow:Reduce to /job:localhost/replica:0/task:0/device:CPU:0 then broadcast to ('/job:localhost/replica:0/task:0/device:CPU:0',).
I0223 04:38:43.759790 140623330674560 cross_device_ops.py:414] Reduce to /job:localhost/replica:0/task:0/device:CPU:0 then broadcast to ('/job:localhost/replica:0/task:0/device:CPU:0',).
INFO:tensorflow:Reduce to /job:localhost/replica:0/task:0/device:CPU:0 then broadcast to ('/job:localhost/replica:0/task:0/device:CPU:0',).
I0223 04:38:43.761969 140623330674560 cross_device_ops.py:414] Reduce to /job:localhost/replica:0/task:0/device:CPU:0 then broadcast to ('/job:localhost/replica:0/task:0/device:CPU:0',).
Train for 1093 steps, validate for 313 steps
Epoch 1/2
1093/1093 [==============================] - 1161s 1s/step - loss: 0.4250 - accuracy: 0.8925 - val_loss: 0.1785 - val_accuracy: 0.9474
Epoch 2/2
1093/1093 [==============================] - 1144s 1s/step - loss: 0.1220 - accuracy: 0.9664 - val_loss: 0.1622 - val_accuracy: 0.9532
I0223 05:18:29.677855 140623330674560 run_classifier.py:420] test_accuracy: 0.951200008392334

 

查看原代碼里定義的metrics(loss、accuracy),以及自定義的metrics(各類別的F1、precise、recall等)

1 import utils
2 
3 mp = utils.MetricPlot('/content/process_dir/')
4 mp.draw_history_curves()
5 mp.draw_metric_curves()
6 mp.draw_all_curves()

 

 

訓練完成之后,就可以進行預測了。

首先參考create_finetune_data.py,將待預測數據轉換為tfrecord格式並存儲

# 預測,生成待預測數據
!python ./models-2.1.0/official/nlp/bert/do_pred_data.py \
    --input_data_dir=${PRED_DATA_DIR} \
    --vocab_file=${BASE_DIR}/model/hub_config/vocab.txt \
    --predict_data_output_path=${PRED_DATA_DIR}/${TASK_NAME}_predict.tf_record \
    --meta_data_file_path=${PRED_DATA_DIR}/${TASK_NAME}_meta_data \
    --max_seq_length=128

然后參照run_classifier.py,讀取待預測tfrecord數據,加載之前訓練好的模型(save_model.h5),開始預測,將結果保存在文件中

# 開始預測
!python ./models-2.1.0/official/nlp/bert/do_predict.py \
    --input_meta_data_path=${PRED_DATA_DIR}/${TASK_NAME}_meta_data \
    --predict_data_path=${PRED_DATA_DIR}/${TASK_NAME}_predict.tf_record \
    --bert_config_file=${BASE_DIR}/model/hub_config/bert_config.json \
    --predict_batch_size=32 \
    --hub_module_url=https://tfhub.dev/tensorflow/bert_zh_L-12_H-768_A-12/1 \
    --model_export_path=${BASE_DIR}/model/saved_model/save_model.h5 \
    --predict_output_dir=${PRED_DATA_DIR}/result \
# 如果選擇tf2.1.0版本,則此處會觸發tf底層的一個bug(https://github.com/tensorflow/neural-structured-learning/issues/41),導致加載模型失敗

最終訓練出來的文件內容是這樣的:第一列是標簽的編號,第二列是標簽,由於是串行訓練,因此輸出的標簽和輸入的預測文本是一一對齊的。

1    poi
4    webfic
7    science
0    human
0    human
0    human
1    poi
7    science

至此,整個finetuneing訓練+預測過程全部結束。歡迎拍磚指導~~~~

稍后再研究一下如何部署到生產環境…… 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM