본문 바로가기

Etc

파이썬 협업필터링(Collaborative Filtering), 추천 알고리즘 - 3

파이썬 협업필터링 Collaborative Filtering(3), 상관분석 correlation analysis

에서 이어집니다.

이전 포스팅에 사용한 유클리디안 거리공식을 활용한 유사도 측정에는 문제점이 있다.
특정인물의 점수기준이 극단적으로 너무 낮거나 높다면 제대로 된 결과를 도출해낼 수 없는 것이다.

예를 들어 나에게 영화를 평가할때 일정 기준이 있어 기대를 충족하지 못하면 모두 0점을 주고, 아니면 모두 만점을 주면 전체 데이터를 해치는 결과를 낳는다. 이것을 보완하는 것이 correlation analysis(상관분석) 이다.

상관분석은 두 변수간의 선형적 관계에 대한 분석으로 
쉽게말해 점수간 관계에 따라 점을 찍은 후 그 점이 분포한 모양에 따라 상관관계를 도출해내는 것이다.
아래 그림과 같이 두 변인 x,y에 대해 x가 변화할때 y가 변화되면 x,y는 상관관계에 있다고 한다.

(양의 상관관계와 음의 상관관계)

1. 데이터 정의

예제를 위한 데이터 딕셔너리 critics를 새로 정의한다. 
이전 포스팅에 쓰였던 데이터 critics보다는 조금 더 큰 데이터다.
영화의 개수가 많아졌고, 서로 본 영화의 개수도 다르다. 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
critics = {
    '차현석': {
        '택시운전사'2.5,
        '남한산성'3.5,
        '킹스맨:골든서클'3.0,
        '범죄도시'3.5,
        '아이 캔 스피크'2.5,
        'The Night Listener'3.0,
    },
    '황해도': {
        '택시운전사'1.0,
        '남한산성'4.5,
        '킹스맨:골든서클'0.5,
        '범죄도시'1.5,
        '아이 캔 스피크'4.5,
        'The Night Listener'5.0,
    },
    '김미희': {
        '택시운전사'3.0,
        '남한산성'3.5,
        '킹스맨:골든서클'1.5,
        '범죄도시'5.0,
        'The Night Listener'3.0,
        '아이 캔 스피크'3.5,
    },
    '김준형': {
        '택시운전사'2.5,
        '남한산성'3.0,
        '범죄도시'3.5,
        'The Night Listener'4.0,
    },
    '이은비': {
        '남한산성'3.5,
        '킹스맨:골든서클'3.0,
        'The Night Listener'4.5,
        '범죄도시'4.0,
        '아이 캔 스피크'2.5,
    },
    '임명진': {
        '택시운전사'3.0,
        '남한산성'4.0,
        '킹스맨:골든서클'2.0,
        '범죄도시'3.0,
        'The Night Listener'3.5,
        '아이 캔 스피크'2.0,
    },
    '심수정': {
        '택시운전사'3.0,
        '남한산성'4.0,
        'The Night Listener'3.0,
        '범죄도시'5.0,
        '아이 캔 스피크'3.5,
    },
    '박병관': {'남한산성'4.5'아이 캔 스피크'1.0,
             '범죄도시'4.0},
}
cs

2. scatter plot 그려보기

상관분석 전, 분석에 쓰일 scatter plot을 간단히 그려보자.
한글 출력을 위해 matplotlibfont_managerimport한다.
1
2
3
from matplotlib import font_manager, rc #한글이 나오게
font_name = font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
rc('font', family=font_name)
cs

