3.2 简单的评估和基准

现在,我们可以访问一个已划分词块语料,可以评估词块划分器。我们开始为没有什么意义的词块解析器cp建立一个基准,它不划分任何词块:

  1. >>> from nltk.corpus import conll2000
  2. >>> cp = nltk.RegexpParser("")
  3. >>> test_sents = conll2000.chunked_sents('test.txt', chunk_types=['NP'])
  4. >>> print(cp.evaluate(test_sents))
  5. ChunkParse score:
  6. IOB Accuracy: 43.4%
  7. Precision: 0.0%
  8. Recall: 0.0%
  9. F-Measure: 0.0%

IOB 标记准确性表明超过三分之一的词被标注为O,即没有在NP词块中。然而,由于我们的标注器没有找到 任何 词块,其精度、召回率和 F-度量均为零。现在让我们尝试一个初级的正则表达式词块划分器,查找以名词短语标记的特征字母开头的标记(如CD, DTJJ)。

  1. >>> grammar = r"NP: {<[CDJNP].*>+}"
  2. >>> cp = nltk.RegexpParser(grammar)
  3. >>> print(cp.evaluate(test_sents))
  4. ChunkParse score:
  5. IOB Accuracy: 87.7%
  6. Precision: 70.6%
  7. Recall: 67.8%
  8. F-Measure: 69.2%

正如你看到的,这种方法达到相当好的结果。但是,我们可以采用更多数据驱动的方法改善它,在这里我们使用训练语料找到对每个词性标记最有可能的块标记(I, OB)。换句话说,我们可以使用 一元标注器 4)建立一个词块划分器。但不是尝试确定每个词的正确的词性标记,而是根据每个词的词性标记,尝试确定正确的词块标记。

3.1中,我们定义了UnigramChunker类,使用一元标注器给句子加词块标记。这个类的大部分代码只是用来在 NLTK 的ChunkParserI接口使用的词块树表示和嵌入式标注器使用的 IOB 表示之间镜像转换。类定义了两个方法:一个构造函数[1],当我们建立一个新的 UnigramChunker 时调用;以及parse方法[3],用来给新句子划分词块。

  1. class UnigramChunker(nltk.ChunkParserI):
  2. def __init__(self, train_sents): ![[1]](/projects/nlp-py-2e-zh/Images/f4891d12ae20c39b685951ad3cddf1aa.jpg)
  3. train_data = [[(t,c) for w,t,c in nltk.chunk.tree2conlltags(sent)]
  4. for sent in train_sents]
  5. self.tagger = nltk.UnigramTagger(train_data) ![[2]](/projects/nlp-py-2e-zh/Images/e5fb07e997b9718f18dbf677e3d6634d.jpg)
  6. def parse(self, sentence): ![[3]](/projects/nlp-py-2e-zh/Images/6372ba4f28e69f0b220c75a9b2f4decf.jpg)
  7. pos_tags = [pos for (word,pos) in sentence]
  8. tagged_pos_tags = self.tagger.tag(pos_tags)
  9. chunktags = [chunktag for (pos, chunktag) in tagged_pos_tags]
  10. conlltags = [(word, pos, chunktag) for ((word,pos),chunktag)
  11. in zip(sentence, chunktags)]
  12. return nltk.chunk.conlltags2tree(conlltags)

构造函数[1]需要训练句子的一个列表,这将是词块树的形式。它首先将训练数据转换成适合训练标注器的形式,使用tree2conlltags映射每个词块树到一个word,tag,chunk三元组的列表。然后使用转换好的训练数据训练一个一元标注器,并存储在self.tagger供以后使用。

parse方法[3]接收一个已标注的句子作为其输入,以从那句话提取词性标记开始。它然后使用在构造函数中训练过的标注器self.tagger,为词性标记标注 IOB 词块标记。接下来,它提取词块标记,与原句组合,产生conlltags。最后,它使用conlltags2tree将结果转换成一个词块树。

现在我们有了UnigramChunker,可以使用 CoNLL2000 语料库训练它,并测试其表现:

  1. >>> test_sents = conll2000.chunked_sents('test.txt', chunk_types=['NP'])
  2. >>> train_sents = conll2000.chunked_sents('train.txt', chunk_types=['NP'])
  3. >>> unigram_chunker = UnigramChunker(train_sents)
  4. >>> print(unigram_chunker.evaluate(test_sents))
  5. ChunkParse score:
  6. IOB Accuracy: 92.9%
  7. Precision: 79.9%
  8. Recall: 86.8%
  9. F-Measure: 83.2%

这个分块器相当不错,达到整体 F-度量 83%的得分。让我们来看一看通过使用一元标注器分配一个标记给每个语料库中出现的词性标记,它学到了什么:

  1. >>> postags = sorted(set(pos for sent in train_sents
  2. ... for (word,pos) in sent.leaves()))
  3. >>> print(unigram_chunker.tagger.tag(postags))
  4. [('#', 'B-NP'), ('$', 'B-NP'), ("''", 'O'), ('(', 'O'), (')', 'O'),
  5. (',', 'O'), ('.', 'O'), (':', 'O'), ('CC', 'O'), ('CD', 'I-NP'),
  6. ('DT', 'B-NP'), ('EX', 'B-NP'), ('FW', 'I-NP'), ('IN', 'O'),
  7. ('JJ', 'I-NP'), ('JJR', 'B-NP'), ('JJS', 'I-NP'), ('MD', 'O'),
  8. ('NN', 'I-NP'), ('NNP', 'I-NP'), ('NNPS', 'I-NP'), ('NNS', 'I-NP'),
  9. ('PDT', 'B-NP'), ('POS', 'B-NP'), ('PRP', 'B-NP'), ('PRP$', 'B-NP'),
  10. ('RB', 'O'), ('RBR', 'O'), ('RBS', 'B-NP'), ('RP', 'O'), ('SYM', 'O'),
  11. ('TO', 'O'), ('UH', 'O'), ('VB', 'O'), ('VBD', 'O'), ('VBG', 'O'),
  12. ('VBN', 'O'), ('VBP', 'O'), ('VBZ', 'O'), ('WDT', 'B-NP'),
  13. ('WP', 'B-NP'), ('WP$', 'B-NP'), ('WRB', 'O'), ('``', 'O')]

它已经发现大多数标点符号出现在 NP 词块外,除了两种货币符号#它也发现限定词(DT)和所有格(PRP

建立了一个一元分块器,很容易建立一个二元分块器:我们只需要改变类的名称为BigramChunker,修改3.1[2]构造一个BigramTagger而不是UnigramTagger。由此产生的词块划分器的性能略高于一元词块划分器:

  1. >>> bigram_chunker = BigramChunker(train_sents)
  2. >>> print(bigram_chunker.evaluate(test_sents))
  3. ChunkParse score:
  4. IOB Accuracy: 93.3%
  5. Precision: 82.3%
  6. Recall: 86.8%
  7. F-Measure: 84.5%