개발자를 위한 LLM 할루시네이션 제어 방법들 소개

TL;DR

코드1는 작성 중. (스테이블 디퓨전처럼 공개 못할지도..)

시작하며

여느 ML 프로젝트가 그렇듯, LLM 도 그럴듯한 데모 만들기는 쉽다. GPT4 와 같이 엄청 큰 모델을 예산제한없이 쓸 수 있다면 훨씬 쉬워진다.

하지만 프로덕션 레벨의 성능(정확도/커버리지 등) 을 확보하는것은 매우 어렵다.

최근 이 어려움의 한복판에 있는 것이 할루시네이션(hallucination)2 이라고 부르는 특징이다. (그 다음이 한국어 모델의 부재로 인해 ChatGPT 가 강제되는 것)

개인적으로 할루시네이션은 문제가 아니며 LLM 의 특징이라고 생각하며, 제거하는 것이 아니라 제어하는 방법을 찾아야 한다고 생각한다.

할루시네이션에 대해서 아직 완벽한 해결방법은 없지만 방법들이 계속 나오고 있으므로, 시간이 지나면 temperature 처럼 파라미터 수준으로 제어할 수 있을 것으로 기대된다.

본 글에서는 모델 튜닝없이 할루시네이션을 제어할 수 있는 방법 몇 가지만 공유해본다.

할루시네이션 제어 방법들

데이터 전처리

LLM 도 다른 ML들 처럼 데이터 전처리가 매우 중요하다.

데이터 가공과정 (예, 요약) 에서 중요한 키워드 등이 빠져버리면 이후의 모든 과정이 의미가 없어지기 때문이다.

LLM 에서 자연어 전처리는 기존 NLP 보다 매우 간단한 편으로, 보통 문법교정을 한 뒤 chunk 로 나누는 작업정도만 진행한다.

처리해야 하는 문서가 너무 길거나 noise 가 많이 껴있는 경우 데이터를 따로 뽑아내거나, 요약을 하는 식으로 chunk 를 나누게 된다.

데이터를 뽑아내기 위해 큰 LLM 을 사용하는 경우, 원본 문서의 특징 키워드를 잘 유지 하도록 프롬프트 엔지니어링을 잘 해줘야 하며, 또한 chunk 를 너무 잘게 자르면 중복된 단어가 많이 들어가게 되므로 chunk 와 overlay 의 크기를 잘 결정해줘야 한다.

RAG (Retrieval Augmented Generation)

사용자 쿼리가 LLM 에 없는 정보를 요구하는 경우, 해당 데이터를 이용하여 모델을 파인튜닝을 하는 것이 가장 좋다.

하지만 PEFT 를 쓰더라도 파인튜닝 비용이 부담되는 경우가 많기 때문에, RAG 로 파인튜닝을 대체해서 쓰는 추세이다. (새로운 모든 데이터에 대해서 파인튜닝을 하지 않아도 되기 때문에도 많이 쓴다.)

RAG 구조가 단순하고 파인튜닝 없이도 쓸만한 성능까지 올리는 것이 매우 쉽기 때문에 대부분의 LLM 프로젝트에서 사용하고 있다.

하지만 RAG 를 사용한다고 해도, 데이터를 chunking 하는 방법, 임베딩 모델의 성능, 벡터DB 의 검색 알고리즘, 프롬프트 작성 방법 등 구현 방법에 따라 여전히 할루시네이션이 발생할 수 있다.

Re-rank

RAG 를 사용하면 일반적으로 연관된 문서를 k 개 가져와서 사용하게 된다.

그런데 이 때, k+1 번째 데이터에만 정답이 들어 있다면 할루시네이션이 발생하게 되므로 컨텍스트 크기가 긴 모델들을 사용하여 k 값을 크게 주려고 하는 편이다.

하지만 k 가 커질 수록 모델의 메모리 사용량과 생성속도가 느려지므로 k 개를 마냥 크게 주기 보다 좀 더 최적화하는 방식을 연구하고 있다.

(또한, 최근 연구들에 의하면 컨텍스트를 일정 개수 이상 주는 것이 큰 의미 없다고 한다, 특히 처음과 끝의 컨텍스트에 정답이 배치될 수록 성능이 좋다. 이 외에도 프롬프트를 최적화하여 불필요한 정보를 제거하여 프롬프트 압축하는 방법들도 연구되고 있다.)

최근에는 작은 k 값을 유지하면서도 성능을 올리기 위한 방법으로 rerank 를 많이 사용한다. 대략의 순서는 아래와 같다.

  1. 벡터 데이터베이스에서 2k~3k 개의 문서를 가져온다.
  2. 해당 문서를 NLI (natural language inference) 또는 rerank 를 위한 모델(주로 샴 네트워크로 학습된 모델)로 사용자 쿼리에 대해 re-rank 한다.
  3. rerank 된 상위 k 개의 문서를 LLM 에 컨텍스트로 전달해서 RAG 를 진행한다.

일단 센텐스 임베딩을 통해 연관이 있는 것은 추려냈으니, 다른 메트릭을 통해 문서의 연관도를 재검증해본다는 아이디어이다.

이 때, 단순 rerank 만 하는것보다 BM25 같은 키워드 매칭 점수를 함께 반영하는 것도 성능향상에 도움이 되며, rerank 단계를 진행하지 않는다면 RAG 쿼리시에 반영하는 경우도 많다.

위의 내용을 대략읽어보면 검색엔진 검색 랭킹 최적화와 비슷하다는 것을 알수 있다. 따라서 검색 랭킹 최적화에 사용하는 다양한 기법들을 적용해보면 더 성능을 높일 수 있을 것이다.

