호돌찌의 AI 연구소
article thumbnail

이전 글에서 이어서 이야기를 합니다. 이번 글은 '평가(Evaluation)' 에 관련된 글입니다. 예시를 통해서 강의를 설명하기 때문에 강의를 들어보시는 것을 추천합니다. 

- 이전 글

2023.08.12 - [후기/강의] - ChatGPT API로 시스템 구축하기 강의 요약(1) - Building Systems with the ChatGPT API

 

ChatGPT API로 시스템 구축하기 강의 요약(1) - Building Systems with the ChatGPT API

AI 기술 교육, 실습, 협업 커뮤니티 기반으로 이루어져 있으며 AI Education Tech 회사인 DLAI 에서는 최근 양질의 LLM이나 Generation 관련 Topic 들을 중심으로 하여 Short course를 계속 제작하고 있으며 이를

hotorch.tistory.com

 

- 강의 URL

https://www.deeplearning.ai/short-courses/building-systems-with-chatgpt/

 

Building Systems with the ChatGPT API

Level up your use of LLMs. Learn to break down complex tasks, automate workflows, chain LLM calls, and get better outputs.

www.deeplearning.ai

 

 

Evaluation Part 1 - Prompt 기반 평가 과정 


이전에 지도학습 기반의 AI 모델과 Prompt 기반의 AI 모델의 개발 프로세스 차이는 다음과 같이 표현하였습니다.

 

 

하지만 지도학습 방식에서도 평가 데이터를 수집하는 것이 만만치 않기 때문에 Prompt 기반으로 평가하는 단계를 제시합니다.

  1. 3~5개 정도의 예제를 통해 handful 형태로 평가를 하기 위한 프롬프트를 조정하고 작동하도록 합니다. 
  2. 추가 테스트를 하면서 까다로운 몇 가지 예시들에 부딪히게 되는데 이를 조금씩 조정을 합니다. 
  3. 그 후 small size 예제들을 측정하기 위한 metric을 개발하고, 이를 측정합니다.
  4. 이제 개발한 시스템에 대한 신뢰도를 높이기 위해 더 많은 테스트 데이터를 수집하고 측정합니다.

 

Evaluation Part 1 - 평가 프롬프트 준비하기


위와 같이 단계별로만 보면 이해하기 어려울 수 있습니다. 따라서 예제를 통해 설명한다면 다음과 같습니다.

 

def find_category_and_product_v1(user_input,products_and_category):

    delimiter = "####"
    system_message = f"""
    You will be provided with customer service queries. \
    The customer service query will be delimited with {delimiter} characters.
    Output a python list of json objects, where each object has the following format:
        'category': <one of Computers and Laptops, Smartphones and Accessories, Televisions and Home Theater Systems, \
    Gaming Consoles and Accessories, Audio Equipment, Cameras and Camcorders>,
    AND
        'products': <a list of products that must be found in the allowed products below>


    Where the categories and products must be found in the customer service query.
    If a product is mentioned, it must be associated with the correct category in the allowed products list below.
    If no products or categories are found, output an empty list.
    

    List out all products that are relevant to the customer service query based on how closely it relates
    to the product name and product category.
    Do not assume, from the name of the product, any features or attributes such as relative quality or price.

    The allowed products are provided in JSON format.
    The keys of each item represent the category.
    The values of each item is a list of products that are within that category.
    Allowed products: {products_and_category}
    

    """
    
    few_shot_user_1 = """I want the most expensive computer."""
    few_shot_assistant_1 = """ 
    [{'category': 'Computers and Laptops', \
'products': ['TechPro Ultrabook', 'BlueWave Gaming Laptop', 'PowerLite Convertible', 'TechPro Desktop', 'BlueWave Chromebook']}]
    """
    
    messages =  [  
    {'role':'system', 'content': system_message},    
    {'role':'user', 'content': f"{delimiter}{few_shot_user_1}{delimiter}"},  
    {'role':'assistant', 'content': few_shot_assistant_1 },
    {'role':'user', 'content': f"{delimiter}{user_input}{delimiter}"},  
    ] 
    return get_completion_from_messages(messages)

 

