AI

Gemma 3 미세 조정: 맞춤형 Q&A 데이터셋을 활용한 단계별 가이드

Bright Data와 Unsloth를 활용하여 리뷰 수집부터 배포까지, 맞춤형 Trustpilot QA 데이터로 Google Gemma 3를 정밀 조정하세요.
9 분 읽기
A graphic featuring the logo for 'Gemma 3' over a blue background, surrounded by abstract representations of code snippets and user interface elements, along with icons suggesting security and performance.

2026년 3월 출시된 구글의 최신 오픈소스 AI 모델 Gemma 3는 제한된 하드웨어 자원에서도 효율적으로 작동하면서도 많은 독점 LLM에 필적하는 인상적인 성능을 제공합니다. 이 오픈소스 AI의 발전은 다양한 플랫폼에서 작동하며 전 세계 개발자들에게 접근 가능한 형식으로 강력한 기능을 제공합니다.

본 가이드에서는 트러스트파일럿 리뷰에서 추출한 맞춤형 질문-답변 데이터셋으로 Gemma 3을 미세 조정하는 과정을 안내합니다. Bright Data를 활용해 고객 리뷰를 수집하고, 이를 구조화된 QA 쌍으로 가공한 후, 최소한의 컴퓨팅 자원으로 효율적인 미세 조정을 위해 Unsloth를 활용할 것입니다. 최종적으로 특정 분야 질문을 이해하는 맞춤형 AI 어시스턴트를 구축하고, Hugging Face Hub에 호스팅할 준비를 마칠 수 있습니다.

시작해 보겠습니다!

Gemma 3 이해하기

Google의 Gemma 3 패밀리는 2026년 3월 출시되었으며, 1B, 4B, 12B, 27B 매개변수라는 네 가지 공개 무게 크기로 제공됩니다. 모두 단일 GPU에서 실행되도록 설계되었습니다.

  • 1B 모델은 텍스트 전용이며 32K 토큰 컨텍스트 창을 지원합니다.
  • 4B, 12B, 27B 모델은 다중 모드(텍스트 + 이미지) 입력을 추가하고 128K 토큰 창을 지원합니다.

LMArena 인간 선호도 리더보드에서 Gemma 3-27B-IT는 Llama 3 405B 및 DeepSeek-V3와 같은 훨씬 더 큰 모델보다 높은 점수를 기록하며, 멀티 GPU 설치 공간 없이도 최첨단 품질을 제공합니다.

A scatter plot showing model performance (ELO Score) versus model size (in billion parameters). The plot features various models indicated by points, including Gemma 3 27B IT, Qwen 2.5 72B Instruct, Llama 3.3 70B Instruct, Meta Llama 3.1 70B Instruct, and more. The x-axis represents model size, while the y-axis represents ELO Score, with specific points highlighted for their performance.

이미지 출처: Gemma 3 소개

Gemma 3 모델의 주요 특징

Gemma 3 모델의 주목할 만한 특징은 다음과 같습니다:

  • 4B, 12B, 27B 모델에서다중 모드 입력 (텍스트 + 이미지) 지원합니다.
  • 긴 컨텍스트 지원— 최대 128K 토큰(1B 모델은 32K).
  • 다국어 지원 – 기본 제공 35개 이상의 언어 지원, 사전 훈련된 140개 이상의 언어 지원.
  • 양자화 인식 훈련 (QAT) – 공식 QAT 버전은 높은 품질을 유지하면서 메모리 사용량을 약 3배까지 크게 줄입니다.
  • 함수 호출 및 구조화된 출력 – 호출 자동화 및 구조화된 응답 수신을 위한 내장 지원 포함.
  • 효율성 – 단일 GPU/TPU 또는 휴대폰, 노트북, 워크스테이션에 이르는 소비자 기기에서도 실행되도록 설계되었습니다.
  • 안전성(ShieldGemma) – 통합 콘텐츠 필터링 프레임워크를 제공합니다.

Gemma 3를 미세 조정해야 하는 이유

미세 조정은 Gemma 3과 같은 사전 훈련된 모델을 사용하여 특정 영역이나 작업에 대한 새로운 행동을 가르치며, 처음부터 훈련하는 데 드는 시간과 비용을 절약합니다. 컴팩트한 설계와 4B+ 변형의 다중 모드 지원으로 Gemma 3은 가볍고 저렴하며, 제한된 리소스를 가진 하드웨어에서도 미세 조정이 가능합니다.

미세 조정의 이점은 다음과 같습니다:

  • 도메인 특화 – 모델이 산업별 언어를 이해하고 해당 도메인 내 전문 작업에서 더 나은 성능을 발휘하도록 돕습니다.
  • 지식 강화 – 모델의 초기 훈련 데이터에 포함되지 않았던 중요한 사실과 맥락을 추가합니다.
  • 행동 개선 – 모델의 응답 방식을 조정하여 브랜드의 어조나 선호하는 출력 형식에 맞춥니다.
  • 자원 최적화 – 새로운 모델을 처음부터 훈련하는 것에 비해 훨씬 적은 컴퓨팅 자원을 사용하면서도 고품질 결과를 달성합니다.

필수 조건

이 튜토리얼을 시작하기 전에 다음 사항을 확인하십시오:

  • 시스템에Python 3.9 이상이 설치되어 있어야 합니다.
  • Python 프로그래밍에 대한 기본 지식.
  • GPU 지원 컴퓨팅 환경(예: Google Colab, Jupyter Notebook 또는 Kaggle Notebooks)에 대한 접근 권한.
  • 머신 러닝 및 대규모 언어 모델(LLM)의 기본 원리에 대한 이해.
  • VS Code 또는 유사한 IDE 사용 경험.

외부 서비스에 대한 접근 자격 증명도 필요합니다:

파인 튜닝을 위한 맞춤형 데이터셋 구축

모델이 학습해야 할 행동을 데이터셋이 정확히 반영할 때 파인 튜닝이 가장 효과적입니다. 특정 사용 사례에 맞춤화된 사용자 지정 데이터셋을 생성하면 모델 성능을 획기적으로 향상시킬 수 있습니다. “쓰레기를 넣으면 쓰레기가 나온다” 는 고전적인 원칙을 기억하세요. 데이터셋 준비에 시간을 투자하는 것이 중요한 이유입니다.

