ltntorch 0.1.0
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.
- checksums.yaml +7 -0
- data/AGENTS.md +34 -0
- data/CONTRIBUTING.md +66 -0
- data/changelog.md +68 -0
- data/code_of_conduct.md +84 -0
- data/examples/1_binary_classification.rb +108 -0
- data/examples/2_multi_class_single_label_classification.rb +130 -0
- data/examples/3_multi_class_multi_label_classification.rb +158 -0
- data/examples/4_semi_supervised_pattern_recognition.rb +249 -0
- data/examples/5_regression.rb +103 -0
- data/examples/6_clustering.rb +126 -0
- data/examples/7_learning_embeddings_with_ltn.rb +166 -0
- data/examples/datasets/crabs.dat +201 -0
- data/examples/datasets/iris_test.csv +31 -0
- data/examples/datasets/iris_training.csv +121 -0
- data/examples/datasets/readme.md +9 -0
- data/examples/datasets/real-estate.csv +415 -0
- data/examples/support/example.rb +92 -0
- data/examples/support/mnist.rb +77 -0
- data/lib/ltn/connective.rb +56 -0
- data/lib/ltn/constant.rb +30 -0
- data/lib/ltn/diag.rb +54 -0
- data/lib/ltn/function.rb +28 -0
- data/lib/ltn/fuzzy_ops.rb +428 -0
- data/lib/ltn/lambda_model.rb +24 -0
- data/lib/ltn/ltn_object.rb +47 -0
- data/lib/ltn/model_wrapper.rb +72 -0
- data/lib/ltn/predicate.rb +38 -0
- data/lib/ltn/process_ltn_objects.rb +58 -0
- data/lib/ltn/quantifier.rb +176 -0
- data/lib/ltn/variable.rb +49 -0
- data/lib/ltn/version.rb +8 -0
- data/lib/ltn.rb +45 -0
- data/license.md +22 -0
- data/readme.md +166 -0
- data/sig/ltn.rbs +207 -0
- data/tutorials/1_grounding_non_logical_symbols.rb +146 -0
- data/tutorials/2_grounding_connectives.rb +126 -0
- data/tutorials/2b_operators_and_gradients.rb +78 -0
- data/tutorials/3_knowledgebase_and_learning.rb +124 -0
- metadata +94 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Mauro Tortonesi.
|
|
5
|
+
# Ported from LTNtorch, copyright 2021-2024 by Tommaso Carraro.
|
|
6
|
+
|
|
7
|
+
module LTN
|
|
8
|
+
# Makes the groundings of LTN objects compatible for an element-wise
|
|
9
|
+
# predicate, function or connective (LTN broadcasting).
|
|
10
|
+
#
|
|
11
|
+
# Every object is expanded to the union of the free variables of all the
|
|
12
|
+
# objects, in the order in which they first appear, by repeating its values
|
|
13
|
+
# along the variables it lacks. The variable axes are then flattened into a
|
|
14
|
+
# single leading batch axis, so that the i-th row of every processed object
|
|
15
|
+
# refers to the same combination of individuals.
|
|
16
|
+
#
|
|
17
|
+
# Unlike LTNtorch, this never modifies the given objects: LTNtorch rebinds
|
|
18
|
+
# the value and free variables of intermediate results (objects whose value
|
|
19
|
+
# has a grad_fn), so reusing such a formula afterwards sees the broadcast
|
|
20
|
+
# grounding. The processed values are the same.
|
|
21
|
+
#
|
|
22
|
+
# @parameter objects [Array(LTNObject)] the objects to process.
|
|
23
|
+
# @returns [Array] a triple: the processed objects, the labels of all the
|
|
24
|
+
# free variables, and the number of individuals of each of them.
|
|
25
|
+
# @raises [TypeError] if `objects` is not an array of LTN objects.
|
|
26
|
+
def self.process_ltn_objects(objects)
|
|
27
|
+
unless objects.is_a?(Array) && objects.all?(LTNObject)
|
|
28
|
+
raise TypeError, "The objects should be an Array of LTNObject"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
vars_to_n = {}
|
|
32
|
+
objects.each do |object|
|
|
33
|
+
object.free_vars.each_with_index { |var, index| vars_to_n[var] = object.shape[index] }
|
|
34
|
+
end
|
|
35
|
+
vars = vars_to_n.keys
|
|
36
|
+
n_individuals_per_var = vars_to_n.values
|
|
37
|
+
|
|
38
|
+
processed = objects.map do |object|
|
|
39
|
+
value = object.value
|
|
40
|
+
vars_in_object = object.free_vars.dup
|
|
41
|
+
|
|
42
|
+
(vars - vars_in_object).each do |new_var|
|
|
43
|
+
new_var_index = vars_in_object.size
|
|
44
|
+
value = value.unsqueeze(new_var_index)
|
|
45
|
+
value = Torch.repeat_interleave(value, vars_to_n[new_var], dim: new_var_index)
|
|
46
|
+
vars_in_object << new_var
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
permutation = vars.map { |var| vars_in_object.index(var) } + (vars_in_object.size...value.dim).to_a
|
|
50
|
+
value = value.permute(permutation)
|
|
51
|
+
value = value.reshape([-1] + value.shape[vars_in_object.size..])
|
|
52
|
+
|
|
53
|
+
LTNObject.new(value, vars.dup)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
[processed, vars, n_individuals_per_var]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Mauro Tortonesi.
|
|
5
|
+
# Ported from LTNtorch, copyright 2021-2024 by Tommaso Carraro.
|
|
6
|
+
|
|
7
|
+
module LTN
|
|
8
|
+
# An LTN quantifier: aggregates the truth values of a formula along the axes
|
|
9
|
+
# of the quantified variables, with an aggregation operator (see
|
|
10
|
+
# LTN::FuzzyOps).
|
|
11
|
+
#
|
|
12
|
+
# forall = LTN::Quantifier.new(LTN::FuzzyOps::AggregPMeanError.new(p: 2), :forall)
|
|
13
|
+
# exists = LTN::Quantifier.new(LTN::FuzzyOps::AggregPMean.new(p: 2), :exists)
|
|
14
|
+
# forall.call(x, exists.call(y, p.call(x, y)))
|
|
15
|
+
#
|
|
16
|
+
# Guarded quantification restricts the aggregation to the individuals that
|
|
17
|
+
# satisfy a condition:
|
|
18
|
+
#
|
|
19
|
+
# forall.call(x, p.call(x), cond_vars: [x]) { |x| x.value.sum(1).gt(0.5) }
|
|
20
|
+
class Quantifier
|
|
21
|
+
QUANTIFIERS = {:forall => :forall, :exists => :exists, "f" => :forall, "e" => :exists}.freeze
|
|
22
|
+
|
|
23
|
+
# @attribute [FuzzyOps::AggregationOperator] the aggregation operator.
|
|
24
|
+
attr_reader :agg_op
|
|
25
|
+
|
|
26
|
+
# @attribute [Symbol] :forall or :exists.
|
|
27
|
+
attr_reader :quantifier
|
|
28
|
+
|
|
29
|
+
# @parameter agg_op [FuzzyOps::AggregationOperator] the aggregation operator.
|
|
30
|
+
# @parameter quantifier [Symbol, String] :forall or :exists (LTNtorch's "f" and "e" work too).
|
|
31
|
+
# It determines the truth value of a guarded quantification over no individuals:
|
|
32
|
+
# 1 for :forall, 0 for :exists.
|
|
33
|
+
# @raises [TypeError] if `agg_op` is not an aggregation operator.
|
|
34
|
+
# @raises [ArgumentError] if `quantifier` is not one of the above.
|
|
35
|
+
def initialize(agg_op, quantifier)
|
|
36
|
+
unless agg_op.is_a?(FuzzyOps::AggregationOperator)
|
|
37
|
+
raise TypeError, "Quantifier.new: argument 'agg_op' (position 1) must be a " \
|
|
38
|
+
"LTN::FuzzyOps::AggregationOperator, not #{agg_op.class}"
|
|
39
|
+
end
|
|
40
|
+
unless QUANTIFIERS.key?(quantifier)
|
|
41
|
+
raise ArgumentError, "Expected parameter 'quantifier' to be :exists (or 'e') for the existential " \
|
|
42
|
+
"quantifier, or :forall (or 'f') for the universal one, but got #{quantifier.inspect}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@agg_op = agg_op
|
|
46
|
+
@quantifier = QUANTIFIERS.fetch(quantifier)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Quantifies `formula` over `vars`, then resets any diagonal
|
|
50
|
+
# quantification on them (see LTN.diag).
|
|
51
|
+
#
|
|
52
|
+
# @parameter vars [Variable, Array(Variable)] the quantified variables.
|
|
53
|
+
# @parameter formula [LTNObject] the formula.
|
|
54
|
+
# @parameter cond_vars [Variable, Array(Variable), nil] the variables of
|
|
55
|
+
# the guard, for guarded quantification.
|
|
56
|
+
# @parameter cond_fn [#call, nil] the guard: given the (broadcast) LTN
|
|
57
|
+
# objects of `cond_vars`, returns a boolean tensor with one value per
|
|
58
|
+
# combination of their individuals. It can be given as a block instead.
|
|
59
|
+
# @parameter kwargs [Hash] passed to the aggregation operator, e.g. `p: 4`.
|
|
60
|
+
# @returns [LTNObject] the result, whose free variables are those of the
|
|
61
|
+
# formula minus the quantified ones. Guarded quantification returns
|
|
62
|
+
# float64 values, as in LTNtorch.
|
|
63
|
+
# @raises [ArgumentError] if only one of `cond_vars` and `cond_fn` is
|
|
64
|
+
# given, a quantified variable does not appear in the formula, or the
|
|
65
|
+
# formula's values are not in [0, 1].
|
|
66
|
+
# @raises [TypeError] if an argument has the wrong type.
|
|
67
|
+
def call(vars, formula, cond_vars: nil, cond_fn: nil, **kwargs, &block)
|
|
68
|
+
if cond_fn && block
|
|
69
|
+
raise ArgumentError, "Expected the guard as either 'cond_fn' or a block, but got both."
|
|
70
|
+
end
|
|
71
|
+
cond_fn ||= block
|
|
72
|
+
if cond_fn && cond_vars.nil?
|
|
73
|
+
raise ArgumentError, "Since 'cond_fn' has been set, 'cond_vars' must be set as well, but got nil."
|
|
74
|
+
end
|
|
75
|
+
if cond_vars && cond_fn.nil?
|
|
76
|
+
raise ArgumentError, "Since 'cond_vars' has been set, 'cond_fn' must be set as well, but got nil."
|
|
77
|
+
end
|
|
78
|
+
vars = variables(vars, "vars")
|
|
79
|
+
unless formula.is_a?(LTNObject)
|
|
80
|
+
raise TypeError, "Expected parameter 'formula' to be an LTNObject, but got #{formula.class}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
FuzzyOps.check_values(formula.value)
|
|
84
|
+
|
|
85
|
+
aggregation_vars = vars.map { |var| var.free_vars.first }.uniq
|
|
86
|
+
if cond_fn
|
|
87
|
+
cond_vars = variables(cond_vars, "cond_vars")
|
|
88
|
+
unless cond_fn.respond_to?(:call)
|
|
89
|
+
raise TypeError, "Expected parameter 'cond_fn' to be a function, but got #{cond_fn.class}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
formula, mask = self.class.compute_mask(formula, cond_vars, cond_fn, aggregation_vars)
|
|
93
|
+
output = agg_op.call(formula.value, dim: aggregation_dims(formula, aggregation_vars), mask: mask.value, **kwargs)
|
|
94
|
+
# The aggregation is undefined (NaN) where the guard leaves no
|
|
95
|
+
# individuals: forall is then vacuously true, exists false.
|
|
96
|
+
output = FuzzyOps.where(Torch.isnan(output), (quantifier == :forall) ? 1.0 : 0.0, output.double)
|
|
97
|
+
else
|
|
98
|
+
output = agg_op.call(formula.value, dim: aggregation_dims(formula, aggregation_vars), **kwargs)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
LTN.undiag(*vars)
|
|
102
|
+
LTNObject.new(output, formula.free_vars - aggregation_vars)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Prepares a guarded quantification: broadcasts `formula` over the
|
|
106
|
+
# condition variables it lacks, and computes the mask of `cond_fn`. Both
|
|
107
|
+
# are transposed so that their leading axes are the condition variables
|
|
108
|
+
# that are not quantified, then the quantified ones.
|
|
109
|
+
#
|
|
110
|
+
# Unlike LTNtorch, this does not modify `formula`.
|
|
111
|
+
#
|
|
112
|
+
# @parameter formula [LTNObject] the formula.
|
|
113
|
+
# @parameter cond_vars [Array(Variable)] the variables of the guard.
|
|
114
|
+
# @parameter cond_fn [#call] the guard.
|
|
115
|
+
# @parameter aggregation_vars [Array(String)] labels of the quantified variables.
|
|
116
|
+
# @returns [Array(LTNObject)] the formula and the mask, with the same shape and free variables.
|
|
117
|
+
def self.compute_mask(formula, cond_vars, cond_fn, aggregation_vars)
|
|
118
|
+
cond_vars_not_in_formula = cond_vars.reject { |var| formula.free_vars.include?(var.free_vars.first) }
|
|
119
|
+
unless cond_vars_not_in_formula.empty?
|
|
120
|
+
processed, _, n_individuals = LTN.process_ltn_objects([formula] + cond_vars_not_in_formula)
|
|
121
|
+
formula = LTNObject.new(processed.first.value.view(n_individuals), processed.first.free_vars)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
cond_labels = cond_vars.map { |var| var.free_vars.first }
|
|
125
|
+
cond_not_aggregated = cond_labels.reject { |var| aggregation_vars.include?(var) }
|
|
126
|
+
cond_aggregated = cond_labels.select { |var| aggregation_vars.include?(var) }
|
|
127
|
+
not_in_cond = formula.free_vars.reject { |var| cond_labels.include?(var) }
|
|
128
|
+
formula = transpose_vars(formula, (cond_not_aggregated + cond_aggregated + not_in_cond).uniq)
|
|
129
|
+
|
|
130
|
+
processed_cond_vars, cond_vars_order, n_individuals_per_var = LTN.process_ltn_objects(cond_vars)
|
|
131
|
+
mask = cond_fn.call(*processed_cond_vars)
|
|
132
|
+
mask = LTNObject.new(mask.reshape(n_individuals_per_var), cond_vars_order)
|
|
133
|
+
mask = transpose_vars(mask, (cond_not_aggregated + cond_aggregated).uniq)
|
|
134
|
+
|
|
135
|
+
if formula.shape != mask.shape
|
|
136
|
+
(formula, mask), _, n_individuals_per_var = LTN.process_ltn_objects([formula, mask])
|
|
137
|
+
mask = LTNObject.new(mask.value.view(n_individuals_per_var), mask.free_vars)
|
|
138
|
+
formula = LTNObject.new(formula.value.view(n_individuals_per_var), formula.free_vars)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
[formula, mask]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @parameter object [LTNObject] the object to transpose.
|
|
145
|
+
# @parameter new_vars_order [Array(String)] its free variables, in the new order.
|
|
146
|
+
# @returns [LTNObject] a new object with the axes of the free variables permuted.
|
|
147
|
+
# (LTNtorch modifies and returns `object`.)
|
|
148
|
+
def self.transpose_vars(object, new_vars_order)
|
|
149
|
+
permutation = new_vars_order.map { |var| object.free_vars.index(var) }
|
|
150
|
+
LTNObject.new(object.value.permute(permutation), new_vars_order)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def inspect
|
|
154
|
+
"Quantifier(agg_op=#{agg_op.inspect}, quantifier=#{quantifier.inspect})"
|
|
155
|
+
end
|
|
156
|
+
alias_method :to_s, :inspect
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def variables(vars, name)
|
|
161
|
+
return [vars] if vars.is_a?(Variable)
|
|
162
|
+
return vars if vars.is_a?(Array) && vars.all?(Variable)
|
|
163
|
+
|
|
164
|
+
types = vars.is_a?(Array) ? vars.map(&:class).inspect : vars.class
|
|
165
|
+
raise TypeError, "Expected parameter '#{name}' to be a Variable or an Array of Variable, but got #{types}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def aggregation_dims(formula, aggregation_vars)
|
|
169
|
+
aggregation_vars.map do |var|
|
|
170
|
+
formula.free_vars.index(var) ||
|
|
171
|
+
raise(ArgumentError, "Variable #{var.inspect} does not appear in the formula (free variables: " \
|
|
172
|
+
"#{formula.free_vars.inspect})")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
data/lib/ltn/variable.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Mauro Tortonesi.
|
|
5
|
+
# Ported from LTNtorch, copyright 2021-2024 by Tommaso Carraro.
|
|
6
|
+
|
|
7
|
+
module LTN
|
|
8
|
+
# An LTN variable: a sequence of individuals of the domain, grounded as a
|
|
9
|
+
# tensor whose first dimension indexes the individuals.
|
|
10
|
+
class Variable < LTNObject
|
|
11
|
+
# @attribute [String] the label given to the variable, which #free_vars
|
|
12
|
+
# replaces temporarily during diagonal quantification (see LTN.diag).
|
|
13
|
+
attr_reader :latent_var
|
|
14
|
+
|
|
15
|
+
# @parameter var_label [String] the label of the variable. Labels starting
|
|
16
|
+
# with `diag_` are reserved for diagonal quantification.
|
|
17
|
+
# @parameter individuals [Torch::Tensor] the individuals, one per row.
|
|
18
|
+
# float64 values are converted to float32.
|
|
19
|
+
# @parameter add_batch_dim [Boolean] whether to turn a 1-D tensor of n
|
|
20
|
+
# individuals into a n x 1 tensor, so that each individual is a vector.
|
|
21
|
+
# @raises [TypeError] if the label is not a String or the individuals are not a tensor.
|
|
22
|
+
# @raises [ArgumentError] if the label starts with `diag_`.
|
|
23
|
+
def initialize(var_label, individuals, add_batch_dim: true)
|
|
24
|
+
unless var_label.is_a?(String)
|
|
25
|
+
raise TypeError, "Variable.new: argument 'var_label' (position 1) must be a String, not #{var_label.class}"
|
|
26
|
+
end
|
|
27
|
+
if var_label.start_with?("diag_")
|
|
28
|
+
raise ArgumentError, "Labels starting with 'diag_' are reserved for diagonal quantification."
|
|
29
|
+
end
|
|
30
|
+
unless individuals.is_a?(Torch::Tensor)
|
|
31
|
+
raise TypeError, "Variable.new: argument 'individuals' (position 2) must be a Torch::Tensor, not #{individuals.class}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
super(individuals, [var_label])
|
|
35
|
+
|
|
36
|
+
# Avoids dtype mismatches with float32 models. (LTNtorch converts CPU
|
|
37
|
+
# float64 tensors only; here any float64 tensor is converted.)
|
|
38
|
+
@value = @value.float if @value.dtype == :float64
|
|
39
|
+
@value = @value.view(@value.shape[0], 1) if @value.dim == 1 && add_batch_dim
|
|
40
|
+
@value = @value.to(LTN.device)
|
|
41
|
+
@latent_var = var_label
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def inspect
|
|
45
|
+
"Variable(value=#{value.inspect}, free_vars=#{free_vars.inspect})"
|
|
46
|
+
end
|
|
47
|
+
alias_method :to_s, :inspect
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/ltn/version.rb
ADDED
data/lib/ltn.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Mauro Tortonesi.
|
|
5
|
+
# Ported from LTNtorch, copyright 2021-2024 by Tommaso Carraro.
|
|
6
|
+
|
|
7
|
+
require "torch"
|
|
8
|
+
|
|
9
|
+
require_relative "ltn/version"
|
|
10
|
+
require_relative "ltn/ltn_object"
|
|
11
|
+
require_relative "ltn/fuzzy_ops"
|
|
12
|
+
require_relative "ltn/constant"
|
|
13
|
+
require_relative "ltn/variable"
|
|
14
|
+
require_relative "ltn/process_ltn_objects"
|
|
15
|
+
require_relative "ltn/lambda_model"
|
|
16
|
+
require_relative "ltn/model_wrapper"
|
|
17
|
+
require_relative "ltn/predicate"
|
|
18
|
+
require_relative "ltn/function"
|
|
19
|
+
require_relative "ltn/diag"
|
|
20
|
+
require_relative "ltn/connective"
|
|
21
|
+
require_relative "ltn/quantifier"
|
|
22
|
+
|
|
23
|
+
# Logic Tensor Networks (LTN) on top of torch.rb: a Ruby port of LTNtorch
|
|
24
|
+
# (https://github.com/tommasocarraro/LTNtorch).
|
|
25
|
+
module LTN
|
|
26
|
+
class Error < StandardError; end
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# The device on which LTN places the groundings of its variables.
|
|
30
|
+
#
|
|
31
|
+
# LTNtorch picks CUDA at import time when available. Here the default is
|
|
32
|
+
# chosen lazily, on first access, and can be overridden at any time, e.g.
|
|
33
|
+
# `LTN.device = Torch.device("cpu")` to force CPU in tests.
|
|
34
|
+
#
|
|
35
|
+
# @returns [Torch::Device]
|
|
36
|
+
def device
|
|
37
|
+
@device ||= Torch.device(Torch::CUDA.available? ? "cuda" : "cpu")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @parameter device [Torch::Device, String] e.g. `"cpu"` or `"cuda:0"`.
|
|
41
|
+
def device=(device)
|
|
42
|
+
@device = device.is_a?(Torch::Device) ? device : Torch.device(device)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/license.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright, 2026, by Mauro Tortonesi.
|
|
4
|
+
Copyright, 2021-2024, by Tommaso Carraro (LTNtorch, from which this library is ported).
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
data/readme.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# LTNtorch-rb
|
|
2
|
+
|
|
3
|
+
An implementation of [Logic Tensor Networks](https://arxiv.org/abs/2012.13635)
|
|
4
|
+
(LTN) on top of [torch.rb](https://github.com/ankane/torch.rb), ported from
|
|
5
|
+
[LTNtorch](https://github.com/tommasocarraro/LTNtorch), the PyTorch
|
|
6
|
+
implementation by Tommaso Carraro. It is developed at the [Big Data and
|
|
7
|
+
Compute Continuum Research Lab](https://bdcc.unife.it) of the University of
|
|
8
|
+
Ferrara, Italy, as a companion to the
|
|
9
|
+
[reinforce](https://github.com/DSG-UniFE/reinforce) Reinforcement Learning
|
|
10
|
+
library.
|
|
11
|
+
|
|
12
|
+
LTN is a neuro-symbolic framework: predicates, functions and constants of a
|
|
13
|
+
first-order logic language are grounded as (possibly learnable) tensors and
|
|
14
|
+
neural networks, and the axioms of a knowledge base are evaluated with
|
|
15
|
+
differentiable fuzzy logic semantics. Maximizing the satisfaction of the
|
|
16
|
+
axioms becomes the training objective.
|
|
17
|
+
|
|
18
|
+
**Status:** under active development. The core of LTN (groundings,
|
|
19
|
+
predicates, functions, connectives, quantifiers, guarded and diagonal
|
|
20
|
+
quantification, and the fuzzy operators) is ported, and checked against
|
|
21
|
+
LTNtorch's results; see `changelog.md` for progress and for the
|
|
22
|
+
differences from LTNtorch.
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
LTNtorch-rb requires the [torch.rb](https://rubygems.org/gems/torch-rb) gem,
|
|
27
|
+
which provides Ruby bindings for LibTorch (the C++ core of PyTorch). torch.rb
|
|
28
|
+
compiles against exactly one LibTorch version (0.27.x needs LibTorch 2.15.x).
|
|
29
|
+
|
|
30
|
+
On macOS you can install LibTorch with Homebrew:
|
|
31
|
+
|
|
32
|
+
$ brew install pytorch
|
|
33
|
+
|
|
34
|
+
Otherwise, download LibTorch from [the PyTorch Web site](https://pytorch.org/),
|
|
35
|
+
extract it, and point Bundler at it:
|
|
36
|
+
|
|
37
|
+
$ bundle config build.torch-rb --with-torch-dir=/path/to/libtorch
|
|
38
|
+
|
|
39
|
+
For more info, see [torch.rb's readme](https://github.com/ankane/torch.rb).
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
Add the gem to your application's gems.rb (or Gemfile):
|
|
44
|
+
|
|
45
|
+
$ bundle add ltntorch
|
|
46
|
+
|
|
47
|
+
or install it directly:
|
|
48
|
+
|
|
49
|
+
$ gem install ltntorch
|
|
50
|
+
|
|
51
|
+
Then:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
require "ltn"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
Learn a predicate `A` from a knowledge base saying that it holds for one
|
|
60
|
+
cluster of points and not for another:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
require "ltn"
|
|
64
|
+
|
|
65
|
+
positives = Torch.randn(50, 2) * 0.5 + 1.0
|
|
66
|
+
negatives = Torch.randn(50, 2) * 0.5 - 1.0
|
|
67
|
+
|
|
68
|
+
# A learnable predicate: any Torch::NN::Module returning truth values in [0, 1].
|
|
69
|
+
a = LTN::Predicate.new(
|
|
70
|
+
Torch::NN::Sequential.new(
|
|
71
|
+
Torch::NN::Linear.new(2, 8), Torch::NN::ELU.new,
|
|
72
|
+
Torch::NN::Linear.new(8, 1), Torch::NN::Sigmoid.new
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
not_ = LTN::Connective.new(LTN::FuzzyOps::NotStandard.new)
|
|
77
|
+
forall = LTN::Quantifier.new(LTN::FuzzyOps::AggregPMeanError.new(p: 2), :forall)
|
|
78
|
+
sat_agg = LTN::FuzzyOps::SatAgg.new
|
|
79
|
+
|
|
80
|
+
x_pos = LTN::Variable.new("x_pos", positives)
|
|
81
|
+
x_neg = LTN::Variable.new("x_neg", negatives)
|
|
82
|
+
optimizer = Torch::Optim::Adam.new(a.parameters, lr: 0.01)
|
|
83
|
+
|
|
84
|
+
200.times do
|
|
85
|
+
optimizer.zero_grad
|
|
86
|
+
# forall x_pos: A(x_pos), and forall x_neg: not A(x_neg)
|
|
87
|
+
sat = sat_agg.call(
|
|
88
|
+
forall.call(x_pos, a.call(x_pos)),
|
|
89
|
+
forall.call(x_neg, not_.call(a.call(x_neg)))
|
|
90
|
+
)
|
|
91
|
+
(1.0 - sat).backward
|
|
92
|
+
optimizer.step
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Predicates and functions can also be grounded by a block, e.g.
|
|
97
|
+
`LTN::Predicate.new { |x| Torch.sigmoid(x.sum(1)) }`, and quantifiers can be
|
|
98
|
+
guarded by a condition on the individuals:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
forall.call(x, a.call(x), cond_vars: [x]) { |x| x.value.sum(1).gt(0.0) }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Examples and tutorials
|
|
105
|
+
|
|
106
|
+
`examples/` contains Ruby ports of LTNtorch's seven examples, and
|
|
107
|
+
`tutorials/` of its four tutorials, as scripts:
|
|
108
|
+
|
|
109
|
+
| Script | What it shows |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `tutorials/1_grounding_non_logical_symbols.rb` | Constants, predicates, functions and variables |
|
|
112
|
+
| `tutorials/2_grounding_connectives.rb` | Connectives, quantifiers, diagonal and guarded quantification |
|
|
113
|
+
| `tutorials/2b_operators_and_gradients.rb` | Choosing fuzzy operators, and their gradients |
|
|
114
|
+
| `tutorials/3_knowledgebase_and_learning.rb` | Learning from a knowledge base, in mini-batches too |
|
|
115
|
+
| `examples/1_binary_classification.rb` | A predicate learned from positive and negative examples |
|
|
116
|
+
| `examples/2_multi_class_single_label_classification.rb` | Iris, with a softmax classifier as predicate |
|
|
117
|
+
| `examples/3_multi_class_multi_label_classification.rb` | Crabs, with mutually exclusive labels, and queries |
|
|
118
|
+
| `examples/4_semi_supervised_pattern_recognition.rb` | MNIST digits learned from their sums only (downloads MNIST; `--default-init` for the LTN paper's accuracy) |
|
|
119
|
+
| `examples/5_regression.rb` | Real estate prices, with a smooth equality and diagonal quantification |
|
|
120
|
+
| `examples/6_clustering.rb` | Clustering without labels, with guarded quantification |
|
|
121
|
+
| `examples/7_learning_embeddings_with_ltn.rb` | Smokers, friends and cancer: learned embeddings of individuals |
|
|
122
|
+
|
|
123
|
+
Run them with, e.g., `bundle exec ruby examples/6_clustering.rb`; set
|
|
124
|
+
`EPOCHS` to train for fewer epochs. Their results are close to those of
|
|
125
|
+
LTNtorch's notebooks, and `bundle exec sus benchmark/examples.rb` checks
|
|
126
|
+
that they keep learning.
|
|
127
|
+
|
|
128
|
+
The API follows LTNtorch's, so its
|
|
129
|
+
[documentation](https://tommasocarraro.github.io/LTNtorch/) and
|
|
130
|
+
[tutorials](https://github.com/tommasocarraro/LTNtorch/tree/main/tutorials)
|
|
131
|
+
apply, with Ruby conventions: optional arguments are keywords, symbols are
|
|
132
|
+
called with `#call`, and quantifiers take `:forall`/`:exists`.
|
|
133
|
+
|
|
134
|
+
## Contributing
|
|
135
|
+
|
|
136
|
+
See `CONTRIBUTING.md` for setup, tests, linting and coverage.
|
|
137
|
+
|
|
138
|
+
1. Fork it.
|
|
139
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
140
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
|
141
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
|
142
|
+
5. Create new Pull Request.
|
|
143
|
+
|
|
144
|
+
## Credits and citation
|
|
145
|
+
|
|
146
|
+
LTNtorch-rb is a port of LTNtorch; if you use it in your work, please also
|
|
147
|
+
cite the original software and the LTN paper:
|
|
148
|
+
|
|
149
|
+
- T. Carraro, *LTNtorch: PyTorch implementation of Logic Tensor Networks*,
|
|
150
|
+
[doi:10.5281/zenodo.6394282](https://doi.org/10.5281/zenodo.6394282).
|
|
151
|
+
- S. Badreddine, A. d'Avila Garcez, L. Serafini, M. Spranger, *Logic Tensor
|
|
152
|
+
Networks*, Artificial Intelligence 303, 2022,
|
|
153
|
+
[arXiv:2012.13635](https://arxiv.org/abs/2012.13635).
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
This software is available as open source under the terms of the [MIT
|
|
158
|
+
License](https://opensource.org/licenses/MIT); see `license.md`.
|
|
159
|
+
|
|
160
|
+
### Developer Certificate of Origin
|
|
161
|
+
|
|
162
|
+
This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
|
|
163
|
+
|
|
164
|
+
### Contributor Covenant
|
|
165
|
+
|
|
166
|
+
This project is governed by [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
|