빅데이타 & 머신러닝/생성형 AI (ChatGPT etc)

Langchain을 이용한 LLM 애플리케이션 개발 #8 - 프롬프트 예제 선택기

Terry Cho 2024. 1. 4. 16:35

프롬프트 예제 선택기를 이용한 동적으로 프롬프트 삽입하기

조대협 (http://bcho.tistory.com)

 

프롬프트를 통한 정확도를 높이기 위한 기법인 프롬프트 튜닝에서 가장 큰 효과를 볼 수 있는 방식이 프롬프트에 질문과 답변에 대한 예제를 추가하는 방법이다. 이렇게 질문과 답변 예제를 추가 하는 방식을 N-Shot 프롬프팅이라고 한다. 예제가 없는 경우 Zero-Shot 프롬프팅, 2개의 예제가 있는 경우 2-Shot 프롬프팅이라고 한다. 보통 2~3개의 예제만 있어도 답변을 정확도를 크게 높일 수 있다. 

 

프롬프트에 예제를 정적으로 미리 추가해놓을 수 도 있지만 질문의 내용이나 종류에 따라서 동적으로 질문에 대한  예시를 선택하여 프롬프트에 삽입하면 좀 더 좋은 결과를 얻을 수 있다. 특히 챗봇처럼 다이나믹하게 질의의 내용이 바뀔 수 있는 경우 유용하게 사용할 수 있는데, 랭체인에서는 동적으로 예제를 선택하여 삽입할 수 있는 예제 선택기 (Example Selector)기능을 제공한다. 

 

기본 원리는 데이터베이스나 메모리에 여러개의 예제들을 미리 저장해놓고, 질문에 따라서 가장 유사한 예제를 찾아서 선택하는 방식인데, 예제를 선택하는 알고리즘에 따라서 다양한 예제 선택기(Example Selector)를 제공한다. 각각의 예제 선택기에 대해서 알아보자. 

길이 기반 (Length)

길이 기반 예제 선택기는 전체 프롬프트의 길이를 기반으로 예제를 선택한다. 보통 LLM의 입력 토큰 길이에는 제약이 있기 때문에, 무조건적으로 많은 예제를 넣을 수 없다. 이 길이 기반 예제 선택기 (LengthBasedExampleSelector)는 전체 프롬프트 길이를 기준으로 예제를 선택한다.

예를 들어 프롬프트의 질문의 길면, 예제를 상대적으로 적게 넣어서 전체 프롬프트 길이를 맞추고, 반대로 프롬프트의 질문 길이가 짧으면 예제를 많이 넣어서 전체 프롬프트 길이를 맞춘다. 

 

아래 예제는 음식의 종류를 구분하는 프롬프트 작성 코드로. 길이 기반 예제 선택기를 사용한 예제이다. 

 

from langchain.prompts import FewShotPromptTemplate, PromptTemplate

from langchain.prompts.example_selector import LengthBasedExampleSelector

 

examples = [

    {"food":"Kimchi","category":"Korean food"},

    {"food":"Chocolate","category":"dessert"},

    {"food":"Pasta","category":"Italian food"},

    {"food":"Americano","category":"Coffee"}

]

 

example_prompt = PromptTemplate(template="Food:{food} Category:{category}",

                                input_variables=["food","category"])                               

example_selector = LengthBasedExampleSelector(examples = examples, example_prompt=example_prompt,max_length=30)

dynamic_prompt = FewShotPromptTemplate(

    example_selector = example_selector,

    example_prompt = example_prompt,

    prefix = "What is the category of this food?",

    suffix = "Food: {food}",

    input_variables = ["food"]

)

 

output=dynamic_prompt.format(food="Korean BBQ")

print(len(output.split()),output)




examples에는 예제 데이터가 있고, example_prompt를 이용하여, 예제 프롬프트를 생성한다. 다음 LengthBasedExampleSelector를 생성하는데, 이때 예제 프롬프트 템플릿(example_prompt)와, 예제 템플릿에서 사용할 값(examples)를 넘기고, max_length를 지정하는데,  전체 프롬프트의 문자열의 길이 이다. (문자수가 아니라 단어수를 사용한다. )

실행 결과는 다음과 같다. 모든 예제를 다 넣어서 총 약 20개의 단어로 출력한다. 

 

20 What is the category of this food?

 

Food:Kimchi Category:Korean food

 

Food:Chocolate Category:dessert

 

Food:Pasta Category:Italian food

 

Food:Americano Category:Coffee

 

Food: Korean BBQ



프롬프트의 질문 길이를 늘이면 어떻게 될까?

 

output=dynamic_prompt.format(food="""Korean BBQ: Grilled meat, vibrant flavors, charcoal aroma. 

Tradition meets friends and family in a delightful dining experience, creating lasting memories.

""")

print(len(output.split()),output)

 

출력 결과는 다음과 같이 34개의 단어로 예제는 한개만을 포함하였다. 

 

34 What is the category of this food?

 

Food:Kimchi Category:Korean food

 

Food:Chocolate Category:dessert

 

Food: Korean BBQ: Grilled meat, vibrant flavors, charcoal aroma. 

Tradition meets friends and family in a delightful dining experience, creating lasting memories.

 

LengthBasedExampleSelector는 질문의 길이에 따라서 예제를 몇개를 포함시킬 것인지를 결정하는데 유용하게 사용할 수 있다. 

N-그램 오버랩 (N-gram Overlap)

N-gram은 연속된 N개의 아이템(일반적으로 단어 또는 문자)으로 이루어진 순서 있는 집합을 나타낸다.

예를 들어, "I love natural language processing."라는 문장에서 2-gram을 추출하면 다음과 같다:

  • "I love"
  • "love natural"
  • "natural language"
  • "language processing"

여기서 N은 연속된 아이템의 개수를 나타낸다. 그래서 2-gram은 두 개의 연속된 아이템으로 이루어진 구조를 의미한다. 유사하게, 3-gram은 세 개의 연속된 아이템으로 이루어진 구조를 나타내며, 일반적으로 bigram, trigram, 4-gram, 5-gram 등과 같이 표현된다.

 

N-gram overlap selector는 프롬프트와 예제 프롬프트에서 겹치는 n-gram의 수를 카운트하여, 겹치는 n-gram이 많은 예제를 사용하는 예제 선택기 이다. 

 

예를 들어 “Sushi is my favorite choice for party food” 라는 프롬프트가 있을때, 아래와 같이 2개의 예제가 있다고 가정하자. 

  • Chocolate is an easy to gain weight food. 
  • Kimchi is my favorite food.

1-gram을 기준으로 첫번째 예제는 “is”와 “food”가 중첩되고, 두번째 예제는 “my”,”favorite”,”food” 3개의 단어가 중첩되기 때문에, 두번째 예제가 더 높은 순위를 차지하게 되고, 두 번째 예제가 추천된다. 

 

NGramOverlapExampleSelector를 사용하는 예제 코드를 보자. 

앞의 예제와 거의 유사하지만 NGramOverlapExampleSelector를 사용하였고, 추가로 threshold 값이 인자로 전달된것을 볼 수 있다. 

  • threshold 값은 -1 이 디폴트이고, 제안된 예제를 모두 포함한다. 단 n-gram overlap score에 따라서 중첩되는 단어가 많은 순서대로 예제를 포함한다. 중첩되는 n-gram 이 없더라도 예제에 포함된다.
  • threshold 값이 0 이면, 중첩되는 n-gram이 있는 예제만을 리턴한다.
  • threshold 값이 1 이상이면, 예제를 포함하지 않는다. 

아래 예제는 threshold 값을 -1로 했기 때문에, n-gram 중첩에 상관 없이 모든 예제를 프롬프트에 포함하되, n-gram 중첩이 높은 순서대로 소팅해서 포함한다. 

 

from langchain.prompts import FewShotPromptTemplate, PromptTemplate

from langchain.prompts.example_selector.ngram_overlap import NGramOverlapExampleSelector

 

examples = [

    {"food":"Kimchi is my favorite food.","category":"Korean food"},

    {"food":"Chocolate is easy to gain weight food.","category":"dessert"},

    {"food":"I love pasta; it's my favorite comfort food after work.","category":"Italian food"},

    {"food":"Sipping an Americano, contemplating weight, a mindful morning routine begins.","category":"Coffee"}

]

 

example_prompt = PromptTemplate(template="Food:{food} Category:{category}",

                                input_variables=["food","category"])       

example_selector = NGramOverlapExampleSelector(examples = examples, example_prompt=example_prompt,threshold=-1.0)

dynamic_prompt = FewShotPromptTemplate(

    example_selector = example_selector,

    example_prompt = example_prompt,

    prefix = "What is the category in the food? .",

    suffix = "Food: {food}",

    input_variables = ["food"]

)

 

output=dynamic_prompt.format(food="Sushi is my favorite choice for party food")

print(output)



실행결과를 보면 다음과 같다. 

첫번째 문장은 “favorite”,”food”,”is” 와 같이 중첩이 많은 “Kimchi is my favorite food” 라는 예제가 중첩되었고, 마지막은 중첩 되는 단어가 없는 “Sipping an Americano, contemplating weight, a mindful morning routine begins. “까지 모두 포함되었다. 

 

What is the category in the food? .

 

Food:Kimchi is my favorite food. Category:Korean food

 

Food:I love pasta; it's my favorite comfort food after work. Category:Italian food

 

Food:Chocolate is easy to gain weight food. Category:dessert

 

Food:Sipping an Americano, contemplating weight, a mindful morning routine begins. Category:Coffee

 

Food: Sushi is my favorite choice for party food



Threshold 값을 0으로 바꾸고 실행하면 다음과 같은 결과를 얻을 수 있다. 

중첩되는 단어가 없는 두개의 예제는 제외되고, 중첩되는 단어가 있는 나머지 두 예제만 포함된것을 확인할 수 있다. 

 

What is the category in the food? .

 

Food:Kimchi is my favorite food. Category:Korean food

 

Food:I love pasta; it's my favorite comfort food after work. Category:Italian food

 

Food: Sushi is my favorite choice for party food

 

N-Gram Overlap Example Selector는 같은 단어가 나오는 빈도로 예제를 찾아내기 때문에, 문맥에 대한 의미가 없는 단순한 방식으로 높은 정확도를 기대하기 어렵지만, 단순히 유사 단어로 검색해야 하는 시나리오나 빠른 속도가 필요한 경우에는 유용하게 사용할 수 있다. 

유사도 기반 (Similarity) 

앞의 두 예제 선택기가 길이나, 단어의 중첩 개수와 같은 단순한 방법을 사용했다고 하면, 유사도 기반의 예제 선택기는 입력되는 프롬프트와 예제 프롬프트 문장간의 유사도를 기반으로 예제를 추천한다. 구글과 같은 검색엔진에서 검색 키워드에 대한 유사한 문서를 추출하는 개념으로 생각하면 된다. 

이 유사도에 대한 이론적인 설명은 뒤에 Reteriveal 부분에서 다시 설명하기로 한다. 

 

간단하게 개념만 짚고 넘어간다면, 프롬프트 문장이 있을때, 이 문장을 벡터 평면에 맵핑하고, 프롬프트 예제 문장들 역시 벡터 평면에 맵핑한 후에, 서로 문맥적으로 의미가 유사한 문장을 찾아서 랭킹을 매기는 방식이다. 

즉 의미적으로 비슷한 예제를 찾아낼 수 있는 방식이라고 생각하면 된다.

 

유사도 기반 예제 선택기를 사용하기 위해서는 Langchain 이외에, 두 가지 외부 컴포넌트가 필요하다. 

유사도 측정을 하기 위해서는 먼저 프롬프트 예제와 프롬프트를 벡터 값으로 변경해야 하는데, 이 과정을 임베딩이라고 하고, 보통 임베딩 머신러닝 모델을 이용하여 텍스트를 벡터 값으로 임베딩한다. 이를 위해서 아래 예제에서는 OpenAI의 Embedding API를 사용하였다. 

 

임베딩이 된 후에는 이 임베딩 된 데이터를 저장하고,임베딩된 프롬프트와 프롬프트 예제간의 유사도 기반의 검색 기능이 필요한데, 이렇게 임베딩 벡터를 저장하고 검색하는 기능을 제공하는 솔루션을 벡터 데이터베이스 라고 한다. Pinecone 등의 벡터 데이터베이스 등이 있는데,  이 예제에서는 Chroma 벡터 데이터 베이스를 사용하였다. 별도의 서버 기동이 필요없고 로컬 환경에서 파이썬 모듈의 일부로 쉽게 구동할 수 있기 때문이다.

 

주: 임베딩이나 벡터 검색, 벡터데이터 베이스에 대한 구체적인 개념과 내용에 대해서는 뒤에 Retrieval 부분에서 다시 상세하게 설명한다.   

 

먼저 pip install 명령을 이용하여 Chroma 데이터 베이스를 설치한다. 

!pip install chromadb

 

다음은 유사도 기반 예제 검색을 사용하는 예제 코드이다. 

from langchain.embeddings import OpenAIEmbeddings

from langchain.prompts import FewShotPromptTemplate, PromptTemplate

from langchain.prompts.example_selector import SemanticSimilarityExampleSelector

from langchain.vectorstores import Chroma

 

import os

os.environ["OPENAI_API_KEY"] = "{YOUR_API_KEY}"

 

examples = [

    {"input":"Happy.","category":"emotion"},

    {"input":"BBQ","category":"food"},

    {"input":"Golf","category":"Sports"},

    {"input":"Student","category":"Person"}

]

 

example_prompt = PromptTemplate(template="Input:{input} Category:{category}",

                                input_variables=["input","category"])       

example_selector = SemanticSimilarityExampleSelector.from_examples(examples, OpenAIEmbeddings(),Chroma,k=1,)

dynamic_prompt = FewShotPromptTemplate(

    example_selector = example_selector,

    example_prompt = example_prompt,

    prefix = "What is the category of the input? .",

    suffix = "input: {input}",

    input_variables = ["input"]

)

 

output=dynamic_prompt.format(input="Sushi")

print(output)

 

input 입력에 따라서 분류를 결정하는 코드인데, examples에 프롬프트 예제로 Happy는 emotion (감정), BBQ는 food(음식) 과 같이 분류의 예제를 만들어 놓았다. 

이를 SemanticSimilarityExampleSelector를 이용하여 프롬프트 예제를 선택하게 한다. 

  • examples는 예제로 사용될 프롬프트 변수들의 집합이.
  • OpenAIEmbeddings() 예제와 프롬프트를 임베딩하는데 사용할 임베딩 API 이다. 여기서는 OpenAI에서 제공하는 임베딩 API를 사용하였다.
  • Chroma 임베딩된 데이터를 저장하고 검색할 수 있는 벡터 데이터 베이스를 지정한다. 여기서는 Chroma 데이터 베이스를 사용하였는, Redis 벡터데이터베이스나 Pinecone등 다른 벡터 데이터 베이스도 사용이 가능하다. 
  • 마직막으로 k 값은 유사도 기반으로 몇개의 예제를 리턴할것인지를 결정한. 

 

실행결과는 다음과 같다.

질의는 해당 input에 대해서 Category를 지정하는 질의이고, Input으로는 “Sushi”를 입력하였다. 유사도 기반 예제 선택기에 의해서 Sushi는 예제중에서 BBQ와 같은 음식 의미를 가지고 있기 때문에 예제로 BBQ 예제가 선택되어서 프롬프트에 삽입되었다. 

What is the category of the input? .

 

Input:BBQ Category:food

 

input: Sushi

 

통계요약 알고리즘 기반 (Maximal Marginal Relevance,MMR)

유사도 기반의 알고리즘은 프롬프트를 하나의 큰 의미 벡터로 표현하여, 그와 유사한 예제를 찾는 개념이다. 즉 하나의 의미에 대한 유사한 예제를 찾기 때문에 예제의 다양성이 떨어질 수 있다. 예를 들어 질의가 하나의 개념에 대한 질의일 경우에는 크게 문제가 없지만 질의가 두개 이상의 개념에 질의인 경우 어떻게 될까?

예를 들어, 사용자가 자연어 처리와 기계 학습에 대한 학문적인 관심을 가지고 있다고 가정하자. 그리고 아래와 같이 3개의 예제 문서가 있다고 하자. 질의는 “최신 자연어 처리 기술과 기계학습에 대해서 알려줘.”

 

  • 문서1: "최신 자연어 처리 기술의 적용"
  • 문서2: "신규 기계 학습 알고리즘의 개발 동향"
  • 문서3: "자연어 텍스트 데이터의 효과적인 전처리 방법"

 

이 문제는 “자연어 처리"와 “기계 학습" 두 가지 토픽에 대한 검색을 필요로 하는 문제인데, 유사도 기반에서 임베딩된 데이터가 자연어쪽으로 편향되어 있다면 문서1,3의 자연어 처리에 관련된 문서만 검색을 해낼것이고, “기계학습" 토픽에 대한 문서는 검색 결과에서 누락될 것이다.

 

이렇게 유사도 기반 검색에서의 다양성 문제를 해결하기 위한 알고리즘이 통계 요약 알고리즘 (이하 MMR) 방식이다. MMR 방식은 유사도 기반 알고리즘과 비슷하지만 검색 결과를 낼때 앞의 검색 결과에서 나온 토픽을 제외 한다. 예를 들어 위의 예제에서는

  • 첫번째 검색 결과는 유사도 기반 검색을 사용하기 때문에, 문서 1의 자연어에 관련된 문서를 리턴한다. 
  • 두번째 순위 검색에서는 앞의 검색 결과가 “자연어" 토픽에 대한 검색이었기 때문에, “자연어"의 개념을 제외한 나머지 문서들을 검색해서 문서 2. “기계학습"에 관련된 문서를 리턴한다. 

MMR 방식의 예제 선택기는 유사도 기반의 다양성 문제를 해결할 수 있는 기술이기는 하지만 연산량이 다른 알고리즘에 비해서 상대적으로 길기 때문에 응답시간과 성능에 대한 충분한 테스트 후에 사용하기를 권장한다. 

 

아래 예제 코드를 보자. 기본적으로 MMR도 유사도 기반 방식과 마찬가지로 임베딩과 벡터 데이터 베이스를 위한 검색을 사용하기 때문에, 마찬가지로 임베딩 API가 필요하고, OpenAIEmbeddings()를 사용하였다. 벡터데이터베이스도 앞에서 사용한 Chroma를 그대로 사용하였다.

 

변경된 부분은 MaxMarginalRelevanceExampleSelector.from_examples 으로, 아래 예제를 보면 examples에서 첫번째 다섯번째 토픽은 날씨에 관련된 내용이고, 두번째부터 네번째 토픽은 경제에 관련된 토픽이다. 프롬프로트로 질문은 “나는 경제와 이번주 날씨에 대해서 알고 싶어"로 두개의 토픽에 대하여 질문을 하였다.  

 

from langchain.embeddings import OpenAIEmbeddings

from langchain.prompts import FewShotPromptTemplate, PromptTemplate

from langchain.prompts.example_selector import (

    MaxMarginalRelevanceExampleSelector,

    SemanticSimilarityExampleSelector,

)

from langchain.vectorstores import Chroma

 

import os

os.environ["OPENAI_API_KEY"] = "{YOUR_API_KEY}"

 

examples = [

    {"input":"Please summarize the weather news.\n"

     ,"summary":"Today's weather: Sunny skies, mild temperatures,"\

     " and a gentle breeze. Enjoy the pleasant conditions throughout the day!"},

    {"input":"Please summarize the economy news.\n","summary":"Global stocks rise on positive economic data;"\

     "inflation concerns persist. Tech sector outperforms; central banks closely monitor."},

    {"input":"Please summarize retail news.\n","summary":"Major retailer announces record-breaking sales during holiday shopping season"},

    {"input":"What is stock market trend?\n","summary":"Investor optimism grows amid easing global trade tensions"},

    {"input":"Typhoon related news.\n","summary":"IAirports and schools close ahead of approaching typhoon threat"},

    

]

 

example_prompt = PromptTemplate(template="Input:{input} Summary:{summary}",

                                input_variables=["input","summary"])       

example_selector = MaxMarginalRelevanceExampleSelector.from_examples(examples, OpenAIEmbeddings(),Chroma,k=2,)

dynamic_prompt = FewShotPromptTemplate(

    example_selector = example_selector,

    example_prompt = example_prompt,

    suffix = "input: {input}\nSummary:",

    prefix = "",

    input_variables = ["input"]

)

 

output=dynamic_prompt.format(input="I want to know the economy trends and weather this week.")

print(output)




결과는 아래와 같다. 

첫번째 예제 프롬프트는 날씨에 대한 예제 프롬프트가 선택되었고, 두번째 프롬프트는 날씨에 대한 프롬프트 예제를 제외하고, 주식 시장에 관련된 예제가 선택되었다. 

Input:Please summarize the weather news.

Summary:Today's weather: Sunny skies, mild temperatures, and a gentle breeze. Enjoy the pleasant conditions throughout the day!

 

Input:What is stock market trend? 

Summary:Investor optimism grows amid easing global trade tensions

 

input: I want to know the economy trends and weather this week.

Summary:

 

유사도 기반이나 MMR 방식이 문장의 유사도를 기반으로 예제를 선택하는 만큼 N-그램등에 비해서 정확되는 높을 수 있겠지만, 말 그대로 문장이 유사한지 아닌지를 기반으로 하는 것이지, 어떤 예제가 적정하다는 판단에 의해서 예제를 선택하는 것이 아니다. Langchain에서는 Custom Example Parser를 직접 제작할 수 있는데 좀더 높은 수준의 예제 선택기가 필요하다면 LLM을 이용하여 예제를 선택하도록 구현해보는 것을 권장한다.

예를 들어 “이 예제는 주식 시장 질문에 대한 예제이다. 예제:{예제 프롬프트}”와 같이 각 예제 별로 메타정보(예제의 용도)를 서술한 뒤에, 이 메타정보를 기반으로 LLM이 적절한 예제를 선택하도록 구성할 수 있다.

 

그리드형