diff --git a/code/experiment.py b/code/experiment.py index 38dd3c2..5e3e50c 100644 --- a/code/experiment.py +++ b/code/experiment.py @@ -2,8 +2,7 @@ import os import time import dill from tqdm import tqdm - -from collections import defaultdict +import copy class Experiment: @@ -19,7 +18,7 @@ class Experiment: self.base_dir = self.experiment_name self.next_iteration = 0 self.log_messages = [] - self.data_storage = defaultdict(list) + self.historical_particles = dict() def __enter__(self): self.dir = os.path.join(self.base_dir, 'experiments', 'exp-{name}-{id}-{it}'.format( @@ -31,7 +30,7 @@ class Experiment: return self def __exit__(self, exc_type, exc_value, traceback): - self.save(experiment=self) + self.save(experiment=self.without_particles()) self.save_log() self.next_iteration += 1 @@ -43,14 +42,26 @@ class Experiment: with open(os.path.join(self.dir, "{name}.txt".format(name=log_name)), "w") as log_file: for log_message in self.log_messages: print(str(log_message), file=log_file) - + + def __copy__(self): + copy_ = Experiment(name=self.experiment_name,) + copy_.__dict__ = {attr: self.__dict__[attr] for attr in self.__dict__ if + attr not in ['particles', 'historical_particles']} + return copy_ + + def without_particles(self): + self_copy = copy.copy(self) + # self_copy.particles = [particle.states for particle in self.particles] + self_copy.historical_particles = {key: val.states for key, val in self.historical_particles.items()} + return self_copy + def save(self, **kwargs): for name, value in kwargs.items(): with open(os.path.join(self.dir, "{name}.dill".format(name=name)), "wb") as dill_file: dill.dump(value, dill_file) def add_trajectory_segment(self, run_id, trajectory): - self.data_storage[run_id].append(trajectory) + self.historical_particles[run_id].append(trajectory) return diff --git a/code/network.py b/code/network.py index 48c9b92..a18377d 100644 --- a/code/network.py +++ b/code/network.py @@ -3,7 +3,9 @@ import copy import numpy as np from keras.models import Sequential +from keras.callbacks import Callback from keras.layers import SimpleRNN, Dense +import keras.backend as K from util import * from experiment import * @@ -12,6 +14,20 @@ from experiment import * os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' +class SaveStateCallback(Callback): + def __init__(self, net, epoch=0): + super(SaveStateCallback, self).__init__() + self.net = net + self.init_epoch = epoch + + def on_epoch_end(self, epoch, logs={}): + description = dict(time=epoch+self.init_epoch) + description['action'] = 'train_self' + description['counterpart'] = None + self.net.save_state(**description) + return + + class NeuralNetwork(PrintingObject): @staticmethod @@ -64,6 +80,7 @@ class NeuralNetwork(PrintingObject): self.params = dict(epsilon=0.00000000000001) self.params.update(params) self.keras_params = dict(activation='linear', use_bias=False) + self.states = [] def get_model(self): raise NotImplementedError @@ -147,6 +164,23 @@ class NeuralNetwork(PrintingObject): def print_weights(self, weights=None): print(self.repr_weights(weights)) + def make_state(self, **kwargs): + weights = self.get_weights_flat() + state = {'class': self.__class__.__name__, 'weights': weights} + if any(np.isinf(weights)): + return None + state.update(kwargs) + return state + + def save_state(self, **kwargs): + state = self.make_state(**kwargs) + if state is not None: + self.states += [state] + else: + pass + def get_states(self): + return self.states + class WeightwiseNeuralNetwork(NeuralNetwork): @@ -600,10 +634,11 @@ class TrainingNeuralNetworkDecorator(): self.model_compiled = True return self - def train(self, batchsize=1): + def train(self, batchsize=1, store_states=True, epoch=0): self.compiled() x, y = self.net.compute_samples() - history = self.net.model.fit(x=x, y=y, verbose=0, batch_size=batchsize) + savestatecallback = SaveStateCallback(net=self.net, epoch=epoch) if store_states else None + history = self.net.model.fit(x=x, y=y, verbose=0, batch_size=batchsize, callbacks=[savestatecallback]) return history.history['loss'][-1] def train_other(self, other_network, batchsize=1): @@ -611,6 +646,7 @@ class TrainingNeuralNetworkDecorator(): other_network.compiled() x, y = other_network.net.compute_samples() history = self.net.model.fit(x=x, y=y, verbose=0, batch_size=batchsize) + return history.history['loss'][-1] @@ -648,17 +684,21 @@ if __name__ == '__main__': if True: # ok so this works quite realiably with FixpointExperiment() as exp: - run_count = 1000 - net = TrainingNeuralNetworkDecorator(WeightwiseNeuralNetwork(width=2, depth=2))\ - .with_params(epsilon=0.0001).with_keras_params(optimizer='sgd') - for run_id in tqdm(range(run_count+1)): - loss = net.compiled().train() - if run_id % 100 == 0: - net.print_weights() - # print(net.apply_to_network(net)) - print("Fixpoint? " + str(net.is_fixpoint())) - print("Loss " + str(loss)) - print() + for i in range(10): + + run_count = 1000 + net = TrainingNeuralNetworkDecorator(WeightwiseNeuralNetwork(width=2, depth=2))\ + .with_params(epsilon=0.0001).with_keras_params(optimizer='sgd') + for run_id in tqdm(range(run_count+1)): + loss = net.compiled().train(epoch=run_id) + if run_id % 100 == 0: + net.print_weights() + # print(net.apply_to_network(net)) + print("Fixpoint? " + str(net.is_fixpoint())) + print("Loss " + str(loss)) + print() + exp.historical_particles[i] = net + K.clear_session() if False: # this does not work as the aggregation function screws over the fixpoint computation.... # TODO: check for fixpoint in aggregated space... diff --git a/code/soup.py b/code/soup.py index 1f51e71..ec26cb7 100644 --- a/code/soup.py +++ b/code/soup.py @@ -57,28 +57,30 @@ class Soup: other_particle_id = int(prng() * len(self.particles)) other_particle = self.particles[other_particle_id] particle.attack(other_particle) - description['attacking'] = other_particle.get_uid() + description['action'] = 'attacking' + description['counterpart'] = other_particle.get_uid() if prng() < self.params.get('train_other_rate'): other_particle_id = int(prng() * len(self.particles)) other_particle = self.particles[other_particle_id] particle.train_other(other_particle) - description['training'] = other_particle.get_uid() + description['action'] = 'train_other' + description['counterpart'] = other_particle.get_uid() for _ in range(self.params.get('train', 0)): loss = particle.compiled().train() description['fitted'] = self.params.get('train', 0) description['loss'] = loss + description['action'] = 'train_self' + description['counterpart'] = None if self.params.get('remove_divergent') and particle.is_diverged(): new_particle = self.generate_particle() self.particles[particle_id] = new_particle - description['died'] = True - description['cause'] = 'divergent' - description['substitute'] = new_particle.get_uid() + description['action'] = 'divergent_dead' + description['counterpart'] = new_particle.get_uid() if self.params.get('remove_zero') and particle.is_zero(): new_particle = self.generate_particle() self.particles[particle_id] = new_particle - description['died'] = True - description['cause'] = 'zero' - description['substitute'] = new_particle.get_uid() + description['action'] = 'zweo_dead' + description['counterpart'] = new_particle.get_uid() particle.save_state(**description) def count(self): @@ -120,15 +122,22 @@ class ParticleDecorator: return self.uid def make_state(self, **kwargs): - state = {'class': self.net.__class__.__name__, 'weights': self.net.get_weights()} + weights = self.net.get_weights_flat() + if any(np.isinf(weights)): + return None + state = {'class': self.net.__class__.__name__, 'weights': weights} state.update(kwargs) return state def save_state(self, **kwargs): state = self.make_state(**kwargs) - self.states += [state] + if state is not None: + self.states += [state] + else: + pass def update_state(self, number, **kwargs): + raise NotImplementedError('Result is vague') if number < len(self.states): self.states[number] = self.make_state(**kwargs) else: diff --git a/code/visualization.py b/code/visualization.py index 773b67a..15e4f3c 100644 --- a/code/visualization.py +++ b/code/visualization.py @@ -24,20 +24,24 @@ def build_args(): return arg_parser.parse_args() -def build_from_soup(soup): +def build_from_soup_or_exp(soup): particles = soup.historical_particles - particle_dict = [dict(trajectory=[timestamp['weights'] for timestamp in particle], - fitted=[timestamp['fitted'] for timestamp in particle], - loss=[timestamp['loss'] for timestamp in particle], - time=[timestamp['time'] for timestamp in particle]) for particle in particles.values()] - return particle_dict + particle_list = [] + for particle in particles.values(): + particle_dict = dict( + trajectory=[event['weights'] for event in particle], + time=[event['time'] for event in particle], + action=[event['action'] for event in particle], + counterpart=[event['counterpart'] for event in particle] + ) + particle_list.append(particle_dict) + return particle_list def plot_latent_trajectories(soup_or_experiment, filename='latent_trajectory_plot'): assert isinstance(soup_or_experiment, (Experiment, Soup)) bupu = cl.scales['11']['div']['RdYlGn'] - data_dict = soup_or_experiment.data_storage if isinstance(soup_or_experiment, Experiment) \ - else build_from_soup(soup_or_experiment) + data_dict = build_from_soup_or_exp(soup_or_experiment) scale = cl.interp(bupu, len(data_dict)+1) # Map color scale to N bins # Fit the mebedding space @@ -91,25 +95,22 @@ def plot_latent_trajectories_3D(soup_or_experiment, filename='plot'): def norm(val, a=0, b=0.25): return (val - a) / (b - a) - data_dict = soup_or_experiment.data_storage if isinstance(soup_or_experiment, Experiment) \ - else build_from_soup(soup_or_experiment) + data_list = build_from_soup_or_exp(soup_or_experiment) bupu = cl.scales['11']['div']['RdYlGn'] - scale = cl.interp(bupu, len(data_dict)+1) # Map color scale to N bins + scale = cl.interp(bupu, len(data_list)+1) # Map color scale to N bins # Fit the embedding space transformer = TSNE() - for particle_dict in data_dict: - array = np.asarray([np.hstack([x.flatten() for x in timestamp]).flatten() - for timestamp in particle_dict['trajectory']]) - particle_dict['trajectory'] = array + for particle_dict in data_list: + array = np.asarray(particle_dict['trajectory']) transformer.fit(array) # Transform data accordingly and plot it data = [] - for p_id, particle_dict in enumerate(data_dict): + for p_id, particle_dict in enumerate(data_list): transformed = transformer._fit(particle_dict['trajectory']) - trace = go.Scatter3d( + line_trace = go.Scatter3d( x=transformed[:, 0], y=transformed[:, 1], z=np.asarray(particle_dict['time']), @@ -120,9 +121,28 @@ def plot_latent_trajectories_3D(soup_or_experiment, filename='plot'): # showlegend=True, hoverinfo='text', mode='lines') - data.append(trace) - layout = go.Layout(scene=dict(aspectratio=dict(x=2, y=2, z=1), + line_start = go.Scatter3d(mode='markers', x=[transformed[0, 0]], y=[transformed[0, 1]], + z=np.asarray(particle_dict['time'][0]), + marker=dict( + color='rgb(255, 0, 0)', + size=4 + ), + showlegend=False + ) + + line_end = go.Scatter3d(mode='markers', x=[transformed[-1, 0]], y=[transformed[-1, 1]], + z=np.asarray(particle_dict['time'][-1]), + marker=dict( + color='rgb(0, 0, 0)', + size=4 + ), + showlegend=False + ) + + data.extend([line_trace, line_start, line_end]) + + layout = go.Layout(scene=dict(aspectratio=dict(x=2, y=2, z=2), xaxis=dict(tickwidth=1, title='Transformed X'), yaxis=dict(tickwidth=1, title='transformed Y'), zaxis=dict(tickwidth=1, title='Epoch')),