목차
0. 들어가며
1. 모델 간 성능 비교를 위한 기준 설정
2. 베이스라인부터 전통 머신러닝까지
3. 딥러닝 - 신경망으로 한 단계 더
4. 학습 없이 이긴다 - 제로샷 프론티어 모델
5. 프론티어 모델을 직접 파인튜닝해보다
6. 의외의 결과, 그리고 그게 알려주는 것
7. 마무리 - 전체 성적표와 다음 이야기
0. 들어가며
지난 글에서는 Week 6 프로젝트 - 상품 설명 기반 가격 예측 (THE PRICE IS RIGHT)의 앞부분 데이터 전처리를 다뤘다.
아마존 상품 데이터를 긁어와 정제하고, 학습 80만 / 검증 1만 / 테스트 1만 건의 데이터셋을 만들어 허깅페이스 허브에 올리는 데까지가 지난 글의 내용이었다.
이번 글은 정제한 데이터를 기반으로
(1) 전통 머신러닝부터
(2) 딥러닝과 제로샷 프론티어 모델
(3) 프론티어 모델을 직접 파인튜닝하는 해
차례로 가격 예측을 돌려본다.
강의 Week 6의 Day 3-5에 해당한다.
미리 말하면, 이 글에는 작은 반전이 하나 있다.
"프론티어 모델을 내 데이터로 파인튜닝하면 더 좋아지겠지"라는 기대가 보기 좋게 빗나간다.
그리고 그 빗나감이 다음 Week 7(오픈소스 모델 QLoRA 파인튜닝)으로 넘어가는 가장 중요한 동기가 된다.
1. 모델 간 성능 비교를 위한 기준 설정
여러 모델을 비교하려면, "무엇으로 성능을 비교할 것인가"부터 정해야 한다.
Day 3은 모델을 돌리는 코드 보다 평가 체계(evaluation) 부터 시작한다.
1-1) 세 가지 지표 - Error, MSE, R²
이 프로젝트는 가격을 맞히는 회귀 문제라, 세 가지 지표를 같이 본다.
먼저
1. Error
곧 평균 절대 오차(MAE)다.
average_error = sum(errors) / size
# errors = abs(예측값 - 실제값)
"평균적으로 얼마나 틀렸나"를 그대로 보여주는, 가장 직관적인 지표다.
측정 단위가 달러($)라 Error: $42.50이면 "평균적으로 실제 가격과 42.50달러 차이로 예측했다"는 뜻이고, 낮을수록 좋다.
이 글의 모든 모델은 이 MAE 하나로 줄 세운다.
2. MSE(평균 제곱 오차)
mse = mean_squared_error(truths, guesses)
# = 평균((예측값 - 실제값)^2)
오차를 제곱해서 평균낸 값이라, 크게 틀린 예측에 훨씬 큰 패널티를 준다(오차 100달러면 10,000점).
단위가 달러 제곱이라 직관적이진 않지만, 모델 사이의 미세한 차이를 비교할 때 쓸모가 있다.
3. R²(결정계수)
한마디로 설명하면 "이 모델이 그냥 평균값만 답하는 것보다 얼마나 더 나은가"다.
(대학교 때 배웠던 기초 통계학 부분을 다시 공부하는 느낌이었다)
R² = 1 - (내 모델의 오차 합) / (평균만 쓸 때의 오차 합)
- 100%: 완벽한 예측
- 0%: 그냥 전체 평균을 답하는 것과 똑같음
- 음수: 평균을 답하는 것보다도 못함
가격 예측에서 가장 게으른 모델은 "모든 상품 = 전체 평균 가격"이라고 답하는 것인데,
R²는 그 게으른 기준선 대비 내 모델이 얼마나 더 설명력이 있는지를 0-100%로 환산해준다.
한 가지 주의할 점도 있다.
R²가 높아도 Error(달러)가 크면 실제로는 별로일 수 있다.
평균 가격이 500달러인 데이터에서 R² 80%인데 Error가 150달러라면, 설명력은 그럴듯해도 실용적으로는 여전히 크게 틀리는 것이다. 그래서 Error("얼마나 틀리냐")와 R²("얼마나 이해하냐")를 같이 봐야 한다.
1-2) 색상으로 한눈에 - 평가 하베스트
강의에서 제공한 Tester(평가 하베스트)는 예측 하나하나를 색으로 칠해 보여준다. 기준은 이렇다.
- 초록: 오차 40달러 미만 또는 상대 오차 20% 미만
- 주황: 오차 80달러 미만 또는 상대 오차 40% 미만
- 빨강: 그 이상
절대 오차와 상대 오차를 OR로 묶은 게 포인트다.
1,000달러짜리 상품을 950달러로 맞히면 절대 오차는 50달러라 초록은 아니지만 상대 오차 5%라 초록이 된다.
비싼 상품과 싼 상품을 같은 잣대로 보지 않으려는 장치다.
그리고 결과를 산점도로 그린다. x축은 실제 가격, y축은 예측 가격이고, 파란 대각선(y=x)에 점이 가까울수록 정확한 예측이다.

