오늘은 openai cookbook 에 있는 Embeddings 부문의 Text comparison examples 에 있는 Semantic_text_search_using_embeddings.ipynb 예제를 살펴 보겠습니다.
우선 이 예제를 살펴 보기 전에 준비해야 할 사항들이 몇가지 있습니다.
여기서는 대용량의 데이터 속에서 의미론적으로 유사한 정보를 embeddings를 사용해서 검색하는 방법을 알아 볼 겁니다.
현재 구글 같은 검색엔진에서는 delicious beans 를 입력하면 이 두 단어가 들어가 있거나 어느 한 단어라도 들어가 있는 정보를 검색해서 보여 줍니다.
하지만 embedding을 통해서 검색을 하면 이 delicious beans의 질문의 의미와 유사한 정보를 검색해서 알려 줍니다.
예를 들어 I want to play with cat. 이라는 문장이 있습니다. 고객은 고양이와 놀고 싶은 거죠.
그런데 cat 이 없는 경우 이와 대체할 수 있는 다른 서비스를 생각한다고 칩시다.
여러분 한테는 car 와 dog 가 있습니다. 어떤 것을 추천 하겠습니까?
사람이라면 dog를 추천할 겁니다. 고객이 왜 cat와 놀고 싶은지 그 의미를 알기 때문이죠.
그런데 기계적으로 살펴보면 car 가 cat 과 두글자나 같으니까 dog 보다는 더 유사하다고 볼 것입니다.
단어의 유사성을 근거로 추천한 car 와 문의의 의미를 분석한 후 추천한 dog 중 고객은 후자에 더 만족할 확률이 높습니다.
이런 일을 하는 것이 embeddings를 이용한 의미론적 문자 검색 (Semantic text search using embeddings) 입니다.
이 예제에서는 검색을 하기 위한 data pool 이 요구 됩니다. 그 데이터세트는 2012년 10월까지 아마존 고객들이 음식관련 리뷰를 남긴 568,454개의 food review 데이터를 사용할 겁니다.
이 데이터는 csv 형식으로 돼 있는데 아래 링크에서 다운 받아서 여러분의 python 파일이 있는 폴더에 data 라는 sub-folder를 생성한 후 거기에 복사해 넣으세요.
이 csv 파일을 열어보면 아래와 같이 생겼습니다.
각 리뷰에 대한 productId, UserId, Score, Summary, Text, combined, n_tokens 그리고 embedding 컬럼들이 있습니다.
이제 코드를 작성해 보죠.
숫자로 된 embedding을 다루어야 하니까 파이썬에서 수학적인 기능을 제공하는 numpy 모듈과 대용량 데이터를 사용해야 하니까 pandas 모듈이 필요할 겁니다.
우선 필요한 모듈들을 import 하고 항상 사용하는 openai 에 api_key 인증 받는 부분을 아래와 같이 작성합니다.
그리고 다운 받았던 csv 파일을 pandas 모듈의 read_csv() 메소드를 사용해서 읽어 옵니다.
위의 코드는 다음과 같이 각 줄마다 설명됩니다.
- import openai: openai 모듈을 가져옵니다. 이 모듈을 사용하여 OpenAI API와 상호 작용할 수 있습니다.
- from pprint import pprint: pprint 모듈에서 pprint 함수를 가져옵니다. pprint 함수는 보기 좋게 출력하는 데 사용됩니다.
- import numpy as np: numpy 모듈을 가져옵니다. 이 모듈은 수치 계산과 배열 연산에 사용되는 다양한 기능을 제공합니다.
- import pandas as pd: pandas 모듈을 가져옵니다. 이 모듈은 데이터 분석과 조작에 사용되는 다양한 기능을 제공합니다.
- def open_file(filepath):: open_file이라는 함수를 정의합니다. 이 함수는 파일을 열어서 내용을 읽은 후 문자열로 반환합니다.
- with open(filepath, 'r', encoding='utf-8') as infile:: 파일을 열고 infile 변수에 할당합니다. 'utf-8'로 인코딩된 텍스트 파일을 엽니다.
- return infile.read(): 파일 내용을 읽어서 반환합니다.
- openai.api_key = open_file('openaiapikey.txt'): openaiapikey.txt 파일을 열어서 내용을 읽은 후, OpenAI API 키로 설정합니다.
- datafile_path = "data/fine_food_reviews_with_embeddings_1k.csv": 데이터 파일의 경로를 지정합니다. "data" 폴더 안에 있는 fine_food_reviews_with_embeddings_1k.csv 파일입니다.
- df = pd.read_csv(datafile_path): pandas의 read_csv 함수를 사용하여 CSV 파일을 읽어서 DataFrame으로 저장합니다.
- df["embedding"] = df.embedding.apply(eval).apply(np.array): DataFrame의 "embedding" 열에 대해 eval 함수를 적용하여 문자열을 Python 객체로 변환한 후, np.array 함수를 적용하여 배열로 변환합니다. 이렇게 변환된 임베딩 값들은 "embedding" 열에 저장됩니다.
여기에서 pandas 모듈을 사용하기 위해 기본적인 것들을 알면 좋을 것 같습니다.
구글링해서 찾아보세요.
저는 아래 글을 봤는데 정리가 잘 돼 있는 것 같더라구요.
https://dandyrilla.github.io/2017-08-12/pandas-10min/
짧은 지식을 근거로 16번째 줄을 해석해 보면...
df["embedding"] = df.embedding.apply(eval).apply(np.array)
csv라는 파일 (df) 에서 embedding 컬럼에 df.embedding.apply(eval).apply(np.array) 를 해 준다는 의미 입니다.
apply(eval).apply(np.array)는 openai Guide의 Embeddings Use cases 페이지에서 그 사용법을 다루었습니다.
저장된 파일에서 데이터를 load 하는 방법이라고 나와 있습니다.
일단 이렇게 사용하는 것이라고 알아 두어야 겠습니다.
그 다음에는 openai.embeddings_utils라는 모듈에서 get_embedding 과 cosine_similarity를 import 합니다.
from openai.embeddings_utils import get_embedding, cosine_similarity
여기서는 python의 from ... import ... 구문을 이해 하셔야 합니다.
import 모듈 하면 모듈 전체를 가져 오는 것이고 from 모듈 import 이름 하면 그 모듈 내에서 필요한 메소드만 가져오는 방법입니다.
이렇게 하면 이 메소드를 사용할 때 모듈.메소드 이런 형식이 아니라 그냥 메소드 이름만 사용하면 됩니다.
>>> import tkinter
>>> tkinter.widget = tkinter.Label(None, text='I love Python!')
>>> tkinter.widget.pack()
이렇게 사용 해야 하는 것을...
>>> from tkinter import *
>>> widget = Label(None, text='I love Python!')
>>> widget.pack()
이렇게 사용하도록 하는 겁니다.
그러면 openai.embeddings_utils라는 모듈의 get_embedding과 cosine_similarity 함수들을 보는 방법을 알려 드리겠습니다.
https://github.com/openai/openai-python
위 페이지에 가시면 openai 의 파이썬 관련 모듈들을 보실 수 있습니다.
openai 폴더로 들어가서 embeddings_utils.py 를 클릭합니다.
그러면 이 모듈을 보실 수 있습니다.
여기에서 get_embedding 함수를 보겠습니다.
이 함수는 openai api 의 JSON 형식의 response 중에서 embedding 부분만 return 하도록 만든 함수 입니다.
이전 글 에서는 이 부분을 그냥 ["data"][0]["embedding"] 를 사용해서 코딩을 해서 사용했습니다.
그 다음은 cosine_similarity 를 보겠습니다.
간단한 함수 입니다. 다 파라미터를 input 값으로 받아서 np.dot과 np.linalg.norm 을 사용해서 계산 한 후 return 합니다.
linalg.norm()은 벡터의 크기라고 합니다.
자세한 건 모르겠고 이 cosine_similarity 함수는 두 벡터값을 입력 받아서 두 값의 유사성을 계산해서 return 하는 일을 합니다.
지금까지 진행한 소스코드 입니다.
import openai
from pprint import pprint
import numpy as np
import pandas as pd
def open_file(filepath):
with open(filepath, 'r', encoding='utf-8') as infile:
return infile.read()
openai.api_key = open_file('openaiapikey.txt')
datafile_path = "data/fine_food_reviews_with_embeddings_1k.csv"
df = pd.read_csv(datafile_path)
df["embedding"] = df.embedding.apply(eval).apply(np.array)
from openai.embeddings_utils import get_embedding, cosine_similarity
일단 여기까지 하고 실행 해 보세요.
저 같은 경우는 embeddings_utils 모듈에서 import 하는 모듈들이 없다면서 에러를 많이 발생 시키더라구요.
참고로 embeddings_utils 모듈에서 import 하는 다른 모듈들은 아래와 같습니다.
에러는 이런 식으로 납니다.
해결은 그냥 pip install 모듈이름 -> 이렇게 해 주면 됩니다.
저는 이 이외에도 plotly, scipy, sklearn 등의 모듈이 없다고 해서 새로 install 했습니다.
sklearn 같은 경우에는 더 이상 사용하지 않는 모듈이라고 나왔습니다.
그래서 위에서 시키는 데로 pip install scikit-learn 을 해서 해결했습니다.
여기까지 필요한 모듈을 import 하고 open ai에 api_key를 보내서 인증 받고 csv 파일을 읽는 작업까지 했습니다.
이제 일을 처리하기 위한 사전 준비는 다 됐습니다.
이제 할 일은 입력값을 받아서 openai 의 embedding 기능을 이용해서 벡터 값을 받아 오는 겁니다.
product_embedding = get_embedding(
입력값,
engine="text-embedding-ada-002"
)
여기서 원래는 openai.Embedding.create() API 를 사용해서 벡터값을 받아와서 ['data'][0]['embedding'] 를 사용해서 JSON 형식의 response에서 embedding 값만 받아 왔었습니다.
그런데 이 소스코드에서는 openai.embeddings_utils 라는 모듈의 get_embedding() 이라는 함수를 써서 곧바로 embedding 값만 받아 옵니다.
그러면 원래 csv에 있던 embedding 값과 입력값에 대한 embedding 값을 모두 가지게 되었습니다.
다음 할 일은 cosine_similarity() 를 통해서 유사값만 계산해 주면 됩니다.
그러면 그곳에서 가장 유사성에 높은 것만 뽑으면 유용한 정보를 얻을 수 있는 것입니다.
이것을 구현한 함수는 아래와 같습니다.
위의 코드는 다음과 같이 각 줄마다 설명됩니다.
- from openai.embeddings_utils import get_embedding, cosine_similarity: openai.embeddings_utils 모듈에서 get_embedding, cosine_similarity 함수를 가져옵니다. 이 함수들은 임베딩을 가져오고 코사인 유사도를 계산하는 데 사용됩니다.
- def search_reviews(df, product_description, n=3, pprint=True):: search_reviews라는 함수를 정의합니다. 이 함수는 DataFrame과 제품 설명을 받아와서 해당 제품과 유사한 리뷰를 검색합니다. 기본적으로 상위 3개의 유사한 리뷰를 반환하며, pprint 매개변수가 True로 설정되면 결과를 보기 좋게 출력합니다.
- product_embedding = get_embedding(product_description, engine="text-embedding-ada-002"): get_embedding 함수를 사용하여 제품 설명에 대한 임베딩을 가져옵니다. "text-embedding-ada-002" 엔진을 사용하여 임베딩을 생성합니다.
- df["similarity"] = df.embedding.apply(lambda x: cosine_similarity(x, product_embedding)): DataFrame의 "embedding" 열에 대해 cosine_similarity 함수를 적용하여 제품 임베딩과의 유사도를 계산한 후, "similarity" 열에 저장합니다.
- results = (df.sort_values("similarity", ascending=False).head(n).combined.str.replace("Title: ", "").str.replace("; Content:", ": ")): DataFrame을 "similarity" 열을 기준으로 내림차순 정렬하고 상위 n개의 결과를 선택합니다. 이후 "combined" 열에서 "Title: "과 "; Content:"를 제거한 후, "results" 변수에 저장합니다.
- if pprint: for r in results: print(r[:200]); print(): pprint 매개변수가 True로 설정되어 있으면 결과를 보기 좋게 출력합니다. 각 결과의 첫 200자를 출력하고 빈 줄을 출력합니다.
- return results: 결과를 반환합니다.
search_reviews() 라는 함수가 있고 입력 파라미터는 4개 입니다.
변수 df 는 csv 정보이고 product_description 은 입력값입니다. n=3와 pprint=True는 부가적인 파라미터 입니다.
22~25번째 줄 까지가 입력값을 openai api를 통해서 openai 로 보내고 거기에 대한 벡터값을 return 받아서 product_embedding이라는 변수에 저장하는 겁니다.
26번째 줄에는 cosine_similarity()를 통해서 이 입력값에 대한 임베딩 값과 csv 파일에 있는 review의 임베딩 값의 유사성을 구하는 겁니다.
lamda x를 사용해서 csv의 각각의 리뷰마다 이 유사값을 계산하는 겁니다.
df['similarity"]는 similarity라는 컬럼을 새로 만들고 이 컬럼에 계산된 유사값을 넣는다는 의미 입니다.
그 다음 28번째 줄에서 33번째 줄까지 보겠습니다.
result라는 변수에 어떤 값들을 넣는데요.
df.sort_values() 에서 처음에 하는 것이 similarity 컬럼을 sorting 하는 겁니다. 이렇게 하면 내림차순으로 정리가 되서 가장 유사성이 높은 것이 맨 위로 가게 되죠.
그 다음 head(n) 함수는 pandas의 dataframe에 있는 함수로 상위 n개의 값만 가진다는 의미 입니다. n 은 이 함수의 입력 파라미터로서 디폴트 값은 3 이고 다른 값이 입력되면 그 값을 따릅니다.
그 다음은 combined 라는 컬럼의 Title: 을 "" 로 바꾸고 ; Content: 를 :로 바꾼다는 의미입니다.
.combined.str.replace("Title: ", "")
.str.replace("; Content:", ": ")
if pprint:
for r in results:
print(r[:200])
print()
이 부분은 pprint 일 경우 첫번째 200 캐릭터만 출력하라는 겁니다.
그리고 38번째 줄에서는 두 입력 파라미터의 유사값을 계산해서 가공한 값인 results를 return 합니다.
이제 이 함수를 사용하기만 하면 됩니다.
results = search_reviews(df, "delicious beans", n=3)
print('delicious beans\n')
pprint(results)
위의 코드는 다음과 같이 각 줄마다 설명됩니다.
- results = search_reviews(df, "delicious beans", n=3): search_reviews 함수를 사용하여 DataFrame df에서 "delicious beans"와 유사한 리뷰를 상위 3개 검색한 결과를 results 변수에 저장합니다.
- print('delicious beans\n'): "delicious beans"를 출력합니다. \n은 줄바꿈을 의미합니다.
- pprint(results): pprint 함수를 사용하여 results를 출력합니다. pprint 함수는 결과를 보기 좋게 출력하는 역할을 합니다.
저는 이 Sementic_text_search_using_embeddings.ipynb 에서 제시한 모든 입력값들에 Korean food 와 Kimchi 를 추가해서 출력해 보았습니다.
results = search_reviews(df, "delicious beans", n=3)
print('delicious beans\n')
pprint(results)
results = search_reviews(df, "whole wheat pasta", n=3)
print('whole wheat pasta\n')
pprint(results)
results = search_reviews(df, "bad delivery", n=1)
print('bad delivery\n')
pprint(results)
results = search_reviews(df, "spoilt", n=1)
print('spoilt\n')
pprint(results)
results = search_reviews(df, "pet food", n=2)
print('pet food\n')
pprint(results)
results = search_reviews(df, "Korean food", n=2)
print('Korean food\n')
pprint(results)
results = search_reviews(df, "Kimchi", n=2)
print('Kimchi\n')
pprint(results)
결과 값은 아래와 같이 나왔습니다.
Kimchi와 관련해서는 아래처럼 나오네요.
Kimchi에 대한 직접적인 언급한 부분이 한군데도 없나 봅니다.
이런 경우 이전 검색 방법으로 하면 Kimchi 라는 단어가 없으니까 아무것도 출력이 안될겁니다.
하지만 이 임베딩의 경우에는 단어나 문구, 문장들의 의미를 부여한 벡터 값을 가지고 검색하는 겁니다.
그래서 위 두가지 답이 나왔습니다.
첫번째 답은 한국의 맵고 유명한 누들이라고 하는 걸로 봐서 신라면에 대한 평가인 것 같습니다.
이게 왜 유사할까요?
Kimchi에 대한 임베딩 값에는 Korea 와 유사성이 가까운 값도 있고, 김치는 맵다는 값도 있나 봅니다.
그러니까 매운 한국 음식이라는 표현이 있는 이 제품 평가가 그나마 가장 가까운 거라고 ChatGPT는 평가 했습니다.
두번째는 매운 음식이라는 표현이 있네요.. 그래서 Kimchi와 유사성이 있다고 한 것 같습니다.
이렇게 기존의 검색 방법과 임베딩의 검색 방법의 차이는 이 Open AI 의 임베딩은 1536개의 부동 소수점 숫자들을 가지고 있는데 그 숫자들은 어떤 의미를 부여하고 있다는 겁니다.
그래서 딱히 찾고자 하는 단어가 다른 문장에 없어도 의미가 가장 가까운 문장을 찾을 수 있다는 겁니다.
이렇게 해서 대용량 데이터세트 내에서 openai의 임베딩 기능을 통한 Semantic text search 에 대한 예제를 살펴 봤습니다.
전체 소스코드는 여기에 있습니다.
import openai
from pprint import pprint
import numpy as np
import pandas as pd
def open_file(filepath):
with open(filepath, 'r', encoding='utf-8') as infile:
return infile.read()
openai.api_key = open_file('openaiapikey.txt')
datafile_path = "data/fine_food_reviews_with_embeddings_1k.csv"
df = pd.read_csv(datafile_path)
df["embedding"] = df.embedding.apply(eval).apply(np.array)
from openai.embeddings_utils import get_embedding, cosine_similarity
# search through the reviews for a specific product
def search_reviews(df, product_description, n=3, pprint=True):
product_embedding = get_embedding(
product_description,
engine="text-embedding-ada-002"
)
df["similarity"] = df.embedding.apply(lambda x: cosine_similarity(x, product_embedding))
results = (
df.sort_values("similarity", ascending=False)
.head(n)
.combined.str.replace("Title: ", "")
.str.replace("; Content:", ": ")
)
if pprint:
for r in results:
print(r[:200])
print()
return results
results = search_reviews(df, "delicious beans", n=3)
print('delicious beans\n')
pprint(results)
results = search_reviews(df, "whole wheat pasta", n=3)
print('whole wheat pasta\n')
pprint(results)
results = search_reviews(df, "bad delivery", n=1)
print('bad delivery\n')
pprint(results)
results = search_reviews(df, "spoilt", n=1)
print('spoilt\n')
pprint(results)
results = search_reviews(df, "pet food", n=2)
print('pet food\n')
pprint(results)
results = search_reviews(df, "Korean food", n=2)
print('Korean food\n')
pprint(results)
results = search_reviews(df, "Kimchi", n=2)
print('Kimchi\n')
pprint(results)