全文共2875字,預計學習時長20分鐘或更長
如果你是個對神經網絡有所了解的初級數據科學家,或是個對深度學習略有耳聞的機器學習愛好者,一定要讀一讀這篇文章。本文介紹了使用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
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. 搭建神經網絡
使用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)),其實現就留給讀者作為練習吧。
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
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垂類自媒體 「讀芯術」