Web-机器学习-全-
Web 机器学习(全)
原文:
annas-archive.org/md5/c6a6a3cc62013923d6d9811870df6177译者:飞龙
前言
数据科学和机器学习,尤其是,正在成为科技商业环境中领先的主题,用于评估用户不断产生的数据量。本书将解释如何使用 Python 通过 Django 开发网络商业应用,以及如何使用一些特定的库(sklearn、scipy、nltk、Django 以及其他一些库)来操作和分析(通过机器学习技术)在应用中生成或使用的数据。
本书涵盖内容
第一章,使用 Python 的实用机器学习介绍,讨论了主要机器学习概念以及数据科学专业人士在 Python 中处理数据所使用的库。
第二章,机器学习技术 – 无监督学习,描述了用于聚类数据集和从数据中提取主要特征的算法。
第三章,监督机器学习,展示了最相关的监督算法来预测数据集的标签。
第四章,网络挖掘技术,讨论了组织、分析和从网络数据中提取信息的主要技术。
第五章,推荐系统,详细介绍了迄今为止在商业环境中使用的最流行的推荐系统。
第六章,Django 入门,介绍了开发网络应用的主要 Django 特性和特点。
第七章,电影推荐系统网络应用,描述了一个示例,将第五章,推荐系统,和第六章,Django 入门中开发的机器学习概念应用于实践,向最终网络用户推荐电影。
第八章,电影评论情感分析应用,涵盖了使用第三章,监督机器学习,第四章,网络挖掘技术,和第六章,Django 入门中解释的知识,分析在线电影评论的情感及其重要性的另一个示例。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
您需要这本书什么
读者应拥有安装了 Python 2.7 的计算机,以便能够运行(并修改)章节中讨论的代码。
这本书适合谁
任何对机器学习感兴趣或有统计背景的编程(Python)人员,无论是否追求数据科学职业,都将从阅读这本书中受益。
术语
在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“通过在终端中键入以下命令安装 Django 库:sudo pip install django。”
代码块设置如下:
INSTALLED_APPS = (
...
'rest_framework',
'rest_framework_swagger',
'nameapp',
)
任何命令行输入或输出都应如下编写:
python manage.py migrate
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“如您所见,页面主体由两个框指定,用于填写人员的姓名和电子邮件地址,按下添加将其添加到数据库。”
注意
警告或重要注意事项以这种方式出现在一个框中。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,请简单地通过电子邮件发送至 <feedback@packtpub.com> ,并在邮件主题中提及书籍标题。
如果您在某个主题领域有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您的账户下载这本书的示例代码文件。www.packtpub.com。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持选项卡上。
-
点击代码下载与勘误。
-
在搜索框中输入书籍名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书籍的来源。
-
点击代码下载。
您也可以通过点击 Packt Publishing 网站上本书页面的代码文件按钮来下载代码文件。您可以通过在搜索框中输入书名来访问此页面。请注意,您需要登录您的 Packt 账户。
文件下载后,请确保使用最新版本解压缩或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Machine-Learning-for-the-Web。我们还有其他来自我们丰富图书和视频目录的代码包可供在github.com/PacktPublishing/下载。请查看它们!
下载本书中的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表的彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/Machine-Learning-for-the-Web下载此文件。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata来报告它们,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站或添加到该标题的错误清单部分。
要查看之前提交的错误清单,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书名。所需信息将出现在错误清单部分。
盗版
互联网上版权材料的盗版是所有媒体中持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过发送链接到疑似盗版材料至 <copyright@packtpub.com > 来与我们联系。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题和建议
如果您对本书的任何方面有问题,您可以通过 <questions@packtpub.com > 联系我们,我们将尽力解决问题。
第一章. 使用 Python 的实用机器学习简介
在科技行业,分析和挖掘商业数据的能力正变得越来越重要。所有与网络世界相关的公司都会产生可以被利用来改善其业务或出售给其他公司的数据。这些可以用于商业的信息量巨大,需要使用数据科学(或数据挖掘)专业人士的专长对其进行重构和分析。数据科学采用被称为机器学习算法的技术来将数据转化为模型,这些模型能够预测商业环境高度关注的某些实体的行为。本书就是关于这些在当今科技商业世界中至关重要的算法和技术,以及如何在真实商业环境中高效地部署它们。你将学习最相关的机器学习技术,并将有机会在一系列旨在增强商业意识和运用本书中学到的技能的练习和应用程序中应用它们。我们期望你已经熟悉 Python 编程语言、线性代数和统计方法,以便完全掌握本书中讨论的主题。
- 在线有关于这些主题的许多教程和课程,但我们建议你阅读官方的 Python 文档(
docs.python.org/),A. Bluman 的《基础统计学》和 G. Casella、R. L. Berger 的《统计推断》来了解统计的主要概念和方法,以及 G. Strang 的《线性代数及其应用》来学习线性代数。
本介绍章节的目的是让你熟悉机器学习专业人士在 Python 中使用的更高级的库和工具,如NumPy、pandas和matplotlib,这些将帮助你掌握实施以下章节中介绍的技术所需的技术知识。在继续使用本书中使用的教程和库的描述之前,我们想澄清机器学习领域的主要概念,并给出一个机器学习算法如何在真实环境中预测有用信息的实际示例。
通用机器学习概念
在本书中,我们将讨论并使用最相关的机器学习算法,并通过练习使你熟悉它们。为了解释这些算法并理解本书的内容,我们需要了解一些一般概念,以下将进行描述。
首先,对机器学习的良好定义是计算机科学的一个子领域,它从模式识别、人工智能和计算学习理论等领域发展而来。机器学习也可以被视为一种数据挖掘工具,它更侧重于数据分析方面来理解提供的数据。这个学科的目的在于开发能够从先前看到的数据中学习的程序,这些程序通过可调参数(通常是双精度值数组)设计为自动调整以改进预测结果。通过这种方式,计算机可以预测一种行为,泛化数据的潜在结构,而不是像传统的数据库系统那样仅仅存储(或检索)值。因此,机器学习与计算统计学相关联,后者也试图根据先前数据预测行为。机器学习算法的常见工业应用包括垃圾邮件过滤、搜索引擎、光学字符识别和计算机视觉。现在我们已经定义了这个学科,我们可以更详细地描述每个机器学习问题中使用的术语。
任何学习问题都始于一个包含n个样本的数据集,这些样本用于预测未来未知数据的属性。每个样本通常由多个值组成,因此它是一个向量。这个向量的组成部分被称为特征。例如,想象一下根据其特征预测二手车的价格:制造年份、颜色、发动机大小等。数据集中的每辆汽车i将是一个特征向量x(i),它对应于其颜色、发动机大小和其他许多属性。在这种情况下,也与每辆汽车i相关联的一个目标(或标签)变量y(i),它是二手车的价格。一个训练示例由一对(x(i), y(i))组成,因此用于学习的完整数据点集称为训练数据集。符号x将表示特征(输入)值的空间,而y表示目标(输出)值的范围。用于解决该问题的机器学习算法将通过一个数学模型来描述,其中一些参数需要在训练集中调整。在训练阶段完成后,使用另外两个集合来评估预测的性能:验证集和测试集。验证集用于在多个模型中选择返回最佳结果的模型,而测试集通常用于确定所选模型的实际精确度。通常,数据集被分为 50%的训练集、25%的验证集和 25%的测试集。
学习问题可以分为两大类(这两类在本书中都有广泛的介绍):
-
无监督学习:训练数据集由输入特征向量 x 给出,没有任何相应的标签值。通常的目标是使用聚类算法在数据中找到相似示例,或将数据从高维空间投影到几个维度(如主成分分析等盲信号分离算法)。由于通常每个训练示例都没有目标值,无法直接从数据中评估模型的误差;你需要使用一种技术来评估每个簇内的元素彼此相似以及与其他簇成员的不同。这是无监督学习和监督学习之间的一大区别。
-
监督学习:每个数据样本都以一对形式给出,包括一个输入特征向量和标签值。任务是推断参数以预测测试数据的目标值。这类问题可以进一步分为:
-
分类:数据目标属于两个或多个类别,目标是学习如何从训练集中预测未标记数据的类别。分类是监督学习的一种离散形式(与连续形式相对),其中标签有有限数量的类别。分类问题的实际例子是手写数字识别,其目标是将每个特征向量匹配到有限数量的离散类别之一。
-
回归:标签是一个连续变量。例如,根据孩子的年龄和体重预测孩子身高的问题就是一个回归问题。
-
我们将在第二章中关注无监督学习方法,机器学习技术:无监督学习,而最相关的监督学习算法将在第三章中讨论,监督机器学习。 第四章,网络挖掘技术将探讨网络挖掘技术领域,这些技术也可以被视为监督和无监督方法。推荐系统,再次属于监督学习类别,在第五章,推荐系统中描述。然后,在第六章,开始使用 Django中介绍了Django网络框架,并在第七章,电影推荐系统网络应用中详细介绍了推荐系统(使用 Django 框架和第五章,推荐系统中解释的算法)的示例。我们用 Django 网络挖掘应用的示例结束本书,使用第四章,网络挖掘技术中学到的一些技术。到本书结束时,你应该能够理解不同的机器学习方法,并能够在使用 Django 的真正工作网络应用中部署它们。
我们通过提供一个示例来继续本章,说明机器学习如何在实际商业问题和 Python 库(NumPy、pandas 和 matplotlib)的教程中使用,这些库对于将每个后续章节中学到的算法付诸实践至关重要。
机器学习示例
为了进一步解释机器学习如何使用真实数据,我们考虑以下示例(以下代码可在作者的 GitHub 书籍文件夹github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_1/中找到)。我们从UC Irvine 机器学习仓库(archive.ics.uci.edu)的互联网广告数据集中获取数据。从各种网页上收集了网络广告,并将每个广告转换为一个数值特征向量。从ad.names文件中我们可以看到,前三个特征代表页面中的图像大小,其他特征与图像 URL 或文本中特定单词或短语的呈现有关(总共有 1558 个特征)。标签值要么是ad,要么是nonad,这取决于页面是否有广告。以下是一个ad.data中的网页示例:
125, 125, ..., 1.0, 1, 0, ad.
基于此数据,一个经典的机器学习任务就是找到一个模型来预测哪些页面是广告,哪些不是(分类)。首先,我们考虑包含完整特征向量和标签的数据文件ad.data,但其中也包含用?表示的缺失值。我们可以使用 pandas Python 库将?转换为-1(下一段将提供关于 pandas 库的完整教程):
import pandas as pd
df = pd.read_csv('ad-dataset/ad.data',header=None)
df=df.replace({'?': np.nan})
df=df.replace({' ?': np.nan})
df=df.replace({' ?': np.nan})
df=df.replace({' ?': np.nan})
df=df.replace({' ?': np.nan})
df=df.fillna(-1)
使用ad.data文件中的数据创建了一个DataFrame,首先将每个?替换为空值(使用replace函数),然后替换为-1(使用fillna函数)。现在必须将每个标签转换为数值(数据中的所有其他值也是如此):
adindices = df[df.columns[-1]]== 'ad.'
df.loc[adindices,df.columns[-1]]=1
nonadindices = df[df.columns[-1]]=='nonad.'
df.loc[nonadindices,df.columns[-1]]=0
df[df.columns[-1]]=df[df.columns[-1]].astype(float)
df.apply(lambda x: pd.to_numeric(x))
每个ad.标签已转换为1,而nonad.值已被替换为0。所有列(特征)都需要是数值和浮点类型(使用astype函数和通过lambda函数的to_numeric函数)。
我们希望使用scikit-learn库提供的支持向量机(SVM)算法(见第三章,监督机器学习)来预测数据中的 20%标签。首先,我们将数据分为两个集合:一个训练集(80%)和一个测试集(20%):
import numpy as np
dataset = df.values[:,:]
np.random.shuffle(dataset)
data = dataset[:,:-1]
labels = dataset[:,-1].astype(float)
ntrainrows = int(len(data)*.8)
train = data[:ntrainrows,:]
trainlabels = labels[:ntrainrows]
test = data[ntrainrows:,:]
testlabels = labels[ntrainrows:]
使用 Numpy 提供的库(下一段将提供教程),在分割数据以确保两个集合中的行随机选择之前,对数据进行打乱(使用random.shuffle函数)。-1表示数组的最后一列不考虑。
现在我们使用训练数据来训练我们的 SVM 模型:
from sklearn.svm import SVC
clf = SVC(gamma=0.001, C=100.)
clf.fit(train, trainlabels)
我们已经定义了clf变量,它声明了具有参数值的 SVM 模型。然后调用fit函数将模型与训练数据拟合(详见第三章,监督机器学习以获取更多详细信息)。预测 20%测试案例的平均准确度如下,使用得分函数:
score=clf.score(test,testlabels)
print 'score:',score
运行前面的代码(完整代码可在作者的 GitHub 账户的chapter_1文件夹中找到)得到 92%的准确率,这意味着 92%的预测标签测试案例与真实标签一致。这是机器学习的力量:从以前的数据中,我们能够推断出页面是否包含广告。为了实现这一点,我们本质上使用 NumPy 和 pandas 库准备和操作了数据,然后使用scikit-learn库在清理后的数据上应用了 SVM 算法。由于本书将大量使用numpy和pandas(以及一些matplotlib)库,以下段落将讨论如何安装这些库以及如何使用这些库操作(甚至创建)数据。
安装和导入模块(库)
在继续讨论库之前,我们需要明确如何安装我们想在 Python 中使用的每个模块。安装模块的常用方法是使用终端中的pip命令:
>>> sudo pip install modulename
然后通常使用以下语句将模块导入到代码中:
import numpy as np
在这里,numpy是库名称,np是从中可以访问库中任何函数X的引用名称,使用np.X而不是numpy.X。我们将假设所有库(scipy、scikit-learn、pandas、scrapy、nltk以及所有其他库)都已按这种方式安装和导入。
准备、操作和可视化数据 – NumPy、pandas 和 matplotlib 教程
大多数数据以非常不实用的形式出现,无法应用于机器学习算法。正如我们在示例(上一段)中看到的,数据可能包含缺失值或非数值列,这些数据不适合输入到任何机器学习技术中。因此,机器学习专业人士通常花费大量时间清理和准备数据,将其转换为适合进一步分析或可视化的形式。本节将介绍如何使用numpy和pandas在 Python 中创建、准备和操作数据,而matplotlib部分将提供在 Python 中绘制图表的基础。Python shell 已被用于讨论 NumPy 教程,尽管 IPython 笔记本中所有版本的代码以及纯 Python 脚本都可在作者的 GitHub 的chapter_1文件夹中找到。pandas 和 matplotlib 使用 IPython 笔记本进行讨论。
使用 NumPy
Numerical Python 或 NumPy 是一个开源的 Python 扩展库,是数据分析和高性能科学计算的基本模块。该库支持 Python 处理大型、多维数组,并提供预编译的数值函数。此外,它还提供了一组丰富的数学函数来操作这些数组。
该库提供了以下功能:
-
快速的多维数组用于向量算术运算
-
标准数学函数,用于对整个数据数组进行快速操作
-
线性代数
-
排序、唯一和集合操作
-
统计和聚合数据
NumPy 的主要优势是,与标准的 Python 操作相比,常规数组操作的速度更快。例如,对 10000000 个元素进行传统求和:
>>> def sum_trad():
>>> start = time.time()
>>> X = range(10000000)
>>> Y = range(10000000)
>>> Z = []
>>> for i in range(len(X)):
>>> Z.append(X[i] + Y[i])
>>> return time.time() - start
将其与 Numpy 函数进行比较:
>>> def sum_numpy():
>>>
start = time.time()
>>> X = np.arange(10000000)
>>> Y = np.arange(10000000)
>>> Z=X+Y
>>> return time.time() - start
>>> print 'time sum:',sum_trad(),' time sum numpy:',sum_numpy()
time sum: 2.1142539978 time sum numpy: 0.0807049274445
所用时间是2.1142539978和0.0807049274445。
数组创建
数组对象是 NumPy 库提供的主要功能。数组相当于 Python 列表,但数组中的每个元素都具有相同的数值类型(通常是 float 或 int)。可以使用以下代码通过array函数从列表定义数组类型转换。向它传递两个参数:要转换的列表和新生成数组的数据类型:
>>> arr = np.array([2, 6, 5, 9], float)
>>> arr
array([ 2., 6., 5., 9.])
>>> type(arr)
<type 'numpy.ndarray'>
反之,可以使用以下代码将数组转换为列表:
>>> arr = np.array([1, 2, 3], float)
>>> arr.tolist()
[1.0, 2.0, 3.0]
>>> list(arr)
[1.0, 2.0, 3.0]
注意
将一个数组赋值给新变量时,不会在内存中创建一个新的副本,它只是将新名称链接到同一个原始对象。
要从现有对象创建新对象,需要使用copy函数:
>>> arr = np.array([1, 2, 3], float)
>>> arr1 = arr
>>> arr2 = arr.copy()
>>> arr[0] = 0
>>> arr
array([0., 2., 3.])
>>> arr1
array([0., 2., 3.])
>>> arr2
array([1., 2., 3.])
或者,可以使用以下方式用一个单一值填充数组:
>>> arr = np.array([10, 20, 33], float)
>>> arr
array([ 10., 20., 33.])
>>> arr.fill(1)
>>> arr
array([ 1., 1., 1.])
数组也可以使用random子模块随机创建。例如,将数组的长度作为函数的输入,permutation将找到一系列随机整数:
>>> np.random.permutation(3)
array([0, 1, 2])
另一种方法,normal,将从正态分布中抽取一系列数字:
>>> np.random.normal(0,1,5)
array([-0.66494912, 0.7198794 , -0.29025382, 0.24577752, 0.23736908])
0是分布的均值,1是标准差,5是要抽取的数组元素的数量。要使用均匀分布,随机函数将返回介于0和1之间的数字(不包括1):
>>> np.random.random(5)
array([ 0.48241564, 0.24382627, 0.25457204, 0.9775729 , 0.61793725])
NumPy 还提供了一系列用于创建二维数组(矩阵)的函数。例如,要创建给定维度的单位矩阵,可以使用以下代码:
>>> np.identity(5, dtype=float)
array([[ 1., 0., 0., 0., 0.],
[ 0., 1., 0., 0., 0.],
[ 0., 0., 1., 0., 0.],
[ 0., 0., 0., 1., 0.],
[ 0., 0., 0., 0., 1.]])
eye函数返回沿第 k 对角线为 1 的矩阵:
>>> np.eye(3, k=1, dtype=float)
array([[ 0., 1., 0.],
[ 0., 0., 1.],
[ 0., 0., 0.]])
创建新数组(1 维或 2 维)最常用的函数是zeros和ones,它们创建具有指定维度的数组,并用这些值填充。这些函数是:
>>> np.ones((2,3), dtype=float)
array([[ 1., 1., 1.],
[ 1., 1., 1.]])
>>> np.zeros(6, dtype=int)
array([0, 0, 0, 0, 0, 0])
zeros_like和ones_like函数则创建一个与现有数组类型相同的新数组,具有相同的维度:
>>> arr = np.array([[13, 32, 31], [64, 25, 76]], float)
>>> np.zeros_like(arr)
array([[ 0., 0., 0.],
[ 0., 0., 0.]])
>>> np.ones_like(arr)
array([[ 1., 1., 1.],
[ 1., 1., 1.]])
另一种创建二维数组的方法是将一维数组使用vstack(垂直合并)合并:
>>> arr1 = np.array([1,3,2])
>>> arr2 = np.array([3,4,6])
>>> np.vstack([arr1,arr2])
array([[1, 3, 2],
[3, 4, 6]])
使用分布创建二维数组也是可能的,使用 random 子模块。例如,通过以下命令创建一个从 0 到 1 的均匀分布的 2x3 随机矩阵:
>>> np.random.rand(2,3)
array([[ 0.36152029, 0.10663414, 0.64622729],
[ 0.49498724, 0.59443518, 0.31257493]])
另一个常用分布是多元正态分布:
>>> np.random.multivariate_normal([10, 0], [[3, 1], [1, 4]], size=[5,])
array([[ 11.8696466 , -0.99505689],
[ 10.50905208, 1.47187705],
[ 9.55350138, 0.48654548],
[ 10.35759256, -3.72591054],
[ 11.31376171, 2.15576512]])
列表 [10,0] 是均值向量,[[3, 1], [1, 4]] 是协方差矩阵,5 是要抽取的样本数量。
| 方法 | 描述 |
|---|---|
tolist |
用于将 NumPy 数组转换为列表的函数 |
copy |
用于复制 NumPy 数组值的函数 |
ones , zeros |
用于创建全零或全一数组的函数 |
zeros_like , ones_like |
用于创建与输入列表形状相同的二维数组的函数 |
fill |
用于用特定值替换数组条目的函数 |
identity |
用于创建单位矩阵的函数 |
eye |
用于创建具有第 k 个对角线上的一个条目的矩阵的函数 |
vstack |
用于将数组合并到二维数组的函数 |
random 子模块:random , permutation , normal , rand , multivariate_normal ,以及其他 |
Random 子模块创建从分布中抽取样本的数组 |
数组操作
所有用于访问、切片和操作 Python 列表的常规操作都可以以相同的方式或类似的方式应用于数组:
>>> arr = np.array([2., 6., 5., 5.])
>>> arr[:3]
array([ 2., 6., 5.])
>>> arr[3]
5.0
>>> arr[0] = 5.
>>> arr
array([ 5., 6., 5., 5.])
使用 unique 也可以选择唯一值:
>>> np.unique(arr)
array([ 5., 6., 5.])
数组的值也可以使用 sort 进行排序,其索引使用 argsort:
>>> np.sort(arr)
array([ 2., 5., 5., 6.])
>>> np.argsort(arr)
array([0, 2, 3, 1])
使用 shuffle 函数也可以随机重新排列数组元素的顺序:
>>> np.random.shuffle(arr)
>>> arr
array([ 2., 5., 6., 5.])
NumPy 还有一个内置函数用于比较数组 array_equal:
>>> np.array_equal(arr,np.array([1,3,2]))
False
多维数组与列表不同。实际上,使用逗号(而不是列表的括号)指定维度列表。例如,二维数组(即矩阵)的元素可以通过以下方式访问:
>>> matrix = np.array([[ 4., 5., 6.], [2, 3, 6]], float)
>>> matrix
array([[ 4., 5., 6.],
[ 2., 3., 6.]])
>>> matrix[0,0]
4.0
>>> matrix[0,2]
6.0
使用冒号 : 符号在切片的初始值和结束值之间进行每个维度的切片操作:
>>> arr = np.array([[ 4., 5., 6.], [ 2., 3., 6.]], float)
>>> arr[1:2,2:3]
array([[ 6.]])
当单个 : 表示该轴上的所有元素都被考虑时:
>>> arr[1,:]
array([2, 3, 6])
>>> arr[:,2]
array([ 6., 6.])
>>> arr[-1:,-2:]
array([[ 3., 6.]])
使用 flatten 函数可以从多维数组中获得一维数组:
>>> arr = np.array([[10, 29, 23], [24, 25, 46]], float)
>>> arr
array([[ 10., 29., 23.],
[ 24., 25., 46.]])
>>> arr.flatten()
array([ 10., 29., 23., 24., 25., 46.])
也可以检查数组对象以获取有关其内容的信息。使用属性 shape 可以找到数组的大小:
>>> arr.shape
(2, 3)
在这种情况下,arr 是一个两行三列的矩阵。dtype 属性返回数组中存储的值的类型:
>>> arr.dtype
dtype('float64')
float64 是一种用于存储双精度(8 字节)实数的数值类型(类似于常规 Python 中的 float 类型)。还有其他数据类型,如 int64 , int32 , string,并且数组可以从一种类型转换为另一种类型。例如:
>>>int_arr = matrix.astype(np.int32)
>>>int_arr.dtype
dtype('int32')
当在数组上使用 len 函数时,它返回第一维的长度:
>>>arr = np.array([[ 4., 5., 6.], [ 2., 3., 6.]], float)
>>> len(arr)
2
与 Python 中的 for 循环类似,可以使用 in 关键字检查值是否包含在数组中:
>>> arr = np.array([[ 4., 5., 6.], [ 2., 3., 6.]], float)
>>> 2 in arr
True
>>> 0 in arr
False
可以通过使用函数 reshape 以这种方式操作数组,使其元素在不同维度上重新排列。例如,一个有八行一列的矩阵可以被重塑为一个有四行两列的矩阵:
>>> arr = np.array(range(8), float)
>>> arr
array([ 0., 1., 2., 3., 4., 5., 6., 7.])
>>> arr = arr.reshape((4,2))
>>> arr
array([[ 0., 1.],
[ 2., 3.],
[ 4., 5.],
[ 6., 7.]])
>>> arr.shape
(4, 2)
此外,还可以创建转置矩阵;也就是说,可以使用转置函数获得一个新数组,其最后两个维度被交换:
>>> arr = np.array(range(6), float).reshape((2, 3))
>>> arr
array([[ 0., 1., 2.],
[ 3., 4., 5.]])
>>> arr.transpose()
array([[ 0., 3.],
[ 1., 4.],
[ 2., 5.]])
数组也可以使用 T 属性进行转置:
>>> matrix = np.arange(15).reshape((3, 5))
>>> matrix
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
>>>matrix .T
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 6, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
另一种重新排列数组元素的方法是使用 newaxis 函数增加维度性:
>>> arr = np.array([14, 32, 13], float)
>>> arr
array([ 14., 32., 13.])
>> arr[:,np.newaxis]
array([[ 14.],
[ 32.],
[ 13.]])
>>> arr[:,np.newaxis].shape
(3,1)
>>> arr[np.newaxis,:]
array([[ 14., 32., 13.]])
>>> arr[np.newaxis,:].shape
(1,3)
在这个例子中,在每种情况下,新数组都有两个维度,由 newaxis 生成的一个维度长度为 1。
在 NumPy 中,通过 concatenate 函数执行数组连接操作,其语法取决于数组的维度。可以链式连接多个一维数组,指定要连接的数组为一个元组:
>>> arr1 = np.array([10,22], float)
>>> arr2 = np.array([31,43,54,61], float)
>>> arr3 = np.array([71,82,29], float)
>>> np.concatenate((arr1, arr2, arr3))
array([ 10., 22., 31., 43., 54., 61., 71., 82., 29.])
使用多维数组时,需要指定沿哪个轴连接多个数组。否则,NumPy 默认沿第一个维度进行连接:
>>> arr1 = np.array([[11, 12], [32, 42]], float)
>>> arr2 = np.array([[54, 26], [27,28]], float)
>>> np.concatenate((arr1,arr2))
array([[ 11., 12.],
[ 32., 42.],
[ 54., 26.],
[ 27., 28.]])
>>> np.concatenate((arr1,arr2), axis=0)
array([[ 11., 12.],
[ 32., 42.],
[ 54., 26.],
[ 27., 28.]])
>>> np.concatenate((arr1,arr2), axis=1)
array([[ 11., 12., 54., 26.],
[ 32., 42., 27., 28.]])
通常会将大量数据保存为二进制文件而不是直接格式。NumPy 提供了一个函数 tostring,用于将数组转换为二进制字符串。当然,也支持逆操作,即使用 fromstring 例程将二进制字符串转换为数组。例如:
>>> arr = np.array([10, 20, 30], float)
>>> str = arr.tostring()
>>> str
'\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x004@\x00\x00\x00\x00\x00\x00>@'
>>> np.fromstring(str)
array([ 10., 20., 30.])
| 方法 | 描述 |
|---|---|
unique |
从数组中选择唯一值的功能 |
random , shuffle |
用于随机重新排列数组元素的函数 |
sort , argsort |
sort 按照递增顺序对数组值进行排序,而 argsort 对数组索引进行排序,使得数组按递增顺序排列 |
array_equal |
比较两个数组并返回一个布尔值(如果它们相等则为 True,否则为 False) |
flatten |
将二维数组转换为单维数组 |
transpose |
计算二维数组的转置 |
reshape |
将二维数组的条目重新排列成不同的形状 |
concatenate |
将二维数组连接成一个矩阵 |
fromstring , tostring |
将数组转换为二进制字符串 |
数组操作
NumPy 显然支持常见的数学运算。例如:
>>> arr1 = np.array([1,2,3], float)
>>> arr2 = np.array([1,2,3], float)
>>> arr1 + arr2
array([2.,4., 6.])
>>> arr1–arr2
array([0., 0., 0.])
>>> arr1 * arr2
array([51, 4., 9.])
>>> arr2 / arr1
array([1., 1., 1.])
>>> arr1 % arr2
array([0., 0., 0.])
>>> arr2**arr1
array([1., 4., 9.])
由于任何操作都是逐元素应用的,因此数组必须具有相同的大小。如果不满足此条件,将返回错误:
>>> arr1 = np.array([1,2,3], float)
>>> arr2 = np.array([1,2], float)
>>> arr1 + arr2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: shape mismatch: objects cannot be broadcast to a single shape
错误表明对象不能进行 广播,因为使用不同大小的数组执行操作的唯一方法称为广播。这意味着数组具有不同数量的维度,维度较少的数组将被重复,直到它与另一个数组的维度相匹配。考虑以下情况:
>>> arr1 = np.array([[1, 2], [3, 4], [5, 6]], float)
>>> arr2 = np.array([1, 2], float)
>>> arr1
array([[ 1., 2.],
[ 3., 4.],
[ 5., 6.]])
>>> arr2
array([1., 1.])
>>> arr1 + arr2
array([[ 2., 4.],
[ 4., 6.],
[ 6., 8.]])
数组 arr2 被广播为一个与 arr1 大小匹配的二维数组。因此,arr2 在 arr1 的每个维度上重复,相当于以下数组:
array([[1., 2.],[1., 2.],[1., 2.]])
如果我们想使数组广播的方式更明确,newaxis 常量允许我们指定我们想要如何广播:
>>> arr1 = np.zeros((2,2), float)
>>> arr2 = np.array([1., 2.], float)
>>> arr1
array([[ 0., 0.],[ 0., 0.]])
>>> arr2
array([1., 2.])
>>> arr1 + arr2
array([[-1., 3.],[-1., 3.]])
>>> arr1 + arr2[np.newaxis,:]
array([[1., 2.],[1., 2.]])
>>> arr1 + arr2[:,np.newaxis]
array([[1.,1.],[ 2., 2.]])
与 Python 列表不同,数组可以使用条件进行查询。一个典型的例子是使用布尔数组来过滤元素:
>>> arr = np.array([[1, 2], [5, 9]], float)
>>> arr >= 7
array([[ False, False],
[False, True]], dtype=bool)
>>> arr[arr >= 7]
array([ 9.])
可以使用多个布尔表达式来对数组进行子集化:
>>> arr[np.logical_and(arr > 5, arr < 11)]
>>> arr
array([ 9.])
可以使用整数数组来指定索引,以选择另一个数组的元素。例如:
>>> arr1 = np.array([1, 4, 5, 9], float)
>>> arr2 = np.array([0, 1, 1, 3, 1, 1, 1], int)
>>> arr1[arr2]
array([ 1., 4., 4., 9., 4., 4., 4.])
arr2 表示从数组 arr1 中选择元素的有序索引:arr1 的零、第一、第一、第三、第一、第一和第一个元素按此顺序被选中。同样,列表也可以用于相同的目的:
>>> arr = np.array([1, 4, 5, 9], float)
>>> arr[[0, 1, 1, 3, 1]]
array([ 1., 4., 4., 9., 4.])
为了用多维数组复制相同的操作,必须将多个一维整数数组放入选择括号中,每个维度一个。
第一个选择数组表示矩阵条目中的第一个索引的值,而第二个选择数组上的值表示矩阵条目的列索引。以下示例说明了这个概念:
>>> arr1 = np.array([[1, 2], [5, 13]], float)
>>> arr2 = np.array([1, 0, 0, 1], int)
>>> arr3 = np.array([1, 1, 0, 1], int)
>>> arr1[arr2,arr3]
array([ 13., 2., 1., 13.])
arr2 中的值是 arr1 中条目的第一个索引(行),而 arr3 是第二个索引(列)的值,因此 arr1 中第一个选择的条目对应于第 1 行第 1 列,即 13。
函数 take 可以用于对整数数组应用选择,并且它的工作方式与方括号选择相同:
>>> arr1 = np.array([7, 6, 6, 9], float)
>>> arr2 = np.array([1, 0, 1, 3, 3, 1], int)
>>> arr1.take(arr2)
array([ 6., 7., 6., 9., 9., 6.])
可以通过在 take 函数上指定轴参数来沿给定维度选择多维数组的子集:
>>> arr1 = np.array([[10, 21], [62, 33]], float)
>>> arr2 = np.array([0, 0, 1], int)
>>> arr1.take(arr2, axis=0)
array([[ 10., 21.],
[ 10., 21.],
[ 62., 33.]])
>>> arr1.take(arr2, axis=1)
array([[ 10., 10., 21.],
[ 62., 62., 33.]])
put 函数是 take 函数的反面,它从数组中获取值并将它们放在调用 put 方法的数组中指定的索引处:
>>> arr1 = np.array([2, 1, 6, 2, 1, 9], float)
>>> arr2 = np.array([3, 10, 2], float)
>>> arr1.put([1, 4], arr2)
>>> arr1
array([ 2., 3., 6., 2., 10., 9.])
我们以注意,乘法对于二维数组来说仍然是逐元素进行的(并不对应矩阵乘法):
>>> arr1 = np.array([[11,22], [23,14]], float)
>>> arr2 = np.array([[25,30], [13,33]], float)
>>> arr1 * arr2
array([[ 275., 660.],
[ 299., 462.]])
| 方法 | 描述 |
|---|---|
take |
从由第二个数组给出的索引中选择数组的值 |
put |
用另一个数组在给定位置上的值替换数组中的值 |
线性代数运算
矩阵与它的转置的内积是最常见的矩阵操作,即 X^T X 使用 np.dot 计算:
>>> X = np.arange(15).reshape((3, 5))
>>> X
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
>>> X.T
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 6, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
>>>np.dot(X .T, X)#X^T X
array([[ 2.584 , 1.8753, 0.8888],
[ 1.8753, 6.6636, 0.3884],
[ 0.8888, 0.3884, 3.9781]])
存在函数可以直接计算数组上的不同类型的产品(inner、outer 和 cross)。
对于一维数组(向量)来说,内积对应于点积:
>>> arr1 = np.array([12, 43, 10], float)
>>> arr2 = np.array([21, 42, 14], float)
>>> np.outer(arr1, arr2)
array([[ 252., 504., 168.],
[ 903., 1806., 602.],
[ 210., 420., 140.]])
>>> np.inner(arr1, arr2)
2198.0
>>> np.cross(arr1, arr2)
array([ 182., 42., -399.])
NumPy 还包含一个子模块 linalg,它有一系列函数用于在矩阵上执行线性代数计算。矩阵的行列式可以计算如下:
>>> matrix = np.array([[74, 22, 10], [92, 31, 17], [21, 22, 12]], float)
>>> matrix
array([[ 74., 22., 10.],
[ 92., 31., 17.],
[ 21., 22., 12.]])
>>> np.linalg.det(matrix)
-2852.0000000000032
同样,可以使用函数 inv 生成矩阵的逆:
>>> inv_matrix = np.linalg.inv(matrix)
>>> inv_matrix
array([[ 0.00070126, 0.01542777, -0.02244039],
[ 0.26192146, -0.23772791, 0.11851332],
[-0.48141655, 0.4088359 , -0.09467041]])
>>> np.dot(inv_matrix,matrix)
array([[ 1.00000000e+00, 2.22044605e-16, 4.77048956e-17],
[ -2.22044605e-15, 1.00000000e+00, 0.00000000e+00],
[ -3.33066907e-15, -4.44089210e-16, 1.00000000e+00]])
计算矩阵的特征值和特征向量很简单:
>>> vals, vecs = np.linalg.eig(matrix)
>>> vals
array([ 107.99587441, 11.33411853, -2.32999294])
>>> vecs
array([[-0.57891525, -0.21517959, 0.06319955],
[-0.75804695, 0.17632618, -0.58635713],
[-0.30036971, 0.96052424, 0.80758352]])
| 方法 | 描述 |
|---|---|
dot |
两个数组之间的点积 |
inner |
多维数组之间的内积 |
linalg模块包含如linalg.det、linalg.inv、linalg.eig等函数 |
linalg是一个收集了多种线性代数方法的模块,其中包括矩阵的行列式(det)、矩阵的逆(inv)以及矩阵的特征值和特征向量(eig) |
统计和数学函数
NumPy 提供了一套用于计算数组中数据统计函数的函数。聚合操作,如求和、平均值、中位数和标准差,作为数组的属性可用。例如,创建一个随机数组(来自正态分布),可以通过两种方式计算平均值:
>>> arr = np.random.rand(8, 4)
>>> arr.mean()
0.45808075801881332
>>> np.mean(arr)
0.45808075801881332
>>> arr.sum()
14.658584256602026
以下表格显示了函数的完整列表:
| 方法 | 描述 |
|---|---|
mean |
元素的平均值。如果数组为空,则默认将平均值设置为Na N。 |
std , var |
计算数组的标准差(std)和方差(var)的函数。可以指定一个可选的自由度参数(默认为数组的长度)。 |
min , max |
确定数组中包含的最小值(min)和最大值(max)的函数。 |
argmin , argmax |
这些函数返回最小元素(argmin)和最大元素(argmax)的索引。 |
理解 pandas 模块
pandas 是一个强大的 Python 模块,包含了一系列用于分析数据结构的功能。pandas 依赖于 NumPy 库,并且旨在使数据分析操作变得简单快捷。该模块在性能方面优于常规 Python 函数,尤其是在读取或写入文件或创建数据库时;pandas 是执行数据操作的最佳选择。以下段落讨论了探索数据中包含的信息的主要方法以及如何对其进行操作。我们首先描述数据在 pandas 中的存储方式以及如何将其加载到其中。
注意
在本书的其余部分,我们对pandas使用以下导入约定:
import pandas as pd
因此,每当代码中包含字母pd时,它指的是pandas。
探索数据
为了将称为DataFrame的数据库结构引入 pandas,我们需要描述包含任何 NumPy 数据类型的数据以及与其关联的数据标签数组,即其索引的一维数组-like 对象。这种结构称为Series,一个简单的例子是:

