[译]基于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阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容