이어서 간다. 이번 글에서 다룰 주제는

  • OSPEP를 이용한 DRS의 tight contraction factor
  • OSPEP를 이용한 optimal parameter selection
  • CVXPY를 이용한 위 주제들과 관련된 구현 몇 가지

 

Part 3. Tight Analytic Contraction Factors for DRS

결과부터 소개한다. 우리가 앞에서 준비했던, 논문의 정리 4.1.만 소개한다. 그래도 논문 링크를 열고 정리 4.1.~4.4.를 한 번 확인하는 것을 추천.

결과도 매우 중요하지만, 아무래도 일단 관심이 가는 것은 대체 이렇게 복잡한 것을 어떻게 증명했냐는 것이다. 논문의 말을 옮기면,

  • 기본적으로 Mathematica 같은 Computer Algebra System은 필요하다
  • OSPEP에서 얻은 Primal/Dual Problem을 활용한다. 이들을 풀기 위해서, 다음과 같은 접근을 사용한다.
  • Primal : 핵심 아이디어는 worst-case operator가 $\mathbb{R}^2$ 위에서 존재할 것이라는 추측이다. 이는 Primal Problem의 해 $G^{*}$의 rank가 2 이하라는 것과 동치인데, 이 추측의 근거는 complementary slackness라고 한다. 이러한 2차원 위에서의 worst-case analysis는 SDP 대신 non-convex quadratic programming 문제로 분석할 수 있고, 이 문제에 대응되는 Karush–Kuhn–Tucker condition을 이용하여 문제를 해결했다고 한다.
  • Dual : 역시 여기서도 Dual Problem의 해로 얻어지는 행렬 $S^{*}$가 rank 2 이하라는 가정을 한다. 근거는 Primal과 동일하다. 이 가정으로 $\rho^2$과 $\lambda_\mu^A$, $\lambda_\beta^B$ 사이의 관계식을 추가적으로 얻고, 이를 constraint로 하여 $\rho^2$을 최소화했다고 한다. 말이 쉽지만 정말 어려워 보인다..
  • 이렇게 Primal/Dual 문제를 (analytical) 푼 다음, 그 결과가 같음을 확인하면 자동으로 증명이 끝난다.

Appendix를 구경하면 이 결과의 엄밀한 증명을 직접 볼 수 있다. ㄹㅇ 가슴이 웅장해진다... 

 

Part 4 : Optimal Parameter Selection with OSPEP

여기서는 operator class $\mathcal{Q}_1$, $\mathcal{Q}_2$를 알고 있을 때 parameter $\alpha, \theta$를 optimal 하게 설정하는 방법을 다룬다.

이는 결국 $\rho^2_{*} (\alpha, \theta)$를 해당 $\alpha, \theta$에 대한 optimal contraction factor라고 할 때, $$\rho^2_{*} = \inf_{\alpha, \theta > 0} \rho^2_{*}(\alpha, \theta)$$와 대응되는 $\alpha_{*}$, $\theta_{*}$를 찾는 문제와 같다. 원래 $\theta \in (0, 2)$라는 조건이 있으나, 이 조건은 relax 해도 무방하다. 

 

$\rho^2_{*}(\alpha, \theta)$의 값 자체는 SDP를 풀어서 계산할 수 있으니, 이차원 위에서 미분 불가능한 함수를 최소화하는 일반적인 알고리즘이 있다면 이를 사용하여 문제를 해결할 수도 있을 것이다. 그러나, 조금 더 작업을 거치면 더욱 편하게 문제를 풀 수 있다. $\theta$를 최적화 문제 안에 집어넣자.

 

Dual Problem $$\begin{equation*} \begin{aligned} \text{minimize} \quad & \rho^2 \\ \text{subject to} \quad & \lambda_\mu^A, \lambda_\beta^B \ge 0 \\ \quad & S = -M_O - \lambda_\mu^A M_\mu^A - \lambda_\beta^B M_{\beta}^B + \rho^2 M_I \succeq 0 \end{aligned} \end{equation*}$$을 다시 생각해보자. 우리가 $\alpha, \theta$를 고정된 값이 아니라 최적화 문제의 변수로 두지 못하는 이유는, 행렬 $S$의 entry가 $\alpha, \theta$의 nonlinear term을 포함하고 있기 때문이다. $\alpha$는 아예 분모에 있고, $\theta$는 $\theta^2$ 항이 $S$에 있다. 여기서 $\theta$에 대한 quadratic term을 제거하기 위해, Schur Complement를 사용한다.

 

