自然语言处理指南(第1部分)
自然语言处理(NLP)包含一系列技术,用以实现诸多不同的目标。下表中列出了解决某些特定问题对应的技术。
你想要 |
你要看 |
---|---|
将类似的词分组以搜索 |
词干提取;分词;文档分析 |
查找具有相似含义的词语以搜索 |
潜在语义分析 |
生成名称 |
词汇拆分 |
估计阅读文本需要多长时间 |
阅读时间 |
估计一段文本阅读的难度 |
文本可读性 |
识别文本的语言 |
语言识别 |
生成文本摘要 |
SumBasic(基于词);基于图的算法:TextRank(基于关系);潜在语义分析(基于语义) |
查找类似文件 |
潜在语义分析 |
识别文本中的实体(即城市,人物) |
分档分析 |
推测文本表达的态度 |
文档分析 |
翻译一段文本 |
文档分析 |
我们将按一般意义上的“分析文档”和“提取文档意义”来讨论文档分析(而非句法或语法分析,因为二者英文均为 parsing ——译者注)。 因此,我们会涉及实际的自然语言句法分析,但把更多的时间花在其他技术上。就编程语言理解而言,语法分析方为可行之道,不过也有某些特定的自然语言可供选择。换言之,我们讨论的大都是“你将使用什么技术”而不是“进行句法分析以完成目标”。
例如,如果你想在一个编程语言文件中找到所有的for
语句,你能通过语法分析计算for
s 的个数;而在自然语言文件种,你可能会使用类似于词干提取的技术来找到所有提到的“猫”。
这很重要,因为自然语言句法分析背后的理论可能与编程语言语法分析背后的理论是一致的; 然而,其实际操作又是非常不同的。事实上,你不会为自然语言构建一个语法分析器(Parser)——也就是说,除非你在使用人工智能或是一个研究人员,甚至就算在这种情况下,你也很少使用语法分析器。正相反,你会找到一个算法,作为文档的简化模型,只用以解决你的特定问题。
总之,你是寻找一些技巧使你不必真的去解析一种自然语言。这就是为什么在计算机科学领域,我们通常称“自然语言处理”而非“自然语言解析”。
需要数据的算法
我们将了解每个问题的具体解决方案。请注意,这些具体的解决方案本身可能相当复杂。他们越高级,就越不依赖于简单的算法。通常情况下,他们需要一个该语言的庞大数据库。而这样做的合乎逻辑的结果是,该工具很难移植于另一种语言。或者说,这个工具具有一定的可移植性,但是建立数据库需要大量的投入。就比如,你很可能找到一个可用的用以创建英文文本摘要的工具,但不能创建意大利文的摘要。
因此,在这一系列指南中,我们主要关注英文工具。尽管我们会提到这些工具是否适用于其他语言,但你不需要知道语言之间的理论差异,例如性、数、格的数量。不要,你要知道,一种语言与英语差异越大,应用这些技术或工具就越难。
例如,你不太能找到能够与中文(或者说“中文写作系统”)配合的工具。这些语言不一定是编程上难以理解的,但是对它们的研究可能会比较少,或者分析方法可能与英语所采用的完全不同。
本指南的结构
我们按要完成的任务组织文章结构 ——这意味着工具及其解释按照它们所适用的任务进行分组。例如,有一节是关于度量文本某种属性(比如它的难度)的。一般来说,它们按照难度从小到大的顺序排列 ——给单词分类比给文档分类要更加容易。始于简单的信息检索技术,终于严格意义上的自然语言处理领域。
我们认为这种提供所需信息的方法最为有效:如果你需要做某某事,我们直接展示你能够使用的方法和工具。
词类分类
“词类分类”这种表述包括将词汇分组的技术和库。
相似词汇分组
我们将介绍两种以信息检索为目的相似词汇分组方法。总的来说,这些方法用以从文档池中找到包含我们关心词汇的文档的方法。这是很有用的,因为如果用户搜索包含“朋友(friend)”这个词的文档,他们很可能也对包含“友谊(friendship)”、“交友(friended)”和“好友(friends)”的文档感兴趣。
所以说白了,在本节中,我们不会讨论根据语义来将词汇分组的方法,例如识别所有宠物或所有英国城镇名。
这两种方法分别是“词干提取”和“词汇拆分”。前者的算法依赖语言,而后者不是。我们将分两部分来分析。
词干提取
词干提取是找到一个词的词干(stem)或者词根(root)的过程。在这种情况下,词干不一定是语言学家所论的形态上的词根。所以它不是单词的某种形式,你可能没法在词汇表上找到。例如,一个算法可能由“consoling(安慰)”一词生成词干“consol”,而在一个词汇表中,你会它的词根是“console”。
词干提取的典型应用是将具有相同词干的词的所有实例组合在一起以供在搜索库中使用。因此,如果用户搜索包含“friend”的文档,他们也会找到“friends”或“friended”的文档。
波特词干算法
让我们来谈谈一个通过移除后缀以提取词干的算法:有效和广泛使用的 Porter 词干算法。该算法最初由Martin Porter为英语设计。对于其他语言(如法语或俄语),也有基于 Porter 的或受其启发的算法。你可以在 Snowball 这个网站上找到所有的算法。Snowball 是一种用来描述词干提取算法的简单语言,不过这些算法也有简单的英文描述。
篇幅所限,本指南无法完整叙述该算法。但是,它的基础部分很容易掌握。从根本上说,该算法将一个单词分成若干区域,然后如果这些区域完整包含了这些后缀的话,替换或移除某些后缀。例如,Porter 2(即更新版本)算法指出:
R1 是元音后第一个非元音之后的区域,如果没有非元音则为单词结尾。
如果在 R1 区域内找到了“-tional”,则用“-tion”替换之。
举例:
-
confrontational
的 R1 区域为-frontational
- 其 R1 区完全包含了
-tional
-
confrontational
变成了confrontation
波特词干提取器是纯算法的,这意味着它不依赖于外部数据库或计算规则(即参照训练集创建的规则)。这是一个很大的优势,因为它易于预测和实施。劣势在于不能处理例外情况,而且已知错误难以解决。例如,该算法对“university(大学)”和“universal(通用的)”创建相同的词干。
波特词干提取器并非完美的——但它简单,有效,且易于实现。对于像英语这样的语言来说,任何有能力的开发者都可以实现一个词干提取器。正因如此,你能找到基于各种著名编程语言的实现,我们在此不一一列出。
在其他语言上的典型问题
大多数与英语接近的语言,如德语甚至罗曼语族,通常都很容易提取词干。实际上,算法本身的设计就很复杂,需要高深的语言学知识。但是,一旦有人花大力气设计号一个算法,再去实现算法就容易了。
在词干提取中,两种类型的语言往往会遇到许多问题。第一种是黏着语。我们不谈其语言学意义,其问题就在于黏着语的词根堆满了前缀和后缀。
特别地,如土耳其语就很容易引起问题,因为它既是一种黏着语,也是一种拼接语,这意味着土耳其语中的一个词基本上可以代表整个英语句子。这使得设计一个土耳其语词干提取算法十分困难,就算能开发出来也未必有用——因为如果你提取的是土耳其语单词,那么每个句子最后只会有一个词干,丢失了很多信息。
第二类问题源于那些词汇没有明确定义的语言。中文是没有字母表的语言的典型,它只有表示概念的符号。所以,词干提取对中国人来说没有意义,就连确定概念的明确界限也很困难。划分文本间词汇组成的问题被称为分词。在英语中,你可以通过查找空格或标点符号来找到词汇间的界限,中文则没有这样的东西。
词汇拆分
另一种进行词汇分组的方法是将词汇分割开来。这种方法的核心是把文字分解成字符串。这些字符被称为k-grams( n 元模型),也被称为n-grams characters ( n 元字符模型)( n-grams 有时也表示以单词为组,即 n 元单词模型)。字符序列以滑动的方式构建,在每个步中前进一个字符,以指示字的边界的特殊符号开始和结束。例如,happy
的 3 元模型是:
- $ha
- hap
- app
- ppy
- py $
用符号$来表示单词的开始和结束。
用于搜索的确切方法超出了本文的范围。一般而言,你对搜索项进行上述处理,然后比较输入的 n 元模型与文档中的某个词二者的出现次数。通常情况下是选用一个统计系数,如 Jaccard 相似系数,以确定多相似的词汇要被分在一组(即有多少共同元)。例如,由于相似系数高,你会把“cat”和“cats”分组,或者“cat”和“catty”。
需要注意几点:n 元模型的顺序和拼写错误。n 元模型的顺序无关紧要,从理论上说,完全不同的单词可能碰巧具有相同的 n 元模型。不过在实践中,这不会发生。这种方法并不精确,这意味着它也可以防止用户的拼写错误。例如,即使用户将“locomotive”拼成了“locamotive”,它仍可能显示正确的结果。那是因为 10个 3 元模型中有 7 个是相匹配的。完全匹配会排在更高的位置,但因为“locamotive”这个词并不存在,所以它一般没有其他匹配。
限制和有效性
这种技术的巨大优势在于,它不仅仅是算法简单,而且还适用于所有语言。你不需要为法语建立不同于英语的 n 元模型,制药以相同的方式拆分这些单词就好。不过重要的是要注意有效性的细节——你必须选择正确的大小n以获得最好的结果。
这个理想数字取决于该种语言中单词的长度,它应该低于或等于平均单词长度。不同的语言取值不同,但一般来说,选择 4 或 5 都能成功。只选择一种并不能得出最好的结果,尽管这也行得通。
缺点是它看起来非常愚蠢,真的:它太简单了,它怎么行得通呢?但实际上它真的可以,就算不比词干提取法好,它至少也行得通呀(PDF)。就是这么无耻地有效,并且还有许多其他用途。我们现在来看一个应用:
生成名称
一般情况下,生成貌似真实的虚假单词很困难,而且用处有限。你可以为一种伪造语言生成许多短语,但要太多了。但是,你可以通过编程生成逼真的假名字,用于游戏或任何架空世界建构的需求。
这里有几种可行的方法,最简单的大概是这样工作的:
- 创建一个你想要生成的同类型名字的数据库(即罗马名字,太空蜥蜴人的名字等)。
- 以 n 元模型处理输入的名字 (如 Mark 的 3 元模型 -> $ma - mar - ark - rk$)。
- 将概率与 n 元模型相关联:在原始数据库中出现的频率越高,其出现在生成名称中的概率就越高。
- 生成新的名字!
这有许多变种。例如,你可以将不同数量的 n 元模型结合起来以满足特定要求(如所有名称以 2 元模型开头,以 4 元模型结尾)。
你也可以仅通过检查序列以特定顺序出现的概率来提高生成名字的可靠性。例如,如果你随机以“ar”开头,之后音节更可能是“th”而非“or”。
这种方法并不完美,但通常工作得很不错。这里有几个简单的示例: langgen
和 VNameGenerator
,它们体现了我们提到的方法,同时还有一些别的方法。
结论
第一部分就到此为止了!在第 2 部分中,我们将讨论对文档分类。在以后的文章中,我们会讨论文档理解,文档分析,情感分析,自然语言处理的库等等。
敬请关注!
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Python自学成才之路 多进程开发
- Python自学成才之路 进程间通信
- android 功耗(1)---android 功耗分析方法和优化
- 为什么要写 tf.Graph().as_default()
- tf.get_variable_scope().reuse_variables() 的使用
- Tensorflow小技巧整理:
- Tensorflow学习笔记——Summary用法
- 神经网络优化(损失函数:自定义损失函数、交叉熵、softmax())
- C++ STL stack 用法
- 遍历string时 使用for(char& c : s) for(char c : s) 的区别
- vc dll静态函数导出
- 利用GDB调试 MSQL
- 手把手教学-MySQL主从复制架构转换MGR架构(mysq_shell版)
- 手把手教学-MySQL主从复制架构转换MGR架构(手动版)
- 云数据库VS自建数据库,到底该如何抉择?