sábado, 10 de julio de 2021

Descripción general de árboles de decisión (Decision trees) y bosques aleatorios (Random Forest)

Los árboles de decisión (Decision trees) representan una clase de modelos de aprendizaje automático muy potentes. A menudo pueden lograr una precisión muy alta sin dejar de ser interpretables. Eso es lo que hace que los árboles de decisión sean tan valiosos. En lo que respecta a los modelos de aprendizaje automático, la claridad de la representación de la información es ejemplar. El conocimiento que un árbol de decisiones aprende a través del entrenamiento se expresa directamente como una estructura jerárquica. Y esta estructura, retiene y muestra el conocimiento para que se entienda fácilmente. El objetivo es construir un árbol con un conjunto de decisiones jerárquicas que eventualmente conduzcan a un resultado final.

árboles de decisión (Decision trees)  y bosques aleatorios (Random Forest)

Las decisiones se seleccionan de tal manera que el árbol se mantenga lo más pequeño posible, mientras que al mismo tiempo se busca un nivel de precisión muy alto.

Los modelos de árboles de decisión se crean mediante inducción y poda. Inducción, este es el proceso en el que construimos el árbol. Y esto representa todos los límites jerárquicos de decisión basados en los datos. Comenzamos determinando la mejor característica en el conjunto de datos para dividir los datos. Luego, los datos se dividen en subconjuntos que contienen valores posibles para la mejor característica. Esencialmente, esta división define un nodo en el árbol, o para decirlo de otra manera, cada nodo es un punto donde podemos dividir en función de una determinada característica de nuestros datos. Los nuevos nodos de árbol se generan de forma recursiva utilizando el subconjunto de datos creado en el paso anterior. Y seguimos dividiendo hasta que optimizamos la precisión y, al mismo tiempo, minimizamos el número de divisiones o nodos.

Para seleccionar la mejor función para usar y el tipo de división particular, generalmente usamos un algoritmo que minimice la función de costo. Intenta iterativamente diferentes puntos de división y, al final, selecciona el que tiene el costo más bajo. 

Para un árbol de regresión, utiliza el error al cuadrado simple como función de costo. Para una clasificación, utiliza la función de índice genio. (genie index function) donde, pk representa la proporción de instancias de entrenamiento de clase k en un nodo de predicción específico. Idealmente, un nodo debería tener un valor de error de cero. El resultado es que cada división genera una sola clase el 100% del tiempo.

Tener una sola clase por división en nuestro conjunto de datos se denomina ganancia de información. Si elegimos una división en la que cada salida tiene una combinación relativamente alta de clases, la ganancia de información es baja. Realmente no sabemos nada mejor después de la división si un nodo o característica específica, por así decirlo, tiene alguna influencia en la clasificación de nuestros datos. Por el contrario, si la división tiene un alto porcentaje de cada clase para cada salida, tiene una gran ganancia de información. 

Para evitar que el árbol de decisiones se vuelva enorme y, en consecuencia, se sobreajuste al conjunto de datos de entrenamiento, establecemos algunos criterios de detención predefinidos. Este suele ser un recuento mínimo de la cantidad de ejemplos de entrenamiento asignados a cada nodo hoja.

Una de las principales desventajas de los árboles de decisión es que son propensos al sobreajustarse. La poda es el proceso de eliminar estructuras innecesarias de un árbol de decisiones. Esto reduce la complejidad, ya que la complejidad después de todo es el número de divisiones en el árbol y también proporciona una mejor generalización. Un método de poda muy eficaz implica evaluar el efecto que tiene la eliminación de cada nodo en la función de costos. Si no hay muchos cambios, es probable que el nodo deba podarse.

Bosque Aleatorio (Random Forest)

Un bosque aleatorio es un algoritmo de aprendizaje supervisado. Construye un bosque o un conjunto de árboles de decisión, generalmente entrenados usando el método de ensacado. La ventaja conceptual detrás del método de ensacado es que una combinación debería aumentar el resultado general o mejorar el resultado general. En general, un bosque aleatorio crea varios árboles de decisión y luego los combina para obtener una predicción más precisa y un mejor resultado general.

Las características clave de los bosques aleatorios es que aportan una mayor aleatoriedad al modelo mientras se hacen crecer los árboles de decisión. En lugar de buscar la característica más importante al dividir un nodo, busca la mejor característica en un subconjunto aleatorio de características. Esto da como resultado una diversidad más amplia que normalmente significa un mejor modelo. En consecuencia, el algoritmo solo tiene en cuenta un subconjunto aleatorio de las características al dividir un nodo. Los árboles pueden hacerse aún más aleatorios aplicando umbrales aleatorios para cada característica en lugar de buscar los mejores umbrales posibles como lo hace un árbol de decisiones normal.


sábado, 3 de julio de 2021

Estructuras de datos Python

Listas

Probablemente la estructura de datos más fundamental en Python es la lista. Una lista es simplemente un array ordenado. (Es similar a lo que en otros lenguajes podría llamarse un array, pero con algunas funciones adicionales.)

