프로젝트

(프로젝트)머신러닝_Netflix 영화 데이터셋을 이용한 '협업 필터링' + '콘텐츠 기반 필터링' 영화 추천_2) 콘텐츠 기반 필터링_cosine similarity

하방주인장 2023. 6. 25. 13:29

 

목차

     

    1. Netflix Movie Recommendation System: Content Based Filtering

    https://www.kaggle.com/datasets/shivamb/netflix-shows

     

    Netflix Movies and TV Shows

    Listings of movies and tv shows on Netflix - Regularly Updated

    www.kaggle.com

     

     

    - Content Based Filtering(콘텐츠 기반 필터링) 이란?

    콘텐츠 기반 필터링은 아이템 간 유사성을 사용하여 사용자가 좋아하는 아이템과 유사한 항목을 추천하는 방법이다. 예를 들어, 사용자 A가 귀여운 고양이 동영상을 2개 시청하면 시스템은 사용자에게 귀여운 동물 동영상을 추천해주는 것이다. 

     

    이번 프로젝트에서는 아이템의 비슷한 정도(유사도)를 수치로 계산하기 위해 아이템을 벡터 형태로 표현하고 이들 벡터 간의 유사도 계산을 하여 유사도가 높은 순으로 추천을 할 것 이다. 때문에, 원 핫 인코딩을 사용하여 넷플릭스 영화 데이터셋의 Categorical feature(범주형 데이터)를 0과 1로 표현하여 범주의 개수를 크기로 갖는 벡터를 만들기로 한다.

     

    https://developers.google.com/machine-learning/recommendation/overview/candidate-generation?hl=ko 

     

    후보 생성 개요  |  Machine Learning  |  Google for Developers

    이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 의견 보내기 후보 생성 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 후보 생성은

    developers.google.com

     

    https://tech.kakao.com/2021/12/27/content-based-filtering-in-kakao/

     

    카카오 AI추천 : 카카오의 콘텐츠 기반 필터링 (Content-based Filtering in Kakao)

    카카오 서비스 사용자들의 아이템(콘텐츠 또는 상품) 소비 패턴을 살펴보면, 기존에 소비한 아이템과 유사한 아이템을 소비하는 경우를 쉽게 찾아볼 수 있습니다. 예를 들면, 브런치의 특정 작

    tech.kakao.com

     

     

    01) Business Understanding

    사용자가 시청한 영화와 비슷한 내용이거나 특별한 관계가 있는 영화를 추천

     

    02) Data Understanding

    2-1. Data Load

    import pandas as pd
    df = pd.read_csv('data/netflix_titles.csv')

     

    2-2. EDA

    # Null 값 확인
    df.info() # null 값 있음
    
    # 출시 년도 확인
    df.describe() # 1925~2021년
    
    # object describe 확인
    df.describe(include='object')

     

    2-3. Data Preparation

    - director

    # 1. 감독이 여러 명일 경우 첫 번째 감독 이름만 적용
    
    # 감독 이름 중 ','가 포함된 경우
    df['director'].fillna('').apply(lambda x: ',' in x).sum() # 614
    
    # 감독이 여러 명일 경우 가장 첫 번째 감독 이름만 적용한 후 unique한 감독 수
    df['director'].fillna('').apply(lambda x: x.split(',')[0].strip()).nunique() # 4406

     

    총 4406명의 unique한 감독이 있다. 데이터의 행이 약 8800개인데, one-hot encoding으로 4400개의 행 추가를 하는 것은 얻을 것이 적고 평균 한 감독에 대해 같은 감독의 다른 영화 한 개를 더 추천받을 수 있는 정도이므로 컬럼 사용을 안하기로 결정했다.

     

    - Cast

    # 각 행에 있는 배우들 리스트 생성
    cast_dict = {}
    for cast_list in df['cast'].fillna('').apply(lambda x: x.split(',')):
        if cast_list:
            # cast는 각 배우(1명)
            for cast in cast_list:
                if cast.strip() in cast_dict.keys():
                    cast_dict[cast.strip()] +=1
                else:
                    # 키를 추가
                    cast_dict[cast.strip()] = 1
                    
    # cast_df 생성
    cast_df = pd.DataFrame(cast_dict.items(), columns=['cast','freq'])
    
    cast_df.describe() # min, 25%, 50% = 1, 75% = 2, max = 825
    
    # 작품이 4개 이상인 배우
    cast_df[cast_df['freq'].gt(3)].sort_values(by='freq', ascending=False)
    
    # cast_list_enc 생성
    cast_list_enc = cast_df['cast'][cast_df['freq'].gt(3)].tolist()[1:] # [1:] 슬라이싱 통해 빈값을 제거
    cast_list_enc.sort() # cast_list_enc = sorted(cast_list_enc)
    
    # 각 영화의 배우들 리스트 cast_list 생성 
    cast_one_hot_list = []
    for cast_list in df['cast'].fillna('').apply(lambda x: x.split(',')):
        tmp_list = [0] * len(cast_list_enc) # 3246개의 0으로 채움
        if cast_list:
            # cast: 각 배우
            for cast in cast_list:
                if cast.strip() in cast_list_enc:
                    tmp_list[cast_list_enc.index(cast.strip())] = 1 # 해당 인덱스에 1로 채움
        cast_one_hot_list.append(tmp_list)
    
    len(cast_one_hot_list) # 8807 => 맞게 추출됌
    
    # cast_one_hot_df 생성
    cast_one_hot_df = pd.DataFrame(cast_one_hot_list, columns=cast_list_enc) # 각 컬럼은 해당 영화/TV Show에 출연했는지 여부를 의미

    작품이 4개 이상인 배우가 3247명으로 데이터행(약 8,800개) 보다 작은 수이기 때문에 one-hot encoding으로 사용하기로 결정했다.

     

    - Country

    # 유니크한 개수
    df['country'].nunique() # 748
    
    # country에 , 가 포함된 개수
    df['country'].fillna('').str.contains(',').sum() # 1320
    
    # 여러 나라가 있을 경우, 가장 앞에 있는 나라만 포함
    df['country'].fillna('country_na').apply(lambda x: x.split(',')[0].strip()).nunique() # 87
    country_dummy = pd.get_dummies(df['country'].fillna('country_na').apply(lambda x: x.split(',')[0].strip())).drop(columns=['','country_na'])

    country 컬럼의 유니크한 개수는 748개이고 ,가 포함된 개수는 1320개 이다. 때문에, 여러 나라가 있을 경우, 가장 앞에 있는 나라만 포함하는 것을 전재로 하여 one-hot encoding을 진행했다.

     

    - date_added

    # Null 값 확인
    df['date_added'].info()
    
    # 출시 연도의 값으로 Null 값 채우기
    df.loc[df['date_added'].isnull(), 'date_added'] = df[df['date_added'].isnull()]['release_year']
    
    # date_added 컬럼의 연도로 year 컬럼 만들기
    df['year'] = df['date_added'].apply(lambda x: str(x)[-4:]).astype('int')

    date_added 컬럼은 넷플릭스에 포함된 연도이다. 해당 컬럼은 Null 값이 존재함으로 출시연도의 값으로 Null 값으로 채우고 date_added 컬럼의 연도만 추출하여 'year' 컬럼을 생성한다.

     

    - rating

    # 빈도가 가장 높은 값 확인
    df['rating'].describe() # TV-MA
    
    # 빈도가 가장 높은 값으로 null 값 fill
    rating_dummy = pd.get_dummies(df['rating'].fillna('TV-MA'), prefix='rating')

    rating 컬럼에는 Null값이 존재함으로 빈도가 가장 높은 값으로 Null 값을 채워준다.

     

    - duration

    # 범주화
    def apply_duration(x):
        if 'Season' in x:
            return 'season'
        elif 'min' in x:
            min_num = int(x.split()[0])
            if min_num < 60:
                return 'short_duration'
            elif min_num < 120:
                return 'middle_duration'
            elif min_num < 180:
                return 'long_duration'
            else:
                return 'longer_duration'
        else:
            raise ValueError('Value Error')
            
    # 원핫인코딩
    duration_dummy = pd.get_dummies(df['duration'].fillna('150 min').apply(apply_duration))

    duration 컬럼은 러닝타임에 대한 컬럼인데 One-hot encoding을 위해 범주화를 해주어야 한다. 때문에, season은 season으로 60분 보다 작으면 short-duration, 120 보다 작으면 middle-duration, 180 보다 작으면 long-duration, 나머지는 longer-duration으로 변경해준다. 

     

    - listed in

    # 가장 앞에 있는 장르만 선택
    df['listed_in'].apply(lambda x: x.split(',')[0].strip())
    
    # 원핫인코딩
    listed_in_dummy = pd.get_dummies(df['listed_in'].apply(lambda x: x.split(',')[0].strip()))

    여러 장르가 포함된 경우 가장 앞에 있는 장르만 선택하여 원핫인코딩을 진행한다.

     

     

    - 데이터 합치기

    # 기존 df에서 활용한 컬럼만 추출 (+ type 컬럼 get dummpy (Movie/ TV Show))
    df_concat = pd.get_dummies(df, columns=['type'])[['release_year', 'year', 'type_Movie', 'type_TV Show']]
    
    # one-hot encoding이 된 컬럼은 어차피 0, 1로 구성되어있기 때문에 전체 df 에 Scaling을 적용해도 문제가 되지않음
    # 연도 관련 컬럼에 대해 minmax scaling 적용
    from sklearn.preprocessing import minmax_scale
    df_concat.loc[:,:] = minmax_scale(df_concat) # DataFrame 형태 유지(컬럼명도 유지)
    
    # 병합
    df_concat = pd.concat([df_concat, listed_in_dummy, cast_one_hot_df, country_dummy, duration_dummy, rating_dummy], axis=1)
    
    # 임베딩 생성
    embeddings = df_concat.values
    embeddings.shape # (8807, 3393)
    
    # 콘텐츠 기반 전처리 끝난 df 저장
    df.to_csv('data/cb_df.csv', index=False)

    기존 데이터프레임에서 활용한 컬럼만 추출하여 합쳐서 df_concat 을 만들고 연도 관련 컬럼에 대해 minmax scaling을 적용하여 정규화를 진행한 후 원핫인코딩을 진행한 df들과 최종적으로 합쳐서 embedding을 생성한다.

     

    03) Modeling

    # matrix 들을 넣어주면 각 행과 열을 모두 연산을 해줌
    from sklearn.metrics.pairwise import cosine_similarity
    cosine_similarity_matrix = cosine_similarity(embeddings, embeddings)
    cosine_similarity_matrix.shape # (8807, 8807)
    
    # model 저장
    import pickle 
    with open('data/cosine_similarity_matrix.pickle','wb') as fw:
        pickle.dump(cosine_similarity_matrix, fw)

     

    04) Deployment

    # 영화 제목 입력 시 index를 반환하는 함수 생성
    def get_index(title):
        index = df.index[df['title'] == title][0]
        return index    
    
    # 유사한 영화를 추천해주는 함수 생성
    def most_similar(idx, top_n=10):
        df['cosine_similarity'] = cosine_similarity_matrix[idx]
        return df.sort_values(by='cosine_similarity', ascending=False)[:top_n]
        
    index = get_index('Operation Chromite') # 7670
    most_similar(7670, top_n=4)

    콘텐츠 기반 영화 추천 결과