Това е 6-та статия от тази поредица, в която се опитвам да прекодирам упражненията в (стария) курс за машинно обучение от Андрю Нг (където упражненията по програмиране се правят с помощта на Octave). Намерението ми да пиша тези статии е да помогна на обучаемите в този курс да използват Python като алтернатива, докато изпълняват упражненията. Моля, не се колебайте да разгледате и предишните части от тази поредица:
Част1: Модел на линейна регресия с една характеристика
Част2: Линейна регресия с множество функции
Част3: (Нерегулирана ) Модел на логистична регресия
Част 4: Регуляризирана логистична регресия
Част 5: Класификация едно срещу всички и невронна мрежа

В Част 5 ние вече сме внедрили предаване напред за нашата невронна мрежа, използвайки дадените обучени параметри (за theta1 и theta2). В това упражнение ще приложим алгоритъма за обратно разпространение, за да научим параметрите за невронната мрежа и да го използваме за прогнозиране на ръкописните цифри (същия набор от данни, който използвахме в предишното упражнение).

Наборът от данни съдържа 5000 примера за обучение на ръкописни цифри, като всеки пример за обучение е изображение на цифрата с размери 20 на 20 пиксела в сива скала. И така, нашият набор от данни за обучение е матрица 5000 на 400. Втората част от набора за обучение е 5000-измерен вектор y, който съдържа етикети за набора за обучение. Има общо 10 класа („1“, „2“, „3“, …., „10“). (Моля, обърнете внимание, че цифрата 0 е обозначена като „10“, докато цифрите от 1 до 9 са обозначени като „1“ до „9“ в техния естествен ред.) Моля, вижте Част 5 за зареждане на набора от данни и визуализирането му.

По-долу са основните стъпки, които ще приложим в това упражнение:
a) Ще изградим две функции: една за изчисляване на цената (с регулиране) с помощта на предаване напред и друга за изчисляване на градиента на тита с помощта на обратното разпространение.
b) За да сме сигурни, че нашето обратно разпространение е внедрено правилно, ще изградим функция за изчисляване на числения градиент (т.е. за изчисляване на градиента с помощта на „крайни разлики“, което ни дава числени оценки на градиентите) и ще я използваме за да проверим отново резултатите от нашия алгоритъм за обратно разпространение.
c) И накрая, ще използваме усъвършенстван метод за оптимизация, за да научим добър набор от параметри и да проверим точността на модела (на базата на набора за обучение).

Нека пропуснем визуализацията на дадения набор от данни (както вече го направихме в Част 5) и да преминем направо към представянето на нашия модел.

1. Представяне на модела

Подобно на нашия модел в част 5, ние ще използваме невронна мрежа от 3 слоя - входен слой, скрит слой и изходен слой. В нашия входен слой имаме общо 400 единици (с изключение на допълнителната единица за отклонение). Скритият слой има 25 единици (с изключение на единицата за отклонение), а изходният слой има 10 единици (съответстващи на 10-цифрени класове).

2. Регулирана функция на разходите

Функцията на разходите за нашата невронна мрежа (съставена от 400 единици във входния слой, 25 единици в скрития слой и 10 единици в изходния слой) с регуляризация се дава от:

, където m = 5000 (брой примери за обучение), K = 10 (общ брой възможни етикети).

Също така, моля, имайте предвид, че:
1) Не трябва да регулираме термините, които съответстват на отклонението. За Theta1 и Theta2 това съответства на първата колона на всяка матрица.
2) Оригиналните етикети (в променливата y) бяха 1,2,…,10. За целите на обучението на невронна мрежа трябва да прекодираме етикетите като вектори, съдържащи само стойности от 0 или 1. Например, ако X(i) е изображение на цифрата 5, тогава съответният y(i) трябва да бъде 10-измерен вектор с 1 в петия ред и други елементи, равни на 0.
3) В кода по-долу входът „para“ е векторът, получен от разгръщането на матриците за тегло Theta1 и Theta2.

def sigmoid(z):
    g = 1/(1 + np.exp(-z))
    return g
# NNCostFunc implements the neural network cost function for a 2 layer neural network which performs classification.
# This function will compute the cost.
def NNCostFunc(para, input_layer_size, hidden_layer_size, labels, x,y,lambdaa):
    Theta1 = para[0: hidden_layer_size * (input_layer_size+1)].reshape((hidden_layer_size, input_layer_size+1))
    
    Theta2 = para[hidden_layer_size*(input_layer_size+1)::].reshape((labels, hidden_layer_size+1))
    
#Theta1 will be (25x401) matrix and Theta2 will be (10x26) matrix
    
    m = y.size #no.of training examples
    
    # Forward Propagation
    y_recode = np.zeros((m, labels)) 
    for i in range(m):               
        y_recode[i, y[i]-1] = 1      
# recode the original y vector to a 5000x10 matrix with only 0s and 1s. 
                                     