$$\begin{equation*} \begin{aligned} S &= -M_O - \lambda_\mu^A M_\mu^A - \lambda_\beta^B M_{\beta}^B + \rho^2 M_I \\ S' &= - \lambda_\mu^A M_\mu^A - \lambda_\beta^B M_{\beta}^B + \rho^2 M_I \end{aligned} \end{equation*} $$라고 정의하면, $$M_O = (1, \theta, -\theta)^T (1, \theta, -\theta)$$이므로, Schur Complement의 성질에서 $S \succeq 0$는 $v = (1, \theta, -\theta)^T$라 했을 때 $$\overline{S} = \left[ \begin{matrix} S' & v \\ v^T & 1 \end{matrix} \right] \succeq 0$$과 동치이다. 또한, $S'$은 각 entry에 $\theta$에 대한 non-linear term이 없으므로, $\theta$까지 SDP에 넣을 수 있다.

 

즉, 새로운 문제를 $$\begin{equation*} \begin{aligned} \text{minimize} \quad & \rho^2 \\ \text{subject to} \quad & \lambda_\mu^A, \lambda_\beta^B, \theta \ge 0 \\ \quad & S' = - \lambda_\mu^A M_\mu^A - \lambda_\beta^B M_{\beta}^B + \rho^2 M_I \\ \quad & v = (1, \theta, -\theta)^T \\ \quad & \overline{S} = \left[ \begin{matrix} S' & v \\ v^T & 1 \end{matrix} \right] \succeq 0 \end{aligned} \end{equation*}$$로 바꾸면, $\theta$에 대한 최적화까지 자동으로 된다. 이제 남은 것은 $\alpha$에 대한 최적화다.

 

이렇게 해서 얻은 주어진 $\alpha$에 대한 optimal contraction factor를 $\rho^2_{*}(\alpha)$라고 하자. 이를 최소화하면 된다.

변수 하나에 대한 (미분 불가능한) 함수의 minimization은 다양한 방법이 있다. 그런데, 실험 결과에 따르면

  •  함수 $\rho^2_{*}(\alpha)$는 연속함수이다. (별로 놀랍지는 않은 사실)
  • 함수 $\rho^2_{*}(\alpha)$는 unimodal 함수이다. (놀라운 사실)

그러므로, 이를 최소화하기 위해서 (PS를 했던 사람의 입장에서 쉽게 생각하면) 삼분탐색을 사용해도 좋다.

사실 scipy나 matlab 등에서 이미 derivative-free optimization이 구현이 되어있기 때문에, 이걸 가져다 쓰면 된다.

 

Part 5 : Programming OSPEP with CVXPY

이건 논문에는 없고 (정확하게는 CVXPY로 하지 않고 MATLAB 등으로 한 것 같다) 그냥 내가 심심해서 하는 것이다.

논문의 저자들이 사용한 코드를 보고 싶다면, github.com/AdrienTaylor/OperatorSplittingPerformanceEstimation로 가자. 

내가 여기서 짠 코드들은 github.com/rkm0959/rkm0959_implements에 가면 있다. 

 

Task 1 : Verifying Theorem 4.1. Numerically

CVXPY를 이용하여 Theorem 4.1.을 검증해보자. 방법은 다음과 같다.

  • $\alpha, \theta, \mu, \beta$를 적당히 랜덤하게 선정한다.  
  • 대응되는 Primal/Dual Problem을 CVXPY로 해결한다. SDP를 풀어야하니, blas+lapack을 설치해야 한다.
  • 또한, Theorem 4.1.을 이용하여 optimal contraction factor를 계산한다. 
  • Theorem 4.1.에는 Case가 총 5가지가 있다. 이때, 어떤 Case에서 계산이 되었는지도 기록한다.
  • Primal Problem, Dual Problem, Theorem 4.1.로 얻은 답이 일치하는지 확인한다.
  • 위 과정을 여러 번 반복한 후, 5가지의 Case를 각각 몇 번 다루었는지를 확인한다.

