[譯]基于Tensorflow的交通標志識別

原文出處:https://github.com/waleedka/traffic-signs-tensorflow/blob/master/notebook1.ipynb

這是用深度學習構建交通標志識別模型例子的第一部分。目標是構建一個模型,它能夠檢測和分類交通標志。

第一步: 交通標志分類

我將以一個小目標:分類作為開始。給一個交通標志的圖片,我們的模型應該能夠告訴我們它的類型(比如是“停止”標志,“限速”標志,“讓行”標志等)。

在這個工程中,我用的python版本是3.5,Tensorflow是0.11,還用了Numpy,Scikit Image和Matplotlib庫,這些都是機器學習中的標準庫。為了方便,我創建了一個包含了許多深度學習工具庫的docker:https://hub.docker.com/r/waleedka/modern-deep-learning/ 。你可以用以下的命令來運行它:

docker run -it -p 8888:8888 -p 6006:6006 -v ~/traffic:/traffic waleedka/modern-deep-learning

需要注意的是我的工程目錄是在~/traffic下,我在我的Docker中將其映射到了/traffic目錄下,如果你用了不同的目錄,請修改它。

第一步,讓我們導入我們所需要的庫。

import os
import random
import skimage.data
import skimage.transform
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf

# 運行圖形嵌入到notebook中
%matplotlib inline

訓練數據集

我們將用比利時的交通標志數據集。進入http://btsd.ethz.ch/shareddata/ 網站,下載相關的訓練集和測試集數據。在那個網頁上有很多數據集,你只需要下載在BelgiumTS for Classification (cropped images)目錄下的兩個文件就行了:

  • BelgiumTSC_Training (171.3MBytes)
  • BelgiumTSC_Testing (76.5MBytes)

在下載完成后解壓文件,你的工程文件路徑應該看起來像下面這樣:

/traffic/datasets/BelgiumTS/Training/
/traffic/datasets/BelgiumTS/Testing/

兩個目錄中都包含了62個子目錄,目錄名字是從00000到00061的編號。這些目錄名表示的是一個標簽,而目錄下的圖片就是該標簽的樣本。

加載訓練數據

Training目錄下包含了名字從00000到00061連續編號的子目錄。這些名字代表了標簽是從0到61編號,每個目錄下的交通標志圖片就是屬于該標簽的樣本。這些圖片是用一種古老的格式.ppm來存儲的,幸運的是,Scikit Image庫支持這個格式。

def load_data(data_dir):
    """Loads a data set and returns two lists:
    
    images: a list of Numpy arrays, each representing an image.
    labels: a list of numbers that represent the images labels.
    """
    # Get all subdirectories of data_dir. Each represents a label.
    directories = [d for d in os.listdir(data_dir) 
                   if os.path.isdir(os.path.join(data_dir, d))]
    # Loop through the label directories and collect the data in
    # two lists, labels and images.
    labels = []
    images = []
    for d in directories:
        label_dir = os.path.join(data_dir, d)
        file_names = [os.path.join(label_dir, f) 
                      for f in os.listdir(label_dir) if f.endswith(".ppm")]
        # For each label, load it's images and add them to the images list.
        # And add the label number (i.e. directory name) to the labels list.
        for f in file_names:
            images.append(skimage.data.imread(f))
            labels.append(int(d))
    return images, labels


# Load training and testing datasets.
ROOT_PATH = "/traffic"
train_data_dir = os.path.join(ROOT_PATH, "datasets/BelgiumTS/Training")
test_data_dir = os.path.join(ROOT_PATH, "datasets/BelgiumTS/Testing")

images, labels = load_data(train_data_dir)

這里我們加載了兩個list列表:

  • images列表包含了一組圖像,每個圖像都轉換為numpy數組。
  • labels列表是標簽,值為0到61的整數。

我們將所有的數據集都加載進內存中通常并不是一個好主意,不過這個數據集并不大而且我們又試著保持代碼的簡潔性,這樣做也未嘗不可。我們在下一個部分再來改進它。對于更大的數據集,我們就應該考慮批量載入數據,用一個單獨的線程來加載數據塊,然后喂給訓練線程。

探索數據集

先看看我們總共有多少圖像和標簽?

print("Unique Labels: {0}\nTotal Images: {1}".format(len(set(labels)), len(images)))

顯示每組標簽的第一幅圖像。

def display_images_and_labels(images, labels):
    """Display the first image of each label."""
    unique_labels = set(labels)
    plt.figure(figsize=(15, 15))
    i = 1
    for label in unique_labels:
        # Pick the first image for each label.
        image = images[labels.index(label)]
        plt.subplot(8, 8, i)  # A grid of 8 rows x 8 columns
        plt.axis('off')
        plt.title("Label {0} ({1})".format(label, labels.count(label)))
        i += 1
        _ = plt.imshow(image)
    plt.show()

