반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리


반응형

요즘은 open ai cook book 의 예제들을 살펴 보고 있습니다. 이 cookbook 페이지는 이곳입니다.

https://github.com/openai/openai-cookbook

 

GitHub - openai/openai-cookbook: Examples and guides for using the OpenAI API

Examples and guides for using the OpenAI API. Contribute to openai/openai-cookbook development by creating an account on GitHub.

github.com

 

여기서 Embeddings의 Text comparison examples 페이지에 있는 Question_answering_using_embedings.ipynb 가 오늘 공부할 내용입니다.

 

여기에서 다루는 것은 GPT-3에게 소스 정보를 제공하고 그 안에서 GPT-3가 내용을 검색해서 답변을 하도록 하는 방법을 설명합니다.

 

예를 들어 어떤 사람이 10년 넘게 여기 저기 다니면서 찍은 수천장의 사진이 있는데 그 중에서 내가 Florida 해변에서 찍은 사진을 찾고 싶을 때.. 

그냥 GPT-3 나 ChatGPT 에게 찾아 달라고 하면 수 많은 인터넷 정보에서 사진들의 메타 데이터를 검색해서 찾을 테니 시간도 많이 걸리고 내가 아닌 다른 사람이 인터넷에 올린 사진을 보여 줄 수도 있습니다.

이런 경우 내 사진이 있는 폴더 정보를 제공하면서 이 안에서 찾아 보라고 얘기할 수 있겠죠.

그러면 정확히 내 사진들 속에서만 검색을 해서 보여 줄 겁니다. 리소스 사용도 저렴하게 이뤄 질 수 있겠죠.

 

이렇게 정답이 있는 소스 문서를 제공함으로서 더 안정적이고 정확한 답변을 얻어야 할 필요성이 있을 때 사용할 수 있는 예제 입니다.

 

이 예제 에서는 2020년 하계 올림픽에 대한 위키피디아 글을 사용합니다. 

참고로 이 데이터 수집 과정에 대한 소스 코드는 이곳에 있습니다.  https://github.com/openai/openai-cookbook/blob/1b21dcd6e9c9cfb4d6f0d64f1106d7353ef65c40/examples/fine-tuned_qa/olympics-1-collect-data.ipynb

 

GitHub - openai/openai-cookbook: Examples and guides for using the OpenAI API

Examples and guides for using the OpenAI API. Contribute to openai/openai-cookbook development by creating an account on GitHub.

github.com

위 데이터 수집 과정까지 다 지금 알아둘 필요는 없을 것 같아서 저는 다시 원래 페이지에 있는 내용을 다룰 겁니다.

오늘 공부할 내용은 이 페이지에 있습니다.

 

https://github.com/openai/openai-cookbook/blob/main/examples/Question_answering_using_embeddings.ipynb

 

GitHub - openai/openai-cookbook: Examples and guides for using the OpenAI API

Examples and guides for using the OpenAI API. Contribute to openai/openai-cookbook development by creating an account on GitHub.

github.com

 

우선 이곳에서 첫번째로 보여주는 소스코드 부분은 이렇습니다.

 

import numpy as np
import openai
import pandas as pd
import pickle
import tiktoken

COMPLETIONS_MODEL = "text-davinci-003"
EMBEDDING_MODEL = "text-embedding-ada-002"

위의 코드는 다음과 같이 각 줄마다 설명됩니다.

  1. import numpy as np: NumPy 라이브러리를 가져옵니다. NumPy는 과학적 계산을 위한 파이썬 패키지로, 배열과 행렬 연산에 유용합니다.
  2. import openai: OpenAI 라이브러리를 가져옵니다. OpenAI는 인공지능 모델과 API를 사용할 수 있는 플랫폼입니다.
  3. import pandas as pd: pandas 라이브러리를 가져옵니다. pandas는 데이터 분석과 조작을 위한 파이썬 패키지로, 데이터를 효율적으로 다룰 수 있는 기능을 제공합니다.
  4. import pickle: pickle 라이브러리를 가져옵니다. pickle은 객체를 직렬화하고 복원하기 위한 파이썬 표준 라이브러리입니다.
  5. import tiktoken: tiktoken 라이브러리를 가져옵니다. tiktoken은 텍스트를 토큰화하는 데 사용되는 라이브러리입니다.
  6. COMPLETIONS_MODEL = "text-davinci-003": COMPLETIONS_MODEL 변수를 정의하고, 값으로 "text-davinci-003"을 할당합니다. 이는 OpenAI의 대화형 작업을 위한 모델을 나타냅니다.
  7. EMBEDDING_MODEL = "text-embedding-ada-002": EMBEDDING_MODEL 변수를 정의하고, 값으로 "text-embedding-ada-002"를 할당합니다. 이는 OpenAI의 텍스트 임베딩 모델을 나타냅니다.
  8.  

numpy, openai 그리고 pandas 모듈을 이전 글에서 다루었습니다.

pickle과 tiktoken 모듈이 처음 보는 모듈이네요.

 

설명을 보면 이 pickle은 python object hierarchy를 byte stream 으로 혹은 그 반대로 convert 하는 모듈이라고 되어 있습니다.

https://docs.python.org/3/library/pickle.html

 

pickle — Python object serialization

Source code: Lib/pickle.py The pickle module implements binary protocols for serializing and de-serializing a Python object structure. “Pickling” is the process whereby a Python object hierarchy is...

docs.python.org

데이터 구조를 byte stream으로 변환하면 저장하거나 네트워크로 전송할 수 있습니다.

이런 것을 marshalling 이라고 하고 그 반대를 unmarshalling 이라고 합니다.

 

이런 작업을 할 수 있는 모듈은 아래 세가지가 있습니다.

 

marshal은 셋 중 가장 오래된 모듈이다. 이것은 주로 컴파일된 바이트코드 또는 인터프리터가 파이썬 모듈을 가져올 떄 얻는 .pyc 파일을 읽고 쓰기 위해 존재한다. 때문에 marshal로 객체를 직렬화할 수 있더라도, 이를 추천하지는 않는다.

 

json 모듈은 셋 중 가장 최신의 모듈이다. 이를 통해 표준 JSON 파일로 작업을 할 수 있다. json 모듈을 통해 다양한 표준 파이썬 타입(bool, dict, int, float, list, string, tuple, None)을 직렬화, 역직렬화할 수 있다. json은 사람이 읽을 수 있고, 언어에 의존적이지 않다는 장점이 있다.

 

pickle 모듈은 파이썬에서 객체를 직렬화 또는 역직렬화하는 또 다른 방식이다. json 모듈과는 다르게 객체를 바이너리 포맷으로 직렬화한다. 이는 결과를 사람이 읽을 수 없다는 것을 의미한다. 그러나 더 빠르고, 사용자 커스텀 객체 등 더 다양한 파이썬 타입으로 동작할 수 있음을 의미한다.

 

이 내용은 아래 블로그에 자세하게 정리 돼 있어서 도움이 됐습니다.

 

http://scshim.tistory.com/614

 

[Python] pickle 모듈 - 파이썬에서 객체를 영속화하는 방법

다음 글(https://realpython.com/python-pickle-module)을 번역, 정리한 글입니다. 목차 · 파이썬의 직렬화 · 파이썬 pickle 모듈 내부 · 파이썬 pickle 모듈의 프로토콜 포맷 · Picklable and Unpicklable Types · Pickled Ob

scshim.tistory.com

 

그 다음 새로운 모듈은 tiktoken 인데요 저의 경우는 이 모듈이 인스톨 되어 있지 않다고 에러가 나와서 pip install tiktoken 을 사용해서 install 했습니다.

 

 

이 tiktoken 모듈은 openai API 페이지의 Guides 의 Embeddings 페이지에서 설명 됐습니다. Limitations & risks 부분을 보시면 됩니다.

 

https://platform.openai.com/docs/guides/embeddings/limitations-risks

 

OpenAI API

An API for accessing new AI models developed by OpenAI

platform.openai.com

 

더 자세히 보시려면 위 페이지에 있는 링크들을 따라 가시면 이 tiktoken 에 대한 설명과 예제가 있는 페이지들을 보실 수 있습니다.

 

모듈에 대해 대충 알았으니 첫번째 예제를 만들어서 실행 해 보겠습니다.

 

 

첫번째 예제를 약간 변형해서 만들어 봤습니다.

우선 COMPETIONS_MODEL 변수에 davinci-003 모델 대신 ada-001 모듈을 넣었습니다.

답변 내용보다 api 테스트용이라서 비용이 저렴하고 속도가 빠른 모듈을 사용하겠습니다.

 

그 다음은 계속 사용했던 api key 를 제공하는 부분입니다. (12 ~ 16번째 줄)

 

그 다음 코드를 보면 prompt 에는 2020년 하계 올림픽 남자 높이 뛰기 부문에서 누가 우승 했는가를 물어보는 문장이 있습니다.

그리고 openai.Completion.create() api 를 사용해서 GPT-3 에게 이 질문을 하고 그 응답을 받습니다.

 

여기서 뒤에 ["choices"][0]["text"].strip(" \n") 부분을 붙이지 않으면 응답은 JSON 형식으로 출력 됩니다.

 

 

여기에서 응답 부분인 text 부분만 보기 위해서 이 부분을 넣은 겁니다.

이것을 프린트 하면 아래와 같습니다.

 

 

미국이 2020년 하계 올림픽 높이 뛰기 부문에서 우승 했다고 나옵니다.

 

이 cookbook 예제 페이지에서는 아래와 같이 나왔다고 하네요.

 

 

내가 사용한 모델(ada-001)과 이 예제가 사용한 모델(davinci-003)이 다르기 때문에 답변이 다른 겁니다.

하여간 둘 다 오답입니다.

이렇게 Completion으로 일반적인 방법으로 질문 했을 때는 오답이 나옵니다.

 

여기서 문제는 이 GPT-3 가 잘 모르면서 아무 대답이나 했다는 겁니다.

 

이렇게 되면 GPT-3의 답변을 신뢰할 수가 없겠죠.

 

이것을 방지하기 위해 prompt를 이렇게 바꾸겠습니다.

 

"""Answer the question as truthfully as possible, and if you're unsure of the answer, say "Sorry, I don't know".

Q: Who won the 2020 Summer Olympics men's high jump?
A:"""

질문하기 전에 이렇게 요청을 했습니다. 

가능하면 진실을 이야기 하고 확실하게 알지 못한다면 Sorry, I don't know라고 대답하라.

이렇게 해 두고 2020년 하계 올림픽 높이 뛰기 우승은 누가 했느냐고 묻습니다.

 

이렇게 했을 경우 제가 ada-001 모델을 사용했을 경우 Sorry, I don't know. 라는 답변을 얻었습니다.

 

davinci-003 모델을 사용한 이 예제에서도 Sorry, I don't know라는 답변을 얻었다고 하네요.

GPT-3는 2020년 하계 올림픽에서 누가 높이 뛰기에서 우승을 했는지 확실히 알지 못하는 겁니다.

 

그 다음 예제에서는 이런 prompt를 사용했습니다.

 

prompt = """Answer the question as truthfully as possible using the provided text, and if the answer is not contained within the text below, say "I don't know"

Context:
The men's high jump event at the 2020 Summer Olympics took place between 30 July and 1 August 2021 at the Olympic Stadium.
33 athletes from 24 nations competed; the total possible number depended on how many nations would use universality places 
to enter athletes in addition to the 32 qualifying through mark or ranking (no universality places were used in 2021).
Italian athlete Gianmarco Tamberi along with Qatari athlete Mutaz Essa Barshim emerged as joint winners of the event following
a tie between both of them as they cleared 2.37m. Both Tamberi and Barshim agreed to share the gold medal in a rare instance
where the athletes of different nations had agreed to share the same medal in the history of Olympics. 
Barshim in particular was heard to ask a competition official "Can we have two golds?" in response to being offered a 
'jump off'. Maksim Nedasekau of Belarus took bronze. The medals were the first ever in the men's high jump for Italy and 
Belarus, the first gold in the men's high jump for Italy and Qatar, and the third consecutive medal in the men's high jump
for Qatar (all by Barshim). Barshim became only the second man to earn three medals in high jump, joining Patrik Sjöberg
of Sweden (1984 to 1992).

Q: Who won the 2020 Summer Olympics men's high jump?
A:"""

이 prompt에서는 2020년 하계 올림픽 남자 높이뛰기 부문 금메달 리스트가 두명이 된 사연을 소개 하면서 그 이름도 거론하고 있습니다.

 

이렇게 내용을 제공하고 GPT-3에게 누가 우승을 했는지 물어 봤습니다.

 

제가 사용한 ada-001 모델은 간단하게 I don't know 라고 응답 했네요.

모델을 davinci-003로 바꿔 보았습니다.

 

답변은 이렇게 바뀌었습니다.

 

역시 비싼 모델이 좋네요.

 

openai cookbook에서도 위와 같은 답변을 얻었습니다.

 

여기서는 제대로 된 답을 얻기 위해서 그 배경 정보를 제공하는 방법을 사용했습니다.

대략 10줄 정도 되는 문장을 제공했는데요.

 

그런데 제가 처음에 얘기했던 10년치 사진에서 찾는 방법 같이 방대한 양의 정보를 제공해야 할 경우는 어떻게 할까요?

 

이런 경우를 위해서 이 페이지에서는 Embeddings를 사용하는 방법을 보여줄 것입니다.

작동 방법은 두단계로 이루어 지는데 첫번째는 관련된 정보를 검색한 후 이것을 기반으로 질문에 맞는 답변을 작성하는 겁니다.

첫번째 단계는 Embeddings API를 사용하고 두번째 단계는 Completions API를 사용합니다.

 

아래와 같은 스텝들을 밟게 됩니다.

 

The steps are:

  • Preprocess the contextual information by splitting it into chunks and create an embedding vector for each chunk.
  • 컨텍스트 정보를 청크로 분할하여 전처리하고 각 청크에 대한 임베딩 벡터를 생성합니다.
  • On receiving a query, embed the query in the same vector space as the context chunks and find the context embeddings which are most similar to the query.
  • 쿼리를 수신하면 컨텍스트 청크와 동일한 벡터 공간에 쿼리를 포함하고 쿼리와 가장 유사한 컨텍스트 포함을 찾습니다.
  • Prepend the most relevant context embeddings to the query prompt.
  • 가장 관련성이 높은 컨텍스트 임베딩을 쿼리 프롬프트 앞에 추가합니다.
  • Submit the question along with the most relevant context to GPT, and receive an answer which makes use of the provided contextual information.
  • 가장 관련성이 높은 컨텍스트와 함께 질문을 GPT에 제출하고 제공된 컨텍스트 정보를 활용하는 답변을 받습니다.

참고로 지금까지 진행한 코드는 아래와 같습니다.

import numpy as np
import openai
import pandas as pd
import pickle
import tiktoken
from pprint import pprint

# COMPLETIONS_MODEL = "text-davinci-003"
COMPLETIONS_MODEL = "text-ada-001"
EMBEDDING_MODEL = "text-embedding-ada-002"

def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')


prompt = """Answer the question as truthfully as possible using the provided text, and if the answer is not contained within the text below, say "I don't know"

Context:
The men's high jump event at the 2020 Summer Olympics took place between 30 July and 1 August 2021 at the Olympic Stadium.
33 athletes from 24 nations competed; the total possible number depended on how many nations would use universality places 
to enter athletes in addition to the 32 qualifying through mark or ranking (no universality places were used in 2021).
Italian athlete Gianmarco Tamberi along with Qatari athlete Mutaz Essa Barshim emerged as joint winners of the event following
a tie between both of them as they cleared 2.37m. Both Tamberi and Barshim agreed to share the gold medal in a rare instance
where the athletes of different nations had agreed to share the same medal in the history of Olympics. 
Barshim in particular was heard to ask a competition official "Can we have two golds?" in response to being offered a 
'jump off'. Maksim Nedasekau of Belarus took bronze. The medals were the first ever in the men's high jump for Italy and 
Belarus, the first gold in the men's high jump for Italy and Qatar, and the third consecutive medal in the men's high jump
for Qatar (all by Barshim). Barshim became only the second man to earn three medals in high jump, joining Patrik Sjöberg
of Sweden (1984 to 1992).

Q: Who won the 2020 Summer Olympics men's high jump?
A:"""

result = openai.Completion.create(
    prompt=prompt,
    temperature=0,
    max_tokens=300,
    top_p=1,
    frequency_penalty=0,
    presence_penalty=0,
    model=COMPLETIONS_MODEL
)["choices"][0]["text"].strip(" \n")


pprint(result)

 

1) Preprocess the document library

 

이제 문서를 제공하고 그 안에서 GPT-3 에게 찾아보라고 할 건데요.

# We have hosted the processed dataset, so you can download it directly without having to recreate it.
# This dataset has already been split into sections, one row for each section of the Wikipedia page.

df = pd.read_csv('https://cdn.openai.com/API/examples/data/olympics_sections_text.csv')
df = df.set_index(["title", "heading"])
print(f"{len(df)} rows in the data.")
df.sample(5)

문서는 이미 작성돼 있는 olympics_sections_text.csv를 원격으로 불러 옵니다. 이때 Pandas의 read.csv() 함수를 사용합니다.

이 csv 파일에는 title, heading, content, tokens라는 필드가 있고 그 안에 각각 정보들이 있습니다.

 

다음 줄에서는 pandas의 dataframe 의 set_index()를 사용해서 title과 heading 컬럼을 인덱스로 설정합니다.

그 다음은 dataframe에 있는 데이터가 총 몇줄인지를 출력하고 그 다음에 그 중 5개를 추리는 일을 합니다.

df.sample(5) 은 랜덤하게 5개의 줄만 추리는 일을 합니다.

 

전체 소스코드를 보면 이렇습니다.

 

import pandas as pd
from pprint import pprint

# We have hosted the processed dataset, so you can download it directly without having to recreate it.
# This dataset has already been split into sections, one row for each section of the Wikipedia page.

df = pd.read_csv('https://cdn.openai.com/API/examples/data/olympics_sections_text.csv')
df = df.set_index(["title", "heading"])
print(f"{len(df)} rows in the data.")
result = df.sample(5)
pprint(result)

아직 openai도 사용하지 않았고 데이터를 다루기 위해서 pandas 모듈만 사용했습니다.

 

여기까지를 실행하면 아래와 같이 나옵니다.

 

이 문서에는 총 3964 개의 row들이 있고 sample(5)를 통해서 random 하게 뽑은 데이터는 위와 같이 첫번째 실행과 두번째 실행이 각각 다릅니다.

 

이제 데이터 세트가 확보 됐으니 각 데이터 마다 임베딩 값을 받아 오는 작업을 해야 합니다.

 

def get_embedding(text: str, model: str=EMBEDDING_MODEL) -> list[float]:
    result = openai.Embedding.create(
      model=model,
      input=text
    )
    return result["data"][0]["embedding"]

get_embedding() 이라는 함수를 만들었습니다.

질문과 모델을 openai.Embedding.create() api 를 통해서 전달하고 그 중 embedding에 해당 하는 데이터만 list 형식으로 return 합니다.

 

def compute_doc_embeddings(df: pd.DataFrame) -> dict[tuple[str, str], list[float]]:
    """
    Create an embedding for each row in the dataframe using the OpenAI Embeddings API.
    
    Return a dictionary that maps between each embedding vector and the index of the row that it corresponds to.
    """
    return {
        idx: get_embedding(r.content) for idx, r in df.iterrows()
    }

compute_doc_embeddings() 라는 함수 입니다.

 

위의 코드는 다음과 같이 각 줄마다 설명됩니다.

  1. def compute_doc_embeddings(df: pd.DataFrame) -> dict[tuple[str, str], list[float]]:: compute_doc_embeddings 함수를 정의합니다. 이 함수는 pandas DataFrame을 입력받고, 각 행에 대한 임베딩을 OpenAI Embeddings API를 사용하여 생성합니다. 반환값은 각 임베딩 벡터와 해당하는 행 인덱스를 매핑하는 딕셔너리입니다. 반환값의 타입은 (str, str) 튜플을 키로하고, float 값들의 리스트를 값으로 갖는 딕셔너리입니다.
  2. return {idx: get_embedding(r.content) for idx, r in df.iterrows()}: 딕셔너리 컴프리헨션을 사용하여 임베딩 딕셔너리를 생성하여 반환합니다. df.iterrows()를 사용하여 DataFrame의 각 행에 대해 순회하며 행 인덱스와 행의 content를 get_embedding() 함수에 전달하여 임베딩을 얻습니다. 이 임베딩을 해당하는 행 인덱스와 매핑하여 딕셔너리에 추가합니다.

 

여기서는 pandas의 dataframe으로 확보된 데이터를 get_embedding() 함수를 호출하면서 전달합니다.

여기서 return 값은 임베딩 벡터와 이에 해당하는 행의 인덱스를 매핑하는 사전(dict) 를 반환합니다.

 

tuple은 순서가 있는 객체의 집합을 나타내는 데이터 타입입니다. 새로운 요소를 추가하거나 기존 요소를 삭제할 수 없습니다.

dict 는 key, value 로 이루어져 있습니다. 접근해서 수정이 가능합니다.

 

df.itterrows()는 행에 대해서 순환 반복 한다는 겁니다.

이렇게 되면 데이터세트의 각 행마다 get_embedding()을 호출하고 거기에 대한 embedding 값을 받게 됩니다.

행이 총 3964였으니 openai.Embedding.create() api가 그만큼 호출 된다는 얘기이고 또 그만큼 과금 된다는 얘기이네요.

 

얼마가 과금 되는지는 나중에 보기로 하고 다음 코드를 살펴 보겠습니다.

 

def load_embeddings(fname: str) -> dict[tuple[str, str], list[float]]:
    """
    Read the document embeddings and their keys from a CSV.
    
    fname is the path to a CSV with exactly these named columns: 
        "title", "heading", "0", "1", ... up to the length of the embedding vectors.
    """
    
    df = pd.read_csv(fname, header=0)
    max_dim = max([int(c) for c in df.columns if c != "title" and c != "heading"])
    return {
           (r.title, r.heading): [r[str(i)] for i in range(max_dim + 1)] for _, r in df.iterrows()
    }
  1. def load_embeddings(fname: str) -> dict[tuple[str, str], list[float]]:: load_embeddings 함수를 정의합니다. 이 함수는 CSV에서 문서 임베딩과 그에 해당하는 키를 읽어옵니다. fname은 다음과 같은 이름을 가진 열이 정확히 포함된 CSV 파일의 경로입니다: "title", "heading", "0", "1", ..., 임베딩 벡터의 길이까지.
  2. df = pd.read_csv(fname, header=0): pd.read_csv() 함수를 사용하여 주어진 CSV 파일을 DataFrame으로 읽어옵니다. header=0을 설정하여 첫 번째 행을 열 이름으로 사용합니다.
  3. max_dim = max([int(c) for c in df.columns if c != "title" and c != "heading"]): DataFrame의 열 중 "title"과 "heading"이 아닌 열들에 대해 정수로 변환한 값을 리스트로 만든 후, 그 중 가장 큰 값을 max_dim 변수에 할당합니다. 이는 임베딩 벡터의 최대 차원을 결정합니다.
  4. return {(r.title, r.heading): [r[str(i)] for i in range(max_dim + 1)] for _, r in df.iterrows()}: 딕셔너리 컴프리헨션을 사용하여 임베딩 딕셔너리를 생성하여 반환합니다. df.iterrows()를 사용하여 DataFrame의 각 행에 대해 순회하며 행의 "title"과 "heading"을 키로, 열 이름에 해당하는 인덱스를 사용하여 값을 추출하여 임베딩 벡터로 사용합니다. 이를 키와 값을 매핑하여 딕셔너리에 추가합니다.

 

load_embeddings() 함수에서는 fname (파일이름)을 입력값으로 받습니다. 그리고 dirc[tuble[str,str], list[float] 형으로 계산된 값을 반환 합니다.

 

CSV 파일에서 key값과 거기에 해당하는 임베딩 값들을 불러 옵니다.

 

데이터 세트의 컬럼수 만큼 for 루프를 돌리게 되고 (title, heading 이라고 컬럼 이름이 돼 있는 행은 제외) 거기서 얻은 최대값을 max_dim 변수에 넣습니다.

 

그리고 반환 값은 각 행만큼 for 루프를 돌리고 그 행마다 max_dim + 1 만큼 루프를 돌려서 얻은 값을 반환하게 됩니다.

 

해당 파일은 아래와 같이 지정해 줍니다.

 

document_embeddings = load_embeddings("https://cdn.openai.com/API/examples/data/olympics_sections_document_embeddings.csv")

# ===== OR, uncomment the below line to recaculate the embeddings from scratch. ========

# document_embeddings = compute_doc_embeddings(df)

이 파일의 내용은 이렇습니다.

 

 

이 파일에는 각 행마다 embedding 값이 있습니다.

이것을 하지 않고 compute_doc_embeddings(df)를 한다면 아래 문서를 불러와서 각 행마다 openai.Embedding.create() api를 호출해서 임베딩 값을 받는 작업을 할 겁니다.

https://cdn.openai.com/API/examples/data/olympics_sections_text.csv

 

그러면 이 api를 3964번 호출하고 그만큼 과금이 될 겁니다.

그것을 피하게 하기 위해서 openai에서는 이미 임베딩 작업을 끝낸 아래 문서를 제공하는 것 같습니다.

 

document_embeddings = load_embeddings("https://cdn.openai.com/API/examples/data/olympics_sections_document_embeddings.csv")

아래는 일단 샘플로 일부분만 프린트 해 본 겁니다.

# An example embedding:
example_entry = list(document_embeddings.items())[0]
print(f"{example_entry[0]} : {example_entry[1][:5]}... ({len(example_entry[1])} entries)")

지금까지의 전체 코드를 보면 이렇습니다.

import numpy as np
import openai
import pandas as pd
import pickle
import tiktoken
from pprint import pprint

# COMPLETIONS_MODEL = "text-davinci-003"
COMPLETIONS_MODEL = "text-ada-001"
EMBEDDING_MODEL = "text-embedding-ada-002"

def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

# We have hosted the processed dataset, so you can download it directly without having to recreate it.
# This dataset has already been split into sections, one row for each section of the Wikipedia page.

df = pd.read_csv('https://cdn.openai.com/API/examples/data/olympics_sections_text.csv')
df = df.set_index(["title", "heading"])
print(f"{len(df)} rows in the data.")
result = df.sample(5)
pprint(result)

def get_embedding(text: str, model: str=EMBEDDING_MODEL) -> list[float]:
    result = openai.Embedding.create(
      model=model,
      input=text
    )
    return result["data"][0]["embedding"]

def compute_doc_embeddings(df: pd.DataFrame) -> dict[tuple[str, str], list[float]]:
    """
    Create an embedding for each row in the dataframe using the OpenAI Embeddings API.
    Return a dictionary that maps between each embedding vector and the index of the row that it corresponds to.
    """
    return {
        idx: get_embedding(r.content) for idx, r in df.iterrows()
    }
    
def load_embeddings(fname: str) -> dict[tuple[str, str], list[float]]:
    """
    Read the document embeddings and their keys from a CSV.
    fname is the path to a CSV with exactly these named columns: 
        "title", "heading", "0", "1", ... up to the length of the embedding vectors.
    """
    
    df = pd.read_csv(fname, header=0)
    max_dim = max([int(c) for c in df.columns if c != "title" and c != "heading"])
    return {
           (r.title, r.heading): [r[str(i)] for i in range(max_dim + 1)] for _, r in df.iterrows()
    }
    
document_embeddings = load_embeddings("https://cdn.openai.com/API/examples/data/olympics_sections_document_embeddings.csv")
# ===== OR, uncomment the below line to recaculate the embeddings from scratch. ========
# document_embeddings = compute_doc_embeddings(df)

# An example embedding:
example_entry = list(document_embeddings.items())[0]
print(f"{example_entry[0]} : {example_entry[1][:5]}... ({len(example_entry[1])} entries)")

우선 25번째줄 pprint(result) 는 이전에 설명했던 것이고 그 아래 코드들은 이와는 별도의 코드 블럭입니다.

 

document_embeddings = load_embeddings("https://cdn.openai.com/API/examples/data/olympics_sections_document_embeddings.csv")
# ===== OR, uncomment the below line to recaculate the embeddings from scratch. ========
# document_embeddings = compute_doc_embeddings(df)

load_embeddings() 함수에 위의 csv 파일을 넘겨 주면 load_embeddings() 함수에서는 이 파일을 읽어서 키값과 임베딩 값들을 dict 형으로 반환합니다.

 

만약 위와 같이 사용하지 않고 그 아래에 있는 document_embeddings = compute_doc_embeddings(df)를 사용했다면 21번째 줄에 있는 df를 compute_doc_embeddings() 함수로 보내서 각 행마다 임베딩 값을 얻어 오겠지만...

df = pd.read_csv('https://cdn.openai.com/API/examples/data/olympics_sections_text.csv')
df = df.set_index(["title", "heading"])

여기서는 이 작업을 하지 않고 이미 임베딩 값을 계산해서 가지고 있는 문서를 로드해서 사용하기 때문에 그 윗부분은 필요 없게 된것입니다.

 

하지만 제대로 작동하기 위해서는 compute_doc_embeddings(df)를 사용해서 데이터세트의 각 행마다 임베딩 값을 받아오는 절차를 거치는 것이 정석입니다.

 

25번째 줄의 pprint(result) 이전 단계를 제외하고 그 밑의 흐름을 살펴보면 이렇습니다.

 

먼저 document_embeddings 변수에 위에 명시한 csv 파일을 로딩 합니다.

이 csv 파일에는 임베딩 값이 세팅돼 있습니다.

정식적인 절차를 거치려면 21번째 줄에서 만든 df 를 compute_doc_embeddings(df) 함수로 보내고 이 함수에서는 각 행마다 get_embedding() 함수를 통해 임베딩 값을 얻습니다.

 

그러면 openai.Embedding.create() api는 각 행의 갯수 만큼 (3694) 호출이 되서 각 행에 맞는 임베딩 값을 매칭 시켜 줄 것입니다.

 

이 코드를 실행하면 아래와 같이 나옵니다.

빨간 색으로 표시된 부분이 이번에 새로 추가한 코드에서 출력한 것입니다.

 

# An example embedding:
example_entry = list(document_embeddings.items())[0]
print(f"{example_entry[0]} : {example_entry[1][:5]}... ({len(example_entry[1])} entries)")

해당 문서의 첫 행을 출력한 것입니다.

 

여기까지가 데이터 세트를 가지고 각 데이터마다 거기에 해당하는 임베딩 값을 얻는것 까지 완성 했습니다.

 

이제 필요한 소스데이터의 모든 행에 대해 임베딩 값을 가지고 있으니 사용자가 질문하면 의미적으로 알맞는 답을 제공할 준비가 돼 있습니다.

 

다음에는 이러한 사용자의 질문에 알맞는 답변을 내 놓는 작업을 하겠습니다.

 

2) Find the most similar document embeddings to the question embedding

 

해야할 작업은 일단 질문을 받으면 이 질문에 대한 임베딩을 계산하고 이와 가장 유사한 문서 섹션을 찾는 것입니다.

 

def vector_similarity(x: list[float], y: list[float]) -> float:
    """
    Returns the similarity between two vectors.
    
    Because OpenAI Embeddings are normalized to length 1, the cosine similarity is the same as the dot product.
    """
    return np.dot(np.array(x), np.array(y))
  1. def vector_similarity(x: list[float], y: list[float]) -> float:: vector_similarity 함수를 정의합니다. 이 함수는 두 벡터 간의 유사도를 반환합니다.
  2. return np.dot(np.array(x), np.array(y)): 두 벡터 x와 y의 유사도를 계산하여 반환합니다. np.array(x)와 np.array(y)를 사용하여 리스트를 NumPy 배열로 변환한 후, np.dot() 함수를 사용하여 두 벡터의 내적(도트 곱)을 계산합니다. OpenAI Embeddings는 길이가 1로 정규화되어 있기 때문에 코사인 유사도는 내적과 동일합니다.

 

첫번째 vector_similarity() 함수는 두개의 list를 입력값으로 받고 float 형식의 값을 반환합니다.

np.dot() 함수를 사용해서 두 벡터의 유사성 값을 반환 하는 것이죠.

OpenAI의 임베딩 값은 최대값이 1로 세팅 되어 있어서 np.dot()으로 하나 cosine similarity로 하나 그 값은 같습니다.

 

def order_document_sections_by_query_similarity(query: str, contexts: dict[(str, str), np.array]) -> list[(float, (str, str))]:
    """
    Find the query embedding for the supplied query, and compare it against all of the pre-calculated document embeddings
    to find the most relevant sections. 
    
    Return the list of document sections, sorted by relevance in descending order.
    """
    query_embedding = get_embedding(query)
    
    document_similarities = sorted([
        (vector_similarity(query_embedding, doc_embedding), doc_index) for doc_index, doc_embedding in contexts.items()
    ], reverse=True)
    
    return document_similarities
  1. def order_document_sections_by_query_similarity(query: str, contexts: dict[(str, str), np.array]) -> list[(float, (str, str))]:: order_document_sections_by_query_similarity 함수를 정의합니다. 이 함수는 주어진 쿼리와 사전에 계산된 문서 임베딩과의 유사성을 비교하여 가장 관련성이 높은 섹션을 찾습니다. 결과로 관련성이 내림차순으로 정렬된 문서 섹션의 리스트를 반환합니다.
  2. query_embedding = get_embedding(query): 주어진 쿼리에 대한 임베딩을 얻어옵니다. get_embedding() 함수를 사용하여 쿼리의 임베딩 벡터를 가져옵니다.
  3. document_similarities = sorted([...], reverse=True): 문서 임베딩과 쿼리 임베딩 간의 유사성을 비교하여 관련성이 내림차순으로 정렬된 리스트를 생성합니다. contexts.items()를 통해 사전의 각 항목을 순회하면서 문서 인덱스와 문서 임베딩을 가져옵니다. vector_similarity() 함수를 사용하여 쿼리 임베딩과 문서 임베딩 간의 유사도를 계산하고, 결과를 튜플로 구성하여 리스트에 추가합니다.
  4. return document_similarities: 문서의 관련성이 내림차순으로 정렬된 문서 섹션의 리스트를 반환합니다.

 

그 다음 은 order_document_sections_by_query_similarity() 함수 입니다.

 

질문에 대한 임베딩 값을 get_embedding() 함수를 통해 받습니다.

그리고 그 값을 dict 형식으로 되어 있는 context의 item 만큼 for 루프를 돌리면서 각 행의 임베딩 값과의 유사성 값을 vector_similarity() 함수를 통해 얻어 옵니다. 그리고 그 값을 정렬 합니다.

이 정렬된 값은 document_similarities라는 변수에 담겨서 return 됩니다.

 

이제 질문을 던지기만 하면 됩니다.

 

order_document_sections_by_query_similarity("Who won the men's high jump?", document_embeddings)[:5]
order_document_sections_by_query_similarity("Who won the women's high jump?", document_embeddings)[:5]

지금까지의 코드는 이렇습니다.

 

실행되지 않는 부분은 모두 주석 처리 했습니다.

import numpy as np
import openai
import pandas as pd
import pickle
import tiktoken
from pprint import pprint

# COMPLETIONS_MODEL = "text-davinci-003"
COMPLETIONS_MODEL = "text-ada-001"
EMBEDDING_MODEL = "text-embedding-ada-002"

def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

# We have hosted the processed dataset, so you can download it directly without having to recreate it.
# This dataset has already been split into sections, one row for each section of the Wikipedia page.

"""
df = pd.read_csv('https://cdn.openai.com/API/examples/data/olympics_sections_text.csv')
df = df.set_index(["title", "heading"])
print(f"{len(df)} rows in the data.")
result = df.sample(5)
pprint(result)
"""

def get_embedding(text: str, model: str=EMBEDDING_MODEL) -> list[float]:
    result = openai.Embedding.create(
      model=model,
      input=text
    )
    return result["data"][0]["embedding"]

#def compute_doc_embeddings(df: pd.DataFrame) -> dict[tuple[str, str], list[float]]:
    """
    Create an embedding for each row in the dataframe using the OpenAI Embeddings API.
    Return a dictionary that maps between each embedding vector and the index of the row that it corresponds to.
    """
#    return {
#        idx: get_embedding(r.content) for idx, r in df.iterrows()
#    }
   
def load_embeddings(fname: str) -> dict[tuple[str, str], list[float]]:
    """
    Read the document embeddings and their keys from a CSV.
    fname is the path to a CSV with exactly these named columns: 
        "title", "heading", "0", "1", ... up to the length of the embedding vectors.
    """
    
    df = pd.read_csv(fname, header=0)
    max_dim = max([int(c) for c in df.columns if c != "title" and c != "heading"])
    return {
           (r.title, r.heading): [r[str(i)] for i in range(max_dim + 1)] for _, r in df.iterrows()
    }
    
document_embeddings = load_embeddings("https://cdn.openai.com/API/examples/data/olympics_sections_document_embeddings.csv")
# ===== OR, uncomment the below line to recaculate the embeddings from scratch. ========
# document_embeddings = compute_doc_embeddings(df)

"""
# An example embedding:
example_entry = list(document_embeddings.items())[0]
print(f"{example_entry[0]} : {example_entry[1][:5]}... ({len(example_entry[1])} entries)")
"""
def vector_similarity(x: list[float], y: list[float]) -> float:
    """
    Returns the similarity between two vectors.
    
    Because OpenAI Embeddings are normalized to length 1, the cosine similarity is the same as the dot product.
    """
    return np.dot(np.array(x), np.array(y))

def order_document_sections_by_query_similarity(query: str, contexts: dict[(str, str), np.array]) -> list[(float, (str, str))]:
    """
    Find the query embedding for the supplied query, and compare it against all of the pre-calculated document embeddings
    to find the most relevant sections. 
    
    Return the list of document sections, sorted by relevance in descending order.
    """
    query_embedding = get_embedding(query)
    
    document_similarities = sorted([
        (vector_similarity(query_embedding, doc_embedding), doc_index) for doc_index, doc_embedding in contexts.items()
    ], reverse=True)
    
    return document_similarities
    
result = order_document_sections_by_query_similarity("Who won the men's high jump?", document_embeddings)[:5]
result2 =  order_document_sections_by_query_similarity("Who won the women's high jump?", document_embeddings)[:5]
pprint(result)
pprint(result2)

 

이 소스 코드의 실행 결과는 아래와 같습니다.

 

 

각 질문에 대해 가장 유사한 섹션 5군데를 뽑아 내는데 성공했습니다.

 

이제 질문에 가장 유사한 내용을 소스코드에서 찾아 내는데까지 성공했습니다.

여기까지 하는데 이용한 api 는 openai.Embedding.create() 입니다.

이제 이것을 가지고 제대로 된 답변을 하도록 하면 됩니다.

이 작업은 openai.Completion.create() api로 할 겁니다.

이 api를 사용하기 위해 적절한 prompt를 만드는 일을 먼저 하겠습니다.

 

3) Add the most relevant document sections to the query prompt

MAX_SECTION_LEN = 500
SEPARATOR = "\n* "
ENCODING = "gpt2"  # encoding for text-davinci-003

encoding = tiktoken.get_encoding(ENCODING)
separator_len = len(encoding.encode(SEPARATOR))

f"Context separator contains {separator_len} tokens"
  1. MAX_SECTION_LEN = 500: 최대 섹션 길이를 500으로 설정합니다. 섹션은 문서에서 추출한 일부 텍스트입니다.
  2. SEPARATOR = "\n* ": 섹션 사이에 삽입되는 구분자입니다. 각 섹션은 구분자로 구분됩니다.
  3. ENCODING = "gpt2": 텍스트를 인코딩할 때 사용되는 인코딩 방식입니다. "gpt2"를 사용합니다.
  4. encoding = tiktoken.get_encoding(ENCODING): tiktoken 라이브러리를 사용하여 지정된 인코딩 방식으로 인코딩 객체를 생성합니다. 이 객체를 사용하여 텍스트를 토큰화하고 인코딩합니다.
  5. separator_len = len(encoding.encode(SEPARATOR)): 인코딩된 구분자의 길이를 계산합니다. 구분자를 인코딩하고 그 길이를 측정하여 변수 separator_len에 저장합니다.
  6. f"Context separator contains {separator_len} tokens": f-string을 사용하여 변수 separator_len의 값을 포함한 문자열을 생성합니다. 이 문자열은 구분자가 포함하는 토큰 수에 대한 정보를 제공합니다.

 

이 코드에서 나중에 써먹는 부분은 MAX_SECTION_LEN 부분과 seperator_len 부분입니다.

seperator_len은 "gpt2"를 tiktoken을 사용해서 인코딩을 먼저 합니다.

참고로 tiktoken 의 get_encoding() 을 보려면 아래 페이지에 가면 볼 수 있습니다.

 

https://github.com/openai/tiktoken/blob/main/tiktoken/registry.py

 

GitHub - openai/tiktoken

Contribute to openai/tiktoken development by creating an account on GitHub.

github.com

 

그리고 나서 SEPARATOR인 "\n* " 도 같이 인코딩 한 length를 separator_len 에 담습니다.

여기서 사용한 encode()를 보려면 이곳으로 가면 됩니다.

 

https://github.com/openai/tiktoken/blob/main/tiktoken/core.py

 

GitHub - openai/tiktoken

Contribute to openai/tiktoken development by creating an account on GitHub.

github.com

 

이 MAX_SECTION_LEN 와 seperator_len은 아래 함수에서 사용합니다.

 

def construct_prompt(question: str, context_embeddings: dict, df: pd.DataFrame) -> str:
    """
    Fetch relevant 
    """
    most_relevant_document_sections = order_document_sections_by_query_similarity(question, context_embeddings)
    
    chosen_sections = []
    chosen_sections_len = 0
    chosen_sections_indexes = []
     
    for _, section_index in most_relevant_document_sections:
        # Add contexts until we run out of space.        
        document_section = df.loc[section_index]
        
        chosen_sections_len += document_section.tokens + separator_len
        if chosen_sections_len > MAX_SECTION_LEN:
            break
            
        chosen_sections.append(SEPARATOR + document_section.content.replace("\n", " "))
        chosen_sections_indexes.append(str(section_index))
            
    # Useful diagnostic information
    print(f"Selected {len(chosen_sections)} document sections:")
    print("\n".join(chosen_sections_indexes))
    
    header = """Answer the question as truthfully as possible using the provided context, and if the answer is not contained within the text below, say "I don't know."\n\nContext:\n"""
    
    return header + "".join(chosen_sections) + "\n\n Q: " + question + "\n A:"
  1. def construct_prompt(question: str, context_embeddings: dict, df: pd.DataFrame) -> str: construct_prompt라는 함수를 정의합니다. 이 함수는 질문, 문맥 임베딩 및 데이터프레임을 인자로 받고, 문자열을 반환합니다.
  2. most_relevant_document_sections = order_document_sections_by_query_similarity(question, context_embeddings): 주어진 질문과 문맥 임베딩을 사용하여 가장 관련성이 높은 문서 섹션을 찾습니다. order_document_sections_by_query_similarity 함수를 호출하여 결과를 가져옵니다.
  3. chosen_sections = []: 선택된 문서 섹션을 저장하기 위한 빈 리스트를 생성합니다.
  4. chosen_sections_len = 0: 선택된 섹션들의 총 길이를 저장하는 변수를 초기화합니다.
  5. chosen_sections_indexes = []: 선택된 섹션들의 인덱스를 저장하기 위한 빈 리스트를 생성합니다.
  6. for _, section_index in most_relevant_document_sections:: most_relevant_document_sections에서 각 문서 섹션의 인덱스를 반복합니다.
  7. document_section = df.loc[section_index]: 데이터프레임에서 해당 인덱스의 문서 섹션을 가져옵니다.
  8. chosen_sections_len += document_section.tokens + separator_len: 선택된 섹션들의 총 길이에 현재 섹션의 길이와 구분자의 길이를 추가합니다.
  9. if chosen_sections_len > MAX_SECTION_LEN: break: 선택된 섹션들의 총 길이가 최대 섹션 길이를 초과하면 반복문을 종료합니다.
  10. chosen_sections.append(SEPARATOR + document_section.content.replace("\n", " ")): 선택된 섹션 리스트에 현재 섹션을 추가합니다. 구분자와 개행 문자를 적절히 처리하여 섹션을 생성합니다.
  11. chosen_sections_indexes.append(str(section_index)): 선택된 섹션의 인덱스를 문자열로 변환하여 인덱스 리스트에 추가합니다.
  12. print(f"Selected {len(chosen_sections)} document sections:"): 선택된 섹션의 개수를 출력합니다.
  13. print("\n".join(chosen_sections_indexes)): 선택된 섹션의 인덱스를 출력합니다.
  14. header = """Answer the question as truthfully as possible using the provided context, and if the answer is not contained within the text below, say "I don't know."\n\nContext:\n""": 질문에 대해 가능한 정직하게 답하되, 제공된 문맥을 사용하여 답변을 찾을 수 없는 경우 "I don't know."라고 말하라는 헤더를 생성합니다.
  15. return header + "".join(chosen_sections) + "\n\n Q: " + question + "\n A:": 헤더, 선택된 섹션들, 질문을 조합하여 최종 프롬프트 문자열을 생성하고 반환합니다.

 

이 construct_prompt() 함수는 question (string)과 context_embeddings (dict type) 그리고 df (pd.DataFrame type) 이렇게 3가지를 입력값으로 받습니다.

question은 prompt로 쓰일 질문일테고 context_embeddings는 나중에 보게 될텐데 임베딩 값을 가지고 있는 csv인 document_embeddings 입니다.  이렇게 선언을 했었죠.

document_embeddings = load_embeddings("https://cdn.openai.com/API/examples/data/olympics_sections_document_embeddings.csv")

정식으로 하려면 위와 같이 하지 않고 아래처럼 compute_doc_embeddings() 함수로 df 를 보내서 임베딩 값을 따로 만들어야 합니다. 

document_embeddings = compute_doc_embeddings(df)

그러면 해당 api를 3천번 넘게 호출할 것이고 그만큼 과금이 될 겁니다.

그래서 여기서는 위에서와 같이 임베딩 값이 이미 있는 문서를 그냥 불러와서 씁니다.

 

세번째 파라미터인 df는 이 소스코드 초반에 선언 했습니다.

df = pd.read_csv('https://cdn.openai.com/API/examples/data/olympics_sections_text.csv')
df = df.set_index(["title", "heading"])

 

이렇게 3가지를 보내서 질문에 해당하는 알맞는 답을 만드는게 이 함수가 하는 일입니다.

return 값은 string type 입니다.

 

함수 내부를 보면 order_document_sections_by_query_similarity() 함수에 question과 context_embedding을 보냅니다.

그래서 질문과 context_embedding의 각 행에 있는 정보들간의 similarities를 구하다음 sorting 한 값을 반환하는게 그 함수가 하는 일입니다.

 

반환 받은 값은 most_relevant_document_sections 변수에 담기게 됩니다.

 

그 다음은 이 변수안에 있는 행 만큼 for 루프를 돕니다.

이 루프에서 하는 일은 각 행의 위치 값을 document_section에 넣습니다.

여기서 사용한 df.loc() 에 대해서 알고 싶으면 이곳에 가서 보면 됩니다.

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html

 

pandas.DataFrame.loc — pandas 1.5.3 documentation

A slice object with labels, e.g. 'a':'f'. Warning Note that contrary to usual python slices, both the start and the stop are included

pandas.pydata.org

 

그 다음에 document_section의 tokens 값과 separator_len 을 더한 값을 chosen_section_len에 넣습니다.

만약에 이 chosen_section_len 이 MAX_SECTION_LEN (500) 보다 크면 break 합니다.

크지 않으면 아래 부분을 계속 실행을 합니다.

 

다음은 documentsection의 content에 있는 \n을 " " 로 replace 해서 SEPARATOR 와 합한 값을 chosen_section에 넣습니다.

SEPARATOR 가 이미 \n 이 들어가 있기 때문에 document_section의 content에 있는 \n 가 필요 없기 때문입니다.

이 부분은 예쁘게 출력하기 위해서 필요한 부분 입니다.

그 다음엔 chosen_sections_indexes에 section_index를 추가 합니다.

 

그 다음은 출력문이 두개 나오게 됩니다. 이건 나중에 보겠습니다.

그리고 header 에 정확한 정보를 제공하고 답이 확실하지 않으면 I don't know라고 대답하라고 하는 지시문이 있습니다.

그 다음은 이 지시문과 질문을 합친 값을 return 합니다.

 

이 construct_prompt() 함수는 바로 아래에서 사용이 됩니다.

 

prompt = construct_prompt(
    "Who won the 2020 Summer Olympics men's high jump?",
    document_embeddings,
    df
)

print("===\n", prompt)

누가 2020년 하계 올림픽 남자 높이 뛰기 우승자인가? 라는 질문과 document_embeddings와 df를 construct_prompt() 에 보냅니다.

 

그 다음 이 prompt를 프린트 합니다.

 

여기까지 실행하면 아래와 같은 결과물을 얻습니다.

 

 

질문과 유사성이 있는 section을 두가지 찾았습니다.

그리고 그 두가지에 해당하는 정보를 출력했습니다.

이것은 construct_prompt() 안에 있는 print에서 한 일입니다.

 

그 다음은 맨 마지막의 print("===\n", prompt)에서 출력한 내용입니다.

 

header와 context와 질문이 출력 됐습니다.

 

이제 이 header와 context와 질문을 가지고 거기에 맞는 답을 A: 에 출력하는 작업만 하면 됩니다.

 

 

4) Answer the user's question based on the context.

Completions API를 사용해서 답을 얻으면 됩니다.

 

COMPLETIONS_API_PARAMS = {
    # We use temperature of 0.0 because it gives the most predictable, factual answer.
    "temperature": 0.0,
    "max_tokens": 300,
    "model": COMPLETIONS_MODEL,
}
def answer_query_with_context(
    query: str,
    df: pd.DataFrame,
    document_embeddings: dict[(str, str), np.array],
    show_prompt: bool = False
) -> str:
    prompt = construct_prompt(
        query,
        document_embeddings,
        df
    )
    
    if show_prompt:
        print(prompt)

    response = openai.Completion.create(
                prompt=prompt,
                **COMPLETIONS_API_PARAMS
            )

    return response["choices"][0]["text"].strip(" \n")
  1. def answer_query_with_context(query: str, df: pd.DataFrame, document_embeddings: dict[(str, str), np.array], show_prompt: bool = False) -> str: answer_query_with_context라는 함수를 정의합니다. 이 함수는 질문, 데이터프레임, 문서 임베딩 및 show_prompt 옵션을 인자로 받고, 문자열을 반환합니다.
  2. prompt = construct_prompt(query, document_embeddings, df): construct_prompt 함수를 호출하여 질문과 문서 임베딩, 데이터프레임을 기반으로 프롬프트를 생성합니다.
  3. if show_prompt: print(prompt): show_prompt 옵션이 True인 경우 프롬프트를 출력합니다.
  4. response = openai.Completion.create(prompt=prompt, **COMPLETIONS_API_PARAMS): OpenAI의 Completion API를 사용하여 프롬프트에 대한 응답을 생성합니다. COMPLETIONS_API_PARAMS는 함수에서 정의되지 않았으므로 해당 부분은 누락된 것으로 보입니다.
  5. return response["choices"][0]["text"].strip(" \n"): API 응답에서 추출한 결과 텍스트를 반환합니다. 결과 텍스트에서 앞뒤로 공백과 개행 문자를 제거합니다.

 

answer_query_with_context() 함수가 그 답변을 하는 일을 할 겁니다.

 

입력 값으로 질문과 df와 document_embeddings를 받습니다. 추가로 show_prompt 도 False로 설정한 입력값이 있습니다.

그리고 return 값은 string 입니다.

 

prompt를 세팅하고 if 문으로 가는데 show_prompt가 False 이므로 그 아래 print(prompt)는 실행을 하지 않습니다.

그 아래 openai.Completion.create() api를 호출합니다. 

이 때 전달하는 정보는 prompt이고 거기에 더해서 COMPLETIONS_API_PARAMS도 보내 집니다.

 

prompt는 아까 위에서 보았던 construct_prompt() 에서 얻은 내용입니다.

 

이렇게 해서 openai.Completion.create() api로부터 response를 받게 되는데 그 값은 response에 담깁니다.

이 response 중에서 choices안의 text 내용만 return 합니다.

 

이제 이 함수를 이용해서 질문을 던지게 되면 COMPLETION api를 통해서 제대로 된 답을 얻을 수있게 됩니다.

 

query = "Who won the 2020 Summer Olympics men's high jump?"
answer = answer_query_with_context(query, df, document_embeddings)

print(f"\nQ: {query}\nA: {answer}")

이렇게 보내겠습니다.

저는 ada-001 모델로 해서 이런 답을 얻었습니다.

 

davinci-003 모델을 사용한 cookbook에서는 이런 답을 얻었답니다.

 

둘 다 정답을 맞추었는데 역시 davinci-003 이 더 자세하게 답 해 주네요.

ada-001은 그냥 우승자만 알려주고 끝인데 davinci-003은 그 기록까지 알려 줍니다.

 

예제에서는 그 다음에도 7가지의 질문을 더 예로 들었습니다.

저는 일일이 이 질문을 하드 코딩으로 소스코드를 수정한 다음에 다시 파이썬 파일을 실행하지 않고 사용자가 계속 질문을 입력할 수 있도록 소스 코드를 약간 고쳤습니다.

 

while True:
    query = input('Enter your question here: ')
    answer = answer_query_with_context(query, df, document_embeddings)
    print(f"\nQ: {query}\nA: {answer}")

 

이렇게 해서 얻은 다음 질문과 답변은 이렇습니다. (ada-001 모델)

davinci-003 모델을 사용한 cookbook 의 대답은 이렇습니다.

 

뭐 답은 거의 같습니다.

 

두번째 대답은 조금 이상하네요.

 

역시 davinci-003 모델의 답변이 더 정확합니다. 

ada-001 모델은 1위 국가는 맞추었는데 메달 숫자가 11,417개라는 오답을 주었습니다.

 

다음 질문들도 보냈고 응답을 받았습니다.

대답은 대부분 오답이었습니다.

어쨌든 이 예제에서 배우고자 했던 소스를 제공하고 거기에 대해 embedding 을 이용해 유사값을 구한 다음 Completion을 통해 질문에 대한 답을 구하는 과정을 완료 했습니다.

 

참고로 제가 ada-001을 통해 받은 답변은 이렇습니다.

 

 

cookbook이 davinci-003을 통해 얻은 답은 이렇습니다.

 

 

길고 복잡한 내용이었는데 한번 쭉 분석해 보니 대충 이해가 가네요.

완전히 소화 하려면 좀 더 봐야 할것 같지만...

실전에서 몇번 써먹으면 확실하게 알 텐데.... 이걸 실전에서 써먹을 날이 있을 지....

 

참고로 제가 작성한 전체 소스코드는 아래와 같습니다.

 

import numpy as np
import openai
import pandas as pd
import pickle
import tiktoken
from pprint import pprint

# COMPLETIONS_MODEL = "text-davinci-003"
COMPLETIONS_MODEL = "text-ada-001"
EMBEDDING_MODEL = "text-embedding-ada-002"

def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

# We have hosted the processed dataset, so you can download it directly without having to recreate it.
# This dataset has already been split into sections, one row for each section of the Wikipedia page.


df = pd.read_csv('https://cdn.openai.com/API/examples/data/olympics_sections_text.csv')
df = df.set_index(["title", "heading"])
#print(f"{len(df)} rows in the data.")
#result = df.sample(5)
#pprint(result)


def get_embedding(text: str, model: str=EMBEDDING_MODEL) -> list[float]:
    result = openai.Embedding.create(
      model=model,
      input=text
    )
    return result["data"][0]["embedding"]

#def compute_doc_embeddings(df: pd.DataFrame) -> dict[tuple[str, str], list[float]]:
    """
    Create an embedding for each row in the dataframe using the OpenAI Embeddings API.
    Return a dictionary that maps between each embedding vector and the index of the row that it corresponds to.
    """
#    return {
#        idx: get_embedding(r.content) for idx, r in df.iterrows()
#    }
   
def load_embeddings(fname: str) -> dict[tuple[str, str], list[float]]:
    """
    Read the document embeddings and their keys from a CSV.
    fname is the path to a CSV with exactly these named columns: 
        "title", "heading", "0", "1", ... up to the length of the embedding vectors.
    """
    
    df = pd.read_csv(fname, header=0)
    max_dim = max([int(c) for c in df.columns if c != "title" and c != "heading"])
    return {
           (r.title, r.heading): [r[str(i)] for i in range(max_dim + 1)] for _, r in df.iterrows()
    }
    
document_embeddings = load_embeddings("https://cdn.openai.com/API/examples/data/olympics_sections_document_embeddings.csv")
# ===== OR, uncomment the below line to recaculate the embeddings from scratch. ========
# document_embeddings = compute_doc_embeddings(df)

"""
# An example embedding:
example_entry = list(document_embeddings.items())[0]
print(f"{example_entry[0]} : {example_entry[1][:5]}... ({len(example_entry[1])} entries)")
"""
def vector_similarity(x: list[float], y: list[float]) -> float:
    """
    Returns the similarity between two vectors.
    
    Because OpenAI Embeddings are normalized to length 1, the cosine similarity is the same as the dot product.
    """
    return np.dot(np.array(x), np.array(y))

def order_document_sections_by_query_similarity(query: str, contexts: dict[(str, str), np.array]) -> list[(float, (str, str))]:
    """
    Find the query embedding for the supplied query, and compare it against all of the pre-calculated document embeddings
    to find the most relevant sections. 
    
    Return the list of document sections, sorted by relevance in descending order.
    """
    query_embedding = get_embedding(query)
    
    document_similarities = sorted([
        (vector_similarity(query_embedding, doc_embedding), doc_index) for doc_index, doc_embedding in contexts.items()
    ], reverse=True)
    
    return document_similarities
"""    
result = order_document_sections_by_query_similarity("Who won the men's high jump?", document_embeddings)[:5]
result2 =  order_document_sections_by_query_similarity("Who won the women's high jump?", document_embeddings)[:5]
pprint(result)
pprint(result2)
"""
MAX_SECTION_LEN = 500
SEPARATOR = "\n* "
ENCODING = "gpt2"  # encoding for text-davinci-003

encoding = tiktoken.get_encoding(ENCODING)
separator_len = len(encoding.encode(SEPARATOR))

f"Context separator contains {separator_len} tokens"

def construct_prompt(question: str, context_embeddings: dict, df: pd.DataFrame) -> str:
    """
    Fetch relevant 
    """
    most_relevant_document_sections = order_document_sections_by_query_similarity(question, context_embeddings)
    
    chosen_sections = []
    chosen_sections_len = 0
    chosen_sections_indexes = []
     
    for _, section_index in most_relevant_document_sections:
        # Add contexts until we run out of space.        
        document_section = df.loc[section_index]
        
        chosen_sections_len += document_section.tokens + separator_len
        if chosen_sections_len > MAX_SECTION_LEN:
            break
            
        chosen_sections.append(SEPARATOR + document_section.content.replace("\n", " "))
        chosen_sections_indexes.append(str(section_index))
            
    # Useful diagnostic information
#    print(f"Selected {len(chosen_sections)} document sections:")
#    print("\n".join(chosen_sections_indexes))
    
    header = """Answer the question as truthfully as possible using the provided context, and if the answer is not contained within the text below, say "I don't know."\n\nContext:\n"""
    
    return header + "".join(chosen_sections) + "\n\n Q: " + question + "\n A:"
    
prompt = construct_prompt(
    "Who won the 2020 Summer Olympics men's high jump?",
    document_embeddings,
    df
)

#print("===\n", prompt)

COMPLETIONS_API_PARAMS = {
    # We use temperature of 0.0 because it gives the most predictable, factual answer.
    "temperature": 0.0,
    "max_tokens": 300,
    "model": COMPLETIONS_MODEL,
}

def answer_query_with_context(
    query: str,
    df: pd.DataFrame,
    document_embeddings: dict[(str, str), np.array],
    show_prompt: bool = False
) -> str:
    prompt = construct_prompt(
        query,
        document_embeddings,
        df
    )
    
    if show_prompt:
        print(prompt)

    response = openai.Completion.create(
                prompt=prompt,
                **COMPLETIONS_API_PARAMS
            )

    return response["choices"][0]["text"].strip(" \n")
    
"""    
query = "Who won the 2020 Summer Olympics men's high jump?"
answer = answer_query_with_context(query, df, document_embeddings)

print(f"\nQ: {query}\nA: {answer}")
"""
while True:
    query = input('Enter your question here: ')
    answer = answer_query_with_context(query, df, document_embeddings)
    print(f"\nQ: {query}\nA: {answer}")

 

 

 

 

 

반응형


반응형

오늘은 openai cookbook 에 있는 Embeddings 부문의 Text comparison examples 에 있는 Semantic_text_search_using_embeddings.ipynb 예제를 살펴 보겠습니다.

 

우선 이 예제를 살펴 보기 전에 준비해야 할 사항들이 몇가지 있습니다.

 

여기서는 대용량의 데이터 속에서 의미론적으로 유사한 정보를 embeddings를 사용해서 검색하는 방법을 알아 볼 겁니다.

현재 구글 같은 검색엔진에서는 delicious beans 를 입력하면 이 두 단어가 들어가 있거나 어느 한 단어라도 들어가 있는 정보를 검색해서 보여 줍니다.

하지만 embedding을 통해서 검색을 하면 이 delicious beans의 질문의 의미와 유사한 정보를 검색해서 알려 줍니다.

 

예를 들어 I want to play with cat. 이라는 문장이 있습니다. 고객은 고양이와 놀고 싶은 거죠.

그런데 cat 이 없는 경우 이와 대체할 수 있는 다른 서비스를 생각한다고 칩시다.

여러분 한테는 car 와 dog 가 있습니다. 어떤 것을 추천 하겠습니까?

사람이라면 dog를 추천할 겁니다. 고객이 왜 cat와 놀고 싶은지 그 의미를 알기 때문이죠.

그런데 기계적으로 살펴보면 car 가 cat 과 두글자나 같으니까 dog 보다는 더 유사하다고 볼 것입니다.

단어의 유사성을 근거로 추천한 car 와 문의의 의미를 분석한 후 추천한 dog 중 고객은 후자에 더 만족할 확률이 높습니다.

 

이런 일을 하는 것이 embeddings를 이용한 의미론적 문자 검색 (Semantic text search using embeddings) 입니다.

 

이 예제에서는 검색을 하기 위한 data pool 이 요구 됩니다. 그 데이터세트는 2012년 10월까지 아마존 고객들이 음식관련 리뷰를 남긴 568,454개의 food review 데이터를 사용할 겁니다. 

이 데이터는 csv 형식으로 돼 있는데 아래 링크에서 다운 받아서 여러분의 python 파일이 있는 폴더에 data 라는 sub-folder를 생성한 후 거기에 복사해 넣으세요.

 

https://github.com/openai/openai-cookbook/blob/main/examples/data/fine_food_reviews_with_embeddings_1k.csv

 

GitHub - openai/openai-cookbook: Examples and guides for using the OpenAI API

Examples and guides for using the OpenAI API. Contribute to openai/openai-cookbook development by creating an account on GitHub.

github.com

이 csv 파일을 열어보면 아래와 같이 생겼습니다.

 

 

각 리뷰에 대한 productId, UserId, Score, Summary, Text, combined, n_tokens 그리고 embedding 컬럼들이 있습니다.

 

이제 코드를 작성해 보죠.

 

숫자로 된 embedding을 다루어야 하니까 파이썬에서 수학적인 기능을 제공하는 numpy 모듈과 대용량 데이터를 사용해야 하니까 pandas 모듈이 필요할 겁니다.

 

우선 필요한 모듈들을 import 하고 항상 사용하는 openai 에 api_key 인증 받는 부분을 아래와 같이 작성합니다.

그리고 다운 받았던 csv 파일을 pandas 모듈의 read_csv() 메소드를 사용해서 읽어 옵니다.

 

위의 코드는 다음과 같이 각 줄마다 설명됩니다.

  1. import openai: openai 모듈을 가져옵니다. 이 모듈을 사용하여 OpenAI API와 상호 작용할 수 있습니다.
  2. from pprint import pprint: pprint 모듈에서 pprint 함수를 가져옵니다. pprint 함수는 보기 좋게 출력하는 데 사용됩니다.
  3. import numpy as np: numpy 모듈을 가져옵니다. 이 모듈은 수치 계산과 배열 연산에 사용되는 다양한 기능을 제공합니다.
  4. import pandas as pd: pandas 모듈을 가져옵니다. 이 모듈은 데이터 분석과 조작에 사용되는 다양한 기능을 제공합니다.
  5. def open_file(filepath):: open_file이라는 함수를 정의합니다. 이 함수는 파일을 열어서 내용을 읽은 후 문자열로 반환합니다.
  6. with open(filepath, 'r', encoding='utf-8') as infile:: 파일을 열고 infile 변수에 할당합니다. 'utf-8'로 인코딩된 텍스트 파일을 엽니다.
  7. return infile.read(): 파일 내용을 읽어서 반환합니다.
  8. openai.api_key = open_file('openaiapikey.txt'): openaiapikey.txt 파일을 열어서 내용을 읽은 후, OpenAI API 키로 설정합니다.
  9. datafile_path = "data/fine_food_reviews_with_embeddings_1k.csv": 데이터 파일의 경로를 지정합니다. "data" 폴더 안에 있는 fine_food_reviews_with_embeddings_1k.csv 파일입니다.
  10. df = pd.read_csv(datafile_path): pandas의 read_csv 함수를 사용하여 CSV 파일을 읽어서 DataFrame으로 저장합니다.
  11. df["embedding"] = df.embedding.apply(eval).apply(np.array): DataFrame의 "embedding" 열에 대해 eval 함수를 적용하여 문자열을 Python 객체로 변환한 후, np.array 함수를 적용하여 배열로 변환합니다. 이렇게 변환된 임베딩 값들은 "embedding" 열에 저장됩니다.

 

여기에서 pandas 모듈을 사용하기 위해 기본적인 것들을 알면 좋을 것 같습니다.

구글링해서 찾아보세요.

저는 아래 글을 봤는데 정리가 잘 돼 있는 것 같더라구요.

https://dandyrilla.github.io/2017-08-12/pandas-10min/

 

판다스(pandas) 기본 사용법 익히기

데이터 분석을 위한 파이썬 라이브러리인 판다스(pandas) 의 기본 사용법을 소개해 놓은 ‘10 Minutes to pandas’ 를 번역해 놓은 글입니다. pandas 의 기본 사용법을 익히시려는 분들에게 실습을 천천히

dandyrilla.github.io

 

짧은 지식을 근거로 16번째 줄을 해석해 보면...

 

df["embedding"] = df.embedding.apply(eval).apply(np.array)

 

csv라는 파일 (df) 에서 embedding 컬럼에 df.embedding.apply(eval).apply(np.array) 를 해 준다는 의미 입니다.

apply(eval).apply(np.array)는 openai Guide의 Embeddings Use cases 페이지에서 그 사용법을 다루었습니다.

 

 

저장된 파일에서 데이터를 load 하는 방법이라고 나와 있습니다.

일단 이렇게 사용하는 것이라고 알아 두어야 겠습니다.

 

그 다음에는 openai.embeddings_utils라는 모듈에서 get_embedding 과 cosine_similarity를 import 합니다.

 

from openai.embeddings_utils import get_embedding, cosine_similarity

 

여기서는 python의 from ... import ... 구문을 이해 하셔야 합니다.

import 모듈 하면 모듈 전체를 가져 오는 것이고 from 모듈 import 이름 하면 그 모듈 내에서 필요한 메소드만 가져오는 방법입니다.

이렇게 하면 이 메소드를 사용할 때 모듈.메소드 이런 형식이 아니라 그냥 메소드 이름만 사용하면 됩니다.

 

>>> import tkinter

>>> tkinter.widget = tkinter.Label(None, text='I love Python!')

>>> tkinter.widget.pack()

 

이렇게 사용 해야 하는 것을...

 

>>> from tkinter import *

>>> widget = Label(None, text='I love Python!')

>>> widget.pack()

 

이렇게 사용하도록 하는 겁니다.

 

그러면 openai.embeddings_utils라는 모듈의 get_embedding과 cosine_similarity 함수들을 보는 방법을 알려 드리겠습니다.

 

https://github.com/openai/openai-python

 

GitHub - openai/openai-python: The OpenAI Python library provides convenient access to the OpenAI API from applications written

The OpenAI Python library provides convenient access to the OpenAI API from applications written in the Python language. - GitHub - openai/openai-python: The OpenAI Python library provides convenie...

github.com

위 페이지에 가시면 openai 의 파이썬 관련 모듈들을 보실 수 있습니다.

 

openai 폴더로 들어가서 embeddings_utils.py 를 클릭합니다.

그러면 이 모듈을 보실 수 있습니다.

 

여기에서 get_embedding 함수를 보겠습니다.

 

이 함수는 openai api 의 JSON 형식의 response  중에서 embedding 부분만 return 하도록 만든 함수 입니다.

이전 글 에서는 이 부분을 그냥 ["data"][0]["embedding"] 를 사용해서 코딩을 해서 사용했습니다.

 

그 다음은 cosine_similarity 를 보겠습니다.

 

 

간단한 함수 입니다. 다 파라미터를 input 값으로 받아서 np.dot과 np.linalg.norm 을 사용해서 계산 한 후 return 합니다.

linalg.norm()은 벡터의 크기라고 합니다.

자세한 건 모르겠고 이 cosine_similarity 함수는 두 벡터값을 입력 받아서 두 값의 유사성을 계산해서 return 하는 일을 합니다.

 

지금까지 진행한 소스코드 입니다.

 

import openai
from pprint import pprint
import numpy as np
import pandas as pd


def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

datafile_path = "data/fine_food_reviews_with_embeddings_1k.csv"

df = pd.read_csv(datafile_path)
df["embedding"] = df.embedding.apply(eval).apply(np.array)

from openai.embeddings_utils import get_embedding, cosine_similarity

 

일단 여기까지 하고 실행 해 보세요.

저 같은 경우는 embeddings_utils 모듈에서 import 하는 모듈들이 없다면서 에러를 많이 발생 시키더라구요.

참고로 embeddings_utils 모듈에서 import 하는 다른 모듈들은 아래와 같습니다.

 

 

에러는 이런 식으로 납니다.

해결은 그냥 pip install 모듈이름 -> 이렇게 해 주면 됩니다.

 

저는 이 이외에도 plotly, scipy, sklearn 등의 모듈이 없다고 해서 새로 install 했습니다.

 

sklearn 같은 경우에는 더 이상 사용하지 않는 모듈이라고 나왔습니다.

 

그래서 위에서 시키는 데로 pip install scikit-learn 을 해서 해결했습니다.

 

 

여기까지 필요한 모듈을 import 하고 open ai에 api_key를 보내서 인증 받고 csv 파일을 읽는 작업까지 했습니다.

 

이제 일을 처리하기 위한 사전 준비는 다 됐습니다.

 

이제 할 일은 입력값을 받아서 openai 의 embedding 기능을 이용해서 벡터 값을 받아 오는 겁니다.

 

    product_embedding = get_embedding(
        입력값,
        engine="text-embedding-ada-002"
    )

 

여기서 원래는 openai.Embedding.create() API 를 사용해서 벡터값을 받아와서 ['data'][0]['embedding'] 를 사용해서 JSON 형식의 response에서 embedding 값만 받아 왔었습니다.

 

그런데 이 소스코드에서는 openai.embeddings_utils 라는 모듈의 get_embedding() 이라는 함수를 써서 곧바로 embedding 값만 받아 옵니다.

 

그러면 원래 csv에 있던 embedding 값과 입력값에 대한 embedding 값을 모두 가지게 되었습니다.

 

다음 할 일은 cosine_similarity() 를 통해서 유사값만 계산해 주면 됩니다.

 

그러면 그곳에서 가장 유사성에 높은 것만 뽑으면 유용한 정보를 얻을 수 있는 것입니다.

 

이것을 구현한 함수는 아래와 같습니다.

 

위의 코드는 다음과 같이 각 줄마다 설명됩니다.

  1. from openai.embeddings_utils import get_embedding, cosine_similarity: openai.embeddings_utils 모듈에서 get_embedding, cosine_similarity 함수를 가져옵니다. 이 함수들은 임베딩을 가져오고 코사인 유사도를 계산하는 데 사용됩니다.
  2. def search_reviews(df, product_description, n=3, pprint=True):: search_reviews라는 함수를 정의합니다. 이 함수는 DataFrame과 제품 설명을 받아와서 해당 제품과 유사한 리뷰를 검색합니다. 기본적으로 상위 3개의 유사한 리뷰를 반환하며, pprint 매개변수가 True로 설정되면 결과를 보기 좋게 출력합니다.
  3. product_embedding = get_embedding(product_description, engine="text-embedding-ada-002"): get_embedding 함수를 사용하여 제품 설명에 대한 임베딩을 가져옵니다. "text-embedding-ada-002" 엔진을 사용하여 임베딩을 생성합니다.
  4. df["similarity"] = df.embedding.apply(lambda x: cosine_similarity(x, product_embedding)): DataFrame의 "embedding" 열에 대해 cosine_similarity 함수를 적용하여 제품 임베딩과의 유사도를 계산한 후, "similarity" 열에 저장합니다.
  5. results = (df.sort_values("similarity", ascending=False).head(n).combined.str.replace("Title: ", "").str.replace("; Content:", ": ")): DataFrame을 "similarity" 열을 기준으로 내림차순 정렬하고 상위 n개의 결과를 선택합니다. 이후 "combined" 열에서 "Title: "과 "; Content:"를 제거한 후, "results" 변수에 저장합니다.
  6. if pprint: for r in results: print(r[:200]); print(): pprint 매개변수가 True로 설정되어 있으면 결과를 보기 좋게 출력합니다. 각 결과의 첫 200자를 출력하고 빈 줄을 출력합니다.
  7. return results: 결과를 반환합니다.

 

search_reviews() 라는 함수가 있고 입력 파라미터는 4개 입니다.

변수 df 는 csv 정보이고 product_description 은 입력값입니다. n=3와 pprint=True는 부가적인 파라미터 입니다.

22~25번째 줄 까지가 입력값을 openai api를 통해서 openai 로 보내고 거기에 대한 벡터값을 return 받아서 product_embedding이라는 변수에 저장하는 겁니다.

 

26번째 줄에는 cosine_similarity()를 통해서 이 입력값에 대한 임베딩 값과 csv 파일에 있는 review의 임베딩 값의 유사성을 구하는 겁니다.

 

lamda x를 사용해서 csv의 각각의 리뷰마다 이 유사값을 계산하는 겁니다.

