ultk.language.semantics

Classes for modeling the meanings of a language.

Meanings are modeled as things which map linguistic forms to objects of reference. The linguistic forms and objects of reference can in principle be very detailed, and future work may elaborate the meaning classes and implement a Form class.

In efficient communication analyses, simplicity and informativeness can be measured as properties of semantic aspects of a language. E.g., a meaning is simple if it is easy to represent, or to compress into some code; a meaning is informative if it is easy for a listener to recover a speaker's intended literal meaning.

Examples:
>>> from ultk.language.semantics import Referent, Meaning, Universe
>>> from ultk.language.language import Expression
>>> # construct the meaning space for numerals
>>> numerals_universe = NumeralUniverse(referents=[NumeralReferent(str(i)) for i in range(1, 100)])
>>> # construct a list of referents for the expression 'a few'
>>> a_few_refs = [NumeralReferent(name=str(i)) for i in range(2, 6)]
>>> a_few_meaning = NumeralMeaning(referents=a_few_refs, universe=numerals_universe)
>>> # define the expression
>>> a_few = NumeralExpression(form="a few", meaning=a_few_meaning)
  1"""Classes for modeling the meanings of a language.
  2
  3    Meanings are modeled as things which map linguistic forms to objects of reference. The linguistic forms and objects of reference can in principle be very detailed, and future work may elaborate the meaning classes and implement a Form class.
  4
  5    In efficient communication analyses, simplicity and informativeness can be measured as properties of semantic aspects of a language. E.g., a meaning is simple if it is easy to represent, or to compress into some code; a meaning is informative if it is easy for a listener to recover a speaker's intended literal meaning.
  6
  7    Examples:
  8
  9        >>> from ultk.language.semantics import Referent, Meaning, Universe
 10        >>> from ultk.language.language import Expression
 11        >>> # construct the meaning space for numerals
 12        >>> numerals_universe = NumeralUniverse(referents=[NumeralReferent(str(i)) for i in range(1, 100)])
 13        >>> # construct a list of referents for the expression 'a few'
 14        >>> a_few_refs = [NumeralReferent(name=str(i)) for i in range(2, 6)]
 15        >>> a_few_meaning = NumeralMeaning(referents=a_few_refs, universe=numerals_universe)
 16        >>> # define the expression
 17        >>> a_few = NumeralExpression(form="a few", meaning=a_few_meaning)
 18"""
 19
 20from dataclasses import dataclass
 21from functools import cached_property
 22from typing import Any, Generic, TypeVar, Union
 23from ultk.util.frozendict import FrozenDict
 24
 25import numpy as np
 26import pandas as pd
 27
 28T = TypeVar("T")
 29
 30
 31class Referent:
 32    """A referent is some object in the universe for a language.
 33
 34    Conceptually, a Referent can be any kind of object.  This functions like a generic python object that is _immutable_ after initialization.
 35    At initialization, properties can be specified either by passing a dictionary or by keyword arguments.
 36    """
 37
 38    def __init__(self, name: str, properties: dict[str, Any] = {}, **kwargs) -> None:
 39        """Initialize a referent.
 40
 41        Args:
 42            name: a string representing the name of the referent
 43        """
 44        self.name = name
 45        self.__dict__.update(properties, **kwargs)
 46        self._frozen = True
 47
 48    def __setattr__(self, __name: str, __value: Any) -> None:
 49        if hasattr(self, "_frozen") and self._frozen:
 50            raise AttributeError("Referents are immutable.")
 51        else:
 52            object.__setattr__(self, __name, __value)
 53
 54    def __str__(self) -> str:
 55        return str(self.__dict__)
 56
 57    def __lt__(self, other):
 58        return self.name < other.name
 59
 60    def __eq__(self, other) -> bool:
 61        return self.name == other.name and self.__dict__ == other.__dict__
 62
 63    def __hash__(self) -> int:
 64        return hash((self.name, frozenset(self.__dict__.items())))
 65
 66    def __repr__(self) -> str:
 67        return f"Referent({self.name}, {self.__dict__})"
 68
 69
 70@dataclass(frozen=True)
 71class Universe:
 72    """The universe is the collection of possible referent objects for a meaning."""
 73
 74    referents: tuple[Referent, ...]
 75    prior: tuple[float, ...]
 76
 77    def __init__(self, referents, prior=None):
 78        # use of __setattr__ is to work around the issues with @dataclass(frozen=True)
 79        object.__setattr__(self, "referents", referents)
 80        # When only referents are passed in, make the priors a unifrom distribution
 81        object.__setattr__(
 82            self, "prior", prior or tuple(1 / len(referents) for _ in referents)
 83        )
 84
 85    @cached_property
 86    def _referents_by_name(self):
 87        return {referent.name: referent for referent in self.referents}
 88
 89    @cached_property
 90    def size(self):
 91        return len(self.referents)
 92
 93    @cached_property
 94    def prior_numpy(self) -> np.ndarray:
 95        return np.array(self.prior)
 96
 97    def __getitem__(self, key: Union[str, int]) -> Referent:
 98        if type(key) is str:
 99            return self._referents_by_name[key]
100        elif type(key) is int:
101            return self.referents[key]
102        else:
103            raise KeyError("Key must either be an int or str.")
104
105    def __str__(self):
106        referents_str = ",\n\t".join([str(point) for point in self.referents])
107        return f"Points:\n\t{referents_str}\nDistribution:\n\t{self.prior}"
108
109    def __len__(self) -> int:
110        return len(self.referents)
111
112    @classmethod
113    def from_dataframe(cls, df: pd.DataFrame):
114        """Build a Universe from a DataFrame.
115        It's assumed that each row specifies one Referent, and each column will be a property
116        of that Referent.  We assume that `name` is one of the columns of the DataFrame.
117
118        Args:
119            a DataFrame representing the meaning space of interest, assumed to have a column `name`
120        """
121        records = df.to_dict("records")
122        referents = tuple(Referent(record["name"], record) for record in records)
123        default_prob = 1 / len(referents)
124        # prior = FrozenDict({ referent: getattr(referent, "probability", default_prob) for referent in referents })
125        prior = tuple(
126            getattr(referent, "probability", default_prob) for referent in referents
127        )
128        return cls(referents, prior)
129
130    @classmethod
131    def from_csv(cls, filename: str):
132        """Build a Universe from a CSV file.  This is a small wrapper around
133        `Universe.from_dataframe`, so see that documentation for more information.
134        """
135        df = pd.read_csv(filename)
136        return cls.from_dataframe(df)
137
138
139@dataclass(frozen=True)
140class Meaning(Generic[T]):
141    """A meaning maps Referents to any type of object.
142
143    For instance, sentence meanings are often modeled as sets of points (e.g. possible worlds).
144    These correspond to mappings from points (i.e. Referents) to truth values, corresponding to the characteristic function of a set.
145    But, in general, meanings can have a different output type for, e.g. sub-sentential meanings..
146
147    Properties:
148        mapping: a `FrozenDict` with `Referent` keys, but arbitrary type `T` as values.
149
150        universe: a Universe object.  The `Referent`s in the keys of `mapping` are expected to be exactly those in `universe`.
151
152        _dist: a mapping representing a probability distribution over referents to associate with the meaning. By default, will be assumed to be uniform over the "true-like" `Referent`s in `mapping` (see `.dist`).
153    """
154
155    mapping: FrozenDict[Referent, T]
156    # With the mapping, `universe` is not conceptually needed, but it is very useful to have it lying around.
157    # `universe` should be the keys to `mapping`.
158    universe: Universe
159    # _dist: FrozenDict[Referent, float] = FrozenDict({})
160    _dist = False  # TODO: clean up
161
162    @property
163    def dist(self) -> FrozenDict[Referent, float]:
164        if self._dist:
165            # normalize weights to distribution
166            total_weight = sum(self._dist.values())
167            return FrozenDict(
168                {
169                    referent: weight / total_weight
170                    for referent, weight in self._dist.items()
171                }
172            )
173        else:
174            num_true_like = sum(1 for value in self.mapping.values() if value)
175            if num_true_like == 0:
176                raise ValueError("Meaning must have at least one true-like referent.")
177            return FrozenDict(
178                {
179                    referent: (1 / num_true_like if self.mapping[referent] else 0)
180                    for referent in self.mapping
181                }
182            )
183
184    def is_uniformly_false(self) -> bool:
185        """Return True if all referents in the meaning are mapped to False (or coercible to False).In the case where the meaning type is boolean, this corresponds to the characteristic function of the empty set."""
186        return all(not value for value in self.mapping.values())
187
188    def __getitem__(self, key: Referent) -> T:
189        return self.mapping[key]
190
191    def __iter__(self):
192        """Iterate over the referents in the meaning."""
193        return iter(self.mapping)
194
195    def __bool__(self):
196        return bool(self.mapping)  # and bool(self.universe)
197
198    def __str__(self):
199        return "Mapping:\n\t{0}".format(
200            "\n".join(f"{ref}: {self.mapping[ref]}" for ref in self.mapping)
201        )  # \ \nDistribution:\n\t{self.dist}\n"
class Referent:
32class Referent:
33    """A referent is some object in the universe for a language.
34
35    Conceptually, a Referent can be any kind of object.  This functions like a generic python object that is _immutable_ after initialization.
36    At initialization, properties can be specified either by passing a dictionary or by keyword arguments.
37    """
38
39    def __init__(self, name: str, properties: dict[str, Any] = {}, **kwargs) -> None:
40        """Initialize a referent.
41
42        Args:
43            name: a string representing the name of the referent
44        """
45        self.name = name
46        self.__dict__.update(properties, **kwargs)
47        self._frozen = True
48
49    def __setattr__(self, __name: str, __value: Any) -> None:
50        if hasattr(self, "_frozen") and self._frozen:
51            raise AttributeError("Referents are immutable.")
52        else:
53            object.__setattr__(self, __name, __value)
54
55    def __str__(self) -> str:
56        return str(self.__dict__)
57
58    def __lt__(self, other):
59        return self.name < other.name
60
61    def __eq__(self, other) -> bool:
62        return self.name == other.name and self.__dict__ == other.__dict__
63
64    def __hash__(self) -> int:
65        return hash((self.name, frozenset(self.__dict__.items())))
66
67    def __repr__(self) -> str:
68        return f"Referent({self.name}, {self.__dict__})"