display_images_and_labels(images, labels)

這些數據集看起來還不錯啊!這些交通標志在每個圖像中占了很大部分的面積,這將讓我們的工作變得容易些:我們不需要在每幅圖像中再單獨識別到標志上,只用做好我們的物體分類就可以了。而且圖像包含了大量角度和情況,有助于我們模型的推廣。

然后還有個問題,雖然這些圖像都是正方形的,但它們并不都是一樣的大小,它們有著不同的縮放比例。我們的簡單神經網絡的輸入需要是固定大小的輸入,因此,我們需要對數據進行預處理。我們接下來將進行數據預處理,但首先,我們先挑選一個標簽,看看它里面包含的圖像。來看看標簽32:

def display_label_images(images, label):
    """Display images of a specific label."""
    limit = 24  # show a max of 24 images
    plt.figure(figsize=(15, 5))
    i = 1

    start = labels.index(label)
    end = start + labels.count(label)
    for image in images[start:end][:limit]:
        plt.subplot(3, 8, i)  # 3 rows, 8 per row
        plt.axis('off')
        i += 1
        plt.imshow(image)
    plt.show()

display_label_images(images, 32)

有意思吧,我們的數據看起來將所有的限速標志都歸為了同一個類,不管標志上面的數字是多少。在剛開始的時候充分理解數據集是必要的,它可以在我們對輸出預測的時候減少很多不必要的麻煩。

我們繼續來看看其他的標簽,看看標簽26和27,他們都是在一個紅圈里有數字,因此我們的模型必須能很好地對這3類數據進行區分才行。

處理不同大小的圖片?

許多神經網絡都希望有一個固定大小的輸入,我們的神經網絡也是如此。但是正如上面看到的一樣,我們數據集中的圖像并不都是一個大小的啊,那怎么辦呢?一個常用的做法是選一個高寬比,然后將每個圖片都拉伸到那個比例,但在這個過程中,我們必須確保我們沒有裁剪到這些交通標志的一部分。這看起來需要我們手工進行!我們用一個簡單點的解決辦法:我們調整圖像到固定的一個大小,不用管那些圖像是不是被水平或垂直拉伸了。一個人能輕而易舉的識別出被拉伸了的圖片,我們希望我們的模型也能識別。

我們的輸入數據越大的話,得到的模型也就越大,這樣訓練它的時間就會花的越久,因此我們再把圖片的尺寸變小一些。在開發的早期階段,我們希望能快速的訓練模型,而不是當我們調整代碼后每次都在迭代的時候等待很長時間。

那么,我們的圖像大小是多少呢?

for image in images[:5]:
    print("shape: {0}, min: {1}, max: {2}".format(image.shape, image.min(), image.max()))

打印信息:

shape: (141, 142, 3), min: 0, max: 255
shape: (120, 123, 3), min: 0, max: 255
shape: (105, 107, 3), min: 0, max: 255
shape: (94, 105, 3), min: 7, max: 255
shape: (128, 139, 3), min: 0, max: 255

這些尺寸大小看起來都在128x128左右。如果我們將尺寸調整為32x32,尺寸會是原來的1/16,減少了模型數據量。而且32x32對于識別這些標志來說基本上已經足夠大了,因此,我們就這樣干。

我也認為經常打印min()和max()函數的值是一個好的習慣。這樣做能讓我們很容易的發現我們數據的邊界范圍并有助于及早的發現bug。

# 調整圖像
images32 = [skimage.transform.resize(image, (32, 32)) for image in images]
display_images_and_labels(images32, labels)

可以看到,32x32的圖像雖然不是那么清晰,但仍然能夠辨認。注意上面顯示圖像的尺寸要比實際尺寸大一些,因為matplotlib庫讓它們自動適應網格的大小。我們來打印一些圖像的尺寸看看是否符合我們的要求:

for image in images32[:5]:
    print("shape: {0}, min: {1}, max: {2}".format(image.shape, image.min(), image.max()))

打印信息:

shape: (32, 32, 3), min: 0.007391237745099785, max: 1.0
shape: (32, 32, 3), min: 0.003576899509803602, max: 1.0
shape: (32, 32, 3), min: 0.0015567555147030507, max: 1.0
shape: (32, 32, 3), min: 0.0567746629901964, max: 0.9692670036764696
shape: (32, 32, 3), min: 0.026654411764708015, max: 0.98952205882353

可以看到打印出來的大小是符合我們所要求的。但是打印出來的最小值和最大值現在卻是在0到1.0之間,并不是像我們上面看到的0-255。那是因為resize函數自動為我們進行了歸一化。將數據歸一化到0.0-1.0范圍很常見,因此我們保持這樣既可。但要記住,如果之后想要把圖像轉換到0-255的正常范圍,記得乘上255這個值。