df['similarity"]는 similarity라는 컬럼을 새로 만들고 이 컬럼에 계산된 유사값을 넣는다는 의미 입니다.

 

그 다음 28번째 줄에서 33번째 줄까지 보겠습니다.

result라는 변수에 어떤 값들을 넣는데요. 

df.sort_values() 에서 처음에 하는 것이 similarity 컬럼을 sorting 하는 겁니다. 이렇게 하면 내림차순으로 정리가 되서 가장 유사성이 높은 것이 맨 위로 가게 되죠.

그 다음 head(n) 함수는 pandas의 dataframe에 있는 함수로 상위 n개의 값만 가진다는 의미 입니다. n 은 이 함수의 입력 파라미터로서 디폴트 값은 3 이고 다른 값이 입력되면 그 값을 따릅니다.

그 다음은 combined 라는 컬럼의 Title: 을 "" 로 바꾸고 ; Content: 를 :로 바꾼다는 의미입니다.

        .combined.str.replace("Title: ", "")
        .str.replace("; Content:", ": ")

    if pprint:
        for r in results:
            print(r[:200])
            print()

 

이 부분은 pprint 일 경우 첫번째 200 캐릭터만 출력하라는 겁니다.

 

그리고 38번째 줄에서는 두 입력 파라미터의 유사값을 계산해서 가공한 값인 results를 return 합니다.

 

이제 이 함수를 사용하기만 하면 됩니다.

 

results = search_reviews(df, "delicious beans", n=3)

print('delicious beans\n')

pprint(results)

 

위의 코드는 다음과 같이 각 줄마다 설명됩니다.

  1. results = search_reviews(df, "delicious beans", n=3): search_reviews 함수를 사용하여 DataFrame df에서 "delicious beans"와 유사한 리뷰를 상위 3개 검색한 결과를 results 변수에 저장합니다.
  2. print('delicious beans\n'): "delicious beans"를 출력합니다. \n은 줄바꿈을 의미합니다.
  3. pprint(results): pprint 함수를 사용하여 results를 출력합니다. pprint 함수는 결과를 보기 좋게 출력하는 역할을 합니다.

 

저는 이 Sementic_text_search_using_embeddings.ipynb 에서 제시한 모든 입력값들에 Korean food 와 Kimchi 를 추가해서 출력해 보았습니다.

 

results = search_reviews(df, "delicious beans", n=3)
print('delicious beans\n')
pprint(results)

results = search_reviews(df, "whole wheat pasta", n=3)
print('whole wheat pasta\n')
pprint(results)

results = search_reviews(df, "bad delivery", n=1)
print('bad delivery\n')
pprint(results)

results = search_reviews(df, "spoilt", n=1)
print('spoilt\n')
pprint(results)

results = search_reviews(df, "pet food", n=2)
print('pet food\n')
pprint(results)

results = search_reviews(df, "Korean food", n=2)
print('Korean food\n')
pprint(results)

results = search_reviews(df, "Kimchi", n=2)
print('Kimchi\n')
pprint(results)

 

결과 값은 아래와 같이 나왔습니다.

 

Kimchi와 관련해서는 아래처럼 나오네요.

Kimchi에 대한 직접적인 언급한 부분이 한군데도 없나 봅니다.

이런 경우 이전 검색 방법으로 하면 Kimchi 라는 단어가 없으니까 아무것도 출력이 안될겁니다.

하지만 이 임베딩의 경우에는 단어나 문구, 문장들의 의미를 부여한 벡터 값을 가지고 검색하는 겁니다.

 

그래서 위 두가지 답이 나왔습니다.

 

첫번째 답은 한국의 맵고 유명한 누들이라고 하는 걸로 봐서 신라면에 대한 평가인 것 같습니다.

이게 왜 유사할까요?

 

Kimchi에 대한 임베딩 값에는 Korea 와 유사성이 가까운 값도 있고, 김치는 맵다는 값도 있나 봅니다.

그러니까 매운 한국 음식이라는 표현이 있는 이 제품 평가가 그나마 가장 가까운 거라고 ChatGPT는 평가 했습니다.

두번째는 매운 음식이라는 표현이 있네요.. 그래서 Kimchi와 유사성이 있다고 한 것 같습니다.

 

이렇게 기존의 검색 방법과 임베딩의 검색 방법의 차이는 이 Open AI 의 임베딩은 1536개의 부동 소수점 숫자들을 가지고 있는데 그 숫자들은 어떤 의미를 부여하고 있다는 겁니다.

 

그래서 딱히 찾고자 하는 단어가 다른 문장에 없어도 의미가 가장 가까운 문장을 찾을 수 있다는 겁니다.

 

이렇게 해서 대용량 데이터세트 내에서 openai의 임베딩 기능을 통한 Semantic text search 에 대한 예제를 살펴 봤습니다.

 

전체 소스코드는 여기에 있습니다.

 

import openai
from pprint import pprint
import numpy as np
import pandas as pd


def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

datafile_path = "data/fine_food_reviews_with_embeddings_1k.csv"

df = pd.read_csv(datafile_path)
df["embedding"] = df.embedding.apply(eval).apply(np.array)

from openai.embeddings_utils import get_embedding, cosine_similarity

# search through the reviews for a specific product
def search_reviews(df, product_description, n=3, pprint=True):
    product_embedding = get_embedding(
        product_description,
        engine="text-embedding-ada-002"
    )
    df["similarity"] = df.embedding.apply(lambda x: cosine_similarity(x, product_embedding))

    results = (
        df.sort_values("similarity", ascending=False)
        .head(n)
        .combined.str.replace("Title: ", "")
        .str.replace("; Content:", ": ")
    )
    if pprint:
        for r in results:
            print(r[:200])
            print()
    return results


results = search_reviews(df, "delicious beans", n=3)
print('delicious beans\n')
pprint(results)

results = search_reviews(df, "whole wheat pasta", n=3)
print('whole wheat pasta\n')
pprint(results)

results = search_reviews(df, "bad delivery", n=1)
print('bad delivery\n')
pprint(results)

results = search_reviews(df, "spoilt", n=1)
print('spoilt\n')
pprint(results)

results = search_reviews(df, "pet food", n=2)
print('pet food\n')
pprint(results)

results = search_reviews(df, "Korean food", n=2)
print('Korean food\n')
pprint(results)

results = search_reviews(df, "Kimchi", n=2)
print('Kimchi\n')
pprint(results)

 

반응형


반응형

이번에는 Openai cookbook 을 통해 얻은 정보를 가지고 embedding에 대한 예제 소스를 만들어 보도록 하겠습니다.

openai cookbook은 openai에서 제공하는 예제들이 있는 페이지 입니다.

 

https://github.com/openai/openai-cookbook

 

GitHub - openai/openai-cookbook: Examples and guides for using the OpenAI API

Examples and guides for using the OpenAI API. Contribute to openai/openai-cookbook development by creating an account on GitHub.

github.com

 

이 중에 Embeddings 부분으로 가게 되면 총 8개의 링크가 있습니다.

 

 

이 중 가장 처음에 있는 text comparison examples 로 갑니다.

 

그러면 Text comparison examples 가 나오게 되는데 오늘 다룰 부분은 예제로 곧바로 가지 않고 이 페이지에서 소개 된 블로그 중 한 곳의 내용을 가지고 소스 코드를 만들 계획입니다.

 

 

2022년 1월 블로그로 갑니다.

 

이곳에는 openai에서 embedding 기능을 선보이면서 설명한 내용이 있습니다.

 

이 내용도 읽어 보시면 Embedding을 이해하는데 아주 도움이 많이 됩니다.

 

이 페이지에서 핵심 내용은 아래 그림입니다.

 

이것은 각 아이템별로 embedding 값을 가지고 그 위치를 좌표에 표시한 것입니다.

 

feline (고양이류) friends say 에서 가장 가까운 것은 meow (야옹) 입니다.

canine (개과) companions say 에 가장 가까운 것은 woof (멍멍) 이구요.

bovine (소과) buddies say 에 가장 가까운 것은 moo (음메) 입니다.

 

미식축구의 쿼터백은 이들과는 아주 멀리 떨어져 있죠.

 

이렇게 Embedding 값은 여러 아이템들의 유사성을 비교할 수 있도록 해 줍니다.

 

이 표 아래에 보시면 아래 소스 코드가 나옵니다.

 

여기서 PRINT RESPONSE를 클릭하면 아래 처럼 보입니다.

 

아주 간단한 request와 response 인데요.

이 간단한 요청과 응답에 대해서는 바로 전 글에서 자세히 설명 했습니다.

 

https://coronasdk.tistory.com/1260

 

Open AI API - GPT 3 - Embedding API 예제 살펴 보기

오늘은 Open AI API 에서 제공하는 기본 Embedding 예제 소스 코드를 살펴 보겠습니다. 아래 글을 보시면 해당 API 페이지의 내용을 보실 수 있습니다. https://coronasdk.tistory.com/1237 Embeddings - openai.Embedding.c

coronasdk.tistory.com

 

오늘은 이것을 어떻게 의미있게 사용할 지에 대해 알아보겠습니다.

 

저는 위에 제시된 3개의 동물과 3개의 울음소리가 서로 얼마나 유사성이 있는지 알고 싶었습니다.

 

그래서 첫번째로 한 일은 request에 위의 6가지 (각 동물들과 그 울음소리) 정보를 모두 보내는 일을 했습니다.

우선 각 요소별 embedding 값을 받아야지 서로 비교할 수 있으니까요.

 

9번째 줄 까지는 계속 사용했던 부분이니까 설명은 생략하겠습니다.

 

우선 inputVal 이라는 변수에 이 6개의 아이템을 배열로 담았습니다.

 

그리고 openai.Embedding.create() 에 이를 input 값으로 보냈습니다.

여기서의 목적은 유사성을 알아보는 것이라서 모델은 text-similarity-ada-001을 사용했습니다. 

davinci 보다 성능은 떨어지지만 저렴한 ada 모델을 썼습니다.

 

일단 이 값을 pprint를 이용해서 출력 해 봤습니다.

 

어마어마하게 긴 response를 받았습니다.

 

 

입력값이 6개 이므로 index는 0~5로 총 6개의 embedding 값을 받았습니다.

토큰은 총 19개 이군요.

 

일단 입력한 6개 아이템들에 대한 임베딩값을 받았으니 그 값들을 비교해서 유사성을 추출해 내면 됩니다.

이전 글에서 살펴 봤듯이 하나의 입력 아이템 당 1536개의 부동 소숫점 배열을 응답으로 받았습니다.

이것을 그냥 비교하기는 불가능 합니다.

 

여기에서 파이썬에 있는 numpy 모듈의 dot() 메소드를 사용해야 합니다.

 

우선 그 방법을 알아보기 위해서 두개의 입력값에 대한 임베딩 값을 비교해 보겠습니다.

 

방법은 간단합니다. 

우선 첫번째 입력값과 두번째 입력값을 각각 변수에 담습니다.

첫번째 입력값은 feline friends go 이고 두번째 입력값은 meow 이었죠.

 

이 두 임베딩 값을 dot() 메소드에 파라미터 값으로 전달합니다.

여기서 return 받은 값은 similarity_score1 에 저장이 됩니다.

 

그리고 이 값을 출력해 보겠습니다.

 

 

위와 같은 값이 나왔습니다.

이제 사람이 좀 알아 볼 수 있는 값이 나왔네요.

두 아이템간의 유사성은 0.8432 ...... 입니다.

 

이제 방법을 찾았으니 모든 6개 입력값들을 짝을 지어서 유사성을 알아 보겠습니다.

동물과 울음소리가 각각 3개씩 이니까 총 9본을 비교해 보면 되겠네요.

 

일단 무식하지만 이 작업을 하나 하나씩 아래와 같이 스크립팅을 해 봤습니다.

 

이 코드의 결과는 아래와 같습니다.

 

이제 거의 다 왔습니다.

 

이제 두가지 작업만 더 하죠.

 

우선  고객이 알기 쉽도록 표시해 줍시다.

 

이런식으로 두개의 입력값에 대한 유사성은 이렇다라고 문장으로 보여 주려고 하는데요.

이렇게 하면 아래 에러가 뜹니다.

 

입력값은 string 이고 유사성에 대한 값은 numpy.float64 이기 때문입니다. 

다 string 타입으로 통일 시켜야 겠습니다.

방법은 간단합니다. numpy.float64 를 srt() 로 감싸 주면 됩니다.

 

출력값은 아래와 같습니다.

 

 

이제 고객의 요구조건에 딱 맞는 결과물을 제공할 수 있게 되었습니다.

 

고양이과의 울음소리는 meow 가 가장 유사성이 있네요.

개과도 meow로 나오는데 아마 ada 모델이 성능이 조금 떨어져서 그럴 겁니다.

ada 모델은 woof 보다 meow 가 개 울음소리에 더 가깝다고 보는 것 같습니다.

davinci 모델을 사용하면 답변이 제대로 나올 겁니다.

소과의 울음소리는 moo 가 가장 유사성이 있네요.

 

결과물의 퀄리티는 좀 떨어집니다. 이럴 경우 돈을 더 써서 davinci 모델을 사용해서 고객에게 전달을 해야지 제대로 판매를 할 수 있겠네요.

 

지금은 API 기능을 배우는 중이라 결과물에 대한 퀄리티는 크게 신경 쓰지 않겠습니다.

 

근데 결과값은 제대로 나왔지만 프로그래머로서 코드가 마음에 안 듭니다.

 

이 긴 부분은 패턴이 있기 때문에 충분히 refactoring을 할 수 있습니다.

 

for 루프를 이용해서 아래와 같이 만들어 보았습니다.

 

inputVal의 아이템 숫자 (6) 만큼 루프를 돌리면서 일을 할 겁니다.

2로 나누어 지면 즉 0,2,4번째 아이템일 경우 그 다음 일을 합니다.

즉 고양이, 개, 소 인 부분에서 3가지 울음 소리와 비교하려고 이렇게 한 겁니다.

 

두번째 for 루프는 울음 소리를 x에 담기 위해서 만들었습니다.

range(1,6,2) 의 의미는 1서부터 시작해서 6까지 2단계씩 뛰면서 x에 그 값을 담아라 입니다.

즉 첫번째 루프에서는 1이 그 다음은 2단계 뛴 3이 그리고 마지막엔 5가 담깁니다.

 

여기서 index 값에는 동물들이 있는 위치값이 그리고 x에는 울음소리가 있는 위치값이 담깁니다.

 

이걸 가지고 그 밑에 줄에서 이용을 합니다.

 

resultVal = inputVal[index] + ' and ' + inputVal[x] + ' is ' + str(np.dot(resp['data'][index]['embedding'], resp['data'][x]['embedding']))

그리고 이 값을 resultList 에 append 합니다.

 

이렇게 하면 위에 26줄에 걸쳐서 했던 일을 10줄도 안되게 줄일 수 있습니다.

 

 

답변도 위에서와 같이 나옵니다.

 

아마 22번째 줄을 좀 더 가시성이 있도록 바꾸고 싶으신 분들도 많을 겁니다.

저도 좀 그런데... 오늘은 그냥 여기까지 하겠습니다.

 

이렇게 해서 Openai API 중에서 Embedding 기능을 유의미하게 사용하는 방법을 살펴 봤습니다.

 

완성된 소스 코드는 아래와 같습니다.

 

import openai
from pprint import pprint
import numpy as np

def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

inputVal = ["feline friends go", "meow","canine companions say" , "woof", "bovine buddies say", "moo"]

resp = openai.Embedding.create(
    input=inputVal,
    model="text-similarity-ada-001")
    #model="text-similarity-davinci-001")
    
resultList = list()
for index, val in enumerate(inputVal):
    if index%2 == 0:
            for x in range(1,6,2):
                resultVal = inputVal[index] + ' and ' + inputVal[x] + ' is ' + str(np.dot(resp['data'][index]['embedding'], resp['data'][x]['embedding']))
                resultList.append(resultVal)
    
pprint(resultList)

 

P.S. 

 

2022년 12월에 발표된 새 모델인 text-embedding-ada-002 를 사용하면 더 빠르고 정확하면서 비용이 더 저렴한 것 같습니다.

 

import openai
from pprint import pprint
import numpy as np

def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

inputVal = ["feline friends go", "meow","canine companions say" , "woof", "bovine buddies say", "moo"]

resp = openai.Embedding.create(
    input=inputVal,
    model="text-embedding-ada-002")
    #model="text-similarity-ada-001")
    #model="text-similarity-davinci-001")
    
resultList = list()
for index, val in enumerate(inputVal):
    if index%2 == 0:
            for x in range(1,6,2):
                resultVal = inputVal[index] + ' and ' + inputVal[x] + ' is ' + str(np.dot(resp['data'][index]['embedding'], resp['data'][x]['embedding']))
                resultList.append(resultVal)
    
pprint(resultList)

 

위 코드를 사용하면 아래와 같은 결과를 얻게 됩니다.

 

고양이는 meow 이고 개는 woof 그리고 소는 moo 가 가장 유사성이 높다고 나오네요.

 

제대로 원하는 답을 얻은 것 같습니다. 저렴한 모델을 사용하면서요.

 

이 새 모델에 대한 설명은 2022년 12월에 올라온 openai 블로그 글에 자세히 나와 있습니다.

https://openai.com/blog/new-and-improved-embedding-model/

 

New and Improved Embedding Model

We are excited to announce a new embedding model which is significantly more capable, cost effective, and simpler to use. The new model, text-embedding-ada-002, replaces five separate models for text search, text similarity, and code search, and outperform

openai.com

 

반응형


반응형

오늘은 Open AI API 에서 제공하는 기본 Embedding 예제 소스 코드를 살펴 보겠습니다.

 

 

아래 글을 보시면 해당 API 페이지의 내용을 보실 수 있습니다.

 

https://coronasdk.tistory.com/1237

 

Embeddings - openai.Embedding.create()

https://beta.openai.com/docs/api-reference/embeddings Embeddings Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. 기계 학습 모델 및 알고리즘에서 쉽게 사용할 수 있는 주

coronasdk.tistory.com

이 예제 코드를 실행하기 위해서 약간 고친 저의 코드는 아래와 같습니다.

 

 

print를 좀 이쁘게 하기 위해서 pprint를 import 했고 api key를 전달하는 부분을 파일에서 읽어서 전달하도록 바꾸었습니다.

openai.Embedding.create() 부분은 예제와 똑 같습니다

 

이것을 실행하면 이렇게 나옵니다.

 

 

시작하는 부분은 이렇습니다.

입력 값은 "The food was delicious and the waiter..." 입니다. 

이에 대한 응답은 JSON 형식으로 response를 받게 되고 data 밑에 embedding 이라는 항목이 있고 그 안데 배열 형식으로 수 많은 숫자들이 있습니다. 모든 숫자는 0 이하의 부동 소숫점이고 양수와 음수들이 번갈아 가며 있습니다.

소숫점 아래 숫자들은 18자리 입니다.

이 부동 소숫점 들이 몇개나 있는지 알아봤습니다.

총 1536개가 있었습니다.

 

이것만 가지고는 무슨 의미인지 전혀 알 수 없습니다.

 

이 embeddings 기능을 사용하려면 이 response를 조금 가공 해야 합니다.

의미를 알 수 있도록 가공하는 방법에 대해서는 다음 글에서 자세하게 다룰 예정입니다.

 

오늘은 간단한 방법만 알아 보겠습니다.

 

일단 response 부분을 더 보겠습니다.

 

response의 맨 아랫부분을 보면 이렇습니다.

 

 

부동 소숫점 배열이 있는 embedding 부분이 끝나면 index 가 있습니다.

현재 값은 0 입니다.

이것은 우리가 request를 할 때 한가지만 전달해서 그렇습니다.

 

request의 input 부분에 여러개를 전달하면 각각에 대한 embedding 값을 반환하고 index는 0,1,2,3 이렇게 증가하면서 몇번째 request에 대한 임베딩 값인지 알 수 있게 해 줍니다.

 

그 다음 object는 embedding 이라는 것을 말해 줍니다.

 

그 다음은 모델 정보가 나옵니다. 우리는 request에서 text-embedding-ada-002를 사용했습니다.

응답에 나온 것을 보니까 정확하게는 text-embedding-ada-002-v2 모델을 사용했네요.

 

밑줄엔 object 아이템이 있고 이것은 list 를 사용했다고 알려 주는 것 같습니다.

 

그 밑에 usage 부분에는 사용된 토큰 값이 표시 됩니다.

이 토큰의 양에 따라서 사용료가 부과가 되니까 이것도 중요한 정보 입니다.

 

우리가 사용한 토큰은 총 8개 입니다.

 

토큰에 대해 알고 싶으시면 아래 글에 간단히 설명 돼 있습니다.

 

https://coronasdk.tistory.com/1257

 

GPT-3 API로 초간단 Chatbot 만들기

오늘은 Python 과 ChatGPT API로 간단한 챗봇을 만들어 보겠습니다. import os import openai def open_file(filepath): with open(filepath, 'r', encoding='utf-8') as infile: return infile.read() openai.api_key = open_file('openaiapikey.txt') wh

coronasdk.tistory.com

 

일단 여기서 핵심 정보는 embedding 부분입니다.

 

이것을 다루려면 이 부분만 따로 추려서 다루어야 합니다.

 

response 중에 embedding 만을 따로 추리는 방법은 아래와 같습니다.

 

embeddings = response['data'][0]['embedding']

 

당연히 이 부분을 출력하면 embedding 값들만 나오겠죠.

 

 

이 embedding은 대개 유사성을 측정하는데 사용합니다.

그렇기 때문에 두개 이상의 대상에 대해 유사성을 측정하게 되는데요.

 

이 경우 이 값들을 유의미하게 사용하기 위해서 numpy의 dot() 이라는 메소드를 사용합니다.

이 dot() 메소드는 두개의 input array가 1차원인 벡터들인 경우 사용합니다.

 

다음 글에서는 이 embedding 을 가지고 유의미하게 사용하는 예제를 만들고 분석해 보겠습니다.

반응형

OpenAI API : GPT-3 : Embeddings Sample Code

2023. 2. 9. 00:36 | Posted by 솔웅


반응형

OpenAI 에서 제공하는 서비스에는 아래처럼 5가지로 분류할 수 있습니다.

Text completion

Code completion

Image generation

Fine-tuning

Embeddings

 

이 중 챗봇은 맨 처음의 Text completion 서비스를 사용했습니다.

오늘은 이 중 Embeddings에 대해 알아 보겠습니다.

좀 어려운 부분입니다.

 

OpenAI의 Embeddings 의 개념에 대해 알아보려면 여기를 참조하세요.

https://coronasdk.tistory.com/1222

 

Guides - Embeddings

https://beta.openai.com/docs/guides/embeddings/what-are-embeddings OpenAI API An API for accessing new AI models developed by OpenAI beta.openai.com Embeddings What are embeddings? OpenAI’s text embeddings measure the relatedness of text strings. Embeddi

coronasdk.tistory.com

Embeddings관련 openai API를 알아보려면 여기를 참조하세요.

https://coronasdk.tistory.com/1237

 

Embeddings - openai.Embedding.create()

https://beta.openai.com/docs/api-reference/embeddings Embeddings Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. 기계 학습 모델 및 알고리즘에서 쉽게 사용할 수 있는 주

coronasdk.tistory.com

그리고 이 Tensorflow.org의 자료도 참고하시면 더 깊게 이해하는데 좋습니다. (한글로도 제공됩니다.)

https://www.tensorflow.org/text/guide/word_embeddings

 

단어 임베딩  |  Text  |  TensorFlow

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 단어 임베딩 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 자습서에는 단어 임베딩

www.tensorflow.org

 

Embeddings는 OpenAI 의 GPT-3 가 사용자로부터 받은 파라미터가 다양한 보기에 얼마나 유사성이 있는지를 반환해 주는 서비스 입니다.

반환값은 여러개의 유사성들이 되죠. 이는 벡터 Vector 입니다.

벡터란 1 dimensional matrix를 말합니다.

embedding = vector with semantic meaning (어떤 의미가 있는 vector)

 

예를 들어 아래와 같은 벡터가 있습니다.

[X, Y]

 

여기에 의미를 부여해 보겠습니다.

 X는 social power 이고 값은 max 1.0 - min -1.0 입니다.

Y는 gender이고 max 1.0 - min -1.0 입니다. 1.0 은 완전 남성이고 -1.0은 완전 여성입니다.

 

그러면 값이 [1.0 , 1.0]  은 황제를 나타낼 수 있겠죠. 사회적 최강자이고 남성성도 만빵인 황제요.

황제가 [1.0 , 0.5] 일 수도 있죠. 사회적 최 강자 이지만 약간 덜 남성적인 성격일 수도 있으니까요.

 

[-1.0, 0]인 값은 사회적 파워가 없는 사람, 자유가 박탈된 사람이 되겠죠. 감옥에 있는 사람이 되겠죠. 그리고 성별은 중성인 무엇인가가 될 것입니다.

이렇듯 제공된 벡터를 가지고 그 사람이 제공된 조건에서 어디쯤에 속할지를 가늠하는 것이 Embedding의 역할 입니다.

 

여기서는 2개의 dimension을 사용했습니다. 참고로 OpenAI의 모델별 output dimensions는 아래와 같습니다. 

 

이제 기본적인 개념 정리는 여기까지 하고 OpenAI API에서 이 Embedding 기능을 사용하는 간단한 파이썬 예제를 보겠습니다.

 

 

우선 2번째 줄의 numpy는 python의 기본 수학 모듈입니다.  배열을 다룰 때 많이 사용됩니다. 

자세한 사항은 아래 웹사이트를 참조하세요.

https://numpy.org/

 

NumPy

Powerful N-dimensional arrays Fast and versatile, the NumPy vectorization, indexing, and broadcasting concepts are the de-facto standards of array computing today. Numerical computing tools NumPy offers comprehensive mathematical functions, random number g

numpy.org

 

https://ko.wikipedia.org/wiki/NumPy

 

NumPy - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. NumPy("넘파이"라 읽는다)는 행렬이나 일반적으로 대규모 다차원 배열을 쉽게 처리할 수 있도록 지원하는 파이썬의 라이브러리이다. NumPy는 데이터 구조 외에도

ko.wikipedia.org

 

import numpy as np 는 numpy를 사용하기 위해 import 하는 것이고 이 numpy를 np 라는 이름으로 사용하겠다는 것입니다.

pprint는 pritty print의 약자로 이쁘게 프린트 해 주는 python의 메소드 입니다.

 

그 다음의 open_file() 함수는 반복되서 나오는 거라서 설명을 생략하겠습니다.

 

gpt3_embedding() 함수가 openAI의 API를 사용하기 위해 만든 함수입니다.

엔진은 text-embedding-ada-002를 사용했습니다.

 

2022년 12월 15일에 발표된 자료에 따르면 이 모델이 성능도 좋으며 가장 저렴한 모델이라고 소개 돼 있습니다.

 

https://openai.com/blog/new-and-improved-embedding-model/

 

New and Improved Embedding Model

We are excited to announce a new embedding model which is significantly more capable, cost effective, and simpler to use. The new model, text-embedding-ada-002, replaces five separate models for text search, text similarity, and code search, and outperform

openai.com

 

이 함수에서 가장 중요한 부분은 openai.Embedding.create() 입니다.

OpenAI의 embedding 기능을 사용하기 위한 API 를 호출하는 겁니다.

파라미터로는 input 과 engine 이 있습니다.

여기서 engine은 model로 사용해도 됩니다. model이 아마 최근에 바뀐 파라미터 이름인 것 같습니다.

 

 

이 함수는 content를 받아서 ASCII 형식으로 인코딩 한 값을 다시 디코드해서 content에 담습니다.

(이 부분은 따로 하지 않아도 작동 됩니다.)

 

이 전달받은 content를 openai.Embedding.create() 을 사용해서 OpenAI에 사용할 모델 이름과 함께 보내고 거기서 받은 값을 response 변수에 담습니다.

 

받은 응답 (JSON 형식) 중에 data 배열의 첫번째 항목에서 embedding  부분을 vector에 담다서 이를 리턴합니다.

 

그 다음 함수는 similarity인데요.

이는 두 개의 파라미터 (벡터 형식)를 받아서 numpy의 dot 메소드를 사용해서 처리한 다음에 그 값을 반환하는 겁니다.

 

이 메소드는 벡터의 각 element들을 곱한 값들을 더한 값을 반환합니다.

 

파이썬의 이 dot 메소드는 여기에서 보시면 됩니다.

https://numpy.org/doc/stable/reference/generated/numpy.dot.html

 

numpy.dot — NumPy v1.24 Manual

Output argument. This must have the exact kind that would be returned if it was not used. In particular, it must have the right type, must be C-contiguous, and its dtype must be the dtype that would be returned for dot(a,b). This is a performance feature.

numpy.org

그 다음 22번째 줄에서는 openai에 openaiapikey.txt 파일에 저장돼 있는 key 값을 전달하고 있습니다.

이 key 값이 valid 하면 openai api를 사용할 수 있는 권한을 가지게 됩니다.

 

그 다음 함수는 match_class() 함수 입니다.

이 함수는 vector와 classes라는 두 개의 파라미터를 받습니다.

 

아래에 보시면 알겠지만 classes 는 35번째 줄에 있는 categories 값입니다.

위에 설명했지만 openai 의 각 모델들은 수 많은 dimention을 가지고 있습니다. 

이렇게 카테고리를 설정해 주지 않으면 아주 많은 리턴값이 나오게 됩니다.

그래서 이 함수를 만든건데요. 

우선 반환값은 list() 로 할 겁니다.

그 다음 classes (categories) 에 있는 값들을 하나 하나 for loop를 돌면서 처리 합니다.

두 벡터값을 similarity() 함수로 보내서 np.dot 값을 받아 오는 것이죠.

그 반환된 값은 score에 저장이 되고 그 값은 아래 info 변수에서 활용 됩니다.

info에는 각 카테고리별로 similarity에서 받아온 score를 넣습니다.

그 값들을 전부 results에 넣게 되고 이 값을 반환하게 됩니다.

 

이제 함수에 대한 설명은 다 됐고 실제 이것이 어떻게 실행이 되는지 보겠습니다.

 

34번째 if 문은 여러번 설명한 파이썬 문법입니다. 이 파이썬 파일이 실행 됐을 경우 아래 내용들이 처리 됩니다.

다른 파이썬 파일에서 import 되면 아래 내용이 처리되지않을 겁니다.

 

categories = ['plant', 'reptile', 'mammal', 'fish', 'bird', 'pet', 'wild animal']

 

카테고리는 이렇게 7개를 정했습니다. 사용자가 입력한 값이 이 중 어느것에 가장 가까운지 알아 볼 겁니다.

여기에는 다른 값들을 추가해도 됩니다.

예를 들어 food 나 brand 뭐 이런것을 추가해도 될 겁니다. 

 

그 다음은 classes라는 list()를 생성했습니다.

 

그리고 나서 for 루프가 나오는데요. 이 for 루프는 categories에 있는 인수들 만큼 루프를 돌립니다.

첫번째로 gpt3_embedding(c) 에 각 인수를 전달해서 그 값을 vector에 담습니다.

그 다음 info 에서는 이를 category 별로 그 vector 값이 담기게 합니다.

그리고 아까 만들었든 classes라는 리스트에 이 값을 담습니다.

 

이러면 categories의 각 인수들 마다 gpt 3 에서 받은 벡터값이 있게 됩니다.

 

이 벡터값을 이제 사용하게 됩니다.

 

43번째 줄을 보면 while 무한 루프를 만들었습니다.

사용자로부터 계속 입력값을 받기 위함이죠.

 

44번째 줄은 파이썬의 input() 메소드를 사용해서 사용자로부터 입력 받은 값을 a 라는 변수에 넣는 겁니다.

 

이 사용자가 입력한 값의 벡터값을 gpt3-embedding() 함수를 통해서 받습니다.

 

이러면 우리는 입력한 값의 벡터값과 아까 설정해 두었던 categories에 있는 각 인수들의 벡터값을 갖고 있습니다.

 

그러면 이제 입력한 값이 categories의 각 인수들과 얼마나 유사한지 알 수 있습니다.

 

47번째 줄에서는 match_class() 함수로 이 두 값을 보내서 각 카테고리별로 유사성 점수가 어떤지 정리한 값을 받습니다.

그 값은 result에 담기게 되고 pprint()를 이용해서 그 값을 이쁘게 출력을 하게 됩니다.

 

이걸 실행해 봤습니다.

 

 

첫번째로 frog 개구리는 새일 가능성이 가장 높고 그 다음은 물고기일 가능성이 높다고 나오네요. 

그 다음 파충류일 가능성이 세번째로 높습니다.

양서류라는 보기가 없어서 그럴까요?

 

그 다음 sockeye는 연어의 종류인데요. 결과는 물고기일 확률이 제일 높게 나옵니다. 그 다음은 새, 그리고 음식 뭐 이런 순으로 나가네요.

 

그 다음은 개구리를 대문자 F를 사용해서 입력했습니다.

그러면 파충류일 가능성이 제일 높다고 나오네요.

 

다음 호랑이는 야생동물일 가능성이 가장 높게 나오고 그 다음은 포유류와 유사성이 높다고 나옵니다.

 

국수를 입력했을 때는 역시 음식이 가장 유사하고 그 다음은 물고기, 식물 뭐 이런 순으로 나옵니다.

 

구찌를 입력했을 때는 브랜드와 가장 유사하고 그 다음은 음식, 그 다음은 새 이렇게 나옵니다.

 

아까 개구리가 약간 이상하게 나와서... 보기에 양서류 (amphibians)를 추가 했습니다.

 

 

 

그 결과는 Frog 일 경우 양서류와 가장 유사하고 그 다음이 파충류로 나옵니다.

 

frog 일 경우에는 새일 가능성이 가장 높고 그 다음이 물고기 - 파충류 - 양서류 이런 순서네요.

 

일단 답은 100% 만족스럽지 않지만 Openai GPT 3 의 Embedding 기능에 대해서 어느 정도 감이 잡혔습니다.

 

참고로 이 임베딩은 아래와 같은 경우에 사용될 수 있습니다.

 

  • Search (where results are ranked by relevance to a query string)
  • Clustering (where text strings are grouped by similarity)
  • Recommendations (where items with related text strings are recommended)
  • Anomaly detection (where outliers with little relatedness are identified)
  • Diversity measurement (where similarity distributions are analyzed)
  • Classification (where text strings are classified by their most similar label)

전체 소스 코드는 아래에 있습니다.

 

import openai
import numpy as np  # standard math module for python
from pprint import pprint


def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()


def gpt3_embedding(content, model='text-embedding-ada-002'):
    content = content.encode(encoding='ASCII',errors='ignore').decode()
    response = openai.Embedding.create(input=content,model=model)
    vector = response['data'][0]['embedding']  # this is a normal list
    return vector


def similarity(v1, v2):  # return dot product of two vectors
    return np.dot(v1, v2)


openai.api_key = open_file('openaiapikey.txt')


def match_class(vector, classes):
    results = list()
    for c in classes:
        score = similarity(vector, c['vector'])
        info = {'category': c['category'], 'score': score}
        results.append(info)
    return results


if __name__ == '__main__':
    categories = ['plant', 'reptile', 'mammal', 'fish', 'bird', 'pet', 'wild animal', 'food', 'brand',  'amphibians']
    classes = list()
    for c in categories:
        vector = gpt3_embedding(c)
        info = {'category': c, 'vector': vector}
        classes.append(info)
    #print(classes)
    #exit(0)
    while True:
        a = input('Enter a lifeform here: ')
        vector = gpt3_embedding(a)
        #print(a, vector)
        result = match_class(vector, classes)
        pprint(result)

반응형


반응형

이전 글에서 GPT 3 API로 초 간단 챕봇을 만들었습니다.

아래 내용이 그 소스 코드 입니다. (자세한 사항은 이전 글을 참조하세요)

https://coronasdk.tistory.com/1257

 

 

GPT-3 API로 초간단 Chatbot 만들기

오늘은 Python 과 ChatGPT API로 간단한 챗봇을 만들어 보겠습니다. import os import openai def open_file(filepath): with open(filepath, 'r', encoding='utf-8') as infile: return infile.read() openai.api_key = open_file('openaiapikey.txt') wh

coronasdk.tistory.com

 

오늘 볼 소스 코드는 아래와 같습니다.

 

이전 소스코드 보다 많이 복잡해 진 것 같지만 별다른 변화는 없고 그냥 26번째 줄 list를 추가했다는 내용밖에 없습니다.

 

우선 1~8번째 줄은 openai api를 사용하기 위해 api key를 제공하는 겁니다. 이전에 다룬 부분이니까 넘어가겠습니다.

 

그다음 10~23번째 줄은 openai.Completion.create() api를 사용하기 위해 만든 함수 입니다.

이전 초간단 챗봇 코드 보다 전달하는 파라미터를 많이 설정했습니다.

이 부분도 이전 글에서 다루었습니다.

 

https://coronasdk.tistory.com/1254

 

OpenAI API 첫 소스코드 분석 (초보자를 위한 해석)

지난번에 OpenAI API 연결을 테스트 하기 위해 만들었던 소스코드를 분석해 보겠습니다. 첫번째 import OpenAI는 OpenAI API 를 사용하기 위해 필요한 겁니다. 이것은 로컬에 OpenAI 를 깔았기 때문에 사용

coronasdk.tistory.com

25번째 줄은 이 파이썬 파일을 실행 했을 경우 그 아래 코드를 실행하라는 의미 입니다.

다른 파이썬 파일을 실행하고 그 파이썬 파일에서 이 파일을 import 한다면 그 아래 내용은 실행되지 않습니다.

그 설명도 윗 글에서 했습니다.

 

그 아래 while 문도 바로 전 글에서 다룬 부분인데 다른 부분은 list()를 추가 했다는 겁니다.

list()를 추가 한 이유는 대화를 할 때 이전 대화와 맥락이 맞는 답변을 받기 위해서 입니다.

 

그러기 위해서는 이전의 질문과 대답을 모두 같이 보내면 됩니다.

그러기 위해서 list를 사용하구요.

 

우선 26번째 줄에서 conversation 이라는 변수를 만들었고 이 변수에는 리스트가 담길 것이라고 선언했습니다.

아래 줄 while True: 는 그냥 아래 내용을 계속 실행하라는 무한 루프이구요.

user_input = input('USER: ') 는 사용자로 부터 입력 받은 내용을 user_input에 담는 겁니다.

이전 소스코드에소 그대로 있습니다. 다른 부분은 아래 라인 입니다.

 

이 user_input을 그대로 prompt로 사용하는 것이 아니라 위에 만들어 놓은 conversation이라는 리스트에 담는 겁니다.

 

conversation.append('USER: %s' % user_input)

 

%s 는 자바에서도 사용하는 것인다. string 형식의 내용이 담길 것이라는 거고 그 string은 ' ' 이 작은 따옴표 밖에 있는 % 에 나오는 내용이 됩니다.

 

그러면 conversation에 user_input 이 담기게 됩니다.

그 다음에 prompt 변수가 나옵니다.

 

여기서는 prompt_chat.txt 라는 파일의 내용을 불러오게 되는데요.

이 파일에는 다름과 같은 내용이 담겨져 있습니다.

이 대화는 USER 와 JAX가 나누는 대화이고 JAX는 세계 평화를 목표로 하는 감성적인 기계이다 라고 상황을 설정해 놓았습니다.

이렇게 상황을 설정하면 GPT 3 는 JAX 의 성격에 맞는 답변을 찾아서 보내 줍니다.

그 아래 <<BLOCK>> 은 의미가 없고 그냥 31번째 줄에서 보여 주듯이 위에서 설정한 text_block을 replace 해주기 위해 만들어 놓은 겁니다.

 

prompt = open_file('prompt_chat.txt').replace('<<BLOCK>>', text_block)

 

이렇게 되면 prompt에는 prompt_chat.txt에 기존에 있는 내용에 text_block을 합한 내용이 저장되게 됩니다.

 

prompt = prompt + '\JAX: '

 

부분은 답변을 표시할 때 그 앞에 JAX: 를 나타내기 위해서 만든 겁니다.

 

그러면 이제 질문이 완성 됐습니다.

 

이 질문을 이용해서 opanai.Completion.create() api를 사용해서 질문을 던지고 답변을 받으면 됩니다.

이 일을 하는 함수는 그 위에 gpt3_completion() 입니다.

 

response = gpt3_completion(prompt)

 

그 함수에 prompt를 던지고 openai로 부터 받는 응답은 response에 담기게 됩니다.

 

그 다음은 그 응답을 print 하는 겁니다.

 

이 대답은 다시 conversation에 추가 됩니다.

 

conversation.append('JAX: %s' % response)

 

이렇게 하면 다음번 질문을 할 때 이전 질문과 대답까지 다 합해서 openai의 GPT3에게 보내서 이전 대화와 맥락이 맞는 답변을 듣게 됩니다.

 

 

이렇게 미리 설정해 놓은 상황과 이전 응답에 맥락이 맞는 대화를 할 수 있는 챗봇을 만들었습니다.

다시 말씀 드리지만 위 응답은 GPT3의 가장 저렴한 테스트 모델인 text-ada-001을 사용했습니다.

비용 절감 차원에서 이 모델로 테스트 하고 있습니다.

text-davinci-003 모델을 사용하면 좀 더 그럴 듯한 대화를 나누실 수 있습니다.

 

전체 소스코드는 아래와 같습니다.

 

import openai

def open_file(filepath) :
    with open(filepath, 'r', encoding='utf-8') as infile :
        return infile.read()
        

openai.api_key=open_file('openaiapikey.txt')

def gpt3_completion(prompt, engine='text-davinci-003', temp=0.7, top_p = 1.0, tokens =400, freq_pen=0.0, pres_pen=0.0, stop=['JAX: ', 'USER: ']) :
    prompt = prompt.encode(encoding='ASCII', errors='ignore').decode()
    response = openai.Completion.create(
        #engine=engine,
        engine='text-ada-001', 
        prompt=prompt,
        temperature=temp,
        max_tokens=tokens,
        top_p=top_p,
        frequency_penalty=freq_pen,
        presence_penalty=pres_pen,
        stop=stop)
    text = response['choices'][0]['text'].strip()
    return text
  
if __name__ == '__main__' :
    conversation = list()
    while True:
        user_input = input('USER: ')
        conversation.append('USER: %s' % user_input)
        text_block = '\n'.join(conversation)
        prompt = open_file('prompt_chat.txt').replace('<<BLOCK>>', text_block)
        prompt = prompt + '\JAX: '
        response = gpt3_completion(prompt)
        print('JAX: ', response)
        conversation.append('JAX: %s' % response)
        

 

반응형

GPT-3 API로 초간단 Chatbot 만들기

2023. 2. 8. 00:13 | Posted by 솔웅


반응형

오늘은 Python 과 ChatGPT API로 간단한 챗봇을 만들어 보겠습니다.

 

import os
import openai

def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

while True:
    prompt = input("\n Ask OpenAI Anything: ")
    completions = openai.Completion.create(prompt = prompt,
                                            engine='text-ada-001', #engine="text-davinci-003",
                                            max_tokens=100)   
    print(completions)  

아직 완성된 코드는 아닙니다. 하나 하나 보겠습니다.

 

Openai.api_key 부분 까지는 계속 반복되는 코드 블럭입니다.

Openai에게 api 사용 권한을 받기 위해 api key를 제공하는 부분 입니다.

 

챗봇 기능이 이뤄지는 부분은 while 문 부터 입니다.

 

while True: 는 무한 루프 입니다.

이 프로그램을 실행하면 끝마치는 방법은 강제 종료 (Ctrl Z) 해야 합니다.

 

그 다음은 prompt 변수 부분입니다.

이 변수는 openai.Completion.create api를 사용할 때 필요한 건데요. 이전 글에서도 봤듯이 사용자가 궁금해 하는 질문이 여기에 들어가게 됩니다.

prompt = input("\n Ask OpenAI Anything: ")

 

input()은 Python 의 메소드로 사용자의 입력을 받겠다는 겁니다.

이 부분은 실행되면 "" 안에 있는 내용이 출력 되고 Python은 사용자의 입력을 받을 준비를 하게 됩니다.

 

 

그 다음 부분이 openai의 API 입니다.

openai.Completion.create() api를 사용해서 질문을 던지고 대답을 받습니다.

파라미터는 간단하게 3가지만 전달합니다.

질문은 prompt 이구요. 여기에는 사용자가 입력한 질문이 들어가게 됩니다.

사용하는 openai 모델은 text-ada-001 입니다. 가장 저렴한 모델입니다.

좋은 모델을 사용하려면 text-davinci-003을 쓰면 됩니다.

요즘 유행하는 ChatGPT는 아직 api가 공개 되지 않았습니다.

나중에 공개 되면 그 모델 이름을 넣으면 ChatGPT API를 사용할 수 있습니다.

마지막 파라미터로는 max_tokens 가 사용 됐습니다.

Token 에 대해서는 여기서 알아 보세요.

 

https://platform.openai.com/tokenizer

 

OpenAI API

An API for accessing new AI models developed by OpenAI

platform.openai.com

 

여기서는 특정 문장이 몇개의 토큰으로 이뤄 졌는지 알 수 있는데요.

예를 들어 What is a capital city of South Korea? 가 몇개의 토큰으로 이뤄 져 있는지 알아 보겠습니다.

 

 

글자 수는 총 38자 이지만 토큰은 9개 이네요. 이 토큰은 GPT 모델이 사용하는 단위 입니다. 대개 한 단어가 1개의 토큰으로 이뤄 집니다. (그렇지 않은 경우도 있구요.)

위 경우를 보면 tokenized는 token과 ized 이렇게 두개의 토큰으로 간주 하는 것을 볼 수 있습니다.

 

하여간 이 코드에서 사용한 아래 스크립트 내용은 최대 100개의 토큰까지 허용하겠다는 겁니다.

max_tokens=100

 

토큰이 많으면 비용과 시간이 늘어 날 수 있겠죠.

 

이렇게 openai.Completion.creat() api를 이용해서 질문을 보내면 response를 받게 되는데요.

이 response는 completions라는 변수에 담기게 됩니다.

 

print(completions) 는 이 completions를 출력하라는 python 메소드 입니다.

 

이 코드를 실행해서 질문을 던져 보겠습니다.

 

남한의 수도는 어디냐고 물어봤더니 긴 Json 형식의 대답을 받았습니다.

openai.Completion.creat() api 는 질문에 대한 답을 이런 형식으로 줍니다.

다양한 정보가 담겨 있습니다.

 

질문에 대한 답변은 choices 에 있는 text 부분입니다.

서울이 남한의 수도라는 대답이 담겨 있습니다.

 

이 부분만 따로 뽑아서 표시해야 제대로 된 chatbot이 될 것 같습니다.

 

그 방법은 아래와 같이 하면 됩니다.

 

completion = completions.choices[0].text

print(completion)

 

completions의 choices 라는 배열 안에 있는 text 값만 따로 completion이라는 변수에 담아서 이것을 출력 하는 겁니다.

 

이렇게 하면 다음과 같이 질문을 계속 주고 받을 수 있습니다.

 

답변이 모두 정답은 아닙니다.

왜냐하면 여기서 사용한 모델은 가장 저렴한 테스트 용인 text-ada-001 이기 때문입니다.

좀 더 정확한 답을 받으려면 text-davinci-003 를 사용하면 됩니다. 이 모델이 조금 더 비싸죠.

 

이 모델은 남한의 수도만 서울이라고 맞히고 북한, 캐나다, 호주의 수도는 다 틀렸습니다.

(정답은 북한 - 평양, 캐나다 - 오타와, 호주 - 캔버라 입니다.)

 

이렇게 계속 질문을 하고 답변을 받는 채팅 기능이 완성 됐습니다.

 

전체 코드는 아래와 같습니다.

 

import os
import openai

def open_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as infile:
        return infile.read()

openai.api_key = open_file('openaiapikey.txt')

while True:
    prompt = input("\n Ask OpenAI Anything: ")
    completions = openai.Completion.create(prompt = prompt,
                                            engine='text-ada-001', #engine="text-davinci-003",
                                            max_tokens=100)
    completion = completions.choices[0].text
    print(completion)

 

 

한가지 불완전한 부분이 있는데요.

 

GPT3와 응답을 하게 되면 GPT3 가 이전 질문을 기억하고 그 맥락에 맞는 답변을 하게 됩니다.

그런데 이렇게 openai.Completion.create() api 에 질문을 하나만 넣으면 이 전에 대화 했던 내용은 전혀 참고가 될 수 없습니다.

 

맥락이 있는 대화를 할 수 있는 챗봇을 만들려면 이 질문 부분에 이전의 질문까지 다 같이 보내야 합니다.

 

이 부분은 list를 사용해서 간단히 해결 할 수 있습니다.

 

그 소스는 다음 글에서 분석해 보겠습니다.

반응형


반응형

지난번에 OpenAI API 연결을 테스트 하기 위해 만들었던 소스코드를 분석해 보겠습니다.

 

첫번째 import OpenAI는 OpenAI API 를 사용하기 위해 필요한 겁니다.

이것은 로컬에 OpenAI 를 깔았기 때문에 사용 가능하게 된 것입니다.

지난 글에 아래 명령어를 사용해서 OpenAI를 깔았었습니다.

 

pip install openai

pip install openai --upgrade

 

Pip 명령어는 Python을 깔았기 때문에 사용 가능한 것입니다.

자세한 사항은 이전 글을 참조하세요.

 

https://coronasdk.tistory.com/1252

 

로컬 개발 환경 세팅하기 : Python , OpenAI Install

OpenAI API 를 사용하기 위해서는 OpenAI 를 로컬에 깔아야 하고 이 OpenAI API를 사용해서 어플리케이션을 만들 언어도 깔아야 합니다. 저는 파이썬을 깔겠습니다. 파이썬은 이곳에서 다운 받아서 인스

coronasdk.tistory.com

 

두번째는 함수 입니다. 

 

OpenAI를 사용하기 위해서는 내가 Open AI로 부터 받은 API KEY를 제공해서 인증을 받아야 합니다. 일정의 비밀번호이죠.

Open AI API는 유료입니다. 

지난 글에서 간단한 질문 하나 하는데 1원정도가 청구 되는 걸 보았습니다.

유료이기 때문에 나의 API KEY를 사용해서 인증을 받고 그 다음에 사용하는 만큼 금액이 청구 됩니다.

당연히 이 API KEY를 보내지 않으면 OpenAI API를 사용할 수 없습니다.

 

이 API KEY를 보내는 방법은 8번째 줄에 있습니다.

openai.api_key = "My API KEY"

 

그런데 여기에 키를 하드 코딩 하면 보안상 문제가 될 수 있고 또한 이 키가 변경이 되었을 때 일일이 모든 파일에 있는 키 정보를 업데이트 해야 합니다. 관리상의 문제가 있죠.

 

그래서 보통 이런 경우는 별도의 파일을 만들어서 관리를 하고 파이썬 파일 안에서는 이 파일을 열고 그 내용을 읽어서 사용합니다.

 

이렇게 파일을 열고 그 내용을 읽는 부분을 함수로 만든 부분이 3~5째 줄에 있는 내용입니다.

 

def open_file(filepath) : 
    with open(filepath, 'r', encoding='utf-8') as infile : 
        return infile.read() 

 

파이썬에서 함수를 만들려면 def 로 시작하면 됩니다. 그 다음은 함수 이름이 오고 그 다음 괄호 안에 파라미터들을 넣습니다.파라미터가 여러개 있는 경우 쉼표 , 로 구분합니다. 그리고 마지막엔 : 로 끝납니다.

 

그 다음 줄은 함수의 내용입니다. 

 

파이썬에서 파일을 열고 읽는 방법은 아래와 같습니다.open("파일 이름", r,)두번째 파라미터인 r은 이 파일을 읽겠다는 겁니다. w 는 파일에 내용을 쓸 때 사용하고 a는 파일 내용 마지막에 새로운 내용을 추가할 때 사용할 수 있습니다.일반적으로 프로그래밍에서는 파일을 열었으면 마지막에 더 이상 사용하지 않을 때 이 파일을 close()해주어야 합니다. file_data = open("file.txt")print(file_data.readline(), end="")file_data.close()

 

이렇게 해야 되는데요. with를 사용해면 이 close() 부분을 자동으로 해 줍니다.아래 두 줄은 위의 세 줄과 똑 같은 겁니다.

 

with open("file.txt) as file_data:print(file_data.readline(), end="")

 

Close()는 with 문을 나올 때 이루어 집니다.

 

참고로 파이썬에서는 들여쓰기로 영역을 지정합니다.자바에서는 함수 (메소드)를 선언 할 때 {}로 지정하는 것과 차이가 있습니다.

 

그러므로 파이썬에서는 들여쓰기를 할 때 주의 해야 합니다.If, for, class, def 등을 사용할 때 그 줄 끝에 : 가 나오게 되는데 그 다음줄은 반드시 들여쓰기를 해야 합니다.그리고 블럭 내의 들여쓰기 칸 수는 같습니다.

위반시에는 indentationError: unexpected indent 라는 에러를 출력합니다.

 

 

이제 위의 코드를 해석할 수 있는 사전 지식은 다 갖추었습니다.

 

open_file()이라는 함수를 만든다는 것이 첫째줄에서 이야기 하는 겁니다. 파라미터는 filepath 입니다.

다음에 칸을 들여써서 with open() 을 사용해서 파일을 엽니다. 

열 파일은 filepath입니다. 나중에 이 함수를 호출 할 때 제공해 주어야 합니다.

r은 이 파일을 읽겠다는 의미이고 세번째 파라미터는 그 파일의 인코딩 형식입니다. Txt 파일은 Ute-8이라고 선언해 주면 됩니다.

세번째 파라미터는 생략해도 작동을 할 겁니다. 보다 정확하게 하기 위해 선언 해 주셔도 됩니다.

 

as infile 은 변수 이름이 infile 이라는 겁니다. 

파일을 열었으니까 그 내용이 infile에 저장 돼 있는 겁니다.

 

그 다음은 infile의 내용을 read()를 사용해서 가져오고 그 내용을 return 하는 겁니다.

 

이로서 open_file() 함수는 다 이해 했습니다.

 

이 함수를 사용하는 부분이 바로 8번째 줄입니다.

 

openai.api_key=open_file('openaiapikey.txt')

 

openai.api_key 는 OpenAI에서 정한 규칙입니다. API 키를 제공하기 위해서는 이 변수에 API 키 정보를 담으면 됩니다.

= 이후에 내용이 아까 만들었던 함수를 호출하는 부분입니다.

파라미터는 openaiapikey.txt 입니다. 따로 폴더 정보가 없으면 현재 폴더에서 해당 파일을 찾아서 열게 됩니다.이 텍스트 파일은 미리 만들어서 그 안에 API 키 정보를 넣어 두어야 합니다.

 

자 이러면 OpenAI 에 내 API 키를 제공했고 이 키가 유효하다면 지금부터 OpenAI API 를 사용할 수 있습니다.

 

10번째 줄은 또 다른 함수를 선언 한 것입니다.

 

gpt3_completion() 이란 함수를 선언했고 파라미터는 8개나 되네요.이 파라미터들은 함수 안에서 사용하게 될 겁니다.

이 줄은 :로 끝났고 그 아래서 부터는 들여쓰기를 해서 이 함수의 영역을 나타냅니다.

이 함수는 OpenAI 의 Completion.create() API 를 사용하기 위해 만드는 겁니다.

 

우선 Completion.create()에 대해 알아야 합니다.

 

이것은 제 블로그의 Open AI > API REFERENCE > Completions - openai.Completion.create() 를 보시면 자세한 사항을 볼 수 있습니다.

 

https://coronasdk.tistory.com/1234

 

Completions - openai.Completion.create()

https://beta.openai.com/docs/api-reference/completions Completions Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. 프롬프트가 주어지면 모델은

coronasdk.tistory.com

 

 

여기서 제일 중요한 파라미터는 prompt 입니다.

내가 OpenAI 에 질문할 내용이죠.

11번째 줄은 이 prompt 변수는 ASCII 형식으로 인코딩을 한 다음에 다시 디코딩을 한다는 내용입니다.

사실 이 부분이 없어도 이 스크립트는 에러 없이 실행 됩니다. ASCII 형식으로 한다는 것을 확실하게 하기 위해 이 부분을 넣었습니다.

 

두번째로 중요한 부분은 model 입니다.

내가 OpenAI의 어떤 모델을 사용할 것인가를 API에 알려 주는 것이죠.

 

모델들에 대해 알고 싶다면 제 블로그의 Open AI > GET STARTED > Get Started - Models를 참고하세요.

 

https://coronasdk.tistory.com/1212

 

Get Started - Models

https://beta.openai.com/docs/models/overview OpenAI API An API for accessing new AI models developed by OpenAI beta.openai.com Models Overview The OpenAI API is powered by a family of models with different capabilities and price points. You can also custom

coronasdk.tistory.com

 

 

이 예제에서는 text-davinci-003 이란 모델을 사용하겠다고 API에게 전달하고 있습니다.다음에는 테스트용으로는 text-Ada-001을 사용해야 겠습니다. 비용이 가장 저렴한 것 같네요.

 

이 저렴한 모델을 사용했더니 대답이 이렇게 나오네요.The two Koreas will be unified under a single government and president by 2020.

 

2020년 까지 통일이 된다네요.

 

이 모델로 하면 내용은 신뢰할 수 없을 것 같습니다. 그냥 API 테스트 용으로만 사용해야 될 것 같네요.

 

그 외의 파라미터 들은 https://coronasdk.tistory.com/1234 를 참조하세요.

 

이 함수는 openai.Completion.create()을 사용하기 위한 함수라고 했습니다.

그렇긴 때문에 가장 중요한 부분은 12번째 줄부터 있는 response 변수 부분입니다.

이 변수에는 openai.Completion.create() 이 들어가고 각각의 파라미터들이 세팅 돼 있습니다.

이렇게 되면 response라는 변수에 Open AI 의 GPT 3가 질문에 대답한 내용이 담기게 됩니다.

질문은 prompt에 있었구요.

21번째 줄에서는 이 응답 내용 앞과 뒤에 공백이 있다면 그 공백을 없애기 위해 strip()을 사용해서 그 결과를 text 에 담았습니다.

이 strip() 함수는 파이썬 함수입니다.

Python 튜토리얼을 참조하세요.

https://www.w3schools.com/python/ref_string_strip.asp

 

Python String strip() Method

W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.

www.w3schools.com

22번째 줄에서는 이 text를 리턴 합니다.

 

여기까지가 gpt3_completion() 함수의 내용입니다.

요약하면 이 함수는 openai.Completion.create() API를 이용해서 Open AI에 질문을 하고 응답을 받은 후 그 내용을 반환하는 함수입니다.

 

가장 핵심적인 부분이죠.

 

여기까지가 함수 들이었구요. 이제 실질적으로 실행되는 부분이 24번째 줄에 있습니다.

 

일단 __name__ 이라는 변수가 나오는데 이 변수는 파이썬에 내장 돼 있는 변수입니다.

여기에는 일단 이 파이썬 파일의 이름 (모듈 이름)이 담기게 됩니다.

만약 다른 파이썬 파일을 실행 시키는데 그 파일에서 현재의 파일을 import 해서 사용하는 경우에 __name__ 변수에 현재의 파이썬 파일 이름 (모듈 이름) 이 담기게 됩니다.

하지만 이 파일 안에서 이 함수를 실행시키면 __name__ 변수에는 파일 이름이 아니라 __main__이라는 값이 들어가게 됩니다.

 

그러므로 if 문은 이 파일이 외부에서 import 된 것이 아니라 이 파일 자체가 실행이 돼었다면 이라는 뜻입니다.

자세한 내용은 이 글을 참조하세요.

 

https://lovelydiary.tistory.com/297

 

파이썬) __name__ 변수는 뭐지? (+if __name__=="__main__"의 의미..)

#1. 변수를 정의할 때 꼭 필요한 문장? 함수를 정의하는 것과 관련하여 파이썬 강의를 듣는 중이었다. 함수의 기능을 열심히 정의를 하고 나서 선생님이 꼭 마지막에 희한한 무언가를 붙이셨다.

lovelydiary.tistory.com

그러므로 이 파이썬을 실행하게 되면 25~27번째 줄이 실행 됩니다.

 

prompt 는 질문이 들어가게 됩니다.

이 소스코드에서는 When will South Korea and North Korea be unified?: <- 이렇게 질문 했습니다.

 

그 다음에는 response 변수에 gpt3_completion() 함수에서 반환 된 값을 저장하게 됩니다.

그리고 마지막 줄인 27번째 줄에서는 이 response 에 담겨 있는 답변을 print 하게 됩니다.

 

이 파일을 실행하면 같은 모델이라도 매번 다른 답변이 나옵니다.

여러 모델 별로도 답변의 질이 다르구요.

 

아래는 가장 저렴한 text-ada-001을 사용했을 때 답변 중 하나입니다.

 

여기에서는 북한의 리더인 F Mkotsoalae 가 살해 되면 통일 될 것이라고 나오네요.

 

북한의 리더 이름도 틀렸고 내용도 좀 허접합니다.

 

가장 좋은 모델인 text-davinci-003 을 사용해 보겠습니다.

 

통일이 될지 안 될지 그리고 된다면 언제쯤 될지 예측하는 것은 불가능 하다네요. 2차대전 이후로 갈라 졌고 협상이 이어지고 있지만 통일에 대한 타임라인은 없다는게 이 모델의 응답입니다.

 

이것은 text-davinci-003 모델이 답한 겁니다.

 

요즘 OpenAI 에서 ChatGPT라는 좀 더 진보된 인공지능 모델을 선보여서 화제입니다.

이 모델은 아직 API 가 제공되지 않고 있습니다.

조만간 제공하겠다는 안내문은 있습니다.

 

CharGPT 웹사이트로 가서 질문해 보니 아래와 같은 답변을 얻을 수 있었습니다.

 

 

text-davinci-003과 거의 유사한 답변을 얻을 수 있었습니다.

답변이 조금 더 세련 된 것 같기도 하고......

 

하여간 지금까지 OpenAI API 테스트를 위해 만든 소스코드를 해석해 봤습니다.

 

반응형


반응형

OpenAI API 를 사용하기 위해서는 OpenAI 를 로컬에 깔아야 하고 이 OpenAI API를 사용해서 어플리케이션을 만들 언어도 깔아야 합니다.

 

저는 파이썬을 깔겠습니다.

 

파이썬은 이곳에서 다운 받아서 인스톨 하면 됩니다.

 

https://www.python.org/downloads/

 

Download Python

The official home of the Python Programming Language

www.python.org

다 깐 다음에는 아래 명령어로 버전을 확인 하고 최신버전으로 업그레이드도 합니다.

python --version

pip install pip --upgrade

 

 

그 pip을 이용해서 openAI를 인스톨 합니다.

 

pip install openai

pip install openai --upgrade

 

여기까지 하면 openai API로 어플리케이션을 개발 할 수 있습니다.

 

저는 참고로 소스 관리를 위해 github 세팅을 했고 편집 툴로 notepad++를 깔았습니다.

 

https://github.com/

 

GitHub: Let’s build from here

GitHub is where over 94 million developers shape the future of software, together. Contribute to the open source community, manage your Git repositories, review code like a pro, track bugs and feat...

github.com

https://notepad-plus-plus.org/downloads/

 

Downloads | Notepad++

 

notepad-plus-plus.org

 

이제 첫번째 파일을 한번 만들어 보죠. (저는 notepad ++를 사용해서 만들었습니다.)

 

import openai

def open_file(filepath) :
    with open(filepath, 'r', encoding='utf-8') as infile :
        return infile.read()
        

openai.api_key=open_file('openaiapikey.txt')

def gpt3_completion(prompt, engine='text-davinci-002', temp=0.7, top_p = 1.0, tokens =400, freq_pen=0.0, pres_pen=0.0, stop=['<<END>>']) :
    prompt = prompt.encode(encoding='ASCII', errors='ignore').decode()
    response = openai.Completion.create(
        engine=engine,
        prompt=prompt,
        temperature=temp,
        max_tokens=tokens,
        top_p=top_p,
        frequency_penalty=freq_pen,
        presence_penalty=pres_pen,
        stop=stop)
    text = response['choices'][0]['text'].strip()
    return text
  
if __name__ == '__main__' :
    prompt = 'When will South Korea and North Korea be unified?:'
    response = gpt3_completion(prompt)
    print(response)

 

이렇게 만들었습니다.

소스 코드 설명은 아래 카테고리에 있는 글들을 참고하세요.

https://coronasdk.tistory.com/category/Open%20AI

 

'Open AI' 카테고리의 글 목록

개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들

coronasdk.tistory.com

참고로 8번째 줄의 openaiapikey.txt 는 같은 폴더에 이 파일을 만들고 그 안에 OpenAI로부터 받은 API Key를 넣으면 됩니다.

 

Open AI 에 던진 질문은 남한과 북한은 언제 통일이 될까? 입니다.

When will South Korea and North Korea be unified?:

 

이제 이것을 실행하고 그 답을 볼까요?

실행은 아래와 같이 합니다.

python hello_world.py

 

응답은 이렇게 나왔네요.

 

한국말로 번역하면

"한반도와 지역 전체의 정치적 상황을 포함한 여러 요인에 따라 변동성이 크기 때문에 남북한이 언제 통일될지 예측할 수 없습니다."

 

이렇게 나왔습니다.

 

한번 더 질문을 해 보겠습니다.

 

"확실한 답은 없지만 많은 전문가들은 가까운 미래에 통일이 이루어질 가능성은 낮다고 보고 있습니다."

첫번째와는 조금 다르게 나왔는데요. 뭐 크게 다르지는 않네요.

 

한번만 더 해보죠.

 

"통일 시기는 북한의 비핵화, 경제 발전, 남북 관계 등 여러 요인에 따라 달라지기 때문에 정해진 날짜는 없다."

 

표현은 약간 다르지만 대충 언제 통일 될지는 알 수 없고 여러 대내외적 요인에 따라 달라질 수 있으며 가까운 미래에 통일될 가능성은 낮다는 내용입니다.

 

이로서 로컬에 개발 환경 세팅하고 OpenAI API 와 처음으로 소통해 봤습니다.

 

성공~~~~~~

 

P.S.

참고로 이 API를 이용하는 것은 유료입니다.

위와 같이 세번 이용한 금액은 0.00262 달러 입니다.

원화로 하면 3.23원 쯤 됩니다. 그러니까 저런 간단한 질문 하나 하면 1원 쯤 지불 해야 하네요.

이렇게 작업한 내용은 아래와 같이 github repository에 저장합니다.

 

git add . ==> 업데이트되거나 새로 생성된 파일을 추가한다.

git status

git commit -am "initial commit"

git push

 

이러면 내 소스파일을 github에 저장할 수 있습니다.

 

 

그럼....

반응형

Parameter details

2023. 1. 17. 02:27 | Posted by 솔웅


반응형

https://beta.openai.com/docs/api-reference/parameter-details

 

Parameter details

Frequency and presence penalties

 

The frequency and presence penalties found in the Completions API can be used to reduce the likelihood of sampling repetitive sequences of tokens. They work by directly modifying the logits (un-normalized log-probabilities) with an additive contribution.

 

Completions API에 있는 빈도 및 존재 여부 패널티를 사용하여 반복적인 토큰 시퀀스를 샘플링할 가능성을 줄일 수 있습니다. 추가 기여로 로짓(정규화되지 않은 로그 확률)을 직접 수정하여 작동합니다.

 

mu[j] -> mu[j] - c[j] * alpha_frequency - float(c[j] > 0) * alpha_presence

 

Where:

  • mu[j] is the logits of the j-th token
  • mu[j]는 j번째 토큰의 로짓입니다.
  • c[j] is how often that token was sampled prior to the current position
  • c[j]는 현재 위치 이전에 해당 토큰이 샘플링된 빈도입니다.
  • float(c[j] > 0) is 1 if c[j] > 0 and 0 otherwise
  • float(c[j] > 0)은 c[j] > 0이면 1이고 그렇지 않으면 0입니다.
  • alpha_frequency is the frequency penalty coefficient
  • alpha_frequency는 빈도 페널티 계수입니다.
  • alpha_presence is the presence penalty coefficient
  • alpha_presence는 존재 페널티 계수입니다.

 

 

As we can see, the presence penalty is a one-off additive contribution that applies to all tokens that have been sampled at least once and the frequency penalty is a contribution that is proportional to how often a particular token has already been sampled.

 

보시다시피 존재 패널티는 적어도 한 번 샘플링된 모든 토큰에 적용되는 일회성 추가 기여이며 빈도 페널티는 특정 토큰이 이미 샘플링된 빈도에 비례하는 기여입니다.

 

Reasonable values for the penalty coefficients are around 0.1 to 1 if the aim is to just reduce repetitive samples somewhat. If the aim is to strongly suppress repetition, then one can increase the coefficients up to 2, but this can noticeably degrade the quality of samples. Negative values can be used to increase the likelihood of repetition.

 

페널티 계수의 합리적인 값은 반복적인 샘플을 어느 정도 줄이는 것이 목표인 경우 약 0.1에서 1 사이입니다. 반복을 강력하게 억제하는 것이 목적이라면 계수를 최대 2까지 높일 수 있지만 이렇게 하면 샘플의 품질이 눈에 띄게 저하될 수 있습니다. 음수 값을 사용하여 반복 가능성을 높일 수 있습니다.

 

반응형