Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Optimizer 

3""" 

4import numpy as np 

5from sklearn.model_selection import train_test_split 

6from typing import Any, Dict, List, Tuple, Union 

7from .base_optimizer import BaseOptimizer 

8from .fit_methods import fit 

9from .tools import ScatterData 

10 

11 

12class Optimizer(BaseOptimizer): 

13 """ 

14 This optimizer finds a solution to the linear 

15 :math:`\\boldsymbol{A}\\boldsymbol{x}=\\boldsymbol{y}` problem. 

16 

17 One has to specify either `train_size`/`test_size` or 

18 `train_set`/`test_set` If either `train_set` or `test_set` (or both) 

19 is specified the fractions will be ignored. 

20 

21 Warning 

22 ------- 

23 Repeatedly setting up a Optimizer and training 

24 *without* changing the seed for the random number generator will yield 

25 identical or correlated results, to avoid this please specify a different 

26 seed when setting up multiple Optimizer instances. 

27 

28 Parameters 

29 ---------- 

30 fit_data : tuple(numpy.ndarray, numpy.ndarray) 

31 the first element of the tuple represents the fit matrix `A` 

32 (`N, M` array) while the second element represents the vector 

33 of target values `y` (`N` array); here `N` (=rows of `A`, 

34 elements of `y`) equals the number of target values and `M` 

35 (=columns of `A`) equals the number of parameters 

36 fit_method : str 

37 method to be used for training; possible choice are 

38 "least-squares", "lasso", "elasticnet", "bayesian-ridge", "ardr", 

39 "rfe", "split-bregman" 

40 standardize : bool 

41 if True the fit matrix and target values are standardized before fitting, 

42 meaning columns in the fit matrix and th target values are rescaled to 

43 have a standard deviation of 1.0. 

44 train_size : float or int 

45 If float represents the fraction of `fit_data` (rows) to be used for 

46 training. If int, represents the absolute number of rows to be used for 

47 training. 

48 test_size : float or int 

49 If float represents the fraction of `fit_data` (rows) to be used for 

50 testing. If int, represents the absolute number of rows to be used for 

51 testing. 

52 train_set : tuple or list(int) 

53 indices of rows of `A`/`y` to be used for training 

54 test_set : tuple or list(int) 

55 indices of rows of `A`/`y` to be used for testing 

56 check_condition : bool 

57 if True the condition number will be checked 

58 (this can be sligthly more time consuming for larger 

59 matrices) 

60 seed : int 

61 seed for pseudo random number generator 

62 

63 Attributes 

64 ---------- 

65 train_scatter_data : ScatterData 

66 target and predicted value for each row in the training set 

67 test_scatter_data : ScatterData 

68 target and predicted value for each row in the test set 

69 """ 

70 

71 def __init__(self, 

72 fit_data: Tuple[np.ndarray, np.ndarray], 

73 fit_method: str = 'least-squares', 

74 standardize: bool = True, 

75 train_size: Union[int, float] = 0.75, 

76 test_size: Union[int, float] = None, 

77 train_set: Union[Tuple[int], List[int]] = None, 

78 test_set: Union[Tuple[int], List[int]] = None, 

79 check_condition: bool = True, 

80 seed: int = 42, 

81 **kwargs) -> None: 

82 

83 super().__init__(fit_data, fit_method, standardize, check_condition, 

84 seed) 

85 

86 self._kwargs = kwargs 

87 

88 # setup train and test sets 

89 self._setup_rows(train_size, test_size, train_set, test_set) 

90 

91 # will be populate once running train 

92 self._rmse_train = None 

93 self._rmse_test = None 

94 self._contributions_train = None 

95 self._contributions_test = None 

96 self.train_scatter_data = None 

97 self.test_scatter_data = None 

98 

99 def train(self) -> None: 

100 """ Carries out training. """ 

101 

102 # select training data 

103 A_train = self._A[self.train_set, :] 

104 y_train = self._y[self.train_set] 

105 

106 # perform training 

107 self._fit_results = fit(A_train, y_train, self.fit_method, 

108 self.standardize, self._check_condition, 

109 **self._kwargs) 

110 self._rmse_train = self.compute_rmse(A_train, y_train) 

111 self.train_scatter_data = ScatterData(y_train, self.predict(A_train)) 

112 

113 # perform testing 

114 if self.test_set is not None: 

115 A_test = self._A[self.test_set, :] 

116 y_test = self._y[self.test_set] 

117 self._rmse_test = self.compute_rmse(A_test, y_test) 

118 self.test_scatter_data = ScatterData(y_test, self.predict(A_test)) 

119 else: 

120 self._rmse_test = None 

121 self.test_scatter_data = None 

122 

123 def _setup_rows(self, 

124 train_size: Union[int, float], 

125 test_size: Union[int, float], 

126 train_set: Union[Tuple[int], List[int]], 

127 test_set: Union[Tuple[int], List[int]]) -> None: 

128 """ 

129 Sets up train and test rows depending on which arguments are 

130 specified. 

131 

132 If `train_set` and `test_set` are `None` then `train_size` and 

133 `test_size` are used. 

