blob: 42492c9b276abc10a815319cbdce48796f6dabd4 [file]
#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------
#
# THIS SCRIPT CHOOSES A GLM REGRESSION MODEL IN A STEPWISE ALGIRITHM USING AIC
# EACH GLM REGRESSION IS SOLVED USING NEWTON/FISHER SCORING WITH TRUST REGIONS
#
# INPUT PARAMETERS:
# ---------------------------------------------------------------------------------------------
# NAME TYPE DEFAULT MEANING
# ---------------------------------------------------------------------------------------------
# X Matrix --- Matrix X of feature vectors
# Y Matrix --- Response Matrix Y with 1 column
# link Int 2 Link function code: 1 = log, 2 = Logit, 3 = Probit, 4 = Cloglog
# yneg Double 0.0 Response value for Bernoulli "No" label, usually 0.0 or -1.0
# icpt Int 0 Intercept presence, X columns shifting and rescaling:
# 0 = no intercept, no shifting, no rescaling;
# 1 = add intercept, but neither shift nor rescale X;
# 2 = add intercept, shift & rescale X columns to mean = 0, variance = 1
# tol Double 0.000001 Tolerance (epsilon)
# disp Double 0.0 (Over-)dispersion value, or 0.0 to estimate it from data
# moi Int 200 Maximum number of outer (Newton / Fisher Scoring) iterations
# mii Int 0 Maximum number of inner (Conjugate Gradient) iterations, 0 = no maximum
# thr Double 0.01 Threshold to stop the algorithm: if the decrease in the value of AIC falls below thr
# no further features are being checked and the algorithm stops
# ---------------------------------------------------------------------------------------------
# OUTPUT: Matrix beta, whose size depends on icpt:
# icpt=0: ncol(X) x 1; icpt=1: (ncol(X) + 1) x 1; icpt=2: (ncol(X) + 1) x 2
#
# AIC Double --- AIC value
# B Matrix --- Estimated regression parameters (betas)
# S Matrix --- The selected features ordered as computed by the algorithm
# ---------------------------------------------------------------------------------------------
# THE StepGLM SCRIPT CURRENTLY SUPPORTS BERNOULLI DISTRIBUTION FAMILY AND THE FOLLOWING LINK FUNCTIONS ONLY!
# - LOG
# - LOGIT
# - PROBIT
# - CLOGLOG
source("./scripts/builtin/glm.dml") as glm;
m_stepGLM = function (
Matrix[Double] X,
Matrix[Double] Y,
Int link = 2,
Double yneg = 0.0,
Int icpt = 0,
Double tol = 0.000001,
Double disp = 0.0,
Int moi = 200,
Int mii = 0,
Double thr = 0.01
) return (
Double AIC,
Matrix[Double] B,
Matrix[Double] S
)
{
intercept_status = icpt;
bernoulli_No_label = yneg;
distribution_type = 2;
if (distribution_type == 2 & ncol(Y) == 1) {
is_Y_negative = (Y == bernoulli_No_label);
Y = cbind (1 - is_Y_negative, is_Y_negative);
count_Y_negative = sum (is_Y_negative);
if (count_Y_negative == 0) {
stop ("StepGLM Input Error: all Y-values encode Bernoulli YES-label, none encode NO-label");
}
if (count_Y_negative == nrow(Y)) {
stop ("StepGLM Input Error: all Y-values encode Bernoulli NO-label, none encode YES-label");
}
}
X_orig = X;
num_records = nrow (X_orig);
num_features = ncol (X_orig);
# BEGIN STEPWISE GENERALIZED LINEAR MODELS
continue = TRUE;
columns_fixed = matrix (0, rows = 1, cols = num_features);
columns_fixed_ordered = matrix (0, rows = 1, cols = 1);
# X_global stores the best model found at each step
X_global = matrix (0, rows = num_records, cols = 1);
if (intercept_status == 0) {
# Compute AIC of an empty model with no features and no intercept (all Ys are zero)
[AIC_best, ignore_B1, ignore_S1] = internal_glm(X=X_global, Y=Y, intercept_status=0, num_features_orig=num_features, Selected=columns_fixed_ordered, link=link, disp=disp, tol=tol, moi=moi, mii=mii);
} else {
# compute AIC of an empty model with only intercept (all Ys are constant)
all_ones = matrix (1, rows = num_records, cols = 1);
[AIC_best, ignore_beta2, ignore_S2] = internal_glm(X=all_ones, Y=Y, intercept_status=0, num_features_orig=num_features, Selected=columns_fixed_ordered, link=link, disp=disp, tol=tol, moi=moi, mii=mii);
}
# First pass to examine single features
AICs = matrix (AIC_best, rows = 1, cols = num_features);
parfor (i in 1:num_features) {
[AIC_1, ignore_beta3, ignore_S3] = internal_glm(X=X_orig[,i], Y=Y, intercept_status=intercept_status, num_features_orig=num_features, Selected=columns_fixed_ordered, link=link, disp=disp, tol=tol, moi=moi, mii=mii);
AICs[1,i] = AIC_1;
}
# Determine the best AIC
column_best = 0;
for (k in 1:num_features) {
AIC_cur = as.scalar (AICs[1,k]);
if ( (AIC_cur < AIC_best) & ((AIC_best - AIC_cur) > abs (thr * AIC_best)) ) {
column_best = k;
AIC_best = as.scalar(AICs[1,k]);
}
}
if (column_best == 0) {
if (intercept_status == 0) {
# Compute AIC of an empty model with no features and no intercept (all Ys are zero)
[AIC_best, ignore_beta4, ignore_S4] = internal_glm(X=X_global, Y=Y, intercept_status=0, num_features_orig=num_features, Selected=columns_fixed_ordered, link=link, disp=disp, tol=tol, moi=moi, mii=mii);
} else {
# compute AIC of an empty model with only intercept (all Ys are constant)
[AIC_best, ignore_beta5, ignore_S5] = internal_glm(X=all_ones, Y=Y, intercept_status=0, num_features_orig=num_features, Selected=columns_fixed_ordered, link=link, disp=disp, tol=tol, moi=moi, mii=mii);
}
};
columns_fixed[1,column_best] = 1;
columns_fixed_ordered[1,1] = column_best;
X_global = X_orig[,column_best];
while (continue) {
# Subsequent passes over the features
parfor (i in 1:num_features) {
if (as.scalar(columns_fixed[1,i]) == 0) {
# Construct the feature matrix
X_loop = cbind (X_global, X_orig[,i]);
[AIC_2, ignore_beta6, ignore_S6] = internal_glm(X=X_loop, Y=Y, intercept_status=intercept_status, num_features_orig=num_features, Selected=columns_fixed_ordered, link=link, disp=disp, tol=tol, moi=moi, mii=mii);
AICs[1,i] = AIC_2;
}
}
# Determine the best AIC
for (k in 1:num_features) {
AIC_cur = as.scalar (AICs[1,k]);
if ( (AIC_cur < AIC_best) & ((AIC_best - AIC_cur) > abs (thr * AIC_best)) & (as.scalar(columns_fixed[1,k]) == 0) ) {
column_best = k;
AIC_best = as.scalar(AICs[1,k]);
}
}
# cbind best found features (i.e., columns) to X_global
if (as.scalar(columns_fixed[1,column_best]) == 0) { # new best feature found
columns_fixed[1,column_best] = 1;
columns_fixed_ordered = cbind (columns_fixed_ordered, as.matrix(column_best));
if (ncol(columns_fixed_ordered) == num_features) { # all features examined
X_global = cbind (X_global, X_orig[,column_best]);
continue = FALSE;
} else {
X_global = cbind (X_global, X_orig[,column_best]);
}
} else {
continue = FALSE;
}
}
# run GLM with selected set of features
[AIC, B, S] = internal_glm(X=X_global, Y=Y, intercept_status=intercept_status, num_features_orig=num_features, Selected=columns_fixed_ordered, link=link, disp=disp, tol=tol, moi=moi, mii=mii);
}
compute_AIC = function(Matrix[Double] X, Matrix[Double] Y, Matrix[Double] B, Int link, Int icpt)
return (Double aic)
{
# Isolate unscaled parameters; accounts for m_glm returning 2 columns when icpt=2
beta = B[, 1];
if (icpt > 0) {
ones = matrix(1, rows=nrow(X), cols=1);
X_design = cbind(X, ones);
} else {
X_design = X;
}
eta = X_design %*% beta;
# Map inverse link functions
if (link == 1) {
# log, see https://en.wikipedia.org/wiki/Generalized_linear_model#Link_function
mu = exp(eta);
} else if (link == 2) {
# logit, see https://en.wikipedia.org/wiki/Generalized_linear_model#Link_function
mu = 1 / (1 + exp(-eta));
} else if (link == 3) {
# probit approximation, page 1487 in "Qualitative Response Models: A Survey (1981)" by Takeshi Amemiya
mu = 1 / (1 + exp(-1.6 * eta));
} else if (link == 4) {
# cloglog, see https://search.r-project.org/CRAN/refmans/VGAM/html/clogloglink.html
mu = 1 - exp(-exp(eta));
} else {
stop("Unsupported link function code: " + link);
mu = eta;
}
# Constrain to prevent numerical discontinuity in log(0)
mu = max(mu, 1e-15);
mu = min(mu, 1 - 1e-15);
Y_yes = Y[, 1];
Y_neg = Y[, 2];
LL = sum(Y_yes * log(mu) + Y_neg * log(1 - mu));
k = nrow(beta);
aic = 2 * k - 2 * LL;
}
internal_glm = function (
Matrix[Double] X,
Matrix[Double] Y,
Int intercept_status,
Double num_features_orig,
Matrix[Double] Selected,
Int link,
Double disp,
Double tol,
Int moi,
Int mii
) return (
Double AIC,
Matrix[Double] B,
Matrix[Double] S
) {
# bernoulli family parameters, see table in ./scripts/builtin/m_glm.dml for details.
new_link = link;
new_lpow = 1.0;
if (link == 1) {
new_link = 1;
new_lpow = 0.0;
}
B = glm::m_glm(X=X, Y=Y, dfam=2, vpow=0.0, link=new_link, lpow=new_lpow, yneg=0.0,
icpt=intercept_status, disp=disp, reg=0.0, tol=tol,
moi=moi, mii=mii, verbose=FALSE);
AIC = compute_AIC(X, Y, B, link, intercept_status);
S = Selected;
}