nlp-py-cb-merge-1

57 阅读1小时+

Python 自然语言处理秘籍(二)

原文:annas-archive.org/md5/209bb9c5cbc97ace0712ae52223b18f3

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:词性标注和语法

在本章中,我们将涵盖以下内容:

  • 探索内置标注器

  • 编写自己的标注器

  • 训练自己的标注器

  • 学习编写自己的语法

  • 编写概率上下文无关语法(CFG)

  • 编写递归 CFG

介绍

本章主要聚焦于使用 Python NLTK 学习以下内容:

  • 标注器

  • CFG

标注是使用词性POS)对给定句子中的单词进行分类的过程。帮助实现这一过程的软件称为标注器。NLTK 支持多种标注器。我们将在本章中介绍以下标注器:

  • 内置标注器

  • 默认标注器

  • 正则表达式标注器

  • 查找标注器

CFG 描述了一组规则,可以在正式语言规范中应用于文本,以生成新的文本集。

语言中的 CFG 包含以下内容:

  • 启动标记

  • 一组终结符(结束符号)的标记

  • 一组非终结符(非结束符号)的标记

  • 定义重写规则的规则或产生式,帮助将非终结符转换为终结符或非终结符

探索内置标注器

在以下实例中,我们使用 Python NLTK 库来了解给定文本中的词性标注功能。

我们将使用 Python NLTK 库中的以下技术:

  • Punkt 英语分词器

  • 平均感知标注器

这些标注器的数据集可以通过在 Python 提示符下调用nltk.download()从你的 NLTK 分发包中下载。

准备工作

你应该在系统中安装工作中的 Python(推荐 Python 3.6),并配备 NLTK 库及其所有集合,以获得最佳体验。

如何操作...

  1. 打开 atom 编辑器(或喜欢的编程编辑器)。

  2. 创建一个名为Exploring.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的...

现在,让我们回顾一下我们刚刚编写的程序,并深入了解细节:

import nltk

这是我们程序中的第一条指令,它指示 Python 解释器从磁盘加载模块到内存,并使得 NLTK 库在程序中可用:

simpleSentence = "Bangalore is the capital of Karnataka."

在本指令中,我们创建了一个名为simpleSentence的变量,并为其分配了一个硬编码的字符串:

wordsInSentence = nltk.word_tokenize(simpleSentence)

在本指令中,我们调用了 NLTK 的内置分词函数word_tokenize();它将给定句子分解为单词,并返回一个 Python list数据类型。一旦函数计算完结果,我们使用=(等号)操作符将结果赋值给一个名为wordsInSentence的变量:

print(wordsInSentence)

在这条指令中,我们调用了 Python 内置的print()函数,它会将给定的数据结构显示在屏幕上。在我们的例子中,我们显示了所有分词后的单词列表。仔细观察输出;我们正在屏幕上显示一个 Python list 数据结构,它由用逗号分隔的所有字符串组成,所有列表元素都被方括号括起来:

partsOfSpeechTags = nltk.pos_tag(wordsInSentence)

在这条指令中,我们调用了 NLTK 内置的标注器pos_tag(),它接受wordsInSentence变量中的单词列表并识别词性。一旦识别完成,将返回一个元组列表,每个元组包含分词后的单词和词性标识符:

print(partsOfSpeechTags)

在这条指令中,我们调用了 Python 内置的print()函数,它将给定的参数打印到屏幕上。在我们的例子中,我们可以看到一个元组列表,每个元组包含原始单词和词性标识符。

编写你自己的标注器

在接下来的示例中,我们将通过编写自己的标注器来探索 NLTK 库。我们将编写以下类型的标注器:

  • 默认标注器

  • 正则表达式标注器

  • 查找标注器

正在准备

你应该在系统中安装一个工作中的 Python(推荐 Python 3.6),以及 NLTK 库及其所有集合,以获得最佳体验。

你还应该安装python-crfsuite以运行这个示例。

如何操作...

  1. 打开你的 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为OwnTagger.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出。

它是如何工作的...

现在,让我们回顾一下我们刚刚写的程序,深入理解:

import nltk

这是我们程序中的第一条指令;它指示 Python 解释器将模块从磁盘加载到内存中,并使 NLTK 库在程序中可用:

def learnDefaultTagger(simpleSentence):
  wordsInSentence = nltk.word_tokenize(simpleSentence)
  tagger = nltk.DefaultTagger("NN")
  posEnabledTags = tagger.tag(wordsInSentence)
  print(posEnabledTags)

所有这些指令都在定义一个新的 Python 函数,它接受一个字符串作为输入并在屏幕上打印该句子中的单词以及默认标注。我们进一步理解这个函数,看看它在做什么:

def learnDefaultTagger(simpleSentence):

在这条指令中,我们定义了一个新的 Python 函数,名为learnDefaultTagger;它接受一个名为simpleSentence的参数:

wordsInSentence = nltk.word_tokenize(simpleSentence)

在这条指令中,我们从 NLTK 库中调用了word_tokenize函数。我们将simpleSentence作为第一个参数传递给这个函数。当数据被该函数处理后,返回值将存储在wordsInSentence变量中。它是一个包含单词的列表:

tagger = nltk.DefaultTagger("NN")

在这条指令中,我们创建了一个DefaultTagger()类的对象,该类来自 Python nltk 库,并将NN作为参数传递给它。这将初始化标注器并将实例分配给tagger变量:

posEnabledTags = tagger.tag(wordsInSentence)

在本说明中,我们正在调用tag()函数,它属于tagger对象,该函数接受来自wordsInSentence变量的分词,并返回标记化单词的列表。该列表保存在posEnabledTags中。请记住,句子中的所有单词都将被标记为NN,因为这是标记器应该做的。这就像一个非常基础的标注,甚至没有涉及词性(POS):

print(posEnabledTags)

在这里,我们调用了 Python 内置的print()函数来检查posEnabledTags变量的内容。我们可以看到,句子中的所有单词都会被标记为NN:

def learnRETagger(simpleSentence):
  customPatterns = [
    (r'.*ing$', 'ADJECTIVE'),
    (r'.*ly$', 'ADVERB'),
    (r'.*ion$', 'NOUN'),
    (r'(.*ate|.*en|is)$', 'VERB'),
    (r'^an$', 'INDEFINITE-ARTICLE'),
    (r'^(with|on|at)$', 'PREPOSITION'),
    (r'^\-?[0-9]+(\.[0-9]+)$', 'NUMBER'),
    (r'.*$', None),
  ]
  tagger = nltk.RegexpTagger(customPatterns)
  wordsInSentence = nltk.word_tokenize(simpleSentence)
  posEnabledTags = tagger.tag(wordsInSentence)
  print(posEnabledTags)

这些是创建一个新函数learnRETagger()的指令,该函数接受一个字符串作为输入,并使用正则表达式标注器输出正确标记的所有令牌列表。

让我们试着一次理解一条指令:

def learnRETagger(simpleSentence):

我们正在定义一个名为*learnRETagger*的 Python 函数,该函数接受一个名为*simpleSentence*的参数。

为了理解下一条指令,我们需要学习更多关于 Python 列表、元组和正则表达式的知识:

  • Python 列表是一种数据结构,是一组有序的元素

  • Python 元组是一种不可变(只读)数据结构,是一组有序的元素

  • Python 正则表达式是以字母r开头的字符串,并遵循标准的 PCRE 符号:

customPatterns = [
  (r'.*ing$', 'ADJECTIVE'),
  (r'.*ly$', 'ADVERB'),
  (r'.*ion$', 'NOUN'),
  (r'(.*ate|.*en|is)$', 'VERB'),
  (r'^an$', 'INDEFINITE-ARTICLE'),
  (r'^(with|on|at)$', 'PREPOSITION'),
  (r'^\-?[0-9]+(\.[0-9]+)$', 'NUMBER'),
  (r'.*$', None),
]

即使这看起来很大,这其实是一个单一的指令,它做了很多事情:

  • 创建一个名为customPatterns的变量

  • 使用``定义一个新的 Python 列表数据类型

  • 向此列表添加八个元素

  • 该列表中的每个元素是一个元组,元组中有两个元素

  • 元组中的第一项是一个正则表达式

  • 元组中的第二项是一个字符串

现在,将前面的指令翻译成易于理解的形式,我们添加了八个正则表达式来标记句子中的单词,标记类型包括ADJECTIVEADVERBNOUNVERBINDEFINITE-ARTICLEPREPOSITIONNUMBERNone类型。

我们通过识别英语单词中的某些模式,来识别它们属于某个特定的词性(POS)。

在前面的例子中,这些是我们用来标记英语单词词性的线索:

  • ing结尾的单词可以标记为ADJECTIVE,例如running

  • ly结尾的单词可以标记为ADVERB,例如willingly

  • ion结尾的单词可以标记为NOUN,例如intimation

  • ateen结尾的单词可以标记为VERB,例如terminatedarkenlighten

  • an结尾的单词可以标记为INDEFINITE-ARTICLE

  • withonat这样的单词是PREPOSITION

  • -123.0984这样的单词可以标记为NUMBER

  • 我们将其他所有单词标记为NoneNone是 Python 内置的数据类型,用于表示“无”。

tagger = nltk.RegexpTagger(customPatterns)

在这条指令中,我们创建了一个 NLTK 内置正则表达式标注器RegexpTagger的实例。我们将customPatterns变量中的元组列表作为第一个参数传递给类,以初始化对象。这个对象可以在未来通过名为tagger的变量引用:

wordsInSentence = nltk.word_tokenize(simpleSentence)

按照常规流程,我们首先尝试使用 NLTK 内置的word_tokenize()函数对simpleSentence中的字符串进行分词,并将标记列表存储在wordsInSentence变量中:

posEnabledTags = tagger.tag(wordsInSentence)

现在我们调用常规表达式标注器的tag()函数,对wordsInSentence变量中的所有单词进行标注。这个标注过程的结果存储在posEnabledTags变量中:

print(posEnabledTags)

我们调用了 Python 内置的print()函数,在屏幕上显示posEnabledTags数据结构的内容:

def learnLookupTagger(simpleSentence):
  mapping = {
    '.': '.', 'place': 'NN', 'on': 'IN',
    'earth': 'NN', 'Mysore' : 'NNP', 'is': 'VBZ',
    'an': 'DT', 'amazing': 'JJ'
  }
  tagger = nltk.UnigramTagger(model=mapping)
  wordsInSentence = nltk.word_tokenize(simpleSentence)
  posEnabledTags = tagger.tag(wordsInSentence)
  print(posEnabledTags)

让我们仔细看一下:

def learnLookupTagger(simpleSentence):

我们定义了一个新函数learnLookupTagger,它接受一个字符串作为参数并存入simpleSentence变量:

tagger = nltk.UnigramTagger(model=mapping)

在这条指令中,我们从nltk库中调用了UnigramTagger。这是一种查找标注器,它接受我们创建并赋值给mapping变量的 Python 字典。一旦对象创建完成,它将存储在tagger变量中,供以后使用:

wordsInSentence = nltk.word_tokenize(simpleSentence)

在这里,我们使用 NLTK 内置的word_tokenize()函数对句子进行分词,并将结果保存在wordsInSentence变量中:

posEnabledTags = tagger.tag(wordsInSentence)

一旦句子被分词,我们通过传入wordsInSentence变量中的标记列表来调用标注器的tag()函数。这个计算的结果被赋值给posEnabledTags变量:

print(posEnabledTags)

在这条指令中,我们将在屏幕上打印posEnabledTags中的数据结构,便于进一步检查:

testSentence = "Mysore is an amazing place on earth. I have visited Mysore 10 times."

我们创建了一个名为testSentence的变量,并将一个简单的英文句子赋给它:

learnDefaultTagger(testSentence)

我们调用了在本教程中创建的learnDefaultTagger函数,传入testSentence作为第一个参数。该函数执行完成后,我们将看到句子的词性标注结果:

learnRETagger(testSentence)

在这个表达式中,我们调用了learnRETagger()函数,传入了相同的测试句子testSentence变量。这个函数的输出是根据我们自己定义的正则表达式标记的标签列表:

learnLookupTagger(testSentence)

这个函数learnLookupTagger的输出是从句子testSentence中提取的所有标签,这些标签是通过我们创建的查找字典进行标记的。

训练你自己的标注器

在这个教程中,我们将学习如何训练我们自己的标注器并将训练好的模型保存到磁盘,以便以后用于进一步的计算。

准备工作

你应该在系统中安装了一个可用的 Python(推荐 Python 3.6)版本,并且安装了 NLTK 库及其所有集合,以便获得最佳体验。

如何实现...

  1. 打开你的 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个新的文件,命名为Train3.py

  3. 输入以下源代码:

![1. 保存文件。1. 使用 Python 解释器运行程序。1. 你将看到以下输出:

它是如何工作的...

让我们理解程序是如何工作的:

import nltk
import pickle

在这两条指令中,我们将nltkpickle模块加载到程序中。pickle模块实现了强大的序列化和反序列化算法,用于处理非常复杂的 Python 对象:

def sampleData():
  return [
    "Bangalore is the capital of Karnataka.",
    "Steve Jobs was the CEO of Apple.",
    "iPhone was Invented by Apple.",
    "Books can be purchased in Market.",
  ]

在这些指令中,我们定义了一个名为sampleData()的函数,它返回一个 Python 列表。基本上,我们返回了四个样本字符串:

def buildDictionary():
  dictionary = {}
  for sent in sampleData():
    partsOfSpeechTags = nltk.pos_tag(nltk.word_tokenize(sent))
    for tag in partsOfSpeechTags:
      value = tag[0]
      pos = tag[1]
      dictionary[value] = pos
    return dictionary

现在我们定义了一个名为buildDictionary()的函数;它一次从sampleData()函数生成的列表中读取一个字符串。每个字符串都使用nltk.word_tokenize()函数进行分词。结果的标记被添加到一个 Python 字典中,其中字典的键是句子中的单词,值是词性。计算完字典后,它会返回给调用者:

def saveMyTagger(tagger, fileName):
  fileHandle = open(fileName, "wb")
  pickle.dump(tagger, fileHandle)
  fileHandle.close()

在这些说明中,我们定义了一个名为saveMyTagger()的函数,该函数接受两个参数:

  • tagger:一个指向词性标注器的对象

  • fileName:这是包含要存储tagger对象的文件名

我们首先以写入二进制wb)模式打开文件。然后,使用pickle模块的dump()方法,将整个 tagger 存储到文件中,并调用fileHandleclose()函数:

def saveMyTraining(fileName):
  tagger = nltk.UnigramTagger(model=buildDictionary())
  saveMyTagger(tagger, fileName)

在这些指令中,我们定义了一个名为saveMyTraining的新函数;它接受一个名为fileName的参数。

我们正在构建一个nltk.UnigramTagger()对象,其模型输出来自buildDictionary()函数(该函数本身是通过我们定义的字符串样本集构建的)。一旦创建了tagger对象,我们调用saveMyTagger()函数将其保存到磁盘:

def loadMyTagger(fileName):
  return pickle.load(open(fileName, "rb"))

在这里,我们定义了一个新的函数loadMyTagger(),它以fileName作为唯一参数。该函数从磁盘读取文件,并将其传递给pickle.load()函数,该函数将从磁盘反序列化 tagger 并返回对它的引用:

sentence = 'Iphone is purchased by Steve Jobs in Bangalore Market'
fileName = "myTagger.pickle"

在这两条指令中,我们定义了两个变量,sentencefileName,分别包含我们想要分析的样本字符串和我们希望存储词性标注器的文件路径:

saveMyTraining(fileName)

这条指令实际上调用了saveMyTraining()函数,并以myTagger.pickle作为参数。因此,我们基本上是在这个文件中存储训练好的 tagger:

myTagger = loadMyTagger(fileName)

在这条指令中,我们将myTagger.pickle作为参数传递给loadMyTagger()函数,它从磁盘加载 tagger,进行反序列化,并创建一个对象,然后将该对象分配给myTagger变量:

print(myTagger.tag(nltk.word_tokenize(sentence)))

在这条指令中,我们调用了刚刚从磁盘加载的 tagger 的tag()函数。我们用它来分词我们创建的样本字符串。

处理完成后,输出会显示在屏幕上。

学习编写你自己的语法

在自动机理论中,CFG 由以下几个部分组成:

  • 一个起始符号/符号

  • 一组符号/符号集,表示终结符

  • 一组符号/符号集,表示非终结符

  • 定义起始符号/符号以及可能的结束符号/符号的规则(或产生式)

符号/符号可以是任何特定于我们考虑的语言的元素。

例如:

  • 在英语语言中,a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z 是符号/符号/字母。

  • 在十进制计数系统中,0, 1, 2, 3, 4, 5, 6, 7, 8, 9 是符号/符号/字母。

通常,规则(或产生式)采用 巴科斯-诺尔范式BNF)表示法编写。

准备就绪

你应该在系统上安装一个可工作的 Python(推荐 Python 3.6),并且安装 NLTK 库。

如何做到这一点...

  1. 打开你的 atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个新的文件,命名为 Grammar.py

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行该程序。

  3. 你将看到以下输出:

它是如何工作的...

现在,让我们回顾一下我们刚刚编写的程序,并深入了解细节:

import nltk

我们将 nltk 库导入到当前程序中:

import string

该指令将 string 模块导入到当前程序中:

from nltk.parse.generate import generate

该指令从 nltk.parse.generate 模块导入 generate 函数,帮助从我们将要创建的 CFG 中生成字符串:

productions = [
  "ROOT -> WORD",
  "WORD -> ' '",
  "WORD -> NUMBER LETTER",
  "WORD -> LETTER NUMBER",
]