고품질 데이터셋은 다음을 충족해야 합니다:

  • 특정 사용 사례에 부합해야 합니다 – 데이터셋이 목표 애플리케이션과 일치할수록 모델 출력의 관련성이 높아집니다.
  • 일관된 형식 유지 – 질문-답변 쌍과 같은 균일한 구조는 모델이 패턴을 더 효과적으로 학습하는 데 도움이 됩니다.
  • 다양한 사례를 포함해야 합니다 – 다양한 시나리오는 모델이 서로 다른 입력값에 대해 일반화하는 데 도움이 됩니다.
  • 깨끗하고 오류가 없어야 합니다 – 불일치와 잡음을 제거하면 모델이 원치 않는 행동을 학습하는 것을 방지할 수 있습니다.

다음과 같은 원시 리뷰로 시작하겠습니다:

A screenshot of a Trustpilot business profile in the Electronics & Technology category, showing a poor rating of 2.3 stars from 873 reviews, with options to write a review or visit the website. The profile is claimed, and there is a note stating companies cannot offer incentives for hiding reviews.

이를 다음과 같은 구조화된 질문-답변 쌍으로 변환합니다:

A dataset table displaying rows with three columns: 'id', 'question', and 'answer'. The 'question' column contains inquiries related to HubSpot's customer support and satisfaction, while the 'answer' column provides insights based on customer reviews.

이 데이터셋을 통해 Gemma 3은 고객 피드백에서 인사이트를 추출하고, 감정 패턴을 식별하며, 실행 가능한 권장 사항을 제공하는 방법을 학습하게 됩니다.

설정 단계

#1 라이브러리 설치: 프로젝트 환경을 열고 requirements.txt 파일에 나열된 모든 필수 Python 라이브러리를 설치하세요. 터미널이나 노트북에서 다음 명령어를 실행하면 됩니다:

pip install -r requirements.txt

#2 환경 변수 구성: 프로젝트 루트 디렉터리에 .env 파일을 생성하고 API 키를 안전하게 저장하세요.

OPENAI_API_KEY="your_openai_key_here"
HF_TOKEN="your_hugging_face_token_here"

1단계: Bright Data를 통한 데이터 수집

가장 중요한 첫 단계는 데이터 소싱입니다. 파인 튜닝 데이터셋을 구축하기 위해 Trustpilot에서 원시 리뷰 데이터를 수집할 것입니다. Trustpilot의 강력한 봇 방지 조치로 인해 Bright Data의 Trustpilot Scraper API를 사용할 것입니다. 이 API는 IP 로테이션, CAPTCHA 해결, 동적 콘텐츠 처리를 효과적으로 관리하여 복잡한 스크래핑 솔루션 구축 없이도 대규모로 구조화된 리뷰를 효율적으로 수집할 수 있게 합니다.

Bright Data API를 사용하여 리뷰를 단계별로 수집하는 방법을 보여주는 Python 스크립트입니다:

import time
import json
import requests
from typing import Optional

# --- 구성 ---
API_KEY = "YOUR_API_KEY"  # 본인의 Bright Data API 키로 대체
DATASET_ID = "gd_lm5zmhwd2sni130p"  # 본인의 데이터셋 ID로 대체
TARGET_URL = "https://www.trustpilot.com/review/hubspot.com"  # 대상 회사 페이지
OUTPUT_FILE = "trustpilot_reviews.json"  # 출력 파일 이름
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
TIMEOUT = 30  # 요청 시간 초과(초)

# --- 함수 ---
def trigger_snapshot() -> Optional[str]:
    """Bright Data 스냅샷 수집 작업을 트리거합니다."""
    print(f"스냅샷 트리거: {TARGET_URL}")
    try:
        resp = requests.post(
            "https://api.brightdata.com/datasets/v3/trigger",
            headers=HEADERS,
            params={"dataset_id": DATASET_ID},
            json=[{"url": TARGET_URL}],
            timeout=TIMEOUT,
        )
        resp.raise_for_status()  # 잘못된 응답(4xx 또는 5xx) 시 HTTPError 발생
        snapshot_id = resp.json().get("snapshot_id")
        print(f"스냅샷 트리거 성공. ID: {snapshot_id}")
        return snapshot_id
    except requests.RequestException as e:
        print(f"스냅샷 트리거 오류: {e}")
    except json.JSONDecodeError:
        print(f"트리거 응답 디코딩 오류: {resp.text}")
    return None

def wait_for_snapshot(snapshot_id: str) -> Optional[list]:
    """스냅샷 데이터가 준비될 때까지 API를 폴링하고 반환합니다."""
    check_url = f"https://api.brightdata.com/datasets/v3/snapshot/{snapshot_id}"
    print(f"스냅샷 {snapshot_id} 완료 대기 중...")
    while True:
    try:
        resp = requests.get(
            check_url,
            headers=HEADERS,
            params={"format": "json"},
            timeout=TIMEOUT,
        )
        resp.raise_for_status()
        # 응답이 최종 데이터(list)인지 상태 정보(dict)인지 확인
            if isinstance(resp.json(), list):
                print("스냅샷 데이터 준비 완료.")
                return resp.json()
            else:
                pass
        except requests.RequestException as e:
            print(f"스냅샷 상태 확인 중 오류: {e}")
            return None  # 오류 발생 시 폴링 중지
        except json.JSONDecodeError:
            print(f"스냅샷 상태 응답 디코딩 오류: {resp.text}")
            return None  # 오류 발생 시 폴링 중지

        print("데이터 준비 완료되지 않음. 30초 대기 중...")
        time.sleep(30)

def save_reviews(reviews: list, output_file: str) -> bool:
    """수집된 리뷰 목록을 JSON 파일로 저장합니다."""
    try:
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(reviews, f, indent=2, ensure_ascii=False)
        print(f"{len(reviews)}개의 리뷰를 {output_file}에 성공적으로 저장했습니다")
        return True
    except (IOError, TypeError) as e:
        print(f"리뷰를 파일에 저장하는 중 오류 발생: {e}")
        return False
    except Exception as e:
        print(f"저장 중 예기치 않은 오류 발생: {e}")
        return False

