├── README.md ├── ch01 └── 1-recsys-intro.ipynb ├── ch02 └── 2-recsys-experimental-method.ipynb ├── ch03 └── 3-recsys-evaluation-indices.ipynb ├── ch04 └── 4-use-user-behavior-data.ipynb ├── ch05 └── 5-user-based-collaborative-filtering.ipynb ├── ch06 └── 6-item-based-collaborative-filtering.ipynb ├── ch07 └── 7-recsys-lfm.ipynb ├── ch08 └── 8-cold-start.ipynb ├── ch09 └── 9-user-tag-data.ipynb ├── materials └── syllabus.md └── recsys_images ├── 640px-Long_tail.svg.png ├── amazon_packaging_sale.png ├── amazon_rec_pane.png ├── keywords_generation.png ├── recsys_ab_test_system.png ├── recsys_participants.png └── user_item_relations.png /README.md: -------------------------------------------------------------------------------- 1 | # 推荐系统入门 2 | 3 | * [教学大纲](./materials/syllabus.md) 4 | * [课程内容](#org6614efg) 5 | 6 | 7 | 8 | ## 课程内容 9 | 10 | | 章节 | 名称 | 11 | |-------|-----------------------------------------------------------------------------| 12 | | 第1章 | [推荐系统概述](./ch01/1-recsys-intro.ipynb) | 13 | | 第2章 | [推荐系统实验方法](./ch02/2-recsys-experimental-method.ipynb) | 14 | | 第3章 | [推荐系统评测指标](./ch03/3-recsys-evaluation-indices.ipynb) | 15 | | 第4章 | [利用用户数据](./ch04/4-use-user-behavior-data.ipynb) | 16 | | 第5章 | [基于用户的协同过滤算法](./ch05/5-user-based-collaborative-filtering.ipynb) | 17 | | 第6章 | [基于物品的协同过滤算法](./ch06/6-item-based-collaborative-filtering.ipynb) | 18 | | 第7章 | [隐语义模型](./ch07/7-recsys-lfm.ipynb) | 19 | | 第8章 | [冷启动问题](./ch08/8-cold-start.ipynb) | 20 | | 第9章 | [利用用户标签数据](./ch09/9-user-tag-data.ipynb) | 21 | -------------------------------------------------------------------------------- /ch01/1-recsys-intro.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 推荐系统概述\n", 13 | "\n" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": { 19 | "ein.tags": "worksheet-0", 20 | "slideshow": { 21 | "slide_type": "-" 22 | } 23 | }, 24 | "source": [ 25 | "## 什么是推荐系统\n", 26 | "- 用户在**有明确需求**的情况下,面对信息过载所采用的措施\n", 27 | "\n", 28 | " 想买一包花生米,有多少种方法?\n", 29 | "\n", 30 | " - 24小时便利店(凭经验浏览所有货架)\n", 31 | " - 超市(借用分类信息)\n", 32 | " - 淘宝、京东(借助搜索引擎)\n", 33 | "\n", 34 | "- 用户**没有明确的需求**,怎么办?\n", 35 | "\n", 36 | " 想看一部电影,面对数不胜数的电影,怎么办?\n", 37 | "\n", 38 | " - 有人推荐\n", 39 | " - **一个自动化的工具,可以分析你的历史兴趣,找符合你兴趣的电影供你选择**\n", 40 | "\n", 41 | "上述“自动化的工具”,就是 **个性化推荐系统** 。\n", 42 | "\n", 43 | "**推荐系统基本任务是联系用户和物品,解决信息过载的问题。**" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": { 49 | "ein.tags": "worksheet-0", 50 | "slideshow": { 51 | "slide_type": "-" 52 | } 53 | }, 54 | "source": [ 55 | "\n", 56 | "### 信息过载\n", 57 | "\n", 58 | "身处信息过载(information overload)1的时代,人们遇到的挑战:\n", 59 | "\n", 60 | "- **信息消费者:**如何从大量信息中找到自己感兴趣的信息是一件非常困难的事情;\n", 61 | "- **信息生产者:**如何让自己生产的信息脱颖而出,受到广大用户的关注,也非常困难。\n", 62 | "\n", 63 | "推荐系统即是解决这一矛盾的重要工具。\n", 64 | "\n", 65 | "推荐系统的任务就是联系用户和信息,一方面**帮助用户发现对自己有价值的信息**,另一方面**让信息能够展现在对它感兴趣的用户面前**。2" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": { 71 | "ein.tags": "worksheet-0", 72 | "slideshow": { 73 | "slide_type": "-" 74 | } 75 | }, 76 | "source": [ 77 | "\n", 78 | "### 分类目录\n", 79 | "\n", 80 | "将著名的网站分门别类,从而方便用户根据类别查找网站。随着互联网规模的扩大,其只能覆盖少量的热门网站,不能满足用户的需求。" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": { 86 | "ein.tags": "worksheet-0", 87 | "slideshow": { 88 | "slide_type": "-" 89 | } 90 | }, 91 | "source": [ 92 | "\n", 93 | "### 搜索引擎\n", 94 | "让用户通过 **关键词** 找到自己需要的信息,但需要用户主动提供准确的关键词来寻找信息,当用户无法找到准确描述自己需求的关键词时,搜索引擎便无能为力了。\n", 95 | "\n", 96 | "推荐系统不需要用户提供明确的需求,而是通过分析用户的历史行为给用户的兴趣建模,从而主动给用户推荐能够满足其兴趣和需求的信息。\n", 97 | "\n", 98 | "推荐系统和搜索引擎对于用户来说是两个互补的工具。搜索引擎满足了用户有明确目的时的主动查找需求,而推荐系统能够在用户没有明确目的的时候帮助他们发现感兴趣的新内容。\n", 99 | "\n", 100 | "从物品的角度出发,推荐系统可以更好地发掘物品的长尾(long tail)。" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": { 106 | "ein.tags": "worksheet-0", 107 | "slideshow": { 108 | "slide_type": "-" 109 | } 110 | }, 111 | "source": [ 112 | "\n", 113 | "### 长尾理论\n", 114 | "\n", 115 | "传统的80/20原则(80%的销售额来自于20%的热门品牌)在互联网的加入下会受到挑战。互联网条件下,由于货架成本极端低廉,电子商务网站往往能出售比传统零售店更多的商品。虽然这些商品绝大多数都不热门,但与传统零售业相比,这些不热门的商品数量极其庞大,因此这些长尾商品的总销售额将是一个不可小觑的数字,也许会超过热门商品(即主流商品) 带来的销售额。主流商品往往代表了绝大多数用户的需求,而长尾商品往往代表了一小部分用户的个性化需求。\n", 116 | "\n", 117 | "因此,如果要通过发掘长尾提高销售额,就必须 **充分研究用户的兴趣** ,而这正是个性化推荐系统主要解决的问题。推荐系统通过发掘用户的行为,找到用户的个性化需求,从而将长尾商品准确地推荐给需要它的用户,帮助用户发现那些他们感兴趣但很难发现的商品。\n", 118 | "\n", 119 | "互联网数据分布满足一种称为 **PowerLaw** 分布3\n", 120 | "\n", 121 | "\\begin{equation}\n", 122 | "f (x) = \\alpha x^k \\nonumber\n", 123 | "\\end{equation}\n", 124 | "\n", 125 | "如果 $k<0$ ,就可以得到下面的图形:\n", 126 | "\n", 127 | "![img](../recsys_images/640px-Long_tail.svg.png)\n", 128 | "\n", 129 | "PowerLaw还有好几种变形,最著名的就是 Zipf's law4与 Pareto Distribution。\n", 130 | "\n", 131 | "现实社会中用户面对很多选择做决定的过程:\n", 132 | "\n", 133 | "- 社会化推荐(social recommendation),即让好友给自己推荐物品\n", 134 | "- 基于内容的推荐(content-based recommendation)\n", 135 | "- 找到和自己历史兴趣相似的一群用户,看看他们的选择,这种方式称之为基于协同过滤(collaborative filtering)的推荐\n", 136 | "\n", 137 | "**推荐算法的本质是通过一定的方式将用户和物品联系起来。**\n", 138 | "\n", 139 | "推荐系统就是自动联系用户和物品的一种工具,它能够在信息过载的环境中帮助用户发现令他们感兴趣的信息,也能将信息推送给对它们感兴趣的用户。" 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "metadata": { 145 | "ein.tags": "worksheet-0", 146 | "slideshow": { 147 | "slide_type": "-" 148 | } 149 | }, 150 | "source": [ 151 | "\n", 152 | "## 个性化推荐系统的应用\n", 153 | "\n", 154 | "个性化推荐系统需要依赖用户的行为数据,因此一般都是作为一个应用存在于不同网站之中。\n", 155 | "\n", 156 | "个性化推荐系统的标准用户界面:\n", 157 | "\n", 158 | "![img](../recsys_images/amazon_rec_pane.png)\n", 159 | "\n", 160 | "- 推荐结果的标题、缩略图以及其他内容属性(推荐的是什么)\n", 161 | "- 推荐结果的平均分(反应了推荐结果的总体质量)\n", 162 | "- 推荐理由(出于隐私的考虑,可允许用户禁用推荐理由)\n", 163 | "\n", 164 | "个性化推荐系统在这些网站中的主要作用是通过分析大量用户行为日志,给不同用户提供不同的个性化页面展示,来提高网站的点击率和转化率。\n" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": { 170 | "ein.tags": "worksheet-0", 171 | "slideshow": { 172 | "slide_type": "-" 173 | } 174 | }, 175 | "source": [ 176 | "- 电子商务\n", 177 | "\n", 178 | " 亚马逊:个性化商品推荐列表、相关商品推荐列表(包含购买和浏览不同用户行为来计算物品相关性,最重要的应用是 **打包销售** )\n", 179 | "\n", 180 | " ![img](../recsys_images/amazon_packaging_sale.png)\n" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "metadata": { 186 | "ein.tags": "worksheet-0", 187 | "slideshow": { 188 | "slide_type": "-" 189 | } 190 | }, 191 | "source": [ 192 | "- 电影和视频网站\n", 193 | " - Netflix\n", 194 | " - YouTube\n", 195 | " - Hulu\n", 196 | " - 优酷\n", 197 | " - 爱奇艺\n", 198 | " - 腾讯视频" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": { 204 | "ein.tags": "worksheet-0", 205 | "slideshow": { 206 | "slide_type": "-" 207 | } 208 | }, 209 | "source": [ 210 | "- 个性化音乐网络电台\n", 211 | " - Pandora(音乐基因工程,专家标注)\n", 212 | " - Last.fm(记录用户听歌记录及对歌曲的反馈)\n", 213 | " - 豆瓣电台\n", 214 | "\n", 215 | " 特点:都不允许用户点歌,而是给用户几种反馈方式——喜欢、不喜欢和跳过。经过用户一定时间的反馈,电台就可以从用户的历史行为中习得用户的兴趣模型,从而使用户的播放列表越来越符合用户对歌曲的兴趣。\n", 216 | "\n", 217 | " 音乐推荐特点:\n", 218 | "\n", 219 | " - 物品空间大\n", 220 | " - 消费每首歌的代价很小\n", 221 | " - 物品种类丰富\n", 222 | " - 听一首歌耗时很少\n", 223 | " - 物品重用率很高\n", 224 | " - 用户充满激情\n", 225 | " - 上下文相关\n", 226 | " - 次序很重要\n", 227 | " - 很多播放列表资源\n", 228 | " - 高度社会化(好友分享)\n", 229 | "\n", 230 | " 音乐是一种非常适合用来推荐的物品,音乐推荐可以支持独立的个性化推荐网站。\n" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": { 236 | "ein.tags": "worksheet-0", 237 | "slideshow": { 238 | "slide_type": "-" 239 | } 240 | }, 241 | "source": [ 242 | "- 社交网络\n", 243 | "\n", 244 | " 个性化推荐主要应用在3个方面:\n", 245 | "\n", 246 | " - 利用用户的社交网络信息对用户进行个性化的物品推荐\n", 247 | " - 信息流的会话推荐\n", 248 | " - 给用户推荐好友\n", 249 | "\n", 250 | " 最宝贵两个数据:一是用户之间的社交网络关系,另一个是用户的偏好信息。\n", 251 | "\n", 252 | " - Facebook\n", 253 | " - Twitter\n", 254 | " - 微博\n" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": { 260 | "ein.tags": "worksheet-0", 261 | "slideshow": { 262 | "slide_type": "-" 263 | } 264 | }, 265 | "source": [ 266 | "- 个性化阅读\n", 267 | " - 今日头条\n", 268 | " - Digg" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": { 274 | "ein.tags": "worksheet-0", 275 | "slideshow": { 276 | "slide_type": "-" 277 | } 278 | }, 279 | "source": [ 280 | "- 基于位置的服务\n", 281 | "\n", 282 | " 往往和社交网络结合在一起。\n", 283 | "\n", 284 | " - Foursquare\n", 285 | " - 街旁\n", 286 | " - 美团\n" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": { 292 | "ein.tags": "worksheet-0", 293 | "slideshow": { 294 | "slide_type": "-" 295 | } 296 | }, 297 | "source": [ 298 | "- 个性化邮件\n", 299 | " - Gmail优先级收件箱\n" 300 | ] 301 | }, 302 | { 303 | "cell_type": "markdown", 304 | "metadata": { 305 | "ein.tags": "worksheet-0", 306 | "slideshow": { 307 | "slide_type": "-" 308 | } 309 | }, 310 | "source": [ 311 | "- 个性化广告\n", 312 | "\n", 313 | " 广告是互联网公司生存的根本,广告的CPC(Cost Per Click,按点击付费)、 CPM(Cost Per Thousand Impressions,按千次显示付费)直接决定很多互联网公司的收入。\n", 314 | "\n", 315 | " 广告定向投放(Ad Targeting),如何将广告投放给它的潜在客户群。个性化广告投放——计算广告学。\n", 316 | "\n", 317 | " 个性化推荐着重于帮助用户找到可能令他们感兴趣的物品,而 **广告推荐着重于帮助广告找到可能对它们感兴趣的用户** ,一个是以用户为核心,另一个是以广告为核心。\n", 318 | "\n", 319 | " 目前的个性化广告投放技术:\n", 320 | "\n", 321 | " - 上下文广告(Google Adsense)\n", 322 | " - 搜索广告\n", 323 | " - 个性化展示广告(雅虎)\n", 324 | "\n", 325 | " Facebook因为拥有大量的用户个人资料,可以很容易地获取用户的兴趣,让广告商选择自己希望对其投放广告的用户。\n" 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "metadata": { 331 | "ein.tags": "worksheet-0", 332 | "slideshow": { 333 | "slide_type": "-" 334 | } 335 | }, 336 | "source": [ 337 | "## 脚注\n", 338 | "\n", 339 | "1 参见。\n", 340 | "\n", 341 | "2 后面将信息统称为“物品”,即可以供用户消费的东西。\n", 342 | "\n", 343 | "3 亦称 **长尾分布** , References:\n", 344 | "\n", 345 | "- [淺談網路世界的Power Law現象〈一〉── 什麼是Power Law](http://mmdays.com/2008/11/22/power_law_1/)\n", 346 | "- [淺談網路世界的Power Law現象〈二〉── 從長尾理論回看台灣唱片業興衰](http://mmdays.com/2008/11/24/power_law_2/)\n", 347 | "- [淺談網路世界的Power Law現象〈三〉── 書籤網站中的Power Group](http://mmdays.com/2008/12/02/power_law_3/)\n", 348 | "- [淺談網路世界的Power Law現象〈四〉── P2P軟體世界中的神人玩家](http://mmdays.com/2008/12/10/power_law_4/)\n", 349 | "\n", 350 | "4 在自然语言的语料库里,一个单词出现的频率与它在频率表里的排名成反比。如果将单词出现的频率按照由高到低排列,则每个单词出现的频率和它在热门排行榜中排名的常数次幂成反比。在英文中大部分词的词频其实很低,只有很少的词被经常使用。" 351 | ] 352 | } 353 | ], 354 | "metadata": { 355 | "kernelspec": { 356 | "display_name": "Python 3", 357 | "language": "python", 358 | "name": "python3" 359 | }, 360 | "language_info": { 361 | "codemirror_mode": { 362 | "name": "ipython", 363 | "version": 3 364 | }, 365 | "file_extension": ".py", 366 | "mimetype": "text/x-python", 367 | "name": "python", 368 | "nbconvert_exporter": "python", 369 | "pygments_lexer": "ipython3", 370 | "version": "3.6.4" 371 | }, 372 | "name": "1-recsys-intro.ipynb", 373 | "toc": { 374 | "colors": { 375 | "hover_highlight": "#ddd", 376 | "running_highlight": "#FF0000", 377 | "selected_highlight": "#ccc" 378 | }, 379 | "moveMenuLeft": true, 380 | "nav_menu": { 381 | "height": "161px", 382 | "width": "252px" 383 | }, 384 | "navigate_menu": true, 385 | "number_sections": false, 386 | "sideBar": true, 387 | "threshold": 4, 388 | "toc_cell": false, 389 | "toc_section_display": "block", 390 | "toc_window_display": false, 391 | "widenNotebook": false 392 | } 393 | }, 394 | "nbformat": 4, 395 | "nbformat_minor": 2 396 | } 397 | -------------------------------------------------------------------------------- /ch02/2-recsys-experimental-method.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 推荐系统实验方法\n", 13 | "什么才是好的推荐系统?\n", 14 | "\n", 15 | "一个完整的推荐系统一般存在3个参与方: **用户** 、 **物品提供者** 和 **提供推荐系统的网站** 。\n", 16 | "\n", 17 | "![img](../recsys_images/recsys_participants.png)\n", 18 | "\n", 19 | "考虑三方利益,实现共赢:\n", 20 | "\n", 21 | "1. 需要满足用户的需求,给用户推荐那些令其感兴趣的物品\n", 22 | "2. 要让所有物品提供者的物品都能够被推荐给对其感兴趣的用户\n", 23 | "3. 能够收集到高质量的用户反馈,不断完善推荐的质量,增加用户和网站的交互,提高网站收入\n", 24 | "\n", 25 | "**预测准确度是推荐系统领域的重要指标(没有之一)。** 其可以比较容易地通过离线方式计算出来,从而方便研究人员快速评价和选择不同的推荐算法。\n", 26 | "\n", 27 | "**准确的预测并不代表好的推荐。**\n", 28 | "\n", 29 | "好的推荐系统不仅 **能够准确预测用户的行为,而且能够扩展用户的视野,帮助用户发现那些他们可能会感兴趣,但却不那么容易发现的东西。** 同时,推荐系统还要能够帮助商家将那些被埋没在长尾中的好商品介绍给可能会对它们感兴趣的用户。" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": { 35 | "ein.tags": "worksheet-0", 36 | "slideshow": { 37 | "slide_type": "-" 38 | } 39 | }, 40 | "source": [ 41 | "\n", 42 | "## 离线实验\n", 43 | "\n", 44 | "1. 通过日志系统获得用户行为数据,并按照一定格式生成一个标准的数据集;\n", 45 | "2. 将数据集按照一定的规则分成训练集和测试集;\n", 46 | "3. 在训练集上训练用户兴趣模型,在测试集上进行预测;\n", 47 | "4. 通过事先定义的离线指标评测算法在测试集上的预测结果。\n", 48 | "\n", 49 | "**不需要实际的系统来供它实验,而只要有一个从实际系统日志中提取的数据集即可。**\n", 50 | "\n", 51 | "| 优点 | 缺点 |\n", 52 | "|----------------------------------|----------------------------------|\n", 53 | "| 不需要用户参与实验 | 无法计算商业上关心的指标 |\n", 54 | "| 速度快,可以测试大量算法 | 离线实验的指标和商业指标存在差距 |" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": { 60 | "ein.tags": "worksheet-0", 61 | "slideshow": { 62 | "slide_type": "-" 63 | } 64 | }, 65 | "source": [ 66 | "\n", 67 | "## 用户调查\n", 68 | "\n", 69 | "注意:预测准确率和用户满意度之间可能存在很大差别,高预测准确率不等于高用户满意度。\n", 70 | "\n", 71 | "用户调查需要一些真实用户,让其在需要测试的推荐系统上完成一些任务。在其完成任务时,观察和记录他们的行为,并让他们回答一些问题。最后,通过分析其行为和答案了解测试系统的性能。\n", 72 | "\n", 73 | "**用户调查是推荐系统评测的一个重要工具,很多离线没有办法评测的与用户主观感受有关的指标都可以通过用户调查获得,相对在线实验风险很低,出现错误后很容易弥补。**\n", 74 | "\n", 75 | "**用户调查的缺点:**\n", 76 | "\n", 77 | "- 成本高,需要用户花大量时间完成一个个任务并回答相关问题,有时需要花钱雇用测试用户\n", 78 | "- 大多数情况下很难进行大规模的用户调查,对于参加人数少的用户调查,很多结论往往没有统计意义\n", 79 | "\n", 80 | "开展用户调查:\n", 81 | "\n", 82 | "针对其缺点, **在做用户调查时,一方面要控制成本,另一方面又要保证结果的统计意义。**\n", 83 | "\n", 84 | "尽量保证测试用户的分布和真实用户的分布相同。\n", 85 | "\n", 86 | "尽量保证是 **双盲实验** 。" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": { 92 | "ein.tags": "worksheet-0", 93 | "slideshow": { 94 | "slide_type": "-" 95 | } 96 | }, 97 | "source": [ 98 | "\n", 99 | "## 在线实验\n", 100 | "\n", 101 | "将推荐系统上线做 **AB测试**1,将其和旧的算法进行比较。\n", 102 | "\n", 103 | "AB测试可以公平获得不同算法实际在线时的性能指标(包括商业上关注的指标),但其周期比较长。一般只用来测试那些在离线实验和用户调查中表现很好的算法。\n", 104 | "\n", 105 | "**切分流量** 是AB测试的关键,一个网站不同的层以及控制这些层的团队需要从一个统一的地方获得自己AB测试的流量,不同层之间的流量应该是独立的。\n", 106 | "\n", 107 | "![img](../recsys_images/recsys_ab_test_system.png \"一个简单的AB测试系统\")" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": { 113 | "ein.tags": "worksheet-0", 114 | "slideshow": { 115 | "slide_type": "-" 116 | } 117 | }, 118 | "source": [ 119 | "\n", 120 | "## 总结\n", 121 | "\n", 122 | "一般来说,一个新的推荐算法最终上线,需要完成上面所说的3个实验。\n", 123 | "\n", 124 | "1. 通过离线实验证明它在很多离线指标上优于现有的算法;\n", 125 | "2. 通过用户调查确定它的用户满意度不低于现有的算法;\n", 126 | "3. 通过在线的AB测试确定它在我们关心的指标上优于现有的算法。" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": { 132 | "ein.tags": "worksheet-0", 133 | "slideshow": { 134 | "slide_type": "-" 135 | } 136 | }, 137 | "source": [ 138 | "## 脚注\n", 139 | "\n", 140 | "1 AB测试是一种很常用的在线评测算法的实验方法。它通过一定的规则将用户随机分成几组,并对不同组的用户采用不同的算法,然后通过统计不同组用户的各种不同的评测指标比较不同算法。" 141 | ] 142 | } 143 | ], 144 | "metadata": { 145 | "kernelspec": { 146 | "display_name": "Python 3", 147 | "name": "python3" 148 | }, 149 | "name": "2-recsys-experimental-method.ipynb" 150 | }, 151 | "nbformat": 4, 152 | "nbformat_minor": 2 153 | } 154 | -------------------------------------------------------------------------------- /ch03/3-recsys-evaluation-indices.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 推荐系统评测指标\n", 13 | "本节将介绍各种推荐系统的评测指标,可用于评价推荐系统各方面的性能。有些指标可以定量计算,有些只能定性描述,有的可通过离线实验计算,有的需通过用户调查获得,还有的只能在线评测。" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": { 19 | "ein.tags": "worksheet-0", 20 | "slideshow": { 21 | "slide_type": "-" 22 | } 23 | }, 24 | "source": [ 25 | "\n", 26 | "## 用户满意度\n", 27 | "\n", 28 | "用户满意度是评测推荐系统的最重要指标,只能通过用户调查或在线实验获得。\n", 29 | "\n", 30 | "用户调查获得用户满意度主要是通过调查问卷的形式。\n", 31 | "\n", 32 | "例如如下的调查问题:\n", 33 | "\n", 34 | "下面哪句话最能描述你看到推荐结果后的感受?\n", 35 | "\n", 36 | "- 推荐的物品都是我非常想看的。\n", 37 | "- 推荐的物品很多我都看过了,确实是符合我兴趣的不错物品。\n", 38 | "- 推荐的物品和我的兴趣是相关的,但我并不喜欢。\n", 39 | "- 不知道为什么会推荐这些物品,它们和我的兴趣丝毫没有关系。\n", 40 | "\n", 41 | "在设计问卷时需要考虑用户各方面的感受,如此用户才能针对问题给出自己准确的回答。\n", 42 | "\n", 43 | "在在线系统中,用户满意度主要通过一些对用户行为的统计得到。\n", 44 | "\n", 45 | "有些网站会通过设计一些用户反馈界面收集用户满意度。\n", 46 | "\n", 47 | "更一般的情况,可以用点击率、用户停留时间和转化率等指标度量用户的满意度。" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": { 53 | "ein.tags": "worksheet-0", 54 | "slideshow": { 55 | "slide_type": "-" 56 | } 57 | }, 58 | "source": [ 59 | "\n", 60 | "## 预测准确度\n", 61 | "\n", 62 | "度量一个推荐系统或者推荐算法预测用户行为的能力。\n", 63 | "\n", 64 | "**最重要的推荐系统离线评测指标,该指标可通过离线实验计算。**\n", 65 | "\n", 66 | "计算该指标时需要有一个离线的数据集,该数据集包含用户的历史行为记录。将该数据集分成训练集和测试集。通过在训练集上建立用户的行为和兴趣模型预测用户在测试集上的行为,并计算预测行为和测试集上实际行为的重合度作为预测准确度。\n", 67 | "\n", 68 | "预测准确度指标:\n", 69 | "\n", 70 | "- 评分预测(预测用户对物品评分的行为)\n", 71 | "\n", 72 | " 评分预测的预测准确度一般通过 **均方根误差(root-mean-square error,RMSE)** 和 **平均绝对误差(mean absolute error,MAE)** 计算。\n", 73 | "\n", 74 | " 对于测试集中的一个用户 $u$ 和 物品 $i$ ,令 $r_{ui}$ 是用户 $u$ 对物品 $i$ 的实际评分,而 $\\hat{r}_{ui}$ 是推荐算法给出的预测评分,则有:\n", 75 | "\n", 76 | " \\begin{equation}\n", 77 | " RMSE = \\sqrt{\\frac{\\sum_{u,i \\in T}(r_{ui} - \\hat{r}_{ui})^2}{|T|}} \\nonumber\n", 78 | " \\end{equation}\n", 79 | "\n", 80 | " MAE采用绝对值计算预测误差,其定义为:\n", 81 | "\n", 82 | " \\begin{equation}\n", 83 | " MAE = \\frac{\\sum_{u,i \\in T}|r_{ui} - \\hat{r}_{ui}|}{|T|} \\nonumber\n", 84 | " \\end{equation}\n", 85 | "\n", 86 | " Netflix认为RMSE加大了对预测不准的用户物品评分的惩罚(平方项的惩罚),因而对系统的评测更加苛刻。研究表明,如果评分系统是基于整数建立的(即用户给的评分都是整数),那么对预测结果取整会降低MAE的误差1。\n", 87 | " \n", 88 | " ```python\n", 89 | " import math\n", 90 | "\n", 91 | " def RMSE(records):\n", 92 | " \"\"\"均方根误差\n", 93 | "\n", 94 | " Args:\n", 95 | " - records:存放用户评分数据,records[i] = [u, i, rui, pui]\n", 96 | " \"\"\"\n", 97 | " return math.sqrt(sum([(rui - pui) ** 2 for u, i, rui, pui in records]) / float(len(records)))\n", 98 | "\n", 99 | " def MAE(records):\n", 100 | " \"\"\"平均绝对误差\n", 101 | "\n", 102 | " Args:\n", 103 | " - records:存放用户评分数据,records[i] = [u, i, rui, pui]\n", 104 | " \"\"\"\n", 105 | " return sum([abs(rui - pui) for u, i, rui, pui in records]) / float(len(records))\n", 106 | " ```\n", 107 | "\n", 108 | "- TopN推荐\n", 109 | "\n", 110 | " 网站提供推荐服务时,一般是给用户一个个性化的推荐列表,这种推荐叫做 **TopN推荐** 。\n", 111 | "\n", 112 | " TopN推荐的预测准确率一般通过准确率(precision)和召回率(recall)度量。\n", 113 | "\n", 114 | " - 召回率\n", 115 | "\n", 116 | " 令 $R(u)$ 是根据用户在训练集上的行为给用户作出的推荐列表,而 $T(u)$ 是用户在测试集上的行为列表。那么,推荐结果的召回率定义为:\n", 117 | "\n", 118 | " \\begin{equation}\n", 119 | " Recall=\\frac{\\sum_{u\\in U}|R(u) \\cap T(u)|}{\\sum_{u\\in U}|T(u)|} \\nonumber\n", 120 | " \\end{equation}\n", 121 | "\n", 122 | " - 准确率\n", 123 | "\n", 124 | " \\begin{equation}\n", 125 | " Precision=\\frac{\\sum_{u\\in U}|R(u) \\cap T(u)|}{\\sum_{u\\in U}|R(u)|} \\nonumber\n", 126 | " \\end{equation}\n", 127 | "\n", 128 | " 下面的代码同时计算一个推荐算法的准确率和召回率:\n", 129 | "\n", 130 | " ```python \n", 131 | " def precision_recall(test, N, Recommend=None):\n", 132 | " \"\"\"准确率、召回率\n", 133 | "\n", 134 | " Args:\n", 135 | " - test: 测试集\n", 136 | " - N: 推荐物品个数\n", 137 | " - Recommend: 推荐模型算法\n", 138 | "\n", 139 | " :return: 准确率和召回率\n", 140 | " \"\"\"\n", 141 | " hit = 0\n", 142 | " n_recall = 0\n", 143 | " n_precision = 0\n", 144 | " for user, items in test.items():\n", 145 | " rank = Recommend(user, N)\n", 146 | " hit += len(rank & items)\n", 147 | " n_recall += len(items)\n", 148 | " n_precision += N\n", 149 | " return hit / (1.0 * n_recall), hit / (1.0 * n_precision) \n", 150 | " ```\n", 151 | "\n", 152 | "有时,为了全面评测TopN推荐的准确率和召回率,一般会选取不同的推荐列表长度N,计算出一组准确率/召回率,然后画出准确率/召回率曲线(precision/recall curve)。" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": { 158 | "ein.tags": "worksheet-0", 159 | "slideshow": { 160 | "slide_type": "-" 161 | } 162 | }, 163 | "source": [ 164 | "\n", 165 | "## 覆盖率\n", 166 | "\n", 167 | "覆盖率(coverage)描述一个推荐系统对物品长尾的发掘能力。\n", 168 | "\n", 169 | "覆盖率有不同的定义方法,最简单的定义为 **推荐系统能够推荐出来的物品占总物品集合的比例。**\n", 170 | "\n", 171 | "系统用户集合为 $U$ ,推荐系统给每个用户推荐一个长度为 $N$ 的物品列表 $R(u)$ ,则覆盖率为:\n", 172 | "\n", 173 | "\\begin{equation}\n", 174 | "Coverage=\\frac{U_{u\\in U}R(u)}{|I|} \\nonumber\n", 175 | "\\end{equation}\n", 176 | "\n", 177 | "覆盖率是一个内容提供商会关心的指标。\n", 178 | "\n", 179 | "覆盖率为100%的推荐系统可以将每个物品都推荐给至少一个用户。\n", 180 | "\n", 181 | "**一个好的推荐系统不仅需要有比较高的用户满意度,也要有较高的覆盖率。**\n", 182 | "\n", 183 | "**为了更细致地描述推荐系统发掘长尾的能力,需要统计推荐列表中不同物品出现次数的分布。** 如果所有的物品都出现在推荐列表中,且出现的次数差不多,那么推荐系统发掘长尾的能力就很好。因此,可以通过研究物品在推荐列表中出现次数的分布描述推荐系统挖掘长尾的能力。\n", 184 | "\n", 185 | "如果这个分布比较平,那么说明推荐系统的覆盖率较高,而如果这个分布较陡峭,说明推荐系统的覆盖率较低。\n", 186 | "\n", 187 | "在信息论和经济学中有两个著名的指标可以用来定义覆盖率。第一个是信息熵:\n", 188 | "\n", 189 | "\\begin{equation}\n", 190 | "H=-\\underset{i=1}{\\overset{n}{\\sum}} p(i) \\log p(i) \\nonumber\n", 191 | "\\end{equation}\n", 192 | "\n", 193 | "$p(i)$ 是物品 $i$ 的流行度除以所有物品流行度之和。\n", 194 | "\n", 195 | "第二个指标是基尼系数(Gini Index)2:\n", 196 | "\n", 197 | "\\begin{equation}\n", 198 | "G=\\frac{1}{n-1}\\underset{j=1}{\\overset{n}{\\sum}}(2j-n-1)p(i_{j}) \\nonumber\n", 199 | "\\end{equation}\n", 200 | "\n", 201 | "这里,$i_{j}$ 是按照物品流行度 $p$ 从小到大排序的物品列表中第 $j$ 个物品。\n", 202 | "\n", 203 | "下面的代码可用来计算给定物品流行度分布后的基尼系数:\n", 204 | "\n", 205 | "```python\n", 206 | "def gini_index(p):\n", 207 | " \"\"\"基尼系数\n", 208 | "\n", 209 | " Args:\n", 210 | " - p: the dict of item popularity\n", 211 | "\n", 212 | " :return: gini index\n", 213 | " \"\"\"\n", 214 | " j = 1\n", 215 | " n = len(p)\n", 216 | " G = 0\n", 217 | " for item, weight in sorted(p.items(), key=lambda q: q[1]):\n", 218 | " G += (2 * j - n - 1) * weight\n", 219 | " j += 1\n", 220 | " return G / float(n - 1)\n", 221 | "```\n", 222 | "\n", 223 | "评测推荐算法具有马太效应:\n", 224 | "\n", 225 | "评测推荐系统是否具有马太效应的简单办法就是使用基尼系数。如果 $G1$ 是从初始用户行为中计算出的物品流行度的基尼系数, $G2$ 是从推荐列表中计算出的物品流行度的基尼系数,那么如果 $G2 > G1$ ,就说明推荐算法具有马太效应。" 226 | ] 227 | }, 228 | { 229 | "cell_type": "markdown", 230 | "metadata": { 231 | "ein.tags": "worksheet-0", 232 | "slideshow": { 233 | "slide_type": "-" 234 | } 235 | }, 236 | "source": [ 237 | "\n", 238 | "## 多样性\n", 239 | "\n", 240 | "多样性描述推荐列表中物品两两之间的不相似性。多样性和相似性是对应的。\n", 241 | "\n", 242 | "假设 $s(i,j)\\in [0,1]$ 定义了物品 $i$ 和 $j$ 之间的相似度,那么用户 $u$ 的推荐列表 $R(u)$ 的多样性定义如下:\n", 243 | "\n", 244 | "\\begin{equation}\n", 245 | "Diversity(R(u))=1-\\frac{\\sum_{i,j\\in R(u),i\\ne j}s(i,j)}{\\frac{1}{2}|R(u)|(|R(u)|-1)} \\nonumber\n", 246 | "\\end{equation}\n", 247 | "\n", 248 | "推荐系统的整体多样性可以定义为所有用户推荐列表多样性的平均值:\n", 249 | "\n", 250 | "\\begin{equation}\n", 251 | "Diversity=\\frac{1}{|U|}\\underset{u\\in U}{\\sum}Diversity(R(u)) \\nonumber\n", 252 | "\\end{equation}\n", 253 | "\n", 254 | "**不同的物品相似度度量函数 $s(i,j)$ 可以定义不同的多样性。**" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": { 260 | "ein.tags": "worksheet-0", 261 | "slideshow": { 262 | "slide_type": "-" 263 | } 264 | }, 265 | "source": [ 266 | "\n", 267 | "## 新颖性\n", 268 | "\n", 269 | "给用户推荐那些他们以前没有听说过的物品。\n", 270 | "\n", 271 | "在一个网站中实现新颖性的最简单办法是吧那些用户之前在网站中对其有过行为的物品从推荐列表中过滤掉。\n", 272 | "\n", 273 | "评测新颖度的最简单办法是利用推荐结果的平均流行度,因为越不热门的物品越可能让用户觉得新颖。如果推荐结果中物品的平均热门程度较低,那么推荐结果就可能有比较高的新颖性。\n", 274 | "\n", 275 | "不同用户不知道的东西是不同的,要准确地统计新颖性需要做用户调查。" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "metadata": { 281 | "ein.tags": "worksheet-0", 282 | "slideshow": { 283 | "slide_type": "-" 284 | } 285 | }, 286 | "source": [ 287 | "\n", 288 | "## 惊喜度(serendipity)\n", 289 | "\n", 290 | "惊喜度与新颖性有什么区别?\n", 291 | "\n", 292 | "如果推荐结果和用户的历史兴趣不相似,但却让用户觉得满意,那么就可以说推荐结果的惊喜度很高,而推荐的新颖性仅取决于用户是否听说过这个推荐结果。\n", 293 | "\n", 294 | "提高推荐惊喜度需要提高推荐结果的用户满意度,同时降低推荐结果和用户历史兴趣的相似度。" 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "metadata": { 300 | "ein.tags": "worksheet-0", 301 | "slideshow": { 302 | "slide_type": "-" 303 | } 304 | }, 305 | "source": [ 306 | "\n", 307 | "## 信任度\n", 308 | "\n", 309 | "如果用户信任推荐系统,那么就会增加用户和推荐系统的交互。\n", 310 | "\n", 311 | "度量推荐系统的信任度只能通过问卷调查的方式,询问用户是否信任推荐系统的推荐结果。\n", 312 | "\n", 313 | "两种方法:\n", 314 | "\n", 315 | "1. 增加推荐系统的透明度(transparency),主要办法是提供推荐解释。\n", 316 | "2. 考虑用户社交网络信息,利用用户好久信息给其做推荐,并用好友进行推荐解释。\n", 317 | "\n", 318 | "此问题的研究主要集中在评论网站Epinion的推荐系统上。" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": { 324 | "ein.tags": "worksheet-0", 325 | "slideshow": { 326 | "slide_type": "-" 327 | } 328 | }, 329 | "source": [ 330 | "\n", 331 | "## 实时性\n", 332 | "\n", 333 | "包括两个方面:\n", 334 | "\n", 335 | "1. 推荐系统需要实时地更新推荐列表来满足用户新的行为变化\n", 336 | "\n", 337 | " 与用户行为相应的实时性,可通过推荐列表的变化速率来评测。如果推荐列表在用户有行为后变化不大,或者没有变化,说明推荐系统的实时性不高。\n", 338 | "\n", 339 | "2. 能够将新加入系统的物品推荐给用户\n", 340 | "\n", 341 | " 主要考验推荐系统处理物品冷启动的能力。\n", 342 | "\n", 343 | " 对于新物品推荐能力,可利用用户推荐列表中有多大比例的物品是当天新加的来评测。" 344 | ] 345 | }, 346 | { 347 | "cell_type": "markdown", 348 | "metadata": { 349 | "ein.tags": "worksheet-0", 350 | "slideshow": { 351 | "slide_type": "-" 352 | } 353 | }, 354 | "source": [ 355 | "\n", 356 | "## 健壮性\n", 357 | "\n", 358 | "健壮性衡量了一个推荐系统抗击作弊的能力。\n", 359 | "\n", 360 | "作弊方法:\n", 361 | "\n", 362 | "- 行为注入攻击(profile injection attack)\n", 363 | "- 针对评分系统的攻击\n", 364 | "\n", 365 | "**利用模拟攻击的方法来评测推荐算法的健壮性。**\n", 366 | "\n", 367 | "提高系统的健壮性,可以选择健壮性高的算法,还有以下方法:\n", 368 | "\n", 369 | "- 设计推荐系统时尽量使用代价比较高的用户行为\n", 370 | "- 在使用数据前,进行攻击检测,从而对数据进行清理" 371 | ] 372 | }, 373 | { 374 | "cell_type": "markdown", 375 | "metadata": { 376 | "ein.tags": "worksheet-0", 377 | "slideshow": { 378 | "slide_type": "-" 379 | } 380 | }, 381 | "source": [ 382 | "\n", 383 | "## 商业目标\n", 384 | "\n", 385 | "最本质的商业目标就是平均一个用户给公司带来的盈利。\n", 386 | "\n", 387 | "不同的网站具有不同的商业目标。\n", 388 | "\n", 389 | "设计推荐系统时需要考虑最终的商业目标,除了满足用户发现内容的需求,也要利用推荐系统加快实现商业上的指标。" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "metadata": { 395 | "ein.tags": "worksheet-0", 396 | "slideshow": { 397 | "slide_type": "-" 398 | } 399 | }, 400 | "source": [ 401 | "\n", 402 | "## 总结\n", 403 | "\n", 404 | "| | 离线实验 | 问卷调查 | 在线实验 |\n", 405 | "|------------|----------|----------|----------|\n", 406 | "| 用户满意度 | × | √ | ○ |\n", 407 | "| 预测准确度 | √ | √ | × |\n", 408 | "| 覆盖率 | √ | √ | √ |\n", 409 | "| 多样性 | ○ | √ | ○ |\n", 410 | "| 新颖性 | ○ | √ | ○ |\n", 411 | "| 惊喜度 | × | √ | × |\n", 412 | "\n", 413 | "对于可以离线优化的指标,应该在给定覆盖率、多样性、新颖性等限制条件下,尽量优化预测准确度。" 414 | ] 415 | }, 416 | { 417 | "cell_type": "markdown", 418 | "metadata": { 419 | "ein.tags": "worksheet-0", 420 | "slideshow": { 421 | "slide_type": "-" 422 | } 423 | }, 424 | "source": [ 425 | "\n", 426 | "\n", 427 | "## 评测维度\n", 428 | "\n", 429 | "增加评测维度的目的是知道一个算法在什么情况下性能最好,可以为融合不同推荐算法取得最好的整体性能带来参考。\n", 430 | "\n", 431 | "一般来说,评测维度分为如下3种:\n", 432 | "\n", 433 | "- **用户维度:** 主要包括用户的人口统计学信息、活跃度以及是不是新用户等。\n", 434 | "- **物品维度:** 包括物品的属性信息、流行度、平均分以及是不是新加入的物品等。\n", 435 | "- **时间维度:** 包括季节,是工作日还是周末,是白天还是晚上等。\n", 436 | "\n", 437 | "如果能够在推荐系统评测报告中包含不同维度下的系统评测指标,就能帮助全面地了解推荐系统性能,找到一个看上去比较弱的算法的优势,发现一个看上去比较强的算法的缺点。" 438 | ] 439 | }, 440 | { 441 | "cell_type": "markdown", 442 | "metadata": { 443 | "ein.tags": "worksheet-0", 444 | "slideshow": { 445 | "slide_type": "-" 446 | } 447 | }, 448 | "source": [ 449 | "## 脚注\n", 450 | "\n", 451 | "1 Gábor Takács、István Pilászy和Bottyán Németb的论文“Major components of the gravity recommendation system”。\n", 452 | "\n", 453 | "2 参见Guy Shani和 Asela Gunawardana的“Evaluating Recommendation Systems”。" 454 | ] 455 | } 456 | ], 457 | "metadata": { 458 | "kernelspec": { 459 | "display_name": "Python 3", 460 | "language": "python", 461 | "name": "python3" 462 | }, 463 | "language_info": { 464 | "codemirror_mode": { 465 | "name": "ipython", 466 | "version": 3 467 | }, 468 | "file_extension": ".py", 469 | "mimetype": "text/x-python", 470 | "name": "python", 471 | "nbconvert_exporter": "python", 472 | "pygments_lexer": "ipython3", 473 | "version": "3.6.4" 474 | }, 475 | "name": "3-recsys-evaluation-indices.ipynb", 476 | "toc": { 477 | "colors": { 478 | "hover_highlight": "#ddd", 479 | "running_highlight": "#FF0000", 480 | "selected_highlight": "#ccc" 481 | }, 482 | "moveMenuLeft": true, 483 | "nav_menu": { 484 | "height": "272px", 485 | "width": "252px" 486 | }, 487 | "navigate_menu": true, 488 | "number_sections": false, 489 | "sideBar": true, 490 | "threshold": 4, 491 | "toc_cell": false, 492 | "toc_section_display": "block", 493 | "toc_window_display": false, 494 | "widenNotebook": false 495 | } 496 | }, 497 | "nbformat": 4, 498 | "nbformat_minor": 2 499 | } 500 | -------------------------------------------------------------------------------- /ch04/4-use-user-behavior-data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 利用用户行为数据\n", 13 | "如何了解一个人呢?\n", 14 | "\n", 15 | "**通过用户留下的文字和行为了解用户兴趣和需求。**\n" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": { 21 | "ein.tags": "worksheet-0", 22 | "slideshow": { 23 | "slide_type": "-" 24 | } 25 | }, 26 | "source": [ 27 | "实现个性化推荐的最理想情况是用户在注册的时候主动告知其喜欢什么。\n", 28 | "\n", 29 | "3个缺点:\n", 30 | "\n", 31 | "1. 现在的自然语言理解技术很难理解用户用来描述兴趣的自然语言;\n", 32 | "2. 用户的兴趣是不断变化的;\n", 33 | "3. 很多时候用户并不知道自己喜欢什么,或者很难用语言描述。\n" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": { 39 | "ein.tags": "worksheet-0", 40 | "slideshow": { 41 | "slide_type": "-" 42 | } 43 | }, 44 | "source": [ 45 | "**基于用户行为分析的推荐算法是个性化推荐系统的重要算法,学术界一般将这种类型的算法称为协同过滤1算法。**\n", 46 | "**用户行为数据在网站上最简单的存在形式就是日志。**\n", 47 | "\n", 48 | "- 原始日志(raw log)\n", 49 | "- 会话日志(session log)\n", 50 | "\n", 51 | " 将多种原始日志按照用户行为汇总,每个会话表示一次用户行为和对应的服务。\n", 52 | "\n", 53 | "- 展示日志(impression log)\n", 54 | "\n", 55 | " 搜索引擎和搜索广告系统服务为每次查询生成的,记录了查询和返回结果。\n", 56 | "\n", 57 | "- 点击日志(click log)\n", 58 | "\n", 59 | " 用户点击了某个返回结果的点击信息。\n", 60 | "\n", 61 | "一个并行程序会周期性地归并展示日志和点击日志,得到的会话日志中每个消息是一个用户提交的查询、得到的结果以及点击。\n", 62 | "\n", 63 | "推荐系统和电子商务网站也会汇总原始日志生成描述用户行为的会话日志。会话日志通常存储在分布式数据仓库中,这些日志记录了用户的各种行为。\n" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": { 69 | "ein.tags": "worksheet-0", 70 | "slideshow": { 71 | "slide_type": "-" 72 | } 73 | }, 74 | "source": [ 75 | "用户行为在个性化推荐系统中一般分为两种:\n", 76 | "- **显性反馈行为(explicit feedback):** 用户明确表示对物品喜好的行为。主要方式就是 **评分** 和 **喜欢/不喜欢** 。\n", 77 | "\n", 78 | "- **隐性反馈行为(implicit feedback):** 不能明确反应用户喜好的行为。最具代表性的就是 **页面浏览行为** 。\n", 79 | "\n", 80 | "| | 显性反馈数据 | 隐性反馈数据 |\n", 81 | "|----------|--------------|----------------|\n", 82 | "| 用户兴趣 | 明确 | 不明确 |\n", 83 | "| 数量 | 较少 | 庞大 |\n", 84 | "| 存储 | 数据库 | 分布式文件系统 |\n", 85 | "| 实时读取 | 实时 | 有延迟 |\n", 86 | "| 正负反馈 | 都有 | 只有正反馈 |\n" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": { 92 | "ein.tags": "worksheet-0", 93 | "slideshow": { 94 | "slide_type": "-" 95 | } 96 | }, 97 | "source": [ 98 | "按照反馈的方向分,可分为:\n", 99 | "- **正反馈:** 用户的行为倾向于指用户喜欢该物品。\n", 100 | "- **负反馈:** 用户的行为倾向于指用户不喜欢该物品。\n", 101 | "\n", 102 | "在显性反馈中,很容易区分一个用户行为是正反馈还是负反馈,在隐性反馈行为中,相对比较难以确定。\n", 103 | "\n", 104 | "| | 显性反馈 | 隐性反馈 |\n", 105 | "|--------------|----------------------------|----------------------------------------|\n", 106 | "| 视频网站 | 用户对视频的评分 | 用户观看视频的日志、浏览视频页面的日志 |\n", 107 | "| 电子商务网站 | 用户对商品的评分 | 购买日志、浏览日志 |\n", 108 | "| 门户网站 | 用户对新闻的评分 | 阅读新闻的日志 |\n", 109 | "| 音乐网站 | 用户对音乐/歌手/专辑的评分 | 听歌的日志 |\n" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": { 115 | "ein.tags": "worksheet-0", 116 | "slideshow": { 117 | "slide_type": "-" 118 | } 119 | }, 120 | "source": [ 121 | "一种统一的表示用户行为的方式,它将一个用户行为表示为6部分,即产生行为的用户和行为的对象、行为的种类、产生行为的上下文、行为的内容和权重。\n", 122 | "\n", 123 | "| 类型 | 标识 | 解释 |\n", 124 | "|------------------|------------------|----------------------------------------------------------------------------------------------------|\n", 125 | "| 产生行为的用户 | user id | 产生行为的用户的唯一标识 |\n", 126 | "| 行为的对象 | item id | 产生行为的对象的唯一标识 |\n", 127 | "| 行为的种类 | behavior type | 行为的种类(比如是购买还是浏览) |\n", 128 | "| 产生行为的上下文 | context | 产生行为的上下文,包括时间和地点等 |\n", 129 | "| 行为的权重 | behavior weight | 行为的权重(如果是观看视频的行为,那么这个权重可以是观看时长;如果是打分行为,这个权重可以是分数) |\n", 130 | "| 行为的内容 | behavior content | 行为的内容(如果是评论行为,那么就是评论的文本;如果是打标签的行为,就是标签) |" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": { 136 | "ein.tags": "worksheet-0", 137 | "slideshow": { 138 | "slide_type": "-" 139 | } 140 | }, 141 | "source": [ 142 | "不同的数据集包含不同的行为,目前比较有代表性的数据集有下面几个。\n", 143 | "- **无上下文信息的隐性反馈数据集**\n", 144 | "\n", 145 | " 每一条行为记录仅仅包含用户ID和物品ID。\n", 146 | "\n", 147 | " Book-Crossing数据集。2\n", 148 | "\n", 149 | "- 无上下文信息的显性反馈数据集\n", 150 | "\n", 151 | " 每一条记录包含用户ID、物品ID和用户对物品的评分。\n", 152 | "\n", 153 | "- 有上下文信息的隐性反馈数据集\n", 154 | "\n", 155 | " 每一条记录包含用户ID、物品ID和用户对物品产生行为的时间戳。\n", 156 | "\n", 157 | " Lastfm数据集。3\n", 158 | "\n", 159 | "- 有上下文信息的显性反馈数据集\n", 160 | "\n", 161 | " 每一条记录包含用户ID、物品ID、用户对物品的评分和评分行为发生的时间戳。\n", 162 | "\n", 163 | " Netflix Prize数据集。4\n", 164 | "\n" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": { 170 | "ein.tags": "worksheet-0", 171 | "slideshow": { 172 | "slide_type": "-" 173 | } 174 | }, 175 | "source": [ 176 | "\n", 177 | "## 用户行为分析\n", 178 | "\n", 179 | "本节将介绍用户行为数据中蕴含的一般规律,这些规律是存在于很多网站中的普遍规律。\n", 180 | "\n", 181 | "\n", 182 | "\n", 183 | "\n", 184 | "### 用户活跃度和物品流行度的分布\n", 185 | "\n", 186 | "互联网上的很多数据分布都满足一种称为Power Law的分布,在互联网领域也称长尾分布。\n", 187 | "\n", 188 | "用户行为数据也蕴含这种规律。令 $f_u(k)$ 为对 $k$ 个物品产生过行为的用户数,令 $f_i(k)$ 为被 $k$ 个用户产生过行为的物品数, $f_u(k)$ 和 $f_i(k)$ 都满足长尾分布:\n", 189 | "\n", 190 | "\\begin{equation}\n", 191 | "\\begin{split}\n", 192 | "& f_i(k) = \\alpha_i k^{\\beta_i} \\\\\n", 193 | "& f_u(k) = \\alpha_u k^{\\beta_u} \\\\\n", 194 | "\\end{split} \\nonumber\n", 195 | "\\end{equation}\n", 196 | "\n", 197 | "可选择 Delicious 和 CiteULike 数据集进行分析。\n", 198 | "\n" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": { 204 | "ein.tags": "worksheet-0", 205 | "slideshow": { 206 | "slide_type": "-" 207 | } 208 | }, 209 | "source": [ 210 | "\n", 211 | "### 用户活跃度和物品流行度的关系\n", 212 | "\n", 213 | "一般认为,新用户倾向于浏览热门的物品,因其对网站还不熟悉,而老用户会逐渐开始浏览冷门的物品。\n", 214 | "\n", 215 | "**用户越活跃,越倾向于浏览冷门的物品。**\n", 216 | "\n" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": { 222 | "ein.tags": "worksheet-0", 223 | "slideshow": { 224 | "slide_type": "-" 225 | } 226 | }, 227 | "source": [ 228 | "\n", 229 | "## 协同过滤算法\n", 230 | "\n", 231 | "仅仅基于用户行为数据设计的推荐算法一般称为协同过滤算法。\n", 232 | "\n", 233 | "协同过滤算法中有很多方法,比如基于邻域的方法(neighborhood-based)、隐语义模型(latent factor model)等。\n" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": { 239 | "ein.tags": "worksheet-0", 240 | "slideshow": { 241 | "slide_type": "-" 242 | } 243 | }, 244 | "source": [ 245 | "\n", 246 | "### 基于邻域(neighborhood-based)的算法\n", 247 | "\n", 248 | "最著名、在业界得到最广泛应用的算法是基于邻域的方法,主要包括:\n", 249 | "\n", 250 | "- **基于用户的协同过滤算法:** 给用户推荐和其兴趣相似的其他用户喜欢的物品。\n", 251 | "- **基于物品的协同过滤算法:** 给用户推荐和其之前喜欢的物品相似的物品。\n", 252 | "\n" 253 | ] 254 | }, 255 | { 256 | "cell_type": "markdown", 257 | "metadata": { 258 | "ein.tags": "worksheet-0", 259 | "slideshow": { 260 | "slide_type": "-" 261 | } 262 | }, 263 | "source": [ 264 | "\n", 265 | "### 实验设计和算法评测\n", 266 | "\n", 267 | "本节将通过离线实验方法评测提到的算法。\n", 268 | "\n", 269 | "首先介绍用到的数据集,然后介绍采用的实验方法和评测指标。\n", 270 | "\n" 271 | ] 272 | }, 273 | { 274 | "cell_type": "markdown", 275 | "metadata": { 276 | "ein.tags": "worksheet-0", 277 | "slideshow": { 278 | "slide_type": "-" 279 | } 280 | }, 281 | "source": [ 282 | "\n", 283 | "#### 数据集\n", 284 | "本章采用GroupLens提供的MovieLens数据集介绍和评测各种算法。 MovieLens数据集有3个不同的版本,本章选用中等大小的数据集。该数据集包含6000多用户对4000多部电影的100万条评分。该数据集是一个评分数据集,用户可以给电影评5个不同等级的分数(1~5分)。本章着重研究隐反馈数据集中的TopN推荐问题,因此忽略了数据集中的评分记录。也就是说,TopN推荐的任务是预测用户会不会对某部电影评分,而不是预测用户在准备对某部电影评分的前提下会给电影评多少分。\n", 285 | "\n", 286 | "加载数据:\n" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": null, 292 | "metadata": { 293 | "autoscroll": false, 294 | "ein.tags": "worksheet-0", 295 | "slideshow": { 296 | "slide_type": "-" 297 | } 298 | }, 299 | "outputs": [], 300 | "source": [ 301 | "def load_movielens(path='./movielens/ml-100k'):\n", 302 | " # get movie titles\n", 303 | " movies = {}\n", 304 | " for line in open(path + '/u.item', encoding='latin-1'):\n", 305 | " id, title = line.split('|')[0:2]\n", 306 | " movies[id] = title\n", 307 | " # load data\n", 308 | " prefs = {}\n", 309 | " for line in open(path + '/u.data', encoding='latin-1'):\n", 310 | " user, movieid, rating, ts = line.split('\\t')\n", 311 | " prefs.setdefault(user, {})\n", 312 | " prefs[user][movies[movieid]] = float(rating)\n", 313 | " return prefs" 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": null, 319 | "metadata": { 320 | "autoscroll": false, 321 | "ein.tags": "worksheet-0", 322 | "slideshow": { 323 | "slide_type": "-" 324 | } 325 | }, 326 | "outputs": [], 327 | "source": [ 328 | "prefs = load_movielens()\n", 329 | "print(prefs['87'])" 330 | ] 331 | }, 332 | { 333 | "cell_type": "markdown", 334 | "metadata": { 335 | "ein.tags": "worksheet-0", 336 | "slideshow": { 337 | "slide_type": "-" 338 | } 339 | }, 340 | "source": [ 341 | "\n", 342 | "#### 实验设计\n", 343 | "\n", 344 | "协同过滤算法的离线实验一般如下设计。\n", 345 | "\n", 346 | "1. 将用户行为数据集按照均匀分布随机分成 $M$ 份(例如取 $M=8$ ),挑选一份作为测试集,将剩下的 $M-1$ 份作为训练集。\n", 347 | "2. 在训练集上建立用户兴趣模型,并在测试集上对用户行为进行预测,统计出相应的评测指标。为了保证评测指标并不是过拟合的结果,需要进行 $M$ 次实验,并且每次都使用不同的测试集。\n", 348 | "3. 将 $M$ 次实验测出的评测指标的平均值作为最终的评测指标。\n", 349 | "\n", 350 | "下面的代码描述了将数据集随机分成训练集和测试集的过程:\n", 351 | "\n", 352 | "```python\n", 353 | "def split_data(data, M, k, seed):\n", 354 | " test = []\n", 355 | " train = []\n", 356 | " random.seed(seed)\n", 357 | " for user, item in data:\n", 358 | " if random.randint(0, M) == k:\n", 359 | " test.append([user, item])\n", 360 | " else:\n", 361 | " train.append([user, item])\n", 362 | " return train, test\n", 363 | "```\n", 364 | "\n", 365 | "每次实验选取不同的 $k(0≤k≤M-1)$ 和相同的随机数种子seed,进行 $M$ 次实验就可以得到 $M$ 个不同的训练集和测试集,然后分别进行实验,用 $M$ 次实验的平均值作为最后的评测指标。这样做主要是防止某次实验的结果是过拟合的结果(over fitting),但如果数据集够大,模型够简单,为了快速通过离线实验初步地选择算法,也可以只进行一次实验。\n", 366 | "\n" 367 | ] 368 | }, 369 | { 370 | "cell_type": "markdown", 371 | "metadata": { 372 | "ein.tags": "worksheet-0", 373 | "slideshow": { 374 | "slide_type": "-" 375 | } 376 | }, 377 | "source": [ 378 | "\n", 379 | "#### 评测指标\n", 380 | "\n", 381 | "对用户 $u$ 推荐 $N$ 个物品(记为 $R(u)$ ),令用户 $u$ 在测试集上喜欢的物品集合为 $T(u)$ ,然后可以通过准确率/召回率评测推荐算法的精度:\n", 382 | "\n", 383 | "\\begin{equation}\n", 384 | "\\begin{split}\n", 385 | "& Recall = \\frac{\\sum_u |R(u) \\cap T(u)|}{\\sum_u |T(u)|} \\\\\n", 386 | "& Precision = \\frac{\\sum_u |R(u) \\cap T(u)|}{\\sum_u |R(u)|} \\\\\n", 387 | "\\end{split} \\nonumber\n", 388 | "\\end{equation}\n", 389 | "\n", 390 | "召回率描述有多少比例的用户——物品评分记录包含在最终的推荐列表中,而准确率描述最终的推荐列表中有多少比例是发生过的用户——物品评分记录。\n", 391 | "\n", 392 | "下面两段代码给出了召回率和准确率的计算方法。\n", 393 | "\n", 394 | "```python\n", 395 | "def recall(train, test, N, GetRecommendation=None):\n", 396 | " hit = 0\n", 397 | " all = 0\n", 398 | " for user in train.keys():\n", 399 | " tu = test[user]\n", 400 | " rank = GetRecommendation(user, N)\n", 401 | " for item, pui in rank:\n", 402 | " if item in tu:\n", 403 | " hit += 1\n", 404 | " all += len(tu)\n", 405 | " return hit / (all * 1.0)\n", 406 | "```\n", 407 | "\n", 408 | "```python\n", 409 | "def precision(train, test, N, GetRecommendation=None):\n", 410 | " hit = 0\n", 411 | " all = 0\n", 412 | " for user in train.keys():\n", 413 | " tu = test[user]\n", 414 | " rank = GetRecommendation(user, N)\n", 415 | " for item, pui in rank:\n", 416 | " if item in tu:\n", 417 | " hit += 1\n", 418 | " all += N\n", 419 | " return hit / (all * 1.0)\n", 420 | "```\n", 421 | "\n", 422 | "下面的公式给出了最简单的覆盖率定义,覆盖率反映了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能够将长尾中的物品推荐给用户。\n", 423 | "\n", 424 | "\\begin{equation}\n", 425 | "Coverage = \\frac{U_{u \\in U} R(u)}{|I|} \\nonumber\n", 426 | "\\end{equation}\n", 427 | "\n", 428 | "该覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有的物品都被推荐给至少一个用户,那么覆盖率就是100%。如下代码可以用来计算推荐算法的覆盖率:\n", 429 | "\n", 430 | "```python\n", 431 | "def coverage(train, test, N, GetRecommendation=None):\n", 432 | " recommend_items = set()\n", 433 | " all_items = set()\n", 434 | " for user in train.keys():\n", 435 | " for item in train[user].keys():\n", 436 | " all_items.add(item)\n", 437 | " rank = GetRecommendation(user, N)\n", 438 | " for item, pui in rank:\n", 439 | " recommend_items.add(item)\n", 440 | " return len(recommend_items) / (len(all_items) * 1.0)\n", 441 | "```\n", 442 | "\n", 443 | "最后,用推荐列表中物品的平均流行度度量推荐结果的新颖度。如果推荐出的物品都很热门,说明推荐的新颖度较低,否则说明推荐结果比较新颖。\n", 444 | "\n", 445 | "```python\n", 446 | "def popularity(train, test, N, GetRecommendation=None):\n", 447 | " item_popularity = dict()\n", 448 | " for user, items in train.items():\n", 449 | " for item in items.keys():\n", 450 | " if item not in item_popularity:\n", 451 | " item_popularity[item] = 0\n", 452 | " item_popularity[item] += 1\n", 453 | " ret = 0\n", 454 | " n = 0\n", 455 | " for user in train.keys():\n", 456 | " rank = GetRecommendation(user, N)\n", 457 | " for item, pui in rank:\n", 458 | " ret += math.log(1 + item_popularity[item])\n", 459 | " n += 1\n", 460 | " ret /= n * 1.0\n", 461 | " return ret\n", 462 | "```\n", 463 | "\n", 464 | "在计算平均流行度时对每个物品的流行度取对数,这是因为物品的流行度分布满足长尾分布,在取对数后,流行度的平均值更加稳定。\n" 465 | ] 466 | }, 467 | { 468 | "cell_type": "markdown", 469 | "metadata": { 470 | "ein.tags": "worksheet-0", 471 | "slideshow": { 472 | "slide_type": "-" 473 | } 474 | }, 475 | "source": [ 476 | "## 脚注\n", 477 | "\n", 478 | "1 协同过滤就是指用户可以齐心协力,通过不断地和网站互动,使自己的推荐列表能够不断过滤掉自己不感兴趣的物品,从而越来越满足自己的需求。\n", 479 | "\n", 480 | "2 参见“Book-Crossing Dataset”,地址为。\n", 481 | "\n", 482 | "3 参见。\n", 483 | "\n", 484 | "4 参见。" 485 | ] 486 | } 487 | ], 488 | "metadata": { 489 | "kernelspec": { 490 | "display_name": "Python 3", 491 | "name": "python3" 492 | }, 493 | "language_info": { 494 | "codemirror_mode": { 495 | "name": "ipython", 496 | "version": 3 497 | }, 498 | "file_extension": ".py", 499 | "mimetype": "text/x-python", 500 | "name": "python", 501 | "nbconvert_exporter": "python", 502 | "pygments_lexer": "ipython3", 503 | "version": "3.6.4" 504 | }, 505 | "name": "4-use-user-behavior-data.ipynb", 506 | "toc": { 507 | "colors": { 508 | "hover_highlight": "#ddd", 509 | "running_highlight": "#FF0000", 510 | "selected_highlight": "#ccc" 511 | }, 512 | "moveMenuLeft": true, 513 | "nav_menu": { 514 | "height": "123px", 515 | "width": "252px" 516 | }, 517 | "navigate_menu": true, 518 | "number_sections": false, 519 | "sideBar": true, 520 | "threshold": 4, 521 | "toc_cell": false, 522 | "toc_section_display": "block", 523 | "toc_window_display": false, 524 | "widenNotebook": false 525 | } 526 | }, 527 | "nbformat": 4, 528 | "nbformat_minor": 2 529 | } 530 | -------------------------------------------------------------------------------- /ch05/5-user-based-collaborative-filtering.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 基于用户的协同过滤算法\n", 13 | "\n", 14 | "\n", 15 | "## 基础算法\n", 16 | "\n", 17 | "在一个在线个性化推荐系统中,当一个用户A需要个性化推荐时,可以先找到和他有相似兴趣的其他用户,然后把那些用户喜欢的、而用户A没有听说过的物品推荐给A。这种方法称为基于用户的协同过滤算法。\n", 18 | "\n", 19 | "主要包括两个步骤。" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": { 25 | "ein.tags": "worksheet-0", 26 | "slideshow": { 27 | "slide_type": "-" 28 | } 29 | }, 30 | "source": [ 31 | "\n", 32 | "### 找到和目标用户兴趣相似的用户集合\n", 33 | "\n", 34 | "**利用行为的相似度计算兴趣的相似度。**\n", 35 | "\n", 36 | "给定用户 $u$ 和用户 $v$ ,令 $N(u)$ 表示用户 $u$ 曾经有过正反馈的物品集合,令 $N(v)$ 为用户 $v$ 曾经有过正反馈的物品集合。通过如下的Jaccard公式1简单地计算 $u$ 和 $v$ 的兴趣相似度:\n", 37 | "\n", 38 | "\\begin{equation}\n", 39 | "w_{uv} = \\frac{| N (u) \\cap N (v) |}{| N (u) \\cup N (v) | } \\nonumber\n", 40 | "\\end{equation}\n", 41 | "\n", 42 | "或者通过余弦相似度2计算3:\n", 43 | "\n", 44 | "\\begin{equation}\n", 45 | "w_{uv} = \\frac{| N (u) \\cap N (v) |}{\\sqrt{| N (u) | | N (v) | }} \\nonumber\n", 46 | "\\end{equation}\n", 47 | "\n", 48 | "比如以下面的用户行为记录为例,举例说明 **UserCF** 计算用户兴趣相似度的例子。\n", 49 | "\n", 50 | "用户行为记录举例:\n", 51 | "\n", 52 | " A -> [a] [b] [d]\n", 53 | " B -> [a] [c]\n", 54 | " C -> [b] [e]\n", 55 | " D -> [c] [d] [e]\n", 56 | "\n", 57 | "用户 $A$ 对物品 ${a, b, d}$ 有过行为,用户 $B$ 对物品 ${a, c}$ 有过行为,利用余弦相似度公式计算用户 $A$ 和用户 $B$ 的兴趣相似度为:\n", 58 | "\n", 59 | "\\begin{equation}\n", 60 | "w_{AB} = \\frac{| \\{a, b, d\\} \\cap \\{a, c\\} |}{\\sqrt{| \\{a, b, d\\} | | \\{a, c\\} |}} = \\frac{1}{\\sqrt{6}} \\nonumber\n", 61 | "\\end{equation}\n", 62 | "\n", 63 | "同理可以计算出用户 $A$ 和用户 $C$ 、 $D$ 的相似度:\n", 64 | "\n", 65 | "\\begin{equation}\n", 66 | "w_{AC} = \\frac{| \\{a, b, d\\} \\cap \\{b, e\\} |}{\\sqrt{| \\{a, b, d\\} | | \\{b, e\\} |}} = \\frac{1}{\\sqrt{6}} \\nonumber\n", 67 | "\\end{equation}\n", 68 | "\n", 69 | "\\begin{equation}\n", 70 | "w_{AD} = \\frac{| \\{a, b, d\\} \\cap \\{c, d, e\\} |}{\\sqrt{| \\{a, b, d\\} | | \\{c, d, e\\} |}} = \\frac{1}{3} \\nonumber\n", 71 | "\\end{equation}\n", 72 | "\n", 73 | "实现余弦相似度的伪代码:\n", 74 | "\n", 75 | "```python\n", 76 | "def user_similarity(train):\n", 77 | " import math\n", 78 | "\n", 79 | " W = dict()\n", 80 | " for u in train.keys():\n", 81 | " for v in train.keys():\n", 82 | " if u == v:\n", 83 | " continue\n", 84 | " W[u][v] = len(train[u].keys() & train[v].keys())\n", 85 | " W[u][v] = W[u][v] / math.sqrt(len(train[u].keys()) * len(train[v].keys()))\n", 86 | " return W\n", 87 | "```\n", 88 | "\n", 89 | "上述代码对两两用户都利用余弦相似度计算相似度。方法的时间复杂度是 $O(|U|\\times|U|)$ ,在用户数很大时非常耗时。\n", 90 | "\n", 91 | "**事实上,很多用户相互之间并没有对同样的物品产生过行为** ,即很多时候 $|N(u) \\cap N(v)|=0$ 。\n", 92 | "\n", 93 | "可以先计算出 $|N(u) \\cap N(v)| \\neq 0$ 的用户对 $(u,v)$ ,再对这种情况除以分母 $\\sqrt{|N(u)||N(v)|}$ 。\n", 94 | "\n", 95 | "建立物品到用户的倒排表,对于每个物品都保存对该物品产生过行为的用户列表。\n", 96 | "\n", 97 | "令稀疏矩阵 $C[u][v] = |N(u) \\cap N(v)|$ ,假设用户 $u$ 和用户 $v$ 同时属于倒排表中 $K$ 个物品对应的用户列表,则 $C[u][v]=K$ 。可以扫描倒排表中每个物品对应的用户列表,将用户列表中的两两用户对应的 $C[u][v]$ **加1** 。最终得到所有用户之间不为0的 $C[u][v]$ 。\n", 98 | "\n", 99 | "相应的伪码:\n", 100 | "\n", 101 | "```python\n", 102 | "def user_similarity(train):\n", 103 | " import math\n", 104 | "\n", 105 | " # build inverse table for item_users\n", 106 | " item_users = dict()\n", 107 | " for u, items in train.items():\n", 108 | " for i in items.keys():\n", 109 | " if i not in item_users:\n", 110 | " item_users[i] = set()\n", 111 | " item_users[i].add(u)\n", 112 | "\n", 113 | " # calculate co-related items between users\n", 114 | " C = dict()\n", 115 | " N = dict()\n", 116 | " for i, users in item_users.items():\n", 117 | " for u in users:\n", 118 | " N[u] += 1\n", 119 | " for v in users:\n", 120 | " if u == v:\n", 121 | " continue\n", 122 | " C[u][v] += 1\n", 123 | "\n", 124 | " # calculate final similarity matrix W\n", 125 | " W = dict()\n", 126 | " for u, related_users in C.items():\n", 127 | " for u, cuv in related_users.items():\n", 128 | " W[u][v] = cuv / math.sqrt(N[u] * N[v])\n", 129 | " return W\n", 130 | "```\n", 131 | "\n", 132 | "下面是使用 **NumPy** 实现的代码示例:\n", 133 | "\n", 134 | "```python\n", 135 | "import numpy as np\n", 136 | "\n", 137 | "\n", 138 | "def cos_sim(x, y):\n", 139 | " \"\"\"余弦相似性\n", 140 | "\n", 141 | " Args:\n", 142 | " - x: mat, 以行向量的形式存储\n", 143 | " - y: mat, 以行向量的形式存储\n", 144 | "\n", 145 | " :return: x 和 y 之间的余弦相似度\n", 146 | " \"\"\"\n", 147 | " numerator = x * y.T # x 和 y 之间的内积\n", 148 | " denominator = np.sqrt(x * x.T) * np.sqrt(y * y.T)\n", 149 | " return (numerator / denominator)[0, 0]\n", 150 | "```\n", 151 | "\n", 152 | "对于任意矩阵,计算任意两个行向量之间的相似度:\n", 153 | "\n", 154 | "```python\n", 155 | "import numpy as np\n", 156 | "\n", 157 | "\n", 158 | "def similarity(data):\n", 159 | " \"\"\"计算矩阵中任意两行之间的相似度\n", 160 | " Args:\n", 161 | " - data: mat, 任意矩阵\n", 162 | "\n", 163 | " :return: w, mat, 任意两行之间的相似度\n", 164 | " \"\"\"\n", 165 | "\n", 166 | " m = np.shape(data)[0] # 用户的数量\n", 167 | " # 初始化相似矩阵\n", 168 | " w = np.mat(np.zeros((m, m)))\n", 169 | "\n", 170 | " for i in range(m):\n", 171 | " for j in range(i, m):\n", 172 | " if not j == i:\n", 173 | " # 计算任意两行之间的相似度\n", 174 | " w[i, j] = cos_sim(data[i], data[j])\n", 175 | " w[j, i] = w[i, j]\n", 176 | " else:\n", 177 | " w[i, j] = 0\n", 178 | " return w\n", 179 | "```\n", 180 | "\n", 181 | "相似度矩阵 $w$ 是一个对称矩阵,而且在相似度矩阵中,约定自身的相似度的值为 $0$ 。" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": { 187 | "ein.tags": "worksheet-0", 188 | "slideshow": { 189 | "slide_type": "-" 190 | } 191 | }, 192 | "source": [ 193 | "\n", 194 | "### 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户\n", 195 | "\n", 196 | "得到用户之间的兴趣相似度后, **UserCF** 算法会给用户推荐和他兴趣最相似的 $K$ 个用户喜欢的物品。\n", 197 | "\n", 198 | "下面的公式度量了 **UserCF** 算法中用户 $u$ 对物品 $i$ 的感兴趣程度:\n", 199 | "\n", 200 | "\\begin{equation}\n", 201 | "p(u,i) = \\sum_{v \\in S(u,K) \\cap N(i)} w_{uv}r_{vi} \\nonumber\n", 202 | "\\end{equation}\n", 203 | "\n", 204 | "其中, $S(u,K)$ 包含和用户 $u$ 兴趣最接近的 $K$ 个用户, $N(i)$ 是对物品 $i$ 有过行为的用户集合, $w_{uv}$ 是用户 $u$ 和用户 $v$ 的兴趣相似度, $r_{vi}$ 代表用户 $v$ 对物品 $i$ 的兴趣。因为使用的是单一行为的隐反馈数据,所以所有的 $r_{vi}=1$ 。\n", 205 | "\n", 206 | "下面的伪码实现了上面的 **UserCF** 算法:\n", 207 | "\n", 208 | "```python\n", 209 | "def recommend(user, train, W, K):\n", 210 | " rank = dict()\n", 211 | " interacted_items = train[user]\n", 212 | " for v, wuv in sorted(W[u].items, key=itemgetter(1), reverse=True)[0:K]:\n", 213 | " for i, rvi in train[v].items:\n", 214 | " if i in interacted_items:\n", 215 | " # we should filter items user interacted before\n", 216 | " continue\n", 217 | " rank[i] += wuv * rvi\n", 218 | " return rank\n", 219 | "```\n", 220 | "\n", 221 | "下面是基于 NumPy 的代码实现:\n", 222 | "\n", 223 | "```python\n", 224 | "import numpy as np\n", 225 | "\n", 226 | "\n", 227 | "def user_based_recommend(data, w, user):\n", 228 | " \"\"\"基于用户相似性为用户 user 推荐物品\n", 229 | "\n", 230 | " Args:\n", 231 | " - data: mat, 用户物品矩阵\n", 232 | " - w: mat, 用户之间的相似度\n", 233 | " - user: int, 用户编号\n", 234 | "\n", 235 | " :return: predict, list, 推荐列表\n", 236 | " \"\"\"\n", 237 | " m, n = np.shape(data)\n", 238 | " interaction = data[user, ] # 用户 user 与物品信息\n", 239 | "\n", 240 | " # 找到用户 user 没有互动过的物品\n", 241 | " not_inter = []\n", 242 | " for i in range(n):\n", 243 | " if interaction[0, i] == 0: # 没有互动的物品\n", 244 | " not_inter.append(i)\n", 245 | "\n", 246 | " # 对没有互动过的物品进行预测\n", 247 | " predict = {}\n", 248 | " for x in not_inter:\n", 249 | " item = np.copy(data[:, x]) # 找到所有用户对商品 x 的互动信息\n", 250 | " for i in range(m): # 对每一个用户\n", 251 | " if item[i, 0] != 0:\n", 252 | " if x not in predict:\n", 253 | " predict[x] = w[user, i] * item[i, 0]\n", 254 | " else:\n", 255 | " predict[x] = predict[x] + w[user, i] + item[i, 0]\n", 256 | " return sorted(predict.items(), key=lambda d: d[1], reverse=True)\n", 257 | "```\n", 258 | "\n", 259 | "在函数user_based_recommend中,主要分为3步:\n", 260 | "\n", 261 | "1. 找到用户user未互动的商品,存放到not_inter中\n", 262 | "2. 利用上面公式对没有互动过的物品进行打分,在打分过程中,首先对用户user未互动过的每一个物品,找到对其互动过的用户,再利用打分公式对该物品打分\n", 263 | "3. 将打分的最终结果按照降序排序并返回\n", 264 | "\n", 265 | "如果是 **TOP N** 推荐,为用户推荐前 $N$ 个打分最高的物品:\n", 266 | "\n", 267 | "```python\n", 268 | "def top_k(predict, n):\n", 269 | " \"\"\"为用户推荐前 n 个物品\n", 270 | "\n", 271 | " Args:\n", 272 | " - predict: list, 排好序的物品列表\n", 273 | " - k: int, 推荐的物品个数\n", 274 | "\n", 275 | " :return: top_recom, list, top n 个物品\n", 276 | " \"\"\"\n", 277 | " top_recom = []\n", 278 | " len_result = len(predict)\n", 279 | " if n >= len_result:\n", 280 | " top_recom = predict\n", 281 | " else:\n", 282 | " for i in range(n):\n", 283 | " top_recom.append(predict[i])\n", 284 | " return top_recom\n", 285 | "```" 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "metadata": { 291 | "ein.tags": "worksheet-0", 292 | "slideshow": { 293 | "slide_type": "-" 294 | } 295 | }, 296 | "source": [ 297 | "通过对不同 $K$ 值下的测量发现:\n", 298 | "\n", 299 | "* 准确率和召回率并不和 $K$ 成线性关系,通过多次测量可以选择合适的 $K$ 值\n", 300 | "* $K$ 越大,推荐的结果越热门,流行度增大\n", 301 | "* $K$ 越大,推荐结果的覆盖率越低" 302 | ] 303 | }, 304 | { 305 | "cell_type": "markdown", 306 | "metadata": { 307 | "ein.tags": "worksheet-0", 308 | "slideshow": { 309 | "slide_type": "-" 310 | } 311 | }, 312 | "source": [ 313 | "\n", 314 | "\n", 315 | "## 用户相似度的改进 **User-IIF** 算法\n", 316 | "\n", 317 | "两个用户对冷门物品采取过同样的行为更能说明他们兴趣的相似度。\n", 318 | "\n", 319 | "John S. Breese在论文4中提出的根据用户行为计算用户的兴趣相似度:\n", 320 | "\n", 321 | "\\begin{equation}\n", 322 | "w_{uv} = \\frac{\\sum_{i \\in N (u) \\cap N (v)} \\frac{1}{\\log{(1 + | N (i) |)}}}{\\sqrt{| N(u) | | N(v) |}} \\nonumber\n", 323 | "\\end{equation}\n", 324 | "\n", 325 | "该公式通过 $\\frac{1}{\\log{(1 + | N (i) |)}}$ 惩罚了用户 $u$ 和用户 $v$ 共同兴趣列表中热门物品对他们相似度的影响。\n", 326 | "\n", 327 | "```python\n", 328 | "def user_similarity(train):\n", 329 | " import math\n", 330 | "\n", 331 | " # build inverse table for item_users\n", 332 | " item_users = dict()\n", 333 | " for u, items in train.items():\n", 334 | " for i in items.keys():\n", 335 | " if i not in item_users:\n", 336 | " item_users[i] = set()\n", 337 | " item_users[i].add(u)\n", 338 | "\n", 339 | " # calculated co-rated items between users\n", 340 | " C = dict()\n", 341 | " N = dict()\n", 342 | " for i, users in item_users.items():\n", 343 | " for u in users:\n", 344 | " N[u] += 1\n", 345 | " for v in users:\n", 346 | " if u == v:\n", 347 | " continue\n", 348 | " C[u][v] += 1 / math.log(1 + len(users))\n", 349 | "\n", 350 | " # calculate final similarity matrix W\n", 351 | " W = dict()\n", 352 | " for u, related_users in C.items():\n", 353 | " for v, cuv in related_users.items():\n", 354 | " W[u][v] = cuv / math.sqrt(N[u] * N[v])\n", 355 | " return W\n", 356 | "```" 357 | ] 358 | }, 359 | { 360 | "cell_type": "markdown", 361 | "metadata": { 362 | "ein.tags": "worksheet-0", 363 | "slideshow": { 364 | "slide_type": "-" 365 | } 366 | }, 367 | "source": [ 368 | "\n", 369 | "\n", 370 | "## 缺点\n", 371 | "\n", 372 | "- 随着网站用户数的增大,计算用户兴趣相似度矩阵越发困难,其运算时间复杂度和空间复杂度的增长和用户数的增长近似于平方关系。\n", 373 | "\n", 374 | "- 很难对推荐结果作出解释。\n" 375 | ] 376 | }, 377 | { 378 | "cell_type": "markdown", 379 | "metadata": { 380 | "ein.tags": "worksheet-0", 381 | "slideshow": { 382 | "slide_type": "-" 383 | } 384 | }, 385 | "source": [ 386 | "## 脚注\n", 387 | "\n", 388 | "1 中文常翻译为“杰卡德公式”,公式如下:\n", 389 | "\n", 390 | "\\begin{equation}\n", 391 | "J(A,B) = \\frac{|A \\cap B|}{| A \\cup B|} = \\frac{| A \\cap B|}{|A| + |B| - |A \\cap B|} \\nonumber\n", 392 | "\\end{equation}\n", 393 | "\n", 394 | "2 假定 $A$ 和 $B$ 是两个 $n$ 维向量, $A$ 是 $[A_1, A_2, \\ldots, A_n]$ , $B$ 是 $[B_1, B_2, \\ldots, B_n]$ ,则 $A$ 与 $B$ 的夹角 $\\theta$ 的余弦等于:\n", 395 | "\n", 396 | "\\begin{eqnarray}\n", 397 | "\\cos \\theta & = & \\frac{\\Sigma^n_{i = 1} (A_i \\times B_i)}\n", 398 | "{\\sqrt{\\Sigma^n_{i = 1} (A_i)^2} \\times \\sqrt{\\Sigma^n_{i = 1}\n", 399 | "(B_i)^2}} \\nonumber\\\\\n", 400 | "& = & \\frac{A \\cdot B}{| A | \\times | B |} \\nonumber\n", 401 | "\\end{eqnarray}\n", 402 | "\n", 403 | "这里的 $A_i$ 和 $B_i$ 分別代表向量 $A$ 和 $B$ 的各分量。\n", 404 | "\n", 405 | "3 这里使用 **Ochiai** 系数,或 **Ochiai-Barkman** 系数:\n", 406 | "\n", 407 | "\\begin{equation}\n", 408 | "K = \\frac{n(A \\cap B)}{\\sqrt{n(A) \\times n(B)}} \\nonumber\n", 409 | "\\end{equation}\n", 410 | "\n", 411 | "这里 $A$ 和 $B$ 是集合, $n(A)$ 是 $A$ 的元素个数。如果集合由位向量所代表,那么可看到Ochiai系数跟余弦相似性是等同的。\n", 412 | "\n", 413 | "4 参见John S. Breese、David Heckerman和Carl Kadie的论文“Empirical Analysis of Predictive Algorithms for Collaborative Filtering” (Morgan Kaufmann Publishers,1998)。" 414 | ] 415 | } 416 | ], 417 | "metadata": { 418 | "kernelspec": { 419 | "display_name": "Python 3", 420 | "name": "python3" 421 | }, 422 | "name": "5-user-based-collaborative-filtering.ipynb" 423 | }, 424 | "nbformat": 4, 425 | "nbformat_minor": 2 426 | } 427 | -------------------------------------------------------------------------------- /ch06/6-item-based-collaborative-filtering.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 基于物品的协同过滤算法1\n", 13 | "\n", 14 | "**目前业界应用最多的算法。**\n", 15 | "\n", 16 | "**给用户推荐和他们之前喜欢的物品相似的物品。**\n", 17 | "\n", 18 | "**其主要通过分析用户的行为记录计算物品之间的相似度。物品A和物品B具有很大的相似度是因为喜欢物品A的用户大都也喜欢物品B。**\n", 19 | "\n", 20 | "**ItemCF** 可利用用户的历史行为给推荐结果提供推荐解释。\n", 21 | "\n", 22 | "ItemCF算法主要分为两步:\n", 23 | "\n", 24 | "1. 计算物品之间的相似度;\n", 25 | "2. 根据物品的相似度和用户的历史行为给用户生成推荐列表。\n" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": { 31 | "ein.tags": "worksheet-0", 32 | "slideshow": { 33 | "slide_type": "-" 34 | } 35 | }, 36 | "source": [ 37 | "从“Customers Who Bought This Item Also Bought”出发,用下面的公式定义物品的相似度:\n", 38 | "\\begin{equation}\n", 39 | "w_{ij} = \\frac{|N(i) \\cap N(j)|}{|N(i)|} \\nonumber\n", 40 | "\\end{equation}\n", 41 | "\n", 42 | "分母 $|N(i)|$ 是喜欢物品 $i$ 的用户数,分子 $|N(i) \\cap N(j)|$ 是同时喜欢物品 $i$ 和物品 $j$ 的用户数。可理解为 **喜欢物品 $i$ 的用户中有多少比例的用户也喜欢物品 $j$** 。\n", 43 | "\n", 44 | "为避免推荐出现热门的物品,用下面的公式:\n", 45 | "\n", 46 | "\\begin{equation}\n", 47 | "w_{ij} = \\frac{|N(i) \\cap N(j)|}{\\sqrt{|N(i)||N(j)|}} \\nonumber\n", 48 | "\\end{equation}\n", 49 | "\n", 50 | "此公式惩罚了物品 $j$ 的权重,减轻了热门物品会和很多物品相似的可能性。\n", 51 | "\n", 52 | "两个物品产生相似度是因为它们共同被很多用户喜欢,即每个用户都可以通过他们的历史兴趣列表给物品“贡献”相似度。其中蕴涵一个假设,即 **每个用户的兴趣都局限在某几个方面** ,如果两个物品属于一个用户的兴趣列表,那么这两个物品可能就属于有限的几个领域,而如果两个物品属于很多用户的兴趣列表,那么它们就可能属于同一个领域,因而有很大的相似度。\n", 53 | "\n", 54 | "用 **ItemCF** 算法计算物品相似度时也可以首先建立用户——物品倒排表(即对每个用户建立一个包含他喜欢的物品的列表),然后对每个用户,将他物品列表中的物品两两在共现矩阵 $C$ 中 **加1** 。\n", 55 | "\n", 56 | "伪代码:\n", 57 | "\n", 58 | "```python\n", 59 | "def item_similarity(train):\n", 60 | " import math\n", 61 | "\n", 62 | " # calculate co-rated users between items\n", 63 | " C = dict()\n", 64 | " N = dict()\n", 65 | " for u, items in train.items():\n", 66 | " for i in items:\n", 67 | " N[i] += 1\n", 68 | " for j in items:\n", 69 | " if i == j:\n", 70 | " continue\n", 71 | " C[i][j] += 1\n", 72 | "\n", 73 | " # calculate final similarity matrix W\n", 74 | " W = dict()\n", 75 | " for i, related_items in C.items():\n", 76 | " for j, cij in related_items.items():\n", 77 | " W[u][v] = cij / math.sqrt(N[i] * N[j])\n", 78 | " return W\n", 79 | "```\n", 80 | "\n", 81 | "C[i][j]记录了同时喜欢物品 $i$ 和物品 $j$ 的用户数。将 $C$ 矩阵归一化可得到物品之间的余弦相似度矩阵 $W$ 。\n", 82 | "\n", 83 | "得到物品之间的相似度后, **ItemCF** 通过如下公式计算用户 $u$ 对一个物品 $j$ 兴趣:\n", 84 | "\n", 85 | "\\begin{equation}\n", 86 | "p_{uj} = \\sum_{i \\in N (u) \\cap S (j, K)} w_{ji} r_{ui} \\nonumber\n", 87 | "\\end{equation}\n", 88 | "\n", 89 | "**和用户历史上感兴趣的物品越相似的物品,越有可能在用户的推荐列表中获得比较高的排名。**\n", 90 | "\n", 91 | "$N (u)$ 是用户喜欢的物品的集合, $S (j,K)$ 是和物品 $j$ 最相似的 $K$ 个物品的集合, $w_{ji}$ 是物品 $j$ 和 $i$ 的相似度, $r_{ui}$ 是用户 $u$ 对物品 $i$ 的兴趣。(对于隐反馈数据集,如果用户 $u$ 对物品 $i$ 有过行为,即可令 $r_{ui}=1$ 。)\n", 92 | "\n", 93 | "伪代码如下:\n", 94 | "\n", 95 | "```python\n", 96 | "def Recommendation(train, user_id, W, K):\n", 97 | " rank = dict()\n", 98 | " ru = train[user_id]\n", 99 | " for i, pi in ru.items():\n", 100 | " for j, wj in sorted(W[i].items(), key=itemgetter(1), reverse=True)[0:K]:\n", 101 | " if j in ru:\n", 102 | " continue\n", 103 | " rank[j] += pi * wj\n", 104 | " return rank\n", 105 | "```" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "metadata": { 112 | "autoscroll": false, 113 | "collapsed": false, 114 | "ein.tags": "worksheet-0", 115 | "slideshow": { 116 | "slide_type": "-" 117 | } 118 | }, 119 | "outputs": [], 120 | "source": [ 121 | "def item_based_recommend(data, w, user):\n", 122 | " \"\"\"\n", 123 | " 基于物品相似度为用户 user 推荐物品\n", 124 | "\n", 125 | " Args:\n", 126 | " - data: mat, 物品用户矩阵\n", 127 | " - w: mat, 物品与物品之间的相似性\n", 128 | " - user: int, 用户编号\n", 129 | "\n", 130 | " :return: predict, list, 推荐列表\n", 131 | " \"\"\"\n", 132 | "\n", 133 | " m, n = np.shape(data) # m: 物品数量 n: 用户数量\n", 134 | " interaction = data[:, user].T # 用户 user 互动物品信息\n", 135 | "\n", 136 | " # 找到用户 user 没有互动的商品\n", 137 | " not_iter = []\n", 138 | " for i in range(m):\n", 139 | " if interaction[0, i] == 0: # 用户 user 未打分项\n", 140 | " not_iter.append(i)\n", 141 | "\n", 142 | " # 对没有互动过的物品进行预测\n", 143 | " predict = {}\n", 144 | " for x in not_iter:\n", 145 | " item = np.copy(interaction) # 获取用户 user 对物品的互动信息\n", 146 | " for j in range(m): # 对每一个物品\n", 147 | " if item[0, j] != 0: # 利用互动过的物品预测\n", 148 | " if x not in predict:\n", 149 | " predict[x] = w[x, j] * item[0, j]\n", 150 | " else:\n", 151 | " predict[x] = predict[x] + w[x, j] * item[0, j]\n", 152 | " # 按照预测的大小从大到小排序\n", 153 | " return sorted(predict.items(), key=lambda d: d[1], reverse=True)" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": { 159 | "ein.tags": "worksheet-0", 160 | "slideshow": { 161 | "slide_type": "-" 162 | } 163 | }, 164 | "source": [ 165 | "ItemCF的一个优势就是可以提供推荐解释,即利用用户历史上喜欢的物品为现在的推荐结果进行解释。\n", 166 | "\n", 167 | "下面的伪码实现了带解释的ItemCF算法:\n", 168 | "\n", 169 | "```python\n", 170 | "def Recommendation(train, user_id, W, K):\n", 171 | " rank = dict()\n", 172 | " ru = train[user_id]\n", 173 | " for i, pi in ru.items():\n", 174 | " for j, wj in sorted(W[i].items(), key=itemgetter(1), reverse=True)[0:K]:\n", 175 | " if j in ru:\n", 176 | " continue\n", 177 | " rank[j].weight += pi * wj\n", 178 | " rank[j].reason[i] = pi * wj\n", 179 | " return rank\n", 180 | "```\n", 181 | "\n", 182 | "**IUF** (Inverse User Frequence),用户活跃度对数的倒数。活跃用户对物品相似度的贡献应该小于不活跃的用户,增加IUF参数来修正物品相似度的计算公式:\n", 183 | "\n", 184 | "\\begin{equation}\n", 185 | "w_{ij} = \\frac{\\sum_{u \\in N(i) \\cap N(j)} \\frac{1}{\\log{1 + |N(u)|}}}{\\sqrt{|N(i)||N(j)|}} \\nonumber\n", 186 | "\\end{equation}\n", 187 | "\n", 188 | "上面的公式只是对活跃用户做了一种软性的惩罚,但对于很多过于活跃的用户,比如上面一位买了网站上80%图书的用户,为了避免相似度矩阵过于稠密,在实际计算中一般直接忽略他的兴趣列表,而不将其纳入到相似度计算的数据集中。\n", 189 | "\n", 190 | "```python\n", 191 | "def item_similarity(train):\n", 192 | " import math\n", 193 | "\n", 194 | " # calculate co-rated users between items\n", 195 | " C = dict()\n", 196 | " N = dict()\n", 197 | " for u, items in train.items():\n", 198 | " for i in users:\n", 199 | " N[i] += 1\n", 200 | " for j in users:\n", 201 | " if i == j:\n", 202 | " continue\n", 203 | " C[i][j] += 1 / math.log(1 + len(items) * 1.0)\n", 204 | " # calculate finial similarity matrix W\n", 205 | " W = dict()\n", 206 | " for i, related_items in C.items():\n", 207 | " for j, cij in related_items.items():\n", 208 | " W[u][v] = cij / math.sqrt(N[i] * N[j])\n", 209 | " return W\n", 210 | "```\n", 211 | "\n", 212 | "将上面的算法记为 **ItemCF-IUF** 。" 213 | ] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "metadata": { 218 | "ein.tags": "worksheet-0", 219 | "slideshow": { 220 | "slide_type": "-" 221 | } 222 | }, 223 | "source": [ 224 | "\n", 225 | "\n", 226 | "## 物品相似度的归一化\n", 227 | "\n", 228 | "Karypis研究发现如果将ItemCF的相似度矩阵按最大值归一化,可提高推荐的准确率。2\n", 229 | "\n", 230 | "如果已经得到了物品相似度矩阵 $w$ ,那么可以用如下公式得到归一化之后的相似度矩阵 $w'$ :\n", 231 | "\n", 232 | "\\begin{equation}\n", 233 | "w_{ij}^{'} = \\frac{w_{ij}}{\\underset{j}{\\max} w_{ij}} \\nonumber\n", 234 | "\\end{equation}\n", 235 | "\n", 236 | "归一化的好处不仅仅在于增加推荐的准确度,它还可以提高推荐的覆盖率和多样性。\n" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": { 242 | "ein.tags": "worksheet-0", 243 | "slideshow": { 244 | "slide_type": "-" 245 | } 246 | }, 247 | "source": [ 248 | "\n", 249 | "\n", 250 | "## UserCF和ItemCF比较\n", 251 | "\n", 252 | "**UserCF的推荐结果着重于反映和用户兴趣相似的小群体的热点,而ItemCF的推荐结果着重于维系用户的历史兴趣。**\n", 253 | "\n", 254 | "**UserCF的推荐更社会化,反映了用户所在的小型兴趣群体中物品的热门程度,而ItemCF的推荐更加个性化,反映了用户自己的兴趣传承。**\n", 255 | "\n", 256 | "个性化新闻推荐更加强调抓住新闻热点,热门程度和时效性是个性化新闻推荐的重点,而个性化相对于这两点略显次要。\n", 257 | "\n", 258 | "在新闻网站中,物品的更新速度远远快于新用户的加入速度,而且对于新用户,完全可以给他推荐最热门的新闻,因此UserCF利大于弊。\n", 259 | "\n", 260 | "图书、电子商务和电影网站中,用户的兴趣是比较固定和持久的。这些网站中个性化推荐的任务是帮助用户发现和他研究领域相关的物品。这些网站的物品更新速度不会特别快,一天一次更新物品相似度矩阵对它们来说不会造成太大的损失,是可以接受的。\n", 261 | "\n", 262 | "| | UserCF | ItemCF |\n", 263 | "|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|\n", 264 | "| 性能 | 适用于用户较少的场合,如果用户很多,计算用户相似度矩阵代价很大 | 适用于物品数明显小于用户数的场合,如果物品 很多(网页),计算物品相似度矩阵代价很大 |\n", 265 | "| 领域 | 时效性较强,用户个性化兴趣不太明显的领域 | 长尾物品丰富,用户个性化需求强烈的领域 |\n", 266 | "| 实时性 | 用户有新行为,不一定造成推荐结果的立即变化 | 用户有新行为,一定会导致推荐结果的实时变化 |\n", 267 | "| 冷启动 | 在新用户对很少的物品产生行为后,不能立即对他进行个性化推荐,因为用户相似度表是每隔一段时间离线计算的。

