Python 机器学习示例第四版(三)

原文:annas-archive.org/md5/143aadf706a620d20916160319321b2e

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:使用文本分析技术挖掘 20 个新闻组数据集

在之前的章节中,我们学习了一些基本的机器学习概念和监督学习算法。从这一章开始,作为我们学习旅程的第二步,我们将详细讲解与文本分析相关的几种重要无监督学习算法和技术。为了让我们的旅程更加有趣,我们将从一个自然语言处理NLP)问题开始——探索 20 个新闻组数据。你将获得实践经验,学习如何处理文本数据,特别是如何将单词和短语转换为计算机可读的数值,并如何清理意义不大的词语。我们还将通过将文本数据映射到二维空间,以无监督学习的方式进行可视化。

我们将详细讨论以下每个主题:

  • 计算机如何理解语言——自然语言处理(NLP)

  • 了解流行的 NLP 库并学习 NLP 基础

  • 获取新闻组数据

  • 探索新闻组数据

  • 思考文本数据的特征

  • 使用 t-SNE 可视化新闻组数据

  • 使用密集向量表示词语——词嵌入

计算机如何理解语言——自然语言处理(NLP)

第一章机器学习与 Python 入门中,我提到过,机器学习驱动的程序或计算机擅长通过处理和利用数据发现事件模式。当数据结构良好或定义明确时,例如在 Microsoft Excel 电子表格表格或关系数据库表格中,为什么机器学习比人类更擅长处理这些数据是直观显而易见的。计算机读取这些数据的方式与人类相同——例如,revenue: 5,000,000 表示收入为 500 万,age: 30 表示年龄为 30;然后计算机会处理各种数据,并以比人类更快的方式生成洞察。然而,当数据是非结构化的,例如人类沟通使用的词语、新闻文章,或某人用另一种语言的演讲时,似乎计算机无法像人类一样理解这些词语(尽管如此)。尽管计算机在理解词语和自然语言方面取得了显著进展,但在许多方面仍无法达到人类级别的理解。

什么是 NLP?

关于词语、原始文本或广义上的自然语言,世界上有大量的信息。自然语言指的是人类用来相互沟通的任何语言。自然语言可以有多种形式,包括但不限于以下内容:

  • 文本,例如网页、短信、电子邮件和菜单

  • 音频,如语音和 Siri 的命令

  • 手势与符号

  • 许多其他形式,如歌曲、乐谱和摩尔斯电码

这个列表是无尽的,我们随时都被自然语言包围着(没错,现在你在读这本书时也是如此)。考虑到这种非结构化数据(自然语言数据)的重要性,我们必须找到方法让计算机理解和推理自然语言,并从中提取数据。配备了 NLP 技术的程序在某些领域已经可以做很多事情,这已经显得有些神奇了!

自然语言处理(NLP)是机器学习的一个重要子领域,处理的是机器(计算机)与人类(自然)语言之间的交互。NLP 任务的数据可以以不同的形式存在,例如社交媒体帖子、网页,甚至医疗处方中的文本,或者语音邮件、控制系统命令,甚至是你最喜欢的歌曲或电影中的音频。如今,NLP 已经广泛应用到我们的日常生活中:我们无法离开机器翻译,天气预报脚本被自动生成,我们觉得语音搜索很方便,我们可以迅速获得问题的答案(例如“加拿大的人口是多少?”),得益于智能问答系统,语音转文字技术帮助有特殊需求的人,等等。

生成式人工智能及其应用,如 ChatGPT,正在进一步推动 NLP 的边界。想象一下这样一个世界,你可以与一个虚拟助手对话,它不仅可以全面回答你的问题,还可以生成各种创意文本格式,如诗歌、代码、剧本、音乐作品、电子邮件、信件等等。通过分析大量的文本数据,它可以学习语言的基本模式和结构,使其能够生成接近人类质量的文本内容。例如,你可以让 ChatGPT 为你的朋友写一首幽默的生日诗,为你的商业制作一封引人注目的营销邮件,甚至为一个新的博客文章头脑风暴一些创意点子。

自然语言处理(NLP)的历史

如果机器能够像人类一样理解语言,我们就认为它们是智能的。1950 年,著名数学家艾伦·图灵在一篇文章《计算机机械与智能》中提出了一个测试作为机器智能的标准。现在这个测试被称为图灵测试plato.stanford.edu/entries/turing-test/),其目的是检验计算机是否能够充分理解语言,从而使人类误以为机器是另一个人。可能你并不惊讶,因为至今没有计算机通过图灵测试,但 1950 年代被认为是自然语言处理历史的起点。

理解语言可能很困难,但自动将文本从一种语言翻译成另一种语言会更容易吗?在我第一次参加编程课程时,实验手册中有粗粒度机器翻译的算法。这种翻译方式涉及查阅字典中的单词,并生成新语言的文本。一种更为实际可行的方法是收集已经由人类翻译的文本,并用这些文本训练计算机程序。在 1954 年,乔治城–IBM 实验(en.wikipedia.org/wiki/Georgetown%E2%80%93IBM_experiment)中,科学家们声称机器翻译将在三到五年内解决。不幸的是,能够超过人类专家翻译的机器翻译系统至今尚未出现。但自从引入深度学习以来,机器翻译已大大发展,并在某些领域取得了令人难以置信的成就,例如社交媒体(Facebook 开源了一个神经机器翻译系统,ai.facebook.com/tools/translate/),实时对话(微软翻译、SwiftKey 键盘和 Google Pixel Buds),以及基于图像的翻译,如 Google 翻译。

会话代理或聊天机器人是自然语言处理中的另一个热门话题。计算机能够与我们对话,重新定义了商业运作的方式。在 2016 年,微软的 AI 聊天机器人 Tay(blogs.microsoft.com/blog/2016/03/25/learning-tays-introduction/)被推出,模拟一个年轻女孩并实时与 Twitter(现为 X)上的用户进行对话。她通过用户发布和评论的内容学习如何说话。然而,她被恶意用户的推文淹没,自动学会了他们的不良行为,并开始在她的动态中输出不当的内容。最终,她在 24 小时内被终止了。像 ChatGPT 这样的生成式 AI 模型是另一个活跃的研究领域,推动了可能性的边界。它们在创造性文本格式或特定任务中可能有所帮助,但实现真正的人类水平的对话理解仍然是一个持续的追求。

自然语言处理应用

还有一些文本分析任务,试图以某种方式组织知识和概念,使它们更容易被计算机程序操作。

我们组织和表示概念的方式被称为本体论。本体论定义了概念及其之间的关系。例如,我们可以有一个所谓的三元组,如("python""language""is-a"),表示两个概念之间的关系,例如Python 是一种语言

与前述案例相比,NLP 的一个重要用例是词性标注PoS tagging),这在很大程度上是一个较低级别的应用。词性是一个词类,如名词或动词。词性标注试图确定句子或更大文档中每个词的适当标签。

以下表格给出了英语 PoS 的示例:

词性示例
名词大卫, 机器
代词他们, 她的
形容词很棒, 了不起
动词阅读, 写作
副词非常, 相当
介词出, 在
连词和, 但是
感叹词哎呀, 哎哟
冠词一, 这

表 7.1: PoS 示例

有多种涉及监督学习的现实世界 NLP 应用,例如之前提到的 PoS 标签,以及情感分析。一个典型的例子是识别新闻情感,这可能在二进制情况下是积极或消极的,或者在多类分类中是积极的、中性的或消极的。新闻情感分析为股市交易提供了重要信号。

另一个我们可以轻易想到的例子是新闻主题分类,其中类别可能是互斥的,也可能不是。在刚才讨论的新闻组例子中,类别是互斥的(尽管稍有重叠),如技术、体育和宗教。然而,需要意识到新闻文章偶尔可能被分配多个类别(多标签分类)。例如,如果涉及到意外的政治参与,关于奥运会的文章可能被标记为体育和政治。

最后,一个也许意想不到但很有趣的应用是命名实体识别NER)。命名实体是明确类别的短语,如人名、公司名、地理位置、日期和时间、数量和货币值。NER 是信息提取的重要子任务,旨在寻找和识别这些实体。例如,我们可以对以下句子进行 NER:SpaceX[组织],一家由著名技术企业家Elon Musk[人物]创立,总部位于加利福尼亚[地点]的公司,宣布将为首次轨道飞行制造下一代直径为9[数量]米的发射车和飞船。

其他关键的 NLP 应用包括:

  • 语言翻译:NLP 推动机器翻译系统,实现从一种语言到另一种语言的自动翻译。像 Google Translate 和 Microsoft Translator 这样的平台利用 NLP 提供实时翻译服务。

  • 语音识别:NLP 在语音识别系统中至关重要,将口语转换为书面文本。像 Siri、Alexa 和 Google Assistant 这样的虚拟助手依赖于 NLP 来理解用户命令并适当地回应。

  • 文本摘要:NLP 可以自动生成简明扼要的长文本摘要,提供内容的快速概览。文本摘要对于信息检索和内容策划非常有用。

  • 语言生成:NLP 模型,如生成预训练变换器GPTs),可以生成类人文本,包括创意写作、诗歌和对话生成。

  • 信息检索:NLP 帮助从大量的非结构化数据中检索信息,如网页、文档和新闻文章。搜索引擎使用 NLP 技术来理解用户查询并检索相关结果。

  • 聊天机器人、问答系统和虚拟助手:NLP 为聊天机器人和虚拟助手提供支持,提供交互式和对话式的体验。这些系统可以回答查询、协助任务,并引导用户完成各种流程。

在下一章中,我们将讨论如何将无监督学习(包括聚类和主题建模)应用于文本数据。我们将从本章的后续部分开始,介绍 NLP 的基础知识。

参观流行的 NLP 库并学习 NLP 基础

现在我们已经介绍了一些 NLP 的实际应用,我们将参观 Python NLP 库的核心工具包。这些包处理广泛的 NLP 任务,如情感分析、文本分类和 NER。

安装著名的 NLP 库

Python 中最著名的 NLP 库包括自然语言工具包NLTK)、spaCyGensimTextBlob。scikit-learn 库也具有令人印象深刻的与 NLP 相关的功能。让我们更详细地了解它们:

  • NLTK:这个库(www.nltk.org/)最初是为教育目的开发的,现在也被广泛应用于工业界。据说,如果不提到 NLTK,就不能谈论 NLP。它是构建基于 Python 的 NLP 应用程序最著名和领先的平台之一。你可以通过在终端中运行以下命令来简单安装它:
sudo pip install -U nltk 

如果你使用conda,执行以下命令:

