GAN系列之经典GAN(一)

reference:

https://zhuanlan.zhihu.com/p/78777020

https://zhuanlan.zhihu.com/p/28853704

GAN全称:Generative Adversarial Network 即生成对抗网络,由Ian J. Goodfellow等人于2014年10月发表在NIPS大会上的论文《Generative Adversarial Nets》中提出。此后各种花式变体Pix2Pix、CYCLEGAN、STARGAN、StyleGAN等层出不穷,在“换脸”、“换衣”、“换天地”等应用场景下生成的图像、视频以假乱真,好不热闹。前段时间PaddleGAN实现的First Order Motion表情迁移模型,能用一张照片生成一段唱歌视频。各种搞笑鬼畜视频火遍全网。用的就是一种GAN模型哦。深度学习三巨神之一的LeCun也对GAN大加赞赏,称“adversarial training is the coolest thing since sliced bread”。

对抗生成模型GAN首先是一个生成模型,和大家比较熟悉的、用于分类的判别模型不同。

判别模型的数学表示是y=f(x),也可以表示为条件概率分布p(y|x)。当输入一张训练集图片x时,判别模型输出分类标签y。模型学习的是输入图片x与输出的类别标签的映射关系。即学习的目的是在输入图片x的条件下,尽量增大模型输出分类标签y的概率。

而生成模型的数学表示是概率分布p(x)。没有约束条件的生成模型是无监督模型,将给定的简单先验分布π(z)(通常是高斯分布),映射为训练集图片的像素概率分布p(x),即输出一张服从p(x)分布的具有训练集特征的图片。模型学习的是先验分布π(z)与训练集像素概率分布p(x)的映射关系。

生成对抗网络一般由一个生成器(生成网络),和一个判别器(判别网络)组成。生成器的作用是,通过学习训练集数据的特征,在判别器的指导下,将随机噪声分布尽量拟合为训练数据的真实分布,从而生成具有训练集特征的相似数据。而判别器则负责区分输入的数据是真实的还是生成器生成的假数据,并反馈给生成器。两个网络交替训练,能力同步提高,直到生成网络生成的数据能够以假乱真,并与与判别网络的能力达到一定均衡。

GAN的本质

其实GAN模型以及所有的生成模型都一样,做的事情只有一件:拟合训练数据的分布。对图片生成任务来说就是拟合训练集图片的像素概率分布。下面我们从原理的角度演示一下GAN的训练过程:

上图中: 黑色点线为训练集数据分布曲线 蓝色点线为判别器输出的分布曲线 绿色实线为生成器输出的分布曲线 z展示的是生成器映射前的简单概率分布(一般是高斯分布)的范围和密度 x展示的是生成器映射后学到的训练集的概率分布的范围和密度 (a)判别器与生成器均未训练呈随机分布 (b)判别器经过训练,输出的分布在靠近训练集“真”数据分布的区间趋近于1(真),在靠近生成器生成的“假”数据分布的区间趋近于0(假) (c)生成器根据判别器输出的(真假)分布,更新参数,使自己的输出分布趋近于训练集“真”数据的分布。 经过(b)(c)(b)(c)…步骤的循环交替。判别器的输出分布随着生成器输出的分布与训练集分布的接近而更加平缓;生成器输出的分布则在判别器输出分布的指引下逐渐趋近于训练集“真”数据的分布。 (d)训练完成时,生成器输出的分布完美拟合了训练集数据的分布,判别器的输出由于生成器的完美拟合而无法判别生成器输出的真伪而呈一条取值约为0.5(真假之间)的直线。

GAN的组成

  1. 解读GAN的loss函数

GAN网络的训练优化目标就是如下公式:

公式出自Goodfellow在2014年发表的论文Generative Adversarial Nets。这里简单介绍下公式的含义和如何应用到代码中。上式中等号左边的部分: V(D,G)表示的是生成样本和真实样本的差异度,可以使用二分类(真、假两个类别)的交叉熵损失。

maxV(D, G)表示在生成器固定的情况下,通过最大化交叉熵损失V(D,G)来更新判别器D的参数。

min maxV(D, G)表示生成器要在判别器最大化真、假图片交叉熵损失V(D,G)的情况下,最小化这个交叉熵损失

首先固定G训练D :

1)训练D的目的是希望这个式子的值越大越好。真实数据希望被D分成1,生成数据希望被分成0。

第一项,如果有一个真实数据被分错,那么log(D(x))<<0,期望会变成负无穷大。

第二项,如果被分错成1的话,第二项也会是负无穷大。

很多被分错的话,就会出现很多负无穷,那样可以优化的空间还有很多。可以修正参数,使V的数值增大。

2)训练G ,它是希望V的值越小越好,让D分不开真假数据。

因为目标函数的第一项不包含G,是常数,所以可以直接忽略 不受影响。

对于G来说 它希望D在划分他的时候能够越大越好,他希望被D划分1(真实数据)。

第二个式子和第一个式子等价。在训练的时候,第二个式子训练效果比较好 常用第二个式子的形式。

证明V是可以收敛导最佳解的。

(1)global optimum 存在

(2)global optimum训练过程收敛

全局优化首先固定G优化D,D的最佳情况为:

1、证明D*G(x)是最优解

由于V是连续的所以可以写成积分的形式来表示期望:

通过假设x=G(z)可逆进行了变量替换,整理式子后得到:

然后对V(G,D)进行最大化:对D进行优化令V取最大

取极值,对V进行求导并令导数等于0.求解出来可得D的最佳解D*G(x)结果一样。

2、假设我们已经知道D*G(x)是最佳解了,这种情况下G想要得到最佳解的情况是:G产生出来的分布要和真实分布一致,即:

在这个条件下,D*G(x)=1/2。

接下来看G的最优解是什么,因为D的这时已经找到最优解了,所以只需要调整G ,令

对于D的最优解我们已经知道了,D*G(x),可以直接把它带进来 并去掉前面的Max

然后对 log里面的式子分子分母都同除以2,分母不动,两个分子在log里面除以2 相当于在log外面 -log(4) 可以直接提出来:

结果可以整理成两个KL散度-log(4)

KL散度是大于等于零的,所以C的最小值是 -log(4)

当且仅当

所以证明了 当G产生的数据和真实数据是一样的时候,C取得最小值也就是最佳解。

如上图所示GAN由一个判别器(Discriminator)和一个生成器(Generator)两个网络组成。

训练时先训练判别器:将训练集数据(Training Set)打上真标签(1)和生成器(Generator)生成的假图片(Fake image)打上假标签(0)一同组成batch送入判别器(Discriminator),对判别器进行训练。计算loss时使判别器对真数据(Training Set)输入的判别趋近于真(1),对生成器(Generator)生成的假图片(Fake image)的判别趋近于假(0)。此过程中只更新判别器(Discriminator)的参数,不更新生成器(Generator)的参数。

然后再训练生成器:将高斯分布的噪声z(Random noise)送入生成器(Generator),然后将生成器(Generator)生成的假图片(Fake image)打上真标签(1)送入判别器(Discriminator)。计算loss时使判别器对生成器(Generator)生成的假图片(Fake image)的判别趋近于真(1)。此过程中只更新生成器(Generator)的参数,不更新判别器(Discriminator)的参数。

