使用诸如Keras、TensorFlow或PyTorch等高级框架,我们可以快速构建非常复杂的模型。这次我们将尝试利用我们的知识,只使用NumPy构建一个完全可操作的神经网络。最后,我们还将使用它来解决简单的分类问题,并将其与用Keras构建的机器学习模型进行性能比较。
导入库
import numpy as np from IPython.display import Image
网络架构
在开始编程之前,让我们先停下来准备一个基本的路线图。我们的目标是创建一个程序,能够创建一个具有指定架构(层的数量和大小以及适当的激活函数)的紧密连接的神经网络。图1给出了这样一个网络的例子。最重要的是,我们必须能够训练我们的网络并利用它进行预测。
上图显示了在我们的神经网络训练过程中需要执行的操作。它还显示了在单个迭代的不同阶段,我们需要更新和读取多少参数。构建正确的数据结构并巧妙地管理其状态是我们任务中最困难的部分。
神经网络层的初始化
让我们从每一层的权重矩阵W和偏差向量b开始。在图3中,我已经准备了小的cheatsheet,这将帮助我们为这些系数标记适当的尺寸。上标[l]表示当前层的索引(从1开始计数),我假设描述NN架构的信息将以列表的形式传递给我们的程序。列表中的每个条目是一个字典描述一个网络层的基本参数:input_dim- 信号矢量的大小作为层的输入,output_dim- 在层输出时得到的激活向量的大小,activation- 在层内使用的激活函数。
NN_ARCHITECTURE = [ {"input_dim": 2, "output_dim": 25, "activation": "relu"}, {"input_dim": 25, "output_dim": 50, "activation": "relu"}, {"input_dim": 50, "output_dim": 50, "activation": "relu"}, {"input_dim": 50, "output_dim": 25, "activation": "relu"}, {"input_dim": 25, "output_dim": 1, "activation": "sigmoid"}, ]
初始化每一层的参数值
最后,让我们关注在这一部分中必须完成的主要任务——层参数的初始化。初始权重不能相等,因为它会导致对称性问题。基本上,如果所有的权值都是一样的,不管输入X是多少,隐藏层中的所有单位也是一样的。在某种程度上,我们陷入了最初的状态,没有任何希望逃离,无论我们的模式和网络的深度如何。在第一次迭代中,使用小值可以提高算法的效率。从图4所示的sigmoid函数图中可以看出,对于接近于0的数,它的导数值最高,这对神经网络(NN)的学习速度有显著影响。在所有参数初始化中,使用小随机数是一种简单的方法,但它保证了我们的算法有足够好的起点。准备好的参数值存储在python字典中,带有唯一标识其所属层的键。字典在函数末尾返回,因此我们可以在算法的下一阶段使用它。Python代码如下:
def init_layers(nn_architecture, seed = 99): # random seed initiation np.random.seed(seed) # number of layers in our neural network number_of_layers = len(nn_architecture) # parameters storage initiation params_values = {} # iteration over network layers for idx, layer in enumerate(nn_architecture): # we number network layers from 1 layer_idx = idx + 1 # extracting the number of units in layers layer_input_size = layer["input_dim"] layer_output_size = layer["output_dim"] # initiating the values of the W matrix # and vector b for subsequent layers params_values['W' + str(layer_idx)] = np.random.randn( layer_output_size, layer_input_size) * 0.1 params_values['b' + str(layer_idx)] = np.random.randn( layer_output_size, 1) * 0.1 return params_values
激活函数
在我们今天将要使用的所有函数中,有一些非常简单但功能强大的函数。有许多激活函数,但是在这个项目中,我决定提供使用其中两个函数——sigmoid和ReLU
def sigmoid(Z): return 1/(1+np.exp(-Z)) def relu(Z): return np.maximum(0,Z) def sigmoid_backward(dA, Z): sig = sigmoid(Z) return dA * sig * (1 - sig) def relu_backward(dA, Z): dZ = np.array(dA, copy = True) dZ[Z <= 0] = 0; return dZ;
正向传播
设计的神经网络结构简单。信息向一个方向流动——它以X矩阵的形式传递,然后穿过隐藏的单位,从而形成预测的矢量Y_hat。为了便于阅读,我将正向传播分解为两个单独的函数——对单个层进行正向传播,对整个NN进行正向传播。
def single_layer_forward_propagation(A_prev, W_curr, b_curr, activation="relu"): Z_curr = np.dot(W_curr, A_prev) + b_curr if activation is "relu": activation_func = relu elif activation is "sigmoid": activation_func = sigmoid else: raise Exception('Non-supported activation function') return activation_func(Z_curr), Z_curr
这部分代码可能是最直观、最容易理解的。给定上一层的输入信号,计算仿射变换Z,然后应用选定的激活函数。通过使用NumPy,我们可以利用向量化-执行矩阵操作,为整个层和整批示例一次。这消除了迭代,大大加快了计算速度。除了计算出的矩阵A,我们的函数还返回中间值z。答案如图2所示。我们在反向的时候需要Z。
使用预先准备好的一层forward函数,我们现在可以轻松地构建整个正向传播步骤。这是一个稍微复杂一点的函数,它的作用不仅是执行预测,还组织中间值的集合。它返回Python dictionary,其中包含为特定层计算的A和Z值。
def full_forward_propagation(X, params_values, nn_architecture): memory = {} A_curr = X for idx, layer in enumerate(nn_architecture): layer_idx = idx + 1 A_prev = A_curr activ_function_curr = layer["activation"] W_curr = params_values["W" + str(layer_idx)] b_curr = params_values["b" + str(layer_idx)] A_curr, Z_curr = single_layer_forward_propagation(A_prev, W_curr, b_curr, activ_function_curr) memory["A" + str(idx)] = A_prev memory["Z" + str(layer_idx)] = Z_curr return A_curr, memory
损失函数
在为了监控我们的进度,并确保我们在正确的方向前进,我们应该定期计算损失函数的值。“一般来说,损失函数旨在显示我们与’理想’解决方案的距离。”它是根据我们计划解决的问题选择的,而像Keras这样的框架有很多选项可供选择。因为我打算测试我们的神经网络(NN)以便对两个类之间的点进行分类,所以我决定使用二元交叉熵,它由以下公式定义。为了获得有关学习过程的更多信息,我还决定实现一个能够计算准确性的函数。
Python代码如下:
def get_cost_value(Y_hat, Y): m = Y_hat.shape[1] cost = -1 / m * (np.dot(Y, np.log(Y_hat).T) + np.dot(1 - Y, np.log(1 - Y_hat).T)) return np.squeeze(cost) # an auxiliary function that converts probability into class def convert_prob_into_class(probs): probs_ = np.copy(probs) probs_[probs_ > 0.5] = 1 probs_[probs_ <= 0.5] = 0 return probs_ def get_accuracy_value(Y_hat, Y): Y_hat_ = convert_prob_into_class(Y_hat) return (Y_hat_ == Y).all(axis=0).mean()
反向传播
很明显,许多缺乏经验的深度学习爱好者认为反向传播是一种令人生畏和难以理解的算法。微积分和线性代数的组合经常会阻碍那些没有扎实的数学训练的人。先看单层中反向传播的Python代码:
def single_layer_backward_propagation(dA_curr, W_curr, b_curr, Z_curr, A_prev, activation="relu"): m = A_prev.shape[1] if activation is "relu": backward_activation_func = relu_backward elif activation is "sigmoid": backward_activation_func = sigmoid_backward else: raise Exception('Non-supported activation function') dZ_curr = backward_activation_func(dA_curr, Z_curr) dW_curr = np.dot(dZ_curr, A_prev.T) / m db_curr = np.sum(dZ_curr, axis=1, keepdims=True) / m dA_prev = np.dot(W_curr.T, dZ_curr) return dA_prev, dW_curr, db_curr
通常人们会将反向传播与梯度下降混为一谈,但事实上这些是两个不同的事情。第一个目的是有效地计算梯度,而第二个目的是使用计算的梯度进行优化。在NN中,我们计算成本函数关于参数的的梯度,但是反向传播可以用于计算任何函数的导数。这种算法的本质是递归使用从微积分中得到的链式规则 – 计算通过组合其他函数创建的函数的导数。该过程 – 对于一个网络层 – 由以下公式描述。不幸的是,由于本文主要关注实际实现,我将省略推导。
就像正向传播一样,我决定将计算分成两个独立的函数。第一个 – 专注于单个层并归结为在NumPy中重写上面的公式。第二个代表完全反向传播。我们首先计算成本函数相对于预测向量的导数 – 正向传播的结果。这非常简单,因为它只包括重写以下公式。然后从结尾开始遍历网络层,并根据图6所示的图计算所有参数的导数。最后,函数返回一个包含我们正在寻找的梯度的python字典。
def full_backward_propagation(Y_hat, Y, memory, params_values, nn_architecture): grads_values = {} m = Y.shape[1] Y = Y.reshape(Y_hat.shape) dA_prev = - (np.divide(Y, Y_hat) - np.divide(1 - Y, 1 - Y_hat)); for layer_idx_prev, layer in reversed(list(enumerate(nn_architecture))): layer_idx_curr = layer_idx_prev + 1 activ_function_curr = layer["activation"] dA_curr = dA_prev A_prev = memory["A" + str(layer_idx_prev)] Z_curr = memory["Z" + str(layer_idx_curr)] W_curr = params_values["W" + str(layer_idx_curr)] b_curr = params_values["b" + str(layer_idx_curr)] dA_prev, dW_curr, db_curr = single_layer_backward_propagation( dA_curr, W_curr, b_curr, Z_curr, A_prev, activ_function_curr) grads_values["dW" + str(layer_idx_curr)] = dW_curr grads_values["db" + str(layer_idx_curr)] = db_curr return grads_values
更新参数值
该方法的目标是使用梯度优化更新网络参数。通过这种方式,我们试图使目标函数更接近最小值。为完成此任务,我们将使用两个作为函数参数提供的字典:params_values存储参数的当前值,grads_values存储针对这些参数计算的成本函数导数。现在,您只需要为每个层应用以下等式。这是一个非常简单的优化算法,但我决定使用它,因为它是更高级优化器的一个很好的起点。
def update(params_values, grads_values, nn_architecture, learning_rate): # iteration over network layers for idx, layer in enumerate(nn_architecture): # we number network layers from 1 layer_idx = idx + 1 params_values["W" + str(layer_idx)] -= learning_rate * grads_values["dW" + str(layer_idx)] params_values["b" + str(layer_idx)] -= learning_rate * grads_values["db" + str(layer_idx)] return params_values;
放在一起
我们已经准备好了所有必要的函数,现在我们只需要把它们按正确的顺序放在一起。为了更好地理解操作的顺序,有必要再次查看图2中的图表。该函数返回经过训练和训练期间度量变化的历史所得到的优化权值。为了进行预测,您只需要使用接收到的权重矩阵和一组测试数据运行完整的正向传播。Python代码如下:
def train(X, Y, nn_architecture, epochs, learning_rate): params_values = init_layers(nn_architecture, 2) cost_history = [] accuracy_history = [] for i in range(epochs): Y_hat, cashe = full_forward_propagation(X, params_values, nn_architecture) cost = get_cost_value(Y_hat, Y) cost_history.append(cost) accuracy = get_accuracy_value(Y_hat, Y) accuracy_history.append(accuracy) grads_values = full_backward_propagation(Y_hat, Y, cashe, params_values, nn_architecture) params_values = update(params_values, grads_values, nn_architecture, learning_rate) return params_values, cost_history, accuracy_history
Python示例及机器学习模型比较
是时候看看我们的机器学习模型是否可以解决一个简单的分类问题。我生成了一个由属于两个类的点组成的数据集,如图7所示。让我们尝试教我们的机器学习模型来对属于这个分布的点进行分类。为了比较,我还在高级框架中准备了一个模型 – Keras。两种模型都具有相同的架构和学习速度。最终,NumPy和Keras模型在测试集上实现了95%的类似精度。但是,我们的模型需要花费几十倍才能达到这样的效果。在我看来,这种状态主要是由于缺乏适当的优化。
生成测试数据
import os from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split import seaborn as sns import matplotlib.pyplot as plt from matplotlib import cm from mpl_toolkits.mplot3d import Axes3D sns.set_style("whitegrid") import keras from keras.models import Sequential from keras.layers import Dense from keras.utils import np_utils from keras import regularizers from sklearn.metrics import accuracy_score # number of samples in the data set N_SAMPLES = 1000 # ratio between training and test sets TEST_SIZE = 0.1 X, y = make_moons(n_samples = N_SAMPLES, noise=0.2, random_state=100) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=TEST_SIZE, random_state=42) # the function making up the graph of a dataset def make_plot(X, y, plot_name, file_name=None, XX=None, YY=None, preds=None, dark=False): if (dark): plt.style.use('dark_background') else: sns.set_style("whitegrid") plt.figure(figsize=(16,12)) axes = plt.gca() axes.set(xlabel="$X_1$", ylabel="$X_2$") plt.title(plot_name, fontsize=30) plt.subplots_adjust(left=0.20) plt.subplots_adjust(right=0.80) if(XX is not None and YY is not None and preds is not None): plt.contourf(XX, YY, preds.reshape(XX.shape), 25, alpha = 1, cmap=cm.Spectral) plt.contour(XX, YY, preds.reshape(XX.shape), levels=[.5], cmap="Greys", vmin=0, vmax=.6) plt.scatter(X[:, 0], X[:, 1], c=y.ravel(), s=40, cmap=plt.cm.Spectral, edgecolors='black') if(file_name): plt.savefig(file_name) plt.close() make_plot(X, y, "Dataset")
模型测试
# Training params_values = train(np.transpose(X_train), np.transpose(y_train.reshape((y_train.shape[0], 1))), NN_ARCHITECTURE, 10000, 0.01) # Prediction Y_test_hat, _ = full_forward_propagation(np.transpose(X_test), params_values, NN_ARCHITECTURE) # Accuracy achieved on the test set acc_test = get_accuracy_value(Y_test_hat, np.transpose(y_test.reshape((y_test.shape[0], 1)))) print("Test set accuracy: {:.2f} - David".format(acc_test))
Keras模型
# Building a model model = Sequential() model.add(Dense(25, input_dim=2,activation='relu')) model.add(Dense(50, activation='relu')) model.add(Dense(50, activation='relu')) model.add(Dense(25, activation='relu')) model.add(Dense(1, activation='sigmoid')) model.compile(loss='binary_crossentropy', optimizer="sgd", metrics=['accuracy']) # Training history = model.fit(X_train, y_train, epochs=200, verbose=0) Y_test_hat = model.predict_classes(X_test) acc_test = accuracy_score(y_test, Y_test_hat) print("Test set accuracy: {:.2f} - Goliath".format(acc_test))
学习过程的可视化
# boundary of the graph GRID_X_START = -1.5 GRID_X_END = 2.5 GRID_Y_START = -1.0 GRID_Y_END = 2 # output directory (the folder must be created on the drive) OUTPUT_DIR = "./binary_classification_vizualizations/" grid = np.mgrid[GRID_X_START:GRID_X_END:100j,GRID_X_START:GRID_Y_END:100j] grid_2d = grid.reshape(2, -1).T XX, YY = grid
Keras 模型
def callback_keras_plot(epoch, logs): plot_title = "Keras Model - It: {:05}".format(epoch) file_name = "keras_model_{:05}.png".format(epoch) file_path = os.path.join(OUTPUT_DIR, file_name) prediction_probs = model.predict_proba(grid_2d, batch_size=32, verbose=0) make_plot(X_test, y_test, plot_title, file_name=file_path, XX=XX, YY=YY, preds=prediction_probs) # Adding callback functions that they will run in every epoch testmodelcb = keras.callbacks.LambdaCallback(on_epoch_end=callback_keras_plot) # Building a model model = Sequential() model.add(Dense(25, input_dim=2,activation='relu')) model.add(Dense(50, activation='relu')) model.add(Dense(50, activation='relu')) model.add(Dense(25, activation='relu')) model.add(Dense(1, activation='sigmoid')) model.compile(loss='binary_crossentropy', optimizer="sgd", metrics=['accuracy']) # Training history = model.fit(X_train, y_train, epochs=200, verbose=0, callbacks=[testmodelcb]) rediction_probs = model.predict_proba(grid_2d, batch_size=32, verbose=0) make_plot(X_test, y_test, "Keras Model", file_name=None, XX=XX, YY=YY, preds=prediction_probs)
NumPy模型
def callback_numpy_plot(index, params): plot_title = "NumPy Model - It: {:05}".format(index) file_name = "numpy_model_{:05}.png".format(index//50) file_path = os.path.join(OUTPUT_DIR, file_name) prediction_probs, _ = full_forward_propagation(np.transpose(grid_2d), params, NN_ARCHITECTURE) prediction_probs = prediction_probs.reshape(prediction_probs.shape[1], 1) make_plot(X_test, y_test, plot_title, file_name=file_path, XX=XX, YY=YY, preds=prediction_probs, dark=True) # Training params_values = train(np.transpose(X_train), np.transpose(y_train.reshape((y_train.shape[0], 1))), NN_ARCHITECTURE, 10000, 0.01, False, callback_numpy_plot) prediction_probs_numpy, _ = full_forward_propagation(np.transpose(grid_2d), params_values, NN_ARCHITECTURE) prediction_probs_numpy = prediction_probs_numpy.reshape(prediction_probs_numpy.shape[1], 1) make_plot(X_test, y_test, "NumPy Model", file_name=None, XX=XX, YY=YY, preds=prediction_probs_numpy)
最后
我希望我的文章能够拓宽你的视野,增加你对神经网络内部编译操作的理解