我们在这里定义了一个新的语法。该语法可以包含以下产生规则:

  • 起始符号是 ROOT

  • ROOT 符号可以生成 WORD 符号

  • WORD 符号可以生成 ' '(空格);这是一个死循环产生规则

  • WORD 符号可以生成 NUMBER 符号后跟 LETTER 符号

  • WORD 符号可以生成 LETTER 符号后跟 NUMBER 符号

这些指令进一步扩展了产生规则。

digits = list(string.digits)
for digit in digits[:4]:
  productions.append("NUMBER -> '{w}'".format(w=digit))
  • NUMBER 可以生成终结符字母 0123

这些指令进一步扩展了产生规则。

letters = "' | '".join(list(string.ascii_lowercase)[:4])
productions.append("LETTER -> '{w}'".format(w=letters))
  • LETTER 可以生成小写字母 abcd

让我们尝试理解这条语法的用途。该语法表示包含诸如 0a1a2aa1a3 等单词的语言。

到目前为止,我们在名为 productions 的列表变量中存储的所有产生规则都已转换为字符串:

grammarString = "\n".join(productions)

我们正在使用 nltk.CFG.fromstring() 方法创建一个新的语法对象,该方法采用我们刚刚创建的 grammarString 变量:

grammar = nltk.CFG.fromstring(grammarString)

这些指令打印出该语言中自动生成的前五个单词,这些单词是根据定义的语法生成的:

for sentence in generate(grammar, n=5, depth=5):
  palindrome = "".join(sentence).replace(" ", "")
  print("Generated Word: {}, Size : {}".format(palindrome, len(palindrome)))

编写一个概率上下文无关文法

概率上下文无关文法(Probabilistic CFG)是一种特殊类型的上下文无关文法,其中所有非终结符号(左侧)的概率之和应该等于 1。

让我们写一个简单的示例以便更好地理解。

准备工作

你应该在系统上安装一个有效的 Python 环境(推荐 Python 3.6),并且需要安装 NLTK 库。

如何操作...

  1. 打开你的 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为 PCFG.py 的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的...

现在,让我们详细看看我们刚刚编写的程序:

import nltk

这条指令将 nltk 模块导入到我们的程序中:

from nltk.parse.generate import generate

这条指令从 nltk.parse.generate 模块导入 generate 函数:

productions = [
  "ROOT -> WORD [1.0]",
  "WORD -> P1 [0.25]",
  "WORD -> P1 P2 [0.25]",
  "WORD -> P1 P2 P3 [0.25]",
  "WORD -> P1 P2 P3 P4 [0.25]",
  "P1 -> 'A' [1.0]",
  "P2 -> 'B' [0.5]",
  "P2 -> 'C' [0.5]",
  "P3 -> 'D' [0.3]",
  "P3 -> 'E' [0.3]",
  "P3 -> 'F' [0.4]",
  "P4 -> 'G' [0.9]",
  "P4 -> 'H' [0.1]",
]

在这里,我们正在定义我们语言的语法,格式如下:

描述内容
起始符号ROOT
非终结符WORD, P1, P2, P3, P4
终结符'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'

一旦我们识别出语法中的符号,就来看一下产生规则是什么样子的:

  • 有一个 ROOT 符号,它是此语法的起始符号

  • 有一个 WORD 符号,其概率为 1.0

  • 有一个 WORD 符号,它可以以 0.25 的概率生成 P1

  • 有一个 WORD 符号,它可以以 0.25 的概率生成 P1 P2

  • 有一个 WORD 符号,它可以以 0.25 的概率生成 P1 P2 P3

  • 有一个 WORD 符号,它可以以 0.25 的概率生成 P1 P2 P3 P4

  • P1 符号可以以 1.0 的概率生成符号 'A'

  • P2 符号可以以 0.5 的概率生成符号 'B'

  • P2 符号可以以 0.5 的概率生成符号 'C'

  • P3 符号可以以 0.3 的概率生成符号 'D'

  • P3 符号可以以 0.3 的概率生成符号 'E'

  • P3 符号可以以 0.4 的概率生成符号 'F'

  • P4 符号可以以 0.9 的概率生成符号 'G'

  • P4 符号可以以 0.1 的概率生成符号 'H'

如果你仔细观察,所有非终结符符号的概率之和等于 1.0。这是 PCFG 的强制要求。

我们将所有生成规则的列表合并成一个字符串,称为 grammarString 变量:

grammarString = "\n".join(productions)

这条指令使用 nltk.PCFG.fromstring 方法创建一个 grammar 对象,并以 grammarString 作为输入:

grammar = nltk.PCFG.fromstring(grammarString)

这条指令使用 Python 内建的 print() 函数在屏幕上显示 grammar 对象的内容。这将总结我们刚刚创建的语法中所有符号和生成规则的总数:

print(grammar)

我们使用 NLTK 内建的 generate 函数从这个语法中打印 10 个字符串,并将它们显示在屏幕上:

for sentence in generate(grammar, n=10, depth=5):
  palindrome = "".join(sentence).replace(" ", "")
  print("String : {}, Size : {}".format(palindrome, len(palindrome)))

编写递归 CFG

递归 CFG 是 CFG 的一种特殊类型,其中生产规则左侧的符号出现在右侧。

回文是递归 CFG 的最佳示例。我们总是可以为给定语言中的回文编写递归 CFG。

为了更好地理解,我们考虑一个只有字母 0 和 1 的语言系统;因此回文可以如下表示:

  • 11

  • 1001

  • 010010

无论我们从哪个方向读取这些字母(从左到右或从右到左),我们总是得到相同的值。这就是回文的特性。

在这个示例中,我们将编写语法来表示这些回文,并使用 NLTK 内建的字符串生成库生成一些回文。

让我们写一个简单的例子来更好地理解。

准备开始

你应该在系统上安装一个正常工作的 Python 环境(推荐使用 Python 3.6),并安装 NLTK 库。

如何操作...

  1. 打开你的 atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为 RecursiveCFG.py 的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到如下输出:

它是如何工作的...

现在,让我们回顾一下刚才编写的程序并深入了解细节。我们正在将 nltk 库导入到我们的程序中,以便将来使用:

import nltk

我们还将 string 库导入到我们的程序中,以便将来使用:

import string

我们正在从 nltk.parse.generate 模块导入 generate 函数:

from nltk.parse.generate import generate

我们创建了一个名为 productions 的新列表数据结构,其中有两个元素。两个元素都是表示我们 CFG 中两个生产规则的字符串:

productions = [
  "ROOT -> WORD",
  "WORD -> ' '"
]

我们正在将十进制数字列表作为 alphabets 变量中的列表进行检索:

alphabets = list(string.digits)

使用数字 0 到 9,我们向列表中添加更多的生产规则。这些是定义回文的生产规则:

for alphabet in alphabets:
  productions.append("WORD -> '{w}' WORD '{w}'".format(w=alphabet))

一旦所有规则都生成完毕,我们将它们作为字符串连接到一个变量 grammarString 中:

grammarString = "\n".join(productions)

在此指令中,我们通过将新构建的 grammarString 传递给 NLTK 内建的 nltk.CFG.fromstring 函数来创建一个新的 grammar 对象:

grammar = nltk.CFG.fromstring(grammarString)

在此指令中,我们通过调用 Python 内建的 print() 函数打印出我们刚刚创建的语法:

print(grammar)

我们使用 NLTK 库的 generate 函数生成五个回文,并将其打印到屏幕上:

for sentence in generate(grammar, n=5, depth=5):
  palindrome = "".join(sentence).replace(" ", "")
  print("Palindrome : {}, Size : {}".format(palindrome, len(palindrome)))

第六章:分块、句子解析和依赖关系

在本章中,我们将执行以下配方:

  • 使用内置的分块器

  • 编写你自己的简单分块器

  • 训练一个分块器

  • 解析递归下降

  • 解析移位规约

  • 解析依赖语法和投影依赖

  • 解析图表

简介

到目前为止,我们已经学习到 Python NLTK 可以用于对给定文本进行词性POS)识别。但有时我们对我们处理的文本中更多细节感兴趣。例如,我可能对在给定文本中找到的某些名人、地点等感兴趣。我们可以维护一个非常大的所有这些名称的字典。但在最简单的形式中,我们可以使用 POS 分析来轻松识别这些模式。

分块是从文本中提取短语的过程。我们将利用 POS 标记算法进行分块。请记住,分块生成的标记(单词)不重叠。

使用内置的分块器

在这个配方中,我们将学习如何使用内置的分块器。以下是在此过程中从 NLTK 中使用的特性:

  • Punkt 标记器(默认)

  • 平均感知标注器(默认)

  • 最大熵 NE 分块器(默认)

准备就绪

您应该已经安装了 Python 以及nltk库。建议事先了解 POS 标记,如第五章,POS 标记和语法

如何做到…

  1. 打开 Atom 编辑器(或您喜欢的编程编辑器)。

  2. 创建一个名为Chunker.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 您将看到以下输出:

工作原理…

