✨ Add regressor class with stacking method
This commit is contained in:
parent
7c8f913f88
commit
15fc10e3da
|
@ -60,6 +60,7 @@ require 'rumale/ensemble/random_forest_regressor'
|
|||
require 'rumale/ensemble/extra_trees_classifier'
|
||||
require 'rumale/ensemble/extra_trees_regressor'
|
||||
require 'rumale/ensemble/stacking_classifier'
|
||||
require 'rumale/ensemble/stacking_regressor'
|
||||
require 'rumale/clustering/k_means'
|
||||
require 'rumale/clustering/mini_batch_k_means'
|
||||
require 'rumale/clustering/k_medoids'
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rumale/base/base_estimator'
|
||||
require 'rumale/base/regressor'
|
||||
|
||||
module Rumale
|
||||
module Ensemble
|
||||
# StackingRegressor is a class that implements regressor with stacking method.
|
||||
#
|
||||
# @example
|
||||
# estimators = {
|
||||
# las: Rumale::LinearModel::Lasso.new(reg_param: 1e-2, random_seed: 1),
|
||||
# mlp: Rumele::NeuralNetwork::MLPRegressor.new(hidden_units: [256], random_seed: 1),
|
||||
# rnd: Rumale::Ensemble::RandomForestRegressor.new(random_seed: 1)
|
||||
# }
|
||||
# meta_estimator = Rumale::LinearModel::Ridge.new(random_seed: 1)
|
||||
# regressor = Rumale::Ensemble::StackedRegressor.new(
|
||||
# estimators: estimators, meta_estimator: meta_estimator, random_seed: 1
|
||||
# )
|
||||
# regressor.fit(training_samples, traininig_values)
|
||||
# results = regressor.predict(testing_samples)
|
||||
#
|
||||
# *Reference*
|
||||
# - Zhou, Z-H., "Ensemble Mehotds - Foundations and Algorithms," CRC Press Taylor and Francis Group, Chapman and Hall/CRC, 2012.
|
||||
class StackingRegressor
|
||||
include Base::BaseEstimator
|
||||
include Base::Regressor
|
||||
|
||||
# Return the base regressors.
|
||||
# @return [Hash<Symbol,Regressor>]
|
||||
attr_reader :estimators
|
||||
|
||||
# Return the meta regressor.
|
||||
# @return [Regressor]
|
||||
attr_reader :meta_estimator
|
||||
|
||||
# Create a new regressor with stacking method.
|
||||
#
|
||||
# @param estimators [Hash<Symbol,Regressor>] The base regressors for extracting meta features.
|
||||
# @param meta_estimator [Regressor/Nil] The meta regressor that predicts values.
|
||||
# If nil is given, Ridge is used.
|
||||
# @param n_splits [Integer] The number of folds for cross validation with k-fold on meta feature extraction in training phase.
|
||||
# @param shuffle [Boolean] The flag indicating whether to shuffle the dataset on cross validation.
|
||||
# @param passthrough [Boolean] The flag indicating whether to concatenate the original features and meta features when training the meta regressor.
|
||||
# @param random_seed [Integer/Nil] The seed value using to initialize the random generator on cross validation.
|
||||
def initialize(estimators:, meta_estimator: nil, n_splits: 5, shuffle: true, passthrough: false, random_seed: nil)
|
||||
check_params_type(Hash, estimators: estimators)
|
||||
check_params_numeric(n_splits: n_splits)
|
||||
check_params_boolean(shuffle: shuffle, passthrough: passthrough)
|
||||
check_params_numeric_or_nil(random_seed: random_seed)
|
||||
@estimators = estimators
|
||||
@meta_estimator = meta_estimator || Rumale::LinearModel::Ridge.new
|
||||
@output_size = nil
|
||||
@params = {}
|
||||
@params[:n_splits] = n_splits
|
||||
@params[:shuffle] = shuffle
|
||||
@params[:passthrough] = passthrough
|
||||
@params[:random_seed] = random_seed || srand
|
||||
end
|
||||
|
||||
# Fit the model with given training data.
|
||||
#
|
||||
# @param x [Numo::DFloat] (shape: [n_samples, n_features]) The training data to be used for fitting the model.
|
||||
# @param y [Numo::DFloat] (shape: [n_samples, n_outputs]) The target variables to be used for fitting the model.
|
||||
# @return [StackedRegressor] The learned regressor itself.
|
||||
def fit(x, y)
|
||||
x = check_convert_sample_array(x)
|
||||
y = check_convert_tvalue_array(y)
|
||||
check_sample_tvalue_size(x, y)
|
||||
|
||||
n_samples, n_features = x.shape
|
||||
n_outputs = y.ndim == 1 ? 1 : y.shape[1]
|
||||
|
||||
# training base regressors with all training data.
|
||||
@estimators.each_key { |name| @estimators[name].fit(x, y) }
|
||||
|
||||
# detecting size of output for each base regressor.
|
||||
@output_size = detect_output_size(n_features)
|
||||
|
||||
# extracting meta features with base regressors.
|
||||
n_components = @output_size.values.inject(:+)
|
||||
z = Numo::DFloat.zeros(n_samples, n_components)
|
||||
|
||||
kf = Rumale::ModelSelection::KFold.new(
|
||||
n_splits: @params[:n_splits], shuffle: @params[:shuffle], random_seed: @params[:random_seed]
|
||||
)
|
||||
|
||||
kf.split(x, y).each do |train_ids, valid_ids|
|
||||
x_train = x[train_ids, true]
|
||||
y_train = n_outputs == 1 ? y[train_ids] : y[train_ids, true]
|
||||
x_valid = x[valid_ids, true]
|
||||
f_start = 0
|
||||
@estimators.each_key do |name|
|
||||
est_fold = Marshal.load(Marshal.dump(@estimators[name]))
|
||||
f_last = f_start + @output_size[name]
|
||||
f_position = @output_size[name] == 1 ? f_start : f_start...f_last
|
||||
z[valid_ids, f_position] = est_fold.fit(x_train, y_train).predict(x_valid)
|
||||
f_start = f_last
|
||||
end
|
||||
end
|
||||
|
||||
# concatenating original features.
|
||||
z = Numo::NArray.hstack([z, x]) if @params[:passthrough]
|
||||
|
||||
# training meta regressor.
|
||||
@meta_estimator.fit(z, y)
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Predict values for samples.
|
||||
#
|
||||
# @param x [Numo::DFloat] (shape: [n_samples, n_features]) The samples to predict the values.
|
||||
# @return [Numo::DFloat] (shape: [n_samples, n_outputs]) The predicted values per sample.
|
||||
def predict(x)
|
||||
x = check_convert_sample_array(x)
|
||||
z = transform(x)
|
||||
@meta_estimator.predict(z)
|
||||
end
|
||||
|
||||
# Transform the given data with the learned model.
|
||||
#
|
||||
# @param x [Numo::DFloat] (shape: [n_samples, n_features]) The samples to be transformed with the learned model.
|
||||
# @return [Numo::DFloat] (shape: [n_samples, n_components]) The meta features for samples.
|
||||
def transform(x)
|
||||
x = check_convert_sample_array(x)
|
||||
n_samples = x.shape[0]
|
||||
n_components = @output_size.values.inject(:+)
|
||||
z = Numo::DFloat.zeros(n_samples, n_components)
|
||||
f_start = 0
|
||||
@estimators.each_key do |name|
|
||||
f_last = f_start + @output_size[name]
|
||||
f_position = @output_size[name] == 1 ? f_start : f_start...f_last
|
||||
z[true, f_position] = @estimators[name].predict(x)
|
||||
f_start = f_last
|
||||
end
|
||||
z = Numo::NArray.hstack([z, x]) if @params[:passthrough]
|
||||
z
|
||||
end
|
||||
|
||||
# Fit the model with training data, and then transform them with the learned model.
|
||||
#
|
||||
# @param x [Numo::DFloat] (shape: [n_samples, n_features]) The training data to be used for fitting the model.
|
||||
# @param y [Numo::DFloat] (shape: [n_samples, n_outputs]) The target variables to be used for fitting the model.
|
||||
# @return [Numo::DFloat] (shape: [n_samples, n_components]) The meta features for training data.
|
||||
def fit_transform(x, y)
|
||||
x = check_convert_sample_array(x)
|
||||
y = check_convert_tvalue_array(y)
|
||||
fit(x, y).transform(x)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def detect_output_size(n_features)
|
||||
x_dummy = Numo::DFloat.new(2, n_features).rand
|
||||
@estimators.each_key.with_object({}) do |name, obj|
|
||||
output_dummy = @estimators[name].predict(x_dummy)
|
||||
obj[name] = output_dummy.ndim == 1 ? 1 : output_dummy.shape[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Rumale::Ensemble::StackingRegressor do
|
||||
let(:x) { two_clusters_dataset[0] }
|
||||
let(:n_samples) { x.shape[0] }
|
||||
let(:n_features) { x.shape[1] }
|
||||
let(:estimators) do
|
||||
{ dtr: Rumale::Tree::DecisionTreeRegressor.new(random_seed: 1),
|
||||
mlp: Rumale::NeuralNetwork::MLPRegressor.new(hidden_units: [8], max_iter: 50, random_seed: 1),
|
||||
rdg: Rumale::LinearModel::LinearRegression.new }
|
||||
end
|
||||
let(:n_base_estimators) { estimators.size }
|
||||
let(:meta_estimator) { nil }
|
||||
let(:passthrough) { false }
|
||||
let(:estimator) do
|
||||
described_class.new(estimators: estimators, meta_estimator: meta_estimator, passthrough: passthrough, random_seed: 1)
|
||||
end
|
||||
let(:predicted) { estimator.predict(x) }
|
||||
let(:score) { estimator.score(x, y) }
|
||||
|
||||
context 'when single target problem' do
|
||||
let(:y) { x[true, 0] + x[true, 1]**2 }
|
||||
|
||||
before { estimator.fit(x, y) }
|
||||
|
||||
it 'learns the model for single regression problem.', :aggregate_failures do
|
||||
expect(estimator.params[:n_splits]).to eq(5)
|
||||
expect(estimator.params[:shuffle]).to be_truthy
|
||||
expect(estimator.params[:passthrough]).to be_falsy
|
||||
expect(estimator.estimators).to be_a(Hash)
|
||||
expect(estimator.meta_estimator).to be_a(Rumale::LinearModel::Ridge)
|
||||
expect(predicted).to be_a(Numo::DFloat)
|
||||
expect(predicted.ndim).to eq(1)
|
||||
expect(predicted.shape[0]).to eq(n_samples)
|
||||
expect(score).to be_within(0.01).of(1.0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multi-target problem' do
|
||||
let(:y) { Numo::DFloat[x[true, 0].to_a, (x[true, 1]**2).to_a].transpose.dot(Numo::DFloat[[0.6, 0.4], [0.0, 0.1]]) }
|
||||
let(:n_outputs) { y.shape[1] }
|
||||
let(:meta_estimator) { Rumale::LinearModel::Lasso.new(random_seed: 1) }
|
||||
let(:copied) { Marshal.load(Marshal.dump(estimator)) }
|
||||
|
||||
before { estimator.fit(x, y) }
|
||||
|
||||
it 'learns the model for multiple regression problem.', :aggregate_failures do
|
||||
expect(estimator.meta_estimator).to be_a(Rumale::LinearModel::Lasso)
|
||||
expect(predicted).to be_a(Numo::DFloat)
|
||||
expect(predicted.ndim).to eq(2)
|
||||
expect(predicted.shape[0]).to eq(n_samples)
|
||||
expect(score).to be_within(0.01).of(1.0)
|
||||
end
|
||||
|
||||
it 'dumps and restores itself using Marshal module.', :aggregate_failures do
|
||||
expect(copied.class).to eq(estimator.class)
|
||||
expect(copied.params).to match(estimator.params)
|
||||
expect(copied.estimators.keys).to eq(estimator.estimators.keys)
|
||||
expect(copied.meta_estimator.class).to eq(estimator.meta_estimator.class)
|
||||
expect(copied.score(x, y)).to eq(score)
|
||||
end
|
||||
|
||||
context 'when used as feature extractor' do
|
||||
let(:meta_features) { estimator.fit_transform(x, y) }
|
||||
let(:n_components) { n_outputs * n_base_estimators }
|
||||
|
||||
it 'extracts meta features', :aggregate_failures do
|
||||
expect(meta_features).to be_a(Numo::DFloat)
|
||||
expect(meta_features.ndim).to eq(2)
|
||||
expect(meta_features.shape[0]).to eq(n_samples)
|
||||
expect(meta_features.shape[1]).to eq(n_components)
|
||||
end
|
||||
|
||||
context 'when concatenating original features' do
|
||||
let(:passthrough) { true }
|
||||
|
||||
it 'extracts meta features concatenated with original features', :aggregate_failures do
|
||||
expect(meta_features).to be_a(Numo::DFloat)
|
||||
expect(meta_features.ndim).to eq(2)
|
||||
expect(meta_features.shape[0]).to eq(n_samples)
|
||||
expect(meta_features.shape[1]).to eq(n_components + n_features)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue