본문 바로가기

공부방/Upstage AI Lab 4기

TinyBert Model로 문장 속 감정 분석하기

Bert(Bidirectional Encoder Representations from Transformers)

트렌스포머의 인코더 구조만 차용해서 만든 자연어 처리(NLP) 대규모 언어 모델. 
디코더 부분 없음 -> 자연어 생성보다 자연어 이해해 초점 맞춤. 
트렌스포머 인코더 레이어를 여러 개 쌓아서 임베딩을 만들고, 마지막에 클래시피파이어 레이어를 붙여서 원하는 결과물을 만든다.
사전학습 모델 (레이블링 되지 않은 큰 데이터셋에서 먼저 데이터를 학습하고, 모델이 일반적인 데이터셋의 분포를 배우게 함 -> 이후 특정 태스크에 대해서 다시 학습시켜서 모델을 좀 더 뾰족하게 만드는 파인 튜닝 거침) - 강의 내용 요약(Overview of Generative LLMs)

TinyBERT

BERT 모델의 경량화 버전. BERT에 비해 매개변수 수와 모델 크기가 크게 감소.
"TinyBERT는 특히 리소스가 제한된 환경에서 BERT와 유사한 성능을 제공하면서도 더 빠르고 효율적으로 작동할 수 있는 솔루션을 제공합니다. 이는 실시간 처리가 필요하거나 계산 리소스가 제한된 애플리케이션에서 매우 유용할 수 있습니다." -클로드


실습

TinyBert 모델을 사용해 영화 리뷰 데이터를 학습시키고, 문장에 담긴 감정을 postive/nagative로 분류하게 만들기

 

1. 데이터 세팅

import pandas as pd

#데이터 가져오기
data = pd.read_csv('https://raw.githubusercontent.com/laxmimerit/All-CSV-ML-Data-Files-Download/master/IMDB-Dataset.csv')

#Hugging Face의 datasets 라이브러리에서 Dataset 클래스는
#머신러닝, 특히 자연어 처리(NLP) 작업을 위한 데이터셋을 효율적으로 관리하고 처리하는 도구
from datasets import Dataset

#pandas DataFrame을 Hugging Face의 Dataset 객체로 변환
dataset = Dataset.from_pandas(data)

#데이터셋을 훈련세트와 테스트세트로 쉽게 분할
dataset = dataset.train_test_split(test_size=0.3)

이렇게 생긴 데이터셋

판다스로 csv를 읽어온다. (강사님이 링크 그대로 사용했는데, 출처를 찾아보니, Laxmi Kant라는 사람의 깃허브 코드 중에 파일이 들어가있었다. 강사님이 이분 데이터로 공부하셨나..?ㅋㅋㅋhttps://github.com/laxmimerit 

어쨌든 이렇게 csv를 읽어온 다음에 자연어처리 작업을 효율적으로(?) 하기 위해서 Dataset이라는 객체로 변환한다. 허깅페이스에서 만든 datasets 라는 라이브러리를 사용한다. 대규모 데이터셋을 다룰 때 메모리에 전부 로드하는 것이 아니라 필요할 때에만 접근하는 방식을 써서 메모리를 효율적으로 사용할 수 있다고 한다.

그리고 데이터를 분할해주니 아래와 같이 결과가 나왔다. 

sentiment에 postive, negative와 같이 감성 레이블을 1과 0으로 바꾼 레이블을 하나 더 추가한다. 

#텍스트 레이블을 숫자로 매핑. 
label2id = {'negative':0, 'positive':1}

#x['sentiment'] = 원래 써있던 텍스트 레이블(negative/positive)를 label2id대로 변환
dataset = dataset.map(lambda x: {'label':label2id[x['sentiment']]})
dataset

DatasetDict({
t
rain: Dataset({ features: ['review', 'sentiment', 'label'], num_rows: 35000 })
test: Dataset({ features: ['review', 'sentiment', 'label'], num_rows: 15000 })
})

이렇게 숫자 레이블을 추가하면 모델의 출력을 좀 더 쉽게 해석할 수 있다. 예를 들어 0.5 이상은 긍정, 미만은 부정.

 

2. 모델 임포트

# 데이터 준비가 끝났습니다. => 모델을 불러와야죠.
from transformers import AutoTokenizer
import torch

device = torch.device('cuda') if torch.cuda.is_available() else torch.device("cpu")

# Hugginc Face에서 tinybert 검색 -> huawei-noah 모델 선택
model = 'huawei-noah/TinyBERT_General_4L_312D'
tokenizer = AutoTokenizer.from_pretrained(model, use_fast=True)
tokenizer

'''
tokenizer 결과

BertTokenizerFast(name_or_path='huawei-noah/TinyBERT_General_4L_312D', vocab_size=30522, model_max_length=1000000000000000019884624838656, is_fast=True, padding_side='right', truncation_side='right', special_tokens={'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}, clean_up_tokenization_spaces=True),  added_tokens_decoder={
	0: AddedToken("[PAD]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
	100: AddedToken("[UNK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
	101: AddedToken("[CLS]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
	102: AddedToken("[SEP]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
	103: AddedToken("[MASK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
}
'''

실습에서 사용한 모델은 TinyBERT 모델 중 huawei-noah/TinyBERT_General_4L_312D

허깅페이스 들어가서 tinybert 검색하면 맨 위에 뜬다. 다운받은 횟수도 제일 많고. (화웨이에서 개발했나봄?)

그 다음에 토크나이저를 초기화하는데, AutoTokenizer.from_pretrained(): 이 부분이 선택한 모델에 맞는 토크나이저를 자동으로 로드한다고 한다. (모델의 설정 파일을 읽어 적절한 토크나이저 클래스를 결정, 모델과 함께 저장된 어휘 파일과 토크나이저 설정을 로드)

근데 토크나이저가 뭔데...?
-> 입력 테스트 전처리, 모델에 입력할 수 있게 변환!
텍스트를 모델이 이해할 수 있도록 작은 단위(토큰)으로 나누는 도구. (모델을 다운받으면 캐시 디렉토리에 함께 저장된다.)
예: "I love NLP" → ["I", "love", "NLP"] 또는 ["I", "love", "N", "##LP"]

  • 텍스트 분할: 문장을 단어나 하위 단어 단위로 나눕니다.
  • 숫자 변환: 각 토큰을 고유한 정수 ID로 변환합니다.
  • 특수 토큰 추가: [CLS], [SEP] 등 모델이 요구하는 특수 토큰을 추가합니다.
  • 패딩/자르기: 입력을 일정한 길이로 맞춥니다.

각 모델은 고유한 어휘 사전을 가지고 있다. 토큰화 방식에도 단어 단위로 할지, 문장 단위로 할지 등 다양한 방식이 있다. 전처리 규칙도 대소문자를 어떻게 처리할지, 특수문자는 어떻게 처리할지가 모델마다 다르다.

이렇게 토크나이저를 거치면 이렇게 문장이 토큰으로 변환된 것을 볼 수 있다. 

tokenizer("Today is monday")

'''
결과

{'input_ids': [101, 2651, 2003, 6928, 102], 
'token_type_ids': [0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1]}
'''

그 다음에 데이터셋의 텍스트를 모델에 넣기 좋게 잘라주는 토큰화 함수를 정의하고, 전체 데이터셋에 토큰화를 적용한다. 

def tokenize(batch):
    # padding=True => 텍스트 데이터를 동일한 길이로 패딩 처리
    # truncation=True => 맥스 토큰 갯수를 넘어가는 문장은 잘라내겠습니다.
    # max_length=300 => 토큰화된 결과의 최대 길이를 300개 (300개 이상의 토큰은 자른다. 300개 이하는 패딩처리)
    temp = tokenizer(batch['review'], padding=True, truncation=True, max_length=300)
    return temp
 
'''
temp 결과에는
input_ids: 토큰의 숫자 ID 시퀀스
attention_mask: 실제 토큰과 패딩 토큰을 구분하는 마스크
token_type_ids: (필요한 경우) 문장 구분을 위한 ID
가 들어있다.
'''
 
dataset = dataset.map(tokenize, batched=True, batch_size=None)
dataset

'''
DatasetDict({
    train: Dataset({
        features: ['review', 'sentiment', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
        num_rows: 35000
    })
    test: Dataset({
        features: ['review', 'sentiment', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
        num_rows: 15000
    })
})
'''

 

 

 

이제 사전학습된 TinyBERT 모델을 감성 분석에 맞게 로드하고 설정한다.

from transformers import AutoModelForSequenceClassification

id2label = {0:'negative', 1:'positive'}
#모델의 숫자 출력을 텍스트 레이블로 해석할 때 사용. 예측 결과를 해석할 때. 

model = AutoModelForSequenceClassification.from_pretrained(
    model, #  사전 학습된 모델의 이름. 여기서는 huawei-noah/TinyBERT_General_4L_312D'
    num_labels = len(label2id), # 레이블 갯수. 분류할 클래스의 숫자, 여기서는 긍정과 부정 2개 
    label2id = label2id, # 레이블 문자열 데이터를 숫자로 매핑 딕셔너리
    id2label = id2label
)

* 긍정/부정을 나누는 작업 => 시퀀스 분류 작업?
시퀀스라는 말이 시계열이랑 살짝 헷갈려서 알아봄.
시퀀스는 '연속된 데이터'를 의미한다. 그래서 텍스트에 단어나 문자가 연속되어 있는 것도 시퀀스라고 부른다. 이 시퀀스는 데이터의 순차적 구조를 나타낸다. 텍스트가 긍정적인지 부정적인지 분류하는 것뿐만 아니라, 이메일이 스팸인지 아닌지, 뉴스 기사의 주제가 무엇인지 등도 시퀀스 분류 작업이라고 할 수 있다. 인풋은 단어들의 시퀀스(문장, 문단)이고 출력은 전체 시퀀스에 대한 단일한 분류 결과(긍정/부정, 맞다/틀리다 등) 전체 시퀀스에 대해 하나의 레이블 예측하면 시퀀스 분류! 

 

다음은 모델 성능 평가를 위한 함수

import evaluate
import numpy as np
accuracy = evaluate.load('accuracy')

def compute_metrics(eval_pred): # 평가지표
    predctions, labels = eval_pred

    predctions = np.argmax(predctions, axis=1)

    return accuracy.compute(predictions=predctions, references=labels) # 예측값, 실제 정답 값

참고로 evaluate는 허깅페이스에서 나온 평가 메트릭 라이브러리이고, 그중에서 정확도를 로드했다. 

이제 학습 시작! 

from transformers import Trainer, TrainingArguments
#Trainer: 모델 훈련을 관리하는 클래스
#TrainingArguments: 훈련 설정을 정의하는 클래스

#훈련 인자 설정하기
args = TrainingArguments(
    output_dir='train_dir', # 학습 결과를 저장할 디렉토리
    overwrite_output_dir=True,
    num_train_epochs=3,
    learning_rate=2e-5,
    per_device_train_batch_size=32, # 각 디바이스당 학습 배치 크기
    per_device_eval_batch_size=32, # 각 디바이스당 평가 배치 크기
    eval_strategy='epoch' # 매 에포크마다 평가
)

#훈련 초기화
trainer = Trainer(
    model=model, 
    args=args,
    train_dataset=dataset['train'],
    eval_dataset=dataset['test'],
    compute_metrics=compute_metrics, #위에서 정의한 함수. 평가 지표를 계산
    tokenizer=tokenizer # 텍스트를 토큰으로 변환하는 도구
)

#훈련 시작
trainer.train()

 

이부분에서 계속 임포트 에러가 나는데

해결하고 다시 써야겠다..