重拾数值优化学习的契机:淘宝猜价格,当我用图像和爬虫将相关商品的价格预估出来后,如何选择一件或多件商品使得被选中商品的总价格接近目标价格呢?
这是一个看似很简单的问题,我们先将其数学形式表达出来:
min x ∈ { 0 , 1 } n ∥ a ⊺ x − b ∥ 2 2 b ∈ R x , a ∈ R n (1) \min_{x\in \{0,1\}^n}\|a^\intercal x-b \|_2^2~~~~~~b\in\mathbb{R}~~~~x,a\in\mathbb{R}^n \tag{1} x∈{0,1}nmin∥a⊺x−b∥22 b∈R x,a∈Rn(1)
解释一下,这里的变量 b 就是我们的目标价格,a 存储了每件商品的预估价格(我们自己预估出来的),x 是元素只能为 0 或 1 的列向量, x i = 0 x_i=0 xi=0 意味中我们选中第 i 件商品; x i = 1 x_i=1 xi=1 意味中我们不选中第 i 件商品。 ∥ ⋅ ∥ 2 2 \|\cdot\|_2^2 ∥⋅∥22 用来衡量误差的经典式子(也可以尝试其他范数衡量)。
整个式子的意思是:找到一个向量 x(它说明了哪些商品选,哪些不选),使得选中的商品的总价格和目标价格差距最小。
从数值优化的角度上看,(1)式是一个约束的二次优化问题,我们首先对(1)式进行化简。
∥ a ⊺ x − b ∥ 2 2 = ( a ⊺ x − b ) ⊺ ( a ⊺ x − b ) = ( x ⊺ a − b ) ( a ⊺ x − b ) = x ⊺ ( a a ⊺ ) x + ( − 2 b a ) ⊺ x + b 2 = x ⊺ Q x + c ⊺ x + b 2 \begin{aligned} \|a^\intercal x-b \|_2^2 &= (a^\intercal x-b)^\intercal (a^\intercal x-b)\\ &=(x^\intercal a-b)(a^\intercal x-b) \\&= x^\intercal (aa^\intercal )x+(-2ba)^\intercal x+b^2 \\ &=x^\intercal Qx+c^\intercal x+b^2 \end{aligned} ∥a⊺x−b∥22=(a⊺x−b)⊺(a⊺x−b)=(x⊺a−b)(a⊺x−b)=x⊺(aa⊺)x+(−2ba)⊺x+b2=x⊺Qx+c⊺x+b2
其中 Q = a a ⊺ , c = − 2 b a Q=aa^\intercal, c=-2ba Q=aa⊺,c=−2ba, 到这里我们可以发现式子里 b 2 b^2 b2 这一项是固定的。因此我们的(1)式等同于下面式子:
min x ∈ { 0 , 1 } n ∥ a ⊺ x − b ∥ 2 2 ⇔ min x ∈ { 0 , 1 } n x ⊺ Q x + c ⊺ x (2) \min_{x\in \{0,1\}^n}\|a^\intercal x-b \|_2^2\Leftrightarrow\min_{x\in \{0,1\}^n}x^\intercal Qx+c^\intercal x\tag{2} x∈{0,1}nmin∥a⊺x−b∥22⇔x∈{0,1}nminx⊺Qx+c⊺x(2)
接下来这一步很重要,将 一个二次01规划化为一次01规划,我们先给出结论
min x ∈ { 0 , 1 } n x ⊺ Q x + c ⊺ x ⇔ min x ∈ { 0 , 1 } n ∑ 1 ≤ i < j ≤ n 2 q i j y i j + ∑ i = 1 n ( c i + q i i ) x i (3) \min_{x\in \{0,1\}^n}x^\intercal Qx+c^\intercal x \Leftrightarrow \min_{x\in \{0,1\}^n}\sum_{1\le i
可以看到化简后(3)式没有二次项,这种变换是针对 0-1规划 设计出来的。思考一下,我们如果要把(2)式里的二次项去掉呢?具体来讲,里面的二次项还有两种形式:一种是平方项 x i 2 x_{i}^2 xi2 ,另外一种是交叉项 x i x j ( i ≠ j ) x_{i}x_j(i\ne j) xixj(i=j)。0-1规划 的特殊性在平方项上展现的淋漓尽致,可以发现:
x i ∗ x i = x i (4) x_{i}*x_i =x_i\tag{4} xi∗xi=xi(4)
因此我们可以直接将平方项变成一次项!!!
而对于交叉项,就没有这么好的性质了,我们采用引入新变量的方式解决它,令
y i j = x i ∗ x j ( 1 ≤ i < j ≤ n ) (5) y_{ij}=x_i*x_j~~(1\le i
因为 x 是有约束的,所以 y 必须也有相应约束!!!
一种等价的约束是:
∀ x i , x j ∈ { 0 , 1 } , y i j = x i ∗ x j i f f y i j = max { x i + x j − 1 , 0 } (6) \forall x_i,~x_j\in\{0,1\},~y_{ij}=x_i*x_j ~~~\mathbf{iff}~~~y_{ij}=\max\{x_i+x_j-1, 0\}\tag{6} ∀xi, xj∈{0,1}, yij=xi∗xj iff yij=max{xi+xj−1,0}(6)
读者可以自行验证,因此我们得到关于 y 的约束条件如下:
y i j ∈ { 0 , 1 } y i j ≤ x i y i j ≤ x j y i j ≥ x i + x j − 1 (7) y_{ij}\in\{0,1\}~~~~~ y_{ij}\le x_i~~~~~y_{ij}\le x_j~~~~~y_{ij}\ge x_i+x_j-1\tag{7} yij∈{0,1} yij≤xi yij≤xj yij≥xi+xj−1(7)
至此,我们证明了(1)到(2),(2)到(3)的等价性,由等价的 传递性,我们得到
min x ∈ { 0 , 1 } n ∥ a ⊺ x − b ∥ 2 2 ⇔ min x , y i j ∈ { 0 , 1 } n ∑ 1 ≤ i < j ≤ n 2 q i j y i j + ∑ i = 1 n ( c i + q i i ) x i (8) \min_{x\in \{0,1\}^n}\|a^\intercal x-b \|_2^2\Leftrightarrow \min_{x,y_{ij}\in \{0,1\}^n}\sum_{1\le i
假设 n 个商品,则有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1) 个 y y y 和 n n n 个 x x x,共有变量 n ( n − 1 ) 2 + n \frac{n(n-1)}{2} +n 2n(n−1)+n 个。相应地,0-1约束也会 n ( n − 1 ) 2 + n \frac{n(n-1)}{2} + n 2n(n−1)+n 个, y y y 与 x x x 之间的约束有 n ( n − 1 ) 2 ∗ 3 \frac{n(n-1)}{2} * 3 2n(n−1)∗3 (由(7)知),共有约束 2 n 2 − n 2n^2-n 2n2−n 个。
也就是说一个形式简单的 n阶二次、带n个约束的优化最终转化为 n ( n − 1 ) 2 + n \frac{n(n-1)}{2} + n 2n(n−1)+n 阶、带 2 n 2 − n 2n^2-n 2n2−n 个约束的线性规划。
由于最终是用一次形式,故不需要使用库 qpsolvers,只需使用线性规划的库 pulp,另外还需使用库 numpy 进行矩阵、向量的变换。
import pulp as pu
import numpy as np
# a是商品预估价格所形成的列表,b是目标价格
def quad_01_programmer(a: list, b):
n = len(a)
a = np.array(a).reshape(-1, 1)
Q = a * a.transpose()
c = (-2) * b * a
# print(Q)
# row = int(3 * n * (n - 1) / 2)
col = int(n * (n - 1) / 2 + n)
var_xy = pu.LpVariable.dicts("z", range(col), cat="Binary")
# print(var_xy)
A = [2 * Q[i, j] for i in range(n - 1) for j in range(i + 1, n)]
c = c.tolist()
c = [int(x) for item in c for x in item]
for i in range(n):
c[i] += Q[i][i]
A = A + c
# print(A)
pro = pu.LpProblem('', sense=pu.LpMinimize)
# pro += pu.lpSum(A[i] * var_xy.values(i) for i in range(col))
pro += pu.lpDot(var_xy.values(), A)
bound = n * (n-1) >> 1
posi = 0
for i in range(n - 1):
for j in range(i + 1, n):
pro += (var_xy[posi] - var_xy[bound + i] <= 0)
posi += 1
posi = 0
for i in range(n - 1):
for j in range(i + 1, n):
pro += (var_xy[posi] - var_xy[bound + j] <= 0)
posi += 1
posi = 0
for i in range(n - 1):
for j in range(i + 1, n):
pro += (var_xy[bound + i] + var_xy[bound + j] - var_xy[posi] <= 1)
posi += 1
pro.solve()
solution = []
# for i in range(n):
# print(pu.value(var_xy[bound + i]))
for i in range(n):
if pu.value(var_xy[bound + i]) == 1:
solution.append(i)
return solution
虽然规模看起来很大,但数值优化算法一般是 迭代算法,系数矩阵的 稀疏程度 会影响到算法的速度。为此以下代码,展示了在 matlab 里面写时它的系数矩阵长什么样,并给出例子。
import pulp as pu
import numpy as np
def quad_01_looklike(a: list, b):
n = len(a)
a = np.array(a).reshape(-1, 1)
Q = a * a.transpose()
c = (-2) * b * a
row = int(3 * n * (n - 1) / 2)
col = int(n * (n - 1) / 2 + n)
# print(row)
# print(col)
# variables = [pu.LpVariable('X%d' % i, cat='Binary') for i in range(row)]
B = np.zeros(row).reshape(-1, 1)
coef_object = [0 for _ in range(col)]
posi = 0
for i in range(n - 1):
for j in range(i + 1, n):
coef_object[posi] = Q[i, j] * 2
posi += 1
for i in range(n):
coef_object[posi] = int(c[i])
posi += 1
# print(coef_object)
A = np.zeros([row, col])
posi = 0
bound = n * (n - 1) >> 1
for i in range(n - 1):
for j in range(i + 1, n):
A[posi, posi] = 1
A[posi, bound + i] = -1
posi += 1
posi2 = 0
for i in range(n - 1):
for j in range(i + 1, n):
A[posi, posi2] = 1
A[posi, bound + j - 1] = -1
posi += 1
posi2 += 1
posi2 = 0
for i in range(n - 1):
for j in range(i + 1, n):
A[posi, posi2] = -1
A[posi, bound + i] = 1
A[posi, bound + j] = 1
posi += 1
posi2 += 1
print(A)
# prob = pu.LpProblem(sense=pu.LpMinimize)
# prob += abs(b- sum([coef_object[i] * variables[i] for i in range(row)]))
a = [1, 2, 3, 4]
b = 6
quad_01_looklike(a, b)
得到结果如下:
[[ 1. 0. 0. 0. 0. 0. -1. 0. 0. 0.]
[ 0. 1. 0. 0. 0. 0. -1. 0. 0. 0.]
[ 0. 0. 1. 0. 0. 0. -1. 0. 0. 0.]
[ 0. 0. 0. 1. 0. 0. 0. -1. 0. 0.]
[ 0. 0. 0. 0. 1. 0. 0. -1. 0. 0.]
[ 0. 0. 0. 0. 0. 1. 0. 0. -1. 0.]
[ 1. 0. 0. 0. 0. 0. -1. 0. 0. 0.]
[ 0. 1. 0. 0. 0. 0. 0. -1. 0. 0.]
[ 0. 0. 1. 0. 0. 0. 0. 0. -1. 0.]
[ 0. 0. 0. 1. 0. 0. 0. -1. 0. 0.]
[ 0. 0. 0. 0. 1. 0. 0. 0. -1. 0.]
[ 0. 0. 0. 0. 0. 1. 0. 0. -1. 0.]
[-1. 0. 0. 0. 0. 0. 1. 1. 0. 0.]
[ 0. -1. 0. 0. 0. 0. 1. 0. 1. 0.]
[ 0. 0. -1. 0. 0. 0. 1. 0. 0. 1.]
[ 0. 0. 0. -1. 0. 0. 0. 1. 1. 0.]
[ 0. 0. 0. 0. -1. 0. 0. 1. 0. 1.]
[ 0. 0. 0. 0. 0. -1. 0. 0. 1. 1.]]
Process finished with exit code 0