pax_global_header 0000666 0000000 0000000 00000000064 14145540156 0014517 g ustar 00root root 0000000 0000000 52 comment=bf86e699e6929127ccb5876d8c62c70785390eb0
jmschrei-apricot-bf86e69/ 0000775 0000000 0000000 00000000000 14145540156 0015415 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/.github/ 0000775 0000000 0000000 00000000000 14145540156 0016755 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/.github/workflows/ 0000775 0000000 0000000 00000000000 14145540156 0021012 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/.github/workflows/python-package.yml 0000664 0000000 0000000 00000002765 14145540156 0024461 0 ustar 00root root 0000000 0000000 # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
python-version: ['3.7', '3.8']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.os }} ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
python -m pip install wheel
python -m pip install numpy>=1.20.0
pip install -r dev-requirements.txt
python setup.py install
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: python setup.py test
jmschrei-apricot-bf86e69/.github/workflows/wheels.yml 0000664 0000000 0000000 00000002314 14145540156 0023024 0 ustar 00root root 0000000 0000000
# This is a basic workflow to help you get started with Actions
name: Wheels
on: workflow_dispatch
jobs:
build_wheels:
name: Build wheels for ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
name: Install Python
with:
python-version: '3.8'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install wheel
python -m pip install numpy>=1.20.0
pip install -r dev-requirements.txt
python setup.py install
- name: Install cibuildwheel
run: python -m pip install cibuildwheel==1.7.2
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_SKIP: "cp27-* cp35-* cp36-* cp39-* pp*" # skip Python 2.7 wheels
CIBW_BEFORE_BUILD: "rm -rf build/"
- name: Upload wheels
run: |
python -m pip install twine
python -m twine upload -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} ./wheelhouse/*.whl
jmschrei-apricot-bf86e69/.gitignore 0000664 0000000 0000000 00000000111 14145540156 0017376 0 ustar 00root root 0000000 0000000 *.pyc
.ipynb_checkpoints
dist/
build/
.eggs/
*.egg-info/
.python-version
jmschrei-apricot-bf86e69/.travis.yml 0000664 0000000 0000000 00000002471 14145540156 0017532 0 ustar 00root root 0000000 0000000 language: python
matrix:
include:
- os: osx
language: generic
env: PYTHON=3.6
- os: osx
language: generic
env: PYTHON=3.7
- os: osx
language: generic
env: PYTHON=3.8
- os: linux
language: generic
env: PYTHON=3.6
- os: linux
language: generic
env: PYTHON=3.7
- os: linux
language: generic
env: PYTHON=3.8
before_install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then if [[ "$PYTHON" == "2.7" ]]; then wget
http://repo.continuum.io/miniconda/Miniconda2-latest-MacOSX-x86_64.sh -O miniconda.sh;
else wget http://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh
-O miniconda.sh; fi else if [[ "$PYTHON" == "2.7" ]]; then wget http://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh
-O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
-O miniconda.sh; fi fi
- bash miniconda.sh -b -p $HOME/miniconda
- export PATH="$HOME/miniconda/bin:$PATH"
- conda config --set always_yes yes --set changeps1 no
- conda update -q conda
- conda info -a
- conda create -q -n test-environment python=$PYTHON numpy scipy
- source activate test-environment
- conda install --channel=numba llvmlite
- pip install -r dev-requirements.txt
install:
- python setup.py install
script:
- nosetests -s -v --logging-filter=-numba
jmschrei-apricot-bf86e69/LICENSE 0000664 0000000 0000000 00000002074 14145540156 0016425 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2018- Jacob Schreiber
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
jmschrei-apricot-bf86e69/README.md 0000664 0000000 0000000 00000023543 14145540156 0016703 0 ustar 00root root 0000000 0000000
[](https://pepy.tech/project/apricot-select) [](https://apricot-select.readthedocs.io/en/latest/?badge=latest)
Please consider citing the [manuscript](https://jmlr.org/papers/volume21/19-467/19-467.pdf) if you use apricot in your academic work!
You can find more thorough documentation [here](https://apricot-select.readthedocs.io/en/latest/index.html).
apricot implements submodular optimization for the purpose of summarizing massive data sets into minimally redundant subsets that are still representative of the original data. These subsets are useful for both visualizing the modalities in the data (such as in the two data sets below) and for training accurate machine learning models with just a fraction of the examples and compute.

Submodular optimization is a well studied field that focuses on set functions which have the *diminishing return* property. These set functions have the dimishing return property, such that adding an item to a set produces a smaller marginal gain than adding the same item to a superset of that set. More formally, for
, the property is
. When these functions represent a notion of diversity, finding the subset of examples that maximize these functions corresponds to finding a minimally redundant set.
There are many built-in submodular functions and optimizers in apricot. These include:
**Functions**
* [Feature-based](https://apricot-select.readthedocs.io/en/latest/functions/featureBased.html)
* [Max (Set) Coverage](https://apricot-select.readthedocs.io/en/latest/functions/maxCoverage.html)
* [Facility Location](https://apricot-select.readthedocs.io/en/latest/functions/facilityLocation.html)
* [Graph Cut](https://apricot-select.readthedocs.io/en/latest/functions/graphCut.html)
* [Sum Redundancy](https://apricot-select.readthedocs.io/en/latest/functions/sumRedundancy.html)
* [Saturated Coverage](https://apricot-select.readthedocs.io/en/latest/functions/saturatedCoverage.html)
* [Mixtures of Functions](https://apricot-select.readthedocs.io/en/latest/functions/mixture.html)
**Optimizers**
* [Naive Greedy](https://apricot-select.readthedocs.io/en/latest/optimizers/naive.html)
* [Lazy Greedy](https://apricot-select.readthedocs.io/en/latest/optimizers/lazy.html)
* [Two-Stage Greedy](https://apricot-select.readthedocs.io/en/latest/optimizers/two-stage.html)
* [Approximate Lazy Greedy](https://apricot-select.readthedocs.io/en/latest/optimizers/approx-lazy.html)
* [Stochastic Greedy](https://apricot-select.readthedocs.io/en/latest/optimizers/stochastic.html)
* [Sample Greedy](https://apricot-select.readthedocs.io/en/latest/optimizers/sample.html)
* [GreeDi](https://apricot-select.readthedocs.io/en/latest/optimizers/greedi.html)
* [Modular Approximation](https://apricot-select.readthedocs.io/en/latest/optimizers/modular.html)
* [Bidirectional Greedy](https://apricot-select.readthedocs.io/en/latest/optimizers/bidirectional.html)
### Installation
apricot can be installed easily from PyPI with `pip install apricot-select`
### Usage
The main objects in apricot are the selectors. Each selector encapsulates a submodular function and the cached statistics that speed up the optimization process. The API of the selectors follows the style of a scikit-learn transformer and consists of a `fit` method, where the examples are selected, a `transform` method, where the subset is selected from the data set, and a `fit_transform` method, where both are done sequentially.
Here is an example of reducing a data set of size 5000 down to 100 examples with a facility location function that uses negative Euclidean distance as a similarity measure.
```python
import numpy
from apricot import FacilityLocationSelection
X = numpy.random.normal(100, 1, size=(5000, 25))
X_subset = FacilityLocationSelection(100, metric='euclidean', optimizer='lazy').fit_transform(X)
```
#### Facility location functions are general purpose
Facility location functions are more general purpose than feature based functions and work whenever a similarity measure can be defined over pairs of examples. When these functions are maximized, elements are selected that represent elements that are currently underrepresented. However, a downside of facility location functions (and all other submodular functions that rely on a similarity matrix) when compared to feature based functions is that the full similarity matrix requires quadratic memory with the number of examples and is generally impractical to store.
Here is an example of applying facility location selection to the MNIST data set.
```python
from apricot import FacilityLocationSelection
from sklearn.datasets import load_digits
data = load_digits()
X_train = data.data[:1250]
selector = FacilityLocationSelection(n_samples, metric='euclidean', optimizer='lazy', verbose=False)
selector.fit(X_train)
```
And here is the performance of logistic regression models trained on subsets of either the MNIST or Fashion MNIST data sets where the subsets are selected using either facility location (orange) or random selection (grey).

The selected examples from facility location selection can be used in several ways other than training machine learning models, such as for visualizing the modalities of data (see the example at the start) or as centroids in a greedy version of k-means clustering. The animation below shows samples being selected according to facility location and compared to random selection, with facility location first selecting a sample that represents the entire data set, then selecting a sample that is in the largest cluster but near the second largest, and then the centers of local neighborhoods. This selection process is much faster than finding centroids using the EM based approaches of K-means or GMMs.

#### Feature based functions scale to summarize massive data sets
Feature-based functions work well when the features correspond to some notion of quantity or importance, e.g. when the features are number of times a word appears in a document, or the strength of a signal at a sensor. When these functions are maximized, the resulting subsets are comprised of examples that are enriched in value in different sets of features. The intuition behind using a feature-based function is that different modalities in the data (like classes or clusters) will exhibit high values in different sets of features, and so selecting examples enriched for different features means covering these modalities better.
When using a feature-based function on the 20 newsgroups data set, one can train a logistic regression model using only 100 samples and get the same performance as using all 1,187 potential samples, much better than using random sampling. The code below shows an example snippet of code usage and the graph shows the results over many subset sizes.
```python
from apricot import FeatureBasedSelection
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
train_data = fetch_20newsgroups(subset='train', categories=('sci.med', 'sci.space'))
vectorizer = TfidfVectorizer(stop_words='english', max_features=1000)
X_train = vectorizer.fit_transform(train_data.data) # This returns a sparse matrix which is supported in apricot
selector = FeatureBasedSelection(100, concave_func='sqrt', optimizer='two-stage', verbose=False)
selector.fit(X_train)
```

#### Initial subsets
The selection process for most optimizers implemented in apricot is greedy, meaning that one example is selected at a time and this is (usually) the best example to include next given those that have already been selected. While a greedy algorithm is not guaranteed to find the best subset of a given size, it was famously shown that this subset cannot have an objective value $1 - e^{-1}$ worse than the optimal subset, and in practice the subsets are usually near-optimal.
apricot exploits the greedy aspect of the algorithms to allow users to define an initial subset to build off of. This can be useful when one already has some elements selected (such as by outside information) and one would like to select elements that are not redundant with these already-selected elements. The initial subsets can be defined in the constructor like so:
```python
import numpy
from apricot import FacilityLocationSelection
X = numpy.random.normal(20, 1, size=(5000, 25))
X_reordered = FacilityLocationSelection(100, initial_subset=[1, 5, 6, 8, 10]).fit_transform(X)
model = FacilityLocationSelection(1000).fit(X)
X_reordered2 = X[model.ranking]
```
#### When should I use submodular selection?
If the amount of data that you have right now is not burdensome to deal with then it may not be helpful to use submodular selection. However, if training even simple models using the amount of data you have is difficult, you might consider summarizing your data set using apricot. If you're currently running many random selections because of the amount of data you have you may find that a single run of apricot will yield a better subset.
jmschrei-apricot-bf86e69/apricot/ 0000775 0000000 0000000 00000000000 14145540156 0017056 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/apricot/__init__.py 0000664 0000000 0000000 00000001074 14145540156 0021171 0 ustar 00root root 0000000 0000000 # __init__.py
# Author: Jacob Schreiber
from .functions import FeatureBasedSelection
from .functions import MaxCoverageSelection
from .functions import FacilityLocationSelection
from .functions import SaturatedCoverageSelection
from .functions import SumRedundancySelection
from .functions import GraphCutSelection
from .functions import MixtureSelection
from .functions import BaseSelection
from .functions import BaseGraphSelection
from .functions import CustomSelection
from .functions import CustomGraphSelection
__version__ = '0.6.1'
jmschrei-apricot-bf86e69/apricot/functions/ 0000775 0000000 0000000 00000000000 14145540156 0021066 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/apricot/functions/__init__.py 0000664 0000000 0000000 00000001050 14145540156 0023173 0 ustar 00root root 0000000 0000000 # __init__.py
# Author: Jacob Schreiber
from .featureBased import FeatureBasedSelection
from .maxCoverage import MaxCoverageSelection
from .facilityLocation import FacilityLocationSelection
from .saturatedCoverage import SaturatedCoverageSelection
from .sumRedundancy import SumRedundancySelection
from .graphCut import GraphCutSelection
from .mixture import MixtureSelection
from .custom import CustomSelection
from .custom import CustomGraphSelection
from .base import BaseSelection
from .base import BaseGraphSelection jmschrei-apricot-bf86e69/apricot/functions/base.py 0000664 0000000 0000000 00000057750 14145540156 0022370 0 ustar 00root root 0000000 0000000 # base.py
# Author: Jacob Schreiber
"""
This file contains code that implements the core of the submodular selection
algorithms.
"""
import numpy
from tqdm import tqdm
from ..optimizers import BaseOptimizer
from ..optimizers import NaiveGreedy
from ..optimizers import LazyGreedy
from ..optimizers import ApproximateLazyGreedy
from ..optimizers import TwoStageGreedy
from ..optimizers import StochasticGreedy
from ..optimizers import BidirectionalGreedy
from ..optimizers import GreeDi
from ..optimizers import SieveGreedy
from ..optimizers import OPTIMIZERS
from ..utils import PriorityQueue
from ..utils import check_random_state
from ..utils import _calculate_pairwise_distances
from scipy.sparse import csr_matrix
class BaseSelection(object):
"""The base selection object.
This object defines the structures that all submodular selection algorithms
should follow. All algorithms will have the same public methods and the
same attributes.
Parameters
----------
n_samples : int
The number of samples to return.
initial_subset : list, numpy.ndarray or None, optional
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'naive'.
optimizer_kwds : dict or None
A dictionary of arguments to pass into the optimizer object. The keys
of this dictionary should be the names of the parameters in the optimizer
and the values in the dictionary should be the values that these
parameters take. Default is None.
reservoir : numpy.ndarray or None
The reservoir to use when calculating gains in the sieve greedy
streaming optimization algorithm in the `partial_fit` method.
Currently only used for graph-based functions. If a numpy array
is passed in, it will be used as the reservoir. If None is passed in,
will use reservoir sampling to collect a reservoir. Default is None.
max_reservoir_size : int
The maximum size that the reservoir can take. If a reservoir is passed
in, this value is set to the size of that array. Default is 1000.
n_jobs : int
The number of threads to use when performing computation in parallel.
Currently, this parameter is exposed but does not actually do anything.
This will be fixed soon.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
ranking : numpy.array int
The selected samples in the order of their gain with the first number in
the ranking corresponding to the index of the first sample that was
selected by the greedy procedure.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples, initial_subset=None, optimizer='lazy',
optimizer_kwds={}, reservoir=None, max_reservoir_size=1000,
n_jobs=1, random_state=None, verbose=False):
if n_samples <= 0:
raise ValueError("n_samples must be a positive value.")
if not isinstance(initial_subset, (list, numpy.ndarray)) and initial_subset is not None:
raise ValueError("initial_subset must be a list, numpy array, or None")
if isinstance(initial_subset, (list, numpy.ndarray)):
initial_subset = numpy.array(initial_subset)
if not isinstance(optimizer, BaseOptimizer):
if optimizer not in OPTIMIZERS.keys():
raise ValueError("Optimizer must be an optimizer object or " \
"a str in {}.".format(str(OPTIMIZERS.keys())))
if isinstance(optimizer, BaseOptimizer):
optimizer.function = self
if verbose not in (True, False):
raise ValueError("verbosity must be True or False")
self.n_samples = n_samples
self.metric = 'ignore'
self.random_state = check_random_state(random_state)
self.optimizer = optimizer
self.optimizer_kwds = optimizer_kwds
self.n_jobs = n_jobs
self.verbose = verbose
self.initial_subset = initial_subset
self.ranking = None
self.idxs = None
self.gains = None
self.subset = None
self.sparse = None
self._X = None
self.sieve_current_values_ = None
self.n_seen_ = 0
self.reservoir_size = 0 if reservoir is None else reservoir.shape[0]
self.reservoir = reservoir
self.max_reservoir_size = max_reservoir_size if reservoir is None else reservoir.shape[0]
self.update_reservoir_ = reservoir is None
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select a subset of examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : BaseGraphSelection
The fit step returns this selector object.
"""
allowed_dtypes = list, numpy.ndarray, csr_matrix
if not isinstance(X, allowed_dtypes):
raise ValueError("X must be either a list of lists, a 2D numpy " \
"array, or a scipy.sparse.csr_matrix.")
if isinstance(X, numpy.ndarray) and len(X.shape) != 2:
raise ValueError("X must have exactly two dimensions.")
if numpy.min(X) < 0.0 and numpy.max(X) > 0.:
raise ValueError("X cannot contain negative values or must be entirely "\
"negative values.")
if self.n_samples > X.shape[0]:
raise ValueError("Cannot select more examples than the number in" \
" the data set.")
if not self.sparse:
if X.dtype != 'float64':
X = X.astype('float64')
if isinstance(self.optimizer, str):
optimizer = OPTIMIZERS[self.optimizer](function=self,
verbose=self.verbose, random_state=self.random_state,
**self.optimizer_kwds)
else:
optimizer = self.optimizer
self._X = X if self._X is None else self._X
self._initialize(X)
if self.verbose:
self.pbar = tqdm(total=self.n_samples, unit_scale=True)
optimizer.select(X, self.n_samples, sample_cost=sample_cost)
if self.verbose == True:
self.pbar.close()
self.ranking = numpy.array(self.ranking)
self.gains = numpy.array(self.gains)
return self
def partial_fit(self, X, y=None, sample_weight=None, sample_cost=None):
allowed_dtypes = list, numpy.ndarray, csr_matrix
if not isinstance(X, allowed_dtypes):
raise ValueError("X must be either a list of lists, a 2D numpy " \
"array, or a scipy.sparse.csr_matrix.")
if isinstance(X, numpy.ndarray) and len(X.shape) != 2:
raise ValueError("X must have exactly two dimensions.")
if not self.sparse:
if X.dtype != 'float64':
X = X.astype('float64')
if not isinstance(self.optimizer, SieveGreedy):
self.optimizer = OPTIMIZERS['sieve'](function=self,
verbose=self.verbose, random_state=self.random_state,
**self.optimizer_kwds)
self._X = X if self._X is None else self._X
self._initialize(X)
if self.verbose:
self.pbar = tqdm(total=self.n_samples, unit_scale=True)
self.optimizer.select(X, self.n_samples, sample_cost=sample_cost)
if self.verbose == True:
self.pbar.close()
self.ranking = numpy.array(self.ranking)
self.gains = numpy.array(self.gains)
self._X = None
return self
def transform(self, X, y=None, sample_weight=None):
"""Transform a data set to include only the selected examples.
This method will return a selection of X and optionally selections
of y and sample_weight. The default setting is to select items based
on the ranking determined in the `fit` step with examples in the same
order as that ranking. Optionally, the whole data set can be returned,
with the weights corresponding to samples that were not selected set
to 0. This setting can be controlled by setting `pipeline=True`.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and the corresponding labels for the rows that have
been selected. Default is None.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The sample weights to transform. If passed in this function will
return the selected labels (y) and the selected samples, even
if no labels were passed in. Default is None.
Returns
-------
X_subset : numpy.ndarray, shape=(n_samples, d)
A subset of the data such that n_samples < n and n_samples is the
integer provided at initialization.
y_subset : numpy.ndarray, shape=(n_samples,), optional
The labels that match with the indices of the samples if y is
passed in. Only returned if passed in.
sample_weight_subset : numpy.ndarray, shape=(n_samples,), optional
The weight of each example.
"""
r = self.ranking
if sample_weight is not None:
if y is None:
return X[r], None, sample_weight[r]
else:
return X[r], y[r], sample_weight[r]
else:
if y is None:
return X[r]
else:
return X[r], y[r]
def fit_transform(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run optimization and select a subset of examples.
This method will first perform the `fit` step and then perform the
`transform` step, returning a transformed data set.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and the corresponding labels for the rows that have
been selected. Default is None.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The sample weights to transform. If passed in this function will
return the selected labels (y) and the selected samples, even
if no labels were passed in. Default is None.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
X_subset : numpy.ndarray, shape=(n_samples, d)
A subset of the data such that n_samples < n and n_samples is the
integer provided at initialization.
y_subset : numpy.ndarray, shape=(n_samples,), optional
The labels that match with the indices of the samples if y is
passed in. Only returned if passed in.
sample_weight_subset : numpy.ndarray, shape=(n_samples,), optional
The weight of each example.
"""
return self.fit(X, y=y, sample_weight=sample_weight,
sample_cost=sample_cost).transform(X, y=y,
sample_weight=sample_weight)
def _initialize(self, X, idxs=None):
n, d = X.shape
self._X = X if self._X is None else self._X
self.sparse = isinstance(X, csr_matrix)
self.ranking = []
self.gains = []
self.subset = numpy.zeros((0, self._X.shape[1]), dtype='float64')
self.current_values = numpy.zeros(d, dtype='float64')
self.current_concave_values = numpy.zeros(d, dtype='float64')
self.mask = numpy.zeros(n, dtype='int8')
if self.initial_subset is not None:
if self.initial_subset.ndim == 1:
if self.initial_subset.dtype == bool:
self.initial_subset = numpy.where(self.initial_subset == 1)[0]
if len(self.initial_subset) + self.n_samples > X.shape[0]:
raise ValueError("When using a mask for the initial subset" \
" must selected fewer than the size of the subset minus" \
" the initial subset size, i.e., n_samples < X.shape[0] -"\
" initial_subset.shape[0].")
if self.initial_subset.max() > X.shape[0]:
raise ValueError("When passing in an integer mask for the initial subset"\
" the maximum value cannot exceed the size of the data set.")
elif self.initial_subset.min() < 0:
raise ValueError("When passing in an integer mask for the initial subset"\
" the minimum value cannot be negative.")
self.mask[self.initial_subset] = 1
self.idxs = numpy.where(self.mask == 0)[0]
def _calculate_gains(self, X, idxs=None):
raise NotImplementedError
def _calculate_sieve_gains(self, X, thresholds, idxs):
n = X.shape[0]
d = X.shape[1] if self.reservoir is None else self.max_reservoir_size
l = len(thresholds)
if self.sieve_current_values_ is None:
self.sieve_current_values_ = numpy.zeros((l, d),
dtype='float64')
self.sieve_selections_ = numpy.zeros((l, self.n_samples),
dtype='int64') - 1
self.sieve_gains_ = numpy.zeros((l, self.n_samples),
dtype='float64') - 1
self.sieve_n_selected_ = numpy.zeros(l,
dtype='int64')
self.sieve_total_gains_ = numpy.zeros(l,
dtype='float64')
self.sieve_subsets_ = numpy.zeros((l, self.n_samples,
self._X.shape[1]), dtype='float64')
else:
j = l - self.sieve_current_values_.shape[0]
if j > 0:
self.sieve_current_values_ = numpy.vstack([
self.sieve_current_values_, numpy.zeros((j, d),
dtype='float64')])
self.sieve_selections_ = numpy.vstack([
self.sieve_selections_, numpy.zeros((j, self.n_samples),
dtype='int64') - 1])
self.sieve_gains_ = numpy.vstack([self.sieve_gains_,
numpy.zeros((j, self.n_samples), dtype='float64')])
self.sieve_n_selected_ = numpy.concatenate([
self.sieve_n_selected_, numpy.zeros(j, dtype='int64')])
self.sieve_total_gains_ = numpy.concatenate([
self.sieve_total_gains_, numpy.zeros(j, dtype='float64')])
self.sieve_subsets_ = numpy.concatenate([self.sieve_subsets_,
numpy.zeros((j, self.n_samples, self._X.shape[1]),
dtype='float64')])
def _select_next(self, X, gain, idx):
self.ranking.append(idx)
self.gains.append(gain)
self.mask[idx] = True
self.idxs = numpy.where(self.mask == 0)[0]
if self.sparse:
X = self._X[idx:idx+1].toarray()
else:
X = self._X[idx:idx+1]
if self.metric != 'precomputed':
self.subset = numpy.concatenate([self.subset, X])
class BaseGraphSelection(BaseSelection):
"""The base graph selection object.
This object defines the structures that all submodular selection algorithms
should follow if they operate on a graph, such as pairwise similarity
measurements. All algorithms will have the same public methods and the same
attributes.
NOTE: All ~pairwise~ values in your data must be positive for these
selection methods to work.
This implementation allows users to pass in either their own symmetric
square matrix of similarity values, or a data matrix as normal and a function
that calculates these pairwise values.
Parameters
----------
n_samples : int
The number of samples to return.
metric : str
The method for converting a data matrix into a square symmetric matrix
of pairwise similarities. If a string, can be any of the metrics
implemented in sklearn (see https://scikit-learn.org/stable/modules/
generated/sklearn.metrics.pairwise_distances.html), including
"precomputed" if one has already generated a similarity matrix. Note
that sklearn calculates distance matrices whereas apricot operates on
similarity matrices, and so a distances.max() - distances transformation
is performed on the resulting distances. For backcompatibility,
'corr' will be read as 'correlation'.
initial_subset : list, numpy.ndarray or None
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'naive'.
optimizer_kwds : dict or None
A dictionary of arguments to pass into the optimizer object. The keys
of this dictionary should be the names of the parameters in the optimizer
and the values in the dictionary should be the values that these
parameters take. Default is None.
n_neighbors : int or None
When constructing a similarity matrix, the number of nearest neighbors
whose similarity values will be kept. The result is a sparse similarity
matrix which can significantly speed up computation at the cost of
accuracy. Default is None.
reservoir : numpy.ndarray or None
The reservoir to use when calculating gains in the sieve greedy
streaming optimization algorithm in the `partial_fit` method.
Currently only used for graph-based functions. If a numpy array
is passed in, it will be used as the reservoir. If None is passed in,
will use reservoir sampling to collect a reservoir. Default is None.
max_reservoir_size : int
The maximum size that the reservoir can take. If a reservoir is passed
in, this value is set to the size of that array. Default is 1000.
n_jobs : int
The number of threads to use when performing computation in parallel.
Currently, this parameter is exposed but does not actually do anything.
This will be fixed soon.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
metric : callable
A function that takes in a data matrix and converts it to a square
symmetric matrix.
ranking : numpy.array int
The selected samples in the order of their gain.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples, metric='euclidean',
initial_subset=None, optimizer='two-stage', optimizer_kwds={},
n_neighbors=None, reservoir=None, max_reservoir_size=1000,
n_jobs=1, random_state=None, verbose=False):
super(BaseGraphSelection, self).__init__(n_samples=n_samples,
initial_subset=initial_subset, optimizer=optimizer,
optimizer_kwds=optimizer_kwds, reservoir=reservoir,
max_reservoir_size=max_reservoir_size, n_jobs=n_jobs,
random_state=random_state, verbose=verbose)
self.metric = metric.replace("corr", "correlation")
self.n_neighbors = n_neighbors
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select a subset of examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : BaseGraphSelection
The fit step returns this selector object.
"""
if isinstance(X, csr_matrix) and self.metric not in ("precomputed", "ignore"):
raise ValueError("Must passed in a precomputed sparse " \
"similarity matrix or a dense feature matrix.")
if self.metric == 'precomputed' and X.shape[0] != X.shape[1]:
raise ValueError("Precomputed similarity matrices " \
"must be square and symmetric.")
X_pairwise = _calculate_pairwise_distances(X, metric=self.metric,
n_neighbors=self.n_neighbors)
self._X = X
return super(BaseGraphSelection, self).fit(X_pairwise, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def partial_fit(self, X, y=None, sample_weight=None, sample_cost=None):
if self.reservoir is None:
self.reservoir = numpy.empty((self.max_reservoir_size, X.shape[1]))
if self.update_reservoir_:
for i in range(X.shape[0]):
if self.reservoir_size < self.max_reservoir_size:
self.reservoir[self.reservoir_size] = X[i]
self.reservoir_size += 1
else:
r = self.random_state.choice(self.n_seen_ + i)
if r < self.max_reservoir_size:
self.reservoir[r] = X[i]
#self.current_values_[:, r] = 0.
X_pairwise = _calculate_pairwise_distances(X,
Y=self.reservoir[:self.reservoir_size], metric=self.metric)
self._X = X
super(BaseGraphSelection, self).partial_fit(X_pairwise, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
self.current_values = numpy.zeros(self.reservoir_size,
dtype='float64')
self.n_seen_ += X.shape[0]
def _initialize(self, X_pairwise, idxs=None):
super(BaseGraphSelection, self)._initialize(X_pairwise, idxs=idxs)
def _calculate_gains(self, X_pairwise):
super(BaseGraphSelection, self)._calculate_gains(X_pairwise)
def _calculate_sieve_gains(self, X, thresholds, idxs):
super(BaseGraphSelection, self)._calculate_sieve_gains(X, thresholds,
idxs)
def _select_next(self, X_pairwise, gain, idx):
super(BaseGraphSelection, self)._select_next(X_pairwise, gain, idx)
jmschrei-apricot-bf86e69/apricot/functions/custom.py 0000664 0000000 0000000 00000037203 14145540156 0022757 0 ustar 00root root 0000000 0000000 # custom.py
# Author: Jacob Schreiber
import numpy
from .base import BaseSelection
from .base import BaseGraphSelection
class CustomSelection(BaseSelection):
"""A selector based off a custom, user-defined feature-based function.
This selector wraps a custom function that is passed in by the user. This
function should take in a matrix containing a subset of the ground set and
output the standard measure of quality of the subset. Each row in the matrix
should be an example and each column should be a feature value for the
example (like a standard data matrix for machine learning).
.. warning::
If the function that the user wants to optimize is graph-based,
i.e., that it determines the quality of a subset using similarities
with other examples instead of the feature values directly,
e.g. facility location, CustomGraphSelection should be used instead.
.. note::
Although apricot is built for submodular functions, there is no explicit
restriction that the function passed in be submodular. Sometimes,
supermodular functions can be reasonably maximized using the same
greedy approaches used on submodular functions.
Parameters
----------
n_samples : int
The number of samples to return.
func : function
The feature-based set function to that this selector should wrap.
initial_subset : list, numpy.ndarray or None
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict, optional
Arguments to pass into the optimizer object upon initialization.
Default is {}.
function_kwds : dict, optional
Arguments to pass into the function object for each call.
Default is {}.
n_jobs : int, optional
The number of cores to use for processing. This value is multiplied
by 2 when used to set the number of threads. If set to -1, use all
cores and threads. Default is -1.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
ranking : numpy.array int
The selected samples in the order of their gain with the first number in
the ranking corresponding to the index of the first sample that was
selected by the greedy procedure.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples, function, initial_subset=None,
optimizer='two-stage', optimizer_kwds={}, function_kwds={},
n_jobs=1, random_state=None, verbose=False):
if callable(function) == False:
raise ValueError("Passed in function must be callable.")
self.function = function
self.function_kwds = function_kwds
super(CustomSelection, self).__init__(n_samples=n_samples,
initial_subset=initial_subset, optimizer=optimizer,
optimizer_kwds=optimizer_kwds, n_jobs=n_jobs,
random_state=random_state, verbose=verbose)
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : CustomSelection
The fit step returns this selector object.
"""
return super(CustomSelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X):
super(CustomSelection, self)._initialize(X)
if self.initial_subset is None:
pass
elif self.initial_subset.ndim == 2:
if self.initial_subset.shape[1] != X.shape[1]:
raise ValueError("The number of columns in the initial subset must " \
"match the number of columns in X.")
elif self.initial_subset.ndim == 1:
self.initial_subset = X[self.initial_subset]
else:
raise ValueError("The initial subset must be either a two dimensional" \
" matrix of examples or a one dimensional mask.")
if self.initial_subset is None:
self.total_gain = 0
else:
self.total_gain = self.function(self.initial_subset)
def _calculate_gains(self, X, idxs=None):
"""This function will return the gain that each example would give.
This function will return the gains that each example would give if
added to the selected set. When a matrix of examples is given, a
vector will be returned showing the gain for each example. When
a single element is passed in, it will return a singe value."""
idxs = idxs if idxs is not None else self.idxs
gains = numpy.zeros(idxs.shape[0], dtype='float64')
x0 = numpy.zeros((1, X.shape[1]))
if self.initial_subset is not None:
X_ = numpy.concatenate([self.initial_subset, self.subset, x0])
else:
X_ = numpy.concatenate([self.subset, x0])
for i, idx in enumerate(idxs):
X_[-1] = X[idx]
gains[i] = self.function(X_, **self.function_kwds) - self.total_gain
return gains
def _calculate_sieve_gains(self, X, thresholds, idxs):
"""This function will update the internal statistics from a stream.
This function will update the various internal statistics that are a
part of the sieve algorithm for streaming submodular optimization. This
function does not directly return gains but it updates the values
used by a streaming optimizer.
"""
super(CustomSelection, self)._calculate_sieve_gains(X,
thresholds, idxs)
raise NotImplementedError
def _select_next(self, X, gain, idx):
"""This function will add the given item to the selected set."""
self.total_gain += gain
super(CustomSelection, self)._select_next(
X, gain, idx)
class CustomGraphSelection(BaseGraphSelection):
"""A selector based off a custom, user-defined graph-based function.
This selector wraps a custom graph-based function that is passed in by the user.
This function should take in a matrix containing a subset of the ground set and
output the standard measure of quality of the subset. Each row in the matrix
should be an example and each column should be the similarity to each element
in the ground set, with the number of columns being equal to the size of the
ground set.
.. note::
Although apricot is built for submodular functions, there is no explicit
restriction that the function passed in be submodular. Sometimes,
supermodular functions can be reasonably maximized using the same
greedy approaches used on submodular functions.
Parameters
----------
n_samples : int
The number of samples to return.
func : function
The graph-based set function to that this selector should wrap.
initial_subset : list, numpy.ndarray or None
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict, optional
Arguments to pass into the optimizer object upon initialization.
Default is {}.
function_kwds : dict, optional
Arguments to pass into the function object for each call.
Default is {}.
n_jobs : int, optional
The number of cores to use for processing. This value is multiplied
by 2 when used to set the number of threads. If set to -1, use all
cores and threads. Default is -1.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
ranking : numpy.array int
The selected samples in the order of their gain with the first number in
the ranking corresponding to the index of the first sample that was
selected by the greedy procedure.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples, function, metric='euclidean',
initial_subset=None, optimizer='two-stage', optimizer_kwds={},
function_kwds={}, n_jobs=1, random_state=None, verbose=False):
if callable(function) == False:
raise ValueError("Passed in function must be callable.")
self.function = function
self.function_kwds = function_kwds
super(CustomGraphSelection, self).__init__(n_samples=n_samples,
metric=metric, initial_subset=initial_subset, optimizer=optimizer,
optimizer_kwds=optimizer_kwds, n_jobs=n_jobs,
random_state=random_state, verbose=verbose)
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : CustomSelection
The fit step returns this selector object.
"""
return super(CustomGraphSelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X):
super(CustomGraphSelection, self)._initialize(X)
if self.initial_subset is None:
pass
elif self.initial_subset.ndim == 2:
if self.initial_subset.shape[1] != X.shape[1]:
raise ValueError("The number of columns in the initial subset must " \
"match the number of columns in X.")
elif self.initial_subset.ndim == 1:
self.initial_subset = X[self.initial_subset]
else:
raise ValueError("The initial subset must be either a two dimensional" \
" matrix of examples or a one dimensional mask.")
if self.initial_subset is None:
self.total_gain = 0
else:
self.total_gain = self.func(self.initial_subset)
def _calculate_gains(self, X, idxs=None):
"""This function will return the gain that each example would give.
This function will return the gains that each example would give if
added to the selected set. When a matrix of examples is given, a
vector will be returned showing the gain for each example. When
a single element is passed in, it will return a singe value."""
idxs = idxs if idxs is not None else self.idxs
gains = numpy.zeros(idxs.shape[0], dtype='float64')
x0 = numpy.zeros((1, X.shape[1]))
if self.initial_subset is not None:
X_ = numpy.concatenate([self.initial_subset, self.subset, x0])
else:
X_ = numpy.concatenate([self.subset, x0])
for i, idx in enumerate(idxs):
X_[-1] = X[idx]
gains[i] = self.function(X_, **self.function_kwds) - self.total_gain
return gains
def _calculate_sieve_gains(self, X, thresholds, idxs):
"""This function will update the internal statistics from a stream.
This function will update the various internal statistics that are a
part of the sieve algorithm for streaming submodular optimization. This
function does not directly return gains but it updates the values
used by a streaming optimizer.
"""
super(CustomGraphSelection, self)._calculate_sieve_gains(X,
thresholds, idxs)
raise NotImplementedError
def _select_next(self, X, gain, idx):
"""This function will add the given item to the selected set."""
self.total_gain += gain
super(CustomGraphSelection, self)._select_next(
X, gain, idx)
jmschrei-apricot-bf86e69/apricot/functions/facilityLocation.py 0000664 0000000 0000000 00000032651 14145540156 0024744 0 ustar 00root root 0000000 0000000 # facilityLocation.py
# Author: Jacob Schreiber
import numpy
from .base import BaseGraphSelection
from ..optimizers import LazyGreedy
from ..optimizers import ApproximateLazyGreedy
from ..optimizers import SieveGreedy
from tqdm import tqdm
from numba import njit
from numba import prange
dtypes = 'void(float64[:,:], float64[:], float64[:], int64[:])'
sdtypes = 'void(float64[:], int32[:], int32[:], float64[:], float64[:], int64[:])'
sieve_dtypes = 'void(float64[:,:], int64, float64[:,:], int64[:,:],' \
'float64[:,:], float64[:], float64[:], int64[:], int64[:])'
def calculate_gains(dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_(X, gains, current_values, idxs):
for i in prange(idxs.shape[0]):
idx = idxs[i]
gains[i] = numpy.maximum(X[idx], current_values).sum()
return calculate_gains_
def calculate_gains_sparse(dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sparse_(X_data, X_indices, X_indptr, gains, current_values, idxs):
for i in prange(idxs.shape[0]):
idx = idxs[i]
start = X_indptr[idx]
end = X_indptr[idx+1]
for j in range(start, end):
k = X_indices[j]
gains[i] += max(X_data[j], current_values[k]) - current_values[k]
return calculate_gains_sparse_
def calculate_gains_sieve(dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sieve_(X, k, current_values, selections, gains,
total_gains, max_values, n_selected, idxs):
n, d = X.shape
t = max_values.shape[0]
for j in prange(t):
for i in range(n):
if n_selected[j] == k:
break
idx = idxs[i]
threshold = (max_values[j] / 2. - total_gains[j]) / (k - n_selected[j])
maximum = numpy.maximum(X[i][:d], current_values[j][:d])
gain = maximum.mean() - total_gains[j]
if gain > threshold:
current_values[j][:d] = maximum
total_gains[j] = maximum.mean()
selections[j, n_selected[j]] = idx
gains[j, n_selected[j]] = gain
n_selected[j] += 1
return calculate_gains_sieve_
class FacilityLocationSelection(BaseGraphSelection):
"""A selector based off a facility location submodular function.
Facility location functions are general purpose submodular functions that,
when maximized, choose examples that represent the space of the data well.
The facility location function is based on maximizing the pairwise
similarities between the points in the data set and their nearest chosen
point. The similarity function can be specified by the user but must be
non-negative where a higher value indicates more similar.
.. note::
All ~pairwise~ values in your data must be non-negative for this
selection to work.
In many ways, optimizing a facility location function is simply a greedy
version of k-medoids, where after the first few examples are selected, the
subsequent ones are at the center of clusters. The function, like most
graph-based functions, operates on a pairwise similarity matrix, and
successively chooses examples that are similar to examples whose current
most-similar example is still very dissimilar. Phrased another way,
successively chosen examples are representative of underrepresented
examples.
The general form of a facility location function is
.. math::
f(X, Y) = \\sum\\limits_{y in Y} \\max_{x in X} \\phi(x, y)
where :math:`f` indicates the function, :math:`X` is a subset, :math:`Y`
is the ground set, and :math:`\\phi` is the similarity measure between two
examples. Like most graph-based functons, the facility location function
requires access to the full ground set.
This implementation allows users to pass in either their own symmetric
square matrix of similarity values, or a data matrix as normal and a function
that calculates these pairwise values.
For more details, see https://las.inf.ethz.ch/files/krause12survey.pdf
page 4.
Parameters
----------
n_samples : int
The number of samples to return.
metric : str, optional
The method for converting a data matrix into a square symmetric matrix
of pairwise similarities. If a string, can be any of the metrics
implemented in sklearn (see
https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise_distances.html),
including "precomputed" if one has already generated a similarity
matrix. Note that sklearn calculates distance matrices whereas apricot
operates on similarity matrices, and so a distances.max() - distances
transformation is performed on the resulting distances. For
backcompatibility, 'corr' will be read as 'correlation'. Default is
'euclidean'.
initial_subset : list, numpy.ndarray or None, optional
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional. Default is None.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict or None
A dictionary of arguments to pass into the optimizer object. The keys
of this dictionary should be the names of the parameters in the optimizer
and the values in the dictionary should be the values that these
parameters take. Default is None.
n_neighbors : int or None
When constructing a similarity matrix, the number of nearest neighbors
whose similarity values will be kept. The result is a sparse similarity
matrix which can significantly speed up computation at the cost of
accuracy. Default is None.
reservoir : numpy.ndarray or None
The reservoir to use when calculating gains in the sieve greedy
streaming optimization algorithm in the `partial_fit` method.
Currently only used for graph-based functions. If a numpy array
is passed in, it will be used as the reservoir. If None is passed in,
will use reservoir sampling to collect a reservoir. Default is None.
max_reservoir_size : int
The maximum size that the reservoir can take. If a reservoir is passed
in, this value is set to the size of that array. Default is 1000.
n_jobs : int
The number of threads to use when performing computation in parallel.
Currently, this parameter is exposed but does not actually do anything.
This will be fixed soon.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
ranking : numpy.array int
The selected samples in the order of their gain.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples, metric='euclidean',
initial_subset=None, optimizer='lazy', optimizer_kwds={},
n_neighbors=None, reservoir=None, max_reservoir_size=1000,
n_jobs=1, random_state=None, verbose=False):
super(FacilityLocationSelection, self).__init__(n_samples=n_samples,
metric=metric, initial_subset=initial_subset, optimizer=optimizer,
optimizer_kwds=optimizer_kwds, n_neighbors=n_neighbors,
reservoir=reservoir, max_reservoir_size=max_reservoir_size,
n_jobs=n_jobs, random_state=random_state, verbose=verbose)
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : FacilityLocationSelection
The fit step returns this selector object.
"""
return super(FacilityLocationSelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X_pairwise):
super(FacilityLocationSelection, self)._initialize(X_pairwise)
if self.initial_subset is None:
pass
elif self.initial_subset.ndim == 2:
raise ValueError("When using facility location, the initial subset"\
" must be a one dimensional array of indices.")
elif self.initial_subset.ndim == 1:
if not self.sparse:
for i in self.initial_subset:
self.current_values = numpy.maximum(X_pairwise[i],
self.current_values).astype('float64')
else:
for i in self.initial_subset:
self.current_values = numpy.maximum(
X_pairwise[i].toarray()[0], self.current_values).astype('float64')
else:
raise ValueError("The initial subset must be either a two dimensional" \
" matrix of examples or a one dimensional mask.")
self.current_values_sum = self.current_values.sum()
self.calculate_gains_ = calculate_gains_sparse if self.sparse else calculate_gains
dtypes_ = sdtypes if self.sparse else dtypes
if self.optimizer in (LazyGreedy, ApproximateLazyGreedy):
self.calculate_gains_ = self.calculate_gains_(dtypes_, False, True, False)
elif self.optimizer in ('lazy', 'approimate-lazy'):
self.calculate_gains_ = self.calculate_gains_(dtypes_, False, True, False)
else:
self.calculate_gains_ = self.calculate_gains_(dtypes_, True, True, False)
#calculate_sieve_gains_ = calculate_gains_sieve_sparse if self.sparse else calculate_gains_sieve
#dtypes_ = sieve_sparse_dtypes if self.sparse else sieve_dtypes
#self.calculate_sieve_gains_ = calculate_sieve_gains_(dtypes_,
# True, True, False)
self.calculate_sieve_gains_ = calculate_gains_sieve(sieve_dtypes,
True, True, False)
def _calculate_gains(self, X_pairwise, idxs=None):
idxs = idxs if idxs is not None else self.idxs
gains = numpy.zeros(idxs.shape[0], dtype='float64')
if self.sparse:
self.calculate_gains_(X_pairwise.data, X_pairwise.indices,
X_pairwise.indptr, gains, self.current_values, idxs)
else:
self.calculate_gains_(X_pairwise, gains, self.current_values, idxs)
gains -= self.current_values_sum
return gains
def _calculate_sieve_gains(self, X_pairwise, thresholds, idxs):
"""This function will update the internal statistics from a stream.
This function will update the various internal statistics that are a
part of the sieve algorithm for streaming submodular optimization. This
function does not directly return gains but it updates the values
used by a streaming optimizer.
"""
super(FacilityLocationSelection, self)._calculate_sieve_gains(
X_pairwise,thresholds, idxs)
if self.sparse:
self.calculate_sieve_gains_(X_pairwise.data, X_pairwise.indices,
X_pairwise.indptr, self.n_samples, self.sieve_current_values_,
self.sieve_selections_, self.sieve_gains_,
self.sieve_total_gains_, thresholds,
self.sieve_n_selected_, idxs)
else:
self.calculate_sieve_gains_(X_pairwise, self.n_samples,
self.sieve_current_values_, self.sieve_selections_,
self.sieve_gains_, self.sieve_total_gains_, thresholds,
self.sieve_n_selected_, idxs)
def _select_next(self, X_pairwise, gain, idx):
"""This function will add the given item to the selected set."""
if self.sparse:
self.current_values = numpy.maximum(
X_pairwise.toarray()[0], self.current_values)
else:
self.current_values = numpy.maximum(X_pairwise,
self.current_values)
self.current_values_sum = self.current_values.sum()
super(FacilityLocationSelection, self)._select_next(
X_pairwise, gain, idx)
jmschrei-apricot-bf86e69/apricot/functions/featureBased.py 0000664 0000000 0000000 00000033424 14145540156 0024040 0 ustar 00root root 0000000 0000000 # featureBased.py
# Author: Jacob Schreiber
import math
import numpy
from .base import BaseSelection
from ..optimizers import LazyGreedy
from ..optimizers import ApproximateLazyGreedy
from tqdm import tqdm
from numba import njit
from numba import prange
import torch
@njit('float64[:](float64[:])', fastmath=True)
def sigmoid(X):
return X / (1. + X)
dtypes = 'void(float64[:,:], float64[:], float64[:], int64[:])'
sparse_dtypes = 'void(float64[:], int32[:], int32[:], float64[:],' \
'float64[:], float64[:], int64[:])'
sieve_dtypes = 'void(float64[:,:], int64, float64[:,:], int64[:,:],' \
'float64[:,:], float64[:], float64[:], int64[:], int64[:])'
sieve_sparse_dtypes = 'void(float64[:], int32[:], int32[:], int64,' \
'float64[:,:], int64[:,:], float64[:,:], float64[:], float64[:],' \
'int64[:], int64[:])'
def calculate_gains(func, dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_(X, gains, current_values, idxs):
for i in prange(idxs.shape[0]):
idx = idxs[i]
gains[i] = func(current_values + X[idx]).sum()
return calculate_gains_
def calculate_gains_sparse(func, dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sparse_(X_data, X_indices, X_indptr, gains,
current_values, current_concave_values, idxs):
for i in prange(idxs.shape[0]):
idx = idxs[i]
start = X_indptr[idx]
end = X_indptr[idx+1]
for j in range(start, end):
k = X_indices[j]
gains[i] += (func(X_data[j] + current_values[k])
- current_concave_values[k])
return calculate_gains_sparse_
def calculate_gains_sieve(func, dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sieve_(X, k, current_values, selections, gains,
total_gains, max_values, n_selected, idxs):
n = X.shape[0]
d = max_values.shape[0]
for j in prange(d):
for i in range(n):
if n_selected[j] == k:
break
idx = idxs[i]
threshold = (max_values[j] / 2. - total_gains[j]) / (k - n_selected[j])
gain = func(current_values[j] + X[i]).sum() - total_gains[j]
if gain > threshold:
current_values[j] += X[i]
total_gains[j] += gain
selections[j, n_selected[j]] = idx
gains[j, n_selected[j]] = gain
n_selected[j] += 1
return calculate_gains_sieve_
def calculate_gains_sieve_sparse(func, dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sieve_sparse_(X_data, X_indices, X_indptr, k,
current_values, selections, gains, total_gains, max_values,
n_selected, idxs):
d = max_values.shape[0]
for j in prange(d):
for i in range(idxs.shape[0]):
if n_selected[j] == k:
break
idx = idxs[i]
start = X_indptr[i]
end = X_indptr[i+1]
threshold = (max_values[j] / 2. - total_gains[j]) / (k - n_selected[j])
gain = 0.0
for l in range(start, end):
m = X_indices[l]
gain += (func(current_values[j, m] + X_data[l]) -
func(current_values[j, m]))
if gain > threshold:
for l in range(start, end):
m = X_indices[l]
current_values[j, m] += X_data[l]
total_gains[j] += gain
selections[j, n_selected[j]] = idx
gains[j, n_selected[j]] = gain
n_selected[j] += 1
return calculate_gains_sieve_sparse_
class FeatureBasedSelection(BaseSelection):
"""A selector based off a feature based submodular function.
Feature-based functions are those that operate on the feature values of
examples directly, rather than on a similarity matrix (or graph) derived
from those features, as graph-based functions do. Because these functions
do not require calculating and storing a :math:`\\mathcal{O}(n^{2})` sized
matrix of similarities, they can easily scale to data sets with millions of
examples.
.. note::
All values in your data must be positive for this selection to work.
The general form of a feature-based function is:
.. math::
f(X) = \\sum\\limits_{d=1}^{D}\\phi\\left(\\sum\\limits_{i=1}^{N} X_{i, d}\\right)
where :math:`f` indicates the function that uses the concave function
:math:`\\phi` and is operating on a subset :math:`X` that has :math:`N`
examples and :math:`D` dimensions. Importantly, :math:`X` is the subset
and not the ground set, meaning that the time it takes to evaluate this
function is proportional only to the size of the selected subset and not
the size of the full data set, like it is for graph-based functions.
See https://ieeexplore.ieee.org/document/6854213 for more details on
feature based functions.
Parameters
----------
n_samples : int
The number of samples to return.
concave_func : str or callable
The type of concave function to apply to the feature values. You can
pass in your own function to apply. Otherwise must be one of the
following:
'log' : log(1 + X)
'sqrt' : sqrt(X)
'sigmoid' : X / (1 + X)
initial_subset : list, numpy.ndarray or None
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict or None
A dictionary of arguments to pass into the optimizer object. The keys
of this dictionary should be the names of the parameters in the optimizer
and the values in the dictionary should be the values that these
parameters take. Default is None.
reservoir : numpy.ndarray or None
The reservoir to use when calculating gains in the sieve greedy
streaming optimization algorithm in the `partial_fit` method.
Currently only used for graph-based functions. If a numpy array
is passed in, it will be used as the reservoir. If None is passed in,
will use reservoir sampling to collect a reservoir. Default is None.
max_reservoir_size : int
The maximum size that the reservoir can take. If a reservoir is passed
in, this value is set to the size of that array. Default is 1000.
n_jobs : int
The number of threads to use when performing computation in parallel.
Currently, this parameter is exposed but does not actually do anything.
This will be fixed soon.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
ranking : numpy.array int
The selected samples in the order of their gain with the first number in
the ranking corresponding to the index of the first sample that was
selected by the greedy procedure.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples, concave_func='sqrt', initial_subset=None,
optimizer='two-stage', optimizer_kwds={}, reservoir=None,
max_reservoir_size=1000, n_jobs=1, random_state=None,
verbose=False):
self.concave_func_name = concave_func
if concave_func == 'log':
self.concave_func = numpy.log1p
elif concave_func == 'sqrt':
self.concave_func = numpy.sqrt
elif concave_func == 'sigmoid':
self.concave_func = sigmoid
elif callable(concave_func):
self.concave_func = concave_func
else:
raise KeyError("Must be one of 'log', 'sqrt', 'sigmoid', or a any other numpy, scipy, or numba-compiled function.")
super(FeatureBasedSelection, self).__init__(n_samples=n_samples,
initial_subset=initial_subset, optimizer=optimizer,
optimizer_kwds=optimizer_kwds, reservoir=reservoir,
max_reservoir_size=max_reservoir_size, n_jobs=n_jobs,
random_state=random_state, verbose=verbose)
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : FeatureBasedSelection
The fit step returns this selector object.
"""
return super(FeatureBasedSelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X):
super(FeatureBasedSelection, self)._initialize(X)
if self.initial_subset is None:
pass
elif self.initial_subset.ndim == 2:
self.current_values = self.initial_subset.sum(axis=0).astype('float64')
elif self.initial_subset.ndim == 1:
self.current_values = X[self.initial_subset].sum(axis=0).astype('float64')
else:
raise ValueError("The initial subset must be either a two dimensional" \
" matrix of examples or a one dimensional mask.")
self.current_concave_values = self.concave_func(self.current_values)
self.current_concave_values_sum = self.current_concave_values.sum()
calculate_gains_ = calculate_gains_sparse if self.sparse else calculate_gains
dtypes_ = sparse_dtypes if self.sparse else dtypes
if self.optimizer in (LazyGreedy, ApproximateLazyGreedy, 'lazy', 'approximate-lazy'):
self.calculate_gains_ = calculate_gains_(self.concave_func,
dtypes_, False, True, False)
else:
self.calculate_gains_ = calculate_gains_(self.concave_func,
dtypes_, True, True, False)
calculate_sieve_gains_ = calculate_gains_sieve_sparse if self.sparse else calculate_gains_sieve
dtypes_ = sieve_sparse_dtypes if self.sparse else sieve_dtypes
self.calculate_sieve_gains_ = calculate_sieve_gains_(self.concave_func,
dtypes_, True, True, False)
def _calculate_gains(self, X, idxs=None):
"""This function will return the gain that each example would give.
This function will return the gains that each example would give if
added to the selected set. When a matrix of examples is given, a
vector will be returned showing the gain for each example. When
a single element is passed in, it will return a singe value."""
idxs = idxs if idxs is not None else self.idxs
gains = numpy.zeros(idxs.shape[0], dtype='float64')
if self.sparse:
self.calculate_gains_(X.data, X.indices, X.indptr, gains,
self.current_values, self.current_concave_values, idxs)
else:
self.calculate_gains_(X, gains, self.current_values, idxs)
gains -= self.current_concave_values_sum
return gains
def _calculate_sieve_gains(self, X, thresholds, idxs):
"""This function will update the internal statistics from a stream.
This function will update the various internal statistics that are a
part of the sieve algorithm for streaming submodular optimization. This
function does not directly return gains but it updates the values
used by a streaming optimizer.
"""
super(FeatureBasedSelection, self)._calculate_sieve_gains(X,
thresholds, idxs)
if self.sparse:
self.calculate_sieve_gains_(X.data, X.indices, X.indptr,
self.n_samples, self.sieve_current_values_,
self.sieve_selections_, self.sieve_gains_,
self.sieve_total_gains_, thresholds,
self.sieve_n_selected_, idxs)
else:
self.calculate_sieve_gains_(X, self.n_samples,
self.sieve_current_values_, self.sieve_selections_,
self.sieve_gains_, self.sieve_total_gains_, thresholds,
self.sieve_n_selected_, idxs)
def _select_next(self, X, gain, idx):
"""This function will add the given item to the selected set."""
if self.sparse:
self.current_values += X.toarray()[0]
else:
self.current_values += X
self.current_concave_values = self.concave_func(self.current_values)
self.current_concave_values_sum = self.current_concave_values.sum()
super(FeatureBasedSelection, self)._select_next(
X, gain, idx)
jmschrei-apricot-bf86e69/apricot/functions/graphCut.py 0000664 0000000 0000000 00000027020 14145540156 0023216 0 ustar 00root root 0000000 0000000 # graphCut.py
# Author: Jacob Schreiber
import numpy
from .base import BaseGraphSelection
from ..utils import _calculate_pairwise_distances
from tqdm import tqdm
from numba import njit
from numba import prange
from scipy.sparse import csr_matrix
sieve_dtypes = 'void(float64[:,:], int64, float64[:,:], int64[:,:],' \
'float64[:,:], float64[:], float64[:], int64[:], float64[:], int64[:])'
def calculate_gains_sieve(dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sieve_(X, k, current_values, selections, gains,
total_gains, max_values, n_selected, row_sums, idxs):
n, d = X.shape
t = max_values.shape[0]
for j in prange(t):
for i in range(n):
if n_selected[j] == k:
break
idx = idxs[i]
threshold = (max_values[j] / 2. - total_gains[j]) / (k - n_selected[j])
gain = row_sums[i] - current_values[j, i]
if gain > threshold:
total_gains[j] += gain
selections[j, n_selected[j]] = idx
gains[j, n_selected[j]] = gain
n_selected[j] += 1
return calculate_gains_sieve_
class GraphCutSelection(BaseGraphSelection):
"""A selector based on using a graph-cut function.
Graph cuts are canonical class of functions that involves selecting
examples that split the similarity matrix into subgraphs well.
.. note::
All ~pairwise~ values in your data must be non-negative for this
selection to work.
The general form of a graph cut function is
.. math::
f(X, V) = \\lambda\\sum_{v \\in V} \\sum_{x \\in X} \\phi(x, v) - \\sum_{x, y \\in X} \\phi(x, y)
where :math:`f` indicates the function, :math:`X` is a subset, :math:`V`
is the ground set, and :math:`\\phi` is the similarity measure between
two examples. Like most graph-based functons, the graph-cut function
requires access to the full similarity matrix.
This implementation allows users to pass in either their own symmetric
square matrix of similarity values, or a data matrix as normal and a function
that calculates these pairwise values.
Parameters
----------
n_samples : int
The number of samples to return.
metric : str, optional
The method for converting a data matrix into a square symmetric matrix
of pairwise similarities. If a string, can be any of the metrics
implemented in sklearn (see
https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise_distances.html),
including "precomputed" if one has already generated a similarity
matrix. Note that sklearn calculates distance matrices whereas apricot
operates on similarity matrices, and so a distances.max() - distances
transformation is performed on the resulting distances. For
backcompatibility, 'corr' will be read as 'correlation'. Default is
'euclidean'.
alpha : float
The weight of the first term in the graph-cut objective, which
measures the how representative the selected examples are of the
ground set. The larger this is, the more likely examples are chosen
that are near points in the ground set, even if there are other
already-selected examples that are similar. Default is 1.
initial_subset : list, numpy.ndarray or None, optional
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional. Default is None.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict or None
A dictionary of arguments to pass into the optimizer object. The keys
of this dictionary should be the names of the parameters in the optimizer
and the values in the dictionary should be the values that these
parameters take. Default is None.
n_neighbors : int or None
When constructing a similarity matrix, the number of nearest neighbors
whose similarity values will be kept. The result is a sparse similarity
matrix which can significantly speed up computation at the cost of
accuracy. Default is None.
reservoir : numpy.ndarray or None
The reservoir to use when calculating gains in the sieve greedy
streaming optimization algorithm in the `partial_fit` method.
Currently only used for graph-based functions. If a numpy array
is passed in, it will be used as the reservoir. If None is passed in,
will use reservoir sampling to collect a reservoir. Default is None.
max_reservoir_size : int
The maximum size that the reservoir can take. If a reservoir is passed
in, this value is set to the size of that array. Default is 1000.
n_jobs : int
The number of threads to use when performing computation in parallel.
Currently, this parameter is exposed but does not actually do anything.
This will be fixed soon.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
pairwise_func : callable
A function that takes in a data matrix and converts it to a square
symmetric matrix.
ranking : numpy.array int
The selected samples in the order of their gain.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples=10, metric='euclidean', alpha=1,
initial_subset=None, optimizer='naive', optimizer_kwds={},
n_neighbors=None, reservoir=None, max_reservoir_size=1000,
n_jobs=1, random_state=None, verbose=False):
self.alpha = alpha
super(GraphCutSelection, self).__init__(n_samples=n_samples,
metric=metric, initial_subset=initial_subset, optimizer=optimizer,
n_neighbors=n_neighbors, reservoir=reservoir,
max_reservoir_size=max_reservoir_size, n_jobs=n_jobs,
random_state=random_state, optimizer_kwds={}, verbose=verbose)
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : GraphCutSelection
The fit step returns this selector object.
"""
return super(GraphCutSelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X_pairwise):
super(GraphCutSelection, self)._initialize(X_pairwise)
if self.reservoir is not None:
X_pairwise = _calculate_pairwise_distances(self._X,
metric=self.metric)
if self.sparse:
self.row_sums = self.alpha * numpy.array(X_pairwise.sum(axis=1))[:,0]
self.current_values = X_pairwise.diagonal().astype('float64')
else:
self.row_sums = self.alpha * X_pairwise.sum(axis=1)
self.current_values = numpy.diag(X_pairwise).astype('float64')
if self.initial_subset is None:
pass
elif self.initial_subset.ndim == 2:
raise ValueError("When using graph-cut, the initial subset"\
" must be a one dimensional array of indices.")
elif self.initial_subset.ndim == 1:
if self.sparse:
for i in self.initial_subset:
self.current_values += X_pairwise[i].toarray()[0] * 2
else:
for i in self.initial_subset:
self.current_values += X_pairwise[i] * 2
else:
raise ValueError("The initial subset must be either a two dimensional" \
" matrix of examples or a one dimensional mask.")
self.calculate_sieve_gains_ = calculate_gains_sieve(sieve_dtypes,
True, True, False)
def _calculate_gains(self, X_pairwise, idxs=None):
idxs = idxs if idxs is not None else self.idxs
gains = self.row_sums[idxs] - self.current_values[idxs]
return gains
def _calculate_sieve_gains(self, X_pairwise, thresholds, idxs):
"""This function will update the internal statistics from a stream.
This function will update the various internal statistics that are a
part of the sieve algorithm for streaming submodular optimization. This
function does not directly return gains but it updates the values
used by a streaming optimizer.
"""
super(GraphCutSelection, self)._calculate_sieve_gains(X_pairwise,
thresholds, idxs)
n, m = X_pairwise.shape[0], len(thresholds)
row_sums = self.alpha * X_pairwise.mean(axis=1)
sieve_current_values_ = numpy.tile(numpy.diag(
_calculate_pairwise_distances(self._X, metric=self.metric)), (m, 1))
for i in range(m):
l = self.sieve_n_selected_[i]
if l == self.n_samples or l == 0:
continue
sieve_current_values_[i] = _calculate_pairwise_distances(
self._X, Y=self.sieve_subsets_[i, :l],
metric=self.metric).mean(axis=1)
if self.sparse:
self.calculate_sieve_gains_(X_pairwise.data,
X_pairwise.indices, X_pairwise.indptr,
self.n_samples, sieve_current_values_,
self.sieve_selections_, self.sieve_gains_,
self.sieve_total_gains_, thresholds,
self.sieve_n_selected_, idxs)
else:
self.calculate_sieve_gains_(X_pairwise, self.n_samples,
sieve_current_values_, self.sieve_selections_,
self.sieve_gains_, self.sieve_total_gains_, thresholds,
self.sieve_n_selected_, row_sums, idxs)
def _select_next(self, X_pairwise, gain, idx):
"""This function will add the given item to the selected set."""
if self.sparse:
self.current_values += X_pairwise.toarray()[0] * 2
else:
self.current_values += X_pairwise * 2
super(GraphCutSelection, self)._select_next(
X_pairwise, gain, idx)
jmschrei-apricot-bf86e69/apricot/functions/maxCoverage.py 0000664 0000000 0000000 00000031434 14145540156 0023706 0 ustar 00root root 0000000 0000000 # maxCoverage.py
# Author: Jacob Schreiber
import numpy
from .base import BaseSelection
from ..optimizers import LazyGreedy
from ..optimizers import ApproximateLazyGreedy
from ..optimizers import SieveGreedy
from numba import njit
from numba import prange
dtypes = 'void(float64[:,:], float64[:], float64[:], float64, int64[:])'
sdtypes = 'void(float64[:], int32[:], int32[:], float64[:], float64[:], float64, int64[:])'
sieve_dtypes = 'void(float64[:,:], int64, float64[:,:], int64[:,:],' \
'float64[:,:], float64[:], float64[:], int64[:], float64, int64[:])'
sieve_sparse_dtypes = 'void(float64[:], int32[:], int32[:], int64,' \
'float64[:,:], int64[:,:], float64[:,:], float64[:], float64[:],' \
'int64[:], float64, int64[:])'
def calculate_gains(dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_(X, gains, current_values, threshold, idxs):
for i in prange(idxs.shape[0]):
idx = idxs[i]
gains[i] = numpy.fmin(current_values + X[idx], threshold).sum()
return calculate_gains_
def calculate_gains_sparse(dtypes, parallel, fastmath, cache):
@njit(sdtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sparse_(X_data, X_indices, X_indptr, gains,
current_values, threshold, idxs):
for i in prange(idxs.shape[0]):
idx = idxs[i]
start = X_indptr[idx]
end = X_indptr[idx+1]
for j in range(start, end):
k = X_indices[j]
gains[i] += min(X_data[j] + current_values[k], threshold) - current_values[k]
return calculate_gains_sparse_
def calculate_gains_sieve(dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sieve_(X, k, current_values, selections, gains,
total_gains, max_values, n_selected, thresh, idxs):
n = X.shape[0]
d = max_values.shape[0]
for j in prange(d):
for i in range(n):
if n_selected[j] == k:
break
idx = idxs[i]
threshold = (max_values[j] / 2. - total_gains[j]) / (k - n_selected[j])
gain = (numpy.fmin(current_values[j] + X[i], thresh).sum() -
total_gains[j])
if gain > threshold:
current_values[j] += X[i]
total_gains[j] += gain
selections[j, n_selected[j]] = idx
gains[j, n_selected[j]] = gain
n_selected[j] += 1
return calculate_gains_sieve_
def calculate_gains_sieve_sparse(dtypes, parallel, fastmath, cache):
@njit(dtypes, parallel=parallel, fastmath=fastmath, cache=cache)
def calculate_gains_sieve_sparse_(X_data, X_indices, X_indptr, k,
current_values, selections, gains, total_gains, max_values,
n_selected, thresh, idxs):
d = max_values.shape[0]
for j in prange(d):
for i in range(idxs.shape[0]):
if n_selected[j] == k:
break
idx = idxs[i]
start = X_indptr[i]
end = X_indptr[i+1]
threshold = (max_values[j] / 2. - total_gains[j]) / (k - n_selected[j])
gain = 0.0
for l in range(start, end):
m = X_indices[l]
gain += (min(current_values[j, m] + X_data[l], thresh) -
current_values[j, m])
if gain > threshold:
for l in range(start, end):
m = X_indices[l]
current_values[j, m] = min(current_values[j, m] + X_data[l],
thresh)
total_gains[j] += gain
selections[j, n_selected[j]] = idx
gains[j, n_selected[j]] = gain
n_selected[j] += 1
return calculate_gains_sieve_sparse_
class MaxCoverageSelection(BaseSelection):
"""A selector based off a coverage function.
Maximum coverage functions aim to maximize the number of features that
have a non-zero element in at least one selected example---there is no
marginal benefit to observing a variable in two examples. If each variable
is thought to be an item in a set, and the data is a binary matrix where a
1 indicates the item is present in the example and 0 indicates it is not,
optimizing a maximum coverage function is a solution to the set coverage
problem. These functions are useful when the space of variables is massive
and each example only sees a small subset of them, which is a common
situation when analyzing text data when the variables are words. The
maximum coverage function is an instance of a feature-based function
when the concave function is minimum.
.. note::
All values in your data must be binary for this selection to work.
The general form of a coverage function is:
.. math::
f(X) = \\sum\\limits_{d=1}^{D} \\min \\left( \\sum\\limits_{n=1}^{N} X_{i, d}, 1 \\right)
where :math:`f` indicates the function that operates on a subset :math:`X`
that has :math:`N` examples and :math:`D` dimensions. Importantly,
:math:`X` is the subset and not the ground set, meaning that the time it
takes to evaluate this function is proportional only to the size of the
selected subset and not the size of the full data set, like it is for
graph-based functions.
See https://www2.cs.duke.edu/courses/fall17/compsci632/scribing/scribe2.pdf
where the problem is described as maximum coverage.
Parameters
----------
n_samples : int
The number of examples to return.
initial_subset : list, numpy.ndarray or None
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict, optional
Arguments to pass into the optimizer object upon initialization.
Default is {}.
n_jobs : int, optional
The number of cores to use for processing. This value is multiplied
by 2 when used to set the number of threads. If set to -1, use all
cores and threads. Default is -1.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
ranking : numpy.array int
The selected samples in the order of their gain with the first number in
the ranking corresponding to the index of the first sample that was
selected by the greedy procedure.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples, threshold=1.0, initial_subset=None,
optimizer='two-stage', optimizer_kwds={}, n_jobs=1, random_state=None,
verbose=False):
self.threshold = threshold
super(MaxCoverageSelection, self).__init__(n_samples=n_samples,
initial_subset=initial_subset, optimizer=optimizer,
optimizer_kwds=optimizer_kwds, n_jobs=n_jobs,
random_state=random_state, verbose=verbose)
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : FeatureBasedSelection
The fit step returns this selector object.
"""
return super(MaxCoverageSelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X):
super(MaxCoverageSelection, self)._initialize(X)
if self.initial_subset is None:
self.current_values = numpy.zeros(X.shape[1], dtype='float64')
elif self.initial_subset.ndim == 2:
self.current_values = numpy.fmin(self.threshold,
self.initial_subset.sum(axis=0).astype('float64'))
elif self.initial_subset.ndim == 1:
self.current_values = numpy.fmin(self.threshold,
X[self.initial_subset].sum(axis=0).astype('float64'))
else:
raise ValueError("The initial subset must be either a two dimensional" \
" matrix of examples or a one dimensional mask.")
self.current_values_sum = self.current_values.sum()
calculate_gains_ = calculate_gains_sparse if self.sparse else calculate_gains
dtypes_ = sdtypes if self.sparse else dtypes
if self.optimizer in (LazyGreedy, ApproximateLazyGreedy):
self.calculate_gains_ = calculate_gains_(dtypes_, False, True, False)
elif self.optimizer in ('lazy', 'approimate-lazy'):
self.calculate_gains_ = calculate_gains_(dtypes_, False, True, False)
else:
self.calculate_gains_ = calculate_gains_(dtypes_, True, True, False)
calculate_sieve_gains_ = calculate_gains_sieve_sparse if self.sparse else calculate_gains_sieve
dtypes_ = sieve_sparse_dtypes if self.sparse else sieve_dtypes
self.calculate_sieve_gains_ = calculate_sieve_gains_(dtypes_, True,
True, False)
def _calculate_gains(self, X, idxs=None):
"""This function will return the gain that each example would give.
This function will return the gains that each example would give if
added to the selected set. When a matrix of examples is given, a
vector will be returned showing the gain for each example. When
a single element is passed in, it will return a singe value."""
idxs = idxs if idxs is not None else self.idxs
gains = numpy.zeros(idxs.shape[0], dtype='float64')
if self.sparse:
self.calculate_gains_(X.data, X.indices, X.indptr, gains,
self.current_values, self.threshold, idxs)
else:
self.calculate_gains_(X, gains, self.current_values,
self.threshold, idxs)
gains -= self.current_values_sum
return gains
def _calculate_sieve_gains(self, X, thresholds, idxs):
"""This function will update the internal statistics from a stream.
This function will update the various internal statistics that are a
part of the sieve algorithm for streaming submodular optimization. This
function does not directly return gains but it updates the values
used by a streaming optimizer.
"""
super(MaxCoverageSelection, self)._calculate_sieve_gains(X,
thresholds, idxs)
if self.sparse:
self.calculate_sieve_gains_(X.data, X.indices, X.indptr,
self.n_samples, self.sieve_current_values_,
self.sieve_selections_, self.sieve_gains_,
self.sieve_total_gains_, thresholds,
self.sieve_n_selected_, self.threshold, idxs)
else:
self.calculate_sieve_gains_(X, self.n_samples,
self.sieve_current_values_, self.sieve_selections_,
self.sieve_gains_, self.sieve_total_gains_, thresholds,
self.sieve_n_selected_, self.threshold, idxs)
def _select_next(self, X, gain, idx):
"""This function will add the given item to the selected set."""
if self.sparse:
self.current_values = numpy.fmin(self.threshold,
self.current_values + X.toarray()[0])
else:
self.current_values = numpy.fmin(self.threshold,
self.current_values + X)
self.current_values_sum = self.current_values.sum()
super(MaxCoverageSelection, self)._select_next(
X, gain, idx) jmschrei-apricot-bf86e69/apricot/functions/mixture.py 0000664 0000000 0000000 00000025721 14145540156 0023144 0 ustar 00root root 0000000 0000000 # mixture.py
# Author: Jacob Schreiber
import numpy
from .base import BaseSelection
from .base import BaseGraphSelection
from ..utils import _calculate_pairwise_distances
from tqdm import tqdm
class MixtureSelection(BaseSelection):
"""A selection approach based on a mixture of submodular functions.
A convenient property of submodular functions is that any linear
combination of submodular functions is still submodular. More generally,
the linear combination of any number of submodular functions (assuming
non-negative weights) is still a submodular function. Because of this,
a mixture of submodular functions can be optimized using the same
algorithms as an individual submodular function. Mixtures can be
useful in situations where there are different important aspects
of data that are each submodular.
The general form of a mixture function is
.. math::
f(X) = \\sum\\limits_{i=1}^{M} \\alpha_{i} g_{i}(X)
where :math:`f` indicates the mixture function, :math:`M` is the number of
functions in the mixture, :math:`X` is a subset, :math:`\\alpha_{i}` is the
weight of the :math:`i`-th function and :math:`g_{i}` is the :math:`i`-th
function.
.. note::
There must be at least two components to the mixture, and all
:math:`\\alpha` must be non-negative.
This class can also be used to add regularizers to the selection procedure.
If a submodular function is mixed with another submodular function that
acts as a regularizer, such as feature based selection mixed with a
custom function measuring some property of the selected subset.
Parameters
----------
n_samples : int
The number of samples to return.
functions : list
The list of submodular functions to mix together. The submodular
functions should be instantiated.
weights : list, numpy.ndarray or None, optional
The relative weight of each submodular function. This is the value
that the gain from each submodular function is multiplied by before
being added together. The default is equal weight for each function.
initial_subset : list, numpy.ndarray or None, optional
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict or None
A dictionary of arguments to pass into the optimizer object. The keys
of this dictionary should be the names of the parameters in the optimizer
and the values in the dictionary should be the values that these
parameters take. Default is None.
reservoir : numpy.ndarray or None
The reservoir to use when calculating gains in the sieve greedy
streaming optimization algorithm in the `partial_fit` method.
Currently only used for graph-based functions. If a numpy array
is passed in, it will be used as the reservoir. If None is passed in,
will use reservoir sampling to collect a reservoir. Default is None.
max_reservoir_size : int
The maximum size that the reservoir can take. If a reservoir is passed
in, this value is set to the size of that array. Default is 1000.
n_jobs : int
The number of threads to use when performing computation in parallel.
Currently, this parameter is exposed but does not actually do anything.
This will be fixed soon.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool, optional
Whether to print output during the selection process.
Attributes
----------
pq : PriorityQueue
The priority queue used to implement the lazy greedy algorithm.
n_samples : int
The number of samples to select.
submodular_functions : list
A concave function for transforming feature values, often referred to as
phi in the literature.
weights : numpy.ndarray
The weights of each submodular function.
ranking : numpy.array int
The selected samples in the order of their gain with the first number in
the ranking corresponding to the index of the first sample that was
selected by the greedy procedure.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples, functions, weights=None, metric='ignore',
initial_subset=None, optimizer='two-stage', optimizer_kwds={}, n_neighbors=None,
reservoir=None, max_reservoir_size=1000, n_jobs=1, random_state=None,
verbose=False):
if len(functions) < 2:
raise ValueError("Must mix at least two functions.")
self.m = len(functions)
self.functions = functions
if weights is None:
self.weights = numpy.ones(self.m, dtype='float64')
else:
self.weights = numpy.array(weights, dtype='float64', copy=False)
super(MixtureSelection, self).__init__(n_samples=n_samples,
initial_subset=initial_subset, optimizer=optimizer,
optimizer_kwds=optimizer_kwds, reservoir=reservoir,
max_reservoir_size=max_reservoir_size, n_jobs=n_jobs,
random_state=random_state, verbose=verbose)
self.metric = metric.replace("corr", "correlation")
self.n_neighbors = n_neighbors
for function in self.functions:
function.initial_subset = self.initial_subset
function.reservoir = reservoir
function.max_reservoir_size = max_reservoir_size
function.metric = 'precomputed'
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : MixtureSelection
The fit step returns this selector object.
"""
# If self.metric is ignore, this will return the same matrix.
# Otherwise, it will convert it to a pairwise similarity matrix.
self._X = X
X = _calculate_pairwise_distances(X, metric=self.metric,
n_neighbors=self.n_neighbors)
return super(MixtureSelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X):
super(MixtureSelection, self)._initialize(X)
for function in self.functions:
function._initialize(X)
def _calculate_gains(self, X, idxs=None):
"""This function will return the gain that each example would give.
This function will return the gains that each example would give if
added to the selected set. When a matrix of examples is given, a
vector will be returned showing the gain for each example. When
a single element is passed in, it will return a singe value."""
idxs = idxs if idxs is not None else self.idxs
gains = numpy.zeros(idxs.shape[0], dtype='float64')
for i, function in enumerate(self.functions):
gains += function._calculate_gains(X, idxs) * self.weights[i]
return gains
def _calculate_sieve_gains(self, X, thresholds, idxs):
super(MixtureSelection, self)._calculate_sieve_gains(X,
thresholds, idxs)
for function in self.functions:
super(function.__class__, function)._calculate_sieve_gains(X,
thresholds, idxs)
t = numpy.zeros(thresholds.shape[0], dtype='float64') - 1
for j in range(X.shape[0]):
x = X[j:j+1]
current_values = []
total_gains = []
gain = numpy.zeros((len(thresholds), self.n_samples), dtype='float64')
for i, function in enumerate(self.functions):
current_values.append(function.sieve_current_values_.copy())
total_gains.append(function.sieve_total_gains_.copy())
function._calculate_sieve_gains(x, t, idxs)
gain += function.sieve_gains_ * self.weights[i]
for l in range(len(thresholds)):
if self.sieve_n_selected_[l] == self.n_samples:
continue
threshold = ((thresholds[l] / 2. - self.sieve_total_gains_[l])
/ (self.n_samples - self.sieve_n_selected_[l]))
if gain[l, self.sieve_n_selected_[l]] > threshold:
self.sieve_total_gains_[l] += gain[l, self.sieve_n_selected_[l]]
self.sieve_selections_[l, self.sieve_n_selected_[l]] = idxs[j]
self.sieve_gains_[l, self.sieve_n_selected_[l]] = gain[l, self.sieve_n_selected_[l]]
self.sieve_n_selected_[l] += 1
else:
v = self.sieve_n_selected_[l]
for i, function in enumerate(self.functions):
vi = function.sieve_n_selected_[l]
if vi != self.sieve_n_selected_[l]:
function.sieve_current_values_[l] = current_values[i][l]
function.sieve_total_gains_[l] = total_gains[i][l]
function.sieve_n_selected_[l] = vi - 1
if vi < self.n_samples:
function.sieve_selections_[l, vi] = 0
function.sieve_gains_[l, vi] = 0
function.sieve_subsets_[l, vi] = 0
def _select_next(self, X, gain, idx):
"""This function will add the given item to the selected set."""
for function in self.functions:
function._select_next(X, gain, idx)
super(MixtureSelection, self)._select_next(X, gain, idx)
jmschrei-apricot-bf86e69/apricot/functions/saturatedCoverage.py 0000664 0000000 0000000 00000024552 14145540156 0025120 0 ustar 00root root 0000000 0000000 # saturatedCoverage.py
# Author: Jacob Schreiber
import numpy
from .base import BaseGraphSelection
from tqdm import tqdm
from numba import njit
from numba import prange
from scipy.sparse import csr_matrix
dtypes = 'void(float64[:,:], float64[:], float64[:], float64[:], int64[:])'
sdtypes = 'void(float64[:], int32[:], int32[:], float64[:], float64[:], float64[:], int64[:])'
@njit(dtypes, nogil=True, parallel=True)
def select_next(X, gains, current_values, max_values, idxs):
for i in prange(idxs.shape[0]):
idx = idxs[i]
gains[i] = numpy.minimum(current_values + X[idx], max_values).sum()
@njit(sdtypes, nogil=True, parallel=True)
def select_next_sparse(X_data, X_indices, X_indptr, gains, current_values, max_values, idxs):
for i in prange(idxs.shape[0]):
idx = idxs[i]
start = X_indptr[idx]
end = X_indptr[idx+1]
for j in range(start, end):
k = X_indices[j]
gains[i] += min(X_data[j] + current_values[k], max_values[k]) - current_values[k]
class SaturatedCoverageSelection(BaseGraphSelection):
"""A saturated coverage submodular selection algorithm.
The saturated coverage function is a graph-based function where the gain
is the sum of the similarities between the candidate example and the
entire ground set up until a certain point. Essentially, each example in
the ground set contributes to the overall function until some prespecified
level of "coverage" is met. Once an example is similar enough to the
selected examples, it stops contributing to the function.
.. note::
All ~pairwise~ values in your data must be positive for this
selection to work.
The general form of a saturated coverage function is
.. math::
f(X, V) = \\sum_{v \\in V} \\min\\{\\sum_{x \\in X} \\phi(x, v), \\alpha\\}
where :math:`f` indicates the function, :math:`X` is a subset, :math:`V`
is the ground set, and :math:`\\phi` is the similarity measure between two
examples, and :math:`\\alpha` is a parameter that, for each point in the
ground set, specifies the maximum similarity that each example can have
to the selected set (the saturation). Like most graph-based functons,
the saturated coverage function requires access to the full ground set.
Parameters
----------
n_samples : int
The number of samples to return.
metric : str, optional
The method for converting a data matrix into a square symmetric matrix
of pairwise similarities. If a string, can be any of the metrics
implemented in sklearn (see
https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise_distances.html),
including "precomputed" if one has already generated a similarity
matrix. Note that sklearn calculates distance matrices whereas apricot
operates on similarity matrices, and so a distances.max() - distances
transformation is performed on the resulting distances. For
backcompatibility, 'corr' will be read as 'correlation'. Default is
'euclidean'.
alpha : float
The threshold at which to saturate. Larger values of alpha are closer
to a modular function and smaller values of alpha are closer to all
examples providing the same, small, gain. Default is 0.1.
initial_subset : list, numpy.ndarray or None, optional
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional. Default is None.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict or None
A dictionary of arguments to pass into the optimizer object. The keys
of this dictionary should be the names of the parameters in the optimizer
and the values in the dictionary should be the values that these
parameters take. Default is None.
n_neighbors : int or None
When constructing a similarity matrix, the number of nearest neighbors
whose similarity values will be kept. The result is a sparse similarity
matrix which can significantly speed up computation at the cost of
accuracy. Default is None.
reservoir : numpy.ndarray or None
The reservoir to use when calculating gains in the sieve greedy
streaming optimization algorithm in the `partial_fit` method.
Currently only used for graph-based functions. If a numpy array
is passed in, it will be used as the reservoir. If None is passed in,
will use reservoir sampling to collect a reservoir. Default is None.
max_reservoir_size : int
The maximum size that the reservoir can take. If a reservoir is passed
in, this value is set to the size of that array. Default is 1000.
n_jobs : int
The number of threads to use when performing computation in parallel.
Currently, this parameter is exposed but does not actually do anything.
This will be fixed soon.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
pairwise_func : callable
A function that takes in a data matrix and converts it to a square
symmetric matrix.
ranking : numpy.array int
The selected samples in the order of their gain.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples=10, metric='euclidean', alpha=0.1,
initial_subset=None, optimizer='two-stage', n_neighbors=None, n_jobs=1,
random_state=None, reservoir=None, max_reservoir_size=None,
optimizer_kwds={}, verbose=False):
self.alpha = alpha
super(SaturatedCoverageSelection, self).__init__(n_samples=n_samples,
metric=metric,initial_subset=initial_subset, optimizer=optimizer,
optimizer_kwds=optimizer_kwds, n_neighbors=n_neighbors,
reservoir=reservoir, max_reservoir_size=max_reservoir_size,
n_jobs=n_jobs, random_state=random_state, verbose=verbose)
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : SaturatedCoverageSelection
The fit step returns this selector object.
"""
return super(SaturatedCoverageSelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X_pairwise):
super(SaturatedCoverageSelection, self)._initialize(X_pairwise)
if self.sparse:
self.max_values = self.alpha * numpy.array(
X_pairwise.sum(axis=1))[:,0]
else:
self.max_values = self.alpha * X_pairwise.sum(axis=1)
if self.initial_subset is None:
return
elif self.initial_subset.ndim == 2:
raise ValueError("When using saturated coverage, the initial subset"\
" must be a one dimensional array of indices.")
elif self.initial_subset.ndim == 1:
if not self.sparse:
for i in self.initial_subset:
self.current_values = numpy.minimum(self.max_values,
self.current_values + X_pairwise[i])
else:
for i in self.initial_subset:
self.current_values = numpy.minimum(self.max_values,
self.current_values + X_pairwise[i].toaray()[0])
else:
raise ValueError("The initial subset must be either a two dimensional" \
" matrix of examples or a one dimensional mask.")
def _calculate_gains(self, X_pairwise, idxs=None):
idxs = idxs if idxs is not None else self.idxs
gains = numpy.zeros(idxs.shape[0], dtype='float64')
if self.sparse:
select_next_sparse(X_pairwise.data,
X_pairwise.indices, X_pairwise.indptr, gains,
self.current_values, self.max_values, idxs)
else:
select_next(X_pairwise, gains, self.current_values,
self.max_values, idxs)
gains -= self.current_values.sum()
return gains
def _select_next(self, X_pairwise, gain, idx):
"""This function will add the given item to the selected set."""
if self.sparse:
self.current_values = numpy.minimum(self.max_values,
X_pairwise.toarray()[0] + self.current_values)
else:
self.current_values = numpy.minimum(self.max_values,
self.current_values + X_pairwise)
super(SaturatedCoverageSelection, self)._select_next(
X_pairwise, gain, idx)
jmschrei-apricot-bf86e69/apricot/functions/sumRedundancy.py 0000664 0000000 0000000 00000022022 14145540156 0024257 0 ustar 00root root 0000000 0000000 # sumRedundancy.py
# Author: Jacob Schreiber
import numpy
from .base import BaseGraphSelection
from tqdm import tqdm
class SumRedundancySelection(BaseGraphSelection):
"""A selector based off a sum redundancy submodular function.
The sum redundancy function is a graph-based function that penalizes
redundancy among the selected set. This approach is straightforward,
in that it simply involved a sum. It is also fast in comparison to a
facility location function because it involves only performing calculation
over the selected set as opposed to the entire ground set. Because the sum
of the similarities is not submodular, it is subtracted from the sum of
the entire similarity matrix, such that examples that are highly similar
to each other result in a lower value than examples that are not very
similar.
.. note::
All ~pairwise~ values in your data must be positive for this
selection to work.
The general form of a sum redundancy function is
.. math::
f(X, V) = \sum_{x, y \in V} \phi(x, y) - \sum_{x, y\in X} \phi(x,y)
where :math:`f` indicates the function, :math:`X` is the selected subset,
:math:`V` is the ground set, and :math:`\phi` is the similarity measure
between two examples. While sum redundancy functions involves calculating
the sum of the entire similarity matrix in principle, in practice if one
is only calculating the gains this step can be ignored.
This implementation allows users to pass in either their own symmetric
square matrix of similarity values, or a data matrix as normal and a function
that calculates these pairwise values.
Parameters
----------
n_samples : int
The number of samples to return.
metric : str, optional
The method for converting a data matrix into a square symmetric matrix
of pairwise similarities. If a string, can be any of the metrics
implemented in sklearn (see
https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise_distances.html),
including "precomputed" if one has already generated a similarity
matrix. Note that sklearn calculates distance matrices whereas apricot
operates on similarity matrices, and so a distances.max() - distances
transformation is performed on the resulting distances. For
backcompatibility, 'corr' will be read as 'correlation'. Default is
'euclidean'.
initial_subset : list, numpy.ndarray or None, optional
If provided, this should be a list of indices into the data matrix
to use as the initial subset, or a group of examples that may not be
in the provided data should beused as the initial subset. If indices,
the provided array should be one-dimensional. If a group of examples,
the data should be 2 dimensional. Default is None.
optimizer : string or optimizers.BaseOptimizer, optional
The optimization approach to use for the selection. Default is
'two-stage', which makes selections using the naive greedy algorithm
initially and then switches to the lazy greedy algorithm. Must be
one of
'random' : randomly select elements (dummy optimizer)
'modular' : approximate the function using its modular upper bound
'naive' : the naive greedy algorithm
'lazy' : the lazy (or accelerated) greedy algorithm
'approximate-lazy' : the approximate lazy greedy algorithm
'two-stage' : starts with naive and switches to lazy
'stochastic' : the stochastic greedy algorithm
'sample' : randomly take a subset and perform selection on that
'greedi' : the GreeDi distributed algorithm
'bidirectional' : the bidirectional greedy algorithm
Default is 'two-stage'.
optimizer_kwds : dict or None
A dictionary of arguments to pass into the optimizer object. The keys
of this dictionary should be the names of the parameters in the optimizer
and the values in the dictionary should be the values that these
parameters take. Default is None.
n_neighbors : int or None
When constructing a similarity matrix, the number of nearest neighbors
whose similarity values will be kept. The result is a sparse similarity
matrix which can significantly speed up computation at the cost of
accuracy. Default is None.
reservoir : numpy.ndarray or None
The reservoir to use when calculating gains in the sieve greedy
streaming optimization algorithm in the `partial_fit` method.
Currently only used for graph-based functions. If a numpy array
is passed in, it will be used as the reservoir. If None is passed in,
will use reservoir sampling to collect a reservoir. Default is None.
max_reservoir_size : int
The maximum size that the reservoir can take. If a reservoir is passed
in, this value is set to the size of that array. Default is 1000.
n_jobs : int
The number of threads to use when performing computation in parallel.
Currently, this parameter is exposed but does not actually do anything.
This will be fixed soon.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process. Only used
for stochastic greedy.
verbose : bool
Whether to print output during the selection process.
Attributes
----------
n_samples : int
The number of samples to select.
pairwise_func : callable
A function that takes in a data matrix and converts it to a square
symmetric matrix.
ranking : numpy.array int
The selected samples in the order of their gain.
gains : numpy.array float
The gain of each sample in the returned set when it was added to the
growing subset. The first number corresponds to the gain of the first
added sample, the second corresponds to the gain of the second added
sample, and so forth.
"""
def __init__(self, n_samples=10, metric='euclidean',
initial_subset=None, optimizer='two-stage', n_neighbors=None,
reservoir=None, max_reservoir_size=1000, n_jobs=1,
random_state=None, optimizer_kwds={}, verbose=False):
super(SumRedundancySelection, self).__init__(n_samples=n_samples,
metric=metric, initial_subset=initial_subset, optimizer=optimizer,
n_neighbors=n_neighbors, reservoir=reservoir,
max_reservoir_size=max_reservoir_size, n_jobs=n_jobs,
random_state=random_state, optimizer_kwds=optimizer_kwds,
verbose=verbose)
def fit(self, X, y=None, sample_weight=None, sample_cost=None):
"""Run submodular optimization to select the examples.
This method is a wrapper for the full submodular optimization process.
It takes in some data set (and optionally labels that are ignored
during this process) and selects `n_samples` from it in the greedy
manner specified by the optimizer.
This method will return the selector object itself, not the transformed
data set. The `transform` method will then transform a data set to the
selected points, or alternatively one can use the ranking stored in
the `self.ranking` attribute. The `fit_transform` method will perform
both optimization and selection and return the selected items.
Parameters
----------
X : list or numpy.ndarray, shape=(n, d)
The data set to transform. Must be numeric.
y : list or numpy.ndarray or None, shape=(n,), optional
The labels to transform. If passed in this function will return
both the data and th corresponding labels for the rows that have
been selected.
sample_weight : list or numpy.ndarray or None, shape=(n,), optional
The weight of each example. Currently ignored in apricot but
included to maintain compatibility with sklearn pipelines.
sample_cost : list or numpy.ndarray or None, shape=(n,), optional
The cost of each item. If set, indicates that optimization should
be performed with respect to a knapsack constraint.
Returns
-------
self : SumRedundancySelection
The fit step returns this selector object.
"""
return super(SumRedundancySelection, self).fit(X, y=y,
sample_weight=sample_weight, sample_cost=sample_cost)
def _initialize(self, X_pairwise, idxs=None):
super(SumRedundancySelection, self)._initialize(X_pairwise, idxs=idxs)
idxs = idxs if idxs is not None else numpy.arange(X_pairwise.shape[0])
for i, idx in enumerate(idxs):
self.current_values[i] = X_pairwise[idx, idx]
if self.initial_subset is None:
return
elif self.initial_subset.ndim == 2:
raise ValueError("When using saturated coverage, the initial subset"\
" must be a one dimensional array of indices.")
elif self.initial_subset.ndim == 1:
if not self.sparse:
for i in self.initial_subset:
self.current_values += X_pairwise[i] * 2
else:
for i in self.initial_subset:
self.current_values += X_pairwise[i].toarray()[0] * 2
else:
raise ValueError("The initial subset must be either a two dimensional" \
" matrix of examples or a one dimensional mask.")
def _calculate_gains(self, X_pairwise, idxs=None):
idxs = idxs if idxs is not None else self.idxs
return -self.current_values[idxs]
def _select_next(self, X_pairwise, gain, idx):
"""This function will add the given item to the selected set."""
if self.sparse:
self.current_values += X_pairwise.toarray()[0] * 2
else:
self.current_values += X_pairwise * 2
super(SumRedundancySelection, self)._select_next(
X_pairwise, gain, idx)
jmschrei-apricot-bf86e69/apricot/optimizers.py 0000664 0000000 0000000 00000112033 14145540156 0021635 0 ustar 00root root 0000000 0000000 # optimizers.py
# Author: Jacob Schreiber
import os
import copy
import numpy
import scipy
from tqdm import tqdm
from .utils import PriorityQueue
from .utils import check_random_state
from .utils import _calculate_pairwise_distances
class BaseOptimizer(object):
"""An approach for optimizing submodular functions.
This object contains a single method, `select`, that implements an
algorithm for optimizing submodular functions. This method will take in the
data set, the number of examples to select, and a stored reference to the
submodular function to be optimized, and yield a subset of examples. The
method operates on the gains provided in the `_calculate_gains` method from
the submodular function object and stores examples using the `_select_next`
method.
Parameters
----------
function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection or None
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized. If None, will be set by the selector when passed in.
self.verbose : bool
Whether to display a progress bar during the optimization process.
"""
def __init__(self, function=None, random_state=None, n_jobs=None,
verbose=False):
self.function = function
self.random_state = check_random_state(random_state)
self.n_jobs = n_jobs
self.verbose = verbose
self.gains_ = None
def select(self, X, k):
raise NotImplementedError
class NaiveGreedy(BaseOptimizer):
"""The naive greedy algorithm for optimization.
The naive greedy algorithm is the simplest greedy approach for optimizing
submodular functions. The approach simply iterates through each example in
the ground set that has not already been selected and calculates the gain
in function value that would result from adding that example to the
selected set. This process is embarassingly parallel and so is extremely
amenable both to parallel processing and distributed computing. Further,
because it is conceptually simple, it is also simple to implement.
The naive greedy algorithm can be specified for any function by passing in
`optimizer='naive'` to the relevant selector object. Here is an example of
specifying the naive greedy algorithm for optimizing a feature-based function.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='naive')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, random_state=None, n_jobs=None,
verbose=False):
super(NaiveGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
cost = 0.0
while cost < k:
gains = self.function._calculate_gains(X)
if sample_cost is None:
idx = numpy.argmax(gains)
best_idx = self.function.idxs[idx]
idx_cost = 1
else:
idxs = numpy.lexsort((numpy.arange(gains.shape[0]), -gains / sample_cost[self.function.idxs]))
for idx in idxs:
best_idx = self.function.idxs[idx]
idx_cost = sample_cost[best_idx]
if cost + idx_cost <= k:
break
else:
break
cost += idx_cost
gain = gains[idx]
self.function._select_next(X[best_idx], gain, best_idx)
if self.verbose == True:
self.function.pbar.update(round(idx_cost, 1))
class LazyGreedy(BaseOptimizer):
"""The lazy/accelerated greedy algorithm for optimization.
The lazy (or accelerated) greedy algorithm is a fast alternate to the
naive greedy algorithm that results in the same items being selected.
This algorithm exploits the diminishing returns property of submodular
functions in order to avoid re-evaluating examples that are known to
provide little gain. If an example has a small gain relative to other
examples, it is unlikely to be the next selected example because that
gain can only go down as more items are selected. Thus, the example
should only be re-evaluated once the gains of other examples have gotten
smaller.
The key idea of the lazy greedy algorithm is to maintain a priority queue
where the examples are the elements in the queue and the priorities are
the gains the last time they were evaluated. The algorithm has two steps.
The first step is to calculate the gain that each example would have if
selected first (the modular upper bound) and populate the priority queue
using these values. The second step is to recalculate the gain of the
first example in the priority queue and then add the example back into
the queue. If the example remains at the front of the queue it is selected
because no other example could have a larger gain once re-evaluated (due
to the diminishing returns property).
While the worst case time complexity of this algorithm is the same as the
naive greedy algorithm, in practice it can be orders of magnitude faster.
Empirically, it appears to accelerate graph-based functions much more
than it does feature-based ones. Functions also seem to be more
accelerated the more curved they are.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='lazy')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.pq : utils.PriorityQueue
The priority queue used to order examples for evaluation.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, random_state=None, n_jobs=None,
verbose=False):
super(LazyGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
cost = 0.0
if sample_cost is None:
sample_cost = numpy.ones(X.shape[0], dtype='float64')
gains = self.function._calculate_gains(X) / sample_cost[self.function.idxs]
self.pq = PriorityQueue(self.function.idxs, -gains)
while cost < k:
best_gain = float("-inf")
best_idx = None
while True:
if len(self.pq.pq) == 0:
return
prev_gain, idx = self.pq.pop()
#prev_gain, idx = self.pq.peek()
prev_gain = -prev_gain
if cost + sample_cost[idx] > k:
continue
if best_idx == idx:
break
idxs = numpy.array([idx])
gain = self.function._calculate_gains(X, idxs)[0] / sample_cost[idx]
#self.pq.swap(idx, -gain)
self.pq.add(idx, -gain)
if gain > best_gain:
best_gain = gain
best_idx = idx
elif gain == best_gain and best_gain == 0.0:
best_gain = gain
best_idx = idx
break
cost += sample_cost[best_idx]
best_gain *= sample_cost[best_idx]
self.function._select_next(X[best_idx], best_gain, best_idx)
if self.verbose == True:
self.function.pbar.update(1)
class ApproximateLazyGreedy(BaseOptimizer):
"""The approximate lazy/accelerated greedy algorithm for optimization.
The approximate lazy greedy algorithm is a simple extension of the lazy
greedy algorithm that, rather than requiring that an element remains at
the top of the priority queue after being re-evaluated, only requires
that the gain is within a certain user-defined percentage of the best
gain to be selected. The key point in this approach is that finding the
very best element while maintaining the priority queue may be expensive,
but finding elements that are good enough is simple. While the best
percentage to use is data set specific, even values near 1 can lead to
large savings in computation.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='approximate-lazy')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.pq : utils.PriorityQueue
The priority queue used to order examples for evaluation.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, beta=0.9, random_state=None,
n_jobs=None, verbose=False):
self.beta = beta
super(ApproximateLazyGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
cost = 0.0
if sample_cost is None:
sample_cost = numpy.ones(X.shape[0], dtype='float64')
gains = self.function._calculate_gains(X) / sample_cost[self.function.idxs]
self.pq = PriorityQueue(self.function.idxs, -gains)
while cost < k:
while True:
if len(self.pq.pq) == 0:
return
prev_gain, idx = self.pq.pop()
prev_gain = -prev_gain
if cost + sample_cost[idx] > k:
continue
idxs = numpy.array([idx])
gain = self.function._calculate_gains(X, idxs)[0] / sample_cost[idx]
if gain >= self.beta * prev_gain:
best_gain = gain
best_idx = idx
break
else:
self.pq.add(idx, -gain)
cost += sample_cost[best_idx]
best_gain *= sample_cost[best_idx]
self.function._select_next(X[best_idx], best_gain, best_idx)
if self.verbose == True:
self.function.pbar.update(1)
class TwoStageGreedy(BaseOptimizer):
"""An approach that switches between two optimizers midway through.
The two-stage greedy optimizer is a general purpose framework for combining
two optimizers by making the first :math:`n` selections out of :math:`k`
total selections using one optimizer, and then making the remainder using
the other. When the first optimizer is random selection and the second
approach is naive/lazy greedy, this becomes partial enumeration. By
default, the first algorithm is the naive greedy optimizer and the second
algorithm is the lazy greedy. This combination results in the same
selection as either optimizer individually but replaces the
computationally intensive first few steps for the priority queue, where
the algorithm may require scanning through almost the entire queue, with
the parallelizable naive greedy algorithm. While, in theory, the lazy
greedy algorithm will never perform more function calls than the naive
greedy algorithm, there are costs associated both with maintaining a
priority queue and with evaluating a single example instead of a batch
of examples.
This optimizer, with the naive greedy optimizer first and the lazy greedy
optimizer second, is the default optimizer for apricot selectors.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='two-stage')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.n_first_selections : int
The number of selections to perform using the naive greedy algorithm
before populating the priority queue and using the lazy greedy
algorithm.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
"""
def __init__(self, function=None, n_first_selections=10,
optimizer1='naive', optimizer2='lazy', random_state=None,
n_jobs=None, verbose=False):
self.n_first_selections = n_first_selections
self.optimizer1 = optimizer1
self.optimizer2 = optimizer2
super(TwoStageGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
if isinstance(self.optimizer1, str):
optimizer1 = OPTIMIZERS[self.optimizer1](function=self.function,
verbose=self.verbose)
elif not isinstance(self.optimizer1, BaseOptimizer):
raise ValueError("optimizer1 must be either a string or an " /
"optimized object.")
else:
optimizer1 = self.optimizer1
if isinstance(self.optimizer2, str):
optimizer2 = OPTIMIZERS[self.optimizer2](function=self.function,
verbose=self.verbose)
elif not isinstance(self.optimizer2, BaseOptimizer):
raise ValueError("optimizer1 must be either a string or an " /
"optimized object.")
else:
optimizer2 = self.optimizer2
optimizer1.select(X, min(self.n_first_selections, k), sample_cost=sample_cost)
if k > self.n_first_selections:
m = k - self.n_first_selections
optimizer2.select(X, m, sample_cost=sample_cost)
class StochasticGreedy(BaseOptimizer):
"""The stochastic greedy algorithm for optimization.
The stochastic greedy algorithm is a simple approach that, for each
iteration, randomly selects a subset of data and then finds the best next
example within that subset. The distinction between this approach and the
sample greedy algorithm is that this subset changes at each iteration,
meaning that the algorithm does cover the entire data set. In contrast,
the sample greedy algorithm is equivalent to manually subsampling the data
before running a selector on it. The size of this subset is proportional
to the number of examples that are chosen and determined in a manner that
results in the same amount of computation being done no matter how many
elements are selected. A key idea from this approach is that, while the
exact ranking of the elements may differ from the naive/lazy greedy
approaches, the set of selected elements is likely to be similar despite
the limited amount of computation.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='stochastic')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
epsilon : float, optional
The inverse of the sampling probability of any particular point being
included in the subset, such that 1 - epsilon is the probability that
a point is included. Default is 0.9.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, epsilon=0.9, random_state=None,
n_jobs=None, verbose=False):
self.epsilon = epsilon
super(StochasticGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
cost = 0.0
if sample_cost is None:
sample_cost = numpy.ones(X.shape[0], dtype='float64')
n = X.shape[0]
subset_size = -numpy.log(self.epsilon) * n / k
subset_size = max(int(subset_size), 1)
while cost < k:
idxs = self.random_state.choice(self.function.idxs,
replace=False, size=min(subset_size,
self.function.idxs.shape[0]))
gains = self.function._calculate_gains(X, idxs) / sample_cost[idxs]
idxs_ = numpy.lexsort((numpy.arange(gains.shape[0]), -gains))
for idx in idxs_:
best_idx = idxs[idx]
if cost + sample_cost[best_idx] <= k:
break
else:
return
cost += sample_cost[best_idx]
self.function._select_next(X[best_idx], gains[idx], best_idx)
if self.verbose == True:
self.function.pbar.update(1)
class SampleGreedy(BaseOptimizer):
"""The sample greedy algorithm for optimization.
The sample greedy algorithm is a simple approach that subsamples the full
data set with a user-defined sampling probability and then runs an
optimization on that subset. This subsampling can lead to obvious speed
improvements because fewer elements as selected, but will generally find
a lower quality subset because fewer elements are present. This approach
is typically used a baseline for other approaches but can save a lot of
time on massive data sets that are known to be highly redundant.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='sample')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
epsilon : float, optional
The sampling probability to use when constructing the subset. A
subset of size n * epsilon will be selected from.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, epsilon=0.9, optimizer='lazy',
random_state=None, n_jobs=None, verbose=False):
self.epsilon = epsilon
self.optimizer = optimizer
super(SampleGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
n = X.shape[0]
subset_size = max(int(n * self.epsilon), 1)
idxs = self.random_state.choice(self.function.idxs,
replace=False, size=n - subset_size)
if isinstance(self.optimizer, str):
optimizer = OPTIMIZERS[self.optimizer](function=self.function,
verbose=self.verbose)
elif not isinstance(self.optimizer, BaseOptimizer):
raise ValueError("optimizer must be either a string or an " /
"optimizer object.")
else:
optimizer = self.optimizer
self.function.mask[idxs] = 1
self.function.idxs = numpy.where(self.function.mask == 0)[0]
optimizer.select(X, k, sample_cost=sample_cost)
class GreeDi(BaseOptimizer):
"""An approach for optimizing submodular functions on massive data sets.
GreeDi is an optimizer that was designed to work on data sets that are
too large to fit into memory. The approach involves first partitioning
the data into :math:`m` equally sized chunks without any overlap. Then,
:math:`l` elements are selected from each chunk using a standard optimizer
like naive or lazy greedy. Finally, these :math:`ml` examples are merged
and a standard optimizer selects :math:`k` examples from this set. In
this manner, the algorithm sacrifices exactness to ensure that memory
limitations are not an issue.
There are a few considerations to keep in mind when using GreeDi.
Naturally, :math:`ml` must both be larger than :math:`k` and also small
enough to fit into memory. The larger :math:`l`, the closer the solution
is to the exact solution but also the more compute time is required.
Conversely, the larger :math:`m` is, the less exact the solution is.
When using a graph-based function, increasing :math:`m` can dramatically
reduce the amount of computation that needs to be performed, as well as
the memory requirements, because the similarity matrix becomes smaller
in size. However, feature-based functions are likely to see less of a
speed improvement because the cost of evaluating an example is independent
of the size of ground set.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='greedi')
selector.fit(X)
Parameters
----------
function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
m : int
The number of partitions to split the data into.
l : int
The number of exemplars to select from each partition. l*m must
be larger than the number of exemplars k that will be selected
later on.
optimizer1 : str or base.Optimizer, optional
The optimizer to use in the first stage of the process where l
exemplars are selected from each partition. Default is 'lazy'.
optimizer2 : str or base.Optimizer, optional
The optimizer to use in the second stage where k exemplars are
selected from the l*m exemplars returned from the first stage.
Default is 'lazy'.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process.
verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection or None
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized. If None, will be set by the selector when passed in.
self.m : int
The number of partitions that the data will be split into.
self.l : int
The number of exemplars that will be selected from each partition.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, m=None, l=None, optimizer1='lazy',
optimizer2='lazy', random_state=None, n_jobs=None, verbose=False):
self.m = m
self.l = l
self.optimizer1 = optimizer1
self.optimizer2 = optimizer2
super(GreeDi, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
if self.m is None and self.l is None:
self.m = 8
self.l = min(k // 4, X.shape[0] // self.m)
if sample_cost is None:
sample_cost = numpy.ones(X.shape[0], dtype='float64')
if k > (self.m * self.l):
raise ValueError("k must be smaller than m * l")
partitions = numpy.arange(X.shape[0]) % self.m
self.random_state.shuffle(partitions)
optimizer1 = OPTIMIZERS[self.optimizer1](function=self.function,
verbose=self.verbose)
optimizer2 = OPTIMIZERS[self.optimizer2](function=self.function,
verbose=self.verbose)
rankings = []
for i in range(self.m):
idxs = numpy.where(partitions == i)[0]
X_subset = X[idxs]
if X.shape[0] == X.shape[1]:
X_subset = X_subset[:, idxs]
self.function._initialize(X_subset)
optimizer1.select(X_subset, self.l, sample_cost=sample_cost[idxs])
rankings.append(idxs[self.function.ranking])
rankings = numpy.concatenate(rankings)
if self.verbose:
self.function.pbar.close()
self.function.pbar = tqdm(total=k)
X_subset = X[rankings]
if X.shape[0] == X.shape[1]:
X_subset = X_subset[:, rankings]
self.function._initialize(X_subset)
optimizer2.select(X_subset, k, sample_cost=sample_cost[rankings])
rankings = rankings[self.function.ranking]
self.function._initialize(X)
for idx in rankings:
gain = self.function._calculate_gains(X, numpy.array([idx]))[0]
self.function._select_next(X[idx], gain, idx)
class RandomGreedy(BaseOptimizer):
"""The naive greedy algorithm for optimization.
This optimization approach is the naive greedy algorithm. At each iteration
of selection it will simply calculate the gain one would get from adding
each example, and then will select the example that has the highest gain.
This algorithm is conceptually simple and easy to parallelize and put on a
GPU, but can be slower than other alternatives because it involves
repeatedly evaluating examples that are not likely to be selected next.
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
"""
def __init__(self, function=None, random_state=None, n_jobs=None,
verbose=False):
super(NaiveGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
cost = 0.0
if sample_cost is None:
sample_cost = numpy.ones(X.shape[0], dtype='float64')
i = 0
while cost < k:
idx = self.random_state.choice(self.random.idxs)
if cost + sample_cost[idx] > k:
continue
cost += sample_cost[idx]
gain = self.function._calculate_gains(X, idxs=numpy.array([idx]))[0]
self.function._select_next(X[idx], gain, idx)
if self.verbose == True:
self.function.pbar.update(round(sample_cost[idx], 2))
class ModularGreedy(BaseOptimizer):
"""An approach that approximates a submodular function as modular.
This approach approximates the submodular function by using its modular
upper-bounds to do the selection. Essentially, a defining characteristic
of submodular functions is the *diminishing returns* property where the
gain of an example decreases with the number of selected examples. In
contrast, modular functions have constant gains for examples regardless
of the number of selected examples. Thus, approximating the submodular
function as a modular function can serve as an upper-bound to the gain
for each example during the selection process. This approximation makes
the function simple to optimize because one would simply calculate the
gain that each example yields before any examples are selected, sort the
examples by this gain, and select the top :math:`k` examples. While this
approach is fast, this approach is likely best paired with a traditional
optimization algorithm after the first few examples are selected.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='modular')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
epsilon : float, optional
The inverse of the sampling probability of any particular point being
included in the subset, such that 1 - epsilon is the probability that
a point is included. Default is 0.9.
random_state : int or RandomState or None, optional
The random seed to use for the random selection process.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, random_state=None, n_jobs=None,
verbose=False):
super(ModularGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
"""Select elements in a naive greedy manner."""
cost = 0.0
if sample_cost is None:
sample_cost = numpy.ones(X.shape[0], dtype='float64')
gains = self.function._calculate_gains(X) / sample_cost[self.function.idxs]
idxs = gains.argsort()[::-1]
for idx in idxs:
if cost + sample_cost[idx] > k:
continue
cost += sample_cost[idx]
gain = self.function._calculate_gains(X, idxs=numpy.array([idx]))[0]
self.function._select_next(X[idx], gain, idx)
if self.verbose == True:
self.function.pbar.update(1)
class BidirectionalGreedy(BaseOptimizer):
"""The bidirectional greedy algorithm.
Most submodular optimizers assume that the function is *monotone*, i.e.,
that the gain from each successive example is positive. However, there
are some cases where the key diminishing returns property holds, but
the gains are not necessarily positive. The most obvious of these is a
difference in submodular functions. In these cases, the naive greedy
algorithm is not guaranteed to return a good result.
The bidirectional greedy algorithm was developed to optimize non-monotone
submodular functions. While it has a guarantee that is lower than the
naive greedy algorithm has for monotone functions, it generally returns
better sets than the greedy algorithm.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='bidirectional')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, random_state=None, n_jobs=None,
verbose=False):
super(BidirectionalGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
"""Select elements in a naive greedy manner."""
cost = 0.0
if sample_cost is None:
sample_cost = numpy.ones(X.shape[0], dtype='float64')
A = numpy.zeros(X.shape[0], dtype=bool)
B = numpy.ones(X.shape[0], dtype=bool)
idxs = numpy.arange(X.shape[0])
self.random_state.shuffle(idxs)
gains = numpy.zeros(X.shape[0])
while True:
for i in idxs:
if A[i] == True:
continue
self.function.initial_subset = A
self.function._initialize(X)
gain_a = self.function._calculate_gains(X[i])
B[i] = False
self.function.initial_subset = B
self.function._initialize(X)
gain_b = -self.function._calculate_gains(X[i])
B[i] = True
if gain_a == gain_b == 0.0:
p = 0.5
else:
p = gain_a / (gain_a + gain_b)
self.gains_[i] = p
if self.random_state.uniform(0, 1) <= p:
A[i] = True
self.function._select_next(X[i], gain_a, i)
if A.sum() == k:
return
else:
B[i] = False
class SieveGreedy(BaseOptimizer):
"""The sieve stream greedy algorithm.
Most submodular optimizers assume that the function is *monotone*, i.e.,
that the gain from each successive example is positive. However, there
are some cases where the key diminishing returns property holds, but
the gains are not necessarily positive. The most obvious of these is a
difference in submodular functions. In these cases, the naive greedy
algorithm is not guaranteed to return a good result.
The bidirectional greedy algorithm was developed to optimize non-monotone
submodular functions. While it has a guarantee that is lower than the
naive greedy algorithm has for monotone functions, it generally returns
better sets than the greedy algorithm.
.. code::python
from apricot import FeatureBasedSelection
X = numpy.random.randint(10, size=(10000, 100))
selector = FeatureBasedSelection(100, 'sqrt', optimizer='bidirectional')
selector.fit(X)
Parameters
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
Attributes
----------
self.function : base.BaseSelection
A submodular function that implements the `_calculate_gains` and
`_select_next` methods. This is the function that will be
optimized.
self.verbose : bool
Whether to display a progress bar during the optimization process.
self.gains_ : numpy.ndarray or None
The gain that each example would give the last time that it was
evaluated.
"""
def __init__(self, function=None, epsilon=0.01, random_state=None,
n_jobs=None, verbose=False):
self.epsilon = epsilon
self.n_seen_ = 0
self.thresholds = [1]
self.max_gain = -1
super(SieveGreedy, self).__init__(function=function,
random_state=random_state, n_jobs=n_jobs, verbose=verbose)
def select(self, X, k, sample_cost=None):
"""Select elements in a naive greedy manner."""
# This is not a great use of the apricot API. An optimizer shouldn't
# be using its own special gain calculating function. However, it is
# much faster to do things this way so I'll keep it in for now.
n, d = X.shape
if sample_cost is None:
sample_cost = numpy.ones(n, dtype='float64')
r = X.shape[1] if self.function.reservoir is not None else 1
marginal_gains = self.function._calculate_gains(X) / sample_cost / r
max_marginal_gain = marginal_gains.max()
if max_marginal_gain > self.max_gain:
self.max_gain = max_marginal_gain
j = len(self.thresholds)
threshold = (1 + self.epsilon) ** j - 1
while threshold <= self.max_gain * k:
self.thresholds.append(threshold)
j += 1
threshold = (1 + self.epsilon) ** j - 1
thresholds = numpy.array(self.thresholds, dtype='float64')
idxs = numpy.arange(n, dtype='int64') + self.n_seen_
self.function._calculate_sieve_gains(X, thresholds, idxs)
best_idx = numpy.argmax(self.function.sieve_total_gains_)
ranking = self.function.sieve_selections_[best_idx]
ranking = ranking[:self.function.sieve_n_selected_[best_idx]]
gain = self.function.sieve_gains_[best_idx]
gain = gain[:self.function.sieve_n_selected_[best_idx]]
for i in range(len(self.thresholds)):
for j in range(k):
m = self.function.sieve_selections_[i, j]
if m >= self.n_seen_:
if isinstance(self.function._X, scipy.sparse.csr_matrix):
self.function.sieve_subsets_[i, j] = self.function._X[m - self.n_seen_].toarray()[0]
else:
self.function.sieve_subsets_[i, j] = self.function._X[m - self.n_seen_]
self.function.ranking = ranking
self.function.gains = gain
self.function.subset = self.function.sieve_subsets_[best_idx]
self.n_seen_ += X.shape[0]
OPTIMIZERS = {
'random' : RandomGreedy,
'modular' : ModularGreedy,
'naive' : NaiveGreedy,
'lazy' : LazyGreedy,
'approximate-lazy' : ApproximateLazyGreedy,
'two-stage' : TwoStageGreedy,
'stochastic' : StochasticGreedy,
'sample' : SampleGreedy,
'greedi' : GreeDi,
'bidirectional' : BidirectionalGreedy,
'sieve' : SieveGreedy,
}
jmschrei-apricot-bf86e69/apricot/utils.py 0000664 0000000 0000000 00000016577 14145540156 0020610 0 ustar 00root root 0000000 0000000 # utils.py
# Author: Jacob Schreiber
"""
This code contains utility functions to support the main functionality of
the code.
"""
import numbers
import numpy
import itertools
from heapq import heappush
from heapq import heappop
from heapq import heapify
from heapq import heapreplace
from scipy.sparse import csr_matrix
from sklearn.metrics import pairwise_distances
from sklearn.neighbors import KNeighborsTransformer
class PriorityQueue(object):
"""A priority queue implementation.
This is an implemented of a priority queue using a heap implemtnation. It
has O(log n) time for adding and popping elements but constant time
deletion. It is based off of the implementation that is provided at
https://docs.python.org/2/library/heapq.html
This implementation assumes that the items with the highest priority
are the ones with the lowest "weight" that gets passed in. If higher
weights are supposed to correspond to higher priority, consider reversing
the sign of the weight.
Parameters
----------
None
Attributes
----------
pq : list
A list containing the heapified priority queue.
lookup : dict
A dictionary containing links to each of the elements, allowing
constant time lookups and deletions.
"""
def __init__(self, items=None, weights=None):
self.counter = itertools.count()
self.pq = []
if items is not None and weights is not None:
for item, weight in zip(items, weights):
entry = [weight, next(self.counter), item]
self.pq.append(entry)
heapify(self.pq)
def add(self, item, weight):
"""Add an element to the priority queue. Runtime is O(log n).
This adds in an element to the priority queue. If that element is
already there it will automatically delete the previous instance
and replace it with the new instance.
Parameters
----------
item : object
The object to be encoded.
weight : double
The priority of the item. The lower the weight the higher the
priority when items get dequeued. If a higher weight is supposed
to correspond to a higher priority, consider reversing the sign of
the weight.
Returns
-------
None
"""
#if item in self.lookup:
# self.remove(item)
entry = [weight, next(self.counter), item]
heappush(self.pq, entry)
def pop(self):
"""Pop the highest priority element from the queue. Runtime is O(log n).
This will remove the highest priority element from the queue. If there
are no elements left in the queue it will raise an error.
Parameters
----------
None
Returns
-------
weight : double
The weight of the element as passed in in the `add` method
item : object
The item that was passed in in the `add` method
"""
weight, _, item = heappop(self.pq)
return weight, item
def peek(self):
"""Peek at the first element in the priority queue.
Parameters
----------
None
Returns
-------
weight : double
The weight of the element as passed in in the `add` method
item : object
The item that was passed in in the `add` method
"""
return self.pq[0][0], self.pq[0][2]
def swap(self, item, weight):
"""An efficient way to pop the first element and add a new element.
This is useful in our context because it allows us to pop the smallest
element and to add a new element, i.e., remove an element and re-add it
with the updated gain.
Parameters
----------
item : object
The object to be encoded.
weight : double
The priority of the item. The lower the weight the higher the
priority when items get dequeued. If a higher weight is supposed
to correspond to a higher priority, consider reversing the sign of
the weight.
Returns
-------
None
"""
entry = [weight, next(self.counter), item]
heapreplace(self.pq, entry)
def check_random_state(seed):
"""Turn seed into a np.random.RandomState instance.
This function will check to see whether the input seed is a valid seed
for generating random numbers. This is a slightly modified version of
the code from sklearn.utils.validation.
Parameters
----------
seed : None | int | instance of RandomState
If seed is None, return the RandomState singleton used by np.random.
If seed is an int, return a new RandomState instance seeded with seed.
If seed is already a RandomState instance, return it.
Otherwise raise ValueError.
"""
if seed is None or seed is numpy.random:
return numpy.random.mtrand._rand
if isinstance(seed, (numbers.Integral, numpy.integer)):
return numpy.random.RandomState(seed)
if isinstance(seed, numpy.random.RandomState):
return seed
raise ValueError('%r cannot be used to seed a numpy.random.RandomState'
' instance' % seed)
def _calculate_pairwise_distances(X, Y=None, metric='precomputed', n_neighbors=None):
if metric in ('precomputed', 'ignore'):
return X
if n_neighbors is None:
if metric == 'euclidean':
X_pairwise = pairwise_distances(X, Y=Y, metric=metric, squared=True)
elif metric == 'correlation' or metric == 'cosine':
# An in-place version of:
# X_pairwise = 1 - (1 - pairwise_distances(X, metric=metric)) ** 2
X_pairwise = pairwise_distances(X, Y=Y, metric=metric)
X_pairwise = numpy.subtract(1, X_pairwise, out=X_pairwise)
X_pairwise = numpy.square(X_pairwise, out=X_pairwise)
X_pairwise = numpy.subtract(1, X_pairwise, out=X_pairwise)
else:
X_pairwise = pairwise_distances(X, Y=Y, metric=metric)
else:
if metric == 'correlation' or metric == 'cosine':
# An in-place version of:
# X = 1 - (1 - pairwise_distances(X, metric=metric)) ** 2
X = pairwise_distances(X, Y=Y, metric=metric)
X = numpy.subtract(1, X, out=X)
X = numpy.square(X, out=X)
X = numpy.subtract(1, X, out=X)
metric = 'precomputed'
if isinstance(n_neighbors, int):
X_pairwise = KNeighborsTransformer(
n_neighbors=n_neighbors, metric=metric
).fit_transform(X)
elif isinstance(n_neighbors, KNeighborsTransformer):
X_pairwise = n_neighbors.fit_transform(X)
if metric == 'correlation' or metric == 'cosine':
if isinstance(X_pairwise, csr_matrix):
X_pairwise.data = numpy.subtract(1, X_pairwise.data,
out=X_pairwise.data)
else:
X_pairwise = numpy.subtract(1, X_pairwise,
out=X_pairwise)
else:
if isinstance(X_pairwise, csr_matrix):
X_pairwise.data = numpy.subtract(X_pairwise.max(),
X_pairwise.data, out=X_pairwise.data)
else:
X_pairwise = numpy.subtract(X_pairwise.max(), X_pairwise,
out=X_pairwise)
return X_pairwise jmschrei-apricot-bf86e69/dev-requirements.txt 0000664 0000000 0000000 00000000126 14145540156 0021454 0 ustar 00root root 0000000 0000000 numpy >= 1.14.2
scipy >= 1.0.0
numba >= 0.43
tqdm >= 4.24.
scikit-learn >= 0.20.0
nose jmschrei-apricot-bf86e69/docs/ 0000775 0000000 0000000 00000000000 14145540156 0016345 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/docs/Makefile 0000664 0000000 0000000 00000001172 14145540156 0020006 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
jmschrei-apricot-bf86e69/docs/_templates/ 0000775 0000000 0000000 00000000000 14145540156 0020502 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/docs/_templates/layout.html 0000664 0000000 0000000 00000000461 14145540156 0022706 0 ustar 00root root 0000000 0000000 {% extends "!layout.html" %}
{% block footer %} {{ super() }}
{% endblock %} jmschrei-apricot-bf86e69/docs/conf.py 0000664 0000000 0000000 00000006243 14145540156 0017651 0 ustar 00root root 0000000 0000000 # Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'apricot'
copyright = '2020, Jacob Schreiber'
author = 'Jacob Schreiber'
# The full version, including alpha/beta/rc tags
release = 'v0.5.0'
master_doc = 'index'
# -- General configuration ---------------------------------------------------
import sys
import os
import subprocess
#subprocess.call('pip install numpydoc', shell=True)
subprocess.call('pip install sklearn', shell=True)
sys.path.insert(0, os.path.abspath('../../apricot'))
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.doctest',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.mathjax',
'sphinx.ext.viewcode',
#'numpydoc'
'sphinx.ext.napoleon'
]
autosummary_generate = True
numpydoc_show_class_members = False
class_members_toctree = False
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
import apricot
# The short X.Y version.
# If you really want the short one use the next line.
# version = '.'.join(pomegranate.__version__.split('.')[0:2])
# Use this version if you want the full version string in the document
version = apricot.__version__
# The full version, including alpha/beta/rc tags.
release = apricot.__version__
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] jmschrei-apricot-bf86e69/docs/features/ 0000775 0000000 0000000 00000000000 14145540156 0020163 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/docs/features/knapsack.rst 0000664 0000000 0000000 00000003445 14145540156 0022516 0 ustar 00root root 0000000 0000000 .. _features.knapsack:
Knapsack Constraints
====================
A knapsack constraint is an alternate to a cardinality constraint (the default in apricot) where each element has a cost and the selected items cannot exceed a total budget. The name (according to `Krause (2012) `_) is a reference to the knapsack problem where one maximizes a modular function subject to a modular cost. In apricot, one is maximizing a *submodular* function subject to a modular cost. There are several ideas that costs might represent but the most intuitive is the simple monetary cost associated with each element. Consider the task of placing sensors around an area given a fixed budget to purchase sensors, where smaller sensors are cheaper than larger sensors. Submodular optimization subject to knapsack constraints is an intuitive way to determine what type of sensor should be placed and where without exceeding a given budget.
Stated more formally, the optimization problem given knapsack constraints is
.. math::
\max\limits_{S} f(S) \enspace s.t. \sum\limits_{v \in S} c(v) \leq B
for a cost function :math:`c`, a submodular function :math:`f`, a subset :math:`S`, and a budget :math:`B`. Further, rather than optimizing the gain directly, the gain is divided by the cost of the element, resulting in the "cost-benefit" gain. Essentially, elements with a high gain and a high cost are prioritized lower than those with the same gain but a lower cost. This means that the optimization process is just a
.. code-block:: python
from apricot import FeatureBasedSelection
X = numpy.random.randint(2, size=(10000, 100), p=[0.99, 0.01])
sample_cost = numpy.abs(numpy.random.randn(10000))
selector = FeatureBasedSelection(100, 'sqrt')
selector.fit(X, sample_cost=sample_cost)
jmschrei-apricot-bf86e69/docs/features/sparse.rst 0000664 0000000 0000000 00000002712 14145540156 0022214 0 ustar 00root root 0000000 0000000 .. _features.sparse:
Sparse Matrices
===============
apricot has built-in support for sparse matrices. This support makes it possible to summarize massive data sets whose dense representations are not feasible to work with, e.g. sparse similarity graphs when using graph-based functions or word counts for text data and feature-based functions. Using a sparse matrix will always (except when ties exist) give the same result that using a dense matrix populated with 0s would, but operating on sparse matrices can be significantly faster process.
Here is an example of using a feature-based function on a very sparse feature matrix. Note that each row can be thought of as an example with binary feature values.
.. code-block:: python
from apricot import FeatureBasedSelection
from scipy.sparse import csr_matrix
X = numpy.random.randint(2, size=(10000, 100), p=[0.99, 0.01])
X_sparse = csr_matrix(X)
selector = FeatureBasedSelection(100, 'sqrt')
selector.fit(X_sparse)
Here is an example of using a graph-based function on a very sparse similarity matrix. Note that X is a pre-computed similarity matrix and must be specified as such to the function.
.. code-block:: python
from apricot import FacilityLocationSelection
from scipy.sparse import csr_matrix
X = numpy.random.randint(10, size=(1000, 1000), p=[0.99, 0.01])
X = numpy.abs((X + X.T) / 2)
X_sparse = csr_matrix(X)
selector = FacilityLocationSelection(100, 'precomputed')
selector.fit(X_sparse)
jmschrei-apricot-bf86e69/docs/features/streaming.rst 0000664 0000000 0000000 00000003703 14145540156 0022711 0 ustar 00root root 0000000 0000000 .. _features.streaming:
Streaming Optimization
======================
Streaming optimization involves optimizing a submodular function on a stream of data. The key assumptions of these methods are that the data cannot all fit in memory at one time and, more restrictively, that each example can only be seen once: after seeing each example, the algorithm must decide whether to keep it or not before moving on to the next example.
Although this problem is difficult, principled approaches for solving it have been proposed that have theoretical guarantees. One such example, `sieve streaming `_, involves making many estimates of what the objective score of the optimal subset would be, and selecting elements based on these elements in parallel. When estimates are found to be too low, the corresponding subsets are discarded, and when certain estimates that were initially too high are found to be plausible, the corresponding subset begins to be populated.
In apricot, streaming optimization can be used with any of the built-in functions by using the `partial_fit` method instead of the `fit` method. Although the algorithm is designed to be applied to one example at a time in a streaming setting, in practice applying the algorithm to batches of data can be much faster while still providing the same answer.
.. note::
Streaming optimization is implemented for mixtures of functions, but not for sum redundancy or saturated coverage functions.
You can read more about streaming optimization `in this tutorial `_.
.. code-block:: python
from apricot import FeatureBasedSelection
X = numpy.random.randint(2, size=(10000, 100), p=[0.99, 0.01])
sample_cost = numpy.exp(numpy.random.randn(10000))
selector = FeatureBasedSelection(100, 'sqrt')
selector.partial_fit(X, sample_cost=sample_cost)
jmschrei-apricot-bf86e69/docs/functions/ 0000775 0000000 0000000 00000000000 14145540156 0020355 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/docs/functions/facilityLocation.rst 0000664 0000000 0000000 00000000232 14145540156 0024401 0 ustar 00root root 0000000 0000000 .. _functions.facility-location:
Facility Location
=================
.. automodule:: apricot.functions.facilityLocation
:members:
:inherited-members:
jmschrei-apricot-bf86e69/docs/functions/featureBased.rst 0000664 0000000 0000000 00000000236 14145540156 0023502 0 ustar 00root root 0000000 0000000 .. _functions.feature-based:
Feature-based Functions
=======================
.. automodule:: apricot.functions.featureBased
:members:
:inherited-members:
jmschrei-apricot-bf86e69/docs/functions/graphCut.rst 0000664 0000000 0000000 00000000172 14145540156 0022664 0 ustar 00root root 0000000 0000000 .. _functions.graph-cut:
Graph Cut
=========
.. automodule:: apricot.functions.graphCut
:members:
:inherited-members:
jmschrei-apricot-bf86e69/docs/functions/maxCoverage.rst 0000664 0000000 0000000 00000000254 14145540156 0023351 0 ustar 00root root 0000000 0000000 .. _functions.max-coverage:
Maximum Coverage
================
A maximum coverage function.
.. automodule:: apricot.functions.maxCoverage
:members:
:inherited-members:
jmschrei-apricot-bf86e69/docs/functions/mixture.rst 0000664 0000000 0000000 00000000166 14145540156 0022607 0 ustar 00root root 0000000 0000000 .. _functions.mixtures:
Mixtures
========
.. automodule:: apricot.functions.mixture
:members:
:inherited-members:
jmschrei-apricot-bf86e69/docs/functions/saturatedCoverage.rst 0000664 0000000 0000000 00000000236 14145540156 0024560 0 ustar 00root root 0000000 0000000 .. _functions.saturated-coverage:
Saturated Coverage
==================
.. automodule:: apricot.functions.saturatedCoverage
:members:
:inherited-members:
jmschrei-apricot-bf86e69/docs/functions/sumRedundancy.rst 0000664 0000000 0000000 00000000215 14145540156 0023726 0 ustar 00root root 0000000 0000000 .. _functions.sum-redundancy:
Sum Redundancy
==============
.. automodule:: apricot.functions.sumRedundancy
:members:
:inherited-members: jmschrei-apricot-bf86e69/docs/imgs/ 0000775 0000000 0000000 00000000000 14145540156 0017304 5 ustar 00root root 0000000 0000000 jmschrei-apricot-bf86e69/docs/imgs/embeddings.png 0000775 0000000 0000000 00000525303 14145540156 0022126 0 ustar 00root root 0000000 0000000 ‰PNG
IHDR 6 .a?ë ¶iTXtXML:com.adobe.xmp
>ùðv ‚iCCPsRGB IEC61966-2.1 (‘u‘»KA‡¿$Нˆ‚‡¨U´±Hðj‘D0js9s‰�ÄãîD‚`Pm|úh+X‚¢beah£rÎ!"f–Ùùö·;Ãî,xc-kUô@6g›‘Ñ�2ŸUªñÒL5TÍ2&£#1ÊÚÛ
7^u¹µÊŸû×ê’–žjá!Í0má1á‰ÛpyS¸YK«ÂÇÂS.(|íê‰"?¹œ*ò‡Ëf,o£°’úʼn_¬¥Í¬°¼œölfYû¹�û27•Ø&ÞŠE„QB(Œ3L˜ ½Ê¤‹>ºeE™üžïü)–$W“Ù �É")ÒØD]–êI‰ºèIònÿÿöÕÒûûŠÕý!¨|pœ—¨Ú€Ï‚ã¼ï;Îçøîá,WÊ_Úƒ�WÑ%}Öà伤%¶àtZîÕT¿%Ÿ¸W×áùêãÐt µsÅžýìsx±UùªØÞ�N9ß0ÿ^Igâìè`' pHYs šœ IDATxœìÝg\×ö8ü53§s
½Wkl`A±`EÅX‚½ S”Øc�ÆBb�± ÆÆÅ±$jÀn4j�XP‘v臜Âi3ó¼îï�1Aõ&ëË‹{îÌì½×ÌÍç“}×ìY›`Y–eY@!„B!„B¨VÈw B!„B!„ú߆ &„B!„B!T'˜`B!„B!„Bu‚ &„B!„B!T'˜`B!„B!„Bu‚ &„B!„B!T'¼w B @ã®q׸ç÷Íï›ß—hF4#š5Êh”Ñ(¢!¢_ÕJ•§ÊSå•,*YT²Èú?Öÿ±þ�ãÀ80ÜÙÌË™—3/s¿½zxõðêñª~*.V\¬¸˜s ç@Î�(�(�(‘«ÈUäZµ~,?–ëããS½=¡'ôDö�ìÙŠZµ(jáÐÓ¡§CO·n7ÜnH›J›J›V½ž‹™‹¿æOI>_>_>ß©‰S§&5o…B¡}´>Z¼¼¼]“ëí{Ú÷´ïiffVß±U¥›ª›ª›š«ËÕåê|6úlôÙÈ“ñd