Skip to main content

텍스트 범주화를 위해 BERT를 미세 조정하는 방법

텍스트 범주화, tf.data 및 tf.Hub를 위해 BERT를 미세 조정하는 읽기 쉬운 코드 퍼스트 시동 장치((code-first reader-friendly kickstart)
Created on February 2|Last edited on February 2
이는 여기에서 볼 수 있는 영어 기사를 번역한 것이다.

Sections: 




BERT는 무엇입니까?

BERT (Bidirectional Encoder Representations from Transformers), 트랜스포머로부터의 양방향 인코더 프리젠테이션)은 다양한 NLP 작업에 대한 최첨단 성능을 향상시킨 Google의 혁신적인 문서이며 다른 많은 혁명적인 아키텍처의 디딤돌이 되었습니다.
BERT가 전체 도메인에 새로운 방향을 설정했다고 해도 과언이 아닙니다. 사전에 트레이닝 된 모델(대규모 데이터 세트에서 트레이닝)과 다운스트림 작업과 무관한 전송 러닝 (Transfer learning)의 분명한 이점을 보여줍니다.
이 보고서에서는 텍스트 범주화를 위해 BERT를 사용하는 방법에 대해 살펴보고 실행 중인 수많은 코드와 예에 대해서 알려드리고자 합니다. 주요 소스를 직접 확인하고 싶다면 주석에 달린 논문 링크 를 참고하세요.
BERT 범주화 모델

텍스트 범주화를 위한 BERT 설정

먼저 TensorFlow와 TensorFlow Model Garden을 설치합니다.:
import tensorflow as tf
print(tf.version.VERSION)
!git clone --depth 1 -b v2.4.0 https://github.com/tensorflow/models.git
또한 TensorFlow 모델을 위해 Github Repo도 복제할 것입니다. 몇 가지 주의할 점은 다음과 같습니다.:
  • –depth 1, 복제하는 동안 Git은 관련 파일의 최신 복사본만 받게 됩니다. 이로 인해 많은 공간과 시간을 절약할 수 있습니다.
  • -b는 특정 가지 (branch)만 복제할 수 있습니다.
여러분의 TensorFlow 2.x 버전과 일치시키십시오.
# install requirements to use tensorflow/models repository
!pip install -Uqr models/official/requirements.txt
# you may have to restart the runtime afterwards, also ignore any ERRORS popping up at this step
이 때 많은 가져오기(Import)가 쏟아지죠, 여러분.
import numpy as np
import tensorflow as tf
import tensorflow_hub as hub
import sys
sys.path.append('models')
from official.nlp.data import classifier_data_lib
from official.nlp.bert import tokenization
from official.nlp import optimization
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set()
import wandb
from wandb.keras import WandbCallback
설치된 여러 버전 및 종속성에 대한 빠른 온전성 검사(sanity-check):

print("TF Version: ", tf.__version__)
print("Eager mode: ", tf.executing_eagerly())
print("Hub version: ", hub.__version__)
print("GPU is", "available" if tf.config.experimental.list_physical_devices("GPU") else "NOT AVAILABLE")

데이터 세트를 받아볼까요

오늘 사용할 데이터 세트는 Kaggle의 ‘Quora Insincere Questions Classification competition’에서 제공됩니다.
원하시는 경우에는 Kaggle에서 트레이닝 세트를 다운로드하거나 아래 링크 에서train.csv를 다운 받으세요.

데이터를 압축 해제하여 Pandas DataFrame으로 읽어보세요.:

그런 후에 다음을 실행합니다.:
# TO LOAD DATA FROM ARCHIVE LINK
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

df = pd.read_csv('https://archive.org/download/quora_dataset_train.csv/quora_dataset_train.csv.zip',
compression='zip',
low_memory=False)
print(df.shape)
df.head(10)
# label 0 == non toxic
# label 1 == toxic
좋습니다. 이제 데이터를 W&B 테이블로 빠르게 시각화해 보겠습니다.



좀 더 탐험해볼까요

라벨 분포 (Label Distribution)

