SFT의 정의와 목적:
- 정의: SFT는 사전 학습(Pre-training) 이후에 LLM(대형 언어 모델)의 성능을 개선하기 위해 사용되는 기법입니다.
- 목적:
- 모델이 특정 채팅 형식을 이해하고 따르도록 학습시켜 대화형 에이전트로 변환시키는 것
- 모델의 일반적인 언어 이해 능력을 특정 작업(예: 요약, 번역)이나 전문 도메인(예: 의료, 법률)에서의 실용성으로 연결시키는 것
- 사전 학습으로 얻은 폭넓은 지식을 실제 사용 목적에 맞게 세밀하게 조정하여, 모델이 주어진 입력에 대해 보다 관련성 높고 일관된 응답을 생성하도록 만듭니다
- 이를 통해 모델은 일반적인 언어 처리 능력을 넘어 특정 과제나 도메인에 최적화된 성능을 발휘할 수 있게 됩니다
여기서 다루는 내용:
- 고품질의 instruction 데이터셋을 만드는 방법
- SFT에 사용되는 다양한 기법들
- 실제로 fine-tuning을 구현하는 방법
instruction dataset을 만드는 과정:
- SFT 의 도전 과제:
- 대부분의 경우, 데이터의 원천은 일반 텍스트입니다. 하지만 자연스러운 ‘instruction과 그에 맞는 답변’ 쌍은 드물기 때문에, 원시 텍스트를 두 요소(명령문과 답변)를 모두 포함하는 형식으로 변환해야 합니다.
- 데이터 품질 중요성:
- 데이터가 정확하고 유용해야 하기 때문에, 각 샘플을 수동으로 확인하고 검증하는 데 많은 시간이 소요됩니다. 이러한 꼼꼼한 검증 작업은 모델 훈련에 사용할 데이터셋의 질을 높이는 데 필수적입니다.
- 데이터 생성 파이프라인:
- 전체 파이프라인은 데이터 생성이라는 큰 틀에서 동작하는거임.
- (1) Data curation (데이터 선별 및 관리):
- 여기서 데이터 증강도 포함됨.
- 대량의 원시 데이터에서 도움이 되는 데이터를 찾는 것 (불필요한 데이터는 제거)
- 데이터를 일관된 형식으로 변환하는 것.
- (2) Data deduplication (중복 제거)
- (3) Data decontamination (오염 데이터 제거)
- 데이터 품질을 위해 노이즈가 있는 데이터, 잘못된 형식의 데이터, 검증을 위한 데이터들이 있는지 검사하는 것.
- (4) Data quality evaluation (데이터 품질 평가)
- 고품질의 데이터만 남기도록 하는 것.
- (5) Data exploration (데이터 탐색)
- 데이터를 분석하고 이해하는 과정임
- 데이터의 특성과 분포 파악 등
instruction dataset 구축을 위한 일반적인 프레임워크:
- Instruction data 란?
- 일반적으로 Instruction dataset은 주로 instruction과 answer의 쌍으로 이루어집니다
- Instruction: 모델에 제공되는 입력으로, fine-tuning 시 컨텍스트 역할을 합니다.
- Answer: 모델이 생성해야 하는 기대 출력입니다.
- 추가 필드:
- 일부 템플릿(예: Alpaca)에서는 instruction 외에도 추가 필드인 inputs와 system을 포함합니다.
- inputs: 모델이 instruction을 수행하는 데 필요한 추가 데이터 제공
- system: 모델의 일반적인 행동을 제어하기 위한 메타 프롬프트 역할, 예를 들어 “도움이 되는 어시스턴트”처럼 특정 행동을 유도합니다.
- 일반적으로 Instruction dataset은 주로 instruction과 answer의 쌍으로 이루어집니다
- 고품질 데이터 구축을 위한 기준:
- 정확성(Accuracy):
- 데이터 샘플이 사실과 일치하며, instruction에 적절하게 대응하는지 확인합니다.
- 다양성(Diversity):
- 데이터셋이 다양한 주제, 상황, 텍스트 길이, 문체를 포괄하여, 모델이 여러 상황에 대응할 수 있도록 합니다.
- 복잡성(Complexity):
- 단순한 과제보다는 다단계 추론이나 복잡한 문제 해결을 요구하는 샘플을 포함시켜, 모델의 능력을 한 단계 높입니다.
- 정확성(Accuracy):
데이터의 양(quantity):
- 오픈소스 데이터셋을 활용하는 법:
- Hugging Face Hub에는 일반 목적용 데이터셋뿐만 아니라 특정 작업이나 도메인에 맞춘 다양한 instruction 데이터셋이 있습니다.
- 새로운 사용 사례를 진행할 때 관련된 오픈소스 데이터셋을 참고하거나 활용하는 것이 유리합니다.
- 만약 샘플 수가 너무 적다면(예: 1,000개 미만), 고품질 데이터를 추가하여 데이터 양을 보완할 필요가 있습니다.
- 모델 크기와 데이터 양의 관계
- 이상적인 샘플 수를 결정하는 것은 쉽지 않은 문제입니다. 데이터 품질과 모델 크기에 따라 크게 달라집니다.
- 예를 들어, 대형 모델(약 70억 파라미터 이상)은 고품질 데이터 1,000개 정도로도 fine-tuning이 가능할 수 있습니다.
- 반면, 소형 모델(약 7억 파라미터)은 올바른 채팅 템플릿을 학습하기 위해 더 많은 샘플이 필요합니다.
- 어쨌든 데이터의 품질은 항상 중요하며, 가능한 많은 고품질 샘플이 바람직합니다.
- 일반 목적 모델 vs. 특정 작업/도메인 모델
- 일반 목적 모델:
- GPT와 같은 광범위한 능력을 재현하기 위해 다양한 주제와 상황을 커버해야 하므로, 더 많은 샘플이 필요합니다.
- 예시로, 01-ai의 Yi 모델은 10,000개 미만의 샘플을 사용한 반면, Meta는 Llama 3의 fine-tuning 전 과정에서 1,000만 개의 샘플을 사용했다고 보고했습니다.
- 오픈소스 커뮤니티에서는 OpenHermes나 Dolphin 같은 모델들이 약 100만 개의 샘플을 사용합니다.
- 따라서, 좋은 일반 목적의 instruct 모델을 위해서는 최소 100만 개의 instruction 샘플을 권장합니다.
- 특정 작업(task-specific) 모델:
- 번역, 요약, 감정 분석 등 하나의 기능에 특화되어 있으므로, 상대적으로 적은 양의 데이터(100 ~ 100,000개 정도)로도 충분히 학습할 수 있습니다.
- 도메인(domain-specific) 모델:
- 특정 분야(의학, 법률, 금융, 전자상거래 등)에 맞춰 LLM의 지식을 미세 조정합니다.
- 도메인의 특성과 사전학습 데이터에서 해당 도메인이 얼마나 대표되는지에 따라 필요한 데이터 양이 크게 달라집니다.
- 예를 들어, 의학이나 법률처럼 방대한 전문 용어와 지식을 다루는 경우는 일반 목적 fine-tuning 만큼의 데이터가 필요할 수 있고, 일부 도메인은 task-specific 모델 수준의 데이터로도 충분할 수 있습니다.
- 일반 목적 모델:
파인튜닝(fine-tuning) 의 한계점:
- 파인튜닝은 모델의 기본 지식을 크게 변경하기보다는, 그 지식을 특정 도메인에 맞게 재조정하는 역할을 합니다.
- 사전 학습은 광범위한 지식을 습득하고 언어의 사용법을 이해하는게 목적이고, 파인튜닝은 언어적인 능력과 논리적인 능력으로 전문성을 쌓아나가는 거임.
- 파인튜닝의 한계:
- 파인튜닝은 모델의 “지식” 자체를 크게 변경하는 것은 아님. 기존에 지식을 조정.
- 특정 도메인에 대해 그 지식을 어떻게 활용할지, 그리고 그 지식의 세부사항들을 얼마나 효과적으로 전달할지를 향상시키는 데 중요한 역할을 함.
- 도메인 용어의 이해와 문맥 이해 -> 이를 바탕으로 사용자가 원하는 응답을 전달하는 것 가능.
- 새로운 지식 습득 보다는 기존 지식을 도메인 분야에 맞게 다듬는 걸 말함.
- 도메인 분야의 문제 해결을 위한 작업 수행은 가능.
- 사전학습 때 아예 포함되지 않았던 도메인 지식이라면 해당 도메인으로 파인튜닝하는 건 큰 의미 없을 수 있음:
- 만약 사전 학습 시점에 전혀 다뤄지지 않은 완전히 생소한 도메인(예: 방대한 양의 전문 지식이 필요한 매우 특수한 분야)이 있다면, 파인튜닝만으로 그 지식을 ‘무(無)에서 유(有)’로 만들어내기는 쉽지 않습니다.
- 즉, 사전에 언급조차 안 된 전혀 새로운 토픽을 오직 파인튜닝 데이터만으로 깊이 있게 가르치려면, 사실상 사전 학습 수준의 대규모·장기간 학습이 필요할 수 있습니다.
- 하지만 사전 학습 중 해당 도메인과 관련된 텍스트(심지어 얕은 수준이라도)가 어느 정도 포함되어 있었다면, 파인튜닝은 그 지식을 더 체계적으로, 전문적으로 다듬고 적용하도록 만드는 데 큰 도움이 됩니다.
- 반면 사전 학습에 '표면적 지식' 정도 (심층적인 전문 지식은 아니더라도)는 습득했다면 파인튜닝은 유의미할 수 있음:
- 조금이라도 해당 지식을 접해본 상태라면, 파인튜닝을 통해 유의미한 향상을 기대할 수 있습니다. -> 큰 틀에 대한 이해라도 하고 있다면 재조정하는데 도움을 줄거니까.
fine-tuning용 데이터를 어떻게 확보(큐레이션)하는지:
- Task-specific 모델과 Domain-specific 모델로 나누어 각각의 데이터 수집 전략을 소개하고, 필요에 따라 Few-shot prompting을 대안으로 고려할 수 있음을 설명합니다
- Task-specific 모델의 데이터 수집 전략:
- 무엇을 하는 모델인지 명확하게 정의 (예) 요약(summarization) 모델, 번역(translation) 모델, 감정 분석 모델 등)
- 기존에 오픈소스에 존재하는 데이터 셋을 이용
- 아니면 기존 데이터셋이 충분하지 않다면, 직접 문장을 작성하거나, 크라우드소싱 등으로 새 데이터를 생성
- Domain-specific 모델의 데이터 수집 전략:
- 특정 분야(도메인)에 대한 깊이 있는 지식이 필요 (예) 의학, 법률, 금융, 엔지니어링, e-commerce, 호스피탈리티 등)
- 전문가와 협업을 추천:
- 데이터 수집 경로, 고품질 데이터 판단, 작업 테스크에 대한 이해를 위해
- 전문가와의 협업이 어렵다면?
- 모델의 목표 정의가 먼저
- 어떤 데이터를 수집해야하는지, 왜 이 데이터가 중요한지에 대한 정의 -> 도메인 지식을 습득해야할 수도
- 준전문가를 섭외하려고 노력하는 것도 방법
- Few-shot prompting과의 비교:
- 파인튜닝을 진행하지 않고, 프롬프트 입력에 모델이 참고할 예시(샘플) 몇 개를 포함시켜 모델이 그 패턴을 따라가도록 하는 기법
- 별도의 긴 학습 과정 없이, 대규모 모델의 이미 학습된 능력을 활용, 적은 양의 샘플 예시로 새 작업 적응 가능하다는게 장점
- 지속적이고 깊이 있는 학습(예: 매우 전문적인 지식을 새로 학습하거나 복잡한 태스크)을 위해서는 결국 fine-tuning이 필요
Rule-based filtering(규칙 기반 필터링):
- 규칙 기반 필터링은 미리 정해진 명시적인 규칙들을 사용하여 데이터 샘플을 평가하고 걸러내는 체계적인 데이터 품질 관리 방법입니다.
- 주요 목표는 특정 기준에 부합하지 않는 샘플을 제거하여 전체 데이터셋의 품질을 높이는 것입니다:
- 필터링도 한번에 전부를 하는게 아니라 단계별로 걸러내는거임.
- 이 방법은 대량의 데이터에 대해 빠르게 적용할 수 있어 매우 확장성이 좋다는거.
- 주요 기법들:
- Length Filtering (길이 필터링):
- 데이터의 응답 길이에 대한 임계값을 설정하는 방법입니다.
- 너무 짧은 응답은 정보가 부족할 수 있고, 너무 긴 응답은 불필요한 내용이 포함될 수 있기 때문에, 태스크나 도메인에 맞게 적절한 길이를 정합니다.
- Keyword Exclusion (키워드 제외):
- 저품질 또는 부적절한 내용을 나타내는 키워드나 구절을 미리 정의한 후, 해당 키워드를 포함하는 샘플을 제거하는 방법입니다.
- 예를 들어, 비속어나 스팸 관련 단어, 혹은 도메인에 맞지 않는 비공식적인 표현 등을 제외할 수 있습니다.
- Format Checking (포맷 체크):
- 코드, JSON, 또는 다른 특정 형식을 가진 데이터셋의 경우, 모든 샘플이 예상된 포맷을 따르는지 확인합니다.
- 이를 통해 일관성을 유지하고, 이후 처리 과정(후처리, 파인튜닝 등)을 용이하게 만듭니다.
- Length Filtering (길이 필터링):
Data deduplication(데이터 중복 제거):
- 필요성:
- 편향된 성능 방지: 일부 데이터가 과도하게 많으면 모델이 특정 유형에 치우쳐 학습되어 성능이 편향될 수 있습니다.
- 중복 제거 기법:
- Exact Deduplication (정확한 중복 제거):
- 데이터 정규화: 텍스트를 소문자로 변환하거나 공백, 특수문자 등을 정리하여 형식을 통일합니다.
- 해시 생성: MD5, SHA-256 같은 해시 알고리즘을 사용하여 각 데이터 항목에 대한 고유 해시값을 생성합니다.
- 중복 제거: 동일한 해시값을 가진 항목들을 찾아내어, 중복된 항목 중 하나만 남깁니다.
- 해시로 비교하는 이유:
- Brute Force 는 O(n^2 * m) n: 문자열 길이 (모든 텍스트 길이가 n이라는 가정), m: 문서 개수
- 해시 비교 방법은 O(n * m) (O(n) 은 해시 테이블을 만드는데 사용됨)
- 해시 알고리즘의 일반적 작동 원리:
- 입력 문자열을 블록 단위(예: 512비트)로 가져옴 -> 비트 처리 -> 최종 결합 및 압축
- 모든 문자열을 다 읽기 때문에 O(n)
- Fuzzy Deduplication (유사 중복 제거)
- MinHash Deduplication:
- 개념: 데이터를 짧은 ’서명(signature)’으로 변환하여, 데이터의 핵심 정보를 담은 지문처럼 사용합니다.
- 과정:
- 데이터(예: 텍스트 문서)를 ’shingles(짧은 연속 단어 집합)’로 나누고, 여러 해시 함수를 적용해 최소 해시 값을 선택합니다.
- 이 서명들을 비교하여, Jaccard 유사도와 같은 지표로 유사도를 측정합니다.
- 장점:
- 계산 복잡도를 낮추면서도 유사한 항목들을 효율적으로 식별할 수 있습니다.
- MinHash Deduplication:
- Semantic Similarity (의미 기반 중복 제거):
- 방법:
- 임베딩(Embedding) + 유사도 측정 (1:1 비교 or ANN):
- Word2Vec, GloVe, FastText와 같은 단어 임베딩 기법이나, BERT, Sentence Transformers 등 최신 언어 모델을 사용하여 텍스트나 문장의 벡터 표현을 생성합니다.
- 생성된 벡터 간의 코사인 유사도 또는 유클리드 거리를 계산하여, 일정 임계값 이상으로 유사한 항목들을 중복으로 판단합니다.
- 목적:
- 주어진 쿼리(문서)와 “비슷한 문서”를 빠르게 찾고 싶을 때 (= “이 문서와 가장 비슷한 문서 TOP-K개 보여줘” 같은 작업을 자주 할 때)
- 일대일 매칭을 자주 해야 할 때(예: 특정 문서와 가장 유사한 후보 TOP-K개 검색)
- 주로 온라인 질의(online query) 상황에서 빠른 근사 최근접 이웃 검색이 필요할 때 사용함.
- 임베딩 + 클러스터링:
- K-means, HDBSCAN, 계층적 클러스터링 등의 기법을 사용해 유사한 항목들을 그룹화한 후, 각 그룹에서 대표 항목만 선택합니다.
- 클러스터링을 한다는 건 분류에 대한 메타 정보도 파악할 수 있다.
- 클러스터링 내의 비교는 접근 난이도, 자동 군집, 소수 군집에 대한 정보가 트레이드 오프임
- KMeans:
- 전체 데이터(문서) 집합을 “K개의 그룹”으로 나누어, 각 클러스터가 서로 어느 정도 유사성을 공유하도록 만드는 것이 주된 목표
- 군집화 결과를 통해, 클러스터 중심(centroid) 으로 대표되는 그룹별 특징을 파악하고 싶을 때
- 즉 데이터 전체를 일괄적으로 여러 “주제”나 “군집”으로 나누고 -> 각 군집 중심(centroid)” 혹은 “대표 데이터”만 빠르게 확인할 때
- 클러스터 내부에서 실제로 가까운 점들(near duplicates)을 추가 필터링(거리 임계값)으로 제거
- 주로 프라인 작업으로 한 번 군집화한 뒤, 해당 클러스터 정보를 바탕으로 통계나 레포트를 뽑는 상황에 사용됨.
- HDBSCAN:
- KMeans처럼 “고정된 K개”가 아니라, 데이터의 밀도와 구조를 기반으로 “몇 개의 클러스터”가 자연스럽게 형성되는지 파악하고 싶을 때
- “군집의 개수”를 직접 정하기 어렵거나, 잡음(노이즈) 데이터나 아웃라이어를 구분해 내고 싶을 때 사용
- 밀집 지역 내부에 모여 있는 샘플들을 중복으로 처리
- 이것도 오프라인 용도로 사용됨.
- 임베딩(Embedding) + 유사도 측정 (1:1 비교 or ANN):
- 효율성 비교:
- 엄격한 임베딩 + 유사도 측정:
- 임베딩 생성과 비교에 시간이 소요됨.
- 임베딩 생성은 O(n) 문서의 길이가 n 이라면, 비교 O(m^2) 문서의 개수가 m 이라면
- 임베딩 + 유사도 측정 (ANN 검색 사용):
- 임베딩 생성: 각 문서에 대해 임베딩을 생성하는 데 O(N) 시간이 걸리므로, M개 문서에 대해서는 O(M × N)이 듬.
- ANN 인덱스 구축: 일반적으로 ANN 알고리즘(예: HNSW, Annoy, FAISS 등)은 인덱스 구축에 평균적으로 O(M log M) 정도의 시간이 소요됩니다 (일반적으로 트리를 만들어서 구축하므로 KD-트리, Ball Tree, 혹은 트리 기반의 구조)
- ANN 검색 쿼리: 각 쿼리(문서)에 대해 근사 최근접 이웃을 찾는 데 보통 O(log M) 시간이 필요하며, M개 문서에 대해 검색하면 총 O(M log M)입니다 (일반적으로 트릴를 통해 검색하므노
- 최종: O(M × N + M log M)
- KMeans 클러스터링 알고리즘:
- 임베딩 생성: O(M * N)
- 엄격히: O(I × M × k × d)
- 상수( I, k, d 상수) 가정 시: O(M)
- HDBSCAN:
- 임베딩 생성: O(M * N)
- 평균적/낮은 차원: 약 O(M log M)
- 최악의 경우(또는 높은 차원): O(M²)
- 엄격한 임베딩 + 유사도 측정:
- Embedding + ANN 검색, Embedding + KMeans, Embedding + HDBSCAN 모두 텍스트 데이터라면 유사도 비교를 한다는 점에서는 동일함.
- 방법:
- Exact Deduplication (정확한 중복 제거):
데이터 오염 제거(Data decontamination):
- 학습 데이터셋에 평가나 테스트셋에 포함된 샘플(정확히 동일하거나 매우 유사한 내용)이 없도록 하는 것입니다.
- 데이터 중복 제거 기술(deduplication) 활용:
- 정확한 일치(Exact matching):
- 해시 함수(MD5, SHA-256 등)나 직접 문자열 비교를 통해 평가셋에 있는 샘플과 정확히 동일한 학습 데이터를 제거합니다.
- 유사 중복 탐지(Near-duplicate detection):
- 평가셋과 매우 유사한 학습 샘플도 제거합니다.
- 이를 위해 MinHash, n-그램 기반의 유사도 계산, 혹은 임베딩을 통한 유사도 측정 방법 등을 사용합니다.
- 정확한 일치(Exact matching):
- 간단한 방법 중 하나는 평가셋을 instruction 데이터셋에 함께 포함시켜, 데이터 중복 제거 단계에서 평가셋과 겹치는 학습 샘플만 제거하는 방식입니다.
데이터 품질 평가(Data quality evaluation):
- 학습 데이터셋의 품질을 점검하는 것은 모델의 성능과 평가의 신뢰성을 높이는 데 필수적입니다.
- 기본적인 평가 요소:
- 학습 데이터셋의 품질을 점검하는 것은 모델의 성능과 평가의 신뢰성을 높이는 데 필수적입니다.
- 다양성(Diversity): 다양한 상황과 표현이 포함되어 있는지
- 복잡성(Complexity): 텍스트가 너무 단순하지 않고 충분한 정보를 담고 있는지
- 평가 방법은 인간이 하는 수동적인 방법 이외에 자동으로 하는 접근 방법도 있음:
- LLM-as-a-judge:
- 대형 언어 모델(LLM)을 평가자로 사용하여 각 샘플의 품질을 점수화하거나 비교 평가
- 비교 평가 방식: “답변 A가 답변 B보다 나은가?”와 같이 상대적으로 평가하는 방식이 절대 점수화보다 성능이 좋은 경향이 있음
- 평가 시 프롬프트 엔지니어링을 통해, 예를 들어 평가 기준(예: 1~4 점 척도)을 정의하고 피드백을 함께 제공하도록 합니다.
- 분류기 학습 또는 보상 모델 (reward model) 이용하는 것도 있음:
- LLM as a judge 는 비용 문제 발생 및 느리기 떄문임. (대규모 데이터 처리에 적합하지 않음)
- Reward Model:
- 원래 RLHF(Reinforcement Learning from Human Feedback) 맥락에서 나온 개념으로, “(지시문, 답변) → 점수” 형태를 출력
- LLM 이 생성한 텍스트들이 인간의 선호에 맞게 잘 만들어졌는지 평가. 몇가지 대표적인 기준들을 만들어놓은거임. (범용적으로 사용할만함)
- 다양한 척도가 있음. (Helpfulness, Correctness, Coherence, Complexity, Verbosity)
- Helpfulness: 답변이 사용자에게 실제로 유용한 정보를 제공하거나 문제 해결에 기여하는 정도
- Correctness (정확성): 답변의 사실적·논리적 오류 여부나, 주어진 질문에 대한 올바른 해결책·사실을 제시하는지 평가
- Coherence (일관성): 답변 내 문장들이 서로 논리적으로 이어지는지, 흐름이나 문맥이 잘 맞는지를 평가
- Complexity (복잡성): 답변이 단순하거나 피상적이지 않고, 심층적인 내용이나 다단계 추론을 요구하는 과제를 충분히 다루는지 평가
- Verbosity (장황함, 장황도): 답변이 너무 장황하거나 불필요하게 길지는 않은지, 혹은 반대로 너무 짧아 핵심 정보를 놓치지는 않는지 측정
- LLM-as-a-judge:
- LLM-as-a-judge의 장점과 한계:
- 여러가지 편향이 있기도 함. (위치 편향, 길이 편향, 모델 내 편향)
- 추가적인 평가 전략:
- 복수의 LLM 사용: 여러 LLM을 활용해 평가하면 편향을 줄이고 평가 일관성을 높일 수 있습니다.
- 간단한 평가 척도와 few-shot 프롬프트: 단순한 점수 체계와 태스크에 특화된 벤치마크를 통해, 평가 결과를 해석하기 쉽게 보다 높은 성능을 내도록
- edu-classifier 접근:
- LLM 으로의 평가 방법이 계산 문제, 비용 문제가 있기 때문에 대규모 파이프라인에 적합하지 않다는 것에 근거해서 나온 방법임.
- 이 방법은 Alpaca 와 같은 접근 방법으로 LLM 을 이용해서 라벨이 달린 대규모 데이터를 생성하고 이를 바탕으로 작은 모델이나 분류기를 학습시켜서 평가를 하는 방법임.
- 즉 핵심 아이디어는 다음과 같음:
- 1단계: 대형 모델을 활용해 데이터셋에 라벨을 부여 (“이 응답은 양호/불량”, “점수 1~4” 등 품질 평가 라벨)
- 2단계: 이러한 대형 모델의 평가 라벨을 정답(ground truth) 삼아, 상대적으로 작은 모델에 학습시켜 자동 분류기를 만듦 (여기서 사용된 모델이 Snowflake/snowflake-arctic-embed-m (임베딩 모델)에 분류 헤드를 붙인 것이 ‘edu-classifier’)
- 여기서 임베딩 모델은 Encoder Only 모델을 말하는거고, 여기에 출력이 담긴 분류 헤더를 붙혀서 분류기를 만드는거임. 여기서 왜 임베딩 모델을 골랐는지 생각해보면 모델의 크기가 작기 때문임. 계산을 빠르게 하려고.
데이터 탐색(Data Exploration):
- 데이터 탐색은 한 번에 끝나는 작업이 아니라, 모델 학습 전반에 걸쳐 지속적으로 이루어지는 과정입니다. 이를 통해 데이터의 특성, 강점, 잠재적 문제점(오류, 편향 등)을 이해할 수 있습니다.
- 데이터 탐색을 통해 발견한 문제점(예: 특정 주제의 과대표집, 불균형, 오류 등)을 바탕으로 데이터를 정제하고, 후속 모델 학습의 품질을 높이는 전략을 수립할 수 있습니다.
- 두 가지 접근 방식:
- 수동 탐색(Manual Exploration): 사람이 직접 데이터를 검토하면서, 형식 오류, 데이터 입력 실수, 논리적 비일관성, 사실 오류 등을 발견합니다.
- 자동 분석(Automated Analysis): 통계 분석, 토큰화, 시각화 도구 등을 사용해 데이터의 어휘 다양성, 편향, 개념 분포 등을 수치화하고 패턴을 인지합니다.
- 데이터 탐색 방법:
- 통계적 분석(Statistical Analysis):
- 예: “전체 문서 중 토큰 길이 50 미만이 30%나 된다.” → 너무 짧은 문서가 많은지 의심 가능
- 예: 특정 단어(“Python”)가 전체의 10% 이상 차지 → 특정 주제 편향 가능
- 예: 전체 데이터의 70%가 특정 분야에 몰려 있음 → 데이터 균형화 작업 필요
- 시각화를 이용해서 통계적 분석을 하는 법:
- 히스토그램/막대그래프: 텍스트 길이 분포, 카테고리별 데이터 비율 등을 직관적으로 파악
- 워드클라우드(Word Cloud): 단어 사용 빈도에 따라 크기가 달라지므로, 자주 쓰이는 핵심어를 손쉽게 확인
- 분산 시각화(Scatter Plot) & UMAP/TSNE: 문서를 임베딩(문장 벡터화) 후 차원 축소 기법(UMAP, t-SNE)으로 2D/3D 공간에 배치하여, 데이터가 어떤 식으로 뭉치고 흩어지는지 시각적으로 관찰
- 토픽 맵/네트워크 그래프: 토픽 클러스터링 결과를 네트워크 그래프 형태로 보여주어 유사 주제끼리 어떻게 연결되는지 파악
- 통계적 분석(Statistical Analysis):
Synthetic Data Generation(합성 데이터 생성)
- 필요성:
- 특정 분야나 특수 애플리케이션에서는 공개 데이터가 부족할 수 있습니다.
- 예를 들어, JavaScript 에러 처리 관련 예시가 부족한 경우, 직접 데이터를 생성해 보완할 수 있습니다.
- 데이터 보강:
- 기존 데이터셋에서 소외된 영역이나 편향된 부분을 보완하는 데도 활용됩니다.
- 수동 생성이나 크라우드소싱은 비용과 시간이 많이 들기 때문에, LLM을 이용한 합성 데이터 생성이 효율적이고 확장성이 좋습니다.
- 합성 데이터 생성의 프로세스:
- Prompt Engineering 를 이용해서 잘 설계된 프롬프트를 준비하면 됨
- 여기서 프롬프트는 LLM이 생성할 데이터의 형식, 내용, 다양성을 결정하며, 예를 들어 Alpaca 데이터셋에서 사용된 다섯 개의 seed prompt가 그 예시입니다.
- 프롬프트에는 구체적인 지시사항, 예시, 제약 조건 등이 포함되어, 생성되는 데이터가 원하는 형식과 내용을 갖추도록 유도합니다
- 이것 말고도 다양한 방법이 있긴 함.
- 합성 데이터 생성 단계:
- 설계된 프롬프트를 바탕으로 LLM이 새로운 instruction-response 쌍을 생성합니다.
- 일부 시스템은 질문/지시문을 먼저 생성한 후, 그에 대응하는 답변을 생성하고, 추가적으로 품질 검증 과정을 거칩니다.
- 생성 데이터의 품질 관리:
- 생성 과정에서 데이터의 복잡성, 응답 길이, 어조, 스타일, 주제 등 다양한 속성을 조절할 수 있습니다. 이를 통해 특정 학습 목표나 기존 데이터셋의 보완 요구에 맞춘 데이터셋을 만들 수 있습니다.
- Outlines와 같은 라이브러리를 사용하면, 특정 형식을 준수하는 데이터 생성이 가능해집니다.
- 기존 데이터셋의 편향이나 누락된 부분을 보완하기 위해, 다양한 관점과 스타일의 데이터를 생성할 수 있습니다.
- 합성 데이터 생성의 도전 과제:
- 합성 데이터가 너무 단순하거나 반복적이면, 모델 학습에 충분한 복잡성과 다양성을 제공하지 못할 수 있습니다.
- LLM이 생성하는 데이터는 해당 모델의 내재된 편향이나 오류를 그대로 반영할 수 있습니다.
데이터 증강(Data Augmentation):
- 데이터 증강은 기존의 instruction 데이터 샘플을 활용하여 데이터의 양뿐만 아니라 질을 높이는 과정입니다. 이는 완전히 새로운 데이터를 생성하는 것(data generation)과는 다르게, 이미 존재하는 데이터를 개선하고 다양화하는 데 초점을 맞춥니다.
- 데이터 증강의 목표:
- 양적 증가: 부족한 영역의 데이터를 보완하기 위해 샘플 수를 늘림
- 질적 개선: 기존 샘플의 다양성 및 복잡성을 높여, 모델이 더 어려운 문제나 다단계 추론에 대응하도록 만듦
- Evol-Instruct 방법:
- 개념: 간단한 지시문을 LLM(예: GPT-4o)을 활용해 더 복잡하고 심도 있게 진화시키는 방법입니다.
- 두 가지 전략:
- In-depth evolving (심층 진화): 기존 지시문의 복잡도를 높이는 데 초점을 맞춥니다.
- Constraints(제약 추가): 추가적인 조건이나 제한을 부여하여 난이도를 높임
- Deepening(깊이 더하기): 표면적인 질문 대신 더 심도 있는 질문을 만들어냄
- Concretizing(구체화): 모호한 개념 대신 구체적인 용어로 대체하여 명확성을 높임
- Increasing reasoning steps(추론 단계 증가): 여러 단계의 추론이 요구되도록 수정
- Complicating input(입력 복잡화): XML, JSON, 코드 조각 등 복잡한 데이터 형식을 추가
- In-breadth evolving (폭 넓은 진화):
- 기존 지시문에서 영감을 받아 완전히 새로운 지시문을 생성, 특히 드물거나 긴 꼬리(long-tailed) 예시를 만들어 다양성을 확보합니다.
- In-depth evolving (심층 진화): 기존 지시문의 복잡도를 높이는 데 초점을 맞춥니다.
- UltraFeedback 방법:
- Evol-Instruct와는 달리, UltraFeedback은 답변의 질에 초점을 맞춥니다.
- 다양한 지시문과 모델을 활용하여 다채로운 답변을 생성한 후, GPT-4와 같은 강력한 모델이 이 답변들을 평가하고, 세부적인 피드백과 수치 점수를 제공합니다.
- 이를 통해 단순한 답변 생성을 넘어서, 답변의 다양성과 질을 더욱 향상시키는 방법입니다.
크롤링한(raw) 텍스트 데이터를 활용해 Instruction 데이터셋을 만드는 과정:
- 이를 위해 큰 문제점 두 가지를 해결해야 함:
- 데이터가 비구조적 (unstructured) 형태라는 점:
- 즉, raw 텍스트만 있을 뿐, (instruction, answer) 쌍이 아니라서 직접 이 쌍을 만들어야 함.
- 이를 해결하기 위해 LLM을 사용하여 텍스트를 “instruction”과 “answer”로 변환한다.
- 이때 “backtranslation” 기법(정해진 답을 주고 질문을 만들게 하거나, 텍스트를 재구성해 질문/답변 쌍을 생성) + rephrasing(원문 스타일 유지 + 포맷 정돈)을 활용한다.
- 샘플 수가 제한되어 있음:
- 크롤링한 기사(글) 수 자체가 많지 않아서, 다양한 instruction 쌍을 만들어야 하는데 데이터가 부족하다.
- 해결책으로 기사(글)를 Chunk 단위로 분할한 뒤, 각 Chunk마다 여러 (instruction, answer) 쌍을 생성해 표본 수를 늘림.
- 데이터가 비구조적 (unstructured) 형태라는 점:
- 비구조적 데이터를 (Instruction, Answer) 쌍으로 만드는 아이디어:
- LLM을 통해 Raw Text → Instruction/Answer로 변환
- 원문이 단순히 길게 써 있는 텍스트라면, 한 문단을 ‘answer’로 삼고, 이에 대응하는 ‘instruction’을 AI가 생성하도록 요청하는 방식이 가능하나, 그대로 쓰면 어색할 수 있음.
- 따라서 rephrasing 기법을 사용해 텍스트를 깔끔하게 다듬고, 작성 스타일을 유지하면서도 잘 정돈된 ‘answer’를 만든다.
- 추가적인 prompt engineering을 통해, 모델이 일정한 형식의 질문/답변을 만들어내게 하며, 이 과정을 자동화해 대규모로 적용 가능.
- LLM을 통해 Raw Text → Instruction/Answer로 변환
- 데이터 수가 부족할 때의 해결책:
- 본문 예시에서는 기사(글)의 수도 적고, 각 기사도 길이가 제한적이므로 더 많은 (instruction, answer) 쌍을 만들어낼 필요가 있다.
- Chunking: 하나의 글을 여러 “덩어리(Chunk)”로 나누고, 각 Chunk마다 3개의 (instruction, answer) 쌍을 생성하면, 데이터셋의 표본 수가 늘어난다.
- LLM의 응답 형식이 일정치 않은 문제:
- LLM이 structured output(예: JSON) 형태로 항상 정확하게 내놓으리라는 보장이 없다. -> 따라서 추가적인 문자열 파싱(string parsing)을 해야 할 수도 있고, 이를 최소화하기 위해 structured generation(JSON 모드, pydantic, 정규식 등)을 활용하면 더 편리하다.
- 예: OpenAI의 “JSON mode”는 보다 엄격한 JSON 출력을 기대할 수 있어, 후속 파싱이 쉬워진다.
- LLM Twin 프로젝트에서 사용된 합성 데이터 파이프라인:
- Raw text (크롤링된 기사 등) → Regex 기반 Data Cleaning
- Chunking (1000~2000자 단위로 텍스트 분할)
- 분할된 chunk를 LLM(GPT-4O-mini 등) 에 넣어 Instruction-answer generation
- 마지막에 Data filtering 규칙을 적용해 품질 낮은 항목 걸러냄
- 최종적으로 Instruction dataset 완성
크롤링한(raw) 텍스트 데이터를 활용해 Instruction 데이터셋을 만드는 과정 - 코드 구현 부분
(1) load_articles_from_json 함수
- Hugging Face Dataset 객체를 생성하기 위해, data["artifact_data"] 안에 들어있는 각 항목(item)에서 필요한 필드들을 추출.
- 이렇게 만든 Dataset은 이후 전처리, chunking, 모델에 넣기 등을 편하게 해줌:
- 데이터 전처리 및 변환: Dataset 객체는 .map(), .filter(), .train_test_split() 같은 내장 함수를 통해 데이터 전처리, 변환, 분할 등을 쉽게 수행할 수 있습니다.
- 일관된 API: 다양한 데이터 소스(JSON, CSV, 텍스트 파일 등)에서 로드한 데이터를 동일한 인터페이스로 다룰 수 있어 코드의 일관성과 재사용성을 높여줍니다.
- 스트리밍 지원: 대규모 데이터셋의 경우, Dataset 클래스는 스트리밍 모드를 지원하여 메모리 사용량을 최적화할 수 있습니다.
- 병렬 처리: 내부적으로 병렬 처리 기능을 지원하여 데이터 전처리 작업을 빠르게 수행할 수 있습니다.
- Hub 업로드 및 공유: Dataset 객체는 .push_to_hub() 메서드를 통해 Hugging Face Hub에 쉽게 업로드하여, 연구 결과를 공유하거나 재현성을 확보할 수 있게 합니다.
- 데이터 버전 관리: Dataset 객체를 사용하면 데이터를 쉽게 버전 관리하고, 변경 사항을 추적할 수 있어 실험의 재현성이 향상됩니다.
- load_articles_from_json("cleaned_documents.json") 형태로 호출하면 JSON 파일에서 여러 기사(글) 정보를 뽑아 Dataset으로 반환합니다.
def load_articles_from_json(file_path: str) -> Dataset:
with open(file_path, "r") as file:
data = json.load(file)
return Dataset.from_dict(
{
"id": [item["id"] for item in data["artifact_data"]],
"content": [item["content"] for item in data["artifact_data"]],
"platform": [item["platform"] for item in data["artifact_data"]],
"author_id": [item["author_id"] for item in data["artifact_data"]],
"author_full_name": [item["author_full_name"] for item in data["artifact_data"]],
"link": [item["link"] for item in data["artifact_data"]],
}
)
(2) 텍스트 정제(clean_text) 및 Chunking 준비 - clean_text 함수:
def clean_text(text):
text = re.sub(r"[^\w\s.,!?']", " ", text)
text = re.sub(r"\s+", " ", text)
return text.strip()
- 정규표현식 [^\w\s.,!?'] : 알파벳, 숫자(\w), 공백(\s), 마침표, 콤마, 느낌표, 물음표, apostrophe(‘)를 제외한 문자들은 “ ”(공백)으로 대체.
- 정규표현식 \s+ : 연속된 공백 문자들을 하나의 공백으로 치환.
- strip() 으로 앞 뒤 공백을 제거함.
- 이 과정을 통해 특수문자나 중복 공백 등을 지우고, 읽기 편한 형태로 통일.
(3) 텍스트 정제(clean_text) 및 Chunking 준비 - extract_substrings 부분 (Chunking):
- sentence_pattern: 정규 표현식 패턴으로, 문장을 구분하기 위해 사용됩니다.
- 이 패턴은 문장 구분 기호(마침표, 물음표, 느낌표) 뒤의 공백을 기준으로 분리하되, 약어나 이름 등에서 발생할 수 있는 잘못된 분할을 피하기 위해 부정 후방 탐색(negative lookbehind) 등을 활용합니다.
- 문장 단위로 청크를 구성함:
- (1) dataset["content"]에서 각 기사를 하나씩 가져옵니다
- (2) clean_text(article)를 호출하여, 특수문자 제거, 불필요한 공백 치환 등으로 텍스트를 정제합니다.
- (3) re.split(sentence_pattern, cleaned_article)로 정제된 텍스트를 문장 단위로 분리합니다.
- (4) 각 문장에 대해 strip()을 사용해 앞뒤 공백을 제거합니다.
- (5) 현재 청크(current_chunk)에 현재 문장을 추가했을 때, 전체 길이가 max_length (2000자)를 넘지 않으면, 문장을 current_chunk에 이어 붙입니다.
- (6) 만약 현재 청크에 문장을 추가하면 max_length를 초과한다면, 먼저 현재 청크의 길이가 최소 min_length (1000자) 이상인 경우에만, current_chunk.strip()을 extracts 리스트에 추가합니다.
def extract_substrings(dataset: Dataset, min_length: int = 1000, max_length: int = 2000) -> List[str]:
extracts = []
sentence_pattern = r"(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?|!)\s"
for article in dataset["content"]:
cleaned_article = clean_text(article)
sentences = re.split(sentence_pattern, cleaned_article)
current_chunk = ""
for sentence in sentences:
sentence = sentence.strip()
if not sentence:
continue
if len(current_chunk) + len(sentence) <= max_length:
current_chunk += sentence + " "
else:
if len(current_chunk) >= min_length:
extracts.append(current_chunk.strip())
current_chunk = sentence + " "
if len(current_chunk) >= min_length:
extracts.append(current_chunk.strip())
return extracts
(4) InstructionAnswerSet 클래스:
- 글에서는 (instruction, answer) 쌍을 체계적으로 관리하기 위해 InstructionAnswerSet 클래스를 만듭니다.
- 이후에 추출된 Chunk 로 (instruction, answer) 쌍을 만드는 거고, InstructionAnswerSet 클래스는 이 쌍 데이터를 관리하기 위한 용도임.
- from_json 클래스 메서드 같은 경우는 모델(LLM) 출력이 JSON 형태일 경우, 문자열을 파싱해서 instruction, answer를 추출한 뒤, pairs 리스트를 만드는 용도임.
- 이 클래스의 __iter__ 같은 경우는 이 클래스를 이터레이터처럼 사용할 수 있도록 정의하는 것. 그래서 for pair in my_set: ... 형태로 순회 가능.
class InstructionAnswerSet:
def __init__(self, pairs: List[Tuple[str, str]]):
self.pairs = pairs
@classmethod
def from_json(cls, json_str: str) -> 'InstructionAnswerSet':
data = json.loads(json_str)
pairs = [(pair['instruction'], pair['answer'])
for pair in data['instruction_answer_pairs']]
return cls(pairs)
def __iter__(self):
return iter(self.pairs)
(5) 추출된 Chunks → LLM 호출로 (Instruction, Answer) 생성:
- 글에서는 “추출된 텍스트 조각(extract)”을 LLM에 넘겨, 그 문맥(텍스트 내용)을 참조하여 (instruction, answer) 쌍을 만들게 함.
- 이 과정은 어떤 모델이든 가능하나, 예시로 GPT-4o mini를 사용.
- 복잡한 reasoning이 필요한 게 아니라 “주어진 글을 참고해 질문/답변 쌍을 뽑아내는” 용도이므로, 저렴한 모델도 충분하다는 논리.
- 아래 프롬프트를 보면 요구사항은 다음과 같음:
- 추출된 텍스트(extract)에 기반해서 5개의 (instruction, answer) 쌍을 만들어라.”
- 명시적 제약: instruction은 “context”나 “시스템” 같은 단어를 직접 언급하지 말 것, 질문은 스스로 완결적이도록 할 것, answer는 원문 스타일을 모방할 것 등.
- JSON 형식으로 결과를 달라고 요청.
def generate_instruction_answer_pairs(
extract: str, client: OpenAI
) -> List[Tuple[str, str]]:
prompt = f"""Based on the following extract, generate five instruction-answer pairs. Each instruction \
must ask to write about a specific topic contained in the context. ...
"""
(6) System 메시지 + JSON 응답 파싱:
- 다읔 코드는 함수 내부에서 실제로 LLM을 호출하는 로직임.
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "You are a helpful assistant ..."
},
{"role": "user", "content": prompt},
],
response_format={"type": "json_object"},
max_tokens=1200,
temperature=0.7,
)
(7) 전체 파이프라인 통합 함수: create_instruction_dataset:
- ThreadPoolExecutor 를 사용해서 병렬로 여러 청크에 대해 generate_instruction_answer_pairs 를 호출함. 속도 향상을 위해.
- 여기서 max_workers=4로 지정(너무 많이 올리면 OpenAI API rate limit에 걸릴 수 있음).
- futures 리스트 내 각 스레드가 (instruction, answer) 쌍 5개씩 생성 → .extend()로 모두 합침.
def create_instruction_dataset(
dataset: Dataset, client: OpenAI, num_workers: int = 4
) -> Dataset:
extracts = extract_substrings(dataset)
instruction_answer_pairs = []
with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
futures = [executor.submit(generate_instruction_answer_pairs, extract, client)
for extract in extracts]
for future in tqdm(concurrent.futures.as_completed(futures),
total=len(futures)):
instruction_answer_pairs.extend(future.result())
instructions, answers = zip(*instruction_answer_pairs)
return Dataset.from_dict(
{"instruction": list(instructions), "output": list(answers)}
)
(8) 파이프라인 실행:
- 원문 로드 → Instruction dataset 생성.
- 즉 실제 코드에서는 create_instruction_dataset(raw_dataset, client) 호출 시, 크롤링된 기사 → substring 추출 → 각 substring 마다 5개 쌍 → 최종적으로 (instruction, output)만 담긴 Dataset 완성.
def main(dataset_id: str) -> Dataset:
client = OpenAI()
# 1. Load the raw data
raw_dataset = load_articles_from_json("cleaned_documents.json")
# 2. Create instruction dataset
instruction_dataset = create_instruction_dataset(raw_dataset, client)
# 3. train/test split and push to Hugging Face Hub
filtered_dataset = instruction_dataset.train_test_split(test_size=0.1)
filtered_dataset.push_to_hub("mlabonne/llmtwin")
return filtered_dataset
Supervised Fine-Tuning (SFT)
SFT 란?
- 사전학습(pre-trained)된 모델을 더 작은 규모의 데이터셋에 대해 다시 학습시켜, 모델이 단순한 다음 토큰 예측 이상의 기능(즉, 질문에 대답하고, 지시를 따르는)하도록 만드는 과정입니다
- 기본 모델은 원래 일반적인 언어 패턴을 학습했지만, 구체적인 사용 시나리오에 특화되어 있지 않습니다. SFT를 통해 모델이 도움이 되는 어시스턴트로 변모하도록 합니다.
SFT의 목적 및 활용:
- 일반 성능 개선: 사전학습된 모델이 다양한 상황에서 더 정확하고 유용하게 동작하도록 미세 조정합니다.
- 새로운 지식 주입: 예를 들어, 새로운 언어, 도메인, 또는 특정 주제에 관한 데이터를 추가하여 모델이 그 영역에 대한 전문성을 갖도록 합니다.
- 특정 태스크에 집중: 번역, 요약, 질의응답 등 특정 작업에 최적화하도록 조정할 수 있습니다.
- 특정 음성이나 스타일 채택: 모델이 특정 톤이나 스타일로 응답하도록 미세 조정할 수 있습니다.
SFT 관련 개념: 저장 형식과 채팅 템플릿:
- 저장 형식 (Storage Formats):
- SFT에서는 instruction과 answer 쌍의 데이터셋을 구축해야 하는데, 이 데이터를 JSON, CSV, 혹은 다른 구조화된 형식으로 저장합니다.
- 이러한 형식은 데이터의 일관성과 후속 처리(예: 파인튜닝 시 데이터 로딩)를 용이하게 해줍니다.
- 채팅 템플릿 (Chat Templates):
- SFT에서는 모델이 대화형 어시스턴트 역할을 할 수 있도록, 데이터를 일정한 포맷(예: 시스템 메시지, 사용자 입력, 어시스턴트 응답)으로 구성하는 경우가 많습니다.
- 템플릿을 사용하면, 모델이 다양한 대화 시나리오를 학습할 때 일관된 구조를 갖게 되어, 실제 사용 시 더 자연스러운 대화형 응답을 생성할 수 있습니다.
SFT 구현 방법: 주요 기법들:
- Full-Finetuning:
- 모델의 모든 파라미터를 대상으로 미세 조정을 수행합니다.
- 이 방식은 전체 모델이 학습 데이터에 맞춰 재조정되므로, 성능 향상에 매우 효과적일 수 있지만, 메모리와 계산 비용이 매우 높습니다.
- Low-Rank Adaptation (LoRA):
- LoRA는 모델의 파라미터 중 일부(일반적으로 가중치 행렬)에 저차원(low-rank) 매트릭스를 추가하여 미세 조정을 수행하는 방법입니다.
- 전체 파라미터를 업데이트하는 대신, 추가된 저차원 행렬만 학습함으로써 메모리와 계산 비용을 크게 절감할 수 있습니다.
- Quantization-aware Low-Rank Adaptation (QLoRA):
- QLoRA는 LoRA와 유사한 기법에, 양자화(Quantization) 개념을 결합한 방식입니다.
- 양자화는 모델 파라미터의 정밀도를 줄여서 메모리 사용량과 계산 비용을 더욱 낮춥니다
언제 파인튜닝을 해야할까?:
- 대부분의 시나리오에서, 바로 모델을 미세 조정(SFT)하기보다는 prompt engineering으로 시작하는 것이 좋습니다.
- 만약 prompt engineering으로 얻은 결과가 요구하는 품질, 비용, 응답 속도 등을 만족하지 못한다면, Instruction Dataset을 생성하는 방법을 고려할 수 있습니다. 그리고 충분한 데이터가 확보된다면 미세 조정(Fine-Tuning)이 하나의 옵션으로 등장하게 됩니다.
Fine-Tuning의 한계와 도전 과제:
- SFT는 사전학습된 모델의 기존 지식을 활용하여 특정 목적에 맞게 재조정합니다. 이 때문에, 사전학습 데이터와 너무 동떨어진 새로운 지식(예: 매우 희귀한 언어 등)을 효과적으로 학습하기 어려울 수 있습니다.
- 새로운 지식을 Fine-Tuning 할 때, 일부 연구에서는 모델이 더 잦은 환각을 일으킬 수 있음을 보여줍니다.
- 미세 조정 과정에서 기존의 사전학습된 지식이 소실될 위험이 있습니다. (Catastrophic Forgetting:)
Instruction dataset formats:
- Instruction dataset은 모델에게 “어떤 작업을 수행하라”는 지시(Instruction)와 그에 대한 예상 답변(Output)을 쌍으로 제공함으로써, 모델이 단순한 다음 토큰 예측만 하는 것이 아니라 실제 질문에 응답하고 지시를 따를 수 있도록 훈련시키는 데 사용됩니다.
- 저장 형식:
- 보통, 각 데이터 샘플은 Python의 dictionary(사전)로 표현됩니다.
- 키(Key): 보통 instruction, output(or answer) 외에도 system이나 input과 같은 prompt의 종류를 나타내는 키가 사용됩니다.
- 값(Value): 해당 키에 대응하는 실제 텍스트가 저장됩니다.
- 글에서는 가장 일반적으로 사용되는 세 가지(또는 네 가지) 포맷을 소개하고 있습니다. 아래 표(Table 5.5)의 예시를 참고하면:
- Alpaca Format:
- {"instruction": "...", "input": "...", "output": "..."}
- "input" 키는 선택적(optional)입니다.
- ShareGPT Format:
- {"conversations": [{"from": "...", "value": "..."}, ...]}
- 대화형 데이터로 구성되어 있으며, 각 메시지가 "from"과 "value"라는 키로 구성됩니다.
- OpenAI Format:
- {"conversations": [{"role": "...", "content": "..."}, ...]}
- 각 메시지에 "role" (예: “system”, “user”, “assistant”)과 "content"를 사용하여 대화의 역할과 내용을 명확하게 구분합니다.
- Raw text Format:
- {"text": "..."}
- 단순한 원시 텍스트를 저장하는 형식입니다. 이는 SFT가 사전학습과 큰 맥락에서는 다르지 않다는 점을 보여줍니다.
- Alpaca Format:
- 다양한 포맷이 필요한 이유: 대화형 어시스턴트를 위한 데이터셋은 대화의 흐름을 나타내는 형식(OpenAI나 ShareGPT)이 유용하고, 간단한 instruction/response 쌍을 위한 경우에는 Alpaca나 OASST 형식이 적합할 수 있습니다.
Chat templates:
- Chat Templates의 목적:
- Instruction 데이터셋을 단순한 (instruction, answer) 쌍에서 벗어나, 대화형 템플릿으로 변환합니다
- 이를 통해 모델은 어떤 부분이 시스템 지시, 사용자 입력, 그리고 모델 응답인지를 명확하게 인식할 수 있습니다.
- 특수 토큰 사용:
- 템플릿은 메시지의 시작과 끝을 명확하게 구분하는 특수 토큰을 포함합니다.
- 예를 들어, OpenAI에서 사용하는 ChatML 템플릿은 <|im_start|>와 <|im_end|> 토큰을 사용하여 각 메시지의 경계를 구분합니다.
- 기본적으로 사전학습된 모델은 대화 형식에 대해 특별한 템플릿을 학습하지 않았기 때문에, 미세 조정(SFT) 시에 일정한 템플릿을 사용해 모델이 대화의 구조를 이해하도록 해야 합니다.
- 그리고 만약 fine-tuning된 모델이 동일한 템플릿으로 학습되지 않았다면, 성능 저하가 발생할 수 있습니다.
다양한 Chat Template 포맷 - ChatML 템플릿 (OpenAI 원본):
- 각 메시지는 <|im_start|>와 <|im_end|> 토큰으로 구분됩니다.
- 메시지 내에 “system”, “user”, “assistant” 같은 문자열이 포함되어, 누가 말하는지를 명확하게 합니다.
- 이런 템플릿은 훈련에서는 Instruction dataset의 (instruction, answer) 쌍이 Chat 템플릿으로 변환되어 모델에 공급됩니다.
- 추론 단계에서는 사용자의 입력(예: system과 user 메시지)만 제공되고, 모델은 <|im_start|>assistant 토큰 이후의 내용을 생성하게 됩니다.
<|im_start|>system
You are a helpful assistant, who always provide explanation. Think like you are answering to a five year old.<|im_end|>
<|im_start|>user
Concepts: building, shop, town
Write a sentence that includes all these words.<|im_end|>
<|im_start|>assistant
In our little town, there is a shop inside a big building where people go to buy their favorite toys and candies.<|im_end|>
다른 템플릿 예시:
- Alpaca: 간단한 텍스트 형식으로 “### Instruction:“과 “### Response:“를 사용
- Llama 3, Phi-3, Gemma: 각각 다른 특수 토큰이나 구조를 사용하여 대화의 시작과 끝, 또는 발화자의 구분을 표현합니다
템플릿의 중요성 및 주의사항:
- 공백과 줄 바꿈:
- Chat 템플릿에서는 모든 공백, 줄 바꿈, 특수 토큰의 정확한 위치가 매우 중요합니다.
- 템플릿이 조금만 달라져도 토큰화(tokenization)에 영향을 미쳐 모델의 입력이 변하고, 이는 성능 저하로 이어질 수 있습니다.
- 템플릿 관리 도구:
- Jinja 같은 템플릿 엔진을 사용하면, 조건문과 루프 등을 활용해 템플릿을 동적으로 생성하고, 일관성을 유지할 수 있습니다.
- Transformers 라이브러리에서도 이러한 템플릿을 지원하여, 학습과 추론 단계 모두에서 안정적으로 사용할 수 있도록 돕습니다.
- 표준화된 템플릿 사용:
- Alpaca, ChatML, Llama 3 등 표준 템플릿 예시들을 사용하면, 학습 데이터셋의 품질과 일관성을 높이고, 다른 연구나 서비스와 호환성을 유지할 수 있습니다.
Parameter-efficient fine-tuning techniques
파라미터 효율적인 미세 조정 기법(Parameter-efficient Fine-Tuning Techniques):
- SFT(지도 미세 조정)는 사전 학습된 모델을 instruction–answer 쌍과 같은 작은 데이터셋으로 재학습하는 과정입니다
- 문헌에서는 다양한 미세 조정 기법이 제안되었지만, 현재 주류로는 Full Fine-Tuning, LoRA, QLoRA 세 가지 기법에 수렴하게 되었습니다.
Full Fine-Tuning이란?
- Full Fine-Tuning은 기본(pre-trained) 모델의 모든 파라미터를 대상으로 미세 조정을 수행하는 가장 직관적인 SFT 방식입니다.
- 즉, 기본 모델이 가진 모든 가중치(Weights)와 바이어스(Biases)를 재학습하여, 모델이 instruction–answer 쌍에 맞게 동작하도록 만듭니다
- 훈련 목표:
- 다음 토큰 예측(next-token prediction)을 계속 사용하지만, 데이터셋의 구조(지시문, 답변 쌍)가 달라짐으로써 모델이 단순 생성기에서 실제 어시스턴트 역할을 수행하도록 학습합니다.
- 장점:
- 모든 파라미터를 업데이트하기 때문에, 이론적으로는 미세 조정 데이터에 가장 잘 맞게 모델을 최적화할 수 있습니다.
- 단점:
- 높은 계산 자원 및 메모리 요구:
- 모든 파라미터를 재학습하므로, 모델 크기에 따라 매우 많은 메모리와 계산 비용이 발생합니다.
- 메모리 사용량은 다음과 같이 추정할 수 있습니다:
- Memory = Parameters + Gradients + Optimizer States + Activations
- Parameters:
- 모델의 학습 가능한 가중치 및 바이어스.
- 예: 32-bit (FP32) 기준이면 4바이트, 16-bit (FP16/BF16) 기준이면 2바이트 정도 소요.
- Gradients:
- 역전파 동안 계산되는 각 파라미터별로 계산되는 기울기(Gradient).
- 대략 파라미터 하나당 4바이트/파라미터 소요.
- Optimizer States:
- Adam과 같은 옵티마이저는 각 파라미터마다 추가적인 상태(예: 모멘텀, 분산)를 저장합니다.
- 파라미터 하나당 2개의 추가 벡터(1차, 2차 모멘텀)를 FP32로 저장
- 보통 8바이트/파라미터 정도.
- Activations:
- 순전파 중 생성되는 중간 결과로, 경우에 따라 메모리 부담이 커질 수 있으나, 배치 크기가 작으면 상대적으로 덜 부담됩니다.
- 이러한 요소들을 합치면, 기본적으로 약 16바이트/파라미터의 메모리 비용이 발생한다고 볼 수 있습니다.
- 예로 7B(70억) 파라미터 모델은 약 112GB의 VRAM, 70B 모델은 약 1,120GB의 VRAM이 필요할 수 있습니다.
- 높은 계산 자원 및 메모리 요구:
LoRA(Low-Rank Adaptation):
- LoRA는 파라미터 효율적 미세 조정(Parameter-Efficient Fine-Tuning) 기법 중 하나로, 대규모 언어 모델(LLM)을 미세 조정하는 데 필요한 계산 자원과 메모리 사용량을 크게 줄이기 위해 고안되었습니다.
- 대형 LLM을 모두 업데이트(Full Fine-Tuning)하는 데 필요한 메모리는 매우 크고, 이를 수행하기 위한 비용이 지나치게 높을 수 있습니다.
- LoRA는 모델의 본래 파라미터(Weights W)를 직접 수정하지 않고, 저순위(low-rank) 행렬인 A, B를 추가로 학습해 모델을 재조정함으로써 이 문제를 해결합니다.
- 장점:
- 메모리 사용량 감소
- 학습 도중 업데이트해야 할 파라미터가 (A, B) 두 행렬뿐이므로, 메모리를 크게 절약할 수 있습니다.
- 실제로, 수십수백 GB의 VRAM이 필요한 Full Fine-Tuning과 달리, 7B 파라미터 모델을 단일 GPU(약 1418GB)로도 미세 조정이 가능해집니다.
- 학습 속도 향상
- 업데이트해야 할 파라미터 수가 현저히 적어, 학습이 일반적으로 빠르게 진행됩니다.
- 원본 모델 파라미터 보존 (Non-destructive)
- 기본 모델의 W는 변경되지 않으며, A, B 매트릭스만 학습됩니다.
- 이를 통해, 기존에 사전학습된 지식을 “파괴”하지 않고, 새로운 데이터나 태스크에 초점을 맞출 수 있습니다.
- Full Fine-Tuning과 유사하거나 더 나은 성능을 낸다고도 함
- 메모리 사용량 감소
LoRA의 원리: 저순위(low-rank) 업데이트:
- 기본 개념
- 일반적으로 파라미터 행렬 W(크기 d×k)에 직접 업데이트를 가하지 않고, W’ = W + BA 형태로 저순위 행렬 B, A를 곱해 만든 항(term)을 추가합니다.
- B와 A의 크기는 각각 d×r, r×k로, r은 보통 매우 작은 값(예: 4~64, 대표적으로 8).
- B×A의 결과는 W와 동일한 차원을 갖되, rank(r)이 낮아 메모리 사용이 크게 줄어듭니다.
- 훈련 과정의 역전파 시 W는 업데이트되지 않고, B와 A에 대해서만 기울기가 계산되며 학습합니다.
- LoRA의 하이퍼파라미터
- Rank (r)
- LoRA에서 학습되는 두 행렬 A(크기 d×r)와 B(크기 r×k)의 내부 차원(저순위)을 결정하는 값입니다. 즉, W’ = W + BA 형태에서 r이 클수록 B(크기 d×r)와 A(크기 r×k)의 차원이 커집니다.
- r이 클수록 모델이 더 풍부한 표현을 학습할 수 있지만, 트레이닝 파라미터 수가 늘어나고, 메모리 사용량이 증가할 수 있습니다.
- 일반적으로 r=8부터 시작해서 256까지 시도하기도 합니다.
- Alpha (α)
- LoRA 업데이트 항에 적용되는 스케일링 계수로, 실제로는 W’ = W + (α/r) × (BA) 형태가 됩니다.
- (α/r)이 LoRA 업데이터의 실제 스케일이 되므로, (α/r) 값이 커지면 LoRA 업데이트가 더 크게 반영되고, 작으면 보수적으로 반영됩니다.
- α를 r의 2배 정도로 잡아 (α/r)=2 정도로 유지하는 것이 흔한 경험적 설정입니다.
- 과적합(Overfitting)이나 미세 조정이 부족(Underfitting)하다고 느껴지면 α/r 값을 조절할 수 있습니다.
- Dropout(선택 사항)
- LoRA 계층(저순위 매트릭스 곱) 결과에 드롭아웃을 적용해 일부 업데이트를 무작위로 무시함으로써, 과적합을 방지하는 기법입니다.
- 일반적으로 0~0.1 사이의 작은 값을 사용합니다.
- Rank (r)
QLoRA:
- QLoRA는 Dettmers et al.에서 제안한 기법으로, LoRA의 기본 아이디어(즉, 모델의 모든 파라미터를 업데이트하지 않고 작은 저순위 행렬(어댑터)만 학습하는 방식)를 양자화(quantization) 기법과 결합한 방법입니다.
- 주요 목표:
- 대규모 언어 모델(LLM)을 미세 조정할 때 필요한 메모리 사용량과 계산 비용을 대폭 줄이는 것입니다.
- 이를 통해 상대적으로 작은 GPU(제한된 메모리에서도 사용 가능한)로도 미세 조정을 가능하게 합니다
- QLoRA의 핵심 기술:
- 모델 양자화
- 모델의 파라미터를 정밀도가 높은 32비트나 16비트 부동소수점 대신, 더 낮은 비트 수(여기서는 4-bit)의 데이터 형식으로 표현하는 방법입니다.
- Custom 4-bit NormalFloat (NF4) 데이터 타입 (NF4는 4비트 정밀도로 데이터를 표현할 수 있기 때문에, 메모리 절감 효과가 매우 큽니다.)
- LoRA 어댑터 적용:
- QLoRA는 기본 모델의 가중치(W)는 업데이트하지 않고, 대신 작은 저순위 행렬들(A와 B)을 추가합니다.
- 이 어댑터들이 학습되는 동안, 실제로 미세 조정되는 파라미터는 이 어댑터들만 업데이트되므로, 전체 모델 파라미터의 수는 매우 작게 유지됩니다.
- Double Quantization:
- QLoRA는 기본 모델의 파라미터를 4비트로 양자화한 후, 이 양자화 상수들(quantization constants)도 다시 양자화하는 방식인 Double Quantization을 적용합니다
- 기본 파라미터를 4비트로 양자화한 후에도, 양자화된 값을 표현하거나 복원하는 데 필요한 양자화 상수(scale, zero-point 등)가 추가 메모리를 차지합니다
- 예를 들어, 어떤 파라미터 값 x가 정수 q로 표현된다면, x ≈ scale × q + zero_point.
- 이를 통해 메모리 사용량을 추가로 줄일 수 있습니다.
- Paged Optimizers:
- Optimizer States의 문제:
- 옵티마이저(예: Adam)는 각 파라미터에 대해 추가적인 상태(모멘텀, 분산 등)를 저장합니다.
- 이러한 상태들은 모델 크기가 커질수록 매우 큰 메모리 용량을 차지하게 됩니다.
- Paged Optimizers는 옵티마이저 상태를 GPU 메모리와 CPU 메모리(또는 다른 저장소) 사이에서 효율적으로 관리하는 기법입니다.
- 이를 통해 GPU 메모리에 한꺼번에 모든 옵티마이저 상태를 로드하지 않고, 필요한 시점에 일부만 메모리에 올리는 “페이지(page)” 기법을 사용합니다.
- 미세 조정 과정에서 발생하는 메모리 스파이크(memory spikes)를 관리하기 위해 Nvidia의 통합 메모리(unified memory) 기능을 활용하는 최적화 기법입니다.
- 훈련 도중에 특정 시점에 옵티마이저 상태가 모두 GPU에 로드되면, 메모리 사용량이 급증할 수 있습니다.
- Paged Optimizers는 이러한 메모리 스파이크를 분산시켜, GPU 메모리 사용량을 일정하게 유지합니다.
- 이는 특히 대규모 모델을 미세 조정할 때, 메모리 사용을 효율적으로 분산시키고 관리할 수 있게 해줍니다.
- Optimizer States의 문제:
- 모델 양자화
- LorA 와의 비교:
- 메모리 절감 효과:
- 예를 들어, 7B 파라미터 모델의 경우, 초기화 시 사용되는 peak 메모리가 LoRA 기준 14GB에서 QLoRA는 9.1GB로 35% 줄어듭니다.
- 훈련 속도:
- 메모리 절감 효과 때문에 QLoRA는 LoRA보다 약 30% 느린 경향이 있습니다.
- 이는 양자화와 추가 최적화(예: double quantization, paged optimizer) 과정에서 계산 비용이 추가되기 때문입니다.
- 모델 성능:
- 성능 면에서는 LoRA와 QLoRA 사이에 큰 차이가 없거나, 미세한 차이만 있습니다.
- 따라서, 주된 선택 기준은 메모리 제약과 훈련 속도에 관한 문제입니다.
- 메모리 절감 효과:
Training Parameters
학습률 (Learning Rate):
- 학습률은 모델의 파라미터가 한 번 업데이트될 때 얼마만큼 변경되는지를 결정하는 하이퍼파라미터입니다.
- 쉽게 말해, “모델이 한 단계 학습할 때 얼만큼 배움(업데이트)할 것인가?“를 정하는 값입니다.
- 너무 낮으면 업데이트 폭이 작아서 학습 속도가 느려지고, 모델이 최적해에 도달하기 어려워집니다.
- 너무 높으면 너무 크게 업데이트되어 학습 과정이 불안정해지고, 발산(diverge)하거나 최적해 근처로 수렴하지 못할 위험이 있습니다.
- 일반적으로 1e-6 (매우 작음)에서 1e-3 (상당히 큼) 사이의 값을 사용하며, Transformer 계열 모델에서는 보통 1e-5 정도가 시작점으로 많이 쓰입니다.
학습률 스케줄러 (Learning Rate Scheduler):
- 학습률 스케줄러는 전체 학습 과정 동안 학습률을 조정해 주는 역할을 합니다.
- 초기에는 높은 학습률로 빠른 진전을 보이고, 이후 점차 학습률을 낮춰 세밀한 조정이 가능하도록 합니다.
- 주요 스케줄러 종류:
- Linear Scheduler:
- 학습률을 일정한 비율로 선형적으로 감소시킵니다.
- Cosine Scheduler:
- 학습률이 처음에는 천천히 감소하다가 점차 더 빠르게 감소하는 코사인 곡선을 따릅니다.
- Linear Scheduler:
- Warmup 기간:
- 보통 전체 학습 단계의 약 5% 정도를 워밍업(warmup) 기간으로 사용하여, 학습 초기에 0에서 시작해 목표 학습률까지 선형으로 증가시킵니다.
- 워밍업 기간을 사용하면, 초기 학습 시 불안정한 업데이트를 피하고, 모델이 안정적으로 학습을 시작할 수 있습니다.
- 예를 들어, 학습률을 3e-4로 시작해 학습이 진행됨에 따라 1e-7까지 감소시키는 방식으로, 워밍업 기간 이후 95% 동안 선형 또는 코사인 스케줄에 따라 학습률이 점진적으로 감소합니다.
- Warmup 기간에는 학습률이 낮게 시작해서 점차 올리는게 좋은건가?
- ㅇㅇ
- 학습 초반에는 모델의 파라미터가 무작위로 초기화되어 있거나, 사전 학습된 가중치도 새로운 태스크에 대해 최적화되어 있지 않은 상태이기 때문에, 큰 학습률로 업데이트하면 너무 큰 변화가 발생하여 학습이 불안정해질 수 있습니다.
- 학습을 하는데 너무 큰 변화가 생겨서 학습이 불안정하다는 건 무슨 의미일까?
- 모델의 파라미터 업데이트가 너무 급격해서 손실 함수(loss function)의 최적화 경로를 제대로 따르지 못하는 상황을 말합니다. 구체적으로 설명하면 학습률이 높으면 각 업데이트마다 모델 파라미터가 크게 바뀌게 됩니다. 이 경우, 현재의 손실 함수의 기울기(gradient)를 따라 이동할 때, 최적의 최소점(로컬 미니멈)을 지나쳐 버리거나, 심지어 손실이 더 커지는 방향으로 갈 수 있습니다.
- → 즉, 모델이 안정적인 수렴 대신 계속해서 진동하거나 발산하는 현상이 발생합니다. (수렴을 할 수 없는 경우를 말함.)
- 지역해에 빠지는 게 아님. 지역해에 빠졌을 때는, 일반적으로 그곳이 최적의 해는 아닐 수 있으므로, 더 많은 학습이나 적절한 학습률 스케줄러를 통해 벗어나야 합니다.
- 지역해에 빠지는 걸 도움을 주는 방법:
- warmup 이 도움을 줌. 어느 방향으로 갈 지.
- Adam과 같은 옵티마이저는 각 파라미터마다 학습률을 적응적으로 조정합니다. (이를 통해, 기울기가 큰 영역에서는 업데이트를 조절하여 급격한 변화로 인한 문제를 완화할 수 있습니다.)
- 모멘텀을 사용하는 옵티마이저는 이전 업데이트 방향을 반영하여, 지역해에서 벗어나 더 좋은 방향으로 이동할 가능성을 높여줍니다.
- Gradient Noise 추가: 업데이트 과정에 약간의 노이즈(랜덤성을 추가)를 도입하면, 미세 조정 과정에서 모델이 지역해에 빠지는 것을 피하고, 다양한 경로를 탐색할 수 있도록 도와줍니다.
Batch Size:
- 배치 사이즈는 모델 업데이트 시 한 번에 처리하는 샘플의 수를 결정하며, 이는 모델의 학습 안정성과 속도, 그리고 메모리 요구량에 큰 영향을 미칩니다
- 배치 사이즈란 한 번의 forward 및 backward pass에서 모델에 공급되는 샘플의 수를 의미합니다.
- 학습 도중에, 모델의 가중치는 한 배치 내 모든 샘플에 대해 계산된 평균 기울기를 기반으로 업데이트됩니다.
- 배치 사이즈가 미치는 영향:
- 배치 사이즈가 클수록 전체 데이터셋의 진짜 기울기에 가까운 안정적인 gradient 추정이 가능합니다.
- 큰 배치 사이즈는 병렬 계산이 가능하여, GPU의 효율성을 높이고 학습 속도를 향상시킬 수 있습니다.
- 한 배치에 포함되는 샘플 수가 많을수록, GPU에 로드해야 하는 데이터 양이 많아져 메모리 요구량이 커집니다.
- 큰 배치의 장점은 학습 속도와 학습의 안정성임. 하지만 메모리 요구사항이 있다. 이 문제를 해결하는 방법은 Gradient Accumulation 임:
- 여러 개의 작은 mini-batch에 대해 forward/backward pass를 수행하면서, 기울기를 누적(accumulate)한 후, 누적된 기울기를 이용해 한 번의 업데이트를 진행합니다.
- 예를 들어, GPU 메모리 제한 때문에 한 번에 8개의 샘플만 처리할 수 있는데, 효과적인 배치 사이즈를 32로 만들고 싶다면, 4번의 mini-batch (각 8개씩)에서 기울기를 누적한 후 모델 업데이트를 수행합니다.
- Gradient Accumulation의 Trade-off:
- 효과적인 배치 사이즈는 늘어나지만, 한 업데이트마다 수행되는 forward/backward pass의 수가 늘어나므로 학습 시간이 증가할 수 있습니다.
- 효율적인 배치 사이즈 계산식:
- Effective Batch Size = Batch Size x GPUs x Gradient Accumulation Steps
- 예를 들어, 2개의 GPU를 사용하고, 각 GPU에서 4개의 샘플씩 처리하며, 4번의 gradient accumulation을 수행하면, 효과적인 배치 사이즈는 4 × 2 × 4 = 32가 됩니다
Maximum Sequence Length:
- 최대 시퀀스 길이는 모델이 한 번에 처리할 수 있는 입력 텍스트의 최대 토큰 수를 의미합니다.
- 일반적으로 512에서 4,096 토큰 사이로 설정되지만, 태스크와 GPU 메모리 용량에 따라 128,000 토큰 이상까지도 설정할 수 있습니다.
- 예시:
- 많은 언어 생성 태스크에서 2,048 토큰이 흔한 최대 길이입니다.
- RAG (Retrieval-Augmented Generation) 같은 애플리케이션에서는 8,192 토큰 이상을 사용할 수도 있습니다.
- 최대 길이가 크면 한 번에 더 많은 정보를 입력할 수 있다는 점은 장점임. 하지만 최대 길이가 커지면, 한 배치에 포함되는 토큰 수가 늘어나 GPU 메모리 사용량과 연산 비용이 증가합니다.
- Truncation (잘라내기):
- 입력 데이터가 최대 길이를 초과하는 경우, 초과하는 토큰들은 잘라내집니다.
- 잘라내기는 보통 시작 부분(Left truncation) 또는 끝 부분(Right truncation)에서 수행됩니다.
- 예를 들어, 최대 길이가 1,024 토큰일 때, 1,500 토큰 입력은 476 토큰이 제거됩니다.
- 배치 크기 및 메모리와의 관계:
- 최대 길이 설정은 배치 크기와 직접적으로 연결됩니다
- 예를 들어, 배치 크기 12와 최대 길이 1,024이면 12,288 토큰을 처리하게 되지만, 최대 길이를 512로 줄이면 6,144 토큰만 처리됩니다.
- 따라서, GPU 메모리와 학습 데이터 특성을 고려하여 최대 길이를 적절히 조정하는 것이 중요합니다.
Packing:
- Packing은 각 배치에서 할당된 최대 길이를 최대한 효율적으로 사용하기 위한 기법입니다.
- 단일 샘플이 최대 길이에 도달하지 않는 경우, 여러 짧은 샘플들을 하나의 배치에 결합하여 처리합니다.
- 최대 시퀀스 길이가 1,024 토큰인데, 실제 샘플들이 200-300 토큰 정도라면, 하나의 배치 슬롯에 3-4개의 샘플을 채울 수 있습니다.
- 이렇게 하면 한 배치 당 처리되는 데이터 양이 증가해 학습 효율이 크게 개선됩니다.
- 여러 샘플을 하나의 시퀀스로 결합할 경우, 모델이 서로 다른 샘플의 토큰들을 혼동하지 않도록 Attention Mask와 같은 기법을 사용해야 합니다.
- 이러한 마스크는 모델이 각 샘플의 경계를 인식하고, 서로의 토큰에 주의를 기울이지 않도록 도와줍니다.
에포크(Epochs)란?:
- 에포크는 전체 학습 데이터셋을 한 번 모두 학습하는 과정을 말합니다.
- 예를 들어, 데이터셋이 100,000개의 샘플로 구성되어 있다면, 한 에포크는 이 100,000개의 샘플을 모두 한 번씩 모델에 입력하는 것을 의미합니다
- LLM 미세 조정에서의 에포크 수:
- LLM 미세 조정에서는 일반적으로 110 에포크가 사용됩니다.
- 많은 경우 25 에포크 정도가 성공적인 결과를 가져오는 것으로 보고됩니다.
- 에포크 수의 결정 요인:
- 태스크 복잡성: 복잡한 작업일수록 모델이 충분히 학습하기 위해 더 많은 에포크가 필요할 수 있습니다.
- 적은 데이터셋의 경우, 모델이 너무 많이 학습하면 오버피팅(overfitting)이 발생할 위험이 있기 때문에 1~3 에포크가 적절할 수 있습니다
- 반면, 데이터셋이 크다면 5~10 에포크가 필요할 수도 있습니다.
- 에포크 수 조정의 Trade-Off:
- 너무 적은 에포크는 모델이 충분한 학습을 하지 못해 Underfitting (과소적합) 현상이 발생할 수 있습니다.
- 너무 많은 에포크는 모델이 학습 데이터에 너무 과도하게 적응하여 Overfitting (과적합) 이 발생할 수 있습니다
- 최적의 에포크 수 결정 방법:
- 학습 중에 검증(validation) 데이터셋에 대한 모델 성능을 지속적으로 체크하여, 에포크 수를 조정합니다.
- Early Stopping:
- 만약 모델의 성능이 일정 시점 이후 더 이상 개선되지 않거나 오히려 악화된다면, 학습을 조기에 중단하여 Overfitting을 방지할 수 있습니다.
- 이 방식은 에포크 수를 동적으로 결정하는 데 도움을 줍니다.
최적화 알고리즘(Optimizers):
- 최적화 알고리즘은 모델의 파라미터를 조정하여 손실 함수(loss function)를 최소화하는 역할을 합니다.
- 이를 통해 모델이 주어진 학습 데이터에서 더 나은 성능을 보이도록 업데이트됩니다.
- AdamW 및 8-bit AdamW:
- AdamW (특히 8-bit 버전)는 대부분의 상황에서 좋은 선택이며, 메모리 효율성을 높이면서도 안정적인 학습을 제공합니다.
- AdamW란?
- AdamW는 Adam(Adaptive Moment Estimation) weight decay를 올바르게 분리하여 적용함으로써 학습 안정성과 일반화 성능을 높이고, Transformer 같은 대규모 모델에 특히 적합함
- Weight Decay(웨이트 디케이): 모델 파라미터들이 지나치게 커지거나, 특정 파라미터가 과도하게 발산(값이 너무 커짐)하는 것을 막기 위해, 파라미터에 일정한 감쇠(Decaying) 효과를 주는 방식이야.
- 왜 “Weight Decay를 올바르게 분리해서 적용한다”는 말이 나오는가?
- 전통적인 Adam 옵티마이저는 가중치 업데이트를 할 때, 학습률(learning rate) 과 기울기(gradient) 에 따라 파라미터를 업데이트하고, 동시에 L2 정규화를 통해 weight decay를 적용합니다.
- 하지만 이 경우, weight decay가 기울기 업데이트와 섞여서 적용되기 때문에, 가중치가 업데이트되는 방식에 부정적인 영향을 줄 수 있습니다.
- 이는 본래 weight decay가 단순히 파라미터 값을 일정 비율로 감소시키는 역할을 해야 하는데, 기울기 업데이트와 섞이면 그 효과가 왜곡될 수 있습니다.
- AdamW는 이 문제를 해결하기 위해, weight decay를 기울기 업데이트와 분리(decoupled) 시켜서 적용합니다. 즉, 먼저 모델의 파라미터를 학습률과 기울기에 따라 업데이트한 후, 따로 가중치 감쇠(일종의 “패널티”)를 적용합니다.
- 왜 Transformer와 대규모 모델에 적합한가?
- 대규모 모델은 수많은 파라미터를 가지고 있어, 학습하는 동안 오버피팅(overfitting)이나 불안정한 업데이트가 발생할 가능성이 큽니다.
- AdamW는 weight decay를 따로 적용함으로써, 각 파라미터가 지나치게 커지는 것을 방지하고, 모델이 보다 일반적인 패턴을 학습하도록 도와줍니다
- 가중치 감쇠는 모델이 과적합되는 것을 방지하는 데 도움을 줍니다.
- AdamW는 Adam(Adaptive Moment Estimation) weight decay를 올바르게 분리하여 적용함으로써 학습 안정성과 일반화 성능을 높이고, Transformer 같은 대규모 모델에 특히 적합함
- 8-bit AdamW:
- 8-bit 버전의 AdamW는 32-bit 버전과 유사한 성능을 보이면서, GPU 메모리 사용량을 줄여줍니다.
- 다만, 8-bit AdamW는 메모리 효율성을 높여주지만, 훈련 속도를 반드시 개선하는 것은 아닙니다.
- 이 옵션은 GPU 메모리 제한이 있는 환경에서 매우 유용합니다.
- AdaFactor:
- AdaFactor는 특히 메모리 제약이 심한 환경에서 사용하기 위해 설계된 옵티마이저입니다.
- 메모리 효율적: AdaFactor는 옵티마이저 상태를 저장하는 데 필요한 메모리를 줄여줍니다.
- 학습률 튜닝 없이 잘 동작: 명시적인 학습률 튜닝이 덜 필요한 장점이 있어, 자원이 제한된 환경에서 편리하게 사용할 수 있습니다.
- 단, 모든 경우에서 AdamW 만큼의 성능을 보장하지는 않을 수 있습니다.
- Paged Optimizers:
- 매우 큰 모델이나 GPU 메모리가 제한된 상황에서, paged AdamW 8-bit과 같은 옵티마이저를 사용하면 메모리 소비를 추가로 줄일 수 있습니다.
- 이 기법은 일부 옵티마이저 상태를 CPU 메모리로 오프로드하여 GPU 메모리 부담을 낮춥니다.
- 메모리 제약이 있을 경우에는 8-bit AdamW나 AdaFactor, 혹은 paged AdamW 8-bit 등이 적합합니다.
- 만약 메모리 여유가 있고 최대 성능을 추구한다면, 비양자화된 adamw_torch 옵티마이저를 사용할 수 있습니다.
Weight Decay:
- Weight Decay는 모델의 학습 과정에서 가중치가 너무 커지지 않도록 패널티(term) 를 손실 함수에 추가하는 정규화(regularization) 기법입니다.
- 큰 가중치는 모델이 특정 입력 특징에 과도하게 의존하게 만들어, 학습 데이터에만 특화된 복잡한 패턴(과적합, overfitting)을 학습할 위험을 높입니다.
- 가중치에 패널티를 부여함으로써, 모델은 더 간단하고 일반화 가능한 특징을 학습하게 되어, 새로운 데이터에 대해서도 좋은 성능을 보일 가능성이 높아집니다.
- Weight Decay의 적용 방식:
- 손실 함수(Loss Function)에 패널티 추가:
- 모델의 기본 손실 함수에, 가중치의 크기에 비례하는 항을 추가합니다.
- 예를 들어, 일반적인 L2 정규화 방식에서는 손실 함수 L에 다음과 같은 항이 추가됩니다:
- L_{\text{total}} = L + \lambda \sum_{i} w_i^2
- 여기서 \lambda 는 weight decay 계수, w_i는 각 가중치 값입니다.
- 이 항은 가중치의 값이 클수록 손실이 커지도록 만들어, 모델이 가중치를 작게 유지하도록 유도합니다.
- 결과적으로 모델은 과도하게 복잡한 특징 대신, 보다 단순하고 일반화 가능한 패턴을 학습하게 됩니다.
- 손실 함수(Loss Function)에 패널티 추가:
- Weight Decay의 설정 값:
- 대부분의 경우, weight decay 값은 0.01에서 0.1 사이로 설정됩니다.
- 특히 AdamW 옵티마이저를 사용할 때는 0.01 정도가 일반적인 시작점으로 많이 사용됩니다.
- 왜 이 범위인가?
- 값이 너무 높으면 모델이 중요한 패턴을 학습하기 어려워지고, 과도하게 제약되어 학습이 제대로 진행되지 않을 수 있습니다.
- 값이 너무 낮으면 충분한 정규화 효과를 보지 못해, 모델이 과적합될 위험이 있습니다.
Gradient checkpointing:
- Gradient checkpointing 은 특히 매우 깊은 네트워크(예: 대형 언어 모델)에서 메모리 사용량을 줄이기 위한 기법입니다.
- 이게 필요한 이유:
- 딥러닝 모델의 학습에서는 순전파(forward pass) 동안 각 레이어의 중간 활성화 값(activations)을 모두 저장합니다.
- 이 활성화 값들은 역전파(backward pass) 시 기울기를 계산하는 데 필요하지만, 매우 깊은 네트워크에서는 저장해야 할 활성화의 양이 기하급수적으로 증가하여 GPU 메모리 제한에 도달할 수 있습니다.
- 메모리 소비를 줄이기 위해 모든 활성화를 저장하지 않고, 일부 레이어의 활성화만 체크포인트로 저장합니다.
- 저장되지 않은 레이어의 활성화는 역전파 과정에서 필요할 때마다 재계산합니다.
- 동작 원리:
- 네트워크의 특정 레이어에서 활성화 값을 저장(체크포인트)합니다.
- 예를 들어, 10개의 레이어가 있을 때 모든 레이어의 활성화를 저장하지 않고, 3, 6, 9번째 레이어의 활성화만 저장할 수 있습니다.
- 역전파 과정에서 체크포인트에 저장되지 않은 활성화 값들은, 필요한 시점에 다시 순전파를 통해 계산합니다.
- 이로 인해 GPU 메모리 사용량은 크게 줄어들지만, 일부 추가 계산 비용(재계산 시간)이 발생합니다.
- Trade-off:
- 메모리 절약 vs. 계산 시간 증가:
- 체크포인트를 적게 사용하면 메모리 사용량은 줄어들지만, 역전파 시 더 많은 활성화를 재계산해야 하므로 계산 시간이 늘어납니다.
- 체크포인트를 많이 사용하면 계산 시간은 줄어들지만, 메모리 사용량은 증가합니다.
- 체크포인트로 일부 활성화 값만 저장하는게 아니라 그냥 디스크 레벨로 내리면 되는거 아닌가?
- 가능은 하지만 효율적이지 않음.
- 대부분의 딥러닝 프레임워크는 GPU와 CPU 간의 메모리 전송을 최적화된 방식으로 관리하지만, 디스크 I/O는 그와 비교해 최적화가 어렵습니다.
- 그냥 재계산하는게 더 효율적이라고 함.
Fine-tuning in practice
모델 선택 시 고려 사항:
- License:
- 모델 라이선스가 상업적 사용을 허용하는지 확인해야 합니다.
- 예를 들어, Meta에서 출시한 Llama 3.1 8B는 “Llama 3.1 Community License Agreement”를 통해 상업적 사용이 가능하므로, 회사에서 사용하기에 적합합니다.
- Budget:
- 파라미터 수가 작을수록 비용이 낮아집니다.
- 10B 미만의 모델은 저렴한 GPU에서도 운영할 수 있으며, 토큰 처리 속도도 빠릅니다.
- Performance:
- 모델의 성능은 일반-purpose 벤치마크 또는 도메인/태스크 별 벤치마크를 통해 평가됩니다.
- 모델이 최종 사용 사례에서 요구하는 능력을 가지고 있는지 확인하는 것이 중요합니다.
Fine-tuning을 위한 도구 및 라이브러리:
- TRL (Transformer Reinforcement Learning):
- Hugging Face에서 개발한 라이브러리로, SFT 및 preference alignment를 위한 최신 알고리즘을 포함합니다.
- 단일 GPU 및 다중 GPU 환경 모두를 지원합니다(FSDP, DeepSpeed).
- Axolotl:
- YAML 구성 파일을 사용해 미세 조정 작업을 간소화한 도구입니다.
- TRL을 기반으로 하며, 여러 데이터셋을 자동으로 결합하는 추가 기능 등을 제공합니다.
- Unsloth:
- custom 커널을 사용해 학습 속도를 2-5배 개선하고, 메모리 사용량을 최대 80% 줄이는 도구입니다. (즉 학습 속도 개선과 메모리 사용량을 줄여줌)
- TRL 기반이며, 모델을 GGUF 양자화 포맷으로 자동 변환하는 등의 유틸리티를 제공합니다.
- 현재는 단일 GPU 환경에서만 사용 가능합니다.
(1) 미세 조정 파이프라인 구현 - 설치 및 환경 설정:
- Unsloth 라이브러리 및 의존성 설치:
- GitHub 저장소나 Unsloth의 저장소에서 최신 버전을 받습니다.
- Hugging Face Hub에 로그인:
- 모델 접근이나, 파인튜닝된 모델을 업로드하려면 Hugging Face Hub 로그인 필요.
- .env 파일에 HF_TOKEN을 추가해 개인 Access Token을 설정합니다.
- Comet ML API Key 설정
- 학습 과정을 추적하고 시각화하기 위해 Comet ML을 사용하는 경우, .env 파일에 COMET_API_KEY를 설정합니다.
(2) 미세 조정 파이프라인 구현 - 라이브러리 임포트:
- TRL (SFTTrainer): 미세 조정과 관련된 로직 제공.
- datasets: 데이터셋 로딩과 전처리에 사용.
- transformers (TrainingArguments, TextStreamer): Hugging Face Transformers 생태계에서 학습 설정 및 텍스트 스트리밍 지원.
- unsloth (FastLanguageModel, is_bfloat16_supported): Unsloth에서 제공하는 고속 모델 로딩 및 bfloat16 지원 체크 기능.
import os
import torch
from trl import SFTTrainer
from datasets import load_dataset, concatenate_datasets
from transformers import TrainingArguments, TextStreamer
from unsloth import FastLanguageModel, is_bfloat16_supported
(3) 미세 조정 파이프라인 구현 - 모델 로딩:
- model_name: 파인튜닝할 공개 가중치 모델(여기서는 “meta-llama/Meta-Llama-3.1-8B”).
- max_seq_length: 최대 토큰 길이를 2,048로 설정.
- load_in_4bit=False:
- True일 경우 QLoRA 방식(4비트 양자화된 모델) 로딩,
- False일 경우 LoRA(16비트/FP16 등) 방식.
- 이 예시에서는 LoRA를 사용하기로 했으며, 이는 VRAM 여유가 있을 때 더 빠른 학습과 높은 품질을 기대할 수 있기 때문입니다. (메모리 제한이 심하면 load_in_4bit=True로 QLoRA로 전환.)
max_seq_length = 2048
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="meta-llama/Meta-Llama-3.1-8B",
max_seq_length=max_seq_length,
load_in_4bit=False,
)
(4) 미세 조정 파이프라인 구현 - LoRA 설정:
- r=32: 저순위 차원(rank)을 32로 설정. (LoRA 행렬 A와 B의 차원을 결정)
- lora_alpha=32: LoRA 업데이트에 적용되는 스케일링 계수.
- lora_dropout=0: 드롭아웃을 꺼서 학습 속도를 높임.
- target_modules: LoRA를 적용할 레이어 목록으로, attention(Q, K, V 등) 및 MLP(업/다운 프로젝션, gate_proj, output_proj)까지 모두 지정해 높은 표현력 확보.
- 이렇게 해서 모든 linear 계층에 LoRA를 적용하면, 모델 업데이트 항이 많아져 성능 향상을 기대할 수 있지만 그만큼 메모리 사용도 조금 증가합니다.
model = FastLanguageModel.get_peft_model(
model,
r=32,
lora_alpha=32,
lora_dropout=0,
target_modules=["q_proj", "k_proj", "v_proj", "up_proj", "down_proj", "o_proj", "gate_proj"],
)
(5) 미세 조정 파이프라인 구현 - 데이터 준비 및 전처리:
- llmtwin: 3,000 샘플 정도로 적으므로, FineTome-Alpaca-100k 중 일부(1만 샘플)로 보강(upsample)하여 최종 13,000 샘플 정도 구성.
dataset1 = load_dataset("mlabonne/llmtwin")
dataset2 = load_dataset("mlabonne/FineTome-Alpaca-100k", split="train[:10000]")
dataset = concatenate_datasets([dataset1, dataset2])
(6) 미세 조정 파이프라인 구현 - Alpaca 포맷 변환:
- Alpaca 스타일 템플릿: “### Instruction: …\n### Response:…”
- 각 샘플 끝에 EOS_TOKEN 추가해 모델이 응답을 끝내도록 함.
alpaca_template = """Below is an instruction that describes a task. Write a response taht appropiartely completes the request
### Instruction:
{}
### Respones:
{}
"""
EOS_TOKEN = tokenizer.eos_token
dataset = dataset.map(format_samples, batched=True, remove_columns=dataset.column_names)
(7) 미세 조정 파이프라인 구현 - 학습/검증 데이터 분할:
- 95%는 학습, 5%는 검증 데이터로 사용.
dataset = dataset.train_test_split(test_size=0.05)
(8) 미세 조정 파이프라인 구현 - 학습 하이퍼파라미터 설정:
- 학습률(learning_rate=3e-4), lr_scheduler_type=“linear”: 초기부터 선형 감소 스케줄.
- 배치 크기:
- per_device_train_batch_size=2, gradient_accumulation_steps=8
- 효과적 배치 사이즈 = 2 * 8 = 16
- 에포크(num_train_epochs=3):
- 비교적 적은 데이터셋이므로 3번 반복.
- fp16/bf16: GPU가 bfloat16을 지원하면 사용, 아니면 fp16.
- optimizer=“adamw_8bit”: 8비트 AdamW로 메모리 절약.
- weight_decay=0.01: 오버피팅 방지 정규화.
- warmup_steps=10: 처음 10 스텝 동안 학습률을 선형 증가.
- 이런 설정으로 학습을 하면, 예시로 A100 GPU에서 약 50분 소요.
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
dataset_text_field="text",
max_seq_length=max_seq_length,
dataset_num_proc=2,
packing=True,
args=TrainingArguments(
learning_rate=3e-4,
lr_scheduler_type="linear",
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
num_train_epochs=3,
fp16=not is_bfloat16_supported(),
bf16=is_bfloat16_supported(),
logging_steps=1,
optim="adamw_8bit",
weight_decay=0.01,
warmup_steps=10,
output_dir="output",
report_to="comet_ml",
seed=0,
),
)
trainer.train()
(9) 미세 조정 파이프라인 구현 - 미세 조정 결과 및 간단 테스트:
- 미세 조정된 모델이 Alpaca 템플릿에 맞춰 응답을 생성하는지 간단히 확인.
FastLanguageModel.for_inference(model)
message = alpaca_prompt.format("Write a paragraph to introduce supervised fine-tuning.", "")
inputs = tokenizer([message], return_tensors="pt").to("cuda")
text_streamer = TextStreamer(tokenizer)
_ = model.generate(**inputs, streamer=text_streamer, max_new_tokens=256, use_cache=True)
(10) 미세 조정 파이프라인 구현 - 모델 저장:
- 학습이 끝난 모델을 로컬에 저장하거나 Hugging Face Hub에 푸시.
model.save_pretrained_merged("model", tokenizer, save_method="merged_16bit")
model.push_to_hub_merged("mLabonne/TwinLlama-3.1-8B", tokenizer, save_method="merged_16bit")
(11) 미세 조정 파이프라인 구현 - Comet ML에서 모니터링:
- 훈련 중 로스(loss), 검증 로스(eval_loss), 학습률(learning_rate), gradient_norm 등을 실시간 확인
- loss: 훈련 손실이 전반적으로 감소해야 함.
- eval_loss: 검증 손실이 함께 낮아지거나 일정 수준에서 머무르는지 확인.
- gradient_norm: 너무 큰지, 안정적인지 관찰.
- Gradient Norm(기울기 노름): 기울기 벡터(수많은 파라미터에 대한 기울기를 하나로 모은 벡터)의 크기를 측정한 값입니다.
- 보통 L2 노름(제곱 후 루트를 취하는 방식)을 사용해, |\nabla w| 라고 표현됩니다.
- 이 값이 큰 경우, 파라미터를 크게 업데이트한다는 의미이고, 작은 경우에는 파라미터 변경 폭이 적습니다.
- 기울기 노름이 지나치게 커지면, 파라미터 업데이트가 매우 커져서 학습이 불안정해지거나 발산(diverge)할 위험이 있습니다.
- 반대로 기울기 노름이 지나치게 작으면, 학습 속도가 매우 느려지거나 정체될 수 있습니다
- 기울기 노름이 커지는 상황이, 때때로 모델이 학습 데이터에 과도하게 적응(오버피팅)하는 신호일 수 있습니다.
- Gradient Clipping(기울기 클리핑) 도 있음. 이는 기울기 노름이 일정 임계값을 초과하지 않도록 억제하는 기법으로, 과도한 업데이트를 막아 학습을 안정적으로 진행하도록 돕습니다.
- 학습률이 계획대로 warmup 후 선형적으로 감소하는지 체크.
'Generative AI > Fine-tuning' 카테고리의 다른 글
LIMA: Less Is More for Alignment (0) | 2025.01.22 |
---|---|
Scaling Relationship On Learning Mathematical Reasoning with Large Language Models (0) | 2025.01.22 |
Beyond Human Data: Scaling Self-Training forProblem-Solving with Language Models (0) | 2025.01.22 |
Reinforced Self-Training (ReST) for Language Modeling (0) | 2025.01.22 |
Magicoder: Empowering Code Generation with OSS-INSTRUCT (0) | 2025.01.22 |