T5 파인튜닝으로 친근한 대화형 챗봇 만들기

목록으로 돌아가기

본 포스트는 paust-team의 깃허브를 기반으로 작성된 글 입니다.


T5 모델

이미지 설명

T5(Traitement avec des Transformateurs pour les Exemples)는 딥 러닝 모델 중 하나로, 텍스트 데이터를 처리하고 다양한 자연어 처리(NLP) 작업을 수행하는 데 사용됩니다. T5 모델은 트랜스포머(Transformer) 아키텍처를 기반으로 하며, “트랜스포머” 아키텍처의 발전된 형태 중 하나입니다.
T5 모델은 OpenAI가 아니라 Google에서 개발되었으며, NLP 분야에서 확장성과 성능을 향상시키는 중요한 모델 중 하나입니다. T5와 같은 모델은 다양한 자연어 이해 및 생성 작업을 자동화하고, 대화형 AI, 기계 번역, 검색 엔진 개선, 질문 응답 시스템, 텍스트 요약, 문서 생성 등의 응용 분야에서 사용됩니다.


데이터

이미지 설명

Aihub에서 제공하는 주제별 텍스트 일상 대화 데이터를 전처리하여 학습에 사용하였다.

Aihub 주제별 텍스트 일상 대화 데이터 다운로드


데이터 전처리 및 환경

모델 전처리 및 환경은 이전 글인 gpt2 개발과 동일하게 가져갔습니다.

GPT2 개발일지로 바로가기

Example 코드

다음은 파인튜닝 없이 T5모델을 바로 사용하는 코드입니다. 현재 모델은 비지도 학습으로 학습된 언어 모델이기 때문에 파인튜닝을 하는 것을 원작자가 추천하고 있습니다.

import torch
from transformers import T5TokenizerFast, T5ForConditionalGeneration

# GPU 사용 가능 여부 확인
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 토크나이저와 모델 로드 및 GPU에 이동
tokenizer = T5TokenizerFast.from_pretrained("paust/pko-chat-t5-large")
model = T5ForConditionalGeneration.from_pretrained("paust/pko-chat-t5-large")
model.to(device)

# 입력 데이터 생성 및 GPU에 이동
prompt_tpl = "사용자가 한 말을 읽고 그에 질문에 답하거나 명령에 응답하는 비서입니다.\n\n사용자:\n{text}\n\n비서:\n"
prompt = prompt_tpl.format(text="한국의 수도는 어디인가요?")
input_ids = tokenizer(prompt, return_tensors='pt').input_ids.to(device)

# 모델로 추론 수행
logits = model.generate(
    input_ids,
    max_length=1024,
    temperature=0.5,
    no_repeat_ngram_size=6,
    do_sample=True,
    num_return_sequences=1,
)
text = tokenizer.batch_decode(logits, skip_special_tokens=True)[0]
print(text)  # 한국의 수도는 서울입니다.



Train 코드

다음은 파인튜닝하는 코드입니다. 먼저 깃허브에서 제공하는 훈련 베이스 라인을 보게되면 다음과 같다.

from transformers import T5TokenizerFast, T5ForConditionalGeneration

tokenizer = T5TokenizerFast.from_pretrained('paust/pko-t5-base')
model = T5ForConditionalGeneration.from_pretrained('paust/pko-t5-base')

input_ids = tokenizer(["qa question: 당신의 이름은 무엇인가요?"]).input_ids
labels = tokenizer(["T5 입니다."]).input_ids
outputs = model(input_ids=input_ids, labels=labels)

print(f"loss={outputs.loss} logits={outputs.logits}")



인풋과 라벨을 토큰나이징해서 모델에 바로 넣는 방식이다. 데이터 로더를 생성하고 이제 내 데이터로 학습을 시켜볼 것이다
이전 처럼 데이터는 동일하기 때문에 인풋으로는 Q열을! 라벨로는 A를 넣어 학습해 볼게요!



import pandas as pd
import torch
from transformers import T5TokenizerFast, T5ForConditionalGeneration, AdamW
from tqdm import tqdm
from torch.utils.data import DataLoader, Dataset

# 디바이스 설정
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# 데이터 로드 및 전처리
def load_and_preprocess_data(file_path):
    df = pd.read_csv(file_path)
    df.dropna(subset=['A', 'Q'], inplace=True)
    df.drop(columns=['id'], inplace=True)  # "id" 컬럼 삭제
    return df

