# Рекомендательные системы

Идея рекомендательных систем на основе ближайшего соседа – использовать предпочтения некоторой группы людей, похожих на данного пользователя, чтобы рекомендовать ему что-либо.

У такой модели много применений:

   * рекомендация товаров интернет-магазинов
   * рекомендация музыки или фильмов
   * рекомендация людей/контента в социальных сетях

Пример рекомендательной системы Amazon.com

Рекомендация фильмов от IMDb.

Естественный способ получить рекомендацию о чем-либо - спросить мнение об этом у друзей или любых людей, которым нравится то же, что и вам. Эту идею можно использовать и для машины: для каждого человека алгоритм просматривает большую группу людей и ищет в ней подгруппу с похожим на данного человека вкусом. Далее создается список того, что еще нравится этим людям, а затем человеку рекомендуются предложения из этого списка. Такой алгоритм называется алгоритмом **коллаборативной фильтрации**.

**Отключаем предупреждения**

Библиотека **warnings** отвечает за то, какие предупреждения (warnings) о работе будут выводиться пользователю.
FutureWarning - предупреждения о том, как изменится работа библиотек в будущих версиях. Такие предупреждения мы будем игнорировать.
Чтобы включить режим игнорирования, мы отбираем все предупреждения из категории FutureWarning и выбираем для них действия 'ignore'.
Это делается вызовом функции simplefilter c задание двух атрибутов: действия action и категории предупреждений category.

**Code 0:**

```python
import warnings
warnings.filterwarnings("ignore")
```

Для красивого вывода на экран сложных объектов будем использовать **pprint**

```python
import pprint
pp = pprint.PrettyPrinter(indent=4)
```

### Подготовка данных

Чтобы хранить сразу много предпочтений для каждого человека, удобнее всего воспользоваться **вложенным словарем**.

Самый простой способ создать словарь в python - использовать фигурные скобки {}. Данные в словаре хранятся в формате ключ – значение, разделенные двоеточием:

```python
dict = {'key': 'value'}
```

Мы будем работать с *вложенным словарем* кинокритиков и выставленных ими оценок для небольшого набора данных о фильмах. Построим его следующим образом:

1) для каждого кинокритика создаем словарь оценок фильмов в формате
   ```python
    scores_dict = {
        'film_1': 'score_1',
        'film_2': 'score_2',
        ...
    }
   ```

2) создаем словарь кинокритиков, где в качестве значений будет соответствующий ему словарь оценок
   ```python
     critics = {
         'name_1': 'scores_dict_1',
         'name_2': 'scores_dict_2',
         ...
     }
   ```

**Code 1:**

```python
# Словарь кинокритиков и выставленных ими оценок
critics={'Lisa Rose': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.5,
 'Just My Luck': 3.0, 'Superman Returns': 3.5, 'You, Me and Dupree': 2.5,
 'The Night Listener': 3.0},
'Gene Seymour': {'Lady in the Water': 3.0, 'Snakes on a Plane': 3.5,
 'Just My Luck': 1.5, 'Superman Returns': 5.0, 'The Night Listener': 3.0,
 'You, Me and Dupree': 3.5},
'Michael Phillips': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.0,
 'Superman Returns': 3.5, 'The Night Listener': 4.0},
'Claudia Puig': {'Snakes on a Plane': 3.5, 'Just My Luck': 3.0,
 'The Night Listener': 4.5, 'Superman Returns': 4.0,
 'You, Me and Dupree': 2.5},
'Mick LaSalle': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0,
 'Just My Luck': 2.0, 'Superman Returns': 3.0, 'The Night Listener': 3.0,
 'You, Me and Dupree': 2.0},
'Jack Matthews': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0,
 'The Night Listener': 3.0, 'Superman Returns': 5.0, 'You, Me and Dupree': 3.5},
'Toby': {'Snakes on a Plane':4.5,'You, Me and Dupree':1.0,'Superman Returns':4.0}}
```

```python
pp.pprint(critics)
```

    {   'Claudia Puig': {   'Just My Luck': 3.0,
                            'Snakes on a Plane': 3.5,
                            'Superman Returns': 4.0,
                            'The Night Listener': 4.5,
                            'You, Me and Dupree': 2.5},
        'Gene Seymour': {   'Just My Luck': 1.5,
                            'Lady in the Water': 3.0,
                            'Snakes on a Plane': 3.5,
                            'Superman Returns': 5.0,
                            'The Night Listener': 3.0,
                            'You, Me and Dupree': 3.5},
        'Jack Matthews': {   'Lady in the Water': 3.0,
                             'Snakes on a Plane': 4.0,
                             'Superman Returns': 5.0,
                             'The Night Listener': 3.0,
                             'You, Me and Dupree': 3.5},
        'Lisa Rose': {   'Just My Luck': 3.0,
                         'Lady in the Water': 2.5,
                         'Snakes on a Plane': 3.5,
                         'Superman Returns': 3.5,
                         'The Night Listener': 3.0,
                         'You, Me and Dupree': 2.5},
        'Michael Phillips': {   'Lady in the Water': 2.5,
                                'Snakes on a Plane': 3.0,
                                'Superman Returns': 3.5,
                                'The Night Listener': 4.0},
        'Mick LaSalle': {   'Just My Luck': 2.0,
                            'Lady in the Water': 3.0,
                            'Snakes on a Plane': 4.0,
                            'Superman Returns': 3.0,
                            'The Night Listener': 3.0,
                            'You, Me and Dupree': 2.0},
        'Toby': {   'Snakes on a Plane': 4.5,
                    'Superman Returns': 4.0,
                    'You, Me and Dupree': 1.0}}

Можно посмотреть, какие оценки выставил критик, выполнив
```python
critics['имя критика']
```

**Code 2:**

```python
critics['Toby']
```

    {'Snakes on a Plane': 4.5, 'You, Me and Dupree': 1.0, 'Superman Returns': 4.0}

Чтобы посмотреть, какие оценки выставил критик определенному фильму, надо добавить еще название фильма:
```python
critics['имя критика']['название фильма']
```

**Code 3:**

```python
critics['Lisa Rose']['Lady in the Water']
```

    2.5

### Критерий похожести

Как мы уже говорили, чтобы выделить подгруппу людей с похожим вкусом, необходимо как-то определить, насколько люди похожи. В нашем случае мы будем сравнивать оценки людей у одинаковых фильмов. Мы рассмотрим два способа сравнения. С помощью
- расстояния Евклида
- корреляции Пирсона

**Оценка по евклидову расстоянию**

Евклидово расстояние для точек $x = (x_1, x_2, ... , x_n)$ и $y = (y_1, y_2, ... , y_n)$ определяется следующим образом:

\begin{equation}
d(x, y) = \sqrt{\sum_{i = 1}^n (x_i - y_i)^2}
\end{equation}

Для вычисления квадратного корня можно воспользоваться готовой функцией **sqrt()** из библиотеки для работы с числами **math**:

**Code 4:**

```python
from math import sqrt
```

Для понимания расстояния Евклида рассмотрим случай, когда *n = 2*. То есть каждый объект выборки (точка) описывается двумя параметрами (координатами). $x = (x_1, x_2)$ и $y = (y_1, y_2)$. Тогда Евклидово расстояние равно расстоянию между точками на плоскости.

\begin{equation}
d(x, y) = \sqrt{\sum_{i = 1}^n (x_i - y_i)^2} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}
\end{equation}

Или, если представить задачу геометрически:

Для примера вычислим расстояния Евклида для близких точек с координатам (1, 1) и (2, 3):

**Code 5:**

```python
sqrt(pow(2-1, 2) + pow(3-1, 2))
```

    2.23606797749979

Теперь вычислим расстояния Евклида для точек, которые находятся далеко (1, 1) и (8, 10):

**Code 6:**

```python
sqrt(pow(8-1, 2) + pow(10-0, 2))
```

    12.206555615733702

Расстояние, вычисленное по этой формуле, будет тем меньше, чем больше сходство людей (чем ближе точки). Нам же нужна функция, значение которой будет, наоборот, большое, если люди сильно похожи друг на друга. То есть схожесть больше (от 0 до 1), если точки ближе друг другу

Для этого будем использовать функцию "похожести" в таком виде:

\begin{equation}
\text{similarity }(x, y) = \dfrac{1}{1 + d(x, y)},
\end{equation}

где $d(x, y)$ - расстояние Евклида.

Пример вычисления сходства по новой функции для точек с координатами (1, 1) и (2, 3):

**Code 7:**

```python
1 / (1 + sqrt(pow(2-1, 2) + pow(3-1, 2)))
```

    0.3090169943749474

Пример вычисления сходства по новой функции для точек с координатами (1, 1) и (8, 10):

**Code 8:**

```python
1 / (1+sqrt(pow(8-1, 2) + pow(10-1, 2)))
```

    0.08063375388365411

Далее реализуем несколько простых функций для коллаборативной фильтрации на основе ближайшего соседа. Чтобы воспользоваться ими, необходимо их импортировать из модуля **recommendations**. Так как мы работаем в онлайн-среде colab, необходимо сначала загрузить модуль из внешнего источника с помощью **wget**.

**Code 9:**

```python
!wget https://www.dropbox.com/s/3z19s9r61zyh37n/recommendations.py
from recommendations import *
```

    --2021-05-17 20:39:44--  https://www.dropbox.com/s/3z19s9r61zyh37n/recommendations.py
    Resolving www.dropbox.com (www.dropbox.com)... 162.125.71.18
    Connecting to www.dropbox.com (www.dropbox.com)|162.125.71.18|:443... connected.
    HTTP request sent, awaiting response... 301 Moved Permanently
    Location: /s/raw/3z19s9r61zyh37n/recommendations.py [following]
    --2021-05-17 20:39:44--  https://www.dropbox.com/s/raw/3z19s9r61zyh37n/recommendations.py
    Reusing existing connection to www.dropbox.com:443.
    HTTP request sent, awaiting response... 302 Found
    Location: https://uc7ec57575c5c7c10bf74e569481.dl.dropboxusercontent.com/cd/0/inline/BOobRVCybCwEMroWXMJKhE1GAzxqt3x3qo37WmId74mo9Dh68PmpKGSAHrtDCjfC3C3_J7D-mz7TRQW9SDvi3PUK_TaGOIUCIrkiTNn3K3C6FzZvcP8dfcboLu-zL3NHnW3hQ7ngRcpqY-Yjt6WJKEQ5/file# [following]
    --2021-05-17 20:39:44--  https://uc7ec57575c5c7c10bf74e569481.dl.dropboxusercontent.com/cd/0/inline/BOobRVCybCwEMroWXMJKhE1GAzxqt3x3qo37WmId74mo9Dh68PmpKGSAHrtDCjfC3C3_J7D-mz7TRQW9SDvi3PUK_TaGOIUCIrkiTNn3K3C6FzZvcP8dfcboLu-zL3NHnW3hQ7ngRcpqY-Yjt6WJKEQ5/file
    Resolving uc7ec57575c5c7c10bf74e569481.dl.dropboxusercontent.com (uc7ec57575c5c7c10bf74e569481.dl.dropboxusercontent.com)... 162.125.71.15
    Connecting to uc7ec57575c5c7c10bf74e569481.dl.dropboxusercontent.com (uc7ec57575c5c7c10bf74e569481.dl.dropboxusercontent.com)|162.125.71.15|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 9624 (9.4K) [text/plain]
    Saving to: ‘recommendations.py’

    recommendations.py  100%[===================>]   9.40K  --.-KB/s    in 0s

    2021-05-17 20:39:46 (105 MB/s) - ‘recommendations.py’ saved [9624/9624]

Функцию для подсчета сходства двух критиков на основе расстояния Евклида реализована в модуле **recommendations** и называется **sim_distance()**. Функция принимает на вход три аргумента:
* словарь критиков
* имя первого критика для сравнения
* имя второго критика для сравнения

Посмотрим, как работает эта функция для двух критиков Lisa Rose и Toby:

**Code 10:**

```python
sim_distance(critics, 'Lisa Rose','Toby')
```

    0.2222222222222222

```python
import numpy as np
```

```python
vals = []

for source in critics:
    for referer in critics:

        vals.append(sim_pearson(critics, source, referer))

vals_arr = np.array(vals)
```

```python
vals_arr
```

    array([ 1.        ,  0.39605902,  0.40451992,  0.56694671,  0.59408853,
            0.74701788,  0.99124071,  0.39605902,  1.        ,  0.2045983 ,
            0.31497039,  0.41176471,  0.96379568,  0.38124643,  0.40451992,
            0.2045983 ,  1.        ,  1.        , -0.25819889,  0.13483997,
           -1.        ,  0.56694671,  0.31497039,  1.        ,  1.        ,
            0.56694671,  0.02857143,  0.89340515,  0.59408853,  0.41176471,
           -0.25819889,  0.56694671,  1.        ,  0.21128856,  0.92447345,
            0.74701788,  0.96379568,  0.13483997,  0.02857143,  0.21128856,
            1.        ,  0.66284898,  0.99124071,  0.38124643, -1.        ,
            0.89340515,  0.92447345,  0.66284898,  1.        ])

