MXNet 是一个轻量级、可移植、灵活的分布式深度学习框架,2017 年 1 月 23 日,该项目进入 Apache 基金会,成为 Apache 的孵化器项目。
尽管现在已经有很多深度学习框架,包括 TensorFlow、Keras、Torch、以及 Caffe,但 Apache MXNet 因其对多 GPU 的分布式支持而越来越受欢迎。
在这篇文章中,我们将介绍如何使用 MXNet 来解决一个经典计算机视觉问题:使用卷积神经网络对交通标志进行分类。网络输入为一张包含交通标志的彩色照片,输出为该标志的类别。
环境准备
- 安装 Anaconda。Anaconda 是一个用于科学计算的 Python 发行版,提供了包管理与环境管理的功能。Anaconda 利用 conda 来进行 package 和 environment 的管理,并且已经包含了 Python 和相关的配套工具。
- 在 conda 下安装 pip,安装命令为“conda install pip”。
- 安装 OpenCV-python 库。OpenCV-python 是一个很强大的计算机视觉库,在这个项目中可以用于处理图像。使用“pip install openvc-python”在 Anaconda 环境下安装 OpenCV。也可以从源文件进行编译(注意:conda 安装 opencv3.0 不能运行)。
- 安装 scikit learn,一个开源的 python 机器学习科学计算库,它将用于对数据进行预处理。安装命令为“conda install scikit-learn”。
- 安装 Jupyter Notebook,安装命令为“conda install jupyter notebook”。
- 安装 MXNet,安装命令为“pip install mxnet”。
数据库
作者使用的数据库是德国交通标志识别基准,来自论文《德国交通标志识别基准:多类别分类竞赛》(J. Stallkamp, M. Schlipsing, J. Salmen, and C. Igel. “The German Traffic Sign Recognition Benchmark: A multi-class classification competition.”),发表在 IEEE International Joint Conference on Neural Networks,2011。该数据集包含 39209 张训练样例和 12630 张测试样例,有 43 种不同的交通标志——停车标志、限速标志、各种警示标志以及其他标志。
数据库中的每张图像大小为 32*32 像素,均为三通道彩色图。每幅图属于一种交通标志。图像种类标签使用 0 到 42 的整数表示。
下载数据集的代码如下:
import pickle # 存储训练数据和测试数据的位置 training_file = "traffic-data/train.p" validation_file = "traffic-data/valid.p" with open(training_file, mode='rb') as f: train = pickle.load(f) with open(validation_file, mode='rb') as f: valid = pickle.load(f) X_train, y_train = train['features'], train['labels'] X_valid, y_valid = valid['features'], valid['labels']
作者从一个 NumPy 阵列中下载数据,数据分为训练、验证和测试集。训练集包含 39209 张大小为 32*32 像素,通道数为 3 的图像,所以 NumPy 阵列的维度为 39209*32*32*3。该项目中作者仅使用了训练集和验证集。作者将使用网上的真实图像来测试所构建的模型。X_train 存储图像,维度为 39209*32*32*3。Y_train 存储图像对应的类标,维度为 39209,包含 0-42 的整数,对应每张图的类标。
下一步,导入包含图片类标到自然语言名称的映射关系的文件“signnames.csv”:
# 类别的实际名称在另一个文件中给出。这里我们载入 csv 文件,包含从类别到标签的映射。 import csv def read_csv_and_parse(): traffic_labels_dict ={} with open('signnames.csv') as f: reader = csv.reader(f) count = -1; for row in reader: count = count + 1 if(count == 0): continue label_index = int(row[0]) traffic_labels_dict[label_index] = row[1] return traffic_labels_dict traffic_labels_dict = read_csv_and_parse() print(traffic_labels_dict)
运行结果如下:
{0: 'Speed limit (20km/h)', 1: 'Speed limit (30km/h)', 2: 'Speed limit (50km/h)', 3: 'Speed limit (60km/h)', 4: 'Speed limit (70km/h)', 5: 'Speed limit (80km/h)', 6: 'End of speed limit (80km/h)', 7: 'Speed limit (100km/h)', 8: 'Speed limit (120km/h)', 9: 'No passing', 10: 'No passing for vehicles over 3.5 metric tons', 11: 'Right-of-way at the next intersection', 12: 'Priority road', 13: 'Yield', 14: 'Stop', 15: 'No vehicles', 16: 'Vehicles over 3.5 metric tons prohibited', 17: 'No entry', 18: 'General caution', 19: 'Dangerous curve to the left', 20: 'Dangerous curve to the right', 21: 'Double curve', 22: 'Bumpy road', 23: 'Slippery road', 24: 'Road narrows on the right', 25: 'Road work', 26: 'Traffic signals', 27: 'Pedestrians', 28: 'Children crossing', 29: 'Bicycles crossing', 30: 'Beware of ice/snow', 31: 'Wild animals crossing', 32: 'End of all speed and passing limits', 33: 'Turn right ahead', 34: 'Turn left ahead', 35: 'Ahead only', 36: 'Go straight or right', 37: 'Go straight or left', 38: 'Keep right', 39: 'Keep left', 40: 'Roundabout mandatory', 41: 'End of no passing', 42: 'End of no passing by vehicles over 3.5 metric tons'}
可以看出 43 个数字对应 43 个图片种类。例如,0 号图片代表 20km/h 的限速标志。
可视化
下面的代码可以帮助我们可视化图像以及类标(标志种类):
# 数据可视化可以让我们对数据有一个更好更直观的理解 import matplotlib.pyplot as plt from matplotlib.figure import Figure %matplotlib inline #该函数为每种交通标志选择一张图并画出该图 def get_images_to_plot(images, labels): selected_image = [] idx = [] for i in range(n_classes): selected = np.where(labels == i)[0][0] selected_image.append(images[selected]) idx.append(selected) return selected_image,idx # 在栅格中画图 def plot_images(selected_image,y_val,row=5,col=10,idx = None): count =0; f, axarr = plt.subplots(row, col,figsize=(50, 50)) for i in range(row): for j in range(col): if(count < len(selected_image)): axarr[i,j].imshow(selected_image[count]) if(idx != None): axarr[i,j].set_title(traffic_labels_dict[y_val[idx[count]]], fontsize=20) axarr[i,j].axis('off') count = count + 1 selected_image,idx = get_images_to_plot(X_train,y_train) plot_images(selected_image,row=10,col=4,idx=idx,y_val=y_train)
下图是可视化的交通图像以及标志:
(点击放大图像)
训练过程
1. 准备数据集
X_train 和 Y_train 组成了训练数据集。可以使用 scikit-learn 对训练数据集进行分割得到验证集,这样可以避免使用出现过的图片测试模型。代码如下:
from sklearn.model_selection import train_test_split X_train_set,X_validation_set,Y_train_set,Y_validation_set = train_test_split ( X_train, Y_train, test_size=0.02, random_state=42)
2. 训练数据预处理
批训练
神经网络训练需要花费大量时间和内存。所以作者将数据分批训练,一批大小为 64。不仅是为了让数据适应内存,而且它可以让 MXNet 尽量利用 GPU 的计算效率。
归一化
除此之外,图像的像素值也进行了归一化,可以使学习算法更快收敛。
下面是对训练数据进行预处理的代码:
batch_size = 64 X_train_set_as_float = X_train_reshape.astype('float32') X_train_set_norm = X_train_set_as_float[:] / 255.0; X_validation_set_as_float = X_valid_reshape.astype('float32') X_validation_set_norm = X_validation_set_as_float[:] / 255.0 ; train_iter =mx.io.NDArrayIter(X_train_set_as_float, y_train_extra, batch_size, shuffle=True) val_iter = mx.io.NDArrayIter(X_validation_set_as_float, y_valid, batch_size,shuffle=True) print("train set : ", X_train_set_norm.shape) print("validation set : ", X_validation_set_norm.shape) print("y train set : ", y_train.shape) print("y validation set :", y_valid.shape)
3. 构建深度网络
目前,对于图像识别这类处在探索研究热点的问题,学界已经设计了很多效果良好的网络结构。所以最好的方法是实现一个已经发表出来的网络结构,然后对其进行改进。作者基于 AlexNet 的结构,构建了一个简化版的卷积神经网络。
AlexNet 是 2012 年发表的一个经典网络,在当年取得了 ImageNet 的最好成绩。
这是 AlexNet 的网络结构图:
(点击放大图像)
网络共有 8 层,其中前 5 层是卷积层,后边 3 层是全连接层,在每一个卷积层中包含了激励函数 RELU 以及局部响应归一化(LRN)处理,然后再经过池化(max pooling),最后的一个全连接层的输出是具有 1000 个输出的 softmax 层,最后的优化目标是最大化平均的多元逻辑回归。
在此之后也有很多更优秀的网络结构被提出,例如 VGGNet 和 ResNet,大家可以选择更好的网络结构去实现。
由于 MXNet 的符号计算构架,该神经网络的代码十分简洁明了:
首先导入数据
data = mx.symbol.Variable('data')
conv 卷积层对图像进行卷积操作,relu 层与 conv 层相连,对输入进行非线性激活,最大池化层(pool)对前一层的输出进行池化操作,也就是随机 drop 掉一些像素,然后减少图像尺寸。这里作者一共设计了三个卷积层。
conv1 = mx.sym.Convolution(data=data, pad=(1,1), kernel=(3,3), num_filter=24, name="conv1") relu1 = mx.sym.Activation(data=conv1, act_type="relu", name= "relu1") pool1 = mx.sym.Pooling(data=relu1, pool_type="max", kernel=(2,2), stride=(2,2),name="max_pool1") # 第二卷积层 conv2 = mx.sym.Convolution(data=pool1, kernel=(3,3), num_filter=48, name="conv2", pad=(1,1)) relu2 = mx.sym.Activation(data=conv2, act_type="relu", name="relu2") pool2 = mx.sym.Pooling(data=relu2, pool_type="max", kernel=(2,2), stride=(2,2),name="max_pool2") #第三卷积层 conv3 = mx.sym.Convolution(data=pool2, kernel=(5,5), num_filter=64, name="conv3") relu3 = mx.sym.Activation(data=conv3, act_type="relu", name="relu3") pool3 = mx.sym.Pooling(data=relu3, pool_type="max", kernel=(2,2), stride=(2,2),name="max_pool3")
在网络的最后是全连接层,全连接层的每一个神经元都和之前的神经元连接在一起。该层后面是有 43 个神经元的全连接层。每一个神经元代表一个图片种类。但是由于神经元的输出是实值,但是分类要求输出为类标,所以作者使用另一个激活函数,使 43 个神经元中的某个值为 1,其他的为 0。最后的一个全连接层的输出是具有 43 个输出的 softmax 层,最后的优化目标是最大化平均的多元逻辑回归。
# 第一层全连接层 flatten = mx.sym.Flatten(data=pool3) fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=500, name="fc1") relu3 = mx.sym.Activation(data=fc1, act_type="relu" , name="relu3") # 第二层全连接层 fc2 = mx.sym.FullyConnected(data=relu3, num_hidden=43,name="final_fc") # softmax 层 mynet = mx.sym.SoftmaxOutput(data=fc2, name='softmax')
4. 训练网络
训练 epoch 为 10,训练好的模型存在 JSON 文件中,并且可以通过测量训练和验证准确率来观测网络“学习”的情况,代码如下:
#创建 adam optimiser 优化器 adam = mx.optimizer.create('adam') #在 check point 存储模型。 model_prefix = 'models/chkpt' checkpoint = mx.callback.do_checkpoint(model_prefix) #加载模型的 API。 model = mx.mod.Module( context = mx.gpu(0), # 如果没有 GPU,改为 mx.gpu()。 symbol = mynet, data_names=['data'] ) #训练模型 10 个 epoch,大约花费 5 分钟。 model.fit( train_iter, eval_data=val_iter, batch_end_callback = mx.callback.Speedometer(batch_size, 64), num_epoch = 10, eval_metric='acc', # evaluation metric is accuracy. optimizer = adam, epoch_end_callback=checkpoint )
5. 载入预训练模型
下面给出了加载第 10 个 epoch 模型(最终模型)的代码。由于将在单张图片上进行测试,所以批尺寸由 64 减到 1,数据维度也变成了 1*3*32*32。
#加载 checkpoint 的模型,这里我们加载的是第 10 个 epoch。 sym, arg_params, aux_params = mx.model.load_checkpoint(model_prefix, 10) # 将加载的参数分配给模型 mod = mx.mod.Module(symbol=sym, context=mx.cpu()) mod.bind(for_training=False, data_shapes=[('data', (1,3,32,32))]) mod.set_params(arg_params, aux_params)
测试过程
测试图像(32*32*3)样例:
(点击放大图像)
测试部分代码:
#预测任意交通图标 from collections import namedtuple Batch = namedtuple('Batch', ['data']) #载入图像,将尺寸压缩到 32*32,然后将图像维度转换成 1*3*32*32 def get_image(url, show=False): # 加载并显示图像 img =cv2.imread(url) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if img is None: return None if show: plt.imshow(img) plt.axis('off') # 转换维度 (批尺寸, RGB 通道数, 宽度, 高度) img = cv2.resize(img, (32, 32)) img = np.swapaxes(img, 0, 2) img = np.swapaxes(img, 1, 2) #交换图像维度,使其成为 3*32*32 大小 img = img[np.newaxis, :] # 添加新的图像维度,使其成为 1*3*32*32 return img def predict(url): img = get_image(url, show=True) # compute the predict probabilities mod.forward(Batch([mx.nd.array(img)])) prob = mod.get_outputs()[0].asnumpy() # print the top-5 prob = np.squeeze(prob) prob = np.argsort(prob)[::-1] for i in prob[0:5]: print('class=%s' %(traffic_labels_dict[i])) predict('traffic-data/Stop.jpg',)
测试结果(前五个预测结果):
class=Stop class=Speed limit (30km/h) class=Speed limit (20km/h) class=Speed limit (70km/h) class=Bicycles crossing
从结果可以看出可能性最高的种类为停车标志,说明预测准确。如果需要对模型有一个更完整的衡量,还需要用测试数据库进行测试,得到最终的分类准确率。
总结
本文我们介绍了使用 MXNet 进行多目标分类任务的方法。作者使用 MXNet,在 AlexNet 的结构基础上构建了一个更为简单的卷积神经网络结构。网络由卷积层,激活函数层,池化层和全连接层组成,采用德国交通标志图像训练数据库对该网络进行训练,实验结果证明网络可以将交通标志进行正确的分类。作者介绍了如何使用 MXNet 对数据进行预处理,构建网络,以及如何加载预训练好的网络模型。可以看出,MXNet 因其在多 GPU 上进行并行训练的能力,以及网络模型构建简单灵活的特性,是一个十分优秀的深度学习框架。
查看英文原文: Classifying traffic signs with MXNet: An introduction to customizing a neural network
感谢薛命灯对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论