[vc_row][vc_column][vc_column_text]En este artículo revisaremos como crear una red neuronal con Python, partiendo desde la definición hasta el programa para ejecutar una red neuronal básica.

Primero, ¿Que es una red neuronal? El cerebro humano consiste en 100 billones de células llamadas neuronas, conectadas entre ellas mediante sinapsis. Si hay suficientes sinapsis de entrada, la neurona también funcionara. A este proceso lo denominamos «pensar»[/vc_column_text][/vc_column][/vc_row][vc_row][vc_column width=»1/2″][vc_single_image image=»1555″ img_size=»medium» alignment=»center»][/vc_column][vc_column width=»1/2″][vc_column_text]

Este proceso se puede modelar creando una red neuronal en la computadora. No es necesario modelar la complejidad biológica de la neurona, solo modelar las reglas a un alto nivel. Se utilizan matrices, que son representados por una grilla de números. Para hacerlo simple, se modelara una neurona simple con tres entradas y una salida.

Se va a entrenar a la neurona para resolver el problema que se presenta líneas abajo. Los primeros cuatro ejemplos son denominados el set de entrenamiento. El resultado, de acuerdo al problema, debería ser «0» o «1».[/vc_column_text][/vc_column][/vc_row][vc_row][vc_column][vc_single_image image=»1559″ img_size=»full» add_caption=»yes» alignment=»center»][/vc_column][/vc_row][vc_row][vc_column][vc_column_text]

Proceso de Entrenamiento

¿Cómo enseñaremos a nuestra neurona a responder correctamente? Le daremos a cada entrada un peso, que puede ser un número positivo o negativo. Una entrada con un gran peso positivo o un gran peso negativo, tendrá un fuerte efecto en la salida de la neurona. Antes de empezar, estableceremos cada peso como un número aleatorio. Luego empezaremos el proceso de entrenamiento:

  1. Tomar las entradas de los valores de entrenamientos, ajustar por los pesos y pasarlas a través de una formula para calcular la salida de las neuronas.
  2. Calcular el error, que es la diferencia entre la salida de la neurona y la salida deseada en el ejemplo del conjunto de entrenamiento.
  3. Dependiendo de la dirección del error, ajuste los pesos ligeramente
  4. Repetir este proceso 10 mil veces.

Eventualmente, los pesos de la neurona alcanzarán un óptimo para el conjunto de entrenamiento. Luego, si permitimos que la neurona piense en una nueva situación, que siga el mismo patrón, debería hacer una buena predicción. Este proceso se llama propagación.[/vc_column_text][/vc_column][/vc_row][vc_row][vc_column][vc_column_text]

Formula para calcular la salida de la neurona

Quizás se pregunte, ¿Cuál es la formula especial para calcular la salida de la neurona?. Primero tomamos la suma ponderada de las entradas de la neurona, que es:

A continuación normalizamos la entrada, considerando que tenemos un resultado que se encuentra entre 0 y 1. Para esto seleccionamos una función matemática conveniente, denominada función Sigmoid:

Si se sustituye la primera ecuación en la segunda, la formula final de la neurona sería:

[/vc_column_text][/vc_column][/vc_row][vc_row][vc_column][vc_column_text]

Formula para ajustar los pesos

Durante el ciclo de entrenamiento es cuando se ajustan los pesos. Pero, ¿Cuanto debemos ajustar los pesos?. Podemos utilizar la formula «Derivada ponderada del error».

¿Por que esta formula? Primero, se requiere que el ajuste sea proporcional al tamaño del error. En segundo lugar, se multiplica por la entrada que es un 0 o un 1. Si la entrada es 0, el peso no se ajusta. Finalmente, multiplicamos por el gradiente de la curva de la función Sigmoid. Para comprender esto último, considere:

  1. Utilizamos la curva de la función Sigmoid para calcular la salida de la neurona.
  2. Si la salida es un número grande, positivo o negativo, esto significa que la neurona esta bastante segura para un lado o para el otro.
  3. La función Sigmoid tiene un gradiente que tiende a cero para números grandes
  4. Si la neurona confía en que el peso existente es correcto, no va a querer sobre ajustarlo. Multiplicando por el gradiente de la curva sigmoid se logra esto.

El gradiente de la curva Sigmoid se puede obtener de la derivada de la función:

Reemplazando la primera ecuación en la segunda, la formula final para el ajuste de los pesos es:

Hay otras formulas alternativas, que permiten a la neurona aprender más rápido, pero la ventaja de esta formula es que es sencilla de implementar.[/vc_column_text][/vc_column][/vc_row][vc_row][vc_column][vc_column_text]

Construyendo el código Python

Aunque no utilizaremos una biblioteca de redes neuronales, importaremos cuatro métodos de la librería NumPy de Python, estas son:

  • exp: la función exponencial
  • array: permite crear matrices
  • dot: multiplica matrices
  • random: proporciona números aleatorios

Por ejemplo, se puede utilizar el método array() para representar el set de entrenamiento:

La función «.T», transpone la matriz de horizontal a vertical.

Entonces, estamos listos para el código fuente. Se han agregado comentarios al código fuente para explicar línea a línea el mismo. Tenga en cuenta que en cada iteración se procesa todo el conjunto de entrenamiento en forma simultanea.[/vc_column_text][/vc_column][/vc_row][vc_row][vc_column][vc_column_text]

El código también esta disponible en github.

Ejecutar la red neuronal y observar los resultados:

De esta forma, hemos construido una red neuronal simple con Python.

Nota:

En el ejemplo se esta utilizando la función «xrange«, esto funciona para Python 2, si esta utilizando Python 3, se debe cambiar por «range«.

Se debe tener en cuenta que se esta utilizando la librería «numpy«, esta debe ser agregada a nuestro programa.


Este ejemplo se encuentra en base a How to build a simple neural network in 9 lines of Python code.[/vc_column_text][/vc_column][/vc_row]

Python: ¿Cómo construir una red neuronal simple?
Si te gusto, comparte ...Share on email
Email
Share on facebook
Facebook
Share on twitter
Twitter
Share on linkedin
Linkedin
Share on google
Google
Etiquetado en:        