# 데이터셋 클래스 정의
class CustomDataset(Dataset):
    def __init__(self, data, tokenizer, max_length=128):
        self.data = data
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        questions = self.data.iloc[idx]['Q']
        answers = self.data.iloc[idx]['A']

        # 토큰 수를 max_length에 맞춰서 자르거나 패딩
        inputs = self.tokenizer(questions, max_length=self.max_length, truncation=True, padding='max_length', return_tensors='pt')
        labels = self.tokenizer(answers, max_length=self.max_length, truncation=True, padding='max_length', return_tensors='pt')

        return {
            'input_ids': inputs.input_ids[0], 
            'labels': labels.input_ids[0]
        }

# 데이터 로드 및 전처리
df = load_and_preprocess_data("totaldata.csv")

# 토크나이저와 모델 로드
tokenizer = T5TokenizerFast.from_pretrained('paust/pko-chat-t5-large')
model = T5ForConditionalGeneration.from_pretrained('paust/pko-chat-t5-large')
model.to(device)

# 학습 설정
optimizer = AdamW(model.parameters(), lr=1e-4)
criterion = torch.nn.CrossEntropyLoss()

num_epochs = 10
log_interval = 100
checkpoint_interval = 0.5  # 0.5 에폭마다 체크포인트 저장

# DataLoader를 사용하여 데이터 로드
batch_size = 4
dataset = CustomDataset(df, tokenizer)
data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# 학습 루프에서 데이터 로더 사용
total_steps = 0
for epoch in range(num_epochs):
    for batch in tqdm(data_loader, total=len(data_loader)):
        input_ids = batch['input_ids'].to(device)
        labels = batch['labels'].to(device)
        
        # 모델 학습
        outputs = model(input_ids=input_ids, labels=labels)
        loss = outputs.loss
        
        # 역전파 및 가중치 업데이트
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        total_steps += 1

        # 0.5 에폭마다 체크포인트 저장
        if total_steps % int(len(dataset) / (batch_size * 2)) == 0:
            checkpoint_dir = f"checkpoint_{total_steps}"
            model.save_pretrained(checkpoint_dir)

# 학습이 완료된 모델 저장
model.save_pretrained("2save")



생각보다 학습 시간이 좀 걸려서 체크포인트를 0.5에폭에 두었다.



Predict 코드

이미지 설명

장차 12시간을 학습시켜 겨우 1에폭 학습시켰다… 이제 1에폭만 학습한 모델의 성능으로 보려고 한다…
예측코드이며 예측을 방법 1,2,3,4로 나누어 테스트를 진행했다.

import torch
from transformers import T5TokenizerFast, T5ForConditionalGeneration

# 디바이스 설정
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 토크나이저와 모델 로드
tokenizer = T5TokenizerFast.from_pretrained('paust/pko-chat-t5-large')
model = T5ForConditionalGeneration.from_pretrained("checkpoint_175626")  # 저장한 모델 경로를 지정하세요.
model.to(device)

# 대화 히스토리 초기화 (최대 4개의 대화를 유지)
conversation_history = []

#######################  1  ###################################################
# # 반복해서 대화
# while True:
#     # 사용자 입력 받기
#     user_input = input("사용자: ")
    
#     # 대화 히스토리에 사용자 입력 추가 (최대 4개 유지)
#     conversation_history.append(f"사용자: {user_input}")
#     if len(conversation_history) > 4:
#         conversation_history.pop(0)  # 가장 오래된 대화 제거
    
#     # 대화 히스토리를 하나의 문자열로 결합
#     conversation_text = "\n".join(conversation_history)
    
#     # 대화 히스토리를 모델 입력에 추가
#     prompt = f"아래 대화를 보고 마지막 사용자의 말에 친구처럼 질문하고 대화해줘.\n\n{conversation_text}\n\n친구:\n"
#     input_ids = tokenizer(prompt, return_tensors='pt').input_ids.to(device)

#     # 모델로 추론 수행
#     logits = model.generate(
#         input_ids,
#         max_length=1024,
#         temperature=0.5,
#         no_repeat_ngram_size=6,
#         do_sample=True,
#         num_return_sequences=1,
#     )
    
#     # 모델 응답 추출
#     model_response = tokenizer.batch_decode(logits, skip_special_tokens=True)[0]
    
#     # 대화 히스토리에 모델 응답 추가 (최대 4개 유지)
#     conversation_history.append(f"친구: {model_response}")
#     if len(conversation_history) > 4:
#         conversation_history.pop(0)  # 가장 오래된 대화 제거
    
#     # 사용자와 모델의 대화 출력
#     print("친구:", model_response)
#######################  1  ###################################################


