<jupyter_start><jupyter_text>Parte 08 - Introducción a los Planes  ContextoAquí introducimos un objeto crucial para escalar industrialmente el Aprendizaje Federado: El Plan. Reduce dramáticamente el uso de ancho de banda, permite esquemas asíncronos y da más autonomía a dispositivos remotos. El concepto original del Plan puede ser encontrado en el artículo [Towards Federated Learning at Scale: System Design](https://arxiv.org/pdf/1902.01046.pdf), pero ha sido adaptado a nuestras necesitades en la librería Pysyft.La intención de un Plan es almacenar una secuencia de operaciones torch, igual que una función, pero permitiendo enviar dicha secuencia de operaciones a trabajadores remotos y mantener una referencia a el. De esta manera, para computar remotamente la secuencia de $n$ operaciones en una entrada remota referenciada a través de punteros, en lugar de enviar $n$ mensajes ahora debes enviar un solo mensaje con las referencias al Plan y los punteros. También puedes proveer tensores con tu función (los que llamamos _state tensors_) para tener funcionalidades extendidas. Los Planes pueden ser vistos como una función que puedes enviar o como una clase que puede ser enviada y ejecutada remotamente. Por lo tanto, para usuarios de alto nivel, la noción de un Plan desaparece y es reemplazada por una característica mágica que permite permite enviar a trabajadores funciones arbitrarias que contienen funciones secuenciales torch.Algo para notar es que la clase de funciones que puedes transformar en Planes está actualmente limitada a secuencias de operaciones torch enganchadas. Esto excluye en particular a estructuras lógicas como declaraciones de `if`, `for` y `while`, aunque estamos trabajando para tener soluciones alternativas pronto. _Para ser completamente precisos, puedes usarlos pero el camino lógico que tomes (el primer `if` que resulte False y 5 bucles en `for`, por ejemplo) en la primer computación de tu Plan será en la que seguirás durante todas las siguientes computaciones, cosa que queremos evitar en la mayoría de los casos._ 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)   Importaciones y especificaciones para el modeloPrimero hagamos las importaciones oficiales.<jupyter_code>import torch
import torch.nn as nn
import torch.nn.functional as F<jupyter_output><empty_output><jupyter_text>Note que en las importaciones específicas a Pysyft: **el trabajador local no debe ser un trabajador de cliente.** *Trabajadores que no son de cliente pueden almacenar objetos y necesitamos de eso para correr un Plan.*<jupyter_code>import syft as sy  # importar la librería Pysyft
hook = sy.TorchHook(torch)  # enganchar PyTorch (por ejemplo para agregar funcionalidad extra)

# IMPORTANTE: El trabajador local no debe ser un trabajador de cliente
hook.local_worker.is_client_worker = False


server = hook.local_worker<jupyter_output><empty_output><jupyter_text>Definimos trabajadores remotos o _devices_, para que sean consistentes con las nociones provistas en el artículo de referencia. Los proveemos de unos datos.<jupyter_code>x11 = torch.tensor([-1, 2.]).tag('input_data')
x12 = torch.tensor([1, -2.]).tag('input_data2')
x21 = torch.tensor([-1, 2.]).tag('input_data')
x22 = torch.tensor([1, -2.]).tag('input_data2')

device_1 = sy.VirtualWorker(hook, id="device_1", data=(x11, x12)) 
device_2 = sy.VirtualWorker(hook, id="device_2", data=(x21, x22))
devices = device_1, device_2<jupyter_output><empty_output><jupyter_text>Ejemplo BásicoDefinamos una función que queremos transformar en un Plan. Hacerlo es tán simple como agregarle un decorador encima de la definición de la función.<jupyter_code>@sy.func2plan()
def plan_double_abs(x):
    x = x + x
    x = torch.abs(x)
    return x<jupyter_output><empty_output><jupyter_text>Revisemos, ¡ahora tenemos un plan!<jupyter_code>plan_double_abs<jupyter_output><empty_output><jupyter_text>Para usar un Plan, necesitas dos cosas: construir el Plan (_por ejemplo, registrar la secuencia de operaciones presentes en la función_) y enviarlo al trabajador / dispositivo. Afortunadamente puedes hacer esto muy fácilmente. Construir un PlanPara construir un Plan solo necesitas llamarlo con algo de datos.Primero consigamos la referencia para datos remotos: una requisición es enviada a la red y el puntero de referencia es regresado.<jupyter_code>pointer_to_data = device_1.search('input_data')[0]
pointer_to_data<jupyter_output><empty_output><jupyter_text>Si le decimos al plan que debe ser ejecutado remotamente en el dispositivo `location:device_1`... obtendremos un error porque el Plan no ha sido construido todavía.<jupyter_code>plan_double_abs.is_built
# Enviar un Plan no construido fallará
try:
    plan_double_abs.send(device_1)
except RuntimeError as error:
    print(error)<jupyter_output><empty_output><jupyter_text>Para construir un Plan solo necesitas llamar a `build` en el Plan y pasar los argumentos necesesarios para ejecutar el Plan (a.k.a unos datos). Cuando el Plan es construido, todos los comandos son ejecutados secuencialmente por el trabajador local,  son atrapados por el Plan y almacenados en el atributo `actions`.<jupyter_code>plan_double_abs.build(torch.tensor([1., -2.]))
plan_double_abs.is_built<jupyter_output><empty_output><jupyter_text>Ahora ya podemos enviar el plan<jupyter_code># Ahora podemos ejecutar exitosamente esta celda
pointer_plan = plan_double_abs.send(device_1)
pointer_plan<jupyter_output><empty_output><jupyter_text>Así como con los tensores, obtenemos un puntero al objeto enviado. Se le llama simplemente un `PointerPlan` Algo importante a recordar es que cuando un Plan es construido, predefinimos las id(s) donde el resultado(s) debe ser almacenado antes de hacer la computación. Esto permitirá enviar comandos asíncronamente, para ya tener una referencia a un resultado virtual y continuar las computaciones locales sin esperar que el resultado remoto. Una aplicación importante es cuando quieras hacer un cómputo de un grupo de datos en device_1 y no quieras esperar a que la computación termine para lanzar otro cómputo de grupo en device_2.  Correr un Plan remotamenteAhora podemos correr el Plan remotamente llamando el puntero al plan con un puntero a unos datos. Esto emite un comando a correr el plan remotamente, para que la localización predefinida de la salida del plan ahora contenga el resultado (recuerda que predefinimos la localización del resultado antes de la computación). Esto tambien necesita solo una ronda de comunicación.El resultado es simplemente un puntero, igual al que llamas cuando usas una función torch enganchada.<jupyter_code>pointer_to_result = pointer_plan(pointer_to_data)
print(pointer_to_result)<jupyter_output><empty_output><jupyter_text>Y solo pedimos el valor de vuelta<jupyter_code>pointer_to_result.get()<jupyter_output><empty_output><jupyter_text>Hacia un ejemplo concreto¿Queremos aplicar los Planes para el Aprendizaje Federado y Profundo, verdad? Veamos un ejemplo ligeramente más complicado, usando redes neuronales como quisieras hacerlo.Nota que ahora transformamos una clase en un Plan. Para hacerlo, heredamos nuestra clase de sy.Plan (en lugar de heredarla de nn.Module).<jupyter_code>class Net(sy.Plan):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

net = Net()
net<jupyter_output><empty_output><jupyter_text>Construyamos el plan usando datos simulados.<jupyter_code>net.build(torch.tensor([1., 2.]))<jupyter_output><empty_output><jupyter_text>Ahora enviamos el Plan a un trabajador remoto<jupyter_code>pointer_to_net = net.send(device_1)
pointer_to_net<jupyter_output><empty_output><jupyter_text>Recuperemos unos datos remotos<jupyter_code>pointer_to_data = device_1.search('input_data')[0]<jupyter_output><empty_output><jupyter_text>Después, la sintaxis es igual que la ejecución secuencial remota normal, igual que la ejecución local. Pero comparada a la ejecución remota, hay solo una ronda de comunicación por cada ejecución.<jupyter_code>pointer_to_result = pointer_to_net(pointer_to_data)
pointer_to_result<jupyter_output><empty_output><jupyter_text>Y obtenemos los resultados como de costumbre<jupyter_code>pointer_to_result.get()<jupyter_output><empty_output><jupyter_text>Et voilà! Hemos visto como reducir dramáticamente la comunicación entre el trabajador local (o servidor) y los dispositivos remotos.  Cambiar de trabajadoresUna característica importante que queremos tener es usar el mismo Plan para varios trabajadores, que cambiemos dependiendo del grupo remoto de datos que estemos considerando. En particular, no queremos reconstruir el Plan cada vez que cambiemos de trabajador. Veamos como hacer esto, usando el ejemplo anterior con nuestra pequeña red.<jupyter_code>class Net(sy.Plan):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)
net = Net()

