# 9장 그림 및 예제 코드

```
# 기본적인 import들
# 이후 그래프 그리는 코드에는 중복으로 적지 않음.
# 다른 곳으로 그래프 그리는 코드를 복사-붙이기 할 때는
# 이 import 코드와 함께 복사-붙이기 해야함
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from mpl_toolkits import mplot3d
import matplotlib.font_manager as mfm
import sympy

# numpy 출력 형식 지정
np.set_printoptions(precision=4, linewidth=150)

# matplotlib 스타일 지정
mpl.style.use('bmh')
mpl.style.use('seaborn-whitegrid')
style = plt.style.library['bmh']
# 스타일 컬러를 쉽게 쓸 수 있도록 리스트 저장
style_colors = [ c['color'] for c in style['axes.prop_cycle'] ]

# 그림을 로컬 폴더에 저장하고 싶으면 True로 수정
file_print = False
```

```
def plot_boundary(X, model, ax):
    # meshgrid를 짜서 Model에 포워드 시키기
    # 뉴럴 네트웍 모델 자체가 함수이므로 (x,y)->z를 하는 것이다.
    x = np.linspace(min(X[:,0]), max(X[:,0]))
    y = np.linspace(min(X[:,1]), max(X[:,1]))

    xx, yy = np.meshgrid(x, y)
    grid_tensor = torch.Tensor(np.c_[xx.flatten(), yy.flatten()])
    zz = model.forward(grid_tensor).detach().numpy().reshape(xx.shape)
    ax.contour(xx, yy, zz, cmap='gray', levels=[0.5])

def logistic(x):
    return 1 / (1+np.exp(-x))

```

## 샘플데이터 만들기

### 결정 경계

```
dcs_bnd_1 = lambda x: -(3/5)*x + 3
dcs_bnd_1_imp = lambda x, y: (3)*x + (5)*y + (-15)

dcs_bnd_2 = lambda x: -2*x + 6
dcs_bnd_2_imp = lambda x, y: (-6)*x + (-3)*y + (18)

x = np.linspace(0, 5, 50)

fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(1, 1, 1)

ax.plot(x, dcs_bnd_1(x), color='k')
ax.plot(x, dcs_bnd_2(x), '--', color='k')

ax.set_xlim(0, 5)
ax.set_ylim(0, 5)

plt.show()
```

### 무작위점 500개 생성

```
np.random.seed(3)

# 0~5에서 무작위 점 500개 추출
N = 500
samples = (np.random.rand(N*2)*5).reshape(N,2)
```

```
samples.shape
```

    (500, 2)

### 생성된 점 500개 그리기

```
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(1, 1, 1)

ax.plot(samples[:,0], samples[:,1], 'o', color='k')
ax.set_ylim(0, 5)
ax.set_xlim(0, 5)

plt.show()
```

## 샘플 데이터에 대한 레이블(타겟) 만들기

```
y1_bin = dcs_bnd_1_imp(samples[:,0], samples[:,1]) > 0
y2_bin = dcs_bnd_2_imp(samples[:,0], samples[:,1]) < 0
```

```
positive_where = np.where((y1_bin | y2_bin))[0]
target = np.zeros(N)
target[positive_where] = 1

print(target.shape)
```

    (500,)

- 이제 다음처럼 데이터가 준비되었음

- `samples`: 점 500개게 (500, 2) 형태로 저장

- `target`: 각점에 대한 레이블 0, 1이 (500,) 형태로 저장

## 그림 9-1

```
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(1, 1, 1)

ax.xaxis.set_tick_params(labelsize=18)
ax.yaxis.set_tick_params(labelsize=18)
ax.set_xlabel('$x$', fontsize=25)
ax.set_ylabel('$y$', fontsize=25)

ax.plot(samples[np.where(target==1)[0],0], samples[np.where(target==1)[0],1], 'o',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)
ax.plot(samples[np.where(target==0)[0],0], samples[np.where(target==0)[0],1], '^',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)

x1 = np.linspace(0, 15/7, 50)
x2 = np.linspace(15/7, 3, 50)

ax.plot(x1, dcs_bnd_1(x1), '--', color='k')
ax.plot(x2, dcs_bnd_2(x2), '--', color='k')

ax.set_ylim(0,5)
ax.set_xlim(0,5)

if file_print == True :
    fig.savefig("imgs/chap9/fig9-1.png", dpi=300, bbox_inches='tight')
    fig.savefig("imgs/chap9/fig9-1.pdf", format='pdf', bbox_inches='tight')

plt.show()
```

## 그림 9-3: $ 3x + 5y - 15 = 0 $ 로 분류

```
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(1, 1, 1)

ax.xaxis.set_tick_params(labelsize=18)
ax.yaxis.set_tick_params(labelsize=18)
ax.set_xlabel('$x$', fontsize=25)
ax.set_ylabel('$y$', fontsize=25)

ax.plot(x, dcs_bnd_1(x), color='k')

ax.plot(samples[y1_bin, 0], samples[y1_bin,1], 'o',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)

not_y1_bin = np.invert(y1_bin)
not_y1_and_target = np.logical_and(not_y1_bin,target)

ax.plot(samples[not_y1_and_target, 0], samples[not_y1_and_target, 1], 'o',
        markerfacecolor='white', markeredgecolor='gray', markersize=8)

ax.plot(samples[np.where(target==0)[0], 0], samples[np.where(target==0)[0], 1], '^',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)

ax.set_ylim(0,5)
ax.set_xlim(0,5)

if file_print == True :
    fig.savefig("imgs/chap9/fig9-3.png", dpi=300, bbox_inches='tight')
    fig.savefig("imgs/chap9/fig9-3.pdf", format='pdf', bbox_inches='tight')

plt.show()
```

## 그림 9-5: $ -6x - 3y + 18 = 0 $로 분류

```
fig = plt.figure(figsize=(7,7))
ax = fig.add_subplot(1,1,1)

ax.xaxis.set_tick_params(labelsize=18)
ax.yaxis.set_tick_params(labelsize=18)
ax.set_xlabel('$x$', fontsize=25)
ax.set_ylabel('$y$', fontsize=25)

ax.plot(x, dcs_bnd_2(x), color='k')

ax.plot(samples[y2_bin, 0], samples[y2_bin, 1], 'o',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)

not_y2_bin = np.invert(y2_bin)
not_y2_and_target = np.logical_and(not_y2_bin,target)

ax.plot(samples[not_y2_and_target, 0], samples[not_y2_and_target, 1], 'o',
        markerfacecolor='white', markeredgecolor='gray', markersize=8)

ax.plot(samples[np.where(target==0)[0], 0], samples[np.where(target==0)[0], 1], '^',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)

ax.set_ylim(0,5)
ax.set_xlim(0,5)

if file_print == True :
    fig.savefig("imgs/chap9/fig9-5.png", dpi=300, bbox_inches='tight')
    fig.savefig("imgs/chap9/fig9-5.pdf", format='pdf', bbox_inches='tight')

plt.show()
```

## 그림 9-8: 위 두 경우를 합성하여 분류하는 경우

```
xx, yy = np.meshgrid(x, x)

a, b, c = 10, -9, 4
o = lambda x, y: logistic( a*logistic(dcs_bnd_1_imp(x, y)) + b*logistic(dcs_bnd_2_imp(x, y)) + c )

fig = plt.figure(figsize=(7,7))
ax = fig.add_subplot()

ax.xaxis.set_tick_params(labelsize=18)
ax.yaxis.set_tick_params(labelsize=18)
ax.set_xlabel('$x$', fontsize=25)
ax.set_ylabel('$y$', fontsize=25)

pred = o(samples[:,0], samples[:,1])
pred_pos = pred >= 0.5
pred_neg = pred < 0.5

ax.contour(xx, yy, o(xx,yy), cmap='gray', levels=[0.5])

# for positive samples
TP = np.logical_and(target==1, pred_pos)
FN = np.logical_and(target==1, pred_neg)

# for negative samples
TN = np.logical_and(target==0, pred_neg)
FP = np.logical_and(target==0, pred_pos)

ax.plot(samples[TP,0], samples[TP,1], 'o',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)
ax.plot(samples[TN,0], samples[TN,1], '^',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)

ax.plot(samples[FN,0], samples[FN,1], 'o',
        markerfacecolor='k', markeredgecolor='w', markeredgewidth=2, markersize=15)
ax.plot(samples[FP,0], samples[FP,1], '^',
         markerfacecolor='k', markeredgecolor='w', markeredgewidth=2, markersize=15)

ax.set_ylim(0,5)
ax.set_xlim(0,5)

if file_print == True :
    fig.savefig("imgs/chap9/fig9-8.png", dpi=300, bbox_inches='tight')
    fig.savefig("imgs/chap9/fig9-8.pdf", format='pdf', bbox_inches='tight')

plt.show()
```

## 그림 9-9

```
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(projection='3d')

plt.rc('xtick',labelsize=15)
plt.rc('ytick',labelsize=15)

x1 = np.linspace(0, 5, 101)
x2 = np.linspace(0, 5, 101)

X1, X2 = np.meshgrid(x1, x2)

ax.contour(X1, X2, o(X1, X2), linewidths=5, cmap='gray', levels=[0.5])
ax.plot_surface(X1, X2, o(X1, X2), color='w', edgecolor='k', alpha=0.5)

ax.plot3D(samples[TP,0], samples[TP,1], 1.0, 'o',
        markerfacecolor='gray', markeredgecolor='k', markersize=8, zorder=10)

ax.plot3D(samples[TN,0], samples[TN,1], 0.0, '^',
        markerfacecolor='gray', markeredgecolor='k', markersize=8, zorder=10)

ax.plot3D(samples[FN,0], samples[FN,1], 0.0, 'o',
        markerfacecolor='k', markeredgecolor='w', markeredgewidth=2, markersize=15, zorder=10)
ax.plot3D(samples[FP,0], samples[FP,1], 1.0, '^',
        markerfacecolor='k', markeredgecolor='w', markeredgewidth=2, markersize=15, zorder=10)

ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.zaxis.set_tick_params(labelsize=15)
ax.set_xlabel(r'$x$', fontsize=20)
ax.set_ylabel(r'$y$', fontsize=20)
ax.set_zlabel(r'$a$', fontsize=20)
ax.view_init(30, -80)

if file_print == True :
    fig.savefig("imgs/chap10/fig10-9.png", dpi=300, bbox_inches='tight')
    fig.savefig("imgs/chap10/fig10-9.pdf", format='pdf', bbox_inches='tight')

plt.show()
```

## network 함수

```
def network(X, W):
    """
    X : (N, D)
    W : (3, 3)
        [b^(1)_1, w^(1)_11, w^(1)_12]
        [b^(1)_2, w^(1)_21, w^(1)_22]
        [b^(2)_1, w^(2)_11, w^(2)_12]

    ret : (N,)

    D, H, A = 2, 2, 1
    """
    X = np.hstack( (np.ones(X.shape[0]).reshape(-1,1), X) ) # (N,D)->(N,D+1)
    Z1 = np.dot(W[:2,:], X.T) # (H,N)=(H,D+1)*(D+1,N)
    A1 = logistic(Z1)           # (H,N)
    A1 = np.vstack((np.ones(A1.shape[1]), A1)) # (H,N)->(H+1,N)
    Z = np.dot(W[-1,:], A1) # (H+1,)*(H+1,N)
    A2 = logistic(Z)         # (N,)

    return A2
```

## network 함수 검증

```
W = np.array([ [-15, 3, 5], [18, -6, -3], [4, 10, -9] ])
pred = network(samples, W)

pred[pred>=0.5] = 1
pred[pred<0.5] = 0

result = pred==target

np.size(result) - np.count_nonzero(result)
```

    2

```
np.random.seed(17)
W = np.random.randn(9)

W.reshape(3,3)
```

    array([[ 0.2763, -1.8546,  0.6239],
           [ 1.1453,  1.0372,  1.8866],
           [-0.1117, -0.3621,  0.1487]])

```
pred = network(samples, W.reshape(3,3))

pred[pred>=0.5] = 1
pred[pred<0.5] = 0

result = pred==target

np.size(result) - np.count_nonzero(result)
```

    163

```
def J(W, X, T):
    """
    W: 함숫값을 결정하는 변수, 가중치 (9,)
    X: 주어진 점 데이터 X, X: (N,D)
    T: 데이터에 대한 클래스 T, 0 또는 1, T: (N,)
    """
    N = X.shape[0]
    W = W.reshape(3,3)

    Y = network(X, W)
    return (1/(2*N)) * ((T-Y)**2).sum()

```

```
# 초기 상태에서 목적함숫값
J(W, samples, target)
```

    0.1263148192185165

```
from scipy import optimize

# https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fmin_cg.html
W_star = optimize.fmin_cg(J, W, args=(samples, target),  gtol=1e-06)
```

    Optimization terminated successfully.
             Current function value: 0.000003
             Iterations: 313
             Function evaluations: 12694
             Gradient evaluations: 1154

```
W_star = W_star.reshape(3,3)
W_star
```

    array([[ 42.9811,  -9.4741, -13.7029],
           [-27.8283,   9.6497,   3.2045],
           [ 19.2321, -40.3183,  39.3389]])

## 그림 9-12

```
x = np.linspace(0, 5, 200)

xx, yy = np.meshgrid(x, x)

dcs_bnd_1_imp_ = lambda x, y: W_star[0, 1]*x + W_star[0, 2]*y + W_star[0, 0]
dcs_bnd_2_imp_ = lambda x, y: W_star[1, 1]*x + W_star[1, 2]*y + W_star[1, 0]

a, b, c = W_star[2, 1], W_star[2, 2], W_star[2, 0]
o = lambda x, y: logistic( a*logistic(dcs_bnd_1_imp_(x, y)) + b*logistic(dcs_bnd_2_imp_(x, y)) + c )

fig = plt.figure(figsize=(7,7))
ax = fig.add_subplot()

ax.xaxis.set_tick_params(labelsize=18)
ax.yaxis.set_tick_params(labelsize=18)
ax.set_xlabel('$x$', fontsize=25)
ax.set_ylabel('$y$', fontsize=25)

pred = o(samples[:,0], samples[:,1])
pred_pos = pred >= 0.5
pred_neg = pred < 0.5

ax.contour(xx, yy, o(xx,yy), cmap='gray', levels=[0.5])

# for positive samples
TP = np.logical_and(target==1, pred_pos)
FN = np.logical_and(target==1, pred_neg)

# for negative samples
TN = np.logical_and(target==0, pred_neg)
FP = np.logical_and(target==0, pred_pos)

ax.plot(samples[TP,0], samples[TP,1], 'o',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)
ax.plot(samples[TN,0], samples[TN,1], '^',
        markerfacecolor='gray', markeredgecolor='gray', markersize=8)

ax.plot(samples[FN,0], samples[FN,1], 'o',
        markerfacecolor='k', markeredgecolor='w', markeredgewidth=2, markersize=15)
ax.plot(samples[FP,0], samples[FP,1], '^',
        markerfacecolor='k', markeredgecolor='w', markeredgewidth=2, markersize=15)

ax.set_ylim(0,5)
ax.set_xlim(0,5)

if file_print == True :
    fig.savefig("imgs/chap9/fig9-12.png", dpi=300, bbox_inches='tight')
    fig.savefig("imgs/chap9/fig9-12.pdf", format='pdf', bbox_inches='tight')

plt.show()
```

