Toy Example of MAS with Creamas =============================== `(full code) `_ .. warning:: (Sat 12.11. 10.04) There was a mistake in the example codes considering :meth:`evaluate` function and the number of elements it should return. There are now two full code examples ``toy_mas.py`` and ``toy_mas2.py`` in the repository. The version that works for you is depended on the version of Creamas: if you have Creamas 0.1.1 then ``toy_mas.py`` should work, and if you have Creamas 0.1.0 then ``toy_mas2.py`` should work. You can check which version you currently have by typing ``pip freeze`` in the terminal while your virtual environment is active. The easiest way to get everything working is upgrading the Creamas and following this example (and the full code ``toy_mas.py``). You can upgrade you Creamas 0.1.0 to 0.1.1 by executing ``pip install --upgrade creamas`` in the terminal. If you are installing Creamas for the first time, then ``pip install creamas`` should automatically install you the latest version (0.1.1). This example is currently written for Creamas 0.1.1, however there is very little that is changed between the two example code version. The differences are in how many elements :meth:`evaluate` returns. With creamas==0.1.0, the evaluation function should return **one** element. With creamas==0.1.1, the evaluation function should return **two** elements. Changing the :meth:`evaluate` also forces some refactoring in :meth:`invent` to reflect these changes. In this example we will create a simple multi-agent system with `Creamas `_. Creamas uses vocabulary typical to MAS in CC: agents, environments, and artifacts. You can find an overview of the library (which should be enough for this example) from the documentation, but here are main points: #. The focus of the agents in the Creamas is on artifact creation. #. Environment is used for establishing communication route between agents. (You can also subclass it to have some additional responsibilities.) #. Artifacts are light wrappers for (any kind of) objects, where you can store some metadata about the artifact, e.g. its creator, evaluations and framings. .. note:: Creamas should be installed in your virtual environment. If you get any import errors while running the code, use ``pip install creamas==0.1.1``. Parsing the Vocabulary ---------------------- Our goal is to make agents which generate (random) words, and evaluate them based on their vocabulary. In this example all the agents will have the same vocabulary, which is generally not desirable, but will do for the sake of the example. To generate the vocabulary, we will define two straightforward parsing functions: .. autofunction:: toy_mas.parse_words .. autofunction:: toy_mas.frequent_words And here is the actual code for both of the functions (docstrings stripped):: def parse_words(self, filename, encoding, word_pattern, wlen_limits): # Filter function to define which words are accepted. def is_word(w): if not word_pattern.match(w.lower()): return False if not len(w) >= self.wlen_limits[0]: return False if not len(w) <= self.wlen_limits[1]: return False return True with open(filename, 'r', encoding=encoding) as f: content = f.read() words = content.split() ret = [w.lower() for w in words if is_word(w)] return ret def frequent_words(self, filename, encoding, word_pattern, wlen_limits, n=20): words = parse_words(filename, encoding, word_pattern, wlen_limits) # Count the number of times each element appears in the list ctr = Counter(words) common = ctr.most_common(n) # return only the words, not their counts return [e[0] for e in common] We will use these two parsing functions in the next section where we actually implement our agents. Creating an Agent Class ----------------------- Now we create a new agent class by subclassing the base class `CreativeAgent `_ from Creamas. ``CreativeAgent`` has a lot of predefined attributes, which we will not touch in here. We will also create the initialization function :func:`__init__`. The initialization function has to call its superclass' initialization function so that all required attributes get initialized. In our initialization function, we will define the acceptable words as a compiled regex pattern and word limits, and then "learn" the vocabulary from a given file. Here is the documentation for our :class:`ToyAgent` (``env`` is the environment for the agent, which we will define later in the example): .. autoclass:: toy_mas.ToyAgent And here is the code (docstrings stripped)e:: class ToyAgent(CreativeAgent): def __init__(self, env, filename, encoding='utf8', n=20, wlen_limits=(2,11), chars='abcdefghijklmnopqrstuvwxyz'): super().__init__(env) self.n = n self.chars = chars self.wlen_limits = wlen_limits self.word_pattern = re.compile(r'^\w+$') self.vocab = frequent_words(filename, encoding=encoding, word_pattern=self.word_pattern, wlen_limits=self.wlen_limits, n=20) Generating words ---------------- As our aim is for a creative (well, at least generative) agent, we need a function which generates us some words (or character strings). In this example we will settle on generating random character strings from the ``chars`` given at agent's initialization time. To this end, our ``generate`` function is very simple (N.B. this is actually a method inside the class ``ToyAgent``, hence the ``self`` parameter):: def generate(self): '''Generate a new word. Word is generated by uniformly drawing from ``chars``. Word length is in ``wlen_limits``. :returns: a word wrapped as :class:`~creamas.core.artifact.Artifact` ''' word_length = random.randint(*self.wlen_limits) word = [random.choice(self.chars) for _ in range(word_length)] word = ''.join(word) return Artifact(self, word, domain=str) Here it is important to see that the generated word is wrapped as `Artifact `_, which can hold some metadata considering the object. We will include some metadata in the following sections, but for now it should be enough to know, that the original object (in this case the string) can be accessed as ``obj`` attribute of the artifact, i.e.:: >>> artifact = Artifact(self, 'blaa', domain=str) >>> artifact.obj 'blaa' The domain of the artifact is not relevant at this point. (It has to do with automatic feature evaluation based on the rules the agent has, but currently the functionality is not properly implemented.) .. note:: Agents in Creamas should always pass each other ``Artifact`` instances (or subclasses of it), for all the library's functionalities to work. Evaluating Words ---------------- Our agent is now capable of generating some random character strings, but has no notion of value (or novelty or surprisingness). To evaluate our strings, we will use the ``vocab`` which was learned from a file when the agent was created, and `Levenshtein distance `_. For the sake of our purposes, it is sufficient to know that the Levenshtein distance computes the minimum number of edit operations by which one of the strings can be changed to another. Here is our evaluation function's documentation: .. automethod:: toy_mas.ToyAgent.evaluate And its code (the code for the Levenshtein distance computation can be found from the full code example, but it is not included here for clarity):: def evaluate(self, artifact): evaluation = 0.0 evaluation_word = artifact.obj matching_word = self.vocab[0] for word in self.vocab: lev = levenshtein(evaluation_word, word) mlen = max(len(evaluation_word), float(len(word))) current_evaluation = 1.0 - (float(lev) / mlen) if current_evaluation > evaluation: evaluation = current_evaluation matching_word = word return evaluation, matching_word .. note:: It is important for the :meth:`evaluate` method to return two elements because of how the Creamas is built. The another element is so called framing for the evaluation, but it can be left blank. If you do not need the another element, put in its place ``None``. Inventing New Words ------------------- With generation and evaluation of words in place, we can move to inventing new words. In this example we will simply try to create a number of words, and select the one with the highest evaluation. However, it could be justified to, e.g. start from a previous highly evaluated example and alter it and keep a list of already invented words so that we do not invent the same words repeatedly. Our ``invent`` function is very simple loop:: def invent(self, n=20): best_artifact = self.generate() max_evaluation, match_word = self.evaluate(best_artifact) for _ in range(n-1): artifact = self.generate() evaluation, m_word = self.evaluate(artifact) if evaluation > max_evaluation: best_artifact = artifact max_evaluation = evaluation match_word = m_word # Add evaluation and framing to the artifact best_artifact.add_eval(self, max_evaluation, fr={'match' : match_word}) return best_artifact Here, you can see that we add the evaluation and so called 'framing' to the artifact after we have selected the best artifact. The framing is something that was/is taken into consideration when creating/observing the artifact. Our framing is simply the word that created the best evaluation for our best artifact. Now our agent is almost complete, however, as we want it to interact with other agents, we will make some additions. Voting ------ Our interaction model between the agents is a voting procedure. We run the system in an iterative simulation where on each iteration: #. Each agent produces a number of artifacts and selects the best one #. Each agent adds its best artifact to candidate artifacts #. Agents vote the winner from the candidates .. note:: As all the agents in our example will have the same words (if given the same file in initialization) in their vocabulary, it means that all agents have the same evaluation formula. We will use this naive model just for the example. To run the simulation we will use `Simulation `_ class. All agents that run inside the simulation must implement ``act`` function. For our agents, we will simply call ``invent`` on every act and then add the invented word to candidates of the environment:: async def act(self): artifact = self.invent(self.n) self.env.add_candidate(artifact) .. note:: The ``async`` keyword before the function definition is of key importance here. It tells Python that this function is supposed to be called inside an event loop. We will not go very deeply into the asynchronicity during the course, but if your code complains something about asyncio or event loop, you probably have some kind of async-problems. Next, we will define a subclass of `Environment `_ where we define only one function ``vote``, which is used as a callback on every simulation step:: class ToyEnvironment(Environment): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def vote(self, age): artifacts = self.perform_voting(method='mean') if len(artifacts) > 0: accepted = artifacts[0][0] value = artifacts[0][1] logger.info("Vote winner by {}: {} (val={})" .format(accepted.creator, accepted.obj, value)) else: logger.info("No vote winner!") self.clear_candidates() The ``vote`` function performs the voting with a selected method (voting rule). Here the method is to select the candidate with the best mean evaluation across all the agents. Then we simply announce the winner for this iteration. It is important to clear the candidates after the vote, so that we do not vote repeatedly for same artifacts. The voting functionality is done "under the hood" by some methods inherited from :class:`Environment` and :class:`CreativeAgent`. However, it uses the ``evaluate`` function which we defined for the agents (that's why it is important that it returns two values!). You can check what :meth:`perform_voting` does from `Creamas documentation `_. Running the Simulation ---------------------- Lastly, we will run our agents in the iterative simulation. Here, we will initialize all agents with the same parameters and run it for a short time:: if __name__ == "__main__": filename = '../week1/alice.txt' env = ToyEnvironment.create(('localhost', 5555)) for i in range(10): agent = ToyAgent(env, filename=filename) sim = Simulation(env, log_folder='logs', callback=env.vote) sim.async_steps(10) sim.end() We create environment using its ``create`` function, which needs the address for it, and then create 10 agents into that environment. Then we initialize our simulation with the environment, and set the callback (which is called after each iteration) to our environment's ``vote`` function. Lastly, we run the simulation for 10 steps and end it. .. note:: If you do not remember to call ``end`` after finishing the simulation, you will have some nasty exceptions because the event loop is still running! That's it! Now we have implemented a simple multi-agent system