rulelearn-ripper-interactive 0.2.3__py3-none-any.whl
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.
- rulelearn_ri/__init__.py +6 -0
- rulelearn_ri/algorithms/__init__.py +0 -0
- rulelearn_ri/algorithms/r2n/__init__.py +0 -0
- rulelearn_ri/algorithms/r2n/base.py +28 -0
- rulelearn_ri/algorithms/r2n/functions.py +25 -0
- rulelearn_ri/algorithms/r2n/mask.py +58 -0
- rulelearn_ri/algorithms/r2n/preprocessing_layers.py +192 -0
- rulelearn_ri/algorithms/r2n/r2n_algo.py +287 -0
- rulelearn_ri/algorithms/r2n/simple_rulenet.py +75 -0
- rulelearn_ri/algorithms/r2n/training.py +34 -0
- rulelearn_ri/algorithms/r2n/utilities.py +117 -0
- rulelearn_ri/algorithms/rbm/__init__.py +0 -0
- rulelearn_ri/algorithms/rbm/boolean_rule_cg.py +65 -0
- rulelearn_ri/algorithms/rbm/rbm/BRCG.py +69 -0
- rulelearn_ri/algorithms/rbm/rbm/GLRM.py +98 -0
- rulelearn_ri/algorithms/rbm/rbm/__init__.py +6 -0
- rulelearn_ri/algorithms/rbm/rbm/beam_search.py +302 -0
- rulelearn_ri/algorithms/rbm/rbm/boolean_rule_cg.py +245 -0
- rulelearn_ri/algorithms/rbm/rbm/features.py +544 -0
- rulelearn_ri/algorithms/rbm/rbm/linear_regression.py +393 -0
- rulelearn_ri/algorithms/rbm/rbm/logistic_regression.py +447 -0
- rulelearn_ri/algorithms/rbm/utils.py +13 -0
- rulelearn_ri/algorithms/ripper/__init__.py +10 -0
- rulelearn_ri/algorithms/ripper/base.py +81 -0
- rulelearn_ri/algorithms/ripper/binding.py +541 -0
- rulelearn_ri/algorithms/ripper/mdl.py +97 -0
- rulelearn_ri/algorithms/ripper/pruning.py +212 -0
- rulelearn_ri/algorithms/ripper/ripper.py +600 -0
- rulelearn_ri/algorithms/ripper/ripper_ruleset_generator.py +26 -0
- rulelearn_ri/algorithms/ripper/single_rule.py +425 -0
- rulelearn_ri/dise.py +64 -0
- rulelearn_ri/trxf/__init__.py +0 -0
- rulelearn_ri/trxf/classifier/__init__.py +0 -0
- rulelearn_ri/trxf/classifier/ruleset_classifier.py +153 -0
- rulelearn_ri/trxf/core/__init__.py +5 -0
- rulelearn_ri/trxf/core/boolean_evaluator.py +15 -0
- rulelearn_ri/trxf/core/conjunction.py +50 -0
- rulelearn_ri/trxf/core/dnf_ruleset.py +64 -0
- rulelearn_ri/trxf/core/feature.py +155 -0
- rulelearn_ri/trxf/core/predicate.py +98 -0
- rulelearn_ri/trxf/core/ruleset_generator.py +25 -0
- rulelearn_ri/trxf/core/utils.py +40 -0
- rulelearn_ri/trxf/metrics.py +98 -0
- rulelearn_ri/trxf/pmml_export/__init__.py +4 -0
- rulelearn_ri/trxf/pmml_export/__version__.py +1 -0
- rulelearn_ri/trxf/pmml_export/models/__init__.py +24 -0
- rulelearn_ri/trxf/pmml_export/models/attribute.py +24 -0
- rulelearn_ri/trxf/pmml_export/models/characteristics.py +20 -0
- rulelearn_ri/trxf/pmml_export/models/data_dictionary.py +53 -0
- rulelearn_ri/trxf/pmml_export/models/mining_schema.py +25 -0
- rulelearn_ri/trxf/pmml_export/models/pmml_ruleset_model.py +16 -0
- rulelearn_ri/trxf/pmml_export/models/predicate.py +42 -0
- rulelearn_ri/trxf/pmml_export/models/rule.py +26 -0
- rulelearn_ri/trxf/pmml_export/models/ruleset.py +28 -0
- rulelearn_ri/trxf/pmml_export/models/ruleset_model.py +16 -0
- rulelearn_ri/trxf/pmml_export/models/scorecard.py +35 -0
- rulelearn_ri/trxf/pmml_export/pmml_exporter.py +18 -0
- rulelearn_ri/trxf/pmml_export/reader/__init__.py +2 -0
- rulelearn_ri/trxf/pmml_export/reader/abstract_reader.py +16 -0
- rulelearn_ri/trxf/pmml_export/reader/trxf_reader.py +113 -0
- rulelearn_ri/trxf/pmml_export/serializer/__init__.py +2 -0
- rulelearn_ri/trxf/pmml_export/serializer/abstract_serializer.py +11 -0
- rulelearn_ri/trxf/pmml_export/serializer/nyoka_serializer.py +162 -0
- rulelearn_ri/trxf/scorecard/__init__.py +3 -0
- rulelearn_ri/trxf/scorecard/bins.py +253 -0
- rulelearn_ri/trxf/scorecard/partition.py +71 -0
- rulelearn_ri/trxf/scorecard/scorecard.py +78 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/METADATA +164 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/RECORD +73 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/WHEEL +5 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/licenses/LICENSE +201 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/licenses/NOTICE +11 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/top_level.txt +1 -0
rulelearn_ri/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from torch import nn
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RuleNetwork(nn.Module, ABC):
|
|
7
|
+
"""
|
|
8
|
+
Encapsulates a neural network that corresponds syntactically to some ruleset.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def extract_ruleset(self):
|
|
13
|
+
""" Extract a symbolic representation of the learned ruleset. The semantics of the ruleset must be provided
|
|
14
|
+
externally.
|
|
15
|
+
|
|
16
|
+
:return: A 2D list representing a ruleset in Disjunctive Normal Form. An inner list takes values from {-1, 0, 1}
|
|
17
|
+
representing a single rule, indicating the inclusion of the negated condition, exclusion,
|
|
18
|
+
and inclusion of the condition, respectively.
|
|
19
|
+
E.g. [[1, 0, 0, -1], [0, 1, 0, 0]] represents the ruleset
|
|
20
|
+
"(condition0 ^ not condition3) v condition1" for a rule network with 4 input conditions.
|
|
21
|
+
"""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PredicateLearningLayer(nn.Module, ABC):
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def get_predicates(self, *args, **kwargs):
|
|
28
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DifferentiableHeaviside(torch.autograd.Function):
|
|
5
|
+
@staticmethod
|
|
6
|
+
def forward(ctx, input):
|
|
7
|
+
ctx.save_for_backward(input)
|
|
8
|
+
return torch.heaviside(input, torch.ones_like(input))
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def backward(ctx, grad_output):
|
|
12
|
+
input, = ctx.saved_tensors
|
|
13
|
+
return grad_output * torch.sigmoid(input) * (1 - torch.sigmoid(input))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DifferentiableClamp(torch.autograd.Function):
|
|
17
|
+
@staticmethod
|
|
18
|
+
def forward(ctx, input):
|
|
19
|
+
ctx.save_for_backward(input)
|
|
20
|
+
return torch.clamp(input, max=1)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def backward(ctx, grad_output):
|
|
24
|
+
input, = ctx.saved_tensors
|
|
25
|
+
return grad_output
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
from torch import nn
|
|
5
|
+
|
|
6
|
+
from rulelearn_ri.algorithms.r2n.functions import DifferentiableHeaviside
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WeightMask(nn.Module, ABC):
|
|
10
|
+
"""
|
|
11
|
+
Abstract class for the mask that is applied to the weight matrix of the NN.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, dim_out, dim_in):
|
|
15
|
+
super(WeightMask, self).__init__()
|
|
16
|
+
self.dim_out = dim_out
|
|
17
|
+
self.dim_in = dim_in
|
|
18
|
+
self.loc = torch.nn.Parameter(torch.randn(self.dim_out, self.dim_in), requires_grad=True)
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def forward(self):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SimpleWeightMask(WeightMask):
|
|
26
|
+
"""
|
|
27
|
+
Binary weight mask that is used in the AND/OR layer. Note that depening of the Boolean value cooling, we either implement a cooled sigmoid or stricly binary differentiable heavyside function.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, dim_out, dim_in, temp, cooling):
|
|
31
|
+
super(SimpleWeightMask, self).__init__(dim_out, dim_in)
|
|
32
|
+
self.cooling = cooling
|
|
33
|
+
self.temp = temp
|
|
34
|
+
|
|
35
|
+
def forward(self):
|
|
36
|
+
if self.cooling is True:
|
|
37
|
+
return torch.sigmoid((1 / self.temp) * self.loc)
|
|
38
|
+
else:
|
|
39
|
+
return DifferentiableHeaviside.apply(self.loc)
|
|
40
|
+
|
|
41
|
+
def set_temp(self, temp):
|
|
42
|
+
self.temp = temp
|
|
43
|
+
|
|
44
|
+
def get_temp(self):
|
|
45
|
+
return self.temp
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DeterministicWeightMask(WeightMask):
|
|
49
|
+
"""
|
|
50
|
+
Simple deterministic weight mask.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, mask):
|
|
54
|
+
super(DeterministicWeightMask, self).__init__(mask.size()[0], mask.size()[1])
|
|
55
|
+
self.mask = mask
|
|
56
|
+
|
|
57
|
+
def forward(self):
|
|
58
|
+
return self.mask
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import itertools
|
|
3
|
+
import operator
|
|
4
|
+
from copy import copy
|
|
5
|
+
from functools import reduce
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
from torch import nn
|
|
10
|
+
|
|
11
|
+
import rulelearn_ri.algorithms.r2n.mask as wm
|
|
12
|
+
from rulelearn_ri.algorithms.r2n.base import PredicateLearningLayer
|
|
13
|
+
from rulelearn_ri.trxf.core import Feature, Predicate, Relation
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NegationLayer(nn.Module):
|
|
17
|
+
"""
|
|
18
|
+
Augment the input layer with its negation to explicitly provide negated conditions to the downstream rule net.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super(NegationLayer, self).__init__()
|
|
23
|
+
|
|
24
|
+
def forward(self, x):
|
|
25
|
+
return torch.cat((x, torch.ones_like(x) - x), 1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _normalize_weights(bias, weights):
|
|
29
|
+
max_coef = np.max(np.abs(weights), axis=1)
|
|
30
|
+
normbias = np.divide(bias, max_coef)
|
|
31
|
+
max_coef = np.tile(max_coef, (weights.shape[1], 1)).T
|
|
32
|
+
normweights = np.divide(weights, max_coef)
|
|
33
|
+
return normbias, normweights
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LinearFeatureLayer(PredicateLearningLayer):
|
|
37
|
+
r"""
|
|
38
|
+
This layer takes as input a set of input featues and creates a linear combinations thereof to constuct a predicate in the form \sum w x - b > 0
|
|
39
|
+
where w and b are the weights and biases of the neural network layer.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, dim_in, dim_out, init_temp=1):
|
|
43
|
+
super(LinearFeatureLayer, self).__init__()
|
|
44
|
+
torch.manual_seed(0)
|
|
45
|
+
self.layer = nn.Linear(dim_in, dim_out)
|
|
46
|
+
self.mask = wm.DeterministicWeightMask(torch.ones(dim_out, dim_in))
|
|
47
|
+
self.temp = init_temp
|
|
48
|
+
self.dim_out = dim_out
|
|
49
|
+
|
|
50
|
+
def forward(self, x):
|
|
51
|
+
z = self.mask()
|
|
52
|
+
regularized_weights = z * self.layer.weight
|
|
53
|
+
self.layer.weight.data = regularized_weights
|
|
54
|
+
return torch.sigmoid((1 / self.temp) * self.layer(x))
|
|
55
|
+
|
|
56
|
+
def get_penalty(self):
|
|
57
|
+
# return self.mask().mean()
|
|
58
|
+
return self.layer.weight.norm(1)
|
|
59
|
+
|
|
60
|
+
def update_temp(self, temp):
|
|
61
|
+
self.temp = temp
|
|
62
|
+
|
|
63
|
+
def get_temp(self):
|
|
64
|
+
return self.temp
|
|
65
|
+
|
|
66
|
+
def get_predicates(self, column_names, normalizer, thr, name_format='generic'):
|
|
67
|
+
weight = copy(self.layer.weight.detach().numpy())
|
|
68
|
+
bias = copy(self.layer.bias.detach().numpy())
|
|
69
|
+
assert len(column_names) == weight.shape[1], 'column_names: {} but weight dim_in: {}'.format(column_names,
|
|
70
|
+
weight.shape[1])
|
|
71
|
+
norm_bias, norm_weights = _normalize_weights(bias, weight)
|
|
72
|
+
weight[abs(norm_weights) < thr] = 0
|
|
73
|
+
bias[abs(norm_bias) < thr] = 0
|
|
74
|
+
if normalizer:
|
|
75
|
+
weight = normalizer.transform(weight)
|
|
76
|
+
|
|
77
|
+
features = []
|
|
78
|
+
for w, b in zip(weight, bias):
|
|
79
|
+
terms = [x for x in zip(w, column_names) if abs(x[0]) > 0]
|
|
80
|
+
feature = ' + '.join(list(map(lambda term: str(term[0]) + '*' + str(term[1]), terms)))
|
|
81
|
+
feature += ' + ' + str(b)
|
|
82
|
+
features.append(feature.replace('+ -', '- '))
|
|
83
|
+
|
|
84
|
+
predicates = list(map(lambda expression: Predicate(Feature(expression), Relation.GE, 0), features))
|
|
85
|
+
return predicates
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class BasisExpansionLayer(nn.Module):
|
|
89
|
+
def __init__(self, basis_functions, application_list=None):
|
|
90
|
+
super().__init__()
|
|
91
|
+
self.functions = basis_functions + [lambda x: x]
|
|
92
|
+
self.application_list = application_list
|
|
93
|
+
|
|
94
|
+
def forward(self, x):
|
|
95
|
+
result = []
|
|
96
|
+
batch_size = x.size()[0]
|
|
97
|
+
for i in range(batch_size):
|
|
98
|
+
batch_result = []
|
|
99
|
+
for f in self.functions:
|
|
100
|
+
arity = len(inspect.getfullargspec(f).args)
|
|
101
|
+
args = [x[i]] * arity
|
|
102
|
+
cartesian = torch.cartesian_prod(*args).detach().numpy()
|
|
103
|
+
if arity == 1:
|
|
104
|
+
cartesian = np.expand_dims(cartesian, axis=1)
|
|
105
|
+
batch_result += [f(*args) for args in cartesian]
|
|
106
|
+
result.append(batch_result)
|
|
107
|
+
|
|
108
|
+
return torch.FloatTensor(result)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ExpandedLinearFeatureLayer(PredicateLearningLayer):
|
|
112
|
+
def __init__(self, basis_functions, dim_in, dim_out):
|
|
113
|
+
super().__init__()
|
|
114
|
+
torch.manual_seed(0)
|
|
115
|
+
|
|
116
|
+
self.functions = basis_functions
|
|
117
|
+
self.dim_in = dim_in
|
|
118
|
+
self.dim_out = dim_out
|
|
119
|
+
self.expansion_layer = BasisExpansionLayer(self.functions)
|
|
120
|
+
arities = [len(inspect.getfullargspec(f).args) for f in self.functions] + [1]
|
|
121
|
+
dim_mid = sum([dim_in ** n for n in arities])
|
|
122
|
+
self.linear_feature_layer = LinearFeatureLayer(dim_mid, self.dim_out)
|
|
123
|
+
|
|
124
|
+
def forward(self, x):
|
|
125
|
+
return nn.Sequential(self.expansion_layer, self.linear_feature_layer)(x)
|
|
126
|
+
|
|
127
|
+
def get_predicates(self, column_names, normalizer, thr, name_format='generic'):
|
|
128
|
+
norm_obj = copy(normalizer)
|
|
129
|
+
if normalizer is not None:
|
|
130
|
+
scale = normalizer.scale_
|
|
131
|
+
expanded_scale = _expand_normalizer(scale, self.functions)
|
|
132
|
+
norm_obj.scale_ = expanded_scale
|
|
133
|
+
norm_obj.n_features_in_ = len(expanded_scale)
|
|
134
|
+
expanded_col_names = _expand_columns(column_names, self.functions, name_format)
|
|
135
|
+
return self.linear_feature_layer.get_predicates(expanded_col_names, norm_obj, thr, name_format)
|
|
136
|
+
|
|
137
|
+
def get_penalty(self):
|
|
138
|
+
return self.linear_feature_layer.get_penalty()
|
|
139
|
+
|
|
140
|
+
def update_temp(self, temp):
|
|
141
|
+
self.linear_feature_layer.update_temp(temp)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _expand_columns(column_names, functions, name_format):
|
|
145
|
+
names = []
|
|
146
|
+
for i, f in enumerate(functions):
|
|
147
|
+
arity = len(inspect.getfullargspec(f).args)
|
|
148
|
+
repeat = [column_names] * arity
|
|
149
|
+
cols = itertools.product(*repeat)
|
|
150
|
+
names += ['f{}_'.format(i) + '_'.join(tup) for tup in cols] if name_format == 'generic' \
|
|
151
|
+
else ['*'.join(tup) for tup in cols]
|
|
152
|
+
|
|
153
|
+
return names + column_names
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _expand_normalizer(scale, functions):
|
|
157
|
+
expanded_scale = []
|
|
158
|
+
for i, f in enumerate(functions):
|
|
159
|
+
arity = len(inspect.getfullargspec(f).args)
|
|
160
|
+
repeat = [scale] * arity
|
|
161
|
+
scales = itertools.product(*repeat)
|
|
162
|
+
expanded_scale += [_prod(tup) for tup in scales]
|
|
163
|
+
|
|
164
|
+
return np.concatenate((np.array(expanded_scale), scale))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _prod(iterable):
|
|
168
|
+
return reduce(operator.mul, iterable, 1)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class PredicateLayer(nn.Module):
|
|
172
|
+
"""
|
|
173
|
+
This predicate layer combines categorical and numerical input features and, depending of the NegLayer flag, negates the categorical features.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, learning_layer, negation):
|
|
177
|
+
super(PredicateLayer, self).__init__()
|
|
178
|
+
self.LearningLayer = learning_layer
|
|
179
|
+
self.negation = negation
|
|
180
|
+
|
|
181
|
+
def forward(self, x):
|
|
182
|
+
x_cat_in = x[0]
|
|
183
|
+
x_cat_out = x_cat_in
|
|
184
|
+
x_num_in = x[1]
|
|
185
|
+
x_num_out = x_num_in
|
|
186
|
+
if x_num_in.nelement() != 0:
|
|
187
|
+
x_num_out = self.LearningLayer(x_num_in)
|
|
188
|
+
if x_cat_in.nelement() != 0 and self.negation:
|
|
189
|
+
neglayer = NegationLayer()
|
|
190
|
+
x_cat_out = neglayer(x_cat_out)
|
|
191
|
+
result = torch.cat((x_num_out, x_cat_out), -1)
|
|
192
|
+
return result
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import torch
|
|
6
|
+
from sklearn.preprocessing import StandardScaler, OneHotEncoder
|
|
7
|
+
from torch import nn
|
|
8
|
+
from torch.utils.data import DataLoader, Dataset
|
|
9
|
+
|
|
10
|
+
import rulelearn_ri.algorithms.r2n.preprocessing_layers as pp
|
|
11
|
+
import rulelearn_ri.algorithms.r2n.simple_rulenet as rn
|
|
12
|
+
import rulelearn_ri.algorithms.r2n.utilities as util
|
|
13
|
+
from rulelearn_ri.algorithms.r2n.training import train as train_r2n
|
|
14
|
+
from rulelearn_ri.trxf.core.conjunction import Conjunction
|
|
15
|
+
from rulelearn_ri.trxf.core.dnf_ruleset import DnfRuleSet
|
|
16
|
+
from rulelearn_ri.trxf.core.feature import Feature
|
|
17
|
+
from rulelearn_ri.trxf.core.predicate import Predicate, Relation
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class R2Nalgo:
|
|
21
|
+
"""
|
|
22
|
+
Wrapper for the R2N algorithm which is described in the following paper: arxiv.org/abs/2201.06515.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self,
|
|
26
|
+
cat_columns: list = None,
|
|
27
|
+
num_columns: list = None,
|
|
28
|
+
batch_size: int = 100,
|
|
29
|
+
learning_rate: float = 1e-3,
|
|
30
|
+
coef: float = 1e-3,
|
|
31
|
+
n_rules: int = 25,
|
|
32
|
+
n_conditions: int = 10,
|
|
33
|
+
n_seeds: int = 2,
|
|
34
|
+
init_temp: float = 1,
|
|
35
|
+
min_temp: float = 10 ** -4,
|
|
36
|
+
decay_rate: float = 0.999,
|
|
37
|
+
negation: bool = False,
|
|
38
|
+
normalize_num: bool = False,
|
|
39
|
+
basis_functions: list = None,
|
|
40
|
+
name_format='generic'
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
batch_size : int
|
|
44
|
+
The batch size used during the training of the NN
|
|
45
|
+
learning rate : float
|
|
46
|
+
Learning rate of the neural network
|
|
47
|
+
coef: float
|
|
48
|
+
Sparsity promoting coefficient. Larger is sparser.
|
|
49
|
+
n_rules: int
|
|
50
|
+
Numbers of rules in the and layer
|
|
51
|
+
n_conditions: int
|
|
52
|
+
Numbers of output nodes of the multivariate predicate layer
|
|
53
|
+
n_seeds: int
|
|
54
|
+
Number of times the optimizer and network weights are reinitialized (number of runs)
|
|
55
|
+
max_epochs: int
|
|
56
|
+
Number of epochs ran per seed
|
|
57
|
+
init_temp: float
|
|
58
|
+
Initial temperature of the cooling schedule
|
|
59
|
+
min_temp: float
|
|
60
|
+
Minimal temperature of the cooling schedule
|
|
61
|
+
decay_rate: float
|
|
62
|
+
Decay rate of the cooling schedule
|
|
63
|
+
negation: bool
|
|
64
|
+
Whether negations of categorical features are considered or not
|
|
65
|
+
normalize_num: bool
|
|
66
|
+
Scale the input features before training. Only works for polynomial basis functions.
|
|
67
|
+
basis_functions: list of lambdas
|
|
68
|
+
List of basis expansion functions to be applied to the original input
|
|
69
|
+
name_format: bool
|
|
70
|
+
An option to specify a more informative feature names for some basis expansions (currently only quadratic).
|
|
71
|
+
E.g., x1*x0 instead of f1_x1_x0 as the feature name.
|
|
72
|
+
"""
|
|
73
|
+
super(R2Nalgo, self).__init__()
|
|
74
|
+
self.batch_size = batch_size
|
|
75
|
+
self.learning_rate = learning_rate
|
|
76
|
+
self.coef = coef
|
|
77
|
+
self.n_rules = n_rules
|
|
78
|
+
self.n_conditions = n_conditions
|
|
79
|
+
self.n_seeds = n_seeds
|
|
80
|
+
self.decay_rate = decay_rate
|
|
81
|
+
self.init_temp = init_temp
|
|
82
|
+
self.min_temp = min_temp
|
|
83
|
+
self.max_epochs = math.ceil(
|
|
84
|
+
1.2 * (math.log(self.min_temp) - math.log(self.init_temp)) / math.log(self.decay_rate))
|
|
85
|
+
self.negation = negation
|
|
86
|
+
self.normalize_num = normalize_num
|
|
87
|
+
self.basis_functions = basis_functions
|
|
88
|
+
self.name_format = name_format
|
|
89
|
+
|
|
90
|
+
self.encoder_object = None
|
|
91
|
+
self.norm_object = None
|
|
92
|
+
self.n_cat_features, self.n_num_features = None, None
|
|
93
|
+
self.num_column_names, self.cat_column_names = num_columns, cat_columns
|
|
94
|
+
self.model = None
|
|
95
|
+
self.opt_rule = None
|
|
96
|
+
self.predicate_learning_layer = None
|
|
97
|
+
|
|
98
|
+
def fit(self, train, train_labels):
|
|
99
|
+
"""
|
|
100
|
+
The fit function for R2N algorithm. All float and int dtypes are considered numerical features.
|
|
101
|
+
All others are treated as categorical.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
# Partition the input features through their dtype if cat and num features are not provided.
|
|
105
|
+
train.columns = train.columns.str.replace(' ', '')
|
|
106
|
+
if self.cat_column_names == None:
|
|
107
|
+
self.cat_column_names = list(train.select_dtypes(exclude=['float']).columns.str.replace(' ', ''))
|
|
108
|
+
if self.num_column_names == None:
|
|
109
|
+
self.num_column_names = list(train.select_dtypes(include=['float']).columns.str.replace(' ', ''))
|
|
110
|
+
X_cat, X_num, train_dataloader = self._preprocess(train, train_labels)
|
|
111
|
+
|
|
112
|
+
# Training loop
|
|
113
|
+
loss_fn = nn.MSELoss()
|
|
114
|
+
best_loss = math.inf
|
|
115
|
+
best_acc = -1
|
|
116
|
+
for _ in np.arange(0, self.n_seeds):
|
|
117
|
+
|
|
118
|
+
self.model, mlpnet, rulenet = self._construct_r2n()
|
|
119
|
+
optimizer = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate, amsgrad=True)
|
|
120
|
+
temp = self.init_temp
|
|
121
|
+
for t in range(self.max_epochs):
|
|
122
|
+
temp, loss, success_rate_train = train_r2n(train_dataloader, self.model, loss_fn, optimizer, temp,
|
|
123
|
+
self.min_temp, self.decay_rate, self.coef, mv_reg=self.coef,
|
|
124
|
+
multivariate=(self.n_num_features != 0))
|
|
125
|
+
if t % 10 == 0:
|
|
126
|
+
print("Epoch:", t, "Success rate:", '%.1f' % success_rate_train, "Loss:", '%.5f' % loss,
|
|
127
|
+
"Temperature:", '%.1e' % temp)
|
|
128
|
+
if temp < self.min_temp and loss < best_loss:
|
|
129
|
+
if self.n_num_features > 0:
|
|
130
|
+
self.predicate_learning_layer = mlpnet
|
|
131
|
+
self.opt_rule = rulenet.extract_ruleset()
|
|
132
|
+
best_loss = loss
|
|
133
|
+
best_acc = success_rate_train
|
|
134
|
+
print('---------')
|
|
135
|
+
|
|
136
|
+
print("Max performance of the network:", best_acc)
|
|
137
|
+
|
|
138
|
+
# Post-processing
|
|
139
|
+
binarized_input = _binarize_input(X_num, X_cat, self.predicate_learning_layer, self.negation)
|
|
140
|
+
self.opt_rule = util.simplify_ruleset(util.filter_trivial_predicates(self.opt_rule, binarized_input))
|
|
141
|
+
|
|
142
|
+
def predict(self, features: pd.DataFrame) -> pd.DataFrame:
|
|
143
|
+
"""
|
|
144
|
+
:param features: A pandas dataframe containing feature values
|
|
145
|
+
:return predictions: A pandas dataframe containing a single column called '__predictions__'
|
|
146
|
+
"""
|
|
147
|
+
features.columns = features.columns.str.replace(' ', '')
|
|
148
|
+
_, X_cat_t, X_num_t = self._data_preprocessing(features, None, training=False)
|
|
149
|
+
pred_evaluation = _binarize_input(X_num_t, X_cat_t, self.predicate_learning_layer, self.negation)
|
|
150
|
+
eval_sol = util.batch_apply_ruleset(self.opt_rule, pred_evaluation.T)
|
|
151
|
+
predictions = pd.DataFrame(data=eval_sol, columns=['__predictions__'])
|
|
152
|
+
|
|
153
|
+
return predictions
|
|
154
|
+
|
|
155
|
+
def export_rules_to_trxf_dnf_ruleset(self, thr=0):
|
|
156
|
+
"""
|
|
157
|
+
Args:
|
|
158
|
+
thr: Threshold to remove negligible coefficients. Any coefficient that has a smaller ratio to the dominating
|
|
159
|
+
coefficient than thr will be eliminated in the extracted ruleset.
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
trxf.DnfRuleSet
|
|
164
|
+
"""
|
|
165
|
+
learned_predicates = []
|
|
166
|
+
predetermined_predicates = []
|
|
167
|
+
if self.n_num_features > 0:
|
|
168
|
+
learned_predicates = self.predicate_learning_layer.get_predicates(
|
|
169
|
+
self.num_column_names, self.norm_object, thr, self.name_format)
|
|
170
|
+
if self.n_cat_features > 0:
|
|
171
|
+
a = list(zip(self.cat_column_names, self.encoder_object.categories_))
|
|
172
|
+
for item in a:
|
|
173
|
+
col_name = item[0]
|
|
174
|
+
categories = item[1]
|
|
175
|
+
predetermined_predicates += [Predicate(Feature(col_name), Relation.EQ, v) for v in categories]
|
|
176
|
+
predicates = learned_predicates + predetermined_predicates
|
|
177
|
+
|
|
178
|
+
return DnfRuleSet([Conjunction([pred for pred, w in zip(predicates, rule) if w > 0]) for rule in self.opt_rule],
|
|
179
|
+
then_part='Churn')
|
|
180
|
+
|
|
181
|
+
def _preprocess(self, train, train_labels):
|
|
182
|
+
y, X_cat, X_num = self._data_preprocessing(train, train_labels, training=True)
|
|
183
|
+
self.n_num_features = X_num.shape[1] if X_num is not None else 0
|
|
184
|
+
self.n_cat_features = X_cat.shape[1] if X_cat is not None else 0
|
|
185
|
+
dataset = _NumCatDataset(y, X_num, X_cat)
|
|
186
|
+
train_dataloader = DataLoader(dataset, self.batch_size)
|
|
187
|
+
return X_cat, X_num, train_dataloader
|
|
188
|
+
|
|
189
|
+
def _construct_r2n(self):
|
|
190
|
+
pred_learning = None
|
|
191
|
+
dim_num = 0
|
|
192
|
+
if self.n_num_features > 0:
|
|
193
|
+
if self.basis_functions is not None:
|
|
194
|
+
pred_learning = pp.ExpandedLinearFeatureLayer(self.basis_functions, self.n_num_features, self.n_conditions)
|
|
195
|
+
else:
|
|
196
|
+
pred_learning = pp.LinearFeatureLayer(self.n_num_features, self.n_conditions)
|
|
197
|
+
dim_num = pred_learning.dim_out
|
|
198
|
+
pred_layer = pp.PredicateLayer(pred_learning, self.negation)
|
|
199
|
+
dim_cat = 2 * self.n_cat_features if self.negation else self.n_cat_features
|
|
200
|
+
rulenet = rn.SimpleRuleNet(dim_num + dim_cat, self.n_rules)
|
|
201
|
+
model = nn.Sequential(pred_layer, rulenet)
|
|
202
|
+
return model, pred_learning, rulenet
|
|
203
|
+
|
|
204
|
+
def _data_preprocessing(self, train, train_labels, training):
|
|
205
|
+
"""
|
|
206
|
+
Imports a pandas data frame and selects the numerical ('float') and categorical (NOT 'float') input features and
|
|
207
|
+
returns a pytorch dataframe and the number of categorical and numerical features.
|
|
208
|
+
"""
|
|
209
|
+
X_cat, X_num = None, None
|
|
210
|
+
table_X_num, table_X_cat = train[self.num_column_names], train[self.cat_column_names]
|
|
211
|
+
|
|
212
|
+
if len(self.num_column_names)>0:
|
|
213
|
+
if self.normalize_num:
|
|
214
|
+
if training:
|
|
215
|
+
self.norm_object = StandardScaler(with_mean=False)
|
|
216
|
+
table_X_num = self.norm_object.fit_transform(np.array(table_X_num))
|
|
217
|
+
else:
|
|
218
|
+
assert self.norm_object is not None
|
|
219
|
+
table_X_num = self.norm_object.transform(np.array(table_X_num))
|
|
220
|
+
X_num = np.array(table_X_num, dtype=float)
|
|
221
|
+
|
|
222
|
+
if len(self.cat_column_names)>0:
|
|
223
|
+
if training:
|
|
224
|
+
print(table_X_num.shape[1], "numerical features and ", table_X_cat.shape[1], "categorical features")
|
|
225
|
+
print('---------')
|
|
226
|
+
self.encoder_object, X_cat = _categorical_encoding(table_X_cat.astype(str))
|
|
227
|
+
print(X_cat.shape[1], "categorical dummies")
|
|
228
|
+
print('---------')
|
|
229
|
+
else:
|
|
230
|
+
assert self.encoder_object is not None
|
|
231
|
+
X_cat = self.encoder_object.transform(table_X_cat.astype(str)).toarray()
|
|
232
|
+
|
|
233
|
+
if training:
|
|
234
|
+
y = np.array(train_labels, dtype=int)
|
|
235
|
+
else:
|
|
236
|
+
y = None
|
|
237
|
+
|
|
238
|
+
return y, X_cat, X_num
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Preprocessing function R2N ####
|
|
243
|
+
|
|
244
|
+
def _categorical_encoding(X_cat):
|
|
245
|
+
"""
|
|
246
|
+
Encodes all categorical variables with OneHot encoding
|
|
247
|
+
"""
|
|
248
|
+
ohe = OneHotEncoder(handle_unknown='ignore')
|
|
249
|
+
ohe.fit_transform(X_cat)
|
|
250
|
+
X = ohe.transform(X_cat).toarray()
|
|
251
|
+
return ohe, X
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class _NumCatDataset(Dataset):
|
|
255
|
+
def __init__(self, y, X_num=None, X_cat=None):
|
|
256
|
+
self.X_num = X_num
|
|
257
|
+
self.X_cat = X_cat
|
|
258
|
+
self.num_tensor = torch.tensor(X_num, dtype=torch.float64) if X_num is not None else None
|
|
259
|
+
self.cat_tensor = torch.tensor(X_cat, dtype=torch.float64) if X_cat is not None else None
|
|
260
|
+
self.y_train = torch.tensor(y)
|
|
261
|
+
|
|
262
|
+
def __len__(self):
|
|
263
|
+
return len(self.y_train)
|
|
264
|
+
|
|
265
|
+
def __getitem__(self, idx):
|
|
266
|
+
xcat = self.cat_tensor[idx] if self.X_cat is not None else torch.empty(0)
|
|
267
|
+
xnum = self.num_tensor[idx] if self.X_num is not None else torch.empty(0)
|
|
268
|
+
return (xcat, xnum), self.y_train[idx]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _binarize_input(X_num=None, X_cat=None, predicate_learning_layer=None, negation=False):
|
|
272
|
+
"""
|
|
273
|
+
Evaluates the predicates and concatenates if necessary the numerical and categorical ones.
|
|
274
|
+
"""
|
|
275
|
+
pred_evaluation = None
|
|
276
|
+
if X_num is not None:
|
|
277
|
+
pred_evaluation = (predicate_learning_layer(torch.tensor(X_num).float()) > 0.5).T.detach().numpy()
|
|
278
|
+
if X_cat is not None:
|
|
279
|
+
if X_num is not None:
|
|
280
|
+
pred_evaluation = np.concatenate((pred_evaluation, X_cat.T), axis=0)
|
|
281
|
+
else:
|
|
282
|
+
pred_evaluation = X_cat.T
|
|
283
|
+
if negation is not False:
|
|
284
|
+
pred_evaluation = np.concatenate((pred_evaluation, np.logical_not(X_cat.T)), axis=0)
|
|
285
|
+
if pred_evaluation is None:
|
|
286
|
+
raise ValueError('The input X_num and X_cat should not be both None')
|
|
287
|
+
return pred_evaluation
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
|
|
4
|
+
import rulelearn_ri.algorithms.r2n.mask as wm
|
|
5
|
+
from rulelearn_ri.algorithms.r2n.base import RuleNetwork
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimpleRuleNet(RuleNetwork):
|
|
9
|
+
"""
|
|
10
|
+
Class that assembles the rule network part of r2n. It initializes and configures the conjunction and disjunction layer.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, n_conditions, n_rules, cooling=True):
|
|
13
|
+
super(SimpleRuleNet, self).__init__()
|
|
14
|
+
self.n_rules = n_rules
|
|
15
|
+
self.n_conditions = n_conditions
|
|
16
|
+
self.temp = 1
|
|
17
|
+
self.cooling = cooling
|
|
18
|
+
self.weightmask_1 = wm.SimpleWeightMask(self.n_rules, self.n_conditions, self.temp, self.cooling)
|
|
19
|
+
self.weightmask_2 = wm.SimpleWeightMask(1, self.n_rules, self.temp, self.cooling)
|
|
20
|
+
self.network = nn.Sequential(
|
|
21
|
+
ConjunctionLayer(self.weightmask_1),
|
|
22
|
+
DisjunctionLayer(self.weightmask_2),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def forward(self, x):
|
|
26
|
+
return self.network(x)
|
|
27
|
+
|
|
28
|
+
def extract_ruleset(self):
|
|
29
|
+
ruleset = []
|
|
30
|
+
w_conj = torch.heaviside(self.network[0].mask() - torch.tensor(0.5),
|
|
31
|
+
torch.ones_like(self.network[0].mask()))
|
|
32
|
+
w_disj = torch.heaviside(self.network[1].mask() - torch.tensor(0.5),
|
|
33
|
+
torch.ones_like(self.network[1].mask()))
|
|
34
|
+
for d_idx, d_weight in enumerate(torch.flatten(w_disj).tolist()):
|
|
35
|
+
if d_weight > 0:
|
|
36
|
+
rule = w_conj.tolist()[d_idx]
|
|
37
|
+
if sum(rule) > 0:
|
|
38
|
+
ruleset.append(rule)
|
|
39
|
+
return ruleset
|
|
40
|
+
|
|
41
|
+
def get_penalty(self):
|
|
42
|
+
and_masks = self.network[0].mask()
|
|
43
|
+
or_masks = self.network[1].mask()
|
|
44
|
+
|
|
45
|
+
return (or_masks.sum() + and_masks.sum()) / self.n_rules
|
|
46
|
+
|
|
47
|
+
def update_temp(self, temp):
|
|
48
|
+
self.weightmask_1.set_temp(temp)
|
|
49
|
+
self.weightmask_2.set_temp(temp)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ConjunctionLayer(nn.Module):
|
|
53
|
+
"""
|
|
54
|
+
Conjuction layer (And layer)
|
|
55
|
+
"""
|
|
56
|
+
def __init__(self, mask):
|
|
57
|
+
super(ConjunctionLayer, self).__init__()
|
|
58
|
+
self.mask = mask
|
|
59
|
+
|
|
60
|
+
def forward(self, x):
|
|
61
|
+
neg_x = torch.ones_like(x) - x
|
|
62
|
+
prod = torch.matmul(neg_x, torch.transpose(self.mask().float(), 0, 1))
|
|
63
|
+
return torch.ones_like(prod) - torch.clamp(prod, max=1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DisjunctionLayer(nn.Module):
|
|
67
|
+
"""
|
|
68
|
+
Disjunction layer (OR layer)
|
|
69
|
+
"""
|
|
70
|
+
def __init__(self, mask):
|
|
71
|
+
super(DisjunctionLayer, self).__init__()
|
|
72
|
+
self.mask = mask
|
|
73
|
+
|
|
74
|
+
def forward(self, x):
|
|
75
|
+
return torch.clamp(torch.matmul(self.mask(), torch.transpose(x, 0, 1)), max=1)
|