```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sympy
sympy.init_printing(use_latex='mathjax')
%matplotlib inline
```

# Sympy를 사용한 함수 미분

### Data 분석에서 미분이 필요한 이유
- 데이터 분석에는 미분(differentiation)이 필수적이다. 데이터 분석의 목표는 가장 최적의(optimal)모형을 구하는 일이다. 예측 모형은 입력 데이터 이외에도 모수(parameter)라고 하는 숫자를 가진다. 선형 예측 모형에서는 입력 데이터에 대한 가중치가 모수가 된다. 즉 모수를 어떤 숫자로 정하느냐에 따라 예측 모형의 성능이 달라진다. 즉, 모수를 함수의 입력이라고 한다면 성능은 함수의 출력이 된다. 이 때의 함수는 모형의 모수를 입력받아 모형의 성능을 출력하는 함수로 x데이터를 받아 y 데이터를 출력하는 함수와는 다른 함수라는 점에 주의한다.

- 우리가 원하는 것은 성능이 가장 높게 만드는 모수의 값이다. 이렇게 함수의 출력을 가장 크게 만드는 입력값을 찾는 것을 최적화(optimization) 작업이라고 하는데, 최적화를 하려면 미분 혹은 편미분이 유용하기 때문에 미분을 잘 알고 있어야 한다.

- 다행스러운 점은 데이터 분석에서 필요한 미분의 수준은 그다지 높지 않다는 점이다. 선형 다항식이나 지수 함수의 편미분 정도만 알고 있으면 되고, 대부분의 경우 최적화 라이브러리를 사용하거나, theano, tensorflow등의 라이브러리에도 도함수와 미분값을 계산해 주기 때문에 실제로 직접 미분을 계산하는 일은 많지 않다.

## 기울기 (Slope)

- 최적화를 하는 가장 이상적인 방법은 가능한 모든 x값을 넣어서 y값을 계산해 보고, 이 중에서 가장 큰 혹은 가장 작은 y값을 출력하게 만드는 x의 값을 골라내는 것이다. 이러한 방식을 그리드서치(grid search)라고 한다. 하지만, 그리드 서치를 하려면 엄청나게 많은 연산을 해야 하므로 현실적으로는 사용할 수 없다. 대신 x의 값을 몇가지만 시도해서 그 중 가장 좋은 값을 구하는 수치적 최적화라는 방법을 사용하게 된다.

- 수치적 최적화를 하려면 현재 사용한 x의 값이 아닌 또 다른 x의 값을 시도할 때 어떤 값을 시도해야 할 지를 결정해야 한다. 그러려면, x를 증가시켰을 때 y 값이 증가하는지 감소하는지, 증가한다면 어느 정도 증가하는 지를 알아야 한다. 즉 함수에 들어가는 입력변수의 값이 달라지면 함수의 출력 값이 어떻게 변하는지를 알아야 한다. 이를 기울기(slope) 혹은 민감도(sensitivity)라고 한다.

- 만약 입력변수의 값이 x에서 x2로 달라졌다고 가정하자. 출력 변수는 f(x)이라는 값에서 f(x2)라는 값으로 달라질 것이다. 이를 비율로 나타내면 다음과 같다. 여기에서 dx= x2 - x이다.
- y의 변화량 / x의 변화량 = f(x2) - f(x) / x2 - x = f(x+dx) - f(x) / dx

- 이러한 변화율은 x2를 x1에서 얼마나 멀리 떨어져 있는가 즉, dx의 값에 따라 달라진다. 이를 해결하기 위해 ***기울기(slope)***라는 개념을 사용한다. 기울기는 dx값이 0으로 근접할 때의 변화율을 말한다. 기호로는 다음처럼 쓴다.
- 기울기 = lim dx -> 0 (f(x+dx) - f(x) / dx)

- 이번에는 기울기를 함수의 그래프에서 살펴보자. 함수의 그래프는 앞에서 그린 것 처럼 부드러운 곡선(curve)의 형태로 나타나는 경우가 많다. 이 곡선에 대해 한 점만 공통으로 가지는 접선(tangent)을 그릴 수 있는데, 이 접선이 수평선과 이루는 기울기는 접선이 x 방향으로 이동한 거리와 y방향으로 이동한 거리의 비율을 말한다.
- 기울기 = 접선이y방향으로 이동한 거리 / 접선이 x방향으로 이동한 거리
- 다음 그림에서 x = 0과 x = 1에서의 기울기는 각각1, -2이다.
- x = 0에서의 기울기 1/1 = 1
- x = 1에서의 기울기 -2/1 = -2

```python
def f(x):
    return x**3 - 3*x**2 + x

x = np.linspace(-1, 3, 400)
y = f(x)

plt.plot(x, y)
plt.plot(0, 0, 'ro') # (0,0) red circle
plt.plot(x, x, 'r:') # 빨간 점선
plt.plot(1, -1, 'go') # (1,-1) green circle
plt.plot(x, (3*1 ** 2-6*1+1) * (x-1)-1, 'g--') #녹색 점선

plt.xlim(-3.5, 5.5)
plt.ylim(-4, 2)
plt.xticks(np.arange(-3, 6))
plt.yticks(np.arange(-4, 2))

plt.annotate('', xy=(1, 0), xytext=(0, 0), arrowprops=dict(facecolor='blue'))
plt.annotate('', xy=(1, 1), xytext=(1, 0), arrowprops=dict(facecolor='blue'))
plt.annotate('', xy=(2, -1), xytext=(1, -1), arrowprops=dict(facecolor='blue'))
plt.annotate('', xy=(2, -3), xytext=(2, -1), arrowprops=dict(facecolor='blue'))

plt.show()
```

### 수치 미분

- scipy.misc.derivative 명령을 사용하면 수치적으로 대략적인 미분값을 계산할 수 있다. 인수로는 기울기를 구하고자 하는 함수 f, 기울기를 구할 위치 x, 기울기를 구하기 위해 이동할 거리 dx를 받는다. 기울기는 다음 수식으로 구한다.
- f(x+0.5dx) - f(x-0.5dx) / dx
- dx 인수의 값이 너무 크거나 너무 작으면 정확한 값을 구하지 못할 수 있다.

```python
from scipy.misc import derivative
derivative(f, -0.5, dx=1e-6)
```

    4.749999999886789

```python
derivative(f, 0, dx=1e-6)
```

    1.000000000001

```python
derivative(f, 0.5, dx=1e-6)
```

    -1.2499999999804334

```python
derivative(f, 1, dx=1e-6)
```

    -2.000000000002

```python
derivative(f, 1.5, dx=1e-6)
```

    -1.2500000001747225

```python
derivative(f, 2, dx=1e-6)
```

    1.000000000139778

```python
derivative(f, 2.5, dx=1e-6)
```

    4.749999999553722

```python

```

### 미분

- **미분(differenciation)**이란 어떤 함수로부터 그 함수와 연관성이 있는 새로운 함수를 만들어내는 작업이다. 미분을 통해 만들어진 새로운 함수는 원래 함수의 기울기(slope)를 나타낸다. 미분으로 만들어진 함수를 원래 함수의 도함수(derivative)라고 한다. 원래는 수렴(converge)과 극한(limit)이라는 수학적인 개념을 사용하여 미분을 정의하지만 여기에서는 자세한 설명을 생략한다.

- 도함수는 원래 함수에 프라임(prime)를 붙이거나 원래 함수의 앞에 d/dx를 붙여서 표시한다. 분수처럼 표기하기도 하는데 분모의 위치에는 미분하고자 하는 변수가 오고 분자의 위치에는 미분하는 함수 자체의 기호나 혹은 계산의 결과로 얻어지는 출력 변수를 넣는다. 예를 들어 y=f(x)라는 함수를 미분하여 구한 도함수는 다음과 같이 여러가지 방법으로 표기할 수 있다. 이 식에서는 f'는 "f프라임(prime)"이라고 읽고 df/dx는 'df over dx'라고 읽는다.
- f' = d/dx*f(x) = d/dx*f = df/dx = d/dx(y) = d/dx*y = dy/dx
- 위치 x에서 도함수의 값 f'(x)은 그 위치에서의 함수의 기울기와 같으므로 다음처럼 쓸 수 있다.
f'(x) = f(x)의 기울기

### 미분 공식
- 몇가지 미분 공식을 조합하면 복잡한 함수의 도함수도 쉽게 구할 수 있다. 여기에서는 가장 핵심적인 4가지 공식만을 소개한다.
    - 기본 미분 공식
    - 선형 조합 법칙
    - 곱셈 법칙
    - 연쇄 법칙

#### 기본 미분 공식
- 상수
상수를 미분하면 0이 된다.
- d/dx(c) = 0

- 거듭제곱
x의 n제곱을 미분하면 n-1 제곱으로 제곱수가 1씩 감소한다. 이 공식은 n이 자연수이거나 음의 정수 일 때 성립한다. n = 0일 때는 성립하지 않는다.
- d/dx(x^n) = nx^(n-1)

- log (로그)
로그함수를 미분하면 x^-1이 된다.
- d/dx(logx) = 1/x

- 지수(e)
밑이 오일러 수인 지수함수는 미분해도 변하지 않는다.
- d/dx(e^x) = e^x

#### 선형 조합 법칙
- 어떤 함수에 상수를 곱한 함수를 미분한 결과는 원래 함수의 도함수에 그 상수를 곱한 것과 같다.
- d/dx(cf) = c*d/df(f)

- 어떤 두 함수를 더한 함수를 미분한 결과는 원래 함수의 도함수를 더한 것과 같다.
- d/dx(f1 + f2) = d/dx(f1) + d/dx(f2)

- 위의 결과를 합치면 어떤 함수에 각각 상수를 곱한 후 더한 **선형 조합(linear combination)**은 각 함수의 도함수를 선형 조합한 것과 같다.
- d/dx(c1f1+c2f2) = c1*d/df*f1 + c2*d/df*f2

- 이러한 기본 공식을 사용하여 다음 함수를 미분하면,
- y = 1 + 2x + 3x^2 + 4 exp(x) + 5log(x)
- y' = 2 + 6x + 4exp(x) + 5/x

- 이 방법으로 위에서 그래프를 그렸던 함수의 도함수를 구하면 다음과 같다.
- f(x) = x^3 -3x^2 + x
- f'(x) = 3*x^2 - 6*x + 1
- 도함수의 값이 기울기와 일치하는 것을 알 수 있다.

```python
def fprime(x):
    return 3*x**2 - 6*x+1

x = np.linspace(-1, 3, 400)

plt.figure(figsize=(10,15))

plt.subplot(211)
plt.plot(x, f(x))
plt.xlim(-2,4)
plt.xticks(np.arange(-1,4))
plt.yticks(np.arange(-5,4))
plt.title('f(x)')

plt.subplot(212)
plt.plot(x, fprime(x))
plt.xlim(-2, 4)
plt.xticks(np.arange(-1,4))
plt.yticks(np.arange(-3,11))
plt.title("f'(x)")

plt.show()
```

#### 곱셈 법칙
- 어떤 함수의 형태가 두 개의 함수를 곱한 것과 같을 때는 다음과 같이 각 개별 함수의 도함수를 사용하여 원래의 함수의 도함수를 구한다. 이를 곱셈법칙이라고 한다.
- d/dx*(f*g) = f* dg/dx + df/dx * g
- 곱셈 법칙을 사용하면 다음과 같은 함수를 미분하여,
- f = x*e^x
- 다음과 같은 도함수를 구한다
- df/dx = xe^x + e^e

#### 연쇄법칙
- 연쇄 법칙(Chain Rule)은 미분하고자 하는 함수의 입력 변수가 다른 함수의 출력 변수인 경우 적용할 수 있다.
- f(x) = h(g(x))
- df/dx = dh/dg * dg/dx
- for example) f = exp((x-u)^2/sigma^2)
- f'= 2(x-u)/sigma^2 * exp((x-u)^2/sigma^2)

#### 연쇄법칙의 예: 로그함수의 미분
- d/dxlogf(x) = f'(x)/f(x)

```python

```

```python
a, b, x = sympy.symbols('a, b, x')
f = sympy.exp(a*x ** b)
sympy.simplify(sympy.diff(f, x))
```

$$a b x^{b - 1} e^{a x^{b}}$$

#### 2차 도함수
- 도함수를 한 번 더 미분하여 만들어진 함수를 2차 도함수 (second derivative)라고 한다. 2차 도함수는 2개의 prime 기호를 붙이거나 d^2/dx^2 기호로 표시한다.

- 예를 들어 y = f(x) 라는 함수를 두번 미분하여 구한 2차 도함수는 다음과 같이 표기.
- f'' = d^2/dx^2(f) = d^2/dx^2*f = d^2f/dx^2 = d^2/dx^2(y) = d^2/dx^2*y = d^2y/dx^2

- 2차 도함수는 도함수의 기울기를 나타낸다. 즉 도함수 값이 증가하면 2차 도함수 값음 양수이고, 도함수 값이 감소하면 2차 도함수 값은 음수이다. 2차 도함수 값이 양수인 경우를 볼록 (convex)하다고 하며 2차 도함수 값이 음수인 경우를 오목 (concave)하다고 한다. 이 때 볼록과 오목은 아래에서 바라본 관점이다.
- 다음 예제의 함수는 f''(x)가 음수인 구간(x < 1)에서 오목(concave)하고 f''(x)가 양수인 구간(x>1)에서 볼록(convex)하다

```python
def fprime2(x):
    return 6*x - 6

x = np.linspace(-1, 3, 400)

plt.figure(figsize=(10,15))

plt.subplot(311)
plt.plot(x, f(x))
plt.xlim(-2, 4)
plt.xticks(np.arange(-1, 4))
plt.yticks(np.arange(-5, 4))
plt.title('f(x)')

plt.subplot(312)
plt.plot(x, fprime(x))
plt.xlim(-2, 4)
plt.xticks(np.arange(-1, 4))
plt.yticks(np.arange(-3, 11))
plt.title("f'(x)")

plt.subplot(313)
plt.plot(x, fprime2(x))
plt.xlim(-2, 4)
plt.xticks(np.arange(-1, 4))
plt.title("f'(x)")

plt.show()
```

##  편미분 (Partial Differentiation)

- 만약 함수가 두 개 이상의 독립변수를 가지는 다변수 함수인 경우에도 미분 즉, 기울기는 하나의 변수에 대해서만 구할 수 있다. 이를 편미분(partial differentiation)이라고 한다. 따라서 편미분의 결과로 하나의 함수에 대해 여러 개의 도함수가 나올 수 있다.

- 편미분의 결과 즉 도함수는 독립 변수를 함수의 아랫첨자로 써서 표기하거나 round기호를 사용하여 표기한다. x, y두 개의 독립 변수를 가지는 함수 f(x,y)의 편미분 도함수는 다음과 같이 표기한다.
- fx(x,y) = round(f)/round(x)
- fy(x,y) = round(f)/round(y)

- 편미분을 하는 방법은 변수가 하나인 함수의 미분과 같다. 다만, 어떤 하나의 독립변수에 대해 미분할 때는 다른 독립 변수를 상수로 생각하면 된다. 예를 들어 x,y라는 두 개의 독립 변수를 가지는 함수엣 x로 편미분 할 때는 y는 독립 변수가 아닌 상수로 생각한다. 마찬가지로 y로 편미분을 할 떄는 x는 독립 변수가 아닌 상수로 생각한다.
- f(x,y) = x^2 + 4xy + 4y^2
- fx(x,y) = 2x + 4y
- fy(x,y) = 4x + 8y

- 편미분에 대해서도 2차 도함수를 정의할 수 있다. 편미분의 2차 도함수를 구할 때는 각각의 미분에 쓰이는 독립 변수를 자유롭게 선택할 수 있다.
- 첫번째 미분과 두번째 미분에서 모두 x에 대해 미분하면 다음과 같이 표기한다.
- fxx(x,y) = round^2f/roundx^2 = 2
- fyy(x,y) = round^2f/roundy^2 = 8
- fxy(x,y) = round^2f/roundyroundx = 4
- fyx(x,y) = round^2f/roundxroundy = 4

```python
x, y = sympy.symbols('x y')
f = sympy.exp((x**2) + 2 * (y ** 2))
sympy.diff(sympy.diff(f,x), x)
```

$$4 x^{2} e^{x^{2} + 2 y^{2}} + 2 e^{x^{2} + 2 y^{2}}$$

```python
sympy.diff(sympy.diff(f,x), y)
```

$$8 x y e^{x^{2} + 2 y^{2}}$$

```python
sympy.diff(sympy.diff(f, y), x)
```

$$8 x y e^{x^{2} + 2 y^{2}}$$

```python
sympy.diff(sympy.diff(f, y), y)
```

$$16 y^{2} e^{x^{2} + 2 y^{2}} + 4 e^{x^{2} + 2 y^{2}}$$

#### Sympy
- SymPy는 심볼릭 연산 (symbolic operation)을 지원하기 위한 파이썬 package. 심볼릭 연산이란 사람이 연필로 계산하는 미분/적분과 동일한 형태의 연산을 말한다. 즉 X^2의 미분 연산을 수행하며 그 결과가 2x라는 형태로 출력된다. Deep learning등에 많이 사용되는 파이썬의 theano package나 Tensorflow 패키지도 기울기 함수 계산을 위해 이러한 심볼릭 연산 기능을 갖추고 있다.

```python
import sympy
# Jupyter 노트북에서 수학식의 LaTex 표현을 위해 필요함
sympy.init_printing(use_latex='mathjax')
```

- 심볼릭 연산에서 사용하는 심볼릭 변수(symbolic variable)는 일반 프로그래밍에서 사용하는 변수와 다르다 일반 프로그래밍에서 사용하는 변수는 이미 메모리에 씌여있는 어떤 숫자를 기호로 쓴 것에 지나치지 않지만 심볼릭 변수는 아무런 숫자도 대입이 되어 있지 않다. 따라서 x^2의 미분 연산을 수행하기 위해서는 우선 Sympy의 symbols명령을 사용하여 x라는 기호가 단순한 숫자나 벡터 변수가 아닌 심볼(symbol)임을 알려주어야 한다. 이렇게 정의된 심볼 변수 symbol클래스 자료형이 된다.

```python
x = sympy.symbols('x')
x
```

$$x$$

```python
type(x)
```

    sympy.core.symbol.Symbol

- 일단 심볼 변수를 정의하면 이를 사용하여 다음과 같이 함수를 정의한다. 이 때는 수학 함수는 Sympy전용 함수를 사용해야 한다.

```python
f = x * sympy.exp(x)
f
```

$$x e^{x}$$

- 함수가 정의되면 diff함수로 미분을 할 수 있다. 또한 simplify함수를 써서 소인수 분해 등을 통한 수식 정리가 가능하다

```python
sympy.diff(f)
```

$$x e^{x} + e^{x}$$

```python
sympy.simplify(sympy.diff(f))
```

$$\left(x + 1\right) e^{x}$$

- 편미분을 하는 경우에는 어떤 변수로 미분하는지를 diff함수로 명시해야 한다. Symbols명령을 사용할 떄는 인수로 주는 문자열에 여러개의 심볼 변수를 동시에 넣을 수도 있다.

```python
x, y = sympy.symbols('x y')
f = x ** 2 + 4 * x * y + 4 *  y ** 2
f
```

$$x^{2} + 4 x y + 4 y^{2}$$

```python
sympy.diff(f, x)
```

$$2 x + 4 y$$

```python
sympy.diff(f, y)
```

$$4 x + 8 y$$

- 상수 심볼을 포함하는 함수를 미분하는 경우, Sympy는 어떤 심볼이 상수이고 어떤 심볼이 변수인지 알 수 없기 때문에 편미분인 것처럼 입력 변수를 지정해야 한다.

```python
x, mu, sigma = sympy.symbols('x mu sigma')
f = sympy.exp((-mu + x) ** 2 / sigma ** 2)
f
```

$$e^{\frac{1}{\sigma^{2}} \left(- \mu + x\right)^{2}}$$

```python
sympy.diff(f, x)
```

$$\frac{1}{\sigma^{2}} \left(- 2 \mu + 2 x\right) e^{\frac{1}{\sigma^{2}} \left(- \mu + x\right)^{2}}$$

```python
sympy.simplify(sympy.diff(f, x))
```

$$\frac{2}{\sigma^{2}} \left(- \mu + x\right) e^{\frac{1}{\sigma^{2}} \left(\mu - x\right)^{2}}$$

- 이차 도함수는 다음처럼 구한다.

```python
sympy.diff(f, x, x)
```

$$\frac{2}{\sigma^{2}} \left(1 + \frac{2}{\sigma^{2}} \left(\mu - x\right)^{2}\right) e^{\frac{1}{\sigma^{2}} \left(\mu - x\right)^{2}}$$

#### 연습 문제 5
- 다음 함수를 미분한 도함수를 sympy를 사용하여 구하라. 여기서 k, a, b는 변수가 아니라 상수이다.
- 1. f(x) = x^3 - 1
- 2. f(x) = log(x^2 - 3k)
- 3. f(x) = exp(ax^b)

```python
# 1. f(x) = x^3 - 1
import sympy
sympy.init_printing(use_latex='mathjax') # 기본 설정값
x = sympy.symbols('x')
f = x ** 3 - 1
sympy.diff(f, x)
```

$$3 x^{2}$$

```python
# 2. f(x) = log(x^2 - 3k)
import sympy
sympy.init_printing(use_latex='mathjax')
x, k = sympy.symbols('x k')
f = sympy.log(x ** 2 - 3 * k)
sympy.diff(f, x)
```

$$\frac{2 x}{- 3 k + x^{2}}$$

```python
# 3. f(x) = exp(ax^b)
import sympy
sympy.init_printing(use_latex='mathjax')
x, a, b = sympy.symbols('x a b')
f = sympy.exp(a*x**b)
sympy.simplify(sympy.diff(f, x))
```

$$a b x^{b - 1} e^{a x^{b}}$$

```python
# 연습문제 6
# 다음 함수에 대한 1차, 2차 편미분 Sympy를 구하라
# fx, fy, fxx, fxy, fyx, fyy를 Sympy로 활용
# f(x, y) = exp(x^2 + 2y^2)

# 1) fx
import sympy
sympy.init_printing(use_latex='mathjax')
x, y = sympy.symbols('x y')
f = sympy.exp(x ** 2 + 2 * y ** 2)
f
```

$$e^{x^{2} + 2 y^{2}}$$

```python
#1 fx
sympy.diff(f, x)
```

$$2 x e^{x^{2} + 2 y^{2}}$$

```python
#2 fy
sympy.diff(f, y)
```

$$4 y e^{x^{2} + 2 y^{2}}$$

```python
#3 fxx
sympy.diff(f, x, x)
```

$$2 \left(2 x^{2} + 1\right) e^{x^{2} + 2 y^{2}}$$

```python
#4 fxy
sympy.diff(f, x, y)
```

$$8 x y e^{x^{2} + 2 y^{2}}$$

```python
#5 fyx
sympy.diff(f, y, x)
```

$$8 x y e^{x^{2} + 2 y^{2}}$$

```python
#6 fyy
sympy.diff(f, y, y)
```

$$4 \left(4 y^{2} + 1\right) e^{x^{2} + 2 y^{2}}$$

```python

```


```python
# This code block is for automatic testing purposes, please ignore.
try:
    import openfermion
except:
    import os
    os.chdir('../src/')
```

# Introduction to OpenFermion
Note that all the examples below must be run sequentially within a section.

## Initializing the FermionOperator data structure

Fermionic systems are often treated in second quantization where arbitrary operators can be expressed using the fermionic creation and annihilation operators, $a^\dagger_k$ and $a_k$.  The fermionic ladder operators play a similar role to their qubit ladder operator counterparts, $\sigma^+_k$ and $\sigma^-_k$ but are distinguished by the canonical fermionic anticommutation relations, $\{a^\dagger_i, a^\dagger_j\} = \{a_i, a_j\} = 0$ and $\{a_i, a_j^\dagger\} = \delta_{ij}$. Any weighted sums of products of these operators are represented with the FermionOperator data structure in OpenFermion. The following are examples of valid FermionOperators:

$$
\begin{align}
& a_1 \nonumber \\
& 1.7 a^\dagger_3 \nonumber \\
&-1.7 \, a^\dagger_3 a_1 \nonumber \\
&(1 + 2i) \, a^\dagger_4 a^\dagger_3 a_9 a_1 \nonumber \\
&(1 + 2i) \, a^\dagger_4 a^\dagger_3 a_9 a_1 - 1.7 \, a^\dagger_3 a_1 \nonumber
\end{align}
$$

The FermionOperator class is contained in $\textrm{ops/_fermion_operators.py}$. In order to support fast addition of FermionOperator instances, the class is implemented as hash table (python dictionary). The keys of the dictionary encode the strings of ladder operators and values of the dictionary store the coefficients. The strings of ladder operators are encoded as a tuple of 2-tuples which we refer to as the "terms tuple". Each ladder operator is represented by a 2-tuple. The first element of the 2-tuple is an int indicating the tensor factor on which the ladder operator acts. The second element of the 2-tuple is Boole: 1 represents raising and 0 represents lowering. For instance, $a^\dagger_8$ is represented in a 2-tuple as $(8, 1)$. Note that indices start at 0 and the identity operator is an empty list. Below we give some examples of operators and their terms tuple:

$$
\begin{align}
I & \mapsto () \nonumber \\
a_1 & \mapsto ((1, 0),) \nonumber \\
a^\dagger_3 & \mapsto ((3, 1),) \nonumber \\
a^\dagger_3 a_1 & \mapsto ((3, 1), (1, 0)) \nonumber \\
a^\dagger_4 a^\dagger_3 a_9 a_1 & \mapsto ((4, 1), (3, 1), (9, 0), (1, 0)) \nonumber
\end{align}
$$

Note that when initializing a single ladder operator one should be careful to add the comma after the inner pair. This is because in python ((1, 2)) = (1, 2) whereas ((1, 2),) = ((1, 2),). The "terms tuple" is usually convenient when one wishes to initialize a term as part of a coded routine. However, the terms tuple is not particularly intuitive. Accordingly, OpenFermion also supports another user-friendly, string notation below. This representation is rendered when calling "print" on a FermionOperator.

$$
\begin{align}
I & \mapsto \textrm{""} \nonumber \\
a_1 & \mapsto \textrm{"1"} \nonumber \\
a^\dagger_3 & \mapsto \textrm{"3^"} \nonumber \\
a^\dagger_3 a_1 & \mapsto \textrm{"3^}\;\textrm{1"} \nonumber \\
a^\dagger_4 a^\dagger_3 a_9 a_1 & \mapsto \textrm{"4^}\;\textrm{3^}\;\textrm{9}\;\textrm{1"} \nonumber
\end{align}
$$

Let's initialize our first term! We do it two different ways below.

```python
from openfermion.ops import FermionOperator

my_term = FermionOperator(((3, 1), (1, 0)))
print(my_term)

my_term = FermionOperator('3^ 1')
print(my_term)
```

    1.0 [3^ 1]
    1.0 [3^ 1]

The preferred way to specify the coefficient in openfermion is to provide an optional coefficient argument. If not provided, the coefficient defaults to 1. In the code below, the first method is preferred. The multiplication in the second method actually creates a copy of the term, which introduces some additional cost. All inplace operands (such as +=) modify classes whereas binary operands such as + create copies. Important caveats are that the empty tuple FermionOperator(()) and the empty string FermionOperator('') initializes identity. The empty initializer FermionOperator() initializes the zero operator.

```python
good_way_to_initialize = FermionOperator('3^ 1', -1.7)
print(good_way_to_initialize)

bad_way_to_initialize = -1.7 * FermionOperator('3^ 1')
print(bad_way_to_initialize)

identity = FermionOperator('')
print(identity)

zero_operator = FermionOperator()
print(zero_operator)
```

    -1.7 [3^ 1]
    -1.7 [3^ 1]
    1.0 []
    0

Note that FermionOperator has only one attribute: .terms. This attribute is the dictionary which stores the term tuples.

