GGTalk 开源即时通讯系统源码剖析之:数据库设计 - C#开源即时通讯GGTalk - 博客园

mikel阅读(513)

来源: GGTalk 开源即时通讯系统源码剖析之:数据库设计 – C#开源即时通讯GGTalk – 博客园

自从《开源即时通讯GGTalk 8.0发布,增加Linux客户端,支持在统信UOS、银河麒麟上运行!》一文在博客园发布后,有园友联系我QQ,说能不能整理个更系统更详细地介绍GGTalk源码的文章,现在博客中的介绍比较零散,对于初级程序员而言,面对GGTalk大量的源码,有点不知所措。想想也是如此,于是,我打算写一个系列的文章来完整地介绍GGTalk的方方面面,专题的名字就叫做《GGTalk源码剖析》吧。

一. 概述

这个《GGTalk源码剖析》系列的文章将基于最新的 GGTalk V8.0 进行。
GGTalk V8.0 服务端支持Windows、Linux,客户端支持 Windows、Android、iOS、Linux、以及银河麒麟、统信UOS等国产操作系统。
数据库支持SQLServer、MySQL、以及达梦数据库、人大金仓、南大通用等国产数据库。本篇文章以 MySQL 数据库为例来对GGTalk的数据库设计进行详细的介绍。
还没有GGTalk源码的朋友,可以到 GGTalk源码下载中心 下载。

二. 数据表的设计

最新版本的 GGTalk 数据库一共涉及到九张表,分别为:

  • GGUser:用户表,所有注册用户都保存在该表中。
  • GGGroup:群组表,所有创建的群都保存在该表中。
  • OfflineMessage:离线消息表,当目标用户不在线时,发送给他的消息存在该表中。
  • OfflineFileItem:离线文件表,当目标用户不在线时,发送给他的文件对应的记录存在该表中。
  • GroupBan:群禁言表,当群中的用户被禁言时,对应的记录将存在该表中。
  • ChatMessageRecord:聊天记录表,一对一的聊天记录、群聊天记录都存在该表中。
  • AddFriendRequest:加好友请求表,所有添加好友的请求消息都存在该表中。
  • AddGroupRequest:入群请求表,所有申请入群的请求消息都存在该表中。
  • GGConfiguration:配置表,用于预留存储与GGTalk相关的配置信息。

下面将分别对每一张表的字段进行说明。

1. GGUser(用户表)

所有注册用户都保存在该表中。

字段名称 字段类型 字段说明
UserID varchar(50) 用户ID,主键
PasswordMD5 varchar(100) 经过加密处理的用户密码
Phone varchar(20) 用户手机号码
Name varchar(50) 用户昵称
Friends varchar(4000) 用户好友列表,以 , 分隔
CommentNames varchar(4000) 用户好友备注列表,以 ; 分隔
Signature varchar(100) 用户个性签名
HeadImageIndex int 系统默认头像的索引
HeadImageData mediumblob 用户上传头像的二进制数据
Groups varchar(1000) 用户所在的群组列表,以 , 分隔
UserState int 用户状态:0正常,1冻结,2禁言,3停用
MobileOfflineTime datetime 移动端离线的时间节点
PcOfflineTime datetime PC端离线的时间节点
CreateTime datetime 用户注册的时间节点
Version int 用户版本

补充说明:

  • UserId同时也是 用户账号 和 用户名 。
  • Friends 字段中包含分组信息,每个分组之间以;进行分割。例如:朋友:friend1,friend2;同学:schoolmate1,schoolmate2;
  • CommentNames 字段存储用户好友备注列表数据,以用户ID + : + 备注为一个好友的备注信息,多个备注信息之间以;分割。例如:10000:张三;10001:李四
  • HeadImageIndex 字段存储系统默认头像索引数据,当用户上传头像后,HeadImageData 字段会被赋值,且HeadImageIndex 字段值被设置为-1
  • Version 字段保存用户的版本,初始值为0,每当用户的信息更新,本字段值+1。

2. GGGroup(群组表)

所有创建的群都保存在该表中。

字段名称 字段类型 字段说明
GroupID varchar(20) 群组ID,主键
Name varchar(20) 群组名称
CreatorID varchar(20) 群组创建者的用户ID
Announce varchar(200) 群公告
Members varchar(4000) 群组成员的用户ID列表,以 , 分隔
IsPrivate tinyint 是否允许群组成员之间私聊:0不允许,1允许
CreateTime datetime 创建群组的时间节点
Version int 群组版本

补充说明:

  • Version 字段保存群组在版本,初始值为0,每当群组在信息更新,本字段值+1。
  • Members 字段存储群组成员的用户ID列表数据,注意这个字段和 GGUser表 中的Groups 字段间存在联动关系。例如:当一个用户退出一个群时,这个用户的Groups中会少一个群组ID,同时这个群组的Members中会少一个用户ID。

3. OfflineMessage(离线消息记录表)

此表用于存储离线消息数据。

字段名称 字段类型 字段说明
AutoID int 自增ID,主键
SourceUserID varchar(50) 发送离线消息的用户ID
DestUserID varchar(50) 接收离线消息的用户ID
SourceType int 发送者的设备类型:1DotNET,2Android,4IOS,9Linux
GroupID varchar(50) 该字段用于群离线消息
InformationType int 信息的类型
Information longblob 信息内容
Tag varchar(100) 附带信息
TimeTransfer datetime 服务器接收到要转发离线消息的时间

补充说明:

  • 当离线用户上线时,服务器会把这条消息转发给该用户,同时这条消息会从表中删除。
  • TimeTransfer 字段存储离线文件的路径,默认在服务端程序根目录\bin\Debug\OfflineFiles\接受者的用户ID作为文件名目录下。

4. OfflineFileItem(离线文件表)

当目标用户不在线时,发送给他的文件对应的记录存在该表中。

字段名称 字段类型 字段说明
AutoID int 自增ID,主键
FileName varchar(100) 文件名
FileLength int 文件长度
SenderID varchar(50) 发送者的用户ID
SenderType int 发送者的设备类型:1DotNET,2Android,4IOS,9Linux
AccepterType int 接受者的设备类型:1DotNET,2Android,4IOS,9Linux
AccepterID varchar(50) 接收者的用户ID
RelayFilePath varchar(300) 转发文件的路径

补充说明:
离线文件默认存在服务端的运行目录下的OfflineFiles文件夹下,RelayFilePath 指明了具体的相对路径。
当离线用户上线时,服务器会把这个文件转发给该用户,同时这个文件会从表中删除。

5. GroupBan(群禁言表)

当群中的用户被禁言时,对应的记录将存在该表中。

字段名称 字段类型 字段说明
AutoID int 自增ID,主键
GroupID varchar(20) 群组ID
OperatorID varchar(20) 禁言者的用户ID
UserID varchar(20) 被禁言者的用户ID
Comment2 varchar(50) 本次禁言的备注
EnableTime datetime 本次禁言的截至时间
CreateTime datetime 本次禁言的开始时间

6. ChatMessageRecord(聊天消息记录表)

此表用于存储聊天消息数据。

字段名称 字段类型 字段说明
AutoID bigint 自增ID,主键
SpeakerID varchar(20) 发言者的用户ID
AudienceID varchar(20) 倾听者的用户ID或群组ID
IsGroupChat bit(1) 是否是群组聊天:0不是,1是
Content longblob 聊天内容
OccureTime datetime 消息发送的时间节点

补充说明:
该表除了主键之外,还建有两个联合索引:
KEY IX_ChatMessageRecord (SpeakerID,AudienceID,OccureTime) USING BTREE
KEY IX_ChatMessageRecord_1 (AudienceID,OccureTime) USING BTREE
这两个联合索引,与客户端两种查询聊天记录的方式一一对应。
如此,服务端可以快速地从数据库中加载满足条件的聊天记录返回给客户端。

7. AddFriendRequest(加好友请求表)

所有添加好友的请求消息都存在该表中。

字段名称 字段类型 字段说明
AutoID int 自增ID,主键
RequesterID varchar(50) 申请者的用户ID
AccepterID varchar(50) 接收者的用户ID
RequesterCatalogName varchar(20) 申请者的分组名称
AccepterCatalogName varchar(20) 接受者的分组名称
Comment2 varchar(500) 申请时的备注信息
State int 本次申请的状态:0请求中,1同意,2拒绝
Notified bit(1) 申请是否通知对方:0不通知,1通知
CreateTime datetime 申请添加好友的时间节点

8. AddGroupRequest(入群请求表)

所有申请入群的请求消息都存在该表中。

字段名称 字段类型 字段说明
AutoID int 自增ID,主键
RequesterID varchar(20) 申请者的用户ID
GroupID varchar(20) 群组ID
AccepterID varchar(20) 接收者的用户ID
Comment2 varchar(500) 申请时的备注信息
State int 本次申请的状态:0请求中,1同意,2拒绝
Notified bit(1) 申请是否通知对方:0不通知,1通知
CreateTime datetime 申请加入群组的时间节点

9. GGConfiguration(系统配置表)

用于预留存储与GGTalk相关的配置信息。

字段名称 字段类型 字段说明
GGKey varchar(20) 配置的键名
GGValue varchar(1000) 配置的键值

三. 小结

GGTalk 的数据库只有9张表,而且都比较简单。
每个表都有唯一的主键。
就实际使用来看,ChatMessageRecord 聊天记录表的数据量将是最大的,所以,ChatMessageRecord 表必须建联合索引,以支持快速查询。
在我们接到的定制项目中,对于那些同时在线用户量较大的(比如同时在线大于1万人)使用场景,ChatMessageRecord 我们会采取按月分表的策略来应对,在这种情况下,GGTalk 的服务端代码需要做相应的调整。有机会用到这种策略的朋友,可以和我们交流更多关于该策略的实现方案。
作为《GGTalk源码剖析》的第一篇,差不多就这样了。在接下来的一篇我们将介绍GGTalk服务端全局缓存。
敬请期待:《GGTalk 开源即时通讯系统源码剖析之:服务端全局缓存

Vue + Element ui 实现动态表单,包括新增行/删除行/动态表单验证/提交功能 - 朱季谦 - 博客园

mikel阅读(318)

来源: Vue + Element ui 实现动态表单,包括新增行/删除行/动态表单验证/提交功能 – 朱季谦 – 博客园

最近通过Vue + Element ui实现了动态表单功能,该功能还包括了动态表单新增行、删除行、动态表单验证、动态表单提交功能,趁热打铁,将开发心得记录下来,方便以后再遇到类似功能时,直接拿来应用。

简化的页面效果图如下:
image

最开始,我是用了纯粹的表格形式,后来发现,这种形式在提交的时候,不好对每个输入框做校验,若是表单形式话,就可以直接通过rule设置每个输入框的验证,因此,我就在表格里面嵌套了表单。注意一点是,el-form-item里的 :prop=”scope.$index + ‘.name'”需要对应el-input的 v-model=”studentData[scope.$index].name”,两者都是同一个字段值。

<template>
  <div >
        <div>
          <div>
            <el-button  size="small"  @click="addRow">新增</el-button>
          </div>
          <!--设置的表单-->
          <el-form :model="studentData" ref="data" label-width="auto">
          <el-table
            border
            :header-cell-style="{ 'text-align': 'center' }"
            :cell-style="{ 'text-align': 'center' }"
            :data="studentData"
            ref="table"
            style="width: 100%"
          >

            <el-table-column align="center"   label="姓名">
              <template slot-scope="scope">
              <!--表格里面嵌套表单-->
                <el-form-item
                  :prop="scope.$index + '.name'"
                  :rules="{ required: true, message: '姓名不能为空', trigger: 'blur' }"
                >
                <el-input
                  v-model="studentData[scope.$index].name"
                  autocomplete="off"
                  size="small"
                  placeholder="姓名"
                ></el-input>
                </el-form-item>
              </template>
            </el-table-column>

            <el-table-column align="center"  label="年龄">
              <template slot-scope="scope">
                  <el-form-item
                    :prop="scope.$index + '.age'"
                    :rules="{ required: true, message: '年龄不能为空', trigger: 'blur' }"
                  >
                <el-input
                  v-model="studentData[scope.$index].age"
                  autocomplete="off"
                  size="small"
                  type='number'
                  placeholder="收款方开户行号"
                ></el-input>
                  </el-form-item>
              </template>
            </el-table-column>

            <el-table-column align="center"  label="性别">
              <template slot-scope="scope">
                <el-form-item
                  :prop="scope.$index + '.sex'"
                  :rules="{ required: true, message: '性别不能为空', trigger: 'blur' }"
                >
                <el-input
                  v-model="studentData[scope.$index].sex"
                  autocomplete="off"
                  size="small"
                  placeholder="性别"
                ></el-input>
                </el-form-item>
              </template>
            </el-table-column>

            <el-table-column fixed="right" label="操作" width="100">
              <template slot-scope="scope">
                <el-button
                  @click="handleDeleteRow(studentData[scope.$index])"
                  type="text"
                  size="small"
                >删除</el-button
                >
              </template>
            </el-table-column>
            
          </el-table>
          </el-form>
        </div>
        
        <div slot="footer" class="dialog-footer" style="margin-bottom: 10px">
          <el-button size="mini"  @click="submit">提交</el-button>
          <el-button size="mini" @click="resetForm()">重置</el-button>
        </div>
  </div>
</template>

定义一个存储动态表格数据的数组变量

export default {
  data() {
    return {
      studentData:[],
    };
  },
  ......
}

在methods方法里增加相关方法,分别是新增行、删除行、提交——

methods:{

  /**
   * 新增行
   */
  addRow() {
    let index = this.studentData.length ;
    this.studentData.push({
      key: index,
      name:'',
      age:'',
      sex:'',
    });
  },

  /**
   * 删除行
   * @param row
   */
  handleDeleteRow(row){
    let datas = this.studentData;
    for (var i = 0; i < datas.length; i++){
      if (datas[i].key == row.key){
        datas.splice(i,1);
      }
    }
  },

  /**
   * 提交
   */
  submit() {
    this.$refs["data"].validate(valid => {
      //valid为true,表示表单都已经验证通过,若为false,说明存在表单验证失败
        if (valid) {
          save(this.studentData).then(response => {
            this.$message({
              message: '提交成功',
              type: 'success'
            });
          });
        }
    });
  },

  /**
   * 重置
   */
  resetForm() {
    let datas = this.studentData;
    for (var i = 0; i < datas.length; i++){
      datas[i].name='';
      datas[i].age='';
      datas[i].sex='';
    }
  },
}

设置表单验证规则,可统一在rules设置,也可以在每一输入框单独设置,我这里是单独在每一个输入框里设置,即:rules=”{ required: true, message: ‘姓名不能为空’, trigger: ‘blur’ }”就可以了,当然,还可以做一些更复杂的自定义规则。

<el-table-column align="center"   label="姓名">
          <template slot-scope="scope">
          <!--表格里面嵌套表单-->
            <el-form-item
              :prop="scope.$index + '.name'"
              :rules="{ required: true, message: '姓名不能为空', trigger: 'blur' }"
            >
            <el-input
              v-model="studentData[scope.$index].name"
              autocomplete="off"
              size="small"
              placeholder="姓名"
            ></el-input>
            </el-form-item>
          </template>
        </el-table-column>

Github 6月开源项目Top30精选🔥🔥🔥 - 知乎

mikel阅读(755)

来源: Github 6月开源项目Top30精选🔥🔥🔥 – 知乎

6月项目精选: 主要包含 诗词库/AI模型/ChatGPT/人工智能教育/音频处理等热点项目

1.中华古诗词数据库:chinese-poetry 43.2k⭐

  • 项目地址github.com/chinese-poet
  • Github趋势榜
    • 入选2023-06-24月榜,月增⭐3,548 stars this month
  • 开源时间:2016-09-02
  • 最后更新:2023-05-29
  • 主要语言JavaScript
  • 项目分类:[资源库] [学习社区]
  • 项目标签:[中国古诗词库]
  • 推荐理由:这个项目收集了最全的中华古诗词数据库,包括唐宋两朝近一万四千位古诗人的作品,近5.5万首唐诗和26万首宋诗。此外,还包括了两宋时期1564位词人的21050首词。
  • Star历史曲线:[官网]

2.私人GPT互动:privateGPT 31.3k⭐

  • 项目地址github.com/imartinez/pr
  • Github趋势榜
    • 入选2023-06-21月榜,月增⭐12,237 stars this month
  • 开源时间:2023-05-02
  • 最后更新:2023-06-21
  • 主要语言Python
  • 项目分类:[ChatGPT] [AI]
  • 项目标签:[大语言模型] [GPT模型] [自然语言处理] [智能ChatGPT]
  • 推荐理由:这个项目提供了一个私人的GPT(生成式预训练模型)解决方案,可以在考虑数据安全、隐私保护或断网情况下与GPT和文档进行交互。它为用户提供了一个可靠且安全的方式来使用GPT模型,无论是用于个人用途还是商业应用。
  • Star历史曲线:[项目体验]

3. AI模型工具包:fairseq 26.6k⭐

  • 项目地址github.com/facebookrese
  • Github趋势榜
    • 入选2023-06-21月榜,月增⭐4,550 stars this month
  • 开源时间:2017-08-30
  • 最后更新:2023-06-24
  • 主要语言Python
  • 项目分类:[AI]
  • 项目标签:[神经网络] [AI生成] [机器学习]
  • 推荐理由:这个用Python编写的工具包是Facebook AI研究团队开发的序列到序列模型的工具包。它提供了一套丰富而强大的工具和模型,适用于机器翻译、文本生成等自然语言处理任务。
  • Star历史曲线

4.任天堂Switch模拟器:Ryujinx 23.6k⭐

  • 项目地址github.com/Ryujinx/Ryuj
  • Github趋势榜
    • 入选2023-06-05月榜,月增⭐3,831 stars this month
  • 开源时间:2018-02-05
  • 最后更新:2023-06-25
  • 主要语言C#
  • 项目分类:[游戏]
  • 项目标签:[模拟器]
  • 推荐理由:Ryujinx是用C#编写的任天堂Switch游戏机模拟器。虽然项目目前仍处于实验阶段,稳定性尚有待提高,并且需要至少8GB内存的机器配置,但对于那些具有探险精神的游戏爱好者来说,这是一个值得尝试的模拟器项目。
  • Star历史曲线:[官网] [使用文档]

5.精美UI组件:ui 22.0k⭐

  • 项目地址github.com/shadcn/ui
  • Github趋势榜
    • 入选2023-06-24月榜,月增⭐5,321 stars this month
  • 开源时间:2023-01-04
  • 最后更新:2023-06-25
  • 主要语言TypeScript
  • 项目分类:[设计] [前端]
  • 项目标签:[前端设计] [组件库]
  • 推荐理由:这个项目使用Radix UI和Tailwind CSS构建了一套设计精美的组件。它为开发者提供了一套现代化且易于使用的UI组件,可以轻松地构建出视觉上吸引人的网页和应用程序。
  • Star历史曲线:[官网] [使用文档]

6.跨平台文件管理工具:spacedrive 19.4k⭐

  • 项目地址github.com/spacedriveap
  • Github趋势榜
    • 入选2023-06-12周榜,周增⭐468 stars this week
    • 入选2023-06-24月榜,月增⭐1,877 stars this month
  • 开源时间:2021-09-27
  • 最后更新:2023-06-25
  • 主要语言Rust
  • 项目分类:[工具]
  • 项目标签:[文件管理器] [管理工具]
  • 推荐理由:Spacedrive是一个用Rust编写的跨平台文件管理工具。它基于虚拟分布式文件系统(VDFS)的Rust实现,可以将不同操作系统的设备连接起来,实现统一的文件管理。虽然项目仍在开发阶段,需要用户自行编译客户端,但它提供了一种方便且安全的方式来管理和共享文件。
  • Star历史曲线:[官网] [使用文档]

7.ChatGPT插件:chatgpt-retrieval-plugin 18.4k⭐

  • 项目地址github.com/openai/chatg
  • Github趋势榜
    • 入选2023-06-14月榜,月增⭐1,728 stars this month
  • 开源时间:2023-03-23
  • 最后更新:2023-06-24
  • 主要语言Python
  • 项目分类:[ChatGPT]
  • 项目标签:[ChatGPT插件]
  • 推荐理由:一个方便的工具,可以帮助用户轻松地找到个人或工作文件,通过询问自然语言的问题。它结合了ChatGPT的强大的对话生成能力和文档检索功能,为用户提供高效的信息查询和搜索工具。

8.Rust教程:comprehensive-rust 18.0k⭐

  • 项目地址github.com/google/compr
  • Github趋势榜
    • 入选2023-06-23月榜,月增⭐5,905 stars this month
  • 开源时间:2022-12-21
  • 最后更新:2023-06-25
  • 主要语言Rust
  • 项目分类:[编程语言] [面试求职]
  • 项目标签:[编程学习]
  • 推荐理由:这是Google开放的一个全面的Rust技术教程,涵盖了Rust的基本语法到高级主题,包括泛型、错误处理、Android开发等。通过这个教程,您将全面学习Rust的基础知识,掌握Rust应用程序的编写和常用开发技巧。
  • Star历史曲线:[官网] [使用文档]

9.潘多拉ChatGPT:pandora 16.2k⭐

  • 项目地址github.com/pengzhile/pa
  • Github趋势榜
    • 入选2023-06-24月榜,月增⭐8,649 stars this month
  • 开源时间:2023-02-17
  • 最后更新:2023-06-24
  • 主要语言Python
  • 项目分类:[ChatGPT] [AI]
  • 项目标签:[聊天机器人] [GPT4] [AI聊天]
  • 推荐理由:潘多拉是一个更多功能的ChatGPT,不仅限于命令行界面。它提供了更多的交互方式和功能,可以在不同的环境中进行聊天和对话。无论是在命令行还是其他平台上,潘多拉都为用户提供了更灵活、多样化的ChatGPT体验。
  • Star历史曲线:[官网] [项目体验] [使用文档]

10.源深度学习框架:tinygrad 15.2k⭐

  • 项目地址github.com/geohot/tinyg
  • Github趋势榜
    • 入选2023-06-22日榜,日增⭐51 stars today
    • 入选2023-06-23日榜,日增⭐790 stars today
    • 入选2023-06-24日榜,日增⭐78 stars today
    • 入选2023-06-24月榜,月增⭐3,386 stars this month
  • 开源时间:2020-10-19
  • 最后更新:2023-06-25
  • 主要语言Python
  • 项目分类:[AI]
  • 项目标签:[深度学习] [大语言模型]
  • 推荐理由:这是一个小型的开源深度学习框架。尽管代码行数不足1k,但它提供了足够简单的接口,支持深度学习模型的推理和训练。Tinygrad的设计注重简洁和易用性,使得开发者可以快速上手并构建自己的深度学习模型。
  • Star历史曲线:[项目体验] [使用文档]

11.AI人脸交换:DeepFaceLive 15.0k⭐

12.可视化爬虫软件:EasySpider 13.1k⭐

动图封面

  • 项目地址github.com/NaiboWang/Ea
  • Github趋势榜
    • 入选2023-06-21日榜,日增⭐526 stars today
    • 入选2023-06-22日榜,日增⭐200 stars today
    • 入选2023-06-23日榜,日增⭐203 stars today
    • 入选2023-06-24月榜,月增⭐7,599 stars this month
  • 开源时间:2020-07-18
  • 最后更新:2023-06-24
  • 主要语言JavaScript
  • 项目分类:[工具]
  • 项目标签:[可视化爬虫]
  • 推荐理由:一个可视化爬虫软件,用户可以通过图形化界面进行爬虫任务的设计和执行,无需编写代码。它提供了简单易用的工具,帮助用户快速爬取所需数据。
  • Star历史曲线:[官网] [使用文档] [视频介绍]

