166 lines
5.7 KiB
Python
166 lines
5.7 KiB
Python
# %%
|
|
#
|
|
# This benchmark compares the speed of PCA solvers on datasets of different
|
|
# sizes in order to determine the best solver to select by default via the
|
|
# "auto" heuristic.
|
|
#
|
|
# Note: we do not control for the accuracy of the solvers: we assume that all
|
|
# solvers yield transformed data with similar explained variance. This
|
|
# assumption is generally true, except for the randomized solver that might
|
|
# require more power iterations.
|
|
#
|
|
# We generate synthetic data with dimensions that are useful to plot:
|
|
# - time vs n_samples for a fixed n_features and,
|
|
# - time vs n_features for a fixed n_samples for a fixed n_features.
|
|
import itertools
|
|
from math import log10
|
|
from time import perf_counter
|
|
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from sklearn import config_context
|
|
from sklearn.decomposition import PCA
|
|
|
|
REF_DIMS = [100, 1000, 10_000]
|
|
data_shapes = []
|
|
for ref_dim in REF_DIMS:
|
|
data_shapes.extend([(ref_dim, 10**i) for i in range(1, 8 - int(log10(ref_dim)))])
|
|
data_shapes.extend(
|
|
[(ref_dim, 3 * 10**i) for i in range(1, 8 - int(log10(ref_dim)))]
|
|
)
|
|
data_shapes.extend([(10**i, ref_dim) for i in range(1, 8 - int(log10(ref_dim)))])
|
|
data_shapes.extend(
|
|
[(3 * 10**i, ref_dim) for i in range(1, 8 - int(log10(ref_dim)))]
|
|
)
|
|
|
|
# Remove duplicates:
|
|
data_shapes = sorted(set(data_shapes))
|
|
|
|
print("Generating test datasets...")
|
|
rng = np.random.default_rng(0)
|
|
datasets = [rng.normal(size=shape) for shape in data_shapes]
|
|
|
|
|
|
# %%
|
|
def measure_one(data, n_components, solver, method_name="fit"):
|
|
print(
|
|
f"Benchmarking {solver=!r}, {n_components=}, {method_name=!r} on data with"
|
|
f" shape {data.shape}"
|
|
)
|
|
pca = PCA(n_components=n_components, svd_solver=solver, random_state=0)
|
|
timings = []
|
|
elapsed = 0
|
|
method = getattr(pca, method_name)
|
|
with config_context(assume_finite=True):
|
|
while elapsed < 0.5:
|
|
tic = perf_counter()
|
|
method(data)
|
|
duration = perf_counter() - tic
|
|
timings.append(duration)
|
|
elapsed += duration
|
|
return np.median(timings)
|
|
|
|
|
|
SOLVERS = ["full", "covariance_eigh", "arpack", "randomized", "auto"]
|
|
measurements = []
|
|
for data, n_components, method_name in itertools.product(
|
|
datasets, [2, 50], ["fit", "fit_transform"]
|
|
):
|
|
if n_components >= min(data.shape):
|
|
continue
|
|
for solver in SOLVERS:
|
|
if solver == "covariance_eigh" and data.shape[1] > 5000:
|
|
# Too much memory and too slow.
|
|
continue
|
|
if solver in ["arpack", "full"] and log10(data.size) > 7:
|
|
# Too slow, in particular for the full solver.
|
|
continue
|
|
time = measure_one(data, n_components, solver, method_name=method_name)
|
|
measurements.append(
|
|
{
|
|
"n_components": n_components,
|
|
"n_samples": data.shape[0],
|
|
"n_features": data.shape[1],
|
|
"time": time,
|
|
"solver": solver,
|
|
"method_name": method_name,
|
|
}
|
|
)
|
|
measurements = pd.DataFrame(measurements)
|
|
measurements.to_csv("bench_pca_solvers.csv", index=False)
|
|
|
|
# %%
|
|
all_method_names = measurements["method_name"].unique()
|
|
all_n_components = measurements["n_components"].unique()
|
|
|
|
for method_name in all_method_names:
|
|
fig, axes = plt.subplots(
|
|
figsize=(16, 16),
|
|
nrows=len(REF_DIMS),
|
|
ncols=len(all_n_components),
|
|
sharey=True,
|
|
constrained_layout=True,
|
|
)
|
|
fig.suptitle(f"Benchmarks for PCA.{method_name}, varying n_samples", fontsize=16)
|
|
|
|
for row_idx, ref_dim in enumerate(REF_DIMS):
|
|
for n_components, ax in zip(all_n_components, axes[row_idx]):
|
|
for solver in SOLVERS:
|
|
if solver == "auto":
|
|
style_kwargs = dict(linewidth=2, color="black", style="--")
|
|
else:
|
|
style_kwargs = dict(style="o-")
|
|
ax.set(
|
|
title=f"n_components={n_components}, n_features={ref_dim}",
|
|
ylabel="time (s)",
|
|
)
|
|
measurements.query(
|
|
"n_components == @n_components and n_features == @ref_dim"
|
|
" and solver == @solver and method_name == @method_name"
|
|
).plot.line(
|
|
x="n_samples",
|
|
y="time",
|
|
label=solver,
|
|
logx=True,
|
|
logy=True,
|
|
ax=ax,
|
|
**style_kwargs,
|
|
)
|
|
# %%
|
|
for method_name in all_method_names:
|
|
fig, axes = plt.subplots(
|
|
figsize=(16, 16),
|
|
nrows=len(REF_DIMS),
|
|
ncols=len(all_n_components),
|
|
sharey=True,
|
|
)
|
|
fig.suptitle(f"Benchmarks for PCA.{method_name}, varying n_features", fontsize=16)
|
|
|
|
for row_idx, ref_dim in enumerate(REF_DIMS):
|
|
for n_components, ax in zip(all_n_components, axes[row_idx]):
|
|
for solver in SOLVERS:
|
|
if solver == "auto":
|
|
style_kwargs = dict(linewidth=2, color="black", style="--")
|
|
else:
|
|
style_kwargs = dict(style="o-")
|
|
ax.set(
|
|
title=f"n_components={n_components}, n_samples={ref_dim}",
|
|
ylabel="time (s)",
|
|
)
|
|
measurements.query(
|
|
"n_components == @n_components and n_samples == @ref_dim "
|
|
" and solver == @solver and method_name == @method_name"
|
|
).plot.line(
|
|
x="n_features",
|
|
y="time",
|
|
label=solver,
|
|
logx=True,
|
|
logy=True,
|
|
ax=ax,
|
|
**style_kwargs,
|
|
)
|
|
|
|
# %%
|