```python
my_operator = FermionOperator('4^ 1^ 3 9', 1. + 2.j)
print(my_operator)
print(my_operator.terms)
```

    (1+2j) [4^ 1^ 3 9]
    {((4, 1), (1, 1), (3, 0), (9, 0)): (1+2j)}

## Manipulating the FermionOperator data structure
So far we have explained how to initialize a single FermionOperator such as $-1.7 \, a^\dagger_3 a_1$. However, in general we will want to represent sums of these operators such as $(1 + 2i) \, a^\dagger_4 a^\dagger_3 a_9 a_1 - 1.7 \, a^\dagger_3 a_1$. To do this, just add together two FermionOperators! We demonstrate below.

```python
from openfermion.ops import FermionOperator

term_1 = FermionOperator('4^ 3^ 9 1', 1. + 2.j)
term_2 = FermionOperator('3^ 1', -1.7)
my_operator = term_1 + term_2
print(my_operator)

my_operator = FermionOperator('4^ 3^ 9 1', 1. + 2.j)
term_2 = FermionOperator('3^ 1', -1.7)
my_operator += term_2
print('')
print(my_operator)
```

    (1+2j) [4^ 3^ 9 1] +
    -1.7 [3^ 1]

    (1+2j) [4^ 3^ 9 1] +
    -1.7 [3^ 1]

The print function prints each term in the operator on a different line. Note that the line my_operator = term_1 + term_2 creates a new object, which involves a copy of term_1 and term_2. The second block of code uses the inplace method +=, which is more efficient. This is especially important when trying to construct a very large FermionOperator. FermionOperators also support a wide range of builtins including, str(), repr(), ==, !=, *=, *, /, /=, +, +=, -, -=, - and **. Note that since FermionOperators involve floats, == and != check for (in)equality up to numerical precision. We demonstrate some of these methods below.

```python
term_1 = FermionOperator('4^ 3^ 9 1', 1. + 2.j)
term_2 = FermionOperator('3^ 1', -1.7)

my_operator = term_1 - 33. * term_2
print(my_operator)

my_operator *= 3.17 * (term_2 + term_1) ** 2
print('')
print(my_operator)

print('')
print(term_2 ** 3)

print('')
print(term_1 == 2.*term_1 - term_1)
print(term_1 == my_operator)
```

    (1+2j) [4^ 3^ 9 1] +
    56.1 [3^ 1]

    (9.161299999999999+18.322599999999998j) [4^ 3^ 9 1 3^ 1 3^ 1] +
    (16.166999999999998-21.555999999999997j) [4^ 3^ 9 1 3^ 1 4^ 3^ 9 1] +
    (16.166999999999998-21.555999999999997j) [4^ 3^ 9 1 4^ 3^ 9 1 3^ 1] +
    (-34.87-6.34j) [4^ 3^ 9 1 4^ 3^ 9 1 4^ 3^ 9 1] +
    513.9489299999999 [3^ 1 3^ 1 3^ 1] +
    (-302.32289999999995-604.6457999999999j) [3^ 1 3^ 1 4^ 3^ 9 1] +
    (-302.32289999999995-604.6457999999999j) [3^ 1 4^ 3^ 9 1 3^ 1] +
    (-533.511+711.348j) [3^ 1 4^ 3^ 9 1 4^ 3^ 9 1]

    -4.912999999999999 [3^ 1 3^ 1 3^ 1]

    True
    False

Additionally, there are a variety of methods that act on the FermionOperator data structure. We demonstrate a small subset of those methods here.

```python
from openfermion.ops import normal_ordered
from openfermion.utils import commutator, count_qubits, hermitian_conjugated

# Get the Hermitian conjugate of a FermionOperator, count its qubit, check if it is normal-ordered.
term_1 = FermionOperator('4^ 3 3^', 1. + 2.j)
print(hermitian_conjugated(term_1))
print(term_1.is_normal_ordered())
print(count_qubits(term_1))

# Normal order the term.
term_2 = normal_ordered(term_1)
print('')
print(term_2)
print(term_2.is_normal_ordered())

# Compute a commutator of the terms.
print('')
print(commutator(term_1, term_2))
```

    (1-2j) [3 3^ 4]
    False
    5

    (1+2j) [4^] +
    (-1-2j) [4^ 3^ 3]
    True

    (-3+4j) [4^ 3 3^ 4^] +
    (3-4j) [4^ 3 3^ 4^ 3^ 3] +
    (3-4j) [4^ 4^ 3 3^] +
    (-3+4j) [4^ 3^ 3 4^ 3 3^]

## The QubitOperator data structure
The QubitOperator data structure is another essential part of openfermion. As the name suggests, QubitOperator is used to store qubit operators in almost exactly the same way that FermionOperator is used to store fermion operators. For instance $X_0 Z_3 Y_4$ is a QubitOperator. The internal representation of this as a terms tuple would be $((0, \textrm{"X"}), (3, \textrm{"Z"}), (4, \textrm{"Y"}))$. Note that one important difference between QubitOperator and FermionOperator is that the terms in QubitOperator are always sorted in order of tensor factor. In some cases, this enables faster manipulation. We initialize some QubitOperators below.

```python
from openfermion.ops import QubitOperator

my_first_qubit_operator = QubitOperator('X1 Y2 Z3')
print(my_first_qubit_operator)
print(my_first_qubit_operator.terms)

operator_2 = QubitOperator('X3 Z4', 3.17)
operator_2 -= 77. * my_first_qubit_operator
print('')
print(operator_2)
```

    1.0 X1 Y2 Z3
    {((1, 'X'), (2, 'Y'), (3, 'Z')): 1.0}

    3.17 X3 Z4 +
    -77.0 X1 Y2 Z3

## Jordan-Wigner and Bravyi-Kitaev
openfermion provides functions for mapping FermionOperators to QubitOperators.

```python
from openfermion.ops import FermionOperator
from openfermion.transforms import jordan_wigner, bravyi_kitaev
from openfermion.utils import eigenspectrum, hermitian_conjugated

# Initialize an operator.
fermion_operator = FermionOperator('2^ 0', 3.17)
fermion_operator += hermitian_conjugated(fermion_operator)
print(fermion_operator)

# Transform to qubits under the Jordan-Wigner transformation and print its spectrum.
jw_operator = jordan_wigner(fermion_operator)
print('')
print(jw_operator)
jw_spectrum = eigenspectrum(jw_operator)
print(jw_spectrum)

# Transform to qubits under the Bravyi-Kitaev transformation and print its spectrum.
bk_operator = bravyi_kitaev(fermion_operator)
print('')
print(bk_operator)
bk_spectrum = eigenspectrum(bk_operator)
print(bk_spectrum)
```

    3.17 [2^ 0] +
    3.17 [0^ 2]

    (1.585+0j) X0 Z1 X2 +
    (1.585+0j) Y0 Z1 Y2
    [-3.17 -3.17  0.    0.    0.    0.    3.17  3.17]

    (-1.585+0j) Y0 Y1 +
    (-1.585+0j) X0 X1 Z2
    [-3.17 -3.17  0.    0.    0.    0.    3.17  3.17]

We see that despite the different representation, these operators are iso-spectral. We can also apply the Jordan-Wigner transform in reverse to map arbitrary QubitOperators to FermionOperators. Note that we also demonstrate the .compress() method (a method on both FermionOperators and QubitOperators) which removes zero entries.

```python
from openfermion.transforms import reverse_jordan_wigner

# Initialize QubitOperator.
my_operator = QubitOperator('X0 Y1 Z2', 88.)
my_operator += QubitOperator('Z1 Z4', 3.17)
print(my_operator)

# Map QubitOperator to a FermionOperator.
mapped_operator = reverse_jordan_wigner(my_operator)
print('')
print(mapped_operator)

# Map the operator back to qubits and make sure it is the same.
back_to_normal = jordan_wigner(mapped_operator)
back_to_normal.compress()
print('')
print(back_to_normal)
```

    88.0 X0 Y1 Z2 +
    3.17 Z1 Z4

    -88j [1^ 0^] +
    88j [1^ 0] +
    88j [1 0^] +
    -88j [1 0] +
    176j [2^ 2 1^ 0^] +
    -176j [2^ 2 1^ 0] +
    -176j [2^ 2 1 0^] +
    176j [2^ 2 1 0] +
    3.17 [] +
    -6.34 [1^ 1] +
    -6.34 [4^ 4] +
    12.68 [4^ 4 1^ 1]

    88.0 X0 Y1 Z2 +
    3.17 Z1 Z4

## Sparse matrices and the Hubbard model
Often, one would like to obtain a sparse matrix representation of an operator which can be analyzed numerically. There is code in both openfermion.transforms and openfermion.utils which facilitates this. The function get_sparse_operator converts either a FermionOperator, a QubitOperator or other more advanced classes such as InteractionOperator to a scipy.sparse.csc matrix. There are numerous functions in openfermion.utils which one can call on the sparse operators such as "get_gap", "get_hartree_fock_state", "get_ground_state", etc. We show this off by computing the ground state energy of the Hubbard model. To do that, we use code from the openfermion.hamiltonians module which constructs lattice models of fermions such as Hubbard models.

```python
from openfermion.hamiltonians import fermi_hubbard
from openfermion.transforms import get_sparse_operator, jordan_wigner
from openfermion.utils import get_ground_state

# Set model.
x_dimension = 2
y_dimension = 2
tunneling = 2.
coulomb = 1.
magnetic_field = 0.5
chemical_potential = 0.25
periodic = 1
spinless = 1

# Get fermion operator.
hubbard_model = fermi_hubbard(
    x_dimension, y_dimension, tunneling, coulomb, chemical_potential,
    magnetic_field, periodic, spinless)
print(hubbard_model)

# Get qubit operator under Jordan-Wigner.
jw_hamiltonian = jordan_wigner(hubbard_model)
jw_hamiltonian.compress()
print('')
print(jw_hamiltonian)

# Get scipy.sparse.csc representation.
sparse_operator = get_sparse_operator(hubbard_model)
print('')
print(sparse_operator)
print('\nEnergy of the model is {} in units of T and J.'.format(
    get_ground_state(sparse_operator)[0]))
```

    -0.25 [0^ 0] +
    1.0 [0^ 0 1^ 1] +
    -2.0 [0^ 1] +
    -2.0 [1^ 0] +
    1.0 [0^ 0 2^ 2] +
    -2.0 [0^ 2] +
    -2.0 [2^ 0] +
    -0.25 [1^ 1] +
    1.0 [1^ 1 3^ 3] +
    -2.0 [1^ 3] +
    -2.0 [3^ 1] +
    -0.25 [2^ 2] +
    1.0 [2^ 2 3^ 3] +
    -2.0 [2^ 3] +
    -2.0 [3^ 2] +
    -0.25 [3^ 3]

    0.5 I +
    -0.375 Z0 +
    -0.375 Z1 +
    0.25 Z0 Z1 +
    -1.0 Y0 Y1 +
    -1.0 X0 X1 +
    -0.375 Z2 +
    0.25 Z0 Z2 +
    -1.0 Y0 Z1 Y2 +
    -1.0 X0 Z1 X2 +
    -0.375 Z3 +
    0.25 Z1 Z3 +
    -1.0 Y1 Z2 Y3 +
    -1.0 X1 Z2 X3 +
    0.25 Z2 Z3 +
    -1.0 Y2 Y3 +
    -1.0 X2 X3

      (1, 1)	(-0.25+0j)
      (2, 1)	(-2+0j)
      (4, 1)	(-2+0j)
      (1, 2)	(-2+0j)
      (2, 2)	(-0.25+0j)
      (8, 2)	(-2+0j)
      (3, 3)	(0.5+0j)
      (6, 3)	(2+0j)
      (9, 3)	(-2+0j)
      (1, 4)	(-2+0j)
      (4, 4)	(-0.25+0j)
      (8, 4)	(-2+0j)
      (5, 5)	(0.5+0j)
      (6, 5)	(-2+0j)
      (9, 5)	(-2+0j)
      (3, 6)	(2+0j)
      (5, 6)	(-2+0j)
      (6, 6)	(-0.5+0j)
      (10, 6)	(-2+0j)
      (12, 6)	(2+0j)
      (7, 7)	(1.25+0j)
      (11, 7)	(-2+0j)
      (13, 7)	(2+0j)
      (2, 8)	(-2+0j)
      (4, 8)	(-2+0j)
      (8, 8)	(-0.25+0j)
      (3, 9)	(-2+0j)
      (5, 9)	(-2+0j)
      (9, 9)	(-0.5+0j)
      (10, 9)	(-2+0j)
      (12, 9)	(-2+0j)
      (6, 10)	(-2+0j)
      (9, 10)	(-2+0j)
      (10, 10)	(0.5+0j)
      (7, 11)	(-2+0j)
      (11, 11)	(1.25+0j)
      (14, 11)	(2+0j)
      (6, 12)	(2+0j)
      (9, 12)	(-2+0j)
      (12, 12)	(0.5+0j)
      (7, 13)	(2+0j)
      (13, 13)	(1.25+0j)
      (14, 13)	(-2+0j)
      (11, 14)	(2+0j)
      (13, 14)	(-2+0j)
      (14, 14)	(1.25+0j)
      (15, 15)	(3+0j)

    Energy of the model is -4.250000000000004 in units of T and J.

## Hamiltonians in the plane wave basis
A user can write plugins to openfermion which allow for the use of, e.g., third-party electronic structure package to compute molecular orbitals, Hamiltonians, energies, reduced density matrices, coupled cluster amplitudes, etc using Gaussian basis sets. We may provide scripts which interface between such packages and openfermion in future but do not discuss them in this tutorial.

When using simpler basis sets such as plane waves, these packages are not needed. openfermion comes with code which computes Hamiltonians in the plane wave basis. Note that when using plane waves, one is working with the periodized Coulomb operator, best suited for condensed phase calculations such as studying the electronic structure of a solid. To obtain these Hamiltonians one must choose to study the system without a spin degree of freedom (spinless), one must the specify dimension in which the calculation is performed (n_dimensions, usually 3), one must specify how many plane waves are in each dimension (grid_length) and one must specify the length scale of the plane wave harmonics in each dimension (length_scale) and also the locations and charges of the nuclei. One can generate these models with plane_wave_hamiltonian() found in openfermion.hamiltonians. For simplicity, below we compute the Hamiltonian in the case of zero external charge (corresponding to the uniform electron gas, aka jellium). We also demonstrate that one can transform the plane wave Hamiltonian using a Fourier transform without effecting the spectrum of the operator.

```python
from openfermion.hamiltonians import jellium_model
from openfermion.utils import eigenspectrum, fourier_transform, Grid
from openfermion.transforms import jordan_wigner

# Let's look at a very small model of jellium in 1D.
grid = Grid(dimensions=1, length=3, scale=1.0)
spinless = True

# Get the momentum Hamiltonian.
momentum_hamiltonian = jellium_model(grid, spinless)
momentum_qubit_operator = jordan_wigner(momentum_hamiltonian)
momentum_qubit_operator.compress()
print(momentum_qubit_operator)

# Fourier transform the Hamiltonian to the position basis.
position_hamiltonian = fourier_transform(momentum_hamiltonian, grid, spinless)
position_qubit_operator = jordan_wigner(position_hamiltonian)
position_qubit_operator.compress()
print('')
print (position_qubit_operator)

# Check the spectra to make sure these representations are iso-spectral.
spectral_difference = eigenspectrum(momentum_qubit_operator) -  eigenspectrum(position_qubit_operator)
print('')
print(spectral_difference)
```

    19.50047638754088 I +
    -9.71044945799746 Z0 +
    0.15915494309189535 Z1 +
    -9.71044945799746 Z2 +
    -0.07957747154594767 Z0 Z1 +
    -0.07957747154594767 Z1 Z2 +
    -0.07957747154594767 Z0 Z2

    19.500476387540854 I +
    -6.420581324301009 Z0 +
    -3.289868133696451 Y0 Y1 +
    -3.289868133696451 X0 X1 +
    -3.289868133696454 Y0 Z1 Y2 +
    -3.289868133696454 X0 Z1 X2 +
    -6.4205813243010095 Z1 +
    -3.289868133696451 Y1 Y2 +
    -3.289868133696451 X1 X2 +
    -6.420581324301009 Z2 +
    -0.07957747154594766 Z0 Z1 +
    -0.07957747154594763 Z0 Z2 +
    -0.07957747154594766 Z1 Z2

    [  2.74502643e-14   2.66869860e-14   2.84217094e-14   7.10542736e-15
       2.84217094e-14   2.13162821e-14   2.13162821e-14   0.00000000e+00]

## Basics of MolecularData class

Data from electronic structure calculations can be saved in an OpenFermion data structure called MolecularData, which makes it easy to access within our library. Often, one would like to analyze a chemical series or look at many different Hamiltonians and sometimes the electronic structure calculations are either expensive to compute or difficult to converge (e.g. one needs to mess around with different types of SCF routines to make things converge). Accordingly, we anticipate that users will want some way to automatically database the results of their electronic structure calculations so that important data (such as the SCF integrals) can be looked up on-the-fly if the user has computed them in the past. OpenFermion supports a data provenance strategy which saves key results of the electronic structure calculation (including pointers to files containing large amounts of data, such as the molecular integrals) in an HDF5 container.

The MolecularData class stores information about molecules. One initializes a MolecularData object by specifying parameters of a molecule such as its geometry, basis, multiplicity, charge and an optional string describing it. One can also initialize MolecularData simply by providing a string giving a filename where a previous MolecularData object was saved in an HDF5 container. One can save a MolecularData instance by calling the class's .save() method. This automatically saves the instance in a data folder specified during OpenFermion installation. The name of the file is generated automatically from the instance attributes and optionally provided description. Alternatively, a filename can also be provided as an optional input if one wishes to manually name the file.

When electronic structure calculations are run, the data files for the molecule can be automatically updated. If one wishes to later use that data they either initialize MolecularData with the instance filename or initialize the instance and then later call the .load() method.

Basis functions are provided to initialization using a string such as "6-31g". Geometries can be specified using a simple txt input file (see geometry_from_file function in molecular_data.py) or can be passed using a simple python list format demonstrated below. Atoms are specified using a string for their atomic symbol. Distances should be provided in angstrom. Below we initialize a simple instance of MolecularData without performing any electronic structure calculations.

```python
from openfermion.hamiltonians import MolecularData

# Set parameters to make a simple molecule.
diatomic_bond_length = .7414
geometry = [('H', (0., 0., 0.)), ('H', (0., 0., diatomic_bond_length))]
basis = 'sto-3g'
multiplicity = 1
charge = 0
description = str(diatomic_bond_length)

# Make molecule and print out a few interesting facts about it.
molecule = MolecularData(geometry, basis, multiplicity,
                         charge, description)
print('Molecule has automatically generated name {}'.format(
    molecule.name))
print('Information about this molecule would be saved at:\n{}\n'.format(
    molecule.filename))
print('This molecule has {} atoms and {} electrons.'.format(
    molecule.n_atoms, molecule.n_electrons))
for atom, atomic_number in zip(molecule.atoms, molecule.protons):
    print('Contains {} atom, which has {} protons.'.format(
        atom, atomic_number))
```

    Molecule has automatically generated name H2_sto-3g_singlet_0.7414
    Information about this molecule would be saved at:
    /home/kjs/Projects/OpenFermion/src/openfermion/data/H2_sto-3g_singlet_0.7414

    This molecule has 2 atoms and 2 electrons.
    Contains H atom, which has 1 protons.
    Contains H atom, which has 1 protons.

If we had previously computed this molecule using an electronic structure package, we can call molecule.load() to populate all sorts of interesting fields in the data structure. Though we make no assumptions about what electronic structure packages users might install, we assume that the calculations are saved in OpenFermion's MolecularData objects. Currently plugins are available for [Psi4](http://psicode.org/) [(OpenFermion-Psi4)](http://github.com/quantumlib/OpenFermion-Psi4) and [PySCF](https://github.com/sunqm/pyscf) [(OpenFermion-PySCF)](http://github.com/quantumlib/OpenFermion-PySCF), and there may be more in the future. For the purposes of this example, we will load data that ships with OpenFermion to make a plot of the energy surface of hydrogen. Note that helper functions to initialize some interesting chemical benchmarks are found in openfermion.utils.

```python
# Set molecule parameters.
basis = 'sto-3g'
multiplicity = 1
bond_length_interval = 0.1
n_points = 25

# Generate molecule at different bond lengths.
hf_energies = []
fci_energies = []
bond_lengths = []
for point in range(3, n_points + 1):
    bond_length = bond_length_interval * point
    bond_lengths += [bond_length]
    description = str(round(bond_length,2))
    print(description)
    geometry = [('H', (0., 0., 0.)), ('H', (0., 0., bond_length))]
    molecule = MolecularData(
        geometry, basis, multiplicity, description=description)

    # Load data.
    molecule.load()

    # Print out some results of calculation.
    print('\nAt bond length of {} angstrom, molecular hydrogen has:'.format(
        bond_length))
    print('Hartree-Fock energy of {} Hartree.'.format(molecule.hf_energy))
    print('MP2 energy of {} Hartree.'.format(molecule.mp2_energy))
    print('FCI energy of {} Hartree.'.format(molecule.fci_energy))
    print('Nuclear repulsion energy between protons is {} Hartree.'.format(
        molecule.nuclear_repulsion))
    for orbital in range(molecule.n_orbitals):
        print('Spatial orbital {} has energy of {} Hartree.'.format(
            orbital, molecule.orbital_energies[orbital]))
    hf_energies += [molecule.hf_energy]
    fci_energies += [molecule.fci_energy]

# Plot.
import matplotlib.pyplot as plt
%matplotlib inline

plt.figure(0)
plt.plot(bond_lengths, fci_energies, 'x-')
plt.plot(bond_lengths, hf_energies, 'o-')
plt.ylabel('Energy in Hartree')
plt.xlabel('Bond length in angstrom')
plt.show()
```

## InteractionOperator and InteractionRDM for efficient numerical representations

Fermion Hamiltonians can be expressed as $H = h_0 + \sum_{pq} h_{pq}\, a^\dagger_p a_q + \frac{1}{2} \sum_{pqrs} h_{pqrs} \, a^\dagger_p a^\dagger_q a_r a_s$ where $h_0$ is a constant shift due to the nuclear repulsion and $h_{pq}$ and $h_{pqrs}$ are the famous molecular integrals. Since fermions interact pairwise, their energy is thus a unique function of the one-particle and two-particle reduced density matrices which are expressed in second quantization as $\rho_{pq} = \left \langle p \mid a^\dagger_p a_q \mid q \right \rangle$ and $\rho_{pqrs} = \left \langle pq \mid a^\dagger_p a^\dagger_q a_r a_s \mid rs \right \rangle$, respectively.

Because the RDMs and molecular Hamiltonians are both compactly represented and manipulated as 2- and 4- index tensors, we can represent them in a particularly efficient form using  similar data structures. The InteractionOperator data structure can be initialized for a Hamiltonian by passing the constant $h_0$ (or 0), as well as numpy arrays representing $h_{pq}$ (or $\rho_{pq}$) and $h_{pqrs}$ (or $\rho_{pqrs}$). Importantly, InteractionOperators can also be obtained by calling MolecularData.get_molecular_hamiltonian() or by calling the function get_interaction_operator() (found in openfermion.transforms) on a FermionOperator. The InteractionRDM data structure is similar but represents RDMs. For instance, one can get a molecular RDM by calling MolecularData.get_molecular_rdm(). When generating Hamiltonians from the MolecularData class, one can choose to restrict the system to an active space.

These classes inherit from the same base class, PolynomialTensor. This data structure overloads the slice operator [] so that one can get or set the key attributes of the InteractionOperator: $\textrm{.constant}$, $\textrm{.one_body_coefficients}$ and $\textrm{.two_body_coefficients}$ . For instance, InteractionOperator[(p, 1), (q, 1), (r, 0), (s, 0)] would return $h_{pqrs}$ and InteractionRDM would return $\rho_{pqrs}$. Importantly, the class supports fast basis transformations using the method PolynomialTensor.rotate_basis(rotation_matrix).
But perhaps most importantly, one can map the InteractionOperator to any of the other data structures we've described here.

Below, we load MolecularData from a saved calculation of LiH. We then obtain an InteractionOperator representation of this system in an active space. We then map that operator to qubits. We then demonstrate that one can rotate the orbital basis of the InteractionOperator using random angles to obtain a totally different operator that is still iso-spectral.

```python
from openfermion.hamiltonians import MolecularData
from openfermion.transforms import get_fermion_operator, get_sparse_operator, jordan_wigner
from openfermion.utils import get_ground_state
import numpy
import scipy
import scipy.linalg

# Load saved file for LiH.
diatomic_bond_length = 1.45
geometry = [('Li', (0., 0., 0.)), ('H', (0., 0., diatomic_bond_length))]
basis = 'sto-3g'
multiplicity = 1

# Set Hamiltonian parameters.
active_space_start = 1
active_space_stop = 3

# Generate and populate instance of MolecularData.
molecule = MolecularData(geometry, basis, multiplicity, description="1.45")
molecule.load()

# Get the Hamiltonian in an active space.
molecular_hamiltonian = molecule.get_molecular_hamiltonian(
    occupied_indices=range(active_space_start),
    active_indices=range(active_space_start, active_space_stop))

# Map operator to fermions and qubits.
fermion_hamiltonian = get_fermion_operator(molecular_hamiltonian)
qubit_hamiltonian = jordan_wigner(fermion_hamiltonian)
qubit_hamiltonian.compress()
print('The Jordan-Wigner Hamiltonian in canonical basis follows:\n{}'.format(qubit_hamiltonian))

# Get sparse operator and ground state energy.
sparse_hamiltonian = get_sparse_operator(qubit_hamiltonian)
energy, state = get_ground_state(sparse_hamiltonian)
print('Ground state energy before rotation is {} Hartree.\n'.format(energy))

# Randomly rotate.
n_orbitals = molecular_hamiltonian.n_qubits // 2
n_variables = int(n_orbitals * (n_orbitals - 1) / 2)
numpy.random.seed(1)
random_angles = numpy.pi * (1. - 2. * numpy.random.rand(n_variables))
kappa = numpy.zeros((n_orbitals, n_orbitals))
index = 0
for p in range(n_orbitals):
    for q in range(p + 1, n_orbitals):
        kappa[p, q] = random_angles[index]
        kappa[q, p] = -numpy.conjugate(random_angles[index])
        index += 1

    # Build the unitary rotation matrix.
    difference_matrix = kappa + kappa.transpose()
    rotation_matrix = scipy.linalg.expm(kappa)

    # Apply the unitary.
    molecular_hamiltonian.rotate_basis(rotation_matrix)

# Get qubit Hamiltonian in rotated basis.
qubit_hamiltonian = jordan_wigner(molecular_hamiltonian)
qubit_hamiltonian.compress()
print('The Jordan-Wigner Hamiltonian in rotated basis follows:\n{}'.format(qubit_hamiltonian))

# Get sparse Hamiltonian and energy in rotated basis.
sparse_hamiltonian = get_sparse_operator(qubit_hamiltonian)
energy, state = get_ground_state(sparse_hamiltonian)
print('Ground state energy after rotation is {} Hartree.'.format(energy))
```

    The Jordan-Wigner Hamiltonian in canonical basis follows:
    -7.49894690201071 I +
    0.16199475388004184 Z0 +
    0.01291078027311749 Y0 Z1 Y2 +
    0.01291078027311749 X0 Z1 X2 +
    0.16199475388004186 Z1 +
    0.012910780273117487 Y1 Z2 Y3 +
    0.012910780273117487 X1 Z2 X3 +
    -0.013243698330265966 Z2 +
    -0.013243698330265952 Z3 +
    0.12444770133137588 Z0 Z1 +
    0.011536413200774975 Y0 Y2 +
    0.011536413200774975 X0 X2 +
    0.011536413200774975 Z0 Y1 Z2 Y3 +
    0.011536413200774975 Z0 X1 Z2 X3 +
    0.0029329964409502266 Y0 X1 X2 Y3 +
    -0.0029329964409502266 Y0 Y1 X2 X3 +
    -0.0029329964409502266 X0 X1 Y2 Y3 +
    0.0029329964409502266 X0 Y1 Y2 X3 +
    0.054130445793298836 Z0 Z2 +
    0.05706344223424907 Z0 Z3 +
    -0.0013743761078958677 Y0 Z1 Y2 Z3 +
    -0.0013743761078958677 X0 Z1 X2 Z3 +
    0.05706344223424907 Z1 Z2 +
    -0.0013743761078958677 Y1 Y3 +
    -0.0013743761078958677 X1 X3 +
    0.054130445793298836 Z1 Z3 +
    0.08479609543670981 Z2 Z3
    Ground state energy before rotation is -7.8627731630279865 Hartree.

    The Jordan-Wigner Hamiltonian in rotated basis follows:
    -7.498946902010708 I +
    0.04248358003893336 Z0 +
    0.042483580038933336 Z1 +
    0.0823812751073221 Z0 Z1 +
    -0.005524724636333855 X0 X2 +
    -0.08262397170394276 X0 Z1 X2 +
    -0.005524724636333855 Y0 Y2 +
    -0.08262397170394276 Y0 Z1 Y2 +
    -0.005524724636333857 Z0 X1 Z2 X3 +
    -0.08262397170394277 X1 Z2 X3 +
    -0.005524724636333857 Z0 Y1 Z2 Y3 +
    -0.08262397170394277 Y1 Z2 Y3 +
    -0.024260054468246924 X0 X1 Y2 Y3 +
    0.024260054468246924 X0 Y1 Y2 X3 +
    0.024260054468246924 Y0 X1 X2 Y3 +
    -0.024260054468246924 Y0 Y1 X2 X3 +
    0.1062674755108427 Z2 +
    0.054130445793298815 Z0 Z2 +
    0.1062674755108427 Z3 +
    0.07839050026154575 Z0 Z3 +
    -0.016734989174013656 X0 Z1 X2 Z3 +
    -0.016734989174013656 Y0 Z1 Y2 Z3 +
    0.07839050026154575 Z1 Z2 +
    -0.016734989174013656 X1 X3 +
    -0.016734989174013656 Y1 Y3 +
    0.054130445793298815 Z1 Z3 +
    0.08420840560617016 Z2 Z3
    Ground state energy after rotation is -7.862773163027971 Hartree.

