```
!pip install qiskit
```

    Collecting qiskit
      Downloading https://files.pythonhosted.org/packages/38/4e/f275e4b0bc1b8d1c61d8088d693e480fa396c8f3d32e516655f25c510792/qiskit-0.19.6.tar.gz
    Collecting qiskit-terra==0.14.2
      Downloading https://files.pythonhosted.org/packages/64/84/47c2b87551fe81a435eb4224991c1713d538b0605bda4e3c737f4a9f2cc2/qiskit_terra-0.14.2-cp36-cp36m-manylinux2010_x86_64.whl (6.7MB)
         |████████████████████████████████| 6.7MB 4.9MB/s
    Collecting qiskit-aer==0.5.2
      Downloading https://files.pythonhosted.org/packages/45/6f/2d269684891b634cce6ddb5684fd004c7b6bf986cec8544f4b6f495c8b99/qiskit_aer-0.5.2-cp36-cp36m-manylinux2010_x86_64.whl (23.3MB)
         |████████████████████████████████| 23.3MB 30.2MB/s
    Collecting qiskit-ibmq-provider==0.7.2
      Downloading https://files.pythonhosted.org/packages/92/1f/0c6b290064a471a8a9c1e3366367b46d320efdad6b730eadefbd1f3c4eb0/qiskit_ibmq_provider-0.7.2-py3-none-any.whl (155kB)
         |████████████████████████████████| 163kB 52.5MB/s
    Collecting qiskit-ignis==0.3.3
      Downloading https://files.pythonhosted.org/packages/06/5e/273c6f99b4b64dcbab37990627778ed38d8f52e8060db7fc67f38f09fbb2/qiskit_ignis-0.3.3-py3-none-any.whl (179kB)
         |████████████████████████████████| 184kB 53.1MB/s
    Collecting qiskit-aqua==0.7.3
      Downloading https://files.pythonhosted.org/packages/89/18/00c426f06abfa0461a4d794d833c036f8ac5f88062f368d01f739ee1c032/qiskit_aqua-0.7.3-py3-none-any.whl (1.9MB)
         |████████████████████████████████| 1.9MB 46.0MB/s
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.14.2->qiskit) (5.4.8)
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.14.2->qiskit) (0.3.2)
    Collecting ply>=3.10
      Downloading https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl (49kB)
         |████████████████████████████████| 51kB 7.3MB/s
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.14.2->qiskit) (2.8.1)
    Collecting fastjsonschema>=2.10
      Downloading https://files.pythonhosted.org/packages/3a/2b/5e93ebbdc92fe742bfc006f79336c2ddcb436747ef579bc05057c43ad876/fastjsonschema-2.14.4-py3-none-any.whl
    Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.14.2->qiskit) (1.18.5)
    Collecting retworkx>=0.3.2
      Downloading https://files.pythonhosted.org/packages/6c/80/d8b6add5142ed20ad822b2cadb30bb3cfe57e34ff73a0b56cf75e8343c71/retworkx-0.4.0-cp36-cp36m-manylinux2010_x86_64.whl (956kB)
         |████████████████████████████████| 962kB 45.5MB/s
    Collecting python-constraint>=1.4
      Downloading https://files.pythonhosted.org/packages/37/8b/5f1bc2734ca611943e1d6733ee244238679f6410a10cd45ede55a61a8402/python-constraint-1.4.0.tar.bz2
    Requirement already satisfied: networkx>=2.2; python_version > "3.5" in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.14.2->qiskit) (2.4)
    Collecting sympy>=1.3
      Downloading https://files.pythonhosted.org/packages/1e/ed/4b0576282597e87e7cf3be33fa4f738d5974471f9b59a55b3730c3877530/sympy-1.6.1-py3-none-any.whl (5.8MB)
         |████████████████████████████████| 5.8MB 46.5MB/s
    Collecting marshmallow<4,>=3
      Downloading https://files.pythonhosted.org/packages/a6/11/f824483fcfc4164d733c0a242cc01f7e3b153561500a6a69bba217c9f0f6/marshmallow-3.7.1-py2.py3-none-any.whl (45kB)
         |████████████████████████████████| 51kB 7.0MB/s
    Requirement already satisfied: jsonschema>=2.6 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.14.2->qiskit) (2.6.0)
    Requirement already satisfied: scipy>=1.4 in /usr/local/lib/python3.6/dist-packages (from qiskit-terra==0.14.2->qiskit) (1.4.1)
    Collecting marshmallow-polyfield<6,>=5.7
      Downloading https://files.pythonhosted.org/packages/0c/7e/ceb805d05cd03709b8de122b4ac1e736c4eb91d05281ad2a2bcb484accef/marshmallow-polyfield-5.9.tar.gz
    Requirement already satisfied: cython>=0.27.1 in /usr/local/lib/python3.6/dist-packages (from qiskit-aer==0.5.2->qiskit) (0.29.21)
    Collecting pybind11>=2.4
      Downloading https://files.pythonhosted.org/packages/89/e3/d576f6f02bc75bacbc3d42494e8f1d063c95617d86648dba243c2cb3963e/pybind11-2.5.0-py2.py3-none-any.whl (296kB)
         |████████████████████████████████| 296kB 47.4MB/s
    Collecting nest-asyncio!=1.1.0,>=1.0.0
      Downloading https://files.pythonhosted.org/packages/a0/44/f2983c5be9803b08f89380229997e92c4bdd7a4a510ccee565b599d1bdc8/nest_asyncio-1.4.0-py3-none-any.whl
    Requirement already satisfied: requests>=2.19 in /usr/local/lib/python3.6/dist-packages (from qiskit-ibmq-provider==0.7.2->qiskit) (2.23.0)
    Requirement already satisfied: urllib3>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from qiskit-ibmq-provider==0.7.2->qiskit) (1.24.3)
    Collecting websockets<8,>=7
      Downloading https://files.pythonhosted.org/packages/43/71/8bfa882b9c502c36e5c9ef6732969533670d2b039cbf95a82ced8f762b80/websockets-7.0-cp36-cp36m-manylinux1_x86_64.whl (63kB)
         |████████████████████████████████| 71kB 8.2MB/s
    Collecting requests-ntlm>=1.1.0
      Downloading https://files.pythonhosted.org/packages/03/4b/8b9a1afde8072c4d5710d9fa91433d504325821b038e00237dc8d6d833dc/requests_ntlm-1.1.0-py2.py3-none-any.whl
    Requirement already satisfied: scikit-learn>=0.17 in /usr/local/lib/python3.6/dist-packages (from qiskit-ignis==0.3.3->qiskit) (0.22.2.post1)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/local/lib/python3.6/dist-packages (from qiskit-ignis==0.3.3->qiskit) (49.1.0)
    Collecting docplex
      Downloading https://files.pythonhosted.org/packages/9b/76/ebf5b75a25c4357c9a4758cae7fea778dd7f282b83c2f22e150dc7e3c852/docplex-2.15.194.tar.gz (582kB)
         |████████████████████████████████| 583kB 44.4MB/s
    Collecting pyscf; sys_platform != "win32"
      Downloading https://files.pythonhosted.org/packages/0c/69/c6dad63aec5519c287036e82beb36ca46e4a409604676b8787425fa7499c/pyscf-1.7.3-cp36-cp36m-manylinux1_x86_64.whl (26.0MB)
         |████████████████████████████████| 26.0MB 56.5MB/s
    Collecting quandl
      Downloading https://files.pythonhosted.org/packages/1b/29/185269dbd2e2698c8098b35c52ce73a2c52cf76163e709f9f7789d03ebbb/Quandl-3.5.2-py2.py3-none-any.whl
    Requirement already satisfied: h5py in /usr/local/lib/python3.6/dist-packages (from qiskit-aqua==0.7.3->qiskit) (2.10.0)
    Requirement already satisfied: fastdtw in /usr/local/lib/python3.6/dist-packages (from qiskit-aqua==0.7.3->qiskit) (0.3.4)
    Collecting dlx
      Downloading https://files.pythonhosted.org/packages/54/c0/b8fb5bb727e983b6f5251433ef941b48f38c65bb0bd6ec509e9185bcd406/dlx-1.0.4.tar.gz
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.6/dist-packages (from python-dateutil>=2.8.0->qiskit-terra==0.14.2->qiskit) (1.15.0)
    Requirement already satisfied: decorator>=4.3.0 in /usr/local/lib/python3.6/dist-packages (from networkx>=2.2; python_version > "3.5"->qiskit-terra==0.14.2->qiskit) (4.4.2)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.6/dist-packages (from sympy>=1.3->qiskit-terra==0.14.2->qiskit) (1.1.0)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.7.2->qiskit) (2.10)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.7.2->qiskit) (2020.6.20)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.7.2->qiskit) (3.0.4)
    Collecting ntlm-auth>=1.0.2
      Downloading https://files.pythonhosted.org/packages/ff/84/97c550164b54942b0e908c31ef09d9469f3ba4cd7332a671e2125732f63b/ntlm_auth-1.5.0-py2.py3-none-any.whl
    Collecting cryptography>=1.3
      Downloading https://files.pythonhosted.org/packages/ba/91/84a29d6a27fd6dfc21f475704c4d2053d58ed7a4033c2b0ce1b4ca4d03d9/cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl (2.7MB)
         |████████████████████████████████| 2.7MB 45.9MB/s
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.6/dist-packages (from scikit-learn>=0.17->qiskit-ignis==0.3.3->qiskit) (0.16.0)
    Requirement already satisfied: pandas>=0.14 in /usr/local/lib/python3.6/dist-packages (from quandl->qiskit-aqua==0.7.3->qiskit) (1.0.5)
    Collecting inflection>=0.3.1
      Downloading https://files.pythonhosted.org/packages/d2/cd/f04c661d1b5ba6b7d77008e8fedd28a27a683eeeffcd93c6b2dbe54ea983/inflection-0.5.0-py2.py3-none-any.whl
    Requirement already satisfied: more-itertools in /usr/local/lib/python3.6/dist-packages (from quandl->qiskit-aqua==0.7.3->qiskit) (8.4.0)
    Requirement already satisfied: cffi!=1.11.3,>=1.8 in /usr/local/lib/python3.6/dist-packages (from cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.7.2->qiskit) (1.14.0)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.6/dist-packages (from pandas>=0.14->quandl->qiskit-aqua==0.7.3->qiskit) (2018.9)
    Requirement already satisfied: pycparser in /usr/local/lib/python3.6/dist-packages (from cffi!=1.11.3,>=1.8->cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.7.2->qiskit) (2.20)
    Building wheels for collected packages: qiskit, python-constraint, marshmallow-polyfield, docplex, dlx
      Building wheel for qiskit (setup.py) ... done
      Created wheel for qiskit: filename=qiskit-0.19.6-cp36-none-any.whl size=3019 sha256=e67e380999899f4d9b90dfc379fd26c0cdf47b2391090c479b008fa763e82121
      Stored in directory: /root/.cache/pip/wheels/35/45/55/8b82d355edf738ebd6d7f261f17ef1e7ca9ffc7d139c878e10
      Building wheel for python-constraint (setup.py) ... done
      Created wheel for python-constraint: filename=python_constraint-1.4.0-py2.py3-none-any.whl size=24080 sha256=6a416c3693f3e826b6147065bdb6af8e4ed902526edaaabe86ce1ed1795ad257
      Stored in directory: /root/.cache/pip/wheels/34/31/15/7b070b25d0a549d20ce2e9fe6d727471c2c61ef904720fd40c
      Building wheel for marshmallow-polyfield (setup.py) ... done
      Created wheel for marshmallow-polyfield: filename=marshmallow_polyfield-5.9-cp36-none-any.whl size=8537 sha256=6d768ad466b7af084f9540adcc6f0121b83e0c921b81ed06ffb397a1e2243a2e
      Stored in directory: /root/.cache/pip/wheels/69/27/3e/7472f75eab2db111aa387715fe009af15899459517fc1a1906
      Building wheel for docplex (setup.py) ... done
      Created wheel for docplex: filename=docplex-2.15.194-cp36-none-any.whl size=645115 sha256=e20e7c140ed29da25fec9fec33041395474c8660ae316cb96ee098ac5362e8fa
      Stored in directory: /root/.cache/pip/wheels/ce/fa/5d/8f4d5fed1b8edb72e453cb2ac6fb75c6776b49d7174eb70457
      Building wheel for dlx (setup.py) ... done
      Created wheel for dlx: filename=dlx-1.0.4-cp36-none-any.whl size=5712 sha256=8ecf17be63a92c21b867d96ed06c2e07accc21c9ab702998f8e9d6a3378c74ae
      Stored in directory: /root/.cache/pip/wheels/bb/ba/15/fdd0deb104df3254912998150ba9245668db06b00af5912d1a
    Successfully built qiskit python-constraint marshmallow-polyfield docplex dlx
    Installing collected packages: ply, fastjsonschema, retworkx, python-constraint, sympy, marshmallow, marshmallow-polyfield, qiskit-terra, pybind11, qiskit-aer, nest-asyncio, websockets, ntlm-auth, cryptography, requests-ntlm, qiskit-ibmq-provider, qiskit-ignis, docplex, pyscf, inflection, quandl, dlx, qiskit-aqua, qiskit
      Found existing installation: sympy 1.1.1
        Uninstalling sympy-1.1.1:
          Successfully uninstalled sympy-1.1.1
    Successfully installed cryptography-3.0 dlx-1.0.4 docplex-2.15.194 fastjsonschema-2.14.4 inflection-0.5.0 marshmallow-3.7.1 marshmallow-polyfield-5.9 nest-asyncio-1.4.0 ntlm-auth-1.5.0 ply-3.11 pybind11-2.5.0 pyscf-1.7.3 python-constraint-1.4.0 qiskit-0.19.6 qiskit-aer-0.5.2 qiskit-aqua-0.7.3 qiskit-ibmq-provider-0.7.2 qiskit-ignis-0.3.3 qiskit-terra-0.14.2 quandl-3.5.2 requests-ntlm-1.1.0 retworkx-0.4.0 sympy-1.6.1 websockets-7.0

