Aprendiendo de los datos preservando la privacidad de los mismos
# all_flag

Problemas de privacidad al usar Deep Learning y otras tecnicas de Machine Learning

Es sabido que las redes neuronales o deep learning como se lo conoce ahora es un sub-area del campo de Machine Learning. Todo este grupo de algoritmos se caracterisa del resto de las otras áreas de la Inteligencia Artificial en que hecho de que su principal caracteristica es la capacidad que tienen de aprender utilizando datos, en lugar de usar reglas predefinidas. Pero muchas veces, los datos sobre los que se quiere crear un modelo de machine learning son datos muy personales y privados. Los mejores y más útiles modelos interactúan con los datos más personales de las personas y decirnos cosas sobre nosotros que hubiesen sido dificiles de saber de otra manera. Pero al mismo tiempo dar toda esta información requiere que confiemos en quien va a almacenar estos datos y que los cuidará para protejer nuestra privacidad, lo cual no siempre ocurre. Ejemplos de esto son:

  • Aplicaciones en Medicina: machine learning puede ayudar a mejorar dramáticamente diagnostico de enfermedades, como detección de tumores en imagenes de MRI, detectar con tiempo retinopatía diabética en imagenes de retina, detección de cancer en imagenes de melanoma, entre varias otras aplicaciónes más. Pero este tipo de datos son bastante sensibles ya que son datos de los pacientes, una filtración de este tipo de información sería muy grave.
  • Recomendaciones: ya sea recomendacion de productos, contenido o publicidad, estos modelos buscan personalizar la interacción de los usuarios en los servicios que están utilizando. Mientras más información personal del usuario sea posible de obtener para el modelo de recomendación, mucho mejor será la experiencia del usuario final, que recibirá recomendaciones más significativas. En el 2018 se reveló que una empresa de Cambridge utilizó datos personales de varios usuarios de Facebook para crear un perfil psicológico de cada uno y poder crear campañas de desinformación a través de facebook, que recomendaba anunciós con discursos de odio, con para influenciar campañas electorales en el 2016 en Estados Unidos, influenciar la salida de Inglaterra de la EU (Brexit) entre varios otros escandalos.
  • Credit Scoring: modelos para saber que tan buenos pagadores de prestamos somos. Pueden utilizar informacion personal como historial crediticio, gastos varios y datos demograficos. Esta es información sensible que no querriamos que corra el riesgo de ser revelada a personas mal intencionadas. Por ejemplo, en el 2017 se reveló que Equifax ,una de las más grandes empresas que otorga credit scorings, entre varios otros servicios utilizando información personal de millones de personas, tuvo un breach enorme de información sensible de millones de personas.

Ya que los datos son el recurso primordial para modelos como las redes neuronales, y los casos de uso más significativos de los mismos requiere que interactúen con datos personales, es necesario encontrar una manera de acceder a los mismos sin correr el riesgo de violar la privacidad de las personas.

Que pasaría si en lugar de acumular datos privados en un lugar centralizado para entrenar un modelo de deep learning, pudieramos enviar el modelo a donde se generan los datos y entrenar el modelo desde ahí, evitando así tener un solo punto de fallo desde el cual pueda ocurrir un breach de datos.

Esto significa que: - Tecnicamente para poder participar en el entrenamiento de un modelo de deep learning, los usuarios no necesitan enviar sus datos a nadie, permitiendo así entrenar modelos valiosos con datos de salud, financieros, etc.- Personas y empresas que antes no podían compartir sus datos por cuestiones legales igual podrán generar valor gracias a ellos.

Federated Learning

La premisa federated learning es que multiples datasets contienen información que es útil para resolver un problema, pero es dificil poder acceder a estos datasets en cantidades lo suficientemente grandes como para entrenar un modelo de deep learning que generalice lo suficientemente bien.

Si bien el dataset puede tener suficiente informacion para entrenar un modelo de deep learning, la principal preocupación es que este también pueda contener información que no tenga relación con el aprendizaje del modelo, pero que pueda causar daños a alguien si es revelada.

Federated Learning se trata de enviar el modelo a un entorno seguro y aprender como resolver el problema sin la necesidad de mover los datos a ninguna parte. En este notebook veremos un ejemplo simple de federated learning.

import os
import sys
module_path = os.path.abspath(os.path.join('..'))
if module_path not in sys.path:
    sys.path.append(module_path)
import numpy as np
from collections import Counter
import random
import sys
import codecs

np.random.seed(12345)

Caso de Ejemplo: Detección de SPAM

Digamos que queremos entrenar un modelo para detectar spam entre los correos de varias personas

Este caso de uso se trata de clasificar correos. Para esto vamos a usar un dataset de correos de ENRON, un dataset publico bastante conocido, por el escandalo generado por dicha empresa.

Lectura y preprocesamiento del dataset

vocab, spam, ham = (set(["<unk>"]), list(), list())

# Lecrura de spam
# with codecs.open('datasets/enron-spam/spam.txt', 'r', encoding='utf-8', errors='ignore') as f:
#     raw = f.readlines()
f = codecs.open('datasets/enron-spam/spam.txt', 'r', encoding='utf-8', errors='ignore')
raw = f.readlines()
print('cantidad de mails de spam:', len(raw))
print('Un correo de ejemplo:\n',raw[0])
# test = set(raw[0][:-2].split(" "))
# print(test)
for row in raw:
    # se toma todas las palabras unicas de cada correo
    spam.append(set(row[:-2].split(" ")))
    # por cada una de las palabras del ultimo correo 
    # agregado a la lista de spam
    for word in spam[-1]:
        # se agregan todas las palabras nuevas al vocabulario
        vocab.add(word)

# Repetimos el mismo proceso para el archivo ham.txt
f = codecs.open('datasets/enron-spam/ham.txt', 'r', encoding='utf-8', errors='ignore')
raw = f.readlines()
print('cantidad de mails de ham:', len(raw))
print('Un correo de ejemplo:\n',raw[10])
for row in raw:
    ham.append(set(row[:-2].split(" ")))
    for word in ham[-1]:
        vocab.add(word)
cantidad de mails de spam: 9000
Un correo de ejemplo:
 Subject: dobmeos with hgh my energy level has gone up ! stukm introducing doctor - formulated hgh human growth hormone - also called hgh is referred to in medical science as the master hormone . it is very plentiful when we are young , but near the age of twenty - one our bodies begin to produce less of it . by the time we are forty nearly everyone is deficient in hgh , and at eighty our production has normally diminished at least 90 - 95 % . advantages of hgh : - increased muscle strength - loss in body fat - increased bone density - lower blood pressure - quickens wound healing - reduces cellulite - improved vision - wrinkle disappearance - increased skin thickness texture - increased energy levels - improved sleep and emotional stability - improved memory and mental alertness - increased sexual potency - resistance to common illness - strengthened heart muscle - controlled cholesterol - controlled mood swings - new hair growth and color restore read more at this website unsubscribe 

cantidad de mails de ham: 22032
Un correo de ejemplo:
 Subject: entex transistion the purpose of the email is to recap the kickoff meeting held on yesterday with members from commercial and volume managment concernig the entex account : effective january 2000 , thu nguyen ( x 37159 ) in the volume managment group , will take over the responsibility of allocating the entex contracts . howard and thu began some training this month and will continue to transition the account over the next few months . entex will be thu ' s primary account especially during these first few months as she learns the allocations process and the contracts . howard will continue with his lead responsibilites within the group and be available for questions or as a backup , if necessary ( thanks howard for all your hard work on the account this year ! ) . in the initial phases of this transistion , i would like to organize an entex " account " team . the team ( members from front office to back office ) would meet at some point in the month to discuss any issues relating to the scheduling , allocations , settlements , contracts , deals , etc . this hopefully will give each of you a chance to not only identify and resolve issues before the finalization process , but to learn from each other relative to your respective areas and allow the newcomers to get up to speed on the account as well . i would encourage everyone to attend these meetings initially as i believe this is a critical part to the success of the entex account . i will have my assistant to coordinate the initial meeting for early 1 / 2000 . if anyone has any questions or concerns , please feel free to call me or stop by . thanks in advance for everyone ' s cooperation . . . . . . . . . . . julie - please add thu to the confirmations distributions list 