13.微软人工智能教育学习社区:ai-edu 12.2k⭐

  • 项目地址github.com/microsoft/ai
  • Github趋势榜
    • 入选2023-06-21周榜,周增⭐1,779 stars this week
    • 入选2023-06-24月榜,月增⭐7,174 stars this month
  • 开源时间:2018-11-16
  • 最后更新:2023-06-08
  • 主要语言HTML
  • 项目分类:[面试求职] [学习社区]
  • 项目标签:[AI教育] [机器学习] [系统学习]
  • 推荐理由:这是微软人工智能教育与学习共建社区的开源项目。它由微软亚洲研究院的人工智能教育团队创建,提供了基础教程、实践案例和实践项目三大模块。AI-Edu通过系统化的理论教程和丰富多样的实践案例,帮助学习者学习和掌握人工智能知识,并培养在实际项目中的开发能力。
  • Star历史曲线:[官网] [使用文档]

14.Youtube镜像版:invidious 11.7k⭐

  • 项目地址github.com/iv-org/invid
  • Github趋势榜
    • 入选2023-06-22周榜,周增⭐409 stars this week
    • 入选2023-06-24月榜,月增⭐1,744 stars this month
  • 开源时间:2018-02-07
  • 最后更新:2023-06-24
  • 主要语言Crystal
  • 项目分类:[工具]
  • 项目标签:[镜像站]
  • 推荐理由:YouTube的开源前端替代产品。它提供了一个独立的前端界面,用户可以在不访问YouTube官方网站的情况下浏览和观看YouTube的视频内容。Invidious的开源性质使得用户可以自由定制和控制他们对YouTube内容的访问和体验。

15.矢量数据库:qdrant 11.2k⭐

  • 项目地址github.com/qdrant/qdran
  • Github趋势榜
    • 入选2023-06-05月榜,月增⭐4,275 stars this month
  • 开源时间:2020-05-31
  • 最后更新:2023-06-25
  • 主要语言Rust
  • 项目分类:[中间件] [AI]
  • 项目标签:[AI应用] [矢量数据库]
  • 推荐理由:一个用于下一代AI应用程序的矢量数据库。它提供了高效的矢量索引和检索功能,支持快速的相似度搜索和相关性计算,适用于各种AI应用领域。

16.项目管理工具:plane 10.7k⭐

  • 项目地址github.com/makeplane/pl
  • Github趋势榜
    • 入选2023-06-24月榜,月增⭐8,539 stars this month
  • 开源时间:2022-11-19
  • 最后更新:2023-06-25
  • 主要语言TypeScript
  • 项目分类:[设计]
  • 项目标签:[任务跟踪] [产品设计] [瀑布模型]
  • 推荐理由:Plane是一个简单、可扩展、开源的项目和产品管理工具。它提供了一个基本的任务跟踪功能,使用户可以创建、分配和跟踪任务的进展。同时,Plane还支持各种项目管理框架,如敏捷开发、瀑布模型等,用户可以根据自己的需求选择适合的框架来管理项目。这个工具具有易用性和灵活性,可以帮助团队更好地组织和协调项目工作。

17.可视化UI定制LLM:Flowise 9.2k⭐

  • 项目地址github.com/FlowiseAI/Fl
  • Github趋势榜
    • 入选2023-06-16周榜,周增⭐1,036 stars this week
    • 入选2023-06-24月榜,月增⭐3,917 stars this month
  • 开源时间:2023-03-31
  • 最后更新:2023-06-24
  • 主要语言JavaScript
  • 项目分类:[ChatGPT] [AI]
  • 项目标签:[LangChain] [机器学习]
  • 推荐理由:一个使用LangchainJS拖放UI构建定制化低代码机器学习(LLM)流程的工具。它简化了机器学习流程的开发和部署,让用户能够通过拖放操作设计自己的机器学习工作流程,提高开发效率。

18.链接管理工具:dub 9.1k⭐

  • 项目地址github.com/steven-tey/d
  • Github趋势榜
    • 入选2023-06-19月榜,月增⭐2,228 stars this month
  • 开源时间:2022-08-28
  • 最后更新:2023-06-24
  • 主要语言TypeScript
  • 项目分类:[工具]
  • 项目标签:[管理工具]
  • 推荐理由:一个开源链接管理工具,用于现代营销团队创建、共享和跟踪短链接。它集成了Vercel边缘函数、Upstash Redis和PlanetScale MySQL等技术,提供了一个功能丰富的平台,帮助营销团队轻松创建和管理短链接,并跟踪链接的性能和分析数据。
  • Star历史曲线:[官网]

19.AI数据库:Chat2DB 8.1k⭐

  • 项目地址github.com/alibaba/Chat
  • Github趋势榜
    • 入选2023-06-20日榜,日增⭐1,694 stars today
    • 入选2023-06-21日榜,日增⭐356 stars today
    • 入选2023-06-22日榜,日增⭐357 stars today
    • 入选2023-06-24周榜,周增⭐4,386 stars this week
    • 入选2023-06-24月榜,月增⭐6,591 stars this month
  • 开源时间:2022-11-18
  • 最后更新:2023-06-24
  • 主要语言Java
  • 项目分类:[ChatGPT] [AI]
  • 项目标签:[AI Chat SQL] [智能ChatGPT]
  • 推荐理由:Chat2DB集成了AIGC的能力,能够将自然语言转换为SQL,也可以将SQL转换为自然语言,可以给出研发人员SQL的优化建议,极大的提升人员的效率,是AI时代数据库研发人员的利器,未来即使不懂SQL的运营业务也可以使用快速查询业务数据、生成报表能力。

20.本地OpenAI:LocalAI 7.5k⭐

  • 项目地址github.com/go-skynet/Lo
  • Github趋势榜
    • 入选2023-06-16月榜,月增⭐4,011 stars this month
  • 开源时间:2023-03-19
  • 最后更新:2023-06-25
  • 主要语言Go
  • 项目分类:[工具] [AI]
  • 项目标签:[深度学习] [大语言模型] [ 人工智能]
  • 推荐理由:这个用Go编写的项目是一个自托管、社区驱动的简单本地OpenAI兼容API。它可以在使用消费级硬件的CPU上作为OpenAI的替代品运行,支持ggml兼容模型,例如:LLaMA、alpaca、gpt4all、vicuna、koala、gpt4all-j、cerebras.

21.多模型聊天工具:ChatALL 7.2k⭐

  • 项目地址github.com/sunner/ChatA
  • Github趋势榜
    • 入选2023-06-19日榜,日增⭐56 stars today
    • 入选2023-06-20日榜,日增⭐77 stars today
    • 入选2023-06-21日榜,日增⭐72 stars today
    • 入选2023-06-17月榜,月增⭐5,836 stars this month
  • 开源时间:2023-04-08
  • 最后更新:2023-06-24
  • 主要语言JavaScript
  • 项目分类:[ChatGPT] [AI]
  • 项目标签:[聊天机器人] [大语言模型] [GPT模型] [智能ChatGPT]
  • 推荐理由:ChatALL是一个聊天工具,可以同时与多个聊天模型进行交互,包括ChatGPT、Bing chat、bard、羊驼、Vincuna、Claude、ChatGLM、MOSS、科大讯飞Spark和ERNIE等。它的目标是通过与多个聊天模型对话,从中发现最佳答案,提供更全面和准确的回答。
  • Star历史曲线

22.量化LLM微调工具:qlora 6.1k⭐

23.免费的GPT-4 API:gpt4free-ts 5.5k⭐

  • 项目地址github.com/xiangsx/gpt4
  • Github趋势榜
    • 入选2023-06-21周榜,周增⭐902 stars this week
    • 入选2023-06-24月榜,月增⭐4,959 stars this month
  • 开源时间:2023-05-04
  • 最后更新:2023-06-23
  • 主要语言TypeScript
  • 项目分类:[ChatGPT]
  • 项目标签:[GPT4] [逆向工程]
  • 推荐理由:gpt4free-ts是提供免费OpenAI GPT-4 API的项目,这是xtekky/gpt4free项目的TypeScript版本。它为开发者提供了使用GPT-4的API接口,使得无需支付额外费用就能够访问强大的自然语言处理能力。
  • Star历史曲线

24.实时变声器:voice-changer 5.2k⭐

  • 项目地址github.com/w-okada/voic
  • Github趋势榜
    • 入选2023-06-13月榜,月增⭐3,368 stars this month
  • 开源时间:2022-08-22
  • 最后更新:2023-06-24
  • 主要语言TypeScript
  • 项目分类:[AI]
  • 项目标签:[AI变声] [语音识别]
  • 推荐理由:一个实时的变声器工具,它可以改变声音的音调和效果。用户可以通过这个工具实时对音频进行变声处理,创造出各种有趣和创意的音效效果。
  • Star历史曲线:[视频介绍]

25.实时转录工具:ecoute 4.5k⭐

  • 项目地址github.com/SevaSk/ecout
  • Github趋势榜
    • 入选2023-06-24月榜,月增⭐3,963 stars this month
  • 开源时间:2023-05-08
  • 最后更新:2023-06-21
  • 主要语言Python
  • 项目分类:[视频图像] [ChatGPT] [AI]
  • 项目标签:[AI生成] [语音交互] [对话转录] [GPT模型]
  • 推荐理由:Ecoute是一个实时转录工具,在文本框中为用户的麦克风输入(You)和用户的扬声器输出(Speaker)提供实时转录。它还使用OpenAI的GPT-3.5为用户生成一个基于对话现场转录的建议响应。
  • Star历史曲线

26.后台模版:soybean-admin 4.4k⭐

  • 项目地址github.com/honghuangdc/
  • Github趋势榜
    • 入选2023-06-14月榜,月增⭐775 stars this month
  • 开源时间:2021-09-09
  • 最后更新:2023-06-20
  • 主要语言TypeScript
  • 项目分类:[后端]
  • 项目标签:[NaiveUI模版] [Vue3模版]
  • 推荐理由:这是一个基于Vue3、Vite3、TypeScript、NaiveUI和UnoCSS的清新优雅的中后台模板。它提供了一个现代化的用户界面,适用于中后台管理系统的开发。这个模板使用了最新的前端技术,具有响应式设计和丰富的组件库,可以帮助开发人员快速搭建出漂亮、功能丰富的中后台应用。
  • Star历史曲线:[官网] [项目体验]

27.DragGAN视觉交互:DragGAN 4.2k⭐

  • 项目地址github.com/Zeqiang-Lai/
  • Github趋势榜
    • 入选2023-06-24月榜,月增⭐3,830 stars this month
  • 开源时间:2023-05-20
  • 最后更新:2023-06-25
  • 主要语言Python
  • 项目分类:[AI]
  • 项目标签:[视觉库] [图像生成]
  • 推荐理由:DragGAN是一个全功能实现的模型,提供在线演示和本地部署试用,并已开源其代码和模型。它支持在Windows、macOS和Linux上运行,可用于图像生成和转换任务。
  • Star历史曲线

28.应用程序开发模版 :next-enterprise 3.5k⭐

29.自动量化机器人:Qbot 3.0k⭐

  • 项目地址github.com/UFund-Me/Qbo
  • Github趋势榜
    • 入选2023-06-22月榜,月增⭐2,278 stars this month
  • 开源时间:2022-11-23
  • 最后更新:2023-06-12
  • 主要语言Jupyter Notebook
  • 项目分类:[AI]
  • 项目标签:[强化学习] [深度学习] [量化交易]
  • 推荐理由:Qbot是一个面向AI的量化投资平台,旨在利用AI技术实现量化投资的潜力。该平台提供了一系列工具和功能,使投资者能够运用AI技术进行数据分析、模型构建和交易执行,为量化投资领域带来更多的智能和效益。
  • Star历史曲线:[官网] [使用文档] [视频介绍]

更多Github开源项目

以上就是本期的推荐所有项目,如果你喜欢本期的内容,欢迎收藏和关注OpenGithub社区,我们会定期推送优质的开源项目。

Github历史期刊:

听说后端的你在学 React

mikel阅读(292)

来源: 听说后端的你在学 React

一、React 是什么

在 React 之前前端有三个里程碑意义的 library/framework
  • JQuery 解决了浏览器兼容和 DOM 元素快捷操作问题,其链式操作 API 也对后续前端框架产生了深刻影响;
  • Knockout 提出了前端代码 MVVM 分层理念,数据通过模板映射为 UI 视图,大幅度减少了 DOM 操作;
  • AngularJS 在 MVVM 基础上引入了双向绑定,数据变化自动反映到 UI,视图上的操作也反向自动更新数据;其通过指令拓展 HTML 的风格提升了模板引擎的灵活性,可惜作者引入了大量借鉴服务器编程的概念,让 AugularJS 学习成本直线上升,性能也略有不足;
React 是一个声明式、高效、灵活的用于构建用户界面的 JavaScript library,React 核心理念在于将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称作 “Component”。React 不是 MVC 框架,更像是其中 V,仅仅负责用户交互视图的渲染。
React 带来了三个颠覆性理念,在接下来的章节中将一一介绍:
  • JSX,使用 JavaScript 表达 UI + 交互,充分利用 JavaScript 的灵活性;
  • fx(props) = UI,数据驱动 UI,单向数据流、函数风格的页面组件;
  • Virtual DOM,服务器、客户端使用同一套代码渲染——同构,解决前端应用 SEO 问题;
二、快速初始化 React 项目

使用 Create React App [1]可以快速初始化一个 React Web 项目。
$ npx create-react-app learn-react --template typescript$ cd learn-react$ npm start

执行 npm start后浏览器会在 http://localhost:3000 打开项目首页。

三、调试 React 应用

React 提供了 React Developer Tools[2],集成到了 Chrome Dev Tools,借此可以查看 React 组件树及其对应 Props、State。
app.tsx
import React, { useState } from 'react';
function Button(props: { count: number }): JSX.Element {  const [count, setCount] = useState(props.count);  return (    <button      onClick={() => {        setCount((c) => c + 1);      }}      >      {count}    </button>  );}
function App() {  const [count, setCount] = useState(0);
  return (    <div className="App">      <Button count={5} />    </div>  );}
export default App;

index.tsx

import React from 'react';import * as ReactDOMClient from 'react-dom/client';
import App from './app';
const rootElement = document.querySelector('body') as Element;const root = ReactDOMClient.createRoot(rootElement);
root.render(<App />);

打开 Chrome Dev Tools 可以看到多了一个 Components选项卡

图片

四、Todo project

接下来边学习边做一个 Todo 项目体验一下 React。

图片

五、使用 JSX 做更好的关注点分离

在开始编写 React 程序之前需要了解一下 JSX。JSX 是 React 对 JavaScript 的语法拓展,用来在 JavaScript 文件内通过类HTML标签(HTML-like markup)表达页面的视图与交互逻辑。
<div className="container">  <CustomComponent     onClick={() => {alert('Hello')}}  >    Hello {props.name}!  </CustomComponent></div>
Web 页面由 HTML 内容、CSS 样式、JavaScript 交互构成,长期以来 Web 开发者将三者放在独立的文件中做分离,这实际上是按照技术实现的分离。
图片 图片

传统页面内容主要由 HTML 定义,JavaScript 逻辑是点缀,随着现代网页交互性增强,页面内容很大程度是由 JavaScript 逻辑动态生成,同时渲染逻辑本质上与其他 UI 逻辑内在耦合,比如在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。

因此 React 使用 JSX 把渲染逻辑和 HTML 标签集成到一起。

图片 图片

这样开发者关注的不是 HTML 模板、JavaScript 渲染逻辑这样的技术实现,而是诸如 Sidebar、Form 这样的页面功能单元。

六、使用 JSX 编写 React 组件

返回 JSX 的函数就是 React 最简单的组件,可以和 HTML 标签一样嵌套使用。React 使用 props参数向组件传递数据,提升组件的复用性。
/** * JSX 语法隐式调用 React.createElement * 所以虽然代码中没有调用 React 的语句,仍然需要引入  */import React from 'react'; 
interface IButton {  /** 按钮展示文案 */  text: string;  /** 点击按钮跳转链接 */  link?: string;  /** 点击按钮自定义事件 */  onClick?: (event?: Event) => void}
function Button(props: IButton) {  const { text, link, onClick } = props;
  const redirectHandler = () => {    location.href = link;  };
  return (    <div      className="button"      onClick={onClick | redirectHandler}    >      {text}    </div>  );}
export default Button;

在使用组件时候,通过其标签的属性组装成 props 对象,传递给组件,语法和 HTML attribute 类似,但值可以是任意的 JavaScript 对象。