모델링에 대해 자세히 알아보기 전에 작업 중인 데이터를 이해하시는 것이 좋습니다. 여기, 우리는 우리의 라벨 분포를 살펴볼 것입니다. 테스트 및 트레이닝 세트가 잘 분포되어 있는지 확인하고 다른 몇 가지 예비 작업을 수행할 수 있습니다. 먼저 실행을 통해 라벨 분포를 살펴보겠습니다.:
print(df['target'].value_counts())
df['target'].value_counts().plot.bar()
plt.yscale('log');
plt.title('Distribution of Labels')

라벨 분포

단어 길이 및 문자 길이

이제, 여기서 작업하는 텍스트 데이터를 이해하기 위해 몇 줄의 코드를 실행하겠습니다.
print('Average word length of questions in dataset is {0:.0f}.'.format(np.mean(df['question_text'].apply(lambda x: len(x.split())))))
print('Max word length of questions in dataset is {0:.0f}.'.format(np.max(df['question_text'].apply(lambda x: len(x.split())))))
print('Average character length of questions in dataset is {0:.0f}.'.format(np.mean(df['question_text'].apply(lambda x: len(x)))))



텍스트 범주화 작업을 위한 트레이닝 및 테스트 데이터 준비하기

접근 방식에 대한 참고사항들은 다음과 같습니다.
💡
  • 전체 데이터 세트는 트레이닝하는 데 많은 시간이 소요되기 때문에 데이터의 일부 작은 부분을 사용합니다. train_size를 변경하여 더 많은 데이터를 포함시킬 수 있습니다.
  • 데이터 세트가 매우 불균형하기 때문에 라벨을 기반으로 계층화하여 열차와 테스트 세트 모두에서 동일한 분포를 유지할 것입니다. 이 섹션에서는 데이터를 분석하여 우리가 이 작업을 잘 수행했는지 확인할 것입니다.
train_df, remaining = train_test_split(df, random_state=42, train_size=0.1, stratify=df.target.values)
valid_df, _ = train_test_split(remaining, random_state=42, train_size=0.01, stratify=remaining.target.values)
print(train_df.shape)
print(valid_df.shape)
(130612, 3) (11755, 3)

샘플링된 세트의 단어 및 문자 길이 가져오기

print("FOR TRAIN SET\n")
print('Average word length of questions in train set is {0:.0f}.'.format(np.mean(train_df['question_text'].apply(lambda x: len(x.split())))))
print('Max word length of questions in train set is {0:.0f}.'.format(np.max(train_df['question_text'].apply(lambda x: len(x.split())))))
print('Average character length of questions in train set is {0:.0f}.'.format(np.mean(train_df['question_text'].apply(lambda x: len(x)))))
print('Label Distribution in train set is \n{}.'.format(train_df['target'].value_counts()))
print("\n\nFOR VALIDATION SET\n")
print('Average word length of questions in valid set is {0:.0f}.'.format(np.mean(valid_df['question_text'].apply(lambda x: len(x.split())))))
print('Max word length of questions in valid set is {0:.0f}.'.format(np.max(valid_df['question_text'].apply(lambda x: len(x.split())))))
print('Average character length of questions in valid set is {0:.0f}.'.format(np.mean(valid_df['question_text'].apply(lambda x: len(x)))))
print('Label Distribution in validation set is \n{}.'.format(valid_df['target'].value_counts()))
즉, 문제 지문에서 트레이닝과 검증 집합은 클래스 불균형과 다양한 길이 면에서 비슷해 보입니다.


단어의 질문 텍스트 길이 분포 분석하기

# TRAIN SET
train_df['question_text'].apply(lambda x: len(x.split())).plot(kind='hist');
plt.yscale('log');
plt.title('Distribution of question text length in words')


# VALIDATION SET
valid_df['question_text'].apply(lambda x: len(x.split())).plot(kind='hist');
plt.yscale('log');
plt.title('Distribution of question text length in words')




문자 단위의 질문 텍스트 길이 분포 분석하기

트레이닝과 검증 세트에 대해서 더 알아보면서 우리가 확인하고 싶은 또 다른 한가지는 질문 텍스트 길이가 둘 사이에 대부분 비슷한지 여부입니다. 여기에 거의 비슷한 분포를 갖는 것이 일반적으로 모델을 치우치거나 과적합하는 것을 방지하기 위한 현명한 생각입니다.
# TRAIN SET
train_df['question_text'].apply(lambda x: len(x)).plot(kind='hist');
plt.yscale('log');
plt.title('Distribution of question text length in characters')


# VALIDATION SET
valid_df['question_text'].apply(lambda x: len(x)).plot(kind='hist');
plt.yscale('log');
plt.title('Distribution of question text length in characters')

맞습니다. 심지어 단어와 문자의 질문 길이 분포도 매우 유사합니다. 아직까지는 괜찮은 트레이닝/테스트 분할인 것 같습니다.


데이터 길들이기

다음으로 CPU에서 데이터 집합을 생성하고 전처리하고자 합니다.:
with tf.device('/cpu:0'):
train_data = tf.data.Dataset.from_tensor_slices((train_df['question_text'].values, train_df['target'].values))
valid_data = tf.data.Dataset.from_tensor_slices((valid_df['question_text'].values, valid_df['target'].values))
# lets look at 3 samples from train set
for text,label in train_data.take(3):
print(text)
print(label)

print(len(train_data))
print(len(valid_data))
130612 11755
좋습니다. BERT 해봅시다.

BERT 합시다: TensorFlow Hub에서 사전 트레이닝을 받은 BERT 모델 가져오기

출처
우리는 tfhub에 있는 Uncased BERT를 사용할 것입니다.
BERT 레이어에 제공할 텍스트를 준비하려면 먼저 단어를 토큰화해야 합니다. 여기 토큰나이저(Tokenizer)는 모델 자산으로 존재하며 우리에게도 언케이싱 (Uncasing)이 가능합니다.
필요 시에 변경 가능하도록 모든 매개 변수를 사전의 형태로 설정합니다.:
# Setting some parameters

config = {'label_list' : [0, 1], # Label categories
'max_seq_length' : 128, # maximum length of (token) input sequences
'train_batch_size' : 32,
'learning_rate': 2e-5,
'epochs':5,
'optimizer': 'adam',
'dropout': 0.5,
'train_samples': len(train_data),
'valid_samples': len(valid_data),
'train_split':0.1,
'valid_split': 0.01
}


BERT 도면 레이어 및 토큰나이저 가져오기:


# All details here: https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/2

bert_layer = hub.KerasLayer('https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/2',
trainable=True)
vocab_file = bert_layer.resolved_object.vocab_file.asset_path.numpy()
do_lower_case = bert_layer.resolved_object.do_lower_case.numpy() # checks if the bert layer we are using is uncased or not
tokenizer = tokenization.FullTokenizer(vocab_file, do_lower_case)


일부 트레이닝 샘플 및 토큰화된 ID 확인

input_string = "hello world, it is a wonderful day for learning"
print(tokenizer.wordpiece_tokenizer.tokenize(input_string))
print(tokenizer.convert_tokens_to_ids(tokenizer.wordpiece_tokenizer.tokenize(input_string)))
['hello', 'world', '##,', 'it', 'is', 'a', 'wonderful', 'day', 'for', 'learning'] [7592, 2088, 29623, 2009, 2003, 1037, 6919, 2154, 2005, 4083]


데이터를 준비하세요. BERT용 텍스트 토큰화 및 전처리 하기

데이터 세트의 각 행은 검토 (Review) 텍스트와 해당 라벨로 구성됩니다. 데이터 전처리는 텍스트를 BERT 입력 기능으로 변환하는 것으로 구성됩니다.

  • 입력 단어 ID: 토큰나이저(Tokenizer)의 출력 값이며, 각 문장을 토큰 ID 집합으로 변환합니다.
  • 입력 마스크: 모든 시퀀스를 128(최대 시퀀스 길이)까지 패딩 (Padding)하므로 이러한 패딩이 실제 텍스트 토큰을 방해하지 않도록 하는 일종의 마스크를 만드는 것이 중요합니다. 따라서 패딩을 차단하는 입력 마스크가 필요합니다. 마스크는 실제 토큰의 경우 1이고 패딩 토큰의 경우 0입니다. 실제 토큰만 처리됩니다.
  • 세그먼트 ID: 텍스트 범주화 작업의 경우 시퀀스가 하나 뿐이기 때문에 세그먼트_ids/input_type_ids는 본질적으로 0들의 벡터일 뿐입니다.