El codigo anterior es solo preprocesamiento. Lo preprocesamos para tenerlo listo a la hora de hacer forwardprop utilizando embeddings. Algunas caracteristicas importantes del dataset preprocesado para poder entrenar el modelo son:

  • Todas las palabras son convertidas en una lista de indices
  • Todos los correos son convertidos en listas de 500 palabras exactamente, ya sea recortandolos o rellenandolos con el token <unk>. Hacer esto hace que el dataset sea más fácil de procesar por el modelo
# Tomamos el vocabulario creado y creamos un diccionario
# con las palabras y sus indices
vocab, word2index = (list(vocab), {})
for i, word in enumerate(vocab):
    word2index[word] = i

def to_indices(input, l=500):
    indices = list()
    for line in input:
        # si la linea tiene menos palabras que l
        if (len(line) < l):
            # se completa la linea con el simbolo <unk> tantas
            # veces hasta llegar a una longitud l
            line = list(line) + (['<unk>'] * (l - len(line)))
            idxs = list()
            for word in line:
                idxs.append(word2index[word])
            indices.append(idxs)
    return indices
            

Creacion de estructuras de datos a ser utilizadas para el entrenamiento de los modelos

# Se optienen los indices de spam y ham
spam_idx = to_indices(spam)
ham_idx = to_indices(ham)
# Agrupamos ham y spam en listas para crear
# los conjuntos de prueba y entrenamiento
train_spam_idx = spam_idx[0:-1000]
train_ham_idx = ham_idx[0:-1000]
test_spam_idx = spam_idx[-1000:]
test_ham_idx = ham_idx[-1000:]

# Creamos los conjuntos de test y entrenamiento
train_data = list()
train_target = list()

test_data = list()
test_target = list()

for i in range(max(len(train_ham_idx), len(train_spam_idx))):
    train_data.append(train_spam_idx[i%len(train_spam_idx)])
    train_target.append([1])
    
    train_data.append(train_ham_idx[i%len(train_ham_idx)])
    train_target.append([0])
    
for i in range(max(len(test_ham_idx), len(test_spam_idx))):
    test_data.append(test_spam_idx[i%len(test_spam_idx)])
    test_target.append([1])
    
    test_data.append(test_ham_idx[i%len(test_ham_idx)])
    test_target.append([0])

Definicion de las funciones para entrenar y testear el modelo

Definimos las funciones que nos van a permitir inicializar, entrenar y evaluar el modelo centralizado de detección de spam.

from lightdlf_old.cpu.core import Tensor
from lightdlf_old.cpu.layers import Embedding, MSELoss, CrossEntropyLoss
from lightdlf_old.cpu.optimizers import SGD
# from lightdlf.cpu.core2 import Tensor, Embedding, MSELoss, SGD

def train(model, input_data, target_data, batch_size=500, iterations=5):
    
    criterion = MSELoss()
    optim = SGD(parameters=model.get_parameters(), alpha=0.01)
    
    n_batches = int(len(input_data) / batch_size)
    for iter in range(iterations):
        iter_loss = 0
        for b_i in range(n_batches):

            # el token auxiliar <unk> se tiene que quedar en 0
            # ya que no debe afectar al modelo
            model.weight.data[word2index['<unk>']] *= 0 
            input = Tensor(input_data[b_i*batch_size:(b_i+1)*batch_size], autograd=True)
            target = Tensor(target_data[b_i*batch_size:(b_i+1)*batch_size], autograd=True)

            pred = model.forward(input).sum(1).sigmoid()
            loss = criterion.forward(pred,target)
            # loss.backward(Tensor(np.ones_like(loss.data)))
            loss.backward()
            optim.step()

            iter_loss += loss.data[0] / batch_size

            sys.stdout.write("\r\tLoss:" + str(iter_loss / (b_i+1)))
        print()
    return model