conda install nltk 
  • spaCy:这个库(https://spacy.io/)在行业中比 NLTK 更强大。主要有两个原因:首先,spaCy是用 Cython 编写的,内存优化效果更好(现在你可以理解spaCy中的Cy是什么意思),并且在 NLP 任务中表现优异;其次,spaCy使用最先进的算法解决核心 NLP 问题,例如用于标注和命名实体识别(NER)的卷积神经网络CNN)模型。然而,对于初学者来说,它可能显得较为复杂。如果你有兴趣,下面是安装说明。

在终端中运行以下命令:

pip install -U spacy 

对于conda,执行以下命令:

conda install -c conda-forge spacy 
  • Gensim:这个库 (radimrehurek.com/gensim/) 由 Radim Rehurek 开发,近年来逐渐受到欢迎。它最初在 2008 年设计,用于给定一篇文章后生成相似文章的列表,因此得名(generate similar --> Gensim)。后来,Radim Rehurek 在效率和可扩展性方面对其进行了大幅改进。再次提醒,你可以通过运行以下命令行轻松安装它:
pip install --upgrade gensim 

如果使用 conda,你可以在终端中执行以下命令行:

conda install -c conda-forge gensim 

在安装 Gensim 之前,你需要确保已经安装了依赖项 NumPy 和 SciPy。

  • TextBlob:这个库 (textblob.readthedocs.io/en/dev/) 是一个相对较新的库,建立在 NLTK 之上。它通过易于使用的内置函数和方法,以及对常见任务的包装,简化了自然语言处理(NLP)和文本分析。我们可以通过在终端运行以下命令行安装 TextBlob
pip install -U textblob 

或者,使用 conda:

conda install -c conda-forge textblob 

TextBlob 有一些在 NLTK 中(目前)没有的有用功能,比如拼写检查与纠正、语言检测和翻译。

语料库

NLTK 提供了 100 多个大型且结构良好的文本数据集,称为 NLP 中的 语料库。以下是 NLTK 提供的一些主要语料库:

  • Gutenberg Corpus:来自 Gutenberg 项目的文学作品集合,包含多种语言的数千本书籍。

  • Reuters Corpus:来自路透社新闻服务的新闻文章集合,广泛用于文本分类和主题建模任务。

  • Web and Chat Text:一个包含网络文本和聊天对话的集合,展示了非正式语言和互联网俚语。

  • Movie Reviews Corpus:一个包含电影评论的语料库,常用于情感分析和文本分类任务。

  • Treebank Corpus:来自 Penn Treebank 的解析和标注句子集合,用于训练和评估句法分析器。

  • WordNet:一个英语词汇数据库,包含同义词集(同义词的组合)和上位词(“是”关系)。

语料库可以用作检查单词出现频率的词典,也可以作为模型学习和验证的训练池。一些更有用且有趣的语料库包括 Web Text 语料库、Twitter(X)样本、莎士比亚语料库、情感极性语料库、名字语料库(该语料库包含流行名字的列表,我们很快就会探索这些),WordNet 和 Reuters 基准语料库。完整列表可以在 www.nltk.org/nltk_data 查找到。

在使用任何这些语料库资源之前,我们需要先通过在 Python 解释器中运行以下代码下载它们:

>>> import nltk
>>> nltk.download() 

一个新窗口会弹出,询问你要下载哪些集合(下图中的Collections标签)或语料库(下图中的Corpora标签),以及数据存储的位置:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_07_01.png

图 7.1:NLTK 安装中的“Collections”标签

安装整个流行的包是最快的解决方案,因为它包含了当前学习和未来研究所需的所有重要语料库。安装特定的语料库,如下图所示,也是可以的:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_07_02.png

图 7.2:NLTK 安装中的“Corpora”标签

一旦你想要探索的包或语料库安装完成,你可以查看Names语料库(确保这个语料库已经安装以便进行本例的操作)。

首先,导入名称语料库:

>>> from nltk.corpus import names 

我们可以查看列表中的前10个名称:

>>> print(names.words()[:10])
['Abagael', 'Abagail', 'Abbe', 'Abbey', 'Abbi', 'Abbie',
'Abby', 'Abigael', 'Abigail', 'Abigale'] 

总共有7944个名称,如下所示,执行以下命令后得到的输出:

>>> print(len(names.words()))
7944 

其他语料库也很有趣,值得探索。

除了易于使用且丰富的语料库池,更重要的是,NLTK 还擅长许多 NLP 和文本分析任务,包括标记化、词性标注、命名实体识别(NER)、词干提取和词形还原。接下来我们将讨论这些任务。

标记化

给定一个文本序列,标记化是将其拆分成碎片的任务,这些碎片可以是单词、字符或句子。通常会去除一些字符,如标点符号、数字和表情符号。剩下的碎片就是所谓的标记,用于进一步处理。

由一个词组成的标记在计算语言学中也称为单词单元(unigrams);由两个连续词组成的标记称为二元组(bigrams);由三个连续词组成的标记称为三元组(trigrams);而n-grams是由n个连续词组成的。这里是一个标记化的例子:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_07_03.png

图 7.3:标记化示例

我们可以使用 NLTK 中的word_tokenize函数实现基于单词的标记化。我们将以输入文本'''I am reading a book., 然后是It is Python Machine Learning By Example,, 接着是4th edition.'''作为示例,如下命令所示:

>>> from nltk.tokenize import word_tokenize
>>> sent = '''I am reading a book.
...           It is Python Machine Learning By Example,
...           4th edition.'''
>>> print(word_tokenize(sent))
['I', 'am', 'reading', 'a', 'book', '.', 'It', 'is', 'Python', 'Machine', 'Learning', 'By', 'Example', ',', '3rd', 'edition', '.'] 

得到了单词标记。

word_tokenize函数会保留标点符号和数字,只丢弃空格和换行符。

你可能认为词语标记化仅仅是通过空格和标点符号分割句子。这里有一个有趣的例子,表明标记化比你想象的要复杂:

>>> sent2 = 'I have been to U.K. and U.S.A.'
>>> print(word_tokenize(sent2))
['I', 'have', 'been', 'to', 'U.K.', 'and', 'U.S.A', '.'] 

例如,分词器能准确地将'U.K.''U.S.A'识别为标记,而不是将'U''.'跟在'K'后面。

spaCy 还具有出色的分词功能。它使用一个精确训练的模型,该模型会不断更新。要安装它,我们可以运行以下命令:

python -m spacy download en_core_web_sm 

然后,我们加载en_core_web_sm模型(如果您尚未下载该模型,可以运行python -m spacy download en_core_web_sm来下载),并使用此模型解析句子:

>>> import spacy
>>> nlp = spacy.load('en_core_web_sm')
>>> tokens2 = nlp(sent2)
>>> print([token.text for token in tokens2])
['I', 'have', 'been', 'to', 'U.K.', 'and', 'U.S.A.'] 

我们还可以根据句子对文本进行分段。例如,在相同的输入文本中,使用 NLTK 的sent_tokenize函数,得到以下命令:

>>> from nltk.tokenize import sent_tokenize
>>> print(sent_tokenize(sent))
['I am reading a book.',
'It's Python Machine Learning By Example,\n          4th edition.'] 

返回两个基于句子的标记,因为输入文本中有两个句子。

PoS 标注

我们可以应用 NLTK 的现成标注器,或者组合多个标注器来定制标注过程。直接使用内置的标注功能pos_tag也很容易,例如在pos_tag(input_tokens)中使用,但幕后实际上是通过一个预先构建的监督学习模型进行预测。该模型是基于一个由正确标注的单词构成的大型语料库训练的。

重新使用之前的示例,我们可以如下进行 PoS 标注:

>>> import nltk
>>> tokens = word_tokenize(sent)
>>> print(nltk.pos_tag(tokens))
[('I', 'PRP'), ('am', 'VBP'), ('reading', 'VBG'), ('a', 'DT'), ('book', 'NN'), ('.', '.'), ('It', 'PRP'), ('is', 'VBZ'), ('Python', 'NNP'), ('Machine', 'NNP'), ('Learning', 'NNP'), ('By', 'IN'), ('Example', 'NNP'), (',', ','), ('4th', 'CD'), ('edition', 'NN'), ('.', '.')] 

每个标记后会返回其 PoS 标签。我们可以使用help函数查看标签的含义。例如,查找PRPVBP会得到以下输出:

>>> nltk.help.upenn_tagset('PRP')
PRP: pronoun, personal
   hers herself him himself hisself it itself me myself one oneself ours ourselves ownself self she thee theirs them themselves they thou thy us
>>> nltk.help.upenn_tagset('VBP')
VBP: verb, present tense, not 3rd person singular
   predominate wrap resort sue twist spill cure lengthen brush terminate appear tend stray glisten obtain comprise detest tease attract emphasize mold postpone sever return wag ... 

在 spaCy 中,获取 PoS 标签也很简单。token对象从输入句子解析后具有一个名为pos_的属性,它就是我们要查找的标签。让我们为每个标记打印pos_,如下所示:

>>> print([(token.text, token.pos_) for token in tokens2])
[('I', 'PRON'), ('have', 'VERB'), ('been', 'VERB'), ('to', 'ADP'), ('U.K.', 'PROPN'), ('and', 'CCONJ'), ('U.S.A.', 'PROPN')] 

我们刚刚尝试了使用 NLP 包进行 PoS 标注。那么 NER 呢?让我们在下一部分看看。

命名实体识别(NER)

给定一个文本序列,NER 任务是定位并识别具有确定类别的单词或短语,例如人名、公司名、地点名和日期。我们来看一个使用 spaCy 进行 NER 的示例。

首先,像往常一样对输入句子The book written by Hayden Liu in 2024 was sold at $30 in America进行分词,如下所示:

>>> tokens3 = nlp('The book written by Hayden Liu in 2024 was sold at $30 in America') 

结果token对象包含一个名为ents的属性,它表示命名实体。我们可以按如下方式提取每个识别出的命名实体的标签:

>>> print([(token_ent.text, token_ent.label_) for token_ent in tokens3.ents])
[('Hayden Liu', 'PERSON'), ('2024', 'DATE'), ('30', 'MONEY'), ('America', 'GPE')] 

从结果中我们可以看到,Hayden LiuPERSON2024DATE30MONEYAmericaGPE(国家)。完整的命名实体标签列表请参考spacy.io/api/annotation#section-named-entities

词干提取和词形还原

单词词干提取是将屈折或派生词还原为其词根形式的过程。例如,machinemachines的词干,而learninglearned是从learn派生出的词干。

**词形还原(lemmatization)**是词干提取(stemming)的谨慎版本。在进行词干提取时,它会考虑单词的词性(PoS)。同时,它会追溯到单词的词元(基本或标准形式)。我们将在稍后详细讨论这两种文本预处理技术——词干提取和词形还原。现在,让我们快速看看它们在 NLTK 中的实现步骤:

  1. 导入porter作为三种内置词干提取算法之一(另外两种分别是LancasterStemmerSnowballStemmer),并按如下方式初始化词干提取器:

    >>> from nltk.stem.porter import PorterStemmer
    >>> porter_stemmer = PorterStemmer() 
    
  2. 如下代码所示,对machineslearning进行词干提取:

    >>> porter_stemmer.stem('machines')
    'machin'
    >>> porter_stemmer.stem('learning')
    'learn' 
    

词干提取有时会在必要时截断字母,就像在前面的命令输出中看到的machin

  1. 现在,导入一个基于内置 WordNet 语料库的词形还原算法,并初始化词形还原器:

    >>> from nltk.stem import WordNetLemmatizer
    >>> lemmatizer = WordNetLemmatizer() 
    

与词干提取类似,我们对machineslearning进行词形还原:

>>> lemmatizer.lemmatize('machines')
'machine'
>>> lemmatizer.lemmatize('learning')
'learning' 

为什么learning没有变化?算法默认会为名词找到词元,除非你特别指定。如果你希望将learning视为动词,可以在lemmatizer.lemmatize('learning', nltk.corpus.wordnet.VERB)中指定,这将返回learn

语义和主题建模

Gensim 以其强大的语义和主题建模算法而闻名。主题建模是一个典型的文本挖掘任务,旨在发现文档中隐藏的语义结构。用简单的英语来说,语义结构就是单词出现的分布。这显然是一个无监督学习任务。我们需要做的是输入纯文本,让模型找出抽象的主题。例如,我们可以使用主题建模根据评论中表达的共同主题将电商网站上的产品评论进行分组。我们将在第八章《通过聚类和主题建模发现新闻组数据集中的潜在主题》中详细学习主题建模。

除了强大的语义建模方法,gensim 还提供以下功能:

  • 词嵌入(Word embedding):也称为词向量化,这是一种创新的单词表示方式,能够保留单词的共现特征。在本章后面,我们将深入探讨词嵌入。

  • 相似度查询:此功能检索与给定查询对象相似的对象。这是基于词嵌入技术构建的功能。

  • 分布式计算:此功能使得从数百万文档中高效学习成为可能。

最后但同样重要的是,正如第一章《机器学习与 Python 入门》中所提到的,scikit-learn 是本书中我们贯穿始终使用的主要包。幸运的是,它提供了我们所需的所有文本处理功能,例如分词,并且具备全面的机器学习功能。此外,它还内置了一个加载器,用于加载 20 个新闻组数据集。

现在工具已经准备好并正确安装,接下来是数据呢?

获取新闻组数据

本章中的项目是关于 20 个新闻组数据集。顾名思义,它由从新闻组文章中提取的文本组成。最初由 Ken Lang 收集,现在广泛用于机器学习技术,特别是自然语言处理(NLP)技术的文本应用实验。

数据包含大约 20,000 篇文档,分布在 20 个在线新闻组中。新闻组是互联网上一个人们可以就某一主题提问和回答问题的地方。数据已经经过一定程度的清理,并且已被划分为训练集和测试集。切分点是在某个特定日期。

原始数据来源于qwone.com/~jason/20Newsgroups/,列出了 20 个不同的主题,具体如下:

  • comp.graphics

  • comp.os.ms-windows.misc

  • comp.sys.ibm.pc.hardware

  • comp.sys.mac.hardware

  • comp.windows.x

  • rec.autos

  • rec.motorcycles

  • rec.sport.baseball

  • rec.sport.hockey

  • sci.crypt

  • sci.electronics

  • sci.med

  • sci.space

  • misc.forsale

  • talk.politics.misc

  • talk.politics.guns

  • talk.politics.mideast

  • talk.religion.misc

  • alt.atheism

  • soc.religion.christian

数据集中的所有文档都是英文的。我们可以很容易地从新闻组的名称推断出主题。

该数据集已经标注,每篇文档由文本数据和一个组标签组成。这也使得它非常适合用于监督学习,如文本分类。在本章结束时,您可以根据到目前为止在本书中学到的内容,尝试使用该数据集进行分类练习。

一些新闻组之间关系密切,甚至有重叠——例如,那五个计算机相关的组(comp.graphicscomp.os.ms-windows.misccomp.sys.ibm.pc.hardwarecomp.sys.mac.hardwarecomp.windows.x)。一些新闻组之间的关系则不太密切,比如基督教组(soc.religion.christian)和棒球组(rec.sport.baseball)。

因此,这是无监督学习(如聚类)的一个完美用例,借此我们可以看到相似的主题是否被归类在一起,而不相关的主题是否被分开。此外,我们甚至可以使用主题建模技术发现超越原始 20 个标签的抽象主题。

目前,我们专注于探索和分析文本数据。我们将从获取数据开始。

您可以手动从原始网站或其他许多在线仓库下载数据集。然而,也有很多不同版本的数据集——有些已经以某种方式进行了清理,有些则是原始数据。为了避免混淆,最好使用一致的数据获取方法。scikit-learn 库提供了一个实用函数来加载数据集。数据集一旦下载,将自动缓存。我们无需重复下载同一数据集。

在大多数情况下,缓存数据集,特别是对于相对较小的数据集,是一种良好的实践。其他 Python 库也提供数据下载工具,但并非所有库都实现了自动缓存。这是我们喜爱 scikit-learn 的另一个原因。

和往常一样,我们首先导入 20 个新闻组数据的加载器函数,如下所示:

>>> from sklearn.datasets import fetch_20newsgroups 

然后,我们下载包含所有默认参数的数据集,如下所示:

>>> groups = fetch_20newsgroups()
Downloading 20news dataset. This may take a few minutes.
Downloading dataset from https://ndownloader.figshare.com/files/5975967 (14 MB) 

我们还可以指定一个或多个特定的主题组以及特定的部分(训练集、测试集或两者),并仅加载该数据的子集。加载器函数的所有参数和选项总结在下表中:

参数默认值示例值描述
subset'train''train','test','all'要加载的数据集:训练集、测试集或两者。
data_home~/scikit_learn_data~/myfolder存储和缓存文件的目录。
categoriesNone['sci.space', 'alt.atheism']要加载的新闻组列表。如果是 None,则加载所有新闻组。
shuffleTrueTrue, False布尔值,指示是否洗牌数据。
random_state427, 43用于洗牌数据的随机种子整数。
remove0('headers','footers','quotes')元组,指示要省略每个新闻组帖子中的“头部、尾部和引用”部分。默认情况下不删除任何内容。
download_if_missingTrueTrue, False布尔值,指示如果在本地找不到数据是否下载数据。

表 7.2:fetch_20newsgroups() 函数的参数列表

记住,random_state 对于可重复性很有用。每次运行脚本时,你都能获得相同的数据集。否则,在不同的顺序下处理洗牌数据集可能会带来不必要的变化。

在本节中,我们加载了新闻组数据。接下来,让我们来探索它。

探索新闻组数据

在我们通过任何方式下载 20 个新闻组数据集后,groupsdata 对象会被缓存到内存中。data 对象以键值字典的形式存在。它的键如下:

>>> groups.keys()
dict_keys(['data', 'filenames', 'target_names', 'target', 'DESCR']) 

target_names 键提供了 20 个新闻组的名称:

>>> groups['target_names']
   ['alt.atheism', 'comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware', 'comp.sys.mac.hardware', 'comp.windows.x', 'misc.forsale', 'rec.autos', 'rec.motorcycles', 'rec.sport.baseball', 'rec.sport.hockey', 'sci.crypt', 'sci.electronics', 'sci.med', 'sci.space', 'soc.religion.christian', 'talk.politics.guns', 'talk.politics.mideast', 'talk.politics.misc', 'talk.religion.misc'] 

target 键对应一个新闻组,但它被编码为一个整数:

>>> groups.target
array([7, 4, 4, ..., 3, 1, 8]) 

那么,这些整数的不同值是什么呢?我们可以使用 NumPy 的 unique 函数来弄清楚:

>>> import numpy as np
>>> np.unique(groups.target)
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) 

它们的范围是从 019,表示 groups['target_names'] 中的第 1、2、3、…、20 个新闻组主题。

在多个主题或类别的上下文中,了解主题的分布非常重要。平衡的类别分布最容易处理,因为没有类别被低估或高估。然而,通常我们会遇到偏斜的分布,一个或多个类别占主导地位。

我们将使用seaborn包(seaborn.pydata.org/)来计算类别的直方图,并利用matplotlib包(matplotlib.org/)绘制它。我们可以通过pip来安装这两个包,如下所示:

python -m pip install -U matplotlib
pip install seaborn 

对于conda,你可以执行以下命令行:

conda install -c conda-forge matplotlib
conda install seaborn 

记得在安装seaborn之前先安装matplotlib,因为matplotlibseaborn包的一个依赖项。

现在,让我们显示类别的分布,如下所示:

>>> import seaborn as sns
>>> import matplotlib.pyplot as plt
>>> sns.histplot(groups.target, bins=20)
>>> plt.xticks(range(0, 20, 1))
>>> plt.show() 

请参见以下截图以查看结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_07_04.png

图 7.4:新闻组类别分布

如你所见,分布大致均匀,所以这是我们不必担心的一点。

可视化数据是很好的方法,可以帮助我们大致了解数据的结构,可能出现的问题,以及是否有任何我们需要处理的不规律现象。

其他键非常直观:data包含所有新闻组文档,filenames存储每个文档在你的文件系统中的路径。

现在,让我们通过执行以下命令来查看第一篇文档及其主题编号和名称:

>>> groups.data[0]
"From: lerxst@wam.umd.edu (where's my thing)\nSubject: WHAT car is this!?\nNntp-Posting-Host: rac3.wam.umd.edu\nOrganization: University of Maryland, College Park\nLines: 15\n\n I was wondering if anyone out there could enlighten me on this car I saw\nthe other day. It was a 2-door sports car, looked to be from the late 60s/\nearly 70s. It was called a Bricklin. The doors were really small. In addition,\nthe front bumper was separate from the rest of the body. This is \nall I know. If anyone can tellme a model name, engine specs, years\nof production, where this car is made, history, or whatever info you\nhave on this funky looking car, please e-mail.\n\nThanks,\n- IL\n ---- brought to you by your neighborhood Lerxst ----\n\n\n\n\n"
>>> groups.target[0]
7
>>> groups.target_names[groups.target[0]]
'rec.autos' 

如果random_state没有固定(默认值为42),你可能会在运行前面的脚本时得到不同的结果。

如你所见,第一篇文档来自rec.autos新闻组,且被分配了编号7。阅读这篇文章,我们可以轻松判断它是关于汽车的。实际上,car这个词在文档中出现了好几次。诸如bumper之类的词也显得非常与汽车相关。然而,像doors这样的词不一定与汽车相关,因为它们也可能与家庭装修或其他话题相关。

顺便提一下,区分doorsdoor,或者同一个词的不同大小写(如Doors)并没有太大意义。只有在一些少见的情况下,大小写才是重要的—例如,如果我们试图了解一篇文档是否是关于名为The Doors的乐队,还是关于常见的the doors(木门或其他材料的门)这个概念。

思考文本数据的特征

从前面的分析中,我们可以得出结论,如果我们想要判断一篇文档是否来自rec.autos新闻组,诸如cardoorsbumper等词的出现与否可以是非常有用的特征。一个词的出现与否是一个布尔变量,我们还可以查看某些词的计数。例如,car在文档中出现多次。也许一个词在文本中出现的次数越多,文档与汽车相关的可能性就越大。

计算每个词项的出现次数

看起来我们只关心某些词语的出现次数,或者相关的度量,而不关心词语的顺序。因此,我们可以将文本视为一组词。这被称为词袋模型BoW)。这是一个非常基础的模型,但在实践中效果相当好。我们还可以选择定义一个更复杂的模型,考虑词语的顺序和词性标记(PoS 标签)。然而,这种模型在计算上会更加昂贵,并且编程上更为复杂。实际上,基础的 BoW 模型在大多数情况下已经足够了。我们可以试试看,看看 BoW 模型是否合理。

我们首先将文档转换为矩阵,每行表示每个新闻组文档,每列表示一个词元,或者具体来说,首先是单字词 n-gram。矩阵中每个元素的值表示该词(列)在文档(行)中出现的次数。我们使用的是来自 scikit-learn 的CountVectorizer类来完成这项工作:

>>> from sklearn.feature_extraction.text import CountVectorizer 

计数转换函数的重要参数和选项总结在下表中:

构造函数参数默认值示例值描述
ngram_range(1,1)(1,2), (2,2)输入文本中要提取的 n-gram 的上下限,例如(1,1)表示单词级 n-gram,(1,2)表示单词级和双词级 n-gram。
stop_wordsNone'english' 或列表 ['a','the', 'of'] 或 None要使用的停用词列表:可以是 'english',即内建列表,或者是自定义的输入列表。如果是None,则不删除任何词语。
lowercaseTrueTrue, False是否将所有字符转换为小写字母。
max_featuresNoneNone, 200, 500要考虑的前(最常见的)词元数,如果是None,则考虑所有词元。
binaryFalseTrue, False如果为 true,所有非零计数变为 1。

表 7.3:CountVectorizer()函数的参数列表

我们首先用500个最常见的特征初始化计数向量化器(500 个最频繁的词元):

>>>  count_vector = CountVectorizer(max_features=500) 

如下所示,将其拟合到原始文本数据中:

>>> data_count = count_vector.fit_transform(groups.data) 

现在,计数向量化器捕获前 500 个特征,并从原始文本输入中生成一个词元计数矩阵:

>>> data_count
<11314x500 sparse matrix of type '<class 'numpy.int64'>'
      with 798221 stored elements in Compressed Sparse Row format>