# column index 0 will represent label 1, index 1 is label 2,..., index 9 is label 10 (which is typically 0)
        
    a1 = np.concatenate((np.ones((len(x),1)),x), axis=1) 
    z2 = np.dot(a1, np.transpose(Theta1))
    a2 = sigmoid(z2)
    a2 = np.concatenate((np.ones((m,1)), a2), axis=1)
    z3 = np.dot(a2, np.transpose(Theta2))
    a3 = sigmoid(z3)
    
    term1 = np.multiply(y_recode, np.log(a3))
    term2 = np.multiply((1-y_recode), np.log(1-a3))
    
    # regualarization term
    # here, we will not ignore the first columns of Theta1 and Theta2 which correspond to the bias terms.
    term3 = 0.5*lambdaa*(np.sum(np.sum(np.power(Theta1[:,1:],2))) + np.sum(np.sum(np.power(Theta2[:,1:],2)))) 
    J     = (np.sum((-term1 - term2)) + term3)/m
    return J

3. Обратно размножаване

Сега ще приложим алгоритъма за обратно разпространение, за да изчислим градиента за разходната функция на невронната мрежа.

По-долу са стъпките за алгоритъма за обратно разпространение:

  1. Като се има предвид пример за обучение (X(i), y(i)), първо ще изпълним преден пас, за да изчислим всички активации в цялата мрежа, включително изходната стойност на хипотезата h(x).
  2. За всяка изходна единица k в слой 3 (изходния слой), задайте δ(3) = a(3)-y.
  3. След това за всеки възел в скрития слой ще изчислим термин за грешка δ, който измерва доколко този възел е отговорен за всякакви грешки в нашия изход.
  4. Натрупайте градиентите, изчислени с помощта на формулата: (Имайте предвид, че трябва да пропуснем или премахнем δ за члена на отклонението в скрития слой.) Повторете стъпки от 1 до 4 за всички примери за обучение.

5. Получете (регуларизирания) градиент за функцията на цената на невронната мрежа, като използвате формулата:

# NNGradientFunc implements the neural network back propagation to compute the gradient of theta for a 2 layer neural network
def NNGradientFunc(para, input_layer_size, hidden_layer_size, labels, x,y,lambdaa):
    
    Theta1 = para[0: hidden_layer_size * (input_layer_size+1)].reshape((hidden_layer_size, input_layer_size+1))
    
    Theta2 = para[hidden_layer_size*(input_layer_size+1)::].reshape((labels, hidden_layer_size+1))
    
    Theta1_grad = np.zeros(Theta1.shape) 
    Theta2_grad = np.zeros(Theta2.shape) 
    
    m = y.size 
    n = x.shape[1] 
    y_recode = np.zeros((m, labels)) 
    for i in range(m):               
        y_recode[i, y[i]-1] = 1      
    
# Compute Gradient using Back Propagation
    for i in range(m):
        a_1 = np.transpose(x[i,:].reshape((1,n)))  
        a_1 = np.insert(a_1,0,1,axis=0)            
        z_2 = np.dot(Theta1, a_1)                  
        a_2 = sigmoid(z_2)
        a_2 = np.insert(a_2,0,1,axis=0)            
        z_3 = np.dot(Theta2, a_2)                  
        a_3 = sigmoid(z_3)
        
        y_i = np.transpose(y_recode[i,:].reshape(1,labels)) 
        
        delta3 = a_3 - y_i    
        delta2 = np.multiply(np.dot(np.transpose(Theta2),delta3), sigmoidGradient(np.insert(z_2,0,1,axis=0))) 
        delta2 = delta2[1:,:] # leave out the delta for the bias term. No need to calculate delta0.
        
        Theta2_grad += np.dot(delta3, np.transpose(a_2))   
        Theta1_grad += np.dot(delta2, np.transpose(a_1))   
        
        
    Theta1_grad = (1/m) * Theta1_grad 
    Theta2_grad = (1/m) * Theta2_grad 
    
    # Adding regularization term
    Theta1_grad[:,1:] += (lambdaa/m) * Theta1[:,1:] # for j>=1
    Theta2_grad[:,1:] += (lambdaa/m) * Theta2[:,1:] # for j>=1
    
    # unroll gradients
    Gradients = np.concatenate((Theta1_grad.ravel(), Theta2_grad.ravel()), axis=0)
        
    return Gradients

4. Проверка на градиента

Да предположим, че имаме функция f(ϴ), която уж изчислява частични производни на J(ϴ) w.r.t. ϴ; бихме искали да проверим дали f извежда правилни производни стойности. Ще проверим числено корекцията на f(ϴ), като проверим за всяко i, използвайки формулата по-долу:

където ϴ(i+) е мястото, където i-тият елемент е увеличен с ε, ϴ(i-) е мястото, където i-тият елемент е намалял с ε. (Като приемем, че ε = 0,0001, обикновено ще открием, че лявата и дясната страна на горното уравнение ще съответстват на поне 4-значещи цифри).

По-долу е кодът за изчисляване на числената оценка на градиента:

def NumericalGradient(para, input_layer_size, hidden_layer_size, labels, x,y,lambdaa):
    gradApprox = np.zeros((para.size))
    e = 1e-4
    l = lambdaa
    
    for i in range(para.size):
        thetaPlus = para.copy()
        thetaPlus[i] += e
        JPlus = NNCostFunc(thetaPlus, input_layer_size, hidden_layer_size, labels, x,y,l)
        
        thetaMinus = para.copy()
        thetaMinus[i] -= e
        JMinus = NNCostFunc(thetaMinus, input_layer_size, hidden_layer_size, labels, x,y,l)
        
        gradApprox[i] = (JPlus - JMinus)/ (2*e)  
         
    return gradApprox