判别器结构:

生成器结构:

代码实现:http://139.9.1.231/index.php/2021/12/29/gan/

数据降维方法汇总

网上关于各种降维算法的资料参差不齐,同时大部分不提供源代码。这里有个 GitHub 项目整理了使用 Python 实现了 11 种经典的数据抽取(数据降维)算法,包括:PCA、LDA、MDS、LLE、TSNE 等,并附有相关资料、展示效果。

所谓降维,即用一组个数为 d 的向量 Zi 来代表个数为 D 的向量 Xi 所包含的有用信息,其中 d<D;通俗来讲,即将高维度下降至低维度;将高维数据下降为低维数据。

降维算法资料链接代码展示
PCA资料链接1 资料链接2 资料链接3PCA
KPCA资料链接1 资料链接2 资料链接3KPCA
LDA资料链接1 资料链接2LDA
MDS资料链接1MDS 
ISOMAP资料链接1 资料链接2ISOMAP
LLE资料链接1 资料链接2LLE
TSNE资料链接1TSNE
AutoEncoder无 
FastICA资料链接1FastICA
SVD资料链接1 资料链接2SVD
LE资料链接1资料链接2LE
LPP资料链接1 资料链接2LPP

环境: python3.6 ubuntu18.04(windows10) 需要的库: numpy sklearn tensorflow matplotlib

github:https://github.com/heucoder/dimensionality_reduction_alo_codes 

c++ 复合类型

  • 1、数组

数组声明: typename arrayname[arraysize]

或者 在声明时候赋值 int x[2]={1,2};

如果只对数组的一部分进行初始化,则编译器会把其他元素设置为0,因此:

long tio[5]={10};

如果初始化时方括号内【】为空,编译器自动计算元素个数:

short totals[ ]={1,2,3}

注意:arraysize指定元素的数目 ,必须是整型常数或者const值,具体来说,arraysize不能是变量。当然可以使用new运算符来避开这种限制。

访问数组元素:arrayname[i] i从0开始,到size-1

c++11 初始化数组时,省略等号 doouble x[2] {1,2};

可以不再大括号中包含任何内容 doouble x[2] {} ,这将会把所有元素设为0

c++的标准模板库提供了一种数组替代品 -模板类vector,c++11新增了array。

  • 字符串

c++处理字符串方式有两种 一种来自c语言,另一种基于string类库的方法。

c语言风格字符串性质:以空字符结尾 ,空字符 \0,其ascii码为0.

char dog[3] ={‘b’,’e’,’\0′} //字符串

char cat[3] ={‘a’,’v’,’d’} //字符数组

char bird[11] = “mr. cheepes” //注意“”里不用显示的包括\0,但隐式的包括空字符,所以数组大小必须比实际长度大于一个以上。

使用键盘输入字符串时,将自动加上空字符

注意字符串常量(” “)不能与字符常量(’ ‘)互换。”s”表示字符串,是由’s’和’\0’组成,而’s’表示单个字符。

字符串输入:

cin>>name; cin使用空白(空格、制表符、换行符)来确定字符串结束的位置,因此cin在读取 字符串时只能读取一个单词,然后将其放到数组name中。

为了解决上面的问题,将整个一行字符作为输入:istream中cin提供了类成员函数:getline()和get(),读取输入,直到遇到换行符后才停止。区别 getline() 将丢弃换行符,get将保留换行符在输入队列中。

cin.getline(name,size)

name:存储的数组名,size:读取的字符数,如果size=20,那么最多读取19个字符,且不hi存储最后的换行符,余下空间用于自动存储\0空字符。

cin.get( name,size )

混合输入字符串和数字

  • string类

包含头文件 <string>,string类位于std命名空间中,必须使用using编译指令或者使用std::string引用它。

定义: string str1; string str2=”sssssss”;

赋值: cin>>str1;

显示:cout<<str1

通过访问数组的方法访问string ,str1[3]

string和字符数组区别:可以将 string对象声明为简单变量,而不是数组。声明时候不用指定大小。

c++11 初始化string : string one={“dddfse”}; string two{“sdhfhs”}

string赋值、拼接和附加 string str2 =str1 string str3 =str1+str2

c中ctring头文件中的函数 strcpy(chaar1,charr2) //copy charr2 to charr1 strcat(charr1,charr2) 拼接charr2到charr1中

string的输入输出 cin cout,当读取一行时,使用的方法不同:

数组字符串: cin.getline(charr,20)

string: getline(cin,str1)

  • 结构体

创建该类型变量 : inflatable hat;

可以将结构作为参数传递给函数,也可以让函数返回一个结构,可以使用 = 将一个结构赋给同类型的结构。

可以在定义结构体同时创建结构变量,只需要将变量名放在定义 结构的括号后面。

结构数组

  • 共用体 union

共用体是一种数据格式,能够存储不同的数据类型 ,但只能同时同时存储其中的一种类型。

声明 和struct类似

  • 枚举 enum ,可以替代 const,还允许定义新类型

enum spectrum {red,orange,yellow,green,blue,violet,indigo,ultraviolet};

默认情况下,将整数值赋给枚举变量 ,第一个枚举值为0,以此类推。当然也可以显式指定整数来覆盖默认值。

枚举只有 赋值运算,没有算数运算。

如果使用 整数:需要强制类型转换 枚举名(整数)

设置枚举值

  • 指针、数组

地址运算符 &变量 获得该变量的地址

指针:用于存储值的地址,使用*运算符,可以得到该地址处的值,*被称为间接值运算符,即 x是一个地址,*x时该地址的数值

可以在命名的同时赋值:

int *p

声明一个指针:

int *p or int* p or int*p

可以在声明的同时初始化: int * p= &x

注意:初始化的是 p,而不是*p

new运算符

delete 释放内存

int *p = new int

delete p

注意:只能用delete删除new分配的内存

使用 new来新建动态数组

指针小结

  • 数组名是一个指针,指向的是数组array[0]
  • 将&用于数组名时候,获得的是整个数组的内存地址,因此需要取 &array[0]作为数组指针的地址

指针和字符串

C++处理字符串有两种方式,即:指针方式和数组方式

数组方式:char a[] = “HelloWorld”;
指针方式:const char* s = “HelloWorld”; const可以忽略
接下来详细讲解一下字符串指针

首先,为什么字符串可以直接赋值给指针,即char* s = “HelloWorld”不会报错,不应该是把字符串的地址赋值给指针吗?

原因:这里的双引号做了3件事:

1.申请了空间(在常量区),存放了字符串

  1. 在字符串尾加上了’/0′
    3.返回地址
    为什么字符串指针的指针名输出字符串内容而不是地址?

字符串指针的指针名代表字符串的首地址,但输出字符串指针名时输出的却是完整字符串,如下:

char* s = "HelloWorld";
cout<<s<<endl; //s是字符串的首地址,但却输出HelloWorld
cout<<*s<<endl;  //输出H
cout<<*(s+1)<<endl;  //输出e,s+1是第二个字符的地址
    cout <<static_cast<void *>(s) << endl; //此时输出的才是字符串地址

原因是C++标准库中I/O类对<<操作符重载,在遇到字符型指针时会将其当作字符串名来处理,输出指针所指的字符串。既然这样,那么我们就别让它知道那是字符型指针,所以得用到强制类型转换,用static_cast把字符串指针转换成无类型指针

字符串指针指向的地址可以修改,但所指向的字符串内容不能修改,因为字符串常量是不能改变的

char* s = "HelloWorld";
s="abcd"; //合法
cout<<*(s+1)<<endl;
*(s+1)='d'; //不合法,这里虽然没报错,但这一句实际下一句并未执行
cout<<s<<endl; //未执行

字符串指针数组:

char *p[6]={"ABCD","EFGH","IJKL","MNOP"};
int i;
for(i=0;i<4;i++) 
    cout<<p[i]<<endl;  //输出每个字符串,实际上p[i]为第i个字符串的首地址
for(i=0;i<4;i++) 
    cout<<*p[i];  //输出每个字符串第一个字符AEIM
cout<<endl;
for(i=0;i<4;i++) 

cout<<*(p[i]+1); //输出每个字符串第二个字符BFJN

C++中使用char*定义字符串,不能改变字符串内的字符的内容,但却可以把另外一个字符串(新地址)赋值给它,即p1是一个char型指针变量,其值(指向)可以改变;此时,若指向的新地址为字符串数组的地址,则可更改字符串中的内容

使用new创建动态结构:

类型组合

模板类 vector 和 array

AXI4协议

ZYNQ将高性能ARM Cotex-A系列处理器与高性能FPGA在单芯片内紧密结合,为设计带来了如减小体积和功耗、降低设计风险,增加设计灵活性等诸多优点。在将不同工艺特征的处理器与FPGA融合在一个芯片上之后,片内处理器与FPGA之间的互联通路就成了ZYNQ芯片设计的重中之重。如果Cotex-A9与FPGA之间的数据交互成为瓶颈,那么处理器与FPGA结合的性能优势就不能发挥出来。

AXI的英文全称是Advanced eXtensible Interface,即高级可扩展接口,它是ARM公司所提出的AMBA(Advanced Microcontroller Bus Architecture)协议的一部分

AXI协议就是描述主设备和从设备之间的数据传输方式,在该协议中,主设备和从设备之间通过握手信号建立连接。

AXI协议是一种高性能、高带宽、低延迟的片内总线,具有如下特点:
1、总线的地址/控制和数据通道是分离的;
2、支持不对齐的数据传输;
3、支持突发传输,突发传输过程中只需要首地址;
4、具有分离的读/写数据通道;
5、支持显着传输访问和乱序访问;
6、更加容易进行时序收敛。

在数字电路中只能传输二进制数0和1,因此可能需要一组信号才能高效地传输信息,这一组信号就组成了接口。AXI4协议支持以下三种类型的接口:
1、 AXI4:高性能存储映射接口。
2、 AXI4-Lite:简化版的AXI4接口,用于较少数据量的存储映射通信。
3、 AXI4-Stream:用于高速数据流传输,非存储映射接口。

在这里我们首先解释一下存储映射(Meamory Map)这一概念。如果一个协议是存储映射的,那么主机所发出的会话(无论读或写)就会标明一个地址。这个地址对应于系统存储空间中的一个地址,表明是针对该存储空间的读写操作。

AXI4协议支持突发传输,主要用于处理器访问存储器等需要指定地址的高速数据传输场景。AXI-Lite为外设提供单个数据传输,主要用于访问一些低速外设中的寄存器。而AXI-Stream接口则像FIFO一样,数据传输时不需要地址,在主从设备之间直接连续读写数据,主要用于如视频、高速AD、PCIe、DMA接口等需要高速数据传输的场合。

AXI4:高性能存储映射接口

AXI4接口,它由五个独立的通道构成

1、 读地址
2、 读数据
3、 写地址
4、 写数据
5、 写响应
下面是使用读地址和读数据通道实现读传输过程的示意图:

从图 15.1.1中可以看到,在一个读传输过程中,主机首先在读地址通道给出读地址和控制信号,然后从机由读数据通道返回读出的数据。另外我们需要注意的是,这是一次突发读操作,主机只给出一个地址,从该地址连续突发读出四个数据。

写传输过程如图 15.1.2所示,它用到了写地址、写数据和写响应三个通道。主机在写地址通道给出写地址和控制信号,然后在写数据通道连续突发写四个数据。从机在接收数据之后,在写响应通道给出响应信号。

AXI总线中的每个通道都包含了一组信息信号,还有一个V ALID和一个READY信号。V ALID信号由源端(source)产生,表示当前地址或者数据线上的信息是有效的;而READY信号由目的端(destination)产生,则表示已经准备好接收地址、数据以及控制信息。VALID和READY信号提供了AXI总线中的握手机制,如下图所示:

ACLK为时钟信号,在AXI协议中,所有的输入信号都在是ACLK的上升沿采样,所
有的输出信号必须在ACLK的上升沿之后才能改变。在T1之后,源端将V ALID拉高,表明INFORMA TION信号线上传输的是有效的地址、数据或者控制信息。目的端在T2之后将READY拉高,表明它已经准备好接收数据,此时源端必须保持INFORMA TION数据稳定不变,直到T3时刻进行数据传输。
需要注意的是,源端不允许等目的端的READY信号拉高之后,才将V ALID信号置为有效状态。而且,一旦V ALID拉高,源端必须保持其处于有效状态,直至成功握手(在时钟上升沿检测到V ALID和READY同时为有效状态)。

接下来通过自定义一个AXI4接口的IP核,通过AXI_HP接口对PS端DDR3进行读写测试。

我们在PL内自定义的DDR3 Test IP核作为主设备,通过PS AXI_HP0接口,与DDR控制器进行通信,最终对DDR3存储器进行读写操作。

SDK

