!pip install Theano
!pip install lasagne

import numpy as np
def sum_squares(N):
    return сумма квадратов чисел от 0 до N

%%time
sum_squares(10**8)

"""
Explanation: Theano, Lasagne
и с чем их едят
разминка

напиши на numpy функцию, которая считает сумму квадратов чисел от 0 до N, где N - аргумент
массив чисел от 0 до N - numpy.arange(N)
End of explanation
"""


import theano
import theano.tensor as T



#будущий параметр функции
N = T.scalar("a dimension",dtype='int32')


#рецепт получения суммы квадратов
result = (T.arange(N)**2).sum()

#компиляция функции "сумма квадратов" чисел от 0 до N
sum_function = theano.function(inputs = [N],outputs=result)

%%time
sum_function(10**8)

"""
Explanation: theano teaser
Как сделать то же самое
End of explanation
"""


#входы
example_input_integer = T.scalar("вход - одно число(пример)",dtype='float32')

example_input_tensor = T.tensor4("вход - четырёхмерный тензор(пример)")
#не бойся, тензор нам не пригодится



input_vector = T.vector("вход - вектор целых чисел", dtype='int32')


#преобразования

#поэлементное умножение
double_the_vector = input_vector*2

#поэлементный косинус
elementwise_cosine = T.cos(input_vector)

#разность квадрата каждого элемента и самого элемента
vector_squares = input_vector**2 - input_vector


double_the_vector

#теперь сам:
#создай 2 вектора из чисел float32
my_vector = <вектор из float32>
my_vector2 = <ещё один такой же>

#напиши преобразование, которое считает
#(вектор 1)*(вектор 2) / (sin(вектор 1) +1)
my_transformation = <преобразование>

print (my_transformation)
#то, что получилась не чиселка - это нормально

"""
Explanation: Как оно работает?

Нужно написать "рецепт" получения выходов по входам

То же самое на заумном: нужно описать символический граф вычислений


2 вида зверей - "входы" и "преобразования"


Оба могут быть числами, массивами, матрицами, тензорами и т.п.


Вход - это то аргумент функции. То место, на которое подставится аргумент вызове.


N - вход в примере выше


Преобразования - рецепты вычисления чего-то на основе входов и констант

(T.arange(N)^2).sum() - 3 последовательных преобразования N
Работают почти 1 в 1 как векторные операции в numpy
почти всё, что есть в numpy есть в theano tensor и называется так же
np.mean -> T.mean
np.arange -> T.arange
np.cumsum -> T.cumsum
и так далее...
Совсем редко - бывает, что меняется название или синтаксис - нужно спросить у семинаристов или гугла

Ничего не понятно? Сейчас исправим.
End of explanation
"""


inputs = [<от чего завсит функция>]
outputs = [<что вычисляет функция (можно сразу несколько - списком, либо 1 преобразование)>]

# можно скомпилировать написанные нами преобразования как функцию
my_function = theano.function(
    inputs,outputs,
    allow_input_downcast=True #автоматически прводить типы (необязательно)
 )

#можно вызвать вот-так:
print ("using python lists:")
print (my_function([1,2,3],[4,5,6]))
print()

#а можно так. 
#К слову, ту тип float приводится к типу второго вектора
print ("using numpy arrays:")
print (my_function(np.arange(10),
                  np.linspace(5,6,10,dtype='float')))


"""
Explanation: Компиляция

До этого момента, мы использовали "символические" переменные
писали рецепт вычислений, но ничего не вычисляли
чтобы рецепт можно было использовать, его нужно скомпилировать
End of explanation
"""


#словарик значений для входов
my_function_inputs = {
    my_vector:[1,2,3],
    my_vector2:[4,5,6]
}

#вычислить без компиляции
#если мы ничего не перепутали, 
#должно получиться точно то же, что и раньше
print my_transformation.eval(my_function_inputs)


#можно вычислять преобразования на ходу
print ("сумма 2 векторов", (my_vector + my_vector2).eval(my_function_inputs))