def main():
    """Trustpilot 리뷰 수집을 위한 메인 실행 흐름."""
    print("Trustpilot 리뷰 수집 프로세스 시작 중...")
    snapshot_id = trigger_snapshot()
    if not snapshot_id:
        print("스냅샷 트리거 실패. 종료합니다.")
        return

    reviews = wait_for_snapshot(snapshot_id)
    if not reviews:
    print("스냅샷에서 리뷰를 가져오는 데 실패했습니다. 종료합니다.")
    return

    if not save_reviews(reviews, OUTPUT_FILE):
        print("수집된 리뷰를 저장하는 데 실패했습니다.")
    else:
        print("리뷰 수집 프로세스가 완료되었습니다.")

if __name__ == "__main__":
    main()

이 스크립트는 다음 단계를 수행합니다:

  • 인증: API_KEY를 사용하여 Authorization 헤더를 통해 Bright Data API에 인증합니다.
  • 수집 트리거: 지정된 TARGET_URL (이 경우 HubSpot의 Trustpilot 페이지)에 대해 데이터 수집 ‘스냅샷’을 트리거하는 POST 요청을 전송합니다. 이는 사용자의 DATASET_ID와 연결됩니다.
  • 완료 대기: 반환된 snapshot_id 를 사용하여 API를 주기적으로 폴링하여 데이터 수집이 완료되었는지 확인합니다.
  • 데이터 가져오기: API가 데이터 준비 완료를 알리면, 스크립트가 JSON 형식의 리뷰 데이터를 가져옵니다.
  • 출력 저장: 수집된 리뷰 객체 목록을 구조화된 JSON 파일(trustpilot_reviews.json)로 저장합니다.

결과 JSON 파일의 각 리뷰는 다음과 같은 상세 정보를 제공합니다:

{
    "review_id": "680af52fb0bab688237f75c5",
    "review_date": "2026-04-25T04:36:31.000Z",
    "review_rating": 1,
    "review_title": "자동 갱신 취소 기능이 작동하지 않음",
    "review_content": "저는 Hubspot을 거의 3년간 사용했는데...",
    "reviewer_name": "Steven Barrett",
    "리뷰어 위치": "AU",
    "검증된 리뷰": false,
    "경험 날짜": "2026-04-19T00:00:00.000Z",
    // 간결함을 위해 생략된 추가 필드
}

LLM 훈련에 최적의 데이터를 찾는 방법을 저희 가이드에서 확인하세요: LLM 훈련 데이터의 주요 출처.

2단계: JSON을 마크다운으로 변환하기

원시 리뷰 데이터를 수집한 후, 다음 단계는 처리하기에 적합한 깔끔하고 가독성 있는 형식으로 변환하는 것입니다. 우리는 마크다운을 사용할 것입니다. 마크다운은 가벼운 일반 텍스트 구조를 제공하여 토큰화 과정에서 노이즈를 줄이고, 잠재적으로 미세 조정 성능을 향상시키며, 서로 다른 콘텐츠 섹션 간의 일관된 분리를 보장합니다.

변환을 수행하려면 다음 스크립트를 실행하세요 👉 convert-trustpilot-json-to-markdown.py

이 스크립트는 1단계 출력에서 JSON 데이터를 읽어 구조화된 요약과 개별 고객 리뷰를 포함한 마크다운 파일을 생성합니다.

다음은 마크다운 출력 구조의 예시입니다:

# HubSpot 리뷰 요약
[웹사이트 방문](https://www.hubspot.com/)
**종합 평점**: 2.3
**총 리뷰 수**: 873
**위치**: 미국
**업종**: 전자제품 및 기술

> HubSpot은 선도적인 성장 플랫폼입니다... 더 나은 성장을 위해.
---

### 스티븐 배럿(호주)의 리뷰
- **게시일**: 2026년 4월 25일
- **경험 날짜**: 2026년 4월 19일
- **평점**: 1
- **제목**: *자동 갱신 취소 기능이 작동하지 않음*

저는 허브스팟을 거의 3년간 사용했습니다... 피하세요.

[전체 리뷰 보기](https://www.trustpilot.com/reviews/680af52fb0bab688237f75c5)

---

가이드에서 더 자세히 읽어보시면 AI 에이전트가 HTML보다 마크다운을 선호하는 이유를 알 수 있습니다.

3단계: 문서 분할 및 처리

마크다운 문서가 준비되면, 다음 중요한 단계는 이를 더 작고 관리하기 쉬운 조각으로 나누는 것입니다. 이는 대규모 언어 모델(LLM)이 입력 토큰 제한을 가지고 있으며, 적절한 길이의 예제로 튜닝하는 것이 가장 효과적이기 때문입니다. 또한, 이러한 조각들을 처리하면 모델에 대한 명확성과 일관성을 높일 수 있습니다.

LangChain의 RecursiveCharacterTextSplitter를 사용하여 마크다운 파일을 분할합니다. 이 방법은 구분자 목록을 기반으로 텍스트를 재귀적으로 분할하여 관련 텍스트 조각을 함께 유지하는 데 도움이 됩니다. 분할 지점을 가로지르는 맥락을 보존하기 위해 연속된 청크 사이에 중첩을 적용합니다. 이 프로세스에는 1,024자 크기의 청크와 256자 중첩을 사용합니다.

분할 후 각 청크는 선택적으로 LLM(예: GPT-4o)에 전달되어 리뷰 텍스트의 원래 의미를 엄격히 유지하면서 전체적인 명확성과 일관성을 개선합니다. 이 향상 단계는 후속 미세 조정 과정을 위해 각 청크 내 데이터 구조와 내용을 최적으로 명확하게 만드는 것을 목표로 합니다.

처리된 각 청크에는 고유 식별자가 할당되고 JSON Lines(.jsonl) 파일 형식으로 저장되어 파이프라인의 다음 단계를 준비합니다.

명확도 향상을 위해 LLM을 활용하는 Python 함수는 다음과 같습니다:

def improve_review_chunk(text: str, client: OpenAI, model: str = "gpt-4o") -> str:
    prompt = """이 리뷰의 명확성을 유지하면서 의미를 보존하여 개선하세요:
{text}

추가 설명 없이 개선된 텍스트만 반환하세요."""
    response = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": prompt},
                {"role": "user", "content": text}
            ]
        )
    return response.choices[0].message.content

