使用NLTK中的Stanford NER标注器提取人员和组织机构列表

27
我正在尝试使用Python NLTK中的Stanford命名实体识别器(NER)提取人物和组织列表。 当我运行时:

代码

from nltk.tag.stanford import NERTagger
st = NERTagger('/usr/share/stanford-ner/classifiers/all.3class.distsim.crf.ser.gz',
               '/usr/share/stanford-ner/stanford-ner.jar') 
r=st.tag('Rami Eid is studying at Stony Brook University in NY'.split())
print(r) 

输出结果为:

[('Rami', 'PERSON'), ('Eid', 'PERSON'), ('is', 'O'), ('studying', 'O'),
('at', 'O'), ('Stony', 'ORGANIZATION'), ('Brook', 'ORGANIZATION'),
('University', 'ORGANIZATION'), ('in', 'O'), ('NY', 'LOCATION')]

我想要的是从这个列表中提取出所有以此形式表示的人员和组织:

Rami Eid
Sony Brook University

我尝试遍历元组列表:
for x,y in i:
        if y == 'ORGANIZATION':
            print(x)

但是这段代码只会将每个实体单独显示在一行:

Sony 
Brook 
University

在真实数据中,一个句子中可能会涉及多个组织和人员,我应该如何将它们区分开来?


这可能会有所帮助:https://dev59.com/v2Yr5IYBdhLWcg3wVYsg - Vaulstein
我正在使用Python(NLTK),而不是Java。但也许这可以帮助我解决问题。 - user1680859
1
感谢您提供这个宝贵的答案,但目前我正在寻找如何将单词添加到“PERSON”实体。 - Saravanan Nandhan
尝试这个:https://stackoverflow.com/questions/36668340/unable-to-use-stanford-ner-in-python-module/56016219#56016219 - Sunku Vamsi Tharun Kumar
6个回答

30

感谢@Vaulstein发现的链接,清楚地表明训练有素的斯坦福标注器,在分发时(至少在2012年)不会分块命名实体。来自接受的答案

许多NER系统使用更复杂的标签,如IOB标签,其中代码如B-PERS表示人物实体的起点。CRFClassifier类和特征工厂支持此类标签,但它们未在我们当前分发的模型中使用(截至2012年)

您有以下选项:

  1. 收集具有相同标记的单词序列;例如,所有相邻的标记为PERSON的单词应该作为一个命名实体一起处理。这很容易,但当然有时会合并不同的命名实体。(例如,New York, Boston [and] Baltimore是关于三个城市,而不是一个城市。)编辑:这就是Alvas在被接受的答案中所做的事情。请参见下面的更简单的实现。

  2. 使用nltk.ne_chunk()。它不使用Stanford识别器,但它确实对实体进行分块。(它是一个IOB命名实体标注器的包装器)。

  3. 找出一种方法,在Stanford标记器返回的结果之上进行自己的分块。

  4. 为您感兴趣的领域训练自己的IOB命名实体分块器(使用Stanford工具或NLTK框架)。如果您有时间和资源来正确完成这项工作,它可能会给您带来最好的结果。

编辑:如果你只想提取连续的命名实体序列(选项1),你应该使用itertools.groupby

from itertools import groupby
for tag, chunk in groupby(netagged_words, lambda x:x[1]):
    if tag != "O":
        print("%-12s"%tag, " ".join(w for w, t in chunk))

如果netagged_words是您问题中的(word, type)元组列表,则会产生以下结果:
PERSON       Rami Eid
ORGANIZATION Stony Brook University
LOCATION     NY

请注意,如果两个相同类型的命名实体紧挨着出现,此方法将会将它们合并。例如New York, Boston [and] Baltimore指的是三个城市,而不是一个。

我尝试了nltk chunker,但它的结果并不是最好的。Stanford recognizer 的结果非常好,但我必须解决多术语实体的问题。 - user1680859
2
我理解这一点;否则我也不会费心提及其他的选择了。 - alexis
@alexis 这个 "%-12s"% 是什么意思?这是正则表达式吗? - sharp
@sharp,不是用来对齐不同长度单词的。请参考 https://docs.python.org/3/library/stdtypes.html#old-string-formatting - alexis
属性错误:模块“nltk”没有属性“ne_recognize”。 - dataninsight
1
@DS_ShraShetty,看起来nltk接口在过去的七年中已经改变了,真是个震惊。'ne_recognize'已经被重命名为'ne_chunk',感谢您的提醒。 - alexis