프롬프트 엔지니어링을 통한 검증 (Evaluation)

현재 모델이 생성한 대답을 사용해도 되는지 검증하는 기준은 크게 아래 2가지 인 것 같다.

먼저 신뢰도는 프롬프트 엔지니어링을 통해 모델이 자신의 결과에 대해, 주어진 문서들와 연관이 있는지를 스스로 판단하게 한다.

예를 들어, 벡터 데이터베이스에서 3개의 문서를 가져와서 추론했다면, 모델의 생성결과가 3개의 문서 모두와 관련이 있는지 각각 물어보고 (1개의 프롬프트에서) 모두 연관이 있으면 신뢰할 수 있다는 식이다.

할루시네이션의 정의를 생각해보면, 사실 여부는 LLM 이 판단할 수 없다. 따라서 외부데이터를 쿼리하는 과정이 반드시 필요하다.

이 데이터를 확보하고 사용하는 방법은 정말 여러가지인데, 검색엔진에 쿼리를 해서 결과개수를 확인하는 간단한 방법부터, 다양하고 복잡한 방법들이 있다.

검색을 위한 파라미터나 사용할 방법등은 ReAct 같은 방식으로 모델이 직접 만들어낼 수도 있지만, 여튼 사실성은 외부데이터를 통해 검증할 수 밖에 없다.

검증방법

제대로 된 정보가 주어졌을때 질문에 대한 답을 추론(reasoning) 하는 능력이 중요하므로 결국 추론 능력이 좋은 모델을 사용하는 것이 중요하다.

13B 같이 상대적으로 작은 모델은 약간만 프롬프트가 복잡해져도 이상동작을 하기 때문에, 해당 동작을 제어하는데 시간이 많이 들어간다.

따라서 일단 큰 모델로 시작해서 모델을 작게 줄여나가는 방식이 개발 속도를 올릴 수 있다. 오픈소스 모델로 실험중이라면 together api 같은 서비스들이 큰 도움이 될 것이다.

여튼 추론 능력이 충분하다면, 먼저 프롬프트 작성시 Chain of thought 등을 사용하여 모델이 생각할 시간을 충분히 주고 결과를 생성하도록 한다.

이후 모델이 만든 대답을 few-shot example 을 주면서 스스로 검증할 수 있도록 유도한다.

검증 기준에서 설명한대로 검증은 신뢰도를 중심으로 검증하게 되고, 여력이 되면 사실성에 대한 검증도 하면 된다.

이 글3에서 CoT 및 RAG 를 이용한 할루시네이션 제어 방법을 소개하고 있는데, RAG 만 사용해도 그냥 CoT 보다 성능이 더 좋으며, RAG + SelfCritic 방법을 쓰는 것이 가장 성능이 좋다.

SelfCheckGPT

최근에 나온 논문 하나만 간단히 소개하고 마무리 해본다.

프롬프트 기반의 검증을 기존 BLEU, ROUGE 와 같이 좀 더 정형화 시킨 SelfCheckGPT4 라는 방식이다. 해당 논문에서 소개한 아이디어는 단순한데 효과적인 것 같다.

ICL (in context learning) 의 역할은 모델에서 정답 임베딩 위치를 가이드해주는 역할을 한다고 알려져 있다.

따라서 모델이 신뢰도가 있는 정보를 생성했다면 정답이 있는 영역을 잘 찾았다는 의미이므로, beam search 로 여러개의 답을 생성하더라도 일관된 내용을 말할 것이라는 가정을 할 수 있다.

반대로 ICL 을 통해 정답이 있는 영역을 찾지 못했다면, beam search 로 여러개의 답을 생성하면 각 답은 일관성이 없을 것이다.

이를 이용해, 여러개의 문장을 생성하고 모든 문장이 일관된 정보를 보여주는지 확인한다. 모델이 생성한 문장들을 일관된 문장과 아닌 문장의 비율을 F1 스코어처럼 계산하여 inconsistency 를 확인한다.

약간 더 디테일하게 소개하면 대략 아래와 같은 과정으로 점수를 계산한다.

  1. 사용자의 질문에 대한 답을 beam search 로 n개 만든다.
  2. 가장 높은 확률을 가진 문장을 이용하여, 질문/답변을 생성한다.
  3. 나머지 문장들을 하나씩 이용하여 생성한 질문에 대한 답을 맞춘다.
  4. 답을 맞추면 mach, 못맞추면 mismatch 로 카운팅한다.
  5. #mismatches / (#mismatches + #matches) 를 점수로 사용한다.

여기서는 QA 에 대해서만 소개했지만, 실제 논문에서는 n-gram, BERTScore, NLI 에서 SelfCheck 하는 방법들을 소개하고 있으므로 다른 워크로드를 처리하고 있다면 살펴보면 좋을 것 같다.

마치며

이 글도 드래프트로 대충 써두고 클릭률이 올라가면 수정하거나 할 예정. 일단 쓰는게 중요하다는걸 새삼 느낀다.

나는 머신러닝 엔지니어도 아니고 일때문에 빠르게 공부하고 사용해 본 내용만 적었기 때문에 모든 기법을 소개하지는 못했다.

현재도 방법들이 계속 나오고 있으며, 워크로드와 데이터별로 다양한 방법들을 적용해야 하므로 당분간은 한두가지 방법으로 통일되기는 어렵지 않을까 예상한다. (스테이블 디퓨전도 FID 가 제일 좋은 방법은 아닌거 같지만 딱히 다른 평가방법이 없는거보면…)