# Construir el Plan
net.build(torch.tensor([1., 2.]))<jupyter_output><empty_output><jupyter_text>Aqui están los pasos principales que acabamos de ejecutar<jupyter_code>pointer_to_net_1 = net.send(device_1)
pointer_to_data = device_1.search('input_data')[0]
pointer_to_result = pointer_to_net_1(pointer_to_data)
pointer_to_result.get()<jupyter_output><empty_output><jupyter_text>Y de hecho, puedes construir otros PointerPlans del mismo Plan, dado que la sintaxis es la misma para correr remotamente un plan en otro dispositivo.<jupyter_code>pointer_to_net_2 = net.send(device_2)
pointer_to_data = device_2.search('input_data')[0]
pointer_to_result = pointer_to_net_2(pointer_to_data)
pointer_to_result.get()<jupyter_output><empty_output><jupyter_text>> Nota: Actualmente, con clases Plan, solo puedes usar un método y lo tienes que llamar "forward".  Construir Planes que son funciones automáticamente Para funciones (`@` `sy.func2plan`) que podemos construir automáticamente sin necesidad de explícitamente llamar `build`, en el momento de la creación el Plan ya está construido.Para conseguir esta funcionalidad lo único que necesitas cambiar cuando creas el Plan es configurar un argumento para el decorador llamado `args_shape` el cual debe ser una lista conteniendo las formas de cada argumento.<jupyter_code>@sy.func2plan(args_shape=[(-1, 1)])
def plan_double_abs(x):
    x = x + x
    x = torch.abs(x)
    return x

plan_double_abs.is_built<jupyter_output><empty_output><jupyter_text>El parámetro `args_shape` se usa interamente para crear tensores simulados con la forma dada que son usados para construir el Plan.<jupyter_code>@sy.func2plan(args_shape=[(1, 2), (-1, 2)])
def plan_sum_abs(x, y):
    s = x + y
    return torch.abs(s)

plan_sum_abs.is_built<jupyter_output><empty_output><jupyter_text>¡También puedes darle elementos de estado a las funciones!<jupyter_code>@sy.func2plan(args_shape=[(1,)], state=(torch.tensor([1]), ))
def plan_abs(x, state):
    bias, = state.read()
    x = x.abs()
    return x + bias
pointer_plan = plan_abs.send(device_1)
x_ptr = torch.tensor([-1, 0]).send(device_1)
p = pointer_plan(x_ptr)
p.get()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07838679801296623, 'fraction_numerical': 0.009514187084280543, 'mean_word_length': 5.861929520508377, 'pattern_counts': {'":': 0, '<': 99, '<?xml version=': 0, '>': 100, 'https://': 10, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '23', 'dir': 'jupyter-structured-clean-dedup', 'id': '272617', 'license': 'Apache-2.0', 'n_tokens_mistral': 4005, 'n_tokens_neox': 3961, 'n_words': 1269, 'path': 'examples/tutorials/translations/español/Parte 08 - Introducción a los Planes.ipynb', 'repo_name': 'gbarvinok/federatedlearning', 'size': '11931'}