```
import numpy as np
import matplotlib.pyplot as plt

import torch
from torch import nn
import torch.nn.functional as F
from torchvision import datasets, transforms

from qiskit import *
from qiskit.visualization import *
```

```

```


# 참고 문서
https://pytorch.org/tutorials/beginner/blitz/tensor_tutorial.html#sphx-glr-beginner-blitz-tensor-tutorial-py
https://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html

Deep Learning with PyTorch

What is PyTorch?
================

It’s a Python-based scientific computing package targeted at two sets of
audiences:

-  A replacement for NumPy to use the power of GPUs
-  a deep learning research platform that provides maximum flexibility
   and speed

Getting Started
---------------

Tensors
^^^^^^^

Tensors are similar to NumPy’s ndarrays, with the addition being that
Tensors can also be used on a GPU to accelerate computing.

```python
from __future__ import print_function
import torch
```

```python
x = torch.empty(5, 3) # 5행, 3열의 텐서. 값은 모두 garbage
print(x)
```

    tensor([[-1.7494e+15,  3.0658e-41,  3.3631e-44],
            [ 0.0000e+00,         nan,  3.0658e-41],
            [ 1.1578e+27,  1.1362e+30,  7.1547e+22],
            [ 4.5828e+30,  1.2121e+04,  7.1846e+22],
            [ 9.2198e-39,  7.0374e+22, -4.7380e+14]])

```python
print(x.size()) # size, shape
```

    torch.Size([5, 3])

```python
print(x.shape)
```

    torch.Size([5, 3])

Uninitialized vs random

An uninitialized matrix is declared,
    but does not contain definite known
    values before it is used. When an
    uninitialized matrix is created,
    whatever values were in the allocated
    memory at the time will appear as the initial values.

---

Construct a 5x3 matrix, uninitialized:

즉 값이 garbage임. random이 아님.

```python
x = torch.empty(5, 3)
print(x)
```

    tensor([[-1.7494e+15,  3.0658e-41,  3.3631e-44],
            [ 0.0000e+00,         nan,  0.0000e+00],
            [ 4.4721e+21,  1.5956e+25,  4.7399e+16],
            [ 3.7293e-08,  3.9664e+28,  6.9397e+22],
            [ 1.7260e+25,  2.2856e+20,  5.0948e-14]])

```python
print(torch.get_default_dtype())
```

    torch.float32

```python
print(x.dtype)
```

    torch.float32

torch.empty는 torch의 global default dtype 이면서 garbage 값을 가지는 원소가 채워진 텐서를 생성.

즉 디폴트 타입을 갖는 빈 텐서를 생성.

```python
x = torch.FloatTensor(5,3)
print(x)
```

    tensor([[-1.7494e+15,  3.0658e-41,  3.7835e-44],
            [ 0.0000e+00,         nan,  1.1362e+30],
            [ 1.3733e-14,  6.4069e+02,  4.3066e+21],
            [ 1.1824e+22,  4.3066e+21,  6.3828e+28],
            [ 3.8016e-39,  0.0000e+00,  0.0000e+00]])

주의) torch.Tensor와 torch.tensor 사용 예.

torch.Tensor는 Tensor 클래스를 instantiation. 즉 torch.empty와 같은 동작.

torch.tensor는 인자로 받는 데이터 값을 복제. 인자는 리스트, 튜플, ndarray, 스칼라 값 등 다양한 형태. 텐서를 만드는 가장 일반적 형태임.

```python
x = torch.Tensor(5,3)
print(x, x.dtype) # 의미없는 값.
```

    tensor([[-1.7495e+15,  3.0658e-41,  2.3694e-38],
            [ 9.2196e-41, -1.7494e+15,  3.0658e-41],
            [-1.8418e+15,  3.0658e-41, -1.8247e+15],
            [ 3.0658e-41, -1.8247e+15,  3.0658e-41],
            [-1.8247e+15,  3.0658e-41,  5.0948e-14]]) torch.float32

```python
x = torch.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]])
print(x, x.dtype)
```

    tensor([[0.1000, 1.2000],
            [2.2000, 3.1000],
            [4.9000, 5.2000]]) torch.float32

```python
x = torch.tensor([0, 1]) # dtype을 인자의 값에서 유추하여 설정
print(x, x.dtype)
```

    tensor([0, 1]) torch.int64

```python
y = torch.tensor([0.0, 1.0])
print(y, y.dtype)
```

    tensor([0., 1.]) torch.float32

```python
try:
    z = torch.tensor([[0.11111, 0.222222, 0.3333333]], dtype=torch.float64, device=torch.device('cuda:0'))  # creates a torch.cuda.DoubleTensor
    print(z, z.dtype)
except Exception as e:
    print('Error:', e)
```

    tensor([[0.1111, 0.2222, 0.3333]], device='cuda:0', dtype=torch.float64) torch.float64

CPU 환경에선 위 문장이 오류 발생.

GPU 환경으로 런타임 변경 후 실행.

```python
x = torch.tensor(3.14159)  # Create a scalar (zero-dimensional tensor)
print(x, x.dtype)
```

    tensor(3.1416) torch.float32

```python
x = torch.tensor([])  # Create an empty tensor (of size (0,))
print(x, x.dtype)
```

    tensor([]) torch.float32

```python
try:
    x = torch.tensor(3,2) # 인자가 매치하지 않으므로 오류가 나야 함.
    print(x, x.dtype)
except Exception as e:
    print('Error:', e)
```

    Error: tensor() takes 1 positional argument but 2 were given

---

Random

Construct a randomly initialized matrix:

```python
x = torch.rand(5, 3)
print(x)
```

    tensor([[0.8094, 0.0429, 0.2155],
            [0.5168, 0.0996, 0.3452],
            [0.9604, 0.8239, 0.3854],
            [0.1170, 0.4642, 0.7428],
            [0.0502, 0.6915, 0.2721]])

Construct a matrix filled zeros and of dtype long:

```python
x = torch.zeros(5, 3, dtype=torch.long)
print(x)
```

    tensor([[0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0]])

데이터를 인자로 주어 텐서 생성

Construct a tensor directly from data:

```python
x = torch.tensor([5.5, 3])
print(x)
```

    tensor([5.5000, 3.0000])

or create a tensor based on an existing tensor. These methods
will reuse properties of the input tensor, e.g. dtype, unless
new values are provided by user

```python
x  = x.new_ones(5, 3)      # new_* methods take in sizes
print(x, x.dtype)
```

    tensor([[1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.]]) torch.float32

```python
x  = x.new_ones(5, 3, dtype=torch.double)      # new_* methods take in sizes
print(x, x.dtype)
```

    tensor([[1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.]], dtype=torch.float64) torch.float64

```python
x = torch.randn_like(x, dtype=torch.float)    # override dtype!
print(x, x.dtype)                                      # result has the same size
```

    tensor([[ 1.3166, -1.7305,  0.7656],
            [-1.4310,  1.0884, -0.1551],
            [-1.0313,  0.8847, -0.4714],
            [ 0.4703, -0.3116,  0.2227],
            [ 0.7607, -1.3162,  0.3821]]) torch.float32

Get its size: size() or shape

```python
print(x.size())
print(x.shape)
```

    torch.Size([5, 3])
    torch.Size([5, 3])

Note

``torch.Size`` is in fact a tuple, so it supports all tuple operations.

Operations
^^^^^^^^^^
There are multiple syntaxes for operations. In the following
example, we will take a look at the addition operation.

텐서 더하기

Addition: syntax 1

```python
y = torch.rand(5, 3)
print('x\n', x)
print('y\n', y)
print('x+y\n', x + y)
```

    x
     tensor([[ 1.3166, -1.7305,  0.7656],
            [-1.4310,  1.0884, -0.1551],
            [-1.0313,  0.8847, -0.4714],
            [ 0.4703, -0.3116,  0.2227],
            [ 0.7607, -1.3162,  0.3821]])
    y
     tensor([[0.8318, 0.3397, 0.2212],
            [0.5182, 0.7407, 0.7678],
            [0.5206, 0.1094, 0.2323],
            [0.5380, 0.8538, 0.3870],
            [0.4884, 0.7336, 0.0331]])
    x+y
     tensor([[ 2.1483, -1.3908,  0.9868],
            [-0.9128,  1.8290,  0.6127],
            [-0.5107,  0.9942, -0.2391],
            [ 1.0083,  0.5423,  0.6097],
            [ 1.2491, -0.5826,  0.4153]])

Addition: syntax 2

```python
print(torch.add(x, y))
```

    tensor([[ 2.1483, -1.3908,  0.9868],
            [-0.9128,  1.8290,  0.6127],
            [-0.5107,  0.9942, -0.2391],
            [ 1.0083,  0.5423,  0.6097],
            [ 1.2491, -0.5826,  0.4153]])

Addition: providing an output tensor as argument

```python
result = torch.empty(5, 3)
print(result)
```

    tensor([[-1.7494e+15,  3.0658e-41,  9.8682e-01],
            [-9.1280e-01,  1.8290e+00,  6.1274e-01],
            [-5.1067e-01,  9.9417e-01, -2.3908e-01],
            [ 1.0083e+00,  5.4226e-01,  6.0968e-01],
            [ 1.2491e+00, -5.8257e-01,  4.1526e-01]])

```python
torch.add(x, y, out=result)
print(result)
```

    tensor([[ 2.1483, -1.3908,  0.9868],
            [-0.9128,  1.8290,  0.6127],
            [-0.5107,  0.9942, -0.2391],
            [ 1.0083,  0.5423,  0.6097],
            [ 1.2491, -0.5826,  0.4153]])

```python
res = x + y
print(res)
```

    tensor([[ 2.1483, -1.3908,  0.9868],
            [-0.9128,  1.8290,  0.6127],
            [-0.5107,  0.9942, -0.2391],
            [ 1.0083,  0.5423,  0.6097],
            [ 1.2491, -0.5826,  0.4153]])

Addition: in-place

```python
# adds x to y
y.add_(x)
print(y)
```

    tensor([[ 2.1483, -1.3908,  0.9868],
            [-0.9128,  1.8290,  0.6127],
            [-0.5107,  0.9942, -0.2391],
            [ 1.0083,  0.5423,  0.6097],
            [ 1.2491, -0.5826,  0.4153]])

# **_로 끝나는 operation은 in-place**

Note

Any operation that mutates a tensor in-place is post-fixed with an ``_``.
    For example: ``x.copy_(y)``, ``x.t_()``, will change ``x``.

You can use standard NumPy-like indexing with all bells and whistles!

```python

```

# autograd

```python
import torch

a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
```

```python
a
```

    tensor([2., 3.], requires_grad=True)

```python
b
```

    tensor([6., 4.], requires_grad=True)

```python
Q = 3*a**3 - b**2
```

\begin{align}Q=3a^{3}-b^{3}\end{align}