lista_enteros = [1, 2, 3]

lista_heterogenea = ["cadena", 0.1, True]

lista_de_listas = [lista_enteros, lista_heterogenea,[]]

longitud_lista = len(lista_enteros) # igual a 3

suma_lista = sum(lista_enteros) # igual a 6

Estructuras de datos Python
Simetría

Podemos obtener o establecer el enésimo elemento de una lista con corchetes:

x = range(10) # es la lista [0, 1, ..., 9]

cero = x[0] # igual a 0, las listas son  0-indexadas

uno = x[1] # igual a 1

nueve = x[-1] # igual a 9, lee desde el último elemento

ocho = x[-2] # iguala 8,

x[0] = -1 # ahora x es [-1, 1, 2, 3, ..., 9]

También podemos utilizar corchetes para "dividir" listas:

primeros_tres = x[:3] # [-1, 1, 2]

tres_hasta_final = x[3:] # [3, 4, ..., 9]

uno_al_cuatro = x[1:5] # [1, 2, 3, 4]

ultimos_tres = x[-3:] # [7, 8, 9]

sin_primero_y_ultimo = x[1:-1] # [1, 2, ..., 8]

copia_de_x = x[:] # [-1, 1, 2, ..., 9]

Python tiene un operador in para verificar la pertenencia a la lista:

1 in [1, 2, 3] # True

0 in [1, 2, 3] # False

 

Esta verificación implica examinar los elementos de la lista de uno en uno, lo que significa que probablemente no deberíamos usarlo a menos que sepamos que nuestra lista es bastante pequeña. Es fácil concatenar listas:

x = [1, 2, 3]

x.extend([4, 5, 6]) # x es ahora [1,2,3,4,5,6]

Si no deseamos modificar x, podemos utilizar la adición de lista:

x = [1, 2, 3]

y = x + [4, 5, 6] # y es [1, 2, 3, 4, 5, 6]; x no cambia

Con más frecuencia, agregaremos a las listas un elemento a la vez:

x = [1, 2, 3]

x.append(0) # x es ahora[1, 2, 3, 0]

y = x[-1] # igual a  0

z = len(x) # igual a  4

A menudo es conveniente descomprimir las listas si sabemos cuántos elementos contienen:

x, y = [1, 2] # ahora x es 1, y es 2

Aunque obtendremos un ValueError si no tenemos el mismo número de elementos en ambos lados.

Es común usar un guión bajo para un valor que vamos a desechar:

_, y = [1, 2] # ahora y == 2, no nos importa el primer elemento

Tuplas

Las tuplas son primos de las listas. Prácticamente cualquier cosa que podamos hacer con una lista que no implique modificarla, podremos hacerlo en una tupla. Especificamos una tupla usando paréntesis (o nada) en lugar de corchetes:

mi_lista = [1, 2]

mi_tupla = (1, 2)

otra_tupla = 3, 4

mi_lista[1] = 3 # mi lista es ahora [1, 3]

try:

mi_tupla[1] = 3

except TypeError:

print "no se puede modificar una tupla"

Las tuplas son una forma conveniente de devolver múltiples valores de funciones:

def suma_y_multiplica(x, y):

return (x + y),(x * y)

sp = suma_y_multiplica(2, 3) # igual a (5, 6)

s, p = suma_y_multiplica(5, 10) # s es 15, p es 50

Las tuplas (y listas) también se pueden usar para asignaciones múltiples:

x, y = 1, 2 # ahora x es 1, y es 2

x, y = y, x # intercambiamos variables; ahora x es 2, y es 1

Diccionarios

Una estructura de datos fundamental es un diccionario, que asocia pares clave-valor y nos permite recuperar rápidamente el valor correspondiente a una clave determinada:

dic_vacio = {}

dic_edades = {"Antonio" : 34, "Juan" : 67 }

Podemos buscar el valor de una clave utilizando corchetes:

edad_antonio = dic_edades["Antonio"]  # devuelve 34

Pero obtendrermos un KeyError si intentamos recuperar una clave que no existe en el diccionario:

try:

edad_elena = dic_edades["Elena"]

except KeyError:

print "¡no existe la clave Elena!"

Podemos verificar la existencia de una clave usando:

antonio_existe_en_dic = "Antonio" in dic_edades # True

elena_existe_en_dic = "Elena" in dic_edades # False

Los diccionarios tienen un método get que devuelve un valor predeterminado (en lugar de generar una excepción) cuando buscamos una clave que no está en el diccionario:

edad_antonio = dic_edades.get("Antonio", 0) #devuelve 34

edad_elena = dic_edades.get("Elena", 0) # devuelve 0

 

Asignamos pares clave-valor utilizando los mismos corchetes:

dic_edades ["Juan"] = 68 # reemplaza el valor anterior

dic_edades ["Elena"] = 42 # añade un tercer valor

num_persona = len(dic_edades) # devuelve 3

