Skip to content

Latest commit

 

History

History
602 lines (396 loc) · 37.6 KB

File metadata and controls

602 lines (396 loc) · 37.6 KB

二十七、将朴素贝叶斯用于社交媒体洞察

基于文本的数据集包含许多信息,无论它们是书籍,历史文档,社交媒体,电子邮件还是我们通过书面形式进行交流的任何其他方式。 从基于文本的数据集中提取特征并将其用于分类是一个难题。 但是,有一些常见的文本挖掘模式。

我们使用朴素贝叶斯算法(Naive Bayes algorithm)来查看社交媒体中的歧义术语,该算法是一种功能强大且令人惊讶的简单算法。 朴素贝叶斯采取了一些捷径来正确计算分类的概率,因此名称中的术语朴素。 它也可以很容易地扩展到其他类型的数据集,而不依赖于数字特征。 本章中的模型是文本挖掘研究的基线,因为该过程对于各种数据集都可以很好地运行。

我们将在本章介绍以下主题:

  • 从社交网络 API 下载数据
  • 文字变形金刚
  • 朴素贝叶斯分类器
  • 使用 JSON 保存和加载数据集
  • NLTK 库,用于从文本中提取特征
  • 评估的 F 量度

消除歧义

文本通常为,称为非结构化格式。 那里有很多信息,但那里只是; 没有标题,没有要求的格式,宽松的语法以及等其他问题,使得无法轻松地从文本中提取信息。 数据之间也是高度关联的,有很多提及和交叉引用-只是格式不便于我们提取!

我们可以将书籍中存储的信息与大型数据库中存储的信息进行比较,以了解两者之间的区别。 书中有人物,主题,地点和许多信息。 但是,需要阅读本书,更重要的是,要对其进行解释才能获得此信息。 数据库位于您的服务器上,具有列名和数据类型。 所有信息都在那里,并且所需的解释水平很低。 有关数据的信息(例如其类型或含义)称为元数据,而文本则缺少该信息。 一本书还包含一些表格形式的内容和索引元数据,但程度明显低于数据库。

问题之一是术语消除歧义。 当一个人使用银行这个词时,这是金融消息还是环境消息(例如河岸)? 在许多情况下,这种类型的歧义消除对于人类来说是很容易的(尽管仍然存在麻烦),但是对于计算机而言,消除歧义却要困难得多。

在本章中,我们将消除在 Twitter 流中使用 Python 一词的歧义。 Twitter 上的消息称为推文,并且限制为 140 个字符。 这意味着没有上下文的空间。 尽管通常使用井号来表示推文的主题,但可用的元数据很少。

当人们谈论 Python 时,他们可能在谈论以下事情:

  • 编程语言 Python
  • 经典喜剧团 Monty Python
  • 蛇蟒
  • 一双叫做 Python 的鞋子

可能还有许多其他东西叫做 Python。 我们实验的目的是仅通过一条推文的内容来提及 Python,并确定它是否在谈论编程语言。

从社交网络下载数据

我们将要从 Twitter 下载数据集,并使用它从有用的内容中清除垃圾邮件。 Twitter 提供了一个健壮的 API,用于从其服务器收集信息,该 API 对于小规模使用是免费的。 但是,如果您开始在商业环境中使用 Twitter 的数据,则需要注意一些条件。

首先,您需要注册一个 Twitter 帐户(免费)。 如果您还没有帐户,请访问这个页面并注册一个帐户。

接下来,您需要确保每分钟仅发出一定数量的请求。 目前,该限制为每小时 180 个请求。 确保不违反此限制可能很棘手,因此强烈建议您使用库与 Twitter 的 API 进行通信。

您将需要一个密钥来访问 Twitter 的数据。 转到这个页面并登录到您的帐户。

当您登录后,转到这个页面并单击创建新应用

为您的应用创建的名称和说明以及网站地址。 如果您没有要使用的网站,请插入一个占位符。 将此应用的回调 URL 字段保留为空白-我们将不需要它。 同意使用条款(如果需要),然后单击创建您的 Twitter 应用

保持结果网站保持打开状态–您需要此页面上的访问密钥。 接下来,我们需要一个图书馆与 Twitter 对话。 有很多选择。 我喜欢的一个简称为twitter,是官方 Twitter Python 库。

注意

如果要使用pip至安装软件包,则可以使用pip3 install twitter安装twitter。 如果您使用的是其他系统,请查看这个页面上的文档。

