本文正在参与「金石计划 . 分割6万现金大奖」

前言

本文运用 cpu 版本的 Tensorflow 2.8 ,经过建立 BERT 模型完成文本分类使命。

大纲

  1. python 库预备
  2. BERT 是什么?
  3. 获取并处理 IMDB 数据
  4. 初始 TensorFlow Hub 中的 BERT 处理器和模型
  5. 建立模型
  6. 练习模型
  7. 测验模型
  8. 保存模型
  9. 从头加载模型并进行猜测

1. python 库预备

为了保证能正常运行本文代码,需求保证以下库的版本:

  • tensorflow==2.8.4
  • tensorflow-text==2.8.1
  • tf-models-official==2.7.0
  • python==3.8.0

在装置 tf-models-official 的时分或许会报错 :Microsoft Visual C++ 14.0 or greater is required 。直接进入 visualstudio.microsoft.com/zh-hans/vis… 这儿进行下载新的Microsoft C++ 生成东西,然后装置重启电脑即可。

2. BERT 是什么?

BERT 和其他 Transformer 编码器架构模型都在 NLP 的各种使命上取得了巨大的成功。它们都是运用了多层的注意力机制,能够有效地对文本进行双向的深层次语义编码表明。BERT 模型现已在大型文本语料库上进行了足够的预练习,咱们在运用的时分只需求针对特定使命进行微调即可。

3. 获取并处理 IMDB 数据

(1)运用 tensorflow 的内置函数,从网络大将 Large Movie Review Dataset 数据下载到本地,没有特别指定的话一般方位在当前同级目录下。此数据集是一个电影谈论数据集,其间包含来自 Internet 电影数据库的 50000 条电影谈论的文本,每个文本都对应一个标签标记其为活跃或者消沉的。

(2)咱们将数据中无用的 unsup 文件夹都删掉,这样后边处理数据会愈加方便。

import os
import shutil
import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_text as text
from official.nlp import optimization
import matplotlib.pyplot as plt
tf.get_logger().setLevel('ERROR')
url = 'https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz'
dataset = tf.keras.utils.get_file('aclImdb_v1.tar.gz', url, untar=True, cache_dir='.', cache_subdir='')
dataset_dir = os.path.join(os.path.dirname(dataset), 'aclImdb')
train_dir = os.path.join(dataset_dir, 'train')
remove_dir = os.path.join(train_dir, 'unsup')
shutil.rmtree(remove_dir)

(3)咱们能够直接运用内置函数 text_dataset_from_directory 直接从硬盘读取数据生成 tf.data.Dataset 。

(4)IMDB 数据集现已被分为了练习集和测验集,可是还缺少验证集,所以让咱们需求从练习集中取出 20% 来创立一个验证集。终究练习集 20000 个样本,验证集 5000 个样本,测验集 25000 个样本。每个样本都是 (text,label) 对。

(5)为了保证在加载数据的时分不会呈现 I/O 不会堵塞,咱们在从磁盘加载完数据之后,运用 cache 会将数据保存在内存中,保证在练习模型过程中数据的获取不会成为练习速度的瓶颈。假如说要保存的数据量太大,能够运用 cache 创立磁盘缓存进步数据的读取功率。别的咱们还运用 prefetch 在练习过程中能够并行执行数据的预获取。

AUTOTUNE = tf.data.AUTOTUNE
batch_size = 64
seed = 110
train_datas = tf.keras.utils.text_dataset_from_directory( 'aclImdb/train', batch_size=batch_size, validation_split=0.2, subset='training', seed=seed)
class_names = train_datas.class_names
train_datas = train_datas.cache().prefetch(buffer_size=AUTOTUNE)
val_datas = tf.keras.utils.text_dataset_from_directory(  'aclImdb/train', batch_size=batch_size, validation_split=0.2, subset='validation', seed=seed)
val_datas = val_datas.cache().prefetch(buffer_size=AUTOTUNE)
test_datas = tf.keras.utils.text_dataset_from_directory( 'aclImdb/test', batch_size=batch_size)
test_datas = test_datas.cache().prefetch(buffer_size=AUTOTUNE)

(6)随机取出两个处理好的样本进行展现:

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

成果输出:

Review: b"This 30 minute documentary Bu\xc3\xb1uel made in the early 1930's about one of Spain's poorest regions is,"...
Label : 0 (neg)
Review: b'I\'ve tried to watch this show several times, but for a show called "That \'70s Show," I don\'t find mu'...
Label : 0 (neg)

4. 初识 TensorFlow Hub 中的 BERT 处理器和模型

(1)因为正规的从 TensorFlow Hub 下载模型需求“科学上网”,所以咱们能够到这个镜像网站(hub.tensorflow.google.cn/google/coll… BERT 模型,为了方便咱们快速学习,咱们选用了比较小的 Small BERT ,及其对应的数据输入处理器。一般下载到本地的途径为 C:\Users\(用户名)\AppData\Local\Temp\tfhub_modules\ 下面。

(2)preprocess 能够将文本转化成 BERT 所需求的输入,这样就免去了自己写 Python 代码来预处理文本来习惯 BERT 模型的输入。这儿会对文本处理发生对应的三个张量 input_word_ids、input_type_ids、input_mask :

  • input_word_ids:一个 [batch_size, 128] 的 int32 张量,每个张量包含了每句话中每个 token 对应的整数映射,而且包含了 START、END、PAD 对应的整数符号。如比如所见 how are you 对应的 input_word_ids 向量维度为 128 , 101 对应 START ,102 对应 END ,中心的数字对应文本中的三个单词,其余的 0 对应 PAD 。
  • input_mask:一个 [batch_size, 128] 的 int32 张量,PAD 之前的方位,也便是 START、END、以及 token 对应的整数的方位都是用 1 表明,填充 PAD 之后的方位都用 0 表明。如比如所见 how are you 对应的 input_mask 向量维度都为 128 ,前 5 个方位都是 1 ,后边满是 0 。
  • input_type_ids:一个 [batch_size, 128] 的 int32 张量,假如输入是分段的,那么第一个输入段包含 START 和 END 的对应方位的都为 0 。假如存在第二段则包含 END 在内的输入都用 1 进行表明, 假如存在第三段则用 2 进行表明,也便是每一段都有一个不同的数字进行表明,剩下 PAD 填充的方位依然用 0 表明。如比如所见 how are you 对应的 input_type_ids 向量维度为 128 ,前 5 个方位都是 0 ,因为没有第二段,所以后边都是 PAD 依然用 0 表明。

(3)同样咱们也运用了 small_bert 接纳 preprocess 处理之后的成果,这时咱们能够发生四个对应的张量 pooled_output、sequence_output、default、encoder_outputs ,这儿咱们首要用到前两个:

  • pooled_output:一个 [batch_size, 512] 的 float32 张量,每个张量都是 512 维,表明将每个输入序列都编码为一个 512 维的表明向量。
  • sequence_output:一个 [batch_size, 128,512] 的 float32 张量,每个张量都是 [128, 512] 维,表明每个输入序列的每个 token 的编码成果输出是 512 维的表明。

处理器和模型获取:

preprocess_url  = 'https://hub.tensorflow.google.cn/tensorflow/bert_en_uncased_preprocess/3'
preprocess = hub.KerasLayer(preprocess_url)
bert_url  = 'https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-512_A-8/2'
bert_model = hub.KerasLayer(bert_url)

处理器比如展现:

text_test = ['how are you']
preprocess_result = preprocess(text_test)
print(f'keys           : {list(preprocess_result.keys())}')
print(f'shape          : {preprocess_result["input_word_ids"].shape}')
print(f'input_word_ids : {preprocess_result["input_word_ids"]}')
print(f'input_mask     : {preprocess_result["input_mask"]}')
print(f'input_type_ids : {preprocess_result["input_type_ids"]}')

输出:

keys           : ['input_word_ids', 'input_type_ids', 'input_mask']
shape          : (1, 128)
input_word_ids : [[ 101 2129 2024 2017  102    0    0    0    0    0    0    0    0    0
     0    0    0    0    0    0    0    0    0    0    0    0    0    0
     0    0    0    0    0    0    0    0    0    0    0    0    0    0
     0    0    0    0    0    0    0    0    0    0    0    0    0    0
     0    0    0    0    0    0    0    0    0    0    0    0    0    0
     0    0    0    0    0    0    0    0    0    0    0    0    0    0
     0    0    0    0    0    0    0    0    0    0    0    0    0    0
     0    0    0    0    0    0    0    0    0    0    0    0    0    0
     0    0    0    0    0    0    0    0    0    0    0    0    0    0
     0    0]]
input_mask     : [[1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]]
input_type_ids : [[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]]

模型比如展现:

bert_results = bert_model(preprocess_result)
print(f'Loaded BERT             : {bert_url}')
print(f'Keys                    : {list(bert_results.keys())}')
print(f'Pooled Outputs Shape    :{bert_results["pooled_output"].shape}')
print(f'Sequence Outputs Values :{bert_results["pooled_output"].dtype}')
print(f'Sequence Outputs Shape  :{bert_results["sequence_output"].shape}')
print(f'Sequence Outputs Values :{bert_results["sequence_output"].dtype}')

输出:

Loaded BERT             : https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-512_A-8/2
Keys                    : ['pooled_output', 'sequence_output', 'default', 'encoder_outputs']
Pooled Outputs Shape    :(1, 512)
Sequence Outputs Values :<dtype: 'float32'>
Sequence Outputs Shape  :(1, 128, 512)
Sequence Outputs Values :<dtype: 'float32'>	

5. 建立模型

(1)第一层是输入层,用来接纳用户输入的文本。

(2)第二层是咱们上面现已介绍过得数据处理层,直接用从 TensorFlow Hub 下载的 bert_en_uncased_preprocess 处理器即可。

(3)第三层是咱们的 BERT 层,这儿也是用咱们上面介绍过得模型,直接运用从 TensorFlow Hub 下载的 bert_en_uncased_L-8_H-512_A-8 模型即可。

(4)第四层是一个 Dropout 层,用来将 BERT 输出进行随机丢掉,避免过拟合。

(5)第五层一个输出 1 维向量的全连接层,其实便是输出该样本的分类 logit 。

def create_model():
    text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
    preprocessing_layer = hub.KerasLayer(preprocess, name='preprocessing')
    encoder_inputs = preprocessing_layer(text_input)
    encoder = hub.KerasLayer(bert_url, trainable=True, name='BERT_encoder')
    outputs = encoder(encoder_inputs)
    net = outputs['pooled_output']
    net = tf.keras.layers.Dropout(0.1)(net)
    net = tf.keras.layers.Dense(1, activation=None, name='classifier')(net)
    return tf.keras.Model(text_input, net)
model = create_model()

6. 练习模型

(1)因为这是一个二元分类问题,而且模型终究输出的是概率,因此咱们挑选 BinaryCrossentropy 作为丢失函数。运用 BinaryAccuracy 作为咱们的评价指标,在进行猜测的时分模型输出概率大于 threshold 的猜测为 1 也便是活跃情绪的,小于等于 threshold 的猜测为 0 ,也便是消沉的,threshold 默认是 0.5 。

(2)为了进行微调,咱们运用 BERT 开端练习时用的的优化器:Adam 。该优化器最大程度削减猜测丢失,并经过权重衰减进行正则化,所以它也被称为 AdamW 。

(3)咱们运用与 BERT 预练习相同的学习率(也便是咱们的 init_lr 变量),练习刚开端时,采用较小的学习率,跟着迭代次数添加学习率线性增大,当迭代步到达 num_warmup_steps 时,学习率设置为为初始设定的学习率 init_lr ,然后学习率跟着迭代次数逐渐衰减。BERT 论文中将用于微调的初始学习率设置较小,如:5e-5,3e-5,2e-5 。

(4)为什么运用 adamw 优化器 ?因为刚开端练习时,模型的权重是随机初始化的,此时若挑选一个较大的学习率,或许带来模型优化的不安稳(振荡),挑选 AdamW 优化器,能够使得开端练习的若干 epoches 或者 steps 内学习率较小,在预热的小学习率下,模型能够渐渐趋于安稳,等模型相对安稳后再挑选预先设置的学习率进行练习(此后的学习率是衰减的),有助于使模型收敛速度变快,作用更佳。

print(f'Training model with {bert_url}')
epochs = 5
steps_per_epoch = tf.data.experimental.cardinality(train_datas).numpy()
num_train_steps = steps_per_epoch * epochs
num_warmup_steps = int(0.1*num_train_steps)
optimizer = optimization.create_optimizer(init_lr=3e-5,  num_train_steps=num_train_steps, num_warmup_steps=num_warmup_steps, optimizer_type='adamw')
model.compile(optimizer=optimizer, loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), metrics=tf.metrics.BinaryAccuracy())
history = model.fit(x=train_datas, validation_data=val_datas, epochs=epochs)

