【Python】TensorFlow學習筆記(二):初探 TFRecord

翻開 TensorFlow 的教學手冊,下一個動作大概是想要把教學手冊蓋起來。
會這樣講不是因為 TensorFlow 不好,而是教學手冊的確寫得很高端!

對,很高端!
若您想要體會一下「高端」?
上面的教學手冊有附上連結,進去參觀一下就是了。

言歸正傳。

在本文中,夏恩分三個部份來說明。

張量 — Tensor

TensorFlow 的計算單位,稱之為張量(Tensor)。而計算過程就是流(Flow)

這不是什麼大不了的東西,我們可以看到維基百科中對於張量的定義:

有兩種定義張量的方法:

  • 通常定義張量的物理學或傳統數學方法,是把張量看成一個多維數組,當變換座標或變換基底時,其分量會按照一定變換的規則,這些規則有兩種:即協變或逆變轉換。
  • 通常現代數學中的方法,是把張量定義成某個向量空間或其對偶空間上的多重線性映射,這向量空間在需要引入基底之前不固定任何座標系統。

但物理學家和工程師是首先識別出向量和張量作為實體具有物理上的意義的,它超越了它們的分量所被表述的(經常是任意的)座標系。同樣,數學家發現有一些張量關係在座標表示中更容易推導。

張量其實就是「陣列」的廣義說法。
可以是代表一維陣列(向量)、二維陣列(矩陣)、...、N維陣列(張量)。
N等於一的時候,就是向量;N等於二,就是矩陣。意即「向量」和「矩陣」的稱呼,只是張量的特例。
其實就是換湯不換藥,把我們平常習慣使用的「陣列」,改成聽起來很厲害的「張量」而已。

流 — Flow

這個「流」的概念,真的困擾夏恩好長一段時間。
嚴格說起來,平常我們在寫得程式,也是一種「流」。

宣告變數,然後傳遞變數,回傳,再繼續傳遞...最後輸出結果。

當程式啓動,資料就開始傳輸,直到程式結束。
不過 TensorFlow 的「流」和我們所習慣的「流」不太一樣。

TensorFlow 的流,需要把所有的節點設置好了以後,用「sess.run()」來啟動計算圖。
這時候,資料才會開始從外部或是從記憶體中輸入並且在函數間傳遞。

這樣講其實還是很抽象,舉個例子,如果夏恩想要寫一支「兩個變數相加」的程式。
用 C 語言來寫:

# include<stdio.h>
# include<stdlib.h>

int main()
{
    int A = 10;
    int B = 20;
    int C = A + B;
    printf("%d\n", C);
    system("pause");
}

同樣的功能,如果把程式搬到 TensorFlow 的話:

import tensorflow as tf

# 宣告常數
A = tf.constant(50)
B = tf.constant(100)

# 運算子
C = A + B

# 啟動計算圖
with tf.Session() as sess:
    print(sess.run(C))

一般而言,當程式執行到「C = A + B」時,「C」已經是「A+B」這個動作的執行結果了。
因此我們若是在 C 語言裡面,可以直接執行 printf,來秀出C變數的值。

但是,在 TensorFlow 內,並非如此。
而是等到程式執行到「sess.run()」這個動作,才能夠得到C的值。
若沒有使用它,就算程式已經執行過「C = A + B」這一段程式碼,仍然無法得到C的值。

口說無憑,直接跑跑看就知道。

最後一行是執行結果,答案不是150,而是傳回一個結構。

對於這個問題,夏恩自己的解釋為:

雖然在 TensorFlow 裡面,我們寫了「C = A + B」這個敘述句,
但實際上,是宣告了一個「具有相加功能」的類別!
我們必須要執行這個類別中的「Run」函數,才會真正實現相加的功能。

把 C 語言程式改寫成類別才可以確切的描述 TensorFlow 的執行過程,改寫後的程式如下:

# include<stdio.h>
# include<stdlib.h>

class add
{
    private:
        int X1, X2;
	
