20장. LLM 평가와 모니터링
드리프트를 재는 법
지난달과 이번 달의 주문표를 나란히 놓고 메뉴별 비율을 견주면, 손님이 바뀐 것을 숫자로 알 수 있습니다. 드리프트를 재는 일도 "평소"인 기준 기간과 지금을 견주는 일입니다.
1번에서 평소로 볼 기간(이 장에서는 1~4주)을 정합니다. 2번에서 견줄 기간(주마다)의 질문을 모읍니다. 3번에서 두 분포가 얼마나 다른지 숫자로 잽니다. 4번에서 문턱을 넘으면 살펴봅니다. 지난달 주문표가 기준 기간, 이번 달 주문표가 비교 기간입니다.
재는 값
| 값 | 무엇을 견주나 | 읽는 법 |
|---|---|---|
| PSI | 범주(가장 가까운 문서)의 비율 | 흔히 0.1 아래면 안정, 0.1~0.25 살펴봄, 0.25 넘으면 큰 변화로 본다 |
| KS 검정 | 숫자(검색 점수)의 분포 | p 값이 작을수록(예: 0.01 아래) 두 분포가 다르다 |
| 처음 보는 질문 비율 | 기준 기간의 질문과 뜻이 먼 질문 | 가장 가까운 기준 질문과의 유사도가 문턱 아래인 비율 |
| 결과 지표 | 모름, 싫어요 비율 | 기준 기간보다 크게 오르면 품질 문제 |
PSI 계산
PSI 는 범주마다 (지금 비율 − 기준 비율) × ln(지금 비율 ÷ 기준 비율)을 더한 값입니다. 비율이 0 인 범주는 아주 작은 값으로 바꿔 나눗셈을 막습니다.
import numpy as np
def psi(base, now, eps=1e-4):
"""base, now: {범주: 비율}. 두 분포의 PSI"""
keys = sorted(set(base) | set(now))
b = np.array([base.get(k, 0) for k in keys]) + eps
n = np.array([now.get(k, 0) for k in keys]) + eps
return float(((n - b) * np.log(n / b)).sum())
base = {"대출": 0.6, "열람실": 0.3, "주차": 0.1}
print(f"{psi(base, {'대출': 0.58, '열람실': 0.32, '주차': 0.1}):.3f}") # 거의 같다
print(f"{psi(base, {'대출': 0.4, '열람실': 0.2, '주차': 0.1, '스마트 도서관': 0.3}):.3f}") # 새 주제가 30%코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
import numpy as np | NumPy(Numerical Python, 숫자 계산 파이썬)를 np 라는 짧은 이름으로 불러온다 |
set(base) | set(now) | 두 사전의 범주를 합친다(합집합). 코드에서는 반각 세로줄 연산자다 |
sorted(...) | 범주를 가나다 차례로 늘어놓아 두 배열의 칸 차례를 맞춘다 |
base.get(k, 0) | 범주 k 의 비율을 꺼내고, 없으면 0 을 쓴다 |
np.array([...]) + eps | 목록을 배열로 바꾸고 칸마다 eps(epsilon, 아주 작은 수) 0.0001 을 더한다 |
np.log(n / b) | 칸마다 자연로그(ln)를 구한다. 지금 비율이 기준의 몇 배인지를 로그로 본 값이다 |
.sum(), float(...) | 칸의 값을 모두 더하고, NumPy 숫자를 보통 파이썬 숫자로 바꾼다 |
이 코드가 뜻하는 것
마트 매대별 판매 비율을 지난달과 이번 달로 견주는 일과 같습니다. 매대마다 "비율이 얼마나 바뀌었나"와 "몇 배가 되었나"를 곱해 변화 점수를 매기고, 점수를 모두 더합니다. 여기서 매대가 범주, 점수 합계가 PSI 입니다. 비유가 맞지 않는 곳이 하나 있습니다. 지난달에 없던 매대는 "몇 배"를 셀 수 없어서, 코드는 0 대신 0.0001 이 팔렸다고 치고 셉니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
keys = sorted(...) | 두 기간의 범주를 한 목록으로 | 두 달의 매대 목록 합치기 |
b = ..., n = ... | 범주마다 기준 비율과 지금 비율 | 매대별 판매 비율 두 줄 |
(n - b) * np.log(n / b) | 범주마다 변화 몫 | 매대마다 변화 점수 |
.sum() | 몫을 모두 더한다 | 점수 합계 |
첫 결과 0.002 는 0.1 아래라 안정입니다. 둘째 결과 2.524 는 0.25 를 크게 넘어 큰 변화입니다. 새 주제 하나가 30% 를 차지한 것만으로 이렇게 커집니다.
작게 줄여서 눈으로 보기
둘째 결과 2.524 가 어느 범주에서 왔는지 범주마다 나눠 봅니다.
import numpy as np
keys = ["대출", "열람실", "주차", "스마트 도서관"]
b = np.array([0.6, 0.3, 0.1, 0.0]) + 1e-4 # 기준 비율
n = np.array([0.4, 0.2, 0.1, 0.3]) + 1e-4 # 지금 비율
terms = (n - b) * np.log(n / b) # 범주마다의 몫
for k, t in zip(keys, terms):
print(f"{k:8s} {t:.3f}")
print(f"합계 {terms.sum():.3f}")대출 0.081
열람실 0.041
주차 0.000
스마트 도서관 2.402
합계 2.524| 결과 | 읽는 법 |
|---|---|
대출 0.081 | 0.6 에서 0.4 로 줄어 몫이 생겼다 |
열람실 0.041 | 0.3 에서 0.2 로 줄었다 |
주차 0.000 | 비율이 같으니 몫이 0 이다 |
스마트 도서관 2.402 | 기준 비율을 0.0001 로 보니 0.3 은 3천 배쯤이다. 로그가 커서 합계의 대부분을 차지한다 |
합계 2.524 | 본문 코드의 둘째 결과와 같다 |
PSI 가 크게 나오면 이렇게 범주마다 몫을 나눠 봅니다. 어느 주제가 늘었는지 바로 드러납니다. 기준 기간에 없던 범주의 몫은 eps 를 더 작게 잡을수록 더 커지므로, 숫자 크기보다 "새 범주가 생겼다"는 사실을 먼저 읽습니다.
주의할 점
- 표본이 적은 주에는 숫자가 크게 흔들립니다. 하루보다 한 주 단위로 보고, 질문 수도 함께 적습니다.
- PSI 의 문턱(0.1, 0.25)은 업계에서 흔히 쓰는 어림값입니다. 자기 서비스의 평소 흔들림을 보고 정합니다.