最小可行模型

labels_a = np.array(labels)
images_a = np.array(images32)
print("labels: ", labels_a.shape, "\nimages: ", images_a.shape)

打印信息:

labels: (4575,)
images: (4575, 32, 32, 3)

# Create a graph to hold the model.
graph = tf.Graph()

# Create model in the graph.
with graph.as_default():
    # Placeholders for inputs and labels.
    images_ph = tf.placeholder(tf.float32, [None, 32, 32, 3])
    labels_ph = tf.placeholder(tf.int32, [None])

    # Flatten input from: [None, height, width, channels]
    # To: [None, height * width * channels] == [None, 3072]
    images_flat = tf.contrib.layers.flatten(images_ph)

    # Fully connected layer. 
    # Generates logits of size [None, 62]
    logits = tf.contrib.layers.fully_connected(images_flat, 62, tf.nn.relu)

    # Convert logits to label indexes (int).
    # Shape [None], which is a 1D vector of length == batch_size.
    predicted_labels = tf.argmax(logits, 1)

    # Define the loss function. 
    # Cross-entropy is a good choice for classification.
    loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits, labels_ph))

    # Create training op.
    train = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)

    # And, finally, an initialization op to execute before training.
    # TODO: rename to tf.global_variables_initializer() on TF 0.12.
    init = tf.initialize_all_variables()

print("images_flat: ", images_flat)
print("logits: ", logits)
print("loss: ", loss)
print("predicted_labels: ", predicted_labels)

打印信息:

images_flat: Tensor("Flatten/Reshape:0", shape=(?, 3072), dtype=float32)
logits: Tensor("fully_connected/Relu:0", shape=(?, 62), dtype=float32)
loss: Tensor("Mean:0", shape=(), dtype=float32)
predicted_labels: Tensor("ArgMax:0", shape=(?,), dtype=int64)

開始訓練

# Create a session to run the graph we created.
session = tf.Session(graph=graph)

# First step is always to initialize all variables. 
# We don't care about the return value, though. It's None.
_ = session.run([init])


for i in range(201):
    _, loss_value = session.run([train, loss], 
                                feed_dict={images_ph: images_a, labels_ph: labels_a})
    if i % 10 == 0:
        print("Loss: ", loss_value)

打印信息:

Loss: 4.2588
Loss: 2.88972
Loss: 2.42234
Loss: 2.20074
Loss: 2.06985
Loss: 1.98126
Loss: 1.91674
...

使用模型

session對象包含了我們模型中所有變量的值(即權重)。

# Pick 10 random images
sample_indexes = random.sample(range(len(images32)), 10)
sample_images = [images32[i] for i in sample_indexes]
sample_labels = [labels[i] for i in sample_indexes]

# Run the "predicted_labels" op.
predicted = session.run([predicted_labels], 
                        feed_dict={images_ph: sample_images})[0]
print(sample_labels)
print(predicted)

[7, 7, 19, 32, 39, 16, 18, 3, 38, 41]
[56 61 19 32 39 61 18 40 38 40]

# Display the predictions and the ground truth visually.
fig = plt.figure(figsize=(10, 10))
for i in range(len(sample_images)):
    truth = sample_labels[i]
    prediction = predicted[i]
    plt.subplot(5, 2,1+i)
    plt.axis('off')
    color='green' if truth == prediction else 'red'
    plt.text(40, 10, "Truth:        {0}\nPrediction: {1}".format(truth, prediction), 
             fontsize=12, color=color)
    plt.imshow(sample_images[i])

評估

可視化的結果很有趣,但我們需要一個更加精確的方法來衡量我們模型的準確性。另外,重要的是要用那些它還沒有見過的圖片來進行測試。BelgiumTS提供的驗證數據集Testing就是用來干這個事的。

# Load the test dataset.
test_images, test_labels = load_data(test_data_dir)

# Transform the images, just like we did with the training set.
test_images32 = [skimage.transform.resize(image, (32, 32))
                 for image in test_images]
display_images_and_labels(test_images32, test_labels)
# Run predictions against the full test set.
predicted = session.run([predicted_labels], 
                        feed_dict={images_ph: test_images32})[0]
# Calculate how many matches we got.
match_count = sum([int(y == y_) for y, y_ in zip(test_labels, predicted)])
accuracy = match_count / len(test_labels)
print("Accuracy: {:.3f}".format(accuracy))

打印信息(準確率):

Accuracy: 0.634

最后關閉Session:

# Close the session. This will destroy the trained model.
session.close()

PS:以上只是自己為了看而翻譯的簡譯版,譯完后發現網上有一篇翻譯了,o(╯□╰)o。不過我是直接翻譯的jupyter notebook上的,網上的文章中講的要詳細些:
一次神經網絡的探索之旅-基于Tensorflow的路標識別

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容