```python
Q
```

    tensor([-12.,  65.], grad_fn=<SubBackward0>)

\begin{align}\frac{\partial Q}{\partial a} = 9a^2\end{align}

\begin{align}\frac{\partial Q}{\partial b} = -2b\end{align}

\begin{align}\frac{dQ}{dQ} = 1\end{align}

```python
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)
```

```python
Q
```

    tensor([-12.,  65.], grad_fn=<SubBackward0>)

```python
a.grad, b.grad
```

    (tensor([36., 81.]), tensor([-12.,  -8.]))

```python
# check if collected gradients are correct
print(9*a**2 == a.grad)
print(-2*b == b.grad)
```

    tensor([True, True])
    tensor([True, True])

```python

```

# 일반적으로는 Numpy 배열과 유사한 신택스를 가진다

# Indexing tensors

## Python slicing 과 유사

https://drive.google.com/file/d/1qYbFGElZiXcB4VgUiolBp0f2YZS1pzGw/view?usp=sharing

```python
some_list = list(range(6))
some_list[:]
```

    [0, 1, 2, 3, 4, 5]

```python
print(some_list[1:4])
print(some_list[1:])
print(some_list[:4])
print(some_list[:-1])
print(some_list[1:4:2]) # 1 to 3 ([1,2,3]), step 2 ([1,3])
```

    [1, 2, 3]
    [1, 2, 3, 4, 5]
    [0, 1, 2, 3]
    [0, 1, 2, 3, 4]
    [1, 3]

```python
points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
print(points)
print(points.shape)

```

    tensor([[4., 1.],
            [5., 3.],
            [2., 1.]])
    torch.Size([3, 2])

```python
print(points[1:])
```

    tensor([[5., 3.],
            [2., 1.]])

```python
print(points[1:, :])
```

    tensor([[5., 3.],
            [2., 1.]])

```python
print(points[1:, 0])
```

    tensor([5., 2.])

```python
print(points[None])
```

    tensor([[[4., 1.],
             [5., 3.],
             [2., 1.]]])

```python
print(points[None].size())
```

    torch.Size([1, 3, 2])

```python

```

# unsqueeze - dimension 확장

```python
print(points.unsqueeze(dim=0))
print(points.unsqueeze(dim=0).size())
```

    tensor([[[4., 1.],
             [5., 3.],
             [2., 1.]]])
    torch.Size([1, 3, 2])

```python
unsq_points = points.unsqueeze(dim=0)
print(unsq_points)
print(unsq_points.size())
```

    tensor([[[4., 1.],
             [5., 3.],
             [2., 1.]]])
    torch.Size([1, 3, 2])

# squeeze - dimension 축소
### 원소 1개짜리 dimension을 제거

```python
sq_points = unsq_points.squeeze()
print(sq_points)
print(sq_points.size())
```

    tensor([[4., 1.],
            [5., 3.],
            [2., 1.]])
    torch.Size([3, 2])

---

Resizing: If you want to resize/reshape tensor, you can use ``torch.view``:

```python
x = torch.randn(4, 4)
y = x.view(16)
z = x.view(-1, 8)  # the size -1 is inferred from other dimensions
print('x.size()=', x.size(), '\ny.size()=', y.size(), '\nz.size()=', z.size())
print(x, '\n', y, '\n', z)
```

    x.size()= torch.Size([4, 4])
    y.size()= torch.Size([16])
    z.size()= torch.Size([2, 8])
    tensor([[-0.0413, -0.1832,  2.0176, -1.2004],
            [-0.2057,  0.6926, -0.7840, -1.0130],
            [ 0.8024, -1.2635, -0.2437, -0.2654],
            [-0.3682, -0.4783,  0.2725,  0.0376]])
     tensor([-0.0413, -0.1832,  2.0176, -1.2004, -0.2057,  0.6926, -0.7840, -1.0130,
             0.8024, -1.2635, -0.2437, -0.2654, -0.3682, -0.4783,  0.2725,  0.0376])
     tensor([[-0.0413, -0.1832,  2.0176, -1.2004, -0.2057,  0.6926, -0.7840, -1.0130],
            [ 0.8024, -1.2635, -0.2437, -0.2654, -0.3682, -0.4783,  0.2725,  0.0376]])

---
# transpose

```python
points = torch.tensor([[3.0, 1.0, 2.0], [4.0, 1.0, 7.0]])
print(points)
```

    tensor([[3., 1., 2.],
            [4., 1., 7.]])

## 서로 바꿀 두 디멘션을 지정

```python
points_tr = points.transpose(0, 1)
```

```python
points_tr
```

    tensor([[3., 4.],
            [1., 1.],
            [2., 7.]])

```python
points
```

    tensor([[3., 1., 2.],
            [4., 1., 7.]])

```python
points_t = points.t() # 2D or less dimension
print(points_t)
```

    tensor([[3., 4.],
            [1., 1.],
            [2., 7.]])

## Multidimensional

```python
some_t = torch.ones(3, 4, 5)
transpose_t = some_t.transpose(0, 2) # 0번째와 2번째 디멘션을 transpose
some_t.shape
```

    torch.Size([3, 4, 5])

```python
some_t
```

    tensor([[[1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.]],

            [[1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.]],

            [[1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.],
             [1., 1., 1., 1., 1.]]])

```python
transpose_t.shape
```

    torch.Size([5, 4, 3])

# Contiguous

가장 오른쪽 차원이 순차적으로 저장된 텐서

```python
some_t.is_contiguous()
```

    True

```python
transpose_t.is_contiguous()
```

    False

## Contiguous로 변환하기

```python
points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
```

```python
points_t = points.t()
```

```python
points_t
```

    tensor([[4., 5., 2.],
            [1., 3., 1.]])

```python
points_t.is_contiguous()
```

    False

```python
points_t_cont = points_t.contiguous()
```

```python
points_t_cont
```

    tensor([[4., 5., 2.],
            [1., 3., 1.]])

```python
points_t_cont.contiguous()
```

    tensor([[4., 5., 2.],
            [1., 3., 1.]])

## tensor view()

같은 storage 에서 보는 관점 (shape)만 다르게 함

```python
t = torch.rand(4, 4)
b = t.view(2, 8)
print(t, t.shape)
print(b, b.shape)
```

    tensor([[0.0195, 0.0300, 0.8736, 0.1998],
            [0.9666, 0.4413, 0.6896, 0.7896],
            [0.2183, 0.4706, 0.4858, 0.0434],
            [0.0599, 0.6883, 0.6222, 0.3606]]) torch.Size([4, 4])
    tensor([[0.0195, 0.0300, 0.8736, 0.1998, 0.9666, 0.4413, 0.6896, 0.7896],
            [0.2183, 0.4706, 0.4858, 0.0434, 0.0599, 0.6883, 0.6222, 0.3606]]) torch.Size([2, 8])

```python
t.is_contiguous()
```

    True

```python
b.is_contiguous()
```

    True

## view 같은 operation은 contiguous tensor에서만 동작

```python
x = torch.tensor([[1,2,3], [4,5,6]])
x
```

    tensor([[1, 2, 3],
            [4, 5, 6]])

```python
x.shape
```

    torch.Size([2, 3])

```python
x_t = x.t()
x_t
```

    tensor([[1, 4],
            [2, 5],
            [3, 6]])

```python
y = x.view(3,2) # transpose가 아님
y
```

    tensor([[1, 2],
            [3, 4],
            [5, 6]])

```python
y.is_contiguous()
```

    True

```python
x_t.is_contiguous()
```

    False

```python
try:
    x_t.view(2, 3) # error
except Exception as e:
    print('Error:', e)
```

    Error: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.

```python
x_t_cont = x_t.contiguous()
x_t_cont
```

    tensor([[1, 4],
            [2, 5],
            [3, 6]])

```python
x_t_cont.view(2, 3)
```

    tensor([[1, 4, 2],
            [5, 3, 6]])

```python
x_t
```

    tensor([[1, 4],
            [2, 5],
            [3, 6]])

```python
x = torch.randn(2,3,4)
x_t = x.transpose(0, 2)
print(x.shape)
print(x_t.shape)

```

    torch.Size([2, 3, 4])
    torch.Size([4, 3, 2])

```python
x
```

    tensor([[[ 0.3393,  1.3988, -0.8127,  0.6651],
             [-0.3669,  1.7391,  0.5760,  0.6174],
             [-0.1857, -0.2307,  1.4841,  0.0666]],

            [[ 1.6562, -0.1020, -0.6314, -0.0970],
             [-0.0202,  0.5436, -1.7030,  1.3969],
             [-0.7071, -0.3885,  0.7472,  1.7509]]])

```python
x.view(2, 12)
```

    tensor([[ 0.3393,  1.3988, -0.8127,  0.6651, -0.3669,  1.7391,  0.5760,  0.6174,
             -0.1857, -0.2307,  1.4841,  0.0666],
            [ 1.6562, -0.1020, -0.6314, -0.0970, -0.0202,  0.5436, -1.7030,  1.3969,
             -0.7071, -0.3885,  0.7472,  1.7509]])

```python
try:
    x_t.view(4,6) # Error
except Exception as e:
    print('Error:', e)
```

    Error: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.

## reshape()

```python
y = x.reshape((2, 12))
```

```python
print(y.size())
y
```

    torch.Size([2, 12])

    tensor([[ 0.3393,  1.3988, -0.8127,  0.6651, -0.3669,  1.7391,  0.5760,  0.6174,
             -0.1857, -0.2307,  1.4841,  0.0666],
            [ 1.6562, -0.1020, -0.6314, -0.0970, -0.0202,  0.5436, -1.7030,  1.3969,
             -0.7071, -0.3885,  0.7472,  1.7509]])

```python
z = x.reshape((6, -1)) # -1 은 알아서 맞추라는 의미
```

```python
z
```

    tensor([[ 0.3393,  1.3988, -0.8127,  0.6651],
            [-0.3669,  1.7391,  0.5760,  0.6174],
            [-0.1857, -0.2307,  1.4841,  0.0666],
            [ 1.6562, -0.1020, -0.6314, -0.0970],
            [-0.0202,  0.5436, -1.7030,  1.3969],
            [-0.7071, -0.3885,  0.7472,  1.7509]])

```python
z = x.reshape((3, -1))
```

```python
z
```

    tensor([[ 0.3393,  1.3988, -0.8127,  0.6651, -0.3669,  1.7391,  0.5760,  0.6174],
            [-0.1857, -0.2307,  1.4841,  0.0666,  1.6562, -0.1020, -0.6314, -0.0970],
            [-0.0202,  0.5436, -1.7030,  1.3969, -0.7071, -0.3885,  0.7472,  1.7509]])

```python
y = x.reshape((-1, 2))
```

```python
y
```

    tensor([[ 0.3393,  1.3988],
            [-0.8127,  0.6651],
            [-0.3669,  1.7391],
            [ 0.5760,  0.6174],
            [-0.1857, -0.2307],
            [ 1.4841,  0.0666],
            [ 1.6562, -0.1020],
            [-0.6314, -0.0970],
            [-0.0202,  0.5436],
            [-1.7030,  1.3969],
            [-0.7071, -0.3885],
            [ 0.7472,  1.7509]])

NumPy Bridge
------------

Converting a Torch Tensor to a NumPy array and vice versa is a breeze.

## The Torch Tensor and NumPy array will share their underlying memory locations (if the Torch Tensor is on CPU), and changing one will change the other.

Converting a Torch Tensor to a NumPy Array
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

```python
a = torch.ones(5)
print(a)
print(type(a))
```

    tensor([1., 1., 1., 1., 1.])
    <class 'torch.Tensor'>

```python
b = a.numpy()
print(b)
print(type(b))
```

    [1. 1. 1. 1. 1.]
    <class 'numpy.ndarray'>

Converting NumPy Array to Torch Tensor
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
See how changing the np array changed the Torch Tensor automatically

```python
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
print(b)
```

    [2. 2. 2. 2. 2.]
    tensor([2., 2., 2., 2., 2.], dtype=torch.float64)

All the Tensors on the CPU except a CharTensor support converting to
NumPy and back.

CUDA Tensors
------------

Tensors can be moved onto any device using the ``.to`` method.

```python
# let us run this cell only if CUDA is available
# We will use ``torch.device`` objects to move tensors in and out of GPU
if torch.cuda.is_available():
    device = torch.device("cuda")          # a CUDA device object
    y = torch.ones_like(x, device=device)  # directly create a tensor on GPU
    x = x.to(device)                       # or just use strings ``.to("cuda")``
    z = x + y
    print(z)
    print(z.dtype, z.device)
    print(z.to("cpu", torch.double))       # ``.to`` can also change dtype together!
    print(z.device)
```

    tensor([[[ 1.3393,  2.3988,  0.1873,  1.6651],
             [ 0.6331,  2.7391,  1.5760,  1.6174],
             [ 0.8143,  0.7693,  2.4841,  1.0666]],

            [[ 2.6562,  0.8980,  0.3686,  0.9030],
             [ 0.9798,  1.5436, -0.7030,  2.3969],
             [ 0.2929,  0.6115,  1.7472,  2.7509]]], device='cuda:0')
    torch.float32 cuda:0
    tensor([[[ 1.3393,  2.3988,  0.1873,  1.6651],
             [ 0.6331,  2.7391,  1.5760,  1.6174],
             [ 0.8143,  0.7693,  2.4841,  1.0666]],

            [[ 2.6562,  0.8980,  0.3686,  0.9030],
             [ 0.9798,  1.5436, -0.7030,  2.3969],
             [ 0.2929,  0.6115,  1.7472,  2.7509]]], dtype=torch.float64)
    cuda:0

```python
print(z.to("cpu").dtype)
```

    torch.float32

```python
print(z.to("cpu").device)
```

    cpu


# PMF for Recommender Systems

This is an implementation for Probabilistic Matrix Factorisation.
Source code: https://github.com/ocontreras309/ML_Notebooks/blob/master/PMF_Recommender_Systems.ipynb

We didn't replace the vaiable names, therefore in the script the variable "is_listened" is called "ratings" and the "media_id" is "movieId"

```
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.metrics import f1_score
from sklearn.metrics import roc_auc_score
from sklearn.metrics import confusion_matrix
from collections import Counter
```

```
from google.colab import drive
drive.mount('/content/drive')
```

    Mounted at /content/drive

```
PATH = "/content/drive/MyDrive/music_recommendation/"
df = pd.read_csv(PATH + 'ratings.csv')
df_join=df.copy()
df.head()
```

      Unnamed: 0
      user_id
      media_id
      is_listened

      0
      0
      9241
      222606
      0.0

      1
      1
      16547
      250467
      1.0

      2
      2
      7665
      305197
      1.0

      3
      3
      1580
      900502
      0.0

      4
      4
      1812
      542335
      1.0

```
# for the sake of reproduceability, we set a random_state.
df_join = df_join.sample(frac =0.01,random_state=123)

```

```
print("Before undersampling: ", Counter(df_join.iloc[:,-1]))
```

    Before undersampling:  Counter({1.0: 9923, 0.0: 4370})

The following cell is for undersampling. To test it: just undo the comment at second last line of code.

```
from imblearn.under_sampling import RandomUnderSampler
from sklearn.model_selection import train_test_split
import pandas as pd
import numpy as np
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

X = df_join.iloc[:,:-1]
y = df_join.iloc[:,-1]

# define undersampling strategy
undersample = RandomUnderSampler(sampling_strategy='majority')

# fit and apply the transform
X_train_under, y_train_under = undersample.fit_resample(X, y)

# summarize class distribution
print("After undersampling: ", Counter(y_train_under))

df_Rate=pd.DataFrame(y_train_under, columns=["rating"])
df_ID=pd.DataFrame(X_train_under, columns=["index","userId","movieId"])
#df_join = pd.merge(df_ID, df_Rate, how='inner', left_index=True, right_index=True)
#df_join.head()
```

    After undersampling:  Counter({0.0: 4370, 1.0: 4370})

Let's now prepare our sparse review matrix. For this purpose, we first need to find out how many users and movies we have in our dataset.

```
def get_ratings_matrix(df, train_size=0.75):
    user_to_row = {}
    movie_to_column = {}
    df_values = df.values
    n_dims = 10
    parameters = {}

    uniq_users = np.unique(df_values[:, 1]) #1 , 3
    uniq_movies = np.unique(df_values[:, 2]) #2, 0

    for i, user_id in enumerate(uniq_users):
        user_to_row[user_id] = i

    for j, movie_id in enumerate(uniq_movies):
        movie_to_column[movie_id] = j

    n_users = len(uniq_users)
    n_movies = len(uniq_movies)

    R = np.zeros((n_users, n_movies))

    df_copy = df.copy()
    train_set = df_copy.sample(frac=train_size, random_state=0)
    test_set = df_copy.drop(train_set.index)

    for index, row in train_set.iterrows():
        i = user_to_row[row.userId]
        j = movie_to_column[row.movieId]
        R[i, j] = row.rating

    return R, train_set, test_set, n_dims, n_users, n_movies, user_to_row, movie_to_column
```

Let's now call this function and retrieve all necessary parameters.

```
R, train_set, test_set, n_dims, n_users, n_movies, user_to_row, movie_to_column = get_ratings_matrix(df_join, 0.8)
parameters = {}
```

We will now initialise our parameters. First the $V$ matrix can be initialised randomly using the following distribution:

\begin{equation}
\large
V \sim \mathcal N\left(0, \frac {1} {\lambda_V}\right)
\end{equation}

Let's remember that:

\begin{equation}
\large
U \in \mathbb R^{D\times N}, \qquad V \in \mathbb R^{D\times M}
\end{equation}

Where $N$ is __n_users__, $M$ is __n_movies__ and $D$ is __n_dims__.

```
def initialize_parameters(lambda_U, lambda_V):
    U = np.zeros((n_dims, n_users), dtype=np.float64)
    V = np.random.normal(0.0, 1.0 / 1.5, (n_dims, n_movies))

    parameters['U'] = U
    parameters['V'] = V
    parameters['lambda_U'] = lambda_U
    parameters['lambda_V'] = lambda_V
```

Let's now implement the function that updates U and V. The elements of both matrices can be updated using the following expressions:

\begin{equation}
\large
U_i=\left[\left(V_jV_j^T\right)_{j\in\Omega_{U_i}}+\lambda_UI\right]^{-1}\left(R_{ij}V_j^T\right)_{j\in\Omega_{U_i}}
\end{equation}

\begin{equation}
\large
V_j=\left[\left(U_iU_i^T\right)_{i\in\Omega_{V_j}}+\lambda_VI\right]^{-1}\left(R_{ij}U_i^T\right)_{i\in\Omega_{V_j}}
\end{equation}

```
def update_parameters():
    U = parameters['U']
    V = parameters['V']
    lambda_U = parameters['lambda_U']
    lambda_V = parameters['lambda_V']

    for i in range(n_users):
        V_j = V[:, R[i, :] > 0]
        U[:, i] = np.dot(np.linalg.inv(np.dot(V_j, V_j.T) + lambda_U * np.identity(n_dims)), np.dot(R[i, R[i, :] > 0], V_j.T))

    for j in range(n_movies):
        U_i = U[:, R[:, j] > 0]
        V[:, j] = np.dot(np.linalg.inv(np.dot(U_i, U_i.T) + lambda_V * np.identity(n_dims)), np.dot(R[R[:, j] > 0, j], U_i.T))

    parameters['U'] = U
    parameters['V'] = V
```

Now let's implement the Log-a posteriori:

\begin{equation}
\large
L=-\frac 1 2 \left(\sum_{i=1}^N\sum_{j=1}^M(R_{ij}-U_i^TV_j)_{(i,j) \in \Omega_{R_{ij}}}^2+\lambda_U\sum_{i=1}^N\|U_i\|_{Fro}^2+\lambda_V\sum_{j=1}^M\|V_j\|_{Fro}^2\right)
\end{equation}

```
def log_a_posteriori():
    lambda_U = parameters['lambda_U']
    lambda_V = parameters['lambda_V']
    U = parameters['U']
    V = parameters['V']

    UV = np.dot(U.T, V)
    R_UV = (R[R > 0] - UV[R > 0])

    return -0.5 * (np.sum(np.dot(R_UV, R_UV.T)) + lambda_U * np.sum(np.dot(U, U.T)) + lambda_V * np.sum(np.dot(V, V.T)))
```

The __predict__ function allows us to predict the rating value given the __user_id__ and the __movie_id__ parameters. The value has been scaled within the range 0-5

```
def predict(user_id, movie_id):
    U = parameters['U']
    V = parameters['V']

    r_ij = U[:, user_to_row[user_id]].T.reshape(1, -1) @ V[:, movie_to_column[movie_id]].reshape(-1, 1)

    max_rating = parameters['max_rating']
    min_rating = parameters['min_rating']

    return 0 if max_rating == min_rating else ((r_ij[0][0] - min_rating) / (max_rating - min_rating))
```

The __evaluate__ function will calculate the __F1-Score__ of the model given a dataset (train or test).

```
def evaluate(dataset,thres):
    ground_truths = []
    predictions = []

    for index, row in dataset.iterrows():
        ground_truths.append(row.loc['rating'])
        predictions.append(predict(row.loc['userId'], row.loc['movieId']))

    predictions_class= [0 if x<thres else 1 for x in predictions ]

    return f1_score(ground_truths, predictions_class)

def conf(dataset, thres):
    ground_truths = []
    predictions = []

    for index, row in dataset.iterrows():
        ground_truths.append(row.loc['rating'])
        predictions.append(predict(row.loc['userId'], row.loc['movieId']))

    predictions_class= [0 if x<thres else 1 for x in predictions ]

    return confusion_matrix(ground_truths, predictions_class)
```

```
def evaluate_threshold(dataset, threshold):
    ground_truths = []
    predictions = []

    for index, row in dataset.iterrows():
        ground_truths.append(row.loc['rating'])
        predictions.append(predict(row.loc['userId'], row.loc['movieId']))

    predictions_class= [0 if x< threshold else 1 for x in predictions]

    return f1_score(ground_truths, predictions_class)
```

```
def evaluate_ROC_AUC(dataset):
    ground_truths = []
    predictions = []

    for index, row in dataset.iterrows():
        ground_truths.append(row.loc['rating'])
        predictions.append(predict(row.loc['userId'], row.loc['movieId']))

    #predictions_class= [0 if x< threshold else 1 for x in predictions]

    return roc_auc_score(ground_truths, predictions)
```

For the purposes of scaling, we need the maximum and minimum rating values.

```
def update_max_min_ratings():
    U = parameters['U']
    V = parameters['V']

    R = U.T @ V
    min_rating = np.min(R)
    max_rating = np.max(R)

    parameters['min_rating'] = min_rating
    parameters['max_rating'] = max_rating
```

The __train__ function implements the code necessary for training the model as well as recording the __RMSE__ values on the training and testing sets.

```
def train(n_epochs):
    initialize_parameters(0.3, 0.3)
    log_aps = []
    rmse_train = []
    rmse_test = []

    update_max_min_ratings()
    rmse_train.append(evaluate(train_set,0.5))
    rmse_test.append(evaluate(test_set,0.5))

    for k in range(n_epochs):
        update_parameters()
        log_ap = log_a_posteriori()
        log_aps.append(log_ap)

        if (k + 1) % 10 == 0:
            update_max_min_ratings()

            rmse_train.append(evaluate(train_set,0.5))
            rmse_test.append(evaluate(test_set,0.5))
            print('Log p a-posteriori at iteration', k + 1, ':', log_ap)

    update_max_min_ratings()

    return log_aps, rmse_train, rmse_test
```

Let's train our model!

```
log_ps, rmse_train, rmse_test = train(150)
```

    Log p a-posteriori at iteration 10 : -551.2989173762485
    Log p a-posteriori at iteration 20 : -543.9371518676159
    Log p a-posteriori at iteration 30 : -539.4962484169208
    Log p a-posteriori at iteration 40 : -536.6329387051519
    Log p a-posteriori at iteration 50 : -534.6697570307991
    Log p a-posteriori at iteration 60 : -533.2627431347269
    Log p a-posteriori at iteration 70 : -532.2202416272264
    Log p a-posteriori at iteration 80 : -531.4285099046075
    Log p a-posteriori at iteration 90 : -530.8159990139205
    Log p a-posteriori at iteration 100 : -530.3353325064182
    Log p a-posteriori at iteration 110 : -529.9538884946684
    Log p a-posteriori at iteration 120 : -529.6485201713873
    Log p a-posteriori at iteration 130 : -529.4023910688776
    Log p a-posteriori at iteration 140 : -529.202988209213
    Log p a-posteriori at iteration 150 : -529.0408356976761

Now let's take a look at some graphs

