Skip to content

Commit c09f0d1

Browse files
committed
Remove torch support, move to sklearn for MLP
1 parent 217cb48 commit c09f0d1

6 files changed

Lines changed: 407 additions & 530 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class Approximation{
103103
See the [**Examples**](#examples) section below and the [**Tutorials**](tutorials/README.md) to have an idea of the potential of this package.
104104

105105
## Dependencies and installation
106-
**EZyRB** requires `numpy`, `scipy`, `sklearn`, `matplotlib`, `torch`,
106+
**EZyRB** requires `numpy`, `scipy`, `sklearn`, `matplotlib`,
107107
`pytest` (for local test) and `sphinx` (to generate the documentation). The code
108108
has been tested with Python3.5 version, but it should be compatible with
109109
Python3. It can be installed using `pip` or directly from the source code.

ezyrb/approximation/ann.py

Lines changed: 108 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -3,228 +3,149 @@
33
"""
44

55
import logging
6-
import torch
7-
import torch.nn as nn
86
import numpy as np
7+
from sklearn.neural_network import MLPRegressor
98
from .approximation import Approximation
109

1110
logger = logging.getLogger(__name__)
1211

1312

1413
class 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

Comments
 (0)