Commit 6cd819a0 authored by Martin Řepa's avatar Martin Řepa

clean-up code a little, create actors package

parent 67ed777c
import operator
from typing import List
from config import ModelConfig
import numpy as np
import itertools
import logging
logger = logging.getLogger(__name__)
def create_attacker_actions(dimension: int):
one_axis = np.linspace(0, 1, 101) # [0.00, 0.01, 0.02, ..., 0.99, 1.00]
repeat = dimension - 1
generator = itertools.product(one_axis, *itertools.repeat(one_axis, repeat))
return np.array(list(generator))
class Attacker:
def __init__(self, model_conf: ModelConfig):
self.conf = model_conf.attacker_conf
self.features_count = model_conf.features_count
self.utility = model_conf.attacker_utility
self.actions: np.array = None
if not self.conf.use_gradient_descent:
self.actions = create_attacker_actions(self.features_count)
def get_best_response(self, def_actions: List, def_probs: List):
if self.conf.use_gradient_descent:
self._gradient_best_response(def_actions, def_probs)
else:
return self._discrete_best_response(def_actions, def_probs)
def _discrete_best_response(self, def_actions: List, def_probs: List) -> List:
# Take only defenders actions which are played with non zero probability
non_zero_p = np.where(np.asarray(def_probs) != 0)
actions_2 = np.asarray(def_actions)[non_zero_p]
p2 = np.asarray(def_probs)[non_zero_p]
best_rp = max(self.actions, key=lambda a1: sum(map(operator.mul, map(
lambda a2: self.utility(a1, a2), actions_2), p2)))
return list(best_rp)
def _gradient_best_response(self, def_actions: List, def_probs: List) -> List:
# TODO
pass
def get_initial_action(self):
return self.get_best_response([], [])
def does_br_exists(self, new_br, old_brs, defenders_networks):
it_does = self._does_br_exists(new_br, old_brs, defenders_networks)
if it_does:
logger.debug('This attacker action already exists')
else:
logger.debug('This attacker action does not exist yet')
return it_does
def _does_br_exists(self, new_br, old_brs, defenders_networks):
if self.conf.use_gradient_descent:
return self._exists_by_epsilon(new_br, old_brs, defenders_networks)
else:
return new_br in old_brs
def _exists_by_epsilon(self, new_br, old_brs, defenders_networks):
u = self.utility
new_action_utilities = [u(new_br, a2) for a2 in defenders_networks]
for old_br in old_brs:
as_good = True
for new_utility, nn in zip(new_action_utilities, defenders_networks):
old_utility = u(old_br, nn)
# If difference is at least one time bigger, it's
# not similar action
if abs(old_utility - new_utility) > self.conf.epsion:
as_good = False
break
if as_good:
return True
return False
from typing import List
from config import ModelConfig
from data.loader import np_arrays_from_scored_csv
from neural_networks.network import NeuralNetwork, FormattedData
import numpy as np
import logging
logger = logging.getLogger(__name__)
def prepare_benign_data(raw_x_data) -> FormattedData:
unique, counts = np.unique(raw_x_data, axis=0, return_counts=True)
probs = np.array([count / len(raw_x_data) for count in counts])
benign_y = np.zeros(len(unique))
return FormattedData(unique, probs, benign_y)
class Defender:
def __init__(self, model_conf: ModelConfig):
self.conf = model_conf.defender_conf
self.features_count = model_conf.features_count
self.attacker_utility = model_conf.attacker_utility
# Prepare benign data
raw_x, _ = np_arrays_from_scored_csv(model_conf.benign_data_file_name,
0, model_conf.benign_data_count)
self.benign_data = prepare_benign_data(raw_x)
def get_best_response(self, att_actions: List, att_probs: List) -> NeuralNetwork:
# Take only attacker actions which are played with non zero probability
non_zero_p = np.where(np.asarray(att_probs) != 0)
attack_x = np.asarray(att_actions)[non_zero_p]
attack_probs = np.asarray(att_probs)[non_zero_p]
attack_y = np.ones(len(attack_x))
attack = FormattedData(attack_x, attack_probs, attack_y)
logger.debug('Let\'s train new best NN with this malicious data:')
logger.debug(f'{attack_x}\n')
best_nn = self._train_nn(attack)
for _ in range(1, self.conf.number_of_nn_to_train):
new_nn = self._train_nn(attack)
if new_nn.final_loss < best_nn.final_loss:
logger.debug(f'Found better nn. Old|New value: '
f'{best_nn.final_loss} | {new_nn.final_loss}')
best_nn = new_nn
else:
logger.debug(f'The previous nn was better, dif: '
f'{new_nn.final_loss - best_nn.final_loss}')
return best_nn
def get_initial_action(self) -> NeuralNetwork:
non_attack = self._get_empty_attack()
return self._train_nn(non_attack)
def _get_empty_attack(self) -> FormattedData:
non_features = np.ndarray((0, self.features_count))
non_1d = np.array([])
return FormattedData(non_features, non_1d, non_1d)
def _train_nn(self, attack: FormattedData) -> NeuralNetwork:
# Initialize the model
network = NeuralNetwork(self.features_count, self.conf.nn_conf)
network.set_data(self.benign_data, attack)
network.train()
return network
def does_br_exists(self, new_nn, old_nns, attacker_actions):
logger.debug('Comparing new neural network with the existing ones:')
new_nn_utilities = [ self.attacker_utility(a1, new_nn) +
new_nn.final_fp_cost for a1 in attacker_actions]
for old_nn in old_nns:
as_good = True
for new_utility, action_p1 in zip(new_nn_utilities, attacker_actions):
old_utility = self.attacker_utility(action_p1, old_nn) \
+ old_nn.final_fp_cost
if abs(old_utility - new_utility) > self.conf.defender_epsilon:
as_good = False
break
if as_good:
logger.debug('This neural network already exists')
return True
logger.debug('This neural network does not exist yet')
return False
......@@ -2,81 +2,87 @@ from typing import Callable
import attr
from utility import rate_limit_utility
from utility import attacker_rate_limit_utility
@attr.s
class NeuralNetworkConfig:
# Number of epochs in a neural network training phase
epochs: int = attr.ib(default=400)
epochs: int = attr.ib(default=600)
# String with loss_function definition.
# List of available functions: https://keras.io/losses/
# !!! DEPRECATED !!! for now
loss_function: str = attr.ib(default='binary_crossentropy')
# String with optimizer definition used to compile neural network model
# List of available optimizers: https://keras.io/optimizers/
# !!! DEPRECATED !!! for now
optimizer: str = attr.ib(default='adam')
# Value used for weighting the loss function (during training only) for
# malicious requests. This can be useful to tell the model to "pay more
# attention" to malicious samples.
# Setting it to 1 makes loss function behave equally for both predictions
# during training
# !!! DEPRECATED !!!
fp_weight: int = attr.ib(default=1)
# Learning rate for Adam optimiser
learning_rate = 0.5e-1
@attr.s
class TrainingNnConfig:
# Name of .csv file in src/data/scored directory with scored data which will
# be used as benign data in neural network training phase
benign_data_file_name: str = attr.ib(default='test.csv') #all_benign_scored.csv
class DefenderConfig:
# 2 neural networks are considered the same if difference of game value for
# them and each attacker's action is less than epsion
defender_epsilon: float = attr.ib(default=5e-3)
# Number of benign records to be used
benign_data_count: int = attr.ib(default=1000)
# This number of neural networks will be trained in each double oracle
# iteration and the best one will be considered as a best response
number_of_nn_to_train: int = attr.ib(default=7)
# conf of neural networks
nn_conf: NeuralNetworkConfig = attr.ib(default=NeuralNetworkConfig())
# Number \in [0-1] representing fraction of data used as validation dataset
validation_split: float = attr.ib(default=0.1)
# Specifying number of fake malicious DNS records created each
# iteration of double oracle algorithm from attacker's actions used in
# neural network training phase
malicious_data_count: int = attr.ib(default=100)
@attr.s
class AttackerConfig:
# If set to True gradient descent within pytorch.optim package
# is used to find attacker's best response
# If set to False, attacker actions are discrete and the whole space is
# traversed to find best response. Example for R^2:
# [(.0,.01),(.0,.02),...,(.1,.1)]
use_gradient_descent: bool = attr.ib(default=False)
# 2 attacker actions are considered the same if difference of absolute value
# of attacker's utility function for them and all defender's actions is less
# than this value
# Used only when use_gradient_descent is set to True
epsion: float = attr.ib(default=5e-3)
# Number of random tries to find attacker action using gradient descent.
# The one with best final loss value would be chosen.
# Used only when use_gradient_descent is set to True
tries_for_best_response: int = attr.ib(default=7)
@attr.s
class BaseConfig:
# Sets logger to debug level
debug: bool = attr.ib(default=True)
class ModelConfig:
# Name of .csv file in src/data/scored directory with scored data which will
# be used as benign data in neural network training phase
benign_data_file_name: str = attr.ib(default='test.csv') # all_benign_scored.csv
# Determine whether to plot final results
# Do not use if features_count > 2!
plot_result: bool = attr.ib(default=True)
# Number of benign records to be loaded
benign_data_count: int = attr.ib(default=1000)
# Number of features
features_count: int = attr.ib(default=2)
# This number of neural networks will be trained in each double oracle
# iteration and the best one will be considered as a best response
number_of_nn_to_train: int = attr.ib(default=7)
# Attacker
attacker_conf: AttackerConfig = attr.ib(default=AttackerConfig())
# 2 neural networks are considered the same if difference of game value for
# them and each attacker's action is less than epsion
epsilon: float = attr.ib(default=5e-3)
# Defender
defender_conf: DefenderConfig = attr.ib(default=DefenderConfig())
# Function to calculate utility given the actions
# Function to calculate utility for attacker given the actions
# f: List[float], NeuralNetwork -> float
utility_function: Callable = attr.ib(default=rate_limit_utility)
attacker_utility: Callable = attr.ib(default=attacker_rate_limit_utility)
@attr.s
class RootConfig:
base_conf: BaseConfig = attr.ib(default=BaseConfig())
nn_conf: NeuralNetworkConfig = attr.ib(default=NeuralNetworkConfig())
nn_train_conf: TrainingNnConfig = attr.ib(default=TrainingNnConfig())
# Sets logger to debug level
debug: bool = attr.ib(default=True)
# Determine whether to plot final results
# Do not use if features_count > 2!
plot_result: bool = attr.ib(default=True)
# Configuration of model used
model_conf: ModelConfig = attr.ib(default=ModelConfig())
if __name__ == "__main__":
......
import itertools
import logging
import numpy as np
from src.config import RootConfig
from src.game_solver import GameSolver, Result
from src.visual.plotter import Plotter
......@@ -10,37 +7,28 @@ from src.visual.plotter import Plotter
logger = logging.getLogger(__name__)
def setup_loger(conf: RootConfig):
def setup_loger(debug: bool):
log_format = ('%(asctime)-15s\t%(name)s:%(levelname)s\t'
'%(module)s:%(funcName)s:%(lineno)s\t%(message)s')
level = logging.DEBUG if conf.base_conf.debug else logging.INFO
level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(level=level, format=log_format)
class Game:
def __init__(self, conf: RootConfig = RootConfig()):
setup_loger(conf)
setup_loger(conf.debug)
self._conf = conf
self.result: Result = None
def _create_attacker_actions(self):
one_axis = np.linspace(0, 1, 101) # [0.00, 0.01, 0.02, ..., 0.99, 1.00]
# one_axis = np.linspace(0, 1, 11) # [0.0, 0.1, 0.2, ..., 0.9, 1.0]
axes = self._conf.base_conf.features_count - 1
return list(itertools.product(one_axis, *itertools.repeat(one_axis, axes)))
def solve_game(self):
logger.debug('Creating attacker\'s actions')
actions_attacker = self._create_attacker_actions()
logger.info("Starting game solver")
gs = GameSolver(self._conf)
self.result: Result = gs.double_oracle(actions_attacker)
gs = GameSolver(self._conf.model_conf)
self.result = gs.double_oracle()
self.write_summary()
self.plot_result()
self._write_summary()
self._plot_result()
def write_summary(self):
def _write_summary(self):
print('\n\n-------------------------------------------------')
logger.info(f'Game has ended with value: {self.result.value}')
logger.info('Attacker: action x probability')
......@@ -53,14 +41,15 @@ class Game:
logger.info(f'{nn} x {p}')
print('-------------------------------------------------')
def plot_result(self):
if self._conf.base_conf.plot_result:
logger.debug("Plotting result...")
p = Plotter(self.result.ordered_actions_p1,
self.result.probs_p1,
self.result.ordered_actions_p2,
self.result.probs_p2)
p.plot_result()
def _plot_result(self):
if not self._conf.plot_result:
return
logger.debug("Plotting result...")
p = Plotter(self.result.ordered_actions_p1,
self.result.probs_p1,
self.result.ordered_actions_p2,
self.result.probs_p2)
p.plot_result()
if __name__ == "__main__":
......
This diff is collapsed.
from typing import List, Tuple
from tensorflow import keras
import numpy as np
from sklearn.utils import shuffle
from config import NeuralNetworkConfig, TrainingNnConfig
from neural_networks.network import OrderCounter
def tmp_loss_function(y_true, y_pred):
return keras.backend.mean(100 * keras.backend.square(y_pred - y_true),
axis=-1)
class KerasNeuralNetwork:
def __init__(self, input_features=2,
nn_conf: NeuralNetworkConfig = NeuralNetworkConfig(),
nn_train_conf: TrainingNnConfig = TrainingNnConfig()):
self.model = keras.Sequential([
keras.layers.Dense(10, activation='relu',
input_shape=(input_features,)),
keras.layers.Dense(12, activation='relu'),
keras.layers.Dense(1, activation='sigmoid'),
]
)
# nn_conf.loss_function
self.model.compile(loss=tmp_loss_function, # Or 'binary_crossentropy'
optimizer=nn_conf.optimizer,
metrics=['accuracy'])
self.false_positives = None
self.epochs = nn_conf.epochs
self.fp_weight = nn_conf.fp_weight
self.validation_split = nn_train_conf.validation_split
self.order = OrderCounter.next()
def train(self,
attacker_features_x: List[List[float]],
benign_data: Tuple[np.ndarray, np.ndarray]):
x, y = benign_data
# There are some attacker's features
attacker_features_x = np.array(attacker_features_x)
if len(attacker_features_x[0]):
attacker_features_y = [1 for _ in attacker_features_x]
x = np.concatenate((x, attacker_features_x), axis=0)
y = np.concatenate((y, attacker_features_y), axis=0)
x, y = shuffle(x, y, random_state=1)
self.model.fit(x, y,
validation_split=self.validation_split,
epochs=self.epochs,
class_weight={0: 1, 1: self.fp_weight})
def calc_n0_false_positives(self, x_test: np.ndarray):
limits = self.predict_rate_limit(x_test)
self.false_positives = sum(map(lambda l: l ** 4, limits))
def predict(self, xs: np.ndarray):
return self.model.predict(xs)
def predict_rate_limit(self, xs: np.ndarray):
prediction = self.predict(xs)
return list(map(lambda x: 0 if x < 0.5 else (x - 0.5) * 2, prediction))
def predict_solo(self, attacker_features: List[float]) -> int:
features = np.array([attacker_features])
prediction = self.model.predict(features)
# returns number \in [0, 1]
return prediction[0][0]
def predict_solo_rate_limit(self, attacker_features: List[float]) -> int:
prediction = self.predict_solo(attacker_features)
return 0 if prediction < 0.5 else (prediction - 0.5) * 2
def get_false_positive_rate(self):
return self.false_positives
def __str__(self):
return f'(Neural network {self.order} with FP n0: {self.false_positives})'
if __name__ == "__main__":
pass
......@@ -4,16 +4,16 @@ from pathlib import Path
import attr
import numpy as np
import torch
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from torch import nn
from torch import optim
from config import NeuralNetworkConfig, TrainingNnConfig, RootConfig
from config import NeuralNetworkConfig, RootConfig
from src.data.loader import np_arrays_from_scored_csv
logger = logging.getLogger(__name__)
@attr.s
class FormattedData:
unique_x: np.array = attr.ib()
......@@ -32,8 +32,7 @@ class OrderCounter:
class NeuralNetwork:
def __init__(self, input_features=2,
nn_conf: NeuralNetworkConfig = NeuralNetworkConfig(),
nn_train_conf: TrainingNnConfig = TrainingNnConfig()):
nn_conf: NeuralNetworkConfig = NeuralNetworkConfig()):
self.model = nn.Sequential(
nn.Linear(input_features, 10),
nn.ReLU(),
......@@ -42,24 +41,22 @@ class NeuralNetwork:
nn.Linear(12, 1),
nn.Sigmoid()
)
self.epochs = nn_conf.epochs
self.validation_split = nn_train_conf.validation_split
self._set_weights()
self.conf = nn_conf
self.id = OrderCounter.next()
# Variables used for loss function
self.attacker_actions: FormattedData = None
self.benign_data: FormattedData = None
# Variable for value of loss function in last epoch measuring quality
# Variables from last training epoch measuring quality
self.final_loss = None
# PyTorch built in Binary Cross-entropy loss function
# self.loss_fn = nn.BCELoss()
self.final_fp_cost = None
def __str__(self):
return f'Neural network id:{self.id}, final loss: {self.final_loss}'
def set_data(self, benign_data, attack):
def set_data(self, benign_data: FormattedData, attack: FormattedData):
self.attacker_actions = attack
self.benign_data = benign_data
......@@ -83,20 +80,26 @@ class NeuralNetwork:
# Shuffle before splitting
x, y, probs = shuffle(x, y, probs, random_state=1)
# TODO use validation data aswell
self.x_train = torch.from_numpy(x).float()
self.y_train = torch.from_numpy(y).float()
self.probs_train = torch.from_numpy(probs).float()
def _set_weights(self):
def init_weights(m):
if type(m) == nn.Linear:
torch.nn.init.xavier_uniform(m.weight)
m.bias.data.fill_(.0)
self.model.apply(init_weights)
def train(self):
self._prepare_data()
self._train()
def _train(self):
learning_rate = 0.5e-2
learning_rate = self.conf.learning_rate
optimizer = torch.optim.Adam(self.model.parameters(), lr=learning_rate)
for e in range(self.epochs):
for e in range(self.conf.epochs):
# Forward pass: compute predicted y by passing x to the model.
train_limits = self._limit_predict(self.x_train, with_grad=True)
......@@ -107,7 +110,7 @@ class NeuralNetwork:
# Compute validation loss and report some info
if e % 5 == 0:
logging.debug(f'Epoch: {e}/{self.epochs},\t'
logging.debug(f'Epoch: {e}/{self.conf.epochs},\t'
f'TrainLoss: {loss},\t')
# Before the backward pass, use the optimizer object to zero all of
......@@ -124,7 +127,6 @@ class NeuralNetwork:
self.final_fp_cost = self._fp_cost_tensor(train_limits, self.y_train,
self.probs_train).item()
# TODO use validation set for final_loss (used in best_response_p2)
self.final_loss = loss
def _raw_predict(self, tensor: torch.Tensor):
......@@ -139,6 +141,7 @@ class NeuralNetwork:
raw_prediction = self._raw_predict(x)
# The same as lambda p: 0 if p < 0.5 else (p - 0.5) * 2
# TODO try to use e.g. sigmoid
clamped = raw_prediction.clamp(min=0.5, max=1)
limit = torch.mul(torch.add(clamped, -0.5), 2)
return limit
......
......@@ -188,8 +188,8 @@ def plot_loss(loss):
CURRENT_RUN = 5 # To name figures files in each iteration
if __name__ == "__main__":
# l = [.0, .25, .50, .75, 1.0]
l = [.0, .1, .2, .3, .4, .5, .6, .7, .8, .9, 1.]
l = [.0, .25, .50, .75, 1.0]
# l = [.0, .1, .2, .3, .4, .5, .6, .7, .8, .9, 1.]
attacker_actions = create_attacker_actions()
attacker_actions = list(
......@@ -224,7 +224,7 @@ if __name__ == "__main__":
res = solve_with_lp(attacker_actions, defender_actions, u,
loss_test, benign_data_prob, l)
game_val, attacker_probs, probs_defender = res
break
# Plot results
plot_loss(loss_test)
plot_summarization(defender_actions, probs_defender, l,
......
import itertools
import logging
import operator
import random
from pathlib import Path
from typing import List
import matplotlib.pyplot as plt
import numpy as np
import torch
import logging
from config import RootConfig
from data.loader import np_arrays_from_scored_csv
from neural_networks.network import FormattedData, NeuralNetwork
import numpy as np
import random
optimal_best_response = None
def setup_loger(conf):
def setup_loger(debug: bool):
log_format = ('%(asctime)-15s\t%(name)s:%(levelname)s\t'
'%(module)s:%(funcName)s:%(lineno)s\t%(message)s')
level = logging.DEBUG if conf.base_conf.debug else logging.INFO
level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(level=level, format=log_format)
def plot_gradient_path(points: [], order: int):
plt.title(f'Attempt {order}')
def plot_gradient_path(points: [], colors: ()):
plt.title(f'Attacker gradient descent')
plt.xlabel('x')
plt.ylabel('y')
plt.xlim(0, 1)
plt.ylim(0, 1)
for point in points:
plt.scatter(point[0], point[1], c='black', s=0.7)
for point, c in zip(points, colors):
plt.scatter(point[0], point[1], c=[c], s=0.7)
plt.scatter(optimal_best_response[0], optimal_best_response[1], c='red')
plt.annotate('Optimal by LP', optimal_best_response, color='red')
......@@ -70,12 +69,17 @@ def get_some_trained_nns(count: int) -> tuple:
return nns, probs
def find_attacker_best_responses(nns: List, probs: List, plot_path=True):
def find_attacker_best_responses(nns: List, probs: List, count: int, plot_path=True):
learning_rate = 0.5e-2
best_respnses = []
path_points = []
path_points_colors = []
# Try to find best respond n times
for action_num in range(10):
for action_num in range(count):
# random color
c = tuple(random.uniform(0,1) for i in range(3))
# Get random starting points
x = random.uniform(0.0, 1.0)
......@@ -85,7 +89,6 @@ def find_attacker_best_responses(nns: List, probs: List, plot_path=True):
# Create pytorch adam optimiser
optimizer = torch.optim.Adam([attacker_action], lr=learning_rate)
action_path = []
loss = None
for i in range(500):
for nn, prob in zip(nns, probs):
......@@ -98,7 +101,8 @@ def find_attacker_best_responses(nns: List, probs: List, plot_path=True):
tmp_x = round(attacker_action[0].item(), 4)
tmp_y = round(attacker_action[1].item(), 4)
action_path.append([tmp_x, tmp_y])
path_points.append([tmp_x, tmp_y])
path_points_colors.append(c)
action_str = f'{tmp_x, tmp_y}'
print(f'Attacker action={action_str},'
f' loss={loss.item()}')
......@@ -110,10 +114,10 @@ def find_attacker_best_responses(nns: List, probs: List, plot_path=True):
# Clamp input tensor to allowed range after gradient descent update
attacker_action.data.clamp_(min=0.0, max=1.0)
loss = None
if plot_path:
plot_gradient_path(action_path, action_num)
best_respnses.append(action_str)
if plot_path:
plot_gradient_path(path_points, path_points_colors)
return best_respnses
......@@ -127,16 +131,16 @@ def calc_optimal_br(nns: List, probs: List) -> tuple: