아.. 참고로 티가 났겠지만, 필자는 문과 비개발자다.
훈련, 추론 모두 PEFT를 사용하였다.
LLM 모델 내부에는 거의 10.7B개의 파라미터가 있으며, 용량도 30GB가 넘는다. 내 데이터는 고작 2만개다. 따라서 모델을 최대한 축소시키되, 효율적인 학습이 가능하도록 만들어야 한다. 그것을 위해 4-bit-양자화라는 것을 활용하였다.
# ----- 2) 4bit 양자화 모델
print("\n▶ 2. 모델/토크나이저 로드 (4bit)...")
# 모델 축소
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4비트로 불러오고
bnb_4bit_quant_type="nf4", # nf4를 쓸 거야(왜인지는 모름)
bnb_4bit_compute_dtype=torch.float16, #계산은 16비트 유로댄스
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL_PATH, # 허깅페이스에 있는 모델 이름. "이 모델 다운로드해 줘"
quantization_config=bnb_config, # "다운로드하면서 아까 만든대로 모델을 줄여줘
device_map="auto", # GPU있으면 GPU에 올리고 CPU에 있으면 CPU로(하지만 CPU로 하면 안된다.)
trust_remote_code=True,
)
# 이건 학습 때 메모리 아끼는 다른 기술(gradient checkpointing)이랑 충돌방지 코드라고 한다. 제미나이가 넣어줬다^_^
model.config.use_cache = False
# 사전을 불러온다(토크나이저)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_PATH, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# 모델 준비
model = prepare_model_for_kbit_training(model)
print("✅ 4bit 모델 준비 완료")
출처) Gemini 고마워
정밀도 (Precision) | 비트 수 | 메모리 사용량 | 주요 특징 | 비유 |
---|---|---|---|---|
FP32 (Full Precision) | 32-bit | 기준 (100%) | 원본 상태. 가장 정밀하고 안정적이지만, 가장 무겁고 느림. | 4K UHD 원본 영상 |
FP16 / BF16 (Half Precision) | 16-bit | 50% 감소 | 가장 널리 쓰이는 경량화. 메모리 사용량이 절반으로 줄고 속도가 빨라짐. 약간의 정밀도 손실이 발생할 수 있음. | 고화질 Full HD 영상 |
INT8 (8-bit Integer) | 8-bit | 75% 감소 | 정수(Integer) 연산을 사용하여 추론(Inference) 속도가 매우 빠름. 학습보다는 이미 학습된 모델을 사용할 때 주로 쓰임. | 표준 화질(SD) 방송 영상 |
4-bit (NF4 등) | 4-bit | 87.5% 감소 | 극한의 압축률. QLoRA에서 사용. 놀라울 정도로 적은 성능 저하로 메모리 사용량을 최소화함. | 고효율 압축 스트리밍 영상 (유튜브) |
얼마나 모델을 축소시킬 건지 결정했으니, 이제 학습 방법을 정해야 한다. 물리적인 한계로, 내 목표는 극한의 가성비(내 인생인가)였다. 그 의미에서 나는 QLoRA와 주요 예시 몇 가지를 입력해서 학습하는 방향을 선택하였다. QLoRA는, 간단히 말하면, 중요한 부분만 학습을 시키는 모형이다. 더 이상 알면 다친다..
# ----- 3) LoRA 설정 -----
print("\n▶ 3. LoRA 설정...")
peft_config = LoraConfig(
r=LORA_R, # 크기를 설정한다. 보통 2의 제곱 단위로 바꾸는 것 같다. 나는 64정도로 설정하였다.
lora_alpha=LORA_ALPHA, # LORA_R의 2배 정도로 넣는다고 한다. 이유는 모름
lora_dropout=LORA_DROPOUT, # 보통 통계적 유의도 판단 기준인 0.05정도로 넣어주는 것 같다.
target_modules=LORA_TARGET_MODULES,
bias="none", # 이건 그냥 'none'으로 두는 게 좋다고 함.
task_type="CAUSAL_LM",
)
print("✅ LoRA 설정 완료")
model = PeftModel.from_pretrained(model, FINAL_MODEL_DIR)
#이 다음은 학습된 데이터를 기존 4bit 모델에 덧입힘
#PEFT 사용
# ----- 중략 -----#
# ----- 4) 프롬프트 형태 설정 -----
if "dataset_text_field" in sig:
trainer_kwargs["dataset_text_field"] = "text"
elif "formatting_func" in sig:
# 사용자 말 - 대답 규칙에 대한 명시
# 모델이 역할극을 하면서 배우도록 한다고 한다..
def formatting_func(example):
text = example.get("text", "")
# 원래 데이터("오늘 뭐 먹지?")를 -> "### User:\n오늘 뭐 먹지?\n\n### Assistant:\n" 형식으로 바꿔줌.
return f"### User:\n{text}\n\n### Assistant:\n"
trainer_kwargs["formatting_func"] = formatting_func
출처) Gemini 고마워 2
튜닝 기법 | 핵심 원리 (어떻게 작동하는가?) | 튜닝 대상 | 학습 파라미터 수 | 주요 장점 | 비유 |
---|---|---|---|---|---|
Full Fine-Tuning (기본) | 모델의 모든 파라미터를 새로운 데이터로 재학습 | 모델 전체 | 매우 많음 (100%) | 최고의 성능을 낼 잠재력 | 엔진 전체 분해 및 재조립 |
Adapter Tuning | 기존 레이어 사이에 작은 신경망(어댑터)을 삽입하고, 이 어댑터만 학습 | 추가된 어댑터 모듈 | 적음 | 원조 PEFT, 안정적인 성능 | 오디오 앰프 사이에 이펙터 연결 |
Prompt/Prefix Tuning | 모델 가중치는 고정하고, 입력 앞에 학습 가능한 '가상 프롬프트'를 붙여 모델을 유도 | 가상 프롬프트 벡터 | 가장 적음 | 최고의 효율성, 모델을 전혀 건드리지 않음 | 뛰어난 배우에게 마법의 대본 쥐여주기 |
(IA)³ | 새로운 가중치를 더하는 대신, 레이어를 통과하는 신호의 세기를 조절하는 법을 학습 | 활성값 스케일링 벡터 | 매우 적음 | 극도의 효율성, 때로는 LoRA보다 우수한 성능 | 오디오 믹서의 볼륨 조절기 돌리기 |
LoRA / QLoRA | 기존 가중치 행렬에 작은 '성능 강화 패치'를 덧붙여, 이 패치만 집중적으로 학습 | 추가된 저차원 행렬 | 적음 | 성능과 효율성의 균형, 현재 가장 널리 쓰임 | 고성능 엔진에 터보차저 장착 |
모범생인 LLM이 벼락치기에서 최대 효율을 낼 리는 없다. 그래서 고딩 때 다 짜본 공부계획표 같이 ㅎㅎ.. 배치 사이즈를 설정한 후, 데이터를 조금씩 주입해서 가스라이팅을 했고, 그 결과를 한 번에 업데이트 하는 방식을 활용하였다.
# ----- 4) 학습 (배치 설정 포함) -----
print("\n▶ 4. 파인튜닝 시작...")
training_arguments = TrainingArguments(
output_dir="./results",
num_train_epochs=TRAIN_EPOCHS,
# 한 번에 GPU에 올릴 데이터 묶음의 크기 정함
### 메모리 설정과정 ###
per_device_train_batch_size=BATCH_SIZE,
# 실제 메모리 대비 더 학습을 많이 한 것 같은 효과(?)
gradient_accumulation_steps=GRAD_ACCUM_STEPS, #필요할 때만 메모리를 사용
gradient_checkpointing=True, #메모리가 찰 것 같으면 안 쓰는 데이터를 삭제
optim="paged_adamw_8bit", # 메모리 부족 오류를 막아줌
### 학습 설정과정 ###
learning_rate=LEARNING_RATE, # 기본값
bf16=True, # 안정성(소수점 위부터 동일하게 맞춘다)
fp16=False, # 정밀성(소수점 아래까지 모두 동일하게 맞춘다)
max_grad_norm=0.3, # Start Point. 그냥 디폴트값이다
warmup_ratio=0.03, # 초반 속도를 낮게 설정하여, 모델이 점점 추론속도를 높이도록
lr_scheduler_type="cosine", # 막바지의 학습 속도를 낮춰주는 쿨다운.인데, 학습이 최대화되는 적당한 안전 포인트를 찾아 줌
# 과정 안전장치
logging_steps=50, #50번에 한 번씩 손실 결과를 보여줌
save_steps=200, #중간결과 저장
save_total_limit=2, #최종적으로 저장된 중간결과 삭제해서 최종 결과 2개만 남기게
report_to="none", #학습기록은 리포트하지 않고 내부저장
load_best_model_at_end=False # 학습 막바지(챗봇은 뒤로 갈 수록 말투 학습이 더 많이 되기 때문에. 만일 TRUE 설정을 하면 최대 학습지점을 찾아 저장)
)
하지만.. 이게 다가 아니라
LLM을 글로만 배운 게 여기서 티가 나기 시작했는데,
앞 편에서 가상환경에 대해 3.11을 썼다고 간단히 말했지만, 가상환경 설정부터가 거의 지옥이었다. 버전 3.11 같아서 가상환경 설치하면 3.12같고 3.12를 설치하면 개같이 똑같은 오류 부활하고의 무한 루프에서 뺑뺑이를 돌았다. 온갖 Gen AI를 조진 결과, 두 가지 원인으로 압축되었다.
Pain Point 1. GPU
파인튜닝부터 무언가 뻑이 나기 시작하길래, 데스크리서치를 해 보니 하드웨어 호환성이라는 것을 고려했어야 한다고 한다. LLM 자체가 엄청난 양의 연산을 처리하기 때문에, 대부분은 이를 병렬적으로 처리할 수 있는 GPU가 필수적이라고 한다. 대반전.. 내 노트북에 그런 게 있을 리가 없었다.
Pain Point 2. 사용하는 LLM과 파이썬 버전, 라이브러리 버전의 호환성을 꼭 확인해 보아야 한다. Python, Cuda Toolkit 버전을 확인해 보아야 하며, 다양한 패키지를 사용하는 만큼, 패키지 간 버전의 호환성도 꼭 살펴보아야 한다. (나 같은 경우에는 효율성을 위해 4-bit 양자화를 사용했는데, 그 과정에 필수적인 패키지인 bitsandbytes가 끝까지 괴롭혔다.)
Pytorch/Tensorflow는 자주 사용하는 패키지인 만큼, 깃허부에 오류를 신고하는 페이지도 별도로 있다.
참고)
https://github.com/pytorch/pytorch/issues
https://github.com/tensorflow/tensorflow/issues
두 번째는 가상환경 설정을 통해 해결이 되었지만, GPU가 되는 컴은 나한테는 없는데 어떻게 구하지(?) 라고 고민했다. 하지만, 생각보다 너무 간단하게 해결이 되었는데, 바로 해결책은 Colab 이었다. 구글갓랩에 바로 코드와 데이터를 업로드하고 두근거리는 마음으로 기다렸는데..
-----------------OutOfMemoryError Traceback (most recent call last)/tmp/ipython-input-2384458034.py in <cell line: 0>() 163 else: 164 print(f"✅ GPU 사용 가능: {torch.cuda.get_device_name(0)}")--> 165 train_my_chatbot()
42 frames
/usr/local/lib/python3.11/dist-packages/bitsandbytes/autograd/_functions.py in forward(ctx, A, B, out, bias, quant_state) 320 # 1. Dequantize 321 # 2. MatmulnN--> 322 output = torch.nn.functional.linear(A, F.dequantize_4bit(B, quant_state).to(A.dtype).t(), bias) 323 324 # 3. Save stateOutOfMemoryError: CUDA out of memory. Tried to allocate 224.00 MiB. GPU 0 has a total capacity of 14.74 GiB of which 124.12 MiB is free. Process 27454 has 14.57 GiB memory in use. Of the allocated memory 13.87 GiB is allocated by PyTorch, and 580.25 MiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation. See documentation for Memory Management (https://pytorch.org/docs/stable/notes/cuda.html#environment-variables)
메모리가 부족하단다. (돈 줘.)
그래서, 결국 나는 9.99불을 지출하고 말았다.
9.99불로 경험할 수 있는 레노버 씽크패드 P1 7세대의 맛 (사실 안써봐서 모름),, 아 맛있다~!!
기존 모델 로드 > 학습 모델 결합 > 모델 조율 순서로 진행
# 베이스 모델 로드
print(f"\n[2/4] 베이스 모델을 로드합니다. (모델: {BASE_MODEL_PATH})")
# 추론 시에는 VRAM이 부족할 경우 CPU로 일부를 오프로딩하도록 설정
max_memory = {0: INFER_GPU_MEM_GIB, "cpu": INFER_CPU_MEM_GIB}
base_model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL_PATH,
quantization_config=bnb_config,
device_map="auto",
max_memory=max_memory,
offload_folder="offload", # 오프로딩 시 사용할 폴더
offload_state_dict=True,
trust_remote_code=True,
)
# LoRA 어댑터 결합 및 토크나이저 로드
print(f"\n[3/4] LoRA 결합. (어댑터: {ADAPTER_PATH})")
model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)
tokenizer = AutoTokenizer.from_pretrained(ADAPTER_PATH, trust_remote_code=True)
# 4. 텍스트 생성 파이프라인 설정 및 대화 시작
print(f"\n[4/4] 챗봇 파이프라인을 준비합니다. 대화를 시작하세요!")
print(" (대화를 종료하려면 'quit' 또는 'exit'을 입력하세요)")
streamer = TextStreamer(tokenizer, skip_prompt=True)
pipe = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
streamer=streamer,
)
# 모델의 답변 패턴 조정
# 시스템 프롬프트: 챗봇에게 역할(페르소나)과 규칙을 부여합니다.
# 이 부분을 수정하여 챗봇의 말투나 성격을 바꿀 수 있습니다.
system_prompt = "너는 나의 친한 친구야. 항상 반말로 솔직하게 대답해 줘."
# 최종 프롬프트 조합: 시스템 프롬프트, 예시, 실제 질문을 모두 합칩니다.
prompt = f"### System:\n{system_prompt}\n\n{few_shot_examples}\n\n### User:\n{user_input}\n\n### Assistant:\n"
# 답변 패턴 조절
_ = pipe(
prompt,
max_new_tokens=256,
do_sample=True,
temperature=0.8, # 예상된, 고정된 답을 하지 않게 함. 값이 클수록 고정된 답변을 하지 않음
repetition_penalty=1.2, # 같은 말 반복 방지 기능 추가
top_p=0.9,
top_k=50,
pad_token_id=tokenizer.eos_token_id,
)
if __name__ == "__main__":
run_chatbot()
코드를 입력하세요
그 결과는,
묘하게 개소리만 뱉어내는데, 더 슬픈 건.. 말투가 비슷하다는 거다.
또라이냐고 물어보니 쌍욕하며 급발진도 한다. 어이구 잘한다 내새끼..
나: 내가 쓴 채팅
바로 아랫줄 아무 표시도 없는 것: 챗봇
다음 편에서는
왜 이런 흉한 것이 나왔는지, 해결을 위해서는 어떻게 해야 하는지, 그리고 허깅페이스 내 챗봇 리포팅(장렬한 실패일기)에 대해 다루도록 하겠다..