Con respecto a estos Iteradores Python sabemos que las personas que trabajan en Machine Learning e Inteligencia artificial (AI) dependen mucho de los iteradores (y generadores) en Python.
Son un concepto básico del lenguaje y se usan mucho en paquetes de Machine Learning como scikit-learn o Keras .
Comprender cómo funcionan y por qué los Pythonistas los usan ofrece dos ventajas:
- Puede usarlos de manera más efectiva y eficiente.
- Puede comprender los problemas relacionados con ellos más rápidamente. Esta comprensión le ahorrará mucho tiempo al escribir código y depurarlo.
Le daré los antecedentes necesarios en dos pasos.
Primero, le presento los conceptos subyacentes, incluidos algunos ejemplos fáciles. En segundo lugar, le guío por dos implementaciones ejemplares de scikit-learn .
En este paso, te muestro versiones simplificadas del código fuente. Este enfoque le dará una idea de cómo los desarrolladores convierten estos conceptos abstractos en herramientas de la vida real.
Iteradores Python: Conceptos generales
¿Qué es un iterador?
Iteradores Python son objetos de Python que devuelven un elemento a la vez. Cada vez que solicita un iterador para el elemento siguiente llama a su método __next__
. Si hay otro valor disponible, el iterador lo devuelve. De lo contrario, se genera una excepción StopIteration
.
Este comportamiento, que solo devuelve el siguiente elemento cuando se le solicita, tiene dos ventajas principales:
- Los iteradores Python necesitan menos espacio en la memoria. Recuerdan el último valor y una regla para llegar al siguiente valor en lugar de memorizar todos y cada uno de los elementos de una secuencia (potencialmente muy larga).
- Los iteradores Python no controlan cuánto tiempo puede durar la secuencia que producen. Por ejemplo, no necesitan saber cuántas líneas tiene un archivo o cuántos archivos hay en una carpeta para iterar a través de ellas.
Una nota importante: no confunda iteradores Python con iterables. Los Iterables son objetos que pueden crear iteradores usando su método __iter__
.
Eso es lo que Python hace bajo el capó en for-loops:
for classifier in ["XGBoost", "Logistic Regression", "SVM"]:
if classifier == "XGBoost":
print(f"{classifier} is the best! Maybe.")
else:
print(f"{classifier} is fun. Somehow.")
Internamente, Python llama al método __iter__
de lo que sea que entregues después de in
. Luego usa next
en el iterador creado en un ciclo while hasta que se lanza una excepción StopIteration
.
La razón por la que puedes crear bucles for en Python con muchos tipos es que tienen un método __iter__
implementado.
Los iterables son convenientes, pero no tienen las ventajas de un iterador “puro”. Por ejemplo, la lista que conviertes en un iterador ya usa memoria. Si puede, trate de ir directamente para el iterador en lugar de un iterable.
¿Qué es un generador?
Un generador es una forma simplificada de construir iteradores Python. En lugar de implementar el protocolo de iteración los generadores son funciones que devuelven valores con una declaración de yield
(rendimiento).
La diferencia entre el yield
y return
es que los generadores realizan un seguimiento de sus variables locales. Cada vez que un generador alcanza el yield
, devuelve un valor. A continuación, recuerda el estado actual de las variables dentro de la función y espera la siguiente llamada.
Generadores en paquetes de Machine Learning
Quiero mostrarles las implementaciones de dos ejemplos dentro de scikit-learn . Sin embargo, también hay ejemplos en Keras o Tensorflow . Los invito a que los busquen si desean profundizar su comprensión.
Ejemplo 1: KFold
Uno de los principales desafíos del Machine Learning es evitar el sobreajuste. Muchos algoritmos tienen hiperparámetros o modificaciones para abordar eso, pero el concepto más importante es la validación cruzada.
La idea es dividir un conjunto de datos en pliegues y usar cada pliegue una vez para probar el modelo que fue entrenado en los pliegues restantes.
Por ejemplo, si usa tres pliegues, el modelo entrena en dos de ellos y usa uno para probar.
La razón para usar un generador aquí es que no tiene que recordar toda combinación de índices desde el principio. Es suficiente saber qué índices son para probar en la ronda respectiva. El resto se usará para entrenamiento.
Echemos un vistazo al código fuente (líneas 357 a 434) de scKit-learn ‘s KFold
para ver cómo estaba el generador implementado:
class KFold(...):
def __init__(...):
...
def _iter_test_indices(...):
...
current = 0
for fold_size in fold_sizes:
start, stop = current, current + fold_size
yield indices[start:stop]
current = stop
Hay una declaración de rendimiento o yield
cerca del final de la definición de clase , que indica una función de generador. El nombre del método _iter_test_indices
sugiere que el generador solo arroja los índices para el pliegue de prueba.
Como escribí anteriormente, el entrenamiento se realizará en todos los demás índices, por lo que no es necesario calcularlos explícitamente.
La primera vez que se llama al generador, comienza con el índice 0 (current = 0)
y agrega el tamaño de el doblez para obtener el último índice de este doblez. Luego produce estos índices.
La próxima vez, busca sus variables locales y establece current
el valor de stop
de la última iteración antes de ejecutar otra ronda del for-loop.
Ejemplo 2: GradientBoostingClassifier
La idea básica detrás del aumento de gradiente es combine suficientes estudiantes débiles para obtener un modelo fuerte.
En contraste con los enfoques de embolsado que entrenan a muchos alumnos en paralelo, como el bosque aleatorio, el aumento de los trabajos de forma iterativa.
Entrena a un alumno débil, evalúa sus principales debilidades y trata de compensarlos en la siguiente ronda.
El carácter iterativo de este algoritmo demanda a los generadores. Vamos a verificar el código fuente de GradientBoostingClassifier
, más específicamente las líneas 1693 a 2156.
En aras de la simplicidad, nos enfocamos en un generador específico dentro de GradientBoostingClassfier
class (línea 2151):
def staged_predict_proba(...):
...
for score in self._staged_decision_function(X):
yield self.loss_._score_to_proba(score)
Omito mucho código aquí igualmente la idea básica permanece intacta. Una etapa en el aumento de gradiente es un paso en el proceso iterativo.
Este generador monitorea el error en el conjunto de prueba en cada paso. La razón para usar un generador aquí es que no tiene que definir cuántas etapas hay en una instancia particular de este modelo.
Espero que esta publicación de blog te haya ayudado a comprender las ideas básicas. También espero que los ejemplos del código fuente cierren la brecha entre los conceptos básicos y las implementaciones reales.