튜토리얼: 감정 분석을 위한 BERT 미세 조정하기

튜토리얼: 감정 분석을 위한 BERT 미세 조정하기

    

Skim AI의 머신러닝 연구원 크리스 트란이 처음 게시했습니다.



BERT_for_Sentiment_Analysis





A - 소개

최근 몇 년 동안 자연어 처리 커뮤니티에서는 특히 전이 학습으로의 전환과 같은 많은 획기적인 발전이 있었습니다. ELMo, fast.ai의 ULMFiT, Transformer, OpenAI의 GPT와 같은 모델을 통해 연구자들은 여러 벤치마크에서 최첨단 결과를 달성할 수 있었고 커뮤니티에 고성능의 사전 훈련된 대규모 모델을 제공할 수 있었습니다. 이러한 NLP의 변화는 몇 년 전 컴퓨터 비전에서 특정 작업에 대해 수백만 개의 매개변수가 학습된 딥러닝 네트워크의 하위 계층을 처음부터 새로운 네트워크를 학습하는 대신 다른 작업에 재사용하고 미세 조정할 수 있게 된 NLP의 이미지넷(ImageNet) 순간으로 볼 수 있습니다.

최근 NLP 발전의 가장 큰 이정표 중 하나는 NLP의 새로운 시대의 시작이라고 할 수 있는 Google의 BERT의 출시입니다. 이 노트북에서는 HuggingFace의 트랜스포머 라이브러리를 사용하여 분류 작업을 위해 사전 학습된 BERT 모델을 미세 조정합니다. 그런 다음 TF-IDF 벡터라이저와 나이브 베이즈 분류기를 사용하는 기준 모델과 BERT의 성능을 비교하겠습니다. 이 경우 트랜스포머 라이브러리를 통해 최신 BERT 모델을 빠르고 효율적으로 미세 조정하고 정확도를 높일 수 있습니다. 10% 기준 모델보다 높습니다.

참조:

이해하려면 트랜스포머 (BERT의 기반이 되는 아키텍처)에 대해 알아보고 BERT를 구현하는 방법을 배우려면 다음 자료를 읽어보시길 적극 권장합니다:

B - 설정

1. 필수 라이브러리 로드

0]에 입력합니다:

import os
import re
tqdm에서 tqdm을 가져옵니다.
numpy를 np로 가져 오기
팬더를 pd로 가져옵니다.
matplotlib.pyplot을 plt로 가져옵니다.
%matplotlib 인라인

2. 데이터 세트

2.1. 데이터 세트 다운로드

0]에 입력합니다:

# 데이터 다운로드
요청 가져오기
request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf")
open("data.zip", "wb")를 파일로 사용합니다:
    file.write(request.content)
# 데이터 압축 풀기
zipfile 가져오기
zipfile.ZipFile('data.zip')을 zip으로 가져옵니다:
    zip.extractall('data')

2.2. 열차 데이터 로드

기차 데이터에는 각각 1700개의 불만/비불만 트윗이 포함된 2개의 파일이 있습니다. 데이터의 모든 트윗에는 최소한 항공사의 해시태그가 포함되어 있습니다.

열차 데이터를 로드하고 레이블을 지정합니다. 텍스트 데이터만 사용하여 분류하기 때문에 중요하지 않은 열은 삭제하고 다음과 같은 데이터만 유지합니다. id, 트윗레이블 열을 사용합니다.

0]에 입력합니다:

 # 데이터 로드 및 레이블 설정하기
data_complaint = pd.read_csv('data/complaint1700.csv')
data_complaint['label'] = 0
data_non_complaint = pd.read_csv('data/noncomplaint1700.csv')
data_non_complaint['label'] = 1
# 불만 데이터와 불만 없는 데이터를 연결합니다.
data = pd.concat([data_complaint, data_non_complaint], axis=0).reset_index(drop=True)
# 드롭 '항공사' 열
data.drop(['airline'], inplace=True, axis=1)
# 무작위 샘플 5개 표시
data.sample(5)

Out[0]:

id트윗레이블
198824991정말 반갑습니다. 웃기네 Deplanin...1
129472380오늘 밤 @JetBlue에 매우 실망했습니다. Fligh...0
1090127893내 친구들은 지옥 같은 시간을 보내고 있습니다...0
55358278내가 크리스마스에 원하는 것은 잃어버린 가방뿐입니다...0
207530695네 거짓말하지 않겠습니다... 시도하는 데 매우 관심이 있습니다...1

전체 학습 데이터를 90%의 데이터가 포함된 학습 세트와 10%의 데이터가 포함된 검증 세트로 무작위로 분할합니다. 훈련 세트에서 교차 검증을 사용하여 하이퍼파라미터 튜닝을 수행하고 검증 세트를 사용하여 모델을 비교합니다.

0]에 입력합니다:

sklearn.model_selection에서 train_test_split 가져오기
X = 데이터.트윗.값
y = 데이터.레이블.값
X_train, X_val, y_train, y_val =
    train_test_split(X, y, test_size=0.1, random_state=2020)

2.3. 부하 테스트 데이터

테스트 데이터에는 라벨이 없는 4555개의 예시가 포함되어 있습니다. 약 300개의 예시는 불만을 제기하지 않은 트윗입니다. 우리의 임무는 이들의 id 를 클릭하고 결과가 올바른지 수동으로 검사합니다.

0]에 입력합니다:

# 로드 테스트 데이터
test_data = pd.read_csv('data/test_data.csv')
# 중요한 열 유지
test_data = test_data[['id', 'tweet']]
# 테스트 데이터에서 샘플 5개를 표시합니다.
test_data.sample(5)