27

IOB/BIO意味着Inside, Outside, Beginning(IOB),有时也称为Beginning, Inside, Outside(BIO)

斯坦福NE标记器以IOB/BIO风格返回标记,例如:

[('Rami', 'PERSON'), ('Eid', 'PERSON'), ('is', 'O'), ('studying', 'O'),
('at', 'O'), ('Stony', 'ORGANIZATION'), ('Brook', 'ORGANIZATION'),
('University', 'ORGANIZATION'), ('in', 'O'), ('NY', 'LOCATION')]

('Rami', 'PERSON'), ('Eid', 'PERSON') 被标记为 PERSON,"Rami" 是 NE 块的开头,而 "Eid" 是内部。然后你会看到任何非-NE 都会被标记为 "O"。

提取连续的 NE 块的想法与使用正则表达式进行命名实体识别:NLTK非常相似,但由于 Stanford NE 块分析器 API 不返回易于解析的树形结构,因此您需要执行以下操作:

def get_continuous_chunks(tagged_sent):
    continuous_chunk = []
    current_chunk = []

    for token, tag in tagged_sent:
        if tag != "O":
            current_chunk.append((token, tag))
        else:
            if current_chunk: # if the current chunk is not empty
                continuous_chunk.append(current_chunk)
                current_chunk = []
    # Flush the final current_chunk into the continuous_chunk, if any.
    if current_chunk:
        continuous_chunk.append(current_chunk)
    return continuous_chunk

ne_tagged_sent = [('Rami', 'PERSON'), ('Eid', 'PERSON'), ('is', 'O'), ('studying', 'O'), ('at', 'O'), ('Stony', 'ORGANIZATION'), ('Brook', 'ORGANIZATION'), ('University', 'ORGANIZATION'), ('in', 'O'), ('NY', 'LOCATION')]

named_entities = get_continuous_chunks(ne_tagged_sent)
named_entities = get_continuous_chunks(ne_tagged_sent)
named_entities_str = [" ".join([token for token, tag in ne]) for ne in named_entities]
named_entities_str_tag = [(" ".join([token for token, tag in ne]), ne[0][1]) for ne in named_entities]

print named_entities
print
print named_entities_str
print
print named_entities_str_tag
print

[输出]:
[[('Rami', 'PERSON'), ('Eid', 'PERSON')], [('Stony', 'ORGANIZATION'), ('Brook', 'ORGANIZATION'), ('University', 'ORGANIZATION')], [('NY', 'LOCATION')]]

['Rami Eid', 'Stony Brook University', 'NY']

[('Rami Eid', 'PERSON'), ('Stony Brook University', 'ORGANIZATION'), ('NY', 'LOCATION')]

请注意以下限制:如果两个命名实体是连续的,那么可能会出错,尽管我仍然想不出任何在它们之间没有任何“O”的情况下两个命名实体是连续的例子。


正如@alexis所建议的那样,最好将Stanford NE输出转换为NLTK树:

from nltk import pos_tag
from nltk.chunk import conlltags2tree
from nltk.tree import Tree

def stanfordNE2BIO(tagged_sent):
    bio_tagged_sent = []
    prev_tag = "O"
    for token, tag in tagged_sent:
        if tag == "O": #O
            bio_tagged_sent.append((token, tag))
            prev_tag = tag
            continue
        if tag != "O" and prev_tag == "O": # Begin NE
            bio_tagged_sent.append((token, "B-"+tag))
            prev_tag = tag
        elif prev_tag != "O" and prev_tag == tag: # Inside NE
            bio_tagged_sent.append((token, "I-"+tag))
            prev_tag = tag
        elif prev_tag != "O" and prev_tag != tag: # Adjacent NE
            bio_tagged_sent.append((token, "B-"+tag))
            prev_tag = tag

    return bio_tagged_sent


def stanfordNE2tree(ne_tagged_sent):
    bio_tagged_sent = stanfordNE2BIO(ne_tagged_sent)
    sent_tokens, sent_ne_tags = zip(*bio_tagged_sent)
    sent_pos_tags = [pos for token, pos in pos_tag(sent_tokens)]

    sent_conlltags = [(token, pos, ne) for token, pos, ne in zip(sent_tokens, sent_pos_tags, sent_ne_tags)]
    ne_tree = conlltags2tree(sent_conlltags)
    return ne_tree

