Coverage for mlprodict/onnx_conv/sklconv/svm_converters.py: 91%

161 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-04 02:28 +0100

1""" 

2@file 

3@brief Rewrites some of the converters implemented in 

4:epkg:`sklearn-onnx`. 

5""" 

6import numbers 

7import numpy 

8from scipy.sparse import isspmatrix 

9from onnx import TensorProto 

10from skl2onnx.operator_converters.support_vector_machines import ( 

11 convert_sklearn_svm_regressor) 

12from skl2onnx.common.data_types import guess_numpy_type, guess_proto_type 

13from skl2onnx.common._apply_operation import ( 

14 apply_cast, apply_add, apply_div, apply_mul, apply_concat, 

15 apply_less, apply_abs) 

16 

17 

18def _op_type_domain_regressor(dtype): 

19 """ 

20 Defines *op_type* and *op_domain* based on `dtype`. 

21 """ 

22 if dtype == numpy.float32: 

23 return 'SVMRegressor', 'ai.onnx.ml', 1 

24 if dtype == numpy.float64: 

25 return 'SVMRegressorDouble', 'mlprodict', 1 

26 raise RuntimeError( # pragma: no cover 

27 f"Unsupported dtype {dtype}.") 

28 

29 

30def _op_type_domain_classifier(dtype): 

31 """ 

32 Defines *op_type* and *op_domain* based on `dtype`. 

33 """ 

34 if dtype == numpy.float32: 

35 return 'SVMClassifier', 'ai.onnx.ml', 1 

36 if dtype == numpy.float64: 

37 return 'SVMClassifierDouble', 'mlprodict', 1 

38 raise RuntimeError( # pragma: no cover 

39 f"Unsupported dtype {dtype}.") 

40 

41 

42def new_convert_sklearn_svm_regressor(scope, operator, container): 

43 """ 

44 Rewrites the converters implemented in 

45 :epkg:`sklearn-onnx` to support an operator supporting 

46 doubles. 

47 """ 

48 dtype = guess_numpy_type(operator.inputs[0].type) 

49 if dtype != numpy.float64: 

50 dtype = numpy.float32 

51 op_type, op_domain, op_version = _op_type_domain_regressor(dtype) 

52 convert_sklearn_svm_regressor( 

53 scope, operator, container, op_type=op_type, op_domain=op_domain, 

54 op_version=op_version) 

55 

56 

57def new_convert_sklearn_svm_classifier(scope, operator, container): 

58 """ 

59 Rewrites the converters implemented in 

60 :epkg:`sklearn-onnx` to support an operator supporting 

61 doubles. 

62 """ 

63 dtype = guess_numpy_type(operator.inputs[0].type) 

64 if dtype != numpy.float64: 

65 dtype = numpy.float32 

66 op_type, op_domain, op_version = _op_type_domain_classifier(dtype) 

67 _convert_sklearn_svm_classifier( 

68 scope, operator, container, op_type=op_type, op_domain=op_domain, 

69 op_version=op_version) 

70 

71 

72def _convert_sklearn_svm_classifier( 

73 scope, operator, container, 

74 op_type='SVMClassifier', op_domain='ai.onnx.ml', op_version=1): 

75 """ 

76 Converter for model 

77 `SVC <https://scikit-learn.org/stable/modules/ 

78 generated/sklearn.svm.SVC.html>`_, 

79 `NuSVC <https://scikit-learn.org/stable/modules/ 

80 generated/sklearn.svm.NuSVC.html>`_. 

81 The converted model in ONNX produces the same results as the 

82 original model except when probability=False: 

83 *onnxruntime* and *scikit-learn* do not return the same raw 

84 scores. *scikit-learn* returns aggregated scores 

85 as a *matrix[N, C]* coming from `_ovr_decision_function 

86 <https://github.com/scikit-learn/scikit-learn/blob/master/ 

87 sklearn/utils/multiclass.py#L402>`_. *onnxruntime* returns 

88 the raw score from *svm* algorithm as a *matrix[N, (C(C-1)/2]*. 

89 """ 

90 from sklearn.svm import NuSVC, SVC 

91 proto_dtype = guess_proto_type(operator.inputs[0].type) 

92 if proto_dtype != TensorProto.DOUBLE: # pylint: disable=E1101 

93 proto_dtype = TensorProto.FLOAT # pylint: disable=E1101 

94 numpy_type = numpy.float32 

95 else: 

96 numpy_type = numpy.float64 

97 

98 svm_attrs = {'name': scope.get_unique_operator_name('SVMc')} 

99 op = operator.raw_operator 

100 if isinstance(op.dual_coef_, numpy.ndarray): 

101 coef = op.dual_coef_.ravel() 

102 else: 

103 coef = op.dual_coef_ 

104 intercept = op.intercept_ 

105 if isinstance(op.support_vectors_, numpy.ndarray): 

106 support_vectors = op.support_vectors_.ravel() 

107 elif isspmatrix(op.support_vectors_): 

108 support_vectors = op.support_vectors_.toarray().ravel() 

109 else: 

110 support_vectors = op.support_vectors_ 

111 

112 svm_attrs['kernel_type'] = op.kernel.upper() 

113 svm_attrs['kernel_params'] = [float(_) 

114 for _ in [op._gamma, op.coef0, op.degree]] 

115 svm_attrs['support_vectors'] = support_vectors 

116 

117 if (operator.type in ['SklearnSVC', 'SklearnNuSVC'] or isinstance( 

118 op, (SVC, NuSVC))) and len(op.classes_) == 2: 

119 if isspmatrix(coef): 

120 coef_dense = coef.toarray().ravel() 

121 svm_attrs['coefficients'] = -coef_dense 

122 else: 

123 svm_attrs['coefficients'] = -coef 

124 svm_attrs['rho'] = -intercept 

125 else: 

126 if isspmatrix(coef): 

127 svm_attrs['coefficients'] = coef.todense() 

128 else: 

129 svm_attrs['coefficients'] = coef 

130 svm_attrs['rho'] = intercept 

131 

132 handles_ovr = False 

133 svm_attrs['coefficients'] = svm_attrs['coefficients'].astype(numpy_type) 

134 svm_attrs['support_vectors'] = svm_attrs['support_vectors'].astype( 

135 numpy_type) 

136 svm_attrs['rho'] = svm_attrs['rho'].astype(numpy_type) 

137 

138 options = container.get_options(op, dict(raw_scores=False)) 

139 use_raw_scores = options['raw_scores'] 

140 

141 if operator.type in ['SklearnSVC', 'SklearnNuSVC'] or isinstance( 

142 op, (SVC, NuSVC)): 

143 if len(op.probA_) > 0: 

144 svm_attrs['prob_a'] = op.probA_.astype(numpy_type) 

145 else: 

146 handles_ovr = True 

147 if len(op.probB_) > 0: 

148 svm_attrs['prob_b'] = op.probB_.astype(numpy_type) 

149 

150 if (hasattr(op, 'decision_function_shape') and 

151 op.decision_function_shape == 'ovr' and handles_ovr and 

152 len(op.classes_) > 2): 

153 output_name = scope.get_unique_variable_name('before_ovr') 

154 elif len(op.classes_) == 2 and use_raw_scores: 

155 output_name = scope.get_unique_variable_name('raw_scores') 

156 else: 

157 output_name = operator.outputs[1].full_name 

158 

159 svm_attrs['post_transform'] = 'NONE' 

160 svm_attrs['vectors_per_class'] = op.n_support_.tolist() 

161 

162 label_name = operator.outputs[0].full_name 

163 probability_tensor_name = output_name 

164 

165 if all(isinstance(i, (numbers.Real, bool, numpy.bool_)) 

166 for i in op.classes_): 

