支持向量机(Support Vector Machine)

概念

支持向量机(Support Vector Machine)是Vapnik等于1995年首先提出的,它在解决小样本、非线性及高维模式识别中表现出许多特有的优势,并能够推广应用到函数拟合等其他机器学习问题中。

SVM是建立在统计学习理论的VC 维理论和结构风险最小原理基础上的,根据有限的样本信息在模型的复杂性和学习能力之间寻求最佳折衷,以期获得最好的推广(泛化)能力

优点

  • 支持小样本
  • 支持非线性和高维识别
  • 泛化能力强

任务

在二分类的样本集$D={(x_i,y_i)|i=1,2,…,m},y_i\in{1,-1}$中,SVM希望找到一个超平面将不同样本分开,同时希望两边的数据离超平面有尽量大的间隔,以期获得高分类的容忍度;

image-20240528084404771

超平面方程和上下的边界方程如下,对$\mathbf \omega=(\omega_1,\omega_2,…,\omega_d)$为法向量,$\mathbf b$为位移项;
$$
\mathbf \omega^T\mathbf x+\mathbf b=0,1,-1
$$
假设能将训练样本正确分类,必有:
$$
\left.\left{\begin{array}{ll}\boldsymbol{w}^\mathrm{T}\boldsymbol{x}_i+b\geqslant+1,&y_i=+1 ;\\boldsymbol{w}^\mathrm{T}\boldsymbol{x}_i+b\leqslant-1,&y_i=-1 .\end{array}\right.\right.
$$
在上下边界上使得方程等号成立的点就是支持向量,上下边界之间的间隔Margin如下:
$$
\gamma=\frac2{||\boldsymbol{w}||}
$$
image-20240528085402206

最大化间隔等价于如下最优化问题(Primal形式):
$$
\begin{aligned}&\min_{\boldsymbol{w},b}\quad\frac{1}{2} |\boldsymbol{w}|^{2}\&\mathrm{s.t.} y_{i}(\boldsymbol{w}^{\mathrm{T}}\boldsymbol{x}_{i}+b)\geqslant1,\quad i=1,2,\ldots,m.\end{aligned}
$$
该形式求解算法的复杂度与样本维度有关;

用Lagrange乘子法约束如下:
$$
L(\boldsymbol{w},b,\boldsymbol{\alpha})=\frac{1}{2} |\boldsymbol{w}|^{2}+\sum_{i=1}^{m}\alpha_{i}\left(1-y_{i}(\boldsymbol{w}^{\mathrm{T}}\boldsymbol{x}{i}+b)\right)
$$
求偏导可知:
$$
\begin{aligned}&\boldsymbol{w}=\sum
{i=1}^m\alpha_iy_i\boldsymbol{x}i ,\&0=\sum{i=1}^m\alpha_iy_i .\end{aligned}
$$
当$\alpha_i>0$,意味着第$i$个样本位于最大间隔边界上,也就是支持向量,对于SVM来说,训练结束后大多数样本无需保留,最终模型仅和支持向量有关;

对于二次凸规划问题,满足KKT条件如下(Dual形式):
$$
\max_{\boldsymbol{\alpha}}\sum_{i=1}^m\alpha_i-\frac12\sum_{i=1}^m\sum_{j=1}^m\alpha_i\alpha_jy_iy_j\boldsymbol{x}_i^\mathrm{T}\boldsymbol{x}_j
\s.t.\left.\left{\begin{array}{l}\alpha_i\geqslant0 ;\\y_if(\boldsymbol{x}_i)-1\geqslant0 ;\\\alpha_i\left(y_if(\boldsymbol{x}_i)-1\right)=0 ..\end{array}\right.\right.
$$
这个问题有许多技巧处理,比如SMO算法,求解算法的复杂度与样本数量(等于拉格朗日算子$\alpha$的数量)有关;

核函数解决线性不可分问题

将样本映射到高维空间使得样本在高维空间线性可分,具体来说分类器如下:
$$
f(\boldsymbol{x})=\boldsymbol{w}^\mathrm{T}\phi(\boldsymbol{x})+b
$$
对偶问题如下:
$$
\max_{\alpha}:\sum_{i=1}^{m}\alpha_{i}-\frac{1}{2}:\sum_{i=1}^{m}\sum_{j=1}^{m}\alpha_{i}\alpha_{j}y_{i}y_{j}\phi(\boldsymbol{x}{i})^{\mathrm{T}}\phi(\boldsymbol{x}{j})\
s.t. \sum_{i=1}^m\alpha_iy_i=0 ,
\alpha_i\geqslant0:,\quad i=1,2,\ldots,m
$$
构造核函数:$\kappa(\boldsymbol{x}_i,\boldsymbol{x}_j)=\langle\phi(\boldsymbol{x}_i),\phi(\boldsymbol{x}_j)\rangle=\phi(\boldsymbol{x}_i)^\mathrm{T}\phi(\boldsymbol{x}_j)$

例如:
$$
\begin{aligned}
& k(x,z)=(x\cdot z)^2 \
& x=(x^{(1)},x^{(2)})\text{,}z=(z^{(1)},z^{(2)}) \
&\phi(x)=((x^{(1)})^2,\sqrt{2}x^{(1)}x^{(2)},(x^{(2)})^2)^T \
&\phi(x)\cdot\phi(z)=(x\cdot z)^2=k(x,z)
\end{aligned}
$$
代入得支持向量展式:
$$
\begin{aligned}
f(\boldsymbol{x})& =\boldsymbol{w}^\mathrm{T}\phi(\boldsymbol{x})+b \
&=\sum_{i=1}^m\alpha_iy_i\phi(\boldsymbol{x}i)^\mathrm{T}\phi(\boldsymbol{x})+b \
&=\sum
{i=1}^m\alpha_iy_i\kappa(\boldsymbol{x},\boldsymbol{x}_i)+b .
\end{aligned}
$$

决策树DecisionTree

决策树是基于树结构来进行决策的,以二分类任务为例,我们希望从给定训练数据集学得一个模型用以对新示例进行分类,这个把样本分类的任务,可看作对“当前样本属于正类吗?”这个问题的“决策”或“判定”过程.

这恰是人类在面临决策问题时一种很自然的处理机制,著名的例子如下:

image-20240614030412182

学习决策过程中提出的每个判定问题都是对某个属性的“测试”决策过程的最终结论对应了我们所希望的判定结果

每个测试的结果或是导出最终结论,或者导出进一步的判定问题,其考虑范围是在上次决策结果的限定范围之内

从根结点到每个叶结点的路径对应了一个判定测试序列

决策树学习的目的是为了产生一棵泛化能力强,即处理未见示例能力强的决策树。

决策树是从有类标号的训练元组中学习决策树,适合探测式的知识发现,高维数据;

  • 自上而下地分治构造,直到构造到叶子结点,叶子结点的测试条件如下:

    • 当前结点包含的样本全部属于同一类别;

    • 当前属性集为空,或所有样本在所有属性上取值相同;

    • 当前结点包含的样本集合为空

  • 分类的启发式方法

    • 度量:信息增益/Gini系数/信息增益率
    • ID3算法
      • 选择最高信息增益的属性作为最具分类能力的属性
      • 当前样本所需信息熵为
        $H(D)=-\sum_{i} p_i log(p_i)$
      • 利用属性A来将D分为v个部分$H(D|A)=\sum_{j\le v}\frac{|D_j|}{|D|}H(D_j)$
      • 使用A分支的信息增益
        $IG(A)=H(D)-H(D|A)$
    • C4.5算法
      • 克服划分子集自由一个类的问题,使用规范化信息增益
      • 划分带来的信息(固有值)$IV_A(D)=-\sum_{j\le v}\frac{|D_j|}{|D|}log(\frac{|D_j|}{|D|})$
      • 规范化信息增益率$GainRatio(A) = IG(D|A)/IV_A(D)$
    • Cart算法
      • Gini指数反映数据量数据分区或训练元组D的不纯度

      • $Gini(D)=1-\sum_{i} p_i^2$
        $p_i$代表元组属于类$C_i$的概率

      • 对于D的基于指标A的二元分裂$D_1,D_2$,
        $Gini_A(D)=\frac{|D_1|}{|D|}Gini(D_1)+\frac{|D_2|}{|D|}Gini(D_2)$

      • 选择使得不纯度降低的最大化的二元划分$\Delta Gini(A)=Gini(D)-Gini_A(D)$

  • 算法:

    image-20240528034907462

  • 如何解决Overfit过拟合?

    训练样本只是真实模型下的一个抽样集,导致模型泛化能力不强;

    • 增加样本集
      • 降低模型复杂度
      • Train-Validation—Test
      • 模型选择:正则项等
    • 设定决策树的最大高度来限制树的生长
    • 先剪枝:提前终止树的构造
      • 节点分裂的度量低于阈值,划分停止
    • 后剪枝:从完全生长的树剪去树枝
      • 代价比较大
  • 决策树归纳:提取分类规则

    • 可以提取决策树表示的知识,并以IF-THEN形式的分类规则表示
    • 对从根到树叶的每条路径创建一个规则
    • 沿着给定路径上的每个属性-值对形成规则前件(”IF”部分)的一个合取项
    • 叶节点包含类预测,形成规则后件(”THEN”部分)
    • IF-THEN规则易于理解,尤其树很大时
    • 评估:
      • 覆盖率:$coverage(R)=\frac{n_{cover}}{|D|}$
      • 准确率:$accuracy(R)=\frac{n_{correct}}{n_{cover}}$
      • 解决多个规则触发的冲突:
        • 规模序:按照苛刻性度量
        • 规则序:根据类的重要性度量

在sklrean中默认的划分方式就是gini指数,默认的决策树是CART树;但是gini指数的划分趋向于孤立数据集中数量多的类,将它们分到一个树叶中,而熵偏向于构建一颗平衡的树,也就是数量多的类可能分散到不同的叶子中去了。

分类(Classfication):前置知识

Background

Content

监督学习vs无监督学习

监督学习:分类,回归

  • 对象的类标签已知
  • 通过类标签的指导下学习数据中的模式
  • 利用获取的模式或者模型对新数据进行分类预测

无监督学习:Clustering, Frequent-Patten

  • 数据集中对象的类标记(概念)是未知的;
  • 挖掘潜在的数据内部模式;

生成模型vs判别模型

生成模型:希望从数据中学习出原始的真实数据生成模型。常见的方法是学习数据的联合概率分布。如Naive Bayes,Hidden Markov等。

判别模型:从数据中学习到不同类概念的区别(划分界限)从而进行分类。如KNN,SVM,ANN,Decision Tree

分类器的评估

二分类任务中的混淆矩阵:
$$
\begin{array}{c|c|c}\hline&\text{预测结果}\\hline\text{真实情况}&\text{正例}&\text{反例}\\hline\text{正例}&TP\text{(真正例)}&FN\text{(假反例)}\\hline\text{反例}&FP\text{(假正例)}&TN\text{(真反例)}\\hline\end{array}
$$
更详细的解释:

  • TP:真正例,被正确分类的正样本
  • TN:真负例,被正确分类的负样本
  • FP:假正例,被错误分类的负样本
  • FN:假负例,被错误分类的正样本

评价指标

查准率/精度(precision)为被分类为正样本中分类正确的比例
$$
P=\frac{TP}{TP+FP}
$$
查全率/敏感度(recall)为实际为正样本中分类正确的比例
$$
R=\frac{TP}{TP+FN}
$$

准确度/识别率(accuracy)为全体样本中分类正确的比例
$$
accuracy=\frac{TP+TN}{N+P}
$$
错误率(error)为全体样本中分类错误的比例
$$
error = \frac{FP+FN}{P+N}
$$
特效性$specificity=\frac{TN}{N}$,灵敏性$sensitivity=\frac{TP}{P}$

F度量
$$
F=\frac{2\times precision \times recall}{precision + recall}
$$

$$
F_{\beta}=\frac{(1+\beta^2)\times precision \times recall}{\beta^2 \times precision + recall}
$$

人工神经网络(ANN)

概念

人工神经网络(ANN,Artificial Neural Networks)是在人类对大脑神经网络认识理解的基础上,人工构造的能够实现某种功能的神经网络。

它是理论化的人脑神经网络的数学模型,是基于模仿大脑神经网络结构和功能而建立起来的一种信息处理系统。

神经网络结构和工作机理基本上是以人脑的组织结构和活动规律为背景的,它反映了脑的某些基本特征,但并不是要对人脑部分的真正实现,可以说它是某种抽象、简化或模仿。

如果将大量功能简单的形式神经元通过一定的拓扑结构组织起来,构成群体并行分布式处理的计算结构,那么这种结构就是人工神经网络,在不引起混淆的情况下,统称为神经网络。

ANN有三大要素如下:

  • 神经元的激活规则:主要是指神经元输入到输出之间的映射关系,一般为非线性函数
  • 网络的拓扑结构:不同神经元之间的连接关系
  • 学习算法:通过训练数据来学习神经网络的参数

激活函数

三种形式:阈值型,S型,伪线性型

image-20240528092937689

典型的S型函数可取为连续光滑可导的Sigmoid函数:$f(x)=\frac{1}{1+e^{-x}}$

它有一个良好的性质:$f’\left(x\right)=f\left(x\right)\left(1-f\left(x\right)\right)$

M-P神经元模型

我们将其抽象为多输入单输出得非线性阈值器件,传递函数如下:
$$
y=f\left(\sum_{i=1}^nw_ix_i-\theta\right)
$$
第$i$个突触连接强度为$\omega_i$,$\theta$为神经元阈值,$f$为激活函数

image-20240528092737355

多层感知机(Multi-layer Perceptron, MLP)(前馈神经网络,Feedforward Neural Network, FNN)

设置学习率$\eta$,将阈值看作固定输入-1的dummy node;

将对权重和阈值的学习统一看成权重的调整,迭代过程为:$\omega_i \gets \omega_i+ \eta(y-\hat y)x_i$​;

学习率$\eta \in (0,1)$控制着算法每一轮迭代中的更新步长,若太大则容易振荡,太小则收敛速度又会过慢.

但是要解决复杂问题,单层神经网络是不够用的,因此引入隐层和多层前反馈神经网络:其中输入层神经元接收外界输入,隐层与输出层神经元对信号进行
加工,最终结果由输出层神经元输出;

  • 各神经元分别属于不同的层,层内无连接

  • 相邻两层之间的神经元全部两两连接

  • 整个网络中无反馈,信号从输入层向输出层单向传播,可用一个有向无环图表示

image-20240614075201314

根据通用近似定理,对于具有线性输出层和至少一个使用“挤压”性质的激活函数的隐藏层组成的前馈神经网络,只要其隐藏层神经元的数量足够,它可以以任意的精度来近似任何从一个定义在实数空间中的有界闭集函数。因此神经网络经常被当作一个万能函数使用,进行复杂的特征转换,或者逼近一个复杂的条件分布;

image-20240614182017411

前馈神经网络如何学习分类器?

  • 若$g$​为Logistic回归,那么Logistic回归分类器可以看成神经网络的最后一层;
  • 如果使用softmax回归分类器,相当于网络最后一层设置 C 个神经元,其输出经过softmax函数进行归一化后可以作为每个类的后验概率,$$\hat{\mathbf{y}}=\mathrm{softmax}(\mathbf{z}^{(L)})$$
  • 采用交叉熵损失函数,对于样本(x, y),其损失函数为$\mathcal{L}(\mathbf{y},\hat{\mathbf{y}})=-\mathbf{y}^\mathrm{T}\log\hat{\mathbf{y}}$

神经网络能够通过训练,改变其内部表示,使输入-输出变换朝好的方向发展。训练实质是用同一个训练集的样本反复作用于网络,网络按照一定的训练规则(学习规则或学习算法),自动调节神经元之间的连接强度或拓扑结构,当网络的实际输出满足期望的要求,或者趋于稳定,这认为训练圆满结束。神经网络的学习成果蕴含在权值和阈值当中;

image-20240528094010804

BP(误差反向传播)算法

对于一个BP网络(BP算法训练的多层前反馈神经网络),参数属性如下:

image-20240528102540797

连续不断地在相对于误差函数斜率下降的方向上计算网络权值和偏差的变化而逐渐逼近目标的。每一次权值和偏差的变化都与网络误差的影响成正比,并以反向传播的方式传递到每一层的。

对于训练样例$(\mathbf x_k,\mathbf y_k)$,ANN的预测结果为$\hat{\mathbf y}_j^k=(\hat{y}_1^k,\hat{y}_2^k,..,\hat{y}_l^k)$

BP算法的目标是最小化均方误差:
$$
E_k=\frac12\sum_{j=1}^l(\hat{y}j^k-y_j^k)^2
$$
BP算法的策略是基于梯度下降的,也即,以目标的负梯度方向对参数进行调整:
$$
\Delta w
{hj}=-\eta\frac{\partial E_k}{\partial w_{hj}}
$$
数学上:$g_i=-\frac{\partial E_k}{\partial\hat{y}j^k}\cdot\frac{\partial\hat{y}j^k}{\partial\beta_j}=\hat{y}{j}^{k}(1-\hat{y}{j}^{k})(y_{j}^{k}-\hat{y}_{j}^{k}) $

得到BP算法的迭代方程:
$$
\begin{aligned}
\Delta w_{hj}=&\quad\eta g_jb_h\
\Delta\theta_{j} =&-\eta g_j\
\Delta v_{ih} =&\quad\eta e_hx_i\
\Delta\gamma_{h} =&-\eta e_h
\end{aligned}
$$
image-20240528110628177

如果取损失函数为结构化风险函数:
$$
\begin{aligned}\mathcal{R}(W,\mathbf{b})=\frac{1}{N}\sum_{n=1}^{N}\mathcal{L}(\mathbf{y}^{(n)},\hat{\mathbf{y}}^{(n)})+\frac{1}{2}\lambda|W|_F^2\end{aligned}
$$
那么梯度下降策略可表示为:
$$
W^{(l)}\leftarrow W^{(l)}-\alpha\frac{\partial\mathcal{R}(W,\mathbf{b})}{\partial W^{(l)}}\
\mathbf{b}^{(l)}\leftarrow\mathbf{b}^{(l)}-\alpha\frac{\partial\mathcal{R}(W,\mathbf{b})}{\partial\mathbf{b}^{(l)}}
$$
前馈神经网络信息向前(单向)传递,层内节点之间并不连接,适合于处理静态数据分析,如回归、分类等任务;

对于全连接前馈神经网络来说,有不少缺点:

  1. 容易过拟合;
  2. 训练比较慢,可解释性差;
  3. 权重矩阵参数非常多,不满足局部不变性特征;

卷积神经网络(Convolutional Neural Network, CNN)

卷积神经网络是受生物学上感受野(Receptive Field)的机制而提出的,在视觉神经系统中,一个神经元的感受野是指视网膜上的特定区域,只有这个区域内的刺激才能够激活该神经元;

二维卷积:对于输入矩阵$A_{m\times m}$,核矩阵$B_{n\times n}$,步幅为$s$,卷积结果$R=A*B$是一个阶为$(m-n)/s+1$的方阵

image-20240614184831358

二维卷积有两种不同的方式:

  1. 有效卷积:没有padding
  2. 相同卷积:输入和输出相同

通常没有零填充的最优数量处于有效卷积和相同卷积中间的某个位置;

以下是一个图像中的二维卷积的例子:

image-20240614190204258

image-20240614190230471

卷积神经网络是针对图像处理设计的特殊的网络结构,相对于前馈神经网络有以下特点:

  1. 参数过多,计算效率低;
  2. 忽略了图像的二维结构;
  3. 依赖于空间上下文;
  4. 不够鲁棒;

因为卷积神经网络具有平移不变性,因此在图像处理上有独特的优势;

卷积神经网络包括卷积层,池化层,和全连接层;

  • 卷积层:平移核计算内积;

  • 池化层:池化函数使用某一位置的相邻输出的总体统计特征来代替网络在该位置的输出

    • 最大池化(Max Pooling)函数给出相邻矩形区域内的最大值

      image-20240614191012564

    • 其他常用的池化函数包括相邻矩形区域内的平均值、L2范数以及基于据中心像素距离的加权平均函数

递归神经网络(Recurrent Neural Network,RNN)

反馈神经网络全部或者部分神经元可以接受来自其它神经元或自身信号的神经网络结构,其拓扑结构可以是网状的,也可以是具有一定层级的,反馈神经网络通常被视为一个动态系统,主要关心其随时间变化的动态过程

RNN至少包含一个反馈链接的神经网络结构,特别适合处理时序数据,其基本结构如下所示:

image-20240614191837063

RNN主要模拟的是人脑的记忆,来进行一个反馈的过程;

image-20240614192112304

按时间顺序展开:

image-20240614192147098

按时间顺序全节点展开:

image-20240614192252821

按时间顺序展开(带延迟)

image-20240614192320598

计算机视觉

计算机视觉主要研究的是给机器以“看”的能力,眼睛是人类最重要的传感器,超过一半的大脑都会参与视觉功能

  • 从数字化信息中提取信息、识别理解

  • 计算机成像学:图像处理等底层视觉,如图像去噪、图像超分辨、图像增强、风格变换

  • 图像理解:语义理解等高层视觉,如图像分类、目标检测、人脸识别、语义分割

  • 三维视觉

  • 视频理解

计算机视觉三大会议:CVPR、ICCV、ECCV

图像表征说的是把图像用数学和信息的方式进行表达,有助于我们进行统一处理,于机器学习而言,我们需要构建一个映射函数(模型),以实现基于图像的各种计算机视觉应用;

image-20240614192636736

图像分析:

image-20240614192651525

图像分类是根据图像的语义信息将不同类别图像区分开来,是计算机视觉中重要的基本问题,也是图像检测、图像分割、物体跟踪、行为分析等其他高层视觉任务的基础。图像分类在很多领域有广泛应用,包括安防领域的人脸识别和智能视频分析等,交通领域的交通场景识别,互联网领域基于内容的图像检索和相册自动归类,医学领域的图像识别等。

image-20240614192658610

图像分类的基本流程为:图像数据-特征提取-分类器;

什么是特征提取?

从原始的输入的数据中提取有效的、非冗余的、具有代表性的特征。它通常是原始特征的重新组合,以减少数据的维度,提取或整理出有效的特征以提升后续模型的表示能力

什么是好的特征?

  • 视觉数据复杂多变,同一类物体可能存在不同的形态

  • 同一个物体在不同的角度和环境下数据差别也可能很大

  • 可重复性,可区分性,准确性,有效性,鲁棒性(稳定性、不变性)

图像学习有多种办法,比如Bag-of-Words,End-to-end learning

目标检测主要任务是在给定的图片中精确找到物体所在位置,并标注出物体的类别。其模型可以识别一张图片的多个物体,并可以定位出不同物体(给出边界框)

image-20240614193002529

物体检测有多种办法:Sliding window

目前主流的目标检测算法主要是基于深度学习模型,可以分成两大类:

  1. Two-stage检测算法,其将检测问题划分为两个阶段,首先产生候选区域(region proposals),然后对候选区域分类(一般还需要对位置精修),这类算法的典型代表是基于region proposal的R-CNN系算法,如R-CNN,Fast R-CNN,Faster R-CNN等
  2. One-stage检测算法,其不需要region proposal阶段,直接产生物体的类别概率和位置坐标值,比较典型的算法如YOLO、SSD等

在目标检测中,进行分类和坐标回归之前,需要进行候选区域提取,selective search方法即是一种常用的提取候选区域的方法;

image-20240614193236611

R-CNN是基于region proposal方法的目标检测算法系列开山之作,其先进行区域搜索,然后再对候选区域进行分类

候选区域(Region Proposal):基于Selective Search,利用图像中的纹理、边缘、颜色等信息,保证在选取较少窗口(几千甚至几百)的情况下保持较高的召回率(Recall)

总体来看,R-CNN是非常直观的,就是把检测问题转化为了分类问题,并且采用了CNN模型进行分类,但是效果却很好

R-CNN缺点:

  1. 训练分为多个阶段,微调网络+训练SVM+训练边框回归器
  2. 训练耗时,占用磁盘空间大:5000张图像产生几百G的特征文件
  3. 速度慢: 使用GPU, VGG16模型处理一张图像需要47s
  4. 测试速度慢:每个候选区域需要运行整个前向CNN计算
  5. SVM和bounding box回归是事后操作:在SVM和回归过程中CNN特征没有被学习更新
  6. 输入图片的大小必须要固定(224x224、227x227等)

R-CNN、Fast R-CNN、Faster R-CNN都是two-stage方法,这种方法都有一个共同的特征,即第一个阶段都要产生候选区域(region proposal),之后再对候选区域进行分类和坐标回归等操作。虽然不同算法产生后选取区域的方法不尽相同,但是其目的都是一致的。

但是two-stage方法有其固有缺点,即是第一阶段都要进行候选区域提取会消耗很多时间,使目标检测的耗时增加,效率降低。

因此在此基础上,研究人员对目标检测方法进行了改进,提出了one-stage方法,可以直接对物体的坐标和类别进行回归而不需要进行显式的提取候选区域的过程。

YOLO(You Only Look Once)是2016年提出一个全新的方法,一次性把一整张图片用到一个神经网络中去。网络把图片分成不同的区域,然后给出每个区域的边框预测和概率,并依据概率大小对所有边框分配权重。最后,设置阈值,只输出得分(概率值)超过阈值的检测结果

YOLO的目标检测的流程:

  • 给个一个输入图像,resize图像到448x448,将图像划分成7x7的网格
  • 对于每个网格,我们都预测2个边框(包括每个边框是目标的置信度以及每个边框区域在多个类别上的概率)
  • 根据上一步可以预测出772个目标窗口,然后根据阈值去除可能性比较低的目标窗口,最后NMS去除冗余窗口即可

对于每一个cell

  • 预测B个bounding boxes,每个bounding box有一个confidence score (objectness)
  • 只预测一个物体
  • 假设有C个类别,预测分别属于每个类别的概率
  • 对于PASCAL VOC 2007,有7x7 grid cells, 2 bounding boxes, 以及20个类别

YOLO网络结构如右图。主要采用了AlexNet。卷积层主要用来提取特征,全连接层主要用来预测类别概率和坐标卷积层之后接了一个4096维的全连接层,然后后边又全连接到一个7730维的张量上。7*7就是划分的网格数,在每个网格上预测目标两个可能的位置以及这个位置的目标置信度和类别,也就是每个网格预测两个目标,每个目标的信息有4维坐标信息(中心点坐标+长宽),1个是目标的置信度,还有类别数20(VOC上20个类别),总共就是(4+1)*2+20 = 30维的向量

image-20240614193558912

总的来看,YOLO具有较强的泛化能力

Naive Bayes 分类器

贝叶斯分类算法是统计学的一种分类方法,它是一类利用概率统计知识进行分类的算法。在许多场合,朴素贝叶斯(Naïve Bayes,NB)分类算法可以与决策树和神经网络分类算法相媲美,该算法能运用到大型数据库中,而且方法简单、分类准确率高、速度快。

但由于对数据特征条件独立的强假设,所以如果数据集不符合这种假设,准确率可能会较低。

Bayes决策论

假设有$N$种可能的类别标记,即$\mathcal{Y}={c_1,c_2,\ldots,c_N};$

$\lambda_{ij}$是将一个真实标记为$c_j$的样本误分类为$c_i$所产生的损失.

基于后验概率$P(c_i\mid\boldsymbol{x})$可获得将样本$\boldsymbol x$分类为$c_i$所产生的期望损失;

即在样本$\boldsymbol x$上的“条件风险”(conditional risk)
$$
R(c_i\mid\boldsymbol{x})=\sum_{j=1}^N\lambda_{ij}P(c_j\mid\boldsymbol{x})
$$
我们的任务是寻找一个判定准则$h:\mathcal{X}\mapsto\mathcal{Y}$以最小化总体风险
$$
R\left(h\right)=\mathbb{E}{\boldsymbol{x}}\left[R\left(h\left(\boldsymbol{x}\right)\mid\boldsymbol{x}\right)\right]
$$
对每个样本$\boldsymbol x$,若$h$能最小化条件风险$R(h(\boldsymbol{x})\mid\boldsymbol{x})$,则总体风险$R(h)$也将被最小化.
$$
h^*(\boldsymbol{x})=\arg\min
{c\in\mathcal{Y}}R(c\mid\boldsymbol{x})
$$
若目标是最小化分类器错误率,则误判损失表示为$$\left.\lambda_{ij}=\left{\begin{array}{ll}0,&\mathrm{if~}i=j:;\1,&\mathrm{otherwise},\end{array}\right.\right.$$

此时条件风险 $R( c\mid \boldsymbol{x}) = 1- P( c\mid \boldsymbol{x})$ ,

于是,最小化分类错误率的贝叶斯最优分类器为
$$
h^(\boldsymbol{x})=\underset{c\in\mathcal{Y}}{\operatorname{\arg\max}}P(c\mid\boldsymbol{x})
$$
即对每个样本$x$,选择能使后验概率$P(c\mid\boldsymbol{x})$最大的类别标记.

属性条件独立假设

假设有$d$维属性数目,$x=(x_1,x_2,…,x_d)$
$$
P(c\mid\boldsymbol{x})=\frac{P(c) P(\boldsymbol{x}\mid c)}{P(\boldsymbol{x})}=\frac{P(c)}{P(\boldsymbol{x})}\prod_{i=1}^dP(x_i\mid c)
$$
由极大似然估计可知,贝叶斯判定准则如下:
$$
h_{nb}(\boldsymbol{x})=\arg\max_{c\in\mathcal{Y}}P(c)\prod_{i=1}^dP(x_i\mid c)
$$

拉普拉斯修正

为了避免其他属性携带的信息被训练集中未出现的属性值“抹去”,
在估计概率值时通常要进行“平滑”(smoothing)

避免了因训练集样本不充分而导致概率估值为零的问题,

并且在训练集变大时,修正过程所引入的先验(prior)的影响也会逐渐变得可忽
略,使得估值渐趋向于实际概率值.

KNN(K-NearestNerborhood)学习

特点

  • 懒惰学习(LazyLearning):

    1. 在训练阶段仅仅是把样本保存起来,训练时间开销为零(没有显式的训练过程)
    2. 待收到测试样本后再进行处理
    3. 预测阶段较慢
  • 对$K$值敏感:增大$K$值,一般来说准确率先上升后下降

    讨论1NN在二分类问题的性能:泛化错误率不超过贝叶斯最优分类器的错误率的两倍
    • $K=1$时目标是找到最近邻的1个点,给定测试样本$x$ ,最近邻训练样本为$z$,标签集为$\mathcal{Y}$,抽样的样本独立同分布
    • 对任意测试样本,总能在任意近的范围的范围内找到一个训练样本
    • 令$c^*=arg \max_{c \in \mathcal{Y} } P(c|x) $表示Bayes最优分类器的结果,计算分类出错的概率

    $$
    \begin{aligned}
    P(err)& =1-\sum_{c\in\mathcal{Y} }P(c\mid\boldsymbol{x})P(c\mid\boldsymbol{z}) \
    &\simeq1-\sum_{c\in\mathcal{Y} }P^2(c\mid\boldsymbol{x}) \
    &\leqslant1-P^2(c^\mid\boldsymbol{x}) \
    &=\left(1+P\left(c^
    \mid\boldsymbol{x}\right)\right)\left(1-P\left(c^\mid\boldsymbol{x}\right)\right) \
    &\leqslant2\times\left(1-P\left(c^
    \mid\boldsymbol{x}\right)\right).
    \end{aligned}
    $$

  • 高维诅咒:在高维空间中近邻的点都变得差不多远,而且KDtree优化难以进行

  • 其他:

    • 非参数化parameter-free:不对数据分布做出任何假设
    • 简单,易于实现,内存消耗大,计算成本高,解释性差,预测慢

基本思想

根据距离函数计算待分类样本X和每个训练样本的距离(作为相似度),选择与待分类样本距离最小的K个样本作为X的K个最近邻,最后以X的K个最近邻中的大多数所属的类别作为X的类别。

分类原理

  1. 导入划分训练集和测试集

    • 每条属性应该有若干属性和一个标签
    • 由于KNN是懒惰学习,对于测试集的每一个样本,KNN通过样本的若干属性和训练集的样本的属性的距离
  2. 设置算法的K值

    • $K$为超参数,需要人为设置
    • 算法实现后可通过交叉验证的方式选取最好的$K$
  3. 设置算法的距离指标

    Minkowski距离
    • 对数据对象 $i=(x_{i1},x_{i2},…,x_{ip}),j=(x_{j1},x_{j2},…,x_{jp})$,各维权重为 $\omega=(\omega_1,\omega_2,…,\omega_p)$
    • Minkowski距离:$d_{\omega}(i,j)=\left({\sum_{k=1}^{p}{w_i|x_{ik}-x_{jk}|^q}}\right)^{\frac 1 q}$
    • 注意,各维等价时,$p=1$称为Manhattan距离 $d(i,j)={\sum_{k=1}^{p}{|x_{ik}-x_{jk}|^q}}$,$p=2$称为Euclidean距离 $d(i,j)=\sqrt{\sum_{k=1}^{p}{|x_{ik}-x_{jk}|^2}}$
  4. 遍历所有的测试样本,对每一个样本进行预测

    • 当前的样本为 sample,计算 sample与训练集中的样本(标签为 lb)的距离 d,
    • 把所有距离-标签元组((d, lb))按照 d升序排序
    • 投票:对前 K个元组,找到出现次数最多的标签,这个标签就是预测的结果(平局的情况:我们选择数据点中第一个出现的数据点的标签作为结果,这在python也是很好实现的。)
  5. 记录所有预测的标签,计算准确率

  6. 对所有的可能的$K$​值,交叉验证

注意

  1. K值设定
  • K值选择过小:得到的近邻数过少,会降低分类精度,同时会放大噪声的干扰
  • K值选择过大:k个近邻并不相似的数据亦被包含进来,造成噪声增加而导致分类效果的降低。
  1. 类别的判定方式
  • 投票法没有考虑近邻的距离的远近,距离更近的近邻也许更应该决定最终的分类,所以加权投票法更恰当一些。
  1. 距离度量方式的选择

当变量越多(高维诅咒问题),欧式距离的区分能力越差。

  1. 性能问题

KNN是一种懒惰算法,构造模型很简单但在对测试样本分类地的系统开销大。

策略:采样训练样本量减少训练集的大小;或通过聚类,将聚类所产生的中心点作为新的训练样本。

Lossless-Compression

[TOC]

信息论基础

对于随机事件$A$,定义其自信息(self-information)为
$$
I(A)=-\log P(A)
$$
以2为底时单位为bit,自信息是非负值;

考虑两个随机变量$X,Y$,其取值为$x_i,y_j(1\le i\le n, 1\le j\le m)$,定义事件${X=x_i}, {Y=y_j}$的互信息为
$$
I(x_i,y_i)=\log \left( \frac{P(x_i|y_j)}{P(x_i)}\right)
$$
可以注意到这样的定义是对称的;

image-20241203214243920

定义随机变量的熵(entropy)为其自信息期望:
$$
H(x)=-\sum_{i=1}^n P(x_i)\log P(x_i)
$$
二元信源熵函数如下图,可以看到在$p=0.5$时熵达到了最大值;

image-20241203214708153

两个随机变量的联合熵(cross entropy)为其互信息期望:
$$
H(X,Y)=-\sum_{i=1}^n \sum_{i=1}^m P(x_i,y_j) \log P(x_i,y_j)
$$
条件熵(conditional entropy)为条件自信息期望
$$
H(X|Y)=-\sum_{i=1}^n \sum_{i=1}^m P(x_i,y_j) \log P(x_i|y_j)
$$
不难观察到
$$
I(X,Y)=H(X)-H(X|Y)=H(Y)-H(Y|X)\
H(X,Y)=H(X)+H(Y|X)=H(Y)+H(X|Y)
$$
image-20241203215247874

随机变量$X$位于发送端,$Y$位于接收端,当其间信道可靠性下降时(增加$p\le 0.5$)时, 其互信息减少;

信源编码定理

无记忆信源:独立分布的信息源,当前输出符号的值不取决于先前出现符号的值。

对于一个离散无记忆信源(DMS),其输出符号共有$L$个取值,每个符号的熵为
$$
H(X)=-\sum_{i=1}^n P(x_i)\log P(x_i)\le \log L
$$
前缀条件(prefix condition)是指没有一个码字构成另一个码字的前缀;

即时编码(instantaneous code)是指检测符号序列满足码字中的一个立马进行译码;

Kraft不等式:

存在码字长度为$n_1\le \cdots \le n_L$的二元码的充分必要条件为满足前缀条件
$$
\sum_{k=1}^L 2^{-n_k}\le 1
$$
proof:构造深度为$n_L$的二叉树,码字和叶子结点映射满足不等式$\sum_{i=1}^L 2^{n_L-n_i}\le 2^{n_L}$

image-20241203221834071

熵指定了在编码中每个符号平均位数的下限,即
$$
H(X)\le \overline l
$$
可以构造一种满足前缀条件的编码(著名的Hoffman编码)满足下上界
$$
\overline l < H(X)+1
$$
对于二元定长码(Fixed-Length Coding, FLC)来说,惟一编码需要的比特数为
$$
R=\lceil \log_2 L\rceil
$$
信源编码定理说明,对于任意用来表示信源中符号的前缀码,最小比特数平均至少等于信源的熵;

定义前缀码的效率为
$$
\eta = \frac{H(X)}{\overline R}
$$
若一个压缩过程和对应的解压过程没有造成信息的损失,称这样的压缩过程是无损的(lossless),否则称为有损的(lossy);

记压缩前后的比特数为$B_0,B_1$,定义其压缩率(compression ratio)为
$$
\eta = \frac{B_0}{B_1}, \eta>1.
$$
与定长编码相对的有变长编码(Variable-Length Coding, VLC);

游程编码

游程编码(Run-Length Encoding, RLE),基于利用信源中的内存的编码方式,是一种用于缩减重复字符串(游程)大小的技术,属于FLC;

基本原理:将每个游程表示为计数+符号

例子:S=11111111111111100000000000000000001111可表示为S=(15,1)+(19,0)+(4,1)=01111 1 10011 0 00100 1

适用场景黑白传真和PCX格式图像;

Shannon-Fano编码

Shannon-Fano编码属于VLC,对于每个字符$x$,其编码比特数为
$$
l(x)=\lceil \log\frac 1{p(x)}\rceil
$$
对于字串HELLO的编码如下表,总共花费了10bits完成编码;

Symbol Count Log2 Code bits used
L 2 1.32 0 1
H 1 2.32 10 2
E 1 2.32 110 3
O 1 2.32 111 3

算法步骤:

  1. 根据符号出现的频率计数对符号进行排序;
  2. 递归地将符号分成两部分,每部分的计数数大致相同,直到所有部分都只包含一个符号

这种编码机制得到的码字不是唯一的;

Huffman Coding

传统的Huffman编码是一种基于前缀码自底向上的编码方式,以下是算法流程:

  1. 将信源符号按照概率递减排序;
  2. 重复以下步骤直到只剩下一个符号:
    • 从列表中选择两个频率计数最低的符号
    • 形成一个将这两个符号作为子节点的 Huffman 子树,并创建一个父节点;
    • 将子项的频率计数之和分配给父项,并将其插入到列表中,以便保持顺序;
    • 列表中删除选择的两个符号子节点
  3. 为每个可能的路径分配前缀码字;

以下是X= HELLO的Huffman树

image-20241204133522963

Huffman编码同样也不一定惟一,且具有如下性质:

  • 前缀条件:没有一个字符的Huffman 码是任何其他 Huffman 码的前缀,以排除解码中的任何歧义。
  • 最优性(Optimal):实现了最小冗余编码

平均编码长度和信源的熵满足
$$
\overline l < H(X)+1
$$

Extended-Huffman Coding

霍夫曼编码中的所有码字都具有整数位长度,当信源的某个符号出现非常频繁,其带来的自信息几乎为0,这时用1位来编码都十分浪费;

我们可以将许多信源符号打包成一个整体,再统计概率;

对于信源的符号集
$$
S={s_1,s_2, \cdots, s_n}
$$
拓展Huffman编码真正需要对以下字符串集编码:
$$
S^{(k)}={(s_1…s_1), \cdots, (s_k…s_k)}
$$
最优性可进一步缩紧上界:
$$
H(X)\le \overline l\le H(X)+\frac 1k
$$
当$k$比较大,且$n$很大,要编码的字符表的大小为$O(n^k)$,这有时难以接受;

Adaptive Huffman Coding

传统的Huffman编码需要需要预先统计符号的概率,实践中可能信源的统计特性未知,需要对信源的输出作长时间观察才能得到;

自适应霍夫曼编码可以在数据流到达时,将动态收集和更新统计信源信息;

编码和解码的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def Encoder():
Initial()
while not EOF:
get(c)
encode(c)
update_tree(c)

def Decoder():
Initial()
while not EOF:
get(c)
decode(c)
update_tree(c)
  • 初始化:使用一些最初商定的代码分配给符号,而无需事先了解频率计数;
  • 更新树:增加符号的频率计数+更新树结构;

自适应霍夫曼树必须维持其兄弟属性(sibling),具体来说如下:

  • 节点按从左到右、从下到上的顺序编号,括号中的数字表示计数;
  • 所有节点都按计数递增的顺序排列;如果 sibling 属性即将被违反,则调用 swap 过程以通过重新排列节点来更新树。
  • 当需要swap时,计数为$N$的最远节点将与计数刚刚增加到$N+1$的节点交换;

下图是Swap的一个过程:

image-20241204182230672

霍夫曼树还满足一个附加规则:维护一个NEW符号:

  • 如果要首次发送任何字符/符号,则必须在其前面加上特殊符号 NEW。
  • NEW 的初始代码为 0;
  • NEW 的计数始终保持为 0(计数永远不会增加);

假设我们如下初始化AADCCDD的编码信息:

image-20241204182607058

自适应Huffman树更新如下:

image-20241204182653584

image-20241204182632203

Lempel-Ziv Coding

LZW 使用固定长度的码字来表示通常一起出现的可变长度的符号/字符字符串,例如,英文文本中的单词,许多字母成组出现,这就是一个有记忆的信源,用LZW算法就很有效;

LZW算法是基于字典的,LZW 编码器和解码器在接收数据时动态构建相同的字典。

LZW 将越来越长的重复项放入字典中,然后发出元素的代码,而不是字符串本身(如果该元素已放置在字典中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BEGIN
s = next input character;
while not EOF
{
c = next input character;

if s + c exists in the dictionary
s = s + c;
else
{
output the code for s;
add string s + c to the dictionary with a new code;
s = c;
}
}
output the code for s;
END

以下是对字符串A B AB BA B C AB ABB A的压缩过程:

image-20241204193339510

最后输出的顺序为A B AB BA B C AB ABB A,编码为1 2 4 5 2 3 4 6 1

解码器初始字符串表与编码器使用的字符串表相同。

1
2
3
4
5
6
7
8
9
10
11
12
BEGIN
s = NIL;
while not EOF
{
k = next input code;
entry = dictionary entry for k;
output entry;
if (s != NIL)
add string s + entry[0] to dictionary with a new code;
s = entry;
}
END

下图是刚刚编码的解压过程,就是查字典的过程

image-20241204194128283

在实践中,代码的长度存在上下界$[l_0,l_{\max}]$,字典初始大小为$2^{l_0}$,当字典被填满时,码长总体加1;当达到上界时,字典将删除最近最少使用条目;

Arithmetic Coding

算术编码是一种更现代的编码方法,通常优于霍夫曼编码。

消息由半开区间$ [a, b)$ 表示,其中 $a$ 和$ b$ 是介于 $0$ 和 $1$ 之间的实数。

最初,间隔为 $[0, 1)$。当消息变长时,间隔的长度会缩短,表示间隔所需的位数会增加;

这种编码方式需要为消息设置结束符terminator

区间确定算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
BEGIN
low = 0.0; high = 1.0; range = 1.0;

while (symbol != terminator)
{
get (symbol);
high = low + range * Range_high(symbol);
low = low + range * Range_low(symbol);
range = high - low;
}

output a code so that low <= code < high;
END

以下是对区间的确定过程,以CAEE$为例:

image-20241204195632545

算术编码的最后一步要求生成一个在 [low, high] 范围内的数字,以下算法将确保找到最短的二进制码字:

1
2
3
4
5
6
7
8
9
10
11
BEGIN
code = 0;
k = 1;
while (value(code) < low)
{
assign 1 to the kth binary fraction bit
if (value(code) > high)
replace the kth bit by 0
k = k + 1;
}
END

上述算法的解码过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BEGIN
get binary code and convert to
decimal value = value(code);
Do
{
find a symbol s so that
Range_low(s) <= value < Range_high(s);
output s;
low = Rang_low(s);
high = Range_high(s);
range = high - low;
value = [value - low] / range;
}
Until symbol s is a terminator
END

Lossless-JPEG图像压缩算法

给定图像$I$,其差分图像定义为每个像素的差分
$$
d(x,y)=I(x,y)-I(x-1,y)\
d(x,y)=4I(x,y)-I(x-1,y)-I(x+1,y)-I(x,y-1)-I(x,y+1)
$$
下式是二维Laplace算子离散版本,以下是原图和差分图像的对比

image-20241204201235574

由于正常图像中存在空间冗余,差异图像将具有较窄的直方图,因此熵较小;

在JPEG中,首先需要建立一种预测模型,预测器将最多三个相邻像素的值组合为当前像素的预测值,如下图的中的X所示。

image-20241204201741522

在预测X之前,A,B,C处像素已经被解码完成,预测模型通常选取下表所示;

image-20241204202005150

算法只需要对真实图像和预测图像之间的差异进行压缩即可,压缩算法选择上述任何一种均可,比如Huffman算法;

CMake入门

CMake简介

CMake 是一个跨平台的构建系统生成工具,用于管理编译过程,其主要功能如下:

  • 通过读取配置文件(CMakeLists.txt
  • 生成特定于平台的构建文件(如 Makefile 或 Visual Studio 工程文件);
  • 帮助用户管理项目的依赖关系、编译流程等;

也就是说CMake是一套交叉编译的工具链,通过CMake我们可以更好地管理项目结构;

CMake 相比于传统的构建工具(如直接编写 Makefile),具有以下几个核心特性:

  • 跨平台:CMake 支持多个平台(如 WindowsLinuxmacOS)和编译器(GCCClangMSVC 等),通过生成平台特定的构建系统文件,能够帮助开发者更轻松地在不同平台上编译项目。

  • 依赖管理CMake 能够自动检测并配置项目的依赖库,确保在编译过程中正确地链接外部库。例如,它可以通过 find_package 指令查找和配置第三方库。

  • 增量编译CMake 支持增量编译机制。当源代码部分更新时,它只会重新编译受影响的部分,减少不必要的编译时间。

  • 模块化与可扩展性CMake 允许用户编写模块和脚本,用于管理更复杂的项目结构。它提供了丰富的指令集来配置编译流程,并且可以通过编写自定义的模块扩展其功能。

  • 可集成性CMake 可以生成多种构建工具的配置文件,支持不同的 IDE 和构建系统;

C++工程目录设计最佳实践

一个典型的 C++ 项目目录结构应该具有清晰的分层,常见的目录和文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/project-root
├── CMakeLists.txt # 项目的主 CMake 配置文件
├── src/ # 源文件目录
│ ├── main.cpp # 主程序入口
│ ├── module1.cpp # 模块1的源文件
│ ├── module2.cpp # 模块2的源文件
├── include/ # 公共头文件目录
│ ├── module1.h # 模块1的头文件
│ ├── module2.h # 模块2的头文件
├── lib/ # 外部库(静态或动态库)存放目录
├── bin/ # 存放二进制文件
├── tests/ # 测试代码
│ ├── CMakeLists.txt # 测试目录的 CMake 文件
│ ├── test_module1.cpp # 测试模块1的单元测试
├── examples/ # 示例代码
├── docs/ # 文档(用户指南、API 文档等)
├── build/ # 构建生成目录(通常不放入源码控制系统中)
├── scripts/ # 构建、部署、运行等自动化脚本
├── README.md # 项目说明文件
└── LICENSE # 项目许可证

除此之外,仍有一些细节需要注意:

  1. src/include/ 目录分离

    • src/ 中应该存放项目的 .cpp.c 文件,即项目的具体实现代码。

    • include/则存放公共的 .h.hpp 文件,这些头文件定义了接口,并且其他项目或模块可以包含这些头文件。

    • 使用这种分离策略有助于清晰地定义哪些文件是实现细节,哪些文件是对外暴露的接口;

    • 注意在头文件中不要using namespace std;;

  2. 模块化管理

    • 对于中大型项目,将代码划分为多个逻辑模块或库会使项目更具可维护性和可扩展性;

    • 每个模块通常有自己的源文件和头文件;

    • 每个模块应该尽量自包含,模块的头文件和实现文件可以放在各自的子目录下,例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      /project-root
      ├── src/
      │ ├── module1/
      │ │ ├── module1.cpp
      │ │ └── CMakeLists.txt
      │ ├── module2/
      │ │ ├── module2.cpp
      │ │ └── CMakeLists.txt
      ├── include/
      │ ├── module1/
      │ │ └── module1.h
      │ ├── module2/
      │ │ └── module2.h
  3. 依赖管理

    • lib/ 目录:推荐通过自动化脚本或包管理工具自动下载和构建依赖,而非手动放入外部库的二进制文件;
    • third_party/ 目录:将外部依赖源代码放在 third_party/ 目录中,并使用 CMake 或其他工具集成;
  4. 测试目录结构

    • tests/ 目录应该用于存放所有的测试代码;
    • 推荐使用与项目主目录相同的模块化结构,保证测试代码清晰、独立,方便集成到 CI/CD 流程中
  5. 文档目录

    • docs/ 目录用于存放项目的文档资料,文档可以包含项目概述、API 文档、使用指南、开发者指南等。
    • 推荐使用 Doxygen并将其保存在此目录中;
  6. 构建输出目录

    • build/** 目录用于存放项目的构建输出文件(如中间文件、可执行文件等)。
    • 为了避免污染源码目录,建议将构建生成的文件放入单独的 build/ 目录中,并将此目录加入 .gitignore 中,防止其被加入版本控制;
  7. 使用脚本自动化流程

    • scripts/ 目录可以用于存放一些常用的自动化脚本,如构建、清理、测试、打包、发布等脚本。
    • 通常包括build.sh, test.sh, deploy.sh

CMake基本编译原理

CMake 的编译过程可以分为几个主要步骤:

  1. 配置阶段

    1. 读取 CMakeLists.txt 文件:
      • CMake 通过解析项目中的 CMakeLists.txt 文件来获取项目信息(源文件、依赖库、编译选项等);
    2. 生成构建系统:
      • ``CMake解析CMakeLists.txt` 后,基于当前平台和用户指定的生成器生成特定的构建系统文件这些生成的文件控制项目的编译流程;
  2. 生成阶段

    • CMake 根据配置阶段的结果,生成构建文件(如IDE工程文件),详细描述了如何从源代码生成目标文件;
    • CMake不直接编译代码,而是通过生成的构建系统文件调用具体的编译工具链来进行编译。
  3. 编译阶段

    • 用户运行生成的构建系统(例如执行 make 命令),调用编译器编译源代码,生成目标文件(如 .o 文件)。
    • 链接目标文件,生成最终的可执行文件或库文件(如 .exe.so/.dll 文件)。

简要来说,如果我的项目遵循上述的最佳实践的话,可以通过一个文件编写两条命令实现:

  • 编写CMakeLists.txt;
  • cmake .
  • cmake --build .

CMakeLists.txt编写原则

  1. 模块化:

    • 将项目配置划分为多个 CMake 文件;
    • 使用变量来避免重复代码,并通过函数/宏封装复杂的操作。
  2. 指定最低版本要求

    • 每个项目的 CMakeLists.txt 中明确指定所需的最低 CMake 版本;
  3. 项目声明

    • 定义项目名称和相关语言
  4. 管理编译选项

    • 设置编译器选项:通过 target_compile_options() 为目标指定编译器选项,而不是使用全局变量 CMAKE_CXX_FLAGS,这样可以避免全局配置导致的不必要副作用。
    • 使用 CMAKE_BUILD_TYPE:通过 CMAKE_BUILD_TYPE 设置构建模式(如 DebugRelease)。在实际项目中,建议允许用户在命令行中指定,而非硬编码在 CMakeLists.txt 中;
  5. 处理依赖项

    • 如果项目依赖于外部库,使用 find_package() 查找库,并根据需要设置是否强制要求该库;
    • 对于一些外部库,可以使用 FetchContent 下载和构建它们,避免手动配置;
  6. 管理源文件

    • 将源文件列表集中管理,并根据项目结构清晰划分。例如,可以将源文件分组,并使用 file() 或变量存储文件路径;
  7. 启用自动化测试

    • 建议为项目配置测试框架,并使用 CMake 的 enable_testing()add_test() 来集成测试
  8. 多平台支持

    • 检查系统特性:使用 if() 检查不同的平台或编译器选项,并在 CMakeLists.txt 中做出适应性调整。

    • 设置跨平台编译选项:使用 target_compile_options()target_link_libraries() 管理特定平台的编译选项;

  9. 支持安装和打包

    • 为了方便部署项目,可以在 CMakeLists.txt 中配置安装和打包规则。使用 install() 命令设置如何将生成的可执行文件、库和其他资源安装到系统路径;

CMakeLists.txt配置具体含义

指定CMake的最低版本

1
cmake_minimum_required(VERSION 3.15)

项目命名

1
project(MyDraft)

指定C++标准

1
set(CMAKE_CXX_STANDARD 20)

设置编译器路径

1
2
set(CMAKE_C_COMPILER "/usr/bin/gcc")
set(CMAKE_CXX_COMPILER "/usr/bin/g++")

确保它们只在项目第一次运行时配置, 避免重复配置,或者即使清除CMake缓存;

设置源文件目录

1
file(GLOB SRC_LIST "${PROJECT_SOURCE_DIR}/src/tb/*.cpp")

添加头文件路径

1
include_directories(include)

生成目标可执行文件

1
add_executable(tb ${SRC_LIST})

外部库的引用

fmt库

从 CMake 3.11 开始,可以使用 FetchContent 自动 在 configure 时下载 {fmt} 作为依赖项:

1
2
3
4
5
6
7
8
9
include(FetchContent)

FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt
GIT_TAG e69e5f977d458f2650bb346dadf2ad30c5320281) # 10.2.1
FetchContent_MakeAvailable(fmt)

target_link_libraries(<your-target> fmt::fmt)

注意<your-target>应填写可执行文件的名称

  • 在编译的最后一步,链接器会把编译后的目标文件(即 .o 文件或 .obj 文件)与所依赖的外部库(如静态库 .a 文件或动态库 .so/.dll 文件)链接在一起,生成最终的可执行文件。因此,target 通常是这个最终生成的可执行文件的名称;
  • 如果项目中包含多个不同的可执行文件,每个可执行文件可能会依赖不同的库。在这种情况下,你可以为每个目标定义一个不同的 target,以便生成多个不同的可执行文件;

在头文件内加入

1
2
3
4
5
6
7
8
9
10
11
12
# define FMT_HEADER_ONLY 	//  推荐
# include "fmt/core.h" // char UTF-8主要的格式化函数,支持C++20编译时检查,依赖最小化。
# include "fmt/format.h" // 完整的格式化API,除了额外的格式化函数之外,支持本地化(多语言支持)。
# include "fmt/ranges.h" // 格式化ranges 和 tuples
# include "fmt/chrono.h" // 日期和时间的格式化。
# include "fmt/std.h" // c++标准库类型的格式化支持。
# include "fmt/compile." // 格式化字符串的编译 (编译时格式化字符串检测)。FMT_STRING(s)
# include "fmt/color.h" // 端颜色和文本样式。
# include "fmt/os.h" // 提供系API。
# include "fmt/ostream." // 支持std::ostream。
# include "fmt/printf.h" // 支持printf格式化。
# include "fmt/xchar.h" // 选的wchar_t支持。

注意

相对路径问题

通常,工作目录是在命令行中运行可执行文件的地方;

如果设置bin/为输出目录,那么需要一个上级目录..才能回到项目根目录;

对于头文件的相对路径,通常会设置include_directories(include),直接引用对应模块即可;

示例

以下是一个CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 指定`CMake`的最低版本
cmake_minimum_required(VERSION 3.15)

# 项目命名
project(MyDraft)

# 指定 C++ 标准
set(CMAKE_CXX_STANDARD 20)

# 设置编译器路径
# set(CMAKE_C_COMPILER "/usr/bin/gcc")
# set(CMAKE_CXX_COMPILER "/usr/bin/g++")

# 设置可执行文件的输出目录
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")

# 添加头文件路径
include_directories(include)

# 设置源文件目录
file(GLOB SRC_LIST "${PROJECT_SOURCE_DIR}/src/hello/*.cpp")

# 生成目标可执行文件
add_executable(hello ${SRC_LIST})

# 引入外部库
include(FetchContent)

FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt
GIT_TAG e69e5f977d458f2650bb346dadf2ad30c5320281) # 10.2.1
FetchContent_MakeAvailable(fmt)

target_link_libraries(hello fmt::fmt)

我的目录树如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
.MyCMake
├── bin
│ └── hello
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ │ ├── 3.30.3
│ │ │ ├── CMakeCCompiler.cmake
│ │ │ ├── CMakeCXXCompiler.cmake
│ │ │ ├── CMakeDetermineCompilerABI_C.bin
│ │ │ ├── CMakeDetermineCompilerABI_CXX.bin
│ │ │ ├── CMakeSystem.cmake
│ │ │ ├── CompilerIdC
│ │ │ │ ├── a.out
│ │ │ │ ├── CMakeCCompilerId.c
│ │ │ │ └── tmp
│ │ │ └── CompilerIdCXX
│ │ │ ├── a.out
│ │ │ ├── CMakeCXXCompilerId.cpp
│ │ │ └── tmp
│ │ ├── cmake.check_cache
│ │ ├── CMakeConfigureLog.yaml
│ │ ├── CMakeDirectoryInformation.cmake
│ │ ├── CMakeScratch
│ │ ├── hello.dir
│ │ │ ├── build.make
│ │ │ ├── cmake_clean.cmake
│ │ │ ├── compiler_depend.make
│ │ │ ├── compiler_depend.ts
│ │ │ ├── DependInfo.cmake
│ │ │ ├── depend.make
│ │ │ ├── flags.make
│ │ │ ├── link.txt
│ │ │ ├── progress.make
│ │ │ └── src
│ │ │ └── hello
│ │ │ ├── hello.cpp.o
│ │ │ └── hello.cpp.o.d
│ │ ├── Makefile2
│ │ ├── Makefile.cmake
│ │ ├── pkgRedirects
│ │ ├── progress.marks
│ │ └── TargetDirectories.txt
│ ├── cmake_install.cmake
│ ├── _deps
│ │ ├── fmt-build
│ │ │ ├── CMakeFiles
│ │ │ │ ├── CMakeDirectoryInformation.cmake
│ │ │ │ ├── Export
│ │ │ │ │ └── b834597d9b1628ff12ae4314c3a2e4b8
│ │ │ │ │ ├── fmt-targets.cmake
│ │ │ │ │ └── fmt-targets-noconfig.cmake
│ │ │ │ ├── fmt.dir
│ │ │ │ │ ├── build.make
│ │ │ │ │ ├── cmake_clean.cmake
│ │ │ │ │ ├── cmake_clean_target.cmake
│ │ │ │ │ ├── compiler_depend.make
│ │ │ │ │ ├── compiler_depend.ts
│ │ │ │ │ ├── DependInfo.cmake
│ │ │ │ │ ├── depend.make
│ │ │ │ │ ├── flags.make
│ │ │ │ │ ├── link.txt
│ │ │ │ │ ├── progress.make
│ │ │ │ │ └── src
│ │ │ │ │ ├── format.cc.o
│ │ │ │ │ ├── format.cc.o.d
│ │ │ │ │ ├── os.cc.o
│ │ │ │ │ └── os.cc.o.d
│ │ │ │ └── progress.marks
│ │ │ ├── cmake_install.cmake
│ │ │ ├── fmt-config.cmake
│ │ │ ├── fmt-config-version.cmake
│ │ │ ├── fmt.pc
│ │ │ ├── fmt-targets.cmake
│ │ │ ├── libfmt.a
│ │ │ └── Makefile
│ │ ├── fmt-src
│ │ │ ├── ChangeLog.md
│ │ │ ├── CMakeLists.txt
│ │ │ ├── CONTRIBUTING.md
│ │ │ ├── doc
│ │ │ │ ├── api.rst
│ │ │ │ ├── basic-bootstrap
│ │ │ │ │ ├── layout.html
│ │ │ │ │ ├── README
│ │ │ │ │ └── theme.conf
│ │ │ │ ├── bootstrap
│ │ │ │ │ ├── alerts.less
│ │ │ │ │ ├── badges.less
│ │ │ │ │ ├── bootstrap.less
│ │ │ │ │ ├── breadcrumbs.less
│ │ │ │ │ ├── button-groups.less
│ │ │ │ │ ├── buttons.less
│ │ │ │ │ ├── carousel.less
│ │ │ │ │ ├── close.less
│ │ │ │ │ ├── code.less
│ │ │ │ │ ├── component-animations.less
│ │ │ │ │ ├── dropdowns.less
│ │ │ │ │ ├── forms.less
│ │ │ │ │ ├── glyphicons.less
│ │ │ │ │ ├── grid.less
│ │ │ │ │ ├── input-groups.less
│ │ │ │ │ ├── jumbotron.less
│ │ │ │ │ ├── labels.less
│ │ │ │ │ ├── list-group.less
│ │ │ │ │ ├── media.less
│ │ │ │ │ ├── mixins
│ │ │ │ │ │ ├── alerts.less
│ │ │ │ │ │ ├── background-variant.less
│ │ │ │ │ │ ├── border-radius.less
│ │ │ │ │ │ ├── buttons.less
│ │ │ │ │ │ ├── center-block.less
│ │ │ │ │ │ ├── clearfix.less
│ │ │ │ │ │ ├── forms.less
│ │ │ │ │ │ ├── gradients.less
│ │ │ │ │ │ ├── grid-framework.less
│ │ │ │ │ │ ├── grid.less
│ │ │ │ │ │ ├── hide-text.less
│ │ │ │ │ │ ├── image.less
│ │ │ │ │ │ ├── labels.less
│ │ │ │ │ │ ├── list-group.less
│ │ │ │ │ │ ├── nav-divider.less
│ │ │ │ │ │ ├── nav-vertical-align.less
│ │ │ │ │ │ ├── opacity.less
│ │ │ │ │ │ ├── pagination.less
│ │ │ │ │ │ ├── panels.less
│ │ │ │ │ │ ├── progress-bar.less
│ │ │ │ │ │ ├── reset-filter.less
│ │ │ │ │ │ ├── resize.less
│ │ │ │ │ │ ├── responsive-visibility.less
│ │ │ │ │ │ ├── size.less
│ │ │ │ │ │ ├── tab-focus.less
│ │ │ │ │ │ ├── table-row.less
│ │ │ │ │ │ ├── text-emphasis.less
│ │ │ │ │ │ ├── text-overflow.less
│ │ │ │ │ │ └── vendor-prefixes.less
│ │ │ │ │ ├── mixins.less
│ │ │ │ │ ├── modals.less
│ │ │ │ │ ├── navbar.less
│ │ │ │ │ ├── navs.less
│ │ │ │ │ ├── normalize.less
│ │ │ │ │ ├── pager.less
│ │ │ │ │ ├── pagination.less
│ │ │ │ │ ├── panels.less
│ │ │ │ │ ├── popovers.less
│ │ │ │ │ ├── print.less
│ │ │ │ │ ├── progress-bars.less
│ │ │ │ │ ├── responsive-embed.less
│ │ │ │ │ ├── responsive-utilities.less
│ │ │ │ │ ├── scaffolding.less
│ │ │ │ │ ├── tables.less
│ │ │ │ │ ├── theme.less
│ │ │ │ │ ├── thumbnails.less
│ │ │ │ │ ├── tooltip.less
│ │ │ │ │ ├── type.less
│ │ │ │ │ ├── utilities.less
│ │ │ │ │ ├── variables.less
│ │ │ │ │ └── wells.less
│ │ │ │ ├── build.py
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── conf.py
│ │ │ │ ├── contents.rst
│ │ │ │ ├── fmt.less
│ │ │ │ ├── index.rst
│ │ │ │ ├── python-license.txt
│ │ │ │ ├── _static
│ │ │ │ │ ├── bootstrap.min.js
│ │ │ │ │ ├── breathe.css
│ │ │ │ │ └── fonts
│ │ │ │ │ ├── glyphicons-halflings-regular.eot
│ │ │ │ │ ├── glyphicons-halflings-regular.svg
│ │ │ │ │ ├── glyphicons-halflings-regular.ttf
│ │ │ │ │ └── glyphicons-halflings-regular.woff
│ │ │ │ ├── syntax.rst
│ │ │ │ ├── _templates
│ │ │ │ │ ├── layout.html
│ │ │ │ │ └── search.html
│ │ │ │ └── usage.rst
│ │ │ ├── include
│ │ │ │ └── fmt
│ │ │ │ ├── args.h
│ │ │ │ ├── chrono.h
│ │ │ │ ├── color.h
│ │ │ │ ├── compile.h
│ │ │ │ ├── core.h
│ │ │ │ ├── format.h
│ │ │ │ ├── format-inl.h
│ │ │ │ ├── os.h
│ │ │ │ ├── ostream.h
│ │ │ │ ├── printf.h
│ │ │ │ ├── ranges.h
│ │ │ │ ├── std.h
│ │ │ │ └── xchar.h
│ │ │ ├── LICENSE
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ │ ├── fmt.cc
│ │ │ │ ├── format.cc
│ │ │ │ └── os.cc
│ │ │ ├── support
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ ├── Android.mk
│ │ │ │ ├── bazel
│ │ │ │ │ ├── BUILD.bazel
│ │ │ │ │ ├── README.md
│ │ │ │ │ └── WORKSPACE.bazel
│ │ │ │ ├── build-docs.py
│ │ │ │ ├── build.gradle
│ │ │ │ ├── cmake
│ │ │ │ │ ├── FindSetEnv.cmake
│ │ │ │ │ ├── fmt-config.cmake.in
│ │ │ │ │ ├── fmt.pc.in
│ │ │ │ │ └── JoinPaths.cmake
│ │ │ │ ├── compute-powers.py
│ │ │ │ ├── C++.sublime-syntax
│ │ │ │ ├── docopt.py
│ │ │ │ ├── manage.py
│ │ │ │ ├── printable.py
│ │ │ │ ├── README
│ │ │ │ ├── rtd
│ │ │ │ │ ├── conf.py
│ │ │ │ │ ├── index.rst
│ │ │ │ │ └── theme
│ │ │ │ │ ├── layout.html
│ │ │ │ │ └── theme.conf
│ │ │ │ └── Vagrantfile
│ │ │ └── test
│ │ │ ├── add-subdirectory-test
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ └── main.cc
│ │ │ ├── args-test.cc
│ │ │ ├── assert-test.cc
│ │ │ ├── chrono-test.cc
│ │ │ ├── CMakeLists.txt
│ │ │ ├── color-test.cc
│ │ │ ├── compile-error-test
│ │ │ │ └── CMakeLists.txt
│ │ │ ├── compile-fp-test.cc
│ │ │ ├── compile-test.cc
│ │ │ ├── core-test.cc
│ │ │ ├── cuda-test
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── cpp14.cc
│ │ │ │ └── cuda-cpp14.cu
│ │ │ ├── detect-stdfs.cc
│ │ │ ├── enforce-checks-test.cc
│ │ │ ├── find-package-test
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ └── main.cc
│ │ │ ├── format-impl-test.cc
│ │ │ ├── format-test.cc
│ │ │ ├── fuzzing
│ │ │ │ ├── build.sh
│ │ │ │ ├── chrono-duration.cc
│ │ │ │ ├── chrono-timepoint.cc
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── float.cc
│ │ │ │ ├── fuzzer-common.h
│ │ │ │ ├── main.cc
│ │ │ │ ├── named-arg.cc
│ │ │ │ ├── one-arg.cc
│ │ │ │ ├── README.md
│ │ │ │ └── two-args.cc
│ │ │ ├── gtest
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── gmock
│ │ │ │ │ └── gmock.h
│ │ │ │ ├── gmock-gtest-all.cc
│ │ │ │ └── gtest
│ │ │ │ ├── gtest.h
│ │ │ │ └── gtest-spi.h
│ │ │ ├── gtest-extra.cc
│ │ │ ├── gtest-extra.h
│ │ │ ├── gtest-extra-test.cc
│ │ │ ├── header-only-test.cc
│ │ │ ├── mock-allocator.h
│ │ │ ├── module-test.cc
│ │ │ ├── noexception-test.cc
│ │ │ ├── os-test.cc
│ │ │ ├── ostream-test.cc
│ │ │ ├── posix-mock.h
│ │ │ ├── posix-mock-test.cc
│ │ │ ├── printf-test.cc
│ │ │ ├── ranges-odr-test.cc
│ │ │ ├── ranges-test.cc
│ │ │ ├── scan.h
│ │ │ ├── scan-test.cc
│ │ │ ├── static-export-test
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── library.cc
│ │ │ │ └── main.cc
│ │ │ ├── std-test.cc
│ │ │ ├── test-assert.h
│ │ │ ├── test-main.cc
│ │ │ ├── unicode-test.cc
│ │ │ ├── util.cc
│ │ │ ├── util.h
│ │ │ └── xchar-test.cc
│ │ └── fmt-subbuild
│ │ ├── CMakeCache.txt
│ │ ├── CMakeFiles
│ │ │ ├── 3.30.3
│ │ │ │ └── CMakeSystem.cmake
│ │ │ ├── cmake.check_cache
│ │ │ ├── CMakeConfigureLog.yaml
│ │ │ ├── CMakeDirectoryInformation.cmake
│ │ │ ├── CMakeRuleHashes.txt
│ │ │ ├── fmt-populate-complete
│ │ │ ├── fmt-populate.dir
│ │ │ │ ├── build.make
│ │ │ │ ├── cmake_clean.cmake
│ │ │ │ ├── compiler_depend.make
│ │ │ │ ├── compiler_depend.ts
│ │ │ │ ├── DependInfo.cmake
│ │ │ │ ├── Labels.json
│ │ │ │ ├── Labels.txt
│ │ │ │ └── progress.make
│ │ │ ├── Makefile2
│ │ │ ├── Makefile.cmake
│ │ │ ├── pkgRedirects
│ │ │ ├── progress.marks
│ │ │ └── TargetDirectories.txt
│ │ ├── cmake_install.cmake
│ │ ├── CMakeLists.txt
│ │ ├── fmt-populate-prefix
│ │ │ ├── src
│ │ │ │ └── fmt-populate-stamp
│ │ │ │ ├── fmt-populate-build
│ │ │ │ ├── fmt-populate-configure
│ │ │ │ ├── fmt-populate-done
│ │ │ │ ├── fmt-populate-download
│ │ │ │ ├── fmt-populate-gitclone-lastrun.txt
│ │ │ │ ├── fmt-populate-gitinfo.txt
│ │ │ │ ├── fmt-populate-install
│ │ │ │ ├── fmt-populate-mkdir
│ │ │ │ ├── fmt-populate-patch
│ │ │ │ ├── fmt-populate-patch-info.txt
│ │ │ │ ├── fmt-populate-test
│ │ │ │ └── fmt-populate-update-info.txt
│ │ │ └── tmp
│ │ │ ├── fmt-populate-cfgcmd.txt
│ │ │ ├── fmt-populate-gitclone.cmake
│ │ │ ├── fmt-populate-gitupdate.cmake
│ │ │ └── fmt-populate-mkdirs.cmake
│ │ └── Makefile
│ └── Makefile
├── CMakeLists.txt
├── doc
│ └── CMake入门.md
├── include
│ └── hello
│ └── hello.hpp
├── lib
└── src
└── hello
└── hello.cpp
61 directories, 294 files

我编写了一个引用外部库fmt输出hello,world的程序src/hello/hello.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# include "hello/hello.hpp"

void stdSayHello(int i){
while(i--) fmt::print("hello world_{}\n", i);
}

void fmtSayHello(int i){
while(i--) std::cout<< "hello world_"<<i<<std::endl;
}

int main(){
stdSayHello(3);
fmtSayHello(3);
}

include/中,头文件include/hello/hello.hpp也很简单:

1
2
3
4
# include <iostream>

# define FMT_HEADER_ONLY // 推荐
# include "fmt/core.h"

cd build & cmake .. & cmake --build .执行便在bin/生成了可执行文件,这表明我们编译成功了!

0%