import React from 'react';
/** * 导入 ./button.tsx 中 export 的默认内容,命名为 Button 使用 * .tsx 拓展名可以省略 */import Button from './button';
interface IDialog {  title: string;  content: Element;  showClose: boolean;}
function Dialog(props: IDialog) {  const { title, content, showClose = false, children } = props;
  const hideDialog = () => {    // ...  }
  return (    <div>      <div className="dialog-title"> {title} </div>      <div className="dialog-body"> {content | children} </div>      {/* 没错,Button props 定义的属性,就是这样通过标签属性开放出来的 */}      <Button        title="取消"        onClick={hideDialog}      />      <Button         title="确认"        onClick={() => { }}      />    </div>  );}
export default Dialog;

组件写好后通过 react-dom [3]将组件渲染到页面。

import React from 'react';import ReactDOM from 'react-dom/client';import Dialog from './dialog';
// 把组件渲染到页面 id 为 root 的元素中const rootElement = document.getElementById('root');const root = ReactDOM.createRoot(rootElement);root.render(  <Dialog     title="demo dialog"     content="this is a dialog"    showClose={false}  />);
七、JSX 规则

React 组件有几个约定:
  • 组件名称使用 Pascal 风格(首字母大写),以和 HTML 原生标签(div、p、a 等)区分;
  • 组件仅接受 props一个参数,用来暴露组件可配置属性,其子组件被 React 通过 children属性注入;
  • 在组件内部 props 是只读的,不允许对其进行修改;
1. 必须有根节点

如同上面写的几个简单 demo,JSX 必须有 root 节点,即使多个同级元素没有父节点,也需要用虚拟节点 <></> 来包裹。
{/* 非法的 JSX */}<div id="box1"></div><div id="box2"></div>
{/* 合法的 JSX */}<>    <div id="box1"></div>  <div id="box2"></div></>
2. 所有标签需要闭合

在 HTML 中标签并不一定需要闭合。
<meta charset="UTF-8"><br><img src="https://g.alicdn.com/logo.png">
在 JSX 中可以混合 HTML 原生标签,但所有标签必须闭合。
<>  <meta charset="UTF-8" />  <br/>  <img src="https://g.alicdn.com/logo.png"/></>
3. 和 HTML 属性差异

  • 在 React 中常用的 DOM 特性和属性(包括事件处理)都使用小驼峰命名的方式,例如与 HTML 中的 tabindex 属性对应的 React 的属性是 tabIndex;
  • HTML 部分属性名称与 JavaScript 保留字冲突,在 JSX 中需要使用替代名称;
    图片
  • style 属性 value 是一个 CSS 属性组成的对象,为了让其符合 JavaScript 语法规则,属性名使用驼峰命名(fontSize、backgroundColor),而不是 CSS 属性使用的连字符,这样可以很方便设置动态样式,但静态样式应该依赖 className 和 CSS 文件的配合;
function HelloWorldComponent(props) {    const divStyle = {      // 可以很方便设置动态样式      backgroundImage: 'url(' + props.imgUrl + ')',    // 但静态样式应该尽量通过 className 设置类,通过 css file 解决    // 不推荐 color: 'blue' 这种静态样式直接在 JSX 的写法    color: 'blue',  };
  return (    <div style={divStyle}>      Hello World!    </div>  );}
  • React 对于 Form 表单支持 defaultValue 属性,设置默认值,在运行时取值使用和 HTML 一致的 value属性。
4. 自动转义 content

为了防止 XSS 攻击,JSX 会对直接设置的文本进行转义。
const content = `  这里应该展示一张图片<br>  <img src="https://sc02.alicdn.com/kf/HTB1gUuPUkzoK1RjSZFl761i4VXaw.png" />`;<div>  {content}</div>

页面效果:

图片

在安全性有保障的时候,可以通过 dangerouslySetInnerHTML禁用转义效果,展示 raw HTML
 

const content = `      这里应该展示一张图片<br>      <img src="https://sc02.alicdn.com/kf/HTB1gUuPUkzoK1RjSZFl761i4VXaw.png" />  `;<div dangerouslySetInnerHTML={{ __html: content }}/>

图片

八、在 JSX 中TODO使用 {} 支持 JavaScript 表达式

JSX 中使用 {} 包裹 JavaScript 表达式处理动态逻辑,属性 value、子元素都可以,最常见的几个用法:
  • {变量名}读取变量值,双层 {{}} 并不是特殊语法,而是 {对象} 的快捷写法
<div style={{ color: 'red' }}></div>
// 等同于
const styleObj = { color: 'red' };<div style={styleObj}></div>
  • 三元表达式处理 if-else(if-else 是语句,不是表达式)
  • map 处理循环逻辑,批量生成元素
interface IStuff {  name: string;  sex: 'male' | 'female';}
function App () {  const list: Array<IStuff> = [    { name: 'Byron', sex: 'male' },    { name: 'Casper', sex: 'male' },    { name: 'Junice', sex: 'female' },  ];
  return (    <ul className="stuff-list">      {        list.map(stuff => { // 生成多个          const { name, sex } = stuff;          return (            {            <li              /* 实际编程 className 设置有更好的表达方式,这里仅 demo 三元表达式使用 */}                className={sex === 'male' ? 'stuff-male' : 'stuff-female'}              onClick={() => { alert(name) }}            >              // 读取变量值              {name}            </li>          );        })      }    </ul>  );}
JSX 中注释也需要使用 {} 包裹,但这种写法过于不方便,大部分编译工具都可以处理双斜线风格//注释
九、JSX 的背后

JSX 的返回值既不是 DOM 元素,也不是 HTML 字符串,而是对 DOM 的一个 JSON 描述,这就是 React Element:
<button id="9527" className="btn-primary">   <span style={{ color: 'red' }}>     This is a Button   </span></button>

JSX 用类似这样的结构表达:

 

{  "type": "button",  "props": {    "id": "9527",    "className": "btn-primary",    "children": [      {        "type": "span",        "props": {          "style": { "color": "red" },          "children": "This is a Button"        }      }    ]  }}

编译后实际是这样的调用:

React.createElement("button", {  id: "9527",  className: "btn-primary"},React.createElement("span", {  style: {    color: 'red'  }}, "This is a Button"));
React.createElement(type, props, …children),上文提到过 React 会自动把 children 注入到 props,就是在这个过程。
了解了 JSX 之后可以开始编写静态的 React Component 了。
完整教程见语雀:https://www.yuque.com/sunluyong/fe4java/pwsehvspthh6gtrd

参考链接:

[1]https://create-react-app.dev/

[2]https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi

[3]https://www.npmjs.com/package/react-dom

ChatGPT系统反向代理配置官方教程 

mikel阅读(398)

来源: ChatGPT系统反向代理配置官方教程 – 程序专题 源码论坛,商业源码下载,尽在锦尚中国商业源码论坛


搭建属于自己的ai平台,属实有点繁琐,但是不得不承认的是,太简单的东西,你也挣不到钱的!

1、超管后台选择接口通道为“反向代理”,填入自己海外服务器的域名。

2、添加反向代理

将海外宝塔升级到最新,然后新建一个站点 -> 站点设置 -> 反向代理,

参数见下图:目标URL是 https://api.openai.com,发送域名是 api.openai.com

注意:配置完以后,将反代地址放浏览器打开,出现下图结果说明反向代理配置成功

如果出现502错误,则按下面方法解决:

打开海外宝塔面板 -> 站点设置 -> 反向代理 -> 配置文件

在配置文件里

  1. location /
  2. {
  3. proxy_pass https://api.openai.com;
  4. proxy_set_header Host api.openai.com;
  5. proxy_set_header X-Real-IP $remote_addr;
  6. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  7. proxy_set_header REMOTE-HOST $remote_addr;
  8. proxy_set_header Upgrade $http_upgrade;
  9. proxy_set_header Connection $connection_upgrade;
  10. proxy_http_version 1.1;
  11. # proxy_hide_header Upgrade;

复制代码


#代码下面,新增

proxy_ssl_server_name on;

API网关:中间件设计和实践 | bugstack 虫洞栈

mikel阅读(684)

来源: API网关:中间件设计和实践 | bugstack 虫洞栈

作者:小傅哥
博客:https://bugstack.cn(opens new window)
星球:https://t.zsxq.com/0diYdgP5u (opens new window)– 课程入口

沉淀、分享、成长,让自己和他人都能有所收获!😄


是滴,小傅哥又要准备搞事情了!这次准备下手API网关项目,因为这是所有互联网大厂都有的一个核心服务,承接着来自用户的滴滴打车、美团外卖、京东购物、微信支付,更是大促期间千万级访问量的核心系统。

🤔 那么它是一个什么样的项目呢?为什么会有它的存在?它是怎么设计实现的呢?都用到了哪些技术栈呢?

#一、前言:网关是啥东西

在计算机网络中,网关 (opens new window)(Gateway)是转发其他服务器通信数据的服务器,接收从客户端发送来的请求时,它就像自己拥有资源的源服务器一样对请求进行处理。

API网关也是随着对传统庞大的单体应用(All in one)拆分为众多的微服务(Microservice)以后,所引入的统一通信管理系统。用于运行在外部http请求与内部rpc服务之间的一个流量入口,实现对外部请求的协议转换参数校验鉴权切量熔断限流监控风控等各类共性的通用服务。

#二、大厂:为啥都做网关

各大厂做网关,其实做的就是一套统一方案。将分布式微服务下的RPC到HTTP通信的同类共性的需求,凝练成通用的组件服务,减少在业务需求场景开发下,非业务需求的同类技术诉求的开发成本。

那么以往没有网关的时候怎么做,基本的做法就是再 RPC 服务之上再开发一个对应的 WEB 服务,这些 WEB 服务可以是 Spring MVC 工程,在 Spring MVC 工程中调用 RPC 服务,最终提供 HTTP 接口给到 H5、Web、小程序、APP 等应用中进行使用。如图 1-1 所示

图 1-1 从传统方式到网关设计

传统开发 WEB 服务的几个问题:

  • 问题1:每一个 WEB 应用,都需要与之匹配申请一套工程、域名、机器等资源,一直到部署,研发效率降低,维护成本增加。
  • 问题2:每一个 WEB 应用,都会有所涉及共性需求,限流、熔断、降级、切量等诉求,维护代码成本增加。
  • 问题3:每一个 WEB 应用,在整个使用生命周期内,都会涉及到文档的维护、工程的调试、联调的诉求,类似刀耕火种一样的开发势必降低研发效率。

所以:综上在微服务下的传统开发所遇到的这些问题,让各个大厂都有了自己自研网关的诉求,包括;阿里腾讯百度美团京东网易亚马逊等,都有自己成熟的 API 网关解决方案。毕竟这可以降低沟通成本、提升研发效率、提升资源利用率。

#三、网关:系统架构设计

如果希望实现一个能支撑百亿级吞吐量的网关,那么它就应该是按照分布式架构思维做去中心化设计,支持横向扩展。让每一台网关服务都成为一个算力,把不同的微服务RPC接口,按照权重策略计算动态分配到各个算力组中,做到分布式运算的能力。

此外从设计实现上,要把网关的通信模块、管理服务、SDK、注册中心、运营平台等依次分开单独开发实现,这样才能进行独立的组合包装使用。

这就像为什么 ORM 框架在开发的时候不是与 Spring 强绑定在一起,而是开发一个独立的组件,当需要有 Spring 融合使用的时候,再单独开发一个 Mybatis-Spring 来整合服务。

所以在这里设计网关的时候也是同样的思路,就像官网的通信不应该一开始就把 Netty 相关的服务全部绑定到 Spring 容器,这样即增加了维护成本,也降低了系统的扩展性。

诸如此类的软件架构设计,都会在这套网关微服务架构中体现,整体架构如图 1-2 所示

图 1-2 网关架构设计

整个API网关设计核心内容分为这么五块;

  • 第一块:是关于通信的协议处理,也是网关最本质的处理内容。这里需要借助 NIO 框架 Netty 处理 HTTP 请求,并进行协议转换泛化调用到 RPC 服务返回数据信息。
  • 第二块:是关于注册中心,这里需要把网关通信系统当做一个算力,每部署一个网关服务,都需要向注册中心注册一个算力。而注册中心还需要接收 RPC 接口的注册,这部分可以是基于 SDK 自动扫描注册也可以是人工介入管理。当 RPC 注册完成后,会被注册中心经过AHP权重计算分配到一组网关算力上进行使用。
  • 第三块:是关于路由服务,每一个注册上来的Netty通信服务,都会与他对应提供的分组网关相关联,例如:wg/(a/b/c)/user/… a/b/c 需要匹配到 Nginx 路由配置上,以确保不同的接口调用请求到对应的 Netty 服务上。PS:如果对应错误或者为启动,可能会发生类似B站事故。
  • 第四块:责任链下插件模块的调用,鉴权、授信、熔断、降级、限流、切量等,这些服务虽然不算是网关的定义下的内容,但作为共性通用的服务,它们通常也是被放到网关层统一设计实现和使用的。
  • 第五块:管理后台,作为一个网关项目少不了一个与之对应的管理后台,用户接口的注册维护、mock测试、日志查询、流量整形、网关管理等服务。

综上系统微服务模块结构如下:

序号 系统 描述
1 api-gateway-core 网关核心系统:用于网络通信转换处理,承接http请求,调用RPC服务,责任链模块调用
2 api-gateway-admin 网关管理系统:用于网关接口后台管理,注册下线停用控制
3 api-gateway-sdk 网关注册组件:用于注解方式采集接口,发送消息注册接口
4 api-gateway-center 网关注册中心:提供网关注册中心服务,登记网关接口信息
5 api-gateway-test-provider 网关测试工程:提供RPC接口
6 api-gateway-test-consumer 网关测试工程:消费RPC接口

#四、演示:网关运行效果

趁着周末假期小傅哥已经做了一部分的功能实现,就像小傅哥以前《手写Spring》 (opens new window)《手写Mybatis》 (opens new window)一样,此项目也是渐进式的逐步完成各个模块功能的开发。并参照优秀源码级的项目架构设计,运用抽象和分治的设计技巧,解决功能间的耦合调用和服务设计。同时也结合设计原则和相应场景下的设计模式,开发出高质量易于迭代和维护的代码。部分代码实现和运行如图 1-3 所示

图 1-3 网关运行效果

  • 左侧是API网关核心通信模块,右侧是RPC(Dubbo)服务。通过对网页端发起的 http 请求,经过API网关的协议转换和对RPC的泛化调用包装结果数据并返回到页面,就是中间这张图的运行效果了。
  • 左侧工程的实现,以渐进式分拆模块逐步完成,例如: core-01(Netty通信)、core-02(泛化调用)、core-03(执行器)等,让每一个对API网关感兴趣的读者都能从中学习到;架构的分层、功能的设计、代码的实现。

#五、邀请:一起学习技术

💐以上关API网关的项目,也是小傅哥的星球【码农会锁 (opens new window)】准备带着读者一起利用周末假期学习实践的内容。现在上车你将会通过小傅哥的视频+小册+代码+直播+作业,5方面来与你一起学习,帮助你提升技术实力,为你的职业生涯续期,也为你可以走的更远,可以多赚些钱。

#1. 适合谁学

  1. 一直使用 SpringMVC,想了解分布式架构。
  2. 常年做CRUD开发,手里缺少硬核项目,面试张不开嘴。
  3. 在校准备校招,市面都是流水账项目,面试没竞争力。
  4. 希望多沉淀核心技术,让自己能公司留的下来,也能走的出去。

#2. 能学到啥

  1. 优秀架构:学习微服务架构设计方案
  2. 整洁编码:学习源码级工程编程实现
  3. 扩充技术:学习开发中间件所需技术(Netty、CGlib、SPI、GenericReference)
  4. 打开思路:学习让自己开眼界的项目(技术并不难,但没人告诉你,就走的很难!)

#3. 开发计划

  1. 每周末和假期编写API网关,并同时录制设计和实现的视频、编写小册和定期组织直播解决读者疑惑以及分享。稳定输出,可靠学习
  2. 整个系统的设计和实现,遵循分治和抽象的设计分层原则,按照微服务构建服务模块,以源码级体验学习。
  3. 关于API网关会更注重骨架的结构和核心流程的功能,把最干净的部分交给读者,让读者更加易懂,也易扩展。

【深度学习】目标检测算法总结(R-CNN、Fast R-CNN、Faster R-CNN、FPN、YOLO、SSD、RetinaNet) - 郭耀华 - 博客园

mikel阅读(365)

来源: 【深度学习】目标检测算法总结(R-CNN、Fast R-CNN、Faster R-CNN、FPN、YOLO、SSD、RetinaNet) – 郭耀华 – 博客园

目标检测是很多计算机视觉任务的基础,不论我们需要实现图像与文字的交互还是需要识别精细类别,它都提供了可靠的信息。本文对目标检测进行了整体回顾,第一部分从RCNN开始介绍基于候选区域的目标检测器,包括Fast R-CNN、Faster R-CNN 和 FPN等。第二部分则重点讨论了包括YOLO、SSD和RetinaNet等在内的单次检测器,它们都是目前最为优秀的方法。

一、基于候选区域的目标检测器

1.1  滑动窗口检测器

自从 AlexNet 获得 ILSVRC 2012 挑战赛冠军后,用 CNN 进行分类成为主流。一种用于目标检测的暴力方法是从左到右、从上到下滑动窗口,利用分类识别目标。为了在不同观察距离处检测不同的目标类型,我们使用不同大小和宽高比的窗口。

滑动窗口(从右到左,从上到下)

我们根据滑动窗口从图像中剪切图像块。由于很多分类器只取固定大小的图像,因此这些图像块是经过变形转换的。但是,这不影响分类准确率,因为分类器可以处理变形后的图像。

将图像变形转换成固定大小的图像

变形图像块被输入 CNN 分类器中,提取出 4096 个特征。之后,我们使用 SVM 分类器识别类别和该边界框的另一个线性回归器

滑动窗口检测器的系统工作流程图

下面是伪代码。我们创建很多窗口来检测不同位置的不同目标。要提升性能,一个显而易见的办法就是减少窗口数量。

for window in windows 
    patch = get_patch(image, window) 
  results = detector(patch)

1.2  选择性搜索

我们不使用暴力方法,而是用候选区域方法(region proposal method)创建目标检测的感兴趣区域(ROI)。在选择性搜索(selective search,SS)中,我们首先将每个像素作为一组。然后,计算每一组的纹理,并将两个最接近的组结合起来。但是为了避免单个区域吞噬其他区域,我们首先对较小的组进行分组。我们继续合并区域,直到所有区域都结合在一起。下图第一行展示了如何使区域增长,第二行中的蓝色矩形代表合并过程中所有可能的 ROI

图源:van de Sande et al. ICCV’11

1.3  R-CNN

R-CNN 利用候选区域方法创建了约 2000 个 ROI。这些区域被转换为固定大小的图像,并分别馈送到卷积神经网络中(将原始图像根据ROI切割、reshape再送进NN学习)。该网络架构后面会跟几个全连接层,以实现目标分类并提炼边界框。

使用候选区域、CNN、仿射层来定位目标。以下是 R-CNN 整个系统的流程图:

通过使用更少且更高质量的 ROI,R-CNN 要比滑动窗口方法更快速、更准确。

ROIs = region_proposal(image)
for ROI in ROIs:
    patch = get_patch(image, ROI) 
    results = detector(pach)

 

  • 边界框回归器

候选区域方法有非常高的计算复杂度。为了加速这个过程,我们通常会使用计算量较少的候选区域选择方法构建 ROI,并在后面使用线性回归器(使用全连接层)进一步提炼边界框。

从RCNN到SSD,这应该是最全的一份目标检测算法盘点

使用回归方法将蓝色的原始边界框提炼为红色的

1.4  Fast R-CNN

R-CNN 需要非常多的候选区域以提升准确度,但其实有很多区域是彼此重叠的,因此 R-CNN 的训练和推断速度非常慢。如果我们有 2000 个候选区域,且每一个都需要独立地馈送到 CNN 中,那么对于不同的 ROI,我们需要重复提取 2000 次特征。(R-CNN很多卷积运算是重复的

此外,CNN 中的特征图以一种密集的方式表征空间特征,那么我们能直接使用特征图代替原图来检测目标吗?

直接利用特征图计算 ROI

Fast R-CNN 使用特征提取器(CNN)先提取整个图像的特征,而不是从头开始对每个图像块提取多次。然后,我们可以将创建候选区域的方法直接应用到提取到的特征图上。例如,Fast R-CNN 选择了 VGG16 中的卷积层 conv5 输出的 Feture Map 来生成 ROI,这些关注区域随后会结合对应的特征图以裁剪为特征图块,并用于目标检测任务中。我们使用 ROI 池化将特征图块转换为固定的大小,并馈送到全连接层进行分类和定位。因为 Fast-RCNN 不会重复提取特征,因此它能显著地减少处理时间。

将候选区域直接应用于特征图,并使用 ROI 池化将其转化为固定大小的特征图块

以下是 Fast R-CNN 的流程图:

在下面的伪代码中,计算量巨大的特征提取过程从 For 循环中移出来了,因此速度得到显著提升。Fast R-CNN 的训练速度是 R-CNN 的 10 倍,推断速度是后者的 150 倍。

feature_maps = process(image)
ROIs = region_proposal(feature_maps)
for ROI in ROIs:
    patch = roi_pooling(feature_maps, ROI) 
    results = detector2(patch)

Fast R-CNN 最重要的一点就是包含特征提取器、分类器和边界框回归器在内的整个网络能通过多任务损失函数进行端到端的训练,这种多任务损失即结合了分类损失和定位损失的方法,大大提升了模型准确度。

  • ROI 池化

因为 Fast R-CNN 使用全连接层,所以我们应用 ROI 池化将不同大小的 ROI 转换为固定大小。

为简洁起见,我们先将 8×8 特征图转换为预定义的 2×2 大小。

  • 下图左上角:特征图。
  • 右上角:将 ROI(蓝色区域)与特征图重叠。
  • 左下角:将 ROI 拆分为目标维度。例如,对于 2×2 目标,我们将 ROI 分割为 4 个大小相似或相等的部分。
  • 右下角:找到每个部分的最大值,得到变换后的特征图。

输入特征图(左上),输出特征图(右下),ROI (右上,蓝色框)

按上述步骤得到一个 2×2 的特征图块,可以馈送至分类器和边界框回归器中。

1.5  Faster R-CNN

Fast R-CNN 依赖于外部候选区域方法,如选择性搜索。但这些算法在 CPU 上运行且速度很慢。在测试中,Fast R-CNN 需要 2.3 秒来进行预测,其中 2 秒用于生成 2000 个 ROI。

feature_maps = process(image)
ROIs = region_proposal(feature_maps)  # Expensive!
for ROI in ROIs
    patch = roi_pooling(feature_maps, ROI) 
    results = detector2(patch)

Faster R-CNN 采用与 Fast R-CNN 相同的设计,只是它用内部深层网络代替了候选区域方法。新的候选区域网络(RPN)在生成 ROI 时效率更高,并且以每幅图像 10 毫秒的速度运行。

Faster R-CNN 的流程图与 Fast R-CNN 相同

外部候选区域方法代替了内部深层网络

  • 候选区域网络(RPN)

候选区域网络(RPN)将第一个卷积网络的输出特征图作为输入。它在特征图上滑动一个 3×3 的卷积核,以使用卷积网络(如下所示的 ZF 网络)构建与类别无关的候选区域。其他深度网络(如 VGG 或 ResNet)可用于更全面的特征提取,但这需要以速度为代价。ZF 网络最后会输出 256 个值,它们将馈送到两个独立的全连接层,以预测边界框和两个 objectness 分数,这两个 objectness 分数度量了边界框是否包含目标。我们其实可以使用回归器计算单个 objectness 分数,但为简洁起见,Faster R-CNN 使用只有两个类别的分类器:即带有目标的类别不带有目标的类别

对于特征图中的每一个位置,RPN 会做 k 次预测。因此,RPN 将输出 4×k 个坐标和每个位置上 2×k 个得分。下图展示了 8×8 的特征图,且有一个 3×3 的卷积核执行运算,它最后输出 8×8×3 个 ROI(其中 k=3)。下图(右)展示了单个位置的 3 个候选区域。

此处有 3 种猜想,稍后我们将予以完善。由于只需要一个正确猜想,因此我们最初的猜想最好涵盖不同的形状和大小。因此,Faster R-CNN 不会创建随机边界框。相反,它会预测一些与左上角名为「锚点」的参考框相关的偏移量(如x、y)。我们限制这些偏移量的值,因此我们的猜想仍然类似于锚点。

要对每个位置进行 k 个预测,我们需要以每个位置为中心的 k 个锚点。每个预测与特定锚点相关联,但不同位置共享相同形状的锚点。

这些锚点是精心挑选的,因此它们是多样的,且覆盖具有不同比例和宽高比的现实目标。这使得我们可以以更好的猜想来指导初始训练,并允许每个预测专门用于特定的形状。该策略使早期训练更加稳定和简便。

图源:https://arxiv.org/pdf/1506.01497.pdf

Faster R-CNN 使用更多的锚点。它部署 9 个锚点框:3 个不同宽高比的 3 个不同大小的锚点框。每一个位置使用 9 个锚点,每个位置会生成 2×9 个 objectness 分数和 4×9 个坐标。

  • R-CNN 方法的性能

如下图所示,Faster R-CNN 的速度要快得多。

1.6  基于区域的全卷积神经网络(R-FCN)

假设我们只有一个特征图用来检测右眼。那么我们可以使用它定位人脸吗?应该可以。因为右眼应该在人脸图像的左上角,所以我们可以利用这一点定位整个人脸。

如果我们还有其他用来检测左眼、鼻子或嘴巴的特征图,那么我们可以将检测结果结合起来,更好地定位人脸。

现在我们回顾一下所有问题。在 Faster R-CNN 中,检测器使用了多个全连接层进行预测。如果有 2000 个 ROI,那么成本非常高。

复制代码
feature_maps = process(image)
ROIs = region_proposal(feature_maps)
for ROI in ROIs
    patch = roi_pooling(feature_maps, ROI)
    class_scores, box = detector(patch)         # Expensive!
    class_probabilities = softmax(class_scores)
复制代码

 

R-FCN 通过减少每个 ROI 所需的工作量实现加速(去掉了全连接层)。上面基于区域的特征图与 ROI 是独立的,可以在每个 ROI 之外单独计算。剩下的工作就比较简单了,因此 R-FCN 的速度比 Faster R-CNN 快。

复制代码
feature_maps = process(image)
ROIs = region_proposal(feature_maps)         
score_maps = compute_score_map(feature_maps)
for ROI in ROIs
    V = region_roi_pool(score_maps, ROI)     
    class_scores, box = average(V)                   # Much simpler!
    class_probabilities = softmax(class_scores)
复制代码

现在我们来看一下 5 × 5 的特征图 M,内部包含一个蓝色方块。我们将方块平均分成 3 × 3 个区域。现在,我们在 M 中创建了一个新的特征图,来检测方块的左上角(TL)。这个新的特征图如下图(右)所示。只有黄色的网格单元 [2, 2] 处于激活状态。

在左侧创建一个新的特征图,用于检测目标的左上角

我们将方块分成 9 个部分,由此创建了 9 个特征图,每个用来检测对应的目标区域。这些特征图叫作位置敏感得分图(position-sensitive score map),因为每个图检测目标的子区域(计算其得分)。

生成 9 个得分图

下图中红色虚线矩形是建议的 ROI。我们将其分割成 3 × 3 个区域,并询问每个区域包含目标对应部分的概率是多少。例如,左上角 ROI 区域包含左眼的概率。我们将结果存储成 3 × 3 vote 数组,如下图(右)所示。例如,vote_array[0][0] 包含左上角区域是否包含目标对应部分的得分。

将 ROI 应用到特征图上,输出一个 3 x 3 数组

将得分图(Feature Map)和 ROI 映射到 vote 数组的过程叫作位置敏感 ROI 池化(position-sensitive ROI-pool)。该过程与前面讨论过的 ROI 池化非常接近。

将 ROI 的一部分叠加到对应的得分图上,计算 V[i][j]

在计算出位置敏感 ROI 池化的所有值后,类别得分是其所有元素得分的平均值。

ROI 池化

假如我们有 C 个类别要检测。我们将其扩展为 C + 1 个类别,这样就为背景(非目标)增加了一个新的类别。每个类别有 3 × 3 个得分图,因此一共有 (C+1) × 3 × 3 个得分图。使用每个类别的得分图可以预测出该类别的类别得分。然后我们对这些得分应用 softmax 函数,计算出每个类别的概率。

以下是数据流图,在我们的案例中,k=3。

 

1.7  R-CNN系列总结

我们首先了解了基础的滑动窗口算法:

for window in windows
    patch = get_patch(image, window)
    results = detector(patch)

然后尝试减少窗口数量,尽可能减少 for 循环中的工作量。

ROIs = region_proposal(image)
for ROI in ROIs
    patch = get_patch(image, ROI)
    results = detector(patch)

 

二、单次目标检测器

第二部分,我们将对单次目标检测器(包括 SSD、YOLO、YOLOv2、YOLOv3)进行综述。我们将分析 FPN 以理解多尺度特征图如何提高准确率,特别是小目标的检测,其在单次检测器中的检测效果通常很差。然后我们将分析 Focal loss 和 RetinaNet,看看它们是如何解决训练过程中的类别不平衡问题的。

2.1  单次检测器

Faster R-CNN 中,在分类器之后有一个专用的候选区域网络。

Faster R-CNN 工作流

基于区域的检测器是很准确的,但需要付出代价。Faster R-CNN 在 PASCAL VOC 2007 测试集上每秒处理 7 帧的图像(7 FPS)。和 R-FCN 类似,研究者通过减少每个 ROI 的工作量来精简流程。

feature_maps = process(image)
ROIs = region_proposal(feature_maps)
for ROI in ROIs
    patch = roi_align(feature_maps, ROI)
    results = detector2(patch)    # Reduce the amount of work here!

作为替代,我们是否需要一个分离的候选区域步骤?我们可以直接在一个步骤内得到边界框和类别吗?

feature_maps = process(image)
results = detector3(feature_maps) # No more separate step for ROIs

让我们再看一下滑动窗口检测器。我们可以通过在特征图上滑动窗口来检测目标。对于不同的目标类型,我们使用不同的窗口类型。以前的滑动窗口方法的致命错误在于使用窗口作为最终的边界框,这就需要非常多的形状来覆盖大部分目标。更有效的方法是将窗口当做初始猜想,这样我们就得到了从当前滑动窗口同时预测类别和边界框的检测器。

基于滑动窗口进行预测

这个概念和 Faster R-CNN 中的锚点很相似。然而,单次检测器会同时预测边界框和类别。例如,我们有一个 8 × 8 特征图,并在每个位置做出 k 个预测,即总共有 8 × 8 × k 个预测结果。

64 个位置

在每个位置,我们有 k 个锚点(锚点是固定的初始边界框猜想),一个锚点对应一个特定位置。我们使用相同的锚点形状仔细地选择锚点和每个位置。

使用 4 个锚点在每个位置做出 4 个预测

以下是 4 个锚点(绿色)和 4 个对应预测(蓝色),每个预测对应一个特定锚点。

4 个预测,每个预测对应一个锚点

在 Faster R-CNN 中,我们使用卷积核来做 5 个参数的预测:4 个参数对应某个锚点的预测边框,1 个参数对应 objectness 置信度得分。因此 3× 3× D × 5 卷积核将特征图从 8 × 8 × D 转换为 8 × 8 × 5。

使用 3×3 卷积核计算预测

在单次检测器中,卷积核还预测 C 个类别概率以执行分类(每个概率对应一个类别)。因此我们应用一个 3× 3× D × 25 卷积核将特征图从 8 × 8 × D 转换为 8 × 8 × 25(C=20)。

每个位置做出 k 个预测,每个预测有 25 个参数

单次检测器通常需要在准确率和实时处理速度之间进行权衡。它们在检测太近距离或太小的目标时容易出现问题。在下图中,左下角有 9 个圣诞老人,但某个单次检测器只检测出了 5 个。

 

2.2  SSD (Single Shot MultiBox Detector)

SSD 是使用 VGG19 网络作为特征提取器(和 Faster R-CNN 中使用的 CNN 一样)的单次检测器。我们在该网络之后添加自定义卷积层(蓝色),并使用卷积核(绿色)执行预测。

同时对类别和位置执行单次预测

然而,卷积层降低了空间维度和分辨率。因此上述模型仅可以检测较大的目标为了解决该问题,我们从多个特征图上执行独立的目标检测。采用多尺度特征图独立检测。

使用多尺度特征图用于检测

以下是特征图图示。

图源:https://arxiv.org/pdf/1512.02325.pdf

SSD 使用卷积网络中较深的层来检测目标。如果我们按接近真实的比例重绘上图,我们会发现图像的空间分辨率已经被显著降低,且可能已无法定位在低分辨率中难以检测的小目标。如果出现了这样的问题,我们需要增加输入图像的分辨率。

2.3  YOLO

YOLO 是另一种单次目标检测器。

YOLO  在卷积层之后使用了 DarkNet 来做特征检测。

然而,它并没有使用多尺度特征图来做独立的检测。相反,它将特征图部分平滑化,并将其和另一个较低分辨率的特征图拼接。例如,YOLO 将一个 28 × 28 × 512 的层重塑为 14 × 14 × 2048,然后将它和 14 × 14 ×1024 的特征图拼接。之后,YOLO 在新的 14 × 14 × 3072 层上应用卷积核进行预测。

YOLO(v2)做出了很多实现上的改进,将 mAP 值从第一次发布时的 63.4 提高到了 78.6。YOLO9000 可以检测 9000 种不同类别的目标。

图源:https://arxiv.org/pdf/1612.08242.pdf

以下是 YOLO 论文中不同检测器的 mAP 和 FPS 对比。YOLOv2 可以处理不同分辨率的输入图像。低分辨率的图像可以得到更高的 FPS,但 mAP 值更低。

图源:https://arxiv.org/pdf/1612.08242.pdf

  YOLOv3 使用了更加复杂的骨干网络来提取特征。DarkNet-53 主要由 3 × 3 和 1× 1 的卷积核以及类似 ResNet 中的跳过连接构成。相比 ResNet-152,DarkNet 有更低的 BFLOP(十亿次浮点数运算),但能以 2 倍的速度得到相同的分类准确率。

图源:https://pjreddie.com/media/files/papers/YOLOv3.pdf

YOLOv3 还添加了特征金字塔,以更好地检测小目标。以下是不同检测器的准确率和速度的权衡。

图源:https://pjreddie.com/media/files/papers/YOLOv3.pdf

特征金字塔网络(FPN)

检测不同尺度的目标很有挑战性,尤其是小目标的检测。特征金字塔网络(FPN)是一种旨在提高准确率和速度的特征提取器。它取代了检测器(如 Faster R-CNN)中的特征提取器,并生成更高质量的特征图金字塔。

数据流

FPN(图源:https://arxiv.org/pdf/1612.03144.pdf)

FPN 由自下而上和自上而下路径组成。其中自下而上的路径是用于特征提取的常用卷积网络。空间分辨率自下而上地下降。当检测到更高层的结构,每层的语义值增加。

FPN 中的特征提取(编辑自原论文)

SSD 通过多个特征图完成检测。但是,最底层不会被选择执行目标检测。它们的分辨率高但是语义值不够,导致速度显著下降而不能被使用。SSD 只使用较上层执行目标检测,因此对于小的物体的检测性能较差。

图像修改自论文 https://arxiv.org/pdf/1612.03144.pdf

FPN 提供了一条自上而下的路径,从语义丰富的层构建高分辨率的层。

自上而下重建空间分辨率(编辑自原论文)

虽然该重建层的语义较强,但在经过所有的上采样和下采样之后,目标的位置不精确。在重建层和相应的特征图之间添加横向连接可以使位置侦测更加准确。

增加跳过连接(引自原论文)

下图详细说明了自下而上和自上而下的路径。其中 P2、P3、P4 和 P5 是用于目标检测的特征图金字塔。

FPN 结合 RPN

FPN 不单纯是目标检测器,还是一个目标检测器和协同工作的特征检测器。分别传递到各个特征图(P2 到 P5)来完成目标检测。

FPN 结合 Fast R-CNN 或 Faster R-CNN

在 FPN 中,我们生成了一个特征图的金字塔。用 RPN(详见上文)来生成 ROI。基于 ROI 的大小,我们选择最合适尺寸的特征图层来提取特征块。

困难案例

对于如 SSD 和 YOLO 的大多数检测算法来说,我们做了比实际的目标数量要多得多的预测。所以错误的预测比正确的预测要更多。这产生了一个对训练不利的类别不平衡。训练更多的是在学习背景,而不是检测目标。但是,我们需要负采样来学习什么是较差的预测。所以,我们计算置信度损失来把训练样本分类。选取最好的那些来确保负样本和正样本的比例最多不超过 3:1。这使训练更加快速和稳定。

推断过程中的非极大值抑制

检测器对于同一个目标会做出重复的检测。我们利用非极大值抑制来移除置信度低的重复检测。将预测按照置信度从高到低排列。如果任何预测和当前预测的类别相同并且两者 IoU 大于 0.5,我们就把它从这个序列中剔除。

Focal Loss(RetinaNet)

类别不平衡会损害性能。SSD 在训练期间重新采样目标类和背景类的比率,这样它就不会被图像背景淹没。Focal loss(FL)采用另一种方法来减少训练良好的类的损失。因此,只要该模型能够很好地检测背景,就可以减少其损失并重新增强对目标类的训练。我们从交叉熵损失 CE 开始,并添加一个权重来降低高可信度类的 CE。

例如,令 γ = 0.5, 经良好分类的样本的 Focal loss 趋近于 0。

编辑自原论文

这是基于 FPN、ResNet 以及利用 Focal loss 构建的 RetianNet

RetinaNet

 

原文链接:https://medium.com/@jonathan_hui/what-do-we-learn-from-region-based-object-detectors-faster-r-cnn-r-fcn-fpn-7e354377a7c9
https://medium.com/@jonathan_hui/what-do-we-learn-from-single-shot-object-detectors-ssd-yolo-fpn-focal-loss-3888677c5f4d

深度剖析YOLO系列的原理 - w_x_w1985 - 博客园

mikel阅读(356)

来源: 深度剖析YOLO系列的原理 – w_x_w1985 – 博客园

                     深度剖析YOLO系列的原理

本文系作者原创,转载请注明出处:https://www.cnblogs.com/further-further-further/p/12072225.html

目录

1. YOLO的作用

2. YOLO(v1,v2,v3)的技术演化

 


 

 

 

1. YOLO的作用

yolo是当前目标检测最顶级的算法之一,v1 版本是2016年提出来的,v2 是2017年提出来的,v3 是2018年提出的。

官网地址:https://pjreddie.com/darknet/yolo/

说它最牛掰,有两点:

一是因为它采用深层卷积神经网络,吸收了当前很多经典卷积神经网络架构的优秀思想,在位置检测和对象的识别方面,

性能达到最优(准确率非常高的情况下还能达到实时检测)。

二是因为作者还将代码开源了。真心为作者这种大公无私的心胸点赞。

美中不足的是:作者虽然将代码开源,但是在论文介绍架构原理的时候比较模糊,特别是对一些重要改进,基本上是一笔带过。

现在在网络上有很多关于 YOLO 原理的讲解,个人感觉讲得不是很清楚,总感觉有点知其然而不知其所以然。比如:

yolo 是在什么背景下出现的?

v1 是在哪个经典网络架构上发展起来的?解决了什么问题?又存在什么问题?

v2 针对v1的缺点做了哪些改进?改进后效果怎么样?又存在什么问题?

v3 针对v2的缺点做了哪些改进?

这些问题不搞清楚,我觉得对 yolo 就谈不上真正的理解。废话不多说,下面就来介绍 yolo 的技术演进。

 

2. YOLO(v1,v2,v3)的技术演化

问题1:yolo v1是在什么背景下出现的?

yolo v1是在 R-CNN 基础上发展起来的。

R-CNN(region proposals + cnn),采用卷积神经网络进行目标检测的开山之作。

 

 

主要思想:对输入图片采用selective search 搜索查询算法,提取出大约 2000 个人眼感兴趣的候选边框,然后每个边框都通过一个独立的

卷积神经网络进行预测输出,后面再加上一个SVM(支持向量机)预测分类。

优点:位置检测与对象分类准确率非常高。

缺点:运算量大,检测速度非常慢,在 GPU 加持下一帧检测时间要 13s 左右,这在工程应用上是不可接受的。

造成速度慢的原因主要有 2 个:

> 用搜索查询算法提取 2000 个候选边框

> 每个候选边框都采用一个独立的CNN通道,这意味着卷积核的参数和全连接的参数都是不一样的,总的参数个数是非常恐怖的。

针对这个缺点,yolo v1 做了哪些改进呢。

 

 

上面的图片是官网论文给出的结构图,但是个人感觉画得不太好,它容易让人误认为对输入图片进行网格化处理,每个网格化的小窗口是 7*7。

真实情况是,由最终输出是 7*7*30 大小代表的物理含义是:你可以把输入图片看成是经过了网格化(grid cell 7*7),每个网格化后的小窗口通过 CNN 预测出 1*30。

这里可能有点不好理解。总之一句话,在真正网络架构流程中,没有对输入图片进行任何网格化处理。

 

v1 改进点:

> Backbone: googLeNet22 (采用googLeNet22层卷积结构)

> 输入图片只处理一次(yolo名称的由来),通过多个卷积层提取不同的特征,每次卷积的时候共享卷积核参数。

输入图片只处理一次:表示输入图片只在第一次卷积的时候作为输入进行卷积运算,第一次卷积后的输出作为第二次卷积的输入,通过多个卷积层递进的方式来提取不同的特征。

并且这些特征通过的CNN通道都是相同的,从而可以共享卷积核参数。

> 每个 3*3 卷积核前面引入了 1*1 卷积核,作用有两个,一是提取更丰富的特征,二是减少卷积核的参数个数。

大家可能对减少卷积核参数个数的作用比较难以理解,这里举个例子。

比如输入图片大小是 56*56*256 最终转化的目标大小是 28*28*512。

直接卷积:56*56*256 & 3*3*256*512 -> 56*56*512 & pooling -> 28*28*512

卷积核参数:3*3*256*512 = 1179648

引入 1*1 卷积:56*56*256 & 1*1*256*128 -> 56*56*128 & 3*3*128*512 -> 56*56*512 & pooling -> 28*28*512

卷积核参数:1*1*256*128 + 3*3*128*512 = 622592

经过改进后,图片检测速度非常快,基本上可以达到实时。但是缺点是位置检测准确度低,并且不能检测出小对象物体。

针对 v1 的缺点,v2 做了哪些改进呢?

v2 改进点:

 

 

 

 

> Backbone:darknet19 (采用darknet 19层卷积结构)

> 输入图片批归一化处理(BN),作用是降低不是重要特征的重要性。

这句话可能听得有点晕,举个例子,

你的样本里有两个特征列,一个特征列的数值在[1,10]范围内,另一个特征列的数值在[1000,10000]范围内,

但是真实情况是,你的这两个特征列重要性可能是一样的,只不过你拿到的数据就是这样的,我们知道,

卷积神经网络训练,实质就是一系列数值运算的过程,如果你将这两个特征列直接通过卷积神经网络进行训练,

那最终生成的模型准确率肯定是不高的,所以需要进行归一化处理,将数值归一化到[0,1]范围内,

从而使损失能量在收敛的时候更加平稳。

> 采用 passthrough 算法,解决池化信息丢失的问题,增加细粒度特征的检测(小对象)。

 

 

passthrough 算法主要是为了解决 pooling(池化)的缺点,不管是最大值池化,还是平均值池化,都有一个很明显的问题,

就是会造成信息丢失,passthrough 主要思想是在池化之前,将输入信息进行拆分,一拆为四,经过拆分后的大小就和池化后的输出大小相同,

然后叠加,叠加后的结果主要就是维度变化,这样就能解决池化会造成信息丢失的问题。

> 去掉全连接(FC),将输入图片拉伸(resize)到不同尺寸然后通过卷积神经网络,这样就得到了多尺寸的输出,从而能提升对不同大小对象的预测准确度。

全连接其实就是矩阵乘法运算,矩阵乘法有一个前提,矩阵 A 的列必须与矩阵 B 的行个数相同,否则是不能进行矩阵乘法运算的。

全连接的参数大小是固定的,那么你的输入大小自然就固定了,这样就无法实现多尺寸的输出,所以这里去掉了全连接层。

经过改进后,精度提升明显,特别是对小对象的检测,缺点是对小对象检测准确度不高。

针对 v2 的缺点,v3 又做了哪些改进呢?

v3 改进点:

 

 

上面是 v3 的结构图,是我跟踪代码,查找资料,绘制这张图真心不容易,正确性绝对有保证,大家如果觉得这张图对你理解 yolo 有帮助,麻烦点个赞。

输入大小这里是416*416,输出13*13,26*26,52*52,这里一般要求输入图片大小是 32 的倍数,因为整个卷积神经网络会将图片缩小32倍,16倍,8倍,

这里取最大公倍数32。 123 = 3*(边框坐标 4 + 置信度 1 + 类对象 36)。

> Backbone: darknet53 (采用darknet 53层卷积结构,实际是52层卷积,去掉了全连接层)

可以看出 v3 的卷积层数大约是 v2 的 2.8倍,有个潜在的共识:增加模型准确率的一个直接做法是增加网络的深度和厚度,

(深度是指卷积层数,厚度指卷积核的维度或者是种类数),这里作用自然就是提升精度了。

> 用卷积取代池化

之前我们提到过池化的问题,会造成信息丢失,这里用卷积来实现池化的功能(使图片大小缩小2倍),同时不会造成信息的明显丢失。

> 采用残差网络(resnet)防止梯度消失

梯度消失或者梯度爆炸是在深层的卷积神经网络中才有可能出现的,梯度的计算是通过链式求导得到的,随着网络层的增加,链式求导项就会越来越长,

因为在每一层卷积后的输出都做了归一化处理,所以梯度只会越来越小,有可能为0,而0在后面模块运算中都为0,这样导致的直接后果是:

损失能量在收敛到某一阶段后就停止收敛,最后生成模型的精度自然就不高。而这里采用残差网络就能防止梯度消失,v3 结构图里左下角就是残差网络的结构图。

残差网络的思想:每次卷积后的输出当做残差,将卷积前的输入与残差融合,作为整个输出,即使残差为0,整个输出也不会为0。

从残差网络结构图可以看出,每个3*3卷积核前都引入了1*1卷积,这里保留了 v1 的优秀思想。

从 v3 的结构图可以看出,darknet53网络骨架里大量的引入了残差网络的思想。

> 使用空间金字塔池化网络算法(sppnet spatial pyramid pooling)实现多尺寸的输出

空间金字塔池化网络算法主要思想:不同尺寸的输入通过sppnet模块后生成一个固定尺寸的输出。

在 v3 结构图里,有两个地方用到这个思想:

一个是 13*13*512 经过 1*1 卷积,改变特征维度,变成 13*13*256,经过上采样(upsample,这里采用相邻像素插入算法),

改变特征尺寸,变成 26*26*256,然后与 26*26*512 叠加,生成 26*26*768。

另一个是 26*26*256 与 52*52*256 叠加后生成 52*52*384。

v3 的多尺寸输出与 v2 的多尺寸输出有本质不同,v2多尺寸输出是对输入图片拉伸到不同的尺寸,然后通过卷积神经网络得到不同的输出,

但是这样就存在一个图片失真的问题,因为你是对图片进行的拉伸处理。而 v3 通过sppnet实现的多尺寸输出,就能有效避免图片失真的问题。

> 13*13*123,26*26*123,52*52*123物理意义

 

 

这里用 v1 版本论文图片来解释,物理意义:

表示将输入图片网格化,有 13*13,26*26,52*52 大小,每个网格化的小窗口(grid cell)预测 3 个边框(bounding box),

每个边框包含 4 个位置坐标,1个置信度,36个对象种类。

123 = 3 *(4 + 1 + 36)

这里就存在一个问题:预测输出如此之多,直接用于损失能量的计算,运算量岂不是很恐怖?

确实是这样,所以这里先经过下面的两个步骤的处理:

(1)每个小窗口只取置信度最大的边框,因为yolo规定,只能有一个真实的对象中心坐标属于每个小窗口。

这样,就得出 13*13*3*41 => 13*13*41,26*26*3*41 => 26*26*41,52*52*3*41 => 52*52*41

13*13*41,26*26*41,52*52*41 表示一个真实对象有很多预测重叠边框,比如说上面图里属于狗的预测边框非常多,

但是我们只需要预测最准确的边框,去掉其他属于狗的重叠边框。

(2)采用NMS(非极大值抑制算法)去除重叠边框。

这样 13*13*41,26*26*41,52*52*41 => N*41 (N表示不同对象预测数目,比如说上面图理想情况下,N = 3)。

> 损失能量(采用交叉熵)

 

 

损失能量的计算是v1版本提出来的,这里放到了 v3 来说,有3个改进点:

(1)将位置检测与对象识别作为一个整体,进行训练预测,这从损失能量的计算可以直接反应出来。

(2)位置的宽度和高度先开根号,与归一化的作用相同,降低不是重要特征的重要性。

(3)增加权重参数  ,当边框预测出含有对象时,增大它的权重值,当边框预测出不含有对象时,减小它的权重值,这样就能使损失能量计算更准确。

 

yolo类检测算法解析——yolo v3 - greathuman - 博客园

mikel阅读(380)

来源: yolo类检测算法解析——yolo v3 – greathuman – 博客园

每当听到有人问“如何入门计算机视觉”这个问题时,其实我内心是拒绝的,为什么呢?因为我们说的计算机视觉的发展史可谓很长了,它的分支很多,而且理论那是错综复杂交相辉映,就好像数学一样,如何学习数学?这问题似乎有点笼统、有点宽泛。所以我都会具体问问你想入门计算机视觉的哪个话题,只有顺着一个话题理论联合实际,才有可能扩展到几个话题。

yolo类算法,从开始到现在已经有了3代,我们称之为v1、v2、v3,一路走来,让人能感觉到的是算法的性能在不断的改进,以至于现在成为了开源通用目标检测算法的领头羊(ps:虽然本人一直都很欣赏SSD,但是不得不说V3版本已经达到目前的颠覆)。一直以来,有一个问题困扰许久,那就是如何检测两个距离很近的同类的物体,当然又或者是距离很近的不同类的物体?绝大部分算法都会对传入的data做resize到一个更小的resolution,它们对于这种情况都会给出一个目标框,因为在它们的特征提取或者回归过程看来,这就是一个物体(可想本来就很近,一放缩之间的近距离越发明显了),而事实上这是两个同(或不同)类型的物体靠的很近,这个难题是目标检测和跟踪领域的一个挑战。就好像对小目标的检测,一直以来也被看做是算法的一种评估。但是啊,v3版本却做到了,它对这种距离很近的物体或者小物体有很好的鲁棒性,虽然不能保证百分百,但是这个难题得到了很大程度的解决,激发我对yolo类算法的研究。这也是为什么写这篇文章的目的,在于见证一下这个算法的神奇。其实,百分百的检测,在我看来事实上是不存在的,随着时间的推移,环境的变化,任何妄言百分百准确的算法都是扯,只能是相互调整吧。前几天uber撞人事件其实我最关注的应该是哪个环节存在的问题,还需要改进,撞人是不可避免的,无人车的存在不是让事故不发生,而是让社会进步,科技发展,逐步降低事故发生率的同时改善人们的生活质量。

yolo的v1和v2都不如SSD算法,原谅这么直白,原因是v1版本的448和v2版本的416都不如SSD的300,当然以上结论都是实验测的,v3版本的416应该比SSD512好,可见其性能。

对官方yolo做了实验,实验中,采用同一个视频、同一张显卡,在阈值为0.3的前提下,对比了v3和v2的测试效果之后,有了下面两个疑问:

1.为什么v3和v2版本的测试性能提高很大,但速度却没有降低?

2.为什么v3性能上能有这么大的改进?或者说为什么v3在没有提高输入数据分辨率的前提下,对小目标检测变得这么好?

要回答上述两个问题,必须要看看作者发布的v3论文了,将v3和v2不一样的地方总结一下:

  • loss不同:作者v3替换了v2的softmax loss 变成logistic loss,而且每个ground truth只匹配一个先验框。
  • anchor bbox prior不同:v2作者用了5个anchor,一个折衷的选择,所以v3用了9个anchor,提高了IOU。
  • detection的策略不同:v2只有一个detection,v3一下变成了3个,分别是一个下采样的,feature map为13*13,还有2个上采样的eltwise sum,feature map为26*26,52*52,也就是说v3的416版本已经用到了52的feature map,而v2把多尺度考虑到训练的data采样上,最后也只是用到了13的feature map,这应该是对小目标影响最大的地方。
  • backbone不同:这和上一点是有关系的,v2的darknet-19变成了v3的darknet-53,为啥呢?就是需要上采样啊,卷积层的数量自然就多了,另外作者还是用了一连串的3*3、1*1卷积,3*3的卷积增加channel,而1*1的卷积在于压缩3*3卷积后的特征表示,这波操作很具有实用性,一增一减,效果棒棒。

为什么有这么大的提高?我指的是v2和v3比,同样是416的feature map,我感觉是v2作者当时也是做了很多尝试和借鉴,实现了匹敌SSD的效果,但是他因为被借鉴的内容所困扰,导致性能的停留,因此v3再借鉴,应该是参考了DSSD和FPN,这应该是之后的潮流了,做了一下结果性能提高很大,可能作者本人都没想到。但是作者目前没有写篇论文,认为没有创造性实质性的改变,写了一个report,科研的精神值得肯定!如果对比v2和v3你会发现反差确实很大,所以上面的问题才不奇怪。

又为什么速度没有下降?电脑上同环境测都是15帧左右。先看一下打印的日志:

 v2的日志信息:

复制代码
Demo
layer     filters    size              input                output
    0 conv     32  3 x 3 / 1   416 x 416 x   3   ->   416 x 416 x  32  0.299 BFLOPs
    1 max          2 x 2 / 2   416 x 416 x  32   ->   208 x 208 x  32
    2 conv     64  3 x 3 / 1   208 x 208 x  32   ->   208 x 208 x  64  1.595 BFLOPs
    3 max          2 x 2 / 2   208 x 208 x  64   ->   104 x 104 x  64
    4 conv    128  3 x 3 / 1   104 x 104 x  64   ->   104 x 104 x 128  1.595 BFLOPs
    5 conv     64  1 x 1 / 1   104 x 104 x 128   ->   104 x 104 x  64  0.177 BFLOPs
    6 conv    128  3 x 3 / 1   104 x 104 x  64   ->   104 x 104 x 128  1.595 BFLOPs
    7 max          2 x 2 / 2   104 x 104 x 128   ->    52 x  52 x 128
    8 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
    9 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   10 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   11 max          2 x 2 / 2    52 x  52 x 256   ->    26 x  26 x 256
   12 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   13 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   14 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   15 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   16 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   17 max          2 x 2 / 2    26 x  26 x 512   ->    13 x  13 x 512
   18 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   19 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   20 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   21 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   22 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   23 conv   1024  3 x 3 / 1    13 x  13 x1024   ->    13 x  13 x1024  3.190 BFLOPs
   24 conv   1024  3 x 3 / 1    13 x  13 x1024   ->    13 x  13 x1024  3.190 BFLOPs
   25 route  16
   26 conv     64  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x  64  0.044 BFLOPs
   27 reorg              / 2    26 x  26 x  64   ->    13 x  13 x 256
   28 route  27 24
   29 conv   1024  3 x 3 / 1    13 x  13 x1280   ->    13 x  13 x1024  3.987 BFLOPs
   30 conv    125  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 125  0.043 BFLOPs
   31 detection
mask_scale: Using default '1.000000'
Loading weights from yolo-voc.weights...Done!
复制代码

v3的日志信息:

复制代码
Demo
layer     filters    size              input                output
    0 conv     32  3 x 3 / 1   416 x 416 x   3   ->   416 x 416 x  32  0.299 BFLOPs
    1 conv     64  3 x 3 / 2   416 x 416 x  32   ->   208 x 208 x  64  1.595 BFLOPs
    2 conv     32  1 x 1 / 1   208 x 208 x  64   ->   208 x 208 x  32  0.177 BFLOPs
    3 conv     64  3 x 3 / 1   208 x 208 x  32   ->   208 x 208 x  64  1.595 BFLOPs
    4 res    1                 208 x 208 x  64   ->   208 x 208 x  64
    5 conv    128  3 x 3 / 2   208 x 208 x  64   ->   104 x 104 x 128  1.595 BFLOPs
    6 conv     64  1 x 1 / 1   104 x 104 x 128   ->   104 x 104 x  64  0.177 BFLOPs
    7 conv    128  3 x 3 / 1   104 x 104 x  64   ->   104 x 104 x 128  1.595 BFLOPs
    8 res    5                 104 x 104 x 128   ->   104 x 104 x 128
    9 conv     64  1 x 1 / 1   104 x 104 x 128   ->   104 x 104 x  64  0.177 BFLOPs
   10 conv    128  3 x 3 / 1   104 x 104 x  64   ->   104 x 104 x 128  1.595 BFLOPs
   11 res    8                 104 x 104 x 128   ->   104 x 104 x 128
   12 conv    256  3 x 3 / 2   104 x 104 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   13 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   14 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   15 res   12                  52 x  52 x 256   ->    52 x  52 x 256
   16 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   17 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   18 res   15                  52 x  52 x 256   ->    52 x  52 x 256
   19 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   20 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   21 res   18                  52 x  52 x 256   ->    52 x  52 x 256
   22 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   23 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   24 res   21                  52 x  52 x 256   ->    52 x  52 x 256
   25 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   26 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   27 res   24                  52 x  52 x 256   ->    52 x  52 x 256
   28 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   29 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   30 res   27                  52 x  52 x 256   ->    52 x  52 x 256
   31 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   32 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   33 res   30                  52 x  52 x 256   ->    52 x  52 x 256
   34 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
   35 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
   36 res   33                  52 x  52 x 256   ->    52 x  52 x 256
   37 conv    512  3 x 3 / 2    52 x  52 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   38 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   39 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   40 res   37                  26 x  26 x 512   ->    26 x  26 x 512
   41 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   42 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   43 res   40                  26 x  26 x 512   ->    26 x  26 x 512
   44 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   45 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   46 res   43                  26 x  26 x 512   ->    26 x  26 x 512
   47 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   48 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   49 res   46                  26 x  26 x 512   ->    26 x  26 x 512
   50 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   51 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   52 res   49                  26 x  26 x 512   ->    26 x  26 x 512
   53 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   54 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   55 res   52                  26 x  26 x 512   ->    26 x  26 x 512
   56 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   57 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   58 res   55                  26 x  26 x 512   ->    26 x  26 x 512
   59 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   60 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   61 res   58                  26 x  26 x 512   ->    26 x  26 x 512
   62 conv   1024  3 x 3 / 2    26 x  26 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   63 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   64 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   65 res   62                  13 x  13 x1024   ->    13 x  13 x1024
   66 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   67 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   68 res   65                  13 x  13 x1024   ->    13 x  13 x1024
   69 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   70 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   71 res   68                  13 x  13 x1024   ->    13 x  13 x1024
   72 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   73 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   74 res   71                  13 x  13 x1024   ->    13 x  13 x1024
   75 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   76 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   77 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   78 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   79 conv    512  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 512  0.177 BFLOPs
   80 conv   1024  3 x 3 / 1    13 x  13 x 512   ->    13 x  13 x1024  1.595 BFLOPs
   81 conv    255  1 x 1 / 1    13 x  13 x1024   ->    13 x  13 x 255  0.088 BFLOPs
   82 detection
   83 route  79
   84 conv    256  1 x 1 / 1    13 x  13 x 512   ->    13 x  13 x 256  0.044 BFLOPs
   85 upsample            2x    13 x  13 x 256   ->    26 x  26 x 256
   86 route  85 61
   87 conv    256  1 x 1 / 1    26 x  26 x 768   ->    26 x  26 x 256  0.266 BFLOPs
   88 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   89 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   90 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   91 conv    256  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 256  0.177 BFLOPs
   92 conv    512  3 x 3 / 1    26 x  26 x 256   ->    26 x  26 x 512  1.595 BFLOPs
   93 conv    255  1 x 1 / 1    26 x  26 x 512   ->    26 x  26 x 255  0.177 BFLOPs
   94 detection
   95 route  91
   96 conv    128  1 x 1 / 1    26 x  26 x 256   ->    26 x  26 x 128  0.044 BFLOPs
   97 upsample            2x    26 x  26 x 128   ->    52 x  52 x 128
   98 route  97 36
   99 conv    128  1 x 1 / 1    52 x  52 x 384   ->    52 x  52 x 128  0.266 BFLOPs
  100 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
  101 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
  102 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
  103 conv    128  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 128  0.177 BFLOPs
  104 conv    256  3 x 3 / 1    52 x  52 x 128   ->    52 x  52 x 256  1.595 BFLOPs
  105 conv    255  1 x 1 / 1    52 x  52 x 256   ->    52 x  52 x 255  0.353 BFLOPs
  106 detection
Loading weights from yolov3.weights...Done!
复制代码
百度百科:FLOPS(即“每秒浮点运算次数”,“每秒峰值速度”),是“每秒所执行的浮点运算次数”(floating-point operations per second)的缩写。它常被用来估算电脑的执行效能,尤其是在使用到大量浮点运算的科学计算领域中。正因为FLOPS字尾的那个S,代表秒,而不是复数,所以不能省略掉。
在这里所谓的“浮点运算”,实际上包括了所有涉及小数的运算。这类运算在某类应用软件中常常出现,而它们也比整数运算更花时间。现今大部分的处理器中,都有一个专门用来处理浮点运算的“浮点运算器”(FPU)。也因此FLOPS所量测的,实际上就是FPU的执行速度。而最常用来测量FLOPS的基准程式(benchmark)之一,就是Linpack
可能的原因:yolov2是一个纵向自上而下的网络架构,随着channel数目的不断增加,FLOPS是不断增加的,而v3网络架构是横纵交叉的,看着卷积层多,其实很多多channel的卷积层没有继承性,另外,虽然yolov3增加了anchor centroid,但是对ground truth的估计变得更加简单,每个ground truth只匹配一个先验框,而且每个尺度只预测3个框,v2预测5个框。这样的话也降低了复杂度。

所以这发展的历程应该是这样的:

yolo——SSD——yolov2——FPN、Focal loss、DSSD……——yolov3

最后总结,yolo算法的性能一直都没有被v2发挥出来,而真正被v3发挥出来了,v3这次的借鉴效果实在是太好了。

欢迎加入QQ交流群864933024

YOLO-V4源码详解 - 爱旅行的球迷Engineer - 博客园

mikel阅读(379)

来源: YOLO-V4源码详解 – 爱旅行的球迷Engineer – 博客园

一. 整体架构

整体架构和YOLO-V3相同(感谢知乎大神@江大白),创新点如下:

输入端 –> Mosaic数据增强、cmBN、SAT自对抗训练;

BackBone –> CSPDarknet53、Mish激活函数、Dropblock;

Neck –> SPP、FPN+PAN结构;

Prediction –> GIOU_Loss、DIOU_nms。

二. 输入端

1. 数据加载流程(以训练为例)

“darknet/src/darknet.c”–main()函数:模型入口。

复制代码
......
    // 根据指令进入不同的函数。
    if (0 == strcmp(argv[1], "average")){
        average(argc, argv);
    } else if (0 == strcmp(argv[1], "yolo")){
        run_yolo(argc, argv);
    } else if (0 == strcmp(argv[1], "voxel")){
        run_voxel(argc, argv);
    } else if (0 == strcmp(argv[1], "super")){
        run_super(argc, argv);
    } else if (0 == strcmp(argv[1], "detector")){
        run_detector(argc, argv); // detector.c中,run_detector函数入口,detect操作,包括训练、测试等。
    } else if (0 == strcmp(argv[1], "detect")){
        float thresh = find_float_arg(argc, argv, "-thresh", .24);
        int ext_output = find_arg(argc, argv, "-ext_output");
        char *filename = (argc > 4) ? argv[4]: 0;
        test_detector("cfg/coco.data", argv[2], argv[3], filename, thresh, 0.5, 0, ext_output, 0, NULL, 0, 0);
......
复制代码

“darknet/src/detector.c”–run_detector()函数:train指令入口。

复制代码
......
    if (0 == strcmp(argv[2], "test")) test_detector(datacfg, cfg, weights, filename, thresh, hier_thresh, dont_show, ext_output, save_labels, outfile, letter_box, benchmark_layers); // 测试test_detector函数入口。
    else if (0 == strcmp(argv[2], "train")) train_detector(datacfg, cfg, weights, gpus, ngpus, clear, dont_show, calc_map, mjpeg_port, show_imgs, benchmark_layers, chart_path); // 训练train_detector函数入口。
    else if (0 == strcmp(argv[2], "valid")) validate_detector(datacfg, cfg, weights, outfile);
......
复制代码

“darknet/src/detector.c”–train_detector()函数:数据加载入口。

pthread_t load_thread = load_data(args); // 首次创建并启动加载线程,args为模型训练参数。

“darknet/src/data.c”–load_data()函数:load_threads()分配线程。

复制代码
pthread_t load_data(load_args args) 
{
    pthread_t thread;
    struct load_args* ptr = (load_args*)xcalloc(1, sizeof(struct load_args));
    *ptr = args;
    /* 调用load_threads()函数。 */
    if(pthread_create(&thread, 0, load_threads, ptr)) error("Thread creation failed"); // 参数1:指向线程标识符的指针;参数2:设置线程属性;参数3:线程运行函数的地址;参数4:运行函数的参数。
    return thread;
}
复制代码

“darknet/src/data.c”–load_threads()函数中:多线程调用run_thread_loop()。

复制代码
if (!threads) {
    threads = (pthread_t*)xcalloc(args.threads, sizeof(pthread_t));
    run_load_data = (volatile int *)xcalloc(args.threads, sizeof(int));
    args_swap = (load_args *)xcalloc(args.threads, sizeof(load_args));
    fprintf(stderr, " Create %d permanent cpu-threads \n", args.threads);
    for (i = 0; i < args.threads; ++i) {
        int* ptr = (int*)xcalloc(1, sizeof(int));
        *ptr = i;
        if (pthread_create(&threads[i], 0, run_thread_loop, ptr)) error("Thread creation failed"); // 根据线程个数,调用run_thread_loop函数。
    }
}
复制代码

“darknet/src/data.c”–run_thread_loop函数:根据线程ID调用load_thread()。

复制代码
void *run_thread_loop(void *ptr)
{
    const int i = *(int *)ptr;
    while (!custom_atomic_load_int(&flag_exit)) {
        while (!custom_atomic_load_int(&run_load_data[i])) {
            if (custom_atomic_load_int(&flag_exit)) {
                free(ptr);
                return 0;
            }
            this_thread_sleep_for(thread_wait_ms);
        }
        pthread_mutex_lock(&mtx_load_data);
        load_args *args_local = (load_args *)xcalloc(1, sizeof(load_args));
        *args_local = args_swap[i]; // 传入线程ID,在load_threads()函数中args_swap[i] = args。
        pthread_mutex_unlock(&mtx_load_data);
        load_thread(args_local); // 调用load_thread()函数。
        custom_atomic_store_int(&run_load_data[i], 0);
    }
    free(ptr);
    return 0;
}
复制代码

“darknet/src/data.c”–load_thread()函数中:根据type标识符执行最底层的数据加载任务load_data_detection()。

else if (a.type == DETECTION_DATA){ // 用于检测的数据,在train_detector()函数中,args.type = DETECTION_DATA。
        *a.d = load_data_detection(a.n, a.paths, a.m, a.w, a.h, a.c, a.num_boxes, a.classes, a.flip, a.gaussian_noise, a.blur, a.mixup, a.jitter, a.resize,
            a.hue, a.saturation, a.exposure, a.mini_batch, a.track, a.augment_speed, a.letter_box, a.show_imgs);

“darknet/src/data.c”–load_data_detection()函数根据是否配置opencv,有两个版本,opencv版本中:

基本数据处理:

包括crop、flip、HSV augmentation、blur以及gaussian_noise。(注意,a.type == DETECTION_DATA时,无angle参数传入,没有图像旋转增强)

复制代码
......
        if (track) random_paths = get_sequential_paths(paths, n, m, mini_batch, augment_speed); // 目标跟踪。
        else random_paths = get_random_paths(paths, n, m); // 随机选取n张图片的路径。
        for (i = 0; i < n; ++i) {
            float *truth = (float*)xcalloc(5 * boxes, sizeof(float));
            const char *filename = random_paths[i];
            int flag = (c >= 3);
            mat_cv *src;
            src = load_image_mat_cv(filename, flag); // image_opencv.cpp中,load_image_mat_cv函数入口,使用opencv读取图像。
......
            /* 将原图进行一定比例的缩放。 */
            if (letter_box) 
            {
                float img_ar = (float)ow / (float)oh; // 读取到的原始图像宽高比。
                float net_ar = (float)w / (float)h; // 规定的,输入到网络要求的图像宽高比。
                float result_ar = img_ar / net_ar; // 两者求比值来判断如何进行letter_box缩放。
                if (result_ar > 1)  // sheight - should be increased
                {
                    float oh_tmp = ow / net_ar;
                    float delta_h = (oh_tmp - oh)/2;
                    ptop = ptop - delta_h;
                    pbot = pbot - delta_h;
                }
                else  // swidth - should be increased
                {
                    float ow_tmp = oh * net_ar;
                    float delta_w = (ow_tmp - ow)/2;
                    pleft = pleft - delta_w;
                    pright = pright - delta_w;
                }
            }
            /* 执行letter_box变换。 */
            int swidth = ow - pleft - pright;
            int sheight = oh - ptop - pbot;
            float sx = (float)swidth / ow;
            float sy = (float)sheight / oh;
            float dx = ((float)pleft / ow) / sx;
            float dy = ((float)ptop / oh) / sy;
            /* truth在调用函数后获得所有图像的标签信息,因为对原始图片进行了数据增强,其中的平移抖动势必会改动每个物体的矩形框标签信息,需要根据具体的数据增强方式进行相应矫正,后面的参数就是用于数据增强后的矩形框信息矫正。 */
            int min_w_h = fill_truth_detection(filename, boxes, truth, classes, flip, dx, dy, 1. / sx, 1. / sy, w, h); // 求最小obj尺寸。
            if ((min_w_h / 8) < blur && blur > 1) blur = min_w_h / 8;   // disable blur if one of the objects is too small
            // image_opencv.cpp中,image_data_augmentation函数入口,数据增强。
            image ai = image_data_augmentation(src, w, h, pleft, ptop, swidth, sheight, flip, dhue, dsat, dexp, gaussian_noise, blur, boxes, truth);
......
复制代码

“darknet/src/image_opencv.cpp”–image_data_augmentation()函数:

复制代码
extern "C" image image_data_augmentation(mat_cv* mat, int w, int h, int pleft, int ptop, int swidth, int sheight, int flip,
    float dhue, float dsat, float dexp, int gaussian_noise, int blur, int num_boxes, float *truth)
{
    image out;
    try {
        cv::Mat img = *(cv::Mat *)mat; // 读取图像数据。
        // crop
        cv::Rect src_rect(pleft, ptop, swidth, sheight);
        cv::Rect img_rect(cv::Point2i(0, 0), img.size());
        cv::Rect new_src_rect = src_rect & img_rect;
        cv::Rect dst_rect(cv::Point2i(std::max<int>(0, -pleft), std::max<int>(0, -ptop)), new_src_rect.size());
        cv::Mat sized;
        if (src_rect.x == 0 && src_rect.y == 0 && src_rect.size() == img.size()) {
            cv::resize(img, sized, cv::Size(w, h), 0, 0, cv::INTER_LINEAR);
        }
        else {
            cv::Mat cropped(src_rect.size(), img.type());
            cropped.setTo(cv::mean(img));
            img(new_src_rect).copyTo(cropped(dst_rect));
            // resize
            cv::resize(cropped, sized, cv::Size(w, h), 0, 0, cv::INTER_LINEAR);
        }
        // flip,虽然配置文件里没有flip参数,但代码里有使用。
        if (flip) {
            cv::Mat cropped;
            cv::flip(sized, cropped, 1); // 0 - x-axis, 1 - y-axis, -1 - both axes (x & y)
            sized = cropped.clone();
        }
        // HSV augmentation
        if (dsat != 1 || dexp != 1 || dhue != 0) {
            if (img.channels() >= 3)
            {
                cv::Mat hsv_src;
                cvtColor(sized, hsv_src, cv::COLOR_RGB2HSV); // RGB to HSV
                std::vector<cv::Mat> hsv;
                cv::split(hsv_src, hsv);
                hsv[1] *= dsat;
                hsv[2] *= dexp;
                hsv[0] += 179 * dhue;
                cv::merge(hsv, hsv_src);
                cvtColor(hsv_src, sized, cv::COLOR_HSV2RGB); // HSV to RGB (the same as previous)
            }
            else
            {
                sized *= dexp;
            }
        }
        if (blur) {
            cv::Mat dst(sized.size(), sized.type());
            if (blur == 1) {
                cv::GaussianBlur(sized, dst, cv::Size(17, 17), 0);
            }
            else {
                int ksize = (blur / 2) * 2 + 1;
                cv::Size kernel_size = cv::Size(ksize, ksize);
                cv::GaussianBlur(sized, dst, kernel_size, 0);
            }
            if (blur == 1) {
                cv::Rect img_rect(0, 0, sized.cols, sized.rows);
                int t;
                for (t = 0; t < num_boxes; ++t) {
                    box b = float_to_box_stride(truth + t*(4 + 1), 1);
                    if (!b.x) break;
                    int left = (b.x - b.w / 2.)*sized.cols;
                    int width = b.w*sized.cols;
                    int top = (b.y - b.h / 2.)*sized.rows;
                    int height = b.h*sized.rows;
                    cv::Rect roi(left, top, width, height);
                    roi = roi & img_rect;
                    sized(roi).copyTo(dst(roi));
                }
            }
            dst.copyTo(sized);
        }
        if (gaussian_noise) {
            cv::Mat noise = cv::Mat(sized.size(), sized.type());
            gaussian_noise = std::min(gaussian_noise, 127);
            gaussian_noise = std::max(gaussian_noise, 0);
            cv::randn(noise, 0, gaussian_noise); //mean and variance
            cv::Mat sized_norm = sized + noise;
            sized = sized_norm;
        }
        // Mat -> image
        out = mat_to_image(sized);
    }
    catch (...) {
        cerr << "OpenCV can't augment image: " << w << " x " << h << " \n";
        out = mat_to_image(*(cv::Mat*)mat);
    }
    return out;
}
复制代码

高级数据处理:

主要是mosaic数据增强。

复制代码
......
            if (use_mixup == 0) { // 不使用mixup。
                d.X.vals[i] = ai.data;
                memcpy(d.y.vals[i], truth, 5 * boxes * sizeof(float)); // C库函数,从存储区truth复制5 * boxes * sizeof(float)个字节到存储区d.y.vals[i]。
            }
            else if (use_mixup == 1) { // 使用mixup。
                if (i_mixup == 0) { // 第一个序列。
                    d.X.vals[i] = ai.data;
                    memcpy(d.y.vals[i], truth, 5 * boxes * sizeof(float)); // n张图的label->d.y.vals,i_mixup=1时,作为上一个sequence的label。
                }
                else if (i_mixup == 1) { // 第二个序列,此时d.X.vals已经储存上个序列n张增强后的图。
                    image old_img = make_empty_image(w, h, c);
                    old_img.data = d.X.vals[i]; // 记录上一个序列的n张old_img。
                    blend_images_cv(ai, 0.5, old_img, 0.5); // image_opencv.cpp中,blend_images_cv函数入口,新旧序列对应的两张图进行线性融合,ai只是在i_mixup和i循环最里层的一张图。
                    blend_truth(d.y.vals[i], boxes, truth); // 上一个序列的d.y.vals[i]与这个序列的truth融合。
                    free_image(old_img); // 释放img数据。
                    d.X.vals[i] = ai.data; // 保存这个序列的n张图。
                }
            }
            else if (use_mixup == 3) { // mosaic数据增强。
                if (i_mixup == 0) { // 第一序列,初始化。
                    image tmp_img = make_image(w, h, c);
                    d.X.vals[i] = tmp_img.data;
                }
                if (flip) { // 翻转。
                    int tmp = pleft;
                    pleft = pright;
                    pright = tmp;
                }
                const int left_shift = min_val_cmp(cut_x[i], max_val_cmp(0, (-pleft*w / ow))); // utils.h中,min_val_cmp函数入口,取小(min)取大(max)。
                const int top_shift = min_val_cmp(cut_y[i], max_val_cmp(0, (-ptop*h / oh))); // ptop<0时,取cut_y[i]与-ptop*h / oh较小的,否则返回0。
                const int right_shift = min_val_cmp((w - cut_x[i]), max_val_cmp(0, (-pright*w / ow)));
                const int bot_shift = min_val_cmp(h - cut_y[i], max_val_cmp(0, (-pbot*h / oh)));
                int k, x, y;
                for (k = 0; k < c; ++k) { // 通道。
                    for (y = 0; y < h; ++y) { // 高度。
                        int j = y*w + k*w*h; // 每张图i,按行堆叠索引j。
                        if (i_mixup == 0 && y < cut_y[i]) { // 右下角区块,i_mixup=0~3,d.X.vals[i]未被清0,累计粘贴4块区域。
                            int j_src = (w - cut_x[i] - right_shift) + (y + h - cut_y[i] - bot_shift)*w + k*w*h;
                            memcpy(&d.X.vals[i][j + 0], &ai.data[j_src], cut_x[i] * sizeof(float)); // 由ai.data[j_src]所指内存区域复制cut_x[i]*sizeof(float)个字节到&d.X.vals[i][j + 0]所指内存区域。
                        }
                        if (i_mixup == 1 && y < cut_y[i]) { // 左下角区块。
                            int j_src = left_shift + (y + h - cut_y[i] - bot_shift)*w + k*w*h;
                            memcpy(&d.X.vals[i][j + cut_x[i]], &ai.data[j_src], (w-cut_x[i]) * sizeof(float));
                        }
                        if (i_mixup == 2 && y >= cut_y[i]) { // 右上角区块。
                            int j_src = (w - cut_x[i] - right_shift) + (top_shift + y - cut_y[i])*w + k*w*h;
                            memcpy(&d.X.vals[i][j + 0], &ai.data[j_src], cut_x[i] * sizeof(float));
                        }
                        if (i_mixup == 3 && y >= cut_y[i]) { // 左上角区块。
                            int j_src = left_shift + (top_shift + y - cut_y[i])*w + k*w*h;
                            memcpy(&d.X.vals[i][j + cut_x[i]], &ai.data[j_src], (w - cut_x[i]) * sizeof(float));
                        }
                    }
                }
                blend_truth_mosaic(d.y.vals[i], boxes, truth, w, h, cut_x[i], cut_y[i], i_mixup, left_shift, right_shift, top_shift, bot_shift); // label对应shift调整。
                free_image(ai);
                ai.data = d.X.vals[i];
            }
......
复制代码

三. BackBone

总图:

网络配置文件(.cfg)决定了模型架构,训练时需要在命令行指定。文件以[net]段开头,定义与训练直接相关的参数:

复制代码
[net]
# Testing # 测试时,batch和subdivisions设置为1,否则可能出错。
#batch=1 # 大一些可以减小训练震荡及训练时NAN的出现。
#subdivisions=1 # 必须为为8的倍数,显存吃紧可以设成32或64。
# Training
batch=64 # 训练过程中将64张图一次性加载进内存,前向传播后将64张图的loss累加求平均,再一次性后向传播更新权重。
subdivisions=16 # 一个batch分16次完成前向传播,即每次计算4张。
width=608 # 网络输入的宽。
height=608 # 网络输入的高。
channels=3 # 网络输入的通道数。
momentum=0.949 # 动量梯度下降优化方法中的动量参数,更新的时候在一定程度上保留之前更新的方向。
decay=0.0005 # 权重衰减正则项,用于防止过拟合。
angle=0 # 数据增强参数,通过旋转角度来生成更多训练样本。
saturation = 1.5 # 数据增强参数,通过调整饱和度来生成更多训练样本。
exposure = 1.5 # 数据增强参数,通过调整曝光量来生成更多训练样本。
hue=.1 # 数据增强参数,通过调整色调来生成更多训练样本。
learning_rate=0.001 # 学习率。
burn_in=1000 # 在迭代次数小于burn_in时,学习率的更新为一种方式,大于burn_in时,采用policy的更新方式。
max_batches = 500500 #训练迭代次数,跑完一个batch为一次,一般为类别数*2000,训练样本少或train from scratch可适当增加。
policy=steps # 学习率调整的策略。
steps=400000,450000 # 动态调整学习率,steps可以取max_batches的0.8~0.9。
scales=.1,.1 # 迭代到steps(1)次时,学习率衰减十倍,steps(2)次时,学习率又会在前一个学习率的基础上衰减十倍。
#cutmix=1 # cutmix数据增强,将一部分区域cut掉但不填充0像素而是随机填充训练集中的其他数据的区域像素值,分类结果按一定的比例分配。
mosaic=1 # 马赛克数据增强,取四张图,随机缩放、随机裁剪、随机排布的方式拼接,详见上述代码分析。
复制代码

其余区段,包括[convolutional]、[route]、[shortcut]、[maxpool]、[upsample]、[yolo]层,为不同类型的层的配置参数。YOLO-V4中[net]层之后堆叠多个CBM及CSP层,首先是2个CBM层,CBM结构如下:

复制代码
[convolutional]
batch_normalize=1 # 是否进行BN。
filters=32 # 卷积核个数,也就是该层的输出通道数。
size=3 # 卷积核大小。
stride=1 # 卷积步长。
pad=1 # pad边缘补像素。
activation=mish # 网络层激活函数,yolo-v4只在Backbone中采用了mish,网络后面仍采用Leaky_relu。
复制代码

创新点是Mish激活函数,与Leaky_Relu曲线对比如图:

Mish在负值的时候并不是完全截断,而是允许比较小的负梯度流入,保证了信息的流动。此外,平滑的激活函数允许更好的信息深入神经网络,梯度下降效果更好,从而提升准确性和泛化能力。

两个CBM后是CSP1,CSP1结构如下:

复制代码
# CSP1 = CBM + 1个残差unit + CBM -> Concat(with CBM),见总图。
[convolutional] # CBM层,直接与7层后的route层连接,形成总图中CSPX下方支路。
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=mish

[route] # 得到前面第2层的输出,即CSP开始位置,构建如图所示的CSP第一支路。
layers = -2

[convolutional] # CBM层。
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=mish

# Residual Block
[convolutional] # CBM层。
batch_normalize=1
filters=32
size=1
stride=1
pad=1
activation=mish

[convolutional] # CBM层。
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=mish

[shortcut] # add前面第3层的输出,Residual Block结束。
from=-3
activation=linear

[convolutional] # CBM层。
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=mish

[route] # Concat上一个CBM层与前面第7层(CBM)的输出。
layers = -1,-7
复制代码

接下来的CBM及CSPX架构与上述block相同,只是CSPX对应X个残差单元,如图:

CSP模块将基础层的特征映射划分为两部分,再skip connection,减少计算量的同时保证了准确率。

要注意的是,backbone中两次出现分支,与后续Neck连接,稍后会解释。

四. Neck&Prediction

.cfg配置文件后半部分是Neck和YOLO-Prediction设置,我做了重点注释:

复制代码
### CBL*3 ###
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky # 不再使用Mish。

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky

[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky

### SPP-最大池化的方式进行多尺度融合 ###
[maxpool] # 5*5。
stride=1
size=5

[route]
layers=-2

[maxpool] # 9*9。
stride=1
size=9

[route]
layers=-4

[maxpool] # 13*13。
stride=1
size=13

[route] # Concat。
layers=-1,-3,-5,-6
### End SPP ###

### CBL*3 ###
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky # 不再使用Mish。

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky

[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky

### CBL ###
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky

### 上采样 ###
[upsample]
stride=2

[route]
layers = 85 # 获取Backbone中CBM+CSP8+CBM模块的输出,85从net以外的层开始计数,从0开始索引。

[convolutional] # 增加CBL支路。
batch_normalize=1 
filters=256
size=1
stride=1
pad=1
activation=leaky

[route] # Concat。
layers = -1, -3

### CBL*5 ###
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky

[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky

[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky

### CBL ###
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky

### 上采样 ###
[upsample]
stride=2

[route]
layers = 54 # 获取Backbone中CBM*2+CSP1+CBM*2+CSP2+CBM*2+CSP8+CBM模块的输出,54从net以外的层开始计数,从0开始索引。

### CBL ###
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky

[route] # Concat。
layers = -1, -3

### CBL*5 ###
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=256
activation=leaky

[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=256
activation=leaky

[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky

### Prediction ###
### CBL ###
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=256
activation=leaky

### conv ###
[convolutional]
size=1
stride=1
pad=1
filters=255
activation=linear
[yolo] # 76*76*255,对应最小的anchor box。
mask = 0,1,2 # 当前属于第几个预选框。
# coco数据集默认值,可通过detector calc_anchors,利用k-means计算样本anchors,但要根据每个anchor的大小(是否超过60*60或30*30)更改mask对应的索引(第一个yolo层对应小尺寸;第二个对应中等大小;第三个对应大尺寸)及上一个conv层的filters。
anchors = 12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401
classes=80 # 网络需要识别的物体种类数。
num=9 # 预选框的个数,即anchors总数。
jitter=.3 # 通过抖动增加噪声来抑制过拟合。
ignore_thresh = .7
truth_thresh = 1
scale_x_y = 1.2
iou_thresh=0.213
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou # CIOU损失函数,考虑目标框回归函数的重叠面积、中心点距离及长宽比。
nms_kind=greedynms
beta_nms=0.6
max_delta=5
[route]
layers = -4 # 获取Neck第一层的输出。

### 构建第二分支 ###
### CBL ###
[convolutional]
batch_normalize=1
size=3
stride=2
pad=1
filters=256
activation=leaky

[route] # Concat。
layers = -1, -16

### CBL*5 ###
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky

[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky

[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky

### CBL ###
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky

### conv ###
[convolutional]
size=1
stride=1
pad=1
filters=255
activation=linear

[yolo] # 38*38*255,对应中等的anchor box。
mask = 3,4,5
anchors = 12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401
classes=80
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
scale_x_y = 1.1
iou_thresh=0.213
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou
nms_kind=greedynms
beta_nms=0.6
max_delta=5

[route] # 获取Neck第二层的输出。
layers = -4

### 构建第三分支 ###
### CBL ###
[convolutional]
batch_normalize=1
size=3
stride=2
pad=1
filters=512
activation=leaky

[route] # Concat。
layers = -1, -37

### CBL*5 ###
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky

[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky

[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky

[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky

### CBL ###
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky

### conv ###
[convolutional]
size=1
stride=1
pad=1
filters=255
activation=linear

[yolo] # 19*19*255,对应最大的anchor box。
mask = 6,7,8
anchors = 12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401
classes=80
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
random=1
scale_x_y = 1.05
iou_thresh=0.213
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou
nms_kind=greedynms
beta_nms=0.6
max_delta=5

复制代码

其中第一个创新点是引入Spatial Pyramid Pooling(SPP)模块:

代码中max pool和route层组合,三个不同尺度的max-pooling将前一个卷积层输出的feature maps进行多尺度的特征处理,再与原图进行拼接,一共4个scale。相比于只用一个max-pooling,提取的特征范围更大,而且将不同尺度的特征进行了有效分离;

第二个创新点是在FPN的基础上引入PAN结构:

原版PANet中PAN操作是做element-wise相加,YOLO-V4则采用扩增维度的Concat,如下图:

Backbone下采样不同阶段得到的特征图Concat后续上采样阶对应尺度的的output,形成FPN结构,再经过两个botton-up的PAN结构。

下采样1:前10个block中,只有3个CBM的stride为2,输入图像尺寸变为608/2*2*2=76,filters根据最后一个CBM为256,因此第10个block输出feature map为76*76*256;

下采样2:继续Backbone,同理,第13个block(CBM)输出38*38*512的特征图;

下采样3:第23个block(CBL)输出为19*19*512;

上采样1:下采样3 + CBL + 上采样 = 38*38*256;

Concat1:[上采样1] Concat [下采样2 + CBL] = [38*38*256] Concat [38*38*512 + (256,1)] = 38*38*512;

上采样2:Concat1 + CBL*5 + CBL + 上采样 = 76*76*128;

Concat2:[上采样2] Concat [下采样1 + CBL] = [76*76*128] Concat [76*76*256 + (128,1)] = 76*76*256;

Concat3(PAN1):[Concat2 + CBL*5 + CBL] Concat [Concat1 + CBL*5] = [76*76*256 + (128,1) + (256,2)] Concat [38*38*512 + (256,1)] = [38*38*256] Concat [38*38*256] = 38*38*512;

Concat4(PAN2):[Concat3 + CBL*5 + CBL] Concat [下采样3] = [38*38*512 + (256,1) + (512,2)] Concat [19*19*512] = 19*19*1024;

Prediction①:Concat2 + CBL*5 + CBL + conv = 76*76*256 + (128,1) + (256,1) + (filters,1) = 76*76*filters,其中filters = (class_num + 5)*3,图中默认COCO数据集,80类所以是255;

Prediction②:PAN1 + CBL*5 + CBL + conv = 38*38*512 + (256,1) + (512,1) + (filters,1) = 38*38*filters,其中filters = (class_num + 5)*3,图中默认COCO数据集,80类所以是255;

Prediction③:PAN2 + CBL*5 + CBL + conv = 19*19*1024 + (512,1) + (1024,1) + (filters,1) = 19*19*filters,其中filters = (class_num + 5)*3,图中默认COCO数据集,80类所以是255。

五. 网络构建

上述从backbone到prediction的网络架构,源码中都是基于network结构体来储存网络参数。具体流程如下:

“darknet/src/detector.c”–train_detector()函数中:

复制代码
......    
    network net_map;
    if (calc_map) { // 计算mAP。
        ......
        net_map = parse_network_cfg_custom(cfgfile, 1, 1); // parser.c中parse_network_cfg_custom函数入口,加载cfg和参数构建网络,batch = 1。
        net_map.benchmark_layers = benchmark_layers;
        const int net_classes = net_map.layers[net_map.n - 1].classes;
        int k;  // free memory unnecessary arrays
        for (k = 0; k < net_map.n - 1; ++k) free_layer_custom(net_map.layers[k], 1);
        ......
    }
    srand(time(0));
    char *base = basecfg(cfgfile); // utils.c中basecfg()函数入口,解析cfg/yolo-obj.cfg文件,就是模型的配置参数,并打印。
    printf("%s\n", base);
    float avg_loss = -1;
    network* nets = (network*)xcalloc(ngpus, sizeof(network)); // 给network结构体分内存,用来储存网络参数。
    srand(time(0));
    int seed = rand();
    int k;
    for (k = 0; k < ngpus; ++k) {
        srand(seed);
#ifdef GPU
        cuda_set_device(gpus[k]);
#endif
        nets[k] = parse_network_cfg(cfgfile); // parse_network_cfg_custom(cfgfile, 0, 0),nets根据GPU个数分别加载配置文件。
        nets[k].benchmark_layers = benchmark_layers;
        if (weightfile) {
            load_weights(&nets[k], weightfile); // parser.c中load_weights()接口,读取权重文件。
        }
        if (clear) { // 是否清零。
            *nets[k].seen = 0;
            *nets[k].cur_iteration = 0;
        }
        nets[k].learning_rate *= ngpus;
    }
    srand(time(0));
    network net = nets[0]; // 参数传递给net
    ......
    /* 准备加载参数。 */
    load_args args = { 0 };
    args.w = net.w;
    args.h = net.h;
    args.c = net.c;
    args.paths = paths;
    args.n = imgs;
    args.m = plist->size;
    args.classes = classes;
    args.flip = net.flip;
    args.jitter = l.jitter;
    args.resize = l.resize;
    args.num_boxes = l.max_boxes;
    net.num_boxes = args.num_boxes;
    net.train_images_num = train_images_num;
    args.d = &buffer;
    args.type = DETECTION_DATA;
    args.threads = 64;    // 16 or 64
......
复制代码

“darknet/src/parser.c”–parse_network_cfg_custom()函数中:

复制代码
network parse_network_cfg_custom(char *filename, int batch, int time_steps)
{
    list *sections = read_cfg(filename); // 读取配置文件,构建成一个链表list。
    node *n = sections->front; // 定义sections的首节点为n。
    if(!n) error("Config file has no sections");
    network net = make_network(sections->size - 1); // network.c中,make_network函数入口,从net变量下一层开始,依次为其中的指针变量分配内存。由于第一个段[net]中存放的是和网络并不直接相关的配置参数,因此网络中层的数目为sections->size - 1。
    net.gpu_index = gpu_index;
    size_params params;
    if (batch > 0) params.train = 0;    // allocates memory for Detection only
    else params.train = 1;              // allocates memory for Detection & Training
    section *s = (section *)n->val; // 首节点n的val传递给section。
    list *options = s->options;
    if(!is_network(s)) error("First section must be [net] or [network]");
    parse_net_options(options, &net); // 初始化网络全局参数,包含但不限于[net]中的参数。
#ifdef GPU
    printf("net.optimized_memory = %d \n", net.optimized_memory);
    if (net.optimized_memory >= 2 && params.train) {
        pre_allocate_pinned_memory((size_t)1024 * 1024 * 1024 * 8);   // pre-allocate 8 GB CPU-RAM for pinned memory
    }
#endif  // GPU
    ......
    while(n){ //初始化每一层的参数。
        params.index = count;
        fprintf(stderr, "%4d ", count);
        s = (section *)n->val;
        options = s->options;
        layer l = { (LAYER_TYPE)0 };
        LAYER_TYPE lt = string_to_layer_type(s->type);
        if(lt == CONVOLUTIONAL){ // 卷积层,调用parse_convolutional()函数执行make_convolutional_layer()创建卷积层。
            l = parse_convolutional(options, params);
        }else if(lt == LOCAL){
            l = parse_local(options, params);
        }else if(lt == ACTIVE){
            l = parse_activation(options, params);
        }else if(lt == RNN){
            l = parse_rnn(options, params);
        }else if(lt == GRU){
            l = parse_gru(options, params);
        }else if(lt == LSTM){
            l = parse_lstm(options, params);
        }else if (lt == CONV_LSTM) {
            l = parse_conv_lstm(options, params);
        }else if(lt == CRNN){
            l = parse_crnn(options, params);
        }else if(lt == CONNECTED){
            l = parse_connected(options, params);
        }else if(lt == CROP){
            l = parse_crop(options, params);
        }else if(lt == COST){
            l = parse_cost(options, params);
            l.keep_delta_gpu = 1;
        }else if(lt == REGION){
            l = parse_region(options, params);
            l.keep_delta_gpu = 1;
        }else if (lt == YOLO) { // yolov3/4引入的yolo_layer,调用parse_yolo()函数执行make_yolo_layer()创建yolo层。
            l = parse_yolo(options, params);
            l.keep_delta_gpu = 1;
        }else if (lt == GAUSSIAN_YOLO) {
            l = parse_gaussian_yolo(options, params);
            l.keep_delta_gpu = 1;
        }else if(lt == DETECTION){
            l = parse_detection(options, params);
        }else if(lt == SOFTMAX){
            l = parse_softmax(options, params);
            net.hierarchy = l.softmax_tree;
            l.keep_delta_gpu = 1;
        }else if(lt == NORMALIZATION){
            l = parse_normalization(options, params);
        }else if(lt == BATCHNORM){
            l = parse_batchnorm(options, params);
        }else if(lt == MAXPOOL){
            l = parse_maxpool(options, params);
        }else if (lt == LOCAL_AVGPOOL) {
            l = parse_local_avgpool(options, params);
        }else if(lt == REORG){
            l = parse_reorg(options, params);        }
        else if (lt == REORG_OLD) {
            l = parse_reorg_old(options, params);
        }else if(lt == AVGPOOL){
            l = parse_avgpool(options, params);
        }else if(lt == ROUTE){
            l = parse_route(options, params);
            int k;
            for (k = 0; k < l.n; ++k) {
                net.layers[l.input_layers[k]].use_bin_output = 0;
                net.layers[l.input_layers[k]].keep_delta_gpu = 1;
            }
        }else if (lt == UPSAMPLE) {
            l = parse_upsample(options, params, net);
        }else if(lt == SHORTCUT){
            l = parse_shortcut(options, params, net);
            net.layers[count - 1].use_bin_output = 0;
            net.layers[l.index].use_bin_output = 0;
            net.layers[l.index].keep_delta_gpu = 1;
        }else if (lt == SCALE_CHANNELS) {
            l = parse_scale_channels(options, params, net);
            net.layers[count - 1].use_bin_output = 0;
            net.layers[l.index].use_bin_output = 0;
            net.layers[l.index].keep_delta_gpu = 1;
        }
        else if (lt == SAM) {
            l = parse_sam(options, params, net);
            net.layers[count - 1].use_bin_output = 0;
            net.layers[l.index].use_bin_output = 0;
            net.layers[l.index].keep_delta_gpu = 1;
        }else if(lt == DROPOUT){
            l = parse_dropout(options, params);
            l.output = net.layers[count-1].output;
            l.delta = net.layers[count-1].delta;
#ifdef GPU
            l.output_gpu = net.layers[count-1].output_gpu;
            l.delta_gpu = net.layers[count-1].delta_gpu;
            l.keep_delta_gpu = 1;
#endif
        }
        else if (lt == EMPTY) {
            layer empty_layer = {(LAYER_TYPE)0};
            empty_layer.out_w = params.w;
            empty_layer.out_h = params.h;
            empty_layer.out_c = params.c;
            l = empty_layer;
            l.output = net.layers[count - 1].output;
            l.delta = net.layers[count - 1].delta;
#ifdef GPU
            l.output_gpu = net.layers[count - 1].output_gpu;
            l.delta_gpu = net.layers[count - 1].delta_gpu;
#endif
        }else{
            fprintf(stderr, "Type not recognized: %s\n", s->type);
        }
        ......
        net.layers[count] = l; // 每个解析函数返回一个填充好的层l,将这些层全部添加到network结构体的layers数组中。
        if (l.workspace_size > workspace_size) workspace_size = l.workspace_size; // workspace_size表示网络的工作空间,指的是所有层中占用运算空间最大的那个层的,因为实际上在GPU或CPU中某个时刻只有一个层在做前向或反向运算。
        if (l.inputs > max_inputs) max_inputs = l.inputs;
        if (l.outputs > max_outputs) max_outputs = l.outputs;
        free_section(s);
        n = n->next; // node节点前沿,empty则while-loop结束。
        ++count;
        if(n){ // 这部分将连接的两个层之间的输入输出shape统一。
            if (l.antialiasing) {
                params.h = l.input_layer->out_h;
                params.w = l.input_layer->out_w;
                params.c = l.input_layer->out_c;
                params.inputs = l.input_layer->outputs;
            }
            else {
                params.h = l.out_h;
                params.w = l.out_w;
                params.c = l.out_c;
                params.inputs = l.outputs;
            }
        }
        if (l.bflops > 0) bflops += l.bflops;

        if (l.w > 1 && l.h > 1) {
            avg_outputs += l.outputs;
            avg_counter++;
        }
    }
    free_list(sections);
    ......
    return net; // 返回解析好的network类型的指针变量,这个指针变量会伴随训练的整个过程。
}
复制代码

以卷积层和yolo层为例,介绍网络层的创建过程,convolutional_layer.c中make_convolutional_layer()函数:

复制代码
convolutional_layer make_convolutional_layer(int batch, int steps, int h, int w, int c, int n, int groups, int size, int stride_x, int stride_y, int dilation, int padding, ACTIVATION activation, int batch_normalize, int binary, int xnor, int adam, int use_bin_output, int index, int antialiasing, convolutional_layer *share_layer, int assisted_excitation, int deform, int train)
{
    int total_batch = batch*steps;
    int i;
    convolutional_layer l = { (LAYER_TYPE)0 }; // convolutional_layer其实就是layer。
    l.type = CONVOLUTIONAL; // layer的类型,此处为卷积层。
    l.train = train;
    /* 改变输入和输出的维度。 */
    if (xnor) groups = 1;   // disable groups for XNOR-net
    if (groups < 1) groups = 1; // group将对应的输入输出通道对应分组,默认为1(输出输入的所有通道各为一组),把卷积group等于输入通道,输出通道等于输入通道就实现了depthwize separable convolution结构。
    const int blur_stride_x = stride_x;
    const int blur_stride_y = stride_y;
    l.antialiasing = antialiasing;
    if (antialiasing) {
        stride_x = stride_y = l.stride = l.stride_x = l.stride_y = 1; // use stride=1 in host-layer
    }
    l.deform = deform;
    l.assisted_excitation = assisted_excitation;
    l.share_layer = share_layer;
    l.index = index;
    l.h = h; // input的高。
    l.w = w; // input的宽。
    l.c = c; // input的通道。
    l.groups = groups;
    l.n = n; // 卷积核filter的个数。
    l.binary = binary;
    l.xnor = xnor;
    l.use_bin_output = use_bin_output;
    l.batch = batch; // 训练使用的batch_size。
    l.steps = steps;
    l.stride = stride_x; // 移动步长。
    l.stride_x = stride_x;
    l.stride_y = stride_y;
    l.dilation = dilation;
    l.size = size; // 卷积核的大小。
    l.pad = padding; // 边界填充宽度。
    l.batch_normalize = batch_normalize; // 是否进行BN操作。
    l.learning_rate_scale = 1;
    /* 数组的大小: c/groups*n*size*size。 */
    l.nweights = (c / groups) * n * size * size; // groups默认值为1,出现c的原因是对多个通道的广播操作。
    if (l.share_layer) {
        if (l.size != l.share_layer->size || l.nweights != l.share_layer->nweights || l.c != l.share_layer->c || l.n != l.share_layer->n) {
            printf(" Layer size, nweights, channels or filters don't match for the share_layer");
            getchar();
        }
        l.weights = l.share_layer->weights;
        l.weight_updates = l.share_layer->weight_updates;
        l.biases = l.share_layer->biases;
        l.bias_updates = l.share_layer->bias_updates;
    }
    else {
        l.weights = (float*)xcalloc(l.nweights, sizeof(float));
        l.biases = (float*)xcalloc(n, sizeof(float));
        if (train) {
            l.weight_updates = (float*)xcalloc(l.nweights, sizeof(float));
            l.bias_updates = (float*)xcalloc(n, sizeof(float));
        }
    }
    // float scale = 1./sqrt(size*size*c);
    float scale = sqrt(2./(size*size*c/groups)); // 初始值scale。
    if (l.activation == NORM_CHAN || l.activation == NORM_CHAN_SOFTMAX || l.activation == NORM_CHAN_SOFTMAX_MAXVAL) {
        for (i = 0; i < l.nweights; ++i) l.weights[i] = 1;   // rand_normal();
    }
    else {
        for (i = 0; i < l.nweights; ++i) l.weights[i] = scale*rand_uniform(-1, 1);   // rand_normal();
    }
    /* 根据公式计算输出维度。 */
    int out_h = convolutional_out_height(l);
    int out_w = convolutional_out_width(l);
    l.out_h = out_h; // output的高。
    l.out_w = out_w; // output的宽。
    l.out_c = n; // output的通道,等于卷积核个数。
    l.outputs = l.out_h * l.out_w * l.out_c; // 一个batch的output维度大小。
    l.inputs = l.w * l.h * l.c; // 一个batch的input维度大小。
    l.activation = activation;
    l.output = (float*)xcalloc(total_batch*l.outputs, sizeof(float)); // 输出数组。
#ifndef GPU
    if (train) l.delta = (float*)xcalloc(total_batch*l.outputs, sizeof(float)); // 暂存更新数据的输出数组。
#endif  // not GPU
    /* 三个重要的函数,前向运算,反向传播和更新函数。 */
    l.forward = forward_convolutional_layer;
    l.backward = backward_convolutional_layer;
    l.update = update_convolutional_layer; // 明确了更新的策略。
    if(binary){
        l.binary_weights = (float*)xcalloc(l.nweights, sizeof(float));
        l.cweights = (char*)xcalloc(l.nweights, sizeof(char));
        l.scales = (float*)xcalloc(n, sizeof(float));
    }
    if(xnor){
        l.binary_weights = (float*)xcalloc(l.nweights, sizeof(float));
        l.binary_input = (float*)xcalloc(l.inputs * l.batch, sizeof(float));
        int align = 32;// 8;
        int src_align = l.out_h*l.out_w;
        l.bit_align = src_align + (align - src_align % align);
        l.mean_arr = (float*)xcalloc(l.n, sizeof(float));
        const size_t new_c = l.c / 32;
        size_t in_re_packed_input_size = new_c * l.w * l.h + 1;
        l.bin_re_packed_input = (uint32_t*)xcalloc(in_re_packed_input_size, sizeof(uint32_t));
        l.lda_align = 256;  // AVX2
        int k = l.size*l.size*l.c;
        size_t k_aligned = k + (l.lda_align - k%l.lda_align);
        size_t t_bit_input_size = k_aligned * l.bit_align / 8;
        l.t_bit_input = (char*)xcalloc(t_bit_input_size, sizeof(char));
    }
    /* Batch Normalization相关的变量设置。 */
    if(batch_normalize){
        if (l.share_layer) {
            l.scales = l.share_layer->scales;
            l.scale_updates = l.share_layer->scale_updates;
            l.mean = l.share_layer->mean;
            l.variance = l.share_layer->variance;
            l.mean_delta = l.share_layer->mean_delta;
            l.variance_delta = l.share_layer->variance_delta;
            l.rolling_mean = l.share_layer->rolling_mean;
            l.rolling_variance = l.share_layer->rolling_variance;
        }
        else {
            l.scales = (float*)xcalloc(n, sizeof(float));
            for (i = 0; i < n; ++i) {
                l.scales[i] = 1;
            }
            if (train) {
                l.scale_updates = (float*)xcalloc(n, sizeof(float));

                l.mean = (float*)xcalloc(n, sizeof(float));
                l.variance = (float*)xcalloc(n, sizeof(float));

                l.mean_delta = (float*)xcalloc(n, sizeof(float));
                l.variance_delta = (float*)xcalloc(n, sizeof(float));
            }
            l.rolling_mean = (float*)xcalloc(n, sizeof(float));
            l.rolling_variance = (float*)xcalloc(n, sizeof(float));
        }
    ......
    return l;
}
复制代码

yolo_layer.c中make_yolo_layer()函数:

复制代码
layer make_yolo_layer(int batch, int w, int h, int n, int total, int *mask, int classes, int max_boxes)
{
    int i;
    layer l = { (LAYER_TYPE)0 };
    l.type = YOLO; // 层类别。
    l.n = n; // 一个cell能预测多少个b-box。
    l.total = total; // anchors数目,9。
    l.batch = batch; // 一个batch包含的图像张数。
    l.h = h; // input的高。
    l.w = w; // imput的宽。
    l.c = n*(classes + 4 + 1);
    l.out_w = l.w; // output的高。
    l.out_h = l.h; // output的宽。
    l.out_c = l.c; // output的通道,等于卷积核个数。
    l.classes = classes; // 目标类别数。
    l.cost = (float*)xcalloc(1, sizeof(float)); // yolo层总的损失。
    l.biases = (float*)xcalloc(total * 2, sizeof(float)); // 储存b-box的anchor box的[w,h]。
    if(mask) l.mask = mask; // 有mask传入。
    else{
        l.mask = (int*)xcalloc(n, sizeof(int));
        for(i = 0; i < n; ++i){
            l.mask[i] = i;
        }
    }
    l.bias_updates = (float*)xcalloc(n * 2, sizeof(float)); // 储存b-box的anchor box的[w,h]的更新值。
    l.outputs = h*w*n*(classes + 4 + 1); // 一张训练图片经过yolo层后得到的输出元素个数(Grid数*每个Grid预测的矩形框数*每个矩形框的参数个数)
    l.inputs = l.outputs; // 一张训练图片输入到yolo层的元素个数(对于yolo_layer,输入和输出的元素个数相等)
    l.max_boxes = max_boxes; // 一张图片最多有max_boxes个ground truth矩形框,这个数量时固定写死的。
    l.truths = l.max_boxes*(4 + 1);    // 4个定位参数+1个物体类别,大于GT实际参数数量。
    l.delta = (float*)xcalloc(batch * l.outputs, sizeof(float)); // yolo层误差项,包含整个batch的。
    l.output = (float*)xcalloc(batch * l.outputs, sizeof(float)); // yolo层所有输出,包含整个batch的。
    /* 存储b-box的Anchor box的[w,h]的初始化,在parse.c中parse_yolo函数会加载cfg中Anchor尺寸。*/
    for(i = 0; i < total*2; ++i){
        l.biases[i] = .5;
    }
    /* 前向运算,反向传播函数。*/
    l.forward = forward_yolo_layer;
    l.backward = backward_yolo_layer;
#ifdef GPU
    l.forward_gpu = forward_yolo_layer_gpu;
    l.backward_gpu = backward_yolo_layer_gpu;
    l.output_gpu = cuda_make_array(l.output, batch*l.outputs);
    l.output_avg_gpu = cuda_make_array(l.output, batch*l.outputs);
    l.delta_gpu = cuda_make_array(l.delta, batch*l.outputs);
    free(l.output);
    if (cudaSuccess == cudaHostAlloc(&l.output, batch*l.outputs*sizeof(float), cudaHostRegisterMapped)) l.output_pinned = 1;
    else {
        cudaGetLastError(); // reset CUDA-error
        l.output = (float*)xcalloc(batch * l.outputs, sizeof(float));
    }
    free(l.delta);
    if (cudaSuccess == cudaHostAlloc(&l.delta, batch*l.outputs*sizeof(float), cudaHostRegisterMapped)) l.delta_pinned = 1;
    else {
        cudaGetLastError(); // reset CUDA-error
        l.delta = (float*)xcalloc(batch * l.outputs, sizeof(float));
    }
#endif
    fprintf(stderr, "yolo\n");
    srand(time(0));
    return l;
}
复制代码

这里要强调下”darknet/src/list.h”中定义的数据结构list:

复制代码
typedef struct node{
    void *val;
    struct node *next;
    struct node *prev;
} node;
typedef struct list{
    int size; // list的所有节点个数。
    node *front; // list的首节点。
    node *back; // list的普通节点。
} list; // list类型变量保存所有的网络参数,有很多的sections节点,每个section中又有一个保存层参数的小list。
复制代码

以及”darknet/src/parser.c”中定义的数据结构section:

typedef struct{
    char *type; // section的类型,保存的是网络中每一层的网络类型和参数。在.cfg配置文件中, 以‘[’开头的行被称为一个section(段)。
    list *options; // section的参数信息。
}section;

“darknet/src/parser.c”–read_cfg()函数的作用就是读取.cfg配置文件并返回给list类型变量sections:

复制代码
/* 读取神经网络结构配置文件.cfg文件中的配置数据,将每个神经网络层参数读取到每个section结构体(每个section是sections的一个节点)中,而后全部插入到list结构体sections中并返回。*/
/* param: filename是C风格字符数组,神经网络结构配置文件路径。*/
/* return: list结构体指针,包含从神经网络结构配置文件中读入的所有神经网络层的参数。*/
list *read_cfg(char *filename)
{
    FILE *file = fopen(filename, "r");
    if(file == 0) file_error(filename);
    /* 一个section表示配置文件中的一个字段,也就是网络结构中的一层,因此,一个section将读取并存储某一层的参数以及该层的type。 */
    char *line;
    int nu = 0; // 当前读取行记号。
    list *sections = make_list(); // sections包含所有的神经网络层参数。
    section *current = 0; // 当前读取到的某一层。
    while((line=fgetl(file)) != 0){
        ++ nu;
        strip(line); // 去除读入行中含有的空格符。
        switch(line[0]){
            /* 以'['开头的行是一个新的section,其内容是层的type,比如[net],[maxpool],[convolutional]... */
            case '[':
                current = (section*)xmalloc(sizeof(section)); // 读到了一个新的section:current。
                list_insert(sections, current); // list.c中,list_insert函数入口,将该新的section保存起来。
                current->options = make_list();
                current->type = line;
                break;
            case '\0': // 空行。
            case '#': // 注释。
            case ';': // 空行。
                free(line); // 对于上述三种情况直接释放内存即可。
                break;
            /* 剩下的才真正是网络结构的数据,调用read_option()函数读取,返回0说明文件中的数据格式有问题,将会提示错误。 */
            default:
                if(!read_option(line, current->options)){ // 将读取到的参数保存在current变量的options中,这里保存在options节点中的数据为kvp键值对类型。
                    fprintf(stderr, "Config file error line %d, could parse: %s\n", nu, line);
                    free(line);
                }
                break;
        }
    }
    fclose(file);
    return sections;
}
复制代码

综上,解析过程将链表中的网络参数保存到network结构体,用于后续权重更新。

六. 权重更新

“darknet/src/detector.c”–train_detector()函数中:

复制代码
        ......
        /* 开始训练网络 */
        float loss = 0;
#ifdef GPU
        if (ngpus == 1) {
            int wait_key = (dont_show) ? 0 : 1;
            loss = train_network_waitkey(net, train, wait_key); // network.c中,train_network_waitkey函数入口,分配内存并执行网络训练。
        }
        else {
            loss = train_networks(nets, ngpus, train, 4); // network_kernels.cu中,train_networks函数入口,多GPU训练。
        }
#else
        loss = train_network(net, train); // train_network_waitkey(net, d, 0),CPU模式。
#endif
        if (avg_loss < 0 || avg_loss != avg_loss) avg_loss = loss;    // if(-inf or nan)
        avg_loss = avg_loss*.9 + loss*.1;
        ......
复制代码

以CPU训练为例,”darknet/src/network.c”–train_network()函数,执行train_network_waitkey(net, d, 0):

复制代码
float train_network_waitkey(network net, data d, int wait_key)
{
    assert(d.X.rows % net.batch == 0);
    int batch = net.batch; // detector.c中train_detector函数在nets[k] = parse_network_cfg(cfgfile)处调用parser.c中的parse_net_options函数,有net->batch /= subdivs,所以batch_size = batch/subdivisions。
    int n = d.X.rows / batch; // batch个数, 对于单GPU和CPU,n = subdivision。
    float* X = (float*)xcalloc(batch * d.X.cols, sizeof(float));
    float* y = (float*)xcalloc(batch * d.y.cols, sizeof(float));
    int i;
    float sum = 0;
    for(i = 0; i < n; ++i){
        get_next_batch(d, batch, i*batch, X, y);
        net.current_subdivision = i;
        float err = train_network_datum(net, X, y); // 调用train_network_datum函数得到误差Loss。
        sum += err;
        if(wait_key) wait_key_cv(5);
    }
    (*net.cur_iteration) += 1;
#ifdef GPU
    update_network_gpu(net);
#else   // GPU
    update_network(net);
#endif  // GPU
    free(X);
    free(y);
    return (float)sum/(n*batch);
}
复制代码

其中,调用train_network_datum()函数计算error是核心:

复制代码
float train_network_datum(network net, float *x, float *y)
{
#ifdef GPU
    if(gpu_index >= 0) return train_network_datum_gpu(net, x, y); // GPU模式,调用network_kernels.cu中train_network_datum_gpu函数。
#endif
    network_state state={0};
    *net.seen += net.batch;
    state.index = 0;
    state.net = net;
    state.input = x;
    state.delta = 0;
    state.truth = y;
    state.train = 1;
    forward_network(net, state); // CPU模式,正向传播。
    backward_network(net, state); // CPU模式,BP。
    float error = get_network_cost(net); // 计算Loss。
    return error;
}
复制代码

进一步分析forward_network()函数:

复制代码
void forward_network(network net, network_state state)
{
    state.workspace = net.workspace;
    int i;
    for(i = 0; i < net.n; ++i){
        state.index = i;
        layer l = net.layers[i];
        if(l.delta && state.train){
            scal_cpu(l.outputs * l.batch, 0, l.delta, 1); // blas.c中,scal_cpu函数入口。
        }
        l.forward(l, state); // 不同层l.forward代表不同函数,如:convolutional_layer.c中,l.forward = forward_convolutional_layer;yolo_layer.c中,l.forward = forward_yolo_layer,CPU执行前向运算。
        state.input = l.output; // 上一层的输出传递给下一层的输入。
    }
}
复制代码

卷积层时,forward_convolutional_layer()函数:

复制代码
void forward_convolutional_layer(convolutional_layer l, network_state state)
{
    /* 获取卷积层输出的长宽。*/
    int out_h = convolutional_out_height(l);
    int out_w = convolutional_out_width(l);
    int i, j;
    fill_cpu(l.outputs*l.batch, 0, l.output, 1); // 把output初始化为0。
    /* xnor-net,将inputs和weights二值化。*/
    if (l.xnor && (!l.align_bit_weights || state.train)) {
        if (!l.align_bit_weights || state.train) {
            binarize_weights(l.weights, l.n, l.nweights, l.binary_weights);
        }
        swap_binary(&l);
        binarize_cpu(state.input, l.c*l.h*l.w*l.batch, l.binary_input);
        state.input = l.binary_input;
    }
    /* m是卷积核的个数,k是每个卷积核的参数数量(l.size是卷积核的大小),n是每个输出feature map的像素个数。*/
    int m = l.n / l.groups;
    int k = l.size*l.size*l.c / l.groups;
    int n = out_h*out_w;
    static int u = 0;
    u++;
    for(i = 0; i < l.batch; ++i)
    {
        for (j = 0; j < l.groups; ++j)
        {
            /* weights是卷积核的参数,a是指向权重的指针,b是指向工作空间指针,c是指向输出的指针。*/
            float *a = l.weights +j*l.nweights / l.groups;
            float *b = state.workspace;
            float *c = l.output +(i*l.groups + j)*n*m;
            if (l.xnor && l.align_bit_weights && !state.train && l.stride_x == l.stride_y)
            {
                memset(b, 0, l.bit_align*l.size*l.size*l.c * sizeof(float));
                if (l.c % 32 == 0)
                {
                    int ldb_align = l.lda_align;
                    size_t new_ldb = k + (ldb_align - k%ldb_align); // (k / 8 + 1) * 8;
                    int re_packed_input_size = l.c * l.w * l.h;
                    memset(state.workspace, 0, re_packed_input_size * sizeof(float));
                    const size_t new_c = l.c / 32;
                    size_t in_re_packed_input_size = new_c * l.w * l.h + 1;
                    memset(l.bin_re_packed_input, 0, in_re_packed_input_size * sizeof(uint32_t));
                    // float32x4 by channel (as in cuDNN)
                    repack_input(state.input, state.workspace, l.w, l.h, l.c);
                    // 32 x floats -> 1 x uint32_t
                    float_to_bit(state.workspace, (unsigned char *)l.bin_re_packed_input, l.c * l.w * l.h);
                    /* image to column,就是将图像依照卷积核的大小拉伸为列向量,方便矩阵运算,将图像每一个kernel转换成一列。*/
                    im2col_cpu_custom((float *)l.bin_re_packed_input, new_c, l.h, l.w, l.size, l.stride, l.pad, state.workspace);
                    int new_k = l.size*l.size*l.c / 32;
                    transpose_uint32((uint32_t *)state.workspace, (uint32_t*)l.t_bit_input, new_k, n, n, new_ldb);
                    /* General Matrix Multiply函数,实现矩阵运算,也就是卷积运算。*/
                    gemm_nn_custom_bin_mean_transposed(m, n, k, 1, (unsigned char*)l.align_bit_weights, new_ldb, (unsigned char*)l.t_bit_input, new_ldb, c, n, l.mean_arr);
                }
                else
                { 
                    im2col_cpu_custom_bin(state.input, l.c, l.h, l.w, l.size, l.stride, l.pad, state.workspace, l.bit_align);
                    // transpose B from NxK to KxN (x-axis (ldb = l.size*l.size*l.c) - should be multiple of 8 bits)
                    {
                        int ldb_align = l.lda_align;
                        size_t new_ldb = k + (ldb_align - k%ldb_align);
                        size_t t_intput_size = binary_transpose_align_input(k, n, state.workspace, &l.t_bit_input, ldb_align, l.bit_align);
                        // 5x times faster than gemm()-float32
                        gemm_nn_custom_bin_mean_transposed(m, n, k, 1, (unsigned char*)l.align_bit_weights, new_ldb, (unsigned char*)l.t_bit_input, new_ldb, c, n, l.mean_arr);
                    }
                }
                add_bias(l.output, l.biases, l.batch, l.n, out_h*out_w); //添加偏移项。
                /* 非线性变化,leaky RELU、Mish等激活函数。*/
                if (l.activation == SWISH) activate_array_swish(l.output, l.outputs*l.batch, l.activation_input, l.output);
                else if (l.activation == MISH) activate_array_mish(l.output, l.outputs*l.batch, l.activation_input, l.output);
                else if (l.activation == NORM_CHAN) activate_array_normalize_channels(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output);
                else if (l.activation == NORM_CHAN_SOFTMAX) activate_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output, 0);
                else if (l.activation == NORM_CHAN_SOFTMAX_MAXVAL) activate_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output, 1);
                else activate_array_cpu_custom(l.output, m*n*l.batch, l.activation);
                return;
            }
            else {
                float *im = state.input + (i*l.groups + j)*(l.c / l.groups)*l.h*l.w;
                if (l.size == 1) {
                    b = im;
                }
                else {
                    im2col_cpu_ext(im,   // input
                        l.c / l.groups,     // input channels
                        l.h, l.w,           // input size (h, w)
                        l.size, l.size,     // kernel size (h, w)
                        l.pad * l.dilation, l.pad * l.dilation,       // padding (h, w)
                        l.stride_y, l.stride_x, // stride (h, w)
                        l.dilation, l.dilation, // dilation (h, w)
                        b);                 // output
                }
                gemm(0, 0, m, n, k, 1, a, k, b, n, 1, c, n);
                // bit-count to float
            }
        }
    }
    if(l.batch_normalize){ // BN层,加速收敛。
        forward_batchnorm_layer(l, state);
    }
    else { // 直接加上bias,output += bias。
        add_bias(l.output, l.biases, l.batch, l.n, out_h*out_w);
    }
    /* 非线性变化,leaky RELU、Mish等激活函数。*/
    if (l.activation == SWISH) activate_array_swish(l.output, l.outputs*l.batch, l.activation_input, l.output);
    else if (l.activation == MISH) activate_array_mish(l.output, l.outputs*l.batch, l.activation_input, l.output);
    else if (l.activation == NORM_CHAN) activate_array_normalize_channels(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output);
    else if (l.activation == NORM_CHAN_SOFTMAX) activate_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output, 0);
    else if (l.activation == NORM_CHAN_SOFTMAX_MAXVAL) activate_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.output, 1);
    else activate_array_cpu_custom(l.output, l.outputs*l.batch, l.activation);
    if(l.binary || l.xnor) swap_binary(&l); // 二值化。
    if(l.assisted_excitation && state.train) assisted_excitation_forward(l, state);
    if (l.antialiasing) {
        network_state s = { 0 };
        s.train = state.train;
        s.workspace = state.workspace;
        s.net = state.net;
        s.input = l.output;
        forward_convolutional_layer(*(l.input_layer), s);
        memcpy(l.output, l.input_layer->output, l.input_layer->outputs * l.input_layer->batch * sizeof(float));
    }
}
复制代码

yolo层时,forward_yolo_layer()函数:

复制代码
void forward_yolo_layer(const layer l, network_state state)
{
    int i, j, b, t, n;
    memcpy(l.output, state.input, l.outputs*l.batch * sizeof(float)); // 将层输入直接copy到层输出。
/* 在cpu模式,把预测输出的x,y,confidence和所有类别都sigmoid激活,确保值在0~1之间。*/
#ifndef GPU
    for (b = 0; b < l.batch; ++b) {
        for (n = 0; n < l.n; ++n) {
            int index = entry_index(l, b, n*l.w*l.h, 0); // 获取第b个batch开始的index。
            /* 对预测的tx,ty进行逻辑回归。*/
            activate_array(l.output + index, 2 * l.w*l.h, LOGISTIC);        // x,y,
            scal_add_cpu(2 * l.w*l.h, l.scale_x_y, -0.5*(l.scale_x_y - 1), l.output + index, 1);    // scale x,y
            index = entry_index(l, b, n*l.w*l.h, 4); // 获取第b个batch confidence开始的index。
            activate_array(l.output + index, (1 + l.classes)*l.w*l.h, LOGISTIC); // 对预测的confidence以及class进行逻辑回归。
        }
    }
#endif
    // delta is zeroed
    memset(l.delta, 0, l.outputs * l.batch * sizeof(float)); // 将yolo层的误差项进行初始化(包含整个batch的)。
    if (!state.train) return; // 不是训练阶段,return。
    float tot_iou = 0; // 总的IOU。
    float tot_giou = 0;
    float tot_diou = 0;
    float tot_ciou = 0;
    float tot_iou_loss = 0;
    float tot_giou_loss = 0;
    float tot_diou_loss = 0;
    float tot_ciou_loss = 0;
    float recall = 0;
    float recall75 = 0;
    float avg_cat = 0;
    float avg_obj = 0;
    float avg_anyobj = 0;
    int count = 0;
    int class_count = 0;
    *(l.cost) = 0; // yolo层的总损失初始化为0。
    for (b = 0; b < l.batch; ++b) { // 遍历batch中的每一张图片。
        for (j = 0; j < l.h; ++j) {
            for (i = 0; i < l.w; ++i) { // 遍历每个Grid cell, 当前cell编号[j, i]。
                for (n = 0; n < l.n; ++n) { // 遍历每一个bbox,当前bbox编号[n]。
                    const int class_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4 + 1); // 预测b-box类别s下标。 const int obj_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4); // 预测b-box objectness下标。
                    const int box_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 0); // 获得第j*w+i个cell第n个b-box的index。
                    const int stride = l.w*l.h;
                    /* 计算第j*w+i个cell第n个b-box在当前特征图上的相对位置[x,y],在网络输入图片上的相对宽度、高度[w,h]。*/
                    box pred = get_yolo_box(l.output, l.biases, l.mask[n], box_index, i, j, l.w, l.h, state.net.w, state.net.h, l.w*l.h);
                    float best_match_iou = 0;
                    int best_match_t = 0;
                    float best_iou = 0; // 保存最大IOU。
                    int best_t = 0; // 保存最大IOU的bbox id。
                    for (t = 0; t < l.max_boxes; ++t) { // 遍历每一个GT bbox。
                        box truth = float_to_box_stride(state.truth + t*(4 + 1) + b*l.truths, 1); // 将第t个bbox由float数组转bbox结构体,方便计算IOU。
                        int class_id = state.truth[t*(4 + 1) + b*l.truths + 4]; // 获取第t个bbox的类别,检查是否有标注错误。
                        if (class_id >= l.classes || class_id < 0) {
                            printf("\n Warning: in txt-labels class_id=%d >= classes=%d in cfg-file. In txt-labels class_id should be [from 0 to %d] \n", class_id, l.classes, l.classes - 1);
                            printf("\n truth.x = %f, truth.y = %f, truth.w = %f, truth.h = %f, class_id = %d \n", truth.x, truth.y, truth.w, truth.h, class_id);
                            if (check_mistakes) getchar();
                            continue; // if label contains class_id more than number of classes in the cfg-file and class_id check garbage value
                        }
                        if (!truth.x) break;  // 如果x坐标为0则break,因为定义了max_boxes个b-box。
                        float objectness = l.output[obj_index]; // 预测bbox object置信度。
                        if (isnan(objectness) || isinf(objectness)) l.output[obj_index] = 0;
                        /* 获得预测b-box的类别信息,如果某个类别的概率超过0.25返回1。*/
                        int class_id_match = compare_yolo_class(l.output, l.classes, class_index, l.w*l.h, objectness, class_id, 0.25f);
                        float iou = box_iou(pred, truth); // 计算pred b-box与第t个GT bbox之间的IOU。
                        if (iou > best_match_iou && class_id_match == 1) { // class_id_match=1的限制,即预测b-box的置信度必须大于0.25。
                            best_match_iou = iou;
                            best_match_t = t;
                        }
                        if (iou > best_iou) {
                            best_iou = iou; // 更新最大的IOU。
                            best_t = t; // 记录该GT b-box的编号t。
                        }
                    }
                    avg_anyobj += l.output[obj_index]; // 统计pred b-box的confidence。
                    l.delta[obj_index] = l.cls_normalizer * (0 - l.output[obj_index]); // 将所有pred b-box都当做noobject, 计算其confidence梯度,cls_normalizer是平衡系数。
                    if (best_match_iou > l.ignore_thresh) { // best_iou大于阈值则说明pred box有物体。
                        const float iou_multiplier = best_match_iou*best_match_iou;// (best_match_iou - l.ignore_thresh) / (1.0 - l.ignore_thresh);
                        if (l.objectness_smooth) {
                            l.delta[obj_index] = l.cls_normalizer * (iou_multiplier - l.output[obj_index]);
                            int class_id = state.truth[best_match_t*(4 + 1) + b*l.truths + 4];
                            if (l.map) class_id = l.map[class_id];
                            const float class_multiplier = (l.classes_multipliers) ? l.classes_multipliers[class_id] : 1.0f;
                            l.delta[class_index + stride*class_id] = class_multiplier * (iou_multiplier - l.output[class_index + stride*class_id]);
                        }
                        else l.delta[obj_index] = 0;
                    }
                    else if (state.net.adversarial) { // 自对抗训练。
                        int stride = l.w*l.h;
                        float scale = pred.w * pred.h;
                        if (scale > 0) scale = sqrt(scale);
                        l.delta[obj_index] = scale * l.cls_normalizer * (0 - l.output[obj_index]);
                        int cl_id;
                        for (cl_id = 0; cl_id < l.classes; ++cl_id) {
                            if(l.output[class_index + stride*cl_id] * l.output[obj_index] > 0.25)
                                l.delta[class_index + stride*cl_id] = scale * (0 - l.output[class_index + stride*cl_id]);
                        }
                    }
                    if (best_iou > l.truth_thresh) { // pred b-box为完全预测正确样本,cfg中truth_thresh=1,语句永远不可能成立。
                        const float iou_multiplier = best_iou*best_iou;// (best_iou - l.truth_thresh) / (1.0 - l.truth_thresh);
                        if (l.objectness_smooth) l.delta[obj_index] = l.cls_normalizer * (iou_multiplier - l.output[obj_index]);
                        else l.delta[obj_index] = l.cls_normalizer * (1 - l.output[obj_index]);
                        int class_id = state.truth[best_t*(4 + 1) + b*l.truths + 4];
                        if (l.map) class_id = l.map[class_id];
                        delta_yolo_class(l.output, l.delta, class_index, class_id, l.classes, l.w*l.h, 0, l.focal_loss, l.label_smooth_eps, l.classes_multipliers);
                        const float class_multiplier = (l.classes_multipliers) ? l.classes_multipliers[class_id] : 1.0f;
                        if (l.objectness_smooth) l.delta[class_index + stride*class_id] = class_multiplier * (iou_multiplier - l.output[class_index + stride*class_id]);
                        box truth = float_to_box_stride(state.truth + best_t*(4 + 1) + b*l.truths, 1);
                        delta_yolo_box(truth, l.output, l.biases, l.mask[n], box_index, i, j, l.w, l.h, state.net.w, state.net.h, l.delta, (2 - truth.w*truth.h), l.w*l.h, l.iou_normalizer * class_multiplier, l.iou_loss, 1, l.max_delta);
                    }
                }
            }
        }
        for (t = 0; t < l.max_boxes; ++t) { // 遍历每一个GT box。
            box truth = float_to_box_stride(state.truth + t*(4 + 1) + b*l.truths, 1); // 将第t个b-box由float数组转b-box结构体,方便计算IOU。
            if (truth.x < 0 || truth.y < 0 || truth.x > 1 || truth.y > 1 || truth.w < 0 || truth.h < 0) {
                char buff[256];
                printf(" Wrong label: truth.x = %f, truth.y = %f, truth.w = %f, truth.h = %f \n", truth.x, truth.y, truth.w, truth.h);
                sprintf(buff, "echo \"Wrong label: truth.x = %f, truth.y = %f, truth.w = %f, truth.h = %f\" >> bad_label.list",
                    truth.x, truth.y, truth.w, truth.h);
                system(buff);
            }
            int class_id = state.truth[t*(4 + 1) + b*l.truths + 4];
            if (class_id >= l.classes || class_id < 0) continue; // if label contains class_id more than number of classes in the cfg-file and class_id check garbage value
            if (!truth.x) break;  // 如果x坐标为0则取消,定义了max_boxes个bbox,可能实际上没那么多。
            float best_iou = 0; // 保存最大的IOU。
            int best_n = 0; // 保存最大IOU的b-box index。
            i = (truth.x * l.w); // 获得当前t个GT b-box所在的cell。
            j = (truth.y * l.h); 
            box truth_shift = truth;
            truth_shift.x = truth_shift.y = 0; // 将truth_shift的box位置移动到0,0。
            for (n = 0; n < l.total; ++n) { // 遍历每一个anchor b-box找到与GT b-box最大的IOU。
                box pred = { 0 };
                pred.w = l.biases[2 * n] / state.net.w; // 计算pred b-box的w在相对整张输入图片的位置。
                pred.h = l.biases[2 * n + 1] / state.net.h; // 计算pred bbox的h在相对整张输入图片的位置。
                float iou = box_iou(pred, truth_shift); // 计算GT box truth_shift与预测b-box pred二者之间的IOU。
                if (iou > best_iou) {
                    best_iou = iou; // 记录最大的IOU。
                    best_n = n; // 记录该b-box的编号n。
                }
            }
            int mask_n = int_index(l.mask, best_n, l.n); // 上面记录b-box的编号,是否由该层Anchor预测的。
            if (mask_n >= 0) {
                int class_id = state.truth[t*(4 + 1) + b*l.truths + 4];
                if (l.map) class_id = l.map[class_id];
                int box_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 0); // 获得best_iou对应anchor box的index。
                const float class_multiplier = (l.classes_multipliers) ? l.classes_multipliers[class_id] : 1.0f; // 控制样本数量不均衡,即Focal Loss中的alpha。
                ious all_ious = delta_yolo_box(truth, l.output, l.biases, best_n, box_index, i, j, l.w, l.h, state.net.w, state.net.h, l.delta, (2 - truth.w*truth.h), l.w*l.h, l.iou_normalizer * class_multiplier, l.iou_loss, 1, l.max_delta); // 计算best_iou对应Anchor bbox的[x,y,w,h]的梯度。
                /* 模板检测最新的工作,metricl learning,包括IOU/GIOU/DIOU/CIOU Loss等。*/
                // range is 0 <= 1
                tot_iou += all_ious.iou;
                tot_iou_loss += 1 - all_ious.iou;
                // range is -1 <= giou <= 1
                tot_giou += all_ious.giou;
                tot_giou_loss += 1 - all_ious.giou;
                tot_diou += all_ious.diou;
                tot_diou_loss += 1 - all_ious.diou;
                tot_ciou += all_ious.ciou;
                tot_ciou_loss += 1 - all_ious.ciou;
                int obj_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4); // 获得best_iou对应anchor box的confidence的index。
                avg_obj += l.output[obj_index]; // 统计confidence。
                l.delta[obj_index] = class_multiplier * l.cls_normalizer * (1 - l.output[obj_index]); // 计算confidence的梯度。
                int class_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4 + 1); // 获得best_iou对应GT box的class的index。
                delta_yolo_class(l.output, l.delta, class_index, class_id, l.classes, l.w*l.h, &avg_cat, l.focal_loss, l.label_smooth_eps, l.classes_multipliers); // 获得best_iou对应anchor box的class的index。
                ++count;
                ++class_count;
                if (all_ious.iou > .5) recall += 1;
                if (all_ious.iou > .75) recall75 += 1;
            }
            // iou_thresh
            for (n = 0; n < l.total; ++n) {
                int mask_n = int_index(l.mask, n, l.n);
                if (mask_n >= 0 && n != best_n && l.iou_thresh < 1.0f) {
                    box pred = { 0 };
                    pred.w = l.biases[2 * n] / state.net.w;
                    pred.h = l.biases[2 * n + 1] / state.net.h;
                    float iou = box_iou_kind(pred, truth_shift, l.iou_thresh_kind); // IOU, GIOU, MSE, DIOU, CIOU
                    // iou, n
                    if (iou > l.iou_thresh) {
                        int class_id = state.truth[t*(4 + 1) + b*l.truths + 4];
                        if (l.map) class_id = l.map[class_id];
                        int box_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 0);
                        const float class_multiplier = (l.classes_multipliers) ? l.classes_multipliers[class_id] : 1.0f;
                        ious all_ious = delta_yolo_box(truth, l.output, l.biases, n, box_index, i, j, l.w, l.h, state.net.w, state.net.h, l.delta, (2 - truth.w*truth.h), l.w*l.h, l.iou_normalizer * class_multiplier, l.iou_loss, 1, l.max_delta);
                        // range is 0 <= 1
                        tot_iou += all_ious.iou;
                        tot_iou_loss += 1 - all_ious.iou;
                        // range is -1 <= giou <= 1
                        tot_giou += all_ious.giou;
                        tot_giou_loss += 1 - all_ious.giou;
                        tot_diou += all_ious.diou;
                        tot_diou_loss += 1 - all_ious.diou;
                        tot_ciou += all_ious.ciou;
                        tot_ciou_loss += 1 - all_ious.ciou;
                        int obj_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4);
                        avg_obj += l.output[obj_index];
                        l.delta[obj_index] = class_multiplier * l.cls_normalizer * (1 - l.output[obj_index]);
                        int class_index = entry_index(l, b, mask_n*l.w*l.h + j*l.w + i, 4 + 1);
                        delta_yolo_class(l.output, l.delta, class_index, class_id, l.classes, l.w*l.h, &avg_cat, l.focal_loss, l.label_smooth_eps, l.classes_multipliers);
                        ++count;
                        ++class_count;
                        if (all_ious.iou > .5) recall += 1;
                        if (all_ious.iou > .75) recall75 += 1;
                    }
                }
            }
        }
        // averages the deltas obtained by the function: delta_yolo_box()_accumulate
        for (j = 0; j < l.h; ++j) {
            for (i = 0; i < l.w; ++i) {
                for (n = 0; n < l.n; ++n) {
                    int box_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 0); // 获得第j*w+i个cell第n个b-box的index。
                    int class_index = entry_index(l, b, n*l.w*l.h + j*l.w + i, 4 + 1); // 获得第j*w+i个cell第n个b-box的类别。
                    const int stride = l.w*l.h; // 特征图的大小。
                    averages_yolo_deltas(class_index, box_index, stride, l.classes, l.delta); // 对梯度进行平均。
                }
            }
        }
    }
    ......

// gIOU loss + MSE (objectness) loss
if (l.iou_loss == MSE) {
*(l.cost) = pow(mag_array(l.delta, l.outputs * l.batch), 2);
}
else {
// Always compute classification loss both for iou + cls loss and for logging with mse loss
// TODO: remove IOU loss fields before computing MSE on class
// probably split into two arrays
if (l.iou_loss == GIOU) {
avg_iou_loss = count > 0 ? l.iou_normalizer * (tot_giou_loss / count) : 0; // 平均IOU损失,参考上面代码,tot_iou_loss += 1 – all_ious.iou。
}
else {
avg_iou_loss = count > 0 ? l.iou_normalizer * (tot_iou_loss / count) : 0; // 平均IOU损失,参考上面代码,tot_iou_loss += 1 – all_ious.iou。
}
*(l.cost) = avg_iou_loss + classification_loss; // Loss值传递给l.cost,IOU与分类损失求和。
}

loss /= l.batch; // 平均Loss。
classification_loss /= l.batch;
iou_loss /= l.batch;

……

}
复制代码

再来分析backward_network()函数:

复制代码

void backward_network(network net, network_state state)
{
int i;
float *original_input = state.input;
float *original_delta = state.delta;
state.workspace = net.workspace;
for(i = net.n-1; i >= 0; –i){
state.index = i;
if(i == 0){
state.input = original_input;
state.delta = original_delta;
}else{
layer prev = net.layers[i-1];
state.input = prev.output;
state.delta = prev.delta; // delta是指针变量,对state.delta做修改,就相当与对prev层的delta做了修改。
}
layer l = net.layers[i];
if (l.stopbackward) break;
if (l.onlyforward) continue;
l.backward(l, state); // 不同层l.backward代表不同函数,如:convolutional_layer.c中,l.backward = backward_convolutional_layer;yolo_layer.c中,l.backward = backward_yolo_layer,CPU执行反向传播。
}
}

复制代码

卷积层时,backward_convolutional_layer()函数:

复制代码
void backward_convolutional_layer(convolutional_layer l, network_state state)
{
    int i, j;
    /* m是卷积核的个数,k是每个卷积核的参数数量(l.size是卷积核的大小),n是每个输出feature map的像素个数。*/
    int m = l.n / l.groups;
    int n = l.size*l.size*l.c / l.groups;
    int k = l.out_w*l.out_h;
    /* 更新delta。*/
    if (l.activation == SWISH) gradient_array_swish(l.output, l.outputs*l.batch, l.activation_input, l.delta);
    else if (l.activation == MISH) gradient_array_mish(l.outputs*l.batch, l.activation_input, l.delta);
    else if (l.activation == NORM_CHAN_SOFTMAX || l.activation == NORM_CHAN_SOFTMAX_MAXVAL) gradient_array_normalize_channels_softmax(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.delta);
    else if (l.activation == NORM_CHAN) gradient_array_normalize_channels(l.output, l.outputs*l.batch, l.batch, l.out_c, l.out_w*l.out_h, l.delta);
    else gradient_array(l.output, l.outputs*l.batch, l.activation, l.delta);
    if (l.batch_normalize) { // BN层,加速收敛。
        backward_batchnorm_layer(l, state);
    }
    else { // 直接加上bias。
        backward_bias(l.bias_updates, l.delta, l.batch, l.n, k);
    }
    for (i = 0; i < l.batch; ++i) {
        for (j = 0; j < l.groups; ++j) {
            float *a = l.delta + (i*l.groups + j)*m*k;
            float *b = state.workspace;
            float *c = l.weight_updates + j*l.nweights / l.groups;
            /* 进入本函数之前,在backward_network()函数中,已经将net.input赋值为prev.output,若当前层为第l层,则net.input为第l-1层的output。*/
            float *im = state.input + (i*l.groups + j)* (l.c / l.groups)*l.h*l.w;
            im2col_cpu_ext(
                im,                 // input
                l.c / l.groups,     // input channels
                l.h, l.w,           // input size (h, w)
                l.size, l.size,     // kernel size (h, w)
                l.pad * l.dilation, l.pad * l.dilation,       // padding (h, w)
                l.stride_y, l.stride_x, // stride (h, w)
                l.dilation, l.dilation, // dilation (h, w)
                b);                 // output
            gemm(0, 1, m, n, k, 1, a, k, b, k, 1, c, n); // 计算当前层weights更新。
            /* 计算上一层的delta,进入本函数之前,在backward_network()函数中,已经将net.delta赋值为prev.delta,若当前层为第l层,则net.delta为第l-1层的delta。*/
            if (state.delta) {
                a = l.weights + j*l.nweights / l.groups;
                b = l.delta + (i*l.groups + j)*m*k;
                c = state.workspace;
                gemm(1, 0, n, k, m, 1, a, n, b, k, 0, c, k);
                col2im_cpu_ext(
                    state.workspace,        // input
                    l.c / l.groups,         // input channels (h, w)
                    l.h, l.w,               // input size (h, w)
                    l.size, l.size,         // kernel size (h, w)
                    l.pad * l.dilation, l.pad * l.dilation,           // padding (h, w)
                    l.stride_y, l.stride_x,     // stride (h, w)
                    l.dilation, l.dilation, // dilation (h, w)
                    state.delta + (i*l.groups + j)* (l.c / l.groups)*l.h*l.w); // output (delta)
            }
        }
    }
}
复制代码

yolo层时,backward_yolo_layer()函数:

void backward_yolo_layer(const layer l, network_state state)
{
   axpy_cpu(l.batch*l.inputs, 1, l.delta, 1, state.delta, 1); // 直接把l.delta拷贝给上一层的delta。注意 net.delta 指向 prev_layer.delta。
}

正向、反向传播后,通过get_network_cost()函数计算Loss:

复制代码
float get_network_cost(network net)
{
    int i;
    float sum = 0;
    int count = 0;
    for(i = 0; i < net.n; ++i){
        if(net.layers[i].cost){ // 获取各层的损失,只有detection层,也就是yolo层,有cost。
            sum += net.layers[i].cost[0]; // Loss总和存在cost[0]中,见cost_layer.c中forward_cost_layer()函数。
            ++count;
        }
    }
    return sum/count; // 返回平均损失。
}
复制代码

这里用一张图解释下Loss公式:

 

CIOU_Loss是创新点,与GIOU_Loss相比,引入了重叠面积与中心点的距离Dis_2来区分预测框a与b的定位差异,同时还引入了预测框和目标框的长宽比一致性因子ν,将a与c这种重叠面积与中心点距离相同但长宽比与目标框适配程度有差异的预测框区分开来,如图:

计算好Loss需要update_network():

复制代码
void update_network(network net)
{
    int i;
    int update_batch = net.batch*net.subdivisions;
    float rate = get_current_rate(net);
    for(i = 0; i < net.n; ++i){
        layer l = net.layers[i];
        if(l.update){
            l.update(l, update_batch, rate, net.momentum, net.decay); // convolutional_layer.c中,l.update = update_convolutional_layer。
        }
    }
}
复制代码

update_convolutional_layer()函数:

复制代码
void update_convolutional_layer(convolutional_layer l, int batch, float learning_rate_init, float momentum, float decay)
{
    float learning_rate = learning_rate_init*l.learning_rate_scale;
    axpy_cpu(l.nweights, -decay*batch, l.weights, 1, l.weight_updates, 1); // blas.c中,axpy_cpu函数入口,for(i = 0; i < l.nweights; ++i),l.weight_updates[i*1] -= decay*batch*l.weights[i*1]。
    axpy_cpu(l.nweights, learning_rate / batch, l.weight_updates, 1, l.weights, 1); // for(i = 0; i < l.nweights; ++i),l.weights[i*1] += (learning_rate/batch)*l.weight_updates[i*1]
    scal_cpu(l.nweights, momentum, l.weight_updates, 1); // blas.c中,scal_cpu函数入口,for(i = 0; i < l.nweights; ++i),l.weight_updates[i*1] *= momentum。
    axpy_cpu(l.n, learning_rate / batch, l.bias_updates, 1, l.biases, 1); // for(i = 0; i < l.n; ++i),l.biases[i*1] += (learning_rate/batch)*l.bias_updates[i*1]。
    scal_cpu(l.n, momentum, l.bias_updates, 1); // for(i = 0; i < l.n; ++i),l.bias_updates[i*1] *= momentum。
    if (l.scales) {
        axpy_cpu(l.n, learning_rate / batch, l.scale_updates, 1, l.scales, 1);
        scal_cpu(l.n, momentum, l.scale_updates, 1);
    }
}
复制代码

同样,在network_kernels.cu里,有GPU模式下的forward&backward相关的函数,涉及数据格式转换及加速,此处只讨论原理,暂时忽略GPU部分的代码。

复制代码
void forward_backward_network_gpu(network net, float *x, float *y)
{
......
    forward_network_gpu(net, state); // 正向。
    backward_network_gpu(net, state); // 反向。
......
}
复制代码

CPU模式下,采用带momentum的常规GD更新weights,同时在network.c中也提供了也提供了train_network_sgd()函数接口;GPU模式提供了adam选项,convolutional_layer.c中make_convolutional_layer()函数有体现。

七. 调参总结

本人在实际项目中涉及的是工业中的钢铁表面缺陷检测场景,不到2000张图片,3类,数据量很少。理论上YOLO系列并不太适合缺陷检测的问题,基于分割+分类的网络、Cascade-RCNN等或许是更好的选择,但我本着实验的态度,进行了多轮的训练和对比,整体上效果还是不错的。

1.max_batches: AlexeyAB在github工程上有提到,类别数*2000作为参考,不要少于6000,但这个是使用预训练权重的情况。如果train from scratch,要适当增加,具体要看你的数据情况,网络需要额外的时间来从零开始学习;

2.pretrain or not:当数据量很少时,预训练确实能更快使模型收敛,效果也不错,但缺陷检测这类问题,缺陷目标特征本身的特异性还是比较强的,虽然我的数据量也很少,但scratch的方式还是能取得稍好一些的效果;

3.anchors:cfg文件默认的anchors是基于COCO数据集,可以说尺度比较均衡,使用它效果不会差,但如果你自己的数据在尺度分布上不太均衡,建议自行生成新的anchors,可以直接使用源码里面的脚本,注意,要根据生成anchors的size(1-yolo:<30*30,2-yolo:<60*60,3-yolo:others)来改变索引值masks以及前一个conv层的filters参数;

4.rotate:YOLO-V4在目标检测这一块,其实没有用到旋转来进行数据增强,因此我在线下对数量最少的一个类进行了180旋转对称增强,该类样本数扩增一倍,效果目前还不明显,可能是数据量增加的还是太少,而且我还在训练对比,完成后可以补充;

5.mosaic:马赛克数据增强是必须要有的,mAP值提升比较明显,需要安装opencv,且和cutmix不能同时使用。