위와 같이 내뱉는 결과에 대해서 format과 지시사항을 지정해 줍니다. 하지만 아래와 같은 코드를 run 하고 junk text가 생성되는 경우가 종종 생깁니다. 내가 완벽하다고 생각하는 instruct와 prompt를 구현했다고 하더라도, LLM의 결과는 항상 예상을 벗어나는 경우를 자주 마주치기 때문에 여러 번 테스트와 튜닝을 반복적으로 하는 것이 중요한 것 같습니다.

 

customer_msg_3 = f"""
tell me about the smartx pro phone and the fotosnap camera, the dslr one.
Also, what TVs do you have?"""

products_by_category_3 = find_category_and_product_v1(customer_msg_3,
                                                      products_and_category)
print(products_by_category_3)


customer_msg_4 = f"""
tell me about the CineView TV, the 8K one, Gamesphere console, the X one.
I'm on a budget, what computers do you have?"""

products_by_category_4 = find_category_and_product_v1(customer_msg_4,
                                                      products_and_category)
print(products_by_category_4)

 

products_by_category_3, products_by_category_4 를 run하면 list 이외에 추가적인 텍스트가 생기는 것을 알 수 있다. 

 

그래서 위와 같은 현상을 막기 위해 instruct를 개선하고 few-shot을 추가하여 이전 프롬프트에 추가 삽입하여 평가 방식을 개선합니다. 

 

def find_category_and_product_v2(user_input,products_and_category):
    """
    Added: Do not output any additional text that is not in JSON format.
    Added a second example (for few-shot prompting) where user asks for 
    the cheapest computer. In both few-shot examples, the shown response 
    is the full list of products in JSON only.
    """
    delimiter = "####"
    system_message = f"""
    You will be provided with customer service queries. \
    The customer service query will be delimited with {delimiter} characters.
    Output a python list of json objects, where each object has the following format:
        'category': <one of Computers and Laptops, Smartphones and Accessories, Televisions and Home Theater Systems, \
    Gaming Consoles and Accessories, Audio Equipment, Cameras and Camcorders>,
    AND
        'products': <a list of products that must be found in the allowed products below>
    Do not output any additional text that is not in JSON format.
    Do not write any explanatory text after outputting the requested JSON.


    Where the categories and products must be found in the customer service query.
    If a product is mentioned, it must be associated with the correct category in the allowed products list below.
    If no products or categories are found, output an empty list.
    

    List out all products that are relevant to the customer service query based on how closely it relates
    to the product name and product category.
    Do not assume, from the name of the product, any features or attributes such as relative quality or price.

    The allowed products are provided in JSON format.
    The keys of each item represent the category.
    The values of each item is a list of products that are within that category.
    Allowed products: {products_and_category}
    

    """
    
    few_shot_user_1 = """I want the most expensive computer. What do you recommend?"""
    few_shot_assistant_1 = """ 
    [{'category': 'Computers and Laptops', \
'products': ['TechPro Ultrabook', 'BlueWave Gaming Laptop', 'PowerLite Convertible', 'TechPro Desktop', 'BlueWave Chromebook']}]
    """
    
    few_shot_user_2 = """I want the most cheapest computer. What do you recommend?"""
    few_shot_assistant_2 = """ 
    [{'category': 'Computers and Laptops', \
'products': ['TechPro Ultrabook', 'BlueWave Gaming Laptop', 'PowerLite Convertible', 'TechPro Desktop', 'BlueWave Chromebook']}]
    """
    
    messages =  [  
    {'role':'system', 'content': system_message},    
    {'role':'user', 'content': f"{delimiter}{few_shot_user_1}{delimiter}"},  
    {'role':'assistant', 'content': few_shot_assistant_1 },
    {'role':'user', 'content': f"{delimiter}{few_shot_user_2}{delimiter}"},  
    {'role':'assistant', 'content': few_shot_assistant_2 },
    {'role':'user', 'content': f"{delimiter}{user_input}{delimiter}"},  
    ] 
    return get_completion_from_messages(messages)

 

