微调方法
有多种方法可以针对目标任务微调 BERT。
- 进一步预训练基础 BERT 模型
- 可训练的基础 BERT 模型之上的自定义分类层
- 基础 BERT 模型之上的自定义分类层不可训练(冻结)
请注意,BERT 基础模型仅针对原始论文中的两个任务进行了预训练。
3.1 预训练 BERT ...我们使用两个无监督任务预训练 BERT
- 任务 #1:蒙面 LM
- 任务 #2:下一句预测 (NSP)
因此,基本 BERT 模型就像半生不熟,可以完全针对目标域进行烘焙(第一种方式)。我们可以将它用作我们自定义模型训练的一部分,使用基础可训练(第 2 次)或不可训练(第 3 次)。
第一种方法
如何微调 BERT 以进行文本分类?演示了进一步预训练的第一种方法,并指出学习率是避免灾难性遗忘的关键,在学习新知识的过程中预训练的知识被擦除。
我们发现较低的学习率(例如 2e-5)对于使 BERT 克服灾难性遗忘问题是必要的。在 4e-4 的激进学习率下,训练集无法收敛。
大概这就是BERT 论文使用 5e-5、4e-5、3e-5 和 2e-5 进行微调的原因。
我们使用 32 的批量大小,并对所有 GLUE 任务的数据进行 3 个 epoch 的微调。对于每个任务,我们在开发集上选择了最佳微调学习率(在 5e-5、4e-5、3e-5 和 2e-5 中)
请注意,基础模型预训练本身使用了更高的学习率。
该模型在 Pod 配置中的 4 个云 TPU(总共 16 个 TPU 芯片)上进行了 100 万步训练,批量大小为 256。序列长度限制为 90% 的步长为 128 个标记,其余 10% 的步长为 512 个。使用的优化器是 Adam,学习率为1e-4
, β1=0.9
和 β2= 0.999
,权重衰减为0.01
,学习率预热 10,000 步,之后学习率线性衰减。
将在下面将第一种方法描述为第三种方法的一部分。
仅供参考:
TFDistilBertModel是名称为 的裸基础模型distilbert
。
Model: "tf_distil_bert_model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
distilbert (TFDistilBertMain multiple 66362880
=================================================================
Total params: 66,362,880
Trainable params: 66,362,880
Non-trainable params: 0
第二种方法
Huggingface 采用第二种方法,如使用原生 PyTorch/TensorFlow 进行微调,其中在可训练的基本模型之上TFDistilBertForSequenceClassification
添加了自定义分类层。小的学习率要求也将适用,以避免灾难性的遗忘。classifier
distilbert
from transformers import TFDistilBertForSequenceClassification
model = TFDistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased')
optimizer = tf.keras.optimizers.Adam(learning_rate=5e-5)
model.compile(optimizer=optimizer, loss=model.compute_loss) # can also use any keras loss fn
model.fit(train_dataset.shuffle(1000).batch(16), epochs=3, batch_size=16)
Model: "tf_distil_bert_for_sequence_classification_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
distilbert (TFDistilBertMain multiple 66362880
_________________________________________________________________
pre_classifier (Dense) multiple 590592
_________________________________________________________________
classifier (Dense) multiple 1538
_________________________________________________________________
dropout_59 (Dropout) multiple 0
=================================================================
Total params: 66,955,010
Trainable params: 66,955,010 <--- All parameters are trainable
Non-trainable params: 0
第二种方法的实施
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split
from transformers import (
DistilBertTokenizerFast,
TFDistilBertForSequenceClassification,
)
DATA_COLUMN = 'text'
LABEL_COLUMN = 'category_index'
MAX_SEQUENCE_LENGTH = 512
LEARNING_RATE = 5e-5
BATCH_SIZE = 16
NUM_EPOCHS = 3
# --------------------------------------------------------------------------------
# Tokenizer
# --------------------------------------------------------------------------------
tokenizer = DistilBertTokenizerFast.from_pretrained('distilbert-base-uncased')
def tokenize(sentences, max_length=MAX_SEQUENCE_LENGTH, padding='max_length'):
"""Tokenize using the Huggingface tokenizer
Args:
sentences: String or list of string to tokenize
padding: Padding method ['do_not_pad'|'longest'|'max_length']
"""
return tokenizer(
sentences,
truncation=True,
padding=padding,
max_length=max_length,
return_tensors="tf"
)
# --------------------------------------------------------------------------------
# Load data
# --------------------------------------------------------------------------------
raw_train = pd.read_csv("./train.csv")
train_data, validation_data, train_label, validation_label = train_test_split(
raw_train[DATA_COLUMN].tolist(),
raw_train[LABEL_COLUMN].tolist(),
test_size=.2,
shuffle=True
)
# --------------------------------------------------------------------------------
# Prepare TF dataset
# --------------------------------------------------------------------------------
train_dataset = tf.data.Dataset.from_tensor_slices((
dict(tokenize(train_data)), # Convert BatchEncoding instance to dictionary
train_label
)).shuffle(1000).batch(BATCH_SIZE).prefetch(1)
validation_dataset = tf.data.Dataset.from_tensor_slices((
dict(tokenize(validation_data)),
validation_label
)).batch(BATCH_SIZE).prefetch(1)
# --------------------------------------------------------------------------------
# training
# --------------------------------------------------------------------------------
model = TFDistilBertForSequenceClassification.from_pretrained(
'distilbert-base-uncased',
num_labels=NUM_LABELS
)
optimizer = tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE)
model.compile(
optimizer=optimizer,
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
)
model.fit(
x=train_dataset,
y=None,
validation_data=validation_dataset,
batch_size=BATCH_SIZE,
epochs=NUM_EPOCHS,
)
第三种方法
基本
请注意,图片取自A Visual Guide to Using BERT for First Time并进行了修改。
分词器
Tokenizer生成 BatchEncoding 的实例,可以像 Python 字典一样使用它和 BERT 模型的输入。
保存 encode_plus() 和 batch_encode() 方法的输出(令牌、注意掩码等)。
该类派生自python字典,可以用作字典。此外,该类公开了从单词/字符空间映射到标记空间的实用方法。
参数
- data (dict) -- 由 encode/batch_encode 方法('input_ids'、'attention_mask' 等)返回的列表/数组/张量的字典。
类的data
属性是生成的具有input_ids
和attention_mask
元素的标记。
输入 ID 通常是作为输入传递给模型的唯一必需参数。它们是令牌索引,构建将用作模型输入的序列的令牌的数字表示。
注意掩码
该参数向模型指示应该注意哪些标记,哪些不应该注意。
如果 attention_mask 是0
,则忽略令牌 ID。例如,如果对序列进行填充以调整序列长度,则应忽略填充的单词,因此它们的 attention_mask 为 0。
特殊代币
BertTokenizer 添加特殊标记,用[CLS]
and括起来一个序列[SEP]
。[CLS]
表示分类并[SEP]
分离序列。对于问答或释义任务,[SEP]
将两个句子分开以进行比较。
BertTokenizer
- cls_token (str, optional, defaults to " [CLS] ")进行序列分类时使用
的分类器标记(整个序列的分类而不是每个标记的分类)。当使用特殊标记构建时,它是序列的第一个标记。
- sep_token (str, 可选,默认为“[SEP]”)
分隔符标记,用于从多个序列构建序列时使用,例如用于序列分类的两个序列或用于文本和用于问答的问题。它还用作使用特殊标记构建的序列的最后一个标记。
首次使用 BERT 的视觉指南展示了标记化。
[CLS]
基础模型最后一层输出中的嵌入向量[CLS]
表示基础模型已经学习到的分类。因此,将令牌的嵌入向量 [CLS]
输入到添加在基础模型之上的分类层中。
每个序列的第一个标记始终是a special classification token ([CLS])
。与该标记对应的最终隐藏状态用作分类任务的聚合序列表示。句子对被打包成一个单一的序列。我们以两种方式区分句子。首先,我们用一个特殊的标记([SEP])将它们分开。其次,我们向每个标记添加一个学习嵌入,指示它属于句子 A 还是句子 B。
模型结构如下图所示。
矢量大小
在模型distilbert-base-uncased
中,每个标记都嵌入到大小为768的向量中。基本模型的输出形状为(batch_size, max_sequence_length, embedding_vector_size=768)
。这与关于 BERT/BASE 模型的 BERT 论文一致(如 distilbert- base -uncased 中所示)。
BERT/BASE(L=12,H= 768,A=12,总参数=110M)和BERT/LARGE(L=24,H=1024,A=16,总参数=340M)。
基础模型 - TFDistilBertModel
TFDistilBertModel 类实例化基础 DistilBERT 模型,顶部没有任何特定的头部(与其他类相反,例如 TFDistilBertForSequenceClassification 确实具有添加的分类头)。
我们不希望附加任何特定于任务的头,因为我们只希望基础模型的预训练权重提供对英语的一般理解,并且在微调期间添加我们自己的分类头将是我们的工作过程以帮助模型区分有毒评论。
TFDistilBertModel
生成一个实例,TFBaseModelOutput
其last_hidden_state
参数是模型最后一层的输出。
TFBaseModelOutput([(
'last_hidden_state',
<tf.Tensor: shape=(batch_size, sequence_lendgth, 768), dtype=float32, numpy=array([[[...]]], dtype=float32)>
)])
参数
- last_hidden_state (tf.Tensor of shape (batch_size, sequence_length, hidden_size)) – 模型最后一层输出的隐藏状态序列。
执行
Python 模块
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split
from transformers import (
DistilBertTokenizerFast,
TFDistilBertModel,
)
配置
TIMESTAMP = datetime.datetime.now().strftime("%Y%b%d%H%M").upper()
DATA_COLUMN = 'text'
LABEL_COLUMN = 'category_index'
MAX_SEQUENCE_LENGTH = 512 # Max length allowed for BERT is 512.
NUM_LABELS = len(raw_train[LABEL_COLUMN].unique())
MODEL_NAME = 'distilbert-base-uncased'
NUM_BASE_MODEL_OUTPUT = 768
# Flag to freeze base model
FREEZE_BASE = True
# Flag to add custom classification heads
USE_CUSTOM_HEAD = True
if USE_CUSTOM_HEAD == False:
# Make the base trainable when no classification head exists.
FREEZE_BASE = False
BATCH_SIZE = 16
LEARNING_RATE = 1e-2 if FREEZE_BASE else 5e-5
L2 = 0.01
分词器
tokenizer = DistilBertTokenizerFast.from_pretrained(MODEL_NAME)
def tokenize(sentences, max_length=MAX_SEQUENCE_LENGTH, padding='max_length'):
"""Tokenize using the Huggingface tokenizer
Args:
sentences: String or list of string to tokenize
padding: Padding method ['do_not_pad'|'longest'|'max_length']
"""
return tokenizer(
sentences,
truncation=True,
padding=padding,
max_length=max_length,
return_tensors="tf"
)
基本模型期望input_ids
其attention_mask
形状为(max_sequence_length,)
。Input
分别用 layer为它们生成 Keras 张量。
# Inputs for token indices and attention masks
input_ids = tf.keras.layers.Input(shape=(MAX_SEQUENCE_LENGTH,), dtype=tf.int32, name='input_ids')
attention_mask = tf.keras.layers.Input((MAX_SEQUENCE_LENGTH,), dtype=tf.int32, name='attention_mask')
基础模型层
从基本模型生成输出。基础模型生成TFBaseModelOutput
. 将嵌入馈送[CLS]
到下一层。
base = TFDistilBertModel.from_pretrained(
MODEL_NAME,
num_labels=NUM_LABELS
)
# Freeze the base model weights.
if FREEZE_BASE:
for layer in base.layers:
layer.trainable = False
base.summary()
# [CLS] embedding is last_hidden_state[:, 0, :]
output = base([input_ids, attention_mask]).last_hidden_state[:, 0, :]
分类层
if USE_CUSTOM_HEAD:
# -------------------------------------------------------------------------------
# Classifiation leayer 01
# --------------------------------------------------------------------------------
output = tf.keras.layers.Dropout(
rate=0.15,
name="01_dropout",
)(output)
output = tf.keras.layers.Dense(
units=NUM_BASE_MODEL_OUTPUT,
kernel_initializer='glorot_uniform',
activation=None,
name="01_dense_relu_no_regularizer",
)(output)
output = tf.keras.layers.BatchNormalization(
name="01_bn"
)(output)
output = tf.keras.layers.Activation(
"relu",
name="01_relu"
)(output)
# --------------------------------------------------------------------------------
# Classifiation leayer 02
# --------------------------------------------------------------------------------
output = tf.keras.layers.Dense(
units=NUM_BASE_MODEL_OUTPUT,
kernel_initializer='glorot_uniform',
activation=None,
name="02_dense_relu_no_regularizer",
)(output)
output = tf.keras.layers.BatchNormalization(
name="02_bn"
)(output)
output = tf.keras.layers.Activation(
"relu",
name="02_relu"
)(output)
Softmax 层
output = tf.keras.layers.Dense(
units=NUM_LABELS,
kernel_initializer='glorot_uniform',
kernel_regularizer=tf.keras.regularizers.l2(l2=L2),
activation='softmax',
name="softmax"
)(output)
最终定制模型
name = f"{TIMESTAMP}_{MODEL_NAME.upper()}"
model = tf.keras.models.Model(inputs=[input_ids, attention_mask], outputs=output, name=name)
model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),
metrics=['accuracy']
)
model.summary()
---
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_ids (InputLayer) [(None, 256)] 0
__________________________________________________________________________________________________
attention_mask (InputLayer) [(None, 256)] 0
__________________________________________________________________________________________________
tf_distil_bert_model (TFDistilB TFBaseModelOutput(la 66362880 input_ids[0][0]
attention_mask[0][0]
__________________________________________________________________________________________________
tf.__operators__.getitem_1 (Sli (None, 768) 0 tf_distil_bert_model[1][0]
__________________________________________________________________________________________________
01_dropout (Dropout) (None, 768) 0 tf.__operators__.getitem_1[0][0]
__________________________________________________________________________________________________
01_dense_relu_no_regularizer (D (None, 768) 590592 01_dropout[0][0]
__________________________________________________________________________________________________
01_bn (BatchNormalization) (None, 768) 3072 01_dense_relu_no_regularizer[0][0
__________________________________________________________________________________________________
01_relu (Activation) (None, 768) 0 01_bn[0][0]
__________________________________________________________________________________________________
02_dense_relu_no_regularizer (D (None, 768) 590592 01_relu[0][0]
__________________________________________________________________________________________________
02_bn (BatchNormalization) (None, 768) 3072 02_dense_relu_no_regularizer[0][0
__________________________________________________________________________________________________
02_relu (Activation) (None, 768) 0 02_bn[0][0]
__________________________________________________________________________________________________
softmax (Dense) (None, 2) 1538 02_relu[0][0]
==================================================================================================
Total params: 67,551,746
Trainable params: 1,185,794
Non-trainable params: 66,365,952 <--- Base BERT model is frozen
数据分配
# --------------------------------------------------------------------------------
# Split data into training and validation
# --------------------------------------------------------------------------------
raw_train = pd.read_csv("./train.csv")
train_data, validation_data, train_label, validation_label = train_test_split(
raw_train[DATA_COLUMN].tolist(),
raw_train[LABEL_COLUMN].tolist(),
test_size=.2,
shuffle=True
)
# X = dict(tokenize(train_data))
# Y = tf.convert_to_tensor(train_label)
X = tf.data.Dataset.from_tensor_slices((
dict(tokenize(train_data)), # Convert BatchEncoding instance to dictionary
train_label
)).batch(BATCH_SIZE).prefetch(1)
V = tf.data.Dataset.from_tensor_slices((
dict(tokenize(validation_data)), # Convert BatchEncoding instance to dictionary
validation_label
)).batch(BATCH_SIZE).prefetch(1)
火车
# --------------------------------------------------------------------------------
# Train the model
# https://www.tensorflow.org/api_docs/python/tf/keras/Model#fit
# Input data x can be a dict mapping input names to the corresponding array/tensors,
# if the model has named inputs. Beware of the "names". y should be consistent with x
# (you cannot have Numpy inputs and tensor targets, or inversely).
# --------------------------------------------------------------------------------
history = model.fit(
x=X, # dictionary
# y=Y,
y=None,
epochs=NUM_EPOCHS,
batch_size=BATCH_SIZE,
validation_data=V,
)
要实施第一种方法,请按如下方式更改配置。
USE_CUSTOM_HEAD = False
然后FREEZE_BASE
更改为False
并LEARNING_RATE
更改为5e-5
将在基础 BERT 模型上运行进一步预训练。
保存模型
对于第三种方法,保存模型会导致问题。不能使用 Huggingface 模型的save_pretrained方法,因为该模型不是 Huggingface PreTrainedModel的直接子类。
Keras save_model导致 default 错误,或者在使用Keras load_model加载模型时save_traces=True
导致不同错误。save_traces=True
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-71-01d66991d115> in <module>()
----> 1 tf.keras.models.load_model(MODEL_DIRECTORY)
11 frames
/usr/local/lib/python3.7/dist-packages/tensorflow/python/keras/saving/saved_model/load.py in _unable_to_call_layer_due_to_serialization_issue(layer, *unused_args, **unused_kwargs)
865 'recorded when the object is called, and used when saving. To manually '
866 'specify the input shape/dtype, decorate the call function with '
--> 867 '`@tf.function(input_signature=...)`.'.format(layer.name, type(layer)))
868
869
ValueError: Cannot call custom layer tf_distil_bert_model of type <class 'tensorflow.python.keras.saving.saved_model.load.TFDistilBertModel'>, because the call function was not serialized to the SavedModel.Please try one of the following methods to fix this issue:
(1) Implement `get_config` and `from_config` in the layer/model class, and pass the object to the `custom_objects` argument when loading the model. For more details, see: https://www.tensorflow.org/guide/keras/save_and_serialize
(2) Ensure that the subclassed model or layer overwrites `call` and not `__call__`. The input shape and dtype will be automatically recorded when the object is called, and used when saving. To manually specify the input shape/dtype, decorate the call function with `@tf.function(input_signature=...)`.
据我测试,只有Keras Model save_weights有效。
实验
据我测试有毒评论分类挑战,第一种方法提供了更好的回忆(识别真正的有毒评论,真正的无毒评论)。代码可以访问如下。如果有任何问题,请提供更正/建议。