TensorFlow 2.0 初体验:从 iOS 开发者的视角看 AI 入门

协程在摸鱼
2025-12-28 20:55
阅读 760

去年底,公司接了个“智能相册”的需求——产品经理画了一张大饼:“用户上传照片后,系统能自动识别场景、人物、甚至情绪,还能按时间线智能分组。”听起来很酷,但对我们这支以 iOS 和后端 API 为主的团队来说,AI?那是另一个宇宙的事。

我,一个写了六年 iOS 的老 Swifter,平时连 pip install 都不怎么敲(Mac 上的 Homebrew 早就够用了),突然被拉进这个项目,负责“对接模型服务”。领导原话是:“你逻辑强,学得快,先跑通个 demo,下周五演示。” 好吧,又是熟悉的“下周交付”节奏。当晚回到家,泡了杯速溶咖啡(上海租房太贵,舍不得买手冲),打开 MacBook Pro,开始硬着头皮啃 TensorFlow 2.0。

说实话,一开始我对 TensorFlow 的印象还停留在 1.x 时代那种“写个加法都要建图、会话、初始化”的噩梦里。但这次一上手 2.0,发现 Google 真的听劝了——Eager Execution 默认开启,Keras 直接整合进来,API 简洁得让我这个前端思维的人直呼“这不就是 PyTorch 的感觉吗?”(别打我,iOS 开发偶尔也会看隔壁 Android 团队在干嘛)。

今天这篇技术分享,就结合我们“智能相册”的实战经验,聊聊 TensorFlow 2.0 的基础概念。不讲数学推导,不堆公式,只讲我在项目中踩过的坑、调过的参、以及最后怎么让模型在测试集上准确率干到 85%+ 的综合心得。


为什么选 TensorFlow 2.0?而不是 Core ML?

先说清楚背景:我们最终目标是把模型部署到 iOS App 里,用 Core ML 跑推理。那为什么不直接用 Create ML?因为业务复杂度高——需要同时识别 场景类型(如海滩、办公室、餐厅)、人物数量、以及是否包含宠物。Create ML 的图像分类模板只能做单标签,而我们需要多任务输出。

于是架构定下来了:在服务器用 TensorFlow 2.0 训练一个多输出模型 → 导出为 SavedModel → 用 tfcoreml 工具转成 .mlmodel → 集成进 iOS App

所以,我的学习路径很明确:先搞懂 TF 2.0 怎么训练、怎么评估,再搞定转换。下面这些概念,都是我在实际调试中反复验证过的。


数据准备:别低估“脏数据”的杀伤力

我们的训练数据来自内部员工自愿上传的照片(约 5000 张),标注由外包团队完成。本以为数据 ready 就能开训,结果第一次跑完 val_loss 直接爆到 3.0+,准确率不到 40%。

问题出在哪?标签不一致。比如同一张“咖啡馆自拍”,有人标“餐厅”,有人标“休闲”,还有人标“人物”。更离谱的是,有几张猫的照片被标成了“人物”(估计标注员眼花了)。

实战经验
tf.data.Dataset 加载阶段,我就加了数据清洗逻辑:

def clean_label(label):
    # 统一场景标签映射
    scene_map = {
        'cafe': 'restaurant',
        'bar': 'restaurant',
        'office_building': 'office'
    }
    if label in scene_map:
        return scene_map[label]
    return label

# 在 dataset pipeline 中应用
dataset = dataset.map(lambda x, y: (x, clean_label(y)))

另外,数据增强是必须的。手机拍的照片角度、光线千奇百怪,不增强等于裸奔。TF 2.0 的 ImageDataGenerator 虽然老派,但对新手友好:

datagen = ImageDataGenerator(
    rotation_range=20,
    width_shift_range=0.2,
    height_shift_range=0.2,
    horizontal_flip=True,
    zoom_range=0.2,
    rescale=1./255  # 别忘了归一化!
)

吐槽一句:运维同事一开始把图片存在 NFS 上,IO 慢得像蜗牛。后来改成本地 SSD + tf.data.AUTOTUNE 自动并行加载,训练速度直接翻倍。果然,AI 项目 80% 的时间花在数据上,这话真不假。


