임베딩에 대해 완전히 오판하고 있었던 점은 인풋을 읽고 얼만큼 유사한 텍스트인지 알아서 판단하는 편한 기술 이라고만 여겨왔던 점이었다.
다만 내가 생각한 유사한 인풋은 사람이 생각하기에 유사한 텍스트였고 여기엔 사람의 상식, 지식적인 여부까지 포함된다고 보고 있었지만 사실 임베딩은 그냥 두 인풋의 일치도만 따진다는 점이 핵심이었다.
정확히 임베딩이라는 기술의 의도부터 따지자면 사람이 이해하는 자연어를 컴퓨터가 이해할 수 있게 벡터 데이터로 변경해줄 뿐이다.
그래서 모든 인풋은 단순한 문자열로만 들어갈 뿐이었고 내 오판은 튜닝을 작게 따지면서 바로 눈치챌 수 있었다.
input1: "price:1000"
input2: "price:10000"
"similarity": 0.8698482733763028
input1: "price:1000"
input2: "price:2000"
"similarity": 0.8059371694663092
사람인 내가 이해하기론 1000 <-> 10000 의 유사도가 1000 <-> 2000의 유사도보다 커야 했지만 이 숫자를 수치가 아닌 단순한 문자열로만 보기 때문에 유사도에서부터 크게 이상함을 느끼게 된다.
결국 내가 이전 1~3번 과정에서 튜닝해온 내용중 향기에 대해서는 맞다고 볼 수 있지만 당도, 가격같은 단순한 수치적인 데이터는 딱히 추천도라는 의도에 적합하지 않은 수치였다.
그래서 이를 자연어 처리를 해본 친구에게 물어본 결과 굳이 컴퓨터가 이해시킬려고 하는 문자열이 아닌 수치적인 부분은 임의로 배정하면 된다는 얘기를 들어 그대로 적용하게 됐다.
"level:0": [
1.0,
0.0,
0.0
],
"level:1": [
0.85,
0.1,
0.0
],
"level:2": [
0.7,
0.2,
0.1
],
"level:3": [
0.55,
0.3,
0.2
],
"level:4": [
0.4,
0.4,
0.3
],
"level:5": [
0.25,
0.5,
0.4
], /* 그 외 type은 6가지로 분류되므로 type:RED, type:WHITE 등도 동일 */
사실 블로그를 작성하는 지금 시점에선 굳이 이렇게 3차원 데이터까지 해볼 필요는 없어 보이지만, 의도는 수치가 멀어질 수록 유사도가 떨어지게끔 설정하고 싶어 임의의 3차원 벡터 데이터를 설정하게 됐다.
6가지로만 구분 가능한 당도, 타입은 해결했으니 이제 가격이 문제였다.
가격은 당도처럼 임의의 벡터 데이터로 설정하는게 안되니 이 값들을 0~1로 정규화할 수 있는 방법에 대해 이리저리 찾아보다가 Min-Max Scaling 기법을 사용해서 정규화를 하기로 했다.
임의의 가격 데이터가 10가지 있을 때 Min-Max scaling을 사용하면 아래와 같은 결과가 나온다.
price:3000000 price:342000 price:718000 price:506000 price:38900 price:726000 price:783000 price:27500 price:127000 price:720000
price:3000000 1.000000 0.259066 0.440405 0.331564 0.011687 0.442057 0.462353 0.007876 0.041089 0.433612
price:342000 0.259066 1.000000 0.996430 0.994676 0.841826 0.996510 0.997361 0.806810 0.910853 0.996331
price:718000 0.440405 0.996430 1.000000 0.998824 0.903676 0.999973 0.999191 0.869337 0.939047 0.999991
price:506000 0.331564 0.994676 0.998824 1.000000 0.926639 0.999011 0.998320 0.893223 0.950787 0.999089
price:38900 0.011687 0.841826 0.903676 0.926639 1.000000 0.905031 0.897541 0.983066 0.968460 0.903844
price:726000 0.442057 0.996510 0.999973 0.999011 0.905031 1.000000 0.999405 0.870413 0.939821 0.999993
price:783000 0.462353 0.997361 0.999191 0.998320 0.897541 0.999405 1.000000 0.862233 0.934571 0.999216
price:27500 0.007876 0.806810 0.869337 0.893223 0.983066 0.870413 0.862233 1.000000 0.959126 0.869514
price:127000 0.041089 0.910853 0.939047 0.950787 0.968460 0.939821 0.934571 0.959126 1.000000 0.939199
price:720000 0.433612 0.996331 0.999991 0.999089 0.903844 0.999993 0.999216 0.869514 0.939199 1.000000
핵심은 최대값은 1, 최소값은 0으로 정해두고 중간 가격들은 최대, 최소에 가까워질 수록 분포가 처리된다는 점으로 어차피 추천도에 들어갈 때는 이 분포도와 상관없이 (1 - (정규화된 값의 차))의 절대값
를 계산해 얼만큼 유사한지 판단 가능하기 때문에 적합한 방법이었다.
그래서 이 튜닝이 모두 끝나고 나서야 와인 추천 로직을 완성시켰고 이를 돌려본 결과 꽤 비슷한 와인만 나오는 걸 확인할 수 있었다.
단순히 수치가 똑같은 와인만 나오는게 아니라 이거저거 복합적으로 계산한게 맞긴 한지 추천 결과가 끝에 갈수록 가격, 맛에서 차이가 조금씩 나서 그럴싸한 결과가 나왔다.
// 3,2,2,1,65000원 + 향기, 화이트 와인에 대한 추천 결과 10개 내림차순
[
{
"id": 460,
"name": "프리츠 빈디쉬, 크리스마스 와인 동방박사 리슬링",
"sweetness": 3,
"acidity": 2,
"body": 2,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{apple=[복숭아, 서양배], lemon=[감귤], pineapple=[열대과일], flower=[흰꽃], stone=[미네랄]}",
"price": 55000,
"kind": "리슬링(Riesling)",
"style": "German Riesling",
"country": "독일",
"region": "라인헤센"
},
{
"id": 751,
"name": "그라토 모스카토 다스티",
"sweetness": 3,
"acidity": 2,
"body": 2,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{apple=[모과, 살구, 복숭아], pineapple=[열대과일, 리치, 파인애플], flower=[꽃]}",
"price": 30000,
"kind": "모스카토(Moscato)100%",
"style": "Italian Moscato d'Asti",
"country": "이탈리아",
"region": "아스티"
},
{
"id": 409,
"name": "드 보르톨리, 에머리스가든 모스카토",
"sweetness": 3,
"acidity": 2,
"body": 2,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{apple=[살구, 복숭아], pineapple=[열대과일, 파인애플], flower=[꽃]}",
"price": 23000,
"kind": "프론테낙그리(FrontenacGris),뮈스까(Muscat)",
"style": null,
"country": "호주",
"region": "리베리나"
},
{
"id": 928,
"name": "아스트랄, 모스카토 다스티",
"sweetness": 3,
"acidity": 2,
"body": 1,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{apple=[청사과, 복숭아], lemon=[자몽], pineapple=[멜론], flower=[아카시아], herbal=[민트, 세이지], ripen=[꿀]}",
"price": 53000,
"kind": "모스카토(Moscato)",
"style": "Italian Moscato d'Asti",
"country": "이탈리아",
"region": "아스티"
},
{
"id": 462,
"name": "프리츠 빈디쉬, 크리스마스 와인 산타 리슬링",
"sweetness": 3,
"acidity": 2,
"body": 2,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{pineapple=[열대과일, 리치, 망고], stone=[미네랄]}",
"price": 55000,
"kind": "리슬링(Riesling)",
"style": "German Riesling",
"country": "독일",
"region": "라인헤센"
},
{
"id": 5145,
"name": "라 모란디나 모스카토 다스티",
"sweetness": 3,
"acidity": 2,
"body": 2,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{apple=[모과, 살구], lemon=[오렌지], flower=[꽃], herbal=[허브]}",
"price": 65000,
"kind": "모스카토(Moscato)100%",
"style": "Italian Moscato d'Asti",
"country": "이탈리아",
"region": "랑게"
},
{
"id": 466,
"name": "프리츠 빈디쉬 와이너리, 루드비히스훼어 호니히베르그 리슬링 아우스레제",
"sweetness": 3,
"acidity": 3,
"body": 2,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{apple=[복숭아, 배, 사과], lemon=[시트러스], flower=[꽃], stone=[미네랄], ripen=[꿀]}",
"price": 60000,
"kind": "리슬링(Riesling)",
"style": "German Riesling",
"country": "독일",
"region": "라인헤센"
},
{
"id": 712,
"name": "군트럼, 베르크기르헤 리즐링 아우스레제",
"sweetness": 3,
"acidity": 2,
"body": 3,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{lemon=[시트러스, 자몽, 레몬], pineapple=[열대과일, 멜론, 파인애플], stone=[미네랄, 석유]}",
"price": 80000,
"kind": "리슬링(Riesling)",
"style": "German Riesling",
"country": "독일",
"region": "라인헤센"
},
{
"id": 930,
"name": "비녜도스 데 아기레, 알카 화이트",
"sweetness": 3,
"acidity": 2,
"body": 1,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{apple=[살구], lemon=[레몬], pineapple=[리치], flower=[꽃]}",
"price": 16900,
"kind": "소비뇽블랑(SauvignonBlanc)",
"style": null,
"country": "칠레",
"region": "센트럴"
},
{
"id": 821,
"name": "울쉬드, 모스카토",
"sweetness": 2,
"acidity": 2,
"body": 2,
"tannin": 1,
"wineType": "WHITE",
"aroma": "{apple=[복숭아], lemon=[시트러스, 오렌지], pineapple=[구아바, 열대과일, 리치, 패션프루트, 바나나], flower=[꽃]}",
"price": 25000,
"kind": "모스카토(Moscato)",
"style": null,
"country": "호주",
"region": "빅토리아"
}
]