def test(model, test_input, test_output):
    model.weight.data[word2index['<unk>']] *= 0
    
    input = Tensor(test_input, autograd=True)
    target = Tensor(test_output, autograd=True)
    
    pred = model.forward(input).sum(1).sigmoid()
    return ((pred.data > 0.5) == target.data).mean()

Entrenamiento de un modelo Centralizado

# model = Embedding(vocab_size=len(vocab), dim=2)
model = Embedding(vocab_size=len(vocab), dim=1)
model.weight.data *= 0
criterion = MSELoss()
optim = SGD(parameters=model.get_parameters(), alpha=0.01)
for i in range(3):
    model = train(model, train_data, train_target, iterations=1)
    print("% Correcto en el conjunto de entrenamiento: " + str(test(model, test_data, test_target)*100))
	Loss:0.037140416860871446
% Correcto en el conjunto de entrenamiento: 98.65
	Loss:0.011258669226059108
% Correcto en el conjunto de entrenamiento: 99.15
	Loss:0.008068268387986223
% Correcto en el conjunto de entrenamiento: 99.45

Luego de 3 iteraciones logramos entrenar un modelo que puede predecir correos de spam con una precision del 99.45%

Analisis de los embedings generados

Hemos generado un modelo donde todos los embeddings tienen una dimension de 1, veamos los embeddings de palabras comunes en correos de spam y comunes en correos normales de una empresa

print('Palabras comunes en correos de spam:')
print('\t- palabra: penis', '\n\tidx:', word2index['penis'], ',\n\tembedding:', model.weight.data[word2index['penis']], '\n')
print('\t- palabra: viagra', '\n\tidx:', word2index['viagra'], ',\n\tembedding:', model.weight.data[word2index['viagra']], '\n')
print('\t- palabra: spam', '\n\tidx:', word2index['spam'], ',\n\tembedding:', model.weight.data[word2index['spam']], '\n')
# print('- palabra: cocaine', '\nidx:', word2index['cocaine'], ',\nembedding:', model.weight.data[word2index['cocaine']], '\n')

print('Palabras comunes en correos normales')
print('\t- palabra: critical', '\n\tidx:', word2index['critical'], ',\n\tembedding:', model.weight.data[word2index['critical']], '\n')
print('\t- palabra: assistant', '\n\tidx:', word2index['assistant'], ',\n\tembedding:', model.weight.data[word2index['assistant']], '\n')
print('\t- palabra: meetings', '\n\tidx:', word2index['meetings'], ',\n\tembedding:', model.weight.data[word2index['meetings']], '\n')
Palabras comunes en correos de spam:
	- palabra: penis 
	idx: 45229 ,
	embedding: [0.09662769] 

	- palabra: viagra 
	idx: 20503 ,
	embedding: [0.20160151] 

	- palabra: spam 
	idx: 5850 ,
	embedding: [0.11120405] 

Palabras comunes en correos normales
	- palabra: critical 
	idx: 49238 ,
	embedding: [-0.02622459] 

	- palabra: assistant 
	idx: 27749 ,
	embedding: [-0.02255735] 

	- palabra: meetings 
	idx: 47376 ,
	embedding: [-0.02443877] 

Podemos ver que los embeddings de palabras comunes en correos de spam tienen un valor positivo, mientras que las palabras comunes en correos normales tienden a valores negativos, esto es porque estamos usando la función sigmoide para poder clasificar estos correos, donde:

  • 0 = todos los correos normales o ham
  • 1 = todos los correos que son spam

En la función sigmoide, los valores por debajo de 0 tienden a tendrán como valor 0.5 o menos, como nuestro modelo es un modelo conocido como bag of words, si las palabras comunes en un correo de spam tienen un valor negativo, mientras, más de estas haya en un correo, estas sumarán un numero muy por debajo de 0, por lo que la función sigmoide tenderá a 0, esto se puede ver claramente en los embeddings de las palabras más arriba

