├── riskslim ├── tests │ ├── __init__.py │ ├── test_risk_slim.py │ └── test_loss_functions.py ├── __init__.py ├── loss_functions │ ├── __init__.py │ ├── build_cython_loss_functions.py │ ├── fast_log_loss.pyx │ ├── log_loss.py │ ├── log_loss_weighted.py │ └── lookup_log_loss.pyx ├── defaults.py ├── bound_tightening.py ├── solution_pool.py ├── setup_functions.py ├── heuristics.py ├── utils.py ├── coefficient_set.py ├── mip.py └── initialization.py ├── MANIFEST.in ├── examples ├── README.txt ├── data │ ├── README.txt │ ├── breastcancer_cvindices.csv │ ├── breastcancer_weights.csv │ └── breastcancer_data.csv ├── ex_01_quickstart.py ├── ex_03_constraints.py └── ex_02_advanced_options.py ├── requirements.txt ├── docs ├── images │ └── risk_score_seizure.png ├── references │ └── ustun2019riskslim.bib └── cplex_instructions.md ├── LICENSE ├── .gitignore ├── batch ├── settings_template.json ├── job_template.sh └── train_risk_slim.py ├── README.md └── setup.py /riskslim/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /examples/README.txt: -------------------------------------------------------------------------------- 1 | .. _general_examples: 2 | 3 | General examples 4 | ================ 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cplex 2 | numpy 3 | scipy 4 | Cython 5 | IPython 6 | traitlets 7 | nose 8 | pandas 9 | prettytable -------------------------------------------------------------------------------- /docs/images/risk_score_seizure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ustunb/risk-slim/HEAD/docs/images/risk_score_seizure.png -------------------------------------------------------------------------------- /riskslim/__init__.py: -------------------------------------------------------------------------------- 1 | from .coefficient_set import CoefficientSet 2 | from .lattice_cpa import run_lattice_cpa, setup_lattice_cpa, finish_lattice_cpa 3 | from .utils import load_data_from_csv, print_model -------------------------------------------------------------------------------- /examples/data/README.txt: -------------------------------------------------------------------------------- 1 | .. _datasets: 2 | 3 | Datasets 4 | ================ 5 | 6 | These datasets are used in the examples as well as the experiments in our paper http://arxiv.org/abs/1610.00168 7 | 8 | -------------------------------------------------------------------------------- /docs/references/ustun2019riskslim.bib: -------------------------------------------------------------------------------- 1 | @article{ustun2019jmlr, 2 | author = {Ustun, Berk and Rudin, Cynthia}, 3 | title = {{Learning Optimized Risk Scores}}, 4 | journal = {{Journal of Machine Learning Research}}, 5 | year = {2019}, 6 | volume = {20}, 7 | number = {150}, 8 | pages = {1-75}, 9 | url = {http://jmlr.org/papers/v20/18-615.html} 10 | } -------------------------------------------------------------------------------- /riskslim/loss_functions/__init__.py: -------------------------------------------------------------------------------- 1 | from .log_loss import * 2 | from .log_loss_weighted import * 3 | 4 | try: 5 | from .fast_log_loss import * 6 | except ImportError: 7 | print("warning: could not import fast log loss") 8 | print("warning: returning handle to standard loss functions") 9 | # todo replace with warning object 10 | import log_loss as fast_log_loss 11 | 12 | try: 13 | from .lookup_log_loss import * 14 | except ImportError: 15 | print("warning: could not import lookup log loss") 16 | print("warning: returning handle to standard loss functions") 17 | # todo replace with warning object 18 | import log_loss as lookup_log_loss 19 | 20 | -------------------------------------------------------------------------------- /docs/cplex_instructions.md: -------------------------------------------------------------------------------- 1 | # Downloading & Installing CPLEX 2 | 3 | CPLEX is cross-platform optimization tool solver a Python API. It is free for students and faculty members at accredited institutions. 4 | 5 | To download CPLEX: 6 | 7 | 1. Register for [IBM OnTheHub](https://ur.us-south.cf.appdomain.cloud/a2mt/email-auth) 8 | 2. Download the *IBM ILOG CPLEX Optimization Studio* from the [software catalog](https://www-03.ibm.com/isc/esd/dswdown/searchPartNumber.wss?partNumber=CJ6BPML) 9 | 3. Install CPLEX Optimization Studio. 10 | 4. Setup the CPLEX Python API [as described here](https://www.ibm.com/support/knowledgecenter/SSSA5P_12.8.0/ilog.odms.cplex.help/CPLEX/GettingStarted/topics/set_up/Python_setup.html). 11 | 12 | If you have problems with CPLEX, please check the [CPLEX user manual](http://www-01.ibm.com/support/knowledgecenter/SSSA5P/welcome) or the [CPLEX forums](https://www.ibm.com/developerworks/community/forums/html/forum?id=11111111-0000-0000-0000-000000002059). 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Berk Ustun 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /riskslim/loss_functions/build_cython_loss_functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | This script builds loss functions using Cython on a local machine. 5 | To run this script 6 | 7 | 1. Change to the directory 8 | 9 | $REPO_DIR/riskslim/loss_functions 10 | 11 | 2. Run the following commands in Bash: 12 | 13 | python2 build_cython_loss_functions.py build_ext --inplace 14 | python3 build_cython_loss_functions.py build_ext --inplace 15 | 16 | """ 17 | import numpy 18 | import scipy 19 | from distutils.core import setup 20 | from distutils.extension import Extension 21 | from Cython.Distutils import build_ext 22 | 23 | 24 | #fast log loss 25 | ext_modules = [Extension(name = "fast_log_loss", 26 | sources=["fast_log_loss.pyx"], 27 | include_dirs=[numpy.get_include(), scipy.get_include()], 28 | libraries=["m"], 29 | extra_compile_args = ["-ffast-math"])] 30 | 31 | setup( 32 | cmdclass = {'build_ext': build_ext}, 33 | include_dirs = [numpy.get_include(), scipy.get_include()], 34 | ext_modules = ext_modules, 35 | ) 36 | 37 | #lookup log loss 38 | ext_modules = [Extension(name = "lookup_log_loss", 39 | sources=["lookup_log_loss.pyx"], 40 | include_dirs=[numpy.get_include(), scipy.get_include()], 41 | libraries=["m"], 42 | extra_compile_args = ["-ffast-math"])] 43 | 44 | setup( 45 | cmdclass = {'build_ext': build_ext}, 46 | include_dirs = [numpy.get_include(), scipy.get_include()], 47 | ext_modules = ext_modules, 48 | ) 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # scikit-learn specific 10 | doc/_build/ 11 | doc/auto_examples/ 12 | doc/modules/generated/ 13 | doc/datasets/generated/ 14 | 15 | # sklearn template 16 | doc/ 17 | *.yml 18 | .nojekyll 19 | ci_scripts/ 20 | skltemplate/ 21 | 22 | # riskslim directories 23 | batch/data/ 24 | batch/results/ 25 | batch/log/ 26 | dev/ 27 | cluster/ 28 | 29 | # riskslim files 30 | riskslim_todo.ft 31 | examples/data/*cvindices.csv 32 | examples/data/*weights.csv 33 | examples/ex_00_tests.py 34 | examples/data/recidivism_v01_*.csv 35 | !examples/data/breastcancer_cvindices.csv 36 | !examples/data/breastcancer_weights.csv 37 | riskslim/tests/test_common.py 38 | riskslim/tests/test_template.py 39 | riskslim/loss_functions/build/ 40 | riskslim/loss_functions/*.so 41 | riskslim/loss_functions/*.c 42 | 43 | # Distribution / packaging 44 | .Python 45 | env/ 46 | venv/ 47 | build/ 48 | develop-eggs/ 49 | dist/ 50 | downloads/ 51 | eggs/ 52 | .eggs/ 53 | lib/ 54 | lib64/ 55 | parts/ 56 | sdist/ 57 | var/ 58 | *.egg-info/ 59 | .installed.cfg 60 | *.egg 61 | .idea 62 | 63 | # PyInstaller 64 | # Usually these files are written by a python script from a template 65 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 66 | *.manifest 67 | *.spec 68 | 69 | # Installer logs 70 | pip-log.txt 71 | pip-delete-this-directory.txt 72 | 73 | # Unit test / coverage reports 74 | htmlcov/ 75 | .tox/ 76 | .coverage 77 | .coverage.* 78 | .cache 79 | nosetests.xml 80 | coverage.xml 81 | *,cover 82 | .hypothesis/ 83 | 84 | # Translations 85 | *.mo 86 | *.pot 87 | 88 | # Django stuff: 89 | *.log 90 | 91 | # Sphinx documentation 92 | docs/_build/ 93 | 94 | # PyBuilder 95 | target/ 96 | 97 | # emacs 98 | *~ 99 | *.org 100 | \#*# 101 | 102 | # Jupyter NB Checkpoints 103 | .ipynb_checkpoints/ 104 | -------------------------------------------------------------------------------- /batch/settings_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_runtime": 300.0, 3 | "max_tolerance": 1e-06, 4 | "loss_computation": "normal", 5 | "chained_updates_flag": true, 6 | "round_flag": true, 7 | "polish_flag": true, 8 | "initialization_flag": true, 9 | "add_cuts_at_heuristic_solutions": true, 10 | "tight_formulation": true, 11 | 12 | "polish_rounded_solutions": true, 13 | "polishing_max_runtime": 10.0, 14 | "polishing_max_solutions": 5.0, 15 | "polishing_start_cuts": 0, 16 | "polishing_start_gap": Infinity, 17 | "polishing_stop_cuts": Infinity, 18 | "polishing_stop_gap": 5.0, 19 | "polishing_tolerance": 0.1, 20 | 21 | "rounding_start_cuts": 0, 22 | "rounding_start_gap": Infinity, 23 | "rounding_stop_cuts": 20000, 24 | "rounding_stop_gap": 0.2, 25 | "rounding_tolerance": Infinity, 26 | 27 | "init_display_cplex_progress": false, 28 | "init_display_progress": true, 29 | "init_max_cplex_time_per_iteration": 10.0, 30 | "init_max_iterations": 10000, 31 | "init_max_runtime": 300.0, 32 | "init_max_runtime_per_iteration": 300.0, 33 | "init_max_tolerance": 0.0001, 34 | "init_polishing_after": true, 35 | "init_polishing_max_runtime": 30.0, 36 | "init_polishing_max_solutions": 5, 37 | "init_sequential_rounding_max_runtime": 30.0, 38 | "init_sequential_rounding_max_solutions": 5, 39 | "init_use_sequential_rounding": true, 40 | 41 | "display_cplex_progress": true, 42 | "purge_bound_cuts": false, 43 | "purge_loss_cuts": false, 44 | "cplex_absmipgap": 2.2204460492503131e-16, 45 | "cplex_integrality_tolerance": 2.2204460492503131e-16, 46 | "cplex_mipemphasis": 0, 47 | "cplex_mipgap": 2.2204460492503131e-16, 48 | "cplex_n_cores": 1, 49 | "cplex_nodefilesize": 122880, 50 | "cplex_poolrelgap": NaN, 51 | "cplex_poolreplace": 2, 52 | "cplex_poolsize": 100, 53 | "cplex_randomseed": 0, 54 | "cplex_repairtries": 20 55 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | risk-slim 2 | ======== 3 | 4 | risk-slim is a machine learning method to fit simple customized risk scores in python. 5 | 6 | #### Background 7 | 8 | Risk scores let users make quick risk predictions by adding and subtracting a few small numbers (see e.g., 500 + medical risk scores at [mdcalc.com](https://www.mdcalc.com/)). 9 | 10 | Here is a risk score for ICU risk prediction from our [paper](http://www.berkustun.com/docs/ustun_2017_optimized_risk_scores.pdf). 11 | 12 |
13 |

14 | 15 |

16 |
17 | 18 | #### Video 19 | 20 |

21 | 22 | RiskSLIM KDD 23 | 24 |

