損失函數是模型優化的目標,用於衡量在無數的參數取值中,哪一個是最理想的。損失函數的計算在訓練過程的代碼中,每一輪的訓練代碼均是一致的過程:先根據輸入數據正向計算預測輸出,再根據預測值和真實值計算損失,最后根據損失反向傳播梯度並更新參數。
在之前的方案中,我們照抄了房價預測模型的損失函數-均方誤差。雖然從預測效果來看,使用均方誤差使得損失不斷下降,模型的預測值逐漸逼近真實值,但模型的最終效果不夠理想。原因是不同的機器學習任務有各自適宜的損失函數。房價預測是回歸任務,而手寫數字識別屬於分類任務。分類任務中使用均方誤差作為損失存在邏輯和效果上的缺欠,比如房價可以是0-9之間的任何浮點數,手寫數字識別的數字只可能是0-9之間的10個實數值(標簽)。
在房價預測的案例中,因為房價本身是一個連續的實數值,以模型輸出的數值和真實房價差距作為損失函數(loss)是符合道理的。但對於分類問題,真實結果是標簽,而模型輸出是實數值,導致兩者相減的物理含義缺失。如果模型能輸出十個標簽的概率,對應真實標簽的概率輸出盡可能接近100%,而其他標簽的概率輸出盡可能接近0%,且所有輸出概率之和為1。這是一種更合理的假設!與此對應,真實的標簽值可以轉變成一個10維度的one-hot向量,在對應數字的位置上為1,其余位置為0,比如標簽“6”可以轉變成[0,0,0,0,0,1,0,0,0,0]。
為了實現上述假設,需要引入Softmax函數。它可以將原始輸出轉變成對應標簽的概率,公式如下。
softmax(xi)=exi / Sigmae j x(j=0-N,i=0,1,...c-1)
C是標簽類別個數。 從公式的形式可見,每個輸出的范圍均在0~1之間,且所有輸出之和等於1,這是這種變換后可被解釋成概率的基本前提。對應到代碼上,我們需要在網絡定義部分修改輸出層:self.fc = FC(name_scope, size=10, act='softmax'),即是對全連接層FC的輸出加一個softmax運算。
在該假設下,采用均方誤差衡量兩個概率的差別不是理論上最優的。人們習慣使用交叉熵誤差作為分類問題的損失衡量,因為后者有更合理的物理解釋,詳見《機器學習的思考故事》。
交叉熵的公式
L=-[ Sigma tk logyk + (1-yk) log(1-yk) ]
其中,log表示以e為底數的自然對數。yk代表模型輸出,tk代表各個標簽。tk中只有正確解的標簽為1,其余均為0(one-hot表示)。因此,交叉熵只計算對應着“正確解”標簽的輸出的自然對數。比如,假設正確標簽的索引是“2”,與之對應的神經網絡的輸出是0.6,則交叉熵誤差是−log0.6=0.51;若“2”對應的輸出是0.1,則交叉熵誤差為−log0.1=2.30。由此可見,交叉熵誤差的值是由正確標簽所對應的輸出結果決定的。
自然對數的函數曲線可由如下代碼顯示。
1 import matplotlib.pyplot as plt 2 import numpy as np 3 x = np.arange(0.01,1,0.01) 4 y = np.log(x) 5 plt.title("y=log(x)") 6 plt.xlabel("x") 7 plt.ylabel("y") 8 plt.plot(x,y) 9 plt.show() 10 plt.figure()
如自然對數的圖形所示,當x等於1時,y為0;隨着x向0靠近,y逐漸變小。因此,正確解標簽對應的輸出越大,交叉熵的值越接近0,對應loss越小;當輸出為1時,交叉熵誤差為0。反之,如果正確解標簽對應的輸出越小,則交叉熵的值越大,對應loss越大。
在手寫數字識別任務中,如果在現有代碼中將模型的損失函數替換成交叉熵(cross_entropy),僅改動三行代碼即可:在讀取數據部分,將標簽的類型設置成int,體現它是一個標簽而不是實數值(飛槳框架默認將標簽處理成int64);在網絡定義部分,將輸出層改成“輸出十個標簽的概率”的模式;以及在訓練過程部分,將損失函數從均方誤差換成交叉熵。
- 數據處理部分:label = np.reshape(labels[i], [1]).astype('int64')
- 網絡定義部分:self.fc = FC(name_scope, size=10, act='softmax')
- 訓練過程部分:loss = fluid.layers.cross_entropy(predict, label)
如下是在數據處理部分,修改標簽變量Label的格式。
- 從:label = np.reshape(labels[i], [1]).astype('float32')
- 到:label = np.reshape(labels[i], [1]).astype('int64')
1 #修改標簽數據的格式,從float32到int64 2 import os 3 import random 4 import paddle 5 import paddle.fluid as fluid 6 from paddle.fluid.dygraph.nn import Conv2D, Pool2D, FC 7 import numpy as np 8 from PIL import Image 9 10 import gzip 11 import json 12 13 # 定義數據集讀取器 14 def load_data(mode='train'): 15 16 # 數據文件 17 datafile = './work/mnist.json.gz' 18 print('loading mnist dataset from {} ......'.format(datafile)) 19 data = json.load(gzip.open(datafile)) 20 train_set, val_set, eval_set = data 21 22 # 數據集相關參數,圖片高度IMG_ROWS, 圖片寬度IMG_COLS 23 IMG_ROWS = 28 24 IMG_COLS = 28 25 26 if mode == 'train': 27 imgs = train_set[0] 28 labels = train_set[1] 29 elif mode == 'valid': 30 imgs = val_set[0] 31 labels = val_set[1] 32 elif mode == 'eval': 33 imgs = eval_set[0] 34 labels = eval_set[1] 35 36 imgs_length = len(imgs) 37 38 assert len(imgs) == len(labels), \ 39 "length of train_imgs({}) should be the same as train_labels({})".format( 40 len(imgs), len(labels)) 41 42 index_list = list(range(imgs_length)) 43 44 # 讀入數據時用到的batchsize 45 BATCHSIZE = 100 46 47 # 定義數據生成器 48 def data_generator(): 49 if mode == 'train': 50 random.shuffle(index_list) 51 imgs_list = [] 52 labels_list = [] 53 for i in index_list: 54 img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32') 55 label = np.reshape(labels[i], [1]).astype('int64') 56 imgs_list.append(img) 57 labels_list.append(label) 58 if len(imgs_list) == BATCHSIZE: 59 yield np.array(imgs_list), np.array(labels_list) 60 imgs_list = [] 61 labels_list = [] 62 63 # 如果剩余數據的數目小於BATCHSIZE, 64 # 則剩余數據一起構成一個大小為len(imgs_list)的mini-batch 65 if len(imgs_list) > 0: 66 yield np.array(imgs_list), np.array(labels_list) 67 68 return data_generator
如下是在網絡定義部分,修改輸出層結構。
- 從:self.fc = FC(name_scope, size=1, act=None)
- 到:self.fc = FC(name_scope, size=10, act='softmax')
1 # 定義模型結構 2 class MNIST(fluid.dygraph.Layer): 3 def __init__(self, name_scope): 4 super(MNIST, self).__init__(name_scope) 5 name_scope = self.full_name() 6 # 定義一個卷積層,使用relu激活函數 7 self.conv1 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act='relu') 8 # 定義一個池化層,池化核為2,步長為2,使用最大池化方式 9 self.pool1 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 10 # 定義一個卷積層,使用relu激活函數 11 self.conv2 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act='relu') 12 # 定義一個池化層,池化核為2,步長為2,使用最大池化方式 13 self.pool2 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 14 # 定義一個全連接層,輸出節點數為10 15 self.fc = FC(name_scope, size=10, act='softmax') 16 # 定義網絡的前向計算過程 17 def forward(self, inputs): 18 x = self.conv1(inputs) 19 x = self.pool1(x) 20 x = self.conv2(x) 21 x = self.pool2(x) 22 x = self.fc(x) 23 return x
如下代碼僅修改計算損失的函數,從均方誤差(常用於回歸問題)到交叉熵誤差(常用於分類問題)。
- 從:loss = fluid.layers.square_error_cost(predict, label)
- 到:loss = fluid.layers.cross_entropy(predict, label)
1 #僅修改計算損失的函數,從均方誤差(常用於回歸問題)到交叉熵誤差(常用於分類問題) 2 with fluid.dygraph.guard(): 3 model = MNIST("mnist") 4 model.train() 5 #調用加載數據的函數 6 train_loader = load_data('train') 7 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01) 8 EPOCH_NUM = 5 9 for epoch_id in range(EPOCH_NUM): 10 for batch_id, data in enumerate(train_loader()): 11 #准備數據,變得更加簡潔 12 image_data, label_data = data 13 image = fluid.dygraph.to_variable(image_data) 14 label = fluid.dygraph.to_variable(label_data) 15 16 #前向計算的過程 17 predict = model(image) 18 19 #計算損失,使用交叉熵損失函數,取一個批次樣本損失的平均值 20 loss = fluid.layers.cross_entropy(predict, label) 21 avg_loss = fluid.layers.mean(loss) 22 23 #每訓練了200批次的數據,打印下當前Loss的情況 24 if batch_id % 200 == 0: 25 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 26 27 #后向傳播,更新參數的過程 28 avg_loss.backward() 29 optimizer.minimize(avg_loss) 30 model.clear_gradients() 31 32 #保存模型參數 33 fluid.save_dygraph(model.state_dict(), 'mnist')
loading mnist dataset from ./work/mnist.json.gz ...... epoch: 0, batch: 0, loss is: [2.609301] epoch: 0, batch: 200, loss is: [0.36067933] epoch: 0, batch: 400, loss is: [0.3503476] epoch: 1, batch: 0, loss is: [0.29702342] epoch: 1, batch: 200, loss is: [0.15377608] epoch: 1, batch: 400, loss is: [0.1849378] epoch: 2, batch: 0, loss is: [0.08589315] epoch: 2, batch: 200, loss is: [0.10543882] epoch: 2, batch: 400, loss is: [0.07615029] epoch: 3, batch: 0, loss is: [0.1301367] epoch: 3, batch: 200, loss is: [0.17038517] epoch: 3, batch: 400, loss is: [0.13615657] epoch: 4, batch: 0, loss is: [0.16349195] epoch: 4, batch: 200, loss is: [0.1656445] epoch: 4, batch: 400, loss is: [0.06402704]
雖然上述訓練過程的損失明顯比使用均方誤差算法要小,但因為損失函數量綱的變化,我們無法從比較兩個不同的Loss得出誰更加優秀。怎么解決這個問題呢?我們可以回歸到問題的直接衡量,誰的分類准確率高來判斷。
因為我們修改了模型的輸出格式,所以使用模型做預測時的代碼也需要做相應的調整。從模型輸出10個標簽的概率中選擇最大的,將其標簽編號輸出。
1 # 讀取一張本地的樣例圖片,轉變成模型輸入的格式 2 def load_image(img_path): 3 # 從img_path中讀取圖像,並轉為灰度圖 4 im = Image.open(img_path).convert('L') 5 im.show() 6 im = im.resize((28, 28), Image.ANTIALIAS) 7 im = np.array(im).reshape(1, 1, 28, 28).astype(np.float32) 8 # 圖像歸一化 9 im = 1.0 - im / 255. 10 return im 11 12 # 定義預測過程 13 with fluid.dygraph.guard(): 14 model = MNIST("mnist") 15 params_file_path = 'mnist' 16 img_path = './work/example_0.jpg' 17 # 加載模型參數 18 model_dict, _ = fluid.load_dygraph("mnist") 19 model.load_dict(model_dict) 20 21 model.eval() 22 tensor_img = load_image(img_path) 23 #模型反饋10個分類標簽的對應概率 24 results = model(fluid.dygraph.to_variable(tensor_img)) 25 #取概率最大的標簽作為預測輸出 26 lab = np.argsort(results.numpy()) 27 print("本次預測的數字是: ", lab[0][-1])
本次預測的數字是: 0