aGrUM 3.0.0
a C++ library for (probabilistic) graphical models
DAG2BNLearner_tpl.h
Go to the documentation of this file.
1/****************************************************************************
2 * This file is part of the aGrUM/pyAgrum library. *
3 * *
4 * Copyright (c) 2005-2026 by *
5 * - Pierre-Henri WUILLEMIN(_at_LIP6) *
6 * - Christophe GONZALES(_at_AMU) *
7 * *
8 * The aGrUM/pyAgrum library is free software; you can redistribute it *
9 * and/or modify it under the terms of either : *
10 * *
11 * - the GNU Lesser General Public License as published by *
12 * the Free Software Foundation, either version 3 of the License, *
13 * or (at your option) any later version, *
14 * - the MIT license (MIT), *
15 * - or both in dual license, as here. *
16 * *
17 * (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html) *
18 * *
19 * This aGrUM/pyAgrum library is distributed in the hope that it will be *
20 * useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
21 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS *
22 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, *
25 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR *
26 * OTHER DEALINGS IN THE SOFTWARE. *
27 * *
28 * See LICENCES for more details. *
29 * *
30 * SPDX-FileCopyrightText: Copyright 2005-2026 *
31 * - Pierre-Henri WUILLEMIN(_at_LIP6) *
32 * - Christophe GONZALES(_at_AMU) *
33 * SPDX-License-Identifier: LGPL-3.0-or-later OR MIT *
34 * *
35 * Contact : info_at_agrum_dot_org *
36 * homepage : http://agrum.gitlab.io *
37 * gitlab : https://gitlab.com/agrumery/agrum *
38 * *
39 ****************************************************************************/
40
41#pragma once
42
49
50#include <agrum/BN/learning/paramUtils/DAG2BNLearner.h> // to ease IDE parser
51
52namespace gum {
53
54 namespace learning {
55
57 template < GUM_Numeric GUM_SCALAR >
58 void DAG2BNLearner::_probaVarReordering_(gum::Tensor< GUM_SCALAR >& pot,
59 const gum::Tensor< GUM_SCALAR >& other_pot) {
60 // check that the variables are identical
61 if (!pot.variablesSequence().diffSet(other_pot.variablesSequence()).empty()) {
62 GUM_ERROR(gum::CPTError, "the tensors do not have the same variables")
63 }
64
65 // perform the copy
66 Instantiation i(other_pot);
67 Instantiation j(pot);
68 for (i.setFirst(); !i.end(); ++i) {
69 j.setVals(i);
70 pot.set(j, other_pot[i]);
71 }
72 }
73
75 template < GUM_Numeric GUM_SCALAR >
76 BayesNet< GUM_SCALAR > DAG2BNLearner::createBN(ParamEstimator& estimator, const DAG& dag) {
77 return DAG2BNLearner()._createBN_(estimator, dag, false);
78 }
79
81 template < GUM_Numeric GUM_SCALAR >
82 BayesNet< GUM_SCALAR > DAG2BNLearner::_createBN_(ParamEstimator& estimator,
83 const DAG& dag,
84 const bool compute_log_likelihood) {
85 BayesNet< GUM_SCALAR > bn;
86 log_likelihood_EM_ = 0.0;
87
88 // create a bn with dummy parameters corresponding to the dag
89 const auto& node2cols = estimator.nodeId2Columns();
90 const auto& database = estimator.database();
91 if (node2cols.empty()) {
92 for (const auto id: dag) {
93 bn.add(dynamic_cast< const DiscreteVariable& >(database.variable(id)), id);
94 }
95 } else {
96 for (const auto id: dag) {
97 const std::size_t col = node2cols.second(id);
98 bn.add(dynamic_cast< const DiscreteVariable& >(database.variable(col)), id);
99 }
100 }
101
102 // add the arcs
103 bn.beginTopologyTransformation();
104 for (const auto& arc: dag.arcs()) {
105 bn.addArc(arc.tail(), arc.head());
106 }
107 bn.endTopologyTransformation();
108
109 // estimate the parameters
110 const VariableNodeMap& varmap = bn.variableNodeMap();
111 for (const auto id: dag) {
112 // get the CPT of node id and its variables in the correct order
113 auto& pot = const_cast< Tensor< GUM_SCALAR >& >(bn.cpt(id));
114 const auto& vars = pot.variablesSequence();
115
116 // get the conditioning variables: they are all the variables except
117 // the last one in pot
118 std::vector< NodeId > conditioning_ids(vars.size() - 1);
119 for (auto i = std::size_t(1); i < vars.size(); ++i) {
120 conditioning_ids[i - 1] = varmap.get(*(vars[i]));
121 }
122
123 log_likelihood_EM_
124 += estimator.setParameters(id, conditioning_ids, pot, compute_log_likelihood);
125 }
126
127 return bn;
128 }
129
131 template < GUM_Numeric GUM_SCALAR >
132 BayesNet< GUM_SCALAR > DAG2BNLearner::createBNwithEM(ParamEstimator& bootstrap_estimator,
133 ParamEstimator& EM_estimator,
134 const DAG& dag) {
135 // for EM estimations, we need to disable caches
136 bootstrap_estimator.clear();
137 EM_estimator.clear();
138
139 // bootstrap EM by learning an initial model
140 BayesNet< GUM_SCALAR > bn = createBN< GUM_SCALAR >(bootstrap_estimator, dag);
141
142 return _performEM_(bootstrap_estimator, EM_estimator, std::move(bn));
143 }
144
146 template < GUM_Numeric GUM_SCALAR >
147 BayesNet< GUM_SCALAR > DAG2BNLearner::createBNwithEM(ParamEstimator& bootstrap_estimator,
148 ParamEstimator& EM_estimator,
149 const BayesNet< GUM_SCALAR >& bn) {
150 // for EM estimations, we need to disable caches
151 bootstrap_estimator.clear();
152 EM_estimator.clear();
153
154 auto bn_copy(bn);
155 return createBNwithEM(bootstrap_estimator, EM_estimator, std::move(bn_copy));
156 }
157
159 template < GUM_Numeric GUM_SCALAR >
160 BayesNet< GUM_SCALAR > DAG2BNLearner::createBNwithEM(ParamEstimator& bootstrap_estimator,
161 ParamEstimator& EM_estimator,
162 BayesNet< GUM_SCALAR >&& bn) {
163 // estimate the parameters of the fully zeroed CPTs using the bootstrap estimator
164 const VariableNodeMap& varmap = bn.variableNodeMap();
165 for (const auto id: bn.internalDag()) {
166 // get the CPT of node id and its variables in the correct order
167 auto& pot = const_cast< Tensor< GUM_SCALAR >& >(bn.cpt(id));
168
169 // check if the CPT contains only zeroes
170 bool all_zeroed = true;
171 for (gum::Instantiation inst(pot); !inst.end(); inst.inc()) {
172 if (pot[inst] != 0.0) {
173 all_zeroed = false;
174 break;
175 }
176 }
177
178 // estimate the initial parameters of pot if all_zeroed
179 if (all_zeroed) {
180 // get the conditioning variables: they are all the variables except
181 // the first one in pot
182 const auto& vars = pot.variablesSequence();
183 std::vector< NodeId > conditioning_ids(vars.size() - 1);
184 for (auto i = std::size_t(1); i < vars.size(); ++i) {
185 conditioning_ids[i - 1] = varmap.get(*(vars[i]));
186 }
187
188 // estimate the initial parameters of pot
189 bootstrap_estimator.setParameters(id, conditioning_ids, pot, false);
190 }
191 }
192
193 return _performEM_(bootstrap_estimator, EM_estimator, std::move(bn));
194 }
195
197 template < GUM_Numeric GUM_SCALAR >
198 BayesNet< GUM_SCALAR > DAG2BNLearner::_performEM_(ParamEstimator& bootstrap_estimator,
199 ParamEstimator& EM_estimator,
200 BayesNet< GUM_SCALAR >&& bn) {
201 // if there exist no missing value, there is no need to apply EM
202 if (!EM_estimator.database().hasMissingValues()) {
203 // here we start/stop the approx scheme to be able to display the number
204 // of EM iterations
207
208 auto bn_copy(bn);
209 return bn_copy;
210 }
211
212 if (!this->isEnabledMinEpsilonRate() && !this->isEnabledEpsilon() && !this->isEnabledMaxIter()
213 && !this->isEnabledMaxTime()) {
215 "EM cannot be executed because no stopping criterion among "
216 << "{min rate, min diff, max iter, max time} has been selected")
217 }
218
219 // as bn will be modified, be sure that the DAG is kept unchanged
220 const DAG dag = bn.internalDag();
221
222 // perturb the CPTs to initialize EM
223 if (noiseEM_ != 0.0) {
224 for (const auto& node: bn.nodes()) {
225 bn.cpt(node).noising(noiseEM_).normalizeAsCPT();
226 }
227 }
228
229 // perform EM
230 EM_estimator.setBayesNet(bn);
232
233 // compute the initial value of the log-likelihood
234 log_likelihood_EM_ = 0.0;
235 const VariableNodeMap& varmap = bn.variableNodeMap();
236 EM_estimator.counter_.clear(); // for EM estimations, we need to disable caches
237 for (const auto& node: bn.nodes()) {
238 // get node's CPT and its conditioning variables: they are all the
239 // variables except the first one in pot
240 const auto& pot = const_cast< Tensor< GUM_SCALAR >& >(bn.cpt(node));
241 const auto& vars = pot.variablesSequence();
242 std::vector< NodeId > conditioning_ids(vars.size() - 1);
243 for (auto i = std::size_t(1); i < vars.size(); ++i) {
244 conditioning_ids[i - 1] = varmap.get(*(vars[i]));
245 }
246
247 // compute the log-likelihood
248 IdCondSet idset(node, conditioning_ids, true);
249 const auto& N_ijk = EM_estimator.counter_.counts(idset, true);
250 Instantiation inst(pot);
251 for (std::size_t k = 0, end = pot.domainSize(); k < end; ++k, inst.inc()) {
252 if (N_ijk[k]) { log_likelihood_EM_ += N_ijk[k] * std::log(pot[inst]); }
253 }
254 }
255 double current_log_likelihood = log_likelihood_EM_;
256
257 // it may happen (luckily very seldom) that EM will decrease the
258 // log-likelihood instead of increasing it (see Table 5 on p28 of
259 // https://faculty.washington.edu/fxia/courses/LING572/EM_collins97.pdf
260 // for an example of such a behavior). In this case, instead of iterating
261 // EM and producing worst and worst Bayes nets, we stop the iterations
262 // early and we return the best Bayes net found so far.
263 BayesNet< GUM_SCALAR > best_bn;
264 bool must_return_best_bn = false;
265 unsigned int nb_dec_likelihood_iter = 0;
266 double delta = 0;
267
268 do {
269 // bugfix for parallel execution of VariableElimination
270 const auto& xdag = bn.internalDag();
271 for (const auto node: xdag) {
272 xdag.parents(node);
273 xdag.children(node);
274 }
275
276 EM_estimator.counter_.clear(); // for EM estimations, we need to disable caches
277 BayesNet< GUM_SCALAR > new_bn = _createBN_< GUM_SCALAR >(EM_estimator, dag, true);
279
280 if (log_likelihood_EM_ >= current_log_likelihood) {
281 // here, we increased the log-likelihood, it is fine
282 nb_dec_likelihood_iter = 0;
283 must_return_best_bn = false;
284 } else {
285 // here, we decreased the log-likelihood, so we should keep track of the
286 // best Bayes net found so far. If we decreased too many times the
287 // log-likelihood, we should even stop EM
288 ++nb_dec_likelihood_iter;
289 if (nb_dec_likelihood_iter == 1) {
290 best_bn = bn; // bn is the Bayes net computed at the previous step
291 must_return_best_bn = true;
292 }
293 if (nb_dec_likelihood_iter > max_nb_dec_likelihood_iter_) {
295 return best_bn;
296 }
297 }
298
299 // compute the difference in log-likelihood
300 delta = log_likelihood_EM_ - current_log_likelihood;
301 current_log_likelihood = log_likelihood_EM_;
302
303 bn = std::move(new_bn);
304 } while (continueApproximationScheme(this->isEnabledMinEpsilonRate() ? -log_likelihood_EM_
305 : delta));
306
307 stopApproximationScheme(); // just to be sure of the approximationScheme
308 // has been notified of the end of loop
309
310 return must_return_best_bn ? best_bn : bn;
311 }
312
313 } // namespace learning
314
315} /* namespace gum */
A class that, given a structure and a parameter estimator returns a full Bayes net.
void updateApproximationScheme(unsigned int incr=1)
Update the scheme w.r.t the new error and increment steps.
bool isEnabledEpsilon() const override
Returns true if stopping criterion on epsilon is enabled, false otherwise.
bool isEnabledMaxTime() const override
Returns true if stopping criterion on timeout is enabled, false otherwise.
bool isEnabledMinEpsilonRate() const override
Returns true if stopping criterion on epsilon rate is enabled, false otherwise.
bool continueApproximationScheme(double error)
Update the scheme w.r.t the new error.
void initApproximationScheme()
Initialise the scheme.
void stopApproximationScheme()
Stop the approximation scheme.
bool isEnabledMaxIter() const override
Returns true if stopping criterion on max iterations is enabled, false otherwise.
Base class for dag.
Definition DAG.h:121
Base class for discrete random variable.
Class for assigning/browsing values to tuples of discrete variables.
bool end() const
Returns true if the Instantiation reached the end.
const Sequence< const DiscreteVariable * > & variablesSequence() const final
Returns a const ref to the sequence of DiscreteVariable*.
void set(const Instantiation &i, const GUM_ELEMENT &value) const final
Default implementation of MultiDimContainer::set().
Size domainSize() const final
Returns the product of the variables domain size.
Exception : operation not allowed.
Container used to map discrete variables with nodes.
const DiscreteVariable & get(NodeId id) const
Returns a discrete variable given it's node id.
static BayesNet< GUM_SCALAR > createBN(ParamEstimator &estimator, const DAG &dag)
create a BN from a DAG using a one pass generator (typically ML)
DAG2BNLearner()
default constructor
BayesNet< GUM_SCALAR > createBNwithEM(ParamEstimator &bootstrap_estimator, ParamEstimator &EM_estimator, const DAG &dag)
creates a BN with a given structure (dag) using the EM algorithm
bool hasMissingValues() const
indicates whether the database contains some missing values
The base class for estimating parameters of CPTs.
RecordCounter counter_
the record counter used to parse the database
const Bijection< NodeId, std::size_t > & nodeId2Columns() const
returns the mapping from ids to column positions in the database
double setParameters(const NodeId target_node, const std::vector< NodeId > &conditioning_nodes, Tensor< GUM_SCALAR > &pot, const bool compute_log_likelihood=false)
sets a CPT's parameters and, possibly, return its log-likelihhod
virtual void clear()
clears all the data structures from memory
void setBayesNet(const BayesNet< GUM_SCALAR > &new_bn)
assign a new Bayes net to all the counter's generators depending on a BN
const DatabaseTable & database() const
returns the database on which we perform the counts
void clear()
clears all the last database-parsed counting from memory
const std::vector< double > & counts(const IdCondSet &ids, const bool check_discrete_vars=false)
returns the counts over all the variables in an IdCondSet
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
include the inlined functions if necessary
Definition CSVParser.h:55
gum is the global namespace for all aGrUM entities
Definition agrum.h:46