Skip to content

Commit

Permalink
#4491 - Support for link feature recommendations
Browse files Browse the repository at this point in the history
- Allow recommenders to generate suggestions for link features
- Added unit tests
  • Loading branch information
reckart committed Feb 4, 2024
1 parent 6019ccd commit f3ab196
Show file tree
Hide file tree
Showing 29 changed files with 1,877 additions and 700 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -563,13 +563,12 @@ private void actionAdd(AjaxRequestTarget aTarget)
}

@SuppressWarnings("unchecked")
List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) LinkFeatureEditor.this
.getModelObject().value;
AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject();
var links = (List<LinkWithRoleModel>) LinkFeatureEditor.this.getModelObject().value;
var state = LinkFeatureEditor.this.stateModel.getObject();

LinkWithRoleModel m = new LinkWithRoleModel();
var m = new LinkWithRoleModel();
m.role = (String) field.getModelObject();
int insertionPoint = findInsertionPoint(links);
var insertionPoint = findInsertionPoint(links);
links.add(insertionPoint, m);
state.setArmedSlot(getModelObject(), insertionPoint);

Expand Down Expand Up @@ -600,12 +599,11 @@ private void actionSet(AjaxRequestTarget aTarget)
}

@SuppressWarnings("unchecked")
List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) LinkFeatureEditor.this
.getModelObject().value;
AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject();
var links = (List<LinkWithRoleModel>) LinkFeatureEditor.this.getModelObject().value;
var state = LinkFeatureEditor.this.stateModel.getObject();

// Update the slot
LinkWithRoleModel m = links.get(state.getArmedSlot());
var m = links.get(state.getArmedSlot());
m.role = (String) field.getModelObject();
links.set(state.getArmedSlot(), m); // avoid reordering

Expand All @@ -623,11 +621,10 @@ private void actionSet(AjaxRequestTarget aTarget)
private void actionDel(AjaxRequestTarget aTarget)
{
@SuppressWarnings("unchecked")
List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) LinkFeatureEditor.this
.getModelObject().value;
AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject();
var links = (List<LinkWithRoleModel>) LinkFeatureEditor.this.getModelObject().value;
var state = LinkFeatureEditor.this.stateModel.getObject();

LinkWithRoleModel linkWithRoleModel = links.get(state.getArmedSlot());
var linkWithRoleModel = links.get(state.getArmedSlot());
links.remove(state.getArmedSlot());
state.clearArmedSlot();

Expand All @@ -638,7 +635,7 @@ private void actionDel(AjaxRequestTarget aTarget)

