原文出处: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的路标识别
网友评论