代碼倉庫: https://github.com/brandonlyg/cute-dl
目標
- 增加交叉熵損失函數,使框架能夠支持分類任務的模型。
- 構建一個MLP模型, 在mnist數據集上執行分類任務准確率達到91%。
實現交叉熵損失函數
數學原理
分解交叉熵損失函數
交叉熵損失函數把模型的輸出值當成一個離散隨機變量的分布列。 設模型的輸出為: \(\hat{Y} = f(X)\), 其中\(f(X)\)表示模型。\(\hat{Y}\)是一個m X n矩陣, 如下所示:
把這個矩陣的第i行記為\(\hat{y}_i\), 它是一個\(\\R^{1Xn}\)向量, 它的第j個元素記為\(\hat{y}_{ij}\)。
交叉熵損失函數要求\(\hat{y}_i\)具有如下性質:
特別地,當n=1時, 只需要滿足第一條性質即可。我們先考慮n > 1的情況, 這種情況下n=2等價於n=1,在工程上n=1可以看成是對n=2的優化。
模型有時候並不會保證輸出值有這些性質, 這時損失函數要把\(\hat{y}_i\)轉換成一個分布列:\(\hat{p}_i\), 轉換函數的定義如下:
這里的\(\hat{p}_i\)是可以滿足要求的。函數\(e^{\hat{y}_{ij}}\)是單調增函數,對於任意兩個不同的\(\hat{y}_{ia} < \hat{y}_{ib}\), 都有:\(e^{\hat{y}_{ia}}\) < \(e^{\hat{y}_{ib}}\), 從而得到:\(\hat{p}_{ia} < \hat{p}_{ib}\). 因此這個函數把模型的輸出值變成了概率值,且概率的大小關系和輸出值的大小關系一致。
設數據\(x_i\)的類別標簽為\(y_i\)∈\(\\R^{1Xn}\). 如果\(x_i\)的真實類別為t, \(y_i\)滿足:
\(y_i\)使用的是one-hot編碼。交叉熵損失函數的定義為:
對於任意的\(y_{ij}\), 損失函數中任意一項具有如下的性質:
可看出\(y_{ij}=0\)的項對損失函數的值不會產生影響,所以在計算時可以把這樣的項從損失函數中忽略掉。其它\(y_{ij}=1\)的項當\(\hat{p}_{ij}=y_{ij}=1\)時損失函數達到最小值0。
梯度推導
根據鏈式法則, 損失函數的梯度為:
其中:
把(2), (3)代入(1)中得到:
由於當\(y_{ij}=0\)時, 梯度值為0, 所以這種情況可以忽略, 最終得到的梯度為:
如果模型的輸出值是一個隨機變量的分布列, 損失函數就可以省略掉把\(\hat{y}_{ij}\)轉換成\(\hat{p}_{ij}\)的步驟, 這個時候\(\hat{y}_{ij} = \hat{p}_{ij}\), 最終的梯度變成:
交叉熵損失函數的特殊情況: 只有兩個類別
現在來討論當n=1的情況, 這個時候\(\hat{y}_i\) ∈ \(\\R^{1 X 1}\),可以當成標量看待。
如果模型輸出的不是分布列, 損失函數可以分解為:
損失函數關於輸出值的梯度為:
把(1),(2)代入(3)中得到:
如果模型輸出值時一個隨機變量的分布列, 則有:
實現代碼
這個兩種交叉熵損失函數的實現代碼在cutedl/losses.py中。一般的交叉熵損失函數類名為CategoricalCrossentropy, 其主要實現代碼如下:
'''
輸入形狀為(m, n)
'''
def __call__(self, y_true, y_pred):
m = y_true.shape[0]
#pdb.set_trace()
if not self.__form_logists:
#計算誤差
loss = (-y_true*np.log(y_pred)).sum(axis=0)/m
#計算梯度
self.__grad = -y_true/(m*y_pred)
return loss.sum()
m = y_true.shape[0]
#轉換成概率分布
y_prob = dlmath.prob_distribution(y_pred)
#pdb.set_trace()
#計算誤差
loss = (-y_true*np.log(y_prob)).sum(axis=0)/m
#計算梯度
self.__grad = (y_prob - y_true)/m
return loss.sum()
其中prob_distribution函數把模型輸出轉換成分布列, 實現方法如下:
def prob_distribution(x):
expval = np.exp(x)
sum = expval.sum(axis=1).reshape(-1,1) + 1e-8
prob_d = expval/sum
return prob_d
二元分類交叉熵損失函數類名為BinaryCrossentropy, 其主要實現代碼如下:
'''
輸入形狀為(m, 1)
'''
def __call__(self, y_true, y_pred):
#pdb.set_trace()
m = y_true.shape[0]
if not self.__form_logists:
#計算誤差
loss = (-y_true*np.log(y_pred)-(1-y_true)*np.log(1-y_pred))/m
#計算梯度
self.__grad = (y_pred - y_true)/(m*y_pred*(1-y_pred))
return loss.sum()
#轉換成概率
y_prob = dlmath.sigmoid(y_pred)
#計算誤差
loss = (-y_true*np.log(y_prob) - (1-y_true)*np.log(1-y_prob))/m
#計算梯度
self.__grad = (y_prob - y_true)/m
return loss.sum()
在MNIST數據集上驗證
現在使用MNIST分類任務驗證交叉熵損失函數。代碼位於examples/mlp/mnist-recognize.py文件中. 運行這個代碼前先把原始的MNIST數據集下載到examples/datasets/下並解壓. 數據集下載鏈接為:https://pan.baidu.com/s/1CmYYLyLJ87M8wH2iQWrrFA,密碼: 1rgr
訓練模型的代碼如下:
'''
訓練模型
'''
def fit():
inshape = ds_train.data.shape[1]
model = Model([
nn.Dense(10, inshape=inshape, activation='relu')
])
model.assemble()
sess = Session(model,
loss=losses.CategoricalCrossentropy(),
optimizer=optimizers.Fixed(0.001)
)
stop_fit = session.condition_callback(lambda :sess.stop_fit(), 'val_loss', 10)
#pdb.set_trace()
history = sess.fit(ds_train, 20000, val_epochs=5, val_data=ds_test,
listeners=[
stop_fit,
session.FitListener('val_end', callback=accuracy)
]
)
fit_report(history, report_path+"0.png")
擬合報告:
可以看出,通過一個小時(3699s), 將近600萬步的訓練,模型准確率達到了92%。同樣的模型在tensorflow(CPU版)中經過十幾分鍾的訓練即可達到91%。這說明, cute-dl框架在任務性能上是沒問題的,但訓練模型的速度欠佳。
總結
這個階段框架實現了對分類任務的支持, 在MNIST數據集上驗證模型性能達到預期。模型訓練的速度並不令人滿意。
下個階段,將會給模型添加學習率優化器, 在不損失泛化能力的同時加快模型訓練速度。