-
Notifications
You must be signed in to change notification settings - Fork 93
/
regression_models.py
248 lines (195 loc) · 7.76 KB
/
regression_models.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
"""Simple regression models for readout matrix learning.
This module provides simple linear models that can be used
to compute the readout matrix coefficients with simple
linear regression algorithms, like ridge regularized regression
or any linear model from scikit-learn API.
These models are already packed in the :py:class:`compat.ESN`
class, and can instantiated by passing them as arguments to the `ESN`
object.
In most cases, you won't need to call this module directly. Simply
pass the models to the `ESN` object as parameters.
See the :py:class:`compat.ESN` documentation for more information.
"""
from abc import ABCMeta
import numpy as np
from joblib import Parallel, delayed
from scipy import linalg
from ..type import Data, Weights
from ..utils.parallel import clean_tempfile, get_joblib_backend
from ..utils.validation import add_bias, check_vector
from .utils.parallel import as_memmap
def _solve_ridge(XXT, YXT, ridge):
return linalg.solve(XXT + ridge, YXT.T, assume_a="sym").T
def _prepare_inputs(X, Y, bias=True, allow_reshape=False):
if bias:
X = add_bias(X)
if not isinstance(X, np.ndarray):
X = np.vstack(X)
if not isinstance(Y, np.ndarray):
Y = np.vstack(Y)
X = check_vector(X, allow_reshape=allow_reshape)
Y = check_vector(Y, allow_reshape=allow_reshape)
return X, Y
def _check_tikhnonv_terms(XXT, YXT, x, y):
if x.shape[0] != y.shape[0]:
raise ValueError(
f"Impossible to perform _ridge regression: dimension mismatch "
f"between target sequence of shape {y.shape} and state sequence "
f"of shape {x.shape} ({x.shape[0]} != {y.shape[0]})."
)
if y.shape[1] != YXT.shape[0]:
raise ValueError(
f"Impossible to perform _ridge regression: dimension mismatch "
f"between target sequence of shape {y.shape} and expected output "
f"dimension ({YXT.shape[0]}) ({y.shape[1]} != {YXT.shape[0]})"
)
class _Model(metaclass=ABCMeta):
"""
Base template for model learning classes.
"""
Wout: Weights
_dim_in: int
_dim_out: int
_initialized = False
@property
def initialized(self):
"""A boolean indicating wether the internal parameters of the
model are initialized or not."""
return self._initialized
@property
def dim_in(self):
"""Input dimension of the model (i.e. internal
states dimension)."""
return self._dim_in
@property
def dim_out(self):
"""Output dimension of the model."""
return self._dim_out
def fit(self, X: Data = None, Y: Data = None) -> Weights:
"""Fit states X to targets values Y following the model
learning rule.
Parameters
----------
X : numpy.ndarray or list of numpy.ndarray
Internal states of the reservoir.
Y : numpy.ndarray or list of numpy.ndarray
Targets values for each states.
Returns
-------
numpy.ndarray
A readout matrix of shape (targets dimension,
states dimension + bias (=1)).
"""
raise NotImplementedError
class _OfflineModel(_Model, metaclass=ABCMeta):
def partial_fit(self, X: Data, Y: Data):
"""Partially fit the states X to the targets values
Y. This method can be used to pre-compute some
steps of the final fitting method.
Parameters
----------
X : numpy.ndarray or list of numpy.ndarray
Internal states of the reservoir.
Y : numpy.ndarray or list of numpy.ndarray
Targets values for each states.
"""
raise NotImplementedError
class RidgeRegression(_OfflineModel):
"""Ridge regression model for reservoir output weights
learning.
.. math::
W_{out} = YX^{T} \\cdot (XX^{T} + \\mathrm{_ridge} \\times \\mathrm{Id}_{_dim_in})
where :math:`W_out` is the readout matrix learnt through this regression,
:math:`X` are the internal states, :math:`Y` are the targets vectors,
and :math:`_dim_in` is the internal state dimension (number of units in the
reservoir).
By default, ridge coefficient is set to :math:`0`, which is equivalent to a simple
analytic resolution using pseudo-inverse.
Partial fit method allows to concurrently pre-compute :math:`XX^{T]`
and :math:`YX^{T}` when several independent state sequences are
provided, for performance, as suggested by [1]_.
.. [1] Lukosevicius, M. (2012). A Practical Guide to Applying Echo State
Networks. Neural Networks: Tricks of the Trade.
"""
def __init__(self, ridge=0.0, workers=-1, dtype=np.float64):
self.workers = workers
self._dtype = dtype
self._ridge = ridge
self._ridgeid = None
self._XXT = None
self._YXT = None
@property
def ridge(self):
"""Regularization coefficient of the model."""
return self._ridge
@ridge.setter
def ridge(self, value):
self._ridge = value
if self._initialized:
self._reset_ridge_matrix()
def _reset_ridge_matrix(self):
self._ridgeid = self._ridge * np.eye(self._dim_in + 1, dtype=self._dtype)
def initialize(self, dim_in: int = None, dim_out: int = None):
"""
Initialize the model internal parameters.
Parameters
----------
dim_in : int
States dimension.
dim_out : int
Targets dimension.
"""
if dim_in is not None:
self._dim_in = dim_in
if dim_out is not None:
self._dim_out = dim_out
if getattr(self, "Wout", None) is None:
self.Wout = np.zeros((self._dim_in + 1, self._dim_out), dtype=self._dtype)
if getattr(self, "_XXT", None) is None:
self._XXT = as_memmap(
np.zeros((self._dim_in + 1, self._dim_in + 1), dtype=self._dtype),
caller=self,
)
if getattr(self, "_YXT", None) is None:
self._YXT = as_memmap(
np.zeros((self._dim_out, self._dim_in + 1), dtype=self._dtype),
caller=self,
)
if getattr(self, "_ridgeid", None) is None:
self._reset_ridge_matrix()
self._initialized = True
def clean(self):
"""Clean all internal parameters of the model."""
del self._XXT
del self._YXT
if self._initialized:
self.initialize()
def partial_fit(self, X, Y):
X, Y = _prepare_inputs(X, Y, allow_reshape=True)
if not self._initialized:
raise RuntimeError(
f"RidgeRegression model was never initialized. Call "
f"'initialize() first."
)
_check_tikhnonv_terms(self._XXT, self._YXT, X, Y)
xxt = X.T.dot(X)
yxt = Y.T.dot(X)
# Lock the memory map to avoid increment from
# different processes at the same time (Numpy doesn't like that).
self._XXT += xxt
self._YXT += yxt
def fit(self, X=None, Y=None):
if X is not None and Y is not None:
if isinstance(X, np.ndarray) and isinstance(Y, np.ndarray):
self.partial_fit(X, Y)
elif isinstance(X, list) and isinstance(Y, list):
# if all states and all teachers are given at once,
# perform partial fit anyway to avoid memory overload.
workers = min(self.workers, len(X))
backend = get_joblib_backend(workers)
with Parallel(n_jobs=workers, backend=backend) as parallel:
parallel(delayed(self.partial_fit)(x, y) for x, y in zip(X, Y))
self.Wout = _solve_ridge(self._XXT, self._YXT, self._ridgeid)
clean_tempfile(self)
self.clean()
return self.Wout