代码详解:使用NumPy,教你9步从头搭建神经网络
Photo by Alina Grubnyak on Unsplash
如果你是个对神经网络有所了解的初级数据科学家,或是个对深度学习略有耳闻的机器学习爱好者,一定要读一读这篇文章。本文介绍了使用NumPy从头搭建神经网络的9个步骤,即从数据预处理到反向传播这一“必经之路”。
对机器学习、人工神经网络、Python语法和编程逻辑有些基本理解最好,(但这也不是必需条件,你可以边读边学)。
1. 初始化
导入NumPy。
import numpy as np np.random.seed(42) # for reproducibility
2. 生成数据
深度学习需要大量的数据。网上有很多干净的数据集,但为了使用简单,会选择生成自己的数据集,即输入a和b,输出a + b,a-b和| a-b |。这样可以生成10,000个基准点。
X_num_row, X_num_col = [2, 10000] # Row is no. of feature, col is no.of datum points X_raw = np.random.rand(X_num_row,X_num_col) * 100 y_raw = np.concatenate(([(X_raw[0,:] + X_raw[1,:])], [(X_raw[0,:] -X_raw[1,:])], np.abs([(X_raw[0,:] - X_raw[1,:])]))) # for input a and b, output is a+b; a-b and |a-b| y_num_row, y_num_col = y_raw.shape
Photo by Kristopher Roller on Unsplash
3. 分割测试集与训练集
将数据集划分为训练集(占70%)和测试集(30%)两个子集。训练集只用于神经网络的调整,测试集则是在训练完成后用来测试性能。
train_ratio = 0.7 num_train_datum = int(train_ratio*X_num_col) X_raw_train = X_raw[:,0:num_train_datum] X_raw_test = X_raw[:,num_train_datum:] y_raw_train = y_raw[:,0:num_train_datum] y_raw_test = y_raw[:,num_train_datum:]
4. 数据标准化
训练集中的数据已进行了标准化处理,所以各标准化特征都呈零均值、单位方差的分布。然后可以将上述过程产生的定标器应用于测试集。
class scaler: def __init__(self, mean,std): self.mean = mean self.std = std def get_scaler(row): mean = np.mean(row) std = np.std(row) return scaler(mean, std) def standardize(data, scaler): return (data -scaler.mean) / scaler.std def unstandardize(data, scaler): return (data * scaler.std) +scaler.mean # Construct scalers from training set X_scalers = [get_scaler(X_raw_train[row,:]) for row inrange(X_num_row)] X_train = np.array([standardize(X_raw_train[row,:], X_scalers[row]) forrow in range(X_num_row)]) y_scalers = [get_scaler(y_raw_train[row,:]) for row inrange(y_num_row)] y_train = np.array([standardize(y_raw_train[row,:], y_scalers[row]) forrow in range(y_num_row)]) # Apply those scalers to testing set X_test = np.array([standardize(X_raw_test[row,:], X_scalers[row]) forrow in range(X_num_row)]) y_test = np.array([standardize(y_raw_test[row,:], y_scalers[row]) forrow in range(y_num_row)]) # Check if data has been standardized print([X_train[row,:].mean() for row in range(X_num_row)]) # should beclose to zero print([X_train[row,:].std() for row in range(X_num_row)]) # should be close to one print([y_train[row,:].mean() for row in range(y_num_row)]) # should beclose to zero print([y_train[row,:].std() for row in range(y_num_row)]) # should be close to one
因此,定标器不含有测试集的任何信息。这也是在对神经网络进行调整前我们所希望的结果。
至此,历经上述4个步骤后,数据预处理就完成了。
5. 搭建神经网络
Photo by freestocks.org on Unsplash
使用Python中的类对‘层’进行对象化。每个层(输入层除外)具有权重矩阵W、偏置矢量b和激活函数。各个层都将被附加到名为neural_net的列表中,从而得到全连接的神经网络。
class layer: def __init__(self,layer_index, is_output, input_dim, output_dim, activation): self.layer_index = layer_index# zero indicates input layer self.is_output =is_output # true indicates output layer, false otherwise self.input_dim =input_dim self.output_dim =output_dim self.activation =activation # the multiplicationconstant is sorta arbitrary if layer_index != 0: self.W = np.random.randn(output_dim,input_dim) * np.sqrt(2/input_dim) self.b =np.random.randn(output_dim, 1) * np.sqrt(2/input_dim) # Change layers_dim to configure your own neural net! layers_dim = [X_num_row, 4, 4, y_num_row] # input layer --- hiddenlayers --- output layers neural_net = [] # Construct the net layer by layer for layer_index in range(len(layers_dim)): if layer_index == 0: # ifinput layer neural_net.append(layer(layer_index, False, 0, layers_dim[layer_index],'irrelevant')) elif layer_index+1 ==len(layers_dim): # if output layer neural_net.append(layer(layer_index, True, layers_dim[layer_index-1],layers_dim[layer_index], activation='linear')) else: neural_net.append(layer(layer_index,False, layers_dim[layer_index-1], layers_dim[layer_index], activation='relu')) # Simple check on overfitting pred_n_param =sum([(layers_dim[layer_index]+1)*layers_dim[layer_index+1] for layer_index inrange(len(layers_dim)-1)]) act_n_param = sum([neural_net[layer_index].W.size +neural_net[layer_index].b.size for layer_index in range(1,len(layers_dim))]) print(f'Predicted number of hyperparameters: {pred_n_param}') print(f'Actual number of hyperparameters: {act_n_param}') print(f'Number of data: {X_num_col}') if act_n_param >= X_num_col: raise Exception('It willoverfit.')
最后,使用下列公式,通过计数对超参数的数量进行完整性检查。可用的基准数量应该多于超参数数量,否则就会过度拟合。
N ^ l是第l层的超参数个数,L是层数(不包括输入层)。
6. 前向传播
在给定一组权重和偏差的情况下,定义一个前向传播的函数。各层之间的连接以矩阵形式定义为:
σ 是element-wise 激活函数,上标T表示矩阵的转置。
def activation(input_, act_func): if act_func == 'relu': return np.maximum(input_,np.zeros(input_.shape)) elif act_func == 'linear': return input_ else: raiseException('Activation function is not defined.') def forward_prop(input_vec, layers_dim=layers_dim,neural_net=neural_net): neural_net[0].A = input_vec #Define A in input layer for for-loop convenience for layer_index inrange(1,len(layers_dim)): # W,b,Z,A are undefined in input layer neural_net[layer_index].Z= np.add(np.dot(neural_net[layer_index].W, neural_net[layer_index-1].A),neural_net[layer_index].b) neural_net[layer_index].A =activation(neural_net[layer_index].Z, neural_net[layer_index].activation) return neural_net[layer_index].A
激活函数是逐个定义的。 ReLU实现为a→max(a,0),而sigmoid函数应返回a → 1/(1+e^(-a)),其实现就留给读者作为练习吧。
Photo by Holger Link on Unsplash
7. 反向传播
这是最棘手的一步,很多人都不明白。在定义了用于评估性能的损失度量函数后,就想看一看如果扰乱每个权重或偏差,损失度量会如何变化。即每个权重和偏差对损失度量的敏感程度如何。
def get_loss(y, y_hat, metric='mse'): if metric == 'mse': individual_loss = 0.5 *(y_hat - y) ** 2 returnnp.mean([np.linalg.norm(individual_loss[:,col], 2) for col inrange(individual_loss.shape[1])]) else: raise Exception('Loss metric is notdefined.') def get_dZ_from_loss(y, y_hat, metric): if metric == 'mse': return y_hat - y else: raise Exception('Lossmetric is not defined.') def get_dactivation(A, act_func): if act_func == 'relu': returnnp.maximum(np.sign(A), np.zeros(A.shape)) # 1 if backward input >0, 0 otherwise;then diaganolize elif act_func == 'linear': return np.ones(A.shape) else: raiseException('Activation function is not defined.') def backward_prop(y, y_hat, metric='mse', layers_dim=layers_dim,neural_net=neural_net, num_train_datum=num_train_datum): for layer_index inrange(len(layers_dim)-1,0,-1): if layer_index+1 ==len(layers_dim): # if output layer dZ =get_dZ_from_loss(y, y_hat, metric) else: dZ = np.multiply(np.dot(neural_net[layer_index+1].W.T,dZ), get_dactivation(neural_net[layer_index].A,neural_net[layer_index].activation)) dW = np.dot(dZ,neural_net[layer_index-1].A.T) / num_train_datum db = np.sum(dZ, axis=1,keepdims=True) / num_train_datum neural_net[layer_index].dW = dW neural_net[layer_index].db = db
这通过偏导数∂e/∂W(在代码中表示为dW)和∂e/∂b(在代码中表示为db)来表示,还可以通过分析计算。
这些反向传播方程都假设只有y这一个比较数据。每次迭代的性能仅受一个基准点的影响,所以梯度更新过程会非常嘈杂。为减少噪声,可以使用多个基准。其中∂W(y_1,y_2,...)是∂W(y_1),∂W(y_2),...的平均值,∂b也一样。这些在方程中都没有显示出来,但在下面的代码中可以实现。
8. 迭代优化
现在我们有了训练神经网络的全部要素。
知道权重和偏差的敏感性后,可使用以下更新规则通过梯度下降来迭代最小化(因此用减号表示)损失度量:
W = W - learning_rate * ∂W
b = b - learning_rate * ∂b
Photo by Rostyslav Savchyn on Unsplash
learning_rate = 0.01 max_epoch = 100000 for epoch in range(1,max_epoch+1): y_hat_train =forward_prop(X_train) # update y_hat backward_prop(y_train,y_hat_train) # update (dW,db) for layer_index inrange(1,len(layers_dim)): # update(W,b) neural_net[layer_index].W= neural_net[layer_index].W - learning_rate * neural_net[layer_index].dW neural_net[layer_index].b= neural_net[layer_index].b - learning_rate * neural_net[layer_index].db if epoch % 100000 == 0: print(f'{get_loss(y_train, y_hat_train):.4f}')
9. 测试
如果测试损失没有超出训练损失太多,该模型就可以进行推广。为了解模型的执行情况,我们还制作了一些测试用例。
print(get_loss(y_test, forward_prop(X_test))) def predict(X_raw_any): X_any =np.array([standardize(X_raw_any[row,:], X_scalers[row]) for row inrange(X_num_row)]) y_hat = forward_prop(X_any) y_hat_any =np.array([unstandardize(y_hat[row,:], y_scalers[row]) for row in range(y_num_row)]) return y_hat_any predict(np.array([[30,70],[70,30],[3,5],[888,122]]).T)
这就是使用NumPy从头搭建神经网络的9个步骤。本文所讲述的并非构建和训练神经网络最有效的方法,在未来还有很大的改进空间。有人可能用过一些高级框架(如TensorFlow、PyTorch或Keras)搭建神经网络。不过,仅用低级库进行搭建可以让我们真正弄明白这一神秘科学背后的原理。
留言 点赞 关注
我们一起分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”