Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7c393ff
Add a cross-validation wrapper for optimizers
micyril Jul 26, 2017
f2d11fc
Add grid-search optimization
micyril Jul 26, 2017
15e35b4
Add hyper-parameter tuning
micyril Jul 26, 2017
fa78d07
Add support for metrics that should be maximized
micyril Jul 26, 2017
42d16ee
Add interface for accessing the best model
micyril Jul 27, 2017
33a1120
Fix lambda sets initialization
micyril Jul 27, 2017
dd1db7c
Fix misspellings
micyril Aug 7, 2017
92d75a1
Fix CVFunctionTest
micyril Aug 7, 2017
0b7690c
Fix style
micyril Aug 7, 2017
5eb3dc7
Extend documentation
micyril Aug 9, 2017
56c2ff1
Fix typo
micyril Aug 9, 2017
c6f897f
Make optimal lambdas in the "middle" of the space
micyril Aug 10, 2017
54ac867
Add passing MLAlgorithm type to CVFunction
micyril Aug 10, 2017
46e8c25
Add braces around multiline statement
micyril Aug 10, 2017
7b9604c
Use more verbose names for BAIndex and PIndex
micyril Aug 10, 2017
cce56f5
Use the word "fixed" for "bound"
micyril Aug 10, 2017
85060c8
Revert "Make optimal lambdas in the "middle" of the space"
micyril Aug 11, 2017
6744351
Refactor GridSearch to use DatasetMapper
micyril Aug 14, 2017
9486986
Use the new GridSearch interface in the HPT module
micyril Aug 14, 2017
fc53e01
Add separate comments
micyril Aug 14, 2017
6f65136
Add gradient evaluation to CVFunction
micyril Aug 17, 2017
5a6df55
Add support for GradientDescent in the HPT module
micyril Aug 18, 2017
fd172d9
Refactor template conditions for InitAndOptimize
micyril Aug 18, 2017
fd95da5
Add assertion for argument types of Optimize
micyril Aug 18, 2017
7de610e
Add assertion that input collections aren't empty
micyril Aug 18, 2017
ab46e58
Add const getters in HyperParameterTuner
micyril Aug 25, 2017
5ac45dd
Make CVFunction cache computations for gradient
micyril Aug 25, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/mlpack/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ set(DIRS
cv
data
dists
hpt
kernels
math
metrics
Expand Down
15 changes: 15 additions & 0 deletions src/mlpack/core/hpt/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
set(SOURCES
bind.hpp
cv_function.hpp
cv_function_impl.hpp
deduce_hp_types.hpp
hpt.hpp
hpt_impl.hpp
)

set(DIR_SRCS)
foreach(file ${SOURCES})
set(DIR_SRCS ${DIR_SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/${file})
endforeach()

set(MLPACK_SRCS ${MLPACK_SRCS} ${DIR_SRCS} PARENT_SCOPE)
111 changes: 111 additions & 0 deletions src/mlpack/core/hpt/bind.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* @file bind.hpp
* @author Kirill Mishchenko
*
* Facilities for supporting bound arguments.
*
* mlpack is free software; you may redistribute it and/or modify it under the
* terms of the 3-clause BSD license. You should have received a copy of the
* 3-clause BSD license along with mlpack. If not, see
* http://www.opensource.org/licenses/BSD-3-Clause for more information.
*/
#ifndef MLPACK_CORE_HPT_BIND_HPP
#define MLPACK_CORE_HPT_BIND_HPP

#include <type_traits>

#include <mlpack/core.hpp>

namespace mlpack {
namespace hpt {

template<typename>
struct PreBoundArg;

/**
* Mark the given argument as one that should be bound. It can be applied to
* arguments that are passed to the Optimize method of HyperParameterTuner.
*
* The implementation avoids data coping. If the passed argument is an l-value

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor misspelling---data copying not data coping.

* reference, we store it as a const l-value rerefence inside the returned
* PreBoundArg object. If the passed argument is an r-value reference,
* ligth-weight coping (by taking possesion of the r-value) will be made during
* the initialization of the returned PreBoundArg object.
*/
template<typename T>
PreBoundArg<T> Bind(T&& value)
{
return PreBoundArg<T>{std::forward<T>(value)};
}

/**
* A struct for storing information about a bound argument. Objects of this type
* are supposed to be passed into the CVFunction constructor.
*
* This struct is not meant to be used directly by users. Rather use the
* mlpack::hpt::Bind function.
*
* @tparam T The type of the bound argument.
* @tparam I The index of the bound argument.
*/
template<typename T, size_t I>
struct BoundArg
{
//! The index of the bound argument.
static const size_t index = I;

//! The value of the bound argument.
const T& value;
};

/**
* A struct for marking arguments as ones that should be bound (it can be useful
* for the Optimize method of HyperParameterTuner). Arguments of this type are
* supposed to be converted into structs of the type BoundArg by adding
* information about argument positions.
*
* This struct is not meant to be used directly by users. Rather use the
* mlpack::hpt::Bind function.
*/
template<typename T>
struct PreBoundArg
{
using Type = T;

const T value;
};

/**
* The specialization of the template for references.
*
* This struct is not meant to be used directly by users. Rather use the
* mlpack::hpt::Bind function.
*/
template<typename T>
struct PreBoundArg<T&>
{
using Type = T;

const T& value;
};

/**
* A type function for checking whether the given type is PreBoundArg.
*/
template<typename T>
class IsPreBoundArg
{
template<typename>
struct Implementation : std::false_type {};

template<typename Type>
struct Implementation<PreBoundArg<Type>> : std::true_type {};

public:
static const bool value = Implementation<typename std::decay<T>::type>::value;
};

} // namespace hpt
} // namespace mlpack

#endif
143 changes: 143 additions & 0 deletions src/mlpack/core/hpt/cv_function.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* @file cv_function.hpp
* @author Kirill Mishchenko
*
* A cross-validation wrapper for optimizers.
*
* mlpack is free software; you may redistribute it and/or modify it under the
* terms of the 3-clause BSD license. You should have received a copy of the
* 3-clause BSD license along with mlpack. If not, see
* http://www.opensource.org/licenses/BSD-3-Clause for more information.
*/
#ifndef MLPACK_CORE_HPT_CV_FUNCTION_HPP
#define MLPACK_CORE_HPT_CV_FUNCTION_HPP

#include <mlpack/core.hpp>

namespace mlpack {
namespace hpt {

/**
* This wrapper serves for adapting the interface of the cross-validation
* classes to the one that can be utilized by the mlpack optimizers.
*
* This class is not supposed to be used directly by users. To tune
* hyper-parameters see HyperParameterTuner.
*
* @tparam CVType A cross-validation strategy.
* @tparam TotalArgs The total number of arguments that are supposed to be
* passed to the Evaluate method of a CVType object.
* @tparam BoundArgs Types of arguments (wrapped into the BoundArg struct) that
* should be passed into the Evaluate method of a CVType object but are not
* going to be passed into the Evaluate method of a CVFunction object.
*/
template<typename CVType, size_t TotalArgs, typename... BoundArgs>
class CVFunction
{
public:
/**
* Initialize a CVFunction object.
*
* @param cv A cross-validation object.
* @param BoundArgs Arguments that should be passed into the Evaluate method
* of the CVType object but are not going to be passed into the Evaluate
* method of this object.
*/
CVFunction(CVType& cv, const BoundArgs&... args);

/**
* Run cross-validation with the bound and passed parameters.
*
* @param parameters Arguments (rather than the bound arguments) that should
* be passed into the Evaluate method of the CVType object.
*/
double Evaluate(const arma::mat& parameters);

//! The used machine learning algorithm.
using MLAlgorithm = typename
std::remove_reference<decltype(std::declval<CVType>().Model())>::type;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If CVType exposes its own internal MLAlgorithm type, this could be a lot simpler. That's just an idea, I'm indifferent to what you choose to do here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also make HyperParameterTuner pass the MLAlgorithm type to CVFunction. It will be simpler too, and without making additional requirements for CVType. What do you think?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's fine. CVFunction is very specific to the hyper-parameter tuner and cross-validation, so I don't see a regular user needing to use that class. If you think passing MLAlgorithm to CVFunction is better, that sounds good to me. :)


//! Access and modify the best model so far.
MLAlgorithm& BestModel() { return bestModel; }

private:
//! The type of tuples of BoundArgs.
using BoundArgsTupleType = std::tuple<BoundArgs...>;

//! The amount of bound arguments.
static const size_t BoundArgsAmount =
std::tuple_size<BoundArgsTupleType>::value;

/**
* A struct that finds out whether the next argument for the Evaluate method
* of a CVType object should be a bound argument at the position BAIndex
* rather than an element of parameters at the position PIndex.
*/
template<size_t BAIndex,
size_t PIndex,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it is a little more verbose, but can you use more explicit names than BAIndex and PIndex? I think these are BoundArgIndex and ParamIndex but I am not sure. I think the code would be easier to understand if I was sure of what the arguments here were. Adding some documentation in the comment could be another way to solve this.

bool BoundArgsIndexInRange = BAIndex < BoundArgsAmount>
struct UseBoundArg;

//! A reference to the cross-validation object.
CVType& cv;

//! The bound arguments.
BoundArgsTupleType boundArgs;

//! The best objective so far.
double bestObjective;

//! The best model so far.
MLAlgorithm bestModel;

/**
* Collect all arguments and run cross-validation.
*/
template<size_t BAIndex,
size_t PIndex,
typename... Args,
typename =
typename std::enable_if<BAIndex + PIndex < TotalArgs>::type>
inline double Evaluate(const arma::mat& parameters, const Args&... args);

/**
* Run cross-validation with the collected arguments.
*/
template<size_t BAIndex,
size_t PIndex,
typename... Args,
typename =
typename std::enable_if<BAIndex + PIndex == TotalArgs>::type,
typename = void>
inline double Evaluate(const arma::mat& parameters, const Args&... args);

/**
* Put the bound argument (at the BAIndex position) as the next one.
*/
template<size_t BAIndex,
size_t PIndex,
typename... Args,
typename = typename std::enable_if<
UseBoundArg<BAIndex, PIndex>::value>::type>
inline double PutNextArg(const arma::mat& parameters, const Args&... args);

/**
* Put the element (at the PIndex position) of the parameters as the next one.
*/
template<size_t BAIndex,
size_t PIndex,
typename... Args,
typename = typename std::enable_if<
!UseBoundArg<BAIndex, PIndex>::value>::type,
typename = void>
inline double PutNextArg(const arma::mat& parameters, const Args&... args);
};


} // namespace hpt
} // namespace mlpack

// Include implementation
#include "cv_function_impl.hpp"

#endif
118 changes: 118 additions & 0 deletions src/mlpack/core/hpt/cv_function_impl.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* @file cv_function_impl.hpp
* @author Kirill Mishchenko
*
* The implementation of the class CVFunction.
*
* mlpack is free software; you may redistribute it and/or modify it under the
* terms of the 3-clause BSD license. You should have received a copy of the
* 3-clause BSD license along with mlpack. If not, see
* http://www.opensource.org/licenses/BSD-3-Clause for more information.
*/
#ifndef MLPACK_CORE_HPT_CV_FUNCTION_IMPL_HPP
#define MLPACK_CORE_HPT_CV_FUNCTION_IMPL_HPP

namespace mlpack {
namespace hpt {

template<typename CVType, size_t TotalArgs, typename... BoundArgs>
template<size_t BAIndex, size_t PIndex>
struct CVFunction<CVType, TotalArgs, BoundArgs...>::UseBoundArg<
BAIndex, PIndex, true>
{
using BoundArgType =
typename std::tuple_element<BAIndex, BoundArgsTupleType>::type;

static const bool value = BoundArgType::index == BAIndex + PIndex;
};

template<typename CVType, size_t TotalArgs, typename... BoundArgs>
template<size_t BAIndex, size_t PIndex>
struct CVFunction<CVType, TotalArgs, BoundArgs...>::UseBoundArg<
BAIndex, PIndex, false>
{
static const bool value = false;
};

template<typename CVType, size_t TotalArgs, typename... BoundArgs>
CVFunction<CVType, TotalArgs, BoundArgs...>::CVFunction(
CVType& cv, const BoundArgs&... args) :
cv(cv),
boundArgs(args...),
bestObjective(std::numeric_limits<double>::max())
{ /* Nothing left to do. */ }

template<typename CVType, size_t TotalArgs, typename... BoundArgs>
double CVFunction<CVType, TotalArgs, BoundArgs...>::Evaluate(
const arma::mat& parameters)
{
return Evaluate<0, 0>(parameters);
}

template<typename CVType, size_t TotalArgs, typename... BoundArgs>
template<size_t BAIndex,
size_t PIndex,
typename... Args,
typename>
double CVFunction<CVType, TotalArgs, BoundArgs...>::Evaluate(
const arma::mat& parameters,
const Args&... args)
{
return PutNextArg<BAIndex, PIndex>(parameters, args...);
}

template<typename CVType, size_t TotalArgs, typename... BoundArgs>
template<size_t BAIndex,
size_t PIndex,
typename... Args,
typename,
typename>
double CVFunction<CVType, TotalArgs, BoundArgs...>::Evaluate(
const arma::mat& /* parameters */,
const Args&... args)
{
double objective = cv.Evaluate(args...);

// Change the best model if we have got a better score, or if we probably
// have not assigned any valid (trained) model yet.
if (bestObjective > objective ||
bestObjective == std::numeric_limits<double>::max())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can simplify this, if (objective < bestObjective) would be sufficient to also capture the case where bestObjective == DBL_MAX.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean to use DBL_MAX instead of std::numeric_limits<double>::max()?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I was being lazy---it is quicker to type DBL_MAX than std::numeric_limits<double>::max() and I was using a phone so I did not want to type too much. :)

The idea of what I was saying though, is that there is no need to check if bestObjective == std::numeric_limits<double>::max(); if that condition holds, then it will always be true that objective < bestObjective (...assuming that objective is not std::numeric_limits<double>::max(), but even if that is true, that corner case can be handled by relaxing the conditional to if (objective <= bestObjective) and thereby taking the better objective even when it is the same.). Let me know if I can clarify further.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some trade-offs: we can use less lines (and CPU cycles) for condition checking, but we potentially will do more often the if body. Also the implementation with one condition is inconsistent with GridSearch - as a result we can provide a model trained with some different hyper-parameters than ones returned from Optimize.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I did not realize relaxing the condition made it inconsistent. In that case it seems like we need to leave it as-is.

{
bestObjective = objective;
bestModel = std::move(cv.Model());
}

return objective;
}

template<typename CVType, size_t TotalArgs, typename... BoundArgs>
template<size_t BAIndex,
size_t PIndex,
typename... Args,
typename>
double CVFunction<CVType, TotalArgs, BoundArgs...>::PutNextArg(
const arma::mat& parameters,
const Args&... args)
{
return Evaluate<BAIndex + 1, PIndex>(
parameters, args..., std::get<BAIndex>(boundArgs).value);
}

template<typename CVType, size_t TotalArgs, typename... BoundArgs>
template<size_t BAIndex,
size_t PIndex,
typename... Args,
typename,
typename>
double CVFunction<CVType, TotalArgs, BoundArgs...>::PutNextArg(
const arma::mat& parameters,
const Args&... args)
{
return Evaluate<BAIndex, PIndex + 1>(
parameters, args..., parameters(PIndex, 0));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option here is to access parameters one-dimensionally, with parameters[PIndex] not parameters(PIndex, 0). I am not sure which you think is more clear; I'm indifferent. I just wanted to point out the possibility. :)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed that arma::mat require to pass two indexes. For this concrete case I'm also indifferent, so I guess we can stay it as it is.

}

} // namespace hpt
} // namespace mlpack

#endif
Loading