Machine Learning Spark

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 65

Universidad Central “Marta Abreu” de Las Villas

Facultad de Matemática Física y Computación


Centro de Estudios de Informática

Trabajo de Diploma

Implementación de un algoritmo de aprendizaje


automático en Apache Spark

Autor: Ricardo Sánchez Alba

Tutor: Dr. Carlos Morell Pérez

Santa Clara
2017
“Año 59 de la Revolución”
Hago constar que el presente Trabajo de Diploma fue realizado en la Universidad Central
“Marta Abreu” de Las Villas como parte de la culminación de estudios de la especialidad
de Licenciatura en Ciencia de la Computación, autorizando a que el mismo sea utilizado
por la Institución, para los fines que estime conveniente, tanto de forma parcial como
total y que además no podrá ser presentado en eventos, ni publicados sin autorización de
la Universidad.

Ricardo Sánchez Alba Fecha


Autor

Los abajo firmantes certificamos que el presente trabajo ha sido realizado según acuerdo
de la dirección de nuestro centro y el mismo cumple con los requisitos que debe tener un
trabajo de esta envergadura referido a la temática señalada.

Ricardo Sánchez Alba Fecha


Autor

Dra.Gheisa Ferreira Lorenzo Fecha


Jefa del Departmento

Dr.Carlos Morell Pérez Fecha


Jefe del Seminario
PENSAMIENTO

“Si se coloca el centro de gravedad de la vida no en la vida, sino en el «más allá» -en la
nada-, se le quita a la vida en general el centro de gravedad”.

Friedrich Wilhelm Nietzsche

i
DEDICATORIA

Al maestro.

ii
AGRADECIMIENTOS

A todos los que confiaron en mí, a mi familia, mi pareja, mis amigos. Al café.

iii
RESUMEN

El análisis de grandes cantidades de datos, así como la extracción de conocimiento útil de


estos constituye en la actualidad un reto ya que cada día crecen velozmente los volúmenes
de información generada y se necesitan programas capaces de realizar esta tarea en poco
tiempo.
Durante varios años frameworks de código abierto han sido utilizados para la aplicación

de técnicas de aprendizaje automático en pequeños volúmenes de datos, pero la necesidad

creciente de la industria ha dado como consecuencia una evolución en el área del cómputo

distribuido, surgiendo así herramientas como Apache Hadoop y Apache Spark siendo éste

último entre 10 y 100 veces más rápido que su antecesor.


En este trabajo se propone un procedimiento general para la inclusión de nuevos algoritmos

de aprendizaje automático en el framework Apache Spark y se implementa un algoritmo

de regresión lineal con el fin de validar la metodología propuesta. Se realizaron una serie de

experimentos al software implementado que permitieron valorar las ventajas del framework

Apache Spark para reducir significativamente los tiempos de ejecución cuando este tipo
de algoritmo se somete al procesamiento de cantidades masivas de datos.

iv
ABSTRACT

The analysis of large amounts of data, as well as the extraction of useful knowledge of
these, is now a challenge as each day the volumes of information generated grow rapidly
and programs are needed that can perform this task in a short time.
For several years Open source frameworks have been used for the application of automated
learning techniques in small volumes of data, but the growing need of the industry has

resulted in an evolution in the area of distributed computing, resulting in tools such as

Apache Hadoop and Apache Spark The latter being between 10 and 100 times faster than

its predecessor.

In this paper we propose a general procedure for the inclusion of new algorithms of au-
tomatic learning in the Apache Spark framework and a linear regression algorithm is

implemented in order to validate the proposed methodology.

A series of experiments were performed on the implemented software that allowed to eva-

luate the advantages of the Apache Spark framework to significantly reduce execution

times when this type of algorithm is submitted to the processing of massive amounts of
data.

v
TABLA DE CONTENIDO
Página

PENSAMIENTO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i

DEDICATORIA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii

AGRADECIMIENTOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii

RESUMEN . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iv

ABSTRACT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v

INTRODUCCIÓN . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

CAPÍTULO 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4

1. Marco teórico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

1.1. Apache Spark . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5


1.2. Componentes de Spark . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3. Clústers de Spark . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.4. El modelo de programación de Spark . . . . . . . . . . . . . . . . . . . 9
1.4.1. Resilient Distributed Datasets . . . . . . . . . . . . . . . . . . . 9
1.4.2. Spark SQL, DataFrames y Datasets . . . . . . . . . . . . . . . . 10
1.4.3. Operaciones en Spark . . . . . . . . . . . . . . . . . . . . . . . . 10
1.4.4. Ventajas de la evaluación perezosa . . . . . . . . . . . . . . . . . 11
1.4.5. Manejo de memoria y persistencia en memoria . . . . . . . . . . 11
1.5. Aprendizaje Automático con Spark . . . . . . . . . . . . . . . . . . . . 12
1.5.1. MLlib vs ML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.5.2. DataFrame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5.3. Transformers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.5.4. Estimators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.5.5. Pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.5.6. Parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

vi
1.6. Métodos Lineales - API basada en RDD . . . . . . . . . . . . . . . . . 18
1.6.1. Formulación matemática . . . . . . . . . . . . . . . . . . . . . . 18
1.6.2. Funciones de pérdida . . . . . . . . . . . . . . . . . . . . . . . . 19
1.6.3. Regularizadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.7. Optimización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.7.1. Descenso de gradiente . . . . . . . . . . . . . . . . . . . . . . . . 21
1.7.2. Descenso de gradiente estocástico (SGD) . . . . . . . . . . . . . 21
1.7.3. Esquemas de actualización para SGD distribuido . . . . . . . . . 22
1.7.4. BFGS de memoria limitada (L-BFGS) . . . . . . . . . . . . . . . 23
1.8. Implementación en MLlib . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.8.1. Descenso de gradiente y descenso de gradiente estocástico . . . . 23
1.8.2. L-BFGS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

CAPÍTULO 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

2. Implementación de un modelo de Regresion Lineal personalizado utilizando


Spark ML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

2.1. Modelo de herencia de clases para la API de Spark ML . . . . . . . . . 28


2.2. Pasos propuestos para el ajuste de un procedimiento de Aprendizaje
Automático al modelo de Pipeline de Spark ML . . . . . . . . . . . . 29
2.3. Descripción matemática del algoritmo implementado . . . . . . . . . . 30
2.3.1. Ecuación normal . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.3.2. Redefinición de la multiplicación de matrices para permitir la
escalabilidad del cómputo paralelo . . . . . . . . . . . . . . . . 31
2.4. Características y diseño general de la implementación . . . . . . . . . . 33
2.4.1. Principales clases y funciones implementadas . . . . . . . . . . . 33
2.5. Diagramas de clases asociados a CustomLinearRegression y CustomLi-
nearRegressionModel . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

CAPÍTULO 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

3. HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS . 39

3.1. Preparación del ambiente de desarrollo . . . . . . . . . . . . . . . . . . 39


3.1.1. Creación de un proyecto de Scala con Spark en el IDE IntelliJ
IDEA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.1.2. Compilación y creación del archivo .jar para ser lanzado en un
clúster de Spark mediante la herramienta spark-submit . . . . 40
vii
3.1.3. Carga de los datasets hacia el clúster de Spark con HDFS y eje-
cución del programa en el clúster . . . . . . . . . . . . . . . . 40
3.2. Pruebas preliminares . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.2.1. Pruebas realizadas al dataset E2006.train . . . . . . . . . . . . . 41
3.2.2. Pruebas realizadas al dataset YearPredictionMSD . . . . . . . . 45
3.3. Pruebas de escalabilidad . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.3.1. Características del clúster utilizado: . . . . . . . . . . . . . . . . 46
3.3.2. Pruebas sobre el dataset YearPredictionMSD: . . . . . . . . . . . 47

CONCLUSIONES Y RECOMENDACIONES . . . . . . . . . . . . . . . . . . . . . 52

CONCLUSIONES . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53

RECOMENDACIONES . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

viii
INTRODUCCIÓN

Cada día la cantidad de información de cualquier tipo aumenta aceleradamente y así mis-
mo se hace necesario no solo almacenar esta información sino poder procesarla eficiente y
eficazmente para extraer conocimiento útil de esta. Hastie et al. (2003)

Construir modelos para detectar fraudes de tarjetas de crédito utilizando miles de carac-
terísticas y billones de transacciones; recomendar inteligentemente millones de productos
a millones de usuarios; estimar riesgos financieros utilizando simulaciones de portafolios

incluyendo millones de instrumentos y manipular datos del genoma humano para detectar

asociaciones genéticas con enfermedades eran tareas muy difíciles o imposibles de realizar

hace 5 o 10 años atrás debido a la alta complejidad computacional y las cantidades de


datos asociadas. El modelo de programación funcional MapReduce ha sido pionero en el

tratamiento de estas grandes cantidades de datos dada su facilidad para ser implementado

mediante sistemas de cómputo paralelos. Sistemas distribuidos como Apache Hadoop han

encontrado así su camino y han tenido una amplia aplicación en múltiples empresas.Ryza
et al. (2015)

Antecedentes y planteamiento del problema

Durante un largo periodo de tiempo, frameworks de código abierto como R, PyData y


Octave han sido utilizados para realizar rápidos análisis y construcciones de modelos via-
bles sobre pequeños datasets. Pero un correcto aprovechamiento de estos frameworks sería
extenderlos para ejecutarlos en múltiples computadoras, mantener sus modelos de progra-
mación y reescribir sus interioridades para lidiar eficientemente con características distri-
buidas. Sin embargo, muchos elementos que se asumían por estar en sistemas con un solo

1
INTRODUCCIÓN 2

nodo requieren ser repensados para la computación distribuida. Por ejemplo, porque los
datos deben estar particionados a lo largo de múltiples nodos en un clúster de compu-
tadoras, algoritmos que tenían amplias dependencias de datos sufrirían el hecho de que las
tasas de transferencia de la red son más lentas que el acceso a memoria. Así como que a
medida que aumentan la cantidad de computadoras trabajando en un problema, aumenta
la probabilidad de fallo.Ryza et al. (2015)

Como parte de las investigaciones que se desarrollan en el laboratorio de Inteligencia


Artificial del Centro de Investigaciones de Informática (CEI) se desea poder utilizar el

ambiente de computación distribuida Apache Spark para disminuir significativamente los


tiempos de ejecución de algoritmos de Aprendizaje Automatizado cuando estos se someten
a procesamientos de cantidades masivas de datos. Apache Spark cuenta con un módulo

en desarrollo concebido para el trabajo con algoritmos de esta naturaleza, dentro de este,

se tiene un conjunto de algoritmos de aprendizaje automático y utilidades afines que se

brindan como un paquete pre-elaborado. Es de gran utilidad conocer el procedimiento a


seguir cuando un investigador desea incluir un algoritmo propio y que este aproveche las

ventajas de la arquitectura subyacente definida por los desarrolladores de Apache Spark.

A partir de la problemática descrita se plantea la siguiente pregunta de investigación:


¿Cuál es el procedimiento adecuado para incluir nuevos algoritmos de aprendizaje auto-
mático al ambiente de cómputo distribuido Apache Spark, de forma sencilla y utilizando
las facilidades adecuadas?
De esta forma se desea desarrollar un algoritmo de aprendizaje automático en Apache
Spark mediante la definición de un conjunto de pasos generales para ajustar estos proce-
dimientos al modelo de programación de este framework, lo cual constituye el objetivo
general de este trabajo.
Para dar cumplimiento al anterior objetivo general se proponen los objetivos específicos
INTRODUCCIÓN 3

siguientes:

Determinar las características de la arquitectura de Apache Spark que permiten la

implementación de algoritmos de aprendizaje automático.


Proponer un conjunto de pasos para la implementación de algoritmos de aprendizaje

automático en Apache Spark.


Implementar un algoritmo de aprendizaje automático en Apache Spark utilizando la
arquitectura del modelo del framework y los pasos propuestos en este trabajo.

Evaluar el desempeño del algoritmo bajo el ambiente distribuido de Spark mediante


la realización de experimentos.
CAPÍTULO 1
Capítulo 1
MARCO TEÓRICO

1.1. Apache Spark

Apache Spark es un framework para la computación distribuida; este framework tiene


como propósito simplificar la escritura de programas que se ejecutan en paralelo utilizando
varios núcleos en un cluster de computadoras. El mismo intenta abstraer la tarea de la

planificación de recursos, envíos de trabajo, ejecución, rastreo y comunicación entre nodos,

así como las operaciones de bajo nivel que son inherentes al procesamiento de datos en
paralelo. De esta forma, es similar a otros frameworks de procesamiento distribuido como

Apache Hadoop; no obstante, la arquitectura subyacente es de cierta forma diferente.


Pentreath. (2015)

Spark comenzó como un proyecto de investigación en la Universidad de California, Ber-


keley. La universidad estaba concentrada en el caso de uso de algoritmos de aprendizaje
automatizado en arquitecturas distribuidas. Consecuentemente, se diseñó desde su inicio

para el alto desempeño en aplicaciones de naturaleza iterativa, en las cuales el mismo


dataset se accede múltiples veces. Este desempeño se logra fundamentalmente mediante
la carga de estos dataset en memoria, combinado con una baja latencia y overhead para
emprender tareas de cómputo paralelo. Junto a otras características como la tolerancia
a fallos, estructuras de datos flexibles de memoria distribuida y una poderosa y funcio-
nal API, Spark ha probado ser significativamente útil para un amplio rango de tareas de

5
MARCO TEÓRICO 6

procesamiento de datos a gran escala, así como para el aprendizaje automático y analísis
iterativos. Pentreath. (2015)

Spark se puede ejecutar de cuatro modos diferentes:

El modo local auto-sostenido, donde todos los procesos de Spark corren en la misma
Java Virtual Machine (JVM).
El modo clúster auto-sostenido, usando el framework de planificación de trabajo em-
bebido de Spark.
Usando Mesos, un popular framework de código abierto para cluster-computing.
Usando YARN (comunmente referido como NextGen MapReduce).

Pentreath. (2015)

Figura 1–1: Diagrama del ecosistema de procesamiento de datos incluyendo Spark.


MARCO TEÓRICO 7

1.2. Componentes de Spark

Spark provee un lenguaje de consultas de alto nivel para el procesamiento de datos. En el


ecosistema de Spark, Spark Core es el componente principal de procesamiento de datos,
el mismo tiene APIs en Scala, Java, Python y R. Además incluye otros componentes de
primer orden como Spark SQL, Spark MLlib, Spark ML, Spark Streaming y GraphX los
cuales proveen funcionalidades de procesamiento más específicas.

Figura 1–2: Principales componentes de Spark

Algunos de estos componentes tienen las mismas convenciones de implementación de Spark

Core, otras tienen consideraciones particulares. Un ejemplo de esto es que Spark SQL uti-
liza un motor de optimización de consultas diferente.

Spark SQL introduce dos tipos de datos semi-estructurados en forma de interfaces, DataFrames

y Datasets . Este componente constituye el futuro de Spark, con opciones de almacena-


miento más eficientes, optimizadores avanzados y operaciones directas en datos serializa-

dos. Este componente es fundamental para extraer el mejor desempeño de Spark.Karau


(2017)

ML y MLlib son los componentes de aprendizaje automático de Spark. MLlib fue escrito
junto con Spark. Spark ML está todavía en las primeras etapas de desarrollo y existe úni-
camente luego de la versión 1.2 de Spark. Spark ML provee una API de más alto nivel que
MLlib y tiene el objetivo de facilitar la construcción práctica de cadenas de procesos de
MARCO TEÓRICO 8

aprendizaje automático. La comunidad de Spark planea concentrarse solamente en Spark


ML y no en MLlib, quedando este último caducado en un estado de corrección de errores
solamente. Estos tienen consideraciones de desempeño individuales ya que Spark MLlib
está implementado sobre Spark Core y ML está desarrollado sobre Spark SQL.

Spark Streaming está centrado en la manipulación de datos en flujo. Esta contiene dos
APIs, DStreams y otra basada en streaming estructurado que se encuentra en un estado
de desarrollo alpha la cual utiliza DataFrames de Spark SQL.Karau (2017)

GraphX está concentrado en el procesamiento de grafos. Es uno de los componentes menos


desarrollados de Spark.Karau (2017)

1.3. Clústers de Spark

Un clúster de Spark está constituido por dos tipos de procesos: un programa conductor y

múltiples ejecutores. En el modo local, todos estos procesos están corriendo en la misma

JVM. En un clúster de computadoras estos procesos corren usualmente en nodos por

separado. Pentreath. (2015)

Figura 1–3: Clúster de Spark


MARCO TEÓRICO 9

1.4. El modelo de programación de Spark

Spark puede ser utilizado desde varios lenguajes de programación, entre ellos Python, Java
y Scala. En este documento se utilizará Scala para las secciones de código y las anotaciones
generales de programación dada la claridad sintáctica que este presenta en este tipo de
programa.

Generalmente un programa en Spark se inicia con un dataset, que usualmente esta distri-
buido, utilizando por ejemplo Hadoop Distributed File System (HDFS) y usualmente esta
relacionado con los pasos:

Definir un conjunto de transformaciones en datasets de entrada.


Invocar acciones que devuelven el dataset transformado en almacenamiento persistente

o retornan resultados hacia la memoria local.


Ejecutar procesamientos locales que operan en los resultados computados de manera

distribuida, lo cual puede ayudar a decidir que transformaciones y acciones realizar

posteriormente.

Ryza et al. (2015)

