FastText-快速启动指南-全-
FastText 快速启动指南(全)
原文:
annas-archive.org/md5/edb0de43f18c2f8ccf389e03f95e34fb译者:飞龙
前言
FastText 是一个先进的工具,可用于执行文本分类和构建高效的词向量表示。它是开源的,设计于Facebook 人工智能研究 (FAIR) 实验室。它使用 C++ 编写,并且还提供了 Python 的封装接口。
本书的目标非常宏大,旨在涵盖你在构建实际 NLP 应用时所需的所有技术和知识。它还将介绍 fastText 构建的算法,帮助你清晰理解在什么样的上下文中,能够从 fastText 获得最佳效果。
本书的读者对象
如果你是软件开发人员或机器学习工程师,试图了解 NLP 领域的最新技术,本书将对你有所帮助。本书的很大一部分内容涉及如何创建 NLP 管道的实际问题和考虑事项。如果你是 NLP 研究人员,这本书也很有价值,因为你将了解开发 fastText 软件时所采用的内部算法和开发考虑。所有的代码示例都使用 Jupyter Notebooks 编写。我强烈建议你自己输入这些代码,进行修改和实验。保留这些代码,以便以后在实际项目中使用。
本书的内容
第一章,介绍 FastText,介绍了 fastText 及其在 NLP 中的应用背景。本章将探讨创建该库的动机及其设计目标,阐明库的创建者希望为 NLP 及计算语言学领域带来的用途和好处。还将提供关于如何在工作计算机上安装 fastText 的具体说明。完成本章后,你将能够在自己的计算机上安装并运行 fastText。
第二章,使用 FastText 命令行创建模型,讨论了 fastText 库提供的强大命令行功能。本章介绍了默认的命令行选项,并展示了如何使用它创建模型。如果你只是对 fastText 有一个浅显的了解,那么阅读到本章应该已经足够。
第三章,FastText 中的词表示,解释了如何在 fastText 中创建无监督的词嵌入。
第四章,FastText 中的句子分类,介绍了支持 fastText 句子分类的算法。你还将了解 fastText 如何将大型模型压缩为较小的模型,从而可以部署到内存较小的设备上。
第五章,Python 中的 FastText,介绍了如何通过使用 fastText 官方的 Python 封装或使用 gensim 库来在 Python 中创建模型。gensim 是一个广受欢迎的 Python NLP 库。
第六章,机器学习与深度学习模型,解释了如何将 fastText 集成到你的 NLP 管道中,前提是你有预构建的管道,使用了统计机器学习或深度学习模型。对于统计机器学习,本章使用了 scikit-learn 库;而在深度学习方面,考虑了 Keras、TensorFlow 和 PyTorch。
第七章,将模型部署到移动设备和 Web,主要讲解如何将 fastText 模型集成到实时生产级客户应用中。
如何充分利用本书
理想情况下,你应该具备基本的 Python 代码编写和结构知识。如果你不熟悉 Python 或对编程语言的工作原理不清楚,那么请参考一本关于 Python 的书籍。从数据科学视角出发的 Python 书籍会对你更为适合。
如果你已经对 NLP 和机器学习有一定的基本了解,那么本书应该很容易理解。如果你是 NLP 初学者,只要愿意深入研究本书涉及的数学内容,这应该不会成为太大问题。我已经尽力解释本书涉及的数学概念,但如果你觉得还是太难,请随时联系我们并告知我们。
假设读者有深入探索并尝试所有代码的意愿。
下载示例代码文件
你可以从你的账户在www.packtpub.com下载本书的示例代码文件。如果你从其他地方购买了本书,可以访问www.packtpub.com/support并注册,文件将直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
在www.packtpub.com登录或注册。
-
选择“SUPPORT”标签。
-
点击“Code Downloads & Errata”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
一旦文件下载完成,请确保使用以下最新版本的工具解压或提取文件夹:
-
Windows 平台的 WinRAR/7-Zip
-
Mac 平台的 Zipeg/iZip/UnRarX
-
Linux 平台的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/fastText-Quick-Start-Guide。如果代码有更新,它将会在现有的 GitHub 仓库中更新。
我们的其他书籍和视频代码包也可以通过github.com/PacktPublishing/找到,欢迎查看!
本书中使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如:“诸如 cat、grep、sed 和 awk 等命令已经存在很长时间,其行为在互联网上有详细的文档。”
代码块按如下方式设置:
import csv
import sys
w = csv.writer(sys.stdout)
for row in csv.DictReader(sys.stdin):
w.writerow([row['stars'], row['text'].replace('\n', '')])
当我们希望您注意某个代码块中的特定部分时,相关的行或项会以粗体显示:
import csv
import sys
w = csv.writer(sys.stdout)
for row in csv.DictReader(sys.stdin):
w.writerow([row['stars'], row['text'].replace('\n', '')])
任何命令行输入或输出都按如下方式书写:
$ cat data/yelp/yelp_review.csv | \
python parse_yelp_dataset.py \
> data/yelp/yelp_review.v1.csv
粗体:表示一个新术语、一个重要的词汇或您在屏幕上看到的词。例如,菜单或对话框中的词语会以这样的形式出现在文本中。这里是一个例子:“从管理面板中选择系统信息。”
警告或重要说明如下所示。
提示和技巧如下所示。
联系我们
我们欢迎读者的反馈。
一般反馈:请通过电子邮件 feedback@packtpub.com 并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请通过电子邮件联系我们:questions@packtpub.com。
勘误:尽管我们已经尽力确保内容的准确性,但错误仍然可能发生。如果您发现本书中有错误,我们将非常感谢您向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入详细信息。
盗版:如果您在互联网上发现任何非法复制的我们作品的形式,我们将非常感谢您提供其位置地址或网站名称。请通过电子邮件 copyright@packtpub.com 联系我们,并提供该材料的链接。
如果您有兴趣成为作者:如果您在某个领域具有专业知识,并且有兴趣撰写或为书籍贡献内容,请访问 authors.packtpub.com。
评价
请留下评论。在您阅读并使用本书后,为什么不在您购买书籍的网站上留下评论呢?潜在读者可以看到并参考您的无偏见意见来做出购买决定,我们在 Packt 也能了解您对我们产品的看法,作者也可以看到您对其书籍的反馈。谢谢!
欲了解有关 Packt 的更多信息,请访问 packtpub.com。
第一章:入门步骤
完成本节后,读者将掌握如何安装 fastText 并有效地在任何数据集上运行命令行应用的知识。
在第一章,介绍 fastText,你将获得有关 fastText 及其在自然语言处理(NLP)上下文中应用的描述。本章将阐述构建此库背后的动机、其预期用途以及库创建者希望为 NLP 和计算语言学领域带来的益处。此外,还会提供如何在读者工作机器上安装 fastText 的具体说明。完成本章后,读者将能够在其计算机上安装并运行 fastText。
在第二章,使用 fastText 命令行创建模型,你将了解 fastText 库提供的丰富命令行功能。本章将介绍默认命令行选项及如何使用它们创建模型。如果读者对 fastText 只有表面的兴趣,阅读到本章应该就足够了。
第二章:介绍 FastText
欢迎来到 fastText 快速入门指南。在本章中,你将学习如何安装 fastText,并创建一个稳定的环境,以便将 fastText 应用到你的自然语言处理应用中。
fastText 是一个库,它帮助你生成高效的词向量表示,并且提供开箱即用的文本分类支持。在本书中,我们将看一个特定的应用案例,即机器翻译,并使用 fastText 来实现。我们选择机器翻译作为案例,因为 fastText 声称它在处理未知词汇时具有优势,并且可以应对不同语言,即使这些语言的足够大数据集和语料库可能并不存在。在接下来的章节中,我们将看看 fastText 在这种情况下的表现。同时,我们还会讨论一些通用的技术,帮助你将这些技术扩展到你的特定应用场景。我们将在本章中涵盖以下主题:
-
介绍 fastText
-
在 Windows、Linux 和 macOS 上安装 fastText
-
使用 Docker 镜像部署 fastText
-
在 Mac 系统上安装依赖项
-
安装 Python 依赖项
-
使用
yum包管理器在 RHEL 系统上安装依赖项 -
在基于 Debian 的机器(如 Ubuntu)上安装依赖项
-
使用 pacman 在 Arch Linux 上安装依赖项
介绍 fastText
在今天互联互通的世界中,全球各地生成了大量的文本数据。这些文本信息包含了对事物的描述。例如,人们在亚马逊评论中写关于产品的内容,或者通过 Facebook 帖子表达自己的想法。自然语言处理(NLP)是将机器学习和其他计算技术应用于理解和表示口语及书面文本。以下是 NLP 需要解决的主要挑战:
-
主题建模:一般来说,文本会涉及一个主题。主题建模常用于确定集合文档中可能存在的隐藏结构或“抽象主题”。主题建模的有效应用之一是摘要生成。例如,法律文件通常较为复杂且冗长,因此像这样的系统将帮助读者抓住文件的要点,并提供对事件的高层次描述。
-
句子分类:文本分类是一个重要的挑战,能够将一大段文本分类到不同的标签。例如,一个系统应该能够将“Shahrukh Khan 在迪拜活动中表现出色”正确分类为“娱乐”标签,而将“在 Breach Candy 医院对面的商店发生火灾”分类为“新闻”标签。
-
机器翻译:世界上语言的总数至少有 3,000 种。大约一半的语言使用者少于 10,000 人,约 25%的语言使用者不到 1,000 人。因此,我们可以想象,许多语言正在消失,而当一种语言消失时,我们共同失去了大量的文化遗产。目前最好的翻译系统是谷歌的,但在写作时它仅覆盖 103 种语言,因此开发能够从少量来源训练并具有高度预测能力的机器学习翻译模型非常重要。
-
问答(QA)系统:这里的重点是构建一个基于人们用自然语言提出的问题自动回答问题的系统。围绕封闭领域系统构建的问答系统可以非常精确,因为它们可以检索与搜索项目相关的文档和文本。
-
情感分析:情感分析是理解用户在谈论某事时分享的需求和意图。人们根据情感做出选择。许多人的需求在很大程度上是情感性的,通常人们会非常坦率地表达他们的感受。创建一个能够考虑这一点的系统,将始终为业务增添大量价值。
-
事件提取:使用场景涉及大量数据以文本形式存储。例如,某些法律文本可能描述一个“犯罪”事件,接着是一个“调查”事件,再接着是多个“听证”事件。事件本身可能是嵌套的,例如“听证”事件可能包含“陈述论点”事件和“呈现证据”事件。
-
命名实体检测:构建该系统的重点是提取并根据一些预定义的类别(如人名、组织、地理等)分类实体或特定信息。例如,如果我们看以下文本:“我们在德州南部习惯了辣味食物,”我们可以理解“买家”喜欢“辣味食物”,他的“地理位置”是德州南部。如果数据中有足够的证据表明德州南部的买家喜欢辣味食物,就可以向他们营销更多类似的食品。
-
关系检测:关系检测系统解析文本并识别焦点和主体,然后尝试找出它们之间的关系。例如,句子“Mike has the flu”可以转换为
Person-[RELATION:HAS]->Disease。这些关系随后可以在商业环境中进行探讨,以构建智能应用。
前面的列表涵盖了许多 NLP 从业者所面临的问题。根据使用案例,你可以选择其中的任何挑战,并尝试在你的领域中解决它们。许多先前方法和建模技术的挑战在于,NLP 需要大量的文本数据,并且数据中包含了大量的上下文信息。计算模型很难高效地处理所有这些数据。
迄今为止,NLP 模型仅针对英语进行构建,因为英语文本数据较为丰富。但全球只有 20% 的人口讲英语,且其中大多数为非母语者。构建非英语 NLP 模型的最大障碍是数据的匮乏。因此,我们迫切需要能够在数据有限的情况下构建模型的库。fastText 有潜力改变这一切。fastText 团队发布了 294 种语言的预训练词向量。到本书发布时,更多语言将被添加到其中。
在本章中,我们将看到如何安装 fastText,以便你可以开始使用这个令人惊叹的软件。
提供的一些描述可能不适用于你;例如,针对 Mac 用户的说明可能与 Linux 用户不直接相关,反之亦然。不过,我建议你阅读每个依赖项的完整描述,以便更好地理解。
安装 fastText
根据你的操作系统,你需要确保你的计算机上安装了一些依赖项。在这一部分,你将了解如何根据自己使用的操作系统(Linux、Windows 或 macOS)来安装 fastText。此外,你还将了解根据你的使用需求,应该安装哪些额外的依赖项。我的建议是安装所有软件包,因为在本书中我们将探索使用 fastText 的各种方式。
前提条件
FastText 可以在 Windows、Linux 和 macOS 上运行。FastText 是使用 C++ 语言构建的,因此你首先需要一个良好的 C++ 编译器。
Windows
官方的 Windows 二进制文件不可用,但你可以通过以下链接下载由孟轩霞编译的最新 Windows 二进制文件:github.com/xiamx/fastText/releases。要运行这些二进制文件,你需要安装 Visual C++ 2017。你可以从以下链接下载 Visual C++ 的 64 位版本:support.microsoft.com/en-in/help/2977003/the-latest-supported-visual-c-downloads。接下来,按照通常的安装方式,双击 Visual C++ 安装程序文件即可将其安装到你的 Windows 计算机上。
Linux
你需要安装的前提软件列表如下:
-
GCC-C++;如果你使用 Clang,则需要 3.3 或更高版本。
-
Cmake
-
Python 3.5(你也可以使用 Python 2.7,但我们在本书中将重点讨论 Python 3)
-
NumPy 和 SciPy
-
pybind
可选的依赖项根据你的系统如下:
-
Zip
-
Docker
-
Git
在支持 yum 包管理器的 RHEL 机器上安装依赖项
在 Linux 系统中,你需要安装 g++。在支持 yum 包管理工具的 Fedora/CentOS 上,你可以使用以下命令安装 g++。打开终端或通过你喜欢的 SSH 工具连接到安装服务器并运行以下命令:
$ sudo yum install gcc-c++
CMake 应该默认安装。官方文档中提供了 make 和 cmake 的安装说明。我建议在你的机器上安装 cmake 并使用它来构建 fastText。你可以像之前一样使用 yum 通用命令直接安装 cmake:
$ sudo yum install cmake
要获取完整的 cmake 命令列表,请查看以下链接:cmake.org/cmake/help/v3.2/manual/cmake.1.html。
要安装可选软件,运行以下命令:
$ sudo yum install zip docker git-core
如果你在新服务器上开始并在那里运行 yum 命令,你可能会遇到以下警告:
Failed to set locale, defaulting to C
在这种情况下,安装 glibc 语言包:
$ sudo yum install glibc-langpack-en
现在,你可以跳转到 Anaconda 安装说明部分,安装 Python 依赖项。
在基于 Debian 的机器(如 Ubuntu)上安装依赖项
在 Ubuntu 和 Debian 系统中,apt-get 或 apt 是你的包管理工具。apt 基本上是 apt-get 和其他类似工具的包装器,因此你应该能够互换使用它们。我将展示 apt 命令,如果你使用的是旧版本的 Ubuntu 或 Debian,发现 apt 在你的机器上不起作用,可以将 apt 替换为 apt-get,它应该可以正常工作。此外,如果可能的话,考虑升级你的机器。
类似于 Fedora,要安装 C++,打开终端或通过 SSH 连接到你要安装 fastText 的服务器并运行以下命令。这也会安装 cmake 命令:
$ sudo apt update
$ sudo apt install build-essential
现在安装 cmake:
$ sudo apt install cmake
要安装可选的依赖项,运行以下命令:
$ sudo apt install zip docker git-core
现在,查看 Anaconda 部分,了解如何为 Python 依赖项安装 Anaconda。
apt 命令仅适用于 Ubuntu-16 及之后版本。如果你使用的是旧版 Ubuntu,应该使用 apt-get 命令。
使用 pacman 在 Arch Linux 上安装依赖项
Arch Linux 的首选包管理工具是 pacman,你可以运行以下命令安装必要的构建工具:
$ sudo pacman -S cmake make gcc-multilib
这应该会安装你构建 fastText 所需要的 make、cmake 和 g++ 编译器。虽然 Arch 系统已经安装了 Python 3.x,但我建议按照本章稍后的说明安装 Anaconda,这样你就不会错过任何 Python 依赖项。
要安装可选的依赖项,运行以下命令:
$ sudo pacman -S p7zip git docker
在 Mac 系统上安装依赖项
在 macOS 上,你应该默认安装了 Clang,它设计为替代普通的 C、C++ 和其他类似语言的编译器。可以在终端中使用 clang --version 检查版本是否为 3.3 或更高版本。如果没有 Clang 或版本较旧,可以使用终端中的 xcode 命令行工具进行安装:
$ xcode-select --install
接下来应该会弹出一个对话框,询问你是否要安装开发工具。点击安装按钮。
安装 Python 依赖项
我建议你安装 Anaconda,这样可以避免安装 Python 并使用它进行 fastText 时遇到问题。关于安装 Anaconda 的详细说明,请参考官方文档页面,网址为 conda.io/docs/user-guide/install/linux.html。简而言之,如果你使用 Windows,下载 Windows 安装程序,双击并按照屏幕上的指示操作。对于 macOS,也可以使用图形界面安装。
对于 Linux 和 macOS,下载相应的 bash 文件,然后在终端中运行以下命令:
$ bash downloadedfile.sh
请确保使用标记为 Python 3.x 的安装程序来下载和安装。书中展示的 Python 代码片段将以 Python 3.x 的版本展示。
在 Windows 上安装 fastText
目前,Windows 上并未提供 fastText 的官方二进制文件,因此没有图形界面可以直接安装 fastText。在 Windows 上使用 fastText,你需要执行以下步骤:
-
从 Xua 提供的发布页面下载最新的二进制文件,文件名为 fasttext-win64-latest-Release.zip (
github.com/xiamx/fastText/releases)。 -
这是一个 ZIP 文件,你需要解压缩内容。你会在解压后的文件夹中找到
fasttext_pic.lib、fasttext.lib、fasttext.exe和fasttext.dll文件。这个文件夹将是你进行 fastText 操作的工作目录:

-
创建一个文件夹
data,用于存放所有数据文件。现在,打开 PowerShell 并切换到该文件夹。 -
在 PowerShell 中输入
.\fasttext.exe,你应该能看到输出。
如果没有任何输出,则可能是你的机器上没有安装 Visual C++ Redistributable,你需要安装它。
在 Linux 和 macOS 上安装 fastText
要安装 fastText,请运行以下命令在终端中克隆镜像并进行构建:
$ git clone https://github.com/facebookresearch/fastText.git
$ cd fastText
$ mkdir build && cd build && cmake ..
$ make && make install
本书中将重点介绍为 Python 构建系统。因此,请在相同目录下运行以下命令:
$ pip install .
pip是 Python 的包管理器。fastText 假设使用 UTF-8 编码的文本,这在 Python 3.x 中是默认的。本书中的 Python 代码示例将使用 Python 3.x 展示。fastText 的一个优点是您可以为多种语言构建 fastText 模型,如果您不使用 Python 3.x,则无法利用此功能。如果这不是问题,并且您尝试在 Python 2.7 中使用 fastText,则请参阅末尾的附录,其中将为您提供如何在 Python 2.7 中开发时考虑 UTF-8 的指导。
使用 Docker 镜像来运行 fastText
您还可以使用 Docker 在您的计算机上运行 fastText,无需担心构建它。这可以用来在特定版本之间保持版本控制,从而提供预测性和一致性。您可以从以下链接获取有关如何安装 Docker 的信息:docs.docker.com/install/#cloud。
安装后,在运行以下命令之前,请启动 Docker 服务:
start the docker service.
$ systemctl start docker
# run the below commands to start the fasttext container.
$ docker pull xebxeb/fasttext-docker
您现在应该能够运行 fastText 了:
$ mkdir -p /tmp/data && mkdir -p /tmp/result $ docker run --rm -v /tmp/data:/data -v /tmp/result:/result \
-it xebxeb/fasttext-docker ./classification-example.sh
若要运行docker run命令,您可能需要提供权限并创建特定目录。
总结
在本章中,您已经学习了如何在您选择的环境中安装并开始使用 fastText。
在下一章中,我们将看看如何使用命令行训练 fastText 模型以及如何使用它们。
第三章:使用 FastText 命令行创建模型
FastText 拥有强大的命令行功能。事实上,可以将 fastText 称为一个“命令行优先”的库。现在,许多开发者和研究人员对命令行并不熟悉,我建议你在本章中更加专注于示例。我的希望是,在本章结束时,你能够对命令行文件操作有一定的信心。使用命令行的优势如下:
-
像
cat、grep、sed和awk这样的命令已经有很长的历史,并且它们的行为在互联网上有很好的文档记录。很有可能,对于任何你可能遇到的使用场景,你都可以轻松在 Stack Overflow/Google 上找到代码片段(或者隔壁的同事可能也知道)。 -
由于它们通常是用 C 语言实现的,所以运行非常快速。
-
这些命令非常简洁明了,这意味着你不需要写很多代码,也不需要维护复杂的代码。
我们将了解在 fastText 中如何进行分类和词向量生成。在这一章中,我们将探讨如何使用命令行来实现它们:
-
使用 fastText 进行文本分类
-
FastText 词向量
-
创建词向量
-
Facebook 词向量
-
使用预训练的词向量
使用 fastText 进行文本分类
要访问命令行,打开 Linux 或 macOS 机器上的终端,或在 Windows 机器上按下 Windows + R 输入cmd后按Enter打开命令提示符,然后输入fastText。你应该看到一些输出。如果什么都没看到,或者遇到错误提示“命令未找到”,请查看上一章关于如何在计算机上安装 fastText 的内容。如果你能看到一些输出,说明成功运行,该输出是对所有选项的基本描述。fastText 的命令行选项描述可以在本书的附录中找到。
本章中提到的所有方法和命令行语句都适用于 Linux 和 Mac 机器。如果你是 Windows 用户,请更多关注描述和操作逻辑,按照步骤的逻辑进行操作。关于 Windows 和 Linux 之间命令行差异的有用指南,请参见附录。
在 fastText 中,命令行有两个主要的使用场景,分别如下:
-
文本分类
-
文本表示
fastText 的核心重点之一是文本分类。文本分类是一种技术,通过它我们可以学习输入文本属于哪一类。基本上,这是一个监督学习问题,因此首先,你需要一个包含文本及相应标签的数据集。
大致来说,机器学习算法会对一组矩阵和向量进行某种优化问题的处理。它们并不真正理解“原始文本”,这意味着你需要搭建一个管道,将原始文本转换为数字。以下是可以遵循的步骤:
-
首先,你需要数据,因此对于文本分类,你需要一系列会被标注的文本或文档。你将它们转换成一系列的文本-标签对。
-
下一步叫做分词。分词是将文本划分成独立的片段或词项的过程。分词主要通过理解给定文本中的词边界来完成。世界上许多语言是以空格分隔的,例如英语和法语。而在某些情况下,词边界可能并不清晰,例如在中文、泰米尔语和乌尔都语中。
-
一旦分词完成,依据这个过程,你可能会得到一个“词袋”,它本质上是一个向量,表示文档或句子中某个特定的词是否存在,以及它出现的次数。矩阵中的列是所有出现过的词集,称为字典,而行则表示文档中特定词的计数。这就是词袋-模型方法。
-
将词袋模型转换为 TF-IDF 矩阵,以减少常见术语的权重。使用 TF-IDF 是为了避免文档中常见的术语对结果矩阵产生过大影响。
-
现在你已经有了矩阵,可以将其作为输入传递给分类算法,这将基本上训练一个模型,在这个输入矩阵上进行训练。在这个阶段,比较常见的算法是逻辑回归,以及像 XGBoost、随机森林等算法。
可能需要进行的其他步骤如下:
-
移除停用词。
-
词干提取或启发式地去除词尾。这一过程主要适用于英语及相关语言,因为这些语言中派生词缀的使用非常普遍。
-
向模型中添加 n-grams。
-
同义词集。
-
词性标注。
文本预处理
根据数据集的不同,可能需要执行部分或所有这些步骤:
-
对文本进行分词。
-
将文本转换为小写。这只对使用拉丁字母、希腊字母、西里尔字母和亚美尼亚字母的语言要求。像英语、法语、德语等语言都属于此类。
-
去除空行及其对应内容。
-
移除包含 XML 标签的行(以
<开始)。
这些步骤在两种情况中都应当执行,无论是句子分类还是词向量创建。
英文文本及其他使用拉丁字母的文本。
我们将通过一个示例数据集来理解文本处理。在这一章中,使用的是 Yelp 数据集。这个数据集非常流行,包含了用户的文本评论和评分。在这个数据集中,您将找到来自四个国家的 11 个大都市地区的商户信息。如果您从 Kaggle 的链接下载数据,www.kaggle.com/yelp-dataset/yelp-dataset/data,您会看到各种文件,但在我们的例子中,我们只关心用户在 yelp_review.csv 文件中提供的评论文本。作为挑战,我们将尝试预测评分是否正确。
下载数据
由于这些信息与特定商户相关,并且如果您有兴趣下载并操作数据,请在下载数据之前查看以下步骤:
-
请查看 Yelp 数据集的网页。
-
请查看、同意并遵守 Yelp 的使用条款。
-
从 Kaggle 下载
yelp_review.csv。下载链接在这里:www.kaggle.com/yelp-dataset/yelp-dataset/data。
这是代码:
$ mkdir -p data/yelp
$ cd data/yelp
$ mv ~/Downloads/yelp_review.csv.zip .
$ unzip yelp_review.csv.zip
Archive: yelp_review.csv.zip
inflating: yelp_review.csv
预处理 Yelp 数据
查看数据。始终仔细查看数据。第一行包含表头:
$ head -n1 yelp_review.csv
"review_id","user_id","business_id","stars","date","text","useful","funny","cool"
当您查看其他行时,会发现所有的单个值都有引号。另外,文本字段中有许多地方有换行符。由于 fastText 在文本处理方面的优势,我们将只提取 "stars" 和 "text" 字段,并尝试基于文本字段中的内容预测评分。
您可以使用以下 Python 脚本将文本和评分保存到另一个文件中,因为评论文本包含很多换行符,我们需要将其移除。您可以选择保留换行符,或者将换行符更改为其他分隔符,使文件与 fastText 兼容,但在我们的示例中,我们将删除文本中的换行符。
以下是获取 .csv 文件中相关部分的 Python 代码:
import csv
import sys
w = csv.writer(sys.stdout)
for row in csv.DictReader(sys.stdin):
w.writerow([row['stars'], row['text'].replace('\n', '')])
将此内容保存为名为 parse_yelp_dataset.py 的文件,然后运行以下命令:
$ cat data/yelp/yelp_review.csv | \
python parse_yelp_dataset.py \
> data/yelp/yelp_review.v1.csv
文本标准化
在本节中,我们将介绍一些您可以使用的文本标准化技术。
移除停用词
移除停用词可能会提高或不会提高您模型的性能。因此,请保留两个文件,一个包含停用词,另一个是移除停用词后的文件。我们将在 模型测试与评估 部分讨论如何检查模型性能。
您可以使用以下脚本来移除停用词。这是一个带有 nltk 等依赖项的 Python 脚本,因此请在您的 Anaconda 安装环境中使用它。运行此脚本之前,请确保您已经下载了 nltk 的 'english' 包:
$ python -c "import nltk; nltk.download('stopwords')"
将以下代码保存为名为 remove_stop_words.py 的文件:
import io
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
import sys
def get_lines():
lines = sys.stdin.readlines()
for line in lines:
yield line
stop_words = set(stopwords.words('english'))
for line in get_lines():
words = line.lower().split()
newwords = [w for w in words if w not in stop_words]
print(' '.join(newwords))
要运行文件,您需要将文件内容传递给 Python 文件。不过,在以下解释中,为了简洁起见,我们并没有真正删除停用词。当然,您可以尝试这两种方法。
规范化
由于我们正在处理英语,建议首先将所有大写字母转换为小写字母,如下所示:
$ cat data/yelp/yelp_review.v1.csv \
| tr '[:upper:]' '[:lower:]' \
> data/yelp/yelp_review.v2.csv
使用拉丁字母、希腊字母、西里尔字母和亚美尼亚字母的语言是双字母制的,这意味着有大写字母和小写字母。例如,英语、法语和德语就是这样的语言。只有在这些语言中,您才应小心将所有文本转换为小写字母。处理其他语言的语料库时,这一步是不需要的。
现在,文件的开头已经包含了所有标签。如果我们在所有句子的开头加上__label__,它将为所有标签添加__label__文本。这个标签前缀是必要的,因为库将整个文本作为输入,并且没有特定的方法来区分输入和标签,正如您在scikit-learn或其他库中看到的那样。您当然可以改变具体的标签前缀,正如您将在附录中看到的那样。
所以,要在 fastText 中读取文件并使 fastText 区分普通文本和标签文本,您需要在标签前加上__label__。以下是在命令行中轻松实现这一点的一种方式:
$ cat data/yelp/yelp_review.v2.csv \
| sed -e 's/^/__label__/g' \
> data/yelp/yelp_review.v3.csv
分离并移除一些可能不相关的标点符号:
$ cat data/yelp/yelp_review.v3.csv \
| sed -e "s/'/ ' /g" \
-e 's/"//g' -e 's/\./ \. /g' -e 's/<br \/>/ /g' \
-e 's/,/ , /g' -e 's/(/ ( /g' -e 's/)/ ) /g' \
-e 's/\!/ \! /g' \
-e 's/\?/ \? /g' -e 's/\;/ /g' \
-e 's/\:/ /g' > data/yelp/yelp_review.v4.csv
不要忘记在每次转换后继续检查数据是如何变化的。现在检查数据时,您会看到开头有一个逗号。还有很多点(.):
$ head -n 2 data/yelp/yelp_review.v4.csv
__label__5 , super simple place but amazing nonetheless . it ' s been around since the 30 ' s and they still serve the same thing they started with a bologna and salami sandwich with mustard . staff was very helpful and friendly .
__label__5 , small unassuming place that changes their menu every so often . cool decor and vibe inside their 30 seat restaurant . call for a reservation . we had their beef tartar and pork belly to start and a salmon dish and lamb meal for mains . everything was incredible ! i could go on at length about how all the listed ingredients really make their dishes amazing but honestly you just need to go . a bit outside of downtown montreal but take the metro out and it ' s less than a 10 minute walk from the station .
删除逗号和点。请记住,fastText 不要求删除标点符号和将所有字母转为小写;事实上,在某些情况下,这些可能是重要的。记住,这里提供的所有建议都应该谨慎对待,并尝试您能想到的所有可能的选项。最终目标是训练出最佳的模型。
cat data/yelp/yelp_review.v4.csv | sed 's/\,//g' > data/yelp/yelp_review.v5.csv
cat data/yelp/yelp_review.v5.csv | sed 's/\.//g' > data/yelp/yelp_review.v6.csv
mv data/yelp/yelp_review.v6.csv data/yelp/yelp_review.v5.csv
最后,删除所有连续的空格。请注意,在以下示例中,经过所有这些转换后,文件不再是 .csv 格式,但这对我们来说是可以的,因为归根结底,.csv 文件也是文本文件,因此使用 .txt 或任何其他文本格式应该没有问题。只要文件是 UTF-8 格式的文本文件,您就可以继续使用。
$ cat data/yelp/yelp_review.v5.csv | tr -s " " > data/yelp/yelp_review.v6.csv
打乱所有数据
在训练分类器之前打乱数据非常重要。如果数据的标签被聚集在一起,那么精度和召回率,进而模型的性能将较低。这是因为 fastText 使用随机梯度下降来学习模型。文件中的训练数据是按顺序处理的。在我们的例子中并非如此,因为同一类别的标签并不在一起,但仍然建议您记住这一点,并始终在训练前打乱数据。
在*Nix 系统中,你可以使用 shuffle 命令,如下所示:
$ cat data/yelp/yelp_review.v6.csv | shuf > data/yelp/yelp_review.v7.csv
有时候,shuffle 命令可能会比较慢,在处理大文件时,你可以考虑使用perl单行命令:
$ perl -MList::Util -e 'print List::Util::shuffle <>' \
data/yelp/yelp_review.v6.csv \
> data/yelp/yelp_review.v8.csv
划分训练集和验证集
模型性能评估应始终在独立数据上进行。你应该始终将整个数据集分为训练集和测试集。但是,过度划分数据集会减少用于训练的数据量,因此 80%的比例是一个不错的中间值。你可以使用以下命令将文件划分为 80-20 的比例:
$ awk -v lines=$(wc -l < data/yelp/yelp_review.v9.csv) \
-v fact=0.80 \
'NR <= lines * fact {print > "train.txt"; next} {print > "val.txt"}' \
data/yelp/yelp_review.v9.csv
模型构建
在本节中,我们将探讨模型训练和评估的步骤。
模型训练
现在,你可以开始训练步骤:
$ fasttext supervised -input data/yelp/train.txt -output result/yelp/star_model
训练时将显示输出,最终你应该会看到类似以下的输出:
Read 563M words
Number of words: 1459620
Number of labels: 5
Progress: 100.0% words/sec/thread: 3327124 lr: 0.000000 loss: 0.789366 ETA: 0h 0m
如果你现在检查result/yelp/目录,你应该能看到两个扩展名为.vec和.bin的文件。.bin文件是训练好的分类器,而.vec文件包含了所有单词的词向量。你可以打开.vec文件,它只是一个文本文件。然而,注意使用轻量级文本编辑器,如 Sublime Text 或 Notepad++,来打开它,因为它会是一个大文件。或者,也可以使用命令行工具,如head或tail。
这是我们案例中创建的一些向量。第一行包含向量的维度,在我们案例中是(1459620,100)。接下来的两行是.和the的向量:
1459620 100
. -0.080999 ... -0.029536
the -0.022696 ... 0.084717
模型测试与评估
现在你知道如何在 fastText 中创建模型文件,你需要测试并检查模型的性能,并以实际的方式报告其效能。这可以通过各种性能评估指标来完成。
精确度和召回率
为了测试分类模型的准确性,有两个非常流行的参数,它们被 fastText 支持,那就是精确度(precision)和召回率(recall)。召回率是正确召回的标签占所有实际存在标签的百分比,而精确度是所有正确预测标签占总预测标签的百分比。可以使用以下命令检查这两个参数值:
$ fasttext test result/yelp/star_model.bin data/yelp/val.txt
Now 1052334
P@1 0.685
R@1 0.685
Number of examples: 1052334
目前精确度和召回率为 68%。让我们优化一些参数,看看是否能改进模型。
混淆矩阵
现在你有了一个模型,你还可以使用混淆矩阵查看模型在不同标签上的性能。与精确度和召回率一起,混淆矩阵能够很好地展示真阴性(TN)和假阳性(FP)。在理想情况下,所有对角线上的值都应该很高,而其余单元格的值应接近于零,但在实际情况下,你可能需要选择是否能接受较高的假阳性(FP)值,或者较高的假阴性(FN)值。
要获得混淆矩阵,你需要进行一些后处理。将句子与标签分开。然后,使用预测命令,你将能够预测每个测试行的标签。提供了一个 Python 脚本,可以用来获取混淆矩阵:
$ mv data/yelp/val.testlabel data/yelp/val.testsentences
$ cut -f 1 -d ' ' data/yelp/val.txt > data/yelp/val.testlabel
$ cut -f 2- -d ' ' data/yelp/val.txt > data/yelp/val.testsentences
$ fasttext predict result/yelp/star_model.bin data/yelp/val.testsentences > pexp
$ python fasttext_confusion_matrix.py data/yelp/val.testlabel pexp
Accuracy: 0.716503505541
[[124224 16161 3347 864 1639]
[ 24537 39460 19435 2748 1283]
[ 5514 15646 63775 32668 5424]
[ 1445 1815 22583 139956 78795]
[ 1707 548 3071 59103 386586]]
你可以从这个 gist 下载 Python 脚本:gist.github.com/loretoparisi/41b918add11893d761d0ec12a3a4e1aa#file-fasttext_confusion_matrix-py。或者,你也可以从 GitHub 仓库获取它:
import argparse
import numpy as np
from sklearn.metrics import confusion_matrix
def parse_labels(path):
with open(path, 'r') as f:
return np.array(list(map(lambda x: x[9:], f.read().split())))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Display confusion matrix.')
parser.add_argument('test', help='Path to test labels')
parser.add_argument('predict', help='Path to predictions')
args = parser.parse_args()
test_labels = parse_labels(args.test)
pred_labels = parse_labels(args.predict)
eq = test_labels == pred_labels
print("Accuracy: " + str(eq.sum() / len(test_labels)))
print(confusion_matrix(test_labels, pred_labels))
超参数
在训练模型时,有多个超参数可以调整来提高模型性能。看看这里的一些超参数及其对模型训练的影响。
轮次
默认情况下,fastText 会查看每个数据点五次。你可以使用-epoch命令来改变这一点。在下面的示例中,我们将 epoch 参数改为 25,看看是否会改善我们的模型:
$ fasttext supervised -input data/yelp/train.txt -output result/yelp/star_model -epoch 25
Read 563M words
Number of words: 1459620
Number of labels: 5
Progress: 100.0% words/sec/thread: 3451048 lr: 0.000000 loss: 0.761496 ETA: 0h 0m
模型的结果是 68.6%的精确度和召回率,只有 0.1%的提升:
P@1 0.686
R@1 0.686
学习率
这可能是因为我们已经有了大量的样本。我们可以改变的另一个重要超参数是学习率,使用-lr参数。学习率控制模型在训练过程中的“更新速度”。这个参数控制应用于模型参数的更新大小。将学习率从 0.025 改为 1.0 意味着应用于模型的更新将变大 40 倍。在我们的模型中,我们还可以看到,学习率在最后变成了0。这意味着到最后,模型完全没有学习。我们试着将学习率设为1,看看会发生什么:
$ fasttext supervised -input data/yelp/train.txt -output result/yelp/star_model -lr 1.0
Read 563M words
Number of words: 1459620
Number of labels: 5
Progress: 100.0% words/sec/thread: 3381014 lr: 0.000000 loss: 0.847610 ETA: 0h 0m
这个模型的结果和之前一样。改变学习率时没有任何区别:
P@1 0.686
R@1 0.686
N-gram
我们还有一个可能对模型性能产生巨大影响的超参数。默认情况下,在为模型创建词向量时,会使用单一词(unigrams)。单一词是 n-gram,其中n为 1。n-gram 可以通过以下图示来解释:

来源:stackoverflow.com/a/45477420/5417164
你还可以使用-wordNgrams参数在 fastText 中固定N的值:
$ fasttext supervised -input data/yelp/train.txt -output result/yelp/star_model -wordNgrams 2
Read 563M words
Number of words: 1459620
Number of labels: 5
Progress: 100.0% words/sec/thread: 1141636 lr: 0.000000 loss: 0.687991 ETA: 0h 0m
现在,精确度和召回率为 71.8%,提升了 3.2%。我们再试试更多的调整:
$ fasttext supervised -input data/yelp/train.txt -output result/yelp/star_model -wordNgrams 3
Read 563M words
Number of words: 1459620
Number of labels: 5
Progress: 100.0% words/sec/thread: 620672 lr: 0.000000 loss: 0.633638 ETA: 0h 0m
$ fasttext test result/yelp/star_model.bin data/yelp/val.txt
Now1052334
P@1 0.717
R@1 0.717
Number of examples: 1052334
将 N 设置为 3 导致了性能下降。所以,让我们将 N 的值保持为 2。
你可以结合所有这些参数来创建新的模型:
$ fasttext supervised -input data/yelp/train.txt -output result/yelp/star_model -wordNgrams 2 -lr 1.0 -epoch 10
从预训练的词向量开始
如果你拥有的文本语料库不大,通常建议从为你训练分类器的语言预训练的词向量开始,否则分类结果可能较差。如何从你的语料库创建词向量将在下一节中深入讨论。
$ fasttext supervised -input data/yelp/train.txt -output result/yelp/star_model_withprevecs -pretrainedVectors wiki-news-300d-1M.vec -dim 300
Read 563M words
Number of words: 1459620
Number of labels: 5
Progress: 100.0% words/sec/thread: 1959282 lr: 0.000000 loss: 0.788021 ETA: 0h 0m
在我们的案例中,结果并没有显著改善。精度和召回率略有提高,达到了 68.5%。
寻找最佳的 fastText 超参数
FastText 有许多可以优化的超参数,以便找到模型的最佳平衡点。对于分类器,你可以从损失函数开始,查看改变字符 n-grams 是否有意义,看看改变学习率和维度是否会产生效果。
实现超参数优化的一个流行算法是使用网格搜索方法。由于你的目标是找到一个好的模型,你将拥有一个训练数据集和一个测试数据集。假设训练数据是train.txt文件,测试数据是test.txt文件。你实际上是在解决一个优化问题(P),该问题是权重组合函数,在本例中是 w^' 和超参数的组合,可能是 n-grams、学习率或训练轮次。
所以,你明白了,对于一组固定的超参数值,解决优化问题会得到一个特定的模型。由于最佳模型(可以称之为模型*)是超参数的函数,我们可以将其表示如下:

现在,你可以使用这个模型*来预测训练数据的准确性。因此,超参数优化的目标是找到一组超参数,使得准确性最高。
请注意,计算最佳模型将是相当昂贵的。没有神奇的咒语,也没有魔法公式可以找到最佳模型的超参数。仅仅拿学习率作为超参数,就会让计算变得不切实际。学习率是一个连续变量,你需要输入每一个特定值,计算模型,并检查其性能。
因此,我们采用网格搜索:基本上,根据启发式方法选择一组超参数值,并根据所有这些值的组合,将它们输入计算,最终选出性能最好的那一组值。
这叫做网格搜索,因为当考虑的值集在图表上绘制时,它们看起来像一个网格。
你可以通过为你的超参数定义一个值数组来实现这一点:
dim=(10 20)
lr=(0.1 0.3)
epochs=(5 10)
现在,我们有一个全局变量,用于保存每次找到的更好模型的单个变量,并将其初始化为 0。我们还将有一个全局性能变量来存储当前最佳性能,并初始设置为 0。在这个案例中,由于我们正在尝试三种超参数,因此我们将该变量的长度设置为 3,如下所示:
final=(0 0 0)
performance=0
现在开始实现for循环,循环将遍历所有值的组合。请注意,for循环的深度将取决于你正在循环的超参数数量:
for z in ${dim[@]}
do
for y in ${lr[@]}
do
for x in ${epochs[@]}
do
# train with the current set of parameters
...
# test the current model
...
# see if current model is the best model and update the final variable.
...
done
done
done
正如你可能猜到的,由于我们检查三个超参数的每个值都有两个选项,训练将进行 2 x 2 x 2 = 8 次。因此,如果每次训练步骤大约需要 5 分钟,那么整个过程将需要 8 x 5 分钟,即 40 分钟。
现在,让我们进入平均值部分。以下是训练步骤:
$ ./fasttext supervised -input train.txt -output model -dim "$z" -lr "$y" -epoch "$x"
训练完成后,接下来是测试阶段。我们将测试数据保存到文件中,以便稍后比较结果:
$ ./fasttext test model.bin test.txt > performance.txt
接下来是比较并保存最佳模型:
present_performance=$(cat performance.txt | awk 'NR==2 {print $2}') # get the precision values
if (( $(echo "$present_performance > $performance" | bc -l) )); then
# if current performance is the best performance till date
final[0]="$z"
final[1]="$y"
final[2]="$x"
echo "Performance values changed to ${final[@]}"
echo "present accuracy:"
cat performance.txt
fi
现在,你可以扩展此脚本,加入更多超参数。你可以在仓库中的文件中找到完整代码。
模型量化
借助模型量化,最快的模型能够适应移动设备和小型设备,如 Raspberry Pi。由于代码是开源的,现有 Java 和 Swift 库可以用来加载量化后的模型,并分别在 Android 和 iOS 应用中提供服务。
压缩 fastText 模型的算法是 fastText 与 Facebook AI Research (FAIR) 团队合作创建的。这导致 fastText 模型的体积大幅减少。原本为数百 MB 的 fastText 模型,经过压缩后仅为 1-2 MB。
实现量化可以通过使用 quantize 参数来完成。不过,你仍然需要通过常规路线训练模型:
$ DATADIR=data
$ RESULTDIR=result
$ ./fasttext supervised -input "${DATADIR}/train.txt" -output "${RESULTDIR}/model" -dim 10 -lr 0.1 -wordNgrams 2 -minCount 1 -bucket 10000000 -epoch 5 -thread 4
Read 298M words
Number of words: 1454893
Number of labels: 5
Progress: 100.0% words/sec/thread: 2992746 lr: 0.000000 loss: 0.634722 eta: 0h0m
$ fastText-0.1.0 git:(master) ./fasttext quantize -output "${RESULTDIR}/model" -input "${DATADIR}/train.txt" -qnorm -retrain -epoch 1 -cutoff 100000
Progress: 100.0% words/sec/thread: 2382426 lr: 0.000000 loss: 0.711356 eta: 0h0m h-14m
请注意,量化模型与未量化模型之间存在巨大差异:
$ du -sh $RESULTDIR/model.bin
466M result/yelp/model.bin
$ du -sh $RESULTDIR/model.ftz
1.6M result/yelp/model.ftz
.bin 文件大小约为 466 MB,而量化后的模型只有 1.6 MB。
有趣的是,精度和召回率的数值似乎有所提升。
$ ./fasttext test $RESULTDIR/model.bin $DATADIR/val.txt
N 1052334
P@1 0.699
R@1 0.699
Number of examples: 1052334
$ ./fasttext test $RESULTDIR/model.ftz $DATADIR/val.txt
N 1052334
P@1 0.7
R@1 0.7
Number of examples: 1052334
因此,你几乎不会在性能上看到差异,同时节省了大量空间。你现在可以将此模型部署到更小的设备上。在下一章中,我们将讨论模型量化的工作原理。此外,在第七章,将模型部署到 Web 和移动设备,我们将讨论如何将量化模型打包为 Android 应用的一部分。
不幸的是,目前量化只适用于有监督模型,但未来可能会有所改变,所以请保持你的 fastText 安装版本更新。
理解模型
一旦模型创建完成,你现在可以看到在生成模型时使用的参数。这在你深入思考数据并希望更改某些模型参数时,或作为一般文档记录时非常有用:
$ fasttext dump result/yelp/star_model.bin args
dim 100
ws 5
epoch 5
minCount 1
neg 5
wordNgrams 2
loss softmax
model sup
bucket 2000000
minn 0
maxn 0
lrUpdateRate 100
t 0.0001
dict 参数提供了训练中使用的单词字典信息。在之前的训练过程中,使用了 1,459,625 个单词,具体可以见下文。was 被使用了 8,272,495 次,crinkle-also 在整个句子集合中只使用了一次,等等。它还提供了单词是作为单词还是标签使用的信息。如你所见,标签位于列表的末尾:
$ fasttext dump result/yelp/star_model.bin dict > dumpdict
$ ls -lhrt dumpdict
-rw-rw-r-- 1 joydeep joydeep 27M Apr 2 11:05 dumpdict
$ head dumpdict
1459625
. 34285218 word
the 23539464 word
, 16399539 word
and 16299051 word
i 14330427 word
a 12034982 word
to 11508988
' 8907643 word
was 8272495 word
$ tail dumpdict
m&m\/chocolate 1 word
drops-surprisingly 1 word
crinkle-also 1 word
cookie-humungo 1 word
dishes\/restaurants 1 word
__label__5 1802332 label
__label__4 978722 label
__label__1 585128 label
__label__3 492454 label
__label__2 350698 label
输入和输出的行在数据转储中对应于模型的参数。在我们的模型中,前 1,459,620 行输入是与单个单词相关联的向量,而剩余的 200 万行则用于表示子词。这 200 万个子词被选择来表示整体意义,并且可以通过数据转储中 args 的输出中的 bucket 参数理解。输出行是与上下文或我们的标签相关联的向量。通常,在学习无监督词表示时,这些向量在训练后不会保留下来:
$ fasttext dump result/yelp/star_model.bin input > dumpinput
$ cat dumpinput | wc -l
3459621
$ fasttext dump result/yelp/star_model.bin output > dumpoutput
$ cat dumpoutput | wc -l
5
本节中提到的转换可以在 GitHub 仓库中的transformations.sh文件中查看。
FastText 词向量
fastText 的第二个主要关注点是为输入文本创建词嵌入。在训练过程中,fastText 会查看提供的文本语料库,并形成一个高维向量空间模型,其中它尝试尽可能地封装语义。创建词向量空间的目标是让相似的单词向量尽可能接近。在 fastText 中,这些词向量将保存在两个文件中,类似于你在文本分类中看到的:一个 .bin 文件和一个 .vec 文件。
在本节中,我们将通过使用 fastText 命令行来创建和使用词向量。
创建词向量
接下来,我们将介绍如何在 fastText 中创建词向量。你可能正在处理并为某个特定领域构建解决方案,在这种情况下,我的建议是从该特定领域生成原始文本。但如果原始文本不可用,你可以借助维基百科,它是一个包含多种语言的大型原始文本集合。
从维基百科下载
要开始使用词向量,你需要数据或文本语料库。如果你幸运的话,已经有了文本语料库。如果你不那么幸运,尤其是在你有兴趣解决 NLP 中有趣问题时,可能没有数据可用。在这种情况下,维基百科是你的朋友。维基百科的最佳之处在于,它是全球超过 250 种语言的书面文本的最佳来源。尽管这个数量相比于实际语言种类来说微不足道,但对于大多数用例来说,可能已经足够了。如果你正在处理某种语言,但维基百科资源不够丰富,也许你应该在你的语言社区中提高维基百科重要性的意识,并呼吁社区更多地贡献内容。一旦你确定了目标语言,就可以使用get-wikimedia.sh文件下载维基百科语料库。你可以从 fastText 的 GitHub 仓库获取此文件。稍作更新的版本可以从附录中复制。
使用get-wikimedia.sh文件下载维基百科语料库。
你可以在这个链接获取维基百科上所有语言的文章列表:meta.wikimedia.org/wiki/List_of_Wikipedias。在这个链接中,语言列表是以这种格式给出的:

需要注意的是第三列。记录下你选择的语言的第三个值,并运行bash get-wikimedia.sh:
$ bash get-wikimedia.sh
Saving data in data/wikimedia/20180402
Choose a language (e.g. en, bh, fr, etc.): ja
Chosen language: ja
Continue to download (WARNING: This might be big and can take a long time!)(y/n)? y
Starting download...
--2018-04-02 19:32:40-- https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2
Resolving dumps.wikimedia.org (dumps.wikimedia.org)... 208.80.154.11, 2620:0:861:1:208:80:154:11
Connecting to dumps.wikimedia.org (dumps.wikimedia.org)|208.80.154.11|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2656121742 (2.5G) [application/octet-stream]
Saving to: ‘data/wikimedia/20180402/jawiki-latest-pages-articles.xml.bz2’
jawiki-latest-pages-articles.xml.bz 0%[
你将收到一个 BZ2 文件。要解压 BZ2 文件,运行以下命令:
bzip2 -d enwiki-20170820-pages-articles.xml.bz2
如果你打开这个文件(在操作时请小心,文件很大),你会发现很多不必要的内容,比如 HTML 标签和链接。所以,你需要用wikifil.pl脚本清理文本,这个脚本是 Matt Mahoney 编写的,并且随着 fastText 的 GitHub 文件一起分发。
文本规范化
如果你下载了英文语料库,你可以使用wikifil.pl脚本:
perl wikifil.pl data/enwik9 > data/fil9
在我们的例子中,我们有日文文本,因此我们将使用 WikiExtractor(github.com/attardi/wikiextractor)从 BZ2 文件中提取文本:
find data/jap_wiki/cleaned_jap_text.txt/ -type f -exec cat {} \; | python japanese_parser.py -d /var/lib/mecab/dic/ipadic-utf8 > data/jap_wiki_parsed.txt
仍然有很多标签和属于标签的一部分的英语单词。你需要进行一些额外的处理和文本清理,以便准备好语料库进行训练:
$ cat data/jap_wiki_parsed.txt | sed "s/^.*https.*$//g" >
data/jap_wiki_parsed1.txt
$ cat data/jap_wiki_parsed1.txt | tr '[:upper:]' '[:lower:]' > data/jap_wiki_parsed2.txt
$ cat data/jap_wiki_parsed2.txt | sed
"s/[abcdefghijklmnopqrstuvwxyz]//g" > data/jap_wiki_parsed3.txt
$ cat data/jap_wiki_parsed3.txt | tr -s " " >
data/jap_wiki_parsed4.txt
$ cat data/jap_wiki_parsed4.txt | awk 'NF' | awk '{$1=$1;print}' >
data/jap_wiki_parsed5.txt
现在,你可以开始训练过程了。我们将保留英语数字。
创建单词向量
现在,你可以创建单词向量了。创建result目录:
$ mkdir -p result/jap_wiki
fasttext skipgram -input data/jap_wiki_parsed5.txt -output
result/jap_wiki/jap
fastText 支持两种创建单词向量的算法,skipgram和cbow:
$ fasttext skipgram -input data/jap_wiki/fil_cleaned_jap_text3.txt -
output result/jap_wiki/jap
和
$ fasttext cbow -input data/jap_wiki/fil_cleaned_jap_text3.txt -output
result/jap_wiki/jap_cbow
模型评估
单词向量的模型评估在很大程度上是一个手动过程。在这种情况下,你可以尝试一些单词样本,看看模型是否给出了充分的结果。
你可以用来评估模型的一些方法包括查看模型的最近邻以及查看一些单词的类比。一个常用的方法是通过 t-SNE 可视化。我们将在第四章中讨论 t-SNE 可视化,
FastText 中的句子分类。
最近邻
要获取给定单词的最近邻,传递nn参数,然后你需要提供 BIN 文件的路径。为了简洁起见,我们在这里只显示三个结果:
$ fasttext nn result/jap_wiki/jap.bin
Pre-computing word vectors... done.
Query word? 
 0.949732
 0.945276
 0.943492
类似于“sleep”的单词(
)给出了先前的结果,意思是“返回本土”;“Hachinosu”,这是南极洲的一个山峰;以及“全国选举”。在我们的例子中,结果是随机的,因此不好。
单词类比
单词类比是判断模型是否有效的好方法。类比的原理是,具有相似关系的两个单词组在向量空间中应该彼此距离相似。因此,当你提供“man”(男人)、“woman”(女人)和“king”(国王)等单词时,结果应该是“queen”(女王),因为在一个好的向量空间中,表示“man”和表示“woman”的词向量之间的距离应接近表示“king”和表示“queen”的词向量之间的距离。命令显示了 10 个结果,但这里只显示了前三个:
$ fasttext analogies result/jap_wiki/jap.bin
Query triplet (A - B + C)?  (man woman king)
 0.856853
 0.855393
 0.852085
这个代码块中的符号含义分别是“cracking”(破解)、“Monkey King”(孙悟空)和“King”(国王),在我们的情况下并没有太大帮助。
训练时的其他参数
类似于监督学习,你可以更改各种超参数,并查看生成的模型是否表现更好。因此,你可以使用-minCount参数固定最小的单词出现次数,使用-wordNgrams参数固定单词 n-gram 的最大长度。你还可以更改 fastText 用于哈希单词和字符 n-gram 的桶的数量,从而限制内存使用。如果你的系统内存很大,可以通过传递大于 200 万的值来更改这个桶参数,看看使用-bucket参数时模型的表现是否有所提高。你可以使用-minn和-maxn参数更改字符 n-gram 的最小和最大长度。使用-t更改采样阈值,使用-lr更改学习率,使用-lrUpdateRate更改学习率更新的频率,使用-dim更改词向量的维度,使用-ws更改上下文窗口的大小,使用-epoch更改训练时每一行查看的次数(默认 5 次),使用-neg更改采样的负样本数量,以及更改使用的损失函数,从默认的ns(负采样)更改为 softmax 或hs(分层 softmax)。默认使用的线程数是 12,但通常计算机有四核或八核,因此你可以使用-thread参数更改线程数以优化核心使用。
词汇表外单词
FastText 也支持处理词汇表外的单词。FastText 之所以能够做到这一点,是因为它不仅跟踪单词级别的 n-gram,还跟踪字符级别的 n-gram。因此,像“learn”(学习)、“learns”(学习)和“learned”(学会)这些单词对于它来说看起来是相似的。要获得词汇表外单词的向量,你必须使用二进制模型,这意味着模型文件具有.bin扩展名:
$ fasttext print-word-vectors wiki.it.300.bin < oov_words.txt
Facebook 词向量
Facebook 发布了大量基于维基百科和常规爬虫的预训练词向量,你可以从他们的网站下载并在你的项目中使用(fasttext.cc/docs/en/pretrained-vectors.html)。常规爬虫模型是 CBOW 模型,而维基百科模型是 skip-gram 模型。词向量的维度是 300,使用了长度为 5 的字符 n-gram,窗口大小为 5,负样本数为 10。
使用预训练的词向量
你可以在监督学习任务中使用预训练词向量。这在监督学习部分已简要讨论过。一个示例命令如下所示:
$ fasttext supervised -input train.txt -output model -epoch 25 -
wordNgrams 2 -dim 300 -loss hs -thread 7 -minCount 1 -lr 1.0 -verbose
2 -pretrainedVectors wiki.ru.vec
在使用预训练词向量时需要注意一些事项。你可以在stackoverflow.com/questions/47692906/fasttext-using-pre-trained-word-vector-for-text-classification找到相关信息。
机器翻译
Facebook 在神经机器翻译方面做了大量工作,MUSE 就是其中之一。MUSE 是一个多语言无监督和监督式词嵌入库。MUSE 使用并基于 fastText 词嵌入。通过 fastText 得到的词嵌入是单语的,因此这些向量需要对齐才能有效地进行语言间翻译。作为 MUSE 的一部分,以下是一些功能:
-
fastText 发布了适用于 30 种语言的维基百科监督式词嵌入。这些词嵌入被对齐到一个单一的向量空间中。
-
还发布了 110 个大规模的双语词典,供你训练自己的模型。
总结
在本章中,你了解了如何结合Nix shell 的命令行文本转换能力和 fastText 库,来实现一个训练、验证和预测的流水线。你在本章中探索的命令不仅功能强大,而且执行速度也很快。掌握好命令行操作,再配合 fastText 应用,应该能帮助你快速创建原型并在快节奏的环境中部署它们。至此,本书的第一部分已经完成。
本书的下一部分将介绍构建这个包的理论和算法,下一章将介绍如何使用 fastText 进行无监督学习。
第四章:FastText 模型
完成本节后,你将具备 FastText 创建监督和无监督模型的实际操作知识。此外,你还将了解将这些算法融入 FastText 中的算法和设计决策。
在第三章,FastText 中的词向量表示,你将了解 FastText 中如何创建无监督的词嵌入。
在第四章,FastText 中的句子分类,你将了解驱动 FastText 中句子分类的算法。你还将了解 FastText 如何将大模型压缩为适合低内存设备部署的小模型。
本节的目标是揭示 FastText 中的算法和设计选择,并为你提供生成更好模型或扩展库的知识。
第五章:FastText 中的词表示
现在你已经了解了如何在命令行中创建模型,你可能会想知道 fastText 是如何创建这些词表示的。在本章中,你将了解背后发生的事情以及驱动 fastText 的算法。
本章将涵盖以下内容:
-
词到向量的表示
-
单词表示的类型
-
从文本中获取向量表示
-
fastText 中的模型架构
-
无监督模型
-
fastText skipgram 实现
-
CBOW(连续词袋模型)
-
skipgram 和 CBOW 的比较
-
损失函数和优化
-
Softmax
-
上下文定义
词到向量的表示
几乎所有的机器学习和深度学习算法都在处理向量和矩阵。它们之所以有效,是因为它们的基础数学原理深深根植于线性代数。因此,简而言之,无论是监督学习还是无监督学习,你都需要创建数字矩阵。在其他领域,这不是问题,因为信息通常以数字形式捕捉。例如,在零售行业,销售信息(如销售了多少单位或商店在当前月的收入)都是数字。即使是在计算机视觉这样的更抽象的领域,图像始终以三种基本颜色的像素强度存储:红色、绿色和蓝色。某种颜色的 0 表示没有强度,而 255 表示屏幕上可能的最高强度。同样,在声音的情况下,它被存储为功率谱密度系数。在声音的情况下,麦克风接收到的模拟信号会被转换为离散时间和离散振幅。振幅本质上是指在给定时间内能够传递的位数,因此它本质上是一个数字。
计算机系统中原始文本面临的挑战是,它们通常以字符串形式存储和分析,而这与这些矩阵系统不太兼容。因此,你需要一种方法将文本转换为矩阵。
单词表示的类型
根据目标语言,有各种概念需要注意,以便为给定语料库提供最佳的词表示:
-
分布式词表示:在分布式表示中,词的语义不应仅集中在一个维度上,而应分布在所有维度上。如果没有分布,那么生成的向量可能会过大,这可能在执行必要的向量变换时,既在内存方面也在执行变换所需的时间方面成为限制因素。分布式表示是紧凑的,并且能够表示一个维度数量上指数级的聚类。
-
分布式词表示:你可以认为“猫”和“狗”之间有某种相似性,而“猫”和“老虎”之间又是另一种相似性。专注于捕捉这些隐含关系的词表示被称为分布式词表示。为了获得这样的分布式特性,通常采用以下非常常见的范式:
你可以通过词汇所处的语境来理解其含义
- 约翰·鲁珀特·弗斯(1962)
所以,如果我们以两个句子为例,“玛丽有一只猫”和“玛丽有一只狗”,那么“狗”和“猫”周围的上下文是相同的,因此词表示应该能够通过阅读这两个句子来获得“宠物”这一语境。
- 齐夫定律与海普定律:当我们讲到 n-grams 时,我们会对齐夫定律进行更多讨论,但这里我们将陈述海普定律:
文档(或文档集)中不同词的数量与文档长度的关系(即所谓的词型-词令关系)。
总结来看,齐夫定律和海普定律基本上是在传达同一个意思,那就是你总会遇到新词。因此,你不应该将文档中的稀有词丢弃,而需要一种更能容纳新词的词表示方法。你不能通过构建语言模型并关闭词汇表来宣告任务完成。
从文本中获取词向量表示
在本节中,我们将探讨为目标文本群体提供向量表示的含义。我们将从最简单的一种词向量形式及其实现方法开始。接着,我们将探索其它类型词向量背后的理论依据,最后,我们将深入研究用于在 fastText 中创建词向量的算法。
独热编码
在最简单的方法中,可以将原始文本看作一个词汇的集合,假设每个“词”都以某种方式对整个句子的意义作出贡献。所有的词都有特定的含义,因此它们本身就是类别。一个词的出现意味着该词所代表的类别存在,而缺少该词则意味着该类别不存在。因此,传统方法是将分类变量表示为二进制变量。首先创建词汇表,然后为每个词分配一个唯一位置。接着,通过在相应的索引位置放置 1,其它位置则放置 0,来创建向量。这种创建向量的方式被称为独热编码:

独热模型的实现如下:
# define input string
data = 'the quick brown fox jumped over the lazy dog'
consecutive_words = data.split()
print(data)
# construct the dictionary
all_words = list(set(consecutive_words))
# define a mapping of word to integers
word_to_int = dict((w, i) for i, w in enumerate(all_words))
int_to_word = dict((i, w) for i, w in enumerate(all_words))
# integer encode input data
integer_encoded = [word_to_int[w] for w in consecutive_words]
# one hot encode
onehot_encoded = list()
for value in integer_encoded:
letter = [0 for _ in range(len(all_words))]
letter[value] = 1
onehot_encoded.append(letter)
_ = [print(x) for x in onehot_encoded]
def argmax(vector):
# since vector is actually a list and its one hot encoding hence the
# maximum value is always 1
return vector.index(1)
# invert encoding
inverted = int_to_word[argmax(onehot_encoded[0])]
print(inverted)
尽管独热编码简单易懂且易于实现,但它也有一些缺点:
-
不是分布式表示:每个向量的维度随着词汇表的大小增长而增加。形成的矩阵非常稀疏——这意味着大多数单独的值都是 0。因此,矩阵操作变得计算上非常昂贵,即使是对于一个正常大小的语料库。
-
超出词汇表的词:它无法处理测试时的新词。
-
不是分布式表示:在独热编码中,所有的向量彼此间是等距的。
词袋模型
词袋模型关注的是已知词汇在文档中是否出现,并且只考虑文档中词汇的频率。因此,为了使用词袋方法创建文档矩阵,使用以下算法:
-
查找在所有文档中使用的独立词汇数量。词汇通过空格和标点符号作为分隔符进行识别。
-
使用这些词汇,创建特征空间。对于每个文档,每个特征值是该特征在文档中出现的次数。因此,结果矩阵中的每一行将对应于每个文档。计算每个文档中的词汇数。这是因为每个文档都会生成自己的向量。
-
归一化向量。
例如,假设有两个文档构成整个语料库:
Document1: "John likes to watch movies. Mary likes too."
Document2: "John also likes to watch football games."
所以对于所有的句子,我们的词汇表如下:
['also', 'football', 'games', 'john', 'likes', 'mary', 'movies', 'to', 'too', 'watch']
要获取词袋,我们统计每个词在句子中出现的次数。因此,以下是为每个文档形成的向量:
Document1: {'likes': 2, 'John': 1, 'to': 1, 'watch': 1, 'movies': 1, 'Mary': 1, 'too': 1}
Document2: {'John': 1, 'also': 1, 'likes': 1, 'to': 1, 'watch': 1, 'football': 1, 'games': 1}
词袋方法的主要缺点是丧失了词汇的上下文。例如,“玩具狗”和“狗玩具”这两个词组并不意味着相同的事情,但它们会共享相同的向量。这里展示了词袋方法的一个简单实现:
import collections, re
texts = ['John likes to watch movies. Mary likes too.', 'John also likes to watch football games.']
bagsofwords = [collections.Counter(re.findall(r'\w+', txt)) for txt in texts]
print(bagsofwords[0])
print(bagsofwords[1])
sumbags = sum(bagsofwords, collections.Counter())
print(sumbags)
TF-IDF
仅仅计算文档中词汇的数量可能无法提供足够的有关整个语料库的信息。这个想法是,稀有词汇能提供更多关于文档内容的信息。在 TF-IDF 中,词频被文档频率归一化。直观地说,TF-IDF 让稀有词汇更加突出,同时减小了常见词汇的影响。
N-gram(N-元组)
基于 N-gram 的方法基于 Zipf 定律,Zipf 定律指出:
在人类语言文本中,第 n 个最常见的词出现的频率与 n 成反比。
在所有语言中,有一些词汇比其他词汇使用得更频繁。常见词汇与不常见词汇之间的差异并不是剧烈的,而是连续的。这个定律的另一个重要推论是,如果一个特定频率的文档类别被切断,这不会大幅影响 N-gram 频率的分布。因此,如果我们比较同一类别的文档,它们的频率分布应该是相似的。
N-gram 频率是指重叠词序列的频率。这里有一句话:
“即使现在,他们也在自己的坟墓中交谈。”
- H.P. 洛夫克拉夫特
从这个句子你可以得到以下 n-grams。"_"用来表示句子的开始和结束:
-
1-grams (单元组):Even, now, They, talked, in, Their, tombs(特征数量:7)。
-
2-grams (双元组):(_, Even),(Even, now),(now, They),(They, talked),(talked, in),(in, Their),(Their, tombs),(tombs, _)(特征数量:8)。
-
3-grams (三元组):(_, , Even),(, Even, now),(Even, now, They),(now, They, talked),(They, talked, in),(talked, in, Their),(in, Their, tombs),(Their, tombs, _),(tombs, _, _)(特征:9)。
-
4-grams (三元组):(_, _, , Even),(, , Even, now),(, Even, now, They),(Even, now, They, talked),(now, They, talked, in),(They, talked, in, Their),(talked, in, Their, tombs),(in, Their, tombs, _),(Their, tombs, _, _),(tombs, _, _, _)(特征:10)。
依此类推。
当只处理单元组时,整个句子的概率可以写成如下:
P("Even now They talked in Their tombs.") = P("Even") * P("now") * P("They") * P("talked") * P("in") * P("Their") * P("tombs")
类似地,在双元组的情况下,整个句子的概率可以写成如下:
P("Even now They talked in Their tombs.") = P("Even" | start of sentence) * ("now" | "Even") * ("They" | "now") * ("talked" | "They") * ("in" | "talked") * ("Their" | "in") * ("tombs" | "Their") * (end of sentence | "tombs")
根据最大似然估计,类似 P("now" | "Even")的条件概率可以表示为“Even now”出现的观测次数与“Even”出现的观测次数之比。这个概率模型现在可以用于预测新的句子。
让我们在双元组的情况下构建一个模型。这个文件已从马里兰大学高级计算机研究所的服务器上获取,www.umiacs.umd.edu/,你也可以使用自己的语料库。将它保存在data文件夹中。
现在,以下命令将删除换行符,然后将所有连续的空格压缩为一个空格,接着获取所有的双元组并根据频率进行排序:
$ cat data/persuasion.txt | tr '\n' ' ' | tr -s ' ' | tr -sc 'A-Za-z' '\012' | sed -e '1!{$!p' -e '}' | paste -d' ' - - | sort | uniq -c | sort -nr > data/persuasion_bigrams.txt
现在我们可以使用这个 n-grams 文件构建一个句子生成器:
def get_next_word(ngram_file, word1=None, sentence_length=0):
with open(ngram_file) as f:
for line in f:
_, w1, w2 = line.split()
if word1 is None or word1 == w1:
sentence_length -= 1
word1 = w2
return w1, word1, sentence_length
def build_sentence(ngram_file, sentence_length):
first_word = None
sentence = ''
while sentence_length > 0:
w1, first_word, sentence_length = get_next_word(ngram_file, first_word, sentence_length)
sentence = sentence + ' ' + w1
final_sentence = sentence + ' ' + first_word + '.'
return final_sentence
print(build_sentence('data/persuasion_bigrams.txt', 10))
使用这个 n-gram 句子生成器,我们得到如下句子:
$ python build_sentence_ngrams.py
of the same time to be a very well as she.
如果这让你感兴趣,可以尝试用三元组或更多来构建。
n-grams 的主要缺点是它们非常稀疏,并且在测试数据中遇到新词时无法区分。
fastText 中的模型架构
fastText 模型会有些不同,取决于它们是无监督模型还是有监督模型。在本章中,我们主要讨论无监督模型。
无监督模型
在 fastText 中,你可以选择使用两种模型架构来计算词汇的分布式表示。这些模型分别是 skipgram 和 CBOW。fastText 中使用的模型架构都是分布式架构。因此,目标是为每个词汇项学习一个高维度的密集表示。这个表示应该是分布式的,并且尝试从上下文中学习。
在这两种架构中,你训练一个两层的浅层神经网络来构建词汇的上下文。
Skipgram
在 skipgram 中,考虑了一个大小为k的上下文窗口。所有其他位置都会被跳过,只有面板和单词之间的关系会被探讨。这是通过将单词的 one-hot 编码输入到一个两层的浅层神经网络来实现的。由于输入是 one-hot 编码,隐藏层只包含一行输入隐藏权重矩阵。神经网络的任务是根据单词预测第i个上下文:

每个单词的得分是通过以下方程计算的:

这里,h是隐藏层中的一个向量,W是隐藏输出权重矩阵。在计算出u之后,c多项式权重分布被计算出来,其中c是窗口大小。这些分布是通过以下方程计算的:

这里,w[c,j]是输出层中第c面板上的第j个单词;w[O,c]是输出上下文单词中实际的第c个单词;w[I]是唯一的输入单词;u[c,j]是输出层第c面板上第j个单元的净输入。所以你可以看到,实际上这是试图根据输入单词预测上下文单词。然后,概率会被转换为 softmax。如果你尝试将上面的架构可视化,它应该转化为如下所示:

此外,通过随机采样,较远的单词会被赋予较小的权重。当你给定窗口大小参数时,实际上只配置了最大窗口大小。实际上,实际窗口大小会在 1 和最大窗口大小之间随机选择,适用于每个训练样本。因此,最远的单词以 1/c的概率被选择,而最近的单词则始终被选择。
子词信息 skipgram
skipgram 模型是从 word2vec 实现中直接采用的原始形式。skipgram 模型之所以有效,是因为它强调了特定单词和与之相关联的单词。但是,除了特定的单词,n-grams 的特性也可能包含大量的信息。对于形态学丰富的语言尤其如此。在 fastText 中,作者采用了 word2vec 中的 skipgram 实现,这只是将整个单词的向量表示取出,然后表示为该单词向量实际上是 n-grams 向量表示的总和。因此,在 fastText 中,你之前看到的得分函数(u)实际上被改成了以下形式:

得分 = [3-6 字符级 n-grams] + [单词]
在库中,n-grams 的范围是大于或等于 3 且小于或等于 6 的,因此像Schadenfreude这样的词汇所采集的 n-grams 如下:
"shadenfreude" = {"sha", "had", "ade", ..., "shad", "frue", ..., "freude", ..., "shadenfreude"}
这种方法的主要优势在于,对于超出词汇表的单词,[word]向量不存在,因此得分函数会转变为以下形式:
得分 = [3-6 字符级 n-grams]
实现 skipgram
现在让我们通过一些 Python 代码来理解 skipgram 方法。Keras 库提供了一个非常好且易于理解的 skipgram 函数,你可以查看并理解 skipgram 应该如何实现。在本节的代码中,你可以查看 fasttext skipgram cbow.ipynb 笔记本,它灵感来源于 Keras 的实现。
正如在 skipgram 中讨论的那样,模型的任务是根据单词预测第 i 个上下文。实践中,这是通过从文档中提取词对,然后在第二个词是上下文词时,输出为 1 来实现的。
现在,给定一个序列或一个单独的文档(在此情况下,可能是一个特定的句子),首先创建两个列表:couples 和 labels。现在,对于每个目标词,获取上下文窗口,并在上下文窗口中,对于目标词和上下文词的每一个组合,将该组合捕捉到 couples 列表中,并将标签捕捉到 labels 列表中:
couples = []
labels = []
for i, wi in enumerate(sequence):
if not wi:
continue
window_start = max(0, i - window_size)
window_end = min(len(sequence), i + window_size + 1)
for j in range(window_start, window_end):
if j != i:
wj = sequence[j]
if not wj:
continue
couples.append([wi, wj])
if categorical:
labels.append([0, 1])
else:
labels.append(1)
由于我们目前只捕捉了正例,我们还需要捕捉一些负例,以便有效地训练模型。在负采样的情况下,对于负样本的数量,需要随机生成一些与目标单词无关的词索引:
num_negative_samples = int(len(labels) * negative_samples)
words = [c[0] for c in couples]
random.shuffle(words)
couples += [[words[i % len(words)],
random.randint(1, vocabulary_size - 1)] # basically get some out of context word indices
for i in range(num_negative_samples)]
if categorical:
labels += [[1, 0]] * num_negative_samples # opposite of what you would define for positive samples
else:
labels += [0] * num_negative_samples
你可以将之前的逻辑封装到一个函数中,就像在 Keras 函数 skipgrams 中所做的那样,然后返回组合(由 couples 列表表示)和标签。接下来,这些将传递给神经网络,神经网络将基于这些组合和对应的标签进行训练:
for _ in range(epochs):
loss = 0.
for i, doc in enumerate(tokenizer.texts_to_sequences(corpus)):
data, labels = skipgrams(sequence=doc, vocabulary_size=V, window_size=5, negative_samples=5.)
x = [np.array(x) for x in zip(*data)]
y = np.array(labels, dtype=np.int32)
if x:
loss += model.train_on_batch(x, y)
print(loss)
skipgram 模型本质上是一个隐藏层,夹在输入层和输出层之间。我们可以创建一个简单的 Keras 模型来捕捉这一点:
embedding_dim = 100
# inputs
w_inputs = Input(shape=(1, ), dtype='int32')
w = Embedding(V, embedding_dim)(w_inputs)
# context
c_inputs = Input(shape=(1, ), dtype='int32')
c = Embedding(V, embedding_dim)(c_inputs)
o = Dot(axes=2)([w, c])
o = Reshape((1,), input_shape=(1, 1))(o)
o = Activation('sigmoid')(o)
ft_model = Model(inputs=[w_inputs, c_inputs], outputs=o)
# ft_model.summary()
ft_model.compile(loss='binary_crossentropy', optimizer='adam')
Image(model_to_dot(ft_model, show_shapes=True).create(prog='dot', format='png'))
这将创建以下模型:

最后,一旦模型训练完成,我们将从训练好的嵌入维度的权重中获取向量:
with open('vectors.txt' ,'w') as f:
f.write('{} {}\n'.format(V-1, embedding_dim))
vectors = ft_model.get_weights()[0]
for word, i in tokenizer.word_index.items():
f.write('{} {}\n'.format(word, ' '.join(map(str, list(vectors[i, :])))))
现在我们可以将向量保存在文件中,并在需要时加载它们。
CBOW
CBOW 是 skipgram 的反向操作,其中给定上下文时,特定的词被作为目标。用作上下文词的词数取决于上下文词。所以在这个例子中,假设句子是 "Even now They talked in Their tombs",我们可以取整个上下文 ["Even" "now" "They" "in" "Their" "tombs."],并从中生成单词 "talked"。
所以该算法是将所有单词的一热编码向量作为输入,因为现在我们将所有上下文词作为输入。考虑到窗口大小为 k,那么将有 200 万个一热向量。接着取所有单词的嵌入向量。将词向量平均以得到累积上下文。隐藏层的输出由以下公式生成:

值得注意的是,隐藏层是 skipgram 和 CBOW 之间的主要区别之一,它们是彼此的镜像。
使用我们在定义 skipgram 时看到的相同评分函数生成得分。这个方程几乎是相同的,唯一不同的是,因为我们是根据上下文预测输出中的所有单词,因此需要计算不同列的 u 和ν(由 j 表示):

使用 softmax 将得分转换为概率。现在,我们需要训练这个模型,以使得这些概率与单词的真实概率匹配,即实际单词的 one-hot 编码:

CBOW 架构看起来像下面这样:

CBOW 实现
CBOW 实现比 skipgram 更容易编写代码,因为cbow方法相当直接。对于每个目标单词,你需要获取上下文并尝试预测目标单词,同时保持上下文作为输入。
因此,从实现的角度来看,编写 CBOW 的代码更简单。对于序列中的每个单词,都会创建相同的标签列表,但这个列表将是当前关注的实际目标单词。另一个列表是上下文列表,取决于窗口,它会包含上下文单词。现在,一旦输入和输出确定,我们就可以将它们传递出去,以便模型进行训练:
def generate_data_for_cbow(corpus, window_size, V):
maxlen = window_size*2
corpus = tokenizer.texts_to_sequences(corpus)
for words in corpus:
L = len(words)
for index, word in enumerate(words):
contexts = []
labels = []
s = index - window_size
e = index + window_size + 1
contexts.append([words[i] for i in range(s, e) if 0 <= i < L and i != index])
labels.append(word)
x = sequence.pad_sequences(contexts, maxlen=maxlen)
y = np_utils.to_categorical(labels, V)
yield (x, y)
上述模型的输出将是 NumPy 向量,可以传递给 keras 模型进行批量训练,类似于你在实现 skipgram部分看到的内容。在这里,cbow是 Keras 模型:
for ite in range(5):
loss = 0.
for x, y in generate_data_for_cbow(corpus, window_size, V):
loss += cbow.train_on_batch(x, y)
print(ite, loss)
对于 CBOW 模型,你需要首先定义一个输入层。嵌入层可以是嵌入层的平均值,然后通过softmax函数传递到输出层。因此,我们得到以下内容:
cbow = Sequential()
cbow.add(Embedding(input_dim=V, output_dim=embedding_dim, input_length=window_size*2))
cbow.add(Lambda(lambda x: K.mean(x, axis=1), output_shape=(embedding_dim,)))
cbow.add(Dense(V, activation='softmax'))
你应该看到以下架构正在构建:

在fasttext skipgram cbow.ipynb笔记本中运行代码。你将能够比较使用 skipgram 和 CBOW 创建的向量。
skipgram 和 CBOW 的比较
现在,你可能会想知道在实际训练数据时应该使用哪种架构。以下是一些区分 CBOW 和 skipgram 的指南,帮助你在选择训练数据时做出决策:
-
Skipgram 在少量训练数据上表现良好。即使在稀有单词和短语上,它也能很好地工作。
-
CBOW 的训练速度比 skipgram 更快。在频繁出现的单词和短语上,它也具有更高的准确性。
损失函数和优化
选择损失函数和优化算法是机器学习的基本策略之一。损失函数是一种将成本与当前模型和实际数据分布之间的差异相关联的方法。其思路是,对于特定的损失函数和优化算法配对,优化模型的参数使其尽可能模仿真实数据。
使用神经概率网络的语言模型通常使用最大似然原理进行训练。任务是最大化下一个单词w[t]的概率,将其视为目标,给定前面的单词 h 作为“历史”。我们可以通过 softmax 函数来建模这一点,接下来我们将讨论该函数。
Softmax
最流行的学习模型参数的方法是使用梯度下降。梯度下降基本上是一个优化算法,旨在最小化一个函数,基于负梯度指向的方向。在机器学习中,梯度下降作用的输入函数是为模型决定的损失函数。这个想法是,如果我们朝着最小化损失函数的方向移动,实际模型将“学习”理想的参数,并且理想情况下也能够很好地推广到外部样本或新数据。在实践中,通常会看到这种情况,并且随机梯度下降,作为梯度下降的一种变种,具有较快的训练时间。
为了使梯度下降有效,我们需要一个凸优化函数,并且希望模型输出的对数对于基于梯度的似然优化是良好的,采用最大似然估计(MLE)原理。现在,考虑到对一系列乘积取对数会将其转换为一系列加法,并且由于整个训练数据集的似然实际上是每个样本的个别似然的乘积,因此最大化对数似然变得更加容易,因为这意味着你正在优化每个样本的对数似然和,样本由 k 索引:

现在,我们需要选择一个合适的函数来确定概率,在这种情况下由 P 给出。有一些很好的函数可以使用,其中一个流行的函数是 sigmoid 函数。sigmoid 函数的形状像一个 S:

sigmoid 函数最适用于二分类任务,并且在逻辑回归中使用。
由于我们需要获得单词的后验分布,我们的问题陈述更像是一个多项式分布而不是二项式分布。因此,我们可以选择 softmax 分布,它是 sigmoid 函数在多类问题上的推广。
softmax 函数计算事件在 n 个不同事件上的概率分布。softmax 将一类值转换为概率,且总和为 1。因此,你可以说它有效地将一个 k 维的任意实数值向量压缩成一个 0 到 1 范围内的 k 维实数值向量。该函数由以下方程给出:

你可以使用以下代码查看 softmax 函数的表现:
import numpy as np
import matplotlib.pyplot as plt
def softmax(arr):
return np.exp(arr)/float(sum(np.exp(arr)))
def line_graph(x, y, x_title, y_title):
plt.plot(x, y)
plt.xlabel(x_title)
plt.ylabel(y_title)
plt.show()
graph_x = range(10)
graph_y = softmax(graph_x)
print('Graph X readings: {}'.format(graph_x))
print('Graph Y readings: {}'.format(graph_y))
line_graph(graph_x, graph_y, 'Inputs', 'softmax scores')
如果你在 Jupyter notebook 中运行之前的代码,你应该会看到类似下面的图表。你也可以在 softmax function.ipynb 笔记本中看到这个内容,位于 chapter 3 文件夹下:

注意到随着数值的增大,概率也随之增大。这是 softmax 的一个有趣特性,即对低刺激的反应是一个相当均匀的分布,而对高刺激的反应是接近 0 和 1 的概率。如果你想知道为什么会这样,这是因为指数函数的影响,它聚焦于极端值。
因此,对于给定的输入单词,这个函数将计算所有可能单词的概率。如果你使用 softmax 函数进行训练,那么与实际单词相关的概率应该是最高的:

这里,分数函数可以看作是单词 w[t] 与上下文 h 的兼容性。
由于我们正在使用训练集上的负对数似然来训练这个模型:

现在我们需要使用梯度下降来学习我们的 softmax 模型,因此我们需要计算关于输入单词的梯度:

参数模型的更新将与梯度的方向相反:

在我们的上下文中,设词汇表为 V,隐藏层的大小为 N。相邻层之间的单元是完全连接的。输入是一个独热编码向量,这意味着对于给定的单词输入上下文,只有 V 单元中的一个, {x[1], x[2], ..., x[V]},会是 1,其他所有单元都是 0。
输入层和输出层之间的权重可以通过一个 V x N 矩阵 W 来表示。W 的每一行是与输入层相关的单词的 N 维向量表示 v[w]。形式上,W 的第 i 行是 v[w]^T。给定一个上下文,假设 x[k] = 1 代表一个特定的上下文单词,其它情况下为 0,我们得到以下公式:

本质上,这就是 W 的第 k^(th) 行。我们称之为 ν[wI^T]。从隐藏层到输出矩阵,有一个不同的权重 W^' = {w[ij]^'},它是一个 N x V 矩阵。使用这些权重,你可以为词汇表中的每个单词计算一个分数 u[j]:

这里,ν[w]^'[j] 是矩阵 W^' 的第 j 列。现在,使用 softmax 方程,我们可以得到以下结果:

ν[w] 和 ν[w]^' 是单词 w 的两种表示。 ν[w] 来自 W 的行,W 是输入到隐藏权重矩阵的,而 ν[w]^' 来自 W^' 的列,W^' 是隐藏输出矩阵。在后续分析中,我们将称 ν[w] 为单词 w 的“输入向量”,而 ν[w]^' 为其“输出向量”。考虑 u[j] 为描述的分数,我们可以将损失函数从方程 (6) 转换为如下形式:

分层 softmax
计算 softmax 是高度计算密集型的。对于每个训练实例,我们必须遍历词汇表中的每个单词并计算 softmax。因此,在大词汇表和大规模训练语料库下,这种方法是不可行的。为了解决这个问题,fastText 使用了两种方法:层次化软最大化和负采样方法。在本节中,我们将讨论层次化软最大化,并将在下一节中讨论负采样。在这两种方法中,技巧是认识到我们不需要在每个训练实例中更新所有的输出向量。
在层次化软最大化中,计算出一棵二叉树来表示词汇表中的所有单词。V 个单词必须是树的叶子节点。可以证明,树中有 V-1 个内部节点。对于每个节点,从树的根到该节点存在一条唯一的路径,并且这条路径用于估计表示在叶节点中的单词的概率:

每个单词可以通过从根节点经过内节点的路径到达,这些内节点表示路径上概率质量。这些值是通过简单的 sigmoid 函数生成的,只要我们计算的路径是这些概率质量函数的乘积,定义如下:

在我们特定的情况下,x 是如何计算的?它是通过输入和输出词向量表示的点积来计算的:

在这里,n(w, j) 是从根到 w 的路径上的第 j 个节点。
在层次化软最大化模型中,单词没有输出表示。相反,每个 V - 1 的内部单元都有一个输出向量 ν[n(w,j)]^'。而一个单词作为输出单词的概率定义如下:

在这里,ch(n) 是单元 n 的左孩子;ν[n(w,j)]^' 是内部单元 n(w,j) 的向量表示("输出向量");h 是隐藏层的输出值(在 skipgram 模型中 h = ν[ω],而在 CBOW 中,
);
是一个特殊的函数,定义如下:

为了计算任何输出单词的概率,我们需要知道从根节点到输出单词路径中每个中间节点的概率。
我们定义在一个中间节点上向右走的概率如下:

由于我们计算的是一棵二叉树,向左走的概率如下:

理论上,可以使用许多不同类型的树来实现层次化软最大化(hierarchical softmax)。你可以随机生成树,或者使用现有的语言学资源,如 WordNet。Morin 和 Benzio 使用了这种方法,并展示了相对于随机生成树,效果提升了 258 倍。但这种方式构建的树中的节点通常具有多个边。另一种策略是使用递归划分策略或聚类策略来学习层次结构。聚类算法可以采用贪心方法,正如 Yikang Shen、Shawn Tan、Christopher Pal 和 Aaron Courville 在自组织层次化软最大化中所展示的那样。我们还可以选择 Huffman 编码,这在数据压缩领域传统上被使用。由于我们对通过 Nikhil Pawar(2012)进行的文档聚类非常感兴趣,研究表明,当使用 Huffman 编码将字符串编码为整数时,基于整数实例的聚类效果要有效得多。在 word2vec 和 fastText 中,使用了 Huffman 树。Huffman 树的一个有趣属性是,虽然二叉树的内部节点可能并不总是具有两个子节点,但二叉 Huffman 树的内部节点总是具有两个子节点:

使用 http://huffman.ooz.ie/?text=abcab 生成的树
在构建 Huffman 树时,代码被分配给令牌,代码的长度取决于令牌的相对频率或权重。例如,在前面的例子中,单词 E 的代码是 0000,这是其中一个最长的代码,因此你可以认为这个词在语料库中出现的次数最多。
用于构建树的代码可以在github.com/facebookresearch/fastText/blob/d72255386b8cd00981f4a83ae346754697a8f4b4/src/model.cc#L279找到。
你可以在Vocab类中的encode_huffman方法找到 Python 实现。对于更简单的实现,你可以在仓库中的chapter3文件夹下的huffman coding.ipynb笔记本中找到 Python 实现。
对于更新方程,每个训练实例的计算复杂度从O(V)降低到O(log(V)),这在速度方面是一个巨大的提升。我们仍然大致保留相同数量的参数(与最初的V个输出向量相比,内节点的V-1个向量)。
Google Allo 使用层次化软最大化层使得短语推荐更快速。
负采样
层次化软最大化的替代方法是噪声对比估计(NCE),该方法由 Gutmann 和 Hyvarinen 提出,并由 Mnih 和 Teh 应用于语言建模。NCE 认为,一个好的模型应该能够通过逻辑回归区分数据与噪声。
尽管可以证明 NCE 近似了 softmax 的对数概率,skipgram 模型只关心学习高质量的向量表示,因此只要向量表示保持其质量,我们可以自由简化 NCE。我们通过以下目标定义负采样:

这用于替代 skipgram 目标中的 log(P(W[O] | W[I])) 项。因此,任务是通过逻辑回归区分目标词 w[O] 和来自噪声分布 Pn 的抽样,其中每个数据样本都有 k 个负样本。在 fastText 中,默认情况下会采样五个负样本。
常见词的子采样
在一个非常大的语料库中,最常见的单词可能会出现数亿次,例如 "in"、"the" 和 "a" 这样的词。此类单词提供的信息价值通常低于稀有词。你可以很容易地看出,尽管 fastText 模型通过观察 "France" 和 "Paris" 的共现获益,但通过观察 "France" 和 "the" 的共现获得的收益则要小得多,因为几乎每个单词都频繁地与 "the" 一起出现在句子中。这一思想也可以反向应用。频繁单词的向量表示在训练了几百万个额外的示例后不会发生显著变化。
为了应对稀有词和常见词之间的不平衡,我们使用一种简单的子采样方法:训练集中每个单词 w[i] 被以以下公式计算的概率丢弃:

其中,函数 f 是第 i 个单词 w 的频率,t 是选择的阈值,因此是一个超参数。在 fastText 中,默认的 t 值选择为 0.0001。相关代码可以在 github.com/facebookresearch/fastText/blob/53dd4c5cefec39f4cc9f988f9f39ab55eec6a02f/src/dictionary.cc#L281 找到。
上下文定义
一般来说,对于一个包含 n 个单词 w[1], w[2], ..., w[n] 的句子,单词 w[i] 的上下文来自于围绕该单词的大小为 k 的窗口:

这里,k 是一个参数。但有两个细节:
-
动态窗口大小:所使用的窗口大小是动态的——参数 k 表示最大窗口大小。对于语料库中的每个单词,从 1 到 k 中均匀地采样一个窗口大小 k^'。
-
子采样和稀有词修剪的效果:与 word2vec 类似,fastText 有两个额外的参数用于丢弃一些输入词:出现频率低于
minCount的词既不被视为词汇也不被视为上下文,此外,频繁词汇(由 -t 参数定义)会被下采样。重要的是,这些词会在生成上下文之前从文本中移除。这种方法增加了某些词的有效窗口大小。对频繁词的下采样应该会提高结果嵌入的质量。
摘要
在这一章中,你已经了解了 fastText 中的无监督学习,以及使其成为可能的算法和方法。
下一章将介绍 fastText 如何进行监督学习,你还将了解模型量化在 fastText 中的工作原理。
第六章:FastText 中的句子分类
本章将涵盖以下主题:
-
句子分类
-
fastText 监督学习:
-
架构
-
层次化软最大架构
-
N-gram 特征与哈希技巧:
- Fowler-Noll-Vo(FNV)哈希
-
词嵌入及其在句子分类中的应用
-
-
fastText 模型离散化:
-
压缩:
-
离散化
-
向量离散化:
- 寻找高维空间的代码书
-
产品离散化
-
额外步骤
-
-
句子分类
句子分类涉及理解自然语言中的文本,并确定其可能属于的类别。在文本分类问题集中,你将拥有一组文档 d,它属于语料库 X(包含所有文档)。你还将拥有一组有限的类别 C = {c[1], c[2], ..., c[n]}。类别也称为类别或标签。为了训练一个模型,你需要一个分类器,通常是一个经过充分测试的算法(虽然不一定,但在这里我们讨论的是一个在 fastText 中使用的经过充分测试的算法),你还需要一个包含文档和关联标签的语料库,用来标识每个文档属于哪个类别。
文本分类有许多实际应用,如下所示:
-
创建电子邮件垃圾邮件分类器
-
搜索引擎中的页面排名和索引
-
评论中的情感检测,可以了解客户是否对产品满意
文本分类通常是对人工分类的一种补充。标注文档在很大程度上是主观的,取决于语料库。举例来说,一个示例文档“我喜欢去夏威夷旅行”,可能会被图书馆员视为属于“旅行”类,但医生可能会认为它属于“无关”类。因此,想法是:一组文档将由领域专家进行标注,标注数据将用于训练文本分类器,然后该文本分类器可以用于预测新的输入文本,从而节省领域专家的时间和资源(也许领域专家可以定期检查并审核分类器对输入文本的表现)。此外,所提议的思路是针对一般人群的,并不适用于像 Stack Overflow 这类通过众包进行标注的方式;大多数商业问题没有这种自动标注的奢侈,因此你必须花费一些时间手动标注文档。
为了评估分类模型的表现,我们将训练语料库分为测试集和训练集。只有训练集用于模型训练。完成训练后,我们对测试集进行分类,将预测结果与实际结果进行比较,并衡量性能。正确分类文档的比例与实际文档的比例称为准确率。我们还可以查看另外两个参数,它们可以衡量模型的表现。一个是 召回率,它表示我们回忆出的所有正确标签的百分比,而不是实际存在的标签。我们还可以查看模型的 精准度,即我们查看所有预测标签,并判断其中哪些是最初的实际标签。
fastText 有监督学习
fastText 分类器是建立在线性分类器之上的,特别是 BoW 分类器。在这一节中,你将了解 fastText 分类器的架构以及它是如何工作的。
架构
你可以认为每一段文本和每一个标签实际上都是空间中的一个向量,而这个向量的坐标是我们实际上试图调整和训练的目标,目的是使文本向量和其相关标签的向量在空间中非常接近:

文本的向量表示
在这个示例中,这是一个二维空间中的示例,你会看到类似于“Nigerian Tommy Thompson 也是摔跤界的一个新面孔”以及“James 在开场的第一节得到了 46 分中的 20 分”的文本,它们更接近“体育”标签而非“旅游”标签。
我们可以通过这样的方式来实现:我们将代表文本的向量和代表标签的向量输入评分函数。然后我们得到一个分数,并对所有文本向量与每个可能标签的向量表示之间的分数进行归一化。这样就可以得出给定文本与该标签的概率。分数是单独的值,我们使用之前章节中讨论过的 softmax 函数将它们转换为概率。
fastText 使用类似的向量空间模型进行文本分类,其中单词被压缩成称为嵌入的低维向量。其目的是训练并得到一个向量空间,使得句子向量和标签向量彼此非常接近。要将向量空间模型应用于句子或文档,首先需要选择一个适当的函数,这是一个将多个单词结合起来的数学过程。
组合函数分为两类:无序和句法。无序函数将输入文本视为词袋模型(BoW)嵌入,而句法表示则考虑单词顺序和句子结构。fastText 大多采用无序方法,因为它采用词袋方法,但也通过使用 n-gram 有少量的句法表示,正如我们稍后将看到的那样。
接下来,你可以做的是获取表示并在其上训练一个线性分类器。可以使用的优秀线性分类器有逻辑回归和支持向量机(SVM)。然而,线性分类器有一个小问题。它们在特征和类别之间不共享参数。因此,这可能会限制其对训练样本较少的类别的泛化能力。解决这个问题的方法是使用多层神经网络,或者将线性分类器分解为低秩矩阵,然后在其上运行一个神经网络。
句法表示比 BoW 方法更加稀疏,因此需要更多的训练时间。这使得它们在处理大数据集或计算资源有限时非常昂贵。例如,如果你构建一个递归神经网络来训练句法词表示,这会再次计算昂贵的张量积,并且每个句法分析树的节点中都会有非线性,那么你的训练时间可能会延长至数天,这对于快速反馈周期来说是无法接受的。
因此,你可以使用一个平均网络,它是一个无序模型,可以通过三个简单的步骤来解释:
-
对与输入序列相关联的嵌入向量取平均。
-
将平均值通过一个或多个前馈层。
-
在最终层的表示上执行线性分类。
通过应用一种新的受 dropout 启发的正则化器,可以改进模型。在这种情况下,对于每个训练实例,在计算平均值之前,部分词嵌入将被随机丢弃。在 fastText 中,这是通过子采样频繁词汇来实现的,这在上一章也有讨论,并且同样用于分类器中。
该架构的一般形式如下:

在这种情况下,我们希望将输入的词元序列映射到 k 个标签。我们首先对词嵌入序列ν[ω](其中ω ∈ X)应用组合函数 g。这个组合函数的输出是一个向量 z,作为逻辑回归函数的输入。分类架构与上一章讨论的 cbow 模型相似。
首先,取词嵌入的加权平均值:

将z输入到 softmax 层中会为每个输出标签产生估计的概率:

这里,softmax 函数如下所示:

这里,W_s 是一个 k x d 的矩阵,表示具有 k 个输出标签的数据集,b 是一个偏置项。fastText 使用一个通用的偏置项,形式是一个句子结束字符<s>,该字符会被添加到所有输入样本中。
这个模型可以通过最小化交叉熵误差来训练,对于具有真实标签 y 的单个训练实例,误差计算如下:

在 fastText 中,这被转换为计算预定义类别的概率分布。对于 N 个文档的集合,这将导致最小化类别上的负对数似然:

这里,x[n] 是第 n 个文档的标准化特征袋,y[n] 是标签,A 和 B 是权重矩阵。A 只是 fastText 中单词的查找表。
然后,我们可以使用随机梯度下降不断调整这些坐标,直到我们最大化每个文本正确标签的概率。fastText 通过使用随机梯度下降和线性衰减的学习率,在多个 CPU 上异步地训练这个模型。
fastText 中使用的架构如下:

fastText 的模型架构对于具有 N 元词特征 x[1]、x[2]、...、x[N] 的句子。特征会被嵌入并平均,形成隐藏变量。
现在让我们总结一下架构:
-
它从单词表示开始,单词表示被平均成文本表示,然后输入到线性分类器中。
-
该分类器本质上是一个具有秩约束和快速损失近似的线性模型。
-
文本表示是一个隐藏状态,可以在特征和类别之间共享。
-
使用 softmax 层来获得预定义类别的概率分布。
-
高计算复杂度 O(kh),其中 k 是类别数,h 是文本表示的维度。
层次化 softmax 架构。
计算 softmax 是计算上非常昂贵的,并且对于大规模语料库来说是不可行的,因为这意味着我们不仅要为带有标签的文本找到分数,还要为所有标签的文本找到分数。因此,对于 n 个文本和 m 个标签,这种情况的最坏性能为 O(n²),显然这不是一个好的选择。
此外,softmax 及其他类似的方法在计算概率时并没有考虑类别的语义组织结构。分类器应该知道,将狗分类为潜艇的惩罚应大于将狗分类为狼的惩罚。我们可能有的直觉是,目标标签可能并不是平面的,而是类似树状结构的。
所以现在我们有 k 个类别,我们希望每个输入都能被分类到其中一个类别。我们可以认为这些类别是树的叶节点。此树的结构是有意义的层次结构。进一步假设我们的分类器将输入映射到叶节点上的输出概率分布。希望这个叶节点能代表对应输入的正确类别。树中任何节点的概率就是从根节点到该节点路径的概率。如果该节点位于深度 l + 1,其父节点为 n[1],n[2],...,n[l],则该节点的概率为:

如果一个叶节点位于从根节点到叶节点的路径上,则该叶节点属于该路径上的节点。我们将“胜利”或“赢得”的金额定义为沿从根节点到叶节点路径上对应正确类别节点的概率的加权和。在优化或训练过程中,我们希望最大化这种“赢得”,相反,最小化“损失”。在这种情况下,损失被认为是胜利的负值。
因此,在 fastText 中,使用的是层级分类器,它类似于你在前面章节中看到的层级 softmax。此方法将标签表示为二叉树中的节点,每个节点表示一个概率,因此标签通过从根节点到给定标签路径上的概率来表示。在这种方法中,正确的标签是使用广度优先搜索(BFS)算法生成的。BFS 搜索非常快速,因此可以将复杂度降低到 log[2]n。现在我们只需要计算到正确标签路径的概率。当标签数量很多时,这会显著提高所有标签的计算速度,从而加速模型训练。正如你在上一章节中看到的,层级概率表示趋近于 softmax 概率,因此这种近似方法实际上可以提供相同的模型性能,而且训练速度要快得多。
正如你在前一章节中看到的,在这种情况下,层级分类器的输出是标签。类似于训练词向量,在此情况下会形成一个 Huffman 树。由于我们已经在上一章讨论了 Huffman 树的内部结构,在此我们将稍微修改代码,尝试查看形成的确切树,并找到与之相关的概率。
为了简单起见,我们将使用一个非常小的数据集,标签数量也很少。在这个例子中,以下是一组带有标签的句子,已保存在名为 labeledtextfile.txt 的文件中:
__label__sauce How much does potato starch affect a cheese sauce recipe?
__label__food-safety Dangerous pathogens capable of growing in acidic environments
__label__restaurant Michelin Three Star Restaurant; but if the chef is not there
__label__baking how to seperate peanut oil from roasted peanuts at home?
__label__baking Fan bake vs bake
__label__sauce Regulation and balancing of readymade packed mayonnaise and other sauces
由于 fastText 是用 C++ 编写的,出于性能考虑,它并不直接操作和处理标签字符串。要获取标签的 Huffman 码,你可以将 model.cc 文件中第 81 行的 hierarchicalSoftmax 函数修改为以下内容:
real Model::hierarchicalSoftmax(int32_t target, real lr) {
real loss = 0.0;
grad_.zero();
const std::vector<bool>& binaryCode = codes[target];
std::cout << "\ntarget: " << target << ", vector: ";
for (std::vector<bool>::const_iterator i = binaryCode.begin(); i != binaryCode.end(); ++i)
std::cout << *i << ' ';
std::cout << '\n';
const std::vector<int32_t>& pathToRoot = paths[target];
if (target == 0)
{
std::cout << "will check the path to root for bakings: " << ' ';
for (int32_t i = 0; i < pathToRoot.size(); i++) {
std::cout << pathToRoot[i] << '_' << "target_" << target << "_Individual loss_" << binaryLogistic(pathToRoot[i], binaryCode[i], lr) << ' ';
}
std::cout << '\n';
}
if (target == 1)
{
std::cout << "will check the path to root for sauce: " << '\n';
for (int32_t i = 0; i < pathToRoot.size(); i++) {
std::cout << pathToRoot[i] << '_' << "target_" << target << "_Individual loss_" << binaryLogistic(pathToRoot[i], binaryCode[i], lr) << ' ';
}
std::cout << '\n';
}
if (target == 2)
{
std::cout << "will check the path to root for sauce: " << '\n';
for (int32_t i = 0; i < pathToRoot.size(); i++) {
std::cout << pathToRoot[i] << '_' << "target_" << target << "_Individual loss_" << binaryLogistic(pathToRoot[i], binaryCode[i], lr) << ' ';
}
std::cout << '\n';
}
if (target == 3)
{
std::cout << "will check the path to root for restaurant: " << '\n';
for (int32_t i = 0; i < pathToRoot.size(); i++) {
std::cout << pathToRoot[i] << '_' << "target_" << target << "_Individual loss_" << binaryLogistic(pathToRoot[i], binaryCode[i], lr) << ' ';
}
std::cout << '\n';
}
for (int32_t i = 0; i < pathToRoot.size(); i++) {
loss += binaryLogistic(pathToRoot[i], binaryCode[i], lr);
}
// std::cout << "total loss for target: " << target << " is: " << loss;
// std::cout << '\n';
return loss;
}
如你所见,我正在列举多个标签。但是你可以选择你想要的标签。你将得到类似于这样的输出。你需要获取目标值的最后出现以获得向量:
target: 2, vector: 1 0 1
will check the path to root for sauce:
0_target_2_Individual loss_0.693147 1_target_2_Individual loss_0.681497 2_target_2_Individual loss_0.693147
所以,对应于目标 2 的相似向量是 101。
n-gram 特征和哈希技巧
正如你所见,词汇的词袋(Bag of Words,BoW)用于生成后续分类过程中使用的单词表示。但是词袋是无序的,并且没有任何语法信息。因此,n-gram 词袋被用作额外的特征,以捕捉部分语法信息。
正如我们已经讨论过的那样,大规模的自然语言处理问题几乎总是涉及使用大型语料库。这个语料库总是具有无限数量的唯一单词,正如我们从 Zipf 定律中所看到的那样。单词通常被定义为由分隔符分隔的字符串,例如英语中的空格。因此,采用单词 n-gram 对于大型语料库来说是不可扩展的,这对于准确分类至关重要。
因为这两个因素,天真地形成的矩阵总是稀疏且高维的。你可以尝试使用 PCA 等技术来减少矩阵的维数,但这仍然需要进行矩阵操作,需要如此大量的内存,以至于使整个计算变得不可行。
如果你能做些什么以避免创建字典?类似的问题可以通过所谓的核技巧来解决。核技巧使我们能够在非线性数据上使用线性分类器。在这种方法中,输入数据被转换为高维特征空间。有趣的是,你只需要为这一步指定核函数,而不需要将所有数据转换为特征空间,它就会起作用。换句话说,当你计算距离并应用核函数时,你会得到一个数字。这个数字与如果你将初始点扩展到你的核心指向的更高阶空间并计算它们的内积,你会得到相同的数字:

来源:https://towardsdatascience.com/understanding-the-kernel-trick-e0bc6112ef78
在高维空间中获得内积比在实际点中获得更容易。
文本分类的挑战是互补的。原始输入空间通常是线性可分的(因为通常是人类根据标记选择特征),但训练集的大小和高维度非常大。针对这种常见情况,核技巧的一种互补变体被使用。这种方法被称为哈希技巧。在这里,高维向量在ℜd*空间中被映射到一个低维特征空间*ℜm,使得m << d。我们将在ℜ^m空间中训练分类器。
哈希技巧的核心思想就是哈希函数。因此,在文本分类和类似的 NLP 任务中,我们会使用一种非加密的哈希,如 murmur 或 FNV(后面一章会详细介绍),并将工作映射到一个有限的整数(通常是 32 位或 64 位整数,通常是一个质数的模数)。
以下是定义哈希函数的一些特征:
-
最重要的一点是——如果你将相同的输入传递给哈希函数,它始终会返回相同的输出。
-
哈希函数的选择决定了输出可能的范围。这个范围通常是固定的。例如,现代网站使用 SHA256 哈希,它们会被截断为 128 位。
-
哈希函数是单向的。给定一个哈希值,输入是无法计算出来的。
由于哈希函数具有固定范围,使用哈希函数的一个好处是固定的内存需求。另一个优势是它同样在词汇表外(OOV)方面有所帮助。这一点可能不太明显,让我解释一下。首先,我们不需要处理词汇表。当我们使用词袋表示法(BoW)时,我们将使用一个大的列向量(在我们的例子中是 200 万维),为每个训练样本分配许多元素:
FastText is an open-source, free, lightweight library that allows users to learn text representations and text classifiers
-> [0 0 0 0 ... 0 0 0 0] (2000000 elements)
现在我们将选择一个哈希函数f,它接受字符串作为输入并输出值。换句话说,我们确保我们的哈希函数不会访问超出特征维度的索引。
与维持一个庞大的词汇表的朴素的词袋(BoW)方法相比,哈希函数在处理 OOV(Out-Of-Vocabulary,词汇表外)单词时具有优势。由于向量表示是通过哈希函数创建的,任何字符串,即使是 OOV 单词,也会在哈希空间中有一个对应的向量。确实,新词会降低我们分类器的准确性,但它依然能正常工作。预测时不需要丢弃这些新词。
之所以这样有效的原因来自我们不断提到的 Zipf 定律。可能发生的哈希碰撞(如果有的话),很可能发生在不常见的单词之间,或者是常见单词与不常见单词之间。这是因为常见单词按照定义会更早出现,因此倾向于首先占据位置。因此,碰撞的特征用于分类时,要么不太可能被选为特征选择的对象,要么代表着引导分类器选择它的那个词。
既然我们已经阐明了哈希技巧的好处,现在我们需要集中讨论哈希函数。有许多哈希函数可用于实现哈希技巧,例如,Vowpal Wabbit 和 scikit-learn 的 murmurhash v3。可以在以下维基链接中找到一个很好的非加密哈希函数列表:en.wikipedia.org/wiki/List_of_hash_functions#Non-cryptographic_hash_functions。FastText 使用 FNV-1a 哈希,下面将进行详细讨论。
FNV 哈希
fastText 使用 FNV-1a 哈希算法,它是 FNV 哈希的衍生算法。实现该算法时,首先从初始哈希值FNV_offset_basis开始。对于输入中的每个字节,将哈希值与该字节进行 XOR 运算。然后,将结果与 FNV 质数相乘。伪代码如下:
hash = FNV_offset_basis
for each byte_of_data to be hashed
hash = hash XOR byte_of_data
hash = hash × FNV_prime
return hash
来源:维基百科,en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
在 fastText 中,这在dictionary.cc哈希函数中实现(github.com/facebookresearch/fastText/blob/master/src/dictionary.cc#L143):
uint32_t Dictionary::hash(const std::string& str) const {
uint32_t h = 2166136261;
for (size_t i = 0; i < str.size(); i++) {
h = h ^ uint32_t(str[i]);
h = h * 16777619;
}
return h;
}
如你所见,考虑的偏移量基础值是2166136261,对应的质数是16777619。
FNV 并非加密哈希,但它具有很高的分散性质量,并且其哈希结果的大小可变,从 32 位到 1024 位之间的任何 2 的幂。生成它的公式是有史以来最简单的哈希函数之一,且能实现良好的分散性。它还具有较好的碰撞和偏差抵抗能力。FNV 的一个最佳特性是,尽管它不被视为加密哈希,但对超过 64 位的位数进行反向工程几乎是不可解决的。由于需要将计算开销保持在最小,fastText 使用 32 位,这具有 FNV 偏移量基础值为 2166136261。
查看仓库中chapter4文件夹下的fnv1a.py文件中的 Python 实现。
在 fastText 中,单词级和字符级的 n-gram 被哈希到一个固定数量的桶中。这可以防止在训练模型时占用过多的内存。你可以通过-buckets参数来更改桶的数量。默认情况下,桶的数量固定为 2M(200 万)。
词嵌入及其在句子分类中的应用
正如你在上一章中看到的,词嵌入是将单词表示为ℜ^d形状的向量的数值表示。它们是无监督学习的词表示向量,其中应存在语义相似性的关联。我们还在第七章中讨论了什么是分布式表示和分布式表示,部署模型到 Web 和移动端。
在进行句子分类时,有一个假设是可以利用现有的、接近最新技术的监督式 NLP 系统,并通过使用词嵌入来改进它。你可以使用第二章中展示的 fastText 的 cbow/skipgram 方法来创建你自己的无监督词嵌入,使用 FastText 命令行创建模型,或者你可以从fasttext.cc网站下载现成的词嵌入。
一个可能会出现的问题是,某些词向量表示是否对某些任务更有效。当前对一些特定领域的研究表明,在某些任务中效果较好的词向量表示在其他任务中可能表现不佳。一个常见的例子是,适用于命名实体识别任务的词向量表示,在搜索查询分类等问题领域中可能无法发挥良好效果,反之亦然。
fastText 模型量化
由于 Facebook AI 研究团队的努力,现在有了一种方法,可以得到极小的模型(就硬盘占用空间而言),正如你在第二章的M**odel 量化部分中看到的那样,使用 FastText 命令行创建模型。占用数百 MB 的模型可以被量化到仅为几 MB。例如,你可以查看 Facebook 发布的 DBpedia 模型,网址是fasttext.cc/docs/en/supervised-models.html,你会注意到,常规模型(这是 BIN 文件)的大小为 427MB,而较小的模型(FTZ 文件)仅为 1.7MB。
这种体积的减小是通过舍弃一些在 BIN 文件(或更大模型)中编码的信息来实现的。这里需要解决的问题是如何保留重要信息,并识别不那么重要的信息,从而确保模型的整体准确性和性能不会受到显著影响。在本节中,我们将探讨一些相关的考虑因素。
压缩技术
由于我们对如何压缩大型 fastText 模型文件感兴趣,让我们来看看一些可以使用的压缩技术。
不同的压缩技术可以分为以下几类:
-
无损压缩:顾名思义,无损压缩技术是指那些在压缩然后解压后能恢复相同信息结构的技术,信息没有丢失。此类压缩主要使用统计建模方法。你已经遇到过一种用于这种压缩类型的算法,即霍夫曼编码。
-
有损压缩:有损压缩是尽可能丢弃数据而不丢失数据的利益或有用性的过程。这是一种好的技术,适用于我们不关心重新创建原始数据,而更关注原始数据所代表的意义的情况。正如你可以正确推测的那样,通常使用有损压缩时,你将能够获得更高程度的压缩。
FastText 采用了一种称为产品量化的有损压缩方法。在接下来的部分中,我们将尝试理解什么是量化,然后如何从中得出向量量化的概念,以及它如何实现压缩。接着,我们将看看产品量化是如何成为向量量化在这一任务中的更好变体。
量化
量化的概念来自数字信号处理(DSP)领域。在 DSP 中,当将模拟信号(例如声音波)转换为数字信号时,信号会根据位深度被分解为一系列单独的样本,位深度决定了量化信号将具有的级别数。在 8 比特量化的情况下,你将拥有 2⁸=256 种可能的振幅信号组合,类似地,在 16 比特量化的情况下,你将拥有 2¹⁶=65536 种可能的组合。
在下面的例子中,一个正弦波被量化为 3 比特级别,因此它将支持 8 个值来表示连续的正弦波。你可以查看以下代码,获取product quantization.ipynb笔记本中的图像:

向量量化
向量量化是一种将量化思想应用于向量空间的技术。假设你有一个向量空间ℜ[k],并且一个包含N个k维向量的训练集,目标向量空间为ℜ[k]。向量量化的过程是将你的向量映射到一个有限的向量集合 Y,这些向量属于目标向量空间。每个向量y[i]被称为代码向量或代码字,所有代码字的集合称为代码本:

实现的压缩量取决于代码本的大小。如果我们有一个大小为k的代码本,而输入向量的维度是L,我们需要指定
比特来确定从代码本中选择了哪些代码字。因此,对于一个具有大小k的代码本的 L 维向量量化器,其速率是
。
让我们更详细地了解这一过程。如果你有一个L维的向量,其中L=8,它表示如下:

现在有 8 个数字,因此你需要 3 个比特来将其存储在内存中,前提是你选择使用二进制编码。将数组转换为二进制,你将得到以下向量:

因此,如果你想将每个数字存储为 3 位,那么你需要 24 位来存储之前的数组。如果你想将每个数字的内存消耗减少 1 位呢?如果你能够实现这一点,你就可以只用 16 位来存储之前的数组,从而节省 8 位,达到压缩的效果。为此,你可以考虑将 0、2、4、6 作为你的代码书,这些将映射到向量 00、01、10、11:

在转换过程中,所有介于 0 和 2 之间的数字都会映射到 00,所有从 2 到 4 的数字映射到 01,以此类推。因此,你的原始向量将被更改为以下内容:

该向量在内存中占用的空间只有 8 位。
请注意,我们还可以将目标设置为将表示编码为 1 位。在这种情况下,数组只会使用 2 位的总内存。但我们会丧失更多关于原始分布的信息。因此,推广这个理解,减小代码书的大小会增加压缩比,但原始数据的失真也会增加。
为高维空间找到代码书
设计向量量化器的主要目标是找到一个代码书,指定解码器,以及指定编码器的规则,使得向量空间的整体性能最优。
一类重要的量化器是 Voronoi 或最近邻量化器。给定一组大小为 N 的 L 个代码向量
,以及一个距离度量
,R^k 空间被划分为 L 个不相交的区域,称为 Voronoi 区域,每个代码向量与每个区域相关联。与代码向量 v[j] 相关的特定 Voronoi 区域 Ω[j] 包含所有在 R^k 空间中比任何其他代码向量更接近 v[j] 的点,并且是 v[j] 的最近邻域。以下是一个给定空间的 Voronoi 图示例,相关的代码向量用点表示:

获取上述图表的代码也可以在该仓库的 Jupyter notebook 中找到:github.com/PacktPublishing/Learn-fastText/blob/master/chapter4/product%20quantization.ipynb。
因此,本质上,你可以看到这个过程分为两步:
-
构建 Voronoi 空间:构建 Voronoi 空间的预处理阶段,将其表示为图数据结构,并将每个面(例如,顶点、边和区域)与最接近的代码向量集合关联。
-
寻找代码向量:给定 Voronoi 划分,确定包含查询向量的划分面,关联的代码向量即为所需的最近邻。
产品量化
到目前为止,你可能已经理解了在矢量量化中,如何根据与聚类中心的距离将搜索空间划分为多个区域。如果查询向量被量化到某个区域,那么该区域内的所有其他向量都是潜在的好候选。
不幸的是,如果查询位于边缘位置,那么还必须考虑所有相邻的区域。这看起来可能没什么大不了,直到你意识到,每个 Voronoi 单元格的相邻邻居数会随着空间维度 N 的增加而呈指数增长。请注意,在创建 fastText 向量时,我们通常处理的是高维数据,如 100 维、300 维等。
fastText 中的默认向量是 100 维的向量。一个处理 50 位编码的量化器意味着我们每个分量只有 0.5 位,它包含!个质心(大约 150 TB)。产品量化是一种高效的解决方案来解决这个问题。输入向量 x[i] 被分成 m 个不同的子向量 j
,每个子向量的维度为 D* = D/m,其中D是m的倍数。这些子向量被使用 m 个不同的量化器分别进行量化。给定向量ν因此按以下方式映射:
_
在这里,q[j]是与第j个子向量相关的低复杂度量化器。通过子量化器q[j],我们关联索引Ι[j]、码本C[j]和相应的重构值c[j,i]。
产品量化器(PQ)的重构值通过产品索引集中的一个元素来识别!。因此,码本被定义为笛卡尔积:

该集合的质心是 m 个子量化器的质心的连接。从现在开始,我们假设所有子量化器都有相同的有限个重构值 *k**。在这种情况下,总质心数由以下公式给出:

在 fastText 中,涉及产品量化的两个参数,即子量化器的数量 m 和每个量化索引的位数 b,通常设置为!,且b=8。
因此,PQ 可以以非常低的内存/时间成本生成一个指数级大的码本。PQ 的本质是将高维向量空间分解为子空间的笛卡尔积,然后分别对这些子空间进行量化。最佳的空间分解对于良好的产品量化实现至关重要,根据目前的知识,这通常是通过最小化与空间分解和量化码本相关的量化失真来完成的。解决此优化问题有两种已知方法。其中一种是使用迭代量化。一个简单的迭代找到码向量的例子在笔记本中有展示,可以认为它是一个特定的子量化器。如果你有兴趣,可以查看 Kaiming He 等人的《优化产品量化》。
另一种方法,也是 fastText 采用的方法,是对输入向量假设高斯分布,并通过期望最大化来寻找 k-means。请查看此 k-means 函数中的算法:src/productquantizer.cc#L115.
在对输入矩阵进行量化后,在输出矩阵上进行再训练,同时保持输入矩阵不变。这样做是为了让网络重新调整以适应量化。
附加步骤
以下是可以采取的附加步骤:
-
特征选择与剪枝:剪枝会对那些对分类器决策影响较小的特征进行处理。在分类步骤中,只选择有限数量的K词汇和 n-gram。因此,对于每个文档,首先验证它是否被再训练的特征所覆盖,如果没有,则将具有最高范数的特征添加到再训练特征的集合中。
-
哈希化:词汇和 n-gram 也会进行哈希处理,以进一步节省内存。
如果你决定将这些在模型压缩方法中讨论的技术应用于自己的模型,可以调整其中的各种思路,看看在你的具体领域中是否能获得更好的性能。
-
你可以探索是否有其他距离度量适合用于找到 k-means。
-
你可以根据邻域、熵等因素来改变剪枝策略。
总结
本章内容,你已经深入了解了 fastText 模型设计和实现背后的理论,模型的优势,以及在将其应用到你的机器学习管道时需要考虑的事项。
本书的下一部分将讲解实现与部署,我们将在下一章开始介绍如何在 Python 环境中使用 fastText。
第七章:在你自己的模型中使用 FastText
在本节中,你将学习如何将 fastText 模型与其他流行的 NLP 库(如 Gensim 和 spaCy)结合使用。你还将了解如何将 fastText 作为你管道的一部分,这个管道可能还会包含这些其他工具。
第五章,Python 中的 FastText,讲述了如何在 Python 中创建模型,既可以使用 fastText 的官方 Python 绑定,也可以选择使用 Gensim 库,这是一个流行的 Python NLP 库。
在第六章,机器学习与深度学习模型中,如果你已经建立了使用统计机器学习或深度学习范式的流水线,你将了解如何将 fastText 集成到你的 NLP 管道中。在统计机器学习的情况下,本章以 scikit-learn 库为例;在深度学习的情况下,考虑到 Keras、Tensorflow 和 PyTorch。
第七章,将模型部署到移动端和 Web 端,主要讲解如何进行部署以及如何将 fastText 模型集成到实时生产级客户应用中。
第八章:Python 中的 FastText
fastText 的使用专门是为了将单词和句子转换为高效的向量表示。尽管 fastText 是用 C++编写的,但有社区编写的 Python 绑定可以训练和使用这些模型。同时,Python 是自然语言处理(NLP)中最流行的语言之一,因此有许多其他流行的 Python 库支持 fastText 模型及其训练。Gensim 和 Spacy 是两个受欢迎的库,它们使得加载这些向量、转换、词形还原以及高效地执行其他 NLP 任务变得简单。本章将重点介绍如何使用 Python 与 fastText 及其流行库配合使用,同时展示这两个库在处理 fastText 模型时可以完成的一些常见任务。
本章将涵盖以下主题:
-
FastText 官方绑定
-
PyBind
-
预处理数据
-
无监督学习
-
监督学习
-
Gensim
-
训练 fastText 模型
-
使用 Gensim 进行机器翻译
FastText 官方绑定
安装 Python 官方绑定的步骤在第一章中已经介绍。本节将介绍如何使用官方的 fastText Python 包来训练、加载和使用模型。
使用 Python fastText 库,你将能够实现所有通过命令行可以完成的必要功能。让我们看看如何通过 Python fastText 实现无监督学习和监督学习。
注意:在本章中,我们将使用 Python3,因此代码示例将基于此。如果你使用的是 Python2,请查阅附录,其中包含使用 Python2 时需要注意的事项。
PyBind
Python 绑定是通过优秀的 PyBind 库创建的。PyBind 是一个轻量级的库,旨在将 C++类型暴露给 Python,反之亦然,因此它是创建 fastText 的 Python 绑定的绝佳选择。它支持几乎所有流行的 C++编译器,如 Clang、GCC、Visual Studio 等。此外,PyBind 的创建者声称生成的二进制文件更小。
Python-fastText 库使用 fastText 的 C++ API。
数据预处理
尽管 fastText 在原始文本上的性能相当不错,但在运行无监督算法或分类器之前,建议对数据进行预处理。需要记住的一些要点如下:
-
要训练 fastText,编码需要使用 UTF-8 编码。如果是 Python3 中的字符串,PyBind 可以非常好地将几乎所有文本转换为 UTF-8。如果你使用的是 Python2,则需要注意一个额外的技术细节:你必须将使用的所有字符串都编码为 UTF-8。
-
实现一些基本的字符串处理和规范化操作将使模型表现得更好。
以下是一个简单的函数,可用于规范化你的文档。这个函数在 Python fastText 笔记本中使用:
def normalize(s):
"""
Given a text, cleans and normalizes it. Feel free to add your own stuff.
"""
s = s.lower()
# Replace ips
s = re.sub(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', ' _ip_ ', s)
# Isolate punctuation
s = re.sub(r'([\'\"\.\(\)\!\?\-\\\/\,])', r' \1 ', s)
# Remove some special characters
s = re.sub(r'([\;\:\|•«\n])', ' ', s)
# Replace numbers and symbols with language
s = s.replace('&', ' and ')
s = s.replace('@', ' at ')
s = s.replace('0', ' zero ')
s = s.replace('1', ' one ')
s = s.replace('2', ' two ')
s = s.replace('3', ' three ')
s = s.replace('4', ' four ')
s = s.replace('5', ' five ')
s = s.replace('6', ' six ')
s = s.replace('7', ' seven ')
s = s.replace('8', ' eight ')
s = s.replace('9', ' nine ')
return s
如果你正在使用 pandas 从数据集中提取文本并清理它,你还可以将数据集中缺失的文本值替换为 _empty_ 标签:
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
train['Text'] = train['Text'].fillna('_empty_')
test['Text'] = test['Text'].fillna('_empty_')
无监督学习
fastText 命令行实现了两种算法:cbow 和 skip-gram。使用 Python 库,你应该能够在这两种算法中训练模型。
在 fastText 中进行训练
在 fastText 中进行训练是通过 train_unsupervised 函数完成的。你可以从 model 参数中选择使用哪种算法。
然后,你可以使用以下 Python 代码训练一个 skipgram 模型:
sg_model = fastText.train_unsupervised(input='data.txt', model='skipgram')
这与命令行类似:
./fasttext skipgram -input data.train -output model
同样,要训练一个 cbow 模型,你可以使用以下 Python 代码:
cbow_model = fastText.train_unsupervised(input='data.txt', model='cbow')
命令行中的等效语句是:
./fasttext cbow -input data.train -output model
Python 代码与命令行的区别在于,命令行会将模型保存在文件中,而在 Python 代码中,模型会存在内存中,通过变量引用。如果要保存模型,你需要在 Python 应用中显式地传递命令,例如:
sg_model.save_model("sg_model.bin")
你应该能够传入所有其他训练参数。这些参数以及默认值在这里列出:
sg_model = fastText.train_unsupervised(input, model='skipgram', lr=0.05, dim=100, ws=5, epoch=5, minCount=5, minCountLabel=0, minn=3, maxn=6, neg=5, wordNgrams=1, loss="ns", bucket=2000000, thread=12, lrUpdateRate=100, t=1e-4, label="__label__", verbose=2, pretrainedVectors="")
这些参数的意义与在探索命令行时看到的相同。
评估模型
在无监督学习的情况下,由于缺乏标签,评估变得有些棘手,因为没有可以用来有意义地比较模型结果的标准。在词嵌入的情况下,我们遇到了相同的问题,但由于这是一个相对狭窄的领域,我们可以做出一些主观的判断。fastText 命令行提供了最近邻和查找词语相似度的选项,我们可以在 Python 库中复现这一过程,稍后会看到。
其他技术包括使用基于问题的单词句法和语义表现——例如 Google 发布的 words.txt,以及使用斯坦福稀有词数据库来衡量稀有词的形态学相似性。如果你正在为某个特定领域创建词表示,请记住,这些特定的模型评估技术可能不会产生良好的结果,但这些技术应该是适用的。
词向量
默认情况下,创建的词向量是 100 维的。它们以 NumPy 数组的形式保存在内存中。所以,你应该能够使用 get_word_vector 方法查看词向量:
>>> model.get_word_vector('targetword')
array([ 0.09973086, ... 0.14613365], dtype=float32)
最近邻查询
通常,k 最近邻用于区分模型之间的差异。目标词的向量表示被取出,找到这些向量的邻居,然后查看邻居是否更接近它的含义。由于 fastText 的表示是分布式的,这一假设应该成立。
fastText 命令行提供了一个工具,可以轻松获取最近邻词,但在 Python 中没有简单的方法可以找到它们。util中有一个 find_nearest_neighbor 函数,但它需要向量作为输入。因此,我们需要编写一些代码,创建一个函数,该函数接受单词和目标模型作为输入,并根据模型返回最近邻词。你可以查看 python fastText unsupervised learning.ipynb 文件中的代码来获取最近邻词:
>>> nn(sg_model, ['dog', 'pizza', 'hungry'], k=5)
words similar to dog:
dogs
pup
treats
puppy
dogie
#########################################
words similar to pizza:
pizza;
pizza"
pizzas
"pizza
bread
#########################################
words similar to hungry:
hungry";
hungrygirl
>hungry
hungry-girl
hungries
#########################################
输出结果可以通过对数据进行一些预归一化处理来进行优化。
单词相似性
有多种方法可以找到单词之间的相似性。以 fastText 为例,其中一种找到单词相似性的方法是计算单词在向量空间中的余弦距离。然而,这种方法可能无法找到同义词和反义词之间的相似性,也无法捕捉其他细微的语言构造,而仅仅是基于单词在上下文中的使用给出一个相似度分数。单词“water”和“cup”之间不一定有相似之处,但在特定上下文中,它们通常是一起出现的,因此你可能会发现它们之间的相似度分数较高。
在 Python 库中,你可以编写一个小函数来计算余弦相似度:
def similarity(v1, v2):
n1 = np.linalg.norm(v1)
n2 = np.linalg.norm(v2)
return np.dot(v1, v2) / n1 / n2
v1 = sg_model.get_word_vector('drink')
v2 = sg_model.get_word_vector('drinks')
print(similarity(v1, v2))
本质上,你是通过 get_word_vector 方法获取两个目标单词的词向量,然后计算它们之间的余弦相似度。
模型性能
你可以使用斯坦福 NLP 发布的稀有词数据集来评估模型的性能。通过 examples 文件夹中共享的 compute_similarity 函数,我们可以稍微修改该函数,使其能够在 Python 应用中工作。函数的实现可以在无监督学习笔记本中看到。下载稀有词数据集,你可以在参考文献中找到下载链接,解压后,将文本文件作为第一个参数,模型作为第二个参数传入。你应该能够看到模型如何评估这些稀有单词:
>>> dataset, corr, oov = compute_similarity('data/rw/rw.txt', sg_model)
>>> print("{0:20s}: {1:2.0f} (OOV: {2:2.0f}%)".format(dataset, corr, 0))
rw.txt : 32 (OOV: 0%)
模型可视化
可视化词向量在空间中的分布是一种有效理解模型分布特性的方式。由于词向量的维度非常高,因此你需要一个好的降维技术,以便将向量显示在二维框架中。
t-SNE 是一种流行的降维技术,特别适用于高维数据集的可视化。其思路是尽可能地将相似的单词靠近,同时最大化不相似单词之间的距离。无监督学习笔记本中展示了 t-SNE 模型的代码。在我们的例子中,我们选取了一些单词并将它们绘制在图中:

绘制在图中的单词
如你所见,“water”和“cup”是一起的,因为它们通常在相同的上下文中使用。另两个在一起的词向量是“drink”和“tea”。使用 t-SNE 来理解你的模型可以让你更好地了解模型的效果。
有监督学习
与无监督学习类似,fastText 库也提供了内部 API 来运行有监督学习。因此,运行 fastText 有监督的 Python API 也会创建相同的模型,可以使用命令行应用进行训练。其优势在于,你能够利用所有 Python 数据科学工具来构建 NLP 分类器。
为了展示如何利用 fastText 分类器在 Python 中进行训练,你可以查看代码中的python fastText supervised learning.ipynb笔记本。数据集包含来自亚马逊的精美食品评论,并可以从 Kaggle 网站下载,笔记本中提供了下载链接。
数据预处理和归一化
数据预处理和归一化步骤与无监督学习中的类似。不同之处在于,你需要在标签前添加__label__前缀,或选择你喜欢的标签前缀。同时,它必须以类似于 fastText 命令行的格式保存在 fastText 文件中。由于这是一个分类器,你需要实际创建两个文件,一个用于训练,另一个用于模型验证。将数据集分为训练集和测试集的常见方法是使用 scikit-learn 中的train_test_split函数。
训练模型
要训练模型,你需要在训练文件上使用train_supervised方法:
>>> su_model = fastText.train_supervised(input=train_file, epoch=25, lr=1.0, wordNgrams=2, verbose=2, minCount=1)
这与命令行中的操作类似:
$ ./fasttext supervised -input train_file -output su_model
超参数与有监督学习中的相同。与无监督学习的区别在于,默认的损失函数是softmax而不是ns,并且还有一个额外的label参数:
>>> su_model = fastText.train_supervised(input=train_file, lr=0.1, dim=100, ws=5, epoch=5, minCount=1, minCountLabel=0, minn=0, maxn=0, neg=5, wordNgrams=1, loss="softmax", bucket=2000000, thread=12, lrUpdateRate=100, t=1e-4, label="__label__", verbose=2, pretrainedVectors="")
类似于无监督学习的情况,Python 代码不会将模型保存到文件中,而是将其保存到你定义的变量中,这里是su_model。这个变量su_model是一个 Python NumPy 矩阵,因此我们可以按照标准方式操作它。
要保存模型,你需要调用save_model方法:
>>> su_model.save_model("su_model.bin")
预测
你可以使用get_word_vector方法获取单词的词向量,使用get_sentence_vector方法获取文档的句子向量,并使用predict方法获取模型的预测标签:
>>> su_model.get_word_vector('restaurant')
array([ 0.9739366 , ..., -0.17078586], dtype=float32)
>>> su_model.get_sentence_vector('I love this restaurant')
array([ 0.31301185, ... , -0.21543942], dtype=float32)
>>> su_model.predict('I love this restaurant')
(('__label__5',), array([1.00001001]))
你也可以对测试文档进行预测概率计算:
>>> su_model.predict("I love this restaurant", k=3)
(('__label__5', '__label__2', '__label__3'),
array([1.00001001e+00, 1.00000034e-05, 1.00000034e-05]))
这与命令行中的操作类似:
$ echo "I love this restaurant" | ./fasttext predict-prob su_model.bin - 3
__label__5 1.00001 __label__2 1e-05 __label__3 1e-05
测试模型
获取模型的精度和召回率类似于命令行中看到的操作。与命令行类似,你需要传递测试文件和你需要计算精度和召回率的标签数量。
在下面的示例中,test_file包含测试文件的路径,第二个参数是标签的数量:
>>> n, p, r = su_model.test(test_file, 5)
>>> print("N\t" + str(n))
>>> print("P@{}\t{:.3f}".format(5, p))
>>> print("R@{}\t{:.3f}".format(5, r))
N 113691
P@5 0.200
R@5 1.000
混淆矩阵
混淆矩阵是一种很好的方式来可视化监督模型的性能,特别是分类器。当你在创建混淆矩阵时,实际上是在描述模型分类器在测试集上的表现,而测试集的真实标签是已知的。混淆矩阵在多类分类器中尤其有效,它帮助你了解哪些类别表现更好。由于 fastText 支持分类器,你可以使用它来创建混淆矩阵。
如何获取混淆矩阵,已在监督学习的笔记本中展示。fasttext_confusion_matrix函数接收一个模型变量、pandas 测试数据、标签列名和文本列名:

预测的标签与真实值进行对比。
Gensim
Gensim 是一个流行的开源库,用于处理由 Radim Řehůřek 创建的原始、非结构化的人工生成文本。Gensim 的一些特点包括:
-
内存独立性是 Gensim 的核心价值主张之一,即它应该具有可扩展性,并且不需要将所有文档都保存在 RAM 中。因此,你将能够训练比你机器的内存大得多的文档。
-
Gensim 对各种流行的向量空间算法有高效的实现。最近,fastText 在 Gensim 中的实现也已经完成。
-
还有一些流行数据格式的 IO/包装器和转换器。请记住,fastText 仅支持 UTF-8 格式,因此,如果你有不同格式的数据,Gensim 可能是一个不错的选择。
-
不同的相似度查询算法。因此,你不必局限于 fastText 中提供的算法。
你可以通过 Gensim 使用 fastText 的两种方式:使用 Gensim 本地实现的 fastText,或者通过 Gensim 对 fastText 的包装器。
现在,让我们来看看如何使用 Gensim 训练一个 fastText 模型。
训练 fastText 模型
对于这里展示的示例,我们将使用 Lee 语料库来训练模型。为了获取所需的数据,我建议你从 GitHub 克隆 Gensim 的代码库。
在这里展示的代码示例中,我们将使用 Kaggle 的假新闻数据集来演示 Gensim 的 fastText。首先,下载数据并清理文本:
In [1]: from gensim.models.fasttext import FastText
...: from gensim.corpora import Dictionary
...: import pandas as pd
...: import re
...: from gensim.parsing.preprocessing import remove_stopwords, strip_punctuation
...: import numpy as np
...:
In [2]: df_fake = pd.read_csv('fake.csv')
...: df_fake[['title', 'text', 'language']].head()
...: df_fake = df_fake.loc[(pd.notnull(df_fake.text)) & (df_fake.language == 'english')]
...:
In [3]: # remove stopwords and punctuations
...: def preprocess(row):
...: return strip_punctuation(remove_stopwords(row.lower()))
...:
...: df_fake['text'] = df_fake['text'].apply(preprocess)
...:
In [4]: # Convert data to required input format by LDA
...: texts = []
...: for line in df_fake.text:
...: lowered = line.lower()
...: words = re.findall(r'\w+', lowered)
...: texts.append(words)
...:
我们将首先看一下如何使用 fastText 包装器来训练模型。要使用 fastText 包装器,你需要在机器上安装 fastText。如果你按照第一章的说明,介绍 FastText,你应该已经安装了 fastText。然而,这个包装器已经被弃用,推荐使用 Gensim 的 fastText 实现:
>>> from gensim.models.wrappers.fasttext import FastText as FT_wrapper
>>> # Set FastText home to the path to the FastText executable
>>> ft_home = '/usr/local/bin/fasttext'
>>> # train the model
>>> model_wrapper = FT_wrapper.train(ft_home, lee_train_file)
>>> print(model_wrapper)
如果你有兴趣在 Gensim 中使用 fastText 实现,你需要使用 gensim.models 中的 FastText 类,它不仅包含 fastText 还有 word2vec 和许多其他可以使用的模型:
In [1]:
...: import gensim
...: import os
...: from gensim.models.word2vec import LineSentence
...: from gensim.models.fasttext import FastText
In [2]: # Set file names for train and test data
...: data_dir = '{}'.format(os.sep).join([gensim.__path__[0], 'test', 'test_data']) + os.sep
...: lee_train_file = data_dir + 'lee_background.cor'
...: lee_data = LineSentence(lee_train_file)
In [3]: model = FastText(size=100)
In [4]: model.build_vocab(lee_data)
In [5]: # train the model
...: model.train(lee_data, total_examples=model.corpus_count, epochs=model.epochs)
...: print(model)
FastText(vocab=1762, size=100, alpha=0.025)
超参数
Gensim 支持与 fastText 原生实现相同的超参数。你应该能够设置 Facebook fastText 实现中的大多数超参数。默认值大多数已经存在。以下列出了一些差异:
-
sentences:这可以是一个包含多个标记的列表。通常建议使用标记流,例如你已经看到过的 word2vec 模块中的LineSentence。在 Facebook 的 fastText 库中,这由文件路径表示,并通过-input参数提供。 -
max_vocab_size:用于限制内存大小。如果有更多独特的单词,这将修剪掉不太常见的单词。这个值需要根据你拥有的 RAM 来决定。例如,如果你的内存是 2 GB,那么max_vocab_size的值就是为这 2 GB 内存设置的。如果你没有手动设置这个值,则没有设置限制。 -
cbow_mean:与 fastText 命令有所不同。在原始实现中,对于 cbow,取向量的均值。但在这里,你可以选择通过传递 0 和 1 来使用向量的和,前提是你想尝试均值。 -
batch_words:这是传递的批次的目标大小。默认值为 10,000。这类似于命令行中的-lrUpdateRate,批次数决定了权重何时更新。 -
callbacks:一个回调函数的列表,用于在训练过程中的特定阶段执行。 -
-supervised和-labels参数没有对应的实现,因为 Gensim 只专注于无监督学习。
模型保存和加载
Gensim 提供了保存和加载方法来处理所有模型,fastText 的实现也遵循这一方式:
In [6]: # saving a model trained via Gensim's fastText implementation
...: model.save('saved_model_gensim')
...: loaded_model = FastText.load('saved_model_gensim')
...: print(loaded_model)
...:
FastText(vocab=1762, size=100, alpha=0.025)
In [7]: import os; print(os.path.exists('saved_model_gensim'))
True
通过 load_fasttext_format 类方法,也可以加载二进制 fastText 模型:
In [1]: from gensim.models.fasttext import FastText
In [2]: modelpath = "wiki.simple.bin"
In [3]: model = FastText.load_fasttext_format(modelpath)
In [4]: print(model)
FastText(vocab=111051, size=300, alpha=0.025)
词向量
在 Gensim 中,你可以检查词汇表中是否包含某个词,并获取该词的词向量。由于 fastText 支持为词汇表中没有的词生成词向量,你应该能够即使词不在词汇表中也能获取其词向量。如果词汇表中没有任何字符 n-grams,可能就无法获取词向量:
In [1]: import gensim
...: import os
...: from gensim.models.word2vec import LineSentence
...: from gensim.models.fasttext import FastText
...:
In [2]: # Set file names for train and test data
...: data_dir = '{}'.format(os.sep).join([gensim.__path__[0], 'test', 'test_data']) + os.sep
...: lee_train_file = data_dir + 'lee_background.cor'
...: lee_data = LineSentence(lee_train_file)
...:
In [3]: model = FastText(size=100)
In [4]: # build the vocabulary
...: model.build_vocab(lee_data)
In [5]: # train the model
...: model.train(lee_data, total_examples=model.corpus_count, epochs=model.epochs)
In [6]: print('night' in model.wv.vocab)
True
In [7]: print('nights' in model.wv.vocab) # this is not present
False
In [8]: print(model.wv['night'])
[-0.02308581 ... 0.15816787]
In [9]: print(model.wv['nights'])
[-0.02073629 ... 0.1486301 ]
In [10]: # Raises a KeyError since none of the character ngrams of the word `axe` are present in the training data
...: model.wv['axe']
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-10-902d47f807a0> in <module>()
1 # Raises a KeyError since none of the character ngrams of the word `axe` are present in the training data
----> 2 model.wv['axe']
...
KeyError: 'all ngrams for word axe absent from model'
模型评估
由于 Gensim 实现了无监督算法,因此没有直接的方法来衡量最终模型的好坏。模型评估取决于你的使用场景以及它在最终应用中的表现。
Gensim fastText 提供了多种方法用于查找单词之间的相似度。以下结果是通过加载 wiki.simple.bin 模型得到的。
计算两个单词之间相似度的最简单方法是使用similarity方法:
In []: model.wv.similarity('night', 'nights')
Out[]: 0.9999931241743173
FastText 仅在监督学习期间计算句子或文档向量。根据任务的不同,简单地对句子中所有标准化单词的词嵌入进行平均应该就足够了。
你可以使用n_similarity方法来获取两个文档之间的相似度。根据 Gensim 的文档,这个方法将返回两个文档之间的余弦相似度。这些文档需要作为列表传入:
In []: model.wv.n_similarity(['sushi', 'shop'], ['japanese', 'restaurant'])
Out[]: 0.6041413398970296
In []: model.wv.n_similarity('Obama speaks to the media in Illinois'.lower().split(), 'The president greets the press in Chicago'.lower().spl
...: it())
Out[]: 0.7653119647179297
Gensim 还允许你查找最不相关的文档,类似于找到与众不同的那个:
In []: model.wv.doesnt_match("breakfast cereal dinner lunch".split())
Out[]: 'cereal'
most_similar方法将根据模型给出最相似的单词:
In []: model.wv.most_similar('food')
Out[]:
[('foods', 0.6859725713729858),
('foodstuffs', 0.679445743560791),
('seafood', 0.6695178151130676),
('eat', 0.5922832489013672),
('meals', 0.5820232629776001),
('meat', 0.5773770213127136),
('eaten', 0.5611693263053894),
('nutritious', 0.5602636337280273),
('snacks', 0.5574883818626404),
('cooked', 0.5470614433288574)]
=
Gensim 提供了一个易于使用的方法来在 WordSim 353 基准测试上评估模型。这个数据集是评估向量空间模型的标准数据集。每个单词没有上下文,它们之间的相似度评分在 0 到 10 的范围内逐渐增加。你可以在 Gensim GitHub 仓库的gensim/test/test_data/wordsim353.tsv中找到该文件:
In []: model.wv.evaluate_word_pairs('wordsim353.tsv')
Out[]:
((0.6645467362164186, 2.4591009701535706e-46),
SpearmanrResult(correlation=0.7179229895090848, pvalue=3.58449522917263e-57),
0.0)
第一个结果是皮尔逊相关系数(即我们熟知的普通相关系数),第二个结果是斯皮尔曼相关系数。
你还可以使用most_similar方法来进行 A - B + C 类型的查询:
In []: model.wv.most_similar(positive=['story', 'dove'], negative=['stories']) # Vector('story') - Vector('stories') + Vector('dove')
Out[]:
[('doves', 0.5111404657363892),
('dovepaw', 0.5014846324920654),
('turtledove', 0.4434218406677246),
('dovecote', 0.4430897831916809),
('warbler', 0.43106675148010254),
('warble', 0.40401384234428406),
('asshole', 0.4017521142959595),
('dovre', 0.39799436926841736),
('nothofagus', 0.389825701713562),
('moriarty', 0.388924241065979)]
类似于这种类型的语法和语义相似性测试,如果你正在创建英语单词向量,你可以使用 Google 准备并发布的question-words.txt任务。你可以在gensim/docs/notebooks/datasets/question-words.txt找到这个文本文件。
现在,你可以运行以下代码。同时,将日志设置为 info,这样你就能根据不同字段的百分比获得准确性。数据集中有九种语法比较类型:家族、比较级、最高级、现在分词、民族形容词、过去式和复数:
model.accuracy("question-words.txt")
这将给你一个输出,显示哪里存在不匹配的地方,即答案与单词列表不符。你可以根据这个进行评估。如果你正在训练其他语言,考虑创建一个类似的question-words.txt文件,这个文件可以根据目标语言的不同语法焦点进行构建,这将是一个不错的投资。
Word Mover's Distance
Word Mover's Distance (WMD) 是一种非常有效的方式来衡量两个文档之间的相似性,即使它们之间没有共同的单词。看看以下示例。如果我们考虑 WMD,像greet和speaks这样的词会非常接近:

来源: markroxor.github.io/gensim/static/notebooks/WMD_tutorial.html
在 Gensim 中,你可以使用wmdistance方法来查找两个文档之间的距离,如下所示:
In []: sentence_obama = 'Obama speaks to the media in Illinois'
...: sentence_president = 'The president greets the press in Chicago'
...: sentence_obama = sentence_obama.lower().split()
...: sentence_president = sentence_president.lower().split()
...:
In []: # Remove stopwords.
...: stop_words = stopwords.words('english')
...: sentence_obama = [w for w in sentence_obama if w not in stop_words]
...: sentence_president = [w for w in sentence_president if w not in stop_words]
In []: distance = model.wv.wmdistance(sentence_obama, sentence_president)
...: print('distance = %.4f' % distance)
distance = 4.9691
你可以在你的语料库上初始化一个单词移动相似度类:
from gensim.similarities import WmdSimilarity
num_best = 10
instance = WmdSimilarity(wmd_corpus, model, num_best=10)
这里,wmd_corpus是您的语料库,model是您训练好的 fastText 模型。现在,您可以在实例上运行查询,这只是一个查找操作。
从训练过程中获取更多信息
在我们进行模型训练的过程中,您可能还会想了解模型的进展和性能。了解模型是如何学习的,这对调试模型和提高其性能非常有帮助。
另一个可能出现的问题是训练大型语料库。对大型语料库进行多个周期的训练可能需要很长时间,因此您可能希望在每个周期完成后保存模型。
在这种情况下,Gensim 实现了CallbackAny2Vec类的回调参数,CallbackAny2Vec是来自gensim.models.callbacks模块的子类序列。通过使用这个类,您可以创建在训练过程中某些特定点保存函数的类:
from gensim.test.utils import common_texts as sentences
from gensim.models.callbacks import CallbackAny2Vec
from gensim.models import Word2Vec
from gensim.test.utils import get_tmpfile
class EpochSaver(CallbackAny2Vec):
"Callback to save model after every epoch"
def __init__(self, path_prefix):
self.path_prefix = path_prefix
self.epoch = 0
def on_epoch_end(self, model):
output_path = '{}_epoch{}.model'.format(self.path_prefix, self.epoch)
print("Save model to {}".format(output_path))
model.save(output_path)
self.epoch += 1
# to save the similarity scores
similarity = []
class EpochLogger(CallbackAny2Vec):
"Callback to log information about training"
def __init__(self):
self.epoch = 0
def on_epoch_begin(self, model):
print("Epoch #{} start".format(self.epoch))
def on_epoch_end(self, model):
print("Epoch #{} end".format(self.epoch))
self.epoch += 1
def on_batch_begin(self, model):
similarity.append(model.wv.similarity('woman', 'man'))
EpochSaver类在每个周期结束时保存模型。EpochLogger类做两件事。它打印周期的开始和结束,并且每当开始一个批次循环时,它将相似度分数保存到一个名为 similarity 的列表中。我们稍后会使用这个列表进行可视化。
现在,实例化这些类并将它们传递给模型训练过程:
import gensim
from gensim.models.word2vec import LineSentence
from gensim.models.fasttext import FastText
# Set file names for train and test data
lee_train_file = './gensim/gensim/test/test_data/lee_background.cor'
lee_data = LineSentence(lee_train_file)
model_gensim = FastText(size=100)
# build the vocabulary
model_gensim.build_vocab(lee_data)
# instantiate the callbacks
epoch_saver = EpochSaver(get_tmpfile("temporary_model"))
epoch_logger = EpochLogger()
# train the model
model_gensim.train(lee_data,
total_examples=model_gensim.corpus_count,
epochs=model_gensim.epochs,
callbacks=[epoch_saver, epoch_logger])
print(model_gensim)
当您运行此代码时,您应该能够看到日志记录器在工作并记录每个周期的日志。同时,不同的模型将被保存在磁盘上。
要查看相似度分数随着训练的进展,您可以启动一个 visdom 服务器。Visdom 是 Facebook 推出的可视化工具包,它作为服务器运行。它的优点是您可以将数据发送给它,并且可以使用网页浏览器监控更新的参数。要启动 visdom 服务器,您需要先安装 visdom,然后从命令行运行它:
$ pip install visdom
$ python -m visdom.server
现在,您可以将相似度分数传递给服务器:
import visdom
vis = visdom.Visdom()
trace = dict(x=list(range(len(similarity))), y=similarity, mode="markers+lines", type='custom',
marker={'color': 'red', 'symbol': 104, 'size': "10"},
text=["one", "two", "three"], name='1st Trace')
layout = dict(title="First Plot", xaxis={'title': 'x1'}, yaxis={'title': 'x2'})
vis._send({'data': [trace], 'layout': layout, 'win': 'mywin'})
如果您在http://localhost:8097打开服务器,您应该能够看到这个图:

一个生成的 visdom 图表示例
使用 Gensim 进行机器翻译
根据 Mikolov 2013 年的论文,参考文献中提供了该论文的链接,您可以使用以下方法,该方法包括两个步骤:
-
首先,使用大量文本构建单语模型
-
使用一个小型双语词典来学习语言间的线性投影
因此,对于第一步,您可以直接使用fasttext.cc网站上预构建并共享的 fastText 模型。在这一部分,我们将看看如何使用 Gensim 实现第二步。
目标是训练一个翻译矩阵,它本质上是一个线性变换矩阵,将源语言词向量与目标语言词向量连接起来。
你可以从源语言到目标语言下载转换文件;一个好的来源是 Facebook muse 文档,你感兴趣的语言可能会列在那里。如果没有,你将需要付出努力自己创建转换文件。在本节的示例中,你可以在代码库中找到,en-it.txt 文件用于英到意大利语的翻译,并且它包含了 103,612 个相似单词,因此你可能需要为你的模型创建类似的单词转换文件,以便在性能上达到一定水准。一旦你有了模型和转换文件,将转换文件加载到 word_pair 元组,并将向量加载到各自的源目标模型中。完成后,你可以运行类似于以下代码:
transmat = translation_matrix.TranslationMatrix(source_word_vec, target_word_vec, word_pair)
transmat.train(word_pair)
print ("the shape of translation matrix is: ", transmat.translation_matrix.shape)
在预测时,对于任何给定的新单词,我们可以通过计算 z = Wx 将其映射到另一个语言空间,然后我们找到在目标语言空间中与 z 向量表示最接近的单词。考虑的距离度量是余弦相似度。这与此处所示的代码类似:
# The pair is in the form of (English, Italian), we can see whether the translated word is correct
words = [("one", "uno"), ("two", "due"), ("three", "tre"), ("four", "quattro"), ("five", "cinque")]
source_word, target_word = zip(*words)
translated_word = transmat.translate(source_word, 5)
for k, v in translated_word.iteritems():
print ("word ", k, " and translated word", v)
你应该能够看到类似以下代码的输出:
('word ', 'one', ' and translated word', [u'solo', u'due', u'tre', u'cinque', u'quattro'])
('word ', 'two', ' and translated word', [u'due', u'tre', u'quattro', u'cinque', u'otto'])
('word ', 'three', ' and translated word', [u'tre', u'quattro', u'due', u'cinque', u'sette'])
('word ', 'four', ' and translated word', [u'tre', u'quattro', u'cinque', u'due', u'sette'])
('word ', 'five', ' and translated word', [u'cinque', u'tre', u'quattro', u'otto', u'dieci'])
我们可以看到翻译结果是可信的。向量被绘制在以下图表中:

在图表中绘制的向量
模型训练代码和更详细的可视化评估可以在 Jupyter notebook gensim translation matrix with fasttext.ipynb 中找到。
总结
到此为止,我们已经结束了本章内容,讨论了如何在 Python 环境中进行训练、验证和预测。为了实现这一点,我们重点介绍了两个包:官方的 fastText Python 包和 Gensim 包。
在下一章中,我们将看看如何将 fastText 集成到机器学习或深度学习的工作流中。
第九章:机器学习和深度学习模型
在我们到目前为止讨论的几乎所有应用中,都隐含着一个假设,那就是你正在创建一个新的机器学习 NLP 流水线。现在,情况不一定总是如此。如果你已经在一个成熟的平台上工作,fastText 可能也是一个很好的补充,可以使流水线更强大。
本章将为你提供一些使用流行框架(如 scikit-learn、Keras、TensorFlow 和 PyTorch)实现 fastText 的方法和示例。我们将讨论如何利用其他深度神经网络架构,如卷积神经网络(CNN)或注意力网络,来增强 fastText 词嵌入的能力,解决各种 NLP 问题。
本章涵盖的主题如下:
-
Scikit-learn 和 fastText
-
嵌入层
-
Keras
-
Keras 中的嵌入层
-
卷积神经网络架构
-
TensorFlow
-
PyTorch
-
Torchtext
Scikit-learn 和 fastText
在本节中,我们将讨论如何将 fastText 集成到统计模型中。最常用和流行的统计机器学习库是 scikit-learn,因此我们将重点关注它。
scikit-learn 是最流行的机器学习工具之一,原因是其 API 非常简单且统一。流程如下:
-
你基本上是将数据转换为矩阵格式。
-
然后,你创建预测类的实例。
-
使用实例,你在数据上运行
fit方法。 -
一旦模型创建完成,你可以在其上运行
predict。
这意味着你可以通过定义 fit 和 predict 方法来创建一个自定义分类器。
fastText 的自定义分类器
由于我们有兴趣将 fastText 词向量与线性分类器结合使用,你不能直接传递整个向量,而是需要一种方法来定义一个单一的向量。在这种情况下,我们选择使用均值:
class MeanEmbeddingVectorizer(object):
def __init__(self, ft_wv):
self.ft_wv = ft_wv
if len(ft_wv)>0:
self.dim = ft_wv[next(iter(all_words))].shape[0]
else:
self.dim=0
def fit(self, X, y):
return self
def transform(self, X):
return np.array([
np.mean([self.ft_wv[w] for w in words if w in self.ft_wv]
or [np.zeros(self.dim)], axis=0)
for words in X
])
现在,你需要将令牌字典传递给模型,该字典可以通过 fastText 库构建:
f = load_model(FT_MODEL)
all_words = set([x for tokens in data['tokens'].values for x in tokens])
wv_dictionary = {w: f.get_word_vector(w) for w in all_words}
将整个过程整合在一起
你可以使用 scikit-learn 的 Pipeline 来将整个流程组合在一起,如下所示:
etree_w2v = Pipeline([("fasttext word-vector vectorizer", MeanEmbeddingVectorizer(wv_dictionary)),
("extra trees", ExtraTreesClassifier(n_estimators=200))])
整个代码显示在统计机器学习笔记本中。为了进一步提高模型的效果,如果你能找到更好的方法来减少词向量,TF-IDF 已在共享笔记本中展示。另一种减少词向量的方法是使用哈希转换器。
在接下来的几节中,我们将探讨如何在深度学习模型中嵌入 fastText 向量。
嵌入层
正如你所看到的,当你需要在机器学习中处理文本时,你需要将文本转换为数值。神经网络架构中也有相同的逻辑。在神经网络中,你通过实现嵌入层来做到这一点。所有现代深度学习库都提供了嵌入 API 供使用。
嵌入层是一个有用且多功能的层,用于各种目的:
-
它可以用于学习词嵌入,以便稍后在应用中使用
-
它可以与更大的模型一起使用,在该模型中,嵌入也会作为模型的一部分进行调优
-
它可以用来加载预训练的词嵌入
本节的重点是第三点。我们的想法是利用 fastText 创建更优的词嵌入,然后通过这个嵌入层将其注入到你的模型中。通常,嵌入层会使用随机权重进行初始化,但在这种情况下,我们将通过 fastText 模型的词嵌入来初始化它。
Keras
Keras 是一个广泛流行的高级神经网络 API。它支持 TensorFlow、CNTK 和 Theano 作为后端。由于 Keras 具有用户友好的 API,许多人使用它代替基础库。
Keras 中的嵌入层
嵌入层将是 Keras 网络的第一个隐藏层,你需要指定三个参数:输入维度、输出维度和输入长度。由于我们将使用 fastText 来改善模型,因此还需要传递带有嵌入矩阵的权重参数,并将训练矩阵设置为不可训练:
embedding_layer = Embedding(num_words,
EMBEDDING_DIM,
weights=[embedding_matrix],
input_length=MAX_SEQUENCE_LENGTH,
trainable=False)
另外,我们需要注意的是,我们需要将词映射到整数,并将整数映射回词。在 Keras 中,你可以使用Tokenizer类来实现这一点。
让我们在卷积神经网络(CNN)中看看这一过程的实际应用。
卷积神经网络
当我们讨论将词嵌入和神经网络结合时,卷积神经网络是取得良好成果的一种方法。CNN 是通过在结果上应用多个卷积层和非线性激活函数(如 ReLu 或tanh)创建的。
让我们稍微谈一下什么是卷积。一个函数与另一个函数的卷积是一个积分,表示一个函数在经过另一个函数时的重叠量。你可以把它理解为将一个函数融合到另一个函数中。在信号理论中,专家是这样理解卷积的:输出信号是输入信号与环境冲击响应的卷积。任何环境的冲击响应本质上识别并区分该环境。
在传统的前馈神经网络中,我们将每个输入神经元连接到下一层中的每个输出神经元。而在 CNN 中,我们则使用卷积来计算输出。在训练阶段,CNN 会自动学习过滤器的值。
CNN 通常与词嵌入一起使用,在这里,fastText 就发挥了作用,它能够通过提供更好的词表示来在分类精度上带来巨大的提升。因此,架构由三个关键部分组成:

如果你在分类管道中已经有了这三个架构组件,你可以识别出词嵌入部分,看看是否将其更换为 fastText 能改善预测效果。
在这个示例中,我们将回顾之前的 Yelp 评论示例,并尝试使用卷积神经网络对其进行分类。为了简便起见,我们将使用一个已经发布的预训练数据集,但你可能会处理一个特定领域的用例,因此你应该按照前一章中的示范,整合模型的创建。如你所见,你可以使用 fastText 库或 Gensim 库。
从高层次来看,步骤如下:
-
数据集中的文本样本会被转换为单词索引序列。单词索引 仅仅是一个整数 ID,表示某个单词。我们将只考虑数据集中最常见的 20,000 个单词,并将序列截断至最多 1,000 个单词。这么做是为了计算的方便。你可以尝试这种方法,并找到最能带来通用性的做法。
-
准备一个嵌入矩阵,它将在索引
i处包含单词索引中单词i的嵌入向量。 -
然后将嵌入矩阵加载到 Keras 嵌入层,并将该层设置为冻结状态,以确保在训练过程中不会被更新。
-
紧接其后的层将是卷积网络。最后将有一个 softmax 层,将输出收敛到我们的五个类别。
在这种情况下,我们可以使用 pandas 库来创建输入文本的列表和输出标签的列表:
>>> df = pd.read_csv('yelp_review.csv')
>>> texts = df.text.values
>>> labels = df.stars.values
>>> texts = texts[:20000]
>>> labels = labels[:20000]
>>> print('Found %s texts.' % len(texts))
Found 20000 texts.
现在,我们需要将文本样本和标签格式化为可以输入神经网络的张量。在这一部分,我们将使用Tokenizer类。我们还需要对序列进行填充,以保证所有矩阵的长度相等:
>>> # finally, vectorize the text samples into a 2D integer tensor
>>> tokenizer = Tokenizer(num_words=MAX_NUM_WORDS)
>>> tokenizer.fit_on_texts(texts)
>>> sequences = tokenizer.texts_to_sequences(texts)
>>> word_index = tokenizer.word_index
>>> print('Found %s unique tokens.' % len(word_index))
Found 45611 unique tokens.
>>> data = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH)
>>> labels = to_categorical(np.asarray(labels))
>>> print('Shape of data tensor:', data.shape)
>>> print('Shape of label tensor:', labels.shape)
Shape of data tensor: (20000, 1000)
Shape of label tensor: (20000, 6)
>>> # split the data into a training set and a validation set
>>> indices = np.arange(data.shape[0])
>>> np.random.shuffle(indices)
>>> data = data[indices]
>>> labels = labels[indices]
>>> num_validation_samples = int(VALIDATION_SPLIT * data.shape[0])
>>> x_train = data[:-num_validation_samples]
>>> y_train = labels[:-num_validation_samples]
>>> x_val = data[-num_validation_samples:]
>>> y_val = labels[-num_validation_samples:]
现在我们将使用我们的 fastText 嵌入。在这个案例中,我们使用的是预训练嵌入,但你也可以在训练过程中动态地训练自己的嵌入。你可以选择从.vec文件加载,但由于这是 fastText,我们将从 BIN 文件加载。使用 BIN 文件的优势在于,它可以在很大程度上避免词汇表外的情况。我们将使用 fastText 模型并生成一个嵌入矩阵:
>>> print('Preparing embedding matrix.')
>>># load the fasttext model
>>> f = load_model(FT_MODEL)
>>> # prepare embedding matrix
>>> num_words = min(MAX_NUM_WORDS, len(word_index) + 1)
>>> embedding_matrix = np.zeros((num_words, EMBEDDING_DIM))
>>> for word, i in word_index.items():
... if i >= MAX_NUM_WORDS:
... continue
... embedding_vector = f.get_word_vector(word)
... if embedding_vector is not None:
... # words not found in embedding index will be all-zeros.
... embedding_matrix[i] = embedding_vector
我们将其加载到嵌入层。需要注意的是,trainable参数应该设置为False,以防止在训练过程中更新权重,如下所示:
>>> # load pre-trained word embeddings into an Embedding layer
>>> embedding_layer = Embedding(num_words,
... EMBEDDING_DIM,
... weights=[embedding_matrix],
... input_length=MAX_SEQUENCE_LENGTH,
... trainable=False)
现在,我们可以构建一个 1D 卷积网络来应用于我们的 Yelp 分类问题:
>>> # train a 1D convnet with global maxpooling
>>> sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int32')
>>> embedded_sequences = embedding_layer(sequence_input)
>>> x = Conv1D(128, 5, activation='relu')(embedded_sequences)
>>> x = MaxPooling1D(5)(x)
>>> x = Conv1D(128, 5, activation='relu')(x)
>>> x = MaxPooling1D(5)(x)
>>> x = Conv1D(128, 5, activation='relu')(x)
>>> x = GlobalMaxPooling1D()(x)
>>> x = Dense(128, activation='relu')(x)
>>> preds = Dense(6, activation='softmax')(x)
该模型的摘要如下所示:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 1000) 0
_______________________________________________________________
embedding_1 (Embedding) (None, 1000, 300) 6000000 _______________________________________________________________
conv1d_1 (Conv1D) (None, 996, 128) 192128
_______________________________________________________________
max_pooling1d_1 (MaxPooling1 (None, 199, 128) 0 _______________________________________________________________
conv1d_2 (Conv1D) (None, 195, 128) 82048
_______________________________________________________________
max_pooling1d_2 (MaxPooling1 (None, 39, 128) 0 _______________________________________________________________
conv1d_3 (Conv1D) (None, 35, 128) 82048
_______________________________________________________________
global_max_pooling1d_1 (Glob (None, 128) 0
_______________________________________________________________
dense_1 (Dense) (None, 128) 16512
_______________________________________________________________
dense_2 (Dense) (None, 6) 774
=================================================================
Total params: 6,373,510
Trainable params: 373,510
Non-trainable params: 6,000,000
_________________________________________________________________
现在,你可以尝试一些其他的超参数,看看能否提高准确度。
在本节中,你了解了如何将 fastText 词嵌入作为更大 CNN Keras 分类器的一部分。使用类似的方法,你可以在 Keras 中使用 fastText 嵌入与任何受益于词嵌入的神经网络架构。
TensorFlow
TensorFlow 是由 Google 开发的一个计算库。它现在非常流行,许多公司使用它来创建他们的神经网络模型。在你在 Keras 中看到的内容之后,使用 fastText 增强 TensorFlow 模型的逻辑是相同的。
TensorFlow 中的词嵌入
要在 TensorFlow 中创建词嵌入,你需要创建一个嵌入矩阵,其中所有文档列表中的标记都有唯一的 ID,因此每个文档都是这些 ID 的向量。现在,假设你有一个 NumPy 数组叫做word_embedding,它有vocab_size行和embedding_dim列,并且你想要创建一个张量W。以一个具体的例子,“I have a cat.”可以分解为["I", "have", "a", "cat", "." ],对应的word_ids张量的形状将是 5。为了将这些单词 ID 映射到向量中,创建词嵌入变量并使用tf.nn.embedding_lookup函数:
word_embeddings = tf.get_variable(“word_embeddings”,
[vocabulary_size, embedding_size])
embedded_word_ids = tf.nn.embedding_lookup(word_embeddings, word_ids)
之后,embedded_word_ids张量将在我们的示例中具有shape [5, embedding_size],并包含五个单词的嵌入(密集向量)。
为了能够使用带有预训练向量的词嵌入,创建W作为tf.Variable,并通过tf.placeholder()从 NumPy 数组初始化它:
with tf.name_scope("embedding"):
W = tf.Variable(tf.constant(0.0,
shape=[doc_vocab_size,
embedding_dim]),
trainable=False,
name="W")
embedding_placeholder = tf.placeholder(tf.float32,
[doc_vocab_size, embedding_dim])
embedding_init = W.assign(embedding_placeholder)
embedded_chars = tf.nn.embedding_lookup(W,x)
然后,你可以在 TensorFlow 会话中传递实际的嵌入:
sess = tf.Session()
sess.run(embedding_init, feed_dict={embedding_placeholder: embedding})
这样可以避免在图中存储嵌入的副本,但它确实需要足够的内存来同时保留矩阵的两个副本(一个用于 NumPy 数组,一个用于tf.Variable)。你希望在训练过程中保持词嵌入不变,因此如前所述,词嵌入的可训练参数需要设置为False。
RNN 架构
自然语言处理(NLP)一直被认为是 LSTM 和 RNN 类型神经网络架构的一个绝佳应用案例。LSTM 和 RNN 使用顺序处理。NLP 一直被认为是最大应用案例之一,因为任何句子的意义都是基于上下文的。一个单词的意义可以被认为是基于它之前的所有单词来确定的:

现在,当你运行 LSTM 网络时,你需要将单词转换成嵌入层。通常在这种情况下,会使用随机初始化器。但你可能应该能够通过使用 fastText 模型来提高模型的性能。让我们来看看在这种情况下如何使用 fastText 模型。
在这个例子中,由 Facebook 发布的爬取向量被加载到内存中。在你的使用案例中,你可能需要在你的文本语料库上训练一个 fastText 模型并加载该模型。我们在这里使用 VEC 文件来创建嵌入,但你也可以选择从.bin文件加载,如 Keras 示例中所示:
#Load fasttext vectors
filepath_glove = 'crawl-300d-2M.vec'
glove_vocab = []
glove_embd=[]
embedding_dict = {}
with open(filepath_glove) as file:
for index, line in enumerate(file):
values = line.strip().split() # Word and weights separated by space
if index == 0:
glove_vocab_size = int(values[0])
embedding_dim = int(values[1])
else:
row = line.strip().split(' ')
vocab_word = row[0]
glove_vocab.append(vocab_word)
embed_vector = [float(i) for i in row[1:]] # convert to list of float
embedding_dict[vocab_word]=embed_vector
调用你感兴趣的文本块,然后执行正常的清理步骤。类似于 Keras 中的示例,接下来你将需要一个机制,将标记映射到唯一的整数,并能通过整数获取回标记。因此,我们需要创建一个字典和一个反向字典来存储单词:
#Create dictionary and reverse dictionary with word ids
def build_dictionaries(words):
count = collections.Counter(words).most_common() #creates list of word/count pairs;
dictionary = dict()
for word, _ in count:
dictionary[word] = len(dictionary) #len(dictionary) increases each iteration
reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
return dictionary, reverse_dictionary
dictionary, reverse_dictionary = build_dictionaries(training_data)
我们使用 fastText 模型创建的字典来创建嵌入数组:
#Create embedding array
doc_vocab_size = len(dictionary)
dict_as_list = sorted(dictionary.items(), key = lambda x : x[1])
embeddings_tmp=[]
for i in range(doc_vocab_size):
item = dict_as_list[i][0]
if item in glove_vocab:
embeddings_tmp.append(embedding_dict[item])
else:
rand_num = np.random.uniform(low=-0.2, high=0.2,size=embedding_dim)
embeddings_tmp.append(rand_num)
# final embedding array corresponds to dictionary of words in the document
embedding = np.asarray(embeddings_tmp)
# create tree so that we can later search for closest vector to prediction
tree = spatial.KDTree(embedding)
接下来,我们设置 RNN 模型。我们将每次读取三个单词,因此我们的 x 是一个行数不确定、宽度为三列的矩阵。另一个需要注意的输入是 embedding_placeholder,它每个单词有一行,宽度为 300 维,对应输入词向量的维度数。
然后,TensorFlow 的 tf.nn.embedding_lookup() 函数可以用于从矩阵 W 中查找每个来自 x 的输入,从而生成 3D 张量 embedded_chars。接着,可以将其输入到 RNN 中:
# create input placeholders
x = tf.placeholder(tf.int32, [None, n_input])
y = tf.placeholder(tf.float32, [None, embedding_dim])
# RNN output node weights and biases
weights = { 'out': tf.Variable(tf.random_normal([n_hidden, embedding_dim])) }
biases = { 'out': tf.Variable(tf.random_normal([embedding_dim])) }
with tf.name_scope("embedding"):
W = tf.Variable(tf.constant(0.0, shape=[doc_vocab_size, embedding_dim]), trainable=False, name="W")
embedding_placeholder = tf.placeholder(tf.float32, [doc_vocab_size, embedding_dim])
embedding_init = W.assign(embedding_placeholder)
embedded_chars = tf.nn.embedding_lookup(W,x)
# reshape input data
x_unstack = tf.unstack(embedded_chars, n_input, 1)
# create RNN cells
rnn_cell = rnn.MultiRNNCell([rnn.BasicLSTMCell(n_hidden),rnn.BasicLSTMCell(n_hidden)])
outputs, states = rnn.static_rnn(rnn_cell, x_unstack, dtype=tf.float32)
# capture only the last output
pred = tf.matmul(outputs[-1], weights['out']) + biases['out']
既然我们已经有了 RNN,接下来需要弄清楚如何训练它以及可以使用什么样的代价函数。FastText 内部使用 softmax 函数。softmax 函数在这里可能不适合作为代价函数,因为按照定义,softmax 会在比较之前对向量进行归一化。因此,实际的向量可能会以任意的方式变大或变小。也许有必要让最终的向量与训练集中的向量具有相同的大小,从而与预训练向量的大小相同。在这个例子中,重点是 L2 损失:
cost = tf.reduce_mean(tf.nn.l2_loss(pred-y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
chapter6 folder: https://github.com/PacktPublishing/Learn-fastText/blob/master/chapter6/TensorFlow%20rnn.ipynb.
PyTorch
按照与前两个库相同的逻辑,你可以使用 torch.nn.EmbeddingBag 类来注入预训练的嵌入。虽然有一个小缺点。Keras 和 TensorFlow 假设你的张量实际上是作为 NumPy 数组实现的,而在 PyTorch 中并非如此。PyTorch 实现了 torch 张量。通常这不是问题,但这意味着你需要编写自己的文本转换和分词管道。为了避免重写和重新发明轮子,你可以使用 torchtext 库。
torchtext 库
torchtext 是一个出色的库,能够处理你构建 NLP 模型所需的大部分预处理步骤。基本上,可以将 torchtext 看作是以一种松散的方式充当 配置即代码 的工具。因此,理解 torchtext 数据范式是有意义的,学习大约需要三小时,而不是编写定制代码,虽然看起来可能更简单,但会涉及无数的困惑和调试。而且,torchtext 还能构建预构建的模型,包括 fastText。
现在,让我们来看看是如何实现的。
torchtext 中的数据类
我们将首先调用所有必要的库。请注意,你正在调用包含我们使用所需数据类的数据:
from torchtext import data
import spacy
...
我们将使用 spacy 进行分词步骤,torchtext 在这方面有很好的支持。torchtext 提供了对调用和加载 fastText 库的优异支持:
from torchtext.vocab import FastText
vectors = FastText('simple')
这将下载 wiki.simple.bin 模型。如果你提供名称 en,它将下载并加载 wiki.en.bin。如果加载 fr,它将加载 wiki.fr.bin,以此类推。
你可能会从 CSV 文件或文本文件中加载数据。在这种情况下,你需要打开文件,可能在 pandas 中提取相关字段,然后将其保存到单独的文件中。torchtext 无法区分训练集和验证集,因此你可能还需要将这些文件分开:
def clean_str(string):
string = re.sub(r"[^A-Za-z0-9(),!?\'\`]", " ", string)
string = re.sub(r"\'s", " \'s", string)
string = re.sub(r"\'ve", " \'ve", string)
string = re.sub(r"n\'t", " n\'t", string)
string = re.sub(r"\'re", " \'re", string)
string = re.sub(r"\'d", " \'d", string)
string = re.sub(r"\'ll", " \'ll", string)
string = re.sub(r",", " , ", string)
string = re.sub(r"!", " ! ", string)
string = re.sub(r"\(", " \( ", string)
string = re.sub(r"\)", " \) ", string)
string = re.sub(r"\?", " \? ", string)
string = re.sub(r"\s{2,}", " ", string)
return string.strip().lower()
def prepare_csv(df, seed=999):
df['text'] = df['text'].apply(clean_str)
df_train, df_test = train_test_split(df, test_size=0.2)
df_train.to_csv("yelp_tmp/dataset_train.csv", index=False)
df_test.to_csv("yelp_tmp/dataset_val.csv", index=False)
现在你需要定义数据并构建词汇表。你可以使用数据模块来实现这一点。该模块有数据类来定义管道步骤并运行批处理、填充和数字化。首先,你需要使用 data.Fields 定义字段类型。此类定义了可以用来创建所需张量的常见数据类型。你还可以定义一些常见指令来定义张量应如何创建。一旦字段创建完成,你可以调用 TabularDataset 来使用字段中定义的指令创建数据集。常见的指令作为参数传递:
# Define all the types of fields
# pip install spacy for the tokenizer to work (or remove to use default)
TEXT = data.Field(lower=True, include_lengths=True, fix_length=150, tokenize='spacy')
LABEL = data.Field(sequential=True, use_vocab=False)
# we use the index field to re-sort test data after processing
INDEX = data.Field(sequential=False)
train_fields=[
(text_label, TEXT),
(stars_label, LABEL)
]
train_fields=[
(text_label, TEXT),
(stars_label, LABEL)
]
train = data.TabularDataset(
path='yelp_tmp/dataset_train.csv', format='csv', skip_header=True,
fields=train_fields)
test_fields=[
(id_label, INDEX),
(text_label, TEXT),
(stars_label, LABEL)
]
test = data.TabularDataset(
path='yelp_tmp/dataset_val.csv', format='csv', skip_header=True,
fields=test_fields)
-
sequential=True表示该列包含序列。对于标签,我们可能希望保持这种设置,因为示例基本上是一个比较,但在不是这种情况的情况下,可以将其设置为 false。 -
我们在此指定使用 spacy 作为分词器,但你可以指定自定义函数。
-
fix_length将所有序列填充或裁剪到固定长度,这里是 150。 -
lower指定我们将所有英文字母设置为小写。
一旦数据集创建完成,你需要创建词汇表,以便稍后将标记转换为整数。在这里,我们将从之前加载的 fastText 向量构建词汇表:
max_size = 30000
TEXT.build_vocab(train, test, vectors=vectors, max_size=max_size)
INDEX.build_vocab(test)
使用迭代器
现在你可以使用迭代器来遍历数据集。在这种情况下,我们使用 BucketIterator,它的额外优势是将相似长度的示例聚集在一起批处理。这减少了所需的填充量:
train = data.BucketIterator(train, batch_size=32,
sort_key=lambda x: len(x.text),
sort_within_batch=True, repeat=False)
test = data.BucketIterator(test, batch_size=128,
sort_key=lambda x: len(x.text),
sort_within_batch=True, train=False,
repeat=False)
所以,你将能够在这些迭代器上运行简单的 for 循环,并根据批次提供输入。
将所有步骤结合起来
最后,一旦完成所有这些步骤,你可以初始化你的 PyTorch 模型,并需要将预训练的向量作为模型的权重。在示例中,创建了一个 RNN 模型,并且词向量是从之前的字段向量初始化的。这将处理 PyTorch 中的 lookup_table:
model = RNNModel('GRU', ntokens, emsize, nhidden, 6,
nlayers, dropemb=dropemb, droprnn=droprnn,
bidirectional=True)
model.encoder.weight.data.copy_(TEXT.vocab.vectors)
这里展示的代码仅包括你可能不熟悉的部分。完整的代码可以查看仓库中的 pytorch torchtext rnn.ipynb 笔记本。
总结
在本章中,我们探讨了如何将 fastText 词向量集成到线性机器学习模型或在 Keras、TensorFlow 和 PyTorch 中创建的深度学习模型中。你还看到如何将词向量轻松地融入到你可能在业务应用中使用的现有神经网络架构中。如果你是通过随机值初始化嵌入,我强烈建议你尝试使用 fastText 值进行初始化,然后观察模型的性能是否有所提升。
第十章:部署模型到 Web 和移动端
对于依赖机器学习的公司来说,以可扩展的方式部署模型非常重要。模型应该以创建时的方式运行。无论是监督学习还是无监督学习,fastText 模型的部署可以通过多种方式实现。选择方法将取决于你个人的需求。
在本章中,我们将重点讨论如何在 Web 和移动场景中部署 fastText 模型。涉及的主题包括:
-
部署到 Web
-
Flask
-
FastText 函数
-
Flask 端点
-
部署到更小的设备
-
前提条件 – 完成 Google 教程
-
应用考虑事项
-
添加 fastText 模型
-
Java 中的 FastText
-
向 Android 添加库依赖
-
在 Android 中使用库依赖
-
最终,应用
部署到 Web
现在你已经掌握了创建自己 fastText 模型的方法,你可能需要将它们部署到生产环境,以便可以利用这些模型来创建应用和端点。Python 中有许多框架可以用来创建这样的 Web 应用。Flask、Django 和 Pyramid 是一些流行的 Python Web 框架。在本节中,我们将以 Flask 为例,构建一个简单的 Web 最近邻搜索应用。
Flask
Flask 是一个流行的 Web 框架,被归类为微框架,因为它不需要任何外部工具或库。Flask 内部没有数据库抽象层、表单验证或其他组件。其优点是,你可以用最少的代码行构建一个简单的 Web 应用。这有助于快速原型设计,并让你专注于应用本身的代码。
对于本节讨论的代码,可以查看仓库中的 chapter 7 文件夹。你将找到两个文件,ft_server.py 和 ft_utils.py。ft_utils.py 模块包含与 fastText 服务器相关的代码,而 ft_server.py 包含与 Flask 端点相关的代码。
fastText 函数
如果你查看代码,我们是通过 FT_MODEL 环境变量加载 fastText 模块的。这个模型被作为全局变量加载,以便可以在函数中使用。另一个好处是,当 Flask 应用初始化时,模型也会被加载到内存中。将模型加载到内存中是一个计算密集型操作,因此如果我们将此操作延迟到初始化阶段,将能提高响应时间:
print('loading the model')
FT_MODEL = os.environ.get('FT_MODEL')
if not FT_MODEL:
raise ValueError('No fasttext model has been linked.')
FT_MODEL = fastText.load_model(FT_MODEL)
print('model is loaded')
现在,我们还将基于一个阈值获取词汇表中最常见的词汇,并将其保存在内存中。我们将词向量作为全局变量保存,这样词向量的计算就会在应用初始化时完成,类似于之前的方式:
# Gets words with associated frequency sorted by default by descending order
words, freq = FT_MODEL.get_words(include_freq=True)
words = words[:threshold]
vectors = np.zeros((len(words), FT_MODEL.get_dimension()), dtype=float)
for i in range(len(words)):
wv = FT_MODEL.get_word_vector(words[i])
wv = wv / np.linalg.norm(wv)
vectors[i] = wv
# For efficiency preallocate the memory to calculate cosine similarities
cossims = np.zeros(len(words), dtype=float)
接下来的两个函数基本上是如何根据问题词的词向量与其他词的词向量之间的距离来获取最接近的词。这些函数在第五章,Python 中的 FastText中也有讨论。
你应该能够单独运行这个模块。同时,注意运行整个模块所需的时间。
在我的笔记本上,运行整个代码大约需要 10 秒钟。
Flask 端点
为了简洁起见,本文中只讨论了一个端点。基本上,它用于获取传入的问题词,使用 ft_utils.py 文件中定义的 nn 函数获取答案,然后以 JSON 格式提供答案:
@app.route('/nn/<question_word>')
def nearest_neighbours(question_word):
answers = [a for a in nn(FT_MODEL, question_word, k=5)]
return jsonify(dict(question=question_word, answers=answers))
现在,以开发模式运行应用,这样你就可以调试它:
$ export FLASK_APP=ft_server.py
$ export FLASK_ENV=development
$ export FT_MODEL=wiki.simple.bin
$ flask run
打开一个新的终端并发送一个 curl 请求;你应该能在终端看到响应。你可以看到响应也很快:
$ time curl http://127.0.0.1:5000/nn/dog
{
"answers": [
"dogs",
"catdog",
"sheepdog",
"sheepdogs",
"breed"
],
"question": "dog"
}
$ curl http://127.0.0.1:5000/nn/dog 0.01s user 0.00s system 9% cpu 0.105 total
我们能够在 Flask 应用中获得非常快速的响应,因为我们尽可能将计算开销大的部分代码移至应用初始化阶段。这通常是一个不错的做法。作为 Web 应用的一部分,只做绝对必要的事情,并将请求处理过程中的计算量保持在最低限度。这将确保你构建出高效且实用的 Web 应用,适合部署 fastText 模型和机器学习应用。
部署到较小的设备
正如你在第二章中看到的,使用 FastText 命令行创建模型,你可以使用类似下面的命令从完整模型创建一个压缩的 fastText 模型:
$ ./fasttext quantize -output <model prefix> -input <training file> -qnorm -retrain -epoch <number of epochs> -cutoff <number of words to consider>
在第四章,FastText 中的句子分类,我们也重新讨论了压缩模型的概念,以及如何在性能几乎不丢失的情况下实现压缩。
这使得你也可以在较小的设备上部署机器。首先想到的一个问题是,文件是否可以与 Android 应用打包并部署在 Android 应用中。
在本节中,我将展示所有应该能帮助你部署 Android fastText 应用所需的要求和依赖项。
前提条件 – 完成 Google 教程
一个值得参考的最佳示例是 Google 教程中的 Android 应用示例。如果你是 Android 新手,请访问developer.android.com/training/basics/firstapp/并完成那里的教程。我们在这里不再详细介绍,所以总结起来,步骤是:
-
安装 Android Studio
-
创建一些简单的用户界面
-
创建活动并定义意图
-
构建并创建 APK 文件
对于我们的应用程序,我们将沿着类似的思路进行。我们项目的名称是 Fasttext application。因此,下载最新版本的 Android Studio 并启动它:

继续点击“Next”,选择空活动并点击“Finish”。你应该会进入一个新的项目窗口,其中已经为你完成了大量 Android 的模板代码。现在,打开 Google 的教程并按照教程中的所有步骤进行操作。如果你已经是经验丰富的 Android 开发者,你可以从 GitHub 仓库中打开项目。首先,执行 git fetch 并切换到 android_starting 分支:
git fetch
git checkout android_starting
现在,如果你编译应用并创建 APK,你应该能够看到以下界面。或者,如果你已经设置了 ADB,你可以选择运行并在模拟器中查看应用。
要构建 APK,你可以在 Android Studio 中点击“Build APK(s)”按钮:

请按照 Google Android 教程中“构建简单用户界面”部分的步骤操作,以便最终运行一个类似于这个的简单应用:

现在,按照“开始另一个活动”教程进行操作,完成另一个活动的创建。这个教程的目的是让你能够从第一个活动触发另一个单独的活动:
=> 
应用程序考虑事项
现在你已经有了两个活动,你可能已经猜到了我们 fastText 应用的目标。我们将在第一个活动中输入文本标签,并且它应该在第二个活动中给出标签。
要实现这一点,你需要做两件事:
-
将预构建的 fastText 模型添加到 Android 应用中
-
添加一个库来解析 fastText 模型并提供预测结果
有几点我想在这里提一下。我们将在这里使用 fastText FTZ 模型。你可以争辩说,正如我们所见,fastText 模型是通过产品量化方法构建的,使用了像最近邻中心估计、词汇修剪和哈希等方法。这会导致信息丢失,尽管有争议称这种丢失对性能没有显著影响。但如果你不信服,你可以选择按照前面章节讨论的那样,创建一个 Web 应用并通过 Android 应用访问结果,这意味着将 Android 应用作为只读视图,所有计算都由服务器完成。这是一个合理的方案,但更多的是一个工程挑战,并不在本书的范围内。在本节中,我们更感兴趣的是能否利用 fastText 的能力创建非常小的模型,将其部署到移动设备,并在我们的应用程序中使用它们。
现在,在继续之前,先构建 APK 并注意当前应用程序的大小,因为大小对于用户来说是一个重要问题。如果你希望用户下载并使用应用程序,大小问题不容忽视。你肯定不希望因为内存已满而让用户卸载应用程序,或者用户处在互联网非常慢的地方,下载应用程序会非常耗时。因此,在为小型设备设计应用时,始终要记得考虑应用程序的大小。
目前,构建仅占用 1.5 MB 的内存:
$ du -sh app-debug.apk
1.6M app-debug.apk
添加 fastText 模型
接下来,你需要将 fastText 模型添加到应用程序中。Android 有一个资产管理系统,可以用来实现这一点。
首先,下载或创建一个 FTZ 模型。我从 fastText 网站的监督模型部分下载了dbpedia.ftz文件。该文件大约 1.6 MB,因此不应该增加最终构建的 APK 大小。
Android 有一个资源管理系统,你可以用它来实现这一目的。在主文件夹下创建一个 assets 文件夹,并将你的 FTZ 文件复制/粘贴到其中:

你应该能够看到文件,如下所示:

完成后,添加代码以使用资源管理器访问文件。由于我们在第二个活动中进行预测,让我们在DisplayMessageActivity.java文件中访问该文件。
在onCreate方法中,创建一个AssetManager实例以访问资产文件,并创建一个InputStream实例,以便文件可以转换为流。数据将从这个流中读取和操作,代码如下:
public class DisplayMessageActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_display_message);
// Get the Intent that started this activity and extract the string
Intent intent = getIntent();
String message = intent.getStringExtra(MainActivity.EXTRA_MESSAGE);
// Get the assets from the asset manager.
AssetManager assetManager = getAssets();
InputStream inputStream = null;
try {
inputStream = assetManager.open("dbpedia.ftz");
} catch (IOException e) {
e.printStackTrace();
}
// Capture the layout's TextView and set the string as its text
TextView textView = findViewById(R.id.textView);
textView.setText(message);
}
按下Alt + Enter(在 Mac 上是Option + Enter)来导入缺失的类。你的导入应该如下所示:
import android.content.Intent;
import android.content.res.AssetManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.io.IOException;
import java.io.InputStream;
Java 中的 FastText
现在你已经在 Android 应用中部署了 fastText 模型,你需要一种方法来访问文件并提供预测。你可以通过编写 Java 自定义代码来实现这一点,参考 fastText 的 GitHub 库。这样做的优势在于你对代码有更多的控制,且如果你在编写企业级应用时,可能是唯一的选择,因为 fastText 的代码是 BSD 许可下发布的。另一种选择是将 fastText 代码编译成 Android 库的一部分,但服务原生代码存在很多问题,且可能在所有设备上无法正常工作。对我来说,添加外部依赖的最佳方法是找到能够为你完成工作的核心 Java 库。
幸运的是,在这种情况下,我们有一个符合要求的库。fastText4j是一个由linkfluence在 GitHub 上发布的优秀 Java 库,包含所有 Java 代码,并且能够加载和解析 Java 库。除了能够读取 bin 和 FTZ 文件,我们还能够读取和创建 Java 内存映射文件,尽管在这里我们不会使用它。使用以下仓库克隆这个库,它是原始linkfluence库的克隆:
$ git clone https://github.com/infinite-Joy/fastText4j.git
$ cd fastText4j
$ mvn clean
还需要将包编译为应用程序,因为我们将需要它进行测试和编译。解压生成的文件:
$ mvn install -Papp
$ unzip app/fasttext4j-app.zip
$ cp target/fasttext4j-0.2.1-SNAPSHOT.jar lib/fasttext4j-0.2.1-SNAPSHOT.jar
这将把lib/文件夹和fasttext-mmap.sh文件复制到当前目录。最后的复制步骤现在并非必须,但这样做是为了向你展示,当你对这个仓库做出更改并重新编译 JAR 时,这一步是需要的。现在,文件将包含稍有不同的命令行。修改src/main/java/fasttext/FastText.java中的main方法:
public static void main(String[] args) throws Exception {
Options options = new Options();
Option input = new Option("i", "input", true, "input model path");
input.setRequired(true);
options.addOption(input);
CommandLineParser parser = new DefaultParser();
HelpFormatter formatter = new HelpFormatter();
CommandLine cmd;
try {
cmd = parser.parse(options, args);
} catch (ParseException e) {
System.out.println(e.getMessage());
formatter.printHelp("fasttext.FastText", options);
System.exit(1);
return;
}
String inputModelPath = cmd.getOptionValue("input");
logger.info("Loading fastText model to convert...");
FastText model = FastText.loadModel(inputModelPath);
FastTextPrediction label = model.predict("Good restaurant");
System.out.println(label.label());
}
输出参数已被移除,我们现在将模型作为输入并获取参数。编译此代码并复制我们下载的 FTZ 模型路径。现在我们来测试这个库:
$ mvn clean package
$ cp target/fasttext4j-0.2.1-SNAPSHOT.jar lib/fasttext4j-0.2.1-SNAPSHOT.jar
$ time bash fasttext-mmap.sh -i <path to>/dbpedia.ftz
__label__7
bash fasttext-mmap.sh -i 0.64s user 0.11s system 168% cpu 0.444 total
输出命令将会有大量日志。我们现在不会展示这些日志。只需检查日志中是否有任何错误消息,可能会标记缺少的依赖库。此外,正如你所见,在我的本地机器上加载并提供 FTZ 文件的预测非常快。假设在低性能的 Android 应用中,它也应该表现得很高效。
既然我们已经确认库可以工作并且能够给出预测,移除main方法,因为我们在 Android 应用中不需要它。编译 JAR 并将其放置在lib文件夹中:
$ mvn clean package
$ mvn install -Papp
$ unzip app/fasttext4j-app.zip
$ cp target/fasttext4j-0.2.1-SNAPSHOT.jar lib/fasttext4j-0.2.1-SNAPSHOT.jar
将库依赖添加到 Android 中
检查lib文件夹。所有作为此项目依赖的库都会放在这里。如果我们希望在 Android 应用中使用这个库,必须将这些依赖添加到应用中。
打开 文件 | 新建 | 新建模块...:

导入 JAR/AAR 包:

现在,将lib文件夹中的所有库添加为依赖项。现在,项目结构应该列出这些库作为依赖项。然后,点击应用 | 依赖项,将它们也作为应用的依赖项添加。将库文件作为模块依赖添加:

现在依赖已经添加,我们可以开始在我们的活动中使用这个库了。
在 Android 中使用库依赖
要在 Android 中使用这个库,修改DisplayMessageActivity.java并编写你在库测试阶段看到的相同代码。
在编译之前,在gradle.build文件中添加guava依赖,因为guava库中的UnsignedLong依赖:
compile 'com.google.guava:guava:21.0'
同时,添加编译版本,以便能够编译 Java 代码:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
...
}
buildTypes {
...
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
...
compile 'com.google.guava:guava:21.0'
}
最终的应用
现在,编译应用并在你的手机上运行。你应该能够看到这些变化:
=> 
让我们也来看一下创建的 APK 文件。在我的机器上,应用的大小已变为 4.6 MB。够小了吗?我将把这个问题留给你自己判断:
$du -sh app-debug.apk
4.6M app-debug.apk
总结
在本章中,我们研究了如何在网页和移动环境中实现 fastText 的方法,特别是针对 Android 平台。我们还探讨了在部署到网页或移动设备时需要考虑的不同因素。这将帮助你创建出色的应用,并将 fastText 集成到你的移动应用中。
第十一章:给读者的注意事项
Windows 和 Linux
我们建议您使用 PowerShell 作为 Windows 命令行工具,因为它比简单的 cmd 更强大。
| 任务 | Windows | Linux/macOS |
|---|---|---|
| 创建目录 | mkdir |
mkdir |
| 切换目录 | cd |
cd |
| 移动文件 | move |
mv |
| 解压文件 | 图形界面并双击 | unzip |
| 文件顶部 | get-content |
head |
| 文件内容 | type |
cat |
| 管道 | this pipes objects |
this pipes text |
| 文件底部 | 带有 get-content 的 -wait 参数 |
tail |
python 和 perl 命令在 Windows 中的工作方式与在 bash 中相同,因此您可以以类似方式使用这些文件,特别是 perl 单行命令。
Python 2 和 Python 3
fastText 适用于 Python 2 和 Python 3。尽管如此,您应该注意特定 Python 版本之间的一些差异。
-
print在 Python 2 中是一个语句,而在 Python 3 中是一个函数。这意味着,如果您在 Jupyter Notebook 中查看变量的变化,您需要根据对应的 Python 版本使用适当的print语句。 -
fastText 处理文本时使用 Unicode 编码。Python 3 也处理文本为 Unicode,因此如果您使用 Python 3 编写代码,不会增加额外的开销。但如果您使用 Python 2 开发模型,数据不能是字符串实例。您需要将数据作为 Unicode 处理。以下是一个在 Python 2 中,
str类和unicode类的文本实例示例。
>>> text1 = "some text" # this will not work for fastText
>>> type(text1)
<type 'str'>
>>> text2 = unicode("some text") # in fastText you will need to use this.
>>> type(text2)
<type 'unicode'>
>>>
fastText 命令行
以下是您可以与 fastText 命令行一起使用的参数列表:
$ ./fasttext
usage: fasttext <command> <args>
The commands supported by fasttext are:
supervised train a supervised classifier
quantize quantize a model to reduce the memory usage
test evaluate a supervised classifier
predict predict most likely labels
predict-prob predict most likely labels with probabilities
skipgram train a skipgram model
cbow train a cbow model
print-word-vectors print word vectors given a trained model
print-sentence-vectors print sentence vectors given a trained model
print-ngrams print ngrams given a trained model and word
nn query for nearest neighbors
analogies query for analogies
dump dump arguments,dictionary,input/output vectors
supervised、skipgram 和 cbow 命令用于训练模型。predict、predict-prob 用于在有监督模型上进行预测。test、print-word-vectors、print-sentence-vectors、print-ngrams、nn、analogies 可用于评估模型。dump 命令基本上是用来查找模型的超参数,quantize 用于压缩模型。
您可以用于训练的超参数列表稍后会列出。
fastText 有监督
$ ./fasttext supervised
Empty input or output path.
The following arguments are mandatory:
-input training file path
-output output file path
The following arguments are optional:
-verbose verbosity level [2]
The following arguments for the dictionary are optional:
-minCount minimal number of word occurences [1]
-minCountLabel minimal number of label occurences [0]
-wordNgrams max length of word ngram [1]
-bucket number of buckets [2000000]
-minn min length of char ngram [0]
-maxn max length of char ngram [0]
-t sampling threshold [0.0001]
-label labels prefix [__label__]
The following arguments for training are optional:
-lr learning rate [0.1]
-lrUpdateRate change the rate of updates for the learning rate [100]
-dim size of word vectors [100]
-ws size of the context window [5]
-epoch number of epochs [5]
-neg number of negatives sampled [5]
-loss loss function {ns, hs, softmax} [softmax]
-thread number of threads [12]
-pretrainedVectors pretrained word vectors for supervised learning []
-saveOutput whether output params should be saved [false]
The following arguments for quantization are optional:
-cutoff number of words and ngrams to retain [0]
-retrain whether embeddings are finetuned if a cutoff is applied [false]
-qnorm whether the norm is quantized separately [false]
-qout whether the classifier is quantized [false]
-dsub size of each sub-vector [2]
fastText skipgram
$ ./fasttext skipgram
Empty input or output path.
The following arguments are mandatory:
-input training file path
-output output file path
The following arguments are optional:
-verbose verbosity level [2]
The following arguments for the dictionary are optional:
-minCount minimal number of word occurences [5]
-minCountLabel minimal number of label occurences [0]
-wordNgrams max length of word ngram [1]
-bucket number of buckets [2000000]
-minn min length of char ngram [3]
-maxn max length of char ngram [6]
-t sampling threshold [0.0001]
-label labels prefix [__label__]
The following arguments for training are optional:
-lr learning rate [0.05]
-lrUpdateRate change the rate of updates for the learning rate [100]
-dim size of word vectors [100]
-ws size of the context window [5]
-epoch number of epochs [5]
-neg number of negatives sampled [5]
-loss loss function {ns, hs, softmax} [ns]
-thread number of threads [12]
-pretrainedVectors pretrained word vectors for supervised learning []
-saveOutput whether output params should be saved [false]
The following arguments for quantization are optional:
-cutoff number of words and ngrams to retain [0]
-retrain whether embeddings are finetuned if a cutoff is applied [false]
-qnorm whether the norm is quantized separately [false]
-qout whether the classifier is quantized [false]
-dsub size of each sub-vector [2]
fastText cbow
$ ./fasttext cbow
Empty input or output path.
The following arguments are mandatory:
-input training file path
-output output file path
The following arguments are optional:
-verbose verbosity level [2]
The following arguments for the dictionary are optional:
-minCount minimal number of word occurences [5]
-minCountLabel minimal number of label occurences [0]
-wordNgrams max length of word ngram [1]
-bucket number of buckets [2000000]
-minn min length of char ngram [3]
-maxn max length of char ngram [6]
-t sampling threshold [0.0001]
-label labels prefix [__label__]
The following arguments for training are optional:
-lr learning rate [0.05]
-lrUpdateRate change the rate of updates for the learning rate [100]
-dim size of word vectors [100]
-ws size of the context window [5]
-epoch number of epochs [5]
-neg number of negatives sampled [5]
-loss loss function {ns, hs, softmax} [ns]
-thread number of threads [12]
-pretrainedVectors pretrained word vectors for supervised learning []
-saveOutput whether output params should be saved [false]
The following arguments for quantization are optional:
-cutoff number of words and ngrams to retain [0]
-retrain whether embeddings are finetuned if a cutoff is applied [false]
-qnorm whether the norm is quantized separately [false]
-qout whether the classifier is quantized [false]
-dsub size of each sub-vector [2]
Gensim fastText 参数
Gensim 支持与 fastText 本地实现相同的超参数。您应该能够按如下方式设置它们:
-
sentences:这可以是一个包含标记的列表的列表。一般来说,建议使用标记流,如之前提到的 word2vec 模块中的LineSentence。在 Facebook fastText 库中,这由文件路径提供,并通过-input参数传递。 -
sg:可以是 1 或 0。1 表示训练 skip-gram 模型,0 表示训练 CBOW 模型。在 Facebook fastText 库中,等效操作是传递skipgram和cbow参数。 -
size:词向量的维度,因此必须是整数。与原始实现一致,默认选择 100。这与 Facebook fastText 实现中的-dim参数类似。 -
window:围绕一个词语考虑的窗口大小。这与原始实现中的-ws参数相同。 -
alpha:这是初始学习率,类型为浮动数。它与第二章中看到的-lr参数相同,使用 FastText 命令行创建模型。 -
min_alpha:这是训练过程中学习率降至的最小值。 -
seed:这是为了可复现性。为了让种子生效,线程数也需要设置为 1。 -
min_count:文档中单词的最小频率,低于此频率的单词将被丢弃。类似于命令行中的-minCount参数。 -
max_vocab_size:用于限制 RAM 大小。如果词汇表中有更多的唯一单词,那么会修剪掉频率较低的单词。这个值需要根据你拥有的 RAM 大小来决定。例如,如果你有 2GB 内存,则max_vocab_size需要为 10M * 2 = 2000 万(20 000 000)。 -
sample:用于对单词进行下采样。类似于 fasttext 命令行中的"-t"参数。 -
workers:训练的线程数,类似于 fastText 命令中的-thread参数。 -
hs:可以是 0 或 1。如果是 1,则会使用层次化 softmax 作为损失函数。 -
negative:如果你想使用负采样作为损失函数,则将hs设置为 0,并将 negative 设为非零正数。请注意,损失函数仅支持两种功能:层次化 softmax 和负采样。简单的 softmax 不被支持。这个参数和hs一起,等同于fasttext命令中的-loss参数。 -
cbow_mean:这里与 fastText 命令有些不同。在原始实现中,对于cbow会取向量的均值。但在这种情况下,你可以选择通过传递 0 来使用和 1 来尝试均值。 -
hashfxn:用于随机初始化权重的哈希函数。 -
iter:样本的迭代次数或周期数。这与命令行中的-epoch参数相同。 -
trim_rule:用于指定是否应保留某些词汇或将其修剪掉的函数。 -
sorted_vocab:接受的值为 1 或 0。如果为 1,则词汇表将在索引之前进行排序。 -
batch_words:这是传递的批次的目标大小。默认值为 10000。这与命令行中的-lrUpdateRate有些相似,因为批次数决定了权重何时更新。 -
min_n和max_n:字符 n-grams 的最小和最大长度。 -
word_ngrams:丰富子词信息,以便在训练过程中使用。 -
bucket:字符 n-gram 被哈希到一个固定大小的向量上。默认情况下使用 200 万词的桶大小。
-
callbacks:在训练过程中特定阶段执行的回调函数列表。
第十二章:参考文献
第三章
-
独热编码:
machinelearningmastery.com/how-to-one-hot-encode-sequence-data-in-python/ -
表示学习:
github.com/anujgupta82/Representation-Learning-for-NLP -
N-gram 模型:
citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.53.9367 -
TF-IDF:
nlp.stanford.edu/IR-book/html/htmledition/inverse-document-frequency-1.html -
Mikolov 等 2013:
arxiv.org/abs/1310.4546 -
Maas 和 cgpotts 论文:
web.stanford.edu/~cgpotts/papers/wvSent_acl2011.pdf -
scikit-learn 中的词袋模型:
scikit-learn.org/stable/modules/feature_extraction.html#the-bag-of-words-representation -
Kaggle 上的 word2vec
www.kaggle.com/c/word2vec-nlp-tutorial -
Heap 法则:
en.wikipedia.org/wiki/Heaps%27_law -
句子和文档的分布式表示,Mikolov 等:
cs.stanford.edu/~quocle/paragraph_vector.pdf -
Skip-gram: McCormick, C. (2016, April 19), Word2Vec 教程 - Skip-Gram 模型
mccormickml.com/2016/04/19/word2vec-tutorial-the-skip-gram-model/ -
TensorFlow 中的 word2vec 实现:
github.com/tensorflow/tensorflow/blob/r1.1/tensorflow/examples/tutorials/word2vec/word2vec_basic.py -
Word2vec 详解:
arxiv.org/abs/1411.2738 -
推导负采样:
arxiv.org/abs/1402.3722 -
组合分布式语义学:
youtu.be/hTmKoHJw3Mg -
fastText 和 skipgram:
debajyotidatta.github.io/nlp/deep/learning/word-embeddings/2016/09/28/fast-text-and-skip-gram/ -
skip-gram 和 CBOW:
iksinc.online/tag/continuous-bag-of-words-cbow/ -
斯坦福大学关于 CBOW 和 skip-gram 的讲座:
cs224d.stanford.edu/lecture_notes/notes1.pdf -
fasttext PyTorch:
github.com/PetrochukM/PyTorch-NLP -
Levy,Omer 和 Goldberg Yoav(2014),基于依赖关系的词嵌入,第 52 届计算语言学协会年会,ACL 2014——会议论文集,2. 302-308. 10.3115/v1/P14-2050
-
噪声对比估计与负采样的笔记:
arxiv.org/abs/1410.8251 -
Sebastian Ruder,关于词嵌入 - 第二部分:逼近 Softmax,
ruder.io/word-embeddings-softmax,2016。 -
可扩展的层次分布式语言模型。
papers.nips.cc/paper/3583-a-scalable-hierarchical-distributed-language-model.pdf -
Softmax 函数及其导数。
eli.thegreenplace.net/2016/the-softmax-function-and-its-derivative/ -
什么是 Softmax 回归,它与逻辑回归有什么关系?,Sebastian Raschka。
www.kdnuggets.com/2016/07/softmax-regression-related-logistic-regression.html -
Softmax 回归,
ufldl.stanford.edu/tutorial/supervised/SoftmaxRegression/ -
Google Allo:
research.googleblog.com/2016/05/chat-smarter-with-allo.html -
层次化概率神经网络语言模型,Morin 和 Bengio,2005,
www.iro.umontreal.ca/~lisa/pointeurs/hierarchical-nnlm-aistats05.pdf -
可扩展的层次分布式语言模型。Mnih,Andriy 和 Hinton,Geoffrey E. 2009,
papers.nips.cc/paper/3583-a-scalable-hierarchical-distributed-language-model -
自组织层次 Softmax,arXiv:1707.08588v1 [cs.CL] 2017 年 7 月 26 日
-
基于哈夫曼编码算法的有效文本聚类方法, Nikhil Pawar, 2012 年,
www.ijsr.net/archive/v3i12/U1VCMTQ1NjE=.pdf -
Tomas Mikolov, Ilya Sutskever, Kai Chen, Gregory S. Corrado 和 Jeffrey Dean, 词和短语的分布式表示及其组合性, 载于《神经信息处理系统进展》第 26 卷:第 27 届神经信息处理系统年会,2013 年,会议论文集,2013 年 12 月 5-8 日,美国内华达州湖塔霍,页 3111-3119。
-
debajyotidatta.github.io/nlp/deep/learning/word-embeddings/2016/09/28/fast-text-and-skip-gram/ -
github.com/nzw0301/keras-examples/blob/master/Skip-gram-with-NS.ipynb
第四章
-
Vladimir Zolotov 和 David Kung 2017 年,fastText 线性文本分类器的分析与优化,
arxiv.org/abs/1702.05531 -
线性模型的文本分类,
www.cs.umd.edu/class/fall2017/cmsc723/slides/slides_03.pdf -
什么是文本分类,斯坦福,
nlp.stanford.edu/IR-book/html/htmledition/the-text-classification-problem-1.html#sec:classificationproblem -
nlp.stanford.edu/IR-book/html/htmledition/text-classification-and-naive-bayes-1.html -
Joseph Turian, Lev Ratinov, 和 Yoshua Bengio, 2010 年,词表示:一种简单而通用的半监督学习方法, 载于 第 48 届计算语言学协会年会论文集 (ACL '10), 计算语言学协会,斯特劳兹堡,美国,384-394。
-
[Weinberger 等, 2009] Kilian Weinberger, Anirban Dasgupta, John Langford, Alex Smola 和 Josh Attenberg, 2009 年, 特征哈希用于大规模多任务学习. 载于 ICML 会议
-
developers.googleblog.com/2018/04/text-embedding-models-contain-bias.html -
PyTorch 中的 Softmax 分类器:
www.youtube.com/watch?v=lvNdl7yg4Pg -
分类的层次损失:arXiv:1709.01062v1 [cs.LG],2017 年 9 月 1 日
-
Svenstrup,Dan & Meinertz Hansen,Jonas & Winther,Ole. (2017),哈希嵌入用于高效的词表示
-
极快的文本特征提取用于分类和索引,乔治·福尔曼和埃文·基尔申鲍姆著
-
阿曼德·朱林, 等人. FastText.zip: 压缩文本分类模型,2016,
arxiv.org/abs/1612.03651 -
shodhganga.inflibnet.ac.in/bitstream/10603/132782/14/12_chapter%204.pdf -
基于 Voronoi 投影的快速最近邻搜索算法:盒子搜索和映射表搜索技术,V. 拉马苏布拉马尼安,K.K. 皮利瓦尔。1997 年,
www.sciencedirect.com/science/article/pii/S1051200497903006 -
如何用 Python 从零实现学习向量量化。杰森·布朗尼,2016,
machinelearningmastery.com/implement-learning-vector-quantization-scratch-python/ -
赫尔维·热戈、马蒂斯·杜兹和科尔德莉亚·施密德,用于最近邻搜索的产品量化,IEEE Trans. PAMI,2011 年 1 月。
-
韩松、毛会子和威廉·J·达利。深度压缩:通过修剪、训练量化和霍夫曼编码压缩深度神经网络,在 ICLR 中,2016 年
-
孙捷、何凯明、柯启发和孙剑,优化的产品量化用于近似最近邻搜索。 在 CVPR 中,2013 年 6 月
-
期望最大化,乔伊迪普·巴塔查尔吉。
medium.com/technology-nineleaps/expectation-maximization-4bb203841757 -
陈文林、詹姆斯·T·威尔逊、史蒂芬·泰瑞、基利安·Q·温伯格和陈逸欣,利用哈希技巧压缩神经网络, arXiv:1504.04788, 2015
-
Kai Zeng, Kun She, 和 Xinzheng Niu, 基于邻域熵的合作博弈理论特征选择, 计算智能与神经科学, vol. 2014, 文章 ID 479289, 10 页, 2014,
doi.org/10.1155/2014/479289.
第五章
-
大规模语料主题建模软件框架, Radim, 2010
-
Gensim fastText 教程:
github.com/RaRe-Technologies/gensim/blob/develop/docs/notebooks/FastText_Tutorial.ipynb -
P. Bojanowski, E. Grave, A. Joulin, T. Mikolov, 用子词信息丰富词向量,
arxiv.org/abs/1607.04606 -
Tomas Mikolov, Quoc V Le, Ilya Sutskever, 2013, (利用语言间相似性进行机器翻译) (
arxiv.org/pdf/1309.4168.pdf) -
Georgiana Dinu, Angelikie Lazaridou, 和 Marco Baroni. 2014, 通过缓解中心性问题改善零-shot 学习 (
arxiv.org/pdf/1412.6568.pdf) -
fastText 标准化,
www.kaggle.com/mschumacher/using-fasttext-models-for-robust-embeddings/notebook -
Luong, Minh-Thang 和 Socher, Richard 和 Manning, Christopher D. 2013, (通过递归神经网络改善形态学词表示) (
nlp.stanford.edu/~lmthang/morphoNLM/)
第六章
-
Yoav Goldberg (2015), 神经网络模型入门:自然语言处理
语言处理, (
arxiv.org/abs/1510.00726) -
www.wildml.com/2015/11/understanding-convolutional-neural-networksfor-nlp/ -
machinelearningmastery.com/best-practices-document-classification-deep-learning/ -
pytorch.org/tutorials/beginner/nlp/word_embeddings_tutorial.html
第七章


浙公网安备 33010602011771号