From 5a49833257e672a89a3747078ba01216344f60df Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 13 May 2024 09:40:11 +0200 Subject: [PATCH 01/24] Create class to represent an information need Part of #148 --- usersimcrs/core/__init__.py | 0 usersimcrs/core/information_need.py | 78 ++++++++++++++++++++++++++++ usersimcrs/core/simulation_domain.py | 28 ++++++++++ 3 files changed, 106 insertions(+) create mode 100644 usersimcrs/core/__init__.py create mode 100644 usersimcrs/core/information_need.py create mode 100644 usersimcrs/core/simulation_domain.py diff --git a/usersimcrs/core/__init__.py b/usersimcrs/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/usersimcrs/core/information_need.py b/usersimcrs/core/information_need.py new file mode 100644 index 00000000..b11359ec --- /dev/null +++ b/usersimcrs/core/information_need.py @@ -0,0 +1,78 @@ +"""Interface to represent an information need. + +The information need is divided into two parts: constraints and requests. The +constraints specify the slot-value pairs that the item of interest must satisfy, +while the requests specify the slots for which the user wants information.""" + +from __future__ import annotations + +import random +from collections import defaultdict +from typing import Any, Dict, List + +from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.items.item_collection import ItemCollection + +random.seed(42) + + +def generate_information_need( + domain: SimulationDomain, item_collection: ItemCollection +) -> InformationNeed: + """Generates an information need based on the domain. + + Args: + domain: Domain knowledge. + item_collection: Collection of items. + + Returns: + Information need. + """ + slot_names = domain.get_slot_names() + constraints = {} + nb_constraints = random.randint(1, len(slot_names)) + for s in random.sample(slot_names, nb_constraints): + value = random.choice(item_collection.get_possible_property_values(s)) + constraints[s] = value + + requestable_slots = domain.get_requestable_slots() + nb_requests = random.randint(1, len(requestable_slots)) + requests = random.sample(requestable_slots, nb_requests) + + return InformationNeed(constraints, requests) + + +class InformationNeed: + def __init__( + self, constraints: Dict[str, Any], requests: List[str] + ) -> None: + """Initializes an information need. + + Args: + constraints: Slot-value pairs representing constraints on the item + of interest. + requests: Slots representing the desired information. + """ + self.constraints = constraints + self.requested_slots = defaultdict( + Any, {slot: None for slot in requests} + ) + + def get_constraint_value(self, slot: str) -> Any: + """Returns the value of a constraint slot. + + Args: + slot: Slot. + + Returns: + Value of the slot. + """ + return self.constraints.get(slot) + + def get_requestable_slots(self) -> List[str]: + """Returns the list of requestable slots.""" + return [ + slot + for slot in self.requested_slots + if not self.requested_slots[slot] + ] diff --git a/usersimcrs/core/simulation_domain.py b/usersimcrs/core/simulation_domain.py new file mode 100644 index 00000000..0b2a8698 --- /dev/null +++ b/usersimcrs/core/simulation_domain.py @@ -0,0 +1,28 @@ +"""Simulation domain knowledge.""" + +from typing import List + +from dialoguekit.core.domain import Domain + + +class SimulationDomain(Domain): + def __init__(self, config_file: str) -> None: + """Initializes the domain knowledge. + + Args: + config_file: Path to the domain configuration file. + + Raises: + KeyError: If the domain configuration file does not contain the + field 'requestable_slots'. + """ + super().__init__(config_file) + if "requestable_slots" not in self._config: + raise KeyError( + "The domain configuration file must contain the field " + "'requestable_slots'." + ) + + def get_requestable_slots(self) -> List[str]: + """Returns the list of requestable slots.""" + return list(self._config["requestable_slots"].keys()) From 634b2c765bce8661dd93db1e79f1dbcbf7521714 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 13 May 2024 10:02:08 +0200 Subject: [PATCH 02/24] Add tests for information need --- tests/core/test_information_need.py | 95 +++++++++++++++++++ tests/data/domains/movies.yaml | 5 + tests/items/test_item_collection.py | 23 +++-- tests/items/test_ratings.py | 4 +- .../test_simple_preference_model.py | 4 +- usersimcrs/core/information_need.py | 4 +- usersimcrs/core/simulation_domain.py | 2 +- usersimcrs/items/item.py | 4 +- usersimcrs/items/item_collection.py | 4 +- usersimcrs/run_simulation.py | 11 ++- .../agenda_based/agenda_based_simulator.py | 16 +++- .../user_modeling/pkg_preference_model.py | 4 +- usersimcrs/user_modeling/preference_model.py | 5 +- .../user_modeling/simple_preference_model.py | 12 ++- 14 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 tests/core/test_information_need.py diff --git a/tests/core/test_information_need.py b/tests/core/test_information_need.py new file mode 100644 index 00000000..0f0f9907 --- /dev/null +++ b/tests/core/test_information_need.py @@ -0,0 +1,95 @@ +"""Tests for the InformationNeed class.""" + +import pytest + +from usersimcrs.core.information_need import ( + InformationNeed, + generate_information_need, +) +from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.items.item_collection import ItemCollection + +DOMAIN_YAML_FILE = "tests/data/domains/movies.yaml" +ITEMS_CSV_FILE = "tests/data/items/movies_w_keywords.csv" + + +@pytest.fixture +def domain() -> SimulationDomain: + """Domain fixture.""" + return SimulationDomain(DOMAIN_YAML_FILE) + + +@pytest.fixture +def item_collection(domain: SimulationDomain) -> ItemCollection: + """Item collection fixture.""" + mapping = { + "title": {"slot": "TITLE"}, + "genres": { + "slot": "GENRE", + "multi-valued": True, + "delimiter": "|", + }, + } + + item_collection = ItemCollection() + item_collection.load_items_csv( + ITEMS_CSV_FILE, + id_col="movieId", + domain=domain, + domain_mapping=mapping, + ) + return item_collection + + +def test_generate_information_need( + domain: SimulationDomain, item_collection: ItemCollection +) -> None: + """Test generate_information_need. + + Args: + domain: Simulation domain. + item_collection: Item collection. + """ + information_need = generate_information_need(domain, item_collection) + assert information_need.constraints is not None + assert information_need.requested_slots is not None + + +@pytest.fixture +def information_need() -> InformationNeed: + """Information need fixture.""" + constraints = {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"} + requests = ["plot", "rating"] + return InformationNeed(constraints, requests) + + +@pytest.mark.parametrize( + "slot,expected_value", + [ + ("GENRE", "Comedy"), + ("DIRECTOR", "Steven Spielberg"), + ("KEYWORDS", None), + ], +) +def test_get_constraint_value( + information_need: InformationNeed, slot: str, expected_value: str +) -> None: + """Test get_constraint_value. + + Args: + information_need: Information need. + slot: Slot. + expected_value: Expected value. + """ + assert information_need.get_constraint_value(slot) == expected_value + + +def test_get_requestable_slots(information_need: InformationNeed) -> None: + """Test get_requestable_slots. + + Args: + information_need: Information need. + """ + assert information_need.get_requestable_slots() == ["plot", "rating"] + information_need.requested_slots["rating"] = 4.5 + assert information_need.get_requestable_slots() == ["plot"] diff --git a/tests/data/domains/movies.yaml b/tests/data/domains/movies.yaml index 26953cf5..4ddd6b2e 100644 --- a/tests/data/domains/movies.yaml +++ b/tests/data/domains/movies.yaml @@ -1,6 +1,11 @@ +name: test_movies slot_names: TITLE: GENRE: ACTOR: KEYWORD: DIRECTOR: + +requestable_slots: + - plot + - rating diff --git a/tests/items/test_item_collection.py b/tests/items/test_item_collection.py index 2a9184f4..65c01f62 100644 --- a/tests/items/test_item_collection.py +++ b/tests/items/test_item_collection.py @@ -1,9 +1,10 @@ """Tests for ItemCollection.""" + from typing import Any, Dict import pytest -from dialoguekit.core.domain import Domain +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection DOMAIN_YAML_FILE = "tests/data/domains/movies.yaml" @@ -11,9 +12,9 @@ @pytest.fixture -def domain() -> Domain: +def domain() -> SimulationDomain: """Domain fixture.""" - return Domain(DOMAIN_YAML_FILE) + return SimulationDomain(DOMAIN_YAML_FILE) @pytest.fixture @@ -33,7 +34,9 @@ def movie() -> Dict[str, Any]: } -def test_load_items_csv(domain: Domain, movie: Dict[str, Any]) -> None: +def test_load_items_csv( + domain: SimulationDomain, movie: Dict[str, Any] +) -> None: """Tests items loading with a domain and mapping.""" item_collection = ItemCollection() mapping = { @@ -59,7 +62,7 @@ def test_load_items_csv(domain: Domain, movie: Dict[str, Any]) -> None: assert item.get_property("KEYWORD") is None -def test_get_possible_property_values(domain: Domain) -> None: +def test_get_possible_property_values(domain: SimulationDomain) -> None: """Tests using slot with different types (str, list) and unknown slot.""" item_collection = ItemCollection() mapping = { @@ -80,9 +83,13 @@ def test_get_possible_property_values(domain: Domain) -> None: genres = item_collection.get_possible_property_values("GENRE") assert len(genres) == 20 - assert {"Adventure", "Animation", "Children", "Comedy", "Fantasy"}.issubset( - genres - ) + assert { + "Adventure", + "Animation", + "Children", + "Comedy", + "Fantasy", + }.issubset(genres) assert not {"Biography", "Short Film"}.issubset(genres) titles = item_collection.get_possible_property_values("TITLE") diff --git a/tests/items/test_ratings.py b/tests/items/test_ratings.py index 2656f921..0d2b3f1b 100644 --- a/tests/items/test_ratings.py +++ b/tests/items/test_ratings.py @@ -3,8 +3,8 @@ from typing import Dict, List import pytest -from dialoguekit.core.domain import Domain +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection from usersimcrs.items.ratings import Ratings, user_item_sampler @@ -43,7 +43,7 @@ def simple_user_item_sampler( @pytest.fixture def ratings() -> Ratings: """Ratings fixture.""" - domain = Domain(DOMAIN_YAML_FILE) + domain = SimulationDomain(DOMAIN_YAML_FILE) item_collection = ItemCollection() item_collection.load_items_csv( ITEMS_CSV_FILE, diff --git a/tests/simulator/user_modeling/test_simple_preference_model.py b/tests/simulator/user_modeling/test_simple_preference_model.py index 388bdd13..b45ce7f8 100644 --- a/tests/simulator/user_modeling/test_simple_preference_model.py +++ b/tests/simulator/user_modeling/test_simple_preference_model.py @@ -1,8 +1,8 @@ """Tests for SimplePreferenceModel.""" import pytest -from dialoguekit.core.domain import Domain +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection from usersimcrs.items.ratings import Ratings from usersimcrs.user_modeling.simple_preference_model import ( @@ -25,7 +25,7 @@ @pytest.fixture def preference_model() -> SimplePreferenceModel: """Preference model fixture.""" - domain = Domain(DOMAIN_YAML_FILE) + domain = SimulationDomain(DOMAIN_YAML_FILE) item_collection = ItemCollection() item_collection.load_items_csv( ITEMS_CSV_FILE, diff --git a/usersimcrs/core/information_need.py b/usersimcrs/core/information_need.py index b11359ec..cf55e94c 100644 --- a/usersimcrs/core/information_need.py +++ b/usersimcrs/core/information_need.py @@ -32,7 +32,9 @@ def generate_information_need( constraints = {} nb_constraints = random.randint(1, len(slot_names)) for s in random.sample(slot_names, nb_constraints): - value = random.choice(item_collection.get_possible_property_values(s)) + value = random.choice( + list(item_collection.get_possible_property_values(s)) + ) constraints[s] = value requestable_slots = domain.get_requestable_slots() diff --git a/usersimcrs/core/simulation_domain.py b/usersimcrs/core/simulation_domain.py index 0b2a8698..d3a279e1 100644 --- a/usersimcrs/core/simulation_domain.py +++ b/usersimcrs/core/simulation_domain.py @@ -25,4 +25,4 @@ def __init__(self, config_file: str) -> None: def get_requestable_slots(self) -> List[str]: """Returns the list of requestable slots.""" - return list(self._config["requestable_slots"].keys()) + return self._config["requestable_slots"] diff --git a/usersimcrs/items/item.py b/usersimcrs/items/item.py index 15222693..11a2cec9 100644 --- a/usersimcrs/items/item.py +++ b/usersimcrs/items/item.py @@ -2,7 +2,7 @@ from typing import Any, Dict -from dialoguekit.core.domain import Domain +from usersimcrs.core.simulation_domain import SimulationDomain class Item: @@ -10,7 +10,7 @@ def __init__( self, item_id: str, properties: Dict[str, Any] = None, - domain: Domain = None, + domain: SimulationDomain = None, ) -> None: """Creates an item. diff --git a/usersimcrs/items/item_collection.py b/usersimcrs/items/item_collection.py index 88161738..3e2fe993 100644 --- a/usersimcrs/items/item_collection.py +++ b/usersimcrs/items/item_collection.py @@ -9,8 +9,8 @@ from typing import Any, Dict, List, Set from dialoguekit.core.annotation import Annotation -from dialoguekit.core.domain import Domain +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item import Item # Mapping configuration: for each csv field as key, it provides a dict with @@ -64,7 +64,7 @@ def add_item(self, item: Item) -> None: def load_items_csv( self, file_path: str, - domain: Domain, + domain: SimulationDomain, domain_mapping: MappingConfig, id_col: str = "ID", delimiter: str = ",", diff --git a/usersimcrs/run_simulation.py b/usersimcrs/run_simulation.py index 43ec47a0..8fb96e5f 100644 --- a/usersimcrs/run_simulation.py +++ b/usersimcrs/run_simulation.py @@ -11,7 +11,6 @@ import yaml from dialoguekit.connector.dialogue_connector import DialogueConnector from dialoguekit.core.dialogue import Dialogue -from dialoguekit.core.domain import Domain from dialoguekit.core.intent import Intent from dialoguekit.core.utterance import Utterance from dialoguekit.nlg import ConditionalNLG @@ -30,12 +29,15 @@ from dialoguekit.utils.dialogue_reader import json_to_dialogues from sample_agents.moviebot_agent import MovieBotAgent +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection from usersimcrs.items.ratings import Ratings from usersimcrs.simulator.agenda_based.agenda_based_simulator import ( AgendaBasedSimulator, ) -from usersimcrs.simulator.agenda_based.interaction_model import InteractionModel +from usersimcrs.simulator.agenda_based.interaction_model import ( + InteractionModel, +) from usersimcrs.simulator.user_simulator import UserSimulator from usersimcrs.user_modeling.simple_preference_model import ( SimplePreferenceModel, @@ -68,7 +70,7 @@ def main(config: confuse.Configuration, agent: Agent) -> None: raise TypeError(f"agent must be Agent, not {type(agent).__name__}") # Loads domain, item collection, and preference data - domain = Domain(config["domain"].get()) + domain = SimulationDomain(config["domain"].get()) item_collection = ItemCollection() item_collection.load_items_csv( @@ -306,7 +308,8 @@ def load_cosine_classifier( gt_intents.append(Intent(turn["intent"])) utterances.append( Utterance( - turn["utterance"], participant=DialogueParticipant.AGENT + turn["utterance"], + participant=DialogueParticipant.AGENT, ) ) intent_classifier = IntentClassifierCosine(intents=gt_intents) diff --git a/usersimcrs/simulator/agenda_based/agenda_based_simulator.py b/usersimcrs/simulator/agenda_based/agenda_based_simulator.py index 131fc9ae..50680c49 100644 --- a/usersimcrs/simulator/agenda_based/agenda_based_simulator.py +++ b/usersimcrs/simulator/agenda_based/agenda_based_simulator.py @@ -5,16 +5,18 @@ from dialoguekit.core.annotated_utterance import AnnotatedUtterance from dialoguekit.core.annotation import Annotation -from dialoguekit.core.domain import Domain from dialoguekit.core.intent import Intent from dialoguekit.core.utterance import Utterance from dialoguekit.nlg import ConditionalNLG from dialoguekit.nlu.nlu import NLU from nltk.stem import WordNetLemmatizer +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection from usersimcrs.items.ratings import Ratings -from usersimcrs.simulator.agenda_based.interaction_model import InteractionModel +from usersimcrs.simulator.agenda_based.interaction_model import ( + InteractionModel, +) from usersimcrs.simulator.user_simulator import UserSimulator from usersimcrs.user_modeling.preference_model import PreferenceModel @@ -29,7 +31,7 @@ def __init__( interaction_model: InteractionModel, nlu: NLU, nlg: ConditionalNLG, - domain: Domain, + domain: SimulationDomain, item_collection: ItemCollection, ratings: Ratings, ) -> None: @@ -137,7 +139,9 @@ def _generate_elicit_response_intent_and_annotations( return self._interaction_model.INTENT_DONT_KNOW, None # type: ignore[attr-defined] # noqa - def _generate_item_preference_response_intent(self, item_id: str) -> Intent: + def _generate_item_preference_response_intent( + self, item_id: str + ) -> Intent: """Generates response preference intent for a given item id. Args: @@ -220,7 +224,9 @@ def generate_response( agent_intent ): possible_items = ( - self._item_collection.get_items_by_properties(agent_annotations) + self._item_collection.get_items_by_properties( + agent_annotations + ) if agent_annotations else [] ) diff --git a/usersimcrs/user_modeling/pkg_preference_model.py b/usersimcrs/user_modeling/pkg_preference_model.py index e4c2e9a4..b424aa94 100644 --- a/usersimcrs/user_modeling/pkg_preference_model.py +++ b/usersimcrs/user_modeling/pkg_preference_model.py @@ -8,8 +8,8 @@ depends on the release of the PKG API. See: https://github.com/iai-group/UserSimCRS/issues/110 """ -from dialoguekit.core.domain import Domain +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection from usersimcrs.items.ratings import Ratings from usersimcrs.user_modeling.preference_model import PreferenceModel @@ -18,7 +18,7 @@ class PKGPreferenceModel(PreferenceModel): def __init__( self, - domain: Domain, + domain: SimulationDomain, item_collection: ItemCollection, historical_ratings: Ratings, historical_user_id: str = None, diff --git a/usersimcrs/user_modeling/preference_model.py b/usersimcrs/user_modeling/preference_model.py index 649f3e14..bd48c811 100644 --- a/usersimcrs/user_modeling/preference_model.py +++ b/usersimcrs/user_modeling/preference_model.py @@ -10,8 +10,7 @@ from abc import ABC, abstractmethod from typing import Tuple -from dialoguekit.core.domain import Domain - +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection from usersimcrs.items.ratings import Ratings @@ -30,7 +29,7 @@ class PreferenceModel(ABC): def __init__( self, - domain: Domain, + domain: SimulationDomain, item_collection: ItemCollection, historical_ratings: Ratings, historical_user_id: str = None, diff --git a/usersimcrs/user_modeling/simple_preference_model.py b/usersimcrs/user_modeling/simple_preference_model.py index 6f399b94..822e0c38 100644 --- a/usersimcrs/user_modeling/simple_preference_model.py +++ b/usersimcrs/user_modeling/simple_preference_model.py @@ -23,9 +23,9 @@ import random -from dialoguekit.core.domain import Domain from dialoguekit.participant.user_preferences import UserPreferences +from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection from usersimcrs.items.ratings import Ratings from usersimcrs.user_modeling.preference_model import ( @@ -37,7 +37,7 @@ class SimplePreferenceModel(PreferenceModel): def __init__( self, - domain: Domain, + domain: SimulationDomain, item_collection: ItemCollection, historical_ratings: Ratings, historical_user_id: str = None, @@ -74,7 +74,9 @@ def get_item_preference(self, item_id: str) -> float: ValueError: If the item does not exist in the collection. """ self._assert_item_exists(item_id) - preference = self._item_preferences.get_preference(KEY_ITEM_ID, item_id) + preference = self._item_preferences.get_preference( + KEY_ITEM_ID, item_id + ) if not preference: preference = random.choice([-1, 1]) self._item_preferences.set_preference( @@ -96,5 +98,7 @@ def get_slot_value_preference(self, slot: str, value: str) -> float: preference = self._slot_value_preferences.get_preference(slot, value) if not preference: preference = random.choice([-1, 1]) - self._slot_value_preferences.set_preference(slot, value, preference) + self._slot_value_preferences.set_preference( + slot, value, preference + ) return preference From ea422e54b693192f002f49f2348506a1823198ac Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 13 May 2024 10:06:52 +0200 Subject: [PATCH 03/24] Fix version rasa --- requirements/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ed190921..83788425 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,6 +7,7 @@ mypy docformatter types-PyYAML types-requests +rasa==3.4.0 dialoguekit==0.0.8 confuse websockets<11.0 From 025da95f9918c8a6dea2ffd79abefe52dea25c5b Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 13 May 2024 10:15:54 +0200 Subject: [PATCH 04/24] Update requirements to reduce backtracking --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 83788425..65ed7069 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,7 +7,7 @@ mypy docformatter types-PyYAML types-requests -rasa==3.4.0 +botocore>=1.29.29 dialoguekit==0.0.8 confuse websockets<11.0 From 3e7e45f750fab75d52d16ae8c7b9c0f2dc90715d Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 13 May 2024 10:22:48 +0200 Subject: [PATCH 05/24] Update test --- tests/core/test_information_need.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/core/test_information_need.py b/tests/core/test_information_need.py index 0f0f9907..7f3cb58e 100644 --- a/tests/core/test_information_need.py +++ b/tests/core/test_information_need.py @@ -51,8 +51,13 @@ def test_generate_information_need( item_collection: Item collection. """ information_need = generate_information_need(domain, item_collection) - assert information_need.constraints is not None - assert information_need.requested_slots is not None + assert all(information_need.constraints.values()) + assert all( + [ + slot in domain.get_requestable_slots() + for slot in information_need.requested_slots + ] + ) @pytest.fixture From e4315eccc60e6cc2ae821e14782614474640d541 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 13 May 2024 10:23:38 +0200 Subject: [PATCH 06/24] Format with black --- .../simulator/agenda_based/agenda_based_simulator.py | 8 ++------ usersimcrs/user_modeling/simple_preference_model.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/usersimcrs/simulator/agenda_based/agenda_based_simulator.py b/usersimcrs/simulator/agenda_based/agenda_based_simulator.py index 50680c49..c442b0a6 100644 --- a/usersimcrs/simulator/agenda_based/agenda_based_simulator.py +++ b/usersimcrs/simulator/agenda_based/agenda_based_simulator.py @@ -139,9 +139,7 @@ def _generate_elicit_response_intent_and_annotations( return self._interaction_model.INTENT_DONT_KNOW, None # type: ignore[attr-defined] # noqa - def _generate_item_preference_response_intent( - self, item_id: str - ) -> Intent: + def _generate_item_preference_response_intent(self, item_id: str) -> Intent: """Generates response preference intent for a given item id. Args: @@ -224,9 +222,7 @@ def generate_response( agent_intent ): possible_items = ( - self._item_collection.get_items_by_properties( - agent_annotations - ) + self._item_collection.get_items_by_properties(agent_annotations) if agent_annotations else [] ) diff --git a/usersimcrs/user_modeling/simple_preference_model.py b/usersimcrs/user_modeling/simple_preference_model.py index 822e0c38..acdc2469 100644 --- a/usersimcrs/user_modeling/simple_preference_model.py +++ b/usersimcrs/user_modeling/simple_preference_model.py @@ -74,9 +74,7 @@ def get_item_preference(self, item_id: str) -> float: ValueError: If the item does not exist in the collection. """ self._assert_item_exists(item_id) - preference = self._item_preferences.get_preference( - KEY_ITEM_ID, item_id - ) + preference = self._item_preferences.get_preference(KEY_ITEM_ID, item_id) if not preference: preference = random.choice([-1, 1]) self._item_preferences.set_preference( @@ -98,7 +96,5 @@ def get_slot_value_preference(self, slot: str, value: str) -> float: preference = self._slot_value_preferences.get_preference(slot, value) if not preference: preference = random.choice([-1, 1]) - self._slot_value_preferences.set_preference( - slot, value, preference - ) + self._slot_value_preferences.set_preference(slot, value, preference) return preference From a1a82b99e85f96f9d563f904b7990133bb7135e6 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Mon, 13 May 2024 10:27:24 +0200 Subject: [PATCH 07/24] Format docstring --- usersimcrs/core/information_need.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/usersimcrs/core/information_need.py b/usersimcrs/core/information_need.py index cf55e94c..df5a9f68 100644 --- a/usersimcrs/core/information_need.py +++ b/usersimcrs/core/information_need.py @@ -2,7 +2,8 @@ The information need is divided into two parts: constraints and requests. The constraints specify the slot-value pairs that the item of interest must satisfy, -while the requests specify the slots for which the user wants information.""" +while the requests specify the slots for which the user wants information. +""" from __future__ import annotations @@ -57,7 +58,7 @@ def __init__( """ self.constraints = constraints self.requested_slots = defaultdict( - Any, {slot: None for slot in requests} + None, {slot: None for slot in requests} ) def get_constraint_value(self, slot: str) -> Any: From 3ac51929aaf1934806f632405dbf208749ce3cd2 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 29 May 2024 11:00:58 +0200 Subject: [PATCH 08/24] Add dialogue state and dialogue state tracker --- usersimcrs/dialogue_management/__init__.py | 0 .../dialogue_management/dialogue_state.py | 31 ++++++++++ .../dialogue_state_tracker.py | 56 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 usersimcrs/dialogue_management/__init__.py create mode 100644 usersimcrs/dialogue_management/dialogue_state.py create mode 100644 usersimcrs/dialogue_management/dialogue_state_tracker.py diff --git a/usersimcrs/dialogue_management/__init__.py b/usersimcrs/dialogue_management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/usersimcrs/dialogue_management/dialogue_state.py b/usersimcrs/dialogue_management/dialogue_state.py new file mode 100644 index 00000000..d10096c6 --- /dev/null +++ b/usersimcrs/dialogue_management/dialogue_state.py @@ -0,0 +1,31 @@ +"""Representation of the dialogue state. + +The dialogue state includes the turn count, a list of dialogue acts per turn +for both the agent and the user, and the belief state. The belief state is a +dictionary that maps slot names to slot values. +""" + +from collections import defaultdict +from dataclasses import dataclass, field +from typing import DefaultDict, List + +from dialoguekit.core.dialogue_act import DialogueAct + + +@dataclass +class DialogueState: + """Dialogue state. + + Attributes: + turn_count: Turn count. + agent_dacts: List of dialogue acts per turn for the agent. + user_dacts: List of dialogue acts per turn for the user. + belief_state: Belief state. + """ + + turn_count: int = 0 + agent_dacts: List[List[DialogueAct]] = field(default_factory=list) + user_dacts: List[List[DialogueAct]] = field(default_factory=list) + belief_state: DefaultDict[str, str] = field( + default_factory=lambda: defaultdict(str) + ) diff --git a/usersimcrs/dialogue_management/dialogue_state_tracker.py b/usersimcrs/dialogue_management/dialogue_state_tracker.py new file mode 100644 index 00000000..6569d87f --- /dev/null +++ b/usersimcrs/dialogue_management/dialogue_state_tracker.py @@ -0,0 +1,56 @@ +"""Interface for dialogue state tracking.""" + +from typing import List + +from dialoguekit.core.dialogue_act import DialogueAct +from dialoguekit.participant import DialogueParticipant + +from usersimcrs.dialogue_management.dialogue_state import DialogueState + + +class DialogueStateTracker: + def __init__(self) -> None: + """Initializes the dialogue state tracker.""" + self._dialogue_state = DialogueState() + + def get_current_state(self) -> DialogueState: + """Returns the current dialogue state. + + Returns: + DialogueState. + """ + return self._dialogue_state + + def update_state( + self, + dialogue_acts: List[DialogueAct], + participant: DialogueParticipant, + ) -> None: + """Updates the dialogue state based on the dialogue acts. + + Args: + dialogue_acts: Dialogue acts. + participant: Dialogue participant. + """ + if participant == DialogueParticipant.USER: + self._dialogue_state.user_dacts.append(dialogue_acts) + else: + self._dialogue_state.agent_dacts.append(dialogue_acts) + + self.update_belief_state(dialogue_acts) + + def update_belief_state(self, dialogue_acts: List[DialogueAct]) -> None: + """Updates the belief state based on the dialogue acts. + + Args: + dialogue_acts: Dialogue acts. + """ + for dialogue_act in dialogue_acts: + for annotation in dialogue_act.annotations: + self._dialogue_state.belief_state[ + annotation.slot + ] = annotation.value + + def reset_state(self) -> None: + """Resets the dialogue state.""" + self._dialogue_state = DialogueState() From b7208c549539543808a73cf34179073cd2d14407 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 29 May 2024 11:14:01 +0200 Subject: [PATCH 09/24] Add tests for DST --- .../test_dialogue_state_tracker.py | 66 +++++++++++++++++++ .../dialogue_state_tracker.py | 1 + 2 files changed, 67 insertions(+) create mode 100644 tests/dialogue_management/test_dialogue_state_tracker.py diff --git a/tests/dialogue_management/test_dialogue_state_tracker.py b/tests/dialogue_management/test_dialogue_state_tracker.py new file mode 100644 index 00000000..2e465bb5 --- /dev/null +++ b/tests/dialogue_management/test_dialogue_state_tracker.py @@ -0,0 +1,66 @@ +"""Tests for the dialogue state tracker module.""" + +import pytest +from dialoguekit.core.annotation import Annotation +from dialoguekit.core.dialogue_act import DialogueAct +from dialoguekit.core.intent import Intent +from dialoguekit.participant import DialogueParticipant + +from usersimcrs.dialogue_management.dialogue_state_tracker import ( + DialogueStateTracker, +) + + +@pytest.fixture(scope="module") +def dialogue_state_tracker() -> DialogueStateTracker: + """Fixture for the dialogue state tracker.""" + dst = DialogueStateTracker() + + initial_state = dst.get_current_state() + assert initial_state.turn_count == 0 + assert initial_state.agent_dacts == [] + assert initial_state.user_dacts == [] + assert initial_state.belief_state == {} + return dst + + +def test_update_state_agent( + dialogue_state_tracker: DialogueStateTracker, +) -> None: + """Tests dialogue state update with agent dialogue acts.""" + dialogue_acts = [ + DialogueAct(Intent("greets")), + DialogueAct(Intent("elicit"), annotations=[Annotation("GENRE")]), + ] + + dialogue_state_tracker.update_state( + dialogue_acts, DialogueParticipant.AGENT + ) + + current_state = dialogue_state_tracker.get_current_state() + assert current_state.turn_count == 1 + assert current_state.agent_dacts == [dialogue_acts] + assert current_state.user_dacts == [] + assert current_state.belief_state == {"GENRE": None} + + +def test_update_state_user( + dialogue_state_tracker: DialogueStateTracker, +) -> None: + """Tests dialogue state update with user dialogue acts.""" + dialogue_acts = [ + DialogueAct( + Intent("inform"), annotations=[Annotation("GENRE", "comedy")] + ), + DialogueAct(Intent("request"), annotations=[Annotation("YEAR")]), + ] + + dialogue_state_tracker.update_state( + dialogue_acts, DialogueParticipant.USER + ) + + current_state = dialogue_state_tracker.get_current_state() + assert current_state.turn_count == 2 + assert len(current_state.agent_dacts) == 1 + assert current_state.user_dacts == [dialogue_acts] + assert current_state.belief_state == {"GENRE": "comedy", "YEAR": None} diff --git a/usersimcrs/dialogue_management/dialogue_state_tracker.py b/usersimcrs/dialogue_management/dialogue_state_tracker.py index 6569d87f..7248652e 100644 --- a/usersimcrs/dialogue_management/dialogue_state_tracker.py +++ b/usersimcrs/dialogue_management/dialogue_state_tracker.py @@ -38,6 +38,7 @@ def update_state( self._dialogue_state.agent_dacts.append(dialogue_acts) self.update_belief_state(dialogue_acts) + self._dialogue_state.turn_count += 1 def update_belief_state(self, dialogue_acts: List[DialogueAct]) -> None: """Updates the belief state based on the dialogue acts. From 1d2dacd38fd9b776e757ea8ec691035bbc001687 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 29 May 2024 11:26:12 +0200 Subject: [PATCH 10/24] Black --- tests/dialogue_management/test_dialogue_state_tracker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/dialogue_management/test_dialogue_state_tracker.py b/tests/dialogue_management/test_dialogue_state_tracker.py index 2e465bb5..6b673552 100644 --- a/tests/dialogue_management/test_dialogue_state_tracker.py +++ b/tests/dialogue_management/test_dialogue_state_tracker.py @@ -55,9 +55,7 @@ def test_update_state_user( DialogueAct(Intent("request"), annotations=[Annotation("YEAR")]), ] - dialogue_state_tracker.update_state( - dialogue_acts, DialogueParticipant.USER - ) + dialogue_state_tracker.update_state(dialogue_acts, DialogueParticipant.USER) current_state = dialogue_state_tracker.get_current_state() assert current_state.turn_count == 2 From 41e6fbaf3283d1ca61d0f561a218a649ab36ee28 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 29 May 2024 11:40:32 +0200 Subject: [PATCH 11/24] Implement transformer model --- .../simulator/neural_based/core/__init__.py | 0 .../neural_based/core/feature_handler.py | 23 ++++ .../neural_based/core/transformer.py | 118 ++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 usersimcrs/simulator/neural_based/core/__init__.py create mode 100644 usersimcrs/simulator/neural_based/core/feature_handler.py create mode 100644 usersimcrs/simulator/neural_based/core/transformer.py diff --git a/usersimcrs/simulator/neural_based/core/__init__.py b/usersimcrs/simulator/neural_based/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/usersimcrs/simulator/neural_based/core/feature_handler.py b/usersimcrs/simulator/neural_based/core/feature_handler.py new file mode 100644 index 00000000..1095986f --- /dev/null +++ b/usersimcrs/simulator/neural_based/core/feature_handler.py @@ -0,0 +1,23 @@ +"""Interface to build feature vector for neural-based user simulator.""" + +from abc import ABC, abstractmethod + +import torch +from dialoguekit.core.annotated_utterance import AnnotatedUtterance + + +class FeatureHandler(ABC): + @abstractmethod + def get_feature_vector( + self, utterance: AnnotatedUtterance, **kwargs + ) -> torch.Tensor: + """Builds a feature vector for a given utterance. + + Args: + utterance: Annotated utterance. + kwargs: Additional arguments. + + Raises: + NotImplementedError: If not implemented in derived class. + """ + raise NotImplementedError diff --git a/usersimcrs/simulator/neural_based/core/transformer.py b/usersimcrs/simulator/neural_based/core/transformer.py new file mode 100644 index 00000000..6058c92c --- /dev/null +++ b/usersimcrs/simulator/neural_based/core/transformer.py @@ -0,0 +1,118 @@ +"""Encoder-only transformer model for neural-based simulator.""" + +import math + +import torch +import torch.nn as nn + + +class PositionalEncoding(nn.Module): + def __init__( + self, + d_model: int, + dropout: float = 0.1, + max_len: int = 5000, + **kwargs, + ) -> None: + """Initializes positional encoding layer. + + Args: + d_model: Dimension of the model. + dropout: Dropout rate. Defaults to 0.1. + max_len: Maximum length of the input sequence. Defaults to 5000. + """ + super(PositionalEncoding, self).__init__() + self.dropout = nn.Dropout(p=dropout) + + position = torch.arange(max_len).unsqueeze(1) + div_term = torch.exp( + torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model) + ) + pe = torch.zeros(max_len, 1, d_model) + pe[:, 0, 0::2] = torch.sin(position * div_term) + pe[:, 0, 1::2] = torch.cos(position * div_term) + self.register_buffer("pe", pe) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """Performs forward pass. + + Args: + x: Input tensor. + + Returns: + Positional encoded tensor. + """ + x = x + self.pe[: x.size(0)] + return self.dropout(x) + + +class TransformerEncoderModel(nn.Module): + def __init__( + self, + input_dim: int, + output_dim: int, + nhead: int, + hidden_dim: int, + num_encoder_layers: int, + num_token: int, + dropout: float = 0.5, + ) -> None: + """Initializes a encoder-only transformer model. + + Args: + input_dim: Size of the input vector. + output_dim: Size of the output vector. + nhead: Number of heads. + hidden_dim: Hidden dimension. + num_encoder_layers: Number of encoder layers. + num_token: Number of tokens in the vocabulary. + dropout: Dropout rate. Defaults to 0.5. + """ + super(TransformerEncoderModel, self).__init__() + self.d_model = input_dim + + self.pos_encoder = PositionalEncoding(input_dim, dropout) + self.embedding = nn.Embedding(num_token, input_dim) + + # Encoder layers + norm_layer = nn.LayerNorm(input_dim) + encoder_layer = nn.TransformerEncoderLayer( + d_model=input_dim, + nhead=nhead, + dim_feedforward=hidden_dim, + ) + self.encoder = nn.TransformerEncoder( + encoder_layer, + num_layers=num_encoder_layers, + norm=norm_layer, + ) + + self.linear = nn.Linear(input_dim, output_dim) + self.softmax = nn.Softmax(dim=-1) + + self.init_weights() + + def init_weights(self) -> None: + """Initializes weights of the network.""" + initrange = 0.1 + self.embedding.weight.data.uniform_(-initrange, initrange) + self.linear.bias.data.zero_() + self.linear.weight.data.uniform_(-initrange, initrange) + + def forward( + self, src: torch.Tensor, src_mask: torch.Tensor = None + ) -> torch.Tensor: + """Performs forward pass. + + Args: + src: Source tensor. + src_mask: Mask tensor. + + Returns: + Output tensor. + """ + src = self.embedding(src) * math.sqrt(self.d_model) + src = self.pos_encoder(src) + output = self.encoder(src, mask=src_mask) + output = self.linear(output) + return output From 853cfbb31861fb784c99f9462d1fc8fe669c54e3 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 29 May 2024 11:48:37 +0200 Subject: [PATCH 12/24] Fix pre-commit --- usersimcrs/dialogue_management/dialogue_state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usersimcrs/dialogue_management/dialogue_state.py b/usersimcrs/dialogue_management/dialogue_state.py index d10096c6..8c504191 100644 --- a/usersimcrs/dialogue_management/dialogue_state.py +++ b/usersimcrs/dialogue_management/dialogue_state.py @@ -1,7 +1,7 @@ """Representation of the dialogue state. -The dialogue state includes the turn count, a list of dialogue acts per turn -for both the agent and the user, and the belief state. The belief state is a +The dialogue state includes the turn count, a list of dialogue acts per turn for +both the agent and the user, and the belief state. The belief state is a dictionary that maps slot names to slot values. """ From 7b8ba168047cb4282e3964d911a12ea99a7194c2 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 29 May 2024 13:02:24 +0200 Subject: [PATCH 13/24] Add TUS feature handler --- tests/core/test_information_need.py | 4 +- .../simulator/tus/test_tus_feature_handler.py | 214 ++++++++++++++ .../neural_based/tus/tus_feature_handler.py | 267 ++++++++++++++++++ 3 files changed, 482 insertions(+), 3 deletions(-) create mode 100644 tests/simulator/tus/test_tus_feature_handler.py create mode 100644 usersimcrs/simulator/neural_based/tus/tus_feature_handler.py diff --git a/tests/core/test_information_need.py b/tests/core/test_information_need.py index 70ab9e81..5c22c241 100644 --- a/tests/core/test_information_need.py +++ b/tests/core/test_information_need.py @@ -20,9 +20,7 @@ def test_generate_random_information_need( domain: Simulation domain. item_collection: Item collection. """ - information_need = generate_random_information_need( - domain, item_collection - ) + information_need = generate_random_information_need(domain, item_collection) assert all(information_need.constraints.values()) assert all( [ diff --git a/tests/simulator/tus/test_tus_feature_handler.py b/tests/simulator/tus/test_tus_feature_handler.py new file mode 100644 index 00000000..81c30d39 --- /dev/null +++ b/tests/simulator/tus/test_tus_feature_handler.py @@ -0,0 +1,214 @@ +"""Tests for TUS's feature handler.""" + +from typing import List + +import pytest +from dialoguekit.core.annotated_utterance import AnnotatedUtterance +from dialoguekit.core.annotation import Annotation +from dialoguekit.core.dialogue_act import DialogueAct +from dialoguekit.participant import DialogueParticipant + +from usersimcrs.core.information_need import InformationNeed +from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.dialogue_management.dialogue_state import DialogueState +from usersimcrs.simulator.neural_based.tus.tus_feature_handler import ( + TUSFeatureHandler, +) + + +@pytest.fixture +def feature_handler() -> TUSFeatureHandler: + """Returns the feature handler.""" + _feature_handler = TUSFeatureHandler( + domain=SimulationDomain("tests/data/domains/movies.yaml"), + user_actions=["inform", "request"], + agent_actions=["elicit", "recommend", "bye"], + ) + + assert _feature_handler._user_actions == ["inform", "request"] + assert _feature_handler._agent_actions == [ + "elicit", + "recommend", + "bye", + ] + + return _feature_handler + + +def test__create_slot_index(feature_handler: TUSFeatureHandler) -> None: + """Tests the creation of the slot index.""" + assert all( + slot in feature_handler.slot_index.keys() + for slot in [ + "TITLE", + "GENRE", + "ACTOR", + "KEYWORD", + "DIRECTOR", + "PLOT", + "RATING", + ] + ) + + +@pytest.mark.parametrize( + "slot, previous_state, state, expected_representation", + [ + ( + "DIRECTOR", + DialogueState(), + DialogueState(), + [0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0], + ), + ( + "DIRECTOR", + DialogueState(), + DialogueState( + user_dacts=[ + DialogueAct( + "inform", [Annotation("DIRECTOR", "Steven Spielberg")] + ) + ], + belief_state={"DIRECTOR": "Steven Spielberg"}, + ), + [0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1], + ), + ( + "KEYWORD", + DialogueState(), + DialogueState( + agent_dacts=[DialogueAct("elicit", [Annotation("KEYWORD")])], + belief_state={"KEYWORD": None}, + ), + [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], + ), + ], +) +def test_get_basic_information_feature( + feature_handler: TUSFeatureHandler, + information_need: InformationNeed, + slot: str, + previous_state: DialogueState, + state: DialogueState, + expected_representation: List[int], +) -> None: + """Tests the basic information feature.""" + assert ( + feature_handler.get_basic_information_feature( + slot, information_need, state, previous_state + ) + == expected_representation + ) + + +@pytest.mark.parametrize( + "dialogue_acts, expected_representation", + [ + ([], [0, 0, 0, 0, 0, 0, 0, 0, 0]), + ( + [DialogueAct("elicit", [Annotation("GENRE")])], + [0, 1, 0, 0, 0, 0, 0, 0, 0], + ), + ( + [ + DialogueAct( + "recommend", [Annotation("TITLE", "The Godfather")] + ), + DialogueAct("bye"), + ], + [0, 0, 0, 0, 0, 1, 1, 0, 0], + ), + ], +) +def test_get_agent_action_feature( + dialogue_acts: List[DialogueAct], + expected_representation: List[int], + feature_handler: TUSFeatureHandler, +) -> None: + """Tests the agent action feature.""" + assert ( + feature_handler.get_agent_action_feature(dialogue_acts) + == expected_representation + ) + + +def test_get_slot_index_feature(feature_handler: TUSFeatureHandler) -> None: + """Tests the slot index feature.""" + slots = list(feature_handler.slot_index.keys()) + i = slots.index("GENRE") + expected = [0] * len(slots) + expected[i] = 1 + assert feature_handler.get_slot_index_feature("GENRE") == expected + + +@pytest.mark.parametrize( + "user_action_vector", + [ + None, + [0, 1, 0, 0, 0, 0], + ], +) +def test_get_slot_feature_vector( + user_action_vector: List[int], + feature_handler: TUSFeatureHandler, + information_need: InformationNeed, +) -> None: + """Tests the slot feature vector.""" + slot_feature_vector = feature_handler.get_slot_feature_vector( + "DIRECTOR", + DialogueState(), + DialogueState( + user_dacts=[ + DialogueAct( + "inform", [Annotation("DIRECTOR", "Steven Spielberg")] + ) + ], + belief_state={"DIRECTOR": "Steven Spielberg"}, + ), + information_need, + [DialogueAct("elicit", [Annotation("GENRE")])], + user_action_vector, + ) + user_action_vector = user_action_vector if user_action_vector else [0] * 6 + assert slot_feature_vector[21:27] == user_action_vector + + +@pytest.mark.parametrize( + "utterance, expected_num_action_slots", + [ + ( + AnnotatedUtterance( + "What genre are you interested in?", + participant=DialogueParticipant.AGENT, + intent="elicit", + annotations=[Annotation("GENRE")], + ), + 4, + ), + ( + AnnotatedUtterance( + "Who should be the main actor?", + participant=DialogueParticipant.AGENT, + intent="elicit", + annotations=[Annotation("ACTOR")], + ), + 5, + ), + ], +) +def test_get_feature_vector( + utterance: AnnotatedUtterance, + expected_num_action_slots: int, + feature_handler: TUSFeatureHandler, + information_need: InformationNeed, +) -> None: + """Tests the turn feature vector.""" + turn_vector = feature_handler.get_feature_vector( + utterance, + DialogueState(), + DialogueState(), + information_need, + {"GENRE": [0, 1, 0, 0, 0, 0]}, + ) + assert len(turn_vector) == expected_num_action_slots + assert len(turn_vector[0]) == len(turn_vector[1]) == 35 diff --git a/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py b/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py new file mode 100644 index 00000000..1dc5d560 --- /dev/null +++ b/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py @@ -0,0 +1,267 @@ +"""Feature handler for the TUS simulator. + +For a matter of simplicity, the feature handler supports only one domain unlike +the original implementation. +""" + +from typing import Dict, List + +from dialoguekit.core.annotated_utterance import AnnotatedUtterance +from dialoguekit.core.dialogue_act import DialogueAct + +from usersimcrs.core.information_need import InformationNeed +from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.dialogue_management.dialogue_state import DialogueState +from usersimcrs.simulator.neural_based.core.feature_handler import ( + FeatureHandler, +) + + +class TUSFeatureHandler(FeatureHandler): + def __init__( + self, + domain: SimulationDomain, + agent_actions: List[str], + user_actions: List[str] = ["inform", "request"], + ) -> None: + """Initializes the feature handler. + + Args: + domain: Domain knowledge. + agent_actions: Agent actions. + user_actions: User actions. Defaults to ["inform", "request"]. + """ + self._domain = domain + self._user_actions = user_actions + self._agent_actions = agent_actions + self.action_slots = set() + self._create_slot_index() + + def _create_slot_index(self) -> Dict[str, int]: + """Creates an index for slots. + + Returns: + Slot index. + """ + slots = set( + self._domain.get_slot_names() + + self._domain.get_requestable_slots() + ) + self.slot_index = {slot: index for index, slot in enumerate(slots)} + + def get_basic_information_feature( + self, + slot: str, + information_need: InformationNeed, + state: DialogueState, + previous_state: DialogueState, + ) -> List[int]: + """Builds feature vector for basic information. + + It concatenates the value in agent state, user state, slot type, + completion status, and first mention. + + Args: + slot: Slot. + information_need: Information need. + state: Current state. + previous_state: Previous state. + + Returns: + Feature vector for basic information. + """ + # Represents the value of the slot in the user/agent state. + # It is a 4-dimensional vector, where each dimension corresponds to the + # following values: "none", "?", "don't care", and "other values". + v_user_value = [0] * 4 + if ( + slot not in information_need.constraints.keys() + and slot not in information_need.requested_slots.keys() + ): + v_user_value[0] = 1 + elif ( + slot in information_need.requested_slots.keys() + and information_need.requested_slots.get(slot) is None + ): + v_user_value[1] = 1 + elif information_need.get_constraint_value(slot) == "dontcare": + v_user_value[2] = 1 + else: + v_user_value[3] = 1 + + v_agent_value = [0] * 4 + if slot not in state.belief_state.keys(): + v_agent_value[0] = 1 + elif ( + slot in state.belief_state.keys() + and state.belief_state.get(slot) is None + ): + v_agent_value[1] = 1 + elif state.belief_state.get(slot) == "dontcare": + v_agent_value[2] = 1 + else: + v_agent_value[3] = 1 + + # Whether or not the slot is a constraint or requestable slot + v_type = [0, 0] + if slot in information_need.constraints.keys(): + v_type[0] = 1 + if slot in information_need.requested_slots.keys(): + v_type[1] = 1 + + # Whether or not a constraint or informable slot has been fulfilled + v_ful = ( + [1] + if ( + state.belief_state.get(slot) is not None + and state.belief_state.get(slot) + == information_need.get_constraint_value(slot) + ) + or information_need.requested_slots.get(slot) + else [0] + ) + + # Whether or not this is the first mention of the slot + v_first = [0] + if ( + slot not in previous_state.belief_state.keys() + and slot in state.belief_state.keys() + ): + v_first = [1] + + return v_user_value + v_agent_value + v_type + v_ful + v_first + + def get_agent_action_feature( + self, agent_dacts: List[DialogueAct] + ) -> List[int]: + """Builds feature vector for agent action. + + It concatenates action vectors represented as 3-dimensional vectors that + describe whether the slot and value are absent, only slot is present, or + both slot and value are present. + + Args: + agent_dacts: Agent dialogue acts. + + Returns: + Feature vector for agent action. + """ + v_agent_action = {intent: [0] * 3 for intent in self._agent_actions} + for agent_dact in agent_dacts: + if agent_dact.intent in self._agent_actions: + if not agent_dact.annotations: + v_agent_action[agent_dact.intent][0] = 1 + for annotation in agent_dact.annotations: + if annotation.slot and annotation.value: + v_agent_action[agent_dact.intent][2] = 1 + elif annotation.slot and annotation.value is None: + v_agent_action[agent_dact.intent][1] = 1 + return sum(v_agent_action.values(), []) + + def get_slot_index_feature(self, slot: str) -> List[int]: + """Builds feature vector for slot index. + + Args: + slot: Slot. + + Returns: + Feature vector for slot index. + """ + v_slot_index = [0] * len(self.slot_index) + v_slot_index[self.slot_index[slot]] = 1 + return v_slot_index + + def get_slot_feature_vector( + self, + slot: str, + previous_state: DialogueState, + state: DialogueState, + information_need: InformationNeed, + agent_dacts: List[DialogueAct], + user_action_vector: List[int] = None, + ) -> List[int]: + """Builds the feature vector for a slot. + + It concatenate the basic information, user action, agent action, and + slot index feature vectors. + + Args: + slot: Slot. + previous_state: Previous state. + state: Current state. + information_need: Information need. + agent_dact: Agent dialogue acts. + user_action_vector: User action feature vector (output vector for + previous turn). Defaults to None. + + Returns: + Feature vector for the slot. + """ + v_user_action = user_action_vector if user_action_vector else [0] * 6 + agent_dacts = [] + for dact in agent_dacts: + for annotation in dact.annotations: + if annotation.slot == slot or annotation.slot is None: + agent_dacts.append(dact) + + return ( + self.get_basic_information_feature( + slot, information_need, state, previous_state + ) + + self.get_agent_action_feature(agent_dacts) + + v_user_action + + self.get_slot_index_feature(slot) + ) + + def get_feature_vector( + self, + utterance: AnnotatedUtterance, + previous_state: DialogueState, + state: DialogueState, + information_need: InformationNeed, + user_action_vectors: Dict[str, List[int]] = {}, + ) -> List[int]: + """Builds the feature vector for a turn. + + It comprises the feature vectors for all slots that in the + information need and mentioned during the conversation. + + Args: + utterance: Agent utterance with annotations. + slots: Slots. + previous_state: Previous state. + state: Current state. + information_need: Information need. + user_action_vectors: User action feature vectors per slot. Defaults + to an empty dictionary. + + Returns: + Feature vector for the turn. + """ + try: + agent_dacts = utterance.dialogue_acts + except AttributeError: + agent_dacts = [ + DialogueAct(utterance.intent, utterance.annotations) + ] + + self.action_slots.update( + [ + annotation.slot + for dact in agent_dacts + for annotation in dact.annotations + ] + + list(information_need.constraints.keys()) + + list(information_need.requested_slots.keys()) + ) + return [ + self.get_slot_feature_vector( + slot, + previous_state, + state, + information_need, + agent_dacts, + user_action_vectors.get(slot, None), + ) + for slot in self.action_slots + ] From b511be182805e54132bb7ed87068ee3c725fb288 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 29 May 2024 13:06:18 +0200 Subject: [PATCH 14/24] Update tests --- tests/conftest.py | 21 +++++++++++++++++++++ tests/core/test_information_need.py | 23 +++-------------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 85a0f577..5c963374 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,9 @@ import pytest +from usersimcrs.core.information_need import InformationNeed from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.items.item import Item from usersimcrs.items.item_collection import ItemCollection DOMAIN_YAML_FILE = "tests/data/domains/movies.yaml" @@ -43,3 +45,22 @@ def item_collection(domain: SimulationDomain): yield item_collection item_collection.close() os.remove("tests/data/items.db") + + +@pytest.fixture(scope="module") +def information_need() -> InformationNeed: + """Information need fixture.""" + constraints = {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"} + requests = ["PLOT", "RATING"] + target_items = [ + Item( + "1", + { + "GENRE": "Comedy", + "DIRECTOR": "Steven Spielberg", + "RATING": 4.5, + "PLOT": "A movie plot", + }, + ) + ] + return InformationNeed(target_items, constraints, requests) diff --git a/tests/core/test_information_need.py b/tests/core/test_information_need.py index 5c22c241..b27329f0 100644 --- a/tests/core/test_information_need.py +++ b/tests/core/test_information_need.py @@ -20,7 +20,9 @@ def test_generate_random_information_need( domain: Simulation domain. item_collection: Item collection. """ - information_need = generate_random_information_need(domain, item_collection) + information_need = generate_random_information_need( + domain, item_collection + ) assert all(information_need.constraints.values()) assert all( [ @@ -31,25 +33,6 @@ def test_generate_random_information_need( assert len(information_need.target_items) == 1 -@pytest.fixture -def information_need() -> InformationNeed: - """Information need fixture.""" - constraints = {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"} - requests = ["PLOT", "RATING"] - target_items = [ - Item( - "1", - { - "GENRE": "Comedy", - "DIRECTOR": "Steven Spielberg", - "RATING": 4.5, - "PLOT": "A movie plot", - }, - ) - ] - return InformationNeed(target_items, constraints, requests) - - @pytest.mark.parametrize( "slot,expected_value", [ From 7daa047f40a5a07c61314cdbdaae1ded2e631a55 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 29 May 2024 13:06:44 +0200 Subject: [PATCH 15/24] Black --- tests/core/test_information_need.py | 4 +--- .../simulator/neural_based/tus/tus_feature_handler.py | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/core/test_information_need.py b/tests/core/test_information_need.py index b27329f0..c75fd8c0 100644 --- a/tests/core/test_information_need.py +++ b/tests/core/test_information_need.py @@ -20,9 +20,7 @@ def test_generate_random_information_need( domain: Simulation domain. item_collection: Item collection. """ - information_need = generate_random_information_need( - domain, item_collection - ) + information_need = generate_random_information_need(domain, item_collection) assert all(information_need.constraints.values()) assert all( [ diff --git a/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py b/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py index 1dc5d560..496318b6 100644 --- a/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py +++ b/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py @@ -44,8 +44,7 @@ def _create_slot_index(self) -> Dict[str, int]: Slot index. """ slots = set( - self._domain.get_slot_names() - + self._domain.get_requestable_slots() + self._domain.get_slot_names() + self._domain.get_requestable_slots() ) self.slot_index = {slot: index for index, slot in enumerate(slots)} @@ -241,9 +240,7 @@ def get_feature_vector( try: agent_dacts = utterance.dialogue_acts except AttributeError: - agent_dacts = [ - DialogueAct(utterance.intent, utterance.annotations) - ] + agent_dacts = [DialogueAct(utterance.intent, utterance.annotations)] self.action_slots.update( [ From 763008bdcb68937b9b26fb7356c88465da1d656d Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Thu, 30 May 2024 14:04:20 +0200 Subject: [PATCH 16/24] Update feature handler --- tests/conftest.py | 4 +- .../simulator/tus/test_tus_feature_handler.py | 89 ++++++++-- .../neural_based/tus/tus_feature_handler.py | 158 ++++++++++++++---- 3 files changed, 201 insertions(+), 50 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5c963374..c1093984 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -50,13 +50,13 @@ def item_collection(domain: SimulationDomain): @pytest.fixture(scope="module") def information_need() -> InformationNeed: """Information need fixture.""" - constraints = {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"} + constraints = {"GENRE": "comedy", "DIRECTOR": "Steven Spielberg"} requests = ["PLOT", "RATING"] target_items = [ Item( "1", { - "GENRE": "Comedy", + "GENRE": "comedy", "DIRECTOR": "Steven Spielberg", "RATING": 4.5, "PLOT": "A movie plot", diff --git a/tests/simulator/tus/test_tus_feature_handler.py b/tests/simulator/tus/test_tus_feature_handler.py index 81c30d39..74db4215 100644 --- a/tests/simulator/tus/test_tus_feature_handler.py +++ b/tests/simulator/tus/test_tus_feature_handler.py @@ -3,9 +3,11 @@ from typing import List import pytest +import torch from dialoguekit.core.annotated_utterance import AnnotatedUtterance from dialoguekit.core.annotation import Annotation from dialoguekit.core.dialogue_act import DialogueAct +from dialoguekit.core.intent import Intent from dialoguekit.participant import DialogueParticipant from usersimcrs.core.information_need import InformationNeed @@ -16,7 +18,7 @@ ) -@pytest.fixture +@pytest.fixture(scope="function") def feature_handler() -> TUSFeatureHandler: """Returns the feature handler.""" _feature_handler = TUSFeatureHandler( @@ -66,7 +68,8 @@ def test__create_slot_index(feature_handler: TUSFeatureHandler) -> None: DialogueState( user_dacts=[ DialogueAct( - "inform", [Annotation("DIRECTOR", "Steven Spielberg")] + Intent("inform"), + [Annotation("DIRECTOR", "Steven Spielberg")], ) ], belief_state={"DIRECTOR": "Steven Spielberg"}, @@ -77,7 +80,9 @@ def test__create_slot_index(feature_handler: TUSFeatureHandler) -> None: "KEYWORD", DialogueState(), DialogueState( - agent_dacts=[DialogueAct("elicit", [Annotation("KEYWORD")])], + agent_dacts=[ + DialogueAct(Intent("elicit"), [Annotation("KEYWORD")]) + ], belief_state={"KEYWORD": None}, ), [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], @@ -106,15 +111,15 @@ def test_get_basic_information_feature( [ ([], [0, 0, 0, 0, 0, 0, 0, 0, 0]), ( - [DialogueAct("elicit", [Annotation("GENRE")])], + [DialogueAct(Intent("elicit"), [Annotation("GENRE")])], [0, 1, 0, 0, 0, 0, 0, 0, 0], ), ( [ DialogueAct( - "recommend", [Annotation("TITLE", "The Godfather")] + Intent("recommend"), [Annotation("TITLE", "The Godfather")] ), - DialogueAct("bye"), + DialogueAct(Intent("bye")), ], [0, 0, 0, 0, 0, 1, 1, 0, 0], ), @@ -145,11 +150,11 @@ def test_get_slot_index_feature(feature_handler: TUSFeatureHandler) -> None: "user_action_vector", [ None, - [0, 1, 0, 0, 0, 0], + torch.tensor([0, 1, 0, 0, 0, 0]), ], ) def test_get_slot_feature_vector( - user_action_vector: List[int], + user_action_vector: torch.Tensor, feature_handler: TUSFeatureHandler, information_need: InformationNeed, ) -> None: @@ -160,17 +165,22 @@ def test_get_slot_feature_vector( DialogueState( user_dacts=[ DialogueAct( - "inform", [Annotation("DIRECTOR", "Steven Spielberg")] + Intent("inform"), + [Annotation("DIRECTOR", "Steven Spielberg")], ) ], belief_state={"DIRECTOR": "Steven Spielberg"}, ), information_need, - [DialogueAct("elicit", [Annotation("GENRE")])], + [DialogueAct(Intent("elicit"), [Annotation("GENRE")])], user_action_vector, ) - user_action_vector = user_action_vector if user_action_vector else [0] * 6 - assert slot_feature_vector[21:27] == user_action_vector + user_action_vector = ( + user_action_vector + if user_action_vector is not None + else torch.tensor([0] * 6) + ) + assert torch.equal(slot_feature_vector[21:27], user_action_vector) @pytest.mark.parametrize( @@ -180,7 +190,7 @@ def test_get_slot_feature_vector( AnnotatedUtterance( "What genre are you interested in?", participant=DialogueParticipant.AGENT, - intent="elicit", + intent=Intent("elicit"), annotations=[Annotation("GENRE")], ), 4, @@ -189,7 +199,7 @@ def test_get_slot_feature_vector( AnnotatedUtterance( "Who should be the main actor?", participant=DialogueParticipant.AGENT, - intent="elicit", + intent=Intent("elicit"), annotations=[Annotation("ACTOR")], ), 5, @@ -208,7 +218,52 @@ def test_get_feature_vector( DialogueState(), DialogueState(), information_need, - {"GENRE": [0, 1, 0, 0, 0, 0]}, + {"GENRE": torch.tensor([0, 1, 0, 0, 0, 0])}, + ) + assert len(turn_vector) == expected_num_action_slots * 35 + + +def test_get_label_vector( + feature_handler: TUSFeatureHandler, information_need: InformationNeed +) -> None: + """Tests generation of label vector for slot.""" + feature_handler.action_slots = set(["GENRE", "DIRECTOR"]) + feature_handler.action_slots.update(information_need.constraints.keys()) + feature_handler.action_slots.update(information_need.requested_slots.keys()) + + user_utterance = AnnotatedUtterance( + "I am looking for a comedy movie directed by Steven Spielberg.", + participant=DialogueParticipant.USER, + intent=Intent("inform"), + annotations=[ + Annotation("GENRE", "comedy"), + Annotation("DIRECTOR", "Steven Spielberg"), + ], + ) + state = DialogueState( + turn_count=1, + user_dacts=[ + DialogueAct( + Intent("inform"), + [ + Annotation("GENRE", "comedy"), + Annotation("DIRECTOR", "Steven Spielberg"), + ], + ) + ], + agent_dacts=[DialogueAct(Intent("greets"))], + belief_state={"GENRE": "comedy", "DIRECTOR": "Steven Spielberg"}, + ) + label_vector = feature_handler.get_label_vector( + user_utterance, state, information_need ) - assert len(turn_vector) == expected_num_action_slots - assert len(turn_vector[0]) == len(turn_vector[1]) == 35 + assert label_vector.shape == (4, 6) + for i, slot in enumerate(feature_handler.action_slots): + if slot in ["GENRE", "DIRECTOR"]: + assert torch.equal( + label_vector[i], torch.tensor([0, 0, 0, 1, 0, 0]) + ) + else: + assert torch.equal( + label_vector[i], torch.tensor([1, 0, 0, 0, 0, 0]) + ) diff --git a/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py b/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py index 496318b6..1c7d6765 100644 --- a/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py +++ b/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py @@ -4,8 +4,9 @@ the original implementation. """ -from typing import Dict, List +from typing import Dict, List, Set +import torch from dialoguekit.core.annotated_utterance import AnnotatedUtterance from dialoguekit.core.dialogue_act import DialogueAct @@ -34,18 +35,16 @@ def __init__( self._domain = domain self._user_actions = user_actions self._agent_actions = agent_actions - self.action_slots = set() + self.action_slots: Set[str] = set() self._create_slot_index() - def _create_slot_index(self) -> Dict[str, int]: - """Creates an index for slots. + def reset(self) -> None: + """Resets the feature handler.""" + self.action_slots = set() - Returns: - Slot index. - """ - slots = set( - self._domain.get_slot_names() + self._domain.get_requestable_slots() - ) + def _create_slot_index(self) -> None: + """Creates an index for slots.""" + slots = self._domain.get_slot_names() self.slot_index = {slot: index for index, slot in enumerate(slots)} def get_basic_information_feature( @@ -147,14 +146,15 @@ def get_agent_action_feature( """ v_agent_action = {intent: [0] * 3 for intent in self._agent_actions} for agent_dact in agent_dacts: - if agent_dact.intent in self._agent_actions: + intent_label = agent_dact.intent.label + if intent_label in self._agent_actions: if not agent_dact.annotations: - v_agent_action[agent_dact.intent][0] = 1 + v_agent_action[intent_label][0] = 1 for annotation in agent_dact.annotations: if annotation.slot and annotation.value: - v_agent_action[agent_dact.intent][2] = 1 + v_agent_action[intent_label][2] = 1 elif annotation.slot and annotation.value is None: - v_agent_action[agent_dact.intent][1] = 1 + v_agent_action[intent_label][1] = 1 return sum(v_agent_action.values(), []) def get_slot_index_feature(self, slot: str) -> List[int]: @@ -177,8 +177,8 @@ def get_slot_feature_vector( state: DialogueState, information_need: InformationNeed, agent_dacts: List[DialogueAct], - user_action_vector: List[int] = None, - ) -> List[int]: + user_action_vector: torch.Tensor = None, + ) -> torch.Tensor: """Builds the feature vector for a slot. It concatenate the basic information, user action, agent action, and @@ -196,19 +196,23 @@ def get_slot_feature_vector( Returns: Feature vector for the slot. """ - v_user_action = user_action_vector if user_action_vector else [0] * 6 + v_user_action = ( + user_action_vector + if user_action_vector is not None + else torch.tensor([0] * 6) + ) agent_dacts = [] for dact in agent_dacts: for annotation in dact.annotations: if annotation.slot == slot or annotation.slot is None: agent_dacts.append(dact) - return ( + return torch.tensor( self.get_basic_information_feature( slot, information_need, state, previous_state ) + self.get_agent_action_feature(agent_dacts) - + v_user_action + + v_user_action.tolist() + self.get_slot_index_feature(slot) ) @@ -218,8 +222,8 @@ def get_feature_vector( previous_state: DialogueState, state: DialogueState, information_need: InformationNeed, - user_action_vectors: Dict[str, List[int]] = {}, - ) -> List[int]: + user_action_vectors: Dict[str, torch.Tensor] = {}, + ) -> torch.Tensor: """Builds the feature vector for a turn. It comprises the feature vectors for all slots that in the @@ -251,14 +255,106 @@ def get_feature_vector( + list(information_need.constraints.keys()) + list(information_need.requested_slots.keys()) ) - return [ - self.get_slot_feature_vector( - slot, - previous_state, - state, - information_need, - agent_dacts, - user_action_vectors.get(slot, None), + return torch.cat( + [ + self.get_slot_feature_vector( + slot, + previous_state, + state, + information_need, + agent_dacts, + user_action_vectors.get(slot, None), + ) + for slot in self.action_slots + ], + ) + + def get_label_vector( + self, + user_utterance: AnnotatedUtterance, + current_state: DialogueState, + information_need: InformationNeed, + ) -> torch.Tensor: + """Builds the label vector for a turn. + + It comprises a one-hot encoded vector that determines the value of each + slot. + + Args: + user_utterance: User utterance with annotations. + current_state: Current state. + information_need: Information need. + + Returns: + Label vector for the turn. + """ + try: + user_dacts = user_utterance.dialogue_acts + except AttributeError: + user_dacts = [ + DialogueAct(user_utterance.intent, user_utterance.annotations) + ] + + output = [] + for slot in self.action_slots: + o = self._get_label_vector_slot( + user_dacts, slot, current_state, information_need ) - for slot in self.action_slots - ] + output.append(o) + + return torch.tensor(output) + + def _get_label_vector_slot( + self, + user_dacts: List[DialogueAct], + slot: str, + current_state: DialogueState, + information_need: InformationNeed, + ): + """Builds the label vector for a slot. + + It is a 6-dimensional vector, where each dimension corresponds to the + following values: "none", "don't care", "?", "from information need", + "from belief state", and "random". + + Args: + user_dacts: User dialogue acts. + slot: Slot. + current_state: Current state. + information_need: Information need. + + Returns: + Label vector for the slot. + """ + o = [0] * 6 + for dact in user_dacts: + for annotation in dact.annotations: + if annotation.slot == slot: + if annotation.value == "dontcare": + o[1] = 1 + elif annotation.value is None: + # The value is requested by the user + o[2] = 1 + elif ( + annotation.value + == information_need.get_constraint_value(slot) + or annotation.value + == information_need.requested_slots.get(slot) + ): + # The value is taken from the information need + o[3] = 1 + continue + elif annotation.value == current_state.belief_state.get( + slot + ): + # The value was previously mentioned and is + # retrieved from the belief state + o[4] = 1 + else: + # The slot's value is randomly chosen + o[5] = 1 + + if o == [0] * 6: + # The slot is not mentioned in the user utterance + o[0] = 1 + return o From 6901be43d98e11c8ecb6b8db74be0739bb9ef4e4 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Tue, 4 Jun 2024 09:53:02 +0200 Subject: [PATCH 17/24] Reorganize modules --- tests/conftest.py | 4 +- tests/core/test_information_need.py | 1 - .../test_dialogue_state_tracker.py | 4 +- tests/items/test_item_collection.py | 1 - .../simulator/tus/test_tus_feature_handler.py | 77 +++--------- .../tus/tus_feature_handler.py | 64 +++++----- .../simulator/neural_based/core/__init__.py | 0 .../neural_based/core/feature_handler.py | 23 ---- .../neural_based/core/transformer.py | 118 ------------------ 9 files changed, 48 insertions(+), 244 deletions(-) rename usersimcrs/simulator/{neural_based => neural}/tus/tus_feature_handler.py (86%) delete mode 100644 usersimcrs/simulator/neural_based/core/__init__.py delete mode 100644 usersimcrs/simulator/neural_based/core/feature_handler.py delete mode 100644 usersimcrs/simulator/neural_based/core/transformer.py diff --git a/tests/conftest.py b/tests/conftest.py index 5c90e5b3..23394342 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,13 +49,13 @@ def item_collection(domain: SimulationDomain): @pytest.fixture(scope="module") def information_need() -> InformationNeed: """Information need fixture.""" - constraints = {"GENRE": "comedy", "DIRECTOR": "Steven Spielberg"} + constraints = {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"} requests = ["PLOT", "RATING"] target_items = [ Item( "1", { - "GENRE": "comedy", + "GENRE": "Comedy", "DIRECTOR": "Steven Spielberg", "RATING": 4.5, "PLOT": "A movie plot", diff --git a/tests/core/test_information_need.py b/tests/core/test_information_need.py index c75fd8c0..65d60c7a 100644 --- a/tests/core/test_information_need.py +++ b/tests/core/test_information_need.py @@ -7,7 +7,6 @@ generate_random_information_need, ) from usersimcrs.core.simulation_domain import SimulationDomain -from usersimcrs.items.item import Item from usersimcrs.items.item_collection import ItemCollection diff --git a/tests/dialogue_management/test_dialogue_state_tracker.py b/tests/dialogue_management/test_dialogue_state_tracker.py index 9788b3cb..8a0c709c 100644 --- a/tests/dialogue_management/test_dialogue_state_tracker.py +++ b/tests/dialogue_management/test_dialogue_state_tracker.py @@ -56,9 +56,7 @@ def test_update_state_user( DialogueAct(Intent("request"), annotations=[Annotation("YEAR")]), ] - dialogue_state_tracker.update_state( - dialogue_acts, DialogueParticipant.USER - ) + dialogue_state_tracker.update_state(dialogue_acts, DialogueParticipant.USER) current_state = dialogue_state_tracker.get_current_state() assert current_state.utterance_count == 2 diff --git a/tests/items/test_item_collection.py b/tests/items/test_item_collection.py index 3f935c41..b4829905 100644 --- a/tests/items/test_item_collection.py +++ b/tests/items/test_item_collection.py @@ -5,7 +5,6 @@ import pytest from dialoguekit.core.annotation import Annotation -from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection diff --git a/tests/simulator/tus/test_tus_feature_handler.py b/tests/simulator/tus/test_tus_feature_handler.py index 74db4215..fcb43c55 100644 --- a/tests/simulator/tus/test_tus_feature_handler.py +++ b/tests/simulator/tus/test_tus_feature_handler.py @@ -7,18 +7,17 @@ from dialoguekit.core.annotated_utterance import AnnotatedUtterance from dialoguekit.core.annotation import Annotation from dialoguekit.core.dialogue_act import DialogueAct -from dialoguekit.core.intent import Intent from dialoguekit.participant import DialogueParticipant from usersimcrs.core.information_need import InformationNeed from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.dialogue_management.dialogue_state import DialogueState -from usersimcrs.simulator.neural_based.tus.tus_feature_handler import ( +from usersimcrs.simulator.neural.tus.tus_feature_handler import ( TUSFeatureHandler, ) -@pytest.fixture(scope="function") +@pytest.fixture def feature_handler() -> TUSFeatureHandler: """Returns the feature handler.""" _feature_handler = TUSFeatureHandler( @@ -66,10 +65,9 @@ def test__create_slot_index(feature_handler: TUSFeatureHandler) -> None: "DIRECTOR", DialogueState(), DialogueState( - user_dacts=[ + user_dialogue_acts=[ DialogueAct( - Intent("inform"), - [Annotation("DIRECTOR", "Steven Spielberg")], + "inform", [Annotation("DIRECTOR", "Steven Spielberg")] ) ], belief_state={"DIRECTOR": "Steven Spielberg"}, @@ -80,8 +78,8 @@ def test__create_slot_index(feature_handler: TUSFeatureHandler) -> None: "KEYWORD", DialogueState(), DialogueState( - agent_dacts=[ - DialogueAct(Intent("elicit"), [Annotation("KEYWORD")]) + agent_dialogue_acts=[ + DialogueAct("elicit", [Annotation("KEYWORD")]) ], belief_state={"KEYWORD": None}, ), @@ -111,15 +109,15 @@ def test_get_basic_information_feature( [ ([], [0, 0, 0, 0, 0, 0, 0, 0, 0]), ( - [DialogueAct(Intent("elicit"), [Annotation("GENRE")])], + [DialogueAct("elicit", [Annotation("GENRE")])], [0, 1, 0, 0, 0, 0, 0, 0, 0], ), ( [ DialogueAct( - Intent("recommend"), [Annotation("TITLE", "The Godfather")] + "recommend", [Annotation("TITLE", "The Godfather")] ), - DialogueAct(Intent("bye")), + DialogueAct("bye"), ], [0, 0, 0, 0, 0, 1, 1, 0, 0], ), @@ -163,16 +161,15 @@ def test_get_slot_feature_vector( "DIRECTOR", DialogueState(), DialogueState( - user_dacts=[ + user_dialogue_acts=[ DialogueAct( - Intent("inform"), - [Annotation("DIRECTOR", "Steven Spielberg")], + "inform", [Annotation("DIRECTOR", "Steven Spielberg")] ) ], belief_state={"DIRECTOR": "Steven Spielberg"}, ), information_need, - [DialogueAct(Intent("elicit"), [Annotation("GENRE")])], + [DialogueAct("elicit", [Annotation("GENRE")])], user_action_vector, ) user_action_vector = ( @@ -190,7 +187,7 @@ def test_get_slot_feature_vector( AnnotatedUtterance( "What genre are you interested in?", participant=DialogueParticipant.AGENT, - intent=Intent("elicit"), + intent="elicit", annotations=[Annotation("GENRE")], ), 4, @@ -199,7 +196,7 @@ def test_get_slot_feature_vector( AnnotatedUtterance( "Who should be the main actor?", participant=DialogueParticipant.AGENT, - intent=Intent("elicit"), + intent="elicit", annotations=[Annotation("ACTOR")], ), 5, @@ -221,49 +218,3 @@ def test_get_feature_vector( {"GENRE": torch.tensor([0, 1, 0, 0, 0, 0])}, ) assert len(turn_vector) == expected_num_action_slots * 35 - - -def test_get_label_vector( - feature_handler: TUSFeatureHandler, information_need: InformationNeed -) -> None: - """Tests generation of label vector for slot.""" - feature_handler.action_slots = set(["GENRE", "DIRECTOR"]) - feature_handler.action_slots.update(information_need.constraints.keys()) - feature_handler.action_slots.update(information_need.requested_slots.keys()) - - user_utterance = AnnotatedUtterance( - "I am looking for a comedy movie directed by Steven Spielberg.", - participant=DialogueParticipant.USER, - intent=Intent("inform"), - annotations=[ - Annotation("GENRE", "comedy"), - Annotation("DIRECTOR", "Steven Spielberg"), - ], - ) - state = DialogueState( - turn_count=1, - user_dacts=[ - DialogueAct( - Intent("inform"), - [ - Annotation("GENRE", "comedy"), - Annotation("DIRECTOR", "Steven Spielberg"), - ], - ) - ], - agent_dacts=[DialogueAct(Intent("greets"))], - belief_state={"GENRE": "comedy", "DIRECTOR": "Steven Spielberg"}, - ) - label_vector = feature_handler.get_label_vector( - user_utterance, state, information_need - ) - assert label_vector.shape == (4, 6) - for i, slot in enumerate(feature_handler.action_slots): - if slot in ["GENRE", "DIRECTOR"]: - assert torch.equal( - label_vector[i], torch.tensor([0, 0, 0, 1, 0, 0]) - ) - else: - assert torch.equal( - label_vector[i], torch.tensor([1, 0, 0, 0, 0, 0]) - ) diff --git a/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py b/usersimcrs/simulator/neural/tus/tus_feature_handler.py similarity index 86% rename from usersimcrs/simulator/neural_based/tus/tus_feature_handler.py rename to usersimcrs/simulator/neural/tus/tus_feature_handler.py index 1c7d6765..b65a7339 100644 --- a/usersimcrs/simulator/neural_based/tus/tus_feature_handler.py +++ b/usersimcrs/simulator/neural/tus/tus_feature_handler.py @@ -13,9 +13,7 @@ from usersimcrs.core.information_need import InformationNeed from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.dialogue_management.dialogue_state import DialogueState -from usersimcrs.simulator.neural_based.core.feature_handler import ( - FeatureHandler, -) +from usersimcrs.simulator.neural.core.feature_handler import FeatureHandler class TUSFeatureHandler(FeatureHandler): @@ -130,7 +128,7 @@ def get_basic_information_feature( return v_user_value + v_agent_value + v_type + v_ful + v_first def get_agent_action_feature( - self, agent_dacts: List[DialogueAct] + self, agent_dialogue_acts: List[DialogueAct] ) -> List[int]: """Builds feature vector for agent action. @@ -139,22 +137,21 @@ def get_agent_action_feature( both slot and value are present. Args: - agent_dacts: Agent dialogue acts. + agent_dialogue_acts: Agent dialogue acts. Returns: Feature vector for agent action. """ v_agent_action = {intent: [0] * 3 for intent in self._agent_actions} - for agent_dact in agent_dacts: - intent_label = agent_dact.intent.label - if intent_label in self._agent_actions: - if not agent_dact.annotations: - v_agent_action[intent_label][0] = 1 - for annotation in agent_dact.annotations: + for agent_dialogue_act in agent_dialogue_acts: + if agent_dialogue_act.intent in self._agent_actions: + if not agent_dialogue_act.annotations: + v_agent_action[agent_dialogue_act.intent][0] = 1 + for annotation in agent_dialogue_act.annotations: if annotation.slot and annotation.value: - v_agent_action[intent_label][2] = 1 + v_agent_action[agent_dialogue_act.intent][2] = 1 elif annotation.slot and annotation.value is None: - v_agent_action[intent_label][1] = 1 + v_agent_action[agent_dialogue_act.intent][1] = 1 return sum(v_agent_action.values(), []) def get_slot_index_feature(self, slot: str) -> List[int]: @@ -176,7 +173,7 @@ def get_slot_feature_vector( previous_state: DialogueState, state: DialogueState, information_need: InformationNeed, - agent_dacts: List[DialogueAct], + agent_dialogue_acts: List[DialogueAct], user_action_vector: torch.Tensor = None, ) -> torch.Tensor: """Builds the feature vector for a slot. @@ -189,7 +186,7 @@ def get_slot_feature_vector( previous_state: Previous state. state: Current state. information_need: Information need. - agent_dact: Agent dialogue acts. + agent_dialogue_acts: Agent dialogue acts. user_action_vector: User action feature vector (output vector for previous turn). Defaults to None. @@ -201,17 +198,17 @@ def get_slot_feature_vector( if user_action_vector is not None else torch.tensor([0] * 6) ) - agent_dacts = [] - for dact in agent_dacts: - for annotation in dact.annotations: + agent_dialogue_acts = [] + for dialogue_act in agent_dialogue_acts: + for annotation in dialogue_act.annotations: if annotation.slot == slot or annotation.slot is None: - agent_dacts.append(dact) + agent_dialogue_acts.append(dialogue_act) return torch.tensor( self.get_basic_information_feature( slot, information_need, state, previous_state ) - + self.get_agent_action_feature(agent_dacts) + + self.get_agent_action_feature(agent_dialogue_acts) + v_user_action.tolist() + self.get_slot_index_feature(slot) ) @@ -242,15 +239,17 @@ def get_feature_vector( Feature vector for the turn. """ try: - agent_dacts = utterance.dialogue_acts + agent_dialogue_acts = utterance.dialogue_acts except AttributeError: - agent_dacts = [DialogueAct(utterance.intent, utterance.annotations)] + agent_dialogue_acts = [ + DialogueAct(utterance.intent, utterance.annotations) + ] self.action_slots.update( [ annotation.slot - for dact in agent_dacts - for annotation in dact.annotations + for dialogue_act in agent_dialogue_acts + for annotation in dialogue_act.annotations ] + list(information_need.constraints.keys()) + list(information_need.requested_slots.keys()) @@ -262,7 +261,7 @@ def get_feature_vector( previous_state, state, information_need, - agent_dacts, + agent_dialogue_acts, user_action_vectors.get(slot, None), ) for slot in self.action_slots @@ -289,16 +288,16 @@ def get_label_vector( Label vector for the turn. """ try: - user_dacts = user_utterance.dialogue_acts + user_dialogue_acts = user_utterance.dialogue_acts except AttributeError: - user_dacts = [ + user_dialogue_acts = [ DialogueAct(user_utterance.intent, user_utterance.annotations) ] output = [] for slot in self.action_slots: o = self._get_label_vector_slot( - user_dacts, slot, current_state, information_need + user_dialogue_acts, slot, current_state, information_need ) output.append(o) @@ -306,7 +305,7 @@ def get_label_vector( def _get_label_vector_slot( self, - user_dacts: List[DialogueAct], + user_dialogue_acts: List[DialogueAct], slot: str, current_state: DialogueState, information_need: InformationNeed, @@ -318,7 +317,7 @@ def _get_label_vector_slot( "from belief state", and "random". Args: - user_dacts: User dialogue acts. + user_dialogue_acts: User dialogue acts. slot: Slot. current_state: Current state. information_need: Information need. @@ -327,8 +326,8 @@ def _get_label_vector_slot( Label vector for the slot. """ o = [0] * 6 - for dact in user_dacts: - for annotation in dact.annotations: + for dialogue_act in user_dialogue_acts: + for annotation in dialogue_act.annotations: if annotation.slot == slot: if annotation.value == "dontcare": o[1] = 1 @@ -343,7 +342,6 @@ def _get_label_vector_slot( ): # The value is taken from the information need o[3] = 1 - continue elif annotation.value == current_state.belief_state.get( slot ): diff --git a/usersimcrs/simulator/neural_based/core/__init__.py b/usersimcrs/simulator/neural_based/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/usersimcrs/simulator/neural_based/core/feature_handler.py b/usersimcrs/simulator/neural_based/core/feature_handler.py deleted file mode 100644 index 1095986f..00000000 --- a/usersimcrs/simulator/neural_based/core/feature_handler.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Interface to build feature vector for neural-based user simulator.""" - -from abc import ABC, abstractmethod - -import torch -from dialoguekit.core.annotated_utterance import AnnotatedUtterance - - -class FeatureHandler(ABC): - @abstractmethod - def get_feature_vector( - self, utterance: AnnotatedUtterance, **kwargs - ) -> torch.Tensor: - """Builds a feature vector for a given utterance. - - Args: - utterance: Annotated utterance. - kwargs: Additional arguments. - - Raises: - NotImplementedError: If not implemented in derived class. - """ - raise NotImplementedError diff --git a/usersimcrs/simulator/neural_based/core/transformer.py b/usersimcrs/simulator/neural_based/core/transformer.py deleted file mode 100644 index 6058c92c..00000000 --- a/usersimcrs/simulator/neural_based/core/transformer.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Encoder-only transformer model for neural-based simulator.""" - -import math - -import torch -import torch.nn as nn - - -class PositionalEncoding(nn.Module): - def __init__( - self, - d_model: int, - dropout: float = 0.1, - max_len: int = 5000, - **kwargs, - ) -> None: - """Initializes positional encoding layer. - - Args: - d_model: Dimension of the model. - dropout: Dropout rate. Defaults to 0.1. - max_len: Maximum length of the input sequence. Defaults to 5000. - """ - super(PositionalEncoding, self).__init__() - self.dropout = nn.Dropout(p=dropout) - - position = torch.arange(max_len).unsqueeze(1) - div_term = torch.exp( - torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model) - ) - pe = torch.zeros(max_len, 1, d_model) - pe[:, 0, 0::2] = torch.sin(position * div_term) - pe[:, 0, 1::2] = torch.cos(position * div_term) - self.register_buffer("pe", pe) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - """Performs forward pass. - - Args: - x: Input tensor. - - Returns: - Positional encoded tensor. - """ - x = x + self.pe[: x.size(0)] - return self.dropout(x) - - -class TransformerEncoderModel(nn.Module): - def __init__( - self, - input_dim: int, - output_dim: int, - nhead: int, - hidden_dim: int, - num_encoder_layers: int, - num_token: int, - dropout: float = 0.5, - ) -> None: - """Initializes a encoder-only transformer model. - - Args: - input_dim: Size of the input vector. - output_dim: Size of the output vector. - nhead: Number of heads. - hidden_dim: Hidden dimension. - num_encoder_layers: Number of encoder layers. - num_token: Number of tokens in the vocabulary. - dropout: Dropout rate. Defaults to 0.5. - """ - super(TransformerEncoderModel, self).__init__() - self.d_model = input_dim - - self.pos_encoder = PositionalEncoding(input_dim, dropout) - self.embedding = nn.Embedding(num_token, input_dim) - - # Encoder layers - norm_layer = nn.LayerNorm(input_dim) - encoder_layer = nn.TransformerEncoderLayer( - d_model=input_dim, - nhead=nhead, - dim_feedforward=hidden_dim, - ) - self.encoder = nn.TransformerEncoder( - encoder_layer, - num_layers=num_encoder_layers, - norm=norm_layer, - ) - - self.linear = nn.Linear(input_dim, output_dim) - self.softmax = nn.Softmax(dim=-1) - - self.init_weights() - - def init_weights(self) -> None: - """Initializes weights of the network.""" - initrange = 0.1 - self.embedding.weight.data.uniform_(-initrange, initrange) - self.linear.bias.data.zero_() - self.linear.weight.data.uniform_(-initrange, initrange) - - def forward( - self, src: torch.Tensor, src_mask: torch.Tensor = None - ) -> torch.Tensor: - """Performs forward pass. - - Args: - src: Source tensor. - src_mask: Mask tensor. - - Returns: - Output tensor. - """ - src = self.embedding(src) * math.sqrt(self.d_model) - src = self.pos_encoder(src) - output = self.encoder(src, mask=src_mask) - output = self.linear(output) - return output From 381fd32ce2cdc5506f97777c1bbda540ef3ba286 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:00:30 +0200 Subject: [PATCH 18/24] Update feature handler --- tests/conftest.py | 54 ++- .../simulator/tus/test_tus_feature_handler.py | 87 ++--- .../simulator/neural/core/feature_handler.py | 13 +- .../neural/tus/tus_feature_handler.py | 368 +++++++++++++----- 4 files changed, 348 insertions(+), 174 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 23394342..12bf8171 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,9 @@ from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item import Item from usersimcrs.items.item_collection import ItemCollection +from usersimcrs.simulator.neural.tus.tus_feature_handler import ( + TUSFeatureHandler, +) DOMAIN_YAML_FILE = "tests/data/domains/movies.yaml" ITEMS_CSV_FILE = "tests/data/items/movies_w_keywords.csv" @@ -19,6 +22,25 @@ def domain() -> SimulationDomain: return SimulationDomain(DOMAIN_YAML_FILE) +@pytest.fixture(scope="module") +def information_need() -> InformationNeed: + """Information need fixture.""" + constraints = {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"} + requests = ["PLOT", "RATING"] + target_items = [ + Item( + "1", + { + "GENRE": "Comedy", + "DIRECTOR": "Steven Spielberg", + "RATING": 4.5, + "PLOT": "A movie plot", + }, + ) + ] + return InformationNeed(target_items, constraints, requests) + + @pytest.fixture(scope="session") def item_collection(domain: SimulationDomain): """Item collection fixture.""" @@ -47,19 +69,21 @@ def item_collection(domain: SimulationDomain): @pytest.fixture(scope="module") -def information_need() -> InformationNeed: - """Information need fixture.""" - constraints = {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"} - requests = ["PLOT", "RATING"] - target_items = [ - Item( - "1", - { - "GENRE": "Comedy", - "DIRECTOR": "Steven Spielberg", - "RATING": 4.5, - "PLOT": "A movie plot", - }, - ) +def feature_handler(domain: SimulationDomain) -> TUSFeatureHandler: + """Returns the feature handler.""" + _feature_handler = TUSFeatureHandler( + domain=domain, + max_turn_feature_length=40, + context_depth=2, + user_actions=["inform", "request"], + agent_actions=["elicit", "recommend", "bye"], + ) + + assert _feature_handler._user_actions == ["inform", "request"] + assert _feature_handler._agent_actions == [ + "elicit", + "recommend", + "bye", ] - return InformationNeed(target_items, constraints, requests) + + return _feature_handler diff --git a/tests/simulator/tus/test_tus_feature_handler.py b/tests/simulator/tus/test_tus_feature_handler.py index fcb43c55..f1ddaeed 100644 --- a/tests/simulator/tus/test_tus_feature_handler.py +++ b/tests/simulator/tus/test_tus_feature_handler.py @@ -7,35 +7,16 @@ from dialoguekit.core.annotated_utterance import AnnotatedUtterance from dialoguekit.core.annotation import Annotation from dialoguekit.core.dialogue_act import DialogueAct +from dialoguekit.core.intent import Intent from dialoguekit.participant import DialogueParticipant from usersimcrs.core.information_need import InformationNeed -from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.dialogue_management.dialogue_state import DialogueState from usersimcrs.simulator.neural.tus.tus_feature_handler import ( TUSFeatureHandler, ) -@pytest.fixture -def feature_handler() -> TUSFeatureHandler: - """Returns the feature handler.""" - _feature_handler = TUSFeatureHandler( - domain=SimulationDomain("tests/data/domains/movies.yaml"), - user_actions=["inform", "request"], - agent_actions=["elicit", "recommend", "bye"], - ) - - assert _feature_handler._user_actions == ["inform", "request"] - assert _feature_handler._agent_actions == [ - "elicit", - "recommend", - "bye", - ] - - return _feature_handler - - def test__create_slot_index(feature_handler: TUSFeatureHandler) -> None: """Tests the creation of the slot index.""" assert all( @@ -67,7 +48,8 @@ def test__create_slot_index(feature_handler: TUSFeatureHandler) -> None: DialogueState( user_dialogue_acts=[ DialogueAct( - "inform", [Annotation("DIRECTOR", "Steven Spielberg")] + Intent("inform"), + [Annotation("DIRECTOR", "Steven Spielberg")], ) ], belief_state={"DIRECTOR": "Steven Spielberg"}, @@ -79,7 +61,7 @@ def test__create_slot_index(feature_handler: TUSFeatureHandler) -> None: DialogueState(), DialogueState( agent_dialogue_acts=[ - DialogueAct("elicit", [Annotation("KEYWORD")]) + DialogueAct(Intent("elicit"), [Annotation("KEYWORD")]) ], belief_state={"KEYWORD": None}, ), @@ -109,15 +91,15 @@ def test_get_basic_information_feature( [ ([], [0, 0, 0, 0, 0, 0, 0, 0, 0]), ( - [DialogueAct("elicit", [Annotation("GENRE")])], + [DialogueAct(Intent("elicit"), [Annotation("GENRE")])], [0, 1, 0, 0, 0, 0, 0, 0, 0], ), ( [ DialogueAct( - "recommend", [Annotation("TITLE", "The Godfather")] + Intent("recommend"), [Annotation("TITLE", "The Godfather")] ), - DialogueAct("bye"), + DialogueAct(Intent("bye")), ], [0, 0, 0, 0, 0, 1, 1, 0, 0], ), @@ -163,13 +145,14 @@ def test_get_slot_feature_vector( DialogueState( user_dialogue_acts=[ DialogueAct( - "inform", [Annotation("DIRECTOR", "Steven Spielberg")] + Intent("inform"), + [Annotation("DIRECTOR", "Steven Spielberg")], ) ], belief_state={"DIRECTOR": "Steven Spielberg"}, ), information_need, - [DialogueAct("elicit", [Annotation("GENRE")])], + [DialogueAct(Intent("elicit"), [Annotation("GENRE")])], user_action_vector, ) user_action_vector = ( @@ -177,44 +160,46 @@ def test_get_slot_feature_vector( if user_action_vector is not None else torch.tensor([0] * 6) ) - assert torch.equal(slot_feature_vector[21:27], user_action_vector) + assert torch.equal( + torch.tensor(slot_feature_vector[23:29]), user_action_vector + ) @pytest.mark.parametrize( - "utterance, expected_num_action_slots", + "agent_utterance", [ - ( - AnnotatedUtterance( - "What genre are you interested in?", - participant=DialogueParticipant.AGENT, - intent="elicit", - annotations=[Annotation("GENRE")], - ), - 4, + AnnotatedUtterance( + "What genre are you interested in?", + participant=DialogueParticipant.AGENT, + dialogue_acts=[ + DialogueAct( + Intent("elicit"), annotations=[Annotation("GENRE")] + ) + ], ), - ( - AnnotatedUtterance( - "Who should be the main actor?", - participant=DialogueParticipant.AGENT, - intent="elicit", - annotations=[Annotation("ACTOR")], - ), - 5, + AnnotatedUtterance( + "Who should be the main actor?", + participant=DialogueParticipant.AGENT, + dialogue_acts=[ + DialogueAct( + Intent("elicit"), annotations=[Annotation("ACTOR")] + ) + ], ), ], ) -def test_get_feature_vector( - utterance: AnnotatedUtterance, - expected_num_action_slots: int, +def test_build_input_vector( + agent_utterance: AnnotatedUtterance, feature_handler: TUSFeatureHandler, information_need: InformationNeed, ) -> None: """Tests the turn feature vector.""" - turn_vector = feature_handler.get_feature_vector( - utterance, + turn_vector, mask = feature_handler.build_input_vector( + agent_utterance.dialogue_acts, DialogueState(), DialogueState(), information_need, {"GENRE": torch.tensor([0, 1, 0, 0, 0, 0])}, ) - assert len(turn_vector) == expected_num_action_slots * 35 + assert len(turn_vector) == len(mask) == 40 * 2 + assert torch.equal(torch.tensor(mask), torch.tensor([False] * 80)) diff --git a/usersimcrs/simulator/neural/core/feature_handler.py b/usersimcrs/simulator/neural/core/feature_handler.py index 1095986f..0342d48c 100644 --- a/usersimcrs/simulator/neural/core/feature_handler.py +++ b/usersimcrs/simulator/neural/core/feature_handler.py @@ -1,17 +1,21 @@ """Interface to build feature vector for neural-based user simulator.""" from abc import ABC, abstractmethod +from typing import List, Tuple, Union import torch from dialoguekit.core.annotated_utterance import AnnotatedUtterance +FeatureVector = Union[torch.Tensor, List[int]] +FeatureMask = Union[torch.Tensor, List[bool]] + class FeatureHandler(ABC): @abstractmethod - def get_feature_vector( + def build_input_vector( self, utterance: AnnotatedUtterance, **kwargs - ) -> torch.Tensor: - """Builds a feature vector for a given utterance. + ) -> Tuple[FeatureVector, FeatureMask]: + """Builds the input vector for a given utterance. Args: utterance: Annotated utterance. @@ -19,5 +23,8 @@ def get_feature_vector( Raises: NotImplementedError: If not implemented in derived class. + + Returns: + Input vector and mask. """ raise NotImplementedError diff --git a/usersimcrs/simulator/neural/tus/tus_feature_handler.py b/usersimcrs/simulator/neural/tus/tus_feature_handler.py index b65a7339..8af32712 100644 --- a/usersimcrs/simulator/neural/tus/tus_feature_handler.py +++ b/usersimcrs/simulator/neural/tus/tus_feature_handler.py @@ -4,22 +4,34 @@ the original implementation. """ -from typing import Dict, List, Set +from __future__ import annotations +import logging +import os +from typing import Dict, Iterable, List, Tuple + +import joblib import torch from dialoguekit.core.annotated_utterance import AnnotatedUtterance +from dialoguekit.core.annotation import Annotation from dialoguekit.core.dialogue_act import DialogueAct from usersimcrs.core.information_need import InformationNeed from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.dialogue_management.dialogue_state import DialogueState -from usersimcrs.simulator.neural.core.feature_handler import FeatureHandler +from usersimcrs.simulator.neural.core.feature_handler import ( + FeatureHandler, + FeatureMask, + FeatureVector, +) class TUSFeatureHandler(FeatureHandler): def __init__( self, domain: SimulationDomain, + max_turn_feature_length: int, + context_depth: int, agent_actions: List[str], user_actions: List[str] = ["inform", "request"], ) -> None: @@ -27,18 +39,43 @@ def __init__( Args: domain: Domain knowledge. + max_turn_feature_length: Maximum length of a turn feature vector. + context_depth: Number of previous turns to include in the input + vector. agent_actions: Agent actions. user_actions: User actions. Defaults to ["inform", "request"]. """ self._domain = domain + self.max_turn_feature_length = max_turn_feature_length + self.context_depth = context_depth self._user_actions = user_actions self._agent_actions = agent_actions - self.action_slots: Set[str] = set() + self.action_slots: List[str] = list() self._create_slot_index() + # Store user feature vectors for each turn + self.user_feature_history: List[List[FeatureVector]] = list() def reset(self) -> None: """Resets the feature handler.""" - self.action_slots = set() + self.reset_user_feature_history() + self.action_slots = list() + + def reset_user_feature_history(self) -> None: + """Resets the user feature history.""" + self.user_feature_history = list() + + def update_action_slots(self, slots: Iterable[str]) -> None: + """Updates the action slots. + + Action slots are slots present in the information need and mentioned + during the conversation. + + Args: + slots: Slots mentioned in an utterance. + """ + for slot in slots: + if slot not in self.action_slots: + self.action_slots.append(slot) def _create_slot_index(self) -> None: """Creates an index for slots.""" @@ -51,7 +88,7 @@ def get_basic_information_feature( information_need: InformationNeed, state: DialogueState, previous_state: DialogueState, - ) -> List[int]: + ) -> FeatureVector: """Builds feature vector for basic information. It concatenates the value in agent state, user state, slot type, @@ -129,7 +166,7 @@ def get_basic_information_feature( def get_agent_action_feature( self, agent_dialogue_acts: List[DialogueAct] - ) -> List[int]: + ) -> FeatureVector: """Builds feature vector for agent action. It concatenates action vectors represented as 3-dimensional vectors that @@ -143,18 +180,19 @@ def get_agent_action_feature( Feature vector for agent action. """ v_agent_action = {intent: [0] * 3 for intent in self._agent_actions} - for agent_dialogue_act in agent_dialogue_acts: - if agent_dialogue_act.intent in self._agent_actions: - if not agent_dialogue_act.annotations: - v_agent_action[agent_dialogue_act.intent][0] = 1 - for annotation in agent_dialogue_act.annotations: + for dialogue_act in agent_dialogue_acts: + intent_label = dialogue_act.intent.label + if intent_label in self._agent_actions: + if not dialogue_act.annotations: + v_agent_action[intent_label][0] = 1 + for annotation in dialogue_act.annotations: if annotation.slot and annotation.value: - v_agent_action[agent_dialogue_act.intent][2] = 1 + v_agent_action[intent_label][2] = 1 elif annotation.slot and annotation.value is None: - v_agent_action[agent_dialogue_act.intent][1] = 1 + v_agent_action[intent_label][1] = 1 return sum(v_agent_action.values(), []) - def get_slot_index_feature(self, slot: str) -> List[int]: + def get_slot_index_feature(self, slot: str) -> FeatureVector: """Builds feature vector for slot index. Args: @@ -175,7 +213,7 @@ def get_slot_feature_vector( information_need: InformationNeed, agent_dialogue_acts: List[DialogueAct], user_action_vector: torch.Tensor = None, - ) -> torch.Tensor: + ) -> FeatureVector: """Builds the feature vector for a slot. It concatenate the basic information, user action, agent action, and @@ -198,37 +236,38 @@ def get_slot_feature_vector( if user_action_vector is not None else torch.tensor([0] * 6) ) - agent_dialogue_acts = [] + _agent_dialogue_acts = [] + # Filter agent dialogue acts by slot for dialogue_act in agent_dialogue_acts: for annotation in dialogue_act.annotations: if annotation.slot == slot or annotation.slot is None: - agent_dialogue_acts.append(dialogue_act) + _agent_dialogue_acts.append(dialogue_act) - return torch.tensor( - self.get_basic_information_feature( + return ( + [0, 0] # No special token + + self.get_basic_information_feature( slot, information_need, state, previous_state ) - + self.get_agent_action_feature(agent_dialogue_acts) + + self.get_agent_action_feature(_agent_dialogue_acts) + v_user_action.tolist() + self.get_slot_index_feature(slot) ) def get_feature_vector( self, - utterance: AnnotatedUtterance, + agent_dialogue_acts: List[DialogueAct], previous_state: DialogueState, state: DialogueState, information_need: InformationNeed, user_action_vectors: Dict[str, torch.Tensor] = {}, - ) -> torch.Tensor: + ) -> List[FeatureVector]: """Builds the feature vector for a turn. It comprises the feature vectors for all slots that in the information need and mentioned during the conversation. Args: - utterance: Agent utterance with annotations. - slots: Slots. + agent_dialogue_acts: Agent dialogue acts. previous_state: Previous state. state: Current state. information_need: Information need. @@ -238,47 +277,126 @@ def get_feature_vector( Returns: Feature vector for the turn. """ - try: - agent_dialogue_acts = utterance.dialogue_acts - except AttributeError: - agent_dialogue_acts = [ - DialogueAct(utterance.intent, utterance.annotations) - ] - self.action_slots.update( + # Update the action slots with constraints, requested, and mentioned + # slots + self.update_action_slots(information_need.constraints.keys()) + self.update_action_slots(information_need.requested_slots.keys()) + self.update_action_slots( [ annotation.slot for dialogue_act in agent_dialogue_acts for annotation in dialogue_act.annotations ] - + list(information_need.constraints.keys()) - + list(information_need.requested_slots.keys()) ) - return torch.cat( - [ - self.get_slot_feature_vector( - slot, - previous_state, - state, - information_need, - agent_dialogue_acts, - user_action_vectors.get(slot, None), - ) - for slot in self.action_slots - ], + return [ + self.get_slot_feature_vector( + slot, + previous_state, + state, + information_need, + agent_dialogue_acts, + user_action_vectors.get(slot, None), + ) + for slot in self.action_slots + ] + + def _get_special_token_feature_vector(self, token: str) -> FeatureVector: + """Builds the feature vector for a special token. + + Args: + token: Special token, either "[CLS]" or "[SEP]". + + Raises: + ValueError: If the token is not supported. + + Returns: + Feature vector for the special token. + """ + if token not in ["[CLS]", "[SEP]"]: + raise ValueError( + f"Unsupported special token: {token}. Supported tokens: [CLS], " + "[SEP]" + ) + + vector = [1, 0] if token == "[CLS]" else [0, 1] + vector += [0] * 12 # Dimension of basic information feature + vector += [0] * len(self.slot_index) # Dimension of slot index feature + vector += ( + [0] * 3 * len(self._agent_actions) + ) # Dimension of agent action feature + vector += [0] * 6 # Dimension of user action feature + return vector + + def build_input_vector( + self, + agent_dialogue_acts: List[DialogueAct], + previous_state: DialogueState, + state: DialogueState, + information_need: InformationNeed, + user_action_vectors: Dict[str, torch.Tensor] = {}, + ) -> Tuple[List[FeatureVector], FeatureMask]: + """Builds the input vector $V_{input}$ for a turn. + + It concatenates the feature vectors for the last n turns separated by + a special token. Note that is inferred from the list of feature vectors + provided. + The input vector is structured as follows: + [CLS] $V^t$ [SEP] $V^{t-1}$ [SEP] ... [SEP] $V^{t-n}$ [SEP] [PAD] + + Args: + agent_dialogue_acts: Agent dialogue acts. + previous_state: Previous state. + state: Current state. + information_need: Information need. + user_action_vectors: User action feature vectors per slot. Defaults + to an empty dictionary. + + Returns: + Input vector. + """ + current_turn_feature_vector = self.get_feature_vector( + agent_dialogue_acts, + previous_state, + state, + information_need, + user_action_vectors, ) + self.user_feature_history.append(current_turn_feature_vector) + + v_cls = self._get_special_token_feature_vector("[CLS]") + v_sep = self._get_special_token_feature_vector("[SEP]") + input_vector: List[FeatureVector] = [v_cls] + feature_dimension = len(v_cls) + for turn_feature_vector in reversed( + self.user_feature_history[-self.context_depth :] # noqa: E203 + ): + input_vector.extend(turn_feature_vector) + input_vector.append(v_sep) + # input_vector: torch.Tensor = torch.cat(input_vector) + + # Pad the input vector and create mask + max_length = self.max_turn_feature_length * self.context_depth + if len(input_vector) < max_length: + padding = [[0] * feature_dimension] * ( + max_length - len(input_vector) + ) + input_vector += padding + mask = [False] * len(input_vector) + [True] * ( + max_length - len(input_vector) + ) + else: + mask = [False] * max_length + return input_vector[:max_length], mask[:max_length] def get_label_vector( self, user_utterance: AnnotatedUtterance, current_state: DialogueState, information_need: InformationNeed, - ) -> torch.Tensor: + ) -> FeatureVector: """Builds the label vector for a turn. - It comprises a one-hot encoded vector that determines the value of each - slot. - Args: user_utterance: User utterance with annotations. current_state: Current state. @@ -287,72 +405,112 @@ def get_label_vector( Returns: Label vector for the turn. """ - try: - user_dialogue_acts = user_utterance.dialogue_acts - except AttributeError: - user_dialogue_acts = [ - DialogueAct(user_utterance.intent, user_utterance.annotations) - ] + user_dialogue_acts = user_utterance.dialogue_acts + output = [-1] * self.max_turn_feature_length + for dialogue_act in user_dialogue_acts: + for annotation in dialogue_act.annotations: + if annotation.slot not in self.action_slots: + continue + slot_index = self.action_slots.index(annotation.slot) + if slot_index >= self.max_turn_feature_length: + continue + label = self._get_label( + annotation, current_state, information_need + ) + output[slot_index] = label - output = [] - for slot in self.action_slots: - o = self._get_label_vector_slot( - user_dialogue_acts, slot, current_state, information_need - ) - output.append(o) + for i in range(len(self.action_slots)): + if i < self.max_turn_feature_length and output[i] == -1: + # The slot is not mentioned in the user utterance + output[i] = 0 - return torch.tensor(output) + return output - def _get_label_vector_slot( + def _get_label( self, - user_dialogue_acts: List[DialogueAct], - slot: str, + annotation: Annotation, current_state: DialogueState, information_need: InformationNeed, - ): - """Builds the label vector for a slot. - - It is a 6-dimensional vector, where each dimension corresponds to the - following values: "none", "don't care", "?", "from information need", - "from belief state", and "random". + ) -> int: + """Gets the label for a slot. + + The label is a number in [0,5] which represents the following values: + 0: The slot's value is not mentioned in the user utterance. + 1: The slot's value is set to "dontcare". + 2: The slot's value is requested by the user. + 3: The slot's value is taken from the information need. + 4: The slot's value was previously mentioned and is retrieved from the + belief state. + 5: The slot's value is randomly chosen. Args: - user_dialogue_acts: User dialogue acts. - slot: Slot. + annotation: Annotation. current_state: Current state. information_need: Information need. Returns: - Label vector for the slot. + Label. """ - o = [0] * 6 - for dialogue_act in user_dialogue_acts: - for annotation in dialogue_act.annotations: - if annotation.slot == slot: - if annotation.value == "dontcare": - o[1] = 1 - elif annotation.value is None: - # The value is requested by the user - o[2] = 1 - elif ( - annotation.value - == information_need.get_constraint_value(slot) - or annotation.value - == information_need.requested_slots.get(slot) - ): - # The value is taken from the information need - o[3] = 1 - elif annotation.value == current_state.belief_state.get( - slot - ): - # The value was previously mentioned and is - # retrieved from the belief state - o[4] = 1 - else: - # The slot's value is randomly chosen - o[5] = 1 - - if o == [0] * 6: - # The slot is not mentioned in the user utterance - o[0] = 1 - return o + if annotation.value == "dontcare": + return 1 + elif annotation.value is None: + # The value is requested by the user + return 2 + elif annotation.value == information_need.get_constraint_value( + annotation.slot + ) or annotation.value == information_need.requested_slots.get( + annotation.slot + ): + # The value is taken from the information need + return 3 + elif annotation.value == current_state.belief_state.get( + annotation.slot + ): + # The value was previously mentioned and is + # retrieved from the belief state + return 4 + elif ( + annotation.slot + in [ + information_need.constraints.keys(), + information_need.requested_slots.keys(), + current_state.belief_state.keys(), + ] + ) and annotation.value not in [ + information_need.get_constraint_value(annotation.slot), + information_need.requested_slots.get(annotation.slot), + current_state.belief_state.get(annotation.slot), + ]: + # The slot's value is randomly chosen + return 5 + return 0 + + def save_handler(self, path: str) -> None: + """Saves the feature handler. + + Args: + path: Path to the output file. + """ + if not os.path.exists(os.path.dirname(path)): + logging.info(f"Creating directory: {os.path.dirname(path)}") + os.makedirs(os.path.dirname(path)) + + joblib.dump(self, path) + + @classmethod + def load_handler(cls, path: str) -> TUSFeatureHandler: + """Loads a feature handler from a given path. + + Args: + path: Path to load the feature handler from. + + Raises: + FileNotFoundError: If the file is not found. + + Returns: + Feature handler. + """ + if not os.path.exists(path): + raise FileNotFoundError(f"File '{path}' not found.") + + return joblib.load(path) From 8dcb7250a7d8c1736ebb82ad29e8981a75977866 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:05:15 +0200 Subject: [PATCH 19/24] Add missing requirement --- requirements/requirements.txt | 3 ++- tests/simulator/tus/test_tus_feature_handler.py | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b8371444..66848b11 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -12,4 +12,5 @@ dialoguekit==0.0.9 confuse websockets<11.0 nltk -joblib \ No newline at end of file +joblib +torch \ No newline at end of file diff --git a/tests/simulator/tus/test_tus_feature_handler.py b/tests/simulator/tus/test_tus_feature_handler.py index f1ddaeed..9dba6889 100644 --- a/tests/simulator/tus/test_tus_feature_handler.py +++ b/tests/simulator/tus/test_tus_feature_handler.py @@ -172,18 +172,14 @@ def test_get_slot_feature_vector( "What genre are you interested in?", participant=DialogueParticipant.AGENT, dialogue_acts=[ - DialogueAct( - Intent("elicit"), annotations=[Annotation("GENRE")] - ) + DialogueAct(Intent("elicit"), annotations=[Annotation("GENRE")]) ], ), AnnotatedUtterance( "Who should be the main actor?", participant=DialogueParticipant.AGENT, dialogue_acts=[ - DialogueAct( - Intent("elicit"), annotations=[Annotation("ACTOR")] - ) + DialogueAct(Intent("elicit"), annotations=[Annotation("ACTOR")]) ], ), ], From 554b82684466035816afcb2fd0557c58074902df Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:26:48 +0200 Subject: [PATCH 20/24] Add TUS --- .../agenda_based/agenda_based_simulator.py | 8 +- .../simulator/neural/core/transformer.py | 26 +- usersimcrs/simulator/neural/tus/tus.py | 264 ++++++++++++++++++ usersimcrs/simulator/user_simulator.py | 23 +- 4 files changed, 306 insertions(+), 15 deletions(-) create mode 100644 usersimcrs/simulator/neural/tus/tus.py diff --git a/usersimcrs/simulator/agenda_based/agenda_based_simulator.py b/usersimcrs/simulator/agenda_based/agenda_based_simulator.py index dc57dd4e..665be39f 100644 --- a/usersimcrs/simulator/agenda_based/agenda_based_simulator.py +++ b/usersimcrs/simulator/agenda_based/agenda_based_simulator.py @@ -28,12 +28,12 @@ class AgendaBasedSimulator(UserSimulator): def __init__( self, id: str, + domain: SimulationDomain, + item_collection: ItemCollection, preference_model: PreferenceModel, interaction_model: InteractionModel, nlu: NLU, nlg: ConditionalNLG, - domain: SimulationDomain, - item_collection: ItemCollection, ratings: Ratings, ) -> None: """Initializes the agenda-based simulated user. @@ -47,14 +47,12 @@ def __init__( item_collection: Item collection. ratings: Historical ratings. """ - super().__init__(id=id) + super().__init__(id=id, domain=domain, item_collection=item_collection) self._preference_model = preference_model self._interaction_model = interaction_model self._interaction_model.initialize_agenda() self._nlu = nlu self._nlg = nlg - self._domain = domain - self._item_collection = item_collection self._ratings = ratings def _generate_response(self, agent_utterance: Utterance) -> Utterance: diff --git a/usersimcrs/simulator/neural/core/transformer.py b/usersimcrs/simulator/neural/core/transformer.py index 6727112b..4b91e354 100644 --- a/usersimcrs/simulator/neural/core/transformer.py +++ b/usersimcrs/simulator/neural/core/transformer.py @@ -1,4 +1,11 @@ -"""Encoder-only transformer model for neural user simulator.""" +"""Encoder-only transformer model for neural user simulator. + +Implementation inspired by PyTorch documentation and TUS's transformer model. + +Sources: +https://colab.research.google.com/github/pytorch/tutorials/blob/gh-pages/_downloads/dca13261bbb4e9809d1a3aa521d22dd7/transformer_tutorial.ipynb#scrollTo=R8veciavth40 +https://gitlab.cs.uni-duesseldorf.de/general/dsml/tus_public/-/blob/master/convlab2/policy/tus/multiwoz/transformer.py?ref_type=heads +""" import math @@ -54,7 +61,6 @@ def __init__( nhead: int, hidden_dim: int, num_encoder_layers: int, - num_token: int, dropout: float = 0.5, ) -> None: """Initializes a encoder-only transformer model. @@ -69,15 +75,15 @@ def __init__( dropout: Dropout rate. Defaults to 0.5. """ super(TransformerEncoderModel, self).__init__() - self.d_model = input_dim + self.d_model = hidden_dim - self.pos_encoder = PositionalEncoding(input_dim, dropout) - self.embedding = nn.Embedding(num_token, input_dim) + self.pos_encoder = PositionalEncoding(hidden_dim, dropout) + self.embedding = nn.Linear(input_dim, hidden_dim) # Encoder layers - norm_layer = nn.LayerNorm(input_dim) + norm_layer = nn.LayerNorm(hidden_dim) encoder_layer = nn.TransformerEncoderLayer( - d_model=input_dim, + d_model=self.d_model, nhead=nhead, dim_feedforward=hidden_dim, ) @@ -87,7 +93,7 @@ def __init__( norm=norm_layer, ) - self.linear = nn.Linear(input_dim, output_dim) + self.linear = nn.Linear(hidden_dim, output_dim) self.softmax = nn.Softmax(dim=-1) self.init_weights() @@ -113,6 +119,8 @@ def forward( """ src = self.embedding(src) * math.sqrt(self.d_model) src = self.pos_encoder(src) - output = self.encoder(src, mask=src_mask) + src = src.permute(1, 0, 2) + output = self.encoder(src, src_key_padding_mask=src_mask) output = self.linear(output) + output = output.permute(1, 0, 2) return output diff --git a/usersimcrs/simulator/neural/tus/tus.py b/usersimcrs/simulator/neural/tus/tus.py new file mode 100644 index 00000000..c41f9b97 --- /dev/null +++ b/usersimcrs/simulator/neural/tus/tus.py @@ -0,0 +1,264 @@ +"""Transformer-based User Simulator (TUS) + +Reference: Domain-independent User Simulation with Transformers for +Task-oriented Dialogue Systems, Lin et al., 2021. +See: https://arxiv.org/abs/2106.08838 + +Implementation is adapted from the description in the paper and the original +implementation by the authors: +https://gitlab.cs.uni-duesseldorf.de/general/dsml/tus_public +""" + +import logging +import random +from collections import defaultdict +from typing import Any, DefaultDict, Dict, List + +import torch +from dialoguekit.core.annotated_utterance import AnnotatedUtterance +from dialoguekit.core.annotation import Annotation +from dialoguekit.core.dialogue_act import DialogueAct +from dialoguekit.core.utterance import Utterance +from dialoguekit.nlu.nlu import NLU +from dialoguekit.participant import DialogueParticipant + +from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.dialogue_management.dialogue_state_tracker import ( + DialogueStateTracker, +) +from usersimcrs.items.item_collection import ItemCollection +from usersimcrs.simulator.neural.core.feature_handler import ( + FeatureMask, + FeatureVector, +) +from usersimcrs.simulator.neural.core.transformer import ( + TransformerEncoderModel, +) +from usersimcrs.simulator.neural.tus.tus_feature_handler import ( + TUSFeatureHandler, +) +from usersimcrs.simulator.user_simulator import UserSimulator + +logger = logging.getLogger(__name__) + + +class TUS(UserSimulator): + def __init__( + self, + id: str, + domain: SimulationDomain, + item_collection: ItemCollection, + nlu: NLU, + feature_handler: TUSFeatureHandler, + dialogue_state_tracker: DialogueStateTracker, + network_config: Dict[str, Any], + ) -> None: + """Initializes the Transformer-based User Simulator (TUS). + + Args: + id: Simulator ID. + domain: Domain knowledge. + item_collection: Collection of items. + nlu: NLU module. + feature_handler: Feature handler. + dialogue_state_tracker: Dialogue state tracker. + network_config: Network configuration. + """ + super().__init__(id=id, domain=domain, item_collection=item_collection) + self._nlu = nlu + self._feature_handler = feature_handler + self._user_policy_network = TransformerEncoderModel(**network_config) + self._dialogue_state_tracker = dialogue_state_tracker + self._last_user_actions: DefaultDict[str, torch.Tensor] = defaultdict( + lambda: torch.tensor([]) + ) + self._last_turn_input: torch.Tensor = None + + def initialize(self) -> None: + """Initializes the user simulator.""" + self._dialogue_state_tracker.reset_state() + self._last_user_actions.clear() + self._last_turn_input = None + + def _generate_response(self, agent_utterance: Utterance) -> Utterance: + """Generates response to the agent utterance. + + Args: + agent_utterance: Agent utterance. + + Returns: + User utterance. + """ + previous_state = self._dialogue_state_tracker.get_current_state() + # 1. Perform NLU on the agent utterance, i.e., extract dialogue acts or + # intent and annotations. + annotated_agent_utterance = self._annotate_agent_utterance( + agent_utterance + ) + + # 2. Update dialogue state based on the agent utterance. + self._dialogue_state_tracker.update_state( + dialogue_acts=annotated_agent_utterance.dialogue_acts, + participant=DialogueParticipant.AGENT, + ) + + # 3. Extract features for the current turn. + turn_feature, mask = self._feature_handler.build_input_vector( + agent_dialogue_acts=annotated_agent_utterance.dialogue_acts, + previous_state=previous_state, + state=self._dialogue_state_tracker.get_current_state(), + information_need=self.information_need, + user_action_vectors=self._last_user_actions, + ) + + # 5. Predict user dialogue acts based on the features. + user_dialogue_acts = self.predict_user_dialogue_acts( + turn_feature, mask, self._feature_handler.action_slots + ) + + # 6. Generate user utterance based on the predicted actions. + # For now, we only consider the first predicted dialogue act due to + # constraints related to supported NLG in DialogueKit. + response_intent = user_dialogue_acts[0].intent + response_annotations = user_dialogue_acts[0].annotations + response = self._nlg.generate_utterance_text( + intent=response_intent, annotations=response_annotations + ) + response.participant = DialogueParticipant.USER + + # 7. Update dialogue state based on the user utterance. + self._dialogue_state_tracker.update_state( + dialogue_acts=user_dialogue_acts, + participant=DialogueParticipant.USER, + ) + + return response + + def _annotate_agent_utterance( + self, agent_utterance: Utterance + ) -> AnnotatedUtterance: + """Annotates the agent utterance. + + As of now, DialogueKit does not support NLU that can annotate dialogue + acts. So, only one dialogue act is created with the intent and + annotations extracted from the agent utterance. + + Args: + agent_utterance: Agent utterance. + + Returns: + Annotated utterance. + """ + agent_intent = self._nlu.classify_intent(agent_utterance) + agent_annotations = self._nlu.annotate_slot_values(agent_utterance) + dialogue_acts = [ + DialogueAct(intent=agent_intent, annotations=agent_annotations) + ] + utt = AnnotatedUtterance( + text=agent_utterance.text, + participant=DialogueParticipant.AGENT, + dialogue_acts=dialogue_acts, + ) + return utt + + def predict_user_dialogue_acts( + self, + features: List[FeatureVector], + mask: FeatureMask, + action_slots: List[str], + ) -> List[DialogueAct]: + """Predicts user dialogue acts based on the features. + + Args: + features: Feature vector. + mask: Mask vector. + action_slots: Action slots used to predict the user action per slot. + + Returns: + Predicted user dialogue acts. + """ + output = self._user_policy_network(features, mask) + # fmt: off + output = output[ + :, 1 : self._feature_handler.max_turn_feature_length + 1, : # noqa: E203, E501 + ] + # fmt: on + + slot_outputs: Dict[str, int] = {} + for index, slot_name in enumerate(action_slots): + o = int(torch.argmax(output[0, index + 1, :]).item()) + assert o in range(6), f"Invalid output: {o}" + slot_outputs[slot_name] = o + # One-hot encoding of user action for the slot + o_i = torch.zeros(6) + o_i[o] = 1 + self._last_user_actions[slot_name] = o_i + + user_dialogue_acts = self._parse_policy_output( + action_slots, slot_outputs + ) + return user_dialogue_acts + + def _parse_policy_output( + self, action_slots: List[str], slot_outputs: Dict[str, int] + ) -> List[DialogueAct]: + """Parses the policy output to dialogue acts. + + Args: + action_slots: Action slots. + slot_outputs: Output per slot. + + Returns: + Dialogue acts. + """ + belief_state = ( + self._dialogue_state_tracker.get_current_state().belief_state + ) + dialogue_acts = [] + + for slot in action_slots: + o = slot_outputs[slot] + dialogue_act = DialogueAct() + + # Default intent is "inform" + dialogue_act.intent = "inform" + + # Determine the value of the slot + if o == 1: + # The slot's value is requested by the user + dialogue_act.intent = "request" + dialogue_act.annotations.append(Annotation(slot)) + elif o == 2: + # The slot's value is set to "dontcare" + dialogue_act.annotations.append(Annotation(slot, "dontcare")) + elif o == 3: + # The slot's value is taken from the information need + if slot in self.information_need.constraints.keys(): + dialogue_act.annotations.append( + Annotation( + slot, self.information_need.constraints[slot] + ) + ) + elif o == 4: + # The slot's value was previously mentioned and is retrieved + # from the belief state + if slot in belief_state.keys(): + dialogue_act.annotations.append( + Annotation(slot, belief_state[slot]) + ) + elif o == 5: + # The slot's value in the information need is randomly modified + value = random.choice( + list( + self._item_collection.get_possible_property_values(slot) + ) + ) + self.information_need.constraints[slot] = value + dialogue_act.annotations.append(Annotation(slot, value)) + else: + logger.warning(f"{slot} is not mentioned in this turn.") + continue + + dialogue_acts.append(dialogue_act) + + return dialogue_acts diff --git a/usersimcrs/simulator/user_simulator.py b/usersimcrs/simulator/user_simulator.py index 4bb8998a..2bf297e8 100644 --- a/usersimcrs/simulator/user_simulator.py +++ b/usersimcrs/simulator/user_simulator.py @@ -5,14 +5,35 @@ from dialoguekit.core.utterance import Utterance from dialoguekit.participant.user import User, UserType +from usersimcrs.core.information_need import generate_random_information_need +from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.items.item_collection import ItemCollection + class UserSimulator(User, ABC): def __init__( self, id: str, + domain: SimulationDomain, + item_collection: ItemCollection, ) -> None: - """Initializes the user simulator.""" + """Initializes the user simulator. + + Args: + id: User ID. + domain: Domain knowledge. + item_collection: Collection of items. + """ super().__init__(id, UserType.SIMULATOR) + self._domain = domain + self._item_collection = item_collection + self.get_new_information_need() + + def get_new_information_need(self) -> None: + """Generates a new information need.""" + self.information_need = generate_random_information_need( + self._domain, self._item_collection + ) @abstractmethod def _generate_response(self, agent_utterance: Utterance) -> Utterance: From 03d5300210648ec266d69579f2c4feefdbe7b935 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:34:20 +0200 Subject: [PATCH 21/24] Add training script for TUS --- tests/core/test_information_need.py | 80 ++++ tests/data/tus_annotated_dialogues.json | 165 +++++++ tests/simulator/tus/test_tus_dataset.py | 77 ++++ usersimcrs/core/information_need.py | 21 + .../simulator/neural/tus/tus_dataset.py | 163 +++++++ .../tus/user_policy_network_training.py | 415 ++++++++++++++++++ 6 files changed, 921 insertions(+) create mode 100644 tests/data/tus_annotated_dialogues.json create mode 100644 tests/simulator/tus/test_tus_dataset.py create mode 100644 usersimcrs/simulator/neural/tus/tus_dataset.py create mode 100644 usersimcrs/simulator/neural/tus/user_policy_network_training.py diff --git a/tests/core/test_information_need.py b/tests/core/test_information_need.py index 65d60c7a..a339292e 100644 --- a/tests/core/test_information_need.py +++ b/tests/core/test_information_need.py @@ -7,6 +7,7 @@ generate_random_information_need, ) from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.items.item import Item from usersimcrs.items.item_collection import ItemCollection @@ -60,3 +61,82 @@ def test_get_requestable_slots(information_need: InformationNeed) -> None: assert information_need.get_requestable_slots() == ["PLOT", "RATING"] information_need.requested_slots["RATING"] = 4.5 assert information_need.get_requestable_slots() == ["PLOT"] + + +def test_to_dict(information_need: InformationNeed) -> None: + """Test to_dict. + + Args: + information_need: Information need. + """ + assert information_need.to_dict() == { + "target_items": [ + { + "item_id": "1", + "properties": { + "GENRE": "Comedy", + "DIRECTOR": "Steven Spielberg", + "RATING": 4.5, + "PLOT": "A movie plot", + }, + } + ], + "constraints": {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"}, + "requests": ["PLOT", "RATING"], + } + + +def test_from_dict() -> None: + """Tests from_dict.""" + data = { + "target_items": [ + { + "item_id": "1", + "properties": { + "GENRE": "Comedy", + "DIRECTOR": "Steven Spielberg", + "PLOT": "A movie plot", + }, + }, + { + "item_id": "2", + "properties": { + "GENRE": "Drama", + "ACTOR": "Steven Spielberg", + "RATING": 4.5, + }, + }, + ], + "constraints": {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"}, + "requests": ["PLOT", "RATING"], + } + expected_information_need = InformationNeed( + [ + Item( + "1", + { + "GENRE": "Comedy", + "DIRECTOR": "Steven Spielberg", + "PLOT": "A movie plot", + }, + ), + Item( + "2", + {"GENRE": "Drama", "ACTOR": "Steven Spielberg", "RATING": 4.5}, + ), + ], + {"GENRE": "Comedy", "DIRECTOR": "Steven Spielberg"}, + ["PLOT", "RATING"], + ) + loaded_information_need = InformationNeed.from_dict(data) + assert [target.id for target in loaded_information_need.target_items] == [ + target.id for target in expected_information_need.target_items + ] + assert ( + loaded_information_need.constraints + == expected_information_need.constraints + ) + assert ( + loaded_information_need.requested_slots + == expected_information_need.requested_slots + ) diff --git a/tests/data/tus_annotated_dialogues.json b/tests/data/tus_annotated_dialogues.json new file mode 100644 index 00000000..acddef39 --- /dev/null +++ b/tests/data/tus_annotated_dialogues.json @@ -0,0 +1,165 @@ +[ + { + "conversation_id": "1", + "conversation": [ + { + "participant": "USER", + "utterance": "Hello, recommend me an action movie.", + "dialogue_acts": [ + { + "intent": "DISCLOSE", + "slot_values": [["GENRE", "action"]] + } + ] + }, + { + "participant": "AGENT", + "utterance": "Sure, how about Mission Impossible?", + "dialogue_acts": [ + { + "intent": "RECOMMEND", + "slot_values": [["TITLE", "Mission Impossible"]] + } + ] + }, + { + "participant": "USER", + "utterance": "Sounds good. What's it about?", + "dialogue_acts": [ + { + "intent": "INQUIRE", + "slot_values": [["PLOT", null]] + } + ] + }, + { + "participant": "AGENT", + "utterance": "A secret agent is sent on a mission to save the world.", + "dialogue_acts": [ + { + "intent": "INFORM", + "slot_values": [ + ["PLOT", "A secret agent is sent on a mission to save the world."] + ] + } + ] + } + ], + "agent": { + "id": "Agent", + "type": "AGENT" + }, + "user": { + "id": "User", + "type": "USER" + }, + "metadata": { + "information_need": { + "constraints": { + "GENRE": "action", + "ACTOR": "Tom Cruise" + }, + "requests": ["PLOT"], + "target_items": [ + { + "item_id": "1", + "properties": { + "GENRE": "action", + "ACTOR": "Tom Cruise", + "PLOT": "A secret agent is sent on a mission to save the world." + } + } + ] + } + } + }, + { + "conversation_id": "2", + "conversation": [ + { + "participant": "USER", + "utterance": "I want to watch a comedy movie.", + "dialogue_acts": [ + { + "intent": "DISCLOSE", + "slot_values": [["GENRE", "comedy"]] + } + ] + }, + { + "participant": "AGENT", + "utterance": "Do you have a favorite director?", + "dialogue_acts": [ + { + "intent": "ELICIT", + "slot_values": [["DIRECTOR", null]] + } + ] + }, + { + "participant": "USER", + "utterance": "I love Steven Spielberg", + "dialogue_acts": [ + { + "intent": "DISCLOSE", + "slot_values": [["DIRECTOR", "Steven Spielberg"]] + } + ] + }, + { + "participant": "AGENT", + "utterance": "How about The Terminal?", + "dialogue_acts": [ + { + "intent": "RECOMMEND", + "slot_values": [["TITLE", "The Terminal"]] + } + ] + }, + { + "participant": "USER", + "utterance": "What's the rating?", + "dialogue_acts": [ + { + "intent": "INQUIRE", + "slot_values": [["RATING", null]] + } + ] + }, + { + "participant": "AGENT", + "utterance": "The Terminal has a rating of 4.5.", + "dialogue_acts": [ + { + "intent": "INFORM", + "slot_values": [["RATING", "4.5"]] + } + ] + } + ], + "agent": { + "id": "Agent", + "type": "AGENT" + }, + "user": { + "id": "User", + "type": "USER" + }, + "metadata": { + "information_need": { + "constraints": { "GENRE": "Comedy", "DIRECTOR": "Steven Spielberg" }, + "requests": ["RATING"], + "target_items": [ + { + "item_id": "1", + "properties": { + "GENRE": "Comedy", + "DIRECTOR": "Steven Spielberg", + "RATING": 4.5 + } + } + ] + } + } + } +] diff --git a/tests/simulator/tus/test_tus_dataset.py b/tests/simulator/tus/test_tus_dataset.py new file mode 100644 index 00000000..0c364814 --- /dev/null +++ b/tests/simulator/tus/test_tus_dataset.py @@ -0,0 +1,77 @@ +"""Tests for preparing TUS dataset for training.""" + +from typing import List + +import pytest +from dialoguekit.core.dialogue import Dialogue +from dialoguekit.utils.dialogue_reader import json_to_dialogues + +from usersimcrs.simulator.neural.tus.tus_dataset import TUSDataset +from usersimcrs.simulator.neural.tus.tus_feature_handler import ( + TUSFeatureHandler, +) + +TEST_DATA_PATH = "tests/data/tus_annotated_dialogues.json" + + +@pytest.fixture +def dialogues() -> List[Dialogue]: + """Loads dialogues from the test data file.""" + return json_to_dialogues(TEST_DATA_PATH) + + +@pytest.fixture +def tus_dataset(feature_handler: TUSFeatureHandler) -> TUSDataset: + """Returns a TUSDataset instance.""" + return TUSDataset(data_path=TEST_DATA_PATH, feature_handler=feature_handler) + + +def test_init_failure_file_path(feature_handler: TUSFeatureHandler) -> None: + """Tests dataset initialization failure.""" + with pytest.raises(FileNotFoundError): + TUSDataset( + data_path="non_existent_file.json", feature_handler=feature_handler + ) + + +def test_init_failure_dialogue_format( + feature_handler: TUSFeatureHandler, +) -> None: + """Tests dataset initialization failure.""" + with pytest.raises(ValueError): + TUSDataset( + data_path="tests/data/annotated_dialogues.json", + feature_handler=feature_handler, + ) + + +def test_len(tus_dataset: TUSDataset, dialogues: List[Dialogue]) -> None: + """Tests the length of the dataset.""" + assert len(tus_dataset) == len(dialogues) + + +def test_process_dialogue( + tus_dataset: TUSDataset, dialogues: List[Dialogue] +) -> None: + """Tests dialogue processing.""" + dialogue = dialogues[0] + + input_features = tus_dataset.process_dialogue(dialogue) + + assert input_features.get("dialogue_id") == dialogue.conversation_id + assert ( + len(input_features.get("input")) + == len(input_features.get("mask")) + == len(input_features.get("label")) + == len(dialogue.utterances) // 2 + ) + assert all( + len(input_features.get("input")[i]) + == len(input_features.get("mask")[i]) + == 80 + for i in range(len(input_features.get("input"))) + ) + assert all( + len(input_features.get("label")[i]) == 40 + for i in range(len(input_features.get("label"))) + ) diff --git a/usersimcrs/core/information_need.py b/usersimcrs/core/information_need.py index 4b36af1b..deab542b 100644 --- a/usersimcrs/core/information_need.py +++ b/usersimcrs/core/information_need.py @@ -92,3 +92,24 @@ def get_requestable_slots(self) -> List[str]: for slot in self.requested_slots if not self.requested_slots[slot] ] + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> InformationNeed: + """Creates information need from a dictionary.""" + target_items = [Item(**item) for item in data["target_items"]] + return cls( + target_items=target_items, + constraints=data["constraints"], + requests=data["requests"], + ) + + def to_dict(self) -> Dict[str, Any]: + """Returns information need as a dictionary.""" + return { + "target_items": [ + {"item_id": item.id, "properties": item.properties} + for item in self.target_items + ], + "constraints": self.constraints, + "requests": list(self.requested_slots.keys()), + } diff --git a/usersimcrs/simulator/neural/tus/tus_dataset.py b/usersimcrs/simulator/neural/tus/tus_dataset.py new file mode 100644 index 00000000..6710e410 --- /dev/null +++ b/usersimcrs/simulator/neural/tus/tus_dataset.py @@ -0,0 +1,163 @@ +"""Dataset class to encapsulate processing of data for training TUS. + +The data is expected to follow DialogueKit's format in addition to an +information need per dialogue. +""" + +import os +from typing import Any, Dict, List, Union + +import torch +from dialoguekit.core.dialogue import Dialogue +from dialoguekit.participant import DialogueParticipant +from dialoguekit.utils.dialogue_reader import json_to_dialogues + +from usersimcrs.core.information_need import InformationNeed +from usersimcrs.dialogue_management.dialogue_state_tracker import ( + DialogueStateTracker, +) +from usersimcrs.simulator.neural.tus.tus_feature_handler import ( + TUSFeatureHandler, +) + + +class TUSDataset(torch.utils.data.Dataset): + def __init__( + self, + data_path: str, + feature_handler: TUSFeatureHandler, + agent_ids: List[str] = None, + user_ids: List[str] = None, + ) -> None: + """Initializes the dataset. + + Args: + data_path: Path to the data file. + feature_handler: Feature handler. + agent_ids: List of agents' id to filter loaded dialogues. Defaults + to None. + user_ids: List of users' id to filter loaded dialogues. Defaults to + None. + + Raises: + FileNotFoundError: If the data file is not found. + """ + if not os.path.exists(data_path): + raise FileNotFoundError(f"File '{data_path}' not found.") + + self.raw_data = json_to_dialogues( + data_path, + agent_ids=agent_ids, + user_ids=user_ids, + ) + + self.feature_handler = feature_handler + self.input_vectors = self.process_dialogues() + + def __len__(self) -> int: + """Returns the number of dialogues in the dataset.""" + return len(self.raw_data) + + def __getitem__(self, idx: int) -> Dict[str, Any]: + """Retrieves input representation for a given utterance. + + Args: + idx: Index of the utterance. + + Returns: + Input representation. + """ + return { + "input": self.input_vectors["input"][idx], + "mask": self.input_vectors["mask"][idx], + "label": self.input_vectors["label"][idx], + "dialogue_id": self.input_vectors["dialogue_id"][idx], + } + + def process_dialogue(self, dialogue: Dialogue) -> Dict[str, Any]: + """Processes a dialogue to create input representations. + + Args: + dialogue: Dialogue to process. + + Raises: + ValueError: If information need is not found in the dialogue + metadata. + + Returns: + Input representation for each user utterance in the dialogue. + """ + input_representations = { + "input": [], + "mask": [], + "label": [], + "dialogue_id": dialogue.conversation_id, + } + + self.feature_handler.reset() + dst = DialogueStateTracker() + previous_state = dst.get_current_state() + information_need = dialogue.metadata.get("information_need", None) + if information_need is None: + raise ValueError("Information need not found in dialogue metadata.") + information_need = InformationNeed.from_dict(information_need) + + last_user_actions: Dict[str, torch.Tensor] = {} + utterances = dialogue.utterances + for i, utterance in enumerate(utterances): + if utterance.participant == DialogueParticipant.AGENT.name: + dst.update_state( + utterance.dialogue_acts, DialogueParticipant.AGENT + ) + continue + + agent_dialogue_acts = ( + utterances[i - 1].dialogue_acts if i > 0 else [] + ) + feature_vector, mask = self.feature_handler.build_input_vector( + agent_dialogue_acts, + previous_state, + dst.get_current_state(), + information_need, + last_user_actions, + ) + label = self.feature_handler.get_label_vector( + utterance, dst.get_current_state(), information_need + ) + input_representations["input"].append(feature_vector) + input_representations["mask"].append(mask) + input_representations["label"].append(label) + + dst.update_state(utterance.dialogue_acts, DialogueParticipant.USER) + previous_state = dst.get_current_state() + for slot, label in zip(self.feature_handler.action_slots, label): + action = torch.zeros(6) + action[label] = 1 + last_user_actions[slot] = action + + return input_representations + + def process_dialogues(self) -> Dict[str, Union[torch.Tensor, List[str]]]: + """Processes dialogues to create input representations. + + Returns: + Processed dialogues. + """ + processed_data: Dict[str, List[Any]] = { + "input": [], + "mask": [], + "label": [], + "dialogue_id": [], + } + for dialogue in self.raw_data: + self.feature_handler.reset_user_feature_history() + processed_dialogue = self.process_dialogue(dialogue) + for key, value in processed_dialogue.items(): + processed_data[key].extend(value) + + return { + "input": torch.tensor(processed_data["input"], dtype=torch.float32), + "mask": torch.tensor(processed_data["mask"], dtype=torch.bool), + "label": torch.tensor(processed_data["label"], dtype=torch.long), + "dialogue_id": processed_data["dialogue_id"], + } diff --git a/usersimcrs/simulator/neural/tus/user_policy_network_training.py b/usersimcrs/simulator/neural/tus/user_policy_network_training.py new file mode 100644 index 00000000..33eca783 --- /dev/null +++ b/usersimcrs/simulator/neural/tus/user_policy_network_training.py @@ -0,0 +1,415 @@ +"""Script to train the user policy network of the transformer-based US.""" + +import argparse +import logging +import os +from datetime import datetime + +import pandas as pd +import torch +import yaml +from torch.utils.tensorboard import SummaryWriter +from torcheval.metrics import MulticlassConfusionMatrix + +from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.simulator.neural.core.transformer import ( + TransformerEncoderModel, +) +from usersimcrs.simulator.neural.tus.tus_dataset import TUSDataset +from usersimcrs.simulator.neural.tus.tus_feature_handler import ( + TUSFeatureHandler, +) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO) + + +class Trainer: + def __init__( + self, + model: TransformerEncoderModel, + loss_function: torch.nn.Module, + optimizer: torch.optim.Optimizer, + max_turn_feature_length: int, + ) -> None: + """Initializes the trainer. + + Args: + model: User policy network. + loss_function: Loss function. + optimizer: Optimizer. + max_turn_feature_length: Maximum length of the input features. + """ + self.model = model + self.loss_function = loss_function + self.optimizer = optimizer + self.max_turn_feature_length = max_turn_feature_length + + def parse_output(self, output: torch.Tensor) -> torch.Tensor: + """Parses the model output. + + Args: + output: Model output. + + Returns: + Parsed output. + """ + _output = output[ + :, 1 : self.max_turn_feature_length + 1, : # noqa: E203 + ] + _output = torch.reshape( + _output, + ( + _output.shape[0] * _output.shape[1], + _output.shape[-1], + ), + ) + return _output + + def get_loss( + self, prediction: torch.Tensor, target: torch.Tensor + ) -> torch.Tensor: + """Computes the loss. + + Args: + prediction: Predicted values. + target: Target values. + + Returns: + Loss value. + """ + _prediction = self.parse_output(prediction) + return self.loss_function(_prediction, target.view(-1)) + + def train_one_epoch( + self, data_loader: torch.utils.data.DataLoader + ) -> float: + """Trains the model for one epoch. + + Args: + data_loader: Training data. + + Returns: + Average loss. + """ + self.model.train() + total_loss = 0.0 + for batch in data_loader: + self.optimizer.zero_grad() + input_feature = batch["input"] + mask = batch["mask"] + label = batch["label"] + output = self.model(input_feature, mask) + + loss = self.get_loss(output, label) + loss.backward() + self.optimizer.step() + total_loss += loss.item() + + return total_loss / len(data_loader) + + def train( + self, + training_data: torch.utils.data.DataLoader, + validation_data: torch.utils.data.DataLoader, + num_epochs: int, + model_path: str, + tb_writer: SummaryWriter = None, + ) -> None: + """Trains the user policy network for a given number of epochs. + + Args: + training_data: Training data. + validation_data: Validation data. + num_epochs: Number of epochs. + model_path: Path to save the model. + tb_writer: Tensorboard writer. Defaults to None. + """ + best_loss = float("inf") + for epoch in range(num_epochs): + train_loss = self.train_one_epoch(training_data) + # Compute validation loss + valid_loss = self.get_validation_loss(validation_data) + logger.info( + f"Epoch {epoch + 1}/{num_epochs} -- " + f"Train Loss: {train_loss:.4f} / " + f"Validation Loss: {valid_loss:.4f}" + ) + + if tb_writer is not None: + # Log losses to tensorboard + tb_writer.add_scalar("Loss/Train", train_loss, epoch) + tb_writer.add_scalar("Loss/Validation", valid_loss, epoch) + tb_writer.flush() + + if valid_loss < best_loss: + best_loss = valid_loss + torch.save(self.model.state_dict(), model_path) + + def get_validation_loss( + self, validation_data: torch.utils.data.DataLoader + ) -> float: + """Computes the validation loss. + + Args: + validation_data: Validation data. + + Returns: + Average validation loss. + """ + self.model.eval() + total_loss = 0.0 + with torch.no_grad(): + for batch in validation_data: + input_feature = batch["input"] + mask = batch["mask"] + label = batch["label"] + output = self.model(input_feature, mask) + + loss = self.get_loss(output, label) + total_loss += loss.item() + + return total_loss / len(validation_data) + + def evaluate( + self, + test_data: torch.utils.data.DataLoader, + tb_writer: SummaryWriter = None, + ) -> pd.DataFrame: + """Evaluates the user policy network. + + Args: + test_data: Test data. + tb_writer: Tensorboard writer. Defaults to None. + + Returns: + Dataframe with accuracy, precision, recall, and F1 score for each + class (i.e., possible values for a slot). + """ + confusion_matrix = MulticlassConfusionMatrix(num_classes=6) + self.model.eval() + self.model.zero_grad() + with torch.no_grad(): + for batch in test_data: + input_feature = batch["input"] + mask = batch["mask"] + label = batch["label"] + output = self.model(input_feature, mask) + + loss = self.get_loss(output, label) + + predictions = self.parse_output(output) + targets = label.view(-1) + confusion_matrix.update(predictions, targets) + + if tb_writer is not None: + tb_writer.add_scalar("Loss/Test", loss.item()) + tb_writer.flush() + + return self.compute_metrics(confusion_matrix) + + def compute_metrics( + self, confusion_matrix: MulticlassConfusionMatrix + ) -> pd.DataFrame: + """Computes metrics based on the confusion matrix. + + Args: + confusion_matrix: Confusion matrix. + + Returns: + Metrics. + """ + matrix = confusion_matrix.compute() + num_classes = matrix.shape[0] + metrics = dict() + + for i in range(num_classes): + tp = matrix[i, i] + fp = sum(matrix[:, i]) - tp + fn = sum(matrix[i, :]) - tp + tn = sum(sum(matrix)) - tp - fp - fn + + accuracy = ( + (tp + tn) / (tp + tn + fp + fn) + if tp + tn + fp + fn > 0 + else torch.tensor(0) + ) + precision = tp / (tp + fp) if tp + fp > 0 else torch.tensor(0) + recall = tp / (tp + fn) if tp + fn > 0 else torch.tensor(0) + f1 = ( + 2 * precision * recall / (precision + recall) + if precision + recall > 0 + else torch.tensor(0) + ) + metrics[i] = { + "precision": precision.item(), + "recall": recall.item(), + "f1": f1.item(), + "accuracy": accuracy.item(), + } + + df = pd.DataFrame(metrics).T + df["macro avg"] = df.mean(1) + + return df + + +def parse_args() -> argparse.Namespace: + """Parses command-line arguments. + + Returns: + Namespace object containing the arguments. + """ + parser = argparse.ArgumentParser(prog="user_policy_network_training.py") + parser.add_argument( + "--data_path", + type=str, + help="Path to the data file.", + ) + parser.add_argument( + "--domain", + type=str, + help="Path to the domain configuration file.", + ) + parser.add_argument( + "--agent_actions_path", + type=str, + help="Path to the agent actions file.", + ) + parser.add_argument( + "--max_turn_feature_length", + type=int, + default=75, + help="Maximum length of the input features.", + ) + parser.add_argument( + "--context_depth", + type=int, + default=2, + help="Number of turns used to build input representations.", + ) + parser.add_argument( + "--seed", + type=int, + default=42, + help="Random seed.", + ) + parser.add_argument( + "--batch_size", + type=int, + default=32, + help="Batch size.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-3, + help="Learning rate.", + ) + parser.add_argument( + "--num_epochs", + type=int, + default=10, + help="Number of epochs.", + ) + parser.add_argument( + "--output_dir", + type=str, + default="data/models", + help="Output directory.", + ) + parser.add_argument( + "--logs", action="store_true", help="Enable Tensorboard logs." + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + if not os.path.exists(args.output_dir): + os.makedirs(args.output_dir) + + # Tensorboard writer + writer = None + if args.logs: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + writer = SummaryWriter(f"logs/tus_training_{timestamp}") + + # Initialize feature handler + domain = SimulationDomain(args.domain) + with open(args.agent_actions_path, "r") as f: + agent_actions = yaml.safe_load(f) + feature_handler = TUSFeatureHandler( + domain, args.max_turn_feature_length, args.context_depth, agent_actions + ) + feature_handler_path = os.path.join( + args.output_dir, "feature_handler.joblib" + ) + feature_handler.save_handler(feature_handler_path) + + # Load data + logger.info(f"Loading data from {args.data_path}") + dataset = TUSDataset(args.data_path, feature_handler) + train_dataset, valid_dataset, test_dataset = torch.utils.data.random_split( + dataset, + [0.8, 0.1, 0.1], + generator=torch.Generator().manual_seed(args.seed), + ) + + train_data_loader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.batch_size, shuffle=True + ) + valid_data_loader = torch.utils.data.DataLoader( + valid_dataset, batch_size=args.batch_size, shuffle=True + ) + test_data_loader = torch.utils.data.DataLoader( + test_dataset, batch_size=args.batch_size, shuffle=True + ) + + # Define user policy network + # TODO: Make config an argument + config = { + "input_dim": 37, + "output_dim": 6, + "num_encoder_layers": 2, + "nhead": 4, + "hidden_dim": 200, + } + user_policy_network = TransformerEncoderModel(**config) + user_policy_network.to(device) + + # Define loss function + loss_function = torch.nn.CrossEntropyLoss(ignore_index=-1) + + # Define optimizer + optimizer = torch.optim.Adam( + user_policy_network.parameters(), lr=args.learning_rate + ) + + # Initialize trainer + trainer = Trainer( + user_policy_network, + loss_function, + optimizer, + args.max_turn_feature_length, + ) + model_path = os.path.join(args.output_dir, "user_policy_network.pt") + trainer.train( + train_data_loader, + valid_data_loader, + args.num_epochs, + model_path, + tb_writer=writer, + ) + + # Evaluate the model + logger.info("====\nEvaluating the model") + metrics = trainer.evaluate(test_data_loader) + logger.info(f"Metrics:\n{metrics}") + logger.info("====") + + if writer is not None: + writer.close() From 4ee5abea69e6de96b7707b48d2b12b9f9d0a2c1b Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:04:46 +0200 Subject: [PATCH 22/24] Fix tests --- tests/core/test_information_need.py | 1 + tests/data/tus_annotated_dialogues.json | 103 +++++++++++++++--- tests/simulator/tus/test_tus_dataset.py | 2 +- .../simulator/neural/tus/tus_dataset.py | 4 +- 4 files changed, 93 insertions(+), 17 deletions(-) diff --git a/tests/core/test_information_need.py b/tests/core/test_information_need.py index acace5c8..a339292e 100644 --- a/tests/core/test_information_need.py +++ b/tests/core/test_information_need.py @@ -7,6 +7,7 @@ generate_random_information_need, ) from usersimcrs.core.simulation_domain import SimulationDomain +from usersimcrs.items.item import Item from usersimcrs.items.item_collection import ItemCollection diff --git a/tests/data/tus_annotated_dialogues.json b/tests/data/tus_annotated_dialogues.json index acddef39..6520841c 100644 --- a/tests/data/tus_annotated_dialogues.json +++ b/tests/data/tus_annotated_dialogues.json @@ -8,7 +8,14 @@ "dialogue_acts": [ { "intent": "DISCLOSE", - "slot_values": [["GENRE", "action"]] + "slot_values": [ + [ + "GENRE", + "action", + null, + null + ] + ] } ] }, @@ -18,7 +25,14 @@ "dialogue_acts": [ { "intent": "RECOMMEND", - "slot_values": [["TITLE", "Mission Impossible"]] + "slot_values": [ + [ + "TITLE", + "Mission Impossible", + null, + null + ] + ] } ] }, @@ -28,7 +42,14 @@ "dialogue_acts": [ { "intent": "INQUIRE", - "slot_values": [["PLOT", null]] + "slot_values": [ + [ + "PLOT", + null, + null, + null + ] + ] } ] }, @@ -39,7 +60,12 @@ { "intent": "INFORM", "slot_values": [ - ["PLOT", "A secret agent is sent on a mission to save the world."] + [ + "PLOT", + "A secret agent is sent on a mission to save the world.", + null, + null + ] ] } ] @@ -59,7 +85,9 @@ "GENRE": "action", "ACTOR": "Tom Cruise" }, - "requests": ["PLOT"], + "requests": [ + "PLOT" + ], "target_items": [ { "item_id": "1", @@ -82,7 +110,14 @@ "dialogue_acts": [ { "intent": "DISCLOSE", - "slot_values": [["GENRE", "comedy"]] + "slot_values": [ + [ + "GENRE", + "comedy", + null, + null + ] + ] } ] }, @@ -92,7 +127,14 @@ "dialogue_acts": [ { "intent": "ELICIT", - "slot_values": [["DIRECTOR", null]] + "slot_values": [ + [ + "DIRECTOR", + null, + null, + null + ] + ] } ] }, @@ -102,7 +144,14 @@ "dialogue_acts": [ { "intent": "DISCLOSE", - "slot_values": [["DIRECTOR", "Steven Spielberg"]] + "slot_values": [ + [ + "DIRECTOR", + "Steven Spielberg", + null, + null + ] + ] } ] }, @@ -112,7 +161,14 @@ "dialogue_acts": [ { "intent": "RECOMMEND", - "slot_values": [["TITLE", "The Terminal"]] + "slot_values": [ + [ + "TITLE", + "The Terminal", + null, + null + ] + ] } ] }, @@ -122,7 +178,14 @@ "dialogue_acts": [ { "intent": "INQUIRE", - "slot_values": [["RATING", null]] + "slot_values": [ + [ + "RATING", + null, + null, + null + ] + ] } ] }, @@ -132,7 +195,14 @@ "dialogue_acts": [ { "intent": "INFORM", - "slot_values": [["RATING", "4.5"]] + "slot_values": [ + [ + "RATING", + "4.5", + null, + null + ] + ] } ] } @@ -147,8 +217,13 @@ }, "metadata": { "information_need": { - "constraints": { "GENRE": "Comedy", "DIRECTOR": "Steven Spielberg" }, - "requests": ["RATING"], + "constraints": { + "GENRE": "Comedy", + "DIRECTOR": "Steven Spielberg" + }, + "requests": [ + "RATING" + ], "target_items": [ { "item_id": "1", @@ -162,4 +237,4 @@ } } } -] +] \ No newline at end of file diff --git a/tests/simulator/tus/test_tus_dataset.py b/tests/simulator/tus/test_tus_dataset.py index 0c364814..98e2f742 100644 --- a/tests/simulator/tus/test_tus_dataset.py +++ b/tests/simulator/tus/test_tus_dataset.py @@ -3,9 +3,9 @@ from typing import List import pytest + from dialoguekit.core.dialogue import Dialogue from dialoguekit.utils.dialogue_reader import json_to_dialogues - from usersimcrs.simulator.neural.tus.tus_dataset import TUSDataset from usersimcrs.simulator.neural.tus.tus_feature_handler import ( TUSFeatureHandler, diff --git a/usersimcrs/simulator/neural/tus/tus_dataset.py b/usersimcrs/simulator/neural/tus/tus_dataset.py index 6710e410..fd7a7d59 100644 --- a/usersimcrs/simulator/neural/tus/tus_dataset.py +++ b/usersimcrs/simulator/neural/tus/tus_dataset.py @@ -8,10 +8,10 @@ from typing import Any, Dict, List, Union import torch + from dialoguekit.core.dialogue import Dialogue from dialoguekit.participant import DialogueParticipant from dialoguekit.utils.dialogue_reader import json_to_dialogues - from usersimcrs.core.information_need import InformationNeed from usersimcrs.dialogue_management.dialogue_state_tracker import ( DialogueStateTracker, @@ -105,7 +105,7 @@ def process_dialogue(self, dialogue: Dialogue) -> Dict[str, Any]: last_user_actions: Dict[str, torch.Tensor] = {} utterances = dialogue.utterances for i, utterance in enumerate(utterances): - if utterance.participant == DialogueParticipant.AGENT.name: + if utterance.participant == DialogueParticipant.AGENT: dst.update_state( utterance.dialogue_acts, DialogueParticipant.AGENT ) From 46e299a00dfb318aae9080ea9c7fffd3f21a35b5 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:29:24 +0200 Subject: [PATCH 23/24] Quick fixes --- usersimcrs/simulator/neural/tus/tus.py | 33 +++++++++---------- .../simulator/neural/tus/tus_dataset.py | 2 +- .../neural/tus/tus_feature_handler.py | 4 +-- usersimcrs/simulator/user_simulator.py | 4 --- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/usersimcrs/simulator/neural/tus/tus.py b/usersimcrs/simulator/neural/tus/tus.py index c41f9b97..f73dffe7 100644 --- a/usersimcrs/simulator/neural/tus/tus.py +++ b/usersimcrs/simulator/neural/tus/tus.py @@ -15,13 +15,14 @@ from typing import Any, DefaultDict, Dict, List import torch + from dialoguekit.core.annotated_utterance import AnnotatedUtterance -from dialoguekit.core.annotation import Annotation from dialoguekit.core.dialogue_act import DialogueAct +from dialoguekit.core.intent import Intent +from dialoguekit.core.slot_value_annotation import SlotValueAnnotation from dialoguekit.core.utterance import Utterance from dialoguekit.nlu.nlu import NLU from dialoguekit.participant import DialogueParticipant - from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.dialogue_management.dialogue_state_tracker import ( DialogueStateTracker, @@ -139,21 +140,13 @@ def _annotate_agent_utterance( ) -> AnnotatedUtterance: """Annotates the agent utterance. - As of now, DialogueKit does not support NLU that can annotate dialogue - acts. So, only one dialogue act is created with the intent and - annotations extracted from the agent utterance. - Args: agent_utterance: Agent utterance. Returns: Annotated utterance. """ - agent_intent = self._nlu.classify_intent(agent_utterance) - agent_annotations = self._nlu.annotate_slot_values(agent_utterance) - dialogue_acts = [ - DialogueAct(intent=agent_intent, annotations=agent_annotations) - ] + dialogue_acts = self._nlu.extract_dialogue_acts(agent_utterance.text) utt = AnnotatedUtterance( text=agent_utterance.text, participant=DialogueParticipant.AGENT, @@ -221,21 +214,23 @@ def _parse_policy_output( dialogue_act = DialogueAct() # Default intent is "inform" - dialogue_act.intent = "inform" + dialogue_act.intent = Intent("inform") # Determine the value of the slot if o == 1: # The slot's value is requested by the user - dialogue_act.intent = "request" - dialogue_act.annotations.append(Annotation(slot)) + dialogue_act.intent = Intent("request") + dialogue_act.annotations.append(SlotValueAnnotation(slot)) elif o == 2: # The slot's value is set to "dontcare" - dialogue_act.annotations.append(Annotation(slot, "dontcare")) + dialogue_act.annotations.append( + SlotValueAnnotation(slot, "dontcare") + ) elif o == 3: # The slot's value is taken from the information need if slot in self.information_need.constraints.keys(): dialogue_act.annotations.append( - Annotation( + SlotValueAnnotation( slot, self.information_need.constraints[slot] ) ) @@ -244,7 +239,7 @@ def _parse_policy_output( # from the belief state if slot in belief_state.keys(): dialogue_act.annotations.append( - Annotation(slot, belief_state[slot]) + SlotValueAnnotation(slot, ",".join(belief_state[slot])) ) elif o == 5: # The slot's value in the information need is randomly modified @@ -254,7 +249,9 @@ def _parse_policy_output( ) ) self.information_need.constraints[slot] = value - dialogue_act.annotations.append(Annotation(slot, value)) + dialogue_act.annotations.append( + SlotValueAnnotation(slot, value) + ) else: logger.warning(f"{slot} is not mentioned in this turn.") continue diff --git a/usersimcrs/simulator/neural/tus/tus_dataset.py b/usersimcrs/simulator/neural/tus/tus_dataset.py index fd7a7d59..a391363d 100644 --- a/usersimcrs/simulator/neural/tus/tus_dataset.py +++ b/usersimcrs/simulator/neural/tus/tus_dataset.py @@ -87,7 +87,7 @@ def process_dialogue(self, dialogue: Dialogue) -> Dict[str, Any]: Returns: Input representation for each user utterance in the dialogue. """ - input_representations = { + input_representations: Dict[str, Any] = { "input": [], "mask": [], "label": [], diff --git a/usersimcrs/simulator/neural/tus/tus_feature_handler.py b/usersimcrs/simulator/neural/tus/tus_feature_handler.py index 1cab3dca..01f9de02 100644 --- a/usersimcrs/simulator/neural/tus/tus_feature_handler.py +++ b/usersimcrs/simulator/neural/tus/tus_feature_handler.py @@ -14,8 +14,8 @@ import torch from dialoguekit.core.annotated_utterance import AnnotatedUtterance -from dialoguekit.core.annotation import Annotation from dialoguekit.core.dialogue_act import DialogueAct +from dialoguekit.core.slot_value_annotation import SlotValueAnnotation from usersimcrs.core.information_need import InformationNeed from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.dialogue_management.dialogue_state import DialogueState @@ -429,7 +429,7 @@ def get_label_vector( def _get_label( self, - annotation: Annotation, + annotation: SlotValueAnnotation, current_state: DialogueState, information_need: InformationNeed, ) -> int: diff --git a/usersimcrs/simulator/user_simulator.py b/usersimcrs/simulator/user_simulator.py index e6c00146..d8ef359d 100644 --- a/usersimcrs/simulator/user_simulator.py +++ b/usersimcrs/simulator/user_simulator.py @@ -9,10 +9,6 @@ from usersimcrs.core.simulation_domain import SimulationDomain from usersimcrs.items.item_collection import ItemCollection -from usersimcrs.core.information_need import generate_random_information_need -from usersimcrs.core.simulation_domain import SimulationDomain -from usersimcrs.items.item_collection import ItemCollection - class UserSimulator(User, ABC): def __init__( From 1bdedf2ef086df006da3aecb4190cd2899472834 Mon Sep 17 00:00:00 2001 From: Nolwenn <28621493+NoB0@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:05:54 +0200 Subject: [PATCH 24/24] Update TUS feature handler --- .../information_need_prompt_movies_default.txt | 2 +- usersimcrs/simulator/neural/tus/tus_feature_handler.py | 5 ++++- .../simulator/neural/tus/user_policy_network_training.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/datasets/information_need_annotation/information_need_prompt_movies_default.txt b/scripts/datasets/information_need_annotation/information_need_prompt_movies_default.txt index 3d8f8266..c404ca82 100644 --- a/scripts/datasets/information_need_annotation/information_need_prompt_movies_default.txt +++ b/scripts/datasets/information_need_annotation/information_need_prompt_movies_default.txt @@ -4,7 +4,7 @@ Your should be formatted as a JSON object with two fields: constraints and reque Constraints are represented as a dictionary where the keys are the slots, and the values are the values that the movie must have for that slot. The possible slots are: - GENRE: Movie genre - ACTOR: Actor starring in the movie -- KEYWORD: Keyword associated with the movie +- KEYWORDS: Keywords associated with the movie - DIRECTOR: Director of the movie Requests are represented as a list of slots. The possible slots are: - PLOT: Movie plot diff --git a/usersimcrs/simulator/neural/tus/tus_feature_handler.py b/usersimcrs/simulator/neural/tus/tus_feature_handler.py index 01f9de02..da60d621 100644 --- a/usersimcrs/simulator/neural/tus/tus_feature_handler.py +++ b/usersimcrs/simulator/neural/tus/tus_feature_handler.py @@ -202,7 +202,10 @@ def get_slot_index_feature(self, slot: str) -> FeatureVector: Feature vector for slot index. """ v_slot_index = [0] * len(self.slot_index) - v_slot_index[self.slot_index[slot]] = 1 + try: + v_slot_index[self.slot_index[slot]] = 1 + except KeyError: + logging.warning(f"Slot '{slot}' not found in the slot index.") return v_slot_index def get_slot_feature_vector( diff --git a/usersimcrs/simulator/neural/tus/user_policy_network_training.py b/usersimcrs/simulator/neural/tus/user_policy_network_training.py index 33eca783..90dcb359 100644 --- a/usersimcrs/simulator/neural/tus/user_policy_network_training.py +++ b/usersimcrs/simulator/neural/tus/user_policy_network_training.py @@ -316,7 +316,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--output_dir", type=str, - default="data/models", + default="data/models/tus_policy_network", help="Output directory.", ) parser.add_argument( @@ -372,7 +372,7 @@ def parse_args() -> argparse.Namespace: # Define user policy network # TODO: Make config an argument config = { - "input_dim": 37, + "input_dim": 59, "output_dim": 6, "num_encoder_layers": 2, "nhead": 4,