```
thresholds = np.arange(0.1,0.9,0.1)
Test = [evaluate_threshold(test_set,thres) for thres in thresholds]
Train = [evaluate_threshold(train_set,thres) for thres in thresholds]
```

```
_, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
plt.title('Training results')
ax1.plot(np.arange(len(log_ps)), log_ps, label='MAP')
ax1.legend()
ax1.set_xlabel("Epoches")
ax1.set_ylabel("MAP-Score")

ax2.plot(thresholds,Train, label='F1-Score train')
ax2.plot(thresholds,Test, label='F1-Score test')
ax2.set_xlabel("Thresholds")
ax2.set_ylabel("F1-Score")
ax2.legend()

plt.show()
```

```

```

Let's now evaluate our model on both the training and testing sets.

```
Counter(test_set.iloc[:,-1])
```

    Counter({0.0: 864, 1.0: 884})

```
evaluate(test_set,0.4)
```

    0.6659003831417625

```
conf(test_set,0.4)
```

    array([[  7, 857],
           [ 15, 869]])

Let's check diffrent thresholds

```
thresholds = np.arange(0.1,0.9,0.1)
Test = [evaluate_threshold(test_set,thres) for thres in thresholds]
Train = [evaluate_threshold(train_set,thres) for thres in thresholds]
```

```
plt.plot(thresholds,Test,thresholds,Train)
plt.title('Diffrent thresholds')
plt.legend()
plt.xlabel("thresholds")
plt.ylabel("F1-Score")

plt.show()
```

Now the same for ROC AUC

```
evaluate_ROC_AUC(train_set)
```

    0.988957582428156

```
evaluate_ROC_AUC(test_set)
```

    0.4943157417881681

Let's look at the least preferred items.

```
df_join[df_join['userId'] == user_id].sort_values(by=['rating']).head(10)
```

      index
      userId
      movieId
      rating

      54401
      54401
      45
      602051
      0.0

      662015
      662015
      45
      53288981
      0.0

      54402
      54402
      45
      602048
      0.0

      57611
      57611
      45
      3145851
      0.0

      238
      238
      45
      746387
      0.0

      1371182
      1371182
      45
      3437466
      0.0

      48464
      48464
      45
      127539479
      0.0

      679565
      679565
      45
      122659790
      0.0

      1417710
      1417710
      45
      132144404
      0.0

      273536
      273536
      45
      10542675
      0.0

Let's now look at the most likely preferences of the selected user.

```
predictions = np.zeros((n_movies, 1))
movie_to_column_items = np.array(list(movie_to_column.items()))
df_result = pd.DataFrame(columns=['UserID', 'MovieID',  'Prediction'])

for i, movie in enumerate(movie_to_column_items):
    predictions[i] = predict(user_id, movie[0])

indices = np.argsort(-predictions, axis=0)

for j in range(10):
    movie_id = int(movie_to_column_items[np.where(movie_to_column_items[:, 1] == indices[j])][0][0])
    df_row = pd.DataFrame({
        'UserID': user_id,
        'MovieID': movie_id,
        'Prediction': predictions[indices[j]][0][0]
    }, index=[j])
    df_result = df_result.append(df_row, sort=False)

df_result
```

      UserID
      MovieID
      Prediction

      0
      45
      133661814
      0.695394

      1
      45
      130604714
      0.649982

      2
      45
      132123602
      0.646589

      3
      45
      132123606
      0.645993

      4
      45
      132123632
      0.645602

      5
      45
      132123624
      0.645358

      6
      45
      132123608
      0.645049

      7
      45
      132123610
      0.644881

      8
      45
      132123618
      0.644733

      9
      45
      132123616
      0.644639

Now the predictions for least preferred items.

```
df_result = pd.DataFrame(columns=['UserID', 'MovieID', 'Prediction'])
indices = np.argsort(predictions, axis=0)

for j in range(10):
    movie_id = int(movie_to_column_items[np.where(movie_to_column_items[:, 1] == indices[j])][0][0])
    df_row = pd.DataFrame({
        'UserID': user_id,
        'MovieID': movie_id,
        'Prediction': predictions[indices[j]][0][0]
    }, index=[j])
    df_result = df_result.append(df_row, sort=False)

df_result
```

      UserID
      MovieID
      Prediction

      0
      45
      77096691
      0.341392

      1
      45
      13851418
      0.390785

      2
      45
      7851405
      0.392392

      3
      45
      103897208
      0.392392

      4
      45
      14272842
      0.392952

      5
      45
      130559522
      0.395996

      6
      45
      80844388
      0.396928

      7
      45
      121314030
      0.397807

      8
      45
      63767374
      0.397816

      9
      45
      87544327
      0.401624

I hope you enjoyed this exercise. Something you can also try on your own is to implement gradient descent instead of MAP-estimation like I did here. Let me know about any comments you may have.

```
# Save
np.save('/content/drive/MyDrive/music_recommendation/parameters10.npy', parameters)

```

```
# Load
parameters = np.load('/content/drive/MyDrive/music_recommendation/parameters10.npy',allow_pickle='TRUE').item()
```

```

```


# Neuromatch Academy: Week 3, Day 1, Tutorial 3
# Real Neurons: Synaptic transmission - Models of static and dynamic synapses

## Tutorial Objectives

Synapses connect neurons into neural networks or circuits. Specialized electrical synapses make direct, physical connections between neurons. In this tutorial, however, we will focus on chemical synapses, which are more common in the brain. These synapses do not physically join neurons. Instead, eliciting a spike in the presynaptic cell causes a chemical, or neurotransmitter, to be released into a small space between the neurons called the synaptic cleft. Once the chemical diffuses across that space, it causes changes in the membrane of the postsynaptic cell. In this tutorial, we will model chemical synaptic transmission and study some interesting effects produced by static synapses that do not change and dynamic ones that change their effects based on the spiking history of the presynaptic neurons.

First, we will start by writing code to simulate static synapses.
Next, we will extend the model to include synapses whose synaptic strength is dependent on the recent spike history: synapses can either progressively increase or decrease the size of their effects on the postsynaptic neuron, based on the recent firing rate of its pre-synaptic partners. This feature of synapses in the brain is called **Short-Term Plasticity** and causes synapses to undergo *Facilitation* or *Depression*.

Our goals for this tutorial are to:

- simulate static synapses and study how excitation and inhibition affect the patterns in the neurons' spiking output
- define mean- or fluctuation-driven regimes
- simulate short-term dynamics of synapses (facilitation and depression)
- study how a change in presynaptic firing history affects the synaptic weights (i.e., PSP amplitude)

# Setup

```python
# Imports
import matplotlib.pyplot as plt    # import matplotlib
import numpy as np                 # import numpy
import time                        # import time
import ipywidgets as widgets       # interactive display
from scipy.stats import pearsonr   # import pearson correlation
```

```python
#@title Figure Settings
%matplotlib inline
fig_w, fig_h = (8, 6)
my_fontsize = 18
my_params = {'axes.labelsize': my_fontsize,
          'axes.titlesize': my_fontsize,
          'figure.figsize': [fig_w, fig_h],
          'font.size': my_fontsize,
          'legend.fontsize': my_fontsize-4,
          'lines.markersize': 8.,
          'lines.linewidth': 2.,
          'xtick.labelsize': my_fontsize-2,
          'ytick.labelsize': my_fontsize-2}

plt.rcParams.update(my_params)
my_layout = widgets.Layout()
```

```python
#@title Helper functions
def my_GWN(pars, mu, sig, myseed=False):
  """
  Args:
    pars       : parameter dictionary
    mu         : noise baseline (mean)
    sig        : noise amplitute (standard deviation)
    myseed     : random seed. int or boolean
                  the same seed will give the same random number sequence

  Returns:
    I          : Gaussian White Noise (GWN) input
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # set random seed
  # you can fix the seed of the random number generator so that the results
  # are reliable. However, when you want to generate multiple realizations
  # make sure that you change the seed for each new realization.
  if myseed:
      np.random.seed(seed=myseed)
  else:
      np.random.seed()

  # generate GWN
  # we divide here by 1000 to convert units to seconds.
  I =  mu + sig * np.random.randn(Lt) / np.sqrt(dt/1000.)

  return I

def Poisson_generator(pars, rate, n, myseed=False):
  """
  Generates poisson trains

  Args:
    pars       : parameter dictionary
    rate       : noise amplitute [Hz]
    n          : number of Poisson trains
    myseed     : random seed. int or boolean

  Returns:
    pre_spike_train : spike train matrix, ith row represents whether
                      there is a spike in ith spike train over time
                      (1 if spike, 0 otherwise)
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # set random seed
  if myseed:
      np.random.seed(seed=myseed)
  else:
      np.random.seed()

  # generate uniformly distributed random variables
  u_rand = np.random.rand(n, Lt)

  # generate Poisson train
  poisson_train = 1. * (u_rand<rate*dt/1000.)

  return poisson_train

def my_illus_LIFSYN(pars, v_fmp, v):
  """
  Illustartion of FMP and membrane voltage

  Args:
    pars  : parameters dictionary
    v_fmp : free membrane potential, mV
    v     : membrane voltage, mV

  Returns:
    plot of membrane voltage and FMP, alongside with the spiking threshold
    and the mean FMP (dashed lines)
  """

  plt.figure(figsize=(14., 5))
  plt.plot(pars['range_t'], v_fmp, 'r', lw=1., label = 'Free mem. pot.', zorder=2)
  plt.plot(pars['range_t'], v, 'b', lw=1., label = 'True mem. pot', zorder=1, alpha=0.7)
  plt.axhline(-55, 0, 1, color='k', lw=2., ls='--',label = 'Spike Threshold', zorder=1)
  plt.axhline(np.mean(v_fmp),0, 1, color='r', lw=2., ls='--',label = 'Mean Free Mem. Pot.', zorder=1)
  plt.xlabel('Time (ms)')
  plt.ylabel('V (mV)');
  plt.legend(loc=[1.02, 0.68])
  plt.show()

def my_illus_STD(Poi_or_reg=False, rate=20., U0=0.5, tau_d=100., tau_f=50., plot_out=True):
  """
   Only for one presynaptic train

  Args:
    Poi_or_reg : Poisson or regular input spiking trains
    rate       : Rate of input spikes, Hz
    U0         : synaptic release probability at rest
    tau_d      : synaptic depression time constant of x [ms]
    tau_f      : synaptic facilitation time constantr of u [ms]
    plot_out   : whether ot not to plot, True or False

  Returns:

  """

  T_simu = 10.0 * 1000/(1.0*rate) # 10 spikes in the time window
  pars = default_pars(T=T_simu)
  dt, range_t = pars['dt'], pars['range_t']

  if Poi_or_reg:
    # Poisson type spike train
    pre_spike_train = Poisson_generator(pars, rate, n=1)
    pre_spike_train = pre_spike_train.sum(axis=0)
  else:
    # Regular firing rate
    isi_num = int((1e3/rate)/dt)  # number of dt
    pre_spike_train = np.zeros(len(pars['range_t']))
    pre_spike_train[::isi_num] = 1.

  u, R, g = dynamic_syn(g_bar=1.2, tau_syn=5., U0=U0, tau_d=tau_d, tau_f=tau_f,
                    pre_spike_train=pre_spike_train, dt=pars['dt'])

  if plot_out:
    plt.figure(figsize=(12, 6))

    plt.subplot(2,2,1)
    plt.plot(pars['range_t'], R, 'b', label='R')
    plt.plot(pars['range_t'], u, 'r', label='u')
    plt.legend(loc='best')
    plt.xlim((0,pars['T']))
    plt.ylabel(r'$R$ or $u$ (a.u)')
    plt.subplot(2,2,3)
    spT = pre_spike_train>0
    t_sp = pars['range_t'][spT] #spike times
    plt.plot(t_sp, 0.*np.ones(len(t_sp)), 'k|', ms=18, markeredgewidth=2)
    plt.xlabel('Time (ms)');
    plt.xlim((0,pars['T']))
    plt.yticks([])
    plt.title('Presynaptic spikes')

    plt.subplot(1,2,2)
    plt.plot(pars['range_t'], g, 'r', label='STP synapse')
    plt.xlabel('Time (ms)')
    plt.ylabel('g (nS)')
    plt.xlim((0,pars['T']))

    plt.tight_layout()

  if not Poi_or_reg:
    return g[isi_num], g[9*isi_num]

def plot_volt_trace(pars, v, sp):
  """
  Plot trajetory of membrane potential for a single neuron

  Args:
    pars   : parameter dictionary
    v      : volt trajetory
    sp     : spike train

  Returns:
    figure of the membrane potential trajetory for a single neuron
  """

  V_th = pars['V_th']
  dt, range_t = pars['dt'], pars['range_t']
  if sp.size:
      sp_num = (sp/dt).astype(int)-1
      v[sp_num] += 10

  plt.plot(pars['range_t'], v, 'b')
  plt.axhline(V_th, 0, 1, color='k', ls='--', lw=1.)
  plt.xlabel('Time (ms)')
  plt.ylabel('V (mV)')

```

In the `Helper Function`:

- Gaussian white noise generator: `my_GWN(pars, mu, sig, myseed=False)`
- Poissonian spike train generator: `Poisson_generator(pars, rate, n, myseed=False)`

```python
#@title Default value function: `default_pars( **kwargs)`
def default_pars( **kwargs):
    pars = {}

    ### typical neuron parameters###
    pars['V_th']    = -55. # spike threshold [mV]
    pars['V_reset'] = -75. #reset potential [mV]
    pars['tau_m']   = 10. # membrane time constant [ms]
    pars['g_L']     = 10. #leak conductance [nS]
    pars['V_init']  = -65. # initial potential [mV]
    pars['E_L']     = -75. #leak reversal potential [mV]
    pars['tref']    = 2. # refractory time (ms)

    ### simulation parameters ###
    pars['T'] = 400. # Total duration of simulation [ms]
    pars['dt'] = .1  # Simulation time step [ms]

    ### external parameters if any ###
    for k in kwargs:
        pars[k] = kwargs[k]

    pars['range_t'] = np.arange(0, pars['T'], pars['dt']) # Vector of discretized time points [ms]

    return pars
```

## Static synapses

```python
#@title Video: Static and dynamic synapses
from IPython.display import YouTubeVideo
video = YouTubeVideo(id='S82kACA5P0M', width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=S82kACA5P0M

## Simulate synaptic conductance dynamics

The GWN input used in Tutorials 1 and 2 is quite unphysiological input compared to the inputs that a real neuron receives. Synaptic input _in vivo_ consists of a mixture of **excitatory** neurotransmitters, which depolarizes the cell and drives it towards spike threshold, and **inhibitory** neurotransmitters that hyperpolarize it, driving it away from spike threshold. Each of these chemicals cause specific ion channels on the postsynaptic neuron to open, resulting in a change in that neuron's conductance and therefore, the flow of current into or out of the cell.

This process can be modelled by assuming that the presynaptic neuron's spiking activity produces transient changes in the postsynaptic neuron's conductance ($g_{\rm syn}(t)$). Typically, the conductance transient is modelled as an exponential function.

Such conductance transients can be generated using a simple ordinary differential equation (ODE):

\begin{eqnarray}
\frac{dg_{\rm syn}(t)}{dt} &=& \bar{g}_{\rm syn} \sum_k \delta(t-t_k) -g_{\rm syn}(t)/\tau_{\rm syn}
\end{eqnarray}

where $\bar{g}_{\rm syn}$ is the maximum conductance elicited by each incoming spike -- this is often referred to as synaptic weight--and $\tau_{\rm syn}$ is the synaptic time constant. Note that the summation runs over all spikes received by the neuron at time $t_k$.

Ohm's law allows us to convert conductance changes into current as:

\begin{align}
I_{\rm syn}(t) = -g_{\rm syn}(t)(V(t)-E_{\rm syn})  \\
\end{align}

The reversal potential $E_{\rm syn}$ describes the direction of current flow and the excitatory or inhibitory nature of the synapse.

**Thus, incoming spikes are filtered by an exponential-shaped kernel, effectively low-pass filtering the input. In other words, synaptic input is not white noise but it is in fact colored noise, where the color (spectrum) of the noise is determined by the synaptic time constants of both excitatory and inhibitory synapses.**

In a neuronal network, the total synaptic input current $I_{\rm syn}$ is the sum of both excitatory and inhibitory inputs. Assuming the total excitatory and inhibitory conductances received at time $t$ are $g_E(t)$ and $g_I(t)$, and their corresponding reversal potentials are $E_E$ and $E_I$, respectively, then the total synaptic current can be described as:

\begin{align}
I_{\rm syn}(V(t),t) = -g_E(t) (V-E_E) - g_I(t) (V-E_I). \\
\end{align}

Accordingly, the membrane potential dynamics of the LIF neuron under synaptic current drive become:

\begin{align}
\tau_m\frac{dV(t)}{dt} = -(V(t)-E_L) - \frac{g_E(t)}{g_L} (V(t)-E_E) - \frac{g_I(t)}{g_L} (V(t)-E_I) + \frac{I_{\rm inj}}{g_L}.\quad (2)
\end{align}

$I_{\rm inj}$ is an external current injected in the neuron which is under experimental control; it can be GWN, DC or anything else.

We will use Eq. (2) to simulate the conductance-based LIF neuron model below.

In the previous tutorials, we saw how the output of a single neuron (spike count/rate and spike time irregularity), changed when we stimulated the neuron with DC and GWN, respectively. Now, we are in a position to study how the neuron behaves when it is bombarded with both excitatory and inhibitory spikes trains -- as happens *in vivo*.

What kind of input a neuron is receiving? When we do not know, we chose the simplest option. The simplest model of input spikes is given when every input spike arrives independently of other spikes, i.e., we assume that the input is Poissonian.

### Simulate LIF neuron with conductance-based synapses

We are now ready to simulate a LIF neuron with conductance-based synaptic inputs! The following code defines the LIF neuron with synaptic input modelled as conductance transients.

```python
#@title Conductance-based LIF: `run_LIF_cond`

def run_LIF_cond(pars, I_inj, pre_spike_train_ex, pre_spike_train_in):
    """conductance-based LIF dynamics

    Rgs:
      pars               : parameter dictionary
      I_inj              : injected current [pA]. The injected current here can be a value or an array
      pre_spike_train_ex : spike train input from presynaptic excitatory neuron
      pre_spike_train_in : spike train input from presynaptic inhibitory neuron

    Returns:
      rec_spikes         : spike times
      rec_v              : mebrane potential
      gE                 : postsynaptic excitatory conductance
      gI                 : postsynaptic inhibitory conductance
    """

    # Retrieve parameters
    V_th, V_reset = pars['V_th'], pars['V_reset']
    tau_m, g_L = pars['tau_m'], pars['g_L']
    V_init, E_L = pars['V_init'], pars['E_L']
    gE_bar, gI_bar = pars['gE_bar'], pars['gI_bar']
    VE, VI = pars['VE'], pars['VI']
    tau_syn_E, tau_syn_I = pars['tau_syn_E'], pars['tau_syn_I']
    tref = pars['tref']
    dt, range_t = pars['dt'], pars['range_t']
    Lt = range_t.size

    # Initialize
    tr = 0.
    v = np.zeros(Lt)
    v[0] = V_init
    gE = np.zeros(Lt)
    gI = np.zeros(Lt)
    I = I_inj * np.ones(Lt) #ensure I has length Lt

    if pre_spike_train_ex.max() == 0:
      pre_spike_train_ex_total = np.zeros(Lt)
    else:
      pre_spike_train_ex_total = pre_spike_train_ex.sum(axis=0) * np.ones(Lt)

    if pre_spike_train_in.max() == 0:
      pre_spike_train_in_total = np.zeros(Lt)
    else:
      pre_spike_train_in_total = pre_spike_train_in.sum(axis=0) * np.ones(Lt)

    # simulation
    rec_spikes = [] # recording spike times
    for it in range(Lt-1):
      if tr > 0:
            v[it] = V_reset
            tr = tr-1
      elif v[it] >= V_th:         #reset voltage and record spike event
            rec_spikes.append(it)
            v[it] = V_reset
            tr = tref/dt
      #update the synaptic conductance
      gE[it+1] = gE[it] - (dt/tau_syn_E)*gE[it] + gE_bar*pre_spike_train_ex_total[it+1]
      gI[it+1] = gI[it] - (dt/tau_syn_I)*gI[it] + gI_bar*pre_spike_train_in_total[it+1]

      #calculate the increment of the membrane potential
      dv = (-(v[it]-E_L) - (gE[it+1]/g_L)*(v[it]-VE) - \
              (gI[it+1]/g_L)*(v[it]-VI) + I[it]/g_L) * (dt/tau_m)

      #update membrane potential
      v[it+1] = v[it] + dv

    rec_spikes = np.array(rec_spikes) * dt

    return v, rec_spikes, gE, gI
```

#### Exercise 1: Measure the Mean free membrane potential

Let's simulate the conductance-based LIF neuron with presynaptic spike trains genetated by a `Poisson_generator` with rate 10 Hz for both excitatory and inhibitory inputs. Here, we choose 80 exictatory presynaptic spike trains and 20 inhibitory ones.

Previously, we've already learned that CV$_{\rm ISI}$ can describe the irregularity of the output spike pattern. Now, we will introduce the **Free Membrane Potential (FMP)**, which is the membrane potential of the neuron when its spike threshold is removed. Although this is completely artificial,  calculating this quantity allows us to get an idea of how strong the input is. We are mostly interested in knowing the  mean and standard deviation (std.) of the FMP.

```python
# Exercise 1
# To complete the exercise, uncomment the code and fill the missing parts (...)

pars = default_pars(T=1000.)

# Add parameters
pars['gE_bar']    = 2.4  # [nS]
pars['VE']        = 0.   # [mV] excitatory reversal potential
pars['tau_syn_E'] = 2.4  # [ms]
pars['gI_bar']    = 3.   # [nS]
pars['VI']        = -80. # [mV] inhibitory reversal potential
pars['tau_syn_I'] = 5.   # [ms]

# generate presynaptic spike trains
pre_spike_train_ex = Poisson_generator(pars, rate=10, n=80)
pre_spike_train_in = Poisson_generator(pars, rate=10, n=20)

# simulate conductance-based LIF model
v, rec_spikes, gE, gI = run_LIF_cond(pars, 0, pre_spike_train_ex, pre_spike_train_in)
dt, range_t = pars['dt'], pars['range_t']
if rec_spikes.size:
    sp_num = (rec_spikes/dt).astype(int)-1
    v[sp_num] = 10   # draw nicer spikes

####################################################################
## TODO for students: Try to meansure the free membrane potential  #
####################################################################
# In order to measure the free membrane potential, first,
# you should prevent the firing of the LIF neuron
# How to prevent a LIF neuron from firing? Increse the threshold pars['V_th'].

# pars['V_th'] = ...
# v_fmp, _, _, _ = ...

# comment this out when you've filled
#raise NotImplementedError("Student excercise: measure the FMP")

# uncomment when you have filled the excercise
# my_illus_LIFSYN(pars, v, v_fmp)
```

```python
# to_remove solutions
pars = default_pars(T=1000.)
#Add parameters
pars['gE_bar']    = 2.4  # [nS]
pars['VE']        = 0.   # [mV] excitatory reversal potential
pars['tau_syn_E'] = 2.   # [ms]
pars['gI_bar']    = 2.4  # [nS]
pars['VI']        = -80. # [mV] inhibitory reversal potential
pars['tau_syn_I'] = 5.   # [ms]

#generate presynaptic spike trains
pre_spike_train_ex = Poisson_generator(pars, rate=10, n=80)
pre_spike_train_in = Poisson_generator(pars, rate=10, n=20)

# simulate conductance-based LIF model
v, rec_spikes, gE, gI = run_LIF_cond(pars, 0, pre_spike_train_ex, pre_spike_train_in)
dt, range_t = pars['dt'], pars['range_t']
if rec_spikes.size:
    sp_num = (rec_spikes/dt).astype(int)-1
    v[sp_num] = 10   # draw nicer spikes

#measure FMP
pars['V_th'] = 1e3
v_fmp, _, _, _ = run_LIF_cond(pars, 0, pre_spike_train_ex, pre_spike_train_in)

# plotting
with plt.xkcd():
  my_illus_LIFSYN(pars, v_fmp, v)
```

### Parameter Exploration: different ratio of excitation and inhibition

In the following, we can investigate how varying the ratio of excitatory to inhibitory inputs changes the firing rate and the spike time regularity (see the output text).

To change the both the excitatory and inhibitory inputs we will vary their firing rates. *However, if you wish, you can vary the strength and/or the number of these connections as well.*

```python
#@title Conductance-based LIF Explorer with different E/I input

