Многосрочные именованные объекты в именном реестре
Я использую именный указатель 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());
}
}
}
Однако проблема, которую я нахожу, - это идентификация имен и фамилий. Если распознаватель встречает "Джо Смит", он возвращает "Джо" и "Смит" отдельно. Мне бы очень хотелось, чтобы он вернулся "Джо Смит" в качестве одного термина.
Может ли это быть достигнуто через распознаватель, возможно, через конфигурацию? До сих пор я ничего не нашел в джавадоке.
Спасибо!
Ответы
Ответ 1
Это потому, что ваш внутренний цикл выполняет итерацию над отдельными токенами (словами) и добавляет их отдельно. Вам нужно изменить вещи, чтобы сразу добавить целые имена.
Один из способов - заменить внутренний цикл for регулярным циклом с циклом while внутри него, который принимает смежные функции non-O одного и того же класса и добавляет их как единый объект. *
Другим способом было бы использовать вызов метода CRFClassifier:
List<Triple<String,Integer,Integer>> classifyToCharacterOffsets(String sentences)
который даст вам целые объекты, которые вы можете извлечь форму String, используя substring
на исходном входе.
* В моделях, которые мы распространяем, используется простая схема ярлыков IO, где вещи обозначаются как PERSON или LOCATION, а подходящая вещь - просто объединить соседние маркеры с одной и той же меткой. Многие системы NER используют более сложные метки, такие как метки IOB, где такие коды, как B-PERS, указывают, где начинается человеческое лицо. Классы классов и функций CRFClassifier поддерживают такие метки, но они не используются в моделях, которые мы в настоящее время распространяем (начиная с 2012 года).
Ответ 2
Друг метода classifyToCharacterOffsets заключается в том, что (AFAIK) вы не можете получить доступ к метке сущностей.
Как было предложено Кристофером, вот пример цикла, который собирает "смежные вещи, отличные от O". В этом примере также подсчитывается количество вхождений.
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;
}
Ответ 3
У меня была та же проблема, поэтому я тоже посмотрел. Метод, предложенный Кристофером Мэннингем, эффективен, но деликатный момент состоит в том, чтобы знать, как решить, какой тип разделителя подходит. Можно сказать, что должно быть разрешено только пространство, например. "Джон Зорн" → одна сущность. Однако я могу найти форму "Я. Зорн", поэтому я также должен допускать определенные знаки препинания. Но как насчет "Джека, Джеймса и Джо"? Я мог бы получить 2 сущности вместо 3 ( "Джек Джеймс" и "Джо" ).
Копаясь немного в классах Стэнфордского НЭР, я действительно нашел правильную реализацию этой идеи. Они используют его для экспорта объектов в виде отдельных объектов 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
(класс "O" ). Они также используют свойство BeforeAnnotation
, которое представляет строку, отделяющую текущее слово от предыдущего. Эта последняя точка позволяет решить возникшую ранее проблему, касающуюся выбора соответствующего разделителя.
Ответ 4
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 + ' ');
}
Я написал небольшую логику, и она отлично работает. то, что я сделал, это групповые слова с одинаковой меткой, если они смежны.
Ответ 5
Код для вышесказанного:
<List> result = classifier.classifyToCharacterOffsets(text);
for (Triple<String, Integer, Integer> triple : result)
{
System.out.println(triple.first + " : " + text.substring(triple.second, triple.third));
}
Ответ 6
Используйте уже предоставленные вам классификаторы. Я считаю, что это то, что вы ищете:
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);
}
Ответ 7
Вот мой полный код, я использую NLP для ядра Stanford и алгоритм записи для объединения имен 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;
}
}
Ответ 8
Другой подход к работе с многословными сущностями.
Этот код объединяет несколько токенов вместе, если они имеют одну и ту же аннотацию и идут в строке.
Ограничение:
Если тот же токен имеет две разные аннотации, последний будет сохранен.
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;