ne_tagged_sent = [('Rami', 'PERSON'), ('Eid', 'PERSON'), ('is', 'O'), 
('studying', 'O'), ('at', 'O'), ('Stony', 'ORGANIZATION'), 
('Brook', 'ORGANIZATION'), ('University', 'ORGANIZATION'), 
('in', 'O'), ('NY', 'LOCATION')]

ne_tree = stanfordNE2tree(ne_tagged_sent)

print ne_tree

[out]:

  (S
  (PERSON Rami/NNP Eid/NNP)
  is/VBZ
  studying/VBG
  at/IN
  (ORGANIZATION Stony/NNP Brook/NNP University/NNP)
  in/IN
  (LOCATION NY/NNP))

然后:

ne_in_sent = []
for subtree in ne_tree:
    if type(subtree) == Tree: # If subtree is a noun chunk, i.e. NE != "O"
        ne_label = subtree.label()
        ne_string = " ".join([token for token, pos in subtree.leaves()])
        ne_in_sent.append((ne_string, ne_label))
print ne_in_sent

[输出]:
[('Rami Eid', 'PERSON'), ('Stony Brook University', 'ORGANIZATION'), ('NY', 'LOCATION')]

如果我有一系列的句子,是调用每个句子的函数更好还是重新定义函数? - user1680859
这取决于您自己的选择。只要您理解了BIO的工作原理以及如何提取BI,从斯坦福输出中获取NE就不应该难了;) - alvas
2
IOB的解释很好,但这并不是“完美的”。如果有两个相同类型的相邻NE(请参见我的答案中的“选项1”),它会过度分块。从问题中我认为OP理解了这一点。 - alexis
1
关于相邻的NEs:发明IOB格式的整个意义在于它确实会发生。CONLL2002语料库有很多这样的情况。(大多是MISC类型的相邻实体,但也有一些其他类型。) - alexis
1
嗯,公平地说,在这个例子中两个术语都指同一个人。但是例如在“玛丽·雪莱的弗兰肯斯坦”中,应该有两个命名实体。 - alexis
显示剩余5条评论

1

并不完全符合主题作者的要求来打印他想要的内容,也许这可以提供一些帮助。

listx = [('Rami', 'PERSON'), ('Eid', 'PERSON'), ('is', 'O'), ('studying', 'O'),
('at', 'O'), ('Stony', 'ORGANIZATION'), ('Brook', 'ORGANIZATION'),
('University', 'ORGANIZATION'), ('in', 'O'), ('NY', 'LOCATION')]


def parser(n, string):
    for i in listx[n]:
        if i == string:
            pass
        else:
            return i

name = parser(0,'PERSON')
lname = parser(1,'PERSON')
org1 = parser(5,'ORGANIZATION')
org2 = parser(6,'ORGANIZATION')
org3 = parser(7,'ORGANIZATION')


print name, lname
print org1, org2, org3

输出将类似于这样。
Rami Eid
Stony Brook University

不错的尝试,但我认为当您想从Stanford NLP的注释数据中提取命名实体时这并不适用。 - alvas
啊,我的错。如果注释是字符串(例如组织或个人等),这应该产生相同的输出;name = parser(0, None)。 - Kanishk Gandharv

1

警告:

即使您获得了这个模型“all.3class.distsim.crf.ser.gz”,也请不要使用它,因为

    第一个原因:

对于这个模型,斯坦福自然语言处理团队已经公开道歉,因为准确度很差。

    第二个原因:

它的准确度很差,因为它是大小写敏感的。

    解决方法

使用名为"english.all.3class.caseless.distsim.crf.ser.gz"的模型。


0
尝试使用“enumerate”方法。
当您将 NER 应用于单词列表时,一旦创建了 (word,type) 元组,就可以使用 enumerate(list) 枚举该列表。 这将为列表中的每个元组分配索引。
因此,当您从列表中提取 PERSON/ORGANISATION/LOCATION 时,它们将附有索引。
1   Hussein
2   Obama
3   II
6   James
7   Naismith
21   Naismith
19   Tony
20   Hinkle
0   Frank
1   Mahan
14   Naismith
0   Naismith
0   Mahan
0   Mahan
0   Naismith

现在可以根据连续的索引筛选出单个名称。

胡塞恩·奥巴马二世, 詹姆斯·内斯密斯, 托尼·汉克, 弗兰克·马汉


0
使用Python中的pycorenlp包装器,然后使用“entitymentions”作为关键字来获取单个字符串中人员或组织的连续块。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接