my_layout.width = '450px'
@widgets.interact(
    inh_rate = widgets.FloatSlider(20., min=10., max=60., step=5., layout=my_layout),
    exc_rate = widgets.FloatSlider(10., min=2., max=20., step=2., layout=my_layout)
)

def EI_isi_regularity(exc_rate, inh_rate):

  pars = default_pars(T=1000.)
  #Add parameters
  pars['gE_bar']    = 3.   # [nS]
  pars['VE']        = 0.   # [mV] excitatory reversal potential
  pars['tau_syn_E'] = 2.   # [ms]
  pars['gI_bar']    = 3.   # [nS]
  pars['VI']        = -80. # [mV] inhibitory reversal potential
  pars['tau_syn_I'] = 5.   # [ms]

  pre_spike_train_ex = Poisson_generator(pars, rate=exc_rate, n=80)
  pre_spike_train_in = Poisson_generator(pars, rate=inh_rate, n=20) # 4:1

  # Lets first simulate a neuron with identical input but with no spike threshold
  # by setting the threshold to a very high value
  # so that we can look at the free membrane potential
  pars['V_th'] = 1e3
  v_fmp, rec_spikes, gE, gI = run_LIF_cond(pars, 0, pre_spike_train_ex, pre_spike_train_in)

  # Now simulate a LIP with a regular spike threshold
  pars['V_th'] = -55.
  v, rec_spikes, gE, gI = run_LIF_cond(pars, 0, pre_spike_train_ex, pre_spike_train_in)
  dt, range_t = pars['dt'], pars['range_t']
  if rec_spikes.size:
      sp_num = (rec_spikes/dt).astype(int)-1
      v[sp_num] = 10 #draw nicer spikes

  spike_rate = 1e3*len(rec_spikes)/pars['T']
  #print('Spike rate = %.3f (sp/s)' % spike_rate)

  cv_isi = 0.
  if len(rec_spikes)>3:
    isi = np.diff(rec_spikes)
    cv_isi = np.std(isi)/np.mean(isi)
    #print('CV ISI = %.3f' % (cv_isi))

  #print('Mean of Free Mem Pot = %.3f' % (np.mean(v_fmp)))
  #print('STD of Free Mem Pot = %.3f' % (np.std(v_fmp)))

  print('\n')
  plt.figure(figsize=(15., 10))
  plt.subplot(2,1,1)
  plt.text(500, -35, 'Spike rate = %.3f (sp/s), Mean of Free Mem Pot = %.3f' % (spike_rate, np.mean(v_fmp)),
           fontsize=16, fontweight='bold', horizontalalignment='center', verticalalignment='bottom')
  plt.text(500, -38.5, 'CV ISI  = %.3f, STD of Free Mem Pot  = %.3f' % (cv_isi, np.std(v_fmp)),
            fontsize=16, fontweight='bold', horizontalalignment='center', verticalalignment='bottom')

  plt.plot(pars['range_t'], v_fmp, 'r', lw=1., label = 'Free mem. pot.', zorder=2)
  plt.plot(pars['range_t'], v, 'b', lw=1., label = 'mem. pot with spk thr', zorder=1, alpha=0.7)
  plt.axhline(pars['V_th'], 0, 1, color='k', lw=1., ls='--',label = 'Spike Threshold', zorder=1)
  plt.axhline(np.mean(v_fmp),0, 1, color='r', lw=1., ls='--',label = 'Mean Free Mem. Pot.', zorder=1)
  plt.ylim(-76, -39)
  plt.xlabel('Time (ms)')
  plt.ylabel('V (mV)')
  plt.legend(loc=[1.02, 0.68])

  plt.subplot(2,2,3)
  plt.plot(pars['range_t'][::3], gE[::3], 'r', lw=1.)
  plt.xlabel('Time (ms)')
  plt.ylabel(r'$g_E$ (nS)')

  plt.subplot(2,2,4)
  plt.plot(pars['range_t'][::3], gI[::3], 'b', lw=1.)
  plt.xlabel('Time (ms)')
  plt.ylabel(r'$g_I$ (nS)')

  plt.tight_layout()

#_ = widgets.interact(EI_isi_regularity, inh_rate = (10., 60., 5.), exc_rate = (5.,20.,2))
```

### Mean-driven and Fluctuation-driven regimes

If we look at the figure above, we note that when the mean FMP is above spike threshold, the fluctuations in the FMP are rather small and the neuron spikes in a fairly regular fashion. This regime when the mean FMP is above the spike threshold is called **mean-driven regime**.

When the mean FMP is below spike threshold, the fluctuations in the FMP are large and the neuron's spikes are driven by these fluctuations. As a consequence, the neuron spikes in more  Poisson-like fashion. This regime when the mean FMP is below the spike threshold and spikes are driven by the fluctuations is called **fluctuation-driven regime**.

#### Think!

- How much can you increase the spike pattern variability? Under what condition(s) the neuron may also respond with Poisson-type spikes? Note that we injected Poisson-type spikes.
- Link to the balance of excitation and inhibition. One of the definition of excitation and inhibition balance is that mean free membrane potential remains constant as excitatory and inhibitory input rates are increased. What do you think happens to the neuron firing rate as we change excitatory and inhibitory rates while keeping the neuron in balance?  See [Kuhn, Aertsen, and Rotter (2004)](https://www.jneurosci.org/content/jneuro/24/10/2345.full.pdf) for much more on this.

## Short-term synaptic plasticity

Short-term plasticity (STP) is a phenomenon in which synaptic efficacy changes over time in a way that reflects the history of presynaptic activity. Two types of STP, with opposite effects on synaptic efficacy, have been experimentally observed. They are known as Short-Term Depression (STD) and Short-Term Facilitation (STF).

The mathematical model of STP is based on the concept of a limited pool of synaptic resources available for transmission ($R$), such as, for example, the overall amount of synaptic vescicles at the presynaptic terminals. The amount of presynaptic resource changes in a dynamic fashion depending on the recent history of spikes.

Following a presynaptic spike, (i) the fraction $u$ (release probability) of the available pool to be utilized increases due to spike-induced calcium influx to the presynaptic terminal, after which (ii) $u$ is consumed to increase the post-synaptic conductance. Between spikes, $u$ decays back to zero with time constant $\tau_f$ and $R$ recovers to 1 with time constant $\tau_d$. In summary, the dynamics of excitatory (subscript E) STP is given by:

\begin{eqnarray}
&& \frac{du_E}{dt} &= -\frac{u_E}{\tau_f} + U_0(1-u_E^-)\delta(t-t_{\rm sp}) \\[.5mm]
&& \frac{dR_E}{dt} &= \frac{1-R_E}{\tau_d} - u_E^+ R_E^- \delta(t-t_{\rm sp}) \qquad (6)\\[.5mm]
&& \frac{dg_E(t)}{dt} &= -\frac{g_E}{\tau_E} + \bar{g}_E u_E^+ R_E^- \sum_k \delta(t-t_{\rm k}),
\end{eqnarray}
where $U_0$ is a constan determining the increment of $u$ produced by a spike. $u_E^-$ and $R_E^-$ denote the corresponding values just before the spike arrives, whereas $u_E^+$ refers to the moment right after the spike. $\bar{g}_E$ denotes the maximum excitatory conductane, and the calculation of $g_E(t)$ is calculated for all spiketimes $k$. Similarly, one can obtain the dynamics of inhibitory STP.

The interplay between the dynamics of $u$ and $x$ determines whether the joint effect of $ux$ is dominated by depression or facilitation. In the parameter regime of $\tau_d \gg \tau_f$  and large $U_0$, an initial spike incurs a large drop in $x$ that takes a long time to recover; therefore the synapse is STD-dominated. In the regime of $\tau_d \ll \tau_f$ and small $U_0$, the synaptic efficacy is increased gradually by spikes, and consequently, the synapse is STF-dominated. This phenomenological model successfully reproduces the kinetic dynamics of depressed and facilitated synapses observed in many cortical areas.

### Exercise 2: Compute $du$, $dR$ and $dg$

As we learned in several previous tutorials, the Euler numerical integration method involves the calculation of each derivative at step $n$:
\begin{eqnarray}
du_E &=& -\frac{u_E[t]}{\tau_f} dt + U_0(1-u_E[t])\cdot \text{sp_or_not[t+dt]} \\
dR_E &=& \frac{1-R_E[t]}{\tau_d} dt - u_E[t+dt]R_E[t]\cdot \text{sp_or_not[t+dt]} \\
dg_E &=& -\frac{g_E[t]}{\tau_{E}} dt + \bar{g}_Eu_E[t+dt]R_E[t]\cdot \text{sp_or_not[t+dt]}
\end{eqnarray}
where $\text{sp_or_not}=1$ if there's a spike in the time window $dt$, and $\text{sp_or_not}=0$  otherwise. In addition, note that any spike train generated by our `Poisson_generator` is binary. Then, the values are updated:

\begin{aligned}
\begin{eqnarray}
  u_E[t+dt] = u_E[t] + du_E\\
  R_E[t+dt] = R_E[t] + dR_E\\
  g_E[t+dt] = g_E[t] + dg_E
\end{eqnarray}
\end{aligned}

Similarly, one can obtain the dynamics of inhibitory conductance.

```python
# Exercise 2

def dynamic_syn(g_bar, tau_syn, U0, tau_d, tau_f, pre_spike_train, dt):
  """
  Short-term synaptic plasticity

  Args:
    g_bar           : synaptic conductance strength
    tau_syn         : synaptic time constant [ms]
    U0              : synaptic release probability at rest
    tau_d           : synaptic depression time constant of x [ms]
    tau_f           : synaptic facilitation time constantr of u [ms]
    pre_spike_train : total spike train (number) input
                      from presynaptic neuron
    dt              : time step [ms]

  Returns:
    u               : usage of releasable neurotransmitter
    R               : fraction of synaptic neurotransmitter resources available
    g               : postsynaptic conductance
  """

  Lt = len(pre_spike_train)
  # Initialize
  u = np.zeros(Lt)
  R = np.zeros(Lt)
  R[0] = 1.
  g = np.zeros(Lt)

  # simulation
  for it in range(Lt-1):

    #########################################################################
    ## TODO for students: compute du, dx and dg, remove NotImplementedError #
    #########################################################################
    # Note pre_spike_train[i] is binary, which is sp_or_not in the $i$th timebin

    # comment this out when you've finished filling this out.
    raise NotImplementedError("Student excercise: compute the STP dynamics")
    # du = ...
    u[it+1] = u[it] + du

    # dR = ...
    R[it+1] = R[it] + dR

    # dg = ...
    g[it+1] = g[it] + dg

  return u, R, g

# Uncomment this line after completing the dynamic_syn function
# _ = my_illus_STD(Poi_or_reg=False, rate=20., U0=0.5, tau_d=100., tau_f=50.) # Poi_or_reg=False:regular spike train
```

```python
# to_remove solution
def dynamic_syn(g_bar, tau_syn, U0, tau_d, tau_f, pre_spike_train, dt):
  """
  Short-term synaptic plasticity

  Args:
    g_bar           : synaptic conductance strength
    tau_syn         : synaptic time constant [ms]
    U0              : synaptic release probability at rest
    tau_d           : synaptic depression time constant of x [ms]
    tau_f           : synaptic facilitation time constantr of u [ms]
    pre_spike_train : total spike train (number) input
                      from presynaptic neuron
    dt              : time step [ms]

  Returns:
    u               : usage of releasable neurotransmitter
    R               : fraction of synaptic neurotransmitter resources available
    g               : postsynaptic conductance
  """

  Lt = len(pre_spike_train)
  # Initialize
  u = np.zeros(Lt)
  R = np.zeros(Lt)
  R[0] = 1.
  g = np.zeros(Lt)

  # simulation
  for it in range(Lt-1):

    du = - (dt/tau_f)*u[it] + U0 * (1.0-u[it]) * pre_spike_train[it+1]
    u[it+1] = u[it] + du

    dR = (dt/tau_d)*(1.0-R[it]) - u[it+1]*R[it] * pre_spike_train[it+1]
    R[it+1] = R[it] + dR

    dg = - (dt/tau_syn)*g[it] + g_bar*R[it]*u[it+1] * pre_spike_train[it+1]
    g[it+1] = g[it] + dg

  return u, R, g

with plt.xkcd():
  _ = my_illus_STD(Poi_or_reg=False, rate=20., U0=0.5, tau_d=100., tau_f=50.)
```

### Parameter Exploration
Below, an interactive demo that shows how Short-term synaptic depression (STD) changes for different firing rate of the presynaptic spike train and how the amplitude synaptic conductance $g$ changes with every incoming spike until it reaches its stationary state.

Does it matter if the neuron fires in a Poisson manner, rather than regularly?

**Note:** `Poi_or_Reg=1`: for *Posisson type* and `Poi_or_Reg=0`: for *regular* presynaptic spikes.

```python
#@title STD Explorer with input rate
def my_STD_diff_rate(rate, Poi_or_Reg):
      _ = my_illus_STD(Poi_or_reg=Poi_or_Reg, rate=rate)

_ = widgets.interact(my_STD_diff_rate, rate = (10., 100.1, 5.), Poi_or_Reg = (0, 1, 1))
```

### Synaptic depression and presynaptic firing rate
Once, I asked an experimentalist about the experimental values of the PSP amplitude produced by a connection between two neocortical excitatory neurons. She asked: "At what frequency?" I was confused, but you will understand her question now that you know that PSP amplitude depends on the spike history, and therefore on the spike rate of presynaptic neuron.

Hre, we will study how the ratio of the synaptic conductance corresponding to the first and last spike change as a function of the presynaptic firing rate.

For computational effciency, we assume that the presynaptic spikes are regular. This assumption means that we do not have to run multiple trials.

```python
#@title STD conductance ratio with different input rate
# Regular firing rate
input_rate = np.arange(5., 40.1, 5.)
g_1 = np.zeros(len(input_rate)) # record the the PSP at 1st spike
g_2 = np.zeros(len(input_rate)) # record the the PSP at 10th spike

for ii in range(len(input_rate)):
   g_1[ii], g_2[ii] = my_illus_STD(Poi_or_reg=False, rate=input_rate[ii], \
                                   plot_out=False, U0=0.5, tau_d=100., tau_f=50)

plt.figure(figsize=(11, 4.5))

plt.subplot(1,2,1)
plt.plot(input_rate,g_1,'m-o',label = '1st Spike')
plt.plot(input_rate,g_2,'c-o',label = '10th Spike')
plt.xlabel('Rate [Hz]')
plt.ylabel('Conductance [nS]')
plt.legend()

plt.subplot(1,2,2)
plt.plot(input_rate,g_2/g_1,'b-o',)
plt.xlabel('Rate [Hz]')
plt.ylabel(r'Conductance ratio $g_{10}/g_{1}$')
plt.tight_layout()

```

### Parameter Exploration of short-term synaptic facilitation (STF)

Below, we see an illustration of a short-term facilitation example. Take note of the change in the synaptic variables: `U_0`, `tau_d`, and  `tau_f`.

- for STD, `U0=0.5, tau_d=100., tau_f=50.`

- for STP, `U0=0.2, tau_d=100., tau_f=750.`

Also notice how the input rate affects the STF.

```python
#@title STF Explorer with input rate
def my_STD_diff_rate(rate, Poi_or_Reg):
     _ = my_illus_STD(Poi_or_reg=Poi_or_Reg, rate=rate, U0=0.2, tau_d=100., tau_f=750.)

_ = widgets.interact(my_STD_diff_rate, rate = (4., 40.1, 2.), Poi_or_Reg = (0, 1, 1))
```

### Synaptic facilitation and presynaptic firing rate

Here, we will study how the ratio of the synaptic conductance corresponding to the $1^{st}$ and $10^{th}$ spike changes as a function of the presynaptic rate.

```python
#@title STF conductance ratio with different input rate
# Regular firing rate
input_rate = np.arange(2., 40.1, 2.)
g_1 = np.zeros(len(input_rate)) # record the the PSP at 1st spike
g_2 = np.zeros(len(input_rate)) # record the the PSP at 10th spike

for ii in range(len(input_rate)):
   g_1[ii], g_2[ii] = my_illus_STD(Poi_or_reg=False, rate=input_rate[ii], \
                                   plot_out=False, U0=0.2, tau_d=100., tau_f=750.)

plt.figure(figsize=(11, 4.5))

plt.subplot(1,2,1)
plt.plot(input_rate,g_1,'m-o',label = '1st Spike')
plt.plot(input_rate,g_2,'c-o',label = '10th Spike')
plt.xlabel('Rate [Hz]')
plt.ylabel('Conductance [nS]')
plt.legend()

plt.subplot(1,2,2)
plt.plot(input_rate,g_2/g_1,'b-o',)
plt.xlabel('Rate [Hz]')
plt.ylabel(r'Conductance ratio $g_{10}/g_{1}$')
plt.tight_layout()
```

### Think!

Why does the ratio of the first and second-to-last spike conductance changes in a non-monotonic fashion for synapses with STF, even though it decreases monotonically for synapses with STD?

### Conductance-based LIF with STP
Previously, we looked only at how presynaptic firing rate affects the presynaptic resource availability and thereby the synaptic conductance. It is straightforward to imagine that, while the synaptic conductances are changing, the output of the postsynaptic neuron will change as well.

So, let's put the STP on synapses impinging on an LIF neuron and see what happens.

```python
#@title `run_LIF_cond_STP`
def run_LIF_cond_STP(pars, I_inj, pre_spike_train_ex, pre_spike_train_in):
    '''
    conductance-based LIF dynamics

    Expects:
    pars               : parameter dictionary
    I_inj              : injected current [pA]. The injected current here can be a value or an array
    pre_spike_train_ex : spike train input from presynaptic excitatory neuron (binary)
    pre_spike_train_in : spike train input from presynaptic inhibitory neuron (binary)

    Returns:
    rec_spikes : spike times
    rec_v      : mebrane potential
    gE         : postsynaptic excitatory conductance
    gI         : postsynaptic inhibitory conductance
    '''

    # Retrieve parameters
    V_th, V_reset = pars['V_th'], pars['V_reset']
    tau_m, g_L = pars['tau_m'], pars['g_L']
    V_init, V_L = pars['V_init'], pars['E_L']
    gE_bar, gI_bar = pars['gE_bar'], pars['gI_bar']
    U0E, tau_dE, tau_fE = pars['U0_E'], pars['tau_d_E'], pars['tau_f_E']
    U0I, tau_dI, tau_fI = pars['U0_I'], pars['tau_d_I'], pars['tau_f_I']
    VE, VI = pars['VE'], pars['VI']
    tau_syn_E, tau_syn_I = pars['tau_syn_E'], pars['tau_syn_I']
    tref = pars['tref']

    dt, range_t = pars['dt'], pars['range_t']
    Lt = range_t.size

    nE = pre_spike_train_ex.shape[0]
    nI = pre_spike_train_in.shape[0]

    # compute conductance Excitatory synapses
    uE = np.zeros((nE, Lt))
    RE = np.zeros((nE, Lt))
    gE = np.zeros((nE, Lt))
    for ie in range(nE):
        u, R, g = dynamic_syn(gE_bar, tau_syn_E, U0E, tau_dE, tau_fE,
                              pre_spike_train_ex[ie, :], dt)
        uE[ie, :], RE[ie, :], gE[ie, :] = u, R, g
    gE_total = gE.sum(axis=0)

    # compute conductance Inhibitory synapses
    uI = np.zeros((nI, Lt))
    RI = np.zeros((nI, Lt))
    gI = np.zeros((nI, Lt))
    for ii in range(nI):
        u, R, g = dynamic_syn(gI_bar, tau_syn_I, U0I, tau_dI, tau_fI,
                              pre_spike_train_in[ii, :], dt)
        uI[ii, :], RI[ii, :], gI[ii, :] = u, R, g
    gI_total = gI.sum(axis=0)

    # Initialize
    v = np.zeros(Lt)
    v[0] = V_init
    I = I_inj * np.ones(Lt) #ensure I has length Lt

    # simulation
    rec_spikes = [] # recording spike times
    tr = 0.
    for it in range(Lt-1):
      if tr >0:
            v[it] = V_reset
            tr = tr-1
      elif v[it] >= V_th:         #reset voltage and record spike event
            rec_spikes.append(it)
            v[it] = V_reset
            tr = tref/dt

      #calculate the increment of the membrane potential
      dv = (-(v[it]-V_L) - (gE_total[it+1]/g_L)*(v[it]-VE) - \
              (gI_total[it+1]/g_L)*(v[it]-VI) + I[it]/g_L) * (dt/tau_m)

     #update membrane potential
      v[it+1] = v[it] + dv

    rec_spikes = np.array(rec_spikes) * dt

    return v, rec_spikes, uE, RE, gE, RI, RI, gI
```

### Simulation of a postsynaptic neuron with STP synapses driven by Poisson type spike trains

Here we have assumed that both excitatory and inhibitory synapses show short-term depression. Change the nature of synapses and study how spike pattern variability changes.
In the interactive demo, `tau_d = 500*tau_ratio (ms)` and  `tau_f = 300*tau_ratio (ms)`.

You should compare the output of this neuron with what you observed in the previous turorial when synapses were assumed to be static.

_Note: it will take slighly longer time to run each case_

```python
#@title LIF_STP Explorer
def LIF_STP(tau_ratio):
  pars = default_pars(T=1000)
  pars['gE_bar']    = 1.2*4 #[nS]
  pars['VE']        = 0. #[mV]
  pars['tau_syn_E'] = 5. #[ms]
  pars['gI_bar']    = 1.6*4 #[nS]
  pars['VI']        = -80. #[ms]
  pars['tau_syn_I'] = 10. #[ms]

  # here we assume that both Exc and Inh synapses have synaptic depression
  pars['U0_E']        = 0.45
  pars['tau_d_E']     = 500. * tau_ratio #[ms]
  pars['tau_f_E']     = 300. * tau_ratio #[ms]

  pars['U0_I']        = 0.45
  pars['tau_d_I']     = 500. * tau_ratio #[ms]
  pars['tau_f_I']     = 300. * tau_ratio #[ms]

  pre_spike_train_ex = Poisson_generator(pars, rate=15, n=80)
  pre_spike_train_in = Poisson_generator(pars, rate=15, n=20) # 4:1

  v, rec_spikes, uE, RE, gE, uI, RI, gI \
  = run_LIF_cond_STP(pars, 0, pre_spike_train_ex, pre_spike_train_in)

  t_plot_range = pars['range_t']>200
  plt.figure(figsize=(11., 7))
  plt.subplot(2,1,1)
  plot_volt_trace(pars, v, rec_spikes)

  plt.subplot(2,2,3)
  plt.plot(pars['range_t'][t_plot_range], gE.sum(axis=0)[t_plot_range], 'r')
  plt.xlabel('Time (ms)')
  plt.ylabel(r'$g_E$ (nS)')

  plt.subplot(2,2,4)
  plt.plot(pars['range_t'][t_plot_range], gI.sum(axis=0)[t_plot_range], 'b')
  plt.xlabel('Time (ms)')
  plt.ylabel(r'$g_I$ (nS)')

  plt.tight_layout()

_ = widgets.interact(LIF_STP, tau_ratio = (0.2, 1.1, 0.2))
```

### Optional

Vary the parameters of the above simulation and observe spiking pattern of the postsynaptic neuron.
Will the neuron show higher irregularity if the synapses have STP? If yes, what should be the nature of STP on static and dynamic synapses, respectively?

**Task**: Calculate the CV$_{\rm ISI}$ for different `tau_ratio` after simulating the LIF neuron with STP (Hint:`run_LIF_cond_STP` help you understand the irregularity).

### Functional impliations of short-term dynamics of synapses
As you have seen above, if the firing rate is stationary, the synaptic conductance quickly reaches a fixed point. On the other hand, if the firing rate transiently changes, synaptic conductance will vary -- even if the change is as short as a single inter-spike-interval. Such small changes can be observed in a single neuron when input spikes are regular and periodic. If the input spikes are Poissonian then one may have to perform an average over several neurons.

_Come up with other functions that short-term dynamics of synapses can be used to implement and implement them._

## Summary

Congratulations! You have just finished the Tutorial 3 (one to go!). Here, we saw how to model conductance-based synapses and also how to incorporate their short-term dynamics.

We covered the:

- static synapses and how excitation and inhibition affect the neuronal output
- mean- or fluctuation-driven regimes
- short-term dynamics of synapses (both facilitation and depression)

Finally, we incorporated all the aforementioned tools to study how a change in presynaptic firing history affects the synaptic weights!

Next, you will learn about another form of synaptic plasticity based on both pre- and postsynaptic spike times.