【Python】TensorFlow學習筆記(完):卷積深深深幾許

搭建卷積網路的時候,最常卡住的就是卷積核的參數設定。
其實背後的原理並不難,看過幾次就可以上手。

接下來,夏恩在此將延續上一篇的內容,順著卷積的脈絡走下去。
目標也是要來分類數字 0-9 共 10 個類別。

卷積層(convolution)

假設我們要輸入一張 42*42 像素的影像,並設計 5*5 的卷積核 4 個來進行卷積。
其中灰階影像和 RGB 影像差別在於輸入通道數的不同。

換句話說,若輸入影像有 N 的通道,卷積核就要有 N 個通道才能進行卷積。 
卷積核的結構可以是二維也可以是三維,依輸入圖像的通道數來決定。

設計的概念圖如下:

RGB 影像經過卷積運算後,輸出時會將各卷積核內所有通道加總。
也就是說不管輸入影像為幾個通道,輸出的影像通道數量都是固定的。

若單獨來看 RGB 的卷積運算過程的話,示意圖如下:

其實就算沒弄清楚,也不影響我們使用它;
不過了解原理可以用得更好。

要定義卷積層,少不了需要權重(W)和偏差(b)兩個變數。

我們首先設計Weight 和 bias 函數如下:
初始化的時候可以選擇使用常態或隨機分布,其均值和標準差都是可調整的。

def Weight(shape, mean=0, stddev=1):
    # 以常態分佈進行初始化
    init = tf.truncated_normal(shape, mean=mean, stddev=stddev)

    # 以隨機分佈進行初始化
    # init = tf.random_normal(shape, mean=mean, stddev=stddev)

    return tf.Variable(init)

def bias(shape, mean=0, stddev=1):
    init = tf.truncated_normal(shape, mean=mean, stddev=stddev)
    return tf.Variable(init)

# 預定義卷積運算子
def conv2d(x, W, strides=[1, 1, 1, 1]):
    return tf.nn.conv2d(x, W, strides=strides, padding='SAME')

定義完成,接著來構建第一個卷積層:

# Conv1
W_conv1 = Weight([5, 5, 1, 4])
b_conv1 = bias([4])
y_conv1 = conv2d(X, W_conv1) + b_conv1

# X 為輸入影像,格式為:[-1, 42, 42, 1]

最後那個變數 y_conv1 就是輸入影像與卷積核計算的結果。

在定義卷積層的時候,請注意 "strides=[1, 1, 1, 1]" 這句話。

strides 代表卷積核的移動步伐大小,夏恩自己設定是 [1, 1, 1, 1] ,所以不會影響卷積後的影像大小。
若設定改成  [1, 2, 2, 1] ,在 'SAME' 參數時,表示卷積後高與寬都會除以二,
同理可推,若改成 [1, 3, 4, 1] 的話,卷積後的高度除以三;寬度除以四,若有小數點就無條件進位。

若是 'VAILD' 參數時, 計算會比較複雜,輸出的尺寸為 input 的長度 + filter 的長度 - 1 再除以 Stride 值
詳細內容夏恩在上一篇文章中有說明,請讀者在設定參數時務必要詳讀規則。

活化層(activation)

在卷積層之後,我們大多數會給一個活化層。活化函數不會改變輸入資料的維度,
其主要功能是為了加入非線性因素,以彌補線性模型的表達力。

到底「非線性因素」是什麼樣的概念?

夏恩有找到一篇可視化的文章,可以順便觀摩一下高手們的風範。
可视化超参数作用机制:一、动画化激活函数

在 tensorflow 中,提供了非常多的活化函數,像是:

tf.nn.relu()
tf.nn.sigmoid()
tf.nn.tanh()
tf.nn.elu()
tf.nn.dropout()
......

有非常多可以選,對於活化函數的選擇,我們通常先考慮輸入資料的特徵。
當輸入資料的特徵較明顯時,用 tanh 的效果不錯,且在迭代的過程中會不斷地將特徵放大;當特徵不明顯時,用 sigmoid 會比較好。

除此之外,若拿不定主意該使用何種函數時,那就用目前最受歡迎 ReLU 吧!
ReLU 函數能保持 x>0 的時候梯度不衰減,緩解在訓練過程中梯度消失的問題,可以加速收斂。

ReLU 的數學式為:R(z) = max(0, z)

Image result for relu圖片來源:為何在梯度優化中使用ReLU

舉例來說,若輸入值為:[-1, -2, 0, 2, 1]。
根據 ReLU 的定義:R(z) = max(0, z),則輸出結果為:[0, 0, 0, 2, 1]。