Evaluation Part 1 - 평가 데이터 만들기


이 부분이 가장 공수가 많이 드는 부분인데, ideal 한 answer를 따로 구축합니다. 여기에서는 10개 정도 아래와 같이 best 한 질의응답 Set을 구축합니다. (제가 알고 있는 배경지식을 조금 더 더하면 이런 셋을 구축하는 것을 golden set이라고 부르는데, 가장 검수 및 제작 비용이 많이 드는 작업이지만 domain이 넓은 llm을 적용하는데 가장 정확하다고 알려져 있습니다. )

 

msg_ideal_pairs_set = [
    
    # eg 0
    {'customer_msg':"""Which TV can I buy if I'm on a budget?""",
     'ideal_answer':{
        'Televisions and Home Theater Systems':set(
            ['CineView 4K TV', 'SoundMax Home Theater', 'CineView 8K TV', 'SoundMax Soundbar', 'CineView OLED TV']
        )}
    },

    # eg 1
    {'customer_msg':"""I need a charger for my smartphone""",
     'ideal_answer':{
        'Smartphones and Accessories':set(
            ['MobiTech PowerCase', 'MobiTech Wireless Charger', 'SmartX EarBuds']
        )}
    },
   
   (중략)

    # eg 7 # this will output a subset of the ideal answer
    {'customer_msg':f"""What Gaming consoles would be good for my friend who is into racing games?""",
     'ideal_answer':{
        'Gaming Consoles and Accessories':set([
            'GameSphere X',
            'ProGamer Controller',
            'GameSphere Y',
            'ProGamer Racing Wheel',
            'GameSphere VR Headset'
     ])}
    },
    # eg 8
    {'customer_msg':f"""What could be a good present for my videographer friend?""",
     'ideal_answer': {
        'Cameras and Camcorders':set([
        'FotoSnap DSLR Camera', 'ActionCam 4K', 'FotoSnap Mirrorless Camera', 'ZoomMaster Camcorder', 'FotoSnap Instant Camera'
        ])}
    },
    
    # eg 9
    {'customer_msg':f"""I would like a hot tub time machine.""",
     'ideal_answer': []
    }
    
]

 

그 후에 사용자의 질문을 같이 넣었을 때, response가 llm이 생성한 response와 사전에 작성한 ideal 한 response를 유사한지 비교하여 정확도를 산출하는 방식을 보여줍니다. 준비한 eval set의 질의응답 수만큼 반복문을 돌며 평가를 수행하고 알맞은 case의 비율을 계산하는 방식을 보여주는 것이 part 1의 내용입니다.

import json
def eval_response_with_ideal(response,
                              ideal,
                              debug=False):
    
    if debug:
        print("response")
        print(response)
    
    # json.loads() expects double quotes, not single quotes
    json_like_str = response.replace("'",'"')
    
    # parse into a list of dictionaries
    l_of_d = json.loads(json_like_str)
    
    # special case when response is empty list
    if l_of_d == [] and ideal == []:
        return 1
    
    # otherwise, response is empty 
    # or ideal should be empty, there's a mismatch
    elif l_of_d == [] or ideal == []:
        return 0
    
    correct = 0    
    
    if debug:
        print("l_of_d is")
        print(l_of_d)
    for d in l_of_d:

        cat = d.get('category')
        prod_l = d.get('products')
        if cat and prod_l:
            # convert list to set for comparison
            prod_set = set(prod_l)
            # get ideal set of products
            ideal_cat = ideal.get(cat)
            if ideal_cat:
                prod_set_ideal = set(ideal.get(cat))
            else:
                if debug:
                    print(f"did not find category {cat} in ideal")
                    print(f"ideal: {ideal}")
                continue
                
            if debug:
                print("prod_set\n",prod_set)
                print()
                print("prod_set_ideal\n",prod_set_ideal)

            if prod_set == prod_set_ideal:
                if debug:
                    print("correct")
                correct +=1
            else:
                print("incorrect")
                print(f"prod_set: {prod_set}")
                print(f"prod_set_ideal: {prod_set_ideal}")
                if prod_set <= prod_set_ideal:
                    print("response is a subset of the ideal answer")
                elif prod_set >= prod_set_ideal:
                    print("response is a superset of the ideal answer")

    #   list의 총 항목 수에 대한 정확한 개수 계산
    pc_correct = correct / len(l_of_d)
        
    return pc_correct