Con frecuencia utilizaremos los diccionarios como una forma sencilla de representar datos estructurados:

dic_post_python = {

"autor" : "José Pedro",

"texto" : "Entrada de diccionarios con python",

"visitas" : 100,

"hashtags" : ["#python", "#diccionarios", "#datos"]

}

Además de buscar claves específicas, podemos mirarlas todas:

claves_dic = dic_post_python.keys() # lista de claves

valores_dic = dic_post_python.values() # lista de valores

pares_dic = dic_post_python.items() # lista de pares (clave,valor)

 

"autor" in dic_post_python # devuelve True

"José Pedro" in valores_dic # devuelve True

Valor = dic_post_python.get("autor")# devuelve José Pedro

#Para saber si existe una clave en el diccionario

existeClave = dic_post_python.has_key("autor")#devuelve True

Las claves del diccionario deben ser inmutables; en particular, no podemos utilizar listas como claves. Si necesitamos una clave de varias partes, debemos utilizar tuplas o encontrar una manera de convertir la clave en una cadena.

defaultdict

Imaginemos que estamos intentando contar las palabras de un documento. Un enfoque obvio es crear un diccionario en el que las claves sean palabras y los valores sean recuentos. Comprobamos cada palabra, y si ya existe  incrementamos su recuento y si no está la añadimos al diccionario:

contador_palabras = {}

for palabra in documento:

if palabra in contador_palabras:

contador_palabras[palabra] += 1

else:

contador_palabras[palabra] = 1

También podemos utilizar un enfoque diferente y manejar la excepción de intentar buscar una clave faltante:

contador_palabras = {}

for palabra in documento:

try:

contador_palabras [palabra] += 1

except KeyError:

contador_palabras[palabra] = 1

Un tercer enfoque es usar get, que se comporta de forma más elegante con las claves faltantes:

contador_palabras = {}

for palabra in documento:

cuenta_previa = contador_palabras.get(palabra, 0)

contador_palabras[palabra] = cuenta_previa + 1

Defaultdict es útil cuando intentamos buscar una clave, que aún no existe en el diccionario, En ese caso,  primero se agrega con un valor por defecto inicial que se le pasa por argumento al crearla. Para utilizar los defaultdict predeterminados, debemos importarlos desde colecciones:

from collections import defaultdict

contador_palabras = defaultdict(int) # int() produce 0

for palabra in documento:

contador_palabras [palabra] += 1

También pueden ser útiles con list o dict o incluso con nuestras propias funciones:

dd_lista = defaultdict(list) # list() produce una lista vacía

dd_lista[2].append(1) # ahora dd_lista contiene {2: [1]}

dd_dicc = defaultdict(dict) # dict() produce un diccionario vacío

dd_dicc["Jose"]["Ciudad"] = "Madrid" # { "Jose" : { "Ciudad" : Madrid"}}

dd_par = defaultdict(lambda: [0, 0])

dd_par[2][1] = 1 # ahora dd_par contiene {2: [0,1]}

Estos serán útiles cuando utilicemos diccionarios para "recopilar" resultados por alguna clave y no queramos tener que comprobar cada vez para ver si la clave existe aún.

Counter

Un contador convierte una secuencia de valores en claves de mapeo de objetos similares a defaultdict (int) para contar. Lo utilizaremos principalmente para crear histogramas:

from collections import Counter

c = Counter([0, 1, 2, 0]) # c es (basicamente) { 0 : 2, 1 : 1, 2 : 1 }

Esto nos da una forma muy sencilla de resolver nuestro problema de recuentos de palabras:

contador_palabras = Counter(documento)

Una instancia de Counter tiene un método most_common que suele ser útil:

 # imprime las 10 palabras más communes y su conteo

for palabra, conteo in contador_palabras.mas_comun(10):

print palabra, conteo

 

Sets

Se establece otra estructura de datos, que representa una colección de elementos distintos:

s = set()

s.add(1) # s es ahora { 1 }

s.add(2) # s es ahora { 1, 2 }

s.add(2) # s es todavía { 1, 2 }

x = len(s) # igual a 2

y = 2 in s # es True

z = 3 in s # es False

Utilizaremos sets por dos razones principales. La primera es que los sets son muy muy rápidos. Si tenemos una gran cantidad de elementos que queremos utilizar para una prueba, un set es más apropiado que una lista:

lista_palabras = ["a","abaco","abad" … "zurrón","zutano"]

"alguno" in lista_palabras # False, pero chequea cada elemento de la lista

set_palabras = set(lista_palabras)

"alguno" in set_palabras # chequea mucho más rápido

La segunda razón es encontrar los elementos distintos en una colección:

lista_objetos = [1, 2, 3, 1, 2, 3]

num_objetos = len(lista_objetos) # 6

set_objetos = set(lista_objetos) # {1, 2, 3}

num_objetos_distintos = len(set_objetos) # 3

lista_objetos_distintos = list(set_objetos) # [1, 2, 3]

Utilizaremos sets con mucha menos frecuencia que los diccionarios y las listas.