В этой статье будут рассмотрены задания 3 и 4 по программированию нейронных сетей из курса машинного обучения Эндрю Нга. Это также первые сложные нелинейные алгоритмы, с которыми мы столкнулись в этом курсе. Не знаю, как вы, но мне определенно нужно научиться этому заданию. Нейронная сеть составляет основу глубокого обучения, которое имеет широкое применение, например, компьютерное зрение или обработка естественного языка. Таким образом, важно получить фундаментальные права, и кодирование этих назначений на Python - один из способов гарантировать это.

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

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

Загрузка набора данных

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.io import loadmat
# Use loadmat to load matlab files
mat=loadmat("ex3data1.mat")
X=mat["X"]
y=mat["y"]

Поскольку набор данных был предоставлен в формате .mat вместо обычного формата .txt, мне пришлось использовать функцию scipy loadmat для работы. Официальную документацию можно найти здесь. Поскольку loadmat загружает файлы .mat как словарь с именами переменных в качестве ключей, назначить X и y так же просто, как получить доступ к dict с помощью ключей переменных.

X.shape, y.shape

Чтобы лучше понять набор данных, форма данных сообщает нам размер данных. X имеет форму 5000,400, что соответствует 5000 обучающим примерам, каждый из которых имеет 400 функций из своего пикселя 20 X 20. y имеет форму 5000,1, в которой каждый обучающий пример имеет метку в диапазоне от 1 до 10 (цифра «0» помечена как «10» в этом наборе данных).

Визуализация данных

import matplotlib.image as mpimg
fig, axis = plt.subplots(10,10,figsize=(8,8))
for i in range(10):
    for j in range(10):
        axis[i,j].imshow(X[np.random.randint(0,5001),:].reshape(20,20,order="F"), cmap="hot") #reshape back to 20 pixel by 20 pixel
        axis[i,j].axis("off")

Приведенный выше блок кода создает 100 подзаголовков и случайным образом визуализирует 100 из 5000 обучающих примеров с помощью plt.imshow. Обратите внимание, что мы должны изменить форму обучающего примера обратно на 20 X 20 пикселей, прежде чем мы сможем его визуализировать, и добавив order="F" в качестве параметра в функцию изменения формы, чтобы обеспечить вертикальную ориентацию изображения.

Вычисление функции стоимости и градиента

def sigmoid(z):
    """
    return the sigmoid of z
    """
    
    return 1/ (1 + np.exp(-z))
def lrCostFunction(theta, X, y, Lambda):
    """
    Takes in numpy array of theta, X, y, and float lambda to compute the regularized logistic cost function 
    """
    
    m=len(y)
    predictions = sigmoid(X @ theta)
    error = (-y * np.log(predictions)) - ((1-y)*np.log(1-predictions))
    cost = 1/m * sum(error)
    regCost= cost + Lambda/(2*m) * sum(theta[1:]**2)
    
    # compute gradient
    j_0= 1/m * (X.transpose() @ (predictions - y))[0]
    j_1 = 1/m * (X.transpose() @ (predictions - y))[1:] + (Lambda/m)* theta[1:]
    grad= np.vstack((j_0[:,np.newaxis],j_1))
    return regCost[0], grad

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

theta_t = np.array([-2,-1,1,2]).reshape(4,1)
X_t =np.array([np.linspace(0.1,1.5,15)]).reshape(3,5).T
X_t = np.hstack((np.ones((5,1)), X_t))
y_t = np.array([1,0,1,0,1]).reshape(5,1)
J, grad = lrCostFunction(theta_t, X_t, y_t, 3)
print("Cost:",J,"Expected cost: 2.534819")
print("Gradients:\n",grad,"\nExpected gradients:\n 0.146561\n -0.548558\n 0.724722\n 1.398003")

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

def gradientDescent(X,y,theta,alpha,num_iters,Lambda):
    """
    Take in numpy array X, y and theta and update theta by taking num_iters gradient steps
    with learning rate of alpha
    
    return theta and the list of the cost of theta during each iteration
    """
    
    m=len(y)
    J_history =[]
    
    for i in range(num_iters):
        cost, grad = lrCostFunction(theta,X,y,Lambda)
        theta = theta - (alpha * grad)
        J_history.append(cost)
    
    return theta , J_history

def oneVsAll(X, y, num_labels, Lambda):
    """
    Takes in numpy array of X,y, int num_labels and float lambda to train multiple logistic regression classifiers
    depending on the number of num_labels using gradient descent. 
    
    Returns a matrix of theta, where the i-th row corresponds to the classifier for label i
    """
    m, n = X.shape[0], X.shape[1]
    initial_theta = np.zeros((n+1,1))
    all_theta = []
    all_J=[]
    # add intercept terms
    
    X = np.hstack((np.ones((m,1)),X))
    
    for i in range(1,num_labels+1):
        theta , J_history = gradientDescent(X,np.where(y==i,1,0),initial_theta,1,300,Lambda)
        all_theta.extend(theta)
        all_J.extend(J_history)
    return np.array(all_theta).reshape(num_labels,n+1), all_J

Функция gradientDescent - это обычная функция оптимизации, которую мы реализовали ранее. Что касается oneVsAll, он выполняет итерацию по всем классам и обучал набор тета для каждого класса с помощью градиентного спуска (в назначении использовалась функция fmincg). all_theta затем захватывает всю оптимизированную тэту в списке и возвращает в виде массива numpy, преобразованного в матрицу тэты, где i-я строка соответствует классификатору для метки i. np.where здесь пригодится, чтобы получить вектор y с 1/0 для каждого класса для выполнения нашей задачи двоичной классификации в каждой итерации.

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

plt.plot(all_J[0:300])
plt.xlabel("Iteration")
plt.ylabel("$J(\Theta)$")
plt.title("Cost function using Gradient Descent")

Чтобы сделать прогноз, была вычислена вероятность x (i) для каждого класса, и прогноз - это класс с наибольшей вероятностью.

def predictOneVsAll(all_theta, X):
    """
    Using all_theta, compute the probability of X(i) for each class and predict the label
    
    return a vector of prediction
    """
    m= X.shape[0]
    X = np.hstack((np.ones((m,1)),X))
    
    predictions = X @ all_theta.T
    return np.argmax(predictions,axis=1)+1
pred = predictOneVsAll(all_theta, X)
print("Training Set Accuracy:",sum(pred[:,np.newaxis]==y)[0]/5000*100,"%")

Оператор печати print: Training Set Accuracy: 91.46 %

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

Загрузка оптимизированной теты

mat2=loadmat("ex3weights.mat")
Theta1=mat2["Theta1"] # Theta1 has size 25 x 401
Theta2=mat2["Theta2"] # Theta2 has size 10 x 26

Использование прямого распространения для прогнозирования

def predict(Theta1, Theta2, X):
    """
    Predict the label of an input given a trained neural network
    """
    m= X.shape[0]
    X = np.hstack((np.ones((m,1)),X))
    
    a1 = sigmoid(X @ Theta1.T)
    a1 = np.hstack((np.ones((m,1)), a1)) # hidden layer
    a2 = sigmoid(a1 @ Theta2.T) # output layer
    
    return np.argmax(a2,axis=1)+1
pred2 = predict(Theta1, Theta2, X)
print("Training Set Accuracy:",sum(pred2[:,np.newaxis]==y)[0]/5000*100,"%")

Заявление о печати print: Training Set Accuracy: 97.52 %. Гораздо более высокая точность по сравнению с многоклассовой логистической регрессией!

В задании 4 мы работали над созданием нейронной сети с нуля. Начнем с вычисления функции стоимости и градиента теты.

def sigmoidGradient(z):
    """
    computes the gradient of the sigmoid function
    """
    sigmoid = 1/(1 + np.exp(-z))
    
    return sigmoid *(1-sigmoid)