1.4.1. Resilient Distributed Datasets

El núcleo de Spark es un concepto denominado Resilient Distributed Datasets (RDD). Un

RDD es una colección de registros que están distribuidos o particionados a través múl-
tiples nodos en un clúster (para los propósitos del modo local de Spark esto puede ser
pensado de la misma manera). Un RDD en Spark es tolerante a fallos; esto significa que
si un nodo o una tarea falla (por alguna razón aparte de código erróneo del usuario, como
fallas de hardware, perdida de comunicación, entre otras), el RDD puede ser reconstruido
automáticamente desde los restantes nodos y el trabajo se mantendrá intacto, esto es po-
sible ya que los RDDs autocontienen toda la información necesaria para su reconstrucción
MARCO TEÓRICO 10

mediante sus dependencias. Pentreath. (2015)

1.4.2. Spark SQL, DataFrames y Datasets

A diferencia de la API basada en RDD, las interfaces brindadas por Spark SQL proveen a
Spark mas información acerca de las estructuras tanto de los datos como del cómputo que
se está realizando. Internamente, Spark SQL utiliza esta información extra para realizar
optimizaciones en la manipulación de los datos.

Un Dataset es una colección de datos distribuida. Se introdujo en Spark 1.6 para pro-

veer los beneficios de los RDD(tipado fuerte y posibilidad de utilizar poderosas funciones
lambda) junto a los beneficios de el motor de ejecución optimizado de Spark SQL.

Un DataFrame es un Dataset organizado en columnas nombradas. Esto es conceptual-


mente equivalente a una tabla en una base de datos relacional. Los DataFrames pueden

ser construidos desde una amplia colección de orígenes, como campos de datos estructu-

rados, tablas en Hive, bases de datos externas o RDDs existentes.

1.4.3. Operaciones en Spark

Las operaciones que se realizan sobre las colecciones distribuidas de Spark se pueden cla-

sificar en dos grupos: las transformaciones y las acciones. Una transformación aplica una
función sobre todos los registros de un RDD modificándolos de alguna manera, al estilo de

la operación map de los paradigmas de programación funcional. Una acción generalmente


realiza alguna acción de agregación sobre estos registros y devuelve un resultado concreto
al programa principal, lo cuál es coherente con la operación reduce.

Una característica medular de las transformaciones es que estas presentan lazy-evaluation


o evaluación perezosa. Luego de llamar a una transformación Spark no ejecuta el cómpu-
to en las particiones hasta que alguna acción se ejecuta, en este momento comienza la
MARCO TEÓRICO 11

construcción de un grafo acíclico dirigido basado en las dependencias entre las transfor-
maciones de las particiones. Spark evalúa las acciones mediante el cómputo previo de las
transformaciones, en un cómputo retrospectivo.

1.4.4. Ventajas de la evaluación perezosa

La evaluación perezosa permite a Spark combinar operaciones que no necesitan comunica-


ción con el programa principal, por ejemplo operaciones map y filter solo necesitarían

recorrer los datos una vez ejecutando ambas operaciones combinadas en vez de ejecutar
estas operaciones por separado lo cual requeriría recorrer los datos varias veces.

El paradigma de evaluación perezosa de Spark también permite facilidades de implemen-

tación ya que permite encadenar procesamientos y dejar la tarea de su consolidación al


motor de evaluación de Spark.

La inmutabilidad de estas estructuras, al estar implementadas respetando las convenciones


de la programación funcional permiten que este framework brinde la facilidad de escribir

programas utilizando paralelización implícita, esta es un ventaja fundamental ya que redu-

ce significativamente la escritura y el entendimiento de programas paralelos al manifiestar

un nivel de abstracción más alto que frameworks distribuidos anteriores.

1.4.5. Manejo de memoria y persistencia en memoria

La ventaja de desempeño de Spark respecto MapReduce es substancial en casos donde es

necesario cálculos repetidos, esto es posible ya que Spark utiliza persistencia de datos en
memoria. Spark permite la opción de que los ejecutores conserven los datos cargados en
memoria en vez de acceder repetidas veces en el disco, lo cual reduce el tiempo de acceso
significativamente cuando estos datos son accedidos repetidas veces.
Spark provee tres opciones para el manejo de memoria, de manera serializada en memoria,
sin serializar en memoria y una tercera en disco. Cada una con sus particularidades de
rendimiento.
MARCO TEÓRICO 12

La función persist() de la clase RDD permite al usuario controlar la forma de almacena-


miento del RDD. Por defecto, persist() almacena un RDD como objeto deserializado
en memoria, pero esta función puede recibir varios parámetros para controlar la forma

de almacenamiento. Si el espacio ocupado por un RDD es requerido para el cómputo o


persistencia de una nueva partición, la implementación por defecto de los RDD desecha

la partición de uso menos reciente, según la política LRU. No obstante este comporta-
miento puede ser configurado mediante la función persistancePriority() de la class
RDD.Karau (2017)

1.5. Aprendizaje Automático con Spark

Spark contiene dos bibliotecas de aprendizaje automático, Spark MLlib y Spark ML con

APIs marcadamente diferentes, en conjunto incluyen utilidades para clasificación, regre-


sión, agrupamiento, filtrado colaborativo, reducción de dimensionalidad, así como las pri-

mitivas de optimización subyacentes. Estas bibliotecas heredan consideraciones de rendi-

miento de la API basada en RDD y la basada en Datasets. MLlib es la primera de las

dos pero se encuentra en un estado de mantenimiento y corrección de errores solamen-


te. Spark ML es la API nueva, donde el desarrollo activo esta tomando lugar.Karau (2017)

1.5.1. MLlib vs ML

Estructuralmente los algoritmos de aprendizaje automático que se quieran implementar


deben estar dentro de estas bibliotecas o bien utilizar interfaces públicas brindadas por
estas APIs. Esta sección esta enfocada a mostrar los diferentes puntos de vista necesarios
para la elección de la API a utilizar, y enfatiza principalmente en la implementación usan-
do Spark ML.
MARCO TEÓRICO 13

Los diferentes algoritmos de aprendizaje automático están agrupados en la API de Spark


ML según su tipo en los paquetes: classification , regression , recommendation y
clustering . Adicionalmente a los paquetes proveídos por las distribuciones estándar de
Spark existen paquetes de algoritmos construidos por la comunidad de desarrollo de Spark
y están concentrados en el sitio web spark-packages.org.

Una de las diferencias básicas de las APIs está en los tipos de datos con los que estas
trabajan, MLlib usa RDD y ML utiliza DataFrames y Datasets de Spark SQL, esta
diferencia no tiene una importancia marcada ya que estas APIs trabajan con RDD y Da-

tasets de Vectores respectivamente, los cuales son fácilmente transformables entre ellos.

Desde el punto de vista del diseño, Spark MLlib está enfocado en proveer un conjunto de

algoritmos para los usuarios, dejando a un lado la secuencia de procesamiento previo de

limpieza, preparación y selección de características, lo cual queda en manos del usuario de

la biblioteca. Spark ML está enfocada en dar a conocer una API basada en el concepto
de Pipelines o tuberías las cuales no son más que secuencias de procesamientos que van

desde la preparación inicial de los datos hasta las etapas finales de selección y evaluación

de modelos.

1.5.2. DataFrame

El aprendizaje automático se puede aplicar a una amplia variedad de tipos de datos, co-
mo vectores, texto, imágenes y datos estructurados. Esta API adopta el DataFrame de
Spark SQL con el fin de soportar una variedad de tipos de datos. Además de los tipos
enumerados en la guía Spark SQL, un DataFrame también puede utilizar el tipo Vector.
Un DataFrame puede crearse implícita o explícitamente desde un RDD.
MARCO TEÓRICO 14

1.5.3. Transformers

Un Transformers es una abstracción que incluye transformadores de características y mo-


delos aprendidos. Técnicamente, un Transformer implementa un método transform(), el
cual convierte un DataFrame en otro, generalmente agregándole una o más columnas.
Por ejemplo:

Un transformador de características tomaría un DataFrame , leería una columna (ej.


texto), la mapearía a una nueva columna (ej. vector de características), y devolvería
un nuevo DataFrame con la columna mapeada adjunta.
Un modelo de aprendizaje puede tomar un DataFrame , leer la columna que contiene
los vectores de características, predecir la clase para cada vector de características y

devolver un nuevo DataFrame con la columna de las clases adjunta.

1.5.4. Estimators

Un estimador abstrae el concepto de algoritmo de aprendizaje o de cualquier algo-

ritmo que se aplique o se entrene con datos. Técnicamente, un Estimator imple-


menta un método fit() que acepta un DataFrame y produce un Model, el cual es un

Transformer . Por ejemplo, un algoritmo de aprendizaje como LogisticRegression es

un Estimator , y llamando al método fit() entrena a un LogisticRegressionModel, el


