의도 탐지: 커스텀 분류기로 사용자 의도를 효율적으로 파악하기

의도 탐지: 커스텀 분류기로 사용자 의도를 효율적으로 파악하기 (Intent Detection: Identify user intent efficiently with a custom classifier)

이 쿡북에서는 Classifier Factory를 이용한 의도 탐지(intent detection)와 분류에 대해 살펴볼 거예요. 단일 타겟(single-target) 분류를 다루는 특정 예제에 집중해서, 데이터 준비부터 파인튜닝, 추론까지 전 과정을 따라가 볼게요.

출처: 문서

본문

이 쿡북에서는 Classifier Factory를 사용한 의도 탐지와 분류에 대해 탐구할 거예요.

단순하게 유지하기 위해, **단일 타겟 분류(single-target classification)**를 다루는 특정 예제에 집중할게요.

데이터셋 (Dataset)

mteb/amazon_massive_intent 데이터셋의 하위 집합을 사용할 거예요. 이 하위 집합에는 다양한 사용자 요청에 대한 의도가 포함되어 있어요.

하위 집합 (Subset)

하위 집합을 다운로드하고 준비해 봅시다. datasets 라이브러리를 설치하고 데이터셋을 로드할게요.

%%capture
!pip install datasets
# @title Loading and preparing subset
%%capture
# Import necessary libraries
from datasets import load_dataset
import pandas as pd
from sklearn.model_selection import train_test_split

# Load the entire amazon_massive_intent dataset
dataset = load_dataset("mteb/amazon_massive_intent")

# Filter for English language samples
train_samples = dataset["train"].filter(lambda x: x["lang"] == "en")

# Select only the required columns
train_samples = train_samples.select_columns(["text", "label_text"])

# Convert to pandas DataFrame
train_df = pd.DataFrame(train_samples)

# Function to remove labels with less than 200 samples and limit each label to 600 samples
def process_labels(df, min_samples=200, max_samples=600):
    label_counts = df["label_text"].value_counts()
    labels_to_keep = label_counts[label_counts >= min_samples].index
    df = df[df["label_text"].isin(labels_to_keep)]

    # Limit each label to max_samples
    balanced_df = pd.DataFrame()
    for label in labels_to_keep:
        label_samples = df[df["label_text"] == label].sample(
            n=min(len(df[df["label_text"] == label]), max_samples), random_state=42
        )
        balanced_df = pd.concat([balanced_df, label_samples])

    return balanced_df

# Process labels in the training dataset
train_df = process_labels(train_df)

# Split the training data into train, validation, and test sets
train_df, temp_df = train_test_split(
    train_df, test_size=0.2, random_state=42, stratify=train_df["label_text"]
)
validation_df, test_df = train_test_split(
    temp_df, test_size=0.5, random_state=42, stratify=temp_df["label_text"]
)

Python

# Display the test DataFrame to verify
test_df

Python

# @title Data distribution
import matplotlib.pyplot as plt
from collections import Counter

# Function to count the number of samples for each label
def count_labels(samples):
    labels = [sample["label_text"] for sample in samples.to_dict("records")]
    return Counter(labels)

# Count labels for each dataset
train_label_counts = count_labels(train_df)
validation_label_counts = count_labels(validation_df)
test_label_counts = count_labels(test_df)

# Create a single figure with subplots for bar charts
fig, axes = plt.subplots(3, 1, figsize=(12, 18))

# Plot the bar charts for label distribution
def plot_label_distribution(ax, label_counts, title):
    # Sort labels by count in descending order
    sorted_labels, sorted_counts = zip(
        *sorted(label_counts.items(), key=lambda x: x[1], reverse=True)
    )
    ax.bar(sorted_labels, sorted_counts, color="skyblue")
    ax.set_ylabel("Count")
    ax.set_title(title)
    ax.tick_params(axis="x", rotation=45)

# Plot label distribution for each dataset
plot_label_distribution(
    axes[0], train_label_counts, "Train Samples (Label Distribution)"
)
plot_label_distribution(
    axes[1], validation_label_counts, "Validation Samples (Label Distribution)"
)
plot_label_distribution(axes[2], test_label_counts, "Test Samples (Label Distribution)")

plt.tight_layout()
plt.show()

데이터 포맷 (Format Data)

이제 데이터셋을 로드했으니, 학습을 위해 업로드할 올바른 원하는 형식으로 변환할 거예요.

데이터는 다음과 같은 JSONL 형식으로 변환돼요.

{"text": "place a birthday party with ale ross and amy in my calendar", "labels": {"intent": "calendar_set"}}
{"text": "new music tracks", "labels": {"intent": "play_music"}}
{"text": "get me the details of upcoming oscar two thousand and seventeen", "labels": {"intent": "calendar_query"}}
{"text": "is there any event today in my calendar", "labels": {"intent": "calendar_query"}}
{"text": "send email to mommy that i'll be going the party", "labels": {"intent": "email_sendemail"}}
...

라벨의 예는 다음과 같아요.

"labels": {
  "intent": "email_sendemail"
}

단일 타겟(single-target) 분류를 위한 것이에요.

Python

from tqdm import tqdm
import json

def dataset_to_jsonl(dataset, output_file):
    # Extract the unique labels from the dataset
    unique_labels = dataset["label_text"].unique()

    # Open the output file in write mode
    with open(output_file, "w") as f:
        # Iterate over each row in the dataset
        for _, row in tqdm(dataset.iterrows(), total=dataset.shape[0]):
            # Extract the text and label from the row
            text = row["text"]
            intent = row["label_text"]

            # Create the JSON object with the desired structure
            json_object = {"text": text, "labels": {"intent": intent}}

            # Write the JSON object to the file as a JSON line
            f.write(json.dumps(json_object) + "\n")

