前言
苹果在iOS 11中发布了CoreML框架。其实很多人对于CoreML有个误区,那就是他们认为CoreML“无所不能”,既可以训练模型又可以应用模型。其实CoreML只是将训练好的模型放在应用层面上的一个工具。
既然它不能训练模型,那总得有能训练模型的工具吧。那就是TensorFlow该干的活了。其实大体思路就是这样的:TensorFlow构建并训练模型,然后训练好的模型经过一层转换(因为CoreML不能直接支持TensorFlow)交给CoreML来应用。
先上几个效果图:
构建并训练TensorFlow模型
其实小弟我也是刚刚折腾TensorFlow没几天,什么事情都是摸索着做,大牛勿喷。现在手里还没有适合的资源。没办法,就用机器学习界的“Hello World”----MNIST手写数据集来当练习吧。
首先第一步是先构建用于训练MNIST手写数据集的模型,所谓的模型,其实就是SoftMax回归模型。何为SoftMax?
Softmax回归介绍
我们知道MNIST的每一张图片都表示一个数字,从0到9。我们希望得到给定图片代表每个数字的概率。比如说,我们的模型可能推测一张包含9的图片代表数字9的概率是80%但是判断它是8的概率是5%(因为8和9都有上半部分的小圆),然后给予它代表其他数字的概率更小的值。
这是一个使用softmax回归模型的经典案例。softmax模型可以用来给不同的对象分配概率。即使在之后,我们训练更加精细的模型时,最后一步也需要用softmax来分配概率。
softmax回归分两步:第一步
为了得到一张给定图片属于某个特定数字类的证据(evidence),我们对图片像素值进行加权求和。如果这个像素具有很强的证据说明这张图片不属于该类,那么相应的权值为负数,相反如果这个像素拥有有利的证据支持这张图片属于这个类,那么权值是正数。
下面的图片显示了一个模型学习到的图片上每个像素对于特定数字类的权值。红色代表负数权值,蓝色代表正数权值。
我们也需要加入一个额外的偏置量(bias),因为输入往往会带有一些无关的干扰量。因此对于给定的输入图片 x 它代表的是数字 i 的证据可以表示为
其中
代表Wi权重,bi代表数字 i 类的偏置量,j 代表给定图片 x 的像素索引用于像素求和。然后用softmax函数可以把这些证据转换成概率 y:
这里的softmax可以看成是一个激励(activation)函数或者链接(link)函数,把我们定义的线性函数的输出转换成我们想要的格式,也就是关于10个数字类的概率分布。因此,给定一张图片,它对于每一个数字的吻合度可以被softmax函数转换成为一个概率值。softmax函数可以定义为:
展开等式上边的子式,可以得到:
但是更多的时候把softmax模型函数定义为前一种形式:把输入值当成幂指数求值,再正则化这些结果值。这个幂运算表示,更大的证据对应更大的假设模型(hypothesis)里面的乘数权重值。反之,拥有更少的证据意味着在假设模型里面拥有更小的乘数系数。假设模型里的权值不可以是0值或者负值。Softmax然后会正则化这些权重值,使它们的总和等于1,以此构造一个有效的概率分布。对于softmax回归模型可以用下面的图解释,对于输入的xs加权求和,再分别加上一个偏置量,最后再输入到softmax函数中:
如果把它写成一个等式,我们可以得到:
我们也可以用向量表示这个计算过程:用矩阵乘法和向量相加。这有助于提高计算效率。
有了数学理论,就要开始实践了。
构建模型
首先我们的模型肯定得读取数据对不对?而MNIST数据集是有图像和图像对应的标签所组成的,所以我们不仅得读取图像,还得读取图像标签做个对应,代码如下:
def read32(bytestream):
# 由于网络数据的编码是大端,所以需要加上>
dt = numpy.dtype(numpy.int32).newbyteorder('>')
data = bytestream.read(4)
return numpy.frombuffer(data, dt)[0]
def read_labels(filename):
with gzip.open(filename) as bytestream:
magic = read32(bytestream) #读取标签数量60000
numberOfLabels = read32(bytestream) #取出60000个标签组成一个数组
labels = numpy.frombuffer(bytestream.read(numberOfLabels), numpy.uint8)
#声明一个60000*10的二维数组
data = numpy.zeros((numberOfLabels, 10))
for i in range(len(labels)):
data[i][labels[i]] = 1
bytestream.close()
return data
def read_images(filename):
# 把文件解压成字节流
with gzip.open(filename) as bytestream:
magic = read32(bytestream)
numberOfImages = read32(bytestream)
rows = read32(bytestream)
columns = read32(bytestream)
images = numpy.frombuffer(bytestream.read(numberOfImages * rows * columns), numpy.uint8)
images.shape = (numberOfImages, rows * columns)
images = images.astype(numpy.float32)
images = numpy.multiply(images, 1.0 / 255.0)
bytestream.close()
return images
read_images为读取图像的函数,read_labels为读取图像标签的函数。gzip用import gzip就可以导入。代码不是很难,就不班门弄斧了。
既然定义了读取标签和读取图像的函数,那么就得利用它对不对?代码如下:
train_labels = read_labels(train_labels_file)
train_images = read_images(train_images_file)
test_labels = read_labels(t10k_labels_file)
test_images = read_images(t10k_images_file)
其中这四个读取的分别是测试集上的图像和标签、验证集上的图像和标签。
为什么要使用测试集,还要在使用验证集呢?
机器就好比一个学生,平常复习的再好,是骡子是马得出来溜溜不是?模型也一样,在测试集上正确率再高,也不一定代表这个模型就好使(比如过拟合)。所以,验证集才是验证这个模型正确率的关键。一般将训练集和测试集划为7:3。
上边的代码中四个传参分别为:
train_images_file = "MNIST_data/train-images-idx3-ubyte.gz"
train_labels_file = "MNIST_data/train-labels-idx1-ubyte.gz"
t10k_images_file = "MNIST_data/t10k-images-idx3-ubyte.gz"
t10k_labels_file = "MNIST_data/t10k-labels-idx1-ubyte.gz"
这些为我本地放置MNIST数据集的地方,这些.gz文件是从http://yann.lecun.com/exdb/mnist/下载的。
既然读取了数据,那么下一步就是要构建softmax回归模型了,代码如下:
import tensorflow as tf
x = tf.placeholder("float", [None, 784.],name='input/x_input')
W = tf.Variable(tf.zeros([784., 10.]))
b = tf.Variable(tf.zeros([10.]))
y = tf.nn.softmax(tf.matmul(x, W) + b)
y_ = tf.placeholder("float",name='input/y_input')
cross_entropy = -tf.reduce_sum(y_ * tf.log(y))
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
init = tf.initialize_all_variables()
sess = tf.Session()sess.run(init)
for i in range(1200):
batch_xs = train_images[50 * i:50 * i + 50]
batch_ys = train_labels[50 * i:50 * i + 50]
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
correct_prediction = tf.equal(tf.argmax(y, 1, output_type='int32', name='output'), tf.argmax(y_, 1, output_type='int32'))
# correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
print (sess.run(accuracy, feed_dict={x: test_images, y_: test_labels}))
解释一下,首先我们定义x,w,b变量,其中x应该为55000*784的向量,b这个变量前文也说了,是对应10个数字的偏移项,所以自然为55000*10,w因为是要连接x和b的桥梁,所以自然是784*10。然后因为是softmax回归,所以直接用TensorFlow自带的tf.nn.softmax就好,matmul函数代表两个矩阵相乘,也就是w*x。任何一个模型都要定义一个损失函数,则利用的是交叉熵损失函数,cross_entropy = -tf.reduce_sum(y_ * tf.log(y))。我们的目标是为了正确率更高,那就是利用梯度下降要让交叉熵损失更小,用TensorFlow自带的优化器就可以,train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)。下面要做的就是初始化变量(init = tf.initialize_all_variables() sess = tf.Session()sess.run(init))并且迭代训练了(for i in range(1200):)。
训练代码为sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys}),其中batch_xs为测试集中的图像,batch_ys为测试集中的标签。
训练的时候就得检测正确率:
correct_prediction = tf.equal(tf.argmax(y, 1, output_type='int32', name='output'), tf.argmax(y_, 1, output_type='int32'))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
前文说过,模型得在验证集上运行来验证模型,所以下面代码就是在验证集上运行的
sess.run(accuracy, feed_dict={x: test_images, y_: test_labels})
导出TensorFlow模型
现在已经训练好了模型并通过了验证,下面就要把这个模型导出出来。在这里我们导出的是.pb格式的文件。代码如下:
output_graph_def = graph_util.convert_variables_to_constants(sess, sess.graph_def, output_node_names=['output'])
with tf.gfile.FastGFile('model/mnist.pb', mode='wb') as f:
# ’wb’中w代表写文件,b代表将数据以二进制方式写入文件。 f.write(output_graph_def.SerializeToString())
sess.close()
这样我们就把模型保存了下来。下一步,就是转换成CoreML支持的格式了。
转换成.mlmodel文件
需要安装一个CoreMLTools的工具,安装方式其实很简单:
pip install -U coremltools即可安装。
然后新建一个tf2ml.py文件,写入一下代码:
import tfcoreml as tf_converter
tf_converter.convert(tf_model_path='model/mnist.pb', mlmodel_path='model/my_mnist.mlmodel', output_feature_names=['Softmax:0'],input_name_shape_dict={"input/x_input:0":[1,784]})
以上代码中,tf_model_path是保存的.pb文件的路径,mlmodel_path是转换后的.mlmodel文件所在路径,output_feature_names和input_name_shape_dict都是CoreML中所需要的,下文会讲到。
然后运行这个python文件,就可以生成.mlmodel文件了。
在CoreML中使用.mlmodel文件
将.mlmodel导入工程中,然后重新编译一下,Xcode会自动生成一个模型类。查看一下,会发现input_x_input__0,Softmax__0是不是很眼熟?没错,这个就是coremltool转换时我们定义的名字。
然后就是iOS敲代码时间了,CoreML的应用代码我就不详述了,最后运行的样子就是最开头展示的那个样子。
以后我会尝试着把InceptionV3转换到CoreML中。