练习过程,能够看出相当耗时,这也是运用 BERT 的一个明显缺点:

Training model with https://hub.tensorflow.google.cn/tensorflow/small_bert/bert_en_uncased_L-8_H-512_A-8/2
Epoch 1/5
313/313 [==============================] - 3433s 11s/step - loss: 0.4705 - binary_accuracy: 0.7515 - val_loss: 0.3789 - val_binary_accuracy: 0.8124
Epoch 2/5
313/313 [==============================] - 3328s 11s/step - loss: 0.3043 - binary_accuracy: 0.8653 - val_loss: 0.3734 - val_binary_accuracy: 0.8450
Epoch 3/5
313/313 [==============================] - 3293s 11s/step - loss: 0.2301 - binary_accuracy: 0.9024 - val_loss: 0.4295 - val_binary_accuracy: 0.8532
Epoch 4/5
313/313 [==============================] - 3289s 11s/step - loss: 0.1697 - binary_accuracy: 0.9344 - val_loss: 0.4831 - val_binary_accuracy: 0.8492
Epoch 5/5
313/313 [==============================] - 3411s 11s/step - loss: 0.1308 - binary_accuracy: 0.9497 - val_loss: 0.4631 - val_binary_accuracy: 0.8538

7. 测验模型

运用测验数据对练习好的模型进行评价,能够看到准确率到达了 0.8630 ,假如给予足够的调参和练习时刻,作用会更好。