def nnCostFunction(nn_params,input_layer_size, hidden_layer_size, num_labels,X, y,Lambda):
    """
    nn_params contains the parameters unrolled into a vector
    
    compute the cost and gradient of the neural network
    """
    # Reshape nn_params back into the parameters Theta1 and Theta2
    Theta1 = nn_params[:((input_layer_size+1) * hidden_layer_size)].reshape(hidden_layer_size,input_layer_size+1)
    Theta2 = nn_params[((input_layer_size +1)* hidden_layer_size ):].reshape(num_labels,hidden_layer_size+1)
    
    m = X.shape[0]
    J=0
    X = np.hstack((np.ones((m,1)),X))
    y10 = np.zeros((m,num_labels))
    
    a1 = sigmoid(X @ Theta1.T)
    a1 = np.hstack((np.ones((m,1)), a1)) # hidden layer
    a2 = sigmoid(a1 @ Theta2.T) # output layer
    
    for i in range(1,num_labels+1):
        y10[:,i-1][:,np.newaxis] = np.where(y==i,1,0)
    for j in range(num_labels):
        J = J + sum(-y10[:,j] * np.log(a2[:,j]) - (1-y10[:,j])*np.log(1-a2[:,j]))
    
    cost = 1/m* J
    reg_J = cost + Lambda/(2*m) * (np.sum(Theta1[:,1:]**2) + np.sum(Theta2[:,1:]**2))
    
    # Implement the backpropagation algorithm to compute the gradients
    
    grad1 = np.zeros((Theta1.shape))
    grad2 = np.zeros((Theta2.shape))
    
    for i in range(m):
        xi= X[i,:] # 1 X 401
        a1i = a1[i,:] # 1 X 26
        a2i =a2[i,:] # 1 X 10
        d2 = a2i - y10[i,:]
        d1 = Theta2.T @ d2.T * sigmoidGradient(np.hstack((1,xi @ Theta1.T)))
        grad1= grad1 + d1[1:][:,np.newaxis] @ xi[:,np.newaxis].T
        grad2 = grad2 + d2.T[:,np.newaxis] @ a1i[:,np.newaxis].T
        
    grad1 = 1/m * grad1
    grad2 = 1/m*grad2
    
    grad1_reg = grad1 + (Lambda/m) * np.hstack((np.zeros((Theta1.shape[0],1)),Theta1[:,1:]))
    grad2_reg = grad2 + (Lambda/m) * np.hstack((np.zeros((Theta2.shape[0],1)),Theta2[:,1:]))
    
    return cost, grad1, grad2,reg_J, grad1_reg,grad2_reg

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

input_layer_size  = 400
hidden_layer_size = 25
num_labels = 10
nn_params = np.append(Theta1.flatten(),Theta2.flatten())
J,reg_J = nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, 1)[0:4:3]
print("Cost at parameters (non-regularized):",J,"\nCost at parameters (Regularized):",reg_J)

Здесь функция flatten() свернула массив в одно измерение и np.append «развернула» параметры в вектор.

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

def randInitializeWeights(L_in, L_out):
    """
    randomly initializes the weights of a layer with L_in incoming connections and L_out outgoing connections.
    """
    
    epi = (6**1/2) / (L_in + L_out)**1/2
    
    W = np.random.rand(L_out,L_in +1) *(2*epi) -epi
    
    return W
initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size)
initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels)
initial_nn_params = np.append(initial_Theta1.flatten(),initial_Theta2.flatten())

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

def gradientDescentnn(X,y,initial_nn_params,alpha,num_iters,Lambda,input_layer_size, hidden_layer_size, num_labels):
    """
    Take in numpy array X, y and theta and update theta by taking num_iters gradient steps
    with learning rate of alpha
    
    return theta and the list of the cost of theta during each iteration
    """
    Theta1 = initial_nn_params[:((input_layer_size+1) * hidden_layer_size)].reshape(hidden_layer_size,input_layer_size+1)
    Theta2 = initial_nn_params[((input_layer_size +1)* hidden_layer_size ):].reshape(num_labels,hidden_layer_size+1)
    
    m=len(y)
    J_history =[]
    
    for i in range(num_iters):
        nn_params = np.append(Theta1.flatten(),Theta2.flatten())
        cost, grad1, grad2 = nnCostFunction(nn_params,input_layer_size, hidden_layer_size, num_labels,X, y,Lambda)[3:]
        Theta1 = Theta1 - (alpha * grad1)
        Theta2 = Theta2 - (alpha * grad2)
        J_history.append(cost)
    
    nn_paramsFinal = np.append(Theta1.flatten(),Theta2.flatten())
    return nn_paramsFinal , J_history
nnTheta, nnJ_history = gradientDescentnn(X,y,initial_nn_params,0.8,800,1,input_layer_size, hidden_layer_size, num_labels)
Theta1 = nnTheta[:((input_layer_size+1) * hidden_layer_size)].reshape(hidden_layer_size,input_layer_size+1)
Theta2 = nnTheta[((input_layer_size +1)* hidden_layer_size ):].reshape(num_labels,hidden_layer_size+1)

Предупреждение для тех, кто выполняет код. Это займет довольно много времени, в зависимости от вашей вычислительной мощности, и даже больше, если вы оптимизируете свои значения alpha и num_iters. Я использую 0,8 для альфы и 800 для num_iters, но я считаю, что можно добиться большей точности при большей настройке.

pred3 = predict(Theta1, Theta2, X)
print("Training Set Accuracy:",sum(pred3[:,np.newaxis]==y)[0]/5000*100,"%")

На этом я закончил серию. Записная книжка Jupyter будет загружена на мой GitHub по адресу (https://github.com/Benlau93/Machine-Learning-by-Andrew-Ng-in-Python).

Для другой реализации Python в этой серии:

Спасибо за чтение.