167 labels = [int(i) for i in op.classes_] 

168 svm_attrs['classlabels_ints'] = labels 

169 elif all(isinstance(i, str) for i in op.classes_): 

170 labels = [str(i) for i in op.classes_] 

171 svm_attrs['classlabels_strings'] = labels 

172 else: 

173 raise RuntimeError(f"Invalid class label type '{op.classes_}'.") 

174 

175 svm_out = scope.get_unique_variable_name('SVM02') 

176 container.add_node( 

177 op_type, operator.inputs[0].full_name, 

178 [label_name, svm_out], 

179 op_domain=op_domain, op_version=op_version, **svm_attrs) 

180 apply_cast(scope, svm_out, probability_tensor_name, 

181 container, to=proto_dtype) 

182 if len(op.classes_) == 2 and use_raw_scores: 

183 minus_one = scope.get_unique_variable_name('minus_one') 

184 container.add_initializer(minus_one, proto_dtype, [], [-1]) 

185 container.add_node( 

186 'Mul', [output_name, minus_one], operator.outputs[1].full_name, 

187 name=scope.get_unique_operator_name('MulRawScores')) 

188 else: 

189 raise ValueError("Unknown support vector machine model type found " 

190 "'{0}'.".format(operator.type)) 

191 

192 if (hasattr(op, 'decision_function_shape') and 

193 op.decision_function_shape == 'ovr' and handles_ovr and 

194 len(op.classes_) > 2): 

195 # Applies _ovr_decision_function. 

196 # See https://github.com/scikit-learn/scikit-learn/blob/ 

197 # master/sklearn/utils/multiclass.py#L407: 

198 # :: 

199 # _ovr_decision_function(dec < 0, -dec, len(self.classes_)) 

200 # 

201 # ... 

202 # def _ovr_decision_function(predictions, confidences, n_classes): 

203 # 

204 # n_samples = predictions.shape[0] 

205 # votes = numpy.zeros((n_samples, n_classes)) 

206 # sum_of_confidences = numpy.zeros((n_samples, n_classes)) 

207 # k = 0 

208 # for i in range(n_classes): 

209 # for j in range(i + 1, n_classes): 

210 # sum_of_confidences[:, i] -= confidences[:, k] 

211 # sum_of_confidences[:, j] += confidences[:, k] 

212 # votes[predictions[:, k] == 0, i] += 1 

213 # votes[predictions[:, k] == 1, j] += 1 

214 # k += 1 

215 # transformed_confidences = ( 

216 # sum_of_confidences / (3 * (numpy.abs(sum_of_confidences) + 1))) 

217 # return votes + transformed_confidences 

218 

219 cst3 = scope.get_unique_variable_name('cst3') 

220 container.add_initializer(cst3, proto_dtype, [], [3]) 

221 cst1 = scope.get_unique_variable_name('cst1') 

222 container.add_initializer(cst1, proto_dtype, [], [1]) 

223 cst0 = scope.get_unique_variable_name('cst0') 

224 container.add_initializer(cst0, proto_dtype, [], [0]) 

225 

226 prediction = scope.get_unique_variable_name('prediction') 

227 if apply_less is None: 

228 raise RuntimeError( 

229 "Function apply_less is missing. " 

230 "onnxconverter-common is too old.") 

231 proto_dtype = guess_proto_type(operator.inputs[0].type) 

232 if proto_dtype != TensorProto.DOUBLE: # pylint: disable=E1101 

233 proto_dtype = TensorProto.FLOAT # pylint: disable=E1101 

234 apply_less(scope, [output_name, cst0], prediction, container) 

235 iprediction = scope.get_unique_variable_name('iprediction') 

236 apply_cast(scope, prediction, iprediction, container, 

237 to=proto_dtype) 

238 

239 n_classes = len(op.classes_) 

240 sumc_name = [scope.get_unique_variable_name('svcsumc_%d' % i) 

241 for i in range(n_classes)] 