3 pensamientos en “Python: ¿Cómo construir una red neuronal simple?

  • diciembre 17, 2018 a las 6:43 am
    Enlace permanente

    Otra forma de hacer la neurona seria:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-

    class Neurona:
    def __init__(self, tabla, resultado):
    self.tabla = tabla
    self.resultado = resultado

    #Primera fila de entradas: [0, 0, 1]
    entrada = tabla[0]

    #Creamos un peso para cada entrada con un valor de 1: [1, 1, 1]
    #Estos valores normalmente se los dan al azar en otros ejemplos.
    self.peso = []
    self.n_entradas = len(entrada)
    for p in range(self.n_entradas):
    self.peso.append(1)

    #Salida (esto es lo que obtenemos en la primera fila): 1
    self.salida = self.nueva_salida(entrada, self.peso)

    #Primer resultado esperado (Esto es lo que queremos obtener en la primera fila): 0
    esperado = resultado[0]

    #Que tanto se le sube o se le baja a los pesos para encajar con lo que queremos obtener.
    self.coeficiente = 0.5

    #Correcciones a los pesos (estos son los pesos que se modifican): [0, 0, -0.5]
    self.correcciones = self.corregir_peso(self.coeficiente, entrada, self.salida, esperado)

    def nueva_salida(self, entrada, peso):
    sumar = []
    for (e, p) in zip(entrada, peso):
    sumar.append(e * p)

    sumatoria = sum(sumar)
    if sumatoria>= 0:
    return 1
    else:
    return 0

    def corregir_peso(self,coeficiente, entrada, salida, esperado):
    correcciones = []
    for e in entrada:
    correcciones.append(coeficiente * e * (esperado – salida))

    return correcciones

    def nuevo_peso(self, peso, corregir_peso):
    nuevo_peso = []
    for (p, c) in zip(peso, corregir_peso):
    nuevo_peso.append(p + c)

    return nuevo_peso

    def practica(self, veces):
    tabla = self.tabla * veces
    resultado = self.resultado * veces
    for (e, r) in zip(tabla, resultado):
    self.salida = self.nueva_salida(e, self.peso)
    self.correcciones = self.corregir_peso(0.5, e, self.salida, r)
    self.peso = self.nuevo_peso(self.peso, self.correcciones)

    def piensa(self, entrada):
    return self.nueva_salida(entrada, self.peso)

    #El conjunto de prueba.
    tabla = [
    [0, 0, 1],
    [1, 1, 1],
    [1, 0, 1],
    [0, 1, 1],
    ]
    resultado = [
    0,
    1,
    1,
    0
    ]

    #Iniciamos la neurona.
    perceptron = Neurona(tabla, resultado)
    print «Los pesos antes de la práctica.»
    print perceptron.peso
    #Normalmente obtiene los pesos correctos al 3 intento, pero para estar seguros que sean 4 intentos.
    perceptron.practica(4)
    print «Los pesos después de la práctica.»
    print perceptron.peso
    print «La nueva situacion: [1, 0, 0]»
    print perceptron.piensa([1, 0, 0])

    Responder
  • febrero 3, 2020 a las 3:48 pm
    Enlace permanente

    Si bien se algo de Python, trabajo en Java. Dejo el mismo ejemplo pasado a Java.
    Me estoy tomando algo de tiempo para aprender algo de IA, mundo nuevo para mí.
    Gracias por el artículo

    package ia001;

    import java.util.Random;

    public class IA001 extends Array_Handle {

    private Random rand = new Random();
    private float[][] weights;

    public IA001() {

    // Iniciar una red neuronal de una neurona
    // In Python
    // neural_network = NeuralNetwork()

    // In Python
    // print «Random starting synaptic weights: »
    // print neural_network.synaptic_weights

    weights = Gen_Weights();

    System.out.print(«Random starting synaptic weights: «);
    Print_Aarray(weights);

    // El conjunto de pruebas. Tenemos cuatro ejemplos, cada uno consiste en
    // 3 valores de entrada
    // y un valor de salida.
    //
    // In Python
    // training_set_inputs = array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1,
    // 1]])
    // training_set_outputs = array([[0, 1, 1, 0]]).T

    int[][] training_set_inputs = { { 0, 0, 1 }, { 1, 1, 1 }, { 1, 0, 1 }, { 0, 1, 1 } };
    int[][] training_set_outputs = { { 0, 1, 1, 0 } };
    training_set_outputs = transponer_matriz(training_set_outputs);

    // Entrenar la red neuronal utilizando el conjunto de entrenamiento.
    // Realizar 10000 veces y realizar un ajuste más pequeño cada vez.
    // In python
    // neural_network.train(training_set_inputs, training_set_outputs,
    // 10000)

    // Tomar las entradas de los valores de entrenamientos, ajustar por
    // los pesos y pasarlas a través de una formula para calcular la salida
    // de las neuronas. * Calcular el error, que es la diferencia entre la
    // salida de la neurona y la salida deseada en el ejemplo del conjunto
    // de entrenamiento. Dependiendo de la dirección del error, ajuste los
    // pesos ligeramente * Repetir este proceso 10 mil veces.

    train(training_set_inputs, training_set_outputs, 10000);

    // In python
    // print «New synaptic weights after training: »
    // print neural_network.synaptic_weights

    System.out.println();
    System.out.print(«New synaptic weights after training: «);
    Print_Aarray(weights);

    // In python
    // Prueba la red neuronal con una nueva situación
    // print «Considering new situation [1, 0, 0] -> ?: »
    // print neural_network.think(array([1, 0, 0]))

    System.out.println();
    System.out.print(«Considering new situation [0, 0, 0] -> ?: «);
    Print_Aarray(think(new int[][] { { 0, 0, 0 } }));

    System.out.print(«Considering new situation [0, 0, 1] -> ?: «);
    Print_Aarray(think(new int[][] { { 0, 0, 1 } }));

    System.out.print(«Considering new situation [0, 1, 1] -> ?: «);
    Print_Aarray(think(new int[][] { { 0, 1, 1 } }));

    System.out.print(«Considering new situation [1, 1, 1] -> ?: «);
    Print_Aarray(think(new int[][] { { 1, 1, 1 } }));

    System.out.print(«Considering new situation [1, 0, 0] -> ?: «);
    Print_Aarray(think(new int[][] { { 1, 0, 0 } }));

    System.out.print(«Considering new situation [1, 1, 0] -> ?: «);
    Print_Aarray(think(new int[][] { { 1, 1, 0 } }));

    System.out.print(«Considering new situation [0, 1, 1] -> ?: «);
    Print_Aarray(think(new int[][] { { 0, 0, 1 } }));

    }

    /**
    * @param output
    * @return
    *
    * La función Sigmoid, que describe una curva en forma de S Se pasan
    * la suma ponderada de las entradas a través de esta función para
    * normalizarlos entre 0 y 1.
    *
    * In Python: def __sigmoid(self, x): return 1 / (1 + exp(-x))
    */
    private float[][] __sigmoid(float[][] output) {
    float[][] r = output;
    for (int i = 0; i < r.length; i++) {
    r[i][0] = (float) (1 / (1 + Math.exp(-output[i][0])));
    }

    return r;
    }

    /**
    * @param output
    * @return
    *
    * La derivada de la función Sigmoid. Este es el gradiente de la
    * función Sigmoid. Indica la confianza que tenemos en el peso
    * existente.
    *
    * In Python def __sigmoid_derivative(self, x): return x * (1 – x)
    */
    private float __sigmoid_derivative(float x) {
    float out = x * (1 – x);
    /*
    * system.out.println("__sigmoid_derivative: {x" + Float.toString(x) + "
    * /out " + Float.toString(out) + "}");
    */
    return out;
    }

    /**
    * @param training_set_outputs
    * @param output
    * @return
    */
    private float[][] calc_error(int training_set_outputs[][], float[][] output) {

    float[][] out = new float[output.length][1];

    for (int i = 0; i < out.length; i++) {
    out[i][0] = training_set_outputs[i][0] – output[i][0];
    /*
    * System.out.println(Float.toString(training_set_outputs[i][0]) +
    * " – " + Float.toString(output[i][0]) + " = " +
    * Float.toString(out[i][0]));
    */
    }

    /*
    * System.out.print("calc_error, Valores esperados: ");
    * Print_Aarray(training_set_outputs);
    */
    /*
    * System.out.print("calc_error, Valores output: ");
    * Print_Aarray(output);
    */
    /*
    * System.out.print("calc_error, out: "); Print_Aarray(out);
    */

    return out;
    }

    /**
    * @param adjustment
    */
    private void add_adjustment(float[][] adjustment) {
    /*
    * System.out.println("add_adjustment, adjustment ALL: ");
    * Print_Aarray(adjustment);
    */
    for (int i = 0; i < weights[0].length; i++) {
    /*
    * System.out.println("add_adjustment, adjustment: [" +
    * Float.toString(adjustment[i][0]) + "][" +
    * Float.toString(adjustment[i][0]) + "]");
    */
    weights[0][i] = adjustment[i][0] + weights[0][i];
    }

    /*
    * System.out.print("add_adjustment, weights: "); Print_Aarray(weights);
    */
    }

    /**
    * @param training_set_inputs
    * @param error
    * @param output
    * @return
    */
    private float[][] calc_adjustment(int training_set_inputs[][], float[][] error, float[][] output) {
    /*
    * adjustment = dot(training_set_inputs.T, error *
    * self.__sigmoid_derivative(output))
    */

    int[][] training_set_inputsT = transponer_matriz(training_set_inputs);

    /*
    * System.out.print("calc_adjustment, training_set_inputsT: ");
    * Print_Aarray(training_set_inputsT);
    */
    /*
    * System.out.print("calc_adjustment, errors: "); Print_Aarray(error);
    */

    float[][] out = new float[output.length][1];

    /*
    * System.out.
    * println("error[i][0] * __sigmoid_derivative(output[i][0]): ");
    * System.out.println();
    */

    for (int i = 0; i < output.length; i++) {
    out[i][0] += error[i][0] * __sigmoid_derivative(output[i][0]);
    /*
    * System.out.println("error: " + Float.toString(error[i][0]) +
    * " * " + Float.toString(output[i][0]) + " = " +
    * Float.toString(out[i][0]));
    */
    }

    /*
    * System.out.
    * print("calc_adjustment, error * __sigmoid_derivative(output): ");
    * Print_Aarray(out);
    */

    out = dot1(training_set_inputsT, out);

    /*
    * System.out.print("calc_adjustment, OUT: "); Print_Aarray(out);
    */

    return out;
    }

    /**
    * @param training_set_inputs
    * @param training_set_outputs
    * @param number_of_training_iterations
    *
    * Entrenamos a la red neuronal a través de un proceso de prueba
    * y error Se realiza un ajuste de los pesos sinápticos cada vez.
    */
    private void train(int[][] training_set_inputs, int[][] training_set_outputs,
    int number_of_training_iterations) {

    // In Python
    // iteration in xrange(number_of_training_iterations):
    // // Pasar el conjunto de entrenamiento a través de nuestra red
    // neuronal (una sola neurona)
    // output = self.think(training_set_inputs)
    //
    // // Calcular el error(La diferencia entre el resultado deseado
    // // y el resultado obtenido).
    // error = training_set_outputs – output
    //
    // // Multiplica el error por la entrada y nuevamente por el gradiente
    // de la curva Sigmoid
    // // Esto significa que los pesos menos confiables se están ajustando
    // más
    // // Esto significa que las entradas que son cero, no causan cambios en
    // los pesos.
    // adjustment = dot(training_set_inputs.T, error *
    // self.__sigmoid_derivative(output))
    //
    // // Ajuste de los pesos
    // self.synaptic_weights += adjustment

    float[][] output;
    float[][] error;
    float[][] adjustment;

    for (int j = 0; j < number_of_training_iterations; j++) {

    /*
    * System.out.println("train, INTERACCION: " + Integer.toString(j) +
    * " / " + Integer.toString(number_of_training_iterations));
    */

    output = think(training_set_inputs);

    /*
    * System.out.print("train, output: "); Print_Aarray(output);
    */

    error = calc_error(training_set_outputs, output);

    /*
    * System.out.print("train, error: "); Print_Aarray(error);
    */

    // // Multiplica el error por la entrada y nuevamente por el
    // gradiente
    // de la curva Sigmoid
    // // Esto significa que los pesos menos confiables se están
    // ajustando
    // más
    // // Esto significa que las entradas que son cero, no causan
    // cambios en
    // los pesos.
    // adjustment = dot(training_set_inputs.T, error *
    // self.__sigmoid_derivative(output))

    // correcciones.append(coeficiente * e * (esperado – salida))

    adjustment = calc_adjustment(training_set_inputs, error, output);

    add_adjustment(adjustment);

    // System.out.println("———————————————-");

    /*
    * try { Thread.sleep(5000); } catch (InterruptedException e) { //
    * TODO Auto-generated catch block e.printStackTrace(); }
    */
    }
    }

    /**
    * @return
    *
    * Genera un array de [1][3] con numeros al azar
    */
    private float[][] Gen_Weights() {
    float[][] Weights = new float[1][3];
    /*
    * float[][] Weights = {{(float) -0.16595599, (float) 0.44064899,
    * (float) -0.99977125}};
    */
    for (int i = 0; i < Weights[0].length; i++) {
    Weights[0][i] = synaptic_weights();
    }

    return Weights;
    }

    /**
    * La red neuronal piensa.
    *
    * In Python return self.__sigmoid(dot(inputs, self.synaptic_weights))
    *
    * Tomar las entradas de los valores de entrenamientos, ajustar por los
    * pesos y pasarlas a través de una formula para calcular la salida de las
    * neuronas.
    */
    private float[][] think(int[][] training_set_inputs) {

    float[][] output;
    float[][] sigmoid;

    /*
    * System.out.print("think, weights : "); Print_Aarray(weights);
    *
    * System.out.print("think, training_set_inputs: ");
    * Print_Aarray(training_set_inputs);
    */

    // Pasar las entradas a través de nuestra red neuronal (una neurona)

    output = dot(training_set_inputs, weights);

    /*
    * System.out.print("think, output DOT: "); Print_Aarray(output);
    */

    sigmoid = __sigmoid(output);

    /*
    * System.out.print("think, sigmoid OUT: "); Print_Aarray(sigmoid);
    */

    return sigmoid;
    }

    /**
    * Seed el generador de números aleatorios, con una raíz a fin de generar
    * los mismo números aleatorios en cada proceso.
    *
    * In Python random.seed(1) float x = (float) Math.random();
    *
    * Modelamos una sola neurona, con 3 conexiones de entrada y una conexión de
    * salida Asignamos los pesos aleatorios a una matriz de 3×1, con valores en
    * el rango de -1 a 1 y significa 0
    *
    * synaptic_weights = 2 * x.random((3, 1)) – 1
    */
    private float synaptic_weights() {
    int max = 1;
    int min = -1;
    return (rand.nextFloat() * (max – min)) + min;
    }

    /**
    * @param args
    */
    public static void main(String[] args) {
    new IA001();
    }
    }

    /**
    *
    */
    package ia001;

    /**
    * @author GDF {01/2020}
    *
    * Diferentes functiones que emulan la de Python
    *
    */
    public class Array_Handle {

    /**
    *
    */
    public Array_Handle() {

    }

    /**
    * @param A
    * @param B
    * @return
    *
    * Function que realiza la function dot de python. Multiplica dos
    * array
    */
    protected float[][] dot(int[][] A, float[][] B) {

    float[][] out = new float[A.length][1];
    float suma;

    for (int i = 0; i < A.length; i++) {
    suma = 0;
    for (int j = 0; j < A[0].length; j++) {
    suma += A[i][j] * B[0][j];
    }
    out[i][0] = suma;
    }

    return out;
    }

    /**
    * @param A
    * @param B
    * @return
    *
    * Function que realiza la function dot de python Multiplica dos
    * array sin transponer la 2da matriz de entrada
    */
    protected float[][] dot1(int[][] A, float[][] B) {

    float[][] out = new float[A.length][1];
    float suma;

    for (int i = 0; i < A.length; i++) {
    suma = 0;
    for (int j = 0; j < A[0].length; j++) {
    suma += A[i][j] * B[j][0];
    /*
    * System.out.println(Integer.toString(A[i][j]) + " * " +
    * Float.toString(B[j][0]) + " = " + Float.toString(suma));
    */
    }
    out[i][0] = suma;
    }

    return out;
    }

    /**
    * @param matriz
    * @return
    *
    * Remplaza la function T de python
    */
    protected int[][] transponer_matriz(int[][] matriz) {

    int[][] matrizT = new int[matriz[0].length][matriz.length];

    for (int x = 0; x < matriz.length; x++) {
    for (int y = 0; y < matriz[x].length; y++) {
    matrizT[y][x] = matriz[x][y];
    }
    }

    return matrizT;
    }

    /**
    * @param ArrayToPrint
    */
    protected void Print_Aarray(float ArrayToPrint[][]) {

    String toPrint = "{";

    for (int i = 0; i < ArrayToPrint.length; i++) {
    toPrint += "{";
    for (int k = 0; k < ArrayToPrint[0].length; k++) {
    toPrint += Float.toString(ArrayToPrint[i][k]);
    if (k < ArrayToPrint[0].length – 1) {
    toPrint += " , ";
    }
    }
    toPrint += "}";
    }
    toPrint += "}";

    System.out.println(toPrint);
    }

    /**
    * @param ArrayToPrint
    */
    protected void Print_Aarray(int ArrayToPrint[][]) {

    String toPrint = "{";

    for (int i = 0; i < ArrayToPrint.length; i++) {
    toPrint += "{";
    for (int k = 0; k < ArrayToPrint[0].length; k++) {
    toPrint += Integer.toString(ArrayToPrint[i][k]);
    if (k < ArrayToPrint[0].length – 1) {
    toPrint += " , ";
    }
    }
    toPrint += "}";
    }
    toPrint += "}";

    System.out.println(toPrint);
    }

    }

    Responder
  • febrero 4, 2020 a las 2:36 pm
    Enlace permanente

    Sumo el resultado del ejemplo:

    Random starting synaptic weights: {{-0.6987735 , 0.8781359 , 0.40403843}}

    New synaptic weights after training: {{9.6724205 , -0.20813294 , -4.6291714}}

    Considering new situation [0, 0, 0] -> ?: {{0.5}}
    Considering new situation [0, 0, 1] -> ?: {{0.009668454}}
    Considering new situation [0, 1, 1] -> ?: {{0.007866034}}
    Considering new situation [1, 1, 1] -> ?: {{0.99211687}}
    Considering new situation [1, 0, 0] -> ?: {{0.999937}}
    Considering new situation [1, 1, 0] -> ?: {{0.99992245}}
    Considering new situation [0, 1, 1] -> ?: {{0.009668454}}

    Responder

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Facebook