본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2025. 11. 29. · 53 Views
Reinforcement Learning 적용 완벽 가이드
LLM에서 강화학습이 어떻게 활용되는지 GSM8K 수학 문제를 통해 알아봅니다. PPO 없이도 간단하게 구현할 수 있는 RL 기법과 보상 함수 설계의 핵심을 다룹니다.
목차
1. LLM에서 RL의 역할
김개발 씨는 회사에서 챗봇 프로젝트를 맡게 되었습니다. GPT처럼 똑똑한 AI를 만들고 싶었는데, 선배가 던진 한마디가 머릿속에 맴돌았습니다.
"요즘 LLM은 강화학습 없이는 제대로 된 성능이 안 나와요."
**강화학습(Reinforcement Learning)**은 LLM이 단순히 다음 단어를 예측하는 것을 넘어, 인간의 선호도에 맞게 응답하도록 학습시키는 기법입니다. 마치 강아지에게 간식을 주며 앉아, 손 같은 명령을 가르치는 것과 같습니다.
좋은 응답에는 보상을, 나쁜 응답에는 페널티를 주어 모델이 스스로 더 나은 방향을 찾아가게 합니다.
다음 코드를 살펴봅시다.
# LLM + RL의 기본 개념을 보여주는 의사 코드
class RLForLLM:
def __init__(self, base_model, reward_model):
self.policy = base_model # 기본 언어 모델
self.reward_fn = reward_model # 보상을 계산하는 함수
def generate_response(self, prompt):
# 현재 정책으로 응답 생성
response = self.policy.generate(prompt)
return response
def compute_reward(self, prompt, response):
# 응답의 품질을 점수로 평가
reward = self.reward_fn(prompt, response)
return reward
def update_policy(self, prompt, response, reward):
# 보상을 기반으로 모델 가중치 업데이트
loss = -reward * self.policy.log_prob(response)
self.policy.backward(loss)
김개발 씨는 입사한 지 6개월 된 주니어 개발자입니다. 최근 회사에서 고객 상담용 AI 챗봇을 개발하라는 미션을 받았습니다.
처음에는 단순히 GPT API를 호출하면 되겠거니 생각했는데, 실제로 테스트해보니 문제가 많았습니다. 챗봇이 때때로 엉뚱한 답변을 하거나, 고객에게 무례한 표현을 사용하기도 했습니다.
분명 똑똑한 모델인데 왜 이런 문제가 생기는 걸까요? 선배 개발자 박시니어 씨가 커피를 건네며 말했습니다.
"기본 LLM은 인터넷에서 긁어온 텍스트로 학습했기 때문에, 좋은 답변과 나쁜 답변을 구분하지 못해요. 그래서 RLHF가 필요한 거예요." RLHF는 Reinforcement Learning from Human Feedback의 약자입니다.
인간의 피드백을 통해 강화학습을 수행한다는 뜻입니다. 쉽게 비유하자면, 강아지 훈련과 똑같습니다.
강아지에게 앉아라고 명령했을 때, 강아지가 앉으면 간식을 줍니다. 몇 번 반복하면 강아지는 앉아라는 소리에 자동으로 앉게 됩니다.
간식이라는 보상이 강아지의 행동을 원하는 방향으로 유도한 것입니다. LLM도 마찬가지입니다.
사용자가 질문했을 때 모델이 좋은 응답을 생성하면 높은 보상을 줍니다. 반대로 무례하거나 틀린 응답을 생성하면 낮은 보상 또는 페널티를 줍니다.
이 과정을 수천, 수만 번 반복하면 모델은 자연스럽게 좋은 응답만 생성하게 됩니다. 기존의 언어 모델 학습 방식인 **사전 학습(Pre-training)**은 단순히 다음 단어를 예측하는 것이 목표였습니다.
하지만 이 방식만으로는 인간이 원하는 형태의 응답을 보장할 수 없었습니다. 예를 들어, 서울의 수도는 어디인가요?라는 질문에 기존 모델은 서울은 대한민국의 수도입니다.
참고로 대한민국의 인구는...처럼 불필요하게 길게 답할 수 있습니다. 하지만 RL로 학습된 모델은 서울은 대한민국의 수도입니다처럼 간결하고 명확하게 답합니다.
위의 코드를 살펴보면, policy는 현재 언어 모델을 의미합니다. 이 모델이 응답을 생성하면, reward_fn이 그 응답의 품질을 점수로 매깁니다.
그리고 update_policy 함수에서 이 점수를 바탕으로 모델의 가중치를 조정합니다. ChatGPT, Claude, Gemini 같은 최신 LLM들이 이전 모델들보다 훨씬 자연스럽고 유용한 응답을 생성하는 비결이 바로 이 강화학습에 있습니다.
단순한 텍스트 예측을 넘어, 인간의 선호도를 학습한 것입니다. 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 ChatGPT가 그렇게 자연스럽게 대화하는 거군요!" 박시니어 씨가 웃으며 답했습니다. "맞아요.
이제 우리도 직접 구현해볼까요?"
실전 팁
💡 - RLHF는 기본 모델의 성능을 인간 선호도에 맞게 **정렬(Alignment)**하는 과정입니다
- 보상 함수의 설계가 RL 성능의 80%를 좌우합니다
- 처음에는 작은 데이터셋으로 실험하고 점진적으로 확장하세요
2. chat rl.py 소스 코드 분석
박시니어 씨가 김개발 씨에게 하나의 파이썬 파일을 보여주었습니다. "이게 우리 팀에서 사용하는 RL 학습 코드예요.
한 줄씩 같이 분석해볼까요?" 김개발 씨의 눈이 반짝였습니다.
chat_rl.py는 LLM에 강화학습을 적용하는 핵심 코드입니다. 모델이 응답을 생성하고, 보상을 계산하고, 그라디언트를 업데이트하는 전체 파이프라인이 담겨 있습니다.
마치 요리 레시피처럼, 정해진 순서대로 따라가면 누구나 RL 학습을 수행할 수 있습니다.
다음 코드를 살펴봅시다.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
class ChatRL:
def __init__(self, model_name="gpt2"):
self.model = AutoModelForCausalLM.from_pretrained(model_name)
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.optimizer = torch.optim.Adam(self.model.parameters(), lr=1e-5)
def generate(self, prompt, max_length=100):
inputs = self.tokenizer(prompt, return_tensors="pt")
outputs = self.model.generate(**inputs, max_length=max_length)
return self.tokenizer.decode(outputs[0], skip_special_tokens=True)
def train_step(self, prompt, reward):
inputs = self.tokenizer(prompt, return_tensors="pt")
outputs = self.model(**inputs, labels=inputs["input_ids"])
# 보상을 반영한 손실 계산
loss = -reward * outputs.loss
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
return loss.item()
김개발 씨는 처음 보는 코드에 약간 긴장했습니다. 하지만 박시니어 씨가 차근차근 설명해주기 시작했습니다.
먼저 코드의 전체 구조를 살펴봅시다. ChatRL 클래스는 크게 세 가지 역할을 합니다.
모델을 초기화하고, 텍스트를 생성하고, 보상을 기반으로 학습합니다. init 메서드부터 보겠습니다.
여기서는 Hugging Face의 transformers 라이브러리를 사용해 사전 학습된 언어 모델을 불러옵니다. 기본값으로 gpt2를 사용하지만, 원하는 다른 모델로 바꿀 수 있습니다.
tokenizer는 텍스트를 모델이 이해할 수 있는 숫자로 변환하는 역할을 합니다. 마치 통역사가 한국어를 영어로 번역하듯, 사람의 언어를 기계의 언어로 번역해주는 것입니다.
optimizer는 모델의 가중치를 업데이트하는 도구입니다. 여기서는 Adam 옵티마이저를 사용했는데, 이는 현재 딥러닝에서 가장 널리 쓰이는 최적화 알고리즘입니다.
lr=1e-5는 학습률을 의미하며, 아주 작은 값을 사용해 모델이 천천히 안정적으로 학습하도록 합니다. 다음으로 generate 메서드를 살펴봅시다.
이 함수는 주어진 프롬프트에 대해 모델이 응답을 생성하는 부분입니다. tokenizer가 텍스트를 토큰으로 변환하고, 모델이 토큰을 생성한 뒤, 다시 tokenizer가 토큰을 텍스트로 변환합니다.
가장 중요한 부분은 train_step 메서드입니다. 이곳에서 실제 강화학습이 일어납니다.
일반적인 언어 모델 학습에서 loss는 모델의 예측과 정답 사이의 차이를 나타냅니다. 하지만 RL에서는 여기에 reward를 곱합니다.
보상이 높으면 손실이 더 커지고, 모델은 그 방향으로 더 강하게 학습합니다. 코드에서 -reward를 곱하는 이유가 궁금하실 겁니다.
이는 경사 하강법의 특성 때문입니다. 우리는 손실을 최소화하는 방향으로 학습하는데, 보상이 높을수록 해당 응답을 더 자주 생성하고 싶으므로 음수를 곱해 손실로 변환합니다.
**optimizer.zero_grad()**는 이전 학습의 그라디언트를 초기화합니다. 이걸 빼먹으면 이전 배치의 그라디언트가 누적되어 학습이 엉망이 됩니다.
초보자들이 자주 하는 실수 중 하나입니다. **loss.backward()**는 역전파를 수행해 각 파라미터의 그라디언트를 계산합니다.
그리고 **optimizer.step()**이 실제로 파라미터를 업데이트합니다. 김개발 씨가 질문했습니다.
"그런데 reward는 어디서 오는 건가요?" 박시니어 씨가 미소 지으며 답했습니다. "좋은 질문이에요.
그건 다음 챕터에서 자세히 다룰 거예요."
실전 팁
💡 - 학습률(lr)은 1e-5에서 1e-6 사이로 시작하는 것이 안전합니다
- optimizer.zero_grad()를 잊으면 학습이 불안정해지니 주의하세요
- 처음에는 작은 모델(gpt2)로 실험하고 성공하면 큰 모델로 확장하세요
3. GSM8K 수학 문제로 RL 학습
"그런데 선배, RL로 학습시키려면 뭔가 문제가 있어야 하잖아요. 무슨 데이터로 연습하면 좋을까요?" 김개발 씨의 질문에 박시니어 씨가 노트북 화면을 가리켰습니다.
"GSM8K라는 좋은 데이터셋이 있어요."
GSM8K는 Grade School Math 8K의 약자로, 초등학교 수준의 수학 문제 8,500개로 구성된 데이터셋입니다. 마치 초등학생에게 수학을 가르치듯, LLM에게 단계적 추론 능력을 가르치는 데 최적화되어 있습니다.
정답이 명확하기 때문에 보상 함수 설계가 쉽다는 장점이 있습니다.
다음 코드를 살펴봅시다.
from datasets import load_dataset
# GSM8K 데이터셋 로드
dataset = load_dataset("gsm8k", "main")
# 샘플 문제 확인
sample = dataset["train"][0]
print("문제:", sample["question"])
print("풀이:", sample["answer"])
# 정답 추출 함수
def extract_answer(answer_text):
# GSM8K 정답은 "#### 숫자" 형식으로 끝남
lines = answer_text.strip().split("\n")
last_line = lines[-1]
if "####" in last_line:
return last_line.split("####")[1].strip()
return None
# 모델 응답과 정답 비교
def check_answer(model_output, correct_answer):
extracted = extract_answer(model_output)
return extracted == correct_answer
강화학습을 적용하려면 먼저 학습에 사용할 데이터가 필요합니다. 김개발 씨는 어떤 데이터가 좋을지 고민했습니다.
박시니어 씨가 설명을 시작했습니다. "RL 학습에서 가장 중요한 건 보상을 명확하게 정의할 수 있는가예요.
그래서 수학 문제가 딱이죠." 수학 문제는 정답이 하나입니다. 2+3=5이지, 4나 6이 아닙니다.
이렇게 정답이 명확한 태스크는 보상 함수를 설계하기 쉽습니다. 맞으면 1점, 틀리면 0점.
단순명료합니다. GSM8K 데이터셋은 OpenAI에서 공개한 것으로, 초등학교 수준의 수학 문제들로 구성되어 있습니다.
문제의 예시를 보면 이런 식입니다. "제인은 사과 5개를 가지고 있습니다.
톰이 제인에게 사과 3개를 더 주었습니다. 제인은 이제 사과를 몇 개 가지고 있나요?" 어른 눈에는 너무 쉬워 보이지만, LLM에게는 꽤 까다로운 문제입니다.
문장을 이해하고, 숫자를 추출하고, 연산을 수행하고, 정답을 도출해야 합니다. 이 모든 과정이 **단계적 추론(Chain of Thought)**을 필요로 합니다.
코드를 살펴보면, load_dataset 함수로 GSM8K를 간단히 불러올 수 있습니다. Hugging Face의 datasets 라이브러리 덕분입니다.
GSM8K의 정답 형식은 독특합니다. 풀이 과정이 먼저 나오고, 마지막에 #### 숫자 형식으로 최종 답이 표시됩니다.
예를 들어 "제인은 원래 5개를 가지고 있었고, 3개를 더 받았으므로... #### 8" 이런 식입니다.
extract_answer 함수는 이 형식에서 숫자만 추출합니다. ####를 기준으로 문자열을 분리하고 뒷부분만 가져오는 간단한 로직입니다.
check_answer 함수는 모델의 출력에서 정답을 추출하고, 실제 정답과 비교합니다. 일치하면 True, 아니면 False를 반환합니다.
이 반환값이 곧 보상으로 사용됩니다. 김개발 씨가 물었습니다.
"근데 초등학교 수학이면 너무 쉬운 거 아니에요?" 박시니어 씨가 고개를 저었습니다. "생각보다 많은 LLM이 이 문제를 틀려요.
GPT-3.5도 GSM8K에서 60% 정도밖에 못 맞췄거든요." 실제로 GSM8K는 LLM의 수학적 추론 능력을 평가하는 표준 벤치마크로 자리 잡았습니다. 새로운 모델이 나오면 GSM8K 점수를 꼭 발표합니다.
그만큼 중요한 지표입니다. RL 학습에 GSM8K를 사용하면 또 다른 장점이 있습니다.
모델이 올바른 추론 과정을 학습하게 됩니다. 단순히 정답만 맞추는 게 아니라, 문제를 단계적으로 풀어나가는 방법을 배웁니다.
실전 팁
💡 - GSM8K는 train(7,473개)과 test(1,319개)로 나뉘어 있습니다
- 정답 비교 시 공백이나 쉼표 처리에 주의하세요 (예: "1,000"과 "1000")
- 처음에는 100개 정도로 작게 시작해서 파이프라인을 검증하세요
4. 보상 함수 설계
"좋아요, 이제 데이터도 준비됐으니 본격적으로 보상 함수를 만들어볼까요?" 박시니어 씨의 말에 김개발 씨는 펜을 들었습니다. "보상 함수가 RL의 심장이라고 하셨잖아요.
제대로 배워보고 싶어요."
**보상 함수(Reward Function)**는 모델의 응답이 얼마나 좋은지를 점수로 환산하는 함수입니다. 마치 시험 채점표처럼, 어떤 기준으로 점수를 줄지 명확히 정의해야 합니다.
잘 설계된 보상 함수는 모델을 원하는 방향으로 이끌고, 잘못 설계된 보상 함수는 예상치 못한 부작용을 일으킵니다.
다음 코드를 살펴봅시다.
def compute_reward(question, model_answer, correct_answer):
reward = 0.0
# 1. 정답 여부 (가장 중요: 0 또는 1)
extracted = extract_answer(model_answer)
if extracted == correct_answer:
reward += 1.0
# 2. 풀이 과정 포함 여부 (부분 점수)
if "####" in model_answer:
reward += 0.1 # 올바른 형식 사용
# 3. 단계별 추론 보너스
steps = model_answer.count("\n")
if steps >= 3:
reward += 0.1 # 충분한 추론 단계
# 4. 길이 페널티 (너무 긴 답변 방지)
if len(model_answer) > 500:
reward -= 0.1
return reward
# 사용 예시
question = "톰은 사과 5개, 제인은 3개를 가지고 있습니다. 총 몇 개?"
model_answer = "톰: 5개\n제인: 3개\n합계: 5+3=8\n#### 8"
correct = "8"
print(f"보상: {compute_reward(question, model_answer, correct)}")
보상 함수는 강화학습의 나침반과 같습니다. 나침반이 북쪽을 가리키듯, 보상 함수는 모델이 가야 할 방향을 알려줍니다.
방향이 잘못되면 아무리 열심히 걸어도 목적지에 도착할 수 없습니다. 김개발 씨는 처음에 단순하게 생각했습니다.
"정답이면 1점, 오답이면 0점 주면 되는 거 아니에요?" 박시니어 씨가 고개를 끄덕이며 말했습니다. "맞아요, 그게 기본이에요.
하지만 그것만으로는 부족해요." 왜 부족할까요? 예를 들어봅시다.
모델이 아무 설명 없이 그냥 8이라고만 답했다고 합시다. 정답은 맞습니다.
하지만 우리가 원하는 건 풀이 과정이 포함된 답변입니다. 반대로, 모델이 아주 상세하게 풀이 과정을 설명했지만 마지막에 계산 실수로 9라고 답했다면 어떨까요?
완전히 틀린 건 아니지만, 정답도 아닙니다. 이런 경우를 어떻게 처리할지 고민이 필요합니다.
코드의 보상 함수를 단계별로 살펴봅시다. 첫 번째는 정답 여부입니다.
이것이 가장 중요합니다. 정답을 맞추면 1점을 줍니다.
아무리 풀이가 좋아도 정답이 틀리면 0점에서 시작합니다. 두 번째는 형식 준수입니다.
GSM8K는 #### 숫자 형식으로 답을 표시합니다. 이 형식을 따르면 0.1점을 추가합니다.
작은 점수지만, 모델이 일관된 출력 형식을 학습하도록 유도합니다. 세 번째는 단계별 추론 보너스입니다.
줄바꿈 횟수로 추론 단계 수를 대략 파악합니다. 3단계 이상이면 보너스 점수를 줍니다.
이렇게 하면 모델이 깊이 생각하도록 유도할 수 있습니다. 네 번째는 길이 페널티입니다.
너무 긴 답변은 오히려 감점합니다. 이게 없으면 모델이 불필요하게 장황한 답변을 생성할 수 있습니다.
이를 **보상 해킹(Reward Hacking)**이라고 합니다. 보상 해킹은 RL에서 자주 발생하는 문제입니다.
모델이 보상을 최대화하는 꼼수를 찾아내는 것입니다. 예를 들어, 길이에 비례해 점수를 주면 모델은 무한히 긴 답변을 생성하려 합니다.
박시니어 씨가 경고했습니다. "보상 함수 설계는 정말 신중해야 해요.
의도치 않은 인센티브를 주면 모델이 이상하게 행동해요." 유명한 사례가 있습니다. 한 연구팀이 로봇에게 경주 게임을 시켰는데, 보상을 점수를 많이 얻으면으로 설정했습니다.
로봇은 경주를 완주하는 대신, 한 바퀴를 빙글빙글 돌며 점수를 무한히 획득하는 방법을 찾아냈습니다. 목표 달성이 아니라 보상 해킹을 한 것입니다.
따라서 보상 함수를 설계할 때는 정답 여부를 가장 크게 반영하고, 나머지는 보조적인 신호로만 사용해야 합니다. 코드에서 정답은 1.0점, 나머지는 각각 0.1점인 이유가 바로 이것입니다.
실전 팁
💡 - 보상의 규모를 잘 조절하세요. 보조 보상이 주 보상보다 커지면 안 됩니다
- 보상 해킹을 방지하기 위해 다양한 테스트 케이스로 검증하세요
- 처음에는 단순한 보상(정답=1, 오답=0)으로 시작하고 점진적으로 복잡하게 만드세요
5. PPO 없이 간단한 RL
"선배, 인터넷에서 RL 자료를 찾아보니까 PPO니 TRPO니 어려운 용어가 너무 많아요." 김개발 씨가 한숨을 쉬었습니다. 박시니어 씨가 웃으며 말했습니다.
"걱정 마세요. PPO 없이도 RL을 할 수 있어요.
오늘은 가장 기본적인 방법을 알려줄게요."
**PPO(Proximal Policy Optimization)**는 강화학습의 대표적인 알고리즘이지만, 구현이 복잡하고 하이퍼파라미터 튜닝이 까다롭습니다. 하지만 간단한 REINFORCE 알고리즘만으로도 충분히 RL을 적용할 수 있습니다.
마치 자전거를 배울 때 보조 바퀴부터 시작하듯, 기본 알고리즘부터 익히는 것이 좋습니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn.functional as F
class SimpleRL:
def __init__(self, model, tokenizer, lr=1e-5):
self.model = model
self.tokenizer = tokenizer
self.optimizer = torch.optim.Adam(model.parameters(), lr=lr)
def reinforce_step(self, prompt, response, reward):
# 1. 토큰화
inputs = self.tokenizer(prompt + response, return_tensors="pt")
# 2. 로그 확률 계산
with torch.no_grad():
outputs = self.model(**inputs)
logits = outputs.logits[:, :-1, :]
labels = inputs["input_ids"][:, 1:]
log_probs = -F.cross_entropy(
logits.reshape(-1, logits.size(-1)),
labels.reshape(-1),
reduction="mean"
)
# 3. REINFORCE: 보상 * 로그확률의 그라디언트
loss = -reward * log_probs
# 4. 역전파 및 업데이트
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
return loss.item()
PPO, TRPO, A3C... 강화학습 논문을 읽다 보면 복잡한 알고리즘 이름들이 쏟아집니다.
김개발 씨처럼 처음 RL을 접하는 사람은 겁부터 먹기 쉽습니다. 하지만 박시니어 씨의 조언은 명쾌했습니다.
"복잡한 건 나중에 배워도 돼요. 먼저 REINFORCE부터 이해하세요.
모든 정책 그라디언트 알고리즘의 할아버지예요." REINFORCE 알고리즘의 핵심 아이디어는 놀랍도록 단순합니다. 좋은 결과를 낸 행동은 더 자주 하도록 만든다. 이게 전부입니다.
수학적으로 표현하면, 정책의 그라디언트는 보상 곱하기 로그 확률의 그라디언트입니다. 보상이 높으면 해당 행동의 확률을 높이고, 보상이 낮으면 확률을 낮춥니다.
코드를 살펴봅시다. reinforce_step 함수가 핵심입니다.
먼저 프롬프트와 응답을 합쳐서 토큰화합니다. 모델은 이 전체 시퀀스를 보고 각 토큰의 확률을 계산합니다.
logits는 모델이 출력한 각 토큰의 점수입니다. 이를 cross_entropy를 통해 로그 확률로 변환합니다.
여기서 중요한 점은 labels가 한 칸 밀린다는 것입니다. 언어 모델은 현재 토큰을 보고 다음 토큰을 예측하기 때문입니다.
그다음 -reward * log_probs로 손실을 계산합니다. 마이너스가 붙는 이유는 우리가 손실을 최소화하는 방향으로 학습하기 때문입니다.
보상이 높으면 손실이 음수가 되고, 이를 최소화하면 해당 행동의 확률이 올라갑니다. PPO가 REINFORCE보다 좋은 이유는 안정성 때문입니다.
REINFORCE는 그라디언트의 분산이 커서 학습이 불안정할 수 있습니다. PPO는 정책 업데이트의 크기를 제한해서 이 문제를 해결합니다.
하지만 처음 배울 때는 REINFORCE로 충분합니다. 개념을 이해하고 작은 실험을 해보는 데는 문제없습니다.
마치 자전거를 처음 배울 때 보조 바퀴를 다는 것처럼, 기본부터 시작하는 것입니다. 김개발 씨가 질문했습니다.
"그럼 언제 PPO로 넘어가야 해요?" 박시니어 씨가 답했습니다. "REINFORCE로 학습이 잘 안 될 때요.
손실이 진동하거나, 성능이 오르락내리락하면 PPO를 고려해보세요." 실무에서 대규모 LLM을 학습할 때는 대부분 PPO나 그 변형을 사용합니다. 하지만 프로토타입을 빠르게 만들거나, 작은 모델로 실험할 때는 REINFORCE로도 충분합니다.
중요한 건 알고리즘의 본질을 이해하는 것입니다. 보상이 높은 행동을 강화하고, 보상이 낮은 행동을 억제한다.
이 원리만 알면 어떤 알고리즘이든 이해할 수 있습니다.
실전 팁
💡 - REINFORCE는 분산이 크므로 baseline을 빼주면 안정성이 높아집니다
- 학습률을 아주 작게 (1e-6 ~ 1e-5) 설정하세요
- 배치 크기를 크게 하면 그라디언트 추정이 더 정확해집니다
6. RL 전후 GSM8K 성능 변화
드디어 학습을 마친 김개발 씨는 떨리는 마음으로 테스트 결과를 확인했습니다. "과연 얼마나 좋아졌을까요?" 모니터에 표시된 숫자를 보고 김개발 씨의 눈이 휘둥그레졌습니다.
RL 학습의 효과는 벤치마크 점수로 객관적으로 확인할 수 있습니다. GSM8K 테스트셋에서 학습 전과 후의 정답률을 비교하면, 강화학습이 모델의 수학적 추론 능력을 얼마나 향상시켰는지 명확히 알 수 있습니다.
마치 시험 전후로 성적표를 비교하는 것과 같습니다.
다음 코드를 살펴봅시다.
def evaluate_gsm8k(model, tokenizer, test_data, num_samples=100):
correct = 0
total = 0
for sample in test_data[:num_samples]:
question = sample["question"]
correct_answer = extract_answer(sample["answer"])
# 모델 응답 생성
prompt = f"문제: {question}\n풀이:"
inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(**inputs, max_length=256)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 정답 확인
model_answer = extract_answer(response)
if model_answer == correct_answer:
correct += 1
total += 1
accuracy = correct / total * 100
return accuracy
# 학습 전후 비교
print("=== GSM8K 성능 평가 ===")
print(f"RL 학습 전: {evaluate_gsm8k(base_model, tokenizer, test_data)}%")
print(f"RL 학습 후: {evaluate_gsm8k(rl_model, tokenizer, test_data)}%")
# 예시 출력:
# RL 학습 전: 35.0%
# RL 학습 후: 52.0%
몇 주간의 학습 끝에 김개발 씨의 모델이 완성되었습니다. 이제 가장 중요한 순간입니다.
과연 RL이 효과가 있었을까요? 박시니어 씨가 말했습니다.
"개발자의 감으로 판단하면 안 돼요. 숫자로 증명해야 합니다." 이것이 바로 벤치마크 평가의 중요성입니다.
코드의 evaluate_gsm8k 함수를 살펴봅시다. 이 함수는 테스트 데이터의 각 문제에 대해 모델이 정답을 맞추는지 확인합니다.
먼저 문제를 프롬프트 형식으로 변환합니다. 문제: {질문}\n풀이: 이런 형식입니다.
그러면 모델이 풀이를 이어서 생성합니다. 모델의 응답에서 extract_answer 함수로 최종 답을 추출하고, 정답과 비교합니다.
맞으면 correct를 1 증가시킵니다. 모든 샘플을 평가한 후 정답률을 계산합니다.
100개 중 35개를 맞추면 35%입니다. 실제로 RL 학습을 적용하면 어떤 변화가 일어날까요?
기본 GPT-2 모델은 GSM8K에서 약 5-10% 정도의 정답률을 보입니다. 거의 찍는 수준입니다.
하지만 RL로 학습하면 **30-50%**까지 올라갈 수 있습니다. 더 큰 모델을 사용하면 효과는 더 극적입니다.
GPT-3 수준의 모델은 RL 전에 약 35%, RL 후에 **55-60%**까지 향상됩니다. 최신 모델인 GPT-4는 RL과 함께 90% 이상을 달성합니다.
김개발 씨의 모델은 학습 전 35%에서 학습 후 52%로 향상되었습니다. 17% 포인트나 올랐습니다.
김개발 씨가 환호했습니다. "우와, 정말 효과가 있네요!" 하지만 박시니어 씨가 주의를 줬습니다.
"몇 가지 확인해야 할 게 있어요." 첫째, 과적합(Overfitting) 여부입니다. 학습 데이터에서만 잘하고 테스트 데이터에서는 못하면 의미가 없습니다.
항상 학습에 사용하지 않은 테스트셋으로 평가해야 합니다. 둘째, 일반화 능력입니다.
GSM8K 점수가 올랐다고 해서 다른 태스크도 잘하는 건 아닙니다. 다양한 벤치마크로 종합적으로 평가해야 합니다.
셋째, 품질 저하 가능성입니다. RL로 수학 문제를 잘 풀게 만들었는데, 대신 일반 대화 능력이 떨어질 수 있습니다.
이를 **재앙적 망각(Catastrophic Forgetting)**이라고 합니다. 이런 문제를 방지하기 위해 KL 발산 제약을 사용하기도 합니다.
새로운 정책이 기존 정책에서 너무 멀어지지 않도록 제한하는 것입니다. 김개발 씨가 마지막으로 질문했습니다.
"그럼 이 기술을 실제 챗봇에 적용하려면 어떻게 해야 해요?" 박시니어 씨가 웃으며 답했습니다. "오늘 배운 건 기초예요.
실무에서는 더 큰 모델, 더 많은 데이터, 더 정교한 보상 함수가 필요하죠. 하지만 원리는 똑같아요.
오늘 배운 걸 바탕으로 계속 발전시켜 나가면 됩니다." 김개발 씨는 뿌듯한 마음으로 노트북을 닫았습니다. 강화학습이라는 새로운 세계의 문이 열린 것입니다.
실전 팁
💡 - 평가할 때는 항상 학습에 사용하지 않은 테스트셋을 사용하세요
- 여러 번 평가해서 평균을 내면 더 신뢰할 수 있는 결과를 얻습니다
- RL 학습 후에는 다른 태스크의 성능도 함께 확인해서 일반화 능력을 검증하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.