Low-Rank Matrix Completion
This example demonstrates how to use the positive semidefinite cone, as well as reusing a cached workspace and using warm-starting.
Matrix completion is the problem of filling in missing data into a partially observed matrix where the measurements we are given have been corrupted by Gaussian noise. In low-rank matrix completion we have the additional prior knowledge that the matrix we are completing is low-rank. For simplicity we shall also assume that the matrix we are reconstructing is symmetric positive definite. A famous instance of this problem is the Netflix prize.
Concretely, we denote by \(\hat X \in \mathbf{R}^{n \times n}\) the true matrix corrupted by noise, and denote by \(\mathcal{I}\) the set of indices (row and column pairs) from which we receive noisy observations. We shall use the nuclear norm, denoted \(\| \cdot \|_*\), as a convex surrogate for rank, which we shall trade off against the observations using regularization parameter \(\lambda \geq 0\). The low-rank matrix completion problem is given by
over variable \(X \in \mathbf{R}^{n \times n}\) (we use \(\cdot \succeq 0\) to indicate membership in the symmetric positive semidefinite cone).
We can convert this into a more standard form. First, let \(x = \mathrm{vec}(X)\) be the semidefinite vectorization of \(X\) described in cones (and concretely implemented in the code that follows). Further, let \(A\) be the linear operator that extracts the elements of \(x\) for which we have (noisy) observations, and let \(b = A \mathrm{vec}(\hat X)\). Since the nuclear norm of a positive semidefinite matrix is given by its trace we obtain
over variable \(x \in \mathbf{R}^{n(n+1) /2}\) and \(y \in \mathbf{R}^{|\mathcal{I}|}\), where \(I_n\) is the \(n \times n\) identity matrix. From this formulation it is straightforward to convert it into the standard form accepted by SCS. The regularization parameter \(\lambda \geq 0\) trades off the rank of the solution and the quality of the fit, and so we solve the problem for many choices of \(\lambda\). Since \(\lambda\) enters only in the linear part of the objective function, we can reuse the matrix factorization and use warm starting to reduce the computation time.
Python code to solve this is below.
import scs
import numpy as np
import scipy as sp
from scipy import sparse
np.random.seed(1)
# The vec function as documented in api/cones
def vec(S):
n = S.shape[0]
S = np.copy(S)
S *= np.sqrt(2)
S[range(n), range(n)] /= np.sqrt(2)
return S[np.triu_indices(n)]
# The mat function as documented in api/cones
def mat(s):
n = int((np.sqrt(8 * len(s) + 1) - 1) / 2)
S = np.zeros((n, n))
S[np.triu_indices(n)] = s / np.sqrt(2)
S = S + S.T
S[range(n), range(n)] /= np.sqrt(2)
return S
dim = 15 # dim x dim matrix
vlen = int(dim * (dim + 1) / 2) # length of vector x = vec(X)
# Generate true matrix
rank = dim // 5 # low rank
X = np.random.randn(dim, rank)
X = X @ X.T
#############################################################################
# Let's first do some basic sanity checks to ensure that mat, vec are working:
# mat(vec( . )) should be identity
print(f"Should be ~ 0: {np.linalg.norm(X - mat(vec(X)))}")
# Trace( . ) should be vec(I)' vec( . )
print(f"Should be ~ 0: {np.trace(X) - vec(np.eye(dim)) @ vec(X)}")
#############################################################################
num_measurements = vlen // 2 # how many measurements are revealed
# Generate random measurement indices
measurement_idxs = np.random.choice(
np.arange(vlen), size=num_measurements, replace=False
)
# Create A matrix
Ad = np.zeros((num_measurements, vlen))
for i in range(num_measurements):
Ad[i, measurement_idxs[i]] = 1.0
# Noisy measurements of X
measurements = Ad @ vec(X) + 0.01 * np.random.randn(num_measurements) # + noise
# Auxiliary data
In = sparse.eye(vlen)
Im = sparse.eye(num_measurements)
On = sparse.csc_matrix((vlen, vlen))
Onm = sparse.csc_matrix((vlen, num_measurements))
# SCS data
P = sparse.block_diag([On, sparse.eye(num_measurements)], format="csc")
A = sparse.vstack(
[
# zero cone
sparse.hstack([Ad, -Im]),
# positive semidefinite cone
sparse.hstack([-In, Onm]),
],
format="csc",
)
b = np.hstack([measurements, np.zeros(vlen)])
c = np.hstack([np.zeros(vlen + num_measurements)])
data = dict(P=P, A=A, b=b, c=c)
cone = dict(z=num_measurements, s=dim)
# Setup workspace
solver = scs.SCS(data, cone, eps_abs=1e-6, eps_rel=1e-6)
print(f"Solving for lambda = 0")
sol = solver.solve() # lambda = 0
X_hat = mat(sol["x"][:vlen])
print(f"Error: {np.linalg.norm(X_hat - X) / np.linalg.norm(X)}")
# Solve for different values of lambda
lambdas = np.logspace(-6, 1, 11)
for lam in lambdas:
print(f"Solving for lambda = {lam}")
# Re-use workspace, just update the `c` vector
c_new = np.hstack([lam * vec(np.eye(dim)), np.zeros(num_measurements)])
solver.update(c=c_new)
# Solve updated problem
sol = solver.solve() # will warm-start automatically
X_hat = mat(sol["x"][:vlen])
# What is the norm error?
print(f"Error : {np.linalg.norm(X_hat - X) / np.linalg.norm(X)}")
After following the python install instructions, we can run the code yielding output:
Should be ~ 0: 3.0513465426178085e-15
Should be ~ 0: 0.0
------------------------------------------------------------------
SCS v3.1.1 - Splitting Conic Solver
(c) Brendan O'Donoghue, Stanford University, 2012
------------------------------------------------------------------
problem: variables n: 180, constraints m: 180
cones: z: primal zero / dual free vars: 60
s: psd vars: 120, ssize: 1
settings: eps_abs: 1.0e-06, eps_rel: 1.0e-06, eps_infeas: 1.0e-07
alpha: 1.50, scale: 1.00e-01, adaptive_scale: 1
max_iters: 100000, normalize: 1, rho_x: 1.00e-06
acceleration_lookback: 10, acceleration_interval: 10
lin-sys: sparse-direct
nnz(A): 240, nnz(P): 60
Solving for lambda = 0
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 5.12e+00 8.58e-01 3.93e+01 2.40e+01 1.00e-01 7.29e-03
250| 7.51e-03 3.59e-05 2.58e-04 2.03e-04 3.19e-03 1.49e-02
500| 7.41e-03 1.60e-06 6.49e-05 -1.89e-05 9.33e-04 2.48e-02
750| 3.89e-03 1.04e-08 1.56e-06 -7.37e-07 9.33e-04 3.47e-02
800| 1.67e-08 3.76e-10 3.11e-12 -1.56e-12 9.33e-04 3.67e-02
------------------------------------------------------------------
status: solved
timings: total: 3.85e-02s = setup: 1.81e-03s + solve: 3.67e-02s
lin-sys: 3.24e-03s, cones: 3.16e-02s, accel: 3.59e-04s
------------------------------------------------------------------
objective = -0.000000
------------------------------------------------------------------
Error: 2.4700619951554748
Solving for lambda = 1e-06
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 3.04e-06 1.00e-06 1.76e-04 1.21e-04 9.33e-04 1.01e-04
250| 2.31e-02 9.45e-09 7.98e-07 4.11e-05 1.00e-06 1.03e-02
500| 7.27e-03 7.34e-10 4.85e-07 4.16e-05 1.00e-06 2.12e-02
750| 4.47e-03 3.69e-10 4.00e-07 4.18e-05 1.00e-06 3.06e-02
1000| 3.61e-03 1.61e-10 3.42e-07 4.19e-05 1.00e-06 3.78e-02
1250| 3.49e-03 1.59e-10 3.38e-07 4.19e-05 1.00e-06 4.49e-02
1500| 3.10e-03 9.65e-11 3.18e-07 4.20e-05 1.00e-06 5.18e-02
1750| 3.22e-03 3.06e-08 2.75e-07 4.20e-05 1.36e-05 5.90e-02
2000| 1.94e-03 2.37e-09 3.94e-07 4.22e-05 1.36e-05 6.64e-02
2250| 1.81e-03 5.11e-10 4.73e-07 4.23e-05 1.36e-05 7.35e-02
2500| 1.72e-03 5.65e-10 5.08e-07 4.23e-05 1.36e-05 8.07e-02
2750| 1.63e-03 6.25e-10 5.40e-07 4.23e-05 1.36e-05 8.83e-02
3000| 1.53e-03 7.00e-10 5.67e-07 4.24e-05 1.36e-05 9.56e-02
3250| 1.43e-03 4.32e-09 6.40e-07 4.24e-05 4.32e-05 1.03e-01
3500| 1.21e-03 6.07e-09 6.68e-07 4.25e-05 4.32e-05 1.09e-01
3750| 1.03e-03 9.64e-09 7.59e-07 4.27e-05 4.32e-05 1.16e-01
4000| 4.61e-04 2.64e-08 2.03e-07 4.31e-05 4.32e-05 1.23e-01
4250| 2.54e-04 2.09e-09 1.05e-07 4.30e-05 4.32e-05 1.29e-01
4500| 7.91e-05 1.08e-09 3.60e-08 4.31e-05 4.32e-05 1.36e-01
4750| 3.03e-05 1.24e-09 5.48e-09 4.31e-05 4.32e-05 1.42e-01
4775| 9.42e-06 5.65e-10 5.58e-09 4.31e-05 4.32e-05 1.43e-01
------------------------------------------------------------------
status: solved
timings: total: 1.43e-01s = setup: 2.17e-06s + solve: 1.43e-01s
lin-sys: 1.76e-02s, cones: 1.17e-01s, accel: 1.83e-03s
------------------------------------------------------------------
objective = 0.000043
------------------------------------------------------------------
Error : 0.039165998317111615
Solving for lambda = 5.011872336272725e-06
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 1.50e-01 4.95e-06 3.31e-05 1.92e-04 4.32e-05 8.42e-05
250| 3.43e-03 1.34e-07 1.58e-06 2.11e-04 4.32e-05 7.29e-03
500| 1.73e-03 2.11e-09 1.81e-06 2.11e-04 4.32e-05 1.47e-02
750| 1.59e-03 1.01e-09 1.90e-06 2.12e-04 4.32e-05 2.22e-02
1000| 1.52e-03 9.87e-10 1.95e-06 2.12e-04 4.32e-05 2.98e-02
1250| 1.34e-03 9.31e-09 2.10e-06 2.12e-04 1.38e-04 3.71e-02
1500| 1.01e-03 1.04e-08 2.13e-06 2.13e-04 1.38e-04 4.48e-02
1750| 6.80e-04 8.59e-09 1.73e-06 2.14e-04 1.38e-04 5.23e-02
2000| 1.45e+00 2.00e-04 1.53e-06 2.15e-04 1.38e-04 5.95e-02
2250| 4.87e-04 1.13e-08 1.40e-06 2.14e-04 1.38e-04 6.65e-02
2500| 7.75e-04 8.76e-08 1.49e-06 2.16e-04 1.38e-04 7.36e-02
2750| 6.69e-04 2.14e-08 2.14e-06 2.15e-04 1.38e-04 8.19e-02
3000| 2.18e-04 7.41e-09 7.61e-07 2.15e-04 1.38e-04 8.90e-02
3250| 2.17e-04 3.06e-08 1.10e-07 2.15e-04 1.38e-04 9.61e-02
3500| 2.40e-05 4.38e-10 7.45e-08 2.15e-04 1.38e-04 1.03e-01
3600| 4.83e-06 5.28e-10 4.07e-09 2.15e-04 1.38e-04 1.06e-01
------------------------------------------------------------------
status: solved
timings: total: 1.06e-01s = setup: 1.96e-06s + solve: 1.06e-01s
lin-sys: 1.29e-02s, cones: 8.62e-02s, accel: 1.09e-03s
------------------------------------------------------------------
objective = 0.000215
------------------------------------------------------------------
Error : 0.025334768895438196
Solving for lambda = 2.5118864315095822e-05
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 2.44e-01 2.49e-05 1.34e-04 9.44e-04 1.38e-04 9.16e-05
250| 1.61e-03 1.94e-07 5.77e-06 1.06e-03 1.38e-04 7.88e-03
500| 1.25e-03 5.66e-09 5.22e-06 1.06e-03 1.38e-04 1.55e-02
750| 1.08e-03 4.03e-09 4.49e-06 1.06e-03 1.38e-04 2.26e-02
1000| 8.60e-04 1.78e-09 3.69e-06 1.06e-03 1.38e-04 3.04e-02
1250| 7.46e-04 1.21e-08 3.55e-06 1.06e-03 4.46e-04 3.85e-02
1500| 5.59e-04 7.69e-09 2.78e-06 1.06e-03 4.46e-04 4.60e-02
1750| 4.57e-04 1.01e-08 2.73e-06 1.06e-03 4.46e-04 5.33e-02
2000| 2.15e-04 3.68e-09 1.35e-06 1.06e-03 4.46e-04 6.05e-02
2250| 9.98e-05 1.54e-09 6.55e-07 1.06e-03 4.46e-04 6.74e-02
2500| 7.51e-06 5.47e-10 4.88e-08 1.06e-03 4.46e-04 7.47e-02
------------------------------------------------------------------
status: solved
timings: total: 7.47e-02s = setup: 1.88e-06s + solve: 7.47e-02s
lin-sys: 9.02e-03s, cones: 6.11e-02s, accel: 8.98e-04s
------------------------------------------------------------------
objective = 0.001062
------------------------------------------------------------------
Error : 0.021416258779558488
Solving for lambda = 0.0001258925411794166
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 3.71e-01 1.17e-04 4.59e-04 4.61e-03 4.46e-04 8.17e-05
250| 1.44e-03 4.05e-07 1.33e-05 5.27e-03 4.46e-04 7.40e-03
500| 9.07e-04 4.06e-07 5.92e-06 5.28e-03 4.46e-04 1.52e-02
750| 1.14e-04 2.21e-09 1.42e-06 5.28e-03 4.46e-04 2.22e-02
1000| 1.70e-05 3.22e-10 1.49e-07 5.28e-03 4.46e-04 2.97e-02
1025| 9.37e-06 1.07e-10 1.06e-07 5.28e-03 4.46e-04 3.05e-02
------------------------------------------------------------------
status: solved
timings: total: 3.05e-02s = setup: 1.79e-06s + solve: 3.05e-02s
lin-sys: 3.54e-03s, cones: 2.50e-02s, accel: 5.09e-04s
------------------------------------------------------------------
objective = 0.005279
------------------------------------------------------------------
Error : 0.03355024082447967
Solving for lambda = 0.000630957344480193
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 2.03e+00 5.72e-04 7.00e-03 1.84e-02 4.46e-04 8.29e-05
250| 6.36e-03 1.41e-06 1.78e-04 2.61e-02 4.46e-04 7.31e-03
500| 2.35e-02 1.04e-05 5.01e-05 2.62e-02 4.46e-04 1.45e-02
750| 7.01e-04 1.89e-07 2.18e-05 2.62e-02 4.46e-04 2.16e-02
1000| 2.42e-05 1.05e-09 3.62e-07 2.62e-02 4.46e-04 2.87e-02
1100| 7.41e-06 1.86e-10 7.34e-08 2.62e-02 4.46e-04 3.16e-02
------------------------------------------------------------------
status: solved
timings: total: 3.16e-02s = setup: 1.74e-06s + solve: 3.16e-02s
lin-sys: 3.65e-03s, cones: 2.57e-02s, accel: 6.26e-04s
------------------------------------------------------------------
objective = 0.026224
------------------------------------------------------------------
Error : 0.06744664704036951
Solving for lambda = 0.0031622776601683794
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 1.04e+01 2.74e-03 2.66e-01 -2.34e-02 4.46e-04 9.11e-05
250| 8.77e-03 1.73e-05 2.41e-05 1.30e-01 2.01e-03 7.52e-03
500| 7.31e-05 3.12e-08 9.26e-07 1.30e-01 6.56e-03 1.47e-02
575| 1.47e-06 9.60e-10 4.27e-08 1.30e-01 6.56e-03 1.68e-02
------------------------------------------------------------------
status: solved
timings: total: 1.68e-02s = setup: 1.78e-06s + solve: 1.68e-02s
lin-sys: 1.92e-03s, cones: 1.39e-02s, accel: 1.96e-04s
------------------------------------------------------------------
objective = 0.129971
------------------------------------------------------------------
Error : 0.11835628778123863
Solving for lambda = 0.01584893192461111
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 3.47e+00 1.39e-02 3.91e-01 3.49e-01 6.56e-03 8.42e-05
250| 1.03e-04 1.62e-07 1.25e-05 6.43e-01 6.56e-03 7.30e-03
375| 1.71e-06 1.57e-09 1.05e-07 6.43e-01 6.56e-03 1.15e-02
------------------------------------------------------------------
status: solved
timings: total: 1.15e-02s = setup: 1.73e-06s + solve: 1.15e-02s
lin-sys: 1.25e-03s, cones: 9.60e-03s, accel: 1.12e-04s
------------------------------------------------------------------
objective = 0.642509
------------------------------------------------------------------
Error : 0.17415959268201173
Solving for lambda = 0.07943282347242805
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 1.82e+01 6.55e-02 1.19e+01 -3.25e+00 6.56e-03 8.98e-05
225| 4.87e-07 4.73e-09 1.27e-07 3.15e+00 3.61e-02 7.46e-03
------------------------------------------------------------------
status: solved
timings: total: 7.47e-03s = setup: 1.72e-06s + solve: 7.47e-03s
lin-sys: 7.51e-04s, cones: 6.26e-03s, accel: 5.70e-05s
------------------------------------------------------------------
objective = 3.149158
------------------------------------------------------------------
Error : 0.22262545190934194
Solving for lambda = 0.3981071705534969
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 1.65e+01 3.21e-01 5.33e+01 -1.46e+01 3.61e-02 8.96e-05
125| 7.27e-07 1.89e-08 8.50e-07 1.49e+01 1.83e-01 4.27e-03
------------------------------------------------------------------
status: solved
timings: total: 4.27e-03s = setup: 1.76e-06s + solve: 4.27e-03s
lin-sys: 4.41e-04s, cones: 3.58e-03s, accel: 2.71e-05s
------------------------------------------------------------------
objective = 14.885089
------------------------------------------------------------------
Error : 0.3057864439961101
Solving for lambda = 1.9952623149688788
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 1.67e+01 1.55e+00 2.52e+02 -9.34e+01 1.83e-01 8.80e-05
50| 2.65e-07 5.94e-09 4.41e-07 6.15e+01 1.83e-01 1.86e-03
------------------------------------------------------------------
status: solved
timings: total: 1.86e-03s = setup: 1.67e-06s + solve: 1.86e-03s
lin-sys: 1.67e-04s, cones: 1.56e-03s, accel: 7.00e-06s
------------------------------------------------------------------
objective = 61.454584
------------------------------------------------------------------
Error : 0.5124766513093888
Solving for lambda = 10.0
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 8.75e+01 7.97e+00 6.59e+03 -3.72e+03 1.83e-01 9.20e-05
50| 1.31e-06 2.45e-10 6.05e-08 1.32e+02 1.83e-01 1.81e-03
------------------------------------------------------------------
status: solved
timings: total: 1.81e-03s = setup: 1.71e-06s + solve: 1.81e-03s
lin-sys: 1.73e-04s, cones: 1.49e-03s, accel: 1.87e-05s
------------------------------------------------------------------
objective = 132.180037
------------------------------------------------------------------
Error : 0.9959215771443783