创建一个新的 IPython Notebook 以下载数据。 我们将在本章中出于不同的目的创建多个笔记本,因此最好创建一个文件夹来跟踪它们。 第一个笔记本ch6_get_twitter专用于下载新的 Twitter 数据。

首先,我们导入twitter库并设置我们的授权令牌。 消费者密钥消费者秘密将在 Twitter 应用页面的密钥和访问令牌选项卡上可用。 要获取访问令牌,您需要单击同一页面上的创建我的访问令牌按钮。 在以下代码中将密钥输入适当的位置:

import twitter
consumer_key = "<Your Consumer Key Here>"
consumer_secret = "<Your Consumer Secret Here>"
access_token = "<Your Access Token Here>"
access_token_secret = "<Your Access Token Secret Here>"
authorization = twitter.OAuth(access_token, access_token_secret, consumer_key, consumer_secret)

我们将从 Twitter 的search函数获得推文。 我们将创建一个使用我们的授权连接到twitter的阅读器,然后使用该阅读器执行搜索。 在笔记本中,我们设置将存储推文的文件名:

import os
output_filename = os.path.join(os.path.expanduser("~"), "Data", "twitter", "python_tweets.json")

我们还需要json库来保存我们的推文:

import json

接下来,创建一个可以从 Twitter 读取的对象。 我们使用之前设置的授权对象创建该对象:

t = twitter.Twitter(auth=authorization)

然后,我们打开输出文件进行写入。 我们打开它进行追加,这使我们可以重新运行脚本以获得更多推文。 然后,我们使用 Twitter 连接对 Python 进行搜索。 我们只想要为数据集返回的状态。 该代码使用了 tweet,使用json库通过dumps函数创建字符串表示形式,然后将其写入文件中。 然后,它在 tweet 下创建一个空白行,以便我们可以轻松地区分一条 tweet 在文件中的开始和结束位置:

with open(output_filename, 'a') as output_file:
    search_results = t.search.tweets(q="python", count=100)['statuses']
    for tweet in search_results:
        if 'text' in tweet:
            output_file.write(json.dumps(tweet))
            output_file.write("\n\n")

在前面的循环中,我们还执行检查以查看推文中是否有文本。 并非 twitter 返回的所有对象都是实际的 tweet(有些是删除 tweet 的动作,另一些是删除 tweet 的动作)。 关键的区别在于将文本作为密钥,我们对其进行了测试。

运行此命令几分钟将导致 100 条推文添加到输出文件中。

注意

您可以继续运行此脚本以向数据集中添加更多推文,请记住,如果重新运行速度太快(即在 Twitter 获得新的推文返回之前),您可能会在输出文件中获得一些重复项。

加载和分类数据集

在收集了一组推文(我们的数据集)之后,需要标签进行分类。 我们将通过在 IPython Notebook 中设置表单来标记数据集,以允许我们输入标签。

我们以 JSON 格式存储的数据集接近。 JSON 是一种数据格式,它不要求太多结构,并且可以在 JavaScript 中直接读取(因此,其名称为 JavaScript Object Notation)。 JSON 定义了基本对象,例如数字,字符串,列表和字典,如果它们包含非数字数据,则使其成为存储数据集的一种很好的格式。 如果数据集是完全数值的,则可以使用基于矩阵的格式(如 NumPy)来节省空间和时间。

我们的数据集与实际 JSON 之间的关键区别在于,我们在 tweet 之间添加了新行。 这样做的原因是允许我们轻松添加新的 tweet(实际的 JSON 格式不允许这样做)。 我们的格式是 tweet 的 JSON 表示形式,其后是换行符,然后是下一个 tweet,依此类推。

为了解析它,我们可以使用json库,但是我们必须首先用换行符分割文件,以获取实际的 tweet 对象本身。

设置一个新的 IPython Notebook(我叫我的ch6_label_twitter)并输入数据集的文件名。 这与上一部分中保存数据的文件名相同。 我们还定义了用于保存标签的文件名。 代码如下:

import os
input_filename = os.path.join(os.path.expanduser("~"), "Data", "twitter", "python_tweets.json")
labels_filename = os.path.join(os.path.expanduser("~"), "Data", "twitter", "python_classes.json")

如前所述,我们将使用json库,因此也要导入该库:

import json

我们创建一个列表,该列表将存储从文件中收到的推文:

tweets = []

然后,我们遍历文件中的每一行。 我们对没有信息的行不感兴趣(它们为我们分开了 tweet),因此请检查行的长度(减去任何空白字符)是否为零。 如果是,请忽略它并移至下一行。 否则,请使用json.loads(从字符串中加载 JSON 对象)加载推文,并将其添加到我们的推文列表中。 代码如下:

with open(input_filename) as inf:
    for line in inf:
        if len(line.strip()) == 0:
            continue
        tweets.append(json.loads(line))

现在,我们对分类某项是否与我们相关感兴趣(在这种情况下,相关意味着指的是编程语言 Python )。 我们将使用 IPython Notebook 的功能来嵌入 HTML,并在 JavaScript 和 Python 之间进行对话,以创建推文查看器,从而使我们能够轻松,快速地将推文归为垃圾邮件。

该代码将向用户(您)显示一条新推文,并要求提供标签:是否相关? 然后它将存储输入并显示下一个要标记的推文。

首先,我们创建一个存储标签的列表。 无论给定的推文是否引用编程语言 Python,这些标签都将被存储,这将使我们的分类器能够学习如何区分含义。

我们还将检查是否已经有任何标签并加载它们。 如果您需要在贴标签的中途关闭笔记本计算机,这将很有帮助。 此代码将从停止的地方加载标签。 通常,最好考虑如何在中点保存此类任务。 没有什么比损失一个小时的工作更令人痛心的了,因为在保存标签之前计算机崩溃了! 代码如下:

labels = []
if os.path.exists(labels_filename):
    with open(labels_filename) as inf:
        labels = json.load(inf)

接下来,我们创建一个简单的函数,该函数将返回下一个需要标记的推文。 我们可以通过找到尚未标记的第一条推文来算出下一条推文。 代码如下:

def get_next_tweet():
    return tweet_sample[len(labels)]['text']

注意

我们实验的下一步是从用户(您!)收集信息,其中哪些推文是指 Python(编程语言),哪些不是。 到目前为止,在 IPython Notebook 中还没有一种简单明了的纯 Python 交互式反馈方式。 因此,我们将使用一些 JavaScript 和 HTML 从用户那里获得输入。

接下来,我们在 IPython Notebook 中创建一些 JavaScript 以运行我们的输入。 笔记本允许我们使用魔术功能将 HTML 和 JavaScript(以及其他功能)直接嵌入到笔记本本身中。 从顶部的以下行开始新的单元格:

%%javascript

此处的代码将使用 JavaScript,因此出现了花括号。 不用担心,我们将很快返回 Python。 请记住,以下代码必须与%%javascript魔术函数位于同一单元格中。

我们将在 JavaScript 中定义的第一个函数显示了从 IPython Notebooks 中的 JavaScript 与您的 Python 代码进行对话有多么容易。 如果调用此函数,它将在labels数组中添加标签(在python代码中)。 为此,我们将 IPython 内核加载为 JavaScript 对象,并为其提供 Python 命令来执行。 代码如下:

function set_label(label){
    var kernel = IPython.notebook.kernel;
    kernel.execute("labels.append(" + label + ")");
    load_next_tweet();
}

在该函数的结尾,我们调用load_next_tweet函数。 此功能加载要标记的下一条推文。 它以相同的原理运行; 我们加载 IPython 内核并为其执行命令(调用我们先前定义的get_next_tweet函数)。

但是,在中,我们想要得到结果。 这有点困难。 我们需要定义一个callback,这是一个在返回数据时调用的函数。 定义callback的格式超出了该模块的范围。 如果您对更高级的 JavaScript / Python 集成感兴趣,请查阅 IPython 文档。

代码如下:

function load_next_tweet(){
   var code_input = "get_next_tweet()";
   var kernel = IPython.notebook.kernel;
   var callbacks = { 'iopub' : {'output' : handle_output}};
   kernel.execute(code_input, callbacks, {silent:false});
}

回调函数称为handle_output,我们现在将对其进行定义。 当kernel.execute调用的 Python 函数返回值时,将调用此函数。 和以前一样,其完整格式不在本模块的范围之内。 但是,出于我们的目的,结果将作为 text / plain 类型的数据返回,我们将其提取并显示在要在下一个单元格中创建的表单的#tweet_text div中。 代码如下:

function handle_output(out){
   var res = out.content.data["text/plain"];
   $("div#tweet_text").html(res);
}

我们的表单将带有一个div,显示要标记的下一条推文,我们将为其提供ID #tweet_text。 我们还创建了一个文本框,使我们能够捕获按键(否则,Notebook 将捕获按键,而 JavaScript 不会执行任何操作)。 这使我们能够使用键盘设置10的标签,这比使用鼠标单击按钮要快-因为我们至少需要标记 100 条推文。

运行上一个单元格,将一些 JavaScript 嵌入到页面中,尽管结果部分中不会显示任何内容。

