Agentes en Python


Agentes en Python

El recién creado grupo de Telegram https://telegram.me/PyDataVE se creó a partir del interés en el tema de inteligencia artificial, aunque es afín a la misión de la comunidad PyData de ofrecer un espacio de intercambio alrededor de la Ciencia de Datos.

Desde el punto de vista del libro mas utilizado en el planeta para la enseñanza de Inteligencia Artificial "Artificial Intelligence: A Modern Approach" (AIMA) de Russell y Norvig http://aima.cs.berkeley.edu/ quizás el concepto más básico es el de agente.

En este libro se definen los agentes como:

Un agente es cualquier cosa capaz de percibir su entorno a través de sensores (que reciben perceptos) y realizar acciones sobre este entorno mediante actuadores. El comportamiento del agente viene dado por la función del agente que proyecta una secuencia de perceptos en una acción.

Precisamente esta función del agente $f : P^\ast \rightarrow A$ es la representación de la inteligencia, lo que puede permitir al agente comportarse de un modo racional (o no). Es decir, intentar alcanzar el mejor resultado de acuerdo a una medida de rendimiento.

aima-python

En el repositorio "oficial" de python del libro AIMA https://github.com/aimacode/aima-python/blob/master/agents.py codifican del siguiente modo a los agentes. Dado que los agentes son un tipo de cosas con sensores y actuadores, empiezan por definir cosa.

In [1]:
class Thing():
    """This represents any physical object that can appear in an Environment.
    You subclass Thing to get the things you want.  Each thing can have a
    .__name__  slot (used for output only)."""

    def __repr__(self):
        return '<{}>'.format(getattr(self, '__name__', self.__class__.__name__))

    def is_alive(self):
        "Things that are 'alive' should return true."
        return hasattr(self, 'alive') and self.alive

    def show_state(self):
        "Display the agent's internal state.  Subclasses should override."
        print("I don't know how to show_state.")

    def display(self, canvas, x, y, width, height):
        # Do we need this?
        "Display an image of this Thing on the canvas."
        pass

La clase Thing es la definición general (clase abstracta) de una cosa a la que podríamos consultar si está viva o no usando is_alive(), puede tener un estado que mostraría mediante show_state(), y que para fines didácticos podría representarse gráficamente mediante display().

Por su parte, definen al agente de la siguiente manera:

In [2]:
class Agent(Thing):

    """An Agent is a subclass of Thing with one required slot,
    .program, which should hold a function that takes one argument, the
    percept, and returns an action. (What counts as a percept or action
    will depend on the specific environment in which the agent exists.)
    Note that 'program' is a slot, not a method.  If it were a method,
    then the program could 'cheat' and look at aspects of the agent.
    It's not supposed to do that: the program can only look at the
    percepts.  An agent program that needs a model of the world (and of
    the agent itself) will have to build and maintain its own model.
    There is an optional slot, .performance, which is a number giving
    the performance measure of the agent in its environment."""

    def __init__(self, program=None):
        self.alive = True
        self.bump = False
        self.holding = []
        self.performance = 0
        if program is None:
            def program(percept):
                return eval(input('Percept={}; action? ' .format(percept)))
        assert isinstance(program, collections.Callable)
        self.program = program

    def can_grab(self, thing):
        """Returns True if this agent can grab this thing.
        Override for appropriate subclasses of Agent and Thing."""
        return False

Así, mediante herencia se establece que un agente es un tipo de cosa que está viva self.alive = True y que no es un simple obstáculo self.bump = False. El agente puede recolectar cosas mediante can_grab(thing) y las almacenaría en self.holding = [], y tiene una medida de rendimiento interna self. performance.

Finalmente, contiene la colección descrita en program que recibe perceptos y devuelve acciones.

Ambas son definiciones abstractas que se utilizan en los ejemplos que están en el mismo repositorio y que se revisarán con detenimiento en otros artículos.

Modelo de Schelling

Un ejemplo clásico de simulación de agentes es el Modelo Dinámico de Segregación de Thomas Schelling descrito en el artículo de 1971 "Dynamic Models of Segregation" [Journal of Mathematical Sociology. 1 (2): 143–186. doi:10.1080/0022250x.1971.9989794], donde se representa una simulación de segregación racial en la que a partir de una definición simple del comportamiento individual de los agentes es posible observar como emergen comportamientos colectivos. Estas ideas luego son generalizadas en su libro seminal de 1978 "Micromotives and Macrobehavior" http://books.wwnorton.com/books/978-0-393-32946-9/. Vale la pena señalar que Thomas Schelling fue ganador del Premio Nobel de Economía en el 2005.

El modelo de Schelling del mundo es una malla; cada celda representa una casa. Las casas están ocupadas por dos clases de personas, con la etiqueta de color rojo y azul, en aproximadamente el mismo número. Una pequeña proporción de las casas están vacías. En cualquier momento, un agente puede ser feliz o infeliz, dependiendo de los otros agentes en el vecindario. La vecindad de cada casa es el conjunto de ocho células adyacentes. En una versión del modelo, los agentes son felices si tienen al menos dos vecinos de su mismo tipo, e infeliz si tienen uno o cero.

La simulación pasa por la elección de un agente al azar y la comprobación para ver si es feliz. Si es así, no pasa nada; si no, el agente elige una de las celdas desocupadas al azar y se muda.

Se mostrarán a continuación dos definiciones de agentes utilizadas para simular el modelo de Schelling como ejemplos concretos y alternativos de implantación de agentes.

Mesa

Mesa https://github.com/projectmesa/mesa es un framework en Python para el desarrollo de simulaciones basadas en agentes que espera convertirse en una alternativa de las bibliotecas mas utilizadas en simulación multiagente que están desarrolladas en Java. La representación abstracta de un agente en Mesa es muy simple:

In [3]:
class Agent:
    """ Base class for a model agent. """
    def __init__(self, unique_id, model):
        """ Create a new agent. """
        self.unique_id = unique_id
        self.model = model

    def step(self):
        """ A single step of the agent. """
        pass

En este caso se representa como una entidad con una identificación única unique_id, el agente está asociado a un modelo model, y ejecuta pasos step(). La función step() no se refiere en este caso a las acciones que realiza el agente en un paso de la simulación, y no tanto al movimiento del agente.

A partir de está definición se define un agente para el modelo de Schelling https://github.com/projectmesa/mesa/blob/master/examples/Schelling/model.py:

In [4]:
class SchellingAgent(Agent):
    '''
    Schelling segregation agent
    '''
    def __init__(self, pos, model, agent_type):
        '''
         Create a new Schelling agent.
         Args:
            unique_id: Unique identifier for the agent.
            x, y: Agent initial location.
            agent_type: Indicator for the agent's type (minority=1, majority=0)
        '''
        super().__init__(pos, model)
        self.pos = pos
        self.type = agent_type

    def step(self):
        similar = 0
        for neighbor in self.model.grid.neighbor_iter(self.pos):
            if neighbor.type == self.type:
                similar += 1

        # If unhappy, move:
        if similar < self.model.homophily:
            self.model.grid.move_to_empty(self)
        else:
            self.model.happy += 1

Nótese que en este caso el comportamiento reactivo del agente se especifica en el método step(), en cada paso de la simulación el agente verifica el número de vecinos model.grid.neighbor_iter(self.pos) de su mismo tipo y si no satisface la tolerancia del modelo model.homophily (que expresa la necesidad de estar rodeado de agentes del mismo tipo) el agente se mueve a una celda vacía.

El modelo hace referencia al entorno mencionado arriba, y la cantidad de vecinos y su tipo son perceptos que adquiere mediante un proceso sensorial implícito, esta información la convierte en la decisión de realizar la acción de mudarse o quedarse en el mismo lugar.

Think Complexity

Finalmente revisemos la implementación de un agente que hace Allen Downey para este mismo modelo en la 2da Edición de su libro "Think Complexity" https://github.com/AllenDowney/ThinkComplexity2/blob/master/code/chap09.ipynb:

In [5]:
class Agent:
    
    def __init__(self, loc, params):
        """Creates a new agent at the given location.
        
        loc: tuple coordinates
        params: dictionary of parameters
        """
        self.loc = tuple(loc)
        self.age = 0

        # extract the parameters
        max_vision = params.get('max_vision', 6)
        max_metabolism = params.get('max_metabolism', 4)
        min_lifespan = params.get('min_lifespan', 10000)
        max_lifespan = params.get('max_lifespan', 10000)
        min_sugar = params.get('min_sugar', 5)
        max_sugar = params.get('max_sugar', 25)
        
        # choose attributes
        self.vision = np.random.random_integers(max_vision)
        self.metabolism = np.random.uniform(1, max_metabolism)
        self.lifespan = np.random.uniform(min_lifespan, max_lifespan)
        self.sugar = np.random.uniform(min_sugar, max_sugar)

    def step(self, env):
        """Look around, move, and harvest.
        
        env: Sugarscape
        """
        self.loc = env.look_around(self.loc, self.vision)
        self.sugar += env.harvest(self.loc) - self.metabolism
        self.age += 1

    def is_starving(self):
        """Checks if sugar has gone negative."""
        return self.sugar < 0
    
    def is_old(self):
        """Checks if lifespan is exceeded."""
        return self.age > self.lifespan

Fíjense que en este caso se añaden detalles adicionales del estado del agente: edad age y localización loc (coordenadas de su posición en la malla). Aparte de su metabolismo metabolism (esto es, la velocidad con la que consume energía), su alcance visual vision, la duración de su vida lifespan, y la cantidad de azúcar sugar que contiene, como una medida genérica de energía. Los agentes que superan su límite de vida y se quedan sin azúzar son eliminados.

Esta es una variante de los modelos de Schelling denominado Sugarscape desarrollado por Epstein y Axtell en la que se estudia como los agentes son atraídos por fuentes de energía. No deja de ser curioso como estos modelos estudian el comportamiento económico colectivo haciendo una clara analogía con las hormigas.

En este caso, vision es un parámetro que determina el proceso sensorial con el que percibe el entorno a partir de su posición actual env.look_around(self.loc, self.vision), y en esta función es donde se toma la decisión hacia donde moverse En esta versión del modelo se mueve hacia la celda vacía dentro de su campo visual con la mayor cantidad de azúcar.

Estos dos últimos ejemplos de agentes sirven como demostraciones de casos concretos, son agentes reactivos con patrones de comportamiento muy simple. Ya sea la felicidad o el nivel de azúcar son medidas de rendimiento que determinan el comportamiento de los agentes.

Desplegar nikola en github


Importante seguir las instrucciones con atención, no vale la pena intentar ser originales :-D.

Para crear un despliegue de nikola en github como página principal seguir los siguientes pasos:

  1. Crear en tu cuenta de github un repositorio con nombre [usuario].github.io
  2. Clonar ese repositorio y crear un entorno virtual (python 3.4) para esa carpeta.
$ git clone https://github.com/[usuario]/[usuario].github.io.git
$ mkvirtualenv -p /usr/bin/python3 [usuario].github.io # Necesitas tener virtualenvwrapper
$ cd [usuario].github.io.git
  1. Crear una rama "sources" para la nueva instancia de nikola
$ touch README.md # necesitas al menos un commit
$ git add README.md
$ git commit REAME.md -m "Initial commit"
$ git branch sources # la rama con los fuentes de nikola
$ git checkout sources # ahora trabajamos sobre los fuentes
  1. En el nuevo entorno virtual y en la rama sources instalar nikola
$ pip install nikola
$ nikola init ./
  1. En conf.py que debe ahora estar en la raíz del repositorio, descomentar y editar las variables
GITHUB_SOURCE_BRANCH = 'sources'
GITHUB_DEPLOY_BRANCH = 'master'
  1. Finalmente añadir algo de contenido usando nikola new_post o nikola new_page, ¡ojo!, siempre en la rama sources

    Editar el post

$ git add posts/[nuevo_post].rst
$ git commit -a "Add [nuevo_post]"
$ nikola github_deploy

Puede ser conveniente crear el correspondiente .gitignore en la raíz del repositorio, con más o menos el siguiente contenido

.idea/
__pycache__/
output/
cache/
.doit.db

.idea para proyectos de pycharm, el resto son carpetas y archivos generados por nikola.