我正在尝试微调 HuggingFace 的 TFBertModel,以便能够将某些文本分类为单一标签。我已经启动并运行了模型,但从一开始准确率就非常低。我的预期是,由于使用了 BERT 的预训练权重作为起点,准确率应该会很高。我希望能得到一些关于我哪里做错的建议。
我使用的是来自这里的 bbc-text 数据集:
加载数据
df = pd.read_csv(open(<s3 url>),encoding='utf-8', error_bad_lines=False)df = df.sample(frac=1)df = df.dropna(how='any')
值计数
sport 511business 510politics 417tech 401entertainment 386Name: label, dtype: int64
预处理
def preprocess_text(sen):# 将html实体转换为普通文本sentence = unescape(sen)# 移除html标签sentence = remove_tags(sentence)# 移除换行符sentence = remove_newlinechars(sentence)# 移除标点符号和数字sentence = re.sub('[^a-zA-Z]', ' ', sentence)# 转换为小写sentence = sentence.lower()return sentencedef remove_newlinechars(text): return " ".join(text.splitlines()) def remove_tags(text): TAG_RE = re.compile(r'<[^>]+>') return TAG_RE.sub('', text)df['text_prepd'] = df['text'].apply(preprocess_text)
分割数据
train, val = train_test_split(df, test_size=0.30, shuffle=True, stratify=df['label'])
编码标签
from sklearn.preprocessing import LabelEncoderlabel_encoder = LabelEncoder()y_train = np.asarray(le.fit_transform(train['label']))y_val = np.asarray(le.fit_transform(val['label']))
定义 BERT 输入函数
# 初始化 Bert Tokenizerbert_tokenizer_transformer = BertTokenizer.from_pretrained('bert-base-cased')def create_input_array(df, tokenizer, args): sentences = df.text_prepd.values input_ids = [] attention_masks = [] token_type_ids = [] for sent in tqdm(sentences): # `encode_plus` 将会: # (1) 对句子进行分词。 # (2) 在句子开头添加 `[CLS]` 标记。 # (3) 在句子末尾添加 `[SEP]` 标记。 # (4) 将标记映射到它们的 ID。 # (5) 将句子填充或截断至 `max_length`。 # (6) 为 [PAD] 标记创建注意力掩码。 encoded_dict = tokenizer.encode_plus( sent, # 要编码的句子。 add_special_tokens=True, # 添加 '[CLS]' 和 '[SEP]' max_length=args.max_seq_len, # 填充和截断所有句子。 pad_to_max_length=True, return_attention_mask=True, # 构建注意力掩码。 return_tensors='tf', # 返回 tf 张量。 ) # 将编码后的句子添加到列表中。 input_ids.append(encoded_dict['input_ids']) # 以及它的注意力掩码(简单区分填充和非填充)。 attention_masks.append(encoded_dict['attention_mask']) token_type_ids.append(encoded_dict['token_type_ids']) input_ids = tf.convert_to_tensor(input_ids) attention_masks = tf.convert_to_tensor(attention_masks) token_type_ids = tf.convert_to_tensor(token_type_ids) return input_ids, attention_masks, token_type_ids
将数据转换为 BERT 输入
train_inputs = [create_input_array(train[:], tokenizer=tokenizer, args=args)]val_inputs = [create_input_array(val[:], tokenizer=tokenizer, args=args)]
对于 train_inputs, y_train
和 val_inputs, y_val
,我随后应用了下面的函数,该函数会重塑并转换为 numpy 数组。这个函数返回的列表随后作为参数传递给 keras 的 fit 方法。我意识到这样转换为 tf.tensors 然后再转换为 numpy 有点多余,但我认为这不会有影响。我最初尝试使用 tf.datasets,但后来改用了 numpy。
def convert_inputs_to_tf_dataset(inputs,y, args): # args.max_seq_len = 256 ids = inputs[0][1] masks = inputs[0][1] token_types = inputs[0][2] ids = tf.reshape(ids, (-1, args.max_seq_len)) print("Input ids shape: ", ids.shape) masks = tf.reshape(masks, (-1, args.max_seq_len)) print("Input Masks shape: ", masks.shape) token_types = tf.reshape(token_types, (-1, args.max_seq_len)) print("Token type ids shape: ", token_types.shape) ids=ids.numpy() masks = masks.numpy() token_types = token_types.numpy() return [ids, masks, token_types, y]
Keras 模型
# args.max_seq_len = 256# n_classes = 6model = TFBertForSequenceClassification.from_pretrained('bert-base-uncased', trainable=True, num_labels=n_classes)input_ids_layer = Input(shape=(args.max_seq_len, ), dtype=np.int32)input_mask_layer = Input(shape=(args.max_seq_len, ), dtype=np.int32)input_token_type_layer = Input(shape=(args.max_seq_len,), dtype=np.int32)bert_layer = model([input_ids_layer, input_mask_layer, input_token_type_layer])[0]flat_layer = Flatten()(bert_layer)dropout= Dropout(0.3)(flat_layer)dense_output = Dense(n_classes, activation='softmax')(dropout)model_ = Model(inputs=[input_ids_layer, input_mask_layer, input_token_type_layer], outputs=dense_output)
编译和拟合
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)metric = tf.keras.metrics.SparseCategoricalAccuracy('accuracy')model.compile(optimizer='adam', loss=loss, metrics=[metric])model.fit(inputs=..., outputs=..., validation_data=..., epochs=50, batch_size = 32, metrics=metric, verbose=1)Epoch 32/501401/1401 [==============================] - 42s 30ms/sample - loss: 1.6103 - accuracy: 0.2327 - val_loss: 1.6042 - val_accuracy: 0.2308
由于我在使用 BERT,只需要几个周期,因此我在32个周期后期望得到的准确率远高于23%。
回答:
主要问题在于这一行:ids = inputs[0][1]
。实际上,ids 是 inputs[0]
的第一个元素;所以应该是 ids = inputs[0][0]
。
但还有另一个问题可能会导致验证准确率不一致:你应该只对标签映射进行一次 LabelEncoder
的拟合;因此,你应该对验证标签使用 transform
方法,而不是 fit_transform
方法。
此外,不要同时在损失函数中使用 softmax
激活和 from_logits=True
;只使用其中一个(更多信息请见这里)。
还有一个要点是你可能需要为优化器使用更低的学习率。Adam 优化器的默认学习率为 1e-3,考虑到你正在微调预训练模型,这可能太高了。尝试使用更低的学习率,比如 1e-4 或 1e-5;例如 tf.keras.optimizers.Adam(learning_rate=1e-4)
。对于微调预训练模型,使用较高的学习率可能会破坏已学习的权重,并扰乱微调过程(特别是在微调过程开始时,由于生成的大梯度值)。