134 """ 

135 

136 if train_set is None and test_set is None: 

137 train_set, test_set = self._get_rows_via_sizes( 

138 train_size, test_size) 

139 else: 

140 train_set, test_set = self._get_rows_from_indices( 

141 train_set, test_set) 

142 

143 if len(train_set) == 0: 

144 raise ValueError('No training rows selected from fit_data') 

145 

146 if test_set is not None: # then check overlap between train and test 

147 if len(np.intersect1d(train_set, test_set)): 

148 raise ValueError('Overlap between training and test set') 

149 if len(test_set) == 0: 

150 test_set = None 

151 

152 self._train_set = train_set 

153 self._test_set = test_set 

154 

155 def _get_rows_via_sizes(self, 

156 train_size: Union[int, float], 

157 test_size: Union[int, float]) \ 

158 -> Tuple[List[int], List[int]]: 

159 """ Returns train and test rows via sizes. """ 

160 

161 # Handle special cases 

162 if test_size is None and train_size is None: 

163 raise ValueError('Training and test set sizes are None (empty).') 

164 elif train_size is None and abs(test_size - 1.0) < 1e-10: 

165 raise ValueError('Traininig set is empty.') 

166 

167 elif test_size is None: 

168 if train_size == self._n_rows or abs(train_size-1.0) < 1e-10: 

169 train_set = np.arange(self._n_rows) 

170 test_set = None 

171 return train_set, test_set 

172 

173 # split 

174 train_set, test_set = train_test_split(np.arange(self._n_rows), 

175 train_size=train_size, 

176 test_size=test_size, 

177 random_state=self.seed) 

178 

179 return train_set, test_set 

180 

181 def _get_rows_from_indices(self, 

182 train_set: Union[Tuple[int], List[int]], 

183 test_set: Union[Tuple[int], List[int]]) \ 

184 -> Tuple[np.ndarray, np.ndarray]: 

185 """ Returns rows via indices. """ 

186 if train_set is None and test_set is None: 

187 raise ValueError('Training and test sets are None (empty)') 

188 elif test_set is None: 

189 test_set = [i for i in range(self._n_rows) 

190 if i not in train_set] 

191 elif train_set is None: 

192 train_set = [i for i in range(self._n_rows) 

193 if i not in test_set] 

194 return np.array(train_set), np.array(test_set) 

195 

196 @property 

197 def summary(self) -> Dict[str, Any]: 

198 """ comprehensive information about the optimizer """ 

199 info = super().summary 

200 

201 # Add class specific data 

202 info['rmse_train'] = self.rmse_train 

203 info['rmse_test'] = self.rmse_test 

204 info['train_size'] = self.train_size 

205 info['train_set'] = self.train_set 

206 info['test_size'] = self.test_size 

207 info['test_set'] = self.test_set 

208 info['train_scatter_data'] = self.train_scatter_data 

209 info['test_scatter_data'] = self.test_scatter_data 

210 

211 # add kwargs used for fitting 

212 info = {**info, **self._kwargs} 

213 return info 

214 

215 def __repr__(self) -> str: 

216 kwargs = dict() 

217 kwargs['fit_method'] = self.fit_method 

218 kwargs['traininig_size'] = self.train_size 

219 kwargs['test_size'] = self.test_size 

220 kwargs['train_set'] = self.train_set 

221 kwargs['test_set'] = self.test_set 

222 kwargs['seed'] = self.seed 

223 kwargs = {**kwargs, **self._kwargs} 

224 return 'Optimizer((A, y), {})'.format( 

225 ', '.join('{}={}'.format(*kwarg) for kwarg in kwargs.items())) 

226 

227 @property 

228 def rmse_train(self) -> float: 

229 """ root mean squared error for training set """ 

230 return self._rmse_train 

231 

232 @property 

233 def rmse_test(self) -> float: 

234 """ root mean squared error for test set """ 

235 return self._rmse_test 

236 

237 @property 

238 def contributions_train(self) -> np.ndarray: 

239 """ average contribution to the predicted values for 

240 the train set from each parameter """ 

241 return self._contributions_train 

242 

243 @property 

244 def contributions_test(self) -> np.ndarray: 

245 """ average contribution to the predicted values for 

246 the test set from each parameter """ 

247 return self._contributions_test 

248 

249 @property 

250 def train_set(self) -> List[int]: 

251 """ indices of rows included in the training set """ 

252 return self._train_set 

253 

254 @property 

255 def test_set(self) -> List[int]: 

256 """ indices of rows included in the test set """ 

257 return self._test_set 

258 

259 @property 

260 def train_size(self) -> int: 

261 """ number of rows included in training set """ 

262 return len(self.train_set) 

263 

264 @property 

265 def train_fraction(self) -> float: 

266 """ fraction of rows included in training set """ 

267 return self.train_size / self._n_rows 

268 

269 @property 

270 def test_size(self) -> int: 

271 """ number of rows included in test set """ 

272 if self.test_set is None: 

273 return 0 

274 return len(self.test_set) 

275 

276 @property 

277 def test_fraction(self) -> float: 

278 """ fraction of rows included in test set """ 

279 if self.test_set is None: 

280 return 0.0 

281 return self.test_size / self._n_rows