神經網絡是由權值、截距、激活函數、歸一化方法和損失函數逐層構造而成,同樣地,我們將使用由sklearn提供的手寫數字作為神經網絡的數據集。
nums_8x8 = load_digits()
data = nums_8x8.images[13:16] / np.max(nums_8x8.images[13:16])
data = data.reshape(len(data), -1)
labels = nums_8x8.target[13:16]
在將數據放入神經網絡時,記得將像素值歸一化成0到1之間的值。
x_l1 = layer(data, 64, 32)
x_b1 = batch_norm(x_l1)
x_a1 = relu(x_b1)
x_l2 = layer(x_a1, 32, 16)
x_b2 = batch_norm(x_l2)
x_a2 = relu(x_b2)
x_l3 = layer(x_a2, 16, 10)
x_b3 = batch_norm(x_l3)
y_pred = softmax(x_b3)
為了評估神經網絡預測的質量,所有ground truth標籤都應該轉換成 one-hot格式對齊網絡輸出結果,這樣它們才可以被放入一個損失函數。
def one_hot(labels, class_num=10):
return np.eye(class_num, dtype=float)[labels]
y = one_hot(nums_8x8.target[13:16], class_num=10)
loss = dl.cross_entropy(y, y_pred)
print(np.round(loss, decimals=2))
輸出:
[[0.2 0.24 0.02 1.59 0.03 0.03 0.11 0.22 0.02 0.01]
[0.02 0.03 0.26 0.04 1.35 0.03 0.02 0.05 0.29 0.08]
[0.07 0.04 0.07 0.03 0.04 1.19 0.16 0.03 0.05 0.26]]
然而,神經網絡計算過程中的參數都保存在函數中,這使得我們無法有效地獲取並進一步更新它們。定義這些函數的更好方法是使用 class (類),因為類的屬性是可變的。為了使我們的代碼保持一致性,所有的函數將被重寫為類。
在前兩步損失函數與 softmax 函數的反向傳播中,由於沒有轉置和歸一化操作,所以很容易計算梯度。一個常用的導數函數就可以清晰地指導編程工作。相反,為了正確計算梯度,必須認真考慮線性代數的概念
1. Derivative of Loss Function反向傳播的第一步是從損失函數開始的,損失函數應該取決於我們遇到的任務類型: 回歸 or 分類。對於回歸任務,平方損失函數是一個正確的選擇:
class SquareLoss(object):
def loss(self, y, y_pred):
return 0.5 * np.power((y - y_pred), 2)
def gradient(self, y, y_pred):
return -(y - y_pred)
對於分類問題而言,我們則需要選擇交叉熵作為損失函數。
class CrossEntropyLoss(object):
def __call__(self, y, y_pred):
# Avoid division by zero
y_pred = np.clip(y_pred, 1e-15, 1 - 1e-15)
return - y * np.log(y_pred) - (1 - y) * np.log(1 - y_pred)
def gradient(self, y, y_pred):
# Avoid division by zero
y_pred = np.clip(y_pred, 1e-15, 1 - 1e-15)
return - (y / y_pred) + (1 - y) / (1 - y_pred)
對於數據而言,接下來的代碼就是反向傳播的起點,one-hot 格式下的標籤和預測結果需要同事被輸入計算損失函數的梯度,並且每一個函數都需要先被實例化之後才能開始工作。
ce = CrossEntropyLoss()
loss_grad = ce.gradient(y, y_pred)
print(np.round(loss_grad, decimals=2))
輸出:
[[ 1.22 1.27 1.02 -4.92 1.03 1.03 1.12 1.24 1.02 1.01]
[ 1.02 1.03 1.29 1.04 -3.87 1.03 1.02 1.05 1.34 1.08]
[ 1.07 1.04 1.07 1.03 1.04 -3.29 1.18 1.03 1.05 1.3 ]]
第二站來到了softmax函數,這個函數用來把神經網絡的輸出轉化成概率的形式,是個非常重要的環節。
我們想在這裡添加 𝑐 的原因是因為當輸入向量包含大數字時,它可以防止類間的概率多樣性被抹除。為了求導softmax函數,我們將使用除法法則:
若將此一法則套用到 softmax 函數中,則𝑔(𝑥)表示分子而ℎ(𝑥)表示分母:
並且𝑗代表了從1到n個類之間的某一個值,這麼一來,在求一階導的過程中只有對應的類別才會留下常數項。
class Softmax():
def __call__(self, x):
e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
return e_x / np.sum(e_x, axis=-1, keepdims=True)
def gradient(self, x):
p = self.__call__(x)
return p * (1 - p)
當我們想要將softmax函數應用於反向傳播時,注意梯度是基於鏈式法則的。因此,我們不僅要計算softmax函數的梯度,還要乘以先前從損失函數計算出的梯度。
softmax = Softmax()
softmax_grad = softmax.gradient(x_b3) * loss_grad
print(np.round(softmax_grad, decimals=2))
輸出:
[[ 0.18 0.21 0.02 -0.8 0.02 0.03 0.11 0.2 0.02 0.01]
[ 0.02 0.03 0.23 0.04 -0.74 0.03 0.02 0.05 0.25 0.08]
[ 0.07 0.04 0.07 0.03 0.04 -0.7 0.15 0.03 0.05 0.23]