7월 16일부로 글이 없었는데 또 조장이기도 했고 이전 과제성 프로젝트들과 달리 정말 기획부터 쌓아 올려야 하다보니
팀 문서 작성과 조율, 개발 진도 체크등으로 워낙 바빠져서 시간을 빠듯하게 썼기에 팀 문서에 작성한 트러블슈팅 내역을 다시 블로그로 옮겨오기로 했다. 결과적으로는 프로젝트 자체는 의도대로 잘 성공했었다.
처음에 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%인걸 보면 뭔가 생각을 잘못했구나 생각이 들어 추천 로직은 변경을 하기로 했다.