## 신경망 미분하기

```
# pytorch를 임포트 한다.
import torch
```

### 순전파 함수(넘파이)

```
def forward(X, W, T, retopt='all'):
    """
    네트워크를 피드포워드 시킨다. numpy 버전
    X : 네트워크의 입력벡터 shape:(N,2)
    retopt : 네트워크가 순전파되면서 각 레이어에서 계산된 결과 값을
    되돌릴 방법을 설정한다.
        - 'all'  : 모든 층에서 계산된 결과를 튜플 형태로 되돌린다.
        - 'fval' : 함수의 최종 출력값만 되돌린다.
    """
    N = X.shape[0]

    H1 = np.dot(W[:2,1:], X.T)
    Z1 = H1 + W[:2,0].reshape(-1,1)
    A1 = logistic(Z1)

    H2 = np.dot(W[2,1:], A1)
    Z2 = H2 + W[2,0]
    A2 = logistic(Z2)

    C = (1/(2*N)) * ((T-A2)**2).sum()

    if retopt == 'all':
        return (H1, Z1, A1, H2, Z2, A2, C)
    elif retopt == 'fval':
        return C
```

### 순전파 함수(파이토치)

```
def forward_torch(X, W, T, retopt='all'):
    """
    네트워크를 피드포워드 시킨다. pytorch 버전
    X : 네트워크의 입력벡터 size:(N,2)
    retopt : 네트워크가 순전파되면서 각 레이어에서 계산된 결과 값을
    되돌릴 방법을 설정한다.
        - 'all'  : 모든 층에서 계산된 결과를 튜플 형태로 되돌린다.
        - 'fval' : 함수의 최종 출력값만 되돌린다.
    """
    N = X.size()[0]
    T = torch.tensor(T, dtype=torch.double)

    # 계산 결과 검증을 위해 pytorch를 사용하므로 numpy 어레이 뿐 아니라
    # pytorch tensor형태에 대해서도 동일한 연산을 한다.
    H1 = torch.mm(W[:2,1:], torch.t(X))
    Z1 = H1 + W[:2,0].view(-1,1)
    A1 = torch.sigmoid(Z1) #(2,1)

    H2 = torch.mm(W[2:,1:], A1)
    Z2 = H2 + W[2,0]
    A2 = torch.sigmoid(Z2)

    C = (1/(2*N)) * ((T-A2)**2).sum()

    if retopt == 'all':
        return (H1, Z1, A1, H2, Z2, A2, C)
    elif retopt == 'fval':
        return C
```

### 넘파이 어레이와 텐서 프린트 보조함수

```
np.set_printoptions(precision=4, linewidth =150)

def print_tensor(t):
    """
    텐서형 자료를 보기 좋게 프린트하기 위한 보조 함수
    """
    def namestr(obj, namespace):
        return [name for name in namespace if namespace[name] is obj]

    var_name = namestr(t, globals())[0]

    print("{}:{},{}".format(var_name, t.shape, t.dtype))
    print(t)
    print("-------------------------------------------")
```

### 가중치 무작위 초기화

```
np.random.seed(17)

W = np.random.randn(9).reshape(3,3)
W_torch = torch.tensor(W, dtype=torch.double); W_torch.requires_grad=True

print_tensor(W)
print_tensor(W_torch)
```

    W:(3, 3),float64
    [[ 0.2763 -1.8546  0.6239]
     [ 1.1453  1.0372  1.8866]
     [-0.1117 -0.3621  0.1487]]
    -------------------------------------------
    W_torch:torch.Size([3, 3]),torch.float64
    tensor([[ 0.2763, -1.8546,  0.6239],
            [ 1.1453,  1.0372,  1.8866],
            [-0.1117, -0.3621,  0.1487]], dtype=torch.float64, requires_grad=True)
    -------------------------------------------

### 초기 상태에서 목적함숫값

```
# 초기 상태에서 목적함숫값
J(W, samples, target)
```

    0.1263148192185165

```
H1, Z1, A1, H2, Z2, A2, C = forward(samples, W, target)

C
```

    0.1263148192185165

### 데이터 하나로 순전파 시키기

```
N = 1
x = samples[[0]]
x_torch = torch.tensor(x, dtype=torch.double); x_torch.requires_grad=True
t = target[[0]]

print_tensor(x)
print_tensor(x_torch)
print_tensor(t)

```

    x:(1, 2),float64
    [[2.754  3.5407]]
    -------------------------------------------
    x_torch:torch.Size([1, 2]),torch.float64
    tensor([[2.7540, 3.5407]], dtype=torch.float64, requires_grad=True)
    -------------------------------------------
    t:(1,),float64
    [1.]
    -------------------------------------------

```
H1, Z1, A1, H2, Z2, A2, C = forward(x, W, t)

print_tensor(H1)
print_tensor(Z1)
print_tensor(A1)
print_tensor(H2)
print_tensor(Z2)
print_tensor(A2)
print_tensor(C)
```

    H1:(2, 1),float64
    [[-2.8986]
     [ 9.5365]]
    -------------------------------------------
    Z1:(2, 1),float64
    [[-2.6223]
     [10.6818]]
    -------------------------------------------
    A1:(2, 1),float64
    [[0.0677]
     [1.    ]]
    -------------------------------------------
    H2:(1,),float64
    [0.1242]
    -------------------------------------------
    Z2:(1,),float64
    [0.0125]
    -------------------------------------------
    A2:(1,),float64
    [0.5031]
    -------------------------------------------
    C:(),float64
    0.12344827837124746
    -------------------------------------------

```
H1_torch, Z1_torch, A1_torch, H2_torch, Z2_torch, A2_torch, C_torch = forward_torch(x_torch, W_torch, t)

print_tensor(H1_torch)
print_tensor(Z1_torch)
print_tensor(A1_torch)
print_tensor(H2_torch)
print_tensor(Z2_torch)
print_tensor(A2_torch)
print_tensor(C_torch)
```

    H1_torch:torch.Size([2, 1]),torch.float64
    tensor([[-2.8986],
            [ 9.5365]], dtype=torch.float64, grad_fn=<MmBackward>)
    -------------------------------------------
    Z1_torch:torch.Size([2, 1]),torch.float64
    tensor([[-2.6223],
            [10.6818]], dtype=torch.float64, grad_fn=<AddBackward0>)
    -------------------------------------------
    A1_torch:torch.Size([2, 1]),torch.float64
    tensor([[0.0677],
            [1.0000]], dtype=torch.float64, grad_fn=<SigmoidBackward>)
    -------------------------------------------
    H2_torch:torch.Size([1, 1]),torch.float64
    tensor([[0.1242]], dtype=torch.float64, grad_fn=<MmBackward>)
    -------------------------------------------
    Z2_torch:torch.Size([1, 1]),torch.float64
    tensor([[0.0125]], dtype=torch.float64, grad_fn=<AddBackward0>)
    -------------------------------------------
    A2_torch:torch.Size([1, 1]),torch.float64
    tensor([[0.5031]], dtype=torch.float64, grad_fn=<SigmoidBackward>)
    -------------------------------------------
    C_torch:torch.Size([]),torch.float64
    tensor(0.1234, dtype=torch.float64, grad_fn=<MulBackward0>)
    -------------------------------------------

### 단계별 계산

### $\frac{\partial C}{\partial \mathbf{a}^{(2)}}$: (1,1)

```
dA2 = -(t-A2)/N
dA2
```

    array([-0.4969])

```
#                              종속변수, 독립변수, 종속변수와 곱해지는 상위 그래디언트
dA2_torch = torch.autograd.grad(C_torch, A2_torch, torch.tensor(1, dtype=torch.double), retain_graph=True)[0]
dA2_torch
```

    tensor([[-0.4969]], dtype=torch.float64)

### $\frac{\partial C}{\partial \mathbf{z}^{(2)}}$: (1,1)

```
dA2_dZ2 = logistic(Z2)*(1-logistic(Z2))
dZ2 = dA2_dZ2 * dA2
dZ2
```

    array([-0.1242])

```
#                              종속변수, 독립변수, 종속변수와 곱해지는 상위 그래디언트
dZ2_torch = torch.autograd.grad(C_torch, Z2_torch, torch.tensor(1, dtype=torch.double), retain_graph=True)[0]
dZ2_torch
```

    tensor([[-0.1242]], dtype=torch.float64)

### $\frac{\partial C}{\partial \mathbf{b}^{(2)}}$: (1,1)

```
db2 = dZ2
db2
```

    array([-0.1242])

```
#                              종속변수, 독립변수, 종속변수와 곱해지는 상위 그래디언트
dW_torch = torch.autograd.grad(C_torch, W_torch, torch.tensor(1, dtype=torch.double), retain_graph=True)[0]
dW_torch[2,0]
```

    tensor(-0.1242, dtype=torch.float64)

### $\frac{\partial C}{\partial \mathbf{h}^{(2)}}$: (1,1)

```
dH2 = dZ2
dH2_torch = dZ2_torch
```

### $\frac{\partial C}{\partial \mathbf{W}^{(2)}}$: (1,2)

```
dH2.reshape(-1,1)
```

    array([[-0.1242]])

```
dH2_dW2 = A1.T # dH2/dW2는 야코비안이 아니라 일반화 야코비안으로 (1, (1,2))가 되야 함

dW2 = np.dot(dH2_dW2.T, dH2.reshape(-1,1)) # 야코비안 전치 곱하기 그레디언트
dW2.T #여기서 결과를 다시 전치시켜야 (1,2)가 되는 이유는 일반화 야코비안과 관련있고 뒤에 다시 설명해야함.
```

    array([[-0.0084, -0.1242]])

```
dW_torch[2,1:]
```

    tensor([-0.0084, -0.1242], dtype=torch.float64)

### $\frac{\partial C}{\partial \mathbf{a}^{(1)}}$: (2,1)

```
dH2_dA1 = W[2:,1:] # dH2/dA1은 (1,2)인 야코비안이 맞고
dH2_dA1.shape
```

    (1, 2)

```
dA1 = np.dot(dH2_dA1.T, dH2.reshape(-1,1)) # 따라서 야코비안 전치 그래디언트가 성립, W.T * delta

dA1
```

    array([[ 0.045 ],
           [-0.0185]])

```
#                              종속변수, 독립변수, 종속변수와 곱해지는 상위 그래디언트
dA1_torch = torch.autograd.grad(C_torch, A1_torch, torch.tensor(1, dtype=torch.double), retain_graph=True)[0]
dA1_torch
```

    tensor([[ 0.0450],
            [-0.0185]], dtype=torch.float64)

### $\frac{\partial C}{\partial \mathbf{z}^{(1)}}$: (2,1)

```
dA1_dZ1 =  np.zeros((A1.shape[0], Z1.shape[0]))
dA1_dZ1[np.diag_indices(Z1.shape[0])] = (logistic(Z1)*(1-logistic(Z1))).reshape(-1)

dA1_dZ1
```

    array([[6.3132e-02, 0.0000e+00],
           [0.0000e+00, 2.2958e-05]])

```
dZ1 = np.dot(dA1_dZ1.T, dA1)
dZ1
```

    array([[ 2.8396e-03],
           [-4.2398e-07]])

```
#                              종속변수, 독립변수, 종속변수와 곱해지는 상위 그래디언트
dZ1_torch = torch.autograd.grad(C_torch, Z1_torch, torch.tensor(1, dtype=torch.double), retain_graph=True)[0]
dZ1_torch
```

    tensor([[ 2.8396e-03],
            [-4.2398e-07]], dtype=torch.float64)

- 굳이 이렇게 어렵게 하지말고 그냥 엘리먼트 와이즈(아다마르 곱)로 처리하는 것이 더 간편하다.

```
dA1_dZ1 = logistic(Z1)*(1-logistic(Z1))

dA1_dZ1
```

    array([[6.3132e-02],
           [2.2958e-05]])

```
dZ1_ = dA1_dZ1 * dA1
dZ1_
```

    array([[ 2.8396e-03],
           [-4.2398e-07]])

### $\frac{\partial C}{\partial \mathbf{b}^{(1)}}$: (2,1)

```
db1 = dZ1
db1
```

    array([[ 2.8396e-03],
           [-4.2398e-07]])

```
dW_torch[:2,0].view(-1,1)
```

    tensor([[ 2.8396e-03],
            [-4.2398e-07]], dtype=torch.float64)

### $\frac{\partial C}{\partial \mathbf{h}^{(1)}}$: (2,1)

```
dH1 = dZ1
dH1_torch = dZ1_torch

dH1_torch
```

    tensor([[ 2.8396e-03],
            [-4.2398e-07]], dtype=torch.float64)

```
dH1
```

    array([[ 2.8396e-03],
           [-4.2398e-07]])

### $\frac{\partial C}{\partial \mathbf{W}^{(1)}}$: (2,2)

```
dH1_dW1 = np.zeros((2,2,2))
dH1_dW1[0,0,:] = x
dH1_dW1[1,1,:] = x

print_tensor(dH1_dW1)

```

    dH1_dW1:(2, 2, 2),float64
    [[[2.754  3.5407]
      [0.     0.    ]]

     [[0.     0.    ]
      [2.754  3.5407]]]
    -------------------------------------------

```
dW1 = (dH1_dW1*dH1.reshape(dH1.shape[0], 1, 1)).sum(axis=0)
dW1
```

    array([[ 7.8203e-03,  1.0054e-02],
           [-1.1676e-06, -1.5012e-06]])

```
dW_torch[:2,1:]
```

    tensor([[ 7.8203e-03,  1.0054e-02],
            [-1.1676e-06, -1.5012e-06]], dtype=torch.float64)

```
print(dH1.shape)
print(x.shape)
np.dot(dH1, x)
```

    (2, 1)
    (1, 2)

    array([[ 7.8203e-03,  1.0054e-02],
           [-1.1676e-06, -1.5012e-06]])