#!ВАЖНО! если преобразование зависит только от части переменных,
#остальные давать не надо
print ("форма первого вектора", my_vector.shape.eval({
        my_vector:[1,2,3]
    }))


"""
Explanation: хинт для отладки

Если ваша функция большая, компиляция может отнять какое-то время.
Чтобы не ждать, можно посчитать выражение без компиляции
Вы экономите время 1 раз на компиляции, но сам код выполняется медленнее
End of explanation
"""


# Задание 1 - напиши и скомпилируй theano-функцию, которая считает среднеквадратичную ошибку двух векторов-входов
# Вернуть нужно одно число - собственно, ошибку. Обновлять ничего не нужно

<твой код - входы и преобразования>

compute_mse =<твой код - компиляция функции>

#тесты
from sklearn.metrics import mean_squared_error

for n in [1,5,10,10**3]:
    
    elems = [np.arange(n),np.arange(n,0,-1), np.zeros(n),
             np.ones(n),np.random.random(n),np.random.randint(100,size=n)]
    
    for el in elems:
        for el_2 in elems:
            true_mse = np.array(mean_squared_error(el,el_2))
            my_mse = compute_mse(el,el_2)
            if not np.allclose(true_mse,my_mse):
                print ('Wrong result:')
                print ('mse(%s,%s)'%(el,el_2))
                print ("should be: %f, but your function returned %f"%(true_mse,my_mse))
                raise ValueError("Что-то не так")

print ("All tests passed")
            
    

"""
Explanation: Для отладки желательно уменьшить масштаб задачи. Если вы планировали послать на вход вектор из 10^9 примеров, пошлите 10~100.
Если #ОЧЕНЬ нужно послать большой вектор, быстрее скомпилировать функцию обычным способом

Теперь сам: MSE (2 pts)
End of explanation
"""


#cоздадим расшаренную перменную
shared_vector_1 = theano.shared(np.ones(10,dtype='float64'))


#получить (численное) значение переменной
print ("initial value",shared_vector_1.get_value())

#задать новое значение
shared_vector_1.set_value( np.arange(5) )

#проверим значение
print ("new value", shared_vector_1.get_value())

#Заметь, что раньше это был вектор из 10 элементов, а сейчас - из 5. 
#Если граф при этом остался выполним, это сработает.

"""
Explanation: Shared variables

Входы и преобразования - части рецепта. 

Они существуют только во время вызова функции.


Shared переменные - всегда остаются в памяти

им можно поменять значение 
(но не внутри символического графа. Об этом позже)

их можно включить в граф вычислений


хинт - в таких переменных удобно хранить параметры и гиперпараметры

например, веса нейронки или learning rate, если вы его меняете
End of explanation
"""


#напиши рецепт (преобразование), которое считает произведение(поэллементное) shared_vector на input_scalar
#скомпилируй это в функцию от input_scalar

input_scalar = T.scalar('coefficient',dtype='float32')

scalar_times_shared = <рецепт тут>


shared_times_n = <твой код, который компилирует функцию>


print ("shared:", shared_vector_1.get_value())

print ("shared_times_n(5)",shared_times_n(5))

print ("shared_times_n(-0.5)",shared_times_n(-0.5))


#поменяем значение shared_vector_1
shared_vector_1.set_value([-1,0,1])
print ("shared:", shared_vector_1.get_value())

print ("shared_times_n(5)",shared_times_n(5))

print ("shared_times_n(-0.5)",shared_times_n(-0.5))


"""
Explanation: Теперь сам
End of explanation
"""


my_scalar = T.scalar(name='input',dtype='float64')

scalar_squared = T.sum(my_scalar**2)

#производная v_squared по my_vector
derivative = T.grad(scalar_squared,my_scalar)

fun = theano.function([my_scalar],scalar_squared)
grad = theano.function([my_scalar],derivative) 

import matplotlib.pyplot as plt
%matplotlib inline


x = np.linspace(-3,3)
x_squared = list(map(fun,x))
x_squared_der = list(map(grad,x))