# API 호출 중 하나라도 시간이 초과되면 작동하지 않습니다
score_accum = 0
for i, pair in enumerate(msg_ideal_pairs_set):
    print(f"example {i}")
    
    customer_msg = pair['customer_msg']
    ideal = pair['ideal_answer']
    
    # print("Customer message",customer_msg)
    # print("ideal:",ideal)
    response = find_category_and_product_v2(customer_msg,
                                                      products_and_category)

    
    # print("products_by_category",products_by_category)
    score = eval_response_with_ideal(response,ideal,debug=False)
    print(f"{i}: {score}")
    score_accum += score
    

n_examples = len(msg_ideal_pairs_set)
fraction_correct = score_accum / n_examples
print(f"Fraction correct out of {n_examples}: {fraction_correct}")

 

Evaluation Part 2


1. Rubric 구성하여 평가하기

LLM을 사용하여 텍스트를 생성할 때 part 1과 같이 ideal 한 set이 없을 때 평가하는 데 골머리가 아파집니다. part 2에서는 rubric이라는 것을 이용해서 평가하는 방식을 보여줍니다. rubric은 여기 글을 참고해 보시면 좋은데, LLM을 평가하는 영역에서는 생성된 response의 사실적인 내용을 문맥을 비교해서 정답인지 아닌지 또는 척도화를 시키는 등 기준점을 뜻한다고 생각하면 좋습니다. 

 

아래 코드를 보고 이해해 볼 수 있습니다.

def eval_with_rubric(test_set, assistant_answer):

    cust_msg = test_set['customer_msg']
    context = test_set['context']
    completion = assistant_answer
    
    system_message = """\
    You are an assistant that evaluates how well the customer service agent \
    answers a user question by looking at the context that the customer service \
    agent is using to generate its response. 
    """

    user_message = f"""\
You are evaluating a submitted answer to a question based on the context \
that the agent uses to answer the question.
Here is the data:
    [BEGIN DATA]
    ************
    [Question]: {cust_msg}
    ************
    [Context]: {context}
    ************
    [Submission]: {completion}
    ************
    [END DATA]

Compare the factual content of the submitted answer with the context. \
Ignore any differences in style, grammar, or punctuation.
Answer the following questions:
    - Is the Assistant response based only on the context provided? (Y or N)
    - Does the answer include information that is not provided in the context? (Y or N)
    - Is there any disagreement between the response and the context? (Y or N)
    - Count how many questions the user asked. (output a number)
    - For each question that the user asked, is there a corresponding answer to it?
      Question 1: (Y or N)
      Question 2: (Y or N)
      ...
      Question N: (Y or N)
    - Of the number of questions asked, how many of these questions were addressed by the answer? (output a number)
"""

    messages = [
        {'role': 'system', 'content': system_message},
        {'role': 'user', 'content': user_message}
    ]

    response = get_completion_from_messages(messages)
    return response
    
    
# 답변들을 위 프롬프트 함수를 거쳐 평가를 하게할 수 있다. (자세한 코드는 강의 참고)
evaluation_output = eval_with_rubric(cust_prod_info, assistant_answer)
print(evaluation_output)

 

즉, 기준(rubric)을 구성하여 평가하는 방식을 위 프롬프트(user message)처럼 아래와 같은 질문지에 대해 Y/N을 답변하는 것을 알 수 있습니다. 

 

위 프롬프트 함수에서의 항목에 대한 답변만을 하는 것을 알 수 있다.

 

