33"""
44
55import logging
6- import torch
7- import torch .nn as nn
86import numpy as np
7+ from sklearn .neural_network import MLPRegressor
98from .approximation import Approximation
109
1110logger = logging .getLogger (__name__ )
1211
1312
1413class ANN (Approximation ):
1514 """
16- Feed-Forward Artifical Neural Network (ANN).
15+ Feed-Forward Artifical Neural Network (ANN) using sklearn's MLPRegressor .
1716
1817 :param list layers: ordered list with the number of neurons of each hidden
1918 layer.
20- :param torch.nn.modules.activation function: activation function at each
21- layer. A single activation function can be passed or a list of them of
22- length equal to the number of hidden layers.
23- :param list stop_training: list with the maximum number of training
24- iterations (int) and/or the desired tolerance on the training loss
25- (float).
26- :param torch.nn.Module loss: loss definition (Mean Squared if not given).
27- :param torch.optim optimizer: the torch class implementing optimizer.
28- Default value is `Adam` optimizer.
29- :param float lr: the learning rate. Default is 0.001.
30- :param float l2_regularization: the L2 regularization coefficient, it
31- corresponds to the "weight_decay". Default is 0 (no regularization).
32- :param int frequency_print: the frequency in terms of epochs of the print
33- during the training of the network.
34- :param boolean last_identity: Flag to specify if the last activation
35- function is the identity function. In the case the user provides the
36- entire list of activation functions, this attribute is ignored. Default
37- value is True.
19+ :param str activation: activation function for the hidden layers.
20+ Options: 'identity', 'logistic', 'tanh', 'relu' (default).
21+ :param str solver: the solver for weight optimization. Options: 'lbfgs',
22+ 'sgd', 'adam' (default).
23+ :param int max_iter: maximum number of iterations. Default is 200.
24+ :param float tol: tolerance for the optimization. Default is 1e-4.
25+ :param float learning_rate_init: initial learning rate (only for 'sgd'
26+ or 'adam'). Default is 0.001.
27+ :param float alpha: L2 penalty (regularization term) parameter.
28+ Default is 0.0001.
29+ :param int frequency_print: the frequency in terms of epochs to print
30+ training progress. Default is 10.
31+ :param int random_state: random state for reproducibility. Default is None.
32+ :param bool early_stopping: whether to use early stopping to terminate
33+ training when validation score is not improving. Default is False.
34+ :param float validation_fraction: proportion of training data to set aside
35+ as validation set for early stopping. Default is 0.1.
3836
3937 :Example:
4038 >>> import ezyrb
4139 >>> import numpy as np
42- >>> import torch.nn as nn
43- >>> x = np.random.uniform(-1, 1, size =(4, 2))
40+ >>> x = np.random.uniform(-1, 1, size=(4, 2))
4441 >>> y = np.array([np.sin(x[:, 0]), np.cos(x[:, 1]**3)]).T
45- >>> ann = ezyrb.ANN([10, 5], nn.Tanh(), [ 20000,1e-5] )
42+ >>> ann = ezyrb.ANN([10, 5], activation='tanh', max_iter= 20000)
4643 >>> ann.fit(x, y)
4744 >>> y_pred = ann.predict(x)
4845 >>> print(y)
4946 >>> print(y_pred)
5047 >>> print(len(ann.loss_trend))
5148 >>> print(ann.loss_trend[-1])
5249 """
53- def __init__ (self , layers , function , stop_training , loss = None ,
54- optimizer = torch .optim .Adam , lr = 0.001 , l2_regularization = 0 ,
55- frequency_print = 10 , last_identity = True ):
56- """
57- Initialize an Artificial Neural Network.
58-
59- :param list layers: Ordered list with the number of neurons of each hidden layer.
60- :param function: Activation function(s) for each layer.
61- :param stop_training: Stopping criteria for training (iterations and/or tolerance).
62- :param loss: Loss function to use. Default is MSELoss.
63- :param optimizer: Optimizer class to use. Default is Adam.
64- :param float lr: Learning rate. Default is 0.001.
65- :param float l2_regularization: L2 regularization coefficient. Default is 0.
66- :param int frequency_print: Frequency of printing during training. Default is 10.
67- :param bool last_identity: Whether the last activation is identity. Default is True.
68- """
69- logger .debug ("Initializing ANN with layers=%s, lr=%f, "
70- "l2_reg=%f" , layers , lr , l2_regularization )
71- if loss is None :
72- loss = torch .nn .MSELoss ()
73- logger .debug ("Using default MSELoss" )
74-
75- if not isinstance (function , list ): # Single activation function
76- nl = len (layers ) if last_identity else len (layers )+ 1
77- function = [function ] * nl
78- logger .debug ("Replicated activation function %d times" , nl )
79-
80- if not isinstance (stop_training , list ):
81- stop_training = [stop_training ]
82-
83- if torch .cuda .is_available (): # Check if GPU is available
84- logger .info ("Using cuda device" )
85- print ("Using cuda device" )
86- torch .cuda .empty_cache ()
87- self .use_cuda = True
88- else :
89- logger .info ("Using CPU device" )
90- self .use_cuda = False
50+ def __init__ (
51+ self ,
52+ layers ,
53+ activation = "tanh" ,
54+ max_iter = 200 ,
55+ solver = "adam" ,
56+ learning_rate_init = 0.001 ,
57+ alpha = 0.0001 ,
58+ frequency_print = 10 ,
59+ ** kwargs ,
60+ ):
61+ logger .debug (
62+ "Initializing ANN with layers=%s, activation=%s, "
63+ "solver=%s, max_iter=%d, lr=%f, alpha=%f" ,
64+ layers ,
65+ activation ,
66+ solver ,
67+ max_iter ,
68+ learning_rate_init ,
69+ alpha ,
70+ )
9171
9272 self .layers = layers
93- self .function = function
94- self .loss = loss
95- self .stop_training = stop_training
96-
97- self .loss_trend = []
98- self .model = None
99- self .optimizer = optimizer
100-
73+ self .activation = activation
74+ self .solver = solver
75+ self .max_iter = max_iter
76+ self .learning_rate_init = learning_rate_init
77+ self .alpha = alpha
10178 self .frequency_print = frequency_print
102- self .lr = lr
103- self .l2_regularization = l2_regularization
104-
105- def _convert_numpy_to_torch (self , array ):
106- """
107- Converting data type.
108-
109- :param numpy.ndarray array: input array.
110- :return: the tensorial counter-part of the input array.
111- :rtype: torch.Tensor.
112- """
113- return torch .from_numpy (array ).float ()
114-
115- def _convert_torch_to_numpy (self , tensor ):
116- """
117- Converting data type.
118-
119- :param torch.Tensor tensor: input tensor.
120- :return: the vectorial counter-part of the input tensor.
121- :rtype: numpy.ndarray.
122- """
123- return tensor .detach ().numpy ()
124-
125- @staticmethod
126- def _list_to_sequential (layers , functions ):
127-
128- layers_torch = []
129- inout_layers = [[layers [i ], layers [i + 1 ]] for i in range (len (layers )- 1 )]
130-
131- while True :
132- if inout_layers :
133- inp_d , out_d = inout_layers .pop (0 )
134- layers_torch .append (nn .Linear (inp_d , out_d ))
135-
136- if functions :
137- layers_torch .append (functions .pop (0 ))
79+ self .extra_kwargs = kwargs
13880
139- if not functions and not inout_layers :
140- break
141-
142- return nn .Sequential (* layers_torch )
143-
144- def _build_model (self , points , values ):
145- """
146- Build the torch neural network model.
147-
148- Constructs a feed-forward neural network with the specified layers
149- and activation functions.
150-
151- :param numpy.ndarray points: The coordinates of the training points.
152- :param numpy.ndarray values: The training values at the points.
153- """
154- layers = self .layers .copy ()
155- layers .insert (0 , points .shape [1 ])
156- layers .append (values .shape [1 ])
81+ self .model = None
82+ self .loss_trend = []
15783
158- if self .model is None :
159- self .model = self ._list_to_sequential (layers , self .function )
160- else :
161- self .model = self .model
84+ logger .info ("ANN initialized with sklearn MLPRegressor" )
16285
16386 def fit (self , points , values ):
16487 """
16588 Build the ANN given 'points' and 'values' and perform training.
16689
167- Training procedure information:
168- - optimizer: Adam's method with default parameters (see, e.g.,
169- https://pytorch.org/docs/stable/optim.html);
170- - loss: self.loss (if none, the Mean Squared Loss is set by
171- default).
172- - stopping criterion: the fulfillment of the requested tolerance
173- on the training loss compatibly with the prescribed budget of
174- training iterations (if type(self.stop_training) is list); if
175- type(self.stop_training) is int or type(self.stop_training) is
176- float, only the number of maximum iterations or the accuracy
177- level on the training loss is considered as the stopping rule,
178- respectively.
179-
18090 :param numpy.ndarray points: the coordinates of the given (training)
18191 points.
18292 :param numpy.ndarray values: the (training) values in the points.
18393 """
94+ logger .debug (
95+ "Fitting ANN with points shape: %s, values shape: %s" ,
96+ points .shape ,
97+ values .shape ,
98+ )
99+
100+ # Create the MLPRegressor model
101+ self .model = MLPRegressor (
102+ hidden_layer_sizes = tuple (self .layers ),
103+ activation = self .activation ,
104+ solver = self .solver ,
105+ alpha = self .alpha ,
106+ learning_rate_init = self .learning_rate_init ,
107+ max_iter = self .max_iter ,
108+ verbose = False ,
109+ ** self .extra_kwargs ,
110+ )
111+
112+ # Custom training loop to track loss and print progress
113+ self .loss_trend = []
184114
185- self ._build_model (points , values )
186-
187- if self .use_cuda :
188- self .model = self .model .cuda ()
189- points = self ._convert_numpy_to_torch (points ).cuda ()
190- values = self ._convert_numpy_to_torch (values ).cuda ()
115+ # For sklearn, we need to do partial fitting to track loss
116+ # We'll use the standard fit but access loss_curve_ afterwards
117+ logger .info ("Starting ANN training" )
118+
119+ if self .frequency_print > 0 :
120+ # Monkey patch to capture loss during training
121+ original_fit = self .model .fit
122+
123+ def fit_with_logging (X , y ):
124+ result = original_fit (X , y )
125+ if hasattr (self .model , "loss_curve_" ):
126+ self .loss_trend = list (self .model .loss_curve_ )
127+ for i , loss in enumerate (self .loss_trend ):
128+ if (
129+ i == 0
130+ or i == len (self .loss_trend ) - 1
131+ or (i + 1 ) % self .frequency_print == 0
132+ ):
133+ print (f"[epoch { i + 1 :6d} ]\t { loss :e} " )
134+ return result
135+
136+ fit_with_logging (points , values )
191137 else :
192- points = self ._convert_numpy_to_torch (points )
193- values = self ._convert_numpy_to_torch (values )
194-
195- optimizer = self .optimizer (
196- self .model .parameters (),
197- lr = self .lr , weight_decay = self .l2_regularization )
198-
199- n_epoch = 1
200- flag = True
201- while flag :
202- y_pred = self .model (points )
203-
204- loss = self .loss (y_pred , values )
205-
206- optimizer .zero_grad ()
207- loss .backward ()
208- optimizer .step ()
138+ self .model .fit (points , values )
139+ if hasattr (self .model , "loss_curve_" ):
140+ self .loss_trend = list (self .model .loss_curve_ )
209141
210- scalar_loss = loss .item ()
211- self .loss_trend .append (scalar_loss )
142+ logger .info (
143+ "ANN training completed after %d iterations" , self .model .n_iter_
144+ )
145+ if self .loss_trend :
146+ logger .debug ("Final loss: %f" , self .loss_trend [- 1 ])
212147
213- for criteria in self .stop_training :
214- if isinstance (criteria , int ): # stop criteria is an integer
215- if n_epoch == criteria :
216- flag = False
217- elif isinstance (criteria , float ): # stop criteria is float
218- if scalar_loss < criteria :
219- flag = False
220-
221- if (flag is False or
222- n_epoch == 1 or n_epoch % self .frequency_print == 0 ):
223- print (f'[epoch { n_epoch :6d} ]\t { scalar_loss :e} ' )
224-
225- n_epoch += 1
226-
227- return optimizer
148+ return self
228149
229150 def predict (self , new_point ):
230151 """
@@ -234,12 +155,10 @@ def predict(self, new_point):
234155 :return: the predicted values via the ANN.
235156 :rtype: numpy.ndarray
236157 """
237- if self .use_cuda :
238- new_point = self ._convert_numpy_to_torch (new_point ).cuda ()
239- new_point = self ._convert_numpy_to_torch (
240- np .array (new_point .cpu ())).cuda ()
241- y_new = self ._convert_torch_to_numpy (self .model (new_point ).cpu ())
242- else :
243- new_point = self ._convert_numpy_to_torch (np .array (new_point ))
244- y_new = self ._convert_torch_to_numpy (self .model (new_point ))
158+ logger .debug (
159+ "Predicting with ANN for %d points" ,
160+ np .atleast_2d (new_point ).shape [0 ],
161+ )
162+ new_point = np .atleast_2d (new_point )
163+ y_new = self .model .predict (new_point )
245164 return y_new
0 commit comments