AI

OpenAI 워드 임베딩을 이용한 내가 본 애니와 비슷한 애니 추천받기

_data 2025. 5. 14. 22:02

OpenAI의 text-embedding-3-small을 활용해서 내가 본 애니와 비슷한 줄거리의 애니를 추천해주려고 합니다. 예를 들어, '메이드 인 어비스'를 입력하면 비슷한 내용의 애니를 추천해주는거죠!

 

OpenAI API 5$ 구매하기

이 실습을 하기 위해서는 5$의 OpenAI GPT를 구매하셔야 해요.

https://blog.naver.com/gpwls1319/223484503770

 

openai API 발급 및 API 키 유료 이용요금

금융기관종사자이자 블로거인 '프로야근러'입니다. 저도 업무에 챗지피티를 굉장히 많이 이용하...

blog.naver.com

 

API Key 저장하기

gpt.env 파일을 생성해서 API Key를 복붙해줍니다. 메모장에 적어두시고 확장자를 env로 바꿔주면 됩니다.

API_KEY=...

 

 

코랩으로 실습할게요.

저는 드라이브에 .env 파일을 넣어뒀기 때문에 마운트부터 먼저 할게요.

from google.colab import drive
drive.mount('/content/drive')

 

필요한 라이브러리 설치할게요.

!pip install openai
!pip install httpx==0.27.2
path = '/content/drive/MyDrive/.../chatgpt.env'

import os
from openai import OpenAI

def init_api():
  with open(path) as env:
    for line in env:
      key, value = line.strip().split('=')
      os.environ[key] = value

init_api()
client = OpenAI(api_key=os.environ.get('API_KEY'))
print(client)

 

 

<openai.OpenAI object at 0x7a7900848c90>

 

사전 준비도 끝! 비슷한 애니메이션 예측 시작해볼게요.

먼저, 애니메이션 데이터셋이 필요해요. kaggle에 있어요. 해당 데이터셋은 title, synopsis, genre, aired, episode 등의 컬럼으로 구성되어 있어요. 아래 데이터셋을 다운받아도 되는데, 저는 Kaggle API를 활용해서 진행했어요.

https://www.kaggle.com/datasets/marlesson/myanimelist-dataset-animes-profiles-reviews

 

Anime Dataset with Reviews - MyAnimeList

Anime Dataset from myanimelist.net (animes, profiles, reviews) as 2020

www.kaggle.com

# kaggle API
kaggle_api = '/content/drive/MyDrive/.../kaggle.json'

!mkdir -p ~/.kaggle 
!cp $kaggle_api ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

kaggle API 발급은 매우 쉬워요. 넘어가구요! 쉘 명령어를 볼게요.

- mkdir: 폴더를 만들어요. / -p: 이미 폴더가 있다면 넘어가도 돼요. / ~.kaggle : 현재 경로에서 .kaggle 폴더를 만들거예요.
- cp: 복사 / $kaggle_api : 여기서 $의 역할이 중요해요. kaggle_api는 파이썬 환경에서 변수지만 쉘 환경에서는 이해할 수 없어요. 그래서 $을 사용해서 참조 가능한 환경 변수로 돼요. 
- chmod 600: 소유자만 해당 파일을 read, write할 수 있도록 권한 설정해요.

 

이제 파일을 풀어줄게요.

!kaggle datasets download -d marlesson/myanimelist-dataset-animes-profiles-reviews

!unzip myanimelist-dataset-animes-profiles-reviews.zip

 

데이터셋을 볼게요.

import pandas as pd
df = pd.read_csv('animes.csv')
df.head()

 

dataset 일부


 

 

 

임베딩

임베딩은 단어, 문장, 이미지 비정형 데이터를 숫자로 표현하는 것을 말해요. 아래의 예시와 말이죠. 예를 들어, king이라는 글자를 컴퓨터가 이해할 수 없으니 숫자로 표현합니다. 벡터화한다고 생각하면 돼요. 

nltk 버전이 낮으면 작동하지 않는 부분이 있어, 최신화할게요. 그리고, 4개를 설치할게요. 토큰화나 불용어 사용을 위해서입니다.

!pip uninstall -y nltk
!pip install nltk --upgrade

import nltk
nltk.download('punkt')
nltk.download('stopwords')
nltk.download('wordnet')
nltk.download('omw-1.4')

 

 

텍스트 임베딩을 위해 client를 다시 한 번 정의해줄게요.

client = OpenAI( api_key=os.environ['API_KEY'] )

def get_embedding(text, model):
  text = text.replace("\n", " ")
  return client.embeddings.create(
      input = [text],
      model=model
  ).data[0].embedding

 

필요한 함수를 작성할게요.

# 필요한 함수 작성
import pandas as pd
import numpy as np
import nltk

# 코사인 유사도 함수
def cosine_similarity(a,b):
  numerator = np.dot(a,b)
  denominator = np.linalg.norm(a) * np.linalg.norm(b)
  return numerator / denominator

# 혹시 모르니 한 번 더 함수로 정의
def download_nltk_data():
  try:
    nltk.data.find('tokenize/punkt')
  except LookupError:
    nltk.download('punkt')

  try:
    nltk.data.find('corpora/stopwods')
  except LookupError:
    nltk.download('stopwords')

# 전처리 함수 정의
def preprocess_text(text):
  from nltk.corpus import stopwords
  from nltk.stem import PorterStemmer       # 어간 추출(stemming)을 위한 PorterStemmer 임포트
  from nltk.tokenize import word_tokenize

  # text -> token
  tokens = word_tokenize(text)

  tokens = [word.lower() for word in tokens]

  words = [word for word in tokens if word.isalpha()]

  # 불용어(stop words) 제거
  stop_words = set(stopwords.words('english'))  # 영어 불용어 집합 생성
  words = [word for word in words if word not in stop_words]

  # 어간 추출! 예를 들어 running -> run 으로 바꿔줘요. 정해진 규칙이 있어요.
  stemmer = PorterStemmer()
  words = [stemmer.stem(word) for word in words]

  return ' '.join(words)

 

이제 임베딩을 생성할게요.

입력된 애니메이션 시놉시스의 임베딩을 생성하고, 이를 제외한 나머지 애니메이션 시놉시스 임베딩을 생성해서 코사인 유사도를 보는거죠. 1과 가까울수록 비슷한 애니메이션 시놉시스가 되겠네요.

 

중요한 건, 없는 애니메이션이면 없다고 말할 수 있지만, 애니메이션 이름을 잘못 입력할수도 있겠죠. 예를 들어, 'Naruto'를 'Noruto'라고 입력할수도 있겠죠! 이를 방지하기 위해서 오타가 있는 애니메이션의 경우 그 애니메이션 이름 임베딩을 생성하고 다른 애니메이션 이름 임베딩을 생성해서 코사인 유사도를 확인하는거죠. 가장 비슷한 하나의 인덱스로 대체를 하면 되겠죠!

import time

# 임베딩 생성 함수
def get_embedding(text, model):
  text = text.replace('\n', " ")

  return client.embeddings.create(
      input=[text],
      model=model
  ).data[0].embedding

# nltk 패키지 설치
download_nltk_data()

# 데이터셋 경로 정의
dataset_file_path = 'animes.csv'

# 질문하기
input_ani_name = input("감명 깊게 본 애니메이션은 무엇인가요?: ")

# 10개의 행만 먼저 가져오기
df = pd.read_csv(
    dataset_file_path, nrows=10 #100개 해도 상관없지만 빠른 실습을 위해 10개만 해볼게요.
)

# 결측치가 있는 경우, 임베딩이 되지 않기 때문에 시놉시스가 없는 애니 삭제
df.dropna(inplace=True)

# 시놉시스 전처리
df['preprocessed_synopsis'] = df['synopsis'].apply(preprocess_text)

# 모델 정의
model = 'text-embedding-3-small'

# 시놉시스 임베딩
synopsis_embeddings = []
for synopsis in df['preprocessed_synopsis']:
  synopsis_embeddings.append(get_embedding(synopsis, model))
  time.sleep(1.0)

# 입력된 애니 이름 인덱스 가져오기
try:
  input_ani_index = df[df['title'] == input_ani_name].index[0]
except IndexError:
  # 유사한 애니 이름 찾아보기 임베딩 생성
  print("음.. 없는 애니메이션 같아서 비슷한 이름 찾아볼게요!")

  # 1. 입력 애니 제목 임베딩 생성
  input_ani_embedding = get_embedding(input_ani_name, model)
  time.sleep(1.0)

  name_embeddings = []
  for title in df['title']:
    name_embeddings.append(get_embedding(title, model))
    time.sleep(1.0) # API 로드 충돌을 방지 하기 위해

    # 입력 임베딩 생성
    input_ani_embedding = get_embedding(input_ani_name, model)

    # 각 애니이름과 입력된 애니 이름 유사도 계산
    _sim = []
    for name_embedding in name_embeddings:
      _sim.append(cosine_similarity(input_ani_embedding, name_embedding))

    # 유사한 애니 인덱스 가져오기
    input_ani_index = _sim.index(max(_sim))
  ani_title = df.iloc[input_ani_index]['title']
  print(f"{ani_title} 이 애니 기준으로 다시 찾아볼게요!")

except:
  print("진짜 없네요..")
  raise SystemExit


# 입력된 게 같으면 임베딩 비교
similar = []
input_synopsis_embedding = synopsis_embeddings[input_ani_index]

for synopsis_embedding in synopsis_embeddings:
    similarity = cosine_similarity(
        input_synopsis_embedding,  # 입력된 시놉시스의 임베딩
        synopsis_embedding         # 비교할 시놉시스의 임베딩
    )
    similar.append(similarity)  # 계산된 유사도를 리스트에 추가

# 입력된 애니의 시놉시스를 제외한 가장 유사한 시놉시스의 인덱스 가져오기
most_similar_indices = \
    np.argsort(similar)[-6:-1]

# 가장 유사한 애니의 이름 가져오기
similar_ani_names = df.iloc[most_similar_indices]['title'].tolist()

# 결과 출력
print(
    f"{input_ani_name}와 가장 유사한 애니들은 다음과 같아요!"
)
for ani_name in similar_ani_names:
    print(ani_name)

저는 Made In abiss라고 입력했습니다. 원래는 Made in Abyss이지만요. 그랬더니 실제로 Made in Abyss 애니 기준으로 임베딩 생성하고 다른 애니메이션 시놉시스 임베딩해서 코사인 유사도를 봅니다. argsort[-6:-1]로 가장 유사한 5개만 선정해봤어요.

 

10개의 애니메이션만 사용해서 결과가 좋지 않을거예요. 시간이 많은 경우 100개 또는 500개로 해서 돌려보세요!

지금 10개로 했을 땐 메이드 인 어비스와 비슷한 애니는 다음과 같아요.

제가 입력한 애니예요. 모험이 있으면서 심오하면서 기괴하고 잔인한 그런 애니메이션이예요.

 

귀멸의 칼날은 모험 측면에서 비슷한 것 같아요. 1사분면,3사분면에 있는 애니는 모르겠습니다. 둘 다 판타지 애니메이션이라고 하네요. 마지막 센과 치히로의 행방불명은 모험적인 측면과 기괴함이 비슷한 것 같아요.