Out[0]:

id트윗
153959336아메리칸에어 항공편이 2시간 이상 지연되었습니다...
60724101이 오류 메시지가 계속 표시됩니다...
33313179#SeaTac에서 @JetBlue 항공편 탑승을 위해 대기 중...
2696102948사전 좌석 선택 프로를 통과할 때 싫어요...
3585135638부끄러운 줄 아세요 @AlaskaAir

3. 트레이닝을 위한 GPU 설정

Google Colab은 무료 GPU와 TPU를 제공합니다. 대규모 신경망을 훈련할 것이므로 이러한 기능을 활용하는 것이 가장 좋습니다.

메뉴로 이동하여 GPU를 선택하여 추가할 수 있습니다:

런타임 -> 런타임 유형 변경 -> 하드웨어 가속기: GPU

그런 다음 다음 셀을 실행하여 GPU를 디바이스로 지정해야 합니다.

0]에 입력합니다:

import torch
if torch.cuda.is_available():       
    device = torch.device("cuda")
    print(f'{torch.cuda.device_count()} GPU(s)가 있습니다.')
    print('장치 이름:', torch.cuda.get_device_name(0))
else:
    print('GPU를 사용할 수 없습니다, 대신 CPU를 사용합니다.')
    device = torch.device("cpu")
사용 가능한 GPU는 1개입니다.
장치 이름: Tesla T4

C - 기준선: TF-IDF + 나이브 베이즈 분류기¶

이 기본 접근 방식에서는 먼저 TF-IDF를 사용해 텍스트 데이터를 벡터화합니다. 그런 다음 분류기로 나이브 베이즈 모델을 사용합니다.

왜 나이브 베이즈인가? 저는 랜덤 포레스트, 서포트 벡터 머신, XGBoost 등 다양한 머신러닝 알고리즘을 경험해본 결과 나이브 베이즈가 가장 좋은 성능을 내는 것을 확인했습니다. In Scikit-learn 가이드 올바른 추정자를 선택하려면 텍스트 데이터에 나이브 베이즈를 사용해야 한다는 제안도 있습니다. 또한 차원을 줄이기 위해 SVD를 사용해 보았지만 더 나은 성능을 얻지 못했습니다.

1. 데이터 준비

1.1. 전처리

단어 가방 모델에서 텍스트는 문법과 어순을 무시하고 단어가 들어 있는 가방으로 표현됩니다. 따라서 문장의 의미에 크게 기여하지 않는 마침표, 구두점 및 문자를 제거할 수 있습니다.

0]에 입력합니다:

NLTK 가져오기
# 댓글을 달지 않고 "스톱워드" 다운로드하기
nltk.download("stopwords")
nltk.corpus에서 stopwords 가져오기
def text_preprocessing(s):
    """
    - 문장을 소문자로 바꾸기
    - "'t"를 "not"으로 변경합니다.
    - "@name" 제거
    - "?"를 제외한 구두점을 분리하여 제거합니다.
    - 기타 특수 문자 제거
    - "not" 및 "can"을 제외한 중지 단어 제거하기
    - 후행 공백 제거
    """
    s = s.lower()
    # 't를 'not&#39로 변경합니다;
    s = re.sub(r"'t", " not", s)
    # @name 제거
    s = re.sub(r'(@.*?)[s]', ' ', s)
    # '?&#39를 제외한 구두점을 분리하고 제거합니다;
    s = re.sub(r'(['".()!\/,])', r' 1 ', s)
    s = re.sub(r'[^ws?]', ' ', s)
    # 일부 특수 문자 제거
    s = re.sub(r'([;:|-"n])', ' ', s)
    # 'not' 및 'can&#39를 제외한 중지어를 제거합니다;
    s = " ".join([s.split()의 단어에 대한 단어)
                  if word not in stopwords.words('english')
                  또는 단어가 ['not', 'can']])에 있으면
    # 후행 공백 제거
    s = re.sub(r's+', ' ', s).strip()
    반환 s
[nltk_data] 패키지 스톱워드를 /root/nltk_data에 다운로드 중...
[nltk_data] 패키지 스톱워드가 이미 최신 버전입니다!

1.2. TF-IDF 벡터라이저

정보 검색에서, TF-IDF의 줄임말 용어 빈도 - 역 문서 빈도는 컬렉션이나 말뭉치에서 단어가 문서에 얼마나 중요한지를 반영하기 위한 수치 통계입니다. 기계 학습 알고리즘에 공급하기 전에 TF-IDF를 사용하여 텍스트 데이터를 벡터화합니다.

0]에 입력합니다:

%%타임
sklearn.feature_extraction.text에서 TfidfVectorizer 가져오기
# 전처리 텍스트
X_train_preprocessed = np.array([text_preprocessing(text) for text in X_train])
X_val_preprocessed = np.array([text_preprocessing(text) for text in X_val])
# TF-IDF 계산하기
tf_idf = TfidfVectorizer(ngram_range=(1, 3),
                         binary=True,
                         smooth_idf=False)
X_train_tfidf = tf_idf.fit_transform(X_train_preprocessed)
X_val_tfidf = tf_idf.transform(X_val_preprocessed)
CPU 시간: 사용자 5.47초, 시스템: 519ms, 총: 5.99초
벽 시간: 6초

2. 나이브 베이즈 분류기 훈련

2.1. 하이퍼파라미터 조정

교차 검증과 AUC 점수를 사용하여 모델의 하이퍼파라미터를 조정합니다. 함수 get_auc_CV 는 교차 유효성 검사에서 얻은 평균 AUC 점수를 반환합니다.

0]에 입력합니다:

sklearn.model_selection에서 StratifiedKFold, cross_val_score를 가져옵니다.
def get_auc_CV(model):
    """
    교차 검증에서 평균 AUC 점수를 반환합니다.
    """
    # 분할 전에 데이터를 셔플하도록 KFold를 설정합니다.
    kf = StratifiedKFold(5, shuffle=True, random_state=1)
    # AUC 점수 가져오기
    auc = cross_val_score(
        model, X_train_tfidf, y_train, scoring="roc_auc", cv=kf)
    반환 auc.mean()

그리고 멀티노멀NB 클래스에는 하이퍼파라미터가 하나만 있습니다. 알파. 아래 코드는 가장 높은 CV AUC 점수를 제공하는 알파 값을 찾는 데 도움이 됩니다.

0]에 입력합니다:

sklearn.naive_bayes에서 MultinomialNB를 가져옵니다.
res = pd.Series([get_auc_CV(MultinomialNB(i))
                 for i in np.arange(1, 10, 0.1)],
                index=np.arange(1, 10, 0.1))
best_alpha = np.round(res.idxmax(), 2)
print('최고 알파: ', best_alpha)
plt.plot(res)
plt.title('AUC vs. 알파')
plt.xlabel('Alpha')
plt.ylabel('AUC')
plt.show()
최고의 알파:  1.3

2.2. 유효성 검사 집합에 대한 평가

모델의 성능을 평가하기 위해 유효성 검사 세트에서 모델의 정확률과 AUC 점수를 계산합니다.

0]에 입력합니다:

sklearn.metrics에서 accuracy_score, roc_curve, auc를 가져옵니다.
def evaluate_roc(probs, y_true):
    """
    - 테스트 세트의 AUC 및 정확도 인쇄
    - ROC 플롯하기
    @params probs (np.array): 모양이 있는 예측 확률 배열(len(y_true), 2)
    @params y_true (np.array): 모양을 가진 참값의 배열 (len(y_true),)
    """
    preds = probs[:, 1]
    fpr, tpr, threshold = roc_curve(y_true, preds)
    RO_C_AUC = AUC(FPR, TPR)
    print(f'AUC: {roc_auc:.4f}')
    # 테스트 세트에 대한 정확도 얻기
    y_pred = np.where(preds >= 0.5, 1, 0)
    정확도 = accuracy_score(y_true, y_pred)
    print(f'Accuracy: {accuracy*100:.2f}%')
    # 플롯 ROC AUC
    plt.title('수신기 동작 특성')
    plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc)
    plt.legend(loc = '오른쪽 하단')
    plt.plot([0, 1], [0, 1],'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('진양성률')
    plt.xlabel('거짓 양성률')
    plt.show()

TF-IDF와 나이브 베이즈 알고리즘을 결합하여 다음과 같은 정확도를 달성합니다. 72.65% 를 설정합니다. 이 값은 기준 성능이며 미세 조정된 BERT 모델의 성능을 평가하는 데 사용됩니다.

0]에 입력합니다:

# 예측 확률 계산하기
nb_model = 다항식NB(알파=1.8)
nb_model.fit(X_train_tfidf, y_train)
probs = nb_model.predict_proba(X_val_tfidf)
# 분류기 평가
evaluate_roc(probs, y_val)
AUC: 0.8451
정확도: 75.59%

D - BERT 미세 조정

1. 허깅 페이스 라이브러리 설치

Hugging Face의 트랜스포머 라이브러리에는 BERT(Google 제공), GPT(OpenAI 제공), 사전 학습된 모델 가중치 등 최신 NLP 모델을 PyTorch로 구현한 것이 포함되어 있습니다.

1]에서

#!pip 변압기 설치

2. 토큰화 및 입력 서식 지정

텍스트를 토큰화하기 전에 엔티티 멘션(예: @united)과 일부 특수 문자를 제거하는 등 텍스트에 대해 약간의 처리를 수행합니다. 여기서 처리 수준은 BERT가 전체 문장으로 학습되었기 때문에 이전 접근 방식보다 훨씬 적습니다.

0]에 입력합니다:

def text_preprocessing(text):
    """
    - 엔티티 멘션 제거(예: '@united')
    - 오류 수정(예: '&' 를 '&' 로)
    @param text (str): 처리할 문자열.
    반환 텍스트(Str): 처리된 문자열.
    """
    # '@name&#39를 제거합니다;
    text = re.sub(r'(@.*?)[s]', ' ', text)
    # '&' 를 '&&#39로 바꿉니다;
    text = re.sub(r'&', '&', text)
    # 후행 공백 제거
    text = re.sub(r's+', ' ', text).strip()
    텍스트 반환

0]에 입력합니다:

# 문장 0 인쇄
print('Original: ', X[0])
print('Processed: ', text_preprocessing(X[0]))
Original:  유나이티드에 문제가 있습니다. 어제 비행 예정일로부터 24시간 후에 재예약했는데 지금은 로그인 및 체크인을 할 수 없습니다. 도와주실 수 있나요?
처리했습니다:  I'문제가 있습니다. 어제 비행 예정일로부터 24시간 후에 재예약했는데 지금은 로그인 및 체크인을 할 수 없습니다. 도와주실 수 있나요?

2.1. BERT 토큰화

사전 학습된 BERT를 적용하기 위해서는 라이브러리에서 제공하는 토큰화기를 사용해야 합니다. 그 이유는 (1) 모델에 특정 고정 어휘가 있고 (2) BERT 토큰라이저는 어휘를 벗어난 단어를 처리하는 특별한 방법이 있기 때문입니다.

또한 각 문장의 시작과 끝에 특수 토큰을 추가하고, 모든 문장을 하나의 일정한 길이로 패딩 및 자르고, '주의 마스크'로 패딩 토큰이 무엇인지 명시적으로 지정해야 합니다.

그리고 encode_plus 메서드를 사용합니다:

(1) 텍스트를 토큰으로 분할합니다,

(2) 스페셜 추가 [CLS][SEP] 토큰 및

(3) 이러한 토큰을 토큰화 어휘의 인덱스로 변환합니다,

(4) 문장을 최대 길이로 줄이거나 자릅니다.

(5) 주의 마스크를 만듭니다.

0]에 입력합니다:

트랜스포머에서 BERT토큰라이저를 가져옵니다.
# BERT 토큰화 도구 불러오기
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
# 텍스트 집합을 토큰화하는 함수를 만듭니다.
def preprocessing_for_bert(data):
    """사전 학습된 BERT에 대해 필요한 전처리 단계를 수행합니다.
    @param data (np.array): 처리할 텍스트 배열입니다.
    반환 입력_아이디 (torch.Tensor): 모델에 공급할 토큰 ID의 텐서입니다.
    반환 주의_마스크(torch.Tensor): 어떤 토큰에 주목해야 하는지 지정하는
                  토큰을 지정하는 인덱스 텐서.
    """
    # 출력을 저장할 빈 목록 만들기
    input_ids = []
    attention_masks = []
    # 모든 문장에 대해...
    전송된 데이터에 대해
        # encode_plus will:
        # (1) 문장을 토큰화합니다.
        # (2) [CLS][SEP] 토큰을 시작과 끝으로
        # (3) 문장을 최대 길이로 자르기/붙이기
        # (4) 토큰을 ID에 매핑하기
        # (5) 주의 마스크 만들기
        # (6) 출력 사전 반환하기
        encoded_sent = tokenizer.encode_plus(
            text=text_preprocessing(sent), # 전처리 문장
            add_special_tokens = True, # 추가 [CLS][SEP]
            max_length=MAX_LEN, # 잘라내기/패드할 최대 길이
            pad_to_max_length=true, # 최대 길이까지 패드 문장
            #return_tensors='pt', # 파이토치 텐서 반환
            return_attention_mask=true # 주의 마스크 반환
            )
        # 출력을 목록에 추가합니다.
        input_ids.append(encoded_sent.get('input_ids'))
        attention_masks.append(encoded_sent.get('attention_mask'))
    # 목록을 텐서로 변환하기
    input_ids = torch.tensor(input_ids)
    attention_masks = torch.tensor(attention_masks)
    입력_ids, 주의력_마스크 반환

토큰화하기 전에 문장의 최대 길이를 지정해야 합니다.

0]에 입력합니다:

# 열차 데이터와 테스트 데이터 연결하기
all_tweets = np.concatenate([data.tweet.values, test_data.tweet.values])
# 연결된 데이터 인코딩하기
encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets]
# 최대 길이 찾기
max_len = max([encoded_tweets에서 보낸 것에 대해 len(sent)])
print('최대 길이: ', max_len)
최대 길이: 68

이제 데이터를 토큰화해 보겠습니다.

0]에 입력합니다:

# 지정 MAX_LEN
MAX_LEN = 64
# 문장 0과 인코딩된 토큰 ID를 인쇄합니다.
token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy())
print('원본: ', X[0])
print('토큰 IDs: ', token_ids)
# 실행 함수 preprocessing_for_bert 를 훈련 집합과 유효성 검사 집합에 추가합니다.
print('데이터 토큰화...')
train_inputs, train_masks = preprocessing_for_bert(X_train)
val_inputs, val_masks = preprocessing_for_bert(X_val)
Original:  유나이티드에 문제가 있습니다. 어제 비행 예정일로부터 24시간 후에 재예약했는데 지금은 로그인 및 체크인을 할 수 없습니다. 도와주실 수 있나요?
토큰 ID:  [101, 1045, 1005, 1049, 2383, 3314, 1012, 7483, 1045, 2128, 8654, 2098, 2005, 2484, 2847, 2044, 1045, 2001, 4011, 2000, 4875, 1010, 2085, 1045, 2064, 1005, 1056, 8833, 2006, 1004, 4638, 1999, 1012, 2064, 2017, 2393, 1029, 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]
데이터 토큰화...

2.2. PyTorch 데이터로더 생성

토치 데이터로더 클래스를 사용해 데이터 세트에 대한 반복자를 생성하겠습니다. 이렇게 하면 훈련 중 메모리를 절약하고 훈련 속도를 높일 수 있습니다.

0]에 입력합니다:

torch.utils.data에서 Tensor데이터셋, 데이터로더, 랜덤샘플러, 시퀀셜샘플러를 가져옵니다.
# 다른 데이터 유형을 torch.Tensor로 변환하기
train_labels = torch.tensor(y_train)
val_labels = torch.tensor(y_val)
# BERT의 미세 조정을 위해 저자는 16 또는 32의 배치 크기를 권장합니다.
batch_size = 32
# 훈련 세트에 대한 데이터로더를 생성합니다.
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = 랜덤샘플러(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)
# 유효성 검사 집합을 위한 데이터로더를 생성합니다.
val_data = TensorDataset(val_inputs, val_masks, val_labels)
val_sampler = SequentialSampler(val_data)
val_dataloader = DataLoader(val_data, sampler=val_sampler, batch_size=batch_size)

3. 모델 훈련

3.1. BertClassifier 생성

BERT-base는 12개의 트랜스포머 레이어로 구성되며, 각 트랜스포머 레이어는 토큰 임베딩 목록을 받아 출력에 동일한 숨겨진 크기(또는 치수)로 동일한 수의 임베딩을 생성합니다. 최종 트랜스포머 레이어의 출력은 [CLS] 토큰을 시퀀스의 특징으로 사용하여 분류기에 공급합니다.

그리고 트랜스포머 라이브러리에는 BertForSequenceClassification 클래스는 분류 작업을 위해 설계되었습니다. 하지만 새로운 클래스를 생성하여 원하는 분류자를 직접 지정할 수 있도록 할 것입니다.

아래에서는 마지막 숨겨진 레이어를 추출하기 위해 BERT 모델로 BertClassifier 클래스를 생성하겠습니다. [CLS] 토큰과 단일 숨겨진 계층 피드 포워드 신경망을 분류기로 사용합니다.

0]에 입력합니다:

%%타임
import torch
torch.nn을 nn으로 import
트랜스포머에서 BertModel을 가져옵니다.
# BertClassfier 클래스를 만듭니다.
클래스 BertClassifier(nn.Module):
    """분류 작업을 위한 버트 모델입니다.
    """
    def __init__(self, freeze_bert=False):
        """
        @param bert: BertModel 객체
        @param classifier: torch.nn.Module 분류자
        @param freeze_bert (bool): Set False 를 사용하여 BERT 모델을 미세 조정합니다.
        """
        super(BertClassifier, self).__init__()
        # BERT의 숨겨진 크기, 분류기의 숨겨진 크기 및 레이블 수를 지정합니다.
        D_in, H, D_out = 768, 50, 2
        # BERT 모델 인스턴스화
        self.bert = BertModel.from_pretrained('bert-base-uncased')
        # 1계층 피드 포워드 분류기 인스턴스화
        self.classifier = nn.Sequential(
            nn.Linear(D_in, H),
            nn.ReLU(),
            #nn.Dropout(0.5),
            nn.Linear(H, D_out)
        )
        # BERT 모델 동결하기
        if freeze_bert:
            self.bert.parameters()의 param에 대해:
                param.requires_grad = False
    def forward(self, input_ids, attention_mask):
        """
        로그값을 계산하기 위해 BERT와 분류기에 입력을 전달합니다.
        @param input_ids (torch.Tensor): 모양(batch_size,
                      max_length)
        @param attention_mask (torch.Tensor): 주의 마스크를 보유한 텐서
                      모양을 가진 정보(batch_size, max_length)
        반환 로그(torch.Tensor): 모양(batch_size)을 가진 출력 텐서,
                      num_labels)
        """
        # BERT에 입력 피드
        outputs = self.bert(input_ids=input_ids,
                            attention_mask=attention_mask)
        # 토큰의 마지막 숨겨진 상태 추출 [CLS] 분류 작업의 경우
        last_hidden_state_cls = outputs[0][:, 0, :]
        # 로그를 계산하기 위해 분류기에 입력을 피드합니다.
        logits = self.classifier(last_hidden_state_cls)
        로그 반환
CPU 시간: 사용자 38µs, 시스템: 0ns, 총: 38µs
벽 시간 40.1µs

3.2. 옵티마이저 및 학습 속도 스케줄러

Bert 분류기를 미세 조정하려면 최적화 프로그램을 만들어야 합니다. 저자는 하이퍼 파라미터를 따르는 것을 권장합니다:

  • 배치 크기: 16 또는 32
  • 학습 속도(아담): 5e-5, 3e-5 또는 2e-5
  • 에포크 수: 2, 3, 4

허깅페이스는 run_glue.py 스크립트를 구현하는 예제인 트랜스포머 라이브러리를 사용합니다. 스크립트에서는 AdamW 옵티마이저가 사용됩니다.

0]에 입력합니다:

transformers에서 AdamW, get_linear_schedule_with_warmup을 가져옵니다.
def initialize_model(epochs=4):
    """Bert 분류기, 최적화기, 학습률 스케줄러를 초기화합니다.
    """
    # 버트 분류기 초기화
    bert_classifier = BertClassifier(freeze_bert=False)
    # 파이토치에게 GPU에서 모델을 실행하도록 지시합니다.
    bert_classifier.to(device)
    # 옵티마이저 생성
    optimizer = AdamW(bert_classifier.parameters(),
                      lr=5e-5, # 기본 학습 속도
                      eps=1e-8 # 기본 엡실론 값
                      )
    # 총 학습 단계 수
    총_스텝 = len(train_dataloader) * epochs
    # 학습 속도 스케줄러 설정
    스케줄러 = get_linear_schedule_with_warmup(optimizer,
                                                num_warmup_steps=0, # 기본값
                                                NUM_TRINING_STEPS=TOTAL_STEPS)
    반환 bert_classifier, optimizer, scheduler

3.3. 교육 루프

4개의 에포크에 대해 Bert 분류기를 훈련합니다. 각 에포크에서 모델을 훈련하고 검증 세트에서 성능을 평가할 것입니다. 더 자세히 설명하겠습니다:

교육:

  • 데이터로더에서 데이터 압축을 풀고 GPU에 데이터를 로드합니다.
  • 이전 패스에서 계산된 그라데이션 제로화
  • 포워드 패스를 수행하여 로그 및 손실 계산하기
  • 백워드 패스를 수행하여 그라데이션을 계산합니다(loss.backward())
  • 그라데이션의 표준을 1.0으로 클립하여 "폭발하는 그라데이션"을 방지합니다.
  • 모델의 매개변수 업데이트(optimizer.step())
  • 학습률 업데이트(스케줄러.step())