cual es un Model y por lo tanto un Transformer .

1.5.5. Pipeline

En aprendizaje automático es común ejecutar una secuencia de algoritmos para pro-


cesar y aprender de datos. Por ejemplo: un workflow para un procesamiento simple
de texto pudiera incluir varias etapas:
• Separar el texto de cada documento en palabras.

• Convertir cada palabra en un vector numérico de características.


• Aprender un modelo de predicción usando los vectores de características y las
clases.
MARCO TEÓRICO 15

Spark ML representa este flujo de trabajo como un Pipeline , el cual consiste en una
secuencia de PipelineStages ( Transformer s y Estimator s) que se ejecutan en un
orden específico.

Un Pipeline se especifica como una secuencia de etapas, cada una es un Transformer


o un Estimator . Estas etapas se ejecutan en orden y el DataFrame de entrada se
transforma a medida que transita cada etapa. En las etapas de tipo Transformer se
llama al método transform() en los DataFrames . En las etapas de tipo Estimator ,
el método fit() es el llamado para producir un Transformer (el cual forma parte

del PipelineModel), y el método transform() de ese Transformer es llamado en el

DataFrame . Ej, para el flujo de procesamiento de texto:

Figura 1–4: Pipeline.

En la figura, la fila superior representa una Pipeline con tres etapas. Las dos prime-

ras ( Tokenizer y HashingTF ) son Transformers (azul), y el tercero


( LogisticRegression ) es un Estimator (rojo). La fila inferior representa los da-

tos que fluyen a través de la tubería, donde los cilindros indican DataFrames . El
método Pipeline.fit() se llama en el DataFrame original, que contiene docu-
mentos de texto plano y clases. El método Tokenizer.transform() divide los do-

cumentos de texto plano en palabras, añadiendo una nueva columna con palabras
al DataFrame . El método HashingTF.transform() convierte la columna de pa-
labras en vectores de características, añadiendo una nueva columna con los vec-
tores al DataFrame . Ahora, puesto que LogisticRegression es un Estimator ,
la tubería primero llama al método LogisticRegression.fit() para producir un
MARCO TEÓRICO 16

LogisticRegressionModel . Si el Pipeline hubiese tenido más etapas, habría lla-


mado al método LogisticRegressionModel.transform() en el DataFrame antes
antes de pasarlo a la siguiente etapa.ASF (2017)

Una Pipeline es un Estimator . Por lo tanto, luego de ejecutarse su método fit()


se produce un PipelineModel , el cual es un Transformer . Este modelo se usa en

el momento de prueba. Por ejemplo:

Figura 1–5: PipelineModel.

En la figura de arriba, el PipelineModel tiene el mismo número de etapas que el

Pipeline original, pero todos los Estimators en el Pipeline original en este caso
son Transformers . Cuando el método transform() del PipelineModel es llamado

en un dataset de prueba, los datos son enviados en orden a través del Pipeline
ajustado. El método transform() de cada etapa actualiza el dataset y lo envía a la

próxima etapa.
Las Pipelines y los PipelineModels ayudan a asegurarse de que los datos de en-
trenamiento y prueba pasan a través de procesamientos de características idénticos.

Detalles

Las Pipelines se pueden constituir también en una topología no lineal, donde las

etapas no formen una línea directa, siempre y cuando formen un grafo acíclico dirigido
y en este caso las etapas deben ser especificadas siguiendo el ordenamiento topológico.
MARCO TEÓRICO 17

Como las Pipelines pueden operar en DataFrames con tipos variables, estas no
pueden usar chequeo de tipos en tiempo de compilación por lo que se utiliza chequeo
en tiempo de ejecución previo a que se ejecute realmente el Pipeline . Este chequeo
de tipos se realiza utilizando el esquema DataFrame , una descripción de los tipos de
datos de las columnas en el DataFrame .

Las etapas de las Pipelines deben ser instancias únicas. Ej, la misma instan-
cia myHashingTF no debería ser insertada dos veces en una Pipeline ya que las
etapas de las Pipelines tienen un ID único. Sin embargo, diferentes instancias
myHashingTF1 y myHashingTF2 (ambas de tipo HashingTF ) pueden ser inserta-

das en la misma Pipeline ya que diferentes instancias se crean con diferentes IDs.

1.5.6. Parámetros

Los estimadores y transformadores utilizan una API homogénea para la especificación


de parámetros.

Un Param es un parámetro nombrado con documentación autocontenida. Un ParamMap


es un conjunto de pares (parametro, valor).

Existen dos formas fundamentales de pasar parámetros a un algoritmo:


1. Poner los parámetros en una instancia. Por ejemplo, si lr es una instancia de
LogisticRegression , se podría llamar a lr.setMaxIter(10) para hacer que
lr.fit() utilice un máximo de 10 iteraciones.

2. Enviarle un ParamMap a fit() o a transform . Cualquier parámetro en el


ParamMap sobrescribirá parámetros previamente introducidos mediante métodos
setters.
Los parámetros pertenecen a instancias específicas de Estimators y Transformers .
Por ejemplo, si se tienen dos instancias de LogisticRegression lr1 y lr2 , en-
tonces se puede construir un ParamMap con el parámetro maxIter de ambos espe-
cificados de la forma:
MARCO TEÓRICO 18

ParamMap(lr1.maxIter ->10, lr2.maxIter ->20) . Esto es útil si se tienen dos al-


goritmos con el parámetro maxIter en una Pipeline .

1.6. Métodos Lineales - API basada en RDD

1.6.1. Formulación matemática

En la siguiente sección se exponen algunas de las funcionalidades matemáticas esen-


ciales brindadas por MLlib mediante paquetes de algoritmos configurables.

Muchos métodos estándar de aprendizaje automático pueden ser formulados como

un problema de optimización convexo, esto es, la tarea de hallar un mínimo de una


función convexa f que depende de un vector variable w, el cual tiene d entradas.
Formalmente, se puede escribir esto como el problema minw∈Rd f (w), donde la función

objetivo tiene la forma:

1∑ n
f (w) = λR(w) + L(w; xi , yi ).
n i=1
Los vectores xi ∈ Rd son los datos de entrenamiento, para 1 ≤ i ≤ n, y yi ∈ R son sus

clases correspondientes, las que se desea predecir. Se dice que el método es lineal si

L(w; x, y) puede ser expresado como una función de wT x y y. Varios de los algoritmos
de clasificación y regresión de spark.mllib caen en esta categoría.

La función objetivo f tiene dos partes: el regularizador, que controla la complejidad

del modelo y la pérdida, que da una medida del error del modelo en los datos de
entrenamiento. La función de pérdida L(w; .) es típicamente una función convexa en
w. El parámetro de regularización ajustado λ ≥ 0 define la relación entre las dos
finalidades de minimizar la pérdida (esto es, el error de entrenamiento) y minimizar
la complejidad del modelo (esto es, evitar el sobreajuste).
MARCO TEÓRICO 19

1.6.2. Funciones de pérdida

La siguiente tabla resume las funciones de pérdida y sus gradientes o subgradientes

para los métodos que spark.mllib soporta:

funciones de pérdida L(w; x, y) gradiente o subgradiente



 −yx si ywT x < 1,
hinge loss max{0, 1 − ywT x}, y ∈ −1, +1

 0 en otro caso.

( )
pérdida logística log(1 + exp(−ywT x)), y ∈ −1, +1 −y 1 − 1
1+exp(−ywT x)
x

pérdida cuadrática 1 T
2 (w x − y)2 , y ∈ R (wT x − y)x

Nota: Para la formulación matemática se han utilizado a conveniencia los valores -1

y +1 para la variable de clase binaria y, el valor de la clase negativa es tratada en


spark.mllib como 0 para ser consecuentes con la clasificación multiclase.

1.6.3. Regularizadores

La finalidad del regularizador es potenciar modelos simples y evitar el sobreajuste.


Los regularizadores suministrados por spark.mllib son:
MARCO TEÓRICO 20

regularizador R(w) gradiente o subgradiente

cero (sin regularizar) 0 0

L2 1
2
∥w∥22 w

L1 ∥w∥1 sign(w)

red elástica α∥w∥1 + (1 − α) 12 ∥w∥22 αsign(w) + (1 − α)w

Donde sign(w) es el vector consistente de los signos (±1) de todas las entradas de w.

Los problemas regularizados con L2 son generalmente más fáciles de resolver que los

regularizados con L1 dado el nivel de refinamiento. No obstante la regularización con


L1 puede ayudar a promover esparcimiento en los pesos conduciendo a modelos más

pequeños y más interpretables lo cual puede ser útil para la selección de rasgos. Red

elástica es una combinación de las regularizaciones L1 y L2. No es recomendable entre-

nar modelos sin ninguna regularización, especialmente cuando el número de ejemplos


de entrenamiento es pequeño.
ASF (2017)
1.7. Optimización