### 텐서 평활화

```
T = np.arange(8).reshape(2,2,2)
T
```

    array([[[0, 1],
            [2, 3]],

           [[4, 5],
            [6, 7]]])

```
T.reshape(-1,2)

```

    array([[0, 1],
           [2, 3],
           [4, 5],
           [6, 7]])

```
T.reshape(-1,2).reshape(2,2,2)
```

    array([[[0, 1],
            [2, 3]],

           [[4, 5],
            [6, 7]]])

### 텐서를 평활화해서 경사도벡터와 곱하는 한줄 코드

```
np.dot(dH1_dW1.transpose(1,2,0).reshape(-1,2), dH1).reshape(2, 2, 1).squeeze()
```

    array([[ 7.8203e-03,  1.0054e-02],
           [-1.1676e-06, -1.5012e-06]])

## 데이터가 여러개인 경우

```
N=3
x = samples[[0,5,10]]
x_torch = torch.tensor(x, dtype=torch.double); x_torch.requires_grad=True
t = target[[0,5,10]]

print_tensor(x)
print_tensor(x_torch)
print_tensor(t)
```

    x:(3, 2),float64
    [[2.754  3.5407]
     [0.1494 2.2842]
     [1.4176 3.4657]]
    -------------------------------------------
    x_torch:torch.Size([3, 2]),torch.float64
    tensor([[2.7540, 3.5407],
            [0.1494, 2.2842],
            [1.4176, 3.4657]], dtype=torch.float64, requires_grad=True)
    -------------------------------------------
    t:(3,),float64
    [1. 0. 1.]
    -------------------------------------------

### 순전파와 역전파를 동시에 하는 함수

```
def forward_backward(X, W, T):
    """
    네트워크를 포워드, 백워드 시킨다. numpy 버전
    X : 네트워크의 입력벡터 shape:(N,2)
    반환 : 목적함수, (W1에 대한 미분계수, b1에 대한 미분계수,
                     W2에 대한 미분계수, b2에 대한 미분계수)
    """
    # forward
    N = X.shape[0]

    H1 = np.dot(W[:2,1:], X.T)
    Z1 = H1 + W[:2,0].reshape(-1,1)
    A1 = logistic(Z1)

    H2 = np.dot(W[2,1:], A1)
    Z2 = H2 + W[2,0]
    A2 = logistic(Z2)

    C = (1/(2*N)) * ((T-A2)**2).sum()

    # backward, dA->dZ->db->dW
    dA2 = -(T-A2)/N
    dA2_dZ2 = logistic(Z2)*(1-logistic(Z2))
    dZ2 = dA2_dZ2 * dA2

    db2 = dZ2
    dW2 = np.dot(dZ2, A1.T)

    dH2_dA1 = W[2:,1:]
    dA1 = np.dot(dH2_dA1.T, dZ2.reshape(-1,1))
    dA1_dZ1 = logistic(Z1)*(1-logistic(Z1))
    dZ1 = dA1_dZ1 * dA1

    db1 = dZ1
    dW1 = np.dot(dZ1, X)

    return C, (dW1, db1, dW2, db2)

```

### 모든 데이터에 대해 각각 역전파한 미분계수를 평균하기

```
f, dW1, db1, dW2, db2 = 0, 0, 0, 0, 0

for x_, t_ in zip(x, t):
    x_ = np.array([x_])
    t_ = np.array([t_])

    fv, derivs = forward_backward(x_, W, t_)

    f += fv
    dW1 += derivs[0]
    db1 += derivs[1]
    dW2 += derivs[2]
    db2 += derivs[3]

f   /= N
dW1 /= N
db1 /= N
dW2 /= N
db2 /= N

print('C:', f)
print_tensor(dW1)
print_tensor(db1)
print_tensor(dW2)
print_tensor(db2)
```

    C: 0.12001678911062634
    dW1:(2, 2),float64
    [[7.9160e-03 1.2443e-02]
     [1.5168e-06 4.1280e-05]]
    -------------------------------------------
    db1:(2, 1),float64
    [[2.8791e-03]
     [1.8509e-05]]
    -------------------------------------------
    dW2:(2,),float64
    [ 0.0061 -0.0499]
    -------------------------------------------
    db2:(1,),float64
    [-0.0497]
    -------------------------------------------

### 역전파 한번으로 미분계수 구하기

```
x
```

    array([[2.754 , 3.5407],
           [0.1494, 2.2842],
           [1.4176, 3.4657]])

```
H1, Z1, A1, H2, Z2, A2, C = forward(x, W.reshape(3,3), t)

print_tensor(H1)
print_tensor(Z1)
print_tensor(A1)
print_tensor(H2)
print_tensor(Z2)
print_tensor(A2)
print_tensor(C)
```

    H1:(2, 3),float64
    [[-2.8986  1.148  -0.4669]
     [ 9.5365  4.4643  8.0089]]
    -------------------------------------------
    Z1:(2, 3),float64
    [[-2.6223  1.4243 -0.1907]
     [10.6818  5.6096  9.1542]]
    -------------------------------------------
    A1:(2, 3),float64
    [[0.0677 0.806  0.4525]
     [1.     0.9964 0.9999]]
    -------------------------------------------
    H2:(3,),float64
    [ 0.1242 -0.1437 -0.0152]
    -------------------------------------------
    Z2:(3,),float64
    [ 0.0125 -0.2554 -0.1269]
    -------------------------------------------
    A2:(3,),float64
    [0.5031 0.4365 0.4683]
    -------------------------------------------
    C:(),float64
    0.12001678911062633
    -------------------------------------------

```
H1_torch, Z1_torch, A1_torch, H2_torch, Z2_torch, A2_torch, C_torch = forward_torch(x_torch, W_torch, t)

print_tensor(H1_torch)
print_tensor(Z1_torch)
print_tensor(A1_torch)
print_tensor(H2_torch)
print_tensor(Z2_torch)
print_tensor(A2_torch)
print_tensor(C_torch)
```

    H1_torch:torch.Size([2, 3]),torch.float64
    tensor([[-2.8986,  1.1480, -0.4669],
            [ 9.5365,  4.4643,  8.0089]], dtype=torch.float64,
           grad_fn=<MmBackward>)
    -------------------------------------------
    Z1_torch:torch.Size([2, 3]),torch.float64
    tensor([[-2.6223,  1.4243, -0.1907],
            [10.6818,  5.6096,  9.1542]], dtype=torch.float64,
           grad_fn=<AddBackward0>)
    -------------------------------------------
    A1_torch:torch.Size([2, 3]),torch.float64
    tensor([[0.0677, 0.8060, 0.4525],
            [1.0000, 0.9964, 0.9999]], dtype=torch.float64,
           grad_fn=<SigmoidBackward>)
    -------------------------------------------
    H2_torch:torch.Size([1, 3]),torch.float64
    tensor([[ 0.1242, -0.1437, -0.0152]], dtype=torch.float64,
           grad_fn=<MmBackward>)
    -------------------------------------------
    Z2_torch:torch.Size([1, 3]),torch.float64
    tensor([[ 0.0125, -0.2554, -0.1269]], dtype=torch.float64,
           grad_fn=<AddBackward0>)
    -------------------------------------------
    A2_torch:torch.Size([1, 3]),torch.float64
    tensor([[0.5031, 0.4365, 0.4683]], dtype=torch.float64,
           grad_fn=<SigmoidBackward>)
    -------------------------------------------
    C_torch:torch.Size([]),torch.float64
    tensor(0.1200, dtype=torch.float64, grad_fn=<MulBackward0>)
    -------------------------------------------

### $\frac{\partial C}{\partial \mathbf{a}^{(2)}}$: (1,N)

```
dA2 = -(t-A2)/N

dA2
```

    array([-0.1656,  0.1455, -0.1772])

### $\frac{\partial C}{\partial \mathbf{z}^{(2)}}$: (1,N)

```
dA2_dZ2 = logistic(Z2)*(1-logistic(Z2))
dZ2 = dA2_dZ2 * dA2

dZ2
```

    array([-0.0414,  0.0358, -0.0441])

### $\frac{\partial C}{\partial \mathbf{b}^{(2)}}$: (1,1)

```
db2 = dZ2.sum(axis=0, keepdims=True)
dH2 = dZ2

db2
```

    array([-0.0497])

### $\frac{\partial C}{\partial \mathbf{W}^{(2)}}$: (1,2)

```
dH2_dW2 = A1.T
dW2 = np.dot(dH2, dH2_dW2) # 정리된 결과로 야코비안 전치 평활화 따위 하지 않고 바로 delta * a.T

dW2
```

    array([ 0.0061, -0.0499])

```
dH2.shape
```

    (3,)

```
dH2.reshape(1,-1).shape
```

    (1, 3)

```
dH2.reshape(-1,1).shape
```

    (3, 1)

### $\frac{\partial C}{\partial \mathbf{a}^{(1)}}$: (2,N)

```
dH2_dA1 = W[2:,1:] # 분자레이아웃 아코비안 W2 (1,2)
dA1 = np.dot(dH2_dA1.T, dH2.reshape(1,-1)) # W.T * delta

dA1
```

    array([[ 0.015 , -0.013 ,  0.016 ],
           [-0.0062,  0.0053, -0.0066]])

### $\frac{\partial C}{\partial \mathbf{z}^{(1)}}$: (2,N)

```
dA1_dZ1 =  logistic(Z1)*(1-logistic(Z1))

dA1_dZ1
```

    array([[6.3132e-02, 1.5636e-01, 2.4774e-01],
           [2.2958e-05, 3.6357e-03, 1.0576e-04]])

```
dZ1 = dA1_dZ1 * dA1 # 엘리먼트와이즈 곱

dZ1
```

    array([[ 9.4654e-04, -2.0261e-03,  3.9587e-03],
           [-1.4133e-07,  1.9344e-05, -6.9385e-07]])

### $\frac{\partial C}{\partial \mathbf{b}^{(1)}}$: (2,1)

```
db1 = dZ1.sum(axis=1, keepdims=True)
db1
```

    array([[2.8791e-03],
           [1.8509e-05]])

### $\frac{\partial C}{\partial \mathbf{W}^{(1)}}$: (2,2)

```
dW1 = np.dot(dZ1, x) # delta * a.T
dW1
```

    array([[7.9160e-03, 1.2443e-02],
           [1.5168e-06, 4.1280e-05]])

### 파이토치로 미분하기

```
#                              종속변수, 독립변수, 종속변수와 곱해지는 상위 그래디언트
dW_torch = torch.autograd.grad(C_torch, W_torch, torch.tensor(1, dtype=torch.double), retain_graph=True)[0]
dW_torch
```

    tensor([[ 2.8791e-03,  7.9160e-03,  1.2443e-02],
            [ 1.8509e-05,  1.5168e-06,  4.1280e-05],
            [-4.9747e-02,  6.0737e-03, -4.9872e-02]], dtype=torch.float64)

```

```


# Calculations of supercurrents: examples

## The Python conda environment that is used
* We used the following Python environment. See [README.md](README.md) for installation instructions.
```yaml
name: python3
channels:
  - conda-forge
dependencies:
  - python=3.6
  - kwant=1.3
  - holoviews=1.8.1
  - xarray=0.9.6
  - pandas=0.20.3
  - pytables=3.4.2
  - toolz=0.8.2
  - hpc05  # This is only needed if you use a PBS cluster with headnode, as on the TU Delft network
```

```python
import holoviews as hv
import kwant
import numpy as np

# Local imports
import funcs
from funcs import constants, discretized_hamiltonian, get_cuts, add_vlead, hopping_between_cuts

%matplotlib inline
hv.notebook_extension()

def plot_CPR(syst, hopping, params, tol=0.01, max_frequencies=1000):
    phases = np.linspace(-np.pi, np.pi, 51)
    H_0_cache = []
    I = [funcs.current_at_phase(syst, hopping, params, H_0_cache, phase, tol, max_frequencies)
                  for phase in phases]

    return hv.Curve((phases, I), kdims=['phase'], vdims=['$I$'], label='Nummerical CPR')
```

```python
import sympy
```

```python
sympy.__version__
```

    '1.2'

# 1D

```python
syst, hopping = funcs.make_1d_wire(a=10, L=100, L_sc=100)

params = dict(T=0.06, Delta=0.250, mu=15, k=constants.k,
              hbar=constants.hbar, m_eff=constants.m_eff, current_unit=constants.current_unit, c=constants.c,
              B_x=0, B_y=0, B_z=0, g=0, mu_B=0, alpha=0, V=lambda x:0)

plot_CPR(syst, hopping, params)
```

    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The datapath rcparam was deprecated in Matplotlib 3.2.1 and will be removed two minor releases later.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The savefig.frameon rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The text.latex.unicode rcparam was deprecated in Matplotlib 3.0 and will be removed in 3.2.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The verbose.fileo rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The verbose.level rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The datapath rcparam was deprecated in Matplotlib 3.2.1 and will be removed two minor releases later.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The savefig.frameon rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The text.latex.unicode rcparam was deprecated in Matplotlib 3.0 and will be removed in 3.2.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The verbose.fileo rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The verbose.level rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]

# Simpler way of calculating the super current (but slower)

```python
from funcs import matsubara_frequency

def null_G(syst, params, n):
    en = matsubara_frequency(n, params)
    gf = kwant.greens_function(syst, en, out_leads=[0], in_leads=[0],
                               check_hermiticity=False, params=params)
    return gf.data[::2, ::2]

def current_from_G_0(G_0s, H12, phase, params):
    t = H12 * np.exp(1j * phase)
    dim = t.shape[0]
    I = 0
    for G_0 in G_0s:
        V = np.zeros_like(G_0, dtype=complex)
        v = t - H12
        V[:dim, dim:] = v.T.conj()
        V[dim:, :dim] = v
        gf = np.linalg.solve(np.identity(2*dim) - G_0 @ V, G_0)
        H12G21 = t.T.conj() @ gf[dim:, :dim]
        H21G12 = t @ gf[:dim, dim:]
        I += -4 * params['T'] * params['current_unit'] * (np.trace(H21G12) - np.trace(H12G21)).imag
    return I

matsfreqs = 500
G_0s = [null_G(syst, params, n) for n in range(matsfreqs)]
H12 = hopping(syst, params=params)
phases = np.linspace(-np.pi, np.pi, 51)
I = [current_from_G_0(G_0s, H12, phase, params) for phase in phases]
hv.Curve((phases, I), kdims=['phase'], vdims=['$I$'])
```

    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The datapath rcparam was deprecated in Matplotlib 3.2.1 and will be removed two minor releases later.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The savefig.frameon rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The text.latex.unicode rcparam was deprecated in Matplotlib 3.0 and will be removed in 3.2.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The verbose.fileo rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The verbose.level rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The datapath rcparam was deprecated in Matplotlib 3.2.1 and will be removed two minor releases later.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The savefig.frameon rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The text.latex.unicode rcparam was deprecated in Matplotlib 3.0 and will be removed in 3.2.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The verbose.fileo rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]
    /home/bomin/anaconda3/envs/python3/lib/python3.6/_collections_abc.py:841: MatplotlibDeprecationWarning:
    The verbose.level rcparam was deprecated in Matplotlib 3.1 and will be removed in 3.3.
      self[key] = other[key]