>>> data_count[0]
<1x500 sparse matrix of type '<class 'numpy.int64'>'
      with 53 stored elements in Compressed Sparse Row format> 

结果计数矩阵是一个稀疏矩阵,每行仅存储非零元素(因此,只有 798,221 个元素,而不是11314 * 500 = 5,657,000)。例如,第一篇文档被转换为由53个非零元素组成的稀疏向量。

如果你有兴趣查看整个矩阵,随时可以运行以下代码:

>>> data_count.toarray() 

如果你只想查看第一行,可以运行以下代码:

>>> data_count.toarray()[0] 

让我们来看一下从前面的命令得出的以下输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_07_05.png

图 7.5:计数向量化输出

那么,这 500 个最重要的特征是什么呢?它们可以在以下输出中找到:

>>> print(count_vector. get_feature_names_out())
['00' '000' '10' '100' '11' '12' '13' '14' '145' '15' '16' '17' '18' '19' '1993' '20' '21' '22' '23' '24' '25' '26' '27' '30' '32' '34' '40' '50' '93' 'a86' 'able' 'about' 'above' 'ac' 'access' 'actually' 'address' 'after'
……
……
……
 'well' 'were' 'what' 'when' 'where' 'whether' 'which' 'while' 'who' 'whole' 'why' 'will' 'win' 'window' 'windows' 'with' 'without' 'won' 'word' 'work' 'works' 'world' 'would' 'writes' 'wrong' 'wrote' 'year' 'years' 'yes' 'yet' 'you' 'your'] 

我们的第一次尝试看起来并不完美。显然,最常见的标记是数字,或者像a86这样的字母和数字的组合,这些并不传递重要信息。此外,还有许多没有实际意义的词汇,如youthethemthen。此外,一些词汇包含相同的信息,例如telltolduseusedtimetimes。让我们来解决这些问题。

文本预处理

我们首先保留只有字母的单词,这样像00000以及字母和数字的组合如b8f将被移除。过滤函数定义如下:

>>> data_cleaned = []
>>> for doc in groups.data:
...     doc_cleaned = ' '.join(word for word in doc.split()
                                             if word.isalpha())
...     data_cleaned.append(doc_cleaned) 

这将生成一个清理过的新闻组数据版本。

去除停用词

我们没有提到stop_words作为CountVectorizer中的一个重要参数。停用词是那些对区分文档帮助不大的常见词汇。通常,停用词会给词袋模型增加噪音,因此可以去除。

没有通用的停用词列表。因此,根据你使用的工具或软件包,你会移除不同的停用词集合。以 scikit-learn 为例——你可以查看如下的列表:

>>> from sklearn.feature_extraction import _stop_words
>>> print(_stop_words.ENGLISH_STOP_WORDS)
frozenset({latter', 'somewhere', 'further', 'full', 'de', 'under', 'beyond', 'than', 'must', 'has', 'him', 'hereafter', 'they', 'third', 'few', 'most', 'con', 'thereby', 'ltd', 'take', 'five', 'alone', 'yours', 'above', 'hereupon', 'seeming', 'least', 'over', 'amongst', 'everyone', 'anywhere', 'yourself', 'these', 'name', 'even', 'in', 'forty', 'part', 'perhaps', 'sometimes', 'seems', 'down', 'among', 'still', 'own', 'wherever', 'same', 'about', 'because', 'four', 'none', 'nothing', 'could'
……
……
'myself', 'except', 'whom', 'up', 'six', 'get', 'sixty', 'those', 'whither', 'once', 'something', 'elsewhere', 'my', 'both', 'another', 'one', 'a', 'hasnt', 'everywhere', 'thin', 'not', 'eg', 'someone', 'seem', 'detail', 'either', 'being'}) 

要从新闻组数据中去除停用词,我们只需指定stop_words参数:

>>> count_vector_sw = CountVectorizer(stop_words="english", max_features=500) 

除了停用词之外,你可能会注意到一些名字出现在特征中,比如andrew。我们可以使用刚刚处理过的 NLTK 中的Names语料库来过滤名字。

减少词汇的屈折形式和派生形式

如前所述,我们有两种基本策略来处理来自相同词根的单词——词干提取(stemming)和词形还原(lemmatization)。词干提取是一种更快捷的方法,必要时通过切割字母来实现;例如,words 在词干提取后变成 word。词干提取的结果不一定是一个有效的单词。例如,tryingtry 会变成 tri。而词形还原则较慢但更准确。它通过字典查找,保证返回一个有效的单词。回想一下,我们之前使用 NLTK 实现了词干提取和词形还原。

将所有这些(预处理、去除停用词、词形还原和计数向量化)结合起来,我们得到如下结果:

>>> all_names = set(names.words())
>>> def get_cleaned_data(groups, lemmatizer, remove_words):
        data_cleaned = []
        for doc in groups.data:
...         doc = doc.lower()
...         doc_cleaned = ' '.join(lemmatizer.lemmatize(word)
                                  for word in doc.split()
                                  if word.isalpha() and
                                  word not in remove_words)
...         data_cleaned.append(doc_cleaned)
        return data_cleaned
>>> data_cleaned = get_cleaned_data(groups, lemmatizer, all_names)
>>> data_cleaned_count = count_vector_sw.fit_transform(data_cleaned) 

现在这些特征更加有意义:

>>> print(count_vector_sw.get_feature_names_out())
['able', 'accept', 'access', 'according', 'act', 'action', 'actually', 'add', 'address', 'ago', 'agree', 'algorithm', 'allow', 'american', 'anonymous', 'answer', 'anybody', 'apple', 'application', 'apr', 'april', 'arab', 'area', 'argument', 'armenian', 'article', 'ask', 'asked',
……
……
'video', 'view', 'wa', 'want', 'wanted', 'war', 'water', 'way', 'weapon', 'week', 'went', 'western', 'white', 'widget', 'win', 'window', 'woman', 'word', 'work', 'working', 'world', 'worth', 'write', 'written', 'wrong', 'year', 'york', 'young'] 

我们刚刚将每个原始新闻组文档的文本转换为一个大小为500的稀疏向量。对于每个文档的向量,每个元素表示该单词标记在该文档中出现的次数。此外,这 500 个单词标记是基于文本预处理、去除停用词和词形还原后的整体出现次数来选择的。现在,你可能会问,“这样的出现向量是否足够具有代表性,或者这样的出现向量是否能传递足够的信息,帮助我们区分该文档与其他主题的文档?”你将在下一节中找到答案。

使用 t-SNE 可视化新闻组数据

我们可以通过可视化那些表示向量来轻松回答这些问题。如果我们能看到来自同一主题的文档向量聚成一簇,那说明我们在将文档映射为向量时做得很好。那么,怎么做到的呢?它们有 500 维,而我们只能可视化最多三维的数据。我们可以借助 t-SNE 来进行降维。

什么是降维?

降维是一种重要的机器学习技术,它减少特征的数量,同时尽可能保留更多信息。通常通过获得一组新的主特征来执行降维。

如前所述,处理高维数据时,很难进行可视化。给定一个三维图,我们有时并不容易直接观察出任何发现,更别提 10 维、100 维或 1,000 维了。此外,高维数据中的某些特征可能是相关的,从而带来冗余。这就是为什么我们需要降维的原因。

降维不仅仅是从原始特征空间中去除一对特征。它是将原始特征空间转化为一个维度较低的新空间。数据转换可以是线性的,比如著名的主成分分析PCA),它将高维空间中的数据映射到一个低维空间,在这个空间中数据的方差被最大化,我们将在第九章《用支持向量机识别面孔》中讨论这一点;也可以是非线性的,比如神经网络和即将介绍的 t-SNE。非负矩阵分解NMF)是另一种强大的算法,我们将在第八章《用聚类和主题建模发现新闻组数据集中的潜在主题》中详细学习。

归根结底,大多数降维算法都属于无监督学习范畴,因为在数据转换中并没有使用目标或标签信息(如果有的话)。

t-SNE 降维

t-SNE代表t-分布随机邻域嵌入。它是一种流行的非线性降维技术,由 Laurens van der Maaten 和 Geoffrey Hinton 开发(www.cs.toronto.edu/~hinton/absps/tsne.pdf)。t-SNE 在各个领域的数据可视化中得到了广泛应用,包括计算机视觉、自然语言处理、生物信息学和计算基因组学。

顾名思义,t-SNE 将高维数据嵌入低维(通常是二维或三维)空间,同时尽可能保持数据的局部结构和成对相似性。它首先通过为相似数据点分配较高的概率,为不相似的数据点分配极小的概率,来对数据点周围的邻居建模概率分布。注意,相似度和邻居距离是通过欧几里得距离或其他度量来衡量的。然后,t-SNE 构建一个投影到低维空间,其中输入分布和输出分布之间的散度被最小化。原始的高维空间被建模为高斯分布,而输出的低维空间则被建模为 t 分布。

我们将在此使用来自 scikit-learn 的TSNE类实现 t-SNE:

>>> from sklearn.manifold import TSNE 

现在,让我们使用 t-SNE 来验证我们的计数向量表示。

我们选择了三个不同的主题,talk.religion.misccomp.graphicssci.space,并可视化这三个主题的文档向量。

首先,加载这三个标签的文档,如下所示:

>>> categories_3 = ['talk.religion.misc', 'comp.graphics', 'sci.space']
>>> groups_3 = fetch_20newsgroups(categories=categories_3)
>>> data_cleaned = get_cleaned_data(groups_3, lemmatizer, all_names)
>>> data_cleaned_count_3 = count_vector_sw.fit_transform(data_cleaned) 

我们通过相同的过程,使用输入groups_3生成了一个包含 500 个特征的计数矩阵data_cleaned_count_3。你可以参考前面章节中的步骤,只需重复相同的代码。

接下来,我们应用 t-SNE 将 500 维矩阵降到二维矩阵:

>>> tsne_model = TSNE(n_components=2, perplexity=40,
                     random_state=42, learning_rate=500)
>>> data_tsne = tsne_model.fit_transform(data_cleaned_count_3.toarray()) 

我们在TSNE对象中指定的参数如下:

  • n_components:输出维度

  • perplexity:算法中考虑为邻居的最近数据点的数量,通常值在 5 到 50 之间

  • random_state:程序可重复性的随机种子

  • learning_rate:影响寻找最佳映射空间过程的因素,通常值在 10 到 1,000 之间

注意,TSNE对象只接受稠密矩阵,因此我们使用toarray()将稀疏矩阵data_cleaned_count_3转换为稠密矩阵。

我们刚刚成功地将输入维度从 500 降到了 2。最后,我们可以轻松地在二维散点图中可视化,其中 x 轴是第一维度,y 轴是第二维度,颜色 c 基于每个原始文档的主题标签:

>>> plt.scatter(data_tsne[:, 0], data_tsne[:, 1], c=groups_3.target)
>>> plt.show() 

请参阅以下截图以查看最终结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_07_06.png

图 7.6:将 t-SNE 应用于来自三个不同主题的数据

来自三个主题的数据点呈现不同的颜色——绿色、紫色和黄色。我们可以观察到三个明显的聚类。同一主题的数据点彼此接近,而不同主题的数据点则相距较远。显然,计数向量是原始文本数据的良好表示,因为它们保持了三个不同主题之间的区分。

你也可以尝试调整参数,看看是否能获得一个更好的图形,其中三个聚类被更好地分开。

计数向量化在保持文档差异性方面表现良好。那么,如何保持相似性呢?我们也可以使用来自重叠主题的文档进行检验,譬如这五个主题——comp.graphicscomp.os.ms-windows.misccomp.sys.ibm.pc.hardwarecomp.sys.mac.hardwarecomp.windows.x

>>> categories_5 = ['comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware', 'comp.sys.mac.hardware', 'comp.windows.x']
>>> groups_5 = fetch_20newsgroups(categories=categories_5) 

相似的过程(包括文本清理、计数向量化和 t-SNE)会重复进行,最终得到的图形如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_07_07.png

图 7.7:对来自五个相似主题的数据应用 t-SNE

来自这五个与计算机相关的主题的数据点分布杂乱无章,这意味着它们在语境上是相似的。总结来说,计数向量是原始文本数据的简单而又出色的表示方法,因为它们在保持相关主题之间的相似性方面表现优异。现在问题出现了:我们能否改进单词(术语)计数表示?接下来让我们进入下一部分,探索稠密向量表示。

使用稠密向量表示单词——单词嵌入

单词计数表示会产生一个高维稀疏向量,其中每个元素代表特定单词的频率。回想一下,为了避免这个问题,我们之前只查看了500个最常见的单词。否则,我们必须用一个超过 100 万维的向量表示每个文档(取决于词汇表的大小)。此外,单词计数表示缺乏捕捉单词语义或上下文的能力。它只考虑单词在文档或语料库中的频率。相反,单词嵌入将单词表示在稠密连续)的向量空间中。

使用浅层神经网络构建嵌入模型

单词嵌入将每个单词映射到一个固定维度的稠密向量。它的维度远低于词汇表的大小,通常只有几百维。例如,单词machine可以表示为向量[1.4, 2.1, 10.3, 0.2, 6.81]

那么,我们如何将一个单词嵌入到向量中呢?一种解决方案是word2vec(见 Efficient Estimation of Word Representations in Vector Space,Tomas Mikolov、Kai Chen、Greg Corrado 和 Jeff Dean, arxiv.org/pdf/1301.3781);该方法训练一个浅层神经网络,根据周围的其他单词预测一个单词,这叫做连续词袋模型CBOW),或者根据一个单词预测它周围的其他单词,这叫做跳字模型(skip-gram)。训练好的神经网络的权重系数)就是相应单词的嵌入向量。让我们来看一个具体的例子。

给定语料库中的句子我喜欢通过实例学习 Python 机器学习,以及5作为词窗口的大小,我们可以得到以下 CBOW 神经网络的训练集:

神经网络输入神经网络输出
(I, love, python, machine)(reading)
(love, reading, machine, learning)(python)
(reading, python, learning, by)(machine)
(python, machine, by, example)(learning)

表 7.4:CBOW 神经网络的输入和输出

在训练过程中,神经网络的输入和输出是独热编码向量,值为 1 表示词语存在,值为 0 表示词语不存在。我们可以从语料库中逐句构建数百万个训练样本。在网络训练完成后,连接输入层和隐藏层的权重会嵌入每个输入词。

基于跳字模型的神经网络以类似的方式将词嵌入。但它的输入和输出是 CBOW 的反向版本。给定相同的句子,我喜欢通过实例学习 Python 机器学习,以及5作为词窗口的大小,我们可以得到以下跳字模型神经网络的训练集:

神经网络输入神经网络输出
(reading)(i)
(reading)(love)
(reading)(python)
(reading)(machine)
(python)(love)
(python)(reading)
(python)(machine)
(python)(learning)
(machine)(reading)
(machine)(python)
(machine)(learning)
(machine)(by)
(learning)(python)
(learning)(machine)
(learning)(by)
(learning)(example)

表 7.5:跳字模型神经网络的输入和输出

嵌入向量是实数值,每个维度编码词汇中单词的某个意义方面。这有助于保持单词的语义信息,而不是像使用词频方法的独热编码那样丢失它。一个有趣的现象是,语义相似的单词的向量在几何空间中相互接近。例如,聚类(clustering)和分组(grouping)都指机器学习中的无监督聚类,因此它们的嵌入向量非常接近。词嵌入能够捕捉单词及其上下文的意义。

最佳实践

可视化词嵌入是一个有用的工具,可以帮助探索模式、识别单词之间的关系,并评估嵌入模型的效果。以下是一些可视化词嵌入的最佳实践:

  • 降维:词嵌入通常具有高维向量。为了可视化它们,我们需要降低它们的维度。可以使用 PCA 或 t-SNE 等技术将高维数据投影到二维或三维空间,同时保持数据点之间的距离。

  • 聚类:将相似的单词嵌入聚类在一起,以识别具有相似意义或上下文的单词群体。

利用预训练的嵌入模型

训练一个单词嵌入神经网络可能既费时又消耗计算资源。幸运的是,许多组织和研究机构(如 Google、Meta AI Research、OpenAI、斯坦福 NLP 小组和 Hugging Face)已经基于不同种类的语料库开发了预训练的单词嵌入模型,并使其可以供开发者和研究人员在各种 NLP 任务中使用。我们可以直接使用这些预训练模型将单词映射到向量。以下是一些流行的预训练单词嵌入模型:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_07_08.png

图 7.8:流行的预训练单词嵌入模型配置

一旦我们为单个单词获取了嵌入向量,就可以通过对文档中所有单词的向量进行平均来表示一个文档样本。

最佳实践

一旦你拥有了文档中所有单词的嵌入向量,就将它们聚合成一个单一的向量表示整个文档。常见的聚合方法包括平均和求和。更复杂的方法包括以下几种:

  • 加权平均,其中权重基于单词重要性,如 TF-IDF 分数

  • 最大/最小池化,即对所有单词嵌入的每个维度取最大值或最小值

生成的文档样本向量将被下游预测任务所使用,如分类、搜索引擎中的相似性排名和聚类。

现在让我们来玩一下gensim,一个流行的自然语言处理(NLP)包,拥有强大的单词嵌入模块。

首先,我们导入包并加载一个预训练模型,glove-twitter-25,如下所示:

>>> import gensim.downloader as api
>>> model = api.load("glove-twitter-25")
[==================================================] 100.0%
104.8/104.8MB downloaded 

