공개가 되어 있나..? 🤔
최근에 논문을 읽다가 궁금한 점이 생겼습니다.
바로 proprietary models, 즉 기업들이 공개하지 않고 API를 통해 추론 결과만을 제공하는 모델들에서 '토큰별 예측 확률값을 뽑아낼 수 있을지'에 대한 의문이었습니다.
Allen institute of AI가 연구한 Tuning Language Models by Proxy라는 논문에서는 Proxy-tuning이라는 개념을 제시합니다.
(해당 논문 리뷰 링크: https://chanmuzi.tistory.com/472)
이 연구에서는 공개되지 않은 모델들을 활용할 수 있는 방법에 대한 내용을 다룹니다.
최종적으로 어떤 토큰에 대한 확률을 구하여 output을 만들어 낼 때, 이 확률만 알더라도 특정 태스크에 대한 퍼포먼스가 좋아진다는 것이죠.
근데 좀 웃긴 게.. 뭐 실제로 최종 확률만 알더라도 모델 성능을 개선하는 데 도움이 될 수 있다는 것을 Llama 계열의 모델로 증명합니다.
weight도, 모델 아키텍쳐도 다 공개되어 있는 걸로 입증을..
뭐 결과론적으로는 실제로 유의미하기 때문에 기업들이 확률만이라도 공개해 주면 좋겠다~는 것이 연구의 결과라고 볼 수 있긴 한데요..
여튼! 제가 궁금한 것은 그래서 (대표적인) OpenAI API에서 토큰별 확률을 구할 수 없나? 였습니다.
결론만 간단하게 말하자면..
가능합니다. 단, 딱 5개까지의 토큰에 대해서만 확인할 수 있습니다.
(간단한 테스트를 진행하는 동안은 속편히 gpt-3.5-turbo 모델을 사용했으니 참고 부탁드립니다 🙂)
어떻게 확인할 수 있을까? (Playground)
가장 쉽게 확인할 수 있는 방법은 OpenAI 홈페이지의 Playground를 방문하는 것입니다.
(Playground 링크: https://platform.openai.com/playground?mode=complete)
API도 서비스되는 것이 여러 형태를 띠고 있는데, 그중에서도 complete 모드에서 확인 가능합니다.
(Assistant나 Chat 모드가 아닙니다)
여기에 아무 설정도 하지 않고 prompt를 주면 다음과 같이 결과가 출력됩니다.
잘 대답하네요.
이번에는 우측 여러 설정 중에서 맨 아래에 있는 'Show probabilities'를 변경하고 같은 프롬프트를 입력해 보겠습니다.
assistance에 해당하는 단어가 어떤 확률에 의해 예측된 것인지 확인이 가능합니다.
이번에는 해당 옵션을 'Full spectrum'이 아닌 'Most likely'로 변경해 보겠습니다.
예측 확률이 가장 높은 것 하나만 취해서 decoding하는 전략이었음을 알 수 있습니다.
('Full spectrum'에서는 예측 확률이 낮더라도 sampling에 의해 추출되었다고 볼 수 있겠죠)
어떻게 확인할 수 있을까? (API)
이번에는 Playground가 아닌 API를 사용할 때 모델이 토큰별로 예측하는 확률을 확인해 보도록 합시다.
이와 관련된 API Documentation 링크에서 옵션을 먼저 살펴보면 도움이 됩니다.
(ChatGPT 모델에 어떤 식으로 입력을 줄 수 있는지에 대해 간단히 정리된 Cookbook 링크에서도 확인 가능합니다)
우선 OpenAI 클라이언트 객체를 생성해야 합니다.
이때 API key가 필요한데 key를 발급하는 방법에 대해서는 따로 언급하지 않겠습니다 😀
from openai import OpenAI
import os
client = OpenAI(api_key='your api key')
이제 이 클라이언트에게 메세지를 전달하고 response를 요청하는 코드를 작성해야 합니다.
이때 logprobs 라는 옵션을 조정할 수 있습니다.
documentation을 참고해 볼까요?
default값은 False로 되어있다는 것으로 보아 원래는 확률값을 자동적으로 출력하지 않는 게 맞네요!
또 현재로서는 gpt-4-vision-preview를 호출할 땐 사용할 수 없는 옵션이라고 합니다.
이걸 True로 바꿔주고 그 결과를 확인해 보겠습니다.
# Example OpenAI Python library request
MODEL = "gpt-3.5-turbo"
response = client.chat.completions.create(
model=MODEL,
logprobs=True,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Knock knock."},
{"role": "assistant", "content": "Who's there?"},
{"role": "user", "content": "Orange."},
],
temperature=0,
)
response.choices[0].logprobs.content
# [ChatCompletionTokenLogprob(token='Orange', bytes=[79, 114, 97, 110, 103, 101], logprob=-0.000110337794, top_logprobs=[]),
# ChatCompletionTokenLogprob(token=' who', bytes=[32, 119, 104, 111], logprob=-4.0722858e-05, top_logprobs=[]),
# ChatCompletionTokenLogprob(token='?', bytes=[63], logprob=-0.00016742534, top_logprobs=[])]
맨 마지막 코드에 의해 list가 반환되고 그 안에는 logprob이 포함되어 있는 것이 확인됩니다.
여기서 logprob은 절댓값이 작을수록 높은 확률로 예측되고 있다는 것을 의미하게 됩니다.
확률인 x는 0에서부터 1 사이의 값을 갖기 때문에 여기에 log를 취하게 되면 값이 0에 가까울수록 예측 확률이 높았다는 것을 의미하게 됩니다.
따라서 위 예시에서는 세 개의 토큰에 대한 logprob이 위에서부터 아래로 점점 작아지고 있다는 것이 확인됩니다.
이번에는 반환되는 확률값의 개수를 조정해 보겠습니다.
위에서 언급한 바와 같이 최대 5개를 반환할 수 있습니다.
따라서 여기에 사용되는 옵션인 'top_logprobs'는 최소 0, 최대 5의 정수값을 입력으로 받습니다.
response = client.chat.completions.create(
model=MODEL,
logprobs=True,
top_logprobs=5,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Knock knock."},
{"role": "assistant", "content": "Who's there?"},
{"role": "user", "content": "Orange."},
],
temperature=0,
)
response.choices[0].logprobs.content[0].top_logprobs
# [TopLogprob(token='Orange', bytes=[79, 114, 97, 110, 103, 101], logprob=-0.00011272187),
# TopLogprob(token='\n', bytes=[10], logprob=-9.268204),
# TopLogprob(token='Kn', bytes=[75, 110], logprob=-12.291378),
# TopLogprob(token='I', bytes=[73], logprob=-12.962042),
# TopLogprob(token='orange', bytes=[111, 114, 97, 110, 103, 101], logprob=-13.39686)]
총 다섯 개의 토큰에 대한 logprob이 반환된 것이 확인됩니다.
⚠️ 주의 사항
반환 가능한 확률의 개수는 최대 5라고 말씀드렸죠.
이를 초과하는 값을 입력하게 되면 바로 에러가 발생합니다.
response = client.chat.completions.create(
model=MODEL,
logprobs=True,
top_logprobs=10, # 이 값이 문제입니다.
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Knock knock."},
{"role": "assistant", "content": "Who's there?"},
{"role": "user", "content": "Orange."},
],
temperature=0,
)
또, 당연한 것이지만 logprobs는 기본적으로 False로 설정되어 있기 때문에 이를 True로 설정하지 않고 top_logprobs 값을 조정해도 에러가 발생합니다.
response = client.chat.completions.create(
model=MODEL,
top_logprobs=5, # 이것만 설정하면 에러가 발생합니다.
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Knock knock."},
{"role": "assistant", "content": "Who's there?"},
{"role": "user", "content": "Orange."},
],
temperature=0,
)
# BadRequestError: Error code: 400 - {'error': {'message': "The 'top_logprobs' parameter is only allowed when 'logprobs' is enabled.", 'type': 'invalid_request_error', 'param': 'top_logprobs', 'code': None}}
마무리
이상으로 OpenAI의 모델에서 logprob 값을 뽑아내는 방법에 대해 알아보았습니다.
사실 이 값들을 바탕으로 softmax를 취하여 %로 변환하고 그 값들을 전부 합치면 거의 100%가 됩니다.
그래서 (정확히 어떤 연구인지 찾아보지는 못했지만) 이 다섯 개의 토큰만을 활용하고 나머지는 0으로 설정하여 모델 학습을 진행한 경우가 있다고도 들었습니다.
이런 것들조차도 공개하지 않는 이유가 '위험성' 때문이라는 설명을 보기도 했습니다.
지금도 생성형 모델들의 사회 윤리적 이슈가 끊이지 않고 있는데, 저 확률 값을 공개하는 것은 악의적인 유저들에게 대문을 열어주는 것과 같다는 것이죠.
어려운 문제인 것 같긴 합니다만..
추가적인 모델 학습 없이 잘 학습된 모델을 활용하는 커다란 창구들이 닫혀있다는 것은 분명 아쉽긴 하네요..!