# Test 2D system and compare with exact equation

```python
a = 1

syst, hopping = funcs.make_2d_test_system(Y=2*a, X=2*a, a=a)
kwant.plot(syst);

params = dict(T=0.0001, hbar=constants.hbar, m=constants.m_eff,
              Delta=0.001, mu=constants.t*4 / a**2, k=constants.k, current_unit=constants.current_unit, c=constants.c)
kwant.plotter.bands(syst.leads[1], params=params);

# Analytical comparison
phases = np.linspace(-np.pi, np.pi, 51)
N_modes = 2
tanh_part = np.tanh(params['Delta'] / (2 * params['T'] * params['k']) * np.sqrt(1 - np.sin(0.5 * phases)**2))
sum_part = np.sin(phases) / np.sqrt(1 - np.sin(0.5 * phases)**2)
prefactor = constants.eV * (params['Delta'] * constants.meV) / (2 * constants.hbar) # Delta in J
I_analytical = prefactor * N_modes * sum_part * tanh_part * 1e9 # Eq. (35) from arXiv:cond-mat/0406127v2

(plot_CPR(syst, hopping, params, tol=0.001) *
 hv.Curve((phases, I_analytical), kdims=['phase'], vdims=['$I$'], label='Analytical result'))
```

# 3D test system

```python
sx_ = np.array([[0., 1.], [1., 0.]])
sz_ = np.array([[1., 0.], [0., -1.]])

onsite = lambda site, mu: -mu * sz_
onsite_lead = lambda site, mu, delta: -mu * sz_ + delta * sx_
hops = lambda site1, site2, t: -t * sz_

def make_test_system(X, Y, Z):
    lat = kwant.lattice.general(np.eye(3), norbs=2)
    syst = kwant.Builder()
    syst[(lat(x, y, z) for x in range(X) for y in range(Y) for z in range(Z))] = onsite
    syst[lat.neighbors()] = hops

    cuts = get_cuts(syst, lat)
    syst = add_vlead(syst, lat, *cuts)

    lead = kwant.Builder(kwant.TranslationalSymmetry((-1, 0, 0)))
    lead[(lat(0, y, z) for y in range(Y) for z in range(Z))] = onsite_lead
    lead[lat.neighbors()] = hops
    syst.attach_lead(lead)
    syst.attach_lead(lead.reversed())
    syst = syst.finalized()

    hopping = hopping_between_cuts(syst, *cuts)

    return syst, hopping

delta = 0.01
params = dict(mu=0., t=1., delta=delta, k=1, current_unit=1, T=delta/100)
syst, hopping = make_test_system(X=3, Y=3, Z=3)
kwant.plot(syst);
plot_CPR(syst, hopping, params)
```

# 3D system with Discretizer Hamiltonian, squared system

```python
# Test Hamiltonian
syst, hopping = funcs.make_3d_test_system(X=5, Y=3, Z=3, a=1, test_hamiltonian=True)
params = dict(T=0.01, Delta=1, mu=2, t=1, k=1, current_unit=1, c=1)
kwant.plotter.bands(syst.leads[2], params=params)
plot_CPR(syst, hopping, params)
```

```python
# Full Hamiltonian
syst, hopping = funcs.make_3d_test_system(X=30, Y=30, Z=30, a=10, test_hamiltonian=False)
params = dict(T=1, Delta=0.250, mu=35, B_x=0, B_y=0, B_z=0, g=0, alpha=0, V=lambda x:0, **constants.__dict__)
kwant.plot(syst);
kwant.plotter.bands(syst.leads[2], params=params);
plot_CPR(syst, hopping, params)
```

# 3D full wire

```python
syst_pars = dict(a=8, angle=0, site_disorder=True, L=80, L_sc=8,
                 phi=135, r1=50, r2=70, shape='circle', with_leads=True,
                 with_shell=True, with_vlead=False, holes=True)

syst, hopping = funcs.make_3d_wire(**syst_pars)

kwant.plot(syst)
```

```python

```


# Ensemble Regression

```python
# modules
import random as rd  # generating random numbers for distributions
import pandas as pd  # data mangling and transforming
import numpy as np  # handling vectors and matrices
import matplotlib.pyplot as plt  # plotting module
import seaborn as sns  # pairplot & heatmap for correlations
import graphviz  # dendograms
import sympy  # linear dependencies
import torch  # neural net

from math import sqrt  # mathematical operators
from sklearn import tree, linear_model  # lm + regression & classification trees
from sklearn.preprocessing import StandardScaler, MinMaxScaler, Normalizer, PolynomialFeatures # preprocessing tools
from sklearn.feature_selection import VarianceThreshold, RFE, SelectFromModel
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor, ExtraTreesClassifier, AdaBoostRegressor
from sklearn.metrics import mean_squared_error, r2_score

import torch
import torch.nn.functional as F
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from torch.autograd import Variable
```

```python
# plot inside notebook
get_ipython().run_line_magic('matplotlib', 'inline')
```

## Test data

```python
n = 60000
```

```python
df = pd.DataFrame(data={'MTTF': np.random.choice(range(0, 3500), n),
                        'longitude': np.random.choice(range(10, 50), n),
                        'latitude': np.random.choice(range(10, 50), n),
                        'A': np.append(np.random.choice(range(1, 5), n-1), np.nan),
                        'B': np.random.choice(range(1, 500000), n),
                        'C': np.random.choice(range(0, 50000), n),
                        'D': np.random.choice(['Sam', 'Som', 'Sim'], n),
                        'E': np.random.choice([''.join(rd.choices(string.ascii_uppercase, k=5))
                                               for i in range(10)], n),
                       })
```

## Helper Functions

```python
# create new dataframe after removing variables with variance below threshold
def varianceSelection(X, THRESHOLD = .1):
    sel = VarianceThreshold(threshold=THRESHOLD)
    sel.fit_transform(X)
    return X[[c for (s, c) in zip(sel.get_support(), X.columns.values) if s]]

# calculate own score robustly
def calc_zscore(col, ddof=0):
    return (col - col.mean())/col.std(ddof=ddof)
```

```python
## Dataset(s)

### inverter
```

```python
# read data
df = pd.read_csv('inverters.csv', sep=';')
```

```python
# change index to be udid
df.index = df.UDID
```

```python
# additional columns
for v in ['MEAN', 'MEDIAN', 'MIN', 'MAX']:
    df.loc[:,'ACDC_RATIO_'+v] = df.loc[:,'AC_POWER_'+v] / df.loc[:,'DC_POWER_'+v]
    df.loc[:,'AC_POWER_NORM_'+v] = df.loc[:,'AC_POWER_'+v] / df.loc[:,'POWER_NOMINAL']
    df.loc[:,'DC_POWER_NORM_'+v] = df.loc[:,'DC_POWER_'+v] / df.loc[:,'POWER_NOMINAL']
    df.loc[:,'TEMP_DIFF_'+v] = df.loc[:,'TEMPERATURE_'+v] / df.loc[:,'FELTTEMPERATURE_'+v]
```

```python
# keep track of variable with most missings by creating a dummy
df['DC_MISSING_DUMMY'] = df.DC_VOLTAGE_MEAN.isnull()*1
```

```python
# categorization of columns
names_drop = ['UDID', 'DEVICE_TYPE']
names_y1 = ['MEAN_TTF']
names_y2 = ['AVAILABILITY', 'MEDIAN_TTF', 'STD_TTF', 'MEAN_TTR',
            'MEDIAN_TTR', 'STD_TTR', 'MEAN_TBF', 'MEDIAN_TBF', 'STD_TBF']
names_X = list(set(df.columns) - set(names_drop) - set(names_y1) - set(names_y2))
names_disc = ['SOLAR_CLIMATE_ZONE', 'VENDOR', 'POWER_NOMINAL_RANGE', 'POWER_MAX_RANGE']
names_median = []
for n in names_X:
    if 'MEDIAN' in n:
        names_median.append(n)
names_acdc = []
for n in names_X:
    if 'AC' in n or 'DC' in n:
        names_acdc.append(n)
```

```python
df.drop(names_drop, inplace=True, axis=1)
df.head()
```

```python
#### Notizen
Variablen seit 2017:
- DIF
- DNI
- ETR
- FELTT
- GHI
- GNI
- HUMIDITY
- SLP
- TEMP

Wichtige Air Quality Variablen:
- AQI
- pm10
- pm25
- o3
- no2
```

```python
### Air Quality
```

```python
# read data
aqi = pd.read_csv('aqi_out.csv', sep=',')
aqi.index = aqi.UDID
aqi = aqi[['AQI','pm10','pm25','o3','no2']]
```

```python
# join together by index
df = df.join(aqi)
```

```python
## Only consider inverters which had at least 1 failure!!!
```

```python
df = df[df.AVAILABILITY!=100]

## Exploratory View
```

```python
### Boxplot
```

```python
# boxplot
print('MEAN')
plt.boxplot(df.MEAN_TTF)
plt.show()
print('MEDIAN')
plt.boxplot(df.MEDIAN_TTF)
plt.show()

# ### Pairplot

# In[14]:

# # Create pairplot for first groups of median variables
# for i in list(range(8,12,4)):
#     pp = sns.pairplot(df[names_y1+names_median[i-3:i]+['POWER_NOMINAL_RANGE']], hue='POWER_NOMINAL_RANGE')
#     pp.savefig('plots/pp_'+str(i)+'.png')

# ### Correlation Matrix

# In[15]:

corr = df.corr()
sns.set(rc={'figure.figsize':(19,16)})
correlation_matrix = sns.heatmap(corr,
                                 cmap='RdBu',
                                 xticklabels=corr.columns.values,
                                 yticklabels=corr.columns.values,
                                 annot=False)
fig = correlation_matrix.get_figure()
fig.savefig('plots/correlation_matrix_raw.png')

# In[16]:

# remove other ys
df.drop(names_y2, inplace=True, axis=1)

# In[17]:

# move y variable to first place
cols = list(df)
cols.insert(0, cols.pop(cols.index(names_y1[0])))
# use loc to reorder
df = df.loc[:, cols]

# In[18]:

corr = df.corr()
sns.set(rc={'figure.figsize':(19,16)})
correlation_matrix = sns.heatmap(corr,
                                 cmap='RdBu',
                                 xticklabels=corr.columns.values,
                                 yticklabels=corr.columns.values,
                                 annot=False)
fig = correlation_matrix.get_figure()
fig.savefig('plots/correlation_matrix_raw_sorted.png')

# In[19]:

# save top 10 correlated X
top_by_lincorr = list(abs(corr[names_y1[0]]).sort_values()[-15:-5].index)

# In[20]:

top_by_lincorr

# In[21]:

# Create pairplot for relevant variables
pp = sns.pairplot(df[names_y1+top_by_lincorr[5:]+['POWER_NOMINAL_RANGE']], hue='POWER_NOMINAL_RANGE')
pp.savefig('plots/pp_top.png')

# ## Data Cleaning

# In[22]:

# new df object
df_clean = df

# ### Filter known mistakes

# In[23]:

# delete AC mistakes
df_clean = df_clean[df_clean.AC_POWER_MEDIAN.notnull()]

# ### Missing Values

# #### Imputing (by domain knowledge)

# In[24]:

# use raw approximation, if other two values are available for DC
for v in ['MEAN', 'MEDIAN', 'MIN', 'MAX']:
    df_clean['DC_VOLTAGE_'+v].fillna(df_clean['DC_POWER_'+v]/df_clean['DC_CURRENT_'+v], inplace=True)
    df_clean['DC_CURRENT_'+v].fillna(df_clean['DC_POWER_'+v]/df_clean['DC_VOLTAGE_'+v], inplace=True)
    df_clean['DC_POWER_'+v].fillna(df_clean['DC_CURRENT_'+v]*df_clean['DC_VOLTAGE_'+v], inplace=True)

# In[25]:

# user power nominal for few missing values
df_clean['PPEAK'].fillna(df_clean['POWER_NOMINAL'], inplace=True)
df_clean['POWER_MAX'].fillna(df_clean['POWER_NOMINAL'], inplace=True)
df_clean['POWER_MAX_RANGE'].fillna(df_clean['POWER_NOMINAL_RANGE'], inplace=True)

# In[26]:

# transform infs to nan
df_clean = df_clean.replace([np.inf, -np.inf], np.nan)

# #### Imputing (by median)

# In[27]:

check_nulls = df_clean.isnull().sum()
names_mi = list(check_nulls[check_nulls > 0].index)

# In[28]:

for c in names_mi:
    df_clean[c].fillna(df_clean[c].median(), inplace=True)

# In[29]:

# double checking
df_clean.isnull().values.any()

# ### Outlier

# In[30]:

dfo = df_clean.select_dtypes(include=[np.number])

# In[31]:

dfoz = dfo.apply(calc_zscore, axis=0)

# In[32]:

# variables with zero variance
dfoz.fillna(0, inplace=True)

# In[33]:

dfo = dfo[(np.abs(dfoz) < 3).all(axis=1)]

# In[34]:

# join back non-numerical features
df_clean = dfo.join(df_clean.select_dtypes(exclude=[np.number]))

# In[35]:

print(str(len(df) - len(df_clean))+' row(s) got kicked out!')

# ### Dummy vars for non-numeric features

# In[36]:

df_clean = pd.get_dummies(df_clean, drop_first=True)

# In[37]:

df_clean.head()

# ## Feature Cleaning

# In[38]:

X0 = df_clean.drop(names_y1, axis=1)

# ### 1 - Drop features with low variance

# In[39]:

X1 = varianceSelection(X0, THRESHOLD=0.05)  # Variance at least 0.05

# In[40]:

to_drop1 = list(set(X0.columns) - set(X1.columns))

# In[41]:

print(str(len(to_drop1))+' features got kicked out!')

# ### 2 - Drop highly correlated features

# In[42]:

# Create correlation matrix
corr_matrix = X1.corr().abs()

# In[43]:

# Select upper triangle of correlation matrix
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool))

# In[44]:

# Find index of feature columns with correlation greater than 0.95
to_drop2 = [column for column in upper.columns if any(upper[column] > 0.95)]

# In[45]:

# Drop features
X2 = X1.drop(to_drop2, axis=1)

# In[46]:

print(str(len(to_drop2))+' features got kicked out!')

# ### 3 - Drop features which are linearly dependent

# In[47]:

# reduced_form, inds = sympy.Matrix(X2.values).rref()

# In[48]:

# X3 = X2.iloc[:, list(inds)]

# In[49]:

print('We end up with %d features'%X2.shape[1])

# ## Feature Normalization & Scaling

# ### Normalize

# In[50]:

normalizer = Normalizer().fit(X2)
X_norm = pd.DataFrame(normalizer.transform(X2), columns=X2.columns)

# ### Scale

# In[51]:

scaler_mm = MinMaxScaler()
X_scalem = pd.DataFrame(scaler_mm.fit_transform(X2), columns=X2.columns)

# In[52]:

scaler_s = StandardScaler()
X_scales = pd.DataFrame(scaler_s.fit_transform(X2), columns=X2.columns)

# ## Feature Selection

# In[53]:

# define dependent and independent variables
y = df_clean[names_y1[0]]
y_log = np.log(y)
X = X_scalem

# In[54]:

# build 100 trees & show variable importance
clf = RandomForestRegressor(n_estimators=100)
clf = clf.fit(X, y)

# In[55]:

importances = clf.feature_importances_
std = np.std([tree.feature_importances_ for tree in clf.estimators_],
             axis=0)
indices = np.argsort(importances)[::-1]

# Print the feature ranking
print("Feature ranking:")

top_by_rf = []
for f in range(X.shape[1]):
    top_by_rf.append(X.columns[indices[f]])
    print("%d. %s (%f)" % (f + 1, X.columns[indices[f]], importances[indices[f]]))

# **The importances add up to 100% and are calculated based on variance reduction achievable by each feature.**

# In[56]:

# Plot the feature importances of the forest
fig = plt.figure()
plt.title("Feature importances by Random Forest")
plt.bar(range(X.shape[1]), importances[indices],
       color="b", yerr=std[indices], align="center")
plt.xticks(range(X.shape[1]), top_by_rf, rotation=90)
plt.xlim([-1, X.shape[1]])
fig.savefig('plots/rf_feature_importances.png')
plt.show()

# ## Polynomial Features

# In[57]:

X_reduced_by_rf = X_scalem[top_by_rf[:4]]

# In[58]:

poly = PolynomialFeatures(2)
X_poly = poly.fit_transform(X_reduced_by_rf)

# ## Models

# ### Simple linear regression model: w = beta_hat*X + e

# In[59]:

X_reduced = X[top_by_rf]

# In[60]:

# Create linear regression object with normalized variables as benchmark
linreg = linear_model.LinearRegression(normalize=True)

# In[61]:

# Train the model using the training sets
linreg.fit(X_reduced, y)

# In[62]:

# Make predictions using the testing set
y_lr = linreg.predict(X_reduced)

# In[63]:

# The coefficients
print('Coefficients: \n', linreg.coef_)
# The mean squared error
print("Root Mean squared error (RMSE): %.2f"
      % sqrt(mean_squared_error(y, y_lr)))
# R^2
print('R² score: %.2f' % linreg.score(X_reduced, y))
print('Adj. R² score: %.2f' % (1-(1-linreg.score(X_reduced, y))*(len(y)-1)/(len(y)-X_reduced.shape[1]-1)))

# ### Regression Tree

# In[64]:

# create object
dtr = tree.DecisionTreeRegressor(criterion='mse', max_depth=8)

# In[65]:

# fit data
dtr.fit(X, y)

# In[66]:

# Predict (in-sample)
y_dt = dtr.predict(X)

# In[67]:

dot_data = tree.export_graphviz(dtr, out_file=None,
                                feature_names=list(X.columns),
                                filled=True, rounded=True,
                                special_characters=True)
graph = graphviz.Source(dot_data)
graph.render("plots/Regression Tree", format='png')

# In[68]:

print('RMSE for 1 decision tree: %4f'%sqrt(mean_squared_error(y, y_dt)))

# ### Gradient boosted forest

# In[69]:

# create object
gbr = GradientBoostingRegressor(n_estimators=500, max_depth=8,
                                min_samples_split=2, learning_rate=0.01,
                                loss='huber')

# In[70]:

# fit data
gbr.fit(X, y)

# In[71]:

# predict (in-sample)
y_gb = gbr.predict(X)

# In[72]:

print('RMSE for Gradient-Boosted Forest (500): %.4f'%sqrt(mean_squared_error(y, y_gb)))

# ### Ada-boosted Trees

# In[73]:

# create object
rng = np.random.RandomState(1)
abr = AdaBoostRegressor(tree.DecisionTreeRegressor(max_depth=8),
                        learning_rate=0.01, loss='linear',
                        n_estimators=500, random_state=rng)

# In[74]:

# fit data
abr.fit(X, y)

# In[75]:

# Predict (in-sample)
y_ab = abr.predict(X)

# In[76]:

print('RMSE for 500 ada boosted-trees: %4f'%sqrt(mean_squared_error(y, y_ab)))

# ### Neural Net

# #### Data

# In[285]:

# data
inputs = Variable(torch.from_numpy(np.array(X, dtype='float32')))
target = Variable(torch.from_numpy(np.array(y, dtype='float32')))

# In[286]:

# Define data loader
batch_size = 32
train_dl = DataLoader(TensorDataset(inputs, target),
                      batch_size,
                      shuffle=False,
                      num_workers=6)

# #### Define layers of the net

# In[287]:

nn_model = nn.Sequential(
    nn.Linear(inputs.shape[1], 50),
    nn.Tanh(),
    nn.Linear(50, 50),
    nn.Sigmoid(),
    nn.Linear(50, 100),
    nn.Tanh(),
    nn.Linear(100, 1)
    )

# #### define optimizer & loss function

# In[288]:

opt = torch.optim.SGD(nn_model.parameters(), lr=1e-07)
loss_fn = F.mse_loss

# In[289]:

def RMSELoss(yhat,y):
    return torch.sqrt(torch.mean((yhat-y)**2))

loss_fn = RMSELoss

# #### train the model

# In[290]:

# Define a utility function to train the model
def fit(df, num_epochs, model, loss_fn, opt):
    for epoch in range(num_epochs):
        for xb,yb in df:
            # Generate predictions
            pred = model(xb)
            loss = loss_fn(pred, yb)
            # Perform gradient descent
            opt.zero_grad()
            loss.backward()
            opt.step()
        if epoch%10==0:
            print("Epoch %s: Loss %s"%(epoch, loss.data.item()))
    print('Training loss: ', loss_fn(model(inputs), target).data.item())

# In[291]:

# Train the model for X epochs
fit(train_dl, 50, nn_model, loss_fn, opt)

# In[292]:

preds = nn_model(inputs)

# In[293]:

# save predictions as array
y_nn = preds.detach().numpy().squeeze()

# In[294]:

print('RMSE for Neural Net: %4f'%sqrt(mean_squared_error(y, y_nn)))

# In[295]:

plt.hist(y_nn)

# ### Ensemble Predictions

# In[ ]:

y_ens = np.mean([y_dt, y_gb, y_ab, y_nn], axis=0)

# In[ ]:

print('RMSE for Ensemble Predictions: %4f'%sqrt(mean_squared_error(y, y_ens)))

# ## Evaluation

# ### Descriptive plots

# In[ ]:

# create data_frame of predictions and true values
df_preds = pd.DataFrame(data={'y_true':y, 'y_lr':y_lr, 'y_dt':y_dt,
                              'y_gb':y_gb, 'y_ab':y_ab, 'y_nn':y_nn})

# In[ ]:

# add power range
df_preds = df_preds.join(df.POWER_NOMINAL_RANGE)

# In[ ]:

# pairplots
ppp = sns.pairplot(df_preds, hue='POWER_NOMINAL_RANGE')
ppp.savefig('plots/pp_predictions.png')

# In[ ]:

# coordinates for origin line
ol = list(range(0, 40000, 5000))

# In[ ]:

olp = sns.lmplot(x='y_nn', y='y_true', data=df_preds, fit_reg=False,
           hue='POWER_NOMINAL_RANGE', legend=False, palette="Set1")
plt.plot(ol, ol, linewidth=2)
plt.legend(loc='lower right')
olp.savefig('plots/true_vs_predictions.png')

# In[ ]:

resp = sns.residplot(x='y_gb', y='y_true', data=df_preds)
resp.figure.savefig('plots/residuals.png')

# In[ ]:
```


```python
from IPython.core.display import HTML
css_file = './custom.css'
HTML(open(css_file, "r").read())
```

###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2015 [David I. Ketcheson](http://davidketcheson.info)

##### Version 0.2 - May 2021

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from matplotlib import animation
from IPython.display import HTML
font = {'size'   : 15}
matplotlib.rc('font', **font)
```

# Pseudospectral collocation methods

Welcome to lesson 2 -- in which we'll learn about the main focus of this course: pseudospectral collocation methods for wave equations.  But first, a bit of review.

## Advection-diffusion (again)

$$
\newcommand{\F}{\mathcal F}
\newcommand{\Finv}{{\mathcal F}^{-1}}
$$

In notebook 1 we solved the scalar, linear 1D evolution equation:

$$u_t + u_x = \epsilon u_{xx}$$

by taking the Fourier transform, which "diagonalizes" this infinite-dimensional dynamical system:

$$\hat{u}_t + i\xi\hat{u} = -\xi^2 \epsilon \hat{u}$$

with solution

$$\hat{u}(t) = e^{(-i \xi - \epsilon \xi^2)t} \hat{u}(0)$$

for each mode.

To construct the full solution, we simply take the inverse Fourier transform.  All together, this algorithm looks like:

$$
u(t) = \Finv \left(D\left[e^{(-i \xi - \epsilon \xi^2)t}\right]\F(u) \right),
$$

where we have written $D[f(\xi)]$ to denote the diagonal matrix whose $(j,j)$ entry is given by $f(\xi_j)$.
In the exact solution, the wavenumbers $\xi$ range from $-\infty$ to $+\infty$ (and $D$ is infinite), but in practice we compute on a finite interval of length $L$ with $m$ collocation points.
The wavenumbers are then given by the formula

\begin{align}
\xi_j & = \frac{2 \pi j}{L} & \text{for } -m/2 \le j < m/2,
\end{align}

although the FFT routine orders them differently.

We can make the algorithm even more explicit by recognizing that the FFT (and its inverse) is a linear map, so it can be represented as a matrix multiplication.  Thus

$$\F(u) = Fu$$

where $F$ is a certain $m \times m$ matrix.  This matrix has a number of interesting properties (see e.g. Trefethen, Ch. 3), but for the moment we are only interested in the fact that it is a linear operator.  We can reverse engineer it by applying $\F$ to the columns of the identity matrix:

```python
def F_matrix(m):
    F = np.zeros((m,m),dtype=complex)
    for j in range(m):
        v = np.zeros(m)
        v[j] = 1.
        F[:,j] = np.fft.fft(v)
    return F

print(F_matrix(4))
```

Notice that $F$ is symmetric; this is true for any value of $m$.  Also, $F$ is essentially unitary -- that is, it is possible to choose a normalization such that $F$ is unitary, but the normalization in common use means that $FF^*=mI$ (and $F^{-1}F^{-*} = m^{-1} I$).

We won't work with this matrix in practice since applying it naively is much slower than the FFT.  But it is a useful representation.  Thus we can write our "algorithm" simply as

$$
u(t) = F^{-1} D\left[e^{(-i \xi - \epsilon \xi^2)(t-t_0)}\right] F u(t_0).
$$

Now it is completely clear that we are simply applying a similarity transformation that diagonalizes the dynamics of our system.
We can solve any linear, scalar constant-coefficient 1D evolution equation in similar manner, by simply replacing the argument of $D$.  Note that this algorithm is:

- Exact in time (the only error is due to our initial truncation of the Fourier series by sampling the data at a finite set of points); and
- Unconditionally stable.

Here is the implementation as a reminder.

```python
# Spatial grid
m=64                            # Number of grid points in space
L = 2 * np.pi                   # Width of spatial domain
x = np.arange(-m/2,m/2)*(L/m)   # Grid points
dx = x[1]-x[0]                  # Grid spacing

# Temporal grid
tmax=4.0     # Final time
N = 25       # number grid points in time
k = tmax/N   # interval between output times

xi = np.fft.fftfreq(m)*m*2*np.pi/L  # Wavenumber "grid"
# (this is the order in which numpy's FFT gives the frequencies)

# Initial data
u = np.sin(2*x)**2 * (x<-L/4)
uhat0 = np.fft.fft(u)

epsilon=0.01  # Diffusion coefficient
a = 1.0       # Advection coefficient

# Store solutions in a list for plotting later
frames = [u.copy()]

# Now we solve the problem
for n in range(1,N+1):
    t = n*k
    uhat = np.exp(-(1.j*xi*a + epsilon*xi**2)*t) * uhat0
    u = np.real(np.fft.ifft(uhat))
    frames.append(u.copy())

# Set up plotting
fig = plt.figure(figsize=(9,4)); axes = fig.add_subplot(111)
line, = axes.plot([],[],lw=3)
axes.set_xlim((x[0],x[-1])); axes.set_ylim((0.,1.))

def plot_frame(i):
    line.set_data(x,frames[i])
    axes.set_title('t='+str(i*k))

# Animate the solution
anim = matplotlib.animation.FuncAnimation(fig, plot_frame,
                                   frames=len(frames),
                                   interval=200)