chatgpt 3.5 trubo 모델을 사용할 수 있고, 더 많은 텍스트나 평가 항목이 많다면 16k model이나 gpt4 모델을 고려해 볼 수 있습니다. 

 

2. LLM 답변이 expert 답변에 동의하는지 여부 판단하기

두 텍스트가 유사한지 아닌지 평가하는 BLEU 점수를 이용하여 평가하는 것이 일반적이지만, openai의 evals를 이용하여 평가하는 방식 또한 있습니다. 여기를 응용하여 프롬프트를 구성하여 rubric을 구성하여 평가합니다. 실제 assistant의 생성된 답변과 ideal 한 답변을 비교하여 아래 항목 A~E까지 답변을 LLM이 뱉게 하여 평가합니다. 

def eval_vs_ideal(test_set, assistant_answer):

    cust_msg = test_set['customer_msg']
    ideal = test_set['ideal_answer']
    completion = assistant_answer
    
    system_message = """\
    You are an assistant that evaluates how well the customer service agent \
    answers a user question by comparing the response to the ideal (expert) response
    Output a single letter and nothing else. 
    """

    user_message = f"""\
You are comparing a submitted answer to an expert answer on a given question. Here is the data:
    [BEGIN DATA]
    ************
    [Question]: {cust_msg}
    ************
    [Expert]: {ideal}
    ************
    [Submission]: {completion}
    ************
    [END DATA]

Compare the factual content of the submitted answer with the expert answer. Ignore any differences in style, grammar, or punctuation.
    The submitted answer may either be a subset or superset of the expert answer, or it may conflict with it. Determine which case applies. Answer the question by selecting one of the following options:
    (A) The submitted answer is a subset of the expert answer and is fully consistent with it.
    (B) The submitted answer is a superset of the expert answer and is fully consistent with it.
    (C) The submitted answer contains all the same details as the expert answer.
    (D) There is a disagreement between the submitted answer and the expert answer.
    (E) The answers differ, but these differences don't matter from the perspective of factuality.
  choice_strings: ABCDE
"""

    messages = [
        {'role': 'system', 'content': system_message},
        {'role': 'user', 'content': user_message}
    ]

    response = get_completion_from_messages(messages)
    return response

 

실제로 위 코드를 이용하여 내가 가진 답변과 생성된 답변을 오지선다형으로 채점하여 구축한 시스템을 평가해 볼 수 있습니다. 

 

강의를 마무리 + 내 생각


지도학습 기반 AI 모델을 구축해 오다가, 완전 다른 방식의 Prompt 기반 AI를 구성하는 일들을 하면서 새로운 개념들이 쏟아져 나오고 있습니다. 이를 익히기 위해 공부를 게을리해서는 안되고, 내가 만든 시스템이나 프로젝트에 적용하는 것이 실력을 단기간에 빠르게 기를 수 있다 생각합니다. 확실히 DLAI에서 많은 강의들이 계속해서 나오고 있는데 standard를 알려주는 것 같아서 좋은 것 같습니다.

또한 LLM을 바탕으로 하는 서비스들을 개발하면서 매번 어려운 부분은 평가 부분이라고 생각합니다. 그리고 여전히 사람이 개입하는 부분은 남아 있는 것 같습니다. 이를 더 편하고 cost를 적게들이고 평가하는 것에 대해서 꾸준히 연구 및 트렌드를 쫓아야 할 것 같습니다. 나중에 여력이 된다면, 평가에 대한 내용들로 글을 포스팅해보겠습니다.

 

 


아래는 블로그 주인장의 토스 익명 후원 링크입니다. 글이 도움되거나 흡족스러웠다면 후원해주시면 감사하겠습니다.

https://toss.me/hotorch

 

hotorch님에게 보내주세요

토스아이디로 안전하게 익명 송금하세요.

toss.me

 

 

profile

호돌찌의 AI 연구소

@hotorch's AI Labs

포스팅이 도움이 되셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!