느낀 점
- 각 모델을 돌리기전 "성능 비교를 위한 기준"부터 코드로 고정해두니, 이후 모든 모델이 같은 무대 위에서 비교됐다. 에이전트 개발, ai 기능 개발과저에서도 결국 뭐 하나를 바꿨을 때 이런 정량 성능이 어떻게 변했냐를 근거로 변경을 가져가는게 정말 중요하다고 생각한다.
- 지표 하나만 보면 속는다는 게 인상적이었다. R²(설명력)와 Error(실제 오차 금액)를 함께 봐야 한다는 건, 데이터 특성에 따라 한가지 지표가 아니라 여러 지표를 보조해서 봐야할 수 있다는 점을 느끼게 해줬다.
2. 베이스라인부터 전통 머신러닝까지
본격적인 모델 돌리기는 전통 머신러닝부터 시작한다. (대학교 때 처음 머신러닝 공부할때 돌렸던 모델들이 많이 나와 반가웠다 ㅎ)
강의에서 짚은 머신러닝의 핵심 개념이 하나 있다.
목표는 학습 데이터를 잘 외우는 게 아니라 일반화(generalize), 곧 처음 보는 데이터에서도 잘 맞히는 것이다.
그 반대편에 있는 게 과적합(overfitting)으로, 학습 데이터에만 딱 맞춰져 새 데이터에서는 엉망이 되는 상태다.
이 일반화-과적합 구도는 뒤에 나올 딥러닝과 LLM 모델을 돌릴때도 똑같이 적용된다.
2-1) 가장 게으른 출발점 - Random과 Constant
제일 먼저 만드는 건 "이것보다 못하면 안 되는" 바닥 기준(베이스라인)이다.
def random_pricer(item):
return random.randrange(1, 1000)
1달러부터 999달러 사이를 그냥 무작위로 던지는 모델이다. 당연히 평균 오차가 크다(약 382달러).

조금 나은 게 Constant pricer로, 모든 상품에 학습 데이터의 평균 가격 하나만 답한다.
training_prices = [item.price for item in train]
training_average = sum(training_prices) / len(training_prices) # 140.57
def constant_pricer(item):
return training_average
학습 데이터 평균은 140.57달러였다. 어떤 상품이 와도 140.57달러라고만 답하는 모델인데,
이게 의외로 무시 못 할 기준선이다.
앞으로 만들 모델이 이 게으른 상수 모델(전체 가격 평균만 외치는 모델) 조차 못 이기면 의미가 없다.

