【譯】TensorFlow Tutorial #02 Convolutional Neural Network
溫馨提示,TensorFlow更新的太快了,有些程式碼實現方式可能變了,但是思想還是沒有變滴,主要還是理解 ofollow,noindex">原文地址
介紹
前面的教程表明,簡單的線性模型具有大約91%的分類準確度,用於識別MNIST資料集中的手寫數字。
在本教程中,我們將在TensorFlow中實現一個簡單的卷積神經網路,如果您進行一些建議的練習,其分類精度約為99%或更高。
卷積網路通過在輸入影象上移動小濾鏡來工作。 這意味著過濾器被重新用於識別整個輸入影象中的模式。 這使得卷積網路比具有相同數量的變數的完全連線網路更強大。 這反過來使Convolutional Networks更快地進行訓練。
您應該熟悉基本的線性代數,Python和Jupyter Notebook編輯器。 TensorFlow的初學者也可能希望在繼續學習之前學習第一篇教程。
Flowchart下圖大致顯示了下面實現的卷積神經網路中資料的流動方式。

使用濾波器權重在第一卷積層中處理輸入影象。 這導致16個新影象,一個用於卷積層中的每個濾波器。 影象也進行了下采樣,因此影象解析度從28x28降低到14x14。
然後在第二卷積層中處理這16個較小的影象。 我們需要為這16個通道中的每個通道提供濾波器權重,並且我們需要對該層的每個輸出通道使用濾波器權重。 有36個輸出通道,因此在第二個卷積層中總共有16 x 36 = 576個濾波器。 生成的影象再次下采樣到7x7畫素。
第二卷積層的輸出是每個7×7畫素的36個影象。 然後將它們展平為長度為7×7×36 = 1764的單個向量,其用作具有128個神經元(或元件)的完全連線層的輸入。 這將進入另一個具有10個神經元的完全連線的層,每個類對應一個類,用於確定影象的類別,即影象中描繪的數字。
卷積濾波器最初是隨機選擇的,因此分類是隨機進行的。 輸入影象的預測類和真實類之間的誤差被測量為所謂的交叉熵。 然後,優化器使用區分鏈規則自動將此錯誤傳播回捲積網路,並更新過濾器權重,以改善分類錯誤。 這反覆進行數千次,直到分類誤差足夠低。
這些特定的濾鏡權重和中間影象是一次優化執行的結果,如果重新執行此Notebook,可能會有所不同。
請注意,TensorFlow中的計算實際上是在一批影象而不是單個影象上完成的,這使得計算更有效。 這意味著當在TensorFlow中實現時,流程圖實際上還有一個數據維度。
Convolutional Layer
下圖顯示了在第一個卷積層中處理影象的基本思想。 輸入影象描繪了數字7,此處顯示了四個影象副本,因此我們可以更清楚地看到濾鏡如何移動到影象的不同位置。 對於濾鏡的每個位置,在濾鏡和濾鏡下的影象畫素之間計算點積,這導致輸出影象中的單個畫素。 因此,在整個輸入影象上移動濾鏡會導致生成新影象。
紅色濾鏡權重意味著濾鏡對輸入影象中的黑色畫素具有正反應,而藍色畫素意味著濾鏡對黑色畫素具有負反應。
在這種情況下,過濾器可以識別出7位數的水平線,從輸出影象中對該線的較強反應可以看出。