obj对象由两个值组成,左边的索引和右边的关联值。鉴于数据的长度等于N,默认索引从0到N-1。Series的数组和索引对象可以通过其值和索引属性分别获得:

通过应用 NumPy 数组操作(如标量乘法、布尔数组过滤或应用数学函数)来保留索引:

一个 Python 字典可以被转换成一个Series,但索引将对应于键值:

可以指定一个分开的列表作为索引:

在这种情况下,最后一个索引值g没有关联的对象值,因此默认插入一个非数字(NaN)。
术语缺失或NA将用于指代缺失数据。要找到缺失数据,可以在 pandas 中使用isnull和notnull函数:

现在我们可以开始将 CSV 文件加载到 DataFrame 结构中。DataFrame 表示一个包含有序列的数据结构,每一列可以有不同的值类型(数值、字符串、布尔值和其他)。DataFrame 有两个索引(行索引和列索引),它可以被视为一个共享相同索引(列)的Series字典。为了本教程的目的,我们正在使用存储在archive.ics.uci.edu网站上的ad.data文件中的数据(如前所述的机器学习示例中所述)。
使用终端(在这种情况下路径是data_example/ad-dataset/ad-data)以下方式加载数据:

此文件没有标题(设置为none),因此列名是数字,我们可以使用describe函数在数据对象上获取 DataFrame 的摘要:

这总结了定量信息。我们可以看到有1554个数值列(由于没有标题,用数字表示)和3279行(每列称为count)。每一列都有一个统计参数列表(平均值、标准差、最小值、最大值和分位数),这有助于获取数据中包含的定量信息的初始估计。
可以使用columns属性来获取列名:

因此,所有列名都是int64类型,以下命令返回所有列的实际类型:

前四列和标签(最后一列)是object类型,而其他列是int64类型。可以通过两种方式访问列。第一种方法是通过指定列名,就像字典中的键一样:

可以通过指定列名列表来获得多列:

另一种访问列的方法是使用点语法,但只有当列名也可以作为 Python 变量名(即没有空格),并且不与 DataFrame 属性或函数名(如 count 或 sum)相同,并且名称是字符串类型(不是像在这个例子中的int64)时,它才会工作。
要简要了解 DataFrame 的内容,可以使用head()函数。默认情况下,返回列中的前五个项目(或 DataFrame 中的前五行):

相反的方法是tail(),默认情况下返回最后五个项目或行。在tail()或head()函数上指定一个数字,将返回所选列中的前n个项目:

也可以使用 Python 的常规切片语法来获取 DataFrame 的一定数量的行:

这个例子只显示了从1到3的行。
操作数据
可以以不同的方式选择行,例如指定索引或条件如下:

或者指定多个条件:

返回的数据是具有特征1大于0且包含广告的网页。
ix方法允许我们通过指定所需的索引来选择行:

或者使用函数iloc:

差异在于ix在索引列的标签上工作,而iloc在索引的位置上工作(因此它只接受整数)。因此,在这个例子中,ix找到从0到出现标签3的所有行,而iloc函数返回数据框中前3个位置的行。还有一个第三个函数可以访问 DataFrame 中的数据,即loc。这个函数查看与行关联的索引名称,并返回它们的值。例如:

注意,这个函数与 Python 中的正常切片行为不同,因为起始行和结束行都包含在结果中(索引为3的行包含在输出中)。
也可以将整个列设置为某个值:

要也将特定单元格的值设置为所需的值:

或者将整行设置为一系列值(在这个例子中是介于0和1之间的随机值和ad.标签):

在将Series对象中的值数组转换后,可以在 DataFrame 的末尾添加一行:

或者,可以使用loc函数(如 NumPy 中所示)在最后一行添加一行:

通过简单地将新列名赋给值,很容易在 DataFrame 中添加一列:

在这个例子中,新列包含了分配给测试值的所有条目。同样,可以使用drop函数删除该列:

数据集可能由于各种原因包含重复项,因此 pandas 提供了duplicated方法来指示每一行是否是重复的:

更有用的是,drop_duplicates函数返回一个只包含唯一值的 DataFrame。例如,对于标签,唯一值如下:

结果可以转换成一个列表:

正如我们在机器学习示例中所做的那样,这些标签可以使用前面示例中解释的方法转换为数值:

标签列仍然是object类型:

因此,现在可以将该列转换为浮点类型:

前四列包含混合值(字符串、?和浮点数),因此我们需要删除字符串值以将列转换为数值类型。我们可以使用replace函数将所有?(缺失值)实例替换为NaN:

现在我们可以用两种方式处理这些包含缺失数据的行。第一种方法就是使用dropna删除包含缺失值的行:

而不是删除包含缺失数据的行(这可能会导致删除重要信息),可以使用fillna方法填充空条目。对于大多数目的,可以在空单元格中插入一个常数值:

现在所有值都是数值型,可以将列设置为float类型,应用astype函数。或者,我们可以应用一个lambda函数将 DataFrame 中的每一列转换为数值类型:

每个x实例是一个列,to_numeric函数将其转换为最接近的数值类型(在这种情况下是float):
为了使这个教程更加完整,我们想展示如何将两个 DataFrame 连接起来,因为这种操作在实际应用中可能很有用。让我们创建另一个包含随机值的 DataFrame:

使用concat函数可以将这个包含两行的新表与原始 DataFrame 合并,将data1的行放置在data的底部:

与data相比,datatot的行数现在增加了两行(注意,行数与开始时不同,因为我们删除了包含NaN的行)。
Matplotlib 教程
matplotlib.pyplot 是一个收集了一系列用于绘制数据的函数的库,类似于 MATLAB。由于以下章节将使用这个库来可视化一些结果,这里的一个简单示例将解释你将在本书中看到的所有的 matplotlib 代码:

导入库(作为 plt)后,初始化 figure 对象(fig)并添加一个 axis 对象(ax)。通过命令 ax.plot() 绘制到 ax 对象中的每条线都称为句柄。然后,所有后续指令都由 matplotlib.pyplot 记录并在 figure 对象中绘制。在这种情况下,绿色线条已从终端显示并保存为 figure.png 文件,分别使用 plt.show() 和 fig.savefig() 命令。结果是:

简单绘图示例
下一个示例展示了使用 Numpy 数组在一个命令中绘制具有不同格式样式的几条线的绘图:


多条线绘图示例
注意,函数 get_legend_handles_labels() 返回存储在对象 ax 中的句柄和标签列表,并将它们传递给绘图函数 legend。符号 'r--'、'bs' 和 'g^' 分别指代点的形状及其颜色(红色矩形、蓝色正方形和绿色三角形)。linewidth 参数设置线的粗细,而 markersize 设置点的尺寸。
另一个有用的可视化结果的方法是散点图,其中显示了一组数据(使用 NumPy random 子模块生成)中通常两个变量的值:

s 选项表示点的尺寸,colors 是对应于每组点的颜色,句柄直接传递到图例函数(p1、p2、p3):

随机分布点的散点图
关于如何使用 matplotlib 的更多细节,我们建议读者阅读在线材料和学习教程,例如 matplotlib.org/users/pyplot_tutorial.html。
书中使用的科学库
在本书中,为了实现每一章讨论的机器学习技术,某些库是必要的。我们将简要描述以下将使用的最相关的库:
-
SciPy 是基于 NumPy 数组对象的一系列数学方法。它是一个开源项目,因此它利用了来自世界各地开发者的持续编写的方法。使用 SciPy 例程的 Python 软件是高级项目或应用的一部分,与 MATLAB、Octave 或 RLab 等类似框架相当。它提供了从操作和可视化数据函数到并行计算例程的广泛方法,这些例程增强了 Python 语言的灵活性和潜力。
-
scikit-learn(sklearn)是 Python 编程语言的开放源代码机器学习模块。它实现了包括支持向量机、朴素贝叶斯、决策树、随机森林、k-means 和 基于密度的空间聚类应用噪声(DBSCAN)在内的聚类、分类和回归等算法,并且与数值 Python 库(如 NumPy 和 SciPy)进行原生交互。尽管大多数例程是用 Python 编写的,但一些函数是用 Cython 实现的,以实现更好的性能。例如,支持向量机和逻辑回归是用 Cython 包装其他外部库(LIBSVM、LIBLINEAR)编写的。
-
自然语言工具包(NLTK)是 Python 语言处理中用于自然语言处理(NLP)的一组库和函数。NLTK 设计用于支持 NLP 和相关领域的研究和教学,包括人工智能、认知科学、信息检索、语言学和机器学习。它还提供了一系列文本处理例程,用于分词、词干提取、标记、解析、语义推理和分类。NLTK 包括示例代码和示例数据,以及超过 50 个语料库和词汇数据库的接口。
-
Scrapy 是一种用于 Python 编程语言的开放源代码网络爬虫框架。最初设计用于抓取网站,作为一个通用爬虫,它也适用于通过 API 提取数据。Scrapy 项目围绕提供一组指令的 蜘蛛 进行编写。它还提供了一个网络爬虫外壳,允许开发者在实际实现之前测试他们的概念。Scrapy 目前由 Scrapinghub Ltd. 维护,这是一家网络爬取开发和服务的公司。
-
Django 是一个遵循 模型-视图-控制器 架构模式的免费和开源 Web 应用程序框架,用 Python 实现。Django 设计用于创建复杂、以数据库为中心的网站。它还允许我们通过管理界面来管理应用程序,可以创建、读取、删除或更新应用程序中使用的数据。目前有一系列已建立的网站正在使用 Django,例如 Pinterest、Instagram、Mozilla、华盛顿时报和 Bitbucket。
何时使用机器学习
机器学习并非魔法,它可能对所有与数据相关的问题都不一定有益。在本介绍的最后,重要的是要明确机器学习技术在何时极为有用:
-
规则无法编码:一系列人类任务(例如,确定一封电子邮件是否为垃圾邮件)无法通过简单的规则方法有效解决。实际上,多个因素可能影响解决方案,如果规则依赖于大量因素,那么对人类来说手动实施这些规则变得很困难。
-
解决方案不可扩展:每当手动对某些数据进行决策耗时较长时,机器学习技术可以适当地扩展。例如,机器学习算法可以有效地处理数百万封电子邮件并确定它们是否为垃圾邮件。
然而,如果可能找到良好的目标预测,仅通过使用数学规则、计算或无需数据驱动学习的预定方案,这些高级机器学习技术就不再必要(您也不应该使用它们)。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
摘要
在本章中,我们介绍了本书其余部分将使用的机器学习基本概念和术语。我们还介绍了机器学习专业人士用于准备、处理和可视化数据的最重要的库(NumPy、pandas 和 matplotlib)的教程。还提供了一般介绍所有将在以下章节中使用的其他 Python 库。
您应该对机器学习领域实际上能做什么有一个一般性的了解,并且您现在应该熟悉将数据转换为可用格式的方法,以便应用机器学习算法。在下一章中,我们将解释主要的无监督学习算法以及如何使用sklearn库来实现它们。
第二章。无监督机器学习
如我们在第一章中所见,使用 Python 的实用机器学习介绍,无监督学习旨在提供关于未标记数据的洞察性信息。在许多情况下,大数据集(无论是点的数量还是特征的数量)都是非结构化的,并且一开始并不呈现任何信息,因此这些技术被用来突出数据中的隐藏结构(聚类)或在不丢失相关信息的情况下降低其复杂性(降维)。本章将重点关注主要的聚类算法(本章的第一部分)和降维方法(本章的第二部分)。通过提供使用 Python 库的实用示例来突出显示这些方法的差异和优势。所有代码都将可在作者的 GitHub 个人资料中找到,在github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_2/文件夹中。我们现在将开始描述聚类算法。
聚类算法
聚类算法被用于以某种有序的方式重新组织数据,以便可以推断出有意义的结构。一个簇可以被定义为具有某些相似特征的数据点集合。量化数据点相似度的方法决定了聚类的不同类别。
聚类算法可以根据数据操作的不同度量或假设被划分为不同的类别。我们将讨论目前最相关的类别,这些类别包括分布方法、质心方法、密度方法和层次方法。对于每个类别,我们将详细介绍一个特定的算法,并且我们将首先讨论分布方法。将讨论一个示例来比较不同的算法,并且 IPython 笔记本和脚本都可在我的 GitHub 书籍文件夹中找到,链接为github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_2/。
分布方法
这些方法假设数据来自某种分布,并且使用期望最大化算法来找到分布参数的最优值。接下来将讨论期望最大化和高斯聚类的混合。
期望最大化
此算法用于寻找依赖于隐藏(未观测)变量的参数分布模型的最大似然估计。期望最大化包括迭代两个步骤:E 步骤,它创建使用当前参数值评估的对数似然函数,以及M 步骤,在此步骤中计算新的参数值,以最大化 E 步骤的对数似然。
考虑一个包含 N 个元素,{x^((i)) }i = 1,…,N,和一个数据集上的对数似然如下:

在这里,θ 代表参数集,而 z^((i)) 是所谓的隐藏变量。
我们希望找到最大化对数似然参数值,而不了解 z^((i))(未观测变量)的值。考虑一个关于 z^((i)) 的分布,以及 Q(z^((i)) ),例如
。因此:

这意味着 Q(z^((i)) ) 是给定 x^((i)) 参数化的 θ 的隐藏变量 z^((i)) 的后验分布。期望最大化算法来自 Jensen 不等式的使用,并确保执行以下两个步骤:
对数似然收敛到最大值,因此找到了相关的 θ 值。
高斯混合
此方法使用高斯分布的混合来模拟整个数据集。因此,簇的数量将作为混合中考虑的高斯数量给出。给定一个包含 N 个元素的数集,{x^((i)) }i = 1,…,N,其中每个
是一个由高斯混合建模的 d 特征向量,如下所示:

其中:
-
是一个隐藏变量,表示每个 *x^((i)) * 生成的来自高斯成分 -
代表高斯成分的均值参数集 -
代表高斯成分的方差参数集 -
是混合权重,表示随机选择的 *x^((i)) * 被高斯成分 k 生成的概率,其中
,而
是权重集 -
是与点 x^((i)) 相关的参数
的关联高斯成分 k
因此,我们模型的参数是φ,µ和∑。为了估计它们,我们可以写下数据集的对数似然:

为了找到参数的值,我们应用了前一小节中解释的期望最大化算法,其中包含
和
。
在选择参数的第一个猜测之后,我们重复以下步骤直到收敛:
-
E 步骤:通过应用贝叶斯定理得到的规则更新权重
:![高斯混合]()
-
M 步骤:参数更新为以下形式(这些公式来源于解决最大化问题,即设置对数似然函数的导数为零):
![高斯混合]()
![高斯混合]()
![高斯混合]()
注意,期望最大化算法是必需的,因为隐藏变量 z^((i)) 是未知的。否则,它将是一个监督学习问题,其中 z^((i)) 是训练集中每个点的标签(并且使用的监督算法将是高斯判别分析)。因此,这是一个无监督算法,目标也是找到 z^((i)) ,即每个点 x^((i)) 与哪个 K 个高斯成分相关联。实际上,通过计算每个 K 个类别的后验概率
,可以将每个 x(i) 分配给后验概率最高的类 k。在几种情况下,这个算法可以成功地用于聚类(标记)数据。
一个可能的实际例子是一位教授,他有两个不同班级的学生成绩,但没有按班级标记。他希望将成绩分为原始的两个班级,假设每个班级的成绩分布是高斯分布。另一个可以用高斯混合方法解决的问题是根据来自两个不同国家的一组人的身高值来确定每个人的国家,假设每个国家的身高分布遵循高斯分布。
聚类方法
此类收集了所有寻找聚类中心的技术,将数据点分配给最近的聚类中心,并最小化中心与分配给点的距离。这是一个优化问题,最终的中心是向量;它们可能不是原始数据集中的点。聚类数是一个需要事先指定的参数,生成的聚类倾向于具有相似的大小,因此边界线不一定定义良好。此优化问题可能导致局部最优解,这意味着不同的初始化值可能导致略微不同的聚类。最常见的方法是称为 k-means(Lloyd 算法),其中最小化的距离是 欧几里得范数。其他技术将中心寻找为聚类的中位数(k-medians 聚类)或强制中心值是实际数据点。此外,这些方法的变体在定义初始中心的选择上有所不同(k-means++ 或 模糊 c-均值)。
k-means
此算法试图将每个聚类的中心作为所有成员的均值,以最小化中心与分配给点的距离。它可以与分类问题中的 k-最近邻算法相关联,并且生成的聚类集可以表示为 Voronoi 图(一种基于从一组点(在这种情况下,为聚类中心)的距离来划分空间区域的方法)。考虑常用的数据集,
。算法规定选择一个中心数 K,将初始均值聚类中心
分配给随机值,然后迭代以下步骤直到收敛:
-
对于每个数据点 i,计算每个点 i 与每个中心 j 之间的欧几里得平方距离,并找到对应于这些距离最小值的中心索引 d[i]:
。 -
对于每个中心 j,重新计算其均值,作为具有 d_ij 等于 j 的点的均值(即属于均值
的聚类中的点):
。可以证明,此算法相对于以下函数收敛:
![k-means]()
随着迭代次数的增加,它单调递减。由于 F 是一个非凸函数,不能保证最终的最小值是全球最小值。为了避免与局部最小值相关的聚类结果问题,k-means 算法通常多次运行,每次使用不同的随机初始中心均值。然后选择与较低 F 值相关的结果作为最优聚类解决方案。
密度方法
这些方法基于这样的想法,稀疏区域必须被认为是边界(或噪声),而高密度区域应该与聚类的中心相关联。常见的方法被称为基于密度的空间聚类应用噪声(DBSCAN),它通过一定的距离阈值定义两点之间的连接(因此,它与层次算法类似;参见第三章,监督机器学习)。只有当满足一定的密度标准时,两点才被认为是相连的(属于同一个聚类)——在某个半径内,邻近点的数量必须高于一个阈值。另一种流行的方法是均值漂移,其中每个数据点被分配到其邻域密度最高的聚类。由于通过核密度估计计算密度耗时,均值漂移通常比 DBSCAN 或质心方法慢。这类算法的主要优点是能够定义任意形状的聚类,并且能够确定最佳聚类数量,而不是预先将此数量作为参数设置,这使得这些方法适合于聚类未知数量的数据集。
均值漂移
均值漂移是一种非参数算法,它在一个数据集上定义的密度核函数中找到局部最大值的位置。找到的局部最大值可以被认为是数据集
中聚类的中心,最大值的数量是聚类数量。为了作为聚类算法应用,每个点
必须与其邻域的密度相关联:

在这里,h 是所谓的带宽;它估计了影响密度值 f(x^((l))) 的点的邻域半径(即,其他点对
的影响可以忽略不计)。K 是满足这些条件的核函数:
K(x^((i))) 的典型例子包括以下函数:
-
: 高斯核 -
: Epanechnikov 核
均值漂移算法强制最大化 f(x^((l))),这转化为数学方程(记住在函数分析中,最大值是通过将导数设为 0 来找到的):

在这里,K' 是核密度函数 K 的导数。
因此,为了找到与特征向量 x^((l)) 相关的局部最大值位置,可以计算以下迭代方程:

这里,
被称为均值漂移向量。当迭代 t=a 时,算法将收敛,满足条件
。
在方程的支持下,我们可以借助以下图解来解释算法。在第一次迭代 t=0 时,原始点
(红色)散布在数据空间中,计算了均值漂移向量
,并且用交叉( x )标记相同的点以跟踪它们随算法的演变。在迭代 1 时,将使用上述方程获得数据集,并在以下图中用( + )符号显示结果点
:

均值漂移迭代过程中的演变草图
在前一个图中,迭代 0 时原始点用红色(交叉)表示,迭代 1 和 K 时样本点(符号 + 和 *** 分别)向由蓝色方块指示的局部密度最大值移动。
再次,在迭代 K 时,计算了新的数据点
,并在前一个图中用 *** 符号表示。与之前迭代相比,与
相关的密度函数值
更大,因为算法旨在最大化这些值。现在原始数据集与点
明确关联,并且它们收敛到前一个图中用蓝色方块标记的位置。特征向量
现在正塌缩到两个不同的局部最大值,这代表了两个簇的中心。
为了正确使用该方法,需要考虑一些因素。
唯一需要的参数,带宽 h,需要巧妙地调整以达到良好的结果。实际上,h 的值太低可能会导致大量簇的出现,而 h 的值太大可能会合并多个不同的簇。注意,如果特征向量维度数 d 很大,均值漂移方法可能会导致结果不佳。这是因为在一个非常高维的空间中,局部最大值的数量相应很大,迭代方程很容易过早收敛。
层次方法
层次聚类方法,也称为基于连接性的聚类,通过基于距离度量的相似性标准收集元素来形成簇:接近的元素聚集在同一个分区中,而远离的元素被分离到不同的簇中。这类算法分为两种类型:划分聚类和聚类聚类。划分方法首先将整个数据集分配到一个簇,然后将其划分为两个不太相似(距离较远)的簇。每个分区进一步划分,直到每个数据点本身就是一个簇。聚类方法,这是最常用的方法,从数据点开始,每个数据点代表一个簇。然后通过相似性将这些簇合并,直到只剩下一个包含所有数据点的单一簇。这些方法被称为层次聚类,因为这两个类别都是通过迭代创建簇的层次结构,如图所示。这种层次表示称为树状图。在水平轴上,有数据集的元素,在垂直轴上绘制距离值。每条水平线代表一个簇,垂直轴指示哪个元素/簇形成了相关的簇:

在前面的图中,层次聚类从许多簇(数据点)开始,最终形成一个包含整个数据集的单一簇。相反,划分方法从单一簇开始,直到每个簇都包含一个单独的数据点时结束。
通过应用标准来停止聚类/划分策略,最终形成簇。距离标准设定了两个簇合并的最大距离,超过这个距离的两个簇被认为太远而不能合并;簇数量标准设定了簇的最大数量,以防止层次结构继续合并或分割分区。
以下算法给出了聚类的示例:
-
将数据集
中的每个元素 i 分配到不同的簇。 -
计算每对簇之间的距离,并将最近的这对簇合并成一个单一簇,从而减少簇的总数 1 。
-
计算新簇与其他簇的距离。
-
重复步骤 2 和 3,直到只剩下一个包含所有 N 个元素的单一簇。
由于两个簇 C1 和 C2 之间的距离 d(C1,C2) 是根据定义在两个点
之间计算的,并且每个簇包含多个点,因此需要一项标准来决定在计算距离时必须考虑哪些元素(链接标准)。两个簇 C1 和 C2 的常见链接标准如下:
-
单链接:任何元素属于C1与任何元素属于C2之间的距离的最小值如下所示:
![层次方法]()
-
完全链接:任何元素属于C1与任何元素属于C2之间的距离的最大值如下所示:
![层次方法]()
-
无权成对组平均法(UPGMA)或平均链接:任何元素属于C1与任何元素属于C2之间的距离的平均值如下所示:
,其中
分别是C1和C2的元素数量。 -
Ward 算法:这种算法合并不会增加某种异质性度量的分区。它的目标是连接两个聚类C1和C2,这两个聚类由于组合而具有最小增加的变异度量,称为合并成本
。在这种情况下,距离被替换为合并成本,其公式如下所示:![层次方法]()
这里,
分别是 C1 和 C2 的元素数量。
有不同的度量d(c1,c2)可以选择来实现层次算法。最常见的是欧几里得距离:

注意,这类方法在时间效率上并不特别高,因此不适合聚类大型数据集。它对错误聚类的数据点(异常值)也不够鲁棒,这可能导致聚类合并错误。
聚类方法的训练和比较
为了比较刚刚介绍的聚类方法,我们需要生成一个数据集。我们选择了由两个二维多元正态分布给出的两个数据集类别,其均值和协方差分别等于
和
。
数据点使用 NumPy 库生成,并用 matplotlib 进行绘图:
from matplotlib import pyplot as plt
import numpy as np
np.random.seed(4711) # for repeatability
c1 = np.random.multivariate_normal([10, 0], [[3, 1], [1, 4]], size=[100,])
l1 = np.zeros(100)
l2 = np.ones(100)
c2 = np.random.multivariate_normal([0, 10], [[3, 1], [1, 4]], size=[100,])
#add noise:
np.random.seed(1) # for repeatability
noise1x = np.random.normal(0,2,100)
noise1y = np.random.normal(0,8,100)
noise2 = np.random.normal(0,8,100)
c1[:,0] += noise1x
c1[:,1] += noise1y
c2[:,1] += noise2
fig = plt.figure(figsize=(20,15))
ax = fig.add_subplot(111)
ax.set_xlabel('x',fontsize=30)
ax.set_ylabel('y',fontsize=30)
fig.suptitle('classes',fontsize=30)
labels = np.concatenate((l1,l2),)
X = np.concatenate((c1, c2),)
pp1= ax.scatter(c1[:,0], c1[:,1],cmap='prism',s=50,color='r')
pp2= ax.scatter(c2[:,0], c2[:,1],cmap='prism',s=50,color='g')
ax.legend((pp1,pp2),('class 1', 'class2'),fontsize=35)
fig.savefig('classes.png')
为了使示例更真实,已向两个类别添加了正态分布的噪声。结果如下所示:

带噪声的两个多元正态类别
聚类方法使用sklearn和scipy库实现,并再次用 matplotlib 进行绘图:
import numpy as np
from sklearn import mixture
from scipy.cluster.hierarchy import linkage
from scipy.cluster.hierarchy import fcluster
from sklearn.cluster import KMeans
from sklearn.cluster import MeanShift
from matplotlib import pyplot as plt
fig.clf()#reset plt
fig, ((axis1, axis2), (axis3, axis4)) = plt.subplots(2, 2, sharex='col', sharey='row')
#k-means
kmeans = KMeans(n_clusters=2)
kmeans.fit(X)
pred_kmeans = kmeans.labels_
plt.scatter(X[:,0], X[:,1], c=kmeans.labels_, cmap='prism') # plot points with cluster dependent colors
axis1.scatter(X[:,0], X[:,1], c=kmeans.labels_, cmap='prism')
axis1.set_ylabel('y',fontsize=40)
axis1.set_title('k-means',fontsize=20)
#mean-shift
ms = MeanShift(bandwidth=7)
ms.fit(X)
pred_ms = ms.labels_
axis2.scatter(X[:,0], X[:,1], c=pred_ms, cmap='prism')
axis2.set_title('mean-shift',fontsize=20)
#gaussian mixture
g = mixture.GMM(n_components=2)
g.fit(X)
pred_gmm = g.predict(X)
axis3.scatter(X[:,0], X[:,1], c=pred_gmm, cmap='prism')
axis3.set_xlabel('x',fontsize=40)
axis3.set_ylabel('y',fontsize=40)
axis3.set_title('gaussian mixture',fontsize=20)
#hierarchical
# generate the linkage matrix
Z = linkage(X, 'ward')
max_d = 110
pred_h = fcluster(Z, max_d, criterion='distance')
axis4.scatter(X[:,0], X[:,1], c=pred_h, cmap='prism')
axis4.set_xlabel('x',fontsize=40)
axis4.set_title('hierarchical ward',fontsize=20)
fig.set_size_inches(18.5,10.5)
fig.savefig('comp_clustering.png', dpi=100)
k-means 函数和高斯混合模型具有指定的聚类数(n_clusters =2,n_components=2),而 mean-shift 算法具有带宽值 bandwidth=7。层次算法使用 ward 连接和最大(ward)距离 max_d 实现,将 max_d 设置为 110 以停止层次结构。使用 fcluster 函数获取每个数据点的预测类别。k-means 和 mean-shift 方法的预测类别通过 labels_ 属性访问,而高斯混合模型需要使用 predict 函数。k-means、mean-shift 和高斯混合方法使用 fit 函数进行训练,而层次方法使用 linkage 函数进行训练。前述代码的输出显示在以下图中:

使用 k-means、mean-shift、高斯混合模型和层次 ward 方法对两个多元类别进行聚类
mean-shift 和层次方法显示了两个类别,因此参数(带宽和最大距离)的选择是合适的。注意,层次方法的最大距离值是根据以下代码生成的树状图(以下图)选择的:
from scipy.cluster.hierarchy import dendrogram
fig = plt.figure(figsize=(20,15))
plt.title('Hierarchical Clustering Dendrogram',fontsize=30)
plt.xlabel('data point index (or cluster index)',fontsize=30)
plt.ylabel('distance (ward)',fontsize=30)
dendrogram(
Z,
truncate_mode='lastp', # show only the last p merged clusters
p=12,
leaf_rotation=90.,
leaf_font_size=12.,
show_contracted=True,
)
fig.savefig('dendrogram.png')
truncate_mode='lastp' 标志允许我们指定在图中显示的最后合并的数量(在这种情况下,p=12)。前述图清楚地显示,当距离在 100 和 135 之间时,只剩下两个聚类:

IHierarchical clustering dendrogram for the last 12 merges
在前述图的水平轴上,显示在最后 12 合并之前属于每个聚类的数据点数量,用括号()表示。
除了高斯混合模型之外,其他三种算法错误地将一些数据点分类,尤其是 k-means 和层次方法。这一结果证明,正如预期的那样,高斯混合模型是最鲁棒的方法,因为数据集来自相同的分布假设。为了评估聚类的质量,scikit-learn 提供了量化分区正确性的方法:v-measure、完整性和同质性。这些方法需要每个数据点的真实类别值,因此它们被称为外部验证过程。这是因为它们需要应用聚类方法时未使用的额外信息。同质性,h,是一个介于0和1之间的分数,用于衡量每个簇是否只包含单个类别的元素。完整性,c,用一个介于0和1之间的分数量化,是否所有类别的元素都被分配到同一个簇中。考虑一个将每个数据点分配到不同簇的聚类。这样,每个簇将只包含一个类别的元素,同质性为1,但除非每个类别只包含一个元素,否则完整性非常低,因为类别元素分布在许多簇中。反之,如果一个聚类导致将多个类别的所有数据点分配到同一个簇,那么完整性一定是1,但同质性较差。这两个分数具有类似的公式,如下所示:

