[Hands-On] 거대언어모델을 활용한 프롬프트 기반 텍스트 분류

Hugman Sangkeun Jung
22 min readJun 30, 2024

--

(You can find the English version of the post at this link.)

이전 글에서 우리는 분류기술의 기본 개념과 전통적인 방법들에 대해 살펴보았습니다. 이 포스트는 텍스트 분류를 수행하는 여러가지 방법 중 거대언어모델을 활용하여 Prompt에 기반한 분류를 수행하는 실제 코드를 구현해보고 그 결과를 시각화해보도록 하겠습니다.

프롬프트 기반 텍스트 분류란 무엇인가?

프롬프트 기반 텍스트 분류는 대규모 언어 모델(Large Language Model, LLM)을 이용한 비훈련 텍스트 분류 방법입니다. 이 방법은 전통적인 분류 방법과는 다르게 작동합니다. 주요 특징은 다음과 같습니다:

  1. 프롬프트 사용: 모델에게 특정 지시를 주는 짧은 텍스트인 프롬프트를 사용합니다.
  2. 언어 생성 문제로 접근: 분류 문제를 언어 생성 문제로 변환합니다.
  3. 유연성: 고정된 클래스 수에 제한되지 않고 다양한 분류 작업에 적용할 수 있습니다.
  4. 사전 학습 모델 활용: GPT-3.5와 같은 대규모 언어 모델의 지식을 활용합니다.

이 방법의 핵심 아이디어는 분류 작업을 모델이 이해할 수 있는 자연어 질문으로 변환하는 것입니다. 예를 들어, 구체적인 분류 대상을 프롬프트로 던져면서 프롬프트의 일부에 “이 텍스트의 주제는 무엇인가요?”라는 질문을 던짐으로써 언어모델이 직접적으로 분류 결과를 ‘생성’하도록 하는 것입니다.

이제 이러한 기본 개념을 바탕으로 프롬프트 기반 텍스트 분류를 구현해보겠습니다.

환경 준비하기

먼저, 필요한 라이브러리를 설치하고 가져옵니다.

!pip install -qq datasets
!pip install -qq openai

datasets는 Hugging Face의 ag_news 데이터셋을 쉽게 로드하고 처리할 수 있게 해주는 라이브러리입니다. 이 라이브러리를 통해 우리는 텍스트 분류 작업에 필요한 대규모 뉴스 기사 데이터셋을 간편하게 사용할 수 있습니다.

openai는 OpenAI에서 제공하는 강력한 언어 모델인 GPT-3.5를 사용할 수 있게 해주는 Python 클라이언트 라이브러리입니다. 이 라이브러리를 통해 우리는 GPT-3.5 모델에 프롬프트를 전송하고 생성된 응답을 받아올 수 있습니다.

본 포스트에서 구현하는 코드에 꼭 openai의 유료 key가 필요하지는 않습니다. 유료로 결제하지 않은 분들을 위해 저자가 미리 GPT-3.5 모델을 사용하여 생성한 응답 데이터를 준비해 두었습니다. 이 데이터는 본 튜토리얼의 목적을 위해 충분한 샘플을 포함하고 있으며, USE_GENERATED_DATA 변수를 True로 설정하면 이 미리 생성된 데이터를 사용할 수 있습니다. 이후에 자세하게 설명하겠습니다.

데이터 준비

from datasets import load_dataset
import pandas as pd

# Load the AG News dataset from the Hugging Face 'datasets' library
dataset = load_dataset("ag_news")

# Preview the dataset structure
print(dataset)
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 120000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 7600
})
})

AG News 데이터셋은 4개의 주제 카테고리로 분류된 뉴스 기사로 구성되어 있습니다. 데이터셋은 훈련 세트와 테스트 세트로 나뉘어 있으며, 각 세트는 ‘text’‘label’ 두 가지 특성을 가지고 있습니다.

‘text’는 뉴스 기사의 내용이고, ‘label’은 해당 기사의 주제 카테고리를 나타내는 정수값입니다. 훈련 세트는 120,000개의 샘플을, 테스트 세트는 7,600개의 샘플을 포함하고 있어 충분한 양의 데이터로 모델을 학습하고 평가할 수 있습니다.

