최종 프로젝트 트러블슈팅 - 와인 추천 로직(1)

노재원·2024년 9월 4일
0

내일배움캠프

목록 보기
84/90

최종 프로젝트 마무리

7월 16일부로 글이 없었는데 또 조장이기도 했고 이전 과제성 프로젝트들과 달리 정말 기획부터 쌓아 올려야 하다보니

팀 문서 작성과 조율, 개발 진도 체크등으로 워낙 바빠져서 시간을 빠듯하게 썼기에 팀 문서에 작성한 트러블슈팅 내역을 다시 블로그로 옮겨오기로 했다. 결과적으로는 프로젝트 자체는 의도대로 잘 성공했었다.

와추 브로셔

Embedding 써보기

처음에 Embedding API의 사용 예시를 보고 생각했던 방식은 LLM이 알아서 내 인풋을 읽고 두 인풋의 유사성을 점검하는 거라 생각했다.

사용 예시로 본 건 관련 검색어를 추천해주는 로직으로 사용자의 검색어를 Embedding으로 변환해 다른 검색어들과 코사인 유사도를 비교해서 추천 검색어를 찾아내는 것 밖에 없었다.

그래서 처음 와인 데이터를 Embedding으로 추천에 사용하기 위해 집어넣은 인풋은 다음과 같았다.

최초의 인풋

"""
sweetness: ${it.sweetness},
acidity: ${it.acidity},
body: ${it.body},
tannin: ${it.tannin},
type: ${it.wineType},
aroma: ${it.aroma},
price: ${it.price},
kind: ${it.kind},
""".trimIndent()

물론 지금 이 인풋이 틀렸다는 건 알고 있고 최초 과정이라고 생각하면 될 것 같다.

어쨌든 이 인풋을 넣으면 이 문자열 전체를 Embedding한 값인 벡터 데이터로 출력해줘서 1536차원의 Double 데이터를 얻게 되고 이를 코사인 유사도로 비교하는 것이다.

코사인 유사도 계산

Index 0번 와인을 0~9번까지 총 10개와 비교하는 테스트를 가장 먼저 시도했다.

// 벡터 데이터 두 값의 코사인 유사도 계산
fun cosineSimilarity(vec1: List<Double>, vec2: List<Double>): Double {
	val dotProduct = vec1.zip(vec2) { a, b -> a * b }.sum()
	val magnitude1 = sqrt(vec1.sumOf { it * it })
	val magnitude2 = sqrt(vec2.sumOf { it * it })
	return dotProduct / (magnitude1 * magnitude2)
}

// 상위(Top) 의 개수만큼 targetEmbedding 과 비교해 embeddings를 조회한다.
fun recommend(
	targetEmbedding: List<Double>,
	embeddings: List<List<Double>>,
	top: Int = 5
): List<Pair<Int, Double>> {
	return embeddings.mapIndexed { index, embedding ->
		index + 1 to cosineSimilarity(targetEmbedding, embedding)
	}
	.sortedByDescending { it.second }
	.take(top)
}

코사인 유사도 계산 자체는 다행히 쉬웠고 결과는 다음과 같이 나왔다.

[
  {
    "first": 0,
    "second": 1
  },
  {
    "first": 1,
    "second": 0.8690756358160642
  },
  {
    "first": 7,
    "second": 0.8485209195151978
  },
  {
    "first": 4,
    "second": 0.8313532711556401
  },
  {
    "first": 2,
    "second": 0.8272868166431258
  },
  {
    "first": 8,
    "second": 0.8188866359316774
  },
  {
    "first": 6,
    "second": 0.8132226258593864
  },
  {
    "first": 3,
    "second": 0.809315238563566
  },
  {
    "first": 5,
    "second": 0.8067882933010757
  },
  {
    "first": 9,
    "second": 0.8024955780311208
  }
]

0번을 0번과 비교하면 벡터 데이터가 완전히 똑같아 1이 나오고 다른 것들은 대체로 80~86% 정도의 유사도가 나왔다. 이걸 추천도로 표현해서 사용자에게 보여주면 될 것으로 보였는데 문제는 이 값이 정말 믿을만한 유사도인가? 라는게 의심이 많이 됐다.

우선 최초 기획이었던 사용자가 원하는 값에 추천도를 더해서 결과를 바꿀 수 있게 하기를 적용하려면 인풋이 이렇게 생기면 안됐다.

그리고 저 80~86% 정도의 유사도가 정말 비슷한 와인을 추천해줬는지 직접 0~9번 와인을 살펴보면 확 다른 것도 있었고 꽤 비슷한 와인도 있었는데 최대가 86%인걸 보면 뭔가 생각을 잘못했구나 생각이 들어 추천 로직은 변경을 하기로 했다.

0개의 댓글