模型搭建:Keras 是真香

TF 2.0 最大的改进,就是把 Keras 当作高级 API 官方推荐。对我这种不想手动管理计算图的人来说,简直是福音。

我们的模型结构其实不复杂:共享的 CNN 主干 + 三个独立的全连接头(场景分类、人物计数、宠物检测)。用函数式 API 写起来清晰又灵活:

from tensorflow.keras import layers, Model

# 共享主干:用预训练的 MobileNetV2(轻量,适合移动端)
base_model = tf.keras.applications.MobileNetV2(
    input_shape=(224, 224, 3),
    include_top=False,
    weights='imagenet'
)
base_model.trainable = False  # 先冻结,后面 fine-tune

# 输入
inputs = layers.Input(shape=(224, 224, 3))

# 特征提取
x = base_model(inputs, training=False)
x = layers.GlobalAveragePooling2D()(x)

# 场景分类头(10 类)
scene_output = layers.Dense(128, activation='relu')(x)
scene_output = layers.Dropout(0.5)(scene_output)
scene_output = layers.Dense(10, activation='softmax', name='scene')(scene_output)

# 人物数量头(回归问题,0-5 人)
person_output = layers.Dense(64, activation='relu')(x)
person_output = layers.Dropout(0.3)(person_output)
person_output = layers.Dense(1, activation='relu', name='person_count')(person_output)  # 注意:这里不用 sigmoid!

# 宠物检测头(二分类)
pet_output = layers.Dense(32, activation='relu')(x)
pet_output = layers.Dropout(0.3)(pet_output)
pet_output = layers.Dense(1, activation='sigmoid', name='has_pet')(pet_output)

# 构建模型
model = Model(inputs=inputs, outputs=[scene_output, person_output, pet_output])

注意几个细节:

  • 多输出必须命名,否则后面计算 loss 时会报错。
  • 人物数量是回归任务,输出层不能用 softmax/sigmoid,直接用 ReLU 限制非负即可(实际训练中发现 tanh 效果反而差)。
  • 预训练模型记得冻结,先 train head,再 fine-tune。

损失函数与优化器:别乱配,会翻车

多任务学习最头疼的就是 loss 权重分配。一开始我天真地认为“三个 loss 直接相加就行”,结果模型完全偏向场景分类(因为它的 loss 值最大),人物和宠物几乎没学到。

查了论文 + GitHub issues,才知道要动态调整 loss 权重,或者至少做归一化。我们采用简单粗暴但有效的方法:根据验证集上的初始 loss 比例反向赋权

# 先跑一个 epoch,记录各任务 loss
initial_losses = model.evaluate(val_dataset, verbose=0)
scene_loss_init = initial_losses[1]  # 假设索引1是scene loss
person_loss_init = initial_losses[2]
pet_loss_init = initial_losses[3]

# 计算权重(让初始总 loss ≈ 1)
total = scene_loss_init + person_loss_init + pet_loss_init
w_scene = 1.0 / scene_loss_init
w_person = 1.0 / person_loss_init
w_pet = 1.0 / pet_loss_init

# 编译模型
model.compile(
    optimizer='adam',
    loss={
        'scene': 'categorical_crossentropy',
        'person_count': 'mse',          # 回归用 MSE
        'has_pet': 'binary_crossentropy'
    },
    loss_weights={
        'scene': w_scene,
        'person_count': w_person,
        'has_pet': w_pet
    },
    metrics={
        'scene': 'accuracy',
        'person_count': 'mae',
        'has_pet': 'accuracy'
    }
)

这个 trick 让三个任务的 loss 下降曲线终于同步了!算法工程师可能觉得 low,但对我们这种“兼职 AI”的开发者来说,实用主义万岁

优化器方面,Adam 默认参数 (lr=0.001) 起手,后期如果 loss 卡住,就用 ReduceLROnPlateau 自动降学习率:

reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(
    monitor='val_loss',
    factor=0.5,
    patience=3,
    min_lr=1e-6
)