이 단계의 전체 코드는 여기서 확인하세요 👉 split-markdown-into-chunks.py

출력은 JSON Lines 파일로, 각 라인은 고유 식별자와 잠재적으로 개선된 리뷰 콘텐츠를 가진 리뷰 청크를 나타냅니다:

[
  {
    "id": "f8a3b1c9-e4d5-4f6a-8b7c-2d9e0a1b3c4d", // 고유 챕터 ID
    "review": "# HubSpot 리뷰 요약nn[웹사이트 방문](https://www.hubspot.com/)...n---nn### Steven Barrett (AU)의 리뷰n- **게시일**: 2024년 4월 25일...n- **평점**: 1n- **제목**: *자동 갱신 취소 기능이 작동하지 않음*nn저는 Hubspot을 거의 3년간 사용했습니다... [텍스트 계속됨 - 개선 가능]" // 청크 콘텐츠 (정제 가능)
  },
  // ... 더 많은 청크 객체
]

4단계: QA 쌍 생성

최종 데이터 준비 단계에서는 처리된 리뷰 청크를 언어 모델 미세 조정에 적합한 구조화된 질문-답변(QA) 쌍으로 변환합니다. OpenAI의 GPT-4o를 사용하여 3단계에서 생성된 .jsonl 파일의 각 청크에 대해 하나의 QA 쌍을 생성합니다.

각 청크에 대해 스크립트는 신중하게 설계된 시스템 프롬프트를 사용하여 OpenAI API를 호출합니다:

SYSTEM_PROMPT = """
당신은 고객 리뷰를 통찰력 있는 질문-답변 쌍으로 변환하는 전문가입니다. 각 리뷰에 대해 정확히 1개의 고품질 QA 쌍을 생성하세요.

목적:
이 QA 쌍들은 고객 서비스 AI를 훈련시켜 HubSpot 제품에 대한 피드백 패턴을 이해하고 실행 가능한 통찰력을 식별하도록 할 것입니다.

질문 작성 지침:
- 질문은 일반적이고 유사한 상황에 적용 가능해야 합니다
- 이해관계자 관점에서 표현하십시오 (예: "어떤 기능 부족이 고객 불만을 유발하나요?")
- 제품 기능, 사용성, 가격 또는 서비스 영향에 집중하십시오

답변 작성 지침:
- 분석적 답변을 제공하십시오 (3~5문장)
- 인용문 없이 핵심 통찰력 추출
- 실행 가능한 권고사항 제시
- 객관성과 명확성 유지

서식 요구사항:
- "Q:"로 시작하여 질문 기재
- "A:"로 시작하여 일반 텍스트 답변 기재
"""

스크립트에는 일시적인 API 중단을 처리하고 안정적인 실행을 보장하기 위한 내장 속도 제한 및 재시도 메커니즘이 포함되어 있습니다. 전체 구현은 generate-qa-pairs.py에서 확인할 수 있습니다.

출력은 JSON 배열로 저장되며, 각 객체는 생성된 질문-답변 쌍을 포함하며 원본 청크 ID로 연결됩니다:

[
  {
    "id": "82d53a10-9f37-4d03-8d3b-38812e39ecdc",
    "question": "가격 정책 및 고객 지원 문제가 HubSpot의 고객 만족도와 유지율에 어떤 영향을 미칠 수 있나요?",
    "answer": "가격 관련 우려, 특히 고객이 사용 불가능하거나 지원되지 않는 서비스에 과도한 비용을 지불한다고 느낄 경우, 고객 만족도와 유지율에 상당한 영향을 미칠 수 있습니다..."
  }
  // ... 추가 QA 쌍
]

생성 후에는 결과 QA 데이터셋을 Hugging Face Hub에 업로드하는 것이 매우 권장됩니다. 이렇게 하면 미세 조정 및 공유가 용이해집니다. 게시된 데이터셋의 예시는 여기에서 확인할 수 있습니다: trustpilot-reviews-qa-dataset.

Unsloth를 활용한 Gemma 3 미세 조정: 단계별 안내

이제 맞춤형 Q&A 데이터셋을 준비했으니 Gemma 3 모델을 파인 튜닝해 보겠습니다. 표준 Hugging Face 구현 대비 LoRA/QLoRA 훈련 시 상당한 메모리 및 속도 향상을 제공하는 오픈소스 라이브러리 Unsloth를 사용할 것입니다. 이러한 최적화 덕분에 GPU에 충분한 VRAM이 있다면 단일 GPU 환경에서도 Gemma 3 같은 모델의 파인 튜닝이 더 용이해집니다.

Gemma 3 크기 필요한 VRAM 대략적 추정값* 적합한 플랫폼
4B ~15 GB 무료 Google Colab (T4), Kaggle (P100 16 GB)
12B ≥24 GB Colab Pro+ (A100/A10), RTX 4090, A40
27B 22–24 GB (4비트 QLoRA, 배치 크기 = 1); 그 외의 경우 ~40 GB A100 40GB, H100, 멀티 GPU 설정

참고: VRAM 요구량은 배치 크기, 시퀀스 길이 및 특정 양자화 기법에 따라 달라질 수 있습니다. 27B 모델의 요구 사항은 4비트 QLoRA 및 소규모 배치 크기(예: 1 또는 2)를 기준으로 합니다. 더 큰 배치 크기나 덜 공격적인 양자화는 상당히 더 많은 VRAM(~40GB+)이 필요합니다.

초보자의 경우 무료 Colab 노트북에서 4B 모델로 시작하는 것이 권장됩니다. Unsloth를 사용한 로딩, 훈련, 배포를 충분히 지원하기 때문입니다. 12B 또는 27B 모델로 업그레이드하는 것은 고사양 VRAM GPU 또는 유료 클라우드 계층에 접근 가능한 경우에만 고려해야 합니다.

Google Colab에서 런타임 유형을 변경하고 T4 GPU를 선택하려면 다음 단계를 따르세요:

  1. 상단의 런타임 메뉴를 클릭합니다.
  2. 런타임 유형 변경을 선택합니다.
  3. 표시되는 대화 상자에서 하드웨어 가속기 아래에서 GPU를 선택합니다.
  4. 저장을 클릭하여 변경 사항을 적용합니다.