2-2) 선형 회귀(Linear Regression) - 단순 피처 3개로는 어림없다
이제 본격적으로 머신러닝 학습 부터 시켜본다.
상품마다 피처(입력 정보) 3개를 뽑았다.
무게(weight), 무게 정보가 없는지 여부(weight_unknown), 설명 텍스트 길이(text_length)다.
(무게가 0이면 정보 없음으로 간주해 별도 플래그 weight_unknown 을 둠)
np.random.seed(42)
# Separate features and target
feature_columns = ['weight', 'weight_unknown', 'text_length']
X_train = train_df[feature_columns]
y_train = train_df['price']
X_test = test_df[feature_columns]
y_test = test_df['price']
# Train a Linear Regression
model = LinearRegression()
model.fit(X_train, y_train)
for feature, coef in zip(feature_columns, model.coef_):
print(f"{feature}: {coef}")
print(f"Intercept: {model.intercept_}")
# Predict the test set and evaluate
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"Mean Squared Error: {mse}")
print(f"R-squared Score: {r2}")
선형 회귀는 가격 = (weight × a) + (weight_unknown × b) + (text_length × c) + 절편 형태의 수식에서 a, b, c를 데이터로부터 찾아낸다. 결과는 이랬다.
출력:
weight: 0.449 # 무게 1단위 오르면 +$0.45
weight_unknown: -6.63 # 무게 정보 없으면 -$6.63
text_length: 0.247 # 설명 1글자 늘면 +$0.25
Intercept: 51.11
Mean Squared Error: 25615.84
R-squared Score: -0.0595
R²가 음수다. 앞에서 말한 그 "평균만 답하는 것(컨스턴트 프라이서)보다도 못한 모델"이라는 뜻이다.
이유는 단순하다. 텍스트의 길이는 알아도 내용을 모른다.
"TV"인지 "볼펜"인지 구분하지 못하고, 무게가 같아도 금붙이와 플라스틱 장난감을 똑같이 본다.
이 3개 숫자는 가격과 거의 상관이 없었던 것이다.
2-3) NLP를 끼얹다 - 단어를 피처로
그래서 설명문 텍스트의 내용 자체를 피처로 넣는다.
CountVectorizer로 상품 설명을 단어 빈도 벡터로 바꾸는, 이른바 Bag of Words 방식이다.
vectorizer = CountVectorizer(max_features=2000, stop_words='english')
X = vectorizer.fit_transform(documents)
"the", "a" 같은 의미 없는 단어(stop words)는 빼고, 가장 자주 등장하는 단어 2000개만 골라 각 상품을 "이 2000개 단어가 각각 몇 번 나왔나"라는 2000칸짜리 숫자 배열(벡터)로 만든다. 그리고 똑같은 선형 회귀를 돌린다.
regressor = LinearRegression()
regressor.fit(X, prices)
모델 종류는 2-2와 똑같은 선형 회귀인데, 피처를 "숫자 3개"에서 "각 단어가 몇번 나왔는지 2000차원 벡터"로 바꿨을 뿐이다. 그
런데 성능이 확 올라간다. "leather", "luxury", "USB" 같은 단어가 가격과 훨씬 관련이 깊기 때문이다.
같은 머신러닝 알고리즘(선형 회귀)이라도 무엇을 먹이느냐가 성능을 가른다는 걸 가장 선명하게 보여준 대목이었다.

2-4) 앙상블 - Random Forest와 XGBoost
마지막은 결정 트리 여러 개를 묶는 앙상블 두 가지다. 둘 다 트리를 쓰지만 만드는 방식이 정반대다.
Random Forest는 결정 트리 100개를 동시에 만들고, 각 트리가 데이터와 단어를 무작위로 다르게 골라 학습한 뒤 100개 예측의 평균을 낸다. 여럿이 따로 공부하고 투표하는 셈이라, 트리 하나일 때의 과적합을 줄여준다.
rf_model = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=4)
rf_model.fit(X[:15000], prices[:15000]) # 속도 때문에 1.5만개만
def random_forest(item):
x = vectorizer.transform([item.summary])
return max(0, rf_model.predict(x)[0])

XGBoost는 트리를 순서대로 만든다.
새 트리가 앞선 트리들이 틀린 오차를 집중적으로 보정하는 부스팅(boosting) 방식이고, 이 보정을 경사 하강법(gradient descent)으로 한 걸음씩 내려간다. (랜덤 포레스트 보다 빠르고 일반화도 잘됨)
xgb_model = xgb.XGBRegressor(n_estimators=1000, learning_rate=0.1, n_jobs=4, random_state=42)
xgb_model.fit(X, prices) # 전체 80만개 사용
def xg_boost(item):
x = vectorizer.transform([item.summary])
return max(0, xgb_model.predict(x)[0])
learning_rate=0.1은 한 트리가 오차를 한 번에 10%씩만 보정한다는 뜻이고, 그래서 트리가 1000개나 필요하다. 조금씩 여러 번 고쳐 수렴하는 구조다. 결과적으로 XGBoost가 전통 머신러닝 중에서는 가장 좋은 성능을 냈다.

