移动端AI部署正在成为深度学习落地的重要方向。本文将带你完整走一遍在iOS平台上构建PyTorch推理应用的流程,从环境配置到摄像头集成,再到模型推理的端到端实现。

为什么要在iOS上部署PyTorch模型?

在完成了Android平台的模型部署后,将同样的优化模型迁移到iOS平台是一个自然的延伸。移动端机器学习的核心价值在于:让神经网络模型脱离服务器,直接在用户设备上完成推理,从而获得更低的延迟、更好的隐私保护以及离线可用性。

本节将复用此前针对MNIST手写数字识别任务优化过的PyTorch Mobile模型,构建一个iOS应用,通过手机摄像头实时拍摄手写数字并完成分类识别。整个过程涵盖Xcode开发环境搭建、CocoaPods依赖管理、摄像头采集模块编写以及模型推理集成四个关键环节。

[AFFILIATE_SLOT_1]

搭建iOS开发环境与项目初始化

构建iOS应用的第一步是获取Xcode——这是Apple官方为iOS、macOS、watchOS和tvOS平台提供的集成开发环境。需要注意的是,Xcode只能运行在macOS设备上,这是开发iOS应用的硬性前提。

在打开Xcode之前,先通过终端切换到项目目录,执行以下命令完成依赖安装:

pod install

这里使用的CocoaPods是iOS生态中最主流的开源依赖管理工具,其作用类似于Python中的pip。它能够自动下载、配置并集成第三方库,免去手动管理依赖的繁琐过程。如果系统中尚未安装CocoaPods,可以通过以下命令完成安装:

sudo gem install cocoapods

上述命令会解析项目中的Podfile文件并安装所需依赖。当前目录下的Podfile包含以下关键配置:

platform :ios, '15.0'
target 'HelloWorld' do
pod 'LibTorch-Lite', '~> 1.13.0.1'
end

执行安装后,PyTorch库(LibTorch)将被集成到项目中,为iOS应用提供深度学习模型推理所需的底层计算支持。在Xcode中打开项目文件后,界面布局如下图所示:

IDE布局

在左侧文件导航区可以看到Models文件夹,其中存放着经过移动端优化的MNIST模型文件。通过以下命令将模型从原始位置复制到Xcode项目目录:

cp ../Android/app/src/main/assets/optimized_for_mobile_traced_model.pt ./iOS/HelloWorld/HelloWorld/model/model.pt

此外,Info.plist文件是iOS应用的核心配置文件,用于向操作系统声明应用的基本行为和所需权限。为了实现摄像头调用功能,需要在该文件中添加相应的权限声明,如下图所示:

权限配置

在iOS开发规范中,每一项敏感权限都需要向用户明确说明用途,这是Apple对用户隐私保护的基本要求。✅

摄像头图像采集模块的实现

Xcode项目导航区中包含多个Swift源文件(Swift是iOS开发的主流编程语言)。负责摄像头采集功能的核心文件是CameraViewController,其中定义了处理拍摄流程所需的各类对象和方法:

class CaptureViewController: UIViewController, AVCapturePhotoCaptureDelegate {
@IBOutlet var captureButton: UIButton!
@IBOutlet var imageView: UIImageView!
private var captureSession: AVCaptureSession!
private var photoOutput: AVCapturePhotoOutput!
private var previewLayer: AVCaptureVideoPreviewLayer!
private var capturedImage: UIImage?

上述代码定义了以下核心组件:

  • 拍摄按钮对象:关联界面上的拍照按钮
  • 图像视图对象:用于动态展示摄像头捕获的画面
  • 采集会话对象:管理手机摄像头的实时图像流
  • 原始照片对象:存储拍摄得到的原始图像数据
  • 预览层对象:负责向用户渲染实时画面
  • 处理后照片对象:存储经过处理的最终图像

这些对象之后定义了若干关键方法。首先是viewDidLoad,负责视图的初始化配置:

override func viewDidLoad() {
super.viewDidLoad()
setupCamera()
}

该方法会进一步调用setupCamera方法。setupCamera首先确认应用是否已获得摄像头访问权限,然后初始化采集流程所需的各类对象,最终启动摄像头采集会话:

func setupCamera() {
captureSession = AVCaptureSession()
guard let captureDevice = AVCaptureDevice.default(for: .video) else {
fatalError("Cannot access camera.")
}
do {
let input = try AVCaptureDeviceInput(device: captureDevice)
captureSession.addInput(input)
photoOutput = AVCapturePhotoOutput()
captureSession.addOutput(photoOutput)
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill // Maintain aspect ratio
// Calculate the square frame that fits within the screen bounds
let minSideLength = min(view.bounds.width, view.bounds.height)
let previewFrame = CGRect(
x: (view.bounds.width - minSideLength) / 2,
y: (view.bounds.height - minSideLength) / 2,
width: minSideLength,
height: minSideLength
)
previewLayer.frame = previewFrame
view.layer.addSublayer(previewLayer)
captureSession.startRunning()
} catch {
fatalError("Cannot set up camera.")
}
}

以上代码实现了将摄像头图像流捕获到预览层并在界面上实时显示的逻辑。接下来是拍照按钮的响应方法,将用户点击操作与照片存储逻辑关联起来:

@IBAction func captureButtonTapped(_ sender: UIButton) {
let settings = AVCapturePhotoSettings()
photoOutput.capturePhoto(with: settings, delegate: self)
}

最后是图像处理方法,将原始照片数据转换为最终的图像对象并显示在界面上:

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let imageData = photo.fileDataRepresentation(), let image = UIImage(data: imageData) {
capturedImage = cropImage(image, to: previewLayer.frame)
performSegue(withIdentifier: "showImagePreview", sender: self)
}
}