Нека извършим нашата проверка на градиента, като сравним резултатите от нашите функции NNGradientFunc() и NumericalGradient(). Тук, за по-добра ефективност, ще използваме малка невронна мрежа, която взема произволно подмножество от X и y и инициализира произволните тегла за theta1 и theta2.

# This function randomly initializes the weights of a layer with L_in incoming connections and L_out outgoing connections.
def randInitialWeights(L_in, L_out):
    epsilon = 0.12
    weights = np.random.random((L_out, L_in+1)) * 2 * epsilon - epsilon
    return weights
# This function creates a small neural network to check the backpropagation gradients.
# It will output the analytical gradients produced by our backprop code and the numerical gradients (computed using NumericalGradient() function)
# These two gradient computations should result in very similar values.
def checkNNGradients(lambdaa):
    input_layer_size = 3
    hidden_layer_size = 5
    labels = 3
    m = 5
    
    theta1 = randInitialWeights(input_layer_size, hidden_layer_size) 
    theta2 = randInitialWeights(hidden_layer_size, labels)           
    
    # Random subset of data from X and y
    idx = np.random.randint(5000, size = 5)
    xx = X[idx, 0:3]
    yy = 1 + np.transpose(np.mod([i for i in range(m)], labels)) 
    
    # unroll parameters
    parameters = np.concatenate((theta1.ravel(), theta2.ravel()), axis=0)
    
    grad       = NNGradientFunc(parameters, input_layer_size, hidden_layer_size, labels, xx,yy,lambdaa)
    numgrad    = NumericalGradient(parameters, input_layer_size, hidden_layer_size, labels, xx,yy,lambdaa)
    
    # The two columns printed should be very similar
    print("\n".join("{:.5f}  ,{:.5f}".format(x, y) for x, y in zip(numgrad, grad)))
    print("\nLeft- Your Numerical Gradient, Right- Analytical Gradient")
    
    diff = np.linalg.norm(grad-numgrad)/ (np.linalg.norm(numgrad) + np.linalg.norm(grad))
    print("\nIf the backpropagation implementation is correct, then\n the relative difference will be small (less than 1e-9).\n Relative Difference : {}\n".format(diff))

# checking the two gradients using lambda = 1
checkNNGradients(1)

Тъй като theta1 е (5,4) матрица, а theta2 е (3,6) матрица, изходните разгънати параметри ще имат общо 38 стойности. Моментна снимка на изхода е следната:

Поздравления! Нашето обратно разпространение е внедрено правилно и сега можем да го използваме за обучение на нашите параметри със спокойствие.

5. Параметри на обучение

Ще използваме усъвършенстван метод за оптимизация fmin_cg(), за да научим параметрите и да ги използваме, за да проверим точността на нашия модел с помощта на набора за обучение. Тук съм ограничил параметъра maxiter до 50 (и получавам точност на обучение от 95,3%). Възможно е да се получат по-високи точности на обучение чрез обучение на невронната мрежа с повече итерации (ако използваме lambda = 1 и maxiter = 100, можем да получим 98,6% точност).

import scipy.optimize as opt
input_layer = 400
hidden_layer = 25
labels = 10
lambdaa = 1
theta_ = opt.fmin_cg(NNCostFunc, x0=initial_parameters, args = (input_layer,hidden_layer,labels,X,y,lambdaa), fprime = NNGradientFunc,maxiter = 50) # Taking so long
# Reshaping
Theta1_ = theta_[0: hidden_layer * (input_layer+1)].reshape((hidden_layer, input_layer+1))
Theta2_ = theta_[hidden_layer*(input_layer+1)::].reshape((labels, hidden_layer+1))
# Prediction
def predict(t1,t2,x):
    m = x.shape[0]
    labels = t2.shape[0]
    
    a1 = np.concatenate((np.ones((len(x),1)),x), axis=1)
    z2 = np.dot(a1, np.transpose(t1))
    a2 = sigmoid(z2)
    a2 = np.concatenate((np.ones((m,1)), a2), axis=1)
    z3 = np.dot(a2, np.transpose(t2))
    a3 = sigmoid(z3)
    
    hmax    = np.amax(a3, axis=1)   
    prediction = np.argmax(a3, axis =1 )+1  
    prediction = prediction.reshape((prediction.shape[0],1)) 
    
    return prediction
pred = predict(Theta1_, Theta2_,X)
print("Training set accuracy : {}%".format(np.mean((pred == y).astype(float))*100))

Честито още веднъж! Ние успешно внедрихме модел на невронна мрежа за етикетиране на ръкописни цифри и ни дава добра точност. Продължавайте да очаквате с нетърпение част 7, където ще обсъдим компромиса между отклонение и дисперсия, използвайки модел на линейна регресия.

Продължавай да учиш. Насладете се на пътуването!