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

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리


반응형

오늘 다룰 내용은 아래 페이지에 있습니다.

https://github.com/openai/openai-cookbook/blob/main/examples/How_to_stream_completions.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

How to stream completions

 

기본적으로 OpenAI Completions api 콜을 하면 Request된 Prompt에 대해 작업을 완료 한 다음 하나의 response로 답을 보내 옵니다.

 

davinci 모델의 경우 아주 긴 completion을 생성하게 되면 응담까지 몇초가 걸릴 수 있습니다. 2022년 8월 현재 text-davinci-002 모델의 응답시간은 일반적으로 100 completion token당 1초에서 2초 정도가 걸립니다.

 

이 정도도 기다리지 않고 더 빨리 응답을 받고 싶으면 Stream을 사용하실 수 있습니다.

이렇게 되면 Request된 Prompt에 대한 답을 완료하기 전에 일부분에 대한 답을 받아서 인쇄를 시작하거나 Completion 의 시작 부분을 처리할 수 있습니다.

 

Completions를 stream 하려면 API를 호출 할 때 stream=True를 사용하시면 됩니다.  이렇게 하면 data-only 서버가 보낸 이벤트와 같이 텍스트를 streams back 하는 object를 return 하게 됩니다. (답변의 일부분이라도 완료 되면 그 일 부분들을 계속 해서 stream으로 응답을 보내 온 다는 얘기 입니다.)

 

Downsides

 

주의 할 점은 프로덕션 어플리케이션에서 stream=True를 사용하면 completions 의 내용을 조정하기가 더 어려워 집니다. 

(streaming 되는 동안 답변의 일부분만 가지고 있기 때문에 전체 답변에 대한 보정이나 처리 작업을 하는데는 한계가 있다는 얘기 입니다.)

 

이 Streaming response의 다른 작은 단점은 Response에 사용된 토큰 수를 알려 주는 정보가 없다는 겁니다. 

해당 토큰수를 알려면 모든 응답을 결합한 후 toktoken을 사용해서 직접 계산 해야 합니다.

 

Example code

아래에 이 streaming completions를 어떻게 사용하는지에 대한 파이썬 예제가 있습니다.

 

# imports
import openai  # for OpenAI API calls
import time  # for measuring time savings

우선 openai를 import 합니다. 그리고 응답 시간을 알아보기 위해 time 모듈도 import 합니다.

 

# Example of an OpenAI Completion request
# https://beta.openai.com/docs/api-reference/completions/create

# record the time before the request is sent
start_time = time.time()

# send a Completion request to count to 100
response = openai.Completion.create(
    model='text-davinci-002',
    prompt='1,2,3,',
    max_tokens=193,
    temperature=0,
)

# calculate the time it took to receive the response
response_time = time.time() - start_time

# extract the text from the response
completion_text = response['choices'][0]['text']

# print the time delay and text received
print(f"Full response received {response_time:.2f} seconds after request")
print(f"Full text received: {completion_text}")

이 방법은 stream을 사용하지 않은 일반적인 방법입니다.

openai.Completion.create() 파라미터를 보면 stream=True 부분이 없습니다. 즉 응답받는데 streaming을 사용하지 않는 겁니다.

openai.COmpletion.create() api를 호출하기 전 시간을 start_time에 담고 호출 한 후의 시간을 response_time에 담았습니다.

response 받은 데이터 중 choices의 첫번째 아이템에 있는 text 부분만 떼어내서 completion_text 에 담습니다.

 

그런 다음 전체 소요 시간을 print 하고 두번째 print에서는 completion_text를 print 합니다.

 

저는 위 소스코드를 조금 수정해서 openaiapikey를 text 파일에서 읽어와서 인증 받는 부분을 추가했고 Completion api를 호출할 때 davinci보다 저렴한 ada 모델을 사용하는 것으로 했습니다.

 

응답은 2.86초 걸렸고 그 내용은 Full text received 에 있습니다.

 

A streaming completion request

# Example of an OpenAI Completion request, using the stream=True option
# https://beta.openai.com/docs/api-reference/completions/create

# record the time before the request is sent
start_time = time.time()

# send a Completion request to count to 100
response = openai.Completion.create(
    model='text-ada-001', #model='text-davinci-002',
    prompt='1,2,3,',
    max_tokens=193,
    temperature=0,
    stream=True,  # this time, we set stream=True
)

# create variables to collect the stream of events
collected_events = []
completion_text = ''
# iterate through the stream of events
for event in response:
    event_time = time.time() - start_time  # calculate the time delay of the event
    collected_events.append(event)  # save the event response
    event_text = event['choices'][0]['text']  # extract the text
    completion_text += event_text  # append the text
    print(f"Text received: {event_text} ({event_time:.2f} seconds after request)")  # print the delay and text

# print the time delay and text received
print(f"Full response received {event_time:.2f} seconds after request")
print(f"Full text received: {completion_text}")

이번에는 Streaming을 사용하는 에제입니다.

 

openai.Completion.create() 의 파라미터를 보면 stream=True 를 추가 하신것을 보실 수 있습니다.

이것이 streaming을 사용하는 방법 입니다.

 

그 이후의 코드들은 이 스트리밍을 어떻게 receive 하는지 print 해주기 위해 만든 스크립트 들 입니다.

 

for 문이 있는데 이 for 문에서는 response 에 발생한 이벤트만큼 실행 됩니다.

 

이벤트가 일어날 때마다 그 이벤트 시각에서 start_time을 뺍니다. 즉 Completion api 콜 한 시점에서 그 이벤트가 일어난 시점까지의 시간을 알기 위해서 이런 계산을 합니다. 그 값은 event_time에 저장 됩니다.

그리고 collected_events에 이 이벤트를 append 합니다.

그리고 그 이벤트에서 받은 reponse에서 choices의 첫번째 아이템에 있는 text 내용을 가져 옵니다.

completion_text에는 지금까지 받은 evnet_text에 현재 받은 event_text를 추가 합니다.

그리고 for  문 안에 있는 print 문에서는 응답 내용과 그 응답을 받기 까지 얼마나 걸렸는지를 print 해 줍니다.

 

for 문이 완료 되면 전체 소요 시간과 전체 응답을 print 합니다.

 

그러면 아래와 같은 응답을 받습니다.

 

.......

 

Stream을 사용하지 않았을 때는 전체 결과 값을 받는데까지 2.86초 걸렸습니다.

 

그런데 이렇게 Stream을 사용하니까 응답의 시작부분을 0.35초 후부터 받기 시작해서 전체 메세지를 받는데까지 1.52초가 걸렸습니다.

 

이렇게 streaming 해서 받은 값들을 모두 합한 값은 위에 stream을 사용하지 않았을 때와 동일합니다.

 

Time comparison

쿡북의 예에서는 Davinci 모델을 사용해서 Stream을 사용하지 않은 경우는 7.32초가 걸렸고 사용한 경우는 7.25초가 걸렸습니다.

둘 다 거의 비슷하게 걸렸지만 streaming을 사용한 경우에는 0.16초가 지나면서부터 응답의 일부를 받기 시작해서 전체 응답을 받기까지 7.25초가 걸린 겁니다.

 

 

 

 

반응형


반응형

How to count tokens with tiktoken

오늘 다를 예제는 아래 CookBook 페이지에 있는 토큰 관련 에제 입니다.

https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.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

 

tiktoken 은 openAI 에서 제공하는 오픈 소스 토크나이저 입니다.

 

이 모듈은 문자열과 인코딩 방법을 전달하면 그 문자열을 토큰으로 나눠서 리스트에 담아서 반환합니다.

예를 들어 tiktoken is great! 이라는 문자열을 gpt2라는 인코딩을 사용하면 어떻게 토큰이 나누어 지는지 질문하면 아래처럼 response를 줍니다.

["t", "ik", "token", " is", " great", "!"]

이 문자열은 총 6개의 토큰으로 구성 돼 있습니다.

대개 1개의 단어가 1개의 토큰이 되지만 꼭 그렇지만은 않습니다.

그리고 인코딩 방법에 따라서 토큰을 나누는게 조금 다릅니다.

 

GPT-3 같은 모델은 텍스트를 토큰들의 집합으로 봅니다. 그렇기 때문에 문자열을 토큰으로 분할하게 되면 유용하게 사용할 수 있습니다. 

이 모델이 사용하기에 너무 많은 토큰의 갯수는 아닌지를 알아 볼 수도 있고 이 토큰 갯수에 따라서 API 요청에 대한 과금이 매겨지기 때문에 이 요청을 하면 돈이 얼마가 들지 미리 알 수 있습니다.

 

GPT-3의 모델들은 하나의 인코딩만을 사용하는 것이 아니고 모델별로 다른 인코딩을 사용하는 경우가 있습니다.

 

이 tiktoken은 OpenAI 모델들이 사용하는 아래 세가지 인코딩 방법을 제공합니다.

 

p50k_base는 gpt2 와 많은 부분 겹치는 인코딩 입니다. 코드가 아닌 어플리케이션인 경우 일반적으로 동일하게 토큰을 나눕니다.

 

Tokenizer libraries and languages

gpt2 인코딩은 다음과 같은 많은 프로그래밍 언어에서 사용할 수 있는 tokenizer가 제공 됩니다.

 

(OpenAI는 3rd party 라이브러리에 대해서는 그 성능과 리스크에 대한 보장을 하지 않습니다.)

 

p50k_base와 cl100k_base 인코딩의 경우 2023년 1월 현재 tiktoken에서만 tokenizer가 가능합니다.

How strings are typically tokenized

영어로 된 텍스트인 경우 토큰의 길이는 일반적으로 문자 하나이거나 단어 하나가 한개의 토큰이 됩니다. (e.g., "t" or " great")

다른 언어에서는 한문자보다 짧거나 한단어보다 길 수 있습니다.

공백은 일반적으로 단어의 시작으로 그룹화 됩니다. (e.g., " is" instead of "is " or " "+"is")

OpenAI Tokenizer를 사용해서 문자열이 어떻게 tokenized 되는지 빠르게 체크할 수 있습니다.

 

0. Install tiktoken

이 tiktoken을 사용하려면 먼저 이 모듈을 인스톨 해야 합니다.

pip install tiktoken

 

1. Import tiktoken

그러면 이 tiktoken을 import 할 수 있습니다.

import tiktoken

 

2. Load an encoding

tiktoken.get_encoding()을 사용해서 인코딩을 로딩 합니다. 이 때 인코딩 이름을 파라미터로 전달합니다.

처음 이 작업을 할 때는 인터넷에 연결 돼 있어야 합니다.

한번 로딩한 다음에는 인터넷이 연결 돼 있지 않아도 사용할 수 있습니다.

encoding = tiktoken.get_encoding("gpt2")

3. Turn text into tokens with encoding.encode()

.encode() 메소드는 문자열을 token integer들로 변환합니다.

 

 

위에서 tiktoken is great! 이 아래와 같은 토큰들로 나누어 진다고 했습니다.

["t", "ik", "token", " is", " great", "!"]

그렇다면 t는 83이고 ik 는 1134 이고 token 은 30001 ..... 마지막으로 ! 는 0 이 됩니다.

 

아래 함수는 토큰 갯수를 계산해서 반환해 주는 함수 입니다.

def num_tokens_from_string(string: str, encoding_name: str) -> int:
    """Returns the number of tokens in a text string."""
    encoding = tiktoken.get_encoding(encoding_name)
    num_tokens = len(encoding.encode(string))
    return num_tokens

함수 이름은 num_tokens_from_string() 이고 파라미터로는 String 과 인코딩 이름이 전달 됩니다.

그리고 반환 타입은 integer 입니다.

 

get_encoding()을 사용해서 사용할 encoding을 정의 합니다.

그리고 len() 함수를 사용해서 인코드해서 얻은 리스트의 아이템 갯수를 num_tokens 변수에 넣습니다.

이 아이템들이 각각의 토큰들 입니다.

 

이 함수를 사용해서 토큰의 갯수를 아래와 같이 얻을 수 있습니다.

 

4. Turn tokens into text with encoding.decode()

인코딩이 있으면 디코딩도 있겠죠.

.decode()를 사용하면 숫자로 된 토큰 정보를 문자로 바꿀 수 있습니다.

 

Warning : .decode()는 single tokens에 적용될 수 있지만 utf-8 boundaries가 아닌 경우 토큰의 손실이 발생 할 수 있습니다.

single tokens의경우 decode_single_token_bytes() 는 단일 정수 토큰을 그것이 나타내는 바이트로 안전하게 변환합니다. 

여기서 각 string 앞에 있는 b가 가리키는 것은 이 String들이 byte string들이라는 의미 입니다.

 

5. Comparing encodings

그렇다면 각 인코딩 별로 어떻게 입력값을 분할 해서 토큰으로 나누는지를 알아 보겠습니다.

 

def compare_encodings(example_string: str) -> None:
    """Prints a comparison of three string encodings."""
    # print the example string
    print(f'\nExample string: "{example_string}"')
    # for each encoding, print the # of tokens, the token integers, and the token bytes
    for encoding_name in ["gpt2", "p50k_base", "cl100k_base"]:
        encoding = tiktoken.get_encoding(encoding_name)
        token_integers = encoding.encode(example_string)
        num_tokens = len(token_integers)
        token_bytes = [encoding.decode_single_token_bytes(token) for token in token_integers]
        print()
        print(f"{encoding_name}: {num_tokens} tokens")
        print(f"token integers: {token_integers}")
        print(f"token bytes: {token_bytes}")

이 compare_encodings() 함수는 string을 입력 파라미터로 받습니다. return 값은 없고 그냥 이 함수 안에서 계산해서 그 정보를 print 해 줍니다.

 

첫번째 있는 print 문은 Example string : 입력 받은 문자열 을 출력합니다.

그 다음 for 문이 있는데 각 인코딩 별로 이 for 문안에 있는 작업을 해 줄 겁니다.

우선 get_encoding() 을 사용해서 사용할 encoding을 세팅해 줍니다.

그리고 encode()를 사용해서 토큰으로 나눠 줍니다. (결과값을 리스트 형식으로 반환 합니다.)

그리고 len()을 이용해서 이 리스트안에 있는 아이템 갯수들을 num_tokens에 담습니다.

그리고 decode_single_token_bytes()를 사용해서 각 바이트별 텍스트를 token_bytes에 담습니다.

그 다음 인코딩 이름과 토큰 리스트의 인티저 값 그리고 각 바이트 별 문자를 표시해 줍니다.

 

이렇게 한 단어인데 긴것을 넣어 봤습니다. 이것은 반체제주의라는 의미라고 합니다.

gpt2와 p50k_base는 5개의 토큰으로 나눴고 cl100k_base는 6개의 토큰으로 나눴습니다.

 

한번 OpenAI의 토크나이저 페이지에 가서 같은 문자열을 넣고 확인해 보겠습니다.

gpt2와 p50k_base 인코딩과 동일한 결과를 볼 수 있습니다.

이 OpenAI 토크나이저 페이지는 아래에 가시면 볼 수 있습니다.

 

https://platform.openai.com/tokenizer

 

OpenAI API

An API for accessing new AI models developed by OpenAI

platform.openai.com

그 다음 계산식도 넣어보고 일본어도 넣어 봤습니다.

 

그리고 한글도 넣어 봤습니다.

그리고 긴 영어 문장도 한번 넣어 봤구요.

 

 

이 예제들에서는 gpt2와 p50k_base가 동일하게 나왔습니다.

cl100k_base는 어떤 때는 다른 두개보다 더 많은 토큰으로 나누었고 또 어떤 때는 토큰수가 더 적을 때도 있었습니다.

 

인코딩 방법에 따라 이렇게 다른 결과들이 나옵니다.

 

반응형


반응형

지난 글까지 해서 openai cookbook의 embedding 코너는 모두 마쳤습니다.

OpenAI API 를 공부하면서 어떻게 하다가 CookBook 의 Embedding 부터 먼저 공부 했는데요.

이 CookBook은 원래 API usage부터 시작합니다.

 

오늘부터 이 CookBook 순서대로 공부해 볼까 합니다.

 

https://github.com/openai/openai-cookbook/blob/main/examples/How_to_handle_rate_limits.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

How to handle rate limits

OpenAI API를 사용하는데는 일일 제한량이 있습니다. 

API 호출이 이 제한량을 넘을 경우 API는 해당 Request에 대한 Response를 하지 않고 429: 'Too Many Requests 나 RateLimitError 메세지를 보냅니다.

 

이번 예제에서는 이 API Request 제한량을 초과해서 나타나는 오류를 방지하거나 그 오류를 처리하는데 대한 팁을 보여 줍니다.

 

이 제한량 초과 에러를 피하기 위해 병렬 요청 (Parallel Requests)를 조절하는 예제를 보려면 아래 내용을 참고 하세요.

 

https://github.com/openai/openai-cookbook/blob/2f5e350bbe66a418184899b0e12f182dbb46a156/examples/api_request_parallel_processor.py

 

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

Why rate limits exist

이 request 수 제한은 API 관리하는 일반적인 관행입니다. 이것을 관리하는 이유는 몇가지 있습니다.

* 첫번째로는 API 남용과 오용으로부터 시스템을 보호하는데 도움을 줍니다. 예를 들어 악의적인 의도를 가지고 API에 과부하를 일으키거나 서비스를 중단시키려는 의도로 API 요청을 다량 발생 시킬 수 있습니다. 이런 악의적인 공격으로 부터 OpenAI API 서비스를 보호하기 위해 Request 수 제한 정책이 필요합니다.

 

* 둘째로는 이 OpenAI API 자원을 다양한 사람이 공평하게 사용할 수 있도록 하는 목적이 있습니다. 한 개인이나 조직이 과도한 수의 요청을 하면 다른 소비자들의 API 서비스가 중단 될 수 있습니다. 단일 사용자가 만들 수 있는 요청 수를 제한 함으로서 이 API 서비스를 더 많은 사람들이 속도 저하 없이 API를 사용할 수 있는 기회를 더 보장할 수 있습니다.

 

* 세번째 마지막으로 이 요청량 제한은 OpenAI 가 인프라의 총 부하를 관리하는데 도움이 될 수 있습니다. API에 대한 요청이 급격하게 증가하면 서버에 부담을 주고 성능에 문제를 일으킬 수 있습니다. 이 요청량 베한을 설정함으로서 OpenAI는 모든 사용자에게 원활하고 일관된 경험을 유지하는데 도움을 줄 수 있습니다.

 

Default rate limits

2023년 1월 현재 요청량 제한은 아래와 같습니다.

참고로 1000개의 토큰은 한 페이지 분량의 텍스트 입니다.

 

Other rate limit resources

이 요청량 제한에 대해 좀 더 많은 정보를 얻고 싶으면 아래 자료들을 참조하세요.

Requesting a rate limit increase

제한된 요청량 이상으로 API 리소스를 사용하시려면 아래 요청서를 작성하세요.

Example rate limit error

API 요청들이 너무 빨리 보내지면 아래 요청량 제한 에러가 발생합니다. OpenAI 파이썬 라이브러리를 사용한다면 그 에러 메세지는 아래와 같을 겁니다.

아래 코드는 요청량 제한을 초과하는 한 예를 보여 줍니다.

 

import openai  # for making OpenAI API requests

# request a bunch of completions in a loop
for _ in range(100):
    openai.Completion.create(
        model="code-cushman-001",
        prompt="def magic_function():\n\t",
        max_tokens=10,
    )

 

저는 굳이 이 코드는 실행해 보지 않겠습니다.

코드는 그냥 간단합니다. max_tokens를 10으로 설정한 다음에 Completion.create() api를 100번 요청하는 for 문 입니다.

 

How to avoid rate limit errors

Retrying with exponential backoff

이 요청량 제한 오류를 방지하는 쉬운 방법 중 하나는 random exponential backoff로 요청을 자동적으로 재 시도 하는 겁니다.

exponential backoff로 retry를 한다는 의미는 요청량 제한 에러에 도달했을 때 잠시 쉬었다가 실패한 요청을 다시 요청한다는 겁니다. 만약 그 요청이 또다시 실패 한다면 잠시 쉬는 시간이 좀 더 늘어나고 그 다음에 요청하는 과정을 반복하는 겁니다. 이 과정은 요청이 성공할 때까지 이루어지게 할 수도 있고 특정 시도 횟수를 지정해서 그 횟수 만큼만 실행하게 할 수도 있습니다.

 

이 접근법은 다음과 같은 이점이 있습니다.

 

* 자동재시도는 crash나 데이터 손실 없이 요청량 제한 에러를 극복할 수 있다는 의미입니다.

* Exponential backoff는 첫번째 시도를 빠르게 시도할 수 있음을 의미하며 처음 몇번의 재시도가 실패할 경우 점점 더 재시도까지의 쉬는 시간이 점점 더 길어진다는 겁니다. 그렇기 때문에 초기에 성공하면 좀 더 빠른 시간안에 에러를 복구 할 수 있습니다.

* Random jitter를 추가하면 동시에 모든 hitting으로부터 재시도를 하는데 도움을 줍니다.

Note. 실패한 요청은 여러분의 per-minute limit에 포함 되므로 계속 재시도를 해도 작동하지 않을 수 있습니다.

 

아래에 이 방법을 이용하는 몇가지 방법을 소개합니다.

 

Example #1: Using the Tenacity library

Tenacity 는 파이썬으로 작성된 아파치 2.0 라이센스 범용 retrying 라이브러리 입니다. 이것을 이용하면 거의 모든 상황에 재시도 동작을 추가하는 작업을 단순화 할 수 있습니다.

 

요청에 exponential backoff를 추가하려면 tenacity.retry decorator를 사용하면 됩니다.

tenacity.wait_random_exponential  함수를 사용하여 random exponential backoff를 요청에 추가하는 방법을 아래 예제에서 보여 줍니다.

 

Note : Tenacity 라이브러리는 별도로 개발한 회사가 있고 그 회사가 배포한 라이브러리 입니다. OpenAI에서 그 안정성이나 보안을 보장하지는 않습니다.

 

import openai  # for OpenAI API calls
from tenacity import (
    retry,
    stop_after_attempt,
    wait_random_exponential,
)  # for exponential backoff


@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def completion_with_backoff(**kwargs):
    return openai.Completion.create(**kwargs)


completion_with_backoff(model="text-davinci-002", prompt="Once upon a time,")

@retry() 를 사용하며 wait과 stop을 지정합니다. 

재시도 사이 대기시간이 1초에서 60초 사이가 되고 재시도는 6번 시도하라는 겁니다.

이것은 그 아래 함수인 Completion_with_backoff() 함수에 적용 됩니다.

이 함수에서는 openai.Completion.create() api를 호출합니다.

 

그리고 그 함수 밖에서 이 completion_with_backoff()를 호출합니다.

 

Example #2: Using the backoff library

다른 라이브러리로는 backoff 가 있습니다.

Tenacity와 마찬가지로 Backoff 라이브러리는 third-party tool 입니다. OpenAI에서 그 안정성이나 보안성을 보장하지는 않습니다.

 

import backoff  # for exponential backoff
import openai  # for OpenAI API calls


@backoff.on_exception(backoff.expo, openai.error.RateLimitError)
def completions_with_backoff(**kwargs):
    return openai.Completion.create(**kwargs)


completions_with_backoff(model="text-davinci-002", prompt="Once upon a time,")

이 라이브러리는 @backoff.on_exception()이라고 지정합니다.

 

파라미터로는 backoff.expo와 에러메세지인 openai.error.RateLimitError를 전달합니다.

그 아래 함수와 이 함수 호출 부분은 위 예제와 동일합니다.

 

Example 3: Manual backoff implementation

이런 3rd-party 툴을 사용하지 않고 직접 backoff 로직을 구현할 수도 있습니다.

 

# imports
import random
import time

import openai

# define a retry decorator
def retry_with_exponential_backoff(
    func,
    initial_delay: float = 1,
    exponential_base: float = 2,
    jitter: bool = True,
    max_retries: int = 10,
    errors: tuple = (openai.error.RateLimitError,),
):
    """Retry a function with exponential backoff."""

    def wrapper(*args, **kwargs):
        # Initialize variables
        num_retries = 0
        delay = initial_delay

        # Loop until a successful response or max_retries is hit or an exception is raised
        while True:
            try:
                return func(*args, **kwargs)

            # Retry on specified errors
            except errors as e:
                # Increment retries
                num_retries += 1

                # Check if max retries has been reached
                if num_retries > max_retries:
                    raise Exception(
                        f"Maximum number of retries ({max_retries}) exceeded."
                    )

                # Increment the delay
                delay *= exponential_base * (1 + jitter * random.random())

                # Sleep for the delay
                time.sleep(delay)

            # Raise exceptions for any errors not specified
            except Exception as e:
                raise e

    return wrapper


@retry_with_exponential_backoff
def completions_with_backoff(**kwargs):
    return openai.Completion.create(**kwargs)


completions_with_backoff(model="text-davinci-002", prompt="Once upon a time,")

이 에제를 보면 위에서 설명했던 동작인 요청하고 error가 발생하면 재시도를 정해진 횟수만큼 하고 그 재시도 사이의 지연 시간은 점차 증가해 나가는 동작을 하도록 직접 wrapper() 함수 안에 구현을 해 놨습니다.

그리고 최대 재시도 횟수와 지연시간 관련 값들은 retry_with_exponential_backoff() 함수에서 세팅을 했습니다.

그리고 위에 설명한 wrapper() 함수는 이 retry_with_exponential_backoff() 함수에 속해 있습니다.

 

이렇게 설정을 해놓고 이것을 이용하는 방법은 @ decorator를 사용하는 겁니다.

https://builtin.com/software-engineering-perspectives/python-symbol

 

What Is the @ Symbol in Python and How Do I Use It?

What does @property do? How about A @ B? In this tutorial I’ll walk you through all the different ways you can use the @ symbol in Python.

builtin.com

https://dojang.io/mod/page/view.php?id=2427 

 

파이썬 코딩 도장: 42.1 데코레이터 만들기

Unit 42. 데코레이터 사용하기 파이썬은 데코레이터(decorator)라는 기능을 제공합니다. 데코레이터는 장식하다, 꾸미다라는 뜻의 decorate에 er(or)을 붙인 말인데 장식하는 도구 정도로 설명할 수 있습

dojang.io

@retry_with_exponential_backoff -> 이런식으로 사용합니다.

 

How to maximize throughput of batch processing given rate limits

만약 여러분의 애플리케이션이 고객의 실시간 요청을 처리하는 서비스를 제공한다면 backoff and retry는 요청량 제한 에러를 피하면서 latency를 minimize할 수 있는 아주 좋은 전략입니다.

 

그런데 이렇게 서비스 제공 시간이 중요한게 아니라 얼마나 많은 양을 처리하느냐가 더 중요한 애플리케이션이 있을 수 있습니다. 대량의 batch data를 처리하는 것이 더 중요한 경우이죠. 이런 경우 backoff and retry 말고 여러분이 사용할 수 있는 몇가지 다른 방법이 있습니다.

 

Proactively adding delay between requests

만약 여러분의 앱이 요청량 제한에 걸리고 back off 하고 재시도하고 하는 상황이 계속 반복 되면서 금방 요청량 제한에 걸린다면 남아 있는 시간은 제한량 초과 메세지만 받으면서 요청을 계속 할 수 있습니다.  (예를 들어 1분에 20회가 제한량인데 이 20회가 10초만에 도달하면 나머지 50초는 제한량 초과 메세지만 받게 될 것입니다.) 그러면 이 50초 동안의 요청은 낭비 되는 것이죠. 그것을 하기 위해 사용한 내 시스템의 리소스가 낭비 되는 것입니다. 

 

이 경우 잠재적 해결책이 될 수 있는 것은 여러분의 요청량 제한을 계산하고 그 reciprocal에 맞게 지연 시간을 만드는 것입니다.

reciprocal(예: 만약 여러분의 요청량 제한이 분당 20이라면 각 request마다 지연시간을 3~6초씩 추가 해 주는 것)

 

이렇게 하면 요청량 제한의 상한선에 도달하지 않고 낭비되는 요청도 발생 시키지 않는 상황에 가깝게 만들 수 있습니다.

 

Example of adding delay to a request

# imports
import time
import openai

# Define a function that adds a delay to a Completion API call
def delayed_completion(delay_in_seconds: float = 1, **kwargs):
    """Delay a completion by a specified amount of time."""

    # Sleep for the delay
    time.sleep(delay_in_seconds)

    # Call the Completion API and return the result
    return openai.Completion.create(**kwargs)


# Calculate the delay based on your rate limit
rate_limit_per_minute = 20
delay = 60.0 / rate_limit_per_minute

delayed_completion(
    delay_in_seconds=delay,
    model="text-davinci-002",
    prompt="Once upon a time,"
)

이 소스 코드는 위에 예시로 들었던 상황에 맞게 만든 겁니다.

요청량 제한이 분당 20이라면 delay는 60/20 즉 3초가 됩니다.

 

delay_completion() 함수 안데 time.sleep() 을 이 3초로 했기 때문에 이 함수를 호출할 때 마다 3초씩 기다렸다가 openai.Completion.create() api 를 사용하게 되는 겁니다.

 

그러면 이 1분이라는 시간에 할 수 있는 요청량을 최대한으로 사용할 수 있게 됩니다.

단 10초만에 요청량 제한에 걸려서 나머지 50초는 그냥 손 놓고 있는 상황을 피할 수 있게 되는 거죠.

 

Batching requests

OpenAI api에는 분당 요청량 제한 이외에 분당 요청 토큰에 대한 별도의 제한이 있습니다.

만약 여러분이 분당 요청량 제한에 도달했지만 분당 토큰에 여유가 있는 경우 각 요청에 여러 작업을 batch 함으로서 throughput(처리량)을 증가시킬 수 있습니다.

 

프롬프트들의 batch를 보내는 방법은 일반적인 API 콜과 동일하게 작동합니다.

단지 프롬프트 파라미터가 Single string이 아니라 String 의 List라는 것만 다릅니다.

 

Warning: Response 는 그 prompt 의 순서대로 반환을 하지 않을 수 있습니다. 그렇기 때문에 index 필드를 사용하여 그 response를 prompt 의 파라미터 순서와 맞게 일치시키는 작업을 반드시 해야 합니다.

 

Example without batching

import openai  # for making OpenAI API requests


num_stories = 10
prompt = "Once upon a time,"

# serial example, with one story completion per request
for _ in range(num_stories):
    response = openai.Completion.create(
        model="curie",
        prompt=prompt,
        max_tokens=20,
    )

    # print story
    print(prompt + response.choices[0].text)

 

Example with batching

import openai  # for making OpenAI API requests


num_stories = 10
prompts = ["Once upon a time,"] * num_stories

# batched example, with 10 stories completions per request
response = openai.Completion.create(
    model="curie",
    prompt=prompts,
    max_tokens=20,
)

# match completions to prompts by index
stories = [""] * len(prompts)
for choice in response.choices:
    stories[choice.index] = prompts[choice.index] + choice.text

# print stories
for story in stories:
    print(story)

위의 예제는 prompt 'once upon a time' 단일 파라미터로 하는 openai.Completion.create() 호출을 for 문을 통해서 10번 일으켰습니다.

즉 10번 요청을 한 것입니다.

 

그런데 두번째 예제는 prompt 자체를 Once upon a time이 10번 들어가 있는 리스트로 만든 다음 이 리스트를 파라미터로 전달해서 openai.Completion.create() api를 호출 했습니다.

즉 1번 요청한 것입니다.

 

이렇게 함으로서 요청량을 줄일 수 있게 되는 것입니다.

 

두번째 예제의 첫번째 for 문에서는 response를 request의 index 와 맞게 매치 시키는 작업을 하고 있습니다.

그래서 질문 + 응답 이런 식으로 stories[] 에 저장되게 했습니다.

 

이제 이 stories[]를 두번째 for 문처럼 print 하기만 하면 해당 질문과 그에 대한 답변 식으로 출력이 됩니다.

 

Example parallel processing script

대량의 API 요청을 parallel (병렬) 처리하기 위한 예제 스크립트는 아래에 있습니다.

https://github.com/openai/openai-cookbook/blob/2f5e350bbe66a418184899b0e12f182dbb46a156/examples/api_request_parallel_processor.py

 

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

 

이 스크립트에는 다음과 같은 몇가지 편리한 기능들이 결합돼 있습니다.

* 대용량 작업의 메모리 부족을 방지하기 위해 파일에서 request를 스트림하기

* 요청을 동시에 해서 throughput (처리량)을 극대화 하기

* 요청량 제한을 넘지 않기 위해 request 와 token 사용을 모두 제한하기

* 데이터 누락을 피하기 위해 실패한 요청을 재시도 하기

* 오류를 기록해서 그 요청에 대한 문제를 진단할 수 있게 하기

 

아 코드를 그대로 사용하시거나 필요에 맞게 수정해서 사용하시면 됩니다.

 

 

반응형


반응형

오늘 살펴 볼 쿡북 예제는 Embedding long input 입니다.

 

https://github.com/openai/openai-cookbook/blob/main/examples/Embedding_long_inputs.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

 

Open AI 에서는 각 모델별로 입력값 길이의 한계가 있습니다. 입력값의 크기는 토큰을 기준으로 정해 집니다.

 

토큰 관련 설명은 아래 페이지를 참고하세요.

 

https://github.com/openai/openai-cookbook/blob/2f5e350bbe66a418184899b0e12f182dbb46a156/examples/How_to_count_tokens_with_tiktoken.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

오늘 예제는 이 모델의 최대 context 길이보다 더 긴 텍스트는 어떻게 처리를 해야 하는지를 보여 줍니다.

 

1. Model context length

import openai
from tenacity import retry, wait_random_exponential, stop_after_attempt, retry_if_not_exception_type


EMBEDDING_MODEL = 'text-embedding-ada-002'
EMBEDDING_CTX_LENGTH = 8191
EMBEDDING_ENCODING = 'cl100k_base'

# let's make sure to not retry on an invalid request, because that is what we want to demonstrate
@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6), retry=retry_if_not_exception_type(openai.InvalidRequestError))
def get_embedding(text_or_tokens, model=EMBEDDING_MODEL):
    return openai.Embedding.create(input=text_or_tokens, model=model)["data"][0]["embedding"]

가장 먼저 openai를 import 하고 그 다음에 tenacity 모듈을 import 합니다.

tenacity는 런타임 중 오류가 발생해서 종료가 될 때 이 종료하는 것을 막고 다시 retry 하고자 할 때 사용하는 파이썬 모듈입니다.

이 모듈 중에서 retry, wait_random_exponential, Stop_after_attempt, retry_if_not_exception_type 만 import 합니다.

(이렇게 하면 @tenacity.retry 라고 하지 않고 간단하게 @retry 형식으로 사용할 수 있습니다.)

 

그 다음은 openai 모델을 설정하고 context length 를 8191 로 지정하고 encoding은 cl100k_base 로 합니다.

cl100k_base 는 tokenizer로 Max Input Token은 8191 로 정해져 있습니다. 2021년 9월에 발표 됐습니다.

 

자세한 내용은 OpenAI Guide의 Embeddings Overview 페이지를 참조하세요.

https://platform.openai.com/docs/guides/embeddings/what-are-embeddings

 

OpenAI API

An API for accessing new AI models developed by OpenAI

platform.openai.com

그 다음 은 @retry 구문이 나옵니다. retry 사이에 기다리는 시간은 1~20초이고 6번 까지 시도하고 exception type이 InvalidRequestError 가 아닌 경우만 retry를 시도합니다. InvalidRequest인 경우는 Retry를 해도 아무 소용 없으니까요.

 

그 다음은 get_embedding() 함수를 만들었습니다.

 

입력 파라미터로는 text_or_tokens와 모델 이름을 받습니다.

그리고 이 입력값에 대한 embedding 값을 전달받은 모델을 사용해서 openai.Embedding.create() api로 부터 받고 그 값을 return 합니다.

 

여기까지는 아직 아무 일을 한 것이 없습니다.

 

long_text = 'AGI ' * 5000
try:
    get_embedding(long_text)
except openai.InvalidRequestError as e:
    print(e)

여기서는 long_text를 설정해 주는데 AGI 라는 글자를 5천번 반복한 값을 long_text에 넣습니다.

 

그리고 get_embedding()을 호출하면서 이 long_text를 전달합니다.

이 때 이 호출 부분을 try로 감싸면서 openai의 InvalidRequestError 인 경우 이 에러 메세지를 프린트 하도록 합니다.

 

이렇게 하면 이 InvalidRequestError 메세지가 출력 됩니다.

이렇게 openai의 모델이 가지고 있는 Max Input Tokens를 넘어가는 입력 텍스트를 사용해야 할 경우 어떻게 할 수 있는지 아래 예제에서 설명합니다.

 

첫번째 방법은 입력 텍스트를 허용되는 최대 길이로 자르거나 두분째 방법은 입력 텍스트를 chunk하고 이 chunk들을 개별적으로 포함하는 방법 입니다.

 

여기서 한가지 염두에 둘 것은 api 사용 가격 입니다.

 

 

text-embedding-ada-002 모델은 토큰 1 천개 당 가격이 0.0004불 입니다. 즉 0.04 센트이죠.

그럼 위에서 생성한 5000 개의 AGI 인 long_text는 몇개의 토큰이 있는지 보겠습니다.

 

이 long_text 입력값은 총 1만 1개의 토큰을 가지고 있습니다.

0.0004 X (10001 / 1000)을 하면 0.004 불이 나옵니다. 

 

이 모델의 maximum 입력 값으로 요청을 해도 1센트도 안 나오네요.

 

참고로 저 위에 토큰 수를 계산하는 것은 아래 페이지에 가시면 볼 수 있습니다.

https://platform.openai.com/tokenizer

 

OpenAI API

An API for accessing new AI models developed by OpenAI

platform.openai.com

1. Truncating the input text

위에서 말한 첫번째 해결책 입니다. 모델에서 허용하는 최대 입력값 크기만큼 잘면 되죠.

여기서 크기는 토큰으로 정해지기 때문에 먼저 입력값이 몇개의 토큰으로 이루어져 있는지 알아봐야 합니다.

Tokenizer 페이지에서 그 작업을 했었죠. 10001 이었습니다. 그리고 위에서 사용한 모델은 8191 개의 토큰이 입력 허용 최대값이구요.

 

아래 방법이 이렇게 입력값을 토큰화 해서 자르는 부분입니다.

import tiktoken

def truncate_text_tokens(text, encoding_name=EMBEDDING_ENCODING, max_tokens=EMBEDDING_CTX_LENGTH):
    """Truncate a string to have `max_tokens` according to the given encoding."""
    encoding = tiktoken.get_encoding(encoding_name)
    return encoding.encode(text)[:max_tokens]

openai의 모듈중 하나인 tiktoken 모듈을 import 합니다.

https://github.com/openai/tiktoken

 

GitHub - openai/tiktoken

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

github.com

 

그리고 truncate_text_tokens() 함수를 만들었고 입력 파라미터는 text, encoding_name, 그리고 max_tokens을 받습니다.

이 함수는 주어진 인코딩으로 max_tokens를 갖도록 문자열을 자르는 일을 합니다.

일단 encoding을 정해 줍니다. tiktoken.get_encoding() 함수를 사용하고 인코딩 이름은 위에서 정한 값을 사용합니다.

EMBEDDING_ENCODING = 'cl100k_base'

 

참고로 tiktoken에서는 OpenAI 모델에서 사용하는 아래 3가지 인코딩을 지원한다고 하네요.

https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.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

 

그 다음 부분이 이 인코딩을 기준으로 max_tokens 만큼 truncate 해서 return 해 주는 부분 입니다.

return encoding.encode(text)[:max_tokens]

 

이렇게 해서 받은 값을 출력해 보았습니다.

 

AGI의 반복이라서 그런지 똑같은 숫자들만 반복 되네요.

Truncate은 뒷부분을 잘라내는 겁니다.

maximum limit 까지만 남기고 나머지는 잘라내는 거죠.

 

예제 처럼 이 truncated 값을 get_embedding 에 보내는 length를 출력해 보았습니다.

 

 

1536 이 나왔습니다. 임베딩 숫자의 차원이죠. len() 을 하지 않고 그대로 출력을 했다면 1536개의 정수를 가진 리스트가 출력 됐을 겁니다.

 

get_embedding() 함수가 에러 없이 작동 했다는 이야기네요.

 

 

여기까지가 입력값을 truncate 해서 제한량을 넘는 입력값을 처리하는 방법을 살펴 보았습니다.

초과 되는 부분을 잘라 내 버리는 거라서... 이게 과연 정확한 결과를 얻을 수 있을 지 의심되네요.

tiktoken의 encode()[:max_tokens] 를 사용해서 truncate 한 다음에 openai api로 request를 하면 됩니다.

 

여기까지 하고 과금된 액수를 확인 했습니다.

 

거의 과금이 안 됐네요.

맨 마지막 막대 그래프인데 1센트도 청구가 안 됐습니다.

그 전의 가장 긴 막대 그래프가 어제 (2/24) 에 사용했던 건데 0.01달러 (1센트) 청구 된 거거든요.

 

2. Chunking the input text

Truncate은 작동을 하게는 할 수 있지만 잠재적으로 중요성이 있을 수도 있는 부분을 잘라내 버리는 것은 뭔가 단점이라고 할 수 있을 것 같습니다.

Truncate 이외에 할 수 있는 방법은 Chunk (뭉태기)로 나누는 겁니다.

큰 입력값을 여러 뭉태기로 나누고 그것들을 개별적으로 request해서 response를 받는 것이다.

그렇지 않으면 이 Chunk embedding들을 평균화 해서 request한 다음 response 할 수 있습니다.

 

이 예제에서는 큰 입력값을 chunk로 나누어 주는 모듈을 사용할 겁니다.

이 모듈은 아래 페이지에서 보실 수 있습니다.

https://docs.python.org/3/library/itertools.html#itertools-recipes

 

itertools — Functions creating iterators for efficient looping

This module implements a number of iterator building blocks inspired by constructs from APL, Haskell, and SML. Each has been recast in a form suitable for Python. The module standardizes a core set...

docs.python.org

 

방법은 아래와 같이 시작합니다.

from itertools import islice

def batched(iterable, n):
    """Batch data into tuples of length n. The last batch may be shorter."""
    # batched('ABCDEFG', 3) --> ABC DEF G
    if n < 1:
        raise ValueError('n must be at least one')
    it = iter(iterable)
    while (batch := tuple(islice(it, n))):
        yield batch

우선 itertools 의 islice 함수를 import 합니다.

그리고 batched() 함수를 만들고 입력값으로는 iterable과 n을 받습니다.

 

그 다음 입력 받은 데이터를 길이가 n인 tuple로 batch 처리 합니다. 마지막 batch는 길이가 n 보다 작을 수 있습니다.

 

만약 길이 n이 1보다 작으면 에러를 발생 시킵니다.

 

raise는 에러를 발생시키는 파이썬 명령어 입니다. 

이것을 사용하면 정해진 에러를 발생하고 프로그램을 멈춥니다.

 

https://www.w3schools.com/python/ref_keyword_raise.asp

 

Python raise Keyword

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

에러의 종류는 아래 페이지에서 보실 수 있습니다.

 

https://docs.python.org/3/library/exceptions.html#ValueError

 

Built-in Exceptions

In Python, all exceptions must be instances of a class that derives from BaseException. In a try statement with an except clause that mentions a particular class, that clause also handles any excep...

docs.python.org

입력값이 1보다 작지 않으면 입력받은 iterable을 iter() 로 처리합니다.

iter()은 iterator 객체를 생성하는 파이썬 함수 입니다.

https://www.w3schools.com/python/ref_func_iter.asp

 

Python iter() Function

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

그 다음은 while문을 돌립니다.

islice() 는 아래와 같이 작동 합니다.

islice(it,n) 은 입력받은 it 값을 n 만큼씩 자르는 겁니다. 이 값을 tuple 형식으로 담습니다.

tuple은 아래 설명을 참조하세요. (여러 개의 아이템을 하나의 변수에 담을 때 tuple을 사용할 수 있습니다.)

 

https://www.w3schools.com/python/python_tuples.asp

 

Python Tuples

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

그리고 while문 안에 있는 := 는 2019년 10월에 새로 나온 파이썬 신택스로 좀 더 큰 expression의 일 부분으로 value를 변수에 할당하는 일을 합니다. 그리고 return까지 합니다.  별명은 바다코끼리 연산자 입니다.

 

https://docs.python.org/3/whatsnew/3.8.html

 

What’s New In Python 3.8

Editor, Raymond Hettinger,. This article explains the new features in Python 3.8, compared to 3.7. Python 3.8 was released on October 14, 2019. For full details, see the changelog. Summary – Releas...

docs.python.org

간단히 말해서 할당과 반환을 동시에 하는 연산자 입니다.

https://bio-info.tistory.com/120

 

[Python] 유용한 새로운 연산자! 바다코끼리 연산자 := (walrus operator)

Python 3.8부터 바다코끼리 연산자 (:=)가 도입되었습니다. 저도 최근에 알게 된, 매우 생소한 연산자입니다. 간단히 말해, 할당과 반환을 동시에 하는 연산자입니다. 개념과 목적, 예시를 알아보겠

bio-info.tistory.com

다음으로 나오는 yield 키워드는 결과 값을 return 하는 키워드 입니다.

yield는 generator를 반환합니다.

자세한 사항은 아래 글을 참조하세요.

https://www.daleseo.com/python-yield/

 

파이썬의 yield 키워드와 제너레이터(generator)

Engineering Blog by Dale Seo

www.daleseo.com

https://realpython.com/introduction-to-python-generators/

 

How to Use Generators and yield in Python – Real Python

In this step-by-step tutorial, you'll learn about generators and yielding in Python. You'll create generator functions and generator expressions using multiple Python yield statements. You'll also learn how to build data pipelines that take advantage of th

realpython.com

그래서 맨 마지막에 yield batch의 의미는 입력받은 context인 it를 입력받은 정수 n 의 크기만큼 나눈 결과 값인 batch를 반환하라는 겁니다.

 

즉 큰 데이터를 특정 크기 만큼의 뭉태기로 나눈 값이죠.

 

이제 이 값을 토큰으로 인코딩 한 다음 chunk로 나누는 일을 할 겁니다.

 

def chunked_tokens(text, encoding_name, chunk_length):
    encoding = tiktoken.get_encoding(encoding_name)
    tokens = encoding.encode(text)
    chunks_iterator = batched(tokens, chunk_length)
    yield from chunks_iterator

이 일을 하는 함수 이름은 chunked_tokens() 이고 입력값은 3가지를 받습니다.

 

get_encoding() 으로 encoding을 정하고

encode()를 사용해서 tokens를 할당 합니다. (이부분은 위에서도 다룬 부분 입니다.)

그 다음에 위에 만들었던 batched() 함수를 사용해서 여러 뭉태기로 나눈 값을 받아서 chunks_iterator에 할당합니다.

그리고 이 chunks_iterator를 yield를 사용해서 반환합니다.

 

이제 마지막 마무리 작업을 하는 함수를 만들겠습니다.

 

import numpy as np


def len_safe_get_embedding(text, model=EMBEDDING_MODEL, max_tokens=EMBEDDING_CTX_LENGTH, encoding_name=EMBEDDING_ENCODING, average=True):
    chunk_embeddings = []
    chunk_lens = []
    for chunk in chunked_tokens(text, encoding_name=encoding_name, chunk_length=max_tokens):
        chunk_embeddings.append(get_embedding(chunk, model=model))
        chunk_lens.append(len(chunk))

    if average:
        chunk_embeddings = np.average(chunk_embeddings, axis=0, weights=chunk_lens)
        chunk_embeddings = chunk_embeddings / np.linalg.norm(chunk_embeddings)  # normalizes length to 1
        chunk_embeddings = chunk_embeddings.tolist()
    return chunk_embeddings

numpy 모듈을 import 합니다.

 

그리고 len_safe_get_embedding() 이라는 함수를 만듭니다. 

입력값은 text와 모델, max_tokens,와 인코딩 이름 입니다.

위에서 만든 함수들에서 다 사용되는 값들입니다.

첫번째로 한 일은 chunk_embeddings와 chunk_lens라는 리스트를 초기화 합니다. 

(참고로 파이썬에서 [] 는 배열-리스트-이고 () 는 튜플이고 {} 는 딕셔너리 입니다.)

https://gostart.tistory.com/58

 

[Python] Python 에서 (), [], {} 의 용도

Python 에서 [], (), {} 용도 배열[Array] 튜플(Tuple) 딕셔너리{Dictionary} 선언 arr = [] tup = () dic = {} 초기화 arr = [1, 2, 3, 4] tup = (1, 2, 3, 4) dic = {"january":1, "February": 2, "March":3 } 불러오기 arr[0] tup[0] dic["March"]

gostart.tistory.com

그 다음 for 루프에서는 위에 만들었던 chunked_tokens() 함수에서 return 받은 배열 수 만큼 루프를 돕니다.

그 배열(리스트)의 각 아이템별로 chunk_embeddings.append()를 해서 위에서 초기화 했던 chunk_embeddings 배열에 하나 하나 넣습니다. 

그 넣는 값들이 get_embedding() 해서 openai api인 openai.Embedding.create()에서 얻은 각 아이템별 임베딩 값입니다.

 

이제 chunk_embeddings에는 openai로부터 받은 임베딩 값이 있습니다.

그리고 chunk_lens 배열에도 각 chunk의 length들을 배열로 넣구요.

 

그 다음 for 문이 끝나면 if 문이 나옵니다.

if average: 는 위에 average= true라고 정의 돼 있으므로 무조건 실행 됩니다.

 

if 문 안을 보면 처음에 위에서 받은 chunk_embeddings 배열의 즉 각 아이템별 임베딩 값의 average 값을 구합니다.

다른 파라미터인 axis와 weights는 아래 글을 보세요.

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

 

numpy.average — NumPy v1.24 Manual

Return the average along the specified axis. When returned is True, return a tuple with the average as the first element and the sum of the weights as the second element. sum_of_weights is of the same type as retval. The result dtype follows a genereal pat

numpy.org

 

이렇게 평균 낸 값을 normalize 합니다. 

https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html

 

numpy.linalg.norm — NumPy v1.24 Manual

[1] G. H. Golub and C. F. Van Loan, Matrix Computations, Baltimore, MD, Johns Hopkins University Press, 1985, pg. 15

numpy.org

그리고 마지막으로 이 chunk_embeddings 값을 리스트 형식으로 convert 해준 후 return 합니다.

 

여러개 뭉테기로 나눠서 받은 임베딩 값을 평균 내서 하나로 만들고 또 normalize 하고 이것을 리스트로 만드는 것 까지 했습니다.

 

average_embedding_vector = len_safe_get_embedding(long_text, average=True)
chunks_embedding_vectors = len_safe_get_embedding(long_text, average=False)

print(average_embedding_vector)
print(chunks_embedding_vektor)

 

자 이제 위에서 만든 long_text에 대한 임베딩 값을 chunk로 나누는 방법으로 받습니다.

average_embedding_vector는 average를 True로 해서 그 평균 값을 받습니다.

그리고 chunk_embedding_vectors는 average를 하지 않은 값을 보여 줍니다.

 

average_embedding_vector는 아래와 같습니다.

 

 

1536개의 정수가 있는 임베딩 값입니다.

 

그 다음 average를 하지 않은 chunks_embedding_vectors입니다.

 

똑 같은 것 같지만 이것은 평균을 내기 전의 값이기 때문에 여러개의 임베딩 값이 있습니다.

Notepad ++ 에서 , 의 숫자를 한번 검색해 봤습니다.

 

 

average 는 1535 개여서 총 숫자가 1536 인 1개의 임베딩 값임을 알 수 있고

두번째 average를 하지 않은 것은 콤마 숫자가 3071개여서 여기서는 2개의 chunk 가 있었다는 것을 알 수 있습니다.

 

예제에서 처럼 length를 출력 해 봤습니다.

 

 

방금전 살펴 본 대로 설명이 나오네요.

average=True인 경우는 한개의 1536 dimension (차원) 인 임베딩이 결과 값이고

average=False인 경우는 2개의 임베딩 벡터가 있고 그것은 chunk가 2개였기 때문이라고 되어 있습니다.

 

Summary 

 

오늘 배운 내용은 입력값이 각 모델이 허용하는 입력값 크기보다 클 경우 사용할 수 있는 방법을 알아 봤습니다.

첫번째로는 허용하는 값만큼만 취하고 나머지는 truncate 해 버리는 방법입니다.

두번째는 허용하는 크기 만큼의 chunk로 나눠서 각 chunk 별로 embedding 값을 따로 받는 것입니다.

그리고 이 각각의 임베딩 값들을 numpy 의 average() 함수를 이용해서 평균을 낼 수 있습니다.

 

오늘은 입력값의 토큰 갯수가 많은 관계로 과금이 조금 많이 됐습니다. 3센트 (50원)

 

반응형


반응형

지난 글에서는 임베딩 값을 2D로 시각화 하는 예제를 분석해 봤습니다.

오늘은 임베딩 값을 3D로 시각화 하는 예제를 분석해 보겠습니다.

openai-cookbook/Visualizing_embeddings_in_3D.ipynb at main · openai/openai-cookbook · GitHub

 

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

 

Visualizing embeddings in 3D

지난 2D 예제에서는 1536 차원의 데이터를 2차원으로 만들어서 2D로 visualization을 했습니다.

오늘 예제에서는 이것을 3차원으로 만들어서 3D로 visualization을 하는 겁니다.

 

이것을 위해서  PCA를 사용합니다. (2D에서는 t-SJE를 이용했습니다.)

 

이 예제에서 사용하는 dbpedia_samples.jsonl은 이곳에서 구할 수 있습니다.

openai-cookbook/dbpedia_samples.jsonl at main · openai/openai-cookbook · GitHub

 

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

이 데이터의 첫 두줄은 아래와 같습니다. (총 200 줄이 있습니다.)

 

{"text": " Morada Limited is a textile company based in Altham Lancashire. Morada specializes in curtains.", "category": "Company"}
{"text": " The Armenian Mirror-Spectator is a newspaper published by the Baikar Association in Watertown Massachusetts.", "category": "WrittenWork"}

 

text 와 category 두 항목이 있습니다. 

text는 한 문장이 있고 category에는 말 그대로 카테고리들이 있습니다.

어떤 카테고리들이 있고 각 카테고리는 몇개씩 있는지 알아 보겠습니다.

 

여기서는 pandas 모듈을 사용합니다.

read_json()을 사용해서 데이터세트를 읽어 옵니다. (samples)

그리고 이 데이터세트의 category를 수집해서 unique 한 리스트를 만든 후 정렬을 합니다. (categories)

 

print("Categories of DBpedia samples:", samples["category"].value_counts())

이것을 위 방식으로 프린트를 하면 이런 결과를 얻습니다.

카테고리는 총 14개가 있고 그 중에 가장 많이 있는 카테고리는 Artist 로 21번 나옵니다.

그 외에 다른 카테고리들과 각 카테고리별 갯수를 표시합니다.

 

그리고 samples.head() 를 하면 아래 결과를 얻습니다.

 

 

text와 category를 표 형식으로 보여 줍니다. head()를 사용하면 디폴트로 상위 5줄을 print 합니다.

 

그 다음은 openai api를 이용해서 각 text별로 임베딩 값을 받아 옵니다.

 

import openai
from openai.embeddings_utils import get_embeddings

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

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

# NOTE: The following code will send a query of batch size 200 to /embeddings
matrix = get_embeddings(samples["text"].to_list(), engine="text-embedding-ada-002")

embeddings_utils 의 get_embeddings를 사용해서 각 text 별로 openai로 부터 임베딩 값을 받아 옵니다.

Note : openai api는 유료입니다. 200개의 데이터에 대한 임베딩값을 받아오는데 대한 과금이 붙습니다.

저는 이 소스코드를 테스트 하는 과정에서 1센트가 과금이 되었습니다.

 

2. Reduce the embedding dimensionality

from sklearn.decomposition import PCA
pca = PCA(n_components=3)
vis_dims = pca.fit_transform(matrix)
samples["embed_vis"] = vis_dims.tolist()

이 부분에서 각 아이템별 임베딩을 PCA를 이용해서 3차원으로 만든다.

sklearn.decomposition.PCA — scikit-learn 1.2.1 documentation

 

sklearn.decomposition.PCA

Examples using sklearn.decomposition.PCA: A demo of K-Means clustering on the handwritten digits data A demo of K-Means clustering on the handwritten digits data Principal Component Regression vs P...

scikit-learn.org

PCA는 Principal Component Analysis (주성분 분석) 의 약자이다.

고차원의 데이터 (high-dimensional data)를 저차원으로 축소하여 새로운 데이터를 생성하는 방법이다.

PCA를 사용하면 N차원 데이터의 정보를 최대한 보존하면서 저차원의 데이터로 표현할 수 있다.

 

<PCA의 원리 요약>

1. 수학적인 방법으로 원래 데이터의 주성분(Principal Component)을 찾는다.
주성분은 원래 데이터의 차원의 수만큼 나온다.

2. 축소하려는 차원의 수만큼 주성분을 사용하여 새로운 데이터를 만든다.

 

PCA(주성분분석)의 원리와 응용 (tistory.com)

 

PCA(주성분분석)의 원리와 응용

1. PCA(Principal Component Analysis, 주성분분석)의 정의 PCA는 고차원의 데이터(high-dimensional data)를 저차원으로 축소(Reduction)하여 새로운 데이터를 생성하는 방법이다. PCA를 사용하면 N차원 데이터의 정

ds-dongjin.tistory.com

 

이 부분에서 PCA를 이용해 3차원으로 만든 데이터를 vis_dims에 담습니다.

 

여기까지 만든 데이터를 출력해 보면 아래와 같습니다.

openai api로 부터 받은 임베딩 값 (1536 차원)을 3차원으로 축소 시킨 값입니다.

이 값들을 리스트 타입으로 만들어서 해당 데이터에 embed_vis 컬럼에 넣는 것이 그 다음 줄에서 하는 작업입니다.

 

이제 각 text들의 임베딩 값을 3차원으로 축소 했으니 3D 그래픽으로 표현 할 수 있습니다.

3. Plot the embeddings of lower dimensionality

여기서 제 경우에는 ipympl 모듈이 없다는 메세지가 나와서 이 모듈을 설치 해야 했습니다.

이 모듈은 jupyter Lab에서 matplotlib 모듈을 사용할 수 있도록 하는 모듈입니다.

ipympl — ipympl (matplotlib.org)

 

ipympl — ipympl

Toggle in-page Table of Contents

matplotlib.org

 

이제 matplotlib를 JupyterLab에서 사용할 수 있습니다.

이 matplotlib의 pyplot은 지난 예제에서도 산점도 그래프를 그릴때 사용했었습니다. (scatter)

 

%matplotlib widget
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure(figsize=(10, 5))
ax = fig.add_subplot(projection='3d')
cmap = plt.get_cmap("tab20")

# Plot each sample category individually such that we can set label name.
for i, cat in enumerate(categories):
    sub_matrix = np.array(samples[samples["category"] == cat]["embed_vis"].to_list())
    x=sub_matrix[:, 0]
    y=sub_matrix[:, 1]
    z=sub_matrix[:, 2]
    colors = [cmap(i/len(categories))] * len(sub_matrix)
    ax.scatter(x, y, zs=z, zdir='z', c=colors, label=cat)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.legend(bbox_to_anchor=(1.1, 1))

 

pyplot의 figure() 함수는 이제 모양을 시작하겠다는 겁니다.

여기서는 figsize라는 파라미터를 사용했는데 Width, height 를 인치로 나타내는 겁니다.

그외 다른 파라미터들도 많은데 자세한 내용은 아래 페이지를 참조하세요.

matplotlib.pyplot.figure — Matplotlib 3.7.0 documentation

 

matplotlib.pyplot.figure — Matplotlib 3.7.0 documentation

The layout mechanism for positioning of plot elements to avoid overlapping Axes decorations (labels, ticks, etc). Note that layout managers can measurably slow down figure display. Defaults to None (but see the documentation of the Figure constructor regar

matplotlib.org

 

그 다음 나오는 add_subplot은 해당 figure에 Axes를 추가하는 것입니다.

matplotlib.figure — Matplotlib 3.7.0 documentation

 

[EDA Practice] Subplot 그리기 (tistory.com)

 

[EDA Practice] Subplot 그리기

이전 포스팅에서 matplotlib이나 seaborn을 통해서 그래프를 생성하면 자동으로 AxesSubplot 객체가 생성되었다. AxesSubplot은 Figure 객체에 포함된 객체이지만, 일반적으로는 하나밖에 생성이 안된다. 그

insighted-h.tistory.com

여기서는 projection 파라미터를 사용해서 3D 그래프로 그리도록 합니다.

 

그 다음 get_cmap()은 원하는 colormap의 타입을 정할 때 사용합니다. 여기서는 tab20 을 선택했습니다.

 

matplotlib.pyplot.get_cmap — Matplotlib 3.7.0 documentation

 

matplotlib.pyplot.get_cmap — Matplotlib 3.7.0 documentation

If a Colormap instance, it will be returned. Otherwise, the name of a colormap known to Matplotlib, which will be resampled by lut. The default, None, means rcParams["image.cmap"] (default: 'viridis').

matplotlib.org

이 예제에서 사용한 tab20은 아래와 같이 설명 돼 있습니다.

 

Choosing Colormaps in Matplotlib — Matplotlib 3.7.0 documentation

 

Choosing Colormaps in Matplotlib — Matplotlib 3.7.0 documentation

Note Click here to download the full example code Choosing Colormaps in Matplotlib Matplotlib has a number of built-in colormaps accessible via matplotlib.colormaps. There are also external libraries that have many extra colormaps, which can be viewed in t

matplotlib.org

 

다음에 나오는 for 문은 레이블 이름을 설정할 수 있도록 각 샘플 범주를 개별적으로 플로팅 합니다.

 

3차원을 위한 x,y,z에 해당 값과 color를 배정하고 scatter를 이용해서 3차원 좌표 안에 점을 찍습니다.

matplotlib.pyplot.legend — Matplotlib 3.7.0 documentation

 

legend() 함수는 정해진 legend를 어디에 위치할지 정하는 겁니다.

이 예제에서는 bbox_to_ancho,r 파라미터를 사용했는데 이 파라미터도 위 링크에 가시면 자세하기 볼 수 있습니다.

legend를 어디에 위치시키느냐를 결정하는 겁니다.

 

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

참고로 legend는 오른쪽에 있는 범례 (Album, Animal ....) 입니다.

 

이 부분을 아래와 같이 고쳐 보겠습니다.

ax.legend(loc = 'lower right')

 

그러면 이 범례 부분의 위치가 바뀌었습니다.

이렇게 bbox_to_anchor는 그래프 밖에 범례를 위치시키고 싶을 때 사용합니다.

 

이 Openai api CookBook의 예제를 실행하면 아래와 같은 결과를 얻습니다.

 

각 카테고리 별로 다른 색으로 표시된 점들이 3차원 그래프 안에 표시 돼 있습니다.

이러면 어떤 카테고리에 있는 데이터들이 어느 위치에 분포해 있는지 알 수 있게 됩니다.

 

오늘 예제는 openai api로 부터 받은 리스트의 아이템별 임베딩값(1536 dimention) 을 PCA를 이용해서 3 차원 (3 dimention)으로 바꾸는 작업을 1차로 했습니다.

그 다음 matplotlib.pyplot 모듈을 이용해서 이 3차원 데이터들을 3차원 그래픽으로 표현을 했습니다.

 

 

반응형


반응형

오늘 다룰 예제는 Visualizing the dmbeddings in 2D 입니다.

 

openai-cookbook/Visualizing_embeddings_in_2D.ipynb at main · openai/openai-cookbook · GitHub

 

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

 

여기서는 t-SNE를 사용해서 임베딩의 dimensionality (차원)dmf 1536에서 2로 줄일겁니다. 일단 임베딩이 이렇게 줄어들면 그것을 2D scattter plot (2D 산점도 플롯)을 할 수 있게 됩니다. 

오늘 쓸 데이터도 fine_food_reviews_with_embeddings_1k.csv 입니다.

아래 글에서 이 데이터를 다운 받을 수 있는 방법을 설명했습니다.

IT 기술 따라잡기 :: Openai cookbook - Embeddings - Text comparison examples - Semantic text search using embeddings (tistory.com)

 

Openai cookbook - Embeddings - Text comparison examples - Semantic text search using embeddings

오늘은 openai cookbook 에 있는 Embeddings 부문의 Text comparison examples 에 있는 Semantic_text_search_using_embeddings.ipynb 예제를 살펴 보겠습니다. 우선 이 예제를 살펴 보기 전에 준비해야 할 사항들이 몇가지

coronasdk.tistory.com

이 데이터를 만드는 파이썬 코드는 아래에 있습니다.

 

openai-cookbook/Obtain_dataset.ipynb at 2f5e350bbe66a418184899b0e12f182dbb46a156 · openai/openai-cookbook · GitHub

 

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

 

1. Reduce dimensionality

t-SNE decomposition (분해)를 사용해서 dimensionality를 2차원으로 줄입니다.

 

import pandas as pd
from sklearn.manifold import TSNE
import numpy as np

# Load the embeddings
datafile_path = "data/fine_food_reviews_with_embeddings_1k.csv"
df = pd.read_csv(datafile_path)

# Convert to a list of lists of floats
matrix = np.array(df.embedding.apply(eval).to_list())

# Create a t-SNE model and transform the data
tsne = TSNE(n_components=2, perplexity=15, random_state=42, init='random', learning_rate=200)
vis_dims = tsne.fit_transform(matrix)
vis_dims.shape

모듈은 pandas, numpy 그리고 sklearn.manifold의 TSNE를 사용합니다.

모두 이전 글에서 배운 모듈들 입니다.

 

판다스의 read_csv() 함수를 사용해서 csv 데이터 파일을 읽습니다.

 

그 다음 numpy 의 array()를 사용해서 csv 파일의 embedding 컬럼에 있는 값들을 리스트 형식으로 변환합니다.

 

그리고 TSNE()를 사용해서 계산해 줍니다.

이 함수는 고차원 데이터를 시각화 해 주는 것입니다.

sklearn.manifold.TSNE — scikit-learn 1.2.1 documentation

 

sklearn.manifold.TSNE

Examples using sklearn.manifold.TSNE: Comparison of Manifold Learning methods Comparison of Manifold Learning methods Manifold Learning methods on a severed sphere Manifold Learning methods on a se...

scikit-learn.org

fit_transform()은 전달받은 파라미터를 embedded space로 fit 해 주고 transformed output으로 return 합니다.

 

 

그 다음 shape으로 이 데이터의 차원이 어떤지 봅니다.

이것을 실행하면 아래와 같은 값을 얻을 수 있습니다.

 

이 csv 파일의 embedding 컬럼에는 1000개의 행이 있고 그것은 2차원으로 돼 있다는 의미 입니다.

이 작업은 TSNE()에서 2차원으로 줄이는 작업을 한 것입니다.

파라미터 중 n_component=2 라고 돼 있어서 그런 겁니다.

perplexity 함수는 가장 가까운 neighbor들의 수와 관계가 있는 manifold 학습 알고리즘 입니다.

더 큰 데이터세트는는 더 큰 perplexity를 요구합니다. 5에서 50사이를 선택합니다.

random_state는 난수 생성과 관련이 있습니다.

그 다음 init은 initialization을 의미하는 것으로 임베딩을 초기화 하는 겁니다. 이 임베딩을 random 이라고 초기화 합니다.

그리고 learning_rate는 그 숫자가 너무 높을 경우 데이터의 neighbor 와의 거리를 너무 가깝게 설정하게 돼 공처럼 분포할 수 있고 너무 낮으면 빽뺵한 구름처럼 보일 수 있습니다.  

이 외에도 TSNE()에서 사용할 수 있는 다른 파라미터들이 많이 있습니다. 

이곳에서 확인하세요.

sklearn.manifold.TSNE — scikit-learn 1.2.1 documentation

 

sklearn.manifold.TSNE

Examples using sklearn.manifold.TSNE: Comparison of Manifold Learning methods Comparison of Manifold Learning methods Manifold Learning methods on a severed sphere Manifold Learning methods on a se...

scikit-learn.org

하여간 이 TSNE() 함수에서 2차원으로 줄인 겁니다.

 

TSNE() 함수를 이용하기 전의 matrix의 값은 아래와 같습니다.

 

이 것을 TSNE()로 처리를 하면 이렇게 됩니다.

 

이 값을 shape을 이용해서 리스트의 크기와 차원을 표시하면 위에 처럼 1000,2 라고 나옵니다.

 

2. Plotting the embeddings

위에서 처럼 2차원으로 데이터를 정리하면 2D 산점도 분포도를 그릴 수 있다고 했습니다.

아래에서는 그것을 그리기 전에 알아보기 쉽도록 각 review에 대한 색을 지정해서 알아보기 쉽도록 합니다.

이 색은 별점 점수와 ranging 데이터를 기반으로 빨간색에서 녹색에 걸쳐 표현됩니다.

 

import matplotlib.pyplot as plt
import matplotlib
import numpy as np

colors = ["red", "darkorange", "gold", "turquoise", "darkgreen"]
x = [x for x,y in vis_dims]
y = [y for x,y in vis_dims]
color_indices = df.Score.values - 1

colormap = matplotlib.colors.ListedColormap(colors)
plt.scatter(x, y, c=color_indices, cmap=colormap, alpha=0.3)
for score in [0,1,2,3,4]:
    avg_x = np.array(x)[df.Score-1==score].mean()
    avg_y = np.array(y)[df.Score-1==score].mean()
    color = colors[score]
    plt.scatter(avg_x, avg_y, marker='x', color=color, s=100)

plt.title("Amazon ratings visualized in language using t-SNE")

matplotlib 모듈도 visualization 관련 모듈입니다.

이 모듈은 numPy 라이브러리와 같이 많이 쓰입니다.

 

Matplotlib — Visualization with Python

 

Matplotlib — Visualization with Python

seaborn seaborn is a high level interface for drawing statistical graphics with Matplotlib. It aims to make visualization a central part of exploring and understanding complex datasets. statistical data visualization Cartopy Cartopy is a Python package des

matplotlib.org

 

[CCTV] 5.matplotlib기초 (tistory.com)

 

[CCTV] 5.matplotlib기초

서울시 CCTV 분석하기 프로젝트 5. matplotlib기초 matplotlib란? 파이썬의 대표 시각화 도구 Matplotlib는 Python 프로그래밍 언어 및 수학적 확장 NumPy 라이브러리를 활용한 플로팅 라이브러리이다. Tkinter ,

ruby-jieun.tistory.com

필요한 모듈들을 import 한 후에 colors 라는 배열에 5가지 색을 지정했습니다.

그리고 위에서 만들었던 vis_dims 데이터의 값을 x 와 y 변수에 넣습니다.

그러면 2차원으로 축소시킨 임베딩 값의 x는 x 변수에 y는 y 변수에 따로 담깁니다.

 

그리고 데이터 파일의 Score 컬럼에 있는 값에서 1을 뺀 숫자를 color_indices에 넣습니다.

 

다음줄에 있는 matplotlib.colors.ListedColormap() 은 칼라 리스트로부터 Colormap object를 생성하는 함수입니다.

matplotlib.colors.ListedColormap — Matplotlib 3.7.0 documentation

 

matplotlib.colors.ListedColormap — Matplotlib 3.7.0 documentation

Number of entries in the map. The default is None, in which case there is one colormap entry for each element in the list of colors. If the list will be truncated at N. If the list will be extended by repetition.

matplotlib.org

5.차원 축소를 사용한 데이터 압축, 머신러닝교과서, python (tistory.com)

 

5.차원 축소를 사용한 데이터 압축, 머신러닝교과서, python

* 본 포스팅은 머신러닝교과서를 참조하여 작성되었습니다. 5.1 주성분 분석을 통한 비지도 차원 축소 특성 선택 vs 특성 추출 - 원본 특성을 유지한다면 특성 선택 - 새로운 특성 공간으로 데이터

justgwon.tistory.com

다음에 나오는 함수는 matplotlib.pyplot.scatter 함수입니다. 데이터를 좌표에 점으로 표현하는 함수 입니다.

matplotlib.pyplot.scatter — Matplotlib 3.7.0 documentation

 

matplotlib.pyplot.scatter — Matplotlib 3.7.0 documentation

Fundamentally, scatter works with 1D arrays; x, y, s, and c may be input as N-D arrays, but within scatter they will be flattened. The exception is c, which will be flattened only if its size matches the size of x and y.

matplotlib.org

x,y 값과 컬러, 마커모양 line 두께, 테두리 선, 투명도 등을 정의하는 파라미터들을 사용할 수 있습니다.

 

Matplotlib의 Pyplot 모듈로 Scatter Plot 그리기 (glanceyes.com)

 

Matplotlib의 Pyplot 모듈로 Scatter Plot 그리기

2022년 2월 3일(목)부터 4일(금)까지 네이버 부스트캠프(boostcamp) AI Tech 강의를 들으면서 개인적으로 중요하다고 생각되거나 짚고 넘어가야 할 핵심 내용들만 간단하게 메모한 내용입니다. 틀리거

glanceyes.com

좌표위에 점으로 표시하면 그 거리에 따라서 좀 더 가까운 것과 먼것 그리고 가까운 것들의 군집, 다른 군집들과 동떨어져 있는 특정 군집이나 특정 데이터 등을 알 수 있습니다.

 

여기까지 하면 아래와 같은 산점도가 표시 됩니다.

 

1000 개의 점이 있을 겁니다. 2차원화된 임베딩 값을 x,y값으로 해서 표시한 것입니다.

 

다음 나오는 for 루프는 5번 돕니다. 별점이 1점부터 5점까지 5가지가 있기 때문입니다.

위에 color_indices 에서 별점 - 1을 했기 때문에 값은 0~4까지의 숫자들이 있게 됩니다.

여기서 하는 일은 각 별점별로 평균을 내서 좌표로 표시하는 겁니다.

보시면 마커는 X로 돼 있습니다.

 

이 부분만 따로 표시를 하면 아래와 같이 됩니다.

 

각 별점별 평균 값은 좌표상 위와 같이 찍힙니다.

 

그러면 아까 위에 있었던 산점도와 이 별점별 평균 산점도를 같이 표시 하겠습니다.

 

위와 같이 나타납니다.

 

각 별점별 평균 위치와 각 데이터별 위치가 색깔별로 표시 돼 있습니다.

데이터들이 40 근처에는 별로 없습니다.

그리고 별점별 평균은 0-20 사이에 주로 몰려 있구요.

데이터들 중 40-60 사이의 것들이 이 평균과는 좀 떨어져서 군집을 이루고 있네요.

그 안의 별점별 분포도는 특이성을 띄는 것 같지는 않습니다. 다만 빨간색이 거의 없네요.

 

이런 식으로 임베딩 데이터를 t-SNE 로 2차원으로 바꾼 다음 matplotlib.pyplot 으로 시각화 해서 2D로 표시하는 것이 이 예제에서 보여 주는 것 입니다.

 

openai api 에서 받은 임베딩 데이터를 2D 로 시각화 하는 예제였습니다.

openai-cookbook/Visualizing_embeddings_in_2D.ipynb at main · openai/openai-cookbook · GitHub

 

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

 

반응형


반응형

오늘은 Openai cookbook의 embeddings에 있는 Clustering embeddings 예제를 공부해 보겠습니다.

openai-cookbook/Clustering.ipynb at main · openai/openai-cookbook · GitHub

 

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

여기서 사용하는 예제는 아래 글을 보시면 구하실 수 있습니다.

IT 기술 따라잡기 :: Openai cookbook - Embeddings - Text comparison examples - Semantic text search using embeddings (tistory.com)

 

Openai cookbook - Embeddings - Text comparison examples - Semantic text search using embeddings

오늘은 openai cookbook 에 있는 Embeddings 부문의 Text comparison examples 에 있는 Semantic_text_search_using_embeddings.ipynb 예제를 살펴 보겠습니다. 우선 이 예제를 살펴 보기 전에 준비해야 할 사항들이 몇가지

coronasdk.tistory.com

첫번째 예제 코드와 실행 결과는 아래와 같습니다.

 

 

이번에는 command prompt 가 아닌 Jupyter lab을 사용해서 실행해 보았습니다.

이전 예제에서 계속 사용해 왔던 numpy와 pandas 모듈을 import 합니다.

그리고 csv 파일을 datafile_path에 담습니다.

 

그리고 pandas 모듈의 read_csv() 함수를 이용해 해당 파일을 읽어오고 이를 df변수에 담습니다.

그 다음줄은 df에 embedding이라는 컬럼을 만들고 (있으면 덮어쓰고) 그곳에 df.embedding.apply(eval).apply(np.array) 을 담습니다.

string을 numpy array로 변환시키는 부분입니다.

이건 공식처럼 외우셔도 괜찮을 것 같습니다.

아래 페이지에 가면 그 사용법이 나옵니다.

Embeddings - OpenAI API

 

OpenAI API

An API for accessing new AI models developed by OpenAI

platform.openai.com

 

 

그 다음은 df의 embedding의 값을 numpy의 vstack() 함수를 사용해서 행을 기준으로 병합합니다.

numpy.vstack(tup, *, dtype=None, casting='same_kind')

Stack arrays in sequence vertically (row wise).

 

아래 페이지에는 더 자세하게 설명 돼 있습니다.

 

numpy.vstack — NumPy v1.24 Manual

 

numpy.vstack — NumPy v1.24 Manual

If provided, the destination array will have this dtype. Cannot be provided together with out.

numpy.org

그 다음 shape은 배열의 모양을 말합니다. 

이 배열에는 총 1000개의 embedding이 있고 각 embedding 마다 1536개의 float 형식의 값이 있습니다.

그래서 matrix.shape 을 하면 (1000,1536) 이라는 값이 나옵니다.

 

1. Find the clusters using K-means

다음은 clustering을 지원해 주는 python의 모듈인 sklearn을 사용합니다.

sklearn의 cluster에 있는 KMeans라는 함수를 import 합니다.

KMeans와 관련한 설명은 여기에 있습니다.

2.3. Clustering — scikit-learn 1.2.1 documentation

 

2.3. Clustering

Clustering of unlabeled data can be performed with the module sklearn.cluster. Each clustering algorithm comes in two variants: a class, that implements the fit method to learn the clusters on trai...

scikit-learn.org

클러스터 수는 4개로 하고 KMeans를 사용해서 클러스터링 관련 계산을 해서 kmeans에 담습니다.

KMeans는 임의로 중심을 정하는 부분과 모든 데이터에 대해 중심 거리를 각각 구해서 가장 거리가 작은 중심으로 grouping을 하고 각 그룹마다 다시 평균을 구하는 것을 평균의 변화가 거의 없을 때까지 반복합니다.

 

k-평균 알고리즘 - 위키백과, 우리 모두의 백과사전 (wikipedia.org)

 

k-평균 알고리즘 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. k-평균 알고리즘(K-means clustering algorithm)은 주어진 데이터를 k개의 클러스터로 묶는 알고리즘으로, 각 클러스터와 거리 차이의 분산을 최소화하는 방식으로 동작

ko.wikipedia.org

여기서는 그룹을 4개로 나누라는 의미입니다.

 

그 다음에 나오는 fit()과 labels_ 는 항상 따라 다니더라구요.

 

fit()은 k-means clustring을 compute  해 주고 labels_는 각 포인트별로 라벨링을 해 주는 함수 입니다.

 

관련해서는 아래 페이지에 자세한 내용이 있습니다.

 

sklearn.cluster.KMeans — scikit-learn 1.2.1 documentation

 

sklearn.cluster.KMeans

Examples using sklearn.cluster.KMeans: Release Highlights for scikit-learn 1.1 Release Highlights for scikit-learn 1.1 Release Highlights for scikit-learn 0.23 Release Highlights for scikit-learn 0...

scikit-learn.org

 

그 다음에는 데이터 프레임에 Cluster라는 컬럼에 이 labels를 넣습니다.

 

그 다음은 pandas의 데이터를 다루는 함수들이 나옵니다.

Cluster를 기준으로 groupby를 하고 score.mean()으로 평균을 구합니다.

그리고sort_values()로 정렬을 했습니다.

 

 

이 결과는 4개의 그룹으로 clustering을 했고 각 그룹별 평균값일 표시한 겁니다.

 

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

 

 

sklearn의 manifold  모듈의 TSNE 함수를 import 합니다.

sklearn.manifold.TSNE — scikit-learn 1.2.1 documentation

 

sklearn.manifold.TSNE

Examples using sklearn.manifold.TSNE: Comparison of Manifold Learning methods Comparison of Manifold Learning methods Manifold Learning methods on a severed sphere Manifold Learning methods on a se...

scikit-learn.org

이것은 고차원 데이터를 시각화 해 주는 툴입니다.

 

그 다음은 matplotlib 모듈입니다. 이 모듈도 Visualization 관련 모듈입니다.

Matplotlib — Visualization with Python

 

Matplotlib — Visualization with Python

seaborn seaborn is a high level interface for drawing statistical graphics with Matplotlib. It aims to make visualization a central part of exploring and understanding complex datasets. statistical data visualization Cartopy Cartopy is a Python package des

matplotlib.org

[CCTV] 5.matplotlib기초 (tistory.com)

 

[CCTV] 5.matplotlib기초

서울시 CCTV 분석하기 프로젝트 5. matplotlib기초 matplotlib란? 파이썬의 대표 시각화 도구 Matplotlib는 Python 프로그래밍 언어 및 수학적 확장 NumPy 라이브러리를 활용한 플로팅 라이브러리이다. Tkinter ,

ruby-jieun.tistory.com

 

그 다음은 TSNE()의 값을 tsne 변수에 담고 이것을 fit_transform() 해서 vis_dim2에 담습니다.

fit_transform(X[, y]) Fit X into an embedded space and return that transformed output.

그리고 x와 y를 설정합니다. 

 

그리고 for 문을 통해 각 4개의 그룹별로 루프를 돌게 만들어서 각 그룹별로 색을 다르게 표현하도록 합니다.

 

plt.scatter()는 산점도를 그리는 함수 입니다.

 

matplotlib.pyplot.scatter — Matplotlib 3.7.0 documentation

 

matplotlib.pyplot.scatter — Matplotlib 3.7.0 documentation

Fundamentally, scatter works with 1D arrays; x, y, s, and c may be input as N-D arrays, but within scatter they will be flattened. The exception is c, which will be flattened only if its size matches the size of x and y.

matplotlib.org

[시각화] plt.scatter()를 활용한 산점도 그리기 (tistory.com)

 

[시각화] plt.scatter()를 활용한 산점도 그리기

0. 학습 환경 matplotlib: 3.3.4 seaborn: 0.11.1 금일 학습에는 seaborn 라이브러리에서 제공하는 iris 데이터를 사용하고자 합니다. seaborn에서 제공하는 다른 데이터셋은 '.get_dataset_names()'를 통해 확인이 가

scent-of-light.tistory.com

그리고 plt.title()에서 이 표의 제목을 정해주면 결과와 같은 그림을 얻을 수 있습니다.

 

 

 

4개의 그룹중에 녹색 그룹은 다른 그룹들과 좀 동떨어져 있는 것을 보실 수 있습니다.

 

2. Text samples in the clusters & naming the clusters

지금까지는 raw data를 clustering 하는 법과 이 clustering 한 데이터를 시각화 해서 보여주는 방법을 보았습니다.

 

이제 openai의 api를 이용해서 각 클러스터의 랜덤 샘플들을 보여 주는 코드입니다. 

openai.Completion.create() api를 사용할 것이고 모델 (engine)은 text-ada-001을 사용합니다.

prompt는 아래 질문 입니다.

What do the following customer reviews have in common?

그러면 각 클러스터 별로 review 를 분석한 값들이 response 됩니다.

 

우선 아래 코드를 실행 해 보겠습니다.

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')

# Reading a review which belong to each group.
rev_per_cluster = 5

for i in range(n_clusters):
    print(f"Cluster {i} Theme:", end=" ")

    reviews = "\n".join(
        df[df.Cluster == i]
        .combined.str.replace("Title: ", "")
        .str.replace("\n\nContent: ", ":  ")
        .sample(rev_per_cluster, random_state=42)
        .values
    )
    response = openai.Completion.create(
        engine="text-ada-001",  #"text-davinci-003",
        prompt=f'What do the following customer reviews have in common?\n\nCustomer reviews:\n"""\n{reviews}\n"""\n\nTheme:',
        temperature=0,
        max_tokens=64,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0,
    )
    print(response)

openai를 import 하고 openai api key를 제공하는 부분으로 시작합니다.

그리고 rev_per_cluster는 5로 합니다.

그 다음 for 문에서 n_clusters만큼 루프를 도는데 위에서 n_clusters는 4로 설정돼 있었습니다.

 

reviews에는 Title과 Content 내용을 넣는데 샘플로 5가지를 무작위로 뽑아서 넣습니다.

 

그리고 이 reviews 값을 prompt에 삽입해서 openai.Completion.create() api로 request 합니다.

 

그러면 이 prompt에 대한 response 가 response 변수에 담깁니다.

 

이 response 만 우선 출력해 보겠습니다.

 

Cluster 0 Theme: {
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "logprobs": null,
      "text": " Customer reviews:gluten free, healthy bars, content:\n\nThe customer reviews have in common that they save money on Amazon by ordering by themselves by looking for gluten free healthy bars. The bars are also delicious."
    }
  ],
  "created": 1677191195,
  "id": "cmpl-6nEKppB6SqCz07LYTcaktEAgq06hm",
  "model": "text-ada-001",
  "object": "text_completion",
  "usage": {
    "completion_tokens": 44,
    "prompt_tokens": 415,
    "total_tokens": 459
  }
}
Cluster 1 Theme: {
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "logprobs": null,
      "text": " Cat food\n\nMessy, undelicious, and possibly unhealthy."
    }
  ],
  "created": 1677191195,
  "id": "cmpl-6nEKpGffRc2jyJB4gNtuCa09dG2GT",
  "model": "text-ada-001",
  "object": "text_completion",
  "usage": {
    "completion_tokens": 15,
    "prompt_tokens": 529,
    "total_tokens": 544
  }
}
Cluster 2 Theme: {
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "logprobs": null,
      "text": " Coffee\n\nThe customer's reviews have in common that they are among the best in the market, Rodeo Drive, and that the customer is able to enjoy their coffee half and half because they have an Amazon account."
    }
  ],
  "created": 1677191196,
  "id": "cmpl-6nEKqxza0t8vGRAiK9K5RtCy3Gwbl",
  "model": "text-ada-001",
  "object": "text_completion",
  "usage": {
    "completion_tokens": 45,
    "prompt_tokens": 443,
    "total_tokens": 488
  }
}
Cluster 3 Theme: {
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "logprobs": null,
      "text": " Customer reviews of different brands of soda."
    }
  ],
  "created": 1677191196,
  "id": "cmpl-6nEKqKuxe4CVJTV4GlIZ7vxe6F85o",
  "model": "text-ada-001",
  "object": "text_completion",
  "usage": {
    "completion_tokens": 8,
    "prompt_tokens": 616,
    "total_tokens": 624
  }
}
​

이 respons를 보시면 각 Cluster 별로 응답을 받았습니다.

위에 for 문에서 각 클러스터별로 request를 했기 때문입니다.

이제 이 중에서 실제 질문에 대한 답변인 choices - text  부분만 뽑아 보겠습니다.

 

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')

# Reading a review which belong to each group.
rev_per_cluster = 5

for i in range(n_clusters):
    print(f"Cluster {i} Theme:", end=" ")

    reviews = "\n".join(
        df[df.Cluster == i]
        .combined.str.replace("Title: ", "")
        .str.replace("\n\nContent: ", ":  ")
        .sample(rev_per_cluster, random_state=42)
        .values
    )
    response = openai.Completion.create(
        engine="text-ada-001",  #"text-davinci-003",
        prompt=f'What do the following customer reviews have in common?\n\nCustomer reviews:\n"""\n{reviews}\n"""\n\nTheme:',
        temperature=0,
        max_tokens=64,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0,
    )
 
    print(response["choices"][0]["text"].replace("\n", ""))

답변은 아래와 같습니다.

 

Cluster 0 Theme:  Customer reviews:gluten free, healthy bars, content:The customer reviews have in common that they save money on Amazon by ordering by themselves by looking for gluten free healthy bars. The bars are also delicious.
Cluster 1 Theme:  Cat foodMessy, undelicious, and possibly unhealthy.
Cluster 2 Theme:  CoffeeThe customer's reviews have in common that they are among the best in the market, Rodeo Drive, and that the customer is able to enjoy their coffee half and half because they have an Amazon account.
Cluster 3 Theme:  Customer reviews of different brands of soda.

 

다음엔 df에서 샘플을 뽑아 내는 코드가 있습니다.

 

sample_cluster_rows = df[df.Cluster == i].sample(rev_per_cluster, random_state=42)

df.sample(n) 은 n개의 행들을 임의로 추출합니다.

random_state은 샘플링 결과를 고정시키기 위한 것입니다.

 

[Pandas] 파이썬 데이터프레임 랜덤 샘플링 방법 : df.sample (tistory.com)

 

[Pandas] 파이썬 데이터프레임 랜덤 샘플링 방법 : df.sample

Python의 판다스 모듈로 데이터프레임의 행들을 랜덤 추출할 수 있는 df.sample 기능에 대하여 사용 예제를 정리해보도록 하겠습니다. 이해를 돕기 위하여 아래의 데이터프레임 df에 대하여 행 샘플

jimmy-ai.tistory.com

pandas.DataFrame.sample — pandas 1.5.3 documentation (pydata.org)

 

pandas.DataFrame.sample — pandas 1.5.3 documentation

Default ‘None’ results in equal probability weighting. If passed a Series, will align with target object on index. Index values in weights not found in sampled object will be ignored and index values in sampled object not in weights will be assigned we

pandas.pydata.org

 sample_cluster_rows = df[df.Cluster == i].sample(rev_per_cluster, random_state=42)

 

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

 

Cluster 0 Theme:      Unnamed: 0   ProductId          UserId  Score  \
117         400  B008JKU2CO  A1XV4W7JWX341C      5   
25          274  B008JKTH2A  A34XBAIFT02B60      1   
722         534  B0064KO16O  A1K2SU61D7G41X      5   
289           7  B001KP6B98   ABWCUS3HBDZRS      5   
590         948  B008GG2N2S  A1CLUIIJL6EHLU      5   

                                               Summary  \
117  Loved these gluten free healthy bars, saved $$...   
25   Should advertise coconut as an ingredient more...   
722                                        very good!!   
289                                  Excellent product   
590                                          delicious   

                                                  Text  \
117  These Kind Bars are so good and healthy & glut...   
25   First, these should be called Mac - Coconut ba...   
722  just like the runts<br />great flavor, def wor...   
289  After scouring every store in town for orange ...   
590  Gummi Frogs have been my favourite candy that ...   

                                              combined  n_tokens  \
117  Title: Loved these gluten free healthy bars, s...        96   
25   Title: Should advertise coconut as an ingredie...        78   
722  Title: very good!!; Content: just like the run...        43   
289  Title: Excellent product; Content: After scour...       100   
590  Title: delicious; Content: Gummi Frogs have be...        75   

                                             embedding  Cluster  
117  [-0.002289338270202279, -0.01313735730946064, ...        0  
25   [-0.01757248118519783, -8.266511576948687e-05,...        0  
722  [-0.011768403463065624, -0.025617636740207672,...        0  
289  [0.0007493243319913745, -0.017031244933605194,...        0  
590  [-0.005802689120173454, 0.0007485789828933775,...        0  
Cluster 1 Theme:      Unnamed: 0   ProductId          UserId  Score  \
536         731  B0029NIBE8  A3RKYD8IUC5S0N      2   
332         184  B000WFRUOC   A22RVTZEIVHZA      4   
424         153  B0007A0AQW  A15X1BO4CLBN3C      5   
298          24  B003R0LKRW  A1OQSU5KYXEEAE      1   
960         589  B003194PBC  A2FSDQY5AI6TNX      5   

                              Summary  \
536  Messy and apparently undelicious   
332                  The cats like it   
424          cant get enough of it!!!   
298               Food Caused Illness   
960          My furbabies LOVE these!   

                                                  Text  \
536  My cat is not a huge fan. Sure, she'll lap up ...   
332  My 7 cats like this food but it is a little yu...   
424  Our lil shih tzu puppy cannot get enough of it...   
298  I switched my cats over from the Blue Buffalo ...   
960  Shake the container and they come running. Eve...   

                                              combined  n_tokens  \
536  Title: Messy and apparently undelicious; Conte...       181   
332  Title: The cats like it; Content: My 7 cats li...        87   
424  Title: cant get enough of it!!!; Content: Our ...        59   
298  Title: Food Caused Illness; Content: I switche...       131   
960  Title: My furbabies LOVE these!; Content: Shak...        47   

                                             embedding  Cluster  
536  [-0.002376032527536154, -0.0027701142244040966...        1  
332  [0.02162935584783554, -0.011174295097589493, -...        1  
424  [-0.007517425809055567, 0.0037251529283821583,...        1  
298  [-0.0011128562036901712, -0.01970377005636692,...        1  
960  [-0.009749102406203747, -0.0068712360225617886...        1  
Cluster 2 Theme:      Unnamed: 0   ProductId          UserId  Score  \
135         410  B007Y59HVM  A2ERWXZEUD6APD      5   
439         812  B0001UK0CM  A2V8WXAFG1TEOC      5   
326         107  B003VXFK44  A21VWSCGW7UUAR      4   
475         852  B000I6MCSY   AO34Q3JGZU0JQ      5   
692         922  B003TC7WN4  A3GFZIL1E0Z5V8      5   

                               Summary  \
135                  Fog Chaser Coffee   
439                    Excellent taste   
326   Good, but not Wolfgang Puck good   
475             Just My Kind of Coffee   
692  Rodeo Drive is Crazy Good Coffee!   

                                                  Text  \
135  This coffee has a full body and a rich taste. ...   
439  This is to me a great coffee, once you try it ...   
326  Honestly, I have to admit that I expected a li...   
475  Coffee Masters Hazelnut coffee used to be carr...   
692  Rodeo Drive is my absolute favorite and I'm re...   

                                              combined  n_tokens  \
135  Title: Fog Chaser Coffee; Content: This coffee...        42   
439  Title: Excellent taste; Content: This is to me...        31   
326  Title: Good, but not Wolfgang Puck good; Conte...       178   
475  Title: Just My Kind of Coffee; Content: Coffee...       118   
692  Title: Rodeo Drive is Crazy Good Coffee!; Cont...        59   

                                             embedding  Cluster  
135  [0.006498195696622133, 0.006776264403015375, 0...        2  
439  [0.0039436533115804195, -0.005451332312077284,...        2  
326  [-0.003140551969408989, -0.009995664469897747,...        2  
475  [0.010913548991084099, -0.014923149719834328, ...        2  
692  [-0.029914353042840958, -0.007755572907626629,...        2  
Cluster 3 Theme:      Unnamed: 0   ProductId          UserId  Score  \
495         831  B0014X5O1C   AHYRTWABDAG1H      5   
978         642  B00264S63G  A36AUU1UNRS48G      5   
916         686  B008PYVINQ  A1DRWYIO7JN1MD      2   
696         926  B0062P9XPU  A33KQALCZGXG8C      5   
491         828  B000EIE20M  A39QHSDUBR8L0T      3   

                               Summary  \
495  Wonderful alternative to soda pop   
978      So convenient, for so little!   
916                    bot very cheesy   
696                         Delicious!   
491                            Just ok   

                                                  Text  \
495  This is a wonderful alternative to soda pop.  ...   
978  I needed two vanilla beans for the Love Goddes...   
916  Got this about a month ago.first of all it sme...   
696  I am not a huge beer lover.  I do enjoy an occ...   
491  I bought this brand because it was all they ha...   

                                              combined  n_tokens  \
495  Title: Wonderful alternative to soda pop; Cont...       273   
978  Title: So convenient, for so little!; Content:...       121   
916  Title: bot very cheesy; Content: Got this abou...        46   
696  Title: Delicious!; Content: I am not a huge be...        97   
491  Title: Just ok; Content: I bought this brand b...        58   

                                             embedding  Cluster  
495  [0.022326279431581497, -0.018449820578098297, ...        3  
978  [-0.004598899278789759, -0.01737511157989502, ...        3  
916  [-0.010750919580459595, -0.0193503275513649, -...        3  
696  [0.009483409114181995, -0.017691848799586296, ...        3  
491  [-0.0023960231337696314, -0.006881058216094971...        3

여기서 데이터를 아래와 같이 가공을 합니다.

 

    for j in range(rev_per_cluster):
        print(sample_cluster_rows.Score.values[j], end=", ")
        print(sample_cluster_rows.Summary.values[j], end=":   ")
        print(sample_cluster_rows.Text.str[:70].values[j])

Score의 값들을 가지고 오고 끝에는 쉼표 , 를 붙입니다.

그리고 Summary의 값을 가지고 오고 끝에는 : 를 붙입니다.

그리고 Text컬럼의 string을 가지고 오는데 70자 까지만 가지고 옵니다.

 

전체 결과를 보겠습니다.

 

Cluster 0 Theme:  Customer reviews:gluten free, healthy bars, content:The customer reviews have in common that they save money on Amazon by ordering by themselves by looking for gluten free healthy bars. The bars are also delicious.
5, Loved these gluten free healthy bars, saved $$ ordering on Amazon:   These Kind Bars are so good and healthy & gluten free.  My daughter ca
1, Should advertise coconut as an ingredient more prominently:   First, these should be called Mac - Coconut bars, as Coconut is the #2
5, very good!!:   just like the runts<br />great flavor, def worth getting<br />I even o
5, Excellent product:   After scouring every store in town for orange peels and not finding an
5, delicious:   Gummi Frogs have been my favourite candy that I have ever tried. of co
Cluster 1 Theme:  Cat foodMessy, undelicious, and possibly unhealthy.
2, Messy and apparently undelicious:   My cat is not a huge fan. Sure, she'll lap up the gravy, but leaves th
4, The cats like it:   My 7 cats like this food but it is a little yucky for the human. Piece
5, cant get enough of it!!!:   Our lil shih tzu puppy cannot get enough of it. Everytime she sees the
1, Food Caused Illness:   I switched my cats over from the Blue Buffalo Wildnerness Food to this
5, My furbabies LOVE these!:   Shake the container and they come running. Even my boy cat, who isn't 
Cluster 2 Theme:  CoffeeThe customer's reviews have in common that they are among the best in the market, Rodeo Drive, and that the customer is able to enjoy their coffee half and half because they have an Amazon account.
5, Fog Chaser Coffee:   This coffee has a full body and a rich taste. The price is far below t
5, Excellent taste:   This is to me a great coffee, once you try it you will enjoy it, this 
4, Good, but not Wolfgang Puck good:   Honestly, I have to admit that I expected a little better. That's not 
5, Just My Kind of Coffee:   Coffee Masters Hazelnut coffee used to be carried in a local coffee/pa
5, Rodeo Drive is Crazy Good Coffee!:   Rodeo Drive is my absolute favorite and I'm ready to order more!  That
Cluster 3 Theme:  Customer reviews of different brands of soda.
5, Wonderful alternative to soda pop:   This is a wonderful alternative to soda pop.  It's carbonated for thos
5, So convenient, for so little!:   I needed two vanilla beans for the Love Goddess cake that my husbands 
2, bot very cheesy:   Got this about a month ago.first of all it smells horrible...it tastes
5, Delicious!:   I am not a huge beer lover.  I do enjoy an occasional Blue Moon (all o
3, Just ok:   I bought this brand because it was all they had at Ranch 99 near us. I

이제 좀 보기 좋게 됐습니다.

 

이번 예제는 raw 데이터를 파이썬의 여러 모듈들을 이용해서 clustering을 하고 이 cluster별로 openai.Completion.create() api를 이용해서 궁금한 답을 받는 일을 하는 예제를 배웠습니다.

 

큰 raw data를 카테고리화 해서 나누고 이에 대한 summary나 기타 정보를 Completion api를 통해 얻을 수 있는 방법입니다.

 

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

 

# imports
import numpy as np
import pandas as pd

# load data
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)  # convert string to numpy array
matrix = np.vstack(df.embedding.values)
matrix.shape

from sklearn.cluster import KMeans

n_clusters = 4

kmeans = KMeans(n_clusters=n_clusters, init="k-means++", random_state=42)
kmeans.fit(matrix)
labels = kmeans.labels_
df["Cluster"] = labels

df.groupby("Cluster").Score.mean().sort_values()

from sklearn.manifold import TSNE
import matplotlib
import matplotlib.pyplot as plt

tsne = TSNE(n_components=2, perplexity=15, random_state=42, init="random", learning_rate=200)
vis_dims2 = tsne.fit_transform(matrix)

x = [x for x, y in vis_dims2]
y = [y for x, y in vis_dims2]

for category, color in enumerate(["purple", "green", "red", "blue"]):
    xs = np.array(x)[df.Cluster == category]
    ys = np.array(y)[df.Cluster == category]
    plt.scatter(xs, ys, color=color, alpha=0.3)

    avg_x = xs.mean()
    avg_y = ys.mean()

    plt.scatter(avg_x, avg_y, marker="x", color=color, s=100)
plt.title("Clusters identified visualized in language 2d using t-SNE")

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')

# Reading a review which belong to each group.
rev_per_cluster = 5

for i in range(n_clusters):
    print(f"Cluster {i} Theme:", end=" ")

    reviews = "\n".join(
        df[df.Cluster == i]
        .combined.str.replace("Title: ", "")
        .str.replace("\n\nContent: ", ":  ")
        .sample(rev_per_cluster, random_state=42)
        .values
    )
    response = openai.Completion.create(
        engine="text-ada-001",  #"text-davinci-003",
        prompt=f'What do the following customer reviews have in common?\n\nCustomer reviews:\n"""\n{reviews}\n"""\n\nTheme:',
        temperature=0,
        max_tokens=64,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0,
    )
 
    print(response["choices"][0]["text"].replace("\n", ""))

    sample_cluster_rows = df[df.Cluster == i].sample(rev_per_cluster, random_state=42)

    for j in range(rev_per_cluster):
        print(sample_cluster_rows.Score.values[j], end=", ")
        print(sample_cluster_rows.Summary.values[j], end=":   ")
        print(sample_cluster_rows.Text.str[:70].values[j])
반응형


반응형

오늘은 OpenAI Cookbook 에 있는 Embeddings 섹션의 두번째 페이지를 공부해 보겠습니다.

Text comparison exampls에 있는 예제들은 대충 한번 둘러 봤습니다.

여기에는 get_embedding() 관련 에제 두개가 나옵니다.

아주 기초적인 건데요. 이미 두루었기도 하구요.

여기 나오니까 잠깐 살펴보고 가겠습니다.

 

첫번째 소스 입니다.

import openai

embedding = openai.Embedding.create(
    input="Your text goes here", model="text-embedding-ada-002"
)["data"][0]["embedding"]
len(embedding)

이렇게 하면 embedding에는 prompt ("Your text goes here") 에 대한 openai api의 response가 담기게 됩니다.

이것은 JSON 형식으로 되어 있습니다. 그 중에 data 항목의 첫번째에 있는 embedding이란 항목에 있는 값만 embedding에 담기게 됩니다.

 

이것을 그대로 출력해 보겠습니다.

 

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')

embedding = openai.Embedding.create(
    input="Your text goes here", model="text-embedding-ada-002"
)["data"][0]["embedding"]
len(embedding)
print (len(embedding))
print (embedding)

확인을 위해서 소스코드를 약간 바꾸었습니다.

openai api key를 제공하는 부분을 넣었구요.

command 창에서 결과를 확인할 수 있도록 print 문을 넣었습니다.

파이썬 주피터 툴을 쓰시면 print 문 없이 결과를 확인 할 수 있습니다.

 

결과는 이렇습니다.

 

embedding의 length는 1536 이고 그 값은 아래 나온 숫자들입니다.

 

그럼 전체 JSON 형식의 response를 확인하기 위해 ["data"][0]["embedding"] 부분을 빼 보겠습니다.

 

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')

embedding = openai.Embedding.create(
    input="Your text goes here", model="text-embedding-ada-002")
print (embedding)

그 결과는 아래와 같습니다.

 

["data"][0]["embedding"] <- 이 부분이 하는 일은 data의 첫번째 embedding 값들만 받으라는 얘기입니다.

참고로 이 response의 끝부분은 이렇습니다.

 

 

이 response의 data에는 embedding 값만이 아니라 index, object 정보도 있습니다.

그리고 data 이외의 정보들로는 사용한 모델, object 타입 usage 안에는 토큰 정보가 있습니다.

(참고로 이 모델과 토큰에 따라 api 사용료가 달라집니다.)

 

그러니까 이 ["data"][0]["embedding"] 부분이 하는 일은 저 response 중에 data에 있는 embedding 값만 받고 싶을 때 사용할 수 있습니다.

 

그 다음 예제는 아래와 같습니다.

 

import openai
from tenacity import retry, wait_random_exponential, stop_after_attempt


@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6))
def get_embedding(text: str, model="text-embedding-ada-002") -> list[float]:
    return openai.Embedding.create(input=[text], model=model)["data"][0]["embedding"]


embedding = get_embedding("Your text goes here", model="text-embedding-ada-002")
print(len(embedding))

이 글의 주제와는 다른 토픽이지만 처음 보는 파이썬 모듈이 나와서 살펴 보고 넘어 가겠습니다.

tenacity

Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything.

이 모듈은 소스 코드에서 retry 해야 할 필요가 있을 때 사용할 수 있는 모듈이라고 합니다.

 

에러나 예외 처리에 의해 런타임이 종료 될 때가 있는데 이때 Tenacity는 종료 없이 함수를 다시 실행시켜 주는 Python 라이브러리 입니다.

 

이 모듈이 깔려 있지 않으면 pip install tenacity 를 사용해서 인스톨 할 수 있습니다.

 

그 중에 retry와 wait_random_exponential, stop_after_attempt 함수를 import 했습니다.

 

이와 관련한 사용법은 아래 페이지에 있습니다.

https://tenacity.readthedocs.io/en/latest/

 

Tenacity — Tenacity documentation

Tenacity Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything. It originates from a fork of retrying which is sadly no longer maintained. Tenacity isn

tenacity.readthedocs.io

from tenacity import retry, wait_random_exponential, stop_after_attempt
@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6))

이 부분은 실행중에 에러나 예외 처리에 의해 런타임이 종료 되어야 하는 상황이 오면 retry 를 하는데 그 retry 사이의 시간 텀은 1초에서 20초 사이에 랜덤하게 설정하고 retry를 6번 까지만 실행 하라는 의미 입니다.

 

런타임 종료 상황이 오지 않는다면 이 부분은 실행 될 일이 없는 부분입니다.

 

이제 본 내용인 그 다음 코드를 살펴 보겠습니다.

 

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

embedding = get_embedding("Your text goes here", model="text-embedding-ada-002")
print(len(embedding))

 

get_embedding() 함수가 있는데요. 

이 함수에서는 input 값인 string값과 model 값을 input으로 받습니다.

그리고 return은 list[float] 형식입니다.

 

그 다음 return 부분은 위에서 설명한 내용 그대로 입니다.

JSON 형식의 response 중에서 data의 첫번째 인자인 embedding 값을 return 한다는 의미 입니다.

 

그 다음 줄에서는 이 return 값을 embedding에 담기 위해 get_embedding 함수를 호출하고 있습니다.

print(len(embedding)) 은 이 return 값의 length를 출력 합니다.

 

이 소스 코드를 그대로 출력해 보겠습니다.

 

그러기 위해서 api key 관련 부분을 추가했습니다.

import openai
from tenacity import retry, wait_random_exponential, stop_after_attempt

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

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

@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6))
def get_embedding(text: str, model="text-embedding-ada-002") -> list[float]:
    return openai.Embedding.create(input=[text], model=model)["data"][0]["embedding"]


embedding = get_embedding("Your text goes here", model="text-embedding-ada-002")
print(len(embedding))

그리고 결과 값은 아래와 같습니다.

이 페이지는 openai api 중 embeddings_utils.py에 있는 get_embedding() 을 설명하기 위한 페이지 입니다.

이 파이썬 소스 코드에 있는 get_embedding() 함수는 아래와 같습니다.

 

 

여기서 모델은 text-similarity-davinci-001로 돼 있는데 이 model (engine) 파라미터를 text-embedding-ada-002 로 해서 보내면 됩니다.

 

이 파이썬에는 이 외에도 aget_embedding(), get_embeddings(), aget_embeddings(), cosine_similarity() 등 더 많은 메소드들이 있습니다.

 

이곳에 가면 그 내용을 볼 수 있습니다.

 

https://github.com/openai/openai-python/blob/main/openai/embeddings_utils.py

 

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.embeddings_utils를 사용하는 방법을 보실 수 있습니다.

 

https://coronasdk.tistory.com/1263

 

Openai cookbook - Embeddings - Text comparison examples - Semantic text search using embeddings

오늘은 openai cookbook 에 있는 Embeddings 부문의 Text comparison examples 에 있는 Semantic_text_search_using_embeddings.ipynb 예제를 살펴 보겠습니다. 우선 이 예제를 살펴 보기 전에 준비해야 할 사항들이 몇가지

coronasdk.tistory.com

 

오늘은 get embeddings의 아주 기초적인 부분을 짚고 넘어갈 수 있었네요.

 

이 내용은 openai-cookbook 페이지로 가서 Embeddings -> How to get embeddings로 가서면 보실 수 있습니다.

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

 

반응형


반응형

OpenAI의 임베딩 모델에는 fine-tune을 할 수 없지만 임베딩을 커스터마이즈 하기위해 training data를 사용할 수 있습니다.

아래 예제에서는 training data를 사용해서 임베딩을 customize 하는 방법을 보여 줍니다. 

새로운 임베딩을 얻기 위해 임베딩 벡터를 몇배 증가시키도록 custom matrix를 훈련하는 것입니다.

좋은 training data 가 있으면 이 custom matrix는 training label들과 관련된 기능들을 향상시키는데 도움이 될 것입니다.

matrix multiplication을 임베딩을 수정하거나 임베딩간의 distance를 측정하는데 사용하는 distance 함수를 수정하는 방법을 사용할 수 있습니다.

 

https://github.com/openai/openai-cookbook/blob/main/examples/Customizing_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

 

Customizing embeddings

 

이 예제의 training data는 [text_1, text_2, label] 형식 입니다. 

두 쌍이 유사하면 레이블은 +1 이고 유사하지 않으면 -1 입니다.

 

output은 임베딩을 multiply 하는데 사용할 수 있는 matrix 입니다.

임베딩 multiplication을 통해서 좀 더 성능이 좋은 custom embedding을 얻을 수 있습니다. 

 

그 다음 예제는 SNLI corpus에서 가지고 온 1000개의 sentence pair들을 사용합니다. 이 두 쌍은 논리적으로 연관돼 있는데 한 문장이 다른 문장을 암시하는 식 입니다. 논리적으로 연관 돼 있으면 레이블이 positive 입니다. 논리적으로 연관이 별로 없어 보이는 쌍은 레이블이 negative 가 됩니다.

 

그리고 clustering을 사용하는 경우에는 같은 클러스터 내의 텍스트 들로부터 한 쌍을 만듦으로서 positive 한 것을 생성할 수 있습니다. 그리고 다른 클러스터의 문장들로 쌍을 이루어서 negative를 생성할 수 있습니다.

 

다른 데이터 세트를 사용하면 100개 미만의 training example들 만으로도 좋은 성능 개선을 이루는 것을 볼 수 있었습니다. 물론 더 많은 예제를 사용하면 더 좋아지겠죠.

 

이제 소스 코드로 들어가 보겠습니다.

 

0. Imports

 

# imports
from typing import List, Tuple  # for type hints

import numpy as np  # for manipulating arrays
import pandas as pd  # for manipulating data in dataframes
import pickle  # for saving the embeddings cache
import plotly.express as px  # for plots
import random  # for generating run IDs
from sklearn.model_selection import train_test_split  # for splitting train & test data
import torch  # for matrix optimization

from openai.embeddings_utils import get_embedding, cosine_similarity  # for embeddings

import 하는 모듈들이 많습니다.

 

첫째줄 보터 못 보던 모듈이 나왔습니다.

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

 

typing — Support for type hints

Source code: Lib/typing.py This module provides runtime support for type hints. The most fundamental support consists of the types Any, Union, Callable, TypeVar, and Generic. For a full specificati...

docs.python.org

http://pengtory981.tistory.com/19

 

[Python] Typing 모듈

LeetCode 문제를 풀던중 아래와 같은 형식으로 양식이 주어진 것을 보았다. Optional[TreeNode] 와 관련해서 찾아보던 중 파이썬에 Typing이라는 것이 있는 것을 알고 정리해보았다. class Solution: def invertTre

pengtory981.tistory.com

이 모듈에 대한 영문과 한글 설명 페이지입니다.

이 모듈은 data type과 관련된 모듈입니다. 파이썬은 자바와 달리 data type을 따로 명시하지 않을 수 있는데 굳이 type을 명시해야 할 때 이 모듈을 사용할 수 있습니다.

이 예제에서는 typing 모듈의 List와 Tuple을 import 합니다.

list는 순서가 있는 수정 가능한 자료 구조이고 tuple은 순서가 있는 수정이 불가능한 자료구조 입니다.

 

http://wanttosleep1111.tistory.com/11

 

파이썬 리스트, 튜플 (Python List, Tuple)

파이썬 리스트, 튜플 (Python List, Tuple) 1. 리스트 (list) 파이썬의 자료 구조 중 하나 순서가 있고, 수정 가능한 자료 구조 대괄호 [ ]로 작성되고, 리스트 내부 요소는 콤마(,)로 구분 하나의 리스트에

wanttosleep1111.tistory.com

 

그 다음 모듈인 numpy와 pandas 그리고 pickle은 이전 글에서 설명한 모듈입니다.

 

그 다음 나오는 모듈이 plotly.express 이네요.

이것은 데이터를 시각화 하는 모듈입니다.

https://plotly.com/python/plotly-express/

 

Plotly

Over 37 examples of Plotly Express including changing color, size, log axes, and more in Python.

plotly.com

https://www.youtube.com/watch?v=FpCgG85g2Hw 

 

https://blog.naver.com/regenesis90/222559388493

 

[Python] Plotly.express :: scatter() : : 인터랙티브 산점도 그래프 그리기

1. plotly.express:: scatter()의 이해와 표현 plotly는 인터랙티브 그래프를 그려주는 라이브러리입니다. ...

blog.naver.com

위 자료들을 보면 이 plotly.express 모듈을 이해하는데 많은 도움을 받으실 수 있을 겁니다.

 

그 다음 모듈은 random 인데요.

이름만 봐도 무엇을 하는 모듈인지 알겠네요.

다양한 분포를 위한 pseudo-random number generator (유사 난수 생성기) 입니다.

 

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

 

random — Generate pseudo-random numbers

Source code: Lib/random.py This module implements pseudo-random number generators for various distributions. For integers, there is uniform selection from a range. For sequences, there is uniform s...

docs.python.org

http://lungfish.tistory.com/12

 

[python] 내장함수 - random 모듈

🔍예상 검색어 더보기 # 파이썬 랜덤함수 # 파이썬 랜덤 모듈 # 랜덤으로 숫자 생성하기 # python random # random.random #random.randint # python 무작위로 숫자 만들기 # 랜덤으로 숫자 만들기 # 랜덤으로 하

lungfish.tistory.com

 

다음 모듈은 sklearn.model_selection 입니다. 여기에서 train_test_split 함수를 import 하는데요.

이 함수는 배열 또는 행렬을 각각 training 과 test data를 위한 random subset로 split하는 기능이 있습니다.

Split arrays or matrices into random train and test subsets.

 

이 함수는 Machine Learning에서 사용 할 수 있습니다.

 

https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html

 

sklearn.model_selection.train_test_split

Examples using sklearn.model_selection.train_test_split: Release Highlights for scikit-learn 0.24 Release Highlights for scikit-learn 0.24 Release Highlights for scikit-learn 0.23 Release Highlight...

scikit-learn.org

http://asthtls.tistory.com/1186

 

파이썬 머신러닝 완벽 가이드 - 2.4 - Model Selection 모듈 소개

학습/테스트 데이터 세트 분리 - train_test_split() # 학습/테스트 데이터 세트 분리 -train_test_split() from sklearn.datasets import load_iris from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score i

asthtls.tistory.com

 

그 다음 나오는 모듈은 torch 입니다.

이 모듈은 오픈 소스 머신 러닝 라이브러리 (and scientific compution framework) 입니다. Lua 프로그래밍 언어를 기반으로 만들어진 스크립트 언어입니다.

딥 러닝을 위한 광범위한 알고리즘을 제공하고 스크립팅 언어인 LuaJIT 및 Basic C 언어로 구현 되어 있습니다.

https://pypi.org/project/torch/

 

torch

Tensors and Dynamic neural networks in Python with strong GPU acceleration

pypi.org

http://soypablo.tistory.com/41

 

파이토치2.0 번역[수정중]

개요 파이토치의 차세대 2시리즈 릴리스를 향한 첫 걸음인 파이토치 2.0을 소개합니다. 지난 몇 년 동안 파이토치는 파이토치 1.0부터 최신 1.13까지 혁신과 반복을 거듭해왔으며, 리눅스 재단의

soypablo.tistory.com

그 다음으로는 openai.embeddings_utils에 있는 get_embedding과 cosine_similarity 함수를 import 했습니다.

 

이 모듈과 함수에 대한 소스 코드는 아래 openai githug 페이지에서 볼 수 있습니다.

 

https://github.com/openai/openai-python/blob/main/openai/embeddings_utils.py

 

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

저는 torch 모듈이 없다고 나와서 인스톨 했습니다. pip install torch

이제 import 되는 모듈들을 다 살펴 봤습니다.

 

1. Inputs

여기서는 입력 데이터들에 대한 부분 입니다.

진행하기에 앞서 관련 csv 파일을 구해야 합니다.

 

https://nlp.stanford.edu/projects/snli/

 

The Stanford Natural Language Processing Group

The Stanford Natural Language Inference (SNLI) Corpus Natural Language Inference (NLI), also known as Recognizing Textual Entailment (RTE), is the task of determining the inference relation between two (short, ordered) texts: entailment, contradiction, or

nlp.stanford.edu

 

이곳으로 가서 SNLI 1.0 zip 파일을 다운 받으라고 되어 있더라구요.

이 파일을 받고 압축을 풀었더니 3개의 JSON 파일이 있었습니다.

저는 그냥 이중 하나를 CSV 형식으로 convert 했습니다.

 

# input parameters
embedding_cache_path = "data/snli_embedding_cache.pkl"  # embeddings will be saved/loaded here
default_embedding_engine = "babbage-similarity"  # choice of: ada, babbage, curie, davinci
num_pairs_to_embed = 1000  # 1000 is arbitrary - I've gotten it to work with as little as ~100
local_dataset_path = "data/snli_1.0_train_2k.csv"  # download from: https://nlp.stanford.edu/projects/snli/


def process_input_data(df: pd.DataFrame) -> pd.DataFrame:
    # you can customize this to preprocess your own dataset
    # output should be a dataframe with 3 columns: text_1, text_2, label (1 for similar, -1 for dissimilar)
    df["label"] = df["gold_label"]
    df = df[df["label"].isin(["entailment"])]
    df["label"] = df["label"].apply(lambda x: {"entailment": 1, "contradiction": -1}[x])
    df = df.rename(columns={"sentence1": "text_1", "sentence2": "text_2"})
    df = df[["text_1", "text_2", "label"]]
    df = df.head(num_pairs_to_embed)
    return df

여기서 하는 일은 cache 에 있는 데이터를 저장할 pkl 파일을 정하고 사용할 openai 모델을 정했습니다.

그리고 num_pairs_to_embed 변수에 1000을 할당했고 소스 데이터를 local_dataset_path 에 할당했습니다.

 

다음에 process_input_data() 함수가 나오는데요.

pandas 의 DataFrame 형식의 입력값을 받아서 처리한 다음에 다시 DataFrame형식으로 반환합니다.

대개 컬럼 이름들을 바꾸는 거네요.

그리고 DataFrame의 데이터를 1000 (num_pairs_to_embed) 개만 추려서 return 합니다.

 

2. Load and process input data

# load data
df = pd.read_csv(local_dataset_path)

# process input data
df = process_input_data(df)  # this demonstrates training data containing only positives

# view data
df.head()

이제 파일을 열고 위에 만들었던 함수를 통해 처리한 다음 상위 5개를 출력합니다.

df.head() 는 디폴트로 5개를 출력합니다. head() 안에 숫자를 넣으면 그 숫자만큼 출력합니다.

 

이걸 출력하면 이렇게 됩니다.

3. Split data into training test sets

synethetic negatives나 synethetic positives를 생성하기 전에 데이터를 training과 test sets 들로 구분하는 것은 아주 중요합니다.

 

training data의 text 문자열이 test data에 표시되는 것을 원하지 않을 겁니다. 

contamination이 있는 경우 실제 production 보다 test metrics가 더 좋아 보일 수 있습니다.

 

# split data into train and test sets
test_fraction = 0.5  # 0.5 is fairly arbitrary
random_seed = 123  # random seed is arbitrary, but is helpful in reproducibility
train_df, test_df = train_test_split(
    df, test_size=test_fraction, stratify=df["label"], random_state=random_seed
)
train_df.loc[:, "dataset"] = "train"
test_df.loc[:, "dataset"] = "test"

여기에서는 sklearn.model_selection 모듈의 train_test_split() 함수를 사용해서 데이터를 training test sets로 분리 합니다.

 

4. Generate synthetic negatives

다음은 use case에 맞도록 수정해야 할 필요가 있을 수 있는 코드 입니다.

positives와 negarives 가 있는 데이터를 가지고 있을 경우 건너뛰어도 됩니다.

근데 만약 positives 한 데이터만 가지고 있다면 이 코드를 사용해야 할 것입니다. 

이 코드 블록은 negatives 만 생성할 것입니다.

 

만약 여러분이 multiclass data를 가지고 있다면 여러분은 positives와 negatives 모두를 생성하기를 원할 겁니다.

positives는 레이블들을 공유하는 텍스트로 된 쌍이 될 수 있고 negatives는 레이블을 공유하지 않는 텍스트로 된 쌍이 될 수 있습니다.

 

최종 결과물은 text pair로 된 DataFrame이 될 것입니다. 각 쌍은 -1이나 1이 될 것입니다.

 

# generate negatives
def dataframe_of_negatives(dataframe_of_positives: pd.DataFrame) -> pd.DataFrame:
    """Return dataframe of negative pairs made by combining elements of positive pairs."""
    texts = set(dataframe_of_positives["text_1"].values) | set(
        dataframe_of_positives["text_2"].values
    )
    all_pairs = {(t1, t2) for t1 in texts for t2 in texts if t1 < t2}
    positive_pairs = set(
        tuple(text_pair)
        for text_pair in dataframe_of_positives[["text_1", "text_2"]].values
    )
    negative_pairs = all_pairs - positive_pairs
    df_of_negatives = pd.DataFrame(list(negative_pairs), columns=["text_1", "text_2"])
    df_of_negatives["label"] = -1
    return df_of_negatives
negatives_per_positive = (
    1  # it will work at higher values too, but more data will be slower
)
# generate negatives for training dataset
train_df_negatives = dataframe_of_negatives(train_df)
train_df_negatives["dataset"] = "train"
# generate negatives for test dataset
test_df_negatives = dataframe_of_negatives(test_df)
test_df_negatives["dataset"] = "test"
# sample negatives and combine with positives
train_df = pd.concat(
    [
        train_df,
        train_df_negatives.sample(
            n=len(train_df) * negatives_per_positive, random_state=random_seed
        ),
    ]
)
test_df = pd.concat(
    [
        test_df,
        test_df_negatives.sample(
            n=len(test_df) * negatives_per_positive, random_state=random_seed
        ),
    ]
)

df = pd.concat([train_df, test_df])

 

5. Calculate embeddings and cosine similarities

아래 코드 블럭에서는 임베딩을 저장하기 위한 캐시를 생성합니다. 

이렇게 저장함으로서 다시 임베딩을 얻기 위해 openai api를 호출하면서 비용을 지불하지 않아도 됩니다.

 

# establish a cache of embeddings to avoid recomputing
# cache is a dict of tuples (text, engine) -> embedding
try:
    with open(embedding_cache_path, "rb") as f:
        embedding_cache = pickle.load(f)
except FileNotFoundError:
    precomputed_embedding_cache_path = "https://cdn.openai.com/API/examples/data/snli_embedding_cache.pkl"
    embedding_cache = pd.read_pickle(precomputed_embedding_cache_path)


# this function will get embeddings from the cache and save them there afterward
def get_embedding_with_cache(
    text: str,
    engine: str = default_embedding_engine,
    embedding_cache: dict = embedding_cache,
    embedding_cache_path: str = embedding_cache_path,
) -> list:
    print(f"Getting embedding for {text}")
    if (text, engine) not in embedding_cache.keys():
        # if not in cache, call API to get embedding
        embedding_cache[(text, engine)] = get_embedding(text, engine)
        # save embeddings cache to disk after each update
        with open(embedding_cache_path, "wb") as embedding_cache_file:
            pickle.dump(embedding_cache, embedding_cache_file)
    return embedding_cache[(text, engine)]


# create column of embeddings
for column in ["text_1", "text_2"]:
    df[f"{column}_embedding"] = df[column].apply(get_embedding_with_cache)

# create column of cosine similarity between embeddings
df["cosine_similarity"] = df.apply(
    lambda row: cosine_similarity(row["text_1_embedding"], row["text_2_embedding"]),
    axis=1,
)

 

6. Plot distribution of cosine similarity

이 예제에서는 cosine similarity를 사용하여 텍스트의 유사성을 측정합니다. openai 측의 경험상 대부분의 distance functions (L1, L2, cosine similarity)들은 모두 동일하게 작동한다고 합니다. 임베딩 값은 length 가 1로 정규화 되어 있기 때문에 임베딩에서 cosine similarity는 python의 numpy.dot() 과 동일한 결과를 내 놓습니다.

 

그래프들은 유사한 쌍과 유사하지 않은 쌍에 대한 cosine similarity 분포 사이에 겹치는 정도를 보여 줍니다. 겹치는 부분이 많다면 다른 유사한 쌍보다 cosine similarity가 크고 유사하지 않은 쌍이 많다는 의미 입니다.

 

계산의 정확도는 cosine similarity의 특정 임계치인 X 보다 크면 similar (1)을 예측하고 그렇치 않으면 dissimilar (0)을 예측합니다.

 

# calculate accuracy (and its standard error) of predicting label=1 if similarity>x
# x is optimized by sweeping from -1 to 1 in steps of 0.01
def accuracy_and_se(cosine_similarity: float, labeled_similarity: int) -> Tuple[float]:
    accuracies = []
    for threshold_thousandths in range(-1000, 1000, 1):
        threshold = threshold_thousandths / 1000
        total = 0
        correct = 0
        for cs, ls in zip(cosine_similarity, labeled_similarity):
            total += 1
            if cs > threshold:
                prediction = 1
            else:
                prediction = -1
            if prediction == ls:
                correct += 1
        accuracy = correct / total
        accuracies.append(accuracy)
    a = max(accuracies)
    n = len(cosine_similarity)
    standard_error = (a * (1 - a) / n) ** 0.5  # standard error of binomial
    return a, standard_error


# check that training and test sets are balanced
px.histogram(
    df,
    x="cosine_similarity",
    color="label",
    barmode="overlay",
    width=500,
    facet_row="dataset",
).show()

for dataset in ["train", "test"]:
    data = df[df["dataset"] == dataset]
    a, se = accuracy_and_se(data["cosine_similarity"], data["label"])
    print(f"{dataset} accuracy: {a:0.1%} ± {1.96 * se:0.1%}")

 

여기까지 작성한 코드를 실행 해 봤습니다.

 

처음엔 아래 내용을 출력하더니......

그 다음은 아래와 같은 형식으로 한도 끝도 없이 출력 하더라구요.

문서에 총 1만줄이 있던데 한줄 한줄 다 처리하느라 시간이 굉장히 많이 걸릴 것 같습니다.

전 3179 줄까지 출력한 후 시간이 너무 걸려서 강제로 중단 했습니다.

 

저는 text-embedding-ada-002 모델을 사용했습니다.

1천개의 토큰당 0.0004불이 과금 되는데 여기까지 저는 0.01불 즉 1센트가 과금 됐습니다.

 

그러면 0.01 / 0.0004 를 하니까 2만 5천 토큰 정도 사용 했나 보네요.

1만줄을 전부 다 하면 한 5센트 이내로 부과 되겠네요.

하여간 시간도 많이 걸리고 과금도 비교적 많이 되니 실행은 마지막에 딱 한번만 해 보기로 하겠습니다.

 

예제 상에는 6 Plot distribution of cosine similarity 에 있는 코드를 실행하면 아래와 같이 나온다고 합니다.

 

 

7. Optimize the matrix using the training data provided

def embedding_multiplied_by_matrix(
    embedding: List[float], matrix: torch.tensor
) -> np.array:
    embedding_tensor = torch.tensor(embedding).float()
    modified_embedding = embedding_tensor @ matrix
    modified_embedding = modified_embedding.detach().numpy()
    return modified_embedding


# compute custom embeddings and new cosine similarities
def apply_matrix_to_embeddings_dataframe(matrix: torch.tensor, df: pd.DataFrame):
    for column in ["text_1_embedding", "text_2_embedding"]:
        df[f"{column}_custom"] = df[column].apply(
            lambda x: embedding_multiplied_by_matrix(x, matrix)
        )
    df["cosine_similarity_custom"] = df.apply(
        lambda row: cosine_similarity(
            row["text_1_embedding_custom"], row["text_2_embedding_custom"]
        ),
        axis=1,
    )

 

def optimize_matrix(
    modified_embedding_length: int = 2048,  # in my brief experimentation, bigger was better (2048 is length of babbage encoding)
    batch_size: int = 100,
    max_epochs: int = 100,
    learning_rate: float = 100.0,  # seemed to work best when similar to batch size - feel free to try a range of values
    dropout_fraction: float = 0.0,  # in my testing, dropout helped by a couple percentage points (definitely not necessary)
    df: pd.DataFrame = df,
    print_progress: bool = True,
    save_results: bool = True,
) -> torch.tensor:
    """Return matrix optimized to minimize loss on training data."""
    run_id = random.randint(0, 2 ** 31 - 1)  # (range is arbitrary)
    # convert from dataframe to torch tensors
    # e is for embedding, s for similarity label
    def tensors_from_dataframe(
        df: pd.DataFrame,
        embedding_column_1: str,
        embedding_column_2: str,
        similarity_label_column: str,
    ) -> Tuple[torch.tensor]:
        e1 = np.stack(np.array(df[embedding_column_1].values))
        e2 = np.stack(np.array(df[embedding_column_2].values))
        s = np.stack(np.array(df[similarity_label_column].astype("float").values))

        e1 = torch.from_numpy(e1).float()
        e2 = torch.from_numpy(e2).float()
        s = torch.from_numpy(s).float()

        return e1, e2, s

    e1_train, e2_train, s_train = tensors_from_dataframe(
        df[df["dataset"] == "train"], "text_1_embedding", "text_2_embedding", "label"
    )
    e1_test, e2_test, s_test = tensors_from_dataframe(
        df[df["dataset"] == "train"], "text_1_embedding", "text_2_embedding", "label"
    )

    # create dataset and loader
    dataset = torch.utils.data.TensorDataset(e1_train, e2_train, s_train)
    train_loader = torch.utils.data.DataLoader(
        dataset, batch_size=batch_size, shuffle=True
    )

    # define model (similarity of projected embeddings)
    def model(embedding_1, embedding_2, matrix, dropout_fraction=dropout_fraction):
        e1 = torch.nn.functional.dropout(embedding_1, p=dropout_fraction)
        e2 = torch.nn.functional.dropout(embedding_2, p=dropout_fraction)
        modified_embedding_1 = e1 @ matrix  # @ is matrix multiplication
        modified_embedding_2 = e2 @ matrix
        similarity = torch.nn.functional.cosine_similarity(
            modified_embedding_1, modified_embedding_2
        )
        return similarity

    # define loss function to minimize
    def mse_loss(predictions, targets):
        difference = predictions - targets
        return torch.sum(difference * difference) / difference.numel()

    # initialize projection matrix
    embedding_length = len(df["text_1_embedding"].values[0])
    matrix = torch.randn(
        embedding_length, modified_embedding_length, requires_grad=True
    )

    epochs, types, losses, accuracies, matrices = [], [], [], [], []
    for epoch in range(1, 1 + max_epochs):
        # iterate through training dataloader
        for a, b, actual_similarity in train_loader:
            # generate prediction
            predicted_similarity = model(a, b, matrix)
            # get loss and perform backpropagation
            loss = mse_loss(predicted_similarity, actual_similarity)
            loss.backward()
            # update the weights
            with torch.no_grad():
                matrix -= matrix.grad * learning_rate
                # set gradients to zero
                matrix.grad.zero_()
        # calculate test loss
        test_predictions = model(e1_test, e2_test, matrix)
        test_loss = mse_loss(test_predictions, s_test)

        # compute custom embeddings and new cosine similarities
        apply_matrix_to_embeddings_dataframe(matrix, df)

        # calculate test accuracy
        for dataset in ["train", "test"]:
            data = df[df["dataset"] == dataset]
            a, se = accuracy_and_se(data["cosine_similarity_custom"], data["label"])

            # record results of each epoch
            epochs.append(epoch)
            types.append(dataset)
            losses.append(loss.item() if dataset == "train" else test_loss.item())
            accuracies.append(a)
            matrices.append(matrix.detach().numpy())

            # optionally print accuracies
            if print_progress is True:
                print(
                    f"Epoch {epoch}/{max_epochs}: {dataset} accuracy: {a:0.1%} ± {1.96 * se:0.1%}"
                )

    data = pd.DataFrame(
        {"epoch": epochs, "type": types, "loss": losses, "accuracy": accuracies}
    )
    data["run_id"] = run_id
    data["modified_embedding_length"] = modified_embedding_length
    data["batch_size"] = batch_size
    data["max_epochs"] = max_epochs
    data["learning_rate"] = learning_rate
    data["dropout_fraction"] = dropout_fraction
    data[
        "matrix"
    ] = matrices  # saving every single matrix can get big; feel free to delete/change
    if save_results is True:
        data.to_csv(f"{run_id}_optimization_results.csv", index=False)

    return data
# example hyperparameter search
# I recommend starting with max_epochs=10 while initially exploring
results = []
max_epochs = 30
dropout_fraction = 0.2
for batch_size, learning_rate in [(10, 10), (100, 100), (1000, 1000)]:
    result = optimize_matrix(
        batch_size=batch_size,
        learning_rate=learning_rate,
        max_epochs=max_epochs,
        dropout_fraction=dropout_fraction,
        save_results=False,
    )
    results.append(result)
runs_df = pd.concat(results)

# plot training loss and test loss over time
px.line(
    runs_df,
    line_group="run_id",
    x="epoch",
    y="loss",
    color="type",
    hover_data=["batch_size", "learning_rate", "dropout_fraction"],
    facet_row="learning_rate",
    facet_col="batch_size",
    width=500,
).show()

# plot accuracy over time
px.line(
    runs_df,
    line_group="run_id",
    x="epoch",
    y="accuracy",
    color="type",
    hover_data=["batch_size", "learning_rate", "dropout_fraction"],
    facet_row="learning_rate",
    facet_col="batch_size",
    width=500,
).show()

 

이 부분은 7. Optimize the matrix using the training data provided 부분의 소스코드들 입니다.

 

embedding_multiplied_by_matrix() 함수는 임베딩 값과 matrix 값을 받아서 처리한 다음에 np.array 형식의 결과값을 반환합니다.

 

그 다음 함수는 apply_matrix_to_embeddings_dataframe() 함수로 기존의 임베딩 값과 새로운 cosine similarities 값으로 계산을 하는 일을 합니다.  여기에서 위의 함수인 embedding_multiplied_by_matrix() 함수를 호출한 후 그 결과 값을 받아서 처리합니다.

 

그 다음에도 여러 함수들이 있는데 따로 설명을 하면 너무 길어질 것 같네요.

각자 보면서 분석을 해야 할 것 같습니다.

 

여기까지 실행하면 아래와 같은 결과값을 볼 수 있습니다.

 

8. Plot the before & after, showing the results of the best matrix found during training

matrix 가 좋을 수록 similar pairs 와 dissimilar pairs를 더 명확하게 구분 할 수 있습니다.

 

# apply result of best run to original data
best_run = runs_df.sort_values(by="accuracy", ascending=False).iloc[0]
best_matrix = best_run["matrix"]
apply_matrix_to_embeddings_dataframe(best_matrix, df)

runs_df 는 위에서 정의한 df 변수 이름이고 그 다음의 sort_values() 는 pandas의 함수입니다.

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

 

pandas.DataFrame.sort_values — pandas 1.5.3 documentation

previous pandas.DataFrame.sort_index

pandas.pydata.org

데이터를 sorting 하는 일을 합니다.

이렇게 정렬된 matrix 데이터를 best_matrix에 넣습니다.

그리고 이 정렬된 matrix 값을 7번에서 만든 apply_matrix_to_embeddings_dataframe() 에서 처리하도록 합니다.

 
# plot similarity distribution BEFORE customization
px.histogram(
    df,
    x="cosine_similarity",
    color="label",
    barmode="overlay",
    width=500,
    facet_row="dataset",
).show()

test_df = df[df["dataset"] == "test"]
a, se = accuracy_and_se(test_df["cosine_similarity"], test_df["label"])
print(f"Test accuracy: {a:0.1%} ± {1.96 * se:0.1%}")

# plot similarity distribution AFTER customization
px.histogram(
    df,
    x="cosine_similarity_custom",
    color="label",
    barmode="overlay",
    width=500,
    facet_row="dataset",
).show()

a, se = accuracy_and_se(test_df["cosine_similarity_custom"], test_df["label"])
print(f"Test accuracy after customization: {a:0.1%} ± {1.96 * se:0.1%}")

 

여기서 이용한 함수는 6번에서 만든 accuracy_and_se() 함수 입니다. 정확도를 계산하는 함수였습니다.

 

여기서는 customization이 되기 전의 데이터인 cosine_similarity와 customization이 된 이후의 데이터인 cosine_similarrity_custom에 대해 accuracy_and_se() 함수로 정확도를 계산 한 값을 비교할 수 있도록 해 줍니다.

 

보시는 바와 같이 커스터마이징을 한 데이터가 정확도가 훨씬 높습니다.

 

best_matrix  # this is what you can multiply your embeddings by

이렇게 해서 얻은 best_matrix 를 가지고 사용하면 훨씬 더 좋은 결과를 얻을 수 있습니다.

 

여기까지가 Customizing embeddings 에 대한 내용이었습니다.

 

아주 복잡한 내용 이었던 것 같습니다.

 

몇번 실행해 보면서 코드를 분석해야지 어느 정도 소화를 할 수 있을 것 같습니다.

 

참고로 저는 기존에 1만줄이 있던 소스 파일을 2천줄로 줄여서 사용했습니다.

혹시 도움이 되실까 해서 2천줄로 줄인 csv 파일을 업로드 합니다.

 

snli_1.0_train_2k.csv
1.46MB

그리고 아래는 이 단원을 공부하면서 작성한 소스 코드 전체 입니다.

 

# imports
from typing import List, Tuple  # for type hints

import openai
import numpy as np  # for manipulating arrays
import pandas as pd  # for manipulating data in dataframes
import pickle  # for saving the embeddings cache
import plotly.express as px  # for plots
import random  # for generating run IDs
from sklearn.model_selection import train_test_split  # for splitting train & test data
import torch  # for matrix optimization

from openai.embeddings_utils import get_embedding, cosine_similarity  # for embeddings

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

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

# input parameters
embedding_cache_path = "data/snli_embedding_cache.pkl"  # embeddings will be saved/loaded here
#default_embedding_engine = "babbage-similarity"  # choice of: ada, babbage, curie, davinci
#default_embedding_engine = "ada-similarity"
default_embedding_engine = "text-embedding-ada-002"
num_pairs_to_embed = 1000  # 1000 is arbitrary - I've gotten it to work with as little as ~100
local_dataset_path = "data/snli_1.0_train_2k.csv"  # download from: https://nlp.stanford.edu/projects/snli/


def process_input_data(df: pd.DataFrame) -> pd.DataFrame:
    # you can customize this to preprocess your own dataset
    # output should be a dataframe with 3 columns: text_1, text_2, label (1 for similar, -1 for dissimilar)
    df["label"] = df["gold_label"]
    df = df[df["label"].isin(["entailment"])]
    df["label"] = df["label"].apply(lambda x: {"entailment": 1, "contradiction": -1}[x])
    df = df.rename(columns={"sentence1": "text_1", "sentence2": "text_2"})
    df = df[["text_1", "text_2", "label"]]
    df = df.head(num_pairs_to_embed)
    return df
    
# load data
df = pd.read_csv(local_dataset_path)

# process input data
df = process_input_data(df)  # this demonstrates training data containing only positives

# view data
result = df.head()

print(result)

# split data into train and test sets
test_fraction = 0.5  # 0.5 is fairly arbitrary
random_seed = 123  # random seed is arbitrary, but is helpful in reproducibility
train_df, test_df = train_test_split(
    df, test_size=test_fraction, stratify=df["label"], random_state=random_seed
)
train_df.loc[:, "dataset"] = "train"
test_df.loc[:, "dataset"] = "test"

# generate negatives
def dataframe_of_negatives(dataframe_of_positives: pd.DataFrame) -> pd.DataFrame:
    """Return dataframe of negative pairs made by combining elements of positive pairs."""
    texts = set(dataframe_of_positives["text_1"].values) | set(
        dataframe_of_positives["text_2"].values
    )
    all_pairs = {(t1, t2) for t1 in texts for t2 in texts if t1 < t2}
    positive_pairs = set(
        tuple(text_pair)
        for text_pair in dataframe_of_positives[["text_1", "text_2"]].values
    )
    negative_pairs = all_pairs - positive_pairs
    df_of_negatives = pd.DataFrame(list(negative_pairs), columns=["text_1", "text_2"])
    df_of_negatives["label"] = -1
    return df_of_negatives
    
negatives_per_positive = (
    1  # it will work at higher values too, but more data will be slower
)
# generate negatives for training dataset
train_df_negatives = dataframe_of_negatives(train_df)
train_df_negatives["dataset"] = "train"
# generate negatives for test dataset
test_df_negatives = dataframe_of_negatives(test_df)
test_df_negatives["dataset"] = "test"
# sample negatives and combine with positives
train_df = pd.concat(
    [
        train_df,
        train_df_negatives.sample(
            n=len(train_df) * negatives_per_positive, random_state=random_seed
        ),
    ]
)
test_df = pd.concat(
    [
        test_df,
        test_df_negatives.sample(
            n=len(test_df) * negatives_per_positive, random_state=random_seed
        ),
    ]
)

df = pd.concat([train_df, test_df])

# establish a cache of embeddings to avoid recomputing
# cache is a dict of tuples (text, engine) -> embedding
try:
    with open(embedding_cache_path, "rb") as f:
        embedding_cache = pickle.load(f)
except FileNotFoundError:
    precomputed_embedding_cache_path = "https://cdn.openai.com/API/examples/data/snli_embedding_cache.pkl"
    embedding_cache = pd.read_pickle(precomputed_embedding_cache_path)


# this function will get embeddings from the cache and save them there afterward
def get_embedding_with_cache(
    text: str,
    engine: str = default_embedding_engine,
    embedding_cache: dict = embedding_cache,
    embedding_cache_path: str = embedding_cache_path,
) -> list:
    print(f"Getting embedding for {text}")
    if (text, engine) not in embedding_cache.keys():
        # if not in cache, call API to get embedding
        embedding_cache[(text, engine)] = get_embedding(text, engine)
        # save embeddings cache to disk after each update
        with open(embedding_cache_path, "wb") as embedding_cache_file:
            pickle.dump(embedding_cache, embedding_cache_file)
    return embedding_cache[(text, engine)]


# create column of embeddings
for column in ["text_1", "text_2"]:
    df[f"{column}_embedding"] = df[column].apply(get_embedding_with_cache)

# create column of cosine similarity between embeddings
df["cosine_similarity"] = df.apply(
    lambda row: cosine_similarity(row["text_1_embedding"], row["text_2_embedding"]),
    axis=1,
)

# calculate accuracy (and its standard error) of predicting label=1 if similarity>x
# x is optimized by sweeping from -1 to 1 in steps of 0.01
def accuracy_and_se(cosine_similarity: float, labeled_similarity: int) -> Tuple[float]:
    accuracies = []
    for threshold_thousandths in range(-1000, 1000, 1):
        threshold = threshold_thousandths / 1000
        total = 0
        correct = 0
        for cs, ls in zip(cosine_similarity, labeled_similarity):
            total += 1
            if cs > threshold:
                prediction = 1
            else:
                prediction = -1
            if prediction == ls:
                correct += 1
        accuracy = correct / total
        accuracies.append(accuracy)
    a = max(accuracies)
    n = len(cosine_similarity)
    standard_error = (a * (1 - a) / n) ** 0.5  # standard error of binomial
    return a, standard_error


# check that training and test sets are balanced
px.histogram(
    df,
    x="cosine_similarity",
    color="label",
    barmode="overlay",
    width=500,
    facet_row="dataset",
).show()

for dataset in ["train", "test"]:
    data = df[df["dataset"] == dataset]
    a, se = accuracy_and_se(data["cosine_similarity"], data["label"])
    print(f"{dataset} accuracy: {a:0.1%} ± {1.96 * se:0.1%}")

def embedding_multiplied_by_matrix(
    embedding: List[float], matrix: torch.tensor
) -> np.array:
    embedding_tensor = torch.tensor(embedding).float()
    modified_embedding = embedding_tensor @ matrix
    modified_embedding = modified_embedding.detach().numpy()
    return modified_embedding


# compute custom embeddings and new cosine similarities
def apply_matrix_to_embeddings_dataframe(matrix: torch.tensor, df: pd.DataFrame):
    for column in ["text_1_embedding", "text_2_embedding"]:
        df[f"{column}_custom"] = df[column].apply(
            lambda x: embedding_multiplied_by_matrix(x, matrix)
        )
    df["cosine_similarity_custom"] = df.apply(
        lambda row: cosine_similarity(
            row["text_1_embedding_custom"], row["text_2_embedding_custom"]
        ),
        axis=1,
    )
    
def optimize_matrix(
    modified_embedding_length: int = 2048,  # in my brief experimentation, bigger was better (2048 is length of babbage encoding)
    batch_size: int = 100,
    max_epochs: int = 100,
    learning_rate: float = 100.0,  # seemed to work best when similar to batch size - feel free to try a range of values
    dropout_fraction: float = 0.0,  # in my testing, dropout helped by a couple percentage points (definitely not necessary)
    df: pd.DataFrame = df,
    print_progress: bool = True,
    save_results: bool = True,
) -> torch.tensor:
    """Return matrix optimized to minimize loss on training data."""
    run_id = random.randint(0, 2 ** 31 - 1)  # (range is arbitrary)
    # convert from dataframe to torch tensors
    # e is for embedding, s for similarity label
    def tensors_from_dataframe(
        df: pd.DataFrame,
        embedding_column_1: str,
        embedding_column_2: str,
        similarity_label_column: str,
    ) -> Tuple[torch.tensor]:
        e1 = np.stack(np.array(df[embedding_column_1].values))
        e2 = np.stack(np.array(df[embedding_column_2].values))
        s = np.stack(np.array(df[similarity_label_column].astype("float").values))

        e1 = torch.from_numpy(e1).float()
        e2 = torch.from_numpy(e2).float()
        s = torch.from_numpy(s).float()

        return e1, e2, s

    e1_train, e2_train, s_train = tensors_from_dataframe(
        df[df["dataset"] == "train"], "text_1_embedding", "text_2_embedding", "label"
    )
    e1_test, e2_test, s_test = tensors_from_dataframe(
        df[df["dataset"] == "train"], "text_1_embedding", "text_2_embedding", "label"
    )

    # create dataset and loader
    dataset = torch.utils.data.TensorDataset(e1_train, e2_train, s_train)
    train_loader = torch.utils.data.DataLoader(
        dataset, batch_size=batch_size, shuffle=True
    )

    # define model (similarity of projected embeddings)
    def model(embedding_1, embedding_2, matrix, dropout_fraction=dropout_fraction):
        e1 = torch.nn.functional.dropout(embedding_1, p=dropout_fraction)
        e2 = torch.nn.functional.dropout(embedding_2, p=dropout_fraction)
        modified_embedding_1 = e1 @ matrix  # @ is matrix multiplication
        modified_embedding_2 = e2 @ matrix
        similarity = torch.nn.functional.cosine_similarity(
            modified_embedding_1, modified_embedding_2
        )
        return similarity

    # define loss function to minimize
    def mse_loss(predictions, targets):
        difference = predictions - targets
        return torch.sum(difference * difference) / difference.numel()

    # initialize projection matrix
    embedding_length = len(df["text_1_embedding"].values[0])
    matrix = torch.randn(
        embedding_length, modified_embedding_length, requires_grad=True
    )

    epochs, types, losses, accuracies, matrices = [], [], [], [], []
    for epoch in range(1, 1 + max_epochs):
        # iterate through training dataloader
        for a, b, actual_similarity in train_loader:
            # generate prediction
            predicted_similarity = model(a, b, matrix)
            # get loss and perform backpropagation
            loss = mse_loss(predicted_similarity, actual_similarity)
            loss.backward()
            # update the weights
            with torch.no_grad():
                matrix -= matrix.grad * learning_rate
                # set gradients to zero
                matrix.grad.zero_()
        # calculate test loss
        test_predictions = model(e1_test, e2_test, matrix)
        test_loss = mse_loss(test_predictions, s_test)

        # compute custom embeddings and new cosine similarities
        apply_matrix_to_embeddings_dataframe(matrix, df)

        # calculate test accuracy
        for dataset in ["train", "test"]:
            data = df[df["dataset"] == dataset]
            a, se = accuracy_and_se(data["cosine_similarity_custom"], data["label"])

            # record results of each epoch
            epochs.append(epoch)
            types.append(dataset)
            losses.append(loss.item() if dataset == "train" else test_loss.item())
            accuracies.append(a)
            matrices.append(matrix.detach().numpy())

            # optionally print accuracies
            if print_progress is True:
                print(
                    f"Epoch {epoch}/{max_epochs}: {dataset} accuracy: {a:0.1%} ± {1.96 * se:0.1%}"
                )

    data = pd.DataFrame(
        {"epoch": epochs, "type": types, "loss": losses, "accuracy": accuracies}
    )
    data["run_id"] = run_id
    data["modified_embedding_length"] = modified_embedding_length
    data["batch_size"] = batch_size
    data["max_epochs"] = max_epochs
    data["learning_rate"] = learning_rate
    data["dropout_fraction"] = dropout_fraction
    data[
        "matrix"
    ] = matrices  # saving every single matrix can get big; feel free to delete/change
    if save_results is True:
        data.to_csv(f"{run_id}_optimization_results.csv", index=False)

    return data
    
# example hyperparameter search
# I recommend starting with max_epochs=10 while initially exploring
results = []
max_epochs = 30
dropout_fraction = 0.2
for batch_size, learning_rate in [(10, 10), (100, 100), (1000, 1000)]:
    result = optimize_matrix(
        batch_size=batch_size,
        learning_rate=learning_rate,
        max_epochs=max_epochs,
        dropout_fraction=dropout_fraction,
        save_results=False,
    )
    results.append(result)
    
runs_df = pd.concat(results)

# plot training loss and test loss over time
px.line(
    runs_df,
    line_group="run_id",
    x="epoch",
    y="loss",
    color="type",
    hover_data=["batch_size", "learning_rate", "dropout_fraction"],
    facet_row="learning_rate",
    facet_col="batch_size",
    width=500,
).show()

# plot accuracy over time
px.line(
    runs_df,
    line_group="run_id",
    x="epoch",
    y="accuracy",
    color="type",
    hover_data=["batch_size", "learning_rate", "dropout_fraction"],
    facet_row="learning_rate",
    facet_col="batch_size",
    width=500,
).show()

# apply result of best run to original data
best_run = runs_df.sort_values(by="accuracy", ascending=False).iloc[0]
best_matrix = best_run["matrix"]
apply_matrix_to_embeddings_dataframe(best_matrix, df)

# plot similarity distribution BEFORE customization
px.histogram(
    df,
    x="cosine_similarity",
    color="label",
    barmode="overlay",
    width=500,
    facet_row="dataset",
).show()

test_df = df[df["dataset"] == "test"]
a, se = accuracy_and_se(test_df["cosine_similarity"], test_df["label"])
print(f"Test accuracy: {a:0.1%} ± {1.96 * se:0.1%}")

# plot similarity distribution AFTER customization
px.histogram(
    df,
    x="cosine_similarity_custom",
    color="label",
    barmode="overlay",
    width=500,
    facet_row="dataset",
).show()

a, se = accuracy_and_se(test_df["cosine_similarity_custom"], test_df["label"])
print(f"Test accuracy after customization: {a:0.1%} ± {1.96 * se:0.1%}")

best_matrix  # this is what you can multiply your embeddings by
반응형


반응형

오늘 다룰 예제는 Rocommendation 관련 예제입니다.

https://github.com/openai/openai-cookbook/blob/main/examples/Recommendation_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

바로 내가 어느 물건을 사면 그것과 비슷한 다른 물건에도 관심이 있을 것이라고 판단하고 광고가 나오고.

어떤 영화를 보면 그런 영화를 좋아 하는 사람들이 좋아 할만한 영화들을 추천하고,

어떤 것에 관심을 표현하면 그것을 근거로 다른 관심 가질 문한 것들을 추천하는 그런 기능입니다.

 

이 Recommendation은 검색기능과 유사한데 어떤 텍스트가 입력값이 아니라 입력값이 세트로 된 아이템들이라는 것입니다.

여기서도 cosine similarity 점수를 사용합니다. 

 

이번 예제에서는 권장(recommend) 할만한 비슷한 아이템들을 임베딩을 사용해서 찾는 방법을 보여 줄 것입니다.

여기서 다를 데이터 세트는  AG's corpus of news articles 에 있습니다.

 

오늘의 예제에서는 주어진 기사와 관계된 다른 기사들을 보여 줄 겁니다.

 

1. Imports

여기서 사용할 예제는 pandas와 pickle 입니다.

둘 다 이전에 설명 했습니다.

pickle 모듈은 지난 글 예제에 있었는데 정작 사용은 하지 않았습니다. 그 때 설명 했는데요.

아래 내용이 그때 설명한 내용입니다.

 

 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

 

# imports
import pandas as pd
import pickle

from openai.embeddings_utils import (
    get_embedding,
    distances_from_embeddings,
    tsne_components_from_embeddings,
    chart_from_components,
    indices_of_nearest_neighbors_from_distances,
)

# constants
EMBEDDING_MODEL = "text-embedding-ada-002"

 

그리고 embeddings_utils에서 get_embedding 등 여러 api 함수들을 import 합니다.

 

embeddings_utils.py 파일은 아래 페이지에서 보실 수 있습니다.

 

https://github.com/openai/openai-python/blob/main/openai/embeddings_utils.py

 

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

각 api 메소드들은 이렇게 생겼습니다.

get_embedding()

 

distances_from_embeddings()

tsne_components_from_embeddings()

 

chart_from_components()

indices_of_nearest_neighbors_from_distances()

 

잠깐 어떤 일을 하는 api 함수들인지 살펴 보고 가면 나중에 소스코드를 이해하는데 도움이 될 것입니다.

 

 

2. Load data

이제 위에서 말했던 csv 형식으로 된 소스 파일을 다룰 차례입니다.

 

# load data (full dataset available at http://groups.di.unipi.it/~gulli/AG_corpus_of_news_articles.html)
dataset_path = "data/AG_news_samples.csv"
df = pd.read_csv(dataset_path)

# print dataframe
n_examples = 5
df.head(n_examples)

위에 주석을 보면  http://groups.di.unipi.it/~gulli/AG_corpus_of_news_articles.html 에 데이터 세트가 있다고 하는데 이곳에는 bz2 형식의 파일만 있습니다.

그 파일을 unzip 했더니 그 안에 120만개의 아이템들이 있더라구요.

bz2 를 csv 로 convert 시키는 온라인 툴에서 변환을 시도 했는데 너무 크기가 커서 실패 했습니다.

그래서 데이터소스가 없어서 이번 예제는 실행은 못 해 보고 소스만 분석하겠습니다.

 

위의 소스 코드는 데이터를 pandas의 read_csv() 로 읽어서 첫번째 5개만 df에 담는 역할을 합니다.

이런 결과를 얻을 수 있습니다.

 

 

# print the title, description, and label of each example
for idx, row in df.head(n_examples).iterrows():
    print("")
    print(f"Title: {row['title']}")
    print(f"Description: {row['description']}")
    print(f"Label: {row['label']}")

 

그 다음은 Title, Description, Lable 을 print 하는 부분 입니다.

여기까지 만들고 실행하면 아래 결과를 얻을 수 있습니다.

 

Title: World Briefings
Description: BRITAIN: BLAIR WARNS OF CLIMATE THREAT Prime Minister Tony Blair urged the international community to consider global warming a dire threat and agree on a plan of action to curb the  quot;alarming quot; growth of greenhouse gases.
Label: World

Title: Nvidia Puts a Firewall on a Motherboard (PC World)
Description: PC World - Upcoming chip set will include built-in security features for your PC.
Label: Sci/Tech

Title: Olympic joy in Greek, Chinese press
Description: Newspapers in Greece reflect a mixture of exhilaration that the Athens Olympics proved successful, and relief that they passed off without any major setback.
Label: Sports

Title: U2 Can iPod with Pictures
Description: SAN JOSE, Calif. -- Apple Computer (Quote, Chart) unveiled a batch of new iPods, iTunes software and promos designed to keep it atop the heap of digital music players.
Label: Sci/Tech

Title: The Dream Factory
Description: Any product, any shape, any size -- manufactured on your desktop! The future is the fabricator. By Bruce Sterling from Wired magazine.
Label: Sci/Tech

여기까지 진행하면 아래와 같은 소스코드를 얻을 수 있을 겁니다.

 

# imports
import pandas as pd
import pickle
import openai

from openai.embeddings_utils import (
    get_embedding,
    distances_from_embeddings,
    tsne_components_from_embeddings,
    chart_from_components,
    indices_of_nearest_neighbors_from_distances,
)

# constants
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')

# load data (full dataset available at http://groups.di.unipi.it/~gulli/AG_corpus_of_news_articles.html)
dataset_path = "data/AG_news_samples.csv"
df = pd.read_csv(dataset_path)

# print dataframe
n_examples = 5
df.head(n_examples)

# print the title, description, and label of each example
for idx, row in df.head(n_examples).iterrows():
    print("")
    print(f"Title: {row['title']}")
    print(f"Description: {row['description']}")
    print(f"Label: {row['label']}")

 

3. Build cache to save embeddings

이 기사들에 대해 임베딩 값을 얻기 전에 임베딩을 값을 저장할 캐시를 세팅해 보겠습니다. 

이렇게 얻은 임베딩을 저장해서 재 사용하면 이 값을 얻기 위해 openai api를 call 하지 않아도 되기 때문에 비용을 절감할 수 있습니다.

 

이 캐시는 dictionary로  (text, model) 의 tuples로 매핑되 있습니다.

이 캐시는 Python pickle 파일로 저장 될 것입니다.

 

# establish a cache of embeddings to avoid recomputing
# cache is a dict of tuples (text, model) -> embedding, saved as a pickle file

# set path to embedding cache
embedding_cache_path = "data/recommendations_embeddings_cache.pkl"

# load the cache if it exists, and save a copy to disk
try:
    embedding_cache = pd.read_pickle(embedding_cache_path)
except FileNotFoundError:
    embedding_cache = {}
with open(embedding_cache_path, "wb") as embedding_cache_file:
    pickle.dump(embedding_cache, embedding_cache_file)

# define a function to retrieve embeddings from the cache if present, and otherwise request via the API
def embedding_from_string(
    string: str,
    model: str = EMBEDDING_MODEL,
    embedding_cache=embedding_cache
) -> list:
    """Return embedding of given string, using a cache to avoid recomputing."""
    if (string, model) not in embedding_cache.keys():
        embedding_cache[(string, model)] = get_embedding(string, model)
        with open(embedding_cache_path, "wb") as embedding_cache_file:
            pickle.dump(embedding_cache, embedding_cache_file)
    return embedding_cache[(string, model)]

 

처음에 저장될 위치와 pkl 파일 이름을 embedding_cache_path 변수에 담습니다.

다음은 이 cache가 있으면 카피를 저장하는 보분입니다.

pandas의 read_pickle() 함수를 통해 읽습니다. (embedding_cache에 담음)

이 파일을 오픈할 때 사용한 wb 는 파일을 binary format으로 오픈하고 쓰기 기능이 있다는 의미 입니다.

 

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

입력값으로는 string과 openai의 모델명 그리고 embedding_cache를 받습니다.

출력은 리스트 형식입니다.

 

다음 if 문은 sriing과 model 이 embedding_cache.keys() 에 없다면 get_embedding()을 통해서 임베딩 값을 얻는 일을 합니다. 여기서도 파일은 바이너리 형태로 열고 쓰기 기능이 허락돼 있습니다.

pickle.dump()는 해당 내용을 파일에 저장할 때사용하는 pickle 모듈의 api 함수 입니다.

저장할 값과 그 값이 저장될 파일이 파라미터로 전달 됩니다.

https://www.digitalocean.com/community/tutorials/python-pickle-example

 

Python Pickle Example | DigitalOcean

 

www.digitalocean.com

그리고 나서 이 저장된 값을 return 합니다.

 

# as an example, take the first description from the dataset
example_string = df["description"].values[0]
print(f"\nExample string: {example_string}")

# print the first 10 dimensions of the embedding
example_embedding = embedding_from_string(example_string)
print(f"\nExample embedding: {example_embedding[:10]}...")

이 부분은 위에 작성한 스크립트가 잘 작동하는지 print 해 보는 겁니다.

 

여기까지 작성 한 것을 실행 하면 아래 출력을 얻을 수 있습니다.

 

 

4. Recommend similar articles based on embeddings

비슷한 기사를 찾기 위해서는 아래 3단계를 거쳐야 합니다.

1. 모든 기사의 description들에 대해 similarity 임베딩 값을 얻는다.

2. 소스 타이틀과 다른 모든 기사들간의 distance를 계산한다.

3. source title에 다른 기사들의 closest를 프린트 한다.

def print_recommendations_from_strings(
    strings: list[str],
    index_of_source_string: int,
    k_nearest_neighbors: int = 1,
    model=EMBEDDING_MODEL,
) -> list[int]:
    """Print out the k nearest neighbors of a given string."""
    # get embeddings for all strings
    embeddings = [embedding_from_string(string, model=model) for string in strings]
    # get the embedding of the source string
    query_embedding = embeddings[index_of_source_string]
    # get distances between the source embedding and other embeddings (function from embeddings_utils.py)
    distances = distances_from_embeddings(query_embedding, embeddings, distance_metric="cosine")
    # get indices of nearest neighbors (function from embeddings_utils.py)
    indices_of_nearest_neighbors = indices_of_nearest_neighbors_from_distances(distances)

    # print out source string
    query_string = strings[index_of_source_string]
    print(f"Source string: {query_string}")
    # print out its k nearest neighbors
    k_counter = 0
    for i in indices_of_nearest_neighbors:
        # skip any strings that are identical matches to the starting string
        if query_string == strings[i]:
            continue
        # stop after printing out k articles
        if k_counter >= k_nearest_neighbors:
            break
        k_counter += 1

        # print out the similar strings and their distances
        print(
            f"""
        --- Recommendation #{k_counter} (nearest neighbor {k_counter} of {k_nearest_neighbors}) ---
        String: {strings[i]}
        Distance: {distances[i]:0.3f}"""
        )

    return indices_of_nearest_neighbors

 

이 소스 코드가 그 일을 합니다.

 

모든 string들에 대한 임베딩 값들을 받아서 distance를 구합니다. (distances_from_embeddings())

그리고 나서 가장 가까운 neighbor들을 구합니다. (indices_of_nearest_neighbors_from_distances())

 

그리고 query string을 print 합니다.

그리고 indices_of_nearest_neighbors 에 있는 요소들 만큼 for 루프를 돌리면서 Recommendation을 String, Distance 정보와 함께 print 합니다.

최종적으로 indices_of_nearest_neighbors를 return 합니다.

 

5. Example recommendations

우선 Tony Blair에 대한 유사한 아티클들을 먼저 보죠.

article_descriptions = df["description"].tolist()

tony_blair_articles = print_recommendations_from_strings(
    strings=article_descriptions,  # let's base similarity off of the article description
    index_of_source_string=0,  # let's look at articles similar to the first one about Tony Blair
    k_nearest_neighbors=5,  # let's look at the 5 most similar articles
)

이렇게 하면 다음과 같은 정보를 얻을 수 있습니다.

 

 

첫 4개 기사에 토니 블레어가 언급 돼 있고 다섯번째에는 런던발 기후 변화에 대한 내용이 있습니다. 이것도 토니 블레어와 연관이 있다고 얘기할 수 있겠네요.

 

그러면 두번째 주제인 NVIDIA에 대한 결과물을 보겠습니다.

 

chipset_security_articles = print_recommendations_from_strings(
    strings=article_descriptions,  # let's base similarity off of the article description
    index_of_source_string=1,  # let's look at articles similar to the second one about a more secure chipset
    k_nearest_neighbors=5,  # let's look at the 5 most similar articles
)

 

결과를 보면 #1이 다른 결과물들 보다 가장 유사성이 큰 것을 볼 수 있습니다. (거리가 가깝다)

그 내용도 주어진 주제와 가장 가깝습니다.

 

 

Appendix: Using embeddings in more sophisticated recommenders

이 추천 시스템을 빌드하기 위한 좀 더 정교한 방법은 항목의 인기도, 사용자 클릭 데이터 같은 수많은 signal들을 가지고 machine learning 모델을 훈련 시키는 것입니다.

추천 시스템에서도 임베딩은 아주 유용하게 이용될 수 있습니다.

아직 기존의 유저 데이터가 없는 신제품에 대한 정보 같은 것들에 대해서 특히 이 임베딩은 잘 사용될 수 있습니다.

 

Appendix: Using embeddings to visualize similar articles

이 임베딩을 시각화 할 수도 있습니다. t-SNE 혹은 PCA와 같은 기술을 이용해서 임베딩을 2차원 또는 3차원 챠트로 만들 수 있습니다.

여기서는 t-SNE를 사용해서 모든 기사 설명을 시각화 해 봅니다.

(이와 관련된 결과물은 실행 때마다 조금씩 달라질 수 있습니다.)

 

# get embeddings for all article descriptions
embeddings = [embedding_from_string(string) for string in article_descriptions]
# compress the 2048-dimensional embeddings into 2 dimensions using t-SNE
tsne_components = tsne_components_from_embeddings(embeddings)
# get the article labels for coloring the chart
labels = df["label"].tolist()

chart_from_components(
    components=tsne_components,
    labels=labels,
    strings=article_descriptions,
    width=600,
    height=500,
    title="t-SNE components of article descriptions",
)

 

다음은 source article, nearest neighbors 혹은 그 외 다른 것인지에 따라 다른 색으로 나타내 보는 코드 입니다.

 

# create labels for the recommended articles
def nearest_neighbor_labels(
    list_of_indices: list[int],
    k_nearest_neighbors: int = 5
) -> list[str]:
    """Return a list of labels to color the k nearest neighbors."""
    labels = ["Other" for _ in list_of_indices]
    source_index = list_of_indices[0]
    labels[source_index] = "Source"
    for i in range(k_nearest_neighbors):
        nearest_neighbor_index = list_of_indices[i + 1]
        labels[nearest_neighbor_index] = f"Nearest neighbor (top {k_nearest_neighbors})"
    return labels


tony_blair_labels = nearest_neighbor_labels(tony_blair_articles, k_nearest_neighbors=5)
chipset_security_labels = nearest_neighbor_labels(chipset_security_articles, k_nearest_neighbors=5
)

 

# a 2D chart of nearest neighbors of the Tony Blair article
chart_from_components(
    components=tsne_components,
    labels=tony_blair_labels,
    strings=article_descriptions,
    width=600,
    height=500,
    title="Nearest neighbors of the Tony Blair article",
    category_orders={"label": ["Other", "Nearest neighbor (top 5)", "Source"]},
)
# a 2D chart of nearest neighbors of the chipset security article
chart_from_components(
    components=tsne_components,
    labels=chipset_security_labels,
    strings=article_descriptions,
    width=600,
    height=500,
    title="Nearest neighbors of the chipset security article",
    category_orders={"label": ["Other", "Nearest neighbor (top 5)", "Source"]},
)

 

이런 과정을 통해 결과를 시각화 하면 좀 더 다양한 정보들을 얻을 수 있습니다.

 

이번 예제는 여기서 사용된 소스 데이터를 구하지 못해서 직접 실행은 못해 봤네요.

 

다음에 소스데이터를 구할 기회가 생기면 한번 직접 실행 해 봐야 겠습니다.

 

이 예제를 전부 완성하면 아래와 같은 소스 코드가 될 것입니다.

 

# imports
import pandas as pd
import pickle
import openai

from openai.embeddings_utils import (
    get_embedding,
    distances_from_embeddings,
    tsne_components_from_embeddings,
    chart_from_components,
    indices_of_nearest_neighbors_from_distances,
)

# constants
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')

# load data (full dataset available at http://groups.di.unipi.it/~gulli/AG_corpus_of_news_articles.html)
dataset_path = "data/AG_news_samples.csv"
df = pd.read_csv(dataset_path)

# print dataframe
n_examples = 5
df.head(n_examples)

# print the title, description, and label of each example
for idx, row in df.head(n_examples).iterrows():
    print("")
    print(f"Title: {row['title']}")
    print(f"Description: {row['description']}")
    print(f"Label: {row['label']}")
    
# establish a cache of embeddings to avoid recomputing
# cache is a dict of tuples (text, model) -> embedding, saved as a pickle file

# set path to embedding cache
embedding_cache_path = "data/recommendations_embeddings_cache.pkl"

# load the cache if it exists, and save a copy to disk
try:
    embedding_cache = pd.read_pickle(embedding_cache_path)
except FileNotFoundError:
    embedding_cache = {}
with open(embedding_cache_path, "wb") as embedding_cache_file:
    pickle.dump(embedding_cache, embedding_cache_file)

# define a function to retrieve embeddings from the cache if present, and otherwise request via the API
def embedding_from_string(
    string: str,
    model: str = EMBEDDING_MODEL,
    embedding_cache=embedding_cache
) -> list:
    """Return embedding of given string, using a cache to avoid recomputing."""
    if (string, model) not in embedding_cache.keys():
        embedding_cache[(string, model)] = get_embedding(string, model)
        with open(embedding_cache_path, "wb") as embedding_cache_file:
            pickle.dump(embedding_cache, embedding_cache_file)
    return embedding_cache[(string, model)]
    
# as an example, take the first description from the dataset
example_string = df["description"].values[0]
print(f"\nExample string: {example_string}")

# print the first 10 dimensions of the embedding
example_embedding = embedding_from_string(example_string)
print(f"\nExample embedding: {example_embedding[:10]}...")

def print_recommendations_from_strings(
    strings: list[str],
    index_of_source_string: int,
    k_nearest_neighbors: int = 1,
    model=EMBEDDING_MODEL,
) -> list[int]:
    """Print out the k nearest neighbors of a given string."""
    # get embeddings for all strings
    embeddings = [embedding_from_string(string, model=model) for string in strings]
    # get the embedding of the source string
    query_embedding = embeddings[index_of_source_string]
    # get distances between the source embedding and other embeddings (function from embeddings_utils.py)
    distances = distances_from_embeddings(query_embedding, embeddings, distance_metric="cosine")
    # get indices of nearest neighbors (function from embeddings_utils.py)
    indices_of_nearest_neighbors = indices_of_nearest_neighbors_from_distances(distances)

    # print out source string
    query_string = strings[index_of_source_string]
    print(f"Source string: {query_string}")
    # print out its k nearest neighbors
    k_counter = 0
    for i in indices_of_nearest_neighbors:
        # skip any strings that are identical matches to the starting string
        if query_string == strings[i]:
            continue
        # stop after printing out k articles
        if k_counter >= k_nearest_neighbors:
            break
        k_counter += 1

        # print out the similar strings and their distances
        print(
            f"""
        --- Recommendation #{k_counter} (nearest neighbor {k_counter} of {k_nearest_neighbors}) ---
        String: {strings[i]}
        Distance: {distances[i]:0.3f}"""
        )

    return indices_of_nearest_neighbors
    
 article_descriptions = df["description"].tolist()

tony_blair_articles = print_recommendations_from_strings(
    strings=article_descriptions,  # let's base similarity off of the article description
    index_of_source_string=0,  # let's look at articles similar to the first one about Tony Blair
    k_nearest_neighbors=5,  # let's look at the 5 most similar articles
)

chipset_security_articles = print_recommendations_from_strings(
    strings=article_descriptions,  # let's base similarity off of the article description
    index_of_source_string=1,  # let's look at articles similar to the second one about a more secure chipset
    k_nearest_neighbors=5,  # let's look at the 5 most similar articles
)

# get embeddings for all article descriptions
embeddings = [embedding_from_string(string) for string in article_descriptions]
# compress the 2048-dimensional embeddings into 2 dimensions using t-SNE
tsne_components = tsne_components_from_embeddings(embeddings)
# get the article labels for coloring the chart
labels = df["label"].tolist()

chart_from_components(
    components=tsne_components,
    labels=labels,
    strings=article_descriptions,
    width=600,
    height=500,
    title="t-SNE components of article descriptions",
)

# create labels for the recommended articles
def nearest_neighbor_labels(
    list_of_indices: list[int],
    k_nearest_neighbors: int = 5
) -> list[str]:
    """Return a list of labels to color the k nearest neighbors."""
    labels = ["Other" for _ in list_of_indices]
    source_index = list_of_indices[0]
    labels[source_index] = "Source"
    for i in range(k_nearest_neighbors):
        nearest_neighbor_index = list_of_indices[i + 1]
        labels[nearest_neighbor_index] = f"Nearest neighbor (top {k_nearest_neighbors})"
    return labels


tony_blair_labels = nearest_neighbor_labels(tony_blair_articles, k_nearest_neighbors=5)
chipset_security_labels = nearest_neighbor_labels(chipset_security_articles, k_nearest_neighbors=5
)

# a 2D chart of nearest neighbors of the Tony Blair article
chart_from_components(
    components=tsne_components,
    labels=tony_blair_labels,
    strings=article_descriptions,
    width=600,
    height=500,
    title="Nearest neighbors of the Tony Blair article",
    category_orders={"label": ["Other", "Nearest neighbor (top 5)", "Source"]},
)

# a 2D chart of nearest neighbors of the chipset security article
chart_from_components(
    components=tsne_components,
    labels=chipset_security_labels,
    strings=article_descriptions,
    width=600,
    height=500,
    title="Nearest neighbors of the chipset security article",
    category_orders={"label": ["Other", "Nearest neighbor (top 5)", "Source"]},
)
반응형
이전 1 2 3 4 5 다음