这里:
-
是给定簇分配的类别 *C^l * 的条件熵 -
是给定类别成员资格的簇的条件熵 ![聚类方法的训练和比较]()
-
H(C[l] ) 是类别的熵:
![聚类方法的训练和比较]()
-
H(C) 是簇的熵:
![聚类方法的训练和比较]()
-
N[pc] 是簇 c 中类别 p 的元素数量,N[p] 是类别 p 的元素数量,N[c] 是簇 c 的元素数量
v-measure 是同质性和完整性的调和平均值:

这些度量需要真实标签来评估聚类的质量,而通常这并不是真实场景。另一种方法仅使用聚类本身的数据,称为 轮廓,它计算每个数据点与其所属簇的成员和其他簇的成员的相似性。如果平均每个点与其所属簇的点的相似性高于其他点,则簇被合理地定义,分数接近 1(否则接近 -1)。对于公式,考虑每个点 i 和以下量:
-
d[s] (i) 是点 i 到同一簇中点的平均距离
-
d[rest] (i) 是点 i 到所有其他簇中其余点的最小距离
轮廓可以定义为
,而轮廓分数是所有数据点的 s(i) 的平均值。
我们所讨论的四种聚类算法与以下四个度量值相关联,这些值是使用 sklearn(scikit-learn)计算得出的:
from sklearn.metrics import homogeneity_completeness_v_measure
from sklearn.metrics import silhouette_score
res = homogeneity_completeness_v_measure(labels,pred_kmeans)
print 'kmeans measures, homogeneity:',res[0],' completeness:',res[1],' v-measure:',res[2],' silhouette score:',silhouette_score(X,pred_kmeans)
res = homogeneity_completeness_v_measure(labels,pred_ms)
print 'mean-shift measures, homogeneity:',res[0],' completeness:',res[1],' v-measure:',res[2],' silhouette score:',silhouette_score(X,pred_ms)
res = homogeneity_completeness_v_measure(labels,pred_gmm)
print 'gaussian mixture model measures, homogeneity:',res[0],' completeness:',res[1],' v-measure:',res[2],' silhouette score:',silhouette_score(X,pred_gmm)
res = homogeneity_completeness_v_measure(labels,pred_h)
print 'hierarchical (ward) measures, homogeneity:',res[0],' completeness:',res[1],' v-measure:',res[2],' silhouette score:',silhouette_score(X,pred_h)
The preceding code produces the following output:
kmeans measures, homogeneity: 0.25910415428 completeness: 0.259403626429 v-measure: 0.259253803872 silhouette score: 0.409469791511
mean-shift measures, homogeneity: 0.657373750073 completeness: 0.662158204648 v-measure: 0.65975730345 silhouette score: 0.40117810244
gaussian mixture model measures, homogeneity: 0.959531296098 completeness: 0.959600517797 v-measure: 0.959565905699 silhouette score: 0.380255218681
hierarchical (ward) measures, homogeneity: 0.302367273976 completeness: 0.359334499592 v-measure: 0.32839867574 silhouette score: 0.356446705251
如前图分析所示,高斯混合模型在同质性、完整性和 v-measure 测量值方面具有最佳值(接近 1);均值漂移具有合理的值(约为 0.5);而 k-means 和层次方法导致较差的值(约为 0.3)。相反,轮廓分数对所有方法来说都相当不错(介于 0.35 和 0.41 之间),这意味着簇被合理地定义。
维度降低
维度降低,也称为特征提取,是指将由大量维度给出的大数据空间转换到较少维度的子空间的操作。结果子空间应仅包含初始数据的最相关信息,执行此操作的技术分为线性和非线性。维度降低是一类广泛的技术,它有助于从大量数据集中提取最相关信息,降低其复杂性,同时保留相关信息。
最著名的算法,主成分分析(PCA),是将原始数据线性映射到无相关维度的子空间,以下将对其进行讨论。本段中显示的代码可在作者的 GitHub 书籍文件夹的 IPython 笔记本和脚本版本中找到:github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_2/。
主成分分析 (PCA)
主成分分析算法旨在识别数据集相关信息的子空间。实际上,由于数据点在某些数据维度上可能存在相关性,PCA 将找到数据变化的少数不相关维度。例如,汽车轨迹可以用一系列变量来描述,如速度(km/h 或 m/s)、经纬度位置、从选定点的米数位置以及从选定点的英里位置。显然,维度可以减少,因为速度变量和位置变量提供了相同的信息(相关变量),因此相关子空间可以由两个不相关维度组成(一个速度变量和一个位置变量)。PCA 不仅找到不相关的一组变量,还找到方差最大的维度。也就是说,在 km/h 和英里/小时的速度之间,算法将选择具有最高方差的变量,这可以通过函数 速度[km/h]=3.6速度[m/s]* 之间的线简单地表示(通常更接近 km/h 轴,因为 1 km/h = 3.6 m/s ,速度投影在 km/h 轴上比在 m/s 轴上更分散):

m/s 和 km/h 之间的线性函数
前面的图表示了 m/s 和 km/h 之间的线性函数。沿 km/h 轴的点投影具有较大的方差,而沿 m/s 轴的投影具有较低的方差。线性函数 速度[km/h]=3.6速度[m/s]* 沿着轴的方差大于两个轴。
现在我们准备详细讨论该方法及其特性。可以证明,找到方差最大的不相关维度等同于计算以下步骤。像往常一样,我们考虑特征向量
:
-
数据集的平均值:
![主成分分析 (PCA)]()
-
均值平移后的数据集:
![主成分分析 (PCA)]()
-
调整比例后的数据集,其中每个特征向量分量
已除以标准差
,其中 ![主成分分析 (PCA)]()
-
样本协方差矩阵:
![主成分分析 (PCA)]()
-
k 个最大的特征值
及其相关的特征向量 ![主成分分析 (PCA)]()
-
投影到 k 个特征向量子空间上的特征向量
,其中
是具有 N 行和 k 列的特征向量矩阵
最终的特征向量(主成分),
位于子空间R^k上,仍然保留了原始向量的最大方差(和信息)。
注意,这种技术在处理高维数据集时特别有用,例如在人脸识别中。在这个领域,必须将输入图像与图像数据库中的其他图像进行比较,以找到正确的人。PCA 的应用被称为主成分面,它利用了这样一个事实:每张图像中的大量像素(变量)是相关的。例如,背景像素都是相关的(相同的),因此可以应用降维,并在较小的子空间中比较图像是一种更快的方法,可以给出准确的结果。Eigenfaces 的实现示例可以在作者的 GitHub 个人资料github.com/ai2010/eigenfaces上找到。
PCA 示例
作为 PCA 的使用示例以及第一章中讨论的 NumPy 库的示例,使用 Python 的实用机器学习介绍,我们将确定沿直线y=2x分布的二维数据集的主成分,该数据集具有随机(正态分布)噪声。数据集和相应的图(见下图)已使用以下代码生成:
import numpy as np
from matplotlib import pyplot as plt
#line y = 2*x
x = np.arange(1,101,1).astype(float)
y = 2*np.arange(1,101,1).astype(float)
#add noise
noise = np.random.normal(0, 10, 100)
y += noise
fig = plt.figure(figsize=(10,10))
#plot
plt.plot(x,y,'ro')
plt.axis([0,102, -20,220])
plt.quiver(60, 100,10-0, 20-0, scale_units='xy', scale=1)
plt.arrow(60, 100,10-0, 20-0,head_width=2.5, head_length=2.5, fc='k', ec='k')
plt.text(70, 110, r'$v¹$', fontsize=20)
#save
ax = fig.add_subplot(111)
ax.axis([0,102, -20,220])
ax.set_xlabel('x',fontsize=40)
ax.set_ylabel('y',fontsize=40)
fig.suptitle('2 dimensional dataset',fontsize=40)
fig.savefig('pca_data.png')
下图显示了结果数据集。很明显,存在一个数据分布的方向,它对应于我们将从数据中提取的主成分
。

