如何使用Python从头开始构建一个简单的神经网络
神经网络正变得越来越受欢迎,作为机器学习和人工智能的核心领域,它们将在未来几年的技术、科学和工业中发挥重要作用。它的高度流行已经产生了许多框架,这些框架允许你在不知道它们背后的完整理论的情况下非常容易地实现神经网络。
为了更加深入地理解神经网络,我们将从头开始实际实现NN,而不使用任何框架。这可能比使用框架困难一些,但您将能够更好地理解神经网络背后的机制。当然,在大型项目中,首选框架实现,因为使用框架可以设置起来更容易,更快捷。
本教程中使用numpy库(用于线性代数操作的科学库)。假设你安装了python和pip,你可以通过运行这个命令来安装numpy:
pip install numpy
神经网络实际上是许多变量的函数:它接受输入,进行计算并产生输出。我们喜欢将其可视化为不同层中的神经元,层中的每个神经元与前一层和下一层中的所有神经元相连。所有的计算都发生在那些神经元内,并取决于将神经元相互连接的权重。因此,我们所要做的就是学习正确的权重,以获得所需的输出。
它们的结构通常非常复杂,包括许多层,甚至超过100万个神经元,以便能够处理我们这个时代的大型数据集。然而,为了理解大型深度神经网络是如何工作的,我们应该从最简单的开始。
因此,下面我们将实现一个非常简单的两层网络。为了做到这一点,我们还需要一个非常简单的数据集,因此我们将在示例中使用XOR数据集,如下所示。A和B是神经网络的两个输入,A XOR B是输出。我们将尝试让我们的神经网络学习权值,这样无论它接受哪对A和B作为输入,它都会返回相应的结果。
首先,我们需要定义神经网络的结构。因为我们的数据集相对简单,只有一个隐藏层的网络就可以了。所以我们将有一个输入层,一个隐藏层和一个输出层。接下来我们需要激活函数。sigmoid函数是最后一层的一个很好的选择,因为它输出0到1之间的值,而tanh(双曲正切)在隐藏层中工作得更好,但是其他常用函数都可以工作(例如ReLU)。所以神经网络的结构如下所示:
这里,要学习的参数是权重W1,W2和偏差b1,b2。如您所见,W1和b1连接输入层和隐藏层,而W2,b2连接隐藏层和输出层。根据NN的基本理论,我们知道激活A1和A2计算如下:
A1 = h(W1*X + b1) A2 = g(W2*A1 + b2)
其中g和h是我们选择的两个激活函数(对于我们sigmoid和tanh),W1,W1,b1,b2通常是矩阵。
现在让我们跳转到实际的代码中。代码风格遵循教授提出的指导原则。
首先,我们将实现如下定义的sigmoid激活函数:g(z)= 1 /(1 + e ^( - z))其中z通常是矩阵。幸运的是numpy支持使用矩阵进行计算,因此代码相对简单:
import numpy as np def sigmoid(z): return 1/(1 + np.exp(-z))
接下来,我们必须初始化参数。权重矩阵W1和W2将从正态分布中随机初始化,而偏差b1和b2将初始化为零。函数initialize_parameters(n_x, n_h, n_y)将每层的单元数作为输入,并正确初始化参数:
def initialize_parameters(n_x, n_h, n_y): W1 = np.random.randn(n_h, n_x) b1 = np.zeros((n_h, 1)) W2 = np.random.randn(n_y, n_h) b2 = np.zeros((n_y, 1)) parameters = { "W1": W1, "b1" : b1, "W2": W2, "b2" : b2 } return parameters
下一步是实现正向传播。函数forward_prop(X, parameters)将神经网络输入矩阵X和参数字典作为输入,并返回NN A2的输出,其中包含一个缓存字典,稍后在反向传播中使用。
def forward_prop(X, parameters): W1 = parameters["W1"] b1 = parameters["b1"] W2 = parameters["W2"] b2 = parameters["b2"] Z1 = np.dot(W1, X) + b1 A1 = np.tanh(Z1) Z2 = np.dot(W2, A1) + b2 A2 = sigmoid(Z2) cache = { "A1": A1, "A2": A2 } return A2, cache
现在我们要计算损失函数。我们将使用交叉熵损失函数。Calculate_cost(A2, Y)将NN A2和groundtruth矩阵Y的结果作为输入,返回交叉熵损失:
def calculate_cost(A2, Y): cost = -np.sum(np.multiply(Y, np.log(A2)) + np.multiply(1-Y, np.log(1-A2)))/m cost = np.squeeze(cost) return cost
现在神经网络算法最困难的部分,反向传播。这个函数将返回损失函数相对于我们网络的4个参数(W1, W2, b1, b2)的梯度:
def backward_prop(X, Y, cache, parameters): A1 = cache["A1"] A2 = cache["A2"] W2 = parameters["W2"] dZ2 = A2 - Y dW2 = np.dot(dZ2, A1.T)/m db2 = np.sum(dZ2, axis=1, keepdims=True)/m dZ1 = np.multiply(np.dot(W2.T, dZ2), 1-np.power(A1, 2)) dW1 = np.dot(dZ1, X.T)/m db1 = np.sum(dZ1, axis=1, keepdims=True)/m grads = { "dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2 } return grads
我们将使用Gradient Descent算法更新我们的参数:
def update_parameters(parameters, grads, learning_rate): W1 = parameters["W1"] b1 = parameters["b1"] W2 = parameters["W2"] b2 = parameters["b2"] dW1 = grads["dW1"] db1 = grads["db1"] dW2 = grads["dW2"] db2 = grads["db2"] W1 = W1 - learning_rate*dW1 b1 = b1 - learning_rate*db1 W2 = W2 - learning_rate*dW2 b2 = b2 - learning_rate*db2 new_parameters = { "W1": W1, "W2": W2, "b1" : b1, "b2" : b2 } return new_parameters
到目前为止,我们已经实现了一轮训练所需的所有函数。现在我们要做的就是将它们放在一个名为model()的函数中,并从主程序中调用model()。
Model()函数将特征矩阵X、标签矩阵Y、单位数n_x、n_h、n_y、要运行梯度下降算法的迭代次数和梯度下降的学习率作为输入,并结合上述所有函数返回模型的训练参数:
def model(X, Y, n_x, n_h, n_y, num_of_iters, learning_rate): parameters = initialize_parameters(n_x, n_h, n_y) for i in range(0, num_of_iters+1): a2, cache = forward_prop(X, parameters) cost = calculate_cost(a2, Y) grads = backward_prop(X, Y, cache, parameters) parameters = update_parameters(parameters, grads, learning_rate) if(i%100 == 0): print('Cost after iteration# {:d}: {:f}'.format(i, cost)) return parameters
上述函数将返回神经网络的训练参数。函数predict(X, parameters)以我们想要计算XOR函数的2个数字和模型的训练参数作为输入,并通过使用0.5的阈值返回期望的结果y_forecast:
def predict(X, parameters): a2, cache = forward_prop(X, parameters) yhat = a2 yhat = np.squeeze(yhat) if(yhat >= 0.5): y_predict = 1 else: y_predict = 0 return y_predict
现在让我们转到主程序并声明我们的矩阵X,Y和超参数n_x,n_h,n_y,num_of_iters,learning_rate:
np.random.seed(2) # The 4 training examples by columns X = np.array([[0, 0, 1, 1], [0, 1, 0, 1]]) # The outputs of the XOR for every example in X Y = np.array([[0, 1, 1, 0]]) # No. of training examples m = X.shape[1] # Set the hyperparameters n_x = 2 #No. of neurons in first layer n_h = 2 #No. of neurons in hidden layer n_y = 1 #No. of neurons in output layer num_of_iters = 1000 learning_rate = 0.3
完成上述所有设置后,在其上训练模型如下述Python代码:
trained_parameters = model(X, Y, n_x, n_h, n_y, num_of_iters, learning_rate)
最后让我们对一对随机数进行预测:
# Test 2X1 vector to calculate the XOR of its elements. # You can try any of those: (0, 0), (0, 1), (1, 0), (1, 1) X_test = np.array([[1], [1]]) y_predict = predict(X_test, trained_parameters) # Print the result print('Neural Network prediction for example ({:d}, {:d}) is {:d}'.format( X_test[0][0], X_test[1][0], y_predict))
让我们看看我们的结果。如果我们运行我们的文件,请使用此命令说xor_nn.py
python xor_nn.py
我们得到以下结果,这确实是正确的,因为1XOR1 = 0!
当然,为了训练具有多个层和隐藏单元的大型网络,您可能需要使用上述算法的一些变体,例如,您可能需要使用批量梯度下降而不是梯度下降或使用更多层,但是主要思想是如上所述的简单NN。
您可以自己调整超参数,并尝试不同的神经网络架构。例如,您可以尝试较少的迭代次数,因为Cost似乎快速下降,其中1000个可能有点大。