From c19f1fd93d9379e0aed2e1d7f3f5e98e4b91a4d3 Mon Sep 17 00:00:00 2001 From: dblumenthal Date: Mon, 8 Oct 2018 11:20:05 +0200 Subject: [PATCH] implemented HED --- src/env/common_types.hpp | 3 +- src/env/ged_env.ipp | 3 + src/methods/all_methods.hpp | 6 +- src/methods/hed.hpp | 87 ++++++++++ src/methods/hed.ipp | 219 ++++++++++++++++++++++++++ tests/unit_tests/src/ged_env_test.cpp | 12 ++ 6 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 src/methods/hed.hpp create mode 100644 src/methods/hed.ipp diff --git a/src/env/common_types.hpp b/src/env/common_types.hpp index 9902315fa2..afb0c1aa8c 100644 --- a/src/env/common_types.hpp +++ b/src/env/common_types.hpp @@ -172,7 +172,8 @@ struct Options { BIPARTITE_ML, //!< Selects ged::BipartiteML. REFINE, //!< Selects ged::Refine. BP_BEAM, //!< Selects ged::BPBeam. - SIMULATED_ANNEALING //!< Selects ged::SimulatedAnnealing. + SIMULATED_ANNEALING, //!< Selects ged::SimulatedAnnealing. + HED //!< Selects ged::HED. }; /*! diff --git a/src/env/ged_env.ipp b/src/env/ged_env.ipp index 86bdc72c48..fa2b3398d0 100644 --- a/src/env/ged_env.ipp +++ b/src/env/ged_env.ipp @@ -399,6 +399,9 @@ set_method(Options::GEDMethod method, const std::string & options) { case Options::GEDMethod::SIMULATED_ANNEALING: ged_method_ = new SimulatedAnnealing(ged_data_); break; + case Options::GEDMethod::HED: + ged_method_ = new HED(ged_data_); + break; #ifdef GUROBI case Options::GEDMethod::F1: ged_method_ = new F1(ged_data_); diff --git a/src/methods/all_methods.hpp b/src/methods/all_methods.hpp index fb00a28348..ff2eeeaf00 100644 --- a/src/methods/all_methods.hpp +++ b/src/methods/all_methods.hpp @@ -41,10 +41,11 @@ // Declarations of concrete derived classes of GEDMethod. #include "branch_tight.hpp" // Declares BranchTight. Dependencies: "ged_method.hpp". -#include "anchor_aware_ged.hpp" // Declares Exact. Dependencies: "ged_method.hpp". +#include "anchor_aware_ged.hpp" // Declares Exact. Dependencies: "ged_method.hpp". #include "partition.hpp" // Declares Partition. Dependencies: "ged_method.hpp". #include "hybrid.hpp" // Declares Hybrid. Dependencies: "ged_method.hpp", "partition.hpp". #include "branch_compact.hpp" // Declares BranchCompact. Dependencies: "ged_method.hpp". +#include "hed.hpp" // Declares HED. Dependencies: "ged_method.hpp". #include "simulated_annealing.hpp" // Declares SimulatedAnnealing. Dependencies: "ged_method.hpp", "lsape_based_method.hpp" // Declarations of concrete derived classes of MIPBasedMethod. @@ -91,10 +92,11 @@ // Definitions of concrete derived classes of GEDMethod. #include "branch_tight.ipp" // Defines BranchTight. Dependencies: "branch_tight.hpp", "ged_method.hpp". -#include "anchor_aware_ged.ipp" // Defines Exact. Dependencies: "exact.hpp", "ged_method.hpp", "ipfp.hpp". +#include "anchor_aware_ged.ipp" // Defines Exact. Dependencies: "exact.hpp", "ged_method.hpp", "ipfp.hpp". #include "partition.ipp" // Defines Partition. Dependencies: "partition.hpp", "ged_method.hpp". #include "hybrid.ipp" // Defines Hybrid. Dependencies: "hybrid.hpp", "ged_method.hpp", "partition.hpp", "branch_uniform.hpp". #include "branch_compact.ipp" // Defines BranchCompact. Dependencies: "branch_compact.hpp", "ged_method.hpp". +#include "hed.ipp" // Defines HED. Dependencies: "hed.hpp", "ged_method.hpp". #include "simulated_annealing.ipp" // Defines SimulatedAnnealing. Dependencies: "simulated_annealing.hpp", "ged_method.hpp", "lsape_based_method.hpp", "bipartite.hpp", "branch_hpp", "branch_fast.hpp", "node.hpp", "ring.hpp", "subgraph.hpp", "walks.hpp". // Definitions of concrete derived classes of MIPBasedMethod. diff --git a/src/methods/hed.hpp b/src/methods/hed.hpp new file mode 100644 index 0000000000..341b511773 --- /dev/null +++ b/src/methods/hed.hpp @@ -0,0 +1,87 @@ +/*************************************************************************** + * * + * Copyright (C) 2018 by David B. Blumenthal * + * * + * This file is part of GEDLIB. * + * * + * GEDLIB is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as published * + * by the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * GEDLIB is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with GEDLIB. If not, see . * + * * + ***************************************************************************/ + +/*! + * @file hed.hpp + * @brief ged::HED class declaration. + */ + +#ifndef SRC_METHODS_HED_HPP_ +#define SRC_METHODS_HED_HPP_ + +namespace ged { + +/*! + * @brief Computes a lower bound for general edit costs. + * @details Implements the Hausdorff Edit Distance (HED) suggested in: + * - A. Fischer, C. Y. Suen, V. Frinken, K. Riesen, and H. Bunke: + * “Approximation of graph edit distance based on Hausdorff matching”, + * https:://doi.org/10.1016/j.patcog.2014.07.015 + * + * Supports the following options: + * | \--@ @ | modified parameter | default | more information | + * | ------------------------------ | ------------------ | -------- | ---------------- | + * | \--threads @ | number of threads | 1 | can be used by derived classes | + * | \--lsape-model ECBP\|EBP\|FLWC\|FLCC\|FBP\|SFBP\|FBP0 | model for optimally solving LSAPE | @p ECBP | ged::LSAPESolver::Model | + */ +template +class HED : public GEDMethod { + +public: + + virtual ~HED(); + + HED(const GEDData & ged_data); + +private: + + LSAPESolver::Model lsape_model_; + + std::size_t num_threads_; + + // Inherited member functions from GEDMethod. + + virtual void ged_run_(const GEDGraph & g, const GEDGraph & h, Result & result) final; + + virtual bool ged_parse_option_(const std::string & option, const std::string & arg) final; + + virtual std::string ged_valid_options_string_() const final; + + virtual void ged_set_default_options_() final; + + // Helper member functions. + + void populate_instance_(const GEDGraph & g, const GEDGraph & h, DMatrix & lsape_instance) const; + + double compute_substitution_cost_(const GEDGraph & g, const GEDGraph & h, GEDGraph::NodeID i, GEDGraph::NodeID k) const; + + double compute_deletion_cost_(const GEDGraph & g, GEDGraph::NodeID i) const; + + double compute_insertion_cost_(const GEDGraph & h, GEDGraph::NodeID k) const; +}; + +} + + + + + +#endif /* SRC_METHODS_HED_HPP_ */ diff --git a/src/methods/hed.ipp b/src/methods/hed.ipp new file mode 100644 index 0000000000..fbd4d1a206 --- /dev/null +++ b/src/methods/hed.ipp @@ -0,0 +1,219 @@ +/*************************************************************************** + * * + * Copyright (C) 2018 by David B. Blumenthal * + * * + * This file is part of GEDLIB. * + * * + * GEDLIB is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as published * + * by the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * GEDLIB is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with GEDLIB. If not, see . * + * * + ***************************************************************************/ + +/*! + * @file hed.ipp + * @brief ged::HED class definition. + */ + +#ifndef SRC_METHODS_HED_IPP_ +#define SRC_METHODS_HED_IPP_ + +namespace ged { + +// === Definitions of destructor and constructor. === +template +HED:: +~HED() {} + +template +HED:: +HED(const GEDData & ged_data) : +GEDMethod(ged_data), +lsape_model_{LSAPESolver::Model::ECBP}, +num_threads_{1} {} + +// === Definitions of member functions inherited from GEDMethod. === +template +void +HED:: +ged_run_(const GEDGraph & g, const GEDGraph & h, Result & result) { + DMatrix lsape_instance(g.num_nodes() + 1, h.num_nodes() + 1); + populate_instance_(g, h, lsape_instance); + double hed{0}; + hed += lsape_instance.matrix().rowwise().minCoeff().sum(); + hed += lsape_instance.matrix().colwise().minCoeff().sum(); + result.set_lower_bound(hed); +} + +template +bool +HED:: +ged_parse_option_(const std::string & option, const std::string & arg) { + if (option == "threads") { + try { + num_threads_ = std::stoul(arg); + } + catch (...) { + throw Error(std::string("Invalid argument \"") + arg + "\" for option threads. Usage: options = \"[--threads ] [...]"); + } + if (num_threads_ <= 0) { + throw Error(std::string("Invalid argument \"") + arg + "\" for option threads. Usage: options = \"[--threads ] [...]"); + } + return true; + } + else if (option == "lsape-model") { + if (arg == "EBP") { + lsape_model_ = LSAPESolver::EBP; + } + else if (arg == "FLWC") { + lsape_model_ = LSAPESolver::FLWC; + } + else if (arg == "FLCC") { + lsape_model_ = LSAPESolver::FLCC; + } + else if (arg == "FBP") { + lsape_model_ = LSAPESolver::FBP; + } + else if (arg == "SFBP") { + lsape_model_ = LSAPESolver::SFBP; + } + else if (arg == "FBP0") { + lsape_model_ = LSAPESolver::FBP0; + } + else if (arg != "ECBP") { + throw ged::Error(std::string("Invalid argument ") + arg + " for option lsape-model. Usage: options = \"[--lsape-model ECBP|EBP|FLWC|FLCC|FBP|SFBP|FBP0] [...]\""); + } + return true; + } + return false; +} + +template +std::string +HED:: +ged_valid_options_string_() const { + return "[--lsape-model ] [--threads ]"; +} + +template +void +HED:: +ged_set_default_options_() { + lsape_model_ = LSAPESolver::ECBP; + num_threads_ = 1; +} + +// === Definitions of private helper member functions. === +template +void +HED:: +populate_instance_(const GEDGraph & g, const GEDGraph & h, DMatrix & lsape_instance) const { + +#ifdef _OPENMP + omp_set_num_threads(this->num_threads_ - 1); +#pragma omp parallel for if(this->num_threads_ > 1) +#endif + for (std::size_t row_in_master = 0; row_in_master < lsape_instance.num_rows(); row_in_master++) { + for (std::size_t col_in_master = 0; col_in_master < lsape_instance.num_cols(); col_in_master++) { + if ((row_in_master < g.num_nodes()) and (col_in_master < h.num_nodes())) { + lsape_instance(row_in_master, col_in_master) = compute_substitution_cost_(g, h, row_in_master, col_in_master); + } + else if (row_in_master < g.num_nodes()) { + lsape_instance(row_in_master, h.num_nodes()) = compute_deletion_cost_(g, row_in_master); + } + else if (col_in_master < h.num_nodes()) { + lsape_instance(g.num_nodes(), col_in_master) = compute_insertion_cost_(h, col_in_master); + } + } + } +} + +template +double +HED:: +compute_substitution_cost_(const GEDGraph & g, const GEDGraph & h, GEDGraph::NodeID i, GEDGraph::NodeID k) const { + // Collect node substitution costs. + double cost{this->ged_data_.node_cost(g.get_node_label(i), h.get_node_label(k))}; + + // Initialize subproblem. + DMatrix subproblem(g.degree(i) + 1, h.degree(k) + 1); + + // Collect edge deletion costs. + std::size_t j{0}; + for (auto ij = g.incident_edges(i).first; ij != g.incident_edges(i).second; ij++, j++) { + subproblem(j, h.degree(k)) = this->ged_data_.edge_cost(g.get_edge_label(*ij), ged::dummy_label()) / 2.0; + } + + // Collect edge insertion costs. + std::size_t l{0}; + for (auto kl = h.incident_edges(k).first; kl != h.incident_edges(k).second; kl++, l++) { + subproblem(g.degree(i), l) = this->ged_data_.edge_cost(ged::dummy_label(), h.get_edge_label(*kl)) / 2.0; + } + j = 0; + + // Collect edge relabelling costs. + for (auto ij = g.incident_edges(i).first; ij != g.incident_edges(i).second; ij++, j++) { + l = 0; + for (auto kl = h.incident_edges(k).first; kl != h.incident_edges(k).second; kl++, l++) { + subproblem(j, l) = this->ged_data_.edge_cost(g.get_edge_label(*ij), h.get_edge_label(*kl)) / 2.0; + } + } + + // Solve subproblem. + LSAPESolver subproblem_solver(&subproblem); + subproblem_solver.set_model(this->lsape_model_); + subproblem_solver.solve(); + + // Update and return overall substitution cost. + cost += subproblem_solver.minimal_cost(); + return cost / 2; +} + +template +double +HED:: +compute_deletion_cost_(const GEDGraph & g, GEDGraph::NodeID i) const { + // Collect node deletion cost. + double cost{this->ged_data_.node_cost(g.get_node_label(i), ged::dummy_label())}; + + // Collect edge deletion costs. + auto incident_edges_i = g.incident_edges(i); + for (auto ij = incident_edges_i.first; ij != incident_edges_i.second; ij++) { + cost += this->ged_data_.edge_cost(g.get_edge_label(*ij), ged::dummy_label()) / 2.0; + } + + // Return overall deletion cost. + return cost; +} + +template +double +HED:: +compute_insertion_cost_(const GEDGraph & h, GEDGraph::NodeID k) const { + // Collect node insertion cost. + double cost{this->ged_data_.node_cost(ged::dummy_label(), h.get_node_label(k))}; + + // Collect edge insertion costs. + auto incident_edges_k = h.incident_edges(k); + for (auto kl = incident_edges_k.first; kl != incident_edges_k.second; kl++) { + cost += this->ged_data_.edge_cost(ged::dummy_label(), h.get_edge_label(*kl)) / 2.0; + } + + // Return overall insertion cost. + return cost; +} + +} + + + +#endif /* SRC_METHODS_HED_IPP_ */ diff --git a/tests/unit_tests/src/ged_env_test.cpp b/tests/unit_tests/src/ged_env_test.cpp index 2b70506289..cf1b848f4e 100644 --- a/tests/unit_tests/src/ged_env_test.cpp +++ b/tests/unit_tests/src/ged_env_test.cpp @@ -155,6 +155,18 @@ TEST_CASE("testing on MAO graphs") { std::cout << "upper bound = " << env.get_upper_bound(g, h) << ", runtime = " << env.get_runtime(g, h) << "\n"; } + SECTION("HED") { + std::cout << "\n===running HED ===\n"; + env.set_method(ged::Options::GEDMethod::HED, "--threads 1"); + env.run_method(g, h); + std::cout << "\noptions = \"--threads 1\"\n"; + std::cout << "lower bound = " << env.get_lower_bound(g, h) << ", runtime = " << env.get_runtime(g, h) << "\n"; + env.set_method(ged::Options::GEDMethod::HED, "--threads 4"); + env.run_method(g, h); + std::cout << "\noptions = \"--threads 4\"\n"; + std::cout << "lower bound = " << env.get_lower_bound(g, h) << ", runtime = " << env.get_runtime(g, h) << "\n"; + } + #ifdef GUROBI SECTION("BLPNoEdgeLabels") { std::cout << "\n===running BLPNoEdgeLabels ===\n";