Имена с несколькими терминами в Stanford Named Entity Recognizer
Я использую Stanford Named Entity Recognizer http://nlp.stanford.edu/software/CRF-NER.shtml и он работает нормально. Это
List<List<CoreLabel>> out = classifier.classify(text);
for (List<CoreLabel> sentence : out) {
for (CoreLabel word : sentence) {
if (!StringUtils.equals(word.get(AnswerAnnotation.class), "O")) {
namedEntities.add(word.word().trim());
}
}
}
Однако проблема, которую я нахожу, заключается в определении имен и фамилий. Если распознаватель встречает "Джо Смит", он возвращает "Джо" и "Смит" отдельно. Мне бы очень хотелось, чтобы слово "Джо Смит" возвращалось как один термин.
Может ли это быть достигнуто через распознаватель, может быть, через конфигурацию? Я не нашел ничего в Javadoc до сих пор.
Спасибо!
8 ответов
Это потому, что ваш внутренний цикл for выполняет итерации по отдельным токенам (словам) и добавляет их отдельно. Вы должны изменить вещи, чтобы добавить целые имена сразу.
Один из способов состоит в том, чтобы заменить внутренний цикл for обычным циклом for с циклом while внутри него, который принимает смежные не-O вещи того же класса и добавляет их как единый объект.*
Другой способ - использовать вызов метода CRFClassifier:
List<Triple<String,Integer,Integer>> classifyToCharacterOffsets(String sentences)
который даст вам целые сущности, которые вы можете извлечь строковую форму с помощью substring
на оригинальном входе.
* Модели, которые мы распространяем, используют простую схему необработанных меток ввода-вывода, в которой вещи помечены как ЛИЦО или МЕСТО, и целесообразно просто объединить смежные токены с одной меткой. Многие системы NER используют более сложные метки, такие как метки IOB, где коды, такие как B-PERS, указывают, где начинается сущность человека. Классы CRFClassifier и фабрики объектов поддерживают такие метки, но они не используются в моделях, которые мы распространяем в настоящее время (по состоянию на 2012 год).
Аналог метода classifyToCharacterOffsets заключается в том, что (AFAIK) вы не можете получить доступ к метке сущностей.
По предложению Кристофера, вот пример цикла, который собирает "смежные не-вещи". В этом примере также подсчитывается количество вхождений.
public HashMap<String, HashMap<String, Integer>> extractEntities(String text){
HashMap<String, HashMap<String, Integer>> entities =
new HashMap<String, HashMap<String, Integer>>();
for (List<CoreLabel> lcl : classifier.classify(text)) {
Iterator<CoreLabel> iterator = lcl.iterator();
if (!iterator.hasNext())
continue;
CoreLabel cl = iterator.next();
while (iterator.hasNext()) {
String answer =
cl.getString(CoreAnnotations.AnswerAnnotation.class);
if (answer.equals("O")) {
cl = iterator.next();
continue;
}
if (!entities.containsKey(answer))
entities.put(answer, new HashMap<String, Integer>());
String value = cl.getString(CoreAnnotations.ValueAnnotation.class);
while (iterator.hasNext()) {
cl = iterator.next();
if (answer.equals(
cl.getString(CoreAnnotations.AnswerAnnotation.class)))
value = value + " " +
cl.getString(CoreAnnotations.ValueAnnotation.class);
else {
if (!entities.get(answer).containsKey(value))
entities.get(answer).put(value, 0);
entities.get(answer).put(value,
entities.get(answer).get(value) + 1);
break;
}
}
if (!iterator.hasNext())
break;
}
}
return entities;
}
У меня была такая же проблема, поэтому я тоже посмотрел ее. Метод, предложенный Кристофером Мэннингом, эффективен, но тонким моментом является знание того, как решить, какой тип сепаратора является подходящим. Можно сказать, что должен быть разрешен только пробел, например, "Джон Цорн" >> одна сущность. Тем не менее, я могу найти форму "J.Zorn", поэтому я должен также разрешить некоторые знаки препинания. Но как насчет "Джека, Джеймса и Джо"? Я мог бы получить 2 лица вместо 3 ("Джек Джеймс" и "Джо").
Немного покопавшись в классах NER Стэнфорда, я действительно нашел правильную реализацию этой идеи. Они используют его для экспорта объектов в форме единого String
объекты. Например, в методе PlainTextDocumentReaderAndWriter.printAnswersTokenizedInlineXML
, у нас есть:
private void printAnswersInlineXML(List<IN> doc, PrintWriter out) {
final String background = flags.backgroundSymbol;
String prevTag = background;
for (Iterator<IN> wordIter = doc.iterator(); wordIter.hasNext();) {
IN wi = wordIter.next();
String tag = StringUtils.getNotNullString(wi.get(AnswerAnnotation.class));
String before = StringUtils.getNotNullString(wi.get(BeforeAnnotation.class));
String current = StringUtils.getNotNullString(wi.get(CoreAnnotations.OriginalTextAnnotation.class));
if (!tag.equals(prevTag)) {
if (!prevTag.equals(background) && !tag.equals(background)) {
out.print("</");
out.print(prevTag);
out.print('>');
out.print(before);
out.print('<');
out.print(tag);
out.print('>');
} else if (!prevTag.equals(background)) {
out.print("</");
out.print(prevTag);
out.print('>');
out.print(before);
} else if (!tag.equals(background)) {
out.print(before);
out.print('<');
out.print(tag);
out.print('>');
}
} else {
out.print(before);
}
out.print(current);
String afterWS = StringUtils.getNotNullString(wi.get(AfterAnnotation.class));
if (!tag.equals(background) && !wordIter.hasNext()) {
out.print("</");
out.print(tag);
out.print('>');
prevTag = background;
} else {
prevTag = tag;
}
out.print(afterWS);
}
}
Они перебирают каждое слово, проверяя, имеет ли оно тот же класс (ответ), что и предыдущий, как объяснялось ранее. Для этого они используют преимущества того факта, что выражения, рассматриваемые как не являющиеся объектами, помечаются с использованием так называемых backgroundSymbol
(класс "О"). Они также используют собственность BeforeAnnotation
, которая представляет собой строку, отделяющую текущее слово от предыдущего. Этот последний пункт позволяет решить проблему, которую я изначально поднял, в отношении выбора подходящего разделителя.
Код для выше:
<List> result = classifier.classifyToCharacterOffsets(text);
for (Triple<String, Integer, Integer> triple : result)
{
System.out.println(triple.first + " : " + text.substring(triple.second, triple.third));
}
List<List<CoreLabel>> out = classifier.classify(text);
for (List<CoreLabel> sentence : out) {
String s = "";
String prevLabel = null;
for (CoreLabel word : sentence) {
if(prevLabel == null || prevLabel.equals(word.get(CoreAnnotations.AnswerAnnotation.class)) ) {
s = s + " " + word;
prevLabel = word.get(CoreAnnotations.AnswerAnnotation.class);
}
else {
if(!prevLabel.equals("O"))
System.out.println(s.trim() + '/' + prevLabel + ' ');
s = " " + word;
prevLabel = word.get(CoreAnnotations.AnswerAnnotation.class);
}
}
if(!prevLabel.equals("O"))
System.out.println(s + '/' + prevLabel + ' ');
}
Я просто написал небольшую логику, и она работает нормально. то, что я сделал, это сгруппировал слова с одинаковыми метками, если они смежные.
Используйте уже предоставленные вам классификаторы. Я считаю, что это то, что вы ищете:
private static String combineNERSequence(String text) {
String serializedClassifier = "edu/stanford/nlp/models/ner/english.all.3class.distsim.crf.ser.gz";
AbstractSequenceClassifier<CoreLabel> classifier = null;
try {
classifier = CRFClassifier
.getClassifier(serializedClassifier);
} catch (ClassCastException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(classifier.classifyWithInlineXML(text));
// FOR TSV FORMAT //
//System.out.print(classifier.classifyToString(text, "tsv", false));
return classifier.classifyWithInlineXML(text);
}
Другой подход к работе с несколькими словами. Этот код объединяет несколько токенов, если они имеют одну и ту же аннотацию и идут подряд.
Ограничение:
Если один и тот же токен содержит две разные аннотации, последняя будет сохранена.
private Document getEntities(String fullText) {
Document entitiesList = new Document();
NERClassifierCombiner nerCombClassifier = loadNERClassifiers();
if (nerCombClassifier != null) {
List<List<CoreLabel>> results = nerCombClassifier.classify(fullText);
for (List<CoreLabel> coreLabels : results) {
String prevLabel = null;
String prevToken = null;
for (CoreLabel coreLabel : coreLabels) {
String word = coreLabel.word();
String annotation = coreLabel.get(CoreAnnotations.AnswerAnnotation.class);
if (!"O".equals(annotation)) {
if (prevLabel == null) {
prevLabel = annotation;
prevToken = word;
} else {
if (prevLabel.equals(annotation)) {
prevToken += " " + word;
} else {
prevLabel = annotation;
prevToken = word;
}
}
} else {
if (prevLabel != null) {
entitiesList.put(prevToken, prevLabel);
prevLabel = null;
}
}
}
}
}
return entitiesList;
}
Импорт:
Document: org.bson.Document;
NERClassifierCombiner: edu.stanford.nlp.ie.NERClassifierCombiner;
Вот мой полный код, я использую ядро Stanford NLP и пишу алгоритм для объединения имен Multi Term.
import edu.stanford.nlp.ling.CoreAnnotations;
import edu.stanford.nlp.ling.CoreLabel;
import edu.stanford.nlp.pipeline.Annotation;
import edu.stanford.nlp.pipeline.StanfordCoreNLP;
import edu.stanford.nlp.util.CoreMap;
import org.apache.log4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Created by Chanuka on 8/28/14 AD.
*/
public class FindNameEntityTypeExecutor {
private static Logger logger = Logger.getLogger(FindNameEntityTypeExecutor.class);
private StanfordCoreNLP pipeline;
public FindNameEntityTypeExecutor() {
logger.info("Initializing Annotator pipeline ...");
Properties props = new Properties();
props.setProperty("annotators", "tokenize, ssplit, pos, lemma, ner");
pipeline = new StanfordCoreNLP(props);
logger.info("Annotator pipeline initialized");
}
List<String> findNameEntityType(String text, String entity) {
logger.info("Finding entity type matches in the " + text + " for entity type, " + entity);
// create an empty Annotation just with the given text
Annotation document = new Annotation(text);
// run all Annotators on this text
pipeline.annotate(document);
List<CoreMap> sentences = document.get(CoreAnnotations.SentencesAnnotation.class);
List<String> matches = new ArrayList<String>();
for (CoreMap sentence : sentences) {
int previousCount = 0;
int count = 0;
// traversing the words in the current sentence
// a CoreLabel is a CoreMap with additional token-specific methods
for (CoreLabel token : sentence.get(CoreAnnotations.TokensAnnotation.class)) {
String word = token.get(CoreAnnotations.TextAnnotation.class);
int previousWordIndex;
if (entity.equals(token.get(CoreAnnotations.NamedEntityTagAnnotation.class))) {
count++;
if (previousCount != 0 && (previousCount + 1) == count) {
previousWordIndex = matches.size() - 1;
String previousWord = matches.get(previousWordIndex);
matches.remove(previousWordIndex);
previousWord = previousWord.concat(" " + word);
matches.add(previousWordIndex, previousWord);
} else {
matches.add(word);
}
previousCount = count;
}
else
{
count=0;
previousCount=0;
}
}
}
return matches;
}
}