[量子ニューラルネット] 勾配降下vs非勾配　どっちが速いのだ？

# 量子ニューラルネット

PennyLaneを用いて量子ニューラルネットワークを学習させた際の計算時間を考えます。
学習には非勾配最適化(e.g. Nelder-Mead)と、勾配最適化(e.g. Adam)があります。
古典ニューラルネットワーク機械学習では、常識的に、勾配最適化のほうが速いとされます。
量子の場合もそうなのでしょうか？

PennyLaneは勾配降下が出来ることを売りにしていますが、本当に有用なのでしょうか？
非勾配vs勾配のフェアな比較は、ほとんど見られなかったので、やってみました。

# 非勾配vs勾配　の比較条件

- 非勾配はscipy.optimize.minimize
- 学ぶ対象はsin(x)
- 学習対象のデータセット、ニューラルネットワークモデルは同一とする。
- Optimizerには、勾配はAdam、非勾配はNelder-Meadとする。
    - iteration回数は揃える。ただし非勾配については、必ずしもiteration回数=パラメータupdate回数にはならないよう。
    - optimizerのハイパラは、収束コストが両者でだいたい等しくなる程度に設定する。

厳密に出来ていない箇所もありますが、ご容赦ください。

# コード

## 勾配降下

```python
import pennylane as qml
from pennylane import numpy as np
from matplotlib import pyplot as plt
num_of_data = 64
X =  np.random.uniform(high=2 * np.pi, size=(num_of_data,1))
Y = np.sin(X[:,0])

########  パラメータ  #############
n_qubits = 2 ## qubitの数
n_layers = 2 # q_layer
dev = qml.device("lightning.qubit", wires=n_qubits, shots=None) # define a device

# Initial circuit parameters
var_init = np.random.uniform(high=2 * np.pi, size=(n_layers, n_qubits, 3))

@qml.qnode(dev, diff_method='adjoint')
def quantum_neural_net(var, x):
    qml.templates.AngleEmbedding(x, wires=range(n_qubits))
    qml.templates.StronglyEntanglingLayers(var, wires=range(n_qubits))
    return qml.expval(qml.PauliZ(0))
        
def square_loss(desired, predictions):
    loss = 0
    for l, p in zip(desired, predictions):
        loss = loss + (l - p) ** 2
    loss = loss / len(desired)
    return loss

def cost(var, features, desired):
    preds = [quantum_neural_net(var, x) for x in features]
    return square_loss(desired, preds)

opt = qml.AdamOptimizer(0.1)
import time

hist_cost = []
var = var_init
for it in range(50):
    t1 = time.time() 
    var, _cost = opt.step_and_cost(lambda v: cost(v, X, Y), var)
    t2 = time.time() 
    elapsed_time = t2-t1
    print("Iter:"+str(it)+", cost="+str(_cost.numpy()))
    print(f"Time：{elapsed_time}")
    hist_cost.append(_cost)

plt.plot(10*np.log10(hist_cost),'o-')

Y_pred = [quantum_neural_net(var, x) for x in X]
plt.plot(X[:,0],Y_pred,'o')

```

`Iter:0, cost=0.32403188045966985
Time：0.4249999523162842
Iter:1, cost=0.17929298669460514
Time：0.38500022888183594
Iter:2, cost=0.11539541686157953
Time：0.38000011444091797`

学習時間：0.4 x 50 = 20 sec

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878664/3ac6c27b-85ec-1695-24ac-605c2993c0c2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878664/6f36ba9e-bafd-c591-65c4-f05f25180647.png)



## 非勾配

ほぼ同じですが、scipy.optimize.minimize は1次元配列しか変数に取れないことに注意します。
(1次元で渡して、cost関数内部でrehshapeします）

```python

# Initial circuit parameters
var_init = np.random.uniform(high=2 * np.pi, size=(n_layers*n_qubits*3)) # one-dimensional array

@qml.qnode(dev, diff_method='adjoint')
def quantum_neural_net(var, x):
    var_3d_array = np.reshape(var,(n_layers,n_qubits,3))
    qml.templates.AngleEmbedding(x, wires=range(n_qubits))
    qml.templates.StronglyEntanglingLayers(var_3d_array, wires=range(n_qubits))
    return qml.expval(qml.PauliZ(0))

from scipy.optimize import minimize
hist_cost = []
var = var_init

count = 0
def cbf(Xi):
    global count
    global hist_cost
    count += 1
    cost_now = cost(Xi,X,Y)
    hist_cost.append(cost_now)
    print('iter = '+str(count)+' | cost = '+str(cost_now))
    
result = minimize(fun=cost, x0=var_init, args=(X,Y) ,method='Nelder-Mead', callback=cbf, options={"maxiter":50})
t2 = time.time() 
elapsed_time = t2-t1
print(f"Time：{elapsed_time}")
hist_cost.append(_cost)
print(f"Time per iteration : {elapsed_time/50}")       

plt.plot(10*np.log10(hist_cost[1:len(hist_cost)-1]),'o-') 
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878664/9085184e-080c-443b-ffda-b93e5338ed7b.png)


`iter = 1 | cost = 0.3949460800587229
iter = 2 | cost = 0.3949460800587229
iter = 3 | cost = 0.3949460800587229
iter = 4 | cost = 0.3949460800587229`

`Time：41.019999980926514
Time per iteration : 0.8203999996185303`

勾配降下の2倍の時間がかかりました。
思ったほど差がない・・と思いました。

では、パラメータ数が増えるとどうなるでしょうか。

`n_layers = 2→6` にしてみます。(パラメータ数が3倍になります)

勾配では
`Iter:0, cost=0.8111990194081542
Time：0.940000057220459
Iter:1, cost=0.3314598963846486
Time：0.8700001239776611
Iter:2, cost=0.1956043243597784
Time：0.8700001239776611`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878664/36bd4fa1-6ac7-c257-8e6d-f181ba55a493.png)



非勾配では

`iter = 1 | cost = 1.004932862586646
iter = 2 | cost = 1.004932862586646
iter = 3 | cost = 1.004932862586646
iter = 4 | cost = 1.004932862586646
iter = 5 | cost = 1.004932862586646
iter = 6 | cost = 1.004932862586646
iter = 7 | cost = 1.004932862586646`

`Time：98.82499980926514
Time per iteration : 1.9764999961853027`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878664/fe6d6615-f01c-3fa6-2512-5a87616076c0.png)

やはり速度差は2倍程度だったのですが、非勾配は全く収束していません。
非勾配の場合、パラメータ数が増えるとパラメータupdateに何度もiterationを必要とするようです。
iteration回数を4倍にしてみますと、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878664/382cec9b-066f-f5a3-476e-300dfbd2e340.png)

収束はしています。しかし計算時間は勾配法の8倍かかっていますし、まだ収束コストで負けています。

このように、勾配降下はパラメータ数が多いとアドバンテージが大きくなることがわかりました。
直感的にそうかもしれませんが、実際みてみると大変面白い結果ではないでしょうか。

# まとめ

勾配降下はやっぱり速いのだ。