    public:
        //建構函數
        add(int A, int B)
        {
            X1 = A;
            X2 = B;
        }
	    
        // 執行後才能得到相加結果
        int run()
        {
            return X1 + X2;
        }
};

int main()
{
    int A = 10;
    int B = 20;
	
    //宣告具有相加功能的類別
    // C.X1 = 50; C.X2 = 100;
    add C = add(A, B);
	
    printf("%d\n", C.run());
    system("pause");
}

以上是夏恩的理解,實際上是不是真的這樣做,大概要去問問 Google 的工程師們才能確定。

讀取影像檔 & 寫入 TFRecord 檔

只要有去看過 TensorFlow 官網的人,大概都會有一個相同的疑問:

官網只有示範如何下載已打包的資料集,只有簡短的兩句話...

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

那如果是我自己的資料集勒!?
鬼才知道那個資料集是怎麼出現的...(翻桌)

所以接著我們就來打包自己的影像檔吧!
首先來看看怎麼用 Python 讀取影像檔,畢竟要寫檔案之前,要先把原始檔案讀取進來。

Python 大概提供幾種讀取影像檔的方法:

1. PIL.Image.open
2. scipy.ndimage.imread
3. scipy.misc.imread
4. skimage.io.imread
5. cv2.imread
6. matplotlib.image.imread

我個人是比較喜歡 5,也就是用 opencv 來幫忙,以下簡單示範讀檔用法:

import cv2

# Loads a color image
I1 = cv2.imread('/home/shayne/fig1.jpg')

# Loads image in grayscale mode
I2 = cv2.imread('/home/shayne/fig1.jpg', 0)

# Second argument is a flag which specifies the way image should be read.
# cv2.IMREAD_COLOR : Loads a color image. Any transparency of image will be neglected. It is the default flag.
# cv2.IMREAD_GRAYSCALE : Loads image in grayscale mode
# cv2.IMREAD_UNCHANGED : Loads image as such including alpha channel
# Note Instead of these three flags, you can simply pass integers 1, 0 or -1 respectively.

不論用哪一種方法,讀進來的檔案都是一樣的 uint8 格式。

不過上面這個是讀取一個檔案,如果有一群檔案的話,
以夏恩的檔案為例,其檔案結構長這樣:

一個資料夾內,包含了 0 ~ 9 個數字,每個資料夾內又有好幾千張圖檔。
這時候,我們得先設計一個函數,把所有的檔案名稱取回來,函數如下:

這邊先預告一下,這支 get_File 功能過於簡單,之後會新增降採樣&隨機打亂數據功能。
在此僅先用簡單的程式碼,較方便說明與理解。

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

以上程式碼改寫自:Tensorflow tutorial_TFRecord tutorial_02
另外補充os.walk的用法可以參考:Python:使用os.walk()遞迴印出資料夾中所有目錄及檔名

簡單來說,os.walk 會傳回一個由三個元素的 tuple 所組成的串列,
tuple 裡面的值分別是(資料夾名稱、下一層資料夾串列、本資料夾內所有的檔案串列),
由這些資料組合出所有的往下的樹狀目錄的內容。

從這個 tuple 的第一個參數可以知道目前在處理的資料夾是哪一個;
而第二個參數用來了解在此資料夾中還有幾個下層資料夾,分別叫什麼名字;
第三個參數就是此資料夾中的所有檔案名稱。

當我們把所有資料夾內的檔案名稱取回來之後,就可以把檔案轉化成 TensorFlow 專屬的格式:

—— TFRecords檔案格式。

這種檔案格式會把資料轉換成二進位的資料,在讀取時要搭配 TensorFlow 的檔案隊列(或稱佇列,Queue),才能把資料再讀取出來。

寫入TFRecord 檔案的標準做法是:

step 1. 把所有資料轉換成「tf.train.Feature」格式。

step 2. 把所有的「tf.train.Feature」包裝成「tf.train.Features」格式。

step 3. 把所有的「tf.train.Features」組合成「tf.train.Example」格式。

step 4. 利用「tf.python_io.TFRecordWriter」將「tf.train.Example」寫入成 TFRecord 檔案。

程式碼如下:

# 轉Int64資料為 tf.train.Feature 格式
def int64_feature(value):
    if not isinstance(value, list):
        value = [value]
    return tf.train.Feature(int64_list=tf.train.Int64List(value=value))

# 轉Bytes資料為 tf.train.Feature 格式
def bytes_feature(value):
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

def convert_to_TFRecord(images, labels, filename):
    n_samples = len(labels)
    TFWriter = tf.python_io.TFRecordWriter(filename)

    print('\nTransform start...')
    for i in np.arange(0, n_samples):
        try:
            image = cv2.imread(images[i], 0)

            if image is None:
                print('Error image:' + images[i])
            else:
                image_raw = image.tostring()

            label = int(labels[i])
            
            # 將 tf.train.Feature 合併成 tf.train.Features
            ftrs = tf.train.Features(
                    feature={'Label': int64_feature(label),
                             'image_raw': bytes_feature(image_raw)}
                   )
        
            # 將 tf.train.Features 轉成 tf.train.Example
            example = tf.train.Example(features=ftrs)

            # 將 tf.train.Example 寫成 tfRecord 格式
            TFWriter.write(example.SerializeToString())
        except IOError as e:
            print('Skip!\n')

    TFWriter.close()
    print('Transform done!')

以上程式碼同樣改寫自:Tensorflow tutorial_TFRecord tutorial_02

到這邊,讓我打個岔。
一開始夏恩會覺得困惑,若影像標籤(Labels)不是數字的話,還要用 Int64 來轉檔嗎?
例如要辨識的類別是貓、狗、外星人之類的。

對於這個問題,夏恩的回答是:沒錯,請繼續用 Int 的格式。 
用法是先在程式中進行編號,像是:

貓=1
狗=2
外星人=3
...

當辨識完成後,再轉換為原本的標籤。

其原因是當我們要建模的時候,會先將 Labels 使用獨熱編碼 tf.one_hot() 做轉換,像這樣:
>>>  labels = tf.one_hot( indices = labels, depth = depth )
也就是會將原本的標籤轉換成獨熱編碼。

這樣做的好處有:
    1.解決了分類器不好處理屬性數據的問題。
    2.在一定程度上也起到了擴充特徵的作用。

關於這部分,可以參考:
機器學習實戰:數據預處理之獨熱編碼(One-Hot Encoding)

言下之意,就算不使用 Int64 也是可以,但是可能會面臨更多的問題。

好,打岔完畢。

完成寫入 TFRecord 檔案的程式之後,現在可以繼續撰寫主程式來轉檔了! 

import os
import cv2
import numpy as np
import tensorflow as tf

def main():
    # 資料集的位置
    train_dataset_dir = '/home/shayne/Dataset_Train/'
    
    # 取回所有檔案路徑
    images, labels = get_File(train_dataset_dir)
    
    # 開始寫入 TRRecord 檔案
    convert_to_TFRecord(images, labels, '/home/shayne/Train.tfrecords')

if __name__ == '__main__':
    main()

如無意外,當程式執行完畢之後,在我們指定的路徑下,就可以看到「Train.tfrecords」這個檔案。

相關的讀寫 TFRecord 的文章有許多,可以參考:
- TensorFlow 輸入管線 Pipeline 從檔案讀取資料學習筆記
TensorFlow 寫入與讀取 TFRecords 檔案格式教學

今天先到這兒,希望對於如何寫 TFRecord 檔這一塊可以提供大家實質的幫助。
至於讀取 TFRecord 檔和建模,且讓夏恩休息一下。

下一章會延續這裡的話題,繼續討論該如何從 TFRecord 取出資料,
那將是一場硬仗,請喝杯咖啡再繼續看下去。

【Python】TensorFlow學習筆記(三):再探 TFRecord