plt.plot(x, x_squared,label="x^2")
plt.plot(x, x_squared_der, label="derivative")
plt.legend()

"""
Explanation: T.grad, самое вкусное

theano умеет само считать производные. Все, которые существуют.
Производные считаются в символическом, а не численном виде

Ограничения
* За раз можно считать производную скалярной функции по одной или нескольким скалярным или векторным аргументам
* Функция должна на всех этапах своего вычисления иметь тип float32 или float64 (т.к. на множестве целых чисел производная не имеет смысл)
End of explanation
"""



my_vector = T.vector('float64')

#посчитай производные этой функции по my_scalar и my_vector
#warning! Не пытайся понять физический смысл этой функции
weird_psychotic_function = ((my_vector+my_scalar)**(1+T.var(my_vector)) +1./T.arcsinh(my_scalar)).mean()/(my_scalar**2 +1) + 0.01*T.sin(2*my_scalar**1.5)*(T.sum(my_vector)* my_scalar**2)*T.exp((my_scalar-4)**2)/(1+T.exp((my_scalar-4)**2))*(1.-(T.exp(-(my_scalar-4)**2))/(1+T.exp(-(my_scalar-4)**2)))**2


der_by_scalar,der_by_vector = градиент функции сверху по скаляру и вектору (можно дать списком)


compute_weird_function = theano.function([my_scalar,my_vector],weird_psychotic_function)
compute_der_by_scalar = theano.function([my_scalar,my_vector],der_by_scalar)


#график функции и твоей производной
vector_0 = [1,2,3]

scalar_space = np.linspace(0,7)

y = [compute_weird_function(x,vector_0) for x in scalar_space]
plt.plot(scalar_space,y,label='function')
y_der_by_scalar = [compute_der_by_scalar(x,vector_0) for x in scalar_space]
plt.plot(scalar_space,y_der_by_scalar,label='derivative')
plt.grid();plt.legend()


"""
Explanation: теперь сам
End of explanation
"""


#умножим shared вектор на число и сохраним новое значение обратно в этот shared вектор

inputs = [input_scalar]
outputs = [scalar_times_shared] #вернём вектор, умноженный на число

my_updates = {
    shared_vector_1:scalar_times_shared #и этот же результат запишем в shared_vector_1
}

compute_and_save = theano.function(inputs, outputs, updates=my_updates)

shared_vector_1.set_value(np.arange(5))

#изначальное значение shared_vector_1
print ("initial shared value:" ,shared_vector_1.get_value())

# теперь вычислим функцию (значение shared_vector_1 при этом поменяется)
print ("compute_and_save(2) returns",compute_and_save(2))

#проверим, что в shared_vector_1
print ("new shared value:" ,shared_vector_1.get_value())



"""
Explanation: Последний штрих - Updates


updates - это способ изменять значения shared переменных каждый раз В КОНЦЕ вызова функции


фактически, это словарь {shared_переменная: рецепт нового значения}, который добавляется в функцию при компиляции


Например,
End of explanation
"""


from sklearn.datasets import load_digits
mnist = load_digits(2)

X,y = mnist.data, mnist.target


print ("y [форма - %s]:"%(str(y.shape)),y[:10])

print ("X [форма - %s]:"%(str(X.shape)))
print (X[:3])

# переменные и входы
shared_weights = <твой код>
input_X = <твой код>
input_y = <твой код>

predicted_y = <предсказание логрегрессии на input_X (вероятность класса)>
loss = <логистическая ошибка (число - среднее по выборке)>

grad = <градиент loss по весам модели>



updates = {
    shared_weights: <новое значение весов после шага градиентного спуска>
}

train_function = <функция, которая по X и Y возвращает ошибку и обновляет веса>
predict_function = <функция, которая по X считает предсказание для y>

from sklearn.cross_validation import train_test_split
X_train,X_test,y_train,y_test = train_test_split(X,y)

from sklearn.metrics import roc_auc_score

for i in range(5):
    loss_i = train_function(X_train,y_train)
    print ("loss at iter %i:%.4f"%(i,loss_i))
    print ("train auc:",roc_auc_score(y_train,predict_function(X_train)))
    print ("test auc:",roc_auc_score(y_test,predict_function(X_test)))

    
print ("resulting weights:")
plt.imshow(shared_weights.get_value().reshape(8,-1))
plt.colorbar()

"""
Explanation: Логистическая регрессия
Что нам потребуется:
* Веса лучше хранить в shared-переменной
* Данные можно передавать как input
* Нужно 2 функции:
 * train_function(X,y) - возвращает ошибку и изменяет веса на 1 шаг по граиденту (через updates)
 * predict_fun(X) - возвращает предсказанные ответы ("y") по данным
End of explanation
"""


from mnist import load_dataset
X_train,y_train,X_val,y_val,X_test,y_test = load_dataset()

print (X_train.shape,y_train.shape)

plt.imshow(X_train[0,0])

import lasagne

input_X = T.tensor4("X")

#размерность входа (None означает "может изменяться")
input_shape = [None,1,28,28]

target_y = T.vector("target Y integer",dtype='int32')

"""
Explanation: lasagne

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

В качестве демо-задачи выберем то же распознавание чисел, но на большем масштабе задачи
* картинки 28x28
* 10 цифр
End of explanation
"""


#входной слой (вспомогательный)
input_layer = lasagne.layers.InputLayer(shape = input_shape,input_var=input_X)

#полносвязный слой, который принимает на вход input layer и имеет 100 нейронов.
# нелинейная функция - сигмоида как в логистической регрессии
# слоям тоже можно давать имена, но это необязательно
dense_1 = lasagne.layers.DenseLayer(input_layer,num_units=50,
                                   nonlinearity = lasagne.nonlinearities.sigmoid,
                                   name = "hidden_dense_layer")

#ВЫХОДНОЙ полносвязный слой, который принимает на вход dense_1 и имеет 10 нейронов -по нейрону на цифру
#нелинейность - softmax - чтобы вероятности всех цифр давали в сумме 1
dense_output = lasagne.layers.DenseLayer(dense_1,num_units = 10,
                                        nonlinearity = lasagne.nonlinearities.softmax,
                                        name='output')


#предсказание нейронки (theano-преобразование)
y_predicted = lasagne.layers.get_output(dense_output)

#все веса нейронки (shared-переменные)
all_weights = lasagne.layers.get_all_params(dense_output)
print (all_weights)

"""
Explanation: Так задаётся архитектура нейронки
End of explanation
"""


#функция ошибки - средняя кроссэнтропия
loss = lasagne.objectives.categorical_crossentropy(y_predicted,target_y).mean()


accuracy = lasagne.objectives.categorical_accuracy(y_predicted,target_y).mean()

#сразу посчитать словарь обновлённых значений с шагом по градиенту, как раньше
updates_sgd = lasagne.updates.rmsprop(loss, all_weights,learning_rate=0.01)

#функция, которая обучает сеть на 1 шаг и возвращащет значение функции потерь и точности
train_fun = theano.function([input_X,target_y],[loss,accuracy],updates= updates_sgd)

#функция, которая считает точность
accuracy_fun = theano.function([input_X,target_y],accuracy)

"""
Explanation: дальше вы могли бы просто

задать функцию ошибки вручную
посчитать градиент ошибки по all_weights
написать updates
но это долго, а простой шаг по градиенту - не самый лучший смособ оптимизировать веса

Вместо этого, опять используем lasagne
End of explanation
"""


# вспомогательная функция, которая возвращает список мини-батчей для обучения нейронки

#на вход
# X - тензор из картинок размером (много, 1, 28, 28), например - X_train
# y - вектор из чиселок - ответов для каждой картинки из X; например - Y_train
#batch_size - одно число - желаемый размер группы