让我们试着理解程序是如何工作的。这条指令将nltk模块导入程序中:

import nltk

这是我们作为这个配方的一部分要分析的数据。我们将这个字符串添加到名为text的变量中:

text = "Lalbagh Botanical Gardens is a well known botanical garden in Bengaluru, India."

这个指令将把给定的文本分成多个句子。结果是存储在名为sentences的列表中的句子:

sentences = nltk.sent_tokenize(text)

在这个指令中,我们正在循环遍历我们提取的所有句子。每个句子都存储在名为sentence的变量中:

for sentence in sentences:

这个指令将句子分成不重叠的单词。结果存储在名为words的变量中:

words = nltk.word_tokenize(sentence)

在这个指令中,我们使用 NLTK 提供的默认标注器进行 POS 分析。一旦识别完成,结果将存储在名为tags的变量中:

tags = nltk.pos_tag(words)

在这个指令中,我们调用nltk.ne_chunk()函数,它为我们执行分块部分。结果存储在名为 chunks 的变量中。实际上,结果是包含树路径的树结构化数据:

chunks = nltk.ne_chunk(tags)

这会打印出在给定输入字符串中识别的分块。分块会被括号 "(" 和 ")" 包裹,以便与输入文本中的其他单词区分开来。

print(chunks)

编写你自己的简单分块器

在本教程中,我们将编写自己的正则表达式分块器。由于我们将使用正则表达式来编写此分块器,我们需要了解一些编写正则表达式以进行分块时的不同之处。

在第四章《正则表达式》中,我们理解了正则表达式及其编写方法。例如,形式为 [a-z, A-Z]+ 的正则表达式匹配英语句子中的所有单词。

我们已经理解,通过使用 NLTK,我们可以识别出词性的简写(如 VNNNNP 等)。我们能否使用这些词性编写正则表达式?

答案是肯定的。你猜对了。我们可以利用基于词性的正则表达式编写。由于我们使用词性标签来编写这些正则表达式,它们被称为标签模式。

就像我们写一个给定自然语言的字母(a-z)来匹配不同的模式一样,我们也可以利用词性(POS)根据 NLTK 匹配的词性来匹配单词(来自字典的任意组合)。

这些标签模式是 NLTK 最强大的功能之一,因为它们使我们能够仅通过基于词性的正则表达式来匹配句子中的单词。

为了更深入地了解这些,让我们进一步探讨:

"Ravi is the CEO of a Company. He is very powerful public speaker also."

一旦我们识别了词性,结果如下所示:

[('Ravi', 'NNP'), ('is', 'VBZ'), ('the', 'DT'), ('CEO', 'NNP'), ('of', 'IN'), ('a', 'DT'), ('Company', 'NNP'), ('.', '.')]
[('He', 'PRP'), ('is', 'VBZ'), ('very', 'RB'), ('powerful', 'JJ'), ('public', 'JJ'), ('speaker', 'NN'), ('also', 'RB'), ('.', '.')]

之后,我们可以使用这些信息来提取名词短语。

让我们仔细查看前面的词性输出。我们可以得出以下观察:

  • 分块是一个或多个连续的 NNP

  • 分块是 NNP 后跟 DT

  • 分块是 NP 后跟另一个 JJ

通过这三个简单的观察,我们来编写一个基于词性的正则表达式,它在 BNF 形式中称为标签短语:

NP -> <PRP>
NP -> <DT>*<NNP>
NP -> <JJ>*<NN>
NP -> <NNP>+

我们感兴趣的是从输入文本中提取以下分块:

  • Ravi

  • the CEO

  • a company

  • powerful public speaker

让我们编写一个简单的 Python 程序来完成任务。

准备开始

你应该已经安装了 Python,并且安装了 nltk 库。对正则表达式有一定了解会很有帮助。

如何操作……

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为 SimpleChunker.py 的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的……

现在,让我们理解程序是如何工作的:

这条指令将 nltk 库导入当前程序:

import nltk

我们声明了一个 text 变量,其中包含我们要处理的句子:

text = "Ravi is the CEO of a Company. He is very powerful public speaker also."

在这个指令中,我们编写正则表达式,这些正则表达式是通过词性标注(POS)来写的,因此它们被特别称为标签模式。这些标签模式不是随意创建的,而是从前面的示例中精心制作的。

grammar = '\n'.join([
  'NP: {<DT>*<NNP>}',
  'NP: {<JJ>*<NN>}',
  'NP: {<NNP>+}',
])

让我们理解这些标签模式:

  • NP 后跟一个或多个 <DT>,然后是一个 <NNP>

  • NP 后跟一个或多个 <JJ>,然后是一个 <NN>

  • NP 后跟一个或多个 <NNP>

我们处理的文本越多,就能发现更多这样的规则。这些规则是特定于我们处理的语言的。所以,这是我们应该做的练习,以便在信息提取方面变得更强大:

sentences = nltk.sent_tokenize(text)

首先,我们使用 nltk.sent_tokenize() 函数将输入文本分割成句子:

for sentence in sentences:

这个指令会遍历所有句子的列表,并将每个句子分配给 sentence 变量:

words = nltk.word_tokenize(sentence)

这个指令使用 nltk.word_tokenize() 函数将句子分割成标记,并将结果存入 words 变量:

tags = nltk.pos_tag(words)

这个指令会对单词变量(其中包含一个单词列表)进行词性标注,并将结果存入 tags 变量(每个单词会被正确地标注上相应的词性标签):

chunkparser = nltk.RegexpParser(grammar)

这个指令调用 nltk.RegexpParser 来解析我们之前创建的语法。对象保存在 chunkparser 变量中:

result = chunkparser.parse(tags)

我们使用对象解析这些标签,结果存储在 result 变量中:

print(result)

现在,我们使用 print() 函数在屏幕上显示已识别的词块。输出结果是一个树状结构,显示了单词及其对应的词性。

训练词块解析器

在这个示例中,我们将学习训练过程,训练我们自己的词块解析器,并进行评估。

在开始训练之前,我们需要了解我们处理的数据类型。一旦我们对数据有了基本了解,就必须根据需要提取的信息来训练它。一种特定的训练数据的方法是使用 IOB 标注法来标记从给定文本中提取的词块。

自然地,我们在句子中找到了不同的单词。从这些单词中,我们可以找出它们的词性。稍后在进行词块分析时,我们需要根据单词在文本中的位置进一步标注单词。

取以下示例:

"Bill Gates announces Satya Nadella as new CEO of Microsoft"

一旦我们完成了词性标注和词块分析,我们将看到类似这样的输出:

Bill NNP B-PERSON
Gates NNP I-PERSON
announces NNS O
Satya NNP B-PERSON
Nadella NNP I-PERSON
as IN O
new JJ O
CEO NNP B-ROLE
of IN O
Microsoft NNP B-COMPANY

这叫做 IOB 格式,每行由三个由空格分隔的标记组成。

描述
IOB 中的第一列输入句子中的实际单词
IOB 中的第二列单词的词性
IOB 中的第三列词块标识符,包含 I(词块内)、O(词块外)、B(词块的开始词)以及适当的后缀来表示单词的类别

让我们在图示中查看这一过程:

一旦我们有了 IOB 格式的训练数据,我们可以进一步利用它,通过将其应用于其他数据集来扩展我们的 chunker 的适用范围。如果我们从头开始训练,或者想从文本中识别新的关键字类型,训练是非常昂贵的。

让我们尝试写一个简单的 chunker,使用regexparser,看看它能给出什么类型的结果。

准备就绪

你应该已经安装了 Python,并且安装了nltk库。

如何操作……

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为TrainingChunker.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的……

这条指令将nltk模块导入到当前程序中:

import nltk

这条指令将conll2000语料库导入到当前程序中:

from nltk.corpus import conll2000

这条指令将treebank语料库导入到当前程序中:

from nltk.corpus import treebank_chunk

我们定义了一个新函数mySimpleChunker()。我们还定义了一个简单的标签模式,用于提取所有词性为NNP(专有名词)的单词。这个语法用于我们的 chunker 提取命名实体:

def mySimpleChunker():
  grammar = 'NP: {<NNP>+}'
  return nltk.RegexpParser(grammar)

这是一个简单的 chunker,它不会从给定的文本中提取任何内容。用于检查算法是否正常工作:

def test_nothing(data):
  cp = nltk.RegexpParser("")
  print(cp.evaluate(data))

这个函数在测试数据上使用mySimpleChunker(),并评估数据与已经标记的输入数据的准确性:

def test_mysimplechunker(data):
  schunker = mySimpleChunker()
  print(schunker.evaluate(data))

我们创建了一个包含两个数据集的列表,一个来自conll2000,另一个来自treebank

datasets = [
  conll2000.chunked_sents('test.txt', chunk_types=['NP']),
  treebank_chunk.chunked_sents()
]

我们对两个数据集进行迭代,并在前 50 个 IOB 标记的句子上调用test_nothing()test_mysimplechunker(),以查看 chunker 的准确性。

