5cbd3f29e3
Fuzz / Run fuzz harnesses (${{ github.event_name == 'schedule' && 'nightly' || 'smoke' }}) (push) Has been cancelled
Create Releases / call-mac (push) Has been cancelled
Create Releases / call-linux (push) Has been cancelled
Create Releases / call-sdist (push) Has been cancelled
Create Releases / call-win (push) Has been cancelled
Create Releases / call-pyodide (push) Has been cancelled
Windows_No_Exception_CI / build (x64, 3.10) (push) Has been cancelled
Check URLs / build (push) Has been cancelled
Create Releases / Attest CI build artifacts (push) Has been cancelled
Create Releases / Check for Publish release build to pypi (push) Has been cancelled
Create Releases / Check for Publish preview build to test.pypi-weekly (push) Has been cancelled
Create Releases / Publish preview build to test.pypi-weekly (push) Has been cancelled
Create Releases / Check for Publish release build to test.pypi (rc-candidates) (push) Has been cancelled
Create Releases / Publish release build to test.pypi (push) Has been cancelled
Create Releases / Check for Publish preview build to pypi-weekly (push) Has been cancelled
Create Releases / Publish preview build to pypi-weekly (push) Has been cancelled
Create Releases / Publish release build to pypi (push) Has been cancelled
Create Releases / test source distribution (push) Has been cancelled
clang-tidy / clang-tidy (push) Has been cancelled
Lint / Validate SBOM (push) Has been cancelled
Lint / Enforce style (push) Has been cancelled
CI / Test windows-2022, 3.14, External, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test windows-latest, 3.10, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test windows-latest, 3.14, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test windows-latest, 3.14t, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, Internal, debug=1, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, External, debug=0, unity_build=1, onnx_ml=1, autogen=1 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, External, debug=0, unity_build=0, onnx_ml=0, autogen=0 (push) Has been cancelled
CI / Test macos-latest, 3.10, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test macos-latest, 3.14, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test macos-latest, 3.14t, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, External, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.10, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
CI / Test ubuntu-24.04, 3.14t, Internal, debug=0, unity_build=0, onnx_ml=1, autogen=0 (push) Has been cancelled
Pixi CI / Install and lint (ubuntu-24.04-arm) (push) Has been cancelled
Pixi CI / Install and lint (windows-2022) (push) Has been cancelled
Pixi CI / Xcode generator build (push) Has been cancelled
Pixi CI / Install and test (macos-latest, default) (push) Has been cancelled
Pixi CI / Install and test (ubuntu-24.04-arm, default) (push) Has been cancelled
Pixi CI / Install and test (ubuntu-latest, default) (push) Has been cancelled
Pixi CI / Install and test (windows-2022, default) (push) Has been cancelled
Pixi CI / Install and test (macos-latest, oldies) (push) Has been cancelled
Pixi CI / Install and test (ubuntu-24.04-arm, oldies) (push) Has been cancelled
Pixi CI / Install and test (ubuntu-latest, oldies) (push) Has been cancelled
Pixi CI / Install and test (windows-2022, oldies) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
Generate and publish ONNX docs / build (push) Has been cancelled
Generate and publish ONNX docs / deploy (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
269 lines
9.2 KiB
Python
269 lines
9.2 KiB
Python
from __future__ import annotations
|
|
|
|
from enum import IntEnum
|
|
from typing import TYPE_CHECKING
|
|
|
|
import numpy as np
|
|
|
|
from onnx.reference.ops.aionnxml._op_run_aionnxml import OpRunAiOnnxMl
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
|
|
class AggregationFunction(IntEnum):
|
|
AVERAGE = 0
|
|
SUM = 1
|
|
MIN = 2
|
|
MAX = 3
|
|
|
|
|
|
class PostTransform(IntEnum):
|
|
NONE = 0
|
|
SOFTMAX = 1
|
|
LOGISTIC = 2
|
|
SOFTMAX_ZERO = 3
|
|
PROBIT = 4
|
|
|
|
|
|
class Mode(IntEnum):
|
|
LEQ = 0
|
|
LT = 1
|
|
GTE = 2
|
|
GT = 3
|
|
EQ = 4
|
|
NEQ = 5
|
|
MEMBER = 6
|
|
|
|
|
|
class Leaf:
|
|
def __init__(self, weight: float, target_id: int) -> None:
|
|
self.weight = weight
|
|
self.target_id = target_id
|
|
|
|
# Produce the weight and target index
|
|
def predict(self, x: np.ndarray) -> np.ndarray: # noqa: ARG002
|
|
return np.array([self.weight, self.target_id])
|
|
|
|
def _print(self, prefix: list, indent: int = 0) -> None:
|
|
prefix.append(
|
|
" " * indent + f"Leaf WEIGHT: {self.weight}, TARGET: {self.target_id}\n"
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
prefix = []
|
|
self._print(prefix)
|
|
return "".join(prefix)
|
|
|
|
|
|
class Node:
|
|
compare: Callable[[float, float | set[float]], bool]
|
|
true_branch: Node | Leaf
|
|
false_branch: Node | Leaf
|
|
feature: int
|
|
|
|
def __init__(
|
|
self,
|
|
mode: Mode,
|
|
value: float | set[float],
|
|
feature: int,
|
|
missing_tracks_true: bool,
|
|
) -> None:
|
|
if mode == Mode.LEQ:
|
|
self.compare = lambda x: (
|
|
x[feature].item() <= value
|
|
or (missing_tracks_true and np.isnan(x[feature].item()))
|
|
)
|
|
elif mode == Mode.LT:
|
|
self.compare = lambda x: (
|
|
x[feature].item() < value
|
|
or (missing_tracks_true and np.isnan(x[feature].item()))
|
|
)
|
|
elif mode == Mode.GTE:
|
|
self.compare = lambda x: (
|
|
x[feature].item() >= value
|
|
or (missing_tracks_true and np.isnan(x[feature].item()))
|
|
)
|
|
elif mode == Mode.GT:
|
|
self.compare = lambda x: (
|
|
x[feature].item() > value
|
|
or (missing_tracks_true and np.isnan(x[feature].item()))
|
|
)
|
|
elif mode == Mode.EQ:
|
|
self.compare = lambda x: (
|
|
x[feature].item() == value
|
|
or (missing_tracks_true and np.isnan(x[feature].item()))
|
|
)
|
|
elif mode == Mode.NEQ:
|
|
self.compare = lambda x: (
|
|
x[feature].item() != value
|
|
or (missing_tracks_true and np.isnan(x[feature].item()))
|
|
)
|
|
elif mode == Mode.MEMBER:
|
|
self.compare = lambda x: (
|
|
x[feature].item() in value
|
|
or (missing_tracks_true and np.isnan(x[feature].item()))
|
|
)
|
|
self.mode = mode
|
|
self.value = value
|
|
self.feature = feature
|
|
|
|
def predict(self, x: np.ndarray) -> float:
|
|
if self.compare(x):
|
|
return self.true_branch.predict(x)
|
|
return self.false_branch.predict(x)
|
|
|
|
def _print(self, prefix: list, indent: int = 0) -> None:
|
|
prefix.append(
|
|
" " * indent
|
|
+ f"Node CMP: {self.mode}, SPLIT: {self.value}, FEATURE: {self.feature}\n"
|
|
)
|
|
self.true_branch._print(prefix, indent + 1)
|
|
self.false_branch._print(prefix, indent + 1)
|
|
|
|
def __repr__(self) -> str:
|
|
prefix = []
|
|
self._print(prefix)
|
|
return "".join(prefix)
|
|
|
|
|
|
class TreeEnsemble(OpRunAiOnnxMl):
|
|
def _run(
|
|
self,
|
|
X,
|
|
nodes_splits,
|
|
nodes_featureids,
|
|
nodes_modes,
|
|
nodes_truenodeids,
|
|
nodes_falsenodeids,
|
|
nodes_trueleafs,
|
|
nodes_falseleafs,
|
|
leaf_targetids,
|
|
leaf_weights,
|
|
tree_roots,
|
|
post_transform=PostTransform.NONE, # noqa: ARG002
|
|
aggregate_function=AggregationFunction.SUM,
|
|
nodes_hitrates=None, # noqa: ARG002
|
|
nodes_missing_value_tracks_true=None,
|
|
membership_values=None,
|
|
n_targets=None,
|
|
):
|
|
if membership_values is None:
|
|
# assert that no set membership ever appears
|
|
if any(mode == Mode.MEMBER for mode in nodes_modes):
|
|
raise ValueError(
|
|
"Cannot have set membership node without specifying set members"
|
|
)
|
|
elif np.isnan(membership_values).sum() != sum(
|
|
int(mode == Mode.MEMBER) for mode in nodes_modes
|
|
):
|
|
raise ValueError(
|
|
"Must specify membership values for all set membership nodes"
|
|
)
|
|
|
|
# Build each tree in the ensemble. Note that the tree structure is implicitly defined by following the true and false indices in
|
|
# `nodes_truenodeids` and `nodes_falsenodeids` to the leaves of each tree.
|
|
set_membership_iter = (
|
|
iter(membership_values) if membership_values is not None else None
|
|
)
|
|
|
|
def build_node(current_node_index, is_leaf) -> Node | Leaf:
|
|
if is_leaf:
|
|
return Leaf(
|
|
leaf_weights[current_node_index], leaf_targetids[current_node_index]
|
|
)
|
|
|
|
if nodes_modes[current_node_index] == Mode.MEMBER:
|
|
# parse next sequence of set members
|
|
set_members = set()
|
|
while (set_member := next(set_membership_iter)) and not np.isnan(
|
|
set_member
|
|
):
|
|
set_members.add(set_member)
|
|
node = Node(
|
|
nodes_modes[current_node_index],
|
|
set_members,
|
|
nodes_featureids[current_node_index],
|
|
(
|
|
nodes_missing_value_tracks_true[current_node_index]
|
|
if nodes_missing_value_tracks_true is not None
|
|
else False
|
|
),
|
|
)
|
|
else:
|
|
node = Node(
|
|
nodes_modes[current_node_index],
|
|
nodes_splits[current_node_index],
|
|
nodes_featureids[current_node_index],
|
|
(
|
|
nodes_missing_value_tracks_true[current_node_index]
|
|
if nodes_missing_value_tracks_true is not None
|
|
else False
|
|
),
|
|
)
|
|
|
|
# recurse true and false branches
|
|
node.true_branch = build_node(
|
|
nodes_truenodeids[current_node_index],
|
|
nodes_trueleafs[current_node_index],
|
|
)
|
|
node.false_branch = build_node(
|
|
nodes_falsenodeids[current_node_index],
|
|
nodes_falseleafs[current_node_index],
|
|
)
|
|
return node
|
|
|
|
trees = []
|
|
for root_index in tree_roots:
|
|
# degenerate case (tree == leaf)
|
|
is_leaf = (
|
|
nodes_trueleafs[root_index]
|
|
and nodes_falseleafs[root_index]
|
|
and nodes_truenodeids[root_index] == nodes_falsenodeids[root_index]
|
|
)
|
|
trees.append(build_node(root_index, is_leaf))
|
|
|
|
# predict each sample through tree
|
|
raw_values = [
|
|
np.apply_along_axis(tree.predict, axis=1, arr=X) for tree in trees
|
|
]
|
|
weights, target_ids = zip(
|
|
*[np.split(x, 2, axis=1) for x in raw_values], strict=False
|
|
)
|
|
weights = np.concatenate(weights, axis=1)
|
|
target_ids = np.concatenate(target_ids, axis=1).astype(np.int64)
|
|
if aggregate_function in (
|
|
AggregationFunction.SUM,
|
|
AggregationFunction.AVERAGE,
|
|
):
|
|
result = np.zeros((len(X), n_targets), dtype=X.dtype)
|
|
elif aggregate_function == AggregationFunction.MIN:
|
|
result = np.full((len(X), n_targets), np.finfo(X.dtype).max)
|
|
elif aggregate_function == AggregationFunction.MAX:
|
|
result = np.full((len(X), n_targets), np.finfo(X.dtype).min)
|
|
else:
|
|
raise NotImplementedError(
|
|
f"aggregate_transform={aggregate_function!r} not supported yet."
|
|
)
|
|
for batch_num, (w, t) in enumerate(zip(weights, target_ids, strict=False)):
|
|
weight = w.reshape(-1)
|
|
target_id = t.reshape(-1)
|
|
if aggregate_function == AggregationFunction.SUM:
|
|
for value, tid in zip(weight, target_id, strict=False):
|
|
result[batch_num, tid] += value
|
|
elif aggregate_function == AggregationFunction.AVERAGE:
|
|
for value, tid in zip(weight, target_id, strict=False):
|
|
result[batch_num, tid] += value / len(trees)
|
|
elif aggregate_function == AggregationFunction.MIN:
|
|
for value, tid in zip(weight, target_id, strict=False):
|
|
result[batch_num, tid] = min(result[batch_num, tid], value)
|
|
elif aggregate_function == AggregationFunction.MAX:
|
|
for value, tid in zip(weight, target_id, strict=False):
|
|
result[batch_num, tid] = max(result[batch_num, tid], value)
|
|
else:
|
|
raise NotImplementedError(
|
|
f"aggregate_transform={aggregate_function!r} not supported yet."
|
|
)
|
|
|
|
return (result,)
|