# Map the prediction index to the class name (if you have a label map)
label_map = {0: "World", 1: "Sports", 2: "Business", 3: "Sci/Tech"}

label_map은 데이터셋의 정수 레이블을 해당하는 주제 카테고리 이름으로 매핑하는 사전입니다. 이 매핑을 통해 우리는 모델의 예측 결과를 사람이 이해하기 쉬운 카테고리 이름으로 쉽게 변환할 수 있습니다. 예를 들어, 모델이 0을 예측했다면 이는 “World” 카테고리에 해당함을 알 수 있습니다. 이러한 매핑은 결과 해석과 시각화 과정에서 매우 유용하게 사용됩니다.

클라우드 기반 언어모델 연결 준비

이 튜토리얼을 수행하기 위해서는 몇 가지 사전 준비가 필요합니다. 주요 사항은 OpenAI API key입니다. 이미 API key를 가지고 있다면, 아래 코드에서 해당 key를 설정해주세요. 없으시다면 굳이 따로 구매하지 않고 진행해도 괜찮습니다.

이 강의에서는 초기에 key 필드를 비워두었습니다. OpenAI API key가 없어도 다음 두 가지 방법으로 튜토리얼을 진행할 수 있습니다:

  1. 저자가 미리 프롬프트에 대한 텍스트를 생성해 두었습니다. 아래 섹션에서 USE_GENERATED_DATA=True로 설정하고 진행할 수 있습니다.
  2. GPT-3.5를 사용하고 싶지 않다면, 다른 클라우드 기반 LLM 서비스를 사용하거나 LLaMA 3와 같은 오픈 소스 LLM의 로컬 인스턴스를 연결하여 본 코드에서 수행하는 것처럼 문장들을 생성하면 됩니다.
# !!!! NOTE
#USE_GENERATED_DATA = False # <-- use GPT 3.5
USE_GENERATED_DATA = True # <-- use pre-generated texts from GPT 3.5

프롬프트 구성기 구현

# Function to add a prompt to the texts
def make_topic_cls_prompt(text):
return f"Task : Topic Classification. Topic-class must be one of the (World, Sports, Business, Sci/Tech). The text is [{text}]. What is topic?"

이 함수는 주어진 텍스트에 대한 프롬프트를 생성합니다. 예를 들어, “The stock market experienced significant fluctuations today.” 라는 문장을 넣어주면 그 결과로 “Task : Topic Classification. Topic-class must be one of the (World, Sports, Business, Sci/Tech). The text is [The stock market experienced significant fluctuations today.]. What is topic?” 라는 텍스트를 리턴해줍니다. 일종의 컨텐스트를 보강한 문서형태가 리턴되는것이죠. 최종적으로 이 텍스트가 거대언어모델의 프롬프트로 입력됩니다.

def map_generated_to_topic(generated_text):
generated_text = generated_text.lower()
if "world" in generated_text:
return "World"
elif "sport" in generated_text:
return "Sports"
elif "business" in generated_text:
return "Business"
elif "sci" in generated_text:
return "Sci/Tech"
elif "technology" in generated_text:
return "Sci/Tech"
else:
return "Unknown"

이 함수는 언어모델이 생성한 텍스트를 분석하여 적절한 주제 카테고리로 매핑합니다. 이 부분이 최종 분류 성능을 결정하는 부분이기 때문에 매우 섬세한 설계가 필요한 부분입니다. 하지만 본 포스트에서는 전체적인 구현 과정을 설명하는 튜토리얼이기 때문에 이 부분은 매우 단순하게 처리했습니다.

GPT 3.5 언어모델 준비

OpenAI 모델 준비

if USE_GENERATED_DATA == False:
import os
# Replace 'your_openai_api_key' with your actual OpenAI API key
os.environ["OPENAI_API_KEY"] = "" # <--- set your OPENAI API KEY

# -----
from openai import OpenAI
client = OpenAI()
import json

def query_to_gpt(text):
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": f"{text}"}
]
)
return response.choices[0].message. Content

# ---
sample_text = "Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\band of ultra-cynics, are seeing green again."

# Query GPT-3.5
prompt = make_topic_cls_prompt(sample_text)
generated_text = query_to_gpt(prompt)
print("Generated text:", generated_text)

# Prediction with generated text
# : Map the generated text to a topic
predicted_topic = map_generated_to_topic(generated_text)
print(f"Predicted Topic: {predicted_topic}")

이 코드는 OpenAI의 GPT-3.5 모델을 사용하기 위한 설정과 함수를 정의합니다. 먼저, USE_GENERATED_DATAFalse일 때만 이 코드가 실행됩니다. 즉, 실시간으로 GPT-3.5를 사용하고자 할 때 활성화됩니다. 만약 True라면 이 코드는 실행되지 않고, 대신 미리 생성된 데이터를 사용하게 됩니다. 이는 OpenAI API 키가 없는 사용자들도 튜토리얼을 진행할 수 있게 해주는 옵션입니다.

os.environ["OPENAI_API_KEY"]에 실제 OpenAI API 키를 입력해야 합니다. 이는 OpenAI 서비스를 이용하기 위한 인증 키입니다. API 키는 개인정보이므로 공개된 코드에 직접 입력하지 않도록 주의하세요.

만약 USE_GENERATED_DATAFalse 였다면 위 sample text 에 대해서 실제 예측 결과 한 건이 출력되어야 합니다.

자 이로써 이제 분류기를 위한 모든 준비가 모두 끝났습니다. 프롬프트 기반 분류기에서는 어떠한 신경망 훈련도 필요하지 않습니다. 대신, 거대 언어 모델의 능력을 활용하여 텍스트를 분류합니다. 위에서 간단하게 구현한 프롬프트 구성기, Query 를 날리는 API 함수, 생성결과물의 분류라벨 해석기 등등만 있으면 바로 테스트 데이터에 ‘분류’를 수행할 준비가 끝난 것입니다.

평가데이터에 대한 분류 수행

import requests
from datasets import Dataset

if USE_GENERATED_DATA == True:
print("[Download] dataset download")
url = 'https://www.dropbox.com/scl/fi/n7wappv0dbh6jta6ghfon/subset_dataset.json?rlkey=0lcqfndfcndn6vfsudthbrmmm&st=nli9kv1s&dl=1'

# Download the file content
response = requests.get(url)
response.raise_for_status() # Ensure the request was successful
dataset_json = response.json() # Convert the response to JSON

# Convert JSON data to DataFrame
df = pd.DataFrame(dataset_json)

# Convert DataFrame to Hugging Face dataset
subset_test_dataset = Dataset.from_pandas(df)
print("Dataset loaded and converted to Hugging Face dataset format.")
else:
subset_test_dataset = dataset['test'].select( range(100) ) # sample 100
subset_test_dataset = subset_test_dataset.add_column("generated_text", ['']*len(subset_test_dataset))

위 코드는 USE_GENERATED_DATA 옵션에 따라 미리 생성해둔 데이터셋을 다운로드하거나 직접 사용자 언어모델을 통해 생성한 데이터셋을 준비합니다. 선택된 데이터셋은 Hugging Face의 Dataset 형식으로 변환됩니다.

subset_test_dataset
Dataset({
features: ['text', 'label', 'generated_text', '__index_level_0__'],
num_rows: 100
})

데이터셋의 구조를 간단히 살펴보면, 텍스트, 레이블, 생성된 텍스트등이 포함되어 있습니다. 간단히 5개 쿼리 텍스트에 대한 실제 언어모델의 결과물을 살펴보면 아래와 같습니다.

subset_test_dataset['generated_text'][:5]
['Business',
'The topic is Sci/Tech.',
'The topic of the text is Sci/Tech.',
'The topic of the text is Sci/Tech, as it discusses the use of a prediction unit to forecast wildfires.',
'The topic of the text is "Sci/Tech" (Science/Technology) as it discusses Southern California\'s efforts to reduce air pollution from dairy cow manure.']

보다시피 우리가 생성 프롬프트를 Topic label 만 생성하라고 했음에도 라벨만 생성한 경우도 있고, 문장형태로 길게 생성하는 경우도 있습니다. 분류 성능향상을 위해서는 이 문장을 ‘라벨’로 바꾸는 기능에 조금 더 신경을 써야 합니다.