HTML(anim.to_jshtml())
```

## Variable coefficients

Things become more interesting as soon as we introduce spatially-varying coefficients or nonlinearity.  In either case, the Fourier transform no longer provides a global diagonalization -- instead, it can be thought of as diagonalizing the system at one instant in time.  That means that we will have to discretize in time, and the time step we can use will be restricted by accuracy and (possibly) stability considerations.

Consider now the **variable-coefficient advection equation**

$$ u_t + a(x) u_x = 0.$$

In a purely spectral method, we would take the Fourier transform of this equation and we would need to compute a convolution of $a(x)$ with $u$, which is computationally quite expensive relative to the rest of what we will do.  Specifically, the convolution would require ${\mathcal O}(m^2)$ operations, whereas the FFT requires only ${\mathcal O}(m \log m)$.  To avoid this expense, we use the "pseudospectral" approach: we compute the derivative $u_x$ in the spectral way:

$$ u_x = F^{-1} \left(D\left[i\xi\right] F u\right),$$

and then multiply by $a(x)$ in physical space:

$$ (a(x) u_x)_j = a(x_j) \left(F^{-1}  \left(D\left[i\xi\right] F u\right) \right)_j.$$

Letting
$$
    A = \begin{pmatrix} a(x_1) \\ & a(x_2) \\ & & \ddots \\ & & & a(x_m) \end{pmatrix},
$$
we have the semi-discrete system

$$
U'(t) = -A \Finv \left(D\left[i\xi\right] \F(U)\right).
$$

### Time discretization

Next we need to integrate in time.  To choose an appropriate time integrator, we would like to know the spectrum of our semi-discretization.  We can compute it explicitly.

We have a linear ODE system of the form $U'(t) = MU(t)$, where
$$M = -AF^{-1}D[i\xi]F.$$
The code below computes the spectrum of $M$ for a few choices of $a(x)$.

```python
m = 32
F = F_matrix(m)
Finv = np.linalg.inv(F)
L = 2 * np.pi
xi = np.fft.fftfreq(m)*m*2*np.pi/L
D = np.diag(1.j*xi)
x = np.arange(-m/2,m/2)*(L/m)

M = np.dot(Finv,np.dot(D,F))
```

```python
def plot_spectrum(a,m=64):
    F = F_matrix(m)
    Finv = np.linalg.inv(F)
    L = 2 * np.pi
    xi = np.fft.fftfreq(m)*m*2*np.pi/L
    D = np.diag(1.j*xi)
    x = np.arange(-m/2,m/2)*(L/m)
    A = np.diag(a(x))
    M = -np.dot(A,np.dot(Finv,np.dot(D,F)))
    lamda = np.linalg.eigvals(M)
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8,4),
                                   gridspec_kw={'width_ratios': [3, 1]})
    ax1.plot(x,a(x)); ax1.set_xlim(x[0],x[-1])
    ax1.set_xlabel('$x$'); ax1.set_ylabel('$a(x)$')
    ax2.plot(np.real(lamda),np.imag(lamda),'ko')
    ax2.axis('equal')
    ax2.set_xlabel('$Re(\lambda)$')
    ax2.set_ylabel('$Im(\lambda)$')
    plt.tight_layout()
```

```python
a = lambda x : np.ones(len(x))
plot_spectrum(a,m=32)
```

```python
a = lambda x : 2 + np.sin(x)
plot_spectrum(a)
```

We see that the eigenvalues of $M$ are purely imaginary, regardless of the choice of $a(x)$.  In fact we could see this *a priori*, since $M$ is the product of a real diagonal matrix $A$ and a matrix that is similar to $D$.

It is also straightforward to see that the largest eigenvalues of $M$ have magnitude equal to $\max_i |a(x_i)| m/2$.

Thus we should choose a time integrator whose absolute stability region includes part of the imaginary axis -- ideally, a large part of it.  A simple integrator of this type is the explicit midpoint method (also referred to as the *leapfrog* method.  For our problem it amounts to
$$
U^{n+1} = U^{n-1} + 2\Delta t M U^n.
$$
Since this is a 2-step method, we need some other way to take the first step.  For that, we use the explicit Euler method:
$$
U^{n+1} = U^n + \Delta t M U^n.
$$

```python
#a = lambda x : np.ones(len(x))
a = lambda x : 2 + np.sin(x)

m = 64
F = F_matrix(m)
Finv = np.linalg.inv(F)
L = 2 * np.pi
xi = np.fft.fftfreq(m)*m*2*np.pi/L
D = np.diag(1.j*xi)
x = np.arange(-m/2,m/2)*(L/m)
A = np.diag(a(x))
M = -np.dot(A,np.dot(Finv,np.dot(D,F)))

# Initial data
u = np.sin(2*x)**2 * (x<-L/4)
dx = x[1]-x[0]
dt = 2.0/m/np.max(np.abs(a(x)))/2.
T = 10.
N = int(np.round(T/dt))

frames = [u.copy()]
skip = N//100

# Start with an explicit Euler step
u_new = u + dt*np.dot(M,u)

# Now we solve the problem
for n in range(1,N+1):
    t = n*dt
    u_old = u.copy()
    u = u_new.copy()
    u_new = u_old + 2*dt*np.dot(M,u)
    if ((n % skip) == 0):
        frames.append(u_new.copy())

# Set up plotting
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8,8), sharex=True)

line1, = ax1.plot([],[],lw=3)
ax1.set_xlim((x[0],x[-1])); ax1.set_ylim((-0.1,1.1))
ax2.plot(x,a(x),lw=3); ax2.set_ylim(0,3.1)

def plot_frame(i):
    line1.set_data(x,frames[i])
    ax1.set_title('t='+str(i*skip*dt))

# Animate the solution
anim = matplotlib.animation.FuncAnimation(fig, plot_frame,
                                   frames=len(frames),
                                   interval=200)

HTML(anim.to_jshtml())
```

### Exercise: absolute stability
The region of absolute stability for the midpoint method is the interval $S=[-i,i]$.  The numerical solution will be stable only if $\lambda \Delta t \in S$ for all eigenvalues $\lambda$ of $M$.

Determine the maximum stable step size.

What happens if you take a step size larger than this value?

Now, in addition to truncating the Fourier series of the initial condition, we are truncating the Taylor series of the evolution in time.  This leads to truncation error that accumulates, leading to a solution that is less accurate as time progresses.  Set the $\Delta t$ equal to the largest stable step size and notice the oscillations that appear in front of the pulse as it propagates.

Try reducing the size of $\Delta t$ to decrease the amount of truncation error.

Try some different velocity functions $a(x)$.

## A nonlinear wave equation

Finally, let us consider the case in which the speed of propagation is given by the solution value $u$ itself:

$$
    u_t + u u_x = 0.
$$

We will again use the pseudospectral approach to approximate the term $uu_x$, by differentiating in frequency space and then multiplying in physical space.

We can implement this easily, using the code above but replacing the entries in the matrix $A$ by the values of $u$ on the grid.  We need to construct this matrix at each time step now, since $u$ is changing in time.

This implementation is very inefficient but since we are working on a small grid in one dimension we can afford to ignore efficiency for now.  Run the code below.  Can you explain what happens?  Can you "fix" the problem that occurs by adjusting the parameters in the code?

```python
m = 64
F = F_matrix(m)
Finv = np.linalg.inv(F)
L = 2 * np.pi
xi = np.fft.fftfreq(m)*m*2*np.pi/L
D = np.diag(1.j*xi)
x = np.arange(-m/2,m/2)*(L/m)

# Initial data
u = np.sin(2*x)**2 * (x<-L/4) + 1.
dx = x[1]-x[0]
dt = 2.0/m/np.max(np.abs(a(x)))/2.
T = 10.
N = int(np.round(T/dt))

frames = [u.copy()]
skip = N//100

# Start with an explicit Euler step
A = np.diag(u)
M = -np.dot(A,np.dot(Finv,np.dot(D,F)))
u_new = u + dt*np.dot(M,u)

# Now we solve the problem
for n in range(1,N+1):
    t = n*dt
    u_old = u.copy()
    u = u_new.copy()

    A = np.diag(u)
    M = -np.dot(A,np.dot(Finv,np.dot(D,F)))
    u_new = u_old + 2*dt*np.dot(M,u)
    if ((n % skip) == 0):
        frames.append(u_new.copy())

# Set up plotting
fig, ax1 = plt.subplots(1, 1, figsize=(8,4))

line1, = ax1.plot([],[],lw=3)
ax1.set_xlim((x[0],x[-1])); ax1.set_ylim((0.,2.1))

def plot_frame(i):
    line1.set_data(x,frames[i])
    ax1.set_title('t='+str(i*skip*dt))

# Animate the solution
anim = matplotlib.animation.FuncAnimation(fig, plot_frame,
                                   frames=len(frames),
                                   interval=200)

HTML(anim.to_jshtml())
```

```python

```


# Aplicaciones de Redes Bayesianas

> Con el marco teórico que hemos desarrollado hasta el día de hoy, podemos revisar algunas aplicaciones interesantes de lo que hemos venido estudiando.

## 1. Naive Bayes

En la tarea de redes Bayesianas vimos el modelo de Naive Bayes:

Vimos que, para todo par de nodos de características $X_i$ y $X_j$, este modelo induce que:

$$
(X_i \perp X_j | C),
$$

lo cual no siempre es válido. Aún así es un modelo bastante utilizado por su simplicidad, y poque aunque la suposición de independencia no siempre se satisface, da resultados razonablemente buenos en ciertos tipos de problemas.

```python
# Importar pandas y numpy
import pandas as pd
import numpy as np
```

```python
# Lectura de datos del Titanic
data = pd.read_csv("titanic/train.csv", index_col=0)
```

```python
# Datos
data.head()
```

      Survived
      Pclass
      Name
      Sex
      Age
      SibSp
      Parch
      Ticket
      Fare
      Cabin
      Embarked

      PassengerId

      1
      0
      3
      Braund, Mr. Owen Harris
      male
      22.0
      1
      0
      A/5 21171
      7.2500
      NaN
      S

      2
      1
      1
      Cumings, Mrs. John Bradley (Florence Briggs Th...
      female
      38.0
      1
      0
      PC 17599
      71.2833
      C85
      C

      3
      1
      3
      Heikkinen, Miss. Laina
      female
      26.0
      0
      0
      STON/O2. 3101282
      7.9250
      NaN
      S

      4
      1
      1
      Futrelle, Mrs. Jacques Heath (Lily May Peel)
      female
      35.0
      1
      0
      113803
      53.1000
      C123
      S

      5
      0
      3
      Allen, Mr. William Henry
      male
      35.0
      0
      0
      373450
      8.0500
      NaN
      S

```python
# Trabajar solo con categóricas
data = data[["Pclass", "Sex", "Age", "SibSp",
             "Parch", "Embarked", "Survived"]]
```

```python
# Exlainers y target
X = data.drop(columns=["Survived"])
y = data["Survived"]
```

```python
X
```

      Pclass
      Sex
      Age
      SibSp
      Parch
      Embarked

      PassengerId

      1
      3
      male
      22.0
      1
      0
      S

      2
      1
      female
      38.0
      1
      0
      C

      3
      3
      female
      26.0
      0
      0
      S

      4
      1
      female
      35.0
      1
      0
      S

      5
      3
      male
      35.0
      0
      0
      S

      ...
      ...
      ...
      ...
      ...
      ...
      ...

      887
      2
      male
      27.0
      0
      0
      S

      888
      1
      female
      19.0
      0
      0
      S

      889
      3
      female
      NaN
      1
      2
      S

      890
      1
      male
      26.0
      0
      0
      C

      891
      3
      male
      32.0
      0
      0
      Q

891 rows × 6 columns

```python
# Importar librerías necesarias
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.naive_bayes import CategoricalNB
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
```

```python
# Definición de clase para encoding de varias columnas
class LabelEncoderMulti(BaseEstimator, TransformerMixin):
    def __init__(self, features):
        self.features = features
        self.encoders = None

    def fit(self, X, y=None):
        self.encoders = dict()
        for feature in self.features:
            self.encoders[feature] = LabelEncoder().fit(X[:, feature])

        return self

    def transform(self, X, y=None):
        Xt = X.copy()
        for feature in self.features:
            Xt[:, feature] = self.encoders[feature].transform(X[:, feature])

        return Xt

    def fit_transform(self, X, y=None):
        return self.fit(X).transform(X)