private void actionToggleArmedState(AjaxRequestTarget aTarget, Item<LinkWithRoleModel> aItem)
{
AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject();
var state = LinkFeatureEditor.this.stateModel.getObject();

if (state.isArmedSlot(getModelObject(), aItem.getIndex())) {
state.clearArmedSlot();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
import de.tudarmstadt.ukp.clarin.webanno.model.Tag;
import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence;
import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token;
import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport;
import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport;
import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport;
import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler;
import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState;
import de.tudarmstadt.ukp.inception.rendering.editorstate.FeatureState;
Expand All @@ -54,7 +57,6 @@
import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupport;
import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureType;
import de.tudarmstadt.ukp.inception.schema.api.feature.LinkWithRoleModel;
import de.tudarmstadt.ukp.inception.support.WebAnnoConst;
import de.tudarmstadt.ukp.inception.support.json.JSONUtil;
import de.tudarmstadt.ukp.inception.support.uima.ICasUtil;

Expand Down Expand Up @@ -92,23 +94,22 @@ public void setBeanName(String aBeanName)
}

@Override
public List<FeatureType> getSupportedFeatureTypes(AnnotationLayer aAnnotationLayer)
public List<FeatureType> getSupportedFeatureTypes(AnnotationLayer aLayer)
{
List<FeatureType> types = new ArrayList<>();
var types = new ArrayList<FeatureType>();

// Slot features are only supported on span layers
if (!WebAnnoConst.CHAIN_TYPE.equals(aAnnotationLayer.getType())
&& !WebAnnoConst.RELATION_TYPE.equals(aAnnotationLayer.getType())) {
if (!ChainLayerSupport.TYPE.equals(aLayer.getType())
&& !RelationLayerSupport.TYPE.equals(aLayer.getType())) {
// Add layers of type SPAN available in the project
for (AnnotationLayer spanLayer : annotationService
.listAnnotationLayer(aAnnotationLayer.getProject())) {
for (var spanLayer : annotationService.listAnnotationLayer(aLayer.getProject())) {

if (Token.class.getName().equals(spanLayer.getName())
|| Sentence.class.getName().equals(spanLayer.getName())) {
continue;
}

if (WebAnnoConst.SPAN_TYPE.equals(spanLayer.getType())) {
if (SpanLayerSupport.TYPE.equals(spanLayer.getType())) {
types.add(new FeatureType(spanLayer.getName(), "Link: " + spanLayer.getUiName(),
featureSupportId));
}
Expand Down Expand Up @@ -189,7 +190,7 @@ public void generateFeature(TypeSystemDescription aTSD, TypeDescription aTD,
AnnotationFeature aFeature)
{
// Link type
TypeDescription linkTD = aTSD.addType(aFeature.getLinkTypeName(), "", CAS.TYPE_NAME_TOP);
var linkTD = aTSD.addType(aFeature.getLinkTypeName(), "", CAS.TYPE_NAME_TOP);
linkTD.addFeature(aFeature.getLinkTypeRoleFeatureName(), "", CAS.TYPE_NAME_STRING);
linkTD.addFeature(aFeature.getLinkTypeTargetFeatureName(), "", aFeature.getType());

Expand All @@ -201,7 +202,7 @@ public void generateFeature(TypeSystemDescription aTSD, TypeDescription aTD,
@Override
public List<LinkWithRoleModel> getFeatureValue(AnnotationFeature aFeature, FeatureStructure aFS)
{
Feature linkFeature = aFS.getType().getFeatureByBaseName(aFeature.getName());
var linkFeature = aFS.getType().getFeatureByBaseName(aFeature.getName());

if (linkFeature == null) {
wrapFeatureValue(aFeature, aFS.getCAS(), null);
Expand All @@ -215,7 +216,7 @@ public void setFeatureValue(CAS aCas, AnnotationFeature aFeature, int aAddress,
throws AnnotationException
{
if (aValue instanceof List && aFeature.getTagset() != null) {
for (LinkWithRoleModel link : (List<LinkWithRoleModel>) aValue) {
for (var link : (List<LinkWithRoleModel>) aValue) {
if (!annotationService.existsTag(link.role, aFeature.getTagset())) {
if (!aFeature.getTagset().isCreateTag()) {
throw new IllegalArgumentException("[" + link.role
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,21 @@ public String getSourceFeatureName()
return sourceFeatureName;
}

public Feature getSourceFeature(CAS aCas)
{
return getAnnotationType(aCas).getFeatureByBaseName(getSourceFeatureName());
}

public String getTargetFeatureName()
{
return targetFeatureName;
}

public Feature getTargetFeature(CAS aCas)
{
return getAnnotationType(aCas).getFeatureByBaseName(getTargetFeatureName());
}

@Override
public List<Pair<LogMessage, AnnotationFS>> validate(CAS aCas)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import static de.tudarmstadt.ukp.clarin.webanno.brat.schema.BratSchemaGeneratorImpl.getBratTypeName;
import static de.tudarmstadt.ukp.clarin.webanno.model.ScriptDirection.RTL;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.uima.fit.util.CasUtil.getType;
import static org.apache.uima.fit.util.CasUtil.select;
Expand Down Expand Up @@ -52,19 +51,16 @@
import de.tudarmstadt.ukp.clarin.webanno.brat.render.model.SentenceComment;
import de.tudarmstadt.ukp.clarin.webanno.brat.render.model.SentenceMarker;
import de.tudarmstadt.ukp.clarin.webanno.brat.render.model.TextMarker;
import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer;
import de.tudarmstadt.ukp.dkpro.core.api.segmentation.TrimUtils;
import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence;
import de.tudarmstadt.ukp.inception.rendering.paging.Unit;
import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VAnnotationMarker;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VArc;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VComment;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VID;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VMarker;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VSentenceMarker;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VSpan;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VTextMarker;
import de.tudarmstadt.ukp.inception.support.text.TextUtils;
import de.tudarmstadt.ukp.inception.support.uima.ICasUtil;
Expand Down Expand Up @@ -100,7 +96,7 @@ public String getId()
@Override
public GetDocumentResponse render(VDocument aVDoc, RenderRequest aRequest)
{
GetDocumentResponse aResponse = new GetDocumentResponse();
var aResponse = new GetDocumentResponse();
aResponse.setRtlMode(RTL == aRequest.getState().getScriptDirection());
aResponse.setFontZoom(aRequest.getState().getPreferences().getFontZoom());
aResponse.setWindowBegin(aVDoc.getWindowBegin());
Expand All @@ -125,9 +121,9 @@ public GetDocumentResponse render(VDocument aVDoc, RenderRequest aRequest)

private void renderLayers(GetDocumentResponse aResponse, VDocument aVDoc)
{
for (AnnotationLayer layer : aVDoc.getAnnotationLayers()) {
for (VSpan vspan : aVDoc.spans(layer.getId())) {
List<Offsets> offsets = vspan.getRanges().stream() //
for (var layer : aVDoc.getAnnotationLayers()) {
for (var vspan : aVDoc.spans(layer.getId())) {
var offsets = vspan.getRanges().stream() //
.flatMap(range -> split(aResponse.getSentenceOffsets(), aVDoc.getText(),
aVDoc.getWindowBegin(), range.getBegin(), range.getEnd()).stream())
.map(range -> {
Expand All @@ -137,11 +133,10 @@ private void renderLayers(GetDocumentResponse aResponse, VDocument aVDoc)
range.setEnd(span[1]);
return range;
}) //
.collect(toList());
.toList();

Entity entity = new Entity(vspan.getVid(), getBratTypeName(vspan.getLayer()),
offsets, vspan.getLabelHint(), vspan.getColorHint(),
vspan.isActionButtons());
var entity = new Entity(vspan.getVid(), getBratTypeName(vspan.getLayer()), offsets,
vspan.getLabelHint(), vspan.getColorHint(), vspan.isActionButtons());
if (!layer.isShowTextInHover()) {
// If the layer is configured not to display the span text in the popup, then
// we simply set the popup to the empty string here.
Expand All @@ -160,9 +155,8 @@ private void renderLayers(GetDocumentResponse aResponse, VDocument aVDoc)
aResponse.addEntity(entity);
}

for (VArc varc : aVDoc.arcs(layer.getId())) {

Relation arc = new Relation(varc.getVid(), getBratTypeName(varc.getLayer()),
for (var varc : aVDoc.arcs(layer.getId())) {
var arc = new Relation(varc.getVid(), getBratTypeName(varc.getLayer()),
getArgument(varc.getSource(), varc.getTarget()), varc.getLabelHint(),
varc.getColorHint());
aResponse.addRelation(arc);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider;
import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature;
import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer;
import de.tudarmstadt.ukp.clarin.webanno.model.LinkMode;
import de.tudarmstadt.ukp.clarin.webanno.model.Project;
import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument;
import de.tudarmstadt.ukp.clarin.webanno.security.model.User;
Expand Down Expand Up @@ -186,6 +187,10 @@ private List<VLazyDetailGroup> lookupExtensionLevelDetails(VID aVid, SourceDocum
return emptyList();
}

if (aFeature.getLinkMode() == LinkMode.WITH_ROLE) {
return emptyList();
}

var result = new ArrayList<VLazyDetailGroup>();
var extension = extensionRegistry.getExtension(aVid.getExtensionId());
var value = extension.getFeatureValue(aDocument, aUser, aCas, aVid, aFeature);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.commons.collections4.MultiValuedMap;
Expand Down Expand Up @@ -87,8 +86,8 @@ private MultiValuedMap<Pair<String, String>, String> trainModel(List<Triple> aDa
{
MultiValuedMap<Pair<String, String>, String> model = new ArrayListValuedHashMap<>();

for (Triple t : aData) {
Pair<String, String> key = Pair.of(t.governor, t.dependent);
for (var t : aData) {
var key = Pair.of(t.governor, t.dependent);
model.put(key, t.label);
}

Expand All @@ -102,45 +101,45 @@ public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd)
MultiValuedMap<Pair<String, String>, String> model = aContext.get(KEY_MODEL).orElseThrow(
() -> new RecommendationException("Key [" + KEY_MODEL + "] not found in context"));

Type sampleUnitType = getType(aCas, SAMPLE_UNIT);
var sampleUnitType = getType(aCas, SAMPLE_UNIT);

Type predictedType = getPredictedType(aCas);
Feature governorFeature = predictedType.getFeatureByBaseName(FEAT_REL_SOURCE);
Feature dependentFeature = predictedType.getFeatureByBaseName(FEAT_REL_TARGET);
Feature predictedFeature = getPredictedFeature(aCas);
Feature isPredictionFeature = getIsPredictionFeature(aCas);
Type attachType = getAttachType(aCas);
Feature attachFeature = getAttachFeature(aCas);
Feature scoreFeature = getScoreFeature(aCas);
var predictedType = getPredictedType(aCas);
var governorFeature = predictedType.getFeatureByBaseName(FEAT_REL_SOURCE);
var dependentFeature = predictedType.getFeatureByBaseName(FEAT_REL_TARGET);
var predictedFeature = getPredictedFeature(aCas);
var isPredictionFeature = getIsPredictionFeature(aCas);
var attachType = getAttachType(aCas);
var attachFeature = getAttachFeature(aCas);
var scoreFeature = getScoreFeature(aCas);

// Relations are predicted only within the sample units - thus instead of looking at the
// whole document for potential relations, we only need to look at those units that overlap
// with the current prediction request area
var units = selectOverlapping(aCas, sampleUnitType, aBegin, aEnd);
for (AnnotationFS sampleUnit : units) {
for (var sampleUnit : units) {
Collection<AnnotationFS> baseAnnotations = selectCovered(attachType, sampleUnit);
for (AnnotationFS governor : baseAnnotations) {
for (AnnotationFS dependent : baseAnnotations) {
for (var governor : baseAnnotations) {
for (var dependent : baseAnnotations) {

if (governor.equals(dependent)) {
continue;
}

String governorLabel = governor.getStringValue(attachFeature);
String dependentLabel = dependent.getStringValue(attachFeature);
var governorLabel = governor.getStringValue(attachFeature);
var dependentLabel = dependent.getStringValue(attachFeature);

Pair<String, String> key = Pair.of(governorLabel, dependentLabel);
Collection<String> occurrences = model.get(key);
Map<String, Long> numberOfOccurrencesPerLabel = occurrences.stream() //
var key = Pair.of(governorLabel, dependentLabel);
var occurrences = model.get(key);
var numberOfOccurrencesPerLabel = occurrences.stream() //
.collect(groupingBy(identity(), counting()));

double totalNumberOfOccurrences = occurrences.size();
var totalNumberOfOccurrences = occurrences.size();

for (String relationLabel : occurrences) {
double score = numberOfOccurrencesPerLabel.get(relationLabel)
for (var relationLabel : occurrences) {
var score = numberOfOccurrencesPerLabel.get(relationLabel)
/ totalNumberOfOccurrences;
AnnotationFS prediction = aCas.createAnnotation(predictedType,
governor.getBegin(), governor.getEnd());
var prediction = aCas.createAnnotation(predictedType, governor.getBegin(),
governor.getEnd());
prediction.setFeatureValue(governorFeature, governor);
prediction.setFeatureValue(dependentFeature, dependent);
prediction.setStringValue(predictedFeature, relationLabel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_SCORE_EXPLANATION_SUFFIX;
import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_SCORE_SUFFIX;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.apache.uima.cas.CAS.TYPE_NAME_BOOLEAN;
import static org.apache.uima.cas.CAS.TYPE_NAME_DOUBLE;
import static org.apache.uima.cas.CAS.TYPE_NAME_STRING;
Expand All @@ -34,13 +33,13 @@
import org.apache.uima.fit.factory.CasFactory;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.metadata.TypeSystemDescription;
import org.apache.uima.util.CasCopier;
import org.apache.uima.util.TypeSystemUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature;
import de.tudarmstadt.ukp.inception.support.WebAnnoConst;
import de.tudarmstadt.ukp.inception.support.uima.SegmentationUtils;

public class RecommenderTypeSystemUtils
{
Expand All @@ -53,8 +52,7 @@ public static CAS makePredictionCas(CAS aOriginalCas, AnnotationFeature... aFeat
RecommenderTypeSystemUtils.addPredictionFeaturesToTypeSystem(tsd, asList(aFeatures));
var predictionCas = CasFactory.createCas(tsd);
predictionCas.setDocumentText(aOriginalCas.getDocumentText());
SegmentationUtils.splitSentences(predictionCas);
SegmentationUtils.tokenize(predictionCas);
CasCopier.copyCas(aOriginalCas, predictionCas, false);
return predictionCas;
}

Expand Down Expand Up @@ -83,8 +81,7 @@ public static void addPredictionFeaturesToTypeSystem(TypeSystemDescription tsd,
td.addFeature(modeFeatureName, "Suggestion mode", TYPE_NAME_STRING);
}

var layers = features.stream().map(AnnotationFeature::getLayer).distinct()
.collect(toList());
var layers = features.stream().map(AnnotationFeature::getLayer).distinct().toList();
for (var layer : layers) {
var td = tsd.getType(layer.getName());
if (td == null) {
Expand Down
Loading

0 comments on commit f3ab196

Please sign in to comment.