정리하면 전통 ML 안에서도 선형 회귀(숫자 3개) → 선형 회귀(NLP) → Random Forest → XGBoost 순으로 또렷하게 좋아졌다.
핵심은 "어떤 알고리즘이냐"만큼이나 "어떤 피처를 주느냐"가 성능을 좌우한다는 점이다.
강사는 전통 ML이 옛날 기술이 아니라 특징이 명확한 문제에선 지금도 산업에서 활발히 쓰인다는 메세지를 했다.
실제로 광고 쪽에서도 여러 추천 모델을 비롯한 다양한 모델들을 전통 ML로 아직까지 많이 하고 있으니까
특징이 명확한 문제에선 ML 도 여전히 좋은 선택이라고 생각한다.
느낀 점
- 가장 게으른 Constant 모델을 기준선으로 깔고 시작하는 구조가 좋았다. 새 기능을 붙일 때도 "아무것도 안 한 상태" 혹은 "지금 운영 중인 단순 로직" 대비 얼마나 나아졌는지를 항상 기준선으로 두고 비교해야 과장 없이 가치를 판단할 수 있다고 느꼈다.
- 같은 선형 회귀 알고리즘인데 피처만 바꿔 성능이 뒤집힌 게 인상적이었다. 모델을 더 똑똑한 걸로 갈아끼우기 전에, 지금 모델에 "무엇을 입력으로 주고 있나"를 먼저 의심하는 게 더 빠른 길일 수 있다는 교훈으로 읽혔다.
3. 딥러닝 - 신경망으로 한 단계 더
전통 ML 다음은 딥러닝을 돌려봤다.
신경망을 어떻게 학습시키는지 먼저 짚고, 직접 PyTorch로 작은 신경망을 하나 만든다.
3-1) 신경망 학습의 4단계
파라미터를 가진 모델이 학습한다는 건, 결국 이 네 단계를 데이터를 보며 계속 반복하는 일이다.
- forward pass: 입력을 넣어 예측값(ŷ)을 낸다.
- loss 계산: 예측값과 정답(y)이 얼마나 다른지 손실을 구한다.
- backward pass: 신경망을 거슬러 올라가며 "파라미터를 어느 쪽으로 조금씩 바꾸면 손실이 줄어드는지"(기울기, gradient)를 계산한다.
- optimization: 그 방향으로 파라미터를 아주 조금씩 움직인다.
여기서 두 가지 하이퍼파라미터가 나온다.
epoch은 전체 데이터를 몇 번 훑을지(한 번만 보는 게 아니라 여러 번 반복 학습),
learning rate는 4번 단계에서 한 걸음을 얼마나 크게 내디딜지다.
이 forward → loss → backward → optimize 루프는 작은 신경망이든 거대한 LLM이든 본질적으로 똑같다.
3-2) 8층 바닐라 신경망
상품 설명을 신경망에 넣으려면 먼저 숫자로 바꿔야 한다.
여기서는 HashingVectorizer로 설명문을 5000차원 벡터로 만든다(각 단어가 등장했는지 여부로 0/1로 표시).
vectorizer = HashingVectorizer(n_features=5000, binary=True, stop_words='english')
그 위에 8개 층을 쌓은 단순한(바닐라) 신경망을 올린다.
5000차원 입력을 128 → 64로 좁히고, 점점 좁아지는 8개 층(layer)를 거친 뒤 마지막에 1개(가격)로 뽑아낸다.
층 사이에는 ReLU(비선형성을 주는 활성화 함수)를 끼운다.
class NeuralNetwork(nn.Module):
def __init__(self, input_size):
super().__init__()
self.layer1 = nn.Linear(input_size, 128)
self.layer2 = nn.Linear(128, 64)
self.layer3 = nn.Linear(64, 64)
self.layer4 = nn.Linear(64, 64)
self.layer5 = nn.Linear(64, 64)
self.layer6 = nn.Linear(64, 64)
self.layer7 = nn.Linear(64, 64)
self.layer8 = nn.Linear(64, 1)
self.relu = nn.ReLU()
def forward(self, x):
x = self.relu(self.layer1(x))
x = self.relu(self.layer2(x))
x = self.relu(self.layer3(x))
x = self.relu(self.layer4(x))
x = self.relu(self.layer5(x))
x = self.relu(self.layer6(x))
x = self.relu(self.layer7(x))
return self.layer8(x)
손실 함수는 MSE, 옵티마이저는 Adam(learning rate 0.001)을 쓰고, 전체 데이터를 2 epoch 돌렸다.
이 신경망의 학습 가능한 파라미터는 669,249개다. (프론티어 LLM 모델은 수십억개 이상)
loss_function = nn.MSELoss() # 오차 = (예측-정답)²
optimizer = optim.Adam(model.parameters(), lr=0.001)
EPOCHS = 2
for epoch in range(EPOCHS):
model.train()
for batch_X, batch_y in tqdm(train_loader):
optimizer.zero_grad()
outputs = model(batch_X) # ① 순전파(예측)
loss = loss_function(outputs, batch_y) # ② 손실 계산
loss.backward() # ③ 역전파(기울기)
optimizer.step() # ④ 가중치 업데이트
model.eval()
with torch.no_grad():
val_outputs = model(X_val)
val_loss = loss_function(val_outputs, y_val)
print(f'Epoch [{epoch+1}/{EPOCHS}], Train Loss: {loss.item():.3f}, Val Loss: {val_loss.item():.3f}')