Screenshot of a coding environment showing the option to change runtime type in a Python notebook, featuring various hardware accelerator options including T4 GPU and a loading progress indicator for model files on the left.

1단계: 환경 설정

먼저 필요한 라이브러리를 설치합니다. Colab 또는 Jupyter 환경에서는 코드 셀에서 직접 다음 명령어를 실행할 수 있습니다.

%%capture
!pip install --no-deps unsloth vllm
import sys, re, requests; modules = list(sys.modules.keys())
for x in modules: sys.modules.pop(x) if "PIL" in x or "google" in x else None
!pip install --no-deps bitsandbytes accelerate xformers==0.0.29.post3 peft "trl==0.15.2" triton cut_cross_entropy unsloth_zoo
!pip install sentencepiece protobuf datasets huggingface_hub hf_transfer

# vLLM 요구사항 - vLLM은 numpy 재설치로 인해 Colab을 중단시킵니다
f = requests.get("https://raw.githubusercontent.com/vllm-project/vllm/refs/heads/main/requirements/common.txt").content
with open("vllm_requirements.txt", "wb") as file:
    file.write(re.sub(rb"(transformers|numpy|xformers)[^n]{1,}n", b"", f))
!pip install -r vllm_requirements.txt

설치된 주요 패키지에 대한 간략한 설명:

  • unsloth: 융합 커널(fused kernels) 같은 기법을 활용해 더 빠르고 메모리 효율적인 LLM 훈련 및 로딩을 위한 핵심 최적화 기능을 제공합니다.
  • peft: 파라미터 효율적 미세 조정(LoRA 등) 방법론. 전체 모델 대신 소수의 추가 파라미터만 훈련할 수 있게 합니다.
  • trl: 트랜스포머 강화 학습(Transformer Reinforcement Learning). 지도 학습 기반 미세 조정을 단순화하는 SFTTrainer를 포함합니다.
  • bitsandbytes: k-bit(4비트 및 8비트) 양자화를 지원하여 모델의 메모리 사용량을 획기적으로 줄입니다.
  • accelerate: 다양한 하드웨어 구성(단일 GPU, 다중 GPU 등)에서 PyTorch 훈련을 원활하게 실행하는 Hugging Face 라이브러리.
  • datasets: 데이터셋을 효율적으로 로드, 처리 및 관리하기 위한 Hugging Face 라이브러리입니다.
  • transformers: 사전 훈련된 모델, 토큰화기 및 유틸리티를 위한 Hugging Face의 핵심 라이브러리입니다.
  • huggingface_hub: Hugging Face Hub와 상호작용하기 위한 유틸리티(로그인, 다운로드, 업로드).
  • vllm (선택 사항): 빠른 LLM 추론 엔진. 배포 시 필요하면 별도로 설치할 수 있습니다.

2단계: Hugging Face 인증

모델을 다운로드하고 나중에 미세 조정된 결과를 업로드하려면 환경에서 Hugging Face Hub에 로그인해야 합니다.

import os
from huggingface_hub import login
from google.colab import userdata

hf_token = userdata.get('HF_TOKEN')
if not hf_token:
    raise ValueError("실행 전에 HF_TOKEN 환경 변수를 설정하세요.")

try:
    login(hf_token)
    print("Hugging Face Hub에 성공적으로 로그인했습니다.")
except Exception as e:
    print(f"Hugging Face Hub 로그인 오류: {e}")

Google Colab에서 Hugging Face 토큰을 관리하는 가장 안전한 방법은 “Secrets” 탭을 사용하는 것입니다:

A screenshot from Google Colab showing the 'Secrets' section where users can configure environment variables. It displays options to add a new secret, with a highlighted 'HF_TOKEN' name and its respective value obscured. Instructions on how to access the secrets in Python are provided below.

3단계: 모델 및 토큰화기 로드

파인 튜닝을 시작하기 위해 Unsloth의 FastModel을 사용하여 Gemma 3 명령어 튜닝 모델을 효율적으로 로드하겠습니다. 이 예제에서는 Unsloth가 일반적인 Colab GPU의 메모리 제약 조건에 맞도록 최적화한 4비트 양자화 버전인 unsloth/gemma-3-4b-it 모델을 사용합니다.

Hugging Face에서 Unsloth의 Gemma 3 컬렉션을 확인해 보세요. GGUF, 4비트, 16비트 형식으로 제공되는 1B, 4B, 12B, 27B 크기의 모델이 포함되어 있습니다.

from unsloth import FastModel
from unsloth.chat_templates import get_chat_template
import torch # CUDA 확인을 위해 torch 임포트

# CUDA 사용 가능 여부 확인
if not torch.cuda.is_available():
    raise RuntimeError("CUDA가 사용 불가능합니다. 이 튜토리얼에는 GPU가 필요합니다.")
print(f"CUDA 사용 가능: {torch.cuda.is_available()}")
print(f"CUDA 장치 이름: {torch.cuda.get_device_name(0)}")