242 vote_name = [scope.get_unique_variable_name('svcvote_%d' % i) 

243 for i in range(n_classes)] 

244 sumc_add = {n: [] for n in sumc_name} 

245 vote_add = {n: [] for n in vote_name} 

246 k = 0 

247 for i in range(n_classes): 

248 for j in range(i + 1, n_classes): 

249 name = scope.get_unique_operator_name( 

250 'ArrayFeatureExtractor') 

251 ext = scope.get_unique_variable_name('Csvc_%d' % k) 

252 ind = scope.get_unique_variable_name('Cind_%d' % k) 

253 container.add_initializer( 

254 ind, TensorProto.INT64, [], [k]) # pylint: disable=E1101 

255 container.add_node( 

256 'ArrayFeatureExtractor', [output_name, ind], 

257 ext, op_domain='ai.onnx.ml', name=name) 

258 sumc_add[sumc_name[i]].append(ext) 

259 

260 neg = scope.get_unique_variable_name('Cneg_%d' % k) 

261 name = scope.get_unique_operator_name('Neg') 

262 container.add_node( 

263 'Neg', ext, neg, op_domain='', name=name, 

264 op_version=6) 

265 sumc_add[sumc_name[j]].append(neg) 

266 

267 # votes 

268 name = scope.get_unique_operator_name( 

269 'ArrayFeatureExtractor') 

270 ext = scope.get_unique_variable_name('Vsvcv_%d' % k) 

271 container.add_node( 

272 'ArrayFeatureExtractor', [iprediction, ind], 

273 ext, op_domain='ai.onnx.ml', name=name) 

274 vote_add[vote_name[j]].append(ext) 

275 neg = scope.get_unique_variable_name('Vnegv_%d' % k) 

276 name = scope.get_unique_operator_name('Neg') 

277 container.add_node( 

278 'Neg', ext, neg, op_domain='', name=name, 

279 op_version=6) 

280 neg1 = scope.get_unique_variable_name('Vnegv1_%d' % k) 

281 apply_add(scope, [neg, cst1], neg1, container, broadcast=1, 

282 operator_name='AddCl_%d_%d' % (i, j)) 

283 vote_add[vote_name[i]].append(neg1) 

284 

285 # next 

286 k += 1 

287 

288 for k, v in sumc_add.items(): 

289 name = scope.get_unique_operator_name('Sum') 

290 container.add_node( 

291 'Sum', v, k, op_domain='', name=name, op_version=8) 

292 for k, v in vote_add.items(): 

293 name = scope.get_unique_operator_name('Sum') 

294 container.add_node( 

295 'Sum', v, k, op_domain='', name=name, op_version=8) 

296 

297 conc = scope.get_unique_variable_name('Csvcconc') 

298 apply_concat(scope, sumc_name, conc, container, axis=1) 

299 conc_vote = scope.get_unique_variable_name('Vsvcconcv') 

300 apply_concat(scope, vote_name, conc_vote, container, axis=1) 

301 

302 conc_abs = scope.get_unique_variable_name('Cabs') 

303 apply_abs(scope, conc, conc_abs, container) 

304 

305 conc_abs1 = scope.get_unique_variable_name('Cconc_abs1') 

306 apply_add(scope, [conc_abs, cst1], conc_abs1, container, broadcast=1, 

307 operator_name='AddF0') 

308 conc_abs3 = scope.get_unique_variable_name('Cconc_abs3') 

309 apply_mul(scope, [conc_abs1, cst3], conc_abs3, container, broadcast=1) 

310 

311 final = scope.get_unique_variable_name('Csvcfinal') 

312 apply_div( 

313 scope, [conc, conc_abs3], final, container, broadcast=0) 

314 

315 output_name = operator.outputs[1].full_name 

316 apply_add( 

317 scope, [conc_vote, final], output_name, container, broadcast=0, 

318 operator_name='AddF1')