根據上一步卷積層的結果,活化層的構建:

# ReLU1
relu1 = tf.nn.relu(y_conv1)

池化層(pooling)

在活化層之後,就是池化層。
池化的功能很單純,就是將輸入的影像縮小以減少特徵圖的維度並且保留重要的特徵,

其優點如下:

1. 減少後續計算層需要參數,加快系統運作的效率。
2. 抗干擾的作用,降低影像中的雜訊。
3. 減少過擬合。

其實第2,3點是同一件事,因為雜訊若不處理就很容易造成過擬合!

另外,池化層和卷積層相同,皆使用「核」來取出各區域的值並運算,目前主要有三種:
最大池化、平均池化、隨機池化。

以最大池化為例,定義池化核的大小為 2*2,步長為2。
如下圖,在 4*4 像素的影像上,池化核每次移動僅保留最大值。

池化函數的參數和卷積函數非常類似,夏恩就偷個懶,直接把上一篇的參數貼過來:

tf.nn.max_pool( input,
                ksize,
                strides,
                padding,
                data_format='NHWC',
                name=None )

1. input & data_format:

張量,資料類型為 float32 或 float64,預設資料格式由 data_format 參數設定。
預設為 data_format = 'NHWC',即輸入格式為 [ batch, height, width, channels ];
若改為 data_format = 'NCHW',即輸入格式為 [ batch, channels, height, width ];

batch:  圖片數量
height: 圖片高度
width:   圖片寬度
channels:圖片通道數,例如 RGB 影像 channels = 3;灰階影像 channels = 1。

2. filter:

池化核,資料類型為須與 input 相同,
其輸入格式為:[ filter_height, filter_width, in_channels,  out_channels]。

filter_height:   池化核的長度
filter_width:    池化核的寬度
in_channels:  輸入圖片的通道數
out_channels:輸出圖片的通道數

3. strides:

移動步數,資料格式為 List int ,長度為 4 的一維張量,每一個值與 input 各維度對應,
特別注意的是,strides 的第一個和第四個值必定為 1 ,即 strides[0] = strides[3] = 1。

4. padding:

直接舉例說明,假設 input width = 13;filter width = 6;stride = 5。

" VALID " = 不做任何填充,最右邊 column 與最下面 row 會被丟棄。對於 " VALID " 輸出的形狀是 ceil[ (I+F-1)/S ],也就是 input 的長度 + filter 的長度 - 1 再除以 Stride 值,最後無條件進位。

inputs:         1  2  3  4  5  6  7  8  9  10 11 (12 13)
               |________________|                dropped
                              |_________________|

" SAME " = 嘗試向左和向右均勻填充,但如果要添加的列數是奇數,它將向右添加額外的列,如本例中的情況。對於 "SAME " 輸出的形狀是 ceil[  I / S ],也就是 input 的長度除以 Stride 值,最後無條件進位。

            pad|                                      |pad
inputs:      0 |1  2  3  4  5  6  7  8  9  10 11 12 13|0  0
            |________________|
                           |_________________|
                                          |________________|

看明白池化函數的用法後,我們來實際試試看:
夏恩用真實的照片來展示池化的功能,以下為夏威夷的海灘照:

undefined圖片來源:2017年《夏威夷雜誌》票選年度夏威夷最佳5大海灘!

來看看威基基海灘經過夏恩蹂躪後,呃...我是說池化後,會變成如何:

# -*- coding: utf-8 -*-
"""
Created on Fri Feb 22 15:08:40 2019

@author: Shayne
"""

import cv2
import tensorflow as tf

I = cv2.imread('1.jpg')

# 池化核 5*5
ksize=[1, 5, 5, 1]
# 步長 = 2
strides=[1, 2, 2, 1]

# 輸入影像格式設定
img_shape = [1, I.shape[0], I.shape[1], I.shape[2]]

# 池化運算
x = tf.placeholder(tf.float32, shape = img_shape)
pool = tf.nn.max_pool(x, ksize, strides,'SAME')

with tf.Session() as sess:
    I1 = sess.run(pool, feed_dict={x:I.reshape(img_shape)})

# 轉回正常影像格式,並存檔
I1 = I1.reshape(I1.shape[1], I1.shape[2], I1.shape[3])  
cv2.imwrite('new.jpg', I1)

哇嗚!這什麼東西?

沒錯,您沒看錯,這就是池化的功能。
再強調一次,這不是均值濾波!

事實上,該影像的原圖為:550*1200 像素;池化後變成 275*600 像素。

先在這邊暫停一下。
其實池化跟卷積後輸出尺寸的計算方式一樣,同一套模式照搬過來就好。

以此範例來看,使用的池化核步長為 strides=[1, 2, 2, 1],格式為 'SAME',因此長與寬皆除以2。
若改成 strides=[1, 3, 4, 1],那麼輸出影像尺寸為長度除以3,寬度除以4。

好的,言歸正傳。

池化的功能就是要降低運算量,如上圖,雖然影像變模糊,但山還是山、水還是水。
對於神經網路來說,這樣能強化物體特徵且降低運算量,是很好用的技巧!

以下是構建池化層的程式碼:

# 池化層預定義
# 池化核之大小 = 2*2
# 步長 = 2
def max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1]):
    return tf.nn.max_pool(x, ksize=ksize, strides=strides, padding='SAME')

# Pool1
pool1 = max_pool(relu1)

再來一次

走筆至此,夏恩已經搭建了一個卷積+ReLU+池化的結構。
接著,可以再重複一次上述的過程。

當我們在設計卷積網路時,必須考慮需要多少個卷積層 — 太多浪費;太少沒用。
目前已經有許多經過驗證與效果不錯的網路架構,例如VGG,ResNet等,都可以作為參考的對象。

那這一次,夏恩把卷積核改成 3*3,輸出 2 張影像;
活化層仍然使用 ReLU;也使用同樣最大池化。示意圖如下:

補充:上圖中輸入影像為 RGB 影像為方便說明,實際夏恩所提供的程式範本是使用較簡單的灰階影像。

構建之程式碼:

# Conv2
W_conv2 = Weight([3, 3, 4, 2])
b_conv2 = bias([2])
y_conv2 = conv2d(pool1, W_conv2) + b_conv2
    
# ReLU2
relu2 = tf.nn.relu(y_conv2)
    
# Pool2
pool2 = max_pool(relu2)

全連接層(fully connected)

到了這一步,其實就回到傳統的神經網路了。

在前面提到的,卷積層、活化層和池化層等操作是為了找原始數據的空間特徵,
全連接層則是將前面算出來的特徵進行統整,因此主要功能為「分類器」。

在學術界對於全連接層的存在之必要性也是有相當的討論。
主要幾個說法是全連接層參數量大(因為全連接層的每個神經元都與上一層每個神經元相接),
耗費運算資源卻仍有大量的冗餘參數,容易導致過擬合使模型難以訓練......另一方面,
許多研究也提到全連接層可以做為模型的防火牆,在遷移學習的時候起到保護的作用。

參考資料:全連接層的作用是什麼?

但不論如何,既然我們是來練習搭建卷積網路的,那就直接用下去吧!

這邊夏恩設計如下:

1. 一個 16 個神經元的全連接層。
2. 輸出層(分 10 類,故有 10 個神經元)。

輸出層也是全連接層,只是它的每個神經元就對應了我們需要的輸出類別。

全連接層和前述的卷積層在圖示上最大的不同是對於權重變數的表現方式。
卷積層的權重畫法是一片片疊起來的,全連接層則使用灰線來代表;另外,
在卷積層的計算是用二維卷積函數(tf.nn.conv2d),在全連接層是用矩陣相乘(tf.matmul)。

最後要補充的就是在全連接層除了權重之外,同樣有偏差項,
不過為了圖面的整潔乾淨,夏恩就將偏差項省略不畫了。

若上面的這張示意圖沒問題的話,那就來寫一段程式吧!

# FC1
W_fc1 = Weight([11*11*2, 16])
b_fc1 = bias([16])

# 要把前面的網路的輸出壓扁成一維陣列
h_flat = tf.reshape(pool2, [-1, 11*11*2])
y_fc1 = tf.matmul(h_flat, W_fc1) + b_fc1

# ReLU3
relu3 = tf.nn.relu(y_fc1)

# FC2 - 輸出層
W_fc2 = Weight([16, 10]) # 分類0-9,共 10 類
b_fc2 = bias([10])

y = tf.matmul(relu3, W_fc2) + b_fc2

# ==============================================================#
#                以下為損失函數與最佳化方法設定
# ==============================================================#

# Cost optimizer
lossFcn = tf.nn.softmax_cross_entropy_with_logits_v2
cost = tf.reduce_mean(lossFcn(labels=y_, logits=y))

# 使用 AdamOptimizer 進行迭代
train_step = tf.train.AdamOptimizer(0.001).minimize(cost)

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

開始訓練模型

卷積網路建立完成後,就可以開始訓練了!
夏恩在此提供訓練用的 TFRecord 檔,有需要可自行下載:py_Train.tfrecords

最後附上訓練模型的原始碼:

# -*- coding: utf-8 -*-
"""
作者:Shayne
程式簡介:練習用的CNN範例
"""

import tensorflow as tf

def read_and_decode(filename, BATCH_SIZE, MAX_EPOCH): 
    # 建立文件名隊列
    filename_queue = tf.train.string_input_producer([filename], 
                                                    num_epochs=MAX_EPOCH)
    
    # 數據讀取器
    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 = 1000 + 3 * BATCH_SIZE,
                                 min_after_dequeue = 1000)

    return image_batch, label_batch

def Weight(shape, mean=0, stddev=1):
    init = tf.truncated_normal(shape, mean=mean, stddev=stddev)
    return tf.Variable(init)

def bias(shape, mean=0, stddev=1):
    init = tf.truncated_normal(shape, mean=mean, stddev=stddev)
    return tf.Variable(init)

def conv2d(x, W, strides=[1, 1, 1, 1]):
    return tf.nn.conv2d(x, W, strides=strides, padding='SAME')

def max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1]):
    return tf.nn.max_pool(x, ksize=ksize, strides=strides, padding='SAME')

# ========================================================================#
#                              主程式區                                   #
# ========================================================================#
    
filename = './py_Train.tfrecords'
BATCH_SIZE = 128
MAX_EPOCH = 20
LABEL_NUM = 10

# Input images & labels
X  = tf.placeholder(tf.float32, shape = [None, 42, 42, 1])
y_ = tf.placeholder(tf.float32, shape = [None, LABEL_NUM])

# 卷積網路建立
# Conv1
W_conv1 = Weight([5, 5, 1, 4])
b_conv1 = bias([4])
y_conv1 = conv2d(X, W_conv1) + b_conv1

# ReLU1
relu1 = tf.nn.relu(y_conv1)

# Pool1
pool1 = max_pool(relu1)

# Conv2
W_conv2 = Weight([3, 3, 4, 2])
b_conv2 = bias([2])
y_conv2 = conv2d(pool1, W_conv2) + b_conv2

# ReLU2
relu2 = tf.nn.relu(y_conv2)

# Pool2
pool2 = max_pool(relu2)

# FC1
W_fc1 = Weight([11*11*2, 16])
b_fc1 = bias([16])
h_flat = tf.reshape(pool2, [-1, 11*11*2])
y_fc1 = tf.matmul(h_flat, W_fc1) + b_fc1

# ReLU3
relu3 = tf.nn.relu(y_fc1)

# FC2 - 輸出層
W_fc2 = Weight([16, 10]) # 分類0-9,共 10 類
b_fc2 = bias([10])

y = tf.matmul(relu3, W_fc2) + b_fc2

# Cost optimizer
lossFcn = tf.nn.softmax_cross_entropy_with_logits_v2
cost = tf.reduce_mean(lossFcn(labels=y_, logits=y))

# 使用 AdamOptimizer 進行迭代
train_step = tf.train.AdamOptimizer(0.001).minimize(cost)

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

# 設定輸入資料來源
img_bat, lb_bat = read_and_decode(filename, BATCH_SIZE, MAX_EPOCH)
train_x = tf.reshape(img_bat, [-1, 42, 42, 1])
train_y = tf.one_hot(lb_bat, LABEL_NUM)

with tf.Session() as sess:
    # 變數初始化
    sess.run(tf.global_variables_initializer())
    sess.run(tf.local_variables_initializer())
    
    # 啟動 TFRecord 佇列
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)

    i = 0
    try:
        while not coord.should_stop():
            image_train, label_train = sess.run([train_x, train_y])
            sess.run(train_step, feed_dict={ X : image_train, 
                                             y_: label_train})
            
            if i % 50 == 0:
                train_accuracy = sess.run(accuracy, feed_dict={ X  : image_train, 
                                                                y_ : label_train})

                print('Iter %d, accuracy %4.2f%%' % (i,train_accuracy*100))
            i += 1
            
    except tf.errors.OutOfRangeError:
        print('Done!')
        
    finally:
        coord.request_stop()
            
    coord.join(threads)

使用本範例來訓練的結果,如無意外應該很糟。
那是因為夏恩為了簡化說明,把卷積網路的參數設很低:

這是正常的!

若把卷積層的參數調升,訓練結果就會大幅改善。
讀者可自行嘗試抽換卷積層參數:

# Conv1
W_conv1 = Weight([5, 5, 1, 16])
b_conv1 = bias([16])
y_conv1 = conv2d(X, W_conv1) + b_conv1

# ReLU1
relu1 = tf.nn.relu(y_conv1)

# Pool1
pool1 = max_pool(relu1)

# Conv2
W_conv2 = Weight([3, 3, 16, 32])
b_conv2 = bias([32])
y_conv2 = conv2d(pool1, W_conv2) + b_conv2

# ReLU2
relu2 = tf.nn.relu(y_conv2)

# Pool2
pool2 = max_pool(relu2)

# FC1
W_fc1 = Weight([11*11*32, 64])
b_fc1 = bias([64])
h_flat = tf.reshape(pool2, [-1, 11*11*32])
y_fc1 = tf.matmul(h_flat, W_fc1) + b_fc1

# ReLU3
relu3 = tf.nn.relu(y_fc1)

# FC2 - 輸出層
W_fc2 = Weight([64, 10]) # 分類0-9,共 10 類
b_fc2 = bias([10])

# 訓練用
y = tf.matmul(relu3, W_fc2) + b_fc2

抽換參數後,可以得到截然不同的訓練成果:

小結

TensorFlow的學習筆記到此終於告一段落。
接下來的其他分享內容會再開新的主題重新開始。

其實夏恩已經離開基礎的卷積網路很久了,
現在大多是在用 Faster-RCNN 或是 LSTM。

也使得這系列的文章就這樣一直欠著,直到今天。

可是夏恩覺得若沒有完成這篇文章,事情就不算結束。
所以就趁著周末空閒之餘做個了結,也算是給自己一個交代。

深度學習的世界很大,沒隔多久就會推出一堆新東西。
甚至,夏恩也可以預見本系列的文章再沒多久也會隨著 TF 的進步而被淘汰。

不過這也不是什麼大不了的事情。

我們只要確認自己還在前進的路上就好,
進步不會作假,學習的軌跡都會留下。

2019.02.23  Shayne

※ 備註 ※
本文中的說明圖示,若無說明圖片來源者皆為夏恩純手工製作,引用請註明出處。

附錄

夏恩有把程式改寫成 class 的形式,使用上比較容易,但不易閱讀。
因此把該程式放在附錄,有興趣的讀者可自行查閱。

相關資料可自行從夏恩的Github上下載:CNN-example

# -*- coding: utf-8 -*-
"""
作者:Shayne
程式簡介:使用 class 方式構建卷積網路
"""

import cv2
import tensorflow as tf

def read_and_decode(filename, BATCH_SIZE, MAX_EPOCH): 
    # 建立文件名隊列
    filename_queue = tf.train.string_input_producer([filename], 
                                                    num_epochs=MAX_EPOCH)
    
    # 數據讀取器
    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 = 1000 + 3 * BATCH_SIZE,
                                 min_after_dequeue = 1000)

    return image_batch, label_batch

class myCNN:
    
    def __init__(self, LABEL_NUM):
        # Hyperparameters
        self.LABEL_NUM  = LABEL_NUM
        self.sess = tf.Session()
        
        # Input images & labels
        self.x  = tf.placeholder(tf.float32, shape = [None, 42, 42, 1])
        self.y_ = tf.placeholder(tf.float32, shape = [None, self.LABEL_NUM])
        self.drop_prop = tf.placeholder(tf.float32)
        
    def weight_variable(self, shape, mean=0, stddev=1):
        initial = tf.truncated_normal(shape, mean=mean, stddev=stddev)
        return tf.Variable(initial)
    
    def bias_variable(self, shape, mean=0, stddev=1):
        initial = tf.truncated_normal(shape, mean=mean, stddev=stddev)
        return tf.Variable(initial)
    
    def max_pool_2x2(self, x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1]):
        return tf.nn.max_pool(x, ksize=ksize, strides=strides, padding='SAME')
    
    def conv2d(self, x, W, strides=[1, 1, 1, 1]):
        return tf.nn.conv2d(x, W, strides=strides, padding='SAME') 
    
    def build(self):
        # Layers
        # Conv1
        self.W_conv1 = self.weight_variable([5, 5, 1, 16])
        self.b_conv1 = self.bias_variable([16])
        self.y_conv1 = self.conv2d(self.x, self.W_conv1) + self.b_conv1
        # ReLU1
        self.relu1 = tf.nn.relu(self.y_conv1)
        # Pool1
        self.pool1 = self.max_pool_2x2(self.relu1)    
        # Conv2
        self.W_conv2 = self.weight_variable([3, 3, 16, 32])
        self.b_conv2 = self.bias_variable([32])
        self.y_conv2 = self.conv2d(self.pool1, self.W_conv2) + self.b_conv2
        # ReLU2
        self.relu2 = tf.nn.relu(self.y_conv2)      
        # Pool2
        self.pool2 = self.max_pool_2x2(self.relu2)
        # FC1
        self.W_fc1 = self.weight_variable([11*11*32, 128])
        self.b_fc1 = self.bias_variable([128])
        self.h_flat = tf.reshape(self.pool2, [-1, 11*11*32])
        self.y_fc1 = tf.matmul(self.h_flat, self.W_fc1) + self.b_fc1
        # ReLU3
        self.relu3 = tf.nn.relu(self.y_fc1)
        # dropout
        self.drop = tf.nn.dropout(self.relu3, self.drop_prop)
        # FC2
        self.W_fc2 = self.weight_variable([128, self.LABEL_NUM])
        self.b_fc2 = self.bias_variable([self.LABEL_NUM])
        self.y  = tf.matmul(self.drop, self.W_fc2) + self.b_fc2
        
        # for predict
        self.y1 = tf.matmul(self.relu3, self.W_fc2) + self.b_fc2
        self.y_pred = tf.argmax(tf.nn.softmax(self.y1), 1)
        
    def train(self, train_x, train_y):
        
        # Cost optimizer
        lossFcn = tf.nn.softmax_cross_entropy_with_logits_v2
        cost = tf.reduce_mean(lossFcn(labels=self.y_, logits=self.y))
    
        train_step = tf.train.AdamOptimizer(0.001).minimize(cost)
        
        correct_prediction = tf.equal(tf.argmax(self.y_, 1), tf.argmax(self.y, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        
        self.sess.run(tf.global_variables_initializer())
        self.sess.run(tf.local_variables_initializer())
        
        coord = tf.train.Coordinator()
        threads = tf.train.start_queue_runners(coord=coord, sess=self.sess)

        i = 0
        try:
            while not coord.should_stop():
                image_train, label_train = self.sess.run([train_x, train_y])
                self.sess.run(train_step, feed_dict={self.x : image_train, 
                                                     self.y_: label_train, 
                                                     self.drop_prop: 0.5})
                
                if i % 50 == 0:
                    train_accuracy = self.sess.run(accuracy, feed_dict={
                                                        self.x  : image_train, 
                                                        self.y_ : label_train,
                                                        self.drop_prop: 1.0})

                    print('Iter %d, accuracy %4.2f%%' % (i,train_accuracy*100))
                i += 1
                
        except tf.errors.OutOfRangeError:
            print('Done!')
            
        finally:
            coord.request_stop()
                
        coord.join(threads)

    # 存檔
    def save(self, save_path):
        self.saver = tf.train.Saver()
        tf.add_to_collection('x', self.x)
        tf.add_to_collection('y', self.y_pred)
        self.saver.save(self.sess, save_path)
        
    # 重建
    def restore(self, model_path):
        saver = tf.train.import_meta_graph(model_path+".meta")
        saver.restore(self.sess, model_path)
        self.x = tf.get_collection('x')[0]
        self.y_pred = tf.get_collection('y')[0]
    
    # 預測 
    def predict(self, img):
        img = img.reshape(-1,42,42,1)
        result = self.sess.run(self.y_pred, feed_dict={self.x: img})
        return result


# ========================================================================#
#                              主程式區                                   #
# ========================================================================#
        
filename = './py_Train.tfrecords'
BATCH_SIZE = 128
MAX_EPOCH = 20
LABEL_NUM = 10

# feed data
img_bat, lb_bat = read_and_decode(filename, BATCH_SIZE, MAX_EPOCH)
train_x = tf.reshape(img_bat, [-1, 42, 42, 1])
train_y = tf.one_hot(lb_bat, LABEL_NUM)

# 建模
Model = myCNN(LABEL_NUM)

Model.build()
Model.train(train_x, train_y)

I = cv2.imread('D:/Dataset/9/img18.jpg')
A = Model.predict(I[:,:,0]/255)
print(A)

# 存檔
Model.save('./model/test_model')

# 重新讀取檔案建模
Model1 = myCNN(LABEL_NUM)
Model1.restore('./model/test_model')

B = Model1.predict(I[:,:,0]/255)
print(B)