model, tokenizer = FastModel.from_pretrained(
    model_name="unsloth/gemma-3-4b-it", # Unsloth에서 최적화한 4B 명령어 튜닝 모델 사용
    max_seq_length=2048, # 최대 컨텍스트 길이 설정
    load_in_4bit=True,   # 4비트 양자화 활성화
    full_finetuning=False, # PEFT(LoRA) 사용
    token=hf_token,      # Hugging Face 토큰 전달)


# Gemma 3에 맞는 채팅 템플릿 적용
tokenizer = get_chat_template(tokenizer, chat_template="gemma-3")

print("모델 및 토큰라이저 로드 성공.")

이 코드에서 일어나는 일:

  • FastModel.from_pretrained(): Unsloth의 최적화된 모델 로더.
  • model_name="unsloth/gemma-3-4b-it": 로드할 모델 변형을 지정합니다. Unsloth에서 사전 최적화된 4B 명령어 튜닝(it) 버전을 선택합니다.
  • max_seq_length=2048: 모델이 한 번에 처리할 수 있는 최대 토큰 수를 설정합니다. 데이터 청크 길이와 원하는 컨텍스트 창에 따라 이 값을 조정하여 메모리 사용량과 긴 입력 처리 능력 사이의 균형을 맞추세요.
  • load_in_4bit=True: 제한된 VRAM 환경에서 훈련 시 필수 설정입니다. bitsandbytes를 사용하여 모델 가중치를 4비트 정밀도로 로드합니다.
  • full_finetuning=False: Unsloth에 PEFT/LoRA 미세 조정을 위한 모델 준비를 지시합니다. 즉, 모든 모델 매개변수가 아닌 어댑터 레이어만 훈련됩니다.
  • get_chat_template(tokenizer, chat_template="gemma-3"): 토큰화기를 감싸 프롬프트를 Gemma 3이 기대하는 채팅 형식(<start_of_turn>사용자명...n<end_of_turn><start_of_turn>모델명...n<end_of_turn>)으로 자동 포맷팅합니다. 이는 지시 사항을 따르는 모델을 올바르게 미세 조정하고 모델이 예상되는 대화 순서에 따라 응답을 생성하도록 학습시키는 데 매우 중요합니다.

4단계: 훈련용 데이터셋 로드 및 준비

이전에 Hugging Face Hub에 업로드한 데이터셋을 로드한 다음 토큰화기 및 트레이너가 기대하는 채팅 기반 형식으로 변환합니다.

from datasets import load_dataset
from unsloth.chat_templates import standardize_data_formats, train_on_responses_only # train_on_responses_only는 이전에 임포트됨

# 1. Hugging Face Hub에서 데이터셋 로드
dataset_name = "triposatt/trustpilot-reviews-qa-dataset" # 자신의 데이터셋 이름으로 대체
dataset = load_dataset(dataset_name, split="train")

print(f"데이터셋 '{dataset_name}' 로드됨.")
print(dataset)

# 2. 비정상적인 형식 정규화 ( 'question' 및 'answer' 필드 존재 확인)
dataset = standardize_data_formats(dataset)
print("데이터셋 정규화 완료.")

# 3. 예제를 채팅 템플릿 형식으로 변환하는 함수 정의
def formatting_prompts_func(examples):
    """질문-답변 쌍을 Gemma 3 채팅 템플릿 형식으로 포맷팅합니다."""
    questions = examples["question"]
    answers = examples["answer"]
    texts = []
    for q, a in zip(questions, answers):
        # 대화를 역할과 내용의 리스트 구조로 구성
        conv = [
            {"role": "user", "content": q},
            {"role": "assistant", "content": a},
        ]
        # 채팅 템플릿 적용
        txt = tokenizer.apply_chat_template(
            conv,
            tokenize=False, # 토큰 ID가 아닌 문자열 반환
            add_generation_prompt=False # 모델 시작 태그를 아직 끝에 추가하지 않음
        )
        # Gemma 3 토큰화기는 기본적으로 <bos>를 추가하며, 트레이너가 이를 다시 추가함
        # 중복 <bos> 토큰을 피하기 위해 여기서 제거함
        txt = txt.removeprefix(tokenizer.bos_token)
        texts.append(txt)
    return {"text": texts}

# 4. 데이터셋에 포맷팅 함수 적용
dataset = dataset.map(formatting_prompts_func, batched=True, remove_columns=["question", "answer"])
print("채팅 템플릿으로 포맷팅된 데이터셋.")
print(dataset) # 새로 생성된 'text' 열 확인

이 코드에서:

  • load_dataset(): Hugging Face Hub에서 Q&A 데이터셋을 가져옵니다.
  • standardize_data_formats(): 서로 다른 데이터셋 간에 필드명이 일관되도록 보장합니다. 이 경우 특히 ‘question’과 ‘answer’를 찾습니다.
  • formatting_prompts_func(): 이 핵심 함수는 Q&A 쌍을 배치 단위로 처리합니다. tokenizer.apply_chat_template() 메서드를 사용하여 Gemma 3 명령어 미세 조정에 적합한 형식으로 문자열을 변환합니다. 이 형식에는 모델이 대화 구조를 이해하는 데 필수적인 <start_of_turn>usern<start_of_turn>modeln과 같은 특수 턴 토큰이 포함됩니다. SFTTrainer가 자체 <bos> 토큰을 추가하므로 초기 <bos> 토큰은 제거합니다.
  • dataset.map(...): formatting_prompts_func를 전체 데이터셋에 효율적으로 적용하여, 포맷팅된 문자열을 포함하는 새로운 ‘text’ 열을 생성하고 원본 열을 제거합니다.

5단계: LoRA 및 트레이너 구성

이제 trl 라이브러리에서 PEFT(LoRA) 설정과 SFTTrainer를 구성합니다. LoRA는 사전 훈련된 모델의 핵심 레이어에 작고 훈련 가능한 행렬을 주입하는 방식으로 작동합니다. 미세 조정 시 이 작은 어댑터 행렬만 업데이트되므로 훈련해야 할 매개변수 수가 크게 줄어들어 메모리 사용량을 최소화합니다.

from trl import SFTTrainer, SFTConfig
import torch

# 1. LoRA 구성
model = FastModel.get_peft_model(
    model,
    r=8, # LoRA 순위(일반적인 값) - 낮은 순위는 매개변수 수가 적고, 높은 순위는 표현력이 더 큼
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj", # 어텐션 레이어
        "gate_proj", "up_proj", "down_proj"      # MLP 레이어
    ],
    # 언어 레이어를 미세 조정하려면 True로 설정 (텍스트 작업 권장)
    # 및 어텐션/MLP 모듈(LoRA 적용 대상)
    finetune_language_layers=True,
    finetune_attention_modules=True,
    finetune_mlp_modules=True,
    # finetune_vision_layers=False, # 다중 모달 모델(12B/27B)에만 해당
    lora_alpha=8, # LoRA 스케일링 계수 (보통 r과 동일하게 설정)
    lora_dropout=0, # LoRA 레이어용 드롭아웃
    bias="none", # 바이어스 항 훈련 안 함
    use_gradient_checkpointing="unsloth", # 메모리 최적화
    random_state=1000, # 재현성을 위한 시드
    use_rslora=False, # 순위 안정화 LoRA (선택적 대체)
    # modules_to_save=["embed_tokens", "lm_head"], # 선택적: 임베딩/출력 레이어 훈련)


print("PEFT (LoRA)용 모델 구성 완료.")

# 2. SFTTrainer 구성
# 데이터셋 크기와 에포크 수를 기반으로 적절한 max_steps 결정
# 데모용으로 적은 단계 수 사용 (예: 30)
# 실제 사용 시 단계 수 = (데이터셋_크기 / 배치_크기 / 기울기_누적) * 에포크_수 계산
dataset_size = len(데이터셋)
per_device_train_batch_size = 2 # GPU VRAM에 따라 조정
gradient_accumulation_steps = 4 # 더 큰 배치 크기 시뮬레이션을 위한 기울기 누적 (batch_size * grad_accum = 8)
num_train_epochs = 3 # 예시: 3 에포크

# 총 훈련 단계 계산
total_steps = int((dataset_size / per_device_train_batch_size / gradient_accumulation_steps) * num_train_epochs)
# 데이터셋이 작거나 계산 결과가 1단계 미만일 경우 max_steps가 0이 아니도록 보장
max_steps = max(30, total_steps) # 최소값 설정 또는 적절히 계산

print(f"{num_train_epochs} 에포크에 대한 총 훈련 단계 계산: {total_steps}. max_steps={max_steps} 사용")

sft_config = SFTConfig(
    dataset_text_field="text", # 데이터셋 내 형식화된 채팅 텍스트를 포함하는 열
    per_device_train_batch_size=per_device_train_batch_size,
    gradient_accumulation_steps=gradient_accumulation_steps,
    warmup_steps=max(5, int(max_steps * 0.03)), # 초기 몇 단계에 대한 워밍업 (예: 전체 단계의 3%)
    max_steps=max_steps, # 총 훈련 단계 수
    learning_rate=2e-4, # 학습률
    logging_steps=max(1, int(max_steps * 0.01)), # 총 단계의 1%마다 기록 (최소 1)
    optim="adamw_8bit", # 8비트 AdamW 최적화기 (메모리 효율적)
    weight_decay=0.01, # L2 정규화
    lr_scheduler_type="linear", # 선형 학습률 감쇠
    seed=3407, # 난수 시드
    report_to="none", # 필요하지 않은 경우 W&B 같은 플랫폼 보고 비활성화
    output_dir="./results", # 체크포인트 및 로그 저장 디렉토리
)

# 3. SFTTrainer 인스턴스 생성
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    eval_dataset=None, # 선택사항: 검증 데이터셋 제공
    args=sft_config,)


