C---深度学习-一-
C++ 深度学习(一)
译者:飞龙
C++ 深度学习
本书献给作为读者的你——感谢你的好奇心、坚持不懈,以及不断学习、构建和完善的意愿。——Bill Chen
序言
深度学习重塑了现代软件,为视觉、语言、语音、推荐、医疗健康和机器人等应用提供动力。虽然 Python 已成为实验的标准语言,但生产系统往往依赖 C++ 来获得性能、效率和可控性。当延迟、内存使用和硬件优化至关重要时,C++ 仍然是深度学习技术栈中的关键组成部分。
这正是本书的价值所在。它为读者提供了一条从深度学习概念到 C++ 实际实现的实用路径。本书并未将 C++ 视为次要工具,而是将其作为构建、优化和部署现代深度学习系统的重要环境。
我最欣赏的是本书全生命周期的方法。它从环境搭建、数据准备和 CUDA 加速等基础内容开始,接着介绍核心模型家族,包括神经网络、CNN、RNN、LSTM 和生成模型。它还涵盖了分布式训练、模型压缩、推理优化、部署、调试、监控和可解释性。
这使得本书既适用于进入深度学习领域的 C++ 工程师,也适用于希望从 Python 原型转向生产级系统的机器学习从业者。它弥合了理论与部署之间的鸿沟,展示了如何构建不仅准确,而且高效、可靠和实用的模型。
希望你喜欢这本书。
Bill Chen
贡献者
关于作者
Bill Chen 是 Meta 的机器学习工程师,专注于深度学习、CUDA 和 C++。他拥有肯塔基大学的生物信息学博士学位,并在应用 AI 领域担任过生产和教学职务。他曾在 NVIDIA 深度学习学院授课,获得了 NVIDIA 认证助理:生成式 AI 多模态资格认证,并曾担任 UCSC 硅谷延伸学院的兼职机器学习教员。他的工作包括 Facebook 群组搜索建模和手术时长预测。在本书中,他结合行业经验和教学经验,指导读者使用 C++ 构建高性能深度学习系统。
我衷心感谢我的编辑 Nathanya,感谢她在本书写作过程中给予的耐心、灵活性和支持,尤其是当我在美国陆军服役期间。她的理解和鼓励使我在那段艰苦时期能够继续这个项目。我也感谢审稿人提出的宝贵意见和对稿件的仔细阅读,并感谢整个 Packt 团队在出版过程中给予的指导和支持。
Vikash Gupta Ph.D. 是位于华盛顿州西雅图的 Amazon Web Services(AWS)的高级解决方案架构师。他在法国 INRIA 获得计算生物学博士学位,研究重点为神经影像和统计建模。在 AWS,他应用深度学习和人工智能推进医学影像技术,为 MONAI 等医疗健康开源计划做出贡献。他还曾担任俄亥俄州立大学的研究科学家和梅奥诊所的助理教授。他已发表 60 多篇同行评审论文。
我要感谢家人一直以来的支持,尤其是我的妻子 Garima Saraowgi,她在整个写作过程中给予了我极大的支持。我还要感谢我的老师们,特别是 Xavier Pennec 教授,他在我的职业生涯中不断挑战和引导我。此外,我要感谢 Packt 团队的贡献,特别是 Nathanya Dias,她对此书内容进行了认真审校,并让我保持动力和专注。最后,本书献给所有努力学习并构建新解决方案的学生们——为了后来者。
— Gustave, Claire Obscure
关于审稿人
Sriram M 是一位 AI 架构师、顾问和技术作者,拥有二十年的行业经验。他专注于量子 AI、高级机器学习和安全模型部署。他是《使用生成式 AI 进行时间序列预测》的合著者,并担任技术书籍审稿人,以严谨、研究驱动的方式评估每份手稿。作为导师和客座教员,他向多样化的学习者教授 Python 和 AI,并通过出版物、同行评审和量子储层计算方面的持续研究为学术界做出积极贡献。他还是行业活动的特邀演讲者。
Vishwas BV 是一位高级首席数据科学家和 AI 研究员,拥有 13 年以上在制造、能源、电信和零售领域构建可扩展机器学习、生成式 AI 和智能体 AI 系统的经验。Vishwas 专注于将复杂的业务问题转化为生产级 AI 架构,包括 RAG、多智能体工作流和时间序列基础模型。他是 Springer 出版的两本书的作者,也是一项已获授权的美国专利的共同发明人,该专利涉及 AI 驱动的排放建模,支持了一笔数十亿美元的交易。他拥有 AWS ML 和 Azure ML 认证,将深厚的技术专长与可衡量的业务影响力相结合。
前言
深度学习已成为现代软件系统中最重要的技术之一,为计算机视觉、语音、语言、推荐、医疗健康、金融、机器人以及实时决策等应用提供动力。虽然大多数深度学习工作流都是基于 Python 实现的,但许多生产环境最终需要 C++ 提供的速度、控制力和系统级效率。本书旨在向你展示如何弥合这一差距。本书面向机器学习工程师、深度学习从业者、软件工程师以及具备技术背景的数据科学家,他们希望了解如何在 C++ 中构建、训练、优化、部署和监控深度学习系统。在本书中,你将经历以下过程:从搭建 C++ 深度学习环境和准备数据开始,到构建核心神经架构(如多层感知机、卷积网络、循环网络、生成模型和基于 Transformer 的系统),最后在性能至关重要的环境中部署、调试、监控和解释生产模型。
C++ 依然是高性能深度学习应用最强大的语言之一,因为它提供了对内存、并发、CPU/GPU 资源及运行时行为的精细控制。当你需要低延迟推理、高效模型服务、硬件感知优化或在资源受限系统上部署时,这一点至关重要。然而,对许多从业者而言,从基于 Python 的实验转向 C++ 实现,可能会感觉像跨越一道缺乏实用指南的鸿沟。本书旨在通过将深度学习概念与现代 C++ 的动手实现模式相结合来弥合这一鸿沟。
我们首先奠定让你高效工作所需的基础。你将首先回顾关键的深度学习概念,并为模型开发搭建一个可用的 C++ 环境。接下来,你将学习如何高效地准备和预处理数据,以及如何使用 CUDA 在 GPU 上加速训练和推理。这些早期章节旨在为你提供在 C++ 中开展严肃深度学习工作所需的概念基础与工具。
在环境和基础知识准备就绪后,本书将转向模型构建与训练。你将从基础神经网络开始,理解 C++ 中的前向传播、反向传播、优化和评估。然后,你将把这些思想扩展到多层感知器、用于图像任务的卷积神经网络,以及用于序列数据的循环架构,如 C++ 中的循环神经网络(RNNs)和长短期记忆网络(LSTMs)。接下来,本书将扩展到生成建模,涵盖自编码器、生成对抗网络(GANs)以及核心大语言模型概念,然后进入分布式训练、多 GPU 执行、并行化以及量化、剪枝等模型压缩策略。
本书的最后部分聚焦于模型在开发中工作之后会发生什么。你将学习如何使用 TorchScript 和开放神经网络交换(ONNX)等格式为推理准备模型,将其部署到云、本地或边缘环境,并针对现实世界性能进行优化。在此基础上,本书还涵盖调试生产故障、在数据变化时重新训练模型、监控生产系统,以及通过局部可解释模型无关解释(LIME)、SHapley 加法解释(SHAP)和梯度加权类激活映射(Grad-CAM)等可解释性技术提高透明度。最后,你将获得关于 C++ 中完整深度学习生命周期的实际视角,从实现到运营可靠性。
本书刻意注重实践。它不是将 C++ 仅仅视为机器学习的另一种语法,而是将 C++ 视为构建可部署深度学习系统的严肃工程环境。在此过程中,你将看到性能问题、软件设计选择、部署约束和可观测性考量如何影响生产级 AI 系统的构建方式。我的目标不仅是帮助你理解深度学习在 C++ 中如何工作,还要帮助你编写快速、健壮且适合实际使用的系统。
在撰写本书时,我希望能将许多从业者分别经历的两个世界结合起来:深度学习理论与实验的世界,以及生产级系统工程的世界。C++ 正处于这一交汇点。它使我们能够超越原型,进入可靠、高效的实现,从而大规模服务于要求苛刻的应用。我希望这本书能帮助你对此转变充满信心,并为在现代 C++ 中构建深度学习系统提供实用的基础。
本书读者对象
本书面向希望在 C++ 中构建高性能深度学习系统的机器学习工程师、深度学习工程师、软件工程师和数据科学家。对于那些已经熟悉机器学习和神经网络基础知识,并希望进一步接近生产级实现、优化和部署的读者来说,本书尤其有用。
你应该熟悉 C++ 的核心编程概念,例如类、内存管理和基本并发。对机器学习和深度学习概念(包括神经网络、训练循环和常见模型架构)有基础到中级的理解,将有助于你充分利用本书。熟悉 PyTorch 或 TensorFlow 等框架是有帮助的,但事先有使用其 Python API 的经验就足够了;本书将帮助你把这些经验转化为 C++ 工作流。
如果你是以 Python 为先的从业者,希望了解深度学习系统如何在更低级、对性能敏感的环境中实现,本书将帮助你完成这一转变。如果你是希望将系统背景应用于现代 AI 工作负载的 C++ 开发者,本书将为你提供一条进入深度学习的结构化路径。如果你从事实时或资源受限的应用程序,且延迟、吞吐量和效率至关重要,那么本书正是根据这些约束编写的。
本书涵盖的内容
-
第 1 章,C++ 深度学习入门与环境搭建,介绍深度学习的核心概念,解释为什么 C++ 对性能密集型 AI 工作负载具有价值,并逐步讲解一个实用 C++ 深度学习环境的搭建。
-
第 2 章,C++ 中的数据准备与预处理,涵盖深度学习的数据流水线方面,包括归一化、数据增强、高效加载、大数据集的内存感知处理,以及 C++ 中的自定义数据集加载器。
-
第 3 章,CUDA 在 C++ 深度学习中的 GPU 加速,介绍 CUDA,并展示如何使用 GPU 加速来加速训练和推理、优化关键性能代码路径以及分析瓶颈。
-
第 4 章,在 C++ 中构建基础神经网络,逐步讲解一个简单神经网络的实现,包括神经元、激活函数、前向传播、损失计算、反向传播和基于梯度的优化。
-
第 5 章,C++ 中的多层感知器,扩展到更深的前馈网络,展示如何添加隐藏层、使用 dropout 和批归一化、调整超参数,并评估在实际任务上的性能。
-
第 6 章,C++ 中的卷积神经网络,介绍基于图像的数据,展示如何使用神经网络进行图像分类、图像分割,以及各种图像预处理和数据增强技术。
-
第 7 章,C++ 中的循环神经网络与长短期记忆网络,讨论序列数据,特别是文本数据,涵盖其数学基础、训练算法(BPTT)、实用的 C++ 实现、文本处理技术、词嵌入,以及在文本预测和神经机器翻译中的实际应用。
-
第 8 章,C++ 中的生成网络、自编码器与大语言模型,涵盖基础生成式 AI 架构和 GANs,以及自回归文本生成原理、采样策略,以及用于评估生成模型性能的综合评价指标。
-
第 9 章,C++ 中的 Transformer 与大语言模型微调,探讨 Transformer 架构及其组件、其编码器-解码器实现,进而介绍分布式训练策略和模型压缩。
-
第 10 章,部署和优化推理模型,说明如何导出模型用于部署、在生产环境中提供服务、优化推理性能,以及构建 C++ 实时推理 API。
-
第 11 章,已部署模型的调试与重新训练,展示如何诊断性能下降、检测并应对模型漂移、改进日志记录和调试工作流,以及构建实用的重新训练策略。
-
第 12 章,监控已部署模型,聚焦于生产可观测性,包括跟踪延迟、准确率和资源利用率,设置日志系统,并长期维护模型质量。
-
第 13 章,深度学习模型中的可解释性与透明性,介绍 LIME、SHAP 和 Grad-CAM 等实用可解释性技术,并讨论可解释性如何支持更安全、更可信的部署。
如何充分利用本书
要充分利用本书,你需要具备 C++ 的实用知识,并对机器学习和深度学习基础有基本了解。你不必是这里涵盖的每个模型家族的专家,但你应该已经熟悉监督学习、损失函数、梯度下降以及训练和评估模型的一般概念。使用 Python 深度学习框架的经验也会有所帮助,尤其是在你使用本书向更注重性能的 C++ 实现转变时。
如果你准备直接动手实践代码示例,而不是只在概念上阅读,你将从本书中获得更多收获。当你亲自编译、运行、分析和修改代码时,本书中的主题会变得更加清晰。由于本书涵盖环境搭建、CUDA 加速、分布式训练、部署格式和推理优化,因此最好能有一个开发环境,让你在学习过程中同时实验 CPU 和 GPU 工作流。
下载示例代码文件
本书的代码包托管在 GitHub 上,地址为 https://github.com/PacktPublishing/Deep-Learning-with-C-。我们还有来自丰富图书和视频目录的其他代码包,可在 https://github.com/PacktPublishing 获取。快去看看!
使用的约定
本书通篇使用了一些文本约定。
CodeInText:表示文本中的代码词、类名、函数名、文件名、文件扩展名、路径名、模型文件、库名和命令行参数。例如:“我们切换到 eval(),创建一个示例输入,并跟踪模块以生成 tinynet.ts。”
代码块设置如下:
export TORCH=/path/to/libtorch
clang++ -std=c++17 -O2 micro_batcher_demo.cpp \
-I"$TORCH/include" -I"$TORCH/include/torch/csrc/api/include" \
-L"$TORCH/lib" -Wl,-rpath,"$TORCH/lib" \
-ltorch -lc10 -pthread -o mb_demo
./mb_demo
前言
所有命令行输入或输出都按如下格式书写:
!nvcc --version
!nvidia-smi
粗体:表示新术语、重要词语或你屏幕上看到的文字。例如,菜单或对话框中的词语在文本中这样显示。比如:“在深入探讨之前,我们先从结构化/表格信号转向非结构化模态。”
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果你对本书的任何方面有疑问,或有任何一般性反馈,请发送电子邮件至 customercare@packt.com,并在邮件主题中提及书名。
勘误:尽管我们已尽力确保内容的准确性,但错误仍可能发生。如果你在本书中发现错误,我们将不胜感激你能向我们报告。请访问 http://www.packt.com/submit-errata,点击提交勘误,并填写表单。
盗版:如果你在互联网上以任何形式遇到我们的作品的非法副本,请将地址或网站名称提供给我们,我们将不胜感激。请通过 copyright@packt.com 联系我们,并附上材料链接。
如果你有兴趣成为作者:如果你有擅长的主题,并且有兴趣撰写或参与编写一本书,请访问 http://authors.packt.com/。
警告或重要说明会以这种方式显示。
提示和技巧会以这种方式显示。
随书免费福利
本书附带免费福利以支持你的学习。立即激活它们以获得即时访问权限(有关说明,请参阅“如何解锁”一节)。
以下是购买后可立即解锁内容的快速概览:
如何解锁
扫描二维码(或访问 packtpub.com/unlock)。按名称搜索本书,确认版本,然后按照页面上的步骤操作。
注:请妥善保管你的发票。直接从 Packt 购买不需要发票。
分享你的想法
一旦你阅读了使用C++进行深度学习,我们很乐意听到你的想法!请点击此处直接前往本书的亚马逊评论页面并分享你的反馈。
你的评论对我们和技术社区都很重要,并将帮助我们确保提供优质的内容。
第1部分:C++深度学习基础
在本书的这一部分,你将构建在 C++ 中有效使用深度学习所需的技术基础。我们将从深度学习的核心概念开始,然后搭建 C++ 开发环境,包括本书中使用的关键库和工具。你还将学习如何高效地准备和预处理数据,以及如何使用计算统一设备架构(CUDA)来加速图形处理器(GPU)上的训练和推理。在本部分结束时,你将能够在 C++ 中实现神经网络,并扎实掌握支持现代深度学习系统的工具、数据管道和性能基础。
本书的这一部分包括以下章节:
-
第1章,使用C++进行深度学习与环境搭建简介
-
第2章,C++中的数据准备与预处理
-
第3章,使用C++进行深度学习中的CUDA GPU加速
第1章:使用C++进行深度学习与环境搭建简介
深度学习已成为现代人工智能(AI)的基石,为从自动驾驶汽车到高级推荐系统的一切提供动力。在本章中,我们将介绍深度学习的核心原理,并引导你完成使用 C++ 搭建深度学习环境的过程。你将理解为什么 C++ 在高性能任务中备受青睐,尤其是在优化内存使用和高效利用 CPU 与 GPU 资源方面。
除了涵盖理论基础外,本章还将为你提供配置 C++ 开发环境的实用技能,包括安装相关的深度学习库。
在本章中,我们将讨论以下主题:
-
深度学习概念简介
-
使用 C++ 进行深度学习的优势
-
为深度学习搭建 C++ 环境
-
C++ 在深度学习中的关键性能优势
到本章结束时,你将能够开始用 C++ 编写深度学习模型,这使本章成为你深度学习之旅中至关重要的第一步。
技术要求
本节详细说明阅读本章及本书内容所需的前提条件:
-
硬件:
-
CPU(必需):64位 x86-64 或 Apple Silicon
-
推荐:支持 AVX2(大多数现代 Intel/AMD CPU)以获得更快的 BLAS/线性代数性能
-
内存:≥8 GB RAM(对于较大示例,推荐 ≥16 GB)
-
磁盘:≥15 GB 可用空间(工具链 + 库 + 示例数据)
-
GPU(可选,适用于 CUDA 章节):计算能力 ≥7.0 的 NVIDIA GPU(例如 V100/T4/A10/A100/H100):
-
驱动程序:与你的 CUDA Toolkit 版本匹配的最新 NVIDIA 驱动程序
-
显存(VRAM):推荐 ≥8 GB 以获得良好体验
-
-
Apple Silicon 没有 CUDA;请在本地使用仅 CPU 的构建,或为 GPU 章节使用带 NVIDIA GPU 的云虚拟机。
-
-
操作系统:
-
Linux:Ubuntu 20.04/22.04(或同等版本)
-
Windows:Windows 10/11(x64)
-
macOS:13 Ventura 或更高版本(Apple Silicon 或 Intel)
-
WSL2(Windows):支持并推荐用于类 Linux 的工作流程
-
-
编译器和构建工具:
-
C++ 标准:C++17(部分示例也可用 C++20 编译)
-
编译器:
-
Linux:GCC ≥11 或 Clang ≥14
-
Windows:微软 Visual C++(MSVC)— Visual Studio 2022,v19.3x
-
macOS:Apple Clang ≥14
-
-
你的购买包含一份免费 PDF 副本 + 独家附加内容
你的购买包含本书的一份无 DRM 的 PDF 副本、Packt+ 图书馆的 7 天试用(无需信用卡)以及额外的独家附加内容。请参阅前言中的随书免费福利一节,立即解锁它们并最大化你的学习体验。
-
构建系统:CMake ≥3.22(主要),不过许多代码片段也显示单文件 g++/clang++ 命令
-
包管理器(可选但有帮助):Linux 上的
apt/dnf,macOS 上的Homebrew,Windows 上的vcpkg或choco -
本书使用的核心库:
-
Eigen 3.4+(仅头文件):用于从零开始演示的线性代数(CPU)
-
LibTorch(PyTorch C++ API):推理/训练示例(所有操作系统上均可 CPU 构建;在 Linux/Windows 上支持 NVIDIA GPU 的 CUDA 构建):
- 预计下载量较大(约 1–2 GB)
-
OpenCV 4.x(可选):在少数示例中用于图像 I/O 可视化
-
GoogleTest(可选):在部分章节中用于单元测试
-
我们会始终注明仅使用 CPU 的路径。当存在 GPU 路径时,我们会明确标注。
-
CUDA(可选 GPU 路径):
-
CUDA Toolkit:12.x(Toolkit +
nvcc) -
驱动程序:匹配的 NVIDIA 驱动程序(使用
nvidia-smi验证) -
Nsight 工具(可选但推荐):Nsight Systems/Compute,用于性能分析
-
nvidia-container-toolkit(可选):用于 Docker GPU 工作流
-
-
编辑器和集成开发环境(IDE):
-
任何现代 C++ IDE/编辑器均可:VS Code、CLion 或 Visual Studio
-
启用 CMake Tools 或正确设置你的编译器套件
-
-
网络访问:
- 需要稳定的互联网连接来获取工具链、LibTorch 归档、模型和小型数据集。某些构件较大,因此快速连接会有所帮助。
-
云(如果你没有本地 NVIDIA GPU):
-
从 AWS/Azure/GCP 租用带 NVIDIA GPU 的按需虚拟机(推荐 Ubuntu LTS 镜像)
-
对于轻量级探索,Colab/Kaggle 可用于不需要系统级安装的 CUDA 部分
-
使用C++的深度学习简介与环境设置
-
环境验证(快速检查):
运行以下代码(根据您的操作系统/Shell调整);您应该看到合理的版本且没有错误:
# Compiler & CMake g++ --version cmake --version # (Linux/WSL) Check BLAS-friendly CPU flags lscpu | egrep 'avx|sse' # Look for AVX/AVX2 # LibTorch headers present (after you download & set TORCH path) ls $TORCH/include/torch/torch.h # Optional: CUDA toolchain & driver nvcc --version nvidia-smi -
已知兼容性说明:
-
Windows + LibTorch:确保MSVC工具集与LibTorch构建匹配;将
c10.dll/torch_cpu.dll(以及CUDA 动态链接库(DLL),如果使用)与可执行文件一起放在PATH中。 -
macOS(Apple Silicon):使用仅CPU的LibTorch构建;没有CUDA。Homebrew有助于处理Eigen/OpenCV。
-
WSL2:在Windows上安装NVIDIA驱动程序,并在Ubuntu中安装WSL2 CUDA用户空间;在WSL内使用
nvidia-smi验证。
-
-
最低配置与推荐配置:
-
最低配置(仅CPU):C++17编译器、CMake、Eigen、LibTorch(CPU)、8 GB内存
-
推荐配置(为了舒适体验和GPU章节):支持C++20的工具链、CMake、Eigen、LibTorch(CPU + CUDA)、OpenCV、Nsight工具、16 GB内存、NVIDIA GPU(≥8 GB显存)
-
第1章:深度学习概念简介
深度学习已经改变了人工智能,使机器能够学习复杂模式并以前所未有的准确性做出明智决策。从核心来看,深度学习主要依赖于神经网络——一种受人类大脑结构和功能启发的深度学习算法。
神经网络——深度学习的构建模块
神经网络由相互连接的人工神经元(或节点)层组成,这些层执行计算并传播结果。神经网络的基本结构包括以下内容:
-
输入层:接收初始数据
-
隐藏层:处理信息
-
输出层:产生最终结果或预测
在图1.1中,我们展示了一个具有相互连接的输入层、隐藏层和输出层的简单神经网络。
图1.1:具有输入层、隐藏层和输出层的简单神经网络概览
每个节点之间的连接都带有在训练过程中调整的权重。网络通过缩小其预测与实际结果之间的差距来学习,这个过程称为反向传播。神经网络的核心是感知器——一个基本单元,可以看作一个线性模型,其输出通过非线性激活函数传递,以引入灵活性并捕获复杂模式。
图1.2:这是感知器的简化版本
在图1.2中,我们可以看到感知器的前半部分实际上是输入(x)和权重(w)的线性组合,即(z = \sum_{i=1}^{n} w_i x_i)。然后,得到的值将传入激活函数(\varphi)。这提供了非线性。非线性防止神经网络中的所有神经元坍缩成一个等效神经元,从而使网络能够学习复杂函数。
最常用的激活函数是ReLU、sigmoid和tanh。它们引入了以下能力:
-
增加非线性:如果没有激活函数,无论我们堆叠多少层,网络都会表现得像一条大直线。这些函数将输出弯曲成曲线,使模型能够捕获更复杂的模式。
-
做出更好的决策:例如,在图像分类中,一条直线可能不足以区分猫和狗。激活函数让网络绘制弯曲的边界,这更擅长分离混乱的现实世界数据。
-
塑造输出:
-
ReLU像一个开关:它保留正数并将负数变为零。它快速且高效。
-
Sigmoid将值压缩到0和1之间。它非常适合输出类似于概率的场景。
-
Tanh将值压缩到-1和1之间,因此它可以表示正负信号。
-
-
释放深度学习的力量:由于激活函数,神经网络的每一层都可以学习不同的东西。将它们堆叠在一起,网络便有能力表示非常复杂的关系——从识别语音,到检测照片中的物体,再到理解语言。
下图展示了常见的激活函数。
图1.3:常见激活函数:ReLU、sigmoid和tanh( https://www.researchgate.net/figure/Sigmoid-tanh-and-ReLU-functions_fig4_358602222 )
我们已经了解了激活函数;现在,让我们看看深度学习的类别。
学习范式的类型
深度学习包含多种学习范式,每种范式适用于不同类型的问题:
-
监督学习:使用数据集训练模型,其中每个输入都与正确的输出配对。模型从这些示例中学习模式,以便为新输入预测适当的输出。常见应用包括图像分类和语音识别。
-
无监督学习:模型在未标记的数据中发现模式。聚类和降维是常见的应用。
-
半监督学习:结合了监督方法(使用少量标记数据)和无监督方法(使用大量未标记数据)。
-
强化学习(RL):模型通过与环境的反馈进行学习,环境根据其行为给予奖励或惩罚。
让我们更详细地了解每种深度学习类型。
监督学习
监督学习是深度学习和机器学习中最广泛使用的方法之一。它依赖于标记数据集,通常称为真实值(ground truth),其中每个输入都与正确的输出配对。通过反复将模型暴露于这些标记示例,算法学习将输入映射到所需输出,并将其知识泛化到新的、未见过的数据。
两种最常见的监督学习任务是分类和回归。对于分类任务,模型将预测一个离散标签。最常见的示例之一是判断一封电子邮件是否是垃圾邮件。另一个示例是预测0到9之间的手写数字。
下图对比了线性回归和逻辑回归——一个显示为简单的线性模型,另一个显示为单神经元(感知器)神经网络。
图1.4:左:使用神经网络进行数字分类。右:使用回归模型进行房价预测
相比之下,对于回归任务,模型将预测一个连续值。示例包括根据房屋特征预测房价、根据身高和年龄估算人的体重,或预测第二天的温度。与分类(输出属于固定类别集合)不同,回归输出可以取广泛的数值。
监督学习支撑着许多实际应用,包括图像识别、语音转文本系统、医疗诊断支持和金融预测。其有效性在很大程度上取决于标记数据的可用性和质量,以及模型在不过度拟合训练集的情况下捕获模式的能力。
无监督学习
无监督学习本质上是监督学习的对立面。在这种情况下,输入数据没有标签。你可能会问:“如果没有提供答案,模型到底在学习什么?”模型通过发现数据本身的模式、结构或分组来学习,而无需
第 1 章
而不是被告知什么是“正确”的输出。
例如,无监督学习可以根据购买行为将相似客户聚类在一起,即使我们事先不知道他们的类别。它还可以降低大型数据集的维度,突出最重要的特征。与专注于使用有标签结果进行预测的监督学习不同,无监督学习更关乎发现——揭示人类可能不易察觉的隐藏关系和洞见。
常见技术包括以下几种:
-
聚类(例如 k-means、层次聚类):将具有相似特征的数据点分组
-
降维(例如 PCA、t-SNE):在保留重要结构的同时简化数据
-
关联规则学习(例如购物篮分析):发现特征或事件如何共现的规则
下图展示了聚类的核心思想。左图是 k-means 演示,将数据点划分为多个组;右图是一个生物医学示例,聚类揭示了基因共表达关系。
图 1.5:左图:K-means 聚类。右图:无监督学习在生物医学用例中的应用( https://commons.wikimedia.org/wiki/File%3AHeatmap.png )
简而言之,监督学习使用标签回答特定问题,而无监督学习则让数据自己讲述故事,帮助我们探索未知。
接下来,我们来看看半监督学习和强化学习。
半监督学习与强化学习
半监督学习融合了监督学习和无监督学习的原理。监督学习完全依赖有标签数据,无监督学习则在无标签情况下工作,而半监督学习则兼取两者之长。少量有标签数据与大量无标签数据一起使用,使模型能够更有效地泛化,同时减少昂贵的标注成本。例如,在医学影像中,可能只有数量有限的扫描带有专家验证的诊断,但存在数千张无标签扫描图像。半监督方法——如伪标签自训练、一致性正则化或基于图的技术——可以利用这两种数据源,构建出比仅使用监督数据更强的模型。
相比之下,强化学习完全不涉及标签,而是关乎交互和决策。智能体通过与环境交互、采取行动并获得奖励或惩罚来学习。随着时间推移,它会不断优化其策略——即将状态映射到动作的策略——以最大化长期奖励。这种设置使强化学习非常适合序列决策问题,在序列决策中,每个选择都会影响未来的结果。现实世界的例子包括 AlphaGo 等游戏系统、学习导航的自主机器人,以及适应用户行为的推荐引擎。
总之,半监督学习和强化学习展示了深度学习如何超越经典的监督模型:前者利用有标签和无标签数据来提高效率,后者则通过试错和奖励来掌握复杂行为。
既然你已经掌握了深度学习的核心内容——核心思想、示例算法及其构建模块——让我们来探讨为什么 C++ 是实现和部署这些系统的理想选择。
使用 C++ 进行深度学习的优势
虽然 Python 等语言因其在深度学习中的易用性而广受欢迎,但 C++ 具有几个显著优势,使其成为高性能深度学习应用的绝佳选择:
-
性能优先:C++ 编译为本地机器码,避免了解释器开销,通常比脚本运行时提供更低的延迟和更高的吞吐量
-
内存管理:C++ 提供对内存分配和释放的细粒度控制,这对于优化大规模深度学习模型至关重要
-
硬件优化:C++ 允许进行底层硬件优化,使开发者能够充分利用 CPU 和 GPU 的能力
-
与现有系统集成:许多生产系统都是用 C++ 构建的,因此更容易将深度学习模型集成到现有基础设施中
-
可移植性:C++ 代码可以针对多种平台编译,确保在不同系统上具有一致的性能
在了解了 C++ 的这些优势之后,我相信你已经迫不及待想要直接开始写代码了!但在那之前,我们先来了解一下如何搭建一个合适的 C++ 深度学习环境。
搭建 C++ 深度学习环境
要开启你的 C++ 深度学习之旅,你需要搭建一个合适的开发环境。这个过程涉及几个步骤,我们将在接下来的章节中逐一介绍。
使用免费的 Google Colab
Google Colab 是一个很好的起点,它免费且提供了一个强大的编码平台,无需高端硬件即可使用。在 Colab 上搭建 C++ 环境需要安装必要的库,并配置环境以兼容深度学习框架。这种方法让你可以在云端测试和运行代码,从而更容易地试验模型和算法。Colab 对 GPU 和 TPU 的支持还可以加速你的工作,为探索 C++ 在深度学习中的应用提供了一种经济高效的解决方案。
开始使用 Google Colab 的步骤如下:
-
创建笔记本:前往 Google Colab,选择 新建笔记本 开始编码。
-
编写代码单元格:Colab 使用单元格来编写代码和 Markdown,允许用户同时编写代码和说明。
-
执行单元格:按 Shift + Enter 运行单元格,或使用每个单元格上的 运行 按钮。
-
设置运行时环境:在 运行时 | 更改运行时类型 下,你可以选择 CPU、GPU 或 TPU。
图 1.6:单击“更改运行时类型”选项
- 选择 GPU 选项,即可在 Colab 中使用 GPU。
图 1.7:在下一页中,选择你可用的 GPU 选项
- 在 Colab 中选定 GPU 后,编程环境就可以使用了。为了验证设置是否正确以及 CUDA 是否可用,请在 Colab 笔记本中打开一个新的代码单元格,输入以下命令:
!nvcc --version
!nvidia-smi
在 Colab 代码单元格中运行这些命令后,你应该会看到类似下面的输出:
图 1.8:使用 !nvcc --version 检查 CUDA 编译器版本
第一个命令(!nvcc --version)显示系统中安装的 NVIDIA CUDA 编译器驱动(nvcc)的版本。输出显示以下信息:
-
编译器驱动程序来自 NVIDIA,专为 CUDA 构建
-
构建日期为 2023 年 8 月 15 日
-
CUDA 编译工具为 12.2 发行版,具体版本为 12.2.140
下面是第二个命令的输出。你可以看到当前 Colab 中 GPU 的规格。
图 1.9:使用 !nvidia-smi 显示 NVIDIA 系统信息
第二个命令(!nvidia-smi)全称是 NVIDIA 系统管理接口,它提供系统中可用 NVIDIA GPU 的详细信息。输出包括以下内容:
-
驱动版本:535.104.05
-
CUDA 版本:12.2
-
GPU 信息:
-
型号:Tesla T4
-
温度:66°C
-
功耗:11 W,最大为 70 W
-
显存:总显存为 15,360 MiB,当前已使用 0 MiB
-
GPU 利用率:当前为 0%,表示 GPU 处于空闲状态
-
进程:当前没有活动进程正在使用 GPU
-
现在,你已经准备好通过一个 C++ 程序来了解深度学习的实际示例了。
让我们在 Colab 中编写一些 CUDA 代码并运行它:
%%writefile welcome_from_cuda.c
#include <stdio.h>
__global__ void welcome() {
printf("Welcome to DL with C++ — block %d, thread %d\n", blockIdx.x,
threadIdx.x);
}
int main() {
welcome<<<3,3>>>(); // 3 blocks, 3 threads each
cudaDeviceSynchronize();
return 0;
}
在单元格中运行以下命令:
%%bash
nvcc -std=c++14 -x cu -arch=sm_75 welcome_from_cuda.c -o welcome
第1章
让我们看一下代码的输出:
图 1.10:“welcome from”练习的输出,演示了如何使用 C++ 访问 GPU,并打印消息 “Welcome to DL with C++ from block x, thread y”
你已经看到,利用免费的在线资源可以轻松地为 C++ 深度学习项目编写代码。现在让我们看看如何在本地搭建环境。
本地搭建 C++ 开发环境
要在本地使用 C++ 构建和运行深度学习应用程序,你需要准备一个合适的环境。该设置包括四个主要步骤——安装编译器、选择 IDE、配置构建工具以及添加深度学习库:
-
安装 C++ 编译器:选择并安装适合你操作系统的 C++ 编译器:
-
Windows:MSVC 或 MinGW
-
macOS:Xcode 命令行工具(包含 Clang)
-
Linux:GNU 编译器套件(GCC)
-
-
设置 IDE:虽然并非严格必需,但 IDE 可以极大地提升你的生产力。常见的选择包括:
-
Visual Studio(Windows):微软出品的功能丰富的 IDE,与 MSVC 编译器紧密集成。它提供了优秀的调试工具、GUI 设计能力和项目模板。Visual Studio 还能轻松管理 NuGet 包和外部库。
-
CLion(跨平台):JetBrains 出品的商业 IDE,提供智能代码补全、重构以及与 CMake 的紧密集成。CLion 因其现代 UI 和高生产力特性而在 C++ 社区广受欢迎。
-
Eclipse CDT(跨平台):Eclipse 的 C++ 开发工具(CDT)是一款免费开源 IDE。虽然不如 CLion 精美,但健壮且高度可定制,非常适合偏好开源工具的用户。
-
Code::Blocks(跨平台):一款轻量级开源 IDE。虽然比 Visual Studio 或 CLion 简单,但设置快速便捷,是初学者的良好起点。
-
-
安装构建工具:除了编译器和 IDE 之外,你还需要 构建工具 来管理项目的编译和链接。对于小型
Hello World程序,运行一条g++ file.cpp -o output命令就足够了。但随着项目规模的增长——尤其是使用依赖多个外部组件的深度学习库时——手动编译变得不切实际。两个最重要的工具如下:-
CMake:
-
一个广泛使用的跨平台构建系统生成器。
-
无需编写复杂的编译器命令,你只需在一个简单的文本文件(
CMakeLists.txt)中描述你的项目。然后 CMake 会生成特定于平台的构建文件(例如,Windows 上的 Visual Studio 项目,Linux 上的 Makefile)。 -
它是许多深度学习框架(例如 PyTorch、TensorFlow)的标准构建系统。
-
以下是一个 CMake 文件示例:
cmake_minimum_required(VERSION 3.10) project(HelloWorld) set(CMAKE_CXX_STANDARD 14) add_executable(hello main.cpp) -
运行
cmake . && make将把main.cpp编译成可执行文件。
-
-
Make:
-
常用于 Linux 和 Unix 系统。
-
依靠 Makefile 来指定文件应如何编译和链接。
-
仍然被广泛使用,不过在较大的项目中,通常通过 CMake 来管理。
-
以下是一个 Makefile 示例:
all: g++ main.cpp -o hello
-
构建工具对深度学习项目至关重要,因为它们能自动化依赖管理(例如链接 CUDA、cuDNN、LibTorch 和 TensorFlow);简化多文件项目的编译;确保跨 Windows、macOS 和 Linux 的跨平台兼容性;并提供一致的配置,使团队协作更加高效。
-
-
安装深度学习库:有多种 C++ 库可用于深度学习。我们将重点关注其中最流行的一些:
-
PyTorch C++ API(LibTorch):PyTorch 的 C++ 前端 LibTorch 为 C++ 深度学习提供了一个强大而灵活的框架。要安装 LibTorch,请执行以下步骤:
-
从 PyTorch 网站下载预构建库(
https://pytorch.org/get-started/locally/)。图 1.11:来自 pytorch.org 的 PyTorch 设置快速参考
-
在终端中,从 PyTorch 网站复制并运行该命令:
% wget https://download.pytorch.org/libtorch/cpu/libtorch-macos-arm64-2.8.0.zip -
使用
unzip libtorch-macos-arm64-2.8.0.zip将下载的压缩包解压到系统上的某个位置。 -
使用适当的环境变量配置你的
compile命令,使其指向你的 LibTorch 安装位置(参见下面的 Bash 示例)。
在你的 C++ 项目中包含 LibTorch 的示例:
#include <torch/torch.h> #include <iostream> int main() { // Create a 3x3 tensor with random values between 0 and 1 torch::Tensor X = torch::rand({3, 3}); // Add a constant value to every element torch::Tensor Y = X + 5; // Apply ReLU activation torch::Tensor Z = torch::relu(Y); // Print all results std::cout << "Original tensor X:\n" << X << "\n\n"; std::cout << "After adding 5 (Y):\n" << Y << "\n\n"; std::cout << "After ReLU activation (Z):\n" << Z << std::endl; return 0; }运行以下 bash 命令:
% clang++ example.cpp -o example \ -I ./libtorch/include \ -I ./libtorch/include/torch/csrc/api/include \ -L ./libtorch/lib \ -ltorch -ltorch_cpu -lc10 \ -Wl,-rpath,./libtorch/lib \ -std=c++17输出应为:
% ./example Original tensor X: 0.4462 0.4752 0.4493 0.1307 0.3102 0.7832 0.4145 0.7671 0.9496 [ CPUFloatType{3,3} ] After adding 5 (Y): 5.4462 5.4752 5.4493 5.1307 5.3102 5.7832 5.4145 5.7671 5.9496 [ CPUFloatType{3,3} ] After ReLU activation (Z): 5.4462 5.4752 5.4493 5.1307 5.3102 5.7832 5.4145 5.7671 5.9496 [ CPUFloatType{3,3} ] -
-
开放神经网络交换(ONNX)运行时:ONNX 提供了一种在不同框架之间交换深度学习模型的方法。ONNX Runtime 是一个专注于性能的 ONNX 模型推理引擎。要安装 ONNX Runtime,请执行以下步骤:
-
从 ONNX Runtime 的 GitHub 发布页面(
https://github.com/microsoft/onnxruntime/)下载预构建二进制文件。 -
在你的项目中包含必要的头文件并链接该库。
在 C++ 中使用 ONNX Runtime 的示例:
#include <onnxruntime_cxx_api.h> int main() { Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test"); Ort::SessionOptions session_options; Ort::Session session(env, "path/to/model.onnx", session_options); // ... (model inference code) return 0; } -
-
TensorFlow C++ API:虽然 TensorFlow 主要用于 Python,但它也为高性能应用提供了 C++ API。要使用 TensorFlow 的 C++ API,请执行以下步骤:
-
从源代码构建 TensorFlow,或使用预构建库。
-
将你的项目与 TensorFlow C++ 库链接。
在 C++ 中使用 TensorFlow 的示例:
#include "tensorflow/cc/client/client_session.h" #include "tensorflow/cc/ops/standard_ops.h" int main() { tensorflow::Scope root = tensorflow::Scope::NewRootScope(); auto A = tensorflow::ops::Const(root, {{1, 2}, {3, 4}}); auto B = tensorflow::ops::Const(root, {{5, 6}, {7, 8}}); auto C = tensorflow::ops::MatMul(root, A, B); tensorflow::ClientSession session(root); std::vector<tensorflow::Tensor> outputs; TF_CHECK_OK(session.Run({C}, &outputs)); std::cout << outputs[0].matrix<int>() << std::endl; return 0; } -
-
工具链就绪后,让我们开始使用 C++。首先,我们将考察它的关键性能优势——精确的内存控制、CPU 级调优(SIMD、缓存感知布局和自定义分配器),以及基于 CUDA 的 GPU 加速。然后,我们将通过在 LibTorch 中实现一个小型前馈网络来验证这些想法,从而端到端地验证整个环境。
C++ 在深度学习中的关键性能优势
C++ 提供了一些与深度学习尤为相关的性能优势:
-
内存管理:C++ 允许精确控制内存的分配和释放,这在使用大型数据集和复杂模型时至关重要。这种控制能力让开发者能够优化内存使用并降低开销。
C++ 中自定义内存管理的示例:
class MyTensor { private: float* data;
第1章:使用C++进行深度学习简介与环境搭建
size_t size;
public:
MyTensor(size_t s) : size(s) {
data = new float[size];
}
~MyTensor() {
delete[] data;
}
// ... (other methods)
};
- CPU优化:C++允许进行底层优化,可显著提升CPU性能:
- SIMD指令:利用单指令多数据(SIMD)指令进行并行处理
- 缓存友好的数据结构:设计最大化缓存效率的数据结构
- 自定义内存分配器:为特定用例实现专用分配器
使用Intel Intrinsics的SIMD指令示例:
#include <immintrin.h>
#include <cstddef>
// Adds two input arrays and writes the result to the output array.
void addArraysAVX(const float* lhs, const float* rhs, float* out, std::size_t length) {
std::size_t i = 0;
const std::size_t step = 8; // AVX processes 8 float values at once
for (; i + step <= length; i += step) {
__m256 leftVec = _mm256_loadu_ps(lhs + i);
__m256 rightVec = _mm256_loadu_ps(rhs + i);
__m256 sumVec = _mm256_add_ps(leftVec, rightVec);
_mm256_storeu_ps(out + i, sumVec);
}
// Handle any remaining elements that do not fill a full AVX register.
for (; i < length; ++i) {
out[i] = lhs[i] + rhs[i];
}
}
- GPU加速:C++与GPU计算库(如用于NVIDIA GPU的CUDA)集成良好,可对深度学习计算进行大规模并行化。请在此处阅读示例:
https://www.adaface.com/blog/software-development-interview-questions/。
在C++中实现简单神经网络
为了演示深度学习概念在C++中的实际应用,让我们实现一个基本的前馈神经网络。这个LibTorch示例定义了一个微型的全连接MNIST风格网络,包含两个线性层(784→128→10)和中间的ReLU激活函数,将一批64个虚拟的28×28灰度“图像”展平,执行一次前向传播,应用log_softmax生成每类对数概率,并打印输出形状[64, 10]。这对于验证你的C++/LibTorch设置和模型连接非常有用(不涉及训练)。
以下C++代码片段来自mnist_fc.cpp,我们首先定义神经网络结构:
#include <torch/torch.h>
#include <iostream>
// Define the neural network structure
struct Net : torch::nn::Module {
Net() {
// Input layer to hidden layer
fc1 = register_module("fc1", torch::nn::Linear(784, 128));
// Hidden layer to output layer
fc2 = register_module("fc2", torch::nn::Linear(128, 10));
}
torch::Tensor forward(torch::Tensor x) {
x = torch::relu(fc1->forward(x.reshape({x.size(0), 784})));
x = fc2->forward(x);
return torch::log_softmax(x, /*dim=*/1);
}
torch::nn::Linear fc1{nullptr}, fc2{nullptr};
};
然后定义如何使用网络的主函数。
int main() {
// Create an instance of the network
auto net = std::make_shared<Net>();
// Create a dummy input tensor
auto input = torch::randn({64, 1, 28, 28});
// Forward pass
auto output = net->forward(input);
std::cout << "Output size: " << output.sizes() << std::endl;
return 0;
}
此示例演示了如何定义简单的神经网络结构、创建网络实例,并使用虚拟输入数据执行前向传播。
在bash中运行以下命令:
% clang++ mnist_fc.cpp -std=c++17 -O2 \
-I ./libtorch/include \
-I ./libtorch/include/torch/csrc/api/include \
-L ./libtorch/lib \
-ltorch -ltorch_cpu -lc10 \
-Wl,-rpath,$PWD/libtorch/lib \
-o mnist_fc
输出应为:
./mnist_fc
Output size: [64, 10]
Output size: [64, 10]表示网络处理了一批64张图像,并为每张图像生成了10个分类——每个类别(数字0–9)一个。由于模型以log_softmax结尾,每行包含对数概率(其指数化后的概率之和为1)。该形状确认了前向传播以及输入/层维度连接正确。
小结
本章向你介绍了深度学习的基本概念,并为设置用于深度学习任务的C++环境提供了全面指南。我们探讨了使用C++进行深度学习的优势,包括其性能优势和底层系统资源控制。通过理解这些核心原则并设置开发环境,你现在已准备好开始使用C++探索深度学习世界。随着不断深入,你将能够利用C++的强大功能来构建和优化复杂的深度学习模型,突破人工智能的可能性边界。
问题
在继续之前,这里有一些常见问题解答,以澄清示例中你会遇到的常见符号和构建/运行时问题——尤其是涉及LibTorch设置和平台差异的问题:
- 神经网络中输入层、隐藏层和输出层的作用分别是什么?
- 为什么ReLU、sigmoid和tanh等激活函数在深度学习中很重要?
- 监督学习和无监督学习有什么区别?
- 为什么C++是高性能深度学习应用的强有力选择?
- 在LibTorch神经网络示例中,输出大小
[64, 10]表示什么?
延伸阅读
如需深入研究和主要参考——尤其是当你需要确切的C++ API或平台细节时——请从以下资源开始:
- PyTorch C++ (LibTorch)教程:使用PyTorch C++前端 (
https://docs.pytorch.org/tutorials/advanced/cpp_frontend.html) - 适用于macOS GPU的PyTorch MPS (Metal)后端说明 (
https://docs.pytorch.org/docs/stable/notes/mps.html) - ONNX Runtime C/C++ API文档及C++入门指南 (
https://onnxruntime.ai/docs/api/) - TensorFlow C++ API参考和从源码构建指南 (
https://www.tensorflow.org/api_docs/cc)
答案
以下是FAQ的简明答案,包含macOS友好的编译/运行细节以及关于Apple硬件上GPU支持的说明:
- 输入层接收原始特征并将其传递到网络中。隐藏层通过应用学习到的权重、偏置和激活函数,将这些特征转换为更有用的内部表示。输出层将最终的隐藏表示转换为任务所需的预测,例如类别分数、概率或数值。
- 激活函数很重要,因为它们为网络引入非线性。如果没有它们,具有多层结构的神经网络将表现得像单一线性变换,这将严重限制网络的学习能力。
- 因此,关键区别在于:监督学习从有标签数据中学习,而无监督学习处理的是无标签数据。
- C++为开发者提供了对内存、执行和硬件交互的细粒度控制,这在性能关键型系统中非常重要。它与GPU库和推理运行时集成良好,开销极小,非常适合注重延迟、吞吐量和资源效率的生产环境。
- 形状
[64, 10]表示模型为一个包含64个示例的批次产生了输出,每个示例有10个输出值。在分类场景中,这10个值通常代表10个类别的分数或logits。
获取本书的PDF版本及更多内容
扫描二维码(或前往packtpub.com/unlock)。按名称搜索本书,确认版本,然后按照页面上的步骤操作。
https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/deep-learning-with-c/img/f45df257dfb11cd9b58ac2c5f6935210_59_0.png
https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/deep-learning-with-c/img/f45df257dfb11cd9b58ac2c5f6935210_59_1.png
注意:请准备好你的发票。直接从Packt网站购买则无需发票。
2 C++ 中的数据准备与预处理
数据准备是高效深度学习(DL)工作流的基石。输入数据的表示、结构和质量直接影响模型的学习效率、性能和泛化能力。现代数据通常是异构的,融合了数值、文本、图像和时间序列元素,因此需要能够处理多模态复杂性的预处理流水线。本章探讨 C++ 中的高级预处理技术,重点关注性能和生产级流水线,涵盖结构化和非结构化数据处理、缺失值填补、特征工程、数据增强以及与 PyTorch 的高效集成。
通过详细的代码示例和理论支撑,本章展示了如何设计可扩展的 C++ 预处理工作流,使其在各种数据集上都能高效运行。讨论还将强调预处理与硬件约束、可扩展性和分布式计算之间的相互作用,为生产环境中的高效数据流水线管理提供见解。此外,将特别强调多模态数据处理和领域特定变换,以最大化模型的鲁棒性和实用性。
本章将涵盖以下主题:
- 理解数据预处理在深度学习中的重要性
- 结构化与非结构化数据——综合技术与示例
- 探索归一化和数据增强技术
- 集成 PyTorch Dataset API 以构建数据流水线
- 管理大规模数据集
学完本章后,你将了解如何构建生产级的 C++ 预处理流程:类型安全的加载器(表格/文本/图像/时间序列);缺失值填补、缩放、分词和特征工程;确定性/即时数据增强;与 PyTorch 的紧密集成;基于 mmap/单指令多数据(SIMD)的高吞吐并行 I/O;面向分布式训练的分片、缓存和流式处理;以及用于可重现、健壮流水线的模式检查、测试、性能分析和版本控制。
技术要求
本节详细说明遵循本章内容所需的先决条件。由于重点在于 C++ 中的数据准备与预处理,你应该有一个可用的 C++ 开发环境,并且能够访问示例中使用的库。
建议使用兼容 C++17 或更高版本的编译器。在 Linux 上,这意味着 GCC 11+ 或 Clang 14+。在 Windows 上,建议使用搭配 Visual Studio 2022 的 MSVC。在 macOS 上,Apple Clang 14+ 或更高版本应该可以正常工作。由于许多示例涉及多文件程序和外部依赖,还建议使用 CMake 3.22 或更高版本作为主要构建系统。
本章设计为可在 Linux、Windows 和 macOS 上运行。大多数示例都是基于 CPU 的,因此不需要 GPU。至少 8 GB 内存的机器足以进行较小的实验,而建议使用 16 GB 或更高内存,以便更顺畅地处理更大的数据集、图像流水线和外部库。
本章使用了多个库。LibTorch 用于基于张量的预处理、Dataset API 示例和基于自编码器的降维。Eigen 3.4+ 和 Armadillo 用于矩阵运算、缩放、嵌入和特征变换。mlpack 用于缺失值填补、PCA 和 t-SNE 等任务。OpenCV 4.x 用于图像预处理示例,包括调整大小、裁剪、直方图均衡化和边缘检测。
某些部分还依赖可选库。ONNX Runtime 可用于运行预训练的自然语言处理(NLP)模型,例如用于嵌入、命名实体识别和情感分析的 BERT。SentencePiece 可用于子词分词,而 libstemmer 支持词干提取示例。对于音频预处理和语谱图生成,建议使用 FFTW。
本章的实用最小环境配置包括 C++17 编译器、CMake、LibTorch、Eigen、Armadillo、mlpack 和 OpenCV。如果你想同时运行 NLP 和音频示例,还应额外安装 ONNX Runtime、SentencePiece、libstemmer 和 FFTW。
在整个章节中,我们会指出某个库何时是可选的,以及何时可以使用更简单的仅 CPU 路径。
理解数据预处理在深度学习中的重要性
数据预处理在深度学习中的重要性如下图所示:假设我们想根据两个特征预测房价;即使不考虑缺失值和噪声问题——也就是说,即使数据集完全“干净”——我们仍然必须进行预处理。因为面积特征的取值范围很大,从 0 到 3500,而卧室特征的范围仅在 0 到 5 之间,这导致模型的代价/损失曲面呈现病态,梯度下降呈之字形震荡,模型难以收敛。对每个特征进行归一化(例如,将 min-max 归一化到 [0,1])的预处理可以重塑这些等高线,使其接近圆形,从而实现更大、更稳定的步长,带来更快、更可靠的收敛。
图 2.1:说明数据预处理重要性的示意图
在设计模型之前,我们需要驯服数据——从 图 2.1 中强调的尺度不匹配开始,再到通常需要排查的问题:缺失值、异常值、不一致的编码、偏斜分布和潜在的数据泄漏。弥合这一差距正是预处理的工作:通过有条理的步骤,将杂乱无章的原始输入转换成尺度可比、表现良好的张量。带着这个目标,让我们看看预处理的作用以及它如何为深度学习准备数据。
预处理的作用
如前所述,原始数据很少能直接用于模型——通常是不完整、有噪声且不一致的。预处理将其转换为结构化、归一化、有意义的表示,以针对深度学习进行优化。
预处理的好处包括以下几点:
- 更快的收敛,因为归一化输入减少了训练早期的梯度不稳定性
- 降噪,通过去除无关信息和有影响力的异常值
- 更高的效率,通过紧凑的表示减少内存占用和计算成本
- 更强的泛化能力,当数据增强被保守地应用以保持标签语义时
- 多模态支持,能够在文本、表格、图像和信号数据之间实现共享变换
- 领域特定适配,流水线针对医疗保健、金融和计算机视觉(CV)工作负载进行调优
然而,预处理并非没有挑战,包括以下内容:
- 类别不平衡
- 异构数据的跨特征归一化
- 大数据集上的内存/计算限制
- 跨分布式系统的扩展
- 满足实时/近实时要求
让我们首先看看数据的两大类:结构化数据和非结构化数据。
结构化与非结构化数据——综合技术与示例
真实世界的数据集分为两大类:结构化数据(具有显式模式的表格/时间序列)和非结构化数据(图像、文本、音频、视频)。虽然两者都需要清洗和归一化,但目标不同——结构化数据强调模式验证、缺失值填补、编码和缩放;非结构化数据则侧重于解码、分词、调整大小/裁剪和数据增强。本节对比这些工作流,并提供简洁、地道的 C++ 模式(LibTorch、Dataset/DataLoader、OpenCV、SentencePiece),可直接用于生产环境。
第 2 章
结构化数据
结构化数据遵循固定的模式,例如关系数据库或 CSV 文件。示例包括数值测量、类别标签和时间序列。结构化数据通常需要理解其统计属性和领域特定的细微之处,才能实现最优预处理。
结构化数据的一些全面预处理技术如下:
- 处理缺失值:均值替换、前向填充、后向填充、k 近邻(k-NN)插补以及基于回归的插补等技术
- 编码类别特征:独热编码、频率编码、序数编码、二进制编码,以及为神经网络(NN)输入层定制的嵌入
- 特征缩放与标准化:方法包括最小-最大缩放、Z-score 标准化、鲁棒缩放、对数变换和幂变换
- 降维:主成分分析(PCA)、t-分布随机邻域嵌入(t-SNE)和自编码器(AEs),用于减少冗余或噪声特征
- 时间序列工程:滚动窗口聚合、指数平滑、差分、傅里叶变换和基于时间的特征提取等技术
- 特征交互工程:多项式特征生成和交互项计算,以增强模型表达能力
详细示例
下面来看上一节中提到的预处理技术的详细示例。
处理缺失值
处理缺失值有多种方法:
- 均值替换:用特征的列均值替换每个缺失值。实现简单、速度快(单次遍历),并能保持特征的尺度不变——适合用作快速基线或在缺失较少时使用。然而,它会扭曲分布、缩小方差并减弱相关性,这可能会使模型产生偏差——尤其是当数据并非完全随机缺失时。
mean_substitution_example.cpp:
#include <vector>
#include <numeric>
// Replace missing values (NaN) with the mean of the column
std::vector<double> meanSubstitution(const std::vector<double>& data) {
double sum = 0;
int count = 0;
for (const auto& val : data) {
if (!std::isnan(val)) {
sum += val;
count++;
}
}
double mean = sum / count;
std::vector<double> filledData = data;
for (auto& val : filledData) {
if (std::isnan(val)) {
val = mean;
}
}
return filledData;
}
- 前向填充:将最后一个观测值向前传递,以填充时间序列中后续的缺口。它速度快,能保持短期水平的连续性,并且对有状态信号(例如设备模式)效果良好,但它可能会在较长缺口上传播过时或异常值,并抹平真实的动态变化。
forward_fill_example.cpp:
以下代码实现了前向填充插补,将每个 NaN 替换为其左侧最近的非缺失值(注意:开头的 NaN 保持不变):
#include <vector>
#include <cmath>
// Replace missing values with the last non-missing value
std::vector<double> forwardFill(const std::vector<double>& data) {
std::vector<double> filledData = data;
for (size_t i = 1; i < filledData.size(); ++i) {
if (std::isnan(filledData[i])) {
filledData[i] = filledData[i - 1];
}
}
return filledData;
}
- 后向填充:用下一个观测值填充每个缺失点,实质上是沿时间向后传播信息。与前向填充插补一样,它简单高效,但如果用在训练流程中,可能会泄漏未来信息,并且可能过度平滑长缺口。
backward_fill_example.cpp:
#include <vector>
#include <cmath>
// Replace missing values with the next non-missing value
std::vector<double> backwardFill(const std::vector<double>& data) {
std::vector<double> filledData = data;
for (size_t i = filledData.size() - 1; i > 0; --i) {
if (std::isnan(filledData[i - 1])) {
filledData[i - 1] = filledData[i];
}
}
return filledData;
}
- k-NN 插补:通过在两行均观测到的特征子集上计算距离,找到 k 个最相似的行来填充缺失值,然后聚合这些邻居(数值型用均值/中位数,类别型用众数;通常按距离加权)。它要求进行特征缩放并仔细选择距离度量(例如欧氏距离或马氏距离);性能取决于 k,并且在高维空间中可能会下降。
knn_imputation_example.cpp:
int main() {
using std::numeric_limits; using std::cout; using std::endl;
double NaN = numeric_limits<double>::quiet_NaN();
std::vector<std::vector<double>> data = {
{1.00, 2.00, 3.00},
{1.10, NaN, 3.20}, // <- target row to impute
{0.90, 2.10, 2.90},
{1.20, 1.90, 3.10}
};
int targetIndex = 1;
int k = 2;
auto print_row = [&](const char* label, const std::vector<double>& r){
cout << label << " [";
for (size_t j = 0; j < r.size(); ++j) {
if (std::isnan(r[j])) cout << "NaN";
else cout << r[j];
if (j + 1 < r.size()) cout << ", ";
}
cout << "]" << endl;
};
print_row("Before:", data[targetIndex]);
auto imputed = knnImputation(data, k, targetIndex);
print_row("After: ", imputed);
return 0;
}
- 基于回归的插补:通过在该特征存在的行上训练模型,然后将其应用于缺口,从而从其他观测特征预测该特征的缺失值。模型可以是线性的(岭回归/Lasso)或非线性的(随机森林/梯度提升机(GBMs)),并且应在每个训练折内部(或通过交叉拟合)进行拟合,以避免数据泄漏。
regression_imputation_example.cpp:
#include <vector>
#include <cmath>
// Replace missing values using linear regression coefficients
std::vector<double> regressionImputation(const std::vector<double>& target, const std::vector<double>& predictor, double slope, double intercept) {
std::vector<double> filledData = target;
for (size_t i = 0; i < filledData.size(); ++i) {
if (std::isnan(filledData[i])) {
filledData[i] = slope * predictor[i] + intercept;
}
}
return filledData;
}
编码类别特征
下面来看编码类别特征的技术:
- 独热编码:将具有 N 个类别的类别特征转换为 N 个二进制列(当前类别为 1,否则为 0),避免虚假的序数关系,并在线性/NN 模型中表现良好。注意高基数特征可能导致维度爆炸——可以使用
<UNK>桶、哈希或正则化来缓解。 - 当类别特征的所有独热指示变量都被保留时,它们按构造必然是线性相关的,因为每个样本恰好只有一个指示变量为激活状态。对于线性模型,可以通过省略一个类别作为参考水平,或对系数施加显式约束(例如零和约束)来消除这种共线性。这些调整恢复了可辨识性,主要影响系数的解释而非预测性能,并且对于正则化模型或基于树的模型通常不是必需的。
one_hot_example.cpp:
#include <vector>
#include <map>
#include <string>
第二章:C++ 中的数据准备与预处理
// Convert categorical variables into binary vectors
std::vector<std::vector<int>> oneHotEncode(const std::vector<std::string>& categories) {
std::map<std::string, int> categoryMap;
int index = 0;
for (const auto& cat : categories) {
if (categoryMap.find(cat) == categoryMap.end()) {
categoryMap[cat] = index++;
}
}
std::vector<std::vector<int>> encoded(categories.size(), std::vector<int>(categoryMap.size(), 0));
for (size_t i = 0; i < categories.size(); ++i) {
encoded[i][categoryMap[categories[i]]] = 1;
}
return encoded;
}
- 频数编码:将每个类别映射为它在训练数据中出现的次数或相对频率,生成一个单一的数值特征,既保留了类别流行度,又不会导致维度爆炸。在每个训练折内分别计算频数以避免数据泄漏,并可考虑使用对数频数或截断来处理长尾类别。
代码示例 frequency_encode_example.cpp:
#include <vector>
#include <map>
#include <string>
// Encode categories based on their frequency
std::vector<int> frequencyEncode(const std::vector<std::string>& categories) {
std::map<std::string, int> freqMap;
for (const auto& cat : categories) {
freqMap[cat]++;
}
std::vector<int> encoded;
for (const auto& cat : categories) {
encoded.push_back(freqMap[cat]);
}
return encoded;
}
- 序数编码:将类别映射为体现有意义顺序的整数(例如 XS < S < M < L < XL),生成一个保留排序关系的单一数值特征。仅当顺序确实存在时才可使用,否则会引入虚假的序数关系;在训练集上拟合映射,并将未见过的类别路由到
<UNK>编码。
代码示例:ordinal_encode_example.cpp
#include <vector>
#include <map>
#include <string>
// Assign an ordinal value to each category
std::vector<int> ordinalEncode(const std::vector<std::string>& categories) {
std::map<std::string, int> categoryMap;
int index = 0;
for (const auto& cat : categories) {
if (categoryMap.find(cat) == categoryMap.end()) {
categoryMap[cat] = index++;
}
}
std::vector<int> encoded;
for (const auto& cat : categories) {
encoded.push_back(categoryMap[cat]);
}
return encoded;
}
- 二进制编码:先将每个类别映射为一个整数 ID,再将该 ID 表示为二进制的 ⌈log₂N⌉ 位,从而大幅降低维度。注意事项:它会引入伪序数结构和虚假的邻近性;应在训练集上拟合 ID,并为未见过的类别保留一个
<UNK>标记。
代码示例:binary_encoding_demo.cpp
#include <vector>
#include <string>
#include <bitset>
// Convert categories to binary-encoded values
std::vector<std::string> binaryEncode(const std::vector<int>& categories) {
std::vector<std::string> encoded;
for (const auto& cat : categories) {
encoded.push_back(std::bitset<8>(cat).to_string());
}
return encoded;
}
- 嵌入编码:将每个类别表示为一个与模型联合训练得到的稠密向量,既能捕捉类别之间的相似性,又避免了独热编码的维度爆炸。请为未见过的类别使用
<UNK>/OOV标记,并保持适当的嵌入维度,配合正则化以避免过拟合。
下面的代码实现了一个简易的类别嵌入编码器:它将每个唯一的字符串类别映射为索引,构建一个(num_categories × embeddingSize)的嵌入表,表中填充确定性的正弦值(sin(i + j)),并为每个输入类别返回对应的嵌入向量:
#include <vector>
#include <map>
#include <string>
#include <cmath>
// Dummy embedding logic
std::vector<std::vector<double>> embeddingEncode(const std::vector<std::string>& categories, int embeddingSize) {
std::map<std::string, int> categoryMap;
int index = 0;
for (const auto& cat : categories) {
if (categoryMap.find(cat) == categoryMap.end()) {
categoryMap[cat] = index++;
}
}
std::vector<std::vector<double>> embeddings(categoryMap.size(), std::vector<double>(embeddingSize, 0));
for (size_t i = 0; i < embeddings.size(); ++i) {
for (size_t j = 0; j < embeddingSize; ++j) {
embeddings[i][j] = std::sin(i + j); // Example: sinusoidal encoding
}
}
std::vector<std::vector<double>> encoded;
for (const auto& cat : categories) {
encoded.push_back(embeddings[categoryMap[cat]]);
}
return encoded;
}
特征缩放与标准化
特征缩放与标准化的常用技术如下:
- 最小-最大缩放:通过 (x-最小值)/(最大值-最小值) 将特征线性映射到固定区间(通常是
[0,1]),保留相对距离。它简单快速,但对异常值敏感——应在训练集上估计取值范围,并考虑使用截断或稳健变体。
代码示例:minmax_scale_demo.cpp
#include <vector>
#include <algorithm>
// Scale features to a specific range [minRange, maxRange]
std::vector<double> minMaxScale(const std::vector<double>& data, double minRange, double maxRange) {
double minVal = *std::min_element(data.begin(), data.end());
double maxVal = *std::max_element(data.begin(), data.end());
std::vector<double> scaledData;
for (const auto& val : data) {
double scaled = minRange + ((val - minVal) / (maxVal - minVal)) * (maxRange - minRange);
scaledData.push_back(scaled);
}
return scaledData;
}
- Z-score 标准化:使用 (x-均值)/标准差 将特征标准化为零均值和单位方差(其中均值和标准差在训练集上计算),使特征具有可比性并稳定优化过程。它适用于近似高斯分布的数据,但对异常值敏感;若存在重尾分布,可考虑稳健缩放。
代码示例:zscore_normalize_demo.cpp
#include <vector>
#include <cmath>
#include <numeric>
// Standardize features to have a mean of 0 and standard deviation of 1
std::vector<double> zScoreNormalize(const std::vector<double>& data) {
double mean = std::accumulate(data.begin(), data.end(), 0.0) / data.size();
double variance = 0.0;
for (const auto& val : data) {
variance += std::pow(val - mean, 2);
}
variance /= data.size();
double stddev = std::sqrt(variance);
std::vector<double> normalizedData;
for (const auto& val : data) {
normalizedData.push_back((val - mean) / stddev);
}
return normalizedData;
}
- 稳健缩放:以中位数为中心,并按四分位距(IQR = Q3-Q1)进行缩放:即 (x-中位数)/IQR,使其对异常值和重尾分布具有鲁棒性。应在训练集上计算统计量;当 IQR 接近零时,使用截断或回退策略加以防范。
代码示例:scaling.robust_scale_demo.cpp
#include <vector>
#include <algorithm>
// Scale features using the median and interquartile range (IQR)
std::vector<double> robustScale(const std::vector<double>& data) {
std::vector<double> sortedData = data;
std::sort(sortedData.begin(), sortedData.end());
double median = sortedData[sortedData.size() / 2];
double q1 = sortedData[sortedData.size() / 4];
double q3 = sortedData[3 * sortedData.size() / 4];
double iqr = q3 - q1;
std::vector<double> scaledData;
for (const auto& val : data) {
scaledData.push_back((val - median) / iqr);
}
return scaledData;
}
- 对数变换:压缩较大数值并降低右偏,通常能稳定方差并改善线性模型的拟合效果。使用
log1p(x)来处理零值;对于负数或混合符号数据,可考虑使用 Yeo-Johnson 变换;同时务必在训练集上计算参数,以避免数据泄漏。
代码示例:log_transform_demo.cpp
#include <vector>
#include <cmath>
// Apply a log transformation to reduce skewness in data
std::vector<double> logTransform(const std::vector<double>& data) {
std::vector<double> transformedData;
for (const auto& val : data) {
transformedData.push_back(std::log(val));
}
return transformedData;
}
transformedData.push_back(std::log1p(val)); // log(1 + x) to handle zero values
}
return transformedData;
}
-
幂变换:应用单调幂函数(例如,Box-Cox 用于 x>0,Yeo-Johnson 用于 x∈R)来稳定方差并使特征更接近高斯分布,从而改善线性模型的假设和优化。通过训练集上的最大似然估计(MLE)/交叉验证选择 λ;当存在零或负值时使用 Yeo-Johnson。
-
power_transform_demo.cpp:
#include <vector>
#include <cmath>
// Apply a power transformation to stabilize variance
std::vector<double> powerTransform(const std::vector<double>& data, double power) {
std::vector<double> transformedData;
for (const auto& val : data) {
transformedData.push_back(std::pow(val, power));
}
return transformedData;
}
降维
降维技术包括以下内容:
-
PCA:将数据投影到最大方差的正交方向上,压缩特征、减少噪声并使输入去相关。仅拟合训练集;先标准化,然后根据解释方差(例如95%)或交叉验证选择分量。(请先用Eigen安装。)
-
pca_demo.cpp:
#include <vector>
#include <Eigen/Dense>
// Perform PCA to reduce data dimensions
Eigen::MatrixXd performPCA(const Eigen::MatrixXd& data, int numComponents) {
Eigen::MatrixXd mean = data.colwise().mean();
Eigen::MatrixXd centered = data.rowwise() - mean;
Eigen::MatrixXd cov = (centered.adjoint() * centered) / double(data.rows() - 1);
Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> solver(cov);
Eigen::MatrixXd eigenvectors = solver.eigenvectors().rightCols(numComponents);
return centered * eigenvectors;
}
-
t-SNE:一种非线性技术,将高维数据映射到2D/3D以进行可视化,保留局部邻域但不保留全局距离(簇间间隙没有意义)。它对困惑度/学习率/种子敏感,并且不是自然的归纳式方法——如果需要嵌入新点,请使用参数化t-SNE或均匀流形近似与投影(UMAP)。
-
tsne_demo.cpp:
#include <vector>
#include "TSNE.h" // External library required
// Use t-SNE for visualizing high-dimensional data
std::vector<std::vector<double>> performTSNE(const std::vector<std::vector<double>>& data, int dimensions = 2) {
TSNE tsne;
std::vector<std::vector<double>> reducedData;
tsne.run(data, reducedData, dimensions);
return reducedData;
}
-
AEs:通过低维瓶颈重构输入来学习非线性降维,产生紧凑、任务有用的嵌入。诸如去噪和变分自编码器(VAE)等变体可提高鲁棒性或施加概率结构;标准化输入并仅在训练集上拟合以避免泄漏。
-
autoencoder_demo.cpp:
struct AE : torch::nn::Module {
torch::nn::Linear enc{nullptr}, dec{nullptr};
AE(int d, int h){ enc=register_module("enc", torch::nn::Linear(d,h));
dec=register_module("dec", torch::nn::Linear(h,d)); }
torch::Tensor forward(torch::Tensor x){ return dec(torch::relu(enc(x))); }
};
时间序列工程
现在,让我们继续介绍时间序列工程技术:
-
滚动窗口聚合:在滑动时间窗口上计算特征(例如,均值、总和、标准差、最小/最大值、指数移动平均(EMA))以捕获局部水平和波动性。仔细选择滞后窗口与居中窗口,处理边缘效应/缺失数据,并避免训练中的前视泄漏。
-
rolling_mean_demo.cpp:
#include <vector>
// Compute rolling averages over a fixed window size
std::vector<double> rollingMean(const std::vector<double>& data, int windowSize) {
std::vector<double> result(data.size(), 0.0);
for (size_t i = 0; i <= data.size() - windowSize; ++i) {
double sum = 0;
for (int j = 0; j < windowSize; ++j) {
sum += data[i + j];
}
result[i + windowSize - 1] = sum / windowSize;
}
return result;
}
-
指数平滑:通过对过去观测值应用几何衰减权重来生成平滑水平(以及短期预测):,0<α<1。诸如Holt(趋势)和Holt-Winters(趋势+季节性)等扩展可处理更丰富的时间序列动态;在训练集上调整参数以避免泄漏。
-
exponential_smoothing_demo.cpp:
#include <vector>
// Apply exponential smoothing
std::vector<double> exponentialSmoothing(const std::vector<double>& data, double alpha) {
std::vector<double> smoothed(data.size(), 0.0);
smoothed[0] = data[0];
for (size_t i = 1; i < data.size(); ++i) {
smoothed[i] = alpha * data[i] + (1 - alpha) * smoothed[i - 1];
}
return smoothed;
}
-
差分:将序列转换为变化而非水平——例如,一阶差分(或季节性差分)——以去除趋势/季节性并促进平稳性。使用所需的最小阶数(过度差分会放大噪声),因果计算以避免泄漏,并记得通过累积求和进行反转以用于预测。
-
differencing_demo.cpp:
#include <vector>
// Perform first-order differencing to remove trends
std::vector<double> differencing(const std::vector<double>& data) {
std::vector<double> differencedData;
for (size_t i = 1; i < data.size(); ++i) {
differencedData.push_back(data[i] - data[i - 1]);
}
return differencedData;
}
-
傅里叶变换:将信号分解为正弦分量,暴露主频率/周期和频谱功率,用于特征或季节性建模(通过快速傅里叶变换(FFT))。在训练窗口上应用,并进行去趋势/加窗以减少频谱泄漏,同时因果计算以避免前视。
-
fft_demo.cpp:
#include <vector>
#include <complex>
// Apply Fourier Transformation
std::vector<std::complex<double>> performFFT(const std::vector<double>& data) {
size_t n = data.size();
std::vector<std::complex<double>> transformed(n);
for (size_t k = 0; k < n; ++k) {
std::complex<double> sum(0.0, 0.0);
for (size_t t = 0; t < n; ++t) {
double angle = 2 * M_PI * t * k / n;
sum += std::polar(data[t], -angle);
}
transformed[k] = sum;
}
return transformed;
}
-
基于时间的特征提取:直接从时间戳中提取基于日历和时钟的预测因子,例如一天中的小时、一周中的星期几、月份、季度、周末标志和节假日指示器。当季节性、商业周期或人类行为影响目标变量时,这些特征尤其有用。诸如一天中的小时或一年中的月份等周期性变量通常使用正弦和余弦变换进行编码,以保留其周期性。与其他时间序列预处理步骤一样,特征应以因果方式生成,以避免来自未来时间戳的泄漏。
-
time_feature_demo.cpp:#include <cmath> struct TimeFeatures { int hour; int dayOfWeek; int month; double hourSin; double hourCos; }; TimeFeatures extractTimeFeatures(int hour, int dayOfWeek, int month) { TimeFeatures f; f.hour = hour; f.dayOfWeek = dayOfWeek; f.month = month; f.hourSin = std::sin(2.0 * M_PI * hour / 24.0); f.hourCos = std::cos(2.0 * M_PI * hour / 24.0); return f; }
因此,序列变为:
- 滚动窗口聚合
- 指数平滑
- 差分
- 傅里叶变换
- 基于时间的特征提取
特征交互工程
以下是特征交互工程技术:
-
多项式特征生成:通过幂次和交互项扩展输入(例如,),使线性模型能够捕捉曲线和交叉效应。使用适度的阶数,先进行标准化,并应用正则化(岭回归/Lasso)来抑制多重共线性和特征爆炸。
-
polynomial_features_demo.cpp:#include <vector> // Generate polynomial features of degree 2 std::vector<std::vector<double>> generatePolynomialFeatures(const std::vector<double>& data) { std::vector<std::vector<double>> polynomialFeatures; for (const auto& val : data) { polynomialFeatures.push_back({val, val * val}); } return polynomialFeatures; } -
交互项:将特征相乘或组合(例如,或),以捕捉纯线性项遗漏的非加法效应。结合领域知识谨慎添加,先进行标准化/中心化以限制多重共线性,并使用正则化或特征选择来防止过拟合。
-
interaction_terms_demo.cpp:#include <vector> // Compute interaction terms between features std::vector<std::vector<double>> computeInteractionTerms(const std::vector<std::vector<double>>& features) { std::vector<std::vector<double>> interactionTerms(features.size()); for (size_t i = 0; i < features.size(); ++i) { for (size_t j = 0; j < features[i].size(); ++j) { for (size_t k = j + 1; k < features[i].size(); ++k) { interactionTerms[i].push_back(features[i][j] * features[i][k]); } } } return interactionTerms; }
本节阐述了是什么——缺失值填充、编码、缩放、降维、时间序列特征和交互——以便你理解目标和陷阱(数据泄漏、稳定性、队列效应)。接下来,让我们用 C++ 库来解决怎么做:同样的技术通过 Eigen/Armadillo、mlpack/oneDAL 和 FFT 后端(可选 GPU 上的 cuBLAS/cuFFT/cuML)实现,以获得简洁的代码、更好的数值稳定性和生产级速度。
库驱动的预处理(相同技术,更少的代码)
本节重新审视了你之前看到的相同预处理技术(缺失值填充、编码、缩放、降维、时间序列操作),但使用成熟支持的 C++ 库来实现。数学原理和意图没有改变;库主要为你提供简洁的代码、速度(SIMD/GPU)和经过实战检验的数值算法。在相关之处,我们会指出库特有的变体(例如,基于奇异值分解(SVD)与基于特征值分解(EVD)的 PCA、FFT 后端)以及任何 API 细节,但从概念上讲,这些仍然是相同的预处理。
我们还可以使用现有的库以少得多的代码完成相同的预处理:
- dlib:机器学习和数据处理工具
- Eigen:用于变换和嵌入的线性代数库
- mlpack:可扩展的机器学习与预处理
- Armadillo:用于数据处理的线性代数库
- 开放神经网络交换(ONNX)运行时:用于降维和嵌入的预训练模型
- OpenCV:图像和数据操作(例如,用于傅里叶变换)。
- libtorch:面向 C++ 的 PyTorch 绑定
使用 mlpack 处理缺失值
让我们看看如何使用 mlpack 处理缺失值(为此,你需要安装 mlpack 和 Armadillo)。这里,我们通过 mlpack 加载一个 Armadillo 矩阵,然后填充列均值并进行前向填充——这与之前的技术相同,但使用 Armadillo 操作完成。这种方法可靠、简洁且快速。
-
mlpack_impute_demo.cpp:#include <mlpack/core.hpp> #include <mlpack/methods/imputation/map.hpp> arma::mat data; mlpack::data::Load("data.csv", data); // Load data into Armadillo matrix // Mean Substitution mlpack::data::MeanImputation<double> meanImputer; meanImputer.Impute(data, 0, arma::datum::nan, 0); // Forward Fill mlpack::data::MapImputation<double> forwardImputer( [](double prev, double curr) { return std::isnan(curr) ? prev : curr; }); forwardImputer.Impute(data, 0, arma::datum::nan, 0); // Save processed data mlpack::data::Save("processed_data.csv", data);
使用 mlpack 和标准模板库(STL)编码分类特征
你可以使用 mlpack 和 STL 执行分类特征编码,如下所示:
-
mlpack_encoding_demo_min.cpp:#include <mlpack/core.hpp> #include <unordered_map> #include <string> #include <vector> // One-Hot Encoding std::unordered_map<std::string, int> categoryToIndex; int index = 0; for (const auto& category : categories) { if (categoryToIndex.find(category) == categoryToIndex.end()) { categoryToIndex[category] = index++; } } arma::mat oneHotMatrix(categories.size(), categoryToIndex.size(), arma::fill::zeros); for (size_t i = 0; i < categories.size(); ++i) { oneHotMatrix(i, categoryToIndex[categories[i]]) = 1; } // Frequency Encoding std::unordered_map<std::string, int> frequency; for (const auto& category : categories) { frequency[category]++; } arma::vec frequencyEncoded(categories.size()); for (size_t i = 0; i < categories.size(); ++i) { frequencyEncoded(i) = frequency[categories[i]]; }
使用 Armadillo 进行特征缩放与标准化
使用 Armadillo,我们可以执行特征缩放和标准化,如下所示:
-
armadillo_scaling_demo.cpp:#include <armadillo> // Min-Max Scaling arma::mat minMaxScaled = (data - arma::min(data)) / (arma::max(data) - arma::min(data)); // Z-Score Normalization arma::mat zScoreNormalized = (data - arma::mean(data)) / arma::stddev(data); // Robust Scaling arma::mat q1 = arma::quantile(data, 0.25); arma::mat q3 = arma::quantile(data, 0.75); arma::mat robustScaled = (data - arma::median(data)) / (q3 - q1); // Log Transformation arma::mat logTransformed = arma::log(1 + data); // Power Transformation arma::mat powerTransformed = arma::pow(data, 0.5);
降维
降维可以按如下方式进行:
-
使用 mlpack:
-
pca_mlpack4_demo.cpp:#include <mlpack/methods/pca/pca.hpp> #include <mlpack/methods/tsne/tsne.hpp> // PCA mlpack::pca::PCA<> pca; arma::mat reducedData; pca.Apply(data, reducedData, 2); // Reduce to 2 components // t-SNE mlpack::tsne::TSNE<> tsne; arma::mat tsneResult; tsne.Apply(data, tsneResult, 2); // Reduce to 2 dimensions
-
-
使用 libtorch 进行自编码器(AE):
-
autoencoder_libtorch_demo.cpp:#include <torch/torch.h> // Define autoencoder struct Autoencoder : torch::nn::Module { torch::nn::Linear encoder{nullptr}, decoder{nullptr}; Autoencoder(int inputDim, int hiddenDim) { encoder = register_module("encoder", torch::nn::Linear(inputDim, hiddenDim)); decoder = register_module("decoder", torch::nn::Linear(hiddenDim, inputDim)); } torch::Tensor forward(torch::Tensor x) { x = torch::relu(encoder->forward(x)); return decoder->forward(x); } };
-
}
};
torch::Tensor autoencode(torch::Tensor data, int hiddenDim) {
int inputDim = data.size(1);
Autoencoder model(inputDim, hiddenDim);
auto optimizer = torch::optim::Adam(model.parameters(), 0.01);
for (int epoch = 0; epoch < 100; ++epoch) {
optimizer.zero_grad();
auto output = model.forward(data);
auto loss = torch::mse_loss(output, data);
loss.backward();
optimizer.step();
}
return model.encoder->forward(data);
}
使用 mlpack 和 OpenCV 进行时间序列工程
我们可以使用 mlpack 和 OpenCV 按如下方式执行时间序列工程:
ts_preproc_armadillo_opencv_demo.cpp:
#include <mlpack/core.hpp>
#include <opencv2/opencv.hpp>
// Rolling Window Aggregation
arma::mat rollingMean = arma::conv(data, arma::ones(5) / 5, "same");
// Exponential Smoothing
arma::mat smoothed(data.n_rows, data.n_cols);
smoothed.row(0) = data.row(0);
for (size_t i = 1; i < data.n_rows; ++i) {
smoothed.row(i) = 0.3 * data.row(i) + 0.7 * smoothed.row(i - 1);
// Alpha = 0.3
}
// Differencing
arma::mat differenced = data.rows(1, data.n_rows - 1) - data.rows(0, data.n_rows - 2);
// Fourier Transform
cv::Mat fftResult;
cv::dft(cv::Mat(data), fftResult, cv::DFT_COMPLEX_OUTPUT);
使用 Armadillo 进行特征交互工程
以下是如何在特征交互工程中使用 Armadillo 的示例:
- 使用以下 Armadillo 代码片段从矩阵数据(
rows= 样本,cols= 特征)构造多项式(平方)特征和两两交互项;它会先追加平方列,然后构建所有特征间的乘积:
#include <armadillo>
// Polynomial Features
arma::mat polyFeatures(data.n_rows, data.n_cols * 2);
polyFeatures.cols(0, data.n_cols - 1) = data;
polyFeatures.cols(data.n_cols, polyFeatures.n_cols - 1) = arma::square(data);
// Interaction Terms
arma::mat interactionTerms(data.n_rows, data.n_cols * (data.n_cols - 1) / 2);
size_t col = 0;
for (size_t i = 0; i < data.n_cols; ++i) {
for (size_t j = i + 1; j < data.n_cols; ++j) {
interactionTerms.col(col++) = data.col(i) % data.col(j);
}
}
在深入探讨之前,我们从结构化/表格型信号转向非结构化模态。目标保持不变——在不泄漏的情况下进行清洗、规范化和合理的增强——但基本操作发生了变化:自然语言处理(NLP)中的分词和文本规范化,图像中的尺寸调整/色彩空间规范化和几何增强,音频中的重采样/去噪和频谱图(例如,梅尔频率倒谱系数(MFCCs)),以及视频中的帧采样和时间增强。接下来,我们将在按模态分类的标题(例如,图像归一化、文本分词)下复刻前面的技术,并展示简洁、基于库的解决方案(OpenCV、libtorch、tokenizers/ONNX),用更少的代码实现同样的目标。
非结构化数据
非结构化数据包括图像、文本数据、音频和视频。这些格式缺乏内在组织,需要专门的技术将其预处理为模型兼容的表示。非结构化数据的动态特性要求灵活且模块化的预处理方法,以适应不断变化的模型需求和数据特征。
在本节中,我们将介绍非结构化数据的全面预处理技术。
文本数据
文本数据的预处理技术包括:
- 分词、停用词去除、词干提取和大小写规范化
- 基于嵌入的向量化,使用全局词向量表示(GloVe)、Word2Vec 或 Transformers(例如,来自 Transformers 的双向编码器表示(BERT))
- 序列填充和截断,以统一模型输入长度
- 高级技术,例如命名实体识别(NER)、情感分析和词形还原
以下示例使用了成熟的 C++ 库来处理 NLP 任务。这些库简化了流程,使您可以用最少的代码执行分词、嵌入和文本预处理等任务。
C++ 中用于 NLP 和预处理的库包括:
- SentencePiece(C++):子词分词(字节对编码(BPE)/Unigram);Unicode 安全
- libstemmer(Snowball,C):快速词干提取(Porter2 等),带有可从 C++ 调用的简单 C API
- ICU/Boost.Locale:提供 Unicode 规范化、大小写折叠和分割功能
- Eigen/Armadillo:用于加载/平均词向量及其他变换的线性代数库
- FastText/GloVe(文件):可以加载到 Eigen/Armadillo 中的预训练词向量
- ONNX Runtime(C/C++):无需 Python 即可运行导出的 Transformer 模型(BERT/稳健优化的 BERT 预训练方法(RoBERTa)、NER、情感分析)
- UDPipe(C++):分词 + 词性标注(POS)+ 词形还原(支持多种语言),并带有预训练模型
让我们详细回顾这些技术。
分词、停用词去除、词干提取
让我们看看如何使用标准 C++ 和 libstemmer 进行经典的文本清理;如果您需要模型兼容的 ID,可以换用 SentencePiece 进行子词分词:
tokenize_stem_demo.cpp:
std::vector<std::string> tokenizeWords(const std::string &s)
{
static const std::regex rx(R"([A-Za-z]+(?:'[A-Za-z]+)?)");
std::sregex_iterator it(s.begin(), s.end(), rx), end;
std::vector<std::string> out;
for (; it != end; ++it)
{
std::string t = it->str();
std::transform(t.begin(), t.end(), t.begin(), ::tolower);
out.push_back(std::move(t));
}
return out;
}
std::vector<std::string> removeStop(const std::vector<std::string> &toks)
{
static const std::unordered_set<std::string> stop = {
"a", "an", "the", "and", "or", "but", "if", "in", "on", "of",
"to", "for", "is", "are", "was", "were", "with", "as", "by", "at"
};
std::vector<std::string> out;
out.reserve(toks.size());
for (const auto &w : toks)
{
if (!stop.count(w))
{
out.push_back(w);
}
}
return out;
}
std::vector<std::string> stemEnglish(const std::vector<std::string> &toks)
{
std::vector<std::string> out;
out.reserve(toks.size());
sb_stemmer *st = sb_stemmer_new("english", nullptr);
for (const auto &w : toks)
{
const sb_symbol *s = sb_stemmer_stem(
st,
(const sb_symbol *)w.data(),
(int)w.size()
);
out.emplace_back((const char *)s, sb_stemmer_length(st));

浙公网安备 33010602011771号