for dataset in datasets:
  test_nothing(dataset[:50])
  test_mysimplechunker(dataset[:50])

递归下降解析

递归下降解析器属于一种解析器家族,它从左到右读取输入,并以自顶向下的方式构建解析树,同时以先序遍历的方式遍历节点。由于语法本身是使用 CFG 方法表达的,解析是递归性质的。这种解析技术用于构建编译器,以解析编程语言的指令。

在本教程中,我们将探讨如何使用 NLTK 库自带的 RD 解析器。

准备就绪

你应该已经安装了 Python,并且安装了nltk库。

如何操作……

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为ParsingRD.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

这个图是输入中第二个句子通过 RD 解析器解析后的输出:

它是如何工作的……

让我们看看程序是如何工作的。在这条指令中,我们导入了nltk库:

import nltk

在这些说明中,我们定义了一个新的函数 SRParserExample;它接受一个 grammar 对象和 textlist 作为参数:

def RDParserExample(grammar, textlist):

我们通过调用 nltk.parse 库中的 RecursiveDescentParser 来创建一个新的解析器对象。我们将 grammar 传递给这个类进行初始化:

parser = nltk.parse.RecursiveDescentParser(grammar)

在这些说明中,我们遍历 textlist 变量中的句子列表。每个文本项都使用 nltk.word_tokenize() 函数进行分词,然后将结果单词传递给 parser.parse() 函数。一旦解析完成,我们会将结果显示在屏幕上,并展示解析树:

for text in textlist:
  sentence = nltk.word_tokenize(text)
  for tree in parser.parse(sentence):
    print(tree)
    tree.draw()

我们使用 grammar 创建一个新的 CFG 对象:

grammar = nltk.CFG.fromstring("""
S -> NP VP
NP -> NNP VBZ
VP -> IN NNP | DT NN IN NNP
NNP -> 'Tajmahal' | 'Agra' | 'Bangalore' | 'Karnataka'
VBZ -> 'is'
IN -> 'in' | 'of'
DT -> 'the'
NN -> 'capital'
""")

这些是我们用来理解解析器的两个样本文本:

text = [
  "Tajmahal is in Agra",
  "Bangalore is the capital of Karnataka",
]

我们调用 RDParserExample 使用 grammar 对象和样本文本列表。

RDParserExample(grammar, text)

移位归约解析

在这个教程中,我们将学习如何使用和理解移位归约解析。

移位归约解析器是特殊类型的解析器,它们从左到右解析单行句子的输入文本,从上到下解析多行句子的输入文本。

对于输入文本中的每个字母/符号,解析过程如下:

  • 从输入文本中读取第一个符号并将其推送到堆栈(移位操作)

  • 从堆栈中读取完整的解析树,并查看可以应用哪些生成规则,通过从右到左读取生成规则(归约操作)

  • 这个过程会一直重复,直到我们用尽所有的生成规则,这时我们认为解析失败

  • 这个过程会一直重复,直到所有输入都被消耗完,我们认为解析成功

在以下示例中,我们看到只有一个输入文本会被成功解析,另一个则无法解析。

准备就绪

你应该已经安装了 Python,并且安装了 nltk 库。需要了解如何编写语法规则。

如何做...

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个新的文件,命名为 ParsingSR.py

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的...

让我们看看程序是如何工作的。在这条指令中,我们导入了 nltk 库:

import nltk

在这些说明中,我们定义了一个新的函数 SRParserExample;它接受一个 grammar 对象和 textlist 作为参数:

def SRParserExample(grammar, textlist):

我们通过调用 nltk.parse 库中的 ShiftReduceParser 来创建一个新的解析器对象。我们将 grammar 传递给这个类进行初始化:

parser = nltk.parse.ShiftReduceParser(grammar)

在这些说明中,我们遍历 textlist 变量中的句子列表。每个文本项都使用 nltk.word_tokenize() 函数进行分词,然后将结果单词传递给 parser.parse() 函数。一旦解析完成,我们会将结果显示在屏幕上,并展示解析树:

for text in textlist:
  sentence = nltk.word_tokenize(text)
  for tree in parser.parse(sentence):
    print(tree)
    tree.draw()

这些是我们用来理解移位归约解析器的两个样本文本:

text = [
  "Tajmahal is in Agra",
  "Bangalore is the capital of Karnataka",
]

我们使用 grammar 创建一个新的 CFG 对象:

grammar = nltk.CFG.fromstring("""
S -> NP VP
NP -> NNP VBZ
VP -> IN NNP | DT NN IN NNP
NNP -> 'Tajmahal' | 'Agra' | 'Bangalore' | 'Karnataka'
VBZ -> 'is'
IN -> 'in' | 'of'
DT -> 'the'
NN -> 'capital'
""")

我们使用 grammar 对象和示例句子的列表调用 SRParserExample

SRParserExample(grammar, text)

解析依赖语法和投影依赖

在这个食谱中,我们将学习如何解析依赖语法并使用投影依赖解析器。

依赖语法基于这样一个概念:有时,句子中的单词之间存在直接关系。此食谱中的示例清楚地展示了这一点。

准备就绪

你应该安装 Python,并且需要安装nltk库。

如何实现...

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为 ParsingDG.py 的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的...

让我们看看程序是如何工作的。这个指令将 nltk 库导入程序:

import nltk

这条指令使用 nltk.grammar.DependencyGrammar 类创建了一个 grammar 对象。我们正在向语法中添加以下生成规则:

grammar = nltk.grammar.DependencyGrammar.fromstring("""
'savings' -> 'small'
'yield' -> 'savings'
'gains' -> 'large'
'yield' -> 'gains'
""")

让我们更深入了解这些生成规则:

  • smallsavings 相关

  • savingsyield 相关

  • largegains 相关

  • gainsyield 相关

这是我们将运行解析器的示例句子。它被存储在一个名为sentence的变量中:

sentence = 'small savings yield large gains'

这条指令使用我们刚刚定义的 grammar 创建一个新的 nltk.parse.ProjectiveDependencyParser 对象:

dp = nltk.parse.ProjectiveDependencyParser(grammar)

在这个 for 循环中,我们做了很多事情:

for t in sorted(dp.parse(sentence.split())):
  print(t)
  t.draw()

前面的 for 循环做了以下操作:

  • 我们正在拆分句子中的单词

  • 所有单词列表作为输入传递给 dp 对象

  • 解析后的结果通过 sorted() 内置函数进行排序

  • 遍历所有树形路径并将它们显示在屏幕上,同时以漂亮的树形结构呈现结果

解析图表

图表解析器是适用于自然语言的特殊类型解析器,因为自然语言的语法通常是模糊的。它们使用动态编程来生成所需的结果。

动态编程的好处是,它将给定问题分解为子问题,并将结果存储在一个共享位置,算法可以在遇到相似子问题时重复使用这些结果。这大大减少了反复计算相同问题的需求。

在这个食谱中,我们将学习 NLTK 库提供的图表解析功能。

准备就绪

你应该安装 Python,并且需要安装 nltk 库。理解语法是很有帮助的。

如何实现...

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为 ParsingChart.py 的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的...

让我们看看程序是如何工作的。此指令将CFG模块导入程序:

from nltk.grammar import CFG

本指令将ChartParserBU_LC_STRATEGY功能导入程序:

from nltk.parse.chart import ChartParser, BU_LC_STRATEGY

我们正在为示例创建一个语法规则。所有的产生式都以 BNF 形式表示:

grammar = CFG.fromstring("""
S -> T1 T4
T1 -> NNP VBZ
T2 -> DT NN
T3 -> IN NNP
T4 -> T3 | T2 T3
NNP -> 'Tajmahal' | 'Agra' | 'Bangalore' | 'Karnataka'
VBZ -> 'is'
IN -> 'in' | 'of'
DT -> 'the'
NN -> 'capital'
""")

语法由以下部分组成:

  • 一个起始符号S,它生成T1 T4

  • 非终结符号T1T2T3T4,它们分别生成NNP VBZDT NNIN NNPT2T2 T3

  • 终结符号,即来自英语词典的单词

使用语法对象BU_LC_STRATEGY创建一个新的图表解析器对象,并且我们已将trace设置为True,以便在屏幕上看到解析过程:

cp = ChartParser(grammar, BU_LC_STRATEGY, trace=True)

我们将在本程序中处理这个示例字符串,它存储在名为sentence的变量中:

sentence = "Bangalore is the capital of Karnataka"

本指令从示例句子创建一个单词列表:

tokens = sentence.split()

本指令将单词列表作为输入,然后开始解析。解析的结果将存储在chart对象中:

chart = cp.chart_parse(tokens)

我们正在将图表中所有可用的解析树存储到parses变量中:

parses = list(chart.parses(grammar.start()))

本指令打印当前chart对象中所有边的总数:

print("Total Edges :", len(chart.edges()))

本指令将所有解析树打印到屏幕上:

for tree in parses: print(tree)

本指令在 GUI 控件中显示图表的漂亮树状视图:

tree.draw()

第七章:信息提取与文本分类

在本章中,我们将介绍以下几种方法:

  • 使用内置的命名实体识别(NER)

  • 创建、反向构建和使用字典

  • 创建你自己的命名实体(NE)

  • 选择特征集

  • 使用分类方法分割句子

  • 文档分类

  • 编写带有上下文的词性标注器

介绍

信息检索是一个庞大的领域,并且面临许多挑战。在前面的章节中,我们了解了正则表达式、语法、词性POS)标注和短语块分析。紧接着的自然步骤是识别给定文本中的感兴趣实体。明确来说,当我们处理大量数据时,我们最关心的是查找是否提到任何著名的人物、地点、产品等。这些被称为命名实体,在自然语言处理(NLP)中占有重要地位。我们将在接下来的例子中进一步了解这些概念。此外,我们还将探讨如何利用输入文本中的线索来对大量文本进行分类,并解释更多示例,敬请期待!

理解命名实体

到目前为止,我们已经学习了如何解析文本、识别词性并提取短语块。接下来我们需要关注的是如何识别专有名词,也就是命名实体。

命名实体帮助我们理解文本中所指的内容,以便进一步对数据进行分类。由于命名实体通常包含多个词,因此有时很难从文本中识别出这些实体。

让我们通过以下示例来理解什么是命名实体:

句子命名实体
汉皮位于通加布赫德拉河的南岸汉皮,通加布赫德拉河
巴黎因时尚而闻名巴黎
哈利法塔是迪拜的摩天大楼之一哈利法塔,迪拜
杰夫·维纳是领英的首席执行官杰夫·维纳,领英

让我们仔细看看这些内容,并尝试理解:

  1. 即使南岸是指一个方向,但它不能算作命名实体,因为我们无法仅凭此唯一确定对象。

  2. 即使时尚是一个名词,我们也不能完全将其视为命名实体。

  3. 摩天大楼是一个名词,但摩天大楼有许多可能的含义。

  4. 首席执行官是一个职务,可能由许多人担任。因此,这也不能算作命名实体。

为了更好地理解,让我们从类别的角度来看这些命名实体:

类别命名实体示例
TIMEZONE亚洲/加尔各答,印度标准时间,协调世界时
LOCATION孟买,加尔各答,埃及
RIVERS恒河,雅穆纳河,尼罗河
COSMETICS美宝莲深珊瑚口红,欧莱雅卓越发色染发膏
CURRENCY100 比特币,1.25 印度卢比
DATE2017 年 8 月 17 日,2016 年 2 月 19 日
TIME上午 10:10
PERSON萨蒂亚·纳德拉,杰夫·维纳,比尔·盖茨

使用内置的命名实体识别(NER)

Python NLTK 内置了对命名实体识别NER)的支持。为了使用此功能,首先我们需要回顾一下到目前为止我们做了什么:

  1. 将大文档拆分成句子。

  2. 将句子拆分成单词(或标记)。

  3. 确定句子中的词性。

  4. 从句子中提取连续的单词块(不重叠)。

  5. 根据分块模式为这些单词分配 IOB 标签。

下一个逻辑步骤是进一步扩展算法,以便作为第六步找出命名实体。因此,我们将在这个示例中使用直到第五步的预处理数据。

我们将使用treebank数据来理解命名实体识别(NER)过程。记住,数据已经预先按照 IOB 格式标记。没有训练过程,任何我们在这里看到的算法都无法工作。(所以,这里没有什么魔法!)

为了理解训练过程的重要性,假设我们有这样一个例子:考古部门需要找出哪些印度著名地点在社交网络网站上被提到并被推文,且这些信息使用的是卡纳达语。

假设他们已经在某处获得了数据,并且数据的规模是 TB 甚至 PB 级,如何找出所有这些名字呢?这时我们需要从原始输入中获取一个示例数据集,并进行训练过程,进一步使用这个训练过的数据集来提取卡纳达语中的命名实体。

准备工作

你应该已经安装了 Python,并且安装了nltk库。

如何进行...

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为NER.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的...

代码看起来很简单,对吧?然而,所有的算法都在nltk库中实现。那么,让我们深入了解这个简单的程序是如何提供我们所需结果的。这个指令将nltk库导入到程序中:

import nltk

这三个指令定义了一个名为sampleNE()的新函数。我们从treebank语料库中导入第一个已标记的句子,然后将其传递给nltk.ne_chunk()函数以提取命名实体。此程序的输出包括所有命名实体及其正确的类别:

def sampleNE():
  sent = nltk.corpus.treebank.tagged_sents()[0]
  print(nltk.ne_chunk(sent))

这三个指令定义了一个名为sampleNE2()的新函数。我们从treebank语料库中导入第一个已标记的句子,然后将其传递给nltk.ne_chunk()函数以提取命名实体。此程序的输出包括所有命名实体,但没有正确的类别。如果训练数据集不够准确,无法为命名实体分配正确的类别(如人名、组织、地点等),这非常有帮助:

def sampleNE2():
  sent = nltk.corpus.treebank.tagged_sents()[0]
  print(nltk.ne_chunk(sent, binary=True))

这三条指令将调用之前定义的两个示例函数,并在屏幕上打印结果。

if __name__ == '__main__':
  sampleNE()
  sampleNE2()

创建、反转和使用字典

作为一种通用编程语言,Python 支持许多内置的数据结构。其中,最强大的数据结构之一就是字典。在我们深入了解字典之前,先来看看这些数据结构的用途。简而言之,数据结构帮助程序员存储、检索和遍历存储在这些结构中的数据。每种数据结构都有自己的一组行为和性能优势,程序员应在为特定任务选择数据结构之前理解这些特点。

让我们回到字典。字典的基本用例可以通过一个简单的例子来解释:

All the flights got delayed due to bad weather

我们可以对前面的句子使用词性识别。但是,如果有人问在这个句子中flights的词性是什么,我们应该有一个高效的方式来查找这个词。这就是字典的作用。字典可以看作是一对一的数据映射关系。同样,这个一对一映射是在我们讨论的数据单元的最高抽象层次。如果你是 Python 专家,你也知道如何实现多对多映射。在这个简单的例子中,我们需要像这样的东西:

flights -> Noun
Weather -> Noun

现在让我们回答一个不同的问题。是否可以打印句子中所有名词的单词列表?是的,对于这个问题,我们也将学习如何使用 Python 字典。

准备工作

你需要安装 Python 和nltk库,才能运行这个示例。

如何做……

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为Dictionary.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的……

现在,让我们通过回顾我们迄今为止写的指令,更深入地理解字典。我们正在将nltk库导入程序:

import nltk

我们正在定义一个名为LearningDictionary的新类:

class LearningDictionary():

我们正在为LearningDictionary创建一个构造函数,该函数接受sentence文本作为参数:

def __init__(self, sentence):

该指令使用nltk.word_tokenize()函数将句子分解为单词,并将结果保存在类成员words中:

self.words = nltk.word_tokenize(sentence)

该指令识别words的词性并将结果保存在标记的类成员中:

self.tagged = nltk.pos_tag(self.words)

该指令调用了在类中定义的buildDictionary()函数:

self.buildDictionary()

该指令调用了在类中定义的buildReverseDictionary()函数:

 self.buildReverseDictionary()

该指令定义了一个新的类成员函数,名为buildDictionary()

 def buildDictionary(self):

这条指令在类中初始化一个空的dictionary变量。这两条指令遍历所有的标记过的pos列表元素,然后将每个word作为键,词性(POS)作为值,赋值给dictionary

self.dictionary = {}
for (word, pos) in self.tagged:
  self.dictionary[word] = pos

这条指令定义了另一个类成员函数,名为buildReverseDictionary()

def buildReverseDictionary(self):

这条指令将一个空的dictionary初始化为类成员rdictionary

self.rdictionary = {}

这条指令遍历所有的dictionary键,并将dictionary中的键放入一个名为key的局部变量中:

 for key in self.dictionary.keys():

这条指令提取给定key(词)对应的value(词性),并将其存储在名为value的局部变量中:

value = self.dictionary[key]

这四条指令检查给定的key(词)是否已存在于反向字典变量(rdictionary)中。如果存在,那么我们将当前找到的词添加到列表中。如果没有找到该词,我们会创建一个大小为 1 的新列表,将当前词作为成员:

if value not in self.rdictionary:
  self.rdictionary[value] = [key]
else:
  self.rdictionary[value].append(key)

该函数返回YesNo,取决于给定的词是否在dictionary中找到:

def isWordPresent(self, word):
  return 'Yes' if word in self.dictionary else 'No'

该函数通过查阅dictionary返回给定词的词性。如果未找到该词性,则返回一个特殊值None

def getPOSForWord(self, word):
  return self.dictionary[word] if word in self.dictionary else None

这两条指令定义了一个函数,该函数通过查阅rdictionary(反向字典)返回给定词性(POS)下的所有词。如果未找到该词性,则返回一个特殊值None