#что нужно сделать
# 1) перемешать данные
# - важно перемешать X и y одним и тем же образом, чтобы сохранить соответствие картинки ответу на неё
# 3) побить данные на подгруппы так, чтобы в каждой подгруппе было batch_size картинок и ответов
# - если число картинок не делится на batch_size, одну подгруппу можно вернуть другого размера
# 4) вернуть список (или итератор) пар:
# - (подгруппа картинок, ответы из y на эту подгруппу)
def iterate_minibatches(X, y, batchsize):
    
    
    
    
    
    return X_minibatches, Y_minibatches # можно сделать списки, а ещё лучше - генератором через yield 
    
    
    
    
        
        
#
#
#
#
#
#
#
# Всё плохо и ты не понимаешь, что от тебя хотят?
# можешь поискать похожую функцию в примере
# https://github.com/Lasagne/Lasagne/blob/master/examples/mnist.py

"""
Explanation: Вот и всё, пошли её учить

данных теперь много, поэтому лучше учиться стохастическим градиентным спуском
для этого напишем функцию, которая бьёт выпорку на мини-батчи (в обычном питоне, не в theano)
End of explanation
"""


import time

num_epochs = 100 #количество проходов по данным

batch_size = 50 #размер мини-батча

for epoch in range(num_epochs):
    # In each epoch, we do a full pass over the training data:
    train_err = 0
    train_acc = 0
    train_batches = 0
    start_time = time.time()
    for batch in iterate_minibatches(X_train, y_train,batch_size):
        inputs, targets = batch
        train_err_batch, train_acc_batch= train_fun(inputs, targets)
        train_err += train_err_batch
        train_acc += train_acc_batch
        train_batches += 1

    # And a full pass over the validation data:
    val_acc = 0
    val_batches = 0
    for batch in iterate_minibatches(X_val, y_val, batch_size):
        inputs, targets = batch
        val_acc += accuracy_fun(inputs, targets)
        val_batches += 1

    
    # Then we print the results for this epoch:
    print("Epoch {} of {} took {:.3f}s".format(
        epoch + 1, num_epochs, time.time() - start_time))

    print("  training loss (in-iteration):\t\t{:.6f}".format(train_err / train_batches))
    print("  train accuracy:\t\t{:.2f} %".format(
        train_acc / train_batches * 100))
    print("  validation accuracy:\t\t{:.2f} %".format(
        val_acc / val_batches * 100))

test_acc = 0
test_batches = 0
for batch in iterate_minibatches(X_test, y_test, 500):
    inputs, targets = batch
    acc = accuracy_fun(inputs, targets)
    test_acc += acc
    test_batches += 1
print("Final results:")
print("  test accuracy:\t\t{:.2f} %".format(
    test_acc / test_batches * 100))

if test_acc / test_batches * 100 > 99:
    print ("Achievement unlocked: колдун 80 уровня")
else:
    print ("Нужно больше магии!")

"""
Explanation: Процесс обучения
End of explanation
"""


from mnist import load_dataset
X_train,y_train,X_val,y_val,X_test,y_test = load_dataset()

print (X_train.shape,y_train.shape)

import lasagne

input_X = T.tensor4("X")

#размерность входа (None означает "может изменяться")
input_shape = [None,1,28,28]

target_y = T.vector("target Y integer",dtype='int32')

#входной слой (вспомогательный)
input_layer = lasagne.layers.InputLayer(shape = input_shape,input_var=input_X)


<моя архитектура>

#ВЫХОДНОЙ полносвязный слой, который принимает на вход dense_1 и имеет 10 нейронов -по нейрону на цифру
#нелинейность - softmax - чтобы вероятности всех цифр давали в сумме 1
dense_output = lasagne.layers.DenseLayer(<предвыходной_слой>,num_units = 10,
                                        nonlinearity = lasagne.nonlinearities.softmax,
                                        name='output')


#предсказание нейронки (theano-преобразование)
y_predicted = lasagne.layers.get_output(dense_output)

#все веса нейронки (shared-переменные)
all_weights = lasagne.layers.get_all_params(dense_output)
print (all_weights)

#функция ошибки - средняя кроссэнтропия
loss = lasagne.objectives.categorical_crossentropy(y_predicted,target_y).mean()