신경망 학습의 핵심 4단계(foward pass -> loss 계산 -> backward pass -> optimization)
전체 데이터 2번(epoch =2) 훑음. Train loss 줄어드는 거 볼 수 있음
model = NeuralNetwork(input_size=5000)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
결과는 평균 오차 63.33달러. 전통 ML 최고 성적이었던 XGBoost(68.23)보다 조금 더 나았다. 단어 등장 여부만 보는 단순한 신경망인데도 앙상블을 넘어선 것이다.

느낀 점
- "거대한 LLM도 결국 이 forward → loss → backward → optimize 루프의 반복"이라는 설명이 딥러닝을 막연한 블랙박스에서 한 덩어리 메커니즘으로 보이게 해줬다. 모델 학습에서의 기본 골격을 볼 수 있어서 좋았다
4. 학습 없이 이긴다 - 제로샷 프론티어 모델
여기서부터가 이 글에서 가장 인상적이었던 구간이다. 전통 ML과 신경망은 모두 아마존 상품 데이터로 "학습"을 시켰다. 그런데 프론티어 모델(GPT, Claude, Gemini 등)은 데이터로 따로 단 한 줄도 학습시키지 않고, 그냥 상품 설명을 프롬프트로 던지기만 한다(제로샷).
그것만으로 어느정도 성능까지 가는지 보는 게 핵심이다.
4-1) 먼저 사람 기준선부터
비교를 위해 사람의 실력도 재둔다.
테스트셋 100개를 CSV로 내보내, 강사(Ed)가 직접 가격을 눈대중으로 적은 뒤 그 결과를 다시 읽어 평가했다.
def human_pricer(item):
idx = test.index(item)
return human_predictions[idx]
사람의 평균 오차는 87.62달러였다. 앞서 본 XGBoost(68.23)나 신경망(63.33)이 이미 사람보다 나은 셈이다.

4-2) 프롬프트 한 줄로 묻기
프론티어 모델에 보내는 건 정말 단순하다.
시스템 메시지도 없이, 사용자 메시지 하나로 "이 상품 가격을 추정해라, 설명 없이 가격만 답해라"라고만 한다.
def messages_for(item):
message = f"Estimate the price of this product. Respond with the price, no explanation\n\n{item.summary}"
return [{"role": "user", "content": message}]
호출은 litellm의 completion 하나로 여러 회사 모델을 같은 방식으로 돌린다. 예를 들어 GPT-5.1은 이렇게 부른다.
def gpt_5__1(item):
response = completion(model="gpt-5.1", messages=messages_for(item),
reasoning_effort='high', seed=42)
return response.choices[0].message.content
같은 패턴으로 GPT-4.1-nano, Claude Opus 4.5, Gemini 3 Pro, 그리고 가벼운 Gemini 2.5 Flash Lite까지 돌려봤다.





4-3) 결과
평균 오차(달러, 낮을수록 좋음)는 이랬다.
GPT-5.1 45.57 <- 1위
Claude Opus 4.5 45.73
Gemini 3 Pro 51.21
Gemini 2.5 Flash. 53.66
GPT-4.1-nano 85.71
-------------------------- (참고: 우리가 학습시킨 모델들)
8층 신경망 63.33
XGBoost 68.23
사람(Ed) 87.62
우리 데이터로 한참을 학습시킨 신경망(63.97)과 XGBoost(68.23)를, 학습이라곤 전혀 안 한 프론티어 모델들이 제로샷으로 대부분 앞섰다. (작은 모델인 gpt 4.1 nano 정도를 제외하면)
LLM 모델이 세상에 대해 이미 알고 있는 방대한 지식(사전 학습에 투여된 세상의 지식)이,
이 작은 가격 예측 문제에서도 그대로 힘을 발휘한 것이다.
느낀 점
- "학습을 안 시킨 LLM 모델이 자체 데이터로 학습시킨 ML/DL 모델을 이긴다"는 결과가 꽤나 충격적이었다. 그래도 전통 ML/DL의 영역이라고 느꼈던 가격 예측 문제 조차 이 결과가 나왔다는 게 신기했다(물론 ML/DL 성능 낼라면 더 제대로된 피처엔지니어링 필요한건 맞겠지만)
- 기능을 만들 때 처음부터 자체 모델을 학습시키려 들기보다 잘 만들어진 프론티어 모델에 프롬프트로 일을 시켜보는 게 합리적 출발점이라는 걸 수치로 보여줬다. 많은 문제에서 제로샷이 이미 강력한 베이스라인이다.
- 같은 messages_for 한 함수로 회사가 다른 모델 대여섯 개를 똑같이 호출해 비교한 구조가 기획 관점에서 와닿았다. 모델을 갈아끼울 수 있게 추상화해두면, 새 모델이 나올 때마다 같은 평가셋으로 즉시 재보고 교체 결정을 내릴 수 있다.
5. 프론티어 모델을 직접 파인튜닝해보다
마지막 단계는 프론티어 모델을 내 데이터로 직접 파인튜닝하는 것이다.
base 모델로는 가장 작은 축인 GPT-4.1-nano를 골랐다.
5-1) 파인튜닝이란 - 그리고 LoRA
프론티어 모델은 우리가 가중치 전체를 직접 만질 수 없다.
대신 OpenAI에 요청하면 내부적으로 LoRA(저랭크 어댑터) 방식으로 소수의 파라미터 사본만 학습시켜준다.
모델 전체를 다시 만드는 게 아니라, 기존 모델을 원하는 방향으로 살짝 "넛지"하는 셈이다. 그 결과로 나만의 GPT 변형 모델이 하나 생긴다.
5-2) 파인튜닝의 세 갈래 - SFT, DPO, RFT
강의에서 Open AI 플랫폼에서 지원하는 파인튜닝 종류를 정리해줬는데, 이게 뒤의 결과를 이해하는 열쇠가 된다.
- SFT(지도 파인튜닝): 입력과 정답 출력 예시를 그대로 보여주는 방식. 분류, 특정 포맷 생성, 지시 따르기 교정 등에 쓴다. 이번에 쓴 방식이다.
- DPO: 좋은 예와 나쁜 예를 함께 줘서 "사람이 어느 쪽을 선호하는지"를 학습시킨다. 좋아요/싫어요 같은 주관적 선호를 반영할 때 쓴다.
- RFT: 정답이 딱 정해져 있지 않고, 응답에 점수를 매기는 채점 프로그램(이 채점기가 LLM일 수도 있다)으로 학습한다. 리즈닝 모델에서만 쓸 수 있다.
그리고 강사가 짚은 중요한 전제가 하나 있다. 프론티어 모델 파인튜닝의 진짜 용도는 스타일이나 톤 맞추기, 출력 신뢰성 높이기, 복잡한 프롬프트에서의 실패 수정, 엣지케이스 처리, 새로운 스킬 부여라는 점이다. 이 복선을 6번에서 회수한다.
5-3) 3단계 파이프라인
파인튜닝 과정은 3 단계다.
- JSONL 학습셋을 만들어 OpenAI에 업로드
- 학습 실행 - train/validation loss가 줄어드는지 모니터링
- 결과 평가 후 튜닝하고 반복
먼저 학습 데이터를 JSONL로 만든다. 제로샷 때와 달리 assistant 메시지에 정답 가격을 넣어준다. 곧 "이 설명에는 이 가격으로 답해라"라는 예시를 보여주는 것이다.
def messages_for(item):
message = f"Estimate the price of this product. Respond with the price, no explanation\n\n{item.summary}"
return [
{"role": "user", "content": message},
{"role": "assistant", "content": f"${item.price:.2f}"}
]
OpenAI가 권장하는 대로 적은 수의 예시(학습 100개, 검증 50개)만 골라 파일로 쓴다.
fine_tune_train = train[:100]
fine_tune_validation = val[:50]
그 파일을 업로드하고 파인튜닝 작업을 건다.
with open("jsonl/fine_tune_train.jsonl", "rb") as f:
train_file = openai.files.create(file=f, purpose="fine-tune")
openai.fine_tuning.jobs.create(
training_file=train_file.id,
validation_file=validation_file.id,
model="gpt-4.1-nano-2025-04-14",
seed=42,
hyperparameters={"n_epochs": 1, "batch_size": 1},
suffix="pricer"
)
작업을 걸면 OpenAI 대시보드에서 업로드한 JSONL, loss 곡선, 시간별 loss 변화를 실시간으로 볼 수 있다.
OpenAI 작업 큐에 들어가 한참 대기하는 시간이 좀 길긴 했다.(오히려 실제 학습이 돌아가는 시간보다)