Federated Learning: Volviendo el modelo Centralizado en uno Federado

El ejemplo anterior es la forma tradicional de entrenar un modelo de machine learning en donde:

  1. Cada usuario envia sus datos a un servidor central
  2. El servidor central entrena un modelo global en base a los datos
  3. El modelo y los datos quedan en el servidor central

Al tener todos los datos en un servidor central, tenemos el problema que habíamos menciondo, de el cliente pierde el control de sus datos y por ende de su privacidad. Un breach en el servidor central es suficiente para vulnerar la privacidad de miles de usuarios.

Como habíamos mencionado, la solucion a este problema es utilizar federated learning. Para ello simulemos un entorno de entrenamiento donde tengamos usuarios con multiples colecciones diferentes de correos

bob = (train_data[0:1000], train_target[0:1000])
alice = (train_data[1000:2000], train_target[1000:2000])
sue = (train_data[2000:], train_target[2000:])
print("cantidad de correos por usuario")
print('- bob',len(bob[0]))
print('- alice',len(alice[0]))
print('- sue',len(sue[0]))
cantidad de correos por usuario
- bob 1000
- alice 1000
- sue 39908

Ahora que tenemos estos tres datasets, podemos hacer el mismo entrenamiento que habíamos hecho anteriormente

model = Embedding(vocab_size=len(vocab), dim=1)
model.weight.data *= 0
import copy

for i in range(3):
    # Tomamos el modelo que inicializamos y por cada set de datos
    # Creamos una copia del modelo (deepcopy) y entrenamos
    # un modelo por cada conjunto de datos
    print('Iniciando la ronda de entrenamiento...')
    print('\tPaso 1: enviamos el modelo a Bob')
    bob_model = train(copy.deepcopy(model), bob[0], bob[1], iterations=1)
    
    print('\n\tPaso 2: enviamos el modelo a Alice')
    alice_model = train(copy.deepcopy(model), alice[0], alice[1], iterations=1)
    
    print('\n\tPaso 3: enviamos el modelo a Sue')
    sue_model = train(copy.deepcopy(model), sue[0], sue[1], iterations=1)
    
    print('\n\tModelo promedio de todos los modelos')
    model.weight.data = (bob_model.weight.data + \
                         alice_model.weight.data + \
                         sue_model.weight.data)/3
    
    print('\t% Correcto en el conjunto de entrenamiento: ' + \
          str(test(model, test_data, test_target)*100))
    print('Iteramos\n')
Iniciando la ronda de entrenamiento...
	Paso 1: enviamos el modelo a Bob
	Loss:0.21908166249699718

	Paso 2: enviamos el modelo a Alice
	Loss:0.2937106899184867

	Paso 3: enviamos el modelo a Sue
	Loss:0.03333996697717589

	Modelo promedio de todos los modelos
	% Correcto en el conjunto de entrenamiento: 84.05
Iteramos

Iniciando la ronda de entrenamiento...
	Paso 1: enviamos el modelo a Bob
	Loss:0.0662536748363041

	Paso 2: enviamos el modelo a Alice
	Loss:0.0959537422555682

	Paso 3: enviamos el modelo a Sue
	Loss:0.02029024788114074

	Modelo promedio de todos los modelos
	% Correcto en el conjunto de entrenamiento: 92.25
Iteramos

Iniciando la ronda de entrenamiento...
	Paso 1: enviamos el modelo a Bob
	Loss:0.030819682914453826

	Paso 2: enviamos el modelo a Alice
	Loss:0.03580324891736089

	Paso 3: enviamos el modelo a Sue
	Loss:0.01536846160847025

	Modelo promedio de todos los modelos
	% Correcto en el conjunto de entrenamiento: 98.8
Iteramos

Entrenando de esta manera obtenemos un modelo con casi el mismo rendimiento que el modelo centralizado, y en teoría no necesitamos tener acceso a los datos de entrenamiento para que cada usuario cambie el modelo de alguna manera.

