<jupyter_start><jupyter_text>Parte 12 bis - Entrenamiento Seguro y Evaluación con MNISTCuando construimos Soluciones de Aprendizaje Automático como un Servicio (MLaaS), la compañía puede pedir acceso a los datos desde otros socios para entrenar su modelo. En salud o finanzas, el modelo y los datos son críticos: los parámetros del modelo son un activo comercial mientras que los datos personales son algo estrictamente regulado. En este contexto, una solución posible es encriptar ambos el modelo y los datos para entrenar el modelo de aprendizaje automático con los valores encriptados. Esto garantiza que por ejemplo, la compañía no acceda a los registros médicos de los pacientes y que las instalaciones médicas no puedan observar el modelo al que contribuyen. Existen varios esquemas que permiten computaciones sobre datos encriptados, la Computación Segura Multiparte (SMPC), el Encriptado Homomórfico (FHE/SHE) y el Encriptado Funcional (FE). Nos enfocaremos en la Computación Multiparte (introducida en el tutorial 5) la cual consiste en uso compartido de aditivos privados y se basa en los protocolos de cifrado SecureNN y SPDZ.La configuración exacta de este tutorial es la siguiente: considera que eres el único servidor y te gustaría entrenar tu modelo con datos en poder de $n$ trabajadores. El servidor comparte el secreto del modelo y manda cada parte del secreto a un trabajador. Los trabajadores también comparten el secreto de sus datos y lo intercambian entre ellos. En la configuración que estudiaremos hay 2 trabajadores: Alice y Bob. Después de intercambiar secretos, cada uno ahora tiene su propia parte, una parte del otro trabajador y una parte del modelo. Ahora la computación puede empezar entrenando privadamente el modelo usando los protocolos criptográficos apropiados. Una vez que el modelo es entrenado, todas las partes pueden ser regresadas al servidor para que las descifre. Esto es ilustrado con la siguiente figura: ![SMPC Illustration](https://github.com/OpenMined/PySyft/raw/11c85a121a1a136e354945686622ab3731246084/examples/tutorials/material/smpc_illustration.png) Para dar un ejemplo de este proceso, asumamos que Alice y Bob tienen ambos una parte del conjuto de datos MNIST y entrenemos un modelo para hacer clasificación de digitos.Autores:- <NAME> - Twitter [@theoryffel](https://twitter.com/theoryffel) - GitHub: [@LaRiffle](https://github.com/LaRiffle)- <NAME> - Twitter [@bobbyawagner](https://twitter.com/bobbyawagner) - GitHub: [@robert-wagner](https://github.com/robert-wagner)- <NAME> - Twitter [@hereismari](https://twitter.com/hereismari) - GitHub: [@mari-linhares](https://github.com/mari-linhares)	Traducción:- <NAME> - Twitter: [@arturomf94](https://twitter.com/arturomf94) - <NAME> - Twitter: [@ricardopretelt](https://twitter.com/ricardopretelt)- <NAME> - Github: [@socd06](https://github.com/socd06) - <NAME>-Quevedo - GitHub: [@thefirebanks](https://www.github.com/thefirebanks)  1. Demostración de Entrenamiento Encriptado con MNIST  Importaciones y configuración del entrenamiento<jupyter_code>import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms

import time<jupyter_output><empty_output><jupyter_text>Esta clase describe todos los hiperparámetros para el entrenamiento. Nota que aquí todos son públicos.<jupyter_code>class Arguments():
    def __init__(self):
        self.batch_size = 64
        self.test_batch_size = 64
        self.epochs = epochs
        self.lr = 0.02
        self.seed = 1
        self.log_interval = 1 # Log info at each batch
        self.precision_fractional = 3

args = Arguments()

_ = torch.manual_seed(args.seed)<jupyter_output><empty_output><jupyter_text>Aquí están las importaciones de PySyft. Nos conectamos a dos trabajadores remotos que llamaremos `alice` y `bob` y pedimos otro trabajador llamado `crypto_provider` que da todos los primitivos cripto que necesitamos.<jupyter_code>import syft as sy  # importar la librería Pysyft 
hook = sy.TorchHook(torch)  # enganchar PyTorch para agregar funcionalidad extra como el Aprendizaje Federado y el Aprendizaje Encriptado

# funciones de simulación 
def connect_to_workers(n_workers):
    return [
        sy.VirtualWorker(hook, id=f"worker{i+1}")
        for i in range(n_workers)
    ]
def connect_to_crypto_provider():
    return sy.VirtualWorker(hook, id="crypto_provider")

workers = connect_to_workers(n_workers=2)
crypto_provider = connect_to_crypto_provider()<jupyter_output><empty_output><jupyter_text>Conseguir acceso y compartir el secreto de los datosAquí usamos una función utilitaria que simula el siguiente comportamiento: asumimos que el conjunto de datos MNIST está distribuído en partes las cuales están en poder de uno de nuestros trabajadores. Los trabajadores entonces dividen sus datos en grupos y comparten el secreto de sus datos entre ellos. El objeto final regresado es un iterable sobre los grupos de secretos partidos, que llamamos **private data loader**. Nota que durante el proceso el trabajador local (nosotros) nunca tuvo acceso a los datos.Como usualmente, obtenemos un conjunto de datos de entrenamiento y prueba privado, y se comparten los secretos de ambas las entradas y las etiquetas.<jupyter_code>def get_private_data_loaders(precision_fractional, workers, crypto_provider):
    
    def one_hot_of(index_tensor):
        """
        Transform to one hot tensor
        
        Example:
            [0, 3, 9]
            =>
            [[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
             [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
             [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]
            
        """
        onehot_tensor = torch.zeros(*index_tensor.shape, 10) # 10 clases para MNIST
        onehot_tensor = onehot_tensor.scatter(1, index_tensor.view(-1, 1), 1)
        return onehot_tensor
        
    def secret_share(tensor):
        """
        Transform to fixed precision and secret share a tensor
        """
        return (
            tensor
            .fix_precision(precision_fractional=precision_fractional)
            .share(*workers, crypto_provider=crypto_provider, requires_grad=True)
        )
    
    transformation = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
    
    train_loader = torch.utils.data.DataLoader(
        datasets.MNIST('../data', train=True, download=True, transform=transformation),
        batch_size=args.batch_size
    )
    
    private_train_loader = [
        (secret_share(data), secret_share(one_hot_of(target)))
        for i, (data, target) in enumerate(train_loader)
        if i < n_train_items / args.batch_size
    ]
    
    test_loader = torch.utils.data.DataLoader(
        datasets.MNIST('../data', train=False, download=True, transform=transformation),
        batch_size=args.test_batch_size
    )
    
    private_test_loader = [
        (secret_share(data), secret_share(target.float()))
        for i, (data, target) in enumerate(test_loader)
        if i < n_test_items / args.test_batch_size
    ]
    
    return private_train_loader, private_test_loader
    
    
private_train_loader, private_test_loader = get_private_data_loaders(
    precision_fractional=args.precision_fractional,
    workers=workers,
    crypto_provider=crypto_provider
)<jupyter_output><empty_output><jupyter_text>Especificación del Modelo Aquí está el modelo que usaremos, es uno bastante sencillo pero [ha demostrado un rendimiento razonablemente bueno en MNIST] (https://towardsdatascience.com/handwritten-digit-mnist-pytorch-977b5338e627)<jupyter_code>class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 128)
        self.fc2 = nn.Linear(128, 64)
        self.fc3 = nn.Linear(64, 10)

    def forward(self, x):
        x = x.view(-1, 28 * 28)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x<jupyter_output><empty_output><jupyter_text>Funciones de Entrenamiento y PruebaEl entrenamiento se hace casi como de costumbre, la verdadera diferencia es que no podemos usar las pérdidas como probabilidad de registro negativa (`F.nll_loss` en PyTorch) porque es bastante complicado reproducir estas funciones en SMPC. En cambio, usamos una pérdida más simple por Error Cuadrático Medio (MSE).<jupyter_code>def train(args, model, private_train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(private_train_loader): # <-- ahora son datos privados
        start_time = time.time()
        
        optimizer.zero_grad()
        
        output = model(data)
        
        # loss = F.nll_loss(output, target)  <-- aquí no es posible
        batch_size = output.shape[0]
        loss = ((output - target)**2).sum().refresh()/batch_size
        
        loss.backward()
        
        optimizer.step()

        if batch_idx % args.log_interval == 0:
            loss = loss.get().float_precision()
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\tTime: {:.3f}s'.format(
                epoch, batch_idx * args.batch_size, len(private_train_loader) * args.batch_size,
                100. * batch_idx / len(private_train_loader), loss.item(), time.time() - start_time))<jupyter_output><empty_output><jupyter_text>¡La función para probar no cambia!<jupyter_code>def test(args, model, private_test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in private_test_loader:
            start_time = time.time()
            
            output = model(data)
            pred = output.argmax(dim=1)
            correct += pred.eq(target.view_as(pred)).sum()

    correct = correct.get().float_precision()
    print('\nTest set: Accuracy: {}/{} ({:.0f}%)\n'.format(
        correct.item(), len(private_test_loader)* args.test_batch_size,
        100. * correct.item() / (len(private_test_loader) * args.test_batch_size)))<jupyter_output><empty_output><jupyter_text>¡Corramos el entrenamiento!Unas cuantas notas sobre lo que está pasando aquí. Primero, compartimos el secreto de todos los parámetros del modelo con nuestros trabajadores. Segundo, convertimos los hiperparámetros del optimizador a precisión fija. Nota que no necesitamos compartir el secreto de ellos porque son públicos en nuestro contexto, pero como los valores secretos compartidos viven en campos finitos todavía necesitamos moverlos en los campos finitos usando `.fix_precision` para ejecutar consistentemente operaciones como la actualización de los pesos $W \leftarrow W - \alpha * \Delta W$.<jupyter_code>model = Net()
model = model.fix_precision().share(*workers, crypto_provider=crypto_provider, requires_grad=True)

optimizer = optim.SGD(model.parameters(), lr=args.lr)
optimizer = optimizer.fix_precision() 

for epoch in range(1, args.epochs + 1):
    train(args, model, private_train_loader, optimizer, epoch)
    test(args, model, private_test_loader)<jupyter_output><empty_output><jupyter_text>¡Ahí lo tienes! 75% de precisión usando solo una pequeña fracción del conjunto de datos MNIST, empleando el 100% de entrenamiento encriptado.  2. DiscusiónTomemos un vistazo más de cerca al poder del entrenamiento encriptado analizando lo que acabamos de hacer.  2.1 Tiempo de ComputaciónLo primero es obviamente el tiempo de ejecución. Como seguramente te has dado cuenta, es mucho más lento que el entrenamiento regular solo de texto. En particular, una iteración sobre 1 grupo de 64 elementos toma 3.2s mientras que solo 13ms solamente usando PyTorch. Mientras que esto podría verse como un bloqueador, solo recuerda que aquí todo pasó remotamente y en el mundo encriptado: ni un solo dato fué revelado. Más específicamente, el tiempo para procesar un elemento es 50ms que no está tan mal. La verdadera pregunta es analizar cuando el entrenamiento encriptado es necesario y cuando la predicción encriptada por si sola es suficiente. Por ejemplo, 50ms para hacer una predicción en un escenario listo para producción es completamente aceptable.Un cuello de botella principal es el uso de costosas funciones de activación: activación relu con SMPC es muy costosa porque utiliza comparación privada y el protocolo SecureNN. Como una ilustración, si reemplazamos relu con una activación cuadrática como es hecho en varios artículos de investigación sobre computación encriptada como CryptoNets, bajamos de 3.2s a 1.2s.Como una regla general, la idea principal es encriptar solo lo que sea necesario, y este tutorial te enseña lo simple que puede ser hacerlo.  2.2 Retropropagación con SMPCPuedes preguntarte como hacemos retropropagación y actualizaciones de gradientes aunque estemos trabajando con enteros en campos finitos. Para hacerlo, hemos desarrollado un nuevo tensor syft llamado AutogradTensor. Este tutorial lo usó intensivamente aunque puedes no haberlo visto. Veámoslo imprimiendo el peso de un modelo:<jupyter_code>model.fc3.bias<jupyter_output><empty_output><jupyter_text>Y un elemento de datos:<jupyter_code>first_batch, input_data = 0, 0
private_train_loader[first_batch][input_data]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08074627993050834, 'fraction_numerical': 0.014502605937004306, 'mean_word_length': 3.9569449644327968, 'pattern_counts': {'":': 0, '<': 52, '<?xml version=': 0, '>': 49, 'https://': 12, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '250018', 'license': 'Apache-2.0', 'n_tokens_mistral': 4325, 'n_tokens_neox': 4141, 'n_words': 1348, 'path': 'examples/tutorials/translations/español/Parte 12 bis - Entrenamiento Encriptado con MNIST.ipynb', 'repo_name': 'gbarvinok/federatedlearning', 'size': '13303'}