Skip to content
This repository has been archived by the owner on Nov 3, 2023. It is now read-only.

[TOD] World, world metrics, script, tests #4178

Merged
merged 33 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6cb4b86
[TOD] Core converesation structure, serialization, const tokens
Nov 15, 2021
1480def
fix test by adding init folder
Nov 16, 2021
de84801
[Tod] Agents, teacher metrics, and tests for these
Nov 16, 2021
638eb28
[TOD] World, world metrics, script, tests
Nov 16, 2021
0e3f492
hmmm... hoping stacks don't bite me. (change that was kept in upper d…
Nov 16, 2021
0643a62
Merge branch 'simpler_tod_1_core_only' into simpler_tod_2_agents_teac…
Nov 16, 2021
37aced2
minor, remove commented out print
Nov 16, 2021
4f91279
Merge branch 'simpler_tod_2_agents_teachers' into simpler_tod_3_world
Nov 16, 2021
b05930f
comment
Nov 16, 2021
5086e85
more comment updates (not sure if it actually helps clarity..)
Nov 16, 2021
51ed1a9
Merge branch 'main' into simpler_tod_1_core_only
Nov 16, 2021
a6508be
Merge branch 'simpler_tod_1_core_only' into simpler_tod_2_agents_teac…
Nov 16, 2021
eebc36b
Merge branch 'simpler_tod_2_agents_teachers' into simpler_tod_3_world
Nov 16, 2021
3675781
use same version of black as in the pre-commit hook
Nov 16, 2021
086c91c
Merge branch 'simpler_tod_2_agents_teachers' into simpler_tod_3_world
Nov 16, 2021
0bc961e
use same version of black as in the pre-commit hook
Nov 16, 2021
dfc4989
Merge branch 'main' into simpler_tod_2_agents_teachers
Nov 29, 2021
2f15448
address eric comments; add new readme + more documentation
Nov 30, 2021
abd1c7e
Merge branch 'simpler_tod_2_agents_teachers' into simpler_tod_3_world
Nov 30, 2021
5d0197d
minor wording change
Nov 30, 2021
39792a8
Merge branch 'simpler_tod_2_agents_teachers' into simpler_tod_3_world
Nov 30, 2021
76bfa89
add more documtnation to world tests (following comment on teacher te…
Nov 30, 2021
73c5c7a
minor comment update
Nov 30, 2021
7ab9d70
update to respect actual count of episodes (I think this might have i…
Dec 1, 2021
c6c728d
Merge branch 'main' into simpler_tod_2_agents_teachers
Dec 1, 2021
b3283d0
Merge branch 'simpler_tod_2_agents_teachers' into simpler_tod_3_world
Dec 1, 2021
0580ff0
Merge branch 'main' into simpler_tod_2_agents_teachers
Dec 2, 2021
e00accf
Merge branch 'simpler_tod_2_agents_teachers' into simpler_tod_3_world
Dec 2, 2021
7b24acf
Merge branch 'main' into simpler_tod_3_world
Dec 18, 2021
83439b5
update comments to be a bit more descriptive of what's happening
Dec 18, 2021
f7d210e
lint
Dec 20, 2021
55c198c
generate -> setup to be correct
Dec 20, 2021
82c88b6
address paul comments
Dec 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def filter_tests_with_circleci(test_list):
('datatests/', 'data'),
('parlai/tasks/', 'teacher'),
('tasks/', 'tasks'),
('tod/', 'tod'),
]


Expand Down
6 changes: 6 additions & 0 deletions parlai/core/tod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Core classes for Task-Oriented Dialog (TOD)

For understanding usage of these classes, start with `tod_agents.py` (for understanding how to setup agents such that they work with new datasets) and `parlai/scripts/tod_world_script.py` (for understanding how to run simulations with the TOD conversations format.

As a convention, files that have elements that are expected to be referenced outside of this directory (and outisde of `parlai/projects/tod_simulator`) are prefixed wth `tod_`.

5 changes: 5 additions & 0 deletions parlai/core/tod/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python3

# Copyright (c) Facebook, Inc. and its affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
159 changes: 159 additions & 0 deletions parlai/core/tod/teacher_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env python3

# Copyright (c) Facebook, Inc. and its affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

"""
Task Oriented Dialogue (TOD) teacher metrics.
"""
from typing import Optional, List, Dict, Any
from parlai.core.metrics import AverageMetric, BleuMetric, F1Metric, Metric, Metrics


class SlotMetrics(Metrics):
"""
Helper container which encapsulates standard slot metrics in task oriented learning
(jga, slot_p, slot_r, etc).

Due to differences in dialogue representations between tasks, the input is pre-
parsed ground truth and predicted slot dictionaries.

The 'jga+nlg' metric assumes a balanced set of JGA and NLG scores such that
2 * Avg(JGA, NLG_BLEU) = Avg(JGA + NLG_BLEU)
The `jga+nlg` metric assumes that `NlgMetrics` is used to calculated the other side.
"""

def __init__(
self,
teacher_slots: Dict[str, str],
predicted_slots: Dict[str, str],
prefixes: Optional[List] = None,
shared: Dict[str, Any] = None,
avg_jga_nlg_bleu: bool = False,
) -> None:
super().__init__(shared=shared)
self.prefixes = prefixes if prefixes else []
# jga and optionally Avg(jga,nlg_bleu)
self.add_with_prefixes("jga", AverageMetric(teacher_slots == predicted_slots))
if len(teacher_slots) > 0:
self.add_with_prefixes(
"jga_noempty", AverageMetric(teacher_slots == predicted_slots)
)
else:
self.add_with_prefixes(
"jga_empty", AverageMetric(teacher_slots == predicted_slots)
)

if avg_jga_nlg_bleu:
# add one half of Avg(jga,nlg_bleu), NlgMetrics class (below) adds NLG-BLEU
self.add("jga+nlg", AverageMetric(teacher_slots == predicted_slots))
# precision
for pred_slot_name, pred_value in predicted_slots.items():
slot_p = AverageMetric(teacher_slots.get(pred_slot_name) == pred_value)
self.add_with_prefixes("slot_p", slot_p)
self.add_with_prefixes("slot_f1", SlotF1Metric(slot_p=slot_p))
# recall
for teacher_slot_name, teacher_value in teacher_slots.items():
slot_r = AverageMetric(
predicted_slots.get(teacher_slot_name) == teacher_value
)
self.add_with_prefixes("slot_r", slot_r)
self.add_with_prefixes("slot_f1", SlotF1Metric(slot_r=slot_r))

def add_with_prefixes(self, name, value):
self.add(name, value)
for prefix in self.prefixes:
self.add(f"{prefix}/{name}", value)


class NlgMetrics(Metrics):
"""
Helper container for generation version of standard metrics (F1, BLEU, ..).
"""

def __init__(
self,
guess: str,
labels: Optional[List[str]],
prefixes: Optional[List[str]] = None,
shared: Dict[str, Any] = None,
avg_jga_nlg_bleu: bool = False,
) -> None:
super().__init__(shared=shared)
self.prefixes = prefixes if prefixes else []
bleu = BleuMetric.compute(guess, labels)
f1 = F1Metric.compute(guess, labels)
self.add_with_prefixes("nlg_bleu", bleu)
self.add_with_prefixes("nlg_f1", f1)
if avg_jga_nlg_bleu:
# add one half of Avg(jga,nlg_bleu), SlotMetrics class (above) adds JGA
self.add("jga+nlg", bleu)

def add_with_prefixes(self, name, value):
self.add(name, value)
for prefix in self.prefixes:
self.add(f"{prefix}/{name}", value)


AverageType = Optional[AverageMetric]


def _average_type_sum_helper(first: AverageType, second: AverageType) -> AverageType:
"""
Helper to deal with Nones.

We are "clever" in how we aggregate SlotF1Metrics (See SlotMetrics `__init__`) in
that we add precision and recall values separately, but this means we need to handle
None.
"""
if first is None:
return second
if second is None:
return first
return first + second


class SlotF1Metric(Metric):
"""
Metric to keep track of slot F1.

Keeps track of slot precision and slot recall as running metrics.
"""

__slots__ = ("_slot_p", "_slot_r")

@property
def macro_average(self) -> bool:
"""
Indicates whether this metric should be macro-averaged when globally reported.
"""
return True

def __init__(self, slot_p: AverageType = None, slot_r: AverageType = None):
if not isinstance(slot_p, AverageMetric) and slot_p is not None:
slot_p = AverageMetric(slot_p)
if not isinstance(slot_r, AverageMetric) and slot_r is not None:
slot_r = AverageMetric(slot_r)
self._slot_p = slot_p
self._slot_r = slot_r

def __add__(self, other: Optional["SlotF1Metric"]) -> "SlotF1Metric":
# NOTE: hinting can be cleaned up with "from __future__ import annotations" when
# we drop Python 3.6
if other is None:
return self
slot_p = _average_type_sum_helper(self._slot_p, other._slot_p)
slot_r = _average_type_sum_helper(self._slot_r, other._slot_r)
return type(self)(slot_p=slot_p, slot_r=slot_r)

def value(self) -> float:
if self._slot_p is None or self._slot_r is None:
return float("nan")
else:
slot_p = self._slot_p.value()
slot_r = self._slot_r.value()
if slot_p == 0.0 and slot_r == 0.0:
return float("nan")
else:
return 2 * (slot_p * slot_r) / (slot_p + slot_r)
Loading