```python
vals_arr.mean(), vals_arr.std()
```

    (0.5159356584296166, 0.4713252827635987)

```python
norm_vals_arr = vals_arr - np.eye(len(vals_arr))
```

```python
norm_vals_arr.mean(), norm_vals_arr.std()
```

    (0.49552749516431055, 0.49207640901553995)

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

```python
plt.violinplot(vals_arr.flatten(), vert=False, showmedians=True)
```

```python
critics['Lisa Rose']
```

    {'Lady in the Water': 2.5,
     'Snakes on a Plane': 3.5,
     'Just My Luck': 3.0,
     'Superman Returns': 3.5,
     'You, Me and Dupree': 2.5,
     'The Night Listener': 3.0}

```python
critics['Toby']
```

    {'Snakes on a Plane': 4.5, 'You, Me and Dupree': 1.0, 'Superman Returns': 4.0}

Эта функция работает также как:

\begin{equation}
\text{similarity }(x, y) = \dfrac{1}{1 + d(x, y)},
\end{equation}

где $d(x, y)$ - расстояние Евклида.

Однако, по осям будет фильмы и оценки от критиков. Если у критиков нет одинаковых фильмов, то мы этих критиков не рассматриваем

**Коэффициент корреляции Пирсона**

Другой способ определять степень схожести - использовать коэффициент корреляции Пирсона. Этот коэффициент показывает, насколько два набора данных хорошо ложатся на прямую.

Два кинокритика со средним значением корреляции 0.41 (слева) и два кинокритика с высоким значением корреляции 0.75 (справа).

Корреляция Пирсона считается по формуле:

\begin{equation}
    \text{corr}(x, y) = \dfrac{\sum_{i = 1}^n (x_i - \frac{\sum_{j = 1}^n x_j}{n}) (y_i - \frac{\sum_{j = 1}^n y_j}{n})}{\sqrt{\sum_{i = 1}^n (x_i - \frac{\sum_{j = 1}^n x_j}{n})^2 \sum_{i = 1}^n (y_i - \frac{\sum_{j = 1}^n y_j}{n})^2}}
\end{equation}

По аналогии с функцией **sim_distance()** в модуле **recommendations** реализована функция для подсчета сходства двух критиков на основе корреляции Пирсона. Она называется **sim_pearson()**. Функция принимает на вход те же самые аргументы, что и **sim_distance()**:
* словарь критиков
* имя первого критика для сравнения
* имя второго критика для сравнения

Посмотрим, как работает эта функция для двух критиков Lisa Rose и Gene Seymour:

**Code 11:**

```python
sim_pearson(critics,'Lisa Rose','Gene Seymour')
```

    0.3960590171906697

```python
from scipy.stats import pearsonr
pearsonr(list(critics['Lisa Rose'].values()), list(critics['Gene Seymour'].values()))
```

    (0.4950737714883372, 0.3180601480969472)

```python
sim_pearson(critics,'Lisa Rose','Gene Seymour')
```

    0.3960590171906697

```python
sim_pearson(critics, 'Lisa Rose', 'Toby')
```

    0.9912407071619302

### Ранжирование критиков

После того как мы выбрали критерий для сравнения двух людей, можно начать искать для каждого человека подгруппу других людей с наиболее похожими вкусами.

Для этого в модуле **recommendations** реализована функция **topMatches()**. Функция возвращает список n наилучших соответствий для человека из словаря critics.

Соответственно, аргументы у функции:
* словарь критиков *critics\_dict*
* имя персоны *person*, для которой подбираем соответствия
* число *n* наилучших соответствий

Кроме этого, так как мы хотим использовать различные расстояния, то в качестве аргумента мы будем также передавать имя функции-расстояния (*similarity*).

Пример вычисления топ-3 критиков похожих на Toby по корреляции Пирсона:

**Code 12:**

```python
topMatches(critics, 'Toby', n=3, similarity=sim_pearson)
```

    [(0.9912407071619302, 'Lisa Rose'),
     (0.9244734516419049, 'Mick LaSalle'),
     (0.8934051474415642, 'Claudia Puig')]

Как работает эта функция:
*  считаем схожесть интересующего нас критика с каждым другим критиком используя функцию **similarity**
*  сортируем результаты
*  возвращаем топ **n** результатов

Пример вычисления топ-3 критиков похожих на Toby по евклидову расстоянию:

**Code 13:**

```python
pp.pprint({
    k: v
    for k, v in critics.items()
    if k in ('Toby', 'Mick LaSalle', 'Lisa Rose')
})
```

    {   'Lisa Rose': {   'Just My Luck': 3.0,
                         'Lady in the Water': 2.5,
                         'Snakes on a Plane': 3.5,
                         'Superman Returns': 3.5,
                         'The Night Listener': 3.0,
                         'You, Me and Dupree': 2.5},
        'Mick LaSalle': {   'Just My Luck': 2.0,
                            'Lady in the Water': 3.0,
                            'Snakes on a Plane': 4.0,
                            'Superman Returns': 3.0,
                            'The Night Listener': 3.0,
                            'You, Me and Dupree': 2.0},
        'Toby': {   'Snakes on a Plane': 4.5,
                    'Superman Returns': 4.0,
                    'You, Me and Dupree': 1.0}}

```python
topMatches(critics, 'Toby', n=3, similarity=sim_distance)
```

    [(0.3076923076923077, 'Mick LaSalle'),
     (0.2857142857142857, 'Michael Phillips'),
     (0.23529411764705882, 'Claudia Puig')]

Выбор расстояния для подбора похожести зависит от конкретной задачи. Имеет смысл попробовать разные критерии и посмотреть, какой из них дает наилучший результат.

## Рекомендация фильмов (User-based подход)

Мы умеем находить людей с похожим мнением как у рассматриваемого человека. Но как получить конкретную рекомендацию с набором фильмов? Можно было бы просто посмотреть на то, какие фильмы понравились похожему человеку, и выбрать из них не просмотренные. Однако, такой способ не стабилен: можем, например, отобрать критика, которому по каким-то причинам понравился фильм, получивший негативные оценки от остальных.

Чтобы разрешить эту проблему, можно ранжировать сами фильмы, вычисляя взвешенную сумму оценок критиков. Мы будем брать оценку фильма у критика и умножать его на коэффициент похожести с тем, для которого мы строим рекомендацию. Затем полученный результат суммировать и нормировать.

Идея такого подхода в том, что люди похожие на тебя вносят больший вклад в "рейтинг" фильма.

Этот подход реализован в модуле **recommendations** в функции **getRecommendations()**. Функция принимает на вход следующие аргументы:
* словарь критиков *critics\_dict*
* имя персоны *person*, для которой подбираем соответствия
* имя функции-критерия для сравнения

**Code 14:**

```python
getRecommendations(critics,'Toby', sim_pearson)
```

    [(3.347789526713101, 'The Night Listener'),
     (2.832549918264162, 'Lady in the Water'),
     (2.5309807037655645, 'Just My Luck')]

```python
critics['Toby']
```

    {'Snakes on a Plane': 4.5, 'You, Me and Dupree': 1.0, 'Superman Returns': 4.0}

Как работает эта функция:
*  считаем схожесть пользователя с каждым другим пользователем
*  перебираем фильмы, которые смотрели другие пользователи, но не смотрел текущий пользователь
*  нормируем рейтинг фильмов на основе того насколько он понравился другим пользователям, и того насколько каждый из других пользователей похож на текущего пользователя
*  сортируем список

**Code 15:**

```python
getRecommendations(critics,'Toby', sim_distance)
```

    [(3.5002478401415877, 'The Night Listener'),
     (2.7561242939959363, 'Lady in the Water'),
     (2.461988486074374, 'Just My Luck')]

Мы получили ранжированный список фильмов, а также прогноз оценки, которую поставит Toby этим фильмам.

## Коллаборативная фильтрация по сходству объектов (Item-based collaborative filtering)

Мы реализовали механизм выдачи рекомендаций таким образом, что для создания набора данных необходимы оценки, выставленные каждым
пользователем. Для нескольких тысяч людей это, возможно, и будет работать, но, когда речь идет о миллионах пользователей и товаров, сравнение каждого пользователя со всеми другими, а затем сравнение товаров, займет недопустимо много времени.

Техника, которую мы применяли до сих пор, называется **коллаборативной фильтрацией по схожести пользователей**. Альтернатива известна под названием **коллаборативная фильтрация по схожести образцов**.

Основная идея процедуры фильтрации по схожести образцов заключается в том, чтобы для каждого образца заранее вычислить большинство похожих на него. Тогда для выдачи рекомендаций алгоритму достаточно будет найти те образцы, которым пользователь выставил наивысшие оценки, а затем сформировать список образцов, максимально похожих на эти.

Отметим одно существенное отличие: хотя на первом шаге необходимо исследовать все данные, результаты сравнения образцов изменяются не так часто, как результаты сравнения пользователей.

Чтобы сравнивать образцы, нужно первым делом построить полный набор данных о похожих образцах. В первую очередь, нам нужно переделать словарь предпочтений критиков таким образом, чтобы теперь был словарь фильмов и их оценок:
```python
film_dict = {
    'film_name_1':
    {'critic_1': 'score_1',
     'critic_2': 'score_2',
     ...
    },
    ...
}
```
Далее для каждого фильма посчитать наиболее похожие образцы и коэффициенты похожести для них. Для подсчета схожести будем использовать евклидово расстояние.

В модуле **recommendations** есть функция **calculateSimilarItems()**, которая делает сразу оба шага. На вход её нужно подать словарь с оценками критиков *critics* и число образцов, которые мы считаем наиболее похожими (остальные не выводим).

**Code 16:**

```python
itemsim = calculateSimilarItems(critics, n=10)
```

```python
pp.pprint(itemsim)
```

    {   'Just My Luck': [   (0.2222222222222222, 'Lady in the Water'),
                            (0.18181818181818182, 'You, Me and Dupree'),
                            (0.15384615384615385, 'The Night Listener'),
                            (0.10526315789473684, 'Snakes on a Plane'),
                            (0.06451612903225806, 'Superman Returns')],
        'Lady in the Water': [   (0.4, 'You, Me and Dupree'),
                                 (0.2857142857142857, 'The Night Listener'),
                                 (0.2222222222222222, 'Snakes on a Plane'),
                                 (0.2222222222222222, 'Just My Luck'),
                                 (0.09090909090909091, 'Superman Returns')],
        'Snakes on a Plane': [   (0.2222222222222222, 'Lady in the Water'),
                                 (0.18181818181818182, 'The Night Listener'),
                                 (0.16666666666666666, 'Superman Returns'),
                                 (0.10526315789473684, 'Just My Luck'),
                                 (0.05128205128205128, 'You, Me and Dupree')],
        'Superman Returns': [   (0.16666666666666666, 'Snakes on a Plane'),
                                (0.10256410256410256, 'The Night Listener'),
                                (0.09090909090909091, 'Lady in the Water'),
                                (0.06451612903225806, 'Just My Luck'),
                                (0.05333333333333334, 'You, Me and Dupree')],
        'The Night Listener': [   (0.2857142857142857, 'Lady in the Water'),
                                  (0.18181818181818182, 'Snakes on a Plane'),
                                  (0.15384615384615385, 'Just My Luck'),
                                  (0.14814814814814814, 'You, Me and Dupree'),
                                  (0.10256410256410256, 'Superman Returns')],
        'You, Me and Dupree': [   (0.4, 'Lady in the Water'),
                                  (0.18181818181818182, 'Just My Luck'),
                                  (0.14814814814814814, 'The Night Listener'),
                                  (0.05333333333333334, 'Superman Returns'),
                                  (0.05128205128205128, 'Snakes on a Plane')]}

```python
itemsim['Superman Returns']
```

    [(0.16666666666666666, 'Snakes on a Plane'),
     (0.10256410256410256, 'The Night Listener'),
     (0.09090909090909091, 'Lady in the Water'),
     (0.06451612903225806, 'Just My Luck'),
     (0.05333333333333334, 'You, Me and Dupree')]

Получили вложенный словарь с фильмами и их похожестью на другие фильмы.

Теперь мы готовы выдавать рекомендации, пользуясь словарем данных о схожести образцов. По аналогии с тем, что мы уже делали, необходимо получить список всех образцов, которым пользователь выставлял оценки, найти похожие и взвесить их с учетом коэффициентов подобия (умножить оценку на коэффициент подобия).

Этот механизм реализован в функции **getRecommendedItems()** из **recommendations**. На вход подается:
* словарь с оценками пользователя (в нашем случае это словарь критиков, *critics*)
* словарь с данными о схожести образцов, *itemsim*
* имя критика, для которого строим рекомендации

Получим рекомендации для пользователя Toby:

**Code 17:**

```python
critics['Toby']
```

    {'Snakes on a Plane': 4.5, 'You, Me and Dupree': 1.0, 'Superman Returns': 4.0}

```python
getRecommendedItems(critics, itemsim, 'Toby')
```

    [(3.182634730538922, 'The Night Listener'),
     (2.5983318700614575, 'Just My Luck'),
     (2.4730878186968837, 'Lady in the Water')]

## Метрики качества для рекомендательных систем

Для оценки качества работы рекомендательных систем можно рассмотреть две специальные метрики **Hit Rate** (HR) и **Mean Reciprocal Rank** (MRR).

Определим как **попадание** (**hit**) - ситуацию, когда фильм был рекомендован пользователю в топ-N и пользователь посмотрел его.

**Hit Rate (HR)**

**Hit Rate (процент попаданий)** определяется как общее число попаданий, нормированное на количество пользователей.

$$
\text{HR} = \frac{1}{\text{# test users}} \sum_{\text{test users}}{\text{hit}}, \quad
$$

Чем больше процент попаданий, тем лучше будет наша система рекомендаций.

*Замечание*: в некотором смысле, Hit Rate представляет собой производное от классических ML метрик precision и recall.

**Mean Reciprocal Rank (MRR)**

Отличие метрики **Mean Reciprocal Rank** от предыдущей в том, что мы учитываем не общее количество попаданий, а обратное рангу (месту в списке рекомендаций) **первое попадание**. Например, если фильм, просмотренный пользователем (попадание), стоял на втором месте, обратное рангу будет равно $\frac{1}{2}$, а если бы стоял на третьем - $\frac{1}{3}$ и так далее.

$$
\text{MRR} = \frac{1}{\text{# test users}} \sum_{\text{test users}}{\frac{1}{\text{hit rank}}}
$$

Рассмотрим на примере. Toby получил следующий список рекомендаций:

**Code 18:**

```python
getRecommendations(critics,'Toby', sim_pearson)
```

    [(3.347789526713101, 'The Night Listener'),
     (2.832549918264162, 'Lady in the Water'),
     (2.5309807037655645, 'Just My Luck')]

Допустим, после этого Toby посмотрел 5 фильмов и выставил им следующие оценки:

|Score|Film|
|----|--------------------|
|4.5 | The Departed |
|4   | The Night Listener |
|3.9 | Lady in the Water |
|3.7 | The Firm |
|3   | Just My Luck |

Как мы видим, только два фильма из списка рекомендаций попали в личный топ-3 Toby. Получается, что **попадание (hits)** у него 2.

**HR = $\dfrac{1}{1} * 2 = 2$**, так как выборка тестовых пользователей (test\_users) состоит из одного Toby.

* **$\frac{1}{hit\_rank}$** для фильма The Night Listener - это $\dfrac{1}{2}$
* **$\frac{1}{hit\_rank}$** для фильма Lady in the Water - это $\dfrac{1}{3}$

Но фильм **The Night Listener** стоит выше в рейтинге Toby, чем **Lady in the Water**, поэтому:

**MRR = $\dfrac{1}{1}*\dfrac{1}{2} = \dfrac{1}{2}$**, так как, опять же, выборка тестовых пользователей (test\_users) состоит из одного Toby.

## Бонус* : Рекомендация на данных MovieLens

В этом примере мы рассмотрим реальный набор данных с оценками фильмов, который называется MovieLens. Источник данных: http://grouplens.org/datasets/movielens/

Датасет представляет собой набор фильмов, которые 162541 пользователей сайта MovieLens оценивали по 5-бальной шкале. В наборе также есть дополнительная информация такая как описание фильмов тегами, но в нашем упражнении мы её использовать не будем для упрощения. Также, чтобы уменьшить время вычислений, будем использовать данный только 943 пользователей. Каждый пользователь в датасете оценил минимум 20 фильмов.

Загруженный датасет будет в следующем виде:

```python

preferences = {
    'user_id_1' : {
        'film_name_1' : rate,
        'film_name_2' : rate,
        ...
        'film_name_21' : rate,
    },
    ...
    'user_id_943' : {
        'film_name_1' : rate,
        'film_name_2' : rate,
        ...
        'film_name_30' : rate,
    }
}

```

Чтобы загрузить набор данных MovieLens, в модуле **recommendations** есть функция **loadMovieLens()**. Она возвращает словарь предпочтений фильмов для набора людей. Пользователи зашифрованы идентификатором. Для каждого пользователя имеется название
фильма, а также оценка, выставленная фильму данным пользователе.

Перед запуском функции необходимо предварительно загрузить из внешнего источника файлы с данными с помощью **wget**.

**Code 19:**

```python
!wget https://www.dropbox.com/s/e0nehgkgv3uoad3/u.item
!wget https://www.dropbox.com/s/7sy6mzs6d2skp8h/u.data
```

    --2020-02-18 12:00:36--  https://www.dropbox.com/s/e0nehgkgv3uoad3/u.item
    Resolving www.dropbox.com (www.dropbox.com)... 162.125.8.1, 2620:100:601f:1::a27d:901
    Connecting to www.dropbox.com (www.dropbox.com)|162.125.8.1|:443... connected.
    HTTP request sent, awaiting response... 301 Moved Permanently
    Location: /s/raw/e0nehgkgv3uoad3/u.item [following]
    --2020-02-18 12:00:36--  https://www.dropbox.com/s/raw/e0nehgkgv3uoad3/u.item
    Reusing existing connection to www.dropbox.com:443.
    HTTP request sent, awaiting response... 302 Found
    Location: https://uc80a4a8acda4f8a83537167ecb7.dl.dropboxusercontent.com/cd/0/inline/AyUVWsLDXZPoVYNSJeMt21Q4vINdd8b0DzzZJ8owSi7pMWukrQwK6fEI-6CMsjRE6L6KNTZ1LKNSQPYAx0GBjV0mD_9p_OG11P6FZdIXU8fIKBkdeajSPCO4dsjz_0Bo9I4/file# [following]
    --2020-02-18 12:00:36--  https://uc80a4a8acda4f8a83537167ecb7.dl.dropboxusercontent.com/cd/0/inline/AyUVWsLDXZPoVYNSJeMt21Q4vINdd8b0DzzZJ8owSi7pMWukrQwK6fEI-6CMsjRE6L6KNTZ1LKNSQPYAx0GBjV0mD_9p_OG11P6FZdIXU8fIKBkdeajSPCO4dsjz_0Bo9I4/file
    Resolving uc80a4a8acda4f8a83537167ecb7.dl.dropboxusercontent.com (uc80a4a8acda4f8a83537167ecb7.dl.dropboxusercontent.com)... 162.125.9.6, 2620:100:601b:6::a27d:806
    Connecting to uc80a4a8acda4f8a83537167ecb7.dl.dropboxusercontent.com (uc80a4a8acda4f8a83537167ecb7.dl.dropboxusercontent.com)|162.125.9.6|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 236344 (231K) [text/plain]
    Saving to: ‘u.item’

    u.item              100%[===================>] 230.80K  --.-KB/s    in 0.08s

    2020-02-18 12:00:37 (2.86 MB/s) - ‘u.item’ saved [236344/236344]

    --2020-02-18 12:00:38--  https://www.dropbox.com/s/7sy6mzs6d2skp8h/u.data
    Resolving www.dropbox.com (www.dropbox.com)... 162.125.8.1, 2620:100:601f:1::a27d:901
    Connecting to www.dropbox.com (www.dropbox.com)|162.125.8.1|:443... connected.
    HTTP request sent, awaiting response... 301 Moved Permanently
    Location: /s/raw/7sy6mzs6d2skp8h/u.data [following]
    --2020-02-18 12:00:38--  https://www.dropbox.com/s/raw/7sy6mzs6d2skp8h/u.data
    Reusing existing connection to www.dropbox.com:443.
    HTTP request sent, awaiting response... 302 Found
    Location: https://uc30623e45f824e89668cc389d69.dl.dropboxusercontent.com/cd/0/inline/AyWOp5S9WAFWuv6_KviLLcIAH8Fky85du3ls26wznBhcJLLT00SxBk3EE_O7zN-nfGsUDCgGWaoYe8jDK3hKa-_Al8o92cUOBoKSxMzaRaStw0BqZCWkCIIcIMFeIZLPZ_I/file# [following]
    --2020-02-18 12:00:38--  https://uc30623e45f824e89668cc389d69.dl.dropboxusercontent.com/cd/0/inline/AyWOp5S9WAFWuv6_KviLLcIAH8Fky85du3ls26wznBhcJLLT00SxBk3EE_O7zN-nfGsUDCgGWaoYe8jDK3hKa-_Al8o92cUOBoKSxMzaRaStw0BqZCWkCIIcIMFeIZLPZ_I/file
    Resolving uc30623e45f824e89668cc389d69.dl.dropboxusercontent.com (uc30623e45f824e89668cc389d69.dl.dropboxusercontent.com)... 162.125.9.6, 2620:100:601f:6::a27d:906
    Connecting to uc30623e45f824e89668cc389d69.dl.dropboxusercontent.com (uc30623e45f824e89668cc389d69.dl.dropboxusercontent.com)|162.125.9.6|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 1979173 (1.9M) [text/plain]
    Saving to: ‘u.data’

    u.data              100%[===================>]   1.89M  --.-KB/s    in 0.1s

    2020-02-18 12:00:39 (13.6 MB/s) - ‘u.data’ saved [1979173/1979173]

```python
preferences = loadMovieLens()
```

Выведем фильмы для пользователя с ID 87 и рейтинги этих фильмов

**Code 20:**

```python
pp.pprint(preferences['87'])
```

    {   '2001: A Space Odyssey (1968)': 5.0,
        'Ace Ventura: Pet Detective (1994)': 4.0,
        'Addams Family Values (1993)': 2.0,
        'Addicted to Love (1997)': 4.0,
        'Adventures of Priscilla, Queen of the Desert, The (1994)': 3.0,
        'Adventures of Robin Hood, The (1938)': 5.0,
        'Air Force One (1997)': 3.0,
        'Air Up There, The (1994)': 3.0,
        'Alien (1979)': 4.0,
        'American President, The (1995)': 5.0,
        'Annie Hall (1977)': 4.0,
        'Apocalypse Now (1979)': 4.0,
        'Babe (1995)': 5.0,
        'Baby-Sitters Club, The (1995)': 2.0,
        'Back to the Future (1985)': 5.0,
        'Bad Boys (1995)': 4.0,
        'Bananas (1971)': 5.0,
        'Barcelona (1994)': 3.0,
        'Batman & Robin (1997)': 4.0,
        'Batman (1989)': 3.0,
        'Batman Returns (1992)': 3.0,
        'Big Green, The (1995)': 3.0,
        'Big Squeeze, The (1996)': 2.0,
        'Birdcage, The (1996)': 4.0,
        'Blade Runner (1982)': 4.0,
        'Blues Brothers, The (1980)': 5.0,
        'Boomerang (1992)': 3.0,
        'Boot, Das (1981)': 4.0,
        'Brady Bunch Movie, The (1995)': 2.0,
        'Braveheart (1995)': 4.0,
        'Bridge on the River Kwai, The (1957)': 5.0,
        'Broken Arrow (1996)': 3.0,
        'Butch Cassidy and the Sundance Kid (1969)': 5.0,
        'Citizen Kane (1941)': 4.0,
        "City Slickers II: The Legend of Curly's Gold (1994)": 3.0,
        'Clear and Present Danger (1994)': 5.0,
        'Cliffhanger (1993)': 3.0,
        'Clockwork Orange, A (1971)': 4.0,
        'Clueless (1995)': 4.0,
        'Con Air (1997)': 4.0,
        'Conan the Barbarian (1981)': 3.0,
        'Coneheads (1993)': 4.0,
        'Cool Hand Luke (1967)': 5.0,
        'Cops and Robbersons (1994)': 3.0,
        'Cowboy Way, The (1994)': 3.0,
        'Crow, The (1994)': 3.0,
        'Dances with Wolves (1990)': 5.0,
        "Dante's Peak (1997)": 3.0,
        'Dave (1993)': 4.0,
        'Days of Thunder (1990)': 5.0,
        'Dead Man Walking (1995)': 4.0,
        'Dead Poets Society (1989)': 5.0,
        'Deer Hunter, The (1978)': 3.0,
        'Demolition Man (1993)': 3.0,
        'Desperado (1995)': 3.0,
        'Die Hard (1988)': 4.0,
        'Die Hard: With a Vengeance (1995)': 4.0,
        'Down Periscope (1996)': 4.0,
        'Dragonheart (1996)': 4.0,
        'Dumb & Dumber (1994)': 4.0,
        'Dunston Checks In (1996)': 1.0,
        'E.T. the Extra-Terrestrial (1982)': 3.0,
        'Ed Wood (1994)': 3.0,
        'Empire Strikes Back, The (1980)': 5.0,
        'Endless Summer 2, The (1994)': 3.0,
        'Evil Dead II (1987)': 2.0,
        'Executive Decision (1996)': 3.0,
        'Fargo (1996)': 5.0,
        'First Wives Club, The (1996)': 2.0,
        'Fish Called Wanda, A (1988)': 5.0,
        'Forget Paris (1995)': 4.0,
        'Four Weddings and a Funeral (1994)': 5.0,
        'French Kiss (1995)': 5.0,
        'Fugitive, The (1993)': 5.0,
        'Full Metal Jacket (1987)': 4.0,
        'Get Shorty (1995)': 5.0,
        'Glory (1989)': 4.0,
        'Godfather, The (1972)': 4.0,
        'GoldenEye (1995)': 4.0,
        'Good, The Bad and The Ugly, The (1966)': 5.0,
        'GoodFellas (1990)': 4.0,
        'Grease (1978)': 4.0,
        'Great White Hype, The (1996)': 3.0,
        'Groundhog Day (1993)': 5.0,
        'Grumpier Old Men (1995)': 4.0,
        'Hard Target (1993)': 4.0,
        'Heat (1995)': 3.0,
        'Heathers (1989)': 3.0,
        'Home Alone (1990)': 4.0,
        'Hoop Dreams (1994)': 4.0,
        'Hot Shots! Part Deux (1993)': 4.0,
        'House Arrest (1996)': 3.0,
        'I Love Trouble (1994)': 3.0,
        'I.Q. (1994)': 5.0,
        'In the Army Now (1994)': 4.0,
        'In the Line of Fire (1993)': 5.0,
        'Independence Day (ID4) (1996)': 5.0,
        'Indiana Jones and the Last Crusade (1989)': 5.0,
        'Inkwell, The (1994)': 3.0,
        "It's a Wonderful Life (1946)": 5.0,
        'Jack (1996)': 3.0,
        'Jeffrey (1995)': 3.0,
        'Jimmy Hollywood (1994)': 3.0,
        "Joe's Apartment (1996)": 2.0,
        'Junior (1994)': 4.0,
        'Jurassic Park (1993)': 5.0,
        'Kingpin (1996)': 4.0,
        'Lightning Jack (1994)': 3.0,
        'Lost World: Jurassic Park, The (1997)': 3.0,
        'Low Down Dirty Shame, A (1994)': 3.0,
        'M*A*S*H (1970)': 5.0,
        'Magnificent Seven, The (1954)': 5.0,
        'Major Payne (1994)': 3.0,
        'Man of the House (1995)': 3.0,
        'Manchurian Candidate, The (1962)': 4.0,
        'Mars Attacks! (1996)': 3.0,
        'Mask, The (1994)': 3.0,
        'Matilda (1996)': 3.0,
        'Maverick (1994)': 3.0,
        'Michael (1996)': 4.0,
        'Mighty Aphrodite (1995)': 3.0,
        'Milk Money (1994)': 4.0,
        'Mission: Impossible (1996)': 4.0,
        "Monty Python's Life of Brian (1979)": 4.0,
        'Mother (1996)': 2.0,
        'Mrs. Doubtfire (1993)': 4.0,
        'Multiplicity (1996)': 3.0,
        'Muppet Treasure Island (1996)': 3.0,
        'My Favorite Year (1982)': 3.0,
        'Naked Gun 33 1/3: The Final Insult (1994)': 4.0,
        'Net, The (1995)': 5.0,
        'Nine Months (1995)': 4.0,
        'Nutty Professor, The (1996)': 4.0,
        'Platoon (1986)': 3.0,
        'Prefontaine (1997)': 5.0,
        'Private Benjamin (1980)': 4.0,
        'Professional, The (1994)': 4.0,
        'Program, The (1993)': 3.0,
        'Pulp Fiction (1994)': 4.0,
        "Pyromaniac's Love Story, A (1995)": 3.0,
        'Quiet Man, The (1952)': 5.0,
        'Raging Bull (1980)': 3.0,
        'Raiders of the Lost Ark (1981)': 5.0,
        'Raising Arizona (1987)': 3.0,
        'Reality Bites (1994)': 3.0,
        'Renaissance Man (1994)': 5.0,
        'Return of the Jedi (1983)': 5.0,
        'Return of the Pink Panther, The (1974)': 4.0,
        'River Wild, The (1994)': 4.0,
        'Sabrina (1995)': 4.0,
        'Santa Clause, The (1994)': 4.0,
        "Schindler's List (1993)": 4.0,
        'Searching for Bobby Fischer (1993)': 4.0,
        'Serial Mom (1994)': 1.0,
        'Shadow, The (1994)': 3.0,
        'Shawshank Redemption, The (1994)': 5.0,
        "Singin' in the Rain (1952)": 4.0,
        'Sleeper (1973)': 4.0,
        'Sleepers (1996)': 4.0,
        'Sleepless in Seattle (1993)': 5.0,
        'Sneakers (1992)': 4.0,
        'So I Married an Axe Murderer (1993)': 2.0,
        'Something to Talk About (1995)': 2.0,
        'Son in Law (1993)': 4.0,
        'Speechless (1994)': 4.0,
        'Speed (1994)': 5.0,
        'Speed 2: Cruise Control (1997)': 3.0,
        'Star Trek III: The Search for Spock (1984)': 4.0,
        'Star Trek IV: The Voyage Home (1986)': 5.0,
        'Star Trek: First Contact (1996)': 4.0,
        'Star Trek: The Motion Picture (1979)': 3.0,
        'Star Trek: The Wrath of Khan (1982)': 5.0,
        'Star Wars (1977)': 5.0,
        'Stargate (1994)': 5.0,
        'Sting, The (1973)': 5.0,
        'Strange Days (1995)': 3.0,
        'Strictly Ballroom (1992)': 3.0,
        'Striptease (1996)': 2.0,
        'Supercop (1992)': 3.0,
        'Swimming with Sharks (1995)': 3.0,
        'Switchblade Sisters (1975)': 2.0,
        'Terminator 2: Judgment Day (1991)': 5.0,
        'Terminator, The (1984)': 5.0,
        'That Old Feeling (1997)': 4.0,
        'That Thing You Do! (1996)': 4.0,
        'This Is Spinal Tap (1984)': 5.0,
        'To Die For (1995)': 3.0,
        'To Kill a Mockingbird (1962)': 4.0,
        'To Wong Foo, Thanks for Everything! Julie Newmar (1995)': 3.0,
        'Tommy Boy (1995)': 4.0,
        'Top Gun (1986)': 5.0,
        'Treasure of the Sierra Madre, The (1948)': 4.0,
        'True Lies (1994)': 5.0,
        'Truth About Cats & Dogs, The (1996)': 4.0,
        'Twelve Monkeys (1995)': 4.0,
        'Twister (1996)': 4.0,
        "Ulee's Gold (1997)": 3.0,
        'Under Siege (1992)': 4.0,
        'Up in Smoke (1978)': 3.0,
        'Vegas Vacation (1997)': 4.0,
        'Victor/Victoria (1982)': 4.0,
        'Waterworld (1995)': 4.0,
        "Weekend at Bernie's (1989)": 3.0,
        'When Harry Met Sally... (1989)': 5.0,
        'While You Were Sleeping (1995)': 5.0,
        'Wizard of Oz, The (1939)': 5.0,
        'Wyatt Earp (1994)': 3.0,
        'Young Frankenstein (1974)': 5.0,
        'Young Guns (1988)': 3.0,
        'Young Guns II (1990)': 2.0}

Посмотрим, какие рекомендации получим с помощью коллаборативной фильтрации по схожести пользователей. Напомним, что для этого реализована функция **getRecommendations()**, которая выдает рекомендации для данного пользователя по похожести на других и предположительную его оценку фильму.

Функция принимает на вход следующие аргументы:
* словарь пользователей, в этом случае это *preferences*
* имя персоны *person*, для которой подбираем соответствия
* имя функции-критерия для сравнения

Выведем первые топ-30 рекомендаций для пользователя с ID 87, для этого в квадратных скобочках укажем [0:30].

*Замечание*: может появиться предупреждение RuntimeWarning, это связано с тем, что теперь мы работаем с большой выборкой и при подсчете корреляции Пирсона может возникать деление на ноль. В этом случае мы присваиваем корреляции значение ноль.

**Code 21:**

```python
getRecommendations(preferences, '87', sim_pearson)[0:10]
```

    [(5.0, 'They Made Me a Criminal (1939)'),
     (5.0, 'Star Kid (1997)'),
     (5.0, 'Santa with Muscles (1996)'),
     (5.0, 'Saint of Fort Washington, The (1993)'),
     (5.0, 'Marlene Dietrich: Shadow and Light (1996) '),
     (5.0, 'Great Day in Harlem, A (1994)'),
     (5.0, 'Entertaining Angels: The Dorothy Day Story (1996)'),
     (5.0, 'Boys, Les (1997)'),
     (4.898844431289231, 'Legal Deceit (1997)'),
     (4.815019082242706, 'Letter From Death Row, A (1998)')]

Интересно также сравнить полученный результат с рекомендациями по схожести образцов. Напомним, чтобы сравнить образцы, нужно в первую очередь перестроить наш набор данных из набора предпочтений пользователей в набор оценок фильмов. Для этого используем функцию **calculateSimilarItems()**. На вход её нужно подать словарь с оценками пользователей *prefernces*. Кроме этого, мы передадим аргумент *n = 50*, так как хотим оставить только 50 наиболее похожих образцов в новом словаре.

В этот раз мы работаем с большим набором данных и вычисления могут занять некоторое время, поэтому функция также будет выводить сообщение о том, сколько фильмов она уже просмотрела.

**Code 22:**

```python
item_similarities = calculateSimilarItems(preferences, n=50)
```

    100 / 1664
    200 / 1664
    300 / 1664
    400 / 1664
    500 / 1664
    600 / 1664
    700 / 1664
    800 / 1664
    900 / 1664
    1000 / 1664
    1100 / 1664
    1200 / 1664
    1300 / 1664
    1400 / 1664
    1500 / 1664
    1600 / 1664

Посмотрим на результат:

**Code 23:**

```python
item_similarities["What's Eating Gilbert Grape (1993)"]
```

    [(1.0, 'Yankee Zulu (1994)'),
     (1.0, 'Woman in Question, The (1950)'),
     (1.0, 'Witness (1985)'),
     (1.0, "Wend Kuuni (God's Gift) (1982)"),
     (1.0, 'Vie est belle, La (Life is Rosey) (1987)'),
     (1.0, 'Vermont Is For Lovers (1992)'),
     (1.0, 'Van, The (1996)'),
     (1.0, 'Two or Three Things I Know About Her (1966)'),
     (1.0, 'Two Much (1996)'),
     (1.0, 'Two Deaths (1995)'),
     (1.0, 'Two Bits (1995)'),
     (1.0, 'Touki Bouki (Journey of the Hyena) (1973)'),
     (1.0, 'Total Eclipse (1995)'),
     (1.0, 'To Have, or Not (1995)'),
     (1.0, 'To Cross the Rubicon (1991)'),
     (1.0, 'Tigrero: A Film That Was Never Made (1994)'),
     (1.0, 'The Courtyard (1995)'),
     (1.0, 'Terror in a Texas Town (1958)'),
     (1.0, 'Talking About Sex (1994)'),
     (1.0, 'T-Men (1947)'),
     (1.0, 'Symphonie pastorale, La (1946)'),
     (1.0, 'Swept from the Sea (1997)'),
     (1.0, 'Stranger in the House (1997)'),
     (1.0, 'Stonewall (1995)'),
     (1.0, 'Squeeze (1996)'),
     (1.0, 'Spirits of the Dead (Tre passi nel delirio) (1968)'),
     (1.0, 'Somebody to Love (1994)'),
     (1.0, 'So Dear to My Heart (1949)'),
     (1.0, 'Sliding Doors (1998)'),
     (1.0, 'Shooting Fish (1997)'),
     (1.0, 'Shadows (Cienie) (1988)'),
     (1.0, 'Secret Adventures of Tom Thumb, The (1993)'),
     (1.0, 'Scream of Stone (Schrei aus Stein) (1991)'),
     (1.0, 'Salut cousin! (1996)'),
     (1.0, 'Romper Stomper (1992)'),
     (1.0, 'Relative Fear (1994)'),
     (1.0, 'Quartier Mozart (1992)'),
     (1.0, 'Promise, The (Versprechen, Das) (1994)'),
     (1.0, 'Prefontaine (1997)'),
     (1.0, "Pharaoh's Army (1995)"),
     (1.0, 'Nosferatu a Venezia (1986)'),
     (1.0, 'Nina Takes a Lover (1994)'),
     (1.0, 'Niagara, Niagara (1997)'),
     (1.0, 'Mr. Jones (1993)'),
     (1.0, 'Mostro, Il (1994)'),
     (1.0, 'Modern Affair, A (1995)'),
     (1.0, 'Mighty, The (1998)'),
     (1.0, 'Metisse (Café au Lait) (1993)'),
     (1.0, 'Man from Down Under, The (1943)'),
     (1.0, 'Liebelei (1933)')]

Теперь получим топ-30 рекомендаций для пользователя с учетом похожести фильмов. Используем функцию **getRecommendedItems()**, которой на вход подается:
* словарь с оценками пользователя (*preferences*)
* словарь с данными о схожести фильмов, *item_similarities*
* ID пользователя, для которого строим рекомендации

**Code 24:**

```python
getRecommendedItems(preferences, item_similarities, '87')[0:30]
```

    [(5.0, "What's Eating Gilbert Grape (1993)"),
     (5.0, 'Vertigo (1958)'),
     (5.0, 'Usual Suspects, The (1995)'),
     (5.0, 'Toy Story (1995)'),
     (5.0, 'Titanic (1997)'),
     (5.0, 'Sword in the Stone, The (1963)'),
     (5.0, 'Stand by Me (1986)'),
     (5.0, 'Sling Blade (1996)'),
     (5.0, 'Silence of the Lambs, The (1991)'),
     (5.0, 'Shining, The (1980)'),
     (5.0, 'Shine (1996)'),
     (5.0, 'Sense and Sensibility (1995)'),
     (5.0, 'Scream (1996)'),
     (5.0, 'Rumble in the Bronx (1995)'),
     (5.0, 'Rock, The (1996)'),
     (5.0, 'Robin Hood: Prince of Thieves (1991)'),
     (5.0, 'Reservoir Dogs (1992)'),
     (5.0, 'Police Story 4: Project S (Chao ji ji hua) (1993)'),
     (5.0, 'House of the Spirits, The (1993)'),
     (5.0, 'Fresh (1994)'),
     (5.0, 'Denise Calls Up (1995)'),
     (5.0, 'Day the Sun Turned Cold, The (Tianguo niezi) (1994)'),
     (5.0, 'Before the Rain (Pred dozhdot) (1994)'),
     (5.0, 'Assignment, The (1997)'),
     (5.0, '1-900 (1994)'),
     (4.875, "Ed's Next Move (1996)"),
     (4.833333333333333, 'Anna (1996)'),
     (4.8, 'Dark City (1998)'),
     (4.75, 'Flower of My Secret, The (Flor de mi secreto, La) (1995)'),
     (4.75, 'Broken English (1996)')]

Выведем список фильмов, которые пользователь с id `87` оценил максимальным баллом:

**Code 25:**

```python
{x: preferences['87'][x] for x in preferences['87'] if preferences['87'][x] == 5}
```

    {'2001: A Space Odyssey (1968)': 5.0,
     'Adventures of Robin Hood, The (1938)': 5.0,
     'American President, The (1995)': 5.0,
     'Babe (1995)': 5.0,
     'Back to the Future (1985)': 5.0,
     'Bananas (1971)': 5.0,
     'Blues Brothers, The (1980)': 5.0,
     'Bridge on the River Kwai, The (1957)': 5.0,
     'Butch Cassidy and the Sundance Kid (1969)': 5.0,
     'Clear and Present Danger (1994)': 5.0,
     'Cool Hand Luke (1967)': 5.0,
     'Dances with Wolves (1990)': 5.0,
     'Days of Thunder (1990)': 5.0,
     'Dead Poets Society (1989)': 5.0,
     'Empire Strikes Back, The (1980)': 5.0,
     'Fargo (1996)': 5.0,
     'Fish Called Wanda, A (1988)': 5.0,
     'Four Weddings and a Funeral (1994)': 5.0,
     'French Kiss (1995)': 5.0,
     'Fugitive, The (1993)': 5.0,
     'Get Shorty (1995)': 5.0,
     'Good, The Bad and The Ugly, The (1966)': 5.0,
     'Groundhog Day (1993)': 5.0,
     'I.Q. (1994)': 5.0,
     'In the Line of Fire (1993)': 5.0,
     'Independence Day (ID4) (1996)': 5.0,
     'Indiana Jones and the Last Crusade (1989)': 5.0,
     "It's a Wonderful Life (1946)": 5.0,
     'Jurassic Park (1993)': 5.0,
     'M*A*S*H (1970)': 5.0,
     'Magnificent Seven, The (1954)': 5.0,
     'Net, The (1995)': 5.0,
     'Prefontaine (1997)': 5.0,
     'Quiet Man, The (1952)': 5.0,
     'Raiders of the Lost Ark (1981)': 5.0,
     'Renaissance Man (1994)': 5.0,
     'Return of the Jedi (1983)': 5.0,
     'Shawshank Redemption, The (1994)': 5.0,
     'Sleepless in Seattle (1993)': 5.0,
     'Speed (1994)': 5.0,
     'Star Trek IV: The Voyage Home (1986)': 5.0,
     'Star Trek: The Wrath of Khan (1982)': 5.0,
     'Star Wars (1977)': 5.0,
     'Stargate (1994)': 5.0,
     'Sting, The (1973)': 5.0,
     'Terminator 2: Judgment Day (1991)': 5.0,
     'Terminator, The (1984)': 5.0,
     'This Is Spinal Tap (1984)': 5.0,
     'Top Gun (1986)': 5.0,
     'True Lies (1994)': 5.0,
     'When Harry Met Sally... (1989)': 5.0,
     'While You Were Sleeping (1995)': 5.0,
     'Wizard of Oz, The (1939)': 5.0,
     'Young Frankenstein (1974)': 5.0}

Валидация для моделей рекомендательных систем устроена достаточно сложно.
Например, нам нужно забыть часть рейтингов фильмов для пользователя, а потом попытаться восстановить их с
помощью нашей модели.

В данном случае, мы можем оценить адекватность полученных рекомендаций, просмотрев списки фильмов. Однако, на текущем датасете, из-за размера выборки, такое сделать сложно.

Приведем примеры интерпретации:
* В случае использования расстояния, основанного на коррелции, модель рекомендует в первую очередь боевики
* В случае использование похожестей фильмов мы видим как общепризнанные шедевры, так и боевики
* В реальности пользователю нравятся фильмы, которые нравятся всем, приключенческие фильмы и боевики

С такой точки зрения оба списка рекомендаций кажутся адекватным, а то, что они не пересекаются связано с тем,
что есть много похожих друг на друга фильмов - и мы не можем рекомендовать сразу все такие фильмы пользователю.

Для рекомендательных систем есть специальные библиотеки, например, https://surprise.readthedocs.io/, однако, их применение требует более глубокого знакомства с темой.

```python
real = list({x: preferences['87'][x] for x in preferences['87'] if preferences['87'][x] == 5}.keys())
```

```python
for i in range(0,50):
  print(getRecommendedItems(preferences, item_similarities, '87')[:50][i][1] in real)
```

    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False
    False


```python
import sys
sys.path.append('..')
import os
import torch
import numpy as np
import matplotlib.pyplot as plt
from sympy import simplify_logic
import time
from sklearn.metrics import accuracy_score
import pandas as pd
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.tree import _tree, export_text
from torchvision import datasets, transforms
from torch.utils.data import TensorDataset, DataLoader
from torch.utils.data.sampler import SubsetRandomSampler
import torch.nn.functional as F
from sklearn.preprocessing import OneHotEncoder
from sklearn.model_selection import train_test_split

from lens.utils.base import validate_network, set_seed, tree_to_formula
from lens.utils.relu_nn import get_reduced_model, prune_features
from lens.utils.psi_nn import prune_equal_fanin
from lens import logic

results_dir = './results/mnist'
if not os.path.isdir(results_dir):
    os.makedirs(results_dir)

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
concepts = [f'c{i}' for i in range(10)]
n_rep = 10
tot_epochs = 4001
prune_epochs = 2000
concepts
```

    ['c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9']

```python
# MNIST problem
num_workers = 0
batch_size = 128
valid_size = 0.2
# Data augmentation for train data + conversion to tensor
train_transforms = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))

])# Data augmentation for test data + conversion to tensor
test_transforms= transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,),(0.5,))
])
train_data = datasets.MNIST(root='./data', train=True, download=True, transform=train_transforms)
test_data = datasets.MNIST(root='./data', train=False, download=True, transform=test_transforms)
```

```python
# Finding indices for validation set
num_train = len(train_data)
indices = list(range(num_train))
#Randomize indices
np.random.shuffle(indices)
split = int(np.floor(num_train*valid_size))
train_index, test_index = indices[split:], indices[:split]# Making samplers for training and validation batches
train_sampler = SubsetRandomSampler(train_index)
valid_sampler = SubsetRandomSampler(test_index)# Creating data loaders
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, sampler=train_sampler, num_workers=num_workers)
valid_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, sampler=valid_sampler, num_workers=num_workers)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size, num_workers=num_workers)
```

```python
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # convolutional layers
        self.conv1 = torch.nn.Conv2d(1, 8, 3, padding=1)
        self.conv2 = torch.nn.Conv2d(8, 16, 3, padding =1)
        # linear layers
        self.fc1 = torch.nn.Linear(784, 256)
        self.fc2 = torch.nn.Linear(256, 128)
        self.fc3 = torch.nn.Linear(128, 64)
        self.fc4 = torch.nn.Linear(64, 10)
        # dropout
        self.dropout = torch.nn.Dropout(p=0.2)
        # max pooling
        self.pool = torch.nn.MaxPool2d(2, 2)

    def forward(self, x):
        # convolutional layers with ReLU and pooling
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        # flattening the image
        x = x.view(-1, 7*7*16)
        # linear layers
        x = self.dropout(F.relu(self.fc1(x)))
        x = self.dropout(F.relu(self.fc2(x)))
        x = self.dropout(F.relu(self.fc3(x)))
        x = self.fc4(x)
        return F.softmax(x, dim=1)

model = Net()
print(model)
model.cuda()
```

```python
criterion = torch.nn.CrossEntropyLoss()
if os.path.isfile('./models/mnist/trained_model.pt'):
    model.load_state_dict(torch.load('./models/mnist/trained_model.pt'))

else:
    # epochs to train for
    epochs = 25
    set_seed(0)

    # tracks validation loss change after each epoch
    minimum_validation_loss = np.inf

    #optimizer = torch.optim.SGD(model.parameters(), lr = 0.001)
    optimizer = torch.optim.AdamW(model.parameters(), lr=0.001)
    criterion = torch.nn.CrossEntropyLoss()
    for epoch in range(1, epochs+1):

        train_loss = 0
        valid_loss = 0

        # training steps
        model.train()
        for batch_index, (data, target) in enumerate(train_loader):
            # moves tensors to GPU
            data, target = data.cuda(), target.cuda()
            # clears gradients
            optimizer.zero_grad()
            # forward pass
            output = model(data)
            # loss in batch
            loss = criterion(output, target)
            # backward pass for loss gradient
            loss.backward()
            # update paremeters
            optimizer.step()
            # update training loss
            train_loss += loss.item()*data.size(0)

        # validation steps
        model.eval()
        for batch_index, (data, target) in enumerate(valid_loader):
            # moves tensors to GPU
            data, target = data.cuda(), target.cuda()
            # forward pass
            output = model(data)
            # loss in batch
            loss = criterion(output, target)
            # update validation loss
            valid_loss += loss.item()*data.size(0)

        # average loss calculations
        train_loss = train_loss/len(train_loader.sampler)
        valid_loss = valid_loss/len(valid_loader.sampler)

        # Display loss statistics
        print(f'Current Epoch: {epoch}\nTraining Loss: {round(train_loss, 6)}\nValidation Loss: {round(valid_loss, 6)}')

        # Saving model every time validation loss decreases
        if valid_loss <= minimum_validation_loss and epoch > 20:
            print(f'Validation loss decreased from {round(minimum_validation_loss, 6)} to {round(valid_loss, 6)}')
            torch.save(model.state_dict(), './models/mnist/trained_model.pt')
            minimum_validation_loss = valid_loss
            print('Saving New Model')

    model.load_state_dict(torch.load('./models/mnist/trained_model.pt'))
```

```python
# tracking test loss
test_loss = 0.0
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))

model.eval()
classes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for batch_idx, (data, target) in enumerate(test_loader):
    # move tensors to GPU
    data, target = data.cuda(), target.cuda()
    # forward pass
    output = model(data)
    # batch loss
    loss = criterion(output, target)
    # test loss update
    test_loss += loss.item()*data.size(0)
    # convert output probabilities to predicted class
    _, pred = torch.max(output, 1)
    # compare predictions to true label
    correct_tensor = pred.eq(target.data.view_as(pred))
    correct = np.squeeze(correct_tensor.numpy()) if not torch.cuda.is_available() else np.squeeze(correct_tensor.cpu().numpy())
    # calculate test accuracy for each object class
    for i in range(10):
        label = target.data[i]
        class_correct[label] += correct[i].item()
        class_total[label] += 1

# average test loss
test_loss = test_loss/len(test_loader.dataset)
print(f'Test Loss: {round(test_loss, 6)}')

for i in range(10):
    if class_total[i] > 0:
        print(f'Test Accuracy of {classes[i]}: {round(100*class_correct[i]/class_total[i], 2)}%')
    else:
        print(f'Test Accuracy of {classes[i]}s: N/A (no training examples)')

print(f'Full Test Accuracy: {round(100. * np.sum(class_correct) / np.sum(class_total), 2)}% {np.sum(class_correct)} out of {np.sum(class_total)}')
```

```python
pred_c_train = []
true_c_train = []
model.eval()
for batch_index, (data, target) in enumerate(train_loader):
    # moves tensors to GPU
    data, target = data.cuda(), target.cuda()
    # forward pass
    output = model(data)
    pred = torch.argmax(output, 1)
    pred_c_train.append(pred)
    true_c_train.append(target)
```

```python
pred_c_train = torch.cat(pred_c_train)
true_c_train = torch.cat(true_c_train)
pred_c_train.shape
```

```python
y_trainval = (true_c_train % 2 == 1).to(torch.long)
y_trainval.shape
```

```python
x_trainval = OneHotEncoder(sparse=False).fit_transform(pred_c_train.cpu().detach().numpy().reshape(-1, 1))
x_trainval = torch.FloatTensor(x_trainval)
x_train, x_val, y_train, y_val = train_test_split(x_trainval, y_trainval, test_size=300, random_state=42)
print(x_train.shape)
print(x_val.shape)
```

```python
pred_c_test = []
true_c_test = []
model.eval()
for batch_index, (data, target) in enumerate(test_loader):
    # moves tensors to GPU
    data, target = data.cuda(), target.cuda()
    # forward pass
    output = model(data)
    pred = torch.argmax(output, 1)
    pred_c_test.append(pred)
    true_c_test.append(target)
```

```python
pred_c_test = torch.cat(pred_c_test)
true_c_test = torch.cat(true_c_test)
pred_c_test.shape
```

```python
y_test = (true_c_test % 2 == 1).to(torch.long)
y_test.shape
```

```python
x_test = OneHotEncoder(sparse=False).fit_transform(true_c_test.cpu().detach().numpy().reshape(-1, 1))
x_test = torch.FloatTensor(x_test)
x_test.shape
```

```python
def train_nn(x_train, y_train, need_pruning, seed, device, relu=False, verbose=False):
    set_seed(seed)
    x_train = x_train.to(device)
    y_train = y_train.to(device)
    layers = [
        torch.nn.Linear(x_train.size(1), 100),
        torch.nn.ReLU() if relu else torch.nn.LeakyReLU(),
        torch.nn.Linear(100, 50),
        torch.nn.ReLU() if relu else torch.nn.LeakyReLU(),
        torch.nn.Linear(50, 30),
        torch.nn.ReLU() if relu else torch.nn.LeakyReLU(),
        torch.nn.Linear(30, 2),
        torch.nn.Softmax(dim=1),
    ]
    model = torch.nn.Sequential(*layers).to(device)

    optimizer = torch.optim.AdamW(model.parameters(), lr=0.001)
    loss_form = torch.nn.CrossEntropyLoss()
    model.train()
    for epoch in range(tot_epochs):
        # forward pass
        optimizer.zero_grad()
        y_pred = model(x_train)
        # Compute Loss
        loss = loss_form(y_pred, y_train)

        for module in model.children():
            if isinstance(module, torch.nn.Linear):
                loss += 0.001 * torch.norm(module.weight, 1)
                loss += 0.001 * torch.norm(module.bias, 1)
                break

        # backward pass
        loss.backward()
        optimizer.step()

        if epoch > prune_epochs and need_pruning:
            prune_features(model, n_classes=1, device=device)
            need_pruning = False

        # compute accuracy
        if epoch % 500 == 0 and verbose:
            y_pred_d = torch.argmax(y_pred, dim=1)
            accuracy = y_pred_d.eq(y_train).sum().item() / y_train.size(0)
            print(f'\t Epoch {epoch}: train accuracy: {accuracy:.4f}')

    return model
```

```python
def train_psi_nn(x_train, y_train, need_pruning, seed, device, verbose=False):
    set_seed(seed)
    x_train = x_train.to(device)
    y_train = y_train.to(device).to(torch.float)
    layers = [
        torch.nn.Linear(x_train.size(1), 10),
        torch.nn.Sigmoid(),
        torch.nn.Linear(10, 4),
        torch.nn.Sigmoid(),
        torch.nn.Linear(4, 1),
        torch.nn.Sigmoid(),
    ]
    model = torch.nn.Sequential(*layers).to(device)

    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    loss_form = torch.nn.BCELoss()
    model.train()
    for epoch in range(tot_epochs):
        # forward pass
        optimizer.zero_grad()
        y_pred = model(x_train).squeeze()
        # Compute Loss
        loss = loss_form(y_pred, y_train)

        for module in model.children():
            if isinstance(module, torch.nn.Linear):
                loss += 0.00001 * torch.norm(module.weight, 1)

        # backward pass
        loss.backward()
        optimizer.step()

        if epoch > prune_epochs and need_pruning:
            model = prune_equal_fanin(model, 2, validate=True, device=device)
            need_pruning = False

        # compute accuracy
        if epoch % 500 == 0 and verbose:
            y_pred_d = y_pred > 0.5
            accuracy = y_pred_d.eq(y_train).sum().item() / y_train.size(0)
            print(f'\t Epoch {epoch}: train accuracy: {accuracy:.4f}')

    return model
```

```python
def c_to_y(method, need_pruning, relu, verbose=False):
    methods = []
    splits = []
    explanations = []
    explanations_inv = []
    model_accuracies = []
    explanation_accuracies = []
    explanation_accuracies_inv = []
    elapsed_times = []
    elapsed_times_inv = []
    for seed in range(n_rep):
        explanation, explanation_inv = '', ''
        explanation_accuracy, explanation_accuracy_inv = 0, 0

        print(f'Seed [{seed+1}/{n_rep}]')

        if method == 'tree':
            classifier = DecisionTreeClassifier(random_state=seed)
            classifier.fit(x_train.cpu().detach().numpy(), y_train.cpu().detach().numpy())
            y_preds = classifier.predict(x_test.cpu().detach().numpy())
            model_accuracy = accuracy_score(y_test.cpu().detach().numpy(), y_preds)

            target_class = 1
            start = time.time()
            explanation = tree_to_formula(classifier, concepts, target_class)
            elapsed_time = time.time() - start
            explanation_accuracy = model_accuracy

            target_class_inv = 0
            start = time.time()
            explanation_inv = tree_to_formula(classifier, concepts, target_class_inv)
            elapsed_time_inv = time.time() - start
            explanation_accuracy_inv = model_accuracy

        else:
            if method == 'psi':
                # positive class
                target_class = 1
                model = train_psi_nn(x_train, y_train.eq(target_class), need_pruning, seed, device, verbose)
                y_preds = model(x_test.to(device)).cpu().detach().numpy()
                model_accuracy = accuracy_score(y_test.cpu().detach().numpy(), y_preds>0.5)

            else:
                model = train_nn(x_train, y_train, need_pruning, seed, device, relu, verbose)
                y_preds = model(x_test.to(device)).cpu().detach().numpy()
                model_accuracy = accuracy_score(y_test.cpu().detach().numpy(), y_preds.argmax(axis=1))

            # positive class
            target_class = 1
            start = time.time()
            if method == 'psi':
                global_explanation = logic.generate_fol_explanations(model, device)[0]
            else:
                global_explanation, _, _ = logic.relu_nn.combine_local_explanations(model,
                                                                                x_val.to(device),
                                                                                y_val.to(device),
                                                                                topk_explanations=5,
                                                                                target_class=target_class,
                                                                                method=method, device=device)
            elapsed_time = time.time() - start

            if global_explanation:
                explanation_accuracy, _ = logic.base.test_explanation(global_explanation, target_class, x_test, y_test)
                explanation = logic.base.replace_names(global_explanation, concepts)

            # negative class
            target_class_inv = 0
            if method == 'psi':
                model = train_psi_nn(x_train, y_train.eq(target_class_inv), need_pruning, seed, device, verbose)

            start = time.time()
            if method == 'psi':
                global_explanation_inv = logic.generate_fol_explanations(model, device)[0]
            else:
                global_explanation_inv, _, _ = logic.relu_nn.combine_local_explanations(model,
                                                                                x_val.to(device),
                                                                                y_val.to(device),
                                                                                topk_explanations=5,
                                                                                target_class=target_class_inv,
                                                                                method=method, device=device)
            elapsed_time_inv = time.time() - start
            if global_explanation_inv:
                explanation_accuracy_inv, _ = logic.base.test_explanation(global_explanation_inv,
                                                                          target_class_inv, x_test, y_test)
                explanation_inv = logic.base.replace_names(global_explanation_inv, concepts)

        if verbose:
            print(f'\t Model\'s accuracy: {model_accuracy:.4f}')
            print(f'\t Class {target_class} - Global explanation: "{explanation}" - Accuracy: {explanation_accuracy:.4f}')
            print(f'\t Elapsed time {elapsed_time}')
            print(f'\t Class {target_class_inv} - Global explanation: "{explanation_inv}" - Accuracy: {explanation_accuracy_inv:.4f}')
            print(f'\t Elapsed time {elapsed_time_inv}')

        methods.append(method)
        splits.append(seed)
        explanations.append(explanation)
        explanations_inv.append(explanation_inv)
        model_accuracies.append(model_accuracy)
        explanation_accuracies.append(explanation_accuracy)
        explanation_accuracies_inv.append(explanation_accuracy_inv)
        elapsed_times.append(elapsed_time)
        elapsed_times_inv.append(elapsed_time_inv)

    results = pd.DataFrame({
        'method': methods,
        'split': splits,
        'explanation': explanations,
        'explanation_inv': explanations_inv,
        'model_accuracy': model_accuracies,
        'explanation_accuracy': explanation_accuracies,
        'explanation_accuracy_inv': explanation_accuracies_inv,
        'elapsed_time': elapsed_times,
        'elapsed_time_inv': elapsed_times_inv,
    })
    results.to_csv(os.path.join(results_dir, f'results_{method}.csv'))

    return results
```

# General pruning

```python
method = 'pruning'
need_pruning = True
relu = False
results_pruning = c_to_y(method, need_pruning, relu)
results_pruning
```

# LIME

```python
# method = 'lime'
# need_pruning = False
# relu = False
# results_lime = c_to_y(method, need_pruning, relu)
# results_lime
```

# ReLU

```python
method = 'weights'
need_pruning = False
relu = True
results_weights = c_to_y(method, need_pruning, relu)
results_weights
```

# Psi network

```python
method = 'psi'
need_pruning = True
relu = False
results_psi = c_to_y(method, need_pruning, relu, verbose=False)
results_psi
```

# Decision tree

```python
method = 'tree'
need_pruning = False
relu = False
results_tree = c_to_y(method, need_pruning, relu)
results_tree
```

# Summary

```python
cols = ['model_accuracy', 'explanation_accuracy', 'explanation_accuracy_inv', 'elapsed_time', 'elapsed_time_inv']
mean_cols = [f'{c}_mean' for c in cols]
sem_cols = [f'{c}_sem' for c in cols]

# pruning
df_mean = results_pruning[cols].mean()
df_sem = results_pruning[cols].sem()
df_mean.columns = mean_cols
df_sem.columns = sem_cols
summary_pruning = pd.concat([df_mean, df_sem])
summary_pruning.name = 'pruning'

# # lime
# df_mean = results_lime[cols].mean()
# df_sem = results_lime[cols].sem()
# df_mean.columns = mean_cols
# df_sem.columns = sem_cols
# summary_lime = pd.concat([df_mean, df_sem])
# summary_lime.name = 'lime'

# weights
df_mean = results_weights[cols].mean()
df_sem = results_weights[cols].sem()
df_mean.columns = mean_cols
df_sem.columns = sem_cols
summary_weights = pd.concat([df_mean, df_sem])
summary_weights.name = 'weights'

# psi
df_mean = results_psi[cols].mean()
df_sem = results_psi[cols].sem()
df_mean.columns = mean_cols
df_sem.columns = sem_cols
summary_psi = pd.concat([df_mean, df_sem])
summary_psi.name = 'psi'

# tree
df_mean = results_tree[cols].mean()
df_sem = results_tree[cols].sem()
df_mean.columns = mean_cols
df_sem.columns = sem_cols
summary_tree = pd.concat([df_mean, df_sem])
summary_tree.name = 'tree'

summary = pd.concat([summary_pruning,
#                      summary_lime,
                     summary_weights, summary_psi, summary_tree], axis=1).T
summary.columns = mean_cols + sem_cols
summary
```

```python
summary.to_csv(os.path.join(results_dir, 'summary.csv'))
```


# Scenario F - Dataset Real (Beers NIR)

In this scenario the the probabilistic model is tested on real-world datasets. The model parameters differ per dataset, and are set by the user. The probabilistic model is formulated as follows:

\begin{equation}
\large y = f(x) = \sum\limits_{m=1}^M \big[A_m \cdot f_{pseudo-Voigt}(x)\big] + a_0 + a_1 \cdot x + \epsilon
\end{equation}

where:

\begin{equation}
\large f_{pseudo-Voigt}(x) = \eta \cdot \frac{\sigma_m^2}{(x-\mu_m)^2 + \sigma_m^2} + (1 - \eta) \cdot e^{-\frac{(x-\mu_m)^2}{2\cdot\sigma_m^2}}
\end{equation}

```python
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pymc3 as pm
import arviz as az

#az.style.use('arviz-darkgrid')

print('Running on PyMC3 v{}'.format(pm.__version__))
```

    Running on PyMC3 v3.8

## Import local modules

```python
import os
import sys
sys.path.append('../../modules')
import datagen as dg
import models as mdl
import results as res
import figures as fig
import settings as cnf
from data import Data
```

## Local configuration

```python
# output for results and images
out_path      = './output_data_real_beers'
file_basename = out_path + '/scenario_data_real_beers'

conf = {}

# scenario name
conf['scenario'] = 'dataset real'

# initialization method for sampler
conf['init_mode'] = 'adapt_diag'

# probabilistic model (priors) (normal/lognormal/eta_rv)
conf['prior_model'] = 'normal'

# provide peak positions to the model as testvalues ('yes'/'no')
conf['peak_info'] = 'yes'

# model mode ('train'/eval')
conf['model_mode'] = 'eval'

# number of cores to run sampling chains on
conf['ncores'] = 2

# number of samples per chain
conf['nsamples'] = 2000
```

```python
# if the output dir does not exist, create it
if not os.path.exists(out_path):
    os.makedirs(out_path)

conf
```

    {'scenario': 'dataset real',
     'init_mode': 'adapt_diag',
     'prior_model': 'normal',
     'peak_info': 'yes',
     'model_mode': 'eval',
     'ncores': 2,
     'nsamples': 2000}

## Save configuration

```python
cnf.save(out_path, conf)
```

# Import data and plot

```python
# dataset filelocation
dataset_filename = '../../../data/beers/NIR_beers.csv'

# dataset object init
data_obj = Data(dataset_filename, shuffle='yes')
data_obj.data.head(5)
```

      500
      502.85
      505.7
      508.55
      511.4
      514.25
      517.1
      519.95
      522.8
      525.65
      ...
      2469.34999999996
      2472.19999999996
      2475.04999999996
      2477.89999999996
      2480.74999999996
      2483.59999999996
      2486.44999999996
      2489.29999999996
      2492.14999999996
      label

      43
      0.083477
      0.083599
      0.083728
      0.083864
      0.083996
      0.084117
      0.084235
      0.084370
      0.084518
      0.084649
      ...
      0.777067
      0.782554
      0.787664
      0.792388
      0.796683
      0.800610
      0.804201
      0.807321
      0.809980
      1

      34
      -0.003401
      -0.003281
      -0.003153
      -0.003031
      -0.002922
      -0.002821
      -0.002724
      -0.002629
      -0.002529
      -0.002418
      ...
      0.680850
      0.686305
      0.691412
      0.696080
      0.700388
      0.704347
      0.707879
      0.710993
      0.713759
      1

      1
      -0.007983
      -0.007857
      -0.007716
      -0.007581
      -0.007481
      -0.007393
      -0.007294
      -0.007196
      -0.007099
      -0.006991
      ...
      0.672060
      0.677424
      0.682413
      0.687002
      0.691147
      0.694890
      0.698302
      0.701316
      0.703913
      2

      16
      0.086181
      0.086304
      0.086432
      0.086568
      0.086696
      0.086819
      0.086941
      0.087052
      0.087158
      0.087289
      ...
      0.786842
      0.792500
      0.797777
      0.802655
      0.807141
      0.811207
      0.814830
      0.818077
      0.820963
      1

      37
      -0.002702
      -0.002589
      -0.002469
      -0.002357
      -0.002263
      -0.002178
      -0.002092
      -0.002007
      -0.001918
      -0.001821
      ...
      0.681876
      0.687409
      0.692493
      0.697092
      0.701362
      0.705330
      0.708713
      0.711816
      0.714572
      2

5 rows × 701 columns

```python
# select single class for inference
sclass = 2
df_cls = data_obj.data.loc[(data_obj.data['label'] == sclass)]
```

```python
# drop non-data columns
df = df_cls.drop(columns=['label'])
```

```python
print("total number of spectra      : {0}".format(len(data_obj.data)))
print("total number of x-values     : {0}".format(len(np.array(df.columns.to_list(), dtype='float32'))))
print("total number of classes      : {0}".format(data_obj.classes))
print("selected class for inference : {0}".format(sclass))
print("number of selected spectra   : {0}".format(len(df)))
print("\nclass distribution:")
print(data_obj.data[data_obj.label_column].value_counts())
```

    total number of spectra      : 44
    total number of x-values     : 700
    total number of classes      : 2
    selected class for inference : 2
    number of selected spectra   : 16

    class distribution:
    1    28
    2    16
    Name: label, dtype: int64

```python
# number of samples to run inference on
nsamples = 15

# set peak information for spectrum
peaks = [1000, 1470, 1690, 2165, 2225, 2550]

# baseline profile
base_shape = 'linear'

# plot dataset (nsamples spectra, classes mixed)
fig.plot_datasets_real(df, peaks, nsamples, savefig='yes', fname=file_basename)
```

```python
# compare samples of different classes
df_lst = []
df_lst.append(df)
df_lst.append(data_obj.data.loc[(data_obj.data['label'] == 1)].drop(columns=['label']))

# plot dataset (nsamples spectra, classes mixed)
fig.plot_datasets_real_compare(df_lst, nsamples, savefig='yes', fname=file_basename + "_cmp")
```

# Initialize models and run inference

```python
ldata  = []
lpeaks = []

# add dataframes and peakinfo to list (for multiple classes or data per class)
ldata.append(df)
lpeaks.append(peaks)
```

```python
# convert pandas data to numpy arrays
x_val = np.array(df.columns, dtype='float32')

# store dataset y-values in list
cols = ldata[0].columns
y_val = [ldata[i][cols].values[:nsamples] for i in range(len(ldata))]
```

```python
# initialize models and run inference
models = []
traces = []

for i in range(len(ldata)):
    if conf['peak_info'] == 'yes':
        plist = np.array(lpeaks[0], dtype='float32').flatten()
        plist.sort()
        model_g = mdl.model_pvoigt(xvalues=x_val, observations=y_val[i], npeaks=len(peaks),
                              mu_peaks=plist, pmodel=conf['prior_model'], baseline=base_shape)
    else:
        model_g = mdl.model_pvoigt(xvalues=x_val, observations=y_val[i], npeaks=len(peaks),
                                              pmodel=conf['prior_model'], baseline=base_shape)

    models.append(model_g)

    with model_g:
        if conf['model_mode'] == 'train':
            print("running inference on dataset #{0}/{1}".format(i+1,len(ldata)))
            trace_g = pm.sample(conf['nsamples'], init=conf['init_mode'], cores=conf['ncores'])
            traces.append(trace_g)
            # save inference results
            pm.backends.text.dump(out_path + '/traces_%02d' % (i+1), trace_g)
        else:
            # load traces from disk
            print("loading dataset #{0}/{1}".format(i+1,len(ldata)))
            trace_g = pm.backends.text.load(out_path + '/traces_%02d' % (i+1))
            traces.append(trace_g)
```

    loading dataset #1/1

# Model visualization

```python
# display first model
pm.model_to_graphviz(models[0])
```

```python
# save model figure as image
img = pm.model_to_graphviz(models[0])
img.render(filename=file_basename + '_model', format='png');
```

# Collect results and save

```python
# posterior predictive traces
ppc = [pm.sample_posterior_predictive(traces[i], samples=500, model=models[i]) for i in range(len(traces))]
```

    /home/johan/VirtualEnv/ppsda/lib/python3.6/site-packages/pymc3/sampling.py:1247: UserWarning: samples parameter is smaller than nchains times ndraws, some draws and/or chains may not be represented in the returned posterior predictive sample
      "samples parameter is smaller than nchains times ndraws, some draws "
    100%|██████████| 500/500 [00:59<00:00,  8.41it/s]

```python
# various plots to inspect the inference results
varnames = mdl.get_varnames(traces[0])

#az.plot_trace(traces[2], varnames, compact=True);
#az.plot_trace(traces[2], varnames, divergences='top');
#az.plot_autocorr(traces[0], varnames);
#az.plot_posterior(traces[8], varnames);

#for idx, trace in enumerate(traces):
#    az.plot_forest(trace, var_names = varnames, r_hat=True, ess=True);

#az.summary(traces[1], varnames)
```

```python
if conf['model_mode'] == 'train':
    # collect the results and display
    df = res.get_results_summary(traces, ppc, y_val, varnames)
else:
    # load results from disk
    df = pd.read_csv(file_basename + '.csv')
    df.index += 1
df
```

      r_hat
      mcse
      ess
      bfmi
      r2
      waic
      epsilon

      1
      1.383429
      0.660543
      22.971429
      1.169884
      0.993579
      -52996.994605
      0.019358

```python
if conf['model_mode'] == 'train':
    # save results to .csv
    df.to_csv(file_basename + '.csv', index=False)
```

```python
cnf.close(out_path)
```

# Plot posterior (single dataset)

```python
# dataset to plot
n_dataset = 0

# trace number in matrix
n_trace = 0

#fig.plot_posterior_single(x_val, ldata[n_dataset], ppc[n_trace], 5,
#                              file_basename, hpd_color='yellow', peak_pos=peaks)
fig.plot_posterior_single(x_val, ldata[n_dataset], ppc[n_trace], 10,
                              file_basename, hpd_color='orange', peak_pos=peaks, no_data='yes')
```

```python
az.plot_posterior(traces[n_trace], ['mu', 'amp', 'sigma', 'epsilon'], credible_interval=0.95);
```

```python
az.plot_posterior(traces[n_trace], ['a0'], credible_interval=0.95);
```

```python
az.plot_posterior(traces[n_trace], ['a1'], credible_interval=0.95);
```


# Reinforcement Learning: REINFORCE (Policy Gradient Algorithm)

## Overview

In this notebook, we'll cover policy gradient algorithms, and we'll implement `REINFORCE`, the archetypal policy gradient algorithm. We'll test it by solving the cartpole problem in the Open AI gym.

```
import numpy as np
import pandas as pd
import datetime

# Import tensorflow
#!pip install tensorflow-gpu==1.14.0 > /dev/null 2>&1
import tensorflow as tf
import tensorflow.keras as K
print(tf.__version__)

# Check that tf sees the GPU
device_name = tf.test.gpu_device_name()
print(device_name)

# Import libraries for plotting
import matplotlib as mpl
import matplotlib.pyplot as plt
plt.style.use('seaborn-pastel')

%matplotlib inline
%config InlineBackend.figure_format = 'retina'  # this makes plot in high res
```

    2.4.0
    /device:GPU:0

Since we are in a remote notebook, we cannot display the progress of the environment in real time. Instead, we store the renderings and show a video at the end of the episode (refer to [this](https://star-ai.github.io/Rendering-OpenAi-Gym-in-Colaboratory/) guide in case you need it). The only advice that I can give is to import `gym` _after_ the update below.

```
#remove " > /dev/null 2>&1" to see what is going on under the hood
!pip install gym pyvirtualdisplay > /dev/null 2>&1
!apt-get install -y xvfb python-opengl ffmpeg > /dev/null 2>&1
```

```
# Maybe
# !apt-get update > /dev/null 2>&1
# !apt-get install cmake > /dev/null 2>&1
# !pip install --upgrade setuptools 2>&1
# !pip install ez_setup > /dev/null 2>&1
# !pip install gym[atari] > /dev/null 2>&1
```

```
# Open AI gym
import gym
from gym import logger as gymlogger
from gym.wrappers import Monitor
gymlogger.set_level(40) #error only
import math
import random
import glob
import io
import base64
from IPython.display import HTML
from IPython import display as ipythondisplay
```

```
from pyvirtualdisplay import Display
display = Display(visible=0, size=(2880, 1800))
display.start()
```

    <pyvirtualdisplay.display.Display at 0x7f1f97063978>

The function below is needed to display the video. I slightly modified it from the original one (that you can in the guide I linked above) to avoid the infinite repetition loop of the video.

```
"""
Utility functions to enable video recording of gym environment and displaying it
To enable video, just do "env = wrap_env(env)""
"""

def show_video():
  mp4list = glob.glob('video/*.mp4')
  if len(mp4list) > 0:
    mp4 = mp4list[0]
    video = io.open(mp4, 'r+b').read()
    encoded = base64.b64encode(video)
    ipythondisplay.display(HTML(data=''''''.format(encoded.decode('ascii'))))
  else:
    print("Could not find video")

def wrap_env(env):
  env = Monitor(env, './video', force=True)
  return env
```

## OpenAI Gym Cartpole

The Cartpole problem is a discrete control problem where we try to keep the pole vertical by moving the cart below it.

Upon loading the environment, we launch a simulation where the agent chooses at random from the action sample space the next action. Finally, we show the video of the result. What happens is that the problem is considered unsolved (= game over) if the angle between pole and the line orthogonal to the cart axis is larger than a threshold. The parameter `done` specifies when the experiment is over.

```
# Load the environment and start
env = wrap_env(gym.make("CartPole-v0"))
```

```
observation = env.reset()

while True:

    env.render()

    action = env.action_space.sample()

    observation, reward, done, info = env.step(action)

    if done:
      break;

env.close()
```

```
show_video()
```

To better understand the inputs and outputs of the environment, let consider the action space and the observation space.

The action space is of type `Discrete(n)` where `n` is the number of actions. This is equivalent to the discrete set $\{ 0, 1, 2, \dotsc, n-1 \}$.

```
env.action_space
```

    Discrete(2)

The observation space is of type `Box(n)`, which means that it is the Cartesian product of `n` intervals.

```
env.observation_space
```

    Box(-3.4028234663852886e+38, 3.4028234663852886e+38, (4,), float32)

```
[env.observation_space.low, env.observation_space.high]
```

    [array([-4.8000002e+00, -3.4028235e+38, -4.1887903e-01, -3.4028235e+38],
           dtype=float32),
     array([4.8000002e+00, 3.4028235e+38, 4.1887903e-01, 3.4028235e+38],
           dtype=float32)]

When we make a step in the environment, the feedback that we get includes the observation:

```
#env = gym.make('CartPole-v0')
# env = Monitor(env, './video', force=True)
env.reset()

obs, r, done, _ = env.step(0)
print(obs)
```

    [-0.0248302  -0.19857408  0.00899464  0.29775695]

## Policy Gradient Algorithms

Policy gradient algorithms seek to learn the policy via gradient descent, as the name suggests. They differ from value-based algorithms that instead learn the Q-function and derive the policy from there, and thus indirectly.

To be more precise, a policy gradient algorithm seeks to learn the probability of each action given the current state, i.e., the policy $\pi(s,a)$. Then we take the action by randomly sampling $\pi(s,a)$.

In order to fit $\pi(s,a)$, we need to define an objective function. Since the neural network can approximate “any” function, we shall keep the neural network find the parameters $\theta$ of the policy $\pi_\theta$ that maximizes the future expected discounted reward:

$$ \theta^* = \arg\max_\theta \mathbf{E}\!\left[\sum_{t=0}^T\gamma^t r(s_t,a_t)\right] $$

where the expectation is over trajectories $\tau=(s_0, a_0, r_0, s_1, a_1, r_1, \cdots, r_{T})$, $r_t:=r(s_t,a_t)$ is the reward that we get when we take action $a_t$ in state $s_t$, and $T$ is the length of the episode.

As a shorthand, define the future discounted reward
$$ R(\tau) =\sum_{t=0}^T\gamma^t r(s_t,a_t). $$
The expected value of $R$ is just the value function. To be extremely clear, it is very important to recognize that $R$ depends on the trajectory, but it does not depend on the weights of the policy. In fact, two different policies that suggest the same actions would get to the same value of $R$. In other words, $R$ depends on the actions, not on why or how those actions have been taken. This is very important in deriving the gradient with respect to $\theta$ below.

The trajectory follows a Markov decision process, i.e.

$$ p_\theta(\tau) = \prod_{t=0}^T p(s_{t}|a_{t-1},s_{t-1})\pi_\theta(a_t|s_t) $$
with the convention that $p(s_0|a_{-1}, s_{-1})=p_0(s_0)$ where $p_0$ is the density of the distribution of initial states. Putting all together:

$$ \theta^* = \arg\max_\theta \int \prod_{t=0}^T ds_t\, da_t \, \pi_\theta(a_t|s_t) p(s_{t}|a_{t-1}, s_{t-1}) \, R(\tau). $$

To find the maximum, we compute the gradient with respect to $\theta$ of the objective function above:

\begin{align} \nabla_\theta \mathbf{E}[R(\tau)]
& = \int d\tau \nabla p_\theta(\tau) R(\tau) \\ & =  \int d\tau p_\theta(\tau) \nabla_\theta\!\log p_\theta(\tau) R(\tau) \\
& = \mathbf{E}[\nabla_\theta\!\log p_\theta(\tau) R(\tau)] \\
& = \mathbf{E}\!\left[\sum_{t=0}^T \nabla_\theta\!\log \pi_\theta(a_t|s_t) R(\tau)\right].
\end{align}
Notice that $R(\tau)$ multiplies the entire sum, that is,

$$ \nabla_\theta \mathbf{E}[R(\tau)]  = \mathbf{E}\!\left[R(\tau)\sum_{t=0}^T \nabla_\theta\!\log \pi_\theta(a_t|s_t) \right]. $$

By integrating back in $\theta$, we get that an equivalent objective function to maximize is given by the expression above without the gradient, i.e.
$$ \theta^* = \arg\max_\theta \mathbf{E}\!\left[R(\tau)\sum_{t=0}^T \log \pi_\theta(a_t|s_t) \right]. $$

From a theoretical perspective, this is pretty much what a policy gradient algorithm does.

From a practical perspective, the integral is evaluated via the Monte Carlo method over several trajectories (episodes). That is, we sample a trajectory, optimize over it, and repeat this process several times.

The way it is implemented below is the following. We sample a trajectory with the previously available $\pi_\theta$. We save in memory all steps, actions, and rewards. We form the artificial distribution $\tilde{\pi}_t(a, a_t)=\delta_{a,a_t}$ where $\delta$ is the Kronecker delta. This is our “target distribution” since we expect, at convergence, that there is always a best action to take and that $\pi_\theta$ suggests such best action. To compare $\pi_\theta$ and $\tilde{\pi}_t$ we use the categorical crossentropy. The loss function is given by

\begin{align} L(\tau) & = R(\tau)\sum_{t=0}^T h(\tilde{\pi}_t, \pi_\theta) \\ & = R(\tau)\sum_{t=0}^T  {\color{green}{\sum_{a\in\mathcal{A}} -\tilde{\pi}_t(a) \log (\pi_\theta(a))}} \\
& = -R(\tau)\sum_{t=0}^T  \log (\pi_\theta(a_t)).
\end{align}
Minimizing $L(\tau)$ is equivalent to the maximization problem that we illustrated above.

To speed up the learning process, we provide all steps at once to the network, and compute a slightly different loss. Firstly, let us simplify the loss above via

$$ L(\tau) \approx -R(\tau) \log(\pi_\theta(a_0)).$$

Secondly, define the discounted future reward from $t'$ to $T$,

$$R(\tau_{t'}) := R(s_{t'}, a_{t'}, r_{t'}, \cdots, s_{T}, a_{T}, r_{T}),$$

for all $t'\in[0:T]$. Finally, we define a cumulative loss by summing up the loss at each step in the episode:
\begin{align} L(\tau) & \approx - \sum_{t=0}^T R(\tau_{t}) \log (\pi_\theta(a_t)).
\end{align}

```
# REINFORCE
class REINFORCE:

  def __init__(self, state_size, action_size, gamma=None, max_steps=None):
    # max_steps is the maximum number of batches [s, a, r, s_] or epochs remembered

    # Parameters
    self.state_size = state_size
    self.action_size = action_size

    self.memory = list()
    if gamma is None:
      self.gamma = 0.99
    else:
      self.gamma = gamma

    if max_steps is None:
      self.max_steps = 200
    else:
      self.max_steps = max_steps

    self.lr = 0.005       # learning rates

    # actor network
    self.actor = self.build_actor()

  def remember(self, s, a, r, s_, done):
    self.memory.append([s, a, r, s_, done])
    if len(self.memory) > self.max_steps: # if too long
      self.memory.pop(0) # forget the oldest

  def forget(self):
    self.memory = list()

  # actor learns the policy: input is state; output is distribution over actions (policy)
  def build_actor(self, n_hidden_1=None, n_hidden_2=None):

    if n_hidden_1 == None:
      n_hidden_1 = 6 * self.state_size

    if n_hidden_2 == None:
      n_hidden_2 = 6 * self.state_size

    model = K.Sequential()
    model.add(K.layers.Dense(n_hidden_1, activation=tf.nn.elu, input_dim=self.state_size))  # input
    model.add(K.layers.Dense(n_hidden_2, activation=tf.nn.elu))
    model.add(K.layers.Dense(self.action_size, activation='softmax'))  # output

    # loss is categorical_crossentropy since pi_theta* (vector) should be equal to one-hot action (vector)
    # because there is always a best action to be taken
    #
    model.compile(optimizer=K.optimizers.RMSprop(lr=self.lr), loss='categorical_crossentropy')

    return model

  # actor implements policy gradient
  def policy(self, s):
    policy = self.actor.predict(s, batch_size=1).flatten()
    a = np.random.choice(self.action_size, 1, p=policy)[0]
    return a

  # learn from memory
  def learn(self):

    # replay the entire episode
    # minibatch = random.sample(self.memory, batch_size)
    s, a, r, s_, done = zip(*self.memory)
    a = np.reshape(a, (-1, 1))
    T = a.shape[0] # epochs in memory
    a_one_hot = np.zeros((T, self.action_size))
    a_one_hot[np.arange(T), a.reshape(-1)] = 1   # size: T x action_size

    s = np.concatenate(s) # or np.vstack(s)
    target_actor = a_one_hot # actions
    # s_ = np.concatenate(s_)

    R = np.zeros(T,)
    R[T-1] = r[T-1]
    for t in reversed(range(T-1)):
      R[t] = self.gamma * R[t+1] + r[t]
    R = R.reshape(-1, 1)

    self.actor.fit(s, target_actor, sample_weight=R, epochs=1, verbose=0)
    # self.actor.train_on_batch(s, target_actor, sample_weight=R)

    # v = self.critic.predict(s)
    # v_ = self.critic.predict(s_)
    # target_actor[:, a] = r + (1-done) * self.gamma * v_ - v # advantage
```

## Training

```
seed = 0
np.random.seed(seed)
tf.random.set_seed(seed)

# Restart environment
# env = Monitor(env, './video', force=True)
MAX_REWARD = 200
env._max_episode_steps = MAX_REWARD

# Parameters
n_episodes = 300
winning_streak = 10  # after this number of successive successes, training stops
reward_history = np.zeros(n_episodes)
gamma = 0.99
steps_in_memory = 200  # number of steps to remember

A = np.arange(env.action_space.n)
dim_state_space = env.observation_space.shape[0]

# Start training
agent = REINFORCE(dim_state_space, env.action_space.n, gamma, steps_in_memory)

# init
s = env.reset()
s = np.reshape(s, [1, dim_state_space])

# for _ in range(n_steps_in_memory):
#   a = agent.policy(s)
#   s_, r, done, _ = env.step(a)
#   s_ = np.reshape(s_, [1, dim_state_space])
#   agent.remember(s, a, r, s_, done)

template = "\rEpisode: {:3d}/{:3d} | Reward: {:3.0f} | Duration: {:.2f} s"
```

```
for e in range(n_episodes):
  start_time = datetime.datetime.now()

  s = env.reset()
  s = np.reshape(s, [1, dim_state_space])

  done = False
  cum_reward = 0

  while not done:

    a = agent.policy(s)
    s_, r, done, _ = env.step(a)
    s_ = np.reshape(s_, [1, dim_state_space])
    agent.remember(s, a, r, s_, done)
    cum_reward += r
    s = s_

  agent.learn()
  agent.forget()

  dt = datetime.datetime.now() - start_time
  print(template.format(e+1, n_episodes, cum_reward, dt.total_seconds()), end='')

  reward_history[e] = cum_reward
```

    Episode: 300/300 | Reward: 200 | Duration: 4.90 s

```
plt.plot(reward_history[0:e], label='Reward')
plt.xlabel('Episodes')
plt.ylabel('Cumulative reward')
plt.tight_layout()
plt.show()
```

Empirical observations:

*   it is well known that policy gradient algorithms are noisy, and indeed we can observe it very clearly in the
*   increasing the learning rate accelerates learning (e.g. going from 0.001 to 0.01 more than halves the number of epochs required to reach the maximum score)
*   we can modify $J_t$ by subtracting a “baseline” so as to reduce the variance of the results; one very important baseline is such that $J_t$ becomes the so-called advantage, which forms the basis for actor-critic algorithms

## Trying it

```
env = wrap_env(gym.make("CartPole-v0"))
s = env.reset()
s = np.reshape(s, [1, dim_state_space])

done = False
cum_reward = 0

while not done:

  env.render()
  a = agent.policy(s)
  s_, r, done, _ = env.step(a)
  s_ = np.reshape(s_, [1, dim_state_space])
  agent.remember(s, a, r, s_, done)
  cum_reward += r
  s = s_

env.close()

print('We got a reward equal to {:.0f}'.format(cum_reward))
```

    We got a reward equal to 200

```
show_video()
```

```

```