{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "![MOSEK ApS](https://www.mosek.com/static/images/branding/webgraphmoseklogocolor.png )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Binary quadratic problems\n", "\n", "We discuss a simple solver for unconstrained binary quadratic problems, that is optimization problems of the form\n", "\n", "\$$\n", "\\begin{array}{ll}\n", "\\textrm{minimize} & x^TQx+P^Tx+R \\\\\n", "\\textrm{subject to}& x\\in\\{0,1\\}^n,\n", "\\end{array}\n", "\$$\n", "\n", "where $Q$ is symmetric. This framework can encode many hard problems, such as max-cut or maximum clique. Note that the continuous version with $x\\in[0,1]^n$ need not be convex.\n", "\n", "We present a simple SDP-based branch-and-bound solver. This implementation is not intended to compete with specialized tools such as [BiqCrunch](http://lipn.univ-paris13.fr/BiqCrunch/) or other sophisticated algorithms for this problem. The aim is rather to show that one can achieve pretty decent results with under 100 lines of quickly prototyped Python code using the MOSEK Fusion API.\n", "\n", "For complete source code and examples see the folder at [GitHub](https://github.com/MOSEK/Tutorials/tree/master/Fusion/BinaryQuadratic-SDP).\n", "\n", "# Algorithm\n", "\n", "We use the standard semidefinite relaxation of the problem, namely:\n", "\n", "\$$\n", "\\begin{array}{ll}\n", "\\textrm{minimize} & \\sum_{ij}Q_{ij}X_{ij} + \\sum_i P_ix_i + R \\\\\n", "\\textrm{subject to}& \\left[\\begin{array}{cc}X & x\\\\ x^T & 1\\end{array}\\right]\\succeq 0,\\\\\n", " & \\mathrm{diag}(X) = x.\n", "\\end{array}\n", "\$$\n", "\n", "The value of the relaxation is a lower bound for the original problem as we see by setting $X=xx^T$ for $x\\in\\{0,1\\}^n$.\n", "\n", "The algorithm maintains a lower bound lb, upper bound ub (objective value of the best integer solution found) and a queue of active nodes. We follow these rules:\n", "\n", "* In each iteration we pick the node with smallest objective value of the relaxation.\n", "* Each time a relaxation is solved we round the solution to nearest integers and try to use the result to improve the upper bound.\n", "* We branch on the variable whose value in the relaxation is closest to $\\frac12$. Both child nodes, obtained by fixing that variable to $0$ or $1$, are smaller instances of the same problem with easy to compute $Q',P',R'$.\n", "\n", "The main node-processing loop follows below.\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# The node-processing loop\n", "def solveBB(self):\n", " while self.active:\n", " # Pop the node with minimum objective from the queue\n", " obj, _, Q, P, R, curr, idxmap, pivot = heapq.heappop(self.active)\n", "\n", " self.lb = obj\n", "\n", " # Optimality is proved\n", " if self.lb >= self.ub - self.gaptol:\n", " self.active = []\n", " return\n", "\n", " self.stats()\n", "\n", " if len(P) <= 1: continue\n", "\n", " pidx = idxmap[pivot]\n", "\n", " # Construct and solve relaxations of two child nodes\n", " # where the pivot variable is assigned 0 or 1\n", " for val in [0, 1]:\n", " idxmap0 = np.delete(idxmap, pivot)\n", " curr0 = np.copy(curr)\n", " curr0[pidx] = val\n", "\n", " QT = np.delete(Q, pivot, axis=0)\n", " Q0 = np.delete(QT, pivot, axis=1)\n", " QZ = QT[:,pivot]\n", " PT = np.delete(P, pivot)\n", " P0 = PT + 2*val*QZ\n", " R0 = val*Q[pivot,pivot] + val*P[pivot] + R\n", "\n", " obj0, pivot0 = self.solveRelaxation(Q0, P0, R0, curr0, idxmap0)\n", "\n", " heapq.heappush(self.active, (obj0, self.relSolved, Q0, P0, R0, curr0, idxmap0, pivot0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The methods which solve the relaxation, find the pivot (branching) index and update the best solution are straightforward. We demonstrate only the fragment which defines the semidefinite relaxation in Fusion API:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# SDP relaxation \n", "# min QX + Px + R\n", "# st Z = [X, x; x^T, 1] >> 0\n", "def fusionSDP(Q, P, R):\n", " n = len(P)\n", " M = Model(\"fusionSDP\")\n", " Z = M.variable(\"Z\", Domain.inPSDCone(n+1))\n", " X = Z.slice([0,0], [n,n])\n", " x = Z.slice([0,n], [n,n+1])\n", " M.constraint(Expr.sub(X.diag(), x), Domain.equalsTo(0.))\n", " M.constraint(Z.index(n,n), Domain.equalsTo(1.))\n", "\n", " M.objective(ObjectiveSense.Minimize, Expr.add([Expr.constTerm(R), Expr.dot(P,x), Expr.dot(Q,X)]))\n", "\n", " return M, x" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The solver is straightforward to use. It suffices to write:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "solver = BB(Q, P, R)\n", "solver.solve()\n", "solver.summary()\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "REL_SOLVED ACTIVE_NDS OBJ_BOUND BEST_OBJ\n", " 1 0 -10000000000.0 -34.1829220672\n", " 1 0 -40.9578610377 -34.1829220672\n", " 2 0 -40.9578610377 -34.5354934947\n", " 3 1 -40.0967961795 -34.5354934947\n", " 5 2 -40.0967961795 -37.6612921792\n", " 5 2 -39.9359433001 -37.6612921792\n", " 7 3 -38.9245605048 -37.6612921792\n", " 9 4 -38.4153688863 -37.6612921792\n", " 11 5 -38.1908232037 -37.6612921792\n", " 13 6 -38.1309765476 -37.6612921792\n", " 15 7 -37.9917627647 -37.6612921792\n", " 17 8 -37.8758057302 -37.6612921792\n", " 19 9 -37.6906063328 -37.6612921792\n", " 21 10 -37.6747901176 -37.6612921792\n", " 23 11 -37.6613714044 -37.6612921792\n", " 25 0 -37.6612921492 -37.6612921792\n", "val = -37.6612921792\n", "sol = [0 0 1 0 1 1 0 1 0 1 0 1 1 1 1 0 0 1 0 1 1 0 1 0 1 0 0 0 0 0]\n", "relaxations = 25\n", "intpntIter = 243\n", "optimizerTime = 0.194850206375\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Example 1. Random data\n", "\n", "We consider a random problem\n", "\n", "\$$\n", "\\mathrm{minimize}_{x\\in\\{0,1\\}^n}\\ x^TQx\n", "\$$\n", "\n", "where $Q_{i,j}\\sim \\mathrm{Normal}(0,1)$ for $i\\geq j$. We generate instances with $30\\leq n\\leq 100$. \n", "\n", "![](files/stats/random.png )\n", "\n", "Horizontal scale: $n$. Each dot corresponds to an instance. Left: number of relaxations solved. Right: total time spent in the MOSEK optimizer (sec.). Log scale." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Example 2: BiqMac\n", "\n", "We next consider problems of the same form from the library [BiqMac](http://biqmac.aau.at/biqmaclib.html) of binary quadratic problems. We take all instances with $n\\leq 100$. Since all the $Q$ matrices in BiqMac are integral, we can use a stronger termination criterion:" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "\n", "if np.ceil(self.lb) >= self.ub:\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![](files/stats/biq.png )\n", "\n", "Horizontal scale: $n$. Each dot corresponds to an instance. Left: number of relaxations solved. Right: total time spent in the MOSEK optimizer (sec.). Log scale." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Example 3: Binary least squares\n", "\n", "Binary least squares is the problem\n", "\n", "\$$\n", "\\mathrm{minimize}_{x\\in\\{0,1\\}^n}\\ \\|Ax-b\\|_2^2 \\quad (=x^T(A^TA)x-(2A^Tb)^Tx+b^Tb)\n", "\$$\n", "\n", "where $A\\in\\mathbb{R}^{m\\times n}, b\\in \\mathbb{R}^m$. This is a mixed-integer SOCP, so we can compare our solver with the solutions obtained directly with MOSEK. As suggested in [Park, Boyd 2017](https://web.stanford.edu/~boyd/papers/pdf/int_least_squares.pdf) we generate reasonably hard random data by taking\n", "\n", "\$$\n", "\\begin{array}{l}\n", "A\\in\\mathbb{R}^{m\\times n},\\quad A_{i,j}\\sim \\mathrm{Normal}(0,1)\\\\\n", "b=Ac,\\ c\\in\\mathbb{R}^n,\\ c_i\\sim\\mathrm{Uniform}(0,1)\n", "\\end{array}\n", "\$$\n", "\n", "For this test we fix $n=50$ and vary $40\\leq m\\leq 150$.\n", "\n", "![](files/stats/bls.png )\n", "\n", "Horizontal scale: $m$. Each dot corresponds to an instance. Blue: this algorithm. Red: mixed-integer SOCP in MOSEK. Left: number of relaxations solved. Right: total time spent in the MOSEK optimizer (single-thread, sec.). Log scale." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Small executable example" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "REL_SOLVED ACTIVE_NDS OBJ_BOUND BEST_OBJ\n", " 1 0 -10000000000.0 -33.82841986228613\n", " 1 0 -38.964075740162144 -33.82841986228613\n", " 2 0 -38.964075740162144 -35.385939851022236\n", " 3 1 -38.1921615228864 -35.385939851022236\n", " 5 2 -37.90655091386468 -35.385939851022236\n", " 7 3 -37.4440427985024 -35.385939851022236\n", " 9 4 -37.35261510608118 -35.385939851022236\n", " 11 5 -37.08333365956387 -35.385939851022236\n", " 12 5 -37.08333365956387 -35.682369297628\n", " 13 6 -36.86248188395808 -35.682369297628\n", " 15 7 -36.83311033169883 -35.682369297628\n", " 17 8 -36.66241508003347 -35.682369297628\n", " 19 9 -36.6218827352734 -35.682369297628\n", " 21 10 -36.511788608469644 -35.682369297628\n", " 23 11 -36.341141651093864 -35.682369297628\n", " 25 12 -36.25532343851607 -35.682369297628\n", " 27 13 -36.045055821776245 -35.682369297628\n", " 29 14 -36.002354367603616 -35.682369297628\n", " 31 15 -35.802361179707304 -35.682369297628\n", " 33 16 -35.789284694829355 -35.682369297628\n", " 35 17 -35.74261587243334 -35.682369297628\n", " 37 18 -35.70278561398737 -35.682369297628\n", " 39 19 -35.69573463951267 -35.682369297628\n", " 41 0 -35.68236925395102 -35.682369297628\n", "val = -35.682369297628\n", "sol = [0 1 1 1 1 1 1 1 0 0 1 1 0 0 1 1 1 1 0 0 0 1 1 1 0]\n", "relaxations = 41\n", "intpntIter = 414\n", "optimizerTime = 0.33767127990722656\n" ] } ], "source": [ "import branchbound\n", "from branchbound import BB \n", "import numpy as np\n", "\n", "n=25\n", "Q1 = np.random.normal(0.0, 1.0, (n,n))\n", "solver = BB((Q1+Q1.transpose())/2, np.random.uniform(-1.0, 3.0, n), 0.0)\n", "solver.solve()\n", "solver.summary()" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "
This work is licensed under a Creative Commons Attribution 4.0 International License. The **MOSEK** logo and name are trademarks of Mosek ApS. The code is provided as-is. Compatibility with future release of **MOSEK** or the Fusion API are not guaranteed. For more information contact our [support](mailto:support@mosek.com). " ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4" } }, "nbformat": 4, "nbformat_minor": 1 }