# Save files
dataset_to_jsonl(train_df, "train.jsonl")
dataset_to_jsonl(validation_df, "validation.jsonl")
dataset_to_jsonl(test_df, "test.jsonl")

데이터가 올바르게 변환·저장됐어요. 이제 모델을 학습시킬 수 있어요.

학습 (Training)

모델을 학습시키는 방법은 두 가지예요. la platforme에서 업로드·학습하거나, API를 통해 학습할 수 있어요.

먼저 mistralai를 설치할게요.

%%capture
!pip install mistralai

그리고 클라이언트를 설정해요. 여기에서 API 키를 만들 수 있어요.

from mistralai.client import Mistral
import os

# Set the API key for Mistral
api_key = "API_KEY"

# Set your Weights and Biases key
wandb_key = "WANDB_KEY"

# Initialize the Mistral client
client = Mistral(api_key=api_key)

학습 세트와 검증 세트(선택)의 2개 파일을 업로드할게요. 검증 세트는 검증 손실(validation loss)에 사용됩니다.

# Upload the training data
training_data = client.files.upload(
    file={
        "file_name": "train.jsonl",
        "content": open("train.jsonl", "rb"),
    }
)

# Upload the validation data
validation_data = client.files.upload(
    file={
        "file_name": "validation.jsonl",
        "content": open("validation.jsonl", "rb"),
    }
)

데이터가 업로드됐으니 작업(job)을 만들 수 있어요.

우리는 사용자들이 상당히 많은 지표를 추적할 수 있도록 Weights and Biases 통합을 지원하는데, 이 기능을 적극 권장해요. 프로젝트 이름과 키를 제공하면 사용할 수 있어요.

Python

# Create a fine-tuning job
created_job = client.fine_tuning.jobs.create(
    model="ministral-3b-latest",
    job_type="classifier",
    training_files=[{"file_id": training_data.id, "weight": 1}],
    validation_files=[validation_data.id],
    hyperparameters={"training_steps": 100, "learning_rate": 0.00004},
    auto_start=False,
    integrations=[
        {
            "project": "intent-classifier",
            "api_key": wandb_key,
        }
    ]
)
print(json.dumps(created_job.model_dump(), indent=4))

작업이 생성되면 epoch 수와 같은 세부사항을 검토할 수 있어요. 덕분에 작업을 시작하기 전에 정보를 바탕으로 결정을 내릴 수 있어요.

작업을 가져와서 시작 전에 검증 프로세스가 완료될 때까지 기다릴게요. 이 검증 단계는 작업이 시작할 준비가 되었는지 보장해요.

Python

# Retrieve the job details
retrieved_job = client.fine_tuning.jobs.get(job_id=created_job.id)
print(json.dumps(retrieved_job.model_dump(), indent=4))

import time
from IPython.display import clear_output

# Wait for the job to be validated
while retrieved_job.status not in ["VALIDATED"]:
    retrieved_job = client.fine_tuning.jobs.get(job_id=created_job.id)

    clear_output(wait=True)  # Clear the previous output (User Friendly)
    print(json.dumps(retrieved_job.model_dump(), indent=4))
    time.sleep(1)

이제 작업을 실행할 수 있어요.

Python

# Start the fine-tuning job
client.fine_tuning.jobs.start(job_id=created_job.id)

# Retrieve the job details again
retrieved_job = client.fine_tuning.jobs.get(job_id=created_job.id)
print(json.dumps(retrieved_job.model_dump(), indent=4))

작업이 이제 시작되고 있어요. 상태를 추적하면서 정보를 출력해 볼게요.

여러 지표를 추적하려면 Weights and Biases 통합 사용을 강력히 권장해요.

WANDB

학습 (Training):

intent-train-loss.png

평가/검증 (Eval/Validation):

intent-validation-loss.png

Python

# Wait for the job to complete
while retrieved_job.status in ["QUEUED", "RUNNING"]:
    retrieved_job = client.fine_tuning.jobs.get(job_id=created_job.id)

    clear_output(wait=True)  # Clear the previous output (User Friendly)
    job_info = json.dumps(retrieved_job.model_dump(), indent=4)
    if len(job_info) > 10000:
        print(job_info[:5000] + "\n[...]\n" + job_info[-5000:])
    else:
        print(job_info)
    time.sleep(5)

추론 (Inference)

모델이 학습되어 사용할 준비가 됐어요! 테스트 세트의 샘플로 테스트해 볼게요.

Python

# Load the test samples
with open("test.jsonl", "r") as f:
    test_samples = [json.loads(l) for l in f.readlines()]

# Classify the first test sample
classifier_response = client.classifiers.classify(
    model=retrieved_job.fine_tuned_model,
    inputs=[test_samples[0]["text"]],
)
print("Text:", test_samples[0]["text"])
print("Classifier Response:", json.dumps(classifier_response.model_dump(), indent=4))

가장 높은 점수의 결과는 weather_query로, 99%가 넘는 점수를 기록했어요!

여기까지가 나만의 분류기를 학습하고 배치 추론을 사용하는 간단한 가이드예요.

더 구체적인 멀티 라벨(multi-label) 분류기에 대해서는 이 쿡북을 방문해 보세요.

**멀티 타겟(multi-target)**에 대한 제품 중심의 심층 가이드와 LLM과 우리 분류기의 평가 비교에 대해서는 이 쿡북을 방문해 보세요.

더 알아보기 (Learn more)