A referent is some object in the universe for a language.

Conceptually, a Referent can be any kind of object. This functions like a generic python object that is _immutable_ after initialization. At initialization, properties can be specified either by passing a dictionary or by keyword arguments.

Referent(name: str, properties: dict[str, typing.Any] = {}, **kwargs)
39    def __init__(self, name: str, properties: dict[str, Any] = {}, **kwargs) -> None:
40        """Initialize a referent.
41
42        Args:
43            name: a string representing the name of the referent
44        """
45        self.name = name
46        self.__dict__.update(properties, **kwargs)
47        self._frozen = True

Initialize a referent.

Arguments:
  • name: a string representing the name of the referent
name
@dataclass(frozen=True)
class Universe:
 71@dataclass(frozen=True)
 72class Universe:
 73    """The universe is the collection of possible referent objects for a meaning."""
 74
 75    referents: tuple[Referent, ...]
 76    prior: tuple[float, ...]
 77
 78    def __init__(self, referents, prior=None):
 79        # use of __setattr__ is to work around the issues with @dataclass(frozen=True)
 80        object.__setattr__(self, "referents", referents)
 81        # When only referents are passed in, make the priors a unifrom distribution
 82        object.__setattr__(
 83            self, "prior", prior or tuple(1 / len(referents) for _ in referents)
 84        )
 85
 86    @cached_property
 87    def _referents_by_name(self):
 88        return {referent.name: referent for referent in self.referents}
 89
 90    @cached_property
 91    def size(self):
 92        return len(self.referents)
 93
 94    @cached_property
 95    def prior_numpy(self) -> np.ndarray:
 96        return np.array(self.prior)
 97
 98    def __getitem__(self, key: Union[str, int]) -> Referent:
 99        if type(key) is str:
100            return self._referents_by_name[key]
101        elif type(key) is int:
102            return self.referents[key]
103        else:
104            raise KeyError("Key must either be an int or str.")
105
106    def __str__(self):
107        referents_str = ",\n\t".join([str(point) for point in self.referents])
108        return f"Points:\n\t{referents_str}\nDistribution:\n\t{self.prior}"
109
110    def __len__(self) -> int:
111        return len(self.referents)
112
113    @classmethod
114    def from_dataframe(cls, df: pd.DataFrame):
115        """Build a Universe from a DataFrame.
116        It's assumed that each row specifies one Referent, and each column will be a property
117        of that Referent.  We assume that `name` is one of the columns of the DataFrame.
118
119        Args:
120            a DataFrame representing the meaning space of interest, assumed to have a column `name`
121        """
122        records = df.to_dict("records")
123        referents = tuple(Referent(record["name"], record) for record in records)
124        default_prob = 1 / len(referents)
125        # prior = FrozenDict({ referent: getattr(referent, "probability", default_prob) for referent in referents })
126        prior = tuple(
127            getattr(referent, "probability", default_prob) for referent in referents
128        )
129        return cls(referents, prior)
130
131    @classmethod
132    def from_csv(cls, filename: str):
133        """Build a Universe from a CSV file.  This is a small wrapper around
134        `Universe.from_dataframe`, so see that documentation for more information.
135        """
136        df = pd.read_csv(filename)
137        return cls.from_dataframe(df)

The universe is the collection of possible referent objects for a meaning.

Universe(referents, prior=None)
78    def __init__(self, referents, prior=None):
79        # use of __setattr__ is to work around the issues with @dataclass(frozen=True)
80        object.__setattr__(self, "referents", referents)
81        # When only referents are passed in, make the priors a unifrom distribution
82        object.__setattr__(
83            self, "prior", prior or tuple(1 / len(referents) for _ in referents)
84        )
referents: tuple[Referent, ...]
prior: tuple[float, ...]
size
90    @cached_property
91    def size(self):
92        return len(self.referents)
prior_numpy: numpy.ndarray
94    @cached_property
95    def prior_numpy(self) -> np.ndarray:
96        return np.array(self.prior)
@classmethod
def from_dataframe(cls, df: pandas.core.frame.DataFrame):
113    @classmethod
114    def from_dataframe(cls, df: pd.DataFrame):
115        """Build a Universe from a DataFrame.
116        It's assumed that each row specifies one Referent, and each column will be a property
117        of that Referent.  We assume that `name` is one of the columns of the DataFrame.
118
119        Args:
120            a DataFrame representing the meaning space of interest, assumed to have a column `name`
121        """
122        records = df.to_dict("records")
123        referents = tuple(Referent(record["name"], record) for record in records)
124        default_prob = 1 / len(referents)
125        # prior = FrozenDict({ referent: getattr(referent, "probability", default_prob) for referent in referents })
126        prior = tuple(
127            getattr(referent, "probability", default_prob) for referent in referents
128        )
129        return cls(referents, prior)

Build a Universe from a DataFrame. It's assumed that each row specifies one Referent, and each column will be a property of that Referent. We assume that name is one of the columns of the DataFrame.

Arguments:
  • a DataFrame representing the meaning space of interest, assumed to have a column name
@classmethod
def from_csv(cls, filename: str):
131    @classmethod
132    def from_csv(cls, filename: str):
133        """Build a Universe from a CSV file.  This is a small wrapper around
134        `Universe.from_dataframe`, so see that documentation for more information.
135        """
136        df = pd.read_csv(filename)
137        return cls.from_dataframe(df)

Build a Universe from a CSV file. This is a small wrapper around Universe.from_dataframe, so see that documentation for more information.

