如何使用深度学习和维基百科来创建图书推荐系统
深度学习可以做一些令人难以置信的事情,但通常在学术论文中使用模糊或者只需要大公司可用的计算资源。尽管如此,深度学习的应用可以在没有高级学位的个人计算机上完成。在本文中,我们将看到如何使用神经网络嵌入来创建书籍推荐系统,使用书籍上的所有维基百科文章。
我们的推荐系统将基于链接到类似维基百科页面的书籍彼此相似的想法。我们可以通过使用神经网络学习书籍嵌入和维基百科链接来表示这种相似性,从而提出建议。最终结果是有效的推荐系统和深度学习的实际应用。
斯蒂芬霍金最简近的时间史
该项目的完整代码在GitHub上以Jupyter Notebook的形式提供。如果您没有GPU,您还可以在Kaggle上找到笔记本,您可以在这里免费使用GPU训练您的神经网络。本文将重点介绍实现,以及早期文章中介绍的神经网络嵌入概念。(要了解如何检索我们将使用的数据 - 维基百科上的所有书籍文章 - 请查看本文。)
这个项目改编自深度学习手册,这本书是一本很好的书,附有深入学习的动手实例。
目录
神经网络嵌入
嵌入是一种将离散 - 分类 - 变量表示为连续向量的方法。与诸如单热编码的编码方法相比,神经网络嵌入是低维的并且是学习的,这意味着它们在嵌入空间中将类似的实体彼此更接近地放置。
为了创建嵌入,我们需要一个神经网络嵌入模型和一个监督机器学习任务。我们网络的最终结果将是每本书的表示,作为50个连续数字的向量。
虽然嵌入本身并不那么有趣 - 它们只是向量 - 它们可以用于三个主要目的:
- 在嵌入空间中查找最近邻居
- 作为机器学习模型的输入
- 低维可视化
该项目主要涵盖第一个用例,但我们也将看到如何从嵌入中创建可视化。神经网络嵌入的实际应用包括用于机器翻译的字嵌入和用于分类变量的实体嵌入。
数据:维基百科上的所有书籍
与通常的数据科学项目一样,我们需要从高质量的数据集开始。在本文中,我们了解了如何下载和处理维基百科上的每篇文章,搜索有关书籍的任何页面。我们保存了书名,基本信息,书页上指向其他维基百科页面的链接(wikilinks)以及指向外部网站的链接。要创建推荐系统,我们需要的唯一信息是标题和wikilinks。
Book Title: 'The Better Angels of Our Nature'
Wikilinks:
['Steven Pinker',
'Nation state',
'commerce',
'literacy',
'Influence of mass media',
'Rationality',
"Abraham Lincoln's first inaugural address",
'nature versus nurture',
'Leviathan']
即使在使用神经网络时,探索和清理数据也很重要,在笔记本中我会对原始数据进行多次修正。例如,查看链接最多的页面:
维基百科页面通常与维基百科上的书籍相关联。
我们看到前四页是通用的,无助于提出建议。一本书的格式并没有告诉我们的内容:知道一本书paperback
或者hardcover
不允许我们-或神经网络-要找出其他的书也有相似之处。因此,我们可以删除这些链接,以帮助神经网络区分书籍。
考虑最终目的可以在数据清理阶段提供帮助,仅此操作就可以显着改善建议。
出于纯粹的好奇心,我想找到与维基百科上其他书籍最相关的书籍。这些是10本“连接最多”的维基百科书籍:
维基百科上的书籍通常与维基百科上的其他书籍相关联。
这是参考作品和经典书籍的混合,这是有道理的。
数据清理后,我们有一套41758个独特的wikilink和37020本独特的书籍。希望每个人都有一本书!
一旦我们确信我们的数据是干净的,我们就需要开发一个受监督的机器学习任务,并带有标记的训练样例。
监督学习任务
为了学习有意义的嵌入,必须训练我们的神经网络以实现目标。根据项目的指导性假设 - 类似的书籍链接到类似的维基百科页面 - 我们可以将问题表述如下:给定(书名,wikilink)对,确定书中是否存在wikilink。
我们实际上不需要向网络提供书籍文章。相反,我们将提供数十万个由书名,wikilink和标签组成的培训示例。我们给网络提供了一些真实的例子 - 实际上存在于数据集中 - 以及一些错误的例子,并最终学习嵌入以区分wikilink何时在书页上。
表达监督学习任务是该项目最重要的部分。嵌入是针对特定任务学习的,仅与该问题相关。如果我们的任务是确定Jane Austen写的是哪些书籍,那么嵌入就会反映出这个目标,将Austen写的书放在嵌入空间中。我们希望通过培训来判断一本书是否在其页面上有某个wikilink,网络会学习嵌入式内容,这些内容会将类似的书籍 - 就内容而言 - 彼此更接近。
一旦我们概述了学习任务,我们需要在代码中实现它。首先,因为神经网络只能接受整数输入,我们创建从每个唯一的书到整数的映射:
# 将书籍映射到索引和索引
book_index = {book[0]: idx for idx, book in enumerate(books)}
book_index['Anna Karenina']
22494
我们也对链接做同样的事情。在此之后,为了创建训练集,我们列出了数据中的所有(book,wikilink)对。这需要遍历每本书并在其页面上记录每个wikilink的示例:
pairs = []
# Iterate through each book
for book in books:
title = book[0]
book_links = book[2]
# Iterate through wikilinks in book article
for link in book_links:
# Add index of book and index of link to pairs
pairs.extend((book_index[title],
link_index[link]))
这为我们提供了总共772798个真实示例,我们可以从中抽样来训练模型。为了生成错误的例子 - 稍后完成 - 我们将随机选择一个链接索引和书籍索引,确保它不在pairs
,然后将其用作否定观察。
关于培训/测试集的注意事项
虽然使用单独的验证和测试集是正常监督机器学习任务的必要条件,但在这种情况下,我们的主要目标不是制作最准确的模型,而是生成嵌入。预测任务只是我们为这些嵌入训练网络的手段。在培训结束时,我们不会在新数据上测试我们的模型,因此我们不需要评估性能或使用验证集来防止过度拟合。为了获得最好的嵌入,我们将使用所有示例进行培训。
嵌入模型
尽管神经网络嵌入在技术上听起来很复杂,但使用Keras深度学习框架它们相对容易实现。(如果您不熟悉深度学习,我建议您从Keras开始.TensorFlow可能会给您更多控制权,但是Keras不能被开发用于开发)。
嵌入模型有5层:
- 输入:书籍和链接的并行输入
- 嵌入:书籍和链接的并行长度50嵌入
- Dot:通过计算点积来合并嵌入
- 重塑:需要将点积形成单个数字
- 密集:一个具有Sigmoid的输出神经元
在嵌入式神经网络中,嵌入是神经网络的参数 - 权重 - 在训练期间进行调整,以最小化目标的损失。神经网络将书和链接作为整数,并输出0和1之间的预测,并与真实值进行比较。该模型使用Adam
优化器(随机梯度下降的变体)进行编译,在训练期间,该模型改变嵌入以最小化binary_crossentropy
该二元分类问题。
以下是完整模型的代码:
from keras.layers import Input, Embedding, Dot, Reshape, Dense
from keras.models import Model
def book_embedding_model(embedding_size = 50, classification = False):
"""使用Keras功能API嵌入书籍和wikil墨水的模型。
被训练去辨别一个链接是否存在于一本书的页面上"""
# 两个输入都是一维的
book = Input(name = 'book', shape = [1])
link = Input(name = 'link', shape = [1])
# 嵌入这本书 形状将是 (None, 1, 50)
book_embedding = Embedding(name = 'book_embedding',
input_dim = len(book_index),
output_dim = embedding_size)(book)
# Embedding the link (shape will be (None, 1, 50))
link_embedding = Embedding(name = 'link_embedding',
input_dim = len(link_index),
output_dim = embedding_size)(link)
# 沿着第二个轴将图层合并到一个点积
# (shape will be (None, 1, 1))
merged = Dot(name = 'dot_product', normalize = True,
axes = 2)([book_embedding, link_embedding])
# Reshape to be a single number (shape will be (None, 1))
merged = Reshape(target_shape = [1])(merged)
# 分类输出
out = Dense(1, activation = 'sigmoid')(merged)
model = Model(inputs = [book, link], outputs = out)
# 使用指定的优化器和损失进行编译
model.compile(optimizer = 'Adam', loss = 'binary_crossentropy',
metrics = ['accuracy'])
return model
这个相同的框架可以用于许多嵌入模型。要理解的重点是嵌入是模型参数(权重)以及我们想要的最终结果。我们并不关心模型是否准确,我们想要的是相关的嵌入。
我们习惯于模型中的权重是进行准确预测的一种手段,但在嵌入模型中,权重是目标,预测是学习嵌入的一种手段。
如模型摘要所示,有近400万个权重:
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
================================================================================================
book (InputLayer) (None, 1) 0
__________________________________________________________________________________________________
link (InputLayer) (None, 1) 0
__________________________________________________________________________________________________
book_embedding (Embedding) (None, 1, 50) 1851000 book[0][0]
__________________________________________________________________________________________________
link_embedding (Embedding) (None, 1, 50) 2087900 link[0][0]
__________________________________________________________________________________________________
dot_product (Dot) (None, 1, 1) 0 book_embedding[0][0]
link_embedding[0][0]
__________________________________________________________________________________________________
reshape_1 (Reshape) (None, 1) 0 dot_product[0][0]
================================================================================================
Total params: 3,938,900
Trainable params: 3,938,900
Non-trainable params: 0
通过这种方法,我们不仅可以嵌入书籍,还可以嵌入链接,这意味着我们可以比较所有通过书籍链接的维基百科页面。
生成培训样本
神经网络是批处理学习者,因为他们在一小组样本上进行训练 - 观察 - 在称为时期的许多轮次中进行训练。训练神经网络的常用方法是使用发生器。这是一个yields
(不returns
)批量样本的函数,因此整个结果不会保存在内存中。虽然这不是这个问题的问题,但是生成器的好处是大型训练集不需要全部加载到内存中。
我们的发生器接受培训pairs
,每批次阳性样品的数量(n_positive
),以及每批次的阴性:阳性样品的比例(negative_ratio
)。每次调用时,生成器都会产生一批新的正负样本。为了获得积极的例子,我们随机抽样真实的对。对于反面的例子,我们随机抽样一本书和链接,确保这个配对不是真正的配对,然后将它添加到批处理中。
下面的代码完整地显示了生成器。
import numpy as np
import random
random.seed(100)
def generate_batch(pairs, n_positive = 50, negative_ratio = 1.0):
"""生成批次的培训样本。
随机选择正样本
从成对和随机选择否定。"""
#创建空数组来存放批处理
batch_size = n_positive * (1 + negative_ratio)
batch = np.zeros((batch_size, 3))
# 继续提取样本
while True:
# 随机选择积极的例子
for idx, (book_id, link_id) in enumerate(random.sample(pairs, n_positive)):
batch[idx, :] = (book_id, link_id, 1)
idx += 1
# 添加负面示例,直到达到批量大小
while idx < batch_size:
# 随机选择
random_book = random.randrange(len(books))
random_link = random.randrange(len(links))
# 检查以确保这不是一个积极的例子
if (random_book, random_link) not in pairs_set:
# 添加到批处理和增量索引
batch[idx, :] = (random_book, random_link, neg_label)
idx += 1
# 一定要清洗
np.random.shuffle(batch)
yield {'book': batch[:, 0], 'link': batch[:, 1]}, batch[:, 2]
每次我们在发电机上打call next时,我们都会得到一个新的训练批。
next(generate_batch(pairs, n_positive = 2, negative_ratio = 2))
({'book': array([ 6895., 29814., 22162., 7206., 25757., 28410.]),
'link': array([ 260., 11452., 5588., 34924., 22920., 33217.])},
array([ 1., -1., 1., -1., -1., -1.]))
通过监督任务,训练生成器和嵌入模型,我们已准备好学习书籍嵌入。
培训模式
有一些训练参数可供选择。第一个是每批中的正例数。一般来说,我尝试从小批量开始并增加它直到性能开始下降。此外,我们需要选择为每个正面例子训练的负面例子的数量。我建议尝试一些选项,看看什么效果最好。由于我们没有使用验证集来实现早期停止,因此我选择了一些时期,超过这些时期,训练损失不会减少。
n_positive = 1024
gen = generate_batch(pairs, n_positive, negative_ratio = 2)
# Train
h = model.fit_generator(gen, epochs = 15,
steps_per_epoch = len(pairs) // n_positive)
(如果训练参数看起来是随意的,在某种意义上它们是,但是基于深度学习中概述的最佳实践。像机器学习的大多数方面一样,训练神经网络主要是经验性的。)
一旦网络完成训练,我们就可以提取嵌入。
# 提取嵌入
book_layer = model.get_layer('book_embedding')
book_weights = book_layer.get_weights()[0]
应用嵌入:制作建议
嵌入本身是相当无趣的:它们只是每本书和每个链接的50个数字向量:
但是,我们可以将这些向量用于两个不同的目的,第一个是制作我们的图书推荐系统。为了在嵌入空间中找到最接近查询书的书,我们采用该书的向量,并找到带有所有其他书籍向量的点积。如果我们的嵌入被归一化,则向量之间的点积表示余弦相似度,范围从-1,最不相似,到+1,最相似。
查询Leo Tolstoy对经典战争与和平的嵌入产生:
Books closest to War and Peace.
Book: Anna Karenina Similarity: 0.92
Book: The Master and Margarita Similarity: 0.92
Book: Demons (Dostoevsky novel) Similarity: 0.91
Book: The Idiot Similarity: 0.9
Book: Crime and Punishment Similarity: 0.9
这些建议很有意义!这些都是俄罗斯经典小说。当然,我们可以为GoodReads提供这些相同的建议,但为什么不自己构建系统呢?我鼓励您使用笔记本并亲自探索嵌入。
Books closest to The Fellowship of the Ring.
Book: The Return of the King Similarity: 0.96
Book: The Silmarillion Similarity: 0.93
Book: Beren and Lúthien Similarity: 0.91
Book: The Two Towers Similarity: 0.91
除了嵌入书籍之外,我们还嵌入了链接,这意味着我们可以找到与给定维基百科页面最相似的链接:
Pages closest to steven pinker.
Page: the blank slate Similarity: 0.83
Page: evolutionary psychology Similarity: 0.83
Page: reductionism Similarity: 0.81
Page: how the mind works Similarity: 0.79
目前,我正在阅读Stephen Jay Gould撰写的一系列精彩文章,称为Bully for Breontosaurus。接下来我应该阅读什么?
我下一本书的建议。
嵌入的可视化
嵌入的最有趣的方面之一是它们可以用于可视化诸如相对于彼此的小说或非小说的概念。这需要进一步降维技术以使尺寸达到2或3.最常用的还原技术是另一种嵌入方法:t-分布式随机邻域嵌入(TSNE)。
从维基百科上所有图书的37,000维空间开始,我们使用嵌入将其映射到50维,然后使用TSNE将其映射到2维。这导致以下图像:
这个图像本身并不那么有启发性,但是一旦我们开始按照图书特征对其进行着色,我们就会开始看到集群出现:
有一些明确的团块(只有前10个类型被突出显示),非小说和科幻小说有不同的部分。鉴于新颖内容的多样性,这些小说似乎到处都是有意义的。
我们也可以在国家嵌入:
我对这些国家的独特性感到有些惊讶!显然,澳大利亚的书籍非常独特。
此外,我们可以突出显示维基百科地图中的某些书籍:
笔记本中有更多可视化,您可以自己创建。我会再向你展示10本“联系最紧密”的书:
有关TSNE的一点需要注意的是,它试图保留原始空间中矢量之间的距离,但由于它减少了维数,因此可能会扭曲原始分离。因此,在50维嵌入空间中彼此接近的书籍可能不是2维TSNE嵌入中的最近邻居。
交互式可视化
这些可视化非常有趣,但我们可以使用专为可视化神经网络嵌入而设计的TensorFlow 投影仪工具制作出令人惊叹的交互式图形。我打算写一篇关于如何使用这个工具的文章,但是现在这里有一些结果:
要以交互方式探索书籍样本,请访问此处。
潜在的其他项目
数据科学项目通常不是完全靠自己发明的。我工作的很多项目都是来自其他数据科学家的想法,我会根据这些想法进行调整,改进和建立一个独特的项目。(该项目的灵感来自深度学习食谱中的电影推荐类似项目。)
考虑到这种态度,这里有几种方法可以建立在这项工作上:
- 使用外部链接而不是wikilinks 创建嵌入。这些是维基百科以外的网页,可能会产生不同的嵌入。
- 使用嵌入来训练有监督的机器学习模型来预测书籍特征,包括流派,作者和国家。
- 在维基百科上选择一个主题类别,并创建自己的推荐系统。您可以使用人物,地标甚至历史事件。您可以使用此笔记本来获取数据,并使用此笔记本进行嵌入。
这绝不是一个家庭作业,只是一些项目的想法,如果你想把你读到的东西付诸实践。如果你决定参加一个项目,我会很高兴听到它!
结论
神经网络嵌入是一种将离散分类变量表示为连续向量的方法。作为学习的低维表示,它们可用于查找类似的类别,作为机器学习模型的输入,或可视化概念图。在这个项目中,我们使用神经网络嵌入来创建一个有效的图书推荐系统,该系统的基础是链接到相似页面的书籍彼此相似。
创建神经网络嵌入的步骤是:
- 收集资料。神经网络需要许多训练样例。
- 制定监督任务以学习反映问题的嵌入。
- 构建并训练嵌入式神经网络模型。
- 提取嵌入以进行推荐和可视化。
详细信息可以在笔记本中找到,我鼓励任何人在这个项目上进行构建。虽然由于技术复杂性或计算资源,深度学习似乎势不可挡,但这是可以在学习量有限的个人计算机上完成的许多应用程序之一。深度学习是一个不断发展的领域,这个项目是通过构建有用系统开始的好方法。而且,当你不学习深度学习时,现在你知道你应该读什么了!