평가:

  • 데이터 압축을 풀고 GPU에 로드
  • 포워드 패스
  • 유효성 검사 세트에 대한 손실 및 정확도 계산

아래 스크립트에는 교육 및 평가 루프에 대한 자세한 내용이 설명되어 있습니다.

0]에 입력합니다:

import random
import time
# Specify loss function
loss_fn = nn.CrossEntropyLoss()
def set_seed(seed_value=42):
    """Set seed for reproducibility.
    """
    random.seed(seed_value)
    np.random.seed(seed_value)
    torch.manual_seed(seed_value)
    torch.cuda.manual_seed_all(seed_value)
def train(model, train_dataloader, val_dataloader=None, epochs=4, evaluation=False):
    """Train the BertClassifier model.
    """
    # Start training loop
    print("Start training...n")
    for epoch_i in range(epochs):
        # =======================================
        #               Training
        # =======================================
        # Print the header of the result table
        print(f"{'Epoch':^7} | {'Batch':^7} | {'Train Loss':^12} | {'Val Loss':^10} | {'Val Acc':^9} | {'Elapsed':^9}")
        print("-"*70)
        # Measure the elapsed time of each epoch
        t0_epoch, t0_batch = time.time(), time.time()
        # Reset tracking variables at the beginning of each epoch
        total_loss, batch_loss, batch_counts = 0, 0, 0
        # Put the model into the training mode
        model.train()
        # For each batch of training data...
        for step, batch in enumerate(train_dataloader):
            batch_counts +=1
            # Load batch to GPU
            b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch)
            # Zero out any previously calculated gradients
            model.zero_grad()
            # Perform a forward pass. This will return logits.
            logits = model(b_input_ids, b_attn_mask)
            # Compute loss and accumulate the loss values
            loss = loss_fn(logits, b_labels)
            batch_loss += loss.item()
            total_loss += loss.item()
            # Perform a backward pass to calculate gradients
            loss.backward()
            # Clip the norm of the gradients to 1.0 to prevent "exploding gradients"
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            # Update parameters and the learning rate
            optimizer.step()
            scheduler.step()
            # Print the loss values and time elapsed for every 20 batches
            if (step % 20 == 0 and step != 0) or (step == len(train_dataloader) - 1):
                # Calculate time elapsed for 20 batches
                time_elapsed = time.time() - t0_batch
                # Print training results
                print(f"{epoch_i + 1:^7} | {step:^7} | {batch_loss / batch_counts:^12.6f} | {'-':^10} | {'-':^9} | {time_elapsed:^9.2f}")
                # Reset batch tracking variables
                batch_loss, batch_counts = 0, 0
                t0_batch = time.time()
        # Calculate the average loss over the entire training data
        avg_train_loss = total_loss / len(train_dataloader)
        print("-"*70)
        # =======================================
        #               Evaluation
        # =======================================
        if evaluation == True:
            # After the completion of each training epoch, measure the model's performance
            # on our validation set.
            val_loss, val_accuracy = evaluate(model, val_dataloader)
            # Print performance over the entire training data
            time_elapsed = time.time() - t0_epoch
            print(f"{epoch_i + 1:^7} | {'-':^7} | {avg_train_loss:^12.6f} | {val_loss:^10.6f} | {val_accuracy:^9.2f} | {time_elapsed:^9.2f}")
            print("-"*70)
        print("n")
    print("Training complete!")
def evaluate(model, val_dataloader):
    """After the completion of each training epoch, measure the model's performance
    on our validation set.
    """
    # Put the model into the evaluation mode. The dropout layers are disabled during
    # the test time.
    model.eval()
    # Tracking variables
    val_accuracy = []
    val_loss = []
    # For each batch in our validation set...
    for batch in val_dataloader:
        # Load batch to GPU
        b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch)
        # Compute logits
        with torch.no_grad():
            logits = model(b_input_ids, b_attn_mask)
        # Compute loss
        loss = loss_fn(logits, b_labels)
        val_loss.append(loss.item())
        # Get the predictions
        preds = torch.argmax(logits, dim=1).flatten()
        # Calculate the accuracy rate
        accuracy = (preds == b_labels).cpu().numpy().mean() * 100
        val_accuracy.append(accuracy)
    # Compute the average accuracy and loss over the validation set.
    val_loss = np.mean(val_loss)
    val_accuracy = np.mean(val_accuracy)
    return val_loss, val_accuracy

이제 BertClassifier 훈련을 시작하겠습니다!

0]에 입력합니다:

set_seed(42) # 재현성을 위한 시드 설정
bert_classifier, optimizer, scheduler = initialize_model(epochs=2)
train(bert_classifier, train_dataloader, val_dataloader, epochs=2, evaluation=True)
교육 시작하기...

 에포크 | 배치 | 열차 손실 | 발 손실 | 발 정확도 | 경과 시간
----------------------------------------------------------------------
   1 | 20 | 0.630467 | - | - | 7.58
   1 | 40 | 0.497330 | - | - | 7.01
   1 | 60 | 0.502320 | - | - | 7.11
   1 | 80 | 0.491438 | - | - | 7.19
   1 | 95 | 0.486125 | - | - | 5.35
----------------------------------------------------------------------
   1 | - | 0.524515 | 0.439601 | 78.81 | 35.54
----------------------------------------------------------------------


 에포크 | 배치 | 열차 손실 | Val 손실 | Val Acc | 경과 시간
----------------------------------------------------------------------
   2 | 20 | 0.287401 | - | - | 7.83
   2 | 40 | 0.260870 | - | - | 7.60
   2 | 60 | 0.287706 | - | - | 7.67
   2 | 80 | 0.283311 | - | - | 7.87
   2 | 95 | 0.280315 | - | - | 5.87
