C贸mo hacer Traducci贸n Autom谩tica con RNNs (Red Neuronal Recurrente)

Translation into Spanish of an interesting article by Thomas Tracey, an American engineering & product leader focused on enterprise ML, who shows us here how to build a Recurrent Neural Network step by step.

machine translationrnntranslation into spanish
23 May, 2022 Building a recurrent Neural Network for Machine Translation
23 May, 2022 Building a recurrent Neural Network for Machine Translation

A free translation by Chema, a Spain-based translator specializing in English to Spanish translations with a focus on technology, machine learning and machine translation.

An original text written by Thomas Tracey, originally published in
https://towardsdatascience.com/language-translation-with-rnns-d84d43b40571

* * *

C贸mo crear una red neuronal recurrente que traduzca del Ingl茅s al Franc茅s

Esta publicaci贸n explora mi trabajo en el聽proyecto final聽del programa聽Udacity Artificial Intelligence Nanodegree.聽Mi objetivo es ayudar a otros estudiantes y profesionales que se encuentren en las primeras fases de su desarrollo en el aprendizaje autom谩tico (ML) y la inteligencia artificial (IA).

Dicho esto, tenga en cuenta que soy product manager de profesi贸n (no tengo el t铆tulo de ingeniero o cient铆fico de datos). Lo que sigue pretende ser una explicaci贸n semi-t茅cnica pero accesible de los conceptos y algoritmos de ML. Si algo de lo que sigue es inexacto o si tiene ud. comentarios constructivos, no dude en contactarme.

Puede encontrar mi repositorio de Github aqu铆 y el repositorio fuente original de Udacity, aqu铆.

Objetivo del proyecto

En este proyecto construyo una red neuronal profunda como parte de un pipeline de traducci贸n autom谩tica. El pipeline acepta texto en Ingl茅s como entrada y devuelve su traducci贸n al Franc茅s. El objetivo es lograr la mayor precisi贸n de traducci贸n posible.

Por qu茅 es importante la Traducci贸n Autom谩tica

La capacidad de comunicarse entre s铆 es una parte fundamental inherente al ser humano. Hay casi 7.000 idiomas diferentes en todo el mundo. A medida que nuestro mundo se vuelve cada vez m谩s conectado, la traducci贸n de idiomas proporciona un puente cultural y econ贸mico fundamental entre personas de diferentes pa铆ses y grupos 茅tnicos. Algunos de los casos de uso m谩s obvios incluyen:

  • negocios : comercio internacional, inversiones, contratos, finanzas
  • comercio : viajes, compra de bienes y servicios extranjeros, atenci贸n al cliente
  • medios : acceder a la informaci贸n a trav茅s de la b煤squeda, compartir informaci贸n a trav茅s de las redes sociales, localizaci贸n de contenido y publicidad
  • educaci贸n : intercambio de ideas, colaboraci贸n, traducci贸n de trabajos de investigaci贸n
  • gobierno : relaciones exteriores, negociaci贸n

Para satisfacer estas necesidades, las empresas de tecnolog铆a est谩n invirtiendo mucho en la traducci贸n autom谩tica.聽Esta inversi贸n y los avances recientes en el aprendizaje profundo han producido importantes mejoras en la calidad de la traducci贸n.聽Seg煤n Google,聽cambiar al aprendizaje profundo produjo un aumento del 60% en la precisi贸n de la traducci贸n 聽en comparaci贸n con el enfoque basado en frases que se usaba anteriormente en聽Google Translate. Hoy, Google y Microsoft pueden traducir m谩s de 100 idiomas diferentes y se est谩n acercando a la precisi贸n del nivel humano en muchos de ellos.

Sin embargo, aunque la traducci贸n autom谩tica ha progresado mucho, a煤n no es perfecta. 馃槵

驴Mala traducci贸n o carnivorismo extremo?

Enfoque para este proyecto

Para traducir un texto de Ingl茅s a Franc茅s necesitamos construir una red neuronal recurrente (RNN). Antes de sumergirnos en la implementaci贸n, primero hagamos una introducci贸n sobre las RNN y por qu茅 son 煤tiles para tareas de NLP.

Descripci贸n general de RNN

Las RNN est谩n dise帽adas para tomar secuencias de texto como input, devolver secuencias de texto como output, o ambas cosas. Se denominan recurrentes porque las capas ocultas de la red tienen un bucle en el que el output y el estado de la celda de cada paso de tiempo se convierten en input en el siguiente paso del proceso. Esta recurrencia funciona como una especie de memoria. Permite que la informaci贸n contextual fluya a trav茅s de la red para que los resultados relevantes de los procesos anteriores se puedan aplicar a las operaciones de la red en el siguiente proceso.

Esto es an谩logo a nuestro procedimiento de lectura. Mientras lee esta publicaci贸n, est谩 almacenando informaci贸n importante de palabras y oraciones anteriores y us谩ndola como contexto para comprender cada palabra y oraci贸n nueva.

Otros tipos de redes neuronales no pueden hacer esto (todav铆a). Imagine que est谩 utilizando una red neuronal convolucional (CNN) para realizar la detecci贸n de objetos en una pel铆cula. Actualmente, no hay forma de que la informaci贸n de los objetos detectados en escenas anteriores informe la detecci贸n de objetos del modelo en la escena actual. Por ejemplo, si se detectaron una sala de audiencias y un juez en una escena anterior, esa informaci贸n podr铆a ayudar a clasificar correctamente el mazo del juez en la escena actual, en lugar de clasificarlo err贸neamente como un martillo o un mazo. Pero las CNN no permiten que este tipo de contexto de serie temporal fluya a trav茅s de la red como lo hacen las RNN.

Configuraci贸n RNN

Dependiendo del caso de uso, querr谩 configurar su RNN para manejar entradas y salidas de diversas formas. Para este proyecto, utilizaremos un proceso de muchos a muchos en el que la entrada es una secuencia de palabras en ingl茅s y la salida es una secuencia de palabras en franc茅s (la cuarta desde la izquierda en el siguiente diagrama).

Diagrama de diferentes tipos de secuencias RNN. Cr茅dito: Andrej Karpathy

Cada rect谩ngulo es un vector y las flechas representan funciones (por ejemplo, multiplicaci贸n de matrices). Los vectores de entrada est谩n en rojo, los vectores de salida est谩n en azul y los vectores verdes mantienen el estado de RNN (m谩s sobre esto pronto).

De izquierda a derecha: (1) Modo de procesamiento est谩ndar sin RNN, desde entrada de tama帽o fijo hasta salida de tama帽o fijo (por ejemplo, clasificaci贸n de im谩genes). (2) Salida de secuencia (por ejemplo, el subt铆tulo de imagen toma una imagen y genera una oraci贸n de palabras). (3) Entrada de secuencia (por ejemplo, an谩lisis de sentimiento en el que una oraci贸n dada se clasifica como expresi贸n de sentimiento positivo o negativo). (4) Entrada de secuencia y salida de secuencia (por ejemplo, traducci贸n autom谩tica: un RNN lee una oraci贸n en ingl茅s y luego emite una oraci贸n en franc茅s). (5) Entrada y salida de secuencia sincronizada (por ejemplo, clasificaci贸n de video donde deseamos etiquetar cada cuadro del video). Tenga en cuenta que en todos los casos no hay restricciones preespecificadas en las secuencias de longitudes porque la transformaci贸n recurrente (verde) es fija y se puede aplicar tantas veces como queramos.

鈥 Andrej Karpathy, La eficacia irrazonable de las redes neuronales recurrentes

Construyendo el pipeline

A continuaci贸n se muestra un resumen de los diversos pasos de preprocesamiento y modelado. Los pasos de alto nivel incluyen:

  1. Preprocesamiento : carga y examen de datos, limpieza, tokenizaci贸n, relleno
  2. Modelado : construir, entrenar y probar el modelo
  3. Predicci贸n : genere traducciones espec铆ficas del ingl茅s al franc茅s y compare las traducciones de salida con las traducciones de verdad del terreno
  4. Iteraci贸n : iterar sobre el modelo, experimentando con diferentes arquitecturas

Para obtener un tutorial m谩s detallado que incluya el c贸digo fuente, consulte el cuaderno de Jupyter Notebook en el repositorio del proyecto .

Frameworks

Usamos Keras para el frontend y TensorFlow para el backend en este proyecto. Prefiero usar Keras sobre TensorFlow porque la sintaxis es m谩s simple, lo que hace que la creaci贸n de capas del modelo sea m谩s intuitiva. Sin embargo, hay una compensaci贸n con Keras ya que pierde la capacidad de realizar personalizaciones detalladas. Pero esto no afectar谩 los modelos que estamos construyendo en este proyecto.

Preprocesamiento

Cargar y examinar datos

Aqu铆 hay una muestra de los datos. Los inputs son oraciones en Ingl茅s; los outputs son las correspondientes traducciones en Franc茅s.

Cuando ejecutamos un recuento de palabras, podemos ver que el vocabulario para el conjunto de datos es bastante peque帽o. Se hizo as铆 para este proyecto, para poder entrenar los modelos en un tiempo razonable.

Limpieza

No es necesario realizar una limpieza adicional en este punto. Los datos ya se convirtieron a min煤sculas y se dividieron para que haya espacios entre todas las palabras y la puntuaci贸n.

Nota : para otros proyectos de NLP, es posible que deba realizar pasos adicionales como: eliminar etiquetas HTML, eliminar palabras vac铆as, eliminar puntuaci贸n o convertir a representaciones de etiquetas, etiquetar las partes del discurso o realizar la extracci贸n de entidades.

Tokenizaci贸n

A continuaci贸n, necesitamos tokenizar los datos, es decir, convertir el texto a valores num茅ricos. Esto permite que la red neuronal realice operaciones en los datos de entrada. Para este proyecto, cada palabra y signo de puntuaci贸n recibir谩 una identificaci贸n 煤nica. (Para otros proyectos de NLP, podr铆a tener sentido asignar a cada personaje una identificaci贸n 煤nica).

Cuando ejecutamos el tokenizador, crea un 铆ndice de palabras, que luego se usa para convertir cada oraci贸n en un vector.

Relleno

Cuando alimentamos nuestras secuencias de ID de palabras en el modelo, cada secuencia debe tener la misma longitud. Para lograr esto, se agrega relleno a cualquier secuencia que sea m谩s corta que la longitud m谩xima (es decir, m谩s corta que la oraci贸n m谩s larga).

Codificaci贸n One-Hot (no utilizada)

En este proyecto, nuestras secuencias de entrada ser谩n un vector que contiene una serie de n煤meros enteros. Cada n煤mero entero representa una palabra en ingl茅s (como se ve arriba). Sin embargo, en otros proyectos, a veces se realiza un paso adicional para convertir cada n煤mero entero en un vector codificado one-hot. No usamos codificaci贸n one-hot (OHE) en este proyecto, pero ver谩 referencias a ella en ciertos diagramas (como el que se muestra a continuaci贸n). Simplemente no quer铆a que te confundieras.

Una de las ventajas de OHE es la eficiencia, ya que puede funcionar a una velocidad de reloj m谩s r谩pida que otras codificaciones . La otra ventaja es que OHE representa mejor los datos categ贸ricos donde no existe una relaci贸n ordinal entre diferentes valores. Por ejemplo, supongamos que clasificamos a los animales como mam铆feros, reptiles, peces o aves. Si los codificamos como 1, 2, 3, 4 respectivamente, nuestro modelo puede suponer que existe un orden natural entre ellos, lo cual no existe. No es 煤til estructurar nuestros datos de manera que los mam铆feros est茅n antes que los reptiles y as铆 sucesivamente. Esto puede inducir a error a nuestro modelo y provocar resultados deficientes. Sin embargo, si luego aplicamos una codificaci贸n one-hot a estos enteros, cambi谩ndolos a representaciones binarias (1000, 0100, 0010, 0001 respectivamente), entonces el modelo no puede inferir ninguna relaci贸n ordinal.

Pero, uno de los inconvenientes de OHE es que los vectores pueden volverse muy largos y dispersos. La longitud del vector est谩 determinada por el vocabulario, es decir, el n煤mero de palabras 煤nicas en su corpus de texto. Como vimos en el paso de examen de datos anterior, nuestro vocabulario para este proyecto es muy peque帽o: solo 227 palabras en ingl茅s y 355 palabras en franc茅s. En comparaci贸n, el Oxford English Dictionary tiene 172.000 palabras . Pero, si incluimos varios nombres propios, tiempos verbales y argot, podr铆a haber millones de palabras en cada idioma. Por ejemplo, word2vec de Google est谩 entrenado en un vocabulario de 3 millones de palabras 煤nicas. Si us谩ramos OHE en este vocabulario, el vector para cada palabra incluir铆a un valor positivo (1) rodeado por 2,999,999 ceros.