Los métodos lineales utilizan optimizaciones convexas para optimizar las funciones
objetivo. spark.mllib utiliza dos métodos, SGD y L-BFGS. Actualmente, la mayo-

ría de las API de algoritmos soportan Descenso de Gradiente Etocástico (SGD por
sus siglas en inglés), y algunos otros soportan L-BFGS.
MARCO TEÓRICO 21

1.7.1. Descenso de gradiente

El método más simple para resolver problemas de optimización de la forma minw∈Rd f (w)

es el del descenso de gradiente. Estos métodos de optimización de primer orden (in-


cluyendo descenso de gradiente y sus variantes estocásticas) están bien condicionados
para cómputo distribuido y a gran escala.

El método de descenso de gradiente tiene como principal objetivo encontrar un míni-


mo local de una función, tomando iterativamente pasos en la dirección del descenso
más pronunciado, lo cual es el opuesto de la derivada de la función en el punto actual.
Si la función objetivo f no es diferenciable en todos los argumentos pero se mantiene

convexa, entonces un sub-gradiente es la generalización natural para el gradiente y


asume el rol de definir la dirección del paso. En cualquier caso, calcular el gradiente

o el sub-gradiente de f computacionalmente es costoso ya que requiere una pasada


total al dataset completo con el fin de calcular la contribución de todos los términos

de pérdida.

1.7.2. Descenso de gradiente estocástico (SGD)

Los problemas de optimización cuya función objetivo f está escrita como una suma es-

tán particularmente condicionados para ser resueltos utilizando SGD. En aprendizaje

automático supervisado para la formulación se utiliza comúnmente:

1∑ n
f (w) = λR(w) + L(w; xi , yi ). (1.1)
n i=1

esto es especialmente natural, ya que la pérdida total esta escrita como un promedio
de las pérdidas individuales provenientes de cada punto.
Un subgradiente estocástico es una elección aleatoria de un vector, que puede ser
probablemente el subgradiente real de la función objetivo. Tomando un punto i ∈ [1..n]
uniformemente aleatorio, obtenemos un subgradiente estocástico de (1) respecto a w
MARCO TEÓRICO 22

de la siguiente forma:

fw,i = L′w,i + λRw′ ,

donde L′w,i ∈ Rd es un subgradiente de la parte de la función de pérdida determi-


nado por el i-ésimo punto, esto es L′w,i ∈ ∂
∂w
L(w; xi , yi ). Rw′ es un subgradiente del
regularizador R(w), esto es, Rw′ ∈ ∂
∂w
R(w). El término Rw′ no depende de cuál pun-
to aleatorio se halla escogido. Teniendo en cuenta la opción aleatoria escogida de

i ∈ [1..n], tenemos que fw,i es un subgradiente del objetivo original f , lo cual significa

que E[fw,i ]∈ ∂
∂w
f (w).

Ejecutar SGD entonces se convierte en avanzar en la dirección del subgradiente esto-



cástico negativo fw,i , que es:


w(t+1) := w(t) − γfw,i (1.2)

Longitud de paso. El parámetro γ es la longitud de paso, la cual en la implementa-

ción por defecto se escoge decreciente con la raíz cuadrada del contador de iteraciones,
esto es γ := √s en la t-ésima iteración, con el parámetro de entrada s (longitud de
t

paso). La selección de la mejor longitud de paso para el método SGD es delicado en

la práctica y es un tema de investigación activo.ASF (2017)

1.7.3. Esquemas de actualización para SGD distribuido

La implementación de SGD utilizada en GradientDescent utiliza un muestreo simple

(distribuido) de los ejemplos de datos. Recuérdese que la pérdida como parte del
∑n ∑n
problema de optimización (1) es 1
n i=1 L(w; xi , yi ), y por tanto 1
n i=1 L′w,i podría
ser el verdadero subgradiente. Dado que esto requeriría acceso total al dataset, el
parámetro miniBatchFraction especifica cual fracción del total de datos usar en

cambio. El promedio de los gradientes sobre este subconjunto definido como

1 ∑ ′
L ,
|S| i∈S w,i
MARCO TEÓRICO 23

es un gradiente estocástico. S es el subconjunto de muestra, con tamaño |S| =


miniBatchFraction ·n. En cada iteración el muestreo sobre el dataset distribuido
(RDD), así como el cómputo de la suma de los resultados parciales de cada worker es

realizado por las rutinas estándar de Spark.

Si a la fracción de puntos miniBatchFraction se le hace corresponder el valor 1


(original), entonces el paso resultante en cada iteración es el descenso de gradiente
original. En ese caso no hay aleatoriedad ni varianza en las direcciones de paso utiliza-
das. En el otro extremo, si miniBatchFraction se escoge muy pequeño, por ejemplo,

si se toma tal que

|S| = miniBatchFraction ·n = 1, entonces el algoritmo es equivalente al SGD es-


tándar. En este caso, la dirección del paso depende de la uniformidad del muestreo

aleatorio del punto.

1.7.4. BFGS de memoria limitada (L-BFGS)

L-BFGS es un algoritmo de optimización de la familia de los métodos quasi-Newton

para resolver problemas de optimización de la forma minw∈Rd f (w). El método L-BFGS

aproxima localmente la función objetivo como cuadrática, sin evaluar la segunda de-
rivada parcial para construir la matriz Hessiana. La matriz Hessiana se aproxima por
evaluaciones previas del gradiente, por lo que es permisible la escalabilidad vertical

(por número de características de entrenamiento). Como resultado, L-BFGS logra a

menudo convergencia rápida comparado con otras optimizaciones de primer orden.

1.8. Implementación en MLlib

1.8.1. Descenso de gradiente y descenso de gradiente estocástico

Los métodos descenso de gradiente incluyendo descenso de subgradiente estocástico


están incluidos como primitivas de bajo nivel en MLlib sobre las cuales se desarrollan
varios algoritmos de aprendizaje.
MARCO TEÓRICO 24

La clase para SGD GradientDescent establece los siguientes parámetros:

• Gradient es la clase que calcula el gradiente estocástico de la función a opti-


mizar. MLlib incluye clases de gradiente para funciones de pérdida comunes. La
clase gradiente toma como entrada un ejemplo de entrenamiento, su clase y el
valor del parámetro actual.
• Updater es la clase que realiza el paso del descenso, esto es, actualizando los
pesos en cada iteración, dado un gradiente de la componente de pérdida. Este es el
encargado de realizar la actualización de la componente de regularización. MLlib

incluye actualizadores para casos sin regularización, así como regularizadores L1

y L2.
• stepSize es un valor escalar que denota el tamaño inicial del paso para el

descenso del gradiente. Todos los actualizadores en MLlib utilizan un tamaño de



paso en la t-ésima iteración igual a stepSize / t.
• numIterations es el número de iteraciones a realizar.
• regParam es el parámetro de regularización cuando se utiliza regularizaciones
L1 o L2.

• miniBatchFraction es la fracción del total de datos que se muestrea en cada

iteración para el cálculo de la dirección del gradiente.


1.8.2. L-BFGS

L-BFGS constituye actualmente solo una primitiva de optimización de bajo nivel y si se

quiere utilizar en un algoritmo de aprendizaje automático como LogisticRegression


o LinearRegression es necesario pasarle el gradiente de la función objetivo y el ac-
tualizador manualmente en vez de poder usar la API de entrenamiento de la manera
que se hace con LogisticRegressionWithSGD .
El método LBFGS.runLBFGS toma los siguientes parámetros:

• Gradient (ídem a SGD)


MARCO TEÓRICO 25

• Updater es la clase que efectúa el cálculo del gradiente y la pérdida de la función


objetivo de la componente de regularización para L-BFGS.
• numCorrections es el número de correcciones en la actualización de L-BFGS.
Se recomienda 10.
• maxNumIterations es el máximo número de iteraciones que L-BFGS ejecutará.
• regParam es el parámetro usado para la regularización.
• convergenceTol controla que tanto cambio relativo es permitido cuando se dice
que L-BFGS converge. Este debe ser no negativo. Valores pequeños son menos
tolerantes y causan mas iteraciones en la ejecución.

El resultado es una tupla que contiene: como primer elemento, una matriz columna

que contiene los pesos de cada característica y el segundo elemento es un arreglo que
contiene la pérdida calculada para cada iteración.
CAPÍTULO 2
Capítulo 2
IMPLEMENTACIÓN DE UN MODELO DE
REGRESION LINEAL PERSONALIZADO
UTILIZANDO SPARK ML

En orden de agregar nuevas funcionalidades a Spark ML se tienen dos opciones, la pri-

mera es implementar los algoritmos utilizando transformaciones a los RDD siguiendo las
convenciones de Spark MLlib y seguir a partir de ahí, para Spark ML este acercamiento

es válido también pero se pierden características integradas muy útiles, incluyendo la po-