model.evaluate(test_datas)

输出:

391/391 [==============================] - 1153s 3s/step - loss: 0.4290 - binary_accuracy: 0.8630

8. 保存模型

将练习好的模型保存到本地,以后能够随时读取模型进行猜测作业。

dataset_name = 'imdb'
saved_model_path = './{}_bert'.format(dataset_name.replace('/', '_'))
model.save(saved_model_path, include_optimizer=False)

9. 从头加载模型并进行猜测

咱们将运用上面现已存在的模型 model 和方才从头加载的模型 reloaded_model 进行猜测,将一个活跃情绪样本和一个消沉情绪样本输入模型,发现能够猜测正确(接近),而且两个模型的成果是相同的。

def print_my_examples(inputs, results):
    result_for_printing =  [f'input: {inputs[i]:<30} : score: {results[i][0]:.6f}' for i in range(len(inputs))]
    print(*result_for_printing, sep='\n')
examples = ['The movie was great!', 'The movie was terrible...']
reloaded_model = tf.saved_model.load(saved_model_path)
reloaded_results = tf.sigmoid(reloaded_model(tf.constant(examples)))
original_results = tf.sigmoid(model(tf.constant(examples)))
print('Results from reloaded_model:')
print_my_examples(examples, reloaded_results)
print('Results from model:')
print_my_examples(examples, original_results)

成果输出:

Results from reloaded_model:
input: The movie was great!           : score: 0.994967
input: The movie was terrible...      : score: 0.002266
Results from model:
input: The movie was great!           : score: 0.994967
input: The movie was terrible...      : score: 0.002266