Bert는 두 가지 작업에 대한 트레이닝을 받았습니다.
  1. 문장에서 무작위로 가린 단어들을 채우세요.
  2. 주어진 두 문장 중, 어떤 문장이 먼저 나왔나요?
# This provides a function to convert row to input features and label,
# this uses the classifier_data_lib which is a class defined in the tensorflow model garden we installed earlier

def create_feature(text, label, label_list=config['label_list'], max_seq_length=config['max_seq_length'], tokenizer=tokenizer):
"""
converts the datapoint into usable features for BERT using the classifier_data_lib

Parameters:
text: Input text string
label: label associated with the text
label_list: (list) all possible labels
max_seq_length: (int) maximum sequence length set for bert
tokenizer: the tokenizer object instantiated by the files in model assets

Returns:
feature.input_ids: The token ids for the input text string
feature.input_masks: The padding mask generated
feature.segment_ids: essentially here a vector of 0s since classification
feature.label_id: the corresponding label id from lable_list [0, 1] here

"""

# since we only have 1 sentence for classification purpose, textr_b is None
example = classifier_data_lib.InputExample(guid = None,
text_a = text.numpy(),
text_b = None,
label = label.numpy())
# since only 1 example, the index=0
feature = classifier_data_lib.convert_single_example(0, example, label_list,
max_seq_length, tokenizer)

return (feature.input_ids, feature.input_mask, feature.segment_ids, feature.label_id)


  • Dataset.map을 사용하여 데이터 세트의 각 요소에 이 기능을 적용하려고 합니다. Dataset.map은 그래프 모드에서 실행되며 그래프 탠서(tensor)는 값이 없습니다.
  • 그래프 모드에서는 TensorFlow Ops와 그 기능들만 사용할 수 있습니다.
따라서 이 함수를 직접 매핑할 수 없습니다. tf.py_function으로 래핑해야 합니다. tf.py_function는 래핑된 Python 함수에 정규 텐서(이에 접속하기 위한 값과 .numpy()방법이 있음)를 전달합니다.

신속한 실행(Eager Execution)을 위해 Python 함수를 TensorFlow op로 래핑

def create_feature_map(text, label):
"""
A tensorflow function wrapper to apply the transformation on the dataset.
Parameters:
Text: the input text string.
label: the classification ground truth label associated with the input string

Returns:
A tuple of a dictionary and a corresponding label_id with it. The dictionary
contains the input_word_ids, input_mask, input_type_ids
"""

input_ids, input_mask, segment_ids, label_id = tf.py_function(create_feature, inp=[text, label],
Tout=[tf.int32, tf.int32, tf.int32, tf.int32])
max_seq_length = config['max_seq_length']

# py_func doesn't set the shape of the returned tensors.
input_ids.set_shape([max_seq_length])
input_mask.set_shape([max_seq_length])
segment_ids.set_shape([max_seq_length])
label_id.set_shape([])

x = {
'input_word_ids': input_ids,
'input_mask': input_mask,
'input_type_ids': segment_ids
}
return (x, label_id)
모델로 전달되는 마지막 데이터 포인트는 x와 라벨 형식으로서의 사전 형식입니다 (사전에 완전 일치하는 키가 있음).

데이터가 흐르게 해볼까요: tf.data를 사용하여 최종 입력 파이프라인을 작성하기



트레이닝 및 테스트 데이터 세트에 변환(Transformation) 적용

# Now we will simply apply the transformation to our train and test datasets
with tf.device('/cpu:0'):
# train
train_data = (train_data.map(create_feature_map,
num_parallel_calls=tf.data.experimental.AUTOTUNE)

.shuffle(1000)
.batch(32, drop_remainder=True)
.prefetch(tf.data.experimental.AUTOTUNE))