sibilidad de ejecutar meta-algoritmos como búsqueda de parámetros con cross-validación,

la segunda vía es extender el modelo de Spark ML Pipeline.

Para agregar un nuevo algoritmo a una pipeline se necesita implementar ya sea Estimator
o Transformer , las cuales implementan a su vez la interfaz PipelineStage . Para los al-
goritmos que no necesitan entrenamiento, se puede implementar la interfaz Transformer ,

y para algoritmos con entrenamiento la interfaz Estimator , ambas en org.apache.spark.ml .


Holden Karau (2017)

Además de las funciones transform o fit , todas las etapas de una pipeline necesitan
proveer transformSchema y un constructor copy o implementar una clase que realice

esta tarea. copy es utilizado para clonar la etapa actual, con cualquier parámetro adicio-
nal que se desee agregar.

27
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 28

La función transformSchema es la encargada de producir la estructura bajo la cual se


realiza la salida para cualquier conjunto de parámetros y un esquema de entrada. Además
esta función es la encargada de validar si el esquema de entrada es adecuado para la etapa
(por ejemplo, que la columna de entrada sea del tipo esperado).

2.1. Modelo de herencia de clases para la API de Spark ML

La API de Spark puede ser de cierta forma vista desde dos perspectivas la API pública y
la API privada. Estas están delimitadas por el paquete declarado en los archivos de código
donde están implementadas las clases.
Las clases recomendadas para la extensión de Spark ML ( Estimator y Transfomer )

pueden ser heredadas de manera natural desde un proyecto de usuario siempre que se

tengan las respectivas dependencias a la biblioteca estándar de Spark. Por otra parte,

los desarrolladores de Spark implementan las nuevas funcionalidades dentro de los pa-
quetes internos de Spark, en este caso org.apache.spark.ml que al mismo tiempo se

encuentran en un nivel inferior en la jerarquía de clases respecto a las clases Estimator


y Transformer , en lo que se refiere a la extensión del modelo de Pipelines.

Figura 2–1: Herencia asociada a Estimator


Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 29

Figura 2–2: Herencia asociada a Transformer


2.2. Pasos propuestos para el ajuste de un procedimiento de Aprendizaje
Automático al modelo de Pipeline de Spark ML

Dada la homogeneidad estructural presentada por la API de Spark ML, las potencialidades

de desempeño de las estructuras que este utiliza y la creciente comunidad de desarrollado-

res que se encuentran en una intensa labor de extensión de esta API se decidió utilizarla

para el caso de estudio de la inclusión de un algoritmo de aprendizaje automático que

funcionara como vehículo para ejemplificar el proceso de implementación.


Para ello se confeccionaron una serie de pasos generales a seguir para ajustar un algoritmo

que se desee implementar utilizando los beneficios de esta API. Básicamente estos pasos

son:

Definir matemáticamente el algoritmo a implementar.

Redefinir las estructuras y subrutinas asociadas para condicionarlas al modelado dis-


tribuido necesario para la paralelización. Esto es, condicionar la futura implementación
utilizando DataFrame y Dataset de Spark SQL y el paradigma de programación
funcional.
Determinar si el algoritmo necesita entrenamiento o si es una transformación directa
de los datos de entrada.
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 30

En caso de necesitarse entrenamiento, implementar la interfaz Estimator de Spark


ML en la cual el código del entrenamiento debe estar implementado en el método
fit()
Implementar la interfaz Transformer en cuyo método transform() se debe situar
el código de la transformación de los datos.

En caso de requerirse la integración del algoritmo como parte de una Pipeline


se debe tener en cuentra la impementación de los métodos transformSchema() de
ambas interfaces donde se explicita la transformación en la estructura de los datos en
la entrada y salida de esa etapa de la Pipeline

2.3. Descripción matemática del algoritmo implementado

2.3.1. Ecuación normal

Se desea predecir el valor de una variable y mediante su aproximación a la función lineal:


n
h(x) = θi xi = θT x,
i=0

siendo n el número de variables de entrada. Un método utilizado para escoger los paráme-

tros θ es definir una función que estime qué tan cerca están los valores de h(x(i) ) respecto
a los valores correspondientes de y (i) . Sea la función de costo:

1∑ m
J(θ) = (hθ (x(i) ) − y (i) )2 .
2 i=1

Esta función es comúnmente denominada función de costo de mínimos cuadrados. Sea X


la matriz que contiene las instancias de entrenamiento y y el vector de valores de la función
objetivo asociados a estas instancias. Una posible solución al problema de regresión lineal
es utilizar la minimización explicita mediante el uso de la ecuación normal:

θ = (X T X)−1 X T y,

lo que es equivalente a la resolución del sistema lineal:

X T Xθ = X T y
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 31

2.3.2. Redefinición de la multiplicación de matrices para permitir la escala-


bilidad del cómputo paralelo

La resolución de la ecuación normal como método para calcular los valores de los pa-
rámetros θ incluye el cómputo de la multiplicación de matrices. Este procedimiento es
implementado por varias bibliotecas locales de álgebra lineal dada la frecuencia de su uti-
lización. Para un correcto aprovechamiento de las capacidades del cómputo distribuido
en este frecuente procedimiento algebraico se hace necesario plantear la multiplicación de
matrices de manera tal que las matrices puedan ser fraccionadas a lo largo de un clúster
de computadoras así como poder computar porciones de la matriz resultado de manera
distribuida.

Sea:

   
 a1,1 a1,2 · · · a1,m   b1,1 b1,2 ··· b1,n 
   
   
a a2,2 · · · 
a2,m  b b2,2 ··· b2,n 
 2,1  2,1 
An,m =
 . ..
, Bm,n = 

..   . .. .. 

 .. ..  .. ..
 . . .   . . . 
   
   
an,1 an,2 · · · an,m bm,1 bm,2 · · · bm,n

La formulación de la multiplicación con el operador · del elemento (i, j) de la matriz (A·B)

se define como:


(A · B)i,j = ai,k bk,j
k
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 32

La multiplicación completa en una notación exhaustiva luciría así:


 
 a1,1 b1,1 + a1,2 b2,1 + . . . + a1,m bm,1 . . . a1,1 b1,m + a1,2 b2,m + . . . + a1,m bm,m 
 
 
 a b + a b + ... + a b 
 2,1 1,1 2,2 2,1 2,m m,1 . . . a2,1 b1,m + a2,2 b2,m + . . . + a2,m bm,m 
(A · B) = 
 ..
,

 . 
 
 
 
am,1 b1,1 + am,2 b2,1 + . . . + am,m bm,1 . . . am,1 b1,m + am,2 b2,m + . . . + am,m bm,m

Esta formulación puede ser reescrita de la siguiente manera:

     
 a1,1 b1,1 . . . a1,1 b1,m   a1,2 b2,1 . . . a1,2 b2,m   a1,m bm,1 . . . a1,m bm,m 
     
     
(A·B) = 
 a2,1 b1,1 ... a2,1 b1,m  
+ a2,2 b2,1 ... a2,2 b2,m +. . .+

 a2,m bm,1 ... a2,m bm,m 

     
     
am,1 b1,1 . . . am,1 b1,m am,2 b2,1 . . . am,2 b2,m am,m bm,1 . . . am,m bm,m

Si utilizamos la definición de producto tensorial ⊗ la suma anterior puede ser escrita como:


(A · B) = ai ⊗ b⃗i )
(⃗
i

Golub and van Loan (1996)

Esta formulación nos permite realizar un fraccionamiento de las matrices operando de


manera que estas pueden ser distribuidas en un clúster de computadoras pero acarrea la
ai , b⃗i ) para aplicar el operador ⊗ mediante la operación
inconveniente de generar los pares (⃗

map y reducir los accesos a los datos por lo que el almacenamiento necesario para una

correcta paralelización es mucho mayor.


En el caso de la multiplicación X T X presente en la ecuación normal se pueden aprovechar
varias bondades. Dado que la multiplicación es de la matriz por ella misma, no es necesario
generar los pares (⃗
ai , a⃗i ) por lo que es menos costoso en cuestiones de espacio de almacena-
ai ⊗ a⃗i ) = a⃗i T · a⃗i es
miento. La otra particularidad de esta multiplicación es que dado que (⃗
una matriz simétrica, no es necesario calcular todos los elementos, sino solo los elementos
de la triangular inferior y se disminuyen significativamente los cálculos necesarios.
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 33

2.4. Características y diseño general de la implementación

La implementación del algoritmo se realizó siguiendo el modelo de programación de la API


de Spark ML basada en Pipelines. El lenguaje de programación escogido fue Scala versión
2.11.8 incluida dentro de la biblioteca de Spark versión 2.0.1. Para las tareas locales de
álgebra lineal se utilizó la biblioteca LAPACK incluida en esa versión de Spark. El IDE
utilizado para la implementación fue IntelliJ IDEA version 2016.3. La versión de Java
utilizada por Scala en esta implementación fue la 8 actualización 91.

2.4.1. Principales clases y funciones implementadas

Clase Instance

Representa una instancia de entrenamiento que contiene un atributo label que representa

la clase de esa instancia y un atributo features que almacena el vector de características

de la instancia.

Clase CustomLinearRegression

Esta case hereda de la clase Estimator de Spark ML. Es la encargada de construir

un modelo a partir de las instancias de entrenamiento. En este caso el modelo construi-

do es un modelo de regresión lineal utilizando la ecuación normal. El principal método


implementado en esta clase es fit el cual es el encargado de construir el modelo apren-

dido a partir de las instancias, recibe como parámetro un Dataset de Spark SQL con los
datos de entrenamiento.
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 34

Pseudocódigo de la función fit

Entrada: dataset . Objeto que contiene las instancias de entrenamiento.

Salida: Objeto de CustomLinearRegressionModel que contiene el vector θ de coefi-


cientes para evaluar la función lineal.
1. trainInstances ← dataset.select("features", "label")
2. l2RegularizationParam ← 0.1
3. Calcular ambos miembros de la ecuación normal X T Xθ = X T y.
4. Aplicar regularización L2 sobre el miembro X T Xθ.
5. Resolver el sistema lineal X T Xθ = X T y sobre la variable θ.

6. Devolver una nueva instancia de CustomLinearRegressionModel con el valor del

vector θ.

Clase CustomLinearRegressionModel

Esta clase hereda de la clase Model de Spark ML. Es la encargada de representar el


modelo aprendido, así como de contener las funcionalidades para transformar los datos

de entrada y predecir características para nuevas instancias. Concretamente esta clase

contiene un método transform que recibe un Dataset de Spark SQL como datos de

entrada, en este caso, instancias para las cuales se desea predecir el valor de la clase me-
diante la evaluación de una función lineal que utiliza los valores de las características y
devuelve un DataFrame de Spark SQL que contiene los valores arrojados por la predicción.
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 35

Pseudocódigo de la función transform

Entrada: Dataset que contiene las instancias para las cuales se desea predecir la
variable continua y de la clase a partir del vector de características de cada una y el

vector de coeficientes θ aprendido.


Salida: DataFrame que contiene las instancias de entrada con la columna adicional de
valores arrojados por la evaluación de la función lineal para cada una de esas instancias.
1. instances ← dataset.select("features", "label")
2. Evaluar la función lineal mediante el cálculo de θT ⃗x para cada instancia.
3. Agregar la columna de valores arrojados por la predicción al dataset de entrada.

4. Devolver el nuevo dataset en forma de DataFrame

También se implementaron varias funciones necesarias para las tareas de cálculo algebraico

y estadísticas sobre los resultados.

Función rmse

Esta función es la encargada de calcular el error cuadrático medio de la estimación reali-

zada por la función lineal respecto a los valores reales de la variable y para instancias de

las cuales se conoce este dato. De manera que esta medida de error puede ser utilizada

para valorar qué tan acertada está la estimación.

Función outerVecProduct(v: Vector)

Esta función es la encargada de calcular el producto externo de un vector por si mismo,


concretamente la operación ⃗a ⊗ ⃗a. La implementación de este método solo calcula los ele-
metos de la matriz triangular asociada al resultado, dado que es una matriz simétrica y
devuelve esta matriz en representación vectorial por filas.
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 36

Función vecAdd(v1: Vector, v2: Vector)

Esta función realiza la suma de dos vectores

Función vecScale(v1: Vector, s: Double)

Realiza la multiplicación de un vector por un valor escalar.

Es importante resaltar que al realizarse la implementación con Scala, que es un lengua-


je de programación funcional las estructuras para la representación de colecciones como

vectores y matrices, así como de las estructuras distribuidas de Spark son inmutables e
implementan eficientemente el paradigma map-reduce. En el caso de la implementación de
las estructuras de Spark este modelo de programación permite la paralelización implícita

por lo que las operaciones de la naturaleza map están implementadas para ser escalables

por definición.

Como consecuencia de haberse implementado este algoritmo siguiendo el modelo de pro-

gramación de Spark ML este está condicionado para ser integrado como parte de una

Pipeline y ser utilizado como parte de una cadena de procesamientos sucesivos de datos.
Implementación de un modelo de Regresion Lineal personalizado utilizando Spark ML 37

2.5. Diagramas de clases asociados a CustomLinearRegression y


CustomLinearRegressionModel

Figura 2–3: Diagrama asociado a CustomLinearRegression

Figura 2–4: Diagrama asociado a CustomLinearRegressionModel


CAPÍTULO 3
Capítulo 3
HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN
DE LOS RESULTADOS

3.1. Preparación del ambiente de desarrollo

En esta sección se describen los pasos a seguir para la creación de la aplicación final
utilizando Spark mediante su API en Scala, así como su preparación para la ejecución en
un clúster.

3.1.1. Creación de un proyecto de Scala con Spark en el IDE IntelliJ IDEA

Pasos a seguir para la creación del proyecto en IntelliJ IDEA:

1. Descargar el IDE IntelliJ IDEA de su sitio web oficial.


2. Descargar el plugin de Scala asociado a la versión del IDE desde la misma web.

3. Descargar Spark desde su sitio web oficial.


4. Extraer el contenido de Spark en el directorio de trabajo.

5. Instalar el IDE.
6. Agregar el plugin de Scala mediante la opción: File >Settings >Plugins >Install plugin
from disk
7. Crear un proyecto nuevo de Scala.
8. Agregar las dependencias externas de Spark mediante la opción: File >Project Struc-
ture >Libraries >+ >Seleccionar los archivos .jar del directorio jars de Spark excluyendo
los relacionados con Scala
9. Agregar el SDK de Scala en otra biblioteca mediante los pasos anteriores utilizando
los archivos relacionados con Scala.
10. Asociar la biblioteca de Scala como el SDK de Scala del proyecto actual.

39
HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 40

Luego de la implementación de la aplicación y realización de pruebas bajo el ambiente


local esta puede ser preparada para su ejecución en un clúster de computadoras.

3.1.2. Compilación y creación del archivo .jar para ser lanzado en un clúster
de Spark mediante la herramienta spark-submit

Una manera práctica de ejecutar la aplicación en un clúster de Spark es mediante la he-


rramienta spark-submit contenida en las distribuciones estándar de Spark, para esto se
hace necesario crear el archivo .jar ejecutable y copiarlo hacia el directorio de trabajo del
clúster de computadoras.

Para la creación del archivo .jar en el IDE IntelliJ IDEA es necesario crear un artefacto
de compilación mediante el menú: File >Project Structure >Artifacts >+ >Crear un ar-

tefacto de compilación de tipo jar, excluir las dependencias. Es importante resaltar que

no se deben incluir las dependencias locales de Spark pues la aplicación debe utilizar la

distribución de Spark instalada previamente en el clúster y que esta se desempeñe correc-

tamente bajo este ambiente. Luego de ejecutar el artefacto de compilación se obtiene el


archivo .jar que debe ser copiado hacia el clúster, en el caso de esta investigación se utilizó

la herramienta WinSCP para esta tarea.

3.1.3. Carga de los datasets hacia el clúster de Spark con HDFS y ejecución
del programa en el clúster

Para almacenar los datasets en el sistema de archivos distribuido de Hadoop se debe prime-
ramente copiar el dataset a un directorio local en el clúster, esto puede hacerse mediante
la herramienta WinSCP, luego que el dataset esté completamente copiado hacia ese direc-
torio se debe utilizar el comando hadoop fs -copyFromLocal <localFile><HDFS-dir>

donde localFile es la ruta absoluta del dataset almacenado localmente el el clúster y


HDFS-dir es la ruta al directorio donde está montado el sistema de archivos distribuido
HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 41

de Hadoop.

Para el lanzamiento de la aplicación en el clúster se utilizó la herramienta spark-sumbit .

Los parámetros utilizados fueron:

–-master yarn Para especificar que se desea utilizar YARN como manejador de
clúster.
–-driver-memory 4096m para especificar el tamaño de memoria de la JVM del pro-
grama driver en este caso 4 GiB.
–-executor-memory 4096m análogamente para la memoria en los ejecutores.
–-num-executors 3 para especificar la cantidad de ejecutores a utilizar.
–-executor-cores 3 para especificar la cantidad de núcleos en cada ejecutor, en

este caso 3 núcleos virtuales.

A manera de resumen el comando sería, por ejemplo:


spark-sumbit –-master yarn

–-num-executors 7
–-driver-memory 1024m

–-executor-memory 1024m
–-executor-cores 2 ejemplo.jar

Donde ejemplo.jar es el ejecutable del programa que se sea lanzar.

3.2. Pruebas preliminares

3.2.1. Pruebas realizadas al dataset E2006.train

Csie.ntu.edu.tw (2017)
Características del dataset:

Tamaño en disco: 485 MB.

Número de instancias: 16087 / 3308 (test)


HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 42

Número de características: 150360


Localización:
https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression.html

Prueba 1.1:
Historial de eventos en el clúster de Spark:

Figura 3–1: Algoritmo de regresión lineal personalizado usando ecuación normal

Figura 3–2: Algoritmo de regresión lineal contenido en Spark

Observaciones: El algoritmo personalizado implementado utiliza la solución por ecua-


ción normal sin distinguir el tamaño de los datos de entrada. En el cálculo de los términos
HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 43

de la ecuación normal de esta implementación se generan grandes cantidades de datos ya


que se calcula X T X y X T y lo cual tiene un alto costo en almacenamiento causando que
la memoria de trabajo se use excesivamente y el recolector de basura de las JVM tome
demasiado tiempo en reusar la memoria causando una falla en el proceso, específicamente
el error es del tipo java.lang.OutOfMemoryError: GC overhead limit exceeded . La
2
complejidad computacional del almacenamiento utilizado es del orden O( n2 +n) para cada
instancia, siendo n la dimension de los vectores de características. Una posible solución
sería aumentar el tamaño del montículo de las JVM de acuerdo al tamaño de la instancia
del problema, lo cual está limitado por la capacidad del clúster utilizado. El valor utili-

zado por los desarrolladores de Spark para definir el tamaño máximo de los vectores de

características permisibles es 4096, garantizándose así la viabilidad de este método. En la


clase WeightedLeastSquares de Spark ML se puede observar el siguiente comentario de

los desarrolladores:

Figura 3–3: Detalle del algoritmo de regresión lineal contenido en Spark

El algoritmo de regresión lineal contenido en Spark tiene en cuenta el tamaño de los datos
de entrada y toma decisiones de solución diferentes (Normal equation, SGD, L-BFGS), en

este algoritmo se utiliza una optimización en las operaciones reduce más costosas, esta

operación denominada treeAggregate es una generalización de la operación reduce y


su propósito es disminuir la carga en memoria y procesamiento causada por la agregación
masiva hacia el programa driver, en este caso los datos se van agregando parcialmente en
forma de árbol a lo largo de todas sus particiones, de manera que el proceso de agregación
es colaborativo entre los nodos.
HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 44

Figura 3–4: Diferencias entre Aggregate y Tree Aggregate.


Prueba 1.2:
Nota: Se modificó la operación reduce donde se encontraba el mayor costo computacional,

esta vez se implementó utilizando treeAgreggate como intento de optimización para no so-

brecargar el programa driver. Esta implementacion se ejecutó utilizando el mismo dataset


que la Prueba 1.1 con el objetivo de reafirmar que el causante del fallo es la complejidad

espacial de la operación y no la carga de procesamiento por agregación masiva.

Historial de eventos en el clúster de Spark:

Figura 3–5: Algoritmo de regresión lineal personalizado usando ecuación normal


Observaciones: El algoritmo sufre el mismo fallo que en la prueba 1.
HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 45

3.2.2. Pruebas realizadas al dataset YearPredictionMSD

Csie.ntu.edu.tw (2017)

Características del dataset:

Tamaño en disco: 553 MB.


Número de instancias: 463715 / 51630 (test)
Número de características: 90

Localización:
https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression.html

Prueba 1.3:

Historial de eventos en el clúster de Spark:

Figura 3–6: Algoritmo de regresión lineal personalizado usando ecuación normal

Figura 3–7: Algoritmo de regresión lineal contenido en Spark


HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 46

Observaciones: En esta prueba se utilizó un dataset de mayor tamaño en disco. Presenta


vectores de características de menor tamaño que las pruebas anteriores pero muchas más
instancias de entrenamiento. El algoritmo resuelve eficientemente el cómputo solicitado.
Los tiempos de ejecución tanto de la implementación personalizada como la contenida en
Spark son similares.

3.3. Pruebas de escalabilidad

3.3.1. Características del clúster utilizado:

El clúster utilizado para las pruebas presenta las siguientes prestaciones:

Cantidad de Nodos: 10

Versión de Spark: 2.1.0


Manejador de clúster: YARN

Configuración de YARN:
Nodo:

Memoria reservada para todos los contenedores de YARN en un nodo: 12GB

CPU:

Porciento de CPU físico para todos los contenedores en un nodo: 80 %


Número de núcleos virtuales: 3
Contenedor:

Tamaño de memoria los contenedores: Mínimo 1024 MB; Máximo 12 GB


Tamaño de los contenedores (CPU): Mínimo 1; Máximo 3 (Núcleos virtuales)
Tamaño de montículo de las JVM: 3064 MB
HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 47

3.3.2. Pruebas sobre el dataset YearPredictionMSD:

Todas las pruebas se realizaron utilizando la herramienta spark-submit con los siguien-
tes parámetros en común:

–-master yarn
–-driver-memory 4098m

–-executor-memory 4098m
–-executor-cores 3

Figura 3–8: Historial de eventos con 1 ejecutor

Figura 3–9: Historial de eventos con 2 ejecutores


HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 48

Figura 3–10: Historial de eventos con 3 ejecutores

Figura 3–11: Historial de eventos con 4 ejecutores

Figura 3–12: Historial de eventos con 5 ejecutores


HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 49

Figura 3–13: Historial de eventos con 6 ejecutores

Figura 3–14: Historial de eventos con 7 ejecutores


HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 50

Figura 3–15: Historial de eventos con 8 ejecutores

Figura 3–16: Historial de eventos con 9 ejecutores


HERRAMIENTAS, TÉCNICAS Y VALIDACIÓN DE LOS RESULTADOS 51

Figura 3–17: Historial de eventos con 10 ejecutores

Figura 3–18: Tiempos de ejecución de las pruebas

Figura 3–19: Escalabilidad del algoritmo personalizado


CONCLUSIONES Y
RECOMENDACIONES
CONCLUSIONES

Las ideas que a continuación se expondrán resultan una síntesis de los aspectos fundamen-
tales trabajados en la investigación.

El estudio de la arquitectura de Spark permitió reconocer las prácticas seguidas por


sus desarrolladores en el proceso de construcción, así como las principales ventajas pro-
porcionadas por la API, específicamente en los módulos relacionados con aprendizaje
automático, profundizándose el conocimiento sobre este framework.

Se desarrolló un procedimiento general para guiar el proceso de implementación de

nuevos algoritmos de aprendizaje automático sobre Spark ML, utilizando su modelo

de programación, lo cuál permite extender las funcionalidades brindadas por esta

herramienta.
Se llevó a cabo la implementación de un algoritmo de regresión lineal, utilizando la

ecuación normal, siguiendo los pasos propuestos con el fin de su validación.

La etapa de pruebas al algoritmo implementado permitió observar varios resultados

favorables, entre ellos:


• Las capacidades del software de Apache Spark en el manejo de grandes volúmenes
de datos en poco tiempo.
• El aprovechamiento por parte del algoritmo implementado de las ventajas de la
plataforma subyacente.

• La escalabilidad del software implementado mediante los pasos propuestos y el


modelo de programación de la API de Apache Spark, observándose una reducción
de los tiempo de ejecución a medida que aumentan las prestaciones del hardware
disponible en un clúster de computadoras.

53
RECOMENDACIONES

Para darle continuidad y perfeccionar esta investigación se proponen las siguientes reco-
mendaciones:

Vigilar la evolución del desarrollo del framework Apache Spark.


Desarrollar otros algoritmos de aprendizaje automático siguiendo los pasos propuestos
en este trabajo.

54
REFERENCIAS BIBLIOGRÁFICAS

ASF (2017). Apache Spark Official Website. https://spark.apache.org/docs/2.1.0/


ml-guide.html. [Online; accessed 19-May-2017].
Csie.ntu.edu.tw (2017). Libsvm Official Website. http://www.csie.ntu.edu.tw/~cjlin/
libsvmtools/datasets/ref.html#SK09a. [Online; accessed 8-Jun-2017].
Golub, Gene H. and Charles F. van Loan (1996). Matrix computations. 3rd ed.. Johns
Hopkins University Press.
Hastie, Trevor, Robert Tibshirani and Jerome Friedman (2003). The Elements of Statistical

Learning: Data Mining, Inference, and Prediction. corrected ed.. Springer.


Holden Karau (2017). Extend Spark ML for your own mo-

del/transformer types. https://www.oreilly.com/learning/


extend-spark-ml-for-your-own-modeltransformer-types. [Online; accessed

8-May-2017].
Karau, Warren (2017). High Performance Spark. O’REILLY.

Pentreath. (2015). Machine Learning with Spark. PACKT.


Ryza, Laserson, Owen and Wills (2015). Advanced Analytics with Spark. O’REILLY.

55

También podría gustarte