```

```python
# Dividir datos en entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1)
```

```python
# Pipeline de modelo
NB_model = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", LabelEncoderMulti(features=[1, -1])),
    ("classifier", CategoricalNB(alpha=0.1))
])
```

```python
# Entrenamiento
NB_model.fit(X_train, y_train)
```

    Pipeline(memory=None,
             steps=[('imputer',
                     SimpleImputer(add_indicator=False, copy=True, fill_value=None,
                                   missing_values=nan, strategy='most_frequent',
                                   verbose=0)),
                    ('encoder', LabelEncoderMulti(features=[1, -1])),
                    ('classifier',
                     CategoricalNB(alpha=0.1, class_prior=None, fit_prior=True))],
             verbose=False)

```python
# Score de entrenamiento
NB_model.score(X_train, y_train)
```

    0.818976279650437

```python
# Score de los datos de prueba
NB_model.score(X_test, y_test)
```

    0.7666666666666667

```python
# CPDs
classifier = NB_model.named_steps["classifier"]
```

```python
classifier.feature_log_prob_
```

    [array([[-8.51398806, -1.9195746 , -1.72164363, -0.39399423],
            [-8.01763716, -0.93508859, -1.35706201, -1.05067002]]),
     array([[-1.89218104, -0.16339305],
            [-0.37637392, -1.15946368]]),
     array([[-8.52931937, -6.1314241 , -4.41844551, -6.1314241 , -5.48479693,
             -8.52931937, -6.1314241 , -6.1314241 , -5.48479693, -4.59749374,
             -5.48479693, -5.09533217, -8.52931937, -8.52931937, -4.8157473 ,
             -8.52931937, -3.91419885, -4.26663949, -3.38765581, -3.51203953,
             -3.81978917, -3.38765581, -3.51203953, -4.01845986, -1.36614698,
             -3.51203953, -4.01845986, -4.26663949, -3.27704594, -3.81978917,
             -3.51203953, -4.26663949, -4.01845986, -4.13487022, -4.01845986,
             -4.59749374, -3.81978917, -4.59749374, -4.41844551, -4.13487022,
             -4.01845986, -4.8157473 , -4.26663949, -4.8157473 , -4.41844551,
             -4.13487022, -5.09533217, -4.13487022, -5.09533217, -5.48479693,
             -4.8157473 , -5.09533217, -5.09533217, -8.52931937, -4.8157473 ,
             -6.1314241 , -5.48479693, -5.48479693, -5.48479693, -5.48479693,
             -5.48479693, -5.09533217, -5.48479693, -8.52931937, -5.48479693,
             -5.09533217, -6.1314241 , -8.52931937, -8.52931937, -8.52931937,
             -5.09533217, -5.48479693, -8.52931937, -8.52931937, -6.1314241 ,
             -8.52931937, -8.52931937, -8.52931937, -8.52931937, -8.52931937,
             -8.52931937],
            [-3.78001962, -4.32912743, -4.60871229, -4.11087386, -3.78001962,
             -4.32912743, -4.99817706, -5.64480422, -4.99817706, -4.99817706,
             -8.0426995 , -5.64480422, -5.64480422, -4.99817706, -4.60871229,
             -4.32912743, -4.11087386, -4.11087386, -3.53183999, -3.93182563,
             -4.99817706, -4.11087386, -3.42757898, -4.11087386, -1.62760254,
             -3.93182563, -4.32912743, -3.78001962, -3.78001962, -3.78001962,
             -3.53183999, -3.93182563, -3.53183999, -4.99817706, -3.93182563,
             -3.53183999, -3.53183999, -5.64480422, -4.32912743, -4.32912743,
             -3.93182563, -5.64480422, -3.93182563, -5.64480422, -4.99817706,
             -4.11087386, -8.0426995 , -5.64480422, -4.11087386, -4.32912743,
             -4.11087386, -5.64480422, -4.60871229, -5.64480422, -4.60871229,
             -5.64480422, -4.99817706, -8.0426995 , -4.60871229, -8.0426995 ,
             -4.99817706, -8.0426995 , -4.99817706, -4.99817706, -8.0426995 ,
             -8.0426995 , -8.0426995 , -8.0426995 , -8.0426995 , -8.0426995 ,
             -8.0426995 , -8.0426995 , -8.0426995 , -8.0426995 , -8.0426995 ,
             -8.0426995 , -8.0426995 , -8.0426995 , -8.0426995 , -8.0426995 ,
             -5.64480422]]),
     array([[-0.32325075, -1.73393314, -3.56623088, -3.80546057, -3.63979344,
             -4.8014187 , -8.51499077, -8.51499077, -4.25231089],
            [-0.49580248, -1.10058857, -3.30975359, -4.58529659, -4.58529659,
             -8.01928379, -8.01928379, -8.01928379, -8.01928379]]),
     array([[-0.20787076, -2.40312247, -2.59838774, -5.47006737, -4.80101774,
             -4.80101774, -6.11669453],
            [-0.38767888, -1.63680945, -2.15783924, -4.58463826, -8.01862547,
             -8.01862547, -8.01862547]]),
     array([[-2.03527776, -2.47115456, -0.24223902],
            [-1.24766553, -2.37895284, -0.47774868]])]

```python
X.describe()
```

      Pclass
      Age
      SibSp
      Parch

      count
      891.000000
      714.000000
      891.000000
      891.000000

      mean
      2.308642
      29.699118
      0.523008
      0.381594

      std
      0.836071
      14.526497
      1.102743
      0.806057

      min
      1.000000
      0.420000
      0.000000
      0.000000

      25%
      2.000000
      20.125000
      0.000000
      0.000000

      50%
      3.000000
      28.000000
      0.000000
      0.000000

      75%
      3.000000
      38.000000
      1.000000
      0.000000

      max
      3.000000
      80.000000
      8.000000
      6.000000

## Regresión lineal

Desde una perspectiva Bayesiana, podemos representar las VA objetivo $y$ y parámetros $w$, datos $X$ de un modelo lineal $y=Xw + \epsilon$ como:

Como no nos interesa modelar los datos, sino los parámetros, estudiamos la distribución posterior $P(w | X, y)$, la cual se puede reescribir como:

$$
P(w | X, y) = \frac{P(y, w | X)}{P(y | X)}.
$$

Por otra parte, usando la definición de probabilidad condicional y la factorización inducida, tenemos que:

$$
P(y, w | X) = P(y | X, w) P(w),
$$

de donde

$$
P(w | X, y) = \frac{P(y, w | X)}{P(y | X)} = \frac{P(y | X, w) P(w)}{P(y | X)}.
$$

Estas distribuciones tienen nombres muy específicos:

- $P(w | X, y)$ se conoce como la **distribución posterior**.
- $P(y | X, w)$ se conoce como la **función de verosimilitud**.
- $P(w)$ se conoce como la **distribución previa**.
- $P(y | X)$ se conoce como la **distribución de evidencia**.

1. La distribución posterior la queremos calcular a través de las demás.

2. La función de verosimilitud la podemos suponer razonablemente como:

$$
P(y | X, w) = \mathcal{N}(y | X w, \sigma^2 I) \propto \exp\left\{-\frac{1}{2 \sigma^2} ||y - X w||^2\right\},
$$

3. De la misma forma, para codificar el conocimiento previo sobre los parámetros podemos suponer:

$$
P(w) = \mathcal{N}(w | 0, \gamma^2 I),
$$

La distribución de evidencia es especialmente compleja. Para ver porqué, respondamos las preguntas:

- ¿Qué significa $P(y | X)$?
- ¿Qué implicaría conocer $P(y | X)$?

Por esto, los métodos Bayesianos rondan a través de evitar el cálculo de esta distribución a través de distintas maneras.

### 1. Estimador MAP

En general nos interesa encontrar la **distribución posterior**, sin embargo, para este ejemplo, encontraremos sólo **la moda** de la misma.

De modo que para encontrar el valor más probable de los parámetros según la evidencia de los datos y el objetivo, hacemos:

$$
\arg \max_{w} P(w | X, y) = \arg \max_{w} \frac{P(y, w | X)}{P(y | X)} = \arg \max_{w} P(y, w | X) = \arg \max_{w} P(y | X, w) P(w)
$$

___
**Propiedad de los logaritmos:** dado que el logaritmo es una función creciente,

$$
\arg \max_x f(x) = \arg \max_x \log f(x) = \arg \min_x - \log f(x)
$$
___

Así que el anterior problema lo podemos reescribir como:

$$
\arg \min_w -\log P(y | X, w) - \log P(w)
$$

Desarrollando los logaritmos en el pizarrón:

\begin{align}
-\log P(y | X, w) - \log P(w) & = \dots \\
                              & = \dots \\
                              & = \dots \\
                              & = \log C + \frac{1}{2\sigma^2} (y - X w)^T (y - X w) + \frac{1}{2\gamma^2} w^T w \\
                              & = \log C + \frac{1}{2\sigma^2} ||y - X w||^2 + \frac{1}{2\gamma^2} ||w||^2
\end{align}

Finalmente, podemos resolver el problema

$$
\arg \min_w ||y - X w||^2 + \lambda ||w||^2,
$$

con $\lambda = \frac{\sigma^2}{\gamma^2}$.  Interpretación de $\lambda$.

Se puede demostrar que la solución es:

$$
w^{*}_{reg} = (X^T X + \lambda I)^{-1} X^T y
$$

```python
# Importar numpy
import numpy as np
from matplotlib import pyplot as plt
```

```python
# Fijar semilla para reproducibilidad
np.random.seed(1001)
# Dispersión de los datos
sigma = 2
# Generar datos (recta + ruido)
N = 101
x = np.linspace(0, 10, N)
y = 5 + 2 * x + sigma * np.random.normal(size=(N,))
```

```python
# Graficar
plt.figure(figsize=(6, 4))
plt.plot(x, y, 'or', ms=5, label="Data")
plt.xlabel("x")
plt.ylabel("y")
```

```python
# Importar sklearn.linear_model.Ridge
from sklearn.linear_model import Ridge
# Importar sklearn.preprocessing.StandardScaler
from sklearn.preprocessing import StandardScaler
```

```python
# Scaler-regressor pipeline
regressor_model = Pipeline([
    # ("scaler", StandardScaler()),
    ("reg", Ridge(alpha=0.01))
])
```

```python
# Ajuste del modelo
regressor_model.fit(x.reshape((-1, 1)), y)
```

    Pipeline(memory=None,
             steps=[('reg',
                     Ridge(alpha=0.01, copy_X=True, fit_intercept=True,
                           max_iter=None, normalize=False, random_state=None,
                           solver='auto', tol=0.001))],
             verbose=False)

```python
# Parámetros más probables
regressor_model.named_steps["reg"].coef_, regressor_model.named_steps["reg"].intercept_
```

    (array([2.06247012]), 4.6752114411804655)

```python
# Gráfico
plt.figure(figsize=(6, 4))
plt.plot(x, y, 'or', ms=5, label="Data")
plt.plot(x, regressor_model.predict(x.reshape((-1, 1))))
plt.xlabel("x")
plt.ylabel("y")
```

### 2. Distribución previa conjugada

Bajo el framework de estimación MAP, quitamos el denominador puesto que este no dependía de $w$, que era sobre lo que estábamos maximizando.

Esto lo podemos generalizar, dándonos cuenta que la distribución posterior $P(w | X, y)$ es distribución sobre $w$, y que la distribución de evidencia $P(y | X)$ es tan solo un factor **normalizante** sobre el producto $P(y | X, w) P(w)$.

¿De qué nos sirve esto? Veamos:

La función de verosimilitud $P(y | X, w)$ la suponemos una distribución normal de la forma:

$$
P(y | X, w) = \mathcal{N}(y | X w, \sigma^2 I) \propto \exp\left\{-\frac{1}{2 \sigma^2} ||y - X w||^2\right\}.
$$

Notemos que, para cualquier $w_0 \in \mathbb{R}^d$:

\begin{align}
||y - X w||^2 & = ||X w - y||^2 \\
              & = ||X (w - w_0) + X w_0 - y||^2 \\
              & = \left(X (w - w_0) + X w_0 - y\right)^T \left(X (w - w_0) + X w_0 - y\right) \\
              & = (w - w_0)^T X^T X (w - w_0) + 2 (w - w_0)^T X^T (X w_0 - y) + ||X w_0- y||^2
\end{align}

En particular, haciendo $w_0 = w^* = \left(X^T X\right)^{-1} X^T y$ (solución de mínimos cuadrados), se observa que:

\begin{array}
2 (w - w^*)^T X^T (X w^* - y) & = 2 (Xw - Xw^*)^T  (X w^* - y) \\
                              & = 2 \left[w^T X^T X w^* - w^T X^Ty - (w^*)^T X^T X w^* + (w^*)^T X^T y \right]\\
                              & = 2 \left[w^T \underbrace{X^T X \left(X^T X\right)^{-1}}_{I} X^T y - w^T X^Ty - y^T X \underbrace{\left(X^T X\right)^{-1} X^T X}_{I} \left(X^T X\right)^{-1} X^T y + y^T X \left(X^T X\right)^{-1} X^T y\right] \\
                              & = 2 \left[w^T X^T y - w^T X^Ty - y^T X  \left(X^T X\right)^{-1} X^T y + y^T X \left(X^T X\right)^{-1} X^T y\right] \\
                              & = 0.
\end{array}

De esta manera:

$$
||y - X w||^2 = (w - w^*)^T X^T X (w - w^*) + \underbrace{||X w^*- y||^2 }_{f(X, y)}
$$

Así, concluimos que

\begin{align}
P(y | X, w) & = \mathcal{N}(y | X w, \sigma^2 I) \\
            & \propto \exp\left\{-\frac{1}{2 \sigma^2} ||y - X w||^2\right\} \\
            & \propto \exp\left\{-\frac{1}{2 \sigma^2} (w - w^*)^T X^T X (w - w^*)\right\} \\
            & \propto \mathcal{N}\left(w | w^*, \sigma^2 \left(X^T X\right)^{-1}\right)
\end{align}

#### ¡La función de verosimilitud es proporcional a una distribución Normal sobre los parámetros!

Retomando la distribución posterior, sabemos que

$$
P(w | X, y) \propto P(y | X, w) P(w) \propto \mathcal{N}\left(w | w^*, \sigma^2 \left(X^T X\right)^{-1}\right) P(w).
$$

___
El producto de dos densidades normales univariadas es otra densidad normal univariada. Esto se extiende al caso multivariable con:

$$
\mu_3 = \Sigma_2 \left(\Sigma_1 + \Sigma_2\right)^{-1} \mu_1 + \Sigma_1 \left(\Sigma_1 + \Sigma_2\right)^{-1} \mu_2, \qquad \Sigma_3 = \Sigma_1 \left(\Sigma_1 + \Sigma_2\right)^{-1} \Sigma_2,
$$

o equivalentemente

$$
\Sigma_3^{-1} = \Sigma_1^{-1} + \Sigma_2^{-1} , \qquad
\mu_3 = \Sigma_2 \Sigma_3^{-1} \mu_1 + \Sigma_1 \Sigma_3^{-1}\mu_2
$$

De manera que una previa conjugada para el caso anterior es:

$$
P(w) = \mathcal{N}(w | 0, \gamma^2 I),
$$
___

con lo cual:

$$
P(w | X, y) = \mathcal{N}\left(w | w^{**}, \Sigma^{*}\right).
$$

con

$$
\left(\Sigma^{*}\right)^{-1} = \frac{1}{\sigma^2} X^T X + \frac{1}{\gamma^2}I, \qquad w^{**} = \left(\frac{1}{\sigma^2} X^T X + \frac{1}{\gamma^2}I \right)^{-1}\frac{1}{\sigma^2} X^T X w^{*} = \left(X^T X + \frac{\sigma^2}{\gamma^2}I \right)^{-1} X^T y = w^{*}_{reg},
$$

```python
# Ya teníamos los datos. Graficamos
plt.figure(figsize=(6, 4))
plt.plot(x, y, 'or', ms=5, label="Data")
plt.xlabel("x")
plt.ylabel("y")
```

```python
# Construimos X
X = np.concatenate((np.ones((len(x), 1)), x.reshape(-1, 1)), axis=1)
X
```

    array([[ 1. ,  0. ],
           [ 1. ,  0.1],
           [ 1. ,  0.2],
           [ 1. ,  0.3],
           [ 1. ,  0.4],
           [ 1. ,  0.5],
           [ 1. ,  0.6],
           [ 1. ,  0.7],
           [ 1. ,  0.8],
           [ 1. ,  0.9],
           [ 1. ,  1. ],
           [ 1. ,  1.1],
           [ 1. ,  1.2],
           [ 1. ,  1.3],
           [ 1. ,  1.4],
           [ 1. ,  1.5],
           [ 1. ,  1.6],
           [ 1. ,  1.7],
           [ 1. ,  1.8],
           [ 1. ,  1.9],
           [ 1. ,  2. ],
           [ 1. ,  2.1],
           [ 1. ,  2.2],
           [ 1. ,  2.3],
           [ 1. ,  2.4],
           [ 1. ,  2.5],
           [ 1. ,  2.6],
           [ 1. ,  2.7],
           [ 1. ,  2.8],
           [ 1. ,  2.9],
           [ 1. ,  3. ],
           [ 1. ,  3.1],
           [ 1. ,  3.2],
           [ 1. ,  3.3],
           [ 1. ,  3.4],
           [ 1. ,  3.5],
           [ 1. ,  3.6],
           [ 1. ,  3.7],
           [ 1. ,  3.8],
           [ 1. ,  3.9],
           [ 1. ,  4. ],
           [ 1. ,  4.1],
           [ 1. ,  4.2],
           [ 1. ,  4.3],
           [ 1. ,  4.4],
           [ 1. ,  4.5],
           [ 1. ,  4.6],
           [ 1. ,  4.7],
           [ 1. ,  4.8],
           [ 1. ,  4.9],
           [ 1. ,  5. ],
           [ 1. ,  5.1],
           [ 1. ,  5.2],
           [ 1. ,  5.3],
           [ 1. ,  5.4],
           [ 1. ,  5.5],
           [ 1. ,  5.6],
           [ 1. ,  5.7],
           [ 1. ,  5.8],
           [ 1. ,  5.9],
           [ 1. ,  6. ],
           [ 1. ,  6.1],
           [ 1. ,  6.2],
           [ 1. ,  6.3],
           [ 1. ,  6.4],
           [ 1. ,  6.5],
           [ 1. ,  6.6],
           [ 1. ,  6.7],
           [ 1. ,  6.8],
           [ 1. ,  6.9],
           [ 1. ,  7. ],
           [ 1. ,  7.1],
           [ 1. ,  7.2],
           [ 1. ,  7.3],
           [ 1. ,  7.4],
           [ 1. ,  7.5],
           [ 1. ,  7.6],
           [ 1. ,  7.7],
           [ 1. ,  7.8],
           [ 1. ,  7.9],
           [ 1. ,  8. ],
           [ 1. ,  8.1],
           [ 1. ,  8.2],
           [ 1. ,  8.3],
           [ 1. ,  8.4],
           [ 1. ,  8.5],
           [ 1. ,  8.6],
           [ 1. ,  8.7],
           [ 1. ,  8.8],
           [ 1. ,  8.9],
           [ 1. ,  9. ],
           [ 1. ,  9.1],
           [ 1. ,  9.2],
           [ 1. ,  9.3],
           [ 1. ,  9.4],
           [ 1. ,  9.5],
           [ 1. ,  9.6],
           [ 1. ,  9.7],
           [ 1. ,  9.8],
           [ 1. ,  9.9],
           [ 1. , 10. ]])

```python
from scipy.stats import multivariate_normal
```

```python
# Gamma
gamma = 5
# Distribución posterior
l = sigma**2 / gamma**2
w_reg = np.linalg.inv(X.T.dot(X) + l * np.eye(2)).dot(X.T).dot(y)
cov = np.linalg.inv(X.T.dot(X) / sigma**2 + np.eye(2) / gamma**2)
posterior = multivariate_normal(mean=w_reg, cov=cov)
```

```python
# Parámetros distribución posterior
posterior.mean, posterior.cov
```

    (array([4.64799742, 2.06644029]),
     array([[ 0.15509629, -0.02314761],
            [-0.02314761,  0.00463686]]))

```python
from mpl_toolkits.mplot3d import Axes3D
```

```python
# Gráfico
w0 = np.linspace(3.8, 5.5, 100)
w1 = np.linspace(1.9, 2.2, 100)
w0, w1 = np.meshgrid(w0, w1)
z = posterior.pdf(np.dstack([w0, w1]))
fig = plt.figure(figsize=(8, 4))
ax1 = fig.add_subplot(121, projection='3d')
ax1.plot_surface(w0, w1, z)
ax2 = fig.add_subplot(122)
ax2.contour(w0, w1, z)
plt.show()
```

```python
# Parámetros más probables y diferentes posibilidades
for i in range(100):
    wp = posterior.rvs()
    plt.plot(x, X.dot(wp), 'k', alpha=0.3)