간단한 사용법을 익히기 위해 (1,1)의 사과, (2,2)의 바나나, (3,3)의 포도로 그래프에 점을 찍어봤다.
1
2
3
4
5
6
7
8
plt.figure(figsize=(14,8))
plt.plot([1,2,3],[1,2,3],'g^'#각각의 값과 점의 모양설정
plt.text(1,1,'사과'#텍스트 찍기
plt.text(2,2,'바나나')
plt.text(3,3,'포도')
#각 축의 크기 설정
plt.axis([0,6,0,6]) # 그래프의 x축,y축 크기설정
plt.show()
cs


대강의 사용법을 익혔다면 이제 critics의 data를 이용해 scatter plot을 그려보는 함수를 만들 것이다.
drawGraph(data,name1,name2)를 정의한다. 위의 과일plot 찍기 구조를 따라하면 된다. 주석을 참고.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# critics data 이용해 scatter plot 그리기
def drawGraph(data, name1, name2):
    plt.figure(figsize=(14,8)) # plot 크기설정
    
    # plot 좌표를 위한 list 선언
    li = []
    li2 = []
    
    for i in critics[name1]: # i = 키 값
        if i in data[name2]: # 같은 영화를 평가했을때만
            li.append(critics[name1][i]) # name1의 평점 li[]에 추가
            li2.append(critics[name2][i]) # name2의 평점 li2[]에 추가
            plt.text(critics[name1][i],critics[name2][i],i) # 영화 제목 text 찍기
            
    plt.plot(li,li2,'ro'#plot그리기
    
    #각 축의 크기 설정 (0에서 6까지)
    plt.axis([0,6,0,6])
 
    # x축과 y축 이름 설정
    plt.xlabel(name1)
    plt.ylabel(name2)
 
    # 그리기
    plt.show()
cs

위 함수를 이용해 황해도와 임명진의 영화평점에 따른 scatter plot을 그려보았다.
1
drawGraph(critics,'황해도','임명진')
cs

아래와 같은 그래프가 나올 것이다.


마찬가지로 심수정과 차현석의 scatter plot도 그려보자.
1
drawGraph(critics,'심수정','차현석')
cs

이렇게 좀 더 몰려있는 모양의 그래프가 나온다.

3. 두 명간 상관계수 구하기

위 scatter plot에 따른 상관계수를 구하기 위해 피어슨 상관계수(Pearson Correlation Coefficient) 공식을 이용하겠다. 피어슨 상관계수는 값이 -1부터 1까지 도출되며, 1에 가까울수록 양의 상관관계, -1에 가까울수록 음의 상관관계를 가진다.

피어슨 상관계수

( 음... ... 흠.... .... )
역시 복잡해 보이지만 해체해보면 사칙연산을 벗어나지 않는다.


위 공식에 따라 피어슨 상관계수를 구하는 함수 sim_pearson(data, name1, name2)를 만들어본다.
x값은 name1의 영화 평점이 될 것이고, y값은 name2의 영화 평점이 될 것이다. 
n은 영화 개수가 될 것이고, 같은 영화를 평가했을때 count라는 변수를 통해 +1 시켜주도록 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 피어슨 상관계수 구하기
def sim_pearson(data, name1, name2):
    sumX=0 # X의 합
    sumY=0 # Y의 합
    sumPowX=0 # X 제곱의 합
    sumPowY=0 # Y 제곱의 합
    sumXY=0 # X*Y의 합
    count=0 #영화 개수
    
    for i in data[name1]: # i = key
        if i in data[name2]: # 같은 영화를 평가했을때만
            sumX+=data[name1][i]
            sumY+=data[name2][i]
            sumPowX+=pow(data[name1][i],2)
            sumPowY+=pow(data[name2][i],2)
            sumXY+=data[name1][i]*data[name2][i]
            count+=1
    
    return ( sumXY- ((sumX*sumY)/count) )/ sqrt( (sumPowX - (pow(sumX,2/ count)) * (sumPowY - (pow(sumY,2)/count)))
 
cs

위 함수에 실제로 값을 넣어, 둘 사이의 상관계수를 구해보자.
황해도와 임명진.
1
2
3
sim_pearson(critics,'황해도','임명진')
 
0.41791069697885247
cs

심수정과 차현석.
1
2
3
sim_pearson(critics, '심수정','차현석')
 
0.7470178808339965
cs

그래프에서 몰려 있는 모양을 본 것처럼, 심수정과 차현석이 좀 더 높은 유사성을 지닌다.

4. 전체 인원과의 상관계수 구하기

두 사람간 상관계수를 구하는 함수를 완성했으니 이제 전체 데이터를 돌면서 기준이 될 사람과 나머지 전체간의 상관계수를 한 번에 구해보자. 
이미 만들어놨던 top_match(data,name,index,sim_function)을 약간만 변형해 사용하겠다. 파라미터는 이전 포스팅(링크) 참고.
1
2
3
4
5
6
7
8
9
# 딕셔너리 돌면서 상관계수순으로 정렬
def top_match(data, name, index=3, sim_function=sim_pearson):
    li=[]
    for i in data: #딕셔너리를 돌고
        if name!=i: #자기 자신이 아닐때만
            li.append((sim_function(data,name,i),i)) #sim_function()을 통해 상관계수를 구하고 li[]에 추가
    li.sort() #오름차순
    li.reverse() #내림차순
    return li[:index]
cs
역시 보기 편하게 내림차순으로 정렬하도록 했다.

실제로 박병관과 나머지 인원과의 피어슨 상관계수를 구해보고, 내림차순으로 6위까지 출력해보자.
1
2
3
4
5
6
7
8
top_match(critics, '박병관',6)
 
[(0.9912407071619299'차현석'),
(0.9244734516419049'임명진'),
(0.8934051474415647'이은비'),
(0.66284898035987'심수정'),
(0.38124642583151164'김미희'),
(-0.38124642583151164'황해도')]
cs

차현석과 박병관은 0.99의 상관계수를 지녀 거의 유사한 취향인 것을 알 수 있다.

5. 실제 영화 추천하고 예상평점 구하기

이제 위에서 구한 함수들을 통해 실제 영화를 추천하고 예상평점을 구해보는 일만 남았다.

상관관계는 공동으로 평가를 내린 영화를 기준으로 구한 것이지만, 
영화추천은 당연하게도 아직 대상이 평가를 내리지 않은 영화여야만 한다.

또한 상관관계가 가장 높은 사람 하나만을 기준으로 영화를 추천하고 예상평점을 구하는 것은 우리가 원하는 바가 아니다. 유사도의 값을 근거로 하되 일정 기준을 충족하는 사람이라면 모두 예상평점과 추천영화를 구하는 데 참고할 수 있다. 

마지막 과정은 다음과 같다.
  1. 대상을 제외한 모든 사람들의 영화 평점과 유사도를 통해 추측평점((유사도 x (타인의)영화평점)을 구한다. 예를 들어 차현석이 택시운전사에 2.5점을 줬다면, 박병관은 차현석과 99%의 유사도를 가지고 있으므로 2.47점을 줄 것이라고 추측한다. 
  2. 그 추측평점들의 총합을 구한다.
  3. 추측평점 총합계/유사도 합계를 통해 모든 사람을 근거로 한 예상평점을 뽑아낼 수 있다.
  4. 이 예상평점 값을 보지 않은 영화를 대상으로 모두 구해서 아직 보지 않은 영화중 예상평점이 가장 높은 영화를 추천해주면 될 것이다. 
그럼 이제 실제로 예상평점을 뽑아낼 함수를 정의해보자.
함수명은 getRecommendation()으로 하겠다. 
위 과정을 밟기 위해 필요한 값은 데이터, 기준이 되는 사람, 타인과의 상관계수다. 아래와 같이 쓸 수 있다:
getRecommendation(data, person, sim_function)

함수에 쓰일 데이터 중 위에서 정의한 top_match()를 통해 함수내 미리 상관계수를 뽑아내는 과정이 필요하다. 결과값은 리스트고, 각 데이터는 튜플형태의 (예상평점, 영화제목) 형태로 만들 것이다.
 
실제로 영화를 추천하는 과정에서 상관관계가 음수면 고려할 가치가 없다. 오히려 정확도를 깎으리라 예상할 수 있으므로 조건을 통해 제외해준다.
자세한 것은 주석을 참고.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def getRecommendation (data,person,sim_function=sim_pearson):
    result = top_match(critics, person ,len(data))
    
    simSum=0 # 유사도 합을 위한 변수
    score=0 # 평점 합을 위한 변수
    li=[] # 리턴을 위한 리스트
    score_dic={} # 유사도 총합을 위한 dic
    sim_dic={} # 평점 총합을 위한 dic
 
    for sim,name in result: # 튜플이므로 한번에 
        if sim<0 : continue #유사도가 양수인 사람만
        for movie in data[name]: 
            if movie not in data[person]: #name이 평가를 내리지 않은 영화
                score+=sim*data[name][movie] # 그사람의 영화평점 * 유사도
                score_dic.setdefault(movie,0# 기본값 설정
                score_dic[movie]+=score # 합계 구함
 
                # 조건에 맞는 사람의 유사도의 누적합을 구한다
                sim_dic.setdefault(movie,0
                sim_dic[movie]+=sim
 
            score=0  #영화가 바뀌었으니 초기화한다
    
    for key in score_dic: 
        score_dic[key]=score_dic[key]/sim_dic[key] # 평점 총합/ 유사도 총합
        li.append((score_dic[key],key)) # list((tuple))의 리턴을 위해서.
    li.sort() #오름차순
    li.reverse() #내림차순
    return li

cs

이제 완성된 함수에 데이터와 기준이 될 사람만 넣으면 된다. 
1
2
3
4
5
getRecommendation(critics, '박병관')
 
[(3.467750847406967'The Night Listener'),
(2.8325499182641614'택시운전사'),
(2.5309807037655645'킹스맨:골든서클')]
cs

결과

기준이 되는 박병관이 '데이터에 존재하는 영화 중 보지 않은 영화'는 나이트크롤러, 택시운전사, 킹스맨:골든서클이며
그는 나이트크롤러에 3.4점을, 택시운전사에 2.8점을, 킹스맨:골든서클에 2.5점의 평점을 주리라고 예상할 수 있다.

따라서 내림차순 정렬을 통해 도출된 list상 index가 0번이면서 가장 높은 예상평점을 가진 나이트크롤러를 그에게 추천해줄 수 있을 것이다.