Acerca de:

Este blog contiene los códigos, ejemplos y bases de datos que he usado cuando aprendía acerca de algún tema específico. En lugar de borrarlos (una vez dominado ya el tema), he decidido publicarlos :)

domingo, 17 de enero de 2016

Implementación del Chatbot Eliza en Prolog

El algoritmo, que permite que Eliza genere una respuesta acorde a las consultas del usuario, se basa en comparar término a término la data ingresada versus una lista de templates con las respuestas predefinidas. Las ideas iniciales para desarrollar este algoritmo fueron obtenidas de [2] y de [3], a partir de estas fuentes se realizó una implementación propia.

El formato de cada template es el siguiente:

template([lista con las palabras del estímulo], [lista con las palabras de la respuesta de Eliza], [lista con las posiciones de las palabras clave en el estímulo empezando de cero]).

Un template de ejemplo tendrá la forma:



Las palabras clave le permiten a Eliza generar una coincidencia para diferentes listas de palabras, tales como “i am sad.” o “i am angry.”. Las palabras claves se reemplazarán en la respuesta en las posiciones donde hay números y no palabras.
 

La función que realiza la comparación es la función “match”. En el estímulo, las palabras clave están marcadas por predicados, mientras que el resto de los términos son atómicos. Cuando la función match detecta un dato no atómico, usando la función de SWI-Prolog “atom”, lo ignora y continúa con la comparación término a término entre la data ingresada y el estímulo. Si se encuentra un template que coincida, devolverá la respuesta correspondiente y la lista de índices. Los índices deben coincidir con las posiciones de los datos no atómicos en el estímulo del template.
 

La función “replace0” se encarga de reemplazar las palabras claves de la data ingresada en las posiciones donde la respuesta del template tiene números. Las palabras clave pueden ser una o más. Para saber qué palabra clave corresponde a qué posición en la respuesta, los números se van incrementando de uno en uno. Si no hay palabras para reemplazar, la lista de índices será una lista vacía.

Esta función llama a las siguientes funciones ya implementadas en SWI-Porlog:

nth0: devuelve el elemento de una lista en una posición dada.

select: reemplaza el valor de un elemento de una lista por otro en función a sus valores.

append: concatena dos listas. En este caso va concatenando las palabras de la respuesta, junto con la palabra reemplazada en la posición donde hay un número, palabra por palabra.
 

La función “replace0” también genera las respuestas a preguntas y consultas del tipo “do you like ...?”, “you are ...” y “do you (verb) ...?”. Los templates correspondientes tienen el siguiente formato:
 


Si la función “replace0” detecta una de las banderas (flags) configuradas, buscará la respuesta correspondiente en las funciones elizaDoes, elizaIs y ElizaLikes, las cuales evalúan los hechos que determinan si Eliza hace, es o le gusta algo. 

El código donde se declaran estos hechos es el siguiente:


El código que le permite al usuario ingresar sus consultas es el siguiente:


La función “readln” convierte las consultas del usuario en una lista. Luego el programa escogerá el primer template de la base del conocimiento, a continuación el estímulo de ese template, y la consulta del usuario (Input), son ingresados a la función “match”. Si el estímulo no coincide con la consulta, Prolog empezará el backtracking, escogiendo el siguiente template y repitiendo la operación. El último template de la base de conocimiento hace que la función “match” retorne verdadero si ninguno de los otros estímulos de los otros templates coincide con la consulta del usuario. Esto evita que se rompa el bucle de Eliza y finalice el programa de forma abrupta. A continuación se llama a la función “replace0”, para luego imprimir la respuesta en la pantalla. Eliza seguirá esperando las consultas del usuario mientras éste no ingrese “bye” o “bye.”.
 

II

Conversando con Eliza


Para interactuar con el programa Eliza, se debe ejecutar el intérprete SWI-Prolog, abrir el archvo eliza_complete.pl y compilarlo. En la consola de SWI-Prolog se deberá ingresar “eliza.” Eliza mostrará un mensaje de bienvenida y esperará por más consultas del usuario. Éstas deben estar en minúsculas y tener un punto al final.

Una conversación de ejemplo es la siguiente:



Eliza da las respuestas en forma de listas de palabras, separadas por comas y entre corchetes. Se mantuvo este formato porque en una aplicación Eliza real, es muy probable que las respuesta halladas requieran un procesamiento posterior.

La aplicación ha sido probada en SWI-Prolog versión 7.2.1. No se ha probado en versiones anteriores. No funcionará en versiones de SWI-Prolog que no tengan implementadas las funciones length, nth0, atom y select.