这段代码的核心功能是将捕获的照片流转换为图像数据表示,并对其进行裁剪以适应屏幕显示范围。

集成PyTorch模型推理

应用中最核心的源代码位于ViewController文件中。该文件包含的ViewController类定义了实现模型推理和结果显示的关键对象和方法:

class PreviewViewController: UIViewController {
@IBOutlet var imageView: UIImageView!
@IBOutlet var resultView: UITextView!
var capturedImage: UIImage?

首先定义用于显示拍摄图像的图像视图对象,以及用于呈现模型预测结果的标签对象。此处初始化的图像对象,正是拍摄界面最终存储图像时使用的同一对象,它将作为输入传递给PyTorch模型进行推理。

接下来定义模型加载逻辑,用于加载经过移动端优化的PyTorch模型:

private lazy var module: TorchModule = {
if let filePath = Bundle.main.path(forResource: "model", ofType: "pt"),
let module = TorchModule(fileAtPath: filePath) {
return module
} else {
fatalError("Can't find the model file!")
}
}()

代码首先检查模型文件是否存在于目标路径,然后将其加载为TorchModule对象。这一步正是需要通过CocoaPods安装LibTorch的原因。LibTorch具备多重优势:提供简化的序列化操作、支持设备无关的高效计算,并能与PyTorch生态系统无缝集成。

然后定义标签映射变量,用于将模型输出的原始数值映射到0至9这10个数字类别:

private lazy var labels: [String] = {
if let filePath = Bundle.main.path(forResource: "digits", ofType: "txt"),
let labels = try? String(contentsOfFile: filePath) {
return labels.components(separatedBy: .newlines)
} else {
fatalError("Can't find the text file!")
}
}()

以上代码会加载labels.txt文件(与模型文件位于同一目录),该文件简单地列出了数字0到9,每行一个。最后是核心的推理方法,实现了模型推理和结果显示的完整逻辑:

override func viewDidLoad() {
super.viewDidLoad()
imageView.image = capturedImage
guard let resizedImage = capturedImage?.resized(to: CGSize(width: 28, height: 28)),
var pixelBuffer = resizedImage.grayscaleNormalized() else {
return
}
//        imageView.image = resizedImage
guard let outputs = module.predict(image: UnsafeMutableRawPointer(&pixelBuffer)) else {
return
}
print("Raw Predictions: \(outputs)") // Print the raw predictions array
// Find the index of the maximum value in the outputs array
if let maxIndex = outputs.indices.max(by: { outputs[$0].floatValue < outputs[$1].floatValue }) {
let predictedDigit = maxIndex // This is the predicted digit
print("Predicted Digit: \(predictedDigit)")
resultView.text = "Predicted Digit: \(predictedDigit)"
} else {
print("Unable to determine predicted digit")
resultView.text = "Unable to determine predicted digit"
}
}

在此方法中,首先将图像调整为28×28像素,然后转换为灰度图像,并使用基于MNIST数据集的均值和标准差对像素值进行归一化。随后调用模型执行推理,获取类别概率分布。系统记录原始概率值用于调试,同时将最高概率对应的数字类别转换为字符串显示在界面上。

下图展示了多个预览界面示例,可以看到模型能准确识别拍摄图像中的手写数字:

识别结果

该应用可通过无线连接或USB数据线将iPhone与运行Xcode的Mac相连来部署运行。连接成功后,在Xcode窗口顶部的设备下拉菜单中选择目标设备,点击运行按钮即可构建并启动应用。

[AFFILIATE_SLOT_2]

总结与展望

本文完整介绍了将PyTorch优化的MNIST手写数字识别模型部署到iOS平台的流程。通过配置Xcode开发环境、集成LibTorch库、使用Swift编写摄像头采集与图像处理模块,实现了对手写数字的实时拍摄和识别。应用成功将拍摄图像预处理为模型输入格式,执行推理并显示识别结果,验证了移动端AI模型在iOS设备上的有效部署能力。

这一方案可以进一步扩展到更复杂的自然语言处理或图像识别任务中,为移动端智能应用开发提供坚实基础。