#<возможно добавить регуляризатор>

accuracy = lasagne.objectives.categorical_accuracy(y_predicted,target_y).mean()

#сразу посчитать словарь обновлённых значений с шагом по градиенту, как раньше
updates_sgd = <поиграться с методами>

#функция, которая обучает сеть на 1 шаг и возвращащет значение функции потерь и точности
train_fun = theano.function([input_X,target_y],[loss,accuracy],updates= updates_sgd)

#функция, которая считает точность
accuracy_fun = theano.function([input_X,target_y],accuracy)

#итерации обучения

num_epochs = сколько_эпох #количество проходов по данным

batch_size = сколько_картинок_в_минибатче #размер мини-батча

for epoch in range(num_epochs):
    # In each epoch, we do a full pass over the training data:
    train_err = 0
    train_acc = 0
    train_batches = 0
    start_time = time.time()
    for batch in iterate_minibatches(X_train, y_train,batch_size):
        inputs, targets = batch
        train_err_batch, train_acc_batch= train_fun(inputs, targets)
        train_err += train_err_batch
        train_acc += train_acc_batch
        train_batches += 1

    # And a full pass over the validation data:
    val_acc = 0
    val_batches = 0
    for batch in iterate_minibatches(X_val, y_val, batch_size):
        inputs, targets = batch
        val_acc += accuracy_fun(inputs, targets)
        val_batches += 1

    
    # Then we print the results for this epoch:
    print("Epoch {} of {} took {:.3f}s".format(
        epoch + 1, num_epochs, time.time() - start_time))

    print("  training loss (in-iteration):\t\t{:.6f}".format(train_err / train_batches))
    print("  train accuracy:\t\t{:.2f} %".format(
        train_acc / train_batches * 100))
    print("  validation accuracy:\t\t{:.2f} %".format(
        val_acc / val_batches * 100))

test_acc = 0
test_batches = 0
for batch in iterate_minibatches(X_test, y_test, 500):
    inputs, targets = batch
    acc = accuracy_fun(inputs, targets)
    test_acc += acc
    test_batches += 1
print("Final results:")
print("  test accuracy:\t\t{:.2f} %".format(
    test_acc / test_batches * 100))



"""
Explanation: Нейронка твоей мечты

Задача - сделать нейронку, которая получит точность 99% на валидации (validation accuracy)
+1 балл за каждые 0.1% сверх 99%
Вариант "is fine too" - 97.5%. 
Чем выше, тем лучше.

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

размер сети
бОльше нейронов, 
бОльше слоёв, 
почти наверняка нужны свёртки

Пх'нглуи мглв'нафх Ктулху Р'льех вгах'нагл фхтагн! 


регуляризация - чтобы не переобучалось

приплюсовать к функции ошибки какую-нибудь сумму квадратов весов

можно сделать вручную, а можно - http://lasagne.readthedocs.org/en/latest/modules/regularization.html


Метод оптимизации - rmsprop, nesterov_momentum, adadelta, adagrad и т.п.

сходятся быстрее и иногда - к лучшему оптимуму

имеет смысл поиграть с размером батча, количеством эпох и скоростью обучения


Dropout - для борьбы с переобучением


lasagne.layers.DropoutLayer(предыдущий_слой, p=вероятность_занулить)


Свёрточные слои 

network = lasagne.layers.Conv2DLayer(предыдущий_слой,
    num_filters = число нейронов,
    filter_size = (ширина_квадрата, высота_квадрата),
    nonlinearity = нелинейная_функция)

ВАРНУНГ! могут учиться долго на CPU

Однако мы всё равно рекоммендуем обучить хотя бы маленькую свёртку



Любые другие слои и архитектуры

http://lasagne.readthedocs.org/en/latest/modules/layers.html

Pooling, Batch Normalization, etc


Наконец, можно поиграть с нелинейностями в скрытых слоях

tanh, relu, leaky relu, etc

Для удобства, ниже есть заготовка решения, которое можно заполнять, а можно выкинуть и написать своё
End of explanation
"""


