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__":
......
import logging
import operator
from itertools import count
from typing import List
import attr
import numpy as np
import pulp
from config import RootConfig
from src.data.loader import np_arrays_from_scored_csv
from src.neural_networks.network import NeuralNetwork, FormattedData
from Actors.Attacker import Attacker
from Actors.Defender import Defender
from config import ModelConfig
from src.neural_networks.network import NeuralNetwork
logger = logging.getLogger(__name__)
......@@ -23,159 +22,45 @@ class Result:
probs_p2: List = attr.ib()
# Lazy evaluation wrapper for new best response similarity calculation
class LazyWrapper(object):
def __init__(self, func):
self.func = func
self.value = None
def __call__(self):
try:
return self.value
except AttributeError:
self.value = self.func()
return self.value
class GameSolver:
def __init__(self, conf: RootConfig):
self.conf = conf
self.utility = conf.base_conf.utility_function
train = conf.nn_train_conf
raw_benign_x, _ = np_arrays_from_scored_csv(train.benign_data_file_name,
0, train.benign_data_count)
self.benign_data = self.prepare_benign_data(raw_benign_x)
def prepare_benign_data(self, raw_x_data):
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)
def _get_trained_nn(self, attack: FormattedData) -> NeuralNetwork:
# Initialize the model
network = NeuralNetwork(self.conf.base_conf.features_count,
self.conf.nn_conf,
self.conf.nn_train_conf)
network.set_data(self.benign_data, attack)
network.train()
return network
def get_empty_data(self) -> FormattedData:
non_features = np.ndarray((0, self.conf.base_conf.features_count))
non_1d = np.array([])
return FormattedData(non_features, non_1d, non_1d)
def double_oracle(self, actions_p1: List) -> Result:
non_attack = self.get_empty_data()
def __init__(self, conf: ModelConfig):
self.attacker_utility = conf.attacker_utility
# Define game actors
self.attacker = Attacker(conf)
self.defender = Defender(conf)
def double_oracle(self) -> Result:
# Get initial actions as the first ones
played_actions_p1 = set(actions_p1[:1])
played_actions_p2 = {self._get_trained_nn(non_attack)}
played_actions_p1 = [self.attacker.get_initial_action()]
played_actions_p2 = [self.defender.get_initial_action()]
# TODO refactor all python lists to numpy arrays
for i in count():
logger.debug(f'Iteration: {i}\n')
ordered_actions_p1 = list(played_actions_p1)
ordered_actions_p2 = list(played_actions_p2)
# Solve current game with linear programming
value, probs_p1, probs_p2 = self.solve_zero_sum_game_pulp(
ordered_actions_p1, ordered_actions_p2)
value, probs_p1, probs_p2 = self.solve_zero_sum_game(
played_actions_p1, played_actions_p2)
# Find best responses for each player given the mixture strategies
br_p1 = self.best_response_p1(actions_p1, ordered_actions_p2, probs_p2)
br_p2 = self.best_response_p2(ordered_actions_p1, probs_p1)
br_p1 = self.attacker.get_best_response(played_actions_p2, probs_p2)
br_p2 = self.defender.get_best_response(played_actions_p1, probs_p1)
br_p1_exists = self.does_br_p1_exist(br_p1, ordered_actions_p1,
ordered_actions_p2)
br_p2_exists = self.does_br_p2_exist(br_p2, ordered_actions_p2,
ordered_actions_p1)
br_p1_exists = self.attacker.does_br_exists(br_p1, played_actions_p1,
played_actions_p2)
br_p2_exists = self.defender.does_br_exists(br_p2, played_actions_p2,
played_actions_p1)
# If there is no new action in best responses, algorithm ends
if br_p1_exists and br_p2_exists:
return Result(value, ordered_actions_p1, probs_p1,
ordered_actions_p2, probs_p2)
return Result(value, played_actions_p1, probs_p1,
played_actions_p2, probs_p2)
# Otherwise add new actions to lists and continue
if not br_p1_exists: played_actions_p1.add(br_p1)
if not br_p2_exists: played_actions_p2.add(br_p2)
def does_br_p1_exist(self, new_br, old_brs, neural_networks_p2):
logger.debug('Comparing new br of player1 with the existing ones:')
if new_br in old_brs:
logger.debug('This attacker action already exists')
return True
else: # TODO just for test
logger.debug('This attacker action does not exist yet')
return False
utilities_of_new_action = [self.utility(new_br, a2) for a2 in neural_networks_p2]
for old_br in old_brs:
as_good = True
for new_utility, nn in zip(utilities_of_new_action, neural_networks_p2):
old_utility = self.utility(old_br, nn)
if abs(old_utility - new_utility) > 0.05:
as_good = False
break
if as_good:
logger.debug('This attacker action already exists')
return True
logger.debug('This attacker action does not exist yet')
return False
def does_br_p2_exist(self, new_nn: NeuralNetwork, old_nns: List,
actions_p1):
# TODO tried adding fp_cost aswell for test
logger.debug('Comparing new neural network with the existing ones:')
utilities_of_new_nn = [self.utility(a1, new_nn)+new_nn.final_fp_cost for a1 in actions_p1]
for old_nn in old_nns:
as_good = True
for new_utility, action_p1 in zip(utilities_of_new_nn, actions_p1):
old_utility = self.utility(action_p1, old_nn) + old_nn.final_fp_cost
if abs(old_utility - new_utility) > self.conf.base_conf.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
def best_response_p1(self, actions_p1, used_actions_p2, probs_p2):
# Take only defenders actions which are played with non zero probability
non_zero_p = np.where(np.asarray(probs_p2) != 0)
actions_2 = np.asarray(used_actions_p2)[non_zero_p]
p2 = np.asarray(probs_p2)[non_zero_p]
return max(actions_p1, key=lambda a1: sum(map(operator.mul, map(
lambda a2: self.utility(a1, a2), actions_2), p2)))
def best_response_p2(self, used_actions_p1, probs_p1):
# Take only attacker actions which are played with non zero probability
non_zero_p = np.where(np.asarray(probs_p1) != 0)
unique_attack_x = np.asarray(used_actions_p1)[non_zero_p]
attack_probs = np.asarray(probs_p1)[non_zero_p]
attack_y = np.ones(len(unique_attack_x))
attack = FormattedData(unique_attack_x, attack_probs, attack_y)
logger.debug('Let\'s train new best NN with this malicious data:')
logger.debug(f'{unique_attack_x}\n')
best_nn = self._get_trained_nn(attack)
for _ in range(1, self.conf.base_conf.number_of_nn_to_train):
new_nn = self._get_trained_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')
return best_nn
def solve_zero_sum_game_pulp(self, actions_p1: List[List[float]],
if not br_p1_exists: played_actions_p1.append(br_p1)
if not br_p2_exists: played_actions_p2.append(br_p2)
def solve_zero_sum_game(self, actions_p1: List[List[float]],
actions_p2: List[NeuralNetwork]):
logger.debug('Going to solve current state with LP')
......@@ -196,12 +81,8 @@ class GameSolver:
# Calc false positive cost with benign data probability distribution
fp_cost = 0
benign_points = self.benign_data.unique_x
benign_probs = self.benign_data.probs_x
for features, features_prob in zip(benign_points, benign_probs):
for nn, nn_prob in zip(actions_p2, probs_p_two):
l = nn.predict_single_limit(features)
fp_cost += (l**4) * features_prob * nn_prob
for nn, nn_prob in zip(actions_p2, probs_p_two):
fp_cost += nn.final_fp_cost * nn_prob
# Define main constraints
constraints = []
......@@ -209,7 +90,7 @@ class GameSolver:
suma = [fp_cost]
j = 0
for a2 in actions_p2:
suma.append(probs_p_two[j] * self.utility(a1, a2))
suma.append(probs_p_two[j] * self.attacker_utility(a1, a2))
j += 1
constraints.append(pulp.lpSum(suma) <= v)