De esta manera, es posible descubrir algo de los datasets con los que se está entrenando? Que de alguna manera, durante el entrenamiento, se pueda descubrir algo del dataset de un usuario y así vulnerar la privacidad del mismo?

Vulnerando Federated Learning

Veamos un ejemplo en donde como es posible que un modelo memorice información del conjunto de entrenamiento y por ende, vulnerar la privacidad de un usuario.

Federated Learning tiene dos grandes desafíos:

  • Rendimiento o Performance
  • Privacidad

Los cuales son más difíciles de manejar cuando cada usuario tiene un dataset de entrenamiento con muy pocos ejemplos. Si tenemos miles de ususarios, cada uno con muy pocos ejemplos pasa que:

  1. El modelo en lugar de generalizar, empieza a memorizar los datos utilizados para su entrenamiento.
  2. Se pasa más tiempo enviando y recibiendo el modelo de los usuarios y poco tiempo entrenando el modelo en sí.

Miremos un ejemplo donde uno de los usuarios tiene muy pocos ejemplos de datos

bobs_email = ["my", "computer", "password", "is", "pizza"]

bob_input = np.array([[word2index[x] for x in bobs_email]])
bob_target = np.array([0])

model = Embedding(vocab_size=len(vocab), dim=1)
model.weight.data *= 0

bobs_model = train(copy.deepcopy(model), 
                   bob_input, 
                   bob_target, 
                   iterations=1, 
                   batch_size=1)
	Loss:0.25

Entrenamos el modelo de bob, pero bob solo tenía un correo, y no solamente eso, dicho correo contenía información sensible sobre como acceder a su computadora. Ahora, lo que nosotros obtuvimos es un modelo, no los datos de bob. Aún así, es posible vulnerar la privacidad de bob?

for i, v in enumerate(model.weight.data - bobs_model.weight.data):
    if (v != 0):
        print(vocab[i])
password
computer
pizza
is
my

Y así como así, solo se necesitó saber como variaron los pesos al actualizar el modelo para poder descubrir la contraseña de la computadora de bob, violando así su privacidad.

Que pasaría si pudieramos encriptar los modelos, realizar operaciones sobre el mismo y luego desencriptarlo para proteger la información?

Cifrado homomórfico

Realizar operaciones aritmeticas sobre valores encriptados es posible

Básicamente poder realizar operaciones aritméticas sobre valores encriptados se llama cifrado homomorfico. Cifrado Homomorfico es toda un área de investigacion en sí misma, en este notebook nos vamos a centrar en la capacidad de realizar adiciones entre valores encriptados. Contamos con:

  • Una clave pública para encriptar los valores y
  • Una clave privada para desencriptar los valores

Veamos un ejemplo de esto:

# En caso de ser la primera vez que se corre este notebook 
# y no se tiene la libreria phe, instalarla con la siguiente linea
# https://github.com/n1analytics/python-paillier
# !pip install phe
import phe
public_key, private_key = phe.generate_paillier_keypair(n_length=1024)

x = public_key.encrypt(5)
y = public_key.encrypt(3)

z = x + y

z_plain = private_key.decrypt(z)
print('El valor de z es:', z_plain)
El valor de z es: 8
# Otras operacioes posibles con encriptacion o cifrado homomorfico
w = x + 1
w_plain = private_key.decrypt(w)
print('El valor de w es:', w_plain)
w = x * 2
w_plain = private_key.decrypt(w)
print('El valor de w es:', w_plain)
El valor de w es: 6
El valor de w es: 10

Hagamos un ejemplo de lo que sería entrenar un modelo con encriptación homomorfica. Primero creemos una funcion que nos permita encriptar un modelo que hayamos entrenado

def train_and_encrypt(model, input, target, pubkey):
    # A fines demostrativos, esta funcion solo funciona 
    # para Embeddings con una sola dimension
    new_model = train(copy.deepcopy(model), input, target, iterations=1)
    
    encrypt_weights = list()
    for val in new_model.weight.data[:,0]:
        encrypt_weights.append(public_key.encrypt(val))
    ew = np.array(encrypt_weights).reshape(new_model.weight.data.shape)
    
    return ew
