深度學習之Bert中文分類學習

2021-05-10 21:01:15

深度學習之Bert中文分類學習

BERT實驗

預訓練結果分析

tfhub_handle_preprocess = "https://hub.tensorflow.google.cn/tensorflow/bert_zh_preprocess/3"
bert_preprocess_model = hub.KerasLayer(tfhub_handle_preprocess)

text_test = ['我真是個天才啊!!']
text_preprocessed = bert_preprocess_model(text_test)

print(f'Keys       : {list(text_preprocessed.keys())}')
print(f'Shape      : {text_preprocessed["input_word_ids"].shape}')
print(f'Word Ids   : {text_preprocessed["input_word_ids"][0, :12]}')
print(f'Input Mask : {text_preprocessed["input_mask"][0, :12]}')
print(f'Type Ids   : {text_preprocessed["input_type_ids"][0, :12]}')

列印結果

Keys       : ['input_mask', 'input_type_ids', 'input_word_ids']
Shape      : (1, 128)
Word Ids   : [ 101 2769 4696 3221  702 1921 2798 1557 8013 8013  102    0]
Input Mask : [1 1 1 1 1 1 1 1 1 1 1 0]
Type Ids   : [0 0 0 0 0 0 0 0 0 0 0 0]

Shape中的128猜想應該是 最大長度。

Keys對應了三個屬性,但其實bert應該是有7個特徵屬性。為什麼另外四個屬性在這裡沒有,目前不是很清楚,但我覺得Hub裡面的Advance topics估計是在講這個事情。但由於現在要做的任務是文字分類,以下的四個特徵是不需要的。

  • input_ids: 輸入的token對應的id
  • input_mask: 輸入的mask,1代表是正常輸入,0代表的是padding的輸入
  • segment_ids: 輸入的0:代表句子A或者padding句子,1代表句子B
  • masked_lm_positions:我們mask的token的位置
  • masked_lm_ids:我們mask的token的對應id
  • masked_lm_weights:我們mask的token的權重,1代表是真實mask的,0代表的是padding的mask
  • next_sentence_labels:句子A和B是否是上下句

接下來看下輸出怎麼理解。為了保證自己的猜想正確,我又丟了一句真是個天才啊!!進去看結果。把兩個結果合併起來是這樣的。

Word Ids   : [ 101 2769 4696 3221  702 1921 2798 1557 8013 8013  102    0]
Word Ids   : [ 101 4696 3221  702 1921 2798 1557 8013 8013  102    0    0]
Input Mask : [1 1 1 1 1 1 1 1 1 1 1 0]
Input Mask : [1 1 1 1 1 1 1 1 1 1 0 0]
Type Ids   : [0 0 0 0 0 0 0 0 0 0 0 0]
Type Ids   : [0 0 0 0 0 0 0 0 0 0 0 0]

有意思吧。bert目前沒有分片語的概念,它是一個一個詞分開的。根據資料檢視bert的預訓練分成兩步,這個顯然是第一步。

我真是個天才啊啊!被分割成了[CLS] 我 真 是 個 天 才 啊 啊 ![SEP],所以這個跟Word Ids是能對得上的。
TypeIds應該跟segment_ids是對等的,也是對得上的,都是0。
Input Mask也是對得上的。它的演演算法如下
mask是1表示是"真正"的Token,0則是Padding出來的。
在後面的Attention時會通過tricky的技巧讓模型不能attend to這些padding出來的Token上。
input_mask = [1] * len(input_ids)

結果輸出分析

tfhub_handle_encoder = "https://hub.tensorflow.google.cn/tensorflow/bert_zh_L-12_H-768_A-12/4"
bert_model = hub.KerasLayer(tfhub_handle_encoder)

bert_results = bert_model(text_preprocessed)
print(f'Keys       : {list(bert_results.keys())}')
print(f'Pooled Outputs Shape:{bert_results["pooled_output"].shape}')
print(f'Pooled Outputs Values:{bert_results["pooled_output"][0, :12]}')
print(f'Sequence Outputs Shape:{bert_results["sequence_output"].shape}')
print(f'Sequence Outputs Values:{bert_results["sequence_output"][0, :12]}')
  • sequence_output:維度【batch_size, seq_length, hidden_size】,這是訓練後每個token的詞向量。
  • pooled_output:維度是【batch_size, hidden_size】,每個sequence第一個位置CLS的向量輸出,用於分類任務。

BERT實戰中文多分類

參考官方的BERT分類,官方用的是情感分析的場景,其實跟中文多分類場景類似。
要做的任務就是換BERT的pre-process和encoder,再把最後一層輸出換成多分類即可。

基本可以照搬。