25 | 26 | 27 | #### Reference 28 | 29 | If you use risk-slim in your research, we would appreciate a citation to the following paper ([bibtex](/docs/references/ustun2019riskslim.bib)! 30 | 31 | Learning Optimized Risk Scores
32 | Berk Ustun and Cynthia Rudin
33 | Journal of Machine Learning Research, 2019. 34 | 35 | ## Installation 36 | 37 | Run the following snippet in a Unix terminal to install risk-slim and complete a test run. 38 | 39 | ``` 40 | git clone https://github.com/ustunb/risk-slim 41 | cd risk-slim 42 | pip install -e . # install in editable mode 43 | bash batch/job_template.sh # batch run 44 | ``` 45 | 46 | ### Requirements 47 | 48 | risk-slim requires Python 3.5+ and CPLEX 12.6+. For instructions on how to download and install, click [here](/docs/cplex_instructions.md). 49 | 50 | 51 | 52 | ## Contributing 53 | 54 | I'm planning to pick up development again in Fall 2020. I can definitely use a hand! If you are interested in contributing, please reach out! 55 | 56 | Here's the current development roadmap: 57 | 58 | - [sci-kit learn interface](http://scikit-learn.org/stable/developers/contributing.html#rolling-your-own-estimator) 59 | - support for open source solver in [python-mip](https://github.com/coin-or/python-mip) 60 | - basic reporting tools (roc curves, calibration plots, model reports) 61 | - documentation 62 | - pip -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # Copyright (C) 2017 Berk Ustun 4 | 5 | import os 6 | import sys 7 | from setuptools import setup, find_packages, dist 8 | from setuptools.extension import Extension 9 | 10 | #resources 11 | #setuptools http://setuptools.readthedocs.io/en/latest/setuptools.html 12 | #setuptools + Cython: http://stackoverflow.com/questions/32528560/ 13 | 14 | DISTNAME = 'riskslim' 15 | DESCRIPTION = "optimized risk scores on large-scale datasets" 16 | AUTHOR = 'Berk Ustun' 17 | AUTHOR_EMAIL = 'berk@seas.harvard.edu' 18 | URL = 'https://github.com/ustunb/risk-slim' 19 | LICENSE = 'new BSD' 20 | DOWNLOAD_URL = 'https://github.com/ustunb/risk-slim' 21 | VERSION = '0.0.0' 22 | 23 | # Install setup requirements 24 | dist.Distribution().fetch_build_eggs(['Cython', 'numpy', 'scipy']) 25 | 26 | #read requirements as listed in txt file 27 | try: 28 | import numpy 29 | except ImportError: 30 | print('numpy is required for installation') 31 | sys.exit(1) 32 | 33 | try: 34 | import scipy 35 | except ImportError: 36 | print('scipy is required for installation') 37 | sys.exit(1) 38 | 39 | try: 40 | from Cython.Build import cythonize 41 | except ImportError: 42 | print('Cython is required for installation') 43 | sys.exit(1) 44 | 45 | #fast log loss 46 | extensions =[ 47 | Extension( 48 | DISTNAME + ".loss_functions." + "fast_log_loss", 49 | [DISTNAME + "/loss_functions/fast_log_loss.pyx"], 50 | include_dirs=[numpy.get_include(), scipy.get_include()], 51 | libraries=["m"], 52 | extra_compile_args=["-ffast-math"] 53 | ), 54 | Extension( 55 | DISTNAME + ".loss_functions." + "lookup_log_loss", 56 | [DISTNAME + "/loss_functions/lookup_log_loss.pyx"], 57 | include_dirs=[numpy.get_include(), scipy.get_include()], 58 | libraries=["m"], 59 | extra_compile_args=["-ffast-math"]) 60 | ] 61 | 62 | 63 | if __name__ == "__main__": 64 | 65 | old_path = os.getcwd() 66 | local_path = os.path.dirname(os.path.abspath(sys.argv[0])) 67 | 68 | os.chdir(local_path) 69 | sys.path.insert(0, local_path) 70 | 71 | with open('requirements.txt') as f: 72 | INSTALL_REQUIRES = [l.strip() for l in f.readlines() if l] 73 | 74 | setup( 75 | name=DISTNAME, 76 | packages=find_packages(), 77 | ext_modules=cythonize(extensions), 78 | author=AUTHOR, 79 | author_email=AUTHOR_EMAIL, 80 | description=DESCRIPTION, 81 | install_requires=INSTALL_REQUIRES, 82 | license=LICENSE, 83 | url=URL, 84 | version=VERSION, 85 | download_url=DOWNLOAD_URL, 86 | zip_safe=False, 87 | ) 88 | -------------------------------------------------------------------------------- /batch/job_template.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a template to show how to train RiskSLIM from a Bash command shell 3 | # You should adapt this to run RiskSLIM on a batch computing environment (e.g. AWS Batch) 4 | # 5 | # To test the script, run the following command from risk-slim directory: 6 | # 7 | # `bash batch/job_template.sh` 8 | # 9 | # To see a detailed list of all arguments that can be passed into risk_slim, use: 10 | # 11 | # `python "batch/train_risk_slim.py --help` 12 | # 13 | # or 14 | # 15 | # `python2 "batch/train_risk_slim.py --help` 16 | # 17 | # Recommended Directory Structure for Batch Computing: 18 | # 19 | # risk-slim/ 20 | # └──batch/ 21 | # └──data/ location of CSV files for data (ignored in git) 22 | # └──logs/ directory where log files are printed out (ignored in git) 23 | # └──results/ directory where results files are stored (ignored in git) 24 | # └──doc/ 25 | # └──examples/ 26 | # └──riskslim/ directory where code is stored (do not change this to be able to pull from GitHub) 27 | # └──setup.py 28 | # 29 | # Advantaged settings are be configured through a JSON file. See: `batch/settings_template.json` for a template 30 | # The values can be changed directly using a text editor, or programmatically using a tool such as 31 | # `jq` https://stedolan.github.io/jq/ 32 | 33 | #directories 34 | repo_dir=$(pwd) 35 | data_dir="${repo_dir}/examples/data" #change to /batch/data/ for your own data 36 | batch_dir="${repo_dir}/batch" 37 | results_dir="${batch_dir}/results" 38 | log_dir="${batch_dir}/logs" 39 | 40 | #set job parameters 41 | data_name="breastcancer" 42 | data_file="${data_dir}/${data_name}_data.csv" 43 | 44 | cvindices_file="${data_dir}/${data_name}_cvindices.csv" 45 | #weights_file="${data_dir}/${data_name}_weights.csv" 46 | fold=0 47 | 48 | max_coef=5 49 | max_size=5 50 | max_offset=-1 51 | w_pos=1.00 52 | c0_value=1e-6 53 | 54 | timelimit=60 55 | 56 | #results_file and log_file must have a UNIQUE name for each job to avoid overwriting existing files 57 | run_name="${data_name}_fold_${fold}" 58 | run_time=$(date +"%m_%d_%Y_%H_%M_%S") 59 | results_file="${results_dir}/${run_name}_results.p" 60 | log_file="${log_dir}/${run_name}_${run_time}.log" 61 | 62 | #comment out the following in case testing / OK with overwriting 63 | #for safety, train_risk_slim.py will not run if results_file exists on disk 64 | rm -f "${results_file}" #c 65 | 66 | #create directories that do not exist 67 | mkdir -p "${results_dir}" 68 | mkdir -p "${log_dir}" 69 | 70 | #addition settings can be modified by changing a JSON file 71 | #complete list of settings is in: risk-slim/batch/settings_template.json 72 | settings_file="${results_dir}/${run_name}_settings.json" 73 | cp "${batch_dir}/settings_template.json" "${settings_file}" 74 | 75 | #run command 76 | python3 "${batch_dir}/train_risk_slim.py" \ 77 | --data "${data_file}" \ 78 | --results "${results_file}" \ 79 | --cvindices "${cvindices_file}" \ 80 | --fold "${fold}" \ 81 | --timelimit "${timelimit}" \ 82 | --settings "${settings_file}" \ 83 | --w_pos "${w_pos}" \ 84 | --c0_value "${c0_value}" \ 85 | --max_size "${max_size}" \ 86 | --max_coef "${max_coef}" \ 87 | --max_offset "${max_offset}" \ 88 | --log "${log_file}" 89 | 90 | exit 91 | W -------------------------------------------------------------------------------- /examples/ex_01_quickstart.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pprint 3 | import numpy as np 4 | import riskslim 5 | 6 | # data 7 | data_name = "breastcancer" # name of the data 8 | data_dir = os.getcwd() + '/examples/data/' # directory where datasets are stored 9 | data_csv_file = data_dir + data_name + '_data.csv' # csv file for the dataset 10 | sample_weights_csv_file = None # csv file of sample weights for the dataset (optional) 11 | 12 | # problem parameters 13 | max_coefficient = 5 # value of largest/smallest coefficient 14 | max_L0_value = 5 # maximum model size (set as float(inf)) 15 | max_offset = 50 # maximum value of offset parameter (optional) 16 | c0_value = 1e-6 # L0-penalty parameter such that c0_value > 0; larger values -> sparser models; we set to a small value (1e-6) so that we get a model with max_L0_value terms 17 | 18 | 19 | # load data from disk 20 | data = riskslim.load_data_from_csv(dataset_csv_file = data_csv_file, sample_weights_csv_file = sample_weights_csv_file) 21 | 22 | # create coefficient set and set the value of the offset parameter 23 | coef_set = riskslim.CoefficientSet(variable_names = data['variable_names'], lb = -max_coefficient, ub = max_coefficient, sign = 0) 24 | coef_set.update_intercept_bounds(X = data['X'], y = data['Y'], max_offset = max_offset) 25 | 26 | constraints = { 27 | 'L0_min': 0, 28 | 'L0_max': max_L0_value, 29 | 'coef_set':coef_set, 30 | } 31 | 32 | # major settings (see riskslim_ex_02_complete for full set of options) 33 | settings = { 34 | # Problem Parameters 35 | 'c0_value': c0_value, 36 | # 37 | # LCPA Settings 38 | 'max_runtime': 30.0, # max runtime for LCPA 39 | 'max_tolerance': np.finfo('float').eps, # tolerance to stop LCPA (set to 0 to return provably optimal solution) 40 | 'display_cplex_progress': True, # print CPLEX progress on screen 41 | 'loss_computation': 'fast', # how to compute the loss function ('normal','fast','lookup') 42 | # 43 | # LCPA Improvements 44 | 'round_flag': True, # round continuous solutions with SeqRd 45 | 'polish_flag': True, # polish integer feasible solutions with DCD 46 | 'chained_updates_flag': True, # use chained updates 47 | 'add_cuts_at_heuristic_solutions': True, # add cuts at integer feasible solutions found using polishing/rounding 48 | # 49 | # Initialization 50 | 'initialization_flag': True, # use initialization procedure 51 | 'init_max_runtime': 120.0, # max time to run CPA in initialization procedure 52 | 'init_max_coefficient_gap': 0.49, 53 | # 54 | # CPLEX Solver Parameters 55 | 'cplex_randomseed': 0, # random seed 56 | 'cplex_mipemphasis': 0, # cplex MIP strategy 57 | } 58 | 59 | # train model using lattice_cpa 60 | model_info, mip_info, lcpa_info = riskslim.run_lattice_cpa(data, constraints, settings) 61 | 62 | #print model contains model 63 | riskslim.print_model(model_info['solution'], data) 64 | 65 | #model info contains key results 66 | pprint.pprint(model_info) 67 | 68 | -------------------------------------------------------------------------------- /riskslim/loss_functions/fast_log_loss.pyx: -------------------------------------------------------------------------------- 1 | import cython 2 | import numpy as np 3 | cimport numpy as np 4 | cimport scipy.linalg.cython_blas as blas 5 | cimport libc.math as math 6 | 7 | DTYPE = np.float64 8 | ctypedef np.float64_t DTYPE_T 9 | 10 | @cython.boundscheck(False) 11 | @cython.wraparound(False) 12 | @cython.nonecheck(False) 13 | @cython.cdivision(False) 14 | def log_loss_value(np.ndarray[DTYPE_T, ndim=2, mode="fortran"] Z, np.ndarray[DTYPE_T, ndim=1, mode="fortran"] rho): 15 | 16 | cdef: 17 | int N = Z.shape[0] 18 | int D = Z.shape[1] 19 | int lda = N 20 | int incx = 1 #increments of rho 21 | int incy = 1 #increments of y 22 | double alpha = 1.0 23 | double beta = 0.0 24 | np.ndarray[DTYPE_T, ndim=1, mode = "fortran"] y = np.empty(N, dtype = DTYPE) 25 | Py_ssize_t i 26 | DTYPE_T total_loss = 0.0 27 | int zero_score_cnt = 0 28 | 29 | #compute scores 30 | #calls dgemv from BLZS which computes y = alpha * trans(Z) + beta * y 31 | #see: http://www.nag.com/numeric/fl/nagdoc_fl22/xhtml/F06/f06paf.xml 32 | blas.dgemv("N", &N, &D, &alpha, &Z[0,0], &lda, &rho[0], &incx, &beta, &y[0], &incy) 33 | 34 | #compute loss 35 | for i in range(N): 36 | if (y[i] < 0): 37 | total_loss += math.log(1.0 + math.exp(y[i])) - y[i] 38 | elif (y[i] > 0): 39 | total_loss += math.log1p(math.exp(-y[i])) 40 | else: 41 | zero_score_cnt += 1 42 | 43 | total_loss += zero_score_cnt * math.M_LN2 44 | return total_loss/N 45 | 46 | @cython.boundscheck(False) 47 | @cython.wraparound(False) 48 | @cython.nonecheck(False) 49 | @cython.cdivision(False) 50 | def log_loss_value_and_slope(np.ndarray[DTYPE_T, ndim=2, mode="fortran"] Z, np.ndarray[DTYPE_T, ndim=1, mode="fortran"] rho): 51 | 52 | cdef: 53 | int N = Z.shape[0] 54 | int D = Z.shape[1] 55 | int lda = N 56 | int incx = 1 #increments of rho 57 | int incy = 1 #increments of y 58 | double alpha = 1.0 59 | double beta = 0.0 60 | Py_ssize_t i 61 | DTYPE_T total_loss = 0.0 62 | DTYPE_T exp_value 63 | np.ndarray[DTYPE_T, ndim=1, mode = "fortran"] y = np.empty(N, dtype = DTYPE) 64 | np.ndarray[DTYPE_T, ndim=1, mode = "fortran"] loss_slope = np.empty(D, dtype = DTYPE) 65 | 66 | #compute scores 67 | #calls dgemv from BLAS which computes y = alpha * trans(Z) + beta * y 68 | #see: http://www.nag.com/numeric/fl/nagdoc_fl22/xhtml/F06/f06paf.xml 69 | blas.dgemv("N", &N, &D, &alpha, &Z[0,0], &lda, &rho[0], &incx, &beta, &y[0], &incy) 70 | 71 | #exponentiate scores, compute mean scores and probabilities 72 | for i in range(N): 73 | if y[i] < 0: 74 | exp_value = math.exp(y[i]) 75 | total_loss += math.log(1.0 + exp_value) - y[i] 76 | y[i] = (exp_value / (1.0 + exp_value)) - 1.0 77 | else: 78 | exp_value = math.exp(-y[i]) 79 | total_loss += math.log1p(exp_value) 80 | y[i] = (1.0 / (1.0 + exp_value)) - 1.0 81 | 82 | #compute loss slope 83 | alpha = 1.0/N 84 | blas.dgemv("T", &N, &D, &alpha, &Z[0,0], &lda, &y[0], &incx, &beta, &loss_slope[0], &incy) 85 | return (total_loss/N), loss_slope 86 | 87 | @cython.boundscheck(False) 88 | @cython.wraparound(False) 89 | @cython.nonecheck(False) 90 | @cython.cdivision(False) 91 | def log_loss_value_from_scores(np.ndarray[DTYPE_T, ndim=1, mode="fortran"] scores): 92 | 93 | cdef: 94 | Py_ssize_t N = scores.shape[0] 95 | DTYPE_T total_loss = 0.0 96 | int zero_score_cnt = 0 97 | Py_ssize_t i 98 | DTYPE_T s 99 | 100 | #compute loss 101 | for i in range(N): 102 | s = scores[i] 103 | if s < 0: 104 | total_loss += math.log(1.0 + math.exp(s)) - s 105 | elif s > 0: 106 | total_loss += math.log1p(math.exp(-s)) 107 | else: 108 | zero_score_cnt += 1 109 | 110 | total_loss += zero_score_cnt * math.M_LN2 111 | return total_loss/N 112 | -------------------------------------------------------------------------------- /riskslim/loss_functions/log_loss.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def log_loss_value(Z, rho): 4 | """ 5 | computes the value and slope of the logistic loss in a numerically stable way 6 | see also: http://stackoverflow.com/questions/20085768/ 7 | 8 | Parameters 9 | ---------- 10 | Z numpy.array containing training data with shape = (n_rows, n_cols) 11 | rho numpy.array of coefficients with shape = (n_cols,) 12 | 13 | Returns 14 | ------- 15 | loss_value scalar = 1/n_rows * sum(log( 1 .+ exp(-Z*rho)) 16 | 17 | """ 18 | scores = Z.dot(rho) 19 | pos_idx = scores > 0 20 | loss_value = np.empty_like(scores) 21 | loss_value[pos_idx] = np.log1p(np.exp(-scores[pos_idx])) 22 | loss_value[~pos_idx] = -scores[~pos_idx] + np.log1p(np.exp(scores[~pos_idx])) 23 | loss_value = loss_value.mean() 24 | return loss_value 25 | 26 | def log_loss_value_and_slope(Z, rho): 27 | """ 28 | computes the value and slope of the logistic loss in a numerically stable way 29 | this function should only be used when generating cuts in cutting-plane algorithms 30 | (computing both the value and the slope at the same time is slightly cheaper) 31 | 32 | see also: http://stackoverflow.com/questions/20085768/ 33 | 34 | Parameters 35 | ---------- 36 | Z numpy.array containing training data with shape = (n_rows, n_cols) 37 | rho numpy.array of coefficients with shape = (n_cols,) 38 | 39 | Returns 40 | ------- 41 | loss_value scalar = 1/n_rows * sum(log( 1 .+ exp(-Z*rho)) 42 | loss_slope: (n_cols x 1) vector = 1/n_rows * sum(-Z*rho ./ (1+exp(-Z*rho)) 43 | 44 | """ 45 | scores = Z.dot(rho) 46 | pos_idx = scores > 0 47 | exp_scores_pos = np.exp(-scores[pos_idx]) 48 | exp_scores_neg = np.exp(scores[~pos_idx]) 49 | 50 | #compute loss value 51 | loss_value = np.empty_like(scores) 52 | loss_value[pos_idx] = np.log1p(exp_scores_pos) 53 | loss_value[~pos_idx] = -scores[~pos_idx] + np.log1p(exp_scores_neg) 54 | loss_value = loss_value.mean() 55 | 56 | #compute loss slope 57 | log_probs = np.empty_like(scores) 58 | log_probs[pos_idx] = 1.0 / (1.0 + exp_scores_pos) 59 | log_probs[~pos_idx] = exp_scores_neg / (1.0 + exp_scores_neg) 60 | loss_slope = Z.T.dot(log_probs - 1.0) / Z.shape[0] 61 | 62 | return loss_value, loss_slope 63 | 64 | def log_loss_value_from_scores(scores): 65 | """ 66 | computes the logistic loss value from a vector of scores in a numerically stable way 67 | where scores = Z.dot(rho) 68 | 69 | see also: http://stackoverflow.com/questions/20085768/ 70 | 71 | this function is used for heuristics (discrete_descent, sequential_rounding). 72 | to save computation when running the heuristics, we store the scores and 73 | call this function to compute the loss directly from the scores 74 | this reduces the need to recompute the dot product. 75 | 76 | Parameters 77 | ---------- 78 | scores numpy.array of scores = Z.dot(rho) 79 | 80 | Returns 81 | ------- 82 | loss_value scalar = 1/n_rows * sum(log( 1 .+ exp(-Z*rho)) 83 | 84 | """ 85 | 86 | pos_idx = scores > 0 87 | loss_value = np.empty_like(scores) 88 | loss_value[pos_idx] = np.log1p(np.exp(-scores[pos_idx])) 89 | loss_value[~pos_idx] = -scores[~pos_idx] + np.log1p(np.exp(scores[~pos_idx])) 90 | loss_value = loss_value.mean() 91 | return loss_value 92 | 93 | def log_probs(Z, rho): 94 | """ 95 | compute the probabilities of the logistic loss function in a way that is numerically stable 96 | 97 | see also: http://stackoverflow.com/questions/20085768/ 98 | Parameters 99 | ---------- 100 | Z numpy.array containing training data with shape = (n_rows, n_cols) 101 | rho numpy.array of coefficients with shape = (n_cols,) 102 | 103 | Returns 104 | ------- 105 | log_probs numpy.array of probabilities under the logit model 106 | """ 107 | 108 | scores = Z.dot(rho) 109 | pos_idx = scores > 0 110 | log_probs = np.empty_like(scores) 111 | log_probs[pos_idx] = 1.0 / (1.0 + np.exp(-scores[pos_idx])) 112 | log_probs[~pos_idx] = np.exp(scores[~pos_idx]) / (1.0 + np.exp(scores[~pos_idx])) 113 | return log_probs 114 | -------------------------------------------------------------------------------- /riskslim/loss_functions/log_loss_weighted.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def log_loss_value(Z, weights, total_weights, rho): 4 | """ 5 | computes the value and slope of the logistic loss in a numerically stable way 6 | supports sample non-negative weights for each example in the training data 7 | see http://stackoverflow.com/questions/20085768/ 8 | 9 | Parameters 10 | ---------- 11 | Z numpy.array containing training data with shape = (n_rows, n_cols) 12 | rho numpy.array of coefficients with shape = (n_cols,) 13 | total_weights numpy.sum(total_weights) (only included to reduce computation) 14 | weights numpy.array of sample weights with shape (n_rows,) 15 | 16 | Returns 17 | ------- 18 | loss_value scalar = 1/n_rows * sum(log( 1 .+ exp(-Z*rho)) 19 | 20 | """ 21 | scores = Z.dot(rho) 22 | pos_idx = scores > 0 23 | loss_value = np.empty_like(scores) 24 | loss_value[pos_idx] = np.log1p(np.exp(-scores[pos_idx])) 25 | loss_value[~pos_idx] = -scores[~pos_idx] + np.log1p(np.exp(scores[~pos_idx])) 26 | loss_value = loss_value.dot(weights) / total_weights 27 | return loss_value 28 | 29 | def log_loss_value_and_slope(Z, weights, total_weights, rho): 30 | """ 31 | computes the value and slope of the logistic loss in a numerically stable way 32 | supports sample non-negative weights for each example in the training data 33 | this function should only be used when generating cuts in cutting-plane algorithms 34 | (computing both the value and the slope at the same time is slightly cheaper) 35 | 36 | see http://stackoverflow.com/questions/20085768/ 37 | 38 | Parameters 39 | ---------- 40 | Z numpy.array containing training data with shape = (n_rows, n_cols) 41 | rho numpy.array of coefficients with shape = (n_cols,) 42 | total_weights numpy.sum(total_weights) (only included to reduce computation) 43 | weights numpy.array of sample weights with shape (n_rows,) 44 | 45 | Returns 46 | ------- 47 | loss_value scalar = 1/n_rows * sum(log( 1 .+ exp(-Z*rho)) 48 | loss_slope: (n_cols x 1) vector = 1/n_rows * sum(-Z*rho ./ (1+exp(-Z*rho)) 49 | 50 | """ 51 | scores = Z.dot(rho) 52 | pos_idx = scores > 0 53 | exp_scores_pos = np.exp(-scores[pos_idx]) 54 | exp_scores_neg = np.exp(scores[~pos_idx]) 55 | 56 | #compute loss value 57 | loss_value = np.empty_like(scores) 58 | loss_value[pos_idx] = np.log1p(exp_scores_pos) 59 | loss_value[~pos_idx] = -scores[~pos_idx] + np.log1p(exp_scores_neg) 60 | loss_value = loss_value.dot(weights) / total_weights 61 | 62 | #compute loss slope 63 | log_probs = np.empty_like(scores) 64 | log_probs[pos_idx] = 1.0 / (1.0 + exp_scores_pos) 65 | log_probs[~pos_idx] = (exp_scores_neg / (1.0 + exp_scores_neg)) 66 | log_probs -= 1.0 67 | log_probs *= weights 68 | loss_slope = Z.T.dot(log_probs) / total_weights 69 | 70 | return loss_value, loss_slope 71 | 72 | def log_loss_value_from_scores(weights, total_weights, scores): 73 | """ 74 | computes the logistic loss value from a vector of scores in a numerically stable way 75 | where scores = Z.dot(rho) 76 | 77 | see also: http://stackoverflow.com/questions/20085768/ 78 | 79 | this function is used for heuristics (discrete_descent, sequential_rounding). 80 | to save computation when running the heuristics, we store the scores and 81 | call this function to compute the loss directly from the scores 82 | this reduces the need to recompute the dot product. 83 | 84 | Parameters 85 | ---------- 86 | scores numpy.array of scores = Z.dot(rho) 87 | total_weights numpy.sum(total_weights) (only included to reduce computation) 88 | weights numpy.array of sample weights with shape (n_rows,) 89 | 90 | Returns 91 | ------- 92 | loss_value scalar = 1/n_rows * sum(log( 1 .+ exp(-Z*rho)) 93 | 94 | """ 95 | pos_idx = scores > 0 96 | loss_value = np.empty_like(scores) 97 | loss_value[pos_idx] = np.log1p(np.exp(-scores[pos_idx])) 98 | loss_value[~pos_idx] = -scores[~pos_idx] + np.log1p(np.exp(scores[~pos_idx])) 99 | loss_value = loss_value.dot(weights) / total_weights 100 | 101 | return loss_value -------------------------------------------------------------------------------- /examples/data/breastcancer_cvindices.csv: -------------------------------------------------------------------------------- 1 | 2 2 | 5 3 | 2 4 | 4 5 | 4 6 | 5 7 | 5 8 | 3 9 | 2 10 | 5 11 | 5 12 | 2 13 | 1 14 | 2 15 | 3 16 | 5 17 | 5 18 | 1 19 | 3 20 | 4 21 | 4 22 | 1 23 | 1 24 | 1 25 | 1 26 | 3 27 | 4 28 | 5 29 | 2 30 | 4 31 | 3 32 | 2 33 | 5 34 | 2 35 | 5 36 | 1 37 | 3 38 | 1 39 | 4 40 | 1 41 | 5 42 | 4 43 | 3 44 | 1 45 | 5 46 | 5 47 | 5 48 | 2 49 | 1 50 | 5 51 | 4 52 | 3 53 | 2 54 | 3 55 | 1 56 | 2 57 | 1 58 | 2 59 | 4 60 | 3 61 | 3 62 | 2 63 | 3 64 | 1 65 | 1 66 | 1 67 | 5 68 | 3 69 | 2 70 | 4 71 | 1 72 | 3 73 | 5 74 | 3 75 | 1 76 | 1 77 | 4 78 | 3 79 | 2 80 | 1 81 | 4 82 | 5 83 | 2 84 | 5 85 | 2 86 | 2 87 | 3 88 | 2 89 | 3 90 | 2 91 | 1 92 | 3 93 | 5 94 | 3 95 | 1 96 | 2 97 | 4 98 | 3 99 | 2 100 | 5 101 | 5 102 | 3 103 | 4 104 | 2 105 | 4 106 | 5 107 | 3 108 | 2 109 | 4 110 | 4 111 | 3 112 | 4 113 | 2 114 | 5 115 | 1 116 | 4 117 | 4 118 | 4 119 | 3 120 | 1 121 | 2 122 | 1 123 | 2 124 | 3 125 | 3 126 | 4 127 | 5 128 | 1 129 | 4 130 | 4 131 | 3 132 | 4 133 | 1 134 | 2 135 | 1 136 | 3 137 | 3 138 | 2 139 | 2 140 | 3 141 | 1 142 | 1 143 | 3 144 | 3 145 | 5 146 | 4 147 | 1 148 | 4 149 | 1 150 | 3 151 | 4 152 | 3 153 | 4 154 | 1 155 | 1 156 | 3 157 | 4 158 | 2 159 | 2 160 | 3 161 | 5 162 | 1 163 | 1 164 | 2 165 | 5 166 | 1 167 | 3 168 | 2 169 | 2 170 | 5 171 | 4 172 | 1 173 | 5 174 | 2 175 | 4 176 | 3 177 | 1 178 | 1 179 | 5 180 | 4 181 | 4 182 | 1 183 | 1 184 | 3 185 | 5 186 | 4 187 | 1 188 | 3 189 | 5 190 | 1 191 | 4 192 | 5 193 | 5 194 | 2 195 | 2 196 | 5 197 | 4 198 | 5 199 | 2 200 | 4 201 | 4 202 | 1 203 | 1 204 | 5 205 | 3 206 | 1 207 | 1 208 | 5 209 | 5 210 | 4 211 | 2 212 | 4 213 | 5 214 | 1 215 | 4 216 | 4 217 | 2 218 | 2 219 | 2 220 | 1 221 | 1 222 | 3 223 | 4 224 | 5 225 | 3 226 | 4 227 | 4 228 | 3 229 | 3 230 | 3 231 | 3 232 | 5 233 | 3 234 | 3 235 | 3 236 | 1 237 | 1 238 | 3 239 | 2 240 | 5 241 | 2 242 | 5 243 | 4 244 | 5 245 | 4 246 | 2 247 | 1 248 | 4 249 | 4 250 | 4 251 | 2 252 | 4 253 | 4 254 | 4 255 | 3 256 | 3 257 | 3 258 | 5 259 | 1 260 | 2 261 | 1 262 | 2 263 | 1 264 | 2 265 | 3 266 | 5 267 | 4 268 | 5 269 | 1 270 | 3 271 | 4 272 | 2 273 | 3 274 | 4 275 | 5 276 | 1 277 | 2 278 | 3 279 | 4 280 | 5 281 | 5 282 | 5 283 | 2 284 | 3 285 | 2 286 | 2 287 | 5 288 | 3 289 | 4 290 | 3 291 | 3 292 | 2 293 | 5 294 | 4 295 | 2 296 | 1 297 | 1 298 | 3 299 | 2 300 | 3 301 | 2 302 | 5 303 | 4 304 | 1 305 | 2 306 | 2 307 | 1 308 | 2 309 | 2 310 | 1 311 | 5 312 | 4 313 | 5 314 | 5 315 | 5 316 | 2 317 | 1 318 | 1 319 | 2 320 | 3 321 | 3 322 | 2 323 | 2 324 | 5 325 | 2 326 | 2 327 | 3 328 | 1 329 | 5 330 | 5 331 | 4 332 | 3 333 | 5 334 | 2 335 | 4 336 | 1 337 | 5 338 | 2 339 | 2 340 | 5 341 | 2 342 | 4 343 | 1 344 | 5 345 | 1 346 | 5 347 | 3 348 | 5 349 | 2 350 | 3 351 | 1 352 | 2 353 | 4 354 | 3 355 | 3 356 | 2 357 | 3 358 | 4 359 | 2 360 | 5 361 | 2 362 | 2 363 | 5 364 | 2 365 | 4 366 | 3 367 | 1 368 | 1 369 | 4 370 | 4 371 | 3 372 | 3 373 | 1 374 | 1 375 | 5 376 | 5 377 | 4 378 | 3 379 | 3 380 | 4 381 | 5 382 | 2 383 | 5 384 | 2 385 | 5 386 | 1 387 | 1 388 | 1 389 | 4 390 | 2 391 | 2 392 | 1 393 | 4 394 | 3 395 | 5 396 | 3 397 | 3 398 | 2 399 | 5 400 | 5 401 | 3 402 | 2 403 | 1 404 | 2 405 | 4 406 | 1 407 | 2 408 | 5 409 | 1 410 | 5 411 | 4 412 | 4 413 | 5 414 | 1 415 | 3 416 | 3 417 | 2 418 | 1 419 | 4 420 | 3 421 | 3 422 | 4 423 | 3 424 | 4 425 | 5 426 | 2 427 | 5 428 | 2 429 | 1 430 | 4 431 | 5 432 | 1 433 | 5 434 | 3 435 | 5 436 | 5 437 | 5 438 | 5 439 | 3 440 | 4 441 | 2 442 | 1 443 | 5 444 | 3 445 | 2 446 | 4 447 | 4 448 | 4 449 | 1 450 | 1 451 | 4 452 | 4 453 | 2 454 | 5 455 | 1 456 | 1 457 | 4 458 | 1 459 | 3 460 | 5 461 | 1 462 | 1 463 | 4 464 | 2 465 | 2 466 | 4 467 | 2 468 | 1 469 | 3 470 | 2 471 | 1 472 | 1 473 | 3 474 | 1 475 | 3 476 | 2 477 | 5 478 | 5 479 | 2 480 | 1 481 | 4 482 | 3 483 | 3 484 | 5 485 | 3 486 | 3 487 | 3 488 | 1 489 | 3 490 | 1 491 | 5 492 | 5 493 | 4 494 | 2 495 | 2 496 | 3 497 | 1 498 | 4 499 | 3 500 | 1 501 | 1 502 | 5 503 | 4 504 | 1 505 | 3 506 | 4 507 | 4 508 | 1 509 | 3 510 | 5 511 | 5 512 | 1 513 | 1 514 | 4 515 | 4 516 | 5 517 | 4 518 | 1 519 | 2 520 | 4 521 | 5 522 | 1 523 | 4 524 | 2 525 | 5 526 | 4 527 | 5 528 | 1 529 | 2 530 | 1 531 | 2 532 | 3 533 | 5 534 | 5 535 | 4 536 | 2 537 | 4 538 | 3 539 | 4 540 | 4 541 | 2 542 | 2 543 | 4 544 | 5 545 | 4 546 | 4 547 | 2 548 | 1 549 | 5 550 | 1 551 | 2 552 | 5 553 | 1 554 | 2 555 | 3 556 | 3 557 | 1 558 | 2 559 | 3 560 | 4 561 | 2 562 | 3 563 | 4 564 | 5 565 | 4 566 | 1 567 | 2 568 | 3 569 | 2 570 | 1 571 | 2 572 | 5 573 | 1 574 | 5 575 | 1 576 | 2 577 | 1 578 | 3 579 | 5 580 | 2 581 | 5 582 | 5 583 | 1 584 | 2 585 | 3 586 | 3 587 | 4 588 | 5 589 | 5 590 | 4 591 | 3 592 | 5 593 | 2 594 | 4 595 | 5 596 | 2 597 | 5 598 | 4 599 | 4 600 | 1 601 | 3 602 | 5 603 | 2 604 | 2 605 | 4 606 | 1 607 | 2 608 | 4 609 | 3 610 | 3 611 | 4 612 | 3 613 | 2 614 | 3 615 | 1 616 | 4 617 | 3 618 | 4 619 | 3 620 | 5 621 | 2 622 | 5 623 | 4 624 | 4 625 | 4 626 | 3 627 | 2 628 | 1 629 | 5 630 | 3 631 | 1 632 | 3 633 | 2 634 | 4 635 | 3 636 | 5 637 | 5 638 | 1 639 | 2 640 | 3 641 | 4 642 | 2 643 | 5 644 | 4 645 | 1 646 | 5 647 | 3 648 | 2 649 | 5 650 | 5 651 | 3 652 | 2 653 | 4 654 | 3 655 | 4 656 | 5 657 | 4 658 | 4 659 | 4 660 | 4 661 | 1 662 | 4 663 | 5 664 | 3 665 | 2 666 | 1 667 | 2 668 | 3 669 | 1 670 | 1 671 | 2 672 | 3 673 | 2 674 | 1 675 | 5 676 | 1 677 | 5 678 | 1 679 | 4 680 | 3 681 | 3 682 | 5 683 | 3 684 | -------------------------------------------------------------------------------- /examples/ex_03_constraints.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import cplex as cplex 4 | import pprint 5 | import riskslim 6 | 7 | # data 8 | import riskslim.coefficient_set 9 | 10 | data_name = "breastcancer" # name of the data 11 | data_dir = os.getcwd() + '/examples/data/' # directory where datasets are stored 12 | data_csv_file = data_dir + data_name + '_data.csv' # csv file for the dataset 13 | sample_weights_csv_file = None # csv file of sample weights for the dataset (optional) 14 | 15 | # problem parameters 16 | max_coefficient = 5 # value of largest/smallest coefficient 17 | max_L0_value = 5 # maximum model size 18 | max_offset = 50 # maximum value of offset parameter (optional) 19 | c0_value = 1e-6 # L0-penalty parameter such that c0_value > 0; larger values -> sparser models; we set to a small value (1e-6) so that we get a model with max_L0_value terms 20 | w_pos = 1.00 # relative weight on examples with y = +1; w_neg = 1.00 (optional) 21 | 22 | # load data from disk 23 | data = riskslim.load_data_from_csv(dataset_csv_file = data_csv_file, sample_weights_csv_file = sample_weights_csv_file) 24 | N, P = data['X'].shape 25 | 26 | # create coefficient set and set the value of the offset parameter 27 | coef_set = riskslim.CoefficientSet(variable_names=data['variable_names'], lb=-max_coefficient, ub=max_coefficient, sign=0) 28 | coef_set.update_intercept_bounds(X = data['X'], y = data['Y'], max_offset = max_offset) 29 | 30 | # create constraint 31 | trivial_L0_max = P - np.sum(coef_set.C_0j == 0) 32 | max_L0_value = min(max_L0_value, trivial_L0_max) 33 | 34 | constraints = { 35 | 'L0_min': 0, 36 | 'L0_max': max_L0_value, 37 | 'coef_set':coef_set, 38 | } 39 | 40 | 41 | # major settings (see riskslim_ex_02_complete for full set of options) 42 | settings = { 43 | # Problem Parameters 44 | 'c0_value': c0_value, 45 | 'w_pos': w_pos, 46 | # 47 | # LCPA Settings 48 | 'max_runtime': 300.0, # max runtime for LCPA 49 | 'max_tolerance': np.finfo('float').eps, # tolerance to stop LCPA (set to 0 to return provably optimal solution) 50 | 'display_cplex_progress': True, # print CPLEX progress on screen 51 | 'loss_computation': 'normal', # how to compute the loss function ('normal','fast','lookup') 52 | # 53 | # RiskSLIM MIP settings 54 | 'drop_variables': False, 55 | # 56 | # LCPA Improvements 57 | 'round_flag': False, # round continuous solutions with SeqRd 58 | 'polish_flag': False, # polish integer feasible solutions with DCD 59 | 'chained_updates_flag': False, # use chained updates 60 | 'initialization_flag': False, # use initialization procedure 61 | 'init_max_runtime': 300.0, # max time to run CPA in initialization procedure 62 | 'add_cuts_at_heuristic_solutions': True, # add cuts at integer feasible solutions found using polishing/rounding 63 | # 64 | # CPLEX Solver Parameters 65 | 'cplex_randomseed': 0, # random seed 66 | 'cplex_mipemphasis': 0, # cplex MIP strategy 67 | } 68 | 69 | # turn on at your own risk 70 | settings['round_flag'] = False 71 | settings['polish_flag'] = False 72 | settings['chained_updates_flag'] = False 73 | settings['initialization_flag'] = False 74 | 75 | 76 | # initialize MIP for lattice CPA 77 | mip_objects = riskslim.setup_lattice_cpa(data, constraints, settings) 78 | 79 | # add operational constraints 80 | mip, indices = mip_objects['mip'], mip_objects['indices'] 81 | get_alpha_name = lambda var_name: 'alpha_' + str(data['variable_names'].index(var_name)) 82 | get_alpha_ind = lambda var_names: [get_alpha_name(v) for v in var_names] 83 | 84 | # to add a constraint like "either "CellSize" or "CellShape" 85 | # you must formulate the constraint in terms of the alpha variables 86 | # alpha[cell_size] + alpha[cell_shape] <= 1 to MIP 87 | mip.linear_constraints.add( 88 | names = ["EitherOr_CellSize_or_CellShape"], 89 | lin_expr = [cplex.SparsePair(ind = get_alpha_ind(['UniformityOfCellSize', 'UniformityOfCellShape']), 90 | val = [1.0, 1.0])], 91 | senses = "L", 92 | rhs = [1.0]) 93 | 94 | mip_objects['mip'] = mip 95 | 96 | # pass MIP back to lattice CPA so that it will solve 97 | model_info, mip_info, lcpa_info = riskslim.finish_lattice_cpa(data, constraints, mip_objects, settings) 98 | 99 | #model info contains key results 100 | pprint.pprint(model_info) 101 | riskslim.print_model(model_info['solution'], data) 102 | 103 | # mip_output contains information to access the MIP 104 | mip_info['risk_slim_mip'] #CPLEX mip 105 | mip_info['risk_slim_idx'] #indices of the relevant constraints 106 | 107 | # lcpa_output contains detailed information about LCPA 108 | pprint.pprint(lcpa_info) 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /riskslim/defaults.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | INTERCEPT_NAME = '(Intercept)' 4 | 5 | # Settings 6 | DEFAULT_LCPA_SETTINGS = { 7 | # 8 | 'c0_value': 1e-6, 9 | 'w_pos': 1.00, 10 | # 11 | # MIP Formulation 12 | 'drop_variables': True, #drop variables 13 | 'tight_formulation': True, #use a slightly tighter MIP formulation 14 | 'include_auxillary_variable_for_objval': True, 15 | 'include_auxillary_variable_for_L0_norm': True, 16 | # 17 | # LCPA Settings 18 | 'max_runtime': 300.0, # max runtime for LCPA 19 | 'max_tolerance': 0.000001, # tolerance to stop LCPA 20 | 'display_cplex_progress': True, # setting to True shows CPLEX progress 21 | 'loss_computation': 'normal', # type of loss computation to use ('normal','fast','lookup') 22 | 'chained_updates_flag': True, # use chained updates 23 | 'initialization_flag': False, # use initialization procedure 24 | 'initial_bound_updates': True, # update bounds before solving 25 | 'add_cuts_at_heuristic_solutions': True, #add cuts at integer feasible solutions found using polishing/rounding 26 | # 27 | # LCPA Rounding Heuristic 28 | 'round_flag': True, # round continuous solutions with SeqRd 29 | 'polish_rounded_solutions': True, # polish solutions rounded with SeqRd using DCD 30 | 'rounding_tolerance': float('inf'), # only solutions with objective value < (1 + tol) are rounded 31 | 'rounding_start_cuts': 0, # cuts needed to start using rounding heuristic 32 | 'rounding_start_gap': float('inf'), # optimality gap needed to start using rounding heuristic 33 | 'rounding_stop_cuts': 20000, # cuts needed to stop using rounding heuristic 34 | 'rounding_stop_gap': 0.2, # optimality gap needed to stop using rounding heuristic 35 | # 36 | # LCPA Polishing Heuristic 37 | 'polish_flag': True, # polish integer feasible solutions with DCD 38 | 'polishing_tolerance': 0.1, # only solutions with objective value (1 + polishing_ub_to_objval_relgap) are polished. setting to 39 | 'polishing_max_runtime': 10.0, # max time to run polishing each time 40 | 'polishing_max_solutions': 5.0, # max # of solutions to polish each time 41 | 'polishing_start_cuts': 0, # cuts needed to start using polishing heuristic 42 | 'polishing_start_gap': float('inf'), # min optimality gap needed to start using polishing heuristic 43 | 'polishing_stop_cuts': float('inf'), # cuts needed to stop using polishing heuristic 44 | 'polishing_stop_gap': 5.0, # max optimality gap required to stop using polishing heuristic 45 | # 46 | # Internal Parameters 47 | 'purge_loss_cuts': False, 48 | 'purge_bound_cuts': False, 49 | } 50 | 51 | DEFAULT_CPLEX_SETTINGS = { 52 | 'randomseed': 0, # random seed 53 | 'mipemphasis': 0, # cplex MIP strategy 54 | 'mipgap': np.finfo('float').eps, # 55 | 'absmipgap': np.finfo('float').eps, # 56 | 'integrality_tolerance': np.finfo('float').eps, # 57 | 'repairtries': 20, # number of tries to repair user provided solutions 58 | 'poolsize': 100, # number of feasible solutions to keep in solution pool 59 | 'poolrelgap': float('nan'), # discard if solutions 60 | 'poolreplace': 2, # solution pool 61 | 'n_cores': 1, # number of cores to use in B & B (must be 1) 62 | 'nodefilesize': (120 * 1024) / 1, # node file size 63 | } 64 | 65 | DEFAULT_CPA_SETTINGS = { 66 | # 67 | 'type': 'cvx', 68 | 'display_progress': True, # print progress of initialization procedure 69 | 'display_cplex_progress': False, # print of CPLEX during intialization procedure 70 | 'save_progress': False, # print progress of initialization procedure 71 | 'update_bounds': True, 72 | # 73 | 'max_runtime': 300.0, # max time to run CPA in initialization procedure 74 | 'max_runtime_per_iteration': 15.0, # max time per iteration of CPA 75 | # 76 | 'max_coefficient_gap': 0.49, # stopping tolerance for CPA (based on gap between consecutive solutions) 77 | 'min_iterations_before_coefficient_gap_check': 250, 78 | # 79 | 'max_iterations': 10000, # max # of cuts needed to stop CPA 80 | 'max_tolerance': 0.0001, # stopping tolerance for CPA (based on optimality gap) 81 | } 82 | 83 | DEFAULT_INITIALIZATION_SETTINGS = { 84 | 'type': 'cvx', 85 | 'use_rounding': True, # use SeqRd in initialization procedure 86 | 'rounding_max_runtime': 30.0, # max runtime for Rs in initialization procedure 87 | 'rounding_max_solutions': 5, # max solutions to round using Rd 88 | # 89 | 'use_sequential_rounding': True, # use SeqRd in initialization procedure 90 | 'sequential_rounding_max_runtime': 30.0, # max runtime for SeqRd in initialization procedure 91 | 'sequential_rounding_max_solutions': 5, # max solutions to round using SeqRd 92 | # 93 | 'polishing_after': True, # polish after rounding 94 | 'polishing_max_runtime': 30.0, # max runtime for polishing 95 | 'polishing_max_solutions': 5 # max solutions to polish 96 | } 97 | 98 | # Initialization Settings includes CPA Settings 99 | DEFAULT_INITIALIZATION_SETTINGS.update(DEFAULT_CPA_SETTINGS) 100 | 101 | # LCPA Settings includes Initialization and CPLEX settings 102 | DEFAULT_LCPA_SETTINGS.update({'init_%s' % k: v for k,v in DEFAULT_INITIALIZATION_SETTINGS.items()}) 103 | DEFAULT_LCPA_SETTINGS.update({'cplex_%s' % k: v for k,v in DEFAULT_CPLEX_SETTINGS.items()}) -------------------------------------------------------------------------------- /examples/data/breastcancer_weights.csv: -------------------------------------------------------------------------------- 1 | 1 2 | 1 3 | 1 4 | 1 5 | 1 6 | 1 7 | 1 8 | 1 9 | 1 10 | 1 11 | 1 12 | 1 13 | 1 14 | 1 15 | 1 16 | 1 17 | 1 18 | 1 19 | 1 20 | 1 21 | 1 22 | 1 23 | 1 24 | 1 25 | 1 26 | 1 27 | 1 28 | 1 29 | 1 30 | 1 31 | 1 32 | 1 33 | 1 34 | 1 35 | 1 36 | 1 37 | 1 38 | 1 39 | 1 40 | 1 41 | 1 42 | 1 43 | 1 44 | 1 45 | 1 46 | 1 47 | 1 48 | 1 49 | 1 50 | 1 51 | 1 52 | 1 53 | 1 54 | 1 55 | 1 56 | 1 57 | 1 58 | 1 59 | 1 60 | 1 61 | 1 62 | 1 63 | 1 64 | 1 65 | 1 66 | 1 67 | 1 68 | 1 69 | 1 70 | 1 71 | 1 72 | 1 73 | 1 74 | 1 75 | 1 76 | 1 77 | 1 78 | 1 79 | 1 80 | 1 81 | 1 82 | 1 83 | 1 84 | 1 85 | 1 86 | 1 87 | 1 88 | 1 89 | 1 90 | 1 91 | 1 92 | 1 93 | 1 94 | 1 95 | 1 96 | 1 97 | 1 98 | 1 99 | 1 100 | 1 101 | 1 102 | 1 103 | 1 104 | 1 105 | 1 106 | 1 107 | 1 108 | 1 109 | 1 110 | 1 111 | 1 112 | 1 113 | 1 114 | 1 115 | 1 116 | 1 117 | 1 118 | 1 119 | 1 120 | 1 121 | 1 122 | 1 123 | 1 124 | 1 125 | 1 126 | 1 127 | 1 128 | 1 129 | 1 130 | 1 131 | 1 132 | 1 133 | 1 134 | 1 135 | 1 136 | 1 137 | 1 138 | 1 139 | 1 140 | 1 141 | 1 142 | 1 143 | 1 144 | 1 145 | 1 146 | 1 147 | 1 148 | 1 149 | 1 150 | 1 151 | 1 152 | 1 153 | 1 154 | 1 155 | 1 156 | 1 157 | 1 158 | 1 159 | 1 160 | 1 161 | 1 162 | 1 163 | 1 164 | 1 165 | 1 166 | 1 167 | 1 168 | 1 169 | 1 170 | 1 171 | 1 172 | 1 173 | 1 174 | 1 175 | 1 176 | 1 177 | 1 178 | 1 179 | 1 180 | 1 181 | 1 182 | 1 183 | 1 184 | 1 185 | 1 186 | 1 187 | 1 188 | 1 189 | 1 190 | 1 191 | 1 192 | 1 193 | 1 194 | 1 195 | 1 196 | 1 197 | 1 198 | 1 199 | 1 200 | 1 201 | 1 202 | 1 203 | 1 204 | 1 205 | 1 206 | 1 207 | 1 208 | 1 209 | 1 210 | 1 211 | 1 212 | 1 213 | 1 214 | 1 215 | 1 216 | 1 217 | 1 218 | 1 219 | 1 220 | 1 221 | 1 222 | 1 223 | 1 224 | 1 225 | 1 226 | 1 227 | 1 228 | 1 229 | 1 230 | 1 231 | 1 232 | 1 233 | 1 234 | 1 235 | 1 236 | 1 237 | 1 238 | 1 239 | 1 240 | 1 241 | 1 242 | 1 243 | 1 244 | 1 245 | 1 246 | 1 247 | 1 248 | 1 249 | 1 250 | 1 251 | 1 252 | 1 253 | 1 254 | 1 255 | 1 256 | 1 257 | 1 258 | 1 259 | 1 260 | 1 261 | 1 262 | 1 263 | 1 264 | 1 265 | 1 266 | 1 267 | 1 268 | 1 269 | 1 270 | 1 271 | 1 272 | 1 273 | 1 274 | 1 275 | 1 276 | 1 277 | 1 278 | 1 279 | 1 280 | 1 281 | 1 282 | 1 283 | 1 284 | 1 285 | 1 286 | 1 287 | 1 288 | 1 289 | 1 290 | 1 291 | 1 292 | 1 293 | 1 294 | 1 295 | 1 296 | 1 297 | 1 298 | 1 299 | 1 300 | 1 301 | 1 302 | 1 303 | 1 304 | 1 305 | 1 306 | 1 307 | 1 308 | 1 309 | 1 310 | 1 311 | 1 312 | 1 313 | 1 314 | 1 315 | 1 316 | 1 317 | 1 318 | 1 319 | 1 320 | 1 321 | 1 322 | 1 323 | 1 324 | 1 325 | 1 326 | 1 327 | 1 328 | 1 329 | 1 330 | 1 331 | 1 332 | 1 333 | 1 334 | 1 335 | 1 336 | 1 337 | 1 338 | 1 339 | 1 340 | 1 341 | 1 342 | 1 343 | 1 344 | 1 345 | 1 346 | 1 347 | 1 348 | 1 349 | 1 350 | 1 351 | 1 352 | 1 353 | 1 354 | 1 355 | 1 356 | 1 357 | 1 358 | 1 359 | 1 360 | 1 361 | 1 362 | 1 363 | 1 364 | 1 365 | 1 366 | 1 367 | 1 368 | 1 369 | 1 370 | 1 371 | 1 372 | 1 373 | 1 374 | 1 375 | 1 376 | 1 377 | 1 378 | 1 379 | 1 380 | 1 381 | 1 382 | 1 383 | 1 384 | 1 385 | 1 386 | 1 387 | 1 388 | 1 389 | 1 390 | 1 391 | 1 392 | 1 393 | 1 394 | 1 395 | 1 396 | 1 397 | 1 398 | 1 399 | 1 400 | 1 401 | 1 402 | 1 403 | 1 404 | 1 405 | 1 406 | 1 407 | 1 408 | 1 409 | 1 410 | 1 411 | 1 412 | 1 413 | 1 414 | 1 415 | 1 416 | 1 417 | 1 418 | 1 419 | 1 420 | 1 421 | 1 422 | 1 423 | 1 424 | 1 425 | 1 426 | 1 427 | 1 428 | 1 429 | 1 430 | 1 431 | 1 432 | 1 433 | 1 434 | 1 435 | 1 436 | 1 437 | 1 438 | 1 439 | 1 440 | 1 441 | 1 442 | 1 443 | 1 444 | 1 445 | 1 446 | 1 447 | 1 448 | 1 449 | 1 450 | 1 451 | 1 452 | 1 453 | 1 454 | 1 455 | 1 456 | 1 457 | 1 458 | 1 459 | 1 460 | 1 461 | 1 462 | 1 463 | 1 464 | 1 465 | 1 466 | 1 467 | 1 468 | 1 469 | 1 470 | 1 471 | 1 472 | 1 473 | 1 474 | 1 475 | 1 476 | 1 477 | 1 478 | 1 479 | 1 480 | 1 481 | 1 482 | 1 483 | 1 484 | 1 485 | 1 486 | 1 487 | 1 488 | 1 489 | 1 490 | 1 491 | 1 492 | 1 493 | 1 494 | 1 495 | 1 496 | 1 497 | 1 498 | 1 499 | 1 500 | 1 501 | 1 502 | 1 503 | 1 504 | 1 505 | 1 506 | 1 507 | 1 508 | 1 509 | 1 510 | 1 511 | 1 512 | 1 513 | 1 514 | 1 515 | 1 516 | 1 517 | 1 518 | 1 519 | 1 520 | 1 521 | 1 522 | 1 523 | 1 524 | 1 525 | 1 526 | 1 527 | 1 528 | 1 529 | 1 530 | 1 531 | 1 532 | 1 533 | 1 534 | 1 535 | 1 536 | 1 537 | 1 538 | 1 539 | 1 540 | 1 541 | 1 542 | 1 543 | 1 544 | 1 545 | 1 546 | 1 547 | 1 548 | 1 549 | 1 550 | 1 551 | 1 552 | 1 553 | 1 554 | 1 555 | 1 556 | 1 557 | 1 558 | 1 559 | 1 560 | 1 561 | 1 562 | 1 563 | 1 564 | 1 565 | 1 566 | 1 567 | 1 568 | 1 569 | 1 570 | 1 571 | 1 572 | 1 573 | 1 574 | 1 575 | 1 576 | 1 577 | 1 578 | 1 579 | 1 580 | 1 581 | 1 582 | 1 583 | 1 584 | 1 585 | 1 586 | 1 587 | 1 588 | 1 589 | 1 590 | 1 591 | 1 592 | 1 593 | 1 594 | 1 595 | 1 596 | 1 597 | 1 598 | 1 599 | 1 600 | 1 601 | 1 602 | 1 603 | 1 604 | 1 605 | 1 606 | 1 607 | 1 608 | 1 609 | 1 610 | 1 611 | 1 612 | 1 613 | 1 614 | 1 615 | 1 616 | 1 617 | 1 618 | 1 619 | 1 620 | 1 621 | 1 622 | 1 623 | 1 624 | 1 625 | 1 626 | 1 627 | 1 628 | 1 629 | 1 630 | 1 631 | 1 632 | 1 633 | 1 634 | 1 635 | 1 636 | 1 637 | 1 638 | 1 639 | 1 640 | 1 641 | 1 642 | 1 643 | 1 644 | 1 645 | 1 646 | 1 647 | 1 648 | 1 649 | 1 650 | 1 651 | 1 652 | 1 653 | 1 654 | 1 655 | 1 656 | 1 657 | 1 658 | 1 659 | 1 660 | 1 661 | 1 662 | 1 663 | 1 664 | 1 665 | 1 666 | 1 667 | 1 668 | 1 669 | 1 670 | 1 671 | 1 672 | 1 673 | 1 674 | 1 675 | 1 676 | 1 677 | 1 678 | 1 679 | 1 680 | 1 681 | 1 682 | 1 683 | 1 -------------------------------------------------------------------------------- /examples/ex_02_advanced_options.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import pprint 4 | import riskslim 5 | 6 | # data 7 | data_name = "breastcancer" # name of the data 8 | data_dir = os.getcwd() + '/examples/data/' # directory where datasets are stored 9 | data_csv_file = data_dir + data_name + '_data.csv' # csv file for the dataset 10 | sample_weights_csv_file = None # csv file of sample weights for the dataset (optional) 11 | 12 | # problem parameters 13 | max_coefficient = 5 # value of largest/smallest coefficient 14 | max_L0_value = 5 # maximum model size 15 | max_offset = 50 # maximum value of offset parameter (optional) 16 | c0_value = 1e-6 # L0-penalty parameter such that c0_value > 0; larger values -> sparser models; we set to a small value (1e-6) so that we get a model with max_L0_value terms 17 | w_pos = 1.00 # relative weight on examples with y = +1; w_neg = 1.00 (optional) 18 | 19 | # load dataset 20 | data = riskslim.load_data_from_csv(dataset_csv_file = data_csv_file, sample_weights_csv_file = sample_weights_csv_file) 21 | N, P = data['X'].shape 22 | 23 | # coefficient set 24 | coef_set = riskslim.CoefficientSet(variable_names = data['variable_names'], lb=-max_coefficient, ub=max_coefficient, sign=0) 25 | coef_set.update_intercept_bounds(X = data['X'], y = data['Y'], max_offset = max_offset) 26 | 27 | # create constraint dictionary 28 | N, P = data['X'].shape 29 | trivial_L0_max = P - np.sum(coef_set.C_0j == 0) 30 | max_L0_value = min(max_L0_value, trivial_L0_max) 31 | 32 | constraints = { 33 | 'L0_min': 0, 34 | 'L0_max': max_L0_value, 35 | 'coef_set': coef_set, 36 | } 37 | 38 | # Run RiskSLIM 39 | settings = { 40 | # 41 | 'c0_value': c0_value, 42 | 'w_pos': w_pos, 43 | # 44 | # LCPA Settings 45 | 'max_runtime': 300.0, # max runtime for LCPA 46 | 'max_tolerance': np.finfo('float').eps, # tolerance to stop LCPA (set to 0 to return provably optimal solution) 47 | 'display_cplex_progress': True, # set to True to print CPLEX progress 48 | 'loss_computation': 'lookup', # how to compute the loss function ('normal','fast','lookup') 49 | # 50 | # Other LCPA Heuristics 51 | 'chained_updates_flag': True, # use chained updates 52 | 'add_cuts_at_heuristic_solutions': True, # add cuts at integer feasible solutions found using polishing/rounding 53 | # 54 | # LCPA Rounding Heuristic 55 | 'round_flag': False, # round continuous solutions with SeqRd 56 | 'polish_rounded_solutions': True, # polish solutions rounded with SeqRd using DCD 57 | 'rounding_tolerance': float('inf'), # only solutions with objective value < (1 + tol) are rounded 58 | 'rounding_start_cuts': 0, # cuts needed to start using rounding heuristic 59 | 'rounding_start_gap': float('inf'), # optimality gap needed to start using rounding heuristic 60 | 'rounding_stop_cuts': 20000, # cuts needed to stop using rounding heuristic 61 | 'rounding_stop_gap': 0.2, # optimality gap needed to stop using rounding heuristic 62 | # 63 | # LCPA Polishing Heuristic 64 | 'polish_flag': False, # polish integer feasible solutions with DCD 65 | 'polishing_tolerance': 0.1, # only solutions with objective value (1 + tol) are polished. 66 | 'polishing_max_runtime': 10.0, # max time to run polishing each time 67 | 'polishing_max_solutions': 5.0, # max # of solutions to polish each time 68 | 'polishing_start_cuts': 0, # cuts needed to start using polishing heuristic 69 | 'polishing_start_gap': float('inf'), # min optimality gap needed to start using polishing heuristic 70 | 'polishing_stop_cuts': float('inf'), # cuts needed to stop using polishing heuristic 71 | 'polishing_stop_gap': 0.0, # max optimality gap required to stop using polishing heuristic 72 | # 73 | # Initialization Procedure 74 | 'initialization_flag': True, # use initialization procedure 75 | 'init_display_progress': True, # show progress of initialization procedure 76 | 'init_display_cplex_progress': False, # show progress of CPLEX during intialization procedure 77 | # 78 | 'init_max_runtime': 300.0, # max time to run CPA in initialization procedure 79 | 'init_max_iterations': 10000, # max # of cuts needed to stop CPA 80 | 'init_max_tolerance': 0.0001, # tolerance of solution to stop CPA 81 | 'init_max_runtime_per_iteration': 300.0, # max time per iteration of CPA 82 | 'init_max_cplex_time_per_iteration': 10.0, # max time per iteration to solve surrogate problem in CPA 83 | # 84 | 'init_use_rounding': True, # use Rd in initialization procedure 85 | 'init_rounding_max_runtime': 30.0, # max runtime for Rd in initialization procedure 86 | 'init_rounding_max_solutions': 5, # max solutions to round using Rd 87 | # 88 | 'init_use_sequential_rounding': True, # use SeqRd in initialization procedure 89 | 'init_sequential_rounding_max_runtime': 10.0, # max runtime for SeqRd in initialization procedure 90 | 'init_sequential_rounding_max_solutions': 5, # max solutions to round using SeqRd 91 | # 92 | 'init_polishing_after': True, # polish after rounding 93 | 'init_polishing_max_runtime': 30.0, # max runtime for polishing 94 | 'init_polishing_max_solutions': 5, # max solutions to polish 95 | # 96 | # CPLEX Solver Parameters 97 | 'cplex_randomseed': 0, # random seed 98 | 'cplex_mipemphasis': 0, # cplex MIP strategy 99 | } 100 | 101 | # train model using lattice_cpa 102 | model_info, mip_info, lcpa_info = riskslim.run_lattice_cpa(data, constraints, settings) 103 | 104 | #model info contains key results 105 | pprint.pprint(model_info) 106 | riskslim.print_model(model_info['solution'], data) 107 | 108 | # mip_output contains information to access the MIP 109 | mip_info['risk_slim_mip'] #CPLEX mip 110 | mip_info['risk_slim_idx'] #indices of the relevant constraints 111 | 112 | # lcpa_output contains detailed information about LCPA 113 | pprint.pprint(lcpa_info) 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /riskslim/tests/test_risk_slim.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pprint 3 | 4 | import numpy as np 5 | import riskslim 6 | 7 | # Dataset Strategy 8 | # 9 | # variables: binary, real, 10 | # N+: 0, 1, >1 11 | # N-: 0, 1, >1 12 | 13 | 14 | # Testing Strategy 15 | # 16 | # loss_computation normal, fast, lookup 17 | # max_coefficient 0, 1, >1 18 | # max_L0_value 0, 1, >1 19 | # max_offset 0, 1, Inf 20 | # c0_value eps, 1e-8, 0.01, C0_max 21 | # sample_weights no, yes 22 | # w_pos 1.00, < 1.00, > 1.00 23 | # initialization on, off 24 | # chained_updates on, off 25 | # polishing on, off 26 | # seq_rd on, off 27 | 28 | # data 29 | data_name = "breastcancer" # name of the data 30 | data_dir = os.getcwd() + '/examples/data/' # directory where datasets are stored 31 | data_csv_file = data_dir + data_name + '_data.csv' # csv file for the dataset 32 | sample_weights_csv_file = None # csv file of sample weights for the dataset (optional) 33 | 34 | default_settings = { 35 | # 36 | 'c0_value': 1e-6, 37 | 'w_pos': 1.00, 38 | # 39 | # LCPA Settings 40 | 'max_runtime': 300.0, # max runtime for LCPA 41 | 'max_tolerance': np.finfo('float').eps, # tolerance to stop LCPA (set to 0 to return provably optimal solution) 42 | 'display_cplex_progress': True, # set to True to print CPLEX progress 43 | 'loss_computation': 'normal', # how to compute the loss function ('normal','fast','lookup') 44 | 'tight_formulation': True, # use a slightly formulation of surrogate MIP that provides a slightly improved formulation 45 | # 46 | # Other LCPA Heuristics 47 | 'chained_updates_flag': True, # use chained updates 48 | 'add_cuts_at_heuristic_solutions': True, # add cuts at integer feasible solutions found using polishing/rounding 49 | # 50 | # LCPA Rounding Heuristic 51 | 'round_flag': True, # round continuous solutions with SeqRd 52 | 'polish_rounded_solutions': True, # polish solutions rounded with SeqRd using DCD 53 | 'rounding_tolerance': float('inf'), # only solutions with objective value < (1 + tol) are rounded 54 | 'rounding_start_cuts': 0, # cuts needed to start using rounding heuristic 55 | 'rounding_start_gap': float('inf'), # optimality gap needed to start using rounding heuristic 56 | 'rounding_stop_cuts': 20000, # cuts needed to stop using rounding heuristic 57 | 'rounding_stop_gap': 0.2, # optimality gap needed to stop using rounding heuristic 58 | # 59 | # LCPA Polishing Heuristic 60 | 'polish_flag': True, # polish integer feasible solutions with DCD 61 | 'polishing_tolerance': 0.1, # only solutions with objective value (1 + tol) are polished. 62 | 'polishing_max_runtime': 10.0, # max time to run polishing each time 63 | 'polishing_max_solutions': 5.0, # max # of solutions to polish each time 64 | 'polishing_start_cuts': 0, # cuts needed to start using polishing heuristic 65 | 'polishing_start_gap': float('inf'), # min optimality gap needed to start using polishing heuristic 66 | 'polishing_stop_cuts': float('inf'), # cuts needed to stop using polishing heuristic 67 | 'polishing_stop_gap': 5.0, # max optimality gap required to stop using polishing heuristic 68 | # 69 | # Initialization Procedure 70 | 'initialization_flag': False, # use initialization procedure 71 | 'init_display_progress': True, # show progress of initialization procedure 72 | 'init_display_cplex_progress': False, # show progress of CPLEX during intialization procedure 73 | # 74 | 'init_max_runtime': 300.0, # max time to run CPA in initialization procedure 75 | 'init_max_iterations': 10000, # max # of cuts needed to stop CPA 76 | 'init_max_tolerance': 0.0001, # tolerance of solution to stop CPA 77 | 'init_max_runtime_per_iteration': 300.0, # max time per iteration of CPA 78 | 'init_max_cplex_time_per_iteration': 10.0, # max time per iteration to solve surrogate problem in CPA 79 | # 80 | 'init_use_sequential_rounding': True, # use SeqRd in initialization procedure 81 | 'init_sequential_rounding_max_runtime': 30.0, # max runtime for SeqRd in initialization procedure 82 | 'init_sequential_rounding_max_solutions': 5, # max solutions to round using SeqRd 83 | 'init_polishing_after': True, # polish after rounding 84 | 'init_polishing_max_runtime': 30.0, # max runtime for polishing 85 | 'init_polishing_max_solutions': 5, # max solutions to polish 86 | # 87 | # CPLEX Solver Parameters 88 | 'cplex_randomseed': 0, # random seed 89 | 'cplex_mipemphasis': 0, # cplex MIP strategy 90 | } 91 | 92 | 93 | def test_risk_slim(data_csv_file, sample_weights_csv_file = None, max_coefficient = 5, max_L0_value = 5, max_offset = 50, c0_value = 1e-6, w_pos = 1.00, settings = None): 94 | 95 | # load dataset 96 | data = riskslim.load_data_from_csv(dataset_csv_file = data_csv_file, sample_weights_csv_file = sample_weights_csv_file) 97 | N, P = data['X'].shape 98 | 99 | # offset value 100 | coef_set = riskslim.CoefficientSet(variable_names=data['variable_names'], lb=-max_coefficient, ub=max_coefficient, sign=0) 101 | coef_set.update_intercept_bounds(X = data['X'], y = data['Y'], max_offset = max_offset, max_L0_value = max_L0_value) 102 | 103 | # create constraint dictionary 104 | trivial_L0_max = P - np.sum(coef_set.C_0j == 0) 105 | max_L0_value = min(max_L0_value, trivial_L0_max) 106 | 107 | constraints = { 108 | 'L0_min': 0, 109 | 'L0_max': max_L0_value, 110 | 'coef_set':coef_set, 111 | } 112 | 113 | # Train model using lattice_cpa 114 | model_info, mip_info, lcpa_info = riskslim.run_lattice_cpa(data, constraints, settings) 115 | 116 | #model info contains key results 117 | pprint.pprint(model_info) 118 | 119 | # lcpa_output contains detailed information about LCPA 120 | pprint.pprint(lcpa_info) 121 | 122 | return True 123 | 124 | 125 | test_risk_slim(data_csv_file = data_csv_file, max_coefficient = 5, max_L0_value = 5, max_offset = 50, settings = default_settings) 126 | test_risk_slim(data_csv_file = data_csv_file, max_coefficient = 5, max_L0_value = 1, max_offset = 50, settings = default_settings) 127 | test_risk_slim(data_csv_file = data_csv_file, max_coefficient = 5, max_L0_value = 0, max_offset = 50, settings = default_settings) 128 | test_risk_slim(data_csv_file = data_csv_file, max_coefficient = 5, max_L0_value = 0, max_offset = 0, settings = default_settings) 129 | 130 | 131 | -------------------------------------------------------------------------------- /riskslim/loss_functions/lookup_log_loss.pyx: -------------------------------------------------------------------------------- 1 | import cython 2 | import numpy as np 3 | cimport numpy as np 4 | cimport scipy.linalg.cython_blas as blas 5 | cimport libc.math as math 6 | 7 | DTYPE = np.float64 8 | ctypedef np.float64_t DTYPE_t 9 | 10 | #create loss_value_table for logistic loss 11 | @cython.boundscheck(False) 12 | @cython.wraparound(False) 13 | @cython.nonecheck(False) 14 | @cython.cdivision(False) 15 | def get_loss_value_table(int min_score, int max_score): 16 | 17 | cdef: 18 | int lookup_offset = -min_score 19 | np.ndarray[DTYPE_t, ndim=1, mode = "fortran"] loss_value_table = np.empty(max_score - min_score + 1, dtype = DTYPE) 20 | Py_ssize_t i = 0 21 | int s = min_score 22 | 23 | while (s < 0): 24 | loss_value_table[i] = math.log(1.0 + math.exp(s)) - s 25 | i += 1 26 | s += 1 27 | 28 | if s == 0: 29 | loss_value_table[i] = math.M_LN2 30 | i += 1 31 | s += 1 32 | 33 | while s <= max_score: 34 | loss_value_table[i] = math.log1p(math.exp(-s)) 35 | i += 1 36 | s += 1 37 | return loss_value_table, lookup_offset 38 | 39 | #create prob_value_table for logistic loss 40 | @cython.boundscheck(False) 41 | @cython.wraparound(False) 42 | @cython.nonecheck(False) 43 | @cython.cdivision(False) 44 | def get_prob_value_table(int min_score, int max_score): 45 | 46 | cdef: 47 | int lookup_offset = -min_score 48 | np.ndarray[DTYPE_t, ndim=1, mode = "fortran"] prob_value_table = np.empty(max_score - min_score + 1, dtype = DTYPE) 49 | Py_ssize_t i = 0 50 | DTYPE_t exp_value 51 | int s = min_score 52 | 53 | while (s < 0): 54 | exp_value = math.exp(s) 55 | prob_value_table[i] = (exp_value / (1.0 + exp_value)) - 1.0 56 | i += 1 57 | s += 1 58 | 59 | if (s == 0): 60 | prob_value_table[i] = -0.5 61 | i += 1 62 | s += 1 63 | 64 | while (s <= max_score): 65 | exp_value = math.exp(-s) 66 | prob_value_table[i] = (1.0 / (1.0 + exp_value)) - 1.0 67 | i += 1 68 | s += 1 69 | 70 | return prob_value_table, lookup_offset 71 | 72 | #create both loss and prob tables for logistic loss 73 | @cython.boundscheck(False) 74 | @cython.wraparound(False) 75 | @cython.nonecheck(False) 76 | @cython.cdivision(False) 77 | def get_loss_value_and_prob_tables(int min_score, int max_score): 78 | 79 | cdef: 80 | int lookup_offset = -min_score 81 | int table_size = max_score - min_score + 1 82 | np.ndarray[DTYPE_t, ndim=1, mode = "fortran"] loss_value_table = np.empty(table_size, dtype = DTYPE) 83 | np.ndarray[DTYPE_t, ndim=1, mode = "fortran"] prob_value_table = np.empty(table_size, dtype = DTYPE) 84 | Py_ssize_t i = 0 85 | DTYPE_t exp_value 86 | int s = min_score 87 | 88 | while (s < 0): 89 | exp_value = math.exp(s) 90 | loss_value_table[i] = math.log(1.0 + exp_value) - s 91 | prob_value_table[i] = (exp_value / (1.0 + exp_value)) - 1.0 92 | i += 1 93 | s += 1 94 | 95 | if (s == 0): 96 | loss_value_table[i] = math.M_LN2 97 | prob_value_table[i] = -0.5 98 | i += 1 99 | s += 1 100 | 101 | while (s <= max_score): 102 | exp_value = math.exp(-s) 103 | loss_value_table[i] = math.log1p(exp_value) 104 | prob_value_table[i] = (1.0 / (1.0 + exp_value)) - 1.0 105 | i += 1 106 | s += 1 107 | 108 | return loss_value_table, prob_value_table, lookup_offset 109 | 110 | ############################################################################################################## 111 | ############################################################################################################## 112 | 113 | @cython.boundscheck(False) 114 | @cython.wraparound(False) 115 | @cython.nonecheck(False) 116 | @cython.cdivision(False) 117 | def log_loss_value(np.ndarray[DTYPE_t, ndim=2, mode="fortran"] Z, 118 | np.ndarray[DTYPE_t, ndim=1, mode="fortran"] rho, 119 | np.ndarray[DTYPE_t, ndim=1, mode="fortran"] loss_value_table, 120 | int lookup_offset): 121 | 122 | cdef: 123 | int N = Z.shape[0] 124 | int D = Z.shape[1] 125 | int incx = 1 #increments of rho 126 | int incy = 1 #increments of y 127 | double alpha = 1.0 128 | double beta = 0.0 129 | np.ndarray[DTYPE_t, ndim=1, mode = "fortran"] y = np.empty(N, dtype = DTYPE) 130 | Py_ssize_t i 131 | DTYPE_t total_loss = 0.0 132 | 133 | #get scores using dgemv, which computes: y <- alpha * trans(Z) + beta * y 134 | #see also: (http://www.nag.com/numeric/fl/nagdoc_fl22/xhtml/F06/f06paf.xml) 135 | blas.dgemv("N", &N, &D, &alpha, &Z[0,0], &N, &rho[0], &incx, &beta, &y[0], &incy) 136 | 137 | #compute loss 138 | for i in range(N): 139 | total_loss += loss_value_table[(y[i]) + lookup_offset] 140 | 141 | return total_loss/N 142 | 143 | @cython.boundscheck(False) 144 | @cython.wraparound(False) 145 | @cython.nonecheck(False) 146 | @cython.cdivision(False) 147 | def log_loss_value_from_scores( 148 | np.ndarray[DTYPE_t, ndim=1, mode="fortran"] scores, 149 | np.ndarray[DTYPE_t, ndim=1, mode="fortran"] loss_value_table, 150 | int lookup_offset): 151 | 152 | cdef: 153 | Py_ssize_t i 154 | Py_ssize_t N = scores.shape[0] 155 | DTYPE_t total_loss = 0.0 156 | 157 | #compute loss 158 | for i in range(N): 159 | total_loss += loss_value_table[((scores[i]) + lookup_offset)] 160 | 161 | return total_loss/N 162 | 163 | @cython.boundscheck(False) 164 | @cython.wraparound(False) 165 | @cython.nonecheck(False) 166 | @cython.cdivision(False) 167 | def log_loss_value_and_slope( 168 | np.ndarray[DTYPE_t, ndim=2, mode="fortran"] Z, 169 | np.ndarray[DTYPE_t, ndim=1, mode="fortran"] rho, 170 | np.ndarray[DTYPE_t, ndim=1, mode="fortran"] loss_value_table, 171 | np.ndarray[DTYPE_t, ndim=1, mode="fortran"] prob_value_table, 172 | int lookup_offset): 173 | 174 | cdef: 175 | int N = Z.shape[0] 176 | int D = Z.shape[1] 177 | int lda = N 178 | int incx = 1 #increments of rho 179 | int incy = 1 #increments of y 180 | double alpha = 1.0 181 | double beta = 0.0 182 | Py_ssize_t i 183 | int lookup_index 184 | DTYPE_t total_loss = 0.0 185 | np.ndarray[DTYPE_t, ndim=1, mode = "fortran"] y = np.empty(N, dtype = DTYPE) 186 | np.ndarray[DTYPE_t, ndim=1, mode = "fortran"] loss_slope = np.empty(D, dtype = DTYPE) 187 | 188 | #get scores using dgemv, which computes: y <- alpha * trans(Z) + beta * y 189 | #see also: (http://www.nag.com/numeric/fl/nagdoc_fl22/xhtml/F06/f06paf.xml) 190 | blas.dgemv("N", &N, &D, &alpha, &Z[0,0], &lda, &rho[0], &incx, &beta, &y[0], &incy) 191 | 192 | #exponentiate scores, compute mean scores and probabilities 193 | for i in range(N): 194 | lookup_index = ( y[i]) + lookup_offset 195 | total_loss += loss_value_table[lookup_index] 196 | y[i] = prob_value_table[lookup_index] 197 | 198 | #compute loss slope 199 | alpha = 1.0/N 200 | blas.dgemv("T", &N, &D, &alpha, &Z[0,0], &lda, &y[0], &incx, &beta, &loss_slope[0], &incy) 201 | 202 | return (total_loss/N), loss_slope 203 | -------------------------------------------------------------------------------- /riskslim/bound_tightening.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def chained_updates(bounds, C_0_nnz, new_objval_at_feasible = None, new_objval_at_relaxation = None, MAX_CHAIN_COUNT = 20): 5 | 6 | new_bounds = dict(bounds) 7 | 8 | # update objval_min using new_value (only done once) 9 | if new_objval_at_relaxation is not None: 10 | if new_bounds['objval_min'] < new_objval_at_relaxation: 11 | new_bounds['objval_min'] = new_objval_at_relaxation 12 | 13 | # update objval_max using new_value (only done once) 14 | if new_objval_at_feasible is not None: 15 | if new_bounds['objval_max'] > new_objval_at_feasible: 16 | new_bounds['objval_max'] = new_objval_at_feasible 17 | 18 | # we have already converged 19 | if new_bounds['objval_max'] <= new_bounds['objval_min']: 20 | new_bounds['objval_max'] = max(new_bounds['objval_max'], new_bounds['objval_min']) 21 | new_bounds['objval_min'] = min(new_bounds['objval_max'], new_bounds['objval_min']) 22 | new_bounds['loss_max'] = min(new_bounds['objval_max'], new_bounds['loss_max']) 23 | return new_bounds 24 | 25 | # start update chain 26 | chain_count = 0 27 | improved_bounds = True 28 | 29 | while improved_bounds and chain_count < MAX_CHAIN_COUNT: 30 | 31 | improved_bounds = False 32 | L0_penalty_min = np.sum(np.sort(C_0_nnz)[np.arange(int(new_bounds['L0_min']))]) 33 | L0_penalty_max = np.sum(-np.sort(-C_0_nnz)[np.arange(int(new_bounds['L0_max']))]) 34 | 35 | # loss_min 36 | if new_bounds['objval_min'] > L0_penalty_max: 37 | proposed_loss_min = new_bounds['objval_min'] - L0_penalty_max 38 | if proposed_loss_min > new_bounds['loss_min']: 39 | new_bounds['loss_min'] = proposed_loss_min 40 | improved_bounds = True 41 | 42 | # L0_min 43 | if new_bounds['objval_min'] > new_bounds['loss_max']: 44 | proposed_L0_min = np.ceil((new_bounds['objval_min'] - new_bounds['loss_max']) / np.min(C_0_nnz)) 45 | if proposed_L0_min > new_bounds['L0_min']: 46 | new_bounds['L0_min'] = proposed_L0_min 47 | improved_bounds = True 48 | 49 | # objval_min = max(objval_min, loss_min + L0_penalty_min) 50 | proposed_objval_min = min(new_bounds['loss_min'], L0_penalty_min) 51 | if proposed_objval_min > new_bounds['objval_min']: 52 | new_bounds['objval_min'] = proposed_objval_min 53 | improved_bounds = True 54 | 55 | # loss max 56 | if new_bounds['objval_max'] > L0_penalty_min: 57 | proposed_loss_max = new_bounds['objval_max'] - L0_penalty_min 58 | if proposed_loss_max < new_bounds['loss_max']: 59 | new_bounds['loss_max'] = proposed_loss_max 60 | improved_bounds = True 61 | 62 | # L0_max 63 | if new_bounds['objval_max'] > new_bounds['loss_min']: 64 | proposed_L0_max = np.floor((new_bounds['objval_max'] - new_bounds['loss_min']) / np.min(C_0_nnz)) 65 | if proposed_L0_max < new_bounds['L0_max']: 66 | new_bounds['L0_max'] = proposed_L0_max 67 | improved_bounds = True 68 | 69 | # objval_max = min(objval_max, loss_max + penalty_max) 70 | proposed_objval_max = new_bounds['loss_max'] + L0_penalty_max 71 | if proposed_objval_max < new_bounds['objval_max']: 72 | new_bounds['objval_max'] = proposed_objval_max 73 | improved_bounds = True 74 | 75 | chain_count += 1 76 | 77 | return new_bounds 78 | 79 | 80 | def chained_updates_for_lp(bounds, C_0_nnz, new_objval_at_feasible = None, new_objval_at_relaxation = None, MAX_CHAIN_COUNT = 20): 81 | 82 | new_bounds = dict(bounds) 83 | 84 | # update objval_min using new_value (only done once) 85 | if new_objval_at_relaxation is not None: 86 | if new_bounds['objval_min'] < new_objval_at_relaxation: 87 | new_bounds['objval_min'] = new_objval_at_relaxation 88 | 89 | # update objval_max using new_value (only done once) 90 | if new_objval_at_feasible is not None: 91 | if new_bounds['objval_max'] > new_objval_at_feasible: 92 | new_bounds['objval_max'] = new_objval_at_feasible 93 | 94 | if new_bounds['objval_max'] <= new_bounds['objval_min']: 95 | new_bounds['objval_max'] = max(new_bounds['objval_max'], new_bounds['objval_min']) 96 | new_bounds['objval_min'] = min(new_bounds['objval_max'], new_bounds['objval_min']) 97 | new_bounds['loss_max'] = min(new_bounds['objval_max'], new_bounds['loss_max']) 98 | return new_bounds 99 | 100 | # start update chain 101 | chain_count = 0 102 | improved_bounds = True 103 | C_0_min = np.min(C_0_nnz) 104 | C_0_max = np.max(C_0_nnz) 105 | L0_penalty_min = C_0_min * new_bounds['L0_min'] 106 | L0_penalty_max = min(C_0_max * new_bounds['L0_max'], new_bounds['objval_max']) 107 | 108 | while improved_bounds and chain_count < MAX_CHAIN_COUNT: 109 | 110 | improved_bounds = False 111 | # loss_min 112 | if new_bounds['objval_min'] > L0_penalty_max: 113 | proposed_loss_min = new_bounds['objval_min'] - L0_penalty_max 114 | if proposed_loss_min > new_bounds['loss_min']: 115 | new_bounds['loss_min'] = proposed_loss_min 116 | improved_bounds = True 117 | 118 | # L0_min and L0_penalty_min 119 | if new_bounds['objval_min'] > new_bounds['loss_max']: 120 | proposed_L0_min = (new_bounds['objval_min'] - new_bounds['loss_max']) / C_0_min 121 | if proposed_L0_min > new_bounds['L0_min']: 122 | new_bounds['L0_min'] = proposed_L0_min 123 | L0_penalty_min = max(L0_penalty_min, C_0_min * proposed_L0_min) 124 | improved_bounds = True 125 | 126 | # objval_min = max(objval_min, loss_min + L0_penalty_min) 127 | proposed_objval_min = min(new_bounds['loss_min'], L0_penalty_min) 128 | if proposed_objval_min > new_bounds['objval_min']: 129 | new_bounds['objval_min'] = proposed_objval_min 130 | improved_bounds = True 131 | 132 | # loss max 133 | if new_bounds['objval_max'] > L0_penalty_min: 134 | proposed_loss_max = new_bounds['objval_max'] - L0_penalty_min 135 | if proposed_loss_max < new_bounds['loss_max']: 136 | new_bounds['loss_max'] = proposed_loss_max 137 | improved_bounds = True 138 | 139 | # L0_max and L0_penalty_max 140 | if new_bounds['objval_max'] > new_bounds['loss_min']: 141 | proposed_L0_max = (new_bounds['objval_max'] - new_bounds['loss_min']) / C_0_min 142 | if proposed_L0_max < new_bounds['L0_max']: 143 | new_bounds['L0_max'] = proposed_L0_max 144 | L0_penalty_max = min(L0_penalty_max, C_0_max * proposed_L0_max) 145 | improved_bounds = True 146 | 147 | # objval_max = min(objval_max, loss_max + penalty_max) 148 | proposed_objval_max = new_bounds['loss_max'] + L0_penalty_max 149 | if proposed_objval_max < new_bounds['objval_max']: 150 | new_bounds['objval_max'] = proposed_objval_max 151 | L0_penalty_max = min(L0_penalty_max, proposed_objval_max) 152 | improved_bounds = True 153 | 154 | chain_count += 1 155 | 156 | return new_bounds 157 | -------------------------------------------------------------------------------- /riskslim/tests/test_loss_functions.py: -------------------------------------------------------------------------------- 1 | #noinspection 2 | import numpy as np 3 | 4 | import riskslim.loss_functions.fast_log_loss as fast 5 | import riskslim.loss_functions.log_loss as normal 6 | import riskslim.loss_functions.log_loss_weighted as weighted 7 | import riskslim.loss_functions.lookup_log_loss as lookup 8 | from riskslim.setup_functions import _setup_training_weights 9 | 10 | np.random.seed(seed = 0) 11 | 12 | #initialize data matrix X and label vector Y 13 | n_rows = 1000000 14 | n_cols = 20 15 | rho_ub = 100 16 | rho_lb = -100 17 | 18 | #helper function s 19 | def generate_binary_data(n_rows = 1000000, n_cols = 20): 20 | X = np.random.randint(low=0, high=2, size=(n_rows, n_cols)) 21 | Y = np.random.randint(low=0, high=2, size=(n_rows, 1)) 22 | pos_ind = Y == 1 23 | Y[~pos_ind] = -1 24 | return X, Y 25 | 26 | def generate_integer_model(n_cols = 20, rho_ub = 100, rho_lb = -100, sparse_pct = 0.5): 27 | rho = np.random.randint(low=rho_lb, high=rho_ub, size=n_cols) 28 | rho = np.require(rho, dtype=Z.dtype, requirements=['F']) 29 | nnz_count = int(sparse_pct * np.floor(n_cols / 2)) 30 | set_to_zero = np.random.choice(range(0, n_cols), size=nnz_count, replace=False) 31 | rho[set_to_zero] = 0.0 32 | return rho 33 | 34 | def get_score_bounds(Z_min, Z_max, rho): 35 | pos_ind = np.where(rho>0.0)[0] 36 | neg_ind = np.where(rho<0.0)[0] 37 | s_min, s_max = 0, 0 38 | 39 | for j in pos_ind: 40 | s_max += rho[j] * Z_max[j] 41 | s_min += rho[j] * Z_min[j] 42 | 43 | for j in neg_ind: 44 | s_max += rho[j] * Z_min[j] 45 | s_min += rho[j] * Z_max[j] 46 | 47 | return s_min, s_max 48 | 49 | def get_score_bounds_from_range(Z_min, Z_max, rho_lb, rho_ub, L0_max = None): 50 | "global variables: L0_reg_ind" 51 | edge_values = np.vstack([Z_min * rho_lb, 52 | Z_max * rho_lb, 53 | Z_min * rho_ub, 54 | Z_max * rho_ub]) 55 | 56 | if L0_max is None or L0_max == Z_min.shape[0]: 57 | s_min = np.sum(np.min(edge_values, axis = 0)) 58 | s_max = np.sum(np.max(edge_values, axis = 0)) 59 | else: 60 | min_values = np.min(edge_values, axis = 0) 61 | s_min_reg = np.sum(np.sort(min_values[L0_reg_ind])[0:L0_max]) 62 | s_min_no_reg = np.sum(min_values[~L0_reg_ind]) 63 | s_min = s_min_reg + s_min_no_reg 64 | 65 | max_values = np.max(edge_values, axis = 0) 66 | s_max_reg = np.sum(-np.sort(-max_values[L0_reg_ind])[0:L0_max]) 67 | s_max_no_reg = np.sum(max_values[~L0_reg_ind]) 68 | s_max = s_max_reg + s_max_no_reg 69 | 70 | return s_min, s_max 71 | 72 | 73 | #generate data 74 | X, Y = generate_binary_data(n_rows, n_cols) 75 | Z = X * Y 76 | Z = np.require(Z, requirements=['F'], dtype=np.float64) 77 | rho = generate_integer_model(n_cols, rho_ub, rho_lb) 78 | L0_reg_ind = np.ones(n_cols, dtype='bool') 79 | L0_reg_ind[0] = False 80 | Z_min = np.min(Z, axis = 0) 81 | Z_max = np.max(Z, axis = 0) 82 | 83 | #setup weights 84 | weights = _setup_training_weights(Y, w_pos = 1.0, w_neg = 1.0, w_total_target = 2.0) 85 | 86 | #create lookup table 87 | min_score, max_score = get_score_bounds_from_range(Z_min, Z_max, rho_lb, rho_ub, L0_max = n_cols) 88 | loss_value_tbl, prob_value_tbl, loss_tbl_offset = lookup.get_loss_value_and_prob_tables(min_score, max_score) 89 | loss_tbl_offset = int(loss_tbl_offset) 90 | 91 | #assert correctnes of log_loss from scores function 92 | for s in range(int(min_score), int(max_score)+1): 93 | normal_value = normal.log_loss_value_from_scores(np.array(s, dtype = Z.dtype, ndmin = 1)) #loss_value_tbl[s+loss_tbl_offset] 94 | cython_value = fast.log_loss_value_from_scores(np.array(s, dtype = Z.dtype, ndmin = 1)) 95 | table_value = loss_value_tbl[s+loss_tbl_offset] 96 | lookup_value = lookup.log_loss_value_from_scores(np.array(s,dtype = Z.dtype, ndmin = 1), loss_value_tbl, loss_tbl_offset) 97 | assert(np.isclose(normal_value, cython_value, rtol = 1e-06)) 98 | assert(np.isclose(table_value, cython_value, rtol = 1e-06)) 99 | assert(np.isclose(table_value, normal_value, rtol = 1e-06)) 100 | assert(np.equal(table_value, lookup_value)) 101 | 102 | 103 | #python implementations need to be 'C' aligned instead of D aligned 104 | Z_py = np.require(Z, requirements = ['C']) 105 | rho_py = np.require(rho, requirements = ['C']) 106 | scores_py = Z_py.dot(rho_py) 107 | 108 | #define tests 109 | def normal_value_test(): return normal.log_loss_value(Z_py, rho_py) 110 | def fast_value_test(): return fast.log_loss_value(Z, rho) 111 | def lookup_value_test(): return lookup.log_loss_value(Z, rho, loss_value_tbl, loss_tbl_offset) 112 | 113 | def normal_cut_test(): return normal.log_loss_value_and_slope(Z_py, rho_py) 114 | def fast_cut_test(): return fast.log_loss_value_and_slope(Z, rho) 115 | def lookup_cut_test(): return lookup.log_loss_value_and_slope(Z, rho, loss_value_tbl, prob_value_tbl, loss_tbl_offset) 116 | 117 | # def dynamic_lookup_value_test(): 118 | # s_min_dynamic, s_max_dynamic = get_score_bounds(Z_min, Z_max, rho) 119 | # tbl, offset = lookup.get_loss_value_table(s_min_dynamic, s_max_dynamic) 120 | # return lookup.log_loss_value(Z, rho, tbl, offset) 121 | 122 | #check values and cuts 123 | normal_cut = normal_cut_test() 124 | cython_cut = fast_cut_test() 125 | lookup_cut = lookup_cut_test() 126 | assert(np.isclose(fast_value_test(), lookup_value_test())) 127 | assert(np.isclose(normal_cut[0], cython_cut[0])) 128 | assert(np.isclose(lookup_cut[0], cython_cut[0])) 129 | assert(all(np.isclose(normal_cut[1], cython_cut[1]))) 130 | assert(all(np.isclose(lookup_cut[1], cython_cut[1]))) 131 | print("passed cut tests") 132 | 133 | 134 | #weighted tests 135 | def weighted_value_test(weights): return weighted.log_loss_value(Z_py, weights, np.sum(weights), rho_py) 136 | def weighted_cut_test(weights): return weighted.log_loss_value_and_slope(Z_py, weights, np.sum(weights), rho_py) 137 | def weighted_scores_test(weights): return weighted.log_loss_value_from_scores(weights, np.sum(weights), scores_py) 138 | 139 | 140 | #w_pos = w_neg = 1.0 141 | weights = _setup_training_weights(Y, w_pos = 1.0, w_neg = 1.0, w_total_target = 2.0) 142 | 143 | weights_match_unit_weights = all(weights == 1.0) 144 | 145 | if weights_match_unit_weights: 146 | print("tests for match between normal and weighted loss function") 147 | #value 148 | assert(np.isclose(normal_value_test(), weighted_value_test(weights))) 149 | assert(np.isclose(normal_value_test(), weighted_scores_test(weights))) 150 | 151 | #cut 152 | normal_cut = normal_cut_test() 153 | weighted_cut = weighted_cut_test(weights) 154 | assert(np.isclose(normal_cut[0], weighted_cut[0])) 155 | assert(all(np.isclose(normal_cut[1], weighted_cut[1]))) 156 | 157 | print("passed all tests for weighted implementations when w_pos = w_neg = 1.0") 158 | 159 | 160 | #w_pos = w_neg = 1.0 161 | w_pos = 0.5 + np.random.rand() 162 | w_neg = 1.0 163 | weights = _setup_training_weights(Y, w_pos = 0.5 + np.random.rand(), w_neg = 1.0, w_total_target = 2.0) 164 | weighted_value = weighted_value_test(weights) 165 | weighted_cut = weighted_cut_test(weights) 166 | weighted_value_from_scores = weighted_scores_test(weights) 167 | 168 | assert(np.isclose(weighted_value, weighted_value_from_scores)) 169 | assert(np.isclose(weighted_value, weighted_cut[0])) 170 | print("passed all tests for weighted loss functions when w_pos = %1.2f and w_neg = %1.2f" % (w_pos, w_neg)) 171 | 172 | 173 | # print 'timing for loss value computation \n' 174 | # %timeit -n 20 normal_value = normal_value_test() 175 | # %timeit -n 20 cython_value = fast_value_test() 176 | # %timeit -n 20 lookup_value = lookup_value_test() 177 | # 178 | # print 'timing for loss cut computation \n' 179 | # %timeit -n 20 normal_cut = normal_cut_test() 180 | # %timeit -n 20 cython_cut = fast_cut_test() 181 | # %timeit -n 20 lookup_cut = lookup_cut_test() 182 | 183 | 184 | -------------------------------------------------------------------------------- /batch/train_risk_slim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | This file is to train a RiskSLIM model in a batch computing environment 5 | It parses command line arguments, and can be called as: 6 | 7 | python train_risk_slim.py --data="${data_file}" --results="${results_file}" 8 | 9 | where: 10 | 11 | data_file csv file containing the training data 12 | results_file file name for the save file; needs to be unique and not already exist on disk 13 | 14 | Use "python train_risk_slim.py --help" for a description of additional arguments. 15 | 16 | Copyright (C) 2017 Berk Ustun 17 | """ 18 | import os 19 | import sys 20 | import time 21 | import argparse 22 | import logging 23 | import pickle 24 | import json 25 | import numpy as np 26 | 27 | # add the source directory to search path to avoid module import errors if riskslim has not been installed 28 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 29 | from riskslim.utils import load_data_from_csv, setup_logging 30 | from riskslim.coefficient_set import CoefficientSet 31 | from riskslim.lattice_cpa import run_lattice_cpa, DEFAULT_LCPA_SETTINGS 32 | 33 | # uncomment for debugging 34 | 35 | # TODO: run the following when building 36 | # with open(settings_json, 'w') as outfile: 37 | # json.dump(DEFAULT_LCPA_SETTINGS, outfile, sort_keys = False, indent=4) 38 | 39 | def setup_parser(): 40 | """ 41 | Create an argparse Parser object for RiskSLIM command line arguments. 42 | This object determines all command line arguments, handles input 43 | validation and default values. 44 | 45 | See https://docs.python.org/3/library/argparse.html for configuration 46 | """ 47 | 48 | #parser helper functions 49 | def is_positive_integer(value): 50 | parsed_value = int(value) 51 | if parsed_value <= 0: 52 | raise argparse.ArgumentTypeError("%s is an invalid positive int value" % value) 53 | return parsed_value 54 | 55 | def is_positive_float(value): 56 | parsed_value = float(value) 57 | if parsed_value <= 0.0: 58 | raise argparse.ArgumentTypeError("%s must be a positive value" % value) 59 | return parsed_value 60 | 61 | def is_negative_one_or_positive_integer(value): 62 | parsed_value = int(value) 63 | if not (parsed_value == -1 or parsed_value >= 1): 64 | raise argparse.ArgumentTypeError("%s is an invalid value (must be -1 or >=1)" % value) 65 | else: 66 | return parsed_value 67 | 68 | def is_file_on_disk(file_name): 69 | if not os.path.isfile(file_name): 70 | raise argparse.ArgumentTypeError("the file %s does not exist!" % file_name) 71 | else: 72 | return file_name 73 | 74 | def is_file_not_on_disk(file_name): 75 | if os.path.isfile(file_name): 76 | raise argparse.ArgumentTypeError("the file %s already exists on disk" % file_name) 77 | else: 78 | return file_name 79 | 80 | def is_valid_fold(value): 81 | parsed_value = int(value) 82 | if parsed_value < 0: 83 | raise argparse.ArgumentTypeError("%s must be a positive integer" % value) 84 | return parsed_value 85 | 86 | parser = argparse.ArgumentParser( 87 | prog='train_risk_slim', 88 | description='Train a RiskSLIM classifier from the command shell', 89 | epilog='Copyright (C) 2017 Berk Ustun', 90 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 91 | ) 92 | 93 | parser.add_argument('--data', 94 | type=str, 95 | required=True, 96 | help='csv file with training data') 97 | 98 | parser.add_argument('--results', 99 | type=str, 100 | required=True, 101 | help='name of results file (must not already exist)') 102 | 103 | parser.add_argument('--cvindices', 104 | type=is_file_on_disk, 105 | help='csv file with indices for K-fold CV') 106 | 107 | parser.add_argument('--fold', 108 | type=is_valid_fold, 109 | default=0, 110 | help='index of test fold; set as 0 to use all data for training') 111 | 112 | parser.add_argument('--weights', 113 | type=is_file_on_disk, 114 | help='csv file with non-negative weights for each point') 115 | 116 | parser.add_argument('--settings', 117 | type=is_file_on_disk, 118 | help='JSON file with additional settings for LCPA') 119 | 120 | parser.add_argument('--timelimit', 121 | type=is_negative_one_or_positive_integer, 122 | default=300, 123 | help='time limit on training (in seconds); set as -1 for no time limit') 124 | 125 | parser.add_argument('--max_size', 126 | type = is_negative_one_or_positive_integer, 127 | default=-1, 128 | help='maximum number of non-zero coefficients; set as -1 for no limit') 129 | 130 | parser.add_argument('--max_coef', 131 | type=is_positive_integer, 132 | default=5, 133 | help='value of upper and lower bounds for any coefficient') 134 | 135 | parser.add_argument('--max_offset', 136 | type=is_negative_one_or_positive_integer, 137 | default=-1, 138 | help='value of upper and lower bound on offset parameter; set as -1 to use a conservative value') 139 | 140 | parser.add_argument('--c0_value', 141 | type=is_positive_float, 142 | default=1e-6, 143 | help='l0 regularization parameter; set as a positive number between 0.00 and log(2)') 144 | 145 | parser.add_argument('--w_pos', 146 | type=is_positive_float, 147 | default=1.00, 148 | help='w_pos') 149 | 150 | parser.add_argument('--log', 151 | type=str, 152 | help='name of the log file') 153 | 154 | parser.add_argument('--silent', 155 | action='store_true', 156 | help='flag to suppress logging to stderr') 157 | 158 | return parser 159 | 160 | if __name__ == '__main__': 161 | 162 | parser = setup_parser() 163 | parsed = parser.parse_args() 164 | parsed_dict = vars(parsed) 165 | parsed_string = [key + ' : ' + str(parsed_dict[key]) + '\n' for key in parsed_dict] 166 | parsed_string.sort() 167 | 168 | # setup logging 169 | logger = logging.getLogger() 170 | logger = setup_logging(logger, log_to_console =(not parsed.silent), log_file = parsed.log) 171 | logger.setLevel(logging.INFO) 172 | logger.info("running 'train_risk_slim.py'") 173 | logger.info("working directory: %r" % os.getcwd()) 174 | logger.info("parsed the following variables:\n-%s" % '-'.join(parsed_string)) 175 | 176 | # check results_file does not exist 177 | if os.path.isfile(parsed.results): 178 | logger.error("results file %s already exists)" % parsed.results) 179 | logger.error("either delete %s or choose a different name" % parsed.results) 180 | sys.exit(1) 181 | 182 | # check settings_json exists / or use default settings 183 | settings = dict(DEFAULT_LCPA_SETTINGS) 184 | if parsed.settings is not None: 185 | with open(parsed.settings) as json_file: 186 | loaded_settings = json.load(json_file) 187 | loaded_settings = {str(key): loaded_settings[key] for key in loaded_settings if key in settings} 188 | settings.update(loaded_settings) 189 | 190 | #overwrite parameters specified by the user 191 | settings['max_runtime'] = float('inf') if parsed.timelimit == -1 else parsed.timelimit 192 | settings['c0_value'] = parsed.c0_value 193 | settings['w_pos'] = parsed.w_pos 194 | 195 | # check if sample weights file was specified, if not set as None 196 | logger.info("loading data and sample weights") 197 | 198 | data = load_data_from_csv(dataset_csv_file = parsed.data, 199 | sample_weights_csv_file = parsed.weights, 200 | fold_csv_file = parsed.cvindices, 201 | fold_num = parsed.fold) 202 | N, P = data['X'].shape 203 | 204 | # initialize coefficient set and offset parameter 205 | logger.info("creating coefficient set and constraints") 206 | max_coefficient = parsed.max_coef 207 | max_model_size = parsed.max_size if parsed.max_size >= 0 else float('inf') 208 | max_offset = parsed.max_offset if parsed.max_offset >= 0 else float('inf') 209 | 210 | coef_set = CoefficientSet(variable_names = data['variable_names'], 211 | lb = -max_coefficient, 212 | ub = max_coefficient, 213 | sign = 0) 214 | coef_set.update_intercept_bounds(X = data['X'], y = data['Y'], max_offset = max_offset, max_L0_value = max_model_size) 215 | 216 | #print coefficient set 217 | if not parsed.silent: 218 | print(coef_set) 219 | 220 | constraints = { 221 | 'L0_min': 0, 222 | 'L0_max': max_model_size, 223 | 'coef_set': coef_set, 224 | } 225 | 226 | # fit RiskSLIM model using Lattice Cutting Plane Algorithm 227 | model_info, mip_info, lcpa_info = run_lattice_cpa(data, constraints, settings) 228 | 229 | # save output to disk 230 | results = { 231 | "date": time.strftime("%d/%m/%y", time.localtime()), 232 | "data_file": parsed.data, 233 | "fold_file": parsed.cvindices, 234 | "fold_num": parsed.settings, 235 | "results_file": parsed.results, 236 | } 237 | results.update(model_info) 238 | 239 | coef_set = results.pop('coef_set') 240 | results['coef_set_ub'] = coef_set.ub 241 | results['coef_set_lb'] = coef_set.lb 242 | results['coef_set_signs'] = coef_set.sign 243 | results['coef_set_c0'] = coef_set.c0 244 | 245 | logger.info("saving results...") 246 | with open(parsed.results, 'wb') as outfile: 247 | pickle.dump(results, outfile, protocol=pickle.HIGHEST_PROTOCOL) 248 | 249 | logger.info("saved results as pickle file: %r" % parsed.results) 250 | logger.info('''to access results, use this snippet: 251 | 252 | \t\t\t import pickle 253 | \t\t\t f = open(results_file, 'rb') 254 | \t\t\t results = pickle.load(f) 255 | ''' 256 | ) 257 | logger.info("finished training") 258 | logger.info("quitting\n\n") 259 | sys.exit(0) 260 | -------------------------------------------------------------------------------- /riskslim/solution_pool.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import prettytable as pt 3 | 4 | class SolutionPool(object): 5 | """ 6 | Helper class used to store solutions to the risk slim optimization problem 7 | """ 8 | 9 | def __init__(self, obj): 10 | 11 | if isinstance(obj, SolutionPool): 12 | 13 | self._P = obj.P 14 | self._objvals = obj.objvals 15 | self._solutions = obj.solutions 16 | 17 | elif isinstance(obj, int): 18 | 19 | assert obj >= 1 20 | self._P = int(obj) 21 | self._objvals = np.empty(0) 22 | self._solutions = np.empty(shape = (0, self._P)) 23 | 24 | elif isinstance(obj, dict): 25 | 26 | assert len(obj) == 2 27 | objvals = np.copy(obj['objvals']).flatten().astype(dtype = np.float_) 28 | solutions = np.copy(obj['solutions']) 29 | n = objvals.size 30 | if solutions.ndim == 2: 31 | assert n in solutions.shape 32 | if solutions.shape[1] == n and solutions.shape[0] != n: 33 | solutions = np.transpose(solutions) 34 | elif solutions.ndim == 1: 35 | assert n == 1 36 | solutions = np.reshape(solutions, (1, solutions.size)) 37 | else: 38 | raise ValueError('solutions has more than 2 dimensions') 39 | 40 | self._P = solutions.shape[1] 41 | self._objvals = objvals 42 | self._solutions = solutions 43 | 44 | else: 45 | raise ValueError('cannot initialize SolutionPool using %s object' % type(obj)) 46 | 47 | 48 | def __len__(self): 49 | return len(self._objvals) 50 | 51 | 52 | @staticmethod 53 | def solution_string(solution, float_fmt = '%1.3f'): 54 | solution_string = '' 55 | for j in range(len(solution)): 56 | if SolutionPool.is_integral(solution[j]): 57 | solution_string += ' ' + str(int(solution[j])) 58 | else: 59 | solution_string += ((' ' + float_fmt) % solution[j]) 60 | return solution_string 61 | 62 | 63 | def table(self): 64 | x = pt.PrettyTable(align = 'r', float_format = '1.3', hrules = pt.ALL) 65 | x.add_column("objval", self._objvals.tolist()) 66 | x.add_column("solution", list(map(self.solution_string, self._solutions))) 67 | return str(x) 68 | 69 | 70 | def __repr__(self): 71 | return self.table() 72 | 73 | 74 | def __str__(self): 75 | return self.table() 76 | 77 | 78 | def copy(self): 79 | return SolutionPool(self) 80 | 81 | 82 | @property 83 | def P(self): 84 | return int(self._P) 85 | 86 | 87 | @property 88 | def objvals(self): 89 | return self._objvals 90 | 91 | 92 | @property 93 | def solutions(self): 94 | return self._solutions 95 | 96 | 97 | @objvals.setter 98 | def objvals(self, objvals): 99 | if hasattr(objvals, "__len__"): 100 | if len(objvals) > 0: 101 | self._objvals = np.copy(list(objvals)).flatten().astype(dtype = np.float_) 102 | elif len(objvals) == 0: 103 | self._objvals = np.empty(0) 104 | else: 105 | self._objvals = float(objvals) 106 | 107 | 108 | @solutions.setter 109 | def solutions(self, solutions): 110 | if solutions.ndim == 2: 111 | assert self._P in solutions.shape 112 | if solutions.shape[0] == self._P and solutions.shape[1] != self._P: 113 | solutions = np.transpose(solutions) 114 | elif solutions.ndim == 1: 115 | solutions = np.reshape(solutions, (1, solutions.size)) 116 | else: 117 | raise ValueError('incorrect solution dimensions') 118 | 119 | self._solutions = np.copy(solutions) 120 | 121 | 122 | def append(self, pool): 123 | if len(pool) == 0: 124 | return self 125 | else: 126 | return self.add(pool.objvals, pool.solutions) 127 | 128 | 129 | def add(self, objvals, solutions): 130 | 131 | if isinstance(objvals, np.ndarray) or isinstance(objvals, list): 132 | n = len(objvals) 133 | if n == 0: 134 | return self 135 | if isinstance(solutions, np.ndarray): 136 | if solutions.ndim == 2: 137 | assert n in solutions.shape 138 | assert self._P in solutions.shape 139 | if solutions.shape[0] == self._P and solutions.shape[1] != self._P: 140 | solutions = np.transpose(solutions) 141 | elif solutions.ndim == 1: 142 | assert n == 1 143 | solutions = np.reshape(solutions, (1, solutions.size)) 144 | else: 145 | raise ValueError('incorrect solution dimensions') 146 | elif isinstance(solutions, list): 147 | solutions = np.array(solutions) 148 | assert solutions.shape[0] == n 149 | assert solutions.shape[1] == self._P 150 | else: 151 | raise TypeError('incorrect solution type') 152 | else: 153 | objvals = float(objvals) #also assertion 154 | solutions = np.reshape(solutions, (1, self._P)) 155 | 156 | self._objvals = np.append(self._objvals, objvals) 157 | self._solutions = np.append(self._solutions, solutions, axis = 0) 158 | return self 159 | 160 | 161 | def filter(self, filter_ind): 162 | idx = np.require(filter_ind, dtype = 'bool').flatten() 163 | if len(self) > 0 and any(idx == 0): 164 | self._objvals = self._objvals[idx] 165 | self._solutions = self._solutions[idx, :] 166 | return self 167 | 168 | 169 | def distinct(self): 170 | if len(self) > 0: 171 | _, idx = np.unique(self._solutions, return_index = True, axis = 0) 172 | self._objvals = self._objvals[idx] 173 | self._solutions = self._solutions[idx, :] 174 | return self 175 | 176 | 177 | def sort(self): 178 | if len(self) > 0: 179 | idx = np.argsort(self._objvals) 180 | self._objvals = self._objvals[idx] 181 | self._solutions = self._solutions[idx, :] 182 | return self 183 | 184 | 185 | def map(self, mapfun, target = 'all'): 186 | assert callable(mapfun), 'map function must be callable' 187 | if target is 'solutions': 188 | return list(map(mapfun, self.solutions)) 189 | elif target is 'objvals': 190 | return list(map(mapfun, self.objvals)) 191 | elif target is 'all': 192 | return list(map(mapfun, self.objvals, self.solutions)) 193 | else: 194 | raise ValueError('target must be either solutions, objvals, or all') 195 | 196 | 197 | @staticmethod 198 | def is_integral(solution): 199 | return np.all(solution == np.require(solution, dtype = 'int_')) 200 | 201 | 202 | def remove_nonintegral(self): 203 | return self.filter(list(map(self.is_integral, self.solutions))) 204 | 205 | 206 | def compute_objvals(self, get_objval): 207 | compute_idx = np.flatnonzero(np.isnan(self._objvals)) 208 | self._objvals[compute_idx] = np.array(list(map(get_objval, self._solutions[compute_idx, :]))) 209 | return self 210 | 211 | 212 | def remove_suboptimal(self, objval_cutoff): 213 | return self.filter(self.objvals <= objval_cutoff) 214 | 215 | 216 | def remove_infeasible(self, is_feasible): 217 | return self.filter(list(map(is_feasible, self.solutions))) 218 | 219 | 220 | class FastSolutionPool(object): 221 | """ 222 | Helper class used to store solutions to the risk slim optimization problem 223 | SolutionQueue designed to work faster than SolutionPool. 224 | It is primarily used by the callback functions in risk_slim 225 | """ 226 | 227 | def __init__(self, P): 228 | self._P = int(P) 229 | self._objvals = np.empty(shape = 0) 230 | self._solutions = np.empty(shape = (0, P)) 231 | 232 | 233 | def __len__(self): 234 | return len(self._objvals) 235 | 236 | @property 237 | def P(self): 238 | return self._P 239 | 240 | @property 241 | def objvals(self): 242 | return self._objvals 243 | 244 | @property 245 | def solutions(self): 246 | return self._solutions 247 | 248 | 249 | def add(self, new_objvals, new_solutions): 250 | if isinstance(new_objvals, (np.ndarray, list)): 251 | n = len(new_objvals) 252 | self._objvals = np.append(self._objvals, np.array(new_objvals).astype(dtype = np.float_).flatten()) 253 | else: 254 | n = 1 255 | self._objvals = np.append(self._objvals, float(new_objvals)) 256 | 257 | new_solutions = np.reshape(new_solutions, (n, self._P)) 258 | self._solutions = np.append(self._solutions, new_solutions, axis = 0) 259 | 260 | 261 | def get_best_objval_and_solution(self): 262 | if len(self) > 0: 263 | idx = np.argmin(self._objvals) 264 | return float(self._objvals[idx]), np.copy(self._solutions[idx,]) 265 | else: 266 | return np.empty(shape = 0), np.empty(shape = (0, self.P)) 267 | 268 | 269 | def filter_sort_unique(self, max_objval = float('inf')): 270 | 271 | # filter 272 | if max_objval < float('inf'): 273 | good_idx = np.less_equal(self._objvals, max_objval) 274 | self._objvals = self._objvals[good_idx] 275 | self._solutions = self._solutions[good_idx,] 276 | 277 | if len(self._objvals) >= 2: 278 | _, unique_idx = np.unique(self._solutions, axis = 0, return_index = True) 279 | self._objvals = self._objvals[unique_idx] 280 | self._solutions = self._solutions[unique_idx,] 281 | 282 | if len(self._objvals) >= 2: 283 | sort_idx = np.argsort(self._objvals) 284 | self._objvals = self._objvals[sort_idx] 285 | self._solutions = self._solutions[sort_idx,] 286 | 287 | return self 288 | 289 | 290 | def clear(self): 291 | self._objvals = np.empty(shape = 0) 292 | self._solutions = np.empty(shape = (0, self._P)) 293 | return self 294 | 295 | 296 | def table(self): 297 | x = pt.PrettyTable(align = 'r', float_format = '1.4', hrules=pt.ALL) 298 | x.add_column("objval", self._objvals.tolist()) 299 | x.add_column("solution", list(map(self.solution_string, self._solutions))) 300 | return str(x) 301 | 302 | @staticmethod 303 | def solution_string(solution): 304 | solution_string = '' 305 | for j in range(len(solution)): 306 | if SolutionPool.is_integral(solution[j]): 307 | solution_string += ' ' + str(int(solution[j])) 308 | else: 309 | solution_string += (' %1.4f' % solution[j]) 310 | return solution_string 311 | 312 | def __repr__(self): 313 | return self.table() 314 | 315 | 316 | def __str__(self): 317 | return self.table() -------------------------------------------------------------------------------- /riskslim/setup_functions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .coefficient_set import CoefficientSet, get_score_bounds 3 | from .utils import print_log 4 | 5 | 6 | def setup_loss_functions(data, coef_set, L0_max = None, loss_computation = None, w_pos = 1.0): 7 | """ 8 | 9 | Parameters 10 | ---------- 11 | data 12 | coef_set 13 | L0_max 14 | loss_computation 15 | w_pos 16 | 17 | Returns 18 | ------- 19 | 20 | """ 21 | #todo check if fast/lookup loss is installed 22 | assert loss_computation in [None, 'weighted', 'normal', 'fast', 'lookup'] 23 | 24 | Z = data['X'] * data['Y'] 25 | 26 | if 'sample_weights' in data: 27 | sample_weights = _setup_training_weights(Y = data['Y'], sample_weights = data['sample_weights'], w_pos = w_pos) 28 | use_weighted = not np.all(np.equal(sample_weights, 1.0)) 29 | else: 30 | use_weighted = False 31 | 32 | integer_data_flag = np.all(Z == np.require(Z, dtype = np.int_)) 33 | use_lookup_table = isinstance(coef_set, CoefficientSet) and integer_data_flag 34 | if use_weighted: 35 | final_loss_computation = 'weighted' 36 | elif use_lookup_table: 37 | final_loss_computation = 'lookup' 38 | else: 39 | final_loss_computation = 'fast' 40 | 41 | if final_loss_computation != loss_computation: 42 | print_log("switching loss computation from %s to %s" % (loss_computation, final_loss_computation)) 43 | 44 | if final_loss_computation == 'weighted': 45 | 46 | from riskslim.loss_functions.log_loss_weighted import \ 47 | log_loss_value, \ 48 | log_loss_value_and_slope, \ 49 | log_loss_value_from_scores 50 | 51 | Z = np.require(Z, requirements = ['C']) 52 | total_sample_weights = np.sum(sample_weights) 53 | 54 | compute_loss = lambda rho: log_loss_value(Z, sample_weights, total_sample_weights, rho) 55 | compute_loss_cut = lambda rho: log_loss_value_and_slope(Z, sample_weights, total_sample_weights, rho) 56 | compute_loss_from_scores = lambda scores: log_loss_value_from_scores(sample_weights, total_sample_weights, scores) 57 | 58 | elif final_loss_computation == 'normal': 59 | 60 | from riskslim.loss_functions.log_loss import \ 61 | log_loss_value, \ 62 | log_loss_value_and_slope, \ 63 | log_loss_value_from_scores 64 | 65 | Z = np.require(Z, requirements=['C']) 66 | compute_loss = lambda rho: log_loss_value(Z, rho) 67 | compute_loss_cut = lambda rho: log_loss_value_and_slope(Z, rho) 68 | compute_loss_from_scores = lambda scores: log_loss_value_from_scores(scores) 69 | 70 | elif final_loss_computation == 'fast': 71 | 72 | from riskslim.loss_functions.fast_log_loss import \ 73 | log_loss_value, \ 74 | log_loss_value_and_slope, \ 75 | log_loss_value_from_scores 76 | 77 | Z = np.require(Z, requirements=['F']) 78 | compute_loss = lambda rho: log_loss_value(Z, rho) 79 | compute_loss_cut = lambda rho: log_loss_value_and_slope(Z, rho) 80 | compute_loss_from_scores = lambda scores: log_loss_value_from_scores(scores) 81 | 82 | elif final_loss_computation == 'lookup': 83 | 84 | from riskslim.loss_functions.lookup_log_loss import \ 85 | get_loss_value_and_prob_tables, \ 86 | log_loss_value, \ 87 | log_loss_value_and_slope, \ 88 | log_loss_value_from_scores 89 | 90 | s_min, s_max = get_score_bounds(Z_min = np.min(Z, axis=0), 91 | Z_max = np.max(Z, axis=0), 92 | rho_lb = coef_set.lb, 93 | rho_ub = coef_set.ub, 94 | L0_reg_ind = np.array(coef_set.c0) == 0.0, 95 | L0_max = L0_max) 96 | 97 | 98 | Z = np.require(Z, requirements=['F'], dtype = float) 99 | print_log("%d rows in lookup table" % (s_max - s_min + 1)) 100 | 101 | loss_value_tbl, prob_value_tbl, tbl_offset = get_loss_value_and_prob_tables(s_min, s_max) 102 | compute_loss = lambda rho: log_loss_value(Z, rho, loss_value_tbl, tbl_offset) 103 | compute_loss_cut = lambda rho: log_loss_value_and_slope(Z, rho, loss_value_tbl, prob_value_tbl, tbl_offset) 104 | compute_loss_from_scores = lambda scores: log_loss_value_from_scores(scores, loss_value_tbl, tbl_offset) 105 | 106 | # real loss functions 107 | if final_loss_computation == 'lookup': 108 | 109 | from riskslim.loss_functions.fast_log_loss import \ 110 | log_loss_value as loss_value_real, \ 111 | log_loss_value_and_slope as loss_value_and_slope_real,\ 112 | log_loss_value_from_scores as loss_value_from_scores_real 113 | 114 | compute_loss_real = lambda rho: loss_value_real(Z, rho) 115 | compute_loss_cut_real = lambda rho: loss_value_and_slope_real(Z, rho) 116 | compute_loss_from_scores_real = lambda scores: loss_value_from_scores_real(scores) 117 | 118 | else: 119 | 120 | compute_loss_real = compute_loss 121 | compute_loss_cut_real = compute_loss_cut 122 | compute_loss_from_scores_real = compute_loss_from_scores 123 | 124 | return (Z, 125 | compute_loss, 126 | compute_loss_cut, 127 | compute_loss_from_scores, 128 | compute_loss_real, 129 | compute_loss_cut_real, 130 | compute_loss_from_scores_real) 131 | 132 | 133 | def _setup_training_weights(Y, sample_weights = None, w_pos = 1.0, w_neg = 1.0, w_total_target = 2.0): 134 | 135 | """ 136 | Parameters 137 | ---------- 138 | Y - N x 1 vector with Y = -1,+1 139 | sample_weights - N x 1 vector 140 | w_pos - positive scalar showing relative weight on examples where Y = +1 141 | w_neg - positive scalar showing relative weight on examples where Y = -1 142 | 143 | Returns 144 | ------- 145 | a vector of N training weights for all points in the training data 146 | 147 | """ 148 | 149 | # todo: throw warning if there is no positive/negative point in Y 150 | 151 | # process class weights 152 | assert w_pos > 0.0, 'w_pos must be strictly positive' 153 | assert w_neg > 0.0, 'w_neg must be strictly positive' 154 | assert np.isfinite(w_pos), 'w_pos must be finite' 155 | assert np.isfinite(w_neg), 'w_neg must be finite' 156 | w_total = w_pos + w_neg 157 | w_pos = w_total_target * (w_pos / w_total) 158 | w_neg = w_total_target * (w_neg / w_total) 159 | 160 | # process case weights 161 | Y = Y.flatten() 162 | N = len(Y) 163 | pos_ind = Y == 1 164 | 165 | if sample_weights is None: 166 | training_weights = np.ones(N) 167 | else: 168 | training_weights = sample_weights.flatten() 169 | assert len(training_weights) == N 170 | assert np.all(training_weights >= 0.0) 171 | #todo: throw warning if any training weights = 0 172 | #todo: throw warning if there are no effective positive/negative points in Y 173 | 174 | # normalization 175 | training_weights = N * (training_weights / sum(training_weights)) 176 | training_weights[pos_ind] *= w_pos 177 | training_weights[~pos_ind] *= w_neg 178 | 179 | return training_weights 180 | 181 | 182 | def setup_penalty_parameters(coef_set, c0_value = 1e-6): 183 | """ 184 | 185 | Parameters 186 | ---------- 187 | coef_set 188 | c0_value 189 | 190 | Returns 191 | ------- 192 | c0_value 193 | C_0 194 | L0_reg_ind 195 | C_0_nnz 196 | """ 197 | assert isinstance(coef_set, CoefficientSet) 198 | assert c0_value > 0.0, 'default L0_parameter should be positive' 199 | c0_value = float(c0_value) 200 | C_0 = np.array(coef_set.c0) 201 | L0_reg_ind = np.isnan(C_0) 202 | C_0[L0_reg_ind] = c0_value 203 | C_0_nnz = C_0[L0_reg_ind] 204 | return c0_value, C_0, L0_reg_ind, C_0_nnz 205 | 206 | 207 | def setup_objective_functions(compute_loss, L0_reg_ind, C_0_nnz): 208 | 209 | get_objval = lambda rho: compute_loss(rho) + np.sum(C_0_nnz * (rho[L0_reg_ind] != 0.0)) 210 | get_L0_norm = lambda rho: np.count_nonzero(rho[L0_reg_ind]) 211 | get_L0_penalty = lambda rho: np.sum(C_0_nnz * (rho[L0_reg_ind] != 0.0)) 212 | get_alpha = lambda rho: np.array(abs(rho[L0_reg_ind]) > 0.0, dtype = np.float_) 213 | get_L0_penalty_from_alpha = lambda alpha: np.sum(C_0_nnz * alpha) 214 | 215 | return (get_objval, get_L0_norm, get_L0_penalty, get_alpha, get_L0_penalty_from_alpha) 216 | 217 | 218 | def get_loss_bounds(Z, rho_ub, rho_lb, L0_reg_ind, L0_max = float('nan')): 219 | # min value of loss = log(1+exp(-score)) occurs at max score for each point 220 | # max value of loss = loss(1+exp(-score)) occurs at min score for each point 221 | 222 | rho_lb = np.array(rho_lb) 223 | rho_ub = np.array(rho_ub) 224 | 225 | # get maximum number of regularized coefficients 226 | L0_max = Z.shape[0] if np.isnan(L0_max) else L0_max 227 | num_max_reg_coefs = min(L0_max, sum(L0_reg_ind)) 228 | 229 | # calculate the smallest and largest score that can be attained by each point 230 | scores_at_lb = Z * rho_lb 231 | scores_at_ub = Z * rho_ub 232 | max_scores_matrix = np.maximum(scores_at_ub, scores_at_lb) 233 | min_scores_matrix = np.minimum(scores_at_ub, scores_at_lb) 234 | assert (np.all(max_scores_matrix >= min_scores_matrix)) 235 | 236 | # for each example, compute max sum of scores from top reg coefficients 237 | max_scores_reg = max_scores_matrix[:, L0_reg_ind] 238 | max_scores_reg = -np.sort(-max_scores_reg, axis=1) 239 | max_scores_reg = max_scores_reg[:, 0:num_max_reg_coefs] 240 | max_score_reg = np.sum(max_scores_reg, axis=1) 241 | 242 | # for each example, compute max sum of scores from no reg coefficients 243 | max_scores_no_reg = max_scores_matrix[:, ~L0_reg_ind] 244 | max_score_no_reg = np.sum(max_scores_no_reg, axis=1) 245 | 246 | # max score for each example 247 | max_score = max_score_reg + max_score_no_reg 248 | 249 | # for each example, compute min sum of scores from top reg coefficients 250 | min_scores_reg = min_scores_matrix[:, L0_reg_ind] 251 | min_scores_reg = np.sort(min_scores_reg, axis=1) 252 | min_scores_reg = min_scores_reg[:, 0:num_max_reg_coefs] 253 | min_score_reg = np.sum(min_scores_reg, axis=1) 254 | 255 | # for each example, compute min sum of scores from no reg coefficients 256 | min_scores_no_reg = min_scores_matrix[:, ~L0_reg_ind] 257 | min_score_no_reg = np.sum(min_scores_no_reg, axis=1) 258 | 259 | min_score = min_score_reg + min_score_no_reg 260 | assert (np.all(max_score >= min_score)) 261 | 262 | # compute min loss 263 | idx = max_score > 0 264 | min_loss = np.empty_like(max_score) 265 | min_loss[idx] = np.log1p(np.exp(-max_score[idx])) 266 | min_loss[~idx] = np.log1p(np.exp(max_score[~idx])) - max_score[~idx] 267 | min_loss = min_loss.mean() 268 | 269 | # compute max loss 270 | idx = min_score > 0 271 | max_loss = np.empty_like(min_score) 272 | max_loss[idx] = np.log1p(np.exp(-min_score[idx])) 273 | max_loss[~idx] = np.log1p(np.exp(min_score[~idx])) - min_score[~idx] 274 | max_loss = max_loss.mean() 275 | 276 | return min_loss, max_loss 277 | -------------------------------------------------------------------------------- /riskslim/heuristics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | #todo: finish specifications 4 | #todo: add input checking (with ability to turn off) 5 | #todo: Cython implementation 6 | 7 | def sequential_rounding(rho, Z, C_0, compute_loss_from_scores_real, get_L0_penalty, objval_cutoff = float('Inf')): 8 | """ 9 | 10 | Parameters 11 | ---------- 12 | rho: P x 1 vector of continuous coefficients 13 | Z: N x P data matrix computed as X * Y 14 | C_0: N x 1 vector of L0 penalties. C_0[j] = L0 penalty for rho[j] for j = 0,..., P. 15 | compute_loss_from_scores_real: function handle to compute loss using N x 1 vector of scores, where scores = Z.dot(rho) 16 | get_L0_penalty: function handle to compute L0_penalty from rho 17 | objval_cutoff: objective value used for early stopping. 18 | the procedure will stop if the objective value achieved by an intermediate solution will exceeds objval_cutoff 19 | 20 | Returns 21 | ------- 22 | 23 | rho: P x 1 vector of integer coefficients (if early_stop_flag = False, otherwise continuous solution) 24 | best_objval: objective value achieved by rho (if early_stop_flag = False, otherwise NaN) 25 | early_stop_flag: True if procedure was stopped early (in which case rho is not integer feasible) 26 | 27 | """ 28 | 29 | assert callable(compute_loss_from_scores_real) 30 | assert callable(get_L0_penalty) 31 | 32 | P = rho.shape[0] 33 | 34 | rho_floor = np.floor(rho) 35 | floor_is_zero = np.equal(rho_floor, 0) 36 | dist_from_start_to_floor = rho_floor - rho 37 | 38 | rho_ceil = np.ceil(rho) 39 | ceil_is_zero = np.equal(rho_ceil, 0) 40 | dist_from_start_to_ceil = rho_ceil - rho 41 | 42 | dimensions_to_round = np.flatnonzero(np.not_equal(rho_floor, rho_ceil)).tolist() 43 | 44 | scores = Z.dot(rho) 45 | best_objval = compute_loss_from_scores_real(scores) + get_L0_penalty(rho) 46 | while len(dimensions_to_round) > 0 and best_objval < objval_cutoff: 47 | 48 | objvals_at_floor = np.repeat(np.nan, P) 49 | objvals_at_ceil = np.repeat(np.nan, P) 50 | current_penalty = get_L0_penalty(rho) 51 | 52 | for idx in dimensions_to_round: 53 | 54 | # scores go from center to ceil -> center + dist_from_start_to_ceil 55 | Z_dim = Z[:, idx] 56 | base_scores = scores + dist_from_start_to_ceil[idx] * Z_dim 57 | objvals_at_ceil[idx] = compute_loss_from_scores_real(base_scores) 58 | 59 | # move from ceil to floor => -1*Z_j 60 | base_scores -= Z_dim 61 | objvals_at_floor[idx] = compute_loss_from_scores_real(base_scores) 62 | 63 | if ceil_is_zero[idx]: 64 | objvals_at_ceil[idx] -= C_0[idx] 65 | elif floor_is_zero[idx]: 66 | objvals_at_floor[idx] -= C_0[idx] 67 | 68 | 69 | # adjust for penalty value 70 | objvals_at_ceil += current_penalty 71 | objvals_at_floor += current_penalty 72 | best_objval_at_ceil = np.nanmin(objvals_at_ceil) 73 | best_objval_at_floor = np.nanmin(objvals_at_floor) 74 | 75 | if best_objval_at_ceil <= best_objval_at_floor: 76 | best_objval = best_objval_at_ceil 77 | best_dim = np.nanargmin(objvals_at_ceil) 78 | rho[best_dim] += dist_from_start_to_ceil[best_dim] 79 | scores += dist_from_start_to_ceil[best_dim] * Z[:, best_dim] 80 | else: 81 | best_objval = best_objval_at_floor 82 | best_dim = np.nanargmin(objvals_at_floor) 83 | rho[best_dim] += dist_from_start_to_floor[best_dim] 84 | scores += dist_from_start_to_floor[best_dim] * Z[:, best_dim] 85 | 86 | dimensions_to_round.remove(best_dim) 87 | #assert(np.all(np.isclose(scores, Z.dot(rho)))) 88 | 89 | early_stop_flag = best_objval > objval_cutoff 90 | return rho, best_objval, early_stop_flag 91 | 92 | 93 | def discrete_descent(rho, Z, C_0, rho_ub, rho_lb, get_L0_penalty, compute_loss_from_scores, descent_dimensions = None, active_set_flag = True): 94 | 95 | """ 96 | Given a initial feasible solution, rho, produces an improved solution that is 1-OPT 97 | (i.e. the objective value does not decrease by moving in any single dimension) 98 | at each iteration, the algorithm moves in the dimension that yields the greatest decrease in objective value 99 | the best step size is each dimension is computed using a directional search strategy that saves computation 100 | 101 | Parameters 102 | ---------- 103 | rho: P x 1 vector of continuous coefficients 104 | Z: N x P data matrix computed as X * Y 105 | C_0: N x 1 vector of L0 penalties. C_0[j] = L0 penalty for rho[j] for j = 0,..., P. 106 | rho_ub 107 | rho_lb 108 | compute_loss_from_scores_real: function handle to compute loss using N x 1 vector of scores, where scores = Z.dot(rho) 109 | get_L0_penalty: function handle to compute L0_penalty from rho 110 | descent_dimensions 111 | 112 | Returns 113 | ------- 114 | 115 | """ 116 | """ 117 | 118 | """ 119 | assert callable(compute_loss_from_scores) 120 | assert callable(get_L0_penalty) 121 | 122 | # initialize key variables 123 | MAX_ITERATIONS = 500 124 | MIN_IMPROVEMENT_PER_STEP = float(1e-8) 125 | P = len(rho) 126 | 127 | # convert solution to integer 128 | rho = np.require(np.require(rho, dtype = np.int_), dtype = np.float_) 129 | 130 | # convert descent dimensions to integer values 131 | if descent_dimensions is None: 132 | descent_dimensions = np.arange(P) 133 | else: 134 | descent_dimensions = np.require(descent_dimensions, dtype = np.int_) 135 | 136 | if active_set_flag: 137 | descent_dimensions = np.intersect1d(np.flatnonzero(rho), descent_dimensions) 138 | 139 | descent_dimensions = descent_dimensions.tolist() 140 | 141 | base_scores = Z.dot(rho) 142 | base_loss = compute_loss_from_scores(base_scores) 143 | base_objval = base_loss + get_L0_penalty(rho) 144 | n_iterations = 0 145 | 146 | coefficient_values = {k: np.arange(int(rho_lb[k]), int(rho_ub[k]) + 1) for k in descent_dimensions} 147 | search_dimensions = descent_dimensions 148 | while n_iterations < MAX_ITERATIONS and len(search_dimensions) > 0: 149 | 150 | # compute the best objective value / step size in each dimension 151 | best_objval_by_dim = np.repeat(np.nan, P) 152 | best_coef_by_dim = np.repeat(np.nan, P) 153 | 154 | for k in search_dimensions: 155 | 156 | dim_objvals = _compute_objvals_at_dim(base_rho = rho, 157 | base_scores = base_scores, 158 | base_loss = base_loss, 159 | dim_idx = k, 160 | dim_coefs = coefficient_values[k], 161 | Z = Z, 162 | C_0 = C_0, 163 | compute_loss_from_scores = compute_loss_from_scores) 164 | 165 | # mark points that will improve the current objective value by at least MIN_IMPROVEMENT_PER_STEP 166 | best_dim_idx = np.nanargmin(dim_objvals) 167 | best_objval_by_dim[k] = dim_objvals[best_dim_idx] 168 | best_coef_by_dim[k] = coefficient_values[k][best_dim_idx] 169 | 170 | # recompute base objective value/loss/scores 171 | best_idx = np.nanargmin(best_objval_by_dim) 172 | next_objval = best_objval_by_dim[best_idx] 173 | threshold_objval = base_objval - MIN_IMPROVEMENT_PER_STEP 174 | 175 | if next_objval >= threshold_objval: 176 | break 177 | 178 | best_step = best_coef_by_dim[best_idx] - rho[best_idx] 179 | rho[best_idx] += best_step 180 | base_objval = next_objval 181 | base_loss = base_objval - get_L0_penalty(rho) 182 | base_scores = base_scores + (best_step * Z[:, best_idx]) 183 | 184 | # remove the current best direction from the set of directions to explore 185 | search_dimensions = list(descent_dimensions) 186 | search_dimensions.remove(best_idx) 187 | n_iterations += 1 188 | 189 | return rho, base_loss, base_objval 190 | 191 | 192 | def _compute_objvals_at_dim(Z, C_0, base_rho, base_scores, base_loss, dim_coefs, dim_idx, compute_loss_from_scores): 193 | 194 | """ 195 | finds the value of rho[j] in dim_coefs that minimizes log_loss(rho) + C_0j 196 | 197 | Parameters 198 | ---------- 199 | Z 200 | C_0 201 | base_rho 202 | base_scores 203 | base_loss 204 | dim_coefs 205 | dim_idx 206 | compute_loss_from_scores 207 | 208 | Returns 209 | ------- 210 | 211 | """ 212 | 213 | # copy stuff because ctypes 214 | scores = np.copy(base_scores) 215 | 216 | # initialize parameters 217 | P = base_rho.shape[0] 218 | base_coef_value = base_rho[dim_idx] 219 | base_index = np.flatnonzero(dim_coefs == base_coef_value) 220 | loss_at_coef_value = np.repeat(np.nan, len(dim_coefs)) 221 | loss_at_coef_value[base_index] = float(base_loss) 222 | Z_dim = Z[:, dim_idx] 223 | 224 | # start by moving forward 225 | forward_indices = np.flatnonzero(base_coef_value <= dim_coefs) 226 | forward_step_sizes = np.diff(dim_coefs[forward_indices] - base_coef_value) 227 | n_forward_steps = len(forward_step_sizes) 228 | stop_after_first_forward_step = False 229 | 230 | best_loss = base_loss 231 | total_distance_from_base = 0 232 | 233 | for i in range(n_forward_steps): 234 | scores += forward_step_sizes[i] * Z_dim 235 | total_distance_from_base += forward_step_sizes[i] 236 | current_loss = compute_loss_from_scores(scores) 237 | if current_loss >= best_loss: 238 | stop_after_first_forward_step = i == 0 239 | break 240 | loss_at_coef_value[forward_indices[i + 1]] = current_loss 241 | best_loss = current_loss 242 | 243 | # if the first step forward didn't lead to a decrease in loss, then move backwards 244 | move_backward = stop_after_first_forward_step or n_forward_steps == 0 245 | 246 | if move_backward: 247 | 248 | # compute backward steps 249 | backward_indices = np.flipud(np.where(dim_coefs <= base_coef_value)[0]) 250 | backward_step_sizes = np.diff(dim_coefs[backward_indices] - base_coef_value) 251 | n_backward_steps = len(backward_step_sizes) 252 | 253 | # correct size of first backward step if you took 1 step forward 254 | if n_backward_steps > 0 and n_forward_steps > 0: 255 | backward_step_sizes[0] = backward_step_sizes[0] - forward_step_sizes[0] 256 | 257 | best_loss = base_loss 258 | 259 | for i in range(n_backward_steps): 260 | scores += backward_step_sizes[i] * Z_dim 261 | total_distance_from_base += backward_step_sizes[i] 262 | current_loss = compute_loss_from_scores(scores) 263 | if current_loss >= best_loss: 264 | break 265 | loss_at_coef_value[backward_indices[i + 1]] = current_loss 266 | best_loss = current_loss 267 | 268 | # at this point scores == base_scores + step_distance*Z_dim 269 | # assert(all(np.isclose(scores, base_scores + total_distance_from_base * Z_dim))) 270 | 271 | # compute objective values by adding penalty values to all other indices 272 | other_dim_idx = np.flatnonzero(dim_idx != np.arange(P)) 273 | other_dim_penalty = np.sum(C_0[other_dim_idx] * (base_rho[other_dim_idx] != 0)) 274 | objval_at_coef_values = loss_at_coef_value + other_dim_penalty 275 | 276 | if C_0[dim_idx] > 0.0: 277 | 278 | # increase objective value at every non-zero coefficient value by C_0j 279 | nonzero_coef_idx = np.flatnonzero(dim_coefs) 280 | objval_at_coef_values[nonzero_coef_idx] = objval_at_coef_values[nonzero_coef_idx] + C_0[dim_idx] 281 | 282 | # compute value at coef[j] == 0 if needed 283 | zero_coef_idx = np.flatnonzero(dim_coefs == 0) 284 | if np.isnan(objval_at_coef_values[zero_coef_idx]): 285 | # steps_from_here_to_zero: step_from_here_to_base + step_from_base_to_zero 286 | # steps_from_here_to_zero: -step_from_base_to_here + -step_from_zero_to_base 287 | steps_to_zero = -(base_coef_value + total_distance_from_base) 288 | scores += steps_to_zero * Z_dim 289 | objval_at_coef_values[zero_coef_idx] = compute_loss_from_scores(scores) + other_dim_penalty 290 | # assert(all(np.isclose(scores, base_scores - base_coef_value * Z_dim))) 291 | 292 | # return objective value at feasible coefficients 293 | return objval_at_coef_values 294 | 295 | 296 | -------------------------------------------------------------------------------- /riskslim/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from pathlib import Path 4 | import time 5 | import warnings 6 | import numpy as np 7 | import pandas as pd 8 | import prettytable as pt 9 | from .defaults import INTERCEPT_NAME 10 | 11 | # DATA 12 | def load_data_from_csv(dataset_csv_file, sample_weights_csv_file = None, fold_csv_file = None, fold_num = 0): 13 | """ 14 | 15 | Parameters 16 | ---------- 17 | dataset_csv_file csv file containing the training data 18 | see /datasets/adult_data.csv for an example 19 | training data stored as a table with N+1 rows and d+1 columns 20 | column 1 is the outcome variable entries must be (-1,1) or (0,1) 21 | column 2 to d+1 are the d input variables 22 | row 1 contains unique names for the outcome variable, and the input vairable 23 | 24 | sample_weights_csv_file csv file containing sample weights for the training data 25 | weights stored as a table with N rows and 1 column 26 | all sample weights must be non-negative 27 | 28 | fold_csv_file csv file containing indices of folds for K-fold cross validation 29 | fold indices stored as a table with N rows and 1 column 30 | folds must be integers between 1 to K 31 | if fold_csv_file is None, then we do not use folds 32 | 33 | fold_num int between 0 to K, where K is set by the fold_csv_file 34 | let fold_idx be the N x 1 index vector listed in fold_csv_file 35 | samples where fold_idx == fold_num will be used to test 36 | samples where fold_idx != fold_num will be used to train the model 37 | fold_num = 0 means use "all" of the training data (since all values of fold_idx \in [1,K]) 38 | if fold_csv_file is None, then fold_num is set to 0 39 | 40 | 41 | Returns 42 | ------- 43 | dictionary containing training data for a binary classification problem with the fields: 44 | 45 | - 'X' N x P matrix of features (numpy.ndarray) with a column of 1s for the INTERCEPT_NAME 46 | - 'Y' N x 1 vector of labels (+1/-1) (numpy.ndarray) 47 | - 'variable_names' list of strings containing the names of each feature (list) 48 | - 'Y_name' string containing the name of the output (optional) 49 | - 'sample_weights' N x 1 vector of sample weights, must all be positive 50 | 51 | """ 52 | dataset_csv_file = Path(dataset_csv_file) 53 | if not dataset_csv_file.exists(): 54 | raise IOError('could not find dataset_csv_file: %s' % dataset_csv_file) 55 | 56 | df = pd.read_csv(dataset_csv_file, sep = ',') 57 | 58 | raw_data = df.to_numpy() 59 | data_headers = list(df.columns.values) 60 | N = raw_data.shape[0] 61 | 62 | # setup Y vector and Y_name 63 | Y_col_idx = [0] 64 | Y = raw_data[:, Y_col_idx] 65 | Y_name = data_headers[Y_col_idx[0]] 66 | Y[Y == 0] = -1 67 | 68 | # setup X and X_names 69 | X_col_idx = [j for j in range(raw_data.shape[1]) if j not in Y_col_idx] 70 | X = raw_data[:, X_col_idx] 71 | variable_names = [data_headers[j] for j in X_col_idx] 72 | 73 | # insert a column of ones to X for the intercept 74 | X = np.insert(arr=X, obj=0, values=np.ones(N), axis=1) 75 | variable_names.insert(0, INTERCEPT_NAME) 76 | 77 | 78 | if sample_weights_csv_file is None: 79 | sample_weights = np.ones(N) 80 | else: 81 | sample_weights_csv_file = Path(sample_weights_csv_file) 82 | if not sample_weights_csv_file.exists(): 83 | raise IOError('could not find sample_weights_csv_file: %s' % sample_weights_csv_file) 84 | sample_weights = pd.read_csv(sample_weights_csv_file, sep=',', header=None) 85 | sample_weights = sample_weights.to_numpy() 86 | 87 | data = { 88 | 'X': X, 89 | 'Y': Y, 90 | 'variable_names': variable_names, 91 | 'outcome_name': Y_name, 92 | 'sample_weights': sample_weights, 93 | } 94 | 95 | #load folds 96 | if fold_csv_file is not None: 97 | fold_csv_file = Path(fold_csv_file) 98 | if not fold_csv_file.exists(): 99 | raise IOError('could not find fold_csv_file: %s' % fold_csv_file) 100 | 101 | fold_idx = pd.read_csv(fold_csv_file, sep=',', header=None) 102 | fold_idx = fold_idx.values.flatten() 103 | K = max(fold_idx) 104 | all_fold_nums = np.sort(np.unique(fold_idx)) 105 | assert len(fold_idx) == N, "dimension mismatch: read %r fold indices (expected N = %r)" % (len(fold_idx), N) 106 | assert np.all(all_fold_nums == np.arange(1, K+1)), "folds should contain indices between 1 to %r" % K 107 | assert fold_num in np.arange(0, K+1), "fold_num should either be 0 or an integer between 1 to %r" % K 108 | if fold_num >= 1: 109 | #test_idx = fold_num == fold_idx 110 | train_idx = fold_num != fold_idx 111 | data['X'] = data['X'][train_idx,] 112 | data['Y'] = data['Y'][train_idx] 113 | data['sample_weights'] = data['sample_weights'][train_idx] 114 | 115 | assert check_data(data) 116 | return data 117 | 118 | 119 | def check_data(data): 120 | """ 121 | makes sure that 'data' contains training data that is suitable for binary classification problems 122 | throws AssertionError if 123 | 124 | 'data' is a dictionary that must contain: 125 | 126 | - 'X' N x P matrix of features (numpy.ndarray) with a column of 1s for the INTERCEPT_NAME 127 | - 'Y' N x 1 vector of labels (+1/-1) (numpy.ndarray) 128 | - 'variable_names' list of strings containing the names of each feature (list) 129 | 130 | data can also contain: 131 | 132 | - 'outcome_name' string containing the name of the output (optional) 133 | - 'sample_weights' N x 1 vector of sample weights, must all be positive 134 | 135 | Returns 136 | ------- 137 | True if data passes checks 138 | 139 | """ 140 | # type checks 141 | assert type(data) is dict, "data should be a dict" 142 | 143 | assert 'X' in data, "data should contain X matrix" 144 | assert type(data['X']) is np.ndarray, "type(X) should be numpy.ndarray" 145 | 146 | assert 'Y' in data, "data should contain Y matrix" 147 | assert type(data['Y']) is np.ndarray, "type(Y) should be numpy.ndarray" 148 | 149 | assert 'variable_names' in data, "data should contain variable_names" 150 | assert type(data['variable_names']) is list, "variable_names should be a list" 151 | 152 | X = data['X'] 153 | Y = data['Y'] 154 | variable_names = data['variable_names'] 155 | 156 | if 'outcome_name' in data: 157 | assert type(data['outcome_name']) is str, "outcome_name should be a str" 158 | 159 | # sizes and uniqueness 160 | N, P = X.shape 161 | assert N > 0, 'X matrix must have at least 1 row' 162 | assert P > 0, 'X matrix must have at least 1 column' 163 | assert len(Y) == N, 'dimension mismatch. Y must contain as many entries as X. Need len(Y) = N.' 164 | assert len(list(set(data['variable_names']))) == len(data['variable_names']), 'variable_names is not unique' 165 | assert len(data['variable_names']) == P, 'len(variable_names) should be same as # of cols in X' 166 | 167 | # feature matrix 168 | assert np.all(~np.isnan(X)), 'X has nan entries' 169 | assert np.all(~np.isinf(X)), 'X has inf entries' 170 | 171 | # offset in feature matrix 172 | if INTERCEPT_NAME in variable_names: 173 | assert all(X[:, variable_names.index(INTERCEPT_NAME)] == 1.0), "(Intercept)' column should only be composed of 1s" 174 | else: 175 | warnings.warn("there is no column named INTERCEPT_NAME in variable_names") 176 | 177 | # labels values 178 | assert all((Y == 1) | (Y == -1)), 'Need Y[i] = [-1,1] for all i.' 179 | if all(Y == 1): 180 | warnings.warn('Y does not contain any positive examples. Need Y[i] = +1 for at least 1 i.') 181 | if all(Y == -1): 182 | warnings.warn('Y does not contain any negative examples. Need Y[i] = -1 for at least 1 i.') 183 | 184 | if 'sample_weights' in data: 185 | sample_weights = data['sample_weights'] 186 | type(sample_weights) is np.ndarray 187 | assert len(sample_weights) == N, 'sample_weights should contain N elements' 188 | assert all(sample_weights > 0.0), 'sample_weights[i] > 0 for all i ' 189 | 190 | # by default, we set sample_weights as an N x 1 array of ones. if not, then sample weights is non-trivial 191 | if any(sample_weights != 1.0) and len(np.unique(sample_weights)) < 2: 192 | warnings.warn('note: sample_weights only has <2 unique values') 193 | 194 | return True 195 | 196 | 197 | # MODEL PRINTING 198 | def print_model(rho, data, show_omitted_variables = False): 199 | 200 | variable_names = data['variable_names'] 201 | 202 | rho_values = np.copy(rho) 203 | rho_names = list(variable_names) 204 | 205 | if INTERCEPT_NAME in rho_names: 206 | intercept_ind = variable_names.index(INTERCEPT_NAME) 207 | intercept_val = int(rho[intercept_ind]) 208 | rho_values = np.delete(rho_values, intercept_ind) 209 | rho_names.remove(INTERCEPT_NAME) 210 | else: 211 | intercept_val = 0 212 | 213 | if 'outcome_name' in data: 214 | predict_string = "Pr(Y = +1) = 1.0/(1.0 + exp(-(%d + score))" % intercept_val 215 | else: 216 | predict_string = "Pr(%s = +1) = 1.0/(1.0 + exp(-(%d + score))" % (data['outcome_name'].upper(), intercept_val) 217 | 218 | if not show_omitted_variables: 219 | selected_ind = np.flatnonzero(rho_values) 220 | rho_values = rho_values[selected_ind] 221 | rho_names = [rho_names[i] for i in selected_ind] 222 | rho_binary = [np.all((data['X'][:,j] == 0) | (data['X'][:,j] == 1)) for j in selected_ind] 223 | 224 | #sort by most positive to most negative 225 | sort_ind = np.argsort(-np.array(rho_values)) 226 | rho_values = [rho_values[j] for j in sort_ind] 227 | rho_names = [rho_names[j] for j in sort_ind] 228 | rho_binary = [rho_binary[j] for j in sort_ind] 229 | rho_values = np.array(rho_values) 230 | 231 | rho_values_string = [str(int(i)) + " points" for i in rho_values] 232 | n_variable_rows = len(rho_values) 233 | total_string = "ADD POINTS FROM ROWS %d to %d" % (1, n_variable_rows) 234 | 235 | max_name_col_length = max(len(predict_string), len(total_string), max([len(s) for s in rho_names])) + 2 236 | max_value_col_length = max(7, max([len(s) for s in rho_values_string]) + len("points")) + 2 237 | 238 | m = pt.PrettyTable() 239 | m.field_names = ["Variable", "Points", "Tally"] 240 | 241 | m.add_row([predict_string, "", ""]) 242 | m.add_row(['=' * max_name_col_length, "=" * max_value_col_length, "========="]) 243 | 244 | for name, value_string in zip(rho_names, rho_values_string): 245 | m.add_row([name, value_string, "+ ....."]) 246 | 247 | m.add_row(['=' * max_name_col_length, "=" * max_value_col_length, "========="]) 248 | m.add_row([total_string, "SCORE", "= ....."]) 249 | m.header = False 250 | m.align["Variable"] = "l" 251 | m.align["Points"] = "r" 252 | m.align["Tally"] = "r" 253 | 254 | print(m) 255 | return m 256 | 257 | 258 | # LOGGING 259 | def setup_logging(logger, log_to_console = True, log_file = None): 260 | """ 261 | Sets up logging to console and file on disk 262 | See https://docs.python.org/2/howto/logging-cookbook.html for details on how to customize 263 | 264 | Parameters 265 | ---------- 266 | log_to_console set to True to disable logging in console 267 | log_file path to file for loggin 268 | 269 | Returns 270 | ------- 271 | Logger object that prints formatted messages to log_file and console 272 | """ 273 | 274 | # quick return if no logging to console or file 275 | if log_to_console is False and log_file is None: 276 | logger.disabled = True 277 | return logger 278 | 279 | log_format = logging.Formatter(fmt='%(asctime)s | %(levelname)-8s | %(message)s', datefmt='%m-%d-%Y %I:%M:%S %p') 280 | 281 | # log to file 282 | if log_file is not None: 283 | fh = logging.FileHandler(filename=log_file) 284 | #fh.setLevel(logging.DEBUG) 285 | fh.setFormatter(log_format) 286 | logger.addHandler(fh) 287 | 288 | if log_to_console: 289 | ch = logging.StreamHandler() 290 | #ch.setLevel(logging.DEBUG) 291 | ch.setFormatter(log_format) 292 | logger.addHandler(ch) 293 | 294 | return logger 295 | 296 | 297 | def print_log(msg, print_flag = True): 298 | """ 299 | 300 | Parameters 301 | ---------- 302 | msg 303 | print_flag 304 | 305 | Returns 306 | ------- 307 | 308 | """ 309 | if print_flag: 310 | if isinstance(msg, str): 311 | print('%s | %s' % (time.strftime("%m/%d/%y @ %I:%M %p", time.localtime()), msg)) 312 | else: 313 | print('%s | %r' % (time.strftime("%m/%d/%y @ %I:%M %p", time.localtime()), msg)) 314 | sys.stdout.flush() 315 | 316 | 317 | def validate_settings(settings = None, default_settings = None): 318 | 319 | if settings is None: 320 | settings = dict() 321 | else: 322 | assert isinstance(settings, dict) 323 | settings = dict(settings) 324 | 325 | if default_settings is not None: 326 | assert isinstance(default_settings, dict) 327 | settings = {k: settings[k] if k in settings else default_settings[k] for k in default_settings} 328 | 329 | return settings -------------------------------------------------------------------------------- /riskslim/coefficient_set.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from prettytable import PrettyTable 3 | from .defaults import INTERCEPT_NAME 4 | 5 | 6 | class CoefficientSet(object): 7 | """ 8 | Class used to represent and manipulate constraints on individual coefficients 9 | including upper bound, lower bound, variable type, and regularization. 10 | Coefficient Set is composed of Coefficient Elements 11 | """ 12 | 13 | _initialized = False 14 | _print_flag = True 15 | _check_flag = True 16 | _correct_flag = True 17 | _variable_names = None 18 | 19 | def __init__(self, variable_names, **kwargs): 20 | 21 | # set variables using setter methods 22 | self.variable_names = list(variable_names) 23 | self.print_flag = kwargs.get('print_flag', self._print_flag) 24 | self.check_flag = kwargs.get('check_flag', self._check_flag) 25 | self.correct_flag = kwargs.get('correct_flag', self._correct_flag) 26 | 27 | ub = kwargs.get('ub', _CoefficientElement._DEFAULT_UB) 28 | lb = kwargs.get('lb', _CoefficientElement._DEFAULT_LB) 29 | c0 = kwargs.get('c0', _CoefficientElement._DEFAULT_c0) 30 | vtype = kwargs.get('type', _CoefficientElement._DEFAULT_TYPE) 31 | 32 | ub = self._expand_values(value = ub) 33 | lb = self._expand_values(value = lb) 34 | c0 = self._expand_values(value = c0) 35 | vtype = self._expand_values(value = vtype) 36 | 37 | self._coef_elements = dict() 38 | for name in variable_names: 39 | idx = variable_names.index(name) 40 | self._coef_elements[name] = _CoefficientElement(name = name, ub = ub[idx], lb = lb[idx], c0 = c0[idx], vtype = vtype[idx]) 41 | 42 | self._check_rep() 43 | self._initialized = True 44 | 45 | 46 | @property 47 | def P(self): 48 | return len(self._variable_names) 49 | 50 | 51 | @property 52 | def print_flag(self): 53 | return bool(self._print_flag) 54 | 55 | 56 | @print_flag.setter 57 | def print_flag(self, flag): 58 | self._print_flag = bool(flag) 59 | 60 | 61 | @property 62 | def correct_flag(self): 63 | return bool(self._correct_flag) 64 | 65 | 66 | @correct_flag.setter 67 | def correct_flag(self, flag): 68 | self._correct_flag = bool(flag) 69 | 70 | 71 | @property 72 | def check_flag(self): 73 | return self._check_flag 74 | 75 | 76 | @check_flag.setter 77 | def check_flag(self, flag): 78 | self._check_flag = bool(flag) 79 | 80 | 81 | @property 82 | def variable_names(self): 83 | return self._variable_names 84 | 85 | 86 | @variable_names.setter 87 | def variable_names(self, names): 88 | assert isinstance(names, list), 'variable_names must be a list' 89 | for name in names: 90 | assert isinstance(name, str), 'variable_names must be a list of strings' 91 | assert len(names) == len(set(names)), 'variable_names contain elements with unique names' 92 | if self._variable_names is not None: 93 | assert len(names) == len(self), 'variable_names must contain at least %d elements' % len(self) 94 | self._variable_names = list(names) 95 | 96 | 97 | def index(self, name): 98 | assert isinstance(name, str) 99 | if name in self._variable_names: 100 | return self._variable_names.index(name) 101 | else: 102 | raise ValueError('no variable named %s in coefficient set' % name) 103 | 104 | 105 | def penalized_indices(self): 106 | return np.array(list(map(lambda v: self._coef_elements[v].penalized, self._variable_names))) 107 | 108 | 109 | def update_intercept_bounds(self, X, y, max_offset, max_L0_value = None): 110 | """ 111 | uses data to set the lower and upper bound on the offset to a conservative value 112 | the value is guaranteed to avoid a loss in performance 113 | 114 | optimal_offset = max_abs_score + 1 115 | where max_abs_score is the largest absolute score that can be achieved using the coefficients in coef_set 116 | with the training data. note: 117 | when offset >= optimal_offset, then we predict y = +1 for every example 118 | when offset <= optimal_offset, then we predict y = -1 for every example 119 | thus, any feasible model should do better. 120 | 121 | 122 | Parameters 123 | ---------- 124 | X 125 | y 126 | max_offset 127 | max_L0_value 128 | 129 | Returns 130 | ------- 131 | None 132 | 133 | """ 134 | if INTERCEPT_NAME not in self._coef_elements: 135 | raise ValueError("coef_set must contain a variable for the offset called %s" % INTERCEPT_NAME) 136 | 137 | e = self._coef_elements[INTERCEPT_NAME] 138 | 139 | # get idx of intercept/variables 140 | names = self.variable_names 141 | variable_names = list(names) 142 | variable_names.remove(INTERCEPT_NAME) 143 | variable_idx = np.array([names.index(n) for n in variable_names]) 144 | 145 | # get max # of non-zero coefficients given model size limit 146 | penalized_idx = [self._coef_elements[n].penalized for n in variable_names] 147 | trivial_L0_max = len(penalized_idx) 148 | 149 | if max_L0_value is None: 150 | max_L0_value = trivial_L0_max 151 | 152 | if max_L0_value > 0: 153 | max_L0_value = min(trivial_L0_max, max_L0_value) 154 | 155 | # update intercept bounds 156 | Z = X * y 157 | Z_min = np.min(Z, axis = 0) 158 | Z_max = np.max(Z, axis = 0) 159 | 160 | # get regularized indices 161 | L0_reg_ind = np.isnan(self.C_0j)[variable_idx] 162 | 163 | # get smallest / largest score 164 | s_min, s_max = get_score_bounds(Z_min = Z_min[variable_idx], 165 | Z_max = Z_max[variable_idx], 166 | rho_lb = self.lb[variable_idx], 167 | rho_ub = self.ub[variable_idx], 168 | L0_reg_ind = L0_reg_ind, 169 | L0_max = max_L0_value) 170 | 171 | # get max # of non-zero coefficients given model size limit 172 | conservative_offset = max(abs(s_min), abs(s_max)) + 1 173 | max_offset = min(max_offset, conservative_offset) 174 | e.ub = max_offset 175 | e.lb = -max_offset 176 | 177 | 178 | def tabulate(self): 179 | t = PrettyTable() 180 | t.align = "r" 181 | t.add_column("variable_name", self._variable_names) 182 | t.add_column("vtype", self.vtype) 183 | t.add_column("sign", self.sign) 184 | t.add_column("lb", self.lb) 185 | t.add_column("ub", self.ub) 186 | t.add_column("c0", self.c0) 187 | return str(t) 188 | 189 | 190 | def __len__(self): 191 | return len(self._variable_names) 192 | 193 | 194 | def __str__(self): 195 | return self.tabulate() 196 | 197 | 198 | def __repr__(self): 199 | if self.print_flag: 200 | return self.tabulate() 201 | 202 | 203 | def __getattr__(self, name): 204 | 205 | if name == 'C_0j': 206 | name = 'c0' 207 | 208 | vals = [getattr(self._coef_elements[v], name) for v in self._variable_names] 209 | if name in ['ub', 'lb', 'c0', 'sign', 'vtype']: 210 | return np.array(vals) 211 | else: 212 | return list(vals) 213 | 214 | 215 | def __setattr__(self, name, value): 216 | if self._initialized: 217 | assert all(map(lambda e: hasattr(e, name), self._coef_elements.values())) 218 | attr_values = self._expand_values(value) 219 | for e, v in zip(self._coef_elements.values(), attr_values): 220 | setattr(e, name, v) 221 | self._check_rep() 222 | else: 223 | object.__setattr__(self, name, value) 224 | 225 | 226 | def __getitem__(self, key): 227 | 228 | if isinstance(key, int): 229 | assert 0 <= int(key) <= self.P 230 | return self._coef_elements[self._variable_names[key]] 231 | elif isinstance(key, str): 232 | return self._coef_elements[key] 233 | else: 234 | raise KeyError('invalid key') 235 | 236 | 237 | def __setitem__(self, key, value): 238 | 239 | if isinstance(key, int): 240 | assert 0 <= int(key) <= self.P 241 | key = self._variable_names[key] 242 | elif isinstance(key, str): 243 | assert isinstance(key, str) 244 | assert key in self._variable_names 245 | assert value.name == key 246 | else: 247 | raise KeyError('invalid key') 248 | 249 | assert isinstance(value, _CoefficientElement) 250 | self._coef_elements[key] = value 251 | 252 | 253 | def _check_rep(self): 254 | 255 | if self._check_flag: 256 | 257 | assert len(self._variable_names) == len(set(self._variable_names)) 258 | 259 | for name in self._variable_names: 260 | assert isinstance(name, str) 261 | assert len(name) >= 1 262 | assert self._coef_elements[name]._check_rep() 263 | 264 | if self._correct_flag: 265 | 266 | for name in self._variable_names: 267 | e = self._coef_elements[name] 268 | if name in {'Intercept', '(Intercept)', 'intercept', '(intercept)'}: 269 | if e.c0 > 0 or np.isnan(e.c0): 270 | if self._print_flag: 271 | print("setting c0_value = 0.0 for %s to ensure that intercept is not penalized" % name) 272 | e._c0 = 0.0 273 | 274 | return True 275 | 276 | 277 | def _expand_values(self, value): 278 | 279 | if isinstance(value, np.ndarray): 280 | if value.size == self.P: 281 | value_array = value 282 | elif value.size == 1: 283 | value_array = np.repeat(value, self.P) 284 | else: 285 | raise ValueError("length mismatch; need either 1 or %d values" % self.P) 286 | 287 | elif isinstance(value, list): 288 | if len(value) == self.P: 289 | value_array = value 290 | elif len(value) == 1: 291 | value_array = [value] * self.P 292 | else: 293 | raise ValueError("length mismatch; need either 1 or %d values" % self.P) 294 | 295 | elif isinstance(value, str): 296 | value_array = [str(value)] * self.P 297 | 298 | elif isinstance(value, int): 299 | value_array = [int(value)] * self.P 300 | 301 | elif isinstance(value, float): 302 | value_array = [float(value)] * self.P 303 | 304 | else: 305 | raise ValueError("unknown variable type %s") 306 | 307 | return(value_array) 308 | 309 | 310 | class _CoefficientElement(object): 311 | 312 | _DEFAULT_UB = 5 313 | _DEFAULT_LB = -5 314 | _DEFAULT_c0 = float('nan') 315 | _DEFAULT_TYPE = 'I' 316 | _VALID_TYPES = ['I', 'C'] 317 | 318 | def _is_integer(self, x): 319 | return np.array_equal(x, np.require(x, dtype = np.int_)) 320 | 321 | 322 | def __init__(self, name, ub = _DEFAULT_UB, lb = _DEFAULT_LB, c0 = _DEFAULT_c0, vtype = _DEFAULT_TYPE): 323 | 324 | self._name = str(name) 325 | self._ub = float(ub) 326 | self._lb = float(lb) 327 | self._c0 = float(c0) 328 | self._vtype = vtype 329 | assert self._check_rep() 330 | 331 | 332 | @property 333 | def name(self): 334 | return self._name 335 | 336 | 337 | @property 338 | def vtype(self): 339 | return self._vtype 340 | 341 | 342 | @vtype.setter 343 | def vtype(self, value): 344 | assert isinstance(value, str) 345 | assert value in self._VALID_TYPES 346 | self._vtype = str(value) 347 | 348 | 349 | @property 350 | def ub(self): 351 | return self._ub 352 | 353 | 354 | @ub.setter 355 | def ub(self, value): 356 | if hasattr(value, '__len__'): 357 | assert len(value) == 1 358 | value = value[0] 359 | assert value >= self._lb 360 | self._ub = float(value) 361 | 362 | 363 | @property 364 | def lb(self): 365 | return self._lb 366 | 367 | 368 | @lb.setter 369 | def lb(self, value): 370 | if hasattr(value, '__len__'): 371 | assert len(value) == 1 372 | value = value[0] 373 | assert value <= self._ub 374 | self._lb = float(value) 375 | 376 | 377 | @property 378 | def c0(self): 379 | return self._c0 380 | 381 | 382 | @c0.setter 383 | def c0(self, value): 384 | if np.isnan(value): 385 | self._c0 = float('nan') 386 | else: 387 | assert np.isfinite(value), 'L0 penalty for %s must either be NaN or a finite positive number' % self._name 388 | assert value >= 0.0, 'L0 penalty for %s must either be NaN or a finite positive number' % self._name 389 | self._c0 = float(value) 390 | 391 | 392 | @property 393 | def penalized(self): 394 | return np.isnan(self._c0) or (self._c0 > 0.0) 395 | 396 | 397 | @property 398 | def sign(self): 399 | if self._ub > 0.0 and self._lb >= 0.0: 400 | return 1 401 | elif self._ub <= 0.0 and self._lb < 0.0: 402 | return -1 403 | else: 404 | return 0 405 | 406 | @sign.setter 407 | def sign(self, value): 408 | if value > 0: 409 | self._lb = 0.0 410 | elif value < 0: 411 | self._ub = 0.0 412 | 413 | 414 | def _check_rep(self): 415 | 416 | #name 417 | assert isinstance(self._name, str) 418 | assert len(self._name) >= 1 419 | 420 | #bounds 421 | assert np.isfinite(self.ub) 422 | assert np.isfinite(self.lb) 423 | assert self.ub >= self.lb 424 | 425 | # value 426 | assert self._vtype in self._VALID_TYPES 427 | assert np.isnan(self.c0) or (self.c0 >= 0.0 and np.isfinite(self.c0)) 428 | 429 | return True 430 | 431 | 432 | def __repr__(self): 433 | return self.tabulate() 434 | 435 | 436 | def __str__(self): 437 | return self.tabulate() 438 | 439 | 440 | def tabulate(self): 441 | s = ['-' * 60, 442 | 'variable: %s' % self._name, 443 | '-' * 60, 444 | '%s: %1.1f' % ('ub', self._ub), 445 | '%s: %1.1f' % ('lb', self._lb), 446 | '%s: %1.2g' % ('c0', self._c0), 447 | '%s: %1.0f' % ('sign', self.sign), 448 | '%s: %s' % ('vtype', self._vtype)] 449 | t = '\n' + '\n'.join(s) + '\n' 450 | return t 451 | 452 | 453 | def get_score_bounds(Z_min, Z_max, rho_lb, rho_ub, L0_reg_ind = None, L0_max = None): 454 | 455 | edge_values = np.vstack([Z_min * rho_lb, Z_max * rho_lb, Z_min * rho_ub, Z_max * rho_ub]) 456 | 457 | if (L0_max is None) or (L0_reg_ind is None) or (L0_max == Z_min.shape[0]): 458 | s_min = np.sum(np.min(edge_values, axis=0)) 459 | s_max = np.sum(np.max(edge_values, axis=0)) 460 | else: 461 | min_values = np.min(edge_values, axis=0) 462 | s_min_reg = np.sum(np.sort(min_values[L0_reg_ind])[0:L0_max]) 463 | s_min_no_reg = np.sum(min_values[~L0_reg_ind]) 464 | s_min = s_min_reg + s_min_no_reg 465 | 466 | max_values = np.max(edge_values, axis=0) 467 | s_max_reg = np.sum(-np.sort(-max_values[L0_reg_ind])[0:L0_max]) 468 | s_max_no_reg = np.sum(max_values[~L0_reg_ind]) 469 | s_max = s_max_reg + s_max_no_reg 470 | 471 | return s_min, s_max -------------------------------------------------------------------------------- /examples/data/breastcancer_data.csv: -------------------------------------------------------------------------------- 1 | Benign,ClumpThickness,UniformityOfCellSize,UniformityOfCellShape,MarginalAdhesion,SingleEpithelialCellSize,BareNuclei,BlandChromatin,NormalNucleoli,Mitoses 2 | 0,5,1,1,1,2,1,3,1,1 3 | 0,5,4,4,5,7,10,3,2,1 4 | 0,3,1,1,1,2,2,3,1,1 5 | 0,6,8,8,1,3,4,3,7,1 6 | 0,4,1,1,3,2,1,3,1,1 7 | 1,8,10,10,8,7,10,9,7,1 8 | 0,1,1,1,1,2,10,3,1,1 9 | 0,2,1,2,1,2,1,3,1,1 10 | 0,2,1,1,1,2,1,1,1,5 11 | 0,4,2,1,1,2,1,2,1,1 12 | 0,1,1,1,1,1,1,3,1,1 13 | 0,2,1,1,1,2,1,2,1,1 14 | 1,5,3,3,3,2,3,4,4,1 15 | 0,1,1,1,1,2,3,3,1,1 16 | 1,8,7,5,10,7,9,5,5,4 17 | 1,7,4,6,4,6,1,4,3,1 18 | 0,4,1,1,1,2,1,2,1,1 19 | 0,4,1,1,1,2,1,3,1,1 20 | 1,10,7,7,6,4,10,4,1,2 21 | 0,6,1,1,1,2,1,3,1,1 22 | 1,7,3,2,10,5,10,5,4,4 23 | 1,10,5,5,3,6,7,7,10,1 24 | 0,3,1,1,1,2,1,2,1,1 25 | 0,1,1,1,1,2,1,3,1,1 26 | 1,5,2,3,4,2,7,3,6,1 27 | 0,3,2,1,1,1,1,2,1,1 28 | 0,5,1,1,1,2,1,2,1,1 29 | 0,2,1,1,1,2,1,2,1,1 30 | 0,1,1,3,1,2,1,1,1,1 31 | 0,3,1,1,1,1,1,2,1,1 32 | 0,2,1,1,1,2,1,3,1,1 33 | 1,10,7,7,3,8,5,7,4,3 34 | 0,2,1,1,2,2,1,3,1,1 35 | 0,3,1,2,1,2,1,2,1,1 36 | 0,2,1,1,1,2,1,2,1,1 37 | 1,10,10,10,8,6,1,8,9,1 38 | 0,6,2,1,1,1,1,7,1,1 39 | 1,5,4,4,9,2,10,5,6,1 40 | 1,2,5,3,3,6,7,7,5,1 41 | 1,10,4,3,1,3,3,6,5,2 42 | 1,6,10,10,2,8,10,7,3,3 43 | 1,5,6,5,6,10,1,3,1,1 44 | 1,10,10,10,4,8,1,8,10,1 45 | 0,1,1,1,1,2,1,2,1,2 46 | 1,3,7,7,4,4,9,4,8,1 47 | 0,1,1,1,1,2,1,2,1,1 48 | 0,4,1,1,3,2,1,3,1,1 49 | 1,7,8,7,2,4,8,3,8,2 50 | 1,9,5,8,1,2,3,2,1,5 51 | 1,5,3,3,4,2,4,3,4,1 52 | 1,10,3,6,2,3,5,4,10,2 53 | 1,5,5,5,8,10,8,7,3,7 54 | 1,10,5,5,6,8,8,7,1,1 55 | 1,10,6,6,3,4,5,3,6,1 56 | 1,8,10,10,1,3,6,3,9,1 57 | 1,8,2,4,1,5,1,5,4,4 58 | 1,5,2,3,1,6,10,5,1,1 59 | 1,9,5,5,2,2,2,5,1,1 60 | 1,5,3,5,5,3,3,4,10,1 61 | 0,1,1,1,1,2,2,2,1,1 62 | 1,9,10,10,1,10,8,3,3,1 63 | 1,6,3,4,1,5,2,3,9,1 64 | 0,1,1,1,1,2,1,2,1,1 65 | 1,10,4,2,1,3,2,4,3,10 66 | 0,4,1,1,1,2,1,3,1,1 67 | 1,5,3,4,1,8,10,4,9,1 68 | 1,8,3,8,3,4,9,8,9,8 69 | 0,1,1,1,1,2,1,3,2,1 70 | 0,5,1,3,1,2,1,2,1,1 71 | 1,6,10,2,8,10,2,7,8,10 72 | 0,1,3,3,2,2,1,7,2,1 73 | 1,9,4,5,10,6,10,4,8,1 74 | 1,10,6,4,1,3,4,3,2,3 75 | 0,1,1,2,1,2,2,4,2,1 76 | 0,1,1,4,1,2,1,2,1,1 77 | 0,5,3,1,2,2,1,2,1,1 78 | 0,3,1,1,1,2,3,3,1,1 79 | 0,2,1,1,1,3,1,2,1,1 80 | 0,2,2,2,1,1,1,7,1,1 81 | 0,4,1,1,2,2,1,2,1,1 82 | 0,5,2,1,1,2,1,3,1,1 83 | 0,3,1,1,1,2,2,7,1,1 84 | 1,3,5,7,8,8,9,7,10,7 85 | 1,5,10,6,1,10,4,4,10,10 86 | 1,3,3,6,4,5,8,4,4,1 87 | 1,3,6,6,6,5,10,6,8,3 88 | 0,4,1,1,1,2,1,3,1,1 89 | 0,2,1,1,2,3,1,2,1,1 90 | 0,1,1,1,1,2,1,3,1,1 91 | 0,3,1,1,2,2,1,1,1,1 92 | 0,4,1,1,1,2,1,3,1,1 93 | 0,1,1,1,1,2,1,2,1,1 94 | 0,2,1,1,1,2,1,3,1,1 95 | 0,1,1,1,1,2,1,3,1,1 96 | 0,2,1,1,2,2,1,1,1,1 97 | 0,5,1,1,1,2,1,3,1,1 98 | 1,9,6,9,2,10,6,2,9,10 99 | 1,7,5,6,10,5,10,7,9,4 100 | 1,10,3,5,1,10,5,3,10,2 101 | 1,2,3,4,4,2,5,2,5,1 102 | 0,4,1,2,1,2,1,3,1,1 103 | 1,8,2,3,1,6,3,7,1,1 104 | 1,10,10,10,10,10,1,8,8,8 105 | 1,7,3,4,4,3,3,3,2,7 106 | 1,10,10,10,8,2,10,4,1,1 107 | 1,1,6,8,10,8,10,5,7,1 108 | 0,1,1,1,1,2,1,2,3,1 109 | 1,6,5,4,4,3,9,7,8,3 110 | 0,1,3,1,2,2,2,5,3,2 111 | 1,8,6,4,3,5,9,3,1,1 112 | 1,10,3,3,10,2,10,7,3,3 113 | 1,10,10,10,3,10,8,8,1,1 114 | 0,3,3,2,1,2,3,3,1,1 115 | 0,1,1,1,1,2,5,1,1,1 116 | 0,8,3,3,1,2,2,3,2,1 117 | 1,4,5,5,10,4,10,7,5,8 118 | 0,1,1,1,1,4,3,1,1,1 119 | 0,3,2,1,1,2,2,3,1,1 120 | 0,1,1,2,2,2,1,3,1,1 121 | 0,4,2,1,1,2,2,3,1,1 122 | 1,10,10,10,2,10,10,5,3,3 123 | 1,5,3,5,1,8,10,5,3,1 124 | 1,5,4,6,7,9,7,8,10,1 125 | 0,1,1,1,1,2,1,2,1,1 126 | 1,7,5,3,7,4,10,7,5,5 127 | 0,3,1,1,1,2,1,3,1,1 128 | 1,8,3,5,4,5,10,1,6,2 129 | 0,1,1,1,1,10,1,1,1,1 130 | 0,5,1,3,1,2,1,2,1,1 131 | 0,2,1,1,1,2,1,3,1,1 132 | 1,5,10,8,10,8,10,3,6,3 133 | 0,3,1,1,1,2,1,2,2,1 134 | 0,3,1,1,1,3,1,2,1,1 135 | 0,5,1,1,1,2,2,3,3,1 136 | 0,4,1,1,1,2,1,2,1,1 137 | 0,3,1,1,1,2,1,1,1,1 138 | 0,4,1,2,1,2,1,2,1,1 139 | 0,3,1,1,1,2,1,1,1,1 140 | 0,2,1,1,1,2,1,1,1,1 141 | 1,9,5,5,4,4,5,4,3,3 142 | 0,1,1,1,1,2,5,1,1,1 143 | 0,2,1,1,1,2,1,2,1,1 144 | 1,3,4,5,2,6,8,4,1,1 145 | 0,1,1,1,1,3,2,2,1,1 146 | 0,3,1,1,3,8,1,5,8,1 147 | 1,8,8,7,4,10,10,7,8,7 148 | 0,1,1,1,1,1,1,3,1,1 149 | 1,7,2,4,1,6,10,5,4,3 150 | 1,10,10,8,6,4,5,8,10,1 151 | 0,4,1,1,1,2,3,1,1,1 152 | 0,1,1,1,1,2,1,1,1,1 153 | 1,5,5,5,6,3,10,3,1,1 154 | 0,1,2,2,1,2,1,2,1,1 155 | 0,2,1,1,1,2,1,3,1,1 156 | 1,9,9,10,3,6,10,7,10,6 157 | 1,10,7,7,4,5,10,5,7,2 158 | 0,4,1,1,1,2,1,3,2,1 159 | 0,3,1,1,1,2,1,3,1,1 160 | 0,1,1,1,2,1,3,1,1,7 161 | 0,4,1,1,1,2,2,3,2,1 162 | 1,5,6,7,8,8,10,3,10,3 163 | 1,10,8,10,10,6,1,3,1,10 164 | 0,3,1,1,1,2,1,3,1,1 165 | 0,1,1,1,2,1,1,1,1,1 166 | 0,3,1,1,1,2,1,1,1,1 167 | 0,1,1,1,1,2,1,3,1,1 168 | 0,1,1,1,1,2,1,2,1,1 169 | 1,6,10,10,10,8,10,10,10,7 170 | 1,8,6,5,4,3,10,6,1,1 171 | 1,5,8,7,7,10,10,5,7,1 172 | 0,2,1,1,1,2,1,3,1,1 173 | 1,5,10,10,3,8,1,5,10,3 174 | 0,4,1,1,1,2,1,3,1,1 175 | 1,5,3,3,3,6,10,3,1,1 176 | 0,1,1,1,1,1,1,3,1,1 177 | 0,1,1,1,1,2,1,1,1,1 178 | 0,6,1,1,1,2,1,3,1,1 179 | 1,5,8,8,8,5,10,7,8,1 180 | 1,8,7,6,4,4,10,5,1,1 181 | 0,2,1,1,1,1,1,3,1,1 182 | 1,1,5,8,6,5,8,7,10,1 183 | 1,10,5,6,10,6,10,7,7,10 184 | 1,5,8,4,10,5,8,9,10,1 185 | 0,1,2,3,1,2,1,3,1,1 186 | 1,10,10,10,8,6,8,7,10,1 187 | 1,7,5,10,10,10,10,4,10,3 188 | 0,5,1,1,1,2,1,2,1,1 189 | 0,1,1,1,1,2,1,3,1,1 190 | 0,3,1,1,1,2,1,3,1,1 191 | 0,4,1,1,1,2,1,3,1,1 192 | 0,8,4,4,5,4,7,7,8,2 193 | 0,5,1,1,4,2,1,3,1,1 194 | 0,1,1,1,1,2,1,1,1,1 195 | 0,3,1,1,1,2,1,2,1,1 196 | 1,9,7,7,5,5,10,7,8,3 197 | 1,10,8,8,4,10,10,8,1,1 198 | 0,1,1,1,1,2,1,3,1,1 199 | 0,5,1,1,1,2,1,3,1,1 200 | 0,1,1,1,1,2,1,3,1,1 201 | 1,5,10,10,9,6,10,7,10,5 202 | 1,10,10,9,3,7,5,3,5,1 203 | 0,1,1,1,1,1,1,3,1,1 204 | 0,1,1,1,1,1,1,3,1,1 205 | 0,5,1,1,1,1,1,3,1,1 206 | 1,8,10,10,10,5,10,8,10,6 207 | 1,8,10,8,8,4,8,7,7,1 208 | 0,1,1,1,1,2,1,3,1,1 209 | 1,10,10,10,10,7,10,7,10,4 210 | 1,10,10,10,10,3,10,10,6,1 211 | 1,8,7,8,7,5,5,5,10,2 212 | 0,1,1,1,1,2,1,2,1,1 213 | 0,1,1,1,1,2,1,3,1,1 214 | 1,6,10,7,7,6,4,8,10,2 215 | 0,6,1,3,1,2,1,3,1,1 216 | 0,1,1,1,2,2,1,3,1,1 217 | 1,10,6,4,3,10,10,9,10,1 218 | 1,4,1,1,3,1,5,2,1,1 219 | 1,7,5,6,3,3,8,7,4,1 220 | 1,10,5,5,6,3,10,7,9,2 221 | 0,1,1,1,1,2,1,2,1,1 222 | 1,10,5,7,4,4,10,8,9,1 223 | 1,8,9,9,5,3,5,7,7,1 224 | 0,1,1,1,1,1,1,3,1,1 225 | 1,10,10,10,3,10,10,9,10,1 226 | 1,7,4,7,4,3,7,7,6,1 227 | 1,6,8,7,5,6,8,8,9,2 228 | 0,8,4,6,3,3,1,4,3,1 229 | 1,10,4,5,5,5,10,4,1,1 230 | 0,3,3,2,1,3,1,3,6,1 231 | 1,10,8,8,2,8,10,4,8,10 232 | 1,9,8,8,5,6,2,4,10,4 233 | 1,8,10,10,8,6,9,3,10,10 234 | 1,10,4,3,2,3,10,5,3,2 235 | 0,5,1,3,3,2,2,2,3,1 236 | 0,3,1,1,3,1,1,3,1,1 237 | 0,2,1,1,1,2,1,3,1,1 238 | 0,1,1,1,1,2,5,5,1,1 239 | 0,1,1,1,1,2,1,3,1,1 240 | 0,5,1,1,2,2,2,3,1,1 241 | 1,8,10,10,8,5,10,7,8,1 242 | 1,8,4,4,1,2,9,3,3,1 243 | 0,4,1,1,1,2,1,3,6,1 244 | 0,1,2,2,1,2,1,1,1,1 245 | 1,10,4,4,10,2,10,5,3,3 246 | 0,6,3,3,5,3,10,3,5,3 247 | 1,6,10,10,2,8,10,7,3,3 248 | 1,9,10,10,1,10,8,3,3,1 249 | 1,5,6,6,2,4,10,3,6,1 250 | 0,3,1,1,1,2,1,1,1,1 251 | 0,3,1,1,1,2,1,2,1,1 252 | 0,3,1,1,1,2,1,3,1,1 253 | 0,5,7,7,1,5,8,3,4,1 254 | 1,10,5,8,10,3,10,5,1,3 255 | 1,5,10,10,6,10,10,10,6,5 256 | 1,8,8,9,4,5,10,7,8,1 257 | 1,10,4,4,10,6,10,5,5,1 258 | 1,7,9,4,10,10,3,5,3,3 259 | 0,5,1,4,1,2,1,3,2,1 260 | 1,10,10,6,3,3,10,4,3,2 261 | 1,3,3,5,2,3,10,7,1,1 262 | 1,10,8,8,2,3,4,8,7,8 263 | 0,1,1,1,1,2,1,3,1,1 264 | 1,8,4,7,1,3,10,3,9,2 265 | 0,5,1,1,1,2,1,3,1,1 266 | 1,3,3,5,2,3,10,7,1,1 267 | 1,7,2,4,1,3,4,3,3,1 268 | 0,3,1,1,1,2,1,3,2,1 269 | 0,3,1,1,1,2,1,2,1,1 270 | 0,1,1,1,1,2,1,2,1,1 271 | 0,1,1,1,1,2,1,3,1,1 272 | 1,10,5,7,3,3,7,3,3,8 273 | 0,3,1,1,1,2,1,3,1,1 274 | 0,2,1,1,2,2,1,3,1,1 275 | 1,1,4,3,10,4,10,5,6,1 276 | 1,10,4,6,1,2,10,5,3,1 277 | 1,7,4,5,10,2,10,3,8,2 278 | 1,8,10,10,10,8,10,10,7,3 279 | 1,10,10,10,10,10,10,4,10,10 280 | 0,3,1,1,1,3,1,2,1,1 281 | 1,6,1,3,1,4,5,5,10,1 282 | 1,5,6,6,8,6,10,4,10,4 283 | 0,1,1,1,1,2,1,1,1,1 284 | 0,1,1,1,1,2,1,3,1,1 285 | 1,10,4,4,6,2,10,2,3,1 286 | 1,5,5,7,8,6,10,7,4,1 287 | 0,5,3,4,3,4,5,4,7,1 288 | 0,8,2,1,1,5,1,1,1,1 289 | 1,9,1,2,6,4,10,7,7,2 290 | 1,8,4,10,5,4,4,7,10,1 291 | 0,1,1,1,1,2,1,3,1,1 292 | 1,10,10,10,7,9,10,7,10,10 293 | 0,1,1,1,1,2,1,3,1,1 294 | 1,8,3,4,9,3,10,3,3,1 295 | 1,10,8,4,4,4,10,3,10,4 296 | 0,1,1,1,1,2,1,3,1,1 297 | 0,1,1,1,1,2,1,3,1,1 298 | 1,7,8,7,6,4,3,8,8,4 299 | 0,3,1,1,1,2,5,5,1,1 300 | 0,2,1,1,1,3,1,2,1,1 301 | 0,1,1,1,1,2,1,1,1,1 302 | 1,8,6,4,10,10,1,3,5,1 303 | 0,1,1,1,1,2,1,1,1,1 304 | 0,1,1,1,1,1,1,2,1,1 305 | 1,5,5,5,2,5,10,4,3,1 306 | 1,6,8,7,8,6,8,8,9,1 307 | 0,1,1,1,1,5,1,3,1,1 308 | 0,4,4,4,4,6,5,7,3,1 309 | 1,7,6,3,2,5,10,7,4,6 310 | 0,3,1,1,1,2,1,3,1,1 311 | 1,5,4,6,10,2,10,4,1,1 312 | 0,1,1,1,1,2,1,3,1,1 313 | 0,3,2,2,1,2,1,2,3,1 314 | 1,10,1,1,1,2,10,5,4,1 315 | 0,1,1,1,1,2,1,2,1,1 316 | 1,8,10,3,2,6,4,3,10,1 317 | 1,10,4,6,4,5,10,7,1,1 318 | 1,10,4,7,2,2,8,6,1,1 319 | 0,5,1,1,1,2,1,3,1,2 320 | 0,5,2,2,2,2,1,2,2,1 321 | 1,5,4,6,6,4,10,4,3,1 322 | 1,8,6,7,3,3,10,3,4,2 323 | 0,1,1,1,1,2,1,1,1,1 324 | 1,6,5,5,8,4,10,3,4,1 325 | 0,1,1,1,1,2,1,3,1,1 326 | 0,1,1,1,1,1,1,2,1,1 327 | 1,8,5,5,5,2,10,4,3,1 328 | 1,10,3,3,1,2,10,7,6,1 329 | 0,1,1,1,1,2,1,3,1,1 330 | 0,2,1,1,1,2,1,1,1,1 331 | 0,1,1,1,1,2,1,1,1,1 332 | 1,7,6,4,8,10,10,9,5,3 333 | 0,1,1,1,1,2,1,1,1,1 334 | 0,5,2,2,2,3,1,1,3,1 335 | 0,1,1,1,1,1,1,1,3,1 336 | 1,3,4,4,10,5,1,3,3,1 337 | 1,4,2,3,5,3,8,7,6,1 338 | 0,5,1,1,3,2,1,1,1,1 339 | 0,2,1,1,1,2,1,3,1,1 340 | 0,3,4,5,3,7,3,4,6,1 341 | 1,2,7,10,10,7,10,4,9,4 342 | 0,1,1,1,1,2,1,2,1,1 343 | 0,4,1,1,1,3,1,2,2,1 344 | 1,5,3,3,1,3,3,3,3,3 345 | 1,8,10,10,7,10,10,7,3,8 346 | 1,8,10,5,3,8,4,4,10,3 347 | 1,10,3,5,4,3,7,3,5,3 348 | 1,6,10,10,10,10,10,8,10,10 349 | 1,3,10,3,10,6,10,5,1,4 350 | 0,3,2,2,1,4,3,2,1,1 351 | 0,4,4,4,2,2,3,2,1,1 352 | 0,2,1,1,1,2,1,3,1,1 353 | 0,2,1,1,1,2,1,2,1,1 354 | 1,6,10,10,10,8,10,7,10,7 355 | 1,5,8,8,10,5,10,8,10,3 356 | 0,1,1,3,1,2,1,1,1,1 357 | 0,1,1,3,1,1,1,2,1,1 358 | 0,4,3,2,1,3,1,2,1,1 359 | 0,1,1,3,1,2,1,1,1,1 360 | 0,4,1,2,1,2,1,2,1,1 361 | 0,5,1,1,2,2,1,2,1,1 362 | 0,3,1,2,1,2,1,2,1,1 363 | 0,1,1,1,1,2,1,1,1,1 364 | 0,1,1,1,1,2,1,2,1,1 365 | 0,1,1,1,1,1,1,2,1,1 366 | 0,3,1,1,4,3,1,2,2,1 367 | 0,5,3,4,1,4,1,3,1,1 368 | 0,1,1,1,1,2,1,1,1,1 369 | 1,10,6,3,6,4,10,7,8,4 370 | 0,3,2,2,2,2,1,3,2,1 371 | 0,2,1,1,1,2,1,1,1,1 372 | 0,2,1,1,1,2,1,1,1,1 373 | 0,3,3,2,2,3,1,1,2,3 374 | 1,7,6,6,3,2,10,7,1,1 375 | 0,5,3,3,2,3,1,3,1,1 376 | 0,2,1,1,1,2,1,2,2,1 377 | 0,5,1,1,1,3,2,2,2,1 378 | 0,1,1,1,2,2,1,2,1,1 379 | 1,10,8,7,4,3,10,7,9,1 380 | 0,3,1,1,1,2,1,2,1,1 381 | 0,1,1,1,1,1,1,1,1,1 382 | 0,1,2,3,1,2,1,2,1,1 383 | 0,3,1,1,1,2,1,2,1,1 384 | 0,3,1,1,1,2,1,3,1,1 385 | 0,4,1,1,1,2,1,1,1,1 386 | 0,3,2,1,1,2,1,2,2,1 387 | 0,1,2,3,1,2,1,1,1,1 388 | 1,3,10,8,7,6,9,9,3,8 389 | 0,3,1,1,1,2,1,1,1,1 390 | 0,5,3,3,1,2,1,2,1,1 391 | 0,3,1,1,1,2,4,1,1,1 392 | 0,1,2,1,3,2,1,1,2,1 393 | 0,1,1,1,1,2,1,2,1,1 394 | 0,4,2,2,1,2,1,2,1,1 395 | 0,1,1,1,1,2,1,2,1,1 396 | 0,2,3,2,2,2,2,3,1,1 397 | 0,3,1,2,1,2,1,2,1,1 398 | 0,1,1,1,1,2,1,2,1,1 399 | 1,10,10,10,6,8,4,8,5,1 400 | 0,5,1,2,1,2,1,3,1,1 401 | 1,8,5,6,2,3,10,6,6,1 402 | 0,3,3,2,6,3,3,3,5,1 403 | 1,8,7,8,5,10,10,7,2,1 404 | 0,1,1,1,1,2,1,2,1,1 405 | 0,5,2,2,2,2,2,3,2,2 406 | 0,2,3,1,1,5,1,1,1,1 407 | 0,3,2,2,3,2,3,3,1,1 408 | 1,10,10,10,7,10,10,8,2,1 409 | 0,4,3,3,1,2,1,3,3,1 410 | 0,5,1,3,1,2,1,2,1,1 411 | 0,3,1,1,1,2,1,1,1,1 412 | 1,9,10,10,10,10,10,10,10,1 413 | 0,5,3,6,1,2,1,1,1,1 414 | 1,8,7,8,2,4,2,5,10,1 415 | 0,1,1,1,1,2,1,2,1,1 416 | 0,2,1,1,1,2,1,2,1,1 417 | 0,1,3,1,1,2,1,2,2,1 418 | 0,5,1,1,3,4,1,3,2,1 419 | 0,5,1,1,1,2,1,2,2,1 420 | 0,3,2,2,3,2,1,1,1,1 421 | 0,6,9,7,5,5,8,4,2,1 422 | 1,10,8,10,1,3,10,5,1,1 423 | 1,10,10,10,1,6,1,2,8,1 424 | 0,4,1,1,1,2,1,1,1,1 425 | 0,4,1,3,3,2,1,1,1,1 426 | 0,5,1,1,1,2,1,1,1,1 427 | 1,10,4,3,10,4,10,10,1,1 428 | 0,5,2,2,4,2,4,1,1,1 429 | 0,1,1,1,3,2,3,1,1,1 430 | 0,1,1,1,1,2,2,1,1,1 431 | 0,5,1,1,6,3,1,2,1,1 432 | 0,2,1,1,1,2,1,1,1,1 433 | 0,1,1,1,1,2,1,1,1,1 434 | 0,5,1,1,1,2,1,1,1,1 435 | 0,1,1,1,1,1,1,1,1,1 436 | 1,5,7,9,8,6,10,8,10,1 437 | 0,4,1,1,3,1,1,2,1,1 438 | 0,5,1,1,1,2,1,1,1,1 439 | 0,3,1,1,3,2,1,1,1,1 440 | 1,4,5,5,8,6,10,10,7,1 441 | 0,2,3,1,1,3,1,1,1,1 442 | 1,10,2,2,1,2,6,1,1,2 443 | 1,10,6,5,8,5,10,8,6,1 444 | 1,8,8,9,6,6,3,10,10,1 445 | 0,5,1,2,1,2,1,1,1,1 446 | 0,5,1,3,1,2,1,1,1,1 447 | 0,5,1,1,3,2,1,1,1,1 448 | 0,3,1,1,1,2,5,1,1,1 449 | 0,6,1,1,3,2,1,1,1,1 450 | 0,4,1,1,1,2,1,1,2,1 451 | 0,4,1,1,1,2,1,1,1,1 452 | 1,10,9,8,7,6,4,7,10,3 453 | 1,10,6,6,2,4,10,9,7,1 454 | 1,6,6,6,5,4,10,7,6,2 455 | 0,4,1,1,1,2,1,1,1,1 456 | 0,1,1,2,1,2,1,2,1,1 457 | 0,3,1,1,1,1,1,2,1,1 458 | 0,6,1,1,3,2,1,1,1,1 459 | 0,6,1,1,1,1,1,1,1,1 460 | 0,4,1,1,1,2,1,1,1,1 461 | 0,5,1,1,1,2,1,1,1,1 462 | 0,3,1,1,1,2,1,1,1,1 463 | 0,4,1,2,1,2,1,1,1,1 464 | 0,4,1,1,1,2,1,1,1,1 465 | 0,5,2,1,1,2,1,1,1,1 466 | 1,4,8,7,10,4,10,7,5,1 467 | 0,5,1,1,1,1,1,1,1,1 468 | 0,5,3,2,4,2,1,1,1,1 469 | 1,9,10,10,10,10,5,10,10,10 470 | 1,8,7,8,5,5,10,9,10,1 471 | 0,5,1,2,1,2,1,1,1,1 472 | 0,1,1,1,3,1,3,1,1,1 473 | 0,3,1,1,1,1,1,2,1,1 474 | 1,10,10,10,10,6,10,8,1,5 475 | 1,3,6,4,10,3,3,3,4,1 476 | 1,6,3,2,1,3,4,4,1,1 477 | 0,1,1,1,1,2,1,1,1,1 478 | 1,5,8,9,4,3,10,7,1,1 479 | 0,4,1,1,1,1,1,2,1,1 480 | 1,5,10,10,10,6,10,6,5,2 481 | 0,5,1,2,10,4,5,2,1,1 482 | 0,3,1,1,1,1,1,2,1,1 483 | 0,1,1,1,1,1,1,1,1,1 484 | 0,4,2,1,1,2,1,1,1,1 485 | 0,4,1,1,1,2,1,2,1,1 486 | 0,4,1,1,1,2,1,2,1,1 487 | 0,6,1,1,1,2,1,3,1,1 488 | 0,4,1,1,1,2,1,2,1,1 489 | 0,4,1,1,2,2,1,2,1,1 490 | 0,4,1,1,1,2,1,3,1,1 491 | 0,1,1,1,1,2,1,1,1,1 492 | 0,3,3,1,1,2,1,1,1,1 493 | 1,8,10,10,10,7,5,4,8,7 494 | 0,1,1,1,1,2,4,1,1,1 495 | 0,5,1,1,1,2,1,1,1,1 496 | 0,2,1,1,1,2,1,1,1,1 497 | 0,1,1,1,1,2,1,1,1,1 498 | 0,5,1,1,1,2,1,2,1,1 499 | 0,5,1,1,1,2,1,1,1,1 500 | 0,3,1,1,1,1,1,2,1,1 501 | 1,6,6,7,10,3,10,8,10,2 502 | 1,4,10,4,7,3,10,9,10,1 503 | 0,1,1,1,1,1,1,1,1,1 504 | 0,1,1,1,1,1,1,2,1,1 505 | 0,3,1,2,2,2,1,1,1,1 506 | 1,4,7,8,3,4,10,9,1,1 507 | 0,1,1,1,1,3,1,1,1,1 508 | 0,4,1,1,1,3,1,1,1,1 509 | 1,10,4,5,4,3,5,7,3,1 510 | 1,7,5,6,10,4,10,5,3,1 511 | 0,3,1,1,1,2,1,2,1,1 512 | 0,3,1,1,2,2,1,1,1,1 513 | 0,4,1,1,1,2,1,1,1,1 514 | 0,4,1,1,1,2,1,3,1,1 515 | 0,6,1,3,2,2,1,1,1,1 516 | 0,4,1,1,1,1,1,2,1,1 517 | 1,7,4,4,3,4,10,6,9,1 518 | 0,4,2,2,1,2,1,2,1,1 519 | 0,1,1,1,1,1,1,3,1,1 520 | 0,3,1,1,1,2,1,2,1,1 521 | 0,2,1,1,1,2,1,2,1,1 522 | 0,1,1,3,2,2,1,3,1,1 523 | 0,5,1,1,1,2,1,3,1,1 524 | 0,5,1,2,1,2,1,3,1,1 525 | 0,4,1,1,1,2,1,2,1,1 526 | 0,6,1,1,1,2,1,2,1,1 527 | 0,5,1,1,1,2,2,2,1,1 528 | 0,3,1,1,1,2,1,1,1,1 529 | 0,5,3,1,1,2,1,1,1,1 530 | 0,4,1,1,1,2,1,2,1,1 531 | 0,2,1,3,2,2,1,2,1,1 532 | 0,5,1,1,1,2,1,2,1,1 533 | 1,6,10,10,10,4,10,7,10,1 534 | 0,2,1,1,1,1,1,1,1,1 535 | 0,3,1,1,1,1,1,1,1,1 536 | 1,7,8,3,7,4,5,7,8,2 537 | 0,3,1,1,1,2,1,2,1,1 538 | 0,1,1,1,1,2,1,3,1,1 539 | 0,3,2,2,2,2,1,4,2,1 540 | 0,4,4,2,1,2,5,2,1,2 541 | 0,3,1,1,1,2,1,1,1,1 542 | 0,4,3,1,1,2,1,4,8,1 543 | 0,5,2,2,2,1,1,2,1,1 544 | 0,5,1,1,3,2,1,1,1,1 545 | 0,2,1,1,1,2,1,2,1,1 546 | 0,5,1,1,1,2,1,2,1,1 547 | 0,5,1,1,1,2,1,3,1,1 548 | 0,5,1,1,1,2,1,3,1,1 549 | 0,1,1,1,1,2,1,3,1,1 550 | 0,3,1,1,1,2,1,2,1,1 551 | 0,4,1,1,1,2,1,3,2,1 552 | 1,5,7,10,10,5,10,10,10,1 553 | 0,3,1,2,1,2,1,3,1,1 554 | 0,4,1,1,1,2,3,2,1,1 555 | 1,8,4,4,1,6,10,2,5,2 556 | 1,10,10,8,10,6,5,10,3,1 557 | 1,8,10,4,4,8,10,8,2,1 558 | 1,7,6,10,5,3,10,9,10,2 559 | 0,3,1,1,1,2,1,2,1,1 560 | 0,1,1,1,1,2,1,2,1,1 561 | 1,10,9,7,3,4,2,7,7,1 562 | 0,5,1,2,1,2,1,3,1,1 563 | 0,5,1,1,1,2,1,2,1,1 564 | 0,1,1,1,1,2,1,2,1,1 565 | 0,1,1,1,1,2,1,2,1,1 566 | 0,1,1,1,1,2,1,3,1,1 567 | 0,5,1,2,1,2,1,2,1,1 568 | 1,5,7,10,6,5,10,7,5,1 569 | 1,6,10,5,5,4,10,6,10,1 570 | 0,3,1,1,1,2,1,1,1,1 571 | 0,5,1,1,6,3,1,1,1,1 572 | 0,1,1,1,1,2,1,1,1,1 573 | 1,8,10,10,10,6,10,10,10,1 574 | 0,5,1,1,1,2,1,2,2,1 575 | 1,9,8,8,9,6,3,4,1,1 576 | 0,5,1,1,1,2,1,1,1,1 577 | 1,4,10,8,5,4,1,10,1,1 578 | 1,2,5,7,6,4,10,7,6,1 579 | 1,10,3,4,5,3,10,4,1,1 580 | 0,5,1,2,1,2,1,1,1,1 581 | 1,4,8,6,3,4,10,7,1,1 582 | 0,5,1,1,1,2,1,2,1,1 583 | 0,4,1,2,1,2,1,2,1,1 584 | 0,5,1,3,1,2,1,3,1,1 585 | 0,3,1,1,1,2,1,2,1,1 586 | 0,5,2,4,1,1,1,1,1,1 587 | 0,3,1,1,1,2,1,2,1,1 588 | 0,1,1,1,1,1,1,2,1,1 589 | 0,4,1,1,1,2,1,2,1,1 590 | 1,5,4,6,8,4,1,8,10,1 591 | 1,5,3,2,8,5,10,8,1,2 592 | 1,10,5,10,3,5,8,7,8,3 593 | 0,4,1,1,2,2,1,1,1,1 594 | 0,1,1,1,1,2,1,1,1,1 595 | 1,5,10,10,10,10,10,10,1,1 596 | 0,5,1,1,1,2,1,1,1,1 597 | 1,10,4,3,10,3,10,7,1,2 598 | 1,5,10,10,10,5,2,8,5,1 599 | 1,8,10,10,10,6,10,10,10,10 600 | 0,2,3,1,1,2,1,2,1,1 601 | 0,2,1,1,1,1,1,2,1,1 602 | 0,4,1,3,1,2,1,2,1,1 603 | 0,3,1,1,1,2,1,2,1,1 604 | 0,4,1,1,1,2,1,2,1,1 605 | 0,5,1,1,1,2,1,2,1,1 606 | 0,3,1,1,1,2,1,2,1,1 607 | 0,6,3,3,3,3,2,6,1,1 608 | 0,7,1,2,3,2,1,2,1,1 609 | 0,1,1,1,1,2,1,1,1,1 610 | 0,5,1,1,2,1,1,2,1,1 611 | 0,3,1,3,1,3,4,1,1,1 612 | 1,4,6,6,5,7,6,7,7,3 613 | 0,2,1,1,1,2,5,1,1,1 614 | 0,2,1,1,1,2,1,1,1,1 615 | 0,4,1,1,1,2,1,1,1,1 616 | 0,6,2,3,1,2,1,1,1,1 617 | 0,5,1,1,1,2,1,2,1,1 618 | 0,1,1,1,1,2,1,1,1,1 619 | 1,8,7,4,4,5,3,5,10,1 620 | 0,3,1,1,1,2,1,1,1,1 621 | 0,3,1,4,1,2,1,1,1,1 622 | 1,10,10,7,8,7,1,10,10,3 623 | 0,4,2,4,3,2,2,2,1,1 624 | 0,4,1,1,1,2,1,1,1,1 625 | 0,5,1,1,3,2,1,1,1,1 626 | 0,4,1,1,3,2,1,1,1,1 627 | 0,3,1,1,1,2,1,2,1,1 628 | 0,3,1,1,1,2,1,2,1,1 629 | 0,1,1,1,1,2,1,1,1,1 630 | 0,2,1,1,1,2,1,1,1,1 631 | 0,3,1,1,1,2,1,2,1,1 632 | 0,1,2,2,1,2,1,1,1,1 633 | 0,1,1,1,3,2,1,1,1,1 634 | 1,5,10,10,10,10,2,10,10,10 635 | 0,3,1,1,1,2,1,2,1,1 636 | 0,3,1,1,2,3,4,1,1,1 637 | 0,1,2,1,3,2,1,2,1,1 638 | 0,5,1,1,1,2,1,2,2,1 639 | 0,4,1,1,1,2,1,2,1,1 640 | 0,3,1,1,1,2,1,3,1,1 641 | 0,3,1,1,1,2,1,2,1,1 642 | 0,5,1,1,1,2,1,2,1,1 643 | 0,5,4,5,1,8,1,3,6,1 644 | 1,7,8,8,7,3,10,7,2,3 645 | 0,1,1,1,1,2,1,1,1,1 646 | 0,1,1,1,1,2,1,2,1,1 647 | 0,4,1,1,1,2,1,3,1,1 648 | 0,1,1,3,1,2,1,2,1,1 649 | 0,1,1,3,1,2,1,2,1,1 650 | 0,3,1,1,3,2,1,2,1,1 651 | 0,1,1,1,1,2,1,1,1,1 652 | 0,5,2,2,2,2,1,1,1,2 653 | 0,3,1,1,1,2,1,3,1,1 654 | 1,5,7,4,1,6,1,7,10,3 655 | 1,5,10,10,8,5,5,7,10,1 656 | 1,3,10,7,8,5,8,7,4,1 657 | 0,3,2,1,2,2,1,3,1,1 658 | 0,2,1,1,1,2,1,3,1,1 659 | 0,5,3,2,1,3,1,1,1,1 660 | 0,1,1,1,1,2,1,2,1,1 661 | 0,4,1,4,1,2,1,1,1,1 662 | 0,1,1,2,1,2,1,2,1,1 663 | 0,5,1,1,1,2,1,1,1,1 664 | 0,1,1,1,1,2,1,1,1,1 665 | 0,2,1,1,1,2,1,1,1,1 666 | 1,10,10,10,10,5,10,10,10,7 667 | 1,5,10,10,10,4,10,5,6,3 668 | 0,5,1,1,1,2,1,3,2,1 669 | 0,1,1,1,1,2,1,1,1,1 670 | 0,1,1,1,1,2,1,1,1,1 671 | 0,1,1,1,1,2,1,1,1,1 672 | 0,1,1,1,1,2,1,1,1,1 673 | 0,3,1,1,1,2,1,2,3,1 674 | 0,4,1,1,1,2,1,1,1,1 675 | 0,1,1,1,1,2,1,1,1,8 676 | 0,1,1,1,3,2,1,1,1,1 677 | 1,5,10,10,5,4,5,4,4,1 678 | 0,3,1,1,1,2,1,1,1,1 679 | 0,3,1,1,1,2,1,2,1,2 680 | 0,3,1,1,1,3,2,1,1,1 681 | 0,2,1,1,1,2,1,1,1,1 682 | 1,5,10,10,3,7,3,8,10,2 683 | 1,4,8,6,4,3,4,10,6,1 684 | 1,4,8,8,5,4,5,10,4,1 685 | -------------------------------------------------------------------------------- /riskslim/mip.py: -------------------------------------------------------------------------------- 1 | from math import ceil, floor 2 | import numpy as np 3 | from cplex import Cplex, SparsePair, infinity as CPX_INFINITY 4 | from .coefficient_set import CoefficientSet 5 | from .utils import print_log 6 | 7 | #todo: add loss cut 8 | #todo: add constraint function 9 | #todo: default cplex parameters 10 | #todo: check cores 11 | #todo: pass compute_loss to convert_risk_slim_cplex_solution 12 | 13 | def create_risk_slim(coef_set, input): 14 | """ 15 | create RiskSLIM MIP object 16 | 17 | Parameters 18 | ---------- 19 | input - dictionary of RiskSLIM parameters and formulation 20 | 21 | Returns 22 | ------- 23 | mip - RiskSLIM surrogate MIP without 0 cuts 24 | 25 | Issues 26 | ---- 27 | no support for non-integer Lset "values" 28 | only drops intercept index for variable_names that match '(Intercept)' 29 | 30 | """ 31 | assert isinstance(coef_set, CoefficientSet) 32 | assert isinstance(input, dict) 33 | 34 | # setup printing and loading 35 | function_print_flag = input.get('print_flag', False) 36 | print_from_function = lambda msg: print_log(msg) if function_print_flag else lambda msg: None 37 | 38 | # set default parameters 39 | input.setdefault('w_pos', 1.0) 40 | input.setdefault('w_neg', 2.0 - input['w_pos']) 41 | input.setdefault('C_0', 0.01) 42 | input.setdefault('include_auxillary_variable_for_objval', True) 43 | input.setdefault('include_auxillary_variable_for_L0_norm', True) 44 | input.setdefault('loss_min', 0.00) 45 | input.setdefault('loss_max', float(CPX_INFINITY)) 46 | input.setdefault('L0_min', 0) 47 | input.setdefault('L0_max', len(coef_set)) 48 | input.setdefault('objval_min', 0.00) 49 | input.setdefault('objval_max', float(CPX_INFINITY)) 50 | input.setdefault('relax_integer_variables', False) 51 | input.setdefault('drop_variables', True) 52 | input.setdefault('tight_formulation', False) 53 | input.setdefault('set_cplex_cutoffs', True) 54 | 55 | # variables 56 | P = len(coef_set) 57 | w_pos, w_neg = input['w_pos'], input['w_neg'] 58 | C_0j = np.copy(coef_set.c0) 59 | L0_reg_ind = np.isnan(C_0j) 60 | C_0j[L0_reg_ind] = input['C_0'] 61 | C_0j = C_0j.tolist() 62 | C_0_rho = np.copy(C_0j) 63 | trivial_L0_min = 0 64 | trivial_L0_max = np.sum(L0_reg_ind) 65 | 66 | rho_ub = list(coef_set.ub) 67 | rho_lb = list(coef_set.lb) 68 | rho_type = ''.join(list(coef_set.vtype)) 69 | 70 | # calculate min/max values for loss 71 | loss_min = max(0.0, float(input['loss_min'])) 72 | loss_max = min(CPX_INFINITY, float(input['loss_max'])) 73 | 74 | # calculate min/max values for model size 75 | L0_min = max(input['L0_min'], 0.0) 76 | L0_max = min(input['L0_max'], trivial_L0_max) 77 | L0_min = ceil(L0_min) 78 | L0_max = floor(L0_max) 79 | assert L0_min <= L0_max 80 | 81 | # calculate min/max values for objval 82 | objval_min = max(input['objval_min'], 0.0) 83 | objval_max = min(input['objval_max'], CPX_INFINITY) 84 | assert objval_min <= objval_max 85 | 86 | # include constraint on min/max model size? 87 | nontrivial_L0_min = L0_min > trivial_L0_min 88 | nontrivial_L0_max = L0_max < trivial_L0_max 89 | include_auxillary_variable_for_L0_norm = input['include_auxillary_variable_for_L0_norm'] or \ 90 | nontrivial_L0_min or \ 91 | nontrivial_L0_max 92 | 93 | # include constraint on min/max objective value? 94 | nontrivial_objval_min = objval_min > 0.0 95 | nontrivial_objval_max = objval_max < CPX_INFINITY 96 | include_auxillary_variable_for_objval = input['include_auxillary_variable_for_objval'] or \ 97 | nontrivial_objval_min or \ 98 | nontrivial_objval_max 99 | 100 | has_intercept = '(Intercept)' in coef_set.variable_names 101 | """ 102 | RiskSLIM MIP Formulation 103 | 104 | minimize w_pos*loss_pos + w_neg *loss_minus + 0*rho_j + C_0j*alpha_j 105 | 106 | such that 107 | 108 | L0_min ≤ L0 ≤ L0_max 109 | -rho_min * alpha_j < lambda_j < rho_max * alpha_j 110 | 111 | L_0 in 0 to P 112 | rho_j in [rho_min_j, rho_max_j] 113 | alpha_j in {0,1} 114 | 115 | x = [loss_pos, loss_neg, rho_j, alpha_j] 116 | 117 | optional constraints: 118 | objval = w_pos * loss_pos + w_neg * loss_min + sum(C_0j * alpha_j) (required for callback) 119 | L0_norm = sum(alpha_j) (required for callback) 120 | 121 | 122 | Changes for Tight Formulation (included when input['tight_formulation'] = True): 123 | 124 | sigma_j in {0,1} for j s.t. lambda_j has free sign and alpha_j exists 125 | lambda_j ≥ delta_pos_j if alpha_j = 1 and sigma_j = 1 126 | lambda_j ≥ -delta_neg_j if alpha_j = 1 and sigma_j = 0 127 | lambda_j ≥ alpha_j for j such that lambda_j >= 0 128 | lambda_j ≤ -alpha_j for j such that lambda_j <= 0 129 | 130 | """ 131 | 132 | # create MIP object 133 | mip = Cplex() 134 | vars = mip.variables 135 | cons = mip.linear_constraints 136 | 137 | # set sense 138 | mip.objective.set_sense(mip.objective.sense.minimize) 139 | 140 | # add main variables 141 | loss_obj = [w_pos] 142 | loss_ub = [loss_max] 143 | loss_lb = [loss_min] 144 | loss_type = 'C' 145 | loss_names = ['loss'] 146 | 147 | obj = loss_obj + [0.0] * P + C_0j 148 | ub = loss_ub + rho_ub + [1.0] * P 149 | lb = loss_lb + rho_lb + [0.0] * P 150 | ctype = loss_type + rho_type + 'B' * P 151 | 152 | rho_names = ['rho_%d' % j for j in range(P)] 153 | alpha_names = ['alpha_%d' % j for j in range(P)] 154 | varnames = loss_names + rho_names + alpha_names 155 | 156 | if include_auxillary_variable_for_objval: 157 | objval_auxillary_name = ['objval'] 158 | objval_auxillary_ub = [objval_max] 159 | objval_auxillary_lb = [objval_min] 160 | objval_type = 'C' 161 | 162 | print_from_function("adding auxiliary variable for objval s.t. %1.4f <= objval <= %1.4f" % (objval_min, objval_max)) 163 | obj += [0.0] 164 | ub += objval_auxillary_ub 165 | lb += objval_auxillary_lb 166 | varnames += objval_auxillary_name 167 | ctype += objval_type 168 | 169 | 170 | if include_auxillary_variable_for_L0_norm: 171 | L0_norm_auxillary_name = ['L0_norm'] 172 | L0_norm_auxillary_ub = [L0_max] 173 | L0_norm_auxillary_lb = [L0_min] 174 | L0_norm_type = 'I' 175 | 176 | print_from_function("adding auxiliary variable for L0_norm s.t. %d <= L0_norm <= %d" % (L0_min, L0_max)) 177 | obj += [0.0] 178 | ub += L0_norm_auxillary_ub 179 | lb += L0_norm_auxillary_lb 180 | varnames += L0_norm_auxillary_name 181 | ctype += L0_norm_type 182 | 183 | if input['relax_integer_variables']: 184 | ctype = ctype.replace('I', 'C') 185 | ctype = ctype.replace('B', 'C') 186 | 187 | vars.add(obj = obj, lb = lb, ub = ub, types = ctype, names = varnames) 188 | 189 | # 0-Norm LB Constraints: 190 | # lambda_j,lb * alpha_j ≤ lambda_j <= Inf 191 | # 0 ≤ lambda_j - lambda_j,lb * alpha_j < Inf 192 | for j in range(P): 193 | cons.add(names = ["L0_norm_lb_" + str(j)], 194 | lin_expr = [SparsePair(ind=[rho_names[j], alpha_names[j]], val=[1.0, -rho_lb[j]])], 195 | senses = "G", 196 | rhs = [0.0]) 197 | 198 | # 0-Norm UB Constraints: 199 | # lambda_j ≤ lambda_j,ub * alpha_j 200 | # 0 <= -lambda_j + lambda_j,ub * alpha_j 201 | for j in range(P): 202 | cons.add(names = ["L0_norm_ub_" + str(j)], 203 | lin_expr =[SparsePair(ind=[rho_names[j], alpha_names[j]], val=[-1.0, rho_ub[j]])], 204 | senses = "G", 205 | rhs = [0.0]) 206 | 207 | # objval_max constraint 208 | # loss_var + sum(C_0j .* alpha_j) <= objval_max 209 | if include_auxillary_variable_for_objval: 210 | print_from_function("adding constraint so that objective value <= " + str(objval_max)) 211 | cons.add(names = ["objval_def"], 212 | lin_expr = [SparsePair(ind = objval_auxillary_name + loss_names + alpha_names, val=[-1.0] + loss_obj + C_0j)], 213 | senses = "E", 214 | rhs = [0.0]) 215 | 216 | # Auxiliary L0_norm variable definition: 217 | # L0_norm = sum(alpha_j) 218 | # L0_norm - sum(alpha_j) = 0 219 | if include_auxillary_variable_for_L0_norm: 220 | cons.add(names = ["L0_norm_def"], 221 | lin_expr = [SparsePair(ind = L0_norm_auxillary_name + alpha_names, val = [1.0] + [-1.0] * P)], 222 | senses = "E", 223 | rhs = [0.0]) 224 | 225 | 226 | # drop L0_norm_lb constraint for any variable with rho_lb >= 0 227 | dropped_variables = [] 228 | constraints_to_drop = [] 229 | 230 | # drop alpha / L0_norm_ub / L0_norm_lb for ('Intercept') 231 | if input['drop_variables']: 232 | # drop L0_norm_ub/lb constraint for any variable with rho_ub/rho_lb >= 0 233 | sign_pos_ind = np.flatnonzero(coef_set.sign > 0) 234 | sign_neg_ind = np.flatnonzero(coef_set.sign < 0) 235 | constraints_to_drop.extend(["L0_norm_lb_" + str(j) for j in sign_pos_ind]) 236 | constraints_to_drop.extend(["L0_norm_ub_" + str(j) for j in sign_neg_ind]) 237 | 238 | # drop alpha for any variable where rho_ub = rho_lb = 0 239 | fixed_value_ind = np.flatnonzero(coef_set.ub == coef_set.lb) 240 | variables_to_drop = ["alpha_" + str(j) for j in fixed_value_ind] 241 | vars.delete(variables_to_drop) 242 | dropped_variables += variables_to_drop 243 | alpha_names = [alpha_names[j] for j in range(P) if alpha_names[j] not in dropped_variables] 244 | 245 | if has_intercept: 246 | intercept_idx = coef_set.variable_names.index('(Intercept)') 247 | intercept_alpha_name = 'alpha_' + str(intercept_idx) 248 | vars.delete([intercept_alpha_name]) 249 | 250 | alpha_names.remove(intercept_alpha_name) 251 | dropped_variables.append(intercept_alpha_name) 252 | 253 | print_from_function("dropped L0 indicator for '(Intercept)'") 254 | constraints_to_drop.extend(["L0_norm_ub_" + str(intercept_idx), "L0_norm_lb_" + str(intercept_idx)]) 255 | 256 | if len(constraints_to_drop) > 0: 257 | constraints_to_drop = list(set(constraints_to_drop)) 258 | cons.delete(constraints_to_drop) 259 | 260 | # indices 261 | indices = { 262 | 'n_variables': vars.get_num(), 263 | 'n_constraints': cons.get_num(), 264 | 'names': vars.get_names(), 265 | 'loss_names': loss_names, 266 | 'rho_names': rho_names, 267 | 'alpha_names': alpha_names, 268 | 'loss': vars.get_indices(loss_names), 269 | 'rho': vars.get_indices(rho_names), 270 | 'alpha': vars.get_indices(alpha_names), 271 | 'L0_reg_ind': L0_reg_ind, 272 | 'C_0_rho': C_0_rho, 273 | 'C_0_alpha': mip.objective.get_linear(alpha_names) if len(alpha_names) > 0 else [], 274 | } 275 | 276 | if include_auxillary_variable_for_objval: 277 | indices.update({ 278 | 'objval_name': objval_auxillary_name, 279 | 'objval': vars.get_indices(objval_auxillary_name)[0], 280 | }) 281 | 282 | if include_auxillary_variable_for_L0_norm: 283 | indices.update({ 284 | 'L0_norm_name': L0_norm_auxillary_name, 285 | 'L0_norm': vars.get_indices(L0_norm_auxillary_name)[0], 286 | }) 287 | 288 | # officially change the problem to LP if variables are relaxed 289 | if input['relax_integer_variables']: 290 | old_problem_type = mip.problem_type[mip.get_problem_type()] 291 | mip.set_problem_type(mip.problem_type.LP) 292 | new_problem_type = mip.problem_type[mip.get_problem_type()] 293 | print_from_function("changed problem type from %s to %s" % (old_problem_type, new_problem_type)) 294 | 295 | if input['set_cplex_cutoffs'] and not input['relax_integer_variables']: 296 | mip.parameters.mip.tolerances.lowercutoff.set(objval_min) 297 | mip.parameters.mip.tolerances.uppercutoff.set(objval_max) 298 | 299 | return mip, indices 300 | 301 | 302 | def set_cplex_mip_parameters(cpx, param, display_cplex_progress = False): 303 | """ 304 | Helper function to set CPLEX parameters of CPLEX MIP object 305 | 306 | Parameters 307 | ---------- 308 | mip 309 | param 310 | display_cplex_progress 311 | 312 | Returns 313 | ------- 314 | MIP with parameters 315 | 316 | """ 317 | p = cpx.parameters 318 | p.randomseed.set(param['randomseed']) 319 | p.threads.set(param['n_cores']) 320 | p.output.clonelog.set(0) 321 | p.parallel.set(1) 322 | 323 | if display_cplex_progress is (None or False): 324 | cpx = set_cpx_display_options(cpx, display_mip = False, display_lp = False, display_parameters = False) 325 | 326 | problem_type = cpx.problem_type[cpx.get_problem_type()] 327 | if problem_type == 'MIP': 328 | # CPLEX Memory Parameters 329 | # MIP.Param.workdir.Cur = exp_workdir; 330 | # MIP.Param.workmem.Cur = cplex_workingmem; 331 | # MIP.Param.mip.strategy.file.Cur = 2; %nodefile uncompressed 332 | # MIP.Param.mip.limits.treememory.Cur = cplex_nodefilesize; 333 | 334 | # CPLEX MIP Parameters 335 | p.emphasis.mip.set(param['mipemphasis']) 336 | p.mip.tolerances.mipgap.set(param['mipgap']) 337 | p.mip.tolerances.absmipgap.set(param['absmipgap']) 338 | p.mip.tolerances.integrality.set(param['integrality_tolerance']) 339 | 340 | # CPLEX Solution Pool Parameters 341 | p.mip.limits.repairtries.set(param['repairtries']) 342 | p.mip.pool.capacity.set(param['poolsize']) 343 | p.mip.pool.replace.set(param['poolreplace']) 344 | # 0 = replace oldest /1: replace worst objective / #2 = replace least diverse solutions 345 | 346 | return cpx 347 | 348 | 349 | def set_cpx_display_options(cpx, display_mip = True, display_parameters = False, display_lp = False): 350 | 351 | cpx.parameters.mip.display.set(display_mip) 352 | cpx.parameters.simplex.display.set(display_lp) 353 | 354 | try: 355 | cpx.parameters.paramdisplay.set(display_parameters) 356 | except AttributeError: 357 | pass 358 | 359 | if not (display_mip or display_lp): 360 | cpx.set_results_stream(None) 361 | cpx.set_log_stream(None) 362 | cpx.set_error_stream(None) 363 | cpx.set_warning_stream(None) 364 | 365 | return cpx 366 | 367 | 368 | def add_mip_starts(mip, indices, pool, max_mip_starts = float('inf'), mip_start_effort_level = 4): 369 | """ 370 | 371 | Parameters 372 | ---------- 373 | mip - RiskSLIM surrogate MIP 374 | indices - indices of RiskSLIM surrogate MIP 375 | pool - solution pool 376 | max_mip_starts - max number of mip starts to add (optional; default is add all) 377 | mip_start_effort_level - effort that CPLEX will spend trying to fix (optional; default is 4) 378 | 379 | Returns 380 | ------- 381 | 382 | """ 383 | # todo remove suboptimal using pool filter 384 | assert isinstance(mip, Cplex) 385 | 386 | try: 387 | obj_cutoff = mip.parameters.mip.tolerances.uppercutoff.get() 388 | except: 389 | obj_cutoff = float('inf') 390 | 391 | pool = pool.distinct().sort() 392 | 393 | n_added = 0 394 | for objval, rho in zip(pool.objvals, pool.solutions): 395 | if np.less_equal(objval, obj_cutoff): 396 | mip_start_name = "mip_start_" + str(n_added) 397 | mip_start_obj, _ = convert_to_risk_slim_cplex_solution(rho = rho, indices = indices, objval = objval) 398 | mip_start_obj = cast_mip_start(mip_start_obj, mip) 399 | mip.MIP_starts.add(mip_start_obj, mip_start_effort_level, mip_start_name) 400 | n_added += 1 401 | 402 | if n_added >= max_mip_starts: 403 | break 404 | 405 | return mip 406 | 407 | 408 | def cast_mip_start(mip_start, cpx): 409 | """ 410 | casts the solution values and indices in a Cplex SparsePair 411 | 412 | Parameters 413 | ---------- 414 | mip_start cplex SparsePair 415 | cpx Cplex 416 | 417 | Returns 418 | ------- 419 | Cplex SparsePair where the indices are integers and the values for each variable match the variable type specified in CPLEX Object 420 | """ 421 | 422 | assert isinstance(cpx, Cplex) 423 | assert isinstance(mip_start, SparsePair) 424 | vals = list(mip_start.val) 425 | idx = np.array(list(mip_start.ind), dtype = int).tolist() 426 | types = cpx.variables.get_types(idx) 427 | 428 | for j, t in enumerate(types): 429 | if t in ['B', 'I']: 430 | vals[j] = int(vals[j]) 431 | elif t in ['C']: 432 | vals[j] = float(vals[j]) 433 | 434 | return SparsePair(ind = idx, val = vals) 435 | 436 | 437 | def convert_to_risk_slim_cplex_solution(rho, indices, loss = None, objval = None): 438 | """ 439 | Convert coefficient vector 'rho' into a solution for RiskSLIM CPLEX MIP 440 | 441 | Parameters 442 | ---------- 443 | rho 444 | indices 445 | loss 446 | objval 447 | 448 | Returns 449 | ------- 450 | 451 | """ 452 | global compute_loss 453 | n_variables = indices['n_variables'] 454 | solution_idx = np.arange(n_variables) 455 | solution_val = np.zeros(n_variables) 456 | 457 | # rho 458 | solution_val[indices['rho']] = rho 459 | 460 | # alpha 461 | alpha = np.zeros(len(indices['alpha'])) 462 | alpha[np.flatnonzero(rho[indices['L0_reg_ind']])] = 1.0 463 | solution_val[indices['alpha']] = alpha 464 | L0_penalty = np.sum(indices['C_0_alpha'] * alpha) 465 | 466 | # add loss / objval 467 | need_loss = 'loss' in indices 468 | need_objective_val = 'objval' in indices 469 | need_L0_norm = 'L0_norm' in indices 470 | need_sigma = 'sigma_names' in indices 471 | 472 | # check that we have the right length 473 | # COMMENT THIS OUT FOR DEPLOYMENT 474 | # if need_sigma: 475 | # pass 476 | # else: 477 | # assert (indices['n_variables'] == (len(rho) + len(alpha) + need_loss + need_objective_val + need_L0_norm)) 478 | 479 | if need_loss: 480 | if loss is None: 481 | if objval is None: 482 | loss = compute_loss(rho) 483 | else: 484 | loss = objval - L0_penalty 485 | 486 | solution_val[indices['loss']] = loss 487 | 488 | if need_objective_val: 489 | if objval is None: 490 | if loss is None: 491 | objval = compute_loss(rho) + L0_penalty 492 | else: 493 | objval = loss + L0_penalty 494 | 495 | solution_val[indices['objval']] = objval 496 | 497 | if need_L0_norm: 498 | solution_val[indices['L0_norm']] = np.sum(alpha) 499 | 500 | if need_sigma: 501 | rho_for_sigma = np.array([indices['rho'][int(s.strip('sigma_'))] for s in indices['sigma_names']]) 502 | solution_val[indices['sigma']] = np.abs(solution_val[rho_for_sigma]) 503 | 504 | solution_cpx = SparsePair(ind = solution_idx, val = solution_val.tolist()) 505 | return solution_cpx, objval 506 | -------------------------------------------------------------------------------- /riskslim/initialization.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from cplex import Cplex, SparsePair, infinity as CPX_INFINITY 4 | from .setup_functions import setup_penalty_parameters 5 | from .mip import create_risk_slim, set_cplex_mip_parameters 6 | from .solution_pool import SolutionPool 7 | from .bound_tightening import chained_updates, chained_updates_for_lp 8 | from .heuristics import discrete_descent, sequential_rounding 9 | from .defaults import DEFAULT_CPA_SETTINGS, DEFAULT_INITIALIZATION_SETTINGS 10 | from .utils import print_log, validate_settings 11 | 12 | 13 | def initialize_lattice_cpa(Z, 14 | c0_value, 15 | constraints, 16 | bounds, 17 | settings, 18 | risk_slim_settings, 19 | cplex_settings, 20 | compute_loss_real, 21 | compute_loss_cut_real, 22 | compute_loss_from_scores_real, 23 | compute_loss_from_scores, 24 | get_objval, 25 | get_L0_penalty, 26 | is_feasible): 27 | """ 28 | 29 | Returns 30 | ------- 31 | cuts 32 | solution pool 33 | bounds 34 | 35 | """ 36 | #todo: recompute function handles here if required 37 | assert callable(compute_loss_real) 38 | assert callable(compute_loss_cut_real) 39 | assert callable(compute_loss_from_scores_real) 40 | assert callable(compute_loss_from_scores) 41 | assert callable(get_objval) 42 | assert callable(get_L0_penalty) 43 | assert callable(is_feasible) 44 | 45 | print_log('-' * 60) 46 | print_log('runnning initialization procedure') 47 | print_log('-' * 60) 48 | 49 | # trade-off parameter 50 | _, C_0, L0_reg_ind, C_0_nnz = setup_penalty_parameters(c0_value = c0_value, coef_set = constraints['coef_set']) 51 | 52 | settings = validate_settings(settings, default_settings = DEFAULT_INITIALIZATION_SETTINGS) 53 | settings['type'] = 'cvx' 54 | 55 | # create RiskSLIM LP 56 | risk_slim_settings = dict(risk_slim_settings) 57 | risk_slim_settings.update(bounds) 58 | risk_slim_settings['relax_integer_variables'] = True 59 | risk_slim_lp, risk_slim_lp_indices = create_risk_slim(coef_set = constraints['coef_set'], input = risk_slim_settings) 60 | risk_slim_lp = set_cplex_mip_parameters(risk_slim_lp, cplex_settings, display_cplex_progress = settings['display_cplex_progress']) 61 | 62 | # solve risk_slim_lp LP using standard CPA 63 | cpa_stats, cuts, cpa_pool = run_standard_cpa(cpx = risk_slim_lp, 64 | cpx_indices = risk_slim_lp_indices, 65 | compute_loss = compute_loss_real, 66 | compute_loss_cut = compute_loss_cut_real, 67 | settings = settings) 68 | 69 | # update bounds 70 | bounds = chained_updates(bounds, C_0_nnz, new_objval_at_relaxation = cpa_stats['lowerbound']) 71 | print_log('CPA produced %d cuts' % len(cuts)) 72 | 73 | def rounded_model_size_is_ok(rho): 74 | zero_idx_rho_ceil = np.equal(np.ceil(rho), 0) 75 | zero_idx_rho_floor = np.equal(np.floor(rho), 0) 76 | cannot_round_to_zero = np.logical_not(np.logical_or(zero_idx_rho_ceil, zero_idx_rho_floor)) 77 | rounded_rho_L0_min = np.count_nonzero(cannot_round_to_zero[L0_reg_ind]) 78 | rounded_rho_L0_max = np.count_nonzero(rho[L0_reg_ind]) 79 | return rounded_rho_L0_min >= constraints['L0_min'] and rounded_rho_L0_max <= constraints['L0_max'] 80 | 81 | cpa_pool = cpa_pool.remove_infeasible(rounded_model_size_is_ok).distinct().sort() 82 | 83 | if len(cpa_pool) == 0: 84 | print_log('all CPA solutions are infeasible') 85 | 86 | pool = SolutionPool(cpa_pool.P) 87 | 88 | # round CPA solutions 89 | if settings['use_rounding'] and len(cpa_pool) > 0: 90 | print_log('running naive rounding on %d solutions' % len(cpa_pool)) 91 | print_log('best objective value: %1.4f' % np.min(cpa_pool.objvals)) 92 | rnd_pool, _, _ = round_solution_pool(cpa_pool, 93 | constraints, 94 | max_runtime = settings['rounding_max_runtime'], 95 | max_solutions = settings['rounding_max_solutions']) 96 | 97 | rnd_pool = rnd_pool.compute_objvals(get_objval).remove_infeasible(is_feasible) 98 | print_log('rounding produced %d integer solutions' % len(rnd_pool)) 99 | if len(rnd_pool) > 0: 100 | pool.append(rnd_pool) 101 | print_log('best objective value is %1.4f' % np.min(rnd_pool.objvals)) 102 | 103 | # sequentially round CPA solutions 104 | if settings['use_sequential_rounding'] and len(cpa_pool) > 0: 105 | print_log('running sequential rounding on %d solutions' % len(cpa_pool)) 106 | print_log('best objective value: %1.4f' % np.min(cpa_pool.objvals)) 107 | sqrnd_pool, _, _ = sequential_round_solution_pool(pool = cpa_pool, 108 | Z = Z, 109 | C_0 = C_0, 110 | compute_loss_from_scores_real = compute_loss_from_scores_real, 111 | get_L0_penalty = get_L0_penalty, 112 | max_runtime = settings['sequential_rounding_max_runtime'], 113 | max_solutions = settings['sequential_rounding_max_solutions'], 114 | objval_cutoff = bounds['objval_max']) 115 | 116 | sqrnd_pool = sqrnd_pool.remove_infeasible(is_feasible) 117 | print_log('sequential rounding produced %d integer solutions' % len(sqrnd_pool)) 118 | if len(sqrnd_pool) > 0: 119 | pool = pool.append(sqrnd_pool) 120 | print_log('best objective value: %1.4f' % np.min(pool.objvals)) 121 | 122 | # polish rounded solutions 123 | if settings['polishing_after'] and len(pool) > 0: 124 | print_log('polishing %d solutions' % len(pool)) 125 | print_log('best objective value: %1.4f' % np.min(pool.objvals)) 126 | dcd_pool, _, _ = discrete_descent_solution_pool(pool = pool, 127 | Z = Z, 128 | C_0 = C_0, 129 | constraints = constraints, 130 | compute_loss_from_scores = compute_loss_from_scores, 131 | get_L0_penalty = get_L0_penalty, 132 | max_runtime = settings['polishing_max_runtime'], 133 | max_solutions = settings['polishing_max_solutions']) 134 | 135 | dcd_pool = dcd_pool.remove_infeasible(is_feasible) 136 | if len(dcd_pool) > 0: 137 | print_log('polishing produced %d integer solutions' % len(dcd_pool)) 138 | pool.append(dcd_pool) 139 | 140 | # remove solutions that are not feasible, not integer 141 | if len(pool) > 0: 142 | pool = pool.remove_nonintegral().distinct().sort() 143 | 144 | # update upper and lower bounds 145 | print_log('initialization produced %1.0f feasible solutions' % len(pool)) 146 | if len(pool) > 0: 147 | bounds = chained_updates(bounds, C_0_nnz, new_objval_at_feasible = np.min(pool.objvals)) 148 | print_log('best objective value: %1.4f' % np.min(pool.objvals)) 149 | 150 | print_log('-' * 60) 151 | print_log('completed initialization procedure') 152 | print_log('-' * 60) 153 | return pool, cuts, bounds 154 | 155 | 156 | 157 | def run_standard_cpa(cpx, 158 | cpx_indices, 159 | compute_loss, 160 | compute_loss_cut, 161 | settings = DEFAULT_CPA_SETTINGS, 162 | print_flag = False): 163 | 164 | assert isinstance(cpx, Cplex) 165 | assert isinstance(cpx_indices, dict) 166 | assert callable(compute_loss) 167 | assert callable(compute_loss_cut) 168 | assert isinstance(settings, dict) 169 | 170 | settings = validate_settings(settings, default_settings = DEFAULT_CPA_SETTINGS) 171 | 172 | rho_idx = cpx_indices["rho"] 173 | loss_idx = cpx_indices["loss"] 174 | alpha_idx = cpx_indices["alpha"] 175 | cut_idx = loss_idx + rho_idx 176 | objval_idx = cpx_indices["objval"] 177 | L0_idx = cpx_indices["L0_norm"] 178 | 179 | P = len(cpx_indices["rho"]) 180 | C_0_alpha = np.array(cpx_indices['C_0_alpha']) 181 | C_0_nnz = C_0_alpha[np.flatnonzero(C_0_alpha)] 182 | 183 | if isinstance(loss_idx, list) and len(loss_idx) == 1: 184 | loss_idx = loss_idx[0] 185 | 186 | if len(alpha_idx) > 0: 187 | get_alpha = lambda: np.array(cpx.solution.get_values(alpha_idx)) 188 | else: 189 | get_alpha = lambda: np.array([]) 190 | 191 | bounds = { 192 | 'loss_min': cpx.variables.get_lower_bounds(loss_idx), 193 | 'loss_max': cpx.variables.get_upper_bounds(loss_idx), 194 | 'objval_min': cpx.variables.get_lower_bounds(objval_idx), 195 | 'objval_max': cpx.variables.get_upper_bounds(objval_idx), 196 | 'L0_min': cpx.variables.get_lower_bounds(L0_idx), 197 | 'L0_max': cpx.variables.get_upper_bounds(L0_idx), 198 | } 199 | 200 | if settings['update_bounds'] and settings['type'] == 'cvx': 201 | update_bounds = lambda bounds, lb, ub: chained_updates_for_lp(bounds, C_0_nnz, ub, lb) 202 | elif settings['update_bounds'] and settings['type'] == 'ntree': 203 | update_bounds = lambda bounds, lb, ub: chained_updates(bounds, C_0_nnz, ub, lb) 204 | else: 205 | update_bounds = lambda bounds, lb, ub: bounds 206 | 207 | objval = 0.0 208 | upperbound = CPX_INFINITY 209 | lowerbound = 0.0 210 | n_iterations = 0 211 | n_simplex_iterations = 0 212 | max_runtime = float(settings['max_runtime']) 213 | max_cplex_time = float(settings['max_runtime_per_iteration']) 214 | remaining_total_time = max_runtime 215 | solutions = [] 216 | objvals = [] 217 | 218 | progress_stats = { 219 | 'upperbounds': [], 220 | 'lowerbounds': [], 221 | 'simplex_iterations': [], 222 | 'cut_times': [], 223 | 'total_times': [] 224 | } 225 | 226 | run_start_time = time.time() 227 | while True: 228 | 229 | iteration_start_time = time.time() 230 | cpx.parameters.timelimit.set(min(remaining_total_time, max_cplex_time)) 231 | cpx.solve() 232 | solution_status = cpx.solution.status[cpx.solution.get_status()] 233 | 234 | # get solution 235 | if solution_status not in ('optimal', 'optimal_tolerance', 'MIP_optimal'): 236 | stop_reason = solution_status 237 | stop_msg = 'stopping CPA | solution is infeasible (status = %s)' % solution_status 238 | break 239 | 240 | # get solution 241 | rho = np.array(cpx.solution.get_values(rho_idx)) 242 | alpha = get_alpha() 243 | simplex_iterations = int(cpx.solution.progress.get_num_iterations()) 244 | 245 | # compute cut 246 | cut_start_time = time.time() 247 | loss_value, loss_slope = compute_loss_cut(rho) 248 | cut_lhs = [float(loss_value - loss_slope.dot(rho))] 249 | cut_constraint = [SparsePair(ind = cut_idx, val = [1.0] + (-loss_slope).tolist())] 250 | cut_time = time.time() - cut_start_time 251 | 252 | # compute objective bounds 253 | objval = float(loss_value + alpha.dot(C_0_alpha)) 254 | upperbound = min(upperbound, objval) 255 | lowerbound = cpx.solution.get_objective_value() 256 | relative_gap = (upperbound - lowerbound)/(upperbound + np.finfo('float').eps) 257 | bounds = update_bounds(bounds, lb = lowerbound, ub = upperbound) 258 | 259 | #store solutions 260 | solutions.append(rho) 261 | objvals.append(objval) 262 | 263 | # update run stats 264 | n_iterations += 1 265 | n_simplex_iterations += simplex_iterations 266 | current_time = time.time() 267 | total_time = current_time - run_start_time 268 | iteration_time = current_time - iteration_start_time 269 | remaining_total_time = max(max_runtime - total_time, 0.0) 270 | 271 | # print progress 272 | if print_flag and settings['display_progress']: 273 | print_log("cuts = %d \t UB = %.4f \t LB = %.4f \t GAP = %.4f%%\n" % (n_iterations, upperbound, lowerbound, 100.0 * relative_gap)) 274 | 275 | # save progress 276 | if settings['save_progress']: 277 | progress_stats['upperbounds'].append(upperbound) 278 | progress_stats['lowerbounds'].append(lowerbound) 279 | progress_stats['total_times'].append(total_time) 280 | progress_stats['cut_times'].append(cut_time) 281 | progress_stats['simplex_iterations'].append(simplex_iterations) 282 | 283 | # check termination conditions 284 | if n_iterations >= settings['max_iterations']: 285 | stop_reason = 'aborted:reached_max_cuts' 286 | stop_msg = 'reached max iterations' 287 | break 288 | 289 | if n_iterations >= settings['min_iterations_before_coefficient_gap_check']: 290 | prior_rho = solutions[-2] 291 | coef_gap = np.abs(np.max(rho - prior_rho)) 292 | if np.all(np.round(rho) == np.round(prior_rho)) and coef_gap < settings['max_coefficient_gap']: 293 | stop_reason = 'aborted:coefficient_gap_within_tolerance' 294 | stop_msg = 'stopping CPA | coef gap is within tolerance (%1.4f < %1.4f)' % (coef_gap, settings['max_coefficient_gap']) 295 | break 296 | 297 | if relative_gap < settings['max_tolerance']: 298 | stop_reason = 'converged:gap_within_tolerance' 299 | stop_msg = 'stopping CPA | optimality gap is within tolerance (%1.1f%% < %1.1f%%)' % (100 * settings['max_tolerance'], 100 * relative_gap) 300 | break 301 | 302 | if iteration_time > settings['max_runtime_per_iteration']: 303 | stop_reason = 'aborted:reached_max_train_time' 304 | stop_msg = 'stopping CPA (reached max training time per iteration of %1.0f secs)' % settings['max_runtime_per_iteration'] 305 | break 306 | 307 | if (total_time > settings['max_runtime']) or (remaining_total_time == 0.0): 308 | stop_reason = 'aborted:reached_max_train_time' 309 | stop_msg = 'stopping CPA (reached max training time of %1.0f secs)' % settings['max_runtime'] 310 | break 311 | 312 | # switch bounds 313 | if settings['update_bounds']: 314 | cpx.variables.set_lower_bounds(L0_idx, bounds['L0_min']) 315 | cpx.variables.set_upper_bounds(L0_idx, bounds['L0_max']) 316 | cpx.variables.set_lower_bounds(loss_idx, bounds['loss_min']) 317 | cpx.variables.set_upper_bounds(loss_idx, bounds['loss_max']) 318 | cpx.variables.set_lower_bounds(objval_idx, bounds['objval_min']) 319 | cpx.variables.set_upper_bounds(objval_idx, bounds['objval_max']) 320 | 321 | # add loss cut 322 | cpx.linear_constraints.add(lin_expr = cut_constraint, senses = ["G"], rhs = cut_lhs) 323 | 324 | if print_flag: 325 | print_log(stop_msg) 326 | 327 | #collect stats 328 | stats = { 329 | 'solution': rho, 330 | 'stop_reason': stop_reason, 331 | 'n_iterations': n_iterations, 332 | 'n_simplex_iterations': n_simplex_iterations, 333 | 'objval': objval, 334 | 'upperbound': upperbound, 335 | 'lowerbound': lowerbound, 336 | 'cut_time': cut_time, 337 | 'total_time': total_time, 338 | 'cplex_time': total_time - cut_time, 339 | } 340 | 341 | stats.update(bounds) 342 | if settings['save_progress']: 343 | progress_stats['cplex_times'] = (np.array(stats['total_times']) - np.array(stats['cut_times'])).tolist() 344 | progress_stats['objvals'] = objvals 345 | progress_stats['solutions'] = solutions 346 | stats.update(progress_stats) 347 | 348 | #collect cuts 349 | idx = list(range(cpx_indices['n_constraints'], cpx.linear_constraints.get_num(), 1)) 350 | cuts = { 351 | 'coefs': cpx.linear_constraints.get_rows(idx), 352 | 'lhs': cpx.linear_constraints.get_rhs(idx) 353 | } 354 | 355 | #create solution pool 356 | pool = SolutionPool(P) 357 | if len(objvals) > 0: 358 | pool.add(objvals, solutions) 359 | 360 | return stats, cuts, pool 361 | 362 | 363 | 364 | def round_solution_pool(pool, 365 | constraints, 366 | max_runtime = float('inf'), 367 | max_solutions = float('inf')): 368 | """ 369 | 370 | Parameters 371 | ---------- 372 | pool 373 | constraints 374 | max_runtime 375 | max_solutions 376 | 377 | Returns 378 | ------- 379 | 380 | """ 381 | # quick return 382 | if len(pool) == 0: 383 | return pool 384 | 385 | pool = pool.distinct().sort() 386 | P = pool.P 387 | L0_reg_ind = np.isnan(constraints['coef_set'].c0) 388 | L0_max = constraints['L0_max'] 389 | 390 | 391 | total_runtime = 0.0 392 | total_rounded = 0 393 | rounded_pool = SolutionPool(P) 394 | 395 | for rho in pool.solutions: 396 | 397 | start_time = time.time() 398 | # sort from largest to smallest coefficients 399 | feature_order = np.argsort([-abs(x) for x in rho]) 400 | rounded_solution = np.zeros(shape = (1, P)) 401 | l0_norm_count = 0 402 | 403 | for k in range(P): 404 | j = feature_order[k] 405 | if not L0_reg_ind[j]: 406 | rounded_solution[0, j] = np.round(rho[j], 0) 407 | elif l0_norm_count < L0_max: 408 | rounded_solution[0, j] = np.round(rho[j], 0) 409 | l0_norm_count += L0_reg_ind[j] 410 | 411 | total_runtime += time.time() - start_time 412 | total_rounded += 1 413 | rounded_pool.add(objvals = np.nan, solutions = rounded_solution) 414 | 415 | if total_runtime > max_runtime or total_rounded >= max_solutions: 416 | break 417 | 418 | rounded_pool = rounded_pool.distinct().sort() 419 | return rounded_pool, total_runtime, total_rounded 420 | 421 | 422 | def sequential_round_solution_pool(pool, 423 | Z, 424 | C_0, 425 | compute_loss_from_scores_real, 426 | get_L0_penalty, 427 | max_runtime = float('inf'), 428 | max_solutions = float('inf'), 429 | objval_cutoff = float('inf')): 430 | 431 | """ 432 | runs sequential rounding for all solutions in a solution pool 433 | can be stopped early using max_runtime or max_solutions 434 | 435 | Parameters 436 | ---------- 437 | pool 438 | Z 439 | C_0 440 | compute_loss_from_scores_real 441 | get_L0_penalty 442 | max_runtime 443 | max_solutions 444 | objval_cutoff 445 | L0_min 446 | L0_max 447 | 448 | Returns 449 | ------- 450 | 451 | """ 452 | # quick return 453 | if len(pool) == 0: 454 | return pool, 0.0, 0 455 | 456 | assert callable(get_L0_penalty) 457 | assert callable(compute_loss_from_scores_real) 458 | 459 | # if model size constraint is non-trivial, remove solutions that violate the model size constraint beforehand 460 | pool = pool.distinct().sort() 461 | rounding_handle = lambda rho: sequential_rounding(rho = rho, 462 | Z = Z, 463 | C_0 = C_0, 464 | compute_loss_from_scores_real = compute_loss_from_scores_real, 465 | get_L0_penalty = get_L0_penalty, 466 | objval_cutoff = objval_cutoff) 467 | 468 | 469 | # apply sequential rounding to all solutions 470 | total_runtime = 0.0 471 | total_rounded = 0 472 | rounded_pool = SolutionPool(pool.P) 473 | 474 | for rho in pool.solutions: 475 | 476 | start_time = time.time() 477 | solution, objval, early_stop = rounding_handle(rho) 478 | total_runtime += time.time() - start_time 479 | total_rounded += 1 480 | 481 | if not early_stop: 482 | rounded_pool = rounded_pool.add(objvals = objval, solutions = solution) 483 | 484 | if total_runtime > max_runtime or total_rounded > max_solutions: 485 | break 486 | 487 | rounded_pool = rounded_pool.distinct().sort() 488 | return rounded_pool, total_runtime, total_rounded 489 | 490 | 491 | def discrete_descent_solution_pool(pool, 492 | Z, 493 | C_0, 494 | constraints, 495 | get_L0_penalty, 496 | compute_loss_from_scores, 497 | max_runtime = float('inf'), 498 | max_solutions = float('inf')): 499 | """ 500 | 501 | runs dcd polishing for all solutions in a solution pool 502 | can be stopped early using max_runtime or max_solutions 503 | 504 | 505 | Parameters 506 | ---------- 507 | pool 508 | Z 509 | C_0 510 | constraints 511 | get_L0_penalty 512 | compute_loss_from_scores 513 | max_runtime 514 | max_solutions 515 | 516 | Returns 517 | ------- 518 | 519 | """ 520 | 521 | pool = pool.remove_nonintegral() 522 | if len(pool) == 0: 523 | return pool, 0.0, 0 524 | 525 | assert callable(get_L0_penalty) 526 | assert callable(compute_loss_from_scores) 527 | 528 | rho_ub = constraints['coef_set'].ub 529 | rho_lb = constraints['coef_set'].lb 530 | 531 | polishing_handle = lambda rho: discrete_descent(rho, 532 | Z = Z, 533 | C_0 = C_0, 534 | rho_ub = rho_ub, 535 | rho_lb = rho_lb, 536 | get_L0_penalty = get_L0_penalty, 537 | compute_loss_from_scores = compute_loss_from_scores) 538 | pool = pool.distinct().sort() 539 | 540 | polished_pool = SolutionPool(pool.P) 541 | total_runtime = 0.0 542 | total_polished = 0 543 | start_time = time.time() 544 | for rho in pool.solutions: 545 | polished_solution, _, polished_objval = polishing_handle(rho) 546 | total_runtime = time.time() - start_time 547 | total_polished += 1 548 | polished_pool = polished_pool.add(objvals = polished_objval, solutions = polished_solution) 549 | if total_runtime > max_runtime or total_polished >= max_solutions: 550 | break 551 | 552 | polished_pool = polished_pool.distinct().sort() 553 | return polished_pool, total_runtime, total_polished 554 | --------------------------------------------------------------------------------