现在,我们将使用另一个魔术函数%%html。 毫不奇怪,此魔术功能使我们可以将 HTML 直接嵌入到 Notebook 中。 在新单元格中,从以下这一行开始:

%%html

对于此单元格,我们将使用 HTML 和一些 JavaScript 进行编码。 首先,定义div元素以存储要标记的当前 tweet。 我还添加了一些使用此表单的说明。 然后,创建#tweet_text div,该 div 将存储要标记的下一条推文的文本。 如前所述,我们需要创建一个文本框以捕获按键。 代码如下:

<div name="tweetbox">
    Instructions: Click in textbox. Enter a 1 if the tweet is relevant, enter 0 otherwise.<br>
Tweet: <div id="tweet_text" value="text"></div><br>
<input type=text id="capture"></input><br>
</div>

暂时不要运行单元!

我们创建用于捕获按键的 JavaScript。 在创建表单后,需要定义,因为在上述代码运行之前#tweet_text div 不存在。 我们使用 JQuery 库(IPython 已经在使用,因此我们不需要包含 JavaScript 文件)来添加一个在#capture上按下键时调用的函数。 我们定义的文本框。 但是,请记住,这是%%html单元而不是 JavaScript 单元,因此我们需要将此 JavaScript 封装在[H​​TG3]标签中。

仅当用户按下01时,我们才对按键感兴趣,在这种情况下,将添加相关标签。 我们可以确定存储在e.which中的 ASCII 值按下了哪个键。 如果用户按下 0 或 1,我们将附加标签并清除文本框。 代码如下:

<script>
$("input#capture").keypress(function(e) {
if(e.which == 48) {
    set_label(0);
    $("input#capture").val("");
}else if (e.which == 49){
    set_label(1);
    $("input#capture").val("");
  }
});

所有其他按键均被忽略。

作为本章最后的 JavaScript(我保证),我们称为load_next_tweet()函数。 这将设置要标记的第一个 tweet,然后关闭 JavaScript。 代码如下:

load_next_tweet();
</script>

运行此单元格后,您将获得一个 HTML 文本框以及第一个 tweet 的文本。 单击文本框,如果与我们的目标相关,则输入1(在这种情况下,这意味着是与编程语言 Python 相关的推文),如果与我们的目标无关,则输入 0。 完成此操作后,将加载下一条推文。 输入标签,下一个将加载。 这一直持续到推文用完为止。

完成所有这些操作后,只需将标签保存到我们先前为类值定义的输出文件名即可:

with open(labels_filename, 'w') as outf:
    json.dump(labels, outf)

即使您尚未完成操作,也可以调用前面的代码。 到此为止,您所做的所有标签都将被保存。 再次运行此笔记本将在您离开的地方继续,您可以继续为自己的推文贴上标签。

为此可能需要一段时间! 如果数据集中有很多推文,则需要对所有这些推文进行分类。 如果时间有限,则可以下载我使用的相同数据集,其中包含分类。

从 Twitter 创建可复制的数据集

在数据挖掘中,有很多变量。 这些不仅仅出现在数据挖掘算法中,它们还出现在数据收集,环境和许多其他因素中。 能够复制结果非常重要,因为它使您可以验证或改进结果。

注意

使用算法X在一个数据集上获得 80%的准确性,使用算法Y在另一数据集上获得 90%的准确性并不意味着 Y 会更好。 我们需要能够在相同条件下对相同数据集进行测试,以进行正确比较。

在运行上述代码时,您将获得与我创建和使用的数据集不同的数据集。 主要原因是,Twitter 将根据您执行搜索的时间为您返回与我不同的搜索结果。 即使在那之后,您的推文标签可能与我的标签有所不同。 尽管有一些明显的示例,其中给定的推文与 python 编程语言相关,但总会有灰色区域的标签不明显。 我遇到的一个困难的灰色区域是我看不懂的非英语推文。 在这种特定情况下,Twitter 的 API 中提供了用于设置语言的选项,但是即使这些选项也不是完美的。

由于这些因素,很难在从社交媒体提取的数据库上复制实验,Twitter 也不例外。 Twitter 明确禁止直接共享数据集。

一种解决方案是仅共享鸣叫 ID,您可以自由共享它们。 在本节中,我们将首先创建一个我们可以自由共享的 tweet ID 数据集。 然后,我们将看到如何从该文件下载原始推文以重新创建原始数据集。

首先,我们保存 tweet ID 的可复制数据集。 创建另一个新的 IPython Notebook,首先设置文件名。 这样做的方式与标记相同,但是有一个新文件名可以存储可复制的数据集。 代码如下:

import os
input_filename = os.path.join(os.path.expanduser("~"), "Data", "twitter", "python_tweets.json")
labels_filename = os.path.join(os.path.expanduser("~"), "Data", "twitter", "python_classes.json")
replicable_dataset = os.path.join(os.path.expanduser("~"), "Data", "twitter", "replicable_dataset.json")

我们像上一个笔记本一样加载推文和标签:

import json
tweets = []
with open(input_filename) as inf:
    for line in inf:
        if len(line.strip()) == 0:
            continue
        tweets.append(json.loads(line))
if os.path.exists(labels_filename):
    with open(classes_filename) as inf:
        labels = json.load(inf)

现在,我们通过同时遍历推文和标签并将它们保存在列表中来创建数据集:

dataset = [(tweet['id'], label) for tweet, label in zip(tweets, labels)]

最后,我们将结果保存在文件中:

with open(replicable_dataset, 'w') as outf:
    json.dump(dataset, outf)

现在我们已经保存了推特 ID 和标签,我们可以重新创建原始数据集。 如果要重新创建本章使用的数据集,可以在本课程随附的代码包中找到它。

加载之前的数据集并不困难,但可能需要一些时间。 启动一个新的 IPython Notebook,并像以前一样设置数据集,标签和 tweet ID 文件名。 我在这里调整了文件名,以确保您不会覆盖以前收集的数据集,但可以根据需要随时更改它们。 代码如下:

import os
tweet_filename = os.path.join(os.path.expanduser("~"), "Data", "twitter", "replicable_python_tweets.json")
labels_filename = os.path.join(os.path.expanduser("~"), "Data", "twitter", "replicable_python_classes.json")
replicable_dataset = os.path.join(os.path.expanduser("~"), "Data", "twitter", "replicable_dataset.json")

然后使用 JSON 从文件中加载 tweet ID:

import json
with open(replicable_dataset) as inf:
    tweet_ids = json.load(inf)

保存标签非常容易。 我们只是遍历此数据集并提取 ID。 我们只需两行代码(打开文件并保存推文)就可以轻松完成此操作。 但是,我们不能保证会得到所有的 tweet(例如,自收集数据集以来,某些 tweet 可能已更改为 private),因此标签将不正确地针对数据建立索引。

举例来说,我试图在收集数据后的一天之内重新创建数据集,并且已经丢失了两条推文(它们可能被用户删除或设为私有)。 因此,仅打印出所需标签很重要。 为此,我们首先创建一个空的actual labels列表来存储实际从twitter恢复的推文的标签,然后创建将推文 ID 映射到标签的字典。

代码如下:

actual_labels = []
label_mapping = dict(tweet_ids)

接下来,我们将创建一个twitter服务器来收集所有这些推文。 这将花费更长的时间。 导入我们之前使用的twitter库,创建一个授权令牌,并使用该令牌创建twitter对象:

import twitter
consumer_key = "<Your Consumer Key Here>"
consumer_secret = "<Your Consumer Secret Here>"
access_token = "<Your Access Token Here>"
access_token_secret = "<Your Access Token Secret Here>"
authorization = twitter.OAuth(access_token, access_token_secret, consumer_key, consumer_secret)
t = twitter.Twitter(auth=authorization)

通过使用以下命令将 ID 提取到列表中来迭代每个 Twitter ID:

all_ids = [tweet_id for tweet_id, label in tweet_ids]

然后,我们打开输出文件以保存推文:

with open(tweets_filename, 'a') as output_file:

Twitter API 允许我们一次获得 100 条推文。 因此,我们遍历每 100 条 tweet:

    for start_index in range(0, len(tweet_ids), 100):

要按 ID 进行搜索,我们首先创建一个将所有 ID(在此批次中)连接在一起的字符串:

        id_string = ",".join(str(i) for i in all_ids[start_index:start_index+100])

接下来,我们执行状态/查找 API 调用,该调用由 Twitter 定义。 我们将 ID 列表(我们将其转换为字符串)传递到 API 调用中,以便将这些 tweet 返回给我们:

        search_results = t.statuses.lookup(_id=id_string)

然后,对于搜索结果中的每个推文,我们将其保存到文件中的方式与最初收集数据集时的方式相同:

        for tweet in search_results:
            if 'text' in tweet:
                output_file.write(json.dumps(tweet))
                output_file.write("\n\n")

作为此处的最后一步(仍在前面的if块下),我们想存储此推文的标签。 我们可以使用之前创建的label_mapping字典来执行此操作,查找推特 ID。 代码如下:

                actual_labels.append(label_mapping[tweet['id']])

运行上一个单元格,代码将为您收集所有推文。 如果您创建了一个非常大的数据集,则可能需要一段时间-Twitter 会进行限速请求。 作为此处的最后一步,请将actual_labels保存到我们的classes文件中:

with open(labels_filename, 'w') as outf:
    json.dump(actual_labels, outf)

Creating a replicable dataset from Twitter

文字转换器

现在我们有了数据集,我们将如何对其进行数据挖掘?

基于文本的数据集包括书籍,论文,网站,手稿,编程代码和其他形式的书面表达。 到目前为止,我们已经看到的所有算法都处理数字或分类特征,那么如何将文本转换为该算法可以处理的格式?

有个可以进行的测量。 例如,平均单词和平均句子长度用于预测文档的可读性。 但是,我们现在将研究许多特征类型,例如单词出现。

口碑

最简单但高效的模型中的一种是简单地计算数据集中的每个单词。 我们创建一个矩阵,其中每一行代表数据集中的一个文档,每一列代表一个单词。 单元格的值是该单词在文档中的出现频率。

这是指环王J.R.R.的摘录 托尔金

|   | *天上的精灵王三环,**七个在石头大厅里的矮人,**九个人命中注定要死,为黑暗之王登上黑暗宝座的人在暗影存在的魔多国。**一个环来统治所有人,一个环来找到他们,**一枚戒指将它们全部带走,在黑暗中将它们绑起来。*在暗影存在的魔多国。 |   | |   | - J.R.R. 托尔金在《指环王》中的题词 |

出现在单词中,而出现在中,显示为显示为显示为 ]分别出现四次。 单词出现了 3 次,的单词也出现了 3 次。

我们可以由此创建一个数据集,选择一个单词子集并计算频率:

| **字** | 这 | 一 | 戒指 | 到 | | **频率** | 9 | 4 | 3 | 4 |

我们可以使用counter类对给定的字符串进行简单计数。 在计算单词时,通常将所有字母都转换为小写,这在创建字符串时会执行。 代码如下:

s = """Three Rings for the Elven-kings under the sky,
Seven for the Dwarf-lords in halls of stone,
Nine for Mortal Men, doomed to die,
One for the Dark Lord on his dark throne
In the Land of Mordor where the Shadows lie.
One Ring to rule them all, One Ring to find them,
One Ring to bring them all and in the darkness bind them.
In the Land of Mordor where the Shadows lie. """.lower()
words = s.split()
from collections import Counter
c = Counter(words)

打印c.most_common(5)给出了最频繁出现的前五个单词的列表。 领带处理不佳,仅给出了五个,并且大量单词都并列第五。

词袋模型有三种主要类型。 首先是使用原始频率,如前面的示例所示。 当文档的大小从较少的单词变为很多单词时,这确实有一个缺点,因为的整体值将有很大的不同。 第二种模型是使用归一化频率,其中每个文档的总和等于 1。这是更好的解决方案,因为文档的长度无关紧要。 第三种是简单地使用二进制功能-如果单词完全出现,则值为 1;否则,则返回 0。 我们将在本章中使用二进制表示。

另一种执行标准化的流行方法(可能更流行)称为词频-反向文档频率tf-idf。 在这种加权方案中,术语计数首先被归一化为频率,然后除以它出现在语料库中的文档数。 我们将在第 10 章,“聚类新闻文章”中使用 tf-idf。

有许多库可用于在 Python 中处理文本数据。 我们将使用一个主要的工具,称为自然语言工具包NLTK)。 scikit-learn库还具有执行类似操作的 CountVectorizer类,建议您看一下(我们将在第 9 章,作者归因中使用它)。 但是,NLTK 版本具有更多用于单词标记化的选项。 如果您使用 python 进行自然语言处理,则 NLTK 是一个很好的库。

N-grams

从单个单词袋特征开始的步骤是 n-gram 的特征。 n-gram 是n个连续标记的子序列。 在这种情况下,单词 n-gram 是连续出现的n个单词的集合。

他们的计数方法相同,n 元语法词构成,并放入中。 此数据集中的单元格值是特定 n-gram 在给定文档中出现的频率。

注意

n的值是一个参数。 对于英语,将其设置为 2 到 5 之间是一个好的开始,尽管某些应用要求使用更高的值。

例如,对于 n = 3,我们提取以下引号中的前几个 n-gram:

始终朝着生活的光明面看。

