1 /**
2  *
3  *   Copyright (c) 2005-2021 by Pierre-Henri WUILLEMIN(_at_LIP6) & Christophe GONZALES(_at_AMU)
4  *   info_at_agrum_dot_org
5  *
6  *  This library is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU Lesser General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This library is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU Lesser General Public License for more details.
15  *
16  *  You should have received a copy of the GNU Lesser General Public License
17  *  along with this library.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 
22 /** @file
23  * @brief the class for estimating parameters of CPTs using Maximum Likelihood
24  *
25  * @author Christophe GONZALES(_at_AMU) and Pierre-Henri WUILLEMIN(_at_LIP6)
26  */
27 #ifndef DOXYGEN_SHOULD_SKIP_THIS
28 
29 namespace gum {
30 
31   namespace learning {
32 
33     /// default constructor
34     template < template < typename > class ALLOC >
ParamEstimatorML(const DBRowGeneratorParser<ALLOC> & parser,const Apriori<ALLOC> & external_apriori,const Apriori<ALLOC> & score_internal_apriori,const std::vector<std::pair<std::size_t,std::size_t>,ALLOC<std::pair<std::size_t,std::size_t>>> & ranges,const Bijection<NodeId,std::size_t,ALLOC<std::size_t>> & nodeId2columns,const typename ParamEstimatorML<ALLOC>::allocator_type & alloc)35     INLINE ParamEstimatorML< ALLOC >::ParamEstimatorML(
36        const DBRowGeneratorParser< ALLOC >&                                 parser,
37        const Apriori< ALLOC >&                                              external_apriori,
38        const Apriori< ALLOC >&                                              score_internal_apriori,
39        const std::vector< std::pair< std::size_t, std::size_t >,
40                           ALLOC< std::pair< std::size_t, std::size_t > > >& ranges,
41        const Bijection< NodeId, std::size_t, ALLOC< std::size_t > >&        nodeId2columns,
42        const typename ParamEstimatorML< ALLOC >::allocator_type&            alloc) :
43         ParamEstimator< ALLOC >(parser,
44                                 external_apriori,
45                                 score_internal_apriori,
46                                 ranges,
47                                 nodeId2columns,
48                                 alloc) {
49       GUM_CONSTRUCTOR(ParamEstimatorML);
50     }
51 
52 
53     /// default constructor
54     template < template < typename > class ALLOC >
ParamEstimatorML(const DBRowGeneratorParser<ALLOC> & parser,const Apriori<ALLOC> & external_apriori,const Apriori<ALLOC> & score_internal_apriori,const Bijection<NodeId,std::size_t,ALLOC<std::size_t>> & nodeId2columns,const typename ParamEstimatorML<ALLOC>::allocator_type & alloc)55     INLINE ParamEstimatorML< ALLOC >::ParamEstimatorML(
56        const DBRowGeneratorParser< ALLOC >&                          parser,
57        const Apriori< ALLOC >&                                       external_apriori,
58        const Apriori< ALLOC >&                                       score_internal_apriori,
59        const Bijection< NodeId, std::size_t, ALLOC< std::size_t > >& nodeId2columns,
60        const typename ParamEstimatorML< ALLOC >::allocator_type&     alloc) :
61         ParamEstimator< ALLOC >(parser,
62                                 external_apriori,
63                                 score_internal_apriori,
64                                 nodeId2columns,
65                                 alloc) {
66       GUM_CONSTRUCTOR(ParamEstimatorML);
67     }
68 
69 
70     /// copy constructor with a given allocator
71     template < template < typename > class ALLOC >
ParamEstimatorML(const ParamEstimatorML<ALLOC> & from,const typename ParamEstimatorML<ALLOC>::allocator_type & alloc)72     INLINE ParamEstimatorML< ALLOC >::ParamEstimatorML(
73        const ParamEstimatorML< ALLOC >&                          from,
74        const typename ParamEstimatorML< ALLOC >::allocator_type& alloc) :
75         ParamEstimator< ALLOC >(from, alloc) {
76       GUM_CONS_CPY(ParamEstimatorML);
77     }
78 
79 
80     /// copy constructor
81     template < template < typename > class ALLOC >
ParamEstimatorML(const ParamEstimatorML<ALLOC> & from)82     INLINE ParamEstimatorML< ALLOC >::ParamEstimatorML(const ParamEstimatorML< ALLOC >& from) :
83         ParamEstimatorML< ALLOC >(from, this->getAllocator()) {}
84 
85 
86     /// move constructor with a given allocator
87     template < template < typename > class ALLOC >
ParamEstimatorML(ParamEstimatorML<ALLOC> && from,const typename ParamEstimatorML<ALLOC>::allocator_type & alloc)88     INLINE ParamEstimatorML< ALLOC >::ParamEstimatorML(
89        ParamEstimatorML< ALLOC >&&                               from,
90        const typename ParamEstimatorML< ALLOC >::allocator_type& alloc) :
91         ParamEstimator< ALLOC >(std::move(from), alloc) {
92       GUM_CONS_MOV(ParamEstimatorML);
93     }
94 
95 
96     /// move constructor
97     template < template < typename > class ALLOC >
ParamEstimatorML(ParamEstimatorML<ALLOC> && from)98     INLINE ParamEstimatorML< ALLOC >::ParamEstimatorML(ParamEstimatorML< ALLOC >&& from) :
99         ParamEstimatorML< ALLOC >(std::move(from), this->getAllocator()) {}
100 
101 
102     /// virtual copy constructor with a given allocator
103     template < template < typename > class ALLOC >
clone(const typename ParamEstimatorML<ALLOC>::allocator_type & alloc)104     ParamEstimatorML< ALLOC >* ParamEstimatorML< ALLOC >::clone(
105        const typename ParamEstimatorML< ALLOC >::allocator_type& alloc) const {
106       ALLOC< ParamEstimatorML< ALLOC > > allocator(alloc);
107       ParamEstimatorML< ALLOC >*         new_score = allocator.allocate(1);
108       try {
109         allocator.construct(new_score, *this, alloc);
110       } catch (...) {
111         allocator.deallocate(new_score, 1);
112         throw;
113       }
114 
115       return new_score;
116     }
117 
118 
119     /// virtual copy constructor
120     template < template < typename > class ALLOC >
clone()121     ParamEstimatorML< ALLOC >* ParamEstimatorML< ALLOC >::clone() const {
122       return clone(this->getAllocator());
123     }
124 
125 
126     /// destructor
127     template < template < typename > class ALLOC >
~ParamEstimatorML()128     ParamEstimatorML< ALLOC >::~ParamEstimatorML() {
129       GUM_DESTRUCTOR(ParamEstimatorML);
130     }
131 
132 
133     /// copy operator
134     template < template < typename > class ALLOC >
135     ParamEstimatorML< ALLOC >&
136        ParamEstimatorML< ALLOC >::operator=(const ParamEstimatorML< ALLOC >& from) {
137       ParamEstimator< ALLOC >::operator=(from);
138       return *this;
139     }
140 
141 
142     /// move operator
143     template < template < typename > class ALLOC >
144     ParamEstimatorML< ALLOC >&
145        ParamEstimatorML< ALLOC >::operator=(ParamEstimatorML< ALLOC >&& from) {
146       ParamEstimator< ALLOC >::operator=(std::move(from));
147       return *this;
148     }
149 
150 
151     /// returns the CPT's parameters corresponding to a given set of nodes
152     template < template < typename > class ALLOC >
parameters(const NodeId target_node,const std::vector<NodeId,ALLOC<NodeId>> & conditioning_nodes)153     std::vector< double, ALLOC< double > > ParamEstimatorML< ALLOC >::parameters(
154        const NodeId                                  target_node,
155        const std::vector< NodeId, ALLOC< NodeId > >& conditioning_nodes) {
156       // create an idset that contains all the nodes in the following order:
157       // first, the target node, then all the conditioning nodes
158       IdCondSet< ALLOC > idset(target_node, conditioning_nodes, true);
159 
160       // get the counts for all the nodes in the idset and add the external and
161       // score internal aprioris
162       std::vector< double, ALLOC< double > > N_ijk(this->counter_.counts(idset, true));
163       const bool informative_external_apriori = this->external_apriori_->isInformative();
164       const bool informative_score_internal_apriori
165          = this->score_internal_apriori_->isInformative();
166       if (informative_external_apriori) this->external_apriori_->addAllApriori(idset, N_ijk);
167       if (informative_score_internal_apriori)
168         this->score_internal_apriori_->addAllApriori(idset, N_ijk);
169 
170 
171       // now, normalize N_ijk
172 
173       // here, we distinguish nodesets with conditioning nodes from those
174       // without conditioning nodes
175       if (!conditioning_nodes.empty()) {
176         // get the counts for all the conditioning nodes, and add them the
177         // external and score internal aprioris
178         std::vector< double, ALLOC< double > > N_ij(
179            this->counter_.counts(idset.conditionalIdCondSet(), false));
180         if (informative_external_apriori)
181           this->external_apriori_->addConditioningApriori(idset, N_ij);
182         if (informative_score_internal_apriori)
183           this->score_internal_apriori_->addConditioningApriori(idset, N_ij);
184 
185         const std::size_t conditioning_domsize = N_ij.size();
186         const std::size_t target_domsize       = N_ijk.size() / conditioning_domsize;
187 
188         // check that all conditioning nodes have strictly positive counts
189         for (std::size_t j = std::size_t(0); j < conditioning_domsize; ++j) {
190           if (N_ij[j] == 0) {
191             // get the domain sizes of the conditioning nodes
192             const std::size_t  cond_nb = conditioning_nodes.size();
193             std::vector< Idx > cond_domsize(cond_nb);
194 
195             const auto& node2cols = this->counter_.nodeId2Columns();
196             const auto& database  = this->counter_.database();
197             if (node2cols.empty()) {
198               for (std::size_t i = std::size_t(0); i < cond_nb; ++i) {
199                 cond_domsize[i] = database.domainSize(conditioning_nodes[i]);
200               }
201             } else {
202               for (std::size_t i = std::size_t(0); i < cond_nb; ++i) {
203                 cond_domsize[i] = database.domainSize(node2cols.second(conditioning_nodes[i]));
204               }
205             }
206 
207             // determine the value of each conditioning variable in N_ij[j]
208             std::vector< Idx > offsets(cond_nb);
209             Idx                offset = 1;
210             std::size_t        i;
211             for (i = std::size_t(0); i < cond_nb; ++i) {
212               offsets[i] = offset;
213               offset *= cond_domsize[i];
214             }
215             std::vector< Idx > values(cond_nb);
216             i      = 0;
217             offset = j;
218             for (Idx jj = cond_nb - 1; i < cond_nb; ++i, --jj) {
219               values[jj] = offset / offsets[jj];
220               offset %= offsets[jj];
221             }
222 
223             // create the error message
224             std::stringstream str;
225             str << "The conditioning set <";
226             bool deja = false;
227             for (i = std::size_t(0); i < cond_nb; ++i) {
228               if (deja)
229                 str << ", ";
230               else
231                 deja = true;
232               std::size_t             col = node2cols.empty() ? conditioning_nodes[i]
233                                                               : node2cols.second(conditioning_nodes[i]);
234               const DiscreteVariable& var
235                  = dynamic_cast< const DiscreteVariable& >(database.variable(col));
236               str << var.name() << "=" << var.labels()[values[i]];
237             }
238             auto target_col     = node2cols.empty() ? target_node : node2cols.second(target_node);
239             const Variable& var = database.variable(target_col);
240             str << "> for target node " << var.name()
241                 << " never appears in the database. Please consider using "
242                 << "priors such as smoothing.";
243 
244             GUM_ERROR(DatabaseError, str.str())
245           }
246         }
247 
248         // normalize the counts
249         for (std::size_t j = std::size_t(0), k = std::size_t(0); j < conditioning_domsize; ++j) {
250           for (std::size_t i = std::size_t(0); i < target_domsize; ++i, ++k) {
251             N_ijk[k] /= N_ij[j];
252           }
253         }
254       } else {
255         // here, there are no conditioning nodes. Hence N_ijk is the marginal
256         // probability distribution over the target node. To normalize it, it
257         // is sufficient to divide each cell by the sum over all the cells
258         double sum = 0;
259         for (const double n_ijk: N_ijk)
260           sum += n_ijk;
261 
262         if (sum != 0) {
263           for (double& n_ijk: N_ijk)
264             n_ijk /= sum;
265         } else {
266           std::stringstream str;
267 
268           const auto& node2cols  = this->counter_.nodeId2Columns();
269           const auto& database   = this->counter_.database();
270           auto        target_col = node2cols.empty() ? target_node : node2cols.second(target_node);
271           const Variable& var    = database.variable(target_col);
272           str << "No data for target node " << var.name()
273               << ". It is impossible to estimate the parameters by maximum "
274                  "likelihood";
275           GUM_ERROR(DatabaseError, str.str())
276         }
277       }
278 
279       return N_ijk;
280     }
281 
282   } /* namespace learning */
283 
284 } /* namespace gum */
285 
286 #endif /* DOXYGEN_SHOULD_SKIP_THIS */
287