model = Embedding(vocab_size=len(vocab), dim=1)
model.weight.data *= 0

public_key, private_key = phe.generate_paillier_keypair(n_length=128)
for i in range(4):
    print('\nIniciando la Iteracion de entrenamiento...')
    print('\tPaso 1: enviar modelo a Bob')
    bob_encrypted_model = train_and_encrypt(copy.deepcopy(model),
                                            bob[0], bob[1], public_key)
    print('\n\tPaso 2: enviar modelo a Alice')
    alice_encrypted_model = train_and_encrypt(copy.deepcopy(model),
                                           alice[0], alice[1], public_key)
    print('\n\tPaso 1: enviar modelo a Sue')
    sue_encrypted_model = train_and_encrypt(copy.deepcopy(model),
                                           sue[0], sue[1], public_key)
    print('\n\tPaso 4: Bob, Alice y Sue envian')
    print('\ty agregan sus modelos encriptados ente sí')
    aggregated_model = bob_encrypted_model + \
                       alice_encrypted_model + \
                       sue_encrypted_model
    
    print('\n\tPaso 5: Solo el modelo agregado')
    print('\tse envia devuelta al dueño del modelo')
    print('\tque puede desencriptarlo')
    raw_values = list()
    for val in aggregated_model.flatten():
        raw_values.append(private_key.decrypt(val))
    model.weight.data = np.array(raw_values).reshape(model.weight.data.shape)/3
    
    print("\tCorrectos en el conjunto de prueba:" + \
          str(test(model, test_data, test_target) * 100))
Iniciando la Iteracion de entrenamiento...
	Paso 1: enviar modelo a Bob
	Loss:0.21908166249699718

	Paso 2: enviar modelo a Alice
	Loss:0.2937106899184867

	Paso 1: enviar modelo a Sue
	Loss:0.03333996697717589

	Paso 4: Bob, Alice y Sue envian
	y agregan sus modelos encriptados ente sí

	Paso 5: Solo el modelo agregado
	se envia devuelta al dueño del modelo
	que puede desencriptarlo
	Correctos en el conjunto de prueba:84.05

Iniciando la Iteracion de entrenamiento...
	Paso 1: enviar modelo a Bob
	Loss:0.0662536748363041

	Paso 2: enviar modelo a Alice
	Loss:0.09595374225556819

	Paso 1: enviar modelo a Sue
	Loss:0.02029024788114074

	Paso 4: Bob, Alice y Sue envian
	y agregan sus modelos encriptados ente sí

	Paso 5: Solo el modelo agregado
	se envia devuelta al dueño del modelo
	que puede desencriptarlo
	Correctos en el conjunto de prueba:92.25

Iniciando la Iteracion de entrenamiento...
	Paso 1: enviar modelo a Bob
	Loss:0.030819682914453833

	Paso 2: enviar modelo a Alice
	Loss:0.0358032489173609

	Paso 1: enviar modelo a Sue
	Loss:0.01536846160847025

	Paso 4: Bob, Alice y Sue envian
	y agregan sus modelos encriptados ente sí

	Paso 5: Solo el modelo agregado
	se envia devuelta al dueño del modelo
	que puede desencriptarlo
	Correctos en el conjunto de prueba:98.8

Iniciando la Iteracion de entrenamiento...
	Paso 1: enviar modelo a Bob
	Loss:0.017275589333002585

	Paso 2: enviar modelo a Alice
	Loss:0.018830500591261824

	Paso 1: enviar modelo a Sue
	Loss:0.012752285302780164

	Paso 4: Bob, Alice y Sue envian
	y agregan sus modelos encriptados ente sí

	Paso 5: Solo el modelo agregado
	se envia devuelta al dueño del modelo
	que puede desencriptarlo
	Correctos en el conjunto de prueba:99.05000000000001