슈코딩

[Django] 추천시스템 프로젝트 코드복기 2 본문

개발일지/Project

[Django] 추천시스템 프로젝트 코드복기 2

Roshu 2022. 6. 17. 01:08

 

이번 애니메이션을 주제로한 추천시스템 프로젝트에서 DB에있는 정보들을 불러오는 view 그리고 컨텐츠기반 추천모델, 유저기반 모델 등 한번에 내것으로 만들기가 어려웠던 부분들이 있었다. 그래서 오늘은 각 APP 별로 코드를 짜는데 

잘 이해가 안갔던 부분을 중점으로 코드를 정리를 해보려고 한다. 

 

1. user 앱 views.py 

장르 선택 페이지 

필요한 기능

  • 아이콘 선택시 선택이 된걸 확인 할 수 있게 색상 변경 ( 핑크색, 하트 ) 
  • 아이콘 선택시 선택한 정보가 DB에 저장 add()
  • 중복 선택시 저장되었던 정보가 DB에서 삭제 exists(), remove()
  • 선택은 3가지만 할 수 있게 제한 (1,2 or 4도 안된다. only 3) count()
  • 3개 미만 선택시에 선택완료 버튼 비활성화 (숨김) 

views.py

@login_required
def select_genre_view(request):
    if request.method == 'POST':
        user = request.user
        genre_id = request.POST.get('genre', '')
        genre = Genre.objects.get(id=genre_id)

        exist = user.fav_genre.filter(users__fav_genre=genre).exists()
        my_genres = user.fav_genre.all().values()
        genre_count = user.fav_genre.all().count()
        genre_list = []

        if exist:
            user.fav_genre.remove(genre)
        else:
            if genre_count < 3:
                user.fav_genre.add(genre)
            else:
                messages.error(request, '이미 3개의 장르 선택함.')

        for my_genre in my_genres:
            genre_list.append(str(my_genre['id']))
            #my_genres = fav_genre 안에있는 오브젝트 즉, genre_list에 fav_genre안의 오브젝트의 id값만 str으로 추가

        return render(request, 'user/select_genre.html', {'genre_list': genre_list, 'count': len(genre_list)})

    return render(request, 'user/select_genre.html')

버튼 비활성화 

- 백엔드에서 넘어온 count가 3일경우에만 선택완료 버튼 보이게 함.

선택 아이콘 변경 ( 선택 확인 )

-백엔드에서 넘어온 genre_list 안에 장르에 해당하는 id 숫자가 존재할 경우 변경

 

2. animation 앱 views.py

메인 페이지 

필요한기능

  • 3가지 선택한 장르 기반으로 랜덤 추천
  • 유저기반 추천 모델 
  • 더보기 버튼을 통해 해당 장르의 모든 애니 볼 수 있는 페이지로 이동

views.py

@login_required
def main_view(request):
    user = request.user
    # 유저가 선택한 장르들 가져오기
    main_genres = list(user.fav_genre.all())
    animation_list = Animation.objects.all()


    # 키 = 장르 객체, 밸류 = 애니 정보 리스트가 담긴 딕셔너리 생성
    genre_ani_info = {}
    for main_genre in main_genres:
        # 각 장르마다 그 장르가 있는 애니들 모두 가져오기
        search_list = list(animation_list.filter(Q(genre__name__icontains=main_genre.name)))
        # 랜덤화하고 6개만 가져오기
        random_list = random.sample(search_list, len(search_list))[:6]

        # 각 장르마다 애니정보 딕셔너리들이 담길 리스트 생성
        ani_info_list = []
        # 각 애니마다 애니정보 딕셔너리 생성
        for animation in random_list:
            # 각 애니마다 그 애니의 장르들 모두 가져오기
            genres = Genre.objects.filter(animation__id=animation.id).values()
            # 리스트화 풀어주기
            genre_list = []
            for genre in genres:
                genre_list.append(genre['name'])
            genre_list = ", ".join(genre_list)
            ani_info = {'title': animation.title, 'img': animation.img, 'genre': genre_list, 'id': animation.id}
            ani_info_list.append(ani_info)

        genre_ani_info[main_genre] = ani_info_list

    # 유저 기반 추천 모델
    if len(Recommend.objects.filter(user__id=user.id)) > 0 and len(Recommend.objects.all()) > len(Recommend.objects.filter(user__id=user.id)):
        user_ratings = pd.DataFrame(list(Recommend.objects.all().values()))
        user_ratings.set_index('id', inplace=False)
        user_ratings = user_ratings[['user_id', 'animation_id']]
        user_ratings['rating'] = 1

        user_ratings = user_ratings.pivot_table('rating', index='user_id', columns='animation_id')
        user_ratings = user_ratings.fillna(0)
        user_based_collab = cosine_similarity(user_ratings, user_ratings)
        user_based_collab = pd.DataFrame(user_based_collab, index=user_ratings.index, columns=user_ratings.index)

        user = user_based_collab[user.id].sort_values(ascending=False).index[1]
        result = user_ratings.query(f"user_id == {user}").sort_values(ascending=False, by=user, axis=1)
        recommend_anis = list(result.keys())[:6]

        recommend_anis_list = []
        for recommend_ani in recommend_anis:
            recommend_anis_list.append(Animation.objects.get(id=recommend_ani))


        #템플렛으로 보내줄때는 key, value값을 꺼낼 수 있도록 애니정보의 딕셔너리 아이템들(튜플) 보내주기
        return render(request, 'animation/mainpage.html', {'genre_ani_info': genre_ani_info.items(), 'recommend_anis_list': recommend_anis_list})
    return render(request, 'animation/mainpage.html', {'genre_ani_info': genre_ani_info.items()})

 

#데이터프레임 변환

