图像分类涉及到决定哪些输入的图像所属的类别,例如识别照片作为一个包含"apples"或"oranges"或"香蕉。" 图像分类的两种最常见方法是使用标准的深度神经网络 (DNN),或使用卷积神经网络 (CNN)。在本文中我将介绍 DNN 方法中,使用 CNTK 库。
请参阅图 1,了解本文所要努力的方向。此演示程序创建图像分类模型修改美国国家标准和技术 (MNIST) 数据集的子集。演示训练数据集包含手写数字 1,000 张图像。每个图像是 28 高 28 个像素宽 (784 像素为单位),表示数字 0 到 9。
图 1 DNN 使用 CNTK 的图像分类
演示程序使用 784 输入的节点 (一个用于每个像素)、 两个隐藏的处理层 (各有 400 节点) 和 10 个输出节点 (一个用于每个可能的数字) 创建一个标准的神经网络。使用 10,000 迭代训练模型。逐渐降低丢失 (也称为定型误差) 和预测精度逐渐增加,,指示培训工作。
完成定型后,该演示将适用于 100 个项的测试数据集训练的模型。模型的准确性是 84.00%,因此 84 100 测试图像已正确分类。
本文假定您具有中级或更高的编程技能 C 系列语言,但并不假定您精通 CNTK 或神经网络。此演示程序使用 Python 进行编码。不过,即使不了解 Python,也应该能够跟着我一起操作,并且不会遇到太多麻烦。本文展示了演示程序的所有代码。两个数据文件使用是本文随附的下载中提供。
了解数据
完整的 MNIST 数据集包含 60,000 测试培训到 10,000 个映像的映像。某种程度上非常训练集包含在两个文件,一个用于保存所有像素值,一个都保留相关联的标签值 (0 到 9)。测试映像还包含在两个文件中。
此外,四个源代码文件存储在专用的二进制格式。在使用深层神经网络,到可用表单中获取数据几乎始终是耗时且很难。图 2显示的第一个训练图像内容。关键在于,每个映像 784 像素为单位,每个像素都是 00 小时 (0 十进制) 和 FFh (255 十进制) 之间的值。
图 2 MNIST 图像
之前编写演示程序,我编写了一个实用工具程序读取二进制源代码文件和写入文本文件 CNTK 读取器对象可以轻松地使用其内容的子集。文件 mnist_train_1000_cntk.txt 如下所示:
代码语言:javascript复制|digit 0 0 0 0 0 1 0 0 0 0 |pixels 0 .. 170 52 .. 0
|digit 0 1 0 0 0 0 0 0 0 0 |pixels 0 .. 254 66 .. 0
etc.
将原始的 MNIST 二进制数据传递到 CNTK 格式并不容易。我的实用程序的源代码可从:bit.ly/2ErcCbw。
有 1,000 行数据,每个代表一个映像。标记"| 数字"和"| 像素"指示开始的预测值和预测因子值。数字标签为独热编码其中的 1 位的位置表示数字。因此,在上述代码中前, 两个映像表示"5"和"1"。数据的每一行都具有 784 像素值,其中每个是介于 0 和 255 之间。文件 mnist_test_100_cntk.txt 具有 100 个映像,并使用相同的 CNTK 友好格式。
在大多数神经网络问题中,你想要规范化预测因子值。而不是直接规范化数据文件中的像素值,演示程序规范化数据进行联机,因为稍后您将看到。
演示程序
图 3 展示了完整的演示程序(为节省空间,进行了少量小幅改动)。我删除了所有常规错误检查。我缩进了两个空格字符,而不是常规的四以节省空间。请注意""字符用于行继续符的 Python。
图 3 完整的演示程序列表
代码语言:javascript复制# mnist_dnn.py
# MNIST using a 2-hidden layer DNN (not a CNN)
# Anaconda 4.1.1 (Python 3.5.2), CNTK 2.4
import numpy as np
import cntk as C
def create_reader(path, input_dim, output_dim, rnd_order, m_swps):
x_strm = C.io.StreamDef(field='pixels', shape=input_dim,
is_sparse=False)
y_strm = C.io.StreamDef(field='digit', shape=output_dim,
is_sparse=False)
streams = C.io.StreamDefs(x_src=x_strm, y_src=y_strm)
deserial = C.io.CTFDeserializer(path, streams)
mb_src = C.io.MinibatchSource(deserial, randomize=rnd_order,
max_sweeps=m_swps)
return mb_src
# ===================================================================
def main():
print("nBegin MNIST classification using a DNN n")
train_file = ".\Data\mnist_train_1000_cntk.txt"
test_file = ".\Data\mnist_test_100_cntk.txt"
C.cntk_py.set_fixed_random_seed(1)
input_dim = 784 # 28 x 28 pixels
hidden_dim = 400
output_dim = 10 # 0 to 9
X = C.ops.input_variable(input_dim, dtype=np.float32)
Y = C.ops.input_variable(output_dim) # float32 is default
print("Creating a 784-(400-400)-10 ReLU classifier")
with C.layers.default_options(init=
C.initializer.uniform(scale=0.01)):
h_layer1 = C.layers.Dense(hidden_dim, activation=C.ops.relu,
name='hidLayer1')(X/255)
h_layer2 = C.layers.Dense(hidden_dim, activation=C.ops.relu,
name='hidLayer2')(h_layer1)
o_layer = C.layers.Dense(output_dim, activation=None,
name='outLayer')(h_layer2)
dnn = o_layer # train this
model = C.ops.softmax(dnn) # use for prediction
tr_loss = C.cross_entropy_with_softmax(dnn, Y)
tr_eror = C.classification_error(dnn, Y)
max_iter = 10000 # num batches, not epochs
batch_size = 50
learn_rate = 0.01
learner = C.sgd(dnn.parameters, learn_rate)
trainer = C.Trainer(dnn, (tr_loss, tr_eror), [learner])
# 3. create reader for train data
rdr = create_reader(train_file, input_dim, output_dim,
rnd_order=True, m_swps=C.io.INFINITELY_REPEAT)
mnist_input_map = {
X : rdr.streams.x_src,
Y : rdr.streams.y_src
}
# 4. train
print("nStarting training n")
for i in range(0, max_iter):
curr_batch = rdr.next_minibatch(batch_size,
input_map=mnist_input_map)
trainer.train_minibatch(curr_batch)
if i % int(max_iter/10) == 0:
mcee = trainer.previous_minibatch_loss_average
macc = (1.0 - trainer.previous_minibatch_evaluation_average)
* 100
print("batch M: mean loss = %0.4f, accuracy = %0.2f%% "
% (i, mcee, macc))
print("nTraining complete n")
# 5. evaluate model on test data
rdr = create_reader(test_file, input_dim, output_dim,
rnd_order=False, m_swps=1)
mnist_input_map = {
X : rdr.streams.x_src,
Y : rdr.streams.y_src
}
num_test = 100
test_mb = rdr.next_minibatch(num_test, input_map=mnist_input_map)
test_acc = (1.0 - trainer.test_minibatch(test_mb)) * 100
print("Model accuracy on the %d test items = %0.2f%%"
% (num_test,test_acc))
print("nEnd MNIST classification using a DNN n")
if __name__ == "__main__":
main()
Mnist_dnn.py 演示了一个帮助程序函数 create_reader。所有控制逻辑都都在一个主函数中。由于 CNTK 正年轻和持续开发,它最好添加注释,详述使用哪一版本 (在此情况下 2.4)。
安装 CNTK 可能会需要一些技巧,如果您是初次接触 Python 世界。首先安装 Anaconda Python 分发版,其中包含所需的 Python 解释器,例如 NumPy 和 SciPy,必要的包和 pip 等实用工具。我使用的是包括 Python 3.5 的 Anaconda3 4.1.1 64 位。安装 Anaconda 后,将 CNTK 安装为 Python 包,而不是独立系统,使用 pip 实用工具。我通过普通命令行界面运行的命令如下:
代码语言:javascript复制>pip install https://cntk.ai/PythonWheel/CPU-Only/cntk-2.4-cp35-cp35m-win_amd64.whl
请注意滚轮文件,该值指示该文件中的"cp35"适用于 Python 3.5。请注意;我见过的几乎所有 CNTK 安装失败都是由于 ANACONDA-CNTK 版本不兼容。
读取器函数的签名是 create_reader (路径、 input_dim、 output_dim、 rnd_order、 m_swps)。Path 参数指向的 CNTK 格式的训练或测试文件。Rnd_order 参数是布尔标志,它将设置为 True,以训练数据,因为你想要处理按随机顺序以防止而无需进行培训进度剧烈波动的定型数据。该参数将设置为 False 时读取测试数据评估模型准确性,因为顺序并不重要然后。M_swps 参数 ("最大扫描") 将设置为常量 INFINITELY_REPEAT 定型数据 (以便它可以进行重复处理) 并将设置为 1。对于测试数据评估。
创建模型
演示准备的深度神经网络:
代码语言:javascript复制train_file = ".\Data\mnist_train_1000_cntk.txt"
test_file = ".\Data\mnist_test_100_cntk.txt"
C.cntk_py.set_fixed_random_seed(1)
input_dim = 784
hidden_dim = 400
output_dim = 10
X = C.ops.input_variable(input_dim, dtype=np.float32)
Y = C.ops.input_variable(output_dim) # 32 is default
它通常是一个显式设置 CNTK 全局随机数种子,因此您的结果将是可重现的好办法。输入和输出节点数量通过数据进行确定,而隐藏处理节点数量是自由参数,必须通过反复试验法进行确定。使用 32 位变量是 CNTK 的默认值,就因为获得通过使用 64 位的精度不是值得的性能损失的神经网络通常会开始计费。
神经网络的创建如下:
代码语言:javascript复制with C.layers.default_options(init=
C.initializer.uniform(scale=0.01)):
h_layer1 = C.layers.Dense(hidden_dim,
activation=C.ops.relu, name='hidLayer1')(X/255)
h_layer2 = C.layers.Dense(hidden_dim,
activation=C.ops.relu, name='hidLayer2')(h_layer1)
o_layer = C.layers.Dense(output_dim, activation=None,
name='outLayer')(h_layer2)
dnn = o_layer # train this
model = C.ops.softmax(dnn) # use for prediction
包含语句的 Python 是一种语法快捷方式,可以将一组常见参数应用到多个函数。此处它用于初始化为随机值-0.01 和 0.01 之间的所有网络权重。X 对象保留图像的 784 输入的值。请注意,每个值的规范化方式为除以 255,因此实际输入的值将是范围 [0.0,1.0] 中。
规范化输入的值作为第一个隐藏层的输入。第一个隐藏层的输出用作第二个隐藏层的输入。然后,第二个隐藏层的输出发送到输出层。两个隐藏的层,可使用 ReLU (线性整流单位) 激活它,图像分类通常比标准 tanh 激活更好。
请注意,不对输出节点应用任何激活。这是 CNTK 的不同寻常之处,因为 CNTK 定型函数需要使用未激活的原始值。Dnn 对象是只是便捷别名。model 对象包含 softmax 激活函数,以便在定型后用于预测。由于 Python 按引用进行分配,训练的 dnn 对象还可以训练的模型对象。
对神经网络进行定型
神经网络已准备好通过如下代码进行定型:
代码语言:javascript复制tr_loss = C.cross_entropy_with_softmax(dnn, Y)
tr_eror = C.classification_error(dnn, Y)
max_iter = 10000
batch_size = 50
learn_rate = 0.01
learner = C.sgd(dnn.parameters, learn_rate)
trainer = C.Trainer(dnn, (tr_loss, tr_eror), [learner])
培训丢失 (tr_loss) 对象告知 CNTK 如何定型时度量误差。交叉熵误差通常是分类问题的最佳选择。定型分类错误 (tr_eror) 对象可以用于在定型期间或定型后自动计算错误预测所占的百分比。指定的损失函数是必需的但指定的分类误差函数是可选的。
训练迭代数的最大数目的值,在每次,学习率定型的批中的项目数是必须通过反复试验来确定的所有可用参数。您可以将 learner 对象视为算法,并将 trainer 对象视为使用 learner 查找神经网络权重和偏置值的正确值的对象。随机梯度下降 (sgd) 学习器是大多数基元的算法,但适用于简单的问题。备用方法包括自适应时刻估计 (adam) 和均方根传播 (rmsprop)。
使用这些语句来创建定型数据的读取器对象:
代码语言:javascript复制rdr = create_reader(train_file, input_dim, output_dim,
rnd_order=True, m_swps=C.io.INFINITELY_REPEAT)
mnist_input_map = {
X : rdr.streams.x_src,
Y : rdr.streams.y_src
}
如果您检查中的 create_reader 代码图 3,可以看到,它指定数据文件中使用的标记名称 ("像素"和"数字")。您可以考虑 create_reader 和 reader 对象创建为 DNN 图像分类问题的样板代码的代码。需要更改的只是标记名称和映射字典 (mnist_input_map) 的名称。
一切就绪后,将执行培训,如中所示图 4。
图 4 培训
代码语言:javascript复制print("nStarting training n")
for i in range(0, max_iter):
curr_batch = rdr.next_minibatch(batch_size,
input_map=mnist_input_map)
trainer.train_minibatch(curr_batch)
if i % int(max_iter/10) == 0:
mcee = trainer.previous_minibatch_loss_average
macc = (1.0 -
trainer.previous_minibatch_evaluation_average)
* 100
print("batch M: mean loss = %0.4f, accuracy =
%0.2f%% " % (i, mcee, macc))
演示计划的目的,以便每次迭代处理定型项的一个批处理。许多神经网络库使用术语"epoch"来指代遍历所有定型项。在此示例中,有 1000 个定型项,并且批大小设置为 50,因为一个纪元将为 20 次迭代。
使用固定数目的迭代训练的替代方法是损失/误差低于某个阈值时停止定型。请务必在定型期间显示损失/误差,因为定型失败是规则,而不是异常。交叉熵误差很难解释直接,但你想要查看往往会变得更小的值。而不是显示平均分类误差 ("25%错误"),此演示程序计算并打印平均分类准确度 ("75%更正"),这是在我看来更自然的指标。
评估和使用模型
已训练的图像分类器后,通常需要评估上发出的测试数据的已训练的模型。如中所示,该演示计算分类准确度图 5。
图 5 计算分类准确度
代码语言:javascript复制rdr = create_reader(test_file, input_dim, output_dim,
rnd_order=False, m_swps=1)
mnist_input_map = {
X : rdr.streams.x_src,
Y : rdr.streams.y_src
}
num_test = 100
test_mb = rdr.next_minibatch(num_test,
input_map=mnist_input_map)
test_acc = (1.0 - trainer.test_minibatch(test_mb)) * 100
print("Model accuracy on the %d test items = %0.2f%%"
% (num_test,test_acc)))
新建了数据读取器。请注意,与用于定型的读取器不同,新建的读取器不会按随机顺序遍历数据,且扫描数量设为 1。重新创建 mnist_input_map 字典对象。一个常见错误是尝试并使用原始读取器,但 rdr 对象已更改,因此需要重新创建映射。Test_minibatch 函数返回其最小批参数,在这种情况下是整个 100 个项测试集的平均分类误差。
通常建议在定型后或定型期间保存模型。在 CNTK 中,保存所示:
代码语言:javascript复制mdl_name = ".\Models\mnist_dnn.model"
model.save(mdl_name)
这会保存使用默认的 CNTK v2 格式。也可以使用开放神经网络交换 (ONNX) 格式。请注意,通常需要保存 model 对象 (包含 softmax 激活函数) 而不是 dnn 对象 (无输出激活)。通过以下代码行,可以将已保存的模型从其他程序加载到内存中:
代码语言:javascript复制mdl_name = ".\Models\mnist_dnn.model"
model = C.ops.functions.Function.load(mdl_name)
加载后,就可以使用模型了,就像已定型模型一样。此演示程序不使用已定型模型进行预测。预测代码可能如下所示:
代码语言:javascript复制input_list = [0.55] * 784 # [0.55, 0.55, . . 0.55]
input_vec = np.array(input_list, dtype=np.float32)
pred_probs = model.eval(input_vec)
pred_digit = np.argmax(pred_probs)
print(pred_digit)
Input_list 具有 784 像素值,虚拟输入,每个都有值 0.55 (回想一下对模型进行训练规范化数据,因此必须在规范化的数据源)。像素值复制到一个 NumPy 数组。Eval 函数调用到 1.0 将返回 10 个值的数组的总和并松散解释为概率。Argmax 函数返回的最大值,这是可以方便地预测数字相同的索引 (从 0 到 9)。太巧妙了!
总结
使用深度神经网络用于进行简单的图像分类最常用的方法。但是,Dnn 有至少两个主要限制。首先,Dnn 不缩放至具有大量的像素的图像。其次,Dnn 没有明确考虑到图像像素的几何图形。例如,在 MNIST 图像,第二个像素的正下方的像素是输入文件中的从第一个像素 28 位置。
由于这些限制,以及其他原因,过,使用卷积神经网络 (CNN) 现在是更常见的图像分类的。也就是说,对于简单的图像分类使用 DNN 的任务更容易和通常只是受到 (或甚至更多) 比使用 CNN 效益。
Dr.James McCaffrey 供职于华盛顿地区雷蒙德市沃什湾的 Microsoft Research。他参与过多个 Microsoft 产品的工作,包括 Internet Explorer 和必应。Scripto可通过 jamccaff@microsoft.com 与 McCaffrey 取得联系。
衷心感谢以下 Microsoft 技术专家对本文的审阅:Chris Lee、Ricky Loynd、Ken Tran