Y, dado que estamos usando incrustaciones (en el siguiente paso) para codificar a煤n m谩s las representaciones de palabras, no necesitamos molestarnos con OHE. Cualquier ganancia de eficiencia no vale la pena en un conjunto de datos tan peque帽o.

Modelado

Primero, analicemos la arquitectura de un RNN a un alto nivel. Con referencia al diagrama anterior, hay algunas partes del modelo que debemos tener en cuenta:

  1. entradas _ Las secuencias de entrada se introducen en el modelo con una palabra para cada paso de tiempo. Cada palabra se codifica como un entero 煤nico o un vector codificado en caliente que se asigna al vocabulario del conjunto de datos en ingl茅s.
  2. Incrustaci贸n de capas . Las incrustaciones se utilizan para convertir cada palabra en un vector. El tama帽o del vector depende de la complejidad del vocabulario.
  3. Capas recurrentes (codificador) . Aqu铆 es donde el contexto de los vectores de palabras en pasos de tiempo anteriores se aplica al vector de palabras actual.
  4. Capas Densas (Decodificador) . Estas son capas t铆picas totalmente conectadas que se utilizan para decodificar la entrada codificada en la secuencia de traducci贸n correcta.
  5. Salidas . Los resultados se devuelven como una secuencia de n煤meros enteros o vectores codificados en caliente que luego se pueden asignar al vocabulario del conjunto de datos en franc茅s.

Incrustaciones

Las incrustaciones nos permiten capturar relaciones de palabras sint谩cticas y sem谩nticas m谩s precisas. Esto se logra proyectando cada palabra en un espacio n-dimensional. Las palabras con significados similares ocupan regiones similares de este espacio; cuanto m谩s cerca est谩n dos palabras, m谩s similares son. Y, a menudo, los vectores entre palabras representan relaciones 煤tiles, como g茅nero, tiempo verbal o incluso relaciones geopol铆ticas.

Cr茅dito de la foto: Chris Bail

El entrenamiento de incrustaciones en un gran conjunto de datos desde cero requiere una gran cantidad de datos y computaci贸n. Entonces, en lugar de hacerlo nosotros mismos, normalmente usamos un paquete de incrustaciones previamente entrenado como GloVe o word2vec . Cuando se usan de esta manera, las incrustaciones son una forma de transferencia de aprendizaje . Sin embargo, dado que nuestro conjunto de datos para este proyecto tiene un vocabulario peque帽o y una variaci贸n sint谩ctica baja, usaremos Keras para entrenar las incrustaciones nosotros mismos.

Codificador y decodificador

Nuestro modelo de secuencia a secuencia vincula dos redes recurrentes: un codificador y un decodificador. El codificador resume la entrada en una variable de contexto, tambi茅n llamada estado. Luego, este contexto se decodifica y se genera la secuencia de salida.

Cr茅dito de la imagen: Udacity

Dado que tanto el codificador como el decodificador son recurrentes, tienen bucles que procesan cada parte de la secuencia en diferentes pasos de tiempo. Para imaginar esto, es mejor desenrollar la red para que podamos ver lo que sucede en cada paso de tiempo.

En el siguiente ejemplo, se necesitan cuatro pasos de tiempo para codificar toda la secuencia de entrada. En cada paso de tiempo, el codificador “lee” la palabra de entrada y realiza una transformaci贸n en su estado oculto. Luego pasa ese estado oculto al siguiente paso de tiempo. Tenga en cuenta que el estado oculto representa el contexto relevante que fluye a trav茅s de la red. Cuanto mayor sea el estado oculto, mayor ser谩 la capacidad de aprendizaje del modelo, pero tambi茅n mayores los requisitos de c谩lculo. Hablaremos m谩s sobre las transformaciones dentro del estado oculto cuando cubramos las unidades recurrentes cerradas (GRU).

Cr茅dito de la imagen: versi贸n modificada de Udacity

Por ahora, observe que para cada paso de tiempo despu茅s de la primera palabra de la secuencia hay dos entradas: el estado oculto y una palabra de la secuencia. Para el codificador, es la siguiente palabra en la secuencia de entrada. Para el decodificador, es la palabra anterior de la secuencia de salida.