# valid
valid_data = (valid_data.map(create_feature_map,
num_parallel_calls=tf.data.experimental.AUTOTUNE)
.batch(32, drop_remainder=True)
.prefetch(tf.data.experimental.AUTOTUNE))
keras.Model.fit에 의해 예상되는 tf.data.Datasets 리턴의 결과(특성, 라벨)의 쌍 (Pair)
# train data spec, we can finally see the input datapoint is now converted to the
#BERT specific input tensor
train_data.element_spec



BERT 범주화 모델의 생성, 트레이닝, 추적

영광으로 이끄는 우리의 방식으로 모델링 해보세요!!!

모델 생성��기

BERT 레이어에는 두 가지 출력 값이 있습니다.
  • 전체 입력 시퀀스에 대한 표현이 포함된 모양 [batch_size, 768]의 pooled_output입니다.
  • 각 입력 토큰에 대한 표현(텍스트에서)이 있는 형태 [batch_size, max_seq_length, 768]의 시퀀스_출력입니다.
범주화 작업의 경우 pooled_output에만 신경씁니다.
# Building the model, input ---> BERT Layer ---> Classification Head
def create_model():
input_word_ids = tf.keras.layers.Input(shape=(config['max_seq_length'],),
dtype=tf.int32,
name="input_word_ids")

input_mask = tf.keras.layers.Input(shape=(config['max_seq_length'],),
dtype=tf.int32,
name="input_mask")

input_type_ids = tf.keras.layers.Input(shape=(config['max_seq_length'],),
dtype=tf.int32,
name="input_type_ids")


pooled_output, sequence_output = bert_layer([input_word_ids, input_mask, input_type_ids])
# for classification we only care about the pooled-output.
# At this point we can play around with the classification head based on the
# downstream tasks and its complexity

drop = tf.keras.layers.Dropout(config['dropout'])(pooled_output)
output = tf.keras.layers.Dense(1, activation='sigmoid', name='output')(drop)

# inputs coming from the function
model = tf.keras.Model(
inputs={
'input_word_ids': input_word_ids,
'input_mask': input_mask,
'input_type_ids': input_type_ids},
outputs=output)

return model


여러분의 모델을 트레이닝하세요

# Calling the create model function to get the keras based functional model
model = create_model()

# using adam with a lr of 2*(10^-5), loss as binary cross entropy as only
# 2 classes and similarly binary accuracy
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=config['learning_rate']),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=[tf.keras.metrics.BinaryAccuracy(),
tf.keras.metrics.PrecisionAtRecall(0.5),
tf.keras.metrics.Precision(),
tf.keras.metrics.Recall()])
#model.summary()

모델 요약
모델 아키텍처 요약
tf hub의 한 가지 단점은 전체 모듈을 Keras의 레이어로 가져오기 때문에 모델 요약에서 파라미터와 레이어가 보이지 않는다는 것입니다.
tf.keras.utils.plot_model(model=model, show_shapes=True, dpi=76, )

tf hub 공식 페이지에는 "모듈의 모든 매개 변수를 트레이닝 가능하며, 모든 매개 변수를 미세 조정하는 것이 권장합니다"라고 명시되어 있습니다. 그러므로 우리는 아무것도 멈추지 (Freezing) 않고 전체 모델을 트레이닝할 것입니다.

실험 추적

지금 이 글을 읽고 계신 여러분에게 W&B에 대한 좋은 아이디어가 있을 거라고 생각합니다. 하지만 아이디어가 없더라도 이 글을 쭉 읽어보세요 :)
실험 추적을 시작하기 위해 W&B에 '실행'을 생성할 것입니다.
wandb.init(): 기본 프로젝트 정보 매개변수로 실행을 초기화합니다.:
  • project: 프로젝트 이름은 이 프로젝트에 대한 모든 실험을 추적할 새 프로젝트 탭을 만듭니다.
  • config: 추적할 모든 매개 변수 및 하이퍼 파라미터의 사전입니다.
  • group: 선택 사항이지만 나중에 다른 매개 변수별로 그룹화하는 데 도움이 됩니다.
  • job_type: 작업 유형을 설명하면 나중에 여러 실험을 그룹화하는 데 도움이 됩니다. 예, "train", "evaluate" 등입니다.
