文章目录
- 一、模型文件:人工智能的核心“蓝图”
- (一)模型文件的基本概念
- (二)模型文件的重要性及作用
- (三)模型文件的组成要素
- (四)模型文件的类型及差异
- (五)模型文件与应用程序的协作方式
- (六)模型文件的单元测试与质量保障
- (七)模型文件的更新与持续集成
- 二 创建简单模型并查看文件内容
- 三 ONNX 模型文件:跨框架的桥梁
- 四 Windows 中模型的部署:从模型到应用
一、模型文件:人工智能的核心“蓝图”
(一)模型文件的基本概念
在人工智能的广袤世界里,模型文件犹如一座桥梁,连接着理论与实践。它承载着网络结构、权重矩阵等关键信息,是实现人工智能任务的重要基石。在深入探索模型文件之前,先明确其在人工智能体系中的关键地位。
(二)模型文件的重要性及作用
实现功能的关键载体 模型文件就像是一个神奇的“黑盒”,在图像分析、自然语言处理、语音识别等众多领域发挥着核心作用。以图像分析为例,当输入一张图像时,模型文件能够利用其内部保存的信息,经过复杂的计算,输出图像的类别信息或相关特征。它不仅能够完成单次的任务处理,还为后续的再次训练、推理等操作提供了基础,极大地推动了人工智能技术的传播与发展。
促进人工智能的广泛应用 正是因为有了模型文件,人工智能技术才能够快速地应用到各个领域,为人们的生活和工作带来便利。无论是智能安防系统中的图像识别,还是智能语音助手的语音理解,模型文件都在背后默默发挥着作用。
(三)模型文件的组成要素
数据流图:计算过程的抽象表示
目前,绝大部分深度学习框架将整个 AI 模型的计算过程抽象为数据流图。用户编写的模型构建代码在框架的组织下形成了一个复杂的数据流图,这就如同神经网络的结构蓝图。在程序运行时,框架的执行器会依据调度策略依次执行数据流图中的各个节点,从而完成整个计算过程。例如,在一个图像分类模型中,数据流图描述了图像数据如何在各个层之间流动、转换,最终得出分类结果。
运行参数与权重:模型的灵魂所在
为了能够准确地完成计算任务,模型文件不仅保存了数据流图,还记录了相应的运行参数(Parameters)和训练出来的权重(Weights)。运行参数决定了模型在计算过程中的各种行为,如学习率、迭代次数等;而权重则是模型在训练过程中学习到的知识的体现,它们共同决定了模型对输入数据的处理方式和输出结果的准确性。
(四)模型文件的类型及差异
不同框架下的模型文件特点
由于每个深度学习框架都有其独特的设计理念和工具链,对数据流图的定义和粒度也不尽相同,因此各家的 AI 模型文件存在一定的差异,甚至难以通用。例如,TensorFlow 的 Checkpoint Files 使用 Protobuf 保存数据流图,用 SSTable 保存权重;Keras 则采用 Json 表述数据流图,并用 h5py 保存权重;PyTorch 主要聚焦于动态图计算,其模型文件仅用 pickle 保存了权重,而没有完整的数据流图。
TensorFlow 模型文件的优势
TensorFlow 在设计之初就充分考虑了从训练、预测到部署等一系列复杂需求,其数据流图几乎涵盖了整个过程中可能涉及的所有操作,包括初始化、后向求导及优化算法、设备部署(Device Placement)和分布式化、量化压缩等。这使得 TensorFlow 的模型文件能够完整地描述模型的运行逻辑,便于迁移到各种平台使用,为开发者提供了极大的便利。
(五)模型文件与应用程序的协作方式
运行时链接:便捷的集成方式
当应用程序使用模型文件时,如果存在可以独立执行模型文件的运行时(Runtime),如系统级别的 CoreML、WinML 和软件级别的 Caffe、DarkNet 等,开发者可以在程序中动态链接这些运行时,直接使用模型文件。这种方式简单快捷,能够充分利用现有的运行时环境,减少开发工作量。
编译集成:资源优化的选择
除了运行时链接,还可以将数据流图和执行数据流图的程序(Op Kernel)编译在一起,从而脱离运行时。由于单一模型通常只涉及有限的操作,这种编译方式可以显著减少框架所占用的资源,提高应用程序的运行效率。在将模型集成到应用程序之前,开发者需要使用模型查看工具(如 Netron 等)仔细查看模型的接口、输入输出格式和对应的范围,并对程序中传入模型的输入进行相应的预处理工作,否则可能无法获得预期的效果。
(六)模型文件的单元测试与质量保障
混淆矩阵:可视化模型性能的利器
在机器学习领域,混淆矩阵是一种用于评估模型性能的重要工具。它是一个特定的矩阵,每一列代表预测值,每一行代表实际的类别。通过混淆矩阵,可以直观地了解模型在不同类别上的预测准确性,以及是否存在类别混淆的情况。例如,在一个二分类问题中,混淆矩阵可以清晰地显示真正例、假正例、真反例和假反例的数量,帮助评估模型的准确率、召回率等性能指标。
确保模型质量的重要性
在模型训练过程中,常常会遇到这样的情况:经过一轮训练和测试,模型在大部分测试样例上表现良好,但仍有个别样例表现不佳。通过对这些不佳样例的分析,对模型进行调整和重新训练。然而,重新训练后的模型在特定例子上的准确率虽然可能提高,但无法确定它在原来已经预测准确的例子上是否仍然表现良好。此时,混淆矩阵就发挥了重要作用,它可以帮助直观地可视化模型的质量,确保模型在各种情况下都能保持稳定的性能。
(七)模型文件的更新与持续集成
不同集成方式下的更新策略
在实际应用中,当一个模型文件已经集成到应用程序并发布给众多用户后,如果又训练出了一个新的模型,如何更新众多的应用程序以实现持续开发和持续集成呢?如果应用程序依赖额外的运行时来使用模型,那么只需要更新模型文件即可;而如果是将模型和 Kernel 编译在一起的方式,就需要重新编译程序,这无疑增加了更新的复杂性和工作量。
持续集成的挑战与应对
模型文件的更新涉及到多个方面的考虑,包括兼容性、性能优化等。开发者需要在保证新模型能够准确运行的同时,尽量减少对现有用户的影响。这就要求在模型开发过程中,遵循良好的软件工程实践,如版本控制、接口设计等,以便更方便地进行模型的更新和集成。 ## 二、深入探究模型文件:实例分析与实践操作
二 创建简单模型并查看文件内容
使用 TensorFlow 的 Keras 封装创建模型
为了更深入地理解模型文件,以一个简单的例子来说明。利用 TensorFlow 的 Keras 封装,快速生成一个由一个全连接层和一个 ReLU 激活层组成的模型。首先定义尺寸为 784 的输入数据,可类比为尺寸为 28x28 的单通道图片(如 MNIST 数据集的图片)。然后让输入数据经过一层 Dense 层,设置输出尺寸为 10,最后通过 ReLU 激活层得到最终输出。值得注意的是,这里甚至没有对神经网络进行训练就直接保存了模型,目的是为了先聚焦于模型文件本身的结构和信息。
使用 Netron 查看模型文件信息
保存模型后,可以使用开源工具 Netron 打开模型文件,深入探究其中的奥秘。打开模型后,能够清晰地看到模型的整体网络结构,包括输入节点、Dense 层、ReLU 层和输出节点,这与在代码中定义的完全一致。点击输入节点,还可以查看整个模型的属性及输入输出信息。例如,模型文件的格式为 Keras v2.2.4 - tf,对应的运行时为 tensorflow,这告诉运行该模型需要相应的环境支持。同时,模型需要的输入为 float32[?,784],其中 784 表示每个输入的尺寸,“?”号表明该模型支持批量推理,可一次性处理多个输入。
分析模型节点属性
进一步点击 Dense 节点,可以查看其详细属性。在 Keras 中,Dense 层(全连接层)有一些独特的设置。这里的 Dense 层可以将激活函数内置,但在这个例子中为了方便展示,没有使用内置激活。属性中还显示了权重矩阵使用了 GlorotUniform 初始化(即 Xavier 初始化的另一个名字),偏置矩阵使用了零初始化。展开权重和偏置,可以看到模型文件保存的具体参数值,由于代码中仅进行了初始化而未训练,所以偏置的值全部为 0。同样,点击 ReLU 节点,可以了解到其相关属性,如 negative_slope 控制负区间斜率,threshold 控制激活阈值等。
三 ONNX 模型文件:跨框架的桥梁
ONNX 模型文件的结构与特点
ONNX(开放式神经网络交换)作为一种开放的模型文件标准格式,为不同人工智能框架之间的模型交互提供了可能。其文件结构具有层级性,最顶层是模型(Model),记录了模型文件的基本属性,如 ONNX 标准版本、运算符集版本、制造商信息等,其中最关键的是包含了图(Graph)的信息。图(Graph)可以理解为计算图的一种描述,由输入、输出和节点(Node)组成,节点之间通过相同的名字实现连接,形成一个完整的计算流程。ONNX 支持的运算符类型丰富,开发者可以在 ONNX 官方文档中查看详细列表。
创建 ONNX 节点的步骤
ONNX 采用 Protobuf 格式进行存储,这种格式轻便高效,适合数据存储和交换。在 Python 中,onnx 库提供了创建 ONNX 的帮助类,使操作变得更加便捷。以创建一个全连接层节点为例,由于 ONNX 没有直接的全连接运算符,需要用矩阵乘和矩阵加来组合实现。首先创建权重矩阵的常量节点,定义其输出为 fc_weights;接着创建矩阵乘节点,输入为前面定义的权重矩阵节点输出和输入数据,输出为 matmul_output;然后创建偏置矩阵的常量节点,输出为 fc_bias;最后创建矩阵加节点,输入为矩阵乘节点输出和偏置矩阵节点输出,输出为 add_output。通过这样的步骤,各个节点按照输入输出的名字连接起来,成功组成了全连接层。
weights_node = helper.make_node(op_type = "Constant", inputs = [], outputs = ["fc_weights"], value = helper.make_tensor("fc_weights", TensorProto.FLOAT, weights.shape, weights.flatten().astype(float)))
node_list.append(weights_node)matmul_node = helper.make_node(op_type = "MatMul",inputs = ["input_name", "fc_weights"],outputs = ["matmul_output"])
node_list.append(matmul_node)bias_node = helper.make_node(op_type = "Constant", inputs = [], outputs = ["fc_bias"], value=helper.make_tensor("fc_bias", TensorProto.FLOAT, bias.shape, bias.flatten().astype(float)))
node_list.append(bias_node)matmul_node = helper.make_node(op_type = "Add",inputs = ["matmul_output", "fc_bias"],outputs = ["add_output"])
node_list.append(matmul_node)graph_proto = helper.make_graph(node_list, "test", input_list, output_list)
model_def = helper.make_model(graph_proto, producer_name="test_onnx")
onnx.save(model_def, output_path)
保存多入多出三层神经网络为 ONNX 文件
对于多入多出的三层神经网络(如用于 MNIST 数据集数字识别任务的网络,包含全连接 + Sigmoid、全连接 + Tanh、全连接 + Softmax 等层),可以将其保存为 ONNX 格式的模型文件。前面已经介绍了全连接层节点的构造方法,对于激活层,由于 ONNX 内置了常见的激活函数运算符,如 Relu、Softmax、Sigmoid、Tanh 等,构造相应的节点非常容易。只需根据节点类型,指定输入和输出名字即可创建激活层节点。在读取训练好的权重和偏置数据(存储在 npz 文件中)后,重新保存到 ONNX 文件中。运行代码后,得到 mnist.onnx 文件,通过 Netron 打开可以看到模型结构符合预期,为后续的推理应用做好了准备。
if node["type"] in ["Relu", "Softmax", "Sigmoid", "Tanh"]:activate_node = helper.make_node(node["type"],[node["input_name"]],[node["output_name"]])node_list.append(activate_node)
四 Windows 中模型的部署:从模型到应用
创建 Windows 桌面应用项目
在 Windows 环境中,可以利用微软开源的 ONNX 运行时库(ONNX Runtime)进行高性能的推理,并创建一个桌面应用来实现手写数字的识别功能。这里选择使用 C# 开发语言,首先需要安装 Visual Studio 开发环境。打开 Visual Studio 2017,新建项目,在 Visual C# 分类中选择“WPF 应用”,填写项目名称(如 OnnxDemo),点击确定,即可创建一个空白的 WPF 项目。
添加模型文件到项目
项目创建完成后,需要将模型文件添加到项目中。在解决方案资源管理器中,右键点击项目,选择“添加”->“现有项”,在弹出的对话框中,将文件类型过滤器改为所有文件,导航到模型所在目录(如 mnist.onnx 文件所在目录),选择模型文件并添加。由于模型是在应用运行期间加载的,需要在编译时将模型复制到运行目录下。右键点击模型文件,选择属性,在属性面板中将“生成操作”属性改为“内容”,“复制到输出目录”属性改为“如果较新则复制”。
集成 ONNX Runtime 库
微软开源的 ONNX 运行时库提供了 NuGet 包,方便集成到 Visual Studio 项目中。在解决方案资源管理器中,右键点击引用,选择“管理 NuGet 程序包”。在打开的 NuGet 包管理器中,切换到浏览选项卡,搜索“onnxruntime”,找到“Microsoft.ML.OnnxRuntime”包(当前版本为 1.0.0),点击安装并按提示完成操作。需要注意的是,该版本不支持 AnyCPU 平台,因此需要将项目的目标架构显式改为 x64 或 x86。在解决方案上右键点击,选择配置管理器,在配置管理器对话框中将活动解决方案平台切换为 x64 或 x86,如果没有相应平台,可选择新建并按提示创建。
设计应用程序界面
打开 MainWindow.xaml 文件,将整个 Grid 片段替换为以下代码来设计应用程序界面。界面主要包含一个 InkCanvas(用于手写数字的画布,设置为黑色背景以匹配训练数据的格式)、一个 TextBlock(用于显示识别结果)和一个 Button(用于清除画布)。在 MainWindow 构造函数中调用 InitInk 方法初始化画布,设置画笔颜色为白色,并在每次画完一笔时触发 InkCanvas_StrokeCollected 事件进行识别。同时,添加 BtnClean_Click 事件实现清除画布和清空识别结果的功能。
<Grid><StackPanel><Grid Width="336" Height="336"><InkCanvas x:Name="inkCanvas" Width="336" Height="336" Background="Black"/></Grid><TextBlock x:Name="lbResult" FontSize="26" HorizontalAlignment="Center"/><Button x:Name="btnClean" Content="Clean" Click="BtnClean_Click" FontSize="26"/></StackPanel>
</Grid>private void InitInk()
{// 将画笔改为白色var attr = new DrawingAttributes();attr.Color = Colors.White;attr.IgnorePressure = true;attr.StylusTip = StylusTip.Ellipse;attr.Height = 24;attr.Width = 24;inkCanvas.DefaultDrawingAttributes = attr;// 每次画完一笔时,都触发此事件进行识别inkCanvas.StrokeCollected += InkCanvas_StrokeCollected;
}private void InkCanvas_StrokeCollected(object sender, InkCanvasStrokeCollectedEventArgs e)
{// 从画布中进行识别RecogNumberFromInk();
}private void BtnClean_Click(object sender, RoutedEventArgs e)
{// 清除画布inkCanvas.Strokes.Clear();lbResult.Text = string.Empty;
}
画布数据预处理
为了使画布上的手写数字数据能够被模型接受,需要进行数据预处理。输入数据要求是一个大小为 1x784 的 float 数组,对应 28x28 大小图片的每个像素点的色值,输出是 1x10 的 float 数组,代表识别为数字 0 - 9 的得分。因此,添加了几个函数来实现画布数据到模型输入数据的转换。首先,RenderToBitmap 函数将画布渲染到 28x28 的图片,GetPixels 函数读取图片每个像素点的值,GetInputDataFromInk 函数则生成模型需要的数组。在处理过程中,由于画布为黑白色,直接取 RGB 中的一个分量作为像素色值。
private BitmapSource RenderToBitmap(FrameworkElement canvas, int scaledWidth, int scaledHeight)
{// 将画布渲染到bitmap上RenderTargetBitmap rtb = new RenderTargetBitmap((int)canvas.Width, (int)canvas.Height, 96d, 96d, PixelFormats.Default);rtb.Render(canvas);// 调整bitmap的大小为28*28,与模型的输入保持一致TransformedBitmap tfb = new TransformedBitmap(rtb, new ScaleTransform(scaledWidth / rtb.Width, scaledHeight / rtb.Height));return tfb;
}public byte[] GetPixels(BitmapSource source)
{if (source.Format != PixelFormats.Bgra32)source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);int width = source.PixelWidth;int height = source.PixelHeight;byte[] data = new byte[width * 4 * height];source.CopyPixels(data, width * 4, 0);return data;
}public float[] GetInputDataFromInk()
{var bitmap = RenderToBitmap(inkCanvas, 28, 28);var imageBytes = GetPixels(bitmap);float[] data = new float[784];for (int i = 0; i < 784; i++){// 画布为黑白色的,可以直接取RGB中的一个分量作为此像素的色值int baseIndex = 4 * i;data[i] = imageBytes[baseIndex];}return data;
}
调用模型进行推理并展示结果
在完成数据预处理后,可以调用模型进行推理并展示结果。在 RecogNumberFromInk 方法中,首先从画布获取输入数组,然后从文件中加载模型(指定模型路径为 AppDomain.CurrentDomain.BaseDirectory + “mnist.onnx”)。使用 InferenceSession 加载模型后,创建输入容器,将输入数据转换为 DenseTensor 格式并添加到容器中(指定输入名称为“fc1x”)。接着进行推理,获取推理结果(输出结果是 IReadOnlyList,支持多个输出,但 MNIST 模型只有一个输出),从输出中找到得分最高的数字,并将其显示在 lbResult 文本控件中。至此,所有代码完成,按 F5 即可调试运行程序,在手写数字后即可看到模型的推理结果,实现了从模型文件到实际应用的完整流程。
private void RecogNumberFromInk()
{// 从画布得到输入数组var inputData = GetInputDataFromInk();// 从文件中加载模型string modelPath = AppDomain.CurrentDomain.BaseDirectory + "mnist.onnx";using (var session = new InferenceSession(modelPath)){// 支持多个输入,对于mnist模型,只需要一个输入var container = new List<NamedOnnxValue>();// 输入是大小1*784的一维数组var tensor = new DenseTensor<float>(inputData, new int[] { 1, 784 });// 输入的名称是portcontainer.Add(NamedOnnxValue.CreateFromTensor<float>("fc1x", tensor));// 推理var results = session.Run(container);// 输出结果是IReadOnlyList<NamedOnnxValue>,支持多个输出,对于mnist模型,只有一个输出var result = results.FirstOrDefault()?.AsTensor<float>()?.ToList();// 从输出中取出得分最高的var max = result.IndexOf(result.Max());// 显示在控件中lbResult.Text = max.ToString();}
}