이를 구현한 코드는 다음과 같다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import cvxpy as cp 
import matplotlib.pyplot as plt 
import numpy as np
import math
 
def THM_4_1_scaled(theta, mu, beta):
    assert 0 < theta < 2 and mu > 0 and beta > 0
 
    if mu * beta - mu + beta < 0 and theta <= 2 * (beta + 1* (mu - beta - mu * beta) / (mu + mu * beta - beta - beta * beta - 2 * mu * beta * beta):
        return 1, abs(1 - theta * beta / (beta + 1))
    if mu * beta - mu - beta > 0 and theta <= 2 * (mu * mu + beta * beta + mu * beta + mu + beta - mu * mu * beta * beta) / (mu * mu + beta * beta + mu * mu * beta + mu * beta * beta + mu + beta - 2 * mu * mu * beta * beta):
        return 2, abs(1 - theta * (1 + mu * beta) / (1 + mu) / (1 + beta))
    if theta >= 2 * (mu * beta + mu + beta) / (2 * mu * beta + mu + beta):
        return 3, abs(1 - theta)
    if mu * beta + mu - beta < 0 and theta <= 2 * (mu + 1* (beta - mu - mu * beta) / (beta + mu * beta - mu - mu * mu - 2 * mu * mu * beta):
        return 4, abs(1 - theta * mu / (mu + 1))
    
    ret = (2 - theta) / 4
    ret = ret * ((2 - theta) * mu * (beta + 1+ theta * beta * (1 - mu))
    ret = ret * ((2 - theta) * beta * (mu + 1+ theta * mu * (1 - beta))
    ret = ret / mu / beta
    ret = ret / (2 * mu * beta * (1 - theta) + (2 - theta) * (mu + beta + 1))
    return 5, math.sqrt(ret)
 
def THM_4_1_unscaled(alpha, theta, mu, beta):
    return THM_4_1_scaled(theta, mu * alpha, beta / alpha)
 
def gen_M_O(theta):
    ret = [[1, theta, -theta], 
           [theta, theta * theta, -theta * theta], 
           [-theta, -theta * theta, theta * theta]]
    return np.array(ret)
 
def gen_M_I():
    ret = np.zeros((33))
    ret[00= 1
    return ret
 
def gen_M_mu_A(alpha, mu):
    ret = [[0-1/20], 
           [-1/2-(1+alpha*mu), 1],
           [010]]
    return np.array(ret)
 
def gen_M_beta_B(alpha, beta):
    ret = [[-beta/alpha, 0, beta/alpha + 1/2], 
           [000],
           [beta/alpha + 1/20-beta/alpha-1]]
    return np.array(ret)
 
 
def Primal(alpha, theta, mu, beta):
    G = cp.Variable((33), symmetric = True)
    
    M_O = gen_M_O(theta)
    M_I = gen_M_I()
    M_mu_A = gen_M_mu_A(alpha, mu)
    M_beta_B = gen_M_beta_B(alpha, beta)
 
    constraints = []
    constraints.append(G >> 0)
    constraints.append(cp.trace(M_I @ G) == 1)
    constraints.append(cp.trace(M_mu_A @ G) >= 0)
    constraints.append(cp.trace(M_beta_B @ G) >= 0)
 
    objective = cp.Maximize(cp.trace(M_O @ G))
 
    problem = cp.Problem(objective, constraints)
    problem.solve()
 
    return math.sqrt(problem.value)
 
def Dual(alpha, theta, mu, beta):
    lambda_mu_A = cp.Variable()
    lambda_beta_B = cp.Variable()
    rho_sq = cp.Variable()
 
    M_O = gen_M_O(theta)
    M_I = gen_M_I()
    M_mu_A = gen_M_mu_A(alpha, mu)
    M_beta_B = gen_M_beta_B(alpha, beta)
 
    constraints = []
    constraints.append(lambda_mu_A >= 0)
    constraints.append(lambda_beta_B >= 0)
    constraints.append(-M_O - lambda_mu_A * M_mu_A - lambda_beta_B * M_beta_B + rho_sq * M_I >> 0)
 
    objective = cp.Minimize(rho_sq)
 
    problem = cp.Problem(objective, constraints)
    problem.solve()
 
    return math.sqrt(problem.value)
 
eps = 0.0005
checked = [00000]
 
for _ in range(300):
    alpha = np.random.uniform(low = 0.5, high = 2.0)
    theta = np.random.uniform(low = 0.05, high = 1.95)
    mu = np.random.uniform(low = 0.1, high = 3.9)
    beta = np.random.uniform(low = 0.1, high = 3.9)
 
    assert alpha > 0 and theta > 0
    assert mu > 0 and beta > 0 
 
    val_1 = Primal(alpha, theta, mu, beta)
    val_2 = Dual(alpha, theta, mu, beta)
    whi, val_3 = THM_4_1_unscaled(alpha, theta, mu, beta)
 
    checked[whi - 1+= 1
 
    if abs(val_2 - val_1) > eps or abs(val_3 - val_1) > eps or abs(val_3 - val_2) > eps:
        print(val_1, val_2, val_3)
        print("Failed at Case", whi)
        print("Parameters:", alpha, theta, mu, beta)
        break
 
print("Checked Cases")
for i in range(5):
    print("Case #{} Verified {} Times!".format(i + 1, checked[i]))
 
print("Finished!")
cs

 

이렇게 하여 Theorem 4.1.의 결과를 검수할 수 있다. SDP solver가 문제를 잘 해결할 수 있도록 극단적인 값들은 넣지 않았다.

각 코드의 의미는 거의 자명하니 (식을 그대로 옮긴 것에 불과하다) 자세한 설명은 생략하도록 하겠다. Task 1은 이렇게 끝.

 

Task 2 : Optimal Parameter Selection with OSPEP

이번에는 Part 4에서 다루었던 내용을 구현해보자. 우리가 할 일은

  • 주어진 $\mu, \beta$에 대하여 optimal parameter $\alpha, \theta$를 계산
  • $\rho^2_{*} (\alpha)$의 그래프를 그리고, 실제로 unimodal function인지 눈으로 확인
  • 물론, 이러한 내용 및 그래프는 논문에도 있으며, DRS가 아니라 DYS로 설명되어 있다.

Part 4의 내용을 그대로 CVXPY로 옮기면, 다음과 같은 코드를 얻을 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import cvxpy as cp 
import matplotlib.pyplot as plt 
import numpy as np
import math
 
def THM_4_1_scaled(theta, mu, beta):
    assert 0 < theta < 2 and mu > 0 and beta > 0
 
    if mu * beta - mu + beta < 0 and theta <= 2 * (beta + 1* (mu - beta - mu * beta) / (mu + mu * beta - beta - beta * beta - 2 * mu * beta * beta):
        return 1, abs(1 - theta * beta / (beta + 1))
    if mu * beta - mu - beta > 0 and theta <= 2 * (mu * mu + beta * beta + mu * beta + mu + beta - mu * mu * beta * beta) / (mu * mu + beta * beta + mu * mu * beta + mu * beta * beta + mu + beta - 2 * mu * mu * beta * beta):
        return 2, abs(1 - theta * (1 + mu * beta) / (1 + mu) / (1 + beta))
    if theta >= 2 * (mu * beta + mu + beta) / (2 * mu * beta + mu + beta):
        return 3, abs(1 - theta)
    if mu * beta + mu - beta < 0 and theta <= 2 * (mu + 1* (beta - mu - mu * beta) / (beta + mu * beta - mu - mu * mu - 2 * mu * mu * beta):
        return 4, abs(1 - theta * mu / (mu + 1))
    
    ret = (2 - theta) / 4
    ret = ret * ((2 - theta) * mu * (beta + 1+ theta * beta * (1 - mu))
    ret = ret * ((2 - theta) * beta * (mu + 1+ theta * mu * (1 - beta))
    ret = ret / mu / beta
    ret = ret / (2 * mu * beta * (1 - theta) + (2 - theta) * (mu + beta + 1))
    return 5, math.sqrt(ret)
 
def THM_4_1_unscaled(alpha, theta, mu, beta):
    return THM_4_1_scaled(theta, mu * alpha, beta / alpha)
 
def gen_Base():
    ret = np.zeros((44))
    ret[03= 1
    ret[30= 1
    ret[33= 1
    return ret
 
def gen_M_theta():
    ret = np.zeros((44))
    ret[13= 1
    ret[23= -1
    ret[31= 1
    ret[32= -1
    return ret
 
def gen_M_I():
    ret = np.zeros((44))
    ret[00= 1
    return ret
 
def gen_M_mu_A(alpha, mu):
    ret = [[0-1/200], 
           [-1/2-(1+alpha*mu), 10],
           [0100],
           [0000]]
    return np.array(ret)
 
def gen_M_beta_B(alpha, beta):
    ret = [[-beta/alpha, 0, beta/alpha + 1/20], 
           [0000],
           [beta/alpha + 1/20-beta/alpha-10], 
           [0000]]
    return np.array(ret)
 
 
def opt_val(alpha, mu, beta, retrieve = False):
    lambda_mu_A = cp.Variable()
    lambda_beta_B = cp.Variable()
    theta = cp.Variable()
    rho_sq = cp.Variable()
 
    Base = gen_Base()
    M_theta = gen_M_theta()
    M_I = gen_M_I()
    M_mu_A = gen_M_mu_A(alpha, mu)
    M_beta_B = gen_M_beta_B(alpha, beta)
 
    constraints = []
    constraints.append(lambda_mu_A >= 0)
    constraints.append(lambda_beta_B >= 0)
    constraints.append(theta >= 0)
    constraints.append(Base + theta * M_theta - lambda_mu_A * M_mu_A - lambda_beta_B * M_beta_B + rho_sq * M_I >> 0)
 
    objective = cp.Minimize(rho_sq)
 
    problem = cp.Problem(objective, constraints)
    problem.solve()
 
    if retrieve == True:
        return math.sqrt(problem.value), theta.value
    else:
        return math.sqrt(problem.value)
 
mu = 0.53
beta = 1.35
 
alpha_L = 0.05
alpha_R = 3.95
 
print("Begin Ternary Search")
while alpha_R - alpha_L >= 0.0001:
    left = alpha_L + (alpha_R - alpha_L) / 3
    right = alpha_R - (alpha_R - alpha_L) / 3
    if opt_val(left, mu, beta) < opt_val(right, mu, beta):
        alpha_R = right
    else:
        alpha_L = left
 
opt_alpha = (alpha_L + alpha_R) / 2
opt_rho, opt_theta = opt_val(opt_alpha, mu, beta, True)
 
print("Optimal alpha, theta", opt_alpha, opt_theta)
print("Optimal contraction factor", opt_rho)
print("Check with Theorem 4.1.", THM_4_1_unscaled(opt_alpha, opt_theta, mu, beta))
 
print("Begin Graphing")
= np.linspace(0.253.7550)
= np.array([opt_val(alpha, mu, beta) for alpha in X])
 
plt.plot(X, Y)
plt.show()
 
plt.savefig("Graph.PNG")
 
print("Finished!")
cs

 

여기서 얻는 결론은 $\mu = 0.53$, $\beta = 1.35$일 때, optimal parameter는 $$\alpha_{*} = 1.5949751, \quad \theta_{*} = 1.4244099$$이고, 이때 얻어지는 optimal contraction factor는 $$\rho_{*} = 0.5010598$$이라는 것이다. 또한, 이 경우에 대한 $\rho^2_{*} (\alpha)$의 그래프를 그려서 unimodal function임을 확인할 수 있다. 그래프는 아래와 같다.

당연하지만, $x$축은 $\alpha$의 값, $y$축은 $\rho^2_{*}(\alpha)$의 값이다. 이렇게 Task 2와 논문 정리도 끝 :)