-
Notifications
You must be signed in to change notification settings - Fork 3
Default and optional parameters #2
Comments
Builder pattern is better suited in my opinion and is more adapted in Rust than any other languages. For straightforward builders like plain hyperparams, one can use a handy derive_builder along side |
In some cases macros can work for stuff like this, but honestly the more i use macros the more i think they should be avoided except for very special cases. derive_builder is quite good enough for most use cases. |
Thanks for the feedback! For the above example, would you say that the builder struct should be named The former case could lead to somewhat long names e.g. |
Perhaps a better way is to make a distinction between hyperparam/config and the actual logistic regression via #[derive(Builder)]
struct LogisticRegressionConfig { ... }
struct LogisticRegression {
weights: NDArray,
bias: Option<NDArray>,
config: LogisticRegressionConfig
} |
@ehsanmok How would you instantiate the let config = LogisticRegressionConfigBuilder::default()
.some_option(value)
.build();
let model = LogisticRegression::new(config); ? That would still have an issue with overly verbose config builder names, wouldn't it? |
Oh, yes, yes! let me rephrase. These are the options
struct LogisticRegression {
ws: NDArray,
penatly: String,
....
}
#[derive(Builder)]
struct LogisticRegressionConfig { ... }
struct LogisticRegression {
ws: NDArray,
config: LogisticRegressionConfig,
}
impl LogisticRegression {
fn new() -> Self { ... } // or init method with specific initializer type!
fn with_hyperparams(self, hp: LogisticRegressionConfig) -> Self { ... }
}
#[derive(Builder)]
struct LogisticRegressionConfig { ... }
struct LogisticRegression {
ws: NDArray,
config: LogisticRegressionConfig
}
trait HyperParams {
type Config;
fn with_hyperparams(config: Self::Config) -> Self;
// or
fn with_hyperparams(self, config: Self::Config) -> Self;
}
impl HyperParams for LogisticRegression {
type Config = LogisticRegressionConfig;
....
}
#[derive(HyperParams(type=LinearModel, params=...))] // needs succinct definition though!
struct LogisticRegression {
ws: NDArray,
} Among all these, I prefer the 3rd option which provides more flexibility. Ultimately, we wish to have a clean client scikit-learn setup like let hp = LogisticRegressionConfig::default(). ... .build();
let model = LogisticRegression::new().with_hyperparams(hp);
// or cleaner with `LogisticRegression::with_hyperparams(hp)`
model.train(X_train, y_train)?;
let y_hat = model.predict(X_test)?; |
This is stepping away from the exact builder pattern a little bit, but I like the idea of a config-taking constructor for a given estimate, pretty similar to the trait FromConfig<C> {
fn from_config(config: C) -> Self;
}
struct LogisticRegression {
config: LogisticRegressionConfig,
...
}
impl FromConfig<LogisticRegressionConfig> for LogisticRegression {
fn from_config(config: LogisticRegressionConfig) -> Self {
LogisticRegression { config, ... }
}
} You could add an easy default impl Default for LogisticRegressionConfig { ... }
// This could even be derived -- derive(DefaultFromConfig(config=LogisticRegressionConfig))
impl Default for LogisticRegression {
fn default() -> Self {
Self::from_config(LogisticRegressionConfig::default())
}
} Also, if the config object is just a holding area for parameters, I don't think we necessarily need to have a explicit 'build' method, but then we're getting close to just having a plain struct with struct update syntax, which... I think I'm warming up to? Just brainstorming, really. One other note, and a bit off-topic, but I really would prefer that any config specification of a regularizer (or common loss function, error function, whatever), like "l2", be done using an enum as opposed to a string-based config operation. It saves us doing string parsing and needing to return a run-time error when someone typos their desired penalty when building the config struct. |
I have two issues with struct update syntax:
On the other hand, one could argue that the encoding of constraints can be done by grouping together related params in a struct, e.g. trait FromConfig<C> {
fn from_config(config: C) -> Self;
}
struct LogisticRegression {
config: LogisticRegressionConfig,
...
}
struct LogisticRegressionConfig {
regularizer_config: RegularizerConfig;
optimizer_config: OptimizerConfig;
...
}
enum RegularizerConfig {
L2(f64),
L1(f64),
...
}
struct OptimizerConfig {
...
}
impl FromConfig<LogisticRegressionConfig> for LogisticRegression {
fn from_config(config: LogisticRegressionConfig) -> Self {
LogisticRegression { config, ... }
}
} Being even more aggressive though, having configs for what actually are different entities (regularizer, loss function, ...) in the same struct probably means that we are conflating too much behaviour in our |
Yes, agree! I have actually thought about this that having traits like Overall, I like this discussion and it makes me think more about the design 🙂 |
Perhaps what Rust is missing to make builder pattern a little bit smoother, is something for generating builders, similar to Lombok from Java. |
The duang crate might provide a viable macro solution to this problem. |
Related to the goal of defining a set of common ML traits as discussed in #1, another question is how to define default and optional parameters in ML models.
For instance, let's take as an example the
LogisticRegression
estimator that takes as input following parameters (with their default values),penalty="l2"
tol=0.0001
C=1.0
There are then multiple choices for initializing the model with some of these parameters,
1. Passing parameters explicitly + default method
From a quick look, this pattern (or something similar) appears to be used in the rusty-machine crate (e.g. here). For models with 10+ parameters passing all of them explicitly seems hardly practical.
2. Builder pattern
One could also use the builder pattern and either construct a
LogisticRegressionBuilder
or maybe even use it directly withLogisticRegression
,Is used by rustlearn as far as I can tell (e.g. here).
The advantage is that hyperparameters are already in a struct, which helps if you want to pass them between estimators or serialize them. The disadvantage is it requires to create a builder for each model. Also, I find that having multiple objects called
Hyperparameters
in the code base somewhat confusing (and it will definitely be an issue when searching the code for something).3. Using struct update syntax
Possibly something around the struct update syntax, though I have not explored this topic too much.
(Note: I have not tried to complile it to see if this actually works)
4. Other approaches
This topic was discussed at length in rust-lang/rfcs#323 and related RFCs, but I'm not sure what was accepted as of now or could be used in rust stable now (or in near future).
Comments would be very welcome. Please let me know if I missed something.
The text was updated successfully, but these errors were encountered: