pulsatrix
Loading...
Searching...
No Matches
kernel_shap.hpp
Go to the documentation of this file.
1
7#pragma once
8
9#include <cstdint>
10#include <functional>
11#include <stdexcept>
12#include <string>
13#include <vector>
14
15#include "pulsatrix/assert.hpp"
18#include "pulsatrix/tensor.hpp"
20
21namespace pulsatrix {
22
23namespace detail {
24
26inline double BinomialCoefficient(int64_t n, int64_t k) {
27 double result = 1.0;
28 for (int64_t i = 1; i <= k; ++i) {
29 result = result * static_cast<double>(n - k + i) / static_cast<double>(i);
30 }
31 return result;
32}
33
39inline float ShapKernelWeight(int64_t n, int64_t coalition_size) {
40 double denominator =
41 BinomialCoefficient(n, coalition_size) * static_cast<double>(coalition_size) * static_cast<double>(n - coalition_size);
42 return static_cast<float>(static_cast<double>(n - 1) / denominator);
43}
44
45} // namespace detail
46
66public:
77 [[nodiscard]] Attribution explain(const std::function<Tensor(const Tensor&)>& predict, const Tensor& input,
78 const Tensor& baseline, int64_t target_index, DeviceBackend* backend) const {
79 // External boundary (Mission 2, finding 15 systemic sweep) -- escalated from
80 // PULSATRIX_ASSERT-only.
81 if (input.numel() != baseline.numel()) {
82 throw std::invalid_argument("KernelSHAP::explain: input and baseline must have the same numel");
83 }
84 const int64_t n = input.numel();
85 if (n < 1 || n > 20) { // full 2^n enumeration -- this campaign's small-n scope
86 throw std::invalid_argument("KernelSHAP::explain: input feature count must be in [1, 20]");
87 }
88
89 const std::vector<float> input_values = input.to_host_vector();
90 const std::vector<float> baseline_values = baseline.to_host_vector();
91 DeviceBackend* input_backend = explainer_detail::backend_beside(input, backend);
92 std::vector<float> z_values(static_cast<size_t>(n));
93
94 auto coalition_value = [&](uint64_t mask) {
95 for (int64_t i = 0; i < n; ++i) {
96 bool active = ((mask >> i) & 1u) != 0;
97 const auto idx = static_cast<size_t>(i);
98 z_values[idx] = active ? input_values[idx] : baseline_values[idx];
99 }
100 Tensor z(input.shape(), input_backend, z_values, input.device());
101 Tensor out = predict(z);
102 return out.read_element(target_index);
103 };
104
105 const float f_baseline = coalition_value(0);
106 const uint64_t full_mask = (n == 64) ? ~0ULL : ((1ULL << n) - 1);
107 const float total_diff = coalition_value(full_mask) - f_baseline;
108
109 std::vector<float> phi(static_cast<size_t>(n), 0.0f);
110
111 if (n == 1) {
112 phi[0] = total_diff;
113 } else {
114 std::vector<std::vector<float>> reduced_samples;
115 std::vector<float> reduced_targets;
116 std::vector<float> weights;
117
118 for (uint64_t mask = 1; mask < full_mask; ++mask) {
119 int64_t coalition_size = 0;
120 for (int64_t i = 0; i < n; ++i) {
121 if ((mask >> i) & 1u) {
122 ++coalition_size;
123 }
124 }
125 bool z_last = ((mask >> (n - 1)) & 1u) != 0;
126 float value = coalition_value(mask) - f_baseline;
127 float reduced_target = value - (z_last ? total_diff : 0.0f);
128
129 std::vector<float> reduced_row(static_cast<size_t>(n - 1));
130 for (int64_t i = 0; i < n - 1; ++i) {
131 bool z_i = ((mask >> i) & 1u) != 0;
132 reduced_row[static_cast<size_t>(i)] = (z_i ? 1.0f : 0.0f) - (z_last ? 1.0f : 0.0f);
133 }
134
135 reduced_samples.push_back(std::move(reduced_row));
136 reduced_targets.push_back(reduced_target);
137 weights.push_back(detail::ShapKernelWeight(n, coalition_size));
138 }
139
140 std::vector<float> reduced_phi =
141 fit_weighted_linear_regression(reduced_samples, reduced_targets, weights, 0.0f);
142
143 float sum_reduced = 0.0f;
144 for (int64_t i = 0; i < n - 1; ++i) {
145 phi[static_cast<size_t>(i)] = reduced_phi[static_cast<size_t>(i)];
146 sum_reduced += reduced_phi[static_cast<size_t>(i)];
147 }
148 phi[static_cast<size_t>(n - 1)] = total_diff - sum_reduced;
149 }
150
151 Tensor values(input.shape(), input_backend, phi, input.device());
152
153 return Attribution{"kernel_shap", std::move(values), {{"target_index", std::to_string(target_index)}}};
154 }
155};
156
157} // namespace pulsatrix
PULSATRIX_ASSERT – debug-only invariant check for programmer errors, distinct from throw (used for ca...
First-class explanation result type – values, method, and metadata together.
Vendor-agnostic compute/memory backend. CPUBackend, CUDABackend (Phase 1.5), and HIPBackend (Phase 1....
Definition device_backend.hpp:219
Approximates Shapley values via full coalition enumeration + SHAP-kernel-weighted linear regression,...
Definition kernel_shap.hpp:65
Attribution explain(const std::function< Tensor(const Tensor &)> &predict, const Tensor &input, const Tensor &baseline, int64_t target_index, DeviceBackend *backend) const
Computes Shapley value approximations for one output index.
Definition kernel_shap.hpp:77
N-dimensional tensor. Owns its data buffer exclusively; a DeviceBackend* is injected (not owned) – th...
Definition tensor.hpp:29
DeviceType device() const
Which device this tensor's buffer conceptually resides on.
Definition tensor.hpp:122
int64_t numel() const
Total element count – shape().numel().
Definition tensor.hpp:116
const Shape & shape() const
This tensor's shape.
Definition tensor.hpp:113
float read_element(int64_t flat_index) const
Reads one element to the host through the owning backend, on any device.
std::vector< float > to_host_vector() const
Copies the whole buffer to a host vector through the owning backend, on any device.
Abstract interface isolating vendor-specific memory/compute operations from Tensor/ComputationGraph.
float ShapKernelWeight(int64_t n, int64_t coalition_size)
The SHAP kernel pi(z') = (n-1) / [C(n,|z'|) * |z'| * (n-|z'|)], for 1 <= |z'| <= n-1.
Definition kernel_shap.hpp:39
double BinomialCoefficient(int64_t n, int64_t k)
Binomial coefficient C(n,k), computed iteratively to avoid factorial overflow.
Definition kernel_shap.hpp:26
DeviceBackend * backend_beside(const Tensor &like, DeviceBackend *backend)
The backend to allocate a tensor through that must live beside like.
Definition attribution.hpp:49
Definition acquisition_functions.hpp:16
std::vector< float > fit_weighted_linear_regression(const std::vector< std::vector< float > > &samples, const std::vector< float > &targets, const std::vector< float > &weights, float l2_lambda)
Fits w* = argmin_w sum_i weight_i*(target_i - w^T sample_i)^2 + l2_lambda*||w||^2 via the normal equa...
Definition weighted_linear_regression.hpp:38
An explanation result: the raw attribution values, the method that produced them, and any relevant me...
Definition attribution.hpp:23
N-dimensional tensor – owns a buffer via DeviceBackend*, RAII (Rule of Five).
Weighted least squares via normal equations – the shared fitting primitive Phase 3's LIME and KernelS...