百度PaddlePaddle入門-12(損失函數)


損失函數是模型優化的目標,用於衡量在無數的參數取值中,哪一個是最理想的。損失函數的計算在訓練過程的代碼中,每一輪的訓練代碼均是一致的過程:先根據輸入數據正向計算預測輸出,再根據預測值和真實值計算損失,最后根據損失反向傳播梯度並更新參數。

在之前的方案中,我們照抄了房價預測模型的損失函數-均方誤差。雖然從預測效果來看,使用均方誤差使得損失不斷下降,模型的預測值逐漸逼近真實值,但模型的最終效果不夠理想。原因是不同的機器學習任務有各自適宜的損失函數。房價預測是回歸任務,而手寫數字識別屬於分類任務。分類任務中使用均方誤差作為損失存在邏輯和效果上的缺欠,比如房價可以是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,則交叉熵誤差是−log⁡0.6=0.51;若“2”對應的輸出是0.1,則交叉熵誤差為−log⁡0.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



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM