Programming Collective Intelligence by Toby Segaran. Copyright 2007 Toby Segaran, 978-0-596-52932-1.

앞으로 인공지능(Artificial Intelligence)에 넘어가기 전에 집단지성(Collective Intelligence)에 관한 글 몇개를 작성할 예정입니다. 그리고 위 책은 제가 학습한 책으로 많이 겹치는 내용도 있을 것으로 보입니다. 감사한 마음으로 책의 서문에 있는 저자의 부탁을 써봅니다.


유클리디안 거리 점수로 유사성 검증

우리가 여러번 접하는 데이터 마이닝 시스템 중 하나인 추천 시스템을 간단히 구현하려고 합니다. 그리고 수많은 알고리즘 중 가장 간단히는 유클리디안 거리점수가 있습니다. 보통 컴퓨터 학부생과 물리학자가 생각하는 벡터는 차이가 있다고 합니다. 그런데 여기에서는 그래프와 배열을 연관지어 생각해야 합니다. 저희는 유클리디안 거리점수를 통해서 유사도 점수를 측정할 것입니다.

먼저 위에서 하나의 척도는 어떤 장르에 대한 사람들의 선호도입니다. 그러므로 호러, 로맨스 등등 하나의 장르 당 하나의 척도가 해당됩니다. 그만큼 그래프에 표시될 차원의 수 또한 늘어날 것입니다. 그리고 n차원 그래프의 공간을 선호 공간(preference space)라고 합니다. 프로그램 상의 데이터로는 아래와 같이 표현할 수 있겠습니다.

// NOTE: Soto라는 사람의 선호 장르 데이터
{
  horror: 7.5,
  romance: 6.7
}

// NOTE: Airi라는 사람의 선호 장르 데이터
{
  horror: 7.2,
  romance: 8.3
}

일단은 간단히 하기 위해서 2차원의 키를 가진 데이터로 정의해봤습니다. 키(key)는 일단 가독성을 위해 남겨둡니다. 그리고 유클리디안 거리점수 알고리즘에서 두 원소의 거리를 계산하기 위해 아래와 같은 단계를 사용합니다. (위 사진에서 두 점의 거리 공식이 나와 있습니다)

d(p, q) = d(q, p)

먼저 각각의 p와 q를 Soto와 Airi라고 합니다.

d = sqrt((p1 - q1)^2 + ... + (pn - qn)^2)

그리고 distance 함수(d)를 정의합니다. 이 때 두 수열 p, q의 n 번째는 유클리디안 벡터로 다루어집니다. 마지막으로 저희는 2개의 프로퍼티(호러와 로맨스)를 기준으로 삼았습니다. 이는 2차원과 대치되며 피타고라스의 정리와 같은 형태를 가지게 됩니다.

d = sqrt((p1 - q1)^2 + (p2 - q2)^2)

그리고 저희는 결과값이 0에서 1 사이로 표시되길 원합니다. 당연히 1이 더욱 높은 숫자입니다. 그렇기 때문에 distance 함수에 1을 더하고 역수를 취하면 공식이 완성이 됩니다.

d = 1 / (1 + sqrt((p1 - q1)^2 + (p2 - q2)^2))

n차원일 때의 유사성 비교

공식 자체는 완성이 되었습니다만 저희가 현실에서 쓸 일은 없을 것입니다. 그에 비해 컴퓨터 프로그램으로 작성하는데 확장성을 고려해 n차원에서의 유사성을 비교해주는 스크립트를 작성하도록 하겠습니다. 대부분의 책이나 예제라면 파이썬을 주로 사용하겠지만 개인적인 연습을 위해 JavaScript 또는 타 언어에서 작성하도록 하겠습니다.

먼저 하나의 함수를 기준으로 합니다.

const euclideanDistance = (p, q) => {
  ...
}

변수 p와 q는 비교할 2개의 대상이라고 할 때 각각은 위 예제와 같이 오브젝트 형태 또는 배열의 형태여야 합니다. 예제가 너무 길어질까봐 완전히 정확한 값의 검증까지는 거치지 않겠습니다. (또한 최적화가 되지 않았습니다)

const euclideanDistance = (p, q) => {
  const datas = [p, q]
  let propLength = 0 // NOTE: 각 객체의 속성 갯수 확인을 위한 임시 변수
  let rate = 0 // NOTE: 출력값

  datas.forEach((data, i) => {
    // NOTE: Object인 경우 배열로 바꿔줍니다.
    if (typeof data === 'object' && data !== null) {
      datas[i] = Object.values(data)
    } else if (data instanceof Array) {
      // NOTE: Empty, 배열인 경우 넘기기 위해서입니다.
    } else {
      throw new Error(`Data at position ${i} is not configured. It need to be an Array or Object.`)
    }

    // NOTE: 만약 2개의 속성의 수가 다르다면 오류를 출력합니다.
    if (propLength && propLength !== datas[i].length) {
      throw new Error(`Data at position ${i} is not configured. It need to be an Array or Object.`)
    } 
  }
}

마지막으로 유클리디안 거리 점수의 유사도 공식을 대입합니다. 이 때 Math.pow() 메서드는 2번째 인수만큼의 제곱을 구합니다. 그러므로 여기에서는 2제곱(제곱)을 하는 것과 같습니다.

const euclideanDistance = (p, q) => {
  const datas = [p, q]
  let propLength = 0
  let rate = 0

  datas.forEach((data, i) => {
    /* 값 검증 및 변환 단계 생략 */
  }

  datas[0].forEach((pValue, k) => {
    rate += Math.pow(pValue - datas[1][k], 2)
  }

  return 1 / (1 + Math.sqrt(rate))
}

앞선 코드가 약간 더러워졌지만 일단은 완성입니다. 구조는 0으로 초기화된 rate라는 변수에 p와 q의 n번째 속상값을 더해주고 제곱을 한 것을 모두 더하고 마지막에 제곱근을 더한 것에 1을 더하고 역을 취하면 됩니다. 위에서는 입력값에 오브젝트까지 추가되어 약간 더러워졌는데 아래에 간단히 작성하겠습니다. 입력의 검증 등 모든 군더더기를 뺀 코드입니다.

const euclideanDistance = (p, q /* 이 때 p, q는 배열 */) => {
  let rate = 0

  p.forEach((pv, k) => rate += Math.pow(pv - q[k], 2))

  /* 더 직관적으로 보일 수 있어 for문을 활용한 것도 추가 작성하였습니다.
  for (i of p) {
    rate += Math.pow(p[i] - q[i], 2)
  }
  */

  console.log(rate)

  return 1 / (1 + Math.sqrt(rate))
}

쓸데없이 길었네요. 읽어주셔서 감사합니다.