新物品上线后一段时间,一旦有用户对物品产生行为,就可以将新物品推荐给和对它产生行为的用户兴趣相似的其他用户 | 新用户只要对一个物品产生行为,就可以给他推荐和该物品相关的其他物品

但没有办法在不离线更新物品相似度表的情况下将新物品推荐给用户 |\n", 268 | "| 推荐理由 | 很难提供令用户信服的推荐解释 | 利用用户的历史行为给用户做推荐解释,可以令用户比较信服 |\n" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": { 274 | "ein.tags": "worksheet-0", 275 | "slideshow": { 276 | "slide_type": "-" 277 | } 278 | }, 279 | "source": [ 280 | "\n", 281 | "\n", 282 | "## 哈利玻特问题\n", 283 | "\n", 284 | "ItemCF计算物品相似度的经典公式:\n", 285 | "\n", 286 | "\\begin{equation}\n", 287 | "w_{ij} = \\frac{|N(i) \\cap N(j)|}{\\sqrt{|N(i)||N(j)|}} \\nonumber\n", 288 | "\\end{equation}\n", 289 | "\n", 290 | "为解决哈利波特问题,可以在分母上加大对热门物品的惩罚,采用如下公式:\n", 291 | "\n", 292 | "\\begin{equation}\n", 293 | "w_{ij} = \\frac{|N(i) \\cap N(j)}{|N(i)|^{1 - \\alpha} |N(j)|^{\\alpha}} \\nonumber\n", 294 | "\\end{equation}\n", 295 | "\n", 296 | "其中 $\\alpha \\in [0.5,1]$ 。通过提高 $\\alpha$ ,就可以惩罚热门的 $j$ 。\n", 297 | "\n", 298 | "两个不同领域的最热门物品之间往往具有比较高的相似度。此时,仅仅靠用户行为数据是不能解决这个问题的,因为用户的行为表示这种物品之间应该相似度很高。只能依靠引入物品的内容数据解决这个问题,比如对不同领域的物品降低权重等。这些就不是协同过滤讨论的范畴了。\n" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": { 304 | "ein.tags": "worksheet-0", 305 | "slideshow": { 306 | "slide_type": "-" 307 | } 308 | }, 309 | "source": [ 310 | "## 脚注\n", 311 | "\n", 312 | "1 参见Linden Greg、Smith Brent和York Jeremy的“Amazon.com Recommendations: Item-to-Item Collaborative Filtering.” (IEEE Internet Computing,2003)。\n", 313 | "\n", 314 | "2 参见George Karypis的论文“Evaluation of Item-based Top-N Recommendation Algorithms”。\n" 315 | ] 316 | } 317 | ], 318 | "metadata": { 319 | "kernelspec": { 320 | "display_name": "Python 3", 321 | "name": "python3" 322 | }, 323 | "name": "6-item-based-collaborative-filtering.ipynb" 324 | }, 325 | "nbformat": 4, 326 | "nbformat_minor": 2 327 | } 328 | -------------------------------------------------------------------------------- /ch07/7-recsys-lfm.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 基于模型的协同过滤算法\n", 13 | "\n", 14 | "本节介绍基于模型的协同过滤算法1在Top-N推荐中的应用。\n", 15 | "\n", 16 | "核心思想是 **通过隐含特征(latent factor)联系用户兴趣和物品** 。\n", 17 | "\n", 18 | "思路:对于某个用户,首先得到其兴趣分类,然后从分类中挑选其可能喜欢的物品。\n", 19 | "\n", 20 | "上述基于兴趣分类的方法需要解决3个问题:\n", 21 | "\n", 22 | "1. 如何给物品进行分类?\n", 23 | "2. 如何确定用户对哪些类的物品感兴趣,以及感兴趣的程度?\n", 24 | "3. 对于一个给定的类,选择哪些属于这个类的物品推荐给用户,以及如何确定这些物品在一个类中的权重?" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": { 30 | "ein.tags": "worksheet-0", 31 | "slideshow": { 32 | "slide_type": "-" 33 | } 34 | }, 35 | "source": [ 36 | "\n", 37 | "## 基础算法\n", 38 | "\n", 39 | "不同于通过编辑给物品进行分类,而是从数据出发, **自动地给物品进行分类,** 然后进行个性化推荐。\n", 40 | "\n", 41 | "隐含语义分析技术采取 **基于用户行为统计的自动聚类** ,能较好地解决通过编辑进行分类的5种问题:\n", 42 | "\n", 43 | "- 编辑的意见不能代表各种用户的意见(分类从物品内容出发还是从用户出发)\n", 44 | "\n", 45 | " 隐含语义分析技术的分类来自对用户行为的统计,代表了用户对物品分类的看法。隐含语义分析技术和ItemCF在物品分类方面的思想类似, **如果两个物品被很多用户同时喜欢,那么这两个物品就很有可能属于同一个类。**\n", 46 | "\n", 47 | "- 编辑很难控制分类的粒度\n", 48 | "\n", 49 | " 隐含语义分析技术允许指定最终有多少个分类,这个数字越大,分类的粒度就会越细,反之分类粒度就越粗。\n", 50 | "\n", 51 | "- 编辑很难给一个物品多个分类\n", 52 | "\n", 53 | " 隐含语义分析技术会计算出物品属于每个类的权重,因此每个物品都不是硬性地被分到某一个类中。\n", 54 | "\n", 55 | "- 编辑很难给出多维度的分类\n", 56 | "\n", 57 | " 隐含语义分析技术给出的每个分类都不是同一个维度的,它是基于用户的共同兴趣计算出来的,如果用户的共同兴趣是某一个维度,那么LFM给出的类也是相同的维度。\n", 58 | "\n", 59 | "- 编辑很难决定一个物品在某一个分类中的权重\n", 60 | "\n", 61 | " 隐含语义分析技术可以通过统计用户行为决定物品在每个类中的权重,如果喜欢某个类的用户都会喜欢某个物品,那么这个物品在这个类中的权重就可能比较高。\n", 62 | "\n", 63 | "隐含语义分析技术有很多著名的模型和方法,其中和该技术相关且耳熟能详的名词有LFM(latent factor model)、pLSA、LDA、隐含类别模型(latent class model)、隐含主题模型(latent topic model)、 矩阵分解(matrix factorization)。这些技术和方法在本质上是相通的,其中很多方法都可以用于个性化推荐系统。\n", 64 | "\n", 65 | "下面将以 **矩阵分解** 介绍隐含语义分析技术在推荐系统中的应用。" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": { 71 | "ein.tags": "worksheet-0", 72 | "slideshow": { 73 | "slide_type": "-" 74 | } 75 | }, 76 | "source": [ 77 | "\n", 78 | "### 矩阵分解(Matrix Factorization, MF)\n", 79 | "\n", 80 | "假设用户——物品数据如下表所示:\n", 81 | "\n", 82 | "| | D\\_1 | D\\_2 | D\\_3 | D\\_4 | D\\_5 |\n", 83 | "|------|------|------|------|------|------|\n", 84 | "| U\\_1 | 4 | 3 | - | 5 | - |\n", 85 | "| U\\_2 | 5 | - | 4 | 4 | - |\n", 86 | "| U\\_3 | 4 | - | 5 | - | 3 |\n", 87 | "| U\\_4 | 2 | 3 | - | 1 | - |\n", 88 | "| U\\_5 | - | 4 | 2 | - | 5 |\n", 89 | "\n", 90 | "将其转化为用户——物品矩阵 $R_{m \\times n}$ ,则:\n", 91 | "\n", 92 | "\\begin{equation}\n", 93 | "R_{m \\times n} =\n", 94 | "\\begin{pmatrix}\n", 95 | " 4 & 3 & - & 5 & -\\\\\n", 96 | " 5 & - & 4 & 4 & -\\\\\n", 97 | " 4 & - & 5 & - & 3\\\\\n", 98 | " 2 & 3 & - & 1 & -\\\\\n", 99 | " - & 4 & 2 & - & 5\n", 100 | "\\end{pmatrix} \\nonumber\n", 101 | "\\end{equation}\n", 102 | "\n", 103 | "其中“-”表示未打分项。\n", 104 | "\n", 105 | "矩阵分解是指将一个矩阵分解成两个或者多个矩阵的乘积。\n", 106 | "\n", 107 | "对于上面的用户——物品矩阵 $R_{m \\times n}$ ,假设将其分解成两个矩阵 $P_{m \\times k}$ 和 $Q_{k \\times n}$ ,要使得矩阵 $P_{m \\times k}$ 和 $Q_{k \\times n}$ 的乘积能够还原原始的矩阵 $R_{m \\times n}$ :\n", 108 | "\n", 109 | "\\begin{equation}\n", 110 | "R_{m \\times n} \\approx P_{m \\times k} \\times Q_{k \\times n} = \\hat{R}_{m \\times n} \\nonumber\n", 111 | "\\end{equation}\n", 112 | "\n", 113 | "其中,矩阵 $P_{m \\times k}$ 表示的是 $m \\times k$ 的矩阵,而矩阵 $Q_{k \\times n}$ 表示的是 $k \\times n$ 的矩阵, $k$ 是隐含的参数。" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": { 119 | "ein.tags": "worksheet-0", 120 | "slideshow": { 121 | "slide_type": "-" 122 | } 123 | }, 124 | "source": [ 125 | "\n", 126 | "### 基于矩阵分解的推荐算法\n", 127 | "\n", 128 | "矩阵分解算法属于基于模型的协同过滤算法,后者的步骤主要分为:\n", 129 | "\n", 130 | "1. 建立模型\n", 131 | "2. 利用训练好的模型进行推荐\n", 132 | "\n", 133 | "在基于矩阵分解的推荐算法中,上述两个步骤分别为:\n", 134 | "\n", 135 | "1. 对用户物品矩阵进行分解\n", 136 | "2. 利用分解后的矩阵预测原始矩阵中的未打分项\n", 137 | "\n", 138 | "通过如下公式计算用户 $i$ 对物品 $j$ 的兴趣:\n", 139 | "\n", 140 | "\\begin{equation}\n", 141 | "Preference(i,j) = r_{ij} = p_{i}^{T}q_{j} = \\underset{k=1}{\\overset{K}{\\sum}} p_{i,k}q_{k,j} \\nonumber\n", 142 | "\\end{equation}\n", 143 | "\n", 144 | "$P_{i,k}$ 和 $q_{k,j}$ 是模型的参数, $p_{i,k}$ 度量用户 $i$ 的兴趣和第 $k$ 个隐类的关系, $q_{k,j}$ 度量了第 $k$ 个隐类和物品 $j$ 之间的关系。\n", 145 | "\n", 146 | "如何计算这两个参数?\n", 147 | "\n", 148 | "需要一个训练集,对于每个用户 $i$ ,训练集里包含了用户 $i$ 喜欢的物品和不感兴趣的物品,通过学习这个数据集,获得上面的模型参数。" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": { 154 | "ein.tags": "worksheet-0", 155 | "slideshow": { 156 | "slide_type": "-" 157 | } 158 | }, 159 | "source": [ 160 | "\n", 161 | "#### 损失函数\n", 162 | "\n", 163 | "为了能够求解矩阵 $P_{m \\times k}$ 和 $Q_{k \\times n}$ 的每一个元素,可利用原始的评分矩阵 $R_{m \\times n}$ 与重新构建的评分矩阵 $\\hat{R}_{m \\times n}$ 之间的误差的平方作为损失函数,即:\n", 164 | "\n", 165 | "\\begin{equation}\n", 166 | "e^{2}_{i,j} = (r_{i,j} - \\hat{r}_{i,j})^{2} = (r_{i,j}-\\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})^{2} \\nonumber\n", 167 | "\\end{equation}\n", 168 | "\n", 169 | "最终,需要求解所有的非“-”项的损失之和的最小值:\n", 170 | "\n", 171 | "\\begin{equation}\n", 172 | "\\min loss = \\underset{r_{i,j} \\neq -}{\\sum}{e_{i,j}^{2}} \\nonumber\n", 173 | "\\end{equation}" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": { 179 | "ein.tags": "worksheet-0", 180 | "slideshow": { 181 | "slide_type": "-" 182 | } 183 | }, 184 | "source": [ 185 | "\n", 186 | "#### 损失函数的求解\n", 187 | "\n", 188 | "对于上面平方损失函数的最小值,可以通过梯度下降法求解,梯度下降法核心步骤如下所示:\n", 189 | "\n", 190 | "- 求解损失函数的 **负梯度** 2:\n", 191 | "\n", 192 | " \\begin{equation}\n", 193 | " \\begin{split}\n", 194 | " & \\frac{\\partial}{\\partial p_{i,k}}e^{2}_{i,j} = -2(r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})q_{k,j} = -2e_{i,j}q_{k,j} \\\\\n", 195 | " & \\frac{\\partial}{\\partial q_{k,j}}e^{2}_{i,j} = -2(r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})p_{i,k} = -2e_{i,j}p_{i,k} \\\\\n", 196 | " \\end{split} \\nonumber\n", 197 | " \\end{equation}\n", 198 | "\n", 199 | "- 根据负梯度的方向更新变量:\n", 200 | "\n", 201 | " \\begin{equation}\n", 202 | " \\begin{split}\n", 203 | " & p_{i,k}' = p_{i,k} - \\alpha \\frac{\\partial}{\\partial p_{i,k}}e^{2}_{i,j} = p_{i,k} + 2 \\alpha e_{i,j} q_{k,j} \\\\\n", 204 | " & q_{k,j}' = q_{k,j} - \\alpha \\frac{\\partial}{\\partial q_{k,j}}e^{2}_{i,j} = q_{k,j} + 2 \\alpha e_{i,j} p_{i,k} \\\\\n", 205 | " \\end{split} \\nonumber\n", 206 | " \\end{equation}\n", 207 | "\n", 208 | "通过迭代,直到算法最终收敛。" 209 | ] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "metadata": { 214 | "ein.tags": "worksheet-0", 215 | "slideshow": { 216 | "slide_type": "-" 217 | } 218 | }, 219 | "source": [ 220 | "\n", 221 | "#### 加入正则项的损失函数及求解方法\n", 222 | "\n", 223 | "通常在求解的过程中,为了能够有比较好的泛化能力,防止过拟合,会在损失函数中加入正则项,以对参数进行约束。下面是加入 $L_2$ 正则的损失函数:\n", 224 | "\n", 225 | "\\begin{equation}\n", 226 | "e_{i,j}^{2} = (r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})^{2} + \\frac{\\lambda}{2} \\overset{K}{\\underset{k=1}{\\sum}}(p_{i,k}^{2} + q_{k,j}^{2}) \\nonumber\n", 227 | "\\end{equation}\n", 228 | "\n", 229 | "正则化项的 $\\lambda$ 可通过实验获得。\n", 230 | "\n", 231 | "利用梯度下降法的求解过程为:\n", 232 | "\n", 233 | "- 求解损失函数的负梯度:\n", 234 | "\n", 235 | " \\begin{equation}\n", 236 | " \\begin{split}\n", 237 | " & \\frac{\\partial}{\\partial p_{i,k}}e_{i,j}^{2} = -2(r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})q_{k,j} + \\lambda p_{i,k} = -2e_{i,j}q_{k,j} + \\lambda p_{i,k} \\\\\n", 238 | " & \\frac{\\partial}{\\partial q_{k,j}}e_{i,j}^{2} = -2(r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}q_{i,k} \\cdot q_{k,j})p_{i,k} + \\lambda q_{k,j} = -2e_{i,j}p_{i,k} + \\lambda q_{k,j} \\\\\n", 239 | " \\end{split} \\nonumber\n", 240 | " \\end{equation}\n", 241 | "\n", 242 | "- 根据负梯度的方向更新变量:\n", 243 | "\n", 244 | " \\begin{equation}\n", 245 | " \\begin{split}\n", 246 | " & p_{i,k}' = p_{i,k} - \\alpha \\frac{\\partial}{\\partial p_{i,k}}e_{i,j}^{2} = p_{i,k} - \\alpha (-2e_{i,j}q_{k,j} + \\lambda p_{i,k}) = p_{i,k} + \\alpha (2e_{i,j}q_{k,j} - \\lambda p_{i,k}) \\\\\n", 247 | " & q_{k,j}' = q_{k,j} - \\alpha \\frac{\\partial}{\\partial q_{k,j}}e_{i,j}^{2} = q_{k,j} - \\alpha (-2e_{i,j}p_{i,k} + \\lambda q_{k,j}) = q_{k,j} + \\alpha (2e_{i,j}p_{i,k} - \\lambda q_{k,j}) \\\\\n", 248 | " \\end{split} \\nonumber\n", 249 | " \\end{equation}\n", 250 | "\n", 251 | "通过迭代,直到算法最终收敛。\n", 252 | "\n", 253 | "其中 $\\alpha$ 是学习速率(learning rate),它的选取需要通过反复实验获得。\n", 254 | "\n", 255 | "```ipython\n", 256 | "import numpy as np\n", 257 | "\n", 258 | "\n", 259 | "def sgd(data_matrix, k, alpha, lam, max_cycles):\n", 260 | " \"\"\"使用梯度下降法进行矩阵分解。\n", 261 | "\n", 262 | " Args:\n", 263 | " - data_matrix: mat, 用户物品矩阵\n", 264 | " - k: int, 分解矩阵的参数\n", 265 | " - alpha: float, 学习率\n", 266 | " - lam: float, 正则化参数\n", 267 | " - max_cycles: int, 最大迭代次数\n", 268 | "\n", 269 | " Returns:\n", 270 | " p,q: mat, 分解后的矩阵\n", 271 | " \"\"\"\n", 272 | " m, n = np.shape(data_matrix)\n", 273 | " # initiate p & q\n", 274 | " p = np.mat(np.random.random((m, k)))\n", 275 | " q = np.mat(np.random.random((k, n)))\n", 276 | "\n", 277 | " # start training\n", 278 | " for step in range(max_cycles):\n", 279 | " for i in range(m):\n", 280 | " for j in range(n):\n", 281 | " if data_matrix[i, j] > 0:\n", 282 | " error = data_matrix[i, j]\n", 283 | " for r in range(k):\n", 284 | " error = error - p[i, r] * q[r, j]\n", 285 | " for r in range(k):\n", 286 | " p[i, r] = p[i, r] + alpha * (2 * error * q[r, j] - lam * p[i, r])\n", 287 | " q[r, j] = q[r, j] + alpha * (2 * error * p[i, r] - lam * q[r, j])\n", 288 | "\n", 289 | " loss = 0.0\n", 290 | " for i in range(m):\n", 291 | " for j in range(n):\n", 292 | " if data_matrix[i, j] > 0:\n", 293 | " error = 0.0\n", 294 | " for r in range(k):\n", 295 | " error = error + p[i, r] * q[r, j]\n", 296 | " # calculate loss function\n", 297 | " loss = (data_matrix[i, j] - error) * (data_matrix[i, j] - error)\n", 298 | " for r in range(k):\n", 299 | " loss = loss + lam * (p[i, r] * p[i, r] + q[r, j] * q[r, j]) / 2\n", 300 | "\n", 301 | " if loss < 0.001:\n", 302 | " break\n", 303 | " if step % 1000 == 0:\n", 304 | " print(\"\\titer: %d, loss: %f\" % (step, loss))\n", 305 | " return p, q\n", 306 | "```" 307 | ] 308 | }, 309 | { 310 | "cell_type": "markdown", 311 | "metadata": { 312 | "ein.tags": "worksheet-0", 313 | "slideshow": { 314 | "slide_type": "-" 315 | } 316 | }, 317 | "source": [ 318 | "\n", 319 | "#### 预测\n", 320 | "\n", 321 | "利用上述过程,可得到矩阵 $P_{m \\times k}$ 和 $Q_{k \\times n}$ ,模型便建立好了。\n", 322 | "\n", 323 | "在基于矩阵分解的推荐算法中需要为指定的用户进行推荐其未打分的项,若要计算用户 $i$ 对商品 $j$ 的打分,则计算方法为:\n", 324 | "\n", 325 | "\\begin{equation}\n", 326 | "\\overset{K}{\\underset{k=1}{\\sum}}p_{i,k}q_{k,j} \\nonumber\n", 327 | "\\end{equation}\n", 328 | "\n", 329 | "为用户预测的具体实现的程序如下所示:\n", 330 | "\n", 331 | "```ipython\n", 332 | "def prediction(data_matrix, p, q, user):\n", 333 | " \"\"\"为用户未互动的项打分\n", 334 | "\n", 335 | " Args:\n", 336 | " - data_matrix: mat, 原始用户物品矩阵\n", 337 | " - p: mat, 分解后的矩阵p\n", 338 | " - q: mat, 分解后的矩阵q\n", 339 | " - user: int, 用户的id\n", 340 | "\n", 341 | " Returns:\n", 342 | " - predict: list, 推荐列表\n", 343 | " \"\"\"\n", 344 | " n = np.shape(data_matrix)[1]\n", 345 | " predict = {}\n", 346 | " for j in range(n):\n", 347 | " if data_matrix[user, j] == 0:\n", 348 | " predict[j] = (p[user,] * q[:, j])[0, 0]\n", 349 | "\n", 350 | " # 按照打分从大到小排序\n", 351 | " return sorted(predict.items(), key=lambda d: d[1], reverse=True)\n", 352 | "```" 353 | ] 354 | }, 355 | { 356 | "cell_type": "markdown", 357 | "metadata": { 358 | "ein.tags": "worksheet-0", 359 | "slideshow": { 360 | "slide_type": "-" 361 | } 362 | }, 363 | "source": [ 364 | "\n", 365 | "\n", 366 | "## 隐语义模型和基于邻域的方法的比较\n", 367 | "\n", 368 | "LFM是一种基于机器学习的方法,具有比较好的理论基础。这个方法和基于邻域的方法(比如UserCF、ItemCF)相比,各有优缺点。\n", 369 | "\n", 370 | "- 理论基础\n", 371 | "\n", 372 | " LFM具有比较好的理论基础,它是一种学习方法,通过优化一个设定的指标建立最优的模型。基于邻域的方法更多的是一种基于统计的方法,并没有学习过程。\n", 373 | "\n", 374 | "- 离线计算的空间复杂度\n", 375 | "\n", 376 | " 基于邻域的方法需要维护一张离线的相关表。在离线计算相关表的过程中,如果用户/物品数很多,将会占据很大的内存。假设有 $M$ 个用户和 $N$ 个物品,在计算相关表的过程中,可能会获得一张比较稠密的临时相关表(尽管最终对每个物品只保留 $K$ 个最相关的物品,但在中间计算过程中稠密的相关表是不可避免的),那么假设是用户相关表,则需要 $O(M \\times M)$ 的空间,而对于物品相关表,则需要 $O(N \\times N)$ 的空间。而LFM在建模过程中,如果是 $F$ 个隐类,那么它需要的存储空间是 $O(F \\times (M+N))$ ,这在 $M$ 和 $N$ 很大时可以很好地节省离线计算的内存。\n", 377 | "\n", 378 | "- 离线计算的时间复杂度\n", 379 | "\n", 380 | " 假设有 $M$ 个用户、 $N$ 个物品、 $K$ 条用户对物品的行为记录。那么,UserCF计算用户相关表的时间复杂度是 $O(N \\times (\\frac{K}{N})^2)$ ,而ItemCF计算物品相关表的时间复杂度是 $O(M \\times (\\frac{K}{M})^2)$ 。\n", 381 | "\n", 382 | " 而对于LFM,如果用 $F$ 个隐类,迭代 $S$ 次,那么它的计算复杂度是 $O(K \\times F \\times S)$ 。\n", 383 | "\n", 384 | " 如果 $\\frac{K}{N} > F \\times S$ ,则代表UserCF的时间复杂度低于LFM,如果 $\\frac{K}{M} > F \\times S$ ,则说明ItemCF的时间复杂度低于LFM。在一般情况下,LFM的时间复杂度要稍微高于UserCF和ItemCF,这主要是因为该算法需要多次迭代。但总体上,这两种算法在时间复杂度上没有质的差别。\n", 385 | "\n", 386 | "- 在线实时推荐\n", 387 | "\n", 388 | " UserCF和ItemCF在线服务算法需要将相关表缓存在内存中,然后可以在线进行实时的预测。以ItemCF算法为例,一旦用户喜欢了新的物品,就可以通过查询内存中的相关表将和该物品相似的其他物品推荐给用户。因此,一旦用户有了新的行为,而且该行为被实时地记录到后台的数据库系统中,他的推荐列表就会发生变化。\n", 389 | "\n", 390 | " 而从LFM的预测公式可以看到,LFM在给用户生成推荐列表时,需要计算用户对所有物品的兴趣权重,然后排名,返回权重最大的 $N$ 个物品。那么,在物品数很多时,这一过程的时间复杂度非常高,可达 $O(M \\times N \\times F)$ 。因此,LFM不太适合用于物品数非常庞大的系统,如果要用,我们也需要一个比较快的算法给用户先计算一个比较小的候选列表,然后再用LFM重新排名。\n", 391 | "\n", 392 | " 另一方面,LFM在生成一个用户推荐列表时速度太慢,因此不能在线实时计算,而需要离线将所有用户的推荐结果事先计算好存储在数据库中。因此,LFM不能进行在线实时推荐,也就是说,当用户有了新的行为后,他的推荐列表不会发生变化。\n", 393 | "\n", 394 | "- 推荐解释\n", 395 | "\n", 396 | " ItemCF算法支持很好的推荐解释,它可以利用用户的历史行为解释推荐结果。\n", 397 | "\n", 398 | " 但LFM无法提供这样的解释,它计算出的隐类虽然在语义上确实代表了一类兴趣和物品,却很难用自然语言描述并生成解释展现给用户。" 399 | ] 400 | }, 401 | { 402 | "cell_type": "markdown", 403 | "metadata": { 404 | "ein.tags": "worksheet-0", 405 | "slideshow": { 406 | "slide_type": "-" 407 | } 408 | }, 409 | "source": [ 410 | "## 脚注\n", 411 | "\n", 412 | "1 相关的名词有LFM、LSI、pLSA、LDA、Topic Model和矩阵分解(matrix factorization)。\n", 413 | "\n", 414 | "2 梯度是一个向量,具有大小和方向。负梯度是函数值下降最快的方向。梯度下降是指沿着梯度下降的方向走。导数是梯度上升的方向,所以导数乘以负一是梯度下降方向,也就是说负导数决定你往哪个方向走。\n" 415 | ] 416 | } 417 | ], 418 | "metadata": { 419 | "kernelspec": { 420 | "display_name": "Python 3", 421 | "name": "python3" 422 | }, 423 | "name": "7-recsys-lfm.ipynb" 424 | }, 425 | "nbformat": 4, 426 | "nbformat_minor": 2 427 | } 428 | -------------------------------------------------------------------------------- /ch08/8-cold-start.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 推荐系统冷启动问题\n", 13 | "推荐系统需要根据用户的历史行为和兴趣预测用户未来的行为和兴趣,因此大量的用户行为数据就成为推荐系统的重要组成部分和先决条件。对于很多做纯粹推荐系统的网站,或者很多在开始阶段就希望有个性化推荐应用的网站来说,如何在没有大量用户数据的情况下设计个性化推荐系统并且让用户对推荐结果满意从而愿意使用推荐系统,这就是 **冷启动(cold start)** 的问题。" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": { 19 | "ein.tags": "worksheet-0", 20 | "slideshow": { 21 | "slide_type": "-" 22 | } 23 | }, 24 | "source": [ 25 | "\n", 26 | "## 冷启动问题简介\n", 27 | "\n", 28 | "冷启动问题主要分3类:\n", 29 | "\n", 30 | "- **用户冷启动**\n", 31 | "\n", 32 | " 主要解决如何给新用户做个性化推荐的问题。(缺少新用户的行为数据)\n", 33 | "\n", 34 | "- **物品冷启动**\n", 35 | "\n", 36 | " 主要解决如何将新的物品推荐给可能对它感兴趣的用户这一问题。\n", 37 | "\n", 38 | "- **系统冷启动**\n", 39 | "\n", 40 | " 主要解决如何在一个新开发的网站上(还没有用户,也没有用户行为,只有一些物品的信息)设计个性化推荐系统,从而在网站刚发布时就让用户体验到个性化推荐服务这一问题。\n", 41 | "\n", 42 | "对于上述3种不同的冷启动问题,一般可参考如下解决方案:\n", 43 | "\n", 44 | "- 提供非个性化的推荐\n", 45 | "\n", 46 | " 最简单例子就是热门排行榜,等到用户数据收集到一定的时候,再切换为个性化推荐。\n", 47 | "\n", 48 | "- 利用用户注册时提供的年龄、性别等数据做粗粒度的个性化。\n", 49 | "\n", 50 | "- 利用用户的社交网络账号登录(需用户授权),导入用户在社交网站上的好友信息,然后给用户推荐其好友喜欢的物品。\n", 51 | "\n", 52 | "- 要求用户在登录时对一些物品进行反馈,收集用户对这些物品的兴趣信息,然后给用户推荐那些和这些物品相似的物品。\n", 53 | "\n", 54 | "- 对于新加入的物品,可利用内容信息,将其推荐给喜欢过和它们相似的物品的用户。\n", 55 | "\n", 56 | "- 引入专家的知识,通过一定的高效方式迅速建立起物品的相关度表。" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": { 62 | "ein.tags": "worksheet-0", 63 | "slideshow": { 64 | "slide_type": "-" 65 | } 66 | }, 67 | "source": [ 68 | "\n", 69 | "## 利用用户注册信息\n", 70 | "用户的注册信息分3种:\n", 71 | "\n", 72 | "- **人口统计学信息:** 包括用户的年龄、性别、职业、民族、学历和居住地。\n", 73 | "- **用户兴趣的描述:** 有一些网站会让用户用文字描述他们的兴趣。\n", 74 | "- **从其他网站导入的用户站外行为数据:** 比如用户通过豆瓣、新浪微博的账号登录,就可以在得到用户同意的情况下获取用户在豆瓣或者新浪微博的一些行为数据和社交网络数据。\n", 75 | "\n", 76 | "基于注册信息的个性化推荐流程:\n", 77 | "\n", 78 | "1. 获取用户的注册信息;\n", 79 | "2. 根据用户的注册信息对用户分类;\n", 80 | "3. 给用户推荐他所属分类中用户喜欢的物品。" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": { 86 | "ein.tags": "worksheet-0", 87 | "slideshow": { 88 | "slide_type": "-" 89 | } 90 | }, 91 | "source": [ 92 | "\n", 93 | "\n", 94 | "## 选择合适的物品启动用户的兴趣\n", 95 | "\n", 96 | "一般来说,能够用来启动用户兴趣的物品需要具有以下特点。\n", 97 | "\n", 98 | "- 比较热门\n", 99 | "\n", 100 | " 如果要让用户对一个物品进行反馈,前提是用户知道这个物品是什么东西。以电影为例,如果一开始让用户进行反馈的电影都很冷门,而用户不知道这些电影的情节和内容,也就无法对它们做出准确的反馈。\n", 101 | "\n", 102 | "- 具有代表性和区分性\n", 103 | "\n", 104 | " 启动用户兴趣的物品不能是大众化或老少咸宜的,因为这样的物品对用户的兴趣没有区分性。还以电影为例,用一部票房很高且广受欢迎的电影做启动物品,可以想象的到的是几乎所有用户都会喜欢这部电影,因而无法区分用户个性化的兴趣。\n", 105 | "\n", 106 | "- 启动物品集合需要有多样性\n", 107 | "\n", 108 | " 在冷启动时,我们不知道用户的兴趣,而用户兴趣的可能性非常多,为了匹配多样的兴趣,需要提供具有很高覆盖率的启动物品集合,这些物品能覆盖几乎所有主流的用户兴趣。" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": { 114 | "ein.tags": "worksheet-0", 115 | "slideshow": { 116 | "slide_type": "-" 117 | } 118 | }, 119 | "source": [ 120 | "\n", 121 | "## 利用物品的内容信息\n", 122 | "\n", 123 | "物品冷启动需要解决的问题是如何将新加入的物品推荐给对它感兴趣的用户。\n", 124 | "\n", 125 | "对于UserCF,可以考虑利用物品的内容信息,将新物品先投放给曾经喜欢过和它内容相似的其他物品的用户。只要有一小部分人能够发现并喜欢新的物品,UserCF算法就能将这些物品扩散到更多的用户中。\n", 126 | "\n", 127 | "对于ItemCF,解决物品冷启动问题的办法是频繁更新物品相似度表,但基于用户行为计算物品相似度是非常耗时的事情,主要原因是用户行为日志非常庞大。而且,新物品如果不展示给用户,用户就无法对它产生行为,通过行为日志计算是计算不出包含新物品的相关矩阵的。 **只能利用物品的内容信息计算物品相关表,并且频繁地更新相关表(比如半小时计算一次)。**\n", 128 | "\n", 129 | "**物品的内容信息多种多样,不同类型的物品有不同的内容信息。**\n", 130 | "\n", 131 | "下面是常见物品的常用内容信息:\n", 132 | "\n", 133 | "| 物品 | 信息 |\n", 134 | "|------|--------------------------|\n", 135 | "| 图书 | 标题、作者、出版社、出版年代、丛书名、目录、正文 |\n", 136 | "| 论文 | 标题、作者、作者单位、关键字、分类、摘要、正文 |\n", 137 | "| 电影 | 标题、导演、演员、编剧、类别、剧情简介、发行公司 |\n", 138 | "| 新闻 | 标题、正文、来源、作者 |\n", 139 | "| 微博 | 作者、内容、评论 |\n", 140 | "\n", 141 | "一般来说,物品的内容可以通过向量空间模型1表示,该模型会将物品表示成一个 **关键词** 向量。\n", 142 | "\n", 143 | "如果物品的内容是一些诸如导演、演员等实体的话,可以直接将这些实体作为关键词。\n", 144 | "\n", 145 | "如果内容是文本的形式,则需要引入一些理解自然语言的技术 **抽取关键词** 。\n", 146 | "\n", 147 | "下图展示了从文本生成关键词向量的主要步骤:\n", 148 | "\n", 149 | "![img](../recsys_images/keywords_generation.png \"关键词向量的生成过程\")\n", 150 | "\n", 151 | "对于中文,首先要对文本进行分词2,将字流变成词流,然后从词流中检测出命名实体(如人名、地名、组织名等),这些实体和一些其他重要的词将组成关键词集合,最后对关键词进行排名,计算每个关键词的权重,从而生成关键词向量。\n", 152 | "\n", 153 | "对物品 $d$ ,它的内容表示成一个关键词向量如下:\n", 154 | "\n", 155 | "\\begin{equation}\n", 156 | "d_i = \\{(e_1,w_1),(e_2,w_2),\\cdot\\cdot\\cdot\\} \\nonumber\n", 157 | "\\end{equation}\n", 158 | "\n", 159 | "其中,$e_i$ 就是关键词,$w_i$ 是关键词对应的权重。\n", 160 | "\n", 161 | "如果物品是文本,可以用信息检索领域著名的TF-IDF(Term Frequency-Inverse Document Frequency,词频—逆文档频率。如果某个词比较少见,但是它在一篇文章中多次出现,那么它很可能就反映了这篇文章的特性,可将其定为关键词。)3公式计算词的权重:\n", 162 | "\n", 163 | "\\begin{equation}\n", 164 | "\\begin{split}\n", 165 | "& tf(t,d) = \\frac{f_{t,d}}{\\sum_{t' \\in d}{f_{t',d}}} \\\\\n", 166 | "& idf(t,D) = \\log{\\frac{N}{1 + |\\{d \\in D: t \\in d \\}|}} \\\\\n", 167 | "& tfidf(t,d,D) = tf(t,d) \\cdot idf(t,D) \\\\\n", 168 | "\\end{split} \\nonumber\n", 169 | "\\end{equation}\n", 170 | "\n", 171 | "$d$ 表示一篇文章, $D$ 表示整个文件集合, $N$ 表示文件集合中的文件总数, $|\\{d \\in D: t \\in d \\}|$ 表示文件集合中包含词语 $t$ 的文件数量。某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的TF-IDF。\n", 172 | "\n", 173 | "\\begin{equation}\n", 174 | "w_i = tfidf(e_i,d,D) \\nonumber\n", 175 | "\\end{equation}\n", 176 | "\n", 177 | "如果物品是电影,可以根据演员在剧中的重要程度赋予他们权重。\n", 178 | "\n", 179 | "向量空间模型的优点是简单,缺点是丢失了一些信息,比如关键词之间的关系信息。不过在绝大多数应用中,向量空间模型对于文本的分类、聚类、相似度计算已经可以给出令人满意的结果。\n", 180 | "\n", 181 | "给定物品内容的关键词向量后,物品的内容相似度可以通过向量之间的余弦相似度计算:\n", 182 | "\n", 183 | "\\begin{equation}\n", 184 | "w_{ij} = \\frac{d_i \\cdot d_j}{\\sqrt{\\|d_i\\| \\|d_j\\|}} \\nonumber\n", 185 | "\\end{equation}\n", 186 | "\n", 187 | "在实际应用中,可以首先通过建立关键词——物品的倒排表来计算物品之间的内容相似度,下面是计算的代码:\n", 188 | "\n", 189 | "```python\n", 190 | "def calculate_similarity(entity_items):\n", 191 | " w = dict()\n", 192 | " ni = dict()\n", 193 | " for e, items in entity_items.items():\n", 194 | " for i, wie in items.items():\n", 195 | " add_to_vec(ni, i, wie * wie):\n", 196 | " for j, wje in items.items():\n", 197 | " add_to_mat(w, i, j, wie, wje)\n", 198 | " for i, relate_items in w.items():\n", 199 | " relate_items = {x: y/math.sqrt(ni[i] * ni[x]) for x, y in relate_items.items()}\n", 200 | "```\n", 201 | "\n", 202 | "得到物品的相似度之后,可以利用ItemCF算法的思想,给用户推荐和他历史上喜欢的物品内容相似的物品。\n", 203 | "\n", 204 | "如果用户的行为强烈受某一内容属性的影响,那么内容过滤算法在精度上有可能超过协同过滤算法。但强的内容特征不是所有物品都具有的,且需要丰富的领域知识才能获得,很多时候内容过滤算法的精度比协同过滤算法差。如果将这两种算法融合,能够获得比单独使用这两种算法更好的效果。\n", 205 | "\n", 206 | "向量空间模型在内容数据丰富时可以获得比较好的效果。如果文本很短,关键词很少,向量空间模型就很难计算出准确的相似度。\n", 207 | "\n", 208 | "另外,两篇文章的关键词虽然不同,但关键词所属的话题可能却是相同的。\n", 209 | "\n", 210 | "在这种情况下,首先需要知道文章的话题分布,然后才能准确地计算文章的相似度。" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": { 216 | "ein.tags": "worksheet-0", 217 | "slideshow": { 218 | "slide_type": "-" 219 | } 220 | }, 221 | "source": [ 222 | "\n", 223 | "### LDA话题模型\n", 224 | "\n", 225 | "**如何建立文章、话题和关键词的关系是话题模型(topic model)研究的重点。**\n", 226 | "\n", 227 | "代表性的话题模型有LDA(Latent Dirichlet Allocation)9。\n", 228 | "\n", 229 | "LDA作为一种生成模型,对一篇文档产生的过程进行了建模,其基本思想是,一个人在写一篇文章的时候,会首先想这篇文章要讨论哪些话题,然后思考这些话题用什么词描述,从而最终用词写成一篇文章。 **文章和词之间使用话题来联系的。**\n", 230 | "\n", 231 | "LDA有3种元素,即文档、话题和词语。每一篇文档都表现为词的集合,称为 **词袋模型(bag of words)** 。每个词在一篇文章中属于一个话题。\n", 232 | "\n", 233 | "令 $D$ 为文档集合, $D[i]$ 是第 $i$ 篇文档。 $w[i][j]$ 是第 $i$ 篇文档中的第 $j$ 个词。 $z[i][j]$ 是第 $i$ 篇文档中第 $j$ 个词属于的话题。\n", 234 | "\n", 235 | "LDA计算过程包括初始化和迭代两部分。首先对 $z$ 进行初始化,假设一共有 $K$ 个话题,对第 $i$ 篇文章中的第 $j$ 个词,可以随机给它赋予一个话题。用 $NWZ(w,z)$ 记录词 $w$ 被赋予话题 $z$ 的次数, $NZD(z,d)$ 记录文档 $d$ 中被赋予话题 $z$ 的词的个数。\n", 236 | "\n", 237 | "下面是伪代码:\n", 238 | "\n", 239 | " foreach document i in range(0, |D|):\n", 240 | " foreach word j in range(0, |D(i)|):\n", 241 | " z[i][j] = rand() % K\n", 242 | " NZD[z[i][j], D[i]]++\n", 243 | " NWZ[w[i][j], z[i][j]]++\n", 244 | " NZ[z[i][j]]++\n", 245 | "\n", 246 | "初始化之后,通过迭代使话题的分布收敛到一个合理的分布上去。伪代码如下:\n", 247 | "\n", 248 | " while not converged:\n", 249 | " foreach document i in range(0, |D|):\n", 250 | " foreach word j in range(0, |D(i)|):\n", 251 | " NWZ[w[i][j], z[i][j]]--\n", 252 | " NZ[z[i][j]]--\n", 253 | " NZD[z[i][j], D[i]]--\n", 254 | " z[i][j] = SampleTopic()\n", 255 | " NWZ[w[i][j], z[i][j]]++\n", 256 | " NZ[z[i][j]]++\n", 257 | " NZD[z[i][j], D[i]]++\n", 258 | "\n", 259 | "LDA可以很好地将词组合成不同的话题。4\n", 260 | "\n", 261 | "使用LDA计算物品内容的相似度时,可以先计算出物品在话题上的分布,然后利用两个物品的话题分布计算物品的相似度。 **如果两个物品的话题分布相似,则认为两个物品具有较高的相似度,反之则认为两个物品的相似度较低。**\n", 262 | "\n", 263 | "计算分布的相似度可以利用KL散度5\n", 264 | "\n", 265 | "\\begin{equation}\n", 266 | "D_{\\mathrm{KL}}(P\\|Q) = \\sum_i P(i) \\ln \\frac{P(i)}{Q(i)} \\nonumber\n", 267 | "\\end{equation}\n", 268 | "\n", 269 | "其中 $P$ 和 $Q$ 是两个分布,KL散度越大说明分布的相似度越低。" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": { 275 | "ein.tags": "worksheet-0", 276 | "slideshow": { 277 | "slide_type": "-" 278 | } 279 | }, 280 | "source": [ 281 | "\n", 282 | "\n", 283 | "## 发挥专家的作用\n", 284 | "\n", 285 | "对于既没有用户的行为数据,也没有充足的物品内容信息来计算准确的物品相似度的推荐系统,在建立时,为让用户得到比较好的体验,会选择利用专家进行标注。代表系统是个性化网络电台[Pandora](https://www.pandora.com/)和电影推荐网站[Jinni](http://www.jinni.com/)。\n", 286 | "\n", 287 | "Pandora雇用了一批懂计算机的音乐人进行了一项称为音乐基因的项目6。其针对几万名歌手的歌的各个维度进行标注,使用了400多个特征7(Pandora称这些特征为基因)。标注完所有的歌曲后,每首歌都可以表示为一个400维的向量,然后通过常见的向量相似度算法可以计算出歌曲的相似度。\n", 288 | "\n", 289 | "Jinni也利用与Pandora相似的想法设计了电影基因系统8,让专家给电影进行标注。\n", 290 | "\n", 291 | "下面一些是针对电影的基因分类:\n", 292 | "\n", 293 | "- **心情(Mood):** 表示用户观看电影的心情,比如幽默、兴奋。\n", 294 | "- **剧情(Plot):** 包括电影剧情的标签。\n", 295 | "- **类别(Genres):** 表示电影的类别,主要包括动画片、喜剧片、动作片等分类。\n", 296 | "- **时间(Time/Period):** 电影故事发生的时间。\n", 297 | "- **地点(Place):** 电影故事发生的地点。\n", 298 | "- **观众(Audience):** 电影的主要观众群。\n", 299 | "- **获奖(Praise):** 电影的获奖和评价情况。\n", 300 | "- **风格(Style):** 功夫片、全明星阵容等。\n", 301 | "- **态度(Attitudes):** 电影描述故事的态度。\n", 302 | "- **画面(Look):** 电脑拍摄的画面技术,比如电脑动画。\n", 303 | "- **标记(Flag):** 主要表示电影有没有暴力和色情内容。\n", 304 | "\n", 305 | "Jinni通过专家和机器学习相结合的方法解决了系统冷启动问题,其在电影基因工程中采用了半人工、半自动的方式。首先,它让专家对电影进行标记,每个电影都有大约50个基因,这些基因来自大约1000个基因库。然后,在专家标记一定的样本后,Jinni会使用自然语言理解和机器学习技术,通过分析用户对电影的评论和电影的一些内容属性对电影(特别是新电影)进行自己的标记。同时,Jinni也设计了让用户对基因进行反馈的界面,希望通过用户反馈不断改进电影基因系统。" 306 | ] 307 | }, 308 | { 309 | "cell_type": "markdown", 310 | "metadata": { 311 | "ein.tags": "worksheet-0", 312 | "slideshow": { 313 | "slide_type": "-" 314 | } 315 | }, 316 | "source": [ 317 | "## 脚注\n", 318 | "\n", 319 | "1 参见维基百科Vector Space Model词条。\n", 320 | "\n", 321 | "2 参考了解中文分词方法。\n", 322 | "\n", 323 | "3 参见。\n", 324 | "\n", 325 | "4 参考David M. Blei在其论文中的实验结果。参见David M. Blei、Andrew Y. Ng、Michael I. Jordan的“Latent dirichlet allocation”(Journal of Machine Learning Research 3,2003)\n", 326 | "\n", 327 | "5 **KL散度** 是两个概率分布P和Q差别的非对称性的度量。具体参见。\n", 328 | "\n", 329 | "6 参见“About The Music Genome Project”,地址为。\n", 330 | "\n", 331 | "7 参见。\n", 332 | "\n", 333 | "8 参见。\n", 334 | "\n", 335 | "9 中文翻译为**隐含狄利克雷分布**,参见。" 336 | ] 337 | } 338 | ], 339 | "metadata": { 340 | "kernelspec": { 341 | "display_name": "Python 3", 342 | "name": "python3" 343 | }, 344 | "name": "8-cold-start.ipynb" 345 | }, 346 | "nbformat": 4, 347 | "nbformat_minor": 2 348 | } 349 | -------------------------------------------------------------------------------- /ch09/9-user-tag-data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "ein.tags": "worksheet-0", 7 | "slideshow": { 8 | "slide_type": "-" 9 | } 10 | }, 11 | "source": [ 12 | "# 利用用户标签数据\n", 13 | "推荐系统的目的是联系用户的兴趣和物品,这种联系需要依赖不同的媒介。\n", 14 | "\n", 15 | "目前流行的推荐系统基本上通过3种方式联系用户兴趣和物品:\n", 16 | "\n", 17 | "1. 利用用户喜欢过的物品,给用户推荐与他喜欢过的物品相似的物品(ItemCF)\n", 18 | "2. 利用和用户兴趣相似的其他用户,给用户推荐那些和他们兴趣爱好相似的其他用户喜欢的物品(UserCF)\n", 19 | "3. **通过一些特征(feature)联系用户和物品,给用户推荐那些具有用户喜欢的特征的物品**\n", 20 | "\n", 21 | "![img](../recsys_images/user_item_relations.png \"推荐系统联系用户和物品的几种途径\")\n", 22 | "\n", 23 | "**特征** 有不同的表现方式,可表现为物品的属性集合,也可表现为隐语义向量(latent factor vector),另外一种重要的特征表现方式就是 **标签** 。\n", 24 | "\n", 25 | "**标签** 是一种无层次化结构的、用来描述信息的关键词,它可以用来描述物品的语义。1\n", 26 | "\n", 27 | "根据给物品打标签的人的不同,标签应用一般分为两种:\n", 28 | "\n", 29 | "1. 让作者或者专家给物品打标签;\n", 30 | "2. 让普通用户给物品打标签,也就是UGC(User Generated Content,用户生成的内容)的标签应用。\n", 31 | "\n", 32 | "**UGC的标签系统是一种表示用户兴趣和物品语义的重要方式。** 当一个用户对一个物品打上一个标签,这个标签一方面描述了用户的兴趣,另一方面则表示了物品的语义,从而将用户和物品联系了起来。\n", 33 | "\n", 34 | "本章主要讨论UGC的标签应用,研究用户给物品打标签的行为,探讨如何通过分析这种行为给用户进行个性化推荐。\n", 35 | "\n" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": { 41 | "ein.tags": "worksheet-0", 42 | "slideshow": { 43 | "slide_type": "-" 44 | } 45 | }, 46 | "source": [ 47 | "\n", 48 | "## UGC标签系统的代表应用\n", 49 | "\n", 50 | "- Delicious\n", 51 | "\n", 52 | "- CiteULike\n", 53 | "\n", 54 | "- Last.fm\n", 55 | "\n", 56 | "- 豆瓣\n", 57 | "\n", 58 | "标签系统在各种各样的(音乐、视频和社交等)网站中都得到了广泛应用。\n", 59 | "\n", 60 | "标签系统的最大优势在于可以发挥群体的智能,获得对物品内容信息比较准确的关键词描述,而准确的内容信息是提升个性化推荐系统性能的重要资源。\n", 61 | "\n" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": { 67 | "ein.tags": "worksheet-0", 68 | "slideshow": { 69 | "slide_type": "-" 70 | } 71 | }, 72 | "source": [ 73 | "\n", 74 | "### 标签系统的作用\n", 75 | "\n", 76 | "GroupLens的Shilads Wieland Sen在MoveLens电影推荐系统上的研究,得到的标签系统的不同作用以及每种作用能够影响多大的人群,如下所示:\n", 77 | "\n", 78 | "- **表达:** 标签系统帮助我表达对物品的看法。(30%的用户同意)\n", 79 | "- **组织:** 打标签帮助我组织我喜欢的电影。(23%的用户同意)\n", 80 | "- **学习:** 打标签帮助我增加对电影的了解。(27%的用户同意)\n", 81 | "- **发现:** 标签系统使我更容易发现喜欢的电影。(19%的用户同意)\n", 82 | "- **决策:** 标签系统帮助我判定是否看某一部电影。(14%的用户同意)\n", 83 | "\n" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": { 89 | "ein.tags": "worksheet-0", 90 | "slideshow": { 91 | "slide_type": "-" 92 | } 93 | }, 94 | "source": [ 95 | "\n", 96 | "## 标签系统中的推荐问题\n", 97 | "\n", 98 | "标签系统中的推荐问题主要有以下两个:\n", 99 | "\n", 100 | "1. 如何利用用户打标签的行为为其推荐物品(基于标签的推荐)?\n", 101 | "2. 如何在用户给物品打标签时为其推荐适合该物品的标签(标签推荐)?\n", 102 | "\n", 103 | "首先需要解答的3个问题:\n", 104 | "\n", 105 | "1. 用户为什么要打标签?\n", 106 | "2. 用户怎么打标签?\n", 107 | "3. 用户打什么样的标签?\n", 108 | "\n" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": { 114 | "ein.tags": "worksheet-0", 115 | "slideshow": { 116 | "slide_type": "-" 117 | } 118 | }, 119 | "source": [ 120 | "\n", 121 | "### 用户为什么进行标注2\n", 122 | "\n", 123 | "- **社会维度**\n", 124 | "\n", 125 | " 有些用户标注是给内容上传者使用的(便于上传者组织自己的信息),而有些用户标注是给广大用户使用的(便于帮助其他用户找到信息)。\n", 126 | "\n", 127 | "- **功能维度**\n", 128 | "\n", 129 | " 有些标注用于更好地组织内容,方便用户将来的查找,而另一些标注用于传达某种信息,比如照片的拍摄时间和地点等。\n", 130 | "\n" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": { 136 | "ein.tags": "worksheet-0", 137 | "slideshow": { 138 | "slide_type": "-" 139 | } 140 | }, 141 | "source": [ 142 | "\n", 143 | "### 用户如何打标签\n", 144 | "\n", 145 | "通过对Delicious数据集3的研究,标签的流行度分布也呈现长尾分布。\n", 146 | "\n", 147 | "\\begin{equation}\n", 148 | "\\begin{split}\n", 149 | "& \\log{n(k)} = \\alpha \\log{k} + \\beta = \\log{k^\\alpha} \\cdot e^\\beta \\\\\n", 150 | "& n(k) = e^\\beta \\cdot k^\\alpha = \\gamma \\cdot k^\\alpha \\\\\n", 151 | "\\end{split} \\nonumber\n", 152 | "\\end{equation}\n", 153 | "\n", 154 | "上面公式中, $k$ 表示流行度, $n(k)$ 表示流行度为 $k$ 的标签总数。\n", 155 | "\n" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": { 161 | "ein.tags": "worksheet-0", 162 | "slideshow": { 163 | "slide_type": "-" 164 | } 165 | }, 166 | "source": [ 167 | "\n", 168 | "### 用户打什么样的标签\n", 169 | "\n", 170 | "用户往往不能够给物品打上准确描述物品内容属性的关键词,而可能会给物品打上各种各样奇怪的标签。\n", 171 | "\n", 172 | "下面是统计的Delicious数据集上的标签分类:\n", 173 | "\n", 174 | "- **表明物品是什么**\n", 175 | "- **表明物品的种类**\n", 176 | "- **表明谁拥有物品**\n", 177 | "- **表达用户的观点**\n", 178 | "\n", 179 | " 比如 funny(有趣)、boring(无聊)等\n", 180 | "\n", 181 | "- **用户相关的标签**\n", 182 | "\n", 183 | " 比如 my favorite(我最喜欢的)、my comment(我的评论)等\n", 184 | "\n", 185 | "- **用户的任务**\n", 186 | "\n", 187 | " 比如 to read(即将阅读)、job search(找工作)等\n", 188 | "\n", 189 | "**不同的网站一般会用不同的标签分类系统。** 下面是 Hulu 对电视剧的标签分类:\n", 190 | "\n", 191 | "- **类型(Genre)**\n", 192 | "\n", 193 | " 表示类别。\n", 194 | "\n", 195 | "- **时间(Time)**\n", 196 | "\n", 197 | " 发布时间、事件发生的时间等。\n", 198 | "\n", 199 | "- **人物(People)**\n", 200 | "- **地点(Place)**\n", 201 | "- **语言(Language)**\n", 202 | "- **奖项(Awards)**\n", 203 | "- 其他(Details)\n", 204 | "\n", 205 | " 不能归类到上面各类中的其他所有标签。\n", 206 | "\n" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": { 212 | "ein.tags": "worksheet-0", 213 | "slideshow": { 214 | "slide_type": "-" 215 | } 216 | }, 217 | "source": [ 218 | "\n", 219 | "## 基于标签的推荐系统\n", 220 | "\n", 221 | "用户用标签来描述对物品的看法,因此标签是联系用户和物品的纽带,也是反应用户兴趣的重要数据源。\n", 222 | "\n", 223 | "一个用户标签行为的数据集一般由一个三元组的集合表示,其中记录 $(u, i, b)$ 表示用户 $u$ 给物品 $i$ 打上了标签 $b$ 。4\n", 224 | "\n", 225 | "本章采用的两个不同的数据集,一个是 **Delicious数据集** (每一行由时间、用户ID、网页URL、标签4部分构成),另一个是 **CiteULike数据集** (每行由物品ID、用户ID、时间、标签4部分构成)。\n", 226 | "\n" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": { 232 | "ein.tags": "worksheet-0", 233 | "slideshow": { 234 | "slide_type": "-" 235 | } 236 | }, 237 | "source": [ 238 | "\n", 239 | "### 实验设置\n", 240 | "\n", 241 | "将数据集随机分成10份。分割的键值是用户和物品,不包括标签。挑选1份作为测试集,剩下的9份作为训练集,通过学习训练集中的用户标签数据预测测试集上用户会给什么物品打标签。\n", 242 | "\n", 243 | "对于用户 $u$ ,令 $R(u)$ 为给用户 $u$ 的长度为 $N$ 的推荐列表,里面包含我们认为用户会打标签的物品。令 $T(u)$ 是测试集中用户 $u$ 实际上打过标签的物品集合。\n", 244 | "\n", 245 | "利用准确率和召回率评测个性化推荐算法的精度。\n", 246 | "\n", 247 | "\\begin{equation}\n", 248 | "\\begin{split}\n", 249 | "& Precision = \\frac{|R(u) \\cap T(u)|}{|R(u)|} \\\\\n", 250 | "& Recall = \\frac{|R(u) \\cap T(u)|}{|T(u)|} \\\\\n", 251 | "\\end{split} \\nonumber\n", 252 | "\\end{equation}\n", 253 | "\n", 254 | "将上面的实验进行10次,每次选择不同的测试集,然后将每次实验的准确率和召回率的平均值作为最终的评测结果。\n", 255 | "\n", 256 | "下面是其他的评测指标:\n", 257 | "\n", 258 | "覆盖率:\n", 259 | "\n", 260 | "\\begin{equation}\n", 261 | "Coverage = \\frac{|\\underset{u \\in U}{U} R(u)|}{|I|} \\nonumber\n", 262 | "\\end{equation}\n", 263 | "\n", 264 | "本章用物品标签向量的余弦相似度度量物品之间的相似度。对于每个物品 $i$ ,item_tags[i]存储了物品 $i$ 的标签向量,其中 $item_tags[i][b]$ 是对物品 $i$ 打标签 $b$ 的次数,则物品 $i$ 和 $j$ 的余弦相似度可通过如下代码计算:\n", 265 | "\n", 266 | "```python\n", 267 | "def cosinesim(item_tags, i, j):\n", 268 | " ret = 0\n", 269 | " for b, wib in item_tags[i].items():\n", 270 | " if b in item_tags[j].items():\n", 271 | " ret += wib * item_tags[j][b]\n", 272 | " ni = 0\n", 273 | " nj = 0\n", 274 | " for b, w in item_tags[i].items():\n", 275 | " ni += w * w\n", 276 | " for b, w in item_tags[j].items():\n", 277 | " nj += w * w\n", 278 | " if ret == 0:\n", 279 | " return 0\n", 280 | " return ret / math.sqrt(ni * nj)\n", 281 | "```\n", 282 | "\n", 283 | "得到物品之间的相似度度量后,通过如下公式计算一个推荐列表的多样性:\n", 284 | "\n", 285 | "\\begin{equation}\n", 286 | "Diversity = 1 - \\frac{\\underset{i \\in R(u)}{\\sum} \\underset{j \\in R(u), j \\neq i}{\\sum} Sim(item\\_tags[i],item\\_tags[j])}{\n", 287 | "\\begin{pmatrix}\n", 288 | " |R(u)|\\\\\n", 289 | " 2\\\\\n", 290 | "\\end{pmatrix}} \\nonumber\n", 291 | "\\end{equation}\n", 292 | "\n", 293 | "伪代码如下:\n", 294 | "\n", 295 | "```python\n", 296 | "def Diversity(item_tags, recommend_items):\n", 297 | " ret = 0\n", 298 | " n=0\n", 299 | " for i in recommend_items.keys():\n", 300 | " for j in recommend_items.keys():\n", 301 | " if i == j:\n", 302 | " continue\n", 303 | " ret += CosineSim(item_tags, i, j)\n", 304 | " n += 1\n", 305 | " return ret / (n * 1.0)\n", 306 | "```\n", 307 | "\n", 308 | "推荐系统的多样性为所有用户推荐列表多样性的平均值。\n", 309 | "\n", 310 | "推荐结果的新颖性直接用推荐结果的平均热门程度(AveragePopularity)度量。对于物品 $i$ ,定义它的流行度item_pop(i)为给这个物品打过标签的用户数。而对推荐系统,定义它的热门度如下:\n", 311 | "\n", 312 | "\\begin{equation}\n", 313 | "AveragePopularity = \\frac{\\underset{u}{\\sum} \\underset{i \\in R(u)}{\\sum} \\log{(1 + item\\_pop(i))}}{\\underset{u}{\\sum} \\underset{i \\in R(u)}{\\sum} 1} \\nonumber\n", 314 | "\\end{equation}\n", 315 | "\n" 316 | ] 317 | }, 318 | { 319 | "cell_type": "markdown", 320 | "metadata": { 321 | "ein.tags": "worksheet-0", 322 | "slideshow": { 323 | "slide_type": "-" 324 | } 325 | }, 326 | "source": [ 327 | "\n", 328 | "### 一个最简单算法\n", 329 | "\n", 330 | "一个最简单的基于用户标签的推荐算法:\n", 331 | "\n", 332 | "1. 统计每个用户最常用的标签\n", 333 | "2. 对于每个标签,统计被打过这个标签次数最多的物品\n", 334 | "3. 对于一个用户,首先找到其最常用的标签,然后找到具有这些标签的最热门物品推荐给这个用户\n", 335 | "\n", 336 | "对于上面的算法,用户 $u$ 对物品 $i$ 的兴趣公式如下:\n", 337 | "\n", 338 | "\\begin{equation}\n", 339 | "p(u,i) = \\underset{b}{\\sum}n_{u,b}n_{b,i} \\nonumber\n", 340 | "\\end{equation}\n", 341 | "\n", 342 | "$B(u)$ 是用户 $u$ 打过的标签集合, $B(i)$ 是物品 $i$ 被打过的标签集合, $n_{u,b}$ 是用户 $u$ 打过标签 $b$ 的次数, $n_{b,i}$ 是物品 $i$ 被打过标签 $b$ 的次数。用 **SimpleTagBased** 标记这个算法。\n", 343 | "\n", 344 | "在Python中,遵循如下约定:\n", 345 | "\n", 346 | "- 用records存储标签数据的三元组,其中records[i] = [user, item, tag]\n", 347 | "- 用user_tags存储 $n_{u,b}$ ,其中user_tags[u][b] =$n_{u,b}$\n", 348 | "- 用tag_items存储 $n_{b,i}$ ,其中tag_items[b][i] =$n_{b,i}$\n", 349 | "\n", 350 | "从records中统计出user_tagstag_items:\n", 351 | "\n", 352 | "```python\n", 353 | "def init_stat(records):\n", 354 | " user_tags = dict()\n", 355 | " tag_items = dict()\n", 356 | " user_items = dict()\n", 357 | " for user, item, tag in records.items():\n", 358 | " addValueToMat(user_tags, user, tag, 1)\n", 359 | " addValueToMat(tag_items, tag, item, 1)\n", 360 | " addValueToMat(user_items, user, item, 1)\n", 361 | "```\n", 362 | "\n", 363 | "统计出user_tagstag_items之后,通过如下程序对用户进行个性化推荐:\n", 364 | "\n", 365 | "```python\n", 366 | "def recommend(user):\n", 367 | " recommend_items = dict()\n", 368 | " tagged_items = user_items[user]\n", 369 | " for tag, wut in user_tags[user].items():\n", 370 | " for item, wti in tag_items[tag].items():\n", 371 | " # if items have been tagged, do not recommend them\n", 372 | " if item in tagged_items:\n", 373 | " continue\n", 374 | " if item not in recommend_items:\n", 375 | " recommend_items[item] = wut * wti\n", 376 | " else:\n", 377 | " recommend_items[item] += wut * wti\n", 378 | " return recommend_items\n", 379 | "```\n", 380 | "\n" 381 | ] 382 | }, 383 | { 384 | "cell_type": "markdown", 385 | "metadata": { 386 | "ein.tags": "worksheet-0", 387 | "slideshow": { 388 | "slide_type": "-" 389 | } 390 | }, 391 | "source": [ 392 | "\n", 393 | "### 算法的改进\n", 394 | "\n", 395 | "\n", 396 | "#### TF-IDF\n", 397 | "\n", 398 | "SimpleTagBased算法预测用户对物品感兴趣的公式倾向于给热门标签对应的热门物品很大的权重,因此会造成推荐热门的物品给用户,从而降低推荐结果的新颖性。另外,这个公式利用用户的标签向量对用户兴趣建模,其中每个标签都是用户使用过的标签,而标签的权重是用户使用该标签的次数。这种建模方法的缺点是给热门标签过大的权重,从而不能反应用户个性化的兴趣。\n", 399 | "\n", 400 | "可以借鉴TF-IDF的思想,对这一公式进行改进:\n", 401 | "\n", 402 | "\\begin{equation}\n", 403 | "p(u,i) = \\underset{b}{\\sum} \\frac{n_{u,b}}{\\log{(1+n_{b}^{(u)})}} n_{b,i} \\nonumber\n", 404 | "\\end{equation}\n", 405 | "\n", 406 | "$n_{b}^{(u)}$ 记录了标签 $b$ 被多少个不同的用户使用过。这个算法记为 **TagBasedTFIDF** 。\n", 407 | "\n", 408 | "同理,借鉴TF-IDF的思想对热门物品进行惩罚,从而得到如下公式:\n", 409 | "\n", 410 | "\\begin{equation}\n", 411 | "p(u,i) = \\underset{b}{\\sum} \\frac{n_{u,b}}{\\log{(1+n_{b}^{(u)})}} \\frac{n_{b,i}}{\\log{(1+n_{i}^{(u)})}} \\nonumber\n", 412 | "\\end{equation}\n", 413 | "\n", 414 | "其中, $n_{i}^{(u)}$ 记录了物品 $i$ 被多少个不同的用户打过标签。这个算法记为 **TagBasedTFIDF++** 。\n", 415 | "\n", 416 | "**适当惩罚热门标签和热门物品,在增进推荐结果个性化的同时并不会降低推荐结果的离线精度。**\n", 417 | "\n" 418 | ] 419 | }, 420 | { 421 | "cell_type": "markdown", 422 | "metadata": { 423 | "ein.tags": "worksheet-0", 424 | "slideshow": { 425 | "slide_type": "-" 426 | } 427 | }, 428 | "source": [ 429 | "\n", 430 | "#### 数据稀疏性\n", 431 | "\n", 432 | "在前面的算法中,用户兴趣和物品的联系是通过 $B(u) \\cap B(i)$ 中的标签建立的。为了提高推荐的准确率,可能要对标签集合做扩展,比如若用户曾经用过“推荐系统”这个标签,可以将这个标签的相似标签也加入到用户标签集合中,比如“个性化”、“协同过滤”等标签。\n", 433 | "\n", 434 | "进行标签扩展有很多方法,其中常用的有话题模型(topic model),这里介绍一种基于邻域的方法。\n", 435 | "\n", 436 | "标签扩展的本质是对每个标签找到和它相似的标签,也就是计算标签之间的相似度。最简单的相似度可以是同义词。如果有一个同义词词典,就可以根据这个词典进行标签扩展。如果没有这个词典,可以从数据中统计出标签的相似度。\n", 437 | "\n", 438 | "如果认为同一个物品上的不同标签具有某种相似度,那么当两个标签同时出现在很多物品的标签集合中时,就可以认为这两个标签具有较大的相似度。对于标签 $b$ ,令 $N(b)$ 为有标签 $b$ 的物品的集合, $n_{b,i}$ 为给物品 $i$ 打上标签 $b$ 的用户数,可以通过如下余弦相似度公式计算标签 $b$ 和标签 $b'$ 的相似度:\n", 439 | "\n", 440 | "\\begin{equation}\n", 441 | "sim(b,b') = \\frac{\\underset{i \\in N(b) \\cap N(b')}{\\sum} n_{b,i} n_{b',i}}{\\sqrt{\\underset{i \\in N(b)}{\\sum} n_{b,i}^2 \\underset{i \\in N(b')}{\\sum} n_{b',i}^2}} \\nonumber\n", 442 | "\\end{equation}\n", 443 | "\n" 444 | ] 445 | }, 446 | { 447 | "cell_type": "markdown", 448 | "metadata": { 449 | "ein.tags": "worksheet-0", 450 | "slideshow": { 451 | "slide_type": "-" 452 | } 453 | }, 454 | "source": [ 455 | "\n", 456 | "#### 标签清理\n", 457 | "\n", 458 | "- 不是所有标签都能反应用户的兴趣。\n", 459 | " - 表示情绪的标签\n", 460 | " - 词形不同、词义相同的标签\n", 461 | "\n", 462 | "- 将标签作为推荐解释\n", 463 | "\n", 464 | "如果要把标签呈现给用户,将其作为给用户推荐某一个物品的解释,对标签的质量要求就很高。首先,这些标签不能包含没有意义的停止词或者表示情绪的词,其次这些推荐解释里不能包含很多意义相同的词语。\n", 465 | "\n", 466 | "一般来说有如下标签清理方法:\n", 467 | "\n", 468 | "- 去除词频很高的停止词;\n", 469 | "- 去除因词根不同造成的同义词,比如 recommender system和recommendation system;\n", 470 | "- 去除因分隔符造成的同义词,比如 collaborative\\_filtering 和 collaborative-filtering。\n", 471 | "\n", 472 | "为了控制标签的质量,很多网站也采用了让用户进行反馈的思想,即让用户告诉系统某个标签是否合适。\n", 473 | "\n" 474 | ] 475 | }, 476 | { 477 | "cell_type": "markdown", 478 | "metadata": { 479 | "ein.tags": "worksheet-0", 480 | "slideshow": { 481 | "slide_type": "-" 482 | } 483 | }, 484 | "source": [ 485 | "\n", 486 | "### 基于标签的推荐解释\n", 487 | "\n", 488 | "基于标签的推荐其最大好处是可以利用标签做推荐解释,这方面的代表性应用是豆瓣的个性化推荐系统。\n", 489 | "\n", 490 | "豆瓣读书的个性化推荐应用——“豆瓣猜”。\n", 491 | "\n", 492 | "- **提高了推荐结果的多样性**\n", 493 | "\n", 494 | " 通过标签云,展示了用户的所有兴趣,然后让用户自己根据他今天的兴趣选择相关的标签,得到推荐结果,从而极大地提高了推荐结果的多样性,使得推荐结果更容易满足用户多样的兴趣。\n", 495 | "\n", 496 | "- **提供了推荐解释功能**\n", 497 | "\n", 498 | " 用户通过标签云界面可以知道系统给自己推荐的每一件物品都是基于其认为自己对某个标签感兴趣。对于每个标签,用户总能通过回忆自己之前的行为知道自己是否真的对这个标签感兴趣。\n", 499 | "\n", 500 | " 首先让用户觉得标签云是有道理的,然后让用户觉得从某个标签推荐出的某件物品也是有道理的。\n", 501 | "\n", 502 | "GroupLens的研究人员Jesse Vig对基于标签的解释进行了深入研究。5和 **SimpleTagBased** 算法类似,Jesse Vig将用户和物品之间的关系变成了用户对标签的兴趣(tag preference)和标签与物品的相关度(tag relevance),然后作者用同一种推荐算法给用户推荐物品,但设计了4种标签解释的展示界面。\n", 503 | "\n", 504 | "- **RelSort:** 对推荐物品做解释时使用的是用户以前使用过且物品上有的标签,给出了用户对标签的兴趣和标签与物品的相关度,但标签按照和物品的相关度排序。\n", 505 | "- **PrefSort:** 对推荐物品做解释时使用的是用户以前使用过且物品上有的标签,给出了用户对标签的兴趣和标签与物品的相关度,但标签按照用户的兴趣程度排序。\n", 506 | "- **RelOnly:** 对推荐物品做解释时使用的是用户以前使用过且物品上有的标签,给出了标签与物品的相关度,且标签按照和物品的相关度排序。\n", 507 | "- **PrefOnly:** 对推荐物品做解释时使用的是用户以前使用过且物品上有的标签,给出了用户对标签的兴趣程度,且标签按照用户的兴趣程度排序。\n", 508 | "\n", 509 | "作者对用户设计了3种调查问卷。\n", 510 | "\n", 511 | "首先是关于推荐解释的调查问卷,作者问了如下3个问题:\n", 512 | "\n", 513 | "1. 推荐解释帮助我理解这部电影为什么会被推荐给我:对于这个问题用户认为 **RelSort>PrefOnly>=PrefSort>RelOnly** 。\n", 514 | "2. 推荐解释帮助我判定是否喜欢推荐的电影:对于这个问题用户认为 **RelSort>PrefSort> PrefOnly>RelOnly** 。\n", 515 | "3. 推荐解释帮助我判定观看这部电影是否符合我现在的兴趣:对于这个问题用户认为 **RelSort>PrefSort>RelOnly>PrefOnly** 。\n", 516 | "\n", 517 | "然后,作者调查了用户对不同类型标签的看法。\n", 518 | "\n", 519 | "作者将标签分为主观类(比如对电影的看法)和客观类(比如对电影内容的描述)。作者对每种类型的标签同样问了上面3个问题。\n", 520 | "\n", 521 | "- 这个标签帮助我理解这部电影为什么会被推荐给我:用户认为客观类标签优于主观类标签。\n", 522 | "- 这个标签帮助我判定是否喜欢推荐的电影:用户认为客观类标签优于主观类标签。\n", 523 | "- 这个标签帮助我判定观看这部电影是否符合我现在的兴趣:用户认为客观类标签优于主观类标签。\n", 524 | "\n", 525 | "**客观事实类的标签优于主观感受类标签。**\n", 526 | "\n", 527 | "最后,作者询问了用户对4种不同推荐解释界面的总体满意度,结果显示 **PrefOnly>RelSort>PrefSort>RelOnly** 。\n", 528 | "\n", 529 | "结论:\n", 530 | "\n", 531 | "- 用户对标签的兴趣对帮助用户理解为什么给他推荐某个物品更有帮助;\n", 532 | "- 用户对标签的兴趣和物品标签相关度对于帮助用户判定自己是否喜欢被推荐物品具有同样的作用;\n", 533 | "- 物品标签相关度对于帮助用户判定被推荐物品是否符合他当前的兴趣更有帮助;\n", 534 | "- 客观事实类标签相比主观感受类标签对用户更有作用。\n", 535 | "\n" 536 | ] 537 | }, 538 | { 539 | "cell_type": "markdown", 540 | "metadata": { 541 | "ein.tags": "worksheet-0", 542 | "slideshow": { 543 | "slide_type": "-" 544 | } 545 | }, 546 | "source": [ 547 | "\n", 548 | "## 给用户推荐标签\n", 549 | "\n", 550 | "让用户能够给物品打上高质量的标签,这样才能促进标签系统的良性循环。\n", 551 | "\n", 552 | "很多标签系统都设计了标签推荐模块给用户推荐标签。\n", 553 | "\n", 554 | "\n", 555 | "\n", 556 | "\n", 557 | "### 为什么要给用户推荐标签\n", 558 | "\n", 559 | "一般认为,给用户推荐标签有以下好处:\n", 560 | "\n", 561 | "- **方便用户输入标签**\n", 562 | "\n", 563 | "- **提高标签质量**\n", 564 | "\n", 565 | " 同一个语义不同的用户可能用不同的词语来表示。这些同义词会使标签的词表变得很庞大,而且会使计算相似度不太准确。而使用推荐标签时,可以对词表进行选择,首先保证词表不出现太多的同义词,同时保证出现的词都是一些比较热门的、有代表性的词。\n", 566 | "\n" 567 | ] 568 | }, 569 | { 570 | "cell_type": "markdown", 571 | "metadata": { 572 | "ein.tags": "worksheet-0", 573 | "slideshow": { 574 | "slide_type": "-" 575 | } 576 | }, 577 | "source": [ 578 | "\n", 579 | "### 如何给用户推荐标签\n", 580 | "\n", 581 | "4种简单的给用户 $u$ 推荐和物品 $i$ 相关标签的方法:\n", 582 | "\n", 583 | "1. 给用户 $u$ 推荐整个系统里最热门的标签( **PopularTags** )\n", 584 | "\n", 585 | " 令Tags[b]为标签 $b$ 的热门程度,则算法实现如下:\n", 586 | "\n", 587 | " ```python\n", 588 | " def recommend_popular_tags(user, item, tags, N):\n", 589 | " return sorted(tags.items(), key=itemgetter(1), reverse=True)[:N]\n", 590 | " ```\n", 591 | "\n", 592 | "2. 给用户 $u$ 推荐物品 $i$ 上最热门的标签( **ItemPopularTags** )\n", 593 | "\n", 594 | " 令item_tags[i][b]为物品 $i$ 被打上标签 $b$ 的次数,则算法实现如下:\n", 595 | "\n", 596 | " ```python\n", 597 | " def recommend_item_popular_tags(user, item, item_tags, N):\n", 598 | " return sorted(item_tags[item].items(), key=itemgetter(1), reverse=True)[:N]\n", 599 | " ```\n", 600 | "\n", 601 | "3. 给用户 $u$ 推荐其自己经常使用的标签 ( **UserPopularTags** )\n", 602 | "\n", 603 | " 令user_tags[u][b]为用户 $u$ 使用标签 $b$ 的次数,则算法实现如下:\n", 604 | "\n", 605 | " ```python\n", 606 | " def recommend_user_popular_tags(user, item, user_tags, N):\n", 607 | " return sorted(user_tags[user].items(), key=itemgetter(1), reverse=True)[:N]\n", 608 | " ```\n", 609 | "\n", 610 | "4. 融合第2种和第3种算法( **HybridPopularTags** )\n", 611 | "\n", 612 | " 通过一个系数将推荐结果线性加权,然后生成最终的推荐结果。算法实现如下:\n", 613 | "\n", 614 | " ```python\n", 615 | " def recommend_hybrid_popular_tags(user, item, user_tags, item_tags, alpha, N):\n", 616 | " max_user_tag_weight = max(user_tags[user].values())\n", 617 | " for tag, weight in user_tags[user].items():\n", 618 | " ret[tag] = (1 – alpha) * weight / max_user_tag_weight\n", 619 | "\n", 620 | " max_item_tag_weight = max(item_tags[item].values())\n", 621 | " for tag, weight in item_tags[item].items():\n", 622 | " if tag not in ret:\n", 623 | " ret[tag] = alpha * weight / max_item_tag_weight\n", 624 | " else:\n", 625 | " ret[tag] += alpha * weight / max_item_tag_weight\n", 626 | " return sorted(ret[user].items(), key=itemgetter(1), reverse=True)[:N]\n", 627 | " ```\n", 628 | "\n", 629 | " 上面的实现中,在将两个列表线性相加时都将两个列表按最大值做了归一化,便于控制两个列表对最终结果的影响,而不至于因为物品非常热门而淹没用户对推荐结果的影响,或者因为用户非常活跃而淹没物品对推荐结果的影响。\n", 630 | "\n" 631 | ] 632 | }, 633 | { 634 | "cell_type": "markdown", 635 | "metadata": { 636 | "ein.tags": "worksheet-0", 637 | "slideshow": { 638 | "slide_type": "-" 639 | } 640 | }, 641 | "source": [ 642 | "\n", 643 | "### 实验设置\n", 644 | "\n", 645 | "用同样的方法将数据集按照 **9∶1** 分成训练集和测试集,然后通过训练集学习用户标注的模型。注意,这里切分数据集不再是以user、item为主键,而是以user、item、tag为主键。\n", 646 | "\n", 647 | "```python\n", 648 | "def split_data(records, train, test):\n", 649 | " for user, item, tag in records:\n", 650 | " if random.randint(1, 10) == 1:\n", 651 | " test.append([user, item, tag])\n", 652 | " else:\n", 653 | " train.append([user, item, tag])\n", 654 | " return [train, test]\n", 655 | "```\n", 656 | "\n", 657 | "对于测试集中的每一个用户物品对 $(u,i)$ ,推荐 $N$ 个标签给用户 $u$ 作参考。令 $R(u,i)$ 为给用户 $u$ 推荐的应该在物品 $i$ 上打的标签集合,令 $T(u,i)$ 为用户 $u$ 实际给物品 $i$ 打的标签的集合,利用准确率和召回率评测标签推荐的精度:\n", 658 | "\n", 659 | "\\begin{equation}\n", 660 | "\\begin{split}\n", 661 | "& Precision = \\frac{\\underset{(u,i) \\in Test}{\\sum} |R(u,i) \\cap T(u,i)|}{\\underset{(u,i) \\in Test}{\\sum} |R(u,i)|} \\\\\n", 662 | "& Recall = \\frac{\\underset{(u,i) \\in Test}{\\sum} |R(u,i) \\cap T(u,i)|}{\\underset{(u,i) \\in Test}{\\sum} |T(u,i)|} \\\\\n", 663 | "\\end{split} \\nonumber\n", 664 | "\\end{equation}\n", 665 | "\n", 666 | "很多应用在给用户推荐标签时会直接给出用户最常用的标签,以及物品最经常被打的标签。\n", 667 | "\n", 668 | "基于统计用户常用标签和物品常用标签的算法有一个缺点,就是对新用户或者不热门的物品很难有推荐结果。解决这一问题的两个思路:\n", 669 | "\n", 670 | "1. 从物品的内容数据中抽取关键词作为标签(参考上下文广告6和关键词向量)\n", 671 | "2. 关键词扩展\n", 672 | "\n", 673 | " 针对有结果,但结果不太多的情况。实现标签扩展的关键就是计算标签之间的相似度。\n" 674 | ] 675 | }, 676 | { 677 | "cell_type": "markdown", 678 | "metadata": { 679 | "ein.tags": "worksheet-0", 680 | "slideshow": { 681 | "slide_type": "-" 682 | } 683 | }, 684 | "source": [ 685 | "## 脚注\n", 686 | "\n", 687 | "1 参见。\n", 688 | "\n", 689 | "2 参见Morgan Ames和Mor Naaman的“Why we tag: motivations for annotation in mobile and online media”(CHI 2007,2007)。\n", 690 | "\n", 691 | "3 参见。\n", 692 | "\n", 693 | "4 还可能包括用户打标签的时间、用户的属性数据、物品的属性数据等。\n", 694 | "\n", 695 | "5 参见Jesse Vig、Shilad Wieland Sen和John Riedl的“Tagsplanations: Explaining Recommendations Using Tags”(ACM 2009 Article,2009)。\n", 696 | "\n", 697 | "6 参见Wen-tau Yih、Joshua Goodman和Vitor R. Carvalho的“Finding Advertising Keywords on Web Pages”(ACM 2006 Article,2006)。" 698 | ] 699 | } 700 | ], 701 | "metadata": { 702 | "kernelspec": { 703 | "display_name": "Python 3", 704 | "name": "python3" 705 | }, 706 | "name": "9-user-tag-data.ipynb" 707 | }, 708 | "nbformat": 4, 709 | "nbformat_minor": 2 710 | } 711 | -------------------------------------------------------------------------------- /materials/syllabus.md: -------------------------------------------------------------------------------- 1 | # 《推荐系统入门》课程教学大纲 2 | 3 | - [大纲说明](#org7b444bc) 4 | - [教学设计](#org0ba1f85) 5 | - [教学内容细化](#org4865b20) 6 | 7 | 8 | 9 | ## 大纲说明 10 | 11 | 《推荐系统入门》课程是河北师范大学软件学院软件工程专业机器学习方向的专业必修课,本教学大纲适用于相应专业方向的本科生教学。 12 | 13 | 学习本课程之前,学生应具备一定的高等数学、概率与统计等相关数学课程的数学基础。同时学生应学习了机器学习的基础课程,如分类和聚类算法,了解机器学习模型一般的设计及训练方法。本课程中开发的示例程序使用了Python编程语言,学生应具备良好的Python编程语言知识。另外,在教学过程中,不可避免地需要使用NumPy、Pandas、Matplotlib等科学计算及数据处理等依赖库,这也需要学生对其有一定的学习和了解。 14 | 15 | 通过本课程的学习,要求学生达到下列基本目标: 16 | 17 | - 了解推荐系统是如何工作的 18 | - 了解推荐算法评测的实验方法及评测指标 19 | - 了解如何利用用户行为数据 20 | - 学习掌握基于邻域的推荐算法 21 | - 学习掌握基于模型的推荐算法 22 | - 了解推荐系统冷启动问题并学习一般的解决方案 23 | - 学习了解基于标签的推荐算法 24 | 25 | 26 | 27 | 28 | ## 教学设计 29 | 30 | 本课程在教学上采用理论与实践相结合的授课方式。理论部分通过多媒体课件来讲解基本的概念、思想、方法和原理,实践部分主要以实训任务的方式来使学生巩固理论知识并提高其实际编程能力。 31 | 32 | **教学内容及学时分配** 33 | 34 | | 序号 | 主要内容 | 学时分配 | 理论课时 | 实验课时 | 35 | |------|------------------------|----------|----------|----------| 36 | | 1 | 推荐系统概述 | 2 | 2 | | 37 | | 2 | 推荐系统实验方法 | 2 | 2 | | 38 | | 3 | 推荐系统评测指标 | 3 | 3 | | 39 | | 4 | 利用用户数据 | 3 | 3 | | 40 | | 5 | 基于用户的协同过滤算法 | 6 | 3 | 3 | 41 | | 6 | 基于物品的协同过滤算法 | 6 | 3 | 3 | 42 | | 7 | 隐语义模型 | 6 | 3 | 3 | 43 | | 8 | 冷启动问题 | 2 | 2 | | 44 | | 9 | 利用用户标签数据 | 5 | 3 | 2 | 45 | | 合计 | | 35 | 24 | 11 | 46 | 47 | 48 | 49 | 50 | ## 教学内容细化 51 | 52 | - 推荐系统概述 53 | 54 | - 什么是推荐系统 55 | - 个性化推荐系统的应用 56 | 57 | - 推荐系统实验方法 58 | 59 | - 离线实验 60 | - 用户调查 61 | - 在线实验 62 | 63 | - 推荐系统评测指标 64 | 65 | - 用户满意度 66 | - 预测准确度 67 | - 覆盖率 68 | - 多样性 69 | - 新颖性 70 | - 惊喜度 71 | - 信任度 72 | - 实时性 73 | - 健壮性 74 | - 商业目标 75 | 76 | - 利用用户行为数据 77 | 78 | - 用户行为分析 79 | - 协同过滤算法 80 | - 实验设计和算法评测 81 | 82 | - 基于用户的协同过滤算法 83 | 84 | - 基础算法 85 | - 用户相似度的改进 User-IIF 算法 86 | - 算法缺点 87 | 88 | - 基于物品的协同过滤算法 89 | 90 | - 物品相似度的归一化 91 | - UserCF和ItemCF比较 92 | - 哈利玻特问题 93 | 94 | - 基于模型的协同过滤算法 95 | 96 | - 基于矩阵分解的推荐算法 97 | - 隐语义模型和基于邻域的方法的比较 98 | 99 | - 推荐系统冷启动问题 100 | 101 | - 冷启动问题简介 102 | - 利用用户注册信息 103 | - 选择合适的物品启动用户的兴趣 104 | - 利用物品的内容信息 105 | - 发挥专家的作用 106 | 107 | - 基于标签的推荐 108 | 109 | - UGC标签系统的代表应用 110 | - 标签系统中的推荐问题 111 | - 基于标签的推荐系统 112 | - 给用户推荐标签 113 | -------------------------------------------------------------------------------- /recsys_images/640px-Long_tail.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edu2act/course-RecSys/d5c4050bd88f03599a445fd1362212182354d37e/recsys_images/640px-Long_tail.svg.png -------------------------------------------------------------------------------- /recsys_images/amazon_packaging_sale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edu2act/course-RecSys/d5c4050bd88f03599a445fd1362212182354d37e/recsys_images/amazon_packaging_sale.png -------------------------------------------------------------------------------- /recsys_images/amazon_rec_pane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edu2act/course-RecSys/d5c4050bd88f03599a445fd1362212182354d37e/recsys_images/amazon_rec_pane.png -------------------------------------------------------------------------------- /recsys_images/keywords_generation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edu2act/course-RecSys/d5c4050bd88f03599a445fd1362212182354d37e/recsys_images/keywords_generation.png -------------------------------------------------------------------------------- /recsys_images/recsys_ab_test_system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edu2act/course-RecSys/d5c4050bd88f03599a445fd1362212182354d37e/recsys_images/recsys_ab_test_system.png -------------------------------------------------------------------------------- /recsys_images/recsys_participants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edu2act/course-RecSys/d5c4050bd88f03599a445fd1362212182354d37e/recsys_images/recsys_participants.png -------------------------------------------------------------------------------- /recsys_images/user_item_relations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edu2act/course-RecSys/d5c4050bd88f03599a445fd1362212182354d37e/recsys_images/user_item_relations.png --------------------------------------------------------------------------------