-
Notifications
You must be signed in to change notification settings - Fork 1
/
GAN_AE.py
1265 lines (1053 loc) · 51.9 KB
/
GAN_AE.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Implement a class that build and train a GAN_AE achitecture
import numpy as np
from functools import partial
import concurrent.futures as thd
from threading import RLock
lock = RLock()
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers as Kl
from tensorflow.keras import models as Km
from tensorflow.keras.losses import binary_crossentropy as bc
from tensorflow.keras import optimizers as Kopt
from tensorflow.keras.initializers import glorot_normal as gn
from sklearn.metrics import auc
import matplotlib.pyplot as plt
# Define loss function for AE only model
def MeanL2Norm(y_true, y_pred):
return tf.reduce_mean(tf.sqrt(tf.reduce_sum(tf.square(y_pred - y_true),axis=1)))
# Define DisCo term
def DisCo(y_true, y_pred,var_1,var_2,power=1,alph=1.0):
'''
Taken from https://github.com/gkasieczka/DisCo/blob/master/Disco_tf.py
I just removed the 'normedweight' thing since I don't need it here.
'''
xx = tf.reshape(var_1, [-1, 1])
xx = tf.tile(xx, [1, tf.size(var_1)])
xx = tf.reshape(xx, [tf.size(var_1), tf.size(var_1)])
yy = tf.transpose(xx)
amat = tf.math.abs(xx-yy)
xx = tf.reshape(var_2, [-1, 1])
xx = tf.tile(xx, [1, tf.size(var_2)])
xx = tf.reshape(xx, [tf.size(var_2), tf.size(var_2)])
yy = tf.transpose(xx)
bmat = tf.math.abs(xx-yy)
amatavg = tf.reduce_mean(amat, axis=1)
bmatavg = tf.reduce_mean(bmat, axis=1)
minuend_1 = tf.tile(amatavg, [tf.size(var_1)])
minuend_1 = tf.reshape(minuend_1, [tf.size(var_1), tf.size(var_1)])
minuend_2 = tf.transpose(minuend_1)
Amat = amat-minuend_1-minuend_2+tf.reduce_mean(amatavg)
minuend_1 = tf.tile(bmatavg, [tf.size(var_2)])
minuend_1 = tf.reshape(minuend_1, [tf.size(var_2), tf.size(var_2)])
minuend_2 = tf.transpose(minuend_1)
Bmat = bmat-minuend_1-minuend_2+tf.reduce_mean(bmatavg)
ABavg = tf.reduce_mean(Amat*Bmat,axis=1)
AAavg = tf.reduce_mean(Amat*Amat,axis=1)
BBavg = tf.reduce_mean(Bmat*Bmat,axis=1)
if power==1:
dCorr = tf.reduce_mean(ABavg)/tf.math.sqrt(tf.reduce_mean(AAavg)*tf.reduce_mean(BBavg))
elif power==2:
dCorr = (tf.reduce_mean(ABavg))**2/(tf.reduce_mean(AAavg)*tf.reduce_mean(BBavg))
else:
dCorr = (tf.reduce_mean(ABavg)/tf.math.sqrt(tf.reduce_mean(AAavg)*tf.reduce_mean(BBavg)))**power
return alph*dCorr
# Define the full loss of the AE+D model
def _full_loss(y_true, y_pred,in_true,in_reco,eps):
return bc(y_true, y_pred)+eps*MeanL2Norm(in_true,in_reco)
# Define the full loss of the AE+D model (with extra DisCo term)
#def _full_loss_DisCo(y_true, y_pred,in_true,in_reco,eps,alph,var_1,power):
# var_2 = K.sqrt(K.sum(K.square(y_pred - y_true),axis=1))
# DC = DisCo(var_1,var_2,power)
# return bc(y_true, y_pred)+eps*MeanL2Norm(in_true,in_reco)+alph*DC
class GAN_AE():
'''
Provides all the methods to build, train and test one or several GAN-AE models.
Hyperarameters :
input_dim : Dimention of the AE input
hidden_dim : Dimension of hdden layers between input and latent space (AE architecture)
latent_dim : Dimention of AE latent space (bottleneck size)
dis_dim : dimension of the discriminator hiden layers (D architecture)
epsilon : Combined loss function parameter
alpha : Disco term parameter
power : The power used when calculating DisCo term
NGAN : Number of GAN (AE+discriminant) epochs per training cycle
ND : number of discriminant only epochs per training cycle
Ncycle : Total numbur of training cycle
batch_size : The batchsize used for training
early_stop : specify the number of epochs without improvment that trigger the early stpping (if None, no early stopping)
pretrain_AE : specify if the AE should be pretrained separatly before the GAN training
pretrain_dis : specify if the discriminant should be pretrained separatly before the GAN training
Nmodel : Total number of trained GAN-AE model
Nselec : Total number of selected GAN-AE model for averaging
Nworker : Maximum number of therads to un in parallel (must be 1 for tensorflow version>1.14.0)
'''
def __init__(self,
input_dim=10,hidden_dim=[7],latent_dim=5,
dis_dim=[100,70,50],
epsilon=0.2,alpha=None,power=1,
NGAN=4,ND=10,batch_size=1024,Ncycle=60,early_stop=5,pretrain_AE=False,pretrain_dis=True,
Nmodel=60,Nselec=10,Nworker=4
):
# Initialize parameters
self.input_dim = input_dim
self.hidden_dim = hidden_dim
self.latent_dim = latent_dim
self.dis_dim = dis_dim
self.epsilon = epsilon
self.alpha = alpha
self.power = power
self.NGAN = NGAN
self.ND = ND
self.batch_size = batch_size
self.Ncycle = Ncycle
self.early_stop = early_stop
self.pretrain_AE = pretrain_AE
self.pretrain_dis = pretrain_dis
self.Nmodel = Nmodel
self.Nselec = Nselec
self.Nworker = Nworker
# Initialize results
self.FoM = []
self.AE_weights = []
self.dis_weights = []
self.loss = []
self.distance = []
self.auc = []
return
# Prepare the data by rescaling everything between 0 and 1 (min-max scaler)
def scale_data(self,data):
'''
MinMax scaler (rescale the data between 0 and 1).
Argument :
data : The data to be scaled (given as a numpy array).
Returns :
res : The rescaled data.
dmin : The original minimum of all variables (for inverting the scaling).
dmax : The original maximum of all variables (for inverting the scaling).
'''
# Get the min and max of all variables
dmin = np.array([data[:,i].min() for i in range(data.shape[1])])
dmax = np.array([data[:,i].max() for i in range(data.shape[1])])
# Apply the min-max scaling
res = np.zeros(shape=data.shape)
for i in range(data.shape[1]):
res[:,i] = (data[:,i]-dmin[i])/(dmax[i]-dmin[i])
return res,dmin,dmax
# Invert the min-max scaler
def restore_data(self,data,dmin,dmax):
'''
Invert the MinMax scaler to restore the original data scale.
Argument :
data : The scaled data to be restored (given as a numpy array).
dmin : The original minimum of all variables.
dmax : The original maximum of all variables.
Return :
res : The restored data.
'''
# Revert the min-max scaling
res = np.zeros(shape=data.shape)
for i in range(data.shape[1]):
res[:,i]= (data[:,i]*(dmax[i]-dmin[i]))+dmin[i]
return res
# Build the GAN-AE architecture
def build(self,display=True):
'''
Method that builds a GAN-AE architecture and return its compiled components.
Arguments :
display : Specify if the detail of the built model shoud pe printed after building it.
Default to True.
Returns :
En : The compiled encoder keras model.
AE : The compiled AE keras model (encoder+decoder).
D : The compiled discriminator keras model (MLP).
GAN : The full compiled GAN-AE keras model (AE+D).
'''
# Encoder input
En_in = Kl.Input(shape=(self.input_dim,),name='En_input')
# Encoder hidden layers
for i in range(len(self.hidden_dim)):
if i==0:
En_h = Kl.Dense(self.hidden_dim[i],activation='linear',kernel_initializer=gn(),name='encode{}'.format(i+1))(En_in)
else:
En_h = Kl.Dense(self.hidden_dim[i],activation='linear',kernel_initializer=gn(),name='encode{}'.format(i+1))(En_h)
En_h = Kl.Dropout(0.2)(En_h)
En_h = Kl.ReLU()(En_h)
# Encoder output
En_co = Kl.Dense(self.latent_dim,activation='relu',kernel_initializer=gn(),name='En_code')(En_h)
En = Km.Model(En_in,En_co,name='Encoder')
# Decoder
De_in = Kl.Input(shape=(self.latent_dim,),name='De_input')
N = len(self.hidden_dim)
for i in range(N):
if i==0:
De_h = Kl.Dense(self.hidden_dim[N-i-1],activation='linear',kernel_initializer=gn(),name='encode{}'.format(i+1))(De_in)
else:
De_h = Kl.Dense(self.hidden_dim[N-i-1],activation='linear',kernel_initializer=gn(),name='encode{}'.format(i+1))(De_h)
De_h = Kl.Dropout(0.2)(De_h)
De_h = Kl.ReLU()(De_h)
De_out = Kl.Dense(self.input_dim,activation='sigmoid',kernel_initializer=gn(),name='De_outout')(De_h)
De = Km.Model(De_in,De_out,name='Decoder')
# Full generator/AE
AE_in = Kl.Input(shape=(self.input_dim,),name='Gen_input')
AE_mid = En(AE_in)
AE_out = De(AE_mid)
AE = Km.Model(AE_in,AE_out,name='Generator (AE)')
AE.compile(loss=MeanL2Norm,optimizer=Kopt.Adam(lr=0.0002,beta_1=0.5))
# Discriminator
Din = Kl.Input(shape=(self.input_dim,))
for i in range(len(self.dis_dim)):
if i==0:
Dh = Kl.Dense(self.dis_dim[i],activation='linear',kernel_initializer=gn(),name='Dis{}'.format(i+1))(Din)
else:
Dh = Kl.Dense(self.dis_dim[i],activation='linear',kernel_initializer=gn(),name='Dis{}'.format(i+1))(Dh)
Dh = Kl.Dropout(0.2)(Dh)
Dh = Kl.LeakyReLU(alpha=0.2)(Dh)
Dout = Kl.Dense(1,activation='sigmoid',name='Dis_output')(Dh)
D = Km.Model(Din,Dout,name='Discriminant')
D.compile(loss=bc,optimizer=Kopt.Adam(lr=0.0002,beta_1=0.5))
# Full GAN-AE
GANin = Kl.Input(shape=(self.input_dim,),name='GAN_input')
GANmid1 = En(GANin)
GANmid2 = De(GANmid1)
D.trainable = False
GANout = D(GANmid2)
if(self.alpha!=None):
aux_in = Kl.Input(shape=(1,),name=('aux_input'))
GAN = Km.Model([GANin,aux_in],[GANout,GANmid2],name='GAN_AE')
else:
GAN = Km.Model(GANin,GANout,name='GAN_AE')
# Custom loss function
if(self.alpha==None):
full_loss = partial(_full_loss,in_true=GANin,in_reco=GANmid2,eps=self.epsilon)
GAN.compile(loss=full_loss,optimizer=Kopt.Adam(lr=0.0002,beta_1=0.5))
else:
#full_loss = partial(_full_loss_DisCo,in_true=GANin,in_reco=GANmid2,eps=self.epsilon,alph=self.alpha,var_1=aux_in,power=self.power)
full_loss = partial(_full_loss,in_true=GANin,in_reco=GANmid2,eps=self.epsilon)
full_loss.__name__ = 'full_loss'
var_2 = tf.sqrt(tf.reduce_sum(tf.square(GANmid2 - GANin),axis=1))
full_loss2 = partial(DisCo,var_1=aux_in,var_2=var_2,power=self.power,alph=self.alpha)
full_loss2.__name__ = 'full_loss2'
GAN.compile(loss=[full_loss,full_loss2],optimizer=Kopt.Adam(lr=0.0002,beta_1=0.5))
#GAN.compile(loss=full_loss,optimizer=Kopt.Adam(lr=0.0002,beta_1=0.5))
# Display the sumary if required
if(display==True):
En.summary()
print('')
De.summary()
print('')
AE.summary()
print('')
D.summary()
print('')
GAN.summary()
return En,AE,D,GAN
# Train one GAN-AE model
def train(self,train_data,val_data,aux_data=None,ih=-1):
'''
Train a single GAN-AE model according to the hyperparameters defined for this instance.
Arguments :
train_data : The training dataset given as a numpy array.
val_data : The validation dataset given as a numpy array. This dataset is used to
Evaluate the FoM at each training cycle.
ih : Specify if the ethod is called from a separate thread. This argument is used only
when training multiple GAN-AE models simutaneously (it is then called from the
multi_train method).
If you call this method directly to train a single GAN-AE, please leave it by default.
Note :
All the given dataset must be properly scaled between 0 and 1.
This can be done using the scale_data method before the training.
'''
# Check aux_data
if(self.alpha!=None and aux_data==None):
print('ERROR : You must specifify a auxilary variable for the DisCo term.')
return
# Function to train the discriminator only
def D_train(append=True):
D.tranable = True
# Create a fake dataset using the AE
train_fake = G.predict(train_data)
train_full = np.concatenate((train_data,train_fake))
val_fake = G.predict(val_data)
val_full = np.concatenate((val_data,val_fake))
# Create labels for D
label_train_full = np.concatenate((np.ones(train_data.shape[0]),np.zeros(train_fake.shape[0])))
label_val_full = np.concatenate((np.ones(val_data.shape[0]),np.zeros(val_fake.shape[0])))
# Train D
D.fit(x=train_full,y=label_train_full,
batch_size=self.batch_size,epochs=self.ND,verbose=0,
validation_data=(val_full,label_val_full),shuffle=True
)
# Evaluate D and append results if required
if(append):
res_D = D.evaluate(x=train_full,y=label_train_full,batch_size=self.batch_size,verbose=0)
res_D2 = D.evaluate(x=val_full,y=label_val_full,batch_size=self.batch_size,verbose=0)
loss['D_train'].append(res_D)
loss['D_val'].append(res_D2)
return
# Function to train the full GAN-AE (with D frozen)
def GAN_train():
D.trainable = False
# Train GAN-AE
if(self.alpha==None):
GAN.fit(x=train_data,y=np.ones(train_data.shape[0]),
batch_size=self.batch_size,epochs=self.NGAN,verbose=0,
validation_data=(val_data,np.ones(val_data.shape[0])),shuffle=True
)
else:
GAN.fit(x=[train_data,aux_data[0]],y=[np.ones(train_data.shape[0]),train_data],
batch_size=self.batch_size,epochs=self.NGAN,verbose=0,
validation_data=([val_data,aux_data[1]],[np.ones(val_data.shape[0]),val_data]),shuffle=True
)
# Evaluate G and append loss
res_G = G.evaluate(x=train_data,y=train_data,batch_size=self.batch_size,verbose=0)
res_G2 = G.evaluate(x=val_data,y=val_data,batch_size=self.batch_size,verbose=0)
loss['G_train'].append(res_G)
loss['G_val'].append(res_G2)
# Evaluate full GAN-AE and append loss
if(self.alpha==None):
res_GAN = GAN.evaluate(x=train_data,y=np.ones(train_data.shape[0]),batch_size=self.batch_size,verbose=0)
res_GAN2 = GAN.evaluate(x=val_data,y=np.ones(val_data.shape[0]),batch_size=self.batch_size,verbose=0)
else:
res_GAN = GAN.evaluate(x=[train_data,aux_data[0]],
y=[np.ones(train_data.shape[0]),train_data],batch_size=self.batch_size,verbose=0)
res_GAN2 = GAN.evaluate(x=[val_data,aux_data[1]],
y=[np.ones(val_data.shape[0]),val_data],batch_size=self.batch_size,verbose=0)
loss['GAN_train'].append(res_GAN[0])
loss['GAN_val'].append(res_GAN2[0])
return
# Function to evaluate the model with the FoM
def FoM_eval():
# Evaluate the discriminator part
F = D.predict(val_data)
F = 1-F.mean()
# Add the AE part
F += loss['G_val'][-1]
FoM.append(F)
return
# Initialize a tensorflow session
sess = tf.Session()
# Build the model
if(ih==-1):
En,G,D,GAN = self.build()
else:
En,G,D,GAN = self.build(display=False)
# Check if should pretrain the AE part
if(self.pretrain_AE):
G.fit(x=train_data,y=train_data,
batch_size=self.batch_size,epochs=self.NGAN,verbose=0,
validation_data=(val_data,val_data),shuffle=True
)
# Check if should pretrain the D part
if(self.pretrain_dis):
D_train(append=False)
# Initilize loss containiner
loss = dict()
loss['G_train'] = []
loss['G_val'] = []
loss['D_train'] = []
loss['D_val'] = []
loss['GAN_train'] = []
loss['GAN_val'] = []
# Initilize FoM list
FoM = []
# Main cycle loop
stop = int(0)
best = 100.0
for cyc in range(self.Ncycle):
# D-only epochs
D_train()
# AE+D (with D forzen) epochs
GAN_train()
# Evaluation with FoM
FoM_eval()
# Check the early stop condition (if any)
if(self.early_stop!=None):
# Increment counter if no improvment
if(FoM[-1]>=best):
stop += 1
else:
best = FoM[-1]
stop = 0
# Check if we should stop
if(stop==self.early_stop):
#print(' stopping at cyc {} : {} ({})'.format(cyc,loss['GAN_train'][-1],FoM[-1]))
break
#print(' cyc {} : {} ({})'.format(cyc,loss['GAN_train'][-1],FoM[-1]))
# Convert all result containers in numpy array
loss['G_train'] = np.array(loss['G_train'])
loss['G_val'] = np.array(loss['G_val'])
loss['D_train'] = np.array(loss['D_train'])
loss['D_val'] = np.array(loss['D_val'])
loss['GAN_train'] = np.array(loss['GAN_train'])
loss['GAN_val'] = np.array(loss['GAN_val'])
FoM = np.array(FoM)
# Save results
if(ih==-1):
self.loss = loss
self.FoM = FoM
self.AE_weights = G.get_weights()
self.dis_weights = D.get_weights()
else:
self.loss[ih] = loss
self.FoM[ih] = FoM
self.AE_weights[ih] = G.get_weights()
self.dis_weights[ih] = D.get_weights()
# Clear the tensorflow session
sess.close()
del sess
return
# Train many GAN-AE models and select the best ones
def multi_train(self,train_data,val_data,aux_data=None):
'''
Train multiple GAN-AE models according to to the hyperparameters defined for
this instance.
The training of each individual models is parralelized and is done using the
train method.
Arguments :
train_data : The training dataset given as a numpy array.
val_data : The validation dataset given as a numpy array. This dataset is used to
Evaluate the FoM at each training cycle.
Note :
All the given dataset must be properly scaled between 0 and 1.
This can be done using the scale_data method before the training.
'''
# Initialize result containers
self.loss = np.empty(self.Nmodel,dtype=np.object)
self.FoM = np.empty(self.Nmodel,dtype=np.object)
self.AE_weights = np.empty(self.Nmodel,dtype=np.object)
self.dis_weights = np.empty(self.Nmodel,dtype=np.object)
# Thread pool executor
if(self.Nworker>1):
with thd.ThreadPoolExecutor(max_workers=self.Nworker) as exe:
# Start all threads
for th in range(self.Nmodel):
exe.submit(self.train,train_data,val_data,aux_data,th)
else:
for th in range(self.Nmodel):
self.train(train_data,val_data,aux_data,th)
# Retrieve the final FoM for each trained model
FFoM = np.array([self.FoM[i][-1] for i in range(self.Nmodel)])
# Sort the trained model by ascending FoM
S = FFoM.argsort()
self.FoM = self.FoM[S]
self.loss = self.loss[S]
self.AE_weights = self.AE_weights[S]
self.dis_weights = self.dis_weights[S]
return
# Apply a single GAN-AE model
# If label is given, they can be used to evaluate the model
def apply(self,data,dmin,dmax,var_name=None,label=None,filename=None,do_latent=True,do_reco=True,do_distance=True,do_roc=True,do_auc=True,ih=-1):
'''
Apply one GAN-AE model to a dataset in order to produce result plots.
Arguments :
data : The dataset given as a numpy array.
dmin : Numpy array specifying the true minimums of the input features.
This is used to plot the reconstructed data in real scale.
dmax : Numpy array specifying the true maximums of the input features.
This is used to plot the reconstructed data in real scale.
var_name : The x-axis labels to be used when ploting the original and/or reconstruted
features. If None, the names are built using the 'Var {i}' convention.
Defalut to None.
label : Numpy array with truth label for the data.
The labels are used to separate the background and signal to compute the ROC
curve.
If None, the background and signal are not separated and no ROC curve is
computed.
Default to None.
filename : The base name for the output files. This base is prepended to all the produced
files.
If None, the plots are shown but not saved. Default to None.
do_latent : Boolean specifying if the latent space should be ploted.
Default to True.
do_reco : Boolean specifying if the reconstructed variables should be ploted.
Default to True.
do_distance : Boolean specifying if the Euclidean distance distribution should be ploted.
The obtained distance distributions are recorded within this instance variables.
Default to True.
do_roc : boolean specifying if the ROC curve should be ploted. This requires to give truth
labels.
Default to True.
do_auc : Specify if the AUC should be recorded. Requires to give truth labels and to compute
the ROC curve.
Default to True.
ih : Specify if the ethod is called from a separate thread. This argument is used only
when applying multiple GAN-AE models simutaneously (it is then called from the
multi_apply method).
If you call this method directly to apply a single GAN-AE, please leave it by default.
Note :
All the given dataset must be properly scaled between 0 and 1.
This can be done using the scale_data method before the training.
'''
# Split the background and signal if labels given
if(type(label)!=type(None)):
bkg = data[label==0]
sig = data[label==1]
# Build the GAN-AE model and load the weights
En,AE,D,GAN = self.build(display=False)
if(ih==-1):
AE.set_weights(self.AE_weights)
D.set_weights(self.dis_weights)
else:
AE.set_weights(self.AE_weights[ih])
D.set_weights(self.dis_weights[ih])
# Latent space plot
if(do_latent):
# Apply the encoder model do the data
if(type(label)==type(None)):
cod_data = En.predict(data)
Nplot = cod_data.shape[1]
else:
cod_bkg = En.predict(bkg)
cod_sig = En.predict(sig)
Nplot = cod_bkg.shape[1]
# Do the plot
with lock:
F = plt.figure(figsize=(18,5*(Nplot//3+1)))
plt.suptitle('Latent space distribution')
for i in range(1,Nplot+1):
plt.subplot(3,Nplot//3+1,i)
if(type(label)==type(None)):
plt.hist(cod_data[:,i-1],bins=60,linewidth=2,histtype='step')
else:
plt.hist(cod_bkg[:,i-1],bins=60,linewidth=2,histtype='step',label='background')
plt.hist(cod_sig[:,i-1],bins=60,linewidth=2,histtype='step',label='signal')
plt.legend()
plt.xlabel('latent {}'.format(i),size='large')
if(filename==None):
plt.show()
else:
if(ih==-1):
plt.savefig('{}_latent_space.png'.format(filename),bbox_inches='tight')
else:
plt.savefig('{0:s}_{1:2d}_latent_space.png'.format(filename,ih),bbox_inches='tight')
plt.close(F)
# Reconstructed variables plot
if(do_reco):
# Apply the AE model do the data
if(type(label)==type(None)):
reco_data = AE.predict(data)
Nplot = reco_data.shape[1]
reco_data_restored = self.restore_data(reco_data,dmin,dmax)
data_restored = self.restore_data(data,dmin,dmax)
else:
reco_bkg = AE.predict(bkg)
reco_sig = AE.predict(sig)
Nplot = reco_bkg.shape[1]
reco_bkg_restored = self.restore_data(reco_bkg,dmin,dmax)
reco_sig_restored = self.restore_data(reco_sig,dmin,dmax)
bkg_restored = self.restore_data(bkg,dmin,dmax)
sig_restored = self.restore_data(sig,dmin,dmax)
# Check the original variable names
if(type(var_name)==type(None)):
var_name = np.array(['variable_{}'.format(i) for i in range(1,Nplot+1)])
# Do the plot
with lock:
F = plt.figure(figsize=(18,5*(Nplot//3+1)))
plt.suptitle('Reconstucted variable distribution')
for i in range(1,Nplot+1):
plt.subplot(Nplot//3+1,3,i)
if(type(label)==type(None)):
plt.hist(reco_data_restored[:,i-1],bins=60,linewidth=2,color='b',histtype='step')
plt.hist(data_restored[:,i-1],bins=60,linewidth=2,color='b',linestyle='--',histtype='step')
else:
plt.hist(reco_bkg_restored[:,i-1],bins=60,linewidth=2,color='b',histtype='step',label='background')
plt.hist(bkg_restored[:,i-1],bins=60,linewidth=2,color='b',linestyle='--',histtype='step')
plt.hist(reco_sig_restored[:,i-1],bins=60,linewidth=2,color='r',histtype='step',label='signal')
plt.hist(sig_restored[:,i-1],bins=60,linewidth=2,color='r',linestyle='--',histtype='step')
plt.legend()
plt.xlabel(var_name[i-1])
if(filename==None):
plt.show()
else:
if(ih==-1):
plt.savefig('{}_reco_variables.png'.format(filename),bbox_inches='tight')
else:
plt.savefig('{0:s}_{1:2d}_reco_variables.png'.format(filename,ih),bbox_inches='tight')
plt.close(F)
# Euclidean distance plot
if(do_distance):
# Check if we need to appy the AE model
if(do_reco==False):
if(type(label)==type(None)):
reco_data = AE.predict(data)
else:
reco_bkg = AE.predict(bkg)
reco_sig = AE.predict(sig)
# Compute the Euclidean distance distribution
if(type(label)==type(None)):
dist_data = np.sqrt(np.sum(np.square(data - reco_data),axis=1))
else:
dist_bkg = np.sqrt(np.sum(np.square(bkg - reco_bkg),axis=1))
dist_sig = np.sqrt(np.sum(np.square(sig - reco_sig),axis=1))
# Do the plot
with lock:
F = plt.figure(figsize=(12,8))
plt.title('Euclidean distance distribution')
if(type(label)==type(None)):
plt.hist(dist_data,bins=60,linewidth=2,histtype='step')
else:
plt.hist(dist_bkg,bins=60,linewidth=2,histtype='step',label='background')
plt.hist(dist_sig,bins=60,linewidth=2,histtype='step',label='signal')
plt.legend(fontsize='large')
plt.xlabel('Euclidean distance',size='large')
if(filename==None):
plt.show()
else:
if(ih==-1):
plt.savefig('{}_distance_distribution.png'.format(filename),bbox_inches='tight')
else:
plt.savefig('{0:s}_{1:2d}_distance_distribution.png'.format(filename,ih),bbox_inches='tight')
plt.close(F)
# Save the best models
if(ih==-1):
if(type(label)==type(None)):
self.distance = dist_data
else:
self.distance = [dist_bkg,dist_sig]
else:
if(type(label)==type(None)):
self.distance[:,ih] = dist_data
else:
self.distance[0][:,ih] = dist_bkg
self.distance[1][:,ih] = dist_sig
# ROC curve plot
if(do_roc):
# Check if the distance has been computed
if(do_distance==False):
print("CAN'T COMPUTE THE ROC CURVE !!")
print("Please set 'do_distance=True'")
return
# Check if there are labels
if(type(label)==type(None)):
print("CAN'T COMPUTE THE ROC CURVE !!")
print('Please give truth labels.')
return
# Now we can compute the roc curve
Nbin = 100 # We use 100 points to make the ROC curve
roc_min = min([dist_bkg.min(),dist_sig.min()])
roc_max = max([dist_bkg.max(),dist_sig.max()])
step = (roc_max-roc_min)/Nbin
steps = np.arange(roc_min+step,roc_max+step,step)
roc_x = np.array([dist_sig[dist_sig>th].size/dist_sig.size for th in steps])
roc_y = np.array([dist_bkg[dist_bkg<th].size/dist_bkg.size for th in steps])
roc_r1 = np.linspace(0,1,100)
roc_r2 = 1-roc_r1
# Compute AUC
auc_sig = auc(roc_x,roc_y)
# Do the plot
with lock:
F = plt.figure(figsize=(12,8))
plt.plot(roc_x,roc_y,'-',label='auc={0:.4f}'.format(auc_sig))
plt.plot(roc_r1,roc_r2,'--',label='random class')
plt.legend(fontsize='large')
plt.xlabel('signal efficiency',size='large')
plt.ylabel('background rejection',size='large')
if(filename==None):
plt.show()
else:
if(ih==-1):
plt.savefig('{}_ROC_curve.png'.format(filename),bbox_inches='tight')
else:
plt.savefig('{0:s}_{1:2d}_ROC_curve.png'.format(filename,ih),bbox_inches='tight')
plt.close(F)
# AUC distribution
if(do_auc):
# Check if ROC curve has been calculated
if(do_roc==False):
print("CAN'T COMPUTE AUC !!!")
print("Please set 'do_roc=True'")
return
# Store the computed AUC
if(ih==-1):
self.auc = auc_sig
else:
self.auc[ih] = auc_sig
return
# Apply a many GAN-AE model and average them all
# If label is given, use them to evaluate the models
def multi_apply(self,data,dmin,dmax,var_name=None,label=None,filename=None,do_latent=True,do_reco=True,do_distance=True,do_roc=True,do_auc=True):
'''
Arguments :
data : The dataset given as a numpy array.
dmin : Numpy array specifying the true minimums of the input features.
This is used to plot the reconstructed data in real scale.
dmax : Numpy array specifying the true maximums of the input features.
This is used to plot the reconstructed data in real scale.
var_name : The x-axis labels to be used when ploting the original and/or reconstruted
features. If None, the names are built using the 'Var {i}' convention.
Defalut to None.
label : Numpy array with truth label for the data.
The labels are used to separate the background and signal to compute the ROC
curve.
If None, the background and signal are not separated and no ROC curve is
computed.
Default to None.
filename : The base name for the output files. This base is prepended to all the produced
files. For all the individual models, a unique id is appended to the base name.
If None, the plots are shown but not saved. Default to None.
do_latent : Boolean specifying if the latent space should be ploted.
Default to True.
do_reco : Boolean specifying if the reconstructed variables should be ploted.
Default to True.
do_distance : Boolean specifying if the Euclidean distance distribution should be ploted.
The obtained distance distributions are recorded within this instance variables.
In addition, the averaged distance distribution is also ploted.
Default to True.
do_roc : boolean specifying if the ROC curve should be ploted. This requires to give truth
labels.
In addition, the ROC curve obtained from the averaged distance distribution is also ploted.
Default to True.
do_auc : Specify if the AUC should be recorded. Requires to give truth labels and to compute
the ROC curve.
In addition, a sccater plot showing the AUC versus FoM is also produced.
Default to True.
Note :
All the given dataset must be properly scaled between 0 and 1.
This can be done using the scale_data method before the training.
'''
# Initialize result containers
if(type(label)==type(None)):
if(do_distance):
self.distance = np.empty((data.shape[0],self.Nselec+1))
else:
if(do_distance):
self.distance = [np.empty((label[label==0].size,self.Nselec+1)),np.empty((label[label==1].size,self.Nselec+1))]
if(do_auc):
self.auc = np.empty(self.Nselec+1)
# Thread pool executor
if(self.Nworker>1):
with thd.ThreadPoolExecutor(max_workers=self.Nworker) as exe:
for th in range(self.Nselec):
exe.submit(self.apply,data,dmin,dmax,var_name,label,filename,do_latent,do_reco,do_distance,do_roc,do_auc,th)
else:
for th in range(self.Nselec):
self.apply(data,dmin,dmax,var_name,label,filename,do_latent,do_reco,do_distance,do_roc,do_auc,th)
# Check if we should compute the distance
if(do_distance):
# Compute averaged distance
if(type(label)==type(None)):
dist_data_av = self.distance[:,:self.Nselec].mean(axis=1)
self.distance[:,-1] = dist_data_av
else:
dist_bkg_av = self.distance[0][:,:self.Nselec].mean(axis=1)
dist_sig_av = self.distance[1][:,:self.Nselec].mean(axis=1)
self.distance[0][:,-1] = dist_bkg_av
self.distance[1][:,-1] = dist_sig_av
# Do the plot
F = plt.figure(figsize=(12,8))
plt.title('Averaged Eclidean distance distribution')
if(type(label)==type(None)):
plt.hist(dist_data_av,bins=60,linewidth=2,histtype='step')
else:
plt.hist(dist_bkg_av,bins=60,linewidth=2,histtype='step',label='background')
plt.hist(dist_sig_av,bins=60,linewidth=2,histtype='step',label='signal')
plt.legend(fontsize='large')
plt.xlabel('Averaged Euclidean distance',size='large')
if(filename==None):
plt.show()
else:
plt.savefig('{}_distance_average.png'.format(filename),bbox_inches='tight')
plt.close(F)
if(do_roc):
# Check if the distance has been computed
if(do_distance==False):
print("CAN'T COMPUTE THE ROC CURVE !!")
print("Please set 'do_distance=True'")
return
# Check if there are labels
if(type(label)==type(None)):
print("CAN'T COMPUTE THE ROC CURVE !!")
print('Please give truth labels.')
return
# Now we can compute the roc curve
Nbin = 100 # We use 100 points to make the ROC curve
roc_min = min([dist_bkg_av.min(),dist_sig_av.min()])
roc_max = max([dist_bkg_av.max(),dist_sig_av.max()])
step = (roc_max-roc_min)/Nbin
steps = np.arange(roc_min+step,roc_max+step,step)
roc_x = np.array([dist_sig_av[dist_sig_av>th].size/dist_sig_av.size for th in steps])
roc_y = np.array([dist_bkg_av[dist_bkg_av<th].size/dist_bkg_av.size for th in steps])
roc_r1 = np.linspace(0,1,100)
roc_r2 = 1-roc_r1
# Compute AUC
auc_sig = auc(roc_x,roc_y)
# Do the plot
F = plt.figure(figsize=(12,8))
plt.plot(roc_x,roc_y,'-',label='auc={0:.4f}'.format(auc_sig))
plt.plot(roc_r1,roc_r2,'--',label='random class')
plt.legend(fontsize='large')
plt.xlabel('signal efficiency',size='large')
plt.ylabel('background rejection',size='large')
if(filename==None):
plt.show()
else:
plt.savefig('{0:s}_ROC_curve.png'.format(filename),bbox_inches='tight')
plt.close(F)
# Check if we should do the AUC plot
if(do_auc and type(label)!=type(None)):
# Get the best FoM array.
FFoM = np.array([self.FoM[i][-1] for i in range(self.Nselec)])
# Save the global AUC value
self.auc[-1] = auc_sig
#print(' FFoM={}'.format(FFoM))
#print(' auc={}'.format(self.auc))
# Do a scatter plot FoM vs AUC
F = plt.figure(figsize=(8,8))
plt.title('FoM versus AUC')
plt.scatter(FFoM,self.auc[:-1])
plt.xlabel('FoM',size='large')
plt.ylabel('AUC',size='large')
if(filename==None):
plt.show()
else:
plt.savefig('{}_scatter_AUC.png'.format(filename),bbox_inches='tight')
plt.close(F)
return
# Save the trained models
def save(self,filename):
'''
Save all the parameters and all the trained weights of this GAN-AE instance.
The parameters are stored in a text file and the wights of each AE and D
models are stored in HDF format.
Arguments :
filename : The base name of the save files. The full name of the files is
constructed from this base.
'''
# Save the parameters, loss and FoM
Fparam = open('{}_param.txt'.format(filename),'w')
param = dict()
param['input_dim'] = self.input_dim
param['hidden_dim'] = self.hidden_dim
param['latent_dim'] = self.latent_dim
param['dis_dim'] = self.dis_dim
param['epsilon'] = self.epsilon
param['alpha'] = self.alpha
param['power'] = self.power
param['NGAN'] = self.NGAN
param['ND'] = self.ND
param['batch_size'] = self.batch_size
param['Ncycle'] = self.Ncycle
param['early_stop'] = self.early_stop