print("SFTTrainer 생성 완료.")

# 4. 훈련을 위한 입력 부분 가리기
# 이로써 모델은 어시스턴트의 응답만 생성하도록 학습됩니다
# 모델이 사용자의 프롬프트를 단순히 복사하는 것을 방지합니다
# 채팅 템플릿에서 지시문 및 응답 턴의 리터럴 접두사를 전달합니다
trainer = train_on_responses_only(
    trainer,
    instruction_part="<start_of_turn>usern", # 사용자 콘텐츠 앞 리터럴 문자열
    response_part="<start_of_turn>modeln",  # 모델 콘텐츠 앞의 리터럴 문자열)


print("응답만으로 훈련하도록 트레이너 구성됨.")

이 코드에서:

  • FastModel.get_peft_model(): 로드된 모델을 지정된 매개변수로 LoRA 미세조정에 맞게 구성합니다. r은 어댑터 행렬의 크기를 제어하는 LoRA 순위입니다. target_modules는 어댑터를 적용할 모델 레이어(어텐션, MLP 투영 등)를 지정합니다. lora_alpha는 스케일링 계수입니다. use_gradient_checkpointing은 Unsloth가 제공하는 메모리 절약 기술입니다.
  • SFTConfig(): SFTTrainer의 훈련 하이퍼파라미터를 정의합니다. per_device_train_batch_size와 gradient_accumulation_steps는 함께 작동하여 기울기 계산에 사용되는 유효 배치 크기를 결정합니다. max_steps는 총 훈련 반복 횟수를 설정합니다. learning_rate, optim, weight_decaylr_scheduler_type은 최적화 과정을 제어합니다. dataset_text_field는 트레이너에게 데이터셋의 어느 열에 형식화된 훈련 예제가 포함되어 있는지 알려줍니다.
  • SFTTrainer(): 트레이너를 인스턴스화하여 LoRA로 구성된 모델, 준비된 데이터셋, 토큰화기, SFTConfig에서 정의된 훈련 인수를 통합합니다.
  • train_on_responses_only(): 트레이너의 손실 계산을 수정하는 유틸리티 함수( trl의 일부이며 Unsloth와 호환됨)입니다. 손실은 모델의 예상 응답(<start_of_turn>modeln...)에 해당하는 토큰에 대해서만 계산되도록 설정하며, 사용자의 프롬프트(<start_of_turn>usern...) 토큰은 무시합니다. 이는 모델이 입력 프롬프트를 단순히 반복하거나 완성하는 대신 관련성 있는 답변을 생성하도록 가르치는 데 필수적입니다. 이러한 섹션을 구분하기 위해 채팅 템플릿에서 사용되는 정확한 문자열 접두사를 제공합니다.

6단계: 모델 훈련

모든 설정이 완료되면 미세 조정 프로세스를 시작할 수 있습니다. trainer.train() 메서드는 SFTConfig에 제공된 구성에 따라 훈련 루프를 처리합니다.

# 선택 사항: 훈련 전 CUDA 캐시 지우기
torch.cuda.empty_cache()

print("훈련 시작...")
# 효율성을 위해 혼합 정밀도 훈련 사용
# Unsloth는 GPU 성능과 모델에 따라 자동으로 float16/bf16을 처리합니다
with torch.amp.autocast(device_type="cuda", dtype=torch.float16): # 지원 시 torch.bfloat16 사용
     trainer.train()

print("훈련 완료.")

트레이너는 훈련 손실 등 진행 상황을 출력합니다. 단계별로 손실이 감소하는 것을 관찰할 수 있으며, 이는 모델이 데이터로부터 학습하고 있음을 나타냅니다. 총 훈련 시간은 데이터셋 크기, 모델 크기, 하이퍼파라미터 및 사용된 특정 GPU에 따라 달라집니다. 본 예제의 데이터셋과 T4 GPU에서 실행되는 4B 모델의 경우, 200단계 훈련은 비교적 빠르게 완료될 것입니다(예: 정확한 설정 및 데이터 길이에 따라 15~30분 미만).

7단계: 미세 조정된 모델 테스트(추론)

훈련 후, 미세 조정된 모델이 훈련에 사용된 Trustpilot 리뷰 데이터 기반 질문에 얼마나 잘 응답하는지 테스트해 보겠습니다. 보다 대화형 출력을 위해 TextStreamer와 함께 model.generate 메서드를 사용할 것입니다.

from transformers import TextStreamer

# 데이터셋 내용과 관련된 테스트 질문 정의
questions = [
    "리뷰에서 언급된 일반적인 문제점이나 불만 사항은 무엇인가요?",
    "고객이 제품/서비스에서 가장 좋아하는 점은 무엇인가요?",
    "고객 지원은 어떻게 평가되나요?",
    "가격이나 가치와 관련하여 반복적으로 언급되는 주제가 있나요?"
    # 데이터셋 내용에 따라 여기에 질문 추가
]

# 실시간 출력을 위한 스트리머 설정
# skip_prompt=True는 입력 프롬프트 재출력을 방지
# skip_special_tokens=True는 출력에서 채팅 템플릿 토큰 제거
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)

print("n--- 미세 조정 모델 테스트 중 ---")

# 질문을 반복하며 답변 생성
for idx, q in enumerate(questions, start=1):
    # Gemma 3 채팅 형식에 맞는 대화 프롬프트 구성
    conv = [{"role": "user", "content": q}]

    # 채팅 템플릿 적용 및 생성 프롬프트 토큰 추가
    # add_generation_prompt=True는 <start_of_turn>model 태그 포함
    prompt = tokenizer.apply_chat_template(
        conv,
        add_generation_prompt=True,
        tokenize=False
    )

    # 프롬프트 토큰화 후 GPU로 이동
    inputs = tokenizer([prompt], return_tensors="pt", padding=True).to("cuda")

    # 질문 표시
    print(f"n=== 질문 {idx}: {q}n")

    # 스트리밍으로 응답 생성
    # 토큰화된 입력값을 모델 생성기에 직접 전달
    _ = model.generate(
        **inputs,
        streamer=streamer, # 토큰 단위 출력을 위한 스트리머 사용
        max_new_tokens=256, # 응답 길이 제한
        temperature=0.7, # 무작위성 제어 (낮을수록 결정론적)
        top_p=0.95, # 핵 샘플링
        top_k=64, # 상위 k개 샘플링
        use_cache=True, # 캐시 사용으로 생성 속도 향상
        # 필요 시 중지 조건 추가, 예: <end_of_turn> 이후 중지
        # eos_token_id=tokenizer.eos_token_id,
    )
    # 각 답변 뒤에 구분자 추가
    print("n" + "="*40)

print("n--- 테스트 완료 ---")

아래 이미지에서 모델의 응답을 확인하세요:

Text discussing customer reviews, highlighting common complaints about communication delays and product quality issues, positive feedback on HubSpot's user-friendly design and CRM capabilities, customer support perception as friendly and efficient, and themes regarding value for money in pricing strategies.

🔥 훌륭합니다, 잘 작동하고 있어요!

성공적인 파인 튜닝 과정은 모델이 일반적인 응답이 아닌, 파인 튜닝된 리뷰 콘텐츠에서 직접 도출된 분석적인 답변을 생성함을 의미합니다. 이는 사용자 지정 데이터셋에 존재하는 스타일과 통찰력을 반영합니다.

8단계: 파인튜닝된 모델 저장 및 푸시

마지막으로, 파인튜닝된 LoRA 어댑터와 토큰라이저를 저장하세요. 로컬에 저장하거나 Hugging Face Hub에 푸시하여 간편한 공유, 버전 관리 및 배포가 가능합니다.

# 로컬 경로 및 허브 저장소 ID 정의
new_model_local = "gemma-3-4b-trustpilot-qa-adapter" # 로컬 디렉토리 이름
new_model_online = "YOUR_HF_USERNAME/gemma-3-4b-trustpilot-qa" # 허브 저장소 이름

# 1. 로컬 저장
print(f"모델 어댑터와 토큰라이저를 '{new_model_local}'에 로컬 저장 중...")
model.save_pretrained(new_model_local)
tokenizer.save_pretrained(new_model_local)
print("로컬에 저장됨.")

# 2. Hugging Face Hub에 푸시
print(f"모델 어댑터와 토큰라이저를 Hugging Face Hub '{new_model_online}'에 푸시 중...")
model.push_to_hub(new_model_online, token=hf_token)
tokenizer.push_to_hub(new_model_online, token=hf_token)

이제 튜닝된 모델을 Hugging Face Hub에서 이용할 수 있습니다:

Screenshot of the Hugging Face model card for 'triposatt/gemma-3-4b-trustpilot-qa', displaying the model's details including developer, license, and information about the finetuning process with Unsloath and Hugging Face's TRL library.

결론

이 가이드는 고객 리뷰로부터 분석적 답변을 생성하는 실용적인 사용 사례를 위해 Google의 Gemma 3을 미세 조정하는 엔드투엔드 접근법을 시연했습니다. Bright Data의 웹 스크레이퍼 API를 통해 고품질의 도메인 특화 데이터를 수집하고, LLM 기반 처리로 QA 형식으로 구조화한 후, 리소스 제약 하드웨어에서 Unsloth 라이브러리를 활용해 Gemma 3 4B 모델을 효율적으로 미세 조정하는 전체 워크플로를 다루었습니다.

그 결과, 원시 리뷰 데이터에서 인사이트를 추출하고 감정을 해석하여 구조화된 실행 가능한 답변으로 변환하는 데 능숙한 특화형 LLM이 탄생했습니다. 이 방법은 매우 유연하게 적용 가능합니다. 동일한 워크플로를 활용해 다양한 도메인별 데이터셋에 Gemma 3(또는 기타 적합한 LLM)을 미세 조정함으로써 각기 다른 요구에 맞춘 AI 어시스턴트를 구축할 수 있습니다.

AI 기반 데이터 추출 전략에 대한 심층 탐구를 위해 다음 추가 자료를 참고하십시오:

Unsloth를 활용한 더 정교한 튜닝 최적화 및 예제는 Unsloth 노트북 컬렉션에서 확인하세요.