在輸入上移動過濾器的步長稱為步幅。 水平移動過濾器(x軸)和另一個垂直移動步幅(y軸)有一個跨度。
在下面的原始碼中,步幅在兩個方向上都設定為1,這意味著濾鏡從輸入影象的左上角開始,並在每個步驟中向右移動1個畫素。 當濾鏡到達影象右側的末尾時,濾鏡將向後移動到影象的左側和1個畫素。 這將一直持續到濾鏡到達輸入影象的右下角並生成整個輸出影象。
當濾鏡到達輸入影象的右側和底部的末端時,可以用零填充(白色畫素)。 這會使輸出影象與輸入影象具有完全相同的尺寸。
此外,卷積的輸出可以通過所謂的整流線性單元(ReLU),其僅確保輸出為正,因為負值被設定為零。 輸出也可以通過所謂的max-pooling進行下采樣,max-pooling考慮2x2畫素的小視窗並且僅保留這些畫素中的最大畫素。 這使輸入影象的解析度減半,例如 從28x28到14x14畫素。
注意,第二卷積層更復雜,因為它需要16個輸入通道。 我們想為每個輸入通道分別使用一個過濾器,因此我們需要16個過濾器而不是一個過濾器。 此外,我們需要來自第二卷積層的36個輸出通道,因此總共需要16 x 36 = 576個濾波器用於第二個卷積層。 瞭解其工作原理可能有點難度。
Imports
%matplotlib inline import matplotlib.pyplot as plt import tensorflow as tf import numpy as np from sklearn.metrics import confusion_matrix import time from datetime import timedelta import math 複製程式碼
Configuration of Neural Network
為方便起見,此處定義了卷積神經網路的配置,因此您可以輕鬆查詢和更改這些數字並重新執行Notebook。
# Convolutional Layer 1. filter_size1 = 5# Convolution filters are 5 x 5 pixels. num_filters1 = 16# There are 16 of these filters. # Convolutional Layer 2. filter_size2 = 5# Convolution filters are 5 x 5 pixels. num_filters2 = 36# There are 36 of these filters. # Fully-connected layer. fc_size = 128# Number of neurons in fully-connected layer. 複製程式碼
Load Data
MNIST資料集大約為12 MB,如果它不在給定路徑中,將自動下載。
from mnist import MNIST data = MNIST(data_dir="data/MNIST/") 複製程式碼
複製程式碼MNIST資料集現已載入,由70,000個影象和影象的類號組成。 資料集被分成3個互斥的子集。 我們將僅使用本教程中的培訓和測試集。
print("Size of:") print("- Training-set:\t\t{}".format(data.num_train)) print("- Validation-set:\t{}".format(data.num_val)) print("- Test-set:\t\t{}".format(data.num_test)) Size of: - Training-set:55000 - Validation-set:5000 - Test-set:10000 複製程式碼
複製程式碼為方便起見,複製一些資料維度。
# The number of pixels in each dimension of an image. img_size = data.img_size # The images are stored in one-dimensional arrays of this length. img_size_flat = data.img_size_flat # Tuple with height and width of images used to reshape arrays. img_shape = data.img_shape # Number of classes, one class for each of 10 digits. num_classes = data.num_classes # Number of colour channels for the images: 1 channel for gray-scale. num_channels = data.num_channels 複製程式碼
Helper-function for plotting images
用於在3x3網格中繪製9個影象,並在每個影象下面寫入真實和預測類的函式。
def plot_images(images, cls_true, cls_pred=None): assert len(images) == len(cls_true) == 9 # Create figure with 3x3 sub-plots. fig, axes = plt.subplots(3, 3) fig.subplots_adjust(hspace=0.3, wspace=0.3) for i, ax in enumerate(axes.flat): # Plot image. ax.imshow(images[i].reshape(img_shape), cmap='binary') # Show true and predicted classes. if cls_pred is None: xlabel = "True: {0}".format(cls_true[i]) else: xlabel = "True: {0}, Pred: {1}".format(cls_true[i], cls_pred[i]) # Show the classes as the label on the x-axis. ax.set_xlabel(xlabel) # Remove ticks from the plot. ax.set_xticks([]) ax.set_yticks([]) # Ensure the plot is shown correctly with multiple plots # in a single Notebook cell. plt.show() 複製程式碼
Plot a few images to see if data is correct
# Get the first images from the test-set. images = data.x_test[0:9] # Get the true classes for those images. cls_true = data.y_test_cls[0:9] # Plot the images and labels using our helper-function above. plot_images(images=images, cls_true=cls_true) 複製程式碼

