By the end of this chapter you can state a convex problem in CVXR, solve it, read its status and solution, modify and re-solve it, and recognize when a problem is not something CVXR will accept.
where \(x\) is the variable, the objective \(f_0\) and the inequality functions \(f_1,\ldots,f_m\) are convex, and the equality functions \(g_1,\ldots,g_p\) are affine.
A handy mental picture:
a convex function is upward-curving (a chord lies above the graph),
a concave function is downward-curving,
an affine function is flat — both convex and concave.
We minimize a convex function (or maximize a concave one) over a convex region. The payoff for staying convex: any local solution is automatically global. For much more, see Boyd and Vandenberghe (2004).
2.2 ‘Hello World’
Here is about the simplest problem that shows all three ingredients — an objective, an inequality constraint, and an equality constraint:
with scalar variables \(x\) and \(y\). It is simple enough to solve by hand, so we can check CVXR’s answer. Here it is in CVXR:
# Variables minimized overx <-Variable(1)y <-Variable(1)# Problem definitionobjective <-Minimize(x^2+ y^2)constraints <-list(x >=0, 2* x + y ==1)prob1 <-Problem(objective, constraints)# Solve itsol1 <-psolve(prob1)status(prob1) # did it solve?
[1] "optimal"
sol1 # optimal objective value
[1] 0.2
value(x)
[,1]
[1,] 0.4
value(y)
[,1]
[1,] 0.2
Let’s walk through that carefully, because every CVXR problem is built the same way.
Variables are placeholders. The first two lines create scalar Variable objects. They have no value yet — not until we solve.
x <-Variable(1)y <-Variable(1)
Minimize() builds an objective, not a number. Unlike base R’s min(), this does not compute anything (there is nothing to compute — x and y have no values). It records the goal.
objective <-Minimize(x^2+ y^2)
>= and == build constraints, not TRUE/FALSE. The comparison operators are overloaded: x >= 0 returns a Constraint object the solver will enforce — not a logical. (Without the constraints, the solution would trivially be \(x = y = 0\).)
constraints <-list(x >=0, 2* x + y ==1)
Problem() assembles, psolve() solves. A problem pairs an objective with zero or more constraints. psolve() translates it into a form a convex solver understands, calls the solver, and returns the optimal objective value. Afterwards value() retrieves each variable and status() reports what happened.
Solving by hand: the equality gives \(y = 1 - 2x\), so we minimize \(x^2 + (1-2x)^2 = 5x^2 - 4x + 1\), which is smallest at \(x = 2/5\). Then \(y = 1/5\) and the objective is \(1/5\) — exactly what CVXR found. The world says ‘hi’ back.
2.2.1 Three things that can happen
When you psolve() a problem, status() is one of:
"optimal" — solved; value() gives the optimal variables.
"infeasible" — no point satisfies all constraints (e.g. x >= 1andx <= 0). psolve() returns +Inf (minimization) and variable values are NA.
"unbounded" — the objective can be driven to \(-\infty\). psolve() returns -Inf.
Always glance at status() before trusting a solution.
2.3 Watching the solver work
By default psolve() is quiet. But you can ask it to show its work by passing verbose = TRUE — invaluable when you want to understand what CVXR is doing, or diagnose a slow or stalling solve:
The output has three parts, and they preview the rest of this book:
Compilation — CVXR translates your problem into a standard form a solver understands. The chain (here Dcp2Cone -> ... -> ConeMatrixStuffing -> ...) is the machinery we open up in Chapter 3; for now, note that your symbolic problem becomes solver data.
Numerical solver — the chosen solver’s own iteration log, converging to the answer.
Summary — the status, optimal value, and how long compilation vs. solving took.
Turn it on whenever you are curious or stuck; turn it off (the default) once things work. Every psolve() call in this book accepts it.
2.4 Modifying and re-solving
CVXR objects are ordinary R objects: you can build new problems from old ones. Here we reuse the constraints from prob1, change the objective, and add a constraint:
Error in `construct_solving_chain()`:
! Problem is not DCP compliant.
CVXR refuses. Maximizing a convex function is not a convex problem, and CVXR checks this using a strict rule set called Disciplined Convex Programming (DCP). Follow the rules and your problem is guaranteed convex; break them and CVXR stops you rather than silently returning nonsense. The next chapter is devoted to DCP — this error is your first encounter with it.
2.6 A familiar example: least squares
Let’s connect to something every statistician knows. We generate data from a known linear model \(Y = X\beta + \epsilon\) and recover \(\beta\).
set.seed(123)n <-50; p <-10beta_true <--4:5# the true coefficients, -4 through 5X <-matrix(rnorm(n * p), nrow = n)Y <- X %*% beta_true +rnorm(n)
The textbook tool is lm():
ls_fit <-lm(Y ~0+ X) # no intercept in our modelcoef(ls_fit)
A solved problem gives you more than the coefficients. Once beta has a value, you can evaluate any expression built from it with value(). The fitted values\(X\hat\beta\) and the residuals, for instance, are one line each:
fitted <-value(X %*% beta) # X times beta-hatresid <- Y - fittedhead(round(cbind(fitted, resid), 3))
X %*% beta is a CVXR expression; value() evaluates it at the solution, and the result is ordinary R you can plot, summarize, or feed onward. This little move — any quantity you care about is value() of an expression — is one we will reuse in every chapter.
2.6.2 So what did we gain?
Fair question — we replaced one call to lm() with several lines, and it even runs slower. The payoff appears the moment the problem changes. Suppose we know the coefficients are nonnegative. That is nonnegative least squares, and lm() can no longer help. In CVXR it is one extra constraint:
Same answer — but in CVXR we got there by adding a line, not by finding a new package. That is the whole idea: you describe the problem, CVXR solves it.
2.7 Exercises
Try these by editing the chunks above and re-running. Solutions are one click away.
Exercise 1. Change the least-squares objective to least absolute deviation (minimize the sum of absolute residuals) and solve. Which objective gives a smaller sum of absolute residuals?
TipSolution
lad_obj <-Minimize(sum(abs(Y - X %*% beta)))lad_prob <-Problem(lad_obj)psolve(lad_prob)
LAD minimizes absolute residuals, so by that yardstick it wins; lm minimizes squared residuals and would win on sum of squares. Note we reused beta — its value reflects whichever problem was last solved.
Exercise 2. Add the constraint that the coefficients are nondecreasing: \(\beta_1 \le \beta_2 \le \cdots \le \beta_p\).
Hint: what base-R function takes lagged differences?
TipSolution
base::diff() has a CVXR method, so the monotonicity constraint is just diff(beta) >= 0:
Exercise 3 (⭐). Constrain the first four coefficients to sum to at most zero, \(\sum_{i=1}^{4}\beta_i \le 0\), using a matrix row rather than writing out four terms.
TipSolution
Build a row vector that picks out \(\beta_1,\ldots,\beta_4\):