第一个 n-gram(大小为 3)是始终在上看,第二个是上看,第三个是在明亮的上。 如您所见,n 元语法重叠并覆盖了三个单词。

单词 n-gram 优于使用单个单词。 这个简单的概念通过考虑词的本地环境为词的使用引入了一些上下文,而无需花大量的时间来理解该语言。 使用 n-gram 的一个缺点是矩阵变得更加稀疏-不太可能出现单词 n-gram 两次(特别是在推文和其他简短文档中!)。

专门用于社交媒体和其他短文档的单词 n-gram 不太可能出现在太多不同的推文中,除非它是转推。 但是,在较大的文档中,单词 n-gram 对于许多应用都非常有效。

文本文档的另一种形式的 n-gram 是字符 n-gram。 而不是使用单词集,我们只使用字符集(尽管字符 n-gram 对于它们的计算方式有很多选择!)。 这种类型的数据集可以选择拼写错误的单词,并提供其他好处。 我们将在本章中测试字符 n-gram,然后在第 9 章和“作者身份”中再次看到它们。

其他功能

还有其他可以提取的功能。 这些包括句法功能,例如句子中特定单词的使用。 词性标签在需要理解文本含义的数据挖掘应用中也很流行。 此类功能类型将不在本模块中介绍。 如果您有兴趣了解更多信息,建议使用 Python 3 文本处理和 NLTK 3 食谱Jacob PerkinsPackt Publishing

朴素贝叶斯

朴素贝叶斯是概率模型,毫无疑问地基于朴素的贝叶斯统计解释。 尽管幼稚的方面,该方法在大量上下文中仍然表现良好。 它可以用于对许多不同的特征类型和格式进行分类,但是在本章中我们将重点介绍一种:词袋模型中的二进制特征。

贝叶斯定理

对于我们大多数来说,当我们学习统计学时,我们是从一种常识性方法开始的。 在这种方法中,我们假设数据来自某个分布,并且我们旨在确定该分布的参数。 但是,这些参数(可能不正确)被假定为固定的。 我们使用模型来描述数据,甚至进行测试以确保数据适合我们的模型。

贝叶斯统计代替了人们(非统计学家)实际推理的模型。 我们有一些数据,并使用这些数据来更新模型,以了解发生某件事的可能性。 在贝叶斯统计中,我们使用数据来描述模型,而不是使用模型并通过数据进行确认(按照常客方法)。

贝叶斯定理计算 P(A | B)的值,也就是说,知道B发生了,A的概率是多少。 在大多数情况下,B是观察到的事件,例如,昨天下雨的,A是今天下雨的预报。 对于数据挖掘,B通常是,我们观察到此样本A是*,它属于此类*。 下一节将介绍如何使用贝叶斯定理进行数据挖掘。

贝叶斯定理的方程给出如下:

Bayes' theorem

例如,我们要确定包含药物一词的电子邮件是垃圾邮件的可能性(因为我们认为这样的推文可能是制药垃圾邮件)。

在此上下文中,是该推文是垃圾邮件的概率。 我们可以通过计算数据集中的垃圾邮件百分比来直接从训练数据集中计算 P(A),称为先验信念。 如果我们的数据集中每 100 封电子邮件包含 30 条垃圾邮件,则 *P(A)*为 30/1000.3

在此上下文中,B是*,此推文包含单词“ drugs”。 同样,我们可以通过计算包含药物一词的数据集中的推文百分比来计算 P(B)。 如果我们的训练数据集中每 100 封电子邮件中有 10 封邮件包含药物*字词,则 *P(B)*为 10/100 或 0.1。 请注意,计算此值时,我们不在乎电子邮件是否为垃圾邮件。

P(B|A)是电子邮件中包含毒品单词的可能性,如果它是垃圾邮件。 从我们的训练数据集进行计算也很容易。 我们仔细查看垃圾邮件的训练集,并计算出包含毒品一词的邮件所占的百分比。 在我们的 30 封垃圾邮件中,如果 6 封包含药物一词,则 *P(B | A)*计算为 6/30 或 0.2。

从这里开始,我们使用贝叶斯定理来计算 P(A | B),这是包含药物的推文成为垃圾邮件的可能性。 使用前面的公式,我们看到结果是 0.6。 这表明,如果电子邮件中包含毒品一词,则有 60%的可能性是垃圾邮件。

注意前面示例的经验性质-我们直接从训练数据集中使用证据,而不是从某些先入为主的分布中使用证据。 相反,对此的常识性看法将依靠我们在推文中创建单词概率的分布来计算相似的方程式。

朴素贝叶斯算法

回顾的贝叶斯定理方程,我们可以用它来计算给定样本属于给定类别的概率。 这允许将该方程用作分类算法。

在数据集中以C为给定类,以D为样本,我们创建贝叶斯定理和随后的朴素贝叶斯所需的元素。 朴素贝叶斯是一种分类算法,利用贝叶斯定理来计算新数据样本属于特定类别的概率。

*P(C)*是类别的概率,它是从训练数据集本身计算出来的(就像我们对垃圾邮件示例所做的那样)。 我们只需计算训练数据集中属于给定类别的样本的百分比即可。

*P(D)*是给定数据样本的概率。 由于样本是不同功能之间的复杂交互,因此可能很难计算出来,但是幸运的是,它在所有类中都是一个常数。 因此,我们根本不需要计算它。 稍后我们将看到如何解决此问题。

P(D | C)是数据点属于类别的概率。 由于功能不同,这也可能难以计算。 但是,这是我们介绍朴素贝叶斯算法的朴素部分的地方。 我们天真地假设每个功能都是彼此独立的。 而不是计算 *P(D | C)*的全部概率,我们计算每个特征 D1,D2,D3 等的概率。 然后,我们将它们相乘:

P(D|C) = P(D1|C) x P(D2|C).... x P(Dn|C)

这些值中的每一个都相对易于使用二进制功能进行计算。 我们只计算样本数据集中相等时间的百分比。

相反,如果要执行本部分的非朴素贝叶斯版本,则需要为每个类计算不同特征之间的相关性。 这种计算充其量是不可行的,如果没有大量的数据或适当的语言分析模型,几乎是不可能的。

从开始,该算法非常简单。 我们为每个可能的类别计算 P(C | D),而忽略了 *P(D)*项。 然后,我们选择概率最高的类别。 由于 *P(D)*术语在每个类别中都是一致的,因此忽略它不会对最终预测产生影响。

工作原理

作为的示例,假设我们从数据集中的样本中获得以下(二进制)特征值:[0, 0, 0, 1]

我们的训练数据集包含两个类别,其中 75%的样本属于0类别,而 25%的样本属于1类别。 每个类的特征值的可能性如下:

对于课程 0:[0.3, 0.4, 0.4, 0.7]

对于类别 1:[0.7, 0.3, 0.4, 0.9]

这些值应解释为:对于特征 1 ,对于类别 0,在 30%的情况下为 1。

现在我们可以计算出该样本属于0类的概率。 P(C = 0)= 0.75,这是类别为0的概率。

朴素贝叶斯算法不需要 P(D)。 让我们看一下计算:

P(D|C=0) = P(D1|C=0) x P(D2|C=0) x P(D3|C=0) x P(D4|C=0)
= 0.3 x 0.6 x 0.6 x 0.7
= 0.0756

注意

第二个和第三个值为 0.6,因为样本中该特征的值为0。 列出的概率是每个功能的1值。 因此,0的概率是其倒数:P(0)= 1-P(1)

现在,我们可以计算出属于此类的数据点的概率。 需要注意的重要一点是,我们尚未计算出 P(D),因此这不是真实的概率。 但是,将相同的值与类别 1 的概率进行比较就足够了。让我们看一下计算:

P(C=0|D) = P(C=0) P(D|C=0)
= 0.75 * 0.0756
= 0.0567

现在,我们为类 1 计算相同的值:

P(C=1) = 0.25

天真贝叶斯不需要P(D)。 让我们看一下计算:

P(D|C=1) = P(D1|C=1) x P(D2|C=1) x P(D3|C=1) x P(D4|C=1)
= 0.7 x 0.7 x 0.6 x 0.9
= 0.2646
P(C=1|D) = P(C=1)P(D|C=1)
= 0.25 * 0.2646
= 0.06615

注意

通常,*P(C = 0 | D)+ P(C = 1 | D)*应该等于 1。毕竟,这是仅有的两个可能的选择! 但是,由于我们没有在此处的方程式中包含 *P(D)*的计算,因此概率不是 1。

数据点应分类为属于类别1。 无论如何,您可能已经在通过方程式进行猜测了; 但是,您可能对最终决定如此之近感到有些惊讶。 毕竟,对于类别1,计算 *P(D | C)*的概率要高得多。 这是因为我们引入了一个先验的信念,即大多数样本通常都属于0类。

如果类的大小相等,则得出的概率将有很大不同。 通过将 *P(C = 0)*和 *P(C = 1)*都更改为 0.5,以实现相同的班级大小,然后再次计算结果,自己尝试一下。