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
« 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)
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}.")
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}.")
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)
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)
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
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_
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
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
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)
138 options = container.get_options(op, dict(raw_scores=False))
139 use_raw_scores = options['raw_scores']
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)
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
159 svm_attrs['post_transform'] = 'NONE'
160 svm_attrs['vectors_per_class'] = op.n_support_.tolist()
162 label_name = operator.outputs[0].full_name
163 probability_tensor_name = output_name
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_}'.")
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))
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
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])
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)
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)
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)
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)
285 # next
286 k += 1
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)
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)
302 conc_abs = scope.get_unique_variable_name('Cabs')
303 apply_abs(scope, conc, conc_abs, container)
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)
311 final = scope.get_unique_variable_name('Csvcfinal')
312 apply_div(
313 scope, [conc, conc_abs3], final, container, broadcast=0)
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')