user_ratings = pd.DataFrame(list(Recommend.objects.all().values()))

#Pivot Table

user_ratings = user_ratings.pivot_table('rating', index='user_id', columns='animation_id')

# 코사인 유사도

user_based_collab = cosine_similarity(user_ratings, user_ratings)

#Query

 

이러한 과정을 거쳐서 유사도 값을구해서 같은애니를 추천한 유저를 유사도값을 구해서 그 유저가 추천한 애니를 추천하는 방식이다.

 

 

3. detail 앱 views.py 

 

애니메이션 상세 페이지

필요한기능

  • DB에 저장된 애니메이션 오브젝트 하나의 정보들을 전부 불러와 출력
  • 댓글기능, 북마크기능, 추천기능
  • 장르기반 비슷한 애니 추천 ( 컨텐츠 기반 추천 모델 )

 

views.py

@login_required
def animation_detail(request, id):
    user = request.user
    animation = Animation.objects.get(id=id)
    animations = Animation.objects.all()
    genres = Genre.objects.filter(animation__id=id).values()

    genre_list = []

    if len(genres) > 0:
        for genre in genres:
            name = genre['name']
            genre_list.append(name)
        genre_list = ", ".join(genre_list)
    else:
        genre_list = "장르 정보가 없습니다"

    genre_name_list = []

    for anime in animations:
        info = anime.genre.values()
        temp = []
        for i in info:
            temp.append(i['name'])
        genre_name_list.append(temp)

    genre_name_list = list(map(str, genre_name_list))
    #리스트는 벡터화가 안되기때문에, 리스트안에 리스트가 아닌 리스트 안에 문자열
    cv = CountVectorizer()

    genre_vector = cv.fit_transform(list(map(str, genre_name_list)))  #장르 벡터화

    genre_dic = cv.vocabulary_  # {'sf': 0, '가족': 1 ....}의 dictionary 형태

    neighbors = NearestNeighbors(n_neighbors=10).fit(genre_vector)  #벡터화 진행후 가까운 값 10개를 가져온다.

    detailpage_contents_recommend = np.zeros((0, 10), int)
    print(detailpage_contents_recommend)
    #numpy = 행열 array다루는 메트릭스 연산 행열곱셈 연산 할 수 있는형태

    genre_info = pd.DataFrame(
        genre_vector.toarray(),
        columns=list(sorted(genre_dic.keys(), key=lambda x: genre_dic[x]))
    )

    print(genre_info)
    list_idx = animation.id - 1
    # 벡터화 후 생성된 리스트는 0부터, DB는 1부터라서 안 맞는 거였음 그래서 -1 을 하여 벡터화 인덱스와 DB 인덱스를 통일
    knn_dist, idx = neighbors.kneighbors([genre_info.iloc[list_idx, :]]) # 여기에 list_idx로 참조하면 DB는 list_idx+1값을 보니까 맞게 불러옴
    #genre_info.iloc 행 하나를 가져옴 : 전체를 가져온다. iloc 행 loc 열
    detailpage_contents_recommend = np.append(detailpage_contents_recommend, np.array(idx), axis=0)
    detailpage_contents_recommend = detailpage_contents_recommend.tolist()
    detailpage_contents_recommend = detailpage_contents_recommend[0]

    for idx in range(len(detailpage_contents_recommend)):
        detailpage_contents_recommend[idx] += 1

    same_genre_ani_list = []
    for recommend_ani_id in detailpage_contents_recommend:
        same_genre_ani = Animation.objects.get(id=recommend_ani_id)
        same_genre_ani_list.append(same_genre_ani)


    is_bookmark = Bookmark.objects.filter(user=user, animation=animation).exists()
    is_recommend = Recommend.objects.filter(user=user, animation=animation).exists()
    comments = Comment.objects.filter(animation=animation).order_by('-created_at')
    comment_count = len(Comment.objects.filter(animation=animation).order_by('-created_at'))

    return render(request, 'animation/detail.html', {
        'animation': animation,
        'genre': genre_list,
        'is_bookmark': is_bookmark,
        'is_recommend': is_recommend,
        'comments': comments,
        'comment_count': comment_count,
        'same_genre_ani_list': same_genre_ani_list,
    })

 

메인페이지에서 어려웠던 작업은 모델을 활용해서 추천을 하는 코드인데, 이해하려고 많이 노력해봤지만 역시나..

수학적인 개념들이 들어가서 설명을 들어도 코드가 돌아가는 원리를 이해하는게 정말 어려운 부분인 것 같다. 

 

#장르의 리스트를 문자열로 변형 

genre_name_list = list(map(str, genre_name_list))
#리스트는 벡터화가 안되기때문에, 리스트안에 리스트가 아닌 리스트 안에 문자열

#장르 벡터화

genre_vector = cv.fit_transform(list(map(str, genre_name_list)))  #장르 벡터화

 

#벡터화 진행후 가까운 값 10개를 가져온다.

neighbors = NearestNeighbors(n_neighbors=10).fit(genre_vector)

 

#numpy 행열, array 다루는것? 메트릭스 연산, 행열곱셈 연산을 할 수 있는 형태

detailpage_contents_recommend = np.zeros((0, 10), int)

이 외에도 몇가지 더복잡한 코드가 더있지만, 이해하는게 어려워서 더 공부를 해봐야 할것 같다.

 

이렇게 이번 프로젝트에서 사용했던 코드중에서 핵심만 골라서 정리해봤는데, 역시나 모델관련 코드는 정리하려고 해도

개념이 확실히 잡혀있지 않다보니, 정리하는것도 어려움을 느꼈다.. 계속 다루다보면 익숙해질거라 믿고 오늘의 정리는

여기까지 

Comments