def getWordsForPOS(self, pos):
  return self.rdictionary[pos] if pos in self.rdictionary else None

我们定义了一个名为sentence的变量,用来存储我们感兴趣的待解析字符串:

sentence = "All the flights got delayed due to bad weather"

使用sentence作为参数初始化LearningDictionary()类。一旦类对象创建,它会被赋值给学习变量:

learning = LearningDictionary(sentence)

我们创建一个words列表,用来查看我们感兴趣的词的词性。如果你仔细观察,我们已经包括了一些不在句子中的词:

words = ["chair", "flights", "delayed", "pencil", "weather"]

我们创建一个pos列表,用来查看属于这些词性分类的词:

pos = ["NN", "VBS", "NNS"]

这些指令遍历所有的words,一次处理一个word,通过调用对象的isWordPresent()函数检查该word是否在字典中,然后打印其状态。如果该word在字典中存在,那么我们会打印该词的词性(POS):

for word in words:
  status = learning.isWordPresent(word)
  print("Is '{}' present in dictionary ? : '{}'".format(word, status))
  if status is True:
    print("\tPOS For '{}' is '{}'".format(word, learning.getPOSForWord(word)))

在这些指令中,我们遍历所有的pos。我们一次处理一个词,然后使用getWordsForPOS()函数打印出属于该词性(POS)的所有词:

for pword in pos:
  print("POS '{}' has '{}' words".format(pword, learning.getWordsForPOS(pword)))

选择特征集

特征是nltk库中最强大的组件之一。它们代表了语言中的线索,有助于我们轻松标记正在处理的数据。在 Python 术语中,特征表现为字典,字典中的键是标签,值是从输入数据中提取的线索。

假设我们正在处理一些交通部门的数据,并且我们想知道某个给定的车辆号是否属于卡纳塔克邦政府。现在我们对正在处理的数据毫无头绪。那么我们该如何准确地标记这些号码呢?

让我们尝试学习车辆编号如何提供一些线索来帮助理解它们的含义:

车辆编号关于模式的线索
KA-[0-9]{2} [0-9]{2}普通车辆编号
KA-[0-9]{2}-FKSRTC, BMTC 车辆
KA-[0-9]{2}-G政府车辆

使用这些线索(特征),让我们尝试编写一个简单的程序,告诉我们给定输入编号的分类。

准备就绪

你应该已经安装了 Python,并且安装了nltk库。

如何操作...

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为Features.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的...

现在,让我们看看程序的表现。这两条指令将nltkrandom库导入当前程序:

import nltk
import random

我们正在定义一个 Python 元组的列表,其中元组的第一个元素是车辆编号,第二个元素是预定义的标签,应用于该编号。

这些指令定义了所有编号被分类为三种标签——rtcgov,和oth

sampledata = [
  ('KA-01-F 1034 A', 'rtc'),
  ('KA-02-F 1030 B', 'rtc'),
  ('KA-03-FA 1200 C', 'rtc'),
  ('KA-01-G 0001 A', 'gov'),
  ('KA-02-G 1004 A', 'gov'),
  ('KA-03-G 0204 A', 'gov'),
  ('KA-04-G 9230 A', 'gov'),
  ('KA-27 1290', 'oth')
]

这条指令将sampledata列表中的所有数据打乱,以确保算法不会因为输入序列中元素的顺序而产生偏差:

random.shuffle(sampledata)

这些是我们感兴趣的测试车辆编号,目的是查找其类别:

testdata = [
  'KA-01-G 0109',
  'KA-02-F 9020 AC',
  'KA-02-FA 0801',
  'KA-01 9129'
]

这条指令定义了一个名为learnSimpleFeatures的新函数:

def learnSimpleFeatures():

这些指令定义了一个新函数vehicleNumberFeature(),它接受车辆编号并返回该编号中的第七个字符。返回类型为dictionary

def vehicleNumberFeature(vnumber):
  return {'vehicle_class': vnumber[6]}

这条指令创建了一个特征元组的列表,其中元组的第一个成员是特征字典,第二个成员是数据的标签。执行此指令后,sampledata中的输入车辆编号将不再可见。这是需要记住的关键点之一:

featuresets = [(vehicleNumberFeature(vn), cls) for (vn, cls) in sampledata]

这条指令使用特征字典和应用于featuresets的标签来训练NaiveBayesClassifier。结果保存在分类器对象中,我们将进一步使用它:

classifier = nltk.NaiveBayesClassifier.train(featuresets)

这些指令遍历测试数据,然后打印通过vehicleNumberFeature分类得到的标签。仔细观察输出,你会看到我们编写的特征提取函数在正确标记数字方面表现不佳:

for num in testdata:
  feature = vehicleNumberFeature(num)
  print("(simple) %s is of type %s" %(num, classifier.classify(feature)))

这条指令定义了一个名为learnFeatures的新函数:

def learnFeatures():

这些指令定义了一个新函数vehicleNumberFeature,它返回包含两个键的特征字典。一个键vehicle_class返回字符串中位置为6的字符,vehicle_prev返回位置为5的字符。这些线索对于确保我们消除数据错误标签非常重要:

def vehicleNumberFeature(vnumber):
  return {
    'vehicle_class': vnumber[6],
    'vehicle_prev': vnumber[5]
  }

该指令通过迭代所有输入的训练数据,创建一个featuresets列表和输入标签。和之前一样,原始输入的车辆编号在这里已经不再出现:

featuresets = [(vehicleNumberFeature(vn), cls) for (vn, cls) in sampledata]

该指令在featuresets上创建了一个NaiveBayesClassifier.train()函数,并返回该对象供将来使用:

classifier = nltk.NaiveBayesClassifier.train(featuresets)

这些指令遍历testdata并根据训练好的数据集打印输入的车辆号码的分类情况。在这里,如果仔细观察,你会发现假阳性已经没有了:

for num in testdata:
  feature = vehicleNumberFeature(num)
  print("(dual) %s is of type %s" %(num, classifier.classify(feature)))

调用这两个函数并打印结果到屏幕上。

learnSimpleFeatures()
learnFeatures()

如果我们仔细观察,会发现第一个函数的结果有一个假阳性,无法识别gov车辆。这就是第二个函数表现良好的地方,因为它有更多特征来提高准确性。

使用分类进行句子分割

支持问号(?)、句号(.)和感叹号(!)的自然语言在识别一个陈述是否已经结束,或在标点符号后是否还会继续时给我们带来了挑战。

这是一个经典的待解决问题。

为了解决这个问题,让我们找出可以利用的特征(或线索),以便创建一个分类器,然后使用该分类器在大文本中提取句子。

如果我们遇到像.这样的标点符号,则表示句子结束。如果我们遇到像.这样的标点符号,并且下一个单词的首字母是大写字母,那么它也表示句子结束。

让我们尝试使用这两个特征编写一个简单的分类器来标记句子。

准备就绪

你应该已经安装了 Python,并且安装了nltk库。

如何操作...

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为Segmentation.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的...

该指令将nltk库导入到程序中:

import nltk

这个函数定义了一个修改过的特征提取器,它返回一个包含特征字典的元组,并通过TrueFalse来告诉我们该特征是否表示句子边界:

def featureExtractor(words, i):
    return ({'current-word': words[i], 'next-is-upper': words[i+1][0].isupper()}, words[i+1][0].isupper())

这个函数以sentence作为输入,并返回一个featuresets列表,featuresets是一个包含特征字典和TrueFalse的元组列表:

def getFeaturesets(sentence):
  words = nltk.word_tokenize(sentence)
  featuresets = [featureExtractor(words, i) for i in range(1, len(words) - 1) if words[i] == '.']
  return featuresets

这个函数接收输入文本,将其拆分为单词,然后遍历列表中的每个单词。一旦遇到句号,它会调用classifier来判断是否遇到了句子结束。如果classifier返回True,则表示句子结束,我们继续处理输入中的下一个单词。这个过程会对输入中的所有单词重复:

def segmentTextAndPrintSentences(data):
  words = nltk.word_tokenize(data)
  for i in range(0, len(words) - 1):
    if words[i] == '.':
      if classifier.classify(featureExtractor(words, i)[0]) == True:
        print(".")
      else:
        print(words[i], end='')
    else:
      print("{} ".format(words[i]), end='')
    print(words[-1])

这些指令定义了一些变量,用于训练和评估我们的分类器:

# copied the text from https://en.wikipedia.org/wiki/India
traindata = "India, officially the Republic of India (Bhārat Gaṇarājya),[e] is a country in South Asia. it is the seventh-largest country by area, the second-most populous country (with over 1.2 billion people), and the most populous democracy in the world. It is bounded by the Indian Ocean on the south, the Arabian Sea on the southwest, and the Bay of Bengal on the southeast. It shares land borders with Pakistan to the west;[f] China, Nepal, and Bhutan to the northeast; and Myanmar (Burma) and Bangladesh to the east. In the Indian Ocean, India is in the vicinity of Sri Lanka and the Maldives. India's Andaman and Nicobar Islands share a maritime border with Thailand and Indonesia."

