-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create explainable user model classes #223
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,3 +24,4 @@ data/users.db | |
|
||
# local experimentation | ||
local/ | ||
.vscode/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
genres: | ||
- "You [don't ]like {}." | ||
- "You [don't ]watch {} movies." | ||
- "You [don't ]enjoy {} films." | ||
|
||
actors: | ||
- "You [don't ]like movies with {}." | ||
- "You [don't ]watch films if {} are in them." | ||
- "You [don't ]care for {} in movies." | ||
|
||
directors: | ||
- "You [don't ]like {}' movies." | ||
- "You [don't ]watch films by {}." | ||
- "You [don't ]enjoy movies made by {}." | ||
|
||
keywords: | ||
- "You [don't ]like movies about {}." | ||
- "You [don't ]watch films with {}." | ||
- "You [don't ]care for {} in movies." | ||
|
||
year: | ||
- "You [don't ]like movies from {}." | ||
- "You [don't ]watch films made in {}." | ||
- "You [don't ]enjoy movies from {}." |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
"""Abstract class for creating explainable user models.""" | ||
|
||
from abc import ABC, abstractmethod | ||
from typing import Dict | ||
|
||
from dialoguekit.core import AnnotatedUtterance | ||
|
||
UserPreferences = Dict[str, Dict[str, float]] | ||
|
||
|
||
class ExplainableUserModel(ABC): | ||
@abstractmethod | ||
def generate_explanation( | ||
self, user_preferences: UserPreferences | ||
) -> AnnotatedUtterance: | ||
"""Generates an explanation based on the provided input data. | ||
|
||
Args: | ||
input_data: The input data for which an explanation is to be | ||
generated. | ||
|
||
Returns: | ||
A system utterance containing an explanation. | ||
|
||
Raises: | ||
NotImplementedError: This method must be implemented by a subclass. | ||
""" | ||
raise NotImplementedError( | ||
"This method must be implemented by a subclass." | ||
) |
94 changes: 94 additions & 0 deletions
94
moviebot/explainability/explainable_user_model_tag_based.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
"""Class for creating a tag-based user model explanations. | ||
|
||
The class generates explanations for user preferences in the movie domain. | ||
Currently, the explanations are based on movie tags/attributes that were | ||
explicitly mentioned by the user in the conversation. | ||
Future versions of the class will also support implicit tags/attributes, which | ||
are inferred from the movie recommendation feedback. Explanations are based on | ||
the templates loaded from a YAML file. | ||
""" | ||
|
||
import os | ||
import random | ||
import re | ||
|
||
import yaml | ||
from dialoguekit.core import AnnotatedUtterance | ||
from dialoguekit.participant import DialogueParticipant | ||
|
||
from moviebot.explainability.explainable_user_model import ( | ||
ExplainableUserModel, | ||
UserPreferences, | ||
) | ||
|
||
_DEFAULT_TEMPLATE_FILE = "data/explainability/explanation_templates.yaml" | ||
|
||
|
||
class ExplainableUserModelTagBased(ExplainableUserModel): | ||
def __init__(self, template_file: str = _DEFAULT_TEMPLATE_FILE): | ||
"""Initializes the ExplainableUserModelTagBased class. | ||
|
||
Args: | ||
template_file: Path to the YAML file containing explanation | ||
templates. Defaults to _DEFAULT_TEMPLATE_FILE. | ||
|
||
Raises: | ||
FileNotFoundError: The template file could not be found. | ||
""" | ||
if not os.isfile(template_file): | ||
raise FileNotFoundError( | ||
f"Could not find template file {template_file}." | ||
) | ||
|
||
with open(template_file, "r") as f: | ||
self.templates = yaml.safe_load(f) | ||
|
||
def generate_explanation( | ||
self, user_preferences: UserPreferences | ||
) -> AnnotatedUtterance: | ||
"""Generates an explanation based on the provided user preferences. | ||
|
||
Args: | ||
user_preferences: User preferences. | ||
|
||
Returns: | ||
The generated explanation. | ||
""" | ||
explanation = "" | ||
for category, prefs in user_preferences.items(): | ||
positive_tags = [tag for tag, value in prefs.items() if value == 1] | ||
negative_tags = [tag for tag, value in prefs.items() if value == -1] | ||
|
||
for i, tags in enumerate([positive_tags, negative_tags]): | ||
if len(tags) == 0: | ||
continue | ||
|
||
concatenated_tags = ", ".join(tags) | ||
template = random.choice(self.templates[category]).format( | ||
concatenated_tags | ||
) | ||
|
||
explanation += self._clean_negative_keyword( | ||
template, remove=i == 0 | ||
) | ||
|
||
return AnnotatedUtterance(explanation, DialogueParticipant.AGENT) | ||
|
||
def _clean_negative_keyword( | ||
self, template: str, remove: bool = True | ||
) -> str: | ||
"""Removes or keeps negation in template. | ||
|
||
Args: | ||
template: Template containing negative keyword. | ||
remove: If True, remove the negative keyword. Defaults to True. | ||
|
||
Returns: | ||
Template with negative keyword removed or replaced. | ||
""" | ||
if remove: | ||
return re.sub(r"\[.*?\]", "", template) | ||
|
||
chars_to_remove = "[]" | ||
trans = str.maketrans("", "", chars_to_remove) | ||
return template.translate(trans) |
43 changes: 43 additions & 0 deletions
43
tests/explainability/test_explainable_user_model_tag_based.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import pytest | ||
|
||
from moviebot.explainability.explainable_user_model_tag_based import ( | ||
ExplainableUserModelTagBased, | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def explainable_model() -> ExplainableUserModelTagBased: | ||
return ExplainableUserModelTagBased() | ||
|
||
|
||
def test_generate_explanation_positive(explainable_model): | ||
user_prefs = {"genres": {"action": 1, "comedy": 1}} | ||
explanation = explainable_model.generate_explanation(user_prefs) | ||
assert "action" in explanation.text | ||
assert "comedy" in explanation.text | ||
|
||
|
||
def test_generate_explanation_negative(explainable_model): | ||
user_prefs = {"actors": {"Tom Hanks": -1, "Adam": -1}} | ||
explanation = explainable_model.generate_explanation(user_prefs) | ||
assert "Tom Hanks" in explanation.text | ||
assert "Adam" in explanation.text | ||
|
||
|
||
def test_generate_explanation_mixed(explainable_model): | ||
user_prefs = {"keywords": {"war theme": 1, "comic": -1}} | ||
explanation = explainable_model.generate_explanation(user_prefs) | ||
assert "war theme" in explanation.text | ||
assert "comic" in explanation.text | ||
|
||
|
||
def test_clean_negative_keyword_remove(explainable_model): | ||
template = "You [don't ]like action." | ||
cleaned = explainable_model._clean_negative_keyword(template) | ||
assert cleaned == "You like action." | ||
|
||
|
||
def test_clean_negative_keyword_keep(explainable_model): | ||
template = "You [don't ]like action." | ||
cleaned = explainable_model._clean_negative_keyword(template, remove=False) | ||
assert cleaned == "You don't like action." |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: check if file exists raise exception otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.