# Update CONFIG dict with the name of the model.
config['model_name'] = 'BERT_EN_UNCASED'
print('Training configuration: ', config)

# Initialize W&B run
run = wandb.init(project='Finetune-BERT-Text-Classification',
config=config,
group='BERT_EN_UNCASED',
job_type='train')
이제 다양한 메트릭을 로그하기 위해 W&B에서 제공하는 간단한 콜백을 사용하겠습니다.
WandCallback() : https://docs.wandb.ai/guides/integrations/keras
맞습니다, 이는 콜백을 추가하는 것 만큼이나 간단하죠:D
# Train model
# setting low epochs as It starts to overfit with this limited data, please feel free to change
epochs = config['epochs']
history = model.fit(train_data,
validation_data=valid_data,
epochs=epochs,
verbose=1,
callbacks = [WandbCallback()])
run.finish()


몇 가지 트레이닝 메트릭 및 그래프





평가해 봅시다

검증 세트를 평가하고 W&B를 사용하여 점수를 로그하도록 하겠습니다.
wandb.log(): 스칼라 사전(정확도 및 손실과 같은 메트릭) 및 기타 유형의 wandb 개체를 로그합니다. 여기서는 평가 사전을 그대로 전달하여 기록하겠습니다.
# Initialize a new run for the evaluation-job
run = wandb.init(project='Finetune-BERT-Text-Classification',
config=config,
group='BERT_EN_UNCASED',
job_type='evaluate')



# Model Evaluation on validation set
evaluation_results = model.evaluate(valid_data,return_dict=True)

# Log scores using wandb.log()
wandb.log(evaluation_results)

# Finish the run
run.finish()


모델 및 모델 버전 저장하기

마지막으로 재현 가능한 모델을 W&B로 저장하는 방법을 알아보겠습니다. 즉, 아티팩트와 함께 말이죠.

W&B 아티팩트

모델을 저장하고 다양한 실험을 더 쉽게 추적할 수 있도록 하기 위해 저희는 wandb.artifacts를 사용할 것입니다. W&B 아티팩트는 데이터 세트와 모델을 저장하는 방법입니다.
실행에서 모델 아티팩트를 생성하고 저장하는 세 가지 단계가 있습니다.
  • wandb.Artifact()를 사용하여 빈 아티팩트를 만듭니다.
  • wandb.add_file()을 사용하여 아티팩트에 모델 파일을 추가합니다.
  • wandb.log_artifact()를 콜하여 아티팩트를 저장합니다.
# Save model
model.save(f"{config['model_name']}.h5")

# Initialize a new W&B run for saving the model, changing the job_type
run = wandb.init(project='Finetune-BERT-Text-Classification',
config=config,
group='BERT_EN_UNCASED',
job_type='save')


# Save model as Model Artifact
artifact = wandb.Artifact(name=f"{config['model_name']}", type='model')
artifact.add_file(f"{config['model_name']}.h5")
run.log_artifact(artifact)

# Finish W&B run
run.finish()


W&B 대시보드 살짝 맛보기

참고할 사항은 다음과 같습니다.• 실험과 실행을 그룹화합니다.

  • 모든 트레이닝 로그 및 메트릭을 시각화합니다.
  • 클라우드 인스턴스 또는 물리적 GPU 시스템에 대한 트레이닝 시 시스템 메트릭의 시각화가 유용할 수 있습니다.
  • 표 형식(tabular form)의 하이퍼 파라미터 추적입니다.
  • 아티팩트: 모델 버전 관리 및 스토리지.



BERT 테스트 범주화 요약 및 코드

이 실전 튜토리얼이 유용했길 바라고, 지금까지 쭉 읽어보았으니 여러분 각자에게 도움이 될 아이디어가 떠오르길 바랍니다.
이 게시물의 전체 코드는 여기에서 확인할 수 있습니다.
Iterate on AI agents and models faster. Try Weights & Biases today.