testdata = "The Indian subcontinent was home to the urban Indus Valley Civilisation of the 3rd millennium BCE. In the following millennium, the oldest scriptures associated with Hinduism began to be composed. Social stratification, based on caste, emerged in the first millennium BCE, and Buddhism and Jainism arose. Early political consolidations took place under the Maurya and Gupta empires; the later peninsular Middle Kingdoms influenced cultures as far as southeast Asia. In the medieval era, Judaism, Zoroastrianism, Christianity, and Islam arrived, and Sikhism emerged, all adding to the region's diverse culture. Much of the north fell to the Delhi sultanate; the south was united under the Vijayanagara Empire. The economy expanded in the 17th century in the Mughal Empire. In the mid-18th century, the subcontinent came under British East India Company rule, and in the mid-19th under British crown rule. A nationalist movement emerged in the late 19th century, which later, under Mahatma Gandhi, was noted for nonviolent resistance and led to India's independence in 1947."

提取所有来自traindata变量的特征并存储到traindataset中:

traindataset = getFeaturesets(traindata)

traindataset 上训练 NaiveBayesClassifier,以获得作为对象的 classifier

classifier = nltk.NaiveBayesClassifier.train(traindataset)

testdata 调用该函数,并将所有找到的句子作为输出显示在屏幕上:

segmentTextAndPrintSentences(testdata)

分类文档

在这个教程中,我们将学习如何编写一个分类器,用于对文档进行分类。在我们的案例中,我们将分类丰富站点摘要RSS)订阅源。类别列表是预先确定的,这对分类任务非常重要。

在这个信息时代,有大量的文本数据。对于我们人类来说,正确分类所有信息以供进一步使用几乎是不可能的。这时,分类算法帮助我们根据在示例数据上的训练,正确地分类新产生的文档。

准备开始

你需要安装 Python,并且安装 nltkfeedparser 库。

如何操作...

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为 DocumentClassify.py 的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到如下输出:

它是如何工作的...

让我们看看文档分类是如何工作的。将三个库导入程序:

import nltk
import random
import feedparser

该指令定义了一个新字典,包含指向 Yahoo! 体育的两个 RSS 订阅源。它们是预分类的。我们选择这些 RSS 订阅源的原因是数据易于获取,并且已经为我们的示例进行分类:

urls = {
  'mlb': 'https://sports.yahoo.com/mlb/rss.xml',
  'nfl': 'https://sports.yahoo.com/nfl/rss.xml',
}

初始化空字典变量 feedmap,以便在程序终止之前将 RSS 订阅源列表保存在内存中:

feedmap = {}

获取英文的 stopwords 列表并将其存储在 stopwords 变量中:

stopwords = nltk.corpus.stopwords.words('english')

这个函数 featureExtractor() 接收一个单词列表,然后将它们添加到字典中,其中每个键是单词,值为 True。返回的字典即为给定输入 words 的特征:

def featureExtractor(words):
  features = {}
  for word in words:
    if word not in stopwords:
      features["word({})".format(word)] = True
    return features

创建一个空列表,用于存储所有正确标记的 sentences

sentences = []

遍历字典 urls 的所有 keys(),并将每个键存储在名为 category 的变量中:

for category in urls.keys():

下载一个订阅源,并使用 feedparser 模块的 parse() 函数将结果存储在 feedmap[category] 变量中:

feedmap[category] = feedparser.parse(urls[category])

使用 Python 内置的 print 函数在屏幕上显示正在下载的 url

print("downloading {}".format(urls[category]))

遍历所有的 RSS 条目,并将当前条目存储在一个名为 entry 的变量中:

for entry in feedmap[category]['entries']:

将 RSS 订阅源项目的 summary(新闻文本)存入 data 变量:

data = entry['summary']

我们根据空格将 summary 分解成 words,以便可以将它们传递给 nltk 进行特征提取:

words = data.split()

将当前 RSS 订阅源条目中的所有 words 以及它所属的 category 存储在一个元组中:

sentences.append((category, words))

提取所有 sentences 的特征并将其存储在 featuresets 变量中。然后,对该数组进行 shuffle() 操作,使列表中的所有元素都被随机化,以便算法使用:

featuresets = [(featureExtractor(words), category) for category, words in sentences]
random.shuffle(featuresets)

创建两个数据集,一个是trainset,另一个是testset,用于我们的分析:

total = len(featuresets)
off = int(total/2)
trainset = featuresets[off:]
testset = featuresets[:off]

使用NaiveBayesClassifier模块的train()函数,通过trainset数据创建一个classifier

classifier = nltk.NaiveBayesClassifier.train(trainset)

使用testset打印classifier的准确度:

print(nltk.classify.accuracy(classifier, testset))

使用classifier的内置函数打印此数据的有用特征:

classifier.show_most_informative_features(5)

nfl RSS 项中取四个样本条目。尝试根据title标记文档(记住,我们是基于summary进行分类的):

for (i, entry) in enumerate(feedmap['nfl']['entries']):
  if i < 4:
    features = featureExtractor(entry['title'].split())
    category = classifier.classify(features)
    print('{} -> {}'.format(category, entry['title']))

编写一个带有上下文的词性标注器

在之前的例子中,我们已经编写了基于正则表达式的词性标注器,利用单词后缀如eding等检查单词是否属于给定的词性。在英语中,同一个单词可以根据上下文扮演不同的角色。

例如,单词address可以根据上下文既是名词,也可以是动词:

"What is your address when you're in Bangalore?"
"the president's address on the state of the economy."

让我们尝试编写一个利用特征提取概念来找出句子中单词词性的程序。

准备工作

你应该已经安装了 Python,并且安装了nltk

如何做到这一点…

  1. 打开 Atom 编辑器(或你喜欢的编程编辑器)。

  2. 创建一个名为ContextTagger.py的新文件。

  3. 输入以下源代码:

  1. 保存文件。

  2. 使用 Python 解释器运行程序。

  3. 你将看到以下输出:

它是如何工作的…

让我们看看当前程序是如何工作的。这个指令将nltk库导入到程序中:

import nltk

一些样本字符串,表示单词addresslaugh的双重行为:


sentences = [
  "What is your address when you're in Bangalore?",
  "the president's address on the state of the economy.",
  "He addressed his remarks to the lawyers in the audience.",
  "In order to address an assembly, we should be ready",
  "He laughed inwardly at the scene.",
  "After all the advance publicity, the prizefight turned out to be a laugh.",
  "We can learn to laugh a little at even our most serious foibles."
]

这个函数接受sentence字符串并返回一个包含词汇及其词性标签的列表的列表:


def getSentenceWords():
  sentwords = []
  for sentence in sentences:
    words = nltk.pos_tag(nltk.word_tokenize(sentence))
    sentwords.append(words)
    return sentwords

为了设立基准并看看标签的准确度有多差,这个函数解释了如何使用UnigramTagger仅通过查看当前单词来打印单词的词性。我们将样本文本输入作为学习。与nltk自带的内置标注器相比,这个tagger的表现非常差。但这只是为了帮助我们理解:


def noContextTagger():
  tagger = nltk.UnigramTagger(getSentenceWords())
  print(tagger.tag('the little remarks towards assembly are laughable'.split()))

定义一个名为withContextTagger()的新函数:


def withContextTagger():

这个函数对给定的一组单词进行特征提取,并返回一个字典,包含当前单词和前一个单词的最后三个字符信息:

def wordFeatures(words, wordPosInSentence):
  # extract all the ing forms etc
  endFeatures = {
    'last(1)': words[wordPosInSentence][-1],
    'last(2)': words[wordPosInSentence][-2:],
    'last(3)': words[wordPosInSentence][-3:],
  }
  # use previous word to determine if the current word is verb or noun
  if wordPosInSentence > 1:
    endFeatures['prev'] = words[wordPosInSentence - 1]
  else:
    endFeatures['prev'] = '|NONE|'
    return endFeatures

我们正在构建一个featuredata列表。它包含featurelisttag成员的元组,我们将使用它们通过NaiveBayesClassifier进行分类:

allsentences = getSentenceWords()
featureddata = []
for sentence in allsentences:
  untaggedSentence = nltk.tag.untag(sentence)
  featuredsentence = [(wordFeatures(untaggedSentence, index), tag) for index, (word, tag) in enumerate(sentence)]
  featureddata.extend(featuredsentence)

我们将 50%的数据用于训练,另外 50%的特征提取数据用于测试分类器:

breakup = int(len(featureddata) * 0.5)
traindata = featureddata[breakup:]
testdata = featureddata[:breakup]

这个指令使用训练数据创建classifier

classifier = nltk.NaiveBayesClassifier.train(traindata)

这个指令使用testdata打印分类器的准确度:

print("Accuracy of the classifier : {}".format(nltk.classify.accuracy(classifier, testdata)))

这两个函数打印前面两个函数计算的结果。

noContextTagger()
withContextTagger()