Esta implementación de Eliza no tiene memoria, es decir, no recuerda las consultas ingresadas y sus respuestas, por lo que la coherencia de la conversación depende del usuario. Por ejemplo, para Eliza es válido ingresar primero la consulta “because you are creepy.” Y luego “i hate you.”.
 

Para ampliar las respuestas de Eliza a más estímulos basta crear más templates con los formatos ya mencionados.


En este programa, la aparente inteligencia de Eliza está en función a la amplitud de su base de conocimientos. Como la coincidencia de las consultas del usuario y los estímulos se realiza término a término, la base de conocimiento de Eliza debe ser bastante grande para producir una conversación coherente, y poder respetar género, número, tiempos verbales, etc. Un programa más completo consideraría toda una lista de palabras clave en lugar de sólo una.

El código completo es (Escrito en Prolog y probado en Swi Prolog v7.2.):


% by: Y3l1nna_Broken_Window:
% la función match permite crear un output para varios inputs.

eliza:-	writeln('Hi, I am Eliza your chatbot,
	please enter your query,
	use only lowercases and a dot at the end:'),
	readln(Input),
	eliza(Input),!.
eliza(Input):- Input == ['bye'],
	writeln('Goodbye. I hope I have helped you.'), !.
eliza(Input):- Input == ['bye', '.'],
	writeln('Goodbye. I hope I have helped you.'), !.
eliza(Input) :-
	template(Stim, Resp, IndStim),
	match(Stim, Input),
	% si he llegado aquí es que he
	% hallado el template correcto:
	replace0(IndStim, Input, 0, Resp, R),
	writeln(R),
	readln(Input1),
	eliza(Input1), !.

% algunos saludos
% debido al backtracking, los templates más específicos deben ir antes
% de los más genéricos. Eso se nota acá:
template([hi, my, name, is, s(_), '.'], ['Hi', 0, 'How', are, you, '?'], [4]).
template([hello, my, name, is, s(_), '.'], ['Hello', 'How', are, you, 0, '?'], [4]).

template([hi, ',', my, name, is, s(_), '.'], ['Hi', 0, 'How', are, you, '?'], [5]).
template([hello, ',', my, name, is, s(_), '.'], ['Hello', 'How', are, you, 0, '?'], [5]).

template([hi, _], ['Hi', 'How', are, you, '?'], []).
template([hello, _], ['Hello', 'How', are, you, '?'], []).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

template([i, s(_), i, am, s(_),'.'], [why, do, you, 0, you, are, 1, '?'], [1, 4]).
template([i, s(_), you, '.'], [why, do, you, 0, me ,'?'], [1]).
template([i, am, s(_),'.'], [why, are, you, 0, '?'], [2]).

% pregunta algo que le gusta a eliza
template([do, you, like, s(_), _], [flagLike], [3]).
% pregunta algo que hace eliza
template([do, you, s(_), _], [flagDo], [2]).
% pregunta algo que es eliza
template([you, are, s(_)], [flagIs], [2]).
template([are, you, s(_), '?'], [flagIs], [2]).

template([how, are, you, '?'], [i, am, fine, ',', thanks, for, asking, '.'], []).

template([i, think, _], [well, that, is, just, your, opinion], []).
template([because, _], [that, is, not, a, good, reason, '.'], []).
template([i, have, s(_), with, s(_), '.'], ['You', have, to, deal, with, your, 0, and, your, 1, in, a, mature, way, '.'], [2, 4]).
template([i, s(_),  _], [i, can, recommend, you, a, book, about, that, issue], []).
template([please, s(_), _], ['No', i, can, not, help, ',', i, am, just, a, machine], []). % didn't passed the Turing Test
template([tell, me, a, s(_), _], ['No', i, can, not, ',', i, am, bad, at, that], []). % classic AI problem: una máquina aún no puede contar una historia coherente.

template(_, ['Please', explain, a, little, more, '.'], []). % cuando se ingresa algo que Eliza no tiene en sus templates, esto debe ir al final o eliza responderá a todo con esto.

% Lo que le gusta a eliza : flagLike
elizaLikes(X, R):- likes(X), R = ['Yeah', i, like, X].
elizaLikes(X, R):- \+likes(X), R = ['Nope', i, do, not, like, X].
likes(apples).
likes(ponies).
likes(zombies).

%%%%%%%%%%%%%%%%%%%%%%

% lo que hace eliza: flagDo
elizaDoes(X, R):- does(X), R = ['Yes', i, X, and, i, love, it].
elizaDoes(X, R):- \+does(X), R = ['No', i, do, not, X ,'.', it, is, too, hard, for, me].
does(study).
does(cook).
does(work).

%%%%%%%%%%%%%%%%%
% lo que es eliza: flagIs
elizaIs(X, R):- is0(X), R = ['Yes', i, am, X].
elizaIs(X, R):- \+is0(X), R = ['No', i, am, not, X].
is0(dumb).
is0(weird).
is0(nice).
is0(fine).
is0(happy).
is0(redundant).

%  http://stackoverflow.com/questions/12939425/prolog-access-specific-member-of-list
% magic happens here :D
% lo que hace match es comparar el input y el estímulo término a
% término, ignorando los s(X) en cada template. Si match falla en el
% loop de la función eliza, empezará el backtracking que permite
% recorrer los templates ingresados. match devolverá verdadero si haya
% un estímulo (primer término del template) que coincide con el Input, y
% devolverá la respuesta.
match([],[]).
match([], _):- true.

match([S|Stim],[I|Input]) :-
	atom(S), % si I es un s(X) devuelve falso
	S == I,
	match(Stim, Input),!.

match([S|Stim],[_|Input]) :-
% I es un s(X), lo ignoro y continúo con el resto de la lista
	\+atom(S),
	match(Stim, Input),!.

% esta función es para que se muestre bien en pantalla las respuestas de
% eliza. Además responde a lo que le gusta Eliza y a lo que hace, la
% función select reemplaza el número en la respuesta por la palabra
% clave hallada:
replace0([], _, _, Resp, R):- append(Resp, [], R),!.

% Eliza likes:
replace0([I|_], Input, _, Resp, R):-
	nth0(I, Input, Atom),
	nth0(0, Resp, X),
	X == flagLike,
	elizaLikes(Atom, R).

% Eliza does:
replace0([I|_], Input, _, Resp, R):-
	nth0(I, Input, Atom),
	nth0(0, Resp, X),
	X == flagDo,
	elizaDoes(Atom, R).

% Eliza is:
replace0([I|_], Input, _, Resp, R):-
	nth0(I, Input, Atom),
	nth0(0, Resp, X),
	X == flagIs,
	elizaIs(Atom, R).

replace0([I|Index], Input, N, Resp, R):-
	length(Index, M), M =:= 0,
	nth0(I, Input, Atom),
	select(N, Resp, Atom, R1), append(R1, [], R),!.

replace0([I|Index], Input, N, Resp, R):-
	nth0(I, Input, Atom),
	length(Index, M), M > 0,
	select(N, Resp, Atom, R1),
	N1 is N + 1,
	replace0(Index, Input, N1, R1, R),!.


sábado, 9 de enero de 2016

Listar los archivos en una carpeta y sus subcarpetas de forma recursiva v2

Esta es una versión mejorada de la función listdir_recurd:

import os
import sys
from os import listdir
from os.path import isfile, isdir, join


def listdir_recurd(files_list, root, folder, checked_folders):

    if (folder != root):
        checked_folders.append(folder)

    for f in listdir(folder):
        d = join(folder, f)       

        if isdir(d) and d not in checked_folders:
            listdir_recurd(files_list, root, d, checked_folders)
        else:
            if isfile(d):  # si no hago esto, inserta en la lista el nombre de las carpetas ignoradas
                files_list.append(join(folder, f))

    return files_list


Esta vez, la función devuelve las rutas completas de solamente los archivos que va encontrando, ignorando las carpetas y subcarpetas. Yo necesité esta función para borrar archivos de prueba que generaba otra aplicación, dejando sólo las carpetas vacías:

if __name__ == "__main__":
    # filez = listdir_recurd([], 'D:\test0', 'D:\test0', []) # esto lista todos los archivos
    # filez = listdir_recurd([], 'D:\test', 'D:\test', ['D:\\test\\t1', 'D:\\test\\t2']) # esto omite las carpetas 'D:\\test\\t1' y '
D:\\test\\t2'
    # filez = listdir_recurd([],
'D:\test', 'D:\test', ['D:\\test\\t1']) # esto omite la carpeta 'D:\\test\\t1'
   
    filez = []

    for f in sys.argv[1:]:
         if os.path.exists(f):
            filez += listdir_recurd([], f, f, [])
    

# la línea siguiente es la que hay que temer, borrará todo lo que se haya encontrado
# si por error se ingresa un directorio raíz... lo borrará todo, dejando las carpetas vacías!!
    [os.remove(c) for c in filez if os.path.exists(c)]
    print 'Files Deleted!!'



Como estoy usando Python Tools For Visual Studio, el script se debe setear como Startup File haciéndole click derecho en el explorador de soluciones. Las carpetas cuyos archivos se desean eliminar se ingresan como:


Hay que tener cuidado al borrar archivos con este método ya que no van a la papelera de reciclaje.