如果你运行这行代码,你会看到进度条。glove-twitter-25模型是最小的模型之一,所以下载不会花费太长时间。

我们可以通过以下方式获取一个单词(例如computer)的嵌入向量:

>>> vector = model['computer']
>>> print('Word computer is embedded into:\n', vector)
Word computer is embedded into:
[ 0.64005 -0.019514 0.70148 -0.66123 1.1723 -0.58859 0.25917
-0.81541 1.1708 1.1413 -0.15405 -0.11369 -3.8414 -0.87233
  0.47489 1.1541 0.97678 1.1107 -0.14572 -0.52013 -0.52234
 -0.92349 0.34651 0.061939 -0.57375 ] 

结果是一个 25 维的浮动向量,正如预期的那样。

我们还可以通过most_similar方法获取与computer最相关的前 10 个单词,如下所示:

>>> similar_words = model.most_similar("computer")
>>> print('Top ten words most contextually relevant to computer:\n',
           similar_words)
Top ten words most contextually relevant to computer:
 [('camera', 0.907833456993103), ('cell', 0.891890287399292), ('server', 0.8744666576385498), ('device', 0.869352400302887), ('wifi', 0.8631256818771362), ('screen', 0.8621907234191895), ('app', 0.8615544438362122), ('case', 0.8587921857833862), ('remote', 0.8583616018295288), ('file', 0.8575270771980286)] 

结果看起来很有前景。

最后,我们通过一个简单的示例展示如何生成文档的嵌入向量,如下所示:

>>> doc_sample = ['i', 'love', 'reading', 'python', 'machine',
                 'learning', 'by', 'example']
>>> doc_vector = np.mean([model[word] for word in doc_sample],
                                                           axis=0)
>>> print('The document sample is embedded into:\n', doc_vector)
The document sample is embedded into:
 [-0.17100249 0.1388764 0.10616798 0.200275 0.1159925 -0.1515975
  1.1621187 -0.4241785 0.2912 -0.28199488 -0.31453252 0.43692702
 -3.95395 -0.35544625 0.073975 0.1408525 0.20736426 0.17444688
  0.10602863 -0.04121475 -0.34942 -0.2736689 -0.47526264 -0.11842456
 -0.16284864] 

生成的向量是八个输入单词嵌入向量的平均值

在传统的 NLP 应用中,如文本分类和信息检索任务中,单词频率起着重要作用,单词计数表示仍然是一个优秀的解决方案。在更复杂的领域中,如文本摘要、机器翻译和问答系统,需要理解和单词间的语义关系,单词嵌入得到了广泛应用,并且比传统方法提取了更优的特征。

总结

在本章中,你学习了自然语言处理(NLP)作为机器学习一个重要子领域的基本概念,包括分词、词干提取与词形还原以及词性标注。我们还探索了三个强大的 NLP 包,并使用 NLTK 和 spaCy 完成了一些常见任务。随后,我们继续了主要项目,探索了 20 个新闻组数据。我们首先使用分词技术提取特征,经过文本预处理、停用词去除和词形还原。接着,我们进行了降维并用 t-SNE 进行可视化,证明了计数向量化是文本数据的良好表示方法。接下来,我们采用了更现代的表示技术——词嵌入,并展示了如何利用预训练的嵌入模型。

我们在使用降维作为无监督方法挖掘 20 个新闻组数据时玩得很开心。接下来,在下一章中,我们将继续我们的无监督学习之旅,专注于主题建模和聚类。

练习

  1. 你认为排名前 500 的单词标记都包含有价值的信息吗?如果不是,你能否添加另一份停用词表?

  2. 你能否使用词干提取(stemming)而不是词形还原(lemmatization)来处理 20 个新闻组数据?

  3. 你能将CountVectorizer中的max_features500增加到5000,看看 t-SNE 可视化会受到怎样的影响吗?

  4. 尝试使用 Gensim 中的word2vec-google-news-300模型表示本章讨论的三个主题的数据,并用 t-SNE 进行可视化。评估与使用词频表示法在图 7.6中展示的结果相比,是否有所改善。

加入我们书籍的 Discord 空间

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/yuxi

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/QR_Code1878468721786989681.png

第八章:通过聚类和主题建模发现新闻组数据集中的潜在主题

在上一章中,我们使用 t-SNE 进行了文本可视化。t-SNE 或任何降维算法都是一种无监督学习方法。在本章中,我们将继续我们的无监督学习之旅,特别关注聚类和主题建模。我们将从无监督学习如何在没有指导的情况下进行学习以及它如何擅长发现数据中隐藏的信息开始。

接下来,我们将讨论聚类作为无监督学习的重要分支,它从数据中识别不同的观察组。例如,聚类在市场细分中非常有用,可以将具有相似行为的消费者分为一个组进行营销。我们将对 20 个新闻组文本数据集进行聚类,并看看会产生哪些聚类。

我们将采用的另一种无监督学习方法是主题建模,它是从数据集中提取隐藏主题的过程。你会对从 20 个新闻组数据集中挖掘出的许多有趣主题感到惊讶。

我们将涵盖以下主题:

  • 无指导的学习——无监督学习

  • 开始使用 k-means 聚类

  • 聚类新闻组数据

  • 发现新闻组中的潜在主题

无指导的学习——无监督学习

在上一章中,我们应用了 t-SNE 来可视化新闻组文本数据,并将其降维到二维。t-SNE 或一般的降维技术是一种无监督学习。与监督学习不同,无监督学习没有预定义的标签或类别指导(如类别或成员资格(分类)和连续值(回归))。无监督学习识别输入数据中的内在结构或共性。由于无监督学习没有指导,因此没有明确的正确或错误的结果。无监督学习有自由去发现输入数据下面隐藏的信息。

理解无监督学习的一种简单方法是将其比作通过做大量的模拟考试题目来准备考试。在监督学习中,你会得到这些模拟题的答案。你基本上是找出问题和答案之间的关系,学会如何将问题映射到答案上。希望你最终能够通过正确回答问题,顺利通过实际考试。然而,在无监督学习中,你并没有得到这些模拟题的答案。在这种情况下,你可能会做以下几件事:

  • 将相似的练习题归类,以便你可以一次性学习相关的题目

  • 找出那些高度重复的问题,以便你不必为每一个问题单独花费时间去解答

  • 找出稀有的问题,以便你能更好地为它们做好准备

  • 通过去除模板化文本提取每个问题的关键部分,这样你可以直达要点。

你会注意到,这些任务的结果都非常开放。只要能够描述数据下的共同性和结构,它们就是正确的。

机器学习中的特征通常也称为属性观测值预测变量。问题的答案则是机器学习中的标签,也叫目标目标变量。提供答案的练习问题称为有标签数据,而没有答案的练习问题则是无标签数据。无监督学习适用于无标签数据,并在没有指导的情况下对这些信息进行处理。

无监督学习可以包括以下几种类型:

  • 聚类:这意味着根据共同点对数据进行分组,通常用于探索性数据分析。将相似的练习问题分组,正如前面提到的,便是聚类的一个例子。聚类技术广泛应用于客户细分或为营销活动分组相似的在线行为。本章我们将学习流行的 k-means 聚类算法。

  • 关联:这探索两个或多个特征的特定值的共现。异常检测(也称为离群值检测)是一个典型案例,通过它可以识别稀有的观测值。通过异常检测技术可以在前面的例子中识别出稀有问题。

  • 投影:将原始特征空间映射到一个降维空间,保留或提取一组主变量。提取练习问题的关键部分是一个投影的例子,或者更具体地说,是一个降维过程。我们之前学习过的 t-SNE 就是一个很好的例子。

无监督学习在自然语言处理领域广泛应用,主要是因为获取标注文本数据的困难。与数值数据(如房价、股票数据和在线点击流)不同,标注文本有时是主观的、手动的且繁琐的。在挖掘文本数据时,不需要标签的无监督学习算法显得尤为有效。

第七章使用文本分析技术挖掘 20 个新闻组数据集,你体验了使用 t-SNE 来减少文本数据的维度。现在,让我们通过聚类算法和主题建模技术来探索文本挖掘。我们将从对新闻组数据进行聚类开始。

开始学习 k-means 聚类

新 sgroups 数据自带标签,这些标签是新闻组的类别,其中有一些类别之间紧密相关甚至重叠。例如,五个计算机相关的新闻组:comp.graphicscomp.os.ms-windows.misccomp.sys.ibm.pc.hardwarecomp.sys.mac.hardwarecomp.windows.x,以及两个与宗教相关的新闻组:alt.atheismtalk.religion.misc

现在假设我们不知道这些标签,或者它们不存在。相关主题的样本是否会被聚类到一起?我们将使用 k-means 聚类算法来验证。

k-means 聚类是如何工作的?

k-means 算法的目标是根据特征相似性将数据划分为 k 个组。kk 均值聚类模型的预定义属性。每个 k 个聚类由一个质心(聚类的中心)指定,每个数据样本属于与其最接近的质心对应的聚类。在训练过程中,算法根据提供的数据反复更新 k 个质心。具体步骤如下:

  1. 指定 k:算法需要知道最终要生成多少个聚类。

  2. 初始化质心:算法首先从数据集中随机选择 k 个样本作为质心。

  3. 分配聚类:现在我们有了 k 个质心,共享相同最近质心的样本构成一个聚类。因此,创建了 k 个聚类。请注意,距离通常通过欧几里得距离来衡量。也可以使用其他度量标准,如曼哈顿距离切比雪夫距离,它们列在下表中:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_01.png

图 8.1:距离度量

  1. 更新质心:对于每个聚类,我们需要重新计算其中心点,即该聚类中所有样本的均值。k 个质心将更新为相应聚类的均值。这就是算法被称为k-means的原因。

  2. 重复步骤 3 和 4:我们不断重复分配聚类和更新质心的过程,直到模型收敛,即无法再更新质心或质心更新足够小,或者完成了足够多的迭代。

训练后的 k-means 聚类模型的输出包括以下内容:

  • 每个训练样本的聚类 ID,范围从 1 到 k

  • k 个质心,可以用来对新样本进行聚类——新样本将归属于最近的质心对应的聚类。

k-means 聚类算法很容易理解,其实现也很直接,正如你接下来将发现的那样。

从零实现 k-means 算法

我们将以 scikit-learn 中的 iris 数据集为例。首先加载数据并进行可视化。为简化起见,我们这里只使用原始数据集中的两个特征:

>>> from sklearn import datasets
>>> iris = datasets.load_iris()
>>> X = iris.data[:, 2:4]
>>> y = iris.target 

由于数据集包含三种鸢尾花类别,我们将其用三种不同的颜色表示,如下所示:

>>> import numpy as np
>>> from matplotlib import pyplot as plt
>>> plt.scatter(X[:,0], X[:,1], c=y)
>>> plt.show() 

这将为原始数据图生成如下输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_02.png

图 8.2:原始鸢尾花数据集的绘图

假设我们对标签 y 一无所知,我们尝试将数据分成三个组,因为前面的图中似乎有三个簇(或者你可能说有两个,稍后我们会回来讨论)。让我们执行步骤 1指定 k,以及步骤 2初始化质心,通过随机选择三个样本作为初始质心:

>>> k = 3
>>> np.random.seed(0)
>>> random_index = np.random.choice(range(len(X)), k)
>>> centroids = X[random_index] 

我们可视化数据(不再带标签)以及初始的随机质心:

>>> def visualize_centroids(X, centroids):
...     plt.scatter(X[:, 0], X[:, 1])
...     plt.scatter(centroids[:, 0], centroids[:, 1], marker='*',
                                             s=200, c='#050505')
...     plt.show()
>>> visualize_centroids(X, centroids) 

请参见以下截图,查看数据及初始随机质心:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_03.png

图 8.3:具有随机质心的数据点

现在我们执行步骤 3,这包括基于最近的质心分配簇。首先,我们需要定义一个计算距离的函数,该距离通过欧几里得距离来度量,如下所示:

>>> def dist(a, b):
...     return np.linalg.norm(a - b, axis=1) 

然后,我们开发一个函数,将样本分配给最近的质心簇:

>>> def assign_cluster(x, centroids):
...     distances = dist(x, centroids)
...     cluster = np.argmin(distances)
...     return cluster 

分配完簇后,我们执行步骤 4,即将质心更新为各个簇中所有样本的均值:

>>> def update_centroids(X, centroids, clusters):
...     for i in range(k):
...         cluster_i = np.where(clusters == i)
...         centroids[i] = np.mean(X[cluster_i], axis=0) 

最后,我们有步骤 5,这涉及重复步骤 3步骤 4,直到模型收敛并且出现以下任一情况:

  • 质心移动小于预设的阈值

  • 已经进行了足够的迭代

我们设置了第一个条件的容差和最大迭代次数,如下所示:

>>> tol = 0.0001
>>> max_iter = 100 

初始化簇的起始值,并为所有样本初始化簇,如下所示:

>>> iter = 0
>>> centroids_diff = 100000
>>> clusters = np.zeros(len(X)) 

所有组件准备好后,我们可以逐轮训练模型,其中首先检查收敛性,然后执行步骤 3步骤 4,最后可视化最新的质心:

>>> from copy import deepcopy
>>> while iter < max_iter and centroids_diff > tol:
...     for i in range(len(X)):
...         clusters[i] = assign_cluster(X[i], centroids)
...     centroids_prev = deepcopy(centroids)
...     update_centroids(X, centroids, clusters)
...     iter += 1
...     centroids_diff = np.linalg.norm(centroids -
                                       centroids_prev)
...     print('Iteration:', str(iter))
...     print('Centroids:\n', centroids)
...     print(f'Centroids move: {centroids_diff:5.4f}')
...     visualize_centroids(X, centroids) 

让我们看看从前面的命令生成的以下输出:

  • 迭代 1:请查看迭代 1 的以下输出:

    Iteration: 1
    Centroids:
    [[1.462      0.246     ]
    [5.80285714 2.11142857]
    [4.42307692 1.44153846]]
    Centroids move: 0.8274 
    

迭代 1 后的质心图如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_04.png

图 8.4:第一次迭代后的 k-means 聚类结果

  • 迭代 2:请查看迭代 2 的以下输出:

    Iteration: 2
    Centroids:
    [[1.462      0.246     ]
    [5.73333333 2.09487179]
    [4.37704918 1.40819672]]
    Centroids move: 0.0913 
    

迭代 2 后的质心图如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_05.png

图 8.5:第二轮迭代后的 k-means 聚类结果

  • 迭代 6:请查看迭代 6 的以下输出(在此我们跳过迭代 3 到 5,以避免冗长):

    Iteration: 6
    Centroids:
    [[1.462      0.246     ]
    [5.62608696 2.04782609]
    [4.29259259 1.35925926]]
    Centroids move: 0.0225 
    

迭代 6 后的质心图如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_06.png

图 8.6:第六轮迭代后的 k-means 聚类结果

  • 迭代 7:请查看迭代 7 的以下输出:

    Iteration: 7
    Centroids:
    [[1.462      0.246     ]
    [5.62608696 2.04782609]
    [4.29259259 1.35925926]]
    Centroids move: 0.0000 
    

迭代 7 后的质心图如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_07.png

图 8.7:第七轮迭代后的 k-means 聚类结果

模型在七次迭代后收敛。结果的聚类中心看起来很有希望,我们也可以绘制聚类:

>>> plt.scatter(X[:, 0], X[:, 1], c=clusters)
>>> plt.scatter(centroids[:, 0], centroids[:, 1], marker='*',
                                           s=200, c='r')
>>> plt.show() 

请参阅以下截图获取最终结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_08.png

图 8.8:数据样本及其学习到的聚类中心

如您所见,围绕相同聚类中心的样本形成了一个聚类。经过七次迭代(如果更改 np.random.seed(0) 中的随机种子,您可能会看到稍多或稍少的迭代次数),模型收敛,并且聚类中心不再更新。

使用 scikit-learn 实现 k-means

在开发了我们自己的 k-means 聚类模型后,我们将讨论如何通过执行以下步骤,使用 scikit-learn 进行更快速的解决方案:

  1. 首先,导入 KMeans 类,并初始化一个包含三个聚类的模型,如下所示:

    >>> from sklearn.cluster import KMeans
    >>> kmeans_sk = KMeans(n_clusters=3, n_init='auto', random_state=42) 
    

KMeans 类接受以下重要参数:

构造器参数默认值示例值描述
n_clusters83, 5, 10k 个聚类
max_iter30010, 100, 500最大迭代次数
tol1e-41e-5, 1e-8声明收敛的容忍度
random_stateNone0, 42用于程序可重复性的随机种子

表 8.1:KMeans 类的参数

  1. 然后,我们将模型拟合到数据上:

    >>> kmeans_sk.fit(X) 
    
  2. 之后,我们可以获得聚类结果,包括数据样本的聚类和各个聚类的中心:

    >>> clusters_sk = kmeans_sk.labels_
    >>> centroids_sk = kmeans_sk.cluster_centers_ 
    
  3. 同样,我们绘制聚类及其中心:

    >>> plt.scatter(X[:, 0], X[:, 1], c=clusters_sk)
    >>> plt.scatter(centroids_sk[:, 0], centroids_sk[:, 1], marker='*', s=200, c='r')
    >>> plt.show() 
    

这将产生以下输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_09.png

图 8.9:使用 scikit-learn 学到的聚类中心及数据样本

使用我们从头实现的模型,我们得到与之前相似的结果。

选择 k 的值

让我们回到之前讨论的 k 值的问题。在前面的例子中,设置为 3 更直观,因为我们知道总共有三个类。然而,在大多数情况下,我们并不知道多少个群组是足够的或高效的,同时算法需要一个特定的 k 值来开始。那么,我们该如何选择 k 的值呢?有一种著名的启发式方法,叫做 肘部法则

在肘部法则中,选择不同的 k 值并训练相应的模型;对于每个训练好的模型,计算 平方误差和SSE,也称为 聚类内距离和),并将其与 k 绘制在一起。请注意,对于一个聚类,平方误差(或聚类内距离)是通过计算每个样本到聚类中心的平方距离之和来得出的。选择最优的 k 值是在 SSE 的边际下降开始急剧减小时,这意味着进一步的聚类不再提供任何实质性的提升。

让我们将肘部法则应用于我们在前一节中介绍的示例(通过示例学习正是本书的重点)。我们在iris数据上对不同的k值执行 k-means 聚类:

>>> X = iris.data
>>> y = iris.target
>>> k_list = list(range(1, 7))
>>> sse_list = [0] * len(k_list) 

我们使用完整的特征空间,并将k值范围从16。然后,我们训练各个模型,并分别记录结果的 SSE:

>>> for k_ind, k in enumerate(k_list):
...     kmeans = KMeans(n_clusters=k, n_init='auto', random_state=42)
...     kmeans.fit(X)
...     clusters = kmeans.labels_
...     centroids = kmeans.cluster_centers_
...     sse = 0
...     for i in range(k):
...         cluster_i = np.where(clusters == i)
...         sse += np.linalg.norm(X[cluster_i] - centroids[i])
...     print(f'k={k}, SSE={sse}')
...     sse_list[k_ind] = sse
k=1, SSE=26.103076447039722
k=2, SSE=16.469773740281195
k=3, SSE=15.089477089696558
k=4, SSE=15.0307321707491
k=5, SSE=14.858930749063735
k=6, SSE=14.883090350867239 

最后,我们绘制 SSE 与不同k范围的关系图,如下所示:

>>> plt.plot(k_list, sse_list)
>>> plt.show() 

这将产生以下输出:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_10.png

图 8.10:k-means 肘部法则 – SSE 与 k 的关系

最佳实践

选择适当的相似度度量来计算 k-means 聚类中的距离,取决于数据的性质和分析的具体目标。一些常见的相似度度量包括以下几种:

  • 欧几里得距离:这是默认的度量方法,适用于连续数据,其中特征值之间的差异很重要。

  • 曼哈顿距离(也称为 L1 范数):它计算两点坐标之间绝对差的总和。适用于高维数据以及维度之间不可直接比较的情况。

  • 余弦相似度:对于文本数据或表示为向量的数据,余弦相似度非常有用,因为在这种情况下,向量的大小不如方向重要。

  • 杰卡德相似度:通过比较两个集合的交集与并集来衡量它们之间的相似度。通常用于二元或类别数据。

显然,肘部点是k=3,因为在3之后,SSE 的下降速度显著减缓。因此,k=3是此情况的最优解,这与有三类花朵的事实一致。

聚类新闻组数据集

现在你应该对 k-means 聚类非常熟悉了。接下来,让我们看看使用该算法能够从新闻组数据集中挖掘出什么。我们将以'alt.atheism''talk.religion.misc''comp.graphics''sci.space'四个类别的所有数据为例。然后,我们将使用 ChatGPT 来描述生成的新闻组聚类。ChatGPT 可以生成关于 k-means 聚类形成的聚类的自然语言描述。这有助于理解每个聚类的特征和主题。

使用 k-means 对新闻组数据进行聚类

我们首先从这些新闻组加载数据,并按第七章中所示进行预处理,使用文本分析技术挖掘 20 个新闻组数据集

>>> from sklearn.datasets import fetch_20newsgroups
>>> categories = [
...     'alt.atheism',
...     'talk.religion.misc',
...     'comp.graphics',
...     'sci.space',
... ]
>>> groups = fetch_20newsgroups(subset='all',
                                categories=categories)
>>> labels = groups.target
>>> label_names = groups.target_names
>>> from nltk.corpus import names
>>> from nltk.stem import WordNetLemmatizer
>>> all_names = set(names.words())
>>> lemmatizer = WordNetLemmatizer()
>>> def get_cleaned_data(groups, lemmatizer, remove_words):
        data_cleaned = []
        for doc in groups.data:
...         doc = doc.lower()
...         doc_cleaned = ' '.join(lemmatizer.lemmatize(word)
                                  for word in doc.split()
                                  if word.isalpha() and
                                  word not in remove_words)
...         data_cleaned.append(doc_cleaned)
...     return data_cleaned
>>> data_cleaned = get_cleaned_data(groups, lemmatizer, all_names) 

然后,我们使用 scikit-learn 中的CountVectorizer将清理后的文本数据转换为计数向量:

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> count_vector = CountVectorizer(stop_words="english",
                        max_features=None, max_df=0.5, min_df=2)
>>> data_cv = count_vector.fit_transform(data_cleaned) 

请注意,我们在这里使用的向量化器并不限制特征(词项)数量,而是限制最小和最大文档频率(min_dfmax_df),它们分别是数据集的 2%和 50%。一个词的文档频率是通过计算数据集中包含该词的文档(样本)的比例来衡量的。这有助于过滤掉那些稀有或无关的术语。

输入数据准备好后,我们现在将尝试将其聚类为四个组,具体如下:

>>> k = 4
>>> kmeans = KMeans(n_clusters=k, n_init='auto', random_state=42)
>>> kmeans.fit(data_cv) 

让我们快速检查一下结果聚类的大小:

>>> clusters = kmeans.labels_
>>> from collections import Counter
>>> print(Counter(clusters))
Counter({3: 3360, 0: 17, 1: 7, 2: 3}) 

聚类看起来并不完全正确,大多数样本(3360个样本)聚集在一个大聚类(聚类 3)中。可能出了什么问题?事实证明,我们基于计数的特征不够具有代表性。对于文本数据,更好的数值表示是词频-逆文档频率tf-idf)。与其简单地使用词项计数,或者所谓的词频tf),它为每个词项频率分配一个与文档频率成反比的加权因子。实际上,术语t在文档D中的idf因子计算如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_001.png

这里,n[D]是文档的总数,n[t]是包含术语t的文档数,1是为了避免除以 0 而加上的常数。

在加入idf因子后,tf-idf表示法减少了常见术语(例如getmake)的权重,强调那些很少出现但传达重要意义的术语。

要使用tf-idf表示法,我们只需将CountVectorizer替换为 scikit-learn 中的TfidfVectorizer,如下所示:

>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> tfidf_vector = TfidfVectorizer(stop_words='english',
                                  max_features=None, max_df=0.5, min_df=2) 

参数max_df用于忽略那些文档频率高于给定阈值的术语。在本例中,出现在超过 50%的文档中的术语将在向量化过程中被忽略。min_df指定了一个术语被包含在输出中的最小文档频率要求。那些出现在少于两个文档中的术语将被忽略。

现在,使用tf-idf向量化器和 k-means 聚类算法在结果特征空间中重新进行特征提取:

>>> data_tv = tfidf_vector.fit_transform(data_cleaned)
>>> kmeans.fit(data_tv)
>>> clusters = kmeans.labels_
>>> print(Counter(clusters))
Counter({1: 1478, 2: 797, 3: 601, 0: 511}) 

聚类结果变得更加合理。

我们还通过检查每个聚类包含的内容以及表示每个聚类的前 10 个术语(具有最高 tf-idf 得分的 10 个术语)来更深入地观察聚类结果:

>>> cluster_label = {i: labels[np.where(clusters == i)] for i in
                                                        range(k)}
>>> terms = tfidf_vector.get_feature_names_out()
>>> centroids = kmeans.cluster_centers_
>>> for cluster, index_list in cluster_label.items():
...     counter = Counter(cluster_label[cluster])
...     print(f'cluster_{cluster}: {len(index_list)} samples')
...     for label_index, count in sorted(counter.items(),
                               key=lambda x: x[1], reverse=True):
...         print(f'- {label_names[label_index]}: {count} samples')
...     print('Top 10 terms:')
...     for ind in centroids[cluster].argsort()[-10:]:
...         print(' %s' % terms[ind], end="")
...     print()
cluster_0: 601 samples
- sci.space: 598 samples
- alt.atheism: 1 samples
- talk.religion.misc: 1 samples
- comp.graphics: 1 samples
Top 10 terms: just orbit moon hst nasa mission launch wa shuttle space
cluster_1: 1478 samples
- alt.atheism: 522 samples
- talk.religion.misc: 387 samples
- sci.space: 338 samples
- comp.graphics: 231 samples
Top 10 terms: say people know like think ha just university wa article
cluster_2: 797 samples
- comp.graphics: 740 samples
- sci.space: 49 samples
- talk.religion.misc: 5 samples
- alt.atheism: 3 samples
Top 10 terms: computer need know looking thanks university program file graphic image
cluster_3: 511 samples
- alt.atheism: 273 samples
- talk.religion.misc: 235 samples
- sci.space: 2 samples
- comp.graphics: 1 samples
Top 10 terms: doe bible think believe say people christian jesus wa god 

从我们在前面的结果中观察到的情况来看:

  • cluster_0显然是关于太空的,并且包含几乎所有的sci.space样本以及相关术语,如orbitmoonnasalaunchshuttlespace

  • cluster_1是更为通用的主题。

  • cluster_2更多的是关于计算机图形学和相关术语,例如computerprogramfilegraphicimage

  • cluster_3 是一个有趣的簇,成功地将两个重叠的主题——无神论和宗教——汇集在一起,关键术语包括biblebelievejesuschristiangod

随时尝试不同的 k 值,或使用肘部法找到最佳值(这实际上是本章后面的一个练习)。

通过聚类找到每个文本组的关键术语非常有趣。如果我们能够根据其关键术语描述每个簇,那将更有趣。让我们看看如何在下一节中通过 ChatGPT 进行此操作。

使用 GPT 描述簇

ChatGPT (chat.openai.com/) 是由 OpenAI (openai.com/) 开发的 AI 语言模型,是 生成预训练变换器GPT)系列模型的一部分,具体基于 GPT-3.5(在撰写时 GPT-4 处于测试阶段)架构。ChatGPT 设计用于与用户进行自然语言对话,并提供类似人类的响应。

该模型在大量来自互联网的多样文本数据上进行了训练,使其能够理解并生成各种主题和背景下类似人类的文本。ChatGPT 能够理解用户提出的问题、提示和指令,并根据其训练生成连贯的响应。

ChatGPT 已在各种应用中使用,包括聊天机器人、虚拟助手、内容生成、语言翻译等等。用户通过 API 调用或交互界面与 ChatGPT 进行交互,模型实时生成响应。然而,需要注意的是,虽然 ChatGPT 能够生成印象深刻且与上下文相关的响应,但由于当前语言模型的限制,它偶尔也可能生成不正确或无意义的答案。应对 ChatGPT 的响应应进行意义检查,以提高生成文本的质量和可靠性,并尽量减少误信息的风险。

我们将请求 ChatGPT 描述我们刚生成的簇的步骤如下。

首先,我们如下获取前 100 个术语:

>>> keywords = ' '.join(
                      terms[ind] for ind in centroids[0].argsort()[-100:])
>>> print(keywords)
big power vehicle using alaska look mass money marketing company loss pluto russian scheduled office express probably research software funding billboard online pat access doe telescope april jet usa digest light want prize forwarded way large mar project sci center command technology air government commercial good work servicing know going comet world propulsion people idea design data university day international use orbital long science need time sky program thing make spencer new year earth spacecraft flight henry billion rocket think ha station lunar solar like cost satellite article toronto zoology just orbit moon hst nasa mission launch wa shuttle space 

chat.openai.com 注册(或登录如果您已有账户)后,我们通过使用提示基于以下关键词描述一个共同的主题:要求 ChatGPT 描述主题。请参考下图以获取完整的问题和答案:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_11.png

图 8.11: 要求 ChatGPT 描述第 0 簇的主题

正如 ChatGPT 正确指出的那样,“共同的主题围绕着太空探索、研究、技术和任务的各个方面,提到该领域的关键人物和天体。” 随时可以对其他簇重复相同的过程。您还可以通过遵循以下步骤在 Python 中使用 ChatGPT API 实现相同效果:

  1. 使用pip安装 OpenAI 库:

    pip install openai 
    

您也可以使用conda进行此操作:

conda install openai 
  1. platform.openai.com/account/api-keys 生成一个 API 密钥。请注意,你需要先登录或注册才能进行此操作。

  2. 导入库并设置你的 API 密钥:

    >>> import openai
    >>> openai.api_key = '<YOUR API KEY>' 
    
  3. 创建一个函数,允许你从 ChatGPT 获取响应:

    >>> def get_completion(prompt, model="text-davinci-003"):
        messages = [{"role": "user", "content": prompt}]
        response = openai.ChatCompletion.create(
            model=model,
            messages=messages,
            temperature=0
        )
        return response.choices[0].message["content"] 
    

在这里,我们使用 text-davinci-003 模型。欲了解更多有关不同模型的信息,请访问 platform.openai.com/docs/models

  1. 查询 API:

    >>> response = get_completion(f"Describe a common topic based on the 
        following keywords: {keywords}")
    >>> print(response) 
    

这将返回一个类似于你在网页界面中看到的响应。请注意,API 调用受到你所选计划配额的限制。

到目前为止,我们通过首先将文档分组为聚类,然后提取每个聚类中的顶级术语,来生成主题关键词。主题建模是另一种生成主题关键词的方法,但它以一种更加直接的方式进行。它不仅仅是搜索预先生成的单独聚类中的关键术语。它的做法是直接从文档中提取关键术语的集合。你将在下一节中看到它是如何工作的。

基于密度的空间聚类应用与噪声 (DBSCAN) 是另一种流行的聚类算法,用于识别空间数据中的聚类。与像 k-means 这样的基于质心的算法不同,DBSCAN 不需要提前指定聚类的数量,并且可以发现任意形状的聚类。它通过将数据集划分为连续的高密度区域的聚类,这些区域由低密度区域分隔,同时将异常值标记为噪声。

该算法需要两个参数:epsilon (https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_002.png),定义了两个样本之间的最大距离,使其被视为同一邻域的一部分,以及 min_samples,它指定了形成密集区域所需的最小样本数。

DBSCAN 从随机选择一个点开始,扩展其邻域,找到所有在 ε 距离内可达的点。如果可达点的数量超过 min_samples,该点被标记为核心点,并形成一个新的聚类。这个过程会对所有核心点及其邻域递归执行,直到所有点都被分配到某个聚类中或标记为噪声。

发现新闻组中的潜在主题

主题模型是一种用于发现与主题相关的词汇概率分布的统计模型。在主题建模中,"主题"的定义并不完全符合字典中的解释,而是对应于一个模糊的统计概念,它是文档集合中出现的一个抽象。

当我们阅读一篇文档时,我们期望某些出现在标题或正文中的词汇能够捕捉文档的语义上下文。例如,一篇关于 Python 编程的文章可能会包含classfunction等词汇,而一篇关于蛇的故事可能会包含eggsafraid等词汇。文档通常涉及多个主题;例如,本节讨论的内容包括三项内容:主题建模、非负矩阵分解和潜在狄利克雷分配,我们将很快讨论这些内容。因此,我们可以通过为每个主题分配不同的权重来定义一个加法模型。

主题建模广泛应用于挖掘给定文本数据中的潜在语义结构。有两种流行的主题建模算法——非负矩阵分解NMF)和潜在狄利克雷分配LDA)。我们将在接下来的两节中介绍这两种算法。

使用 NMF 进行主题建模

非负矩阵分解NMF)是一种用于特征提取和数据表示的降维技术。它将一个非负输入矩阵V分解为两个较小矩阵WH的乘积,使得这三个矩阵都不含负值。这两个低维矩阵表示特征及其关联的系数。在自然语言处理的背景下,这三个矩阵具有以下含义:

  • 输入矩阵V是一个大小为n × m的词项计数或 tf-idf 矩阵,其中n是文档或样本的数量,m是词项的数量。

  • 第一个分解输出矩阵W是一个大小为t × m的特征矩阵,其中t是指定的主题数量。W的每一行表示一个主题,每行中的每个元素表示该主题中某个词项的排名。

  • 第二个分解输出矩阵H是一个大小为n × t的系数矩阵。H的每一行表示一个文档,每个元素表示该文档中某个主题的权重。

如何推导WH的计算超出了本书的范围。然而,你可以参考以下示例,以更好地理解 NMF 是如何工作的:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_08_12.png

图 8.12:从输入矩阵 V 中得到的矩阵 W 和矩阵 H 示例

如果你对阅读更多关于 NMF 的内容感兴趣,可以查看 Inderjit S. Dhillon 和 Suvrit Sra 在 NIPS 2005 中发布的原始论文《Generalized Nonnegative Matrix Approximations with Bregman Divergences》。

现在让我们将 NMF 应用到我们的新闻组数据上。Scikit-learn 有一个很好的分解模块,其中包含 NMF:

>>> from sklearn.decomposition import NMF
>>> t = 20
>>> nmf = NMF(n_components=t, random_state=42) 

我们指定了 20 个主题(n_components)作为示例。模型的主要参数包括在下表中:

构造器参数默认值示例值描述
n_componentsNone51020组件数量——在话题建模中,这对应于话题的数量。如果是 None,则为输入特征的数量。
max_iter200100200最大迭代次数。
tol1e-41e-5, 1e-8声明收敛的容差。

表 8.2:NMF 类的参数

我们将词条矩阵作为输入传递给 NMF 模型,但你也可以使用 tf-idf 矩阵。现在,拟合 NMF 模型 nmf,基于词条矩阵 data_cv

>>> nmf.fit(data_cv) 

我们可以在模型训练完成后获得结果话题特征排名 W

>>> print(nmf.components_)
[[0.00000000e+00 0.00000000e+00 0.00000000e+00 ... 0.00000000e+00
  0.00000000e+00 1.82524532e-04]
 [0.00000000e+00 0.00000000e+00 0.00000000e+00 ... 0.00000000e+00
  7.77697392e-04 3.85995474e-03]
 [0.00000000e+00 0.00000000e+00 0.00000000e+00 ... 0.00000000e+00
  0.00000000e+00 0.00000000e+00]
 ...
 [0.00000000e+00 0.00000000e+00 0.00000000e+00 ... 2.71332203e-02
  0.00000000e+00 0.00000000e+00]
 [0.00000000e+00 0.00000000e+00 0.00000000e+00 ... 0.00000000e+00
  0.00000000e+00 4.31048632e-05]
 [0.00000000e+00 0.00000000e+00 0.00000000e+00 ... 0.00000000e+00
  0.00000000e+00 0.00000000e+00]] 

对于每个话题,我们根据其排名显示前 10 个词条:

>>> terms_cv = count_vector.get_feature_names_out()
>>> for topic_idx, topic in enumerate(nmf.components_):
...         print("Topic {}:" .format(topic_idx))
...         print(" ".join([terms_cv[i] for i in topic.argsort()[-10:]]))
Topic 0:
available quality program free color version gif file image jpeg
Topic 1:
ha article make know doe say like just people think
Topic 2:
include available analysis user software ha processing data tool image
Topic 3:
atmosphere kilometer surface ha earth wa planet moon spacecraft solar
Topic 4:
communication technology venture service market ha commercial space satellite launch
Topic 5:
verse wa jesus father mormon shall unto mcconkie lord god
Topic 6:
format message server object image mail file ray send graphic
Topic 7:
christian people doe atheism believe religion belief religious god atheist
Topic 8:
file graphic grass program ha package ftp available image data
Topic 9:
speed material unified star larson book universe theory physicist physical
Topic 10:
planetary station program group astronaut center mission shuttle nasa space
Topic 11:
infrared high astronomical center acronym observatory satellite national telescope space
Topic 12:
used occurs true form ha ad premise conclusion argument fallacy
Topic 13:
gospel people day psalm prophecy christian ha matthew wa jesus
Topic 14:
doe word hanging say greek matthew mr act wa juda
Topic 15:
siggraph graphic file information format isbn data image ftp available
Topic 16:
venera mar lunar surface space venus soviet mission wa probe
Topic 17:
april book like year time people new did article wa
Topic 18:
site retrieve ftp software data information client database gopher search
Topic 19:
use look xv color make program correction bit gamma image 

有许多有趣的话题,例如:

  • 与计算机图形学相关的主题,如 0268

  • 与太空相关的主题,如 349

  • 与宗教相关的,如 5713

一些话题,例如 112,很难解释。这完全正常,因为话题建模是一种自由形式的学习方法。

使用 LDA 进行话题建模

让我们探索另一个流行的话题建模算法——潜在狄利克雷分配LDA)。LDA 是一种生成式概率图模型,通过一定概率的主题混合来解释每个输入文档。它假设每个文档是多个话题的混合,每个话题由特定的词概率分布表示。该算法通过迭代将文档中的词分配给话题,并根据观察到的词共现更新话题分布。再说一次,话题建模中的话题是指具有某种联系的词的集合。换句话说,LDA 基本上处理的是两个概率值,P(term V topic) 和 P(topic V document)。刚开始时这可能很难理解。所以,让我们从最基础的地方开始,LDA 模型的最终结果。

让我们来看一下以下文档集:

Document 1: This restaurant is famous for fish and chips.
Document 2: I had fish and rice for lunch.
Document 3: My sister bought me a cute kitten.
Document 4: Some research shows eating too much rice is bad.
Document 5: I always forget to feed fish to my cat. 

现在,假设我们想要两个话题。由这些文档派生的话题可能如下所示:

Topic 1: 30% fish, 20% chip, 30% rice, 10% lunch, 10% restaurant (which we can interpret Topic 1 to be food related)
Topic 2: 40% cute, 40% cat, 10% fish, 10% feed (which we can interpret Topic 1 to be about pet) 

因此,我们找到每个文档如何通过这两个话题来表示:

Document 1: 85% Topic 1, 15% Topic 2
Document 2: 88% Topic 1, 12% Topic 2
Document 3: 100% Topic 2
Document 4: 100% Topic 1
Document 5: 33% Topic 1, 67% Topic 2 

在看到一个玩具示例后,我们回到它的学习过程:

  1. 指定话题的数量 T。现在我们有话题 1、2、……、T

  2. 对于每个文档,随机为文档中的每个词分配一个话题。

  3. 对于每个文档,计算 P(topic = t V document),即文档中分配给话题 t 的词条所占比例。

  4. 对于每个话题,计算 P(term = w V topic),即词条 w 在所有分配给该话题的词条中的比例。

  5. 对于每个词条 w,根据最新的概率 P(topic = t V document) 和 P(term = w V topic = t) 重新分配其话题。

  6. 在每次迭代中,重复 步骤 3步骤 5,并基于最新的话题分布进行训练。如果模型收敛或达到最大迭代次数,则停止训练。

LDA 是以生成方式进行训练的,它试图从文档中抽象出一组隐藏的主题,这些主题可能会生成某些特定的单词集合。

记住,LDA 模型在实际应用中的表现。LDA 模型也包括在 scikit-learn 库中:

>>> from sklearn.decomposition import LatentDirichletAllocation
>>> t = 20
>>> lda = LatentDirichletAllocation(n_components=t,
                      learning_method='batch',random_state=42) 

再次,我们指定了 20 个主题(n_components)。模型的关键参数包括在下表中:

构造函数参数默认值示例值描述
n_components105, 10, 20组件数——在主题建模的上下文中,这对应于主题的数量。
learning_method"batch""online", "batch"batch模式下,每次更新都使用所有训练数据。在online模式下,每次更新都使用一个小批量的训练数据。一般来说,如果数据量大,online模式更快。
max_iter1010, 20最大迭代次数。
randome_stateNone0, 42随机数生成器使用的种子。

表 8.3:LatentDirichletAllocation 类的参数

对于 LDA 的输入数据,请记住 LDA 只接受词频计数,因为它是一个概率图模型。这不同于 NMF,后者可以使用词频矩阵或 tf-idf 矩阵,只要它们是非负数据。再次,我们使用之前定义的词矩阵作为 LDA 模型的输入。现在,我们在词矩阵data_cv上拟合 LDA 模型:

>>> lda.fit(data_cv) 

我们可以在模型训练完成后获得结果的主题词排名:

>>> print(lda.components_)
[[0.05     2.05    2.05    ...   0.05      0.05    0.05 ]
 [0.05     0.05    0.05    ...   0.05      0.05    0.05 ]
 [0.05     0.05    0.05    ...   4.0336285 0.05    0.05 ]
 ...
 [0.05     0.05    0.05    ...   0.05      0.05    0.05 ]
 [0.05     0.05    0.05    ...   0.05      0.05    0.05 ]
 [0.05     0.05    0.05    ...   0.05      0.05    3.05 ]] 

类似地,对于每个主题,我们根据它们的排名显示前 10 个词,如下所示:

>>> for topic_idx, topic in enumerate(lda.components_):
...         print("Topic {}:" .format(topic_idx))
...         print(" ".join([terms_cv[i] for i in
                                   topic.argsort()[-10:]]))
Topic 0:
atheist doe ha believe say jesus people christian wa god
Topic 1:
moment just adobe want know ha wa hacker article radius
Topic 2:
center point ha wa available research computer data graphic hst
Topic 3:
objective argument just thing doe people wa think say article
Topic 4:
time like brian ha good life want know just wa
Topic 5:
computer graphic think know need university just article wa like
Topic 6:
free program color doe use version gif jpeg file image
Topic 7:
gamma ray did know university ha just like article wa
Topic 8:
tool ha processing using data software color program bit image
Topic 9:
apr men know ha think woman just university article wa
Topic 10:
jpl propulsion mission april mar jet command data spacecraft wa
Topic 11:
russian like ha university redesign point option article space station
Topic 12:
ha van book star material physicist universe physical theory wa
Topic 13:
bank doe book law wa article rushdie muslim islam islamic
Topic 14:
think gopher routine point polygon book university article know wa
Topic 15:
ha rocket new lunar mission satellite shuttle nasa launch space
Topic 16:
want right article ha make like just think people wa
Topic 17:
just light space henry wa like zoology sky article toronto
Topic 18:
comet venus solar moon orbit planet earth probe ha wa
Topic 19:
site format image mail program available ftp send file graphic 

我们刚刚挖掘出了一些有趣的话题,例如:

  • 与计算机图形学相关的话题,如256819

  • 与空间相关的话题,如10111215

  • 与宗教相关的话题,如013

也有一些涉及噪声的话题,例如916,这些可能需要一些想象力来进行解释。再一次,考虑到 LDA 或主题建模,如前所述,属于自由形式学习的范畴,这种观察是完全可以预期的。

摘要

本章的项目是关于在新闻组数据中找到隐藏的相似性,无论是语义组、主题还是词云。我们从无监督学习的功能和典型的无监督学习算法类型开始。接着,我们介绍了无监督学习中的聚类,并详细研究了一种流行的聚类算法——k 均值算法。我们还探讨了如何使用 ChatGPT 根据关键词描述各个聚类的主题。

我们还讨论了 tf-idf 作为文本数据的更高效的特征提取工具。之后,我们对新闻组数据进行了 k-means 聚类,并得到了四个有意义的聚类。通过检查每个聚类中的关键术语,我们直接使用主题建模技术从原始文档中提取代表性术语。我们讨论并实现了两种强大的主题建模方法——NMF 和 LDA。最后,我们通过解释这两种方法得到的主题,玩得不亦乐乎。

到目前为止,我们已经涵盖了所有主要的无监督学习类别,包括降维、聚类和主题建模,后者在某种程度上也是一种降维方法。

在下一章,我们将讨论 支持向量机 (SVMs) 在人脸识别中的应用。SVM 是一种广泛应用于各类分类和回归任务的流行选择,尤其是在处理复杂决策边界时。我们还将介绍另一种降维技术——主成分分析。

练习

  1. 请让 ChatGPT 描述我们通过 k-means 聚类生成的其他聚类。你可以尝试不同的提示词,发现这些聚类中的有趣信息。

  2. 对新闻组数据执行 k-means 聚类,使用不同的 k 值,或者使用肘部法则来找到最佳的 k 值。看看是否能得到更好的分组结果。

  3. 尝试在 NMF 或 LDA 中使用不同的主题数,看看哪一个能产生更有意义的主题。这个练习应该会很有趣。

  4. 你能在整个 20 个新闻组数据上尝试 NMF 或 LDA 吗?最终的主题是充满噪音还是有价值的发现?

加入我们书籍的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/yuxi

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/QR_Code187846872178698968.png

第九章:使用支持向量机识别人脸

在上一章中,我们使用聚类和主题建模技术发现了潜在的主题。本章将继续我们对监督学习和分类的探索,特别是强调支持向量机SVM)分类器。

在高维空间中,SVM 是最受欢迎的算法之一。该算法的目标是找到一个决策边界,以便将不同类别的数据分开。我们将详细讨论它是如何工作的。同时,我们将使用 scikit-learn 实现该算法,并将其应用于解决各种现实生活中的问题,包括我们的主要项目——人脸识别。本章还将介绍一种称为主成分分析的降维技术,它可以提升图像分类器的性能,此外还会涉及支持向量回归。

本章探讨以下主题:

  • 使用 SVM 寻找分隔边界

  • 使用 SVM 分类人脸图像

  • 使用支持向量回归进行估计

使用 SVM 寻找分隔边界

SVM 是另一种优秀的分类器,尤其在高维空间或维度数量大于样本数量的情况下表现出色。

在机器学习分类中,SVM 找到一个最优的超平面,能够最好地将不同类别的观察数据分开。

超平面是一个具有* n - 1 维度的平面,它将观察的 n *维特征空间分割为两个空间。例如,在二维特征空间中的超平面是一个直线,在三维特征空间中,超平面是一个面。选择最佳超平面是为了最大化其在每个空间中到最近点的距离,这些最近点就是所谓的支持向量

以下的玩具示例展示了在二分类问题中,支持向量和分隔超平面(以及稍后我会解释的距离边界)是什么样子的:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_01.png

图 9.1:二分类中支持向量和超平面的示例

支持向量机(SVM)的最终目标是找到一个最优的超平面,但迫切的问题是“我们如何找到这个最优超平面?”在接下来的探索中,你会得到答案。这并不像你想象的那么困难。我们首先要看的,是如何找到一个超平面。

场景 1 – 确定分隔超平面

首先,你需要理解什么样的超平面才是分隔超平面。在以下示例中,超平面C是唯一正确的,它成功地按标签将观察结果分隔开,而超平面AB都未能做到这一点:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_02.png

图 9.2:合格与不合格超平面的示例

这是一个容易观察到的现象。接下来,让我们以正式或数学的方式表达一个分隔超平面。

在二维空间中,一条直线可以由一个斜率向量 w(表示为二维向量)和一个截距 b 来定义。类似地,在 n 维空间中,一个超平面可以由一个 n 维向量 w 和截距 b 来定义。任何位于超平面上的数据点 x 都满足 wx + b = 0。如果满足以下条件,则该超平面是分隔超平面:

  • 对于来自某一类别的任意数据点 x,它满足 wx + b > 0

  • 对于来自另一个类别的任意数据点 x,它满足 wx + b < 0

然而,wb 可能有无数种解。你可以在一定范围内移动或旋转超平面 C,它仍然保持分隔超平面。接下来,你将学习如何从多个可能的分隔超平面中识别出最佳超平面。

场景 2 - 确定最优超平面

看下面的例子:超平面 C 更受青睐,因为它能够最大化正侧最近数据点与自身之间的距离和负侧最近数据点与自身之间的距离之和:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_03.png

图 9.3:最优和次优超平面的示例

正侧最近的点可以构成一个与决策超平面平行的超平面,我们称之为正超平面;相反,负侧最近的点可以构成负超平面。正负超平面之间的垂直距离被称为间隔,其值等于前述两者的距离之和。如果间隔最大化,则该决策超平面被认为是最优的。

训练后的 SVM 模型中的最优(也叫最大间隔)超平面和距离间隔如以下图所示。再次强调,位于间隔上的样本(每一类别各两个样本,另一个类别一个样本,如图所示)即为所谓的支持向量

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_04.png

图 9.4:最优超平面和距离间隔的示例

我们可以通过首先描述正负超平面来从数学上进行解释,具体如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_001.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_002.png

这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_003.png 是正超平面上的一个数据点,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_004.png 是负超平面上的一个数据点。

点与决策超平面之间的距离可以通过以下方式计算:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_006.png

同样,点与决策超平面之间的距离如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_008.png

所以边距变成了 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_009.png。因此,我们需要最小化 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_010.png,以最大化边距。重要的是,为了符合正负超平面上的支持向量是离决策超平面最近的数据点这一事实,我们添加了一个条件:没有数据点位于正负超平面之间:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_011.png

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_012.png

这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_013.png 是一个观察值。这可以进一步组合成如下形式:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_014.png

总结一下,决定 SVM 决策超平面的 wb,是通过以下优化问题进行训练和求解的:

为了解决这个优化问题,我们需要借助二次规划技术,这超出了我们学习旅程的范围。因此,我们不会详细讲解计算方法,而是使用 scikit-learn 中的 SVCLinearSVC 模块来实现分类器,这两个模块分别基于 libsvm (www.csie.ntu.edu.tw/~cjlin/libsvm/) 和 liblinear (www.csie.ntu.edu.tw/~cjlin/liblinear/),这两个是流行的开源 SVM 机器学习库。不过,理解 SVM 的计算概念始终是很有价值的。

Pegasos: SVM 的原始估计子梯度求解器(《数学编程》,2011 年 3 月,第 127 卷,第 1 期,页码 3–30)由 Shai Shalev-Shwartz 等人编写,大规模线性 SVM 的对偶坐标下降法(《第 25 届国际机器学习大会论文集》,页码 408–415)由 Cho-Jui Hsieh 等人编写,是非常好的学习材料。它们涵盖了两种现代方法:子梯度下降法和坐标下降法。

然后,学习到的模型参数 wb 会被用来根据以下条件对新样本 x’ 进行分类:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_021.png

此外,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_022.png 可以表示为数据点 x’ 到决策超平面的距离,也可以解释为预测的置信度:值越大,数据点离决策边界越远,因此预测的确定性越高。

虽然你可能迫不及待想实现 SVM 算法,但我们还是先退后一步,看看一个常见场景,在这个场景中数据点并不是严格线性可分的。试着在以下示例中找到一个分割超平面:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_05.png

图 9.5:一个数据点不能严格线性分割的示例

如何处理那些无法严格线性分隔包含离群值的观察值集的情况呢?让我们在下一节中探讨。

场景 3 – 处理离群值