## Quadratic Hamiltonians and Slater determinants

The general electronic structure Hamiltonian
$H = h_0 + \sum_{pq} h_{pq}\, a^\dagger_p a_q + \frac{1}{2} \sum_{pqrs} h_{pqrs} \, a^\dagger_p a^\dagger_q a_r a_s$ contains terms that act on up to 4 sites, or
is quartic in the fermionic creation and annihilation operators. However, in many situations
we may fruitfully approximate these Hamiltonians by replacing these quartic terms with
terms that act on at most 2 fermionic sites, or quadratic terms, as in mean-field approximation theory.
These Hamiltonians have a number of
special properties one can exploit for efficient simulation and manipulation of the Hamiltonian, thus
warranting a special data structure.  We refer to Hamiltonians which
only contain terms that are quadratic in the fermionic creation and annihilation operators
as quadratic Hamiltonians, and include the general case of non-particle conserving terms as in
a general Bogoliubov transformation.  Eigenstates of quadratic Hamiltonians can be prepared
efficiently on both a quantum and classical computer, making them amenable to initial guesses for
many more challenging problems.

A general quadratic Hamiltonian takes the form
$$H = \sum_{p, q} (M_{pq} - \mu \delta_{pq}) a^\dagger_p a_q + \frac{1}{2} \sum_{p, q} (\Delta_{pq} a^\dagger_p a^\dagger_q + \Delta_{pq}^* a_q a_p) + \text{constant},$$
where $M$ is a Hermitian matrix, $\Delta$ is an antisymmetric matrix,
$\delta_{pq}$ is the Kronecker delta symbol, and $\mu$ is a chemical
potential term which we keep separate from $M$ so that we can use it
to adjust the expectation of the total number of particles.
In OpenFermion, quadratic Hamiltonians are conveniently represented and manipulated
using the QuadraticHamiltonian class, which stores $M$, $\Delta$, $\mu$ and the constant. It is specialized to exploit the properties unique to quadratic Hamiltonians. Like InteractionOperator and InteractionRDM, it inherits from the PolynomialTensor class.

The BCS mean-field model of superconductivity is a quadratic Hamiltonian. The following code constructs an instance of this model as a FermionOperator, converts it to a QuadraticHamiltonian, and then computes its ground energy:

```python
from openfermion.hamiltonians import mean_field_dwave
from openfermion.transforms import get_quadratic_hamiltonian

# Set model.
x_dimension = 2
y_dimension = 2
tunneling = 2.
sc_gap = 1.
periodic = True

# Get FermionOperator.
mean_field_model = mean_field_dwave(
    x_dimension, y_dimension, tunneling, sc_gap, periodic=periodic)

# Convert to QuadraticHamiltonian
quadratic_hamiltonian = get_quadratic_hamiltonian(mean_field_model)

# Compute the ground energy
ground_energy = quadratic_hamiltonian.ground_energy()
print(ground_energy)
```

    -10.0

Any quadratic Hamiltonian may be rewritten in the form
$$H = \sum_p \varepsilon_p b^\dagger_p b_p + \text{constant},$$
where the $b_p$ are new annihilation operators that satisfy the fermionic anticommutation relations, and which are linear combinations of the old creation and annihilation operators. This form of $H$ makes it easy to deduce its eigenvalues; they are sums of subsets of the $\varepsilon_p$, which we call the orbital energies of $H$. The following code computes the orbital energies and the constant:

```python
orbital_energies, constant = quadratic_hamiltonian.orbital_energies()
print(orbital_energies)
print()
print(constant)
```

    [ 1.  1.  1.  1.  4.  4.  4.  4.]

    -10.0

Eigenstates of quadratic hamiltonians are also known as fermionic Gaussian states, and they can be prepared efficiently on a quantum computer. One can use OpenFermion to obtain circuits for preparing these states. The following code obtains the description of a circuit which prepares the ground state (operations that can be performed in parallel are grouped together), along with a description of the starting state to which the circuit should be applied:

```python
from openfermion.utils import gaussian_state_preparation_circuit

circuit_description, start_orbitals = gaussian_state_preparation_circuit(quadratic_hamiltonian)
for parallel_ops in circuit_description:
    print(parallel_ops)
print('')
print(start_orbitals)
```

    ('pht',)
    ((6, 7, 1.5707963267948966, 0.0),)
    ('pht', (5, 6, 1.5707963267948966, 0.0))
    ((4, 5, 1.0471975511965979, -3.1415926535897927), (6, 7, 1.0471975511965981, -3.1415926535897927))
    ('pht', (3, 4, 1.5707963267948966, 0.0), (5, 6, 1.5707963267948966, 0.0))
    ((2, 3, 1.2309594173407741, -8.3266726846886753e-17), (4, 5, 1.2309594173407747, 8.8817841970012523e-16), (6, 7, 1.1071487177940902, 3.1415926535897927))
    ('pht', (1, 2, 1.5707963267948966, 0.0), (3, 4, 1.5707963267948966, 0.0), (5, 6, 1.5707963267948966, 0.0))
    ((0, 1, 1.0471975511965979, -3.1415926535897927), (2, 3, 1.0471975511965976, -3.1415926535897927), (4, 5, 1.3181160716528177, -3.1415926535897927), (6, 7, 1.3181160716528177, -1.3322676295501878e-15))
    ('pht', (1, 2, 1.5707963267948966, 0.0), (3, 4, 1.5707963267948966, 0.0), (5, 6, 1.5707963267948966, 0.0))
    ((2, 3, 0.95531661812450908, 6.0368376963992877e-16), (4, 5, 0.95531661812450885, 0.0), (6, 7, 1.1071487177940904, -1.27675647831893e-15))
    ('pht', (3, 4, 1.5707963267948966, 0.0), (5, 6, 1.5707963267948966, 0.0))
    ((4, 5, 0.78539816339744806, 3.1415926535897922), (6, 7, 0.7853981633974485, -6.106226635438361e-16))
    ((5, 6, 1.5707963267948966, 0.0),)

    []

In the circuit description, each elementary operation is either a tuple of the form $(i, j, \theta, \varphi)$, indicating the operation $\exp[i \varphi a_j^\dagger a_j]\exp[\theta (a_i^\dagger a_j - a_j^\dagger a_i)]$, which is a Givens rotation of modes $i$ and $j$, or the string 'pht', indicating the particle-hole transformation on the last fermionic mode, which is the operator $\mathcal{B}$ such that $\mathcal{B} a_N \mathcal{B}^\dagger = a_N^\dagger$ and leaves the rest of the ladder operators unchanged. Operations that can be performed in parallel are grouped together.

In the special case that a quadratic Hamiltonian conserves particle number ($\Delta = 0$), its eigenstates take the form
$$\lvert \Psi_S \rangle = b^\dagger_{1}\cdots b^\dagger_{N_f}\lvert \text{vac} \rangle,\qquad
b^\dagger_{p} = \sum_{k=1}^N Q_{pq}a^\dagger_q,$$
where $Q$ is an $N_f \times N$ matrix with orthonormal rows. These states are also known as Slater determinants. OpenFermion also provides functionality to obtain circuits for preparing Slater determinants starting with the matrix $Q$ as the input.