@dataclass(frozen=True)
class Meaning(typing.Generic[~T]):
140@dataclass(frozen=True)
141class Meaning(Generic[T]):
142    """A meaning maps Referents to any type of object.
143
144    For instance, sentence meanings are often modeled as sets of points (e.g. possible worlds).
145    These correspond to mappings from points (i.e. Referents) to truth values, corresponding to the characteristic function of a set.
146    But, in general, meanings can have a different output type for, e.g. sub-sentential meanings..
147
148    Properties:
149        mapping: a `FrozenDict` with `Referent` keys, but arbitrary type `T` as values.
150
151        universe: a Universe object.  The `Referent`s in the keys of `mapping` are expected to be exactly those in `universe`.
152
153        _dist: a mapping representing a probability distribution over referents to associate with the meaning. By default, will be assumed to be uniform over the "true-like" `Referent`s in `mapping` (see `.dist`).
154    """
155
156    mapping: FrozenDict[Referent, T]
157    # With the mapping, `universe` is not conceptually needed, but it is very useful to have it lying around.
158    # `universe` should be the keys to `mapping`.
159    universe: Universe
160    # _dist: FrozenDict[Referent, float] = FrozenDict({})
161    _dist = False  # TODO: clean up
162
163    @property
164    def dist(self) -> FrozenDict[Referent, float]:
165        if self._dist:
166            # normalize weights to distribution
167            total_weight = sum(self._dist.values())
168            return FrozenDict(
169                {
170                    referent: weight / total_weight
171                    for referent, weight in self._dist.items()
172                }
173            )
174        else:
175            num_true_like = sum(1 for value in self.mapping.values() if value)
176            if num_true_like == 0:
177                raise ValueError("Meaning must have at least one true-like referent.")
178            return FrozenDict(
179                {
180                    referent: (1 / num_true_like if self.mapping[referent] else 0)
181                    for referent in self.mapping
182                }
183            )
184
185    def is_uniformly_false(self) -> bool:
186        """Return True if all referents in the meaning are mapped to False (or coercible to False).In the case where the meaning type is boolean, this corresponds to the characteristic function of the empty set."""
187        return all(not value for value in self.mapping.values())
188
189    def __getitem__(self, key: Referent) -> T:
190        return self.mapping[key]
191
192    def __iter__(self):
193        """Iterate over the referents in the meaning."""
194        return iter(self.mapping)
195
196    def __bool__(self):
197        return bool(self.mapping)  # and bool(self.universe)
198
199    def __str__(self):
200        return "Mapping:\n\t{0}".format(
201            "\n".join(f"{ref}: {self.mapping[ref]}" for ref in self.mapping)
202        )  # \ \nDistribution:\n\t{self.dist}\n"

A meaning maps Referents to any type of object.

For instance, sentence meanings are often modeled as sets of points (e.g. possible worlds). These correspond to mappings from points (i.e. Referents) to truth values, corresponding to the characteristic function of a set. But, in general, meanings can have a different output type for, e.g. sub-sentential meanings..

Properties:

mapping: a FrozenDict with Referent keys, but arbitrary type T as values.

universe: a Universe object. The Referents in the keys of mapping are expected to be exactly those in universe.

_dist: a mapping representing a probability distribution over referents to associate with the meaning. By default, will be assumed to be uniform over the "true-like" Referents in mapping (see .dist).

Meaning( mapping: ultk.util.frozendict.FrozenDict[Referent, ~T], universe: Universe)
universe: Universe
dist: ultk.util.frozendict.FrozenDict[Referent, float]
163    @property
164    def dist(self) -> FrozenDict[Referent, float]:
165        if self._dist:
166            # normalize weights to distribution
167            total_weight = sum(self._dist.values())
168            return FrozenDict(
169                {
170                    referent: weight / total_weight
171                    for referent, weight in self._dist.items()
172                }
173            )
174        else:
175            num_true_like = sum(1 for value in self.mapping.values() if value)
176            if num_true_like == 0:
177                raise ValueError("Meaning must have at least one true-like referent.")
178            return FrozenDict(
179                {
180                    referent: (1 / num_true_like if self.mapping[referent] else 0)
181                    for referent in self.mapping
182                }
183            )
def is_uniformly_false(self) -> bool:
185    def is_uniformly_false(self) -> bool:
186        """Return True if all referents in the meaning are mapped to False (or coercible to False).In the case where the meaning type is boolean, this corresponds to the characteristic function of the empty set."""
187        return all(not value for value in self.mapping.values())

Return True if all referents in the meaning are mapped to False (or coercible to False).In the case where the meaning type is boolean, this corresponds to the characteristic function of the empty set.