이제 실제 결과를 한번 살펴보죠. 모든 테스트에 대해서 수행하지 않고 일부에 대해서만 수행해 봅시다.

import pandas as pd
from tqdm import tqdm
data = {'text':[], 'pred_label':[], 'ref_label':[], 'llm_answer':[]}

for text, ref_label, _gen in tqdm(zip(subset_test_dataset['text'],
subset_test_dataset['label'],
subset_test_dataset['generated_text']
)):
prompt = make_topic_cls_prompt(text)
generated_text = _gen if USE_GENERATED_DATA else query_to_gpt(prompt)
predicted_label = map_generated_to_topic(generated_text)

data['text'].append(text)
data['pred_label'].append(predicted_label)
data['ref_label'].append(label_map[ref_label])
data['llm_answer'].append(generated_text)

result_df = pd.DataFrame(data)
result_df

언뜻 보아도 꽤나 잘 맞죠? 주제분류를 위한 어떠한 훈련데이터의 준비도, 그 데이터를 이용한 훈련과정도 없었습니다만, 성능이 꽤나 잘 나오고 있음을 확인할 수 있습니다.

하지만 언어모델의 특성상 우리가 원치 않는 결과들도 나올 수 있고, 그것을 라벨로 해석하는 과정에서 몇몇 예외가 나타날 수 있습니다. 예를 들어, 미리 구동해 놓은 분류 결과들을 아래처럼 분석해보면

set( result_df.pred_label.values )
{'Business', 'Sci/Tech', 'Sports', 'Unknown', 'World'}
result_df[ result_df.pred_label == 'Unknown' ].llm_answer.values
array(['The topic of the text is "Entertainment" as it revolves around Michael Jackson and his court appearance for child molestation charges.',
'The topic is Politics, specifically the 2004 presidential election in California between Democratic challenger John Kerry and President Bush.'],
dtype=object)

이렇게 나오고 있음을 알 수 있습니다. Entertainment Politics 라는 전혀 의도하지 않은 라벨이 결과로 나오고 있습니다. 즉 ‘Unknown’ 이라는 클래스가 있는 거죠.

더 세밀하게 이 부분을 예외 처리할 수 있지만, 여기서는 단순하게 모든 ‘Unknown’ 을 ‘World’로 맵핑하도록 하겠습니다.

# Assign 'World' class to 'Unknown' classes
result_df.loc[result_df.pred_label == 'Unknown', 'pred_label'] = 'World'

이제 되었습니다. 테스트 데이터셋에 존재하는 라벨들의 집합과 우리 분류기의 예측 라벨들의 집합을 일치시켰습니다.

구체적으로 성능을 분석해보죠.

평가 및 시각화

import pandas as pd
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix

# Assuming result_df is your DataFrame and it's already defined

# Calculate metrics
accuracy = accuracy_score(result_df['ref_label'], result_df['pred_label'])
precision, recall, f1, _ = precision_recall_fscore_support(result_df['ref_label'], result_df['pred_label'], average='weighted')

# Create a DataFrame to display metrics
metrics_df = pd.DataFrame({
'Metric': ['Accuracy', 'Precision', 'Recall', 'F1-score'],
'Value': [accuracy, precision, recall, f1]
})

metrics_df.T
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix

# Generate the confusion matrix
cm = confusion_matrix(result_df['ref_label'], result_df['pred_label'])

# Get sorted unique labels
labels = sorted(result_df['ref_label'].unique())

# Plot the confusion matrix
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels, yticklabels=labels)
plt.xlabel('Predicted Labels')
plt.ylabel('True Labels')
plt.title('Confusion Matrix')
plt.show()
Confusion Matrix of Prompt-based Topic Classification on the sampled AG-NEWS dataset (Image by the author)

분류의 전체 성능이 약 0.80 정도로 나옵니다. 이것은 꽤나 놀라운 일입니다. 왜냐하면 우리가 주제분류를 위해 들인 노력이 거의 0에 가깝기 때문입니다. 산술적으로 생각하면 총 4개의 라벨이니 랜덤 추측을 하게 되면 0.25 정도의 성능이 나와야 하고, 보통 0.25의 성능을 유의미한 분류 성능으로 끌어 내기 위해 토픽분류 데이터 준비, 신경망 디자인, 신경망 훈련, 예측등의 여러 단계를 거쳐야 어느정도 믿을만한 분류기를 얻을 수 있습니다.

그러나 우리는 신뢰도 높은 ‘거대언어모델’의 생성능력만 사용하여 80%의 분류 성능을 확보했습니다.

이 과정을 기존의 분류기법에 비교해서 그림으로 정리해보면 다음과 같습니다.

Comparison of Head-Based Classification and Prompt-Based Classification methods

프롬프트 분류기 개선 사항

들인 노력에 비해 얻은 결과는 크지만, 조금만 더 노력하면 더 높은 성능까지 올릴 수 있습니다. 본 글에서는 몇 가지 방향만 제시하도록 하겠습니다. 독자분들이 각자 추가적인 개선을 진행해보기를 추천 드립니다.

  1. 프롬프트 엔지니어링
    : 현재의 경우 zero-shot 프롬프트입니다. 어떠한 유사 예제 없이 바로 질문을 물어보고 있죠. 이를 few-shot 프롬프팅 형태로 개선하게 되면 성능이 꽤나 높게 올라갈 것입니다.
  2. 결과-라벨 해석기 고도화
    : 현재는 생성 문장에 특정 라벨이 포함되느냐 마느냐 가지고 단순하게 접근했습니다. 여기에서 나타난 어휘들의 성격과 종류를 고려하여 해석파트를 다시 고도화하게 되면 성능이 더 올라갈 것입니다.
  3. 언어모델 변경
    : 현재는 GPT 3.5 를 쓰고 있습니다. 사실 이 모델도 매우 좋은 언어모델입니다만, 최근에는 GPT 4, GPT4o, Claude Sonnet 등 좋은 언어모델들이 많이 나와 있습니다. 이러한 개선된 언어모델로 ‘교체’만 하는 노력을 들이더라도, 성능은 꽤나 드라마틱하게 바뀝니다.
  4. 앙상블 기법 적용
    : 본 구현체에서는 토픽정보가 담긴 생성문장을 하나의 언어모델-한번의 쿼리로만 얻어냈습니다. 이를 하나의 언어모델-여러번의 쿼리 혹은 여러모델-여러번의 쿼리 형태로 다양한 생성문장을 얻어낸 후 그 결과를 종합하면 성능이 개선될 것입니다.

결론

이번 글에서는 프롬프트 기반 텍스트 분류 기법을 직접 구현하고 AG News 데이터셋을 활용하여 이 방법이 어떻게 작동하는지 살펴보았습니다. 이 방법의 주요 특징과 장단점은 다음과 같습니다:

장점:

  1. 유연성: 다양한 분류 작업에 쉽게 적용할 수 있습니다.
  2. 제로샷 학습: 특정 작업을 위한 추가 학습 없이 사용할 수 있습니다.
  3. 해석 가능성: 모델의 응답을 직접 확인할 수 있어 결과 해석이 용이합니다.

단점:

  1. 속도: API 호출에 의존하기 때문에 대량의 데이터 처리 시 시간이 오래 걸릴 수 있습니다.
  2. 비용: API 사용에 따른 비용이 발생합니다.
  3. 일관성: 모델의 응답이 항상 일관적이지 않을 수 있습니다.

프롬프트 기반 텍스트 분류 기법은 비교적 최근에 개발된 분류기법 중 하나입니다. 분류문제에 접근하는 방식도 그 결과를 내놓는 방식도 기존 기법들과 매우 다르죠. 물론 성능은 아직까지는 전통적인 방식이 더 높을 수 있습니다. 하지만 언어모델의 성능이 점차 가파르게 좋아지고 있고 또 이러한 언어모델을 이해하고 활용하는 방법도 고도화되고 있기 때문에, 프롬프트 기반 분류기술은 향후 매우 유망한 기법이 될 것입니다.

이번 글에서 구현한 코드는 아래 Colab 링크에서 직접 실행해 볼 수 있습니다.

--

--

Hugman Sangkeun Jung

Hugman Sangkeun Jung is a professor at Chungnam National University, with expertise in AI, machine learning, NLP, and medical decision support.