環境準備

pip install -q tensorflow-text
pip install -q tf-models-official

import tensorflow as tf
import os
import shutil

import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_text as text
from official.nlp import optimization  # to create AdamW optimizer

import matplotlib.pyplot as plt

tfhub_handle_preprocess = "https://hub.tensorflow.google.cn/tensorflow/bert_zh_preprocess/3"
tfhub_handle_encoder = "https://hub.tensorflow.google.cn/tensorflow/bert_zh_L-12_H-768_A-12/4"
bert_preprocess_model = hub.KerasLayer(tfhub_handle_preprocess)
bert_model = hub.KerasLayer(tfhub_handle_encoder)

準備資料集

因為是在Colab做的實驗,上傳貌似只能上傳檔案的形式。所以在本地壓縮成一個zip包,上傳到Colab後做解壓。

import zipfile
from pathlib import Path
zFile = zipfile.ZipFile("path.zip","r")
for fileM in zFile.namelist(): 
        zFile.extract(fileM, "path")
zFile.close();

檔案格式的要求跟官方範例一致即可。

檔案目錄
    分類1
        分類1的標題1
        分類1的標題2
        ...
    分類2
    分類3
    ...

拆分資料集

AUTOTUNE = tf.data.AUTOTUNE
batch_size = 32
seed = 42

raw_train_ds = tf.keras.preprocessing.text_dataset_from_directory(
    'train',
    batch_size=batch_size,
    validation_split=0.2,
    subset='training',
    seed=seed)

class_names = raw_train_ds.class_names
print(class_names)
train_ds = raw_train_ds.cache().prefetch(buffer_size=AUTOTUNE)
val_ds = tf.keras.preprocessing.text_dataset_from_directory(
    'train',
    batch_size=batch_size,
    validation_split=0.2,
    subset='validation',
    seed=seed)

val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

看一下資料集

for text_batch, label_batch in train_ds.take(1):
  print(text_batch)
  for i in range(3):
    print(f'Review: {text_batch.numpy()[i]}')
    label = label_batch.numpy()[i]
    print(f'Label : {label} ({class_names[label]})')

定義模型

def build_classifier_model():
  text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
  preprocessing_layer = hub.KerasLayer(tfhub_handle_preprocess, name='preprocessing')
  encoder_inputs = preprocessing_layer(text_input)
  encoder = hub.KerasLayer(tfhub_handle_encoder, trainable=True, name='BERT_encoder')
  outputs = encoder(encoder_inputs)
  net = outputs['pooled_output']
  net = tf.keras.layers.Dropout(0.5)(net)

  net = tf.keras.layers.Dense(6, activation='softmax', name='classifier')(net)
  return tf.keras.Model(text_input, net)

classifier_model = build_classifier_model()
epochs = 10
steps_per_epoch = tf.data.experimental.cardinality(train_ds).numpy()
num_train_steps = steps_per_epoch * epochs
print(num_train_steps)
num_warmup_steps = int(0.1*num_train_steps)

init_lr = 3e-5
optimizer = optimization.create_optimizer(init_lr=init_lr,                                          num_train_steps=num_train_steps,                                          num_warmup_steps=num_warmup_steps,
optimizer_type='adamw')
loss = tf.keras.losses.SparseCategoricalCrossentropy()
metrics = tf.metrics.SparseCategoricalAccuracy()

classifier_model.compile(optimizer=optimizer,
                         loss=loss,
                         metrics=metrics)

訓練模型

history = classifier_model.fit(x=train_ds,
                               validation_data=val_ds,
                               epochs=epochs)

預測資料

發現雖然我只用了300條樣本,train樣本240條,val樣本60條,但是準確率也能到90%。BERT果然叼。

import numpy as np

def print_my_examples(inputs, results):
  result_for_printing = \
    [f'input: {inputs[i]:<30} : class: {class_names[np.argmax(results[i])] }'
                         for i in range(len(inputs))]
  print(*result_for_printing, sep='\n')
  print()

examples = ['德國米技(MIJI)蒸汽噴淋式煮茶壺 全自動保溫泡茶壺HK-K018',
            '公牛插座收納盒裝辦公接線板家用創意電源',
            '生活元素燒水壺辦公室家用多功能小型燒水壺保溫一體煮茶器煮茶壺',
            '信安智囊老人防摔防跌倒術後保護重複使用智慧氣囊馬甲服神器送禮',
            '七度空間衛生巾優雅系列日夜超值組合',
            ' 【貝拉家】貝拉爸爸自己家工廠生產的科技布狗窩']
example_result = classifier_model(tf.constant(examples))

print_my_examples(examples,example_result)

附錄