TensorFlow Graph
#Helper-functions for creating new variables(輔助函式用於建立新變數)
用於在給定形狀中建立新TensorFlow變數並使用隨機值初始化它們的函式。 請注意,此時實際上並未完成初始化,它僅在TensorFlow圖中定義。
def new_weights(shape): return tf.Variable(tf.truncated_normal(shape, stddev=0.05)) def new_biases(length): return tf.Variable(tf.constant(0.05, shape=[length])) 複製程式碼
#Helper-function for creating a new Convolutional Layer(輔助功能,用於建立新的卷積層)
此函式在TensorFlow的計算圖中建立一個新的卷積層。 這裡沒有實際計算,我們只是將數學公式新增到TensorFlow圖。
假設輸入是具有以下尺寸的4維張量:
影象編號。 每個影象的Y軸。 每個影象的X軸。 每個影象的通道。 複製程式碼
注意,輸入通道可以是顏色通道,或者如果輸入是從先前的卷積層產生的,則它可以是濾波器通道。
輸出是另一個4-dim張量,具有以下尺寸:
影象編號,與輸入相同。 每個影象的Y軸。 如果使用2x2池,則輸入影象的高度和寬度除以2。 每個影象的X軸。 同上。 卷積濾波器產生的通道。 複製程式碼
def new_conv_layer(input,# The previous layer. num_input_channels, # Num. channels in prev. layer. filter_size,# Width and height of each filter. num_filters,# Number of filters. use_pooling=True):# Use 2x2 max-pooling. # Shape of the filter-weights for the convolution. # This format is determined by the TensorFlow API. shape = [filter_size, filter_size, num_input_channels, num_filters] # Create new weights aka. filters with the given shape. weights = new_weights(shape=shape) # Create new biases, one for each filter. biases = new_biases(length=num_filters) # Create the TensorFlow operation for convolution. # Note the strides are set to 1 in all dimensions. # The first and last stride must always be 1, # because the first is for the image-number and # the last is for the input-channel. # But e.g. strides=[1, 2, 2, 1] would mean that the filter # is moved 2 pixels across the x- and y-axis of the image. # The padding is set to 'SAME' which means the input image # is padded with zeroes so the size of the output is the same. layer = tf.nn.conv2d(input=input, filter=weights, strides=[1, 1, 1, 1], padding='SAME') # Add the biases to the results of the convolution. # A bias-value is added to each filter-channel. layer += biases # Use pooling to down-sample the image resolution? if use_pooling: # This is 2x2 max-pooling, which means that we # consider 2x2 windows and select the largest value # in each window. Then we move 2 pixels to the next window. layer = tf.nn.max_pool(value=layer, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') # Rectified Linear Unit (ReLU). # It calculates max(x, 0) for each input pixel x. # This adds some non-linearity to the formula and allows us # to learn more complicated functions. layer = tf.nn.relu(layer) # Note that ReLU is normally executed before the pooling, # but since relu(max_pool(x)) == max_pool(relu(x)) we can # save 75% of the relu-operations by max-pooling first. # We return both the resulting layer and the filter-weights # because we will plot the weights later. return layer, weights 複製程式碼
#Helper-function for flattening a layer(輔助功能,用於展平圖層)
卷積層產生具有4維的輸出張量。 我們將在卷積層之後新增完全連線的層,因此我們需要將4-dim張量減小到2-dim,這可以用作完全連線層的輸入。
def flatten_layer(layer): # Get the shape of the input layer. layer_shape = layer.get_shape() # The shape of the input layer is assumed to be: # layer_shape == [num_images, img_height, img_width, num_channels] # The number of features is: img_height * img_width * num_channels # We can use a function from TensorFlow to calculate this. num_features = layer_shape[1:4].num_elements() # Reshape the layer to [num_images, num_features]. # Note that we just set the size of the second dimension # to num_features and the size of the first dimension to -1 # which means the size in that dimension is calculated # so the total size of the tensor is unchanged from the reshaping. layer_flat = tf.reshape(layer, [-1, num_features]) # The shape of the flattened layer is now: # [num_images, img_height * img_width * num_channels] # Return both the flattened layer and the number of features. return layer_flat, num_features 複製程式碼
#Helper-function for creating a new Fully-Connected Layer(輔助功能,用於建立新的全連線層)
此函式在TensorFlow的計算圖中建立一個新的完全連線層。 這裡沒有實際計算,我們只是將數學公式新增到TensorFlow圖。
假設輸入是形狀[di_images,num_inputs]的二維張量。 輸出是2-dim張量的形狀[num_images,num_outputs]。
def new_fc_layer(input,# The previous layer. num_inputs,# Num. inputs from prev. layer. num_outputs,# Num. outputs. use_relu=True): # Use Rectified Linear Unit (ReLU)? # Create new weights and biases. weights = new_weights(shape=[num_inputs, num_outputs]) biases = new_biases(length=num_outputs) # Calculate the layer as the matrix multiplication of # the input and weights, and then add the bias-values. layer = tf.matmul(input, weights) + biases # Use ReLU? if use_relu: layer = tf.nn.relu(layer) return layer 複製程式碼
#Placeholder variables(佔位符變數) 佔位符變數用作TensorFlow計算圖的輸入,每次執行圖時我們都可以更改。 我們稱之為佔位符變數,並在下面進一步說明。
首先,我們為輸入影象定義佔位符變數。 這允許我們更改輸入到TensorFlow圖形的影象。 這是一個所謂的張量,這意味著它是一個多維向量或矩陣。 資料型別設定為float32,形狀設定為[None,img_size_flat],其中None表示張量可以保持任意數量的影象,每個影象是長度為img_size_flat的向量。
x = tf.placeholder(tf.float32, shape=[None, img_size_flat], name='x') 複製程式碼
卷積層期望x被編碼為4-dim張量,因此我們必須對其進行整形,使其形狀為[num_images,img_height,img_width,num_channels]。 請注意,img_height == img_width == img_size和num_images可以使用-1作為第一個維度的大小自動推斷。 所以重塑操作是:
x_image = tf.reshape(x, [-1, img_size, img_size, num_channels]) 複製程式碼
接下來,我們有佔位符變數,用於與佔位符變數x中輸入的影象相關聯的真實標籤。 此佔位符變數的形狀為[None,num_classes],這意味著它可以包含任意數量的標籤,每個標籤是長度為num_classes的向量,在這種情況下為10。
y_true = tf.placeholder(tf.float32, shape=[None, num_classes], name='y_true') 複製程式碼
我們也可以為label設定佔位符變數,但我們將使用argmax計算它。 請注意,這是一個TensorFlow運算子,因此此時不計算任何內容。
y_true_cls = tf.argmax(y_true, axis=1) 複製程式碼
Convolutional Layer 1
建立第一個卷積層。 它將x_image作為輸入並建立num_filters1個不同的過濾器,每個過濾器的寬度和高度等於filter_size1。 最後,我們希望通過使用2x2 max-pooling對影象進行下采樣,使其大小減半。
layer_conv1, weights_conv1 = \ new_conv_layer(input=x_image, num_input_channels=num_channels, filter_size=filter_size1, num_filters=num_filters1, use_pooling=True) 複製程式碼
檢查將由卷積層輸出的張量的形狀。 它是(?,14,14,16),這意味著有任意數量的影象(這是?),每個影象寬14畫素,高14畫素,有16個不同的通道,每個通道一個通道 過濾器
layer_conv1 <tf.Tensor 'Relu:0' shape=(?, 14, 14, 16) dtype=float32> 複製程式碼
Convolutional Layer 2
建立第二個卷積層,它將第一個卷積層的輸出作為輸入。 輸入通道的數量對應於第一卷積層中的濾波器的數量。
layer_conv2, weights_conv2 = \ new_conv_layer(input=layer_conv1, num_input_channels=num_filters1, filter_size=filter_size2, num_filters=num_filters2, use_pooling=True) 複製程式碼
檢查將從此卷積層輸出的張量的形狀。 形狀是(?,7,7,36)? 再次表示存在任意數量的影象,每個影象的寬度和高度為7個畫素,並且有36個通道,每個濾波器一個。
layer_conv2 <tf.Tensor 'Relu_1:0' shape=(?, 7, 7, 36) dtype=float32> 複製程式碼
Flatten Layer
卷積層輸出4-dim張量。 我們現在希望在完全連線的網路中使用這些作為輸入,這需要將張量重新整形或展平為2-dim張量。
layer_flat, num_features = flatten_layer(layer_conv2) 複製程式碼
檢查張量現在是否具有形狀(?,1764),這意味著有任意數量的影象已被展平為每個長度為1764的向量。 請注意,1764 = 7 x 7 x 36。
In [23]:layer_flat Out[23]:<tf.Tensor 'Reshape_1:0' shape=(?, 1764) dtype=float32> In [24]:num_features Out[24]:1764 複製程式碼
Fully-Connected Layer 1
將完全連線的層新增到網路中。 輸入是前一卷積的平坦層。 完全連線層中的神經元或節點的數量是fc_size。 使用ReLU,我們可以學習非線性關係。
layer_fc1 = new_fc_layer(input=layer_flat, num_inputs=num_features, num_outputs=fc_size, use_relu=True) 複製程式碼
檢查完全連線層的輸出是否為形狀(?,128)的張量? 意味著有任意數量的影象,fc_size == 128。
In [26]:layer_fc1 Out[26]:<tf.Tensor 'Relu_2:0' shape=(?, 128) dtype=float32> 複製程式碼
Fully-Connected Layer 2
新增另一個完全連線的層,輸出長度為10的向量,用於確定輸入影象屬於哪10個類。 請注意,此層中不使用ReLU。
In [27]:layer_fc2 = new_fc_layer(input=layer_fc1, num_inputs=fc_size, num_outputs=num_classes, use_relu=False) In [28]:layer_fc2 Out[28]:<tf.Tensor 'add_3:0' shape=(?, 10) dtype=float32> 複製程式碼
Predicted Class
第二個完全連線的層估計輸入影象屬於10個類中的每一個的可能性。 然而,這些估計有點粗糙且難以解釋,因為數字可能非常小或很大,因此我們希望將它們標準化,以便每個元素限制在0和1之間,10個元素總和為1。 這是使用所謂的softmax函式計算的,結果儲存在y_pred中。
y_pred = tf.nn.softmax(layer_fc2) 複製程式碼
class-number是最大元素的索引。
y_pred_cls = tf.argmax(y_pred, axis=1) 複製程式碼
Cost-function to be optimized
為了使模型更好地分類輸入影象,我們必須以某種方式改變所有網路層的變數。 為此,我們首先需要通過將模型y_pred的預測輸出與期望輸出y_true進行比較來了解模型當前的執行情況。
交叉熵是用於分類的效能度量。 交叉熵是一個始終為正的連續函式,如果模型的預測輸出與期望輸出完全匹配,則交叉熵等於零。 因此,優化的目標是最小化交叉熵,使其通過改變網路層的變數儘可能接近零。
TensorFlow具有用於計算交叉熵的內建函式。 請注意,該函式在內部計算softmax,因此我們必須直接使用layer_fc2的輸出,而不是已經應用了softmax的y_pred。
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=layer_fc2, labels=y_true) 複製程式碼
我們現在已經計算了每個影象分類的交叉熵,因此我們可以測量模型對每個影象的單獨執行情況。 但是為了使用交叉熵來指導模型變數的優化,我們需要一個標量值,因此我們只需要對所有影象分類採用交叉熵的平均值。
cost = tf.reduce_mean(cross_entropy) 複製程式碼
Optimization Method現在我們有一個必須最小化的成本度量,然後我們可以建立一個優化器。 在這種情況下,它是AdamOptimizer,它是Gradient Descent的高階形式。
請注意,此時不執行優化。 事實上,根本沒有計算任何東西,我們只需將optimizer-object新增到TensorFlow圖中以便以後執行。
optimizer = tf.train.AdamOptimizer(learning_rate=1e-4).minimize(cost) 複製程式碼
Performance Measures
correct_prediction = tf.equal(y_pred_cls, y_true_cls) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) 複製程式碼
TensorFlow Run
接下來都是訓練、測試、除錯啦~ 方式很多,這裡我就只貼程式碼了
session = tf.Session() session.run(tf.global_variables_initializer()) train_batch_size = 64 # Counter for total number of iterations performed so far. total_iterations = 0 def optimize(num_iterations): # Ensure we update the global variable rather than a local copy. global total_iterations # Start-time used for printing time-usage below. start_time = time.time() for i in range(total_iterations, total_iterations + num_iterations): # Get a batch of training examples. # x_batch now holds a batch of images and # y_true_batch are the true labels for those images. x_batch, y_true_batch, _ = data.random_batch(batch_size=train_batch_size) # Put the batch into a dict with the proper names # for placeholder variables in the TensorFlow graph. feed_dict_train = {x: x_batch, y_true: y_true_batch} # Run the optimizer using this batch of training data. # TensorFlow assigns the variables in feed_dict_train # to the placeholder variables and then runs the optimizer. session.run(optimizer, feed_dict=feed_dict_train) # Print status every 100 iterations. if i % 100 == 0: # Calculate the accuracy on the training-set. acc = session.run(accuracy, feed_dict=feed_dict_train) # Message for printing. msg = "Optimization Iteration: {0:>6}, Training Accuracy: {1:>6.1%}" # Print it. print(msg.format(i + 1, acc)) # Update the total number of iterations performed. total_iterations += num_iterations # Ending time. end_time = time.time() # Difference between start and end-times. time_dif = end_time - start_time # Print the time-usage. print("Time usage: " + str(timedelta(seconds=int(round(time_dif))))) 複製程式碼
def plot_example_errors(cls_pred, correct): # This function is called from print_test_accuracy() below. # cls_pred is an array of the predicted class-number for # all images in the test-set. # correct is a boolean array whether the predicted class # is equal to the true class for each image in the test-set. # Negate the boolean array. incorrect = (correct == False) # Get the images from the test-set that have been # incorrectly classified. images = data.x_test[incorrect] # Get the predicted classes for those images. cls_pred = cls_pred[incorrect] # Get the true classes for those images. cls_true = data.y_test_cls[incorrect] # Plot the first 9 images. plot_images(images=images[0:9], cls_true=cls_true[0:9], cls_pred=cls_pred[0:9]) 複製程式碼
def plot_confusion_matrix(cls_pred): # This is called from print_test_accuracy() below. # cls_pred is an array of the predicted class-number for # all images in the test-set. # Get the true classifications for the test-set. cls_true = data.y_test_cls # Get the confusion matrix using sklearn. cm = confusion_matrix(y_true=cls_true, y_pred=cls_pred) # Print the confusion matrix as text. print(cm) # Plot the confusion matrix as an image. plt.matshow(cm) # Make various adjustments to the plot. plt.colorbar() tick_marks = np.arange(num_classes) plt.xticks(tick_marks, range(num_classes)) plt.yticks(tick_marks, range(num_classes)) plt.xlabel('Predicted') plt.ylabel('True') # Ensure the plot is shown correctly with multiple plots # in a single Notebook cell. plt.show() 複製程式碼
# Split the test-set into smaller batches of this size. test_batch_size = 256 def print_test_accuracy(show_example_errors=False, show_confusion_matrix=False): # Number of images in the test-set. num_test = data.num_test # Allocate an array for the predicted classes which # will be calculated in batches and filled into this array. cls_pred = np.zeros(shape=num_test, dtype=np.int) # Now calculate the predicted classes for the batches. # We will just iterate through all the batches. # There might be a more clever and Pythonic way of doing this. # The starting index for the next batch is denoted i. i = 0 while i < num_test: # The ending index for the next batch is denoted j. j = min(i + test_batch_size, num_test) # Get the images from the test-set between index i and j. images = data.x_test[i:j, :] # Get the associated labels. labels = data.y_test[i:j, :] # Create a feed-dict with these images and labels. feed_dict = {x: images, y_true: labels} # Calculate the predicted class using TensorFlow. cls_pred[i:j] = session.run(y_pred_cls, feed_dict=feed_dict) # Set the start-index for the next batch to the # end-index of the current batch. i = j # Convenience variable for the true class-numbers of the test-set. cls_true = data.y_test_cls # Create a boolean array whether each image is correctly classified. correct = (cls_true == cls_pred) # Calculate the number of correctly classified images. # When summing a boolean array, False means 0 and True means 1. correct_sum = correct.sum() # Classification accuracy is the number of correctly classified # images divided by the total number of images in the test-set. acc = float(correct_sum) / num_test # Print the accuracy. msg = "Accuracy on Test-Set: {0:.1%} ({1} / {2})" print(msg.format(acc, correct_sum, num_test)) # Plot some examples of mis-classifications, if desired. if show_example_errors: print("Example errors:") plot_example_errors(cls_pred=cls_pred, correct=correct) # Plot the confusion matrix, if desired. if show_confusion_matrix: print("Confusion Matrix:") plot_confusion_matrix(cls_pred=cls_pred) 複製程式碼
print_test_accuracy() optimize(num_iterations=900) print_test_accuracy(show_example_errors=True) 複製程式碼