plt.plot(x, y, 'o')
plt.plot(x, X.dot(posterior.mean), 'r', lw=3)
```

#### ¿Qué pasa si el valor que usamos para $\sigma$ se desvía del valor real?

```python
# Gamma y sigma
gamma = 10
sigma = 10
# Distribución posterior
l = sigma**2 / gamma**2
w_reg = np.linalg.inv(X.T.dot(X) + l * np.eye(2)).dot(X.T.dot(y))
Sigma = np.linalg.inv(X.T.dot(X) / sigma**2 + np.eye(2) / gamma**2)
posterior = multivariate_normal(mean=w_reg, cov=Sigma)
```

```python
# Gráfico
w0 = np.linspace(0, 9, 100)
w1 = np.linspace(1, 3, 100)
w0, w1 = np.meshgrid(w0, w1)
z = posterior.pdf(np.dstack([w0, w1]))
fig = plt.figure(figsize=(8, 4))
ax1 = fig.add_subplot(121, projection='3d')
ax1.plot_surface(w0, w1, z)
ax2 = fig.add_subplot(122)
ax2.contour(w0, w1, z)
plt.show()
```

```python
# Parámetros más probables y diferentes posibilidades
for i in range(100):
    wp = posterior.rvs()
    plt.plot(x, X.dot(wp), 'k', alpha=0.3)
plt.plot(x, X.dot(wp), 'k', alpha=0.3, label="Parámetros probables")
plt.plot(x, y, 'o', label="Datos")
plt.plot(x, X.dot(posterior.mean), 'r', lw=3, label="Parámetros más probables")
plt.legend()
```

### Aprendizaje en línea

Este esquema que acabamos de ver es susceptible para aprendizaje en línea. Dado que la distribución posterior es normal cuando la previa es normal (previa conjugada), podemos usar esta distribución posterior para el siguiente paso. La nueva previa para el siguiente paso sería:

$$
P_{k+1}(w) = P_{k}(w | X, y) = \mathcal{N}\left(w | \mu_k, \Sigma_k\right).
$$

**Tarea.** Dado que la función de verosimilitud satisface:

$$
P(y | X, w) \propto \mathcal{N}\left(w | w^*, \sigma^2 \left(X^T X\right)^{-1}\right),
$$

¿Cuál sería la distribución posterior con una previa normal $P(w)  = \mathcal{N}\left(w | \mu, \Sigma\right)$?

Created with Jupyter by Esteban Jiménez Rodríguez.


# Worked through example of the annealing plot

We will nox exemplify how we produced Figure 2 in [Limitations of optimization algorithms on noisy quantum devices](https://arxiv.org/abs/2009.05532). In this figure we plotted what our methods say about the maximal annealing time of noisy annealing devices under amplitude damping and control errors.
We refer to the paper for a detailed explanation of the notation and concepts, but at the end of this notebook we will produce the plot shown in that figure.

In Eq. (10) we show the inequality:
\begin{align}
&D(\mathcal{T}_T(|+\rangle\langle +|^{\otimes n})||\sigma_\gamma)\leq n f(\gamma,r,T,\bar{\Gamma})
\end{align}
where
\begin{align}\label{equ:locadep}
f(\gamma,r,T,\bar{\Gamma})= e^{-r T}\log(2\cosh(\gamma))\nonumber +\frac{2\sinh(\gamma)( 1-e^{-rT}r T-e^{-rT})}{r^2T} \bar{\Gamma}
\end{align}
and $\bar{\Gamma}=n^{-1}\sum_i\Gamma_i$. Also recall that $r=r_1+r_3$, where $r_1$ is the amplitude damping rate and $r_3$ the standard deviation of the control errors. The values of $r_1$ and $r_2$ also pin down the value of $\gamma$,
\begin{align*}
\gamma=\frac{1}{2}\log\left(1+\frac{r_1}{r_3}\right).
\end{align*}

Our bounds assert that the annealing device performs as well as polynomial-time Monte Carlo methods whenever:
\begin{align}
f(\gamma,r,T,\bar{\Gamma})\leq \frac{\|H_I\|}{4\log(2)\|A\|n}\epsilon.
\end{align}
As explained in the text, the term $\frac{\|H_I\|}{4\|A\|n}$ is of constant order. Moreover, numerical experiments with the variational bound show that Monte Carlo methods in the polynomial-time range consistently outperform noisy quantum computers at relative entropy densities in the range $10^{-1}-2\times 10^{-1}$. Thus, we will conservatively pick the threshold $\frac{\|H_I\|}{4\|A\|n}\epsilon=0.025$, which corresponds to $\epsilon=10^{-1}$ for the rigorous guarantee in Eq. (4). Let $\frac{\|H_I\|}{4\|A\|n}\epsilon$ be the threshold value defined below. We will also define the $f$ function.

```python
threshold=0.025

import numpy as np
def f(gamma,r,T,Gamma):
    expo_part=np.exp(-r*T)*np.log(2*np.cosh(gamma))
    poly_part=Gamma*2*np.sinh(gamma)*(1-np.exp(-r*T)*r*T-np.exp(-r*T))/(r*r*T)
    return expo_part+poly_part

def gamma(r1,r3):
    return 0.5*np.log(1+r1/r3)
```

We will now estimate the annealing time for which we lose advantage for realistic noise parameters. As explained in the text, we pick $\Gamma=1$. Letting $J$ the maximal interaction strength of the device, we set $r_3\times J=2\times 10^{-2}$, the value reported in a commercially available device's manual. The value of $r_1$ is not reported, so we will consider various choices. We now have all the tools at hand to estimate the maximal annealing time before we lose advantage. We will do log plots of the relative entropy density, as they make it easier to visualize the bound.

```python
r3=0.02
Gamma=1

import matplotlib as mpl
import matplotlib.pyplot as plt
#setting maximal annealing time we want to plot
Tmax=10**3
annealing_times=xs=np.linspace(0.1,Tmax,300)

#first line with r_1=10^(-2)xr_3

r1=0.01*r3

rel_entropy_dens=[]
for t in annealing_times:
    rel_entropy_dens.append(np.log(f(gamma(r1,r3),r1+2*r3,t,Gamma))/np.log(10))

plt.plot(annealing_times,rel_entropy_dens,'r',label="$r_1=10^{-2} r_3$")
plt.title("Relative entropy density for noisy annealers, $r_3=10^{-2}J$")
plt.xlabel('Annealing time ($x J$)')
plt.ylabel('log-Relative entropy density')

#second plot with r_1=10^(-1)xr_3

r1=0.1*r3

rel_entropy_dens=[]
for t in annealing_times:
    rel_entropy_dens.append(np.log(f(gamma(r1,r3),r1+2*r3,t,Gamma))/np.log(10))

plt.plot(annealing_times,rel_entropy_dens,'orange',label="$r_1=10^{-1} r_3$")

plt.ylim([-4,0])
plt.legend()

```

    <matplotlib.legend.Legend at 0x7ff9e163e290>

Let us now find the points the polynomial-time crossover happens an plot them in the curve:

```python
r3=0.02
Gamma=1
#function that finds when the curve passes a given point
def find_crossover(results,value):
    k=0
    while (results[k]>value):
        k+=1
    return k

#setting maximal annealing time we want to plot
Tmax=4000
annealing_times=xs=np.linspace(0.1,Tmax,Tmax*10)

#font sizes for better visualization
SMALL_SIZE = 12
MEDIUM_SIZE = 14
BIGGER_SIZE = 12

plt.rc('font', size=MEDIUM_SIZE)          # controls default text sizes
plt.rc('axes', titlesize=MEDIUM_SIZE)     # fontsize of the axes title
plt.rc('axes', labelsize=SMALL_SIZE)    # fontsize of the x and y labels
plt.rc('xtick', labelsize=MEDIUM_SIZE)    # fontsize of the tick labels
plt.rc('ytick', labelsize=MEDIUM_SIZE)    # fontsize of the tick labels
plt.rc('legend', fontsize=MEDIUM_SIZE)    # legend fontsize
plt.rc('figure', titlesize=BIGGER_SIZE)  # fontsize of the figure title

#second plot with r_1=10^(-1)xr_3

r1=0.1*r3

rel_entropy_dens=[]
for t in annealing_times:
    rel_entropy_dens.append(np.log(f(gamma(r1,r3),r1+2*r3,t,Gamma))/np.log(10))

#let us now find the polynomial time transition point
t_trans=find_crossover(rel_entropy_dens,np.log(threshold)/np.log(10))
plt.scatter(np.log(annealing_times[t_trans])/np.log(10),rel_entropy_dens[t_trans],color='y')

plt.plot(np.log(annealing_times[0:t_trans])/np.log(10),rel_entropy_dens[0:t_trans],'y',label="$r_1=10^{-1} r_3$")
plt.plot(np.log(annealing_times[t_trans:])/np.log(10),rel_entropy_dens[t_trans:],'y--')

plt.ylim([-4,-0.8])
plt.legend()

#first line with r_1=10^(-2)xr_3

r1=0.01*r3

rel_entropy_dens=[]
for t in annealing_times:

    rel_entropy_dens.append(np.log(f(gamma(r1,r3),r1+2*r3,t,Gamma))/np.log(10))

#let us now find the polynomial time transition point
t_trans=find_crossover(rel_entropy_dens,np.log(threshold)/np.log(10))
plt.scatter(np.log(annealing_times[t_trans])/np.log(10),rel_entropy_dens[t_trans],color='r')

plt.plot(np.log(annealing_times[0:t_trans])/np.log(10),rel_entropy_dens[0:t_trans],'r',label="$r_1=10^{-2} r_3$")
plt.plot(np.log(annealing_times[t_trans:])/np.log(10),rel_entropy_dens[t_trans:],'r--')

plt.title("Relative entropy density for noisy annealers, $r_3=2x 10^{-2}J$")
plt.xlabel('$\log_{10}$-Annealing time (times $J$)')
plt.ylabel('$\log_{10}$-Relative entropy density')

#second plot with r_1=10^(-3)xr_3

r1=0.001*r3

rel_entropy_dens=[]
for t in annealing_times:
    rel_entropy_dens.append(np.log(f(gamma(r1,r3),r1+2*r3,t,Gamma))/np.log(10))

#let us now find the polynomial time transition point
t_trans=find_crossover(rel_entropy_dens,np.log(threshold)/np.log(10))
plt.scatter(np.log(annealing_times[t_trans])/np.log(10),rel_entropy_dens[t_trans],color='m')

plt.plot(np.log(annealing_times[0:t_trans])/np.log(10),rel_entropy_dens[0:t_trans],'m',label="$r_1=10^{-3} r_3$")
plt.plot(np.log(annealing_times[t_trans:])/np.log(10),rel_entropy_dens[t_trans:],'m--')

plt.ylim([-4,1])
plt.xlim([0,4])
plt.legend()

plt.savefig('annealing.pdf')

```

```python

```