NominalCoref.java
001 /*
002  *  NominalCoref.java
003  *
004  *  Copyright (c) 1995-2012, The University of Sheffield. See the file
005  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
006  *
007  *  This file is part of GATE (see http://gate.ac.uk/), and is free
008  *  software, licenced under the GNU Library General Public License,
009  *  Version 2, June 1991 (in the distribution as file licence.html,
010  *  and also available at http://gate.ac.uk/gate/licence.html).
011  *
012  *  $Id: NominalCoref.java 17616 2014-03-10 16:09:07Z markagreenwood $
013  */
014 
015 package gate.creole.coref;
016 
017 import gate.Annotation;
018 import gate.AnnotationSet;
019 import gate.Document;
020 import gate.FeatureMap;
021 import gate.ProcessingResource;
022 import gate.Resource;
023 import gate.creole.ANNIEConstants;
024 import gate.creole.ExecutionException;
025 import gate.creole.ResourceInstantiationException;
026 import gate.creole.metadata.CreoleParameter;
027 import gate.creole.metadata.CreoleResource;
028 import gate.creole.metadata.Optional;
029 import gate.creole.metadata.RunTime;
030 import gate.util.Err;
031 import gate.util.OffsetComparator;
032 import gate.util.SimpleFeatureMapImpl;
033 
034 import java.util.ArrayList;
035 import java.util.HashMap;
036 import java.util.HashSet;
037 import java.util.Iterator;
038 import java.util.Map;
039 import java.util.Set;
040 
041 @CreoleResource(name="ANNIE Nominal Coreferencer", comment="Nominal Coreference resolution component", helpURL="http://gate.ac.uk/userguide/sec:annie:pronom-coref", icon="nominal-coreferencer")
042 public class NominalCoref extends AbstractCoreferencer
043     implements ProcessingResource, ANNIEConstants {
044 
045   private static final long serialVersionUID = 1497388811557744017L;
046 
047   public static final String COREF_DOCUMENT_PARAMETER_NAME = "document";
048 
049   public static final String COREF_ANN_SET_PARAMETER_NAME = "annotationSetName";
050 
051   //annotation features
052   private static final String PERSON_CATEGORY = "Person";
053   private static final String JOBTITLE_CATEGORY = "JobTitle";
054   private static final String ORGANIZATION_CATEGORY = "Organization";
055   private static final String LOOKUP_CATEGORY = "Lookup";
056   private static final String ORGANIZATION_NOUN_CATEGORY = "organization_noun";
057   
058 
059   //scope
060   /** --- */
061   //private static AnnotationOffsetComparator ANNOTATION_OFFSET_COMPARATOR;
062   /** --- */
063   private String annotationSetName;
064   /** --- */
065   private AnnotationSet defaultAnnotations;
066   /** --- */
067   private HashMap<Annotation,Annotation> anaphor2antecedent;
068 
069     /*  static {
070     ANNOTATION_OFFSET_COMPARATOR = new AnnotationOffsetComparator();
071     }*/
072 
073   /** --- */
074   public NominalCoref() {
075     super("NOMINAL");
076     this.anaphor2antecedent = new HashMap<Annotation,Annotation>();
077   }
078 
079   /** Initialise this resource, and return it. */
080   @Override
081   public Resource init() throws ResourceInstantiationException {
082     return super.init();
083   // init()
084 
085   /**
086    * Reinitialises the processing resource. After calling this method the
087    * resource should be in the state it is after calling init.
088    * If the resource depends on external resources (such as rules files) then
089    * the resource will re-read those resources. If the data used to create
090    * the resource has changed since the resource has been created then the
091    * resource will change too after calling reInit().
092   */
093   @Override
094   public void reInit() throws ResourceInstantiationException {
095     this.anaphor2antecedent = new HashMap<Annotation,Annotation>();
096     init();
097   // reInit()
098 
099 
100   /** Set the document to run on. */
101   @Override
102   public void setDocument(Document newDocument) {
103 
104     //0. precondition
105 //    Assert.assertNotNull(newDocument);
106 
107     super.setDocument(newDocument);
108   }
109 
110   /** --- */
111   @Override
112   @RunTime
113   @Optional
114   @CreoleParameter(comment="The annotation set to be used for the generated annotations")
115   public void setAnnotationSetName(String annotationSetName) {
116     this.annotationSetName = annotationSetName;
117   }
118 
119   /** --- */
120   @Override
121   public String getAnnotationSetName() {
122     return annotationSetName;
123   }
124 
125   /**
126    * This method runs the coreferencer. It assumes that all the needed parameters
127    * are set. If they are not, an exception will be fired.
128    *
129    * The process goes like this:
130    * - Create a sorted list of Person and JobTitle annotations.
131    * - Loop through the annotations
132    *    If it is a Person, we add it to the top of a stack.
133    *    If it is a job title, we subject it to a series of tests. If it 
134    *      passes, we associate it with the Person annotation at the top
135    *      of the stack
136    */
137   @Override
138   public void execute() throws ExecutionException{
139 
140     Annotation[] nominalArray;
141 
142     //0. preconditions
143     if (null == this.document) {
144       throw new ExecutionException("[coreference] Document is not set!");
145     }
146 
147     //1. preprocess
148     preprocess();
149 
150     // Out.println("Total annotations: " + defaultAnnotations.size());
151 
152     // Get a sorted array of Tokens.
153     // The tests for job titles often require getting previous and subsequent
154     // tokens, so to save work, we create a single, sorted list of 
155     // tokens.
156     Annotation[] tokens = defaultAnnotations.get(TOKEN_ANNOTATION_TYPE).
157         toArray(new Annotation[0]);
158     java.util.Arrays.sort(tokens, new OffsetComparator());
159 
160     // The current token is the token at the start of the current annotation.
161     int currentToken = 0;
162 
163     // get Person entities
164     //FeatureMap personConstraint = new SimpleFeatureMapImpl();
165     //personConstraint.put(LOOKUP_MAJOR_TYPE_FEATURE_NAME,
166     //                          PERSON_CATEGORY);
167     Set<String> personConstraint = new HashSet<String>();
168     personConstraint.add(PERSON_CATEGORY);
169     AnnotationSet people =
170       this.defaultAnnotations.get(personConstraint);
171 
172     // get all JobTitle entities
173     //FeatureMap constraintJobTitle = new SimpleFeatureMapImpl();
174     //constraintJobTitle.put(LOOKUP_MAJOR_TYPE_FEATURE_NAME, JOBTITLE_CATEGORY);
175     Set<String> jobTitleConstraint = new HashSet<String>();
176     jobTitleConstraint.add(JOBTITLE_CATEGORY);
177     
178     AnnotationSet jobTitles = 
179       this.defaultAnnotations.get(jobTitleConstraint);
180 
181     FeatureMap orgNounConstraint = new SimpleFeatureMapImpl();
182     orgNounConstraint.put(LOOKUP_MAJOR_TYPE_FEATURE_NAME,
183                           ORGANIZATION_NOUN_CATEGORY);
184     AnnotationSet orgNouns =
185       this.defaultAnnotations.get(LOOKUP_CATEGORY, orgNounConstraint);
186 
187     Set<String> orgConstraint = new HashSet<String>();
188     orgConstraint.add(ORGANIZATION_CATEGORY);
189 
190     AnnotationSet organizations =
191       this.defaultAnnotations.get(orgConstraint);
192 
193     // combine them into a list of nominals
194     Set<Annotation> nominals = new HashSet<Annotation>();
195     if (people != null) {
196       nominals.addAll(people);
197     }
198     if (jobTitles != null) {
199       nominals.addAll(jobTitles);
200     }
201     if (orgNouns != null) {
202       nominals.addAll(orgNouns);
203     }
204     if (organizations != null) {
205       nominals.addAll(organizations);
206     }
207 
208     //  Out.println("total nominals: " + nominals.size());
209 
210     // sort them according to offset
211     nominalArray = nominals.toArray(new Annotation[0]);
212     java.util.Arrays.sort(nominalArray, new OffsetComparator());
213     
214     ArrayList<Annotation> previousPeople = new ArrayList<Annotation>();
215     ArrayList<Annotation> previousOrgs = new ArrayList<Annotation>();
216     
217         
218     // process all nominals
219     for (int i=0; i<nominalArray.length; i++) {
220       Annotation nominal = nominalArray[i];
221       
222       // Find the current place in the tokens array
223       currentToken = advanceTokenPosition(nominal, currentToken, tokens);
224       
225       //Out.print("processing nominal [" + stringValue(nominal) + "] ");
226       
227       if (nominal.getType().equals(PERSON_CATEGORY)) {
228   // Add each Person entity to the beginning of the people list
229   // but don't add pronouns
230   Object[] personTokens = getSortedTokens(nominal);
231     
232   if (personTokens.length == 1) {
233     Annotation personToken = (AnnotationpersonTokens[0];
234     
235     String personCategory = (String
236       personToken.getFeatures().get(TOKEN_CATEGORY_FEATURE_NAME);
237     if (personCategory.equals("PP"||
238         personCategory.equals("PRP"||
239         personCategory.equals("PRP$"||
240         personCategory.equals("PRPR$")) {
241         //Out.println("ignoring personal pronoun");
242         continue;
243     }
244   }
245   
246   previousPeople.add(0, nominal);
247   //Out.println("added person");
248       }
249       else if (nominal.getType().equals(JOBTITLE_CATEGORY)) {
250     
251   // Look into the tokens to get some info about POS.
252   Object[] jobTitleTokens = getSortedTokens(nominal);
253   
254   Annotation lastToken = (Annotation)
255     jobTitleTokens[jobTitleTokens.length - 1];
256   
257   // Don't associate if the job title is not a singular noun
258   String tokenCategory = (String
259     lastToken.getFeatures().get(TOKEN_CATEGORY_FEATURE_NAME);
260   // UNCOMMENT FOR SINGULAR PROPER NOUNS (The President, the Pope)
261   //if (! tokenCategory.equals("NN") &&
262   //! tokenCategory.equals("NNP")) {
263   if (! tokenCategory.equals("NN")) {
264       // Out.println("Not a singular noun");
265     continue;
266   }
267   
268   // Don't associate it if it's part of a Person (eg President Bush)
269   if (overlapsAnnotations(nominal, people)) {
270       //Out.println("overlapping annotation");
271     continue;
272   }
273 
274   Annotation previousToken;
275         String previousValue;
276 
277   // Don't associate it if it's proceeded by a generic marker
278         if (currentToken != 0) {
279           previousToken = tokens[currentToken - 1];
280           previousValue = (String
281       previousToken.getFeatures().get(TOKEN_STRING_FEATURE_NAME);
282           if (previousValue.equalsIgnoreCase("a"||
283               previousValue.equalsIgnoreCase("an"||
284               previousValue.equalsIgnoreCase("other"||
285               previousValue.equalsIgnoreCase("another")) {
286               //Out.println("indefinite");
287       continue;
288           }
289         }
290 
291   // nominals immediately followed by Person annotations:
292   // BAD:
293   //   Chairman Bill Gates               (title)
294   // GOOD:
295   //   secretary of state, Colin Powell  (inverted appositive)
296   //   the home secretary David Blunkett (same but no comma, 
297   //                                      possible in transcriptions)
298   // "the" is a good indicator for apposition
299   
300   // Luckily we have an array of all Person annotations in order...
301   if (i < nominalArray.length - 1) {
302     Annotation nextAnnotation = nominalArray[i+1];
303     if (nextAnnotation.getType().equals(PERSON_CATEGORY)) {
304       // is it preceded by a definite article?
305       previousToken = tokens[currentToken - 1];
306       previousValue = (String
307         previousToken.getFeatures().get(TOKEN_STRING_FEATURE_NAME);
308       
309       // Get all tokens between this and the next person
310       int interveningTokens =
311         countInterveningTokens(nominal, nextAnnotation,
312              currentToken, tokens);
313       if (interveningTokens == && 
314         ! previousValue.equalsIgnoreCase("the")) {
315       
316         // There is nothing between the job title and the person,
317         // like "Chairman Gates" -- do nothing.
318         //Out.println("immediately followed by Person");
319         continue;
320       }
321       else if (interveningTokens == 1) {
322         String tokenString =
323           (StringgetFollowingToken(nominal,
324              currentToken, tokens)
325       .getFeatures().get(TOKEN_STRING_FEATURE_NAME);
326         //Out.print("STRING VALUE [" + tokenString + "] ");
327         if (! tokenString.equals(","&&
328     ! tokenString.equals("-")) {
329     //Out.println("nominal and person separated by NOT [,-]");
330     continue;
331         }
332       }
333       
334       // Did we get through all that? Then we must have an 
335       // apposition.
336       
337       anaphor2antecedent.put(nominal, nextAnnotation);
338       //Out.println("associating with " +
339       //  stringValue(nextAnnotation));
340       continue;
341       
342     }
343   }
344   
345   // If we have no possible antecedents, create a new Person
346   // annotation.
347   if (previousPeople.size() == 0) {
348     FeatureMap personFeatures = new SimpleFeatureMapImpl();
349     personFeatures.put("ENTITY_MENTION_TYPE""NOMINAL");
350     this.defaultAnnotations.add(nominal.getStartNode(),
351               nominal.getEndNode(),
352               PERSON_CATEGORY,
353               personFeatures);
354     //Out.println("creating as new Person");
355     continue;
356   }
357 
358   // Associate this entity with the most recent Person
359   int personIndex = 0;
360   
361   Annotation previousPerson =
362     previousPeople.get(personIndex);
363   
364   // Don't associate if the two nominals are not the same gender
365   String personGender = (String
366     previousPerson.getFeatures().get(PERSON_GENDER_FEATURE_NAME);
367   String jobTitleGender = (String
368           nominal.getFeatures().get(PERSON_GENDER_FEATURE_NAME);
369   if (personGender != null && jobTitleGender != null) {
370           if (! personGender.equals(jobTitleGender)) {
371             //Out.println("wrong gender: " + personGender + " " +
372             //            jobTitleGender);
373       continue;
374     }
375   }
376   
377   //Out.println("associating with " +
378   //  previousPerson.getFeatures()
379   //  .get(TOKEN_STRING_FEATURE_NAME));
380   
381   anaphor2antecedent.put(nominal, previousPerson);
382       }
383       else if (nominal.getType().equals(ORGANIZATION_CATEGORY)) {
384         // Add each organization entity to the beginning of
385   // the organization list
386   previousOrgs.add(0, nominal);
387   //Out.println("added organization");
388       }
389       else if (nominal.getType().equals(LOOKUP_CATEGORY)) {
390   // Don't associate it if we have no organizations
391   if (previousOrgs.size() == 0) {
392     //Out.println("no orgs");
393     continue;
394   }
395     
396   // Look into the tokens to get some info about POS.
397   Annotation[] orgNounTokens =
398     this.defaultAnnotations.get(TOKEN_ANNOTATION_TYPE,
399               nominal.getStartNode().getOffset(),
400               nominal.getEndNode().getOffset()).toArray(new Annotation[0]);
401   java.util.Arrays.sort(orgNounTokens, new OffsetComparator());
402   Annotation lastToken = orgNounTokens[orgNounTokens.length - 1];
403   
404   // Don't associate if the org noun is not a singular noun
405   if (! lastToken.getFeatures().get(TOKEN_CATEGORY_FEATURE_NAME)
406       .equals("NN")) {
407       //Out.println("Not a singular noun");
408       continue;
409   }
410   
411   //Out.println("organization noun");
412   // Associate this entity with the most recent Person
413   anaphor2antecedent.put(nominal, previousOrgs.get(0));
414       }
415     }
416 
417     // This method does the dirty work of actually adding new annotations and
418     // coreferring.
419     generateCorefChains(anaphor2antecedent);
420   }
421 
422   /**
423    * This method specifies whether a given annotation overlaps any of a 
424    * set of annotations. For instance, JobTitles occasionally are
425    * part of Person annotations.
426    
427    */
428   private boolean overlapsAnnotations(Annotation a,
429                                       AnnotationSet annotations) {
430     Iterator<Annotation> iter = annotations.iterator();
431     while (iter.hasNext()) {
432       Annotation current = iter.next();
433       if (a.overlaps(current)) {
434         return true;
435       }
436     }
437       
438     return false;
439   }
440 
441   /** Use this method to keep the current token pointer at the right point
442    * in the token list */
443   private int advanceTokenPosition(Annotation target, int currentPosition,
444            Object[] tokens) {
445     long targetOffset = target.getStartNode().getOffset().longValue();
446     long currentOffset = ((Annotationtokens[currentPosition])
447       .getStartNode().getOffset().longValue();
448     
449     if (targetOffset > currentOffset) {
450       while (targetOffset > currentOffset) {
451   currentPosition++;
452   currentOffset = ((Annotationtokens[currentPosition])
453           .getStartNode().getOffset().longValue();
454       }
455     }
456     else if (targetOffset < currentOffset) {
457       while (targetOffset < currentOffset) {
458   currentPosition--;
459   currentOffset = ((Annotationtokens[currentPosition])
460           .getStartNode().getOffset().longValue();
461       }
462     }
463     
464     return currentPosition;
465   }
466 
467   /** Return the number of tokens between the end of annotation 1 and the
468    * beginning of annotation 2. Will return 0 if they are not in order */
469   private int countInterveningTokens(Annotation first, Annotation second,
470              int currentPosition, Object[] tokens) {
471     int interveningTokens = 0;
472 
473     long startOffset = first.getEndNode().getOffset().longValue();
474     long endOffset = second.getStartNode().getOffset().longValue();
475     
476     long currentOffset = ((Annotationtokens[currentPosition])
477       .getStartNode().getOffset().longValue();
478     
479     while (currentOffset < endOffset) {
480       if (currentOffset >= startOffset) {
481         interveningTokens++;
482       }
483       currentPosition++;
484       currentOffset = ((Annotationtokens[currentPosition])
485   .getStartNode().getOffset().longValue();
486     }
487     return interveningTokens;
488   }
489 
490   /** Get the next token after an annotation */
491   private Annotation getFollowingToken(Annotation current, int currentPosition,
492                Object[] tokens) {
493     long endOffset = current.getEndNode().getOffset().longValue();
494     long currentOffset = ((Annotationtokens[currentPosition])
495       .getStartNode().getOffset().longValue();
496     while (currentOffset < endOffset) {
497       currentPosition++;
498       currentOffset = ((Annotationtokens[currentPosition])
499   .getStartNode().getOffset().longValue();
500     }
501     return (Annotationtokens[currentPosition];
502   }
503   
504   /** Get the text of an annotation */
505   @SuppressWarnings("unused")
506   private String stringValue(Annotation ann) {
507     Object[] tokens = getSortedTokens(ann);
508   
509     StringBuffer output = new StringBuffer();
510     for (int i=0;i<tokens.length;i++) {
511       Annotation token = (Annotationtokens[i];
512       output.append(token.getFeatures().get(TOKEN_STRING_FEATURE_NAME));
513       if (i < tokens.length - 1) {
514         output.append(" ");
515       }
516     }
517     return output.toString();
518   }
519     
520   /** Get a sorted array of the tokens that make up a given annotation. */
521   private Annotation[] getSortedTokens(Annotation a) {
522     Annotation[] annotationTokens =
523       this.defaultAnnotations.get(TOKEN_ANNOTATION_TYPE,
524           a.getStartNode().getOffset(),
525           a.getEndNode().getOffset()).toArray(new Annotation[0]);
526     java.util.Arrays.sort(annotationTokens, new OffsetComparator());
527     return annotationTokens;
528   }
529   
530   /** --- */
531   public Map<Annotation,Annotation> getResolvedAnaphora() {
532     return this.anaphor2antecedent;
533   }
534 
535   /** --- */
536   private void preprocess() throws ExecutionException {
537 
538     //0.5 cleanup
539     this.anaphor2antecedent.clear();
540 
541     //1.get all annotation in the input set
542     if this.annotationSetName == null || this.annotationSetName.equals("")) {
543       this.defaultAnnotations = this.document.getAnnotations();
544     }
545     else {
546       this.defaultAnnotations = this.document.getAnnotations(annotationSetName);
547     }
548 
549     //if none found, print warning and exit
550     if (this.defaultAnnotations == null || this.defaultAnnotations.isEmpty()) {
551       Err.prln("Coref Warning: No annotations found for processing!");
552       return;
553     }
554 
555     /*
556     // initialise the quoted text fragments
557     AnnotationSet sentQuotes = this.defaultAnnotations.get(QUOTED_TEXT_TYPE);
558 
559     //if none then return
560     if (null == sentQuotes) {
561       this.quotedText = new Quote[0];
562     }
563     else {
564       this.quotedText = new Quote[sentQuotes.size()];
565 
566       Object[] quotesArray = sentQuotes.toArray();
567       java.util.Arrays.sort(quotesArray,ANNOTATION_OFFSET_COMPARATOR);
568 
569       for (int i =0; i < quotesArray.length; i++) {
570         this.quotedText[i] = new Quote((Annotation)quotesArray[i],i);
571       }
572     }
573     */
574   }
575 
576 }