[](https://colab.research.google.com/github.com/Rishit-dagli/TFUG-Mysuru-2020/blob/master/TFQuantum_starter.ipynb)

# MNIST classification with QNNs

In this notebook you will build a quantum neural network (QNN) to classify a simplified version of MNIST with
[Cirq](https://cirq.readthedocs.io/en/stable/) and TensorFlow Quantum (TFQ). The tutorial in general shows using a simple quantum neural network on a classical data problem. We here follow the approach used in the [Classification with Quantum Neural Networks on Near Term Processors](https://arxiv.org/pdf/1802.06002.pdf) paper by Edward Farhi and Hartmut Neven.

> Note: This notebook is designed to be run in Google Colab if you want to run it locally or on a Jupyter notebook you
would skip the code cells with the `Colab only` comment.

## Setup

### Install TensorFlow 2.x (Colab only)

```python
# Colab only
pip install -q tensorflow==2.1.0
```

### Install TensorFlow Quantum (Colab only)

```python
# Colab only
pip install -q tensorflow-quantum
```

## Imports

Now import TensorFlow and the module dependencies:

```python
import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import sympy
import numpy as np
import seaborn as sns
import collections
import random

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
```

## Preprocessing the data

In the last tutorial you saw how to make a model to do hybrid quantum-classical binary classification. As this is meant to be an easy to follow and reproducoble tutorial, we will perform binary classification with MNIST. We will perform binary classification for this purpose with just two labels here `1` and `5` (Choosen as they look a lot different unlike something like 1 and 7)

### Load the raw data

Load the MNIST dataset distributed with Keras.

```python
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
```

```python
num_of_samples = []

cols = 5
num_classes = 10

fig, axs = plt.subplots(nrows=num_classes, ncols = cols, figsize=(5, 8))
fig.tight_layout()
for i in range(cols):
    for j in range(num_classes):
        x_selected = x_train[y_train == j]
        axs[j][i].imshow(x_selected[random.randint(0, len(x_selected - 1)), :, :], cmap=plt.get_cmap("gray"))
        axs[j][i].axis("off")
        if i == 2:
            axs[j][i].set_title(str(j))
            num_of_samples.append(len(x_selected))
```

```python
x_train, x_test = x_train[..., np.newaxis]/255.0, x_test[..., np.newaxis]/255.0

print("Number of original training examples:", len(x_train))
print("Number of original test examples:", len(x_test))
```

    Number of original training examples: 60000
    Number of original test examples: 10000

Filter the dataset to keep just the 1s and 5s, remove the other classes. At the same time convert the label, `y`, to boolean: `True` for 1 and `False` for 5.

```python
def filter_1_5(x, y):
    keep = (y == 1) | (y == 5)
    x, y = x[keep], y[keep]
    y = y == 1
    return x,y
```

```python
x_train, y_train = filter_1_5(x_train, y_train)
x_test, y_test = filter_1_5(x_test, y_test)

print("Number of filtered training examples:", len(x_train))
print("Number of filtered test examples:", len(x_test))
```

    Number of filtered training examples: 12163
    Number of filtered test examples: 2027

Display a random image

```python
plt.imshow(x_train[random.randint(0,12000), :, :, 0])
plt.colorbar()
```

### Downscale the images

An image size of 28x28 is much too large for current quantum computers. Resize the image down to 4x4:

```python
x_train_small = tf.image.resize(x_train, (4,4)).numpy()
x_test_small = tf.image.resize(x_test, (4,4)).numpy()
```

### Contradictory examples

"Learning to Distinguish Digits", filter the dataset to remove images that are labeled as belonging to both classes mentioned in [this paper](https://arxiv.org/pdf/1802.06002.pdf).

This is not a standard machine-learning procedure, but is included in the interest of following the paper.

```python
def remove_contradicting(xs, ys):
    mapping = collections.defaultdict(set)
    for x,y in zip(xs,ys):
       mapping[tuple(x.flatten())].add(y)

    new_x = []
    new_y = []
    for x,y in zip(xs, ys):
      labels = mapping[tuple(x.flatten())]
      if len(labels) == 1:
          new_x.append(x)
          new_y.append(list(labels)[0])
      else:
          # Do not include images that match more than one label.
          pass

    num_1 = sum(1 for value in mapping.values() if True in value)
    num_5 = sum(1 for value in mapping.values() if False in value)
    num_both = sum(1 for value in mapping.values() if len(value) == 2)

    print("Number of unique images:", len(mapping.values()))
    print("Number of 1s: ", num_1)
    print("Number of 5s: ", num_5)
    print("Number of contradictory images: ", num_both)
    print()
    print("Initial number of examples: ", len(xs))
    print("Remaining non-contradictory examples: ", len(new_x))

    return np.array(new_x), np.array(new_y)
```

```python
x_train_nocon, y_train_nocon = remove_contradicting(x_train_small, y_train)
```

    Number of unique images: 6588
    Number of 1s:  2176
    Number of 5s:  4626
    Number of contradictory images:  214

    Initial number of examples:  12163
    Remaining non-contradictory examples:  7949

### Data as quantum circuits

We will represent each pixel with a qubit, with the state depending on the value of the pixel. The first step is to convert to a binary encoding.

```python
THRESHOLD = 0.5

x_train_bin = np.array(x_train_nocon > THRESHOLD, dtype=np.float32)
x_test_bin = np.array(x_test_small > THRESHOLD, dtype=np.float32)
```

The qubits at pixel indices with values that exceed a threshold, are rotated through an `X gate`

```python
def convert_to_circuit(image):
    values = np.ndarray.flatten(image)
    qubits = cirq.GridQubit.rect(4, 4)
    circuit = cirq.Circuit()
    for i, value in enumerate(values):
        if value:
            circuit.append(cirq.X(qubits[i]))
    return circuit

x_train_circ = [convert_to_circuit(x) for x in x_train_bin]
x_test_circ = [convert_to_circuit(x) for x in x_test_bin]
```

Lets now visualize the circuit

```python
SVGCircuit(x_train_circ[random.randint(0,7949)])
```

Convert these Cirq circuits to tensors for `tfq`. This is made very easy with `convert_to_tensor()` in `tfq`.

```python
x_train_tfcirc = tfq.convert_to_tensor(x_train_circ)
x_test_tfcirc = tfq.convert_to_tensor(x_test_circ)
```

## Quantum neural Network

[The paper](https://arxiv.org/pdf/1802.06002.pdf) propose using two qubit gates, with the readout qubit always acted upon. This is in a way similar to running a [Unitary RNN](https://arxiv.org/abs/1511.06464) in some ways.

### Build the model circuit

Each layer uses `n` instances of the same gate, with each of the data qubits acting on the readout qubit.

Start with a simple class that will add a layer of these gates to a circuit:

```python
class CircuitLayerBuilder():
    def __init__(self, data_qubits, readout):
        self.data_qubits = data_qubits
        self.readout = readout

    def add_layer(self, circuit, gate, prefix):
        for i, qubit in enumerate(self.data_qubits):
            symbol = sympy.Symbol(prefix + '-' + str(i))
            circuit.append(gate(qubit, self.readout)**symbol)
```

Build an example circuit layer:

```python
demo_builder = CircuitLayerBuilder(data_qubits = cirq.GridQubit.rect(4,1),
                                   readout=cirq.GridQubit(-1,-1))

circuit = cirq.Circuit()
demo_builder.add_layer(circuit, gate = cirq.XX, prefix='xx')
SVGCircuit(circuit)
```

Now build a two-layered model, matching the data-circuit size, and include the preparation and readout operations.

```python
def create_quantum_model():

    # Place data qunits on the grid
    data_qubits = cirq.GridQubit.rect(4, 4)
    readout = cirq.GridQubit(-1, -1)
    circuit = cirq.Circuit()

    # Prepare the readout qubit.
    circuit.append(cirq.X(readout))
    circuit.append(cirq.H(readout))

    builder = CircuitLayerBuilder(
        data_qubits = data_qubits,
        readout=readout)

    # Then add layers (experiment by adding more).
    builder.add_layer(circuit, cirq.XX, "xx1")
    builder.add_layer(circuit, cirq.ZZ, "zz1")

    # Finally, prepare the readout qubit.
    circuit.append(cirq.H(readout))

    return circuit, cirq.Z(readout)
```

```python
model_circuit, model_readout = create_quantum_model()
```

### Wrap the model in Keras layer

Build the Keras model with the quantum components. This model is fed the "quantum data", from x_train_circ, that encodes the classical data. It uses a Parametrized Quantum Circuit layer, `tfq.layers.PQC`, to train the model circuit, on the quantum data.

```python
model = tf.keras.Sequential([
    # The input is the data-circuit, encoded as a tf.string
    tf.keras.layers.Input(shape=(), dtype=tf.string),
    # The PQC layer returns the expected value of the readout gate, range [-1,1].
    tfq.layers.PQC(model_circuit, model_readout),
])
```

Now we will compile the model. Since this is a binary classification problem we here use `BinaryCrossentropy`. We shift the output range to $[0,1]$, and treat it as the probability the model assigns to class 3. This could be used with a standard a tf.losses.BinaryCrossentropy loss.

> Note: Another valid approach would be to use the Hinge Loss. Since the the expected readout is in the range $[-1,1]$, optimizing the hinge loss is also valid. To use the hinge loss here you need to make two small adjustments. First convert the `labels`, `y_train_nocon`, from boolean to $[-1,1]$, as expected by the hinge loss.

```python
model.compile(
    loss= tf.keras.losses.BinaryCrossentropy(),
    optimizer=tf.keras.optimizers.Adam(),)
```

```python
EPOCHS = 3
BATCH_SIZE = 32

NUM_EXAMPLES = 500#len(x_train_tfcirc)
x_train_tfcirc_sub = x_train_tfcirc[:NUM_EXAMPLES]
y_train_hinge_sub = y_train_nocon[:NUM_EXAMPLES]
```

```python
print(model.summary())
```

    Model: "sequential"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    pqc (PQC)                    (None, 1)                 32
    =================================================================
    Total params: 32
    Trainable params: 32
    Non-trainable params: 0
    _________________________________________________________________
    None

```python
qnn_history = model.fit(
      x_train_tfcirc_sub, y_train_hinge_sub,
      batch_size=32,
      epochs=EPOCHS,
      verbose=1,
      validation_data=(x_test_tfcirc, y_test))

qnn_results = model.evaluate(x_test_tfcirc, y_test)
```

    Train on 500 samples, validate on 2027 samples
    Epoch 1/3
    500/500 [==============================] - 140s 281ms/sample - loss: 3.7780 - val_loss: 6.6636
    Epoch 2/3
    500/500 [==============================] - 140s 280ms/sample - loss: 3.6805 - val_loss: 6.6183
    Epoch 3/3
    500/500 [==============================] - 140s 281ms/sample - loss: 3.6135 - val_loss: 6.5709
    2027/2027 [==============================] - 16s 8ms/sample - loss: 6.5709

## References

* [Classification with Quantum Neural Networks on Near Term Processors](https://arxiv.org/pdf/1802.06002.pdf)
* [TensorFlow Quantum Whitepaper](https://arxiv.org/abs/2003.02989)
* [TensorFlow Quantum Docs](https://www.tensorflow.org/quantum/api_docs/python/tfq)
* [Announcing TensorFlow Quantum Dev Summit 2020](https://youtu.be/-o9AhIz1uvo)
* [Programming a Quantum Computer with Cirq](https://youtu.be/16ZfkPRVf2w)
* [Announcing TensorFlow Quantum](https://ai.googleblog.com/2020/03/announcing-tensorflow-quantum-open.html)
* [Unitary RNNs](https://arxiv.org/abs/1511.06464)


# エンコーダを学習する

__目次：__

- 視覚刺激から特徴量を造る
- モデルを初期化してアルゴリズムを走らせる
- 学習機の出来を評価する
- 課題一覧

___

ここでの目的は、これまでに習得してきた技法や知見を融合させ、正常に機能する学習機を造ることである。その応用先は視覚刺激から脳活動へのエンコーディングである。まずはこれまでに用意してきた関数やクラスを`import`しておく。

```python

import scripts.FilterBank as fb
import scripts.AlgoIntro as ai
import scripts.AlgoSparseReg as asr

```

作業の流れは下記の通りになる：

    視覚刺激の読み込み --> ガボールフィルタで特徴量を作って保存 --> スパースな線形回帰

これらのタスクを一つずつこなしていく。やり方を明確に伝えるべく、非常に単純なプロトタイプを作っておく。その後の課題では、このプロトタイプをベースにして、エンコーダとしてちゃんと機能するように改善してもらうことになる。

## 視覚刺激から特徴量を造る

```python
import numpy as np
import math

# Import the vim-2 data from the Python binary format we saved earlier.
# Assumptions: that the data is saved in the vim-2 directory,
# already of 96x96 size, with dtype of np.float32.

PIX_W = 96
PIX_H = 96
dtype=np.float32

# Read the raw training data.
shape=(PIX_W,PIX_H,3,108000) # (downsized px, downsized px, rgd channels, time steps)
# Index for temporal down-sampling. Alternatives: do after the feature-building and aggregate.
idx_ds = np.arange(15//2, 108000+1, 15) # need length 7200.
fname = "data/vim-2/X_tr.dat"
with open(fname, mode="br") as fbin:
    print("Reading...", end=" ")
    raw_tr = np.fromfile(file=fbin,dtype=dtype).reshape(shape)[:,:,:,idx_ds] # temporally down-sampled.
    print("OK.")

# Check a few frames.
#num_frames = raw_tr.shape[3]
#frames_to_play = 5
#for t in range(frames_to_play):
#    plt.imshow(raw_tr[:,:,:,t])
#    plt.show()

# Read the raw testing data.
shape=(PIX_W,PIX_H,3,8100) # (downsized px, downsized px, rgd channels, time steps)
# Index for temporal down-sampling. Alternatives: do after the feature-building and aggregate.
idx_ds = np.arange(15//2, 8100+1, 15) # need length 540.
fname = "data/vim-2/X_te.dat"
with open(fname, mode="br") as fbin:
    print("Reading...", end=" ")
    raw_te = np.fromfile(file=fbin, dtype=dtype).reshape(shape)[:,:,:,idx_ds] # temporally down-sampled.
    print("OK.")

# Check a few frames.
#num_frames = raw_tr.shape[3]
#frames_to_play = 5
#for t in range(frames_to_play):
#    plt.imshow(raw_tr[:,:,:,t])
#    plt.show()

```

    Reading... OK.
    Reading... OK.

```python

# Set up the parameters that specify the first filter bank.
myparas = {"freqs": 32/max(PIX_W,PIX_H),
           "dir": math.pi/2,
           "amp": 0.1,
           "sdev": max(PIX_W,PIX_H)/20,
           "phase": 0}
mygrid_h = 3
mygrid_w = 3

print("Getting features (tr)...", end=" ")
X_tr = fb.G2_getfeatures(ims=raw_tr,
                         fil_paras=myparas,
                         gridshape=(mygrid_h,mygrid_w),
                         mode="reflect", cval=0)
print("OK.")

print("Getting features (te)...", end=" ")
X_te = fb.G2_getfeatures(ims=raw_te,
                         fil_paras=myparas,
                         gridshape=(mygrid_h,mygrid_w),
                         mode="reflect", cval=0)
print("OK.")

```

    Getting features (tr)... OK.
    Getting features (te)... OK.

```python
print("Shape of the produced features:")
print("tr:", X_tr.shape)
print("te:", X_te.shape)
```

    Shape of the produced features:
    tr: (7200, 9)
    te: (540, 9)

上のパラメータだけでは、特徴量が非常に少なく、説明能力が到底足りない。増やす方法はいくらでもあるが、別のdictを一つ用意し、特徴量ベクトルを連結させる事例を次に示しておく。

```python
# Set up the parameters that specify the second filter bank.
myparas = {"freqs": 32/max(PIX_W,PIX_H),
           "dir": 0,
           "amp": 0.1,
           "sdev": max(PIX_W,PIX_H)/20,
           "phase": 0}
mygrid_h = 9
mygrid_w = 9

print("Getting features (tr)...", end=" ")
tmp_X = fb.G2_getfeatures(ims=raw_tr,
                         fil_paras=myparas,
                         gridshape=(mygrid_h,mygrid_w),
                         mode="reflect", cval=0)
print("OK.")
X_tr = np.concatenate((X_tr, tmp_X), axis=1) # concatenate!

print("Getting features (te)...", end=" ")
tmp_X = fb.G2_getfeatures(ims=raw_te,
                         fil_paras=myparas,
                         gridshape=(mygrid_h,mygrid_w),
                         mode="reflect", cval=0)
print("OK.")
X_te = np.concatenate((X_te, tmp_X), axis=1) # concatenate!

print("Shape of the produced features:")
print("tr:", X_tr.shape)
print("te:", X_te.shape)
```

    Getting features (tr)... OK.
    Getting features (te)... OK.
    Shape of the produced features:
    tr: (7200, 90)
    te: (540, 90)

上から明らかなように、連結させることで、データ行列に列を追加しているだけである。行数（つまりサンプル数）は一定である。

これまでと同様に、Nishimoto et al. (2011)にしたがって、特徴量のZスコアを計算する（平均ゼロ、分散1.0の標準化）。これをした上で、「外れ値」と見なすべき点の閾値を定め、必要に応じて切断する。公平な学習課題になるように、訓練・検証を別々に扱う。

```python

X_tr = X_tr - np.mean(X_tr, axis=0)
X_tr = X_tr / np.std(X_tr, axis=0)
print("Mean =", np.mean(X_tr, axis=0), "StdDev =", np.std(X_tr, axis=0))

X_te = X_te - np.mean(X_te, axis=0)
X_te = X_te / np.std(X_te, axis=0)
print("Mean =", np.mean(X_te, axis=0), "StdDev =", np.std(X_te, axis=0))

for j in range(X_tr.shape[1]):
    stdval = np.std(X_tr[:,j])
    X_tr[:,j] = np.clip(X_tr[:,j], a_min=(-stdval), a_max=stdval)
    stdval = np.std(X_te[:,j])
    X_te[:,j] = np.clip(X_te[:,j], a_min=(-stdval), a_max=stdval)

```

    Mean = [  2.58357159e-06  -8.92488515e-07   5.57957435e-07  -4.96258338e-07
      -1.69749057e-06   9.48549996e-07  -2.33410134e-07  -1.02121794e-06
       1.77970367e-06  -1.18050307e-08   8.10722497e-07  -1.43411262e-06
       3.32246231e-07   5.24578809e-07   1.41411192e-06   6.26593817e-08
      -1.32421647e-07  -1.81612037e-07   9.25486290e-07   1.96074453e-07
       1.67777966e-06   5.08626314e-08   1.45783030e-08   4.33185022e-07
       4.19616697e-07   1.14723207e-06   1.72787239e-07   3.96553020e-07
       1.54698887e-06  -9.43500140e-07   3.42387295e-07  -1.26283203e-06
       6.70444649e-07  -7.34544471e-07   1.22170479e-06   1.41645467e-06
      -6.07909442e-07   2.06748655e-06   1.42157910e-06  -1.08564893e-06
       1.37013694e-06  -1.70287166e-07  -2.96019834e-07  -5.49770078e-08
      -4.36430184e-07  -8.49920866e-07   6.96124289e-07  -2.97485116e-07
      -1.17873981e-06  -1.08703972e-07  -3.01607770e-07  -1.50195433e-07
      -3.78158347e-07   1.52405761e-07   9.67168177e-08   8.73878605e-07
      -4.91572756e-08   3.38041133e-07  -1.45385670e-07  -4.44245018e-07
      -8.24688186e-07   4.60081623e-07   2.73320410e-07   1.42743602e-06
      -1.08632776e-06   6.17264050e-07  -1.17643015e-06  -8.23777555e-07
       1.83092220e-06   4.18970984e-07  -7.49511855e-07   1.39700046e-06
       3.56568222e-07  -1.55260170e-06  -1.30837577e-06   7.06145329e-07
       1.28703812e-06   8.49184062e-07   7.94892117e-07   2.63253845e-08
      -7.78573281e-07  -3.04786681e-07   6.02404270e-07   1.40218685e-06
      -1.28369368e-06  -5.52998642e-08  -2.83420093e-07   4.07103869e-07
       1.77071740e-06  -2.07353793e-07] StdDev = [ 0.99999827  0.99999785  1.0000006   1.0000025   0.99999905  0.99999976
      1.00000107  1.00000155  1.00000024  0.99999952  0.99999857  1.00000072
      1.00000131  0.99999893  0.99999881  0.99999958  0.99999976  0.99999708
      1.00000072  1.00000012  0.99999785  0.99999976  0.99999958  1.0000006
      0.9999994   0.99999803  0.9999997   0.99999958  0.99999887  0.99999976
      1.00000048  0.99999952  1.00000107  0.99999976  0.9999997   0.99999911
      0.99999982  0.99999833  0.99999928  1.00000036  0.9999997   1.          1.0000006
      1.00000012  0.99999911  0.99999839  0.99999803  1.00000083  1.00000143
      1.          0.99999869  0.99999952  0.99999988  1.00000083  0.99999928
      1.          1.00000024  1.00000048  1.00000286  0.99999952  0.99999982
      1.00000048  1.00000024  0.99999958  1.00000012  0.99999893  0.99999893
      0.99999958  1.00000179  1.          1.00000036  0.9999997   1.00000024
      0.99999934  0.99999917  1.00000155  0.99999976  0.9999994   1.00000036
      1.00000012  1.00000012  0.99999982  0.99999881  1.00000119  1.00000107
      0.9999994   1.0000006   0.99999899  0.99999923  1.00000072]
    Mean = [  2.64688765e-07  -1.96474573e-07   4.01779445e-08  -4.15245694e-07
      -2.53016196e-07   6.95387525e-09   2.20426813e-07  -3.15256131e-07
       3.20305986e-08  -3.60276971e-07   1.32234007e-07   1.12586553e-08
      -2.55858453e-07  -4.05201206e-07  -1.42830388e-07   1.97357608e-07
      -1.58724959e-07  -9.86788038e-08   6.62273847e-10  -4.85667471e-08
       3.49018308e-07   3.86988688e-07   2.03456040e-07   7.66581934e-07
       2.22303242e-07  -1.28591509e-07  -1.60049510e-07  -1.65347700e-07
       2.77934248e-07   1.52433358e-07  -2.35521131e-08   4.64474709e-07
       1.13028065e-07   1.00003348e-07  -2.53319740e-07   1.90845242e-07
       9.09522697e-08  -3.40629498e-07   1.62698598e-07   1.52709305e-07
      -2.16673925e-07   2.63926466e-07  -4.03987030e-08   9.91203137e-08
      -1.52322983e-07  -7.49362812e-07  -4.57851968e-07  -3.92397261e-08
       6.59100436e-08  -4.70490363e-07  -1.64409485e-07   3.10123511e-07
       1.83202573e-07  -5.87436887e-07   1.88306529e-07  -5.52998642e-08
      -1.99785944e-07   5.36441803e-07  -6.27614838e-07   2.08064364e-07
       4.77940958e-08   1.56075870e-07  -5.82800972e-08   2.58949058e-07
       2.79479565e-07  -2.71201145e-07   1.81904539e-07   9.44843990e-08
       1.35655753e-07   1.62036329e-07   2.83011673e-07   2.40184647e-07
       3.85222627e-08   1.01824602e-08   3.68693350e-07   3.67672357e-07
       3.37649283e-07  -2.20413014e-07   1.21748002e-07   5.80814174e-07
       4.48138621e-07  -1.71087411e-08   2.11265359e-07   2.53154184e-07
       1.85491857e-07  -1.17829551e-07  -2.74733253e-07   5.75626338e-08
       1.35103861e-07  -2.75505926e-07] StdDev = [ 0.99999988  0.99999952  0.99999988  0.99999988  0.99999988  1.
      0.99999988  0.9999994   1.          1.          1.          1.
      0.99999994  1.00000012  1.          1.0000006   0.99999982  0.99999988
      0.99999988  1.00000024  1.00000024  0.9999997   1.00000012  0.9999997   1.
      1.          1.00000036  1.00000024  0.99999988  0.9999997   0.99999964
      1.          1.00000036  1.          0.99999994  1.          1.
      0.99999976  0.99999988  0.9999997   0.9999997   1.00000012  1.00000012
      1.          1.          1.00000036  1.0000006   1.00000048  1.
      1.00000036  1.00000012  0.99999982  1.00000036  1.          0.99999994
      0.9999997   1.          0.99999994  0.99999988  0.99999976  1.00000012
      0.99999934  0.99999988  0.99999982  0.99999964  1.          0.99999994
      0.99999976  1.          0.99999982  1.          1.00000012  1.00000012
      1.00000012  0.99999958  1.00000012  0.9999997   1.00000036  1.          1.
      1.00000036  0.99999988  1.00000012  0.99999994  1.          0.99999976
      1.00000036  1.00000012  1.          0.99999994]

思い通りにこれまでの計算ができているのであれば、整列された全特徴量をディスクに書き込み、付属data infoオブジェクトも併せて用意する。

```python
import support.classes as classes

# Make a data_info file for this data, and save as data/encoder/info.dat.
dinfo = classes.DataInfo()
dinfo.mname = "Encoder"

fname = "data/encoder/X_tr.dat"
dtype = raw_tr.dtype
shape = raw_tr.shape
with open(fname, mode="bw") as fbin:
    X_tr.tofile(fbin)
    print("Saved to file.")
dinfo.X_tr["shape"] = X_tr.shape
dinfo.X_tr["path"] = "data/encoder/X_tr.dat"
dinfo.X_tr["dtype"] = X_tr.dtype

fname = "data/encoder/X_te.dat"
dtype = raw_te.dtype
shape = raw_te.shape
with open(fname, mode="bw") as fbin:
    X_te.tofile(fbin)
    print("Saved to file.")
dinfo.X_te["shape"] = X_te.shape
dinfo.X_te["path"] = "data/encoder/X_te.dat"
dinfo.X_te["dtype"] = X_te.dtype
```

    Saved to file.
    Saved to file.

```python
import support.classes as classes

# Make a data_info file for this data, and save as data/encoder/info.dat.
dinfo = classes.DataInfo()
dinfo.mname = "Encoder"

fname = "data/encoder/X_tr.dat"
dtype = raw_tr.dtype
shape = raw_tr.shape
with open(fname, mode="bw") as fbin:
    X_tr.tofile(fbin)
    print("Saved to file.")
dinfo.X_tr["shape"] = X_tr.shape
dinfo.X_tr["path"] = "data/encoder/X_tr.dat"
dinfo.X_tr["dtype"] = X_tr.dtype

fname = "data/encoder/X_te.dat"
dtype = raw_te.dtype
shape = raw_te.shape
with open(fname, mode="bw") as fbin:
    X_te.tofile(fbin)
    print("Saved to file.")
dinfo.X_te["shape"] = X_te.shape
dinfo.X_te["path"] = "data/encoder/X_te.dat"
dinfo.X_te["dtype"] = X_te.dtype

# Clear the raw data from memory.
del [raw_te, raw_tr]

```

    Saved to file.
    Saved to file.

応答に関しては、特別な処理は何も要らないので、`encoder`ディレクトリへ移すだけで十分である。

```
$ mv data/vim-2/y_tr.dat data/vim-2/y_te.dat ./data/encoder/
$ mv data/vim-2/cleanidx_tr.dat data/vim-2/cleanidx_te.dat ./data/encoder/

```

`dinfo`の残りの空欄を埋めてからディスクに書き込む。

```python

import pickle

fname = "data/encoder/y_tr.dat"
dinfo.y_tr["shape"] = (73728, 7200)
dinfo.y_tr["path"] = "data/encoder/y_tr.dat"
dinfo.y_tr["dtype"] = np.float32

fname = "data/encoder/y_te.dat"
dinfo.y_te["shape"] = (73728, 540)
dinfo.y_te["path"] = "data/encoder/y_te.dat"
dinfo.y_te["dtype"] = np.float32

dinfo.misc = {"voxidx": None} # to be filled in later.

with open("data/encoder/info.dat", mode="bw") as fbin:
    pickle.dump(dinfo, fbin)
    print("Saved to file.")

```

    Saved to file.

## モデルを初期化してアルゴリズムを走らせる

特徴量の準備が整っているなら、Jupyterのカーネルをリセットし、ここから新たな作業を始める。

前回で作った`Algo_LASSO_CD`をここで本領発揮してもらう。前回のテストとまったく同様に、多数の$\lambda$候補を用意し、warm startを生かしながら学習させていく。

パフォーマンス評価指標として、Nishimoto et al. (2011)より：

> *"Prediction accuracy was defined as the correlation between predicted and observed BOLD signals. The averaged accuracy across subjects and voxels in early visual areas (V1, V2, V3, V3A, and V3B) was 0.24, 0.39, and 0.40 for the static, nondirectional, and directional encoding models, respectively."*

教育目的なので我々の学習機があらゆる意味で彼らのものに劣っているのだが、めざす基準としてはこの数値には意義がある。特に相関係数0.24は、2次元のガボールフィルタを使ったときに出た数字なので、より現実的な基準と見てもよかろう。

算出方法は次の通りである。訓練データを使って、学習アルゴリズムを一通り走らせると、$\widehat{w}$に対応する`w_est`が定まる。線形モデルを使っているので、新しい入力$X$から$\widehat{y} = X\widehat{w}$を出して、$\widehat{y} \approx y$で近似してみる。この$X$と$y$が`X_te`と`y_te`に対応する。予測信号と本当の信号の相関が強いほどよいということなので、下記の通りに相関係数を出す：

\begin{align}
\text{corr}\,(\widehat{y},y) = \frac{\text{cov}\,(\widehat{y},y)}{\sqrt{\text{var}\,(\widehat{y})\text{var}\,(y)}},
\end{align}

これは`scipy.stats.pearsonr`で計算できる。また、予測・真の平均的な2乗誤差（root mean squared error; RMSE）を求めることもできる：

\begin{align}
\text{RMSE}\,(\widehat{y},y) = \left( \frac{1}{m} \sum_{i=1}^{m} (\widehat{y}_{i}-y_{i})^2 \right)^{1/2},
\end{align}

これはモデルオブジェクトの`mod.eval`というメソッドで実装している。$m$は検証データのサンプル数（ここでは$m=540$）。

```python

import numpy as np
import math
import pickle
import support.parse_model as mp
import scripts.AlgoSparseReg as asr

# Load the general data info object.
with open("data/encoder/info.dat", mode="br") as fbin:
    dinfo = pickle.load(fbin)

# Load the clean index, extracting indices from binary indicators.
with open("data/encoder/cleanidx_tr.dat", mode="br") as fbin:
    #cleanidx_tr_RAW = np.fromfile(file=fbin, dtype=np.uint32)
    cleanidx_tr = np.flatnonzero(np.fromfile(file=fbin, dtype=np.uint32))
    print("length:", cleanidx_tr.size)
with open("data/encoder/cleanidx_te.dat", mode="br") as fbin:
    #cleanidx_te_RAW = np.fromfile(file=fbin, dtype=np.uint32)
    cleanidx_te = np.flatnonzero(np.fromfile(file=fbin, dtype=np.uint32))
    print("length:", cleanidx_te.size)

# Take the intersection of the clean voxel indices and sort.
cleanidx = np.intersect1d(cleanidx_tr,cleanidx_te)
print(cleanidx_tr)
print(cleanidx_te)
print("cleanidx length:", cleanidx.size)

# Load the data info object. We shall modify its voxel index on the fly.
with open("data/encoder/info.dat", mode="br") as fbin:
    dinfo = pickle.load(fbin)
    print(dinfo)

# Initialize model and weights for an individual voxel.
dinfo.misc["voxidx"] = cleanidx[0]
mod = mp.model(dinfo)
w_init = mod.w_initialize()

lam_min = 1 / mod.n

# TODO: set the START and STOP guys with proper computations using mod.X_tr and mod.y_tr.
todo_lambda = np.flipud(np.logspace(start=math.log10(lam_min),
                                    stop=math.log10(1),
                                    num=150))

# Store performance metric statistics for each lambda setting.
err_overlam = np.zeros(todo_lambda.size, dtype=np.float32)
corr_overlam = np.zeros(todo_lambda.size, dtype=np.float32)
spar_overlam = np.zeros(todo_lambda.size, dtype=np.float32)

```

    length: 59928
    length: 63050
    [ 4355  4356  4357 ..., 73567 73568 73569]
    [  300   301   302 ..., 73725 73726 73727]
    cleanidx length: 59928
    X_tr: {'shape': (7200, 90), 'path': 'data/encoder/X_tr.dat', 'dtype': dtype('float32')}
    X_te: {'shape': (540, 90), 'path': 'data/encoder/X_te.dat', 'dtype': dtype('float32')}
    y_tr: {'shape': (73728, 7200), 'path': 'data/encoder/y_tr.dat', 'dtype': <class 'numpy.float32'>}
    y_te: {'shape': (73728, 540), 'path': 'data/encoder/y_te.dat', 'dtype': <class 'numpy.float32'>}
    mname: Encoder
    misc: {'voxidx': None}

```python
# Iterate over the lambda values once, for a single candidate.
print("Working...")
for i in range(todo_lambda.size):

    # Initialize and execute the algorithm.
    al = asr.Algo_LASSO_CD(w_init=w_init,\
                           t_max=20*w_init.size,\
                           lam_l1=todo_lambda[i],\
                           verbose=False)

    for mystep in al:
        al.update(model=mod)

    # Record performance.
    w_est = al.w
    err_overlam[i] = mod.eval(w_est)[0]
    if np.std(np.dot(mod.X_te, w_est)) > 0:
        corrval = mod.corr_te(w_est)
    else:
        corrval = 0  # watch out for zero-variance case.
    corr_overlam[i] = corrval
    spar_overlam[i] = np.count_nonzero(w_est)

    # Update the initializer to the most current observation.
    w_init = w_est

print("Done.")
```

    Working...
    Done.

学習が終わると、その生の成績を`raw/encoder`というディレクトリに保存しておく。

```python
with open("raw/encoder/lasso01_lam.raw", mode="bw") as fbin:
    pickle.dump(todo_lambda, fbin)
    print("Saved to file.")

with open("raw/encoder/lasso01_err.raw", mode="bw") as fbin:
    pickle.dump(err_overlam, fbin)
    print("Saved to file.")

with open("raw/encoder/lasso01_corr.raw", mode="bw") as fbin:
    pickle.dump(corr_overlam, fbin)
    print("Saved to file.")

with open("raw/encoder/lasso01_spar.raw", mode="bw") as fbin:
    pickle.dump(spar_overlam, fbin)
    print("Saved to file.")
```

    Saved to file.
    Saved to file.
    Saved to file.
    Saved to file.

## 学習機の出来を評価する

学習が終わって、あとは成績を集計したり、可視化したりするだけである。
Jupyterのカーネルをリセットし、ここから新たな作業を始める。

```python
import pickle
import matplotlib
import matplotlib.pyplot as plt

with open("raw/encoder/lasso01_lam.raw", mode="br") as fbin:
    todo_lambda = pickle.load(fbin)

with open("raw/encoder/lasso01_err.raw", mode="br") as fbin:
    err_overlam = pickle.load(fbin)

with open("raw/encoder/lasso01_corr.raw", mode="br") as fbin:
    corr_overlam = pickle.load(fbin)

with open("raw/encoder/lasso01_spar.raw", mode="br") as fbin:
    spar_overlam = pickle.load(fbin)

myfig = plt.figure(figsize=(18,4))
ax_err = myfig.add_subplot(1, 3, 1)
plt.ylabel("Root mean squared error (RMSE)")
plt.xlabel("Lambda values")
ax_err.set_yscale('log')
ax_err.set_xscale('log')
ax_err.plot(todo_lambda, err_overlam)

ax_corr = myfig.add_subplot(1, 3, 2)
plt.ylabel("Correlation coefficient")
plt.xlabel("Lambda values")
ax_corr.set_xscale('log')
ax_corr.plot(todo_lambda, corr_overlam)

ax_spar = myfig.add_subplot(1, 3, 3)
plt.ylabel("Number of non-zero weights")
plt.xlabel("Lambda values")
ax_spar.set_xscale('log')
ax_spar.plot(todo_lambda, spar_overlam)

plt.show()
```

明らかなように、これほど単純なモデルと恣意的な学習アルゴリズムの設定では、まったく予測ができない。

さまざまな要因はあるが、特に注視すべき改善点は下記の通りである。

- 初期値`w_init`の決め方。

- $\lambda$の候補の範囲と密度。

- 学習機の反復回数の制限`t_max`。

- フィルタバンクを定めるあらゆるパラメータが大事だが、特に重要なのは`freqs`、`dir`、`sdev`であろう。

- フィルタバンクの豊富さ。グリッドの解像度の高低、多様な空間周波数、向きなどをまんべんなく含むことが必須。

これらの改善点を念頭において、次の「課題一覧」の練習問題に取り組んでください。

## 課題一覧

0. すべてのボクセル分の学習をする必要はなく、Nishimoto et al.のいう「early visual areas」に焦点を当てること。それは両半球においてV1、V2、V3、V3A、V3BというROIのことを指す。訓練データを使い、これらのすべてのクリーンなボクセルを対象に学習させ、検証データにおいて、訓練データでもっとも良かった$\lambda$のときの学習結果の成績を算出すること。ROIごとに、これらの成績（相関係数等）のボクセルの上での平均と分散を求めること。

0. 特徴量の配列を連結させることで、様々なフィルタからの特徴量を利用することができる。実際、良い説明能力を実現しようと思えば、おそらく不可欠な作業であろう。引用している論文でも、多数の空間周波数と向きの組み合わせが使われている。課題として、互いに異なる特徴量を捉えるフィルタバンクを２つ以上用意し、それぞれを学習用の使うこと。そのROIごとに結果を踏まえて、どの領域でどのような情報が必要かと思われるか。また、脳の部位の「選択性」について何がいえるか。

0. 先の課題を、全被験者分のデータを使って行なうこと。被験者の間、同じモデルとアルゴリズムを使って、パフォーマンスに差異があるか。被験者の上でも平均的な成績を求めて、引用している論文の成績と勝負できるか。

0. 手法の更なる強化を測るため、時間の遅延を利用して特徴量を作ることができる。つまり、前の時点の特徴量ベクトルを、現時点の特徴量ベクトルに連結させるという方法。現時点の特徴量を$x_{i}$とすると、$x_{i-1}$が前の時点に相当するので、連結した結果が$\widetilde{x}_{i} = (x_{i},x_{i-1})$になって、この$\widetilde{x}_{i}$を学習に使うことになる。遅延が$k$時点なら、$\widetilde{x}_{i} = (x_{i},x_{i-1},\ldots,x_{i-k})$となる。数ステップを試して、パフォーマンスがもっとも良い遅延はいくらか。

0. 学習結果の良し悪しがROIによってどの程度変わるか。特段良い・悪い成績が見られたROIがあれば、それはどれか。

0. モデルは大事だが、学習アルゴリズムもきわめて大きな役割を果たす。ベストな設定を探るべく、反復回数などの終了条件や$\lambda$の候補範囲、その他のアルゴリズムの工夫を調べること。どのようなアルゴリズム設定がもっとも良い成績につながったか。独自の改造を行った場合、それも併せて説明すること。

0. （おまけ）これまでは同一被験者を前提として、「汎化能力」を評価してきた。当然ながら、被験者間の汎化能力も注目に値する。つまり、2名の被験者のデータを足し合わせて、大きなデータセットにまとめ、訓練データとして使って学習してから、残り1名の被験者のデータを検証データとする。同じモデルとアルゴリズムでも十分なのか。それとも、新たな工夫が必要だったか。また、このときの汎化能力の解釈が、被験者ごとに学習する場合と比べて、どう違うか。

## 参考文献：

 - Nishimoto, Shinji, et al. "Reconstructing visual experiences from brain activity evoked by natural movies." Current Biology 21.19 (2011): 1641-1646.
 - Description of dataset vim-2 (visual imaging 2), at CRCNS - Collaborative Research in Computational Neuroscience. https://crcns.org/data-sets/vc/vim-2/about-vim-2


# Models and Pricing of Financial Derivativs HW_01

**11510691 程远星**

## Question 1

$\DeclareMathOperator*{\argmin}{argmin}
\DeclareMathOperator*{\argmax}{argmax}
\newcommand{\using}[1]{\stackrel{\mathrm{#1}}{=}}
\newcommand{\ffrac}{\displaystyle \frac}
\newcommand{\space}{\text{ }}
\newcommand{\bspace}{\;\;\;\;}
\newcommand{\QQQ}{\boxed{?\:}}
\newcommand{\void}{\left.\right.}
\newcommand{\CB}[1]{\left\{ #1 \right\}}
\newcommand{\SB}[1]{\left[ #1 \right]}
\newcommand{\P}[1]{\left( #1 \right)}
\newcommand{\dd}{\mathrm{d}}
\newcommand{\Tran}[1]{{#1}^{\mathrm{T}}}
\newcommand{\d}[1]{\displaystyle{#1}}
\newcommand{\EE}[2][\,\!]{\mathbb{E}_{#1}\left[#2\right]}
\newcommand{\Var}[2][\,\!]{\mathrm{Var}_{#1}\left[#2\right]}
\newcommand{\Cov}[2][\,\!]{\mathrm{Cov}_{#1}\left(#2\right)}
\newcommand{\Corr}[2][\,\!]{\mathrm{Corr}_{#1}\left(#2\right)}
\newcommand{\I}[1]{\mathrm{I}\left( #1 \right)}
\newcommand{\N}[1]{\mathrm{N} \left( #1 \right)}
\newcommand{\ow}{\text{otherwise}}\bspace$Selling a call option: As the writer of the call option, I give the holder the right to buy an asset at a specified time $T$ for a specified price $K$. My payoff would be $-\max\P{S_T - K,0}$ for european call options. If I sold an american call option, the holder can exercise at any time before $T$.

$\bspace$Buying a put option: As the holder of the put option, I actually was granted the right to sell an asset at a specified time $T$ for a specified price $K$. My payoff would be $\max\P{K - S_T, 0}$ Or before $T$ if what I bought is an american put option.

## Question 2

$\bspace$We can write their profit function on the stock price $S_t$.

- Stock: $100\P{S_t - 94}$
- Option: $2000\big(\max\P{S_t - 95,0} - 4.7\big)$

$\bspace$They intersect at two points, $\P{0,0}$ and $\P{100,600}$. It's generally acknowledge that it's of higher possibility that the stock price moves less than more, thus I personally think that holding the stocks rather than the options have a better chance to profit.

$\bspace$As for the second question, since we've already acquire their intersection, we can say that when the stock price goes higher than $100$, options will win more.

## Question 3

$\bspace$The trader now in the call holder's position. He has paid $c$ to buy the right that he can use $K$ to buy the underlying asset as time $T$. Also in the put writer's position. He has received $p$ and given the right to someone else selling him the asset at price $K$ at time $T$.

$\bspace$To let the prices equal, by the **Put-call parity**, we have $S_0 = Ke^{-rT}$, the time value of $K$ is equal to the initial price of the asset.

## Question 4

$\bspace$We first write its payoff function on the stock price:

$$\begin{align}
p &= 100\P{S_T - 40} + 100\SB{5 - \max\P{S_T - 50, 0}} + 100\SB{\max\P{30 - S_T,0} - 7}\\
&= \begin{cases}
800, &\text{if } S_T \geq 50 \\
100S_T - 4200, &\text{if } 50 \geq S_T \geq 30 \\
-1200, &\text{if } 30 \geq S_T \geq 0
\end{cases}
\end{align}
$$

After that, the payoff would change to:

$$\begin{align}
p &= 100\P{S_T - 40} + 200\SB{5 - \max\P{S_T - 50, 0}} + 200\SB{\max\P{30 - S_T,0} - 7}\\
&= \begin{cases}
5600 - 100S_T, &\text{if } S_T \geq 50 \\
100S_T - 4400, &\text{if } 50 \geq S_T \geq 30 \\
1600 - 100S_T, &\text{if } 30 \geq S_T \geq 0
\end{cases}
\end{align}
$$

## Question 5

$\bspace$The lower bound of the option can be obtained using the formula

$\bspace\begin{align}
c &\geq K e^{-rT} - S_0 \\
&= 15 \cdot e^{-6\% \times 1/12} - 12 \\
&\approx 2.93
\end{align}$

## Qustion 6

$\bspace$The early exercise of an American put option is to sell the stock to the writer at the Strike price $K$ before the expiration date $T$. Suppose he exercised at time $t$ thus his time value of money is $Ke^{-r\P{T-t}}$. But then he can not sell the stock at $K$ at time $T$ any more.

## Question 7

$\bspace$By the put-call parity, we have: $1 + 20 \times e^{-4\% \times 0.25} = p + 19$ thus $p = 1.80$

## Question 8

$\bspace$Based on the put-call parity, $c + Ke^{-rT} = S_0 + p \Longrightarrow c + 49.75 = 47+2.5$. Thus there's always a chance for arbitraging. He can use the same strategy that is to buy a stock and a put option using the borrowed money, $49.5$ with interest rate $6\%$.

$\bspace$Then he can win $50 - 49.5e^{0.06\times 1/12} \approx 0.25 $ if the stock price goes lower than $50$ or more if the stock price goes higher than $50$.

## Question 9

$\P{1}$

$\bspace P \geq p = c + Ke^{-rT}-S_0 = C + Ke^{-rT} - S_0 = 4 + 30 e^{-8\%\times1/4} -31 \approx 2.41$

$\bspace$And to find something that keeps over an American Put, we can use $K$ cash at the beginning, thus $c + K \geq P+ S_0$ always holds. Therefore, $P \leq c + K - S_0 = C + K - S_0 = 4 + 30 - 31 = 3$

$\P{2}$

$\bspace$If the American Put price is greater than $3$ that is to say that $P \geq C + K - S_0$, then we write an American put option and sell it to somebody, then use the money to buy a American call option, to borrow a stock and sell it to gain $S_0$. Send $K$ cash to the bank. Then when the American put option holder want to exercise, we can instantly use $K = 30$ to buy the stock and return to the stock lender. Up to now, we start from nothing to a American call option and a positive payoff and some interest.

## Question 10

$\P{1}$

$\bspace$If not, then $2c_2 > c_1 + c_3$. So that we have the arbitrage chance. First write two call option with strike price $K_2$ and then use the money gained to buy two call option with strike price $K_1$ and $K_3$. We already have some money left now.

$\bspace$Then we the exercise time comes, we can exercise all three at the same time since $2K_2 = K_1 + K_3$, meaning that we gain money from nothing. Thus $2c_2 \leq c_1 + c_3$.

$\P{2}$

$$p_2 \leq 0.5\P{p_1 + p_3}$$

$\bspace$The proof is obvious, similar to the preceding one.

FinMath/Models and Pricing of Financial Derivatives/HW/HW_01.ipynb