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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +34 -0
  3. data/CONTRIBUTING.md +66 -0
  4. data/changelog.md +68 -0
  5. data/code_of_conduct.md +84 -0
  6. data/examples/1_binary_classification.rb +108 -0
  7. data/examples/2_multi_class_single_label_classification.rb +130 -0
  8. data/examples/3_multi_class_multi_label_classification.rb +158 -0
  9. data/examples/4_semi_supervised_pattern_recognition.rb +249 -0
  10. data/examples/5_regression.rb +103 -0
  11. data/examples/6_clustering.rb +126 -0
  12. data/examples/7_learning_embeddings_with_ltn.rb +166 -0
  13. data/examples/datasets/crabs.dat +201 -0
  14. data/examples/datasets/iris_test.csv +31 -0
  15. data/examples/datasets/iris_training.csv +121 -0
  16. data/examples/datasets/readme.md +9 -0
  17. data/examples/datasets/real-estate.csv +415 -0
  18. data/examples/support/example.rb +92 -0
  19. data/examples/support/mnist.rb +77 -0
  20. data/lib/ltn/connective.rb +56 -0
  21. data/lib/ltn/constant.rb +30 -0
  22. data/lib/ltn/diag.rb +54 -0
  23. data/lib/ltn/function.rb +28 -0
  24. data/lib/ltn/fuzzy_ops.rb +428 -0
  25. data/lib/ltn/lambda_model.rb +24 -0
  26. data/lib/ltn/ltn_object.rb +47 -0
  27. data/lib/ltn/model_wrapper.rb +72 -0
  28. data/lib/ltn/predicate.rb +38 -0
  29. data/lib/ltn/process_ltn_objects.rb +58 -0
  30. data/lib/ltn/quantifier.rb +176 -0
  31. data/lib/ltn/variable.rb +49 -0
  32. data/lib/ltn/version.rb +8 -0
  33. data/lib/ltn.rb +45 -0
  34. data/license.md +22 -0
  35. data/readme.md +166 -0
  36. data/sig/ltn.rbs +207 -0
  37. data/tutorials/1_grounding_non_logical_symbols.rb +146 -0
  38. data/tutorials/2_grounding_connectives.rb +126 -0
  39. data/tutorials/2b_operators_and_gradients.rb +78 -0
  40. data/tutorials/3_knowledgebase_and_learning.rb +124 -0
  41. 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
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Mauro Tortonesi.
5
+
6
+ module LTN
7
+ VERSION = "0.1.0"
8
+ end
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.