训练与调优:耐心是第一生产力

我们用 4 块 Tesla T4(感谢公司云资源),batch_size=32,跑了 50 个 epoch。前 20 个 epoch 只训练 heads,后 30 个解冻 MobileNetV2 的最后 30 层做 fine-tune。

关键配置对比表

阶段 学习率 训练层 Epochs Val Accuracy (Scene)
Head Only 0.001 仅新增 Dense 层 20 72.3%
Fine-tune 0.0001 最后 30 层 + heads 30 85.6%

可以看到,fine-tune 带来了 13%+ 的提升。但要注意:学习率必须降低,否则预训练权重会被破坏。

另外,早停(Early Stopping)一定要加!我们有一次因为没加,多跑了 10 个 epoch,结果过拟合了,test accuracy 反而下降。血泪教训:

early_stop = tf.keras.callbacks.EarlyStopping(
    monitor='val_loss',
    patience=5,
    restore_best_weights=True  # 自动恢复最佳权重!
)

模型导出与 iOS 集成:别卡在最后一步

训练完只是开始。要把模型塞进 iPhone,还得过两关:

  1. 导出为 SavedModel
  2. 转换为 Core ML 格式

SavedModel 导出很简单:

model.save('smart_album_model')
# 或者用 tf.saved_model.save(model, 'path')

但转换时遇到大坑:tfcoreml 不支持某些 TF 2.0 ops,尤其是 tf.function 装饰的自定义层。我们的模型因为用了预训练 MobileNetV2,还好没触发这个问题。

转换命令:

pip install tfcoreml
tfcoreml.convert(
    tf_model_path='smart_album_model',
    mlmodel_path='SmartAlbum.mlmodel',
    output_feature_names=['scene', 'person_count', 'has_pet'],
    input_name_shape_dict={'input_1': [1, 224, 224, 3]}
)

成功生成 .mlmodel 后,拖进 Xcode,Swift 调用简直丝滑:

import CoreML

let model = try? SmartAlbum(configuration: MLModelConfiguration())
let pixelBuffer = // 从 UIImage 转来的 CVPixelBuffer
let prediction = try? model?.prediction(input: SmartAlbumInput(image: pixelBuffer))

print("场景: \(prediction?.scene ?? "")")
print("人数: \(prediction?.personCount ?? 0)")
print("有宠物: \(prediction?.hasPet ?? false)")

实测 iPhone 12 上单图推理耗时 ~45ms,完全满足 App 要求(产品经理居然没再提“能不能更快点”)。


总结:iOS 开发者也能玩转 AI

回过头看,从零开始搞 TensorFlow 2.0,其实没想象中那么恐怖。Google 这次真的把易用性做到了极致——Eager Execution 让调试像写 Python 脚本,Keras 抽象掉大部分底层细节,丰富的文档和社区支持也省了不少事

当然,过程中也踩了不少坑:

  • 数据清洗比算法选择更重要
  • 多任务 loss 权重不能拍脑袋定
  • 模型转换是隐藏关卡,提前查兼容性
  • 移动端部署要考虑模型大小和推理速度(MobileNetV2 真香)

现在,“智能相册”功能已经上线两周,用户反馈还不错。虽然离“精准识别用户情绪”还有距离(那是下个版本的故事了),但至少我们证明了:一个专注客户端的团队,也能快速集成 AI 能力

如果你和我一样,是个被临时抓壮丁的“非专业 AI 工程师”,别慌。TensorFlow 2.0 的设计哲学就是“让机器学习变得简单”。从一个小任务开始,跑通 end-to-end 流程,你会发现:AI 并没有那么神秘,它只是另一种“算法”而已——只不过,这次的输入是像素,输出是可能性。

最后送大家一句我在 GitHub issue 里看到的话:“Don’t fear the tensor.

(完)

P.S. 写完这篇已经是凌晨两点,窗外上海的雨还没停。明天还要改 iOS 的 UI 适配,产品经理说“那个 AI 按钮不够圆润”……唉,程序员的命,都是咖啡续的。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