#######################  2  ###################################################
# 반복해서 대화
# while True:
#     # 사용자 입력 받기
#     user_input = input("사용자: ")

#     # 대화 내용에 사용자 입력 추가
#     prompt = f"사용자가 한 말을 읽고 그에 말에 대화하는 친구입니다.\n\n사용자:\n{user_input}\n\n친구:\n"
#     input_ids = tokenizer(prompt, return_tensors='pt').input_ids.to(device)

#     # 모델로 추론 수행
#     logits = model.generate(
#         input_ids,
#         max_length=1024,
#         temperature=0.5,
#         no_repeat_ngram_size=6,
#         do_sample=True,
#         num_return_sequences=1,
#     )
#     text = tokenizer.batch_decode(logits, skip_special_tokens=True)[0]

#     # 친구의 응답 출력
#     print("친구:", text)
#######################  2  ###################################################

#######################  3  ###################################################
# 반복해서 대화
# while True:
#     # 사용자 입력 받기
#     user_input = input("사용자: ")

#     # 대화 내용에 사용자 입력 추가
#     prompt = f"아래 대화를 보고 사용자의 말에 친구처럼 질문하고 대화해줘.\n\n사용자:\n{user_input}\n\n친구:\n"
#     input_ids = tokenizer(prompt, return_tensors='pt').input_ids.to(device)

#     # 모델로 추론 수행
#     logits = model.generate(
#         input_ids,
#         max_length=1024,
#         temperature=0.5,
#         no_repeat_ngram_size=6,
#         do_sample=True,
#         num_return_sequences=1,
#     )
#     text = tokenizer.batch_decode(logits, skip_special_tokens=True)[0]

#     # 친구의 응답 출력
#     print("친구:", text)
#######################  3  ###################################################
    
#######################  4  ###################################################
# 반복해서 대화
while True:
    # 사용자 입력 받기
    user_input = input("사용자: ")

    # 대화 내용에 사용자 입력 추가
    prompt = f"{user_input}"
    input_ids = tokenizer(prompt, return_tensors='pt').input_ids.to(device)

    # 모델로 추론 수행
    logits = model.generate(
        input_ids,
        max_length=1024,
        temperature=0.5,
        no_repeat_ngram_size=6,
        do_sample=True,
        num_return_sequences=1,
    )
    text = tokenizer.batch_decode(logits, skip_special_tokens=True)[0]

    # 친구의 응답 출력
    print("친구:", text)
#######################  4  ###################################################



Predict 성능

결론부터 말하자면 전부 실패! 하하

이미지 설명

대화의 흐름이 이전 넘겨준 값에 대해서 지나치거 이상한 포인트에 맞춰 답변하는 것을 볼 수 있었다
이전 대화의 입력이 출력값을 오히려 망치고 있다는 생각이들어 이전 대화 참조 테스트 방식을 더 하진 않았다
본래 인풋에 있어서 해당 규격으로 더 학습을 시켰다면 이정도는 아니지 않았을까 한다…

이미지 설명

이쯤되면 데이터가 잘 못 된건가 싶다… 답변의 질이 좋지는 않다…

이미지 설명

이상하게 느낌적으로 프롬프트로 넣은 텍스트를 사용자 입력에 답변으로 영향 받아 답변한 느낌이 강했다.
그니깐 프롬프트에 친구라는 단어가 있어서 친구란 단어를 꺼내고 흐름에 맞지 않는 친구 이야기를 한 느낌?이었다

이미지 설명

그래서 아예 프롬프트를 없애고 진행해 봤다.
확실히 이전에 비해 친구란 단어가 직접적으로 답변에 등장하지는 않는거 같다.
그리고 학습 시킨 데이터 자체가 카톡 어투여서 그런가 별 다른 프롬프트 없이 친구처럼 말하는걸 볼 수 있었다.



후기

파라미터도 gpt2보다 큰거 같고 개인이 사용하기에도 그리 큰 문제 없는 크기의 모델 같다.
gpt2 모델 보다 더 좋은 답변과 다양한 답변을 꺼냈다.
gpt2는 하나의 질문에 하나의 답변이 고정적으로 등장했다면 T5 모델은 2개? 3개? 정도의 다른 답변을 꺼내놓았다.
내가 특정 테스크에 맞춘 인풋과 라벨 형식의 학습을 시켰는데 원래 기존 모델은 그냥 비지도 학습된 모델이라고 한다
그냥 데이터를 전처리 없이 비지도 시키면 어떨까 생각이 든다.
결론 gpt2보다 더 좋은 성능을 내는 모델 같다.



References

author-profile
Written by 유찬영

댓글