一个二维数据集。主成分方向 v1 由箭头指示。
算法计算二维数据集和均值偏移数据集的平均值,然后使用相应的标准差进行缩放:
mean_x = np.mean(x)
mean_y = np.mean(y)
u_x = (x- mean_x)/np.std(x)
u_y = (y-mean_y)/np.std(y)
sigma = np.cov([u_x,u_y])
为了提取主成分,我们必须计算特征值和特征向量,并选择与最大特征值相关的特征向量:
eig_vals, eig_vecs = np.linalg.eig(sigma)
eig_pairs = [(np.abs(eig_vals[i]), eig_vecs[:,i])
for i in range(len(eig_vals))]
eig_pairs.sort()
eig_pairs.reverse()
v1 = eig_pairs[0][1]
print v1
array([ 0.70710678, 0.70710678]
为了检查主成分是否如预期地沿着直线分布,我们需要重新缩放其坐标:
x_v1 = v1[0]*np.std(x)+mean_x
y_v1 = v1[1]*np.std(y)+mean_y
print 'slope:',(y_v1-1)/(x_v1-1)
slope: 2.03082418796
结果的斜率大约为2,这与开始时选择的价值一致。scikit-learn库提供了一个可能的无需应用任何缩放或均值偏移即可使用的 PCA 算法实现。要使用sklearn模块,我们需要将缩放后的数据转换成一个矩阵结构,其中每一行是一个具有x,y坐标的数据点:
X = np.array([u_x,u_y])
X = X.T
print X.shape
(100,2)
现在可以启动 PCA 模块,指定我们想要的 主成分数量(在这种情况下为1):
from sklearn.decomposition import PCA
pca = PCA(n_components=1)
pca.fit(X)
v1_sklearn = pca.components_[0]
print v1_sklearn
[ 0.70710678 0.70710678]
主成分与使用逐步方法获得的结果完全相同,[ 0.70710678 0.70710678],因此斜率也将相同。现在可以使用两种方法将数据集转换成新的一个维空间:
#transform in reduced space
X_red_sklearn = pca.fit_transform(X)
W = np.array(v1.reshape(2,1))
X_red = W.T.dot(X.T)
#check the reduced matrices are equal
assert X_red.T.all() == X_red_sklearn.all(), 'problem with the pca algorithm'
断言异常没有被抛出,所以结果显示两种方法之间完全一致。
奇异值分解
此方法基于一个定理,该定理表明一个 d x N 矩阵 X 可以分解如下:

这里:
-
U 是一个 d x d 的单位矩阵
-
∑ 是一个 d x N 的对角矩阵,其中对角线上的元素 s i 被称为奇异值
-
V 是一个 N x N 的单位矩阵
在我们的案例中,X 可以由特征向量
组成,其中每个
是一列。我们可以通过近似奇异值分解来减少每个特征向量 d 的维度。在实践中,我们只考虑最大的奇异值
,因此:

t 代表新的降维空间,其中特征向量被投影。向量 x^((i)) 在新空间中使用以下公式进行转换:

这意味着矩阵
(不是
)代表了 t 维空间中的特征向量。
注意,可以证明这种方法与 PCA 非常相似;事实上,scikit-learn 库使用 SVD 来实现 PCA。
摘要
在本章中,详细讨论了主要的聚类算法。我们实现了它们(使用 scikit-learn)并比较了结果。此外,还介绍了最相关的降维技术——主成分分析,并对其进行了实现。现在,你应该有了使用 Python 及其库在真实场景中应用主要无监督学习技术的知识。
在下一章中,将讨论监督学习算法,包括分类和回归问题。
第三章:监督机器学习
在本章中,我们将讨论最相关的回归和分类技术。所有这些算法都有相同的背景过程,通常算法的名称既指分类方法也指回归方法。以下几节将讨论线性回归算法、朴素贝叶斯、决策树和支持向量机。为了理解如何应用这些技术,我们将使用提到的方法解决一个分类问题和回归问题。本质上,我们将使用标记的训练数据集来训练模型,这意味着找到参数的值,正如我们在引言中所讨论的。通常,代码可在我的 GitHub 文件夹中找到,网址为github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_3/。
我们将以一个额外的算法结束本章,这个算法可能用于分类,尽管它不是专门为此目的设计的(隐马尔可夫模型)。现在,我们将开始解释在预测与数据集相关的真实标签时,方法中错误的一般原因。
模型误差估计
我们提到,训练好的模型用于预测新数据的标签,预测的质量取决于模型泛化的能力,即对训练数据中未出现的案例的正确预测。这是文献中一个众所周知的问题,与两个概念相关:输出的偏差和方差。
偏差是由于算法中的错误假设而产生的错误。给定一个带有标签 y[t] 的点 x^((t)),如果模型用不同的训练集进行训练,那么模型是有偏差的,预测的标签 y[t] ^(pred) 将始终与 y[t] 不同。方差误差则是指给定点 x^((t)) 的不同、错误预测的标签。为了解释这些概念,可以考虑一个以真实值为中心的圆(真实标签),如下面的图所示。预测标签越接近中心,模型越无偏,方差越低(如下面图中的左上角)。其他三种情况也在此处展示:
偏差和方差低误差的模型将具有预测标签为蓝色点(如图所示)集中在红色中心(真实标签)上。高偏差误差发生在预测远离真实标签时,而高方差出现在预测值范围很广时。

偏差和方差示例。
我们已经看到,标签可以是连续的或离散的,分别对应回归和分类问题。大多数模型都适合解决这两个问题,我们将使用词回归和分类来指代同一个模型。更正式地说,给定一组 N 个数据点和相应的标签
,一个具有一组参数
的真实参数值
的模型将具有 均方误差 ( MSE ),等于:

我们将使用均方误差(MSE)作为衡量本章讨论的方法的指标。现在我们将开始描述广义线性方法。
广义线性模型
广义线性模型是一组试图找到形成标签 y[i] 与特征向量 x^((i)) 之间线性关系的 M 参数
的模型,如下所示:

这里,
是模型的误差。寻找参数的算法试图最小化由成本函数 J 定义的模型的总误差:

使用称为 批量梯度下降 的迭代算法来最小化 J :

在这里,α 被称为学习率,它是收敛速度和收敛精度之间的权衡。一个被称为 随机梯度下降 的替代算法,即
的循环:

θ[j] 对于每个训练示例 i 进行更新,而不是等待整个训练集的总和。最后一个算法接近 J 的最小值,通常比批量梯度下降更快,但最终解可能围绕参数的真实值振荡。以下段落描述了最常用的模型
和相应的成本函数 J 。
线性回归
线性回归是最简单的算法,基于以下模型:

成本函数和更新规则是:

岭回归
岭回归,也称为 Tikhonov 正则化,在成本函数 J 中添加了一个项,使得:

,其中λ是正则化参数。附加项具有所需的功能,以偏好一组参数而不是所有可能的解决方案,惩罚所有不同于0的参数θ[j]。θ[j]的最终集合收缩在0周围,降低了参数的方差,但引入了偏差误差。用上标l表示线性回归的参数,岭回归参数通过以下公式相关:

这清楚地表明,λ值越大,岭参数围绕0的收缩越多。
Lasso 回归
Lasso 回归是一个类似于岭回归的算法,唯一的区别在于正则化项是参数绝对值的总和:


逻辑回归
尽管名称如此,此算法用于(二元)分类问题,因此我们定义标签
。模型被赋予所谓的逻辑函数,表示为:

在这种情况下,代价函数定义为以下:

从这个公式中,更新规则形式上与线性回归相同(但模型定义
不同):

注意,对于点p的预测
是一个介于0和1之间的连续值。因此,通常,为了估计类标签,我们在
=0.5 处设置一个阈值,使得:

逻辑回归算法可以使用一对一或一对多的技术应用于多标签问题。使用第一种方法,通过训练K个逻辑回归模型来解决一个有K个类的问题,每个模型假设考虑的类j的标签为+1,其余所有标签为0。第二种方法包括为每对标签训练一个模型 (
训练的模型)。
广义线性模型的概率解释
现在我们已经看到了广义线性模型,让我们找到满足以下关系的参数θ[j]:

在线性回归的情况下,我们可以假设
服从均值为0和方差σ²的正态分布,其概率
等价于:

因此,系统的总似然可以表示如下:

在逻辑回归算法的情况下,我们假设逻辑函数本身是概率:


然后,似然可以表示为:

在这两种情况下,可以证明最大化似然等价于最小化成本函数,因此梯度下降将是相同的。
k-最近邻(KNN)
这是一个非常简单的分类(或回归)方法,其中给定一组具有相应标签y[i] 的特征向量
,将测试点x^((t)) 分配给在K个最近邻
中找到的标签值,使用以下距离度量:
-
欧几里得:
![k-最近邻(KNN)]()
-
曼哈顿:
![k-最近邻(KNN)]()
-
闵可夫斯基:
(如果q=2,这会降低到欧几里得距离)
在回归的情况下,值y[t]是通过将大多数出现替换为标签
的平均值来计算的。最简单的平均(或大多数出现)具有均匀权重,因此每个点的重要性相同,无论它们与 x * ^((t)) 的实际距离如何。然而,可以使用权重等于x^((t)) *的逆距离的加权平均。
朴素贝叶斯
朴素贝叶斯是一种基于贝叶斯概率定理和特征条件独立性假设的分类算法。给定一组m个特征
,以及一组标签(类别)
,标签c(也给定特征集*x[i] *)的概率由贝叶斯定理表示:

这里:
-
被称为似然分布 -
是后验分布 -
是先验分布 -
被称为证据
与特征集
相关的预测类别将是值p,使得概率最大化:

然而,该方程无法计算。因此,需要做出一个假设。
使用条件概率规则
,我们可以将前面公式的分子写为以下形式:



我们现在使用这样的假设:每个特征 x[i] 在给定 c 的条件下是条件独立的(例如,为了计算给定 c 的 x[1] 的概率,标签 c 的知识使得其他特征 x[0] 的知识变得冗余,
):

在这个假设下,具有标签 c 的概率等于:
––––––––(1)
在这里,分子中的 +1 和分母中的 M 是常数,有助于避免 0/0 的情况(拉普拉斯平滑)。
由于(1)的分母不依赖于标签(它是所有可能标签的总和),最终的预测标签 p 通过找到(1)的分子中的最大值来获得:
––––––––(2)
给定通常的训练集
,其中
(M 个特征)对应于标签集合
,P(y=c) 的概率简单地按频率计算,即与类别 c 相关的训练示例数除以示例总数,
。相反,条件概率
通过遵循分布来评估。我们将讨论两个模型,多项式朴素贝叶斯和高斯朴素贝叶斯。
多项式朴素贝叶斯
假设我们想要确定一个由一组单词出现
给出的电子邮件 s 是否是垃圾邮件 (1) 或不是 (0),以便
。M 是词汇表的大小(特征的数量)。有
个单词和 N 个训练示例(电子邮件)。
每个带有标签 y[i] 的电子邮件 x^((i)),满足
,是词汇表中单词 j 在训练示例 l 中出现的次数。例如,
表示单词 1 或 w[1] 在第三封电子邮件中出现的次数。在这种情况下,对似然应用多项式分布:

在这里,前面的归一化常数可以被忽略,因为它们不依赖于标签 y ,因此 arg max 运算符不会受到影响。重要的是对单个单词 w[j] 的评估:在训练集中的概率:

在这里,N[iy] 表示单词 j 发生的次数,它与标签 y 相关,而 N[y] 是训练集中带有标签 y 的部分。
这是对
、
在方程(1)和多项式分布似然中的类似。由于概率上的指数,通常应用对数来计算最终的算法(2):

高斯朴素贝叶斯
如果特征向量 x^((i)) 具有连续值,则可以应用此方法。例如,我们想要将图像分类为 K 个类别,每个特征 j 是一个像素,而 x[j] ^((i)) 是训练集中第 i 张图像的第 j 个像素,训练集包含 N 张图像和标签
。给定一个由像素
表示的无标签图像,在这种情况下,方程(1)中的
变为:

在这里:

并且:

决策树
这类算法旨在通过生成从特征值学习的一系列简单规则来分割数据集,以预测未知标签。例如,考虑一个根据湿度、风速、温度和压力值决定今天是否带伞的案例。这是一个分类问题,以下图所示的决策树示例基于 100 天的数据。以下是一个样本表格:
| 湿度(%) | 压力(mbar) | 风速(Km/h) | 温度(C) | 伞 |
|---|---|---|---|---|
| 56 | 1,021 | 5 | 21 | 是 |
| 65 | 1,018 | 3 | 18 | 否 |
| 80 | 1,020 | 10 | 17 | 否 |
| 81 | 1,015 | 11 | 20 | 是 |

基于过去 100 天记录预测是否带伞的决策树。
在前面的图中,方框中的数字代表需要带伞的天数,而圆圈中的数字表示不需要带伞的天数。
决策树呈现两种类型的节点:决策节点,当应用决策分割时有两个(或更多)分支;以及叶节点,当数据被分类。停止标准通常是最大决策节点数(树的深度)或继续分割所需的最小数据点数(通常在 2 到 5 之间)。决策树学习的目标是构建所有可能节点组合中的 最佳 树,即估计要应用规则的层次结构(换句话说,第一个决策节点应该放在湿度上还是温度上,等等)。更正式地说,给定一个训练集
,其中 *x^((i)) * 在 R^m 中,并对应标签 y[i] ,我们需要找到分割节点 k 的最佳规则。如果选择的特征 j 是连续的,每个分割规则由一个特征 [ j ] 和一个阈值 t^j [k] 组成,该阈值将 S 分割为
和
为
和
,
。节点 k 的最佳分割规则
与测量规则如何将数据分离成具有不同标签的分区(即每个分支将包含最小量的标签混合)的 I 杂乱度函数的最小值相关联:


在这里,
分别表示左右分支上的数据点数量。N[k] 是节点 k 上的数据点数量,而 H 是一个可以采用每个目标值 l 在分支 b(b 可以是左分支或右分支)的概率的不同表达式来假设的度量,
:
-
分支的熵:
![决策树]()
-
分支的基尼不纯度:
![决策树]()
-
误分类:
![决策树]()
- 均方误差(方差):
(其中
)
- 均方误差(方差):
注意,后者通常用于回归问题,而其他则用于分类。还应注意,在文献中,通常将 信息增益 定义为节点 k 的 H 与 
其中 
如果特征 j 是离散的,具有 d 个可能值,则没有二元阈值 t^j [k] 来计算,数据被分割成 d 个分区。度量 H 在 d 个子集中计算。
例如,我们可以使用熵作为不纯度度量 H ,确定前一个示例中第一个节点( k=0 )的规则。
所有特征都是连续的,因此需要 t^j [0] 的值。假设 j=0 是湿度,并按升序排序,我们拥有的数据集中可能的湿度值如下:
| h | 0 | 1 | …. | 98 | 99 |
|---|---|---|---|---|---|
| 伞 | 是 | 否 | …. | 否 | 否 |
| 湿度 | 58 | 62 | …. | 88 | 89 |
| < | >= | < | >= | < | |
| 是 | 0 | 11 | 14 | 32 | 7 |
| 否 | 0 | 89 | 21 | 33 | 13 |
![]() |
0.5 | 0.99 | 0.85 | 0.76 | 0.76 |
因此,湿度特征的阈值值为
= 58;同样地,我们可以计算出温度 *t¹ [0] * ,风速 *t² [0] * 和压力 *t³ [0] * 的阈值值。现在我们可以记录下来,确定第一个节点的最佳规则,计算每个四个特征的纯度:
| 是 | 伞 | 是/否 | 伞 |
|---|---|---|---|
| 否 | |||
| 湿度 j=0 | ![]() |
0 | 0 |
![]() |
11 | 89 | ![]() |
纯度:![]() |
纯度:![]() |
||
| 是 | 伞 | 是/否 | 伞 |
| 否 | |||
| 风速 j=2 | ![]() |
48 | 5 |
![]() |
1 | 46 | ![]() |
纯度 :![]() |
纯度:![]() |
因此,对于节点 0 ,最佳规则如下:

即,具有阈值 t² [0] 的风速特征。我们可以重复相同的程序,找到以下决策节点的最佳规则,直到树的末尾。
决策树学习能够处理大型数据集,尽管它通常不太擅长泛化,尤其是在具有大量特征的情况下( N ≈ M )。在这种情况下,建议设置树的小深度或使用一些降维技术。设置分割的最小数据点数或叶节点中的最小数据点数也有助于防止过拟合。此算法可能导致过复杂的树;它们可以被 剪枝 以减少不影响预测质量的分支。有各种剪枝技术,但它们超出了本书的范围。请注意,还可以同时训练一系列决策树,组成所谓的 随机森林 。随机森林使用原始数据点的随机样本训练每个树,并且每个决策节点学习都有可用的随机特征子集。结果是回归问题中的预测平均值或分类问题中的多数值。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
支持向量机
此算法,支持向量机(SVM),试图在几何上将数据集
分为两个标记为y[i] =+1和y[i] =-1的子集。下一图显示了数据被完美地分为两类(空心圆和黑色圆),即决策边界(或超平面)由黑色线完全分隔两个类的数据(换句话说,没有误分类的数据点):

数据集分为两类(空心和实心圆)的草图,由黑色线(决策边界)分隔
超平面由方程
在数学上描述,其中
是超平面到原点的距离,w是超平面的法线。算法的目标是最大化决策边界与数据点之间的距离。在实践中,我们考虑最接近超平面的点i,称为支持向量,它们位于距离决策边界d[1],d[2]的两个平面H[1],H[2]上,使得:
对于H[1],使得y[i] =+1 ––––––––(1)
对于H[2],使得y[i] =-1 ––––––––(2)
假设d[1] =d[2],共同距离称为边缘,因此支持向量机方法找到w和b的值,以最大化边缘。
由于H[1]和H[2]之间的距离由
给出,因此边缘等于
,支持向量机算法等价于:
使得
,
这里,已经添加了平方运算和因子
,以便使用二次规划方法解决数学问题。现在,问题可以用拉格朗日乘数 a * [i] >0*重写为拉格朗日形式:

将关于
和b的导数设置为0,我们得到:
––––––––(3)
––––––––(4)
因此,优化的拉格朗日函数变为:

这里,
。
这被称为原始问题的对偶形式,它只依赖于 a * i*的最大化:

使用称为二次规划的技术找到解
(当 a * [i] =0 时返回零向量),这些解代表通过公式 (3) 的支持向量 w:
––––––––(5).
一个 s 满足方程(方程 (1) 和 (2) 的组合):

将方程式 (3) 代入,并将两边乘以 y[s](其值为 +1 或 -1),我们得到:

对所有支持向量 N[s] 进行平均,我们可以得到参数 b 的更好估计:
––––––––(6)
方程 (5) 和 (6) 返回定义支持向量机算法的参数值,从而可以预测所有测试点 t 的类别:


如果一条线无法完全将数据点分为两类,我们需要允许数据点被错误分类,错误率为
:


我们需要最大化边缘,同时尽量减少误分类错误。这个条件被转化为这个方程:
使得 
在这里,参数 C 被设置为平衡边缘大小与误分类错误(C=0 简单地没有误分类和最大边缘,C>>1 许多误分类点和一个狭窄的边缘)。应用之前的方法,将双问题提交给拉格朗日乘数条件,并设置上限 C:

到目前为止,我们只考虑了只有两个类别的题目。现实问题可能有多个类别,通常使用两种程序来应用这种方法(如逻辑回归所示):一对多或一对一。给定一个有 M 个类别的题目,第一种方法训练 M 个支持向量机模型,每个模型假设考虑的类别标签为 j +1,其余所有类别为 -1。第二种方法则针对每一对类别 i,j 训练一个模型,导致
个训练模型。显然,第二种方法在计算上更昂贵,但结果通常更精确。
以类似的方式,支持向量机(SVM)可以用于回归问题,即当 y[i] 在 -1 和 1 之间连续时。在这种情况下,目标是找到参数 w 和 b,使得:

我们假设真实值 t[i] 可以与最大
的预测值 y[i] 不同,并且根据 y[i] 是否大于或小于 t[i],预测可以进一步错误分类为
。以下图例显示了对于示例点 i,围绕真实值 t[i] 的各种预测 y[i],以及相关的误差:

预测值 y[i] 围绕真实值 [ti] 附近
最小化问题变为:

如此:

可以证明,相关的对偶问题现在等于:
受限于
。
这里,
是拉格朗日乘子。
新的预测值 y[p] 可以通过应用公式
找到,其中参数 b 可以像以前一样获得——在由支持向量关联的子集 S 上取平均值,该子集由
和
给出:

核技巧
存在一些数据集在某个空间中是不可线性分离的,但如果在正确的空间中变换,那么超平面可以将数据分离成所需的两个或更多类别。考虑以下图例中所示示例:

在二维空间中,左图所示的数据集是不可分离的。将数据集映射到三维空间中,两个类别是可分离的。
我们可以清楚地看到,在二维空间中(左图),这两个类别是不可线性分离的。假设我们随后在数据上应用核函数 K,使得:

现在数据可以通过一个二维平面分离(右图)。SVM 算法上的核函数应用于矩阵 H[ij],替换变量 i,j 上的点积:

在 SVM 算法中常用的核函数:
-
线性核:
![核技巧]()
-
径向基核(RBF):
![核技巧]()
-
多项式核:
![核技巧]()
-
Sigmoid 核:
![核技巧]()
方法比较
我们现在可以测试本章讨论的解决回归问题和分类问题的方法。为了避免过拟合,通常将数据集分为两个集合:训练集,其中模型参数被拟合;测试集,其中评估模型的准确率。然而,可能需要使用第三个集合,即验证集,在其中可以优化超参数(例如,SVM 中的 C 和
或岭回归中的 α)。原始数据集可能太小,无法分为三个集合,而且结果可能会受到训练、验证和测试集中特定数据点选择的影响。解决这个问题的常见方法是通过所谓的交叉验证程序来评估模型——数据集被分为 k 个子集(称为折),模型训练如下:
-
使用 k-1 个折作为训练数据来训练一个模型。
-
结果模型在剩余的数据部分进行测试。
-
这个过程会重复进行,重复次数与最初决定的折数相同,每次使用不同的 k-1 折来训练(因此测试折也不同)。最终准确率是通过在不同 k 次迭代中获得的准确率的平均值来得到的。
回归问题
我们使用存储在 archive.ics.uci.edu/ml/datasets/Housing 和作者仓库 (github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_3/ ) 的波士顿郊区的住房数据集,其中本段中使用的代码也可用。该数据集有 13 个特征:
-
CRIM : 城镇人均犯罪率
-
ZN : 住宅用地中划定为超过 25,000 平方英尺地块的比例
-
INDUS : 每个城镇非零售商业地块的比例
-
CHAS : 查尔斯河虚拟变量( = 1 如果地块边界是河流; 0 否则)
-
NOX : 氮氧化物浓度(每千万分之一)
-
RM : 每套住宅的平均房间数
-
AGE : 1940 年之前建造的业主自住单元的比例
-
DIS : 从五个波士顿就业中心计算出的加权距离
-
RAD : 到辐射高速公路的可达性指数
-
TAX : 每 10,000 美元的完整价值财产税率
-
PTRATIO : 城镇内的师生比例
-
B : 1000(Bk - 0.63)² ,其中 Bk 是城镇中黑人比例
-
LSTAT : 人口中低阶层比例以及我们想要预测的标签是 MEDV,它代表房屋价值(以 1000 美元为单位)
为了评估模型的质量,计算了介绍中定义的均方误差和确定系数,R²。R² 的计算公式如下:

这里,y[i] ^(pred) 表示模型预测的标签。
最佳结果是 R² =1,这意味着模型完美地拟合了数据,而 R² =0 则与一条恒定线模型相关(负值表示拟合越来越差)。使用 sklearn 库计算线性回归、岭回归、Lasso 回归和 SVM 回归的代码如下(IPython 笔记本位于 github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_3/):


使用 pandas 库加载数据集,并通过应用函数 df.iloc[np.random.permutation(len(df))] 打乱交叉验证的子集数据(使用了 10 个折),以随机化交叉验证折。此脚本的输出如下:

使用随机森林(包含 50 棵树)获得最佳模型拟合;它返回平均确定系数为 0.86 和 MSE=11.5。正如预期的那样,决策树回归器的 R² 值低于随机森林(分别为 0.67 和 25),而具有 rbf 内核的支持向量机(C=1,
)是最差的模型,具有巨大的 MSE 错误 83.9 和 R² 的 0.0。具有线性内核的支持向量机(C=1,
)返回了一个相当不错的模型(0.69 R² 和 25.8 MSE)。Lasso 和岭回归器具有可比的结果,大约 0.7 R² 和 24 MSE。提高模型结果的一个重要程序是特征选择。通常情况下,只有总特征的一部分与模型训练相关,而其他特征可能根本不会对模型 R² 贡献。特征选择可以提高 R²,因为误导性数据被忽略,并且训练时间减少(要考虑的特征更少)。
对于某个模型提取最佳特征有许多技术,但在这个上下文中,我们探索所谓的递归特征消除方法(RSE),它本质上考虑与最大绝对权重相关的属性,直到选择到所需数量的特征。在 SVM 算法中,权重就是 w 的值,而对于回归,它们是模型参数 θ。使用 sklearn 内置函数 RFE 指定仅最佳四个属性(best_features):

输出如下:

RFE 函数返回一个布尔值列表(support_ 属性),以指示哪些特征被选中(true 值)以及哪些没有被选中(false 值)。然后使用选中的特征来评估模型,就像我们之前做的那样。
即使只使用四个特征,最佳模型仍然是包含 50 棵树的随机森林,其 R² 值仅略低于使用完整特征集训练的模型( 0.82 对比 0.86)。其他模型——lasso、ridge、决策树和线性 SVM 回归器——的 R² 值下降更显著,但结果仍然与相应的完整训练模型可比较。请注意,KNN 算法不提供特征权重,因此不能应用RFE方法。
分类问题
为了测试本章中学习的分类器,我们使用基于六个特征(购买价格、维护成本、车门数量、可搭载人数、行李箱大小和安全)的汽车评估质量数据集(不准确、准确、良好和非常好)。该数据集可以在archive.ics.uci.edu/ml/datasets/Car+Evaluation或在我的 GitHub 账户上找到,包括此处讨论的代码(github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_3/)。为了评估分类的准确性,我们将使用精确度、召回率和 F 度量。给定只有两个类别(正面和负面)的数据集,我们定义真实正面点数( tp )为正确标记为正面的点数,错误正面( fp )为错误标记为正面的点数(负面点),错误负面( fn )为错误分配到负面类别的点数。使用这些定义,精确度、召回率和 F 度量可以计算如下:



在一个分类问题中,对于给定类别 C 的完美精确度( 1.0 )意味着每个分配给类别 C 的点都属于类别 C(没有关于类别 C 中错误标记的点数量的信息),而召回率等于 1.0 则意味着来自类别 C 的每个点都被标记为属于类别 C(但关于错误分配给类别 C 的其他点的信息没有),
注意,在多类别的情形下,这些指标通常被计算为标签数量的多次,每次考虑一个类别为正面,其他所有类别为负面。然后使用多类别的指标的不同平均值来估计总的精确度、召回率和 F 度量。
对汽车数据集进行分类的代码如下。首先,我们将所有库和数据加载到一个 pandas 数据框中。

以下是一些分类的特征值:
buying 0 v-high, high, med, low
maintenance 1 v-high, high, med, low
doors 2 2, 3, 4, 5-more
persons 3 2, 4, more
lug_boot 4 small, med, big
safety 5 low, med, high
car evaluation 6 unacc,acc,good,vgood
这些被映射成用于分类算法的数字:

由于我们需要计算并保存所有方法的度量值,我们编写了一个标准函数CalcMeasures,并将标签向量Y从特征X中分离出来:

使用了10次交叉验证折数,代码如下:

度量值的存储在数据框中:

每个度量值已被评估四次——根据索引映射填充数组的汽车评估类别的数量:
'acc': 0, 'unacc': 2, 'good': 1, 'vgood': 3
最佳模型是具有 rbf 核的 SVM(C=50),但随机森林(50 棵树)和决策树也返回了优秀的结果(所有四个类别的度量值均超过0.9)。朴素贝叶斯、逻辑回归以及具有线性核的 SVM(C=50)返回了较差的模型,尤其是在准确、好和非常好的类别上,因为这些标签的点很少:

在百分比上,非常好(v-good)和好的比例分别为 3.993%和 3.762%,与 70.0223%的不准确和 22.222%的准确相比。因此,我们可以得出结论,这些算法不适用于预测在数据集中很少出现的类别。
隐藏马尔可夫模型
虽然这种方法不能严格地被认为是一种监督学习算法,但它也可以用来执行与分类非常相似的任务,因此我们决定将其包括在内。为了介绍这个主题,我们将提供一个例子。考虑一个简单的例子,通过观察销售员的目光(眼神接触、低头或看向一边,每个观察值O[i]分别对应0、1和2)来预测你面前的销售员是在说谎还是不是(两个状态
)。想象一下销售员目光的观察序列 O=O * [0] * , O * [1] * , O * [2] * , O * [3] * , O * [4] * ,…是0, 1, 0, 2,…我们想要推断连续时间t、t+1(或在这个例子中,两个连续句子)之间的状态转移矩阵A:

A矩阵的任何一项,a[ij],表示在时间t处于状态S[j]的情况下,在时间t+1处于状态S[i]的概率。因此,0.3(a[01])是在时间t处于说谎状态的情况下,在时间t+1不处于说谎状态的概率,0.6 (a[10] )是相反的情况,0.7(a[00] )表示在时间t和时间t+10.4(a[11] )处于说谎状态的概率,在时间t真诚之后,在时间t+1不处于说谎状态的概率。以类似的方式,可以定义与销售员的三个可能行为相关的矩阵B:

任何条目 b[j(k)] 是在时间 t 给定状态 S[j] 的情况下观察 k 的概率。例如,0.7 ( b[00] ),0.1 ( b[01] ),和 0.2 ( b[02] ) 分别是商人根据行为观察(眼神接触、低头、看向一边)说谎的概率。这些关系在以下图中描述:

商人行为 - 两种状态的隐马尔可夫模型
商人的初始状态分布也可以定义为:
(他在时间 0 的第一句话中更倾向于说谎而不是说实话)。注意,所有这些矩阵
都是行随机矩阵;也就是说,行之和为 1 ,并且没有直接的时间依赖性。一个 隐马尔可夫模型 ( HMM ) 由三个矩阵的组合给出,这些矩阵描述了已知观察序列 *O=O[0] , O[1] ,…O[T-1] * 与相应的隐藏状态序列 *S=S[0] , S[1] ,… S[T-1] * 之间的关系。一般来说,该算法使用的标准符号总结如下:
-
T 是观察序列 O=O[0] , O[1] ,… O[T-1] 和隐藏状态序列 S=S[0] , S[1] ,… S[T-1] 的长度
-
N 是模型中可能(隐藏)状态的数量
-
M 是可能观察值的数量:
![隐马尔可夫模型]()
-
A 是状态转移矩阵
-
B 是观察概率矩阵
-
π 是初始状态分布
在前面的例子中,M=3 , N=2 ,我们想象预测商人说话过程中(这些是隐藏状态)的意图序列 S=S[0] , S[1] ,… S[T-1] ,观察他的行为值 O=O[0] , O[1] ,… O[T-1] 。这是通过计算每个状态序列 S 的概率来实现的:

例如,固定 T=4 ,S=0101 ,和 O=1012 :

同样,我们可以计算所有其他隐藏状态组合的概率,并找到最可能的序列 S 。寻找最可能序列 S 的有效算法是 维特比算法 ,它包括计算从 0 到 t 直到 T-1 的所有部分序列的最大概率。在实践中,我们计算以下量:
-
![隐马尔可夫模型]()
-
对于 t=1,…,T-1 和 i=0,…,N-1 ,在从不同状态 j 来的可能路径中,在时间 t 处处于状态 i 的最大概率是
。与
的最大值相关的部分序列是直到时间 t 的最可能部分序列。 -
最终最可能的序列与时间 T-1 的概率最大值相关:
![隐马尔可夫模型]()
例如,给定前面的模型,长度为 T=2 的最可能序列可以计算如下:
-
P(10)=0.0024
-
P(00)=0.0294
- 因此 d 1 (0)=P(00)=0.0294
-
P(01)=0.076
-
P(11)=0.01
- 因此 d [1] (1)=P(01)=0.076
最终最可能的序列是 00(两个连续的假句子)。
另一种思考最可能序列的方法是最大化正确状态的数量;也就是说,考虑每个时间 t 的状态 i ,其概率
最大。使用称为后向算法的算法,可以证明给定状态 i 的概率
为:

这里:
-
![隐马尔可夫模型]()
-
和
在时间 t ,HMM 处于状态 i 之前的部分观察序列的概率,![隐马尔可夫模型]()
-
和
在时间 t 给定状态为 i 的情况下,从时间 t 到 T-1 的部分序列的概率:![隐马尔可夫模型]()
-
在时间 t 之前和之后保持状态 i 的概率组合导致
的值。
注意,计算最可能序列的两种方法不一定返回相同的结果。
逆向问题——给定序列 O=O[0] ,O[1] ,…O[T-1] * 和参数值 N ,M* ,找到最优的 HMM
——也可以通过迭代使用 Baum-Welch 算法 解决。定义在时间 t 出现状态 i 的概率和转到状态 j 的概率为:
其中
对于
和
。
然后 Baum-Welch 算法如下:
-
初始化
![隐马尔可夫模型]()
-
计算
和 ![隐马尔可夫模型]()
-
重新计算模型矩阵如下:
其中
和
是克罗内克符号,当
时等于 1,否则为 0。 -
迭代直到
收敛。
在下一节中,我们将展示一段 Python 代码,实现这些方程以测试 HMM 算法。
Python 示例
如同往常,这里讨论的 hmm_example.py 文件可在 github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_3/ 找到。
我们首先定义一个类,在其中我们传递模型矩阵:
class HMM:
def __init__(self):
self.pi = pi
self.A = A
self.B = B
维特比算法和最大化正确状态数在以下两个函数中实现:
def ViterbiSequence(self,observations):
deltas = [{}]
seq = {}
N = self.A.shape[0]
states = [i for i in range(N)]
T = len(observations)
#initialization
for s in states:
deltas[0][s] = self.pi[s]*self.B[s,observations[0]]
seq[s] = [s]
#compute Viterbi
for t in range(1,T):
deltas.append({})
newseq = {}
for s in states:
(delta,state) = max((deltas[t-1][s0]*self.A[s0,s]*self.B[s,observations[t]],s0) for s0 in states)
deltas[t][s] = delta
newseq[s] = seq[state] + [s]
seq = newseq
(delta,state) = max((deltas[T-1][s],s) for s in states)
return delta,' sequence: ', seq[state]
def maxProbSequence(self,observations):
N = self.A.shape[0]
states = [i for i in range(N)]
T = len(observations)
M = self.B.shape[1]
# alpha_t(i) = P(O_1 O_2 ... O_t, q_t = S_i | hmm)
# Initialize alpha
alpha = np.zeros((N,T))
c = np.zeros(T) #scale factors
alpha[:,0] = pi.T * self.B[:,observations[0]]
c[0] = 1.0/np.sum(alpha[:,0])
alpha[:,0] = c[0] * alpha[:,0]
# Update alpha for each observation step
for t in range(1,T):
alpha[:,t] = np.dot(alpha[:,t-1].T, self.A).T * self.B[:,observations[t]]
c[t] = 1.0/np.sum(alpha[:,t])
alpha[:,t] = c[t] * alpha[:,t]
# beta_t(i) = P(O_t+1 O_t+2 ... O_T | q_t = S_i , hmm)
# Initialize beta
beta = np.zeros((N,T))
beta[:,T-1] = 1
beta[:,T-1] = c[T-1] * beta[:,T-1]
# Update beta backwards froT end of sequence
for t in range(len(observations)-1,0,-1):
beta[:,t-1] = np.dot(self.A, (self.B[:,observations[t]] * beta[:,t]))
beta[:,t-1] = c[t-1] * beta[:,t-1]
norm = np.sum(alpha[:,T-1])
seq = ''
for t in range(T):
g,state = max(((beta[i,t]*alpha[i,t])/norm,i) for i in states)
seq +=str(state)
return seq
由于概率的乘积会导致下溢问题,所有的 Α [t] (i) 和 Β [t] (i) 都已经乘以一个常数,使得对于
:
-
![Python 示例]()
-
其中 ![Python 示例]()
现在,我们可以使用推销员意图示例中的矩阵初始化 HMM 模型,并使用前面提到的两个函数:
pi = np.array([0.6, 0.4])
A = np.array([[0.7, 0.3],
[0.6, 0.4]])
B = np.array([[0.7, 0.1, 0.2],
[0.1, 0.6, 0.3]])
hmmguess = HMM(pi,A,B)
print 'Viterbi sequence:',hmmguess.ViterbiSequence(np.array([0,1,0,2]))
print 'max prob sequence:',hmmguess.maxProbSequence(np.array([0,1,0,2]))
结果是:
Viterbi: (0.0044, 'sequence: ', [0, 1, 0, 0])
Max prob sequence: 0100
在这个特定的情况下,两种方法返回相同的序列,你可以通过改变初始矩阵轻松验证,算法可能会导致不同的结果。我们得到的行为序列;眼神接触,低头,眼神接触,向旁看,很可能与推销员状态序列;说谎,不说谎,说谎,说谎,概率为 0.0044 有关。
也可能实现 Baum-Welch 算法,在给定的观测序列以及参数 N 和 M 的情况下找到最优的 HMM。以下是代码:
def train(self,observations,criterion):
N = self.A.shape[0]
T = len(observations)
M = self.B.shape[1]
A = self.A
B = self.B
pi = copy(self.pi)
convergence = False
while not convergence:
# alpha_t(i) = P(O_1 O_2 ... O_t, q_t = S_i | hmm)
# Initialize alpha
alpha = np.zeros((N,T))
c = np.zeros(T) #scale factors
alpha[:,0] = pi.T * self.B[:,observations[0]]
c[0] = 1.0/np.sum(alpha[:,0])
alpha[:,0] = c[0] * alpha[:,0]
# Update alpha for each observation step
for t in range(1,T):
alpha[:,t] = np.dot(alpha[:,t-1].T, self.A).T * self.B[:,observations[t]]
c[t] = 1.0/np.sum(alpha[:,t])
alpha[:,t] = c[t] * alpha[:,t]
#P(O=O_0,O_1,...,O_T-1 | hmm)
P_O = np.sum(alpha[:,T-1])
# beta_t(i) = P(O_t+1 O_t+2 ... O_T | q_t = S_i , hmm)
# Initialize beta
beta = np.zeros((N,T))
beta[:,T-1] = 1
beta[:,T-1] = c[T-1] * beta[:,T-1]
# Update beta backwards froT end of sequence
for t in range(len(observations)-1,0,-1):
beta[:,t-1] = np.dot(self.A, (self.B[:,observations[t]] * beta[:,t]))
beta[:,t-1] = c[t-1] * beta[:,t-1]
gi = np.zeros((N,N,T-1));
for t in range(T-1):
for i in range(N):
gamma_num = alpha[i,t] * self.A[i,:] * self.B[:,observations[t+1]].T * \
beta[:,t+1].T
gi[i,:,t] = gamma_num / P_O
# gamma_t(i) = P(q_t = S_i | O, hmm)
gamma = np.squeeze(np.sum(gi,axis=1))
# Need final gamma element for new B
prod = (alpha[:,T-1] * beta[:,T-1]).reshape((-1,1))
gamma_T = prod/P_O
gamma = np.hstack((gamma, gamma_T)) #append one Tore to gamma!!!
newpi = gamma[:,0]
newA = np.sum(gi,2) / np.sum(gamma[:,:-1],axis=1).reshape((-1,1))
newB = copy(B)
sumgamma = np.sum(gamma,axis=1)
for ob_k in range(M):
list_k = observations == ob_k
newB[:,ob_k] = np.sum(gamma[:,list_k],axis=1) / sumgamma
if np.max(abs(pi - newpi)) < criterion and \
np.max(abs(A - newA)) < criterion and \
np.max(abs(B - newB)) < criterion:
convergence = True;
A[:],B[:],pi[:] = newA,newB,newpi
self.A[:] = newA
self.B[:] = newB
self.pi[:] = newpi
self.gamma = gamma
注意,代码使用了模块 copy 中的浅拷贝,它创建了一个新的容器,其中包含对原始对象内容(在这种情况下,pi ,B)的引用。也就是说,newpi 是一个与 pi 不同的对象,但 newpi[0] 是 pi[0] 的引用。相反,NumPy 的 squeeze 函数需要从矩阵中删除冗余维度。
使用相同的行为序列 O=0, 1, 0, 2 ,我们得到最优模型如下:
,
,
这意味着状态序列必须从真实的推销员句子开始,并在两个状态 说谎 和 不说谎 之间持续振荡。一个真实的推销员句子(不说谎)当然与眼神接触值有关,而说谎则与低头和向旁看的行为有关。
在本节关于隐马尔可夫模型(HMM)的简单介绍中,我们假设每个观测值都是一个标量值,但在实际应用中,每个 O[i] 通常是一个特征向量。通常,这种方法被用作分类训练,许多 HMM l [i] 作为预测的类别,然后在测试时选择具有最高
的类别。继续这个例子,我们可以想象构建一个 真实机器 来测试我们遇到的每个销售人员。想象一下,对于我们的说话者每个句子(观测值) O[i] ,我们可以提取三个特征:三种可能的值 e[i] (眼神接触、低头和看向一边),声音音调 v[i] 有三种可能的值(太大声、太小声和平直),以及手部动作 h[i] 有两种可能的值(握手和冷静) O i=(e[i] , v[i] , h[i] ) 。在训练时间,我们让我们的朋友说谎,并使用这些观测值来训练一个使用 Baum-Welch 算法的 HMM l 0。我们重复训练过程,但使用真实句子来训练 l 1。在测试时间,我们记录销售人员的句子 O 并计算两个:
,
。类别预测将是概率最高的那个。
注意,HMM 已经在各个领域得到应用,但它在语音识别任务、手写字符识别和动作识别中的应用表现相当出色。
摘要
在本章中,我们讨论了主要的分类和回归算法,以及实现它们的技巧。你现在应该能够理解在什么情况下可以使用每种方法,以及如何使用 Python 及其库(sklearn 和 pandas)来实现它。
在下一章中,我们将介绍从网络数据中学习最相关的技术(网络数据挖掘)。
第四章:网络挖掘技术
网络数据挖掘技术用于探索在线可用的数据,然后从互联网中提取相关信息。在网络上搜索是一个复杂的过程,需要不同的算法,这些算法将是本章的主要内容。给定一个搜索查询,使用每个网页上的数据获取相关页面,这些数据通常分为页面内容和指向其他页面的页面超链接。通常,搜索引擎具有多个组件:
-
一个用于收集网页的网络爬虫或蜘蛛
-
一个解析器,用于提取内容和预处理网页
-
一个索引器,用于在数据结构中组织网页
-
一个检索信息系统,对与查询相关的最重要文档进行评分
-
一个排名算法,以有意义的方式对网页进行排序
这些部分可以分为网络结构挖掘技术和网络内容挖掘技术。
网络爬虫、索引器和排名过程指的是网络结构(超链接的网络)。搜索引擎的其他部分(解析器和检索系统)是网络内容分析方法,因为网页上的文本信息用于执行这些操作。
此外,可以使用一些自然语言处理技术进一步分析一组网页的内容,例如潜在狄利克雷分配意见挖掘或情感分析工具。这些技术在提取关于网络用户的主观信息方面尤为重要,因此它们在许多商业应用中广泛存在,从市场营销到咨询服务。这些情感分析技术将在本章末进行讨论。现在我们将开始讨论网络结构挖掘类别。
网络结构挖掘
这个网络挖掘领域专注于发现网页之间的关系以及如何使用这种链接结构来找到网页的相关性。对于第一个任务,通常使用蜘蛛,并将链接和收集到的网页存储在索引器中。对于最后一个任务,网页排名评估网页的重要性。
网络爬虫(或蜘蛛)
蜘蛛从一组 URL(种子页面)开始,然后从中提取 URL 以获取更多页面。然后从新页面中提取新链接,这个过程会一直持续到满足某些条件。未访问的 URL 存储在一个称为frontier的列表中,根据如何使用这个列表,我们可以有不同的爬虫算法,例如广度优先和优先级爬虫。在广度优先算法中,下一个要爬取的 URL 来自 frontier 的头部,而新 URL 被追加到 frontier 的尾部。优先级爬虫则使用对未访问 URL 列表的某种重要性估计来确定先爬取哪个页面。请注意,从页面中提取链接的操作是通过解析器完成的,这个操作在网页内容挖掘部分的相应段落中进行了更详细的讨论。
网络爬虫本质上是一种图搜索算法,它检索起始页面的邻域结构,遵循某些标准,如最大链接数(图的深度)、最大爬取页面数或时间限制。然后,蜘蛛可以提取具有有趣结构的 Web 部分,例如枢纽和权威。枢纽是一个包含大量链接的网页,而权威被定义为,一个在其 URL 在其它网页上出现次数较多的页面(它是页面流行度的一个度量)。一个流行的 Python 爬虫实现是由 Scrapy 库提供的,它还采用了并发方法(使用 Twisted 的异步编程)来加速操作。在第七章,电影推荐系统 Web 应用中给出了这个模块的教程,当爬虫将用于提取电影评论信息时。
索引器
索引器是一种将爬虫找到的网页存储在结构化数据库中的方法,以便在给定搜索查询时能够快速检索。最简单的索引方法是将所有页面直接存储,在查询时,只需扫描包含查询中关键词的所有文档。然而,如果页面数量很大(在实践中确实如此),由于计算成本高,这种方法是不可行的。最常见的方法是使用称为倒排索引方案的方法来加速检索,这是大多数流行搜索引擎所使用的。
给定一组网页 p[1] , …, p[k] 和一个包含页面中所有单词的词汇表 V,通过存储如下列表
,…,
,…,
,获得倒排索引数据库。
在这里,
是网页 j 的 ID。可以为每个单词存储额外信息,例如单词的频率计数或它在每个页面上的位置。索引器的实现超出了本书的范围,但为了完整性,本段描述了这些一般概念。
因此,一个包含单词列表的搜索查询将检索与每个单词相关的所有倒排列表,然后合并这些列表。最终列表的顺序将使用排名算法以及信息检索系统来衡量文档与查询的相关性来选择。
排名 – PageRank 算法
排名算法很重要,因为单个信息检索查询可以返回的网页数量可能非常大,因此存在如何选择最相关网页的问题。此外,信息检索模型很容易被通过在页面上插入许多关键词来垃圾邮件化,使得页面与大量查询相关。因此,考虑到网络具有一个超链接——从一个页面指向另一个页面的链接——是估计网页相关性的主要信息来源的事实,已经解决了评估网页在互联网上的重要性(即排名分数)的问题。超链接可以分为:
-
页面 i 的入链:指向页面 i 的超链接
-
页面 i 的出链:从页面 i 指向其他页面的超链接
直观地,一个网页拥有的入链越多,该网页应该越重要。对这种超链接结构的研究是社会网络分析的一部分,已经使用和提出了许多算法。但出于历史原因,我们将解释最著名的算法,称为 PageRank,由 Sergey Brin 和 Larry Page(Google 的创始人)在 1998 年提出。整个想法是将页面的声望计算为指向它的页面的声望之和。如果页面 j 的声望为 P(j),它将平均分配给它指向的所有页面 N[j],使得每个出链获得与 P(j)|N[j] 相等的声望部分。正式地,页面 i 的声望或页面排名分数可以定义为:

在这里,
如果页面 j 指向页面 i;否则它等于 0。A[ij] 被称为邻接矩阵,它表示从节点 j 到节点 i 传播的声望部分。考虑到图中总共有 N 个节点,前面的方程可以重写为矩阵形式:

注意,如果邻接矩阵满足某些条件,此方程等价于具有特征值
的特征系统。另一种解释前述方程的方法是使用马尔可夫链术语——项 A[ij] 变成从节点 j 到节点 i 的转移概率,节点 i 的声望 p(i) 是访问节点 i 的概率。在这种情况下,可能发生两个节点(或更多)相互指向但不对其他页面进行指向。一旦访问了这两个节点之一,就会发生循环,用户将被困在其中。这种情况被称为排名陷阱,(矩阵 A 被称为周期性),解决方案是添加一个转移矩阵项,允许在不遵循由 A 描述的马尔可夫链的情况下随机从一个页面跳转到另一个页面:

在这里,E=ee^T 是一个 N×N 维度的矩阵的一个条目(e 是一个单位向量),d(也称为阻尼因子)是遵循由转移矩阵 A 给出的转移的概率。(1-d) 是随机访问页面的概率。在这种最终形式中,所有节点都相互连接,即使对于特定节点 s 的邻接矩阵有多个 0 条目,A[sj],从图中的所有 N 个节点访问 s 的概率始终有一个等于
的小概率。注意,A 必须是随机的,这意味着每一行都必须求和为 1;
(至少每行有一个不同于 0 的条目或至少每页有一个出链)。可以通过将 P 向量标准化为 e^T P=N 来简化方程:

这可以通过幂迭代法来解决。此算法将在第八章 情感分析器应用,电影评论情感分析器应用中用于实现一个电影评论情感分析系统的示例。此算法的主要优点是它不依赖于查询(因此,可以在查询时离线计算 PageRank 分数并检索),并且它对垃圾邮件非常稳健,因为垃圾邮件发送者不可能在影响力较大的页面上插入指向他们页面的入链。
网络内容挖掘
这种类型的挖掘侧重于从网页内容中提取信息。每个页面通常会被收集和组织(使用解析技术),处理以从文本中去除不重要的部分(自然语言处理),然后使用信息检索系统进行分析,以将相关文档与给定的查询相匹配。以下段落将讨论这三个组件。
解析
一个网页是用 HTML 格式编写的,因此第一步是提取相关信息。HTML 解析器从标签中构建一个树形结构,从而可以从中提取内容。如今,有许多解析器可供选择,但作为一个例子,我们使用 Scrapy 库,见第七章,电影推荐系统 Web 应用,它提供了一个命令行解析器。假设我们想要解析维基百科的主页,en.wikipedia.org/wiki/Main_Page 。我们只需在终端中输入以下内容:
scrapy shell 'https://en.wikipedia.org/wiki/Main_Page'
将准备好使用response对象和xpath语言来解析页面。例如,我们想要获取页面的标题:
In [1]: response.xpath('//title/text()').extract()
Out[1]: [u'Wikipedia, the free encyclopedia']
或者,我们想要提取页面中所有嵌入的链接(这项操作对于爬虫的工作是必需的),这些链接通常放在<a>标签上,URL 值位于href属性中:
In [2]: response.xpath("//a/@href").extract()
Out[2]:
[u'#mw-head',
u'#p-search',
u'/wiki/Wikipedia',
u'/wiki/Free_content',
u'/wiki/Encyclopedia',
u'/wiki/Wikipedia:Introduction',
…
u'//wikimediafoundation.org/',
u'//www.mediawiki.org/']
注意,可以使用更健壮的方式来解析内容,因为网页通常是由非程序员编写的,所以 HTML 可能包含语法错误,浏览器通常会修复这些错误。还要注意的是,由于广告等原因,网页可能包含大量数据,这使得解析相关信息变得复杂。已经提出了不同的算法(例如,树匹配)来识别页面的主要内容,但目前没有 Python 库可用,所以我们决定不再进一步讨论这个话题。然而,请注意,在 newspaper 库中可以找到一个很好的解析实现,用于提取网络文章的主体,它也将在第七章,电影推荐系统 Web 应用中使用。
自然语言处理
一旦提取了网页的文本内容,通常会对文本数据进行预处理,以去除不包含任何相关信息的部分。文本被标记化,即转换成一个单词列表(标记),并且所有标点符号都被移除。另一个常见的操作是移除所有停用词,即那些用于构建句子语法但不包含文本信息的单词(如连词、冠词和介词)等,例如a、about、an、are、as、at、be、by、for、from、how、in、is、of、on、or、that、the、these、this、to、was、what、when、where、who、will、with,以及许多其他单词。
英语(或任何语言)中的许多单词具有相同的词根,但有不同的后缀或前缀。例如,单词 think、thinking 和 thinker 都有相同的词根— think 表示它们的意义相同,但在句子中的作用不同(动词、名词等)。将一组中的所有单词还原到其词根的过程称为 词干提取,为此已经发明了许多算法(Porter、Snowball 和 Lancaster)。所有这些技术都是更广泛算法范围的一部分,称为 自然语言处理,并且它们在 Python 的 nltk 库中实现(通常通过 sudo pip install nltk 安装)。例如,以下代码使用之前描述的技术(使用 Python 接口终端)预处理一个示例文本:

注意,stopwords 列表已通过 nltk dowloader nltk.download('stopwords') 下载。
信息检索模型
信息检索方法需要找到与给定查询最相关的文档。网页中的单词可以使用不同的方法进行建模,例如布尔模型、向量空间模型和概率模型,本书中我们决定讨论向量空间模型及其实现方法。形式上,给定一个包含 V 个单词的词汇表,一个包含 N 页面的集合中的每个网页 d[i](或文档)可以被视为一个单词向量,
,其中属于文档 i 的每个单词 j 由 w[ij] 表示,这可以是根据所选算法的数字(权重)或向量:
-
词语频率-逆文档频率(TF-IDF),w[ij],是一个实数
-
潜在语义分析(LSA),w[ij],是一个实数(与文档 i 的表示无关)
-
Doc2Vec(或 word2vec),w[ij],是一个实数向量(与文档 i 的表示无关)
由于查询也可以表示为一个单词向量,
,因此通过计算查询向量与每个文档之间的相似度度量来找到与向量 q 最相似的网页。最常用的相似度度量称为余弦相似度,对于任何给定的文档 i:

注意,文献中还有其他使用的度量方法(okapi 和 pivoted normalization weighting),但为了本书的目的,它们不是必要的。
以下几节将在本节最后一段的文本案例中应用之前将详细介绍这三种方法。
TF-IDF
此方法计算 w[ij] ,考虑到一个在大量页面中多次出现的单词可能不如在文档子集内多次出现但出现次数较少的单词重要。它由两个因素的乘积给出:
其中:
-
是单词 j 在文档 I 中的标准化频率 -
是逆文档频率,*df[j] * 是包含单词 j 的网页数量
潜在语义分析 (LSA)
此算法的名称来源于以下观点:存在一个潜在空间,其中每个单词(以及每个文档)都可以被有效地描述,假设具有相似意义的单词也出现在相似文本位置。在此子空间上的投影是通过已讨论的(截断)奇异值分解(SVD)方法实现的,参见第二章,机器学习技术 – 无监督学习。我们将此方法应用于 LSA 的上下文中如下:网页被收集到矩阵 X (V ´N ) 中,其中每一列是一个文档:

在这里,U[t] ( V ´d ) 是在具有 d 维度的新潜在空间中投影的单词矩阵,
( d ´ N ) 是将文档转换到子空间后的转置矩阵,而
( d ´ d ) 是具有奇异值的对角矩阵。查询向量本身通过以下方式投影到潜在空间中:

现在,每个由 V[t] 的每一行表示的文档可以通过余弦相似度与 q[t] 进行比较。请注意,文档在潜在空间中的真实数学表示由
(而不是 *V[t] * ) 给出,因为奇异值是空间轴组件的缩放因子,必须考虑。因此,这个矩阵应该与
进行比较。尽管如此,它通常计算 *V[t] * 和 *q[t] * 之间的相似度,而在实践中,哪种方法返回最佳结果仍然未知。
Doc2Vec (word2vec)
此方法将每个词 j , w[j] ,表示为一个向量
,但与其出现的文档 d[i] 无关。Doc2Vec 是 Mikolov 等人最初提出的 word2vec 算法的扩展,它使用神经网络和反向传播来生成词(和文档)向量。由于神经网络(尤其是深度学习)在许多机器学习应用中的重要性日益增加,我们决定在此介绍这种相当高级的方法的主要概念和公式,以便为您介绍一个在未来机器学习的各个领域中都将变得极其重要的主题。以下描述基于 Rong(2014)和 Le 和 Mikolov(2014)的论文,并且符号也反映了文献中目前使用的名称。
Word2vec – 连续词袋和 skip-gram 架构
词汇表 V 中的每个词 j 都用一个长度为 |V| 的向量表示,具有二进制条目 x[j] =(x[1j] , …, x[Vj] ) ,其中只有 x[jj] =1 ,否则为 0 。word2vec 方法训练一个(隐藏)层 N 个神经元(权重),在两种不同的网络架构(以下图中所示)之间进行选择。请注意,这两种架构都只有一层 N 个神经元或权重, h 。这意味着该方法必须被视为浅层学习而不是深层学习,后者通常指的是具有许多隐藏层的网络。连续词袋( CBOW )方法(以下图中右侧所示)使用一组 C 个词作为输入,称为上下文,试图预测输入文本旁边出现的词(目标)。相反的方法称为Skip-gram,其中输入是目标词,网络被训练来预测上下文集(以下图中左侧所示)。请注意,C 被称为窗口参数,它设置上下文词选择距离目标词有多远:

word2vec 算法的 Skip-gram(左侧)和 CBOW(右侧)架构;图取自 X Rong(2015)的《word2vec 参数学习解释》
在这两种情况下,矩阵 W 将输入向量转换为隐藏层,而 W' 将从隐藏层转换到输出层 y ,其中评估目标(或上下文)。在训练阶段,计算真实目标(或上下文)的错误,并用于计算随机梯度下降以更新矩阵 W 和 W' 。我们将在下一节中给出 CBOW 方法的更数学描述。请注意,Skip-gram 方程式类似,我们将参考 Rong(2015)的论文以获取更多详细信息。
CBOW 模型的数学描述
从输入层开始,隐藏层h可以通过计算获得,
,其中
是一个长度为N的向量,代表隐藏层上的词w[i]和w[C]是上下文向量
的平均值。选择目标词w[j],输出层的得分u[j]是通过将向量
(W'的 j 列)与h相乘得到的:

这不是输出层y[j]上的最终值,因为我们想评估在给定上下文C的情况下,目标词w[j]的后验条件概率,这可以通过softmax公式来表示:

现在的训练目标是最大化词汇表中所有词的概率,这等价于
,其中
和索引j^M代表W'中乘积最大的向量,即最可能的目标词。
然后通过计算E相对于W (w[ij] )和W' (w'[ij'] )的条目的导数,得到随机梯度下降方程。每个输出目标词w[j]的最终方程是:

其中
和a是梯度下降的学习率。导数
表示网络相对于真实目标词的误差,以便误差可以反向传播到系统中,系统可以迭代学习。注意,向量
是执行语义操作时使用的常用词向量表示。
更多细节可以在 Rong (2015)的论文中找到。
Doc2Vec 扩展
如 Le 和 Mikolov (2014)所述,Doc2Vec 是 word2vec 方法的自然扩展,其中将文档视为一个额外的词向量。因此,在 CBOW 架构的情况下,隐藏层向量h是上下文向量平均值和文档向量d[i]:

该架构如图所示,被称为分布式内存模型(DM),因为文档d[i]向量只记住由上下文词未表示的文档信息。向量
与从文档d[i]中采样的所有上下文词共享,但矩阵W(和W')对所有文档都是相同的:

一个具有三个单词上下文的分布式内存模型示例(window=3);图来自 Le 和 Mikolov 的《Sentences and Documents 的分布式表示》(2014 年)
另一个提出的架构称为分布式词袋(DBOW),它只考虑输入层中的一个文档向量以及从文档中采样的上下文词集合。已经证明 DM 架构的性能优于 DBOW,因此它是gensim库实现中的默认模型。建议读者阅读 Le 和 Mikolov(2014)的论文以获取更多详细信息。
电影评论查询示例
为了展示之前讨论的三种信息检索方法,我们使用了来自 polarity dataset v2.0 和 27886 个未处理的 html 文件集合 的 IMBD 电影评论,这些数据由 Bo Pang 和 Lillian Lee 提供,网址为www.cs.cornell.edu/people/pabo/movie-review-data/。数据集和代码也存储在作者的 GitHub 账户github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_4/中。从网站上下载并解压movie.zip文件(称为polarity_html.zip),这将创建一个包含所有网页电影评论的movie文件夹(约 2000 个文件)。首先,我们需要从文件中准备数据:

这次我们使用BeautifulSoup解析每个 HTML 网页的电影标题并创建一个字典,moviedict。polarity dataset v2.0.tar.gz包含一个名为review_polarity的文件夹,它位于txt_sentoken/文件夹内,该文件夹将正面和负面评论分割成两个单独的子文件夹(优点和缺点)。这些文件使用以下代码进行预处理:

现在所有 2000 条评论都存储在tot_textreviews列表中,相应的标题在tot_titles中。可以使用sklearn训练 TF-IDF 模型:

在PreprocessTfidf函数之后,将所有预处理技术(去除停用词、分词和词干提取)应用于每个文档。同样,我们可以使用gensim库训练 LSA 模型,指定 10 个潜在维度:

注意,GenSimCorpus函数只是使用常规技术预处理文档,并将它们转换为 gensim LSA 实现可以读取的格式。从lsi对象中,可以获取到将查询转换为潜在空间所需的矩阵U、V和 S:

此外,还计算了单词索引字典dict_words,将查询词转换为dict_corpus中的对应索引词。
最后要训练的模型是 Doc2Vec。首先,我们将数据准备成 gensim Doc2Vec 实现可以处理的形式:

每条评论都放置在一个namedtuple对象中,该对象包含由PreprocessDoc2Vec函数预处理过的单词(移除了停用词并执行了分词)以及代表文件名的标签。请注意,我们没有选择应用词干提取器,因为不使用它时结果通常更好(读者可以通过应用词干提取器来测试结果,将布尔标志doc2vecstem设置为True)。Doc2Vec 训练最终通过以下代码完成:

我们设置了 DM 架构(dm =1),具有 500 维度的隐藏层(size),窗口大小为 10 个单词,并且模型考虑了至少出现一次的所有单词(min_count =1)。其他参数与效率优化方法相关(negative用于负采样和hs用于层次 softmax)。训练持续了20个 epoch,学习率等于0.99。
我们现在可以验证每种方法返回的结果,定义一个查询以检索所有与科幻电影相关的网络文档,即通常用以下单词列表描述的电影:

TF-IDF 方法使用以下脚本返回最相似的五个网页:

注意,该模型使用稀疏矩阵格式来存储数据,因此cosine_similarity函数将向量转换为常规向量。然后它计算相似度。以类似的方式,查询在 LSA 术语中被转换为q[k],并打印出最相似的五个网页:

最后,doc2vec模型使用infer_vector函数将查询列表转换为向量,并通过most_similar函数返回最相似的评论:

注意,模型的random参数需要设置为一个固定值,以便在每次使用优化方法(负采样或层次 softmax)时返回确定性的结果。结果如下:
-
TF-IDF :
![电影评论查询示例]()
-
LSA :
![电影评论查询示例]()
-
Doc2vec:
![电影评论查询示例]()
所有三种方法都显示与查询相关的电影。有趣的是,TF-IDF 的性能优于更先进的 LSA 和 Doc2Vec 算法,因为在夜幕下的热、宝可梦、摇滚恐怖电影秀和野性之物与查询不相关,而 TF-IDF 的结果只显示一部电影(无可奉告)作为不相关。电影查理的安吉拉和蝙蝠侠与罗宾是动作电影,所以它们大部分与单个查询词动作相关。Doc2Vec 返回最差的结果,主要是因为训练数据集太小,无法学习良好的向量表示(例如,谷歌发布了一个基于数十亿文档训练的 word2vec 数据集,或者更多)。网站www.cs.cornell.edu/people/pabo/movie-review-data/提供了一个更大的数据集,因此读者可以尝试使用更多数据训练 Doc2Vec 作为练习。
后处理信息
一旦从网络中收集了网页,除了构建网络搜索引擎之外,还有一些自然语言处理算法能够提取不同商业目的的相关信息。我们将在这里讨论能够从文档集合中提取主要主题(潜在狄利克雷分析)和提取每个网页的情感或意见(意见挖掘技术)的算法。
潜在狄利克雷分配
潜在狄利克雷分配(LDA)是一种属于生成模型类别的自然语言处理算法。该技术基于一些变量的观察,这些变量可以通过其他下划线的未观察到的变量来解释,这些变量是观察到的数据相似或不同的原因。
例如,考虑文本文档,其中单词是观察结果。每个文档可以是多个主题(未观察到的变量)混合的结果,每个单词都指代一个特定的主题。
例如,考虑以下两个描述两家公司的文档:
-
文档 1:改变人们搜索时尚商品的方式,通过视觉识别分享和购买时尚,TRUELIFE 将成为搜索终极趋势的最佳方法 …
-
doc2:Cinema4you 使任何场所都能成为电影院,是一家目前处于测试阶段的新的数字电影媒体发行公司。它应用了视频点播和广播中使用的科技 ...
LDA 是一种自动发现这些文档包含的潜在主题的方法。例如,给定这些文档并要求两个主题,LDA 可能会返回与每个主题相关的以下单词:
-
主题 1:人物、视频、媒体…
-
主题 2:电影技术、识别、广播…
因此,第二个主题可以标记为技术,而第一个主题为商业。
文档随后被表示为以一定概率吐出单词的主题混合:
-
doc1 : 主题 1 42%,主题 2 64%
-
doc2 : 主题 1 21%,主题 2 79%
这种文档表示在诸如不同组页面聚类或提取页面集合的主要共同主题等应用中可能很有用。该算法背后的数学模型将在下一段中解释。
模型
文档被表示为潜在主题上的随机混合,其中每个主题由单词上的分布来表征。LDA 假设对于由 M 个文档组成的语料库,d=(d[1] , …, d[M] ) ,每个 i 包含 N[i] 个单词。如果 V 是词汇表长度,文档 i 中的一个单词由长度为 V 的向量 w[i] 来表示,其中只有一个元素 w[i] [v] =1 ,其余都是 0 :

潜在维度(主题)的数量是 K ,对于每份文档,
是与每个单词 w[i] 相关的主题向量,其中 z[i] 是长度为 K 的 0 向量,除了元素 j ,z[i]^j =1 ,它表示主题 w[i] 已经被抽取。
b 表示 K ´ V 矩阵,其中 b[ij] 代表词汇表中的每个单词 j 被从主题 i 抽取的概率:
。
因此,b 的每一行 i 是主题 i 的单词分布,而每一列 j 是单词 j 的主题分布。使用这些定义,过程描述如下:
-
从选定的分布(通常是泊松分布)中抽取每份文档的长度 N[i] 。
-
对于每份文档 d[i] ,抽取主题分布 q [i] ,作为一个狄利克雷分布 Dir(a) ,其中
和 a 是长度为 K 的参数向量,使得
。 -
对于每份文档 d[i] ,对于每个单词 n ,从
的多项式分布中抽取一个主题。 -
对于每份文档 d[i] ,对于每个单词 n ,以及对于每个主题 z[n] ,从由 b 的第 z[n] 行给出的多项式分布中抽取一个单词 w[n] ,
。
算法的目的是最大化每个文档的后验概率:

应用条件概率定义,分子变为以下内容:

因此,文档 i 在主题向量 z 和单词概率矩阵 b 给定下的概率可以表示为单个单词概率的乘积:

考虑到 z[n] 是一个只有一个分量 j 与 0 不同的向量,z^j [n] =1 ,那么
。将这些表达式代入 (2):

(1)中的分母是通过在 q [i]上积分和在z上求和得到的。主题分布q[i]和每个主题的单词分布(b的行)的最终值是通过通过近似推理技术计算这个概率得到的;这些内容超出了本书的范围。
参数a被称为浓度参数,它表示分布如何在可能的值上分散。浓度参数为1(或k,根据主题建模文献中使用的定义,狄利克雷分布的维度)导致所有概率集合具有相等的可能性。同时,当浓度参数趋向于零时,只有几乎全部质量集中在它们的一个组成部分上的分布可能是可能的(单词在不同主题之间共享较少,并且集中在少数几个主题上)。
例如,一个 100,000 维度的分类分布有 100,000 个单词的词汇量,尽管一个主题可能只由几百个单词表示。因此,典型的浓度参数值在 0.01 到 0.001 之间,或者如果词汇量的大小是数百万个单词或更高,则更低。
根据 L. Li 和 Y. Zhang 的论文使用潜在狄利克雷分配进行文本分类的经验研究,LDA 可以作为文本建模的有效降维方法。然而,尽管该方法在各种应用中表现良好,但仍有一些问题需要考虑。模型的初始化是随机的,这意味着它可能导致每次运行的结果不同。此外,浓度参数的选择很重要,但没有标准的方法来选择它们。
示例
再次考虑电影评论网页,textreviews,在电影评论查询示例部分已经预处理过,并应用 LDA 来测试是否可以收集不同主题的评论。像往常一样,以下代码可在postprocessing.ipynb中找到,位于github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_4/:

如往常一样,我们将每个文档转换成了标记(使用了不同的标记化器)并去除了停用词。为了获得更好的结果,我们过滤掉了那些对页面不提供任何信息的最频繁出现的单词(例如movie和film)。我们忽略了所有出现次数超过 1,000 次或观察次数少于三次的单词:

现在我们可以用 10 个主题来训练 LDA 模型(passes是训练过程中通过语料库的遍历次数):

代码返回与每个主题相关联的以下 10 个最可能的单词:

虽然并非所有主题都有一个简单的解释,但我们确实可以看到主题 2 与单词disney、mulan(一部迪士尼电影)、love相关联,而life是一个关于动画电影的主题,主题 6 与单词action、alien、bad相关联,而planet与科幻电影相关。实际上,我们可以查询所有最可能的主题等于 6 的电影,如下所示:

这将返回:
Rock Star (2001)
Star Wars: Episode I - The Phantom Menace (1999)
Zoolander (2001)
Star Wars: Episode I - The Phantom Menace (1999)
Matrix, The (1999)
Volcano (1997)
Return of the Jedi (1983)
Daylight (1996)
Blues Brothers 2000 (1998)
Alien³ (1992)
Fallen (1998)
Planet of the Apes (2001)
大多数这些标题显然是科幻和奇幻电影,所以 LDA 算法正确地将它们聚类在一起。
注意,由于文档在主题空间中的表示(lda_lfq[corpus]),可以应用聚类算法(见第二章,机器学习技术 - 无监督学习),但这留作读者的练习。还要注意,每次运行 LDA 算法时,由于模型的随机初始化,可能会导致不同的结果(也就是说,如果你的结果与本段中显示的不同是正常的)。
观点挖掘(情感分析)
观点挖掘或情感分析是研究文本以提取作者观点的领域,这通常可以是积极的或消极的(或中性的)。这种分析在市场营销中尤其有用,可以找到公众对产品或服务的意见。标准方法是将情感(或极性),即积极或消极,视为分类问题的目标。一个文档数据集将具有与词汇表中不同单词数量一样多的特征,并且通常使用 SVM 和朴素贝叶斯等分类算法。作为一个例子,我们考虑了已经用于测试 LDA 和信息检索模型的 2,000 条电影评论,这些评论已经标记(正面或负面)。本段中讨论的所有代码都可在postprocessing.ipynb IPython 笔记本中找到,网址为github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_4/。与之前一样,我们导入数据并进行预处理:

数据随后被分成一个训练集(80%)和一个测试集(20%),以nltk库可以处理的方式(每个元组或包含文档单词和标签的字典列表):

现在我们可以使用nltk库训练和测试一个NaiveBayesClassifier(多项式)并检查错误:

代码返回了 28.25%的错误率,但通过计算每份文档中的最佳二元组,可以改善结果。二元组被定义为连续的两个单词的组合,X² 测试用于找到不是偶然出现而是频率较高的二元组。这些特定的二元组包含对文本相关的信息,在自然语言处理术语中被称为搭配。例如,给定一个由两个单词组成的二元组,w1 和 w2 ,在我们的语料库中有 N 个可能的二元组总数,在 w1 和 w2 相互独立出现的零假设下,我们可以通过收集二元组(w1,w2)和其余可能的二元组(例如这些)的出现次数来填充一个二维矩阵 O:
| w1 | Not w1 | |
|---|---|---|
| w2 | 10 | 901 |
| Not w2 | 345 | 1,111,111 |
X² 度量由
给出,其中 O[ij] 是由单词 (i, j) 给出的二元组出现的次数(因此 O[00] =10 等等),而 E[ij] 是二元组 (i, j) 的预期频率(例如,
)。直观上,X² 越高,观察到的频率 O[ij] 与预期均值 E[ij] 的差异越大,因此零假设很可能会被拒绝。二元组是一个好的搭配,它包含比遵循预期均值的二元组更多的信息。可以证明,X² 可以计算为 f 检验(也称为 均值平方列联系数)乘以二元组出现的总次数 N,如下所示:

更多关于搭配和 X² 方法的详细信息可以在 C. D. Manning 和 H. Schuetze 所著的 《统计自然语言处理基础》(1999 年)中找到。注意,X² 作为信息增益度量(此处未讨论),可以被视为一种特征选择方法,如第三章 监督机器学习 中定义的那样。使用 nltk 库,我们可以使用 X² 度量来选择每份文档中最佳的 500 个二元组,然后再次训练一个朴素贝叶斯分类器,如下所示:

这次错误率是 20%,比正常方法低。X²测试也可以用来从整个语料库中提取最有信息量的单词。我们可以测量单个单词的频率与正面(或负面)文档频率的差异,以评估其重要性(例如,如果单词great在正面评论中有较高的X²值,但在负面评论中较低,这意味着该单词提供了评论是正面的信息)。可以通过计算语料库中每个单词的整体频率和正负子集的频率来提取语料库中最显著的 10,000 个单词:

现在,我们可以简单地再次训练一个朴素贝叶斯分类器,只使用每个文档的bestwords集中的单词:

错误率是 12.75%,考虑到相对较小的数据集,这是一个非常低的比率。请注意,为了得到更可靠的结果,应该应用交叉验证方法(见第三章,监督机器学习),但这被留作读者的练习。此外,请注意,Doc2Vec 向量(在电影评论查询示例部分计算)可以用来训练分类器。假设 Doc2Vec 向量已经训练并存储在model_d2v.doc2vec对象中,像往常一样,我们将数据分为训练集(80%)和测试集(20%):

然后,我们可以训练一个 SVM 分类器(径向基函数核(RBF)核)或逻辑回归模型:

逻辑回归和 SVM 的准确率非常低,分别为0.5172和0.5225。这主要是因为训练数据集的大小较小,不允许我们训练具有大量参数的算法,例如神经网络。
摘要
在本章中,我们讨论并实现了用于管理网络数据的最常见和高级算法,使用了 Python 的一系列库。现在你应该对网络挖掘领域面临的挑战有清晰的理解,并且应该能够用 Python 处理一些这些问题。在下一章中,我们将讨论在商业环境中至今使用的最重要的推荐系统算法。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
第五章。推荐系统
推荐系统在用户面临大量产品或服务选择,且无法在合理时间内进行评估时,自然地找到了其应用场景。这些引擎是电子商务业务的重要组成部分,因为它们帮助网络上的客户在众多与最终用户无关的候选物品中决定购买或选择的适当物品。典型的例子包括亚马逊、Netflix、eBay 和 Google Play 商店,它们使用收集的历史数据向每个用户推荐他们可能喜欢的商品。在过去 20 年中,已经开发出了不同的技术,我们将重点关注迄今为止工业界使用的重要(和常用)方法,并具体说明每种方法的优缺点。推荐系统被归类为基于内容的过滤(CBF)和协同过滤(CF)技术,以及其他不同的方法(关联规则、对数似然方法和混合方法)将一起讨论,以及评估它们准确性的不同方式。这些方法将在 MovieLens 数据库上进行测试(来自grouplens.org/datasets/movielens/),该数据库包含来自 943 个用户对 1,682 部电影(1 到 5 的评分)的 10 万条评分。每个用户至少有 20 条评分,每部电影都有一个属于其的类别列表。本章中展示的所有代码,如往常一样,可在github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_5的rec_sys_methods.ipynb文件中找到。
我们将首先介绍用于安排推荐系统使用的数据集的主要矩阵以及通常在讨论以下章节中的算法之前使用的度量指标。
效用矩阵
推荐系统中使用的数据分为两类:用户和物品。每个用户喜欢某些物品,评分值r[ij](从 1 到 5)是与每个用户i和物品j相关联的数据,表示用户对物品的欣赏程度。这些评分值收集在矩阵中,称为效用矩阵R,其中每一行i代表用户i的评分物品列表,而每一列j列出所有对物品j进行过评分的用户。在我们的案例中,数据文件夹ml-100k包含一个名为u.data的文件(以及包含电影标题列表的u.item),该文件已被以下脚本转换为 Pandas DataFrame(并保存到csv, utilitymatrix.csv):

前两行的输出如下:

除了第一个(用户 ID)之外,每个列名定义了电影的名称和 MovieLens 数据库中电影的 ID(由分号分隔)。0 值表示缺失值,我们预计会有大量缺失值,因为用户评估的电影远少于 1,600 部。请注意,评分少于 50 次的电影已被从效用矩阵中删除,因此列数为 604(603 部电影被评分超过 50 次)。推荐系统的目标是预测这些值,但对于某些技术要正常工作,我们最初需要设置这些值(插补)。通常,使用两种插补方法:按用户平均评分或按项目平均评分,这两种方法都在以下函数中实现:

此函数将由本章实现的大多数算法调用,因此我们决定在此处讨论它,作为未来使用的参考。此外,在本章中,效用矩阵 R 将具有 N × M 的维度,其中 N 为用户数量,M 为项目数量。由于不同算法反复使用相似度度量,以下我们将定义最常用的定义。
相似度度量
为了计算两个不同向量 x 和 y 之间的相似度 s,这些向量可以是用户(效用矩阵的行)或项目(效用矩阵的列),通常使用两种度量:
-
余弦相似度:
![相似度度量]()
-
皮尔逊相关系数:
,其中 x 和 y 是两个向量的平均值。
注意,如果平均值为 0,则两个度量是一致的。现在我们可以开始讨论不同的算法,从协同过滤类别开始。以下 sim() 函数将用于评估两个向量之间的相似度:

SciPy 库已被用于计算相似度(请注意,余弦相似度的 SciPy 定义与之前定义的相反,因此值从 1 中减去)。
协同过滤方法
这类方法基于这样的想法:任何用户都会喜欢其他类似用户喜欢的项目。简单来说,基本假设是,与用户 B 相似用户 A 很可能将项目评分与 B 相同,而不是其他方式。在实践中,这个概念通过比较不同用户的品味并使用最相似用户的品味来推断给定用户的未来评分(基于记忆)或通过从用户喜欢的项目中提取一些评分模式并尝试遵循这些模式来预测未来评分来实现。所有这些方法都需要大量的数据才能工作,因为给定的用户的推荐依赖于数据中可以找到多少相似用户。这个问题被称为 冷启动,在文献中得到了很好的研究,通常建议使用 CF 和 CBF 之间的某种混合方法来克服这个问题。在我们的 MovieLens 数据库示例中,我们假设我们有足够的数据来避免冷启动问题。CF 算法的常见问题还包括可扩展性,因为计算量随着用户和产品的数量增加(可能需要一些并行化技术),以及由于任何用户通常评分的项目数量较少而导致的效用矩阵稀疏性(插补通常是处理问题的尝试)。
基于记忆的协同过滤
这个子类使用效用矩阵来计算用户之间的相似度或项目之间的相似度。这些方法存在可扩展性和冷启动问题,但当它们应用于大型或过小的效用矩阵时,它们目前在许多商业系统中被广泛使用。我们将在此后讨论基于用户的协同过滤和基于项目的协同过滤。
基于用户的协同过滤
该方法使用 k-NN 方法(见第三章,监督机器学习)来找到过去评分与所选用户评分相似的用户的评分,以便可以将它们的评分组合成一个加权平均值来返回当前用户的缺失评分。
算法如下:
对于任何给定的用户 i 和尚未评分的项目 j:
-
使用相似度指标 s 找到与具有评分 j 的用户最相似的 K 个用户。
-
计算用户 i 尚未评分的每个项目 j 的预测评分,作为用户 K 的评分加权平均值:
![基于用户的协同过滤]()
在这里
是用户 i 和 k 的平均评分,以补偿主观判断(一些用户慷慨,一些用户挑剔)和 s(i , k) 是相似度指标,如前一段所述。注意,我们甚至可以通过每个用户的评分分布进行归一化,以比较更均匀的评分:

在这里,σ[i] 和 σ[k] 分别是用户 i 和 k 的评分标准差。
此算法有一个输入参数,即邻居数量 K,但在大多数应用中通常 20 到 50 之间的值就足够了。皮尔逊相关系数被发现比余弦相似度返回更好的结果,这可能是由于用户评分的减去使得相关公式使得用户更具有可比性。以下代码用于预测每个用户的缺失评分。
u_vec 代表用户评分值,通过 FindKNeighbours 函数找到最相似的其他用户 K。CalcRating 仅使用前面讨论过的公式(没有传播校正)计算预测评分。请注意,如果效用矩阵非常稀疏以至于没有找到邻居,则预测评分是用户的平均评分。可能发生的情况是预测评分超过 5 或低于 1,因此在这种情况下,预测评分分别设置为 5 或 1。

基于物品的协同过滤
这种方法在概念上与基于用户的 CF 相同,只是相似度是在物品上而不是在用户上计算的。由于大多数情况下用户的数量可以远远大于物品的数量,因此这种方法提供了一种更可扩展的推荐系统,因为物品的相似度可以预先计算,并且当新用户到来时不会改变太多(如果用户数量 N 显著很大)。
对于每个用户 i 和物品 j 的算法如下:
-
使用相似度指标 s 来找到 K 个与 i 已经评分的物品最相似的物品。
-
将预测评分计算为 K 个物品评分的加权平均值:
![基于物品的协同过滤]()
注意,相似度指标可能具有负值,因此我们需要将求和限制在只有正相似度上,以便具有意义(即正的)P[ij](如果我们只对推荐最佳物品感兴趣而不是评分,那么物品的相对顺序将始终是正确的)。即使在这种情况,大多数应用中 20 到 50 之间的 K 值通常就足够了。
该算法使用一个类实现,如下所示:

类 CF_itembased 的构造函数计算用于评估用户缺失评分的 simmatrix 物品相似度矩阵,任何需要通过 CalcRatings 函数评估缺失评分时都可以使用。函数 GetKSimItemsperUser 找到与选定用户(由 u_vec 给出)最相似的 K 个用户,而 CalcRating 仅实现前面讨论过的加权平均评分计算。请注意,如果没有找到邻居,则评分设置为平均值或物品的评分。
最简单的基于物品的协同过滤 – 斜率一
而不是使用之前讨论的度量来计算相似度,可以使用一个非常简单但有效的方法。我们可以计算一个矩阵 D,其中每个条目 d[ij] 是项目 i 和 j 评分的平均差异:

在这里,
是一个变量,用于计算用户 k 是否对 i 和 j 两个项目都进行了评分,因此
是对 i 和 j 两个项目都进行了评分的用户数量。
然后算法如 基于物品的协同过滤 部分所述。对于每个用户 i 和项目 j:
-
找到与项目 j 差异最小的 K 个项目,
(*表示可能的索引值,但为了简单起见,我们将它们重新标记为1到 K)。 -
计算预测评分作为加权平均值:
![最简单的基于物品的协同过滤 – 斜率一]()
尽管这个算法比其他协同过滤算法要简单得多,但它通常与它们的准确性相匹配,计算成本更低,且易于实现。其实现方式与用于基于物品的协同过滤的类非常相似:

唯一的区别是矩阵:现在使用 difmatrix 来计算项目 i 和 j 之间的差异 d(i, j),如前所述,而函数 GetKSimItemsperUser 现在寻找最小的 difmatrix 值以确定 K 个最近邻。由于两个项目至少没有被一个用户评分的可能性(尽管可能性不大),difmatrix 可以有未定义的值,默认设置为 1000。请注意,预测评分也可能超过 5 或低于 1,因此在这种情况下,必须适当地将预测评分设置为 5 或 1。
基于模型的协同过滤
这类方法使用效用矩阵生成一个模型来提取用户如何评分项目的模式。模式模型返回预测评分,填充或近似原始矩阵(矩阵分解)。文献中已经研究了各种模型,我们将讨论特定的 矩阵分解 算法——奇异值分解(SVD,也带有期望最大化),交替最小二乘法(ALS),随机梯度下降(SGD),以及一般的非负矩阵分解(NMF)算法类。
替代最小二乘法(ALS)
这是分解矩阵 R 的最简单方法。每个用户和每个项目都可以在 K 维的特征空间中表示,以便:

在这里,P N×K 是特征空间中的新用户矩阵,而 Q M×K 是同一空间中物品的投影。因此,问题被简化为最小化正则化成本函数 J:

在这里,λ 是正则化参数,它通过惩罚学习到的参数并确保向量 p[i] 和 [q] ^T [j] 的大小不是太大,从而有助于避免过拟合。矩阵条目 Mc[ij] 用于检查用户 i 和物品 j 是否确实进行了评分,因此当 r[ij] >0 时,Mc[ij] 为 1,否则为 0。将每个用户向量 p[i] 和物品向量 q[j] 的 J 的导数设为 0,我们得到以下两个方程:


在这里,R[i] 和 Mc[i] 分别指矩阵 R 和 Mc 的第 i 行,而 R[j] 和 Mc[j] 分别指矩阵 Mc 和 R 的第 j 列。交替固定矩阵 P 和 Q,前述方程可以直接使用最小二乘算法解决,以下函数实现了 Python 中的 ALS 算法:

矩阵 Mc 被称为 mask,变量 l 代表正则化参数 λ,默认设置为 0.001,最小二乘问题已使用 Numpy 库的 linalg.solve 函数解决。这种方法通常比 随机梯度下降 ( SGD ) 和 奇异值分解 ( SVD ) (参见以下章节) 都要精确,但它非常容易实现且易于并行化(因此可以快速)。
随机梯度下降 (SGD)
此方法也属于矩阵分解子类,因为它依赖于对效用矩阵 R 的近似:

在这里,矩阵 P(N×K) 和 Q(M×K) 分别代表具有 K 维潜在特征空间中的用户和物品。每个近似的评分
可以表示如下:

找到了矩阵
,解决正则化平方误差 e² [ij] * 的最小化问题,就像 ALS 方法一样(成本函数 J 如 第三章 ,监督机器学习* ):

这个最小化问题使用梯度下降法解决(参见 第三章 ,监督机器学习 ):


在这里,α 是学习率(见第三章,监督机器学习)和
。该技术通过在两个先前方程之间交替寻找 R(固定 q[kj] 并求解 P[ik],反之亦然)直到收敛。SGD 通常比 SVD(见下一节)更容易并行化(因此可能更快),但在找到良好评分方面不太精确。此方法的 Python 实现如下脚本:

这个 SGD 函数具有默认参数,学习率 α = 0.0001,正则化参数 λ = l = 0.001,最大迭代次数 1000,以及收敛容忍度 tol = 0.001。注意,未评分的项目(评分值为 0)在计算中不被考虑,因此在使用此方法时不需要进行初始填充(插补)。
非负矩阵分解 (NMF)
这是一组方法,它们将矩阵 R 的分解再次视为两个矩阵 P( N × K )和 Q( M × K )(其中 K 是特征空间的维度)的乘积,但它们的元素必须是非负的。一般最小化问题如下:

在这里,α 是一个参数,用于定义要使用哪种正则化项(0 为平方正则化,1 为 lasso 正则化,或它们的混合),λ 是正则化参数。已经开发了几种技术来解决此问题,例如投影梯度、坐标下降和非负约束最小二乘。本书的范围不包括讨论这些技术的细节,但我们将使用以下函数中实现的坐标下降方法:

注意,在因子分解实际发生之前可能已经进行了插补,并且函数 fit_transform 返回的是 P 矩阵,而 Q^T 矩阵存储在 nmf.components_ 对象中。默认情况下,α 值被假定为 0(平方正则化),λ = l = 0.01。由于效用矩阵具有正值(评分),这类方法无疑非常适合预测这些值。
奇异值分解 (SVD)
我们已经在第二章中讨论了此算法,作为降维技术,通过分解为矩阵U、Σ、V来近似矩阵(你应该阅读第二章中的相关部分,以获取更多技术细节)。在这种情况下,SVD 被用作矩阵分解技术,但需要一种插补方法来初始估计每个用户的缺失数据;通常,使用每个效用矩阵行(或列)的平均值或两者的组合(而不是保留零值)。除了直接将 SVD 应用于效用矩阵外,还可以使用以下算法,利用期望最大化(见第二章,无监督机器学习),从矩阵
开始:
-
m 步:执行
![奇异值分解(SVD)]()
-
e 步:
![奇异值分解(SVD)]()
此过程重复进行,直到平方误差之和
小于所选容忍度。实现此算法和简单 SVD 分解的代码如下:

注意,奇异值分解(SVD)由sklearn库提供,并且两种插补平均方法(用户评分平均值和项目评分平均值)都已实现,尽管默认函数是无,这意味着零值保留为初始值。对于期望最大化 SVD,其他默认参数是收敛容忍度(0.0001)和最大迭代次数(10,000)。这种方法(尤其是与期望最大化结合使用)比 ALS 慢,但准确性通常更高。另外请注意,SVD 方法通过减去用户评分平均值来分解效用矩阵,因为这种方法通常表现更好(然后将在 SVD 矩阵计算后添加用户评分平均值)。
我们完成备注,SVD 分解也可以用于基于记忆的 CF,在降维空间(矩阵U或V^T)中比较用户或项目,然后从原始效用矩阵中获取评分(使用 k-NN 方法的 SVD)。
CBF 方法
这类方法依赖于描述项目的数据,然后用于提取用户的特征。在我们的 MovieLens 示例中,每部电影j都有一个包含G个二进制字段的一组,以指示它是否属于以下类型之一:未知、动作、冒险、动画、儿童、喜剧、犯罪、纪录片、剧情、奇幻、黑色电影、恐怖、音乐剧、悬疑、浪漫、科幻、惊悚、战争或西部。
基于这些特性(类型),每部电影由一个具有G维度的二进制向量m[j]描述(电影类型的数量),其中包含电影j中包含的所有类型的条目等于1,否则为0。给定存储在效用矩阵部分提到的dfout效用矩阵的dataframe,这些二进制向量m[j]通过以下脚本从 MovieLens 数据库收集到一个 dataframe 中:

电影内容矩阵已保存在movies_content.csv文件中,以便 CBF 方法使用。
内容推荐系统的目标是生成具有相同字段的用户配置文件,以指示用户喜欢每个类型的程度。这种方法的问题在于项目的描述内容并不总是可用,因此在电子商务环境中并不总是可以采用这种技术。优点是针对特定用户的推荐不受其他用户评分的影响,因此不会因为特定项目的用户评分不足而遭受冷启动问题。将讨论两种方法来找到最佳的推荐方法。第一种方法简单地生成与每个用户观看的每部电影的平均评分相关的用户配置文件,并使用余弦相似度找到与用户偏好最相似的电影。第二种方法是一个正则化线性回归模型,用于从评分和电影特征生成用户配置文件特征,以便可以使用这些用户配置文件预测每个用户尚未观看的电影的评分。
项目特征平均方法
这种方法真的很简单,我们将使用之前讨论过的 MovieLens 示例中描述电影的特性来解释它。该方法的目标是为每个用户i(长度等于G)生成电影类型的偏好向量
。这是通过计算平均评分
和每个类型条目g来完成的;
由用户i(Mi)观看的包含类型g的电影的评分总和减去平均
,然后除以包含类型g的电影数量:

在这里,I[kg]如果电影k包含类型g则为 1;否则为0。
然后将向量
与二进制向量 mj使用余弦相似度进行比较,并将相似度值最高的电影推荐给用户i。该方法的实现由以下 Python 类给出:

构造函数将电影标题列表存储在 Movieslist 中,并将电影特征存储在 Movies 向量中,GetRecMovies 函数生成用户类型的偏好向量,即
(应用前面的公式)称为 features_u,并返回与该向量最相似的项目。
正则化线性回归方法
该方法通过线性模型的参数
学习用户的电影偏好,其中
,其中 N 是用户的数量,G 是每个项目的特征(电影类型)的数量。我们在用户参数 θ[i] (θ[i0] = 1 ) 上添加一个截距值,以及具有相同值 m[j0] =1 的电影向量 *m[j] *,因此
。为了学习参数向量 q * [i] *,我们解决以下正则化最小化问题:

在这里,I[ij] 是 1;即用户 i 观看了该电影,否则 j 是 0,λ 是正则化参数(参见第三章,监督机器学习)。
解决方案是通过应用梯度下降法得到的(参见第三章,监督机器学习)。对于每个用户 i :
-
(k=0) -
(k>0)
由于我们在电影和用户向量中分别添加了 1 个条目,因此区分学习截距参数( k=0 )和其他参数是必要的(截距参数上没有过拟合的可能性,因此不需要对其进行正则化)。在参数 q [i] 学习完成后,推荐通过简单地应用公式
中的任何缺失评分 r[ij] 来执行。
该方法通过以下代码实现:

类 CBF_regression 的构造函数仅执行梯度下降以找到参数 θ[i](称为 Pmatrix),而函数 CalcRatings 在存储的效用矩阵 R 中找到最相似的评分向量(如果用户不在效用矩阵中),然后使用相应的参数向量来预测缺失的评分。
学习推荐系统的关联规则
尽管这种方法在许多商业推荐系统中并不常用,但由于历史数据的原因,关联规则学习确实是一种值得了解的方法,因为它可以应用于解决现实世界中的各种问题。这种方法的主要概念是基于交易数据库 T 中项目的发生频率的某些统计度量来寻找项目之间的关系(例如,一个交易可以是用户 i 看过的电影或 i 购买的商品)。更正式地说,一条规则可以是 {item1,item2} => {item3},即项目集合 ({item1,item2}) 意味着另一个项目集合 ({item3}) 的存在。使用两个定义来表征每个 X=>Y 规则:
-
支持度:给定一个项目集合 X,支持度 supp(X) 是包含集合 X 的交易在总交易中的比例。
-
置信度:它是包含集合 X 的交易中同时包含集合 Y 的交易的比例:conf(X=>Y)=supp(X U Y)/supp(X)*。请注意,置信度 conf(X=>Y) 可以与 conf(Y=>X) 有非常不同的值。
支持度表示交易数据库中某个规则的频率,而置信度表示在集合 X 存在的情况下,集合 Y 发生的概率。换句话说,支持度值被选择用来过滤从数据库中挖掘的规则数量(支持度越高,满足条件的规则越少),而置信度可以被视为集合 X 和 Y 之间的 相似度 指标。在电影推荐系统的案例中,交易数据库可以通过考虑每个用户喜欢的电影来从效用矩阵 R 中生成,我们寻找由集合 X 和 Y 组成的只包含一个项目(电影)的规则。这些规则收集在一个矩阵 ass_matrix 中,其中每个条目 ass_matrix[i][j] 代表规则 i => j 的置信度。给定用户的推荐通过简单地乘以他的评分 u_vec 得到:
,然后按最大值对应的最推荐电影到最少推荐电影的顺序对所有值进行排序:
。因此,这种方法并不预测评分,而是提供电影推荐列表;然而,它速度快,并且与稀疏效用矩阵配合得很好。请注意,为了尽可能快地找到形成集合 X 和 Y 的所有可能的项目组合,文献中已经开发了两种算法:apriori 和 fp-growth(这里没有讨论,因为我们只需要每个集合 X 和 Y 中有一个项目的规则)。
实现该方法的类如下:

类构造函数接受效用矩阵 Umatrix、电影标题列表 Movieslist、支持度 min_support、置信度 min_confidence 阈值(默认 0.1)以及 likethreshold 作为输入参数,这是在事务中考虑电影的最小评分值(默认 3)。函数 combine_lists 找到所有可能的规则,而 filterSet 只将规则减少到满足最小支持度阈值的子集。calc_confidence_matrix 用满足最小阈值的置信度值填充 ass_matrix(否则默认设置为 0),而 GetRecItems 返回根据用户评分 u_vec 提供的推荐电影列表。
对数似然比推荐系统方法
对数似然比(LLR)是衡量两个事件 A 和 B 不太可能是独立的,但比偶然发生(比单个事件频率更高)更可能一起发生的一个度量。换句话说,LLR 指示两个事件 A 和 B 之间可能存在显著的共现,其频率高于正常分布(在两个事件变量上)预测的频率。
泰德·邓宁(tdunning.blogspot.it/2008/03/surprise-and-coincidence.html)已经证明,LLR 可以基于事件 A 和 B 的二项分布,使用具有以下条目的矩阵 k 来表示:
| A | 非 A | |
|---|---|---|
| B | k11 | k12 |
| 非 B | k21 | k22 |

在这里,
和
是衡量向量 p 中包含的信息的香农熵。
注意:
也称为两个事件变量 A 和 B 的互信息(MI),衡量两个事件的发生如何相互依赖。
这个测试也称为 G2,并且已被证明在检测罕见事件的共现(特别是在文本分析中)方面非常有效,因此对于稀疏数据库(或在我们的情况下的效用矩阵)非常有用。
在我们的案例中,事件 A 和 B 是用户对电影 A 和 B 的喜欢或不喜欢,其中“喜欢电影”的事件定义为评分大于 3(反之亦然,对于不喜欢)。因此,算法的实现由以下类给出:

构造函数接受效用矩阵、电影标题列表和 likethreshold 作为输入,该阈值用于定义用户是否喜欢一部电影(默认值为 3)。函数 loglikelihood_ratio 生成包含每个电影对 i 和 j 的所有 LLR 值的矩阵,计算矩阵 k (calc_k) 和相应的 LLR (calc_llr)。函数 GetRecItems 返回具有 u_vec(该方法不预测评分值)给出的评分的用户推荐电影列表。
混合推荐系统
这是一类方法,它们在单个推荐器中结合了协同过滤(CBF)和内容过滤(CF)以实现更好的结果。已经尝试了多种方法,可以总结为以下几类:
-
加权:将协同过滤和内容过滤预测的评分组合成一个加权平均值。
-
混合:分别找到协同过滤和内容过滤预测的电影,然后合并成一个单一列表。
-
切换:基于某些标准,使用协同过滤预测或内容过滤预测。
-
特征组合:将协同过滤和内容过滤的特征一起考虑,以找到最相似的用户或物品。
-
特征增强:类似于特征组合,但使用额外的特征来预测某些评分,然后主要推荐器使用这些评分来生成推荐列表。例如,内容增强协同过滤通过基于内容的模型学习未评分电影的评分,然后采用协同方法来定义推荐。
例如,我们实现了两种混合特征组合方法,将物品的协同过滤方法与基于用户的协同过滤方法相结合。第一种方法使用基于用户的协同过滤到扩展效用矩阵中,该矩阵现在还包含每个用户每个类型的平均评分。Python 类如下:


构造函数生成与每个用户关联的电影类型平均评分特征的扩展效用矩阵 Umatrix_mfeats。函数 CalcRatings 通过比较用户的扩展特征向量使用皮尔逊相关系数找到 K-NN。第二种方法将奇异值分解(SVD)应用于包含每个用户类型偏好的扩展效用矩阵。

与奇异值分解(SVD)方法一样,评分减去用户评分的平均值,类型偏好也减去相同的用户评分的平均值。
推荐系统的评估
我们已经讨论了迄今为止在商业环境中使用的所有最相关的方法。推荐系统的评估可以离线执行(仅使用效用矩阵中的数据)或在线执行(使用效用矩阵数据以及每个用户通过网站实时提供的新的数据)。在线评估过程在 第七章,电影推荐系统网络应用 中讨论,其中包括一个合适的在线电影推荐系统网站。在本节中,我们将使用两个常用于评估推荐系统的离线测试来评估方法的性能:评分的均方根误差和排名准确性。对于所有适用 k 折交叉验证(见 第三章,监督机器学习)的评估,已执行 5 折交叉验证以获得更客观的结果。效用矩阵已使用以下函数分为 5 折:

在这里,df 是一个存储效用矩阵的数据框对象,而 k 是折数。在验证集中,对于每个用户评分向量 u_vec,一半的评分已被隐藏,以便可以预测真实值。

u_vals 存储预测的值,而 u_test 包含用于测试算法的评分。在我们开始比较不同算法的不同度量之前,我们将效用矩阵和电影内容矩阵加载到数据框中,并将数据分为 5 折以进行交叉验证。

df_vals 包含验证集,因此需要应用本节中介绍的 HideRandomRatings 函数。

现在在 movies 矩阵、movieslist 列表和数据框 df_trains、vals_vecs_folds、tests_vecs_folds 中的数据已准备好用于训练和验证前几节中讨论的所有方法。我们可以开始评估 均方根误差 ( RMSE )。
均方根误差 (RMSE) 评估
这种验证技术仅适用于 CF 方法和线性回归 CBF,因为这些算法仅生成预测评分。对于验证集中 u_vals 中的每个评分 rij,使用每种方法计算预测评分
,并得到均方根误差:
RMSE = 
在这里,Nval是u_vals向量中的评分数量。此公式中的平方因子极大地惩罚了大的误差,因此具有低 RMSE(最佳值)的方法的特征是所有预测评分上的小误差分布,而不是像平均绝对误差 MAE=
所偏好的那样在少数评分上有大的误差。
计算基于记忆的 CF 用户和基于物品的 CF 方法的 RMSE 的代码如下:


对于每种方法,调用 SE 函数来计算每个折叠的错误,然后获得折叠的总 RMSE。
使用 5 个最近邻进行基于物品的 CF(斜率一)和 20 个用户基于 CF,这些方法有以下误差:
| 方法 | RMSE | 预测评分数量 |
|---|---|---|
| 基于 CF 用户 | 1.01 | 39,972 |
| 基于 CF 物品 | 1.03 | 39,972 |
| 斜率一 | 1.08 | 39,972 |
| CF-CBF 用户 | 1.01 | 39,972 |
所有这些方法都具有相似的 RMSE 值,但最佳方法是基于物品的协同过滤。
对于基于模型的方法,而不是隐藏验证评分,u_test被包含在效用矩阵中进行训练,然后使用以下脚本来计算 RMSE:

该代码仅计算 CBF 回归和 SVD 的 RMSE,读者可以轻松复制代码来计算其他算法的错误,因为大部分所需代码只是被注释了(SVD 期望最大化,SGD,ALS 和 NMF)。结果如下表所示( K 维特征空间):
| 方法 | RMSE | 预测评分数量 |
|---|---|---|
| CBF 线性回归(a= 0.01, l =0.0001, its=50) | 1.09 | 39,972 |
| SGD ( K=20, 50 its, a =0.00001, l=0.001) | 1.35 | 39,972 |
| ALS ( K=20, 50 its, l =0.001) | 2.58 | 39,972 |
SVD (imputation =useraverage , K =20) |
1.02 | 39,972 |
SVD EM (imputation =itemaverage , iterations=30, K =20) |
1.03 | 39,972 |
HYBRID SVD (imputation =useraverage , K =20) |
1.01 | 39,972 |
NMF ( K =20 imputation =useraverage ) |
0.97 | 39,972 |
如预期,ALS 和 SGD 是最差的方法,但它们被讨论是因为从教学角度来看它们是有指导意义的(它们也较慢,因为实现没有像sklearn库中的方法那样优化)。
所有其他方法的结果都相似。然而,请注意,混合方法的结果略优于相应的 SVD 和基于用户的 CF 算法。请注意,要预测的电影是随机选择的,因此结果可能会有所不同。
分类度量
评分误差 RMSE 并不能真正反映方法的优劣,而是一个在商业环境中并不真正使用的学术度量。网站的目标是呈现与用户相关的内容,而不管用户给出的确切评分是多少。为了评估推荐项目的相关性,使用了精确度、召回率和f1(见第二章,无监督机器学习)度量,其中正确的预测是评分大于 3 的项目。这些度量是在每个算法返回的前 50 个项目(如果算法返回推荐列表或预测评分最高的其他方法的 50 个项目)上计算的。计算这些度量的函数如下:

在这里,布尔值ratingsval表示该方法是否返回评分或推荐列表。我们使用ClassificationMetrics函数的方式与计算所有方法的 RMSE 相同,因此实际评估度量的代码没有显示(你可以将其作为练习来编写)。以下表格总结了所有方法的总结结果(邻居数是最近邻的数量,K是特征空间的维度):
| 方法 | 精确度 | 召回率 | f1 | 预测评分数量 |
|---|---|---|---|---|
| CF 基于用户 ( 邻居数 =20) | 0.6 | 0.18 | 0.26 | 39,786 |
| CBFCF 基于用户 ( 邻居数 =20) | 0.6 | 0.18 | 0.26 | 39,786 |
HYBRID SVD ( K =20, 填充 =用户平均 ) |
0.54 | 0.12 | 0.18 | 39,786 |
| CF 基于项 ( 邻居数 =5) | 0.57 | 0.15 | 0.22 | 39,786 |
| Slope one ( 邻居数 =5) | 0.57 | 0.17 | 0.24 | 39,786 |
SVD EM ( K =20, 迭代次数=30, 填充 =用户平均 ) |
0.58 | 0.16 | 0.24 | 39,786 |
SVD ( K =20, 填充 =项目平均 ) |
0.53 | 0.12 | 0.18 | 39,786 |
| CBF 回归 (a = 0.01, l =0.0001, 迭代次数=50) | 0.54 | 0.13 | 0.2 | 39,786 |
| SGD (K=20, a =0.00001, l =0.001) | 0.52 | 0.12 | 0.18 | 39,786 |
| ALS ( K =20, λ =0.001, 迭代次数=50) | 0.57 | 0.15 | 0.23 | 39,786 |
| CBF 平均 | 0.56 | 0.12 | 0.19 | 39,786 |
| LLR | 0.63 | 0.3 | 0.39 | 39,786 |
NMF ( K =20, λ =0.001, 填充 =ssss ) |
0.53 | 0.13 | 0.19 | 39,786 |
| 关联规则 | 0.68 | 0.31 | 0.4 | 39,786 |
从结果中可以看出,最佳方法是关联规则,LLR、混合 CBFCF 基于用户和基于用户的 CF 方法也有很好的精度。请注意,由于预测的电影是随机选择的,因此结果可能会有所不同。
摘要
在本章中,我们讨论了从协同过滤和基于内容的过滤到两种简单混合算法的最常用推荐系统方法。注意,在文献中还有包含不同数据(用户性别、人口统计、观点、位置、设备等)的模态推荐系统。这些方法更高级,需要更多数据来使用它们。
在第七章,电影推荐系统网络应用,我们将使用本章讨论的方法实现一个网络推荐系统,但在那之前,我们将在第六章,Django 入门中介绍 Django 框架来构建网络应用。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
第六章. Django 入门
Django 是一个开源网络框架,在商业环境中被采用,因为它易于使用、稳定且灵活(它利用了 Python 中可用的多个库)。
在本章中,我们将关注我们认为对于在框架中管理和分析数据至关重要的功能。我们还解释了与构建基本网络应用相关的关键部分,但更详细的信息和资料可以在 docs.djangoproject.com 或其他来源找到。我们将介绍框架的主要部分,包括网络服务器应用的基本概念(设置、模型和命令)、HTML 的基础知识以及 shell 接口,以及 REST 框架接口的一般思想和它在 Django 中的实现(序列化器、REST 调用和 Swagger)。在简要介绍了通过互联网传输数据的 HTTP GET 和 POST 方法之后,我们开始安装和创建 Django 中的新服务器。
HTTP – GET 和 POST 方法的概述
超文本传输协议(HTTP)允许客户端(例如,网页浏览器)与服务器(我们的应用)交互。给定服务器网页的 URL,GET 方法是客户端从服务器查询数据的方式,指定了一些参数。这可以通过以下 curl 命令进行解释:
curl -X GET url_path?name1=value1&name2=value2
在 ? 符号之后,名称/值对指定了要查询的数据,它们由一个 & 符号分隔。
客户端将数据传输到服务器的方式称为 POST,数据位于调用的 body 中:
curl -X POST -d @datafile.txt url_path
现在,我们可以开始讨论如何使用 Django 创建一个新的服务器和应用。
安装和服务器创建
在终端中输入以下命令安装 Django 库:
sudo pip instal django
命令应安装 Django 版本 1.7 或更高版本(作者使用了 1.7 版本)。为了启动一个新的应用,我们输入以下命令:
django-admin startproject test_server
它将生成一个新的文件夹 test_app,其中包含以下文件结构:
└── test_server
├── manage.py
└── test_server
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
我们可以看到,在文件夹内,我们有 manage.py 文件,它允许程序员运行各种操作,还有一个名为 test_app 的子文件夹,其中包含以下文件:
-
settings.py:此文件存储所有参数设置以配置服务器 -
urls.py:此文件收集您网络应用上可用的所有 URL 路径,而网页背后的实际功能通常编写在views.py应用文件中 -
wsgi.py:这是一个模块,使服务器能够与网络应用通信 -
__init__.py:此文件用于将每个文件夹定义为包,以便内部导入模块
在我们的本地机器上,带有 欢迎来到 Django 页面的服务器通过简单地输入以下命令部署在 http://127.0.0 .1:8080/:
python manage.py runserver 8080
在这里,8080是服务器启动的端口(如果没有指定端口,默认情况下服务器在端口 8000启动)。现在服务器已准备就绪,我们可以通过简单地输入以下命令来创建我们想要的任何数量的应用程序:
python manage.py startapp nameapp
这将在根目录下的test_app文件夹内创建一个新的文件夹nameapp:
├── manage.py
├── nameapp
│ ├── __init__.py
│ ├── admin.py
│ ├── migrations
│ ├── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── test_server
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
在解释最重要的设置参数之后,我们将讨论这个文件夹的内容及其功能。请注意,对于 Django 版本 1.9,nameapp文件夹包含apps.py文件,以便在不使用settings.py文件的情况下配置nameapp。
设置
settings.py文件存储了 Django 服务器运行所需的所有配置。需要设置的最重要参数如下:
-
除了默认安装的用于管理网站的常见 Django 应用外,我们还将安装 REST 框架:
INSTALLED_APPS = ( ... 'rest_framework', 'rest_framework_swagger', 'nameapp', )REST 框架是一个允许 Django 应用(在这种情况下为
nameapp)通过 REST API 进行通信的应用程序,而 REST Framework Swagger 只是一个用于管理 REST API 的 Web 交互界面。这些功能将在以下章节中解释。此外,请注意,每个创建的应用都需要添加到这个字段中(在这种情况下,nameapp)。 -
可以使用不同的后端数据库(MySQL、Oracle、PostgreSQL等)来存储数据。在这种情况下,我们使用SQLite3(默认选项):
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'mydatabase', } }网页是用 HTML 编写的,因此需要一个文件夹来存储 HTML 代码。通常使用
templates文件夹来存储网页布局:TEMPLATE_DIRS = ( os.path.join(BASE_DIR, 'templates'), ) -
为了装饰网站,CSS 格式化和 JavaScript 代码通常存储在另一个文件夹
static中,与server文件夹处于同一级别。然后需要配置设置以从文件夹中获取文件:MEDIA_ROOT = os.path.join(BASE_DIR, 'static') STATIC_URL = '/static/' MEDIA_URL = '' STATIC_ROOT = '' STATICFILES_DIRS = ( os.path.join(BASE_DIR, "static"), ) -
要设置网站的 URL,设置被配置为从文件中获取路径(在这种情况下,
test_server/urls.py):ROOT_URLCONF = 'test_server.urls' -
有可能设置一个文件来存储我们希望在代码中用于调试目的的所有打印语句。我们使用
logging库和以下配置:LOGGING = { 'version': 1, 'disable_existing_loggers': True, 'formatters': { 'standard': { 'format': '%(asctime)s %(levelname)s %(name)s %(message)s' }, }, 'handlers': { 'default': { 'level':'DEBUG', 'class':'logging.handlers.RotatingFileHandler', 'filename': 'test_server.log', 'maxBytes': 1024*1024*5, # 5 MB 'backupCount': 5, 'formatter':'standard', }, }, 'loggers': { '': { 'handlers': ['default'], 'level': 'DEBUG', 'propagate': True }, } }在这里,
test_server.log文件存储了使用logging库定义的所有打印语句(例如,logging.debug('write something'))。
现在所有最重要的设置都已配置,我们可以专注于开发一个创建简单电子邮件地址簿的新应用。所以我们像往常一样创建应用:
python manage.py startapp addresesapp
现在,我们在服务器的根目录test_server下添加模板和静态文件夹:
├── addresesapp
│
├── __init__.py
│ ├── admin.py
│ ├── migrations
│ ├── models.py
│ ├── tests.py
│
└── views.py
├── manage.py
└── test_server
├── __init__.py
├── __init__.pyc
├── settings.py
├── settings.pyc
├── static
├── templates
├──
urls.py
└── wsgi.py
注意,INSTALLED_APPS中的nameapp变为addressesapp。在下一节中,我们将讨论实现应用的主要功能。所有代码都可以在作者的 GitHub 仓库的chapter_6文件夹中找到(github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_6)。
编写应用 – 最重要功能
要创建一个存储电子邮件地址的 Web 应用程序,我们需要一个存储数据的表和允许最终用户添加、删除和查看地址簿的网页。此外,我们可能还想将地址簿转换为电子表格格式,或者通过互联网将数据发送到另一个应用程序。Django 有特定的功能来完成所有这些操作(models、views、admin、API REST-framework 和 commands),我们现在将讨论数据是如何存储的。
模型
要创建一个电子邮件地址簿,我们需要在一个表中存储每个联系人的姓名和他们的电子邮件地址。在 Django 中,表被称为模型,并在 models.py 文件中定义:
from django.db import models
from django.utils.translation import ugettext_lazy as _
class Person(models.Model):
name = models.CharField(_('Name'), max_length=255, unique=True)
mail = models.EmailField(max_length=255, blank=True)
#display name on admin panel
def __unicode__(self):
return self.name
在 Django 中,表的列是模型的字段,可以是不同类型:整数、字符等。请注意,Django 会自动为任何新对象添加一个递增的 ID 字段。唯一选项意味着模型中不能存在重复的名称,而空白状态表示字段是否可以为空。__unicode__ 函数是可选的,它用于将每个人渲染为字符串(在这种情况下我们设置了名称字符串)。
现在模型已经创建,我们需要将其应用到 SQLite 数据库中:
python manage.py makemigrations
python manage.py migrate
makemigrations 命令会将模型更改转换为迁移文件(位于 addressesapp 文件夹内的 migrations 文件夹中),而 migrate 命令则将更改应用到数据库模式中。请注意,如果同一网站使用了多个应用程序,那么生成迁移的命令是 python manage.py makemigrations 'appname'。
HTML 网页背后的 URL 和视图
现在我们知道了如何存储数据,我们需要通过网页记录联系人并通过另一个页面显示联系人。在下一节中,将描述这些页面,并简要概述 HTML 页面的主要属性。
HTML 页面
本节中解释的所有代码都存储在 test_server 文件夹下的模板文件夹中。
应用程序的主页允许用户记录新的联系人,如下面的截图所示:

如您所见,页面的主体由两个需要填写的人名和电子邮件地址的框指定,按下 Add 按钮将它们添加到数据库中。HTML 文件 home.html 如下所示:
{% extends "addressesapp/base.html" %}
{% block content %}
<form action="" method="POST">
{% csrf_token %}
<h2 align = Center>Add person to address book </h2>
<p> <br><br></p>
<p align = Center><input type="search" class="span3" placeholder="person" name="name" id="search" autofocus /> </p>
<p align = Center><input type="search" class="span3" placeholder="email" name="email" id="search" autofocus /> </p>
<p align = Center><button type="submit" class="btn btn-primary btn-large pull-center">Add »</button></p>
</form>
{% endblock content %}
我们使用 POST 表单提交了由两个段落字段(由 <p>...</p> 指定)收集的数据,并通过 Add 按钮标签(»:用于在文本后渲染小箭头)激活。页面的标题 Add person to address book 由类型 2 的标题(<h2>...</h2>)渲染。注意 csrt_token 标签,它启用了跨站伪造保护请求(更多信息请参阅 www.squarefree.com/securitytips/web-developers.html#CSRF)。
页面的样式(CSS 和 JavaScript 文件)、页脚以及带有主页、电子邮件簿和查找按钮的页眉栏都在 base.html 文件中定义(见 template 文件夹)。查找按钮被实现为一个表单:
<form class="navbar-search pull-left" action="{% url 'get_contacts' %}" method="GET">
{% csrf_token %}
<div style="overflow: hidden; padding-right: .5em;">
<input type="text" name="term" style="width: 70%;" />
<input type="submit" name="search" value="Find" size="30" style="float: right" />
</div>
</form>
使用 div 标签定义了文本字段和查找按钮,该按钮激活对在 urls.py 文件中定义为 get_contacts 的 URL 的 GET 调用(见下一节)。
要显示的另一个页面是地址簿:

{% extends "addressesapp/base.html" %}
{% block content %}
<h2 align = Center>Email address book</h2>
<P align=Center>[
{% for letter in alphabet %}
which is given by the book.html file:
{% extends "addressesapp/base.html" %}
{% block content %}
<h2 align = Center>Email address book</h2>
<P align=Center>[
{% for letter in alphabet %}
<a href="{% url 'addressesbook' %}?letter={{letter}}" > {{letter}} </a>
{% endfor %}
|<a href="addressesapp/book.html"> Index </a> ] </P>
<section id="gridSystem">
{% for contact in contacts %}
<div class="row show-grid">
<p align = Center><strong> name: </strong>{{ contact.name }} <strong>email:</strong> {{ contact.mail }}    
<a class="right" href="{% url 'delete_person' contact.name %}" > delete </a>
</p>
</div>
{% endfor %}
</section>
{% endblock content %}
再次,base.html 被调用以渲染主标题按钮、页脚和样式。在包含电子邮件地址簿(Email address book)类型的标题(2 级)之后,执行一个基于字母表的循环,{% for letter in alphabet %},以仅显示以相应字母开头的联系人。这是通过调用带有查询字母 {{letter}} 的 addressesbook URL 来实现的。然后渲染显示的联系人列表,循环遍历联系人列表 {% for contact in contacts %}:一个段落标签显示姓名、电子邮件和一个用于从数据库中删除人员的按钮。我们现在将讨论页面动作的实现(添加、查找或删除人员,以及显示地址簿)。
URL 声明和视图
我们现在将讨论 urls.py 和 views.py 如何与每个页面的 HTML 代码协同工作以执行所需动作。
正如我们所见,应用程序的两个主要页面,主页和地址簿,与一个 URL 相关联,在 Django 中在 urls.py 文件中声明:
from django.conf.urls import patterns, include, url
from django.contrib import admin
from addressesapp.api import AddressesList
urlpatterns = patterns('',
url(r'^docs/', include('rest_framework_swagger.urls')),
url(r'^$','addressesapp.views.main'),
url(r'^book/','addressesapp.views.addressesbook',name='addressesbook'),
url(r'^delete/(?P<name>.*)/','addressesapp.views.delete_person', name='delete_person'),
url(r'^book-search/','addressesapp.views.get_contacts', name='get_contacts'),
url(r'^addresses-list/', AddressesList.as_view(), name='addresses-list'),
url(r'^notfound/','addressesapp.views.notfound',name='notfound'),url(r'^admin/', include(admin.site.urls)),)
每个 URL 都由一个正则表达式(URL 字符串前的 r)指定,因此主页由 http://127.0.0.1:8000/(^ 开始符号后跟 $ 结束符号)指定,其动作(添加记录)在 views.py 文件的 main 函数中实现:
def main(request):
context={}
if request.method == 'POST':
post_data = request.POST
data = {}
data['name'] = post_data.get('name', None)
data['email'] = post_data.get('email', None)
if data:
return redirect('%s?%s' % (reverse('addressesapp.views.main'),
urllib.urlencode({'q': data})))
elif request.method == 'GET':
get_data = request.GET
data= get_data.get('q',None)
if not data:
return render_to_response(
'addressesapp/home.html', RequestContext(request, context))
data = literal_eval(get_data.get('q',None))
print data
if not data['name'] and not data['email']:
return render_to_response(
'addressesapp/home.html', RequestContext(request, context))
#add person to emails address book or update
if Person.objects.filter(name=data['name']).exists():
p = Person.objects.get(name=data['name'])
p.mail=data['email']
p.save()
else:
p = Person()
p.name=data['name']
p.mail=data['email']
p.save()
#restart page
return render_to_response(
'addressesapp/home.html', RequestContext(request, context))
当用户提交一个新的联系人以存储时,POST 方法将调用重定向到 GET 方法。如果提供了姓名和电子邮件,将添加一个新的 Person 模型对象,如果已存在则更新。在此方法中,相同名称但大写字母将被视为不同的名称,因此 Andrea、ANDREA 和 andrea 将是三个不同的联系人。要更改此,读者只需在名称字段上应用小写函数,这样三个 andrea 表达式都将指向一个 andrea。
base.html 文件中的查找动作与 http://127.0.0.1:8000/book-search/ URL 相关联,并且动作在 views.py 文件中的 get_contacts 函数中定义:
def get_contacts(request):
logging.debug('here')
if request.method == 'GET':
get_data = request.GET
data= get_data.get('term','')
if data == '':
return render_to_response(
'addressesapp/nopersonfound.html', RequestContext(request, {}))
else:
return redirect('%s?%s' % (reverse('addressesapp.views.addressesbook'),
urllib.urlencode({'letter': data})))
如果用户在文本标题字段中指定了非空字符串,则函数将重定向到带有搜索名称的 addressesbook 函数(否则显示一个未找到的页面)。
标题按钮电子邮件簿链接到 http://127.0.0.1:8000/book/ URL,该 URL 根据 addressesbook 函数显示联系人:
def addressesbook(request):
context = {}
logging.debug('address book')
get_data = request.GET
letter = get_data.get('letter',None)
if letter:
contacts = Person.objects.filter(name__iregex=r"(^|\s)%s" % letter)
else:
contacts = Person.objects.all()
#sorted alphabetically
contacts = sort_lower(contacts,"name")#contacts.order_by("name")
context['contacts']=contacts
alphabetstring='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
context['alphabet']=[l for l in alphabetstring]
return render_to_response(
'addressesapp/book.html', RequestContext(request, context))
def sort_lower(lst, key_name):
return sorted(lst, key=lambda item: getattr(item, key_name).lower())
字母字段存储名称(如果从查找标题按钮重定向)或字母(如果从电子邮件书页调用),并对Person模型中的联系人进行查找。检索到的联系人随后存储在contacts上下文对象中,而字母存储在alphabet上下文对象中。如果没有指定字母,则返回数据库中的所有联系人。请注意,名称可以有大写和小写的第一个字母,因此通常的order_by方法不会按字母顺序对名称进行排序。因此,sort_lower函数将每个名称转换为小写并按字母顺序对联系人进行排序。
删除操作由delete_person函数执行,并通过http://127.0.0.1:8000/delete/(?P<name>.*)/ URL 调用。.*表示所有字符都可用于构成名称(注意,如果我们只想有字符、数字和空格,我们应该使用[a-zA-Z0-9 ]+):
def delete_person(request,name):
if Person.objects.filter(name=name).exists():
p = Person.objects.get(name=name)
p.delete()
context = {}
contacts = Person.objects.all()
#sorted alphabetically
contacts = sort_lower(contacts,"name")#contacts.order_by("name")
context['contacts']=contacts
return render_to_response(
'addressesapp/book.html', RequestContext(request, context))
在数据库的Person表中搜索name查询变量,并将其删除。函数返回带有剩余联系人的电子邮件书页。
同样,未找到的 URL 激活了未找到功能,你现在应该能够理解它是如何工作的。
管理员 URL 指的是 Django 界面(见下文),而 docs 是本书RESTful 应用程序编程接口(API)部分中讨论的 REST 框架 swagger。
管理员
管理面板是管理应用程序的用户界面,可以通过浏览器访问。在admin.py文件中,我们可以使用以下命令添加刚刚创建的模型:
from models import Person
admin.site.register(Person)
所有模型都可以通过用户界面访问:
http://127.0.0.1:8000/admin/
在此链接中,需要用户名和密码。我们使用以下命令创建它:
python manage.py createsuperuser
然后我们输入用户名和密码(在我的情况下,andrea/a)。
现在,我们可以探索下面的面板:

点击人员,我们将看到按名称显示的所有Person对象(因为模型中的__unicode__函数引用的是名称字段):

壳接口
Django 框架还提供了一个 shell 来探索创建的模型并对其进行测试。要启动它,我们在终端中输入以下内容:
python manage.py shell
现在我们可以导入Person模型并对其进行操作:
In [1]: from addressesapp.models import Person
In [2]: newcontact = Person()
In [3]: newcontact.name = 'myfriend1'
In [4]: newcontact.mail = 'bla@.com'
In [5]: newcontact.save()
In [6]: Person.objects.all()
Out[6]: [<Person: ss>, <Person: Andrea Isoni>, <Person: www 1>, <Person: addd-ww>, <Person: myfriend1>]
在这些行中,我们创建了一个新的联系人,myfriend1,并验证它已被添加到Person对象列表中。
命令
Django 框架还允许我们通过manage.py模块编写自定义命令。例如,我们希望将整个联系人列表导出到 CSV 文件。为了实现这一点,我们在management文件夹(每个文件夹中都有__init__.py)内创建一个commands文件夹。该文件实现了将联系人列表导出到 CSV 的自定义命令,扩展了BaseCommand类:
from addressesapp.models import Person
from django.core.management.base import BaseCommand, CommandError
from optparse import make_option
import csv
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--output',
dest='output', type='string',
action='store',
help='output file'),
)
def person_data(self, person):
return [person.name,person.mail]
def handle(self, *args, **options):
outputfile = options['output']
contacts = Person.objects.all()
header = ['Name','email']
f = open(outputfile,'wb')
writer = csv.writer(f, quoting=csv.QUOTE_NONNUMERIC)
writer.writerow(header)
for person in contacts:
writer.writerow(self.person_data(person))
命令必须定义一个handler函数,该函数将执行导出操作。在test_server文件夹中输入以下内容:
python manage.py contacts_tocsv –output='contacts_list.csv'
RESTful 应用程序编程接口(API)
RESTful API 是一种应用程序编程接口,它使用 HTTP 请求(如 GET 和 POST)来管理应用程序的数据。在这种情况下,API 通过curl调用获取地址簿。为了做到这一点,我们在settings.py的INSTALLED_APPS部分中定义了rest_framework应用,然后api.py文件实现了 API:
from rest_framework import viewsets, generics, views
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework.pagination import PageNumberPagination
from addressesapp.serializers import AddressesSerializer
from addressesapp.models import Person
class LargeResultsSetPagination(PageNumberPagination):
page_size = 1000
page_size_query_param = 'page_size'
max_page_size = 10000
class AddressesList(generics.ListAPIView):
serializer_class = AddressesSerializer
permission_classes = (AllowAny,)
pagination_class = LargeResultsSetPagination
def get_queryset(self):
query = self.request.query_params.get
if query('name'):
return Person.objects.filter(name=query('name'))
else:
return Person.objects.all()
我们已经使用ListAPIView类来返回所有Person对象,或者只返回匹配name值的那个对象。由于返回的列表可能太大,我们需要重写PageNumberPagination类,以便在同一页面上显示更多对象;LargeResultsSetPagination类允许每页最多显示 10,000 个对象。这个 API 需要将Person对象转换为 JSON 格式对象,这通过在serializers.py中实现的AddressesSerializer序列化器来完成:
from addressesapp.models import Person
from rest_framework import serializers
class AddressesSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Person
fields = ('id', 'name', 'mail')
现在可以使用curl命令检索地址簿:
curl -X GET http://localhost:8000/addresses-list/
注意 URL 末尾的斜杠。同样,我们可以指定一个名称值来获取他们的电子邮件:
curl -X GET http://localhost:8000/addresses-st/?name=name_value
注意,我们始终可以指定页面查询参数,以防联系人数量太大(或更改分页大小值)。在urls.py文件中,我们还定义了 docs URL 为我们的 Swagger RESTful API,允许用户使用浏览器探索和测试 API:

这是一个用户友好的方式来验证 API 是否按预期工作,并且数据以正确的格式显示。
摘要
在本章中,我们讨论了如何使用 Django 框架创建 Web 应用程序。Django 的主要功能,如models、admin、views、commands、shell和RESTful API,都已被描述,因此读者现在应该具备在现实场景中开发 Web 应用程序的必要知识。
我们将使用这些知识,结合我们在前几章中学到的知识,在接下来的两章中构建我们的电影推荐引擎和电影情感分析应用程序。
第七章。电影推荐系统网络应用程序
本章的目的是解释一个实际案例,展示推荐系统在实际中的应用,使用 Django 框架。我们将实现一个电影推荐系统,其中每个订阅服务的用户将根据我们在 第五章 中讨论的偏好,收到推荐的电影。同样,我们也将使用相同的数据,该数据由 942 个用户对 603 部电影超过 50 次的评分组成。为了接收推荐,每个用户必须对一定数量的电影进行评分,因此实现了一个信息检索系统 (第四章,网络挖掘技术) 来搜索评分的电影。我们将讨论 Django 应用程序的不同部分:设置、模型、用户登录/注销、命令、信息检索系统、推荐系统、管理界面和 API(所有代码均可在作者的 GitHub 上的 chapter_7 文件夹中找到,网址为 github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_7)。自 第六章 介绍了 Django 的主要功能以来,每当使用一个新功能时,也会提供技术解释。现在我们可以开始描述不同的设置和初始设置来运行应用程序。
应用程序设置
我们像往常一样创建并启动 Django:
django-admin startproject server_movierecsys
从 server_movierecsys 文件夹启动应用程序:
python manage.py startapp books_recsys_app
现在需要配置 settings.py。正如我们在 第六章 中所看到的,我们设置了已安装的应用程序、HTML 模板、布局格式化文件夹和 SQLite 数据库:
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_swagger',
'books_recsys_app',
)
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
)
STATIC_URL = '/static/'
STATICFILES_DIRS = ( os.path.join(BASE_DIR, "static"), )
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
除了标准应用程序和其余的框架(swagger)之外,books_recsys_app 已被包含在已安装的应用程序列表中。
在这种情况下,我们需要将数据持久地加载到内存中,以通过不在每个用户请求时计算或检索数据来提高用户体验。为了在内存中保存数据或昂贵的计算结果,我们在 settings.py 中设置了 Django 的缓存系统:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache',
'TIMEOUT': None,
}
}
我们选择了存储在 /var/tmp/django_cache 的 基于文件的缓存 缓存类型,以及一个 None 超时,这意味着缓存中的数据永远不会过期。
要使用管理界面,我们通过以下命令设置 superuser 账户:
python manage.py createsuperuser (admin/admin)
应用程序通过输入以下命令在 http://localhost:8000/ 上运行:
python manage.py runserver
模型
在这个应用程序中,我们需要存储与每部电影相关的数据以及网站每个用户对电影的评分。我们设置了三个模型:
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
array = jsonfield.JSONField()
arrayratedmoviesindxs = jsonfield.JSONField()
lastrecs = jsonfield.JSONField()
def __unicode__(self):
return self.user.username
def save(self, *args, **kwargs):
create = kwargs.pop('create', None)
recsvec = kwargs.pop('recsvec', None)
print 'create:',create
if create==True:
super(UserProfile, self).save(*args, **kwargs)
elif recsvec!=None:
self.lastrecs = json.dumps(recsvec.tolist())
super(UserProfile, self).save(*args, **kwargs)
else:
nmovies = MovieData.objects.count()
array = np.zeros(nmovies)
ratedmovies = self.ratedmovies.all()
self.arrayratedmoviesindxs = json.dumps([m.movieindx for m in ratedmovies])
for m in ratedmovies:
array[m.movieindx] = m.value
self.array = json.dumps(array.tolist())
super(UserProfile, self).save(*args, **kwargs)
class MovieRated(models.Model):
user = models.ForeignKey(UserProfile, related_name='ratedmovies')
movie = models.CharField(max_length=100)
movieindx = models.IntegerField(default=-1)
value = models.IntegerField()
class MovieData(models.Model):
title = models.CharField(max_length=100)
array = jsonfield.JSONField()
ndim = models.IntegerField(default=300)
description = models.TextField()
模型MovieData存储每部电影的详细信息:标题、描述和向量表示(ndim是向量表示的维度)。MovieRated记录登录用户评价的每部电影(每个MovieRated对象都与一个UserProfile相关联,该UserProfile利用网站)。UserProfile模型存储所有注册到网站的用户,以便他们可以评价电影并接收推荐。每个UserProfile通过添加array字段扩展默认的 Django 用户模型,该字段存储用户的所有电影评价,以及recsvec字段,该字段存储他的最后推荐:save函数被覆盖以填充array字段,包含与用户关联的所有MovieRated对象(如果else语句为true),以及填充lastrecs字段,包含最后的推荐(else if语句)。请注意,MovieRated模型有一个UserProfile外键,其related_name等于ratedmovies:在UserProfile模型的save函数中,self.ratedmovies.all()指的是所有具有相同UserProfile值的RatedMovie对象。UserProfile模型上的arrayratedmoviesindxs字段记录用户评价的所有电影,并被应用程序的 API 使用。
要将这些数据结构写入数据库,我们需要运行:
python manage.py makemigrations
python manage.py migrate
命令
本应用中使用的命令用于将数据加载到内存(缓存)中,以加快用户体验。尽管电影数据库与第四章中使用的相同,网络挖掘技术(即 603 部电影,由 942 个用户评价超过 50 次),但每部电影都需要一个描述来设置一个用于评价电影的信息检索系统。我们开发的第一个命令将第四章中使用的效用矩阵中的所有电影标题取出来,从Open Movie Database(OMDb)在线服务收集相应的描述:
from django.core.management.base import BaseCommand
import os
import optparse
import numpy as np
import json
import pandas as pd
import requests
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
optparse.make_option('-i', '--input', dest='umatrixfile',
type='string', action='store',
help=('Input utility matrix')),
optparse.make_option('-o', '--outputplots', dest='plotsfile',
type='string', action='store',
help=('output file')),
optparse.make_option('--om', '--outputumatrix', dest='umatrixoutfile',
type='string', action='store',
help=('output file')),
)
def getplotfromomdb(self,col,df_moviesplots,df_movies,df_utilitymatrix):
string = col.split(';')[0]
title=string[:-6].strip()
year = string[-5:-1]
plot = ' '.join(title.split(' ')).encode('ascii','ignore')+'. '
url = "http://www.omdbapi.com/?t="+title+"&y="+year+"&plot=full&r=json"
headers={"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36"}
r = requests.get(url,headers=headers)
jsondata = json.loads(r.content)
if 'Plot' in jsondata:
#store plot + title
plot += jsondata['Plot'].encode('ascii','ignore')
if plot!=None and plot!='' and plot!=np.nan and len(plot)>3:#at least 3 letters to consider the movie
df_moviesplots.loc[len(df_moviesplots)]=[string,plot]
df_utilitymatrix[col] = df_movies[col]
print len(df_utilitymatrix.columns)
return df_moviesplots,df_utilitymatrix
def handle(self, *args, **options):
pathutilitymatrix = options['umatrixfile']
df_movies = pd.read_csv(pathutilitymatrix)
movieslist = list(df_movies.columns[1:])
df_moviesplots = pd.DataFrame(columns=['title','plot'])
df_utilitymatrix = pd.DataFrame()
df_utilitymatrix['user'] = df_movies['user']
for m in movieslist[:]:
df_moviesplots,df_utilitymatrix=self.getplotfromomdb(m,df_moviesplots,df_movies,df_utilitymatrix)
outputfile = options['plotsfile']
df_moviesplots.to_csv(outputfile, index=False)
outumatrixfile = options['umatrixoutfile']
df_utilitymatrix.to_csv(outumatrixfile, index=False)
命令语法是:
python manage.py --input=utilitymatrix.csv --outputplots=plots.csv –outputumatrix='umatrix.csv'
包含在utilitymatrix文件中的每个电影标题都由getplotfromomdb函数使用 Python 模块中的请求从网站www.omdbapi.com/检索电影的描述(plot)。然后,电影的描述(和titles)与相应的效用矩阵(outputumatrix)一起保存在 CSV 文件(outputplots)中。
另一个命令将电影的描述创建一个信息检索系统(词频逆文档频率(TF-IDF)模型),使用户能够通过输入一些相关词汇来查找电影。然后,这个 tf-idf 模型与初始推荐系统模型(基于物品的协同过滤和对数似然比)一起保存在 Django 缓存中。代码如下:
from django.core.management.base import BaseCommand
import os
import optparse
import numpy as np
import pandas as pd
import math
import json
import copy
from BeautifulSoup import BeautifulSoup
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import WordPunctTokenizer
tknzr = WordPunctTokenizer()
#nltk.download('stopwords')
stoplist = stopwords.words('english')
from nltk.stem.porter import PorterStemmer
stemmer = PorterStemmer()
from sklearn.feature_extraction.text import TfidfVectorizer
from books_recsys_app.models import MovieData
from django.core.cache import cache
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
optparse.make_option('-i', '--input', dest='input',
type='string', action='store',
help=('Input plots file')),
optparse.make_option('--nmaxwords', '--nmaxwords', dest='nmaxwords',
type='int', action='store',
help=('nmaxwords')),
optparse.make_option('--umatrixfile', '--umatrixfile', dest='umatrixfile',
type='string', action='store',
help=('umatrixfile')),
)
def PreprocessTfidf(self,texts,stoplist=[],stem=False):
newtexts = []
for i in xrange(len(texts)):
text = texts[i]
if stem:
tmp = [w for w in tknzr.tokenize(text) if w not in stoplist]
else:
tmp = [stemmer.stem(w) for w in [w for w in tknzr.tokenize(text) if w not in stoplist]]
newtexts.append(' '.join(tmp))
return newtexts
def handle(self, *args, **options):
input_file = options['input']
df = pd.read_csv(input_file)
tot_textplots = df['plot'].tolist()
tot_titles = df['title'].tolist()
nmaxwords=options['nmaxwords']
vectorizer = TfidfVectorizer(min_df=0,max_features=nmaxwords)
processed_plots = self.PreprocessTfidf(tot_textplots,stoplist,True)
mod_tfidf = vectorizer.fit(processed_plots)
vec_tfidf = mod_tfidf.transform(processed_plots)
ndims = len(mod_tfidf.get_feature_names())
nmovies = len(tot_titles[:])
#delete all data
MovieData.objects.all().delete()
matr = np.empty([1,ndims])
titles = []
cnt=0
for m in xrange(nmovies):
moviedata = MovieData()
moviedata.title=tot_titles[m]
moviedata.description=tot_textplots[m]
moviedata.ndim= ndims
moviedata.array=json.dumps(vec_tfidf[m].toarray()[0].tolist())
moviedata.save()
newrow = moviedata.array
if cnt==0:
matr[0]=newrow
else:
matr = np.vstack([matr, newrow])
titles.append(moviedata.title)
cnt+=1
#cached
cache.set('data', matr)
cache.set('titles', titles)
cache.set('model',mod_tfidf)
#load the utility matrix
umatrixfile = options['umatrixfile']
df_umatrix = pd.read_csv(umatrixfile)
Umatrix = df_umatrix.values[:,1:]
cache.set('umatrix',Umatrix)
#load rec methods...
cf_itembased = CF_itembased(Umatrix)
cache.set('cf_itembased',cf_itembased)
llr = LogLikelihood(Umatrix,titles)
cache.set('loglikelihood',llr)
from scipy.stats import pearsonr
from scipy.spatial.distance import cosine
def sim(x,y,metric='cos'):
if metric == 'cos':
return 1.-cosine(x,y)
else:#correlation
return pearsonr(x,y)[0]
class CF_itembased(object):
...
class LogLikelihood(object):
...
运行命令的语法是:
python manage.py load_data --input=plots.csv --nmaxwords=30000 --umatrixfile=umatrix.csv
输入参数使用get_plotsfromtitles命令获取电影的描述,并使用nmaxwords参数指定的最大单词数创建一个tf-idf模型(见第四章,网络挖掘技术)。每部电影的资料也保存在一个MovieData对象中(标题、tf-idf 表示、描述和 tf-idf 词汇表的ndim单词数)。请注意,第一次运行该命令时,需要下载nltk.download('stopwords')中的stopwords(在前面代码中已注释)。
使用以下命令将 tf-idf 模型、标题列表和 tf-idf 电影表示的矩阵保存在 Django 缓存中:
from django.core.cache import cache
...
cache.set('model',mod_tfidf)
cache.set('data', matr)
cache.set('titles', titles)
注意
注意,需要加载 Django 缓存模块(django.core.cache)(在文件开头)才能使用。
同样,实用矩阵(umatrixfile参数)用于初始化应用程序使用的两个推荐系统:基于项目的协同过滤和似然比方法。这两种方法在前面代码中没有编写,因为它们基本上与第五章中描述的代码相同(完整的代码可以在作者的 GitHub 仓库的chapter_7文件夹中找到)。然后,方法和实用矩阵被加载到 Django 缓存中,以便使用:
cache.set('umatrix',Umatrix)
cache.set('cf_itembased',cf_itembased)
cache.set('loglikelihood',llr)
现在,只需调用相应的名称,就可以在网页中使用数据(和模型),正如我们将在以下章节中看到的。
用户注册登录/注销实现
此应用程序可以向注册在网站上的不同用户推荐电影。为了管理注册过程,我们使用标准的User Django 模块,正如我们在模型部分中看到的。网站的每一页都引用base.html页面,该页面实现了一个顶部栏,允许用户注册或登录(右侧):

