├── .gitignore ├── CF.ipynb ├── Cross Validation.ipynb ├── LICENSE ├── MovieLens.ipynb ├── Readme.md ├── TopK.ipynb ├── alpha.ipynb ├── main.py ├── predict.py ├── report ├── K-figure.png ├── Plot │ ├── Plot.html │ ├── Plot.js │ ├── echarts.min.js │ └── theme │ │ ├── macarons.js │ │ └── vintage.js ├── Report.bib ├── Report.pdf ├── Report.tex ├── alpha-figure.png └── rating-pie.png ├── utils.py └── var.py /.gitignore: -------------------------------------------------------------------------------- 1 | dataset/ 2 | __pycache__/ 3 | papers/ 4 | .ipynb* 5 | 6 | *.log 7 | *.aux 8 | *.out 9 | *.toc 10 | *.gz 11 | *.blg 12 | *.bbl 13 | *.fls 14 | 15 | -------------------------------------------------------------------------------- /CF.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import pandas as pd\n", 12 | "import numpy as np\n", 13 | "# import matplotlib.pyplot as plt" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 2, 19 | "metadata": { 20 | "collapsed": false 21 | }, 22 | "outputs": [ 23 | { 24 | "name": "stdout", 25 | "output_type": "stream", 26 | "text": [ 27 | "初始化变量...\n" 28 | ] 29 | } 30 | ], 31 | "source": [ 32 | "print('初始化变量...')\n", 33 | "names = ['user_id', 'item_id', 'rating', 'timestamp']\n", 34 | "trainingset_file = 'dataset/ml-100k/u3.base'\n", 35 | "testset_file= 'dataset/ml-100k/u3.test'\n", 36 | "n_users = 943\n", 37 | "n_items = 1682\n", 38 | "ratings = np.zeros((n_users, n_items))" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 3, 44 | "metadata": { 45 | "collapsed": false, 46 | "scrolled": true 47 | }, 48 | "outputs": [ 49 | { 50 | "name": "stdout", 51 | "output_type": "stream", 52 | "text": [ 53 | "载入训练集...\n", 54 | "数据集样例为:\n", 55 | " user_id item_id rating timestamp\n", 56 | "0 1 1 5 874965758\n", 57 | "1 1 2 3 876893171\n", 58 | "2 1 3 4 878542960\n", 59 | "3 1 4 3 876893119\n", 60 | "4 1 6 5 887431973\n", 61 | "载入完成.\n", 62 | "打分矩阵规模为 943*1682.\n", 63 | "测试集有效打分个数为 80000.\n" 64 | ] 65 | } 66 | ], 67 | "source": [ 68 | "df = pd.read_csv(trainingset_file, sep='\\t', names=names)\n", 69 | "print('载入训练集...')\n", 70 | "print('数据集样例为:')\n", 71 | "print(df.head())\n", 72 | "for row in df.itertuples():\n", 73 | " ratings[row[1]-1, row[2]-1] = row[3]\n", 74 | "print('载入完成.')\n", 75 | "print('打分矩阵规模为 %d*%d.' % (n_users, n_items))\n", 76 | "print('训练集有效打分个数为 %d.' % len(df))" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 4, 82 | "metadata": { 83 | "collapsed": false 84 | }, 85 | "outputs": [ 86 | { 87 | "name": "stdout", 88 | "output_type": "stream", 89 | "text": [ 90 | "测试集矩阵密度为: 5.04%\n", 91 | "\n" 92 | ] 93 | } 94 | ], 95 | "source": [ 96 | "# 计算矩阵密度\n", 97 | "def cal_sparsity():\n", 98 | " sparsity = float(len(ratings.nonzero()[0]))\n", 99 | " sparsity /= (ratings.shape[0] * ratings.shape[1])\n", 100 | " sparsity *= 100\n", 101 | " print('训练集矩阵密度为: {:4.2f}%'.format(sparsity))\n", 102 | "\n", 103 | "cal_sparsity()\n", 104 | "print()" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 5, 110 | "metadata": { 111 | "collapsed": false 112 | }, 113 | "outputs": [], 114 | "source": [ 115 | "def rmse(pred, actual):\n", 116 | " '''计算预测结果的rmse'''\n", 117 | " from sklearn.metrics import mean_squared_error\n", 118 | " pred = pred[actual.nonzero()].flatten()\n", 119 | " actual = actual[actual.nonzero()].flatten()\n", 120 | " return np.sqrt(mean_squared_error(pred, actual))" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 6, 126 | "metadata": { 127 | "collapsed": false 128 | }, 129 | "outputs": [ 130 | { 131 | "name": "stdout", 132 | "output_type": "stream", 133 | "text": [ 134 | "------ Naive算法(baseline) ------\n" 135 | ] 136 | } 137 | ], 138 | "source": [ 139 | "print('------ Naive算法(baseline) ------')" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 7, 145 | "metadata": { 146 | "collapsed": false 147 | }, 148 | "outputs": [], 149 | "source": [ 150 | "def cal_mean():\n", 151 | " '''Calculate mean value'''\n", 152 | " print('计算总体均值,各user打分均值,各item打分均值...')\n", 153 | " global all_mean, user_mean, item_mean\n", 154 | " all_mean = np.mean(ratings[ratings!=0])\n", 155 | " user_mean = sum(ratings.T) / sum((ratings!=0).T)\n", 156 | " item_mean = sum(ratings) / sum((ratings!=0))\n", 157 | " print('是否存在User/Item 均值为NaN?', np.isnan(user_mean).any(), np.isnan(item_mean).any())\n", 158 | " print('对NaN填充总体均值...')\n", 159 | " user_mean = np.where(np.isnan(user_mean), all_mean, user_mean)\n", 160 | " item_mean = np.where(np.isnan(item_mean), all_mean, item_mean)\n", 161 | " print('是否存在User/Item 均值为NaN?', np.isnan(user_mean).any(), np.isnan(item_mean).any())\n", 162 | " print('均值计算完成,总体打分均值为 %.4f' % all_mean)" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 8, 168 | "metadata": { 169 | "collapsed": false 170 | }, 171 | "outputs": [ 172 | { 173 | "name": "stdout", 174 | "output_type": "stream", 175 | "text": [ 176 | "计算训练集各项统计数据...\n", 177 | "计算总体均值,各user打分均值,各item打分均值...\n", 178 | "是否存在User/Item 均值为NaN? False True\n", 179 | "对NaN填充总体均值...\n", 180 | "是否存在User/Item 均值为NaN? False False\n", 181 | "均值计算完成,总体打分均值为 3.5311\n" 182 | ] 183 | } 184 | ], 185 | "source": [ 186 | "print('计算训练集各项统计数据...')\n", 187 | "cal_mean()" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": 9, 193 | "metadata": { 194 | "collapsed": false 195 | }, 196 | "outputs": [], 197 | "source": [ 198 | "def predict_naive(user, item):\n", 199 | " prediction = item_mean[item] + user_mean[user] - all_mean\n", 200 | " return prediction" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": 10, 206 | "metadata": { 207 | "collapsed": false 208 | }, 209 | "outputs": [ 210 | { 211 | "name": "stdout", 212 | "output_type": "stream", 213 | "text": [ 214 | "载入测试集...\n", 215 | "测试集大小为 20000\n", 216 | "采用Naive算法进行预测...\n", 217 | "测试结果的rmse为 0.9691\n", 218 | "\n" 219 | ] 220 | } 221 | ], 222 | "source": [ 223 | "print('载入测试集...')\n", 224 | "test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 225 | "test_df.head()\n", 226 | "predictions = []\n", 227 | "targets = []\n", 228 | "print('测试集大小为 %d' % len(test_df))\n", 229 | "print('采用Naive算法进行预测...')\n", 230 | "for row in test_df.itertuples():\n", 231 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 232 | " predictions.append(predict_naive(user, item))\n", 233 | " targets.append(actual)\n", 234 | "\n", 235 | "print('测试结果的rmse为 %.4f' % rmse(np.array(predictions), np.array(targets)))\n", 236 | "print()" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": 11, 242 | "metadata": { 243 | "collapsed": false 244 | }, 245 | "outputs": [ 246 | { 247 | "name": "stdout", 248 | "output_type": "stream", 249 | "text": [ 250 | "------ item-item协同过滤算法(相似度未归一化) ------\n" 251 | ] 252 | } 253 | ], 254 | "source": [ 255 | "print('------ item-item协同过滤算法(相似度未归一化) ------')" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": 12, 261 | "metadata": { 262 | "collapsed": false 263 | }, 264 | "outputs": [], 265 | "source": [ 266 | "def cal_similarity(ratings, kind, epsilon=1e-9):\n", 267 | " '''利用Cosine距离计算相似度'''\n", 268 | " '''epsilon: 防止Divide-by-zero错误,进行矫正'''\n", 269 | " if kind == 'user':\n", 270 | " sim = ratings.dot(ratings.T) + epsilon\n", 271 | " elif kind == 'item':\n", 272 | " sim = ratings.T.dot(ratings) + epsilon\n", 273 | " norms = np.array([np.sqrt(np.diagonal(sim))])\n", 274 | " return (sim / norms / norms.T)" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 13, 280 | "metadata": { 281 | "collapsed": false 282 | }, 283 | "outputs": [ 284 | { 285 | "name": "stdout", 286 | "output_type": "stream", 287 | "text": [ 288 | "计算相似度矩阵...\n", 289 | "计算完成.\n", 290 | "相似度矩阵样例: (item-item)\n", 291 | "[[ 1. 0.296 0.279 0.388 0.252 0.114 0.518 0.41 0.416 0.199]\n", 292 | " [ 0.296 1. 0.177 0.405 0.211 0.099 0.331 0.31 0.207 0.152]\n", 293 | " [ 0.279 0.177 1. 0.275 0.118 0.104 0.311 0.125 0.207 0.121]\n", 294 | " [ 0.388 0.405 0.275 1. 0.265 0.091 0.411 0.391 0.357 0.219]\n", 295 | " [ 0.252 0.211 0.118 0.265 1. 0.016 0.28 0.214 0.202 0.031]\n", 296 | " [ 0.114 0.099 0.104 0.091 0.016 1. 0.128 0.065 0.164 0.139]\n", 297 | " [ 0.518 0.331 0.311 0.411 0.28 0.128 1. 0.342 0.43 0.279]\n", 298 | " [ 0.41 0.31 0.125 0.391 0.214 0.065 0.342 1. 0.364 0.166]\n", 299 | " [ 0.416 0.207 0.207 0.357 0.202 0.164 0.43 0.364 1. 0.25 ]\n", 300 | " [ 0.199 0.152 0.121 0.219 0.031 0.139 0.279 0.166 0.25 1. ]]\n" 301 | ] 302 | } 303 | ], 304 | "source": [ 305 | "print('计算相似度矩阵...')\n", 306 | "user_similarity = cal_similarity(ratings, kind='user')\n", 307 | "item_similarity = cal_similarity(ratings, kind='item')\n", 308 | "print('计算完成.')\n", 309 | "print('相似度矩阵样例: (item-item)')\n", 310 | "print(np.round_(item_similarity[:10,:10], 3))" 311 | ] 312 | }, 313 | { 314 | "cell_type": "code", 315 | "execution_count": 14, 316 | "metadata": { 317 | "collapsed": false 318 | }, 319 | "outputs": [], 320 | "source": [ 321 | "def predict_itemCF(user, item, k=100):\n", 322 | " '''item-item协同过滤算法,预测rating'''\n", 323 | " nzero = ratings[user].nonzero()[0]\n", 324 | " prediction = ratings[user, nzero].dot(item_similarity[item, nzero])\\\n", 325 | " / sum(item_similarity[item, nzero])\n", 326 | " return prediction" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": 15, 332 | "metadata": { 333 | "collapsed": false 334 | }, 335 | "outputs": [ 336 | { 337 | "name": "stdout", 338 | "output_type": "stream", 339 | "text": [ 340 | "载入测试集...\n", 341 | "测试集大小为 20000\n", 342 | "采用item-item协同过滤算法进行预测...\n", 343 | "测试结果的rmse为 1.0042\n", 344 | "\n" 345 | ] 346 | } 347 | ], 348 | "source": [ 349 | "print('载入测试集...')\n", 350 | "test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 351 | "test_df.head()\n", 352 | "predictions = []\n", 353 | "targets = []\n", 354 | "print('测试集大小为 %d' % len(test_df))\n", 355 | "print('采用item-item协同过滤算法进行预测...')\n", 356 | "for row in test_df.itertuples():\n", 357 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 358 | " predictions.append(predict_itemCF(user, item))\n", 359 | " targets.append(actual)\n", 360 | "\n", 361 | "print('测试结果的rmse为 %.4f' % rmse(np.array(predictions), np.array(targets)))\n", 362 | "print()" 363 | ] 364 | }, 365 | { 366 | "cell_type": "code", 367 | "execution_count": 16, 368 | "metadata": { 369 | "collapsed": false 370 | }, 371 | "outputs": [ 372 | { 373 | "name": "stdout", 374 | "output_type": "stream", 375 | "text": [ 376 | "------ 结合baseline的item-item协同过滤算法(相似度未归一化) ------\n" 377 | ] 378 | } 379 | ], 380 | "source": [ 381 | "print('------ 结合baseline的item-item协同过滤算法(相似度未归一化) ------')" 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": 17, 387 | "metadata": { 388 | "collapsed": false 389 | }, 390 | "outputs": [], 391 | "source": [ 392 | "def predict_itemCF_baseline(user, item, k=100):\n", 393 | " '''结合baseline的item-item CF算法,预测rating'''\n", 394 | " nzero = ratings[user].nonzero()[0]\n", 395 | " baseline = item_mean + user_mean[user] - all_mean\n", 396 | " prediction = (ratings[user, nzero] - baseline[nzero]).dot(item_similarity[item, nzero])\\\n", 397 | " / sum(item_similarity[item, nzero]) + baseline[item]\n", 398 | " return prediction " 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": 18, 404 | "metadata": { 405 | "collapsed": false 406 | }, 407 | "outputs": [ 408 | { 409 | "name": "stdout", 410 | "output_type": "stream", 411 | "text": [ 412 | "载入测试集...\n", 413 | "测试集大小为 20000\n", 414 | "采用结合baseline的item-item协同过滤算法进行预测...\n", 415 | "测试结果的rmse为 0.9345\n", 416 | "\n" 417 | ] 418 | } 419 | ], 420 | "source": [ 421 | "print('载入测试集...')\n", 422 | "test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 423 | "test_df.head()\n", 424 | "predictions = []\n", 425 | "targets = []\n", 426 | "print('测试集大小为 %d' % len(test_df))\n", 427 | "print('采用结合baseline的item-item协同过滤算法进行预测...')\n", 428 | "for row in test_df.itertuples():\n", 429 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 430 | " predictions.append(predict_itemCF_baseline(user, item))\n", 431 | " targets.append(actual)\n", 432 | "\n", 433 | "print('测试结果的rmse为 %.4f' % rmse(np.array(predictions), np.array(targets)))\n", 434 | "print()" 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": 19, 440 | "metadata": { 441 | "collapsed": false 442 | }, 443 | "outputs": [ 444 | { 445 | "name": "stdout", 446 | "output_type": "stream", 447 | "text": [ 448 | "------ user-user协同过滤算法(相似度未归一化) ------\n", 449 | "载入测试集...\n", 450 | "测试集大小为 20000\n", 451 | "采用user-user协同过滤算法进行预测...\n", 452 | "测试结果的rmse为 1.0133\n", 453 | "\n" 454 | ] 455 | } 456 | ], 457 | "source": [ 458 | "print('------ user-user协同过滤算法(相似度未归一化) ------')\n", 459 | "\n", 460 | "def predict_userCF(user, item, k=100):\n", 461 | " '''user-user协同过滤算法,预测rating'''\n", 462 | " nzero = ratings[:,item].nonzero()[0]\n", 463 | " baseline = user_mean + item_mean[item] - all_mean\n", 464 | " prediction = ratings[nzero, item].dot(user_similarity[user, nzero])\\\n", 465 | " / sum(user_similarity[user, nzero])\n", 466 | " # 冷启动问题: 该item暂时没有评分\n", 467 | " if np.isnan(prediction):\n", 468 | " prediction = baseline[user]\n", 469 | " return prediction\n", 470 | "\n", 471 | "print('载入测试集...')\n", 472 | "test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 473 | "test_df.head()\n", 474 | "predictions = []\n", 475 | "targets = []\n", 476 | "print('测试集大小为 %d' % len(test_df))\n", 477 | "print('采用user-user协同过滤算法进行预测...')\n", 478 | "\n", 479 | "for row in test_df.itertuples():\n", 480 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 481 | " predictions.append(predict_userCF(user, item))\n", 482 | " targets.append(actual)\n", 483 | "\n", 484 | "print('测试结果的rmse为 %.4f' % rmse(np.array(predictions), np.array(targets)))\n", 485 | "print()" 486 | ] 487 | }, 488 | { 489 | "cell_type": "code", 490 | "execution_count": 20, 491 | "metadata": { 492 | "collapsed": false 493 | }, 494 | "outputs": [ 495 | { 496 | "name": "stdout", 497 | "output_type": "stream", 498 | "text": [ 499 | "------ 结合baseline的user-user协同过滤算法(相似度未归一化) ------\n", 500 | "载入测试集...\n", 501 | "测试集大小为 20000\n", 502 | "采用结合baseline的user-user协同过滤算法进行预测...\n", 503 | "测试结果的rmse为 0.9519\n", 504 | "\n" 505 | ] 506 | } 507 | ], 508 | "source": [ 509 | "print('------ 结合baseline的user-user协同过滤算法(相似度未归一化) ------')\n", 510 | "\n", 511 | "def predict_userCF_baseline(user, item, k=100):\n", 512 | " '''结合baseline的user-user协同过滤算法,预测rating'''\n", 513 | " nzero = ratings[:,item].nonzero()[0]\n", 514 | " baseline = user_mean + item_mean[item] - all_mean\n", 515 | " prediction = (ratings[nzero, item] - baseline[nzero]).dot(user_similarity[user, nzero])\\\n", 516 | " / sum(user_similarity[user, nzero]) + baseline[user]\n", 517 | " if np.isnan(prediction):\n", 518 | " prediction = baseline[user]\n", 519 | " return prediction\n", 520 | "\n", 521 | "print('载入测试集...')\n", 522 | "test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 523 | "test_df.head()\n", 524 | "predictions = []\n", 525 | "targets = []\n", 526 | "print('测试集大小为 %d' % len(test_df))\n", 527 | "print('采用结合baseline的user-user协同过滤算法进行预测...')\n", 528 | "\n", 529 | "for row in test_df.itertuples():\n", 530 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 531 | " predictions.append(predict_userCF_baseline(user, item))\n", 532 | " targets.append(actual)\n", 533 | " \n", 534 | "print('测试结果的rmse为 %.4f' % rmse(np.array(predictions), np.array(targets)))\n", 535 | "print()" 536 | ] 537 | }, 538 | { 539 | "cell_type": "code", 540 | "execution_count": 21, 541 | "metadata": { 542 | "collapsed": false 543 | }, 544 | "outputs": [ 545 | { 546 | "name": "stdout", 547 | "output_type": "stream", 548 | "text": [ 549 | "------ 经过修正后的协同过滤 ------\n", 550 | "载入测试集...\n", 551 | "测试集大小为 20000\n", 552 | "采用结合baseline的item-item协同过滤算法进行预测...\n", 553 | "测试结果的rmse为 0.9344\n", 554 | "\n" 555 | ] 556 | } 557 | ], 558 | "source": [ 559 | "print('------ 经过修正后的协同过滤 ------')\n", 560 | "def predict_biasCF(user, item, k=100):\n", 561 | " '''结合baseline的item-item CF算法,预测rating'''\n", 562 | " nzero = ratings[user].nonzero()[0]\n", 563 | " baseline = item_mean + user_mean[user] - all_mean\n", 564 | " prediction = (ratings[user, nzero] - baseline[nzero]).dot(item_similarity[item, nzero])\\\n", 565 | " / sum(item_similarity[item, nzero]) + baseline[item]\n", 566 | " if prediction > 5:\n", 567 | " prediction = 5\n", 568 | " if prediction < 1:\n", 569 | " prediciton = 1\n", 570 | " return prediction\n", 571 | "\n", 572 | "print('载入测试集...')\n", 573 | "test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 574 | "test_df.head()\n", 575 | "predictions = []\n", 576 | "targets = []\n", 577 | "print('测试集大小为 %d' % len(test_df))\n", 578 | "print('采用结合baseline的item-item协同过滤算法进行预测...')\n", 579 | "for row in test_df.itertuples():\n", 580 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 581 | " predictions.append(predict_biasCF(user, item))\n", 582 | " targets.append(actual)\n", 583 | "\n", 584 | "print('测试结果的rmse为 %.4f' % rmse(np.array(predictions), np.array(targets)))\n", 585 | "print()" 586 | ] 587 | }, 588 | { 589 | "cell_type": "code", 590 | "execution_count": 22, 591 | "metadata": { 592 | "collapsed": false 593 | }, 594 | "outputs": [ 595 | { 596 | "name": "stdout", 597 | "output_type": "stream", 598 | "text": [ 599 | "------ Top-k协同过滤(item-item, baseline, 矫正)------\n", 600 | "载入测试集...\n", 601 | "测试集大小为 20000\n", 602 | "采用top K协同过滤算法进行预测...\n", 603 | "选取的K值为20.\n", 604 | "测试结果的rmse为 0.9181\n", 605 | "\n" 606 | ] 607 | } 608 | ], 609 | "source": [ 610 | "print('------ Top-k协同过滤(item-item, baseline, 矫正)------')\n", 611 | "def predict_topkCF(user, item, k=10):\n", 612 | " '''top-k CF算法,以item-item协同过滤为基础,结合baseline,预测rating'''\n", 613 | " nzero = ratings[user].nonzero()[0]\n", 614 | " baseline = item_mean + user_mean[user] - all_mean\n", 615 | " choice = nzero[item_similarity[item, nzero].argsort()[::-1][:k]]\n", 616 | " prediction = (ratings[user, choice] - baseline[choice]).dot(item_similarity[item, choice])\\\n", 617 | " / sum(item_similarity[item, choice]) + baseline[item]\n", 618 | " if prediction > 5: prediction = 5\n", 619 | " if prediction < 1: prediction = 1\n", 620 | " return prediction \n", 621 | "\n", 622 | "print('载入测试集...')\n", 623 | "test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 624 | "test_df.head()\n", 625 | "predictions = []\n", 626 | "targets = []\n", 627 | "print('测试集大小为 %d' % len(test_df))\n", 628 | "print('采用top K协同过滤算法进行预测...')\n", 629 | "k = 20\n", 630 | "print('选取的K值为%d.' % k)\n", 631 | "for row in test_df.itertuples():\n", 632 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 633 | " predictions.append(predict_topkCF(user, item, k))\n", 634 | " targets.append(actual)\n", 635 | "\n", 636 | "print('测试结果的rmse为 %.4f' % rmse(np.array(predictions), np.array(targets)))\n", 637 | "print()" 638 | ] 639 | }, 640 | { 641 | "cell_type": "code", 642 | "execution_count": 23, 643 | "metadata": { 644 | "collapsed": false 645 | }, 646 | "outputs": [ 647 | { 648 | "name": "stdout", 649 | "output_type": "stream", 650 | "text": [ 651 | "经检验,在100k数据上,K=20为佳.\n" 652 | ] 653 | } 654 | ], 655 | "source": [ 656 | "print('经检验,在100k数据上,K=20为佳.')" 657 | ] 658 | }, 659 | { 660 | "cell_type": "code", 661 | "execution_count": 24, 662 | "metadata": { 663 | "collapsed": false 664 | }, 665 | "outputs": [ 666 | { 667 | "name": "stdout", 668 | "output_type": "stream", 669 | "text": [ 670 | "------ baseline + item-item + 矫正 + TopK + 归一化矩阵 ------\n" 671 | ] 672 | } 673 | ], 674 | "source": [ 675 | "print('------ baseline + item-item + 矫正 + TopK + 归一化矩阵 ------')" 676 | ] 677 | }, 678 | { 679 | "cell_type": "code", 680 | "execution_count": 25, 681 | "metadata": { 682 | "collapsed": false 683 | }, 684 | "outputs": [ 685 | { 686 | "name": "stdout", 687 | "output_type": "stream", 688 | "text": [ 689 | "计算归一化的相似度矩阵...\n", 690 | "计算完成.\n", 691 | "相似度矩阵样例: (item-item)\n", 692 | "[[ 1. 0.053 0.055 0.028 0.125 0.046 0.051 0.07 0.039 0.022]\n", 693 | " [ 0.053 1. 0.021 0.122 0.021 -0.007 0.052 0.109 -0.061 0.051]\n", 694 | " [ 0.055 0.021 1. -0.035 0.013 0.048 -0.011 -0.003 -0.048 0.044]\n", 695 | " [ 0.028 0.122 -0.035 1. -0.008 -0.028 0.053 0.087 0.028 0.036]\n", 696 | " [ 0.125 0.021 0.013 -0.008 1. -0.011 0.104 0.025 0.043 -0.016]\n", 697 | " [ 0.046 -0.007 0.048 -0.028 -0.011 1. 0.026 -0.071 0.035 0.013]\n", 698 | " [ 0.051 0.052 -0.011 0.053 0.104 0.026 1. 0.051 0.143 0.025]\n", 699 | " [ 0.07 0.109 -0.003 0.087 0.025 -0.071 0.051 1. 0.019 0.043]\n", 700 | " [ 0.039 -0.061 -0.048 0.028 0.043 0.035 0.143 0.019 1. 0.005]\n", 701 | " [ 0.022 0.051 0.044 0.036 -0.016 0.013 0.025 0.043 0.005 1. ]]\n" 702 | ] 703 | } 704 | ], 705 | "source": [ 706 | "def cal_similarity_norm(ratings, kind, epsilon=1e-9):\n", 707 | " '''采用归一化的指标:Pearson correlation coefficient'''\n", 708 | " if kind == 'user':\n", 709 | " # 对同一个user的打分归一化\n", 710 | " rating_user_diff = ratings.copy()\n", 711 | " for i in range(ratings.shape[0]):\n", 712 | " nzero = ratings[i].nonzero()\n", 713 | " rating_user_diff[i][nzero] = ratings[i][nzero] - user_mean[i]\n", 714 | " sim = rating_user_diff.dot(rating_user_diff.T) + epsilon\n", 715 | " elif kind == 'item':\n", 716 | " # 对同一个item的打分归一化\n", 717 | " rating_item_diff = ratings.copy()\n", 718 | " for j in range(ratings.shape[1]):\n", 719 | " nzero = ratings[:,j].nonzero()\n", 720 | " rating_item_diff[:,j][nzero] = ratings[:,j][nzero] - item_mean[j]\n", 721 | " sim = rating_item_diff.T.dot(rating_item_diff) + epsilon\n", 722 | " norms = np.array([np.sqrt(np.diagonal(sim))])\n", 723 | " return (sim / norms / norms.T)\n", 724 | "\n", 725 | "print('计算归一化的相似度矩阵...')\n", 726 | "user_similarity_norm = cal_similarity_norm(ratings, kind='user')\n", 727 | "item_similarity_norm = cal_similarity_norm(ratings, kind='item')\n", 728 | "print('计算完成.')\n", 729 | "print('相似度矩阵样例: (item-item)')\n", 730 | "print(np.round_(item_similarity_norm[:10,:10], 3))" 731 | ] 732 | }, 733 | { 734 | "cell_type": "code", 735 | "execution_count": 39, 736 | "metadata": { 737 | "collapsed": false 738 | }, 739 | "outputs": [ 740 | { 741 | "name": "stdout", 742 | "output_type": "stream", 743 | "text": [ 744 | "载入测试集...\n", 745 | "测试集大小为 20000\n", 746 | "采用归一化矩阵方法,结合其它trick进行预测...\n", 747 | "选取的K值为13.\n", 748 | "测试结果的rmse为 0.9200\n", 749 | "\n" 750 | ] 751 | } 752 | ], 753 | "source": [ 754 | "def predict_norm_CF(user, item, k=20):\n", 755 | " '''baseline + item-item + '''\n", 756 | " nzero = ratings[user].nonzero()[0]\n", 757 | " baseline = item_mean + user_mean[user] - all_mean\n", 758 | " choice = nzero[item_similarity_norm[item, nzero].argsort()[::-1][:k]]\n", 759 | " prediction = (ratings[user, choice] - baseline[choice]).dot(item_similarity_norm[item, choice])\\\n", 760 | " / sum(item_similarity_norm[item, choice]) + baseline[item]\n", 761 | " if prediction > 5: prediction = 5\n", 762 | " if prediction < 1: prediction = 1\n", 763 | " return prediction \n", 764 | "\n", 765 | "print('载入测试集...')\n", 766 | "test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 767 | "test_df.head()\n", 768 | "predictions = []\n", 769 | "targets = []\n", 770 | "print('测试集大小为 %d' % len(test_df))\n", 771 | "print('采用归一化矩阵方法,结合其它trick进行预测...')\n", 772 | "k = 13\n", 773 | "print('选取的K值为%d.' % k)\n", 774 | "for row in test_df.itertuples():\n", 775 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 776 | " predictions.append(predict_norm_CF(user, item, k))\n", 777 | " targets.append(actual)\n", 778 | "\n", 779 | "print('测试结果的rmse为 %.4f' % rmse(np.array(predictions), np.array(targets)))\n", 780 | "print()" 781 | ] 782 | }, 783 | { 784 | "cell_type": "code", 785 | "execution_count": null, 786 | "metadata": { 787 | "collapsed": true 788 | }, 789 | "outputs": [], 790 | "source": [ 791 | "print('------ 测试Top K ------')" 792 | ] 793 | } 794 | ], 795 | "metadata": { 796 | "kernelspec": { 797 | "display_name": "Python 3", 798 | "language": "python", 799 | "name": "python3" 800 | }, 801 | "language_info": { 802 | "codemirror_mode": { 803 | "name": "ipython", 804 | "version": 3 805 | }, 806 | "file_extension": ".py", 807 | "mimetype": "text/x-python", 808 | "name": "python", 809 | "nbconvert_exporter": "python", 810 | "pygments_lexer": "ipython3", 811 | "version": "3.5.1" 812 | } 813 | }, 814 | "nbformat": 4, 815 | "nbformat_minor": 0 816 | } 817 | -------------------------------------------------------------------------------- /Cross Validation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "初始化变量...\n", 15 | "------ 第1/5组样本 ------\n", 16 | "载入训练集dataset/ml-100k/u1.base\n", 17 | "训练集规模为 80000\n", 18 | "训练集矩阵密度为 5.04%\n", 19 | "计算训练集各项统计数据...\n", 20 | "计算相似度矩阵...\n", 21 | "计算完成\n", 22 | "载入测试集dataset/ml-100k/u1.test\n", 23 | "测试集规模为 20000\n", 24 | "测试中...\n", 25 | "测试完成\n", 26 | "------ 第2/5组样本 ------\n", 27 | "载入训练集dataset/ml-100k/u2.base\n", 28 | "训练集规模为 80000\n", 29 | "训练集矩阵密度为 5.04%\n", 30 | "计算训练集各项统计数据...\n", 31 | "计算相似度矩阵...\n", 32 | "计算完成\n", 33 | "载入测试集dataset/ml-100k/u2.test\n", 34 | "测试集规模为 20000\n", 35 | "测试中...\n", 36 | "测试完成\n", 37 | "------ 第3/5组样本 ------\n", 38 | "载入训练集dataset/ml-100k/u3.base\n", 39 | "训练集规模为 80000\n", 40 | "训练集矩阵密度为 5.04%\n", 41 | "计算训练集各项统计数据...\n", 42 | "计算相似度矩阵...\n", 43 | "计算完成\n", 44 | "载入测试集dataset/ml-100k/u3.test\n", 45 | "测试集规模为 20000\n", 46 | "测试中...\n", 47 | "测试完成\n", 48 | "------ 第4/5组样本 ------\n", 49 | "载入训练集dataset/ml-100k/u4.base\n", 50 | "训练集规模为 80000\n", 51 | "训练集矩阵密度为 5.04%\n", 52 | "计算训练集各项统计数据...\n", 53 | "计算相似度矩阵...\n", 54 | "计算完成\n", 55 | "载入测试集dataset/ml-100k/u4.test\n", 56 | "测试集规模为 20000\n", 57 | "测试中...\n", 58 | "测试完成\n", 59 | "------ 第5/5组样本 ------\n", 60 | "载入训练集dataset/ml-100k/u5.base\n", 61 | "训练集规模为 80000\n", 62 | "训练集矩阵密度为 5.04%\n", 63 | "计算训练集各项统计数据...\n", 64 | "计算相似度矩阵...\n", 65 | "计算完成\n", 66 | "载入测试集dataset/ml-100k/u5.test\n", 67 | "测试集规模为 20000\n", 68 | "测试中...\n", 69 | "测试完成\n", 70 | "------ 测试结果 ------\n", 71 | "各方法在交叉验证下的RMSE值:\n", 72 | "baseline: 0.9694\n", 73 | "itemCF: 1.0149\n", 74 | "userCF: 1.0174\n", 75 | "itemCF_baseline: 0.9362\n", 76 | "userCF_baseline: 0.9548\n", 77 | "itemCF_bias: 0.9360\n", 78 | "topkCF(item, k=20): 0.9213\n", 79 | "topkCF(user, k=30): 0.9445\n", 80 | "normCF(item, k=20): 0.9253\n", 81 | "normCF(user, k=30): 0.9550\n", 82 | "blend (alpha=0.7): 0.9154\n", 83 | "交叉验证运行完成\n" 84 | ] 85 | } 86 | ], 87 | "source": [ 88 | "import pandas as pd\n", 89 | "import numpy as np\n", 90 | "import var\n", 91 | "import predict as pre\n", 92 | "import utils\n", 93 | "\n", 94 | "print('初始化变量...')\n", 95 | "names = ['user_id', 'item_id', 'rating', 'timestamp']\n", 96 | "direct = 'dataset/ml-100k/'\n", 97 | "trainingset_files = (direct + name for name in ('u1.base', 'u2.base', 'u3.base', 'u4.base', 'u5.base'))\n", 98 | "testset_files = (direct + name for name in ('u1.test', 'u2.test', 'u3.test', 'u4.test', 'u5.test'))\n", 99 | "\n", 100 | "if __name__ == '__main__':\n", 101 | "\n", 102 | " rmse_baseline = []\n", 103 | " rmse_itemCF = []\n", 104 | " rmse_userCF = []\n", 105 | " rmse_itemCF_baseline = []\n", 106 | " rmse_userCF_baseline = []\n", 107 | " rmse_itemCF_bias = []\n", 108 | " rmse_topkCF_item = []\n", 109 | " rmse_topkCF_user = []\n", 110 | " rmse_normCF_item = []\n", 111 | " rmse_normCF_user = []\n", 112 | " rmse_blend = []\n", 113 | " i = 0\n", 114 | " nums = 5\n", 115 | " for trainingset_file, testset_file in zip(trainingset_files, testset_files):\n", 116 | " i += 1\n", 117 | " print('------ 第%d/%d组样本 ------' % (i, nums))\n", 118 | " df = pd.read_csv(trainingset_file, sep='\\t', names=names)\n", 119 | " \n", 120 | " var.ratings = np.zeros((var.n_users, var.n_items))\n", 121 | " print('载入训练集' + trainingset_file)\n", 122 | " for row in df.itertuples():\n", 123 | " var.ratings[row[1]-1, row[2]-1] = row[3]\n", 124 | " \n", 125 | " print('训练集规模为 %d' % len(df))\n", 126 | "\n", 127 | " sparsity = utils.cal_sparsity()\n", 128 | " print('训练集矩阵密度为 {:4.2f}%'.format(sparsity))\n", 129 | " \n", 130 | " print('计算训练集各项统计数据...')\n", 131 | " utils.cal_mean()\n", 132 | "\n", 133 | " print('计算相似度矩阵...')\n", 134 | " var.user_similarity = utils.cal_similarity(kind='user')\n", 135 | " var.item_similarity = utils.cal_similarity(kind='item')\n", 136 | " var.user_similarity_norm = utils.cal_similarity_norm(kind='user')\n", 137 | " var.item_similarity_norm = utils.cal_similarity_norm(kind='item')\n", 138 | " print('计算完成')\n", 139 | " \n", 140 | " print('载入测试集' + testset_file)\n", 141 | " test_df = pd.read_csv(testset_file, sep='\\t', names=names)\n", 142 | " predictions_baseline = []\n", 143 | " predictions_itemCF = []\n", 144 | " predictions_userCF = []\n", 145 | " predictions_itemCF_baseline = []\n", 146 | " predictions_userCF_baseline = []\n", 147 | " predictions_itemCF_bias = []\n", 148 | " predictions_topkCF_item = []\n", 149 | " predictions_topkCF_user = []\n", 150 | " predictions_normCF_item = []\n", 151 | " predictions_normCF_user = []\n", 152 | " predictions_blend = []\n", 153 | " targets = []\n", 154 | " print('测试集规模为 %d' % len(test_df))\n", 155 | " print('测试中...')\n", 156 | " for row in test_df.itertuples():\n", 157 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 158 | " predictions_baseline.append(pre.predict_baseline(user, item))\n", 159 | " predictions_itemCF.append(pre.predict_itemCF(user, item))\n", 160 | " predictions_userCF.append(pre.predict_userCF(user, item))\n", 161 | " predictions_itemCF_baseline.append(pre.predict_itemCF_baseline(user, item))\n", 162 | " predictions_userCF_baseline.append(pre.predict_userCF_baseline(user, item))\n", 163 | " predictions_itemCF_bias.append(pre.predict_itemCF_bias(user, item))\n", 164 | " predictions_topkCF_item.append(pre.predict_topkCF_item(user, item, 20))\n", 165 | " predictions_topkCF_user.append(pre.predict_topkCF_user(user, item, 30))\n", 166 | " predictions_normCF_item.append(pre.predict_normCF_item(user, item, 20))\n", 167 | " predictions_normCF_user.append(pre.predict_normCF_user(user, item, 30))\n", 168 | " predictions_blend.append(pre.predict_blend(user, item, 20, 30, 0.7))\n", 169 | " targets.append(actual)\n", 170 | " \n", 171 | " rmse_baseline.append(utils.rmse(np.array(predictions_baseline), np.array(targets)))\n", 172 | " rmse_itemCF.append(utils.rmse(np.array(predictions_itemCF), np.array(targets)))\n", 173 | " rmse_userCF.append(utils.rmse(np.array(predictions_userCF), np.array(targets)))\n", 174 | " rmse_itemCF_baseline.append(utils.rmse(np.array(predictions_itemCF_baseline), np.array(targets)))\n", 175 | " rmse_userCF_baseline.append(utils.rmse(np.array(predictions_userCF_baseline), np.array(targets)))\n", 176 | " rmse_itemCF_bias.append(utils.rmse(np.array(predictions_itemCF_bias), np.array(targets)))\n", 177 | " rmse_topkCF_item.append(utils.rmse(np.array(predictions_topkCF_item), np.array(targets)))\n", 178 | " rmse_topkCF_user.append(utils.rmse(np.array(predictions_topkCF_user), np.array(targets)))\n", 179 | " rmse_normCF_item.append(utils.rmse(np.array(predictions_normCF_item), np.array(targets)))\n", 180 | " rmse_normCF_user.append(utils.rmse(np.array(predictions_normCF_user), np.array(targets)))\n", 181 | " rmse_blend.append(utils.rmse(np.array(predictions_blend), np.array(targets)))\n", 182 | " print('测试完成')\n", 183 | " print('------ 测试结果 ------')\n", 184 | " print('各方法在交叉验证下的RMSE值:')\n", 185 | " print('baseline: %.4f' % np.mean(rmse_baseline))\n", 186 | " print('itemCF: %.4f' % np.mean(rmse_itemCF))\n", 187 | " print('userCF: %.4f' % np.mean(rmse_userCF))\n", 188 | " print('itemCF_baseline: %.4f' % np.mean(rmse_itemCF_baseline))\n", 189 | " print('userCF_baseline: %.4f' % np.mean(rmse_userCF_baseline)) \n", 190 | " print('itemCF_bias: %.4f' % np.mean(rmse_itemCF_bias))\n", 191 | " print('topkCF(item, k=20): %.4f' % np.mean(rmse_topkCF_item))\n", 192 | " print('topkCF(user, k=30): %.4f' % np.mean(rmse_topkCF_user))\n", 193 | " print('normCF(item, k=20): %.4f' % np.mean(rmse_normCF_item))\n", 194 | " print('normCF(user, k=30): %.4f' % np.mean(rmse_normCF_user))\n", 195 | " print('blend (alpha=0.7): %.4f' % np.mean(rmse_blend))\n", 196 | " print('交叉验证运行完成')\n", 197 | " " 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": null, 203 | "metadata": { 204 | "collapsed": false 205 | }, 206 | "outputs": [], 207 | "source": [] 208 | } 209 | ], 210 | "metadata": { 211 | "kernelspec": { 212 | "display_name": "Python 3", 213 | "language": "python", 214 | "name": "python3" 215 | }, 216 | "language_info": { 217 | "codemirror_mode": { 218 | "name": "ipython", 219 | "version": 3 220 | }, 221 | "file_extension": ".py", 222 | "mimetype": "text/x-python", 223 | "name": "python", 224 | "nbconvert_exporter": "python", 225 | "pygments_lexer": "ipython3", 226 | "version": "3.5.1" 227 | } 228 | }, 229 | "nbformat": 4, 230 | "nbformat_minor": 0 231 | } 232 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Irmo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MovieLens.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 62, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import numpy as np\n", 12 | "\n", 13 | "num_user = 943\n", 14 | "num_movie = 1682\n", 15 | "# num_ratings = 100000\n", 16 | "all_mean = 0\n", 17 | "# user_mean = np.zeros(num_user,dtype=int)\n", 18 | "# movie_mean = np.zeros(num_movie,dtype=int)\n", 19 | "\n", 20 | "def load_data(filename):\n", 21 | " '''Load training data from dataset'''\n", 22 | " f = open('dataset/ml-100k/' + filename, 'rt')\n", 23 | " t = 0\n", 24 | " ratings = np.zeros((num_user, num_movie), dtype=int).reshape(num_user, num_movie)\n", 25 | " for line in f.readlines():\n", 26 | " user, movie, rating = [int(x) for x in line.split()[:3]]\n", 27 | " if t % 10000 == 0:\n", 28 | " print('Loading %6d case: ' % t, user, movie, rating)\n", 29 | " ratings[user - 1, movie - 1] = rating\n", 30 | " t += 1\n", 31 | " print('Loading complete.')\n", 32 | " print(np.size(ratings))\n", 33 | " return ratings\n", 34 | " " 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 63, 40 | "metadata": { 41 | "collapsed": false 42 | }, 43 | "outputs": [], 44 | "source": [ 45 | "def cal_mean():\n", 46 | " '''Calculate mean value'''\n", 47 | " global all_mean, user_mean, movie_mean\n", 48 | " all_mean = np.mean(ratings[ratings!=0])\n", 49 | " user_mean = sum(ratings.T) / sum((ratings!=0).T)\n", 50 | " movie_mean = sum(ratings) / sum((ratings!=0))\n", 51 | " print(np.isnan(user_mean).any(), np.isnan(movie_mean).any())\n", 52 | " # Replace NaN to all_mean.\n", 53 | " user_mean = np.where(np.isnan(user_mean), all_mean, user_mean)\n", 54 | " movie_mean = np.where(np.isnan(movie_mean), all_mean, movie_mean)\n", 55 | " print(np.isnan(user_mean).any(), np.isnan(movie_mean).any())\n", 56 | " print('Mean rating of all movies is ', round(all_mean,2))" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 64, 62 | "metadata": { 63 | "collapsed": false 64 | }, 65 | "outputs": [], 66 | "source": [ 67 | "def predict_naive(user_id, movie_id):\n", 68 | " '''Naive predict method'''\n", 69 | "# if np.isnan(movie_mean[movie_id]):\n", 70 | "# prediction = round(user_mean[user_id]);\n", 71 | "# else:\n", 72 | " prediction = round(movie_mean[movie_id] + user_mean[user_id] - all_mean, 2)\n", 73 | " return prediction" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 65, 79 | "metadata": { 80 | "collapsed": false 81 | }, 82 | "outputs": [], 83 | "source": [ 84 | "def rmse(predictions, targets):\n", 85 | " return np.sum(np.square(predictions-targets))/np.size(predictions)" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 66, 91 | "metadata": { 92 | "collapsed": false 93 | }, 94 | "outputs": [], 95 | "source": [ 96 | "def test(filename):\n", 97 | " global predictions, targets\n", 98 | " f = open('dataset/ml-100k/' + filename, 'rt')\n", 99 | " predictions = []\n", 100 | " targets = []\n", 101 | " for line in f.readlines():\n", 102 | " user, movie, real_rating = [int(x) for x in line.split()[:3]]\n", 103 | " guess_rating = predict_naive(user-1, movie-1)\n", 104 | " predictions.append(guess_rating)\n", 105 | " targets.append(real_rating)\n", 106 | " predictions = np.array(predictions,dtype=np.double)\n", 107 | " targets = np.array(targets,dtype=np.double)\n", 108 | " loss = rmse(predictions, targets)\n", 109 | " print('Loss = ', round(loss,2))" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 67, 115 | "metadata": { 116 | "collapsed": false 117 | }, 118 | "outputs": [ 119 | { 120 | "name": "stdout", 121 | "output_type": "stream", 122 | "text": [ 123 | "Loading 0 case: 1 1 5\n", 124 | "Loading 10000 case: 189 207 5\n", 125 | "Loading 20000 case: 334 689 3\n", 126 | "Loading 30000 case: 445 895 2\n", 127 | "Loading 40000 case: 535 42 3\n", 128 | "Loading 50000 case: 642 66 5\n", 129 | "Loading 60000 case: 744 657 5\n", 130 | "Loading 70000 case: 849 121 5\n", 131 | "Loading complete.\n", 132 | "1586126\n", 133 | "False True\n", 134 | "False False\n", 135 | "Mean rating of all movies is 3.53\n" 136 | ] 137 | }, 138 | { 139 | "name": "stderr", 140 | "output_type": "stream", 141 | "text": [ 142 | "/Users/irmo/anaconda/lib/python3.5/site-packages/ipykernel/__main__.py:6: RuntimeWarning: invalid value encountered in true_divide\n" 143 | ] 144 | }, 145 | { 146 | "name": "stdout", 147 | "output_type": "stream", 148 | "text": [ 149 | "Loss = 0.96\n" 150 | ] 151 | } 152 | ], 153 | "source": [ 154 | "ratings = load_data('u1.base')\n", 155 | "# all_ratings = load_data('u.data')\n", 156 | "cal_mean()\n", 157 | "test('u1.test')" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 68, 163 | "metadata": { 164 | "collapsed": false 165 | }, 166 | "outputs": [ 167 | { 168 | "data": { 169 | "text/plain": [ 170 | "array([ 2.27272727, 2. , 2.28571429, 3.33333333, 3.7 ,\n", 171 | " 2.88235294, 4.02857143, 3.21428571, 3.45283019, 3.28125 ,\n", 172 | " 3.27160494, 3.48780488, 2.24137931, 2.97368421, 3.14285714,\n", 173 | " 3.44954128, 3.21212121, 3.14814815, 3.91666667, 3.85714286,\n", 174 | " 3.75862069, 3.25925926, 2.57142857, 3.53846154, 2.81081081,\n", 175 | " 2.5 , 2.66666667, 3.04587156, 2.16666667, 2.23529412,\n", 176 | " 2.28571429, 3.125 , 2.88461538, 2.76190476, 3.18518519,\n", 177 | " 2.85 , 1.95 , 2.6875 , 4.05797101, 2.73684211,\n", 178 | " 3.22222222, 3. , 3.2 , 3.16129032, 3.28571429,\n", 179 | " 3.16216216, 2.81132075, 3.03389831, 2.63157895, 3.66666667,\n", 180 | " 3.27777778, 2.33333333, 3.47368421, 2.58823529, 3.2 ,\n", 181 | " 3.125 , 2.33333333, 3.5 , 2.82142857, 3.09677419,\n", 182 | " 2.83333333, 3.63636364, 3.72972973, 4.25 , 3.4 ,\n", 183 | " 3.07142857, 3.47222222, 3.4 , 3.2 , 3.72 ,\n", 184 | " 2.81818182, 2.33333333, 3.84615385, 2.95384615, 3.33333333,\n", 185 | " 2.27272727, 3.14285714, 2.82352941, 2.15 , 4. ,\n", 186 | " 2.8 , 2.6 , 2.25 , 3.82352941, 3.44444444,\n", 187 | " 3.05882353, 1.77777778, 2.3 , 2.16666667, 2.51724138,\n", 188 | " 2.71428571, 2.33333333, 3.2 , 2.5 , 2.82352941,\n", 189 | " 2.66666667, 3.4 , 3.72413793, 3.44444444, 2.66666667,\n", 190 | " 3.72413793, 2.66666667, 3.5625 , 1. , 2.64285714,\n", 191 | " 3. , 3.23076923, 2. , 3.6 , 2.82857143,\n", 192 | " 3.42857143, 2.71428571, 3.28571429, 3.11111111, 3.375 ,\n", 193 | " 3.875 , 2.92857143, 3.42105263, 3.7173913 , 3.14285714,\n", 194 | " 3.7 , 5. , 4. , 3.8 , 4.33333333,\n", 195 | " 3.23076923, 2.66666667, 2.77777778, 3.5 , 4. ,\n", 196 | " 4.125 , 2.53846154, 2.8 , 3.32258065, 3.02564103,\n", 197 | " 3.34782609, 3.76190476, 3.4 , 2.58333333, 2.26666667,\n", 198 | " 2.8 , 4.17647059, 3.83333333, 3.66666667, 2.66666667,\n", 199 | " 2. , 3.46153846, 3.125 , 3.85714286, 3.5 ,\n", 200 | " 2.5 , 3.37037037, 3.28571429, 3.2 , 3.33333333,\n", 201 | " 3.52835 , 2.9 , 4. , 3.54545455, 3.61111111,\n", 202 | " 2.875 , 1.33333333, 2.71428571, 2. , 2. ,\n", 203 | " 3.4 , 3.88888889, 3.41176471, 4. , 3. ,\n", 204 | " 3.14285714, 3.8 , 2.57142857, 3.57142857, 3. ,\n", 205 | " 3.35 , 2. , 2.5 , 2. , 2.8 ,\n", 206 | " 2.375 , 2.2 , 2.13333333, 2.5 , 2.27777778,\n", 207 | " 3. , 3.3 , 2.55555556, 5. , 3.125 ,\n", 208 | " 4.33333333, 3.4 , 3.77777778, 4. , 3.3 ,\n", 209 | " 3. , 3.43243243, 3.25 , 2.78947368, 3.16666667,\n", 210 | " 5. , 4. , 3.88888889, 3.61111111, 3. ,\n", 211 | " 2.7 , 2.45454545, 2.8125 , 2.45454545, 2.5 ,\n", 212 | " 3.66666667, 3. , 2. , 2. , 1.96 ,\n", 213 | " 3. , 2.96666667, 3.4 , 2.93333333, 3.42857143,\n", 214 | " 3.47222222, 2.63157895, 3. , 2.77777778, 2.66666667,\n", 215 | " 3.36 , 2.8 , 2.36842105, 1.8 , 1.90909091,\n", 216 | " 2.36842105, 2.09090909, 3.5 , 2.375 , 4. ,\n", 217 | " 3.52835 , 3.6 , 3.28571429, 2.91666667, 3.75 ,\n", 218 | " 3. , 2. , 3.71428571, 2.8125 , 3.07142857,\n", 219 | " 1.2 , 2. , 3.15384615, 2.9 , 1. ,\n", 220 | " 4.125 , 2.875 , 2.33333333, 1.9 , 2.25 ,\n", 221 | " 2. , 2. , 2.31578947, 2. , 3. ,\n", 222 | " 3. , 3.83333333, 2.9 , 3.16666667, 3.0625 ,\n", 223 | " 3.4 , 3.03703704, 3.44444444, 3.88888889, 2.33333333,\n", 224 | " 2.5 , 2.33333333, 2.84615385, 1.875 , 3. ,\n", 225 | " 2.28571429, 3.375 , 3.64285714, 2.875 , 2.6875 ,\n", 226 | " 3. , 3.5 , 2.78571429, 2.90909091, 3.15384615,\n", 227 | " 3.5 , 1.33333333, 3.33333333, 2. , 1.5 ,\n", 228 | " 2.15384615, 2. , 5. , 3.4 , 3.2 ,\n", 229 | " 3.27777778, 3. , 3.6 , 3. , 2.66666667,\n", 230 | " 4. , 3. , 2.3125 , 2.5 , 2.66666667,\n", 231 | " 1. , 1. , 1. , 3.52835 , 3.52835 ,\n", 232 | " 2.5 , 3.25 , 3.27272727, 2. , 3.1 ,\n", 233 | " 2. , 2.33333333, 2. , 1.75 , 3.52835 ,\n", 234 | " 2.5 , 2.5 , 2.66666667, 2.75 , 1. ,\n", 235 | " 3.33333333, 3. , 3. , 1. , 3. ,\n", 236 | " 3. , 3. , 3.25 , 1. , 2.63636364,\n", 237 | " 1.85714286, 2.33333333, 2.66666667, 1. , 1. ,\n", 238 | " 1. , 4. , 3.52835 , 3.75 , 2. ,\n", 239 | " 2. , 3.33333333, 3.52835 , 1. , 1. ,\n", 240 | " 2.5 , 1. , 2.66666667, 1. , 2.66666667,\n", 241 | " 2.66666667, 2. , 3. , 1. , 2. ,\n", 242 | " 3.5 , 3. , 1. , 3.52835 , 1.5 ,\n", 243 | " 1. , 4.66666667, 3.75 , 3.33333333, 3.5 ,\n", 244 | " 2. , 2.5 , 3.52835 , 1. , 3.88888889,\n", 245 | " 1.75 , 4. , 2.33333333, 3.16666667, 2.33333333,\n", 246 | " 3.25 , 2. , 2.2 , 2. , 3.25 ,\n", 247 | " 3. , 2.5 , 3.8 , 3. , 3. ,\n", 248 | " 2. , 2.25 , 2.33333333, 2.66666667, 2.6 ,\n", 249 | " 4.25 , 3. , 4. , 2.42857143, 3.125 ,\n", 250 | " 2.84615385, 2.75 , 2.33333333, 3.2 , 3. ,\n", 251 | " 3.25 , 2.23529412, 1. , 3.4 , 2.75 ,\n", 252 | " 2.45 , 1.66666667, 2.42857143, 2. , 2.44444444,\n", 253 | " 2.25 , 2. , 3.5 , 2.21428571, 1.33333333,\n", 254 | " 3.33333333, 2.25 , 2.25 , 3. , 2.5 ,\n", 255 | " 3.25 , 3. , 3.72727273, 2.75 , 3. ,\n", 256 | " 3.4 , 1. , 2. , 2.66666667, 2.83333333,\n", 257 | " 2. , 2. , 1.66666667, 3. , 3. ,\n", 258 | " 2.4 , 2.25 , 3.66666667, 2.53333333, 2.75 ,\n", 259 | " 2.92307692, 3. , 3.66666667, 4.71428571, 3. ,\n", 260 | " 3.81818182, 4. , 2. , 3. , 3. ,\n", 261 | " 3.6 , 3.52835 , 3.52835 , 3.33333333, 3. ,\n", 262 | " 2. , 3.6 , 2.5 , 1.5 , 2.33333333,\n", 263 | " 4.2 , 5. , 3.33333333, 3.4 , 2.5 ,\n", 264 | " 2.2 , 3.5 , 3.375 , 2.83333333, 2.875 ,\n", 265 | " 2. , 3. , 2.6 , 2.66666667, 2.4 ,\n", 266 | " 3. , 4. , 3.41666667, 2.5 , 3.33333333,\n", 267 | " 1. , 2.33333333, 3. , 2. , 3. ,\n", 268 | " 3. , 3.52835 , 3.52835 , 1. , 3. ,\n", 269 | " 2.8 , 4. , 3.52835 , 3.25 , 5. ,\n", 270 | " 3.5 , 1.5 , 3.6 , 3.5 , 3.52835 ,\n", 271 | " 3.66666667, 3. , 3. , 1.6 , 3. ,\n", 272 | " 3.2 , 4. , 2. , 3.14285714, 4. ,\n", 273 | " 4. , 2.66666667, 3. , 4. , 3.52835 ,\n", 274 | " 3. , 2.5 , 3. , 4.16666667, 4. ,\n", 275 | " 4. , 2.16666667, 2.33333333, 2.75 , 2. ,\n", 276 | " 3.66666667, 2. , 3.52835 , 1.8 , 3. ,\n", 277 | " 3.52835 , 4. , 3. , 1.75 , 2.8 ,\n", 278 | " 3.5 , 3. , 3.52835 , 2.66666667, 2.5 ,\n", 279 | " 1. , 3. , 1. , 1.5 , 3. ,\n", 280 | " 2. , 2. , 2.75 , 2. , 3. ,\n", 281 | " 2. , 3.52835 , 3.57142857, 1. , 2.75 ,\n", 282 | " 3.52835 , 3.52835 , 3.52835 , 1. , 3.52835 ,\n", 283 | " 1. , 1. , 1. , 1. , 1. ,\n", 284 | " 1. , 1. , 2.5 , 1. , 1. ,\n", 285 | " 1. , 1. , 3. , 1. , 1. ,\n", 286 | " 1. , 3.52835 , 1. , 1. , 2.5 ,\n", 287 | " 3.52835 , 1. , 2. , 3. , 2. ,\n", 288 | " 3. , 3.6 , 4. , 4.5 , 2. ,\n", 289 | " 2. , 3. , 2.5 , 5. , 3.75 ,\n", 290 | " 1. , 3.33333333, 3. , 4. , 3. ,\n", 291 | " 2. , 3.33333333, 2.25 , 1.66666667, 2. ,\n", 292 | " 3. , 3.75 , 4. , 3. , 3. ,\n", 293 | " 4. , 3. , 1. , 3. , 3.22222222,\n", 294 | " 1. , 2.33333333, 4. , 2. , 4. ,\n", 295 | " 1. , 3. , 3.75 , 4. , 3. ,\n", 296 | " 3.5 , 3. , 3. , 2. , 3. ,\n", 297 | " 4. , 3. , 3. , 4.33333333, 3. ,\n", 298 | " 3. , 4.5 , 3.75 , 1.5 , 4. ,\n", 299 | " 3.5 , 3. , 2. , 3. , 4. ,\n", 300 | " 4. , 1.66666667, 5. , 1. , 2. ,\n", 301 | " 3.5 , 3. , 3. , 1. , 2. ,\n", 302 | " 1. , 2.5 , 2. , 3.25 , 2. ,\n", 303 | " 2. , 3. , 3. , 2. , 3. ,\n", 304 | " 1. , 2. , 3. , 4. , 3. ,\n", 305 | " 2. , 3. , 1. , 3. , 2. ,\n", 306 | " 3. , 3. ])" 307 | ] 308 | }, 309 | "execution_count": 68, 310 | "metadata": {}, 311 | "output_type": "execute_result" 312 | } 313 | ], 314 | "source": [ 315 | "movie_mean[1000:1682]" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": 69, 321 | "metadata": { 322 | "collapsed": false 323 | }, 324 | "outputs": [ 325 | { 326 | "name": "stdout", 327 | "output_type": "stream", 328 | "text": [ 329 | "True\n" 330 | ] 331 | } 332 | ], 333 | "source": [ 334 | "t = np.array([1,np.nan,3])\n", 335 | "print(np.isnan(t).any())" 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": null, 341 | "metadata": { 342 | "collapsed": true 343 | }, 344 | "outputs": [], 345 | "source": [] 346 | } 347 | ], 348 | "metadata": { 349 | "kernelspec": { 350 | "display_name": "Python 3", 351 | "language": "python", 352 | "name": "python3" 353 | }, 354 | "language_info": { 355 | "codemirror_mode": { 356 | "name": "ipython", 357 | "version": 3 358 | }, 359 | "file_extension": ".py", 360 | "mimetype": "text/x-python", 361 | "name": "python", 362 | "nbconvert_exporter": "python", 363 | "pygments_lexer": "ipython3", 364 | "version": "3.5.1" 365 | } 366 | }, 367 | "nbformat": 4, 368 | "nbformat_minor": 0 369 | } 370 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## Implementation of Collaborative Filtering 2 | 3 | [Collaborative Filtering](https://en.wikipedia.org/wiki/Collaborative_filtering) is a technique used by some [recommender systems](https://en.wikipedia.org/wiki/Recommender_system). 4 | 5 | [This repository](https://github.com/irmowan/Collaborative-Filtering) is the Python implementation of Collaborative Filtering. 6 | 7 | ### Usage 8 | 9 | *Run:* 10 | 11 | ``` 12 | > python main.py 13 | ``` 14 | 15 | *Notice:* 16 | 17 | **Python Version**: 3.5.1 18 | 19 | **Required modules**: Numpy, Pandas, Matplotlib 20 | 21 | Need to download the dataset first and put it in the `dataset/` folder. 22 | 23 | Or, you can see the [result](https://github.com/irmowan/Collaborative-Filtering/blob/master/Cross%20Validation.ipynb) without downloading the dataset. 24 | 25 | ### Dataset 26 | 27 | [MovieLens](http://grouplens.org/datasets/movielens/), 100K dataset 28 | 29 | ### Report 30 | 31 | [推荐系统的协同过滤算法实现和浅析](https://github.com/irmowan/MovieLens/blob/master/report/Report.pdf) is the pdf version of report. 32 | 33 | ### File tree 34 | 35 | ``` 36 | Python files: 37 | ├── main.py # Main python file including training and testing. 38 | ├── predict.py # Predict functions. 39 | ├── utils.py # Some useful functions, including calculating. 40 | ├── var.py # Define global variables. 41 | 42 | Jupyter Notebook files: 43 | ├── Cross Validation.ipynb # Main file of Collartive Filtering using Cross Validation. 44 | ├── TopK.ipynb # File to choose K in Top-K algorithm. 45 | ├── alpha.ipynb # File to choose alpha in model blending. 46 | ├── MovieLens.ipynb # Early version file for data cleanning. 47 | ├── CF.ipynb # Early version file about Collarative Filtering. 48 | 49 | Others: 50 | ├── LICENSE # MIT LICENSE 51 | ├── papers # ignored. Papers have been cited in report. 52 | ├── dataset # ignored. You can get it from GroupLens Website. 53 | │   ├── ml-100k # 100K MovieLens dataset 54 | ├── report 55 | │   ├── Report.tex # Raw Tex file. Using XeTeX as the engine. 56 | │   ├── Report.bib # References. 57 | │   ├── Report.pdf # Exported pdf report. 58 | │   ├── Plot # Plot folder. Including Echarts. 59 | │   ├── K-figure.png # Pictures included in the report. 60 | │   ├── alpha-figure.png 61 | │   └── rating-pie.png 62 | ``` 63 | 64 | ### License 65 | 66 | [MIT LICENSE](https://github.com/irmowan/MovieLens/blob/master/LICENSE) 67 | 68 | ### Author 69 | 70 | [Irmo](https://github.com/irmowan) 71 | 72 | 2016.6 73 | -------------------------------------------------------------------------------- /TopK.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": { 7 | "collapsed": false, 8 | "scrolled": false 9 | }, 10 | "outputs": [ 11 | { 12 | "name": "stdout", 13 | "output_type": "stream", 14 | "text": [ 15 | "初始化变量...\n", 16 | "------ 第1/5组样本 ------\n", 17 | "载入训练集dataset/ml-100k/u1.base\n", 18 | "计算训练集各项统计数据...\n", 19 | "计算相似度矩阵...\n", 20 | "计算完成\n", 21 | "在训练集上测试...\n", 22 | "载入测试集dataset/ml-100k/u1.test\n", 23 | "测试集规模为 20000\n", 24 | "在测试集上测试...\n", 25 | "测试完成\n", 26 | "------ 第2/5组样本 ------\n", 27 | "载入训练集dataset/ml-100k/u2.base\n", 28 | "计算训练集各项统计数据...\n", 29 | "计算相似度矩阵...\n", 30 | "计算完成\n", 31 | "在训练集上测试...\n", 32 | "载入测试集dataset/ml-100k/u2.test\n", 33 | "测试集规模为 20000\n", 34 | "在测试集上测试...\n", 35 | "测试完成\n", 36 | "------ 第3/5组样本 ------\n", 37 | "载入训练集dataset/ml-100k/u3.base\n", 38 | "计算训练集各项统计数据...\n", 39 | "计算相似度矩阵...\n", 40 | "计算完成\n", 41 | "在训练集上测试...\n", 42 | "载入测试集dataset/ml-100k/u3.test\n", 43 | "测试集规模为 20000\n", 44 | "在测试集上测试...\n", 45 | "测试完成\n", 46 | "------ 第4/5组样本 ------\n", 47 | "载入训练集dataset/ml-100k/u4.base\n", 48 | "计算训练集各项统计数据...\n", 49 | "计算相似度矩阵...\n", 50 | "计算完成\n", 51 | "在训练集上测试...\n", 52 | "载入测试集dataset/ml-100k/u4.test\n", 53 | "测试集规模为 20000\n", 54 | "在测试集上测试...\n", 55 | "测试完成\n", 56 | "------ 第5/5组样本 ------\n", 57 | "载入训练集dataset/ml-100k/u5.base\n", 58 | "计算训练集各项统计数据...\n", 59 | "计算相似度矩阵...\n", 60 | "计算完成\n", 61 | "在训练集上测试...\n", 62 | "载入测试集dataset/ml-100k/u5.test\n", 63 | "测试集规模为 20000\n", 64 | "在测试集上测试...\n", 65 | "测试完成\n", 66 | "------ 测试结果 ------\n", 67 | "item-item协同过滤算法中, 各K值在训练集上的RMSE:\n", 68 | "k = 5: 0.5747\n", 69 | "k = 10: 0.6845\n", 70 | "k = 15: 0.7322\n", 71 | "k = 18: 0.7500\n", 72 | "k = 20: 0.7596\n", 73 | "k = 25: 0.7774\n", 74 | "k = 30: 0.7902\n", 75 | "k = 40: 0.8073\n", 76 | "k = 50: 0.8182\n", 77 | "k = 100: 0.8422\n", 78 | "k = 200: 0.8546\n", 79 | "item-item协同过滤算法中, 各K值在测试集上的RMSE:\n", 80 | "k = 5: 0.9543\n", 81 | "k = 10: 0.9278\n", 82 | "k = 15: 0.9223\n", 83 | "k = 18: 0.9215\n", 84 | "k = 20: 0.9213\n", 85 | "k = 25: 0.9217\n", 86 | "k = 30: 0.9225\n", 87 | "k = 40: 0.9242\n", 88 | "k = 50: 0.9258\n", 89 | "k = 100: 0.9314\n", 90 | "k = 200: 0.9345\n", 91 | "user-user协同过滤算法中, 各K值在训练集上的RMSE:\n", 92 | "k = 5: 0.6101\n", 93 | "k = 10: 0.7242\n", 94 | "k = 15: 0.7699\n", 95 | "k = 18: 0.7866\n", 96 | "k = 20: 0.7951\n", 97 | "k = 25: 0.8113\n", 98 | "k = 30: 0.8225\n", 99 | "k = 40: 0.8373\n", 100 | "k = 50: 0.8465\n", 101 | "k = 100: 0.8660\n", 102 | "k = 200: 0.8747\n", 103 | "user-user协同过滤算法中, 各K值在测试集上的RMSE:\n", 104 | "k = 5: 0.9874\n", 105 | "k = 10: 0.9573\n", 106 | "k = 15: 0.9489\n", 107 | "k = 18: 0.9468\n", 108 | "k = 20: 0.9458\n", 109 | "k = 25: 0.9449\n", 110 | "k = 30: 0.9445\n", 111 | "k = 40: 0.9447\n", 112 | "k = 50: 0.9453\n", 113 | "k = 100: 0.9491\n", 114 | "k = 200: 0.9526\n" 115 | ] 116 | } 117 | ], 118 | "source": [ 119 | "import pandas as pd\n", 120 | "import numpy as np\n", 121 | "import var\n", 122 | "import predict as pre\n", 123 | "import utils\n", 124 | "\n", 125 | "print('初始化变量...')\n", 126 | "names = ['user_id', 'item_id', 'rating', 'timestamp']\n", 127 | "direct = 'dataset/ml-100k/'\n", 128 | "trainingset_files = (direct + name for name in ('u1.base', 'u2.base', 'u3.base', 'u4.base', 'u5.base'))\n", 129 | "testset_files = (direct + name for name in ('u1.test', 'u2.test', 'u3.test', 'u4.test', 'u5.test'))\n", 130 | "\n", 131 | "if __name__ == '__main__':\n", 132 | " \n", 133 | " i = 0\n", 134 | " nums = 5\n", 135 | " k_set = [5, 10, 15, 18, 20, 25, 30, 40, 50, 100, 200]\n", 136 | " rmse_topkCF_item_train = {k:[] for k in k_set}\n", 137 | " rmse_topkCF_user_train = {k:[] for k in k_set}\n", 138 | " rmse_topkCF_item = {k:[] for k in k_set}\n", 139 | " rmse_topkCF_user = {k:[] for k in k_set}\n", 140 | " for trainingset_file, testset_file in zip(trainingset_files, testset_files):\n", 141 | " i += 1\n", 142 | " print('------ 第%d/%d组样本 ------' % (i, nums))\n", 143 | " df = pd.read_csv(trainingset_file, sep='\\t', names=names)\n", 144 | " \n", 145 | " var.ratings = np.zeros((var.n_users, var.n_items))\n", 146 | " print('载入训练集' + trainingset_file)\n", 147 | " for row in df.itertuples():\n", 148 | " var.ratings[row[1]-1, row[2]-1] = row[3]\n", 149 | " \n", 150 | " print('计算训练集各项统计数据...')\n", 151 | " utils.cal_mean()\n", 152 | "\n", 153 | " print('计算相似度矩阵...')\n", 154 | " var.user_similarity = utils.cal_similarity(kind='user')\n", 155 | " var.item_similarity = utils.cal_similarity(kind='item')\n", 156 | " print('计算完成')\n", 157 | "\n", 158 | " predictions_topkCF_item_train = {k:[] for k in k_set}\n", 159 | " predictions_topkCF_user_train = {k:[] for k in k_set}\n", 160 | " targets = []\n", 161 | " print('在训练集上测试...')\n", 162 | " for row in df.itertuples():\n", 163 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 164 | " for k in k_set:\n", 165 | " predictions_topkCF_item_train[k].append(pre.predict_topkCF_item(user, item, k))\n", 166 | " predictions_topkCF_user_train[k].append(pre.predict_topkCF_user(user, item, k))\n", 167 | " targets.append(actual)\n", 168 | " for k in k_set:\n", 169 | " rmse_topkCF_item_train[k].append(utils.rmse(np.array(predictions_topkCF_item_train[k]), np.array(targets)))\n", 170 | " rmse_topkCF_user_train[k].append(utils.rmse(np.array(predictions_topkCF_user_train[k]), np.array(targets))) \n", 171 | " \n", 172 | " print('载入测试集' + testset_file)\n", 173 | " test_df = pd.read_csv(testset_file, sep='\\t', names=names) \n", 174 | " predictions_topkCF_item = {k:[] for k in k_set}\n", 175 | " predictions_topkCF_user = {k:[] for k in k_set}\n", 176 | " targets = []\n", 177 | " print('测试集规模为 %d' % len(test_df))\n", 178 | " print('在测试集上测试...')\n", 179 | " for row in test_df.itertuples():\n", 180 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 181 | " for k in k_set:\n", 182 | " predictions_topkCF_item[k].append(pre.predict_topkCF_item(user, item, k))\n", 183 | " predictions_topkCF_user[k].append(pre.predict_topkCF_user(user, item, k))\n", 184 | " targets.append(actual)\n", 185 | " for k in k_set:\n", 186 | " rmse_topkCF_item[k].append(utils.rmse(np.array(predictions_topkCF_item[k]), np.array(targets)))\n", 187 | " rmse_topkCF_user[k].append(utils.rmse(np.array(predictions_topkCF_user[k]), np.array(targets))) \n", 188 | " print('测试完成')\n", 189 | " \n", 190 | " print('------ 测试结果 ------')\n", 191 | " \n", 192 | " print('item-item协同过滤算法中, 各K值在训练集上的RMSE:')\n", 193 | " for k in sorted(k_set):\n", 194 | " print('k = %3d: %.4f' % (k, np.mean(rmse_topkCF_item_train[k])))\n", 195 | " print('item-item协同过滤算法中, 各K值在测试集上的RMSE:')\n", 196 | " for k in sorted(k_set):\n", 197 | " print('k = %3d: %.4f' % (k, np.mean(rmse_topkCF_item[k])))\n", 198 | " print('user-user协同过滤算法中, 各K值在训练集上的RMSE:')\n", 199 | " for k in sorted(k_set):\n", 200 | " print('k = %3d: %.4f' % (k, np.mean(rmse_topkCF_user_train[k])))\n", 201 | " print('user-user协同过滤算法中, 各K值在测试集上的RMSE:')\n", 202 | " for k in sorted(k_set):\n", 203 | " print('k = %3d: %.4f' % (k, np.mean(rmse_topkCF_user[k])))\n", 204 | " " 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": 23, 210 | "metadata": { 211 | "collapsed": false 212 | }, 213 | "outputs": [ 214 | { 215 | "data": { 216 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhwAAAIGCAYAAAAfs1aBAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3Xt4VOWBB/7ve86ZSzKZ3EiQACFBvASxXMUWai1QKyA/\n1G6r5ed2ddsKWi913RYqj6xifyq7rfrUIrTekaU+q9QV0KLVWLEUcWtRvBIQTCAhQEJuZGYyt3Pe\n3x+TmcxkZjK5zCQzyffThyeZd86ceadHeL/znvcipJQSRERERCmkDHUFiIiIaPhj4CAiIqKUY+Ag\nIiKilGPgICIiopRj4CAiIqKUY+AgIiKilEv7wPHWW29h5syZCY/74osvcMMNN2DGjBmYP38+nnzy\nyahj/vGPf+Daa6/F9OnTsXDhQrz00kupqDIRERF1k9aB44MPPsCqVasSHtfc3Iwf/vCH0DQNjz76\nKL7//e/jN7/5DZ599tnQMUeOHMHy5ctRWlqKxx57DPPnz8fdd9+NN954I5UfgYiIiABoQ12BWLxe\nL5577jn89re/RXZ2Nnw+X4/Hb9myBbqu43e/+x3MZjMuvfRSeDwePP7447j++uuhqiqeeOIJjB8/\nHg8//DAA4JJLLkFzczM2bNiAyy+/fDA+FhER0YiVlj0cf/3rX/HUU0/hrrvuwg9+8IOEx+/duxdz\n5syB2WwOlV122WVobW3FJ598Ejpm3rx5Ea+77LLLcOjQITQ2Nia1/kRERBQpLQPH1KlT8dZbb+Gf\n//mfIYRIeHxNTQ0mTJgQUVZaWhp6rqOjAw0NDTGPkVKipqYmaXUnIiKiaGl5S2X06NF9Ot7hcMBm\ns0WUBR87HA44HI6IsljHEBERUeqkZQ9HX0kp4/aECCEQ3J8u3jGKMiz+byAiIkpbw6KltdvtcDqd\nEWXBx3a7HTk5ORFl3Y8JPh8PN9QlIiIamLS8pdJXZWVlqK2tjSgLPj777LORnZ2N4uLimMcIITBx\n4sQezy+EQGNje3IrTYOmuNjO65fBeP0yF69dZisutif1fMOih2POnDnYu3cv3G53qOzNN99EQUEB\nKioqQse8/fbbEb0Vb775Js4991wUFhYOep2JiIhGkowMHLW1tfjoo49Cj6+77jp4vV4sX74cu3bt\nwu9+9zs8+eSTuOmmm6BpgU6cH/3oR/jyyy/x05/+FH/961+xbt06vPrqq7j99tuH6mMQERGNGBkR\nOLoP9ty4cSOWLVsWelxcXIxNmzZB13Xccccd2Lp1K/793/8d//qv/xo6pqKiAo8//jjq6upw++23\n45133sG6devw7W9/e7A+BhER0YglJEdE9grvQ2Yu3kfObLx+mYvXLrNxDAcRERFlHAYOIiIiSjkG\nDiIiIko5Bg4iIiJKuWGx8Fc6kFLic8dJHO9oxYTsQpxnGw2lFxvPERERjQQMHEmyq+kLvHX6YOBB\nM7Co+AJcMmrS0FaKiIgoTfCWSpLsbamOePzW6YPw6P4hqg0REVF6YeBIEpNQIx77pI5P2+uHqDZE\nRETphYEjSc7PGR1V9mFbbYwjiYiIRh4GjiSZmVcaVVbT0Yxmr3MIakNERJReGDiSZJw1H8XmnKjy\nD9vqhqA2RERE6YWBI0mEEJgRo5fjwzO1MLhdDRERjXAMHEk0PXccuq+80errwNGOpiGpDxERUbpg\n4EiiXFMWzrEVR5V/wNsqREQ0wjFwJFms2yqfnamHx+CaHERENHIxcCTZ5JwxsCqRC7h6pY7P208M\nUY2IiIiGHgNHkpkUFRfmjo0q/4BrchAR0QjGwJECsdbkqHY1ocXrGoLaEBERDT0GjhQotRagyGyL\nKt9/hoNHiYhoZGLgSAEhBKbnRvdyfNBWC8k1OYiIaARi4EiR6Xnjo9bkaPG5cLSjeUjqQ0RENJQY\nOFIk35SFs7OLosq51DkREY1EDBwpFGtNjk/b6+HlmhxERDTCMHCk0AX2MbB0W5PDY/jxefvJIaoR\nERHR0GDgSCGzouFCe/SaHB9yTQ4iIhphGDhSbEbe+KiyL12n0erjmhxERDRyMHCkWFlWIQpN2RFl\nEsB+Dh4lIqIRhIEjxYQQMQePfthWxzU5iIhoxGDgGATTY9xWafI5UdvRMgS1ISIiGnwMHIOgwJQd\nc00ObuhGREQjBQPHIIk1ePST9nr4DH0IakNERDS4GDgGyRR7CcyKGlHmMfz43ME1OYiIaPhj4Bgk\nXJODiIhGMgaOQRRrtsoRZyPO+DqGoDZERESDh4FjEJVlFaIgxpocH57hmhxERDS8MXAMIkWImINH\n323+EtWu00NQIyIiosHBwDHIpudG31Zx6l48c2wv3j59CAYXAyMiomEobQPHiy++iIULF2LatGlY\ntmwZ9u/f3+Pxr776KpYuXYqpU6di8eLFeOGFF6KOWbp0KSoqKiL+zJkzJzkVNnRYPnkDtrefguWz\ntwA99hb0heZsnGMrjiqXAN46fRDP1b4Hh9+TnDoRERGlCS3xIYPv5Zdfxtq1a3HbbbfhwgsvxJYt\nW3DjjTdi+/btGDduXNTxr7zyClauXIklS5bgrrvuQk1NDX71q1+hra0NK1asAAD4fD5UV1dj5cqV\nmD17dui1mpac/wuy/v5HWA6/BwAwHf8MiqsVHbO/G/PYq8ZMxe9rdsOpe6OeO+I6jceq38E1Y2di\nki16sTAiIqJkklLCgIQR9jPwNTi5hEzDDT0WLFiAefPm4Z577gEA+P1+LFq0CPPnz8fdd98ddfzS\npUtht9vx/PPPh8peeOEFrFu3Du+88w7y8vJQVVWF73znO3jttddQXl7e5zo1Nrb3+HzeC6shfO7Q\nYykEzlz9H5C2gpjHt/k68GL9Bzja0RzzeQFg3qjzML/oPChC9Lm+1KW42J7w+lH64vXLXJly7SIa\nXClhwAg1vnr3sojGuXuZEfG8HqPMkBIy9Hxvztl5DsR+n6j6xfocic4fJ1w8/o3rkvr/c9r1cBw9\nehT19fWYP39+qEzTNMybNw+7d++O+ZqamhrcdNNNEWWzZs2C2+3G+++/j8suuwxVVVWwWq0oKytL\nSb2N7DyobV2BQ0gJy6F34Z6xJObxeaYs/GjCHPzl9EH8telw1OWWAN5uOoSajiZcO3Ym7Jo1JfUm\nIorXYPamIdN7aMhypAWtZ1xxG089/H07y8Ib/5gNclTj262s2zn17mVR3+bjNbeUbGkXOGpqaiCE\niAoG48ePR21tLaSUEN2+8ZeUlODEiRMRZbW1gQW16uoCU04PHjyI3Nxc3HHHHdizZw+EEFi0aBFW\nr14Nm8024Hp7y2ci66PXIsrMh/fCPfVyQDXFfI0qFHy7eDLKs0fhj/UfxrzFUu1qCt1iiTX2g4gG\nRkoJCcRtyPQ4DVmP3yLDG7teNIrxviXr3RrFqG/QfWx8470PG1waDGkXOBwOBwBEhQCbzQbDMOBy\nuaKeu/LKK/H73/8eM2bMwMKFC1FdXY1HHnkEiqKgoyOwqNahQ4fQ1NSECy64ADfccAOqqqrw6KOP\n4vjx43j22WcHXG/vuXNg/eQNiLC9URSPE+aaD+GddHGPrz3XNhq3ln8TW098gGpXU9TzTt2L52rf\nwzdHnYtLR50Ds5J2l40oQrAx9EkdfqPzZ9jvwXK/1OEL/gz73S8N+A0dPmnA3KzC5fbGbZBlrMY/\nrKw33dZElHpp13IFh5R078UIUpToiTU33XQTTp8+jXvuuQdr1qxBfn4+1qxZg5UrVyIrKwsAsHLl\nSni9XkydOhVA4JZLQUEBfvazn2Hfvn2YNWvWwOpttcNXNh3m6n0R5eaDu+E9ezaQYBxGrsmKH5bO\nwdunD2FX06GYt1h2NX2BPc1HMMlWjMk5Y1CRcxZsmmVA9abhTXZ2efuNQCPuM/TOxr2rQQ80+p3P\nS73r2F4EhFBZ6NxdgYGIMocCASEEFAgoQkBNwdjBtAscdrsdAOB0OlFYWBgqdzqdUFU1FCDCmUwm\nrF27FqtWrcKJEycwYcIENDY2QkqJvLw8AEBFRUXU6y699FJIKVFVVZUwcBQX2xPW3fjaQujdAofW\nXIdR/kYoYyclfD0ALBt9Eaa1jMczB9/FmbBBqEE+aaDKcQpVjlMQEJiUW4Tpo8ZjSkEJxmTncYBp\nHL25fqlmSAM+w4DP8MNnGPAafviMQGPv03V4jUBj7w2Whf2JVRYq1zt7B3Q97Nxd52KHOVFigUZW\nCTW2CpToMhFZFvl8V5kiFKgI+z3iZ2RZ+LkTliHWcfHr19uyYNhItbQLHGVlZZBSora2FqWlXYtk\n1dXVxZ1d8t5770FRFFx88cWYNCnQsFdVVUEIgcmTJ0PXdezYsQMVFRWYPHly6HVud6BBLyiIPZMk\nXK9GWqvFyBk1AVrTsYjijv/7M1yX/Evi13cqgg0/KfsGttZ/iC97WIFUQuLwmUYcPtOIP1Z/CKui\nYXxWAUqtBSix5iJHs8CmWpCtmmFVtEH5DyoddR8pL6Xs/CYe/e0+9C097rf/7t/ow3sOuvUQGJ29\nAJ3n09nwUxoLbyCV0O/Bb7xxysK+ESuIbMAUIZBlNcPn0SOP6+H8wQZZIPb7qPHeOywgxKuzGqfO\nilAgEL9XPS31cdaqhIQfeuIDu0n2F7W0Cxzl5eUoKSlBZWUl5s6dCyCwhsauXbsiZq6E27lzJ/bv\n348dO3aEyp5//nmUlJTg/PPPhxAC69evx+TJk7Fhw4bQMX/+859hMpkwY8aMpNXfc/4l0N59PqLM\ndHQ/xMwrIbPzen0eu2bFv5Z+DbuavsDbpw/26r8tt+HHYWcjDjsbo55TIJCtmmHTzIGfqgU2NfB7\nlmqCSVGhCQVmoUJTVJiEGioL/FRhVlSoUCBD/+uaTiYlQqVG2O/BAXmy8xgjVI7Q88EyvTMI6KGu\neQN655/wx/HKQ4+N4O86dGlA1gBuv78zCATOzaafwkU2QOGNnhLRxdyXxjdWo6iiNw1lnLLwb81x\nG+9Y5+/5nMHfUyFTpsXS4Ei7wAEAy5cvx/333w+73Y6ZM2diy5YtaG1txQ033AAgMAOlubkZ06ZN\nAwBcc801+N///V88+OCDWLBgAXbs2IF3330XjzzySCi13nzzzbj33nvxwAMPYMGCBfj444+xceNG\nXH/99SgpKUla3X1lM2B8sAOK2xEqE9KA+fBeeKYu6tO5FCGwoOg8VOSchfdaqnHQcSrmTJbeMCDh\n0D1w6FzFlAaHJhRoQoVJ6fwpFGjBEBsWaLXwcCtUaErnz87f83Oz4Wz3xG9oQ4Gg/9/MM+rbLVGG\nSsuFvwBg06ZN2Lx5M1paWlBRUYHVq1eHBnyuXr0a27Ztw4EDB0LHV1ZW4tFHHw3devnJT36Cyy+/\nPOKc27Ztw7PPPoujR4+iqKgI1157bWgl0kT6ktKt+3fC+umbEWWG1Y4z37kHUPuf8QwpcayjGQcc\nJ3Gg/SSafa5+n4tGDgWiW+MeDADhjbvS1bMVHgwigkDnc1EBITosBLvWk4HfkjMXr11mS/YtlbQN\nHOmmL39phLMVudv+P4huI/Wdl/wLfOUzk1IfKSUavO34vP0kql1NqHO3wGv0/R4dDa6oBjvut/zw\nxj0yIAR7C2K+NkYQUEXabpnUK2y0MhevXWYb9mM4hgNpy4ev9CswH/sootxycHfSAocQAmdZcnGW\nJRfzEej9aPC041hHM+rcrWj3u+HUvXD6vXDpXvgkw0g4VSgxuvC7xqv01P0fPD7et/xgQOh+O0EV\nCrvuiWjEYuBIEU/FN6ICh9ZYA7WpFvqo6C3qB0oRAmOsuRhjzUWsZca8hh8uPRA+nH4vnMHfdS88\nui8w66JzhkVwAabgYk0+o2vdBl0aEEIERnV33vsWne8f+F9XeeDeeOfjsN/jlQdDQPBbecRjJViu\nQhUi1Pj35jXFo+xob3FHhAZOHyYiGlwMHCmiF58NPX8s1Nb6iHLzwb+hY+7/O+j1MSsazIqGfFP2\noL/3UCvOskNxMGAQEQ2lzL65m86EgOf8b0QVm2s+gAibwUJERDQSMHCkkHfiTBjmyB4FYfhhPvze\nENWIiIhoaDBwpJJmhnfSV6OKLYf2AJxRQkREIwgDR4p5z/86JCLHDyiuVpjqPhuiGhER0YgmZeBL\nr+4H/B7A64bwOCHcDghXG4SzBcLZkvS35aDRFDNyRsE/fgpMdZ9GlJsP7oZvwtQhqhUR0TAmDcAw\nAj+lARFsYKUEZOCnCD02AEMPHNP5XPBY0fn64Lm6Pw6VhT8O+z14rOj2uOtc4e+ldz4Xq47Bx/HO\n08c69nb5rTufSuplYeAYBJ7zL4kKHKZTh6G0noCRn7xl1YloBAg2krEaNCOsQZE6AhsbxWhgQw1j\nahtY3aIhy+XualC7v1dPIaCXjXfMOlNaYuAYBP4x50HPHQ31TENEueXg39Dx1WuGqFZEaU4agN8H\n6VYDM7tiNjqyb99OEzRo/WngIhvh3n47HUADn0GLQxsALENdCUobDByDoXOKbPb7L0UUm7/8B9wz\n/h9Ic9YQVYwoiQwD8HsgfB6IsJ8IPXbHKOv6CZ87sswf2KjQD6D3+ywTUbpi4Bgk3rMvQtb+VwP/\nsHYSuhfmI/8Hz+R5Q1cxGrkMPdCo+9wQfm+cQBAoixskfB4IvxvC54Xo507GRDT4pBCAUAJ/FAUy\n+HvosQpTkt+TgWOwmKzwnn0xLAd3RxSbD+6Bp+LSwEUm6omhhzX+kSEhfiBwxwkJHgjdN9SfiChl\npFAARQU6G1bZ2ZAGG9Xuj2M3upGPQ68TInRuKTrfI/zcofftPCb8fRKeu9vjbq+NPJeIfC8l8DMU\nJkLn6XoOoaCRePVla5KvCQPHIPKcd0lU4FAdp6HVV8E/7oIhqhWljKFD+Nw93D7wRgSCUGgI6zWA\n391VZviH+hNRmgg1ep0NiQxvSMIbmbCGsHtjFt14RT/uOnf4eSMbt1Cjq4io8+TYs+FweiMb2J7q\n0+cGNnYI4Be49MTAMYiMvNHwlZwP04mDEeXZf/8jOqYtDuwkq6hDVDuC7o8/niAiJAQDQfeQ0O1W\nBBd3GzCpmiE0FQaUiEZGhn177Pqm2fcGrTffPHv9XuGNXa8a8x6+UffYCItefTtNB2qxHV5uT0+d\nGDgGmef8b0QFDsXZAtu7z0P/+M/wXHgZvBMvAlReml6TEtC9EB4XFI8LwusKLGLjdUF4XNBNBrLa\n2iH8bsDnDYWFqJDAgDBgUrNAmiyhn+j2WJqsMcrCj7OGnoNmBhQFxcV2NLLRIsp4bNUGmX/sZOg5\no6A6mqKeUx1NyH7vBVg/eQPuKd8KLIs+koKHlIDfC+FxQukMC93Dg/B2hopuZT2FBU7Ni01CACZz\noJHXzH0KBAh7ritcmNmVTURxjaDWLE0oCtwzr0T27k1x59MrzhZk//2PsH7yJtxTFsB7ztcC/5hn\nEsPoCgQeR6D3weuEcDshvM7OcicUjzMQHnoRHEa6QEAIa+SDDb0pLDD0GAgCgaKrB8HEgEBEg0ZI\nmUGryAyhZHfpqo01sH60E6aTXyQ81rDa4blgPjznzgVMQ/BdXfeFBQRXZ2CIFRq6yoS3AwIj+z8t\nKURXI6+ZQ409tMjeA6lZu3oawkICTOG9CmZANWfMvftk4i2VzMVrl9mKi+1JPR8DRy+l6i+N2lgN\n6ydvwFRflfBYqZpg2Aqi/sjsfEhzVue3XnOgsQIgdF9g6qPuC6y3EP5Y9wH+wE/hc4f1RoTdtgj2\nRIyQ6ZNSKD2MO+gMDN3HJoQHgu6BQTWNyICQbGy0MhevXWZLduDgLZUhphdPhHPBTVBPH4P10zej\n9lwJJ3Qf1DMNUUukEyAVFdJigzRnQ1qyYQR/N2cjuyAPDi+iAkHEbQbNzIBARJRCDBxpQi+aAOe8\nH0NpOQ7rJ2/CdOzjEXlLQqomSEt2KCwYwd8ttq5yS/hzgfKewoKdU/OIiIYcA0eaMQrGwXXpv0Jp\nPRno8Tj6YUZt1hROmqyBnobQn0BwiCyzRQaHTBscS0REvcLAkaaM/DFwXfIvUKYuhOWzv8B87KPA\nqpVDQAqlW++CDYbVFgoJ0pLTdRsjLFxwETMiIgpi4EhzRu5odMxZho6vfT8wI8TZAsXVCuFshuJs\n7XzcFljAyu8NrITpD2wQJ1UToJoCPzUTpGqG1MLKOn9KzRzzloW02GCYswMzYzi2gYiIBoCBI1MI\nAWnNgW7NgT6qdKhrQ0RE1Cdc9YeIiIhSjoGDiIiIUo6Bg4iIiFKOgYOIiIhSjoGDiIiIUo6Bg4iI\niFKOgYOIiIhSjoGDiIiIUo6Bg4iIiFKOgYOIiIhSLm0Dx4svvoiFCxdi2rRpWLZsGfbv39/j8a++\n+iqWLl2KqVOnYvHixXjhhReijvnHP/6Ba6+9FtOnT8fChQvx0ksvpar6REREFCYtA8fLL7+MtWvX\n4qqrrsL69euRm5uLG2+8EcePH495/CuvvIKf//znOO+88/C73/0OP/jBD/CrX/0KTzzxROiYI0eO\nYPny5SgtLcVjjz2G+fPn4+6778Ybb7wxWB+LiIhoxBJSSjnUlehuwYIFmDdvHu655x4AgN/vx6JF\ni0IhobulS5fCbrfj+eefD5W98MILWLduHd555x3k5eXhF7/4BT7//HO88soroWNWrVqFgwcPYvv2\n7Qnr1NjYnoRPRkOhuNjO65fBeP0yF69dZisutif1fGnXw3H06FHU19dj/vz5oTJN0zBv3jzs3r07\n5mtqamowd+7ciLJZs2bB7Xbj/fffBwDs3bsX8+bNizjmsssuw6FDh9DY2JjcD0FEREQR0i5w1NTU\nQAiBsrKyiPLx48ejtrYWsTpkSkpKcOLEiYiy2tpaAEBdXR06OjrQ0NCACRMmRBxTWloKKSVqamqS\n+yGIiIgoQtoFDofDAQCw2WwR5TabDYZhwOVyRb3myiuvxPbt2/HHP/4R7e3t+Pjjj/HII49AURR0\ndHT0eM7w9yQiIqLUSLvAEezBEELEfF5Roqt800034Xvf+x7uuecezJ49GytWrMBNN90EKSWysrL6\ndU4iIiJKHm2oK9Cd3R4YpOJ0OlFYWBgqdzqdUFUVWVlZUa8xmUxYu3YtVq1ahRMnTmDChAlobGyE\nlBL5+fnIyckJnSNc8HHweSIiIkqNtAscZWVlkFKitrYWpaWlofK6ujqUl5fHfM17770HRVFw8cUX\nY9KkSQCAqqoqCCFQUVGB7OxsFBcXh8Z1BNXW1kIIgYkTJyasV7JH69Lg4vXLbLx+mYvXjoLSLnCU\nl5ejpKQElZWVoZknPp8Pu3btipi5Em7nzp3Yv38/duzYESp7/vnnUVJSgvPPPx8AMGfOHLz99tv4\nt3/7t9CtlTfffBPnnntuRE9KPJzalbk4NS+z8fplLl67zJbssKiuXbt2bVLPmARmsxkbN26E1+uF\n1+vFunXrUFNTg//8z/9Ebm4uamtrUVNTgzFjxgAAioqK8NRTT6G1tRUmkwkbN27Em2++iV/+8pc4\n99xzAQRmpDz++OOoqqpCTk4Onn/+eWzduhVr164N9Yr0xOXypvQzU+rYbBZevwzG65e5eO0ym81m\nSer50nLhLwDYtGkTNm/ejJaWFlRUVGD16tWYOnUqAGD16tXYtm0bDhw4EDq+srISjz76aOjWy09+\n8hNcfvnlEefcs2cPHnroIXz55ZcoKSnBzTffjKuvvrpX9WFKz1z8lpXZeP0yF69dZkt2D0faBo50\nw780mYv/6GU2Xr/MxWuX2Yb9SqNEREQ0/DBwEBERUcoxcBAREVHKMXAQERFRyjFwEBERUcoxcBAR\nEVHKMXAQERFRyjFwEBERUcoxcBAREVHKMXAQERFRyjFwEBERUcoxcBAREVHKMXAQERFRyjFwEBER\nUcoxcBAREVHKMXAQERFRyjFwEBERUcppQ10BIiIi6j8pJSQAXRowpISOzp/SgC4lDHT+lGE/Y5bJ\n0OskJBYXX5jUejJwEBERpUiwAe8eBOI9jh0CAo91acBA2PGhQCEhpRzqj5oQAwcREY04RowGPdSw\nxw0GcUJAD+EhE4LAYGHgICKitGF0fmPvfQjouTcg3nkMBoFBx8BBREQJSSlj9gZEjQ9AV4Pf3OpC\nk9MRGRQS9CowCAxfDBxERBlMdvYIhDfowcfxgoAR4zU9hYngcX1lgxlOlzcFn5q6E0JAFQIqFChC\nQBWdP8Meq0JAEQpUdP6McVzwd1WIpNeRgYOIKAWCQaA3IaDHQYGdQaD7ecJvJ1D6EgKBxj68QRcK\nFMQPAaEwECM8hD+vhJ1HSUFASDYGDiIaUeIFgdCgwD5MIYzVqxD+mFkgfQkBKOj2LT9GEAg87h4C\nujX83c8TFhQEAr0PxMBBRGnKLw24dR80j4pmrytOEOjPFEKDQSDNxQsBoVsDcXsDgmVdISDi1kHY\neRQIBoFBxsBBREPKkBIu3Qun7oHDH/zpgdvwQUrA5jXD6eQ4gHQQHgS6N+jB2wLhwaAwz4Z2uGP2\nBnQPAuHhgkFgeGLgIKJBIaWE2/CFQoXT74FD98KlezkzYYAiBgXGCQI9DiRM0BvQ3yBQnG9Ho689\nRZ+aMg0DBxElncfww+n3wKl74fB7Qr0XujSGumqDqvuo/556A7oHgXiPYw0azIQBg0QMHETUb35D\nh0OP7LFw+j3wGvpQV61HyZpC2NUTEKtHITNmDhANFgYOIkpIl0ZgnIXfC0dnuHDqXnTovpS+b5Zq\nQp45GyavmpQphAwCREOHgYOIQozQOAtPZ69FIGC4dF9K94QwKypyNAtsqgU5mhk21QKbaoamqCgu\ntqOxkeMAiDIdAwfRCCSlhNfQQ70VDj3QY+H0e1K6kJSmKIFQoZphCwsYZoX/FBENd/xbTjTM+Qw9\nasqpU/fCl8JxFooQyFbNnb0W5lDvhVXROOWRaIRi4CAaJoLjLBzBHovOgOHW/Sl7TyGALMUEm2ZB\njmrp/GlGlmrmOAkiisDAQZRhDCnRoXujppx2GN6UrqBpUTTYNDNyVEuox8KmmaEKJXVvSkTDBgMH\nUZqSUsJj+OEIuw0SnB2SyoWyNEUJ660IhAqbynEWRDQwafsvyIsvvoinn34aJ0+exOTJk3HXXXdh\n+vTpcY8wTlWSAAAgAElEQVT/4IMP8NBDD6GqqgqFhYW4+uqrcfPNN0PTuj7i0qVL8cUXX0S8rqCg\nAHv37k3Z5yDqDa/hD90CcQTXs9A98BupWyhLESJifEVwvIWF4yyIMp+UgDQAww+h+wHdD2HogO7v\nKjN8Uc8JI/AY0gCKFye1SmkZOF5++WWsXbsWt912Gy688EJs2bIFN954I7Zv345x48ZFHV9bW4sf\n//jHmD17Nh577DFUV1fj17/+NVwuF1atWgUA8Pl8qK6uxsqVKzF79uzQa8MDCVGq+aUBV9gCWcGx\nFh4jteMsstWuqaY5WiBcWBUTx1kQpZtgUAhr/EMhICI8xP7ZdYwOkWYr+6Zla7t+/XosW7YMt9xy\nCwBg7ty5WLRoETZt2oS777476vjXXnsNUkqsX78eFosFc+fORUNDA/7whz+EAseRI0eg6zq+9a1v\noby8fDA/Do1A3TckC04/TfVCWVZVi+ityNEsyFY5zoIo5aQEDD2s9yD4U4fQfd16FsKfiyyH7ocY\npnsLpV3gOHr0KOrr6zF//vxQmaZpmDdvHnbv3h3zNT6fD5qmwWKxhMry8vLgcrng9XphNptRVVUF\nq9WKsrKylH8GGjmGakMyk6KGxld0jbOwwKSoKXtPomEpGBRi9RrovohQkLBnYXjmhKRJu8BRU1MD\nIURUMBg/fjxqa2shpYy6v3zllVdi8+bNeOihh7B8+XIcPXoUmzdvxre//W2YzWYAwMGDB5Gbm4s7\n7rgDe/bsgRACixYtwurVq2Gz2Qbt81FmklLCK/WIDcmCt0NSuSGZKkS3wZuB3guzUDnOgkY2afQQ\nFGL0GoSei9ELQYMi7QKHw+EAgKgQYLPZYBgGXC5X1HOlpaVYuXIl7rnnHjz11FMAgClTpuDBBx8M\nHXPo0CE0NTXhggsuwA033ICqqio8+uijOH78OJ599tkUfyrKJEOxIZnoHMBp61yBM7gSZ5ZiYrCg\n4cXoHMjYfQCj7gsLDnrscQkRP9N7g8B0IIUAVA1S1QBFC/we5yeU4HFq6Hh7kuuTdoEjuF9DvH9k\nFSX6XvTWrVvxH//xH1i2bBkWL16MhoYG/Pa3v8WKFSuwadMmmEwmrFy5El6vF1OnTgUAzJo1CwUF\nBfjZz36Gffv2YdasWan7UJSWhnJDslCo6OyxyOZCWZTuDKOHXoPYocBo0GBuc3QLEQwKiUhFCQsA\n3QOCGhEiYh4TLBNKYNR4mki7wGG3BzKV0+lEYWFhqNzpdEJVVWRlZUW95sknn8S8efOwdu3aUNmU\nKVNwxRVX4JVXXsE//dM/oaKiIup1l156KaSUqKqqShg4iouTnfVosBhSIivfjHafG+1eN9p9Hpzx\nueH0eSDRedNVADABiknABnNS3tesasg1WWE3WZFrDvzMMXGcRX/w71//yLAZD9B9gL+zx6DzsYxR\nFvxddnuMfgQFeQbIEQhradTOP8OUogKqCVA1CLWz50ALPA786fpddHscfpwYpv9GpF3gKCsrg5QS\ntbW1KC0tDZXX1dXFnV1y4sQJfOc734koO/vss5Gfn4/Dhw/DMAxs374dFRUVmDx5cugYt9sNILAW\nRyLcrTKzeA0/TrrPoMHbDmkBzjjcKXsvVSjICQ3ejLEhmQ6gA/B36GiFK2X1GK5G5G6xPUyN7PUA\nxjSYGpltM8Pl9A7Z+/eWVNWI3oIeexa634pQTYHnFA2I0QPfI73zDwDAAODt/JMekh300y5wlJeX\no6SkBJWVlZg7dy6AwCyUXbt2Rcxc6f6aDz/8MKLs6NGjaG1tRWlpKRRFwfr16zF58mRs2LAhdMyf\n//xnmEwmzJgxI3UfiAaNlBJtfjfq3a045WkPzRKxmZPTYxHakCw4zoIbklF38aZG9jgeYWRNjUym\n7gFBqmqccQndfkYcowZuPVDKpV3gAIDly5fj/vvvh91ux8yZM7Flyxa0trbihhtuABBY6Ku5uRnT\npk0DANx666248847sWbNGixZsgSNjY3YsGEDSktLcdVVVwEAbr75Ztx777144IEHsGDBAnz88cfY\nuHEjrr/+epSUlAzZZ6WB8xs6Tnnacdzdina/Z8Dn44ZkI1DMqZG+sB4FTo1MFinQ9wGMoedM3YIC\n/z5mEiFlesboTZs2YfPmzWhpaUFFRQVWr14dGvC5evVqbNu2DQcOHAgdX1lZiY0bN+Lw4cMoKirC\n17/+ddx5550R40C2bduGZ599FkePHkVRURGuvfZarFixolf1GXFduhnA4ffguLsVJz1nelwC3GYz\nwxmnW5cbkqW/Hm+pxJoaGWcAI6dGDkxwxkN0r4Ea91ZEYXEemto8kc8xKGSMZN9SSdvAkW4YONKD\nLg00ehyoc7eizdfRq9fYbGZ4OvzckCxTSAOiox1KRxuEx4k8mwlnWto5NbKf+jU1MtZz/ZjxMCLH\n3wwjw34MB1EsLt0b6M1wn+n1ehgmRUWJNRdTx46Dq8XLcRbpSEoInxvC1QrF1QbF1QbRcSZi6qR0\nmaFmwMDDZOvf1EhT1HPpNjWSRi4GDkpbhpQ47XXguLsVzd7ez+7IM2VhnDUPoy32wAwSkxUdIrVr\na1Av6b5AqHC1QekMGcI/vMJE12yG/gxg7LpNgWE6NZJGLgYOSjtu3Yd6dxvq3W293kVVFQrGWHMx\nzpoHu2ZNcQ2pVwwDwt3eFSw62qC4nUNdq7j6NDVSNcU4pp9TI4lGCAYOSgtSSjT7XKh3t6LR60Rv\nhxblaBaMs+ZjjMUOjd8Ih46UEF5XoOeiow2Ksw3CfQaih8G8SXvrvk6NjDl2gVMjiVKNgYOGlCEl\n6t2tqO1ogauXS4orQmC0xY5x1nzkaVaOzRgKfm/YmIvA7RHhT95tK6mZYGTnQ2blQTlrFLxtnh6C\nAq8/USZg4KAh4/B7cMBxEmd8vVsFNFs1BXozrLmcXTKYDD00a0RxtQZ6MTzJWzFVKgqkNReGLQ9G\nVh5kdh6kOTsUJESxHYbgTAeiTMd/tWnQGVKixtWEox3NodVA4xFCoNhsw1hrPgpN2ezNSDUpITzO\nzl6LYA9Ge1KXxzasNsisPBjZeYFeDKud4x6IRgAGDhpUZ3xuHHCchCPBiqBWVUOJJQ9jrXmwqqZB\nqt0I5Pd0BYvg4M4kLoIlNTOM7HwY2YGeCyMrF9CSs9Q8EWUWBg4aFLo0Ons1WnocEFpozsZ4az5G\nmXO4jHiyGTpEx5nOgNEZLry9WzytN6SiQmbldvZc5EFm50OarBxjQUQAGDhoELT5OnDAcRLOHtZb\nsGlmTM4ZgzxT1iDWbBjrvDUSGnPhaoNwtydtQzApAGnJCeu5yOOtESLqEQMHpYwuDXzpPI1adwvi\ntXNCCJRlFaI8u5D7lwyEzx15a6TjTHJvjZgsET0XRlZeYJYIEVEv8V8MSokWnwtV7Sd7nOpq1yyY\nbB/Dhbr6Svd33hpp7Zw50gbh7d1Mn96Qqto1qDMrMLATJgtvjRDRgDBwUFL5pYEjzkbUdbTGPUYR\nAuXZhSjLGsVxGolIA8IddmukI3hrJEmnF4C02kPTUQOzRmxcBIuIko6Bg5Km2etEleMUOnro1cg1\nWTE5ZwxyNMsg1ixDSAn4PJE9Fx1tSd0RVZqtoZ6LwK2RXN4aIaJBwX9paMB8ho7DzkbUu9viHqMI\ngbOzi1CaVcBejSDdH+ixCJ814ut5unBfSFWDkZUbCBad4y9g4u0rIhoaDBw0IKe9Dhx0nIK7hwGK\neaYsTM45C7aR3KshjdBqnaFZIx5HEm+NiMCtkeywWyMWG8ddEFHaYOCgfvEZOr5wNuCE+0zcY1Qh\nMMlWjHHW/JHVqyElhM8dWkgrcGvkDISRzFsjWaGVOo3sPMisXG5nTkRpjYGD+qzB045DjoYet44v\nMGWjwn4WstURsKqk7uucjhp2a6SHNUf6Sqpa13TUzvEXMI3g3iIiykgMHNRrXsOPQ44GnPLE30hL\nFQrOsRVjnDVveO57YhgQ7vauYNHRBsXtTNrppVAgs3LCei7yeGuEiIYFBg7qlSavE5+3n4C3h9sC\nheZsVOSMQdZw2ftESgivKzQdVXG2QbjPQBhJ3MjMkh1aqTOwHbudt0aIaFhi4KCEGj3t+LT9RNyd\nXTVFwbm20Six5GZ2r4bfGzbmonMrdn/8Kb59JTVTZ6jI65o1wo3MiGiEYOCgHiUKG0VmG87POSvz\ndnQ19NCskdCiWh5X0k4vFQXSGr6RWR6kOZu3RohoxGLgoLgaPe34pP1EzN1dTYqK82yjcZbFnva9\nGlJKCLejs9ci2IPRDiGTeGvEaovouZDWXG5kRkQUhoGDYmro7NmIFTbyTVm4MHcsLEoa/+cjDSht\np6C21MOodsFyJokDOzVzaEpqYPxFLm+NEBElkMYtBg2VRGFjWt54aOm614ZhQG09AbXhSNctElv/\nw4BUlMiei+x8SJOVt0aIiPqIgYMi9BQ2CkzZmJo3Lj3DhqFDbTkOraEawtvRr1NIAUhLTudKnZ37\njVjtvDVCRJQEDBwUkpFhw9ChNtVCa6zu8z4k0mSJ6LkIbGSWYYNfiYgyBAMHAUgQNszZmJqbZmFD\n90NtOgatsaZXq3pKRYXMzg0sqJUVtpEZb40QEQ0KBg7CKU87PushbEzLHQc1XcKG3wvt9DGop2sg\netgwLki3j4JS8RV4PGYgXT4DEdEIxMAxwp3ynMFn7Sdjho3Czp6NtAgbfg+0xhqoTccg9MSboOm5\no+EffTakLR8izw40xl+OnYiIUo+BYwQL9GykedjwuqGdrobaVJdwt1UpACNvTCBoZOUOUgWJiKg3\nGDhGqHa/G5/HuY2SDmFDeFxQG6uhNh9PuECXFAJGfkkgaFhzBqmGRETUFwwcI5AuDXwWZ7nyoQ4b\nwu2A1lgNpaUeIs5y6kFSKNALx0IvnhjYUZWIiNIWA8cI9KXzNJwxZnYMZdgQHe3QGo5AaTsJ0XPO\ngFQU6IWl8BeXA+asQakfERENDAPHCNPsdeFYR0tUeY5mGZqw4fPAVP851NZTCQ+Vqgp91AT4i8oB\nkyX1dSMioqRh4BhBfIaOA44TUeWKEJhiLxn0sCEczTAf+yjhgl1S1aAXlcFfVMY9S4iIMhQDxwhy\nyNkAd4y1KyZlFyFHG8QeAymhNlZDO/lFj+M0pGaCv7gc+qgJXAGUiCjDJfxKu27dOmzZsqXfb3Dd\nddfhggsu6PPrXnzxRSxcuBDTpk3DsmXLsH///h6P/+CDD3Dddddh5syZuOyyy/DYY4/B749sXP/x\nj3/g2muvxfTp07Fw4UK89NJLfa5XpjrlacdJ95mo8nxTFsZnFQxeRfxemGo+gOnEobhhQ5os8I2t\ngKfim9BHT2LYICIaBhIGjueeew6vvfZa3Oe/9a1v4c477+zxHLGmXvbk5Zdfxtq1a3HVVVdh/fr1\nyM3NxY033ojjx4/HPL62thY//vGPkZOTg8ceeww//OEP8dRTT+GRRx4JHXPkyBEsX74cpaWleOyx\nxzB//nzcfffdeOONN/pUt0zk1n046IgeI6EpCi6wl0AZpOW9hasVli/ehXqmMebzUjPDN+4CeCou\nhV5cDqjsgCMiGi4G/C/68ePHMWbMmGTUJWT9+vVYtmwZbrnlFgDA3LlzsWjRImzatAl333131PGv\nvfYapJRYv349LBYL5s6di4aGBvzhD3/AqlWrAABPPPEExo8fj4cffhgAcMkll6C5uRkbNmzA5Zdf\nntT6pxMpJaocp+CLsWjWebbRyBqM3gMpA/ue1B+Mu6aGYSuAd8I0wGxNfX2IiGjQpcGa1ZGOHj2K\n+vp6zJ8/P1SmaRrmzZuH3bt3x3yNz+eDpmmwWLrGIeTl5cHlcsHrDUz/3Lt3L+bNmxfxussuuwyH\nDh1CY2Psb9zDwXF3G5q8zqjy0ZYcjLEMwmqcuh+mYx/BdPxA3LDhHz0R3kmzGTaIiIaxtAscNTU1\nEEKgrKwsonz8+PGora2NeXvmyiuvhKqqeOihh9DW1oaPP/4Ymzdvxre//W2YzWZ0dHSgoaEBEyZM\niHhdaWkppJSoqalJ5UcaMk6/B4edDVHlFkXD+TlnQaT4VoroOAPzF+9CbT0Z83mpavCWz4S/5Hxu\nrEZENMyl3U1yh8MBALDZIleOtNlsMAwDLpcr6rnS0lKsXLkS99xzD5566ikAwJQpU/Dggw8mPGf4\n88OJISU+d5yEHiOgVdjPgllJ4aWXEmpzHbT6AxBGnFso2bnwlc2A5MJdREQjQtp9rQz2YMT79q0o\n0VXeunUr1qxZg2uvvRbPPfccfv3rX+PMmTNYsWIFfD5fv86Z6WpcTTjjc0eVj8vKR5E5hfuN6H6Y\n6j6Fqe6zuGHDXzQB3klfZdggIhpB0q6Hw263AwCcTicKCwtD5U6nE6qqIisrupF68sknMW/ePKxd\nuzZUNmXKFFxxxRV45ZVXsGjRotA5wgUf5+QkboCLi+19/ixDpcXjQqPLAZstcpEsm2bBnJKJ0BQ1\nJe8rO9phHNoPeM4AthgLdKkalLOnQxSNS8n79ySTrh9F4/XLXLx2FJR2gaOsrAxSStTW1qK0tDRU\nXldXh/Ly8pivOXHiBL7zne9ElJ199tnIz8/H4cOHkZ2djeLiYtTW1kYcU1tbCyEEJk6cmLBejY3t\nff8wQ8AvDbzfchQuPXKvFCGAyXlj0NLkSsn7Ki31MB3/DEKPvYW8kWWHr3Q6pLQBg/z/ZXGxPWOu\nH0Xj9ctcvHaZLdlhMe3uJZSXl6OkpASVlZWhMp/Ph127dmHOnDlxX/Phhx9GlB09ehStra2h0DJn\nzhy8/fbbEYNO33zzTZx77rkRPSmZ7oizMSpsAEBZ1ijkmVJwC8PQodV9BvOxj+OGDX/hOHjP+Rp3\ndCUiGsHSrocDAJYvX477778fdrsdM2fOxJYtW9Da2oobbrgBQKBnorm5GdOmTQMA3Hrrrbjzzjux\nZs0aLFmyBI2NjdiwYQNKS0tx1VVXAQB+9KMf4Xvf+x5++tOf4pprrsGePXvw6quv4re//e2Qfc5k\na/G6UNfRGlWea7JiYvaopL+f8LhgOrYfiit6BVMAkIoK/7gLoBcO/i0UIiJKL0ImWAa0oqIiKdMn\nDxw40KfjN23ahM2bN6OlpQUVFRVYvXo1pk6dCgBYvXo1tm3bFnHOyspKbNy4EYcPH0ZRURG+/vWv\n484774zovdizZw8eeughfPnllygpKcHNN9+Mq6++ulf1SfduQSkl9rUdQ1u3gaKKELg4vwy2JO+V\norSdgqn2E4gYe7MAgGHJDsxCyRr6+7fs1s1svH6Zi9cusyX7lkqvAseA30SIPgeOdJPuf2kaPe34\n+Ex9VPl5OaNRmsy9UgwD2slD0Bpr4h6i54+Bb/yFabM0Of/Ry2y8fpmL1y6zJTtwJGwR1q1bl9Q3\npOQzpMQR1+mo8hzNgvHW/OS9kdcN87H9UJzRt20AQAoF/rEV0EeVBkapEhERdUoYOLrP/qD0c8pz\nBk5/9EDRSbaipK0mqjiaYTr6IYTfF/N5ac6Ct2w6ZHZeUt6PiIiGl/To86Z+06WBaldTVHm+KQuj\nTMmZFSJcbTDV7Is7C0XPHQ1f6YWAFmPtDSIiIiQ5cLS0tODYsWMoLCyMWEODUqfe3YYOPbrXIVm9\nG8LbAXPNBzHDhhQC/pLzoBeV8xYKERH1qNeBw+fzYceOHfjoo4+watWqiNU5HQ4H7r33Xrz++usw\nOpeznjRpElatWoVLL700+bUmAIDf0FETo3ejyGxDvik7CW/ghan6HxA+T9RT0mQJ3EKxJXFAKhER\nDVu9Wvjr+PHjWLp0KdasWYOtW7dGbOeu6zp++MMfYufOndB1HVJKSClx+PBh/OQnP8GOHTtSVvmR\nrtbdAq8R2fMgBDDJVjzwkxs6zEc/hOKO3treyLLDc95chg0iIuq1hIFD13XcdNNNqKmpgdlsxty5\nc2G1WkPP//d//zc++eQTAMDFF1+M119/Hfv27cN9990HRVFw3333oaEheot0GhhdGqiNscjXWZZc\n5Ax0zQ0pYar9BIqjJfopkxXe8llAktf1ICKi4S1h4HjllVdw+PBhTJw4Edu2bcPTTz+NkpKS0POb\nN28GENgAbcOGDSgvL4fNZsP3v/993HnnnXA6ndi6dWvqPsEI1eBphy+qd0MkZUVR7cRBqK0no8ql\nqsE7cRZgtsZ4FRERUXwJA8dbb70FIQT+67/+K2qTs4MHD6K+vh5CCCxdujS002vQ97//fWiahl27\ndiW10hQYLNrdGIsd2erAZoqop4/GXNRLCgW+sulpsXIoERFlnoSB47PPPkNxcXFoWfFw7733Xuj3\nWINDbTYbysrKUFdXN8BqUjiH34NWX0dU+dgBLvKltJ2CVh97RVhf6RQY9qIBnZ+IiEauhIGjpaUl\n4hZKuH379gVOoii46KKLYh5js9ngcDgGUEXq7rg7euxGjmZBntb/Wx3C2QrTsY8gYix07xtzLowC\nbsBGRET9lzBwCCGgx1nw6f3334cQAhUVFRHTZMO1tLQgNzd3YLWkEL80cNITvTvrOGt+v9fdEB4n\nzDX7IDqnNEe836jx0Eef3a/zEhERBSUMHMXFxTh+/HhU+aeffoqWlsAshjlz5sR87enTp1FXV4ei\nInbFJ0uDpx3+bsFAFQJnWfo5tsLvgal6X8wly/XcYvjHXcBFvYiIaMASBo7Zs2ejtbUV//d//xdR\nvn379tDvl112WczXvvDCC5BSxr3dQn0X63bKWZZcmBS17yczdJirP4TicUU/lZ0L34RpgOjVUi1E\nREQ9StiaXH311ZBS4uc//zn27NmD9vZ27NixA//zP/8DIQTOO+88TJ8+Pep17733Hp588kkIIbBw\n4cKUVH6kafe7ccbnjiof15/BolLCdOxjKK7oACPNWYG1NtJke3kiIsp8CVuUiy66CN/97nfx0ksv\n4cYbbwyVSymhaRruu+++iOO3b9+Ot99+G2+++SYMw8A3v/lNXHzxxcmv+Qh0PMZU2FyTFbmmvg8W\nVVuOQ207FVUuNVNgrQ0TF/YiIqLk6dVX2Pvvvx9nnXUWnnvuOTidgaWux44di/vuuy+qd2P9+vU4\nfvw4pJSYPn06HnrooeTXegTyGzpOuqMHi4619mM7eL8H2omDUcVSUeAtmwFpjT0AmIiIqL96FTiE\nEPjpT3+KFStWoLq6GiaTCWeffTYUJfqOzDnnnIOSkhJceeWVuPrqq2EymZJe6ZHolKcduowcLKop\nCs6y9H0GkKm+KuYgUV/pVyBzCvtdRyIionj6dJPearVi8uTJPR7z+9//fkAVomhSypiDRcdYcqH1\ncVCn0t4IteVEVLleMBZGfuz1VoiIiAaKUxAyQLvfjXZ/9Bbxfb6dovthqvs8qlhqJvjGnt/f6hER\nESWUsIfDiLEYVH/Euv1CvRNrsGieyQp7H1cW1RqOQHijl0T3l1Rw91ciIkqphIFjypQpA34TIQQ+\n/zz6mzUl5jN0nIqzsmhfCFcb1Bibsuk5hdALxva3ekRERL2SMHBIGWNzDRo0pzxnoHe7BpqiYHRf\nVhaVBkzHP4Podh6pKPCPn8KVRImIKOV6PUsFACZPnowlS5bgK1/5SkorRV0avdEb35VYcqH2YbCo\nevoYFFd0L4l/9CRIi21A9SMiIuqNhIHj0Ucfxc6dO/HOO+/g888/x4EDBzBhwgRcccUVuOKKK3Du\nuecORj1HJENKtMXYhr6kL4NFvR3QTn0RfW5rDvTiiQOpHhERUa8J2ct7Ji6XC3/5y1+wc+dO/O1v\nf4PX64UQAueccw6WLFmCK664AhMmTEh1fYdMY2P7oL9nq68D+1qPRZSZFBXfKJzUu51hpYSp5gOo\nZxojiwXgnfRVSFtBMqubtoqL7UNy/Sg5eP0yF69dZisu7uemoHH0OnCEczgcqKysxJ/+9Cfs3bsX\nfr8fQghceOGFWLJkCRYvXoyzzjorqRUdakPxl6bG1YQjztMRZcWWHEzNHder1yutJ2A++lFUuX9U\naWDsxgjBf/QyG69f5uK1y2xpETjCtbW14Y033sDOnTvx97//HbquQ1EUzJgxA0uWLMGiRYtQWJj5\nq1cOxV+a/W11aPI6I8rOyxmN0qxe9Ez4vbAc2gPhi1y/Q5os8Jx/CaCOnBVg+Y9eZuP1y1y8dpkt\n7QJHuObmZrz++ut47bXXsG/fPkgpoaoqvvrVr+Lpp59O1tsMicH+S2NIib82HY5azvzigrJerb+h\n1X0KrakuqtxbPgNG3vDqfUqE/+hlNl6/zMVrl9mSHTiSuhpXYWEhrrvuOjz++ONYvXo1srOz4ff7\n8e677ybzbUYEh98dc+8Um5p4gS7hbIkZNvS80SMubBARUXro014qPeno6MDbb7+N119/Hbt374bb\n7YaUEoqi4KKLLkrW24wYLTFmp+SbsqEkGixq6DDVfRZVLFUNvrEXJKt6REREfTKgwOFyuSJChsfj\nCYWMWbNmYfHixVi4cCGKioqSVd8Ro9Xniior0LISvk5trIbijl67wz/mXMDct6XQiYiIkqXPgcPp\ndIZCxt/+9rdQyBBCYMaMGaGQMXr06FTUd0QwpESrP3YPR0+Exwnt1JfR58vOhz6qNGn1IyIi6qte\nBQ6n04m//OUvoZDh9XpDIWPatGlYvHgxFi1aNOymwg4Vp+6B34gev5GTYIM1reFLiG7jPqQQ8I2f\nAvRxG3siIqJkShg4brnlFuzZsycUMgBg6tSpoZBRUlKS8kqONC0xbqfkaVk9j9/wdkBpqY8q1osn\nQmYld6QxERFRXyUMHH/5y18CB2oavva1r2Hx4sUYOzawu2hNTQ1qamp69UZz5szpfy1HmNaYA0Z7\nHr+hna6J3pxNM8E/+uyk1o2IiKg/er15m67r2LNnD/bs2dPnN+nP9vQvvvginn76aZw8eRKTJ0/G\nXXfdhenTp8c8dsGCBaivj/52DwC33347br31VgDA0qVL8cUXkfuKFBQUYO/evX2qWypJKWMGjoKe\nxgUhGeoAACAASURBVG/4vVBjTIP1F5UBatImIhEREfVbr1qjga4N1tfXv/zyy1i7di1uu+02XHjh\nhdiyZQtuvPFGbN++HePGRS/rvXHjRni93oiyZ555Brt378aSJUsAAD6fD9XV1Vi5ciVmz54dOk7T\n0qtBdupe+Aw9okwVSo+LfWmnj0F0e41UVeijhu/eNkRElFkStrZVVVWDUY8I69evx7Jly3DLLbcA\nAObOnYtFixZh06ZNuPvuu6OOr6ioiHj8ySefoLKyEvfffz/Ky8sBAEeOHIGu6/jWt74VKktHMcdv\nmKzxx2/ofqhNR6OLC0sBzZzs6hEREfVL2k1dOHr0KOrr6zF//vxQmaZpmDdvHnbv3t2rczzwwAOY\nNm0arr766lBZVVUVrFYrysrKkl7nZIo9fiP+7RS1uQ7C74sok0KBv6g82VUjIiLqt5QHDp/Ph9/8\n5je9Pr6mpgZCiKhgMH78eNTW1ia8PVNZWYmPPvoIv/jFLyLKDx48iNzcXNxxxx2YNWsWLrroIqxZ\nswZOpzPOmQZfYPxGjAW/4g0YNQxop2uiivWCEi7yRUREaaVPgePYsWOorKxEZWUlTp06lfD4ffv2\n4aqrrsLjjz/e6/dwOAKrZNpstohym80GwzDgckU3yOGee+45zJo1C1OnTo0oP3ToEJqamnDBBRfg\niSeewJ133ok33ngDt912W6/rlmou3Qtvt7EYihBxx2+orScgvO6IMikCU2GJiIjSSa9GTJ46dQqr\nV6+OmM2hKAq++93vYs2aNTCbI8cKOJ1O/PrXv8aLL74IwzAgEu3/ESbYgxHvNYoSPyNVV1fj/fff\nx/r166OeW7lyJbxebyiIzJo1CwUFBfjZz36Gffv2YdasWb2uY6rE2j8lz5QFNdaiXVJCbayOKjZy\nz4K05qSiekRERP2WMHC0t7fjmmuuQWNjY8TtDF3XsXXrVjidTjz88MOh8vfeew933XUXTp06BSkl\nzGYzbr755l5XyG4PLFLldDpRWFgYKnc6nVBVFVlZ8dejqKyshM1mwze/+c2o57oPLAWASy+9FFJK\nVFVVJQwcyd6mN5ZjjS2wycjwVp43CsX50e8t25thqF7AFnm8UnEhhJ0LfXU3GNePUofXL3Px2lFQ\nwsDx9NNPo6GhAZqmYcWKFZg/fz4URcGf/vQnPPfcc9i5cyeuv/56TJs2Dc888wwefvhhGIYBKSVm\nz56NX/7yl5g4sfdd/GVlZZBSora2FqWlXft/1NXVJZxd8re//Q2XXnppVI+LruvYsWMHKioqMHny\n5FC52x24HVFQUJCwXo2N7b3+DP0hpcSx5mZ4DH9EudCARl/0e2t1h6A5I6cCG7YCeN0mwJ3aumaa\n4mJ7yq8fpQ6vX+bitctsyQ6LCQPH7t27IYTAunXrsHTp0lD5lClTMGbMGDz44IP405/+hE8//RS/\n+tWvAAR6KVatWoVrrrmmzxUqLy9HSUkJKisrMXfuXACBgae7du2KmLkSy6efforbb789qlxVVaxf\nvx6TJ0/Ghg0bQuV//vOfYTKZMGPGjD7XM9k6DF9U2FCEQK4pxvgNQ4faeiKqWC+MXqOEiIgoHSQM\nHHV1dcjNzY0IG0HLli3DQw89hL/+9a9oaGgAAHz961/Hgw8+OKCN3JYvX477778fdrsdM2fOxJYt\nW9Da2oobbrgBAFBbW4vm5mZMmzYt9Jrjx4/D6XTG7U25+eabce+99+KBBx7AggUL8PHHH2Pjxo24\n/vrr02I/mFjrb+Rq1pjjN5S2UxB6ZDiRqgo9b0zK6kdERDQQCQOH0+mMuA0Rzmw2o6ysDF988QWE\nELjtttuSMuvjuuuug9frxebNm7F582ZUVFTgmWeewfjx4wEEVhbdtm0bDhw4EHpNU1MThBDIzc2N\nec5rr70WZrMZzz77LLZu3YqioiLceuutWLFixYDrmwx9WX9DbTkeVabnjeEy5kRElLaETLCwRUVF\nBWbNmoU//OEPMZ9ftmwZPvroIyxbtgz33ntvSiqZDlJ5H1JKiXdbvoS7W6/FjLzxKDRHTg+GtwOW\nqncgul01z6SLIXMKQdF4Hzmz8fplLl67zJbsMRwDXvgrOE31xz/+8YArM1K5DV9U2BBCIDfGgl9q\ny/GosGFYsiFtiQe+EhERDZWkrTQavN1BfRdr/Y1czQqt+/gNKWPfTikYB/RhrRMiIqLBlnZ7qYxE\nsZYzz4/Ru6E4W6B4IsOJFIBeMDZldSMiIkoGBo40EGvAaEGMAaNqc11UmZEzCjDHXwyNiIgoHfRq\nWkNTUxO2bdsW9zkAcZ8PCt+5lbq4dR869MjdXoUQyOu+f4ruh9IWvX+NXshbWURElP56NUulL3uh\nxHwTIfD5558P6BxDLVUjrU+42/B5+8mIslyTFbPzI3fLVZtqYar7LKJMqho8F8wHFDUldRsuOFI+\ns/H6ZS5eu8w26CuNAki4JXyqXz+cxV5/I9bslPqoMj2/hGGDiIgyQsLAUVVVNRj1GLFiDRjtPn5D\neJxQnC1Rx3EpcyIiyhQcNDqEPLofrqjxG0CeFtnDoTZHT4U1rDmQWXkprR8REVGyMHAMoVj7p+So\nFpjCb5NII/btlMLxXHuDiIgyBgPHEGr1J94/RXE0QfjcEWVSCOgFQ7/hHBERUW8xcAyh2OM3ut1O\naT0ZdYyRWwxolpTVi4iIKNkYOIaIx/DD6fdGled1CxyKoznqGL2Ag0WJiCizMHAMkVjTYXM0C8xK\n2MQhrxvC230pcxFYXZSIiCiDMHAMkd7sn6K4oqfCyiw7oPZq+RQiIvr/2bvv8Kiq9IHj3ynphSQQ\nIBAgNAmYBaRKJAmEFlmKIiCKuyoKojQbXTT+FMWCNGEXBEJdlN4ElSIdpIishRBaMJTQSU+m3d8f\n2Qy5mQkEnCST8H6eJ8/DnLn33HfmMJk3p13hNCThKCVFuX+Kvb03LHIbeiGEEGWQJBylwGAxkW7K\nsSm36eGwl3B4SsIhhBCi7JGEoxTY693w0ruq52+YjWiybe9BID0cQgghyiJJOEqB/fkbBYdTbqEp\ncAsai5snuMhyWCGEEGWPJBylIMWUbVNWcP8Nmb8hhBCiPJGEoxRkF7h/CoCvvggrVCThEEIIUUZJ\nwlHCLIqCwWJWlWk04JZ//obFjCYzxfZcSTiEEEKUUZJwlDCDxWRT5qLRoc13IzZNVioai0V1jKJ3\nRXH1LHiqEEIIUSZIwlHCcuwkHKreDQqZv+HtL3eHFUIIUWZJwlHC7CUcrkVJOGT/DSGEEGWYJBwl\nrOD8DSjQw6EoskJFCCFEuSMJRwm7Ww+HJicdjVl9jKLT5d5DRQghhCijJOEoYfYmjbppddZ/a9Pt\nDaf4gUaaSgghRNkl32Il7G49HPb235DhFCGEEGWdJBwlzF4PhyrhsDN/Qzb8EkIIUdZJwlHC7jhp\n1JCFxqDe9lzRaLB4VCiJ0IQQQohiIwlHCbIoCgbFXg9H7hwOu70bHr6g09uUCyGEEGWJJBwlyGAx\noRS4A6yLVofufxNCZTmsEEKI8koSjhJ0tz04JOEQQghRXknCUYLsr1D535JYkwFtdrrN8xYvv+IO\nSwghhCh2knCUIHvzN/J6OLSZt2yes7h7gd6t2OMSQgghipskHCUox1z4kli5f4oQQojyzGkTjuXL\nl9OlSxeaNGlCv379+OWXXwo9Njo6mtDQULs/M2fOtB53+PBh+vbtS9OmTenSpQurVq0qiZdiZX+X\n0cITDtl/QwghRHnhlOst16xZQ2xsLEOHDiUsLIwlS5bw8ssvs27dOqpXr25z/KxZszAYDKqy+fPn\ns3v3bv7+978DcPr0aQYOHEh0dDTDhw9nz549jB8/Hh8fHzp37lwirytHsZ006qrVg8WMJjPV5jmZ\nvyGEEKK8cMqEY8aMGfTr14/XXnsNgPDwcGJiYliwYAHjx4+3OT40NFT1+Ndff2Xr1q18+OGHhISE\nADBnzhyCg4OZPHkyAG3btuXGjRvMnDmzxBKOwu6joslMQaNYVOWKixuKq2eJxCWEEEIUN6cbUjl3\n7hwXL16kffv21jK9Xk+7du3YvXt3keqYOHEiTZo04YknnrCW7d+/n3bt2qmO69ixIwkJCVy9etUh\nsd9NYXM4Cr1/ikZTEmEJIYQQxc7pejgSExPRaDTUqlVLVR4cHExSUhKKoqC5wxfx1q1bOXbsGN98\n8421LCsriytXrlCzZk3VsTVq1EBRFBITEwkMDHTsCykgd5dR+/twaDPsrFCR+RtCCCHKEafr4UhP\nz92LwsvLS1Xu5eWFxWIhMzPzjucvXLiQ5s2b07hx4yLVmf/54mRUzCgFthnVa7XoKGyFiszfEEII\nUX44XcKR96VcWC+GVlt4yGfPnuXQoUM8//zzDqvTUQpboaLJTkdTYKhF0elRPHyKPSYhhBCipDjd\nkIqPT+4XbUZGBgEBAdbyjIwMdDodHh4ehZ67detWvLy8iIqKUpV7e3tb68gv73He83cSGPjXEgBL\nloKXwVVVVsndmwAlB8VLXa6pUBnfynKHWEf6q+0nSpe0X9klbSfyOF3CUatWLRRFISkpiRo1aljL\nz58/b11xUpg9e/YQGRmJq6v6C9zT05PAwECSkpJU5UlJSWg0GmrXrn3XuK5eTSv6i7AjOTuFjAz1\n0l0fk5HUm1fQFSg3+nhg/ovXE7cFBvr85fYTpUfar+yStivbHJ0sOt2QSkhICEFBQWzdutVaZjQa\n2bFjB23atLnjub/99htNmjSx+1ybNm348ccfVfMotmzZQv369VU9KcUlx86N21y1OtnwSwghxAPB\n6Xo4AAYOHMiHH36Ij48PzZo1Y8mSJdy6dcs6NyMpKYkbN26okosLFy6QkZFRaG/FgAED6N27N8OH\nD6dPnz7s3buXjRs3Mn369BJ5TfbmcHiYzWiMOaoyRaPF4inDKUIIIcoXp0w4nn32WQwGA4sWLWLR\nokWEhoYyf/58goODgdydRdeuXcvx48et51y/fh2NRoOvr6/dOkNDQ5k9ezaff/45w4YNIygoiI8/\n/phOnTqVyGuyd6dYryzb1TGKhy/k3UFWCCGEKCc0SsG1msKuvzoOefjWOVKM2aqyxzLS8UtRbzpm\nCgzBVE29c6r4a2QcuWyT9iu7pO3KtnI/h6O8stfD4Z5l+0GUDb+EEEKUR5JwlABFUTAUmDSqMxtx\nzbHdxEwSDiGEEOWRJBwlwKiYsRQYufLOzkSrUb/9Fndv0KuX9AohhBDlgSQcJaBg7wZABbu9G7Kd\nuRBCiPJJEo4SYG/+hndOhk2ZDKcIIYQoryThKAEF9+DQWMx45mTZHKd4SsIhhBCifJKEowQU7OHw\nyMlAj/pGcoqLO4pr4feJEUIIIcoySThKQMEeDs+sdHQFJ4x6+UEhd7MVQgghyjpJOEpAwfuoeGSn\no7dJOGQ4RQghRPklCUcJKNjD4W7IQlegN8PiKStUhBBClF+ScJQA1RwORUFnMtoMqShuXiUclRBC\nCFFyJOEoZoqiqBIOndmEBkWVcCg6Peic8j56QgghhENIwlHMTIpFtcuoi9mABg3afEMqiotbaYQm\nhBBClBhJOIpZwfkbepMRvUaLhvwJh3tJhyWEEEKUKEk4ilnBFSp6O/M30EsPhxBCiPJNEo5iZtPD\nYTbYThiVIRUhhBDlnCQcxazgLqMuJiN6je0uo0IIIUR5JglHMbM3h8Omh0OGVIQQQpRzknAUs4I9\nHDKkIoQQ4kEkCUcxszekYjNpVIZUhBBClHOScBQzQ/5VKooFndlocx8VRe9awlEJIYQQJUsSjmKW\nv4dDbzICqHcZ1buAVlficQkhhBAlSRKOYmSymDErFutjF7PRzi6jMpwihBCi/JOEoxgZFHubfmlk\nl1EhhBAPHEk4ilGOueCSWDsrVGRJrBBCiAeAJBzFyKAUXBJrO2EUWRIrhBDiASAJRzEqypJY2YND\nCCHEg0ASjmJks+mX3SEVmcMhhBCi/JOEoxgZCt4p1t4eHNLDIYQQ5YqiKCQnX3JYfUePHiEioiWb\nN290WJ2lQRKOYmTbwyFDKkIIUZ5lZmYwaNALDk0OQkJqM2HCBzRt2sxhdZYGSTiKUf4bt2ksZnQW\nk3rTL40GZJdRIYQoN1JTU4mP/8Ohdfr7B9C5cwxBQdUcWm9Jk4SjGN1tl1H0blBw1YoQQogyS1GU\n0g7Bacm3XTExKxZMloK7jIJOtcuoDKcIIZxH797dGT588F3L09LSmDgxlqee6kZ0dDhPP/0Es2fP\nxGAwqM47ffo0Y8e+TUxMezp2bMurr77EwYMHVMcMG/YKb701nK+++hedOkXSo0cXzpw5XWiMEyfG\nEhHRskjlcXFf8eyzTxEd/Rg9enThgw/e5cqVy6pj0tLSmDLlU558sivR0eE891wfVqz4WnXMvHmz\niY5+jF27dtCzZxc6d47i22/X28Rw9OgR+vbtiUajYf78OURGtiI5OZlNmzYQEdGSnTu306dPTzp2\nbEtc3FcAXLhwng8/fI9evf5O+/Zt6Nq1A6NHv8HZs2dU9eafw5H3+NChn5g8+RO6d+9Mx45tGTHi\nNU6dOlnoe1fa9KUdQHllKGT+hnqXUUk4hCirDBYTf6Qlc9OYicWJ/qrVajT4u3jSyKcqrtp7+xWv\nyfcH0Z3KJ0wYzalTJ+nb9xkCAiry+++/smTJAlJTUxg5chwAp0+fYujQgQQEVOSf/xyAXq9n69bv\nGTlyBO+9N5Ho6I7W+v7731+4ePE8Q4aM4NKli9SuXeeOMdqLs2D5woXzWLBgLr17P02dOvW4dOki\ny5cv48SJeBYv/gaNRkN2djZDhrzM1atX6dWrD4GBlfn558NMnz6Z8+f/5I03RlnrNptNfP75x/Tr\n1x+DwUDjxk1tYggJqcPw4W8xffpkoqLaExUVjb+/nzWuSZM+pHfvp/H09CIs7G/cvHmDQYOex9vb\nh969n8bXtwKnTiWwfv0aTp5MYMWK9eh0ukLb5pNPPiQwMJAXXniZtLRUli5dyKhRr7Ny5Qa0Wufr\nT5CEo5jYTBg1yy6jQpQnf6Qlc92QUdph2LAoCtcNGfyRlkzTCsEOr//mzZscOXKIIUNG0K/fcwB0\n69YTRVG4ePGC9bgpUz6lYsWKzJu3FDe33N91vXs/zfDhg5k27XMiI9uh1+d+BeXkZPPeex8SGtrI\nYXFu2fI9jz4azrBhb1rLKleuwtq1K7l06SLVqlVn6dKFnD9/nnnzFluTnCeeeIrZs4NZunQhPXr0\nom7dekDuUEm/fs/x7LP/KPSa/v7+REREMX36ZOrWrU+nTjGq5zt16sJLL71ifbx06ULS09P597/j\nqFGjprXcw8OTpUsXcubMKerXb2C9fkEVK1Zi1qx51mTExcWF2bNn8vPPh2nRotW9vmXFzvlSoHIi\np8CSWPubfskeHEKUVSmmrNIO4Y6KKz5vb288PDxZvXoFO3duJzs7G4AxYyYwZcpMAFJTUzh27CiR\nkZFkZ2eRknKLlJRbpKWlEhERxc2bN1QTK93c3ByabABUrpzbW7FixdfcvHkDgB49nmT+/KVUq1Yd\ngF27fqROnboEBARYY0xJuUVERBSKorBv325VnU2aPHLf8Wg0Gpvz+/d/nnXrvlclGzk52dYEIjPz\nzm0YFdVe1fNRv34DFEXhxo3r9x1ncXLaHo7ly5czb948kpOTadiwIWPGjKFpU9surDw3btxg0qRJ\n7Ny5E4vFQosWLRg3bhw1atSwHtO9e3dOnlSPb/n7+7N//36Hx287pGKQbc2FKEcq6D2csocjTwW9\nR7HU6+LiwqhR4/jkk4m8885oXFxcadq0Ge3aRRMT83dcXV25cOE8AEuWLGHx4sU2dWg0Gi5fTiYs\nrDEAvr4VVM+bTCZSU1NUZW5ubnh5eRc5ziFDXmfMmDeZMeMLZsz4ggYNQnnssUh69HiSgICKQO78\nCYPBQLdunQqNMT9/f/8iX98ef/8AmzKj0cCcObNISIjn/PnzXLp0AYvFgkajQcl3t3F7/PzU8bi4\nuABgNpvtHV7qnDLhWLNmDbGxsQwdOpSwsDCWLFnCyy+/zLp166hevbrN8SaTiRdffBGj0cjEiRPR\naDRMmTKFgQMHsnHjRvR6PUajkbNnzzJy5Ehatrw9sSivS8/RbIdUZA8OIcqTRj5VnX4Oh6MU/ALr\n2LELrVuHs3v3Dvbt28Phwwc5dOgAa9euZM6chZjNuV+U/fv3p0WLcLt15p+nodXqVM/9+usxm8mr\njz/ejXHj3ityjHXr1mPZstX89NN+9u7dxU8/7WfevNl8/fVS5sxZQM2atbBYLDRu3JQBAwbZHbKo\nVClQ9bhgnPeq4LyKY8eO8tZbw/D09KJly1Y0bdqMBg1COX/+PFOmfHrX+gqbc+OsnDLhmDFjBv36\n9eO1114DIDw8nJiYGBYsWMD48eNtjl+zZg1//vkn3333HVWqVAGgevXqDBo0iISEBBo1asTp06cx\nm8106NCBkJCQYn8N9ieNqt9u2dZciLLLVasvljkSpUmr1WI0qleamM1mUlJuERyc21uclZXFyZMn\nqF27Ll27dqdr1+6YTCZmzZrGypXfcPDgARo0CAVAp9PRvLl65Uhi4lkuXbqAu3vhv//q12/A1Kmz\nVGV5X/55X9omk0n1B2P+YQSLxcKpUwl4eXnz2GMRPPZYBAA//riVd98dy4YNaxkyZARVqwaRmZlB\ns2YtVNdKS0vjyJGD1tdcXObNm427uztLlixX9fIcPz6/WK9bWpxuDse5c+e4ePEi7du3t5bp9Xra\ntWvH7t277Z6zbds2IiIirMkGQGhoKLt27aJRo9xxwfj4eNzd3alVq1bxvoD/UfVwKIr9+6hID4cQ\nwokEBFTkzz/PqZa37tmzU/X4zJnTDBkykG+/XWct0+v11smNOp2OihUrERrakDVr1nDt2jXrcSaT\niY8+ep8JE8ZgMhXe7e/t7U3z5i1VP7VqhVhjBDh58oT1+CtXLvPbb/+1PrZYLAwfPpjp0yer6m3Y\n8GFrjABt20Zy6tRJ9u/fqzpuwYK5TJgw5o7LcwuTlxBZLHceDoHcTcL8/AJUyUZ6ejqbNuUuf3XW\noZH75XQ9HImJiWg0GpvEIDg4mKSkJBRFselGOnHiBD169ODLL7/k66+/JiUlhfDwcGJjYwkKCrIe\n4+vry4gRI9i7dy8ajYaYmBjGjh2Ll5eXw19H/vuoaC1mtIoFff49OLRa0Lk4/LpCCHG/OnbswrRp\nn/Pmm0Pp0qUrSUl/smHDWqpWvb3D5cMPh9GkySPMmfMvkpOTqVevHpcvX2bVquXUqhVi7dEYMWIk\nb7zxGi+99BxPPtmbChX82LLlO+Lj/2Dw4KH4+vreV4wdOnRmyZIFvPvuOJ5++hlycnJYvXoFlStX\nISnpTyA3AerT5xkWLpzHuHEjad26DdnZWWzYsBZ3dw+6du0OwHPPvciOHdsZP34UPXv2onbtOhw7\ndpQffthMmzZtadPmsXuOr0KFCmi1Wvbs2UmVKlWIiuoA2F9l8uij4fznP4t4992xtGrVmmvXrvHt\nt+utk1wzM513jtD9cLqEIz09HcAmCfDy8sJisZCZmWnz3I0bN1i1ahXBwcF89NFHZGZm8tlnn/HK\nK6+wdu1atFotCQkJXL9+nUaNGvH8888THx/PtGnTuHDhAnFxcQ5/HfmHVPRm211GFRc3KGPjb0KI\n8q1Xrz6kpaWyceM6pk79jHr1HuLjjz9n2bLFZGXdXjHx8ceTiYubw969u9mwYQ0+Pr60b9+Bl18e\nbB3mCAv7G8uWLePzz7/gm2/+g8lkombNWowfH0uXLl1V172XX4V169bjgw8mERc3l1mzZlC5chX+\n8Y8XycrK4l//mm497qWXXsHHx5dvv13PzJnT0Ol0NG7chHff/YCaNXP/oPX19WX27AXMm/dvduzY\nxvr1q6lSpSovvjiQ/v2fv6/30M3NnUGDXmPZssVMmzaZ4OCa/3uNti9ywIBBWCwWtm37gX37dlOp\nUiCtWj1Kv37P8dxzfThy5DAREe3snl/UPVOciUZxsn1YN27cyMiRI9m7dy8BAbdn9K5YsYJ3332X\nn3/+GQ8P9ezrhx9+GB8fH7Zu3Yq3d+4s5t9++43evXszdepUYmJiiI+P/99mLY2t523atIm33nqL\nJUuW0Lx58zvGdfVqWpFfg0VR+PFagvWxV2YqtS6eIMSzonXjL4uXP4Z6rYtcp7h/gYE+99R+wrlI\n+5Vd0nZlW2Cgj0Prc7o5HD4+uS8wI0PdlZSRkYFOp7NJNgA8PT1p0qSJNdkACAsLw9fXl4SE3C/+\n0NBQVbIBEBkZiaIoxMfHO/Q12FsSq5VdRoUQQjzAnG5IpVatWiiKQlJSkmoPjfPnzxe6uqRmzZoY\njUabcpPJ9L8tac2sX7+e0NBQGjZsaH0+b8OaoqytvpdM72ZOJl45t+8C65Ol4OnmiqfX7TJNJX+0\nDs4eReEcnamLkiXtV3ZJ24k8TpdwhISEEBQUxNatWwkPz12/bTQa2bFjh2rlSn5t27Zl4cKFXL16\nlcDA3KVTBw8eJDMzk2bNmqHT6ZgxYwYNGzZk5syZ1vO+//57XFxceOSRu+8edy/dgldy0sjIuD2r\n2yc9E7PRQma+MmOGGbN0NZYI6dYt26T9yi5pu7LN0cmiLjY2NtahNTqAq6srs2bNwmAwYDAY+Pjj\nj0lMTGTSpEn4+vqSlJREYmIiVavmbmzToEEDVq1axbZt26hUqRK///47sbGxhIaG8vrrrwPg4eFB\nXFwcKSkpuLi4sGnTJqZOnco//vEPunTpcteYMjMNdz0mz01jlmoHQr/Ua1Qwm/HS3e7hMAdUR/GQ\nzL8keHm53VP7Ceci7Vd2SduVbV5ejh36d7oeDoBnn30Wg8HAokWLWLRoEaGhocyfP5/g4NxNdmbN\nmsXatWs5fvw4AAEBASxbtoxPPvmE0aNHo9fr6dChA+PGjbPW2bdvX1xdXYmLi2PFihVUqlSJjSxA\nGgAAIABJREFUIUOGMGjQIIfHX5RdRpH7qAghhHiAON0qFWd1L92Cx9OSuZh9+z4A9RKPEaR1wTff\nvQ1yQiNQ3By//4ewJd26ZZu0X9klbVe2lftVKuWB7S6jdu6jIremF0II8QCRhKMY5F8WqzOb0KCo\nN/3S6UHnlKNZQgghRLGQhKMY5N/WvNBdRoUQQogHiCQcDmZRFAzK7R4OF1PuDG1VwiHDKUIIIR4w\nknA4mMFiIv803Lz5G9p8u4zKChUhhBAPGkk4HCz/cAqA3iy3pRdCCCEk4XCwgntwuJiM6ArcvU+G\nVIQQovxSFIXk5EvFeo2LFy8Ua/3FQRIOB7O9cZsRvU0PhwypCCFEeZSZmcGgQS+wefPGYrvGp59O\n5JNPPiy2+ouLJBwOVpRdRmVIRQghyqfU1FTi4/8o1mscOvQTZXHPTkk4HMzerellW3MhhHgwlMVE\noKRIwuFgOUq+SaOKBb3ZZGeXUVeEEMLZ9O7dneHDB9+1PC0tjYkTY3nqqW5ER4fz9NNPMHv2TAwG\n9Y3aTp8+zdixbxMT056OHdvy6qsvcfDgAdUxw4a9wltvDeerr/5Fp06R9OjRhTNnThca48SJsURE\ntCxSeVzcVzz77FNERz9Gjx5d+OCDd7ly5bLqmLS0NKZM+ZQnn+xKdHQ4zz3XhxUrvlYdM2/ebKKj\nH2PXrh307NmFzp2j+Pbb9TYxHD16hL59e6LRaJg/fw6Rka1ITk4GwGAwMGfOLPr06Un79m3o27cn\n8+bNxmRS/5G6Y8c2Bg78J507RxET04433hjCr78esz4fEdGSy5eTOXr0CJGRrYp16MbRZLtLB8vf\nw6E3mwBFNYdD0buAVlcKkQkhHCndlMPqS79wJvMaJsVS2uFY6TVa6nhWoldQU7zvcYK6psAE98LK\nJ0wYzalTJ+nb9xkCAiry+++/smTJAlJTUxg5MvemmadPn2Lo0IEEBFTkn/8cgF6vZ+vW7xk5cgTv\nvTeR6OiO1vr++99fuHjxPEOGjODSpYvUrl3njjHai7Ng+cKF81iwYC69ez9NnTr1uHTpIsuXL+PE\niXgWL/4GjUZDdnY2Q4a8zNWrV+nVqw+BgZX5+efDTJ8+mfPn/+SNN0ZZ6zabTXz++cf069cfg8FA\n48ZNbWIICanD8OFvMX36ZKKi2hMVFY2/vx8Wi4WRI1/n99//S8+evahZM4T4+OMsWjSfkydPMGnS\nF0BuwvLee+MID4+ge/cnyc7OYuXKb3j99ddYsmQFQUHVmDDhA6ZPn4yfnz/PPz+AsLDGhb5XzkYS\nDgfLMd9l0y8ZThGiXFh96RcSMq6Udhg2TIqFhIwrrL70C/+s0drh9d+8eZMjRw4xZMgI+vV7DoBu\n3XqiKIpq5cSUKZ9SsWJF5s1biptbbuLTu/fTDB8+mGnTPicysh16fe5XUE5ONu+99yGhoY0cFueW\nLd/z6KPhDBv2prWscuUqrF27kkuXLlKtWnWWLl3I+fPnmTdvsTXJeeKJp5g9O5ilSxfSo0cv6tat\nB+QOlfTr9xzPPvuPQq/p7+9PREQU06dPpm7d+nTqFAPApk0bOHr0MJMnz6Bly9w26dkTGjV6mE8/\nnciePbto2zaS7du34uHhwccff26ts0WL1kyYMJqEhHiCgqrRuXMMX301i4CAAGv9ZYUMqThQ7i6j\n+bY1N9nZ1lyWxApRLvyZdbO0Q7ij4orP29sbDw9PVq9ewc6d28nOzgZgzJgJTJkyE4DU1BSOHTtK\nZGQk2dlZpKTcIiXlFmlpqURERHHz5g3VxEo3NzeHJhsAlSvn9lasWPE1N2/eAKBHjyeZP38p1apV\nB2DXrh+pU6cuAQEB1hhTUm4RERGFoijs27dbVWeTJo/cVyw7d27Hz8+fhx5qoLpO69Zt0Gq11utU\nrlyZjIwMpk79jHPnEgGoU6cuS5euJCoq+j7fCechPRwOZFTMqglDepMBrUaj2mVUVqgIUT7U9PB3\nyh6OPDU9/IulXhcXF0aNGscnn0zknXdG4+LiStOmzWjXLpqYmL/j6urKhQvnAViyZAmLFy+2qUOj\n0XD5crJ1OMDXt4LqeZPJRGpqiqrMzc0NLy/vIsc5ZMjrjBnzJjNmfMGMGV/QoEEojz0WSY8eTxIQ\nUBGACxfOYzAY6NatU6Ex5ufvf3/v6YULF7h16+YdrpM7r+Spp/py8OABVq9ewapVywkKqkZ4eATd\nuvWkXr3693VtZyIJhwPZrFAx2+7BIStUhCgfegU1dfo5HI5iNqt3UO7YsQutW4eze/cO9u3bw+HD\nBzl06ABr165kzpyFmM2570f//v1p0SLcbp3552loC8xr+/XXYzaTVx9/vBvjxr1X5Bjr1q3HsmWr\n+emn/ezdu4ufftrPvHmz+frrpcyZs4CaNWthsVho3LgpAwYMsru6pFKlQNXjgnEWlcViJji4Bm+/\nPdbudXx8fADw9PRixozZ/PHHb+zevZMDB/ayevVy1qxZwYQJ/0fHjl3u6/rOQhIOB7K/y6jswSFE\neeStdyuWORKlSavVYjSqV5qYzWZSUm4RHFwDgKysLE6ePEHt2nXp2rU7Xbt2x2QyMWvWNFau/IaD\nBw/QoEEoADqdjubN1StHEhPPcunSBdzdC//jq379BkydOktVlvflr9Xm/k41mUzWOSAAN25ct/7b\nYrFw6lQCXl7ePPZYBI89FgHAjz9u5d13x7Jhw1qGDBlB1apBZGZm0KxZC9W10tLSOHLkoPU1/1VV\nq1YjIeG4zXVMJhM7d/5I5cpVAEhK+pOMjHQaNQqjUaMwXnllCOfOJTJkyMt8/fXSMp9wyBwOB7LX\nw2G7JFZ6OIQQzikgoCJ//nlOtbx1z56dqsdnzpxmyJCBfPvtOmuZXq+nfv0GQG6SUbFiJUJDG7Jm\nzRquXbtmPc5kMvHRR+8zYcIYTCZ1j0R+3t7eNG/eUvVTq1aINUaAkydPWI+/cuUyv/32X+tji8XC\n8OGDmT59sqrehg0ftsYI0LZtJKdOnWT//r2q4xYsmMuECWPuuDy3MHkJkcVyu9erbdtIUlJSWL16\nherYtWtXEhs7jsOHDwIwdernjB79pnVeDEDNmrXw9vax1pt3jfz1lxW62NjY2NIOoizIzDTc9Zhr\nhgxuGjOtjyvevIQPGjx1t/fdMAWGyLBKCfPycitS+wnnJO1XcnJycti1awe//PIzZrOZbdt+YP78\nOQQEVMLHx4fHH+9mnYz53XebuHnzJteuXWH37p0sWDCPoKBqvPrqcLRaLXXq1GPTpvVs3ryRnJxs\nzp49w8yZU/njj98YOPBVWrRoBcDmzRtJT0+nb99nihSjn58/69at4tChg2i1Go4ePcLnn39MhQp+\npKam8OKLA9FqteTk5LBx4zpOnkwgPT2dY8d+5ssvp5CTk8Pbb4+hQgU/GjRoxPbtW9i4cR23bt3i\nypXLLF++jPXr19CmTVsGDBgI5C5X/eWXn+nT5xm8ve88j0Sv17NkyULS0lLR63VUqxZMo0Zh/PTT\nfjZtWs/ly8lcv36dTZvWs3jxAho0CGXEiLfR6/UEBPizfv0a9u/fQ06OgZMnE/jqq39x4sRxBg8e\nZl0xs3Xr95w9ewZvb2/c3T3w8yue+TpeXo7tkZeEo4iK8gvvSk4aqabbmWng9Qv46Fxw17pYy0xV\n64FORrJKknxhlW3SfiWnYcNGaLVafv75MDt2bMNoNDJmzAQuXEjCZDLx+OPdAIiIaEd2dib79u1h\n+/YtJCaeJTKyHePHx+Ll5QXkLkHt2rUzJ0+eYtu2Lfz00368vb159dVhPPlkb+s1N2/eSEZGOn36\nFC3hCAgIoE6duvz++29s3foDFy6cp3//f1KnTj0OH/6JF1/MTRKaNWuBl5c3R44cYvv2rfz3v79Q\nv/5DvPPO+9Spk/vF7ebmRseOMaSnp7J79w527fqRrKwsnnqqrzUJgHtPOFxdXf83d2Q3LVq0onr1\nYDp1isFoNLJv315+/HEb169fJyamK6NHT7C+Z9WrBxMa2ojjx39n585tHDiwFy8vL4YOfYOOHTtb\nr1GlSlUOHz7Ejh3bCQgIuO/VM3fj6IRDo8g+rEVy9WraXY/5b+oFruakA6CxmAk98zOV3Xzw1uU2\nmqKBnL91hoITSUWxCgz0KVL7Ceck7Vd2SduVbYGBPg6tT775HEi1y2jeHhz532K9myQbQgghHkjy\n7edA+VepuJhzEw69Nv8uo7JCRQghxINJEg4HURQFg8XOLqP5N/2SFSpCCCEeUJJwOIhRMWPJv8uo\n2YAWDVqN9HAIIYQQknA4SP7eDcjt4bDd9Et6OIQQQjyYJOFwkIK7jNpLOJAeDiGEEA8oSTgcpOAu\noy5mg819VGRIRQghxINKEg4HKUoPh0waFUII8aCShMNBVD0cimL/PirSwyGEEOIBJQmHg+TkmzSq\ntZjRWsyqIRVFowWdi71ThRBCiHJPEg4HUe0y+r9Nv3Sa23tw4OIG+R8LIYQQDxBJOBxEtcto3qZf\nsgeHEEIIAUjC4RCKoqgSDr0p986W6oRDJowKIZzbsGGv0KdPT1VZZmYmt27dKqWI1ObPn0NkZCuS\nk5OtZYqikJx8qRSjus3RsRw9eoSIiJZs3rzRYXWWJkk4HECj0ajma+RNGNXmG0KRHg4hhLN7/vmX\nGDHiTevjEyfi6d+/N4mJZ0oxqtvatYvmnXf+D39/PwAyMzMYNOgFp/hCLo5YQkJqM2HCBzRt2sxh\ndZYmSTgcpKq7r/XfepMRH707GtV9VCThEEI4txYtWtG2bZT18Zkzp7h+/VopRqRWp049OneOwc0t\nt8c4NTWV+Pg/SjmqXMURi79/AJ07xxAUVM2h9ZYWfWkHUF7U9QzES+dKijGbGi6e+Luo9+WQIRUh\nRFmj5Ls/lDNypvicKRZn5bQ9HMuXL6dLly40adKEfv368csvv9zx+Bs3bjBq1Chat25Ny5YtefXV\nV0lKSlIdc/jwYfr27UvTpk3p0qULq1atcli8Wo2Gau5+NPSpSoBGp+rdAGRbcyGE0xs6dJB1Dsf8\n+XP4+OP/A2zndly9eoUPPniXbt06ER39GAMG9OeHH75T1TVxYizdu3fn11+PMXjwADp0eIy+fXuy\nefNGTCYT//73l/To0YXHH4/mvffGkpqaetf45s2bTURES5KTkzl69Ah9+/ZEo9HYzO0wGAzMmTOL\nPn160r59G/r27cm8ebMxmW7/Ibhp0wYiIlpy6tRJ3nlnFJ07R9GtWydmzZqGxWJh8+aNPPvsU3Tq\nFMGrr77EqVMnC42rsFjyrrFz53b69OlJx45tiYv7CoALF87z4Yfv0avX32nfvg1du3Zg9Og3OHv2\njKre/HM48h4fOvQTkyd/QvfunenYsS0jRrx2x/ichVP2cKxZs4bY2FiGDh1KWFgYS5Ys4eWXX2bd\nunVUr17d5niTycSLL76I0Whk4sSJaDQapkyZwsCBA9m4cSN6vZ7Tp08zcOBAoqOjGT58OHv27GH8\n+PH4+PjQuXNnh8avMebYlMmQihDljCkHl6Tf0KZfR2OxlHY0VopWi8W7IsYaYXCPv3c0+eadRUVF\nc/36NTZsWMs//zmAhg0bAXDt2jUGDnwejUZD377P4O3tw549O/nggwlcv36NZ555zlrXlStXGD36\nTbp3f4KYmK4sX76MSZM+YMuW78jIyODFFwdy7txZVq1ajru7B2PHvnvX+PJiDAmpzfDhbzF9+mSi\notoTFRWNv78fFouFkSNf5/ff/0vPnr2oWTOE+PjjLFo0n5MnTzBp0heq1zpq1Os0btyUYcPeYOfO\n7SxbtoTTp09z5swp+vZ9FovFzMKF85kwYTRLl65Eq7X9Oz0kpI7dWPKuMWnSh/Tu/TSenl6Ehf2N\nmzdvMGjQ83h7+9C799P4+lbg1KkE1q9fw8mTCaxYsR6dTmfTJnk++eRDAgMDeeGFl0lLS2Xp0oWM\nGvU6K1dusBufs3DKhGPGjBn069eP1157DYDw8HBiYmJYsGAB48ePtzl+zZo1/Pnnn3z33XdUqVIF\ngOrVqzNo0CASEhJo1KgRc+bMITg4mMmTJwPQtm1bbty4wcyZMx2bcCgK2Es4ZEhFiHLFJek3dKlX\nSzsMGxqLJTeupN8w1m5+3/XUrVuPsLDGbNiwlpYtW1snLs6e/SUmk5HFi5fj7x8AQK9efXj//XeY\nO/dfPP54N/z8cid1pqam8sYbo3jyyd4AVKkSxKhRr5OUlMSyZavQ63O/ghISTnDo0E/3FJ+/fwAR\nEVFMnz6ZunXr06lTDJDbc3H06GEmT55By5atAejZExo1ephPP53Inj27aNs20lpPWFhjYmMnAhAd\n3ZFu3Tpx+PBPLFr0DbVqhQC5K3UWL47j0qWLVK8ebCcWf7ux5OnUqQsvvfSK9fHSpQtJT0/n3/+O\no0aNmtZyDw9Pli5dyJkzp6hfvwFgf6imYsVKzJo1z5qMuLi4MHv2TH7++TAtWrS6p/exJDldKnTu\n3DkuXrxI+/btrWV6vZ527dqxe/duu+ds27aNiIgIa7IBEBoayq5du2jUKDcr379/P+3atVOd17Fj\nRxISErh61YG/NMwGNIr6rx1FpwOdU+Z2Qoj7pM24Wdoh3FFxxKcoCrt376RJk0fQarWkpNyy/kRF\ntcdgMHDo0AHVORER7az/rlmzFgCPPhpuTTYAgoKqOWxy6s6d2/Hz8+ehhxqo4mvdug1arZZ9+25/\nj2g0GiIibk+S9fLyxt8/gBo1almTjbz4ILd3515pNBqaNHlEVda///OsW/e9KtnIycm2JhCZmVl3\nrDMqqr2q56N+/QYoisKNG9fvOb6S5HTfgomJiWg0GmrVqqUqDw4OJikpCUVRbLqYTpw4QY8ePfjy\nyy/5+uuvSUlJITw8nNjYWIKCgsjKyuLKlSvUrFlTdV6NGjVQFIXExEQCAwMdEr/d4RTp3RCi3LF4\n+TtlD0cei5e/w+u8desWGRnp7N69k127dtg8r9FouHw5WVUWEBBg/XfeMIG/vzo2nU5n/UveZDKR\nmpqiet7NzQ0vL+8ixXjhwgVu3bpJt26dConvcoH4KtrEYi8+AEW5v6GzvJ6g/IzG3HkmCQnxnD9/\nnkuXLmCxWNBoNHe9jp+fOj4Xl9zbZpjNZnuHOw2nSzjS09MB8PLyUpV7eXlhsVjIzMy0ee7GjRus\nWrWK4OBgPvroIzIzM/nss8945ZVXWLt27R3rzH9NR5D5G0I8GIw1wsDZ53A4mOV/94xq164DPXv2\nsntMtWrqeXb25hTYm5eQ59dfjzF8+GBV2eOPd2PcuPeKHGNwcA3efnus3eEIHx8f1eO8ZKKo8d2P\ngu/BsWNHeeutYXh6etGyZSuaNm1GgwahnD9/nilTPr1rfY6Or6Q4XcKR9x+ksDfU3n9ek8mEyWRi\n7ty5eHvnZsHBwcH07t2bH374gWbNmt1znfdLY8y2LZQeDiHKH73bX5ojURb5+fnj7u6OyWSiefOW\nqucuX04mISEed3ePv3SNevUeYurUWaqySpWK3gNdtWo1EhKO06xZC1W5yWRi584fqVy5SiFnlpx5\n82bj7u7OkiXL8fWtYC0/fnx+KUZV/Jwu4cjLPjMyMlRdcRkZGeh0Ojw8bP8ze3p60qRJE2uyARAW\nFoavry8JCQlERkZa68gv73H+8woTGOhz12MALDk6FC9XVZmmkh/aIp4vikdR2084J2m/kuHiokOn\n01jf7woVPAHw9XW3lkVFRbFt2zauX79AaGio9dwPPhjPli1b2LhxI4GBPri753bz5287gyF36aun\np6uqPP+xgYE+1Kljf6MrL6/c3uKKFb0IDPTBbM79He7h4WKtLyamE4cOHeCHH9bTv39/67mLFy9m\n4sSJvPvuuzRr9jA+Prl/CPr5eapi0Wo1uLjoVGWFHZufvVgKOy8zM51KlSpRt+7tCahpaWn88MMm\nALy9c98fP7+899/D7uM8hZU7G6dLOGrVqoWiKCQlJVGjRg1r+fnz5wkJCbF7Ts2aNTEajTblJpMJ\nrVaLp6cngYGBNvtyJCUlodFoqF279l3juno1rUjx66/eRJ9hUJUZMyyYi3i+cLzAQJ8it59wPtJ+\nJcdoNGM2K9b3W6fzQFEU4uIWcfr0n3TqFMOLLw7mwIED9O/fnyef7EPVqkHs3bubAwf20rPnU/j6\nVubq1TSys3N/J+dvuxs3cr+UMzMNqnJ7x9qTkZE7ZH39egYuLmmYTDq0Wi3ff/8D3t5+tGvXkfbt\nH2fFilVMnDiRn38+RsOGD3P69EnWr19DaGhDIiM7c/VqGmlp2SiKwq1bmarrWiwKRqNZVVbYsfkV\njCUqqkOh57Vo8Sj/+c8iXn11KK1atebatWt8++16bt68AcClS9e4ejWNW7cyAUhNzbL7OE9h5X+V\no5MXp1ulEhISQlBQEFu3brWWGY1GduzYQZs2beye07ZtW37++WfVapODBw+SmZnJI4/kzg5u06YN\nP/74o2pMb8uWLdSvX1/Vk/JX2Z80KnM4hBBlQ/6R5+bNWxId3YkDB/YydepnGI1GqlcPZvbsBYSH\nR7Bx4zpmzPiC5OSLDBv2Jm++OapAXbbD2Pn30rjbsXfj5ubOoEGvcfXqFaZNm8ypUwm4uLgwffq/\nefrp/hw5cphp0yazf/8+evXqw+TJX+Lmdvv3cWHXvJ/4CsZy+vTJQs8bMGAQ/fo9x++//8rUqZ/z\n3Xff0qrVo8TF/QeNRsORI4cLve69xOxsNIoT7sf6n//8hw8//JCBAwfSrFkzlixZwtGjR1m7dq11\ntcqNGzdo0qQJkDtptEePHlSqVImhQ4eSlZXFZ599Ro0aNVi6dCkA8fHx9O7dm/bt29OnTx/27t3L\nokWLmD59Op062c5mLqioWaNrwj60Weod83LqtUYphhnjomjkL+SyTdqv7JK2K9sc3cPhlAkHwIIF\nC1i0aBE3b94kNDSUsWPH0rhxYwDGjh3L2rVrOX78uPX4pKQkPvnkE/bv349er6dDhw6MGzdONT9j\n7969fP7555w5c4agoCAGDx7ME088UaR4ivqhcfvjR5tejpzQSBQ3zyKdLxxPfumVbdJ+ZZe0Xdn2\nwCQczqZIHxrFgtuvP6Ap8I5m/60TaG2XXomSIb/0yjZpv7JL2q5sK/dzOMo0o8Em2VD0LpJsCCGE\neOBJwuFAGpPtHhyyy6gQQgghCYdDyS6jQgghhH2ScDiQLIkVQggh7JOEw4E0JtuEQ7Y1F0IIISTh\ncCw791GRIRUhhBBCEg6HsnfjNhlSEUIIISThcCiZwyGEEELYJwmHA9mbw6HoZQ6HEEIIIQmHo1jM\naEzqO9YqGsDF1f7xQgghxANEEg4HsTecgt4NNPIWCyHKhmHDXqFPn56qsszMTG7dulVKEanNnz+H\nyMhWJCcnW8sURSE5+VIpRnVbScRy8eKFYq2/OMm3oaPYG06R+RtCiDLk+edfYsSIN62PT5yIp3//\n3iQmninFqG5r1y6ad975P/z9/QDIzMxg0KAX2Lx5YylHVjKxfPrpRD755MNiq7+4ScLhIHZXqMj8\nDSFEGdKiRSvato2yPj5z5hTXr18rxYjU6tSpR+fOMbi55f5uTU1NJT7+j1KOKldJxHLo0E+U5fut\nSsLhILJCRQhR3jj7l5szxedMsTgruT19Ed3tFsv6SyfQXzmrKjNWrYe5Sr3iDEsUgdwiu2yT9is5\nQ4cO4vLly6xYsY758+cQF/cVGo0GRVGoWrUaK1asA+Dq1Sv8+99f8tNP+8nMzCQkJIR+/f5B584x\n1romTozl9OkE3nhjNDNnTuPkyRNUrFiJF18cSKdOMcyd+282bdqA0WikVavWvPXWWHx9fe8Y37x5\ns1mwYC4rVmzg0qULDB8+2BqfRqNh+fL1VK1aFYPBwIIFc9my5XuuXbtCYGBlunTpyvPPv4Rerwdg\n06YNfPzx/xEX9x8WLPiKgwd/wtXVla5duzF48DC+/34TixfHcfXqFerVe4i33hpDvXr17cZ19OiR\nvxQLwI4d21i6dCHnzp1Dq9XQsOHDDBgwiL/9rQkAEREtVfWPHfsujz/e7S+19904+vb0+rsfIorC\n3pAKssuoEOWWJjsNz33L0CefRGMxlXY4VopWj6lqfTLDn0Fxv7cvDI1GY/13VFQ0169fY8OGtfzz\nnwNo2LARANeuXWPgwOfRaDT07fsM3t4+7Nmzkw8+mMD169d45pnnrHVduXKF0aPfpHv3J4iJ6cry\n5cuYNOkDtmz5joyMDF58cSDnzp1l1arluLt7MHbsu3eNLy/GkJDaDB/+FtOnTyYqqj1RUdH4+/th\nsVgYOfJ1fv/9v/Ts2YuaNUOIjz/OokXzOXnyBJMmfaF6raNGvU7jxk0ZNuwNdu7czrJlSzh9+jRn\nzpyib99nsVjMLFw4nwkTRrN06Uq0WtuBgZCQOn8plqNHj/Dee+MID4+ge/cnyc7OYuXKb3j99ddY\nsmQFQUHVmDDhA6ZPn4yfnz/PPz+AsLDG99S2zkASDgexP6QicziEKK889y3D5eLx0g7DhsZiwuXi\ncTz3LSMjetB911O3bj3CwhqzYcNaWrZsTdOmzQCYPftLTCYjixcvx98/AIBevfrw/vvvMHfuv3j8\n8W74+eVO6kxNTeWNN0bx5JO9AahSJYhRo14nKSmJZctWWf/CT0g4waFDP91TfP7+AURERDF9+mTq\n1q1Pp065vSubNm3g6NHDTJ48g5YtWwPQsyc0avQwn346kT17dtG2baS1nrCwxsTGTgQgOroj3bp1\n4vDhn1i06Btq1QoBclfqLF4cx6VLF6lePdhOLP5/KZbt27fi4eHBxx9/bq2zRYvWTJgwmoSEeIKC\nqtG5cwxffTWLgIAAa/1ljczhcBS725pLwiFEeaW7lljaIdxRccSnKAq7d++kSZNH0Gq1pKTcsv5E\nRbXHYDBw6NAB1TkREe2s/65ZsxYAjz4arhpOCAqq5rDJqTt3bsfPz5+HHmqgiq916zYc6GWcAAAR\nrklEQVRotVr27dttPVaj0RARcXuSrJeXN/7+AdSoUcuabOTFB7m9O8URS+XKlcnIyGDq1M84dy4R\ngDp16rJ06UqioqLv851wPtLD4SB2dxmVSaNClFvmSiFonbCHI4+5UojD67x16xYZGens3r2TXbt2\n2Dyv0Wi4fDlZVRYQEGD9t06nA3J7BPLT6XTWSZcmk4nU1BTV825ubnh5eRcpxgsXLnDr1k26detU\nSHyXC8RX0SYWe/EBKIqlSDHcayxPPdWXgwcPsHr1ClatWk5QUDXCwyPo1q1nofNGyiJJOBxBUQCN\nukinA51L6cQjhCh2meHPOP0cDkezWMwAtGvXgZ49e9k9plq16qrH9uY85J8rUtCvvx5j+PDBqrLH\nH+/GuHHvFTnG4OAavP32WLsrR3x81PNa8pKJosZ3L4oai6enFzNmzOaPP35j9+6dHDiwl9Wrl7Nm\nzQomTPg/Onbs4pB4SpskHI6g0WCpUAXdjds7wJn9qoGD/tMKIZyP4u7zl+ZIlEV+fv64u7tjMplo\n3ryl6rnLl5NJSIjH3d3jL12jXr2HmDp1lqqsUqXAIp9ftWo1EhKO06xZC1W5yWRi584fqVy5yl+K\n714UNZakpD/JyEinUaMwGjUK45VXhnDuXCJDhrzM118vLTcJh8zhcBBj9UaYqtTF7FMRU5V6mKo1\nLO2QhBDiL8nrnbBYcocSdDodjz76GPv37+HUqZOqY2fM+ILx40eRkvLXtkH38fGhefOWqp/88ynu\nFB9A27aRpKSksHr1CtWxa9euJDZ2HIcPH/xL8RXmr8QydernjB79JtnZt+cC1qxZC29vH1UPkVar\nVdVf1kgPh6NodZiqlp+xNiGE8PPzR1EU1qxZyfXr1+jUKYbBg4dy9Ohhhg4dyJNP9qFq1SD27t3N\ngQN76dnzKUJCapdYfBUqVECr1bJnz06qVKlCu3Yd6d79CTZv3si0aZ+TkBBPw4YPc/r0SdavX0No\naEO6du1uPf9etqG627F/JZZ+/Z7l7bdH8NprLxET0w1XV1d27drBxYsXGDDgFes1/Pz8OXXqJGvX\nrqRp0+Yl+l47gvRwCCGEsMo/Ety8eUuioztx4MBepk79DKPRSPXqwcyevYDw8Ag2blzHjBlfkJx8\nkWHD3uTNN0cVqMt2WDn/Xhp3O/Zu3NzcGTToNa5evcK0aZM5dSoBFxcXpk//N08/3Z8jRw4zbdpk\n9u/fR69efZg8+Uvc3G5P5i/smvcT31+JpWXLR5k06Qs8PDxZuHAuM2dOJT09jdjYj1Sbqb300iv4\n+PgyffoUdu/ecc/vV2mTnUaLSHY6LLtkp8qyTdqv7JK2K9scvdOo9HAIIYQQothJwiGEEEKIYicJ\nhxBCCCGKnSQcQgghhCh2knAIIYQQothJwiGEEEKIYicJhxBCCCGKnSQcQgghhCh2knAIIYQQothJ\nwiGEEEKIYicJhxBCCCGKnSQcQgghhCh2Tnt7+uXLlzNv3jySk5Np2LAhY8aMoWnTpoUeP3jwYHbs\n2KEq02g0/Pzzz3h4eADQvXt3Tp48qTrG39+f/fv3Ozx+IYQQQtzmlAnHmjVriI2NZejQoYSFhbFk\nyRJefvll1q1bR/Xq1e2ec+LECV544QW6du2qKs9LNoxGI2fPnmXkyJG0bNnS+rxe75RvgRBCCFGu\nOOW37YwZM+jXrx+vvfYaAOHh4cTExLBgwQLGjx9vc3xaWhqXLl0iIiKCxo0b263z9OnTmM1mOnTo\nQEhISHGGL4QQQogCnG4Ox7lz57h48SLt27e3lun1etq1a8fu3bvtnnPixAk0Gg0PPfRQofXGx8fj\n7u5OrVq1HB6zEEIIIe7M6RKOxMRENBqNTWIQHBxMUlISiqLYnHPixAlcXFyYMmUKrVu3pmnTpowY\nMYJr166pjvH19WXEiBE0b96cFi1a8M4775CRkVHsr0kIIYR40DldwpGeng6Al5eXqtzLywuLxUJm\nZqbNOSdOnMBoNOLt7c3MmTOJjY3ll19+4YUXXsBoNAKQkJDA9evXadSoEXPmzOGNN97ghx9+YOjQ\nocX/ooQQQogHnNPN4cjrwdBoNHaf12ptc6QXX3yRbt260apVKwBatGhBnTp16Nu3L5s3b6ZHjx6M\nHDkSg8FgnePRvHlz/P39eeuttzhy5AjNmzcvplckhBBCCKdLOHx8fADIyMggICDAWp6RkYFOp7Ou\nOsmvdu3a1K5dW1XWuHFjfH19iY+Pp0ePHoSGhtqcFxkZiaIoxMfH3zXhCAz0uZ+XI5yEtF/ZJu1X\ndknbiTxON6RSq1YtFEUhKSlJVX7+/PlCV5ds2rSJw4cP25QbDAb8/f0xm82sWbOG48ePq57Pzs4G\ncvfiEEIIIUTxcbqEIyQkhKCgILZu3WotMxqN7NixgzZt2tg9Z9myZXz00Ueqsh07dpCTk0OrVq3Q\n6XTMmDGDL7/8UnXM999/j4uLC4888ojjX4gQQgghrHSxsbGxpR1EQa6ursyaNQuDwYDBYODjjz8m\nMTGRSZMm4evrS1JSEomJiVStWhWAypUrExcXx5kzZ/Dx8WHXrl1MnDiRdu3a8cILLwC5G4DFxcWR\nkpKCi4sLmzZtYurUqfzjH/+gS5cupfhqhRBCiPJPo9hbZ+oEFixYwKJFi7h58yahoaGMHTvWOuFz\n7NixrF27VjVEsnPnTmbOnMnJkyfx8fGhe/fujBgxAldXV+sxa9euJS4ujnPnzlGpUiX69u3LoEGD\nSvy1CSGEEA8ap004hBBCCFF+ON0cDiGEEEKUP5Jw3MXy5cvp0qULTZo0oV+/fvzyyy+lHZKw49at\nW4SGhtr8jBgxwnrMv/71L9q3b0/Tpk0ZMGAAZ86cKcWIBcC2bdto1qyZTfnd2spgMPDRRx/Rtm1b\nmjVrxvDhw7ly5UpJhS2w33a///67zWewYcOGfPrpp9ZjpO1Kj8ViIS4ujq5du/LII4/w97//naVL\nl6qOKdbPniIKtXr1aqVhw4bKzJkzlZ07dyoDBw5Umjdvrpw/f760QxMF7N+/XwkNDVX27dunHDt2\nzPpz7tw5RVEUZcaMGUqTJk2UJUuWKNu3b1d69+6tREZGKmlpaaUc+YPryJEjSrNmzZRHHnlEVV6U\nthozZozSunVrZc2aNcr333+vdO7cWXniiScUi8VS0i/jgVRY261cuVJ55JFHVJ/BY8eOKZcuXbIe\nI21XeqZPn640btxYmT17trJ//35lxowZSqNGjZS5c+cqilL8nz1JOO6gffv2yvvvv299bDQalQ4d\nOigffvhhKUYl7FmwYIHy2GOP2X0uPT1deeSRR6wfKkVRlJSUFKVZs2ZKXFxcCUUo8uTk5Chz5sxR\nwsLClFatWqm+tIrSVufOnVMaNmyobN682XpMYmKiEhoaqmzZsqXEXseD6E5tpyiKMnHiROXpp58u\n9Pw///xT2q6UmM1mpVmzZsr06dNV5e+//74SHh5eIp89GVIpxP3ctVaUnhMnTtCgQQO7zx07doys\nrCxVW/r6+tKyZUtpy1Kwa9cu5s6dy5gxY3juuedUzxWlrQ4cOIBGo6Fdu3bWY2rVqkW9evXYtWtX\nibyGB9Wd2g5yP4d3umv3/v37pe1KSXp6Ok8++SSdOnVSldeuXZsbN25w4MCBYv/sScJRiPu5a60o\nPSdOnCArK4t+/frRuHFjoqKimDdvHgBnz54FoGbNmqpzatSoQWJiYkmH+sBr3Lgx27Zto3///jb3\nTCpKWyUmJlKpUiXc3d0LPUYUjzu1HeTeJPPSpUs88cQThIWF0blzZ9auXWt9Xtqu9Pj6+vLOO+/Y\n3OZj+/btVK1aleTkZKB4P3tOdy8VZ1GUu9YWfE6UDovFwunTp/H09GT06NFUq1aNHTt28MUXX5Cd\nnY2Liwuurq7o9er/7l5eXtZ2FiWncuXKhT6XkZFx17ZKT0+3+9nz8vKy/tIUxeNObXflyhVu3rzJ\nn3/+yVtvvYWPjw/ffvstY8aMQaPR0LNnT2k7J7NixQoOHDjAO++8UyKfPUk4CqHcx11rRemZPXs2\n1apVo0aNGgC0bNmSjIwM5s6dy+DBg6UdywhFUYrUVtKezqdChQrMnz+fhx56iEqVKgHQpk0bLl++\nzMyZM+nZsycgbecs1q9fT2xsLDExMfTv35/Zs2cX+2dPWrgQ+e9am9+d7lorSodWq6V169bWZCNP\nREQE2dnZeHh4YDAYMJvNquczMjKs7Sycg7e3913bytvb2+ZzWfAYUfLc3NwIDw+3Jht5IiIiSEpK\nIisrS9rOScTFxTF69Giio6P57LPPgJL57EnCUYj7uWutKB1Xrlxh+fLl3Lx5U1Wek5MD5P7lpSgK\n58+fVz2flJRE7dq1SyxOcXchISF3bauQkBCuXbuGwWAo9BhR8hITE1m2bBlGo1FVnp2djbu7Ox4e\nHtJ2TuCLL77gk08+4Yknnvj/9u4lJMoujuP4b1AmJhkXobtCxEWjSKIo4mwSBkKRdkIQgSIGhbsM\nLyNqohgRjQx4CSEkKLBNXgeLQhMXLaRFEATRQoIW4UYNrLzMafHyDg5U3t7H8/j2/ezmeWbxHw4H\nvsztKBqNJj5COYq9R3D8xkFOrYUdGxsb6ujo0OTkZNL1Z8+eKTs7WxcuXJDX601ay9XVVS0uLrKW\nLlNYWLjrWpWVlWlra0uzs7OJ5ywtLenjx48KBoNHPjP+8eXLF3V1dWl+fj7p+osXL1RcXCyJtbPt\n4cOHGh4eVm1trW7fvp30MchR7D2+w/EHV69eVU9Pj/x+v4qKivTo0SOtrKyopqbG9mjY4fTp06qq\nqlI0GpXH41FOTo5mZmb08uVLDQ4Oyufz6cqVK4n7WVlZun//vtLT01VdXW17fOxw8uTJXdfqzJkz\nqqioUHt7u75+/Sq/36++vj7l5uYqFApZfgV/r5KSEhUXF+vWrVtaXV1VZmamnjx5og8fPmh0dFQS\na2fT8vKy7t27p7Nnz6qyslJv375Nup+fn+/43uPwtl386dRauMfGxoYGBgYUi8W0vLysnJwcNTQ0\nJDbB9va2otGonj59qvX1dRUVFamtrY23cS3r7+/XyMiI3rx5k7i2l7X6/v27ent79fz5cxljFAwG\n1dbWpszMTBsv46/0q7VbW1tTJBLRq1evtLKyory8PN28eTPpL9BZOzvGxsYUDod/e//169fy+/2O\n7j2CAwAAOI7vcAAAAMcRHAAAwHEEBwAAcBzBAQAAHEdwAAAAxxEcAADAcQQHAABwHMEBwLrPnz8r\nEAgoEAhocXHxj88dHx9XXl6eAoGALl++/MvDpAC4D8EBwDV+d/T1v8bHxxUOh2WMUWlpqR48eKC0\ntLQjmg7AYRAcAI6FiYmJRGwEg0ENDw/L5/PZHgvAHhEcAFxvcnJSra2tMsbo/PnzGhoa0okTJ2yP\nBWAfCA4ArrYzNkKhkPr7++X1em2PBWCfCA4ArjU1NaXW1lbF43FVVFQoGo0qNTXV9lgADoDgAOBK\n09PTamlpUTwe18WLFxWJRJSSkmJ7LAAHRHAAcJ1YLKbm5mZtb2/L6/Wqqalp11+wAHA3ggOAq8Ri\nMTU1NSkejyslJUU/fvxQOBy2PRaAQyI4ALjK6Oio4vG4amtr1dXVJUlaWFjQyMiI5ckAHAbBAcBV\nPB6P6uvr1dzcrOrqaoVCIRljFIlE9O7dO9vjATggggOAq9TV1amxsTHxuLu7WxkZGdra2lJjY6PW\n19ctTgfgoAgOAK5SXl6e9PjUqVPq6emRMUafPn1SZ2enncEAHArBAcD1ysvLdenSJRljND09rbGx\nMdsjAdgnggPAsdDS0qKsrCwZY9Td3a2lpSXbIwHYB4IDwLHg8/l09+5dpaam6tu3b7px44Y2Nzdt\njwVgjwgOAMfGuXPndO3aNRlj9P79e925c8f2SAD2iOAA4Aoej2dP/yZ6/fp1FRQUSJIeP36subk5\np0cD8B/wGGOM7SEAAMD/G+9wAAAAxxEcAADAcQQHAABwHMEBAAAcR3AAAADHERwAAMBxBAcAAHAc\nwQEAABxHcAAAAMcRHAAAwHEEBwAAcNxP1Tn1YiU7iQ4AAAAASUVORK5CYII=\n", 217 | "text/plain": [ 218 | "" 219 | ] 220 | }, 221 | "metadata": {}, 222 | "output_type": "display_data" 223 | } 224 | ], 225 | "source": [ 226 | "%matplotlib inline\n", 227 | "import matplotlib.pyplot as plt\n", 228 | "import seaborn as sns\n", 229 | "sns.set()\n", 230 | "k_set = [5, 10, 15, 18, 20, 25, 30, 40, 50, 100, 200]\n", 231 | "rmse_topkCF_item_train = [0.5747, 0.6845, 0.7322, 0.7500, 0.7596, 0.7774, 0.7902, 0.8073, 0.8182, 0.8422, 0.8546]\n", 232 | "rmse_topkCF_item = [0.9543, 0.9278, 0.9223, 0.9215, 0.9213, 0.9217, 0.9225, 0.9242, 0.9258, 0.9314, 0.9345]\n", 233 | "rmse_topkCF_user_train = [0.6101, 0.7242, 0.7699, 0.7866, 0.7951, 0.8113, 0.8225, 0.8373, 0.8465, 0.8660, 0.8747]\n", 234 | "rmse_topkCF_user = [0.9874, 0.9573, 0.9489, 0.9468, 0.9458, 0.9449, 0.9445, 0.9447, 0.9453, 0.9491, 0.9526]\n", 235 | "pal = sns.color_palette(\"Set2\", 2)\n", 236 | "\n", 237 | "t = 11\n", 238 | "plt.figure(figsize=(8, 8))\n", 239 | "plt.plot(k_set[:t], rmse_topkCF_user_train[:t], c=pal[0], label='user-user train', alpha=0.5, linewidth=5)\n", 240 | "plt.plot(k_set[:t], rmse_topkCF_user[:t], c=pal[0], label='user-user test', linewidth=5)\n", 241 | "plt.plot(k_set[:t], rmse_topkCF_item_train[:t], c=pal[1], label='item-item train', alpha=0.5, linewidth=5)\n", 242 | "plt.plot(k_set[:t], rmse_topkCF_item[:t], c=pal[1], label='item-item test', linewidth=5)\n", 243 | "plt.legend(loc='best', fontsize=18)\n", 244 | "plt.xticks(fontsize=16);\n", 245 | "plt.yticks(fontsize=16);\n", 246 | "plt.xlabel('K', fontsize=25);\n", 247 | "plt.ylabel('RMSE', fontsize=25);" 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": null, 253 | "metadata": { 254 | "collapsed": true 255 | }, 256 | "outputs": [], 257 | "source": [] 258 | } 259 | ], 260 | "metadata": { 261 | "kernelspec": { 262 | "display_name": "Python 3", 263 | "language": "python", 264 | "name": "python3" 265 | }, 266 | "language_info": { 267 | "codemirror_mode": { 268 | "name": "ipython", 269 | "version": 3 270 | }, 271 | "file_extension": ".py", 272 | "mimetype": "text/x-python", 273 | "name": "python", 274 | "nbconvert_exporter": "python", 275 | "pygments_lexer": "ipython3", 276 | "version": "3.5.1" 277 | } 278 | }, 279 | "nbformat": 4, 280 | "nbformat_minor": 0 281 | } 282 | -------------------------------------------------------------------------------- /alpha.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "初始化变量...\n", 15 | "------ 第1/5组样本 ------\n", 16 | "载入训练集dataset/ml-100k/u1.base\n", 17 | "计算训练集各项统计数据...\n", 18 | "计算相似度矩阵...\n", 19 | "计算完成\n", 20 | "在训练集上测试...\n", 21 | "载入测试集dataset/ml-100k/u1.test\n", 22 | "测试集规模为 20000\n", 23 | "在测试集上测试...\n", 24 | "测试完成\n", 25 | "------ 第2/5组样本 ------\n", 26 | "载入训练集dataset/ml-100k/u2.base\n", 27 | "计算训练集各项统计数据...\n", 28 | "计算相似度矩阵...\n", 29 | "计算完成\n", 30 | "在训练集上测试...\n", 31 | "载入测试集dataset/ml-100k/u2.test\n", 32 | "测试集规模为 20000\n", 33 | "在测试集上测试...\n", 34 | "测试完成\n", 35 | "------ 第3/5组样本 ------\n", 36 | "载入训练集dataset/ml-100k/u3.base\n", 37 | "计算训练集各项统计数据...\n", 38 | "计算相似度矩阵...\n", 39 | "计算完成\n", 40 | "在训练集上测试...\n", 41 | "载入测试集dataset/ml-100k/u3.test\n", 42 | "测试集规模为 20000\n", 43 | "在测试集上测试...\n", 44 | "测试完成\n", 45 | "------ 第4/5组样本 ------\n", 46 | "载入训练集dataset/ml-100k/u4.base\n", 47 | "计算训练集各项统计数据...\n", 48 | "计算相似度矩阵...\n", 49 | "计算完成\n", 50 | "在训练集上测试...\n", 51 | "载入测试集dataset/ml-100k/u4.test\n", 52 | "测试集规模为 20000\n", 53 | "在测试集上测试...\n", 54 | "测试完成\n", 55 | "------ 第5/5组样本 ------\n", 56 | "载入训练集dataset/ml-100k/u5.base\n", 57 | "计算训练集各项统计数据...\n", 58 | "计算相似度矩阵...\n", 59 | "计算完成\n", 60 | "在训练集上测试...\n", 61 | "载入测试集dataset/ml-100k/u5.test\n", 62 | "测试集规模为 20000\n", 63 | "在测试集上测试...\n", 64 | "测试完成\n", 65 | "------ 测试结果 ------\n", 66 | "融合模型中,不同alpha在训练集上的RMSE:\n", 67 | "alpha = 0.00: 0.8225\n", 68 | "alpha = 0.10: 0.8108\n", 69 | "alpha = 0.30: 0.7907\n", 70 | "alpha = 0.50: 0.7754\n", 71 | "alpha = 0.60: 0.7696\n", 72 | "alpha = 0.65: 0.7672\n", 73 | "alpha = 0.70: 0.7651\n", 74 | "alpha = 0.75: 0.7634\n", 75 | "alpha = 0.80: 0.7620\n", 76 | "alpha = 0.90: 0.7601\n", 77 | "alpha = 1.00: 0.7596\n", 78 | "融合模型中,不同alpha在测试集上的RMSE:\n", 79 | "alpha = 0.00: 0.9445\n", 80 | "alpha = 0.10: 0.9368\n", 81 | "alpha = 0.30: 0.9248\n", 82 | "alpha = 0.50: 0.9176\n", 83 | "alpha = 0.60: 0.9159\n", 84 | "alpha = 0.65: 0.9155\n", 85 | "alpha = 0.70: 0.9154\n", 86 | "alpha = 0.75: 0.9156\n", 87 | "alpha = 0.80: 0.9161\n", 88 | "alpha = 0.90: 0.9181\n", 89 | "alpha = 1.00: 0.9213\n" 90 | ] 91 | } 92 | ], 93 | "source": [ 94 | "import pandas as pd\n", 95 | "import numpy as np\n", 96 | "import var\n", 97 | "import predict as pre\n", 98 | "import utils\n", 99 | "\n", 100 | "print('初始化变量...')\n", 101 | "names = ['user_id', 'item_id', 'rating', 'timestamp']\n", 102 | "direct = 'dataset/ml-100k/'\n", 103 | "trainingset_files = (direct + name for name in ('u1.base', 'u2.base', 'u3.base', 'u4.base', 'u5.base'))\n", 104 | "testset_files = (direct + name for name in ('u1.test', 'u2.test', 'u3.test', 'u4.test', 'u5.test'))\n", 105 | "\n", 106 | "if __name__ == '__main__':\n", 107 | " \n", 108 | " i = 0\n", 109 | " nums = 5\n", 110 | " alpha_set = [0, 0.1, 0.3, 0.5, 0.6, 0.65, 0.7, 0.75, 0.8, 0.9, 1]\n", 111 | " rmse_blendCF = {alpha:[] for alpha in alpha_set}\n", 112 | " rmse_blendCF_train = {alpha:[] for alpha in alpha_set}\n", 113 | " \n", 114 | " for trainingset_file, testset_file in zip(trainingset_files, testset_files):\n", 115 | " i += 1\n", 116 | " print('------ 第%d/%d组样本 ------' % (i, nums))\n", 117 | " df = pd.read_csv(trainingset_file, sep='\\t', names=names)\n", 118 | " \n", 119 | " var.ratings = np.zeros((var.n_users, var.n_items))\n", 120 | " print('载入训练集' + trainingset_file)\n", 121 | " for row in df.itertuples():\n", 122 | " var.ratings[row[1]-1, row[2]-1] = row[3]\n", 123 | " \n", 124 | " print('计算训练集各项统计数据...')\n", 125 | " utils.cal_mean()\n", 126 | "\n", 127 | " print('计算相似度矩阵...')\n", 128 | " var.user_similarity = utils.cal_similarity(kind='user')\n", 129 | " var.item_similarity = utils.cal_similarity(kind='item')\n", 130 | " print('计算完成')\n", 131 | " \n", 132 | " predictions_blendCF_train = {alpha:[] for alpha in alpha_set}\n", 133 | " targets = []\n", 134 | " print('在训练集上测试...')\n", 135 | " for row in df.itertuples():\n", 136 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 137 | " for alpha in alpha_set:\n", 138 | " predictions_blendCF_train[alpha].append(pre.predict_blend(user, item, alpha=alpha))\n", 139 | " targets.append(actual)\n", 140 | " for alpha in alpha_set:\n", 141 | " rmse_blendCF_train[alpha].append(utils.rmse(np.array(predictions_blendCF_train[alpha]), np.array(targets))) \n", 142 | "\n", 143 | " print('载入测试集' + testset_file)\n", 144 | " test_df = pd.read_csv(testset_file, sep='\\t', names=names) \n", 145 | " predictions_blendCF = {alpha:[] for alpha in alpha_set}\n", 146 | " targets = []\n", 147 | " print('测试集规模为 %d' % len(test_df))\n", 148 | " print('在测试集上测试...')\n", 149 | " for row in test_df.itertuples():\n", 150 | " user, item, actual = row[1]-1, row[2]-1, row[3]\n", 151 | " for alpha in alpha_set:\n", 152 | " predictions_blendCF[alpha].append(pre.predict_blend(user, item, alpha=alpha))\n", 153 | " targets.append(actual)\n", 154 | " for alpha in alpha_set:\n", 155 | " rmse_blendCF[alpha].append(utils.rmse(np.array(predictions_blendCF[alpha]), np.array(targets))) \n", 156 | " print('测试完成')\n", 157 | " \n", 158 | " print('------ 测试结果 ------')\n", 159 | " \n", 160 | " print('融合模型中,不同alpha在训练集上的RMSE:')\n", 161 | " for alpha in sorted(alpha_set):\n", 162 | " print('alpha = %.2f: %.4f' % (alpha, np.mean(rmse_blendCF_train[alpha])))\n", 163 | " \n", 164 | " print('融合模型中,不同alpha在测试集上的RMSE:')\n", 165 | " for alpha in sorted(alpha_set):\n", 166 | " print('alpha = %.2f: %.4f' % (alpha, np.mean(rmse_blendCF[alpha])))\n", 167 | " " 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 20, 173 | "metadata": { 174 | "collapsed": false 175 | }, 176 | "outputs": [ 177 | { 178 | "data": { 179 | "image/png": "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\njKM/ld577z3Nnj2728cMw+j1cQAAMHb1K2gM9AhxwzAGdD0AABiZ+gwaa9asGYp2AACAUajPoHHl\nlVcORTsAAMAoxGJQAABgmUENGg0NDfrwww9VUVExmE8LAABGqH4tBpWkRCKh5557Th9++KFWrlwp\nv9+feSwUCukHP/iBXnzxRaXTaUnSzJkztXLlSl1wwQWD32oAADAi9GtE48CBA1q2bJluv/12bdiw\nQXV1dZnHUqmUvv71r2vz5s1KpVIyTVOmaWrPnj26+eab9dxzz1nWeAAAkN36DBqpVEo33nijysvL\n5XK5dP7558vj8WQe//Wvf60//elPkqRzzjlHL774orZt26Y777xTNptNd955p2pra617BwAAIGv1\nGTQ2bdqkPXv2aPr06Xr22Wf1i1/8QiUlJZnH169fL0ny+/366U9/qmnTpsnn8+mrX/2qbrvtNoXD\nYW3YsMG6dwAAALJWn0Hjf/7nf2QYhv75n/9Z06dP7/LYrl27VFVVJcMwtGzZMgUCgS6Pf/WrX5XD\n4dBrr702qI0GAAAjQ59B489//rOKi4s1d+7cYx7bunVr5uvuFn36fD5NnTpVlZWVA2wmAAAYifoM\nGg0NDV2mSjrbtm1b25PYbDr77LO7rePz+RQKhQbQRAAAMFL1GTQMw1Aqler2sXfeeUeGYaisrKzL\ndtfOGhoaFAwGj7thTz31lJYsWaJ58+Zp+fLl+uCDD3qt/9577+maa67RWWedpYsuukhr165VMpns\nUmfZsmUqKyvr8rFw4cLjbhsAAOifPs/RKC4u1oEDB44p//jjj9XQ0CDDMHr8ZX3o0CFVVlbq5JNP\nPq5GPfPMM1q9erVuvfVWnXbaaXr88cd13XXXaePGjZo0adIx9SsqKnTttddqwYIFWrt2rfbu3at7\n771XkUhEK1eulNR2DsjevXu1YsUKLViwIHOtw9Hvo0QAAMBx6nNEY8GCBWpsbNTbb7/dpXzjxo2Z\nry+66KJur33yySdlmmaP0yo9efDBB7V8+XLdcsstuuCCC7Ru3Trl5eXpscce67b+Cy+8INM09eCD\nD+r888/X3/7t3+prX/uannrqqUydTz/9VKlUSl/4whc0d+7czMepp556XG0DAAD912fQuOKKK2Sa\npr7zne/ozTffVEtLi5577jn913/9lwzD0Mknn6wzzjjjmOu2bt2qn//85zIMQ0uWLOl3g/bt26eq\nqiotXrw4U+ZwOLRo0SK98cYb3V6TSCTkcDjkdrszZbm5uYpEIorH45KknTt3yuPxaOrUqf1uCwAA\nGJg+5w0i8pIZAAAgAElEQVTOPvtsffnLX9bTTz+t6667LlNumqYcDofuvPPOLvU3btyoV199VS+/\n/LLS6bT+8i//Uuecc06/G1ReXi7DMI4JBJMnT1ZFRYVM05RhGF0eu+yyy7R+/Xrdd999uv7667Vv\n3z6tX79eX/ziF+VyuSS1bcUNBoP61re+pTfffFOGYWjp0qVatWqVfD5fv9sHAAD6r18LFO666y6N\nHz9ev/rVrxQOhyVJEydO1J133nnMaMaDDz6oAwcOyDRNnXHGGbrvvvuOq0EdO1SO/uXv8/mUTqcV\niUSOeay0tFQrVqzQHXfcoUceeUSSNGfOHN19992ZOrt371Z9fb1OPfVUfe1rX9POnTv1wAMP6MCB\nA3r00UePq40AAKB/+hU0DMPQN7/5Td1www3au3evnE6nZsyYIZvt2JmXk046SSUlJbrssst0xRVX\nyOl0HleDTNPMvGZ3unvNDRs26J/+6Z+0fPlyXXzxxaqtrdW//du/6YYbbtBjjz0mp9OpFStWKB6P\nZ84DmT9/vvLz8/Xtb39b27Zt0/z584+rnQAAoG/HteXC4/Fo9uzZvdb52c9+NqAGdZwuGg6HVVBQ\nkCkPh8Oy2+3Kyck55pqf//znWrRokVavXp0pmzNnji655BJt2rRJf/3Xf62ysrJjrrvgggtkmqZ2\n7txJ0AAAwAJZt7dz6tSpMk1TFRUVKi0tzZRXVlZq2rRp3V5TXV2tK6+8skvZjBkzlJeXpz179iid\nTmvjxo0qKyvrEpSi0agkKT8/v892FRcH+qyDgaOfrUcfW48+th59PHL0GTTS6fSgvFB3Ux7dmTZt\nmkpKSrRlyxadf/75ktp2lbz22mtddqIcfc3777/fpWzfvn1qbGxUaWmpbDabHnzwQc2ePVs//elP\nM3V+97vfyel06swzz+yzXXV1Lf1qP05ccXGAfrYYfWw9+th69PHQGKww12fQmDNnzoBfxDAMbd++\nvd/1r7/+et11110KBAI666yz9Pjjj6uxsVFf+9rXJLUd0HX48GHNmzdPkvSNb3xDt912m26//XZd\neumlqqur009/+lOVlpbq8ssvlyTddNNN+sEPfqAf/ehHuvDCC/XRRx9p3bp1+od/+Icej1gHAAAD\n02fQ6FicOZSuueYaxeNxrV+/XuvXr1dZWZl++ctfavLkyZKkdevW6dlnn9WOHTskSUuXLpXD4dC6\ndev03HPPqaioSJ/73Od02223yev1SpKuvvpquVwuPfroo9qwYYOKior0jW98QzfccMOQvz8AAMYK\nw+wjSZSVlWV2gMyePVuXXnqpTj/99ON+oeM5SyMbMUxnPYZDrUcfW48+th59PDSGbOrkgQce0ObN\nm/X6669r+/bt2rFjh6ZMmaJLLrlEl1xyiWbNmjUoDQEAAKNPnyMaHSKRiF555RVt3rxZv//97xWP\nx2UYhk466SRdeumluuSSSzRlyhSr2ztsSM/W438p1qOPrUcfW48+HhqDNaLR76DRWSgU0pYtW/Tb\n3/5Wb731lpLJpAzD0GmnnaZLL71UF198scaPHz8oDcwWfFNbjx8e1qOPrUcfW48+HhrDGjQ6a2pq\n0ksvvaTNmzfrj3/8o1KplGw2m84880xdeumlWrp0aZeDt0Yqvqmtxw8P69HH1qOPrUcfD42sCRqd\nHT58WC+++KJeeOEFbdu2TaZpym6369xzz9UvfvGLwXqZYcE3tfX44WE9+th69LH16OOhMVhBo3+n\naPVTQUGBrrnmGj300ENatWqVvF6vksmk/vCHPwzmywAAgBFi0I4gb21t1auvvqoXX3xRb7zxhqLR\nqEzTlM1m09lnnz1YLwMAAEaQAQWNSCTSJVzEYrFMuJg/f74uvvhiLVmyREVFRYPVXgAAMIIcd9AI\nh8OZcPH73/8+Ey4Mw9CZZ56ZCRfjxo2zor0AAGAE6VfQCIfDeuWVVzLhIh6PZ8LFvHnzdPHFF2vp\n0qWjbksrAAAYmD6Dxi233KI333wzEy4kae7cuZlwwQ3JAABAT/oMGq+88kpbRYdD5513ni6++GJN\nnDhRklReXq7y8vJ+vdDChQtPvJUAAGBE6tfUiWEYSqVSevPNN/Xmm28e94sc723iAQDA6NCvoDHQ\nM72G41bzAABg+PUZNHbu3DkU7QAAAKPQoJ4MCgAA0JnlQSORSOj++++3+mUAAEAWOq6gsX//fm3Z\nskVbtmxRTU1Nn/W3bdumyy+/XA899NAJNxAAAIxc/VoMWlNTo1WrVumtt97KlNlsNn35y1/W7bff\nLpfL1aV+OBzWvffeq6eeekrpdFqGYQxuqwEAwIjQZ9BoaWnRVVddpbq6ui67R1KplDZs2KBwOKx/\n+Zd/yZRv3bpV3/ve91RTUyPTNOVyuXTTTTdZ03oAAJDV+pw6+cUvfqHa2lrZ7Xbdcsst2rBhg55+\n+mn94z/+o2w2mzZv3qwPP/xQkvTLX/5S1157bSZkLFiwQBs3btQtt9xi+RsBAADZp88RjTfeeEOG\nYWjNmjVatmxZpnzOnDmaMGGC7r77bv32t7/Vxx9/rHvuuUeSFAgEtHLlSl111VXWtRwAAGS9Pkc0\nKisrFQwGu4SMDsuXL5fL5dL//u//ZqZPPve5z+n5558nZAAAgL5HNMLhsGbPnt3tYy6XS1OnTtUn\nn3wiwzB066236tZbbx30RgIAgJGpzxGNZDJ5zK6Sznw+nwzD0PLlywkZAACgiwEf2GWztT3Ftdde\nO+DGAACA0WXQTgadPHnyYD0VAAAYJbjXCQAAsAxBAwAAWKZfR5DX19fr2Wef7fExST0+3uGKK644\nzqYBAICRzjA7nyvejbKysgHfq8QwDG3fvn1AzzHc6upahrsJo15xcYB+thh9bD362Hr08dAoLg4M\nyvP0a0Sjjyxi+fUAAGBk6jNo7Ny5cyjaAQAARiEWgwIAAMsQNAAAgGUIGgAAwDIEDQAAYBmCBgAA\nsAxBAwAAWIagAQAALEPQAAAAliFoAAAAyxA0AACAZQgaAADAMgQNAABgGYIGAACwDEEDAABYhqAB\nAAAsQ9AAAACWIWgAAADLEDQAAIBlCBoAAMAyBA0AAGAZgkY/7Gg4qMPxiEzTHO6mAAAwojiGuwEj\nwZ7mWoXDcRW6fJrlK5bP4R7uJgEAMCIQNI5DfTysw4mISj15muYtlNNmH+4mAQCQ1Zg6OU6maWp/\na4O2NuxVVbSJ6RQAAHpB0DhB8XRKO1oO6t2m/WpKtA53cwAAyEoEjX7Icbh6fKw5EdW7jfu1vaVa\nsXRyCFsFAED2I2j0w+KSkzXDVyibYfRYpzrarK0Ne7W/9bDSTKcAACCJoNEvdptN071FOi9/usa7\nAz3WS6bT+iRUpz82lqs+Hh7CFgIAkJ0IGschx+7UacGJOjO3VP5etriGk3F90FSpj5oPqDUVH8IW\nAgCQXQgaJ6DA5dWCvKk6xT9ODlvPXVgXC2lrQ7k+DR9S0kwPYQsBAMgOBI0TZDMMTc7J18L86ZqU\nk6eelm+kTVPlkXq93bBXNbFmtsMCAMYUgsYAuWwOlfnHa0HeVOU5c3qsF00l9XFztd5rqlBLMjqE\nLQQAYPgQNAZJwOHRWbmlmhMokdvW84GrjYlWvdO4T7tCNYqzHRYAMMpxBPkgMgxDEzxBFbn92hep\n1/7Whm63upqmVNnaqJpYi2Z4izTRk9vr1lkAAEYqRjQs4DBsmukr1rn501Tk9vdYL5FOaVeoRu80\n7lNDPDKELQQAYGgQNCzktbs0LzhJ83InyWvv+XTRUDKm95oq9HFzlaKpxBC2EAAAazF1MgSKXH4V\n5PtU2dqgzyL1SvWw1bUm1qJD8ZCmegs1JSdfdoMcCAAY2fhNNkRshqEp3gItzJ+uEk+wx3op09Rn\n4UN6u6FcdbEWtsMCAEY0gsYQc9sdOjVQorPzpijo9PRYrzWV0EfNVfqguVLhZGwIWwgAwOAhaAyT\nXGeOzs6dotmBCXLZ7D3WOxyP6O3GffokVKtEOjWELQQAYOBYozGMDMPQRE+uil1+lUfqVRFt7Haq\nxDRN7W9t0MFYs2b6ilXiDspgOywAYARgRCMLOG12zfKP07l5U1Xg8vZYL55OaUfLQb3btF9NidYh\nbCEAACeGoJFFfA63zghO1tzgROXYnT3Wa05E9W7jfm1vqVaM00UBAFmMqZMsYxiGit0BFbh82t/a\noH2ReqV62HlSHW1WXTyk6TmFmpyTz+miAICsw4hGlrIbNk33Fuq8/Oka7w70WC+ZTuuTcJ3+2Fiu\n+nh4CFsIAEDfCBpZzmN36rTgRJ2VWyq/w91jvXAyrg+aKvVh8wFFUvEhbCEAAD0jaIwQ+S6vFuRN\n1Sn+8XL2sh32UCyktxvK9Wm4TskeTiAFAGCosEZjBLEZhibn5Gmc26+9kXodiDaqu+UbadNUeeSw\nqqJNmpJToEk5eXJwnDkAYBjw22cEctkcOsU/XgvypirPmdNjvXg6pT3hOr11+DPtixxmhAMAMOQI\nGiNYwOHRWbmlmhMokcfe8+BU58Cxn8ABABhCTJ2McIZhaIInqCK3X/sih7W/9bDSPWyHjadT+iRc\np32thzW1fUqFO8QCAKzEb5lRwmHYNNNXpPPyp6nEE1RvR2p0BI4/HP5M+1sP93jbegAABoqgMcrk\n2F06NVCi89pvR99n4AjV6a3DewkcAABLEDRGKW+nwDGhj8ARSyczgaOitYHAAQAYNASNUc5rd2lO\noETn5k3rV+DYHaolcAAABg1BY4zwOdxHAoc7QOAAAAwJgsYY43O4NSc4kcABABgSBI0xqnPgGH8c\ngaOSwAEAOA6cozHG+RxunRacqFAypvJIvWrjLd0eay61BY5dodrMORwlnlzO4QAA9IqgAUmS/6jA\nURNr6bFuNNU5cBSqxBMkcAAAusVvB3TRETjOzW+bUulNW+Co0daGvapsbWRKBQBwDIIGutU5cIw7\njsBxoLWxxyPQAQBjD1Mn6JXf4dbp7VMqeyP1qu1jSmVnqEblrfWallOoEk+ubL2tMgUAjHoEDfRL\nR+BoSUbbFo3GQj3W7Qgc+1oPa6q3QCVuAgcAjFUEDRyXgMOj04OT+hU4WlMJ7Wyp0b7IYU3zFmgC\ngQMAxhyCBk5I58CxN1Kvuj4Cx46WGpUTOABgzMnaxaBPPfWUlixZonnz5mn58uX64IMPeq3/3nvv\n6ZprrtFZZ52liy66SGvXrlUymexS591339XVV1+tM844Q0uWLNHTTz9t5VsYEwIOj+YGJ+mc/Kkq\ndvt7rdsROLY27FVVtIlFowAwBmRl0HjmmWe0evVqXX755XrwwQcVDAZ13XXX6cCBA93Wr6io0LXX\nXiu/36+1a9fq61//uh555BH95Cc/ydT59NNPdf3116u0tFRr167V4sWL9f3vf18vvfTSUL2tUa1z\n4CjqV+A4SOAAgDHAMM3s+yl/4YUXatGiRbrjjjskSclkUkuXLs2Eg6M9/PDDWrdund5++2253W5J\n0r/+67/qP/7jP/Tuu+9Kkr773e9q+/bt2rRpU+a6lStXateuXdq4cWOfbaqr63m3BY7VnIhqb2u9\nDvUypdLBa3dqmrdQp02epPpDfdfHiSsuDvC9bDH62Hr08dAoLu79aIP+yroRjX379qmqqkqLFy/O\nlDkcDi1atEhvvPFGt9ckEgk5HI5MyJCk3NxcRSIRxeNxSdJbb72lRYsWdbnuoosu0u7du1VXVzf4\nb2SMCzo9mhecpAV5U1Xk8vVaN5JKaHvLQb1WtVvVjHAAwKiSdUGjvLxchmFo6tSpXconT56siooK\ndTcAc9lll8lut+u+++5TU1OTPvroI61fv15f/OIX5XK51NraqtraWk2ZMqXLdaWlpTJNU+Xl5Va+\npTEt6PRoXu5kLcib0mfgCCdj2t5yUG837CVwAMAokXVBIxRqGzr3+br+UvL5fEqn04pEIsdcU1pa\nqhUrVuiXv/ylzj33XF199dUqLCzU3Xff3edzdn4c1gk6czKBo7CfIxxvN5QTOABghMu6oNExYmH0\nsP3RZju2yRs2bNDtt9+uq6++Wr/61a907733qrm5WTfccIMSicQJPSesEXTm6IzcyTq7X4Ejngkc\nB6PNBA4AGIGy7hyNQKBt8Uk4HFZBQUGmPBwOy263Kycn55hrfv7zn2vRokVavXp1pmzOnDm65JJL\ntGnTJi1dujTzHJ11/Nnv732XhDR4i2LQplgBnaRxaohFtKupRnWtbQu7fD5Xt/XL0/WqS4R0ct44\nTfTmcQ7HAPC9bD362Hr08ciRdUFj6tSpMk1TFRUVKi0tzZRXVlZq2rRp3V5TXV2tK6+8skvZjBkz\nlJeXpz179sjr9aq4uFgVFRVd6lRUVMgwDE2fPr3PdrHC2TozjSIVOn2qt4dVXl/fY72w4qptapHP\n4dK0nEKNcwcIHMeJ1frWo4+tRx8PjVG762TatGkqKSnRli1bMmWJREKvvfaaFi5c2OM177//fpey\nffv2qbGxMRNWFi5cqFdffbXLYtKXX35Zs2bN6jJyguGR58zReeOna37eFBW4vL3WDSfj+nNLtf7Y\nyJQKAGQ7++rO8w1ZwuVyad26dYrH44rH41qzZo3Ky8v14x//WMFgUBUVFSovL9eECRMkSQUFBXr4\n4Yd18OBB5eTk6P3339cdd9yhYDCo1atXy+l0qrS0VA899JB27twpv9+vJ554Qhs2bNDq1as1c+bM\nPtsUicStfttjns/nVjqaVoknVwUur2LppFpTiR7rJ9Ip1cVDqou3yGmzy2t39bgOB218Pjffyxaj\nj61HHw8Nn8/dd6V+yMoDuyTpscce0/r169XQ0KCysjKtWrVKc+fOlSStWrVKzz77rHbs2JGpv2XL\nFq1bt0579uxRUVGRPve5z+m2227rMlrx5ptv6r777tNnn32mkpIS3XTTTbriiiv61R6G6azX3XBo\nYyKivZF6HY4fu9voaD6HS9O9hRrnChA4esCQs/XoY+vRx0NjsKZOsjZoZBu+qa3X2w+PhvbA0dCP\nwOF3uDXNW6hxLj+B4yj8gLYefWw9+nhoDFbQyLrFoEB38p1e5ed6+xU4QsmYPm6uInAAQBYgaGBE\nyQSOeHvgSPQvcEz3FqqYwAEAQ46ggREp3+VVvqstcHwWOaTGRGuPdUPJmP5E4ACAYUHQwIiW7/Jq\nvmuKDscj2kvgAICsQ9DAqFDg8irfWaqGRGu/A0egPXAUETgAwDIEDYwahmF0Chxtazh6CxwtyZg+\nInAAgKUIGhh12gKHT/nOtl0qn0Xq1dTvwFGkIpePwAEAg4SggVGrc+A43D7C0XfgOEDgAIBBRNDA\nqGcYhgpdPhVkAschNSWiPdbvCBxBp0fTvYUqdBI4AOBEETQwZhxv4GhORPVhE4EDAAaCoIEx5+jA\n8VnkkJoJHABgCYIGxqzOgaM+EdbeSD2BAwAGGUEDY55hGCpy+VXo9B1X4Mh1ejQ1p0CFLr9sBA4A\n6BZBA2h3vIGjKRHVR4kquWx2TfAENdGdK5/DPYQtBoDsR9AAjnK8gSOeTml/pEH7Iw3KdXpU4snV\neFdADpt9CFsNANmJoAH0oHPgOBQPa2/kkFqSsV6vaUpE1ZSI6hOjVuPcAZW4c5XnzGEtB4Axi6AB\n9MEwDBW7/Spy9T9wpExT1dFmVUeb5bU7NcGTqxJ3UB67c4haDQDZgaAB9FPnwHE4EVF1tEl18ZDS\nptnrdZFUQp+FD2lv5JAKnD6VeHJV5PLJbtiGqOUAMHwIGsBx6tgWW+jyKZ5OqibWoupoU5+jHKYp\n1cfDqo+H5bTZNcEdUIknVwGHZ4haDgBDj6ABDIDL5lBpTr5Kc/LVkoyqKtqkmliLEulUr9cl0ilV\ntDaqorVRQadHJe6gxruDcrKAFMAoQ9AABknA4dEpfo9O8hXrUDykqmiTGhIR9TGzouZEVM2JqD4J\n12mcy68ST67ynV4WkAIYFQgawCCzGzaNbx+haE0ldDDWpOpos1pTiV6vS5umDsZadDDWIo/doRJ3\nrko8ucphASmAEYygAVgox+7UdG+RpuUUqiHRqupYk2pjLX0uII2mktobqdfeSL0KXF6VuHNV7Paz\ngBTAiEPQAIaAYRgqcHlV4PLqZN841cZaVBVr6vUgsA6H4xEdjkfkCNs0wR1UiTtXAYebqRUAIwJB\nAxhiTptdk3LyNCknT6FkTNXRJh2MNSvexwLSZDqtytZGVbY2yu9wa6InV+PdAbls/DMGkL34CQUM\nI7/DrVn+cZrZvoC0Otqk+kS4zwWkoWRMu0O12hOuU5HLrxJPUAVOHzd3A5B1CBpAFrAZhsa5Axrn\nDiiWSqq6fQFpJBXv9bq0aao21qLaWIvcNodKPEGVeHLltbuGqOUA0DuCBpBl3HaHpnkLNTWnQE3J\nVlVHm1UTa1HKTPd6XSydVHnksMojh5XnzNFET66K3QE5WEAKYBgRNIAsZRiG8pxe5Tm9muUrVm37\n2RxNidY+r21MtKox0Sp7qFbj208gzeUEUgDDgKABjAAOm10TPbma6MlVOBlTdaxZB6PNiqWTvV6X\nMtOqijapKtokn8Ol2e4SedIOuVlACmCI8NMGGGF8DrdOchRrhrdI9fGwqmNNOhQPy+xjBWk4GdeO\nxmpFIgkVunya6A6q0OVnASkASxE0gBHK1n432WK3X7F0UjXRZlXFmhRO9r6A1DRNHYqFdCgWkstm\n1wRPUBPdufI53EPUcgBjCUEDGAXcNoemeAtUmpOv5mRU1dEm1cRblEz3voA0nk5pf6RB+yMNynV6\nVOLJ1XhXQA5u7gZgkBA0gFHEMAzlOnOU68zRLLPtBNLqaLMaEpE+r21KRNWUiOoTo1bj3AGVuHOV\n58zhBFIAA0LQAEYpu2FTiaftxmyRVFzV0Wa12KMKq/eplZRpqjrarOpos7x2pyZ4clXiDsrDzd0A\nnACCBjAGeO0uzfQVqbDIr91GjaqjTaqLh/q8uVskldBn4UPaGzmkAqdPJZ5cFbl83NwNQL8RNIAx\nxGYYKnT5VOjyKZ5OqibWoqpok0LJWK/XmaZUHw+rPh6W02bXhPazOQKczQGgDwQNYIxy2RwqzcnX\nZE+eQqmYqtpv7tbXAtJEOqWK1kZVtDYqkLm5W1BOFpAC6AZBAxjjDMNQwOHRKX6PTmq/uVtVtEkN\niUifN3drSca0K1SrT8J1Knb5VeLJVYHTywJSABkEDQAZdsOm8e6gxruDak0ldLD95m6tqUSv16VN\nUzWxFtXEWuSxO1TibluEmsMCUmDMI2gA6FaO3anp3iJNyylUQ6JV1bEm1cZa+lxAGk0ltTdSr72R\nehW4vCpx56rY7WcBKTBGETQA9MowDBW4vCpweXWyr+1sjqpYk5oT0T6vPRyP6HA8IkfYpgnuoErc\nuQo43EytAGMIQQNAvzltdk3KydOknDyFkjFVty8gjadTvV6XTKdV2dqoytZG+TMLSANycXM3YNTj\nXzmAE+J3uDXLP04z2xeQVkebVJ8I97mANJSMaXeoVnvCdSpy+VXiCarA6ePmbsAoRdAAMCA2w9A4\nd0Dj3AHFUklVty8gjaR6P4E0bZqqjbWoNtYit82hEk9QJZ5cee2uIWo5gKHA6iwct698ZZm++c2b\nBq3eYOvv6zY0NCga7XudQX/dffeduuCCcwbt+UYit92had5CnZc/TfPzSlXiCcrej5GKWDqp8shh\nvXV4r7Y17ld1tElJs/fzPACMDAQNHLf+LuQbrgV//Xndt956U9dc82U1NjYO2utefvmXdfvtPxy0\n5xvJDMNQntOrUwMl+nzBTM0OjFeuM6df1zYmWrW95aB+X/+pdrQcVEM80udOFwDZi6kTjEk7dvxZ\n4XBoUJ9zzpzTNGfOaYP6nKOBw2bXRE+eJnryFE7GVB1r1sFos2LpZK/Xpcy0qqJNqoo2yWGzqdDZ\ndnR6gcsnN4tIgRGDf60Yk0z+hzwsfA63TnIUa4a3SPXxsKpjTToUD/f595FMpzMHgklS0OnJBI+A\nw8NCUiCLETRwwp5/fqPWr/+lDh06pJNOmqXrrrtJ55xzXq/XfPzxR3rkkZ9p+/Y/S5JOO+10XX/9\nzZo9e06mzlVXXaZzz12ouXPP0K9//agOHDigcePG6+qr/0Z//ddXdXm+//mfl/T4449p//59mjy5\nVNdff0uf7b777jv1wgvPyzAMXXXVMp155nz927/9TLfeeoPcbo/Kymbrqaf+Uzk5Obr//nWaMWOm\nXnlli/77v5/Snj27FYvFVFQ0TosXf0HXX3+znM620y9/9KPVevHF3+qNN97J/Hn79o/1T//0Q61d\ne7927twur9enCy/8om655ZtyucbuokebYajY7Vex269YOqmaaLOqYk0KJ3tfQNqhORFVcyKqvZF6\nuWx2Fbh8KnT6VODysmUWyDL8i7RYPJ3U9paDakhk1zyzzTCU7/Tq1MCEE/rBvGPHdu3Y8WddddXf\nKC8vTxs3/rdWrPiWfvKTtZo/f0G317zzzlatXHmbZs06Rddff7MSibg2b96kb3zjBt1//0/1hS/8\nRabu1q1/0KuvbtFXvrJc+fkFeu65/9b999+riRMn6bzzzpckbd68SWvW/FCnnz5Pt9zyLVVWVuiO\nO1bJMKSSkok9tv3yy7+scDikN954Xd/61rc1bdoMSW3rCj766ANVVVXqG9/4lqqrqzR9+gxt2vSs\n7rnnR/r85/9SN9/8TSWTCb3++qv6z//8tQzD0M03/5/M9Z3XhxiGoYaGBv2///d/dOGFF2np0ku0\ndesf9PTTT8rtdmeuG+vcNoemeAtUmpOv5mRU1dEm1cRb+ry5W4d4OqWD0bbpGMOQgo4cFbp8KnL5\n5LdzOBgw3AgaFtveclD18fBwN+MYadNUfTys7S0HdUbu5OO+PhaL6t57H9C55y6UJC1d+lf6m7+5\nUv/+7w/qkUfWH1PfNE3de+8azZlzutaufThT/uUvX62vfe0a3X//fV2CRl1drR599AnNmDFTknTB\nBalNTI4AACAASURBVIt0xRUX6+WXX9B5552vdDqtn/1srU499TQ9+OBDstvb7hx68smn6O677+y1\n7XPmnKaZM2fpjTde1+c/v0gTJkzo8r5+8IO7VFZ2aqbsySf/Q6efPk9r1tyXKbvyyqv0la8s09tv\nv9VrYAiFWvR//++KzEjMX/3VFfq7v7taL7/8IkHjKIZhKNeZo1xnjmaZbSeQHoqHdDgR6XfoME2p\nKdGqpkSrPgsfktvmUKGrbYol3+nlDrPAMGDXicWakq3D3YRenWj7pk+fmQkZkhQMBvWlL12i3bt3\nqqHh8DH1d+3aqerqKn3+8xeoqakx89HaGtXnPvcX2rNnt2prazP1S0unZkKGJBUUFCo/v0D19fXt\nz7dDDQ2HdcklyzIhQ5KWLLlEgUDwhN6TJLnd7i4hQ5J+9av/0r333t+l7PDhegUCQbW2Rvp8zsWL\nL+ry55NOmqX6+kMn3MaxwG7YVOLJ1enBSfqLgpN0Zm7p/2/vzuOirvY/jr++s7EjuIs7RqKioGYu\nqbmVes2ulZpm6bVSK/uZZXazTS3TVstEK3OP6qaC3ermLbXFJW51S63c9aqAggsIyDrb+f0xMDKC\nwowMgn6ejwePgfNd5sx5wMybc873fGnmH0qAwb3hpkK7lRMFWfyRfYKtGYf5LTOZY3kZ5FgLZZ6O\nEFVEejS8rJbBr1r2aBSrZajYJYcXataseamyxo0bA5CaeoLQ0Nou206cOA7A4sXvsGjRApdtxV3b\nqamphIU5hjFCQkJKnd9kMmEv+s82LS0VTdMIC2vsso9Op6NJk6aevCQAgoNrlSrT6/Xs2bObzZu/\n4dixoxw/nuIMUw0bXnyIplhoaKjLzyaTST7k3KArca+ViADIt1lIN+eSbs7hrCUPWwXbUinFWUse\nZy15HMo9ja/eQB1TIHWMAYSa/DHITd+E8AoJGl7WNqhhtZ+j4Ymyxr2LX56ujO5pe9G9MCZMeJi2\nbcu+BDQ8PJzi9bN0uku/6Rc/f2FhYRn18Hyhp7Lq/tZbr5GQsJbrr48kKqo9gwYNoX37Dsyf/xon\nT570+LmEZ/z0Rpr4hdDELwSbspNpySfdnEO6OZe8cm5nX1KBzcrx/EyO52ei0zRCjH7UNQVSxxQg\nq5MKUYkkaHiZSWfwaA5EdZeaeqJUWVLSMYBSvQxw/j9/X1+/UpNF9+3bQ3Z2Nj4+PhQUlA4OZQkL\na4xSipSUpDLqluoy7HI50tLSSEhYy+DBt/HMMzNdtsnwx5Wn13TOORgAeTYz6eZczphzyLTkVzjc\n25Vy3mkWwF9vLDpvICFGP7nFvRCXQf56hEcOHNjHwYP7nT9nZKSzceMGoqM7Ehxceo5EZGQb6tSp\ny7p1/yA///y8kNzcHJ5//mnmzXsRg6Hiuff66yNp2DCMzz6Ld+nV2Ljx32Rllb/aZ3GPSXm9H+fO\nZQHQvHkLl/LExG2kpCRjs1160SlRtfz1Jpr6hdKxVlN61bmODsGNaewXgq/evf+p8mwWkvMz2ZmV\nwtb0Q+zKSiElP5N8N3pMhBAO0qMhPBIUFMy0aVMYOXI0Op2e9evXYbPZmDLliTL3NxgMTJ36JDNn\nPsP9949h6NBhmEwmPv98PadOneSFF+aUO1xyoccfn84zzzzJpEnjGTLkdk6fPklCwlpq1So9z+JC\nISGhKKX46KPVdOvWg549e5e5X4sW4TRo0JAPP1xBYWEh9evXZ/fu3fz731/i4+NDXl75k0HFlWHQ\ndM61OpSqT25Rb0e6OZdMa36F58nYlOKMOZczRXOtAgwmR2+H0dHbIYuFCXFpEjSEBzS6detBZGQb\nPv74Q7Kzs2jXrj1z5rxKRERr1z1LvAn36dOft95axKpVy1m1ahmapiM8vBWvvDKf7t1vuuhxFyvv\n0aMnr7/+NsuWLWHJkkXUrVuPGTNeICFhbblrJwwYMJAtW75jw4Yv2LXrN2fQuPAwo9HIG2+8w8KF\nb7Fu3acopWjcuAlTp07HarWwYMGbHDiwj+uvjyyz3hV5HcL7NE0j0OBDoMGH5v61sdhtnLXkOYNH\necuhl5RrNZNrNZPEWQw6HbWN/s7g4eNmz4kQ1wJNyfT3Cjl9+tyVrsJVr169IGlnL5M2Lk0pRY6t\nkDNFoSPbmo+n74pBBh/C69VDlwfBRj+5ksVL5Pe4atSrF1Qp55H4LYS4pmmaRpDBlyCDLy3962C2\nW8kw55FuySXDnIu56IqpijhnLeRQ9ilyc81omkawwYdaRj9CDP6EGP1kwTBxTZKgIYQQJZh0Bhr6\nBtPQNxi7UpyzFjiGWCy5ZFsKKnwepRRZlgKyLAUkcRaAQIMPIUY/x5fBX4ZaxDVBfsuFEOIidCWW\nRQ+nLoV2KxlFQyzpltwKL41eLMdaSI61kJR8x5VR/nojIUZ/Z/jw1Rll/o646kjQEEKICvLRGWjk\nW4tGvrWwK0WWNd85oTTHWrE1YErKs1nIs2VxoiDLeX5H6HCEjwC9SYKHqPEkaAghhAeKV9cNNfpz\nXUA9CmwW0i252P0gqSCdAg/WWCm0WzlZeI6ThY6JjkadnlpGP0INjh6PQIOvXE4rahwJGkIIUQl8\n9UYa60OoVzeIpiqEfJuFTEseWZZ8Mq355FrNbp/TYrdxpjCHM4U5gCPcBBl8CDb4EmzwI8jgg7/0\neohqToKGEEJ4gZ/eiJ/eMcwCjt6KLEs+mZY8Mi355NgK3b6M1l5igik45nkYdDqCi66aCS768tEZ\nJHyIakOChhBCVAEfnYH6PkHU93GsTWCx28iy5pNpcXydsxZ4dONFq93ucp+W4ucKMp4PHsEGX7m0\nVlwxEjSEEOIKMOr01DUFUtcUCIBN2cm2FJBpdfR4ZFnysXm4clih3UphiSEXcFzhEmTwJbgogAQa\nfGVBMVElJGgIIUQ1oNd0hJr8CTX5AzjX8Miy5HPWmk+2Jd+txcMu5LjCxeKcaKppEKD3cQ67BBpM\nBOh9pOdDVDoJGkIIUQ2VXMOjGY4FwArsVrKtBWRb8zlnKSTbWoCtnDsQX4xS59f1gCxnua/eQIDe\ncV+YQL0PAQYT/noTeun9EB6SoCGEEDWApmlFE0yNNCia52FXijybmXPWgqIAUkCOtdCjuR7FCmxW\nCmxW0ovuVlv83P56Y1HwcASQQINJFhgTFSJBQ7ht+PChhIU15p133quU/SpbRZ/37Nmz+Pn54evr\n65V6WK1WMjPPUrduPa+cXwhdibvSNsJxdYtN2cm1FjqDR7a1gDyb2eMbxYGjN6X4rrUUnr+ZmV7T\nOYdcAg0+RY8mTDr5aBHnyW+DcFtF/4O5Uv/pVOR5ExO38+KLz7Nixcc0bNiw0uuQlpbGE09M5r77\nxjN48G2Vfn4hLkav6Qg2+hFs9HOWWe02zhWFj+Lej3yb5bKfy6bsJS63Pc9HZyCgKID460346434\n6U346Ayy4Ng1SIKGuCbt3bub3Nyc8nf0UGrqcZKTk7x2fiHcYdDpXSaaApiL5nucsxaQYzWTayu8\n7J6PYoV2K4VmKxnkuZTriod/dEb89Sb89Cb89Eb89UZ8dEYJIVcpCRrimqQq4930Cp5fiMtl0hlc\nLq8FRw9Fns3smCRqc0wUzbWaKbS7v5x6WezFQzCYgVyXbSVDiJ/eVBREJIRcDSRoCI99+eU/Wb16\nOWfOnOG66yJ48MGHuPHGbpc85s8/f2fp0vfYs2c3AFFR7Zkw4WHatGnn3GfEiNvp2rU7HTrE8OGH\nKzh+/Dj16zdg5MjR3HnnCJfzbd78DXFxK0lKOkaTJk2ZMOGRcus9d+5sNmz4Ek3TGDFiKB07dnbO\n5zh69Ajvv7+IHTt+xWq1EBHRmvHjJ7i8LovFwuLF77B9+xbOnDlNSEgoPXv2ZsKERwgKCmLDhi+Z\nO3c2mqYxd+5s5s17kS1bfq5wuwpxpeg1HUFFl7uWZLZbHQGhKHzk2Bzfu3v32kspL4T46ozOIZgc\n30LyCs2YdAZ8dHpMOoNcFVONSdDwshxrIQmpO/lf3hmsHl6G5g0GTUe4f13ubBRDoMHH7eP37t3D\n3r27GTFiNCEhIfzznwlMn/4Y8+fH0rlzlzKP+eWX//DUU48TEdGaCRMexmIx89VXXzB58kTefnsR\n/fv3cu77n//8yHffbWL48FGEhtbm888TePvt1wkLa0y3bj0A+OqrL5g370Xat4/mkUceIyUlmRde\nmIGmQaNGYRet+1//ehe5uTls3foDjz02jRYtwgE4fPgQkyc/SJ06dRk79n4MBgObNn3N9OmPMXPm\ny/TrNwCA+fNfZdOmbxg5cjRhYY353/8OEx//KSkpKcyfv5Do6I7cd994PvxwBX/9651ER3d0u32F\nqE5MOgMmk4FQzg+9FF9um2MtdAaQXJuZXJu50nv0iq+uybM5QkhGRi65ua73jjHodJg0R+hwfOnP\nP2rnA4lRp5dQUsUkaHhZQupODuSeutLVKMWq7BzIPUVC6k7GNu3q9vGFhQW8/voCunbtDsCgQbcx\nevQdvPvuQpYuXV1qf6UUr78+j3bt2hMbu8RZftddIxk37h7efvsNl6Bx+vQpVqz4mPDwVgD07t2H\nYcMGs3HjBrp164Hdbue992Jp2zaKhQvfR693LDJ0/fWtmTt39iXr3q5dFK1aRbB16w/07NnHORn0\nrbdeIySkNsuXf4SPjyN8DR9+N1OmPMSCBW/Qu3cfDAYDGzf+m9tu+ysTJjzsPKe/vz//+c+PFBQU\nEBbWmC5duvLhhyto1649t9wyyO32FaK6K3m5bT3OD78Uh4Icq2POR77NTJ7dQr7NguUyFhwrj9Vu\nx4qdvApMcjXodPjoDJi0CwJJ0WPxNqNOL0M2lUCChpcl5Z+90lW4JE/r17JlK2fIAAgODubWW/9C\nfPynnD2bQWhobZf99+/fR2rqCe68cwRZWZnOcqXgppt6sXbtJ5w6dQpNc8yUb9q0uTNkANSuXYfQ\n0Nqkp6cXnW8vZ89m8MADk5whA2DgwL+wcOFbbr+e7Owsdu3awfDhoygoyKegIN+5rVevm1m0aAH7\n9u0hKqoD9erVZ9Omb2jdug29evUhMDCQBx6YxAMPTHL7eYW42pS85PZCFrutKHxYHI92iyOIeDmE\nXMhqt2O1Fw/TXFqpIKK5/uxT1Eti1CSUXIwEDS9r5hdaLXs0ijXzC/XsuGbNS5U1btwYgNTUE6WC\nxokTxwFYvPgdFi1a4LKt+HLU1NRUwsIcwxghISGlzm8ymbAXjQmnpaWiaRphYY1d9tHpdDRp0tTt\n13P8eAoA8fGfsm7dP0pt1zSNkyfTiIrqwJNPzmDmzBnMm/cir746h6ioDvTu3YchQ24nICCw1LFC\nCAejTk8tnWO10wtZ7DZn6Mi3Fz0WhZLLWXr9cpnttgo9v6Zx0R6S4t4RDQ2dpqEretQAnaZDh4am\nUVSuQ+PKLQ/gDRI0vOzORjHVfo6GJ8r6IygeltWVca8Ee9Ef6oQJD9O2bVSZ5wwPD6egoPgclx5D\nLX7+wsLCMurhfjvbbI5j7rxzBL169Slzn5YtHSGoc+cuxMd/ybZtW/nxx6388stPLFz4FmvWfMKy\nZR9Sq1bpkCSEuDSjTo9R57r+RzFHCCnuBTFjCjRy2nIOi7JhtlsptNuu+JVeSkGhshZdoVP6fcld\njiCioS961JV4dH5fHFCKwooOx3uj4+ei73E9znFMybBTdrkOjXoEXfbrAAkaXhdo8PFoDkR1l5p6\nolRZUtIxgFK9DAANGzomZ/r6+pWaLLpv3x6ys7Px8fGhoKBif6BhYY1RSpGSUnqtitTUVJdhl4po\n1KgRAHq9vlT9jh49QmrqcXx9fbFYLBw8uJ/69RvQv/8t9O9/CwCffBLHu+++w6ZN33DXXSPdem4h\nxKU5QoieYKPjaph6dYI4bT+/QqlSqih0FH9ZMSvr+e9LPiprpawV4m2OZeQVtitY1xaN6lbKeSRo\nCI8cOLCPgwf3ExHRGoCMjHQ2btxAdHRHgoODS+0fGdmGOnXqsm7dP7jttr/i5+f4ryU3N4fnn38a\nq9XK4ME/UNH/BK6/PpKGDcP47LN47rhjhHPy5saN/3aZA3IxxT0mxb0fderUJTKyDV999SWjR4+l\nbl3HH5jVamXu3Nn873+HSEj4iry8LB566H7uvHMEU6dOd3l9Sin0el3R+fVF568B72hC1HCaphUN\nW5T/kWZ3hpKygogVs7K5lIvLJ0FDeCQoKJhp06YwcuRodDo969evw2azMWXKE2XubzAYmDr1SWbO\nfIb77x/D0KHDMJlMfP75ek6dOskLL8wpd7jkQo8/Pp1nnnmSSZPGM2TI7Zw+fZKEhLXUqlWr3GND\nQkJRSvHRR6vp1q0HPXv25rHHpjN16sM88MC93HHHcGrVCmHjxn+zb98eHnroUWeAuvXWwaxfv468\nvDzat48mMzOT9evXUqdOXfr1c/RwhIY6hk++/vor7HY7f/nLULdfnxCi8uk0DR/NMYmzPHalMNut\nWJSNwgsDSXHvSdFwSWWuKXK1kaAhPKDRrVsPIiPb8PHHH5KdnUW7du2ZM+dVZw+Hc88Sczn69OnP\nW28tYtWq5axatQxN0xEe3opXXplP9+43XfS4i5X36NGT119/m2XLlrBkySLq1q3HjBkvkJCwttyJ\nVAMGDGTLlu/YsOELdu78jZ49exMV1Z53313GsmXv8+mnH2O1WmnWrDnPPjuLgQP/4jz2qaeeJSys\nMZs3f8O3327E19ePG264kQkTHiY42BFymjVrwfDho9iw4Qv27dtDp043lDmkJISovnSahq/eiC/G\ncmcr2JQds92GpWjOiGPo5nw4sSo7dqVQqKJHsCs7dhRKgb2o3PHz1dUTqqmr7RV5yenT58rfSVyW\nevWCpJ29TNrY+6SNve9qb2NVInDYncGkdECxK5zlJfcvedzFz3N++4U/q6Lnue369pXyeqRHQwgh\nhKhGNE1DjwZXyRWuMmgshBBCCK+RoCGEEEIIr5GgIYQQQgivkaAhhBBCCK+RoCGEEEIIr5GgIYQQ\nQgivkaAhhBBCCK+RoCGEEEIIr5GgIYQQQgivkaAhhBBCCK+RoCGEEEIIr6m29zpZs2YNy5YtIy0t\njTZt2vD0008TExNT5r79+vXjxIkTZW77v//7PyZPngzA0KFDOXjwoMv20NBQEhMTK7fyQgghhACq\nadBYv349s2bN4tFHHyUqKoq4uDgefPBB/vnPf9K4celbbS9evBiz2exStnz5crZu3cqQIUMAsFgs\nHDlyhOnTp9OlSxfnfgZDtWwCIYQQ4qpQLT9lFy5cyKhRo3jkkUcA6NGjB4MGDWLlypU8++yzpfaP\njIx0+fmPP/5g06ZNzJkzhxYtWgBw+PBhbDYb/fv3d5YJIYQQwruq3RyNY8eOceLECfr27essMxgM\n9OnTh61bt1boHC+//DLR0dEMGzbMWbZv3z58fX1p3rx5pddZCCGEEGWrdj0aR48eRdO0UoGgSZMm\nJCcno5RC07SLHr9p0yZ27drFp59+6lK+f/9+goODeeyxx9i+fTuapjFo0CBmzJhBQECAV16LEEII\nca2rdkEjJycHoNSHf0BAAHa7nby8vEsGg1WrVtG5c2c6dOjgUn7gwAHS09Np27Yt48aNY9++fSxY\nsIDjx4+zYsWKyn8hQgghhKh+QUMpBXDRXgud7uKjPUeOHOGXX35h4cKFpbZNnz4ds9nsDCCdO3cm\nNDSUadOm8euvv9K5c+dKqL0QQgghSqp2QSMoKAiA3Nxcateu7SzPzc1Fr9fj5+d30WM3bdpEQEAA\nN998c6ltF04YBejduzdKKfbt21du0KhXL6iiL0FcBmln75M29j5pY++TNq45qt1k0ObNm6OUIjk5\n2aU8JSWl3KtFtm3bRu/evTGZTC7lNpuN9evXs3fvXpfygoICwLGWhhBCCCEqX7ULGi1atKBRo0Zs\n2rTJWWaxWPj+++/p3r37JY/9888/iY6OLlWu1+tZuHAhsbGxLuVff/01RqORjh07Vk7lhRBCCOGi\n2g2dAEyYMIE5c+YQFBREp06diIuLIzMzk3HjxgGQnJxMRkaGS6g4fvw4ubm5tGzZssxzPvTQQ8yc\nOZOXX36Zfv368fvvv7N48WLGjh1Lo0aNquR1CSGEENeaahk07rnnHsxmM6tXr2b16tVERkayfPly\nmjRpAjhWAv3ss89chkLS09PRNI3g4OAyzzly5EhMJhMrVqxg7dq11K1bl8mTJzNx4sQqeU1CCCHE\ntUhTxZd5CCGEEEJUsmo3R0MIIYQQVw8JGjjuFDtw4ECio6MZNWoUO3fuvOT+Bw8eZNy4cXTs2JG+\nffvywQcfVFFNay532/i3335j7NixdOnShV69evH3v/+d9PT0KqptzeRuG5cUGxtb5iXgojR32zkj\nI4OnnnqKrl270qVLFx5++OFSV9UJV568X9xzzz106tSJAQMGEBsbi9VqraLa1mybN2+mU6dO5e53\nWZ976hqXkJCg2rRpoxYtWqR++OEHNWHCBNW5c2eVkpJS5v7p6enqpptuUvfff7/64Ycf1Lvvvqva\ntm2rli9fXsU1rzncbeNDhw6pDh06qEceeURt2bJFffnll2rAgAFq2LBhymq1VnHtawZ327ik/fv3\nq6ioKBUZGVkFNa3Z3G1ni8Wibr/9djV48GC1ceNGtWnTJjVkyBA1cOBAZbFYqrj2NYO7bZyUlKRi\nYmLUhAkT1Pbt21VcXJyKjo5Wr776ahXXvOb59ddfVadOnVTHjh0vud/lfu5d80Gjb9++avbs2c6f\nLRaL6t+/v5ozZ06Z+y9YsEB169ZNFRYWOsvefvtt1bVrV/kQvAh323j27NlqwIABLu35+++/q9at\nW6sffvjB6/Wtidxt42I2m00NHz5c3XzzzRI0KsDddl6zZo2KiYlRaWlpzrK9e/eqXr16qd27d3u9\nvjWRu238/vvvq+joaFVQUOAsmz9/vurcubPX61pTFRYWqiVLlqioqCh14403lhs0Lvdz75oeOvHk\nTrGJiYl0797dZVGwAQMGkJWVxR9//OH1Otc0nrRxREQE48ePR6/XO8uKL1tOSUnxboVroMu54/GK\nFSvIy8vj3nvv9XY1azxP2nnz5s306tWLBg0aOMsiIyPZsmULbdu29XqdaxpP2thisWAwGPDx8XGW\n1apVi7y8PMxms9frXBNt2bKFpUuX8vTTT1fob/9yP/eu6aBRkTvFlnVMs2bNXMqaNm2KUoqjR496\ns7o1kidtPHr0aO655x6Xsm+//RZN0wgPD/dqfWsiT9oYHG/qsbGxzJkzB6PRWBVVrdE8aef9+/fT\nsmVLYmNj6dmzJ+3bt2fSpEmkpqZWVbVrFE/a+Pbbb0ev1/PGG2+QlZXF77//zurVq7nllltKrRIt\nHDp06MDmzZsZM2bMJe+GXuxyP/eu6aBRkTvFlnVMWfuXPJ84z5M2vlBqaiqvvfYa7du3p1u3bl6p\nZ03maRs/99xz3HHHHbIybgV50s4ZGRnEx8ezbds25s6dy+uvv86hQ4eYNGkSdru9Supdk3jSxk2b\nNmX69OksX76crl27MnLkSOrUqcPcuXOrpM41Uf369QkMDKzw/pf7uVctF+yqKsqDO8UqpS66f0WS\n4bXGkzYuKTU1lb/97W8AzJ8/v1LrdrXwpI0/+eQTkpOTef/9971at6uJJ+1stVqxWq0sXbrU+cbe\npEkThg8fzjfffMOgQYO8V+EayJM2Xrt2Lc8//zyjRo1i8ODBnDp1infeeYeJEyeycuVK6a2rBJf7\nuXdN92iUvFNsSZe6U2xQUFCZ+5c8nzjPkzYuduDAAUaNGkVeXh4rVqxwrgwrXLnbxmlpabzxxhs8\n++yz+Pj4YLPZnP9d22y2iw61XOs8+V329/cnOjra5b/HqKgogoODOXDggHcrXAN50sYffPABffr0\nYdasWXTt2pWhQ4fy/vvv8+uvv/LFF19USb2vdpf7uXdNBw1P7hTbvHnzUvsX/3yx+6xcyzy9G++u\nXbu49957MRqNfPzxx0RERHi5pjWXu22cmJhIXl4eU6ZMoV27drRr145XX30VpRRRUVEsWrSoimpe\ns3jyu9ysWTMsFkupcqvVKj2gZfCkjVNTU0vdTDM8PJyQkBAOHTrkrapeUy73c++aDhqe3Cm2e/fu\nJCYmOm8xD7Bx40ZCQ0Np06aN1+tc03jSxikpKUycOJH69evzj3/8g6ZNm1ZVdWskd9u4X79+rFu3\njnXr1hEfH098fDzjx49H0zTi4+O5++67q7L6NYYnv8s9e/bkt99+4/Tp086yn3/+mby8vAotknSt\n8aSNW7RowY4dO1zKjh07RmZmprx3VJLL/dzTz5o1a5YX61ftmUwmFi9ejNlsxmw2M2/ePI4ePcor\nr7xCcHAwycnJHD16lIYNGwLQqlUrVq9eTWJiIrVr12bDhg289957TJkyRd44LsLdNv773//OoUOH\neOaZZwA4efKk80uv15ealCTca2NfX1/q16/v8nX48GG2bdvG7NmzpX0vwd3f5datWxMfH8/mzZup\nW7cuu3fvZtasWURGRjJ16tQr/GqqJ3fbuHbt2ixZsoS0tDT8/PzYsWMHL7zwAsHBwcyaNUvmaJTj\n559/ZseOHUyaNMlZVumfe+4s8nG1WrFiherbt6+KiYlRo0aNUrt27XJue/rpp0stZPTnn3+q0aNH\nqw4dOqi+ffuqpUuXVnWVa5yKtrHFYlHt2rVTkZGRZX7JCqwX5+7vcUkrV66UBbsqyN12TkpKUpMn\nT1adOnVSN954o5oxY4Y6d+5cVVe7RnG3jTdu3KjuuOMO1b59e9W3b1/13HPPqfT09Kqudo20cOFC\n1alTJ5eyyv7ck7u3CiGEEMJrruk5GkIIIYTwLgkaQgghhPAaCRpCCCGE8BoJGkIIIYTwGgkaQggh\nhPAaCRpCCCGE8BoJGkIIIYTwGgkaQgivio2NJTIykjFjxlTK+SIjI4mMjCQxMbFSzieE8C4J8X9q\n5gAABq1JREFUGkKIGkduSCZEzSFBQwghhBBeI0FDCCGEEF4jQUMIIYQQXmO40hUQQtQ8v/zyC2vW\nrGHnzp2cOXMGq9VKaGgoMTExjB49mu7du5d7jn79+nHixAk+++wz0tLSeO+99zhw4AAmk4nWrVtz\n3333MWDAgEue49tvv2X16tXs3r0bq9VKkyZNGDp0KOPHjy/z9uCnT58mLi6OH3/8kaSkJHJzcwkI\nCKBly5YMHDiQe+65Bx8fH4/bRQhRmty9VQjhljfffJMPPvgATdOoXbs2jRo14ty5cxw/fhyr1QrA\nSy+9xIgRIwDHVSexsbF07tyZjz76yHmefv36kZqaypgxY4iLi8PHx4eIiAjOnDnDyZMnUUpx3333\n8eyzz7o8f2RkJJqmccMNN/DLL78QEBBA8+bNOXXqFOnp6Sil6NGjB8uXL3c5bufOnUycOJHs7Gx8\nfX1p2rQpRqORlJQUzp07h1KKLl26sHr1aplsKkQlkqETIUSF/fTTT3zwwQfo9XrmzZvH9u3bWbdu\nHV9//TWbN2/mxhtvBGDBggUVPmdcXBw33XQT33//PevWreP777/nxRdfxGAwEBcXx1dffVXmcf/9\n73958MEH2b59OwkJCWzbto3HH38cgMTERLZu3erc1263M336dM6dO8ett97K1q1b+eKLL0hISCAx\nMZFp06Y5z7llyxZPm0cIUQYJGkKICtu2bRsmk4lbbrmFYcOGuWxr0KABU6ZMASA9PZ309PRyz6eU\nIiwsjNjYWEJDQ53lI0aM4IEHHkApRWxsbJnH3nTTTTz55JP4+vo6yyZOnEjLli0BR2gotm/fPrKz\ns/Hx8eGll14iKCjIuU2v1/Pggw/StGlTAA4cOFBuvYUQFSdBQwhRYdOmTeP333/ntddeK3N7yQ/9\n/Pz8cs+naRp33303fn5+pbaNGjUKgCNHjnD06NFS22+99dYyzxkREQHA2bNnnWVt27blp59+4uef\nf6ZWrVqljjGbzc7ygoKCcusthKg4mQwqhHCbpmn897//5fDhwyQnJ5OUlMT+/fs5duyYc5+KTv/q\n0KFDmeWNGjUiKCiInJwcjh49SosWLVy2N2jQoMzjAgICUEqVGRhMJhNHjhxhz549JCUlkZyczKFD\nh9i/fz+FhYVomobdbq9QvYUQFSNBQwjhlqVLl7JkyRKys7OdkyY1TaNly5YMGzaMzz77zK3zldXD\nUMzf35+cnByys7NLbTOZTG49z65du5g1axZ79+511hkgNDSUm2++mT179nD8+HG3zimEKJ8EDSFE\nhRVfQaJpGkOGDKFXr15EREQQHh6On58fx44dczto5OXlXXRbTk4OAHXq1Lmseh8+fJhx48ZRWFhI\nREQEd911F61bt6ZVq1bUr18fgNGjR0vQEMILJGgIISrEarWyfPlyNE1j8uTJPProo6X2SUtLc/u8\nBw8e5IYbbihVnpycTG5uLpqmcd1113lU52KrV6+moKCAVq1asW7dujLXyjh58uRlPYcQomwyGVQI\nUSFnz5519j60a9euzH3WrFnj/L54TY3yJCQklFn+8ccfAxATE3PR+RgVlZKSgqZptGrVqsyQsX37\ndk6cOAGAzWa7rOcSQriSoCGEqJDatWtTq1YtlFKsXLmSrKws57aMjAxmzZrFv/71L2dZRa7eUErx\n559/8txzzzn3V0oRFxfHqlWr0DSNJ554wu26XrjgVnh4OEoptm3bxq+//uost9lsfPnllzzxxBPO\nYypytYwQouJk6EQIUSF6vZ6pU6fy4osv8vPPP9OnTx9atGiB2Wzm2LFj2Gw22rZtS2pqKpmZmaSl\npdGmTZtLnlPTNCIiIoiPj2fDhg2Eh4eTmprKmTNn0Ov1zJgxgy5durhd1wuveLn//vv517/+xdmz\nZxkzZgzNmzcnMDCQlJQUsrKyCAgIICYmhh07dsgQihCVTHo0hBAVNnr0aFauXEmPHj0IDg7m4MGD\nZGRkEBMTw8yZM1m7di0333wzAN99953zOE3TLrqs99ixY3nzzTcJDw/n4MGD6HQ6Bg8ezCeffMJ9\n991X5jHlLRF+4fM1atSIzz//nNGjR9OyZUtOnjzJkSNHqFevHmPHjuXzzz9n6tSpaJrGTz/9JGtp\nCFGJ5F4nQogrovheJy+99BLDhw+/0tURQniJ9GgIIYQQwmskaAghhBDCayRoCCGEEMJrJGgIIYQQ\nwmtkMqgQQgghvEZ6NIQQQgjhNRI0hBBCCOE1EjSEEEII4TUSNIQQQgjhNRI0hBBCCOE1EjSEEEII\n4TX/D2O9akZdw3TbAAAAAElFTkSuQmCC\n", 180 | "text/plain": [ 181 | "" 182 | ] 183 | }, 184 | "metadata": {}, 185 | "output_type": "display_data" 186 | } 187 | ], 188 | "source": [ 189 | "%matplotlib inline\n", 190 | "import matplotlib.pyplot as plt\n", 191 | "import seaborn as sns\n", 192 | "sns.set()\n", 193 | "\n", 194 | "# alpha_set\n", 195 | "rmse_blend_testlist = [np.mean(rmse_blendCF[alpha]) for alpha in alpha_set]\n", 196 | "rmse_blend_trainlist = [np.mean(rmse_blendCF_train[alpha]) for alpha in alpha_set]\n", 197 | "\n", 198 | "pal = sns.color_palette(\"Set2\", 2)\n", 199 | "\n", 200 | "plt.figure(figsize=(8, 8))\n", 201 | "plt.plot(alpha_set, rmse_blend_trainlist, c=pal[0], label='blend train', alpha=0.5, linewidth=5)\n", 202 | "plt.plot(alpha_set, rmse_blend_testlist, c=pal[0], label='blend test', linewidth=5)\n", 203 | "\n", 204 | "plt.legend(loc='best', fontsize=18)\n", 205 | "plt.xticks(fontsize=16);\n", 206 | "plt.yticks(fontsize=16);\n", 207 | "plt.xlabel('alpha', fontsize=25);\n", 208 | "plt.ylabel('RMSE', fontsize=25);" 209 | ] 210 | } 211 | ], 212 | "metadata": { 213 | "kernelspec": { 214 | "display_name": "Python 3", 215 | "language": "python", 216 | "name": "python3" 217 | }, 218 | "language_info": { 219 | "codemirror_mode": { 220 | "name": "ipython", 221 | "version": 3 222 | }, 223 | "file_extension": ".py", 224 | "mimetype": "text/x-python", 225 | "name": "python", 226 | "nbconvert_exporter": "python", 227 | "pygments_lexer": "ipython3", 228 | "version": "3.5.1" 229 | } 230 | }, 231 | "nbformat": 4, 232 | "nbformat_minor": 0 233 | } 234 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import var 4 | import predict as pre 5 | import utils 6 | 7 | print('初始化变量...') 8 | names = ['user_id', 'item_id', 'rating', 'timestamp'] 9 | direct = 'dataset/ml-100k/' 10 | trainingset_files = (direct + name for name in ('u1.base', 'u2.base', 'u3.base', 'u4.base', 'u5.base')) 11 | testset_files = (direct + name for name in ('u1.test', 'u2.test', 'u3.test', 'u4.test', 'u5.test')) 12 | 13 | if __name__ == '__main__': 14 | 15 | rmse_baseline = [] 16 | rmse_itemCF = [] 17 | rmse_userCF = [] 18 | rmse_itemCF_baseline = [] 19 | rmse_userCF_baseline = [] 20 | rmse_itemCF_bias = [] 21 | rmse_topkCF_item = [] 22 | rmse_topkCF_user = [] 23 | rmse_normCF_item = [] 24 | rmse_normCF_user = [] 25 | rmse_blend = [] 26 | i = 0 27 | nums = 5 28 | for trainingset_file, testset_file in zip(trainingset_files, testset_files): 29 | i += 1 30 | print('------ 第%d/%d组样本 ------' % (i, nums)) 31 | df = pd.read_csv(trainingset_file, sep='\t', names=names) 32 | 33 | var.ratings = np.zeros((var.n_users, var.n_items)) 34 | print('载入训练集' + trainingset_file) 35 | for row in df.itertuples(): 36 | var.ratings[row[1]-1, row[2]-1] = row[3] 37 | 38 | print('训练集规模为 %d' % len(df)) 39 | 40 | sparsity = utils.cal_sparsity() 41 | print('训练集矩阵密度为 {:4.2f}%'.format(sparsity)) 42 | 43 | print('计算训练集各项统计数据...') 44 | utils.cal_mean() 45 | 46 | print('计算相似度矩阵...') 47 | var.user_similarity = utils.cal_similarity(kind='user') 48 | var.item_similarity = utils.cal_similarity(kind='item') 49 | var.user_similarity_norm = utils.cal_similarity_norm(kind='user') 50 | var.item_similarity_norm = utils.cal_similarity_norm(kind='item') 51 | print('计算完成') 52 | 53 | print('载入测试集' + testset_file) 54 | test_df = pd.read_csv(testset_file, sep='\t', names=names) 55 | predictions_baseline = [] 56 | predictions_itemCF = [] 57 | predictions_userCF = [] 58 | predictions_itemCF_baseline = [] 59 | predictions_userCF_baseline = [] 60 | predictions_itemCF_bias = [] 61 | predictions_topkCF_item = [] 62 | predictions_topkCF_user = [] 63 | predictions_normCF_item = [] 64 | predictions_normCF_user = [] 65 | predictions_blend = [] 66 | targets = [] 67 | print('测试集规模为 %d' % len(test_df)) 68 | print('测试中...') 69 | for row in test_df.itertuples(): 70 | user, item, actual = row[1]-1, row[2]-1, row[3] 71 | predictions_baseline.append(pre.predict_baseline(user, item)) 72 | predictions_itemCF.append(pre.predict_itemCF(user, item)) 73 | predictions_userCF.append(pre.predict_userCF(user, item)) 74 | predictions_itemCF_baseline.append(pre.predict_itemCF_baseline(user, item)) 75 | predictions_userCF_baseline.append(pre.predict_userCF_baseline(user, item)) 76 | predictions_itemCF_bias.append(pre.predict_itemCF_bias(user, item)) 77 | predictions_topkCF_item.append(pre.predict_topkCF_item(user, item, 20)) 78 | predictions_topkCF_user.append(pre.predict_topkCF_user(user, item, 30)) 79 | predictions_normCF_item.append(pre.predict_normCF_item(user, item, 20)) 80 | predictions_normCF_user.append(pre.predict_normCF_user(user, item, 30)) 81 | predictions_blend.append(pre.predict_blend(user, item, 20, 30, 0.7)) 82 | targets.append(actual) 83 | 84 | rmse_baseline.append(utils.rmse(np.array(predictions_baseline), np.array(targets))) 85 | rmse_itemCF.append(utils.rmse(np.array(predictions_itemCF), np.array(targets))) 86 | rmse_userCF.append(utils.rmse(np.array(predictions_userCF), np.array(targets))) 87 | rmse_itemCF_baseline.append(utils.rmse(np.array(predictions_itemCF_baseline), np.array(targets))) 88 | rmse_userCF_baseline.append(utils.rmse(np.array(predictions_userCF_baseline), np.array(targets))) 89 | rmse_itemCF_bias.append(utils.rmse(np.array(predictions_itemCF_bias), np.array(targets))) 90 | rmse_topkCF_item.append(utils.rmse(np.array(predictions_topkCF_item), np.array(targets))) 91 | rmse_topkCF_user.append(utils.rmse(np.array(predictions_topkCF_user), np.array(targets))) 92 | rmse_normCF_item.append(utils.rmse(np.array(predictions_normCF_item), np.array(targets))) 93 | rmse_normCF_user.append(utils.rmse(np.array(predictions_normCF_user), np.array(targets))) 94 | rmse_blend.append(utils.rmse(np.array(predictions_blend), np.array(targets))) 95 | print('测试完成') 96 | print('------ 测试结果 ------') 97 | print('各方法在交叉验证下的RMSE值:') 98 | print('baseline: %.4f' % np.mean(rmse_baseline)) 99 | print('itemCF: %.4f' % np.mean(rmse_itemCF)) 100 | print('userCF: %.4f' % np.mean(rmse_userCF)) 101 | print('itemCF_baseline: %.4f' % np.mean(rmse_itemCF_baseline)) 102 | print('userCF_baseline: %.4f' % np.mean(rmse_userCF_baseline)) 103 | print('itemCF_bias: %.4f' % np.mean(rmse_itemCF_bias)) 104 | print('topkCF(item, k=20): %.4f' % np.mean(rmse_topkCF_item)) 105 | print('topkCF(user, k=30): %.4f' % np.mean(rmse_topkCF_user)) 106 | print('normCF(item, k=20): %.4f' % np.mean(rmse_normCF_item)) 107 | print('normCF(user, k=30): %.4f' % np.mean(rmse_normCF_user)) 108 | print('blend (alpha=0.7): %.4f' % np.mean(rmse_blend)) 109 | print('交叉验证运行完成') 110 | -------------------------------------------------------------------------------- /predict.py: -------------------------------------------------------------------------------- 1 | import var 2 | import numpy as np 3 | 4 | def predict_baseline(user, item): 5 | '''baseline''' 6 | prediction = var.item_mean[item] + var.user_mean[user] - var.all_mean 7 | return prediction 8 | 9 | def predict_itemCF(user, item): 10 | '''item-item协同过滤算法''' 11 | nzero = var.ratings[user].nonzero()[0] 12 | prediction = var.ratings[user, nzero].dot(var.item_similarity[item, nzero])\ 13 | / sum(var.item_similarity[item, nzero]) 14 | return prediction 15 | 16 | def predict_userCF(user, item): 17 | '''user-user协同过滤算法''' 18 | nzero = var.ratings[:,item].nonzero()[0] 19 | prediction = (var.ratings[nzero, item]).dot(var.user_similarity[user, nzero])\ 20 | / sum(var.user_similarity[user, nzero]) 21 | if np.isnan(prediction): 22 | baseline = var.user_mean + var.item_mean[item] - var.all_mean 23 | prediction = baseline[user] 24 | return prediction 25 | 26 | def predict_itemCF_baseline(user, item): 27 | '''结合baseline的item-item CF算法''' 28 | nzero = var.ratings[user].nonzero()[0] 29 | baseline = var.item_mean + var.user_mean[user] - var.all_mean 30 | prediction = (var.ratings[user, nzero] - baseline[nzero]).dot(var.item_similarity[item, nzero])\ 31 | / sum(var.item_similarity[item, nzero]) + baseline[item] 32 | return prediction 33 | 34 | def predict_userCF_baseline(user, item): 35 | '''结合baseline的user-user协同过滤算法,预测rating''' 36 | nzero = var.ratings[:,item].nonzero()[0] 37 | baseline = var.user_mean + var.item_mean[item] - var.all_mean 38 | prediction = (var.ratings[nzero, item] - baseline[nzero]).dot(var.user_similarity[user, nzero])\ 39 | / sum(var.user_similarity[user, nzero]) + baseline[user] 40 | if np.isnan(prediction): prediction = baseline[user] 41 | return prediction 42 | 43 | def predict_itemCF_bias(user, item): 44 | '''结合baseline的item-item CF算法,预测rating''' 45 | nzero = var.ratings[user].nonzero()[0] 46 | baseline = var.item_mean + var.user_mean[user] - var.all_mean 47 | prediction = (var.ratings[user, nzero] - baseline[nzero]).dot(var.item_similarity[item, nzero])\ 48 | / sum(var.item_similarity[item, nzero]) + baseline[item] 49 | if prediction > 5: prediction = 5 50 | if prediction < 1: prediciton = 1 51 | return prediction 52 | 53 | def predict_topkCF_item(user, item, k=20): 54 | '''top-k CF算法,以item-item协同过滤为基础,结合baseline,预测rating''' 55 | nzero = var.ratings[user].nonzero()[0] 56 | baseline = var.item_mean + var.user_mean[user] - var.all_mean 57 | choice = nzero[var.item_similarity[item, nzero].argsort()[::-1][:k]] 58 | prediction = (var.ratings[user, choice] - baseline[choice]).dot(var.item_similarity[item, choice])\ 59 | / sum(var.item_similarity[item, choice]) + baseline[item] 60 | if prediction > 5: prediction = 5 61 | if prediction < 1: prediction = 1 62 | return prediction 63 | 64 | def predict_topkCF_user(user, item, k=30): 65 | '''top-k CF算法,以user-user协同过滤为基础,结合baseline,预测rating''' 66 | nzero = var.ratings[:,item].nonzero()[0] 67 | choice = nzero[var.user_similarity[user, nzero].argsort()[::-1][:k]] 68 | baseline = var.user_mean + var.item_mean[item] - var.all_mean 69 | prediction = (var.ratings[choice, item] - baseline[choice]).dot(var.user_similarity[user, choice])\ 70 | / sum(var.user_similarity[user, choice]) + baseline[user] 71 | if np.isnan(prediction): prediction = baseline[user] 72 | if prediction > 5: prediction = 5 73 | if prediction < 1: prediction = 1 74 | return prediction 75 | 76 | def predict_normCF_item(user, item, k=20): 77 | '''在topK的基础上对item采用归一化的相似度矩阵''' 78 | nzero = var.ratings[user].nonzero()[0] 79 | baseline = var.item_mean + var.user_mean[user] - var.all_mean 80 | choice = nzero[var.item_similarity_norm[item, nzero].argsort()[::-1][:k]] 81 | prediction = (var.ratings[user, choice] - baseline[choice]).dot(var.item_similarity_norm[item, choice])\ 82 | / sum(var.item_similarity_norm[item, choice]) + baseline[item] 83 | if prediction > 5: prediction = 5 84 | if prediction < 1: prediction = 1 85 | return prediction 86 | 87 | def predict_normCF_user(user, item, k=30): 88 | '''在topK的基础上对user采用归一化的相似度矩阵''' 89 | nzero = var.ratings[:,item].nonzero()[0] 90 | choice = nzero[var.user_similarity_norm[user, nzero].argsort()[::-1][:k]] 91 | baseline = var.user_mean + var.item_mean[item] - var.all_mean 92 | prediction = (var.ratings[choice, item] - baseline[choice]).dot(var.user_similarity_norm[user, choice])\ 93 | / sum(var.user_similarity_norm[user, choice]) + baseline[user] 94 | if np.isnan(prediction): prediction = baseline[user] 95 | if prediction > 5: prediction = 5 96 | if prediction < 1: prediction = 1 97 | return prediction 98 | 99 | def predict_blend(user, item, k1=20, k2=30, alpha=0.6): 100 | '''融合模型''' 101 | prediction1 = predict_topkCF_item(user, item, k1) 102 | prediction2 = predict_topkCF_user(user, item, k2) 103 | prediction = alpha * prediction1 + (1-alpha) * prediction2 104 | if prediction > 5: prediction = 5 105 | if prediction < 1: prediction = 1 106 | return prediction -------------------------------------------------------------------------------- /report/K-figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irmowan/Collaborative-Filtering/f04687a1939ab55c09a82cb9f3fdc685183eab3f/report/K-figure.png -------------------------------------------------------------------------------- /report/Plot/Plot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ECharts 7 | 8 | 9 | 11 | 12 | 13 | 14 |
15 | 18 | 19 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /report/Plot/Plot.js: -------------------------------------------------------------------------------- 1 | option = { 2 | title : { 3 | text: '某训练集各打分分布', 4 | subtext: '', 5 | x:'center', 6 | show:false 7 | }, 8 | tooltip : { 9 | trigger: 'item', 10 | formatter: "{a}
{b} : {c} ({d}%)" 11 | }, 12 | legend: { 13 | orient: 'vertical', 14 | left: 'left', 15 | data: ['★','★★','★★★','★★★★','★★★★★'], 16 | show:false 17 | }, 18 | 19 | series : [ 20 | { 21 | name: '访问来源', 22 | type: 'pie', 23 | radius : '50%', 24 | center: ['50%', '60%'], 25 | data:[ 26 | {value:4923, name:'★'}, 27 | {value:9150, name:'★★'}, 28 | {value:21564, name:'★★★'}, 29 | {value:27243, name:'★★★★'}, 30 | {value:17120, name:'★★★★★'} 31 | ], 32 | itemStyle: { 33 | emphasis: { 34 | shadowBlur: 10, 35 | shadowOffsetX: 0, 36 | shadowColor: 'rgba(0, 0, 0, 0.5)', 37 | label:{ 38 | show: true, 39 | formatter: '{b} : {c} ({d}%)' 40 | }, 41 | labelLine :{show:true} 42 | }, 43 | normal:{ 44 | label:{ 45 | show: true, 46 | formatter: '{b} : {c} ({d}%)' 47 | }, 48 | labelLine :{show:true} 49 | } 50 | } 51 | } 52 | , 53 | ] 54 | }; 55 | -------------------------------------------------------------------------------- /report/Plot/theme/macarons.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['exports', 'echarts'], factory); 5 | } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { 6 | // CommonJS 7 | factory(exports, require('echarts')); 8 | } else { 9 | // Browser globals 10 | factory({}, root.echarts); 11 | } 12 | }(this, function (exports, echarts) { 13 | var log = function (msg) { 14 | if (typeof console !== 'undefined') { 15 | console && console.error && console.error(msg); 16 | } 17 | }; 18 | if (!echarts) { 19 | log('ECharts is not Loaded'); 20 | return; 21 | } 22 | 23 | var colorPalette = [ 24 | '#2ec7c9','#b6a2de','#5ab1ef','#ffb980','#d87a80', 25 | '#8d98b3','#e5cf0d','#97b552','#95706d','#dc69aa', 26 | '#07a2a4','#9a7fd1','#588dd5','#f5994e','#c05050', 27 | '#59678c','#c9ab00','#7eb00a','#6f5553','#c14089' 28 | ]; 29 | 30 | 31 | var theme = { 32 | color: colorPalette, 33 | 34 | title: { 35 | textStyle: { 36 | fontWeight: 'normal', 37 | color: '#008acd' 38 | } 39 | }, 40 | 41 | visualMap: { 42 | itemWidth: 15, 43 | color: ['#5ab1ef','#e0ffff'] 44 | }, 45 | 46 | toolbox: { 47 | iconStyle: { 48 | normal: { 49 | borderColor: colorPalette[0] 50 | } 51 | } 52 | }, 53 | 54 | tooltip: { 55 | backgroundColor: 'rgba(50,50,50,0.5)', 56 | axisPointer : { 57 | type : 'line', 58 | lineStyle : { 59 | color: '#008acd' 60 | }, 61 | crossStyle: { 62 | color: '#008acd' 63 | }, 64 | shadowStyle : { 65 | color: 'rgba(200,200,200,0.2)' 66 | } 67 | } 68 | }, 69 | 70 | dataZoom: { 71 | dataBackgroundColor: '#efefff', 72 | fillerColor: 'rgba(182,162,222,0.2)', 73 | handleColor: '#008acd' 74 | }, 75 | 76 | grid: { 77 | borderColor: '#eee' 78 | }, 79 | 80 | categoryAxis: { 81 | axisLine: { 82 | lineStyle: { 83 | color: '#008acd' 84 | } 85 | }, 86 | splitLine: { 87 | lineStyle: { 88 | color: ['#eee'] 89 | } 90 | } 91 | }, 92 | 93 | valueAxis: { 94 | axisLine: { 95 | lineStyle: { 96 | color: '#008acd' 97 | } 98 | }, 99 | splitArea : { 100 | show : true, 101 | areaStyle : { 102 | color: ['rgba(250,250,250,0.1)','rgba(200,200,200,0.1)'] 103 | } 104 | }, 105 | splitLine: { 106 | lineStyle: { 107 | color: ['#eee'] 108 | } 109 | } 110 | }, 111 | 112 | timeline : { 113 | lineStyle : { 114 | color : '#008acd' 115 | }, 116 | controlStyle : { 117 | normal : { color : '#008acd'}, 118 | emphasis : { color : '#008acd'} 119 | }, 120 | symbol : 'emptyCircle', 121 | symbolSize : 3 122 | }, 123 | 124 | line: { 125 | smooth : true, 126 | symbol: 'emptyCircle', 127 | symbolSize: 3 128 | }, 129 | 130 | candlestick: { 131 | itemStyle: { 132 | normal: { 133 | color: '#d87a80', 134 | color0: '#2ec7c9', 135 | lineStyle: { 136 | color: '#d87a80', 137 | color0: '#2ec7c9' 138 | } 139 | } 140 | } 141 | }, 142 | 143 | scatter: { 144 | symbol: 'circle', 145 | symbolSize: 4 146 | }, 147 | 148 | map: { 149 | label: { 150 | normal: { 151 | textStyle: { 152 | color: '#d87a80' 153 | } 154 | } 155 | }, 156 | itemStyle: { 157 | normal: { 158 | borderColor: '#eee', 159 | areaColor: '#ddd' 160 | }, 161 | emphasis: { 162 | areaColor: '#fe994e' 163 | } 164 | } 165 | }, 166 | 167 | graph: { 168 | color: colorPalette 169 | }, 170 | 171 | gauge : { 172 | axisLine: { 173 | lineStyle: { 174 | color: [[0.2, '#2ec7c9'],[0.8, '#5ab1ef'],[1, '#d87a80']], 175 | width: 10 176 | } 177 | }, 178 | axisTick: { 179 | splitNumber: 10, 180 | length :15, 181 | lineStyle: { 182 | color: 'auto' 183 | } 184 | }, 185 | splitLine: { 186 | length :22, 187 | lineStyle: { 188 | color: 'auto' 189 | } 190 | }, 191 | pointer : { 192 | width : 5 193 | } 194 | } 195 | }; 196 | 197 | echarts.registerTheme('macarons', theme); 198 | })); -------------------------------------------------------------------------------- /report/Plot/theme/vintage.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['exports', 'echarts'], factory); 5 | } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { 6 | // CommonJS 7 | factory(exports, require('echarts')); 8 | } else { 9 | // Browser globals 10 | factory({}, root.echarts); 11 | } 12 | }(this, function (exports, echarts) { 13 | var log = function (msg) { 14 | if (typeof console !== 'undefined') { 15 | console && console.error && console.error(msg); 16 | } 17 | }; 18 | if (!echarts) { 19 | log('ECharts is not Loaded'); 20 | return; 21 | } 22 | var colorPalette = ['#d87c7c','#919e8b', '#d7ab82', '#6e7074','#61a0a8','#efa18d', '#787464', '#cc7e63', '#724e58', '#4b565b']; 23 | echarts.registerTheme('vintage', { 24 | color: colorPalette, 25 | backgroundColor: '#fef8ef', 26 | graph: { 27 | color: colorPalette 28 | } 29 | }); 30 | })); -------------------------------------------------------------------------------- /report/Report.bib: -------------------------------------------------------------------------------- 1 | @inproceedings{sarwar2001item, 2 | title={Item-based collaborative filtering recommendation algorithms}, 3 | author={Sarwar, Badrul and Karypis, George and Konstan, Joseph and Riedl, John}, 4 | booktitle={Proceedings of the 10th international conference on World Wide Web}, 5 | pages={285--295}, 6 | year={2001}, 7 | organization={ACM} 8 | } 9 | 10 | @inproceedings{breese1998empirical, 11 | title={Empirical analysis of predictive algorithms for collaborative filtering}, 12 | author={Breese, John S and Heckerman, David and Kadie, Carl}, 13 | booktitle={Proceedings of the Fourteenth conference on Uncertainty in artificial intelligence}, 14 | pages={43--52}, 15 | year={1998}, 16 | organization={Morgan Kaufmann Publishers Inc.} 17 | } 18 | 19 | @article{koren2009bellkor, 20 | title={The bellkor solution to the netflix grand prize}, 21 | author={Koren, Yehuda}, 22 | journal={Netflix prize documentation}, 23 | volume={81}, 24 | pages={1--10}, 25 | year={2009} 26 | } 27 | 28 | @inproceedings{wang2015collaborative, 29 | title={Collaborative deep learning for recommender systems}, 30 | author={Wang, Hao and Wang, Naiyan and Yeung, Dit-Yan}, 31 | booktitle={Proceedings of the 21th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining}, 32 | pages={1235--1244}, 33 | year={2015}, 34 | organization={ACM} 35 | } -------------------------------------------------------------------------------- /report/Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irmowan/Collaborative-Filtering/f04687a1939ab55c09a82cb9f3fdc685183eab3f/report/Report.pdf -------------------------------------------------------------------------------- /report/Report.tex: -------------------------------------------------------------------------------- 1 | % TEX encoding = UTF-8 Unicode 2 | % Compiled by XeLaTeX. 3 | 4 | \documentclass[utf8, a4paper, 11pt, onecolumn]{ctexart} 5 | \usepackage{xeCJK} 6 | \usepackage{setspace} 7 | \usepackage{amsmath} 8 | \usepackage{url} 9 | 10 | \title{推荐系统的协同过滤算法实现与浅析} 11 | %\author{} 12 | %\date{\today} 13 | 14 | \begin{document} 15 | \begin{spacing}{1.5} 16 | 17 | \maketitle 18 | \tableofcontents 19 | \newpage 20 | 21 | \end{spacing} 22 | 23 | \begin{spacing}{1.25} 24 | 25 | \section{项目简介} 26 | 27 | 个性化推荐系统基于用户的兴趣和商品的特性,向用户推荐合适的信息或商品,其在互联网领域,尤其是电子商务、广告业务等方面,具有非常广泛的应用。推荐系统的进步,会更加迎合用户的需求,会为产品赢得更好的口碑,为企业创造更多的收益,形成良性的循环。因而,对推荐系统算法的研究,在实践中不断发展,不断进步。 28 | 29 | 本项目选择推荐系统为主题,以协同过滤(Collaborative Filtering)为主要算法,基于MovieLens数据集,采用了交叉验证的方式,以均方根误差RMSE为评价指标。 30 | 31 | 由于这门课是以算法为核心的课程,因而本项目更加注重算法的具体内容和细节。在项目中,所有核心代码均由自己编写,未调用任何外部算法模块。在报告中,也会主要以算法内容和实现细节为主。 32 | 33 | 算法以baseline为起步。在baseline的基础上,实现了基本的user-user和item-item协同过滤算法,以及基于baseline的协同过滤算法,验证了item-item相比user-user能获得更好的效果。 34 | 35 | 同时,在基本的协同过滤算法上,加上了bias、TopK等优化,进一步提升了模型效果。此外,研究了TopK算法中,K的取值对模型效果的影响,以及关于归一化的相似度矩阵对算法效果的影响。最后,尝试融合了不同的算法并调参,获得了更好的融合模型。 36 | 37 | 在项目过程中,在矩阵计算、相似度处理、评分预测等处,遇到了诸多算法细节问题,并进行了合适的处理。对于矩阵运算的代码,尽量进行了Vectorization,以提高速度。同时,自己重新组织了代码结构,分离了各个功能,使其具有更好的模块性,运行更加流水线化。 38 | 39 | 所有代码及报告,在隐去个人信息后,开源在GitHub平台(\url{https://github.com/irmowan/Collaborative-Filtering})。代码的使用可参见Readme文件。 40 | 41 | \section{平台和工具} 42 | 43 | 以OS X 10.11及Python 3.5为主要开发环境,使用Anaconda作为Python的科学发行版。 44 | 45 | 使用Jupyter Notebook作为生产力工具,可以进行方便的调试。 46 | 47 | 使用Numpy和Pandas作为矩阵运算和数据载入的工具。 48 | 49 | 使用Matplotlib及Echarts.js作为数据可视化工具。 50 | 51 | 使用Git作为版本管理的工具。 52 | 53 | 使用基于Unicode的TeX发行版XeLaTeX撰写报告。 54 | 55 | \section{数据摘要} 56 | 57 | \subsection{数据集} 58 | 59 | 原先,我采用的是NetflixPrize 数据集,NetflixPrize是关于电影评分的数据集。标准的NetflixPrize数据集包含了480189个user,和17770个item,以及总计约1亿的ratings。数据集中还包括了打分的时间,以及各部电影id对应的名称和年份。 60 | 61 | 对于协同过滤方法来说,该数据集产生的评分矩阵规模达$480189 \times 17770$,总元素约有80多亿,在该矩阵上的基本统计已经要耗时数十秒,对该矩阵进行更细粒度的计算则会更慢。 62 | 63 | 因而,我换用了一个数据格式基本相同,但规模更小的数据集MovieLens\ (\url{http://grouplens.org/datasets/movielens/})。它提供了不同规模的数据集,包括100K, 1M, 10M, 20M(均指Rating数)等多个规模的版本。 64 | 65 | 此外,相比于NetflixPrize,其提供了更多的信息,除了打分时间以外,包括用户的性别、年龄、职业、地区,以及电影的名称、发行时间、和丰富的标签(科幻、动作、文艺等)。 66 | 67 | 这些丰富的信息都是可以被推荐系统所利用的。如用户的年龄和职业可以被用来聚类,电影的标签可以用来做基于内容(content-based)的推荐,时间戳可以用来进行时序化的推荐(更新鲜的打分具有更高的权重),这些还可以同协同过滤算法相结合,从而达到提高预测速度和精度的目的。 68 | 69 | 需要注意的是,MovieLens数据集是经过过滤处理的,所有打分少于20个的用户均被过滤。因而,出现在数据集中的用户,每个用户至少对20部影片进行了打分。(而对于每部影片则不然,可能存在没有被打分的影片) 70 | 71 | 为了获得较快的执行速度,报告中展示的所有算法的运行结果,均基于100K版本的MovieLens数据集。该数据集包括943 users, 1682 items, 100000 ratings。 72 | 73 | \subsection{数据格式与交叉验证} 74 | 75 | 通过Pandas的DataFrame读入数据。 76 | 77 | 原先,所有数据采用一组训练集和测试集,训练集和测试集规模比为$4:1$。即,训练集规模为80000,测试集规模为20000。 78 | 79 | 数据每行格式为(user, item, rating, timestamp),训练集及测试集均为此格式。在测试时,所有测试函数以(user, item)对为输入参数,返回预测的rating。 80 | 81 | 在读入数据后,由打分数据填充ratings矩阵,以user为行,item为列,形成$943 \times 1682$的矩阵。 82 | 83 | 为提高指标稳定性,采用了K次交叉验证(K-fold Cross Validation)的方法。将数据切分为5个子样本,每次取4组训练,剩余一组用于测试,循环5次。在交叉意义下的评价指标可见\ref{评价指标}节。 84 | 85 | \textit{\textbf{trick}: 数据集内数据id以1开始,内部变量索引以0开始,做适当调整即可。} 86 | 87 | \subsection{评价指标} 88 | \label{评价指标} 89 | 90 | 对于如何评价算法的优劣程度,需要指定相关的评价指标。 91 | 92 | 一种常见的评价指标是平均绝对误差\textit{Mean Absolute Error}\ (MAE),MAE值越低,则预测效果越好。其定义为: 93 | 94 | \[MAE =\frac{\sum_{i=1}^{N} \lvert r_{xi} - \hat{r}_{xi}\rvert}{N} \] 95 | 96 | 其中,$\hat{r}_{xi}$和$r_{xi}$分别为用户$x$对项目$i$的预测打分及实际打分。MAE值越低,则预测效果越好。 97 | 98 | 而在项目中,选择了均方根误差\textit{Root Mean Squared Error}\ (RMSE)作为评价指标,它与MAE一样,越低则效果越好。其定义如下: 99 | 100 | \[RMSE = \sqrt{\sum_{i = 1}^{N}(r_{xi} - \hat{r}_{xi})^{2}}\] 101 | 102 | 103 | 由于采用了交叉验证的方法,因而选择每组数据集的RMSE均值作为预测方法的最终RMSE值:(k为交叉验证的组数) 104 | 105 | \[\overline{RMSE} = \frac{1}{k}\sum_{i=1}^{k}RMSE_{i}\] 106 | 107 | \section{模型详解与分析} 108 | \label{模型详解} 109 | 110 | \subsection{统计指标及baseline} 111 | 112 | 首先,对于输入数据进行一些统计分析。 113 | 114 | 针对矩阵的稀疏程度,只需做简单运算即可得到,打分矩阵的密度为 $5.04\%$。 115 | 116 | 除零值外,共有五种打分,以某个训练集为例,作出简单的打分分布(图\ref{rating-pie}),可以大致看出各个分数的打分情况。 117 | 118 | \begin{figure}[h] 119 | \centering 120 | \includegraphics[width=1.0\linewidth]{rating-pie.png} 121 | \caption{打分分布图} 122 | \label{rating-pie} 123 | \end{figure} 124 | 125 | 对矩阵的每一行和每一列求平均值,得到了各个user和item的打分均值。需要注意的是,此处要过滤零值,只对非零值求平均,否则无意义。 126 | 127 | \textit{\textbf{trick}: 过滤出非零值不需要每行(列)分别过滤,应采用向量化方法,对整个矩阵执行按行(列)求和和非零值计数运算,直接相除。} 128 | 129 | baseline是基于这些统计量的简单预测。其预测公式为 130 | 131 | \[\hat{r}_{xi} = \mu + ub_{x} + ib_{i}\] 132 | 133 | 其中,$\mu$为总体均值,$ub_{x}$和$ib_{i}$分别是user x和item i的均值与总体均值的偏差。化简可得: 134 | 135 | \[\hat{r}_{xi} = \overline{r_{user\ x}}+ \overline{r_{item\ i}} - \mu\] 136 | 137 | 此算法即为baseline的效果,其RMSE值为\textbf{0.9694}. 138 | 139 | \textit{\textbf{trick}: numpy采用了float64类型存储浮点数,最好不要对其做任何近似操作,只需在最后的输出结果中采用近似表示即可。} 140 | 141 | \subsection{相似度矩阵与协同过滤} 142 | 143 | 协同过滤(Collaborative filtering)基于这样的思想:如果两个user对大多数item的打分相近,说明这两个user的相似度较高,或者若果两个item被大多数user打分相近,也说明这两个item的相似度较高。 144 | 145 | 基于相似度的来源,以上分别被称为user-user协同过滤和item-item协同过滤。 146 | 147 | 相似度采用Cosine距离测量,其公式为: 148 | 149 | \[sim(x,y) = \frac{r_x \cdot r_y}{\| r_x \| \| r_y\|}\] 150 | 151 | 该公式对行和列均有效。其对应的矩阵运算为: 152 | 153 | \[Sim = \frac{R \cdot R^T}{\| R\cdot R^T \|}\] 154 | 155 | \textit{\textbf{trick}: 为防止divide by zero错误,可以在计算时加上一个小偏差$\epsilon$。即,采用$R\cdot R^T + \epsilon$的方式进行实际计算。} 156 | 157 | 在得到user相似度和item相似度后,可以通过其相似度矩阵进行预测。 158 | 159 | 预测公式采用加权平均的方式,为用户对其它item打分和item之间相似度的加权平均(针对item-item协同过滤): 160 | 161 | \[\hat{r}_{xi} = \frac{\sum_{j \in N(x)} s_{ij} \cdot r_{xj}}{\sum_{j \in N(x)} s_{ij}} \] 162 | 163 | 其中$N(x)$为user x打过分的数据。此处不能对所有数据求加权平均,因为其没有打过分的item,求平均值没有意义,反而会增加分母的值,导致预测严重偏差。 164 | 165 | \textit{\textbf{trick}:该预测公式还需要一点补充,即冷启动问题,当该公式分母为0时,结果为NaN,此时可以采用baseline结果代替NaN.} 166 | 167 | 该方法基于item-item的交叉验证结果是\textbf{1.0149},竟然比baseline还高,而基于user-user的结果是\textbf{1.0174},同样超过了1。 168 | 169 | \subsection{结合baseline的协同过滤} 170 | 171 | 所以,baseline的意义是重要的,只采用协同过滤而无视了baseline,效果并没有那么明显。 172 | 将baseline和协同过滤结合起来,在baseline的基础上预测,预测公式改为: 173 | 174 | \[\hat{r}_{xi} = b_{xi} + \frac{\sum_{j \in N(x)} s_{ij} \cdot (r_{xj} - b_{xj})}{\sum_{j \in N(x)} s_{ij}} \] 175 | 176 | 其中,$b_{xi}$为用户x, 项目i的baseline预测值,$N(x)$为用户x打过分的项目集合。同样可以采用向量化计算。 177 | 178 | 经过改进,基于baseline的item-item协同过滤算法可以将RMSE提高到\textbf{0.9362},相比于baseline有了很大的进步。 179 | 180 | 而基于baseline的user-user协同过滤也达到了\textbf{0.9548}. 181 | 182 | 可以得出,在实际应用中,的确item-item的算法表现更加好,大致是商品之间的差异,不如人之间的不同口味差异。 183 | 184 | \textit{\textbf{trick}:在进行到此处时,发现了一处细节优化,即由于最终打分必然是1-5的整数值,那么在预测时,若预测结果小于1,可以返回1为结果,若预测结果大于5,则返回5为结果。这个改进是微小但稳健的,其可以将item-item协同过滤RMSE由\textbf{0.9362}提高到\textbf{0.9360}} 185 | 186 | \subsection{topK协同过滤与K值的选择} 187 | 188 | 在协同过滤的预测中,由于需要针对每个预测计算所有的历史数据,时间开销较大,且并不是所有打过分的项目,均属于和item较相似的范畴。因而,可以采取topK技巧,在所有打过分的item中,过滤出与该item相似度最高的K个item,只对这K个item进行加权平均。 189 | 190 | \[\hat{r}_{xi} = b_{xi} + \frac{\sum_{j \in N_k(x)} s_{ij} \cdot (r_{xj} - b_{xj})}{\sum_{j \in N_k(x)} s_{ij}} \] 191 | 192 | \textit{\textbf{trick}: 如果用户打过的item数不足K,则直接使用所有打过的item。这也是基于某些用户可能只倾向于对其喜欢的商品评分。} 193 | 194 | 在运行时,在采用item-item协同过滤的情况下,选择K=10时,得到的RMSE为\textbf{0.9278},选择K=40时,得到的RMSE为\textbf{0.9242}. 195 | 196 | 可以发现,不同的K值,其效果有一定差异。当K太小时,不能覆盖所有与其较相似的item, 而当K太大时,所选的item可能已经与其不再非常相似。 197 | 198 | 尝试调整K,对不同的K,得到的RMSE如表格\ref{k-table}. 199 | 200 | \begin{table}[t] 201 | \centering 202 | \begin{tabular}{| c | c | c | c | c |} 203 | \hline 204 | \textbf{K值} & item训练集 & item 测试集&user训练集 & user测试集 \\ 205 | \hline 206 | 5 & 0.5747 & 0.9543 & 0.6101 & 0.9874 \\ 207 | \hline 208 | 10 & 0.6845 & 0.9278 & 0.7242 & 0.9573 \\ 209 | \hline 210 | 15 & 0.7322 & 0.9223 & 0.7699 & 0.9489 \\ 211 | \hline 212 | 18 & 0.7500 & 0.9215 & 0.7866 & 0.9468 \\ 213 | \hline 214 | 20 & 0.7596 & 0.9213 & 0.7951 & 0.9458 \\ 215 | \hline 216 | 25 & 0.7774 & 0.9217 & 0.8113 & 0.9449 \\ 217 | \hline 218 | 30 & 0.7902 & 0.9225 & 0.8225 & 0.9445 \\ 219 | \hline 220 | 40 &0.8073 & 0.9242 & 0.8373 & 0.9447 \\ 221 | \hline 222 | 50 & 0.8182 & 0.9258 & 0.8465 & 0.9453 \\ 223 | \hline 224 | 100 & 0.8422 & 0.9314 & 0.8660 & 0.9491 \\ 225 | \hline 226 | 200 & 0.8546 & 0.9345 & 0.8747 & 0.9526 \\ 227 | \hline 228 | \end{tabular} 229 | \caption{不同K值下的RMSE} 230 | \label{k-table} 231 | \end{table} 232 | 233 | 由表格可以大致看出,此处不同的K值得到的RMSE应该呈现U型。 234 | 235 | 可以画出对应的RMSE曲线,如图\ref{k-figure}。 236 | 237 | \begin{figure}[h] 238 | \centering 239 | \includegraphics[width=0.8\linewidth]{k-figure.png} 240 | \caption{不同K值下的RMSE变化图} 241 | \label{k-figure} 242 | \end{figure} 243 | 244 | 由图像中发现,随着K的增大,训练集上的RMSE逐渐增大,而不是一般意义上的逐渐变小。其原因主要是,训练数据融入了均值和相似度,在预测时不可避免地利用到了原先的信息,因而训练集上的RMSE并不具有很强的代表性。 245 | 246 | 测试集上呈现出非典型的U型,U型的低谷即为对应的最优K值。而在K增大时,其带来的负面作用并没有那么大。这大概是因为,采用更多的数据,边缘数据由于其权重的减少,对最终预测值的影响也减小,类似于经济学中的边际效应递减规律。 247 | 248 | 显而易见,不同规模的输入,应该要有不同大小的K值相匹配。更大规模的数据,应该需要更大规模的K值。 249 | 250 | \textit{\textbf{trick}:在这里,我采用的方式是直接设定一个K值集合,因而对于不同规模不具有很好的适应性。而在实际应用中,更好的方式可以是通过学习的方式,去获得一个较优的K值。} 251 | 252 | 对于该数据集而言,当采用item-item协同过滤时,K=20为宜,RMSE为\textbf{0.9213},当采用user-user协同过滤时,K=30为宜,RMSE为\textbf{0.9445}. 253 | 254 | \subsection{归一化的相似度衡量指标} 255 | 256 | 在相似矩阵中,除了采用Cosine距离外,还可以有其它的相似度定义。 257 | 258 | 例如采用Pearson相关系数($Pearson-r$ correlation $corr_{i,j}$)\cite{sarwar2001item},其在算Cosine距离前,首先将同一行(列)的元素减去其平均值,以抹去各人打分标准不同所带来的权重差异,在这个意义下定义了新的相似度矩阵。例如item的相似度计算为: 259 | 260 | \[sim(i,j) = \frac{\sum_{u \in U} (r_{ui}-\overline{r_{i}}) (r_{uj}-\overline{r_{j}})} {\sqrt{\sum_{u \in U} (r_{ui}-\overline{r_{i}})^2} \sqrt{\sum_{u \in U} (r_{uj}-\overline{r_{j}})^2}}\] 261 | 262 | 此即为归一化的相似度矩阵定义。同样,采用向量化方式将加快该矩阵的计算。 263 | 264 | 而同时用于预测的函数,可以保持不变,仅将其中采用的相似度矩阵换成归一化后的相似度矩阵即可。 265 | 266 | 然而,在topK item-item协同过滤的基础上,将原先的相似度矩阵替换为归一化后的相似度矩阵,其RMSE反而从\textbf{0.9213}提高到\textbf{0.9253}(k=20)。 267 | 268 | 至于为什么会出现RMSE反而提高的情况,经与老师讨论及相关查阅后,发现这与数据集本身的一些性质有关,对于某些打分很少的item,做归一化之后,其反而抹去了这些item原来就已经很少的信息。例如一部小众的影片,三四个口味相符的受众同时打出了5分的高分,则归一化之后,一下子抹去了这部电影的高分信息,反而产生了信息的损失。 269 | 270 | 在这种情况下,那么对于user-user协同过滤,由于该数据集中,每个用户至少打过20个评分,这样的弊端应该会被尽量避免。于是,尝试对user-user协同过滤采用归一化后的相似度矩阵。然而效果同样不尽人意,RMSE从\textbf{0.9445}提高到了\textbf{0.9550}(k=30)。 271 | 272 | 关于这个问题,草读了相关的几篇论文,发现其主要有两个方面的考虑。 273 | 274 | 首先,对相似度的度量上,有着广泛的讨论。其中,有一种做法是,依然采用item之间的相似度计算,但是,此时不对item作归一化,而是对user做归一化\cite{sarwar2001item}。 275 | 276 | \[sim(i,j) = \frac{\sum_{u \in U} (r_{ui}-\overline{r_{u}}) (r_{uj}-\overline{r_{u}})} {\sqrt{\sum_{u \in U} (r_{ui}-\overline{r_{u}})^2} \sqrt{\sum_{u \in U} (r_{uj}-\overline{r_{u}})^2}}\] 277 | 278 | 另一种解决方式是,当打分的数量不足时,采用default voting的方法\cite{breese1998empirical}。在这一方法中,使用类似tf-idf的分析法,获得默认的权重值。公式较为复杂: 279 | 280 | \[w(u,v) = \frac{\sum_{i} f_{i} \sum_{i} f_{i} r_{u,i} r_{v,i} - (\sum_i f_i r_{u,i}) (\sum_i f_i r_{vi}) }{\sqrt{UV}}\] 281 | 282 | 其次,由于相似度的衡量方法,在实际使用时,很多本质上很相似的对象,它们的vector distance在Euclidean空间下下可能并不理想\cite{sarwar2001item},这就导致了预测时的偏差。 283 | 284 | 论文\cite{sarwar2001item}中提出的一种改进是,以线性回归模型的结果,取代简单地使用相似对象的raw rating进行预测。 285 | 286 | \[\hat{r}^{'}_{N} = \alpha \hat{r}_i + \beta + \epsilon \] 287 | 288 | 通过回归的方式,确定公式中的$\alpha$和$\beta$。 289 | 290 | \subsection{模型融合与融合参数} 291 | 292 | 此时,协同过滤算法的各种优化价值似乎已经被压榨完。突然灵光一现,想到NetflixPrize最后的获奖算法,多层次多尺度地融合了三百多个模型\cite{koren2009bellkor}。所以,是否可以借鉴这样的思路,尝试一下模型融合在这种情况下,会产生怎样的效果? 293 | 294 | 在以上的模型中,在不考虑归一化相似度矩阵的情况下,具有本质区别的方法有两种,item-item协同过滤和user-user协同过滤,虽然来源于同一数据,但它们是两个不同的维度。于是,尝试将这两者相融合。其中,topK算法的K值分别选取20和30,也即其各自的最优值。 295 | 296 | 对一个测试输入,同时采用两种方法进行预测,并求其均值,也即: 297 | 298 | \[\hat{r}_{xi} = \frac{\hat{r_1}_{xi} + \hat{r_2}_{xi}}{2}\] 299 | 300 | 果真得到了更好的效果,item-item协同过滤的RMSE为\textbf{0.9213}, user-user协同过滤的RMSE为\textbf{0.9445},而融合以后,RMSE降低到了\textbf{0.9176}. 301 | 302 | 进一步,由于item-item协同过滤的效果更好,所以应该在模型融合时对其采用更大的权重,于是,对融合预测函数作适当修改: 303 | 304 | \[\hat{r}_{xi} = 0.6 * \hat{r_1}_{xi} + 0.4 * \hat{r_2}_{xi}\] 305 | 306 | 得到了更好的RMSE值,为\textbf{0.9159}. 可以看出,模型融合的意义很大。 307 | 308 | 类似在topK方法中的思路,可以将公式改进为线性融合函数,将融合程度作为预测函数的一个参数: 309 | 310 | \[\hat{r}_{xi} = \alpha * \hat{r_1}_{xi} + (1-\alpha) * \hat{r_2}_{xi}\] 311 | 312 | 其中,$\alpha \in [0, 1]$,在两个端点处即分别退化为两个模型,而系数$\alpha$则表示了融合程度,也即在融合模型中item-item协同过滤的权重。 313 | 314 | 通过调参,调整$\alpha$的值,可以获得最佳的融合效果。 315 | 316 | 在不同的$\alpha$值下测量RMSE,得到表\ref{alpha-table}. 317 | 318 | \begin{table}[ht] 319 | \centering 320 | \begin{tabular}{|c|c|c|} 321 | \hline 322 | \textbf{$\alpha$} & 训练集RMSE & 测试集RMSE \\ 323 | \hline 324 | 0.00 & 0.8225 & 0.9445 \\ 325 | \hline 326 | 0.10 & 0.8108 & 0.9368 \\ 327 | \hline 328 | 0.30 & 0.7907 & 0.9248 \\ 329 | \hline 330 | 0.50 & 0.7754 & 0.9176 \\ 331 | \hline 332 | 0.60 & 0.7696 & 0.9159 \\ 333 | \hline 334 | 0.65 & 0.7672 & 0.9155 \\ 335 | \hline 336 | 0.70 & 0.7651 & 0.9154 \\ 337 | \hline 338 | 0.75 & 0.7634 & 0.9156 \\ 339 | \hline 340 | 0.80 & 0.7620 & 0.9161 \\ 341 | \hline 342 | 0.90 & 0.7601 & 0.9181 \\ 343 | \hline 344 | 1.00 & 0.7596 & 0.9213 \\ 345 | \hline 346 | \end{tabular} 347 | \caption{不同融合参数$\alpha$下的RMSE} 348 | \label{alpha-table} 349 | \end{table} 350 | 351 | 同样,画出不同$\alpha$下,融合模型的RMSE变化趋势(图\ref{alpha-figure})。 352 | 353 | \begin{figure}[ht] 354 | \centering 355 | \includegraphics[width=0.8\linewidth]{alpha-figure.png} 356 | \caption{不同$\alpha$值下的RMSE变化图} 357 | \label{alpha-figure} 358 | \end{figure} 359 | 360 | 最终,选取$\alpha = 0.70$,得到了\textbf{0.9154}的RMSE。 361 | 362 | 此外,还可以考虑一些非线性的模型融合方式。 363 | 364 | \subsection{其它算法} 365 | 366 | 协同过滤被认为是一种基于内存(Memory-based)的推荐算法。它的推荐速度非常快,但由于产生推荐比较耗时,在实时推荐方面还不够有力。 367 | 368 | 在论文\cite{breese1998empirical}中,作者将还将协同过滤看做是一种概率分布,在这里意义下,一些概率模型就可以发挥其作用,包括Bayesian Classifier, Bayesian Network等。 369 | 370 | 而在2015年最新的SIGKDD中,论文\cite{wang2015collaborative}将Deep Learning用在协同过滤中,将协同过滤同如今正热的深度神经网络相结合,给推荐系统带来了一些新鲜的活力。 371 | 372 | \section{模型结果} 373 | 374 | 在模型详解一节中,详述了各个算法的定义、公式、相关分析,以及各种trick。 375 | 376 | 在同一套数据下,利用交叉验证的方式测量不同方法的性能,其最终的RMSE结果为表格\ref{RMSE-table},需要注意的是,这其中很多方法都是建立在前者的方法上,叠加了前面的方法,一点点尝试改进。 377 | 378 | \begin{table}[hbtp] 379 | \centering 380 | \begin{tabular}{|c|c|} 381 | \hline 382 | \textbf{Method} & \textbf{RMSE} \\ 383 | \hline 384 | baseline & 0.9694 \\ 385 | \hline 386 | itemCF & 1.0149 \\ 387 | \hline 388 | userCF & 1.0174 \\ 389 | \hline 390 | itemCF+baseline & 0.9362 \\ 391 | \hline 392 | userCF+baseline & 0.9548 \\ 393 | \hline 394 | itemCF+bias & 0.9360 \\ 395 | \hline 396 | topkCF(item, k=20) & 0.9213 \\ 397 | \hline 398 | topkCF(user, k=30) &0.9445 \\ 399 | \hline 400 | normCF(item, k=20) &0.9253 \\ 401 | \hline 402 | normCF(user, k=30) &0.9550 \\ 403 | \hline 404 | blendCF($\alpha$=0.70) & 0.9154 \\ 405 | \hline 406 | \end{tabular} 407 | \caption{不同方法的RMSE比较} 408 | \label{RMSE-table} 409 | \end{table} 410 | 411 | 最终,在尝试了各种优化和改进之后,融合了两类协同过滤算法的融合模型取得了\textbf{0.9154}的RMSE值。 412 | 413 | \section{总结} 414 | 415 | 在这个项目中,我以课堂内容为基准,参考一些经典的论文,尝试自己实现协同过滤的算法,获得更好的推荐效果。通过这一过程,我加深了对推荐系统,尤其是协同过滤方法的理解。 416 | 417 | 统计量是非常简单却又及其重要的一个指标,如果不基于baseline,算法好像失去了一个有力的支点,其效果往往不尽人意。 418 | 419 | 最基本的协同过滤算法,采用相似度的衡量方式进行预测。其效果和维度本身的差异性有关,但总是能带来较为显著的效果提升。其需要比较大的计算量,适合于离线推荐。 420 | 421 | topK算法则更进一步,对协同过滤进行了预测效果和预测速度上的双重改进,大大提升了模型效果。其中,关于K值的选择,也非常值得探讨。在现实场景中,这个参数往往要进行相应的调整。 422 | 423 | 原本以为归一化的相似度矩阵,会对模型带来深刻改进。但实践是检验真理的唯一标准,在实践中发现,其RMSE反而提高了不少。对于这一困惑的问题,同老师进行了讨论,也查阅了相关的经典论文,发现这是一个普遍存在的问题,针对这个问题,也有不少解决的思路和方法。 424 | 425 | 最后,模型的线性融合,是一个非常易于实现,且能够提升预测效果的方法。模型的融合在各种现实应用场景中也非常地普遍,其融合的各种技巧,值得继续探究。 426 | 427 | 此外,算法中,关于divide by zero等处理细节,以及 Vectorization等技巧,可以达到避免潜在风险、提升算法效率的作用,同样不能过于忽视。 428 | 429 | 单纯的协同过滤算法,并没有充分利用数据集。在数据集上还可以做更多的工作,例如利用电影的标签,进行基于内容的推荐,利用电影的发布时间,人们更倾向于观看更新的影片。聚类、在现实应用场景中,常常将各类推荐算法相互结合,可能在离线推荐、在线推荐上采用不同的方法,可能对新用户和老用户采用不同的推荐方法。 430 | 431 | 总之,通过这个动手项目,第一次采用了Numpy, Pandas等科学计算库,锻炼了手写算法的能力,阅读了相关的经典论文,加深了对协同过滤和推荐系统的理解,从中学到了很多。 432 | 433 | \bibliographystyle{plain} 434 | \bibliography{Report} 435 | 436 | \end{spacing} 437 | \end{document} 438 | -------------------------------------------------------------------------------- /report/alpha-figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irmowan/Collaborative-Filtering/f04687a1939ab55c09a82cb9f3fdc685183eab3f/report/alpha-figure.png -------------------------------------------------------------------------------- /report/rating-pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irmowan/Collaborative-Filtering/f04687a1939ab55c09a82cb9f3fdc685183eab3f/report/rating-pie.png -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import var 2 | import numpy as np 3 | 4 | def cal_sparsity(): 5 | '''计算矩阵密度''' 6 | sparsity = float(len(var.ratings.nonzero()[0])) 7 | sparsity /= var.n_users * var.n_items 8 | sparsity *= 100 9 | return sparsity 10 | 11 | def cal_mean(): 12 | '''计算总体均值,各user打分均值,各item打分均值...''' 13 | var.all_mean = np.mean(var.ratings[var.ratings!=0]) 14 | var.user_mean = sum(var.ratings.T) / sum((var.ratings!=0).T) 15 | var.item_mean = sum(var.ratings) / sum((var.ratings!=0)) 16 | var.user_mean = np.where(np.isnan(var.user_mean), var.all_mean, var.user_mean) 17 | var.item_mean = np.where(np.isnan(var.item_mean), var.all_mean, var.item_mean) 18 | 19 | def cal_similarity(kind, epsilon=1e-9): 20 | '''利用Cosine距离计算相似度''' 21 | '''epsilon: 防止Divide-by-zero错误,进行矫正''' 22 | if kind == 'user': 23 | sim = var.ratings.dot(var.ratings.T) + epsilon 24 | elif kind == 'item': 25 | sim = var.ratings.T.dot(var.ratings) + epsilon 26 | norms = np.array([np.sqrt(np.diagonal(sim))]) 27 | return (sim / norms / norms.T) 28 | 29 | def cal_similarity_norm(kind, epsilon=1e-9): 30 | '''采用归一化的指标:Pearson correlation coefficient''' 31 | if kind == 'user': 32 | # 对同一个user的打分归一化 33 | rating_user_diff = var.ratings.copy() 34 | for i in range(var.ratings.shape[0]): 35 | nzero = var.ratings[i].nonzero() 36 | rating_user_diff[i][nzero] = var.ratings[i][nzero] - var.user_mean[i] 37 | sim = rating_user_diff.dot(rating_user_diff.T) + epsilon 38 | elif kind == 'item': 39 | # 对同一个item的打分归一化 40 | rating_item_diff = var.ratings.copy() 41 | for j in range(var.ratings.shape[1]): 42 | nzero = var.ratings[:,j].nonzero() 43 | rating_item_diff[:,j][nzero] = var.ratings[:,j][nzero] - var.item_mean[j] 44 | sim = rating_item_diff.T.dot(rating_item_diff) + epsilon 45 | norms = np.array([np.sqrt(np.diagonal(sim))]) 46 | return (sim / norms / norms.T) 47 | 48 | def rmse(pred, actual): 49 | '''计算预测结果的rmse''' 50 | from sklearn.metrics import mean_squared_error 51 | pred = pred[actual.nonzero()].flatten() 52 | actual = actual[actual.nonzero()].flatten() 53 | return np.sqrt(mean_squared_error(pred, actual)) -------------------------------------------------------------------------------- /var.py: -------------------------------------------------------------------------------- 1 | ratings = 0 2 | all_mean = 0 3 | user_mean = 0 4 | item_mean = 0 5 | user_similarity = 0 6 | item_similarity = 0 7 | user_similarity_norm = 0 8 | item_similarity_norm = 0 9 | n_users = 943 10 | n_items = 1682 11 | --------------------------------------------------------------------------------