Adem谩s, recuerde que cuando nos referimos a una “palabra”, en realidad nos referimos a la representaci贸n vectorial de la palabra que proviene de la capa de incrustaci贸n.

Aqu铆 hay otra forma de visualizar el codificador y el decodificador, excepto con una secuencia de entrada en mandar铆n.

Cr茅dito de la imagen: xiandong79.github.io

Capa Bidireccional

Ahora que entendemos c贸mo fluye el contexto a trav茅s de la red a trav茅s del estado oculto, demos un paso m谩s y permitamos que ese contexto fluya en ambas direcciones. Esto es lo que hace una capa bidireccional.

En el ejemplo anterior, el codificador solo tiene un contexto hist贸rico. Pero proporcionar un contexto futuro puede dar como resultado un mejor rendimiento del modelo. Esto puede parecer contradictorio con la forma en que los humanos procesan el lenguaje, ya que solo leemos en una direcci贸n. Sin embargo, los humanos a menudo requieren un contexto futuro para interpretar lo que se dice. En otras palabras, a veces no entendemos una oraci贸n hasta que se proporciona una palabra o frase importante al final. Sucede esto hace cada vez que habla Yoda. 馃槕 馃檹

Para implementar esto, entrenamos dos capas RNN simult谩neamente. La primera capa recibe la secuencia de entrada tal cual y la segunda recibe una copia invertida.

Cr茅dito de la imagen: Udacity

Capa oculta con unidad recurrente cerrada (GRU)

Ahora hagamos nuestro RNN un poco m谩s inteligente. En lugar de permitir que toda la informaci贸n del estado oculto fluya a trav茅s de la red, 驴qu茅 pasar铆a si pudi茅ramos ser m谩s selectivos? Tal vez parte de la informaci贸n sea m谩s relevante, mientras que otra informaci贸n deber铆a descartarse. Esto es esencialmente lo que hace una unidad recurrente cerrada (GRU).

Hay dos puertas en una GRU: una puerta de actualizaci贸n y una puerta de reinicio. Este art铆culo de Simeon Kostadinov explica esto en detalle. En resumen, la puerta de actualizaci贸n (z) ayuda al modelo a determinar cu谩nta informaci贸n de los pasos de tiempo anteriores debe pasar al futuro. Mientras tanto, la puerta de reinicio (r) decide qu茅 cantidad de informaci贸n pasada olvidar.

Cr茅dito de la imagen: analyticsvidhya.com

Modelo definitivo

Ahora que hemos discutido las diversas partes de nuestro modelo, echemos un vistazo al c贸digo.聽Nuevamente, todo el c贸digo fuente est谩 disponible聽aqu铆 en el cuaderno (versi贸n .html).

def  model_final (input_shape, output_sequence_length, english_vocab_size, french_vocab_size):
"""
Build and train a model that incorporates embedding, encoder-decoder, and bidirectional RNN
:param input_shape: Tuple of input shape
:param output_sequence_length: Length of output sequence
:param english_vocab_size: Number of unique English words in the dataset
:param french_vocab_size: Number of unique French words in the dataset
:return: Keras model built, but not trained
"""
# Hyperparameters
learning_rate = 0.003

# Build the layers
model = Sequential()
# Embedding
model.add(Embedding(english_vocab_size, 128, input_length=input_shape[1],
input_shape=input_shape[1:]))

# Encoder
model.add(Bidirectional(GRU(128)))
model.add(RepeatVector(output_sequence_length))
# Decoder
model.add(Bidirectional(GRU(128, return_sequences=True)))
model.add(TimeDistributed(Dense(512, activation='relu')))
model.add(Dropout(0.5))
model.add(TimeDistributed(Dense(french_vocab_size, activation='softmax')))
model.compile(loss=sparse_categorical_crossentropy,
optimizer=Adam(learning_rate),
metrics=['accuracy'])
return mode

Resultados

Los resultados del modelo final se pueden encontrar en la celda 20 del聽cuaderno.

Precisi贸n de validaci贸n: 97,5%
Tiempo de entrenamiento: 23 ciclos

Mejoras futuras

  1. Realice una divisi贸n de datos adecuada (entrenamiento, validaci贸n, prueba).聽Actualmente, no hay un conjunto de pruebas, solo entrenamiento y validaci贸n.聽Obviamente, esto no sigue las mejores pr谩cticas.
  2. LSTM + atenci贸n.聽Esta ha sido la arquitectura de facto para las RNN en los 煤ltimos a帽os, aunque existen聽algunas limitaciones.聽No us茅 LSTM porque聽ya lo hab铆a implementado en TensorFlow en otro proyecto聽y quer铆a experimentar con GRU + Keras para este proyecto.
  3. Entrene en un corpus de texto m谩s grande y m谩s diverso.聽El corpus de texto y el vocabulario de este proyecto son bastante peque帽os, con poca variaci贸n en la sintaxis.聽Como resultado, el modelo es muy fr谩gil.聽Para crear un modelo que generalice mejor, deber谩 entrenar en un conjunto de datos m谩s grande con m谩s variabilidad en la gram谩tica y la estructura de las oraciones.
  4. Capas residuales.聽Podr铆a agregar capas residuales a un LSTM RNN profundo, como se describe en聽este documento.聽O utilice capas residuales como alternativa a LSTM y GRU, como se describe聽aqu铆.
  5. Incrustaciones.聽Si est谩 entrenando en un conjunto de datos m谩s grande, definitivamente debe usar un conjunto de incrustaciones previamente entrenado, como聽word2vec聽o聽GloVe.聽A煤n mejor, use ELMo o BERT.
  • Modelo de Lenguaje Embebido (ELMo).聽Uno de los mayores avances en聽incrustaciones universales聽en 2018 fue聽ELMo, desarrollado por el聽Instituto Allen para IA.聽Una de las principales ventajas de ELMo es que aborda el problema de la polisemia, en el que una sola palabra tiene m煤ltiples significados.聽ELMo se basa en el contexto (no en las palabras), por lo que los diferentes significados de una palabra ocupan diferentes vectores dentro del espacio de incrustaci贸n.聽Con GloVe y word2vec, cada palabra tiene solo una representaci贸n en el espacio de incrustaci贸n.聽Por ejemplo, la palabra “reina” podr铆a referirse a la matriarca de una familia real, una abeja, una pieza de ajedrez o la banda de rock de la d茅cada de 1970.聽Con las incrustaciones tradicionales, todos estos significados est谩n vinculados a un solo vector para la palabra聽reina.聽Con ELMO, estos son cuatro vectores distintos, cada uno con un conjunto 煤nico de palabras de contexto que ocupan la misma regi贸n del espacio incrustado.聽Por ejemplo, esperar铆amos ver palabras como聽reina,聽torre聽y聽pe贸n聽en un espacio vectorial similar relacionado con el juego de ajedrez.聽Y esperar铆amos ver聽reina,聽colmena聽y聽miel聽en un espacio vectorial diferente relacionado con las abejas.聽Esto proporciona un impulso significativo en la codificaci贸n sem谩ntica.
  • Representaciones de Codificador Bidireccional de Transformer聽(BERT)聽.聽En lo que va de 2019, el mayor avance en incrustaciones bidireccionales ha sido聽BERT, que Google ofreci贸 como
    open-source. 驴En qu茅 se diferencia BERT?

Los modelos context-free, como word2vec o GloVe, generan una representaci贸n incrustada de una sola palabra para cada palabra del vocabulario. Por ejemplo, la palabra “banco” tendr铆a la misma representaci贸n sin contexto en “cuenta de banco” y “banco del parque”. En cambio, los modelos contextuales generan una representaci贸n de cada palabra basada en las otras palabras de la oraci贸n. Por ejemplo, en la oraci贸n “Acced铆 a la cuenta de mi banco”, un modelo contextual unidireccional representar铆a “banco” basado en “Acced铆 a” pero no a “cuenta”. Sin embargo, BERT representa “banco” utilizando tanto su contexto anterior como el siguiente: “Acced铆 a la… cuenta”, comenzando desde el fondo de una red neuronal profunda, haci茅ndola profundamente bidireccional.
鈥 Jacob Devlin y Ming-Wei Chang,聽
blog de IA de Google

Contacto

Espero que haya encontrado 煤til este post. Una vez m谩s, si tiene alg煤n comentario, me encantar铆a escucharlo. Publique sus opiniones en los comentarios.

Si desea comentar oportunidades profesionales o colaboraciones, puede encontrarme aqu铆 en LinkedIn聽o ver聽mi portfolio aqu铆聽.

Valora este art铆culo