点击两个按钮之一登录或注册将激活以下代码:
<form class="navbar-search pull-right" action="{% url 'auth' %}" method="GET">
{% csrf_token %}
<div style="overflow: hidden; padding-right: .5em;">
<input type="submit" name="auth_method" value="sign up" size="30" style="float: right" />
<input type="submit" name="auth_method" value="sign in" size="30" style="float: right" />
</div>
</form>
这两个方法引用urls.py:
url(r'^auth/', 'books_recsys_app.views.auth', name='auth')
这将调用views.py中的auth函数:
def auth(request):
if request.method == 'GET':
data = request.GET
auth_method = data.get('auth_method')
if auth_method=='sign in':
return render_to_response(
'books_recsys_app/signin.html', RequestContext(request, {}))
else:
return render_to_response(
'books_recsys_app/createuser.html', RequestContext(request, {}))
elif request.method == 'POST':
post_data = request.POST
name = post_data.get('name', None)
pwd = post_data.get('pwd', None)
pwd1 = post_data.get('pwd1', None)
create = post_data.get('create', None)#hidden input
if name and pwd and create:
if User.objects.filter(username=name).exists() or pwd!=pwd1:
return render_to_response(
'books_recsys_app/userexistsorproblem.html', RequestContext(request))
user = User.objects.create_user(username=name,password=pwd)
uprofile = UserProfile()
uprofile.user = user
uprofile.name = user.username
uprofile.save(create=True)
user = authenticate(username=name, password=pwd)
login(request, user)
return render_to_response(
'books_recsys_app/home.html', RequestContext(request))
elif name and pwd:
user = authenticate(username=name, password=pwd)
if user:
login(request, user)
return render_to_response(
'books_recsys_app/home.html', RequestContext(request))
else:
#notfound
return render_to_response(
'books_recsys_app/nopersonfound.html',
RequestContext(request))
函数将重定向到以下截图所示的注册页面:

如果您已经注册,它将带您到以下截图所示的登录页面:

该页面允许用户创建用户名和密码并登录网站。然后使用这些数据创建一个新的 User Django 模型对象和相关UserProfile对象(注意create参数为True,以保存对象而不关联评分电影数组):
user = User.objects.create_user(username=name,password=pwd)
uprofile = UserProfile()
uprofile.user = user
uprofile.save(create=True)
user = authenticate(username=name, password=pwd)
然后用户使用标准的 Django 方法登录:
from django.contrib.auth import authenticate, login
...
login(request, user)
因此,网站顶部栏看起来像(用户名:a)如下截图所示:

注意,在存在同名用户(新注册异常事件)或用户未找到(登录异常事件)的情况下,这两种情况都已实现,读者可以查看代码以了解这些事件是如何处理的。
登出按钮指的是urls.py:
url(r'^signout/','books_recsys_app.views.signout',name='signout')
这将调用views.py中的signout函数:
from django.contrib.auth import logout
…
def signout(request):
logout(request)
return render_to_response(
'books_recsys_app/home.html', RequestContext(request))
该函数使用标准的 Django 登出方法并重定向到主页(登录和登出按钮将再次显示在顶部栏)。现在,用户可以使用下一节中描述的信息检索系统(搜索引擎)搜索并评分电影。
信息检索系统(电影查询)
为了对电影进行评分,用户需要使用主页进行搜索:

在文本框中输入一些相关词语,页面将通过urls.py中的对应home URL 调用views.py文件中的home函数:
def home(request):
context={}
if request.method == 'POST':
post_data = request.POST
data = {}
data = post_data.get('data', None)
if data:
return redirect('%s?%s' % (reverse('books_recsys_app.views.home'),
urllib.urlencode({'q': data})))
elif request.method == 'GET':
get_data = request.GET
data = get_data.get('q',None)
titles = cache.get('titles')
if titles==None:
print 'load data...'
texts = []
mobjs = MovieData.objects.all()
ndim = mobjs[0].ndim
matr = np.empty([1,ndim])
titles_list = []
cnt=0
for obj in mobjs[:]:
texts.append(obj.description)
newrow = np.array(obj.array)
#print 'enw:',newrow
if cnt==0:
matr[0]=newrow
else:
matr = np.vstack([matr, newrow])
titles_list.append(obj.title)
cnt+=1
vectorizer = TfidfVectorizer(min_df=1,max_features=ndim)
processedtexts = PreprocessTfidf(texts,stoplist,True)
model = vectorizer.fit(processedtexts)
cache.set('model',model)
#cache.set('processedtexts',processedtexts)
cache.set('data', matr)
cache.set('titles', titles_list)
else:
print 'loaded',str(len(titles))
Umatrix = cache.get('umatrix')
if Umatrix==None:
df_umatrix = pd.read_csv(umatrixpath)
Umatrix = df_umatrix.values[:,1:]
cache.set('umatrix',Umatrix)
cf_itembased = CF_itembased(Umatrix)
cache.set('cf_itembased',cf_itembased)
cache.set('loglikelihood',LogLikelihood(Umatrix,movieslist))
if not data:
return render_to_response(
'books_recsys_app/home.html', RequestContext(request, context))
#load all movies vectors/titles
matr = cache.get('data')
titles = cache.get('titles')
model_tfidf = cache.get('model')
#find movies similar to the query
queryvec = model_tfidf.transform([data.lower().encode('ascii','ignore')]).toarray()
sims= cosine_similarity(queryvec,matr)[0]
indxs_sims = list(sims.argsort()[::-1])
titles_query = list(np.array(titles)[indxs_sims][:nmoviesperquery])
context['movies']= zip(titles_query,indxs_sims[:nmoviesperquery])
context['rates']=[1,2,3,4,5]
return render_to_response(
'books_recsys_app/query_results.html',
RequestContext(request, context))
函数开头data参数将存储输入的查询,该函数将使用它将查询转换为使用load_data命令已加载到内存中的模型所表示的 tf-idf 向量:
matr = cache.get('data')
titles = cache.get('titles')
model_tfidf = cache.get('model')
此外,矩阵(键:matr)和电影标题(键:titles)都是从缓存中检索出来的,以返回与查询向量相似的影片列表(参见第四章,网络挖掘技术以获取更多详细信息)。此外,请注意,如果缓存为空,模型(以及其他数据)将直接从该函数的第一个调用中在内存中创建和加载。例如,我们可以输入war作为查询,网站将返回与该查询最相似的影片(query_results.html):

如我们所见,我们有五部电影(在views.py文件的开头我们可以设置每个查询参数的电影数量:nmoviesperquery),其中大部分与战争相关。从这一页我们可以像以下章节中讨论的那样对电影进行评分。
看累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
评分系统
每个用户(登录状态下)可以通过点击电影结果页面电影标题旁边的评分值(1 到 5)来简单地评分。这个动作将触发views.py文件中的rate_movie函数(通过urls.py中的相应 URL):
def rate_movie(request):
data = request.GET
rate = data.get("vote")
movies,moviesindxs = zip(*literal_eval(data.get("movies")))
movie = data.get("movie")
movieindx = int(data.get("movieindx"))
#save movie rate
userprofile = None
if request.user.is_superuser:
return render_to_response(
'books_recsys_app/superusersignin.html', RequestContext(request))
elif request.user.is_authenticated() :
userprofile = UserProfile.objects.get(user=request.user)
else:
return render_to_response(
'books_recsys_app/pleasesignin.html', RequestContext(request))
if MovieRated.objects.filter(movie=movie).filter(user=userprofile).exists():
mr = MovieRated.objects.get(movie=movie,user=userprofile)
mr.value = int(rate)
mr.save()
else:
mr = MovieRated()
mr.user = userprofile
mr.value = int(rate)
mr.movie = movie
mr.movieindx = movieindx
mr.save()
userprofile.save()
#get back the remaining movies
movies = RemoveFromList(movies,movie)
moviesindxs = RemoveFromList(moviesindxs,movieindx)
print movies
context = {}
context["movies"] = zip(movies,moviesindxs)
context["rates"] = [1,2,3,4,5]
return render_to_response(
'books_recsys_app/query_results.html',
RequestContext(request, context))
函数将电影的rate评分存储在MovieRated模型的对象中,并更新用户的相应电影rate向量(通过userprofile.save())。然后,未评分的电影将被发送回query_results.html页面。请注意,用户需要登录才能评分电影,否则将显示要求用户登录的异常事件(页面:pleasesignin.html)。
推荐系统
此函数将使用在views.py文件开头设置的参数:
nminimumrates=5
numrecs=5
recmethod = 'loglikelihood'
这定义了在获得推荐之前需要评分的最少电影数量,向用户显示的推荐数量,以及推荐系统方法。要显示推荐,用户可以点击顶部栏上的推荐按钮:

此操作将触发views.py文件中的movies_recs函数(通过urls.py文件中定义的相应 URL):
def movies_recs(request):
userprofile = None
if request.user.is_superuser:
return render_to_response(
'books_recsys_app/superusersignin.html', RequestContext(request))
elif request.user.is_authenticated():
userprofile = UserProfile.objects.get(user=request.user)
else:
return render_to_response(
'books_recsys_app/pleasesignin.html', RequestContext(request))
ratedmovies=userprofile.ratedmovies.all()
context = {}
if len(ratedmovies)<nminimumrates:
context['nrates'] = len(ratedmovies)
context['nminimumrates']=nminimumrates
return render_to_response(
'books_recsys_app/underminimum.html', RequestContext(request, context))
u_vec = np.array(userprofile.array)
Umatrix = cache.get('umatrix')
movieslist = cache.get('titles')
#recommendation...
u_rec = None
if recmethod == 'cf_userbased':
u_rec = CF_userbased(u_vec,numrecs,Umatrix)
elif recmethod == 'cf_itembased':
cf_itembased = cache.get('cf_itembased')
if cf_itembased == None:
cf_itembased = CF_itembased(Umatrix)
u_rec = cf_itembased.CalcRatings(u_vec,numrecs)
elif recmethod == 'loglikelihood':
llr = cache.get('loglikelihood')
if llr == None:
llr = LogLikelihood(Umatrix,movieslist)
u_rec = llr.GetRecItems(u_vec,True)
#save last recs
userprofile.save(recsvec=u_rec)
context['recs'] = list(np.array(movieslist)[list(u_rec)][:numrecs])
return render_to_response(
'books_recsys_app/recommendations.html',
RequestContext(request, context))
函数将从相应的UserProfile对象检索评分的电影向量,并从缓存中加载指定的推荐系统方法(由recmethod参数指定)。推荐首先存储在userprofile对象中,然后返回到recommendations.html页面。例如,使用cf_itembased方法:

这是在对与单词war相关的五部电影进行评分后的一个示例结果页面(参见前面的截图)。读者可以进一步调整参数和不同的算法来评估差异。
管理界面和 API
为了管理应用程序的数据,可以设置管理界面和 API 点。从管理面板中,我们可以看到电影数据和注册用户,编写以下admin.py文件:
from django.contrib import admin
from books_recsys_app.models import MovieData,UserProfile
class MoviesAdmin(admin.ModelAdmin):
list_display = ['title', 'description']
admin.site.register(UserProfile)
admin.site.register(MovieData,MoviesAdmin)
在urls.py文件中设置相应的admin URL 后:
url(r'^admin/', include(admin.site.urls))
我们应该看到我们的管理面板(在http://localhost:8000/admin/),其中模型和数据与admin.py文件中指定的字段相似:

要设置 API 端点以检索每个注册用户的资料,首先我们需要编写serializers.py,指定我们想要使用的UserProfile模型的哪些字段:
from books_recsys_app.models import UserProfile
from rest_framework import serializers
class UsersSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = UserProfile
fields = ('name', 'arrayratedmoviesindxs','lastrecs')
在这种情况下,我们想要收集用户评分的电影 ID 以及他最后推荐的电影的 ID。然后,在api.py文件中设置 API 如下:
from rest_framework import generics
from rest_framework.permissions import AllowAny
from rest_framework.pagination import PageNumberPagination
from books_recsys_app.serializers import UsersSerializer
from books_recsys_app.models import UserProfile
class LargeResultsSetPagination(PageNumberPagination):
page_size = 1000
page_size_query_param = 'page_size'
max_page_size = 10000
class UsersList(generics.ListAPIView):
serializer_class = UsersSerializer
permission_classes = (AllowAny,)
pagination_class = LargeResultsSetPagination
def get_queryset(self):
query = self.request.query_params.get
if query('name'):
return UserProfile.objects.filter(name=query('name'))
else:
return UserProfile.objects.all()
注意,如果只想收集特定用户的资料,允许查询参数name。在urls.py文件中设置相应的 URL 后:
url(r'^users-list/',UsersList.as_view(),name='users-list')
可以通过终端使用curl命令调用端点:
curl -X GET localhost:8000/users-list/
它也可以通过 swagger 界面进行测试调用(参见第六章,Django 基础:一个简单的 Web 框架)。
摘要
我们刚刚展示了如何使用 Django 框架构建一个推荐电影的程序。你现在应该对如何使用 Python 和其背后的机器学习算法开发一个专业的网络应用程序有了一定的信心。
在下一章中,我们将通过一个关于电影网络情感接受的额外示例,让你对如何高效地使用 Python 编写自己的机器学习网络应用程序有更深入的理解。
第八章. 电影评论情感分析应用
在本章中,我们描述了一个应用,该应用使用本书中描述的算法和方法来确定电影评论的情感。此外,我们将使用Scrapy库通过搜索引擎 API(必应搜索引擎)从不同网站收集评论。电影评论的文本和标题使用 newspaper 库或遵循 HTML 格式页面的某些预定义提取规则进行提取。每个评论的情感是通过在最有信息量的单词上使用朴素贝叶斯分类器(使用 X 2度量)来确定的,这与第四章中提到的网络挖掘技术相同。此外,使用在第四章中讨论的 PageRank 算法(网络挖掘技术)计算与每个电影查询相关的每个页面的排名,以确保完整性。本章将讨论构建应用所使用的代码,包括 Django 模型和视图,以及 Scrapy 爬虫用于从电影评论的网页中收集数据。我们首先给出一个网络应用的示例,并解释所使用的搜索引擎 API 以及如何将其包含在应用中。然后,我们描述如何收集电影评论,将 Scrapy 库集成到 Django 中,存储数据的模型以及管理应用的主要命令。本章中讨论的所有代码都可在作者的 GitHub 仓库中找到,位于chapter_8文件夹内,网址为github.com/ai2010/machine_learning_for_the_web/tree/master/chapter_8。
应用使用概述
主网页如下:

用户可以输入电影名称,如果他们想知道评论的情感和相关性。例如,我们在以下截图中搜索蝙蝠侠大战超人 正义黎明:

该应用从必应搜索引擎收集并爬取了 18 条评论,并使用 Scrapy 库分析了它们的情感(15 条正面评论和 3 条负面评论)。所有数据都存储在 Django 模型中,准备好使用 PageRank 算法计算每个页面的相关性(如前一个截图中所见页面底部的链接)。在这种情况下,使用 PageRank 算法,我们有以下结果:

这是一个与我们电影评论搜索最相关的页面列表,在爬虫爬取器上设置深度参数为 2(有关更多详细信息,请参阅以下章节)。请注意,为了在页面相关性上获得良好的结果,您需要爬取数千个页面(前面的截图显示了大约 50 个爬取的页面)。
要编写应用程序,我们像往常一样启动服务器(参见第六章,Django 入门,和第七章,电影推荐系统 Web 应用程序)以及 Django 的主要应用程序。首先,我们创建一个文件夹来存储所有代码,movie_reviews_analyzer_app,然后我们使用以下命令初始化 Django:
mkdir movie_reviews_analyzer_app
cd movie_reviews_analyzer_app
django-admin startproject webmining_server
python manage.py startapp startapp pages
我们在.py文件中设置设置,就像在第六章,Django 入门,和第七章,电影推荐系统 Web 应用程序的设置部分以及应用程序设置部分所做的那样(当然,在这种情况下,名称是webmining_server而不是server_movierecsys)。
情感分析应用程序的主要视图位于主webmining_server文件夹中的.py文件,而不是我们之前所用的app(页面)文件夹中(参见第六章,Django 入门,和第七章,电影推荐系统 Web 应用程序),因为现在的功能更多地涉及到服务器的通用功能,而不是特定应用程序(页面)。
使网络服务可操作的最后一步是创建一个superuser账户并启动服务器:
python manage.py createsuperuser (admin/admin)
python manage.py runserver
现在已经解释了应用程序的结构,我们可以更详细地讨论不同部分,从用于收集 URL 的搜索引擎 API 开始。
搜索引擎选择和应用程序代码
由于直接从最相关的搜索引擎如 Google、Bing、Yahoo 等抓取数据违反了它们的条款服务,我们需要从它们的 REST API(使用如 Crawlera 等抓取服务crawlera.com/,也是可能的)获取初始审查页面。我们决定使用 Bing 服务,该服务每月免费提供 5,000 次查询。
为了做到这一点,我们注册到 Microsoft 服务以获取允许搜索所需的密钥。简而言之,我们遵循了以下步骤:
-
在
datamarket.azure.com上在线注册。 -
在我的账户中,获取主账户密钥。
-
在开发者 | 注册下注册一个新应用程序(将重定向 URI设置为
https://www.bing.com)
之后,我们可以编写一个函数,检索与我们查询相关的尽可能多的 URL:
num_reviews = 30
def bing_api(query):
keyBing = API_KEY # get Bing key from: https://datamarket.azure.com/account/keys
credentialBing = 'Basic ' + (':%s' % keyBing).encode('base64')[:-1] # the "-1" is to remove the trailing "\n" which encode adds
searchString = '%27X'+query.replace(" ",'+')+'movie+review%27'
top = 50#maximum allowed by Bing
reviews_urls = []
if num_reviews<top:
offset = 0
url = 'https://api.datamarket.azure.com/Bing/Search/Web?' + \
'Query=%s&$top=%d&$skip=%d&$format=json' % (searchString, num_reviews, offset)
request = urllib2.Request(url)
request.add_header('Authorization', credentialBing)
requestOpener = urllib2.build_opener()
response = requestOpener.open(request)
results = json.load(response)
reviews_urls = [ d['Url'] for d in results['d']['results']]
else:
nqueries = int(float(num_reviews)/top)+1
for i in xrange(nqueries):
offset = top*i
if i==nqueries-1:
top = num_reviews-offset
url = 'https://api.datamarket.azure.com/Bing/Search/Web?' + \
'Query=%s&$top=%d&$skip=%d&$format=json' % (searchString, top, offset)
request = urllib2.Request(url)
request.add_header('Authorization', credentialBing)
requestOpener = urllib2.build_opener()
response = requestOpener.open(request)
else:
top=50
url = 'https://api.datamarket.azure.com/Bing/Search/Web?' + \
'Query=%s&$top=%d&$skip=%d&$format=json' % (searchString, top, offset)
request = urllib2.Request(url)
request.add_header('Authorization', credentialBing)
requestOpener = urllib2.build_opener()
response = requestOpener.open(request)
results = json.load(response)
reviews_urls += [ d['Url'] for d in results['d']['results']]
return reviews_urls
API_KEY 参数来自 Microsoft 账户,query 是一个字符串,用于指定电影名称,而 num_reviews = 30 是 Bing API 返回的总 URL 数量。有了包含评论的 URL 列表,我们现在可以设置一个爬虫,使用 Scrapy 从每个网页中提取标题和评论文本。
Scrapy 设置和应用程序代码
Scrapy 是一个 Python 库,用于从网页中提取内容或爬取指向给定网页的页面(有关更多详细信息,请参阅第四章中的Web 爬虫(或蜘蛛)部分,网络挖掘技术)。要安装库,请在终端中输入以下内容:
sudo pip install Scrapy
在bin文件夹中安装可执行文件:
sudo easy_install scrapy
从movie_reviews_analyzer_app文件夹中,我们初始化 Scrapy 项目如下:
scrapy startproject scrapy_spider
此命令将在scrapy_spider文件夹内创建以下目录结构:
├── __init__.py
├── items.py
├── pipelines.py
├── settings.py
├── spiders
├── spiders
│ ├── __init__.py
pipelines.py 和 items.py 文件管理爬取数据的存储和处理方式,它们将在蜘蛛和将 Django 与 Scrapy 集成部分中稍后讨论。settings.py 文件设置了spiders文件夹中定义的每个爬虫(或爬虫)使用的参数。在以下两个部分中,我们描述了在此应用程序中使用的主要参数和爬虫。
Scrapy 设置
settings.py 文件收集了 Scrapy 项目中每个爬虫用于爬取网页的所有参数。主要参数如下:
-
DEPTH_LIMIT: 在初始 URL 之后爬取的后续页面的数量。默认值为0,表示未设置限制。 -
LOG_ENABLED: 允许/拒绝 Scrapy 在执行时在终端上记录日志,默认值为 true。 -
ITEM_PIPELINES = {'scrapy_spider.pipelines.ReviewPipeline': 1000,}: 处理从每个网页中提取的数据的管道函数的路径。 -
CONCURRENT_ITEMS = 200: 在管道中处理的并发项目数量。 -
CONCURRENT_REQUESTS = 5000: Scrapy 处理的并发请求数量的最大值。 -
CONCURRENT_REQUESTS_PER_DOMAIN = 3000: Scrapy 为每个指定域名处理的并发请求数量的最大值。
深度越大,爬取的页面越多,因此爬取所需的时间也越长。为了加快过程,你可以将最后三个参数设置为高值。在此应用程序(spiders 文件夹中),我们设置了两个爬虫:一个用于从每个电影评论 URL 中提取数据(movie_link_results.py)的爬虫,以及一个用于生成指向初始电影评论 URL 的网页链接图(recursive_link_results.py)的爬虫。
爬虫
movie_link_results.py 上的爬虫如下所示:
from newspaper import Article
from urlparse import urlparse
from scrapy.selector import Selector
from scrapy import Spider
from scrapy.spiders import BaseSpider,CrawlSpider, Rule
from scrapy.http import Request
from scrapy_spider import settings
from scrapy_spider.items import PageItem,SearchItem
unwanted_domains = ['youtube.com','www.youtube.com']
from nltk.corpus import stopwords
stopwords = set(stopwords.words('english'))
def CheckQueryinReview(keywords,title,content):
content_list = map(lambda x:x.lower(),content.split(' '))
title_list = map(lambda x:x.lower(),title.split(' '))
words = content_list+title_list
for k in keywords:
if k in words:
return True
return False
class Search(Spider):
name = 'scrapy_spider_reviews'
def __init__(self,url_list,search_key):#specified by -a
self.search_key = search_key
self.keywords = [w.lower() for w in search_key.split(" ") if w not in stopwords]
self.start_urls =url_list.split(',')
super(Search, self).__init__(url_list)
def start_requests(self):
for url in self.start_urls:
yield Request(url=url, callback=self.parse_site,dont_filter=True)
def parse_site(self, response):
## Get the selector for xpath parsing or from newspaper
def crop_emptyel(arr):
return [u for u in arr if u!=' ']
domain = urlparse(response.url).hostname
a = Article(response.url)
a.download()
a.parse()
title = a.title.encode('ascii','ignore').replace('\n','')
sel = Selector(response)
if title==None:
title = sel.xpath('//title/text()').extract()
if len(title)>0:
title = title[0].encode('utf-8').strip().lower()
content = a.text.encode('ascii','ignore').replace('\n','')
if content == None:
content = 'none'
if len(crop_emptyel(sel.xpath('//div//article//p/text()').extract()))>1:
contents = crop_emptyel(sel.xpath('//div//article//p/text()').extract())
print 'divarticle'
….
elif len(crop_emptyel(sel.xpath('/html/head/meta[@name="description"]/@content').extract()))>0:
contents = crop_emptyel(sel.xpath('/html/head/meta[@name="description"]/@content').extract())
content = ' '.join([c.encode('utf-8') for c in contents]).strip().lower()
#get search item
search_item = SearchItem.django_model.objects.get(term=self.search_key)
#save item
if not PageItem.django_model.objects.filter(url=response.url).exists():
if len(content) > 0:
if CheckQueryinReview(self.keywords,title,content):
if domain not in unwanted_domains:
newpage = PageItem()
newpage['searchterm'] = search_item
newpage['title'] = title
newpage['content'] = content
newpage['url'] = response.url
newpage['depth'] = 0
newpage['review'] = True
#newpage.save()
return newpage
else:
return null
我们可以看到,scrapy中的Spider类被Search类继承,并且必须定义以下标准方法以覆盖标准方法:
-
__init__:蜘蛛的构造函数需要定义包含从其提取内容的 URL 的start_urls列表。此外,我们还有自定义变量,如search_key和keywords,它们存储与在搜索引擎 API 上使用的电影标题查询相关的信息。 -
start_requests:当调用spider时,此函数被触发,并声明对start_urls列表中的每个 URL 要执行的操作;对于每个 URL,将调用自定义的parse_site函数(而不是默认的parse函数)。 -
parse_site:这是一个自定义函数,用于解析每个 URL 的数据。为了提取评论的标题及其文本内容,我们使用了 newspaper 库(sudo pip install newspaper),或者在失败的情况下,直接使用一些定义的规则解析 HTML 文件,以避免因不想要的标签而产生的噪声(每个规则结构都是使用sel.xpath命令定义的)。为了达到这个结果,我们选择了某些流行的域名(如rottentomatoes、cnn等),并确保解析能够从这些网站上提取内容(前述代码中没有显示所有提取规则,但它们可以在 GitHub 文件中找到)。然后,使用相关的 Scrapy 项和ReviewPipeline函数将数据存储在页面Django模型中(见下一节)。 -
CheckQueryinReview:这是一个自定义函数,用于检查电影标题(来自查询)是否包含在每个网页的内容或标题中。
要运行蜘蛛,我们需要在 scrapy_spider(内部)文件夹中输入以下命令:
scrapy crawl scrapy_spider_reviews -a url_list=listname -a search_key=keyname
管道
管道定义了当蜘蛛抓取新页面时应该执行的操作。在前面的例子中,parse_site 函数返回一个 PageItem 对象,这触发了以下管道(pipelines.py):
class ReviewPipeline(object):
def process_item(self, item, spider):
#if spider.name == 'scrapy_spider_reviews':#not working
item.save()
return item
此类简单地保存每个项目(在蜘蛛的术语中,即新页面)。
爬虫
正如我们在概述(前述章节)中所示,在存储从评论的 URL 开始的链接页面之后,使用 PageRank 算法计算评论的相关性。执行此操作的爬虫是 recursive_link_results.py:
#from scrapy.spider import Spider
from scrapy.selector import Selector
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.http import Request
from scrapy_spider.items import PageItem,LinkItem,SearchItem
class Search(CrawlSpider):
name = 'scrapy_spider_recursive'
def __init__(self,url_list,search_id):#specified by -a
#REMARK is allowed_domains is not set then ALL are allowed!!!
self.start_urls = url_list.split(',')
self.search_id = int(search_id)
#allow any link but the ones with different font size(repetitions)
self.rules = (
Rule(LinkExtractor(allow=(),deny=('fontSize=*','infoid=*','SortBy=*', ),unique=True), callback='parse_item', follow=True),
)
super(Search, self).__init__(url_list)
def parse_item(self, response):
sel = Selector(response)
## Get meta info from website
title = sel.xpath('//title/text()').extract()
if len(title)>0:
title = title[0].encode('utf-8')
contents = sel.xpath('/html/head/meta[@name="description"]/@content').extract()
content = ' '.join([c.encode('utf-8') for c in contents]).strip()
fromurl = response.request.headers['Referer']
tourl = response.url
depth = response.request.meta['depth']
#get search item
search_item = SearchItem.django_model.objects.get(id=self.search_id)
#newpage
if not PageItem.django_model.objects.filter(url=tourl).exists():
newpage = PageItem()
newpage['searchterm'] = search_item
newpage['title'] = title
newpage['content'] = content
newpage['url'] = tourl
newpage['depth'] = depth
newpage.save()#cant use pipeline cause the execution can finish here
#get from_id,to_id
from_page = PageItem.django_model.objects.get(url=fromurl)
from_id = from_page.id
to_page = PageItem.django_model.objects.get(url=tourl)
to_id = to_page.id
#newlink
if not LinkItem.django_model.objects.filter(from_id=from_id).filter(to_id=to_id).exists():
newlink = LinkItem()
newlink['searchterm'] = search_item
newlink['from_id'] = from_id
newlink['to_id'] = to_id
newlink.save()
scrapy 中的 CrawlSpider 类继承自 Search 类,必须定义以下标准方法以覆盖标准方法(类似于蜘蛛的情况):
-
__init__:这是类的构造函数。start_urls参数定义了蜘蛛开始爬取的起始 URL,直到达到DEPTH_LIMIT值。rules参数设置允许/拒绝爬取的 URL 类型(在这种情况下,忽略具有不同字体大小的相同页面)并定义了用于操作每个检索到的页面的函数(parse_item)。此外,定义了一个自定义变量search_id,它需要存储查询的 ID,以便在其它数据中存储。 -
parse_item: 这是一个被调用来存储从每个检索到的页面中提取的重要数据的自定义函数。为每个页面创建一个新的 DjangoPage模型项(见下文章节),其中包含页面的标题和内容(使用xpathHTML 解析器)。为了执行 PageRank 算法,将链接到每个页面的页面和页面本身的关系保存为Link模型的对象,使用相关的 Scrapy 项(见下文章节)。
要运行爬虫,我们需要在(内部)scrapy_spider 文件夹中输入以下命令:
scrapy crawl scrapy_spider_recursive -a url_list=listname -a search_id=keyname
Django 模型
使用蜘蛛收集的数据需要存储在数据库中。在 Django 中,数据库表被称为模型,并在 models.py 文件中定义(位于 pages 文件夹内)。该文件的内容如下:
from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
class SearchTerm(models.Model):
term = models.CharField(_('search'), max_length=255)
num_reviews = models.IntegerField(null=True,default=0)
#display term on admin panel
def __unicode__(self):
return self.term
class Page(models.Model):
searchterm = models.ForeignKey(SearchTerm, related_name='pages',null=True,blank=True)
url = models.URLField(_('url'), default='', blank=True)
title = models.CharField(_('name'), max_length=255)
depth = models.IntegerField(null=True,default=-1)
html = models.TextField(_('html'),blank=True, default='')
review = models.BooleanField(default=False)
old_rank = models.FloatField(null=True,default=0)
new_rank = models.FloatField(null=True,default=1)
content = models.TextField(_('content'),blank=True, default='')
sentiment = models.IntegerField(null=True,default=100)
class Link(models.Model):
searchterm = models.ForeignKey(SearchTerm, related_name='links',null=True,blank=True)
from_id = models.IntegerField(null=True)
to_id = models.IntegerField(null=True)
在应用程序的首页键入的每个电影标题存储在 SearchTerm 模型中,而每个网页的数据则收集在 Page 模型的对象中。除了内容字段(HTML、标题、URL、内容)外,还记录了评论的情感和图网络中的深度(布尔值也指示网页是否是电影评论页面或仅仅是链接页面)。Link 模型存储了页面之间的所有图链接,然后这些链接被 PageRank 算法用来计算评论网页的相关性。请注意,Page 模型和 Link 模型都通过外键与相关的 SearchTerm 相关联。像往常一样,为了将这些模型作为数据库表编写,我们输入以下命令:
python manage.py makemigrations
python manage.py migrate
为了填充这些 Django 模型,我们需要让 Scrapy 与 Django 交互,这是下文章节的主题。
整合 Django 与 Scrapy
为了使路径易于调用,我们移除了外部的 scrapy_spider 文件夹,这样在 movie_reviews_analyzer_app 中,webmining_server 文件夹与 scrapy_spider 文件夹处于同一级别:
├── db.sqlite3
├── scrapy.cfg
├── scrapy_spider
│ ├── ...
│ ├── spiders
│ │ ...
└── webmining_server
我们将 Django 路径设置到 Scrapy 的 settings.py 文件中:
# Setting up django's project full path.
import sys
sys.path.insert(0, BASE_DIR+'/webmining_server')
# Setting up django's settings module name.
os.environ['DJANGO_SETTINGS_MODULE'] = 'webmining_server.settings'
#import django to load models(otherwise AppRegistryNotReady: Models aren't loaded yet):
import django
django.setup()
现在,我们可以安装允许从 Scrapy 管理 Django 模型的库:
sudo pip install scrapy-djangoitem
在 items.py 文件中,我们按照以下方式编写 Django 模型和 Scrapy 项之间的链接:
from scrapy_djangoitem import DjangoItem
from pages.models import Page,Link,SearchTerm
class SearchItem(DjangoItem):
django_model = SearchTerm
class PageItem(DjangoItem):
django_model = Page
class LinkItem(DjangoItem):
django_model = Link
每个类都继承自 DjangoItem 类,这样使用 django_model 变量声明的原始 Django 模型将自动链接。Scrapy 项目现在已完成,我们可以继续讨论解释处理 Scrapy 提取的数据的 Django 代码以及管理应用程序所需的 Django 命令。
命令(情感分析模型和删除查询)
应用程序需要管理一些不允许最终用户执行的操作,例如定义情感分析模型和删除电影查询以便重新执行,而不是从内存中检索现有数据。以下章节将解释执行这些操作的命令。
情感分析模型加载器
本应用程序的最终目标是确定电影评论的情感(正面或负面)。为了实现这一点,必须使用一些外部数据构建一个情感分类器,然后将其存储在内存(缓存)中,以便每个查询请求使用。这就是以下显示的load_sentimentclassifier.py命令的目的:
import nltk.classify.util, nltk.metrics
from nltk.classify import NaiveBayesClassifier
from nltk.corpus import movie_reviews
from nltk.corpus import stopwords
from nltk.collocations import BigramCollocationFinder
from nltk.metrics import BigramAssocMeasures
from nltk.probability import FreqDist, ConditionalFreqDist
import collections
from django.core.management.base import BaseCommand, CommandError
from optparse import make_option
from django.core.cache import cache
stopwords = set(stopwords.words('english'))
method_selfeatures = 'best_words_features'
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('-n', '--num_bestwords',
dest='num_bestwords', type='int',
action='store',
help=('number of words with high information')),)
def handle(self, *args, **options):
num_bestwords = options['num_bestwords']
self.bestwords = self.GetHighInformationWordsChi(num_bestwords)
clf = self.train_clf(method_selfeatures)
cache.set('clf',clf)
cache.set('bestwords',self.bestwords)
在文件开头,变量method_selfeatures设置了用于训练分类器train_clf的特征选择方法(在这种情况下,特征是评论中的单词;详见第四章,网络挖掘技术,以获取更多详细信息)。最佳单词(特征)的最大数量由输入参数num_bestwords定义。然后,分类器和最佳特征(bestwords)存储在缓存中,以便应用程序(使用cache模块)使用。分类器和选择最佳单词(特征)的方法如下:
def train_clf(method):
negidxs = movie_reviews.fileids('neg')
posidxs = movie_reviews.fileids('pos')
if method=='stopword_filtered_words_features':
negfeatures = [(stopword_filtered_words_features(movie_reviews.words(fileids=[file])), 'neg') for file in negidxs]
posfeatures = [(stopword_filtered_words_features(movie_reviews.words(fileids=[file])), 'pos') for file in posidxs]
elif method=='best_words_features':
negfeatures = [(best_words_features(movie_reviews.words(fileids=[file])), 'neg') for file in negidxs]
posfeatures = [(best_words_features(movie_reviews.words(fileids=[file])), 'pos') for file in posidxs]
elif method=='best_bigrams_words_features':
negfeatures = [(best_bigrams_words_features(movie_reviews.words(fileids=[file])), 'neg') for file in negidxs]
posfeatures = [(best_bigrams_words_features(movie_reviews.words(fileids=[file])), 'pos') for file in posidxs]
trainfeatures = negfeatures + posfeatures
clf = NaiveBayesClassifier.train(trainfeatures)
return clf
def stopword_filtered_words_features(self,words):
return dict([(word, True) for word in words if word not in stopwords])
#eliminate Low Information Features
def GetHighInformationWordsChi(self,num_bestwords):
word_fd = FreqDist()
label_word_fd = ConditionalFreqDist()
for word in movie_reviews.words(categories=['pos']):
word_fd[word.lower()] +=1
label_word_fd['pos'][word.lower()] +=1
for word in movie_reviews.words(categories=['neg']):
word_fd[word.lower()] +=1
label_word_fd['neg'][word.lower()] +=1
pos_word_count = label_word_fd['pos'].N()
neg_word_count = label_word_fd['neg'].N()
total_word_count = pos_word_count + neg_word_count
word_scores = {}
for word, freq in word_fd.iteritems():
pos_score = BigramAssocMeasures.chi_sq(label_word_fd['pos'][word],
(freq, pos_word_count), total_word_count)
neg_score = BigramAssocMeasures.chi_sq(label_word_fd['neg'][word],
(freq, neg_word_count), total_word_count)
word_scores[word] = pos_score + neg_score
best = sorted(word_scores.iteritems(), key=lambda (w,s): s, reverse=True)[:num_bestwords]
bestwords = set([w for w, s in best])
return bestwords
def best_words_features(self,words):
return dict([(word, True) for word in words if word in self.bestwords])
def best_bigrams_word_features(self,words, measure=BigramAssocMeasures.chi_sq, nbigrams=200):
bigram_finder = BigramCollocationFinder.from_words(words)
bigrams = bigram_finder.nbest(measure, nbigrams)
d = dict([(bigram, True) for bigram in bigrams])
d.update(best_words_features(words))
return d
在前面的代码中,编写了三种方法来选择单词:
-
stopword_filtered_words_features:使用自然语言工具包(NLTK)的并列词列表消除stopwords,并将剩余的视为相关单词 -
best_words_features:使用X²度量(NLTK库),选择与正面或负面评论相关的最有信息量的单词(详见第四章,网络挖掘技术,以获取更多详细信息) -
best_bigrams_word_features:使用X²度量(NLTK库)从单词集合中找到 200 个最有信息量的二元组(详见第四章,网络挖掘技术,以获取更多详细信息)
选择的分类器是朴素贝叶斯算法(见第三章,监督机器学习),并且标记的文本(正面、负面情感)来自NLTK.corpus中的movie_reviews。要安装它,请在 Python 中打开一个终端并从corpus安装movie_reviews:
nltk.download()--> corpora/movie_reviews corpus
删除已执行过的查询
由于我们可以指定不同的参数(例如特征选择方法、最佳单词数量等),我们可能希望再次执行并存储具有不同值的评论的情感。为此需要delete_query命令,如下所示:
from pages.models import Link,Page,SearchTerm
from django.core.management.base import BaseCommand, CommandError
from optparse import make_option
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('-s', '--searchid',
dest='searchid', type='int',
action='store',
help=('id of the search term to delete')),)
def handle(self, *args, **options):
searchid = options['searchid']
if searchid == None:
print "please specify searchid: python manage.py --searchid=--"
#list
for sobj in SearchTerm.objects.all():
print 'id:',sobj.id," term:",sobj.term
else:
print 'delete...'
search_obj = SearchTerm.objects.get(id=searchid)
pages = search_obj.pages.all()
pages.delete()
links = search_obj.links.all()
links.delete()
search_obj.delete()
如果在不指定searchid(查询 ID)的情况下运行该命令,将显示所有查询及其相关 ID 的列表。之后,我们可以通过键入以下内容来选择我们想要删除的查询:
python manage.py delete_query --searchid=VALUE
我们可以使用缓存的情感分析模型来向用户展示所选电影的在线情感,正如我们在下一节中解释的那样。
情感评论分析器 – Django 视图和 HTML
本章中解释的大多数代码(命令、必应搜索引擎、Scrapy 和 Django 模型)都用于views.py中的analyzer函数,以支持应用使用概述部分显示的首页(在urls.py文件中声明 URL 为url(r'^$','webmining_server.views.analyzer')之后)。
def analyzer(request):
context = {}
if request.method == 'POST':
post_data = request.POST
query = post_data.get('query', None)
if query:
return redirect('%s?%s' % (reverse('webmining_server.views.analyzer'),
urllib.urlencode({'q': query})))
elif request.method == 'GET':
get_data = request.GET
query = get_data.get('q')
if not query:
return render_to_response(
'movie_reviews/home.html', RequestContext(request, context))
context['query'] = query
stripped_query = query.strip().lower()
urls = []
if test_mode:
urls = parse_bing_results()
else:
urls = bing_api(stripped_query)
if len(urls)== 0:
return render_to_response(
'movie_reviews/noreviewsfound.html', RequestContext(request, context))
if not SearchTerm.objects.filter(term=stripped_query).exists():
s = SearchTerm(term=stripped_query)
s.save()
try:
#scrape
cmd = 'cd ../scrapy_spider & scrapy crawl scrapy_spider_reviews -a url_list=%s -a search_key=%s' %('\"'+str(','.join(urls[:num_reviews]).encode('utf-8'))+'\"','\"'+str(stripped_query)+'\"')
os.system(cmd)
except:
print 'error!'
s.delete()
else:
#collect the pages already scraped
s = SearchTerm.objects.get(term=stripped_query)
#calc num pages
pages = s.pages.all().filter(review=True)
if len(pages) == 0:
s.delete()
return render_to_response(
'movie_reviews/noreviewsfound.html', RequestContext(request, context))
s.num_reviews = len(pages)
s.save()
context['searchterm_id'] = int(s.id)
#train classifier with nltk
def train_clf(method):
...
def stopword_filtered_words_features(words):
...
#Eliminate Low Information Features
def GetHighInformationWordsChi(num_bestwords):
...
bestwords = cache.get('bestwords')
if bestwords == None:
bestwords = GetHighInformationWordsChi(num_bestwords)
def best_words_features(words):
...
def best_bigrams_words_features(words, measure=BigramAssocMeasures.chi_sq, nbigrams=200):
...
clf = cache.get('clf')
if clf == None:
clf = train_clf(method_selfeatures)
cntpos = 0
cntneg = 0
for p in pages:
words = p.content.split(" ")
feats = best_words_features(words)#bigram_word_features(words)#stopword_filtered_word_feats(words)
#print feats
str_sent = clf.classify(feats)
if str_sent == 'pos':
p.sentiment = 1
cntpos +=1
else:
p.sentiment = -1
cntneg +=1
p.save()
context['reviews_classified'] = len(pages)
context['positive_count'] = cntpos
context['negative_count'] = cntneg
context['classified_information'] = True
return render_to_response(
'movie_reviews/home.html', RequestContext(request, context))
插入的电影标题存储在query变量中,并传递给bing_api函数以收集评论的 URL。然后调用 Scrapy 抓取评论文本,这些文本使用clf分类器模型进行处理,并从缓存中检索出选定的最有信息量的单词(bestwords)。在这种情况下,如果缓存为空,则再次生成相同的模型)。然后,将预测的评论情感计数(positive_counts、negative_counts和reviews_classified)发送回home.html(templates文件夹)页面,该页面使用以下 Google 饼图代码:
<h2 align = Center>Movie Reviews Sentiment Analysis</h2>
<div class="row">
<p align = Center><strong>Reviews Classified : {{ reviews_classified }}</strong></p>
<p align = Center><strong>Positive Reviews : {{ positive_count }}</strong></p>
<p align = Center><strong> Negative Reviews : {{ negative_count }}</strong></p>
</div>
<section>
<script type="text/javascript" src="img/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1", {packages:["corechart"]});
google.setOnLoadCallback(drawChart);
function drawChart() {
var data = google.visualization.arrayToDataTable([
['Sentiment', 'Number'],
['Positive', {{ positive_count }}],
['Negative', {{ negative_count }}]
]);
var options = { title: 'Sentiment Pie Chart'};
var chart = new google.visualization.PieChart(document.getElementById('piechart'));
chart.draw(data, options);
}
</script>
<p align ="Center" id="piechart" style="width: 900px; height: 500px;display: block; margin: 0 auto;text-align: center;" ></p>
</div>
函数drawChart调用 Google 的PieChart可视化函数,该函数以数据(正面和负面的计数)作为输入来创建饼图。要了解更多关于 HTML 代码如何与 Django 视图交互的细节,请参阅第六章,开始使用 Django,在HTML 网页背后的 URL 和视图部分。从带有情感计数的结果页面(参见应用使用概述部分),可以使用页面底部的两个链接之一计算抓取评论的 PageRank 相关性。该操作的 Django 代码将在下一节中讨论。
PageRank:Django 视图和算法代码
为了对在线评论的重要性进行排序,我们在应用中实现了 PageRank 算法(参见第四章,网络挖掘技术,在排名:PageRank 算法部分),其实现如下:
from pages.models import Page,SearchTerm
num_iterations = 100000
eps=0.0001
D = 0.85
def pgrank(searchid):
s = SearchTerm.objects.get(id=int(searchid))
links = s.links.all()
from_idxs = [i.from_id for i in links ]
# Find the idxs that receive page rank
links_received = []
to_idxs = []
for l in links:
from_id = l.from_id
to_id = l.to_id
if from_id not in from_idxs: continue
if to_id not in from_idxs: continue
links_received.append([from_id,to_id])
if to_id not in to_idxs: to_idxs.append(to_id)
pages = s.pages.all()
prev_ranks = dict()
for node in from_idxs:
ptmp = Page.objects.get(id=node)
prev_ranks[node] = ptmp.old_rank
conv=1.
cnt=0
while conv>eps or cnt<num_iterations:
next_ranks = dict()
total = 0.0
for (node,old_rank) in prev_ranks.items():
total += old_rank
next_ranks[node] = 0.0
#find the outbound links and send the pagerank down to each of them
for (node, old_rank) in prev_ranks.items():
give_idxs = []
for (from_id, to_id) in links_received:
if from_id != node: continue
if to_id not in to_idxs: continue
give_idxs.append(to_id)
if (len(give_idxs) < 1): continue
amount = D*old_rank/len(give_idxs)
for id in give_idxs:
next_ranks[id] += amount
tot = 0
for (node,next_rank) in next_ranks.items():
tot += next_rank
const = (1-D)/ len(next_ranks)
for node in next_ranks:
next_ranks[node] += const
tot = 0
for (node,old_rank) in next_ranks.items():
tot += next_rank
difftot = 0
for (node, old_rank) in prev_ranks.items():
new_rank = next_ranks[node]
diff = abs(old_rank-new_rank)
difftot += diff
conv= difftot/len(prev_ranks)
cnt+=1
prev_ranks = next_ranks
for (id,new_rank) in next_ranks.items():
ptmp = Page.objects.get(id=id)
url = ptmp.url
for (id,new_rank) in next_ranks.items():
ptmp = Page.objects.get(id=id)
ptmp.old_rank = ptmp.new_rank
ptmp.new_rank = new_rank
ptmp.save()
此代码获取与给定的SearchTerm对象关联的所有链接存储,并为每个页面i在时间t的 PageRank 分数实现,其中P(i)由以下递归方程给出:

在这里,N 是页面的总数,如果页面 j 指向 i,则
( N[j] * 是页面 j 的出链数量);否则,N* 是 0。参数 D 是所谓的 阻尼因子(在前面的代码中设置为 0.85),它表示跟随由转换矩阵 A 给出的转换的概率。方程会迭代,直到满足收敛参数 eps 或达到最大迭代次数 num_iterations。算法通过点击 home.html 页面底部的 抓取并计算页面排名(可能需要很长时间) 或 计算页面排名 链接来调用。该链接链接到 views.py 中的 pgrank_view 函数(通过在 urls.py 中声明的 URL:url(r'^pg-rank/(?P<pk>\d+)/','webmining_server.views.pgrank_view', name='pgrank_view')):
def pgrank_view(request,pk):
context = {}
get_data = request.GET
scrape = get_data.get('scrape','False')
s = SearchTerm.objects.get(id=pk)
if scrape == 'True':
pages = s.pages.all().filter(review=True)
urls = []
for u in pages:
urls.append(u.url)
#crawl
cmd = 'cd ../scrapy_spider & scrapy crawl scrapy_spider_recursive -a url_list=%s -a search_id=%s' %('\"'+str(','.join(urls[:]).encode('utf-8'))+'\"','\"'+str(pk)+'\"')
os.system(cmd)
links = s.links.all()
if len(links)==0:
context['no_links'] = True
return render_to_response(
'movie_reviews/pg-rank.html', RequestContext(request, context))
#calc pgranks
pgrank(pk)
#load pgranks in descending order of pagerank
pages_ordered = s.pages.all().filter(review=True).order_by('-new_rank')
context['pages'] = pages_ordered
return render_to_response(
'movie_reviews/pg-rank.html', RequestContext(request, context))
这段代码调用爬虫收集所有链接到评论的页面,并使用前面讨论过的代码计算 PageRank 分数。然后,这些分数按页面排名分数降序显示在 pg-rank.html 页面上(正如我们在本章的 应用使用概述 部分所展示的)。由于这个函数可能需要很长时间来处理(爬取数千个页面),因此已经编写了 run_scrapelinks.py 命令来运行 Scrapy 爬虫(读者被邀请阅读或修改脚本作为练习)。
管理和 API
作为本章的最后一部分,我们简要描述了一些可能的模型管理方法和实现 API 端点以检索应用程序处理的数据。在 pages 文件夹中,我们可以在 admin.py 文件中设置两个管理界面来检查 SearchTerm 和 Page 模型收集的数据:
from django.contrib import admin
from django_markdown.admin import MarkdownField, AdminMarkdownWidget
from pages.models import SearchTerm,Page,Link
class SearchTermAdmin(admin.ModelAdmin):
formfield_overrides = {MarkdownField: {'widget': AdminMarkdownWidget}}
list_display = ['id', 'term', 'num_reviews']
ordering = ['-id']
class PageAdmin(admin.ModelAdmin):
formfield_overrides = {MarkdownField: {'widget': AdminMarkdownWidget}}
list_display = ['id', 'searchterm', 'url','title','content']
ordering = ['-id','-new_rank']
admin.site.register(SearchTerm,SearchTermAdmin)
admin.site.register(Page,PageAdmin)
admin.site.register(Link)
注意,SearchTermAdmin 和 PageAdmin 都会显示按递减的 ID(在 PageAdmin 的情况下还有 new_rank)排列的对象。以下截图是一个示例:

注意,尽管不是必需的,Link 模型也已经包含在管理界面中(使用 admin.site.register(Link))。更有趣的是,我们可以设置一个 API 端点来检索与电影标题相关的情感计数。在页面文件夹内的 api.py 文件中,我们可以有如下代码:
from rest_framework import views,generics
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from pages.serializers import SearchTermSerializer
from pages.models import SearchTerm,Page
class LargeResultsSetPagination(PageNumberPagination):
page_size = 1000
page_size_query_param = 'page_size'
max_page_size = 10000
class SearchTermsList(generics.ListAPIView):
serializer_class = SearchTermSerializer
permission_classes = (AllowAny,)
pagination_class = LargeResultsSetPagination
def get_queryset(self):
return SearchTerm.objects.all()
class PageCounts(views.APIView):
permission_classes = (AllowAny,)
def get(self,*args, **kwargs):
searchid=self.kwargs['pk']
reviewpages = Page.objects.filter(searchterm=searchid).filter(review=True)
npos = len([p for p in reviewpages if p.sentiment==1])
nneg = len(reviewpages)-npos
return Response({'npos':npos,'nneg':nneg})
PageCounts 类接受搜索(电影的标题)的 ID 作为输入,并返回电影的评论的情感,即正面和负面的计数。要从电影的标题获取 SearchTerm 的 ID,你可以查看管理界面或使用其他 API 端点 SearchTermsList;这简单地返回与相关 ID 一起的电影标题列表。序列化器设置在 serializers.py 文件中:
from pages.models import SearchTerm
from rest_framework import serializers
class SearchTermSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SearchTerm
fields = ('id', 'term')
要调用这些端点,我们又可以再次使用 swagger 接口(见第六章,Django 入门),或者使用终端中的curl命令来执行这些调用。例如:
curl -X GET localhost:8000/search-list/
{"count":7,"next":null,"previous":null,"results":[{"id":24,"term":"the martian"},{"id":27,"term":"steve jobs"},{"id":29,"term":"suffragette"},{"id":39,"term":"southpaw"},{"id":40,"term":"vacation"},{"id":67,"term":"the revenant"},{"id":68,"term":"batman vs superman dawn of justice"}]}
和
curl -X GET localhost:8000/pages-sentiment/68/
{"nneg":3,"npos":15}
摘要
在本章中,我们描述了一个电影评论情感分析网络应用程序,让你熟悉我们在第三章,监督机器学习,第四章,网络挖掘技术,和第六章,Django 入门中讨论的一些算法和库。
这是一段旅程的结束:通过阅读这本书并尝试提供的代码,你应该已经获得了关于目前商业环境中使用的重要机器学习算法的显著实用知识。
现在,你应该已经准备好使用 Python 和一些机器学习算法开发自己的网络应用程序和想法了,这些算法是通过阅读这本书学到的。在现实世界中,今天存在许多具有挑战性的数据相关问题,等待那些能够掌握并应用这本书中讨论的材料的人来解决,而你,已经到达这个位置,无疑是这些人之一。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享




是一个隐藏变量,表示每个 *x^((i)) * 生成的来自高斯成分
代表高斯成分的均值参数集
代表高斯成分的方差参数集
是混合权重,表示随机选择的 *x^((i)) * 被高斯成分 k 生成的概率,其中
,而
是权重集
是与点 x^((i)) 相关的参数
的关联高斯成分 k
:



。
的聚类中的点):
。


: 高斯核
: Epanechnikov 核

,其中
分别是C1和C2的元素数量。
。在这种情况下,距离被替换为合并成本,其公式如下所示:
是给定簇分配的类别 *C^l * 的条件熵
是给定类别成员资格的簇的条件熵 




已除以标准差
,其中 

及其相关的特征向量 
,其中
是具有 N 行和 k 列的特征向量矩阵

(如果q=2,这会降低到欧几里得距离)
被称为似然分布
是后验分布
是先验分布
被称为证据


(其中
)
















。与
的最大值相关的部分序列是直到时间 t 的最可能部分序列。

和
在时间 t ,HMM 处于状态 i 之前的部分观察序列的概率,
和
在时间 t 给定状态为 i 的情况下,从时间 t 到 T-1 的部分序列的概率:
的值。
和 
其中
和
是克罗内克符号,当
时等于 1,否则为 0。
收敛。
其中 
是单词 j 在文档 I 中的标准化频率
是逆文档频率,*df[j] * 是包含单词 j 的网页数量


和 a 是长度为 K 的参数向量,使得
。
的多项式分布中抽取一个主题。
。
,其中 x 和 y 是两个向量的平均值。

(


(k=0)
(k>0)
浙公网安备 33010602011771号