{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# **Surprise 를 이용한 추천 시스템 구축**\n", "## **1 SVD 행렬분해 모델링**\n", "### **01 surprise 내부 데이터를 활용**\n", "- **ml-100k** 샘플 데이터를 활용하여 모델링을 실습합니다\n", "- **UserID, MovieID, ratting** 3개 필드만 사용하여 학습을 합니다 **(협업필터링)**\n", "- 나머지 필드는 학습에서 제외됩니다\n", "```\n", "The Reader class is used to parse a file containing ratings.\n", "Such a file is assumed to specify only one rating per line, \n", "and each line needs to respect the following structure: ::\n", "\n", " user ; item ; rating ; [timestamp]\n", "```" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.0.6\n" ] } ], "source": [ "import surprise \n", "print(surprise.__version__)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 4.13 s, sys: 17 ms, total: 4.15 s\n", "Wall time: 4.15 s\n" ] } ], "source": [ "%%time\n", "# 샘플 데이터를 불러와서 SVD 샘플모델을 생성 합니다\n", "from surprise import SVD, Dataset, accuracy \n", "from surprise.model_selection import train_test_split\n", "data = Dataset.load_builtin('ml-100k') \n", "trainset, testset = train_test_split(data, test_size=.25, random_state=0) \n", "\n", "algo = SVD()\n", "algo.fit(trainset) " ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "prediction type : size:25000\n", "prediction 예측모델의 최초 5개 결과물 추출\n" ] }, { "data": { "text/plain": [ "[Prediction(uid='120', iid='282', r_ui=4.0, est=3.7169020090370926, details={'was_impossible': False}),\n", " Prediction(uid='882', iid='291', r_ui=4.0, est=3.7336545625605893, details={'was_impossible': False}),\n", " Prediction(uid='535', iid='507', r_ui=5.0, est=3.9110320749400636, details={'was_impossible': False}),\n", " Prediction(uid='697', iid='244', r_ui=5.0, est=3.5976295337676976, details={'was_impossible': False}),\n", " Prediction(uid='751', iid='385', r_ui=4.0, est=3.4908622254480104, details={'was_impossible': False})]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "predictions = algo.test(testset)\n", "print('prediction type :{} size:{}'.format(type(predictions),len(predictions)))\n", "print('prediction 예측모델의 최초 5개 결과물 추출')\n", "predictions[:5]" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[('120', '282', 3.7169020090370926),\n", " ('882', '291', 3.7336545625605893),\n", " ('535', '507', 3.9110320749400636)]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# UserID, MovieID, ratting\n", "# DataFrame 3개 필드만 사용하여 학습을 합니다\n", "[ (pred.uid, pred.iid, pred.est) for pred in predictions[:3] ]" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "user: 196 item: 302 r_ui = None est = 4.21 {'was_impossible': False}\n" ] } ], "source": [ "# 사용자 아이디, 아이템 아이디는 를 입력하면\n", "# 결과를 출력하는 모델을 활용 \n", "uid = str(196)\n", "iid = str(302)\n", "pred = algo.predict(uid, iid)\n", "print(pred)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "RMSE: 0.9493\n" ] }, { "data": { "text/plain": [ "0.9493021636428113" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Train/ Test 훈련모델의 검증결과 95% 성능을 출력합니다\n", "accuracy.rmse(predictions)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **02 외부 CSV 데이터를 활용한 모델링**\n", "- **ml-latest-small** 데이터셋을 활용하여 **SVD** 모델을 학습합니다\n", "- **numpy.array()** 데이터를 **Surprise 데이터 셋** 으로 변환 호출합니다" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
userIdmovieIdratingtimestamp
01312.51260759144
1110293.01260759179
2110613.01260759182
\n", "
" ], "text/plain": [ " userId movieId rating timestamp\n", "0 1 31 2.5 1260759144\n", "1 1 1029 3.0 1260759179\n", "2 1 1061 3.0 1260759182" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 데이터셋 불러오기\n", "# ratings_noh.csv : header를 제거 \n", "import pandas as pd\n", "ratings = pd.read_csv('./data/ml-latest-small/ratings.csv')\n", "ratings.head(3)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1.00000000e+00, 3.10000000e+01, 2.50000000e+00, 1.26075914e+09],\n", " [1.00000000e+00, 1.02900000e+03, 3.00000000e+00, 1.26075918e+09],\n", " [1.00000000e+00, 1.06100000e+03, 3.00000000e+00, 1.26075918e+09],\n", " ...,\n", " [6.71000000e+02, 6.36500000e+03, 4.00000000e+00, 1.07094036e+09],\n", " [6.71000000e+02, 6.38500000e+03, 2.50000000e+00, 1.07097966e+09],\n", " [6.71000000e+02, 6.56500000e+03, 3.50000000e+00, 1.07478472e+09]])" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ratings.to_csv('./data/ml-latest-small/ratings_noh.csv', index=False, header=False)\n", "ratings.values" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Numpy Matrix 데이터셋을 Surprise 데이터셋으로 변환합니다\n", "from surprise import Reader, Dataset\n", "reader = Reader(line_format = 'user item rating timestamp', # 필드별 레이블\n", " sep = ',', # 구분기호\n", " rating_scale = (0.5, 5)) # 데이터 min ~ max 범위\n", "data = Dataset.load_from_file('./data/ml-latest-small/ratings_noh.csv', reader=reader)\n", "data" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "RMSE: 0.8908\n" ] }, { "data": { "text/plain": [ "0.8907754769926038" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# n_factors=50 : 학습 모델의 잠재요인 K를 50개로 설정 합니다\n", "from surprise.model_selection import train_test_split\n", "trainset, testset = train_test_split(data, test_size=.25, random_state=0)\n", "algo = SVD(n_factors=50, random_state=0) \n", "algo.fit(trainset) \n", "\n", "# testset 으로 모델을 RMSE 평가 합니다\n", "from surprise import accuracy \n", "predictions = algo.test(testset)\n", "accuracy.rmse(predictions)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **03 Pandas DataFrame을 활용한 모델링**\n", "- **ml-latest-small** 데이터셋을 활용하여 **SVD** 모델을 학습합니다\n", "- **numpy.array()** 데이터를 **Surprise 데이터 셋** 으로 변환 호출합니다\n", "- 데이터 분석 필드는 **MovieLens** 데이터셋을 Default로 정의되어 있습니다" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
userIdmovieIdratingtimestamp
01312.51260759144
1110293.01260759179
2110613.01260759182
\n", "
" ], "text/plain": [ " userId movieId rating timestamp\n", "0 1 31 2.5 1260759144\n", "1 1 1029 3.0 1260759179\n", "2 1 1061 3.0 1260759182" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "from surprise import Reader, Dataset\n", "\n", "ratings = pd.read_csv('./data/ml-latest-small/ratings.csv') \n", "ratings.head(3)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "RMSE: 0.8908\n" ] }, { "data": { "text/plain": [ "0.8907754769926038" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# ratings DataFrame 에서 컬럼은 사용자 아이디, 아이템 아이디, 평점 순서를 지켜야 합니다. \n", "reader = Reader(rating_scale=(0.5, 5.0))\n", "data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)\n", "algo = SVD(n_factors=50, random_state=0)\n", "\n", "trainset, testset = train_test_split(data, test_size=.25, random_state=0)\n", "algo.fit(trainset)\n", "predictions = algo.test( testset )\n", "accuracy.rmse(predictions)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **2 교차 검증(Cross Validation)과 하이퍼 파라미터 튜닝**\n", "### **CSV 데이터셋을 활용**\n", "**cv=5 :** cross-validation iterator 약어로, KFold 검증 구분 갯수를 입력합니다" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Evaluating RMSE, MAE of algorithm SVD on 5 split(s).\n", "\n", " Fold 1 Fold 2 Fold 3 Fold 4 Fold 5 Mean Std \n", "RMSE (testset) 0.8922 0.8964 0.8896 0.9073 0.8931 0.8957 0.0062 \n", "MAE (testset) 0.6891 0.6923 0.6845 0.6966 0.6864 0.6898 0.0043 \n", "Fit time 3.99 3.93 3.95 3.93 4.06 3.97 0.05 \n", "Test time 0.13 0.18 0.18 0.13 0.18 0.16 0.03 \n", "CPU times: user 21.5 s, sys: 23.9 ms, total: 21.5 s\n", "Wall time: 21.5 s\n" ] } ], "source": [ "%%time\n", "from surprise.model_selection import cross_validate \n", "ratings = pd.read_csv('./data/ml-latest-small/ratings.csv')\n", "reader = Reader(rating_scale = (0.5, 5.0)) # Reader 인스턴스\n", "data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)\n", "algo = SVD(random_state = 0) \n", "cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True) " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Bast Score: 0.900002299884766\n", "Params: {'n_epochs': 20, 'n_factors': 50}\n", "CPU times: user 1min 7s, sys: 405 ms, total: 1min 7s\n", "Wall time: 2min 5s\n" ] } ], "source": [ "%%time\n", "# 최적화 검증용 파라미터, GridSearchCV (KFold CV 3개 분할)\n", "from surprise.model_selection import GridSearchCV\n", "param_grid = {'n_epochs': [20, 40, 60], 'n_factors': [50, 100, 200] }\n", "gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=3, n_jobs=-1)\n", "gs.fit(data)\n", "\n", "# 최고 RMSE Evaluation 점수와 그때의 하이퍼 파라미터\n", "print(\"Bast Score: {}\\nParams: {}\".format(\n", " gs.best_score['rmse'], gs.best_params['rmse']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **3 Surprise 를 이용한 개인화 영화 추천 시스템 구축**\n", "- 기본소스는 `AttributeError: 'DatasetAutoFolds' object has no attribute 'global_mean'` 오류를 출력\n", "- 이는 데이터가 동일해서 발생한 문제로, **Train/ Test로 구분한 뒤** 입력해야 합니다\n", "[stackoverflow](https://stackoverflow.com/questions/49263964/datasetautofolds-object-has-no-attribute-global-mean-on-python-surprise)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **01 전체 데이터셋을 활용하여 학습 후 활용**\n", "- **MovieLens 전체 데이터셋을** 학습한 뒤 활용합니다\n", "- 이번에는 **비어있는 평점 정보를 학습 모델로 채웁니다**" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 아래 코드는 train_test_split( ) 구분 없어도 오류없이 출력합니다\n", "# 최신버전 에서는 오류를 출력하지 않고 바로 결과를 출력 합니다\n", "import pandas as pd\n", "from surprise import Reader, Dataset, SVD\n", "ratings = pd.read_csv('./data/ml-latest-small/ratings.csv')\n", "reader = Reader(line_format = 'user item rating timestamp', \n", " sep = ',', \n", " rating_scale = (0.5, 5))\n", "data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)\n", "trainset = data.build_full_trainset()\n", "algo = SVD(n_factors=50, random_state=0)\n", "algo.fit(trainset)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "# # 데이터를 모두 사용하여 학습을 진행합니다\n", "# from surprise.dataset import DatasetAutoFolds\n", "# reader = Reader(line_format = 'user item rating timestamp', \n", "# sep = ',', \n", "# rating_scale = (0.5, 5))\n", "\n", "# # DatasetAutoFolds 클래스로 파일을 불러옵니다 \n", "# data_folds = DatasetAutoFolds(ratings_file = './data/ml-latest-small/ratings_noh.csv', \n", "# reader = reader)\n", "# trainset = data_folds.build_full_trainset()\n", "# algo = SVD(n_epochs=20, n_factors=50, random_state=0)\n", "# algo.fit(trainset)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
movieIdtitlegenres
01Toy Story (1995)Adventure|Animation|Children|Comedy|Fantasy
12Jumanji (1995)Adventure|Children|Fantasy
23Grumpier Old Men (1995)Comedy|Romance
\n", "
" ], "text/plain": [ " movieId title \\\n", "0 1 Toy Story (1995) \n", "1 2 Jumanji (1995) \n", "2 3 Grumpier Old Men (1995) \n", "\n", " genres \n", "0 Adventure|Animation|Children|Comedy|Fantasy \n", "1 Adventure|Children|Fantasy \n", "2 Comedy|Romance " ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 영화에 대한 상세 속성 정보 DataFrame로딩\n", "movies = pd.read_csv('./data/ml-latest-small/movies.csv')\n", "movies.head(3)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "사용자 아이디 9는 영화 아이디 42의 평점 없음\n", " movieId title genres\n", "40 42 Dead Presidents (1995) Action|Crime|Drama\n" ] } ], "source": [ "# userId=9 의 movieId 데이터 추출하여 movieId=42 데이터가 있는지 확인. \n", "movieIds = ratings[ratings['userId']==9]['movieId']\n", "if movieIds[movieIds==42].count() == 0:\n", " print('사용자 아이디 9는 영화 아이디 42의 평점 없음')\n", "\n", "# movies 데이터셋에서 42번 id 로 정보를 추출합니다\n", "print(movies[movies['movieId']==42])" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "user: 9 item: 42 r_ui = None est = 3.54 {'was_impossible': False}\n" ] } ], "source": [ "# 9번 사용자 42번 영화평점이 없을 때, 예측평점을 채웁니다\n", "uid = str(9)\n", "iid = str(42)\n", "pred = algo.predict(uid, iid, verbose=True)" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "평점 매긴 영화수: 45\n", "추천 대상 영화수: 9,080\n", "전체 영화수: 9,125\n" ] } ], "source": [ "# 영화정보를 보기 쉽도록 함수로 구현\n", "def get_unseen_surprise(ratings, movies, userId):\n", " seen_movies = ratings[ratings['userId']== userId]['movieId'].tolist() # 입력 userId 평점목록\n", " total_movies = movies['movieId'].tolist() # 모든 movieId 리스트 \n", " unseen_movies= [movie for movie in total_movies # 평점이 없는 movieId 목록\n", " if movie not in seen_movies]\n", " print('평점 매긴 영화수: {}\\n추천 대상 영화수: {:,}\\n전체 영화수: {:,}'.format(\n", " len(seen_movies),len(unseen_movies),len(total_movies))) \n", " return unseen_movies\n", "\n", "# 9번 사용자의 추천목록을 검색\n", "unseen_movies = get_unseen_surprise(ratings, movies, 9)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.3" } }, "nbformat": 4, "nbformat_minor": 2 }