학습이 끝나면 ft:gpt-4.1-nano-...:pricer:... 형태의 내 전용 모델 이름이 나오고,
이걸로 추론(inference) 한다.
정답을 뺀 프롬프트만 넣고, 가격만 짧게 받으면 되니 토큰도 7개로 제한한다.
def gpt_4__1_nano_fine_tuned(item):
response = openai.chat.completions.create(
model=fine_tuned_model_name,
messages=test_messages_for(item),
max_tokens=7
)
return response.choices[0].message.content
느낀 점
- 프론티어 모델을 파인튜닝 시키는 일이 생각보다 평범한 API 호출 몇 줄로 끝난다는 게 인상적이었다. 데이터를 JSONL로 정리하고, 올리고, 작업을 걸고, loss를 지켜보는 흐름은 그 자체로 한 번쯤 손에 익혀둘 가치가 있었다.
- 오히려 오픈 AI 작업 큐에 들어가는 대기시간이 꽤 걸렸다.
- 파인튜닝을 시작하기 전에 "이건 어떤 종류(SFT/DPO/RFT)이고, 애초에 무엇을 바꾸기 위해 파인튜닝 하고 있나"를 먼저 구분하는 틀이 중요하다고 느꼈다
- 실제 파인튜닝 과정에서 Loss 가 줄어드는 걸 그래프로 시각화 해서 볼 수 있던 것도 좋았던 거 같다
6. 의외의 결과, 그리고 그게 알려주는 것
내심 기대가 있었다. 데이터로 직접 학습까지 시켰으니, 적어도 같은 gpt 4.1 nano의 제로샷 프롬프트로 테스트 했을 때 보다는
결과가 더 나아야 하지 않을까. 결과는 이랬다.
출력:
GPT 4.1 Nano Fine Tuned results
Error: $90.78 MSE: 24,383 r²: -10.9%

파인튜닝한 모델은 평균 오차 90.78달러로, 같은 모델의 제로샷(85.71달러)보다 오히려 조금 더 나빠졌다.
기대와는 반대 방향이었다.
그런데 5-2에서 강사가 깔아둔 복선을 떠올리면, 이건 사실 예상할 수 있는 결과였다.
프론티어 모델 파인튜닝의 용도는 스타일, 톤, 신뢰성, 엣지케이스, 새로운 스킬을 다듬는 것이지, 모델에 없던 도메인 지식을 욱여넣는 게 아니다.
프론티어 모델은 이미 세상에 대한 방대한 지식을 사전 학습으로 갖고 있고, 가격 감각도 그 안에 들어 있다.
거기에 우리가 고른 상품 100개의 가격 예시를 더한다고 해서 그 거대한 지식이 더 좋아질 이유가 없다.
오히려 적은 예시에 과하게 끌려가며 흔들릴 수 있다.
곧 결과가 나빠진 건 실패라기보다 프론티어 모델 파인튜닝이라는 도구를 용도와 안 맞게 쓴 것에 가깝다.
그렇다면 이 가격 예측 문제의 진짜 정답 방향은 무엇일까.
힌트는 강의 후반에 잠깐 등장한 또 다른 신경망에 있었다.
앞서 만든 8층짜리 작은 신경망을 크게 키운, 약 2.8억 개 파라미터의 깊은 신경망이다.
이 모델을 우리가 직접 모은 80만 건 데이터로 처음부터 학습시키자, 평균 오차가 제로샷 1위였던 GPT-5.1에 거의 필적하는 수준까지 내려갔다. (46대로 45대의 gpt 5.1, 클로드 오퍼스 4.5 와 거의 동일)
다른 일은 못 하지만 가격 예측 하나만큼은 프론티어급이 된 것이다.