为了处理无法线性分隔包含离群值的观察值集的场景,我们实际上可以允许离群值的误分类,并尝试最小化由此引入的误差。样本 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_024.png 的误分类误差 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_023.png(也称为 铰链损失)可以表示如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_025.png

连同我们希望减少的最终项 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_026.png,我们希望最小化的最终目标值变为如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_027.png

对于包含 m 个样本的训练集 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_028.pnghttps://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_029.png,… https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_030.png,… 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_031.png,其中超参数 C 控制两项之间的权衡,以下公式适用:

  • 如果选择了较大的 C 值,误分类的惩罚会变得相对较高。这意味着数据分隔的经验法则变得更加严格,模型可能容易过拟合,因为在训练过程中允许的错误很少。具有大 C 值的 SVM 模型具有低偏差,但可能会遭遇高方差。

  • 相反,如果 C 的值足够小,误分类的影响会变得相对较低。这个模型允许比大 C 值模型更多的误分类数据点。因此,数据分隔变得不那么严格。这样的模型具有低方差,但可能会受到高偏差的影响。

下图展示了大与小的 C 值之间的对比:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_06.png

图 9.6:C 值如何影响分隔的严格性和边际

参数 C 决定了偏差与方差之间的平衡。它可以通过交叉验证进行微调,接下来我们将进行实践。

实现 SVM

我们已经大致讲解了 SVM 分类器的基础知识。现在,让我们立即将其应用于一个简单的二分类数据集。我们将使用 scikit-learn 中经典的乳腺癌威斯康星数据集(scikit-learn.org/stable/modules/generated/sklearn.datasets.load_breast_cancer.html)。

让我们看看以下步骤:

  1. 我们首先加载数据集并进行一些基础分析,如下所示:

    >>> from sklearn.datasets import load_breast_cancer
    >>> cancer_data = load_breast_cancer()
    >>> X = cancer_data.data
    >>> Y = cancer_data.target
    >>> print('Input data size :', X.shape)
    Input data size : (569, 30)
    >>> print('Output data size :', Y.shape)
    Output data size : (569,)
    >>> print('Label names:', cancer_data.target_names)
    Label names: ['malignant' 'benign']
    >>> n_pos = (Y == 1).sum()
    >>> n_neg = (Y == 0).sum()
    >>> print(f'{n_pos} positive samples and {n_neg} negative samples.')
    357 positive samples and 212 negative samples. 
    

如你所见,数据集包含 569 个样本,具有 30 个特征;其标签是二元的,其中 63% 的样本为正样本(良性)。再次提醒,在尝试解决任何分类问题之前,请始终检查类别是否不平衡。在这种情况下,它们是相对平衡的。

  1. 接下来,我们将数据分为训练集和测试集:

    >>> from sklearn.model_selection import train_test_split
    >>> X_train, X_test, Y_train, Y_test = train_test_split(X, Y, random_state=42) 
    

为了保证可复现性,别忘了指定随机种子。

  1. 我们现在可以将 SVM 分类器应用于数据。我们首先初始化一个 SVC 模型,并将 kernel 参数设置为 linear(线性核指的是使用线性决策边界来分隔输入空间中的类。我将在 场景 5 中解释什么是核)和惩罚超参数 C 设置为默认值 1.0

    >>> from sklearn.svm import SVC
    >>> clf = SVC(kernel='linear', C=1.0, random_state=42) 
    
  2. 然后,我们在训练集上拟合我们的模型,如下所示:

    >>> clf.fit(X_train, Y_train) 
    
  3. 然后,我们使用训练好的模型对测试集进行预测,直接获得预测准确率:

    >>> accuracy = clf.score(X_test, Y_test)
    >>> print(f'The accuracy is: {accuracy*100:.1f}%')
    The accuracy is: 95.8% 
    

我们的第一个 SVM 模型表现得非常好,达到了 95.8% 的准确率。那么,如何处理多个主题呢?SVM 如何处理多类分类?

场景 4 – 处理多于两类的情况

SVM 和许多其他分类器可以应用于多类情况。我们可以采取两种典型方法,一对多(也称为 一对全)和 一对一

一对多

在一对多设置中,对于 K 类问题,我们构建 K 个不同的二元 SVM 分类器。对于第 k 个分类器,它将第 k 类作为正类,剩余的 K-1 类作为负类整体;表示为 (w[k], b[k]) 的超平面被训练用来分隔这两类。为了预测一个新样本 x’ 的类别,它比较从 1K 的各个分类器得出的预测结果 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_032.png。正如我们在前一部分讨论的,值较大的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_033.png 表示 x’ 属于正类的置信度更高。因此,它将 x’ 分配给预测结果中值最大的类别 i

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_035.png

下图展示了一对多策略如何在三类情况下工作:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_07.png

图 9.7:使用一对多策略的三类分类示例

例如,如果我们有以下内容(rbg 分别表示红十字、蓝点和绿方块类):

w[r]x’+b[r] = 0.78

w[b]x’+b[b] = 0.35

w[g]x’+b[g] = -0.64

我们可以说 x’ 属于红十字类,因为 0.78 > 0.35 > -0.64

如果我们有以下内容:

w[r]x’+b[r] = -0.78

w[b]x’+b[b] = -0.35

w[g]x’+b[g] = -0.64

那么我们可以确定 x’ 属于蓝点类,无论符号如何,因为 -0.35 > -0.64 > -0.78

一对一

在一对一策略中,我们通过构建一组 SVM 分类器进行成对比较,以区分每对类之间的数据点。这将产生 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_036.png 个不同的分类器。

对于与类ij相关联的分类器,超平面(w[ij],b[ij])仅在基于i(可以看作正例)和j(可以看作负例)的观测数据上进行训练;然后,它会根据w[ij]x’+b[ij]的符号,将类ij分配给新样本x’。最后,拥有最多分配的类被视为x’的预测结果。获得最多投票的类为最终胜者。

下图展示了在三类问题中一对一策略的工作原理:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_08.png

图 9.8:使用一对一策略的三类分类示例

一般来说,使用一对多设置的 SVM 分类器与使用一对一设置的分类器在准确性上表现相似。这两种策略的选择主要取决于计算的需求。

尽管一对一方法需要更多的分类器,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_037.png,与一对多方法(K)相比,每对分类器只需要在数据的小子集上进行学习,而不是在一对多设置下使用整个数据集。因此,在一对一设置下训练 SVM 模型通常更加节省内存和计算资源;因此,在实际应用中,正如 Chih-Wei Hsu 和 Chih-Jen Lin 在其论文《多类支持向量机方法比较》中所述,它更为优选(IEEE Transactions on Neural Networks, 2002 年 3 月,卷 13,第 415-425 页)。

scikit-learn 中的多类问题

在 scikit-learn 中,分类器会自动处理多类问题,我们不需要显式编写额外的代码来启用此功能。你可以看到,在以下的葡萄酒分类示例中(scikit-learn.org/stable/modules/generated/sklearn.datasets.load_wine.html#sklearn.datasets.load_wine),如何简单地处理包含三类的数据:

  1. 我们首先加载数据集并进行一些基本分析,如下所示:

    >>> from sklearn.datasets import load_wine
    >>> wine_data = load_wine()
    >>> X = wine_data.data
    >>> Y = wine_data.target
    >>> print('Input data size :', X.shape)
    Input data size : (178, 13)
    >>> print('Output data size :', Y.shape)
    Output data size : (178,)
    >>> print('Label names:', wine_data.target_names)
    Label names: ['class_0' 'class_1' 'class_2']
    >>> n_class0 = (Y == 0).sum()
    >>> n_class1 = (Y == 1).sum()
    >>> n_class2 = (Y == 2).sum()
    >>> print(f'{n_class0} class0 samples,\n{n_class1} class1 samples,\n{n_class2} class2 samples.')
    59 class0 samples,
    71 class1 samples,
    48 class2 samples. 
    

如你所见,该数据集包含 178 个样本和 13 个特征;其标签有三个可能的值,分别占 33%、40%和 27%。

  1. 接下来,我们将数据分为训练集和测试集:

    >>> X_train, X_test, Y_train, Y_test = train_test_split(X, Y, random_state=42) 
    
  2. 现在我们可以将 SVM 分类器应用于数据。我们首先初始化一个SVC模型并将其拟合到训练集上:

    >>> clf = SVC(kernel='linear', C=1.0, random_state=42)
    >>> clf.fit(X_train, Y_train) 
    

SVC模型中,多类支持是根据一对一方案隐式处理的。

  1. 接下来,我们使用训练好的模型对测试集进行预测,并直接获得预测准确率:

    >>> accuracy = clf.score(X_test, Y_test)
    >>> print(f'The accuracy is: {accuracy*100:.1f}%')
    The accuracy is: 97.8% 
    

我们的 SVM 模型在这个多类问题中也表现良好,达到了97.8%的准确率。

  1. 我们还检查了它在各个类上的表现:

    >>> from sklearn.metrics import classification_report
    >>> pred = clf.predict(X_test)
    >>> print(classification_report(Y_test, pred))
                  precision    recall  f1-score   support
               0       1.00      1.00      1.00        15
               1       1.00      0.94      0.97        18
               2       0.92      1.00      0.96        12
        accuracy                           0.98        45
       macro avg       0.97      0.98      0.98        45
    weighted avg       0.98      0.98      0.98        45 
    

看起来很棒!这个例子是不是太简单了?也许是。那在复杂情况下我们该怎么办呢?当然,我们可以调整核函数和 C 超参数的值。如前所述,C 的作用是控制分隔的严格程度,可以通过调节它来实现偏差和方差之间的最佳平衡。那核函数呢?它意味着什么,线性核函数还有什么替代方案?

在接下来的章节中,我们将解答这两个问题。你将看到核技巧如何让支持向量机(SVM)变得如此强大。

场景 5 – 使用核函数解决线性不可分问题

到目前为止,我们找到的超平面都是线性的,例如,在二维特征空间中的一条直线,或者在三维空间中的一个平面。然而,在以下例子中,我们无法找到一个线性超平面来分隔两个类别:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_09.png

图 9.9:线性不可分案例

直观地,我们观察到来自一个类别的数据点比来自另一个类别的数据点离原点更近。到原点的距离提供了可区分的信息。所以我们添加了一个新特征,z=(x[1]²+x[2]²)²,并将原始的二维空间转换为三维空间。在新的空间中,如下图所示,我们可以找到一个超平面来分隔数据(见图 9.10左下角的图),或者在二维视图中找到一条直线(见图 9.10右下角的图)。通过添加这个新特征,数据集在更高维的空间中变得线性可分,(x[1],x[2],z):

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_10.png

图 9.10:将不可分案例变为可分

基于类似的逻辑,带核函数的 SVM被发明出来,用于通过转换原始特征空间!,将其映射到一个更高维的特征空间,通过变换函数!,使得变换后的数据集!变得线性可分。

通过使用观测值!,学习到了一个线性超平面!。对于一个未知样本x’,首先将其转换为!;然后通过!确定预测的类别。

带有核函数的 SVM 能够实现非线性分隔,但它并不会显式地将每个原始数据点映射到高维空间,然后在新空间中进行昂贵的计算。相反,它以一种巧妙的方式实现这一点。

在求解 SVM 优化问题的过程中,特征向量x((1))、*x*((2))、…、x((m))仅以成对的点积形式出现,即*x*((i)) x^((j)),尽管我们在本书中不会数学展开这个内容。使用核函数时,新的特征向量是!,它们的成对点积可以表示为!。从计算效率角度来看,先在两个低维向量上隐式进行成对操作,然后再将结果映射到高维空间,是一种有效的方法。事实上,存在一个满足此要求的函数K

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_047.png

函数K就是所谓的核函数。它是进行转换的数学公式。

有不同类型的核,每种类型适用于不同的数据。

有了核函数,转换!变得隐式,非线性决策边界可以通过简单地将术语!替换为!来高效学习。

数据被转换为更高维的空间。你实际上不需要显式计算这个空间;核函数只需与原始数据一起工作,并进行支持向量机(SVM)所需的计算。

最常用的核函数可能是径向基函数RBF)核(也称为高斯核),定义如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_051.png

在高斯函数中,标准差!控制着允许的变化量或分散量:标准差越大!(或标准差越小!),钟形曲线的宽度越大,数据点分布的范围也越广。因此,!作为核系数决定了核函数拟合观测值的严格程度或一般程度。较大的!意味着允许的小方差和相对准确的拟合训练样本,这可能导致过拟合。相反,较小的!意味着允许的方差较大,且训练样本的拟合较松散,这可能会导致欠拟合。

为了说明这种权衡,让我们对一个玩具数据集应用不同值的 RBF 核:

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> X = np.c_[# negative class
...           (.3, -.8),
...           (-1.5, -1),
...           (-1.3, -.8),
...           (-1.1, -1.3),
...           (-1.2, -.3),
...           (-1.3, -.5),
...           (-.6, 1.1),
...           (-1.4, 2.2),
...           (1, 1),
...           # positive class
...           (1.3, .8),
...           (1.2, .5),
...           (.2, -2),
...           (.5, -2.4),
...           (.2, -2.3),
...           (0, -2.7),
...           (1.3, 2.1)].T
>>> Y = [-1] * 8 + [1] * 8 

八个数据点来自一类,另有八个来自另一类。我们以核系数的三个值124为例进行说明:

>>> gamma_option = [1, 2, 4] 

在每个核系数下,我们拟合一个单独的 SVM 分类器,并可视化训练后的决策边界:

>>> for i, gamma in enumerate(gamma_option, 1):
...     svm = SVC(kernel='rbf', gamma=gamma)
...     svm.fit(X, Y)
...     plt.scatter(X[:, 0], X[:, 1], c=['b']*8+['r']*8, zorder=10)
...     plt.axis('tight')
...     XX, YY = np.mgrid[-3:3:200j, -3:3:200j]
...     Z = svm.decision_function(np.c_[XX.ravel(), YY.ravel()])
...     Z = Z.reshape(XX.shape)
...     plt.pcolormesh(XX, YY, Z > 0 , cmap=plt.cm.Paired)
...     plt.contour(XX, YY, Z, colors=['k', 'k', 'k'],
...            linestyles=['--', '-', '--'], levels=[-.5, 0, .5])
...     plt.title('gamma = %d' % gamma)
...     plt.show() 

请参考以下截图查看最终结果:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_11.png

图 9.11:在不同的 gamma 值下,SVM 分类决策边界

我们可以观察到,较大的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_055.png 会导致较窄的区域,这意味着对数据集的拟合更严格;较小的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_055.png 会导致较宽的区域,这意味着对数据集的拟合较松散。当然,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_055.png 可以通过交叉验证进行微调,以获得最佳性能。

其他常见的核函数包括多项式核函数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_062.png

以及Sigmoid核函数:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_063.png

在缺乏分布先验知识的情况下,RBF 核函数通常更受欢迎,因为多项式核函数有一个额外的可调参数(多项式阶数 d),而经验性的 Sigmoid 核函数在某些参数下可以与 RBF 核函数的表现相当。因此,我们会在给定数据集时讨论线性(也可以看作没有核函数)与 RBF 核函数之间的选择。

在选择线性核函数和 RBF 核函数时

当然,线性可分性是选择合适核函数的经验法则,因为它简单高效。然而,大多数情况下,除非你对数据集有足够的先验知识,或者其特征的维度较低(1 到 3),否则很难识别出线性可分性。

一些常见的先验知识是,文本数据通常是线性可分的,而来自 XOR 函数的数据(en.wikipedia.org/wiki/XOR_gate)则不是。

现在,让我们看一下在以下三种场景中,线性核函数相较于 RBF 更受青睐。

场景 1:特征数量和样本数量都很大(超过 104 或 105)。由于特征空间的维度足够高,RBF 转换所增加的特征不会提供性能提升,反而会增加计算开销。UCI 机器学习库中的一些例子属于这一类型(UCI 机器学习库是一个广泛用于机器学习算法经验分析的数据库和数据生成器集合):

场景 2:特征的数量与训练样本的数量相比明显较大。除了场景 1中提到的原因外,RBF 核函数更容易出现过拟合。这样的场景出现在以下几个例子中:

  • Dorothea 数据集archive.ics.uci.edu/ml/datasets/Dorothea(实例数量:1,950;特征数量:100,000)。该数据集旨在用于药物发现,通过化学化合物的结构分子特征,将其分类为活性或非活性。

  • Arcene 数据集archive.ics.uci.edu/ml/datasets/Arcene(实例数量:900;特征数量:10,000)。这是一个用于癌症检测的质谱数据集。

场景 3:实例数量与特征数量相比显著较大。对于低维数据集,RBF 核通常通过将其映射到更高维空间来提升性能。然而,由于训练复杂度,通常在样本数超过 106 或 107 时,RBF 核在训练集上的效率较低。示例数据集包括以下内容:

除了这三种场景,你还可以考虑尝试使用 RBF 核。

选择线性核与 RBF 核的规则可以总结如下:

场景线性RBF
先验知识如果线性可分如果非线性可分
1 到 3 维可视化数据如果线性可分如果非线性可分
特征数量和实例数量都很大。首选
特征 >> 实例首选
实例 >> 特征首选
其他首选

表 9.1:选择线性核与 RBF 核的规则

再次强调,首选意味着我们可以从这个选项开始;并不意味着这是唯一的选择。

接下来,让我们看看如何分类人脸图像。

使用 SVM 分类人脸图像

最后,是时候利用你刚刚学到的所有知识,构建一个基于 SVM 的人脸图像分类器了。我们将分部分进行,首先探索图像数据集。

探索人脸图像数据集

我们将使用LFW 人脸数据集LFW)(scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_lfw_people.html),该数据集包含了超过 5,000 位名人的 13,000 多张精选人脸图像。每个类别有不同数量的图像样本。

首先,我们加载人脸图像数据,如下所示:

>>> from sklearn.datasets import fetch_lfw_people
Downloading LFW metadata: https://ndownloader.figshare.com/files/5976012
Downloading LFW metadata: https://ndownloader.figshare.com/files/5976009
Downloading LFW metadata: https://ndownloader.figshare.com/files/5976006
Downloading LFW data (~200MB): https://ndownloader.figshare.com/files/5976015
>>> face_data = fetch_lfw_people(min_faces_per_person=80) 

我们只加载至少有80个样本的类别,以确保有足够的训练数据。注意,如果你遇到ImportError: The Python Imaging Library (PIL) is required to load data from jpeg files的错误,请在终端中安装pillow包,方法如下:

pip install pillow 

如果遇到urlopen错误,可以手动从以下链接下载四个数据文件:

然后,你可以将它们放在一个指定的文件夹中,例如当前路径./。因此,加载图像数据的代码如下:

>>> face_data = fetch_lfw_people(data_home='./',
                                 min_faces_per_person=80,   
                                 download_if_missing=False ) 

接下来,我们查看刚刚加载的数据:

>>> X = face_data.data
>>> Y = face_data.target
>>> print('Input data size :', X.shape)
Input data size : (1140, 2914)
>>> print('Output data size :', Y.shape)
Output data size : (1140,)
>>> print('Label names:', face_data.target_names)
Label names: ['Colin Powell' 'Donald Rumsfeld' 'George W Bush' 'Gerhard Schroeder' 'Tony Blair'] 

这个五类数据集包含1,140个样本,每个样本有2,914个维度。作为一种好的实践,我们分析标签分布如下:

>>> for i in range(5):
...     print(f'Class {i} has {(Y == i).sum()} samples.')
Class 0 has 236 samples.
Class 1 has 121 samples.
Class 2 has 530 samples.
Class 3 has 109 samples.
Class 4 has 144 samples. 

数据集相当不平衡。我们在构建模型时要考虑这一点。

现在,让我们绘制一些人脸图像:

>>> fig, ax = plt.subplots(3, 4)
>>> for i, axi in enumerate(ax.flat):
...     axi.imshow(face_data.images[i], cmap='bone')
...     axi.set(xticks=[], yticks=[],
...             xlabel=face_data.target_names[face_data.target[i]])
...
>>> plt.show() 

你将看到以下 12 张带有标签的图像:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_12.png

图 9.12:来自 LFW 人脸数据集的样本

现在我们已经完成了探索性数据分析,接下来我们将在下一部分进入模型开发阶段。

构建基于 SVM 的图像分类器

首先,我们将数据拆分为训练集和测试集:

>>> X_train, X_test, Y_train, Y_test = train_test_split(X, Y,
                                                        random_state=42) 

在这个项目中,维度数大于样本数。这是一个 SVM 能够有效解决的分类问题。在我们的解决方案中,我们将通过交叉验证调整超参数,包括惩罚项C、核函数(线性或 RBF)以及https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_055.png(对于 RBF 核)。

然后,我们初始化一个常见的 SVM 模型:

>>> clf = SVC(class_weight='balanced', random_state=42) 

数据集不平衡,因此我们设置class_weight='balanced'以强调少数类。

我们使用 scikit-learn 的 GridSearchCV 模块在多个候选参数中搜索最佳超参数组合。我们将探索以下超参数候选值:

>>> parameters = {'C': [10, 100, 300],
...               'gamma': [0.0001,  0.0003, 0.001],
...               'kernel' : ['rbf', 'linear'] }
>>> from sklearn.model_selection import GridSearchCV
>>> grid_search = GridSearchCV(clf, parameters, n_jobs=-1, cv=5) 

如果你不确定要为 RBF 核选择哪个合适的 gamma 值,选择1除以特征维度通常是一个可靠的选择。因此,在这个例子中,1/2914 = 0.0003

我们刚刚初始化的GridSearchCV模型将进行五折交叉验证(cv=5),并将在所有可用的核心上并行运行(n_jobs=-1)。然后我们通过简单地应用fit方法来进行超参数调优:

>>> grid_search.fit(X_train, Y_train) 

我们通过以下代码获取最佳超参数集:

>>> print('The best model:\n', grid_search.best_params_)
The best model:
 {'C': 300, 'gamma': 0.001, 'kernel': 'rbf'} 

然后,我们通过以下代码获得在最佳参数集下的五折交叉验证平均性能:

>>> print('The best averaged performance:', grid_search.best_score_)
 The best averaged performance: 0.8456140350877192 

然后,我们获取最佳超参数集的 SVM 模型,并将其应用到测试集上:

>>> clf_best = grid_search.best_estimator_
>>> pred = clf_best.predict(X_test) 

接着,我们计算准确率和分类报告:

>>> print(f'The accuracy is: {clf_best.score(X_test,
...       Y_test)*100:.1f}%')
The accuracy is: 89.8%
>>> from sklearn.metrics import classification_report
>>> print(classification_report(Y_test, pred,
...           target_names=face_data.target_names))
                   precision    recall  f1-score   support
     Colin Powell       0.90      0.88      0.89        64
  Donald Rumsfeld       0.90      0.84      0.87        32
    George W Bush       0.89      0.94      0.92       127
Gerhard Schroeder       0.90      0.90      0.90        29
       Tony Blair       0.90      0.85      0.88        33
        Accuracy                            0.90       285
        macro avg       0.90      0.88      0.89       285
     weighted avg       0.90      0.90      0.90       285 

需要注意的是,我们基于原始训练集对模型进行调优,训练集会被划分为若干折进行交叉训练和验证,然后将最佳模型应用到原始测试集上。我们通过这种方式检验分类性能,以衡量模型的泛化能力,从而确保能够在全新的数据集上做出正确的预测。最佳 SVM 模型达到了89.8%的准确率。

另外,scikit-learn 中还有一个 SVM 分类器,LinearSVCscikit-learn.org/stable/modules/generated/sklearn.svm.LinearSVC.html)。它与SVC有何不同?LinearSVC与具有线性核的SVC相似,但它基于liblinear库实现,优化优于使用线性核的libsvm,并且其惩罚函数更加灵活。

通常,使用LinearSVC模型训练比SVC模型训练更快。这是因为具有高可扩展性的liblinear库是为大规模数据集设计的,而libsvm库的计算复杂度超过二次,无法很好地处理超过 10⁵ 个训练实例的数据。但同样,LinearSVC模型仅限于线性核。

使用 PCA 提升图像分类性能

我们还可以通过使用主成分分析PCA)来压缩输入特征,从而改进图像分类器(参考 Jonathon Shlens 的《主成分分析教程》)。这会减少原始特征空间的维度,并保留特征之间最重要的内部关系。简单来说,PCA 将原始数据投影到一个包含最重要方向(坐标)的较小空间中。我们希望,在特征数量超过训练样本数的情况下,通过使用 PCA 进行降维,减少特征的数量可以防止过拟合。

以下是 PCA 的工作原理:

  1. 数据标准化:在应用 PCA 之前,必须通过减去均值并除以每个特征的标准差来对数据进行标准化。这一步骤确保所有特征处于相同的尺度上,并防止某个特征在分析中占主导地位。

  2. 协方差矩阵计算(Covariance Matrix Calculation):PCA 计算标准化数据的协方差矩阵。协方差矩阵显示每对特征之间的变化关系。协方差矩阵的对角元素表示单个特征的方差,而非对角元素表示特征对之间的协方差。

  3. 特征分解(Eigendecomposition):下一步是对协方差矩阵进行特征分解。特征分解将协方差矩阵分解成其特征向量和特征值。特征向量表示主成分,而相应的特征值则表示每个主成分所解释的方差量。

  4. 选择主成分(Selecting Principal Components):根据特征值的大小降序排列主成分。第一个主成分(PC1)解释了最高的方差,其次是 PC2、PC3 等。通常,你会选择一部分主成分,它们解释了总方差中的重要部分(例如,95% 或更多)。

  5. 投影(Projection):最后,将数据投影到选定的主成分上,以创建原始数据的低维表示。这种低维表示保留了数据中的大部分方差信息,同时减少了特征的数量。

如果你感兴趣,可以阅读更多关于 PCA 的内容,链接为 www.kaggle.com/nirajvermafcb/principal-component-analysis-explained。我们将使用来自 scikit-learn 的 PCA 模块(scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html)来实现 PCA。我们首先应用 PCA 降维,然后在降维后的数据上训练分类器。

在机器学习中,我们通常将多个连续的步骤串联在一起,视为一个“模型”。我们称这个过程为管道化(pipelining)。我们利用来自 scikit-learn 的 pipeline API(scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html)来简化这一过程。

现在,让我们初始化一个 PCA 模型、一个 SVC 模型,并将这两个模型通过管道连接起来:

>>> from sklearn.decomposition import PCA
>>> pca = PCA(n_components=100, whiten=True, random_state=42)
>>> svc = SVC(class_weight='balanced', kernel='rbf',
...           random_state=42)
>>> from sklearn.pipeline import Pipeline
>>> model = Pipeline([('pca', pca),
...                  ('svc', svc)]) 

PCA 组件将原始数据投影到 100 维空间中,接着使用带有 RBF 核的 SVC 分类器。然后,我们在一些选项中执行网格搜索,以寻找最佳模型:

>>> parameters_pipeline = {'svc__C': [1, 3, 10],
...                       'svc__gamma': [0.01,  0.03, 0.003]}
>>> grid_search = GridSearchCV(model, parameters_pipeline ,
                               n_jobs=-1, cv=5)
>>> grid_search.fit(X_train, Y_train) 

最佳实践

在网格搜索 SVM 的超参数(如 C 和 gamma)的初始值时,选择合理的初值非常重要,这样可以高效地找到最佳值。以下是一些最佳实践:

  • 从粗略网格开始:从一个覆盖 C 和 gamma 范围广泛的粗略网格开始。这可以让你快速探索超参数空间,找到有潜力的区域。

  • 考虑特定的知识:在选择初始值时,结合有关问题的先验知识或领域专长。例如,如果你知道数据集噪声较大或包含异常值,可能需要优先考虑较大的 C 值,以便在决策边界上提供更多的灵活性。

  • 使用交叉验证:这有助于评估初始值对未见数据的泛化能力,并指导网格搜索的优化。

  • 迭代优化网格:基于初步交叉验证的结果,围绕表现良好的区域迭代地优化网格。缩小 C 和 gamma 的取值范围,将搜索重点放在可能包含最优值的区域。

最后,我们打印出最佳超参数集和最佳模型的分类性能:

>>> print('The best model:\n', grid_search.best_params_)
The best model:
 {'svc__C': 1, 'svc__gamma': 0.01}
>>> print('The best averaged performance:', grid_search.best_score_)
The best averaged performance: 0.8619883040935671
>>> model_best = grid_search.best_estimator_
>>> print(f'The accuracy is: {model_best.score(X_test, Y_test)*100:.1f}%')
The accuracy is: 92.3%
>>> pred = model_best.predict(X_test)
>>> print(classification_report(Y_test, pred,
                                target_names=face_data.target_names))
                   precision    recall  f1-score   support
     Colin Powell       0.94      0.94      0.94        64
  Donald Rumsfeld       0.93      0.84      0.89        32
    George W Bush       0.91      0.97      0.94       127
Gerhard Schroeder       0.92      0.79      0.85        29
       Tony Blair       0.94      0.91      0.92        33
        accuracy                            0.92       285
        macro avg       0.93      0.89      0.91       285
     weighted avg       0.92      0.92      0.92       285 

由 PCA 和 SVM 分类器组成的模型实现了92.3%的准确率。PCA 提高了基于 SVM 的图像分类器的性能。

在 SVM 成功应用于图像分类之后,我们将探讨其在回归中的应用。

使用支持向量回归进行估计

顾名思义,SVR 是支持向量家族的一部分,是支持向量机SVM)的回归兄弟(或者我们也可以直接称其为SVC)。

回顾一下,SVC 寻求一个最佳的超平面,将不同类别的观测值尽可能分开。而在 SVR 中,我们的目标是找到一个决策超平面(由斜率向量w和截距b定义),使得两个超平面 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_065.png(负超平面)和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_066.png(正超平面)可以覆盖最优超平面的 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_067.png带域。与此同时,最优超平面尽可能平坦,这意味着w越小越好,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/B21047_09_13.png

图 9.13:在 SVR 中找到决策超平面

这意味着通过求解以下优化问题来推导出最优的wb

支持向量回归(SVR)的理论与支持向量机(SVM)非常相似。在接下来的部分中,让我们来看看 SVR 的实现。

实现 SVR

同样,要解决前述的优化问题,我们需要求助于二次规划技术,这些技术超出了我们学习之旅的范围。因此,我们不会详细介绍计算方法,而是使用 scikit-learn 的SVR包(scikit-learn.org/stable/modules/generated/sklearn.svm.SVR.html)来实现回归算法。

支持向量机(SVM)中使用的重要技术,如惩罚项作为偏差与方差之间的权衡,以及核函数(例如 RBF)处理线性不可分的问题,都是可以迁移到 SVR 中的。scikit-learn 中的SVR包也支持这些技术。

让我们这次用SVR解决之前的糖尿病预测问题,正如我们在第五章《使用回归算法预测股票价格》中所做的那样:

  1. 首先,我们加载数据集并检查数据大小,如下所示:

    >>> from sklearn import datasets
    >>> diabetes = datasets.load_diabetes()
    >>> X = diabetes.data
    >>> Y = diabetes.target
    >>> print('Input data size :', X.shape)
    Input data size : (442, 10)
    >>> print('Output data size :', Y.shape)
    Output data size : (442,) 
    
  2. 接下来,我们将最后 30 个样本指定为测试集,其余样本作为训练集:

    >>> num_test = 30   
    >>> X_train = diabetes.data[:-num_test, :]
    >>> y_train = diabetes.target[:-num_test]
    >>> X_test = diabetes.data[-num_test:, :]
    >>> y_test = diabetes.target[-num_test:] 
    
  3. 现在我们可以将 SVR 回归模型应用于数据。首先,我们初始化一个SVC模型,并将其拟合到训练集上:

    >>> from sklearn.svm import SVR
    >>> regressor = SVR(C=100, kernel='linear')
    >>> regressor.fit(X_train, y_train) 
    

在这里,我们从线性核函数开始。

  1. 我们用训练好的模型在测试集上进行预测,并获得预测性能:

    >>> from sklearn.metrics import r2_score
    >>> predictions = regressor.predict(X_test)
    >>> print(r2_score(y_test, predictions))
    0.5868189735154503 
    

使用这个简单的模型,我们能够获得R²为0.59

  1. 让我们通过网格搜索进一步改进,找出以下选项中的最佳模型:

    >>> parameters = {'C': [300, 500, 700],
                      'gamma': [0.3, 0.6, 1],
                      'kernel' : ['rbf', 'linear']}
    >>> regressor = SVR()
    >>> grid_search = GridSearchCV(regressor, parameters, n_jobs=-1,
                                   cv=5)
    >>> grid_search.fit(X_train, y_train) 
    
  2. 在对 18 组超参数进行搜索后,我们找到了以下超参数组合下的最佳模型:

    >>> print('The best model:\n', grid_search.best_params_)
    The best model: {'C': 300, 'gamma': 1.5, 'kernel': 'rbf'} 
    
  3. 最后,我们使用最好的模型进行预测并评估其性能:

    >>> model_best = grid_search.best_estimator_
    >>> predictions = model_best.predict(X_test)
    >>> print(r2_score(Y_test, predictions)) 
    

经过微调后,我们将R²得分提升到0.68

与用于分类的支持向量机(SVM)不同,SVM 的目标是将数据分成不同的类别,而支持向量回归(SVR)则侧重于找到一个最适合数据的函数,通过最小化预测误差并允许一定的容忍度。

总结

在本章中,我们继续了使用支持向量机(SVM)进行监督学习的旅程。你学习了 SVM 的原理、核函数技巧、SVM 的实现方法以及其他机器学习分类的重要概念,包括多类分类策略和网格搜索,同时也学到了使用 SVM 的一些实用技巧(例如选择核函数和调优参数)。最后,我们通过真实的应用案例来实践所学内容,包括人脸识别。你还学习了 SVM 在回归中的扩展,即支持向量回归(SVR)。

在下一章中,我们将回顾你在本书中学到的内容,并研究现实世界机器学习的最佳实践。本章旨在让你的学习万无一失,为整个机器学习工作流程和生产化做好准备。这将是对一般机器学习技术的总结,然后我们将进入最后三章更复杂的主题。

练习

  1. 你能使用LinearSVC模块实现 SVM 吗?你需要调整哪些超参数,能达到的人脸识别的最佳性能是多少?

  2. 你能在图像识别项目中分类更多类别吗?举个例子,你可以设置min_faces_per_person=50。通过网格搜索和交叉验证,你能达到最佳性能吗?

  3. 探索使用 SVR 进行股票价格预测。你可以重用第五章中的数据集和特征生成函数,使用回归算法预测股票价格

加入我们书籍的 Discord 空间

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/yuxi

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/py-ml-ex-4e/img/QR_Code187846872178698968.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值