mirror of
https://github.com/gsi-upm/senpy
synced 2025-10-24 04:08:19 +00:00
Refactored conversion and postprocessing
This commit is contained in:
0
senpy/plugins/postprocessing/__init__.py
Normal file
0
senpy/plugins/postprocessing/__init__.py
Normal file
0
senpy/plugins/postprocessing/emotion/__init__.py
Normal file
0
senpy/plugins/postprocessing/emotion/__init__.py
Normal file
158
senpy/plugins/postprocessing/emotion/centroids.py
Normal file
158
senpy/plugins/postprocessing/emotion/centroids.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from senpy.plugins import EmotionConversionPlugin
|
||||
from senpy.models import EmotionSet, Emotion, Error
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CentroidConversion(EmotionConversionPlugin):
|
||||
'''
|
||||
This plugin converts emotion annotations from a dimensional model to a
|
||||
categorical one, and vice versa. The centroids used in the conversion
|
||||
are configurable and appear in the semantic description of the plugin.
|
||||
'''
|
||||
def __init__(self, info, *args, **kwargs):
|
||||
if 'centroids' not in info:
|
||||
raise Error('Centroid conversion plugins should provide '
|
||||
'the centroids in their senpy file')
|
||||
if 'onyx:doesConversion' not in info:
|
||||
if 'centroids_direction' not in info:
|
||||
raise Error('Please, provide centroids direction')
|
||||
|
||||
cf, ct = info['centroids_direction']
|
||||
info['onyx:doesConversion'] = [{
|
||||
'onyx:conversionFrom': cf,
|
||||
'onyx:conversionTo': ct
|
||||
}, {
|
||||
'onyx:conversionFrom': ct,
|
||||
'onyx:conversionTo': cf
|
||||
}]
|
||||
|
||||
if 'aliases' in info:
|
||||
aliases = info['aliases']
|
||||
ncentroids = {}
|
||||
for k1, v1 in info['centroids'].items():
|
||||
nv1 = {}
|
||||
for k2, v2 in v1.items():
|
||||
nv1[aliases.get(k2, k2)] = v2
|
||||
ncentroids[aliases.get(k1, k1)] = nv1
|
||||
info['centroids'] = ncentroids
|
||||
|
||||
super(CentroidConversion, self).__init__(info, *args, **kwargs)
|
||||
|
||||
self.dimensions = set()
|
||||
for c in self.centroids.values():
|
||||
self.dimensions.update(c.keys())
|
||||
self.neutralPoints = self.get("neutralPoints", dict())
|
||||
if not self.neutralPoints:
|
||||
for i in self.dimensions:
|
||||
self.neutralPoints[i] = self.get("neutralValue", 0)
|
||||
|
||||
def _forward_conversion(self, original):
|
||||
"""Sum the VAD value of all categories found weighted by intensity.
|
||||
Intensities are scaled by onyx:maxIntensityValue if it is present, else maxIntensityValue
|
||||
is assumed to be one. Emotion entries that do not have onxy:hasEmotionIntensity specified
|
||||
are assumed to have maxIntensityValue. Emotion entries that do not have
|
||||
onyx:hasEmotionCategory specified are ignored."""
|
||||
res = Emotion()
|
||||
maxIntensity = float(original.get("onyx:maxIntensityValue", 1))
|
||||
for e in original.onyx__hasEmotion:
|
||||
category = e.get("onyx:hasEmotionCategory", None)
|
||||
if not category:
|
||||
continue
|
||||
intensity = e.get("onyx:hasEmotionIntensity", maxIntensity) / maxIntensity
|
||||
if not intensity:
|
||||
continue
|
||||
centroid = self.centroids.get(category, None)
|
||||
if centroid:
|
||||
for dim, value in centroid.items():
|
||||
neutral = self.neutralPoints[dim]
|
||||
if dim not in res:
|
||||
res[dim] = 0
|
||||
res[dim] += (value - neutral) * intensity + neutral
|
||||
return res
|
||||
|
||||
def _backwards_conversion(self, original):
|
||||
"""Find the closest category"""
|
||||
centroids = self.centroids
|
||||
neutralPoints = self.neutralPoints
|
||||
dimensions = self.dimensions
|
||||
|
||||
def distance_k(centroid, original, k):
|
||||
# k component of the distance between the value and a given centroid
|
||||
return (centroid.get(k, neutralPoints[k]) - original.get(k, neutralPoints[k]))**2
|
||||
|
||||
def distance(centroid):
|
||||
return sum(distance_k(centroid, original, k) for k in dimensions)
|
||||
|
||||
emotion = min(centroids, key=lambda x: distance(centroids[x]))
|
||||
|
||||
result = Emotion(onyx__hasEmotionCategory=emotion)
|
||||
result.onyx__algorithmConfidence = distance(centroids[emotion])
|
||||
return result
|
||||
|
||||
def convert(self, emotionSet, fromModel, toModel, params):
|
||||
|
||||
cf, ct = self.centroids_direction
|
||||
logger.debug(
|
||||
'{}\n{}\n{}\n{}'.format(emotionSet, fromModel, toModel, params))
|
||||
e = EmotionSet()
|
||||
if fromModel == cf and toModel == ct:
|
||||
e.onyx__hasEmotion.append(self._forward_conversion(emotionSet))
|
||||
elif fromModel == ct and toModel == cf:
|
||||
for i in emotionSet.onyx__hasEmotion:
|
||||
e.onyx__hasEmotion.append(self._backwards_conversion(i))
|
||||
else:
|
||||
raise Error('EMOTION MODEL NOT KNOWN')
|
||||
yield e
|
||||
|
||||
def test(self, info=None):
|
||||
if not info:
|
||||
info = {
|
||||
"name": "CentroidTest",
|
||||
"description": "Centroid test",
|
||||
"version": 0,
|
||||
"centroids": {
|
||||
"c1": {"V1": 0.5,
|
||||
"V2": 0.5},
|
||||
"c2": {"V1": -0.5,
|
||||
"V2": 0.5},
|
||||
"c3": {"V1": -0.5,
|
||||
"V2": -0.5},
|
||||
"c4": {"V1": 0.5,
|
||||
"V2": -0.5}},
|
||||
"aliases": {
|
||||
"V1": "X-dimension",
|
||||
"V2": "Y-dimension"
|
||||
},
|
||||
"centroids_direction": ["emoml:big6", "emoml:fsre-dimensions"]
|
||||
}
|
||||
|
||||
c = CentroidConversion(info)
|
||||
|
||||
es1 = EmotionSet()
|
||||
e1 = Emotion()
|
||||
e1.onyx__hasEmotionCategory = "c1"
|
||||
es1.onyx__hasEmotion.append(e1)
|
||||
res = c._forward_conversion(es1)
|
||||
assert res["X-dimension"] == 0.5
|
||||
assert res["Y-dimension"] == 0.5
|
||||
|
||||
e2 = Emotion()
|
||||
e2.onyx__hasEmotionCategory = "c2"
|
||||
es1.onyx__hasEmotion.append(e2)
|
||||
res = c._forward_conversion(es1)
|
||||
assert res["X-dimension"] == 0
|
||||
assert res["Y-dimension"] == 1
|
||||
|
||||
e = Emotion()
|
||||
e["X-dimension"] = -0.2
|
||||
e["Y-dimension"] = -0.3
|
||||
res = c._backwards_conversion(e)
|
||||
assert res["onyx:hasEmotionCategory"] == "c3"
|
||||
|
||||
e = Emotion()
|
||||
e["X-dimension"] = -0.2
|
||||
e["Y-dimension"] = 0.3
|
||||
res = c._backwards_conversion(e)
|
||||
assert res["onyx:hasEmotionCategory"] == "c2"
|
||||
52
senpy/plugins/postprocessing/emotion/ekman2fsre.senpy
Normal file
52
senpy/plugins/postprocessing/emotion/ekman2fsre.senpy
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: Ekman2FSRE
|
||||
module: senpy.plugins.postprocessing.emotion.centroids
|
||||
description: Plugin to convert emotion sets from Ekman to VAD
|
||||
version: 0.2
|
||||
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction
|
||||
neutralValue: 5.0
|
||||
centroids:
|
||||
anger:
|
||||
A: 6.95
|
||||
D: 5.1
|
||||
V: 2.7
|
||||
S: 5.0
|
||||
disgust:
|
||||
A: 5.3
|
||||
D: 8.05
|
||||
V: 2.7
|
||||
S: 5.0
|
||||
fear:
|
||||
A: 6.5
|
||||
D: 3.6
|
||||
V: 3.2
|
||||
S: 5.0
|
||||
happiness:
|
||||
A: 7.22
|
||||
D: 6.28
|
||||
V: 8.6
|
||||
S: 5.0
|
||||
sadness:
|
||||
A: 5.21
|
||||
D: 2.82
|
||||
V: 2.21
|
||||
S: 5.0
|
||||
surprise:
|
||||
A: 5.0
|
||||
D: 5.0
|
||||
V: 5.0
|
||||
S: 10.0
|
||||
centroids_direction:
|
||||
- emoml:big6
|
||||
- emoml:fsre-dimensions
|
||||
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
||||
A: emoml:fsre-dimensions_arousal
|
||||
V: emoml:fsre-dimensions_valence
|
||||
D: emoml:fsre-dimensions_potency
|
||||
S: emoml:fsre-dimensions_unpredictability
|
||||
anger: emoml:big6anger
|
||||
disgust: emoml:big6disgust
|
||||
fear: emoml:big6fear
|
||||
happiness: emoml:big6happiness
|
||||
sadness: emoml:big6sadness
|
||||
surprise: emoml:big6surprise
|
||||
40
senpy/plugins/postprocessing/emotion/ekman2vad.senpy
Normal file
40
senpy/plugins/postprocessing/emotion/ekman2vad.senpy
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Ekman2PAD
|
||||
module: senpy.plugins.postprocessing.emotion.centroids
|
||||
description: Plugin to convert emotion sets from Ekman to VAD
|
||||
version: 0.2
|
||||
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction
|
||||
neutralValue: 5.0
|
||||
centroids:
|
||||
anger:
|
||||
A: 6.95
|
||||
D: 5.1
|
||||
V: 2.7
|
||||
disgust:
|
||||
A: 5.3
|
||||
D: 8.05
|
||||
V: 2.7
|
||||
fear:
|
||||
A: 6.5
|
||||
D: 3.6
|
||||
V: 3.2
|
||||
happiness:
|
||||
A: 7.22
|
||||
D: 6.28
|
||||
V: 8.6
|
||||
sadness:
|
||||
A: 5.21
|
||||
D: 2.82
|
||||
V: 2.21
|
||||
centroids_direction:
|
||||
- emoml:big6
|
||||
- emoml:pad
|
||||
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
||||
A: emoml:pad-dimensions:arousal
|
||||
V: emoml:pad-dimensions:pleasure
|
||||
D: emoml:pad-dimensions:dominance
|
||||
anger: emoml:big6anger
|
||||
disgust: emoml:big6disgust
|
||||
fear: emoml:big6fear
|
||||
happiness: emoml:big6happiness
|
||||
sadness: emoml:big6sadness
|
||||
196
senpy/plugins/postprocessing/emotion/maxEmotion_plugin.py
Normal file
196
senpy/plugins/postprocessing/emotion/maxEmotion_plugin.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from senpy import PostProcessing, easy_test
|
||||
|
||||
|
||||
class MaxEmotion(PostProcessing):
|
||||
'''Plugin to extract the emotion with highest value from an EmotionSet'''
|
||||
author = '@dsuarezsouto'
|
||||
version = '0.1'
|
||||
|
||||
def process_entry(self, entry, params):
|
||||
if len(entry.emotions) < 1:
|
||||
yield entry
|
||||
return
|
||||
|
||||
set_emotions = entry.emotions[0]['onyx:hasEmotion']
|
||||
|
||||
# If there is only one emotion, do not modify it
|
||||
if len(set_emotions) < 2:
|
||||
yield entry
|
||||
return
|
||||
|
||||
max_emotion = set_emotions[0]
|
||||
|
||||
# Extract max emotion from the set emotions (emotion with highest intensity)
|
||||
for tmp_emotion in set_emotions:
|
||||
if tmp_emotion['onyx:hasEmotionIntensity'] > max_emotion[
|
||||
'onyx:hasEmotionIntensity']:
|
||||
max_emotion = tmp_emotion
|
||||
|
||||
if max_emotion['onyx:hasEmotionIntensity'] == 0:
|
||||
max_emotion['onyx:hasEmotionCategory'] = "neutral"
|
||||
max_emotion['onyx:hasEmotionIntensity'] = 1.0
|
||||
|
||||
entry.emotions[0]['onyx:hasEmotion'] = [max_emotion]
|
||||
|
||||
entry.emotions[0]['prov:wasGeneratedBy'] = "maxSentiment"
|
||||
yield entry
|
||||
|
||||
def check(self, request, plugins):
|
||||
return 'maxemotion' in request.parameters and self not in plugins
|
||||
|
||||
# Test Cases:
|
||||
# 1 Normal Situation.
|
||||
# 2 Case to return a Neutral Emotion.
|
||||
test_cases = [
|
||||
{
|
||||
"name":
|
||||
"If there are several emotions within an emotion set, reduce it to one.",
|
||||
"entry": {
|
||||
"@type":
|
||||
"entry",
|
||||
"emotions": [
|
||||
{
|
||||
"@id":
|
||||
"Emotions0",
|
||||
"@type":
|
||||
"emotionSet",
|
||||
"onyx:hasEmotion": [
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "anger",
|
||||
"onyx:hasEmotionIntensity": 0
|
||||
},
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "joy",
|
||||
"onyx:hasEmotionIntensity": 0.3333333333333333
|
||||
},
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "negative-fear",
|
||||
"onyx:hasEmotionIntensity": 0
|
||||
},
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "sadness",
|
||||
"onyx:hasEmotionIntensity": 0
|
||||
},
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "disgust",
|
||||
"onyx:hasEmotionIntensity": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nif:isString":
|
||||
"Test"
|
||||
},
|
||||
'expected': {
|
||||
"@type":
|
||||
"entry",
|
||||
"emotions": [
|
||||
{
|
||||
"@id":
|
||||
"Emotions0",
|
||||
"@type":
|
||||
"emotionSet",
|
||||
"onyx:hasEmotion": [
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "joy",
|
||||
"onyx:hasEmotionIntensity": 0.3333333333333333
|
||||
}
|
||||
],
|
||||
"prov:wasGeneratedBy":
|
||||
'maxSentiment'
|
||||
}
|
||||
],
|
||||
"nif:isString":
|
||||
"Test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":
|
||||
"If the maximum emotion has an intensity of 0, return a neutral emotion.",
|
||||
"entry": {
|
||||
"@type":
|
||||
"entry",
|
||||
"emotions": [{
|
||||
"@id":
|
||||
"Emotions0",
|
||||
"@type":
|
||||
"emotionSet",
|
||||
"onyx:hasEmotion": [
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "anger",
|
||||
"onyx:hasEmotionIntensity": 0
|
||||
},
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "joy",
|
||||
"onyx:hasEmotionIntensity": 0
|
||||
},
|
||||
{
|
||||
"@id":
|
||||
"_:Emotion_1538121033.74",
|
||||
"@type":
|
||||
"emotion",
|
||||
"onyx:hasEmotionCategory":
|
||||
"negative-fear",
|
||||
"onyx:hasEmotionIntensity":
|
||||
0
|
||||
},
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory":
|
||||
"sadness",
|
||||
"onyx:hasEmotionIntensity": 0
|
||||
},
|
||||
{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory":
|
||||
"disgust",
|
||||
"onyx:hasEmotionIntensity": 0
|
||||
}]
|
||||
}],
|
||||
"nif:isString":
|
||||
"Test"
|
||||
},
|
||||
'expected': {
|
||||
"@type":
|
||||
"entry",
|
||||
"emotions": [{
|
||||
"@id":
|
||||
"Emotions0",
|
||||
"@type":
|
||||
"emotionSet",
|
||||
"onyx:hasEmotion": [{
|
||||
"@id": "_:Emotion_1538121033.74",
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "neutral",
|
||||
"onyx:hasEmotionIntensity": 1
|
||||
}],
|
||||
"prov:wasGeneratedBy":
|
||||
'maxSentiment'
|
||||
}],
|
||||
"nif:isString":
|
||||
"Test"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy_test()
|
||||
Reference in New Issue
Block a user