핵심은 이거다. 이미 사전 지식이 많은 거대한 모델을 작은 예시로 살짝 미는 것보다,
지식이 별로 없는 작은 모델을 우리 데이터로 제대로 학습시키는 쪽이 이 문제에는 맞았다.
느낀 점
- "파인튜닝하면 무조건 좋아진다"는 막연한 기대가 깨진 게 오히려 큰 수확이었다. 어떤 기법이든 그게 해결하도록 설계된 문제 유형이 따로 있고, 내 문제가 거기에 해당하는지를 먼저 따져야 한다는 걸 직접 돌려보며 체감했다.
- 프론티어 모델 파인튜닝의 용도를 좀 더 파악해볼 수 있던 게 의미가 있었다
- 또한 실제 프론티어 모델도 파인튜닝을 할 수 있다는 것 자체가 몰랐던 사실이었는데 이렇게 프론티어 모델 플랫폼 단에서 파인튜닝을 지원해주는 거 자체가 신기했다.
- 실제 말로만 듣던 파인튜닝을 직접 해본 것 자체가 의미가 있었고, 용도만 잘 맞출 수 있다면 실 서비스 gpt 모델등으로 만드는 과정에서도 파인튜닝을 시도해볼 수 있겠다는 생각을 했다
7. 마무리 - 전체 성적표와 다음 이야기
Day 3부터 5까지 돌린 모델들을 한 줄로 세우면 이렇다(평균 오차, 달러, 낮을수록 좋음).
GPT-5.1 45.57 제로샷 프론티어
Claude Opus 4.5 45.73 제로샷 프론티어
Gemini 3 Pro 51.21 제로샷 프론티어
Gemini 2.5 Flash Lite 53.66 제로샷 프론티어
8층 신경망 63.33 딥러닝
XGBoost 68.23 전통 ML
Random Forest 72.28 전통 ML
NLP + 선형회귀 76.81 전통 ML
GPT-4.1-nano (제로샷) 85.71 제로샷 프론티어
사람(Ed) 87.62 사람
GPT-4.1-nano (파인튜닝) 90.78 파인튜닝
선형회귀(피처 3개) 101.56 베이스라인
Constant 106.18 베이스라인

정리하면 이번 캡스톤의 결론은 세 가지다.
첫째, 학습을 전혀 안 한 제로샷 프론티어 모델이 우리가 공들여 데이터 바탕으로 학습시킨 ML/DL 모델들을 대부분 앞섰다.
둘째, 그렇다고 전통 ML과 딥러닝이 죽은 것도 아니어서(2.8억 신경망 성능 등), 특징이 명확한 문제에서는 여전히 경쟁력이 있었다.
셋째, 프론티어 모델 파인튜닝은 적어도 이 가격 예측 문제에는 맞는 도구가 아니었다.
그리고 마지막 셋째가 다음 이야기의 출발점이 된다. "거대한 프론티어 모델을 미세조정"하는 게 답이 아니라면,
6번 끝에서 본 깊은 신경망처럼 "우리 데이터로 모델을 제대로 학습"시키는 길이 남는다.
다음 Week 7에서는 바로 그 방향으로, 오픈소스 모델을 QLoRA 기법으로 우리 데이터에 파인튜닝하는 작업을 다룬다.
같은 "파인튜닝"이지만 프론티어 모델에 했던 것과는 의미가 사뭇 다른 시도다.
느낀 점
- 한 문제를 두고 베이스라인부터 전통 ML, 딥러닝, 프론티어, 파인튜닝까지 같은 잣대로 줄 세워본 경험 자체가 값졌다.
- 가장 좋은 결과가 가장 화려한 기법에서 나오지 않았다는 점이 오래 남는다. 제로샷 한 줄이 최고였고 정교한 파인튜닝은 헛돌았다.
다음 글에서 Week7 오픈소스 모델 파인튜닝으로 찾아오겠습니다.
(꾸준히 공부하고 적을테니 많은 관심 부탁드립니다.)
Profile:
Linkedin
'AI 공부' 카테고리의 다른 글
| 16. 3B 오픈소스 모델(Llama 3.2)로 GPT-5.1 이겨보기 — QLoRA 파인튜닝 실전 (0) | 2026.07.11 |
|---|---|
| 15. LoRA·QLoRA 원리 파악 — Llama 3.2 파인튜닝 준비하기 (0) | 2026.06.28 |
| 13. RAG를 넘어 파인튜닝으로 - 그 첫단계, 데이터 큐레이션 및 전처리 (0) | 2026.05.31 |
| 12. RAG의 한계를 어떻게 깰까 — Advanced RAG (Semantic Chunking·Reranking·Query Rewriting 등) (1) | 2026.05.09 |
| 11. RAG, 잘 만들었는지 어떻게 알지? — RAG 평가 시스템 구축 (테스트셋 구축 및 검색/답변 평가 진행) (0) | 2026.05.09 |