1  #include <stdio.h> 
2  #include "xil_cache.h" 
3  #include "xil_printf.h" 
4  #include "xil_io.h" 
5   
6  int main)() 
7  { 
8      int i; 
9      char c; 
10  
11     Xil_DCacheDisable)(); 
12     print("AXI4 PL DDR TEST!\n\r";); 
13  
14     hlwhile(1{){ 
15         scanf("%c"&,&c;); 
16         fif(c==='c'{){ 
17             printf("start\n\r";); 
18             ofor(i=0;i<4096;i=i+4{){ 
19                 printf("%d is %d\n",i(,(int)Xil_In32(0x10000000+i))); 
20             } 
21         } 
22     } 
23  
24     eunreturn 0; 
25 } 

AXI4-Stream协议

AXI4-Stream协议一般被翻译为AXI流协议,是AXI总线的一种演化版本。AXI4流协议作为一个标准接口,用于连接进行数据交换的组件。接口可以用来连接一个单一的主机,主机向接收数据的单一从机发送数据,也可用于连接若干个主机和从机的组件。协议支持共用一组信号线的多个数据流,允许构建一个通用互联。相比于AHB/APB,AXI流协议提出了数据包、数据帧以及传输操作等概念,这也是其被称为流(Stream)的原因。
关于AXI Stream的基本概念解释如下:
传输(Transfer):通过 AXI4 流接口进行的一个单一数据传输。一个单一数据传输由TV ALID和TREADY握手信号定义。
包(Packet):通过 AXI4 流接口被一起传输的一组字节,包类似于 AXI4 的突发。
帧(Frame):一个 AXI4 流中最高级别的字节编组。一帧可以包含很大数量的字节数,例如,一个完整的视频帧缓存。
数据流(Data Stream):从一个源设备到一个目标设备传输的数据。
两个模块之间进行数据传输,需要事先约定好这两个模块之间的传输协议,这是两个信号握手的概念。TV ALID和TREADY信号的握手包含三种情况:TV ALID先于 TREADY 的握手、TREADY先于 TV ALID的握手、TV ALID 和 TREADY 同时发生的握手。

注意该协议中使用上升沿采样
下图中,主机发出了数据和控制信息并将TV ALID 信号置为高。一旦主机驱动了 TV ALID ,主机发出的数据或控制信息必须保持不变,直到从机驱动 TREADY 信号为高表示可以接收数据和控制信息。在这种情况下,一旦从机设置 TREADY 为高,传输就会发生。箭头标示出了传输发生的位置。

下图中,从机在数据和控制信息有效之前驱动TREADY为高。这表示目标设备可以在一个ACLK周期内接收数据和控制信息。在这种情况下,一旦主机驱动 TV ALID 为高,则传输就会发生。箭头标示出了传输发生的位置。

下图中,主机驱动TV ALID为高,从机在同一时钟(ACLK)周期内也驱动TREADY为高。在这种情况下,如图中箭头标注,传输在同一周期内发生。

本次实验我们需要使用Vivado HLS工具生成带有AXI4-Stream接口的IP核,并将此IP核的AXI4-Stream接口连接到“AXI4-Stream to Video Out”模块中的AXI4-Stream接口,如下图所示:

我们重点关注图中的“s_axis_video_tlast”和“s_axis_video_tuser” 信号,其中“s_axis_video_tlast”是AXI4-Stream协议中“TLAST”信号,这个信号设置为高表示一行像素传输结束,“s_axis_video_tuser”是AXI4-Stream协议中的“TUSER”信号,这个信号设置为高表示一帧图像传输开始。时序图如下图所示:

图中的“EOL”表示“End of line”是行传输结束信号,它在一行图像像素传输结束的时候拉高一个时钟周期;图中的“SOF”表示“Start of frame”是帧传输开始信号。它在一帧图像像素传输开始的时候拉高一个时钟周期。

c++ 数据类型

本文主要关于数据类型。面向对象编程的本质就是设计并扩展自己的的数据类型。

2024年 9月
 1
2345678
9101112131415
16171819202122
23242526272829
30  

首先了解c++的内置数据类型 :基本类型和复合类型

基本类型:整形和浮点型 复合类型:数组、指针、字符串、结构 存储数据的方法:变量

  • 简单变量

变量命名规则:

如果想用多个单词组成一个名称,通常使用下划线字符将单词分开,如 my_onions,或者 从第二个单词开始将每个单词的第一个字母大写:myEyeTooth

  • 整型

不含小数的数字 0,-3 ,100。不同的整型使用不同的内存来存储整数。有符号和无符号类型分别表示正负数和正数

short int long longlong 通过不同数目的位存储值:(都是有符号数)

short 至少16位 :short x (short== short int)

int 至少与short一样长

long 至少32位,且至少与int一样长 (long == long int)

lonng long 之少64位,且至少与long一样长

sizeof 运算符,获得变量的所占字节,对于类型名 int等使用时,需要加括号: sizeof (int),如果是对于变量,可加可不加。

#include<iostream>
using  namespace std;
int main(){
    int x_collec =2;
    cout<<"x_collec is"<<sizeof(x_collec)<< endl;
}

初始化:

int year =2022 //如果知道变量初始值,建议定义时候赋初值。

c++11 初始化方法:将大括号用于单值变量,采用这种方法时候,=可以去掉

int x={3} or int x{3}

大括号中不含值默认为0 int z{}

头文件climits

climits定义了符号常量来并表示类型的限制: int n =INT_MAX;

  • 无符号类型

要创建无符号类型,只需要使用unsigned 来修改变量声明。

unsigned   short  x  
unsigned   int  x
unsigned   long  x       
  • char 类型 (也是整型)

char用于存储字符(字母和数字) char x =”M” ,实际上,计算机中存储的是对应的字符编码77,可以将x =x+1,char值位78,对应N,可以通过 (int)x强制转换为78

有些字符不能通过键盘直接输入,比如换行符不能用回车,因此,有了下面的转义字符:

char 占8bit,unsigned char 表示范围0-255, signed char 表示范围-128~127

c++11新增 char16_t char32_t, char16_t 无符号16位, char32_t 32位有符号数,使用前缀u表示 char16_t 类型的字符常量和字符串常量, 使用前缀U表示 char32_t 类型的字符常量和字符串常量 : char16_t ch=u’q’;

  • bool类型

布尔值 true or false,将非 0值解释为true,将0解释为false。字面值true和false都可以通过提升转换(不用显式强制转换)为 int类型,true转换为1,false转换为0。

  • const限定符

常量被初始化后就不能修改了 const int year =2022

const type name =value

浮点数

能够表示 带小数部分的数字

书写浮点数:

1、标准写法 12.34 22.3 0.12 8.0

2、E表示法 3.45E6 指的是3.45与1000000相乘结果,E6指的是10的6次方,6是指数,指数可以是正数也可以是负数。E可以写成e。

  • 浮点类型

三种:float 32位 double 64位 long double 128位,浮点数有精度限制。

float 只能保证6位精确位,double保证13位精确度。

cout所属的ostream类有一个类成员函数,能够精确的控制输出格式-字段宽度、小数位数、采用小数格式还是E格式等。后面会给出实现。

在程序中使用 浮点常量时候,默认会认为是double型,如果要指定类型,在常量后加后缀:

1.23f —-float型

1.23L —–long double

1.23 —-默认double 类型

c++ 算数运算符

加、减、乘、除、求模

除法:如果两个整数相除,结果会是一个整数(小数部分直接舍去),如果两个数中 有一个或两个是浮点数,则小数部分会被保留。(因为系统会将不同操作数进行自动准换成相同的类型)

类型转换

1、初始化和赋值进行的转换

比如赋值时 double x = 3.14f 将一个float型付给double ,如果将double付给float变量,可能会导致降低精度。int x =3.14f 最终x=3(直接丢弃小数部分)

0赋值给bool,会转换为false,非0值会变为true

2、算数运算时

变量提升:在计算表达式时c++将 bool、char、unsigned char 、signed char 和short 转换为int。

3、传递参数时转换

4、强制类型转换

首先要明确一点强制转换不会修改变量本身 ,而是创建一个新的、指定类型的值。

以下两种方法都可以:

(long) x 或者 long (x)

(typename) value type (value)

c++还引入了强制类型转换运算符: static _cast<typename> (value)

C++中的auto声明

c++11新增了auto,让编译器能够根据初始值类型推断变量类型。

typedef

C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。

typedef unsigned char BYTE;
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写
https://pixabay.com/photos/nature-winter-tree-season-outdoors-6891549/

c++ 入门

本科大一的时候学习过c++,但因为后来大部分项目都是用python,所以基本上都还给老师了,但其实回过头发现,很多python开源库都是用c++写的,像opencv,因此很有必要去在回顾一下子c++的基本概念。

2024年 9月
 1
2345678
9101112131415
16171819202122
23242526272829
30  
  1. c++注释

以双斜杠开头: //这是一行注释,c++也能识别c注释,c注释包括在符号/* */之间,可以跨越多行。

#include <iostream>  
int main (){  
#c++ 例子  
    usinng namespace std;  
    cout<<"hello world"  
    cout<<endl;  
    return 0  
}  
  1. 预处理器和iostream

#include <iostream>

  1. 头文件和命名空间

如果使用iostream,而不是iostream.h,则应使用下面的名称空间编译指令来使iostream中的定义对程序可用: using namespace std;

命名空间作用:假如两个封装好的库,都有名为cout的函数,那么在调用cout时,编译器不知道是哪个函数,因此可以把某个库中函数定义到一个命名空间,就可以通过 std:cout (命名空间:函数名)调用,此外,这样写比较麻烦,还可以使用using,而不必使用std前缀 : using namespace std ;使得std中所有名称可用。在大型工程中,一般使用:using std:cout; using std:cin;单独定义所需的函数

  1. 输入输出

cout<<“hello” 和 cin<<a cout 还可以拼接 cout<<“s”<<“v”<<endl;

endl 是一个特殊的c++符号,表示换行,此外还可以使用c中的\n换行符 cout<<“hello \n”

  1. 声明语句和变量

int carrots; 这条语句声明了需要的内存和内存单元名称.为什么需要声明变量:如果不显示的声明,那么当我们在多次使用 carrots 变量时候,如果中间有个写错了 carrot ,系统不会报错,而是认为这是一个新的变量。

变量赋值 a=3

  1. 函数

type functionname(argumentlist){ statements }

函数头: type functionname(argumentlist) ,函数中可以使用using编译指令,起作用范围为函数内部。

如果using 放置在函数定义之前,文件中所有的函数都可以使用std中的元素,using放在特定函数中,则该函数能使用

PS和PL的交互方式汇总

最近有个项目关于FPGA加速神经网络,因此需要了解相关PS和pl交互的方法,从而将权重数据存放在PS的DDR中 ,并在需要时完成数据的存入和读出。这是一篇归纳性的文章,具体的还需要自行查阅资料。

https://wallhaven.cc/

PS-PL数据交互方式

1、IO


个数
分布控制
MIO54BANK0, 1PS直接控制
EMIO64BANK2, 3需要PL配置引脚
GPIOAXI-GPIO

MIO :ZYNQ 分为 PS 和 PL 两部分,那么器件的引脚(Pin)资源同样也分成了两部分。ZYNQ PS 中的外设可以通过 MIO(Multiuse I/O,多用输入/输出)模块连接到 PS 端的引脚上,也可以通过 EMIO 连接到 PL 端的引脚。Zynq-7000 系列芯片一般有 54 个 MIO,个别芯片如 7z007s 只有 32 个。

EMIO : PS 和外部设备之间的通信主要是通过复用的输入/输出(Multiplexed Input/Output,MIO)实现的。除此之外,PS 还可以通过扩展的 MIO(Extended MIO,EMIO)来实现与外部设备的连接。EMIO 使用了 PL 的I/O 资源,当 PS 需要扩展超过 54 个引脚的时候可以用 EMIO,也可以用它来连接 PL 中实现的 IP 模块。

在大多数情况下,PS 端经由 EMIO 引出的接口会直接连接到 PL 端的器件引脚上,通过 IO 管脚约束来指定所连接 PL 引脚的位置。通过这种方式,EMIO 可以为 PS 端实现额外的 64 个输入引脚或 64 个带有输出使能的输出引脚。EMIO 还有一种使用方式,就是用于连接 PL 内实现的功能模块(IP 核),此时 PL 端的 IP 作为 PS 端的一个外部设备。(EMIO既可以将ps和pl端的引脚相连,也可以和pl中的模块相连)

PS 与 PL 最主要的连接方式则是一组 AXI 接口。AXI 互联和接口作为 ZYNQ PS 和 PL 之间的桥梁,能够使两者协同工作,进而形成一个完整的、高度集成的系统。

GPIO : AXI GPIO IP 核为 AXI 接口提供了一个通用的输入/输出接口。与 PS 端的 GPIO 不同,AXI GPIO 是一个软核(Soft IP),即 ZYNQ 芯片在出厂时并不存在这样的一个硬件电路,而是由用户通过配置 PL 端的逻辑资源来实现的一个功能模块。而 PS 端的 GPIO 是一个硬核(Hard IP),它是一个生产时在硅片中实现的功能电路。 AXI GPIO IP 模块的左侧实现了一个 32 位的 AXI4-Lite 从接口,用于主机访问 AXI GPIO 内部各通道的寄存器。

AXI GPIO 框图

中断

参考:

https://blog.csdn.net/wangjie36/article/details/116081755

中断是一种当满足要求的突发事件发生时通知处理器进行处理的信号。中断可以由硬件处理单元和外部设备产生,也可以由软件本身产生。对硬件来说,中断信号是一个由某个处理单元产生的异步信号,用来引起处理器的注意。对软件来说,中断还是一种异步事件,用来通知处理器需要改变代码的执行,不过,轮询所产生的中断的过程是同步的。

Zynq 芯片的 PS 部分是基于使用双核 Cortex-A9 处理器和 GIC pl390 中断控制器的 ARM 架构。中断结构与 CPU 紧密链接,并接受来自 I/O 外设(IOP)和可编程逻辑(PL)的中断。

中断控制器架构图

ZYNQ CPU 软件中断(SGI,Software generatedinterrupts):ZYNQ 共有两个 CPU,每个 CPU 具备各自的 16 个软件中断(中断号0-15)(16–26 reserved):被路由到一个或者两个CPU上,通过写ICDSGIR寄存器产生SGI。

CPU私有外设中断(PPI,private peripheralinterrupts ):私有中断是固定的不能修改。这里有 2 个 PL 到 CPU 的快速中断 nFIQ(中断号27-31):每个CPU都有一组PPI,包括全局定时器、私有看门狗定时器、私有定时器和来自PL的FIQ/IRQ。

ZYNQ PS 和 PL 共享中断(SPI,shared peripheralinterrupts):共享中断就是一些端口共用一个中断请求线:(中断号32-95)。由PS和PL上的各种I/O控制器和存储器控制器产生,这些中断信号被路由到相应的CPU, PL部分有16个共享中断,它们的触发方式可以设置。

FIFO

https://zhuanlan.zhihu.com/p/47847664

FIFO类型读接口写接口
AXI Data FIFOAXI4-fullAXI4-full
AXI-Stream FIFOPS axi4-litePL axi-stream
AXI4-Stream Data FIFOaxi-streamaxi-stream

通过AXI-Stream FIFO完成PS和PL部分的数据交互

  • S_AXI, PS读写FIFO数据接口
  • AXI_STR_TXC, 发送控制端口
  • AXI_STR_TXD,发送数据端口
  • AXI_STR_RXD,接收数据端口

读写fifo例程:

写fifo

//write fifo us1
always@(posedge  wrclk, negedge sys_reset_n_i)
begin
    if (!sys_reset_n_i)
    begin
        fifo_wrreq_ddr3_us <= 0 ;
        fifo_data_ddr3_us <= 0 ;
    end
    else
    begin
        if(fifo_prog_full_ddr3_us!= 1) 
            fifo_wrreq_ddr3_us <= 1 ;
        else
            fifo_wrreq_ddr3_us <= 0 ;        
        if(fifo_wrreq_ddr3_us == 1) 
         begin            
             if(fifo_data_ddr3_us < 64'b1111_1111_1111_1111_1111_1111)
                fifo_data_ddr3_us <=fifo_data_ddr3_us + 1 ;                  
             else 
                 fifo_data_ddr3_us <= 0  ; 
         end  
         else
             fifo_data_ddr3_us <= fifo_data_ddr3_us ;                
    end
end
endmodule

读fifo

assign fifo_rdreq_ddr3_ds = !fifo_empty_ddr3_ds;

always@(posedge sys_clk_i,negedge sys_reset_n_i)
begin
    if(!sys_reset_n_i)
         rd_ck_flag_cp<= 1'b0;
    else
    begin
        if (fifo_q_ddr3_ds_r!==fifo_q_ddr3_ds)
            rd_ck_flag_cp<= 1'b1;  
        else
            rd_ck_flag_cp<= 1'b0;
    end    
end
 //jiao yan cuo wu ji shu    jiao yan wei zi zeng       
always@(posedge sys_clk_i,negedge sys_reset_n_i)
begin
    if(!sys_reset_n_i)
    begin
         rd_ck_cnt <= 64'b0;
         fifo_q_ddr3_ds_r <= 64'b0 ;  
    end
    else  
    begin
        if(rd_ck_flag_cp==1)
            rd_ck_cnt <=rd_ck_cnt+1'b1;
        else
            rd_ck_cnt <= rd_ck_cnt;
             
         if ( ( fifo_rdreq_ddr3_ds==1 ) && (fifo_q_ddr3_ds_r < 64'b1111_1111_1111_1111_1111_1111) )
            
             fifo_q_ddr3_ds_r <= fifo_q_ddr3_ds_r + 1'b1 ; 
         else
             fifo_q_ddr3_ds_r <= 64'b0; 
     end
end

endmodule

BRAM

在 ZYNQ SOC 开发过程中,PL 和 PS 之间经常需要做数据交互。对于传输速度要求较高、数据量大、地址连续的场合,可以通过 AXI DMA 来完成。而对于数据量较少、地址不连续、长度不规则的情况,此时 AXIDMA 便不再适用了。针对这种情况,可以通过 BRAM 来进行数据的交互。

BRAM(Block RAM)是 PL 部分的存储器阵列,PS 和 PL 通过对 BRAM 进行读写操作,来实现数据的交互。在 PL 中,通过输出时钟、地址、读写控制等信号来对 BRAM 进行读写操作;而在 PS 中,处理器并不需要直接驱动 BRAM 的端口,而是通过 AXI BRAM 控制器来对 BRAM 进行读写操作。AXI BRAM 控制器是集成在 Vivado 设计软件中的软核,可以配置成 AXI4-lite 接口模式或者 AXI4 接口模式。

AXI4 接口模式的 BRAM 控制器支持的数据位宽为 32 位、64 位、128 位、512 位和 1024 位,而 AXI4-Lite 接口仅支持 32 位数据位宽。由图 14.1.1 可知,PS 通过 AXI4-Lite 接口访问 BRAM,当使能 ECC 选项时,ECC 允许 AXI 主接口检测和纠正 BRAM 块中的单位和双位错误。AXI BRAM 控制器作为 AXI 总线的从接口,和 AXI 主接口实现互联,来对 BRAM 进行读写操作。针对不同的应用场合,该 IP 核支持单次传输和突发传输两种方式。

PS 端的 M_AXI_GP0 作为主端口,与 PL 端的 AXI BRAM 控制器 IP 核和 PL 读 BRAMIP 核(pl_bram_rd)通过 AXI4 总线进行连接。其中,AXI 互联 IP(AXI Interconnect)用于连接 AXI 存储器映射(memory-mapped)的主器件和从器件;AXI BRAM 控制器作为 PS 端读写 BRAM 的 IP 核;PL 读BRAM IP 核是我们自定义的 IP 核,实现了 PL 端从 BRAM 中读出数据的功能,除此之外,PS 端通过 AXI总线来配置该 IP 核读取 BRAM 的起始地址和个数等。

DMA

DMA(Direct Memory Access,直接存储器访问)是计算机科学中的一种内存访问技术。它允许某些计算机内部的硬件子系统可以独立地直接读写系统内存,而不需中央处理器(CPU)介入处理。DMA 是一种快速的数据传送方式,通常用来传送数据量较多的数据块,很多硬件系统会使用 DMA,包括硬盘控制器、绘图显卡、网卡和声卡,在使用高速 AD/DA 时使用 DMA 也是不错的选择。DMA 是用硬件实现存储器与存储器之间或存储器与 I/O 设备之间直接进行高速数据传输。使用 DMA时,CPU 向 DMA 控制器发出一个存储传输请求,这样当 DMA 控制器在传输的时候,CPU 执行其它操作,传输操作完成时 DMA 以中断的方式通知 CPU。

为了发起传输事务,DMA 控制器必须得到以下数据:
• 源地址 — 数据被读出的地址
• 目的地址 — 数据被写入的地址
• 传输长度 — 应被传输的字节数

DMA 存储传输的过程如下:

  1. 为了配置用 DMA 传输数据到存储器,处理器发出一条 DMA 命令
  2. DMA 控制器把数据从外设传输到存储器或从存储器到存储器,而让 CPU 腾出手来做其它操作。
  3. 数据传输完成后,向 CPU 发出一个中断来通知它 DMA 传输可以关闭了。

ZYNQ 提供了两种 DMA,一种是集成在 PS 中的硬核 DMA,另一种是 PL 中使用的软核 AXI DMAIP。在 ARM CPU 设计的过程中,已经考虑到了大量数据搬移的情况,因此在 CPU 中自带了一个 DMA 控制器 DAMC,这个 DAMC 驻留在 PS 内,而且必须通过驻留在内存中的 DMA 指令编程,这些程序往往由CPU 准备,因此需要部分的 CPU 参与。DMAC 支持高达 8 个通道,所以多个 DMA 结构的核可以挂在单个DMAC 上。DAMC 与 PL 的连接是通过 AXI_GP 接口,这个接口最高支持到 32 位宽度,这也限制了这种模式下的传输速率,理论最高速率为 600MB/s。这种模式不占用 PL 资源,但需要对 DMA 指令编程,会增加软件的复杂性。为了获取更高的传输速率,可以以空间换时间,在 PL 中添加 AXI DMAIP 核,并利用 AXI_HP 接口完成高速的数据传输。

为了获取更高的传输速率,可以以空间换时间,在 PL 中添加 AXI DMAIP 核,并利用 AXI_HP 接口完成高速的数据传输,通过 PL 的 DMA 和 AXI_HP 接口的传输适用于大块数据的高性能传输,带宽高。各种接口方式的比较如下表所示:

ZYNQ 开发板上使用 PL 的 AXI DMA IP 核从 DDR3 中读取数据,并将数据写回到 DDR3 中。

在实际应用中,DMA 一般与产生数据或需求数据的 IP 核相连接,该 IP 核可以是带有 Stream 接口的高速的 AD(模拟转数字)或 DA(数字转模拟) IP 核。不失一般性,在本次实验中,我们使用 AXI4 Stream Data FIFO IP 核来充当这类 IP 进行 DMA 环回实验。

AXI Direct Memory Access重要端口说明:

  • S_AXI_LITE: 配置DMA工作模式
  • M_AXI_MM2S:DDR到DMA数据接口
  • M_AXI_S2MM:DMA数据到DDR接口
  • S_AXIS_S2MM: 接收的DMA数据输出端口
  • M_AXIS_MM2S: 想通过DMA输出的数据写入端口

DDR3

通过对AXI HP接口的操作来实现

AXI 的英文全称是 Advanced eXtensible Interface,即高级可扩展接口,它是 ARM 公司所提出的 AMBA(Advanced Microcontroller Bus Architecture)协议的一部分。

AXI4 协议支持以下三种类型的接口:
1、 AXI4:高性能存储映射接口。
2、 AXI4-Lite:简化版的 AXI4 接口,用于较少数据量的存储映射通信。
3、 AXI4-Stream:用于高速数据流传输,非存储映射接口。

AXI4 协议支持突发传输,主要用于处理器访问存储器等需要指定地址的高速数据传输场景。AXI-Lite为外设提供单个数据传输,主要用于访问一些低速外设中的寄存器。而 AXI-Stream 接口则像 FIFO 一样,数据传输时不需要地址,在主从设备之间直接连续读写数据,主要用于如视频、高速 AD、PCIe、DMA 接口等需要高速数据传输的场合。

读传输

AXI 总线中的每个通道都包含了一组信息信号,还有一个 VALID 和一个 READY 信号,VALID 信号由源端(source)产生,表示当前地址或者数据线上的信息是有效的;而 READY 信号由目的端(destination)产生,则表示已经准备好接收地址、数据以及控制信息。

通过自定义一个 AXI4 接口的 IP 核,通过 AXI_HP 接口对 PS 端 DDR3 进行读写测试。

AXI_HP总线:只能单向传输,从PL到PS端,适用于大数据传输。

系统图

PL实现AXI4接口,通过S_AXI_HP接口读取ps侧DDR3数据. 例程功能:PL,PS向指定地址写数据,对方来读.

AXI-DMA:实现从PS内存到PL高速传输高速通道AXI-HP<—->AXI-Stream的转换
AXI-Datamover:实现从PS内存到PL高速传输高速通道AXI-HP<—->AXI-Stream的转换,只不过这次是完全由PL控制的,PS是完全被动的。
AXI-VDMA:实现从PS内存到PL高速传输高速通道AXI-HP<—->AXI-Stream的转换,只不过是专门针对视频、图像等二维数据的。
AXI-CDMA IP: 这个是由PL完成的将数据从内存的一个位置搬移到另一个位置,无需CPU来插手。这个和我们这里用的Stream没有关系

自定义AXI接口IP

一般应用场景在于PS对某些寄存器的配置,传输少量的数据信息。

少年游

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。 《唐多令》宋·刘过

少年偏爱摇摇欲坠的日落黄,殊不知此刻正拥有的,是一生中最明媚的曙光。

春日游,杏花吹满头。陌上谁家年少,足风流。

韶华不为少年留,恨悠悠,几时休。

可是我现在依然不太会转弯
虽然孤单的人偶尔也想有个伴
冷风又吹的时候想说
这生活会不会有点难
难道是因为当初有话没讲完
堵在喉咙里却始终不敢大声喊
算了 别哭 ————-毛不易《呓语》

HLS打包ip报错解决方法:

在HLS进行ip核打包的时候,出现了Vivado fails to export IPs with the error message “Bad lexical cast: source type value could not be interpreted as target”错误 ,在xilinx官网找到了解决办法:

以下是原帖

https://support.xilinx.com/s/question/0D52E00006uxy49SAA/vivado-fails-to-export-ips-with-the-error-message-bad-lexical-cast-source-type-value-could-not-be-interpreted-as-target?language=en_US

https://support.xilinx.com/s/question/0D52E00006uxnnFSAQ/2022-timestamp-overflow-error-2201011128-is-an-invalid-argument-please-specify-an-integer-value?language=en_US

错误是 Vivado 在导出 IP 步骤 (export_design) 中失败。

上面是错误截图,从2021年变成2022年之后就开始出现了。

我尝试重新启动并运行相同的命令来导出以前测试过的设计上的 xo 文件。他们都提示同样的错误

你好,

我正在尝试从 Vitis HLS 导出 Vivado IP(我尝试了 2020.1、2020.2 和 2021.1 版本)。一切都过去正常工作。

但是,现在它会打印以下错误消息:

错误:“2201011128”是无效参数。请指定一个整数

我注意到机器生成的 tcl 脚本使用当前日期作为修订的名称。正如您在下面的屏幕截图中看到的

图片

上面的屏幕截图来自 2021 年 12 月 31 日的设计,效果很好。以下来自 2022 年无效的设计。

图片似乎数字 22 造成了整数溢出。因为 2^31 小于当前修订号。

这个问题有简单的解决方法吗?

请尝试以下解决方法解决此问题:

1.修改vitis_​​hls项目解决目录下的run_ippack.tcl文件

XX\test\solution1\impl\ip\run_ippack.tcl

示例修改: 

 设置修订版“2201012126”-> 设置修订版“2001012126”

但修改后:

修改修订后,如果我再次开始综合,更改将被覆盖。我如何完成生成设计?

最终解决方案:

我将计算机的日期设置回滚到 2021 年,并禁用了自动时间和日期选项。

YOLO系列(二):yolov1

YOLOv1属于一阶段、anchor-free 目标检测

整体来看,Yolo算法采用一个单独的CNN模型实现end-to-end的目标检测,整个系统如图5所示:首先将输入图片resize到448×448,然后送入CNN网络,最后处理网络预测结果得到检测的目标。相比R-CNN算法,其是一个统一的框架,其速度更快,而且Yolo的训练过程也是end-to-end的。

具体来说,Yolo的CNN网络将输入的图片分割成 \(S \times S\) 网格,然后每个单元格负责去检测那些 中心点落在该格子内的目标,如图6所示,可以看到狗这个目标的中心落在左下角一个单元格内, 那么该单元格负责预测这个狗。每个单元格会预测 \(B\) 个边界框 (bounding box) 以及边界框的 置信度 (confidence score) 。所谓置信度其实包含两个方面,一是这个边界框含有目标的可能性 大小,二是这个边界框的准确度。前者记为 \(\operatorname{Pr}(object)\) ,当该边界框是背景时 (即不包含目 标),此时 \(\operatorname{Pr}(object)=0\) 。而当该边界框包含目标时, \(\operatorname{Pr}(object)=1\) 。边界框的准 确度可以用预测框与实际框 (ground truth) 的IOU (intersection over union,交并比) 来表 征,记为 \(\mathrm{IOU}{\text {pred }}^{\text {truth }}\) 。因此置信度可以定义为 \(\operatorname{Pr}(object) * \mathrm{IOU}{\text {pred }}^{\text {truth }}\) 。很多人可能将Yolo 的置信度看成边界框是否含有目标的概率,但是其实它是两个因子的乘积,预测框的准确度也反映 在里面。边界框的大小与位置可以用4个值来表征: (x, y, w, h),其中 (x, y) 是边界框的中 心坐标,而 w和 h 是边界框的宽与高。还有一点要注意,中心坐标的预测值 (x, y) 是相对于 每个单元格左上角坐标点的偏移值,并且单位是相对于单元格大小的,单元格的坐标定义如图6所 示。而边界框的 \(w\) 和 \(h\) 预测值是相对于整个图片的宽与高的比例,这样理论上4个元素的大小 应该在 \([0,1]\) 范围。这样,每个边界框的预测值实际上包含 5 个元素: \((x, y, w, h, c)\) ,其中 \((x, y)\) 是边界框的中 心坐标,而 \(w\) 和 \(h\) 是边界框的宽与高。还有一点要注意,中心坐标的预测值 \((x, y)\) 是相对于 每个单元格左上角坐标点的偏移值,并且单位是相对于单元格大小的,单元格的坐标定义如图所示。而边界框的\(w\) 和 \(h\) 预测值是相对于整个图片的宽与高的比例,这样理论上4个元素的大小 应该在 \([0,1]\) 范围。这样,每个边界框的预测值实际上包含 5 个元素: \((x, y, w, h, c)\) ,其中 前 4 个表征边界框的大小与位置,而最后一个值是置信度。

还有分类问题,对于每一个单元格其还要给出预测出 C个类别概率值,其表征的是由该单元格负 责预测的边界框其目标属于各个类别的概率。但是这些概率值其实是在各个边界框置信度下的条件 概率,即 \(\operatorname{Pr}\left(\right. class _{i} \mid object )\) 。值得注意的是,不管一个单元格预测多少个边界框,其只预测 一组类别概率值,这是Yolo算法的一个缺点,在后来的改进版本中,Yolo9000是把类别概率预测 值与边界框是绑定在一起的。同时,我们可以计算出各个边界框类别置信度(class-specific confidence scores):


边界框类别置信度表征的是该边界框中目标属于各个类别的可能性大小以及边界框匹配目标的好 坏。后面会说,一般会根据类别置信度来过滤网络的预测框。
总结一下,每个单元格需要预测 \((B * 5+C)\) 个值。如果将输入图片划分为 \(S \times S\) 网格,那 么最终预测值为 \(S \times S \times(B * 5+C)\) 大小的张量。整个模型的预测值结构如下图所示。对 于PASCAL VOC数据,其共有20个类别,如果使用 \(S=7, B=2\) ,那么最终的预测结果就是 \(7 \times 7 \times 30\) 大小的张量。在下面的网络结构中我们会详细讲述每个单元格的预测值的分布位 置。

Yolo采用卷积网络来提取特征,然后使用全连接层来得到预测值。网络结构参考GooLeNet模型,包含24个卷积层和2个全连接层,如图8所示。对于卷积层,主要使用1×1卷积来做channle reduction,然后紧跟3×3卷积。对于卷积层和全连接层,采用Leaky ReLU激活函数:max(x, 0.1x) 。但是最后一层却采用线性激活函数。

损失函数计算如下:

其中第一项是边界框中心坐标的误差项, \(1_{i j}^{obj}\) 指的是第 \(i\) 个单元格存在目标,且该单元格中的第 \(j\) 个边界框负责预测该目标。第二项是边界框的高与宽的误差项。第三项是包含目标的边界框 的置信度误差项。第四项是不包含目标的边界框的置信度误差项。而最后一项是包含目标的单元格 的分类误差项, \(1_{i}^{\text {obj }}\) 指的是第 \(i\) 个单元格存在目标。这里特别说一下置信度的target值 \(C_{i}\) , 如果是不存在目标,此时由于 \(\operatorname{Pr}( object )=0\) ,那么 \(C_{i}=0\) 。如果存在目标,
\(\operatorname{Pr}( object )=1\) ,此时需要确定 \(\mathrm{IOU}{\text {pred }}^{\text {truth }}\) ,当然你希望最好的话,可以将IOU取 1 ,这样 \(C{i}=1\) ,但是在 YOLO实现中,使用了一个控制参数 rescore (默认为 1 ),当其为 1 时,IOU不 是设置为 1 ,而就是计算truth和pred之间的真实 IOU

网络预测: 基于非极大值抑制算法

这个算法不单单是针对Yolo算法的,而是所有的检测算法中都会用到。NMS算法主要解决的是一个目标被多次检测的问题,如图11中人脸检测,可以看到人脸被多次检测,但是其实我们希望最后仅仅输出其中一个最好的预测框,比如对于美女,只想要红色那个检测结果。那么可以采用NMS算法来实现这样的效果:首先从所有的检测框中找到置信度最大的那个框,然后挨个计算其与剩余框的IOU,如果其值大于一定阈值(重合度过高),那么就将该框剔除;然后对剩余的检测框重复上述过程,直到处理完所有的检测框。Yolo预测过程也需要用到NMS算法。

下面就来分析Yolo的预测过程,这里我们不考虑batch,认为只是预测一张输入图片。根据前面的分析,最终的网络输出是 7×7×30 ,但是我们可以将其分割成三个部分:类别概率部分为 [7,7,20] ,置信度部分为 [7,7,2] ,而边界框部分为 [7,7,2,4] (对于这部分不要忘记根据原始图片计算出其真实值)。然后将前两项相乘(矩阵 [7,7,20] 乘以 [7,7,2] 可以各补一个维度来完成 [7,7,1,20]×[7,7,2,1] )可以得到类别置信度值为 [7,7,2,20] ,这里总共预测了 7∗7∗2=98 个边界框。

所有的准备数据已经得到了,那么我们先说第一种策略来得到检测框的结果,我认为这是最正常与自然的处理。首先,对于每个预测框根据类别置信度选取置信度最大的那个类别作为其预测标签,经过这层处理我们得到各个预测框的预测类别及对应的置信度值,其大小都是 [7,7,2] 。一般情况下,会设置置信度阈值,就是将置信度小于该阈值的box过滤掉,所以经过这层处理,剩余的是置信度比较高的预测框。最后再对这些预测框使用NMS算法,最后留下来的就是检测结果。一个值得注意的点是NMS是对所有预测框一视同仁,还是区分每个类别,分别使用NMS。Ng在deeplearning.ai中讲应该区分每个类别分别使用NMS,但是看了很多实现,其实还是同等对待所有的框,我觉得可能是不同类别的目标出现在相同位置这种概率很低吧。

上面的预测方法应该非常简单明了,但是对于Yolo算法,其却采用了另外一个不同的处理思路(至少从C源码看是这样的),其区别就是先使用NMS,然后再确定各个box的类别。其基本过程如图12所示。对于98个boxes,首先将小于置信度阈值的值归0,然后分类别地对置信度值采用NMS,这里NMS处理结果不是剔除,而是将其置信度值归为0。最后才是确定各个box的类别,当其置信度值不为0时才做出检测结果输出。这个策略不是很直接,但是貌似Yolo源码就是这样做的。Yolo论文里面说NMS算法对Yolo的性能是影响很大的,所以可能这种策略对Yolo更好。但是我测试了普通的图片检测,两种策略结果是一样的。