pyhierarchicaltsetlinmachinecuda 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ole-Christoffer Granmo and the University of Agder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyhierarchicaltsetlinmachinecuda
3
+ Version: 0.2.0
4
+ Summary: Hierarchical Tsetlin Machine Architecture.
5
+ Home-page: https://github.com/cair/PyHierarchicalTsetlinMachineCUDA/
6
+ Author: Ole-Christoffer Granmo
7
+ Author-email: ole.granmo@uia.no
8
+ License: MIT
9
+ Keywords: pattern-recognition cuda machine-learning interpretable-machine-learning rule-based-machine-learning propositional-logic tsetlin-machine regression convolution classification multi-layer
10
+ License-File: LICENSE
11
+
12
+ Hierarchical Tsetlin Machine Architecture.
@@ -0,0 +1,133 @@
1
+ # Copyright (c) 2024 Ole-Christoffer Granmo
2
+
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ import numpy as np
22
+ from scipy.sparse import coo_matrix
23
+
24
+ class Graph():
25
+ def __init__(self):
26
+ self.node_name_id = {}
27
+ self.node_id_name = {}
28
+ self.node_edges = {}
29
+ self.node_features = {}
30
+ self.edge_counter = 0
31
+ self.feature_counter = 0
32
+
33
+ def add_node(self, node_name):
34
+ self.node_name_id[node_name] = len(self.node_name_id)
35
+ self.node_id_name[self.node_name_id[node_name]] = node_name
36
+ self.node_features[node_name] = []
37
+ self.node_edges[node_name] = []
38
+
39
+ def add_edge(self, node_name_1, node_name_2, edge_type=0):
40
+ self.node_edges[node_name_1].append((node_name_2, edge_type))
41
+ self.edge_counter += 1
42
+
43
+ def add_feature(self, node_name, symbols):
44
+ if type(symbols) == tuple:
45
+ symbols = " ".join(symbols)
46
+ self.node_features[node_name].append(symbols)
47
+ self.feature_counter += 1
48
+
49
+ class Graphs():
50
+ def __init__(self):
51
+ self.edge_type_id = {}
52
+ self.graphs = []
53
+ self.encoded = False
54
+
55
+ def add(self, graph):
56
+ self.graphs.append(graph)
57
+
58
+ def encode(self, hypervectors = {}, hypervector_size=1024, hypervector_bits=3):
59
+ self.hypervectors = hypervectors
60
+
61
+ self.hypervector_size = hypervector_size
62
+ self.hypervector_bits = hypervector_bits
63
+
64
+ global_feature_counter = 0
65
+ global_edge_counter = 0
66
+ for graph in self.graphs:
67
+ global_feature_counter += graph.feature_counter * self.hypervector_bits
68
+ global_edge_counter += len(graph.node_name_id) + graph.edge_counter*2
69
+
70
+ indexes = np.arange(self.hypervector_size, dtype=np.uint32)
71
+
72
+ feature_row = np.empty(global_feature_counter, dtype=np.uint32)
73
+ feature_col = np.empty(global_feature_counter, dtype=np.uint32)
74
+ feature_data = np.empty(global_feature_counter, dtype=np.uint32)
75
+
76
+ edge_row = np.empty(global_edge_counter, dtype=np.uint32)
77
+ edge_col = np.empty(global_edge_counter, dtype=np.uint32)
78
+ edge_data = np.empty(global_edge_counter, dtype=np.uint32)
79
+
80
+ self.node_count = np.empty(len(self.graphs), dtype=np.uint32)
81
+
82
+ feature_position = 0
83
+ edge_position = 0
84
+ for i in range(len(self.graphs)):
85
+ graph = self.graphs[i]
86
+ self.node_count[i] = len(graph.node_name_id)
87
+ local_edge_position = 0
88
+ for j in range(len(graph.node_name_id)):
89
+ node_name = graph.node_id_name[j]
90
+
91
+ for symbols in graph.node_features[node_name]:
92
+ if symbols not in self.hypervectors:
93
+ self.hypervectors[symbols] = np.random.choice(indexes, size=(self.hypervector_bits), replace=False)
94
+
95
+ for k in range(self.hypervector_bits):
96
+ feature_row[feature_position] = i
97
+ feature_col[feature_position] = self.hypervectors[symbols][k] + j*self.hypervector_size
98
+ feature_data[feature_position] = 1
99
+ feature_position += 1
100
+
101
+ edge_row[edge_position] = i
102
+ edge_col[edge_position] = local_edge_position
103
+ edge_data[edge_position] = len(graph.node_edges[node_name])
104
+ edge_position += 1
105
+ local_edge_position += 1
106
+ for edge in graph.node_edges[graph.node_id_name[j]]:
107
+ edge_row[edge_position] = i
108
+ edge_col[edge_position] = local_edge_position
109
+ edge_data[edge_position] = graph.node_name_id[edge[0]]
110
+ edge_position += 1
111
+ local_edge_position += 1
112
+
113
+ edge_row[edge_position] = i
114
+ edge_col[edge_position] = local_edge_position
115
+
116
+ if edge[1] not in self.edge_type_id:
117
+ self.edge_type_id[edge[1]] = len(self.edge_type_id)
118
+ edge_data[edge_position] = self.edge_type_id[edge[1]]
119
+ edge_position += 1
120
+ local_edge_position += 1
121
+
122
+ self.X = coo_matrix((feature_data, (feature_row, feature_col))).tocsr()
123
+ self.X.sum_duplicates()
124
+
125
+ self.edges = coo_matrix((edge_data, (edge_row, edge_col))).tocsr()
126
+
127
+ self.max_node_count = self.node_count.max()
128
+
129
+ self.signature = np.concatenate((self.X.indptr, self.X.indices, self.edges.indptr, self.edges.indices, self.edges.data))
130
+
131
+ self.encoded = True
132
+
133
+ return
@@ -0,0 +1,510 @@
1
+ # Copyright (c) 2026 Ole-Christoffer Granmo and the University of Agder
2
+
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ # This code implements the Convolutional Tsetlin Machine from paper arXiv:1905.09688
22
+ # https://arxiv.org/abs/1905.09688
23
+
24
+ code_header = """
25
+ #include <curand_kernel.h>
26
+
27
+ #define INT_SIZE 32ULL
28
+
29
+ #define LITERAL_LEAVES (LITERAL_CHUNKS / TA_CHUNKS_PER_LEAF)
30
+
31
+ #if (LITERALS_PER_LEAF % 32 != 0)
32
+ #define FILTER_HIERARCHICAL (~(0xffffffff << (LITERALS_PER_LEAF % INT_SIZE)))
33
+ #else
34
+ #define FILTER_HIERARCHICAL 0xffffffff
35
+ #endif
36
+
37
+ #define VANILLA_TM 0
38
+
39
+ #define WEIGHTED_TM 1
40
+
41
+ #define COALESCED_TM 2
42
+ """
43
+
44
+ code_update = """
45
+ extern "C"
46
+ {
47
+ // Increment the states of each of those 32 Tsetlin Automata flagged in the active bit vector.
48
+ __device__ inline void inc(unsigned int *ta_state, int chunk, unsigned int active)
49
+ {
50
+ unsigned int carry, carry_next;
51
+ int id = chunk*STATE_BITS;
52
+ carry = active;
53
+ for (int b = 0; b < STATE_BITS; ++b) {
54
+ if (carry == 0)
55
+ break;
56
+
57
+ carry_next = ta_state[id + b] & carry; // Sets carry bits (overflow) passing on to next bit
58
+ ta_state[id + b] = ta_state[id + b] ^ carry; // Performs increments with XOR
59
+ carry = carry_next;
60
+ }
61
+
62
+ if (carry > 0) {
63
+ for (int b = 0; b < STATE_BITS; ++b) {
64
+ ta_state[id + b] |= carry;
65
+ }
66
+ }
67
+ }
68
+
69
+ // Decrement the states of each of those 32 Tsetlin Automata flagged in the active bit vector.
70
+ __device__ inline void dec(unsigned int *ta_state, int chunk, unsigned int active)
71
+ {
72
+ unsigned int carry, carry_next;
73
+ int id = chunk*STATE_BITS;
74
+ carry = active;
75
+ for (int b = 0; b < STATE_BITS; ++b) {
76
+ if (carry == 0)
77
+ break;
78
+ carry_next = (~ta_state[id + b]) & carry; // Sets carry bits (overflow) passing on to next bit
79
+ ta_state[id + b] = ta_state[id + b] ^ carry; // Performs increments with XOR
80
+ carry = carry_next;
81
+ }
82
+
83
+ if (carry > 0) {
84
+ for (int b = 0; b < STATE_BITS; ++b) {
85
+ ta_state[id + b] &= ~carry;
86
+ }
87
+ }
88
+ }
89
+
90
+ __device__ inline void update_clause_weight(curandState *localState, int tm_type, int number_of_outputs, int *clause_weight, int clause_output, int y, int class_sum)
91
+ {
92
+ int target = 1 - 2*(class_sum > y);
93
+
94
+ if (target == -1 && curand_uniform(localState) > 1.0*Q/max(1, number_of_outputs-1)) {
95
+ return;
96
+ }
97
+
98
+ int sign = (*clause_weight >= 0) - (*clause_weight < 0);
99
+
100
+ int absolute_prediction_error = abs(y - class_sum);
101
+ if (curand_uniform(localState) <= 1.0*absolute_prediction_error/(2*THRESHOLD)) {
102
+ if (target*sign > 0) {
103
+ if (clause_output && abs(*clause_weight) < INT_MAX) {
104
+ (*clause_weight) += sign;
105
+ }
106
+ } else if (target*sign < 0 && clause_output) {
107
+ // Type II Feedback
108
+
109
+ if (*clause_weight < -1 || *clause_weight > 1 || tm_type == COALESCED_TM) {
110
+ (*clause_weight) -= sign;
111
+ }
112
+
113
+ #if NEGATIVE_CLAUSES == 0
114
+ if (*clause_weight < 1) {
115
+ *clause_weight = 1;
116
+ }
117
+ #endif
118
+ }
119
+ }
120
+ }
121
+
122
+ __device__ inline void update_component_hierarchy(curandState *localState, int number_of_outputs, int *clause_weight, unsigned int *ta_state, int component_output, int *X, int y, int class_sum)
123
+ {
124
+ int target = 1 - 2*(class_sum > y);
125
+
126
+ if (target == -1 && curand_uniform(localState) > 1.0*Q/max(1, number_of_outputs-1)) {
127
+ return;
128
+ }
129
+
130
+
131
+ int sign = (*clause_weight >= 0) - (*clause_weight < 0);
132
+
133
+ int absolute_prediction_error = abs(y - class_sum);
134
+ if (curand_uniform(localState) <= 1.0*absolute_prediction_error/(2*THRESHOLD)) {
135
+ if (target*sign > 0) {
136
+ // Type I Feedback
137
+ for (int ta_chunk = 0; ta_chunk < TA_CHUNKS_PER_LEAF; ++ta_chunk) {
138
+ // Generate random bit values
139
+ unsigned int la_feedback = 0;
140
+ for (int b = 0; b < INT_SIZE; ++b) {
141
+ if (curand_uniform(localState) <= 1.0/S) {
142
+ la_feedback |= (1 << b);
143
+ }
144
+ }
145
+
146
+ if (component_output) {
147
+ #if BOOST_TRUE_POSITIVE_FEEDBACK == 1
148
+ inc(ta_state, ta_chunk, X[ta_chunk]);
149
+ #else
150
+ inc(ta_state, ta_chunk, X[ta_chunk] & (~la_feedback));
151
+ #endif
152
+
153
+ dec(ta_state, ta_chunk, (~X[ta_chunk]) & la_feedback);
154
+ } else {
155
+ dec(ta_state, ta_chunk, la_feedback);
156
+ }
157
+ }
158
+ } else if (target*sign < 0 && component_output) {
159
+ // Type II Feedback
160
+
161
+ for (int ta_chunk = 0; ta_chunk < TA_CHUNKS_PER_LEAF; ++ta_chunk) {
162
+ inc(ta_state, ta_chunk, (~X[ta_chunk]) & (~ta_state[ta_chunk*STATE_BITS + STATE_BITS - 1]));
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ // Evaluate example
169
+ __global__ void evaluate_leaves(unsigned int *global_ta_state, int *component_weights, int *global_component_output, int depth, int *hierarchy_structure_factors, int *hierarchy_structure_alternatives, int *X, int example)
170
+ {
171
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
172
+ int stride = blockDim.x * gridDim.x;
173
+
174
+ int *Xi = &X[(unsigned long long)example*LITERAL_CHUNKS];
175
+
176
+ // Evaluate each clause component (leaf) in separate threads
177
+ for (int clause_component = index; clause_component < CLAUSES*COMPONENTS; clause_component += stride) {
178
+ int clause = clause_component / COMPONENTS;
179
+ int component = clause_component % COMPONENTS;
180
+
181
+ // Get state of current clause component
182
+ unsigned int *ta_state = &global_ta_state[clause_component*TA_CHUNKS_PER_LEAF*STATE_BITS];
183
+
184
+ int component_remainder = component;
185
+ int ta_chunk_base = 0;
186
+ int size = 1;
187
+ for (int d = 0; d < depth-1; ++d) {
188
+ int depth_d_node_index = component_remainder % hierarchy_structure_factors[d];
189
+ component_remainder = component_remainder / hierarchy_structure_factors[d];
190
+
191
+ if (hierarchy_structure_alternatives[d] == 0) {
192
+ ta_chunk_base += size * depth_d_node_index * TA_CHUNKS_PER_LEAF;
193
+ size *= hierarchy_structure_factors[d];
194
+ }
195
+
196
+ if (clause == -1 && d == depth-2) {
197
+ printf("%d: %d (%d) (%d = %d) %d\\n", d, component, depth_d_node_index, ta_chunk_base, (component % (LITERAL_CHUNKS / TA_CHUNKS_PER_LEAF))*TA_CHUNKS_PER_LEAF, component_remainder);
198
+ }
199
+ }
200
+
201
+ // Evaluate clause component
202
+ int component_output = 1;
203
+ for (int ta_chunk = 0; ta_chunk < TA_CHUNKS_PER_LEAF-1; ++ta_chunk) {
204
+ // Compare the TA state of the component (leaf) against the corresponding part of the feature vector
205
+ if ((ta_state[ta_chunk*STATE_BITS + STATE_BITS - 1] & Xi[ta_chunk_base + ta_chunk]) != ta_state[ta_chunk*STATE_BITS + STATE_BITS - 1]) {
206
+ component_output = 0;
207
+ break;
208
+ }
209
+ }
210
+
211
+ if ((ta_state[(TA_CHUNKS_PER_LEAF-1)*STATE_BITS + STATE_BITS - 1] & Xi[ta_chunk_base + TA_CHUNKS_PER_LEAF-1] & FILTER_HIERARCHICAL) != (ta_state[(TA_CHUNKS_PER_LEAF-1)*STATE_BITS + STATE_BITS - 1] & FILTER_HIERARCHICAL)) {
212
+ component_output = 0;
213
+ }
214
+
215
+ global_component_output[clause_component] = component_output;
216
+ }
217
+ }
218
+
219
+ __global__ void evaluate_or_groups(int *child_input, int *or_group_node_output, int number_of_or_group_nodes, int number_of_or_group_addends)
220
+ {
221
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
222
+ int stride = blockDim.x * gridDim.x;
223
+
224
+ // Add up the votes of each OR node child
225
+ for (int or_group_node = index; or_group_node < CLAUSES*number_of_or_group_nodes; or_group_node += stride) {
226
+ // Add OR addends
227
+ int or_group_vote_sum = 0;
228
+ for (int or_addend = 0; or_addend < number_of_or_group_addends; ++or_addend) {
229
+ // Aggregate votes from each child node through addition
230
+ or_group_vote_sum += child_input[or_group_node*number_of_or_group_addends + or_addend];
231
+ //if (child_input[or_group_node*number_of_or_group_addends + or_addend]) {
232
+ // or_group_vote_sum = 1;
233
+ //}
234
+ }
235
+
236
+ // Store or group vote sum as node output
237
+ or_group_node_output[or_group_node] = or_group_vote_sum;
238
+ }
239
+ }
240
+
241
+ __global__ void evaluate_and_groups(int *child_input, int *and_group_node_output, int number_of_and_group_nodes, int number_of_and_group_factors)
242
+ {
243
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
244
+ int stride = blockDim.x * gridDim.x;
245
+
246
+ // Multiply the votes from the children of each AND node
247
+ for (int and_group_node = index; and_group_node < CLAUSES*number_of_and_group_nodes; and_group_node += stride) {
248
+ // Multiply and factors
249
+ int and_group_vote_product = 1;
250
+ for (int and_factor = 0; and_factor < number_of_and_group_factors; ++and_factor) {
251
+ // Aggregate votes from each child node through multiplication
252
+ and_group_vote_product *= child_input[and_group_node*number_of_and_group_factors + and_factor];
253
+ }
254
+
255
+ // Store and group product as node output
256
+ and_group_node_output[and_group_node] = and_group_vote_product;
257
+ }
258
+ }
259
+
260
+ __global__ void propagate_and_group_false_truth_values(int *child_input, int *group_node_output, int number_of_group_nodes, int number_of_group_node_children)
261
+ {
262
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
263
+ int stride = blockDim.x * gridDim.x;
264
+
265
+ // If a group node is false, all children are made false.
266
+ for (int group_node = index; group_node < CLAUSES*number_of_group_nodes; group_node += stride) {
267
+ if (group_node_output[group_node] == 0) {
268
+ for (int and_factor = 0; and_factor < number_of_group_node_children; ++and_factor) {
269
+ if (child_input[group_node*number_of_group_node_children + and_factor] > 0) {
270
+ child_input[group_node*number_of_group_node_children + and_factor] = 0;
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+
277
+ __global__ void evaluate_or_alternatives(int *child_input, int *or_alternatives_node_output, int number_of_or_alternatives_nodes, int number_of_or_alternatives)
278
+ {
279
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
280
+ int stride = blockDim.x * gridDim.x;
281
+
282
+ // Add up the votes from the children of each OR node
283
+ for (int or_alternatives_node = index; or_alternatives_node < CLAUSES*number_of_or_alternatives_nodes; or_alternatives_node += stride) {
284
+ // Sum up votes from each or alternative
285
+ int or_alternatives_vote_sum = 0;
286
+ for (int or_alternative = 0; or_alternative < number_of_or_alternatives; ++or_alternative) {
287
+ // Aggregate same input or alternatives through summation
288
+ or_alternatives_vote_sum += child_input[or_alternatives_node * number_of_or_alternatives + or_alternative];
289
+
290
+ //if (child_input[or_alternatives_node * number_of_or_alternatives + or_alternative]) {
291
+ // or_alternatives_vote_sum = 1;
292
+ //}
293
+ }
294
+
295
+ // Store vote sum as node output
296
+ or_alternatives_node_output[or_alternatives_node] = or_alternatives_vote_sum;
297
+ }
298
+ }
299
+
300
+ __global__ void evaluate_final(int number_of_outputs, int *child_input, int *clause_weights, int *class_sum)
301
+ {
302
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
303
+ int stride = blockDim.x * gridDim.x;
304
+
305
+ // Add up the votes from each clause
306
+ for (int clause = index; clause < CLAUSES; clause += stride) {
307
+ if (child_input[clause]) {
308
+ for (int class_id = 0; class_id < number_of_outputs; ++class_id) {
309
+ int clause_weight = clause_weights[class_id*CLAUSES + clause];
310
+ atomicAdd(&class_sum[class_id], clause_weight * child_input[clause]);
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ // Update state of Tsetlin Automata team
317
+ __global__ void update_hierarchy(curandState *state, int number_of_outputs, unsigned int *global_ta_state, int *clause_weights, int *component_output, int depth, int *hierarchy_structure_factors, int *hierarchy_structure_alternatives, int *class_sum, int *X, int *y, int example)
318
+ {
319
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
320
+ int stride = blockDim.x * gridDim.x;
321
+
322
+ /* Copy state to local memory for efficiency */
323
+ curandState localState = state[index];
324
+
325
+ int *Xi = &X[(unsigned long long)example*LITERAL_CHUNKS];
326
+
327
+ // Calculate clause output first
328
+ for (int clause_component = index; clause_component < CLAUSES*COMPONENTS; clause_component += stride) {
329
+ int clause = clause_component / COMPONENTS;
330
+ int component = clause_component % COMPONENTS;
331
+
332
+ // Get state of current clause component
333
+ unsigned int *ta_state = &global_ta_state[clause_component*TA_CHUNKS_PER_LEAF*STATE_BITS];
334
+
335
+ int component_remainder = component;
336
+ int ta_chunk_base = 0;
337
+ int size = 1;
338
+ for (int d = 0; d < depth-1; ++d) {
339
+ int depth_d_node_index = component_remainder % hierarchy_structure_factors[d];
340
+ component_remainder = component_remainder / hierarchy_structure_factors[d];
341
+
342
+ if (hierarchy_structure_alternatives[d] == 0) {
343
+ ta_chunk_base += size * depth_d_node_index * TA_CHUNKS_PER_LEAF;
344
+ size *= hierarchy_structure_factors[d];
345
+ }
346
+ }
347
+
348
+ for (unsigned long long class_id = 0; class_id < number_of_outputs; ++class_id) {
349
+ int local_class_sum = class_sum[class_id];
350
+ if (local_class_sum > THRESHOLD) {
351
+ local_class_sum = THRESHOLD;
352
+ } else if (local_class_sum < -THRESHOLD) {
353
+ local_class_sum = -THRESHOLD;
354
+ }
355
+ update_component_hierarchy(&localState, number_of_outputs, &clause_weights[class_id*CLAUSES + clause], ta_state, component_output[clause_component], &Xi[ta_chunk_base], y[example*number_of_outputs + class_id], local_class_sum);
356
+ }
357
+ }
358
+
359
+ state[index] = localState;
360
+ }
361
+
362
+ // Update state of Tsetlin Automata team
363
+ __global__ void update_weights(curandState *state, int tm_type, int number_of_outputs, int *clause_weights, int *clause_output, int *class_sum, int *y, int example)
364
+ {
365
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
366
+ int stride = blockDim.x * gridDim.x;
367
+
368
+ /* Copy state to local memory for efficiency */
369
+ curandState localState = state[index];
370
+
371
+ for (unsigned long long clause = index; clause < CLAUSES; clause += stride) {
372
+ for (unsigned long long class_id = 0; class_id < number_of_outputs; ++class_id) {
373
+ int local_class_sum = class_sum[class_id];
374
+ if (local_class_sum > THRESHOLD) {
375
+ local_class_sum = THRESHOLD;
376
+ } else if (local_class_sum < -THRESHOLD) {
377
+ local_class_sum = -THRESHOLD;
378
+ }
379
+ update_clause_weight(&localState, tm_type, number_of_outputs, &clause_weights[class_id*CLAUSES + clause], clause_output[clause], y[example*number_of_outputs + class_id], local_class_sum);
380
+ }
381
+ }
382
+
383
+ state[index] = localState;
384
+ }
385
+ }
386
+ """
387
+
388
+ code_prepare = """
389
+ extern "C"
390
+ {
391
+ __global__ void prepare_weights(curandState *state, int tm_type, int number_of_outputs, int *clause_weights, int *class_sum)
392
+ {
393
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
394
+ int stride = blockDim.x * gridDim.x;
395
+
396
+ curandState localState = state[index];
397
+
398
+ for (unsigned long long clause = index; clause < CLAUSES; clause += stride) {
399
+ for (unsigned long long class_id = 0; class_id < number_of_outputs; ++class_id) {
400
+ #if NEGATIVE_CLAUSES == 1
401
+ if (tm_type == COALESCED_TM) {
402
+ clause_weights[class_id*CLAUSES + clause] = 1 - 2 * (curand(&localState) % 2);
403
+ } else {
404
+ if (class_id*CLAUSES <= clause && clause < (class_id+1)*CLAUSES) {
405
+ clause_weights[class_id*CLAUSES + clause] = 1 - 2 * (clause % 2);
406
+ } else {
407
+ clause_weights[class_id*CLAUSES + clause] = 0;
408
+ }
409
+ }
410
+ #else
411
+ clause_weights[class_id*CLAUSES + clause] = 1;
412
+ #endif
413
+ }
414
+ }
415
+
416
+ state[index] = localState;
417
+ }
418
+
419
+ __global__ void prepare_hierarchy(curandState *state, int number_of_outputs, unsigned int *global_ta_state, int *clause_weights, int *class_sum)
420
+ {
421
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
422
+ int stride = blockDim.x * gridDim.x;
423
+
424
+ curandState localState = state[index];
425
+
426
+ // Evaluate each clause component (leaf) in separate threads
427
+ for (int clause_component = index; clause_component < CLAUSES*COMPONENTS; clause_component += stride) {
428
+ // Get state of current clause component
429
+ unsigned int *ta_state = &global_ta_state[clause_component*TA_CHUNKS_PER_LEAF*STATE_BITS];
430
+ for (int ta_chunk = 0; ta_chunk < TA_CHUNKS_PER_LEAF; ++ta_chunk) {
431
+ for (int b = 0; b < STATE_BITS-1; ++b) {
432
+ ta_state[ta_chunk*STATE_BITS + b] = ~0;
433
+ }
434
+ ta_state[ta_chunk*STATE_BITS + STATE_BITS - 1] = 0;
435
+ }
436
+ }
437
+
438
+ for (unsigned long long clause = index; clause < CLAUSES; clause += stride) {
439
+ for (unsigned long long class_id = 0; class_id < number_of_outputs; ++class_id) {
440
+ #if NEGATIVE_CLAUSES == 1
441
+ clause_weights[class_id*CLAUSES + clause] = 1 - 2 * (curand(&localState) % 2);
442
+ #else
443
+ clause_weights[class_id*CLAUSES + clause] = 1;
444
+ #endif
445
+ }
446
+ }
447
+
448
+ state[index] = localState;
449
+ }
450
+ }
451
+ """
452
+
453
+ code_encode = """
454
+ #include <curand_kernel.h>
455
+
456
+ extern "C"
457
+ {
458
+ __global__ void prepare_encode_hierarchy(unsigned int *X, unsigned int *encoded_X, int number_of_literal_chunks, int number_of_examples)
459
+ {
460
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
461
+ int stride = blockDim.x * gridDim.x;
462
+
463
+ for (unsigned long long i = index; i < number_of_examples * number_of_literal_chunks; i += stride) {
464
+ encoded_X[i] = 0;
465
+ }
466
+ }
467
+
468
+ __global__ void encode_hierarchy(unsigned int *X, unsigned int *encoded_X, int number_of_features, int number_of_literal_chunks, int number_of_leaves, int number_of_features_per_leaf, int number_of_literal_chunks_per_leaf, int append_negated, int number_of_examples)
469
+ {
470
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
471
+ int stride = blockDim.x * gridDim.x;
472
+
473
+ unsigned int *Xi;
474
+ unsigned int *encoded_Xi;
475
+
476
+ for (unsigned long long i = index; i < number_of_examples; i += stride) {
477
+ Xi = &X[i*number_of_features];
478
+ encoded_Xi = &encoded_X[i*number_of_literal_chunks];
479
+
480
+ for (int j = 0; j < number_of_features / number_of_features_per_leaf; ++j) {
481
+ for (int k = 0; k < number_of_features_per_leaf; ++k) {
482
+ if (Xi[j*number_of_features_per_leaf + k] == 1) {
483
+ int leaf_chunk_nr = k / 32;
484
+ int leaf_chunk_pos = k % 32;
485
+ encoded_Xi[j*number_of_literal_chunks_per_leaf + leaf_chunk_nr] |= (1 << leaf_chunk_pos);
486
+ } else if (append_negated) {
487
+ int leaf_chunk_nr = (k + number_of_features_per_leaf) / 32;
488
+ int leaf_chunk_pos = (k + number_of_features_per_leaf) % 32;
489
+ encoded_Xi[j*number_of_literal_chunks_per_leaf + leaf_chunk_nr] |= (1 << leaf_chunk_pos);
490
+ }
491
+ }
492
+ }
493
+ }
494
+ }
495
+ }
496
+ """
497
+
498
+ code_transform = """
499
+ extern "C"
500
+ {
501
+ // Transform examples
502
+ __global__ void transform(unsigned int *global_ta_state, int *X, int *transformed_X)
503
+ {
504
+ int index = blockIdx.x * blockDim.x + threadIdx.x;
505
+ int stride = blockDim.x * gridDim.x;
506
+
507
+ // To be updated
508
+ }
509
+ }
510
+ """
@@ -0,0 +1,502 @@
1
+ # Copyright (c) 2026 Ole-Christoffer Granmo and the University of Agder
2
+
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ # This code implements the Convolutional Tsetlin Machine from paper arXiv:1905.09688
22
+ # https://arxiv.org/abs/1905.09688
23
+
24
+ import numpy as np
25
+
26
+ import PyHierarchicalTsetlinMachineCUDA.kernels as kernels
27
+
28
+ import pycuda.curandom as curandom
29
+ import pycuda.driver as cuda
30
+ import pycuda.autoinit
31
+ from pycuda.compiler import SourceModule
32
+
33
+ from time import time
34
+
35
+ OR_GROUP = "OR a group of children"
36
+ OR_ALTERNATIVES = "Create multiple OR alternatives for a sub hierarchy"
37
+ AND_GROUP = "AND a group of children"
38
+
39
+ VANILLA_TM = 0
40
+ WEIGHTED_TM = 1
41
+ COALESCED_TM = 2
42
+
43
+ g = curandom.XORWOWRandomNumberGenerator()
44
+
45
+ class CommonTsetlinMachine():
46
+
47
+ def __init__(self, number_of_clauses, T, s, tm_type=VANILLA_TM, q=1.0, hierarchy_structure=None, boost_true_positive_feedback=1, number_of_state_bits=8, append_negated=True, grid=(16*13,1,1), block=(128,1,1)):
48
+ self.number_of_clauses = number_of_clauses
49
+ self.number_of_state_bits = number_of_state_bits
50
+ self.T = int(T)
51
+ self.s = s
52
+ self.tm_type = tm_type
53
+ self.q = q
54
+ self.hierarchy_structure = hierarchy_structure
55
+ self.depth = len(hierarchy_structure)
56
+
57
+ self.boost_true_positive_feedback = boost_true_positive_feedback
58
+ self.append_negated = append_negated
59
+ self.grid = grid
60
+ self.block = block
61
+
62
+ # Calculates the number of nodes at each level of the hierarchy
63
+ self.hierarchy_size = [0] * (self.depth + 1)
64
+ self.hierarchy_size[self.depth] = 1
65
+ for d in range(self.depth - 1):
66
+ self.hierarchy_size[self.depth - d - 1] = self.hierarchy_structure[self.depth - d - 1][1] * self.hierarchy_size[self.depth - d]
67
+
68
+ # Represents hierarchy structure for transfer to GPU
69
+ self.hierarchy_structure_factors = [0] * (self.depth - 1)
70
+ self.hierarchy_structure_alternatives = [0] * (self.depth - 1)
71
+ for d in range(1, self.depth):
72
+ self.hierarchy_structure_factors[d-1] = self.hierarchy_structure[d][1]
73
+ if self.hierarchy_structure[d][0] == OR_ALTERNATIVES:
74
+ self.hierarchy_structure_alternatives[d-1] = 1
75
+
76
+ # Calculates total number of features spanned by the hierarchy
77
+ self.number_of_features_hierarchy = 1
78
+ for d in range(self.depth - 1, -1, -1):
79
+ if (self.hierarchy_structure[d][0] == OR_GROUP or self.hierarchy_structure[d][0] == AND_GROUP):
80
+ self.number_of_features_hierarchy *= self.hierarchy_structure[d][1]
81
+
82
+ # Calculates literal chunks per leaf
83
+ self.number_of_features_per_leaf = self.hierarchy_structure[0][1]
84
+ if self.append_negated:
85
+ self.number_of_literals = self.number_of_features_hierarchy * 2
86
+ self.number_of_literals_per_leaf = self.number_of_features_per_leaf * 2
87
+ self.number_of_literal_chunks_per_leaf = int((self.number_of_literals_per_leaf - 1) / 32 + 1)
88
+ else:
89
+ self.number_of_literals = self.number_of_features_hierarchy
90
+ self.number_of_literals_per_leaf = self.number_of_features_per_leaf
91
+ self.number_of_literal_chunks_per_leaf = int((self.number_of_literals_per_leaf - 1) / 32 + 1)
92
+
93
+ # Calculates the number of literal chunks for the full hierarchy
94
+ self.hierarchy_size[0] = self.number_of_literal_chunks_per_leaf * self.hierarchy_size[1]
95
+
96
+ # Calculates number of literal chunks overall for the feature vector (ignores OR alternatives)
97
+ self.number_of_literal_chunks = self.number_of_literal_chunks_per_leaf
98
+ for d in range(self.depth - 1, 0, -1):
99
+ if (self.hierarchy_structure[d][0] == OR_GROUP or self.hierarchy_structure[d][0] == AND_GROUP):
100
+ self.number_of_literal_chunks *= self.hierarchy_structure[d][1]
101
+
102
+ self.cuda_modules()
103
+
104
+ self.first = True
105
+
106
+ def cuda_modules(self):
107
+ parameters = """
108
+ #define CLAUSES %d
109
+ #define DEPTH %d
110
+ #define COMPONENTS %d
111
+ #define LITERALS_PER_LEAF %d
112
+ #define TA_CHUNKS_PER_LEAF %d
113
+ #define LITERAL_CHUNKS %d
114
+ #define STATE_BITS %d
115
+ #define BOOST_TRUE_POSITIVE_FEEDBACK %d
116
+ #define S %f
117
+ #define THRESHOLD %d
118
+ #define Q %f
119
+
120
+ #define NEGATIVE_CLAUSES %d
121
+ """ % (self.number_of_clauses, self.depth, self.hierarchy_size[1], self.number_of_literals_per_leaf, self.number_of_literal_chunks_per_leaf, self.number_of_literal_chunks, self.number_of_state_bits, self.boost_true_positive_feedback, self.s, self.T, self.q, self.negative_clauses)
122
+
123
+ mod_prepare = SourceModule(parameters + kernels.code_header + kernels.code_prepare, no_extern_c=True)
124
+ self.prepare_weights = mod_prepare.get_function("prepare_weights")
125
+ self.prepare_hierarchy = mod_prepare.get_function("prepare_hierarchy")
126
+
127
+ mod_update = SourceModule(parameters + kernels.code_header + kernels.code_update, no_extern_c=True)
128
+ self.update_hierarchy = mod_update.get_function("update_hierarchy")
129
+ self.update_hierarchy.prepare("PiPPPiPPPPPi")
130
+
131
+ self.update_weights = mod_update.get_function("update_weights")
132
+ self.update_weights.prepare("PiiPPPPi")
133
+
134
+ self.evaluate_leaves = mod_update.get_function("evaluate_leaves")
135
+ self.evaluate_leaves.prepare("PPPiPPPi")
136
+
137
+ self.evaluate_final = mod_update.get_function("evaluate_final")
138
+ self.evaluate_final.prepare("iPPP")
139
+
140
+ self.evaluate_and_groups = mod_update.get_function("evaluate_and_groups")
141
+ self.evaluate_and_groups.prepare("PPii")
142
+
143
+ self.propagate_and_group_false_truth_values = mod_update.get_function("propagate_and_group_false_truth_values")
144
+ self.propagate_and_group_false_truth_values.prepare("PPii")
145
+
146
+ self.evaluate_or_groups = mod_update.get_function("evaluate_or_groups")
147
+ self.evaluate_or_groups.prepare("PPii")
148
+
149
+ self.evaluate_or_alternatives = mod_update.get_function("evaluate_or_alternatives")
150
+ self.evaluate_or_alternatives.prepare("PPii")
151
+
152
+ # CUDA modules for encoding input data
153
+ mod_encode = SourceModule(kernels.code_encode, no_extern_c=True)
154
+ self.prepare_encode_hierarchy = mod_encode.get_function("prepare_encode_hierarchy")
155
+ self.encode_hierarchy = mod_encode.get_function("encode_hierarchy")
156
+
157
+
158
+ def encode_X(self, X, encoded_X_hierarchy_gpu):
159
+ number_of_examples = X.shape[0]
160
+
161
+ # Allocates GPU memory for input data
162
+ Xm = np.ascontiguousarray(X.flatten()).astype(np.uint32)
163
+ X_gpu = cuda.mem_alloc(Xm.nbytes)
164
+ cuda.memcpy_htod(X_gpu, Xm)
165
+
166
+ # Prepares for leaf encoding of the input data
167
+ self.prepare_encode_hierarchy(X_gpu, encoded_X_hierarchy_gpu, np.int32(self.number_of_literal_chunks), np.int32(number_of_examples), grid=self.grid, block=self.block)
168
+ cuda.Context.synchronize()
169
+
170
+ # Encodes the input data split across the leaves
171
+ self.encode_hierarchy(X_gpu, encoded_X_hierarchy_gpu, np.int32(self.number_of_features_hierarchy), np.int32(self.number_of_literal_chunks), np.int32(self.hierarchy_size[1]), np.int32(self.number_of_features_per_leaf), np.int32(self.number_of_literal_chunks_per_leaf), np.int32(self.append_negated), np.int32(number_of_examples), grid=self.grid, block=self.block)
172
+ cuda.Context.synchronize()
173
+
174
+ def allocate_gpu_memory(self):
175
+ # GPU memory for accumulating votes, level by level
176
+ self.hierarchy_votes = []
177
+ for d in range(1, self.depth):
178
+ self.hierarchy_votes.append(cuda.mem_alloc(self.number_of_clauses*int(self.hierarchy_size[d])*4))
179
+ self.hierarchy_votes.append(cuda.mem_alloc(self.number_of_clauses*4))
180
+
181
+ # GPU memory for storing hierarchy structure
182
+ self.hierarchy_structure_factors_gpu = cuda.mem_alloc((self.depth-1)*4)
183
+ cuda.memcpy_htod(self.hierarchy_structure_factors_gpu, np.array(self.hierarchy_structure_factors, dtype=np.int32))
184
+
185
+ # GPU memory for storing hierarchy structure
186
+ self.hierarchy_structure_alternatives_gpu = cuda.mem_alloc((self.depth-1)*4)
187
+ cuda.memcpy_htod(self.hierarchy_structure_alternatives_gpu, np.array(self.hierarchy_structure_alternatives, dtype=np.int32))
188
+
189
+ # GPU memory for storing Tsetlin Automata states
190
+ self.ta_state_hierarchy_gpu = cuda.mem_alloc(self.number_of_clauses*self.hierarchy_size[0]*self.number_of_state_bits*4)
191
+ self.clause_weights_gpu = cuda.mem_alloc(self.number_of_outputs*self.number_of_clauses*4)
192
+ self.component_weights_gpu = cuda.mem_alloc(self.number_of_clauses*self.hierarchy_size[1]*4) # Only positive weights...
193
+ self.class_sum_gpu = cuda.mem_alloc(self.number_of_outputs*4)
194
+
195
+ def ta_action(self, clause, leaf, ta):
196
+ ta_state_hierarchy = np.empty(self.number_of_clauses*self.hierarchy_size[1]*self.number_of_literal_chunks_per_leaf*self.number_of_state_bits, dtype=np.uint32)
197
+ cuda.memcpy_dtoh(ta_state_hierarchy, self.ta_state_hierarchy_gpu)
198
+ ta_state_hierarchy = self.ta_state_hierarchy.reshape((self.number_of_clauses, self.hierarchy_size[1], self.number_of_literal_chunks_per_leaf, self.number_of_state_bits))
199
+
200
+ return (ta_state[clause, leaf, ta // 32, self.number_of_state_bits-1] & (1 << (ta % 32))) > 0
201
+
202
+ def get_state(self):
203
+ # To be updated
204
+ ta_state_hierarchy = np.empty(self.number_of_clauses*self.hierarchy_size[1]*self.number_of_literal_chunks_per_leaf*self.number_of_state_bits, dtype=np.uint32)
205
+ cuda.memcpy_dtoh(self.ta_state_hierarchy, self.ta_state_hierarchy_gpu)
206
+ clause_weights = np.empty(self.number_of_outputs*self.number_of_clauses).astype(np.int32)
207
+ cuda.memcpy_dtoh(self.clause_weights, self.clause_weights_gpu)
208
+ return((self.ta_state_hierarchy, self.clause_weights, self.number_of_outputs, self.number_of_clauses, self.hierarchy_structure, self.boost_true_positive_feedback, self.number_of_state_bits, self.append_negated, self.min_y, self.max_y))
209
+
210
+ def set_state(self, state):
211
+ # To be updated
212
+ self.number_of_outputs = state[2]
213
+ self.number_of_clauses = state[3]
214
+ self.hierarchy_structure = state[4]
215
+ self.boost_true_positive_feedback = state[5]
216
+ self.number_of_state_bits = state[6]
217
+ self.append_negated = state[7]
218
+ self.min_y = state[8]
219
+ self.max_y = state[9]
220
+
221
+ self.ta_state_hierarchy_gpu = cuda.mem_alloc(self.number_of_clauses*self.hierarchy_size[0]*self.number_of_state_bits*4)
222
+ self.clause_weights_gpu = cuda.mem_alloc(self.number_of_outputs*self.number_of_clauses*4)
223
+ cuda.memcpy_htod(self.ta_state_hierarchy_gpu, state[0])
224
+ cuda.memcpy_htod(self.clause_weights_gpu, state[1])
225
+
226
+ # Transform input data for processing at next layer
227
+ def transform(self, X):
228
+ None # To be updated
229
+
230
+ def initialize_weights_and_ta_states(self):
231
+ self.prepare_weights(g.state, np.int32(self.tm_type), np.int32(self.number_of_outputs), self.clause_weights_gpu, self.class_sum_gpu, grid=self.grid, block=self.block)
232
+ cuda.Context.synchronize()
233
+
234
+ self.prepare_hierarchy(g.state, np.int32(self.number_of_outputs), self.ta_state_hierarchy_gpu, self.clause_weights_gpu, self.class_sum_gpu, grid=self.grid, block=self.block)
235
+ cuda.Context.synchronize()
236
+
237
+ def evaluate_hierarchy(self, encoded_X_hierarchy, e):
238
+ # Initializes class sums to zero
239
+ class_sum = np.ascontiguousarray(np.zeros(self.number_of_outputs)).astype(np.int32)
240
+ cuda.memcpy_htod(self.class_sum_gpu, class_sum)
241
+
242
+ # Evaluates all the hierarchy leaves in parallel
243
+ self.evaluate_leaves.prepared_call(
244
+ self.grid,
245
+ self.block,
246
+ self.ta_state_hierarchy_gpu,
247
+ self.component_weights_gpu,
248
+ self.hierarchy_votes[0],
249
+ self.depth,
250
+ self.hierarchy_structure_factors_gpu,
251
+ self.hierarchy_structure_alternatives_gpu,
252
+ encoded_X_hierarchy,
253
+ np.int32(e)
254
+ )
255
+ cuda.Context.synchronize()
256
+
257
+ # Propagates votes bottom-up in the hierarchy, starting from the clause components (leaves)
258
+ for d in range(1, self.depth):
259
+ if (self.hierarchy_structure[d][0] == AND_GROUP):
260
+ self.evaluate_and_groups.prepared_call(
261
+ self.grid,
262
+ self.block,
263
+ self.hierarchy_votes[d-1],
264
+ self.hierarchy_votes[d],
265
+ self.hierarchy_size[d + 1],
266
+ self.hierarchy_structure[d][1]
267
+ )
268
+ cuda.Context.synchronize()
269
+ elif self.hierarchy_structure[d][0] == OR_GROUP:
270
+ self.evaluate_or_groups.prepared_call(
271
+ self.grid,
272
+ self.block,
273
+ self.hierarchy_votes[d-1],
274
+ self.hierarchy_votes[d],
275
+ self.hierarchy_size[d + 1],
276
+ self.hierarchy_structure[d][1]
277
+ )
278
+ cuda.Context.synchronize()
279
+ elif self.hierarchy_structure[d][0] == OR_ALTERNATIVES:
280
+ self.evaluate_or_alternatives.prepared_call(
281
+ self.grid,
282
+ self.block,
283
+ self.hierarchy_votes[d-1],
284
+ self.hierarchy_votes[d],
285
+ self.hierarchy_size[d + 1],
286
+ self.hierarchy_structure[d][1]
287
+ )
288
+ cuda.Context.synchronize()
289
+ else:
290
+ printf("Unknown node type!")
291
+ sys.exit()
292
+
293
+ # Adds up the votes from each clause (hierarchy root)
294
+ self.evaluate_final.prepared_call(
295
+ self.grid,
296
+ self.block,
297
+ np.int32(self.number_of_outputs),
298
+ self.hierarchy_votes[self.depth-1],
299
+ self.clause_weights_gpu,
300
+ self.class_sum_gpu
301
+ )
302
+ cuda.Context.synchronize()
303
+
304
+ def _fit(self, X, encoded_Y, epochs=100, incremental=False):
305
+ number_of_examples = X.shape[0]
306
+
307
+ if self.first:
308
+ # Allocates memory and prepares weights and Tsetlin automata states on first run
309
+ self.allocate_gpu_memory()
310
+
311
+ self.initialize_weights_and_ta_states()
312
+
313
+ self.first = False
314
+ elif not incremental:
315
+ # Re-initializes weights and Tsetlin automata states if training is not incremental
316
+ self.initialize_weights_and_ta_states()
317
+
318
+ # Allocates GPU memory for training data
319
+ encoded_X_hierarchy_training_gpu = cuda.mem_alloc(int(number_of_examples * self.number_of_literal_chunks * 4))
320
+ Y_gpu = cuda.mem_alloc(encoded_Y.nbytes)
321
+
322
+ self.encode_X(X, encoded_X_hierarchy_training_gpu)
323
+ cuda.memcpy_htod(Y_gpu, encoded_Y)
324
+
325
+ for epoch in range(epochs):
326
+ for e in range(number_of_examples):
327
+ self.evaluate_hierarchy(encoded_X_hierarchy_training_gpu, e)
328
+
329
+ # Propagates the root value and any intermittent node values back to the leaves.
330
+ # The purpose is to determine which leaves only has True nodes on the path from leaf to root.
331
+ for d in range(self.depth-1, 0, -1):
332
+ self.propagate_and_group_false_truth_values.prepared_call(
333
+ self.grid,
334
+ self.block,
335
+ self.hierarchy_votes[d-1],
336
+ self.hierarchy_votes[d],
337
+ self.hierarchy_size[d + 1],
338
+ self.hierarchy_structure[d][1]
339
+ )
340
+ cuda.Context.synchronize()
341
+
342
+ # Updates the clause components (leaves) based on the propagated truth values
343
+ self.update_hierarchy.prepared_call(
344
+ self.grid,
345
+ self.block,
346
+ g.state,
347
+ np.int32(self.number_of_outputs),
348
+ self.ta_state_hierarchy_gpu,
349
+ self.clause_weights_gpu,
350
+ self.hierarchy_votes[0],
351
+ self.depth,
352
+ self.hierarchy_structure_factors_gpu,
353
+ self.hierarchy_structure_alternatives_gpu,
354
+ self.class_sum_gpu,
355
+ encoded_X_hierarchy_training_gpu,
356
+ Y_gpu,
357
+ np.int32(e)
358
+ )
359
+ cuda.Context.synchronize()
360
+
361
+ # Updates the clause weights
362
+ if (self.tm_type in [WEIGHTED_TM, COALESCED_TM]):
363
+ self.update_weights.prepared_call(
364
+ self.grid,
365
+ self.block,
366
+ g.state,
367
+ np.int32(self.tm_type),
368
+ np.int32(self.number_of_outputs),
369
+ self.clause_weights_gpu,
370
+ self.hierarchy_votes[self.depth-1],
371
+ self.class_sum_gpu,
372
+ Y_gpu,
373
+ np.int32(e)
374
+ )
375
+ cuda.Context.synchronize()
376
+ return
377
+
378
+ def _score(self, X):
379
+ number_of_examples = X.shape[0]
380
+
381
+ encoded_X_hierarchy_test_gpu = cuda.mem_alloc(int(number_of_examples * self.number_of_literal_chunks * 4))
382
+ self.encode_X(X, encoded_X_hierarchy_test_gpu)
383
+
384
+ class_sum = np.ascontiguousarray(np.zeros((self.number_of_outputs, number_of_examples))).astype(np.int32)
385
+ class_sum_example = np.ascontiguousarray(np.zeros(self.number_of_outputs)).astype(np.int32)
386
+
387
+ for e in range(number_of_examples):
388
+ self.evaluate_hierarchy(encoded_X_hierarchy_test_gpu, e)
389
+
390
+ cuda.memcpy_dtoh(class_sum_example, self.class_sum_gpu)
391
+ class_sum[:, e] = class_sum_example
392
+
393
+ class_sum = np.clip(class_sum.reshape((self.number_of_outputs, number_of_examples)), -self.T, self.T)
394
+
395
+ return class_sum
396
+
397
+ class MultiOutputTsetlinMachine(CommonTsetlinMachine):
398
+ def __init__(self, number_of_clauses, T, s, q=1.0, boost_true_positive_feedback=1, number_of_state_bits=8, append_negated=True, grid=(16*13,1,1), block=(128,1,1)):
399
+ self.negative_clauses = 1
400
+ super().__init__(number_of_clauses, T, s, q=q, boost_true_positive_feedback=boost_true_positive_feedback, number_of_state_bits=number_of_state_bits, append_negated=append_negated, grid=grid, block=block)
401
+
402
+ def fit(self, X, Y, epochs=100, incremental=False):
403
+ X = X.reshape(X.shape[0], X.shape[1], 1)
404
+
405
+ self.number_of_outputs = Y.shape[1]
406
+ self.patch_dim = (X.shape[1], 1, 1)
407
+
408
+ self.max_y = None
409
+ self.min_y = None
410
+
411
+ encoded_Y = np.where(Y == 1, self.T, -self.T).astype(np.int32)
412
+ self._fit(X, encoded_Y, epochs = epochs, incremental = incremental)
413
+
414
+ return
415
+
416
+ def score(self, X):
417
+ X = X.reshape(X.shape[0], X.shape[1], 1)
418
+ return self._score(X)
419
+
420
+ def predict(self, X):
421
+ return (self.score(X) >= 0).astype(np.uint32).transpose()
422
+
423
+ class MultiClassTsetlinMachine(CommonTsetlinMachine):
424
+ def __init__(self, number_of_clauses, T, s, tm_type=VANILLA_TM, hierarchy_structure=((AND_GROUP, 1)), q=1.0, boost_true_positive_feedback=1, number_of_state_bits=8, append_negated=True, grid=(16*13,1,1), block=(128,1,1)):
425
+ self.negative_clauses = 1
426
+ super().__init__(number_of_clauses, T, s, tm_type=tm_type, hierarchy_structure=hierarchy_structure, q=q, boost_true_positive_feedback=boost_true_positive_feedback, number_of_state_bits=number_of_state_bits, append_negated=append_negated, grid=grid, block=block)
427
+
428
+ def fit(self, X, Y, epochs=100, incremental=False):
429
+ X = X.reshape(X.shape[0], X.shape[1], 1)
430
+
431
+ self.number_of_outputs = int(np.max(Y) + 1)
432
+ self.patch_dim = (X.shape[1], 1, 1)
433
+
434
+ self.max_y = None
435
+ self.min_y = None
436
+
437
+ encoded_Y = np.empty((Y.shape[0], self.number_of_outputs), dtype = np.int32)
438
+ for i in range(self.number_of_outputs):
439
+ encoded_Y[:,i] = np.where(Y == i, self.T, -self.T)
440
+
441
+ self._fit(X, encoded_Y, epochs = epochs, incremental = incremental)
442
+
443
+ return
444
+
445
+ def score(self, X):
446
+ X = X.reshape(X.shape[0], X.shape[1], 1)
447
+ return self._score(X)
448
+
449
+ def predict(self, X):
450
+ return np.argmax(self.score(X), axis=0)
451
+
452
+ class TsetlinMachine(CommonTsetlinMachine):
453
+ def __init__(self, number_of_clauses, T, s, q=1.0, boost_true_positive_feedback=1, number_of_state_bits=8, append_negated=True, grid=(16*13,1,1), block=(128,1,1)):
454
+ self.negative_clauses = 1
455
+ super().__init__(number_of_clauses, T, s, q=q, boost_true_positive_feedback=boost_true_positive_feedback, number_of_state_bits=number_of_state_bits, append_negated=append_negated, grid=grid, block=block)
456
+
457
+ def fit(self, X, Y, epochs=100, incremental=False):
458
+ X = X.reshape(X.shape[0], X.shape[1], 1)
459
+
460
+ self.number_of_outputs = 1
461
+ self.patch_dim = (X.shape[1], 1, 1)
462
+
463
+ self.max_y = None
464
+ self.min_y = None
465
+
466
+ encoded_Y = np.where(Y == 1, self.T, -self.T).astype(np.int32)
467
+
468
+ self._fit(X, encoded_Y, epochs = epochs, incremental = incremental)
469
+
470
+ return
471
+
472
+ def score(self, X):
473
+ X = X.reshape(X.shape[0], X.shape[1], 1)
474
+ return self._score(X)[0,:]
475
+
476
+ def predict(self, X):
477
+ return int(self.score(X) >= 0)
478
+
479
+ class RegressionTsetlinMachine(CommonTsetlinMachine):
480
+ def __init__(self, number_of_clauses, T, s, boost_true_positive_feedback=1, number_of_state_bits=8, append_negated=True, grid=(16*13,1,1), block=(128,1,1)):
481
+ self.negative_clauses = 0
482
+ super().__init__(number_of_clauses, T, s, boost_true_positive_feedback=boost_true_positive_feedback, number_of_state_bits=number_of_state_bits, append_negated=append_negated, grid=grid, block=block)
483
+
484
+ def fit(self, X, Y, epochs=100, incremental=False):
485
+ X = X.reshape(X.shape[0], X.shape[1], 1)
486
+
487
+ self.number_of_outputs = 1
488
+ self.patch_dim = (X.shape[1], 1, 1)
489
+
490
+ self.max_y = np.max(Y)
491
+ self.min_y = np.min(Y)
492
+
493
+ encoded_Y = ((Y - self.min_y)/(self.max_y - self.min_y)*self.T).astype(np.int32)
494
+
495
+ self._fit(X, encoded_Y, epochs = epochs, incremental = incremental)
496
+
497
+ return
498
+
499
+ def predict(self, X):
500
+ X = X.reshape(X.shape[0], X.shape[1], 1)
501
+
502
+ return 1.0*(self._score(X)[0,:])*(self.max_y - self.min_y)/(self.T) + self.min_y
@@ -0,0 +1,43 @@
1
+ import numpy as np
2
+
3
+ from sklearn.preprocessing import FunctionTransformer
4
+ from sklearn.preprocessing import KBinsDiscretizer
5
+
6
+ import warnings
7
+ warnings.filterwarnings("ignore", category=UserWarning)
8
+
9
+ # From https://github.com/WojciechMigda/Tsetlini/blob/main/lib/examples/california-housing/src/produce_dataset_alt.py
10
+
11
+ def _as_bits(x, nbits):
12
+ s = '1' * x + '0' * (nbits - x)
13
+ return np.array([int(c) for c in s])
14
+
15
+ def _unpack_bits(a, nbits):
16
+ if len(a.shape) > 2:
17
+ raise ValueError("_unpack_bits: input array cannot have more than 2 dimensions, got {}".format(len(a.shape)))
18
+
19
+ a = np.clip(a, 0, nbits)
20
+ a_ = np.empty_like(a, dtype=np.uint64)
21
+ np.rint(a, out=a_, casting='unsafe')
22
+ F = np.frompyfunc(_as_bits, 2, 1)
23
+ rv = np.stack(F(a_.ravel(), nbits)).reshape(a.shape[0], -1)
24
+ return rv
25
+
26
+ class Booleanizer:
27
+ def __init__(self, max_bits_per_feature = 25):
28
+ self.max_bits_per_feature = max_bits_per_feature
29
+
30
+ self.kbd = KBinsDiscretizer(n_bins=max_bits_per_feature+1, encode='ordinal', strategy='quantile')
31
+
32
+ return
33
+
34
+ def fit(self, X):
35
+ self.kbd_fitted = self.kbd.fit(X)
36
+
37
+ return
38
+
39
+ def transform(self, X):
40
+ X_transformed = self.kbd_fitted.transform(X).astype(int)
41
+
42
+ pre = FunctionTransformer(_unpack_bits, validate=False, kw_args={'nbits': self.max_bits_per_feature})
43
+ return pre.fit_transform(X_transformed)
@@ -0,0 +1,33 @@
1
+ # PyHierarchicalTsetlinMachineCUDA
2
+ Implements the Hierarchical Tsetlin Machine in CUDA
3
+
4
+ ## Installation
5
+
6
+ ```bash
7
+ python ./setup.py sdist
8
+ pip install dist/PyHierarchicalTsetlinMachineCUDA-0.2.0.tar.gz
9
+ ```
10
+
11
+ ## Licence
12
+
13
+ MIT License
14
+
15
+ Copyright (c) 2026 Ole-Christoffer Granmo and the University of Agder
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the "Software"), to deal
19
+ in the Software without restriction, including without limitation the rights
20
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ copies of the Software, and to permit persons to whom the Software is
22
+ furnished to do so, subject to the following conditions:
23
+
24
+ The above copyright notice and this permission notice shall be included in all
25
+ copies or substantial portions of the Software.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyhierarchicaltsetlinmachinecuda
3
+ Version: 0.2.0
4
+ Summary: Hierarchical Tsetlin Machine Architecture.
5
+ Home-page: https://github.com/cair/PyHierarchicalTsetlinMachineCUDA/
6
+ Author: Ole-Christoffer Granmo
7
+ Author-email: ole.granmo@uia.no
8
+ License: MIT
9
+ Keywords: pattern-recognition cuda machine-learning interpretable-machine-learning rule-based-machine-learning propositional-logic tsetlin-machine regression convolution classification multi-layer
10
+ License-File: LICENSE
11
+
12
+ Hierarchical Tsetlin Machine Architecture.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ PyHierarchicalTsetlinMachineCUDA/__init__.py
5
+ PyHierarchicalTsetlinMachineCUDA/graph.py
6
+ PyHierarchicalTsetlinMachineCUDA/kernels.py
7
+ PyHierarchicalTsetlinMachineCUDA/tm.py
8
+ PyHierarchicalTsetlinMachineCUDA/tools.py
9
+ pyhierarchicaltsetlinmachinecuda.egg-info/PKG-INFO
10
+ pyhierarchicaltsetlinmachinecuda.egg-info/SOURCES.txt
11
+ pyhierarchicaltsetlinmachinecuda.egg-info/dependency_links.txt
12
+ pyhierarchicaltsetlinmachinecuda.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ from setuptools import *
2
+
3
+ setup(
4
+ name='pyhierarchicaltsetlinmachinecuda',
5
+ version='0.2.0',
6
+ author='Ole-Christoffer Granmo',
7
+ author_email='ole.granmo@uia.no',
8
+ url='https://github.com/cair/PyHierarchicalTsetlinMachineCUDA/',
9
+ license='MIT',
10
+ description='Hierarchical Tsetlin Machine Architecture.',
11
+ long_description='Hierarchical Tsetlin Machine Architecture.',
12
+ keywords ='pattern-recognition cuda machine-learning interpretable-machine-learning rule-based-machine-learning propositional-logic tsetlin-machine regression convolution classification multi-layer',
13
+ packages=['PyHierarchicalTsetlinMachineCUDA']
14
+ )