【Python】TensorFlow學習筆記(四):用 TFRecord 餵食 Softmax Model

弄清楚 TFRecord 檔案的讀寫後,真正的重頭戲才正要開始。

接續上一章的話題,當我們成功地把檔案從 TF 檔取出來的時候,
意味著我們得以去挑戰更高難度的境界 — 建模。

建模前,part 1

還記得上一篇的最後,我們把 TF 檔搞定了對吧?
在這邊夏恩先把之前的程式整理成一個函數,方便之後使用,

當然了,還能順便複習一下之前討論的內容。

def read_and_decode(filename, batch_size): 
    # 建立文件名隊列
    filename_queue = tf.train.string_input_producer([filename], 
                                                    num_epochs=None)
    
    # 數據讀取器
    reader = tf.TFRecordReader()
    _, serialized_example = reader.read(filename_queue)
    
    # 數據解析
    img_features = tf.parse_single_example(
            serialized_example,
            features={ 'Label'    : tf.FixedLenFeature([], tf.int64),
                       'image_raw': tf.FixedLenFeature([], tf.string), })
    
    image = tf.decode_raw(img_features['image_raw'], tf.uint8)
    image = tf.reshape(image, [42, 42])
    
    label = tf.cast(img_features['Label'], tf.int64)

    # 依序批次輸出 / 隨機批次輸出
    # tf.train.batch / tf.train.shuffle_batch
    image_batch, label_batch =tf.train.shuffle_batch(
                                 [image, label],
                                 batch_size=batch_size,
                                 capacity=10000 + 3 * batch_size,
                                 min_after_dequeue=1000)

    return image_batch, label_batch

    # tf.train.shuffle_batch 重要參數說明
    # tensors:   排列的張量。
    # batch_size:從隊列中提取新的批量大小。
    # capacity:  一個整數。隊列中元素的最大數量。
    # min_after_dequeue:出隊後隊列中的最小數量元素,用於確保元素的混合級別。
    

暫停一下,是不是有哪邊不太一樣?
那個「批次輸出」,應該沒有在上一章出現才對?

正確!這個「批次輸出」是為了建模才用上的函數。

Batch_Size 是機器學習中一個重要的參數,Batch 的選擇會決定梯度下降的方向。
批量大小定義了要通過網絡傳播的樣本數量。

如果數據集比較小,那麼可以採用全數據集的方式。
優點就是全數據集的方向更能代表母體,可以準確地找到極值方向。

但隨著數據集增加,夏恩不建議這麼做。
一次性載入所有的數據,只會增加記憶體罷工的機率而已。

另外,全批量的另外一個極端,就是單一批量 ——

一次只載入一個數據。

這種方法也有其缺點,因為每個樣本的修正方向以各自樣本的梯度方向修正,
批次愈小,對於方向的估計愈不準確。

因此我們需要找一個適中的 Batch_Size 值。
如果數據集足夠充分,那麼用一半(甚至少得多)的數據算出來的梯度與用全部數據幾乎一樣的。在合理範圍內,增大 Batch_Size ,能提高內存利用率,與大矩陣乘法的並行化效率。跑完一次 epoch (全數據集)所需的迭代次數減少,對於相同數據量的處理速度進一步加快。在一定範圍內,一般來說 Batch_Size 越大,其確定的下降方向越準,引起訓練震盪越小。

參考資料:https://www.zhihu.com/question/32673260

建模前,part 2

夏恩不確定有多少人發現這個問題。
那就是本系列文第二篇中,有提到的一個函數 get_File,其實是有改進空間的。

首先,夏恩得先澄清,這絕對不是挖坑給您跳,簡化是為了方便說明。
試想當時連程式該怎麼寫都不太確定,還要聽夏恩在一旁嘮叨理論什麼的,感覺肯定不太好吧!

所以當時所簡化的部分,就是現在所要討論的:

訓練集樣本不平衡的問題。

在學術研究與教學中,很多算法都有一個基本假設,那就是數據分佈是均勻的。
當我們把這些算法直接應用於實際數據時,大多數情況下都無法取得理想的結果。
因為實際數據往往分佈得很不均勻。

嚴格地講,任何數據集上都有數據不平衡現象,這往往由問題本身決定的。

不平衡程度相同(即正負樣本比例類似)的兩個問題,解決的難易程度也可能不同,因為問題難易程度還取決於我們所擁有數據有多大。解決這一問題的基本思路是讓正負樣本在訓練過程中擁有相同的話語權,比如利用採樣與加權等方法。採樣分為過採樣(Oversampling)和降採樣(Undersampling),過採樣是把小種類複製多份,降採樣是從大眾類中剔除一些樣本,或者說只從大眾類中選取部分樣本。

參考資料:

另外一篇有討論到訓練集樣本不平衡的論文為:
The Impact of Imbalanced Training Data for Convolutional Neural Networks

以下擷取片段來做說明:

該作者選用了 CIFAR-10 作為數據源來生成不平衡的樣本數據。
CIFAR-10是一個簡單的圖像分類數據集。
共有10類(airplane,automobile,bird,cat,deer,dog, frog,horse,ship,truck)。
每一類含有5000張訓練圖片,1000張測試圖片。

圖片來源:CNN训练Cifar-10技巧

下圖是作者所設計的數據集,圖中可以看出有資料集樣本不平衡的情況。

Dist. 1:類別平衡,每一類都佔用10%的數據。
Dist. 2、Dist. 3:一部分類別的數據比另一部分多。
Dist. 4、Dist 5:只有一類數據比較多。
Dist. 6、Dist 7:只有一類數據比較少。
Dist. 8: 數據個數呈線性分佈。
Dist. 9:數據個數呈指數級分佈。
Dist. 10、Dist. 11:交通工具對應的類別中的樣本數都比動物的多。

以上數據經過訓練後,每一類對應的預測正確率如下:

第一欄,Total 表示總的正確率,後面依序為每一類別分別的正確率。

從實驗結果中可以看出:

1. 類別完全平衡時,結果最好。
2. 類別愈是扭曲,效果越差。

例如 Dist. 3 就比 Dist. 2 更不平衡,效果更差。
其中 Dist. 5 和 Dist. 9 更是完全訓練失敗了。

另外,作者同時實驗了「過採樣」(oversampling)這種平衡數據集的方法。
這裡的過採樣方法是:對每一份數據集中比較少的類,直接複製其中的圖片增大樣本數量直至所有類別平衡。

可以發現過採樣的效果非常好,基本與平衡時候的表現一樣!
不過要注意的是過採樣由於是加大少數樣本的數量,過擬合幾乎是無可避免的問題。

若之後您發現手上的樣本數太少,無法降採樣,那麼可以考慮用過採樣的方法,效果還不錯。

參考資料:訓練集樣本不平衡問題對CNN的影響

有概念之後,我們在回過頭來看看之前寫的程式。

import os
import numpy as np

def get_File(file_dir):
    # The images in each subfolder
    images = []
    # The subfolders
    subfolders = []

    # Using "os.walk" function to grab all the files in each folder
    for dirPath, dirNames, fileNames in os.walk(file_dir):
        for name in fileNames:
            images.append(os.path.join(dirPath, name))

        for name in dirNames:
            subfolders.append(os.path.join(dirPath, name))

    # To record the labels of the image dataset
    labels = []
    count = 0
    for a_folder in subfolders:
        n_img = len(os.listdir(a_folder))
        labels = np.append(labels, n_img * [count])
        count+=1

    subfolders = np.array([images, labels])
    subfolders = subfolders.transpose()

    image_list = list(subfolders[:, 0])
    label_list = list(subfolders[:, 1])
    label_list = [int(float(i)) for i in label_list]
    return image_list, label_list

從這支程式可以看出來,完全沒有考慮到樣本不平衡的問題,對吧!

這邊只有單純地把資料夾底下所有的資料不分青紅皂白地通通抓過來,
然後一股腦兒,全部塞進 TFRecord 檔案內。

所以我們現在需要對這支程式稍作改造。
改造的方向朝著「降採樣」來走,也就是說需要新增的功能是:

1. 自動計算所有類別內的圖檔數量。
2. 以最低圖檔數的類別為基準,從每個類別隨機挑選出基準量的圖檔。

同理可推,若是要過採樣的話,就以最高圖檔數的類別為基準,
其他每個類別就不斷地複製既有的圖檔到指定的數量即可。
夏恩就斗膽省略「過採樣」的程式,偷懶一下。

另外一個話題是有關打亂數據。

之前提到,使用 tf.train.string_input_producer 時,設定 shuffle=True,就可以打亂讀取的順序。
但是這裡的打亂,是僅有打亂輸入的 filename List。

意思是,當我輸入的 filename 為 [1.jpg, 2.jpg, ...] 之類的,這沒問題,就像之前的範例一樣。

那...如果我輸入的是 TFRecord 檔呢?由於所有的檔名都包含在一個 TF 檔案內,
因此輸入的 filename 只有一個 —— 那就是 TF 檔的檔名!
這時候使用 shuffle=True 是沒有意義的。

解決的辦法就是在一開始製作 TF 檔案的時候就把樣本打亂。

根據上述說法,我們除了要降採樣之外,還要新增打亂原本數據的功能。
降採樣的程式經過改造後,範例如下:

import os
import random as r
import numpy as np

def get_File(file_dir):

    # The images in each subfolder
    images = []
    
    # The subfolders
    subfolders = []
 
    # Using "os.walk" function to grab all the files in each folder
    for dirPath, dirNames, fileNames in os.walk(file_dir):
        
        names = []
        for name in fileNames:
            names.append(os.path.join(dirPath, name))

        for name in dirNames:
            subfolders.append(os.path.join(dirPath, name))
        
        # 隨機打亂各個資料夾內的數據
        r.shuffle(names)
        if names != []:
            images.append(names)
         
    # 計算最小檔案數量的資料夾
    mincount = float("Inf")
    for num_folder in subfolders:
        n_img = len(os.listdir(num_folder))
        
        if n_img < mincount:
            mincount = n_img
    
    # 只保留最小檔案數量
    for i in range(len(images)):
        images[i] = images[i][0:mincount]
    
    images = np.reshape(images, [mincount*len(subfolders), ])
    
    # To record the labels of the image dataset
    labels = []
    for count in range(len(subfolders)):
        labels = np.append(labels, mincount * [count])
    
    # 打亂最後輸出的順序,去除每個類別間的隔閡
    subfolders = np.array([images, labels])
    subfolders = subfolders[:, np.random.permutation(subfolders.shape[1])].T
    
    image_list = list(subfolders[:, 0])
    label_list = list(subfolders[:, 1])
    label_list = [int(float(i)) for i in label_list]
    return image_list, label_list

OK,到此為止,複習完畢!
若您的 TFRecord 檔案還是使用舊版的 get_File,記得更新一下!

準備好之前做好的 TFRecord 檔,以及上面整理過的函數,準備來建一個模型吧!

建一個簡單的字元識別模型

這邊夏恩使用 Softmax Regression 來說明。

該模型是 Logistic Regression 在多分類問題上的推廣,在多分類問題中,類別標籤可以取兩個以上的值。Softmax Regression 對於諸如 MNIST 手寫數字分類等問題是很有用的,該問題的目的是辨識 10 個不同的單個數字。

在 Softmax Regression 中,我們解決的是多分類問題(相對於 logistic 回歸解決的二分類問題),類別標籤可以取多個不同的值。對於給定的測試輸入 ,我們想用函數針對每一個類別 j 估算出機率值 。也就是說,我們想估計的每一種分類結果出現的機率。因此,函數將要輸出一個一維的向量(向量元素的和為1)來表示這個估計的機率值。

原文網址:https://read01.com/zya4M2.html

Softmax 函數在數學,尤其是機率論和相關領域中,Softmax函數,或稱歸一化指數函數,是邏輯函數的一種推廣。它能將一個含任意實數的 K 維向量「壓縮」到另一個K維實向量中,使得每一個元素的範圍都在 (0,1) 之間,並且所有元素的和為1。

舉個例子:輸入向量[1,2,3,4,1,2,3],對應的Softmax函數的值為 [0.024, 0.064, 0.175, 0.475, 0.024, 0.064, 0.175]。輸出向量中擁有最大權重的項對應著輸入向量中的最大值「4」。這也顯示了這個函數通常的意義:對向量進行歸一化,凸顯其中最大的值並抑制遠低於最大值的其他分量。

import math

z = [1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0]
z_exp = [math.exp(i) for i in z]

print(z_exp)  
# Result: [2.72, 7.39, 20.09, 54.6, 2.72, 7.39, 20.09]

sum_z_exp = sum(z_exp)  
print(sum_z_exp)  
# Result: 114.98

softmax = [round(i / sum_z_exp, 3) for i in z_exp]
print(softmax)  
# Result: [0.024, 0.064, 0.175, 0.475, 0.024, 0.064, 0.175]

Softmax 函數資料來源:維基百科,Softmax函數

我們可以把模型表示成下圖:

化簡後得到:

圖片來源:https://www.tensorflow.org/versions/r1.2/get_started/mnist/beginners

用數學式來表示如下:

這邊再重複一次。
我們手上的影像資料,就是上式中的 x;我們想要的預估結果是 y。

所謂的訓練模型,就是不斷地跑迴圈,調整權重 W,還有誤差 b。
其實說是人工智慧,或者機器學習,不過就是透過現有的數據,擬定一套算法,
不斷調整,找尋一組可以解釋大部分事情的參數而已。

等等,不行,我不接受我的模型只能解釋 "大部分" 的事情!

問得好!

根據統計學的理論,我們獲得的資料都屬於 "樣本",因為我們通常沒有辦法獲得 "母體" 的資料。
要不就是母體太大,或是資料難以收集;因此人們嘗試歸納「樣本的特性」,來解釋母體。

所以說,模型真的就只能解釋大部分問題。
除非可以證明,我們取得的樣本就是母體,否則偏差將永遠存在!

到此,夏恩假設您已經深刻地了解數學式了。
接著拿起之前做好的 TFRecord 檔,準備來建模了!

首先,一開始寫好的函數,先放進來。

# -*- coding: utf-8 -*-
"""
作者:Shayne
程式簡介:有點難的簡單的建模程式
"""

import tensorflow as tf

def read_and_decode(filename, batch_size): 
    # 建立文件名隊列
    filename_queue = tf.train.string_input_producer([filename], num_epochs=None)
    
    # 數據讀取器
    reader = tf.TFRecordReader()
    _, serialized_example = reader.read(filename_queue)
    
    # 數據解析
    img_features = tf.parse_single_example(
            serialized_example,
            features={ 'Label'    : tf.FixedLenFeature([], tf.int64),
                       'image_raw': tf.FixedLenFeature([], tf.string), })
    
    image = tf.decode_raw(img_features['image_raw'], tf.uint8)
    image = tf.reshape(image, [42, 42])
    
    label = tf.cast(img_features['Label'], tf.int64)

    # 依序批次輸出 / 隨機批次輸出
    # tf.train.batch / tf.train.shuffle_batch
    image_batch, label_batch =tf.train.shuffle_batch(
                                 [image, label],
                                 batch_size=batch_size,
                                 capacity=10000 + 3 * batch_size,
                                 min_after_dequeue=1000)

    return image_batch, label_batch

接著,參數設定是免不了的。

# 自己做好的 TF 檔在哪裡,自己知道
filename = './py_Train.tfrecords'

# batch 可以自由設定
batch_size = 256

# 0-9共10個類別,請根據自己的資料修改
Label_size = 10

再來是重頭戲,模型設定。

# 調用剛才的函數
image_batch, label_batch = read_and_decode(filename, batch_size)

# 轉換陣列的形狀
image_batch_train = tf.reshape(image_batch, [-1, 42*42])

# 把 Label 轉換成獨熱編碼
label_batch_train = tf.one_hot(label_batch, Label_size)

# W 和 b 就是我們要訓練的對象
W = tf.Variable(tf.zeros([42*42, Label_size]))
b = tf.Variable(tf.zeros([Label_size]))

# 我們的影像資料,會透過 x 變數來輸入 
x = tf.placeholder(tf.float32, [None, 42*42])

# 這是參數預測的結果
y = tf.nn.softmax(tf.matmul(x, W) + b)

# 這是每張影像的正確標籤
y_ = tf.placeholder(tf.float32, [None, 10])

# 計算最小交叉熵
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y_, logits=y))

# 使用梯度下降法來找最佳解
train_step = tf.train.GradientDescentOptimizer(0.05).minimize(cross_entropy)

# 計算預測正確率
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

到剛才為止,只是設定模型參數,還沒有開始訓練。
最後才是開始訓練。

with tf.Session() as sess:
    # 初始化是必要的動作
    sess.run(tf.global_variables_initializer())
    sess.run(tf.local_variables_initializer())
    
    # 建立執行緒協調器
    coord = tf.train.Coordinator()
    
    # 啟動文件隊列,開始讀取文件
    threads = tf.train.start_queue_runners(coord=coord)
    
    # 迭代 10000 次,看看訓練的成果
    for count in range(10000):     
        # 這邊開始讀取資料
        image_data, label_data = sess.run([image_batch_train, label_batch_train])
   
        # 送資料進去訓練
        sess.run(train_step, feed_dict={x: image_data, y_: label_data})
        
        # 這裡是結果展示區,每 10 次迭代後,把最新的正確率顯示出來
        if count % 10 == 0:
            train_accuracy = accuracy.eval(feed_dict={x: image_data, y_: label_data})
            print('Iter %d, accuracy %4.2f%%' % (count, train_accuracy*100))

    # 結束後記得把文件名隊列關掉
    coord.request_stop() 
    coord.join(threads)

到這兒,模型就訓練完成了。

夏恩在這個範例中,設定的停止條件是迭代 1 萬次。
通常在一些大規模的數據集不會這麼做,畢竟設定迭代的次數看起來就很不科學。

如果 Boss 問:為什麼你要設定 1 萬次呢?
這時候回答:因為夏恩的範例程式寫1萬次啊!

這肯定是行不通的。

比較科學一點的停止條件像是:當正確率達到 99.99% 的時候,停止迴圈;
或是設定 Epoch 的最大次數,限制迴圈跑完 N 個 Epoch 的時候就停止。

相信這樣會比較有說服力。

本篇文章先在這兒打住,喘口氣,且讓夏恩先到外面呼吸一下新鮮空氣。
下一篇來聊聊該如何儲存模型與調用現有模型。

【Python】TensorFlow學習筆記(五):存檔 & 讀檔