# 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)
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))
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')
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:
- Cada usuario envia sus datos a un servidor central
- El servidor central entrena un modelo global en base a los datos
- 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]))
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')
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:
- El modelo en lugar de generalizar, empieza a memorizar los datos utilizados para su entrenamiento.
- 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)
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])
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)
# 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)
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))