----------------------------------------------------------------------
   2 | - | 0.279978 | 0.454067 | 80.40 | 38.31
----------------------------------------------------------------------


훈련 완료!

3.4. 유효성 검사 집합에 대한 평가

예측 단계는 훈련 루프에서 수행한 평가 단계와 유사하지만 더 간단합니다. 포워드 패스를 수행하여 로그를 계산하고 소프트맥스를 적용하여 확률을 계산합니다.

0]에 입력합니다:

torch.nn.functional을 F로 가져옵니다.
def bert_predict(model, test_dataloader):
    """훈련된 BERT 모델에서 포워드 패스를 수행하여 테스트 세트의 확률을 예측합니다.
    을 예측하기 위해 훈련된 BERT 모델에 포워드 패스를 수행합니다.
    """
    # 모델을 평가 모드로 전환합니다. 드롭아웃 레이어는 테스트 시간
    # 테스트 시간 동안 드롭아웃 레이어가 비활성화됩니다.
    model.eval()
    all_logits = []
    # 테스트 세트의 각 배치에 대해...
    test_dataloader의 배치에 대해..:
        # GPU에 배치 로드
        b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2]
        # 로짓 계산
        를 계산합니다:
            logits = model(b_input_ids, b_attn_mask)
        all_logits.append(logits)
    # 각 배치의 로그를 연결합니다.
    all_logits = torch.cat(all_logits, dim=0)
    # 소프트맥스를 적용하여 확률 계산하기
    probs = F.softmax(all_logits, dim=1).cpu().numpy()
    프로브 반환

0]에 입력합니다:

# 테스트 세트에서 예측 확률 계산하기
probs = bert_predict(bert_classifier, val_dataloader)
# Bert 분류기 평가하기
evaluate_roc(probs, y_val)
AUC: 0.9048
정확도: 80.59%

Bert 분류기는 검증 세트에서 0.90의 AUC 점수와 82.65%의 정확도를 달성했습니다. 이 결과는 기준 방법보다 10점 더 나은 결과입니다.

3.5. 전체 학습 데이터로 모델 훈련하기

0]에 입력합니다:

# 열차 세트와 유효성 검사 세트 연결하기
full_train_data = torch.utils.data.ConcatDataset([train_data, val_data])
full_train_sampler = 랜덤샘플러(full_train_data)
full_train_dataloader = 데이터로더(full_train_data, 샘플러=full_train_sampler, batch_size=32)
# 전체 훈련 데이터에 대해 Bert 분류기를 훈련합니다.
set_seed(42)
bert_classifier, optimizer, scheduler = initialize_model(epochs=2)
train(bert_classifier, full_train_dataloader, epochs=2)
교육 시작하기...

 에포크 | 배치 | 열차 손실 | 발 손실 | 발 정확도 | 경과 시간
----------------------------------------------------------------------
   1 | 20 | 0.664452 | - | - | 8.63
   1 | 40 | 0.587205 | - | - | 8.42
   1 | 60 | 0.522831 | - | - | 8.44
   1 | 80 | 0.476442 | - | - | 8.23
   1 | 100 | 0.467542 | - | - | 8.10
   1 | 106 | 0.483039 | - | - | 2.14
----------------------------------------------------------------------


 에포크 | 배치 | 열차 손실 | Val 손실 | Val Acc | 경과 시간
----------------------------------------------------------------------
   2 | 20 | 0.338174 | - | - | 8.36
   2 | 40 | 0.296080 | - | - | 7.93
   2 | 60 | 0.295626 | - | - | 7.96
   2 | 80 | 0.277470 | - | - | 7.99
   2 | 100 | 0.314746 | - | - | 8.07
   2 | 106 | 0.293359 | - | - | 2.17
----------------------------------------------------------------------


훈련 완료!

4. 테스트 세트에 대한 예측

4.1. 데이터 준비

곧 테스트 세트를 다시 살펴보겠습니다.

0]에 입력합니다:

test_data.sample(5)

Out[0]:

id트윗
47118654친구 및 가족: 젯블루 항공편을 이용하지 마세요. Absol...
197176265델타 어시스트 @로제리오드 나는 프로가 된 적이 없어요...
23672몇 주 만에 첫 비행. 당신을 믿습니다 @아메리카...
2702103263"@USAirways: 우리는 더 이상 머물 수 없다는 것을 알고 있죠?
1355137사우스웨스트에어 여기 SA 공항에서 지켜보는 ...

테스트 세트에 대한 예측을 수행하기 전에 학습 데이터에서 수행한 처리 및 인코딩 단계를 다시 수행해야 합니다. 다행히도, 저희는 preprocessing_for_bert 함수를 사용하여 이를 수행할 수 있습니다.

0]에 입력합니다:

# 실행 preprocessing_for_bert 테스트 세트에서
print('데이터 토큰화...')
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# 테스트 세트에 대한 데이터 로더를 생성합니다.
test_dataset = TensorDataset(test_inputs, test_masks)
test_sampler = SequentialSampler(test_dataset)
test_dataloader = DataLoader(test_dataset, sampler=test_sampler, batch_size=32)
데이터 토큰화...

4.2. 예측

테스트 세트에는 부정적이지 않은 트윗이 약 300개 있습니다. 따라서 부정적이지 않은 트윗이 약 300개가 될 때까지 결정 임계값을 계속 조정할 것입니다.

사용할 임계값은 0.992로, 예측 확률이 99.2%보다 큰 트윗이 긍정적으로 예측된다는 의미입니다. 이 값은 기본 임계값인 0.5에 비해 매우 높은 수치입니다.

테스트 세트를 수동으로 검토한 결과, 감정 분류 작업은 인간에게도 어려운 작업이라는 것을 알게 되었습니다. 따라서 임계값이 높으면 안전한 예측을 할 수 있습니다.

0]에 입력합니다:

# 테스트 세트에서 예측 확률 계산하기
probs = bert_predict(bert_classifier, test_dataloader)
# 확률에서 예측값 가져오기
임계값 = 0.9
preds = np.where(probs[:, 1] > 임계값, 1, 0)
# 부정이 아닌 것으로 예측된 트윗 수
print("비부정으로 예측된 트윗 수: ", preds.sum())
부정적이지 않은 것으로 예측된 트윗 수: 454건

이제 예측한 20개의 무작위 트윗을 살펴보겠습니다. 그 중 17개가 정확하여 BERT 분류기가 약 0.85의 정확도를 획득했음을 보여줍니다.

0]에 입력합니다:

output = test_data[preds==1]
list(output.sample(20).tweet)

Out[0]:

["@델타 @델타항공 델타항공이 또다시 파업을 단행합니다. 미국에서 가장 붐비는 공항의 스카이 라운지가 주말에 문을 닫습니다. 멍청하고 싸구려. 보고 싶어요 @유나이티드",
 사우스웨스트항공에서 꿀에 구운 땅콩을 가져왔습니다. 이 깨달음이 내 하루의 최고점이 될 수 있다는 것이 슬픈 일인가요?#SmallThingsInLife',
 2주 전에 문제 해결을 위해 kana@delta와 contactus.delta@delta에 이메일을 보냈는데 응답이 없습니다. 누구에게 연락해야 하는지 조언 부탁드립니다.', '@델타 지원,
 "#Cancer로 인해 @AlaskaAir에서 출발한 여성이 가족에게 항공료를 기부할 계획입니다 http://t.co/Uj6rispWLb",
 "@유나이티드 (2/2) 가방을 부수지 않았어요. 돈을 내고 수표를 맡길 필요가 없었다면 이렇게 화를 내지 않았을 거예요. 차라리 @아메리칸에어 @사우스웨스트에어 등을 이용하겠어요",
 "거의 모든 항공사를 이용해봤지만 젯블루를 이용하는 것보다 더 좋은 경험은 없었습니다. 품질, 서비스, 편안함, 경제성. A++",
 '@JetBlue 미스 유를 위한 최고의 항공사 #keepingitminty ',
 #39;@firetweet에게 오스틴 톰에서 저와 함께할 마지막 순간 여행을 예약하도록 설득했습니다! 그 이후로 @VirginAmerica 안전송을 계속 부르고 있습니다. 불쌍한 에릭 ',
 #DFW에서 #ord로 이륙을 기다리는 아메리칸에어 http://t.co/j1oDSc6fht',
 '오 @JetBlue 오늘은 B6 충성 고객들에게 슬픈 날입니다. 새로운 "옵션"에 대해 선전하고 계시지만, 서비스/무료 수하물 수수료가 바로 이 항공사의 장점입니다',
 이 항공편의 좋은 점들: 고고(@Gogo)와 버진아메리카의 훌륭한 경험. 좋지 않은 점: 아기 토사물/썩은 참치 냄새.',
 아메리칸항공이 그리울 것 같아요 :(',
 알톤브라운 @유나이티드 @버진아메리카로 전환할 시간입니다',
 아메리칸에어 어드미럴스 클럽, 초바니에게 결코 잘못된 시간은 없습니다! #브레이킹 레코드 #투머니는 #런치',
 "비행 중에 @알래스카항공의 새로운 스트리밍 IFE를 체험하기 위해 제 휴대폰을 훔쳤어요. 좋았어요! 하지만 아이패드가 없는 게 아쉽네요.",
 "@미국항공과 @아메리칸항공의 합병이 완료되기를 기다릴 수 없습니다. 고객 입장에서는 얼마나 번거로운 일인가요!",
 "@JetBlue 나는 파산한 대학생이라 #39;는 큰 문제입니다.",
 "오늘 밤 사우스웨스트에어 2256편으로 베이 지역으로 돌아갈 날을 기다릴 수 없어요!!!!",
 #39;#SFO에서 안개가 걷히기를 기다리며 다음 연결편인 @버진아메리카와 #sxsw를 기다리는 중! #SXSW2015 #Austin',
 "@델타항공 어쨌든 1308편에서 무기한 지연되지 않는 항공편으로 갈아탈 수 있어요.... 그리고 워싱턴으로 돌아가세요!"]

E - 결론

BERT 위에 간단한 숨겨진 계층 신경망 분류기를 추가하고 BERT를 미세 조정함으로써 3,400개의 데이터 포인트만 가지고도 기본 방법보다 10% 포인트 더 나은 최첨단 성능에 가까운 결과를 얻을 수 있었습니다.

또한 BERT는 매우 크고 복잡하며 수백만 개의 파라미터를 가지고 있지만, 2~4회 정도만 미세 조정하면 됩니다. 이러한 결과를 얻을 수 있었던 것은 BERT가 방대한 양의 학습을 통해 이미 우리 언어에 대한 많은 정보를 인코딩했기 때문입니다. 적은 양의 데이터로 단시간에 달성한 놀라운 성능은 BERT가 현재 가장 강력한 NLP 모델 중 하나인 이유를 보여줍니다.


아이디어를 논의해 보세요

    관련 게시물

    비즈니스를 강화할 준비 완료

    LET'S
    TALK
    ko_KR한국어