[
  {
    "path": ".gitignore",
    "content": "# Node rules:\n## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n## Dependency directory\n## Commenting this out is preferred by some people, see\n## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git\nnode_modules\n\n# Book build output\n_book\n\n# eBook build output\n*.epub\n*.mobi\n*.pdf\n\n.history/\n.DS_Store\n.book.json\n.package-lock.json\n.SUMMARY.md"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-学会使用命令帮助.md",
    "content": "- [概述](#概述)\n- [使用 whatis](#使用-whatis)\n- [使用 man](#使用-man)\n- [查看命令程序路径 which](#查看命令程序路径-which)\n- [总结](#总结)\n- [参考资料](#参考资料)\n\n### 概述\n\n`Linux` 命令及其参数繁多，大多数人都是无法记住全部功能和具体参数意思的。在 `linux` 终端，面对命令不知道怎么用，或不记得命令的拼写及参数时，我们需要求助于系统的帮助文档； `linux` 系统内置的帮助文档很详细，通常能解决我们的问题，我们需要掌握如何正确的去使用它们。\n\n* 需要知道某个命令的**简要说明**，可以使用 `whatis`；而更详细的介绍，则可用 `info` 命令；\n* 在只记得**部分命令关键字**的场合，我们可通过 `man -k` 来搜索；\n* 查看命令在哪个**位置**，我们需要使用 `which`；\n* 而对于命令的**具体参数**及使用方法，我们需要用到强大的 `man` ；\n\n### 使用 whatis\n\n使用方法如下：\n\n```bash\n$ whatis ls # 查看 ls 命令的简要说明\nls (1)               - list directory contents\n$ info ls  # 查看 ls 命令的详细说明，会进入一个窗口内，按 q 退出\nFile: coreutils.info,  Node: ls invocation,  Next: dir invocation,  Up: Directory listing\n10.1 'ls': List directory contents\nThe 'ls' program lists information about files (of any type, including\ndirectories).  Options and file arguments can be intermixed arbitrarily,\nas usual.\n... 省略\n```\n\n### 使用 man\n\n查看命令 `cp` 的说明文档。\n\n```bash\n$ man cp  # 查看 cp 命令的说明文档，主要是命令的使用方法及具体参数意思\nCP(1)      User Commands      CP(1)\n\nNAME\n       cp - copy files and directories\n... 省略\n```\n\n在 `man` 的帮助手册中，将帮助文档分为了 `9` 个类别，对于有的关键字可能存在多个类别中， 我们就需要指定特定的类别来查看；（一般我们查询的 bash 命令，归类在1类中）；如我们常用的 `printf` 命令在分类 `1` 和分类 `3` 中都有(CentOS 系统例外)；分类 `1` 中的页面是命令操作及可执行文件的帮助；而3是常用函数库说明；如果我们想看的是 `C` 语言中 `printf` 的用法，可以指定查看分类 `3` 的帮助：\n\n```bash\n$man 3 printf\n```\n\n`man` 页面所属的分类标识(常用的是分类 `1` 和分类 `3` )\n\n```bash\n(1)、用户可以操作的命令或者是可执行文件\n(2)、系统核心可调用的函数与工具等\n(3)、一些常用的函数与数据库\n(4)、设备文件的说明\n(5)、设置文件或者某些文件的格式\n(6)、游戏\n(7)、惯例与协议等。例如Linux标准文件系统、网络协议、ASCⅡ，码等说明内容\n(8)、系统管理员可用的管理条令\n(9)、与内核有关的文件\n```\n\n### 查看命令程序路径 which\n\n查看程序的 `binary` 文件所在路径，可用 `which` 命令。\n\n```bash\n$ which ls  # 查看 ping 程序(命令)的 binary 文件所在路径\n/bin/ls\n$ cd /bin;ls \n```\n\n![image](./images/feaa2c13-1cdf-46e1-948c-535f4c51a9bd.png)\n\n查看程序的搜索路径：\n\n```bash\n$ whereis ls\nls: /bin/ls /usr/share/man/man1/ls.1.gz\n```\n当系统中安装了同一软件的多个版本时，不确定使用的是哪个版本时，这个命令就能派上用场。\n\n### 总结\n\n本文总共讲解了 `whatis info man which whereis` 五个帮助命令的使用，`Linux` 命令的熟练使用需要我们在项目中多加实践、思考和总结。\n\n### 参考资料\n\n[《Linux基础》](https://linuxtools-rst.readthedocs.io/zh_CN/latest/base/index.html)"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-常用命令总结.md",
    "content": "- [1，scp 命令复制远程文件](#1scp-命令复制远程文件)\n- [2，ubuntu 系统使用使用 dpkg 命令安装和卸载 .deb 包](#2ubuntu-系统使用使用-dpkg-命令安装和卸载-deb-包)\n- [3，vim 查找字符串](#3vim-查找字符串)\n- [4，df 和 du 命令使用](#4df-和-du-命令使用)\n- [5，ls -lh 查看指定文件大小](#5ls--lh-查看指定文件大小)\n- [6，ctrl + r，反向查找历史命令](#6ctrl--r反向查找历史命令)\n- [7，find 查找文件和文件夹](#7find-查找文件和文件夹)\n- [8，hdfs 命令详解](#8hdfs-命令详解)\n- [9，top 命令进行程序性能分析](#9top-命令进行程序性能分析)\n- [10，tar 压缩、解压命令](#10tar-压缩解压命令)\n- [11，linux 系统特殊符号作用](#11linux-系统特殊符号作用)\n- [12， linxu 中 shell 变量含义](#12-linxu-中-shell-变量含义)\n- [13，vim 跳转到行尾和行首命令](#13vim-跳转到行尾和行首命令)\n- [14，vscode 列选择快捷键](#14vscode-列选择快捷键)\n- [15，查看 cpu 信息](#15查看-cpu-信息)\n- [16，mkdir -p 创建多层目录](#16mkdir--p-创建多层目录)\n- [17，查看 gcc 所有安装的版本](#17查看-gcc-所有安装的版本)\n- [18，查看系统版本命令](#18查看系统版本命令)\n- [19，shell 读取文件每一行内容并输出](#19shell-读取文件每一行内容并输出)\n- [20，实时查看 GPU 显存信息](#20实时查看-gpu-显存信息)\n- [21，update-alternatives 管理软件版本](#21update-alternatives-管理软件版本)\n- [22，管道和重定向命令](#22管道和重定向命令)\n- [23，Bash快捷输入或删除命令](#23bash快捷输入或删除命令)\n- [24，srun 命令行参数用法](#24srun-命令行参数用法)\n- [参考资料](#参考资料)\n\n\n### 1，scp 命令复制远程文件\n\n```bash\n# 从本地复制到远程：\nscp local_file remote_username@remote_ip:remote_file \n# 从远程复制到本地\nscp root@www.runoob.com:/home/root/others/music /home/space/music/1.mp3 \n```\n\n### 2，ubuntu 系统使用使用 dpkg 命令安装和卸载 .deb 包\n\n```bash\nsudo dpkg -i package_name.deb  # 安装deb包\nsudo dpkg -r package_name # 卸载 deb 包\nsudo apt install path_to_deb_file # 安装deb包\nsudo apt remove program_name # 卸载deb 包\n```\n\n### 3，vim 查找字符串\n\n在 `normal` 模式下按下 `/` 进入查找模式，输入要查找的字符串并按下回车。`Vim` 会跳转到第一个匹配，按下 `n` 查找下一个，按下 `N` 查找上一个，`vim` 还支持正则表达式查找。\n\n### 4，df 和 du 命令使用\n\n* `df` ：查看**磁盘空间**占用情况\n* `du` ：查看**目录**占用的空间大小。\n\n```bash\ndf -hT #查看硬盘使用情况。\ndu -h --max-depth=1 floder_name # 查看当前目录下所有文件/文件夹的空间大小\ndu -h -d 0 . # 查看当前目录空间大小\ndu -sh foldername # 查看指定目录空间大小\n```\n![image](images/c62f2296-e82d-4d3c-bb6f-07c1d581fe6b.png)\n\n### 5，ls -lh 查看指定文件大小\n\n```bash\n$ ls -lh .bashrc  # 能查看文件空间大小,不能查看目录大小\n$ find . -type l -delete # 删除当前目录下的符号链接\n```\n\n### 6，ctrl + r，反向查找历史命令\n\n终端中按下 `ctrl + r`，可弹出搜索历史命令行，输入你要查找你输入过命令的关键信息，即可弹出完整历史命令。\n\n### 7，find 查找文件和文件夹\n\n`find` 支持基于正则表达式查找指定名字的文件，也支持根据文件类型、基于目录深度和文件时间戳进行搜索。\n\n1. 查找目录：find /（查找范围） -name '查找关键字' -type d\n2. 查找文件：find /（查找范围） -name 查找关键字 -print\n\n![image](images/285954fe-e25e-4957-baee-29c1cacff6d9.png)\n\n### 8，hdfs 命令详解\n\n[HDFS 常用命令](http://blog.sanyuehua.net/2017/11/01/Hadoop-HDFS/)\n\n### 9，top 命令进行程序性能分析\n\n`top` 命令是 `Linux` 下常用的性能分析工具，能够实时显示系统中各个进程的资源占用状况，类似于 `Windows` 的任务管理器。\n\n![image](images/78316e49-97f8-41cf-a9d9-b8f7e2ce024d.png)\n\n`load average` 后面分别是 1分钟、5分钟、15分钟的负载情况。数据是每隔 5 秒钟检查一次活跃的进程数，然后根据这个数值算出来的。如果这个数除以 CPU  的数目，**结果高于 5 的时候就表明系统在超负荷运转了**。\n\n### 10，tar 压缩、解压命令\n\n* `tar -zxvf` 解压 `.tar.gz` 和 `.tgz`。\n* `tar -xvf file.tar`  解压 tar 包\n* `tar –cvf jpg.tar ./*.jpg`：将当前目录下所有 `jpg` 文件仅打包成 `jpg.tar` 后。\n* `tar –zcvf xxx.tar.gz ./*.jpg`：打包后以 `gzip` 压缩，命名为 `xxx.tar.gz`。\n\n在参数 `f` 之后的文件档名是自己取的，我们习惯上都用 `.tar` 来作为辨识。 如果加 `z` 参数，则以  `.tar.gz` 或  `.tgz` 来代表 `gzip` 压缩过的 `tar` 包； 如果加 `j` 参数，则以  `.tar.bz2` 来作为 `tar` 包名。\n\n### 11，linux 系统特殊符号作用\n\n1. `$`: 作为变量的前导符，引用一个变量的内容，比如：`echo $PATH`；在正则表达式中被定义为行末（`End of line`）。\n2. `>>` : 表示将符号左侧的内容，以追加的方式输入到右侧文件的末尾行中。\n3. `|`\\*\\* : 管道命令。\\*\\*管道命令 \"|\" 仅能处理前面一个命令传来的正确信息。\n\n### 12， linxu 中 shell 变量含义\n\n* `$1～$n`：添加到 `Shell` 的各参数值。`$1` 是第 `1` 参数、`$2` 是第 `2` 参数…。\n* `$$`：`shell` 脚本本身的 `PID`。\n* `$!`：`shell` 脚本最后运行的后台 `process` 的 `PID`。\n* `$?`：最后运行的命令结束代码（返回值）。\n* `$*`：所有参数列表。如 `\"$*\"` 用`「\"」`括起来的情况、以 `\"$1 $2 … $n\"` 的形式输出所有参数。\n* `$#`：添加到 `shell` 的参数个数。\n* `$0`：`shell` 本身的文件名。\n\n### 13，vim 跳转到行尾和行首命令\n\n![image](images/20c9770d-41d5-4880-9b43-335934bebccd.png)\n\n1. 跳到文本的最后一行行首：按`“G”`,即`“shift+g”`；\n2. 跳到最后一行的最后一个字符 ： 先重复 1 的操作即按“G”，之后按“\\$”键，即`“shift+4”`；\n3. 跳到第一行的第一个字符：先按两次`“g”`；\n4. `^` 跳转行首，`$` 跳转行尾；\n\n### 14，vscode 列选择快捷键\n\n* VSCode 列选择快捷键：Alt+Shift+左键\n\n### 15，查看 cpu 信息\n> 总核数 = 物理 cpu 个数 \\* 每颗物理 cpu 的核数\n总逻辑 cpu 数 = 物理 cpu 个数 \\* 每颗物理 cpu 的核数 \\* 超线程数\n\n1. 查看物理 cpu 个数：`cat /proc/cpuinfo | grep \"physical id\"| sort| uniq| wc -l`\n2. 查看每个物理 cpu 中的 core 个数（核数）：`cat /proc/cpuinfo| grep \"cpu cores\"| uniq`\n3. 查看逻辑 cpu 的个数： `cat /proc/cpuinfo| grep \"processor\"| wc -l` \n\n```bash\n# lscpu | grep -E '^Thread|^Core|^Socket|^CPU\\(' 查看 cpu 核数和线程数\n```\n![image](images/0ac7fde7-15c9-44e4-9bbf-dc22f48a0360.png)\n\n### 16，mkdir -p 创建多层目录\n\n`mkdir -p /xxx/xxx/` 创建多层目录\n\n### 17，查看 gcc 所有安装的版本\n\n```bash\n# centos 系统\nrpm -qa | grep gcc | awk '{print $0}'\n# ubuntu 系统\ndpkg -l | grep gcc | awk '{print $2}'\n# 查看系统当前使用 gcc 版本\ngcc -v\n```\n![image](images/f53e407f-3f8b-4e93-b0de-f84a6f4c9882.png)\n\n### 18，查看系统版本命令\n\n* `lsb_release -a`:  适用于大部分 Linux 系统，会显示出完整的版本信息，`centos` 系统无法直接使用该命令，需要安装 yum install -y redhat-lsb。\n* `cat /etc/os-release`: 适用于所有 Linux 系统。能显示较为全面的系统信息。\n* `cat /proc/version`:  该文件记录了 Linux 内核的版本、用于编译内核的 `gcc` 的版本、内核编译的时间，以及内核编译者的用户名。\n\n![image](images/a1561fd2-f491-4aaf-9a09-c1aa71b1e148.png)\n![image](images/4ab2b6b3-a1cd-4e9e-8f76-fc6d59b1a557.png)\n\n> `release` 文件通常被视为操作系统的标识。在 `/etc` 目录下放置了很多记录着发行版各种信息的文件，每个发行版都各自有一套这样记录着相关信息的文件。`LSB`（Linux 标准库Linux Standard Base）能够打印发行版的具体信息，包括发行版名称、版本号、代号等。\n\n### 19，shell 读取文件每一行内容并输出\n\n```bash\nfor line in `cat  test.txt`\ndo\n    echo $line\ndone\n```\n### 20，实时查看 GPU 显存信息\n\n`watch -n 1 nvidia-smi # 每1s显示一次显存信息`\n\n### 21，update-alternatives 管理软件版本\n\n`update-alternatives` 命令用于处理 Linux 系统中软件版本的切换，使其多版本共存。alternatives 程序所在目录 /etc/alternatives 。语法：\n\n```bash\nupdate-alternatives --help\n用法：update-alternatives [<选项> ...] <命令>\n\n命令：\n  --install <链接> <名称> <路径> <优先级>\n    [--slave <链接> <名称> <路径>] ...\n                           在系统中加入一组候选项。\n  --remove <名称> <路径>   从 <名称> 替换组中去除 <路径> 项。\n  --remove-all <名称>      从替换系统中删除 <名称> 替换组。\n  --auto <名称>            将 <名称> 的主链接切换到自动模式。\n  --display <名称>         显示关于 <名称> 替换组的信息。\n  --query <名称>           机器可读版的 --display <名称>.\n  --list <名称>            列出 <名称> 替换组中所有的可用候选项。\n  --get-selections         列出主要候选项名称以及它们的状态。\n  --set-selections         从标准输入中读入候选项的状态。\n  --config <名称>          列出 <名称> 替换组中的可选项，并就使用其中哪一个，征询用户的意见。\n  --set <名称> <路径>      将 <路径> 设置为 <名称> 的候选项。\n  --all                    对所有可选项一一调用 --config 命令。\n\n<链接> 是指向 /etc/alternatives/<名称> 的符号链接。(如 /usr/bin/pager)\n<名称> 是该链接替换组的主控名。(如 pager)\n<路径> 是候选项目标文件的位置。(如 /usr/bin/less)\n<优先级> 是一个整数，在自动模式下，这个数字越高的选项，其优先级也就越高。\n..........\n```\n\n安装命令\n\n```bash\nsudo update-alternatives --install link name path priority [ --slave slink sname spath]\n# 该命令完成 /usr/bin/gcc 到 /etc/alternatives/gcc 再到 /usr/local/${gcc_version}/bin/gcc 符号链接到建立。\nupdate-alternatives --install /usr/bin/gcc gcc /usr/local/${gcc_version}/bin/gcc 100\n```\n选项注释:\n\n* `link` 是在 /usr/bin/, /usr/local/bin/ 等默认PATH搜索目录\n* `name` 是在 /etc/alternatives 目录中的链接名\n* `path` 是真正的可执行程序的位置，可以在任何位置\n* `priority` 是优先级，数字越大优先级越高\n\n### 22，管道和重定向命令\n\n* 批处理命令连接执行，使用 `|`\n* 串联: 使用分号 `;`\n* 前面成功，则执行后面一条，否则，不执行：`&&`\n* 前面失败，则后一条执行： `||`\n\n实例1：判断 /proc 目录是否存在，存在输出success，不存在输出 failed。\n\n```bash\n$ ls /proc > log.txt && echo  success! || echo failed.\nsuccess!\n$ if ls /proc > log.txt;then echo success!;else echo failed.;fi  # 与前面脚本效果相同\nsuccess!\n$ :> log.txt  # 清空文件\n```\n\n### 23，Bash快捷输入或删除命令\n\n常用快捷键：\n\n```bash\nCtl-U   删除光标到行首的所有字符,在某些设置下,删除全行\nCtl-W   删除当前光标到前边的最近一个空格之间的字符\nCtl-H   backspace,删除光标前边的字符\nCtl-R   匹配最相近的一个文件，然后输出\n```\n\n### 24，srun 命令行参数用法\n\nsrun 命令常见的参数用法解释：\n+ -n：指定启动的 MPI 进程数。\n+ --ntasks-per-node：指定每个节点上的 MPI 进程数。\n+ -c：指定每个 MPI 进程使用的 CPU 核心数量。\n+ -p：指定要使用的分区或队列。\n+ --mem：指定每个 MPI 进程可以使用的内存量。\n+ -t：指定最长的运行时间，格式为 HH:MM:SS。\n+ --gres：指定要请求的通用资源，如 GPU、InfiniBand 等。\n+ -J：指定作业名称。\n+ -o：指定作业输出文件名。\n+ -e：指定作业错误文件名。\n\n\n### 参考资料\n1. [linux find 命令查找文件和文件夹](https://www.cnblogs.com/jiftle/p/9707518.html)\n2. [每天一个linux命令（44）：top命令](https://www.cnblogs.com/peida/archive/2012/12/24/2831353.html)\n3. [Shell中截取字符串的用法小结](https://www.cnblogs.com/kevingrace/p/8868262.html)\n4. [查看 Linux 发行版名称和版本号的 8 种方法](https://linux.cn/article-9586-1.html)\n5. [Linux Commands - Complete Guide](https://linoxide.com/linux-commands-brief-outline-examples/)"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-文件及目录管理.md",
    "content": "- [一，概述](#一概述)\n- [二，文件及目录常见操作](#二文件及目录常见操作)\n  - [2.1，创建、删除、移动和复制](#21创建删除移动和复制)\n  - [2.2，目录切换](#22目录切换)\n  - [2.3，列出目录内容](#23列出目录内容)\n  - [2.4，查找目录或者文件 find/locate](#24查找目录或者文件-findlocate)\n  - [2.5，查看及搜索文件内容](#25查看及搜索文件内容)\n- [三，总结](#三总结)\n- [四，参考资料](#四参考资料)\n\n> 本文大部分内容参看 《Linux基础》一书，根据自己的工程经验和理解加以修改、拓展和优化形成了本篇博客，不适合 Linux 纯小白，适合有一定基础的开发者阅读。\n\n## 一，概述\n**在 Linux 中一切皆文件**。文件管理主要是涉及文件/目录的创建、删除、移动、复制和查询，有`mkdir/rm/mv/cp/find` 等命令。其中 `find` 文件查询命令较为复杂，参数丰富，功能十分强大；查看文件内容是一个比较大的话题，文本处理也有很多工具供我们使用，本文涉及到这两部分的内容只是点到为止，没有详细讲解。另外给文件创建一个别名，我们需要用到 `ln`，使用这个别名和使用原文件是相同的效果。\n\n## 二，文件及目录常见操作\n### 2.1，创建、删除、移动和复制\n创建和删除命令的常用用法如下：\n\n* 创建目录：`mkdir`\n* 删除文件：`rm file(删除目录 rm -r)`\n* 移动指定文件到目标目录中：`mv source_file(文件) dest_directory(目录)` \n* 复制：`cp(复制目录 cp -r)`\n\n这些命令的常用和复杂例子程序如下\n\n```bash\n$ find ./ | wc -l  # 查看当前目录下所有文件个数(包括子目录)\n14995\n$ cp –r test/ newtest   # 使用指令 cp 将当前目录 test/ 下的所有文件复制到新目录 newtest 下\n$ mv test.txt demo.txt  # 将文件 test.txt 改名为 demo.txt\n```\n### 2.2，目录切换\n* 切换到上一个工作目录： `cd -`\n* 切换到 home 目录： `cd or cd ~`\n* 显示当前路径: `pwd`\n* 更改当前工作路径为 path: `$ cd path`\n\n### 2.3，列出目录内容\n* **显示当前目录下的文件及文件属性**：`ls`\n* 按时间排序，以列表的方式显示目录项：`ls -lrt`\n\n`ls` 命令部分参数解释如下：\n\n* `-a`：显示所有文件及目录 (. 开头的隐藏文件也会列出)\n* `-l`：除文件名称外，亦将文件型态、权限、拥有者、文件大小等资讯详细列出\n* `-r`：将文件以相反次序显示(原定依英文字母次序)\n* `-t`： 将文件依建立时间之先后次序列出\n\n常用例子如下：\n\n```bash\n$ pwd\n/\n$ ls -al  # 列出根目录下所有的文件及文件类型、大小等资讯\ntotal 104\ndrwxr-xr-x   1 root root 4096 Dec 24 01:24 .\ndrwxr-xr-x   1 root root 4096 Dec 24 01:24 ..\ndrwxrwxrwx  11 1019 1002 4096 Jan 13 09:34 data\ndrwxr-xr-x  15 root root 4600 Dec 24 01:24 dev\ndrwxr-xr-x   1 root root 4096 Jan  8 03:15 etc\ndrwxr-xr-x   1 root root 4096 Jan 11 05:49 home\ndrwxr-xr-x   1 root root 4096 Dec 23 01:15 lib\ndrwxr-xr-x   2 root root 4096 Dec 23 01:15 lib32\n... 省略\n```\n### 2.4，查找目录或者文件 find/locate\n1，查找文件或目录\n\n```bash\n$ find ./ -name \"cali_bin*\" | xargs file  # 查找当前目录下文件名含有 cali_bin 字符串的文件\n./classifynet_calib_set/cali_bin.txt: ASCII text\n./calib_set/cali_bin.txt:             ASCII text\n./cali_bin.txt:                       ASCII text\n```\n2，查找目标文件夹中是否含有 `obj` 文件:\n\n```bash\n$ find ./ -name '*.o'\n```\n`find` 是实时查找，如果需要更快的查询，可试试 `locate`；locate 会为文件系统建立索引数据库，如果有文件更新，需要定期执行更新命令来更新索引库。\n\n```bash\n$ locate string  # 寻找包含有 string 的路径\n```\n### 2.5，查看及搜索文件内容\n1，查看文件内容命令：`cat` `vi` `head` `tail more`。\n\n```bash\n$ cat -n  # 显示时同时显示行号 \n$ ls -al | more  # 按页显示列表内容\n$ head -1 filename  # 显示文件内容第一行\n$ diff file1 file1  # 比较两个文件间的差别\n```\n2，使用 `egrep` 查询文件内容:\n\n```bash\n$ egrep \"ls\" log.txt  # 查找 log.txt 文件中包含 ls 字符串的行内容\n-rw-r--r--   1 root root       2009 Jan 13 06:56 ls.txt\n```\n\n## 三，查看硬盘或指定目录空间大小\n\n* `df` ：查看**磁盘空间**占用情况\n* `du` ：查看**目录**占用的空间大小。\n\n```bash\ndf -hT #查看硬盘使用情况。\ndu -h --max-depth=1 floder_name # 查看当前目录下所有文件/文件夹的空间大小\ndu -h -d 0 . # 查看当前目录空间大小\ndu -sh foldername # 查看指定目录空间大小\n```\n\n![image](images/c62f2296-e82d-4d3c-bb6f-07c1d581fe6b.png)\n\n## 三，总结\n利用 `ls -al` 命令查看文件属性及权限，已知了 `Linux` 系统内文件的三种身份(文件拥有者、文件所属群组与其他用户)，每种身份都有四种权限(`rwxs`)。可以使用 `chown`, `chgrp`, `chmod` 去修改这些权限与属性。文件是实际含有数据的地方，包括一般文本文件、数据库内容文件、二进制可执行文件(binary program)等等。\n\n* 文件管理，目录的创建、删除、查询、管理: `mkdir` `rm` `mv` `cp`\n* 文件的查询和检索命令： `find` `locate`\n* 查看文件内容命令：`cat` `vi` `tail more`\n* 管道和重定向命令： `;` `|` `&&` `>`\n\n## 四，参考资料\n[《Linux基础》](https://linuxtools-rst.readthedocs.io/zh_CN/latest/base/index.html)"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-文件权限与属性.md",
    "content": "- [一，文件类型](#一文件类型)\n  - [1.1，概述](#11概述)\n  - [1.2，正规文件(regular file)](#12正规文件regular-file)\n  - [1.3，目录(directory)](#13目录directory)\n  - [1.4，链接文件(link)](#14链接文件link)\n  - [1.5，设备与装置文件(device)](#15设备与装置文件device)\n  - [1.6，资料接口文件(sockets)：](#16资料接口文件sockets)\n  - [1.7，数据输送文件(FIFO, pipe)：](#17数据输送文件fifo-pipe)\n  - [1.8，文件拓展名](#18文件拓展名)\n- [二，文件属性与权限](#二文件属性与权限)\n  - [2.1，Linux 文件属性](#21linux-文件属性)\n  - [2.2，Linux 文件权限](#22linux-文件权限)\n  - [2.3，如何改变文件属性和权限](#23如何改变文件属性和权限)\n  - [2.4，文件与目录的权限意义](#24文件与目录的权限意义)\n- [三，Linux 文件属性与权限总结](#三linux-文件属性与权限总结)\n- [四，参考资料](#四参考资料)\n\n> Linux 系统由 Linux 内核、shell、文件系统和第三方应用软件组成。Linux 文件权限与属性是学习 Linux 系统的一个重要关卡，必须理解这个部分内容的概念。\n\n## 一，文件类型\n### 1.1，概述\n\n一个基本概念：任何装置在 Linux 下都是文件，数据沟通的接口也有专属的文件在负责，Linux 的文件种类繁多，常用的是一般文件(`-`)与目录文件(`d`)。\n\n注意：`Linux` 文件类型和文件的文件名所代表的意义是两个不同的概念，在 `linux` 中文件类型与文件扩展名没有关系。它不像 `Windows` 那样是依靠文件后缀名来区分文件类型的，在 `linux` 中文件名只是为了方便操作而的取得名字。\n\n`Linux` 文件类型常见的有：**普通文件、目录、字符设备文件、块设备文件、符号链接文件**等。查看文件类型方法: 可以使用 `ls -al` 命令列出的信息中第一栏十个字符中，第一个字符为文件的类型。\n\n![image](images/2c00f3a1-eb00-4665-b0ed-c8dd415e227a.png)\n\n### 1.2，正规文件(regular file)\n\n就是一般我们在进行存取的类型的文件，在由 `ls -al` 所显示出来的属性方面，第一个字符为 `-`，例如 `-rwxrwxrwx`。另外，依照文件的内容，又大略可以分为：\n\n* 纯文本档(`ASCII`)：Linux 系统中最为常见的一种文件类型，称为纯文本是因为文件内容为人类可以直接读取到的数据，例如数字、字母等。\n* 二进制文件(`binary`)：Linux 系统仅认识且可以执行的二进制文件 binary file，Linux 系统中可执行的文件就是这种类型，例如 cat 就是一个 binary file。\n* 数据格式文件(`data`)： 有些程序在运作的过程当中会读取某些特定格式的文件，那些特定格式的文件可以被称为数据文件 (`data file`)。举例来说，我们的 Linux 在使用者登入时，都会将登录的数据记录在 `/var/log/wtmp` 文件内，该文件是一个 `data file`，它能够通过 `last` 这个指令读出来，但是使用 `cat` 命令读取时会读出乱码，因为他是属于一种特殊格式的文件。\n\n### 1.3，目录(directory)\n第一个属性为 `d`，例如 `drwx------`。\n\n![image](images/6c34e412-890f-4565-8424-998517fb44b2.png)\n\n### 1.4，链接文件(link)\n类似 Windows 系统下的快捷键，第一个属性为 `l`，例如 `lrwxrwxrwx`。\n\n### 1.5，设备与装置文件(device)\n与系统周边设备及存储相关的一些文件，通常集中在 `/dev` 目录下，一般分为两种：\n\n* 区块(block)设备类型：就是一些储存数据， 以提供系统随机存取的接口设备，比如硬盘设备，第一个属性为 `b`。\n* 字符(character)设备文件：一些串行端口的接口设备，例如键盘、鼠标、摄像头等。这些设备的特性是\\*\\*一次性读取\"，不能够截断输出，第一个属性为 `c`。\n\n### 1.6，资料接口文件(sockets)：\n被用于网络上的数据连接了。我们可以启动一个程序来监听客户端的要求， 而客户端就可以透过这个 `socket` 来进行数据的沟通了。第一个属性为 `s`，最常在 `/run` 或 `/tmp` 这些目录中可以看到这种文件类型。\n\n![image](images/cfce666e-0390-441b-bf87-baffb21aa628.png)\n\n### 1.7，数据输送文件(FIFO, pipe)：\n`FIFO` 也是一种特殊的文件类型，他主要的目的在解决多个程序同时存取一个文件所造成的错误问题。`FIFO` 是 `first-in-first-out` 的缩写。第一个属性为 `p`。\n\n### 1.8，文件拓展名\n**注意 Linux 系统的文件是没有所谓的拓展名的**。 一个 Linux 文件能不能被执行，与他的 `ls -al` 展示的文件信息的**第一栏的十个属性**有关， 与文件名根本一点关系也没有，这与 Windows 不同，在Linux 系统下，只要用户的权限有 `x`，文件就可以被执行。拥有了 `x` 权限，表示文件可以被执行，但是只有具有可执行的程序代码文件才能被执行，文本等文件即使有权限也是不能执行成功的。\n\n## 二，文件属性与权限\n### 2.1，Linux 文件属性\n`ls -al` 命令：列出所有的文件详细的权限与属性 (包含隐藏文件-文件名第一个字符为『 `.` 』的文件)。 `ls -al` 展示的文件属性信息如下：\n\n![image](images/eeb259ca-1f6b-4066-a59e-dec2d8c3f41b.png)\n\n* 第一列代表这个文件的类型与权限(permission)；第一列的第一个字符代表这个文件是『目录、 文件或链接文件等等文件类型』：\n   * 当为 `d` 则是目录，例如上图文件名为『.config』的那一行；\n   * 当为 `-` 则是文件，例如上图文件名为『initial-setup-ks.cfg』那一行；\n   * 若是 `l` 则表示为链接文件(link file)；\n   * 若是 `b` 则表示为装置文件里面的可供储存的接口设备(可随机存取装置)；\n   * 若是 `c` 则表示为装置文件里面的串行端口设备，例如键盘、鼠标(一次性读取装置)。\n* 第二列表示有多少文件名连结到此节点(i-node)；\n* 第三列表示这个文件(或目录)的『**拥有者账号**』；\n* 第四列表示这个文件的**所属群组**；\n* 第五列为这个文件的**容量大小**，默认单位为 `bytes`；\n* 第六列为这个文件的建档日期或者是最近的修改日期；\n* 第七列为这个文件的文件名。\n\n`ls -al` 命令展示的文件属性的七个字段的意义很重要，必须理解和熟记，这是掌握 `Linux` 文件权限与目录管理的基础知识。\n\n### 2.2，Linux 文件权限\nLinux 文件的基本权限就有九个，分别是 `owner/group/others` 三种身份各有自己的 `read/write/execute` 权限。在 Linux 中，对于文件的权限（`rwx`），分为三部分，**第一部分是该文件的拥有者所拥有的权限，第二部分是该文件所在用户组的用户所拥有的权限，最后一部分是其他用户所拥有的权限**。\n每个文件/文件夹的属性都用 `10` 个字符表示，第一个字符如果是 `d`：表示文件夹，如果是 `-`：表示文件。用（`rwx`）表示文件权限，其中`r`: 可读`（4）`，`w`: 可写`（2）`，`x`: 可执行（`1`）。举例，`drwxr-xrw-` 表示文件夹拥有者拥有可读可写可执行的权限，**用户所在用户组和其他用户无任何权限**，命令的详细解释如下。\n\n* 从第二到第四位 `(rwx) ` 是文件所有者的权限：可读、可写、可执行。\n* 从第五到第七位 `(r-x)` 文件夹用户拥有者所在组的权限：可读、可执行。\n* 从第八位到第十位 `(rw-)` 其他人对这个文件夹操作的权限.：可读、可写。\n\n`Linux` 系统查看文件/目录权限示例，如下：\n\n```bash\nroot@5b84c8f05303:/data/project# ls -al\ntotal 16\ndrwx------ 4 1018 1002 4096 Jul 20 02:59 .\ndrwx------ 8 1018 1002 4096 Jul 20 02:57 ..\ndrwx------ 6 1018 1002 4096 Jul 20 02:57 DeepPruner\n-rw-r--r-- 1 root root    0 Jul 20 02:59 demo.py\ndrwx------ 8 1018 1002 4096 Jul 20 02:57 nn_tools-master\n```\n![image](images/b7d1fdca-5ae0-4d88-8e0d-00dedeb439b6.png)\n\n### 2.3，如何改变文件属性和权限\n`Linux/Unix` 是多人多工操作系统，所有的文件皆有拥有者。利用 `chown` 命令可以改变文件的拥有者(用户)和群组，用户可以是用户名或者`用户 ID`，组可以是组名或者`组 ID`。**注意，普通用户不能将自己的文件改变成其他的拥有者，其操作权限一般为管理员(****root**** 用户)**；同时用户必须是已经存在系统中的账号，也就是在 `/etc/passwd` 这个文件中有纪录的用户名称才能改变。\n\n三个常用于群组、拥有者、各种身份的权限之修改的命令，如下所示：\n\n* `chown` : 改变文件的拥有者。如递归子目录修改命令： `chown -R user_name folder/`\n* `chgrp` : 改变文件所属群组。\n* `chmod` : 改变文件读、写、执行权限(权限分数 `r:4 w:2 x:1`)属性。如增加脚本可执行权限命令： `chmod a+x myscript` 。\n\n添加用户和用户组的命令：`adduser` `groupadd`，其简单用法如下所示：\n\n* `adduser harley`：新建 harley 用户\n* `passwd harley`：给 harley 用户设置密码\n* `groupadd harley`：新建 harley工作组\n* `useradd -g harley harley`：新建 harley 用户并增加到 harley 工作组\n\n**改变所属群组 chgrp/chown/chmod 命令的用法如下**：\n\n```bash\n$ chgrp [-R] group dirname/filename  # -R : 进行递归(recursive)的持续变更，亦即连同子目录下的所有文件、目录都更新成为这个群组之意。\n$ chown [-R] 账号名称 文件或目录\n$ chown [-R] 账号名称:组名 文件或目录\n$ chmod [-R] xyz 文件或目录  # xyz : 数字类型的权限属性分数值， 为 rwx 属性数值的相加。\n```\n`chgrp` 范例：将 `test` 的工作组从 `harley` 改为 `root`（前提是当前用户切换为 `root` 了，否则会提示权限不足的错误）：\n\n![image](images/375869b7-c94c-4893-b291-9abe6b22356a.png)\n\n`chmod` 范例：将 `.bashrc` 这个文件所有的权限都设定启用。\n\n```bash\nroot@17c30d837aba:~# ls -al .bashrc\n-rw-r--r-- 1 root root 3479 Jan  8 03:14 .bashrc\nroot@17c30d837aba:~# chmod 777 .bashrc\nroot@17c30d837aba:~# ls -al .bashrc\n-rwxrwxrwx 1 root root 3479 Jan  8 03:14 .bashrc\n```\n### 2.4，文件与目录的权限意义\n我们可以利用 `ls -al` 命令查看文件属性及权限，已知了 `Linux` 系统内文件的三种身份(拥有者、群组与其他人)，每种身份都有三种权限(`rwx`)，再使用 `chown`, `chgrp`, `chmod` 去修改这些权限与属性。\n**文件是实际含有数据的地方**，包括一般文本文件、数据库内容文件、二进制可执行文件(binary program)等等。因此，权限对于文件来说，他的意义是这样的：\n\n* `r (read)`：可读取此一文件的实际内容，如读取文本文件的文字内容等；\n* `w (write)`：可以编辑、新增或者是修改该文件的内容(但不含删除该文件)；\n* `x (eXecute)`：该文件具有可以被系统执行的权限。Linux 底下， 文件是否能被执行，是由 `x` 这个权限来决定的！跟文件名是没有绝对的关系，即使是能够执行成功的 `.sh` 脚本文件，如果没有 `x` 可执行权限，文件也不能被执行。\n\n**目录主要的内容是记录文件名列表**，文件名与目录有强烈的关连。对一般文件来说，` rwx` 主要是针对『文件的内容』来设计权限，对目录来说， `rwx` 则是针对『目录内的文件名列表』来设计权限。权限对文件和目录重要性的理解汇总成如下表格：\n\n![image](images/06b2a08b-152d-4553-b17a-7c0e3bb9ccea.png)\n\n## 三，Linux 文件属性与权限总结\n利用 `ls -al` 命令查看文件属性及权限，已知了 `Linux` 系统内**文件的三种身份**(文件拥有者、文件所属群组与其他用户)，**每种身份都有四种权限**(`rwxs`)。可以使用 `chown`, `chgrp`, `chmod` 去修改这些权限与属性。文件是实际含有数据的地方，包括一般文本文件、数据库内容文件、二进制可执行文件(binary program)等等。\n\n## 四，参考资料\n《鸟哥的Linux私房菜 基础篇 第四版》"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-文本处理命令.md",
    "content": "- [概述](#概述)\n- [find 文件查找](#find-文件查找)\n- [grep 文本搜索](#grep-文本搜索)\n- [参考资料](#参考资料)\n\n## 概述\n\n`Linux` 下使用 Shell 处理文本时最常用的工具有： `find、grep、xargs、sort、uniq、tr、cut、paste、wc、sed、awk`。\n\n## find 文件查找\n\n`man` 文档给出的 `find` 命令的一般形式为：\n```shell \nfind [-H] [-L] [-P] [-D debugopts] [-Olevel] [starting-point...] [expression]\n```\n这对于大部分人来说都太复杂了，[-H] [-L] [-P] [-D debugopts] [-Olevel] 这几个选项并不常用，`find` 命令的常用形式可以简化为：\n```shell\n$ find [PATH] [option] [action]\n```\n**1，根据文件或者正则表达式进行匹配**\n\n```shell\n$ find .  # 查找当前目录及子目录下所有文件及文件夹\n$ find /data -name \"*.txt\"  # 在 /data 目录及子目录下查找以 .txt 结尾的文件名\n$ find . \\( -name \"*.txt\" -o -name \"*.pdf\" \\)  # 当前目录及子目录下查找所有以 .txt 和 .pdf 结尾的文件\n$ find . -maxdepth 1 -type d  # 查找当前目录下所有的子目录\n$ find . -maxdepth 1 -regex \".*\\.txt$\"  # 基于正则表达式匹配当前目录下的所有以 .txt 结尾的文件\n./multi_classifynet_infer_ret.txt\n./cali_left_img.txt\n... 省略\n```\n**2，根据文件类型进行搜索**\n\n```shell\nfind . -type 类型参数，f 普通文件，l 符号连接，d 目录，c 字符设备，b 块设备，s 套接字，p Fifo\n$ find . -maxdepth 1 -type d  # 查找当前目录下的所有子目录\n```\n\n**3，基于目录深度搜索**\n\n```shell\n$ find . maxdepth 3 -type f  # 目录向下最大深度限制 3\n```\n\n**4，根据文件时间戳进行搜索**\n\n`find . -type -f 时间戳参数`。与时间有关的选项：共有 `-atime`, `-ctime` 与 `-mtime`，以 `-mtime` 说明\n+ -mtime n ： n 为数字，意义为在 n 天之前的『一天之内』被更改过内容的文件；\n+ -mtime +n ：列出在 n 天之前(不含 n 天本身)被更改过内容的文件名；\n+ -mtime -n ：列出在 n 天之内(含 n 天本身)被更改过内容的文件名。\n+ -newer file ： file 为一个存在的文件，列出比 file 还要新的文件名\n\n```shell\n$ find /etc -newer /etc/passwd  # 寻找 /etc 底下的文件，如果文件日期比 /etc/passwd 新就列出\n```\n\n**5，与文件权限及名称有关的参数:**\n\n+ `-name filename`：搜寻文件名为 `filename` 的文件。\n+ `-size [+-]SIZE`：搜寻比 SIZE 还要大(+)或小(-)的文件。 这个 SIZE 的规格有：`c`: 代表 byte， `k`: 代表 1024 bytes。所以，要找比 50KB还要大的文件，就是 `-size +50k`。\n+ `-type TYPE`：搜寻文件的类型为 TYPE 的， 类型主要有：一般正规文件 (f), 装置文件 (b, c), 目录 (d), 连结档 (l), socket (s), 及 FIFO (p) 等属性。\n+ `-perm mode`：搜寻文件权限『刚好等于』 `mode` 的文件， 这个 mode 为类似 chmod 的属性值， 举例来说， `-rwxr-xr-x` 的属性为 `755`。\n+ `-perm -mode`：搜寻文件权限『必须要全部囊括 mode 的权限』的文件， 举例来说，我们要搜寻 `-rwxr--r--`，亦即 `744` 的文件，使用 `-perm -744`，但是当一个文件的权限为 `-rwxr-xr-x` ，亦即 `755` 时，也会被列出来，因为 `-rwxr-xr-x` 的属性已经包括了` -rwxr--r--` 的属性了。\n+ `-perm /mode`：搜寻文件权限『包含任一 `mode` 的权限』的文件， 举例来说，我们搜寻 -rwxr-xr-x ，亦即 -perm /755 时，但一个文件属性为 -rw-------也会被列出来，因为他有 `-rw....` 的属性存在。\n\n范例：\n\n```shell\nroot@17c30d837aba:/data# find . -maxdepth 1 -perm 777  # 查找当前目录下文件权限刚好等于777 的文件\n.\n./honggaozhang\n./demo.sh\n```\n\n## grep 文本搜索\n\n`grep` 支持使用正则表达式搜索文本，并把匹配的行打印出来。`grep` 命令常见用法，在文件中搜索一个单词，命令会返回一个包含 `“match_pattern”` 的文本行：\n\n```shell\ngrep match_pattern file_name\ngrep \"match_pattern\" file_name\n```\n\n常用参数:\n\n+ `-o`：只输出匹配的文本行，`-v` 只输出没有匹配的文本行\n+ `-c`：统计文件中包含文本的次数： `grep -c “text” filename\n+ `-n`：打印匹配的行号\n+ `-i`：搜索时忽略大小写\n+ `-l`：只打印文件名\n\n```shell\n$ grep \"class\" . -R -n  # 在多级目录中对文本递归搜索(程序员搜代码的最爱)\n$ grep -e \"class\" -e \"vitural\" file  #  匹配多个模式\n```\n\n## 参考资料\n\n+ [【日常小记】linux中强大且常用命令：find、grep](cnblogs.com/skynet/archive/2010/12/25/1916873.html)\n+ 鸟哥的Linux私房菜 基础篇 第四版\n"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-新手必备命令.md",
    "content": "- [概述](#概述)\n- [系统工作](#系统工作)\n- [系统状态检测](#系统状态检测)\n- [文件与目录管理](#文件与目录管理)\n- [文件内容查阅与编辑](#文件内容查阅与编辑)\n- [打包压缩与搜索](#打包压缩与搜索)\n- [常见命令图解](#常见命令图解)\n- [参考资料](#参考资料)\n\n### 概述\n常见执行 `Linux` 命令的格式是这样的：\n\n```bash\n命令名称 [命令参数] [命令对象]\n```\n注意，命令名称、命令参数、命令对象之间请用**空格键**分隔。\n命令对象一般是指要处理的文件、目录、用户等资源，而命令参数可以用长格式（完整的选项名称），也可以用短格式（单个字母的缩写），两者分别用 `--` 与 `-` 作为前缀。\n\n### 系统工作\n1. `echo`：用于在 `shell` 编程中打印 `shell` 变量的值，或者直接输出指定的字符串。\n2. `date`：显示或设置系统时间与日期。\n3. `reboot`：重新启动正在运行的 Linux 操作系统。\n4. `poweroff`：关闭计算机操作系统并且切断系统电源。\n5. `wget`：用来从指定的 `URL`下载文件。wget 非常稳定，它在带宽很窄的情况下和不稳定网络中有很强的适应性，如果是由于网络的原因下载失败，wget 会不断的尝试，直到整个文件下载完毕。\n6. `ps`：将某个时间点的进程运作情况撷取下来，可以搭配 `kill` 指令随时中断、删除不必要的程序。`ps` 命令可以查看进程运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等情况。使用 `ps -l` 则仅列出与你的操作环境 ( `bash`) 有关的进程而已；使用 `ps aux` 观察系统所有进程。\n7. `top`：动态观察进程的变化。\n8. `pstree`：`pstree -A` 列出目前系统上面所有的进程树的相关性。\n9. `pidof`：查找指定名称的进程的进程号 `id` 号。\n10. `kill`：删除执行中的程序或工作，后面必须要加上 `PID` (或者是 `job number`)，用法：`killall -signal 指令名称/PID`。`kill` 可将指定的信息送至程序，预设的信息为 `SIGTERM(15)`,可将指定程序终止，若仍无法终止该程序，可使用 `SIGKILL(9)` 信息尝试强制删除程序。程序或工作的编号可利用 `ps` 指令或 `job` 指令查看。 \n\n### 系统状态检测\n\n1. `ifconfig`：于配置和显示 Linux 内核中网络接口的网络参数。\n2. `uname`：打印当前系统相关信息（内核版本号、硬件架构、主机名称和操作系统类型等），`-a` 或 `--all`：显示全部的信息。\n3. `uptime`：打印系统总共运行了多长时间和系统的平均负载。uptime 命令可以显示的信息显示依次为：现在时间、系统已经运行了多长时间、目前有多少登陆用户、系统在过去的1分钟、5分钟和15分钟内的平均负载。\n4. `free`：显示当前系统未使用的和已使用的内存数目，还可以显示被内核使用的内存缓冲区，`-m`：以MB为单位显示内存使用情况。\n5. `who`：显示**目前**登录系统的用户信息。执行 `who` 命令可得知目前有那些用户登入系统，单独执行 who命令会列出登入帐号，使用的终端机，登入时间以及从何处登入或正在使用哪个 X 显示器。\n6. `last`：显示用户**最近**登录信息。单独执行 last 命令，它会读取 `/var/log/wtmp` 的文件，并把该给文件的内容记录的登入系统的用户名单全部显示出来。\n7. `history`：显示指定数目的指令命令，读取历史命令文件中的目录到历史命令缓冲区和将历史命令缓冲区中的目录写入命令文件。\n8. `sosreport` 命令：收集并打包诊断和支持数据\n\n### 文件与目录管理\n\n1. `pwd` 命令：以绝对路径的方式显示用户当前工作目录。\n2. `cd` 命令：切换工作目录至 `dirname`。 其中 dirName 表示法可为绝对路径或相对路径。`~` 也表示为 `home directory` 的意思，`.`则是表示目前所在的目录，`..` 则表示目前目录位置的上一层目录。\n3. `cp, rm, mv`：复制、删除与移动文件或目录 。\n4. `ls`：显示文件的文件/目录的名字与相关属性。`-l` 参数：长数据串行出，包含文件的属性与权限等等数据 (常用)。\n5. `touch`：有两个功能：一是用于把已存在文件的时间标签更新为系统当前的时间（默认方式），它们的数据将原封不动地保留下来；二是用来创建新的空文件。\n6. `file`：用来探测给定文件的类型。file 命令对文件的检查分为文件系统、魔法幻数检查和语言检查 3 个过程\n\n### 文件内容查阅与编辑\n文件内容查阅命令如下：\n\n* `cat`：由第一行开始显示文件内容\n* `tac`：从最后一行开始显示，可以看出 tac 是 cat 的倒着写！\n* `nl`：显示的时候，顺道输出行号！\n* `more`：一页一页的显示文件内容\n* `less`：与 more 类似，但是比 more 更好的是，他可以往前翻页！\n* `head`：只看头几行\n* `tail`：只看尾巴几行\n* `od`：以二进制的方式读取文件内容！\n\n文件内容查阅命令总结：\n\n* 直接查阅一个文件的内容可以使用 `cat/tac/nl` 这几个命令；\n* 需要翻页检视文件内容使用 `more/less` 命令；\n* 取出文件前面几行 (`head`) 或取出后面几行 (`tail`)文字的功能使用 `head` 和 `tail` 命令，注意 `head` 与 `tail` 都是以『行』为单位来进行数据撷取的；\n\n文本内容编辑命令如下：\n\n1. `tr`：可以用来删除一段讯息当中的文字，或者是进行文字讯息的替换。\n2. `wc`：可以帮我们计算输出的讯息的整体数据。\n3. `stat`：用于显示文件的状态信息。`stat` 命令的输出信息比 `ls` 命令的输出信息要更详细\n4. `cut`：可以将一段讯息的某一段给他『切』出来，处理的讯息是以『行』为单位。\n5. `diff`：在最简单的情况下，比较给定的两个文件的不同。如果使用 `“-”` 代替“文件”参数，则要比较的内容将来自标准输入。`diff` 命令是以逐行的方式，比较文本文件的异同处。如果该命令指定进行目录的比较，则将会比较该目录中具有相同文件名的文件，而不会对其子目录文件进行任何比较操作。\n\n### 打包压缩与搜索\n6. `tar`：利用 `tar` 命令可以把一大堆的文件和目录全部打包成一个文件，这对于备份文件或将几个文件组合成为一个文件以便于网络传输是非常有用的。注意打包是指将一大堆文件或目录变成一个总的文件；压缩则是将一个大的文件通过一些压缩算法变成一个小文件。为什么要区分这两个概念呢？这源于 Linux 中很多压缩程序只能针对一个文件进行压缩，这样当你想要压缩一大堆文件时，你得先将这一大堆文件先打成一个包（`tar` 命令），然后再用压缩程序进行压缩（`gzip bzip2` 命令）。\n7. `grep`：（global search regular expression(RE) and print out the line，全面搜索正则表达式并把行打印出来）一种强大的**文本搜索工具**，能够使用正则表达式搜索文本，并把匹配的行打印出来。`grep` 它是分析一行信息， 若当中有我们所需要的信息，就将该行拿出来。用法：`grep [-acinv] [--color=auto] '搜寻字符串' filename`。\n8. `which`：查找命令的完整文件名。用法：`which [-a] command`，`a` : 将所有由 `PATH` 目录中可以找到的指令均列出，而不止第一个被找到的指令名称。`find` 命令是根据`『PATH』`这个环境变量所规范的路径，去搜寻命令的完整文件名。\n9. `find`：用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为欲查找的目录名。如果使用该命令时，不设置任何参数，则 `find` 命令将在当前目录下查找子目录与文件。并且将查找到的子目录和文件全部进行显示。用法举例：在 `/home` 目录及其子目录下查找以 `.txt` 结尾的文件名 `find /home -name \"*.txt\"`。\n10. `whereis/locate`：`whereis` 只找系统中某些特定目录底下的文件而已， locate则是利用数据库来搜寻文件名，两者速度更快， 但没有实际搜寻硬盘内的文件系统状态。\n\n### 常见命令图解\n这个思维导图记录了常见命令，有利于索引，来源[Linux基础命令（01）【Linux基础命令、ip查看、目录结构、网络映射配置】](https://blog.csdn.net/zkk1973/article/details/80606832)\n\n![image](images/0.3605952030126039.png)\n\n### 参考资料\n* [新手linux命令必须掌握命令](https://man.linuxde.net/xinshoumingling)\n* 鸟哥的Linux私房菜 基础篇 第四版\n* [Linux基础命令（01）【Linux基础命令、ip查看、目录结构、网络映射配置】](https://blog.csdn.net/zkk1973/article/details/80606832)"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-查看cpu、内存和环境等信息.md",
    "content": "使用 `Linux` 系统的过程中，我们经常需要查看**系统、资源、网络、进程、用户**等方面的信息，查看这些信息的常用命令值得了解和熟悉。\n\n1，**系统**信息查看常用命令如下：\n\n```bash\nuname -m && cat /etc/*release # 查询 linux 服务器当前运行环境的操作系统架构及版本\nlsb_release -a         # 查看操作系统版本(裁剪版不一定支持) \ncat /etc/os-release    # 查看操作系统版本 (适用于所有的linux，包括Redhat、SuSE、Debian等发行版，但是在debian下要安装lsb)   \ncat /proc/cpuinfo      # 查看CPU信息\nhostname               # 查看计算机名\nlsusb -tv              # 列出所有USB设备\nenv                    # 查看环境变量\n```\n2，**资源**信息查看常用命令如下：\n\n```bash\nfree -h                # 将内存大小以人类可读的形式显示出来（单位GB）\ndf -h                  # 查看各分区使用情况\ndf -hT                # 查看硬盘使用情况\ndu -sh <目录名>        # 查看指定目录的大小\nuptime                 # 查看系统运行时间、用户数、负载\n```\n3，**网络**信息查看常用命令如下\n\n```bash\nifconfig               # 查看所有网络接口的属性\nroute -n               # 查看路由表\n```\n4，**进程**信息查看常用命令如下\n\n```bash\nps -ef                 # 查看所有进程\ntop                    # 实时显示进程状态\n```\n5，**用户**信息查看常用命令如下\n\n```bash\nw                      # 查看活动用户\nid <用户名>            # 查看指定用户信息\nlast                   # 查看用户登录日志\ncut -d: -f1 /etc/passwd   # 查看系统所有用户\ncut -d: -f1 /etc/group    # 查看系统所有组\ncrontab -l             # 查看当前用户的计划任务\n```\n![image](images/b3e99fdb-ebac-467e-861f-5c034fc7881c.png)\n\n更多命令及理解，参考此[链接](https://blog.csdn.net/bluishglc/article/details/41390589)。\n\n6，**查看操作系统、框架、库以及工具版本命令汇总**：\n\n```bash\ncat /etc/os-release # 适合所有linux系统，查看操作系统版本，显示信息比较全\ncat /etc/issue # 该命令适用于所有Linux系统，显示的版本信息较为简略，只有系统名称和对应版本号。\nuname -a # 显示系统详细信息，包括内核版本号、硬件架构等\ncat /proc/version # 查看linux 内核\nnvcc -V # 查看 cuda 版本\ncat /usr/local/cuda/version.txt # 没有安装 nvcc 条件用\ncat /usr/local/cuda/include/cudnn.h | grep CUDNN_MAJOR -A 2 # 查看cudnn版本\nfind / -name NvInferVersion.h && cat /usr/local/cuda-11.0/targets/x86_64-linux/include/NvInferVersion.h | grep NV_TENSORRT # 查看cudnn版本通用\ngcc -v # 查看 gcc 版本\ncmake -version # 查看 cmake 版本\npkg-config --modversion opencv # 查看 opencv 版本\nffmpeg -version # 查看 ffmpeg 版本\n```\n\n`uname` 命令是用于显示系统信息的命令。它通常用于获取操作系统的一些基本信息。`uname -a` 这将显示包括内核名称、版本、硬件架构等在内的详细信息。uname 命令在 Linux 和类 Unix 系统上都是可用的。\n\n### 参考资料\n[怎么查看Linux服务器硬件信息，这些命令告诉你](https://zhuanlan.zhihu.com/p/144368206)"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-查看和设置环境变量.md",
    "content": "- [一，查看环境变量](#一查看环境变量)\n- [二，环境变量类型](#二环境变量类型)\n- [三，设置环境变量](#三设置环境变量)\n- [四，参考资料](#四参考资料)\n\n## 一，查看环境变量\n\n在 Linux中，**环境变量**是一个很重要的概念。**环境变量可以由系统、用户、`Shell` 以及其他程序来设定**，其是保存在变量 `PATH` 中。环境变量是一个可以被赋值的字符串，赋值范围包括数字、文本、文件名、设备以及其他类型的数据。\n> 值得一提的是，Linux 系统中环境变量的名称一般都是大写的，这是一种约定俗成的规范。\n\n1，使用 `echo` 命令查看单个环境变量，例如：`echo $PATH`；使用 `env` 查看当前系统定义的所有环境变量；使用 `set` 查看所有本地定义的环境变量。查看 `PATH` 环境的实例如下：\n\n![PATH环境](../../data/images/PATH环境.png)\n\n**使用 `unset` 删除指定的环境变量**，`set` 也可以设置某个环境变量的值。清除环境变量的值用 unset 命令。如果未指定值，则该变量值将被设为 NULL。示例如下：\n\n```shell\n$ export TEST=\"Test...\"  # 增加一个环境变量 TEST\n$ env|grep TEST  # 此命令有输入，证明环境变量 TEST 已经存在了\nTEST=Test...\nunset  TEST  # 删除环境变量 TEST\n$ env|grep TEST  # 此命令没有输出，证明环境变量 TEST 已经删除\n```\n\n2，`C` 程序调用环境变量函数\n\n+ `getenv()`： 返回一个环境变量。\n+ `setenv()`：设置一个环境变量。\n+ `unsetenv()`： 清除一个环境变量。\n\n## 二，环境变量类型\n\n1，按照变量的生存周期划分，Linux 变量可分为两类：\n\n+ 永久的：需要修改配置文件，变量永久生效。\n+ 临时的：使用 `export` 命令声明即可，变量在关闭 `shell` 时失效。\n\n2，按作用的范围分，在 Linux 中的变量，可以分为环境变量和本地变量：\n\n+ 环境变量：相当于全局变量，存在于所有的 Shell 中，具有继承性；\n+ 本地变量：相当于局部变量只存在当前 Shell 中，本地变量包含环境变量，非环境变量不具有继承性。\n\n环境变量名称都是**大写**，常用的环境变量意义如下所示。\n\n+ `PATH`：决定了 `shell` 将到哪些目录中寻找命令或程序\n+ `HOME`：当前用户主目录\n+ `HISTSIZE`：历史记录数\n+ `LOGNAME`：当前用户的登录名\n+ `HOSTNAME`：指主机的名称\n+ `SHELL`：当前用户 Shell 类型\n+ `LANGUGE`：语言相关的环境变量，多语言可以修改此环境变量\n+ `MAIL`：当前用户的邮件存放目录\n+ `PS1`：基本提示符，对于 root 用户是 `#`，对于普通用户是 `$`\n\n## 三，设置环境变量\n\n设置环境有多种用途，比如**配置交叉编译工具链的时候一般需要指定编译工具的路径**，此时就需要设置环境变量。\n\n在 `Linux` 中**设置环境变量**有三种方法：\n\n**1，所有用户永久添加环境变量**: `vi /etc/profile`，在 `/etc/profile` 文件中添加变量。\n\n- `vi /etc/profile` # 通过这种方式，在关闭 xshell后，添加的环境变量不生效\n- 文件末尾添加：`export PATH=\"/usr/local/cuda/lib64:$PATH\"`\n- `source /etc/profile` # 激活后，环境变量才可永久生效\n\n**2，当前用户永久添加环境变量**: `vi ~/.bash_profile`，在用户目录下的 `~/.bash_profile` 文件中添加变量。\n\n- `vim ~/.bashrc` # 编辑 `.bashrc` 文件，在关闭 `xshell` 后，添加的环境变量仍然生效\n- 文件末尾添加: `export PATH=\"/usr/local/cuda/lib64:$PATH\"`\n- `source ~/.bashrc` \n\n**3，临时添加环境变量 `PATH`**: 可通过 `export` 命令，如运行命令 `export PATH=/usr/local/cuda/lib64:$PATH`，将 `/usr/local/cuda/lib64` 目录临时添加到环境变量中。查看是否已经设置好，可用命令 `export` 查看。\n\n前面两种方法可以通过 `echo $PATH` 命令查看终端打印结果是否有添加的路径，来确认已经设置好环境变量。\n\n## 四，参考资料\n\n1. [Linux环境变量总结](https://www.jianshu.com/p/ac2bc0ad3d74)\n2. [在Linux里设置环境变量的方法（export PATH）](https://www.cnblogs.com/amboyna/archive/2008/03/08/1096024.html)"
  },
  {
    "path": "1-computer_basics/Linux系统/Linux基础-查看进程命令ps和top.md",
    "content": "- [1，使用 ps 命令找出 CPU 占用高的进程](#1使用-ps-命令找出-cpu-占用高的进程)\n- [2，通过 top 命令定位占用 cpu 高的进程](#2通过-top-命令定位占用-cpu-高的进程)\n- [3，htop 系统监控与进程管理软件](#3htop-系统监控与进程管理软件)\n- [4，参考资料](#4参考资料)\n\n## 1，使用 ps 命令找出 CPU 占用高的进程\n\n`ps` 是 进程状态 `(process status)` 的缩写，它能显示系统中活跃的/运行中的进程的信息。它提供了当前进程及其详细信息，诸如用户名、用户 `ID`、`CPU` 使用率、内存使用、进程启动日期时间、命令名等等的快照。只打印命令名字而不是命令的绝对路径，以运行下面的格式 ps 命令：\n\n```bash\n~$ ps -eo pid,ppid,%mem,%cpu,comm --sort=-%cpu | head\n```\n运行结果如下：\n\n![image](images/fe9c61ae-8f3a-4e13-9285-1a7c84927fa1.png)\n\n上面命令语句的各部分参数解释：\n\n* `ps`：命令名字\n* `-e`：选择所有进程\n* `-o`：自定义输出格式\n* `–sort=-%cpu`：基于 CPU 使用率对输出结果排序\n* `head`：显示结果的前 10 行\n* `PID`：进程的 ID\n* `PPID`：父进程的 ID\n* `%MEM`：进程使用的 RAM 比例\n* `%CPU`：进程占用的 CPU 比例\n* `Command`：进程名字\n\n## 2，通过 top 命令定位占用 cpu 高的进程\n* 查看 `cpu` 占用最高进程（查看前3位）：`top`，然后按下 `M`（大写 `M`）。\n* 查看内存占用最高进程：`top`，然后按下 `P`（大写 `P` ）。\n* 可视化查看所有用户所有进程使用情况：`ps axf`。\n\n在所有监控 `Linux` 系统性能的工具中，`Linux` 的 `top` 命令是最好的也是最知名的一个（`htop` 是其升级版）。`top` 命令提供了 `Linux` 系统运行中的进程的动态实时视图。它能显示系统的概览信息和 `Linux` 内核当前管理的进程列表。它显示了大量的系统信息，如 `CPU` 使用、内存使用、交换内存、运行的进程数、目前系统开机时间、系统负载、缓冲区大小、缓存大小、进程 `PID` 等等。默认情况下，`top` 命令的输出结果按 `CPU` 占用进行排序，每 `5` 秒中更新一次结果。\n\n```bash\nps -ef                 # 查看所有进程\ntop                    # 实时显示进程状态\n```\n`Linux` 系统下执行 `top` 命令得到以下结果（第一列为进程的 `PID`，第二列为进程所属用户）：\n\n![image](images/60434a88-d703-43b8-88c5-ea61dfc4f793.png)\n\n**上图各个参数的意义：**\n\n* `PID`：进程的ID\n* `USER`：进程所有者\n* `PR`：进程的优先级别，越小越优先被执行\n* `NInice`：值\n* `VIRT`：进程占用的虚拟内存\n* `RES`：进程占用的物理内存\n* `SHR`：进程使用的共享内存\n* `S`：进程的状态。S表示休眠，R表示正在运行，Z表示僵死状态，N表示该进程优先值为负数\n* `%CPU`：进程占用CPU的使用率\n* `%MEM`：进程使用的物理内存和总内存的百分比\n* `TIME+`：该进程启动后占用的总的CPU时间，即占用CPU使用时间的累加值。\n* `COMMAND`：进程启动命令名称\n\n通过上图可以看出排在一行的进程 `PID` 2438占用 `cpu` 过高，定位到了进程 `id`。如果只想观察 进程`PID` 2438的 `CPU`和内存以及负载情况，可以使用以下命令：\n\n```bash\ntop -p 2438\n```\n结果如下：\n\n![image](images/74dede33-9491-4a06-8bee-aa789daed0df.png)\n\n还可以通过 `top` 命令定位问题进程中每个`线程`占用 `cpu` 情况，如查看进程 `PID` 2438 的**每一个线程**占用 cpu 的情况，使用如下命令：\n\n```bash\ntop -p 2438 -H\n```\n结果如下（单线程，所以只显示一行）：\n\n![image](images/0f1d4c73-962e-4f86-ac84-83d629539f00.png)\n\n## 3，htop 系统监控与进程管理软件\n与 `top` 只提供最消耗资源的进程列表不同，`htop` 提供所有进程的列表，并且使用彩色标识出处理器、`swap` 和内存状态。可以通过 `htop` 查看单个进程的线程，然后按 `<F2>` 来进入 `htop` 的设置菜单。选择“设置”栏下面的“显示选项”，然后开启“树状视图”和“显示自定义线程名”选项。按 `<F10>` 退出设置。\n\n![image](images/f1d7359b-92b4-4074-84d4-0a381aaf4d1b.png)\n\n![image](images/22ce3092-92be-4333-ac84-38f7ab7f2730.png)\n\n## 4，参考资料\n[线上linux系统故障排查之一：CPU使用率过高](https://www.jianshu.com/p/6d573e42310a)"
  },
  {
    "path": "1-computer_basics/README.md",
    "content": "## 前言\n\n本目录内容旨在分享常用编程开发效率工具、操作系统、`Linux` 系统等方向知识总结和笔记。\n\n## 效率工具\n\n- [git命令学习笔记](./效率工具/git常用命令总结.md)\n- [ubuntu16.04安装mmdetection库](./效率工具/ubuntu16.04安装mmdetection库.md)\n- [Docker基础和常用命令](./效率工具/Docker基础和常用命令.md)\n\n## 计算机系统\n\n* [深入理解计算机系统-第1章计算机系统漫游笔记](操作系统/深入理解计算机系统-第1章计算机系统漫游笔记.md)\n* [深入理解计算机系统-第2章信息的表示和处理](操作系统/深入理解计算机系统-第2章信息的表示和处理.md)\n* [深入理解计算机系统-第3章程序的机器级表示](操作系统/深入理解计算机系统-第3章程序的机器级表示.md)\n  \n## Linux 系统\n\n### Linux 基础\n\n* [Linux 基础-学会使用命令帮助](Linux系统/Linux基础-学会使用命令帮助.md)\n* [Linux 基础-新手必备命令](Linux系统/Linux基础-新手必备命令.md)\n* [Linux 基础-文件权限与属性](Linux系统/Linux基础-文件权限与属性.md)\n* [Linux 基础-文件及目录管理](Linux系统/Linux基础-文件及目录管理.md)\n* [Linux 基础-文本处理命令](Linux系统/Linux基础-文本处理命令.md)\n* [Linux 基础-查看cpu、内存和环境等信息](Linux系统/Linux基础-查看cpu、内存和环境等信息.md)\n* [Linux 基础-查看进程命令ps和top](Linux系统/Linux基础-查看进程命令ps和top.md)\n* [Linux 基础-查看和设置环境变量](Linux系统/Linux基础-查看和设置环境变量.md)\n\n### Linux 进阶\n\n- [ ] 正则表达式与文件格式处理\n- [ ] shell scripts 学习\n\n## 参考资料\n\n- 《鸟哥私房菜-基础篇》\n- 《深入理解计算机系统第三版》\n- [Docker-从入门到实践](https://yeasy.gitbook.io/docker_practice/ \"Docker-从入门到实践\")\n- [Docker教程](https://haicoder.net/docker/docker-course.html \"Docker教程\")"
  },
  {
    "path": "1-computer_basics/操作系统/深入理解计算机系统-第1章计算机系统漫游笔记.md",
    "content": "- [1，信息就是位+上下文](#1信息就是位上下文)\n- [2，程序被其他程序翻译成不同格式](#2程序被其他程序翻译成不同格式)\n- [3，了解编译器如何工作是有大有益处的](#3了解编译器如何工作是有大有益处的)\n- [4，处理器读取存在内存中的指令](#4处理器读取存在内存中的指令)\n- [5，高速缓冲至关重要](#5高速缓冲至关重要)\n- [6，存储设备形成层次结构](#6存储设备形成层次结构)\n- [7，操作系统管理硬件](#7操作系统管理硬件)\n  - [7.1，进程](#71进程)\n  - [7.2，线程](#72线程)\n  - [7.3，虚拟内存](#73虚拟内存)\n  - [7.4，文件](#74文件)\n- [8，系统之间利用网络通信](#8系统之间利用网络通信)\n- [9，重要主题](#9重要主题)\n  - [9.1，Amdahl 定律](#91amdahl-定律)\n  - [9.2，并发与并行](#92并发与并行)\n  - [9.4，计算机系统中抽象的重要性](#94计算机系统中抽象的重要性)\n- [参考资料](#参考资料)\n\n## 1，信息就是位+上下文\n* 计算机系统是由硬件和系统软件组成，它们共同工作来运行应用程序。\n* C 语言是系统级编程的首选，同时它也非常实用于应用级程序的编写。\n\n## 2，程序被其他程序翻译成不同格式\n以`hello` 程序为例来解释程序的生命周期，`hello.c` 文件代码如下:\n\n```cpp\n#include <stdio.h>\n\nint main()\n{\n    printf(\"hello world\\n\");\n    return 0;\n}\n```\n\n为了在系统上运行 `hello.c` 程序，文件中的每条 C 语言都必须被其他程序转化为一系列的**低级机器语言指令**。然后这些指令按照一种称为**可执行目标程序**的格式打包好，并以二进制磁盘文件的形式保存。目标程序也称为**可执行目标文件**。\n\n在 Linux 系统中，`GCC` 编译器将源程序文件 hello.c 翻译（编译）为目标文件 hello 的过程分为四个阶段，如下图所示。执行这四个阶段（预处理器、编译器、汇编器和链接器）的程序一起构成了编译器系统（compilation system）。\n\n![image](images/4q85EDFxENMJTUnhcQydhZR73dgCwmC-YCm8qgzRTiw.png)\n\n## 3，了解编译器如何工作是有大有益处的\n1. **优化程序性能**。了解一些机器代码（汇编代码）以及编译器将不同的 C/C++语句转化为机器代码的方式。比如一个函数调用的开销有多大？while 循环比 for 循环更有效吗？指针引用比数组索引更有效吗等？\n2. **理解链接时出现的错误**。比如静态库和动态库的区别，静态变量和全局变量的区别等。\n3. **避免安全漏洞**。\n\n## 4，处理器读取存在内存中的指令\n典型系统的硬件组织构成: 总线、`I/O` 设备、主存（动态随机存取存取器 `DRAM`）、处理器。\n\n> 主存是临时存储设备，在处理器执行程序时，用来存放程序和程序处理的数据。从逻辑上说，存储器是一个线性的字节数组，每个字节都有其唯一的地址（数组索引），这些地址是从零开始的。\n\n将 hello 程序输出的字符串从存储器写到显示器的过程如下图所示。\n\n![image](images/jNMMhByUJX6mgnQXa06ieKCNof94Uh4cExDepVFSI3Q.png)\n\n## 5，高速缓冲至关重要\n针对处理器与主存之间读取数据的差异，处理器系统设计者采用了更小更快的存储设备，称为高速缓冲器（cache memory，简称 cache 或高速缓冲），作为暂时的集结区域，存放处理器近期可能会需要的信息。\n\n![image](images/ImONa4Ylt2sGRON7xzuR4GPLsH0eNnPp2DdynYWZJwY.png)\n\n## 6，存储设备形成层次结构\n在处理器和一个较大较慢的设备（例如主存）之间插入一个更小更快的存储设备（例如高速缓冲）的想法已经成为一个普遍的观念。实际上，每个计算机系统中的存储设备都被组织成了一个**存储器层次结构**，如图1-9所示。在这个层次结构中，从上至下，设备的访问速度越来越慢、容量越来越大，并且每字节的成本也越来越低。\n\n![image](images/5kUNCag6pjUpDjf8l64j0yLLtWY8y06CCLQoVlVs7s0.png)\n\n## 7，操作系统管理硬件\n所有应用程序对硬件的操作尝试都必须通过操作系统，操作系统有两个基本功能：\n\n1. 防止硬件被失控的应用程序滥用；\n2. 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。\n\n操作系统通过几个基本的抽象概念（**进程、虚拟内存和文件**）来实现这两个功能。文件是对 I/O 设备的抽象表示，虚拟内存是对主存和磁盘 I/O 设备的抽象表示，进程则是对处理器、主存和 I/O 设备的抽象表示。\n\n### 7.1，进程\n**进程**是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程，每个进程都好像在独占地使用硬件。而**并发运行**，则是说一个进程的指令和另一个进程的指令是交错之行的。在大多数系统中，需要运行的进程数是可以多于它们的 `CPU` 个数的。无论在单核还是多核系统中，一个 `CPU` 看上去都像是在**并发**地执行多个进程，这实际是通过处理器在进程间切换来实现的，操作系统实现这种交错执行的机制成为**上下文切换**。\n\n操作系统会保持跟踪进程运行所需的所有状态信息，这种状态信息称为上下文，其包括多种信息，比如 `PC` 和寄存器文件的当前值，以及主存的内容。在任何一个时刻，但处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时，就会进行**上下文切换，**即保存当前进程的上下文、恢复进程的上下文，然后将控制权传递到新进程。图1-12展示了示例 hello 程序运行场景的基本理念。\n\n![image](images/PMxiwceVcvQ8THubjichY3PkaA4WqwpGK4yh-jL24n8.png)\n\n### 7.2，线程\n尽管我们直观认为一个进程只有单一的控制流，但在现代计算机系统中，一个进程实际上可以由多个称为**线程**的执行单元组成，每个线程都运行在进程的上下文中，并共享同样的代码和全局数据。由于网络服务器对并行处理的需求，线程成为越来越重要的编程模型，因为多线程之间比多进程之间更容易共享数据且更高效。\n\n### 7.3，虚拟内存\n**虚拟内存**是一个抽象概念，它为每个进程提供了一个假象，即每个进程都在独占地使用主存，每个进程看到的内存都是一致的，称为**虚拟地址空间**。图 1-13 所示的是 Linux 进程的虚拟地址空间。在 Linux 中，地址空间最上面的区域是保留给操作系统中代码和数据的，这对所有进程来说都是一样。地址空间的底部区域存放用户进程定义的代码和数据。注意，图中的地址从下往上增大的。\n\n![image](images/CNLtOP_LRbWvOvz83RuknMO2cFOCFDQX76sHKRMIerA.png)\n\n每个进程看到的虚拟地址空间由大量准确定义的区构成，每个区都有专门的功能，进程的虚拟地址空间意义如下。\n\n* **程序代码和数据**。对所有进程来说，代码是从同一固定地址开始，进接着的是和全局变量和相对应的数据位置，代码和数据区是直接按照可执行目标文件的内容初始化的。\n* **堆**。代码和数据区后紧随着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小，与此不同，当调用 `malloc` 和 `free` 这样的 C 标准库函数时，堆可以在运行时动态地拓展和收缩。\n* **共享库**。地址空间的中间部分是一块用来存放像 C 标准库和数学库这样的共享库的代码和数据的区域。\n* **栈**。位于用户虚拟地址空间顶部的是**用户栈**，编译器用它来实现函数调用。和堆类似，用户栈在程序执行期间可以动态地拓展和收缩，调用函数栈则增长，从一个函数返回，栈则收缩。\n* **内核虚拟内存**。地址空间顶部的区域是为操作系统内核保留的。\n\n虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互，包括对处理器生成的每个地址的硬件翻译，基本思想是把一个进程虚拟内存的内容存在在磁盘上，然后用主存作为磁盘的高速缓冲。\n\n### 7.4，文件\n文件就是字节序列，每个 I/O 设备包括磁盘、键盘、显示器，甚至网络都可以看成文件，系统中所有输入输出都是通过使用一小组称为 Unix I/O 的系统函数调用读写文件来实现的。\n\n## 8，系统之间利用网络通信\n现代系统通过网络将单个计算机系统连接在一起。\n\n![image](images/TpNXGmzm8PkOUDCgf5Zyyac0QuLMa-O1tL4DQ0TuE-g.png)\n\n## 9，重要主题\n### 9.1，Amdahl 定律\n该定律的主要思想就是，当我们对系统的某个部分加速时，其对系统整体性能的影响取决于该部分的重要性和加速程度。\n\n![image](images/t18gr2bkMba5q2g5z9rRrjWcjBARJ_la_zU8Z2VX06M.png)\n\n### 9.2，并发与并行\n我们用术语并发（`concurrency`）是一个通用的概念，指一个同时具有多个活动的系统；而术语并行（`parallelism`）指的是用并发来是一个系统运行得更快。并行可以在计算机系统的多个抽象层次上运用，这里按照系统层次结构中由高到低的顺序描述三个层次。\n\n**1，线程级并发**\n\n超线程，有时称为**同时多线程**（simultaneous multi-threading），是一项允许一个 `CPU` 执行多个控制流的技术。它涉及 CPU 某些硬件有多个备份，比如程序计数器和寄存器文件，而其他硬件部分只有一份，比如执行浮点算术运算的单元。常规的处理器需要大约 20000 个时钟周期做不同线程间的转换，而超线程的处理器可以在单个周期的基础上决定要执行哪一个线程。\n\n多处理器的使用可以从两方面提高系统性能，首先，它减少了在执行多个任务时模拟并发的需要；其次，它可以使应用程序运行得更快，前提是程序以多线程方式编写。\n\n**2，指令级并行**\n\n在较低的抽象层次上，现代处理器可以同时执行多条指令的属性称为**指令级并行。**如果处理器可以达到比一个周期一条指令更快的执行速率，就称为**超标量**（super-scalar）处理器，大多数现代处理器都支持超标量操作。\n\n**3，单指令、多数据并行**\n\n在最低层次中，许多现代处理器拥有特殊的硬件，允许一条指令产生多个可以并行执行的操作，这种方式称为**单指令、多数据**，即 `SIMD` 并行。\n\n### 9.4，计算机系统中抽象的重要性\n前面我们介绍了计算机系统使用的几个抽象，如图 1-18 所示，在处理器里，指令集架构提供了对实际处理器的抽象。\n\n![image](images/mUJJs9almEPtVReVTMkTpLWDnrPPkiAGN5lcJ7ok94k.png)\n\n操作系统中有四个抽象：**文件是对 I/O 设备的抽象，虚拟内存时对程序存储器的抽象，进程是对一个正在运行的程序的抽象，虚拟机提供对整个计算机的抽象，包括操作系统、处理器和程序**。\n## 参考资料\n* 《深入理解操作系统第三版-第1章》"
  },
  {
    "path": "1-computer_basics/操作系统/深入理解计算机系统-第2章信息的表示和处理笔记.md",
    "content": "- [1，信息存储](#1信息存储)\n  - [1.1，十六进制表示法](#11十六进制表示法)\n  - [1.2，字数据大小](#12字数据大小)\n  - [1.3，寻址和字节顺序](#13寻址和字节顺序)\n- [2，整数表示](#2整数表示)\n- [3，整数运算](#3整数运算)\n- [4，浮点数](#4浮点数)\n  - [4.1，二进制小数](#41二进制小数)\n  - [4.2，IEEE 浮点表示](#42ieee-浮点表示)\n  - [4.3，浮点数的规格化](#43浮点数的规格化)\n  - [4.4，数字示例](#44数字示例)\n  - [4.5，浮点数的数值范围](#45浮点数的数值范围)\n- [参考资料](#参考资料)\n\n> 关于程序的结构和执行，我们需要考虑机器指令如何操作整数和实数数据，以及编译器如何将 C 程序翻译成这样的指令。\n\n现代计算机存储和处理信息是用二进制（二值信号）表示的，因为二值信号更容易表示、存储和传输，如导线上的高电压和低电压。\n\n## 1，信息存储\n大多数计算机使用字节（`byte`），作为最小的可寻址的内存单位，而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组，称为**虚拟内存**（`virtual memory`），内存中的每个字节都由一个唯一的数字来表示，称为它的地址（`address`），所有可能地址的集合称为**虚拟地址空间**（`virtual address space`）。简而言之，这个虚拟地址空间只是一个展现给机器级程序的概念性映像，实际的实现是将动态随机访问存储器（`DRAM`）、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来，为程序提供一个看上去统一的字节数组。\n\n### 1.1，十六进制表示法\n一个字节由 8 位组成，在二进制表示法中，它的值域是 $00000000_2\\sim 11111111_{2}$，如果看成十进制整数，它的值域则是 $0_{10}\\sim 255_{10}$。在 C 语言中，以 0x 或 0X 开头的数字常量被认为是十六进制的值。\n\n![image](images/du15XqDyxT2wKlQmZyDKTIbP51mCrm3AHOUVdkJWhkc.png)\n\n### 1.2，字数据大小\n每台计算机都有一个字长（`word size`），说明指针数据的标称大小（`nominal size`），字长决定的最重要的系统参数就是虚拟地址空间的最大大小，即对于一个字长$w$位的机器而言，虚拟地址的范围为 $0\\sim 2^{w}-1$，程序最多访问 $2^{w}$个字节。\n\n### 1.3，寻址和字节顺序\n**对于跨越多个字节的程序对象，我们必须建立两个规则: 这个对象的地址是什么，以及内存中如何排列这些字节**。对象的存储有两个通用的规则:\n\n* **小端法**（little endian）: 在内存中选择按照从变量最低有效字节（更小数值的那部分）到最高有效字节的顺序存储对象，即最低有效字节在最前面的方式。\n* **大端法**（big endian）: 在内存中选择按照从最高有效字节到最低有效字节的顺序存储对象，即最高有效字节在最前面的方式。\n\n如下图所示，对于变量 $x$，假设其存储地址为 0x100，其十六进制值为 0x1234567，如果存储方式是小端法，那么内存中的存储顺序为 67、45、23、01（对应内存地址是0x100, 0x101, 0x102, 0x103），如果存储方式是大端法，那么内存中的存储顺序为 01、23、45、67。\n\n![image](images/ymZPakWiwkFaKtCt9yb5GwYbw2cl2kt66aKr1pSVoQ8.png)\n\n## 2，整数表示\n\n略\n\n## 3，整数运算\n\n略\n\n## 4，浮点数\n\nIEEE 754 标准用来表示计算机系统中的浮点数定义，即浮点数规则都遵从 IEEE 754 标准。\n\n### 4.1，二进制小数\n理解浮点数的第一步是考虑含有**小数值**的二进制数字，其表示方法如下:\n$$b = \\sum_{i=-n}^{m} 2^{i} \\times b_{i}$$\n\n符号 . 现在变成了二进制的点，**点左边的位的权是 2 的正幂，点右边的位的权是 2 的负幂**。小数的二进制幂表示如下图所示。\n\n![image](images/t7jsbVVmMRGIsiXX6IzRg339QPyKOXbA0aQBfXzG5Q8.png)\n\n### 4.2，IEEE 浮点表示\n定点表示法不能有效地表示非常大的数字。IEEE 标准 754，浮点表示用 $V = (-1)^s \\times M \\times 2^E$表示一个数:\n\n![image](images/0OjCdS_0w64dJv3wcJrcn2xHhZq6sc3xNLVqOR2BAIA.png)\n\n在 IEEE 754 标准中浮点数由三部分组成：**符号位（sign bit），有偏指数（biased exponent），小数（fraction）**。浮点数分为两种，单精度浮点数（single precision）和双精度浮点数（double precision），它们两个所占的位数不同。\n\n* 在单精度浮点格式（C 语言的 `float`）中，符号位，`8` 位指数，`23` 位有效数。\n* 在双精度浮点格式（C 语言的 `double`）中，符号位，`11` 位指数，`52` 位有效数。\n\n![image](images/iWjyenH48I7GpLDjQAqc2yVQhT-7F5H2W8YBbErI86c.png)\n\n给定位表示，根据 `exp` 的值，被编码的浮点数可以分成三种不同的情况（最后一种有两个变种）。图2-33说明了对单精度格式的情况。\n\n![image](images/899dlOzwfvOmBpzyX5VUJ2CGNJp-t115-mGKJkNwqDg.png)\n\n**规格化的值产生的指数的取值范围，对于单精度是 -126～127，而对于双精度是 -1022\\~1023**。\n\n![image](images/KtBlfW18psEh0gDwf8E-uncsC0rDzh0Q9N-QDvp8cc8.png)\n\n### 4.3，浮点数的规格化\n\n若不对浮点数的表示作出明确规定，**同一个浮点数的表示就不是唯一的**。例如 $(1.75)_{10}$可以表示成 $1.11\\times 2^0$，$0.111\\times 2^1$，$0.0111\\times 2^2$等多种形式。当尾数不为 0 时，**尾数域的最高有效位为 1**，这称为浮点数的规格化。否则，以修改阶码同时左右移动小数点位置的办法，使其成为规格化数的形式。\n\n**1，单精度浮点数真值**\n\n> **对于浮点数的规格化的值，其指数的偏差 Bias 是其可能值的一半**: $2^{k-1}-1$(单精度是127，双精度是 1023)。 也就是说，用存储的指数减去此偏差就得到了实际指数。 如果存储的指数小于此偏差，则实际为负指数。\n\nIEEE754 标准中，一个规格化的 32 位浮点数 $x$ 的真值表示为：\n\n$$x = (-1)^{S}\\times (1.M)\\times 2^{e}, e = E-127, e\\in [-126, 127]$$\n\n其中尾数域值是 1.M。因为规格化的浮点数的尾数域最左位总是 1，所以这一位不予存储，默认其隐藏在小数点的左边。在计算指数 e 时，对阶码 E 的计算采用原码的计算方式，**因此 32 位浮点数的 8 bits 的阶码 E 的取值范围是 0 到 255**。其中当 E 为全 0 或者全 1 时，是 IEEE754 规定的特殊情况。去除 0 和 255 这两种特殊情况，那么指数 $e$ 的取值范围就是 $1-127=-126$ 到 $254-127=127$。\n\n因为 $2^{127}$ 大约等于 $10^{38}$，所以单精度的实际极限为 $10^{38}$ ($e^{38}$)。\n\n**2，双精度浮点数真值**\n\n64 位的浮点数中符号为 1 位，阶码域为 11 位，尾数域为 52 位，**指数偏移值是 1023（指数偏差）**。因此规格化的 64 位浮点数 x 的真值是：\n\n$$x = (-1)^{S}\\times (1.M)\\times 2^{e}, e = E-1023, e\\in [-1022,1023]$$\n\n因为 $2^{1023}$ 大约等于 $10^{308}$，所以单精度的实际极限为 $10^{308}$（$e^{308}$）。\n### 4.4，数字示例\n图 2-34 展示了一组数值，它们可以用假定的 6 位格式来表示，有 $k=3$的阶码位和 $n=2$的尾数位，偏置位是 $2^{3-1}-1 = 3$。\n\n![image](images/F8ISuUEs_e2MjSGeLoI0MMJ_MkF1-l97H9dFGHPp2Js.png)\n\n![image](images/M8pGEmsRv5XkQscrjour5-1xZnOag3Ar9Jav9wMTdW0.png)\n\n### 4.5，浮点数的数值范围\n\n下图 2-36 展示了单精度和双精度的浮点数取值范围。\n\n![image](images/gFzfKRqy4vYGCPctO0JWKh4W0c-PxkEz0aJUYsg0gws.png)\n## 参考资料\n*  《深入理解操作系统第三版-第2章》\n* [IEEE 浮点表示形式](https://learn.microsoft.com/zh-cn/cpp/build/ieee-floating-point-representation?view=msvc-170)\n* [IEEE Standard 754 Floating Point Numbers](https://steve.hollasch.net/cgindex/coding/ieeefloat.html)\n\n"
  },
  {
    "path": "1-computer_basics/操作系统/深入理解计算机系统-第3章程序的机器级表示笔记.md",
    "content": "\n- [前言](#前言)\n- [1，历史观点](#1历史观点)\n- [2，程序编码](#2程序编码)\n- [3，数据格式](#3数据格式)\n- [4，访问信息指令](#4访问信息指令)\n  - [4.1，操作数指示符](#41操作数指示符)\n  - [4.2，数据传送指令](#42数据传送指令)\n  - [4.3，数据传送示例](#43数据传送示例)\n  - [4.4，压入和弹出数据](#44压入和弹出数据)\n- [5，算术和逻辑操作指令](#5算术和逻辑操作指令)\n  - [5.1，加载有效地址](#51加载有效地址)\n  - [5.2，一元和二元操作](#52一元和二元操作)\n  - [5.3，移位操作](#53移位操作)\n  - [5.4，总结](#54总结)\n- [6，控制指令](#6控制指令)\n  - [6.1，条件码](#61条件码)\n  - [6.2，访问条件码](#62访问条件码)\n  - [6.3，跳转指令](#63跳转指令)\n  - [6.4，跳转指令的编码](#64跳转指令的编码)\n  - [6.5，用条件控制来实现条件分支](#65用条件控制来实现条件分支)\n  - [6.6，用条件传送来实现条件分支](#66用条件传送来实现条件分支)\n  - [6.7，循环](#67循环)\n- [7，过程](#7过程)\n- [8，数组的分配和访问](#8数组的分配和访问)\n  - [8.1，基本原则](#81基本原则)\n  - [8.2，指针运算](#82指针运算)\n  - [8.3，嵌套到数组（多维数组）](#83嵌套到数组多维数组)\n- [9，异数的数据结构](#9异数的数据结构)\n- [10，在机器级程序中将控制与数据结合起来](#10在机器级程序中将控制与数据结合起来)\n- [11，浮点代码](#11浮点代码)\n- [参考资料](#参考资料)\n\n## 前言\n\n计算机执行机器代码，用字节序列编码低级的操作，包括处理数据、管理内存、读写存储设备上的数据，以及利用网络通信。编译器基于编程语言的规则、目标机器的指令集和操作系统遵循的惯例，经过一系列阶段生成机器代码。\n\n在本章中，我们将详细学习一种特别的汇编语言，了解如何将 C 程序编译成这种形式的机器代码。阅读编译器产生的汇编代码，需要具备的技能不同于手工编写汇编代码，我们必须了解典型的编译器在将 C 程序结构变换成机器代码时所做的转换。相对于 C 代码表示的计算操作，优化编译器能够重新排列执行顺序，消除不必要的计算，用快速操作替换慢速操作，甚至将递归计算变换成迭代计算。但是源代码与对应的汇编代码的对应关系通常不太容易理解，因为这是一种逆向工程（reverse engineering）-通过研究系统和逆向工作。\n\n本章内容会涉及到 x86-64 汇编级指令代码。\n\n## 1，历史观点\n`Intel` 处理器系列俗称 `x86`，经历了一个长期的、不断进化的发展过程。\n\n## 2，程序编码\n`Linux` 系统默认的编译器时 `GCC C` 编译器。编译器选项 `-Og` 会指示编译器使用会生成符合原始 `C` 代码整体结构的机器代码的优化等级，通常使用 `-O1` 或 `-O2` 选项。\n\n`x86-64` 的机器代码和原始的 C 代码差别非常大，一些通常对 C 语言程序员隐藏的处理器状态都是可见的:\n\n* **程序计数器**（PC，在 x86-64 中用 `%rip` 表示）给出将要执行的下一条指令在内存中的地址。\n* **整数寄存器文件**包含 16 个命名的位置，分别存储 64 位的值。这些寄存器可以存储地址（对应于C 语言的指针）或整数数据。有的寄存器被用来记录默写重要的程序状态，有的寄存器保存临时数据，如过程的参数和局部变量，以及函数的返回值。\n* **条件码寄存器**保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化，如用来实现 if 和 else 语句。\n* 一组向量寄存器可以存放一个或多个整数或浮点数值。\n\n## 3，数据格式\n`C` 语言数据类型在 `x86-64` 中的大小。\n\n![image](images/QSQYzFHHj7WPK5uGgWAQdgAaelr3AbiRG3hxWMKHrJs.png)\n\n浮点数主要有两种形式：单精度（4 字节）值，对应于 C 语言数据类型 float，双精度（8 字节）值，对应于 C 语言数据类型 double。如上图所示，大多数 GCC 生成的汇编代码指令都有一个字符的后缀，表明操作数的大小，例如，数据传送指令有四个变种: `movb`（传送字节）、`movw`（传送字）、`movl`（传送双字）和`movq`（传送四字）。\n\n## 4，访问信息指令\n一个 `x86-64` 的中央处理单元（`CPU`）包含一组 `16` 个存储 `64` 位值的**通用目的寄存器，**这些寄存器用来**存储整数数据和指针**。下图显示了这 `16` 个寄存器，它们的名字都以 `%r` 开头，后面还跟着一些不同的命名规则的名字。最初的 8086 中有 8 个 16 位的寄存器，即图 3-2 中的 %ax 到 %bp，每个寄存器都有特殊的用途，它们的名字就反映了这些不同的用途。拓展到 IA32 架构时，这些寄存器也拓展成 32 位寄存器，标号从 %eax 到 %ebp。**拓展到 x86-64 后，用来的 8 个寄存器拓展成 64 位，标号从 ****%rax**** 到 ****%rbp****，除此之外，还增加了 8 个寄存器，标号从 ****%r8**** 到 ****%r15****。**\n\n![image](images/qnsVUQQ249XWv70KXYUBmTdLYPGd6iQub56NJ-d9fUM.png)\n\n### 4.1，操作数指示符\n大多数指令有一个或多个操作数（operand），指示出执行一个操作中要使用的源数据值，以及防止结果的目的位置。不同操作数被分为三种类型。\n\n* 立即数（`immediate`），用来表示常数值。\n* 寄存器（`register`）\n* 内存引用: 它会根据计算出来的地址访问某个内存位置。\n\n如图 3-3 所示，有多重不同的**寻址模式**，允许不同形式的内存引用。\n\n![image](images/wJpUN8D2GNjAICfQftGbUvXOux5B-KNrmz4KQi-chhE.png)\n\n### 4.2，数据传送指令\n最频繁使用的指令是**将数据从一个位置复制到另一个位置**的指令。图 3-4 列出的最简单形式的数据传送指令-MOV 类: **把数据从源位置复制到目的位置，不做任何变化**。\n\n![image](images/yPe9VIy0F1qNvzMcBmQILriZUcc4c-fz4GrYm0hjiQE.png)\n\n简单的数据传送指令示例代码如下。（记住，第一个是源操作数，第二个是目的操作数）\n\n![image](images/WRpsNl_ap3Nm68Gug5LN4nByXA4yQ5hZRRuzKXGVipo.png)\n\n### 4.3，数据传送示例\n![image](images/awQ9HQwF_e_cXZkesyt85CAVtwPbALZTncgEobBiO4E.png)\n\n### 4.4，压入和弹出数据\n栈和队列都是一种\"操作受限\"的线性表（逻辑结构），只允许在一端插入和删除数据；**栈的特性是先进后出，队列是先进先出**。**栈**在处理函数调用过程中很重要，通过 push 指令把数据压入栈中，通过 pop 指令删除数据。\n\n**栈可以可以通过数组实现**，总是从数组的一端插入和删除元素，这一端称为**栈顶，栈顶元素的地址是所有栈中元素地址中最低的，栈指针 ****%rsp**** 保存着站定元素的地址。**入栈和出栈汇编指令描述如下，栈操作指令都只有一个操作数-压入的数据源和弹出的栈顶数据。\n\n![image](images/--PPoOxo2HA8iiV2CMHdheiPHqZcVdjfTNthDnsP7fA.png)\n\n将一个四字值压入栈中，分为两步，**首先先将栈指针减 8，然后将值写到新的栈顶地址**，因此 `pushq` 指令等价于下面两条指令:\n\n```bash\nsubq $8,%rsp Decrement stack pointer\nmoq %rbp,(%rsp)\n```\n![image](images/Zlw0SByXxHNCHycSo-Eg66b7nLQ1LJfjUWdkvIdnu7o.png)\n\n## 5，算术和逻辑操作指令\n图 3-10 列出了 x86-64 的一些整数和逻辑操作。和访问信息指令一样，算术和逻辑操作指令类也有各种带不同大小操作数的变种。例如，指令类 ADD 由四条加法指令组成: addb、addw、addl 和 addq，分别是字节加法、字加法、双字加法和四字加法。**算术和逻辑操作指令分为四组：加载有效地址、一元操作、二元操作和移位。二元操作数有两个操作数，而一元操作数有一个操作数。**\n\n![image](images/AkNj9Ck9ql43MdAiTXagNpHEmRBcRBdVLaEjNou_okU.png)\n\n### 5.1，加载有效地址\n加载有效地址（load effective address）指令 leaq 实际上是 movq 指令的变形。leaq 指令可以简洁地描述普通的算术操作，如果寄存器 %rdx 的值为 x，那么指令 `leaq 7(%rdx, %rdx, 4),%rax` 将设置寄存器 `%rax` 的值为 `5x+7`。目的操作数必须是一个寄存器。\n\n![image](images/mSOUx9Uu2iF_pkpzqSBv2B9xc_WRW5O57imc650fmyA.png)\n\n### 5.2，一元和二元操作\n对于二元操作指令，第一个操作数可以是立即数、寄存器或内存位置，第二个操作可以是寄存器或内存位置。\n\n### 5.3，移位操作\n移位操作指令，先给出移位量，第二项给出要移位的数，移位量可以是立即数，或者在单字节寄存器 `%cl` 中，移位量是由 `%c1` 寄存器的低 `m` 位决定的。例如当寄存器 `%c1` 的十六进制位 0xFF 时，指令 `salb` 会移 7 位，`salw` 会移 15 位，`sall` 会移 31 位，`salq` 会移 63 位。\n\n### 5.4，总结\n![image](images/OqeuQi8kL1BP46nwSbcKdJ0mqtEvna5Wi-55r5F-q1Y.png)\n\n## 6，控制指令\n前面的两类指令都是**直线代码**行为，也就是指令一条接着一条顺序地执行。C 语言中的某些结构，比如条件语句、循环语句和分支语句，要求有条件的执行，根据数据测试的结果来决定操作执行的顺序。\n\n### 6.1，条件码\n除了整数寄存器，CPU 还维护着一组单个位的条件码（condition code）寄存器，它们描述了最近的算术或逻辑操作的属性，可以**通过检测条件码寄存器来执行条件分支指令**。最常用的条件码有：\n\n* `CF`： 进位标志。最近的操作使最高位产生了进位，可用来检查无符号操作的溢出。\n* `ZF`: 零标志。最近的操作得出的结果为 0。\n* `SF`: 符号标志位。最近的操作得到的结果为负数。\n* `OF`: 溢出标志。最近的操作导致一个补码已出-正溢出或负溢出。\n\n除了图 3-10 的整数算术操作指令会设置条件码，还有两类指令 `CMP` 和 `TEST` ，**但它们只设置条件码而不改变任何其他寄存器。**如下图 3-13 所示，`CMP` 指令根据两个操作数之差来设置条件码。\n\n![image](images/K6PjfUTdwF9IUo79rJ-29LTz7siBHlx7hpfBPPisRbY.png)\n\n### 6.2，访问条件码\n条件码通常不会直接读取，常使用方法有三种：\n\n1. 可以根据条件码的某种组合，将一个字节设置为 0 或者1。\n2. 可以有条件跳转到程序的某个其他的部分。\n3. 可以有条件地传送数据。\n\n![image](images/yy0TyW1D5tbPw6VbHTwCtjhoRKzWeqvBA3nD9dneR6A.png)\n\n### 6.3，跳转指令\n**跳转**（jump）指令会导致执行切换到程序中一个全新的位置，示例代码如下。\n\n![image](images/33y4SmPfYDRqPwjBUedJJXuuxhwT3Me9eJ15zEHO1LQ.png)\n\n图 `3-15` 列举了不同的跳转指令，`jmp` 指令是无条件跳转，可以是直接跳转，即跳转目标是作为指令的一部分编码的；也可以是间接跳转，即跳转目标是从寄存器或内存位置中读出的。汇编语言中，直接跳转是会给出一个标号作为跳转目标的，例如上面示例代码中的标号“.L1\"；间接跳转的写法是 \"*\"后面跟一个操作数指示符 jmp \\**%rax，用寄存器 %rax 中的值作为跳转目标。\n\n![image](images/Mo5N1s1-WvO3ejtcXUw5ofMQTYhldNXr4IoIweyERuE.png)\n\n### 6.4，跳转指令的编码\n略\n\n### 6.5，用条件控制来实现条件分支\n![image](images/0aML0CYNT05z5GVXBUco2zBDD62MoNzr3Jj_FDly8fg.png)\n\n### 6.6，用条件传送来实现条件分支\n实现条件操作的传统方法是通过使用控制的条件转移。当条件满足时，程序沿着一条路径执行，而当条件不满足时，就走另一条路径。这种机制虽然简单通用，但是在现代处理器上，它可能会非常低效。\n\n为了理解为什么教育条件数据传送的代码会比基于条件控制转移的代码性能要好，我们必须了解一些关于现代处理器如何运行的知识。处理器是通过流水线（pipelining）来获得高性能，在流水线中，一条指令的处理要经过一系列的阶段，每个阶段执行所需操作的一小部分（例如，从内存取指令、确定指令类型、从内存读数据、执行算术运算、向内存写数据，已经更新程序计数器）。这种方法通过重叠连续指令的步骤获得高性能，例如在取一条指令的同事，执行它前面一条指令的算术运算。要做到这一点，要求事先确定要执行的指令序列，这样才能保持流水线中充满了待执行的指令。\n\n![image](images/lqkdYI5dn7Z3w_AmOnNU_gZDM6HjOYVQRzK_g3ae8EQ.png)\n\n![image](images/wRKt5-V8DGMNjVgX5p_xz8TRlqncKRkVZkiYK6IUsXs.png)\n\n![image](images/FzYI4mQOCdRMTOobIqSzNX4DG6cUhxD50092K-v1sNU.png)\n\n同条件跳转不同，使用条件传送指令，处理器无需预测测试的结果就可以执行条件传送。处理器只是读源值（可能从内存中），检查条件码，然后要么更新目的寄存器，要么保持不变。条件传送指令类如图 3-18 所示。\n\n![image](images/FRLfh6XYdUC9IMcIg0eqAmWP1HEZgVHtCVg5n_wyKO0.png)\n\n![image](images/z8jUtZpnGYEQ9Yr9KYe9tdhg4bOdqhNci4cD2onpecc.png)\n\n### 6.7，循环\n`C` 语言提供了多种循环结构，如 `do-while`、`while` 和 `for`。汇编中没有直接的循环指令存在，但可以用条件测试和跳转指令组合起来实现循环的效果。GCC 和其他汇编器产生的循环代码主要基于两种基本的循环模式：`do-while` 循环和 `while` 循环。这里以给出一个函数实现 $n!$，其 C 代码、goto 代码和汇编代码如下。\n\n![image](images/QVeSGeljjdm5GXeZgEAnborO2yK8uLFSNQxpXWkm4NY.png)\n\n```bash\nlong fact_do(long n)\nn in %rdi\nfact_do:\n    mov1 $1, %eax Set result = 1\n.L2:\n    imulq %rdi, %rax Compute result *= n\n    subq $1, %rdi Decrement n\n    cmpq $1, %rdi Copare n:1\n    jg .L2 if >, goto loop\n    rep; ret Return\n```\n## 7，过程\n过程是软件中一种很重要的抽象，不同编程语言中，过程的形式多样：函数（function）、方法（method）、子例程（subroutine）、处理函数（handler）等等，但是它们都有一些共同的特性。\n\n**编程语言过程调用机制的一个关键特性在于使用了栈数据结构提供的先进后出的内存管理原则。**\n\n![image](images/BYrY1-ffeSExzLuOP-qeIIVMoclFpCROwtIhv6NCY-s.png)\n\n## 8，数组的分配和访问\n> 数据结构-数组的定义和使用参考这篇文章[常见数据结构-数组](https://blog.csdn.net/qq_20986663/article/details/127252593?spm=1001.2014.3001.5501)。\n\n### 8.1，基本原则\n`x86-64` 的内存引用指令可以用来简化数组访问。例如，假设 E 是一个 int 型的数组，而我们想计算 $E[i]$，在此，E 的地址存放在寄存器 %rdx 中，而 i 存放在寄存器 `%rcx` 中，指令 `movl (%rdx, %rcx, 4), %eax` 会执行地址计算 $x_{E} + 4i$,读取这个内存位置的值，并将结果存放到寄存器 `%eax` 中。\n\n### 8.2，指针运算\n单操作数操作符 & 和 \\* 可以产生指针和间接引用指针。\n\n![image](images/2VWtPSaNVKroazokJ2j_HoJ4cCBOz3Xz3Yddlv0nh_U.png)\n\n### 8.3，嵌套到数组（多维数组）\n要访问多维数组元素，编译器会以**数组起始为基地址，（可能需要经过伸缩的）偏移量为索引，产生计算期望的元素的偏移量，然后使用某种 MOV 指令**。对于一个声明如下的二维数组: \n\n```cpp\nT D[R][C]; // T 是数据类型\n```\n它的数组元素 `D[i][j]` 的内存地址为 $\\&D[i][j] = x_{D} + L(C\\cdot i + j)$。\n\n这里，$L$是数据类型 $T$以字节为单位的大小。以 $5\\times 3$的整形数组 A 为例，假设数组起始地址、行列索引 $x_A$、$i$和$j$分别在寄存器 %rdi、%rsi 和 %rdx 中，然后可以用下面的代码将数组元素 A\\[i\\]\\[j\\] 复制到寄存器 %eax 中：\n\n```bash\nA in %rdi, i in %rsi, and j in %rdx\nleaq (%rsi, %rsi, 2), %rax     Compute 3i\nleaq (rdi, %rax, 4), %rax      Compute x_A + 12i\nmovq (rax, %rdx, 4), %eax      Read from M[x_A + 12i + 4j]\n```\n上面这段代码计算元素的地址为 $x_A + 12i + 4j = x_A + 4(3i + j)$，使用了 x86-64 地址运算的伸缩和加法特性。\n\n## 9，异数的数据结构\n略\n\n## 10，在机器级程序中将控制与数据结合起来\n略\n\n## 11，浮点代码\n略\n\n## 参考资料\n《深入理解操作系统第三版-第3章》\n\n"
  },
  {
    "path": "1-computer_basics/操作系统/计算机基础知识.md",
    "content": "\n## 知识点目录\n\n+ 操作系统\n+ 计算机网络\n+ 面向对象\n+ 数据库\n+ Linux系统开发\n+ 常用工具（Cmake/Git/Docker/正则表达式）\n\n## 操作系统\n\n### 操作系统中堆和栈的区别\n\n+ 操作系统中堆和栈都是指内存空间，不同的是堆为按需申请、动态分配，例如 `C++` 中的 `new` 操作（当然 C++ 的 new 不仅仅是申请内存这么简单）。**堆**可以简单理解为当前使用的空闲内存，其申请和释放需要程序员自己写代码管理。\n+ 操作系统中的栈是程序运行时自动拥有的一小块内存，大小在编译时由编译器参数决定，是用于局部变量的存放或者函数调用栈的保存。在 `C` 中声明一个局部变量（如 `int a`）会存放在栈中，当其离开作用域后，所占用的内存则会被释放，栈用于保存函数调用栈时和数据结构的栈是有关系的。在函数调用过程中，常常会多层甚至递归调用。每一个函数调用都有各自的局部变量值和返回值，每一次函数调用其实是先将当前函数的状态压栈，然后在栈顶开辟新空间用于保存新的函数状态，接下来才是函数执行。当函数执行完毕之后，栈先进后出的特性使得后调用的函数先返回，这样可以保证返回值的有序传递，也保证函数现场可以按顺序恢复。操作系统的栈在内存中高地址向低地址增长，也即低地址为栈顶，高地址为栈底。这就导致了栈的空间有限制，一旦局部变量申请过多（例如开个超大数组），或者函数调用太深（例如递归太多次），那么就会导致栈溢出（Stack Overflow），操作系统这时候就会直接把你的程序杀掉。\n\n> 参考[知乎问答-什么是堆？什么是栈？他们之间有什么区别和联系？](https://www.zhihu.com/question/19729973/answer/390903646)\n\n### Linux查看cpu和cache信息\n\n1，`Linux` 查看 `cpu` 信息**命令**：`cat /proc/cpuinfo`。\n\n```shell\n(base) pc:$ cat /proc/cpuinfo\nprocessor\t: 0\nvendor_id\t: GenuineIntel\ncpu family\t: 6\nmodel\t\t: 158\nmodel name\t: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz\nstepping\t: 10\nmicrocode\t: 0xb4\ncpu MHz\t\t: 3192.005\ncache size\t: 12288 KB\nphysical id\t: 0\nsiblings\t: 1\ncore id\t\t: 0\ncpu cores\t: 1\napicid\t\t: 0\ninitial apicid\t: 0\nfpu\t\t: yes\nfpu_exception\t: yes\ncpuid level\t: 22\nwp\t\t: yes\nflags\t\t: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid rdseed adx smap clflushopt xsaveopt xsavec xsaves arat md_clear flush_l1d arch_capabilities\nbugs\t\t: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit srbds\nbogomips\t: 6384.01\nclflush size\t: 64\ncache_alignment\t: 64\naddress sizes\t: 43 bits physical, 48 bits virtual\npower management:\n...\nprocessor\t: 3\n...\n```\n\n2，`Linux` 查询 `L1/L2/L3 cache`大小：`cat /sys/devices/system/cpu/cpu0/cache/index*/size`(`*`为 `0/1/2/3`)\n\n```shell\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index0/size\n32K\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index0/type\nData\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index1/type\nInstruction\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index1/size\n32K\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index2/size\n256K\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index3/size\n12288K\n```\n\n现代 `CPU`的 `L1 cache` 是逻辑核私有的，L1 cache分指令L1 cache和数据L1 cache，大小相等都为 `32 KB`；目前，L2 cache也是片内私有，所以每个核只有`256 KB`；而对于L3 cache，一个物理核CPU的所有逻辑核共享，所以在每个逻辑核来看，L3 cache都为`12288 KB`。本机的虚拟机总共有 `1` 个物理核，而本机共有 `6` 核 `12` 线程，所以可推算得到**本机的 `Cache` 信息：\n\n+ L1 cache：\n  + L1 Data: `192 KB = 32 x 6 KB`\n  + L1 Instruction: `192 KB = 32 x 6 KB`\n+ L2 cache：`1536 KB = 256 X 6 KB`\n+ L3 cache：`12288 KB`\n\n### Windows查看cpu和cache信息\n\n任务管理器--->性能，即可查看 `cpu` 和 `L1/L2/L3 Cache` 大小，如下图所示：\n\n![windows查看cpu信息](../../data//images/cpu信息和Cache大小.png)\n\n或者下载安装 [`cpuz`](https://www.cpuid.com/softwares/cpu-z.html) 软件，打开即可查看，如下图所示。\n\n![windows-zpuz查看cpu信息](../../data//images/cpuz-查询cpu信息.png)\n\n### 并发与并行区别\n\n+ **并发**是指宏观上在一段时间内能同时运行多个程序，而**并行**则指在同一时刻能运行多个指令。\n+ 并行需要硬件支持，如多流水线、多核处理器或者分布式计算系统。\n+ 操作系统通过引入进程和线程，使得程序能够并发运行。\n\n## 数据库\n\n### 什么是事务\n\n事务是指满足 `ACID` 特性的一组操作，可以通过 `Commit` 提交一个事务，也可以用 `Rollback` 进行回滚。\n\n### 并发一致性问题\n\n+ 丢失修改\n+ 读脏数据\n+ 不可重复读\n+ 脏影读\n\n## 多进程与多线程区别\n\n+ **进程是资源分配的最小单位，线程是CPU调度（独立调度）的最小单位**。\n+ 多个进程之间相互独立，一个任务就是一个进程，进程内的“子任务”称为线程；线程是进程的一部分，一个进程至少有一个线程。\n+ 同一个进程中的所有线程的数据是共享的（进程通讯），线程之间可以直接通信；但是进程之间的数据是独立的，进程之间的交流需要借助中间代理（｀IPC`）来实现。\n+ 由于创建或撤销进程时，系统要为之分配资源或回收资源，如内存空间、I/O设备等，所以进程间调用、通讯和切换开销均比多线程大，单个线程的崩溃会导致整个应用的退出。\n+ 存在大量IO，网络耗时或者需要和用户交互等操作时，使用多线程（线程开销小、切换速度快）有利于提高系统的并发性和用户界面快速响应从而提高友好性。\n\n## 进程/线程通信\n\n### 进程间通信\n\n进程间通信（IPC, InterProcess Communication）：是指在不同进程之间传播或交换信息。`IPC` 的方式有：管道、消息队列、信号量、共享存储、Socket等。`Socket`　支持不同主机的两个进程的　`IPC`。\n> python Process类参数：`target`表示调用的对象，就是子进程要执行的任务\n\n`Python` 多线程通信可以通过导入`Process`（创建进程）　和`Queue`(创建队列)：`from multiprocessing import Process, Queue`，具体通信过程如下：\n\n1. 父进程创建 `Queue`,并传递给各个子进程：`q = Queue()` `pw = Process(target=write, args=(q,))`\n2. 分别启动写入数据和读数据子进程：`pw.start()` `pr.start()`\n3. 等待进程技术：`pw.join()`\n\n### 线程通信\n\n一个进程所含的不同线程是共享内存的，所以线程之间共享数据有一个问题是多个线程共享一个变量，导致内容改乱了。解决办法是，如果不同线程间有共享的变量，其中一个方法就是在修改前给其上一把锁`lock`，确保一次只有一个线程能修改。`Python` 创建线程方式如下：\n\n```python\nt = threading.Thread(target=loop, name='LoopThread')  # `target`表示调用的对象(自己定义的任务函数)\n```\n\n`Python` 创建锁可以使用，`threading.lock()` 方法，实现对一个共享变量的锁定，修改完后 `release` 供其它线程使用，具体加锁 `python` 代码如下：\n\n```python\nbalance = 0\nlock = threading.Lock()  # 创建锁\n\ndef run_thread(n):\n    for i in range(100000):\n        # 先要获取锁:\n        lock.acquire()\n        try:\n            # 放心地改吧:\n            change_it(n)\n        finally:\n            # 改完了一定要释放锁:\n            lock.release()\n```\n\n## 并发与并行\n\n> [多进程与多线程的区别](https://www.cnblogs.com/kaituorensheng/p/3603057.html)\n\n并行指物理上同时执行，并发指能够让多个任务在逻辑上交织执行的程序设计。\n\n要让单核　`CPU` 运行多任务，就得用到**并发技术**，实现并发技术相当复杂，最容易理解的是“时间片轮转进程调度算法”，它的思想简单介绍如下：在操作系统的管理下，所有正在运行的进程轮流使用CPU，每个进程允许占用CPU的时间非常短(比如10毫秒)，这样用户根本感觉不出来CPU是在轮流为多个进程服务，就好象所有的进程都在不间断地运行一样。但实际上在任何一个时间内有且仅有一个进程占有CPU。如果一台计算机有多个CPU，情况就不同了，如果进程数小于CPU数，则不同的进程可以分配给不同的CPU来运行，这样，多个进程就是真正同时运行的，这便是并行。但如果进程数大于CPU数，则仍然需要使用并发技术。因此，我们可以得出以下结论：\n\n+ 总线程数<= CPU数量：并行运行\n+ 总线程数 >  CPU数量：并发运行"
  },
  {
    "path": "1-computer_basics/效率工具/Docker基础和常用命令.md",
    "content": "- [一，Docker 简介](#一docker-简介)\n  - [1.1，什么是 Docker](#11什么是-docker)\n  - [1.2，Docker 与虚拟机的区别](#12docker-与虚拟机的区别)\n  - [1.3，Docker 架构](#13docker-架构)\n  - [1.4，为什么用 Docker](#14为什么用-docker)\n- [二，Docker 基本概念](#二docker-基本概念)\n  - [2.1，镜像](#21镜像)\n  - [2.2，容器](#22容器)\n  - [2.3，仓库](#23仓库)\n- [三，Docker 使用](#三docker-使用)\n  - [3.1，Docker 服务](#31docker-服务)\n  - [3.2，下载与使用Docker公共镜像(Images)](#32下载与使用docker公共镜像images)\n- [四，Docker 镜像命令](#四docker-镜像命令)\n- [五，Docker 容器命令](#五docker-容器命令)\n  - [5.1，docker run 命令](#51docker-run-命令)\n  - [5.2 查看、停止、启动和删除容器](#52-查看停止启动和删除容器)\n- [六，参考资料](#六参考资料)\n\n## 一，Docker 简介\n### 1.1，什么是 Docker\n`Docker` 使用 Google 公司推出的 Go 语言 进行开发实现，基于 Linux 内核的 cgroup，namespace，以及 OverlayFS 类的 Union FS 等技术，对进程进行封装隔离，属于**操作系统层面的虚拟化技术**。由于隔离的进程独立于宿主和其它的隔离的进程，因此也称其为容器。**Docker容器**与虚拟机类似，但二者在原理上不同。容器是将[操作系统层虚拟化](https://zh.m.wikipedia.org/wiki/%E4%BD%9C%E6%A5%AD%E7%B3%BB%E7%B5%B1%E5%B1%A4%E8%99%9B%E6%93%AC%E5%8C%96%20%22%E4%BD%9C%E6%A5%AD%E7%B3%BB%E7%B5%B1%E5%B1%A4%E8%99%9B%E6%93%AC%E5%8C%96%22 \"操作系统层虚拟化\")，虚拟机则是虚拟化硬件，因此容器更具有便携性、能更高效地利用服务器。\n\n专业名词 `Docker` 有两个意思：\n\n* 代指整个 Docker 项目。\n* 代指 Docker 引擎。\n\n`Docker` 引擎(Docker Engine)是指一个服务端-客户端结构的应用，主要有这些部分：Docker 守护进程、Docker Engine API（页面存档备份，存于互联网档案馆）、Docker 客户端。\n\n### 1.2，Docker 与虚拟机的区别\n* 传统虚拟机技术是虚拟出一套硬件后，在其上运行一个完整操作系统，在该系统上再运行所需应用进程。\n* Docker 容器内的应用进程直接运行于宿主的内核，容器内没有自己的内核，而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。\n\n|**特性**|**Docker**|**虚拟机**|\n| :-----: | :-----: | :-----: |\n|启动|秒级|分钟级|\n|硬盘使用|一般为 MB|一般为 GB|\n|性能|接近原生|弱于|\n|系统支持量|单机支持上千个容器|一般几十个|\n\n### 1.3，Docker 架构\n![image](../../data/images/docker/L-xDvVjAV1tWJ6Gk6kr-hDXF-AwaxhIXYZEF2FDqzW0.png)\n\nrunc 是一个 Linux 命令行工具，用于根据 OCI 容器运行时规范 创建和运行容器。\n\ncontainerd 是一个守护程序，它管理容器生命周期，提供了在一个节点上执行容器和管理镜像的最小功能集。\n\n### 1.4，为什么用 Docker\nDocker 作为一种**新的虚拟化技术**，跟传统的虚拟化技术相比具有众多的优势：\n\n1. **更高效的利用系统资源**：不需要进行硬件虚拟以及运行完整操作系统等额外开销，Docker 对系统资源的利用率更高。\n2. **更快速的启动时间**：Docker 容器应用直接运行于宿主内核，不需要启动完整的操作系统，所以启动时间可做到秒级的启动时间。\n3. **一致的运行环境**：Docker 镜像提供了除内核外完整的运行时环境，确保开发环境、测试环境、生产环境的一致性。\n4. **持续交付和部署**：开发人员可以通过 Dockerfile 来进行镜像构建，并结合持续集成(Continuous Integration) 系统进行集成测试，而运维人员则可以直接在生产环境中快速部署该镜像，甚至结合持续部署(Continuous Delivery/Deployment) 系统进行自动部署。\n5. **更轻松的迁移**：Docker 可以在很多平台上运行，无论是物理机、虚拟机、公有云、私有云，甚至是笔记本，其运行结果是一致的。\n6. **更轻松的维护和扩展。**\n\n## 二，Docker 基本概念\n\n**Docker 三个基本概念：**\n\n* 镜像（Image）\n* 容器（Container）\n* 仓库（Repository）\n\n### 2.1，镜像\n**操作系统分为内核和用户空间**。对于 Linux 而言，内核启动后，会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像（Image），就相当于是一个 root 文件系统。比如官方镜像 ubuntu:18.04 就包含了完整的一套 Ubuntu 18.04 最小系统的 root 文件系统。\n\n**Docker 镜像** 是一个特殊的文件系统，除了提供容器运行时所需的程序、库、资源、配置等文件外，还包含了一些为运行时准备的一些配置参数（如匿名卷、环境变量、用户等）。镜像 **不包含** 任何动态数据，其内容在构建之后也不会被改变。\n\n**Docker** 镜像并非是像一个 `ISO` 那样的打包文件，镜像只是一个虚拟的概念，其实际体现并非由一个文件组成，而是由一组文件系统组成，或者说，由多层文件系统联合组成。**其被设计为分层存储的架构，镜像构建时，会一层层构建，前一层是后一层的基础**。每一层构建完就不会再发生改变，后一层上的任何改变只发生在自己这一层。分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层，然后进一步添加新的层，以定制自己所需的内容，构建新的镜像。\n\n### 2.2，容器\n\n**容器（Container）：一个运行时实例，它基于镜像（Image）创建，并提供隔离的运行环境**。\n\n镜像（`Image`）和容器（`Container`）的关系，类似面向对象程序设计中的**类和实例**的关系。可以把 Docker容器(Container) 看做是一个简易版的 Linux 环境（包括 root 用户权限、进程空间、用户空间和网络空间等）和运行在其中的应用程序。它可以被启动、开始、停止、 删除。\n\n容器的实质是进程，但与直接在宿主执行的进程不同，容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间，甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里，使用起来，就好像是在一个独立于宿主的系统下操作一样。\n\n容器和镜像一样都是使用分层存储，每一个容器运行时，是以镜像为基础层，在其上创建一个当前容器的存储层，我们可以称这个为容器运行时读写而准备的存储层为**容器存储层**。\n\n### 2.3，仓库\n镜像构建完成后，可以很容器的在**当前宿主主机**上运行，但是如果需要在其他服务器上使用这个镜像，我们就需要一个集中的存储、分发镜像的服务，即**仓库**（Repository）-集中存放镜像的地方。\n\n`Docker` 仓库(`Registry`) 分为公开仓库（`Public`）和私有仓库（`Private`）两种形式。目前 `Docker` 官方维护了一个公共仓库 [Docker Hub](https://hub.docker.com/ \"Docker Hub\")，其中已经包括了数量超过 2,650,000 的镜像。大部分需求都可以通过在 `Docker Hub` 中直接下载镜像来实现。\n\n有时候使用 `Docker Hub` 这样的公共仓库可能不方便，用户可以创建一个**本地仓库**供私人使用。[Docker Registry](https://yeasy.gitbook.io/docker_practice/repository/registry \"Docker Registry\") 是官方提供的工具，可以用于构建私有的镜像仓库。\n\n一个 `Docker Registry` 中可以包含多个**仓库**（`Repository`）；每个仓库可以包含多个**标签**（`Tag`）；**每个标签对应一个镜像**。\n\n通常，一个仓库会包含同一个软件不同版本的镜像，而标签就常用于对应该软件的各个版本。我们可以通过 `<仓库名>:<标签>` 的格式来指定具体是这个软件哪个版本的镜像。如：`ubuntu: 14.04`、`ubuntu: 16.04` 等等。\n\n```shell\n$ docker image ls ubuntu\nREPOSITORY          TAG                 IMAGE ID            CREATED             SIZE\nubuntu              18.04               329ed837d508        3 days ago          63.3MB\nubuntu              bionic              329ed837d508        3 days ago          63.3MB\n```\n\n## 三，Docker 使用\n### 3.1，Docker 服务\n安装 Docker 这里不做介绍。以下是 Linux 系统下，一些 docker 使用命令：\n\n1，**查看 Docker 服务状态**：使用 `systemctl status docker` 命令查看 Docker 服务的状态。其中 Active: active (running) 即表示 Docker 服务为正在运行状态。\n\n![image](../../data/images/docker/ExVYsbQl5Uatb6Rk4h_PevAXETxUgb60O9fP1kYrsY0.png)\n\n2，**停止 Docker 服务**：使用 `systemctl stop docker` 命令。\n\n3，**启动 Docker 服务**：使用 `systemctl start docker` 命令。\n\n4，**重启 Docker 服务**：使用 `systemctl restart docker` 命令。\n\n5，**测试 Docker 是否安装正确**。\n\n```bash\n$ docker run --rm hello-world\nUnable to find image 'hello-world:latest' locally\nlatest: Pulling from library/hello-world\n7050e35b49f5: Pull complete\nDigest: sha256:13e367d31ae85359f42d637adf6da428f76d75dc9afeb3c21faea0d976f5c651\nStatus: Downloaded newer image for hello-world:latest\n \nHello from Docker!\nThis message shows that your installation appears to be working correctly.\n \nTo generate this message, Docker took the following steps:\n 1. The Docker client contacted the Docker daemon.\n 2. The Docker daemon pulled the \"hello-world\" image from the Docker Hub.\n    (arm64v8)\n 3. The Docker daemon created a new container from that image which runs the\n    executable that produces the output you are currently reading.\n 4. The Docker daemon streamed that output to the Docker client, which sent it\n    to your terminal.\n \nTo try something more ambitious, you can run an Ubuntu container with:\n $ docker run -it ubuntu bash\n \nShare images, automate workflows, and more with a free Docker ID:\n https://hub.docker.com/\n \nFor more examples and ideas, visit:\n https://docs.docker.com/get-started/\n```\n### 3.2，下载与使用Docker公共镜像(Images)\n> `macos` 系统环境下操作示例，`ubuntu` 系统可能略有不同。\n\n1，使用 **docker search** 命令从 Docker Repo 搜索 Dokcer 可用的镜像。示例命令：`docker search ubuntu18.04`。\n\n![image](../../data/images/docker/VDaqCzSPqTxG2F5pY6Z3l7nWOUm1mcsufhAug8gNadA.png)\n\n2，使用 **docker image pull** 命令从 Docker Repo 获取指定的 Dokcer镜像(Images)。示例命令: `docker image pull docker.io/hello-world`。拉取名为 docker.io/hello-world 的镜像。\n\n![image](../../data/images/docker/OtA6evJeq4h1M5R-t_8pCiV41eII-K9lIsoBCa4qbBo.png)\n\n3，使用 **docker image ls** 命令查看**本地**的 Dokcer 镜像(Images)。\n\n4，使用 **docker run** 命令运行 Dokcer 镜像(Images)。示例命令：`docker run hello-world`。\n\n![image](../../data/images/docker/Q2A9wZz1qY_yUxPzB7z6nVmcgAEz1T56qZt785MCIP0.png)\n\n5，使用 **docker info** 命令，查看当前 docker容器 的所有的信息。\n\n![image](../../data/images/docker/-rnbIRGlVItC7zPytjVm_D-ReUsB5Jf9cP9S-e2atNA.png)\n\n6，使用 **docker version** 查看容器的版本信息。\n\n```bash\n$ docker --version # 这个命令查看 docker 版本更简单\nDocker version 19.03.13, build 4484c46d9d\n```\n![image](../../data/images/docker/GDJbbeZyUtoedXnjm5MNMOIKgdgoa6cqcnSo-FT7fP8.png)\n\n## 四，Docker 镜像命令\nDocker 镜像(Images) 也可以理解为是一个用于创建 Docker容器(Container) 的静态模板。一个 Docker 镜像(Images) 可以创建很多 Docker容器(Container)。\n\n**Docker 镜像常用命令如下：**\n\n|命令|描述|\n| ----- | ----- |\n|**docker commit**|创建镜像。|\n|**docker images**|查看镜像信息。|\n|**docker load**|导入镜像。|\n|**docker pull**|拉取 Docker 镜像。|\n|**docker push**|上传镜像。|\n|**docker rmi**|删除镜像。|\n|**docker save**|导出镜像。|\n|**docker search**|在 Docker Hub 搜索镜像。|\n|**docker tag**|为镜像打标签。|\n\n## 五，Docker 容器命令\n### 5.1，docker run 命令\n通过 **docker run** 命令可以基于镜像新建一个容器并启动，语法如下：\n\n`docker run [OPTIONS] IMAGE [COMMAND] [ARG...]` \n\n**其他常用容器管理命令如下：**\n\n```bash\n# 新建容器并启动\n$ docker run [镜像名/镜像ID]\n# 启动已终止容器\n$ docker start [容器ID]\n# 列出本机运行的容器\n$ docker ps\n# 停止运行的容器\n$ docker stop [容器ID]\n# 杀死容器进程\n$ docker kill [容器ID]\n# 重启容器\n$ docker restart [容器ID]\n```\n**docker run 命令语法**\n\n1, `docker run` 命令常用选项：可通过 docker run --help 命令查看全部内容。\n\n|**选项**|**说明**|\n| :-----: | :-----: |\n|\\-d, --detach=false|指定容器运行于前台还是后台，默认为 false。|\n|\\-i, --interactive=false|打开 STDIN，用于控制台交互。|\n|\\-t, --tty=false|分配 tty 设备，该可以支持终端登录，默认为 false。|\n|\\-u, --user=\"\"|指定容器的用户。|\n|\\-a, --attach=\\[\\]|登录容器（必须是以 docker run -d 启动的容器）。|\n|\\-w, --workdir=\"\"|指定容器的工作目录。|\n|\\-c, --cpu-shares=0|设置容器 CPU 权重，在 CPU 共享场景使用。|\n|\\-e, --env=\\[\\]|指定环境变量，容器中可以使用该环境变量。|\n|\\-m, --memory=\"\"|指定容器的内存上限。|\n|\\-P, --publish-all=false|指定容器暴露的端口。|\n|\\-p, --publish=\\[\\]|指定容器暴露的端口。|\n|\\-h, --hostname=\"\"|指定容器的主机名。|\n|\\-v, --volume=\\[\\]|给容器挂载存储卷，挂载到容器的某个目录。|\n|–volumes-from=\\[\\]|给容器挂载其他容器上的卷，挂载到容器的某个目录。|\n|–cap-add=\\[\\]|添加权限。|\n|–cap-drop=\\[\\]|删除权限。|\n|–cidfile=\"\"|运行容器后，在指定文件中写入容器 PID 值，一种典型的监控系统用法。|\n|–cpuset=\"\"|设置容器可以使用哪些 CPU，此参数可以用来容器独占 CPU。|\n|–device=\\[\\]|添加主机设备给容器，相当于设备直通。|\n|–dns=\\[\\]|指定容器的 dns 服务器。|\n|–dns-search=\\[\\]|指定容器的 dns 搜索域名，写入到容器的 /etc/resolv.conf 文件。|\n|–entrypoint=\"\"|覆盖 image 的入口点。|\n|–env-file=\\[\\]|指定环境变量文件，文件格式为每行一个环境变量。|\n|–expose=\\[\\]|指定容器暴露的端口，即修改镜像的暴露端口。|\n|–link=\\[\\]|指定容器间的关联，使用其他容器的 IP、env 等信息。|\n|–lxc-conf=\\[\\]|指定容器的配置文件，只有在指定 --exec-driver=lxc 时使用。|\n|–name=\"\"|指定容器名字，后续可以通过名字进行容器管理，links 特性需要使用名字。|\n|–net=“bridge”|器网络设置：<br>1. bridge 使用 docker daemon 指定的网桥。<br>2. host //容器使用主机的网络。<br>3. container:NAME\\_or\\_ID >//使用其他容器的网路，共享 IP 和 PORT 等网络资源。<br>4. none 容器使用自己的网络（类似–net=bridge），但是不进行配置。|\n|–privileged=false|指定容器是否为特权容器，特权容器拥有所有的 capabilities。|\n|–restart=“no”|指定容器停止后的重启策略:<br>1. no：容器退出时不重启。<br>2. on-failure：容器故障退出（返回值非零）时重启。<br>3. always：容器退出时总是重启。|\n|–rm=false|指定容器停止后自动删除容器(不支持以 docker run -d 启动的容器)。|\n|–sig-proxy=true|设置由代理接受并处理信号，但是 SIGCHLD、SIGSTOP 和 SIGKILL 不能被代理。|\n\n2，`Docker` 交互式运行的语法为：`docker run -i -t IMAGE [COMMAND] [ARG]` 。`Docker` 交互式运行，即 `Docker` 启动直接进入 `Docker` 镜像内部。\n\n![image](../../data/images/docker/DN6phxbTI-qJqDunkIdOgz6Gte1a--lUTVsIW5s8HZo.png)\n\n3, **进入容器内部**: 可以使用 `docker exec` 命令在运行中的容器中启动一个交互式 shell：\n\n```bash\ndocker exec -it mynginx /bin/bash\n```\n\n### 5.2 查看、停止、启动和删除容器\n\n```bash\n# 查看正在运行的容器\ndocker ps\n# 查看所有容器（包括停止的）\ndocker ps -a\n# 启动已存在的容器\ndocker start container_name\n# 停止容器\ndocker stop container_name\n# 删除容器（容器必须处于停止状态）\ndocker rm container_name\n```\n\n![image](../../data/images/docker/EJgGulpCOPeBpHrsbzfdNrTuBYAKCaQGoLitWluROl8.png)\n\n## 六，参考资料\n* [Docker-从入门到实践](https://yeasy.gitbook.io/docker_practice/ \"Docker-从入门到实践\")\n* [Docker教程](https://haicoder.net/docker/docker-course.html \"Docker教程\")\n\n"
  },
  {
    "path": "1-computer_basics/效率工具/git工业界实战总结.md",
    "content": "---\nlayout: post\ntitle: git 工业界实战总结\ndate: 2021-09-01 20:00:00\nsummary: 本地仓库由 git 维护的三棵“树\"组成。第一个是 工作目录，它持有实际文件；第二个是暂存区(Index)，它像个缓存区域，临时保存仓库做的改动;最后是 Head，它指向我们的最后一次提交的结果。\ncategories: Linux\n---\n\n## 一 git 入门操作\n\n### 1.1 git 创建代码仓库\n\n第一步：刚下载安装的 `git` 都需要先配置用户名和邮箱：\n\n```bash\ngit config --global user.name \"user_name\"\ngit config --global user.email \"youremail@example.com\"\n```\n\n第二步：要想从 `github` 或者 `gitlab` 上实现 `clone/pull/push` 等操作，首先就得在本地创建 `SSH Key` 文件，在用户主目录下，查看是否有 `.ssh` 目录，看这个目录下是否有 `id_rsa` 和 `id_rsa.pub` 这两个文件，如果没有，则需要打开 shell（windows 系统打开Git Bash），在命令行中输入:\n\n```bash\nssh-keygen -t rsa -C \"youremail@example.com\"\n```\n> SSH 概述： \\*\\*SSH(Secure Shell) \\*\\* 是一种网络协议，用于计算机之间的加密登录。如果一个用户从本地计算机，使用SSH协议登录另一台远程计算机，我们就可以认为，这种登录是安全的，即使被中途截获，密码也不会泄露，原因在于它采用了非对称加密技术(RSA)加密了所有传输的数据。\n\n第三步：登录 `Github`，打开 `\"Account settings”`，“SSH Keys”页面，然后，点“Add SSH Key”，填上任意Title，在Key文本框里粘贴id\\_rsa.pub文件的内容，点“Add Key”，就可以看到已经添加的 `Key` 了。之后你就可以玩转 `Git`了。\n\n> 为什么 GitHub 需要 SSH Key 呢？ \n因为 GitHub 需要识别出你推送的提交确实是你推送的，而不是别人冒充的，而 Git 支持 SSH 协议，所以，GitHub 只要知道了你的公钥，就可以确认只有你自己才能推送。\n\n第四步：上传项目到 github 仓库。配置好用户名和密码后，接下来就是将本地项目代码上传到 `github/gitlab` 仓库了。\n在前面的准备工作完成后，你首先可以在 `gitlab/github` 新建仓库后，这样会得到一个仓库地址，这时候你可以把本地的文件夹上传到这个仓库地址，具体操作步骤命令如下：\n\n```bash\n# 推送现有文件夹到远程仓库地址\ncd existing_folder\ngit init\ngit remote add origin \"你的仓库地址\"\ngit add .\ngit commit -m \"Initial commit\"\ngit push -u origin master\n```\n其他上传方式命令如下图：\n\n![image](../images/git_pratice/fc92417a-079c-4bb9-8d34-0fb3a68eb096.png)\n\n### 1.2 git 基础命令\n\n本地仓库由 git 维护的三棵“树\"组成。\n\n* 第一个是 `工作目录`，它持有实际文件；\n* 第二个是`暂存区(Index)`，它像个缓存区域，临时保存仓库做的改动;\n* 最后是 `Head`，它指向我们的最后一次提交的结果。\n\n对于`分支`来说，在创建仓库的时候，`master` 是”默认的“分支。一般在项目中，要先在其他分支上进行开发，完成后再将它们合并到主分支上 `master`上。一般不建议使用 pull 拉取最新代码，因为 pull 拉取下来后会（配置了 `git config pull.rebase true`）自动和本地分支合并。\n\ngit 基本操作命令如下：\n\n```bash\ngit init       # 创建新的 git 仓库\ngit status  # 查看状态\ngit branch # 查看分支\ngit branch dev  # 创建dev分支\ngit branch -d dev  # 删除 dev 分支\ngit push origin --delete dev # 删除远程分支 【git push origin --参数远程分支名称】\ngit branch -a # 查看远程分支\ngit checkout -b dev # 基于当前分支(master)创建dev分支，并切换到dev分支，dev 分支会关联到 master 分支上\ngit checkout -f test        # 强制切换至 test 分支，丢弃当前分支的修改\ngit checkout master  # 切换到master分支\ngit add filename  # 添加指定文件，把当前文件放入暂存区域\ngit add .  # 表示添加新文件和编辑过的文件不包括删除的文件\ngit add -A  # 表示添加所有内容\ngit commit  # 给暂存区域生成快照并提交\ngit reset -- files # 用来撤销最后一次 git add files，也可以用 git reset 撤销所有暂存区域文件\ngit push origin master  # 推送改动到master分支（前提是已经clone了现有仓库）\ngit remote add origin <server>  # 没有克隆现有仓库，想仓库连接到某个远程服务器\ngit pull  # 更新本地仓库到最新版本（多人合作的项目），以在我们的工作目录中 获取（fetch） 并 合并（merge） 远端的改动\ngit diff <source_branch> <target_branch>  # 查看两个分支差异\ngit diff  # 查看已修改的工作文档但是尚未写入缓冲的改动\ngit rm <file>  # 用于简单的从工作目录中手工删除文件\ngit rm -f <file>  # 删除已经修改过的并且放入暂存区域的文件，必须使用强制删除选项 -f\ngit mv <file>  # 用于移动或重命名一个文件、目录、软链接\ngit log  # 列出历史提交记录\ngit remote -v # 列出所有远程仓库信息, 包括网址\n```\n\n### 1.3 git 操作实例\n\n1，**将其他分支更改的操作提交到主分支**：\n\n```bash\ngit checkout master  # 切换回master分支(当前分支为dev)\ngit merge dev  # 合并（有合并冲突的话得手动更改文件）\n```\n2，**git 如何回退版本**：\n\n```bash\ngit log  # 查看分支提交历史，确认要回退的历史版本\ngit reset --hard  [commit_id]  # 恢复到历史版本\ngit push -f -u origin branch  # 把修改推送到远程仓库 branch 分支\n```\n4，**拉取远程分支到本地**：\n\n```bash\n# 本地已经拉取了仓库代码，想拉取远程某一分支的代码到本地\ngit checkout -b ac_branch origin/ac_branch   # 拉取远程分支到本地(方式一)\ngit fetch origin ac_branch:ac_branch  # 拉取远程分支到本地(方式二)\n```\n5，**查看本地已有分支**\n\n```bash\n# 显示该项目的本地的全部分支，当前分支有 * 号\ngit branch\n```\n6，**查看本地分支和原称分支差异**\n\n![image](../images/git_pratice/17e735cb-eb5d-482a-a364-2be3b3aef3c5.png)\n\n7，**回退版本**\n\n![image](../images/git_pratice/4d420e56-98da-4c25-a9bc-07b4fc7bbad3.png)\n\n## 二 git 工业界实战操作\n\n1, 合并远程 master 分支到本地分支 dev/model_compare\n\n```bash\ngit fetch origin # 拉取最新远程更改\ngit rebase origin/master\ngit rebase --continue\ngit push origin dev/model_compare --force-with-lease\ngit log --oneline --graph\n```\n\n2, 关于多个 commit 注释信息需要的经验。\n\n合并三个 commit, 第一个 commit 必须是 pick，如果想要保留后面最后一个的 commit 信息，则倒数第二个 commit 设为 f, 最后的 commit 改为 s 即可，然后进入 commit 注释修改界面，把第一个 commit 注释信息加 `#` 注释掉即可。\n\n这样就实现了合并三个 commit，但是 commit 信息为最后一个 commit 的信息的目的。\n\n```bash\ngit rebase -i HEAD~3\n```\n\n执行 `rebase` 进入编辑界面, 编辑界面操作详解\n\n```bash\n# Commands:\n# p, pick = use commit\n# r, reword = use commit, but edit the commit message\n# e, edit = use commit, but stop for amending\n# s, squash = use commit, but meld into previous commit\n# f, fixup = like \"squash\", but discard this commit's log message\n# x, exec = run command (the rest of the line) using shell\n# d, drop = remove commit\n```\n\n3, 本地仓库恢复到某个历史状态\n\n```bash\ngit reflog # 显示本地所有对 HEAD （当前分支指针）的操作日志。\ngit reset --hard hash_id # 恢复到指定 hash_id 操作的位置\ngit reset --hard HEAD@{3} # 恢复到前面三步的操作\ngit reset --soft HEAD^ HEAD^ 表示上一个提交。--soft：只移动 HEAD，不 touch 暂存区和工作区\n```\n\n- git log 查看 commit 提交日志信息\n- `HEAD@{0}` 总是指向当前的 HEAD。\n- `HEAD@{1}` 是上一次 HEAD 移动前的位置，依此类推。\n\n4，将远程分支 feature 的指定目录/文件的修改合并到本地分支 develop \n\n```bash\ngit checkout develop # 切换到本地目标分支\ngit fetch origin # 拉取最新远程更改\ngit checkout origin/feature -- src/utils # 把 origin/feature 上 src/utils 目录下的所有文件版本，复制到当前工作区，并标记为已修改 \n\ngit add src/utils # 提交更改\ngit commit -m \"Merge src/utils from origin/feature into develop\"\ngit push origin develop # 推送到远程 \n```\n\n5, git 配置用户名和邮箱\n\n```bash\n\n# 全局配置\ngit config --global user.name \"harleyszhang\"\ngit config --global user.email \"ZHG5200211@outlook.com\"\n\n# 当前仓库配置\ngit config user.name \"harleyszhang\"\ngit config user.email \"ZHG5200211@outlook.com\"\n```\n\n**6， 修改过往 commit 的用户名和邮箱**\n\n```bash\ngit rebase -i HEAD~1 --exec 'git commit --amend --author=\"harleyszhang <zhg5200211@outlook.com>\" --no-edit'\n```\n\n进入 `git rebase` 界面，直接 `:wq` 保存退出。\n\n## 参考资料\n\n3. [Git 教程](https://www.runoob.com/git/git-tutorial.html)\n4. [图解Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)\n5. [git documentation](https://git-scm.com/doc)\n6. [Git 使用简明手册](https://opus.konghy.cn/git-guide/)"
  },
  {
    "path": "1-computer_basics/效率工具/ubuntu16.04安装mmdetection库.md",
    "content": "- [一，更新 pip 和 conda 下载源](#一更新-pip-和-conda-下载源)\n  - [1.1，查看 conda 和 pip 版本](#11查看-conda-和-pip-版本)\n  - [1.2，更新 pip 下载源](#12更新-pip-下载源)\n  - [1.3，更新 conda 下载源](#13更新-conda-下载源)\n- [二，MMDetection 简介](#二mmdetection-简介)\n- [三，MMDetection 安装](#三mmdetection-安装)\n  - [3.1，依赖环境](#31依赖环境)\n  - [3.2，安装过程记录](#32安装过程记录)\n    - [1，安装操作系统+cuda](#1安装操作系统cuda)\n    - [2，安装 Anconda3](#2安装-anconda3)\n    - [3，安装 pytorch-gpu](#3安装-pytorch-gpu)\n    - [4，安装 mmdetection](#4安装-mmdetection)\n    - [5，安装 MMOCR](#5安装-mmocr)\n- [参考资料](#参考资料)\n## 一，更新 pip 和 conda 下载源\n\n默认情况下 `pip` 使用的是国外的镜像，在下载的时候速度非常慢，下面我将介绍如何更新下载源为国内的清华镜像源，其地址为：`https://pypi.tuna.tsinghua.edu.cn/simple`，阿里云镜像的更新方法一样。\n\n### 1.1，查看 conda 和 pip 版本\n```bash\nroot# conda --version\nconda 22.9.0\nroot# pip --version\npip 20.2.4 from /opt/miniconda3/lib/python3.8/site-packages/pip (python 3.8)\n```\n\n如果 `pip` 版本 `<10.0.0`，建议升级 pip 到最新的版本 (>=10.0.0) 以方便后面的更新下载源配置：\n\n```shell\n# 更新 pip 版本命令\npython -m pip install --upgrade pip\n```\n### 1.2，更新 pip 下载源\n\n在下载安装好 `python3+pip` 或 `anconda3` 的基础上，建议更新为清华/阿里镜像源（默认的 `pip` 和 `conda`下载源速度很慢）。\n\n1，`Linux/Mac` 系统，`pip` **全局更新下载源为清华源和和查看下载源地址的命令**如下:\n\n```bash\n# 更新 pip 下载源为清华镜像\npip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple\n# 查看下载源地址\npip3 config list\n```\n\n![update_pip_download_source](./images/update_pip_download_source.png)\n\n2，`Windows` 系统，需要当前在当前用户目录下手动创建配置文件然后修改。\n\n- 资源管理器的地址栏输入 `%appdata%` 后回车，切换到用户路径下的 `appdata` 目录；\n- 进入到 `pip` 文件夹中（没有则手动创建），并创建文件 `pip.ini`，此文件的完整路径为 `%APPDATA%/pip/pip.ini`；\n- 在 `pip.ini` 文件中添加以下内容后，再次使用 `pip`，即会使用新下载源。\n\n```shell\n[global]\ntimeout = 8000\nindex-url = https://pypi.tuna.tsinghua.edu.cn/simple\ntrusted-host = pypi.tuna.tsinghua.edu.cn/simple\n```\n\n`Linux/Mac` 也可通过直接修改配置文件（可能需要 `root` 权限）的方式直接更新下载源，`vim ~/.pip/pip.conf`，修改如下:\n```shell\nglobal.index-url='https://pypi.tuna.tsinghua.edu.cn/simple'\n```\n\n### 1.3，更新 conda 下载源\n\n1，`conda` **更新源的方法**：\n\n各系统都可以通过修改用户目录下的 `.condarc` 文件。`Windows` 用户无法直接创建名为 `.condarc` 的文件，可先执行 `conda config --set show_channel_urls yes` 生成该文件。`Linux/Mac` 系统一般自带 `.condarc` 文件，文件地址为 `~/.condarc`。\n\n2，之后再修改`.condarc` 文件内容如下: \n\n```bash\nchannels:\n  - defaults\nshow_channel_urls: true\ndefault_channels:\n  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main\n  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r\n  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2\ncustom_channels:\n  conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud\n  msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud\n  bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud\n  menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud\n  pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud\n  pytorch-lts: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud\n  simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud\n```\n\n## 二，MMDetection 简介\n`MMDetection` 是一个基于 `PyTorch` 的目标检测开源工具箱。它是 [OpenMMLab](https://openmmlab.com/) 项目的一部分。主分支代码目前支持 `PyTorch 1.5` 以上的版本。主要特性为：\n\n* **模块化设计**\n* **丰富的即插即用的算法和模型**\n* **速度快**\n* **性能高**\n\n> 更多详情请参考 [MMDetection 仓库 README](https://github.com/open-mmlab/mmdetection/blob/master/README_zh-CN.md)。\n\n## 三，MMDetection 安装\n### 3.1，依赖环境\n* 系统：**首选 Linux**，其次 `macOS` 和 `Windows`（理论上支持，实际安装需要踩很多坑）\n* `Python 3.6+`\n* 首选 CUDA 11.3+、其次推荐 CUDA 9.2+\n* 首选 Pytorch 1.9+，其次推荐 PyTorch 1.3+\n* `GCC 5+`\n* [MMCV](https://mmcv.readthedocs.io/en/latest/#installation)\n\n### 3.2，安装过程记录\n#### 1，安装操作系统+cuda\n我是在 `docker` 容器中安装和进行深度学习算法开发的，其操作系统、`cuda`、`gcc` 环境如下：\n\n![image](images/oIxx0rBoZiuqRQc86gpaF5z8otXeCRzQe23Nabds-6E.png)\n\n#### 2，安装 Anconda3\n官网下载 [Anconda3 linux 安装脚本](https://repo.anaconda.com/archive/Anaconda3-2022.10-Linux-x86_64.sh)，并安装 `Anconda3`（很好装一路 `yes` 即可），并使用 `conda` 新建虚拟环境，并激活虚拟环境进入。\n\n```bash\nconda create -n mmlab python=3.8 -y # 创建 mmlab 的虚拟环境，其中python解释器版本为3.8(python3.9版本不行, 没有pytorch_cuda11.0版本)\nconda activate mmlab # 激活虚拟环境进入\n```\n虚拟环境安装成功后的部分过程截图如下所示：\n\n![image](images/vwSKapCUchQN_oQ7_vdLOiZZZKB6nLe0E3wKWLEY4tc.png)\n\n如果你激活虚拟环境出现如下所示错误。\n\n```bash\nCommandNotFoundError: Your shell has not been properly configured to use 'conda activate'.\nTo initialize your shell, run\n\n    $ conda init <SHELL_NAME>\n\nCurrently supported shells are:\n  - bash\n  - fish\n  - tcsh\n  - xonsh\n  - zsh\n  - powershell\n\nSee 'conda init --help' for more information and options.\n\nIMPORTANT: You may need to close and restart your shell after running 'conda init'.\n```\n可通过以下命令重新激活 `conda` 环境，即可解决问题，方法参考自 [stack overflow 问题](https://stackoverflow.com/questions/61915607/commandnotfounderror-your-shell-has-not-been-properly-configured-to-use-conda)。\n\n```bash\nsource ~/anaconda3/etc/profile.d/conda.sh # anaconda3 的安装路径有可能不一样，自行修改\nconda activate mmlab\n```\n#### 3，安装 pytorch-gpu\n首选安装 `pytorch-gpu` 版本，使用**在线安装**命令:\n\n```bash\nconda install pytorch=1.7.1 cudatoolkit=11.0 torchvision=0.8.2 -c pytorch\n```\n> 官网命令的 cuda11.0 的 torchaudio==0.7.2 版本不存在，故去除。\n\n安装过程信息（记得检查 `pytorch` 版本是 `cuda11.0` 的）截图如下：\n\n![image](images/WGJjEZDqVJJZkZNi1MkYFXOaFrKh1UoKu8Cw-gfrl10.png)\n\n安装成功后，进入 `python` 解释器环境，运行以下命令，判断 pytorch-gpu 版本是否安装成功。\n\n```bash\n>>> import torch\n>>> torch.cuda.is_available()\nTrue\n>>> torch.cuda.device_count()\n2\n>>>\n```\n同时可通过以下命令查看 `CUDA` 和 `PyTorch` 的版本\n\n```bash\npython -c 'import torch;print(torch.__version__);print(torch.version.cuda)'\n```\n总的来说，`pytorch` 等各种 `python` 包有**离线和在线**两种方式安装：\n\n* **在线**：`conda/pip` 方法安装，详细命令参考 [pytorch 官网](https://pytorch.org/)，但是这种方式实际测试下来**可能**会有问题，需要自己肉眼检查安装的版本是否匹配。\n* **离线**：浏览器下载安装包，然后通过 `pip` 或者 `conda` 方式离线安装。\n   * `pip` **可通过**[此链接](https://download.pytorch.org/whl/torch_stable.html) 浏览器下载各种 `pytorch` 版本的二进制安装包，**到本地安装**（`pip install *.whl`）。\n   * `conda` 通过[清华源链接](https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/linux-64/)，浏览器下载对应版本压缩包，然后 `conda install --offline pytorch压缩包的全称（后缀都不能忘记）`\n\n> 不通过浏览器下载 `whl` 包，而是 `pip install https://download.pytorch.org/whl/cu110/torch-1.7.1%2Bcu110-cp39-cp39-linux_x86_64.whl` 方式可能会有很多问题，比如网络问题可能会导致安装失败。\n> WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f74f60d6760>: Failed to establish a new connection: Errno 101 Network is unreachable')': /whl/cu110/torch-1.7.1%2Bcu110-cp39-cp39-linux_x86_64.whl\n\n> 或者下载到一半的网络连接时常超过限制。\npip._vendor.urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host='download.pytorch.org', port=443): Read timed out.\n\n#### 4，安装 mmdetection\n> **不建议安装 cpu 版本**，因为很多算子不可用，其次截止到2022-11-3日，macos 系统 cpu 环境的 `mmdet.apis` 是不可用的。\n\n建议使用 [MIM](https://github.com/open-mmlab/mim) 来自动安装 `MMDetection` 及其相关依赖包-`mmcv-full` 。\n\n```bash\npip install openmim # 或者 pip install -U openmim\nmim install mmdet\n```\n![image](images/g7BbKHcsuGPKLEZlVUd0GxL7zhyJzGdZ9L-nmTcJneY.png)\n\n#### 5，安装 MMOCR\n\n`MMOCR` 依赖 `PyTorch`, `MMCV` 和 `MMDetection`。这些依赖环境，我们在前面的步骤中已经安装好了，所以可通过下面命令直接安装 `MMOCR`。\n\n情况 1: 若你需要直接运行 `MMOCR` 或在其基础上进行开发，则通过源码安装：\n```shell\ngit clone https://github.com/open-mmlab/mmocr.git\ncd mmocr\npip3 install -v -e .\n# \"-v\" 会让安装过程产生更详细的输出\n# \"-e\" 会以可编辑的方式安装该代码库，你对该代码库所作的任何更改都会立即生效\n```\n\n情况 2：如果你将 `MMOCR` 作为一个外置依赖库使用，通过 `pip` 安装即可：\n```shell\npip install mmocr\n```\n\n![mmocr_install_success](./images/mmocr_install_success.png)\n## 参考资料\n* `mmdetection` 和 `pytorch` 官网\n* https://download.pytorch.org/whl/torch\\_stable.html"
  },
  {
    "path": "1-computer_basics/效率工具/程序编译工具基础.md",
    "content": "- [Linux 系统下 gcc 编译生成的文件类型](#linux-系统下-gcc-编译生成的文件类型)\n- [cmake/makefile/make 理解](#cmakemakefilemake-理解)\n- [CMake 编译过程](#cmake-编译过程)\n- [CLion 使用笔记](#clion-使用笔记)\n- [Clang 是什么](#clang-是什么)\n- [MinGW 是什么](#mingw-是什么)\n- [GTK 是什么](#gtk-是什么)\n- [GNU 是什么](#gnu-是什么)\n- [GNU 工具链是什么](#gnu-工具链是什么)\n- [GCC 是什么](#gcc-是什么)\n- [gcc 与 g++ 的区别](#gcc-与-g-的区别)\n\n## Linux 系统下 gcc 编译生成的文件类型\n\n- .out 是可执行文件，相当于 win 上的 exe；\n- .o 是编译中间目标文件，相当于 win 上的 .obj；\n- .a 是静态库，多个 .o 练链接得到，用于静态链接；\n- .so 是共享库，用于动态链接，相当于 win 上 .dll；\n\ngcc 编译过程参考文章 [linux下gcc编译生成.out，.o，.a，.so文件](https://www.i4k.xyz/article/u011832525/105228959)。\n\n## cmake/makefile/make 理解\n\n> 参考知乎文章 [5分钟理解make/makefile/cmake/nmake](https://zhuanlan.zhihu.com/p/111110992)。\n\n代码变成可执行文件，叫做编译（compile）；先编译这个，还是先编译那个（即编译的安排），叫做构建（build）。\n\n+ `CMake` 是一个跨平台的、开源的构建工具。cmake 是 `makefile` 的上层工具，它们的目的正是为了产生可移植的 `makefile`，并简化自己动手写 `makefile`时的巨大工作量；\n+ 把 `make` 命令写在文件中，构成一系列构建规则的文件叫做 `Makefile` 文件。`Makefile` 文件有一套自己专门的语法，包括 `echoing`、通配符、模式匹配、变量和赋值符、自动变量等内容。\n+ Linux 中 `make` 是用来编译的，它从 `Makefile中` 读取指令，然后编译。make 的作用是开始进行源代码编译，以及由  Makefile 设置文件提供的一些功能；比如 `make install` 表示进行安装（一般需要有 root 权限），`make uninstall` 是卸载，不加参数就是默认的进行源代码编译。\n\n> `make`工具可以看成是一个智能的批处理工具，它本身并没有编译和链接的功能，而是用类似于批处理的方式—通过调用 `makefile` 文件中用户指定的命令来进行编译和链接的。\n\n## CMake 编译过程\n\nCMake 是针对跨平台编译问题，所设计的工具：它首先允许开发者编写一种平台无关的 `CMakeList.txt` 文件来定制整个编译流程，然后再根据目标用户的平台进一步**自动生成**所需的本地化 `Makefile` 和工程文件，如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。\n\n在 `linux` 平台下使用 `CMake` 生成 `Makefile` 并编译的流程如下：\n1. 编写 `CMake` 配置文件 `CMakeLists.txt`。\n2. 执行命令 `cmake PATH` 或者 `ccmake PATH` 生成 `Makefile` (ccmake 和 cmake 的区别在于前者提供了一个交互式的界面)。其中， `PATH` 是 `CMakeLists.txt` 所在的目录。\n3. 使用 `make` 命令进行编译得到项目可执行文件。`make “-j []”` 指同时运行命令的个数。如果没有这个参数，make运行命令时能运行多少就运行多少。\n\n## CLion 使用笔记\n\n当前 `CLion` 支持五种工具链（来源这篇[博客](https://bbs.huaweicloud.com/blogs/158643)）：\n+ `Visual Studio`, 目前应该是不支持 2019 的, 如果安装之前版本的话, 会自动检测到；\n+ `MinGW`, 是一个 GCC 的 Windows 移植版, 在一般情况下是可以代替在远程主机开发的, 但是并不推荐, 可能有兼容性问题；\n+ `WSL`, 是 Windows 推出的 Linux 子系统, 目前的 WSL1.0 版本是基于底层代码翻译, 可能存在跟MinGW一样的兼容性问题；\n+ `Cygwin`, 是开源界推出的在 Windows 运行 Linux 命令的工具, 跟 WSL1.0 相似；\n+ `Remote Host`, 直接使用远程 Linux 主机的编译工具链。\n\n## [Clang 是什么](https://zh.wikipedia.org/wiki/Clang)\n\n**Clang（发音为/ˈklæŋ/类似英文单字[clang](https://zh.wiktionary.org/wiki/clang)）** 是一个[C](https://zh.wikipedia.org/wiki/C%E8%AA%9E%E8%A8%80)、[C++](https://zh.wikipedia.org/wiki/C%2B%2B)、[Objective-C](https://zh.wikipedia.org/wiki/Objective-C)和[Objective-C++](https://zh.wikipedia.org/wiki/Objective-C%2B%2B)编程语言的** [编译器](https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E5%99%A8)前端 **。它采用了 [LLVM](https://zh.wikipedia.org/wiki/LLVM) 作为其后端，而且由 LLVM2.6 开始，一起发布新版本。它的目标是提供一个[GNU编译器套装](https://zh.wikipedia.org/wiki/GCC)（GCC）的替代品，支持了GNU编译器大多数的编译设置以及非官方语言的扩展。\n\n## MinGW 是什么\n> 参考 [MinGW 维基百科](https://zh.wikipedia.org/wiki/MinGW)\n\n`MinGW`（Minimalist GNU for Windows），又称 `mingw32`，是将 `GCC` 编译器和 `GNU Binutils` 移植到 `Win32` 平台下的产物，包括一系列头文件（Win32API）、库和可执行文件。另有可用于产生 32 位及 64 位 Windows 可执行文件的 MinGW-w64 项目，是从原本 MinGW 产生的分支。如今已经独立发展。MinGW是从Cygwin（1.3.3版）基础上发展而来。\n`GCC` 支持的语言大多在 `MinGW` 也受支持，其中涵盖 `C、C++、Objective-C、Fortran 及 Ada`。对于 C 语言之外的语言，MinGW 使用标准的 GNU 运行库，如 C++ 使用 `GNU libstdc++`。但是 MinGW 使用 Windows 中的C运行库。**因此用 MinGW 开发的程序不需要额外的第三方 DLL 支持就可以直接在 Windows 下运行**，而且也不一定必须遵从 GPL 许可证。这同时造成了 MinGW 开发的程序只能使用 Win32API 和跨平台的第三方库，而缺少 POSIX 支持[3]，大多数 GNU 软件无法在不修改源代码的情况下用 MinGW 编译。\n\n## GTK 是什么\n\n`GTK`（原名`GTK+`）最初是 `GIMP` 的专用开发库（`GIMP Toolkit`），后来发展为 `Unix-like` 系统 （类 Unix 系统）下开发图形界面的应用程序的主流开发工具之一。`GTK` 是自由软件，并且是 `GNU` 计划的一部分。自2019年2月6日起，`GTK+` 改名为 `GTK`。\n`GTK` 使用 `C` 语言开发，但是其设计者使用面向对象技术。也提供了 `C++（gtkmm）、Perl、Ruby、Java 和 Python（PyGTK）`绑定，其他的绑定有 `Ada、D、Haskell、PHP` 和所有的 `.NET` 编程语言。使用 `GTK` 的环境有 `GNOME` 等，`GNOME` 是以 `GTK` 为基础，就是说为 `GNOME` 编写的程序使用 `GTK` 做为其工具箱。\n\n## [GNU 是什么](https://zh.wikipedia.org/wiki/GNU)\n\n`GNU` 是一个自由的操作系统，其内容软件完全以 `GPL` 方式发布。这个操作系统是 `GNU计划` 的主要目标，名称来自 GNU's Not Unix! 的递归缩写，因为 GNU 的设计类似 Unix，但它不包含具著作权的 Unix 代码。作为操作系统，GNU 的发展仍未完成，其中最大的问题是具有完备功能的内核尚未被开发成功。GNU 的内核，称为 `Hurd`，是自由软件基金会发展的重点，但是其发展尚未成熟。在实际使用上，多半使用 `Linux 内核、FreeBSD` 等替代方案，作为系统核心，其中主要的操作系统是 Linux 的发行版。Linux 操作系统包涵了 Linux内核 与其他自由软件项目中的 GNU 组件和软件，可以被称为 `GNU/Linux`（见GNU/Linux命名争议）。\n`GNU` 该系统的基本组成包括 `GNU编译器套装（GCC`）、GNU的C库（ `glibc`）、以及 GNU核心工具组（`coreutils`）[14]，另外也是GNU调试器（`GDB`）、GNU 二进制实用程序（`binutils`）、GNU Cash shell 和 `GNOME` 桌面环境。 GNU开发人员已经向 GNU 应用程序和工具的 Linux 移植 ，现在也广泛应用在其它操作系统中使用，如BSD变体的Solaris，和OS X作出了贡献。\n\n## GNU 工具链是什么\n\n**GNU 工具链**（英语：GNU toolchain）是一个包含了由 GNU 计划所产生的**各种编程工具的集合**，其组成包括我们非常熟悉的 `GCC` 编译器，由自由软件基金会负责维护工作。这些工具形成了一条工具链，用于开发应用程序和操作系统。\n`GNU 工具链`在针对嵌入式系统的 `Linux内核、BSD` 及其它软件的开发中起着至关重要的作用。GNU 工具链中的部分工具也被 `Solaris, Mac OS X, Microsoft Windows (via Cygwin and MinGW/MSYS) and Sony PlayStation 3` 等其它平台直接使用或进行了移植。\n\n## [GCC 是什么](https://zh.wikipedia.org/wiki/GCC)\n\n**GNU 编译器套装（英语：GNU Compiler Collection，缩写为 GCC），指一套[编程语言](https://zh.wikipedia.org/wiki/%E7%B7%A8%E7%A8%8B%E8%AA%9E%E8%A8%80)[编译器](https://zh.wikipedia.org/wiki/%E7%BC%96%E8%AF%91%E5%99%A8)**，以[GPL](https://zh.wikipedia.org/wiki/GPL)及[LGPL](https://zh.wikipedia.org/wiki/LGPL)许可证所发行的[自由软件](https://zh.wikipedia.org/wiki/%E8%87%AA%E7%94%B1%E8%BB%9F%E9%AB%94)，也是[GNU计划](https://zh.wikipedia.org/wiki/GNU%E8%A8%88%E5%8A%83)的关键部分，也是[GNU工具链](https://zh.wikipedia.org/wiki/GNU%E5%B7%A5%E5%85%B7%E9%93%BE)的主要组成部分之一。GCC（特别是其中的C语言编译器）也常被认为是跨平台编译器的事实标准。\n\n它的原名为 `GNU C` 语言编译器（GNU C Compiler），因为它原本只能处理[C语言](https://zh.wikipedia.org/wiki/C%E8%AA%9E%E8%A8%80)。GCC在发布后很快地得到扩展，变得可处理[C++](https://zh.wikipedia.org/wiki/C%2B%2B)。之后也变得可处理[Fortran](https://zh.wikipedia.org/wiki/Fortran)、[Pascal](https://zh.wikipedia.org/wiki/Pascal_(%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80))、[Objective-C](https://zh.wikipedia.org/wiki/Objective-C)、[Java](https://zh.wikipedia.org/wiki/Java)、[Ada](https://zh.wikipedia.org/wiki/Ada)，[Go](https://zh.wikipedia.org/wiki/Go)与其他语言。许多操作系统，包括许多[类Unix](https://zh.wikipedia.org/wiki/%E7%B1%BBUnix)系统，如[Linux](https://zh.wikipedia.org/wiki/Linux)及BSD家族都采用GCC作为标准编译器。\n> GCC 原本用 C 开发，后来因为[LLVM](https://zh.wikipedia.org/wiki/LLVM)、[Clang](https://zh.wikipedia.org/wiki/Clang)的崛起，它更快地将开发语言转换为C++。许多 C 的爱好者在对 C++ 一知半解的情况下主观认定 C++ 的性能一定会输给 C，但是 Ian Lance Taylor 给出了不同的意见，并表明 C++ 不但性能不输给 C，而且能设计出更好，更容易维护的程序。\n\n## gcc 与 g++ 的区别\n> 学习了几篇博客，发现知乎的一个回答相对表达清楚和准确性，链接[在这](https://www.zhihu.com/question/20940822)。\n\n这里的 `gcc` （小写）与前文的 `GCC` 含义是不同的。简单来说，gcc 指的是 GCC 中的 GNU C Compiler（C 编译器）；`g++` 是 G++ 中的 GNU C++ Compiler（C++ 编译器），但实际上 gcc 和 g++ 都不是编译器，也不是编译器的集合，它们只是一种驱动器，根据参数中的文件类型，调用对应的 GNU  编译器，所以更准确的说法是：`gcc` 调用了 C compiler， `g++` 调用了C++ compiler。gcc 和 g++ 的主要区别如下：\n+ 使用 `gcc` 编译 `cpp` 文件可能会报错，因为 `gcc` 编译文件时不会自动链接标准库 `STL`，而 `g++` 会，为了能够使用 `STL`，需要添加参数 `-lstdc++`，`gcc -lstdc++` 和 `g++` 不等价。\n+ 对于 `*.c` 和 `*.cpp` 文件，`gcc` 分别当做 c 和 cpp 文件编译（c 和 cpp 的语法强度是不一样的）。\n+ 对于 `*.c` 和 `*.cpp` 文件，`g++` 则统一当做 `cpp` 文件编译。\n+ gcc 在编译 c 文件时，可使用的预定义宏是比较少的。\n"
  },
  {
    "path": "2-programming_language/README.md",
    "content": "## 前言\n\n本目录内容旨在分享cv算法工程师经常需要使用到的 `c/c++`、`python` 和 `shell` 编程语言的知识总结和学习笔记。\n\n## cpp\n\n* [c++基础-资源管理:堆栈与RAII](cpp/c++基础-资源管理:堆栈与RAII.md)\n* [c++日期和时间编程总结](cpp/c++日期和时间编程总结.md)\n\n## python\n\n* [python3 编程面试题](python3/python3编程面试题.md)\n* [numpy基础-堆叠数组函数总结](python3/numpy基础-堆叠数组函数总结.md)\n* [python数据分析-pandas库入门](python3/python数据分析-pandas库入门.md)\n* [python图像处理-读取图像方式总结](python3/python图像处理-读取图像方式总结.md)\n\n## shell\n\n* [shell 语法基础](shell/shell语法基础.md)\n\n## 参考资料\n\n- 《C++ Primer 第五版》\n- https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5\n- 《Python3 教程-廖雪峰》\n- 《菜鸟教程-shell》\n\n"
  },
  {
    "path": "2-programming_language/cpp/C++日期和时间编程总结.md",
    "content": "- [一，概述](#一概述)\n- [二，C-style 日期和时间库](#二c-style-日期和时间库)\n  - [2.1，数据类型](#21数据类型)\n  - [2.2，函数](#22函数)\n  - [2.3，数据类型与函数关系梳理](#23数据类型与函数关系梳理)\n  - [2.4，时间类型](#24时间类型)\n    - [2.4.1，UTC 时间](#241utc-时间)\n    - [2.4.2，本地时间](#242本地时间)\n    - [2.4.3，纪元时间](#243纪元时间)\n  - [2.5，输出时间和日期](#25输出时间和日期)\n  - [2.6，综合示例代码](#26综合示例代码)\n- [三，chrono 库](#三chrono-库)\n  - [3.1，时钟](#31时钟)\n  - [3.2，与C-style转换](#32与c-style转换)\n  - [3.3，时长 ratio](#33时长-ratio)\n    - [3.3.1，时长运算](#331时长运算)\n  - [3.4，时间间隔 duration](#34时间间隔-duration)\n    - [3.4.1，时间间隔转换函数 duration\\_cast](#341时间间隔转换函数-duration_cast)\n  - [3.5，时间点 time\\_point](#35时间点-time_point)\n    - [3.5.1，时间点运算](#351时间点运算)\n- [参考资料](#参考资料)\n\n> `C++11` 的日期和时间编程内容在 C++ Primer(第五版)这本书并没有介绍，目前网上的文章又大多质量堪忧或者不成系统，故写下这篇文章用作自己的技术沉淀和技术分享，大部分内容来自网上资料，文末也给出了参考链接。\n\n**日期和时间库**是每个编程语言都会提供的内部库，其可以用打印模块耗时，从而方便做性能分析，也可以用作打印运行时间点。本文的内容着重于 C++11-C++17的内容，C++20的日期和时钟库虽然使用更方便也更强大，但是考虑到版本兼容和程序移植问题，故不做深入探讨。\n\n## 一，概述\nC++ 中可以使用的日期时间 API 分为两类：\n\n* `C-style` 日期时间库，位于 <ctime> 头文件中。这是原先 <time.h> 头文件的 C++ 版本。\n* `chrono` 库：**C++ 11 中新增API**，增加了时间点，时长和时钟等相关接口（使用较为复杂）。\n\n在 C++11 之前，C++ 编程只能使用 C-style 日期时间库，其精度只有秒级别，这对于有高精度要求的程序来说，是不够的。但这个问题在C++11 中得到了解决，C++11 中不仅扩展了对于精度的要求，也为不同系统的时间要求提供了支持。另一方面，对于只能使用 C-style 日期时间库的程序来说，C++17 中也增加了 timespec 将精度提升到了纳秒级别。\n\n## 二，C-style 日期和时间库\n`#include <ctime>`  该头文件包含了获取和操作**日期和时间**的函数和**相关数据类型**定义。\n\n### 2.1，数据类型\n|**名称**|**说明**|\n| ----- | ----- |\n|`time_t`|能够表示时间的基本算术类型的别名，能够表示函数 `time` 返回的时间，单位为**秒**级别。|\n|`clock_t`|能够表示时钟滴答计数的基本算术类型的别名（可用作进程运行时间）|\n|`size_t`|`sizeof` 运算符返回的无符号整数类型。|\n|`struct tm`|包含日历日期和时间的结构体类型|\n|timespec\\*|以秒和纳秒表示的时间|\n\n### 2.2，函数\n`C-style` 日期时间库中包含的时间操作函数如下：\n\n|**函数**|**说明**|\n| ----- | ----- |\n|`std::clock_t clock()`|返回自程序启动时起的处理器时钟时间|\n|`double difftime(std::time_t time_end, std::time_t time_beg)`|计算开始和结束之间的**秒数差**|\n|`std::time_t time (time_t* timer)`|返回自纪元起计的**系统当前时间**, 函数可以为空指针|\n|`std::time_t mktime (struct tm * timeptr)`|将 `tm` 格式的时间转换成 `time_t` 表示的时间|\n\n时间转换函数如下：\n\n|函数|说明|\n| ----- | ----- |\n|`char* asctime(const struct tm* timeptr)`|将 `tm` 结构体对象转换为字符串的文本|\n|`char* ctime(const time_t* timer)`|将 `time_t` 对象转换为 `C` 字符串，用于表示日历时间|\n|`struct tm* gmtime(const time_t* time)`|将 `time_t` 转换成 `UTC` 表示的时间|\n|`struct tm* localtime(const time_t* timer)`|将 `time_t` 转换成本地时间|\n\n> `localtime` 函数使用参数 `timer` 指向的值来填充 `tm` 结构体，其中的值表示对应的时间，以本地时区表示。\n\n`strftime` 和 `wcsftime` 函数一般不常用，故不做介绍。`tm` 结构体的一般定义如下：\n\n```cpp\n/* Used by other time functions.  */\nstruct tm\n{\n  int tm_sec;\t\t\t/* Seconds.\t[0-60] (1 leap second) */\n  int tm_min;\t\t\t/* Minutes.\t[0-59] */\n  int tm_hour;\t\t\t/* Hours.\t[0-23] */\n  int tm_mday;\t\t\t/* Day.\t\t[1-31] */\n  int tm_mon;\t\t\t/* Month.\t[0-11] */\n  int tm_year;\t\t\t/* Year\t- 1900.  */\n  int tm_wday;\t\t\t/* Day of week.\t[0-6] */\n  int tm_yday;\t\t\t/* Days in year.[0-365]\t*/\n  int tm_isdst;\t\t\t/* DST.\t\t[-1/0/1]*/\n};\n```\n### 2.3，数据类型与函数关系梳理\n时间和日期相关的函数及数据类型比较多，单纯看表格和代码不是很好记忆，第一个参考链接的作者给出了如下所示的思维导图，方便记忆与理解上面所有函数及数据类型之间各自的联系。\n\n![image](../../data/images/C++日期和时间编程总结/ml59Jk5gnJije7fLRPKilgjgH-Txur8rfcHx4OWf9vA.png)\n\n在这幅图中，以数据类型为中心，带方向的实线箭头表示该函数能返回相应类型的结果。\n\n* `clock` 函数是相对独立的一个函数，它返回进程运行的时间，具体描述见下文。\n* `time_t` 描述了纪元时间，通过 `time` 函数可以获得它，但它只能精确到秒级别。\n* `timespec` 类型在 `time_t` 的基础上，增加了**纳秒**的精度，通过 `timespec_get` 获取。这是 `C++17` 上新增的特性。\n* `tm` 是日历类型，因为它其中**包含了年月日等**信息。通过 gmtime，localtime 和 mktime 函数可以将 time\\_t 和 tm 类型互相转换。\n* 考虑到时区的差异，因此存在 gmtime 和 localtime 两个函数。\n* 无论是 `time_t` 还是 `tm` 结构，都可以将其以字符串格式输出。ctime 和 asctime 输出的格式是固定的。如果需要自定义格式，需要使用 strftime 或者 wcsftime 函数。\n\n### 2.4，时间类型\n#### 2.4.1，UTC 时间\n**协调世界时**（**C**oordinated **U**niversial **T**ime，简称 **UTC**）是最主要的时间标准，其以**原子时秒长**为基础，在时刻上尽量接近于格林威治标准时间。\n\n协调世界时是世界上调节时钟和时间的主要时间标准，它与0度经线的平太阳时相差不超过 1 秒。因此UTC时间+8即可获得北京标准时间（UTC+8）。\n\n#### 2.4.2，本地时间\n**本地时间**与当地的时区相关，例如中国当地时间采用了北京标准时间（`UTC+8`）。\n\n#### 2.4.3，纪元时间\n**纪元时间**（Epoch time）又叫做 Unix 时间或者 POSIX 时间。它表示自1970 年 1 月 1 日 00:00 UTC 以来所经过的**秒数**（不考虑闰秒）。它在操作系统和文件格式中被广泛使用。**<ctime>**** 头文件中通过 ****time\\_t**** 以秒级别表示纪元时间**。\n\n纪元时间这个想法很简单：以一个时间为起点加上一个偏移量便可以表达任何一个其他的时间。\n\n> 为什么选这个时间作为起点，可以点击这里：[Why is 1/1/1970 the “epoch time”?](https://stackoverflow.com/questions/1090869/why-is-1-1-1970-the-epoch-time)。\n\n通过 `time` 函数获取当前时刻的纪元时间示例代码如下：\n\n```cpp\ntime_t epoch_time = time(nullptr);\ncout << \"Epoch time: \" << epoch_time << endl;\n// Epoch time: 1660039180 (日历时间: Tue Aug  9 17:59:40 2022)\n```\n`time` 函数接受一个指针，指向要存储时间的对象，通常可以传递一个空指针，然后通过返回值来接受结果。虽然标准中没有给出定义，但`time_t` 通常使用整形值来实现。\n\n### 2.5，输出时间和日期\n使用 `ctime` 函数，可以将时间以**固定格式的字符串**的形式打印出来，格式为：Www Mmm dd hh:mm:ss yyyy\\\\n。代码示例如下：\n\n```cpp\n// 以字符串形式输出当前时间和日期\ntime_t now = time(nullptr);\ncout << \"Now is: \" << ctime(&now);\n// Now is: Tue Aug  9 18:06:38 2022\n```\n### 2.6，综合示例代码\n`asctime()` 和 `difftime()` 函数等`sample` 代码如下（复制可直接运行）：\n\n```cpp\n/* asctime example */\n#include <stdio.h>      /* printf */\n#include <time.h>       /* time_t, struct tm, time, localtime, asctime */\n#include <vector>\n#include <iostream>\n\nusing namespace std;\n\n// 冒泡排序: 将数据从小到大排序\nvoid bubbleSort(vector<int> &arr){\n    size_t number = arr.size();\n    if (number <= 1) return;\n    int temp;\n    for(int i = 0; i < number; i++){\n        for(int j = 0; j < number-i; j++){\n            if (temp > arr[j+1]){\n                temp = arr[j];\n                arr[j] = arr[j+1];\n                arr[j+1] = temp;\n            }\n        }\n    }\n}\n\n// difftime() 函数: 计算时间差，单位为 s\nvoid difftime_test()\n{\n    vector<int> input_array;\n    for (int i = 90000; i > 0; i--) {\n        input_array.emplace_back(i);\n    }\n    time_t time1 = time(nullptr);\n    bubbleSort(input_array);\n    time_t time2 = time(nullptr);\n    double time_diff = difftime(time2, time1);\n    cout << \"input array size is \" << input_array.size() << \" after bubbleSort time_diff: \" << time_diff << \"s\" << endl;\n}\n\n// astime() 函数: 将本地时间 tm 结构体对象转换为字符串文本\nvoid astime_test()\n{\n    time_t raw_time = time(nullptr);  // 获取当前时刻日历时间\n    struct tm* local_timeinfo = localtime(&raw_time);\n    printf ( \"The current date/time is: %s\", asctime (local_timeinfo) );\n}\n\nint main()\n{\n    difftime_test();\n    astime_test();\n    // 3, 输出当前纪元时间\n    time_t epoch_time = time(nullptr);\n    cout << \"Epoch time: \" << epoch_time << endl;\n    // 4，以字符串形式输出当前时间和日期\n    time_t now = time(nullptr);\n    cout << \"Now is: \" << ctime(&now);\n}\n```\n`g++ time_demo.cpp -std=c++11` 编译后，运行程序 `./a.out` 后，输出结果：\n\n![image](../../data/images/C++日期和时间编程总结/aynStdgXKbMQypwFBrSinqCBacMOMzKB4O5ZfxmRVR8.png)\n\n## 三，chrono 库\n> “chrono” 是英文 chronology 的缩写，其含义是“年表；年代学”。\n\n`chrono` 既是头文件名字也是子命名空间的名字，`chrono` 头文件下的所有 `elements` 都是在 `std::chrono` 命名空间下定义的。\n\n`std::chrono` 是 C++11 引入的日期时间处理库，`chrono` 库里包括三种主要类型：`Clocks`，`Time points` 和 `Durations` 。\n\n![image](../../data/images/C++日期和时间编程总结/Bc07opCOaRwoXfaL6Q9x3FcilncZyWqh1xunhUb_NNI.png)\n\n![image](../../data/images/C++日期和时间编程总结/b0QrXV9457SwR4CamDMD103u3Fka0b0JRO2qknTYtqo.png)\n\n### 3.1，时钟\n`C++11` `chrono` 库中包含了**三种**的时钟类：\n\n|**名称**|**说明**|\n| :-----: | :-----: |\n|`chrono::system_clock`|系统时钟（可以调整）|\n|`chrono::steady_clock`|单调递增时钟（不能调整）|\n|`chrono::high_resolution_clock`|拥有可用的最短嘀嗒周期的时钟|\n\n`system_clock` 是当前所在系统的时钟。因为系统时钟随时都可能被调整，所以如果想要计算两个时间点的时间差，是不推荐使用系统时钟的。\n\n`steady_clock` 会保证时间的单调递增性，只会向前移动不会减少，所以最适合用来度量**时间间隔**。\n\n`high_resolution_clock` 表示实现提供的拥有最小计次周期的时钟。它可以是 system\\_clock 或 steady\\_clock 的别名，也可能是第三个独立时钟。在不同的标准库中，high\\_resolution\\_clock 的实现不一致，所以官方不建议使用这个时钟。\n\n这三个时钟类有一些共同的成员函数和数据类型，如下所示：\n\n|**名称**|**说明**|\n| ----- | ----- |\n|`now()`|静态成员函数，返回当前时间，类型为 clock::time\\_point|\n|`time_point`|成员类型，当前时钟的时间点类型，用于表示一个具体时间，详情见下文“时间点”|\n|`duration`|成员类型，时钟的时长类型，用于表示时间间隔(**一段时间**)，详情见下文“时长”|\n|`rep`|成员类型，时钟的 tick 类型，等同于 clock::duration::rep|\n|`period`|成员类型，时钟的单位，等同于 clock::duration::period|\n|`is_steady`|静态成员类型：是否是稳定时钟，对于 steady\\_clock 来说该值一定是 true|\n\n每一个时钟类都有一个 `now()` 静态函数来获取当前时间，返回的类型由 time\\_*point 描述。**std::chrono::time\\_point** 是模板类，模版类实例如：std::chrono::time\\_point<std::chrono::steady\\_*clock>，这样写比较长，庆幸的是在 C++11 中可以通过 `auto` 关键字来自动推导变量类型。\n\n```cpp\nstd::chrono::time_point<std::chrono::steady_clock> now1 = std::chrono::steady_clock::now();\nauto now2 = std::chrono::steady_clock::now();\n```\n### 3.2，与C-style转换\nsystem\\_*clock 与另外两个 clock 不一样的地方在于，它还提供了两个静态函数用来将 time\\_*point 与 std::time\\_t 来回转换。\n\n|**名称**|**说明**|\n| ----- | ----- |\n|to\\_time\\_t|将系统时钟时间点转换为 `time_t`|\n|from\\_time\\_t|将 `time_t` 转换到系统时钟时间点|\n\n第一篇参考链接的文章给出了下面这幅图来描述 c 风格和 c++11 的几种时间类型的转换：\n\n![image](../../data/images/C++日期和时间编程总结/f5ZI3_vc4AHIDauujjkxUdE6vhYYdwRmDp31hVZsuk0.png)\n\n### 3.3，时长 ratio\n为了支持更高精度的系统时钟，`C++11` 新增了一个新的头文件 `<ratio>` 和类型，用于自定义时间单位。`std::ratio` 是一个**模板类，提供了编译期的比例计算功能，为 std::chrono::duration 提供基础服务**。其声明如下：\n\n```cpp\ntemplate<\n    std::intmax_t Num,\n    std::intmax_t Denom = 1\n> class ratio;\n```\n第一个模板参数 `Num` (numerator) 表示分子，第二个参数 `Denom` (denominator) 表示分母。`typedef ratio<1, 1000> milli;` 表示一千分之一，因为约定了基本计算单位是秒，所以 `milli` 表示一千分之一秒。所以通过 `ratio` 可以表示**毫秒、微秒、纳秒等**。\n\n```cpp\ntypedef ratio<1,1000000000> nano; // 纳秒单位\ntypedef ratio<1,1000000> micro; // 微秒单位\ntypedef ratio<1,1000> milli; // 毫秒单位\ntypedef ratio<1,1> s // 秒单位\n```\nratio 能表达的数值不仅仅是以 10 为基底的，同时也可以表达任意的分数秒，例如：5/7秒，89/23409 秒等等对于一个具体的 ratio 来说，可以通过 den 获取分母的值，num 获取分子的值。不仅仅如此，<ratio>头文件还包含了：`ratio_add，ratio_subtract，ratio_multiply，ratio_divide` 来完成分数的加减乘除四则运算。例如，想要计算 5/7+59/1023，可以用以下代码表示：\n\n```cpp\nratio_add<ratio<5, 7>, ratio<59, 1023>> result;\ndouble value = ((double) result.num) / result.den;\ncout << result.num << \"/\" << result.den << \" = \" << value << endl;\n// 代码输出结果是 5528/7161 = 0.771959\n```\n> 在C++中，如果**分子和分母都是整形，则整形除法结果依然是整形**，即小数点右边部分会被抛弃，因此想要获取 `double` 类型的结果，需要先将其转换成 `double`。\n\n#### 3.3.1，时长运算\n时长对象之间可以进行相加或相减运算。`chrono` 提供了以下几个常用**时长运算的函数**：\n\n|**函数**|**说明**|\n| ----- | ----- |\n|`duration_cast`|进行时长的转换|\n|`floor（C++17）`|以向下取整的方式，将一个时长转换为另一个时长|\n|`ceil（C++17）`|以向上取整的方式，将一个时长转换为另一个时长|\n|`round（C++17）`|转换时长到另一个时长，就近取整，偶数优先|\n|`abs（C++17）`|获取时长的绝对值|\n\n### 3.4，时间间隔 duration\n类模板 std::chrono::duration 表示时间间隔，其声明如下：\n\n```cpp\ntemplate<\n    class Rep,\n    class Period = std::ratio<1>\n> class duration;\n```\n类成员类型描述：\n\n|**member type**|**definition**|**notes**|\n| ----- | ----- | ----- |\n|rep|The first template parameter (`Rep`)|Representation type used as the type for the internal [count](https://cplusplus.com/duration::count) object.|\n|period|The second template parameter (`Period`)|The [ratio](https://cplusplus.com/ratio) type that represents a *period* in seconds.|\n\n> `duration` 由 `Rep` 类型的**计次数**和`Period` 类型的**计次周期**组成，其中计次周期是一个编译期有理数常量，表示从一个计次到下一个的秒数。存储于 duration 的数据仅有 Rep 类型的计次数。若 Rep 是浮点数，则 duration 能表示小数的计次数。 Period 被包含为时长类型的一部分，且只在不同时长间转换时使用。\n\n* `Rep` 表示一种数值类型，用来表示 Period 的数量，比如 int float double (count of ticks)。\n* `Period` 是 std::ratio 类型，用来表示【用秒表示的时间单位】比如 second milisecond (a tick period)。\n* 成员函数 `count()` 返回 `Rep` 类型的 `Period` 数量。\n\n常用的 `duration<Rep, Period>` 已经定义好了，在 `std::chrono` 头文件中，常用时长单位的代码如下：\n\n```cpp\n/// nanoseconds\ntypedef duration<int64_t, nano> \tnanoseconds;\n/// microseconds\ntypedef duration<int64_t, micro> \tmicroseconds;\n/// milliseconds\ntypedef duration<int64_t, milli> \tmilliseconds;\n/// seconds\ntypedef duration<int64_t> \t\tseconds;\n/// minutes\ntypedef duration<int, ratio< 60>> \tminutes;\n/// hours\ntypedef duration<int, ratio<3600>> \thours;\n```\n|**类型**|**定义**|\n| ----- | ----- |\n|`std::chrono::nanoseconds`|duration</\\*至少 64 位的有符号整数类型\\*/, std::nano>|\n|`std::chrono::microseconds`|duration</\\*至少 55 位的有符号整数类型\\*/, std::micro>|\n|`std::chrono::milliseconds`|duration</\\*至少 45 位的有符号整数类型\\*/, std::milli>|\n|`std::chrono::seconds`|duration</\\*至少 35 位的有符号整数类型\\*/>|\n|`std::chrono::minutes`|duration</\\*至少 29 位的有符号整数类型\\*/, std::ratio<60»|\n|`std::chrono::hours`|duration</\\*至少 23 位的有符号整数类型\\*/, std::ratio<3600»|\n\n`duration` 类的 `count()` 成员函数返回时间间隔的具体数值。\n\n#### 3.4.1，时间间隔转换函数 duration\\_cast\n因为有各种 `duration` 表示不同的时长单位，所以 chrono 库提供了 `duration_cast` 函数来换 `duration` 类型，其声明如下：\n\n```cpp\ntemplate <class ToDuration, class Rep, class Period>\nconstexpr ToDuration duration_cast(const duration<Rep,Period>& d);\n```\n其定义比较复杂，但是我们日常使用可以直接使用 `auto` 推导函数返回对象类型，示例代码如下：\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <ratio>\n#include <thread>\n \nvoid f()\n{\n    std::this_thread::sleep_for(std::chrono::seconds(1));\n}\n \nint main()\n{\n    auto t1 = std::chrono::high_resolution_clock::now();\n    f();\n    auto t2 = std::chrono::high_resolution_clock::now();\n    // 整数时长：要求 duration_cast\n    auto int_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1);\n    // 小数时长：不要求 duration_cast\n    std::chrono::duration<double, std::milli> fp_ms = t2 - t1;\n    std::cout << \"f() took \" << fp_ms.count() << \" ms, \"\n              << \"or \" << int_ms.count() << \" whole milliseconds\\n\";\n    // 程序输出结果: f() took 1000.23 ms, or 1000 whole milliseconds\n}\n```\n### 3.5，时间点 time\\_point\n`std::chrono::time_point` 表示时间中的一个点（**一个具体时间）**，如上个世纪80年代、你的生日、今天下午、火车出发时间等，只要它能用计算机时钟表示。其包含了**时钟和时长**两个信息。它被实现成如同存储一个 `Duration` 类型的自 `Clock` 的纪元起始开始的时间间隔的值。其声明如下：\n\n```cpp\ntemplate<\n    class Clock,\n    class Duration = typename Clock::duration\n> class time_point;\n```\n时钟的 `now()` 函数返回的值就是一个时间点。time\\_point 中的 time\\_since\\_epoch() 返回从其时钟起点开始的时长。可以通过两个时间点相减计算一个时间间隔，下面是代码示例:\n\n```cpp\n#include <stdio.h>      /* printf */\n#include <iostream>\n#include <chrono>\n#include <math.h>\n\nusing namespace std;\n\nvoid time_point_test()\n{\n    auto start = chrono::steady_clock::now();\n    double sum = 0;\n    for(int i = 0; i < 100000000; i++) {\n        sum += sqrt(i);\n    }\n    auto end = chrono::steady_clock::now();\n    // 通过两个时间点相减计算一个时间间隔\n    auto time_diff = end - start;\n    // 将时间间隔单位转化为毫秒\n    auto duration = chrono::duration_cast<chrono::milliseconds>(time_diff);\n    cout << \"Sqrt Operation cost : \" << duration.count() << \"ms\" << endl;\n}\n\nint main()\n{\n    time_point_test();\n    // 程序输出结果: Sqrt Operation cost : 838ms\n}\n```\n#### 3.5.1，时间点运算\n时间点有加法和减法操作，计算结果和常识一致：时间点 + 时长 = 时间点；时间点 - 时间点 = 时长。\n\n## 参考资料\n* [C++ 日期和时间编程](https://paul.pub/cpp-date-time/#id-%E6%97%B6%E9%92%9F)\n* [C++日期和时间工具](https://www.apiref.com/cpp-zh/cpp/chrono/duration.html)\n* [C++ <chrono> 头文件内容官方英文版资料](https://cplusplus.com/reference/chrono/)"
  },
  {
    "path": "2-programming_language/cpp/c++基本编程题汇总.md",
    "content": "- [STL 库描述](#stl-库描述)\n- [内存中堆和栈的区别](#内存中堆和栈的区别)\n- [堆栈溢出一般是由什么原因导致的？](#堆栈溢出一般是由什么原因导致的)\n- [sizeof 的作用](#sizeof-的作用)\n- [C++ 面向对象特点](#c-面向对象特点)\n- [多态的理解](#多态的理解)\n- [虚函数的理解](#虚函数的理解)\n- [动态绑定的理解](#动态绑定的理解)\n- [C++构造函数初始化时什么时候只能用初始化列表？](#c构造函数初始化时什么时候只能用初始化列表)\n- [C++ 构造函数和析构函数的初始化顺序](#c-构造函数和析构函数的初始化顺序)\n- [全局变量和局部变量在内存中是否有区别？如果有，是什么区别？](#全局变量和局部变量在内存中是否有区别如果有是什么区别)\n- [C++ 中的 new delete 和 C 语言中的 malloc free 有什么区别](#c-中的-new-delete-和-c-语言中的-malloc-free-有什么区别)\n- [new、delete、malloc、free 区别](#newdeletemallocfree-区别)\n- [static 关键字作用](#static-关键字作用)\n- [类的 static 变量在什么时候初始化？函数的 static 变量在什么时候初始化？](#类的-static-变量在什么时候初始化函数的-static-变量在什么时候初始化)\n- [C++ 变量作用域](#c-变量作用域)\n- [C++ 指针和引用的区别](#c-指针和引用的区别)\n- [C++ 中析构函数的作用](#c-中析构函数的作用)\n- [C++ 静态函数和虚函数的区别](#c-静态函数和虚函数的区别)\n- [++i 和 i++ 区别](#i-和-i-区别)\n- [const 关键字作用](#const-关键字作用)\n- [C++如何传递数组给函数](#c如何传递数组给函数)\n\n## STL 库描述\n\n`STL` 库包括：容器、算法以及融合两者的迭代器。\n\n容器分为**顺序容器**和关联容器。顺序容器比如 `vector` 是一个动态分配存储空间的容器。区别于 `C++` 中的 `array`，`array` 分配的空间是静态的，分配之后不能被改变，而 `vector` 会自动重分配（扩展）空间。\n\n## 内存中堆和栈的区别\n\n- 栈内存：由编译器自动分配释放，存放函数的参数值，局部变量的值等。其操作方式类似于数据结构中的栈，都是先进后出。栈使用的是一级缓存，他们通常都是被调用时处于存储空间中，调用完毕立即释放**。\n- 堆内存：一般由程序员分配释放，若程序员不释放，程序结束时可能由 OS 回收。堆是存放在二级缓存中，生命周期由虚拟机的垃圾回收算法来决定（并不是一旦成为孤儿对象就能被回收）。所以调用这些对象的速度要相对来得低一些。\n\n## 堆栈溢出一般是由什么原因导致的？\n\n没有垃圾回收资源。\n\n## sizeof 的作用\n\n`sizeof` 运算符返回一条表达式或一个类型名字所占的字节数，其满足右结合规律。\n\n## C++ 面向对象特点\n\n数据抽象、继承和动态绑定。\n\n## 多态的理解\n\n- 同一函数作用于不同的对象，会对应不同的实现，从而产生不同的执行结果。在运行时，可以通过**指向基类的指针或引用**，来调用实现派生类中的方法。\n- `C++` 的多态性具体体现在运行和编译两个方面：在程序运行时的多态性通过继承和虚函数 virtual 来体现；在程序编译时多态性体现在函数和运算符的重载上；\n\n## 虚函数的理解\n\n和 `Python` 不同，在 `C++` 中，基类将**类型相关**的函数与派生类不做改变直接继承的函数区分对待。对于某些函数，基类希望它的派生类各自定义适合自身的版本，此使基类就会将这些函数声明成**虚函数**。同时，派生类内部必须在其内部对所有重新定义的虚函数进行声明。（参考 C++ Primer p526 页）\n\n派生类必须使用类派生列表（`class derivation list`）明确指出它是从哪个（哪些）基类继承而来。派生的形式是：首先一个冒号，后面紧跟以逗号分隔的基类列表，其中每个基类前面可以有访问说明符。\n\n```cpp\nclass Student: public People{\n    string names;\n    virtual  double get_gpa (vector<float> scores);\n}\n```\n\n## 动态绑定的理解\n\n在 C++ 语言中，，当我们使用基类的引用（或指针）调用一个虚函数时，将发生动态绑定，即函数运行的版本由实参决定，运行时自动选择函数的版本。\n\n## C++构造函数初始化时什么时候只能用初始化列表？\n\n如果类成员是 `const`、引用。或者属于某种未提供默认构造函数的类类型时，我们必须通过构造函数初始值列表为这些成员提供初始值。（参考 C++ Primer p259）\n\n## C++ 构造函数和析构函数的初始化顺序\n\n> 本回答参考[C++ 构造函数初始化顺序](https://blog.csdn.net/qq_30835655/article/details/66971183), [C++奇奇怪怪的题目之构造析构顺序](http://gaocegege.com/Blog/cpp/cppclass)\n\n有**多个基类的派生类(多继承)** 的构造函数初始化按照如下顺序进行：\n\n1. 先执行虚拟继承的父类的构造函数;\n2. 然后从左到右执行普通继承的父类的构造函数;\n3. 接着按照定义的顺序执行数据成员的初始化;\n4. 最后调用类自身的构造函数；\n\n析构函数就无脑的将构造函数顺序反转即可。多继承形式下的构造函数和单继承形式基本相同，只是要在派生类的构造函数中调用多个基类的构造函数。\n\n实例代码如下：\n\n```cpp\n\n#include <iostream>\n \nusing namespace std;\n \nclass OBJ1\n{\npublic:\n    OBJ1() { cout << \"OBJ1\" << endl; }\n    ~OBJ1() { cout << \"OBJ1 destory\" << endl;}\n};\n \nclass OBJ2\n{\npublic:\n    OBJ2() { cout << \"OBJ2\\n\"; }\n    ~OBJ2(){cout << \"OBJ2 destory\" <<endl;}\n};\n \nclass Base1\n{\npublic:\n    Base1() { cout << \"Base1\" << endl; }\n    ~Base1() { cout << \"Base1 destory\" << endl; }\n};\n \nclass Base2\n{\npublic:\n    Base2() { cout << \"Base2\" << endl; }\n    ~Base2() { cout << \"Base2 destory\" << endl; }\n};\n \nclass Base3\n{\npublic:\n    Base3() { cout << \"Base3\" << endl; }\n    ~Base3() { cout << \"Base3 destory\" << endl; }\n};\n \nclass Base4\n{\npublic:\n    Base4() { cout << \"Base4\" << endl; }\n    ~Base4() { cout << \"Base4 destory\" << endl; }\n};\n \nclass Derived :public Base1, virtual public Base2,\n    public Base3, virtual public Base4\n{\npublic:\n    Derived() { cout << \"Derived ok\" << endl; }\n    ~Derived() { cout << \"Derived destory\" << endl; }\nprotected:\n    OBJ1 obj1;\n    OBJ2 obj2;\n};\n \nint main()\n{\n    Derived aa;\n    cout << \"construct ok\"<<endl;\n    return 0;\n}\n```\n\n程序输出结果如下：\n> Base2\nBase4\nBase1\nBase3\nOBJ1\nOBJ2\nDerived ok\nconstruct ok\nDerived destory\nOBJ2 destory\nOBJ1 destory\nBase3 destory\nBase1 destory\nBase4 destory\nBase2 destory\n\n## 全局变量和局部变量在内存中是否有区别？如果有，是什么区别？\n\n全局变量储存在静态数据区，局部变量在堆栈中。\n\n## C++ 中的 new delete 和 C 语言中的 malloc free 有什么区别\n\n虽然这两者都分别是完成分配内存和释放内存的功能，但是 `C++` 用 `new` 分配内存时会调用构造函数，用 `delete` 释放内存时会调用析构函数。\n\n## new、delete、malloc、free 区别\n\n- `new` 和 `delete` 是 `C++` 的运算符，`malloc` 和 `free` 是 C++/C 语言的标准框函数，都可用于申请**动态**内存和释放内存。\n- `new` 动过调用对象的构造函数来申请动态内存；`delete` 通过调用对象的析构函数来释放内存。\n- 对于非内部数据类型的对象而言，只用 `maloc/free` 是无法满足动态对象的要求。我们知道**对象在创建的同时要自动执行构造函数**，对象在消亡之前要自动执行析构函数。但由于 malloc/free 是库函数而不是运算符，不在编译器控制权限之内，不能够把执行构造函数和析构函数的任务强加于 `malloc/free`。因此 `new`/`delete` 其实比 `malloc`/`free` 更灵活。\n\n## static 关键字作用\n\n+ **声明全局静态变量**：在全局变量前加上关键字 `static`，全局变量就定义成一个全局静态变量，作用域在声明它的文件之外是不可见的，即**从定义之处开始到文件结尾**。\n+ **局部静态变量**：在局部变量前加上关键字 `static`，作用域仍然为局部作用域，即当定义它的函数或者语句块结束的时候，作用域结束。\n+ **静态函数**： 在函数返回类型前加 `static`，静态函数只在声明他的文件中可见，不能被其他文件使用。\n+ **类的静态成员**：在类中，静态成员可以实现多个对象之间的数据共享，即静态成员是类的所有对象中共享的成员，而不是某个对象成员。并且使用静态数据成员不会破坏隐藏的原则，保证了数据的安全性。\n+ **类的静态函数**：把函数成员声明为静态的，就可以把函数与类的任何特定对象独立开来。`静态成员函数即使在类对象不存在的情况下也能被调用`，静态函数只要使用类名加范围解析运算符 `::` 就可以访问(`<类名>::<静态成员函数名>(<参数表>)`)\n\n## 类的 static 变量在什么时候初始化？函数的 static 变量在什么时候初始化？\n\n类的静态成员变量在类实例化之前就已经存在了，并且分配了内存。函数的`static` 变量在执行此函数时进行初始化。\n\n## C++ 变量作用域\n\n作用域即是程序的一个区域，在程序中变量的作用域一般有三个地方：\n\n+ 在函数或者一个代码块内部声明的变量，称为局部变量；\n+ 在函数参数中定义的变量，称为形参；\n+ 在所有函数外部声明的变量，比如在程序文件开头定义的变量，称为全局变量。\n\n## C++ 指针和引用的区别\n\n+ 指针有自己的内存空间，而引用只是一个别名，类似于Python浅拷贝和深拷贝的区别\n+ 不存在空引用, 引用必须链接到一块合法的内存地址；\n+ 一旦引用被初始化为一个对象，就不能指向另一个对象。指针可以在任何时候指向任何一个对象；\n+ 引用必须在创建时被初始化。指针可以在任何时间初始化。\n+ 指针可以有多级，但是引用只能是一级（`int **p`；合法 而 `int &&a` 是不合法的）。\n\n## C++ 中析构函数的作用\n\n析构函数与构造函数对应，类的析构函数是类的一种特殊的成员函数，**它会在每次删除所创建的对象时执行**。析构函数的名称与类的名称是完全相同的，只是在前面加了个波浪号（~）作为前缀，它不会返回任何值，也不能带有任何参数。析构函数有助于在跳出程序（比如关闭文件、释放内存等）前释放资源。\n\n## C++ 静态函数和虚函数的区别\n\n静态函数在编译的时候就已经确定运行时机，虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制，调用的时候会增加一次内存开销。\n\n## ++i 和 i++ 区别\n\n++i 先自增1，再返回，i++，先返回 i，再自增1.\n\n## const 关键字作用\n\n`const`类型的对象在程序执行期间不能被修改改变。\n\n## C++如何传递数组给函数\n\n首先要知道的是，`C++` 传数组给一个函数，该数组类型会自动转换为指针，因此实际传递的是地址。\n\n一维数组作为形参有以下三种方式，多维数组作为形参类似。\n\n- 形参是一个指针；\n- 形参是已定义大小的数组；\n- 形参是未定义大小的数组。\n"
  },
  {
    "path": "2-programming_language/python3/Python数据分析-pandas库入门.md",
    "content": "\n## pandas 库概述\npandas 提供了快速便捷处理结构化数据的大量数据结构和函数。自从2010年出现以来，它助使 Python 成为强大而高效的数据分析环境。pandas使用最多的数据结构对象是 DataFrame，它是一个面向列（column-oriented）的二维表结构，另一个是 Series，一个一维的标签化数组对象。\n\npandas 兼具 NumPy 高性能的数组计算功能以及电子表格和关系型数据库（如SQL）灵活的数据处理功能。它提供了复杂精细的索引功能，能更加便捷地完成重塑、切片和切块、聚合以及选取数据子集等操作。数据操作、准备、清洗是数据分析最重要的技能，pandas 是首选 python 库之一。\n\n个人觉得，学习 pandas 还是最好在 anaconda 的 jupyter 环境下进行，方便断点调试分析，也方便一行行运行代码。\n\n## 安装 pandas\nWindows/Linux系统环境下安装\n\nconda方式安装\n\n```Plain Text\nconda install pandas\n```\npip3方式安装\n\n```Plain Text\npy -3 -m pip install --upgrade pandas    #Windows系统\npython3 -m pip install --upgrade pandas    #Linux系统\n```\n## pandas 库使用\npandas 采用了大量的 NumPy 编码风格，但二者最大的不同是 pandas 是专门为处理表格和混杂数据设计的。而 NumPy 更适合处理统一的数值数组数据。\n\n导入 pandas 模块，和常用的子模块 Series 和 DataFrame\n\n```Plain Text\nimport pands as pd\nfrom pandas import Series,DataFrame\n```\n通过传递值列表来创建 Series，让 pandas 创建一个默认的整数索引:\n\n```Plain Text\ns = pd.Series([1,3,5,np.nan,6,8])\ns\n```\n输出\n\n> 0    1.0\n1    3.0\n2    5.0\n3    NaN\n4    6.0\n5    8.0\ndtype:  float64\n\n## pandas数据结构介绍\n要使用 pandas，你首先就得熟悉它的两个主要数据结构：**Series 和 DataFrame**。虽然它们并不能解决所有问题，但它们为大多数应用提供了一种可靠的、易于使用的基础。\n\n### Series数据结构\nSeries 是一种类似于一维数组的对象，它由一组数据（各种 NumPy 数据类型）以及一组与之相关的数据标签（即索引）组成。仅由一组数据即可产生最简单的 Series。代码示例：\n\n```Plain Text\nimport pandas as pd\nobj = pd.Series([1,4,7,8,9])\nobj\n```\n![image](../../data/images/pandas/mehHOUu698p5GFHptdAl1TS1PZuqzwvUpJKGLHfuqEY.png)\n\nSeries 的字符串表现形式为：索引在左边，值在右边。由于我们没有为数据指定索引，于是会自动创建一个 0 到 N-1（ N 为数据的长度）的整数型索引。也可以通过Series 的 values 和 index 属性获取其数组表示形式和索引对象，代码示例：\n\n```Plain Text\nobj.values\nobj.index # like range(5)\n```\n输出：\n\n> array(\\[ 1, 4, 7, 8, 9\\])\nRangeIndex(start=0, stop=5, step=1)\n\n我们也希望所创建的 Series 带有一个可以对各个数据点进行标记的索引，代码示例：\n\n```Plain Text\nobj2 = pd.Series([1, 4, 7, 8, 9],index=['a', 'b', 'c', 'd'])\nobj2\nobj2.index\n```\n输出\n\n> a    1\nb    4\nc    7\nd    8\ne    9\ndtype:  int64\n\n> Index(\\[‘a’, ‘b’, ‘c’, ‘d’, ‘e’\\], dtype=’object’)\n\n![image](../../data/images/pandas/zjIofANE2EH8FbL8t5R2ivTG3ieIx3DrA1ufwfTJKG4.png)\n\n与普通 NumPy 数组相比，你可以通过索引的方式选取 Series 中的单个或一组值，代码示例：\n\n```Plain Text\nobj2[['a', 'b', 'c']] \nobj2['a']=2\nobj2[['a', 'b', 'c']]\n```\n![image](../../data/images/pandas/pnppCnVHQV3YWboi44iaDHOgOzdAUpnnYFV8EwAnodU.png)\n\n\\[‘a’,’b’,’c\\]是索引列表，即使它包含的是字符串而不是整数。\n\n使用 NumPy 函数或类似 NumPy 的运算（如根据布尔型数组进行过滤、标量乘法、应用数学函数等）都会保留索引值的链接，代码示例：\n\n```Plain Text\nobj2*2\nnp.exp(obj2)\n```\n![image](../../data/images/pandas/dHcDtD_Y6NZGCUIb_zVvp_5lJLBZaTzZTm823dlaqmg.png)\n\n还可以将 Series 看成是一个定长的有序字典，因为它是索引值到数据值的一个映射。它可以用在许多原本需要字典参数的函数中，代码示例：\n\n```Plain Text\ndict = {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000,'Utah': 5000}\nobj3 = pd.Series(dict)\nobj3\n```\n输出\n\n> Ohio 35000\n\n> Oregon 16000\n\n> Texas 71000\n\n> Utah 5000\n\n> dtype: int64\n\n![image](../../data/images/pandas/4GjLOFJv3__gjLkru7fU5DlPrU68R6mmI0Y6daaFPuc.png)\n\n### DataFrame数据结构\nDataFrame 是一个表格型的数据结构，它含有一组有序的列，每列可以是不同的值类型（数值、字符串、布尔值等）。DataFrame 既有行索引也有列索引，它可以被看做由 Series 组成的字典（共用同一个索引）。DataFrame 中的数据是以一个或多个二维块存放的（而不是列表、字典或别的一维数据结构）。\n\n> 虽然 DataFrame 是以二维结构保存数据的，但你仍然可以轻松地将其表示为更高维度的数据（层次化索引的表格型结构，这是 pandas中许多高级数据处理功能的关键要素 ）\n\n创建 DataFrame 的办法有很多，最常用的一种是直接传入一个由等长列表或 NumPy 数组组成的字典，代码示例：\n\n```Plain Text\ndata = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada','Nevada'],\n             'year': [2000, 2001, 2002, 2001, 2002, 2003],\n              'pop': [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]}\nframe = pd.DataFrame(data)\nframe\n```\n结果 DataFrame 会自动加上索引（跟 Series 一样），且全部列会被有序排列，输出如下：\n\n![image](../../data/images/pandas/yw-a9Kc_vSLq0X6TLKUBbT9dve1ihpi437Izvu_L8oM.png)\n\n对于特别大的 DataFrame，head 方法会选取前五行：\n\n```Plain Text\nframe.head()\n```\n如果指定了列序列，则 DataFrame 的列就会按照指定顺序进行排列，代码示例：\n\n```Plain Text\npd.DataFrame(data,columns=['state','year','pop'])\n```\n![image](../../data/images/pandas/6suW8Y3Om3nd9x6b0-GdWDCssQVsltgFion2NLZv-vM.png)\n\n如果传入的列在数据中找不到，就会在结果中产生缺失值，代码示例：\n\n```Plain Text\nframe2 = pd.DataFrame(data,columns=['state','year','pop','debt'],\n                                    index=['one','two','three','four','five','six'])\nframe2\n```\n![image](../../data/images/pandas/GUFyyIupGf-1EizI7KuJBtuogx9O83eWMPZGMHWgNV0.png)\n\n获取 DataFrame 的 columns 和 index，代码示例：\n\n```Plain Text\nframe2.columns\nframe2.index\n```\n输出\n\n> Index(\\[‘state’, ‘year’, ‘pop’, ‘debt’\\], dtype=’object’)\n\n> Index(\\[‘one’, ‘two’, ‘three’, ‘four’, ‘five’, ‘six’\\], dtype=’object’)\n\n![image](../../data/images/pandas/vybZ_9BZL19pm3PW54JMxn_OtrnSB_ibDYHVYiH3l98.png)\n\n通过类似字典标记的方式或属性的方式，可以将 DataFrame 的列获取为一个 Series，代码示例：\n\n```Plain Text\nframe2['state']\nframe2.state\n```\n![image](../../data/images/pandas/LVh77V4fUh10o4mU5AloGJg_spSi3H1cHvrGhxg3whY.png)\n\n列可以通过赋值的方式进行修改，赋值方式类似 Series。例如，我们可以给那个空的 “debt” 列赋上一个标量值或一组值（数组或列表形式），代码示例：\n\n```Plain Text\nframe2.debt = np.arange(6.)\nframe2\n```\n![image](../../data/images/pandas/b5g2YOTWsxoPnIZxyeSOkoV3P_8nxJpXSsf2ZuLO7co.png)\n\n注意：将列表或数组赋值给某个列时，其长度必须跟DataFrame的长度相匹配。\n\n如果赋值的是一个 Series，就会精确匹配 DataFrame 的索引，所有的空位都将被填上缺失值，代码示例：\n\n```Plain Text\nval = pd.Series([-1.2, -1.5, -1.7], index=['two', 'four','five'])\nframe2.debt = val\nframe2\n```\n为不存在的列赋值会创建出一个新列。关键字 **del** 用于删除列。\n\n作为 del 的例子，这里先添加一个新的布尔值的列，state 是否为 ‘Ohio’，代码示例：\n\n```Plain Text\nframe2['eastern'] = frame2.state=='Ohio'\nframe2\n```\n![image](../../data/images/pandas/bVlf3zi_oCdjSzgdHHfeya1rYQUMWgLPWmYUsGMXi30.png)\n\nDataFrame 另一种常见的数据形式是嵌套字典，如果嵌套字典传给 DataFrame，pandas 就会被解释为：外层字典的键作为列，内层键则作为行索引，代码示例：\n\n```Plain Text\n#DataFrame另一种常见的数据形式是嵌套字典\npop = {\n      'Nvidia':{2001:2.4,2002:3.4},\n      'Intel':{2000:3.7,2001:4.7,2002:7.8}\n}\nframe3 = pd.DataFrame(pop,columns=['Nvidia','Intel'])\nframe3\n```\n![image](../../data/images/pandas/5NqSbp4XB_LEPBOnbqRv27Ou3Of3RNL1-6zib6v9l7I.png)\n\n**表5-1列出了DataFrame构造函数所能接受的各种数据**\n\n![image](../../data/images/pandas/_7XUyaBIMZVAO41Ih8BY10W21yMODIQz9SNWmFekhWQ.png)\n\n### 索引对象\npandas 的索引对象负责管理轴标签和其他元数据（比如轴名称等）。构建 Series 或 DataFrame 时，所用到的任何数组或其他序列的标签都会被转换成一个 Index，代码示例：\n\n```Plain Text\nimport numpy as np\nimport pandas as pd\nobj = pd.Series(np.arange(4),index=['a','b','c','d'])\nindex = obj.index\n#index\nindex[:-1]\n```\n![image](../../data/images/pandas/eN7Nbjg4bMr0CpiB0C44WN_TidMdM_1_kemHxwk3XIM.png)\n\n注意：Index 对象是不可变的，因此用户不能对其进行修改。\n\n不可变可以使 Index 对象在多个数据结构之间安全共享，代码示例：\n\n```Plain Text\n#pd.Index储存所有pandas对象的轴标签\n#不可变的ndarray实现有序的可切片集\nlabels = pd.Index(np.arange(3))\nobj2 = pd.Series([1.5, -2.5, 0], index=labels)\nobj2\n#print(obj2.index is labels)\n```\n![image](../../data/images/pandas/HdxGMf8q3VQvdnWZelz2xtTBOQYju7PxKFsJO-Azm74.png)\n\n> 注意：虽然用户不需要经常使用 Index 的功能，但是因为一些操作会生成包含被索引化的数据，理解它们的工作原理是很重要的。\n\n与 python 的集合不同，pandas 的 Index 可以包含重复的标签，代码示例：\n\n```Plain Text\ndup_labels = pd.Index(['foo','foo','bar','alice'])\ndup_labels\n```\n每个索引都有一些方法和属性，它们可用于设置逻辑并回答有关该索引所包含的数据的常见问题。表5-2列出了这些函数。\n\n![image](../../data/images/pandas/NYmjzWqZnomg7PIbJQx3KYgg3--Xb0MpCpjBKSyK4Mc.png)\n\n## pandas 选择数据\n```Plain Text\nimport numpy as np\nimport pandas as pd\n# dates = pd.date_range('20190325', periods=6)\ndates = pd.date_range('20190325', periods=6)\ndf = pd.DataFrame(np.arange(24).reshape((6,4)),index=dates, columns=['A','B','C','D'])\nprint(df)\n'''\n             A   B   C   D\n2019-03-25   0   1   2   3\n2019-03-26   4   5   6   7\n2019-03-27   8   9  10  11\n2019-03-28  12  13  14  15\n2019-03-29  16  17  18  19\n2019-03-30  20  21  22  23\n'''\n# 检索指定A列\nprint(df['A'])    # 等同于print(df.A)\n'''\n2019-03-25     0\n2019-03-26     4\n2019-03-27     8\n2019-03-28    12\n2019-03-29    16\n2019-03-30    20\nFreq: D, Name: A, dtype: int64\n'''\n## 切片选取多行或多列\nprint(df[0:3])    # 等同于print(df['2019-03-25':'2019-03-27'])\n'''\n            A  B   C   D\n2019-03-25  0  1   2   3\n2019-03-26  4  5   6   7\n2019-03-27  8  9  10  11\n'''\n# 根据标签选择数据\n# 获取特定行或列\n# 指定行数据\nprint(df.loc['2019-03-25'])\nbb = df.loc['2019-03-25']\nprint(type(bb))\n'''\nA    0\nB    1\nC    2\nD    3\nName: 2019-03-25 00:00:00, dtype: int64\n<class 'pandas.core.series.Series'>\n'''\n# 指定列, 两种方式\nprint(df.loc[:, ['A', 'B']])    # print(df.loc[:, 'A':'B'])\n'''\n             A   B\n2019-03-25   0   1\n2019-03-26   4   5\n2019-03-27   8   9\n2019-03-28  12  13\n2019-03-29  16  17\n2019-03-30  20  21\n'''\n# 行列同时检索\ncc = df.loc['20190325', ['A', 'B']]\nprint(cc);print(type(cc.values))# numpy ndarray\n'''\nA    0\nB    1\nName: 2019-03-25 00:00:00, dtype: int64\n<class 'numpy.ndarray'>\n'''\nprint(df.loc['20190326', 'A'])\n'''\n4\n'''\n# 根据序列iloc获取特定位置的值, iloc是根据行数与列数来索引的\nprint(df.iloc[1,0])     # 13, numpy ndarray\n'''\n4\n'''\nprint(df.iloc[3:5,1:3]) # 不包含末尾5或3，同列表切片\n'''\n             B   C\n2019-03-28  13  14\n2019-03-29  17  18\n'''\n# 跨行操作\nprint(df.iloc[[1, 3, 5], 1:3])\n'''\n             B   C\n2019-03-26   5   6\n2019-03-28  13  14\n2019-03-30  21  22\n'''\n# 通过判断的筛选\nprint(df[df.A>8])\n'''\n             A   B   C   D\n2019-03-28  12  13  14  15\n2019-03-29  16  17  18  19\n2019-03-30  20  21  22  23\n'''\n```\n## 总结\n本文主要记录了 Series 和 DataFrame 作为 pandas 库的基本结构的一些特性，如何创建 pandas 对象、指定 columns 和 index 创建 Series 和 DataFrame 对象、赋值操作、属性获取、索引对象等，这章介绍操作 Series 和 DataFrame 中的数据的基本手段。\n\n## 参考资料\n* 《利用python进行数据分析》\n\n"
  },
  {
    "path": "2-programming_language/python3/python3编程总结.md",
    "content": "- [Python global 语句的作用](#python-global-语句的作用)\n- [lambda 匿名函数好处](#lambda-匿名函数好处)\n- [Python 错误处理](#python-错误处理)\n- [Python 内置错误类型](#python-内置错误类型)\n- [简述 any() 和 all() 方法](#简述-any-和-all-方法)\n- [Python 中什么元素为假？](#python-中什么元素为假)\n- [提高 Python 运行效率的方法](#提高-python-运行效率的方法)\n- [Python 单例模式](#python-单例模式)\n- [为什么 Python 不提供函数重载](#为什么-python-不提供函数重载)\n- [实例方法/静态方法/类方法](#实例方法静态方法类方法)\n- [\\_\\_new\\_\\_和 \\_\\_init \\_\\_方法的区别](#__new__和-__init-__方法的区别)\n- [Python 的函数参数传递](#python-的函数参数传递)\n- [Python 实现对函参做类型检查](#python-实现对函参做类型检查)\n- [为什么说 Python 是动态语言](#为什么说-python-是动态语言)\n- [Python 装饰器理解](#python-装饰器理解)\n- [map 与 reduce 函数用法解释](#map-与-reduce-函数用法解释)\n- [Python 深拷贝、浅拷贝区别](#python-深拷贝浅拷贝区别)\n- [Python 继承多态理解](#python-继承多态理解)\n- [Python 面向对象的原则](#python-面向对象的原则)\n- [参考资料](#参考资料)\n\n## Python global 语句的作用\n\n在编写程序的时候，如果想要**改变(重新赋值)**函数外部的变量，并且这个变量会作用于许多函数中，就需要告诉 Python 程序这个变量的作用域是全局变量，`global` 语句可以实现定义全局变量的作用。\n\n## lambda 匿名函数好处\n\n精简代码，`lambda`省去了定义函数，`map` 省去了写 `for` 循环过程:\n\n```python\nstr_1 = [\"中国\", \"美国\", \"法国\", \"\", \"\", \"英国\"]\nres = list(map(lambda x: \"填充值\" if x==\"\" else x, str_1))\nprint(res)  # ['中国', '美国', '法国', '填充值', '填充值', '英国']\n```\n\n## Python 错误处理\n\n和其他高级语言一样，`Python` 也内置了一套`try...except...finally...` 的错误处理机制。\n\n当我们认为某些代码可能会出错时，就可以用 `try` 来运行这段代码，如果执行出错，则后续代码不会继续执行，而是直接跳转至跳转至错误处理代码，即 `except` 语句块，执行完 `except` 后，如果有 `finally` 语句块，则执行。至此，执行完毕。跳转至错误处理代码，\n\n## Python 内置错误类型\n\n+ `IOError`：输入输出异常\n+ `AttributeError`：试图访问一个对象没有的属性\n+ `ImportError`：无法引入模块或包，基本是路径问题\n+ `IndentationError`：语法错误，代码没有正确的对齐\n+ `IndexError`：下标索引超出序列边界\n+ `KeyError`: 试图访问你字典里不存在的键\n+ `SyntaxError`: Python 代码逻辑语法出错，不能执行\n+ `NameError`: 使用一个还未赋予对象的变量\n\n## 简述 any() 和 all() 方法\n\n+ `any()`: 只要迭代器中有一个元素为真就为真;\n+ `all()`: 迭代器中所有的判断项返回都是真，结果才为真.\n\n## Python 中什么元素为假？\n\n答案：（0，空字符串，空列表、空字典、空元组、None, False）\n\n## 提高 Python 运行效率的方法\n\n1. 使用生成器，因为可以节约大量内存;\n2. 循环代码优化，避免过多重复代码的执行;\n3. 核心模块用 `Cython PyPy` 等，提高效率;\n4. 多进程、多线程、协程;\n5. 多个 `if elif` 条件判断，可以把最有可能先发生的条件放到前面写，这样可以减少程序判断的次数，提高效率。\n\n## Python 单例模式\n\n## 为什么 Python 不提供函数重载\n\n> 参考知乎[为什么 Python 不支持函数重载？其他函数大部分都支持的？](https://www.zhihu.com/question/20053359)\n\n我们知道 `函数重载` 主要是为了解决两个问题。\n\n1. 可变参数类型。\n2. 可变参数个数。\n\n另外，一个函数重载基本的设计原则是，仅仅当两个函数除了参数类型和参数个数不同以外，其功能是完全相同的，此时才使用函数重载，如果两个函数的功能其实不同，那么不应当使用重载，而应当使用一个名字不同的函数。\n\n1. 对于情况 1 ，函数功能相同，但是参数类型不同，Python 如何处理？答案是根本不需要处理，因为 `Python` 可以接受任何类型的参数，如果函数的功能相同，那么不同的参数类型在 Python 中很可能是相同的代码，没有必要做成两个不同函数。\n2. 对于情况 2 ，函数功能相同，但参数个数不同，Python 如何处理？大家知道，答案就是**缺省参数(默认参数)**。对那些缺少的参数设定为缺省参数(默认参数)即可解决问题。因为你假设函数功能相同，那么那些缺少的参数终归是需要用的。所以，鉴于情况 1 跟 情况 2 都有了解决方案，Python 自然就不需要函数重载了。\n\n## 实例方法/静态方法/类方法\n\n`Python` 类语法中有三种方法，**实例方法，静态方法，类方法**，它们的区别如下：\n\n+ 实例方法只能被实例对象调用，静态方法(由 `@staticmethod` 装饰器来声明)、类方法(由 `@classmethod` 装饰器来声明)，可以被类或类的实例对象调用;\n+ `实例方法`，第一个参数必须要默认传实例对象，一般习惯用self。`静态方法`，参数没有要求。`类方法`，第一个参数必须要默认传类，一般习惯用 `cls` .\n\n实例代码如下：\n\n```python\nclass Foo(object):\n    \"\"\"类三种方法语法形式\n    \"\"\"\n    def instance_method(self):\n        print(\"是类{}的实例方法，只能被实例对象调用\".format(Foo))\n\n    @staticmethod\n    def static_method():\n        print(\"是静态方法\")\n\n    @classmethod\n    def class_method(cls):\n        print(\"是类方法\")\n\n\nfoo = Foo()\nfoo.instance_method()\nfoo.static_method()\nfoo.class_method()\nprint('##############')\nFoo.static_method()\nFoo.class_method()\n```\n\n程序执行后输出如下：\n> 是类 <class '__main__.Foo'> 的实例方法，只能被实例对象调用\n是静态方法\n是类方法\n##############\n是静态方法\n是类方法\n\n## \\_\\_new\\_\\_和 \\_\\_init \\_\\_方法的区别\n\n+ `__init__` 方法并不是真正意义上的构造函数, `__new__` 方法才是(类的构造函数是类的一种特殊的成员函数，它会**在每次创建类的新对象时执行**);\n+ `__new__` 方法用于创建对象并返回对象，当返回对象时会自动调用 `__init__` 方法进行初始化, `__new__` 方法比 `__init__` 方法更早执行;\n+ `__new__` 方法是**静态方法**，而 `__init__` 是**实例方法**。\n\n## Python 的函数参数传递\n\n> 参考这两个链接，stackoverflow的最高赞那个讲得很详细\n[How do I pass a variable by reference?](https://stackoverflow.com/questions/986006/how-do-i-pass-a-variable-by-reference)\n[Python 面试题](https://github.com/taizilongxu/interview_Python#Python%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7)\n\n个人总结（有点不好）：\n\n+ 将可变对象：列表list、字典dict、NumPy数组ndarray和用户定义的类型（类），作为参数传递给函数，函数内部将其改变后，函数外部这个变量也会改变（对变量进行重新赋值除外 `rebind the reference in the method`）\n+ 将不可变对象：字符串string、元组tuple、数值numbers，作为参数传递给函数，函数内部将其改变后，函数外部这个变量不会改变\n\n## Python 实现对函参做类型检查\n\n`Python` 自带的函数一般都会有对函数参数类型做检查，自定义的函数参数类型检查可以用函数 `isinstance()` 实现，例如：\n\n```python\ndef my_abs(x):\n    \"\"\"\n    自定义的绝对值函数\n    :param x: int or float\n    :return: positive number, int or float\n    \"\"\"\n    if not isinstance(x, (int, float)):\n        raise TypeError('bad operand type')\n    if x > 0:\n        return x\n    else:\n        return -x\n```\n\n添加了参数检查后，如果传入错误的参数类型，函数就可以抛出一个 `TypeError` 错误。\n\n## 为什么说 Python 是动态语言\n\n在 `Python` 中，等号 `=` 是赋值语句，可以把`任意数据类型`赋值给变量，同样一个变量可以反复赋值，而且可以是不同类型的变量，例如：\n\n```python\na = 100 # a是int型变量\nprint(a)\na = 'ABC'  # a 是str型变量\nprint(a)\n```\n\nPyhon 这种变量本身类型不固定，可以反复赋值不同类型的变量称为动态语言，与之对应的是静态语言。静态语言在定义变量时必须指定变量类型，如果赋值的时候类型不匹配，就会报错，Java/C++ 都是静态语言（`int a; a = 100`）\n\n## Python 装饰器理解\n\n装饰器本质上是一个 Python 函数或类，**它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能**，装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景，比如：插入日志、性能测试、事务处理、缓存、权限校验等场景，装饰器是解决这类问题的绝佳设计。有了装饰器，我们就可以**抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用**。概括的讲，**装饰器的作用就是为已经存在的对象添加额外的功能**。\n\n## map 与 reduce 函数用法解释\n\n1、`map()` 函数接收两个参数，一个是函数，一个是 Iterable，map 将传入的函数依次作用到序列的**每个元素**，并将结果作为新的 Iterator 返回，简单示例代码如下：\n\n```python\n# 示例１\ndef square(x):\n    return x ** 2\nr = map(square, [1, 2, 3, 4, 5, 6, 7])\nsquareed_list = list(r)\nprint(squareed_list)  # [1, 4, 9, 16, 25, 36, 49]\n# 使用lambda匿名函数简化为一行代码\nlist(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))\n# 示例２\nlist(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))　＃　['1', '2', '3', '4', '5', '6', '7', '8', '9']\n```\n\n**注意map函数返回的是一个Iterator（惰性序列），要通过list函数转化为常用列表结构**。map()作为高阶函数，事实上它是把运算规则抽象了。\n\n2、`reduce()` 函数也接受两个参数，一个是函数（**两个参数**），一个是序列，与 `map` 不同的是**reduce 把结果继续和序列的下一个元素做累积计算**,效果如下：\n`reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)`\n\n示例代码如下：\n\n```python\nfrom functools import reduce\nCHAR_TO_INT = {\n    '0': 0,\n    '1': 1,\n    '2': 2,\n    '3': 3,\n    '4': 4,\n    '5': 5,\n    '6': 6,\n    '7': 7,\n    '8': 8,\n    '9': 9\n}\ndef str2int(str):\n    ints = map(lambda x:CHAR_TO_INT[x], str)  # str对象是Iterable对象\n    return reduce(lambda x,y:10*x + y, ints)\nprint(str2int('0'))\nprint(str2int('12300'))\nprint(str2int('0012345'))  # 0012345\n```\n\n## Python 深拷贝、浅拷贝区别\n\n> Python 中的大多数对象，比如列表 `list`、字典 `dict`、集合 `set`、`numpy` 数组，和用户定义的类型（类），都是可变的。意味着这些对象或包含的值可以被修改。但也有些对象是不可变的，例如数值型 `int`、字符串型 `str` 和元组 `tuple`。\n\n**1、复制不可变数据类型：**\n\n复制不可变数据类型，不管 `copy` 还是 `deepcopy`, 都是同一个地址。当浅复制的值是不可变对象（数值，字符串，元组）时和=“赋值”的情况一样，对象的 `id` 值与浅复制原来的值相同。\n\n**2、复制可变数据类型：**\n\n1. 直接赋值：其实就是对象的引用（别名）。\n2. 浅拷贝(`copy`)：拷贝父对象，不会拷贝对象内部的子对象（拷贝可以理解为创建内存）。产生浅拷贝的操作有以下几种：\n    + 使用切片 `[:]` 操作\n    + 使用工厂函数（如 `list/dir/set` ）, 工厂函数看上去像函数，实质上是类，调用时实际上是生成了该类型的一个实例，就像工厂生产货物一样.\n    + 使用`copy` 模块中的 `copy()` 函数，`b = a.copy()`, `a` 和 `b` 是一个独立的对象，**但他们的子对象还是指向统一对象（是引用）**。\n3. 深拷贝(`deepcopy`)： copy 模块的 `deepcopy()` 方法，**完全**拷贝了父对象及其子对象，两者是完全独立的。**深拷贝，包含对象里面的子对象的拷贝，所以原始对象的改变不会造成深拷贝里任何子元素的改变**。\n\n**注意：浅拷贝和深拷贝的不同仅仅是对组合对象来说，所谓的组合对象（容器）就是包含了其它对象的对象，如列表，类实例。而对于数字、字符串以及其它“原子”类型（没有子对象），没有拷贝一说，产生的都是原对象的引用。**更清晰易懂的理解，可以参考这篇[文章](https://www.runoob.com/w3cnote/python-understanding-dict-copy-shallow-or-deep.html)。\n\n看一个示例程序，就能明白浅拷贝与深拷贝的区别了：\n\n```python\n#!/usr/bin/Python3\n# -*-coding:utf-8 -*-\n\nimport copy\na = [1, 2, 3, ['a', 'b', 'c']]\n\nb = a  # 赋值，传对象的引用\nc = copy.copy(a)  # 浅拷贝\nd = copy.deepcopy(a)  # 深拷贝\n\na.append(4)\na[3].append('d')\n\nprint(id(a), id(b), id(c), id(d))  # a 与 b 的内存地址相同\nprint('a = ', a)\nprint('b = ', b)\nprint('c = ', c)\nprint('d = ', d)  # [1, 2, 3, ['a', 'b', 'c']]\n```\n\n程序输出如下：\n> 2061915781832 2061915781832 2061932431304 2061932811400\n>\n> a =  [1, 2, 3, ['a', 'b', 'c', 'd'], 4]\n> b =  [1, 2, 3, ['a', 'b', 'c', 'd'], 4]\n> c =  [1, 2, 3, ['a', 'b', 'c', 'd']]\n> d =  [1, 2, 3, ['a', 'b', 'c']]\n\n## Python 继承多态理解\n\n- 多态是指对不同类型的变量进行相同的操作，它会根据对象（或类）类型的不同而表现出不同的行为。\n- 继承可以拿到父类的所有数据和方法，子类可以重写父类的方法，也可以新增自己特有的方法。\n- 先有继承，后有多态，不同类的对象对同一消息会作出不同的相应。\n\n## Python 面向对象的原则\n\n+ [Python 工匠：写好面向对象代码的原则（上）](https://www.zlovezl.cn/articles/write-solid-python-codes-part-1/)\n+ [Python 工匠：写好面向对象代码的原则（中）](https://www.zlovezl.cn/articles/write-solid-python-codes-part-2/)\n+ [Python 工匠：写好面向对象代码的原则（下）](https://www.zlovezl.cn/articles/write-solid-python-codes-part-3/)\n\n## 参考资料\n\n1. 参考[这里](https://zhuanlan.zhihu.com/p/23526961)\n2. [110道Python面试题（真题）](https://zhuanlan.zhihu.com/p/54430650)\n3. [关于Python的面试题](https://github.com/taizilongxu/interview_Python#15-__new__%E5%92%8C__init__%E7%9A%84%E5%8C%BA%E5%88%AB)\n4. [继承和多态](http://funhacks.net/explore-python/Class/inheritance_and_polymorphism.html)\n5. [Python 直接赋值、浅拷贝和深度拷贝解析](https://www.runoob.com/w3cnote/python-understanding-dict-copy-shallow-or-deep.html)\n"
  },
  {
    "path": "2-programming_language/python3/python图像处理-读取图像方式总结.md",
    "content": "- [读取并显示图像](#读取并显示图像)\n  - [opencv3库](#opencv3库)\n  - [scikit-image库](#scikit-image库)\n  - [PIL库](#pil库)\n  - [读取图像结果分析](#读取图像结果分析)\n- [打印图像信息](#打印图像信息)\n  - [skimage获取图像信息](#skimage获取图像信息)\n  - [PIL获取图像信息](#pil获取图像信息)\n- [读取并显示图像方法总结](#读取并显示图像方法总结)\n  - [PIL库读取图像](#pil库读取图像)\n  - [Opencv3读取图像](#opencv3读取图像)\n  - [scikit-image库读取图像](#scikit-image库读取图像)\n  - [参考资料](#参考资料)\n\n学习数字图像处理，第一步就是读取图像。这里我总结下如何使用 opencv3,scikit-image, PIL 图像处理库读取图片并显示。\n\n## 读取并显示图像\n### opencv3库\nopencv 读取图像，返回的是矩阵数据，RGB 图像的 shape 是 (height, weight, channel)，dtype 是 uint8。\n\n示例代码如下：\n\n```python\nimport cv2\n# 读入一副彩色图像\nimg_cv2 = cv2.imread('test.jpg',cv2.IMREAD_COLOR)\n# 打印图像尺寸,形状，图像元素数据类型\nprint(type(img_cv2))\nprint(img_cv2.shape)    # (height, width, channel)\nprint(img_cv2.dtype)    # uint8\n# matplotlib绘制显示图像\nplt.figure(1)\nplt.imshow(img_PIL)\nplt.show()\n# cv2绘制显示图像\n# cv2.imshow()\n# cv2.namedWindow('image', cv2.WINDOW_NORMAL)\n# cv2.imshow('image',img_cv2)\n# cv2.waitKey(0)\n# cv2.destroyAllWindows()\n```\n### scikit-image库\n示例代码如下：\n\n```python\nfrom skimage import io\nimg_skimage = io.imread('test.jpg')\n# 打印图像尺寸\nprint(img_skimage.shape)    #(height, width, channel)\n# 绘制显示图像\nio.imshow(img_skimage)\n# import matplotlib.pyplot as plt\n# plt.imshow(img_skimage)\n```\n注意：io.imshow(img\\_skimage)，这一行代码的实质是利用matplotlib包对图片进行绘制，绘制成功后，返回一个matplotlib类型的数据。也就是说scikit-image库对图像的绘制实际上是调用了matplotlib库imshow显示函数。\n\ncv2和skimage读取图像，图像的尺寸可以通过其shape属性来获取，shape返回的是一个tuple元组，第一个元素表示图像的高度，第二个表示图像的宽度，第三个表示像素的通道数。\n\n### PIL库\n示例代码如下：\n\n```python\n# PIL库读取绘制显示图像\n# plt 用于显示图片\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\nimport numpy as np\nimg_PIL = Image.open('test.jpg')\nimg_PIL = np.array(img_PIL)\n# 打印图像类型,尺寸和总像素个数\nprint(type(img_PIL)) # <class 'numpy.ndarray'>\nprint(img_PIL.shape) # (height, width, channel), (1200, 1793, 3)\nprint(img_PIL.size)  # 6454800 = 1200*1793*3\n# 绘制显示图像\nplt.figure(1)\nplt.imshow(img_PIL)\nplt.show()\n```\n### 读取图像结果分析\n分别用Opnecv3和sckit-image读取图像，并用matplotlib库显示。示例代码如下：\n\n```python\nimport cv2\nfrom skimage import io\nimport matplotlib.pyplot as plt\nimg_cv2 = cv2.imread('test.jpg',cv2.IMREAD_COLOR)\nimg_skimage = io.imread('test.jpg')\n# matplotlib显示cv2库读取的图像\nplt.figure('imread picture',figsize=(25,25))\nplt.subplot(121)\nplt.title('cv2 imread picture')\nplt.imshow(img_cv2)\n# matplotlib显示skimage库读取的图像\nplt.subplot(122)\nplt.title('skimage imread picture')\nplt.imshow(img_skimage)\n# 打印图像尺寸,总像素个数,和图像元素数据类型\nprint(img_cv2.shape)\nprint(img_cv2.size)\nprint(img_cv2.dtype)\n```\n![image](../../data/images/python_read_images/zk5YgqAS7448RIT1Bmq40vrCPHLp3pKrCS-_ilrL6Yw.png)\n\n通过以上输出结果对比图，我们会发现，matplotlib绘制显示的cv2库读取的图像与原图有所差别，这是因为opencv3库读取图像的通道时BGR，而正常图像读取的通道都是RGB，matplotlib库显示图像也是按照RGB顺序通道来的，解释完毕。\n\n一点疑惑，我通过查询库函数可知plt.show()第一个参数为要显示的对象（array\\_like），字面意思理解为类似数组的对象，但是很明显，PIL库返回的不是’numpy.ndarray’对象，而是’PIL.JpegImagePlugin.JpegImageFile’对象，那为什么plt.show()函数还是能显示Image.open()函数读取图像返回的结果呢？\n\n程序如下图所示：\n\n![image](../../data/images/python_read_images/WYCTaeuwXi9ICkRMdEn3K0jvGsddfmr415rW0Wl_UCk.png)\n\n## 打印图像信息\n图像常用信息有图像尺寸，像素个数，通道数等。\n\n### skimage获取图像信息\n**注意：scikit-image 库读取和缩放图像速度要慢 opencv 库 近 4 倍。**\n\n```Plain Text\nfrom skimage import io, data\n# create coffee image, return (300, 451, 3) uint8 ndarray\nimg = data.coffee()\nio.imshow(img)      # 显示图片\nprint(type(img))    # 显示类型\nprint(img.dtype)    # 显示图像元素数据类型\nprint(img.shape)    # 显示尺寸\nprint(img.shape[0]) # 图片高度\nprint(img.shape[1]) # 图片宽度\nprint(img.shape[2]) # 图片通道数\nprint(img.size)     # 显示总像素个数=shape[0]*shape[1]*shape[2]\nprint(img.max())    # 最大像素值\nprint(img.min())    # 最小像素值\nprint(img.mean())   # 像素平均值\nprint(img[0][0])    # 图像第一行第一列的像素值\n```\n输出结果如下图：\n\n![image](../../data/images/python_read_images/En5A4Xt4naL49ocumGSh2pHXKBMxqoud1KVL7blZAmw.png)\n\n### PIL获取图像信息\n```Plain Text\n# 获取PIL image图片信息\nim = Image.open('test.jpg')\nprint (type(im))\nprint (im.size) #图片的尺寸\nprint (im.mode) #图片的模式\nprint (im.format) #图片的格式\nprint (im.getpixel((0,0)))#得到像素：\n# img读出来的图片获得某点像素用getpixel((w,h))可以直接返回这个点三个通道的像素值\n```\n输出结果如下：\n\n![image](../../data/images/python_read_images/cqyXYva-pIgE7adHMXrskm-r2h6ivTP5CQeoNWPiaH8.png)\n\nplt.show函数定义如下：\n\n> Signature: plt.imshow(X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, origin=None, extent=None, shape=None, filternorm=1, filterrad=4.0, imlim=None, resample=None, url=None, hold=None, data=None, \\*\\*kwargs)\nDocstring:\nDisplay an image on the axes.\n\n> Parameters\n———-\nX : array\\_like, shape (n, m) or (n, m, 3) or (n, m, 4). Display the image in XX to current axes. XX may be an array or a PIL image. If XX is an array, it can have the following shapes and types:\n\n> – MxN — values to be mapped (float or int)\n– MxNx3 — RGB (float or uint8)\n– MxNx4 — RGBA (float or uint8)\n\n> The value for each component of MxNx3 and MxNx4 float arrays should be in the range 0.0 to 1.0. MxN arrays are mapped to colors based on the ∥∥∥∥ (mapping scalar to scalar) and the cmapcmap (mapping the normed scalar to a color).\n\n## 读取并显示图像方法总结\n### PIL库读取图像\n> PIL.Image.open + numpy\nscipy.misc.imread\nscipy.ndimage.imread\n这些方法都是通过调用PIL.Image.open 读取图像的信息；\n**PIL.Image.open 不直接返回numpy对象**，可以用numpy提供的函数进行转换，参考Image和Ndarray互相转换；\nscipy.ndimage.imread直接返回numpy.ndarray对象，通道顺序为RGB，通道值得默认范围为0-255。\n\n### Opencv3读取图像\n> cv2.imread: 使用opencv读取图像，直接返回numpy.ndarray 对象，通道顺序为BGR ，注意是**BGR**，通道值默认范围0-255。\n\n### scikit-image库读取图像\n> skimage.io.imread: 直接返回numpy.ndarray 对象，通道顺序为RGB，通道值默认范围0-255。\n\n### 参考资料\n* [https://blog.csdn.net/renelian1572/article/details/78761278](https://blog.csdn.net/renelian1572/article/details/78761278)\n* [https://pillow.readthedocs.io/en/5.3.x/index.html](https://pillow.readthedocs.io/en/5.3.x/index.html)\n* [http://scikit-image.org/docs/stable/user\\_guide.html](http://scikit-image.org/docs/stable/user_guide.html)"
  },
  {
    "path": "2-programming_language/python3/python数据分析-堆叠数组函数总结.md",
    "content": "- [numpy 堆叠数组](#numpy-堆叠数组)\n- [ravel() 函数](#ravel-函数)\n- [stack() 函数](#stack-函数)\n- [vstack()函数](#vstack函数)\n- [hstack()函数](#hstack函数)\n- [concatenate() 函数](#concatenate-函数)\n- [参考资料](#参考资料)\n\n## numpy 堆叠数组\n\n在做图像和 nlp 的数组数据处理的时候，经常需要实现两个数组堆叠或者连接的功能，这就需用到 `numpy` 库的一些函数，numpy 库中的常用**堆叠数组**函数如下：\n\n* `stack` : Join a sequence of arrays along a new axis.\n* `hstack`: Stack arrays in sequence horizontally (column wise).\n* `vstack` : Stack arrays in sequence vertically (row wise).\n* `dstack` : Stack arrays in sequence depth wise (along third axis).\n* `concatenate` : Join a sequence of arrays along an existing axis.\n\n## ravel() 函数\n\nravel() 方法可让将多维数组展平成一维数组。如果不指定任何参数，ravel() 将沿着行（第 0 维/轴）展平/拉平输入数组。\n\n示例代码如下:\n\n```python\nstd_array = np.random.normal(3, 2.5, size=(2, 4))\narray1d = std_array.ravel()\nprint(std_array)\nprint(array1d)\n```\n\n程序输出结果如下：\n\n```shell\n[[5.68301857 2.09696067 2.20833423 2.83964393]\n [2.38957339 9.66254303 1.58419716 2.82531094]]\n \n[5.68301857 2.09696067 2.20833423 2.83964393 2.38957339 9.66254303 1.58419716 2.82531094]\n```\n## stack() 函数\n\nstack() 函数原型是 stack(*arrays*, _axis_=0, _out_=*None*)，功能是沿着**给定轴**连接数组序列，轴默认为第0维，即默认沿着第 0 维 stacks 数组。\n\n1，**参数解析：**\n- arrays: 类似数组（数组、列表）的序列，这里的**每个数组必须有相同的shape。**\n- axis: 默认为整形数据，axis决定了沿着哪个维度stack输入数组。\n\n2，**返回：**\n- stacked : `ndarray` 类型。The stacked array has one more dimension than the input arrays.\n\n实例如下：\n\n```python\nimport numpy as np\n# 一维数组进行stack\na1 = np.array([1, 3, 4])    # shape (3,)\nb1 = np.array([4, 6, 7])    # shape (3,)\nc1 = np.stack((a,b))\nprint(c1)\nprint(c1.shape)    # (2,3)\n# 二维数组进行堆叠\na2 = np.array([[1, 3, 5], [5, 6, 9]])    # shape (2,3)\nb2 = np.array([[1, 3, 5], [5, 6, 9]])    # shape (2,3)\nc2 = np.stack((a2, b2), axis=0)\nprint(c2)\nprint(c2.shape)\n```\n输出为：\n\n> [[1 3 4]\n[4 6 7]]\n\n> (2, 3)\n\n> [[[1 3 5]\n[5 6 9]]\n[[1 3 5]\n[5 6 9]]]\n(2, 2, 3)\n\n可以看到，进行 stack 的两个数组必须**有相同的形状**，同时，输出的结果的维度是比输入的数组都要多一维的。我们拿第一个例子来举例，两个含 3 个数的一维数组在第 0 维进行堆叠，其过程等价于先给两个数组增加一个第0维，变为1*3的数组，再在第 0 维进行 `concatenate()` 操作：\n\n```python\na = np.array([1, 3, 4])\nb = np.array([4, 6, 7])\na = a[np.newaxis,:]\nb = b[np.newaxis,:]\nnp.concatenate([a,b],axis=0)\n```\n输出为：\n\n> array([[1, 2, 3],\n      [2, 3, 4]])\n\n## vstack()函数\nvstack函数原型是vstack(tup)，功能是垂直的（按照行顺序）堆叠序列中的数组。tup是数组序列(元组、列表、数组)，数组必须在所有轴上具有相同的shape，除了第一个轴。1-D arrays must have the same length.\n\n```python\n# 一维数组\na = np.array([1, 2, 3])\nb = np.array([2, 3, 4])\nnp.vstack((a,b))\n```\n> array([[1, 2, 3],\n[2, 3, 4]])\n\n```Plain Text\n# 二维数组\na = np.array([[1], [2], [3]])\nb = np.array([[2], [3], [4]])\nnp.vstack((a,b))\n```\n> array([[1],\n[2],\n[3],\n[2],\n[3],\n[4]])\n\n## hstack()函数\nhstack()的函数原型：hstack(tup) ，参数tup可以是元组，列表，或者numpy数组，返回结果为numpy的数组。它其实就是**水平(按列顺序)**把数组给堆叠起来，与vstack()函数正好相反。举几个简单的例子：\n\n```python\n# 一维数组\na = np.array([1, 2, 3])\nb = np.array([2, 3, 4])\nnp.hstack((a,b))\n```\n> array([1, 2, 3, 2, 3, 4])\n\n```python\n# 二维数组\na = np.array([[1], [2], [3]])\nb = np.array([[2], [3], [4]])\nnp.hstack((a,b))\n```\n> array([[1, 2],\n[2, 3],\n[3, 4]])\n\n**vstack()和hstack函数对比：**\n\n这里的**v**是vertically的缩写，代表垂直（沿着行）堆叠数组，这里的**h**是horizontally的缩写，代表水平（沿着列）堆叠数组。\ntup是数组序列(元组、列表、数组)，数组必须在所有轴上具有相同的shape，除了第一个轴。  \n\n## concatenate() 函数\n\nconcatenate()函数功能齐全，理论上可以实现上面三个函数的功能，concatenate()函数**根据指定的维度，对一个元组、列表中的list或者ndarray进行连接**，函数原型：\n\n```python\nnumpy.concatenate((a1, a2, ...), axis=0)\n```\n\n```python\na = np.array([[1, 2], [3，4]])　　　　　　　　　　　　　　　\nb = np.array([[5, 6], [7, 8]])\n# a、b的shape为（2,2），连接第一维就变成（4,2），连接第二维就变成（2,4）\nnp.concatenate((a, b), axis=0)\n```\n> array([[1, 2],\n[3, 4],\n[5, 6],\n[7, 8]])\n\n**注意：axis指定的维度（即拼接的维度）可以是不同的，但是axis之外的维度（其他维度）的长度必须是相同的**。注意 concatenate 函数使用最广，必须在项目中熟练掌握。\n\n## 参考资料\n- [numpy中的hstack()、vstack()、stack()、concatenate()函数详解](https://mp.weixin.qq.com/s?__biz=MzI1MzY0MzE4Mg==&mid=2247484138&idx=2&sn=f1dca4b3790284371fe103b2108d92a6&chksm=e9d0122bdea79b3d832612ae41a68e120a74764d0b557ffc286da1c4163f397c4e780a77a5b9&mpshare=1&scene=1&srcid=0501xRYNtB00dNBcW8UlLzml#rd)\n\n\n\n"
  },
  {
    "path": "2-programming_language/shell/Shell语法基础.md",
    "content": "- [Shell 变量](#shell-变量)\n\t- [使用变量](#使用变量)\n\t- [只读变量](#只读变量)\n\t- [删除变量](#删除变量)\n\t- [变量类型](#变量类型)\n- [Shell 字符串](#shell-字符串)\n\t- [单引号与双引号字符串](#单引号与双引号字符串)\n\t- [获取字符串长度](#获取字符串长度)\n\t- [提取子字符串](#提取子字符串)\n\t- [拼接字符串](#拼接字符串)\n- [Shell 数组](#shell-数组)\n\t- [定义数组](#定义数组)\n\t- [读取数组](#读取数组)\n\t- [获取数组的长度](#获取数组的长度)\n- [Shell 传递参数](#shell-传递参数)\n- [Shell 基本运算符](#shell-基本运算符)\n\t- [算术运算符](#算术运算符)\n\t- [关系运算符](#关系运算符)\n\t- [布尔运算符](#布尔运算符)\n\t- [逻辑运算符](#逻辑运算符)\n\t- [字符串运算符](#字符串运算符)\n- [Shell 信息输出命令](#shell-信息输出命令)\n\t- [Shell echo 命令](#shell-echo-命令)\n\t- [Shell printf 命令](#shell-printf-命令)\n\t- [%d %s %c %f 格式替代符详解:](#d-s-c-f-格式替代符详解)\n\t- [printf 的转义序列](#printf-的转义序列)\n- [Shell test 命令](#shell-test-命令)\n\t- [数值测试](#数值测试)\n\t- [test 检查文件属性](#test-检查文件属性)\n- [Shell 流程控制](#shell-流程控制)\n\t- [if else](#if-else)\n\t- [if else-if else](#if-else-if-else)\n\t- [for 循环](#for-循环)\n\t- [while 语句](#while-语句)\n- [Shell 函数](#shell-函数)\n\t- [局部变量与全局变量](#局部变量与全局变量)\n\t- [递归函数](#递归函数)\n- [常用命令](#常用命令)\n- [Shell 正则表达式](#shell-正则表达式)\n- [参考资料](#参考资料)\n\n## Shell 变量\n\n在 Shell 脚本中，定义变量直接赋值即可，使用变量时需要在变量名前加美元符号`$`，注意**定义变量时变量名和等号之间不能有空格**。变量名的命名必须遵循以下规则：\n\n+ 命名只能使用英文字母，数字和下划线，首个字符不能以数字开头。\n+ 中间不能有空格，可以使用下划线（_）。\n+ 不能使用标点符号。\n+ 不能使用 bash 里的关键字（可用 help 命令查看保留关键字）。\n\n### 使用变量\n\n使用一个定义过的变量，只要在变量名前面加美元符号即可（推荐给所有变量加上花括号，这是一个好的编程习惯），如：\n\n```Shell\n#!/bin/bash\nmy_name=\"hongghao.zhang\"\necho $my_name\necho ${my_name}\n```\n\n> hongghao.zhang\nhongghao.zhang\n\n### 只读变量\n\n使用 `readonly` 命令可以将变量定义为只读变量，只读变量的值不能被改变。\n\n### 删除变量\n\n使用 `unset` 变量可以删除变量，语法：`unset variable_name`。\n\n### 变量类型\n\n运行 shell 时，会同时存在三种变量：\n1) 局部变量： 局部变量在脚本或命令中定义，仅在当前shell实例中有效，其他shell启动的程序不能访问局部变量。\n2) **环境变量**： **所有的程序，包括shell启动的程序，都能访问环境变量**，有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。\n3) shell变量： shell变量是由shell程序设置的特殊变量。shell变量中有一部分是环境变量，有一部分是局部变量，这些变量保证了shell的正常运行。\n\n## Shell 字符串\n\n字符串是 shell 编程中最常用最有用的数据类型（除了数字和字符串，也没啥其它类型好用了），字符串可以用单引号，也可以用双引号，也可以不用引号。单双引号的区别跟 PHP 类似。\n\n### 单引号与双引号字符串\n\n单引号字符串限制：\n\n+ 单引号里的任何字符都会原样输出，单引号字符串中的变量是无效的；\n+ 单引号字串中不能出现单独一个的单引号（对单引号使用转义符后也不行），但可成对出现，作为字符串拼接使用。\n\n双引号字符串优点：\n\n+ 双引号里可以有变量；\n+ 双引号里可以出现**转义字符**，Shell 脚本程序字符型建议都用双引号。\n\n### 获取字符串长度\n\n```Shell\nstring=\"honggao.zhang\"\necho ${#string}  # 输出13\n```\n\n### 提取子字符串\n\n下面实例从字符串第8个字符开始截取5个字符：\n\n```Shell\nstring=\"honggao.zhang\"\necho ${string:7:5}  # 输出zhang\n```\n\n### 拼接字符串\n\n实际脚本中，拼接字符串可能有以下场景：灵活应用即可。\n\n```python\nyour_name=\"qinjx\"\ngreeting=\"hello, \"$your_name\" !\"\ngreeting_1=\"hello, ${your_name} !\"\necho $greeting $greeting_1\n```\n\n## Shell 数组\n\nbash 支持一维数组，不支持多维数组，并且没有限定数组的大小。类似 C 语言，数组的元素下标也是从 0 开始。获取数组中的元素要利用下标，下标可以是整数或算术表达式，其值应大于或等于 0。 \n\n### 定义数组\n\n在 Shell 中，用括号来表示数组，数组元素用\"空格\"符号分割开。定义数组的一般形式为：\n\n```Shell\n数组名=(值1 值2 ... 值n)\n```\n\n### 读取数组\n\n读取数组元素值的一般格式是：\n```Shell\n${数组名[下表标]}\n```\n使用 @ 符号可以获取数组中的所有元素，例如：\n```Shell\necho ${array_name[@]}\n```\n### 获取数组的长度\n获取数组长度的方法与获取字符串长度的方法相同，例如：\n```Shell\n# 取得数组元素的个数\nlength=${#array_name[@]}\n# 或者\nlength=${#array_name[*]}\n# 取得数组单个元素的长度\nlengthn=${#array_name[n]}\n```\n更多内容参考[shell脚本——字符串 数组](https://blog.csdn.net/weixin_42167759/article/details/80702517)。\n\n## Shell 传递参数\n\n命令行执行 Shell 脚本时，向脚本传递参数，脚本内获取参数的格式为：`$n`。n 代表一个数字，1 为脚本的第一个参数，2 为脚本的第二个参数，以此类推。特殊字符表示的参数如下：\n\n|参数处理 | 说明|\n|--------------|-------|\n|$# |传递到脚本的参数个数|\n|$$\t|脚本运行的当前进程ID号|\n|$!\t|后台运行的最后一个进程的ID号|\n|$\\* |以一个单字符形式显示所有向脚本传递的参数，\"\\$1 \\$2 ... $n\"的形式输出所有参数|\n|$@|与 * 相同，但是使用时加引号，并在引号中返回每个参数。如 `\"$@\"` 用「\"」括起来的情况、以 \"\\$1\" \"\\$2\" … \"\\$n\" 的形式输出所有参数。|\n|$-|显示 Shell 使用的当前选项，与set命令功能相同。|\n|$?|显示最后命令的退出状态。0表示没有错误，其他任何值表明有错误。|\n\n示例代码如下：\n\n```Shell\n#!/bin/bash\n# author:harley\necho \"=== $* 演示 ===\"\nfor i in \"$*\"; do\n    echo $i\ndone\necho \"====$@ 演示===\"\nfor i in \"$@\";do\n    echo $i\ndone\n```\n\n执行脚本，`bash demo1.sh harley zhang hong`，输出结果如下：\n\n```\n=== $* 演示 ===\nharley zhang hong\n====$@ 演示===\nharley\nzhang\nhong\n```\n\n## Shell 基本运算符\n\nShell 支持多种运算符，如下：\n\n+ 算法运算符\n+ 关系运算符\n+ 布尔运算符\n+ 字符串运算符\n+ 文件测试运算符\n\n**字符串判断相等用`=`，数值判断相等用`==`。原生 bash 不支持简单的数学运算**，但是可以通过其他命令来实现，例如 awk 和 expr，expr 最常用。`expr`是一款表达式计算工具，使用它能完成表达式的求值操作。用法如下：\n\n```Shell\n#!/bin/bash\nval=`expr 3 + 6`\necho \"两数之和为：\" ${val}  # 两数之和为：9\n```\n\n注意：\n\n+ **表达式和运算符之间要有空格**，例如 2+2 是不对的，必须写成 2 + 2，这与我们熟悉的大多数编程语言不一样。\n+ **条件表达式都要放在方括号之间，并且要有空格**，例如: [$a==$b] 是错误的，必须写成 [&emsp;\\$a&emsp;==&emsp;\\$b&emsp;]。\n+ 完整的算数表达式要被 \\` \\` 包含，注意这个字符不是常用的单引号，在 Esc 键下边。\n+ **bash 不支持浮点运算**，如果需要进行浮点运算，需要借助 bc,awk 处理。\n\n### 算术运算符\n\n|运算符|说明|举例|\n|---------|-------|-----|\n|+|\t加法\t|`expr $a + $b` 结果为 30|\n|-|\t减法|\t`expr $a - $b` 结果为 -10|\n|\\*|\t乘法|\t`expr $a \\* $b` 结果为 200|\n|/|除法|\t`expr $b / $a` 结果为 2|\n|%|取余|\t`expr $b % $a` 结果为 0|\n|=|赋值|\ta=$b 将把变量 b 的值赋给 a|\n|==|相等|用于比较两个数字，相同则返回 true。 `[ $a == $b ]` 返回 false|\n|!=|不相等|用于比较两个数字，不相同则返回 true。 `[ $a != $b ]` 返回 true|\n\n算数运算符实例脚本如下：\n```shelll\n#!/bin/bash\na=10\nb=20\n\nval=`expr $a + $b`\necho \"a + b : $val\"\nval=`expr $a - $b`\necho \"a - b : $val\"\nval=`expr $a \\* $b`\necho \"a * b : $val\"\n```\n脚本运行结果如下：\n> a + b : 30\na - b : -10\na * b : 200\n\n### 关系运算符\n\n关系运算符只支持数字，不支持字符串，除非字符串的值是数字。Shell 的关系运算符和 C/C++/Python 不一样，它们的大于用 `>` 表示即可，但是 Shell 得用关键字表示，下表列出了常用得关系运算符，假定变量 a 为 10，变量 b 为 20：\n\n|参数|说明|举例|\n|------|-----|-------|\n|-eq|等于则为真|`[ $a -eq $b]`返回false|\n|-ne|不等于则为真|`[ $a -ne $b]`返回true|\n|-gt|大于则为真|`[ $a -gt $b]`返回false|\n|-ge|大于等于则为真|`[ $a -ge $b]`返回false|\n|-lt|小于则为真|`[ $a -lt $b]`返回true|\n|-le|小于等于则为真|`[ $a -le $b]`返回true|\n\n这些关系运算符初学时不必全部记住，编写脚本用到时再来查询也可。\n\n### 布尔运算符\n\n|运算符|说明|举例|\n|---------|------|------|\n|`!`|非运算符，表达式为 true 则返回 false，否则返回 true |`[ ! false ]` 返回 true |\n|`-o`|或运算，有一个表达式为 true 则返回 true |`[ $a -lt 20 -o $b -gt 100 ]` 返回 true |\n|`-a`|与运算，两个表达式都为 true 才返回 true |`[ $a -lt 20 -a $b -gt 100 ]` 返回 false |\n\n实例代码如下：\n```shell\n$ a=120;if [ $a != 120 ];then echo \"a != 120\";else echo \"a == 120\";fi   # ! 运算符的用法\na == 120\n```\n\n### 逻辑运算符\n\n|运算符|说明|举例|\n|---------|------|------|\n| `&&` |逻辑的 AND|`[[ $a -lt 100 && $b -gt 100 ]]`|返回 false |\n| `||` |逻辑的 OR| `[[ $a -lt 100 || $b -gt 100 ]]` |返回 true|\n\n### 字符串运算符\n\n下表列出了常用的字符串运算符，假定变量 a 为 \"abc\"，变量 b 为 \"efg\"：\n\n|运算符|说明|举例|\n|---------|------|------|\n|=|检测两个字符串是否相等，相等返回 true|`[ $a = $b ]` 返回 false|\n|!=|检测两个字符串是否相等，不相等返回 true| `[ $a != $b ]` 返回 true|\n|-z|\t检测字符串长度是否为0，为0返回 true| `[ -z $a ]` 返回 false|\n|-n|\t检测字符串长度是否为0，不为0返回 true| `[ -n \"$a\" ]` 返回 true|\n|\\$|检测字符串是否为空，不为空返回true| `[ $a ]`返回true|\n\n字符串运算符使用示例代码如下：\n```Shell\n#!/bin/bash\na=\"abc\"\nb=\"efg\"\n\nif [ $a = $b ]\nthen\n   echo \"$a = $b : a 等于 b\"\nelse\n   echo \"$a = $b: a 不等于 b\"\nfi\nif [ $a != $b ]\nthen\n   echo \"$a != $b : a 不等于 b\"\nelse\n   echo \"$a != $b: a 等于 b\"\nfi\nif [ -z $a ]\nthen\n   echo \"-z $a : 字符串长度为 0\"\nelse\n   echo \"-z $a : 字符串长度不为 0\"\nfi\nif [ -n \"$a\" ]\nthen\n   echo \"-n $a : 字符串长度不为 0\"\nelse\n   echo \"-n $a : 字符串长度为 0\"\nfi\nif [ $a ]\nthen\n   echo \"$a : 字符串不为空\"\nelse\n   echo \"$a : 字符串为空\"\nfi\n```\n执行脚本，输出结果如下：\n> abc = efg: a 不等于 b\nabc != efg : a 不等于 b\n-z abc : 字符串长度不为 0\n-n abc : 字符串长度不为 0\nabc : 字符串不为空\n\n## Shell 信息输出命令\n\n### Shell echo 命令\n\necho 命令用于字符串的输出，`echo`打印字符串默认换行。\n\n### Shell printf 命令\n\nprintf 命令和 echo 命令类似，都是用于信息的输出。\n\n+ printf 命令模仿 C 程序库（library）里的 printf() 程序。\n+ printf 由 POSIX 标准所定义，因此使用 printf 的脚本比使用 echo 移植性好。\n+ printf 使用引用文本或空格分隔的参数，外面可以在 printf 中使用格式化字符串，还可以制定字符串的宽度、左右对齐方式等。默认 printf 不会像 echo 自动添加换行符，我们可以手动添加 `\\n`。\n\n`printf` 命令语法如下：\n```\nprintf format-string [arguments...]\n```\n**参数说明**：\n+ format-string: 为格式控制字符串\n+ arguments:为参数列表\n\n示例程序如下：\n```Shell\n#!/bin/bash\nprintf \"%-10s %-8s %-4s %12s\\n\" 姓名 性别 体重kg 学号\nprintf \"%-10s %-8s %-4.2f %12d\\n\" 郭靖 男 66.1234 2017210675\nprintf \"%-10s %-8s %-4.2f %12d\\n\" 杨过 男 48.6543 2017210688\nprintf \"%-10s %-8s %-4.2f %12d\\n\" 郭芙 女 47.9876 2017210889\n```\n执行脚本，程序输出如下：\n> 姓名 性别 体重kg 学号\n郭靖 男 66.12 2017210675\n杨过 男 48.65 2017210688\n郭芙 女 47.99 2017210889\n\n格式控制字符串解释：\n+ %s %c %d %f都是格式替代符\n+ %-10s 指一个宽度为10个字符（-表示左对齐，没有则表示右对齐），任何字符都会被显示在10个字符宽的字符内，如果不足则自动以空格填充，超过也会将内容全部显示出来。\n+ %-4.2f 指格式化为小数，其中**.2指保留2位小数**。\n\n### %d %s %c %f 格式替代符详解:\n\n+ d: Decimal 十进制整数 -- 对应位置参数必须是十进制整数，否则报错！\n+ s: String 字符串 -- 对应位置参数必须是字符串或者字符型，否则报错！\n+ c: Char 字符 -- 对应位置参数必须是字符串或者字符型，否则报错！\n+ f: Float 浮点 -- 对应位置参数必须是数字型，否则报错!\n\n### printf 的转义序列\n\n|序列|说明|\n|-------|-----|\n|`\\a`| 警告字符，通常为ASCII的BEL字符|\n|`\\f`|换页|\n|`\\n`|换行|\n|`\\t`|水平制表符|\n|`\\r`|回车|\n\n## Shell test 命令\n\nShell 中的 test 命令用于检查某个条件是否成立，可以进行数值、字符和文件三个方面的测试。\n\n### 数值测试\n这是关系运算符，只支持数字，不支持字符串，除非字符串的值是数字。\n\n|参数|说明|\n|------|-----|\n|-eq| 等于则为真|\n|-ne|\t不等于则为真|\n|-gt|\t大于则为真|\n|-ge|\t大于等于则为真|\n|-lt|\t小于则为真|\n|-le|\t小于等于则为真|\n\n符号含义：\n1. eq （equal的缩写），表示等于为真\n2. ne (not equal的缩写），表示不等于为真\n3. gt (greater than的缩写），表示大于为真\n4. ge （greater&equal的缩写），表示大于等于为真\n5. lt （lower than的缩写），表示小于为真\n6. le （lower&equal的缩写），表示小于等于为真\n\n实例代码如下：\n```Shell\n#!/bin/bash\n# 关系运算符判断\nnum1=100\nnum2=333\nif test $num1 -eq $num2\nthen\n  echo \"两个数相等\"\nelse\n  echo \"两个数不相等\"\nfi\n# 算术运算符判断\nstr1=\"honggao\"\nstr2=\"hong.hao\"\necho \"传递的参数为: $*\"\nif [ $1 = $2i ]\nthen\n  echo \"两个输入字符串相等\"\nelse\n  echo \"输入的两个字符串不相等\"\nfi\n```\n执行脚本(`sh comm_test.sh eere wdwe2`)，输出如下：\n> 两个数不相等\n传递的参数为: eere wdwe2\n输入的两个字符串 不相等\n\n### test 检查文件属性\n\n检查文件属性也是 `test` 的常见用法，比如检查一个文件类型是不是普通文件，可以使用 `-f` 选项，检查路径是否是目录可以用 `-d` 选项：\n```python\ntouch test.sh\nfilename=\"test.sh\"\n# 检查文件\nif test -f \"$filename\";then\n    echo \"It's a regular file.\"\nfi\n# 检查目录\ndirname=\"test_directory\"\nmkdir $dirname\nif test -d \"$dirname\";then\n    echo \"It's a directory.\"\nfi\n```\n\n运行脚本，输出如下：\n\n![test命令输出](../../data/images/test命令测试.png)\n\n`test` 命令是shell编程中非常重要的命令，一定要掌握！下面是其他一些常用的文件检查运算符：\n\n```shell\n-b file : 文件存在并且是块设备文件。\n-c file : 文件存在并且是字符设备文件。\n-d file : 文件存在并且是一个目录。\n-e file : 文件存在。\n-f file : 文件存在并且是一般文件。\n-g file : 文件存在并且设置了 setgid 位。\n-h file : 文件存在并且是一个链接文件。\n-p file : 文件存在并且是一个命名管道(FIFO)。\n-r file : 文件存在并且是可读的。\n-s file : 文件存在并且有内容。\n-u file : 文件存在并且设置了 setuid。\n-w file : 文件存在并且是可写的。\n-x file : 文件存在并且是可执行的。\n-S file : 文件存在并且是一个 socket。\n```\n## Shell 流程控制\n\n`Shell` 的流程控制不可为空。\n\n### if else\n\nif else语法格式：\n\n```Shell\nif condition\nthen\n    command1\n    command2\n    command3\nelse\n    command\nfi\n```\n\n### if else-if else\n\n`if else-if else` 语法格式如下：\n```Shell\nif condition1\nthen\n    command1\nelif condition2\nthen\n    command2\nelse\n    commandN\nfi\n```\n\n根据 width、height 计算 BMI 指数脚本实例代码如下：\n\n```Shell\necho \"pleae input your weight and height\"  # 无法支持输入小数\npf=`expr $2 \\* $2`\nbmi=`expr $1 / $pf`\necho \"your bmi is: $bmi\"\na=18\nb=25\nc=28\nd=32\nif [ $bmi -le $a ]\nthen\n  echo \"体重过轻\"\nelif [ $bmi -le $b ]\nthen\n  echo \"体重正常\"\nelif [ $bmi -le $c ]\nthen\n  echo \"体重过重\"\nelif [ $bmi -le $d ]\nthen\n  echo \"体重肥胖\"\nelif [ $bmi -gt $d ]\nthen\n  echo \"严重肥胖\"\nfi\n```\n\n执行脚本(`sh if_else.sh 64 2`)，程序输出如下：\n> pleae input your weight and height\nyour bmi is: 16\n体重过轻\n\n### for 循环\n\nfor 循环格式为：\n```Shell\nfor var in item1 item2 ... itemN\ndo\n    command1\n    command2\n    ...\n    commandN\ndone\n```\n\n### while 语句\n\nwhile 循环用于不断执行一系列命令，也可用于从输入文件中读取数据；命令通常为测试条件，其格式为：\n```Shell\nwhile condition\ndo\n    command\ndone\n```\n\n## Shell 函数\n\nshell 函数中的定义格式如下：\n\n```python\n[ function ] funname [()]\n{\n    action;\n    [return int;]\n}\n```\n\n参数说明：\n+ 可以带 function fun() 定义，也可以直接 `fun()` 定义,不带任何参数。\n+ 执行函数直接使用 `funname` 即可。\n\n### 局部变量与全局变量\n\n```python\n# !/bin/bash\na=\"this is a\" # 定义全局变量\nb=\"this is b\"\nfunction funname() {\n    local_c=\"this is c\" # 定义局部变量\n    echo $a, $b\n    echo $local_c\n    return 0   # shell 函数返回值是整形，并且在 0-257 之间\n}\necho $d  # 打印不会生效，因为 d 是局部变量\nfunname  # 执行函数 funname\n```\n执行上诉程序 `bash fun_test.sh`，输出如下：\n> this is a, this is b\nthis is c\n\n### 递归函数\n\nbash 也是支持递归函数的（能够调用自身的函数），示例程序如下：\n\n```python\n#!/bin/bash\nfunction name() {\n    echo $1\n    name hello\n    sleep 1\n}\nname\n```\n\n运行此脚本后不断打印出 hello，按 `ctrl+c` 结束。\n\n## 常用命令\n\n`ps、grep、awk、sed` 三剑客\n\n## Shell 正则表达式\n\n参考博客[Shell 正则表达式](https://man.linuxde.net/docs/shell_regex.html)。\n\n## 参考资料\n\n- [菜鸟教程-shell教程](https://www.runoob.com/linux/linux-shell-process-control.html)\n- [Linux 命令行与 Shell 脚本教程](Linux 命令行与 Shell 脚本教程)"
  },
  {
    "path": "3-machine_learning/README.md",
    "content": "## 前言\n\n本目录内容旨在分享机器学习数学基础笔记、机器学习算法总结和机器学习经典面试题总结知识。\n\n## 目录\n\n1. [机器学习基本概念总结](./机器学习基本概念总结.md)\n2. [机器学习基础总结](./机器学习基础总结.md)\n3. [机器学习经典算法总结](./机器学习经典算法总结.md)\n4. [机器学习基本原理](./机器学习基本原理.md)\n5. [《机器学习》学习笔记](./《机器学习》学习笔记.md)"
  },
  {
    "path": "3-machine_learning/k-means.py",
    "content": "import numpy as np\nfrom numpy import inf\nfrom matplotlib import pyplot as plt\nimport random\n\ndef distEclud(vecA, vecB):\n    \"两个向量的欧式距离计算公式\"\n    return np.sqrt(sum(np.power(vecA - vecB, 2)))\n\ndef kmeans(dataset, k):\n    \"\"\"K-means 聚类算法\n\n    Args:\n        dataset ([ndarray]): 数据集，二维数组\n        k ([int]): 聚簇数量\n    \"\"\"\n    m = np.shape(dataset)[0]  # 样本个数\n    \n    # 1, 随机初始化聚类中心点\n    center_indexs = random.sample(range(m), k)\n    center = dataset[center_indexs,:]\n    \n    cluster_assessment = np.zeros((m, 2))\n    cluster_assessment[:, 0] = -1  # 将所有的类别置为 -1\n    cluster_changed = True \n    while cluster_changed:\n        cluster_changed = False\n        # 4-8，计算样本x_i与各聚类中心的距离，根据距离最近的聚类中心确定x_j的簇标记，并将对应样本x_i划入相应的簇\n        for i in range(m):\n            # 初始化样本和聚类中心的距离，及样本对应簇\n            min_dist = inf\n            c = 0\n            # 确定每一个样本离哪个中心点最近，和属于哪一簇\n            for j in range(k):\n                dist = distEclud(dataset[i,:], center[j,:])\n                if dist < min_dist:\n                    min_dist = dist\n                    c = i\n            # 更新样本所属簇\n            if cluster_assessment[i, 0] != c:  # 仍存在数据在前后两次计算中有类别的变动，未达到迭代停止要求\n                cluster_assessment[i, :] = c, min_dist\n                cluster_changed = True\n        # 9-15 更新簇中心点位置\n        for j in range(k):\n            changed_center = dataset[cluster_assessment[:,0] == j].mean(axis=0)\n            center[j,:] = changed_center\n            \n    return cluster_assessment, center\n\ndef show_cluster(dataSet, k, centroids, clusterAssement):\n    \"\"\"\n    针对二维数据进行聚类结果绘图\n    \"\"\"\n    numSamples, dim = dataSet.shape\n    mark = ['or', 'ob', 'og', 'ok', '^r', '+r', '<r', 'pr']\n    center_mark = ['*r', '*b', '*g', '*k', '*r', '*r', '*r', '*r']\n\n    for i in range(numSamples):\n        markIndex = int(clusterAssement[i,0])\n        plt.plot(dataSet[i, 0], dataSet[i, 1], mark[markIndex], markersize=2)\n    for j in range(k):\n        plt.plot(centroids[j, 0], centroids[j, 1], center_mark[j], markersize=12)\n    plt.show()\n    \n\nif __name__ == '__main__':\n    x1 = np.random.randint(0, 50, (50, 2))\n    x2 = np.random.randint(40, 100, (50, 2))\n    x3 = np.random.randint(90, 120, (50, 2))\n    x4 = np.random.randint(110, 160, (50, 2))\n    test = np.vstack((x1, x2, x3, x4))\n\n    # 对特征进行聚类\n    result, center = kmeans(test, 4, is_kmeans=False, is_random=False)\n    print(center)\n    # show_cluster(test, 4, center, result)  # 报错"
  },
  {
    "path": "3-machine_learning/《机器学习》学习笔记.md",
    "content": "- [第 2 章 模型评估与选择](#第-2-章-模型评估与选择)\n  - [2.1 经验误差与过拟合](#21-经验误差与过拟合)\n  - [2.2 评估方法](#22-评估方法)\n    - [2.2.1 留出法](#221-留出法)\n    - [2.2.2 交叉验证法](#222-交叉验证法)\n    - [2.2.3 自助法](#223-自助法)\n    - [2.2.4 调参与最终模型](#224-调参与最终模型)\n  - [2.3 性能度量](#23-性能度量)\n    - [2.3.1 错误率与精度](#231-错误率与精度)\n    - [2.3.2 查准率、查全率与F1](#232-查准率查全率与f1)\n    - [2.3.3 ROC 与 AUC](#233-roc-与-auc)\n  - [2.5 偏差与方差](#25-偏差与方差)\n- [第 3 章 线性模型](#第-3-章-线性模型)\n  - [3.5 多分类学习](#35-多分类学习)\n  - [3.6 类别不平衡问题](#36-类别不平衡问题)\n- [第 5 章 神经网络](#第-5-章-神经网络)\n  - [5.1 神经元模型](#51-神经元模型)\n  - [5.2 感知机与多层网络](#52-感知机与多层网络)\n  - [5.3 误差反向传播算法](#53-误差反向传播算法)\n  - [5.4 全局最小与局部最小](#54-全局最小与局部最小)\n  - [5.5 其他常见神经网络](#55-其他常见神经网络)\n  - [5.6 深度学习](#56-深度学习)\n- [第 9 章 聚类](#第-9-章-聚类)\n  - [9.1 聚类任务](#91-聚类任务)\n  - [9.2 性能度量](#92-性能度量)\n  - [9.3 距离计算](#93-距离计算)\n  - [9.4 原型聚类](#94-原型聚类)\n    - [9.4.1 k 均值算法](#941-k-均值算法)\n- [参考资料](#参考资料)\n\n## 第 2 章 模型评估与选择\n### 2.1 经验误差与过拟合\n\n+ 精度：精度=1-错误率。如果在 $m$ 个样本中有 $a$ 个样本分类错误，则错误率 $E=a/m$，精度 = $1-a/m$。\n+ **误差**：一般我们把学习器的实际预测输出与样本的真实输出之间的差异称为“误差”（`error`）。学习器在训练集上的误差称为“训练误差”（`training error`），在**新样本**上的误差称为“泛化误差”（`generalization error`）。\n+ **过拟合**：就是指训练误差和测试误差间距过大，即方差（`variance`）过大，表现为模型泛化性能下降即不够”稳“，正则化目的在于解决过拟合问题。\n+ **欠拟合**：和过拟合相反，指模型的训练误差过大，即偏差（`bias`）过大，表现为模型不够”准“，优化算法目的在于解决欠拟合问题。\n\n好的学习器应该尽可能学出适用于所有潜在样本的”普遍规律“。由于事先无法知道新样本是什么样子，所以无法直接获得泛化误差，同时训练误差又由于过拟合现象的存在而不适合作为标准，那么现实中如何进行模型评估与选择就是一个重要的问题了。\n\n### 2.2 评估方法\n\n通常使用一个测试集来评估学习器对新样本的判别能力，把测试集上的”测试误差“（`testing error`）作为泛化误差的近似。值得注意的是，测试集应该尽可能与训练集互斥，即测试样本尽量不在训练集中出现（学习器之前没有见到过）。\n\n对于一个包含 $m$ 个样本的数据集 $D = {(x_1, y_1)}, (x_2, y_2),...,(x_m, y_m)$，将其划分为训练集 $S$ 和测试集 $T$，有两种常见的方法：**留出法和交叉验证法**。\n\n#### 2.2.1 留出法\n\n”留出法“（`hold-out`） 直接将数据集 $D$ 划分为两个户次互斥的集合，一个集合作为训练集 $S$，另一个作为测试集 $T$，即 $D = S \\cup T$，$S \\cap T = \\emptyset$。值得注意的是，训练/测试集的划分应该尽可能保持数据分布的一致性，避免数据划分过程引入额外的偏差而对最终结果产生影响。从采样(`sampling`)的角度来看数据集的划分过程，则保留类别比例的采样方式称为”分层采样“（`stratified sampling`）。例如数据集 $D$ 有 `1000` 个样本，`800` 个正样本，`200` 个负样本，将 `70%` 的样本作为训练集，`30%` 的样本作为测试集。考虑分层采样，则训练 $S$ 集的构成为：正样本 $= 1000\\times 70\\% \\times (800/1000) = 560$，负样本 $=1000\\times 70\\% \\times (200/1000) = 140$，同理可得测试集 `T` 包含 `240` 个正样本，`60` 个负样本。\n\n另一个问题是即使在给定训练集/测试集样本比例后，依然存在多种划分方式对样本 $D$ 进行分割，例如前 `280` 个正样本和后 `280` 个正样本构建的训练集是不一样的。因此，单次使用留出法得到的的估计结果往往不可靠，在使用留出法时，一般采用若干次随机划分、重复进行实验评估后取平均值作为留出法的评估结果。\n\n#### 2.2.2 交叉验证法\n\n”交叉验证法“（`cross validation`）先将数据集 $D$ 划分为 $k$ 个大小相似的**互斥子集**，即 $D = D_1\\cup D_2\\cup...\\cup D_k，D_i \\cap D_j = \\emptyset$。同时每个子集都应该尽可能保持**数据分布的一致性**，即类别比例相等，从 $D$ 中通过分层采样得到。然后，每次用 $k-1$ 个子集的并集作为训练集，剩下的一个子集作为测试集，这样可获得 $k$ 组训练集/测试集，从而可进行 $k$ 组训练和测试，把 $k$ 次测试结果取平均后返回。交叉验证法评估结果的稳定性和保真性在很大程度上取决于 $k$ 的取值，为了强调这一点，通常把交叉验证法称为“$k$ 折交叉验证”（`k-fold cross validation`）。$k$ 的常用取值分别是 `10、5、20` 等。图 `2.2` 给出了 `10` 折交叉验证的示意图。\n\n![10折交叉验证示意图](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221043341-133674568.png)\n\n“10 次 10 折交叉验证法”与 “100 次留出法“都是进行了 `100` 次训练/测试。\n\n交叉验证法的一个特例是留一法（`Leave-One-Out`，简称 `LDO`），留一法不受随机样本划分方式的影响，因为 $m$ 个样本只有唯一的划分方式划分为 $m$ 个子集-每个子集包含一个样本。虽然留一法的评估结果往往被认为比较准确，但是训练开销简直太大了，真实项目中很少见到有人这样用，而且留一法的估计结果未必永远比其他评估方法准确，毕竟“天下没有免费的午餐”。\n\n#### 2.2.3 自助法\n\n“自助法”（`bootstrapping`） ：给定包含 $m$ 个样本的数据集 $D$，对它进行采样产生数据集 ${D}'$：每次随机从 $D$ 中挑选一个样本，将其拷贝放入 ${D}'$，然后再将其放回 $D$ 中，下次采样依然可能被采样到；这个过程重复 $m$ 次，就得到了包含 $m$ 个样本的数据集 ${D}'$。显然，$D$ 中有一部分样本会在 ${D}'$ 中多次出现，另外一部分样本不出现。做一个简单估计，样本在 $m$ 次采样中始终不被采到的概率是 $(1-\\frac{1}{m})^m$，取极限得到如下所式:\n\n$$\n(1-\\frac{1}{m})^m \\rightarrow \\frac{1}{e}\\approx 0.368\n$$\n\n即通过自助采样，初始数据集 $D$ 中约有 `36.8%` 的样本未出现在采样数据集 ${D}'$ 中。\n\n自助法只适用于数据集较小、难以有效划分训练/测试集的情况。值得注意的是，自助法产生的数据集改变了初始数据集的分布，这回引入估计偏差。\n\n#### 2.2.4 调参与最终模型\n\n需要认为设定的参数称为超参数，模型训练无法优化它的。现实当中常对每个参数选定一个范围和变化补偿，例如在 `[0,0.2]` 范围内以 `0.05` 为补偿，要评估的的候选参数有 `5` 个最终选定的参数从这 `5` 个候选值中产生，结果也许不是最佳值，但这是计算开销和性能估计之间进行折中的结果。\n\n在模型评估与选择过程中由于需要流出一部分数据进行评估测试，事实上我们只使用了一部分数据训练模型。因此，在模型选择完成后，学习算法和参数配置已定，此使应该用数据集 $D$ 重新训练模型。这个模型在训练过程中使用了**所有 $m$ 个训练样本**，这个模型也是最终提交给用户的模型。\n\n另外，值得注意的是，我们通常把学得模型在实际使用过程中遇到的数据集称为测试数据，为了加以区分，前面讲到的模型评估与选择中用于评估测试的数据常称为“验证集（`validation set`）”。\n\n在研究对比不同算法的泛化性能时，我们用测试集上的判别效果来估计模型在实际使用时的泛化能力，而把训练数据另外划分为训练集和验证集，基于验证集上的性能来进行模型选择和调参。\n\n### 2.3 性能度量\n\n对学习器的泛化性能进行评估，不仅需要有效可行的实验估计方法，还需要衡量模型泛化能力的评价标准，这就是性能度量（`performance measure`）。\n\n在预测任务中，给定样本集 $D={(x_1,y_1),(x_2,y_2),...,(x_m,y_m)}$，其中 $y_i$ 是示例 $x_i$ 的真实标签。要评估学习器 $f$ 的性能，需要把学习器预测结果 $f(x)$ 与真实标签 $y$ 进行比较。\n\n回归任务最常用的性能度量是“均方误差”（`mean squared error`）。\n\n$$E(f;D) = \\frac{1}{m}\\sum_{i=1}^{m}(f(x_i)-y_i)^2$$\n\n#### 2.3.1 错误率与精度\n\n+ 错误率：分类错误的样本数占样本总数的比例；\n+ 精度：分类正确样本的样本数占样本总数的比例。\n\n错误率和精度是分类任务中最常用的两种性能度量，适用于二分类，也适用于多分类任务。分类错误率和精度的定义如下：\n\n$$E(f;D) = \\frac{1}{m}\\sum_{i=1}^{m}(f(x_i) \\neq y_i)^2$$\n$$acc(f;D) = \\frac{1}{m}\\sum_{i=1}^{m}(f(x_i) = y_i)^2 = 1 - E(f;D)$$\n\n#### 2.3.2 查准率、查全率与F1\n\n错误率和精度虽然常用，但是并不能满足所有任务需求。比如以西瓜问题为例，假设瓜农拉来一车西瓜，我们用训练好的模型对西瓜进行判别，现如精度只能衡量有多少比例的西瓜被我们判断类别正确（两类：好瓜、坏瓜）。但是若我们更加关心的是“挑出的西瓜中有多少比例是好瓜”，或者”所有好瓜中有多少比例被挑出来“，那么精度和错误率这个指标显然是不够用的。\n\n对于**二分类**问题，可将样例根据真实类别与学习器预测类别的组合划分为真正例（`true positive`）、假正例（`false positive`）、真反例（`true negative`）、假反例（`false negative`）四种情况，令 $TP、FP、TN、FN$ 分别表示其对应的样例数，显然有 $TP+FP+TN+FN = $ 样例总数。分类结果的”混淆矩阵“（`confusion matrix`）如下表所示。\n\n![混淆矩阵](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221043869-2084291202.png)\n\n查准率（精确率） $P$ 与查全率（召回率） $R$ 分别定义为：\n\n$$\nP = \\frac{TP}{TP+FP} \\\\\nR = \\frac{TP}{TP+FN}\n$$\n\n查准率和查全率是一对矛盾的的度量。一般来说，查全率高时，查准率往往偏低；而查全率高时，查准率往往偏低。通常只有在一些简单任务中，才可能使查全率和查准率都很好高。\n\n精准率和召回率的关系可以用一个 `P-R` 图来展示，以查准率 `P` 为纵轴、查全率 `R` 为横轴作图，就得到了查准率－查全率曲线，简称 **P-R** 曲线，`PR` 曲线下的面积定义为 `AP`。\n\n![PR曲线图](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221044321-1100755715.png)\n> 为了绘图方便和美观，示意图显示出单调平滑曲线，但现实任务中的 `P-R` 曲线是非单调、不平滑的，且在很多局部有上下波动。\n\n**如何理解 P-R 曲线呢**？\n\n> 可以从排序型模型或者分类模型理解。以逻辑回归举例，逻辑回归的输出是一个 `0` 到 `1` 之间的概率数字，因此，如果我们想要根据这个概率判断用户好坏的话，我们就必须定义一个阈值 。通常来讲，逻辑回归的概率越大说明越接近 `1`，也就可以说他是坏用户的可能性更大。比如，我们定义了阈值为 `0.5`，即概率小于 `0.5` 的我们都认为是好用户，而大于 `0.5` 都认为是坏用户。因此，对于阈值为 `0.5` 的情况下，我们可以得到相应的**一对**查准率和查全率。\n但问题是：这个阈值是我们随便定义的，我们并不知道这个阈值是否符合我们的要求。 因此，为了找到一个最合适的阈值满足我们的要求，我们就必须遍历 `0` 到 `1` 之间所有的阈值，而每个阈值下都对应着一对查准率和查全率，从而我们就得到了 `PR` 曲线。\n最后如何找到最好的阈值点呢？ 首先，需要说明的是我们对于这两个指标的要求：我们希望查准率和查全率同时都非常高。 但实际上这两个指标是一对矛盾体，无法做到双高。图中明显看到，如果其中一个非常高，另一个肯定会非常低。选取合适的阈值点要根据实际需求，比如我们想要高的查全率，那么我们就会牺牲一些查准率，在保证查全率最高的情况下，查准率也不那么低。\n\n在进行性能比较时，如果一个学习器的曲线被另一个学习器的曲线完全包住，则可断言后者性能优于前者，如图 2.3，学习器 `A` 性能优于学习器 `C`。比较 `P-R` 曲线下面积的大小，也可判别学习器的优劣，它在一定程度上表征了学习器在查准率和查全率上取得”双高“的比例，但这个值不容易估算，因此人们又设计了一些综合考虑查准率、查全率的性能度量，如”平衡点（`Break-Event Point`，简称 `BEP`）“。\n\n`BEP` 是”查准率=查全率“时的取值，基于学习器的比较，可以认为学习器 `A` 优于 `B`。但 `BEP` 过于简单了，更为常用的是 $F1$ 度量。\n\n$$\nF1 = \\frac{2\\times P\\times R}{P+R} = \\frac{2\\times TP}{样例总数+TP-TN}\n$$\n\n$F1$ 度量的一般形式：$F_{\\beta}$，能让我们表达出对查准率/查全率的偏见，$F_{\\beta}$ 计算公式如下：\n\n$$\nF_{\\beta} = \\frac{1+\\beta^{2}\\times P\\times R}{(\\beta^{2}\\times P)+R}\n$$\n\n其中 β > 1 对查全率有更大影响，$\\beta < 1$ 对查准率有更大影响。\n\n很多时候我们会有多个混淆矩阵，例如进行多次训练/测试，每次都能得到一个混淆矩阵；或者是在多个数据集上进行训练/测试，希望估计算法的”全局“性能；又或者是执行多分类任务，**每两两类别**的组合都对应一个混淆矩阵；....总而来说，我们希望能在 $n$ 个二分类混淆矩阵上综合考虑查准率和查全率。\n\n一种直接的做法是先在各混淆矩阵上分别计算出查准率和查全率，记为$(P_1,R_1),(P_2,R_2),...,(P_n,R_n)$然后取平均，这样得到的是”宏查准率（`Macro-P`）“、”宏查准率（`Macro-R`）“及对应的”宏$F1$（`Macro-F1`）“：\n\n$$\\begin{aligned}\nMacro\\ P &= \\frac{1}{n}\\sum_{i=1}^{n}P_i \\\\\nMacro\\ R &= \\frac{1}{n}\\sum_{i=1}^{n}R_i \\\\\nMacro\\ F1 &= \\frac{2 \\times Macro\\ P\\times Macro\\ R}{Macro\\ P + Macro\\ R}\n\\end{aligned}$$\n\n另一种做法是将各混淆矩阵对应元素进行平均，得到 $TP、FP、TN、FN$ 的平均值，再基于这些平均值计算出”微查准率“（`Micro-P`）、”微查全率“（`Micro-R`）和”微$F1$“（`Mairo-F1`）\n\n$$\\begin{aligned}\nMicro\\ P &= \\frac{\\overline{TP}}{\\overline{TP}+\\overline{FP}} \\\\\nMicro\\ R &= \\frac{\\overline{TP}}{\\overline{TP}+\\overline{FN}} \\\\\nMicro\\ F1 &= \\frac{2 \\times Micro\\ P\\times Micro\\ R}{MacroP+Micro\\ R}\n\\end{aligned}$$\n\n#### 2.3.3 ROC 与 AUC\n\n`ROC` 曲线示意图如下。\n\n![PR曲线图](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221044929-398011469.png)\n\n### 2.5 偏差与方差\n\n通过前面的学习，我们已经知道如何设计实验（训练集、验证集的划分）来**对学习器的泛化误差进行评估**，并且也了解了诸如**精度、查准率、查全率及 `F1`** 等性能度量指标。但这不够，我们还希望了解”为什么“具有这样的性能。”偏差-方差分解“（`bias-variance decomposition`）是解释学习算法泛化性能的一种重要工具。\n\n假设对于测试样本 $x$,令 $y_D$ 为 $x$ 在数据集中的标记（即标签，可能会存在噪声），$y$ 才是 $x$ 的真实标记，$f(x;D)$ 为训练集 $D$ 上学得模型 $f$ 在 $x$ 上的预测输出。以回归任务为例，**算法的泛化误差**计算如下：\n\n$$\nE(f;D) = \\mathbb{E}_{D} [(f(x;D) - y_{D})^2]\n$$\n\n直接计算上式是不行的，我们得进行分解，分解之前需要先知道方差、偏差、噪声的定义。学习算法的**期望预测**为\n\n$$\n\\bar{f}(x) = \\mathbb{E}_{D} [(f(x;D)]\n$$\n\n**方差定义**: 模型预测的期望值与预测值之差的平方的期望值，使用样本数相同的不同训练集产生的**方差**为\n\n$$\nD(x) = var(x) = \\mathbb{E}_{D}[(f(x;D) - \\mathbb{E}_{D}[(f(x;D)])^2] = \\mathbb{E}_{D}[(f(x;D) - \\bar{f}(x))^2]\n$$\n\n> 方差在统计描述和概率分布中各有不同的定义，并有不同的公式。\n\n**噪声**为\n\n$$\n\\varepsilon^2 = \\mathbb{E}_{D}[(y_{D} - y)^2]\n$$\n\n模型预测的期望值与真实值的差称为**偏差**（`bias`），即\n\n$$\nbias^2(x) = (\\bar{f}(x)-y)^2\n$$\n\n假定噪声为 `0` ，即 $\\mathbb{E}_{D}[(y_{D}-y)]=0$，有了以上定义，通过多项式展开合并，并利用恒等变形、期望的运算性质可将期望泛化误差公式进行分解得到：\n> 公式推理证明，可参考[这里](https://datawhalechina.github.io/pumpkin-book/#/chapter2/chapter2?id=_241)。\n\n$$\nE(f;D) = \\mathbb{E}_{D}[(f(x;D)-\\bar{f}(x))^2] + (\\bar{f}(x)-y)^2 + \\mathbb{E}_{D}[(y_{D}-y)^2]\n$$\n\n于是，\n\n$$\nE(f;D) = var(x) + bias^2(x) + \\varepsilon^2\n$$\n\n通过上式，可知**泛化误差可分解为方差、偏差与噪声之和**。回顾偏差、方差、噪声的定义：\n\n+ **偏差**：度量了学习算法的期望预测与真实结果的偏离程度，刻画了学习算法本身的拟合能力。\n+ **方差**：度量了同样大小的训练集的变动导致的学习性能的变化，刻画了数据扰动所造成的影响，或者说刻画了模型的稳定性和泛化能力。\n+ **噪声**：表达了当前任务上任何学习算法所能达到的期望泛化误差的下界，即刻画了学习问题本身的难度。\n\n通过对泛化误差进行分解说明，模型泛化性能是由学习算法的能力、数据的充分性以及任务本身的难度所共同决定的。**模型欠拟合**表现为偏差较大, 此时偏差主导了泛化错误率；**模型过拟合**表现为偏差小但是方差很大，方差主导了泛化错误率。\n\n一般来说，偏差与方差是有冲突的，这称为偏差-方差窘境（`bias-variane dilemma`）。\n\n![泛化误差与偏差方差的关系示意图](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221045355-343515804.png)\n\n## 第 3 章 线性模型\n\n### 3.5 多分类学习\n\n### 3.6 类别不平衡问题\n\n$$\\frac{y'}{1-y'} = \\frac{y}{1-y}\\times \\frac{m^-}{m^+}$$\n\n公式中，$y$ 是预测值，分类器决策规则为：若 $\\frac{y}{1-y}$ 则预测为正例。令 $m^+$ 表示正例数目， $m^-$ 表示反例数目，则观测几率是 $\\frac{m^-}{m^+}$。\n\n类别不平衡学习的一个基本策略是 **“再缩放”**（rescaling）。再缩放技术主要有三种方法实现：\n\n+ 对训练集的反类样例进行“欠采样”（undersampling），即去除一些反例使得正负样例数目接近，然后再进行学习。\n+ 对训练集的正类样例进行“过采样”（oversampling），即增加一些正例使得正、负样例数目接近，然后再进行学习（或者叫模型训练）。\n+ 直接对原始训练集进行学习，但是在对训练好的分类器进行预测时，将式（3.48）嵌入到分类器推理过程中，称为“阈值移动”（threshold-moving）。\n\n值得注意的是过采样的方法不能简单地对初始正例样本进行重复采样，否则会招致严重的过拟合；过采样方法的代表性算法 SMOTE 是通过对训练集的正例进行插值来产生额外的正例（图像数据就进行数据增强）。\n\n## 第 5 章 神经网络\n\n### 5.1 神经元模型\n\n神经网络有很多种定义，一种较为广泛的定义是，其是由具有适应性的简单单元组成的广泛并行互连的网络，它的组织能够模拟生物神经系统对真实世界物体所作出的交互反应。\n\n神经网络中最基本的成分是神经元（`neuron`）模型，即上述定义中的简单单元。经典的\"M-P神经元模型”如下所示，其中的 $f$ 表示激活函数，$w$ 表示神经元权重。\n\n![M-P神经元模型](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221045786-1853234660.png)\n\n### 5.2 感知机与多层网络\n\n感知机（`perception`）由两层神经元组成，如下图所示，输入层接收外界输入信号后传递给输出层，输出层是 M-P 神经元，也称“阈值逻辑单元”（`threshold logic unit`）。感知机能容易地实现逻辑与、或、非运算，只需要设置合理的 $w$、$\\theta$ 参数。感知机模型的公式可表示为：\n\n$$y_j = f(\\sum_{i}w_ix_i - \\theta_j)$$\n\n![M-P神经元模型](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221046121-540075924.png)\n\n给定训练数据集，感知机的 $w$ 和 $\\theta$ 参数可通过学习得到。其学习规则很简单，对于训练样本$(x,y)$，如果感知机输出为 $\\hat{y}$，则感知机的参数将做如下调整：\n\n$$\nw_{i} \\leftarrow w_{i} + \\Delta w_i \\\\\n\\Delta w_i = \\eta(y - \\hat{y})x\n$$\n\n其中 $\\eta$ 称为学习率，从上式可以看出当感知机预测输出 $\\hat{y}$ 和样本标签 $y$ 相等时，参数不改变；否则将根据偏差的严重程度进行相应调整。\n\n需要注意的是，感知机只能解决与、或、非这样的线性可分（即存在一个线性超平面将它们分开）问题，但是甚至不能解决异或这样简单的非线性可分问题。\n\n要解决非线性可分问题，可以使用多功能神经元。如下图所示的就是一种常见的“多层前馈神经网络”（`multi-layer feedforward neural`），每层神经元与下一层神经元全互连，同层神经元之间不连接，也不存在跨层连接。\n> “前馈” 并不意味着网络中信号不能反向传播，而单纯是指网络拓扑结构上不存在环或回路。\n\n![多层前馈神经网络](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221046474-1186505537.png)\n\n### 5.3 误差反向传播算法\n\n很明显多层神经网络的学习能力比单层网络（单层感知机）强得多。感知机参数的学习规则很简单，因此其对于多层神经网络是不够用的，由此，诞生了强大的误差反向传播（`error BackPropagation`，简称 `BP`）算法，并使用至今，现阶段的所有深度神经网络的参数都是由 `BP` 算法训练得到的。\n\n反向传播指的是计算神经⽹络参数梯度的⽅法。总的来说，反向传播依据微积分中的链式法则，沿着从输出层到输⼊层的顺序，**依次计算并存储⽬标函数有关神经⽹络各层的中间变量以及参数的梯度**。\n> 前向传播：输入层-->输出层；反向传播：输出层-->输入层。\n\n下面通过前馈神经网络的 `BP` 算法来深入理解 `BP` 过程。\n\n给定训练集 $D = {(x_1, y_1), (x_2, y_2),...,(x_m, y_m)}, x \\in \\mathbb{R}^d, y_i \\in \\mathbb{R}^l$，即每个输入样本都由 $d$ 个属性表征，模型输出的是 $l$ 维实值向量。为了便于 `BP` 算法的推导，下图给出了一个拥有 $d$ 个输入神经元、$l$ 个输出神经元、$q$ 个隐层神经元的多层前馈神经网络，其中输出层第 $j$ 个神经元的阈值用 $\\theta_j$ 表示，隐层第 $h$ 个神经元的阈值用 $\\gamma_h$ 表示。输入层第 $i$ 个神经元与隐层第 $h$ 个神经元之间的连接权重参数为 $v_{ih}$，隐层第 $h$ 个神经元与输出层第 $j$ 个神经元之间的连接权为 $w_{hj}$。记隐层第 $h$ 个神经元接收到的输入为 $\\alpha_h = \\sum_{i=1}^{d}v_{ih}x_i$，输出层第 $j$ 个神经元接收到的输入为 $\\beta_j = \\sum_{h=1}^{q}w_{hj}b_h$，其中 $b_h$ 为隐层第 $h$ 个神经元的输出。这里假设隐层和输出层神经元的激活函数都为 `Sigmoid` 函数。\n\n![BP网络](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221046857-375596699.png)\n\n对训练样本 $(x_k,y_k)$，假设神经网络的输出为 $\\hat{y}_k =(\\hat{y}_1^k,\\hat{y}_2^k,...,\\hat{y}_l^k)$，即\n\n$$\n\\hat{y}_j^k = f(\\beta_j - \\theta_j)\\tag{5.3}，\n$$\n\n则网络在 $(x_k,y_k)$ 上的均方误差损失为\n\n$$\nE_k = \\frac{1}{2}\\sum_{j=1}^{l}(\\hat{y}_j^k - y_k)^2\\tag{5.4}，\n$$\n\n输入层到隐层需要 $d \\times q$ 个权重参数、隐层到输出层需要需要 $q \\times l$ 个权重参数，以及 $q$ 个隐层的神经元阈值、$l$ 个输出层的神经元阈值。`BP` 是一个迭代学习算法，在迭代的每一轮中采用广义的感知机学习规则对参数进行更新估计，因此任意参数 $v$ 的更新估计表达式都可为\n\n$$\nv\\leftarrow v + \\Delta v\\tag{5.5}\n$$\n\n下面我会首先推导出隐层到输出层的权重 $w_{hj}$ 更新公式，而后类别推导 $\\theta_{j}$、$v_{ih}$、$\\gamma_{h}$。\n\n`BP` 算法是基于梯度下降（`gradient desent`）策略，以目标的负梯度方向对参数进行调整，对式$(5.4)$的误差 $E_k$，给定学习率 $\\eta$，可得权重参数更新公式如下：\n\n$$\nw \\leftarrow w + -\\eta \\frac{\\partial E_k}{\\partial w}\n$$\n\n同时，\n\n$$\n\\Delta w_{hj} = -\\frac{\\partial E_k}{\\partial w_{hj}}\\tag{5.6}\n$$\n\n注意到 $w_{hj}$ 先影响到第 $j$ 个输出层神经元的输入值 $\\beta_j$，再影响到其输出值 $\\hat{y}_j^k$，最后才影响到损失 $E_k$，根据梯度的链式传播法则，有\n\n$$\n\\frac{\\partial E_k}{\\partial w_{hj}} = \\frac{\\partial E_k}{\\partial \\hat{y}_j^k}\\frac{\\partial \\hat{y}_j^k}{\\partial \\beta_j}\\frac{\\partial \\beta_j}{\\partial w_{hj}} \\tag{5.7}\n$$ \n\n根据前面 $\\beta_j$ 的定义，显然有\n\n$$\n\\frac{\\partial \\beta_j}{\\partial w_{hj}} = b_h \\tag{5.8}\n$$\n\n再因为 `Sigmoid` 函数的一个很好的性质：\n\n$$\n{f}'(x) = f(x)(1-f(x)) \\tag{5.9}\n$$\n\n再联合式$(5.3)$和$(5.4)$，有：\n\n$$\\begin{align}\n-\\frac{\\partial E_k}{\\partial \\hat{y}_j^k} \\frac{\\partial \\hat{y}_j^k}{\\partial \\beta_j} &= -\\frac{\\partial \\left [\\frac{1}{2} \\sum_{j=1}^{l}(\\hat{y}_{j}^{k} - y_{j}^{k})^{2}\\right ]}{\\partial \\hat{y}_{j}^{k}} \\frac{\\partial \\left [f(\\beta_{j} - \\theta_{j})\\right ]}{\\partial \\beta_{j}} \\nonumber \\\\\n&= -\\frac{1}{2}\\times 2(\\hat{y}_j^k - y_j^k) {f}'(\\beta_j - \\theta_j) \\nonumber \\\\\n&= -(\\hat{y}_j^k - y_j^k) f(\\beta_j - \\theta_j)\\left [1-f(\\beta_j - \\theta_j)\\right ] \\nonumber\\\\\n&= -(\\hat{y}_j^k - y_j^k) \\hat{y}_j^k (1-\\hat{y}_j^k) \\nonumber\\\\\n&= \\hat{y}_j^k (1-\\hat{y}_j^k) (y_j^k - \\hat{y}_j^k ) \\tag{5.10}\n\\end{align}$$\n\n注意这里的 $f$ 是 `Sigmoid` 函数。令 $g_{j} = \\hat{y}_j^{k}(1-\\hat{y}_j^{k})(y_{j}^{k} - \\hat{y}_{j}^{k})$ 。将 $g_{j}$ 和式 $(5.8)$ 带入式 $(5.6)$，就得到了 `BP` 算法中关于 $w_{hj}$ 的更新公式：\n\n$$\n\\Delta w_{hj} = \\eta g_{j}b_{h} \\tag{5.11}\n$$\n\n类似式 $(5.10)$ 的推导过程可得其他参数的更新公式：\n\n$$\n\\Delta \\theta_j = -\\eta g_j \\tag{5.12}\n$$\n$$\n\\Delta v_{ih} = -\\eta e_{h}x_{i} \\tag{5.13}\n$$\n$$\n\\Delta \\theta_j = -\\eta e_h \\tag{5.14}\n$$\n\n$v_{ih}$ 梯度的详细推导公式如下所示。因为，\n\n$$\\begin{aligned} \n\\cfrac{\\partial E_k}{\\partial v_{ih}} &= \\sum_{j=1}^{l} \\cfrac{\\partial E_k}{\\partial \\hat{y}_j^k} \\cdot \\cfrac{\\partial \\hat{y}_j^k}{\\partial \\beta_j} \\cdot \\cfrac{\\partial \\beta_j}{\\partial b_h} \\cdot \\cfrac{\\partial b_h}{\\partial \\alpha_h} \\cdot \\cfrac{\\partial \\alpha_h}{\\partial v{ih}} \\\\\n&= \\sum_{j=1}^{l} \\cfrac{\\partial E_k}{\\partial \\hat{y}_j^k} \\cdot \\cfrac{\\partial \\hat{y}_j^k}{\\partial \\beta_j} \\cdot \\cfrac{\\partial \\beta_j}{\\partial b_h} \\cdot \\cfrac{\\partial b_h}{\\partial \\alpha_h} \\cdot x_i \\\\\n&= \\sum_{j=1}^{l} \\cfrac{\\partial E_k}{\\partial \\hat{y}_j^k} \\cdot \\cfrac{\\partial \\hat{y}_j^k}{\\partial \\beta_j} \\cdot \\cfrac{\\partial \\beta_j}{\\partial b_h} \\cdot f^{\\prime}(\\alpha_h-\\gamma_h) \\cdot x_i \\\\\n&= \\sum_{j=1}^{l} \\cfrac{\\partial E_k}{\\partial \\hat{y}_j^k} \\cdot \\cfrac{\\partial \\hat{y}_j^k}{\\partial \\beta_j} \\cdot w_{hj} \\cdot f^{\\prime}(\\alpha_h-\\gamma_h) \\cdot x_i \\\\\n&= \\sum_{j=1}^{l} (-g_j) \\cdot w_{hj} \\cdot f^{\\prime}(\\alpha_h-\\gamma_h) \\cdot x_i \\\\\n&= -f^{\\prime}(\\alpha_h-\\gamma_h) \\cdot \\sum_{j=1}^{l} g_j \\cdot w_{hj} \\cdot x_i \\\\ \n&= -b_h(1-b_h) \\cdot \\sum_{j=1}^{l} g_j \\cdot w_{hj} \\cdot x_i \\\\\n&= -e_h \\cdot x_i\n\\end{aligned}$$\n\n所以\n\n$$\n\\Delta v_{ih} =-\\eta \\cfrac{\\partial E_k}{\\partial v_{ih}} =\\eta e_h x_i\n$$\n\n![v_{ih} 梯度的详细推导公式](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221047252-538822529.png)\n\n> 最后一层激活使用 `softmax` 激活和交叉熵损失函数的反向传播推导，可参考[这里](https://blog.csdn.net/DaVinciL/article/details/90898194)。\n\n学习率 $\\eta \\in (0,1)$ 控制着算法每一轮迭代中更新的步长，若太大则容易振荡，太小则收敛速度幽会过慢。有时为了做精细调节，可令式$(5.11)$与$(5.12)$使用 $\\eta_1$， 式$(5.13)$与$(5.14)$使用 $\\eta_2$，两者未必相等。\n\n下图给出了 `BP` 算法的工作流程：\n\n![BP算法流程](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221047618-1687530698.png)\n\n对每个训练样本，`BP` 算法操作流程如下：\n\n1. **前向传播**:将输入样本输入给输入神经元，然后逐层将信号前向传播，直到输出层产生结果。\n2. **反向传播**：计算输出层的误差，沿着输出层到输入层的顺序，依次计算并存储损失函数有关各层的中间变量及参数梯度，并对参数进行调整。\n3. `1，2` 过程循环进行，直到达到某些停止条件为止，如训练误差已达到一个很小的值。\n\n> 停止条件与缓解 `BP` 过拟合的策略有关。\n\n值得注意的是，`BP` 算法的目标是要最小化训练集 $D$ 上的累计误差：\n\n$$\nE = \\frac{1}{m}\\sum_{k=1}{m}E_k，\\tag{5.16}\n$$\n\n但是我们前面描述的“标准 BP 算法”的计算过程，其实每次仅针对一个训练样本更新连接权重和阈值的，因此，图5.8中算法发更新规则同样也是基于单个 $E_k$ 推导而得的。如果类似地推导出基于累计误差最小化的更新规则，就得到了累计误差反向传播（`accumulated error backpropagation`）算法。\n\n一般来说，标准 `BP` 算法每次更新仅针对单个样本，参数更新很频繁，而且不同样本对进行更新的效果可能出现“抵消”现象。因此，为了达到同样的累计误差极小点，标准 `BP` 算法往往需要进行更多次数的迭代。而累计 `BP` 算法直接将累计误差最小化，它是在读取整个训练集 $D$ 一遍（`one epoch`）后才对神经网络各层权重参数进行更新，其参数更新频率低得多。但是在很多任务中，累计误差下降到一定程度后，进一步下降会非常缓慢，这是标准 `BP` 往往会更快获得较好的解，尤其是在训练集 $D$ 非常大时很明显。\n> 标准 BP 算法和累计 BP 算法的区别类似于随机梯度下降（stochastic gradient descent，简称 SGD）与标准梯度下降之间的区别。\n\n`[Hornik et al.,1989]` 证明，只需一个包含足够多神经元的隐层（即深度神经网络层数足够多），多层前馈神经网络就能以任意精度逼近任意复杂度的连续函数。但是，深度神经网络层数的选择依然是一个未决问题，在实际应用中通常依靠“试错法”（`trial-by-error`）调整。\n\n因为神经网络的表示能力太过强大，因此 `BP` 神经网络在训练过程中经常遭遇过拟合，即训练误差持续降低，但验证集误差却可能上升。目前常用来缓解 `BP` 网络过拟合问题的策略，有以下两种：\n\n第一种策略是“早停”（`early stopping`）:将数据分为训练集和验证集，训练集用来更新权重参数，验证集用来估计误差，若训练集误差降低但验证集误差升高，则停止训练，同时返回具有最小验证集误差的权重参数模型。\n\n第二种策略是“正则化”（`regulazation`）：其基本思想是在误差目标函数中增加一个用于描述网络复杂度的部分，例如连接权重与阈值的平方和。仍令 $E_k$ 表示第 $k$ 个训练样本上的误差，$w_i$表示连接权重和阈值，则误差目标函数$(5.16)$更改为：\n\n$$\nE = \\lambda \\frac{1}{m}\\sum_k^m E_k + (1- \\lambda)\\sum_{i} w_i^2 \\tag{5.17}\n$$\n\n其中 $\\lambda \\in (0,1)$用于对经验误差与网络复杂度这两项进行折中，常通过交叉验证法估计。\n\n常用的刻画模型复杂度 $R(w)$ 的函数有两种，一种是 $L1$ 正则化，即绝对值的之和；另一种是 $L2$ 正则化，即绝对值平方之和，计算公式如下：\n\n$$\nR(w) = ||w||_1 = \\sum_i|w_i| \\\\\nR(w) = ||w||_2 = \\sum_i|w_i^2| \n$$\n\n无论是哪一种正则化，其基本思想都是希望通过限制权重的大小，使得模型不能任意拟合训练数据中随机噪声。$L1$ 与 $L2$ 有很大区别，$L1$ 正则化会让参数变得更加稀疏，而 $L2$ 不会。所谓参数变得更加稀疏是指会有更多的参数变为 `0`，这样可以达到类似特征选取的功能。\n\n> L2范数正则化（regularization）等价于权重衰减的概念。\n\n### 5.4 全局最小与局部最小\n\n若用 $E$ 表示神经网络在训练集上的误差，则它显然是关于连接权重 $w$ 和阈值 $\\theta$ 的函数。此使，神经网络的训练过程可看作是一个参数寻优的过程，即在参数空间中，寻找一组最优参数使得 $E$ 最小。\n\n显然，参数空间内梯度为零的点，只要其误差函数值小于零点的误差函数值，就是局部极小点；可能存在多个局部极小值，但有且仅有一个全局最小值。\n\n基于梯度的搜索是使用最为广泛的参数寻优方法。在这类方法中，一般先从参数随机初始解出发，迭代寻找最优参数解。每次迭代我们先计算误差函数在当前点的梯度，然后根据梯度确定搜索方向。例如，由于负梯度方向是函数值下降最快的方向，因此梯度下降法就是沿着负梯度方向搜索最优解。若误差函数在当前点的梯度为零，则已达到局部极小，参数更新量也将为零，也意味着参数的迭代更新将在此停止。\n\n![局部最小与全局最小](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221048022-233010177.png)\n\n在现实任务中，我们常用以下启发式策略（非理论策略）来试图“跳出”局部极小，从而进一步接近全局最小：\n\n+ 以多组不同参数值初始化多个神经网络，按标准方法训练后，取其中最小的解作为最终参数。\n+ 使用”模拟退火“（`simulated annealing`）技术。模拟退火在每一步都以一定的概率接受比当前解更差的结果，从而有助于”跳出“局部极小。在每次迭代过程中，接收”次优解“的概率要随着时间的推移而逐渐降低，从而保证算法的稳定性。\n+ 使用随机梯度下降算法。\n\n### 5.5 其他常见神经网络\n\n`2010` 年之前常见的神经网络有 `RBF` 网络、`ART` 网络、`SOM` 网络、`级联相关网络`、`Elman网络`、`Boltzmann` 机。\n### 5.6 深度学习\n\n典型的深度学习模型就是很深层（层数很多）的神经网络。值得注意的是，从增加模型复杂度的角度来看，增加隐藏层的数目显然比增加隐藏层神经元的数目更有效，即**网络的“深”比“宽”重要**。因为隐藏层层数的增加不仅增加了拥有激活函数神经元的数目，还增加了激活函数嵌套的层数，即**非线性能力进一步增强**。\n## 第 9 章 聚类\n\n### 9.1 聚类任务\n\n在“无监督学习”中，训练样本是无标签信息的，目标是通过对无标记训练样本的学习来揭示数据的内在性质和规律，为进一步的数据分析提供基础。这类学习任务中应用最广的就是“聚类”（`clustering`）算法，其他的无监督学习任务还有密度估计（`density estimation`）、异常检测（`anomaly detection`）等。\n\n聚类的任务是将数据集中的样本划分为若干个通常是不相交的**子集**，每个子集称为一个“簇”（`cluster`），簇所对应的概念和语义由使用者来把握。\n\n聚类可用作其他学习任务的一个前驱过程。基于不同的学习策略，有着多种类型的聚类算法，聚类算法涉及的两个基本问题是性能度量和距离计算。\n\n### 9.2 性能度量\n\n聚类性能度量也叫聚类“有效性指标”（`validity index`），和监督学习的性能度量作用类似，都是用来评估算法的好坏，同时也是聚类过程中的优化目标。\n\n聚类性能度量大致有两类。一类是将聚类结果与某个“参考模型（`reference model`）”进行比较，称为“外部指标（`external index`）”；另一类是直接考察聚类结果而不利用任何参考模型，称为“内部指标”（`inderna index`）。\n\n外部指标包括 `Jaccard` 指数（简称 `JC`）、`FM` 指数（简称 `FMI`）和 `Rand` 指数（简称 `RI`），范围在 `[0,1]` 之间，且越大越好；内部指标包括 `DB` 指数（简称 `DBI`）和 `Dunn` 指数（简称 `DI`），`DBI` 值越小越好，`DI` 越大越好。\n> 具体计算公式太多了，这里不给出，可以参考原书 `199` 页。\n\n### 9.3 距离计算\n\n函数 $dist(\\cdot,\\cdot)$ 用于计算两个样本之间的距离，如果它是一个“距离度量”（`distance measure`），则需满足一些基本性质：\n+ 非负性：$dist(x_i,x_j) \\geq 0$；\n+ 同一性：$dist(x_i,x_j)=0$ 当前仅当 $x_i = x_j$；\n+ 对称性：$dist(x_i,x_j) = dist(x_j,x_i)$；\n+ 直递性：$dist(x_i,x_j \\geq dist(x_i,x_k) + dist(x_k,x_j))$。\n\n给定样本 $x_i = (x_{i1};x_{i2};...;x_{in})$ 与 $x_j = (x_{j1};x_{j2};...;x_{jn})$，最常用的是“闵可夫斯距离”（`Minkowski distance`）。\n\n$$\ndist_{mk}(x_i,x_j) = (\\sum_{u=1}^{n}\\left | x_{iu} - x_{ju} \\right |^p)^\\frac{1}{p}\n$$\n\n上式其实就是 $x_i - x_j$ 的 $L_p$ 范数 $\\left \\| x_i - x_j \\right \\|_p$。$p = 2$，上式为欧氏距离。 \n\n属性通常划分为“连续属性”（`continuous attribute`）和“离散属性”（`categotical attribute`），前者在定义域上有无穷多个可能的取值，后者在定义域上是有限个取值。但是在涉及距离计算时，属性上定义了“序”更为重要。能直接在属性上计算距离：“1” 和 “2” 比较近、和“4”比较远，这样的属性称为“有序属性”（`ordinal attribute` ）；而定义域为{汽车、人、椅子}这样的离散属性则不能直接在属性上计算距离，称为“无序属性”（`non-ordinal atribute`）。显然，**闵可夫斯距离可用于有序属性**。\n\n对无序属性采用 `VDM`（`Value Difference Metric`），同时将闵可夫斯距离和 `VDM` 集合可处理混合属性。\n\n### 9.4 原型聚类\n\n原型聚类算法假设聚类结构能通过一组原型刻画，这里的原型是指样本空间中具有代表性的点。一般算法先对原型进行初始化，然后对原型进行迭代更新求解。采用不同的原型表示、不同的求解方式，将产生不同的算法。\n\n#### 9.4.1 k 均值算法\n\n`K-Means` 算法的思想很简单，对于给定的样本集，按照样本之间的距离大小，将样本集划分为 $K$ 个簇，让簇内的点尽量紧密的连在一起，而让簇间的距离尽量的大。\n\n给定样本集 $D = \\{x_1, x_2,...,x_m\\}$，假设簇划分为 $C = \\{C_1,C_2,...,C_m\\}$，算法目标是最小化平方误差。\n\n$$\nE = \\sum_{i=1}^{k}\\sum_{x \\in C_{i}}\\left \\| x - u_i \\right \\|_2^2\n$$\n\n其中 $u_i=\\frac{1}{C_i}\\sum_{x \\in C_{i}}x$ 是簇 $C_i$ 的均值向量，也称质心。上式一定程度上刻画了簇内样本围绕簇均值向量的紧密程度，$E$ 值越小簇内样本相似程度越高。\n\n找到 $E$ 的最优解需要靠擦样本集 $D$ 所有可能的簇划分，这是一个 $NP$ 难问题。$k$ 均值算法采用了贪心策略，通过迭代优化近似求解，算法流程如图 `9.2` 所示。其中第 `1` 行对均值向量进行初始化，`4-8` 行与 `9-16` 行依次对当前簇划分即均值向量迭代更新，若更新后聚类结果保持不变，则在第 `18` 行将当前簇划分结果返回。\n\n![k-means算法流程](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230220221048552-2059208228.png)\n\n参考此[文章](https://www.pythonf.cn/read/116646)的代码，我修改为如下精简版的 `K-Means` 算法代码。\n\n```python\ndef kmeans(dataset, k):\n    \"\"\"K-means 聚类算法\n\n    Args:\n        dataset ([ndarray]): 数据集，二维数组\n        k ([int]): 聚簇数量\n    \"\"\"\n    m = np.shape(dataset)[0]  # 样本个数\n    \n    # 1, 随机初始化聚类中心点\n    center_indexs = random.sample(range(m), k)\n    center = dataset[center_indexs,:]\n    \n    cluster_assessment = np.zeros((m, 2))\n    cluster_assessment[:, 0] = -1  # 将所有的类别置为 -1\n    cluster_changed = True \n    while cluster_changed:\n        cluster_changed = False\n        # 4-8，计算样本x_i与各聚类中心的距离，根据距离最近的聚类中心确定x_j的簇标记，并将对应样本x_i划入相应的簇\n        for i in range(m):\n            # 初始化样本和聚类中心的距离，及样本对应簇\n            min_dist = inf\n            c = 0\n            # 确定每一个样本离哪个中心点最近，和属于哪一簇\n            for j in range(k):\n                dist = distEclud(dataset[i,:], center[j,:])\n                if dist < min_dist:\n                    min_dist = dist\n                    c = i\n            # 更新样本所属簇\n            if cluster_assessment[i, 0] != c:  # 仍存在数据在前后两次计算中有类别的变动，未达到迭代停止要求\n                cluster_assessment[i, :] = c, min_dist  # 更新样本所属簇\n                cluster_changed = True\n        # 9-16 更新簇中心点位置\n        for j in range(k):\n            changed_center = dataset[cluster_assessment[:,0] == j].mean(axis=0)\n            center[j,:] = changed_center\n            \n    return cluster_assessment, center\n\nif __name__ == '__main__':\n    x1 = np.random.randint(0, 50, (50, 2))\n    x2 = np.random.randint(40, 100, (50, 2))\n    x3 = np.random.randint(90, 120, (50, 2))\n    x4 = np.random.randint(110, 160, (50, 2))\n    test = np.vstack((x1, x2, x3, x4))\n\n    # 对特征进行聚类\n    result, center = kmeans(test, 4, is_kmeans=False, is_random=False)\n    print(center) # 打印簇类中心点\n```\n\n## 参考资料\n《机器学习》-周志华"
  },
  {
    "path": "3-machine_learning/机器学习入门总结.md",
    "content": "- [一，机器学习概述](#一机器学习概述)\n  - [1.1，机器学习分类](#11机器学习分类)\n  - [1.2，机器学习任务](#12机器学习任务)\n- [二，线性模型](#二线性模型)\n  - [2.1，基本形式](#21基本形式)\n  - [2.2，线性回归](#22线性回归)\n  - [2.3，多分类学习](#23多分类学习)\n  - [2.4，类别不平衡问题](#24类别不平衡问题)\n- [三，数据清洗与特征处理](#三数据清洗与特征处理)\n  - [3.1，清洗标注数据](#31清洗标注数据)\n  - [3.2，特征分类](#32特征分类)\n  - [3.3，特征处理与分析](#33特征处理与分析)\n- [四，数据预处理基础](#四数据预处理基础)\n  - [4.1，如何处理数据中的缺失值](#41如何处理数据中的缺失值)\n- [参考资料](#参考资料)\n\n## 一，机器学习概述\n\n### 1.1，机器学习分类\n\n所谓机器学习，是关于在计算机上从数据中产生“模型”（model）的算法，即“学习算法”（learning algorithm）。\n\n机器学习方法的分类，根据所处理的数据种类的不同，可以分为监督学习、无监督学习和强化学习等几种类型，如下图所示:\n\n<img src=\"../data/images/ml_basic/3_machine_learing_tasks.png\" alt=\"3 basic types of machine learing tasks\" width=\"80%\" />\n\n- 所谓监督学习，简单理解就是，训练集样本是带标签的。\n- 无监督学习，即训练集样本是不带标签的，即没有老师自导，学生（模型）自学的过程。\n- 强化学习，与监督学习类似，与监督学习不同的是，强化学习不需要带标签的输入输出对，同时也无需对非最优解的精确地纠正。\n\n### 1.2，机器学习任务\n\n我们知道，根据**训练数据是否拥有标记信息（标签）**，学习任务可大致划分为两大类“监督学习” (supervised learning) 和“无监督学习” (unsupervised learning)，其中，分类和回归任务是前者的代表，而聚类则是后者的代表。\n\n本文以西瓜问题为例来通俗定义分类、回归和聚类问题：\n1. 若我们欲预测的是离散值，例如”好瓜“、”坏瓜“，此类学习任务称为”分类“ (`classification`); \n2. 若欲预测的是连续值，例如西瓜成熟度 0.95、 0.37，此类学习任务称为”回归“ (`regression`)。\n3. 若事先不知道西瓜的类别，我们还可以对西瓜做“聚类” (`clustering`)，即将训练集中的西瓜分成若干组，每组称为一个“簇” (`cluster`); 这些自动形成的簇可能对应一些潜在的概念划分，例如“浅色瓜”、 “深色瓜”等。\n\n值得注意的是，机器学习算法的最终目标是使学习到的模型能很好地适用于“新样本”（方差小），而不仅仅只是在训练集样本上表现好（偏差小）。学得模型适用于于新样本的能力，称为“泛化”（generalization）能力。\n\n机器学习中另外两个常见的任务是异常检测和降维。\n1. **异常检测**，是指寻找输入样本 $x_i$ 中所包含的异常数据的问题。在已知正常数据和异常数据标签的前提下，其和有监督分类问题是相同的。\n2. **降维**，是指从高维度数据中提取关键信息，目的是将其转换为容易计算的低维度问题进而求解的方法。\n\n如何理解**维数**？一般地，令 $D = {X_1，x_2..，x_m}$ 表示包含 $m$ 个示例的数据集，每个示例由 $d$ 个属性描述，则每个示例 $x_i = (x_1; x_2;...;x_d)$ 是 $d$ 维样本空间中的一个向量， $\\mathcal X$， 其中 $x_{ij}$ 是 $x_i$ 在第 $j$ 个属性上的取值(例如上述第 3 个西瓜在第 2 个属性上的值是\"硬 挺\" ), 而 $d$ 称为样本的\"维数\" (`dimensionality`)。\n\n## 二，线性模型\n\n本文所涉及的各种机器学习算法大多着重于如何使特定函数与数据集相似（拟合）。\n\n### 2.1，基本形式\n\n假设，给定由 $d$ 个属性（即维数）描述的示例 $x = (x_1; x_2;...;x_d)$， 其中 $x_i$ 是样本 $x$ 在第 $i$ 个属性上的取值，所谓线性模型 (linear model)是指，**试图学得一个通过属性的线性组合来进预测的函数**，其数学形式如下：\n\n$$f(x) = w_{1}x_1 + w_{2}x_2 +  ...+ w_{d}x_d + b\n$$\n\n一般用向量形式写成：\n\n$$f(x) = w^{\\top}x + b\n$$\n\n其中 $w = (w_1; w_2;...;w_d)$，模型参数 $w$ 和 $b$ 学得之后，线性模型也就确定下来了。\n\n从数学角度理解**线性模型**，当因变量和自变量为线性关系时，它就是一种特殊的线性模型。\n\n虽然线性模型形式简单、易于建模，但其是非线性模型的基础，非线性模型 (`nonlinear model`)可在线性模型的基础上通过引入**层级结构或高维映射**而得到。\n\n### 2.2，线性回归\n\n假设，给定数据集 $D = {(x_1, y_1),(x_2, y_2)，...，(x_m, y_m)}$，一共有 $m$ 个样本，其中数据特征 $x_i = (x_{i1}; x_{i2}...; x_{id})$, 数据标签 $y_{i} \\in \\mathbb{R}$。\n\n所谓\"线性回归\" (linear regression)，即试图学得一个线性模型以尽可能准确地预测实值输出标记，用数学公式定义，即线性回归试图学得：\n> 不同教科书中，公式形式不一样，但其意义一样。\n\n$$f(x_i) = ωx_{i} + b， 使得 f(x_i) \\simeq y_{i}$$\n\n试图求解一个最佳模型参数的前提是，先确定最佳误差函数。**均方误差（MSE - mean squared error）函数是回归任务中最常用的性能度量**，因此我们可试图让线性回归模型的均方误差误差函数 $J$ 最小化，即：\n\n$$\n\\begin{aligned}\n(x^{*}, b^{*}) &= \\underset{(w,b)}{\\text{argmin}} \\; J \\\\\n(x^{*}, b^{*}) &= \\underset{(w,b)}{\\text{argmin}} \\frac{1}{2m}\\sum_{i = 1}^{m}(f(x_{i}) - y_i)^2 \\\\\n&= \\underset{(w,b)}{\\text{argmin}}\\frac{1}{2m} \\sum_{i = 1}^{m}(y_i - wx_{i} - b)^2 \\\\\n\\end{aligned}\n$$\n\n均方误差有非常好的几何意义，它对应了常用的欧几里得距离（也简称“欧氏距离” Euclidean distance)。\n\n基于均方误差最小化来进行模型求解的方法称为“最小二乘法” (`least squre method`)。在线性回归中，最小二乘法就是试图找到一条直线，使所有样本到直线上的欧氏距离之和最小。\n> 最小二来法用途很广， 不仅限于线性回归。\n\n![均方差函数的评估原理](../data/images/ml_basic/mse.png)\n\n上图圆形点是样本点，直线是当前的拟合结果。如左图所示，计算样本点到直线的垂直距离，需要先根据直线的斜率来求垂足然后再计算距离，这样计算起来很慢；但实际上，在工程上我们通常使用的是右图的方式，即样本点到直线的**竖直距离**，因为这样计算很方便，用一个减法就可以了。\n\n如果想让误差的值最小，通过对 $w$ 和 $b$ 求导，再令导数为 0（到达最小极值），就是 $w$ 和 $b$ 的最优解。\n\n$$\n\\begin{aligned} \\frac{\\partial{J}}{\\partial{w}} &=\\frac{\\partial{(\\frac{1}{2m}\\sum_{i=1}^m(y_i-wx_i-b)^2)}}{\\partial{w}} \\\\\\ &= \\frac{1}{m}\\sum_{i=1}^m(y_i-wx_i-b)(-x_i) \\end{aligned}\n$$\n\n推导过程省略，最终得到最佳参数解：\n\n$$ w = \\frac{m\\sum_{i=1}^m x_i y_i - \\sum_{i=1}^m x_i \\sum_{i=1}^m y_i}{m\\sum_{i=1}^m x^2_i - (\\sum_{i=1}^m x_i)^2}$$\n\n$$ b= \\frac{1}{m} \\sum_{i=1}^m(y_i-wx_i)$$\n> 注意，上式有很多个变种，在不同的文章可能不同版本。\n\n根据以上公式，可用代码实现“最小二乘法”:\n\n```python\ndef method3(X,Y,m):\n    p = m*sum(X*Y) - sum(X)*sum(Y)\n    q = m*sum(X*X) - sum(X)*sum(X)\n    w = p/q\n    return w\n\ndef calculate_b_1(X,Y,w,m):\n    b = sum(Y-w*X)/m\n    return b\n```\n\n如果模型参数形式为 $w^{\\top}$，即包括两个或两个以上自变量的回归，则称为多元线性回归模型，数学公式如下：\n\n$$f(x_i) = w^{\\top}x_{i} + b， 使得 f(x_i) \\simeq y_{i}$$\n\n### 2.3，多分类学习\n\n现实中常遇到多分类学习任务。有些二分类学习方法可直接推广到多分类， 但在更多情形下，我们是基于一些基本策略，利用二分类学习器来解决多分类问题。\n\n### 2.4，类别不平衡问题\n\n类别不平衡 (`class imbalance`)就是指分类任务中**不同类别的训练样本数目差别很大的情况**。类别不平衡学习的一个基本策略是“再缩放”（rescaling）:\n\n$$\n\\frac{y'}{1-y'} = \\frac{y}{1-y} \\times \\frac{m^-}{m^+}\n$$\n\n其中，$m^+$ 表示正例数目， $m^-$ 表示反例数目，则观测概率是 $\\frac{m^-}{m^+}$，$y$ 是样本标签值，$y'$ 是模型预测概率值。\n\n再缩放的思想虽简单，但实际操作却很难，主要因为\"训练集是真实样本总体的无偏采样\"这个假设往往并不成立，也就是说我们未必能有效地基于训练集观测概率来推断出真实概率。\n\n现有技术大体上有三类做法:\n1. 第一类是直接对训练集里的反类样例进行“**欠采样**”（undersampling），即去除一些反例使得正、反样本数目接近，然后基于新的数据集进行训练。\n2. 第二类是对训练集的正类样例进行“**过采样**”（oversampling），即增加一些正例使得正、反例数目接近。\n3. 第三类则是直接基于原始训练集进行学习，但在用训练好的分类器进行预测时，将“再缩放策略”嵌入到其决策过程中，称为\"阔值移动\" (threshold-moving)。\n\n## 三，数据清洗与特征处理\n\n### 3.1，清洗标注数据\n\n清洗标注数据的方法，主要是是**数据采样**和**样本过滤**。\n\n+ **数据采样**：当模型不能使用全部的数据来训练时，需要对数据进行采样，设定一定的采样率；采样的方法包括**随机采样，固定比例采样**等。\n+ **样本过滤**：包括结合业务情况进行数据的过滤和使用异常点检测算法，常用的异常点检测算法包括：偏差检测（聚类，最近邻算法）等。\n\n### 3.2，特征分类\n\n根据不同的分类方法，可以将特征分为：\n\n+ `Low level` 特征和 `High level` 特征\n+ 稳定特征与动态特征。\n+ 二值特征、连续特征、枚举特征\n\n`Low level` 主要指原始特征，通常不需要或者需要很少的人工处理和干预，例如文本中的词向量特征，图像特征中的像素点大小，用户 `id`，商品 `id` 等。High level 特征是经过比较复杂的处理，结合部分业务逻辑或者规则、模型得到的特征，例如人工打分，模型打分等特征，可以用于较复杂的非线性模型。Low level 比较针对性，覆盖面小。长尾样本的预测值主要受 high level 特征影响。高频样本的预测值主要受 low level 特征影响。\n\n`稳定特征` 是变化频率较少的特征，例如评价平均分，团购单价价格等，在较长时间段内数值都不会发生变化。动态特征是更新变化比较频繁的特征，有些甚至是实时计算得到的特征，例如距离特征，2 小时销量等特征。或者叫做实时特征和非实时特征。针对两类特征的不同可以针对性地设计特征存储和更新方式，例如对于稳定特征，可以建入索引，较长时间更新一次，如果做缓存的话，缓存的时间可以较长。对于动态特征，需要实时计算或者准实时地更新数据，如果做缓存的话，缓存过期时间需要设置的较短。\n\n`二值特征主要是 0/1 特征`，即特征只取两种值：`0 或者 1`，例如`用户 id 特征`：目前的 id 是否是某个特定的 id，`词向量特征`：某个特定的词是否在文章中出现等等。连续值特征是取值为有理数的特征，特征取值个数不定，例如距离特征，特征取值为是0~正无穷。枚举值特征主要是特征有固定个数个可能值，例如今天周几，只有 7 个可能值：周1，周2，…，周日。在实际的使用中，我们可能对不同类型的特征进行转换，例如将枚举特征或者连续特征处理为二值特征。枚举特征处理为二值特征技巧：将枚举特征映射为多个特征，每个特征对应一个特定枚举值，例如今天周几，可以把它转换成7个二元特征：今天是否是周一，今天是否是周二，…，今天是否是周日。连续值处理为二值特征方法：先将连续值离散化（后面会介绍如何离散化)，再将离散化后的特征切分为N个二元特征，每个特征代表是否在这个区间内。\n\n### 3.3，特征处理与分析\n\n对特征进行分类后，需要对特征进行处理，`常用的特征处理方法`如下：\n\n+ 特征归一化，离散化，缺省值处理\n+ 特征降维方法\n+ 特征选择方法\n\n**特征归一化**。在有些算法中，例如线性模型或者距离相关的模型（聚类模型、knn 模型等），特征值的取值范围会对最终的结果产生较大影响，例如输入数据有两种不同的特征，其中的二元特征取值范围 `[0, 1]`，而距离特征取值可能是 [0，正无穷]，两种特征取值范围不一致，导致模型可能会偏向于取值范围较大额特征，为了平衡取值范围不一致的特征，需要对特征进行归一化处理，将特征值取值归一化到 [0,1] 区间，常用的归一化方法包括：\n\n1. `函数归一化`，通过映射函数将特征取值映射到 $[0，1]$ 区间，例如最大最小值归一化方法，是一种线性的映射。还有通过非线性函数的映射，例如 `log` 函数等。\n2. `分维度归一化`，可以使用最大最小归一化方法，但是最大最小值选取的是所属类别的最大最小值，即使用的是局部最大最小值，不是全局的最大最小值。\n3. `排序归一化`，不管原来的特征取值是什么样的，将特征按大小排序，根据特征所对应的序给予一个新的值。\n\n**离散化**。在上面介绍过连续值的取值空间可能是无穷的，为了便于表示和在模型中处理，需要对连续值特征进行离散化处理。常用的离散化方法包括等值划分和等量划分。\n\n1. `等值划分`，是将特征按照值域进行均分，每一段内的取值等同处理。例如某个特征的取值范围为 [0，10]，我们可以将其划分为10段，[0，1)，[1，2)，…，[9，10)。\n2. `等量划分`，是根据样本总数进行均分，每段等量个样本划分为 1 段。例如距离特征，取值范围［0，3000000］，现在需要切分成 10 段，如果按照等比例划分的话，会发现绝大部分样本都在第 1 段中。使用等量划分就会避免这种问题，最终可能的切分是[0，100)，[100，300)，[300，500)，..，[10000，3000000]，前面的区间划分比较密，后面的比较稀疏。\n\n**缺省值处理**。有些特征可能因为无法采样或者没有观测值而缺失，例如距离特征，用户可能禁止获取地理位置或者获取地理位置失败，此时需要对这些特征做特殊的处理，赋予一个缺省值。缺省值如何赋予，也有很多种方法。例如`单独表示，众数，平均值等`。\n\n## 四，数据预处理基础\n\n数据预处理的方法主要包括去除唯一属性、处理缺失值、属性编码、数据标准化正则化、特征选择、主成分分析等。\n\n### 4.1，如何处理数据中的缺失值\n\n可以分为以下 2 种情况：\n\n1，**缺失值较多**：直接舍弃该列特征，否则可能会带来较大噪声，从而对结果造成不良影响。\n  \n2，**缺失值较少**：当缺失值较少（`< 10%`）时，可以考虑对缺失值进行填充，通常有几下几种填充策略：\n  + 用一个**异常值**填充（比如 0 ），对应 `pandas` 函数是 `pd.DataFrame.fillna(0)`\n  + **均值均值**填充：`pd.DataFrame.fillna(DataFrame.mean())`\n  + **相邻数据**填充：`pd.DataFrame.fillna(DataFrame='pad')` 或 `data.fillna(method='bfill')`\n  + **插值填充**：`pd.DataFrame.interpolate()`\n  + **拟合**：简单来说，就是将缺失值也作为一个预测问题来处理：将数据分为正常数据和缺失数据，对有值的数据采用`随机森林`等方法拟合，然后对有缺失值的数据进行预测，然后用预测到的值来进行填充。\n\n## 参考资料\n\n- [Machine learning basics](https://medium.com/@Khuranasoils/machine-learning-basics-f58678cf9c15)\n- 《机器学习》\n- 《智能之门》\n- [机器学习中的数据清洗与特征处理综述](https://tech.meituan.com/2015/02/10/machinelearning-data-feature-process.html)"
  },
  {
    "path": "3-machine_learning/机器学习基本原理.md",
    "content": "- [前言](#前言)\n- [5.1 学习算法](#51-学习算法)\n  - [5.1.1 任务 $T$](#511-任务-t)\n  - [5.1.2 性能度量 $P$](#512-性能度量-p)\n  - [5.1.3 经验 $E$](#513-经验-e)\n  - [5.1.4 示例: 线性回归](#514-示例-线性回归)\n- [5.2 容量、过拟合和欠拟合](#52-容量过拟合和欠拟合)\n  - [5.2.1 没有免费午餐定理](#521-没有免费午餐定理)\n  - [5.2.2 正则化](#522-正则化)\n- [5.3 超参数和验证集](#53-超参数和验证集)\n  - [5.3.1 验证集的作用](#531-验证集的作用)\n  - [5.3.2 交叉验证](#532-交叉验证)\n- [5.4 估计、偏差和方差](#54-估计偏差和方差)\n  - [5.4.1 点估计](#541-点估计)\n  - [5.4.2 偏差](#542-偏差)\n  - [5.4.3 方差和标准差](#543-方差和标准差)\n  - [5.4.4 权衡偏差和方差以最小化均方误差](#544-权衡偏差和方差以最小化均方误差)\n- [5.5 最大似然估计](#55-最大似然估计)\n- [5.6 贝叶斯统计](#56-贝叶斯统计)\n- [5.7 监督学习算法](#57-监督学习算法)\n- [5.8 无监督学习算法](#58-无监督学习算法)\n  - [5.8.1 PCA 降维](#581-pca-降维)\n  - [5.8.2 k-均值聚类](#582-k-均值聚类)\n- [5.9 随机梯度下降](#59-随机梯度下降)\n- [5.10 构建机器学习算法 pipeline](#510-构建机器学习算法-pipeline)\n- [参考资料](#参考资料)\n\n> 本文大部分内容参考《深度学习》书籍，从中抽取重要的知识点，并对部分概念和原理加以自己的总结，适合当作原书的补充资料阅读，也可当作快速阅览机器学习原理基础知识的参考资料。\n\n## 前言\n\n**深度学习是机器学习的一个特定分支**。我们要想充分理解深度学习，必须对机器学习的基本原理有深刻的理解。\n\n大部分机器学习算法都有**超参数**（必须在学习算法外**手动设定**）。**机器学习本质上属于应用统计学**，其更加强调使用计算机对复杂函数进行**统计估计**，而较少强调围绕这些函数证明置信区间；因此我们会探讨两种统计学的主要方法: **频率派估计和贝叶斯推断**。同时，大部分机器学习算法又可以分成**监督学习**和**无监督学习**两类；本文会介绍这两类算法定义，并给出每个类别中一些算法示例。\n\n本章内容还会介绍如何组合不同的算法部分，例如优化算法、代价函数、模型和数据 集，来建立一个机器学习算法。最后，在 5.11 节中，我们描述了一些限制传统机器学习泛化能力的因素。正是这些挑战推动了克服这些障碍的深度学习算法的发展。\n> 大部分深度学习算法都是基于随机梯度下降算法进行求解的。\n\n## 5.1 学习算法\n\n机器学习算法是一种能够从数据中学习的算法。这里所谓的“学习“是指：“如果计算机程序在任务 $T$ 中的性能（以 $P$ 衡量）随着经验 $E$ 而提高，则可以说计算机程序从经验 $E$ 中学习某类任务 $T$ 和性能度量 $P$。”-来自 `Mitchell` (`1997`)\n> 经验 $E$，任务 $T$ 和性能度量 $P$ 的定义范围非常宽广，本文不做详细解释。\n\n### 5.1.1 任务 $T$\n\n从 “任务” 的相对正式的定义上说，学习过程本身不能算是任务。学习是我们所谓的获取完成任务的能力。机器学习可以解决很多类型的任务，一些非常常见的机器学习任务列举如下：\n1. **分类**：在这类任务中，计算机程序需要指定某些输入属于 $k$ 类中的哪一类，例如图像分类中的二分类问题，多分类、单标签问题、多分类多标签问题。\n2. **回归**：在这类任务中，计算机程序需要对给定输入预测数值。为了解决这个任务，学习算法需要输出函数 $f : \\mathbb{R}^n \\to \\mathbb{R}$。除了返回结果的形式不一样外，这类 问题和分类问题是很像的。\n3. **机器翻译**\n4. **结构化输出**\n5. **异常检测**\n6. **合成和采样**\n7. **去噪**\n8. **密度估计或概率质量函数估计**\n9. **输入缺失分类**\n10. **转录**\n11. **缺失值填补**\n\n### 5.1.2 性能度量 $P$\n\n为了评估机器学习算法的能力，我们必须设计其性能的**定量度量**，即算法的精度指标。通常，性能度量 $P$ 特定于系统正在执行的任务 $T$。\n> 可以理解为不同的任务有不同的性能度量。\n\n对于诸如分类、缺失输入分类和转录任务，我们通常度量模型的**准确率**(`accu- racy`)。准确率是指该模型输出正确结果的样本比率。我们也可以通过**错误率**(`error rate`)得到相同的信息。错误率是指该模型输出错误结果的样本比率。\n\n我们使用测试集（`test set`）数据来评估系统性能，将其与训练机器学习系统的训练集数据分开。\n\n值得注意的是，性能度量的选择或许看上去简单且客观，但是选择一个与系统理想表现能对应上的性能度量通常是很难的。\n\n### 5.1.3 经验 $E$\n\n根据学习过程中的不同经验，机器学习算法可以大致分类为两类\n\n- **无监督**(`unsuper-vised`)算法\n- **监督**(`supervised`)算法\n\n**无监督学习算法**(`unsupervised learning algorithm`)训练含有很多特征的数据集，然后学习出这个数据集上有用的结构性质。在深度学习中，我们通常要学习生成数据集的整个概率分布，显式地，比如密度估计，或是隐式地，比如合成或去噪。 还有一些其他类型的无监督学习任务，例如聚类，将数据集分成相似样本的集合。\n\n**监督学习算法**(`supervised learning algorithm`)也训练含有很多特征的数据集，但与无监督学习算法不同的是**数据集中的样本都有一个标签**(`label`)或目标(`target`)。例如，`Iris` 数据集注明了每个鸢尾花卉样本属于什么品种。监督学习算法通过研究 `Iris` 数据集，学习如何根据测量结果将样本划分为三个不同品种。\n\n**半监督学习算法**中，一部分样本有监督目标，另外一部分样本则没有。在多实例学习中，样本的整个集合被标记为含有或者不含有该类的样本，但是集合中单独的样本是没有标记的。\n\n大致说来，无监督学习涉及到观察随机向量 $x$ 的好几个样本，试图显式或隐式地学习出概率分布 $p(x)$，或者是该分布一些有意思的性质; 而监督学习包含观察随机向量 $x$ 及其相关联的值或向量 $y$，然后从 $x$ 预测 $y$，通常是估计 $p(y | x)$。术语监督学习(`supervised learning`)源自这样一个视角，教员或者老师提供目标 $y$ 给机器学习系统，指导其应该做什么。在无监督学习中，没有教员或者老师，算法必须学会在没有指导的情况下理解数据。\n\n无监督学习和监督学习并不是严格定义的术语。它们之间界线通常是模糊的。很多机器学习技术可以用于这两个任务。\n\n尽管无监督学习和监督学习并非完全没有交集的正式概念，它们确实有助于粗略分类我们研究机器学习算法时遇到的问题。传统地，人们将回归、分类或者结构化输出问题称为监督学习。支持其他任务的密度估计通常被称为无监督学习。\n\n表示数据集的常用方法是**设计矩阵**(`design matrix`)。\n\n### 5.1.4 示例: 线性回归\n\n我们将机器学习算法定义为，通过经验以提高计算机程序在某些任务上性能的算法。这个定义有点抽象。为了使这个定义更具体点，我们展示一个简单的机器学习示例: **线性回归**(`linear regression`)。\n\n顾名思义，**线性回归解决回归问题**。 换句话说，目标是构建一个系统，该系统可以将向量 $x \\in \\mathbb{R}$ 作为输入，并预测标量 $y \\in \\mathbb{R}$ 作为输出。在线性回归的情况下，输出是输入的线性函数。令 $\\hat{y}$ 表示模型预测值。我们定义输出为\n\n$$\\hat{y} = w^{⊤}x \\tag{5.3}$$\n\n其中 $w \\in \\mathbb{R}^{n}$ 是**参数**(`parameter`)向量。\n\n参数是控制系统行为的值。在这种情况下，$w_i$ 是系数，会和特征 $x_i$ 相乘之 后全部相加起来。我们可以将 $w$ 看作是一组决定每个特征如何影响预测的权重 (weight)。\n\n通过上述描述，我们可以定义任务 $T$ : 通过输出 $\\hat{y} = w^{⊤}x$ 从 $x$ 预测 $y$。\n\n我们使用**测试集**（`test set`）来评估模型性能如何，将输入的设计矩 阵记作 $\\textit{X}$(test)，回归目标向量记作 $y$(test)。\n\n**回归任务**常用的一种模型性能度量方法是计算模型在测试集上的 **均方误差**(`mean squared error`)。如果 $\\hat{y}$(`test`) 表示模型在测试集上的预测值，那么均方误差表示为:\n\n$$MSE_{test} = \\frac{1}{m} \\sum_{i}(\\hat{y}^{(test)}-y^{(test)})_{i}^{2} \\tag{5.4}$$\n\n直观上，当 $\\hat{y}^{(test)}$ = $y^{(test)}$ 时，我们会发现误差降为 0。\n\n图 5.1 展示了线性回归算法的使用示例。\n\n![图5.1-一个线性回归的例子](../data/images/ml_basc_principle/图5.1-一个线性回归的例子.png)\n\n## 5.2 容量、过拟合和欠拟合\n\n机器学习的挑战主要在于算法如何在测试集（先前未观测的新输入数据）上表现良好，而不只是在训练集上表现良好，即训练误差和泛化误差读比较小，也可理解为算法泛化性比较好。所谓泛化性（generalized）好指的是，算法在在测试集（以前未观察到的输入）上表现良好。\n\n机器学习算法的两个主要挑战是: **欠拟合**（`underfitting`）和**过拟合**（`overfitting`）。\n- 欠拟合是指模型不能在训练集上获得足够低的误差。\n- 而过拟合是指训练误差和和测试误差之间的差距太大。\n\n通过调整模型的**容量**（`capacity`），我们可以控制模型是否偏向于过拟合或者欠拟合。通俗地讲，**模型的容量是指其拟合各种函数的能力**。容量低的模型可能很难拟合训练集，容量高的模型可能会过拟合，因为记住了不适用于测试集的训练集性质。\n\n一种控制训练算法容量的方法是选择**假设空间**(`hypothesis space`)，即**学习算法可以选择作为解决方案的函数集**。例如，线性回归算法将其输入的所有线性函数的集合作为其假设空间。我们可以推广线性回归以在其假设空间中包含多项式，而不仅仅是线性函数。这样做就增加模型的容量。\n\n> 注意，学习算法的效果不仅很大程度上受影响于假设空间的函数数量，也取决于这些函数的具体形式。\n\n当机器学习算法的容量适合于所执行任务的复杂度和所提供训练数据的数量时，算法效果通常会最佳。容量不足的模型不能解决复杂任务。容量高的模型能够解决复杂的任务，但是当其容量高于任务所需时，有可能会过拟合。\n\n图 5.2 展示了上述原理的使用情况。我们比较了线性，二次和 `9` 次预测器拟合真实二次函数的效果。\n\n![图5-2三个模型拟合二次函数](../data/images/ml_basc_principle/图5-2三个模型拟合二次函数.png)\n\n提高机器学习模型泛化性的早期思想是**奥卡姆剃刀**原则，即选择“最简单”的那一个模型。\n\n统计学习理论提供了量化模型容量的不同方法。在这些中，最有名的是 **Vapnik- Chervonenkis 维度**(Vapnik-Chervonenkis dimension, VC)。`VC` 维度量二元分类 器的容量。`VC` 维定义为该分类器能够分类的训练样本的最大数目。假设存在 $m$ 个 不同 $x$ 点的训练集，分类器可以任意地标记该 $m$ 个不同的 $x$ 点，`VC` 维被定义为 $m$ 的最大可能值。\n\n因为可以量化模型的容量，所以使得统计学习理论可以进行量化预测。统计学习理论中最重要的结论阐述了训练误差和泛化误差之间差异的上界随着模型容量增长而增长，但随着训练样本增多而下降 (`Vapnik and Chervonenkis, 1971`; `Vapnik, 1982`; `Blumer et al., 1989`; `Vapnik, 1995`)。这些边界为机器学习算法可以有效解决问题提供了理论 验证，但是它们**很少应用于实际中的深度学习算法**。一部分原因是边界太松，另一部分原因是**很难确定深度学习算法的容量**。由于有效容量受限于优化算法的能力，所以确定深度学习模型容量的问题特别困难。而且我们对深度学习中涉及的非常普遍的**非凸优化问题**的理论了解很少。\n\n虽然更简单的函数更可能泛化(训练误差和测试误差的差距小)，但我们仍然必须选择一个足够复杂的假设来实现低训练误差。通常，随着模型容量的增加，训练误差会减小，直到它逐渐接近最小可能的误差值（假设误差度量具有最小值）。通常，**泛化误差是一个关于模型容量的 U 形曲线函数**。如下图 `5.3` 所示。\n\n![容量和误差之间的典型关系](../data/images/ml_basc_principle/图5-3model_capacity.png)\n\n### 5.2.1 没有免费午餐定理\n\n机器学习的**没有免费午餐定理**（`Wolpert，1996`）指出，对所有可能的数据生成分布进行平均，每个分类算法在对以前未观察到的点进行分类时具有相同的错误率。换句话说，在某种意义上，**没有任何机器学习算法普遍优于其他任何算法**。\n\n上述这个结论听着真的让人伤感，但庆幸的是，这些结论仅在我们考虑**所有可能的数据生成分布**时才成立。如果我们对实际应用中遇到的概率分布类型做出假设，那么我们可以**设计出在这些分布上表现良好的学习算法**。\n\n这意味着机器学习研究的目标**不是找一个通用学习算法或是绝对最好的学习算法**。反之，我们的目标是理解什么样的分布与人工智能获取经验的 “真实世界” 相关，**什么样的学习算法在我们关注的数据生成分布上效果最好**。\n\n**总结**：没有免费午餐定理清楚地阐述了没有最优的学习算法，即暗示我们必须在特定任务上设计性能良好的机器学习算法。\n\n### 5.2.2 正则化\n\n所谓正则化，是指我们通过**修改学习算法，使其降低泛化误差而非训练误差**的方法。\n\n**正则化是一种思想（策略）**，它是机器学习领域的中心问题之一，其重要性只有优化能与其相媲美。\n\n一般地，正则化一个学习函数 $f(x;\\theta)$ 的模型，我们可以给代价函数添加被称为**正则化项**（regularizer）的惩罚。如果正则化项是 $\\Omega(w) = w^{\\top}w$，则称为**权重衰减**（weight decay）。\n\n例如，我们可以加入权重衰减(weight decay)来修改线性回归的目标函数，如偏好于平方 $L^2$ 范数较小权重的目标函数形式:\n\n$$J(w) = MSE_{train} + \\lambda w^{⊤}w \\tag{5.18}$$\n\n其中 $J(w)$ 是目标函数，$w$ 是权重参数。$\\lambda$ 是超参数，需提前设置，**其控制我们对较小权重的偏好强度**。当 $\\lambda = 0$，我们没有任何偏好。$\\lambda$ 越大，则权重越小。最小化 $J(w)$ 会导致权重的选择在**拟合训练数据和较小权重之间进行权衡**。\n\n**和上一节没有最优的学习算法一样，一样的，也没有最优的正则化形式**。反之，我们必须挑选一个非常适合于我们所要解决的任务的正则形式。\n\n## 5.3 超参数和验证集\n\n**超参数的值不是通过学习算法本身学习出来的，而是需要算法定义者手动指定的**。\n\n### 5.3.1 验证集的作用\n\n通常，`80%` 的训练数据用于训练，`20%` 用于验证。验证集是用于估计训练中或训练后的泛化误差，从而**更新超参数**。\n\n### 5.3.2 交叉验证\n\n一个**小规模的测试集**意味着平均测试误差估计的统计不确定性，使得很难判断算法 A 是否比算法 B 在给定的任务上做得更好。解决办法是基于在原始数据上**随机采样或分离**出的不同数据集上**重复训练和测试**，最常见的就是 $k$-折交叉验证，即将数据集分成 $k$ 个 不重合的子集。测试误差可以估计为 $k$ 次计算后的平均测试误差。在第 $i$ 次测试时， 数据的第 $i$ 个子集用于测试集，其他的数据用于训练集。算法过程如下所示。\n> k 折交叉验证虽然一定程度上可以解决小数据集上测试误差的不确定性问题，但代价则是增加了计算量。\n\n![k-折交叉验证算法](../data/images/ml_basc_principle/k-fold.png)\n\nk-折交叉验证的训练集划分方式如下图所示：\n\n![image](https://user-images.githubusercontent.com/37138671/114163327-a0dfe200-995c-11eb-9a0e-4ce4fe13150c.png)\n\n+ 当 `K` 值大的时候， 我们会有更少的 `Bias`(偏差), 更多的 `Variance`。\n+ 当 `K` 值小的时候， 我们会有更多的 `Bias`(偏差), 更少的 `Variance`。\n\n`K` 折交叉验证的代码实现可以参考《Python深度学习》第三章，在模型训练好后，可通过计算所有 `Epoch` 的 `K` 折验证分数的平均值，并绘制每轮的模型验证指标变化曲线，观察哪个 `Epoch` 后模型不再收敛，从而完成模型调参工作。同时，`K` 折交叉验证方式训练模型会得到 `K`个模型，将这个 `K` 个模型在测试集上的推理结果取平均值或者投票，也是一种 `Ensemble` 方式，可以增强模型泛化性，防止过拟合。\n\n```python\n# 计算所有轮次中的 K 折验证分数平均值\naverage_mae_history = [np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]\n```\n\n## 5.4 估计、偏差和方差\n\n统计领域为我们提供了很多工具来实现机器学习目标，不仅可以解决训练集上 的任务，还可以泛化。基本的概念，例如参数估计、偏差和方差，对于正式地刻画泛化、欠拟合和过拟合都非常有帮助。\n\n### 5.4.1 点估计\n\n点估计试图为一些感兴趣的量提供单个 ‘‘最优’’ 预测。一般地，感兴趣的量可以是单个参数也可以是一个向量参数，例如第 5.1.4 节线性回归中的权重，但是也有可能是整个函数。\n\n为了区分参数估计和真实值，我们习惯将参数 $\\theta$ 的点估计表示为 $\\hat{\\theta}$。\n\n令 ${x^{(1)}, . . . , x^{(m)}}$ 是 $m$ 个独立同分布（i.i.d.）的数据点。 点估计(point esti-mator)或统计量(statistics)是这些数据的任意函数:\n$$\n\\hat{\\theta_m} =g(x^{(1)},...,x^{(m)}). \\tag{5.19}\n$$\n\n\n### 5.4.2 偏差\n\n估计的偏差定义如下:\n$$\nbias(\\hat{\\theta_m}) = E(\\hat{\\theta_m}) − \\theta, \\tag{5.19}\n$$\n其中期望作用在所有数据(看作是从随机变量采样得到的)上，$\\hat{\\theta}$ 是用于定义数据生成分布的 $\\theta$ 的真实值。如果 $bias(\\hat{\\theta_m}) = 0$，那么估计量 $\\hat{\\theta_m}$ 则被称为是**无偏** (unbiased)，同时意味着 $E(\\hat{\\theta_m}) = \\theta$。\n\n### 5.4.3 方差和标准差\n\n方差记为 $Var(\\hat{\\theta})$ 或 $\\sigma^{2}$，方差的平方根被称为标准差。\n\n### 5.4.4 权衡偏差和方差以最小化均方误差\n\n偏差和方差度量着估计量的两个不同误差来源。偏差度量着偏离真实函数或参数的误差期望。而方差度量着数据上任意特定采样可能导致的估计期望的偏差。\n\n**偏差和方差的关系和机器学习容量、欠拟合和过拟合的概念紧密相联**。用 MSE 度量泛化误差(偏差和方差对于泛化误差都是有意义的)时，增加容量会增加方差，降低偏差。如图 5.6 所示，我们再次在关于容量的函数中，看到泛化误差的 U 形曲线。\n\n![偏差方差泛化误差和模型容量的关系](../data/images/ml_basc_principle/图5-6.png)\n\n## 5.5 最大似然估计\n\n与其猜测某个函数可能是一个好的估计器，然后分析它的偏差和方差，我们更希望有一些原则，我们可以从中推导出特定的函数，这些函数是不同模型的良好估计器。最大似然估计就是其中最为常用的准则。\n\n## 5.6 贝叶斯统计\n\n到目前为止，我们已经描述了**频率统计**（frequentist statistics）和基于估计单一 $\\theta$  值的方法，然后基于该估计作所有的预测。 另一种方法是在进行预测时考虑所有可能的 $\\theta$ 值。 后者属于**贝叶斯统计**（Bayesian statistics）的范畴。\n\n如 5.4.1 节中讨论的，频率派的视角是真实参数 $\\theta$  是未知的定值，而点估计  $\\hat{\\theta}$ 是考虑数据集上函数(可以看作是随机的)的随机变量。\n\n贝叶斯统计的视角完全不同。贝叶斯用概率反映知识状态的确定性程度。数据集能够被直接观测到，因此不是随机的。另一方面，真实参数 $\\theta$ 是未知或不确定的， 因此可以表示成随机变量。\n\n## 5.7 监督学习算法\n\n回顾 5.1.3 节内容，简单来说，监督学习算法是给定一组输入 $x$ 和输出 $y$ 的训练 集，学习如何关联输入和输出。在许多情况下，输出 $y$ 可能难以自动收集，必须由人类“监督者”提供，但即使训练集目标是自动收集的，该术语仍然适用。\n\n## 5.8 无监督学习算法\n\n回顾第5.1.3节，无监督算法只处理 “特征’’，不操作监督信号。监督和无监督算法之间的区别没有规范严格的定义，因为没有客观的测试来区分一个值是特征还是监督者提供的目标。通俗地说，无监督学习的大多数尝试是指从不需要人为注释的样本的分布中提取信息。该术语通常与密度估计相关，学习从分布中采样、学习从分布中去噪、寻找数据分布的流形或是将数据中相关的样本聚类。\n\n### 5.8.1 PCA 降维\n\n`PCA`（Principal Component Analysis）是学习数据表示的无监督学习算法，常用于高维数据的降维，可用于提取数据的主要特征分量。\n\nPCA 的数学推导可以从最大可分型和最近重构性两方面进行，前者的优化条件为划分后方差最大，后者的优化条件为点到划分平面距离最小。\n\n### 5.8.2 k-均值聚类\n\n另外一个简单的表示学习算法是 $k$-均值聚类。$k$-均值聚类算法将训练集分成 $k$ 个靠近彼此的不同样本聚类。因此我们可以认为该算法提供了 $k$-维的 one-hot 编码向量 $h$ 以表示输入 $x$。当 $x$ 属于聚类 $i$ 时，有 $h_i = 1$，$h$ 的其他项为零。\n\n$k$-均值聚类初始化 k 个不同的中心点 ${μ^{(1)}, . . . , μ^{(k)}}$，然后迭代交换以下两个不同的步骤直到算法收敛。\n\n1. 步骤一，每个训练样本分配到最近的中心点 $μ^{(i) }$ 所代表的聚类 $i$。 \n2. 步骤二，每一个中心点 $μ^{(i) }$ 更新为聚类 $i$ 中所有训练样本 $x^{(j)}$ 的均值。\n\n关于聚类的一个问题是**聚类问题本身是病态的**。这是说没有单一的标准去度量聚类的数据在真实世界中效果如何。我们可以度量聚类的性质，例如类中元素到类中心点的欧几里得距离的均值。这使我们可以判断从聚类分配中重建训练数据的效果如何。然而我们不知道聚类的性质是否很好地对应到真实世界的性质。此外，可能有许多不同的聚类都能很好地对应到现实世界的某些属性。我们可能希望找到和 一个特征相关的聚类，但是得到了一个和任务无关的，同样是合理的不同聚类。\n\n例如，假设我们在包含红色卡车图片、红色汽车图片、灰色卡车图片和灰色汽车图片的数据集上运行两个聚类算法。如果每个聚类算法聚两类，那么可能一个算法将汽车和卡车各聚一类，另一个根据红色和灰色各聚一类。假设我们还运行了第三个聚类算法，用来决定类别的数目。这有可能聚成了四类，红色卡车、红色汽车、灰色卡 车和灰色汽车。现在这个新的聚类至少抓住了属性的信息，但是丢失了相似性信息。 红色汽车和灰色汽车在不同的类中，正如红色汽车和灰色卡车也在不同的类中。该聚类算法没有告诉我们灰色汽车和红色汽车的相似度比灰色卡车和红色汽车的相似度更高，我们只知道它们是不同的。\n\n## 5.9 随机梯度下降\n\n几乎所有的深度学习算法都用到了一个非常重要的优化算法: **随机梯度下降** (stochastic gradient descent, `SGD`)。\n\n机器学习中反复出现的一个问题是好的泛化需要大的训练集，但大的训练集的 计算代价也更大。\n\n机器学习算法中的代价函数通常可以分解成每个样本的代价函数的总和。\n\n![sgd](../data/images/ml_basc_principle/sgd.png)\n\n随机梯度下降的核心是，**梯度是期望**，而期望可使用小规模的样本近似估计。具体来说，在算法的每一步，我们从训练集中均匀抽出一小批量(`minibatch`)样本 $B={x^{(1)},...,x^{(m′)}}$。小批量的数目 $m′$ 通常是一个相对较小的数，一般为 $2^n$（取决于显卡显卡）。重要的是，当训练集大小 m 增长时，$m′$ 通常是固定的。我们可能在拟合几十亿的样本时，但每次更新计算只用到几百个样本。\n\n![sgd2](../data/images/ml_basc_principle/sgd2.png)\n\n梯度下降往往被认为很慢或不可靠。以前，将梯度下降应用到非凸优化问题被认为很鲁莽或没有原则。但现在，我们知道梯度下降用于深度神经网络模型的训练时效果是不错的。优化算法不一定能保证在合理的时间内达到一个局部最小值，但它通常能及时地找到代价函数一个很小的值，并且是有用的。\n\n随机梯度下降在深度学习之外有很多重要的应用。它是在大规模数据上训练大型线性模型的主要方法。对于固定大小的模型，每一步随机梯度下降更新的计算量不取决于训练集的大小 $m$。在实践中，当训练集大小增长时，我们通常会使用一个更大的模型，但这并非是必须的。达到收敛所需的更新次数通常会随训练集规模增大而增加。然而，当 $m$ 趋向于无穷大时，该模型最终会在随机梯度下降抽样完训练 集上的所有样本之前收敛到可能的最优测试误差。继续增加 $m$ 不会延长达到模型可能的最优测试误差的时间。从这点来看，我们可以认为用 SGD 训练模型的渐近代价是关于 $m$ 的函数的 $O(1)$ 级别。\n\n## 5.10 构建机器学习算法 pipeline\n\n几乎所有的深度学习算法都可以被描述为一个相当简单的 `pipeline`: \n\n1. 特定的数据集\n2. 代价函数\n3. 优化过程\n4. 神经网络模型。\n\n## 参考资料\n\n- 《深度学习》\n- [【机器学习】降维——PCA（非常详细）](https://zhuanlan.zhihu.com/p/77151308)\n\n"
  },
  {
    "path": "3-machine_learning/机器学习基本概念总结.md",
    "content": "- [一，余弦相似度与欧氏距离](#一余弦相似度与欧氏距离)\n  - [1.1，余弦相似度](#11余弦相似度)\n  - [1.2，欧式距离](#12欧式距离)\n  - [1.3，余弦相似度和欧氏距离的区别](#13余弦相似度和欧氏距离的区别)\n- [二，Bias(偏差)和Varience(方差)](#二bias偏差和varience方差)\n- [2.1，概念定义](#21概念定义)\n  - [2.2，图形定义](#22图形定义)\n  - [2.3，数学定义](#23数学定义)\n  - [2.4，导致偏差和方差的原因](#24导致偏差和方差的原因)\n  - [2.5，深度学习中的偏差与方差](#25深度学习中的偏差与方差)\n- [三，模型容量、过拟合和欠拟合](#三模型容量过拟合和欠拟合)\n- [四，样本方差与总体方差](#四样本方差与总体方差)\n  - [4.1，方差定义](#41方差定义)\n- [五，先验概率与后验概率](#五先验概率与后验概率)\n  - [5.1，条件概率](#51条件概率)\n  - [5.2，先验概率](#52先验概率)\n  - [5.3，后验概率](#53后验概率)\n  - [5.4，贝叶斯公式](#54贝叶斯公式)\n  - [5.5，后验概率实例](#55后验概率实例)\n- [六，相对熵(KL散度)与交叉熵](#六相对熵kl散度与交叉熵)\n  - [6.1，信息熵](#61信息熵)\n  - [6.2，相对熵/KL散度](#62相对熵kl散度)\n  - [6.3，交叉熵 cross-entroy](#63交叉熵-cross-entroy)\n  - [6.4，为什么交叉熵可以用作代价](#64为什么交叉熵可以用作代价)\n  - [6.5，KL 散度与交叉熵的关系](#65kl-散度与交叉熵的关系)\n- [七，随机梯度下降算法](#七随机梯度下降算法)\n  - [八，超参数和验证集](#八超参数和验证集)\n- [九，正则化方法](#九正则化方法)\n- [参考资料](#参考资料)\n\n> 深度学习是机器学习的一个特定分支，要想充分理解深度学习，就必须对机器学习的基本原理有深刻的理解。机器学习的本质属于应用统计学，其更多地关注如何用计算机统计地估计复杂函数，而不太关注为这些函数提供置信区间，大部分机器学习算法可以分成监督学习和无监督学习两类；通过组合不同的算法部分，例如优化算法、代价函数、模型和数据集可以建立一个完整的机器学习算法。\n\n## 一，余弦相似度与欧氏距离\n\n### 1.1，余弦相似度\n\n通过对两个文本分词，`TF-IDF` 算法向量化，利用空间中两个向量的夹角，来判断这两个向量的相似程度：(`计算夹角的余弦，取值 0-1`)\n\n+ 当两个向量夹角越大，距离越远，最大距离就是两个向量夹角 180°；\n+ 夹角越小，距离越近，最小距离就是两个向量夹角 0°，完全重合。\n+ 夹角越小相似度越高，但由于有可能一个文章的特征向量词特别多导致整个向量维度很高，使得计算的代价太大不适合大数据量的计算。\n\n**计算两个向量a、b的夹角余弦：**\n我们知道，余弦定理：$cos(\\theta) = \\frac {a^2+b^2+c^2}{2ab}$ ，由此推得两个向量夹角余弦的计算公式如下：\n$$cos(\\theta) = \\frac {ab}{||a|| \\times ||b||} = \\frac {x_{1}x_{2}+y_1y_2}{\\sqrt{x^2_1+y^2_1}\\sqrt{x^2_2+y^2_2}}$$\n（分子就是两个向量的内积，分母是两个向量的模长乘积）\n\n### 1.2，欧式距离\n> 欧式距离和 L2 范数计算公式相同。\n\n在欧几里得空间中，欧式距离其实就是向量空间中两点之间的距离。点 $x = (x_{1}, ..., x_{n})$ 和 $y = (y_{1}, ..., y_{n})$ 之间得欧氏距离计算公式如下：\n$$d(x,y) = \\sqrt {((x_{1}-y_{1})^{2} + (x_{2}-y_{2})^{2} + ... + (x_{n}-y_{n})^{2})}$$\n\n### 1.3，余弦相似度和欧氏距离的区别\n\n+ 欧式距离和余弦相似度都能度量 `2` 个向量之间的相似度\n+ 放到向量空间中看，欧式距离衡量`两点之间`的直线距离，而余弦相似度计算的是`两个向量`之间的夹角\n+ 没有归一化时，欧式距离的范围是 `[0, +∞]`，而余弦相似度的范围是 `[-1, 1]`；余弦距离是计算相似程度，而欧氏距离计算的是相同程度（对应值的相同程度）\n+ 归一化的情况下，可以将空间想象成一个超球面（三维），欧氏距离就是球面上两点的直线距离，而向量余弦值等价于两点的球面距离，本质是一样。\n\n## 二，Bias(偏差)和Varience(方差)\n\n当我们讨论预测模型时，预测误差可以分解为我们关心的两个主要子成分：“**偏差**”引起的误差和“**方差**”引起的误差。 在模型最小化偏差和方差的能力之间存在权衡。 了解这两类错误可以帮助我们诊断模型结果，避免出现过拟合或欠拟合的错误。\n \n> **有两个不同的概念都被称为“方差”**。一种是**理论概率分布的方差**。而另一种方差是一组观测值的特征(**统计意义上的方差**)。\n\n## 2.1，概念定义\n\n统计领域为我们提供了很多工具来实现机器学习目标，不仅可以解决训练集上的任务，还可以泛化。偏差-方差指标方法是试图对学习算法（模型）的期望泛化错误率进行拆解:\n\n$$Error = Bias + Varience$$\n> Varience(Error due to Variance), Bias(Error due to Bias)\n\n+ `Error`: 反映的是整个模型的准确度。\n+ `Bias`：偏差引起的误差被视为我们模型的预期（或平均）预测与我们试图预测的正确值之间的差异，即模型的**准确性**。当然，您只有一个模型，因此谈论预期或平均预测值可能看起来有点奇怪。但是，想象一下您可以多次重复整个模型构建过程：每次您收集新数据并运行新分析时都会创建一个新模型。由于基础数据集中的随机性，生成的模型将具有一系列预测。偏差通常衡量这些模型的预测与正确值之间的差距。\n+ `Varience`：方差引起的误差被视为给定数据点的模型预测的可变性，即**即模型的稳定性。同样，假设您可以多次重复整个模型构建过程。方差是给定点的预测在模型的不同实现之间的变化程度。\n+ `Bias`: 反映的是模型在**样本上的输出与真实值之间的误差**，即模型的准确性。以打靶事件为例，`low bias`，一般就得复杂化模型，表现出来就是点都打在靶心中间，但这样容易过拟合 (`overfitting`)，过拟合对应下图是 `high variance`，点很分散。\n+ `Varience`: 反映的是模型每一次输出的结果与模型输出期望之间的误差，即模型的稳定性，是训练集上训练出来的模型在测试集上的表现。同样以打靶事件为例，`low variance` 对应就是点都打的很集中，但不一定是靶心附近，手很稳，但是瞄的不准。\n\n### 2.2，图形定义\n\n`Bias` 和 `Varience` 的图形定义：\n\n我们可以使用靶心图创建偏差和方差的图形可视化。想象一下，目标的中心是一个可以完美预测正确值的模型。当我们远离靶心时，我们的预测会变得越来越糟。\n\n`Low Bias` 表现出来就是点都打在靶心中间，但这样容易过拟合 (`overfitting`)，过拟合对应下图是 `High Variance`，表现就是点很分散，没有集中在一起，手不稳啊（对应就是模型预测结果变化性太大）。\n`Low Variance` 对应就是点都打的很集中，但不一定是靶心附近，手很稳，但是瞄的不一定准。\n\n我们可以绘制四种不同的情况，代表高低偏差和方差的组合。\n![Graphical illustration of bias and variance](../data/images/ml_concept/Graphical_illustration_of_bias_variance.png)\n> 图片来源 [Understanding the Bias-Variance Tradeoff](http://scott.fortmann-roe.com/docs/BiasVariance.html)。\n\n总的来说，参数估计、偏差和方差虽然是统计领域的基本概念，但它们的关系也和机器学习的模型容量、欠拟合和过拟合的概念紧密相联。\n\n偏差和方差度量着估计量的两个不同误差来源：\n- **偏差度量着偏离真实函数或参数的误差期望**。\n- **方差度量着数据上任意特定采样可能导致的估计期望的偏差**。\n\n![模型容量和误差之间的典型关系1](../data/images/ml_concept/model_capacity_bias_varience.png)\n### 2.3，数学定义\n\n假设对测试样本 $x$, 令 $y_{D}$ 为 $x$ 在数据集中的标记，$y$ 为 $x$ 的真实标记， $f(x;D)$ 为在训练集 $D$ 上学习到的模型 $f$ 在 $x$ 上的预测输出。\n+ 训练过程中期望输出与真实标记（标签）的差别称为偏差（`bias`）：$bias^{2}(x) = (\\bar{f} - y)^{2}$\n+ （交叉验证训练模型）使用样本数相同不同训练集训练出来的模型在测试集上产生的`方差`为： $var(x) = E_{D}[(f(x;D) - \\bar{f})^{2}] $\n\n### 2.4，导致偏差和方差的原因\n>  [机器学习中的Bias(偏差)，Error(误差)，和Variance(方差)有什么区别和联系？](https://www.zhihu.com/question/27068705)\n\n+ 偏差通常是由于我们对学习算法做了错误的假设，或者模型的复杂度不够；\n    + 比如真实模型是一个二次函数，而我们假设模型为一次函数，这就会导致偏差的增大（欠拟合）；\n    + 由偏差引起的误差通常在训练误差上就能体现，或者说训练误差主要是由偏差造成的\n+ 方差通常是由于模型的复杂度相对于训练集过高导致的；\n    + 比如真实模型是一个简单的二次函数，而我们假设模型是一个高次函数，这就会导致方差的增大（过拟合）；\n    + 由方差引起的误差通常体现在测试误差相对训练误差的增量上。\n\n### 2.5，深度学习中的偏差与方差\n\n+ 神经网络的拟合能力非常强，因此它的训练误差（偏差）通常较小；\n+ 但是过强的拟合能力会导致较大的方差，使模型的测试误差（泛化误差）增大；\n+ 深度学习的核心工作之一就是**研究如何降低模型的泛化误差**，这类方法统称为`正则化方法`。\n\n## 三，模型容量、过拟合和欠拟合\n\n+  模型容量是指模型拟合各种函数的能力，决定了模型是欠拟合还是过拟合。\n+ **过拟合**：就是指训练误差和测试误差间距过大，即方差（`variance`）过大，表现为模型泛化性能下降即不够”稳“，正则化目的在于解决过拟合问题。\n+ **欠拟合**：和过拟合相反，指模型的训练误差过大，即偏差（`bias`）过大，表现为模型不够”准“，优化算法目的在于解决欠拟合问题。\n+  机器学习模型的目的是解决欠拟合和过拟合的问题，这也是机器学习算法的两个挑战。\n\n> 训练误差 `train error`，泛化误差 `generalization error`（也叫测试误差 `test error`)。\n\n模型容量与偏差、方差的关系图如下所示：\n\n![模型容量和误差之间的典型关系2](../data/images/ml_concept/model_capacity_under_over_fitting.png)\n\n从上图可以看出，当容量增大(x 轴)时，偏差(蓝色虚线)随之减小，而方差(绿色虚线)随之增大，使得泛 化误差(加粗曲线)产生了另一种 U 形。如果我们沿着轴改变容量，会发现**最佳容量**（optimal capacity），当容量小于最佳容量会呈现欠拟合，大于时导致过拟合。这种关系与第一章中讨论的容量、欠拟合和过拟合之间的关系类似。\n\n## 四，样本方差与总体方差\n> 本章中样本方差与总体方差概念是统计学意义上的。\n### 4.1，方差定义\n\n方差是在**概率论**和**统计学**中衡量随机变量或一组数据时离散程度的度量，在统计描述和概率分布中各有不同的定义，并有不同的公式。\n\n概率论中，方差(variance)衡量的是当我们对 $\\textrm{x}$ 依据它的概率分布进行采样时，随机变量 $\\textrm{x}$ 的函数值会呈现多大的差异，简单理解就是用来**度量随机变量和其数学期望之间的偏离程度**。\n\n统计学中，方差是一组观测值的特征，观测值通常是从真实世界的系统中测量的。如果给出系统的所有可能的观测，则它们算出的方差称为总体方差；然而，一般情况下我们只使用总体的一个子集（样本），由此计算出的方差称为样本方差。用样本计算出的方差可认为是对整个总体的方差的估计量。\n\n**1，均方误差（MSE，mean squared error）与均方根误差(RMSE)**\n\n均方误差是预测值与真实值之差的平方和的平均值，即误差平方和的平均数。计算公式形式上接近方差，它的开方叫均方根误差 `RMSE`，均方根误差才和标准差形式上接近。\n计算公式如下：\n\n$$\\frac{1}{n} \\sum_{i=1}^{n}[f(x_i)-y_i]^2$$\n\n在机器学习中均方误差常用作**预测和回归问题的损失函数**，均方误差越小，说明模型预测的越准确，反之则越不准确。\n\n**2，总体方差**\n\n**统计中的总体方差 $\\sigma^2$ 就是对整个总体运用方差计算方法得到的结果**，即样本实际值与实际值的总体平均值之差的平方和的平均值。\n> 另一种定义：各个样本误差之平方（而非取绝对值，使之肯定为正数）相加之后再除以总数。\n\n**总体方差**计算公式如下：\n\n$$\\sigma ^2 = \\frac{\\sum_{i=1}^{N}(X_{i}-\\mu)^2}{N}$$\n\n公式解析：\n1. 因为和样本数无关，所以分母为样本数\n2. 累加每个值和均值差值的平方，对应于每个值相对于均值的偏差，对应于离散程度，平方是对离散程度的加剧，同时能让差值总为正数，以符合偏差的概念意义\n3. $\\sigma$ 的平方表示总体方差，$X$ 表示变量，$\\mu $ 表示总体均值（也叫数学期望），$N$ 表示总体样本数量。\n\n由于方差是数据的平方，与检测值本身相差太大，难以直观的衡量，所以常用方差开根号换算回来，就成了标准差（Standard Deviation）用$\\sigma$ 表示。\n\n**3，样本方差**\n\n在实际项目中，总体均值很难获得，所以常**用样本方差来估计总体方差**（统计术语：样本方差是对总体方差的无偏估计）。所谓**样本方差**，是指样本各单位变量值与其算术平均数的离差平方的平均数。\n\n应用样本统计量替代总体参数，经校正后，样本方差的计算公式如下：\n$$\\sigma ^2 = \\frac{\\sum_{i=1}^{n-1}(X_{i}-\\overline{x_{i}..x_{n}})^2}{n-1}$$\n$\\overline{x_{i}..x_{n}}$ 表示样本均值公式分母由总体方差的 `N` 变为了 `n-1`，使得样本方差更能反映总体方差。\n\n## 五，先验概率与后验概率\n> 更多深入内容可参考《花书》第三章概率与信息论。\n\n### 5.1，条件概率\n\n一个事件发生后另一个事件发生的概率。设 A 与 B 为样本空间 Ω 中的两个事件，其中 P(B)>0。那么在事件 B 发生的条件下，事件 A 发生的条件概率为：\n\n$$\nP(A|B) = \\frac {P(A\\cap B)} {P(B)}\n$$\n\n### 5.2，先验概率\n\n事件发生前的概率，可以是基于以往经验/分析，也可以是基于历史数据的统计，甚至可以是人的主观观点给出。一般是**单独**事件概率，如 $P(x)$, $P(y)$。\n\n### 5.3，后验概率\n\n+ 事情已经发生，要求这件事情发生的原因是由某个因素引起的可能性的大小（**由果推因**：就是在知道“果”之后，去推测“因”的概率）\n+ 后验概率和和先验概率的关系可以通过`贝叶斯`公式求得，公式如下：\n$$\nP(B_{i}|A) = \\frac {P(B_{i}\\cdot P(A|B_{i}}{P(B_{1})\\cdot P(A|B_{1}) + P(B_{2})\\cdot P(A|B_{2}) }\n$$\n\n### 5.4，贝叶斯公式\n\n贝叶斯公式是建立在条件概率的基础上寻找事件发生的原因（即大事件 `A` 已经发生的条件下，分割中的小事件 `Bi` 的概率），设 `B1,B2,...` 是样本空间 `Ω` 的一个划分，则对任一事件 `A（P(A)>0)`, 有：$$P(B_{i}|A) = \\frac {P(A|B_{i})P(B_{i})}{\\sum_{j=1}^{n}P(B_{j})P(A|B_{j})}$$\n\n+ `Bi` 常被视为导致试验结果A发生的”原因“；\n+ `P(Bi)(i=1,2,...)` 表示各种原因发生的可能性大小，故称先验概率；\n+ `P(Bi|A)(i=1,2...)` 则反映当试验产生了结果A之后，再对各种原因概率的新认识，故称后验概率。\n\n### 5.5，后验概率实例\n\n假设一个学校里有 `60％` 男生和 `40%` 女生。女生穿裤子的人数和穿裙子的人数相等，所有男生穿裤子。一个人在远处随机看到了一个穿裤子的学生。那么这个学生是女生的概率是多少？\n+ 使用贝叶斯定理，事件A是看到女生，事件B是看到一个穿裤子的学生。我们所要计算的是 $P(A|B)$。\n+ $P(A)$ 是忽略其它因素，看到女生的概率，在这里是 40%；\n+ $P(A')$ 是忽略其它因素，看到不是女生（即看到男生）的概率，在这里是 60%；\n+ $P(B|A)$ 是女生穿裤子的概率，在这里是 50%；\n+ $P(B|A')$ 是男生穿裤子的概率，在这里是 100%；\n+ $P(B)$ 是忽略其它因素，学生穿裤子的概率，$P(B) = P(B|A)P(A) + P(B|A')P(A')$，在这里是 0.5×0.4 + 1×0.6 = 0.8。\n\n根据贝叶斯定理，我们计算出后验概率P(A|B):\n\n$$\nP(A|B) = \\frac {P(B|A)P(A)}{P(B)} = \\frac {0.5\\times 0.4} {0.8}\n$$\n\n## 六，相对熵(KL散度)与交叉熵\n\n> 也可参考文章[【直观详解】信息熵、交叉熵和相对熵](https://charlesliuyx.github.io/2017/09/11/%E4%BB%80%E4%B9%88%E6%98%AF%E4%BF%A1%E6%81%AF%E7%86%B5%E3%80%81%E4%BA%A4%E5%8F%89%E7%86%B5%E5%92%8C%E7%9B%B8%E5%AF%B9%E7%86%B5/)、[为什么交叉熵（cross-entropy）可以用于计算代价](https://www.zhihu.com/question/65288314)和[神经网络基础部件-损失函数详解](https://github.com/HarleysZhang/deep_learning_alchemy/blob/main/2-deep_learning_basic/%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80%E9%83%A8%E4%BB%B6-%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0%E8%AF%A6%E8%A7%A3.md)。\n\n### 6.1，信息熵\n\n**其实信息熵是香农信息量（$log\\frac{1}{p}$）的期望（均值），它不是针对每条信息，而是针对整个不确定性结果集而言，信息熵越大，事件不确定性就越大。单条信息只能从某种程度上影响结果集概率的分布**。信息熵定义：\n\n$$H(P) = \\sum_{i} P(i)log_{a} \\frac{1}{P(i)} = -\\sum_{i}P(i)log_{a} P(i)$$\n\n$P_{i}$ 表示第 $i$ 个事件发生得概率，总的来说信息熵其实从某种意义上反映了**信息量存储下来需要多少存储空间**。\n总结为：根据真实分布，我们能够找到一个最优策略，以**最小的代价消除系统的不确定性**（比如编码），而这个代价的大小就是**信息熵**。\n\n### 6.2，相对熵/KL散度\n\n`KL` 散度，有时候也叫 `KL 距离`，一般被用于计算两个分布之间的不同，记为 $D_{KL}(P||Q) = H(P,Q) - H(P)$，对于同一个离散随机变量 $\\textrm{x}$ 有两个单独的概率分布 $P(x)$ 和 $Q(x)$，其 `KL` 散度为：\n\n$$D_{KL}(P \\| Q) = \\sum_i P(i)log_{a} \\frac{P(i)}{Q(i)} = \\sum_i P(i)[logP(x) - log Q(x)]$$\n\n当 $P(i) = Q(i)$ 的时候，该值为 `0`，深度学习过程也是一个降低该值的过程，**该值越低，训练出来的概率 $Q$ 越接近样本集概率 $P$，即越准确**，或者可以理解为相对熵是一把标尺，用来衡量两个函数是否相似，相似就是 0。即，**相对熵 = 某个策略的交叉熵 - 信息熵**（根据系统真实分布计算而得的信息熵，为最优策略），**当信息熵为常量时，交叉熵与KL散度相等**。\n### 6.3，交叉熵 cross-entroy\n\n交叉熵是由信息熵而得来的，和 `KL` 散度关系密切，拓展用在机器学习/深度学习中作损失函数。假定在确定性更大的概率分布情况下，用更不确定的存储策略来计算，比如使用 `P` 的概率乘上 `Q` 的存储因子，套用信息熵公式：\n$$H(P,Q) = \\sum_{i} P(i)log_{a} \\frac{1}{Q(i)} = -\\sum_{i}P(x_i)log_{a} Q(x_i)$$\n用预测概率 $q$ 分布，去编码真实标签 $p$ 的分布，得到的信息量。**交叉熵，用来衡量在给定的真实分布下，使用非真实分布指定的策略消除系统的不确定性所需要付出努力的大小**。总的来说，我们的目的是：让熵尽可能小，即存储空间小（消除系统的不确定的努力小）。**交叉熵的一些性质：**\n\n+ 非负。\n+ 和 `KL` 散度相同，交叉熵也不具备对称性，即 $H(P,Q)≠H(Q,P)$。\n+ 交叉熵主要用于描述两个事件之间的相互关系，对同一个分布求交叉熵等于对其求熵\n\n### 6.4，为什么交叉熵可以用作代价\n\n从数学上来理解就是，为了让学到的模型分布更接近真实数据的分布，我们需要最小化模型数据分布与训练数据之间的 `KL 散度`，而因为训练数据的分布是固定的，因此最小化 `KL 散度`等价于最小化交叉熵，而且交叉熵计算更简单，所以机器/深度学习中常用交叉熵 `cross-entroy` 作为分类问题的损失函数。\n\n使用交叉熵损失大大提高了具有 `sigmoid` 和 `softmax` 输出的模型的性能，而当使用均方误差损失时会存在饱和和学习缓慢的问题。\n\n### 6.5，KL 散度与交叉熵的关系\n\n+ `KL` 散度和交叉熵在特定条件下等价\n+ $D_{KL}(P||Q) = H(P,Q) - H(P)$\n\n## 七，随机梯度下降算法\n\n+ 随机梯度下降算法是目前最为广泛应用的一种**神经网络优化算法**，形式为 $θ=θ − ϵg$，$ϵ$ 是学习率，$g$ 是梯度，$θ$ 是权重。\n+ 随机梯度下降优化算法不一定能保证在合理的时间内达到一个局部最小值，但它通常能及时地找到代价函数一个很小的值，并且是有用的。\n\n### 八，超参数和验证集\n\n+ 普通参数指算法权重 $w$ 的值，是可以通过学习算法本身学习得到。**超参数的值不是通过学习算法本身学习出来的，可通过验证集人为选择合适的超参数**。\n+ 将训练数据划分为两个不相交的子集，即训练集和验证集，训练集用于学习普通参数，验证集用于估计训练中或训练后的泛化误差，更新超参数（“训练超参数”）。通常，`80%` 的训练数据用于训练，`20%` 用于验证。\n+ 交叉验证方法适合小规模数据集（例如几百上千张图片）训练模型的情况。\n\n\n## 九，正则化方法\n\n所谓正则化（Regularization），是指我们通过**修改学习算法，使其降低泛化误差而非训练误差**的方法。\n\n**正则化是一种思想（策略）**，它是机器学习领域的中心问题之一，其重要性只有优化能与其相媲美。\n\n一般正则化一个模型，通常通过对**原始损失函数**引入**额外信息**（也叫惩罚项/正则化项），新的损失函数变成了**原始损失函数 + 惩罚项**对形式。惩罚项通常是对权重参数做一些限制，如 L1/L2 范数。如果正则化项是 $\\Omega(w) = w^{\\top}w$，则称为**权重衰减**（weight decay）。\n\n在标准的随机梯度下降中，权重衰减正则化和 L2 正则化的效果相同。因此，权重衰减在一些深度学习框架中通过 L2 正则化来实现。加入了正则化项后的损失函数的变化如下：\n\n对于均方差损失函数：\n\n$$J(w,b)=\\frac{1}{2m}\\sum_{i=1}^m (z_i-y_i)^2 + \\frac{\\lambda}{2m}\\sum_{j=1}^n{w_j^2}$$\n\n对于交叉熵损失函数：\n\n$$J(w,b)= -\\frac{1}{m} \\sum_{i=1}^m [y_i \\ln a_i + (1-y_i) \\ln (1-a_i)]+ \\frac{\\lambda}{2m}\\sum_{j=1}^n{w_j^2} \\tag{6}$$\n\n其中 $J(w, b)$ 是最终损失函数，$w$ 是权重参数。$\\lambda$ 是正则化项的超参数，需提前设置，**其控制我们对较小权重的偏好强度**。当 $\\lambda = 0$，我们没有任何偏好。$\\lambda$ 越大，则权重越小。最小化 $J(w)$ 会导致权重的选择在**拟合训练数据和较小权重之间进行权衡**。\n> 值得注意的是。在较为复杂的优化方法(比如 Adam)中，权重衰减正则化和 L2 正则化并不等价 [Loshchilov et al., 2017b]。\n\n**和上一节没有最优的学习算法一样，一样的，也没有最优的正则化形式**。反之，我们必须挑选一个非常适合于我们所要解决的任务的正则形式。\n\n那么，**L2 正则化为什么能防止过拟合**呢？\n\n我个人理解是，我们一般默认模型参数越小、模型越简单，越简单的模型越不容易过拟合。而 L2 正则化的模型拟合过程中通常都倾向于让权值尽可能小，即最后能构造出一个所有参数都比较小的模型。\n\n常用的参数正则化策略有 L1 和 L2 范数。L1 范数（`L1 norm`）是指向量中各个元素绝对值之和，也有个美称叫“稀疏规则算子”（Lasso regularization）。 比如，向量 $A = [1，-1，3]$， 那么 A 的 L1 范数为 $|1|+|-1|+|3|$。简单总结就是：\n\n+ L1 范数: 为向量 x 各个元素绝对值之和。\n+ L2 范数: 为向量 x 各个元素平方和的 1/2 次方，L2 范数又称 Euclidean 范数或 Frobenius 范数\n+ Lp 范数: 为向量 x 各个元素绝对值 $p$ 次方和的 $1/p$ 次方.\n\n`L1` 范数可以使权值参数稀疏，方便特征提取。L2 范数可以防止过拟合，提升模型的泛化能力。\n\n## 参考资料\n\n1. [机器学习中的Bias(偏差)，Error(误差)，和Variance(方差)有什么区别和联系？](https://www.zhihu.com/question/27068705)\n2. [先验概率，后验概率，似然概率，条件概率，贝叶斯，最大似然](https://blog.csdn.net/suranxu007/article/details/50326873)\n3. [你对贝叶斯统计有何理解](https://www.zhihu.com/question/21134457/answer/169523403)\n4. [Different types of Distances used in Machine Learning Explained](https://tuhinmukherjee74.medium.com/different-types-of-distances-used-in-machine-learning-explained-550e2979752c)\n5. [Understanding the Bias-Variance Tradeoff](http://scott.fortmann-roe.com/docs/BiasVariance.html)"
  },
  {
    "path": "3-machine_learning/机器学习经典算法总结.md",
    "content": "- [一，KNN 算法](#一knn-算法)\n  - [1.1，k 值的选取](#11k-值的选取)\n  - [1.2，KNN 算法思路](#12knn-算法思路)\n- [二，支持向量机算法](#二支持向量机算法)\n  - [2.1，支持向量机简述](#21支持向量机简述)\n  - [2.2，SVM 基本型](#22svm-基本型)\n  - [2.3，对偶问题求解](#23对偶问题求解)\n- [三，K-means 聚类算法](#三k-means-聚类算法)\n  - [3.1，分类与聚类算法](#31分类与聚类算法)\n  - [3.2，K-means 聚类算法](#32k-means-聚类算法)\n- [参考资料](#参考资料)\n\n## 一，KNN 算法\n\n`K` 近邻算法（KNN）是一种基本分类和回归方法。`KNN` 算法的核心思想是如果一个样本在特征空间中的 `k` 个最相邻的样本中的大多数属于一个类别，那该样本也属于这个类别，并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分类样本所属的类别。 如下图：\n\n![KNN算法可视化](../data/images/ml_algorithm/knn_visual.png)\n\n在 `KNN` 中，通过计算对象间距离来作为各个对象之间的非相似性指标，避免了对象之间的匹配问题，在这里距离一般使用欧氏距离或曼哈顿距离，公式如下：\n\n![欧几里得和曼哈顿距离计算公式](../data/images/ml_algorithm/euclidean_distance_formula.png)\n\n同时，KNN通过依据 k 个对象中占优的类别进行决策，而不是单一的对象类别决策，这两点就是KNN算法的优势。\n\n### 1.1，k 值的选取\n\n+ `k` 值较小，模型会变得复杂，容易发生过拟合\n+ `k` 值较大，模型比较简单，容易欠拟合\n\n所以 `k` 值得选取也是一种调参？\n### 1.2，KNN 算法思路\n\n`KNN` 思想就是在训练集中数据和标签已知的情况下，输入测试数据，将测试数据的特征与训练集中对应的特征进行相互比较，找到训练集中与之最为相似的前 `K` 个数据，则该测试数据对应的类别就是K个数据中出现次数最多的那个分类，其算法的描述为：\n1. 计算测试数据与各个训练数据之间的距离；\n2. 按照距离的递增关系进行排序；\n3. 选取距离最小的 `K` 个点；\n4. 确定前 `K` 个点所在类别的出现频率；\n5. 返回前 `K` 个点中出现频率最高的类别作为测试数据的预测分类。\n\n## 二，支持向量机算法\n\n> [机器学习中的算法(2)-支持向量机(SVM)基础](https://www.cnblogs.com/leftnoteasy/archive/2011/05/02/basic-of-svm.html)\n### 2.1，支持向量机简述\n\n+ 支持向量机（Support Vector Machines, SVM）是一种二分类模型。它的基本模型是定义在特征空间上的间隔最大的线性分类器，间隔最大使它有别于感知机；支持向量机还包括核技巧，这使其成为实质上的非线性分类器。\n+ `SVM` 的学习策略是找到最大间隔（两个异类支持向量到超平面的距离之和 $\\gamma = \\frac{2}{||w}$ 称为“间隔”），可形式化为一个求解凸二次规划的问题，也等价于正则化的合页损失函数的最小化问题。\n+ `SVM` 的最优化算法是求解凸二次规划的最优化算法。\n\n### 2.2，SVM 基本型\n\n$$\nmin \\frac{1}{2}||w||^2 \\\\\ns.t. y_{i}(w^Tx_i + b) \\geq 1, i = 1,2,...,m\n$$\n\n`SVM` 的最优化算法是一个凸二次规划（convex quadratic programming）问题，对上式使用拉格朗日乘子法可转化为对偶问题，并优化求解。\n\n### 2.3，对偶问题求解\n\n对 `SVM` 基本型公式的每条约束添加拉格朗日乘子 $\\alpha_i \\geq 0$，则式子的拉格朗日函数如下：\n\n$$\nL(w,b,a) = \\frac 1 2||w||^2 - \\sum{i=1}{n} \\alpha_i (y_i(w^Tx_i+b) - 1)\n$$\n\n经过推导（参考机器学习西瓜书），可得 SVM 基本型的对偶问题：\n\n$$\\max\\limits_{\\alpha} \\sum_{i=1}^{m}-\\frac{1}{2} \\sum_{i=1}^{m} \\sum_{j=1}^{m}\\alpha_i \\alpha_j y_i y_j x^{T}\\_{i} x_j$$\n$$s.t. \\sum_{i=1}^{m} = \\alpha_{i}y_{i} = 0$$\n$$\\alpha_{i}\\geq 0, i=1,2,...,m$$\n\n继续优化该问题，有 `SMO` 方法，SMO 的基本思路是先固定 $\\alpha_i$ 之外的的所有参数，然后求 $\\alpha_i$ 上的极值。\n\n## 三，K-means 聚类算法\n\n### 3.1，分类与聚类算法\n\n+ 分类简单来说，就是根据文本的特征或属性，划分到已有的类别中。也就是说，这些类别是已知的，通过对已知分类的数据进行训练和学习，找到这些不同类的特征，再对未分类的数据进行分类。\n+ 聚类，就是你压根不知道数据会分为几类，通过聚类分析将数据或者说用户聚合成几个群体，那就是聚类了。聚类不需要对数据进行训练和学习。\n+ 分类属于监督学习，聚类属于无监督学习。常见的分类比如决策树分类算法、贝叶斯分类算法等聚类的算法最基本的有系统聚类，`K-means` 均值聚类。\n\n### 3.2，K-means 聚类算法\n\n聚类的目的是找到每个样本 $x$ 潜在的类别 $y$，并将同类别 $y$ 的样本 $x$ 放在一起。在聚类问题中，假定训练样本是 ${x^1,...,x^m}$，每个 $x^i \\in R^n$，没有 $y$。`K-means` 算法是将样本聚类成 $k$ 个簇（`cluster`），算法过程如下：\n\n1. 随机选取 $k$ 个聚类中心（`cluster centroids`）为 $\\mu_1, \\mu_1,...,\\mu_k \\in R^n$。\n2. 重复下面过程，直到质心不变或者变化很小：\n    + 对于每一个样例 $i$ ，计算其所属类别：$$c^i = \\underset{j}{argmin}||x^i - \\mu_j||^2$$\n    + 对于每一个类 $j$，重新计算该类的质心：$$\\mu_j = \\frac {\\sum_{i=1}^{m} 1{c^i} = jx^{i}} { \\sum_{i=1}^{m}1 c^{i} = j}$$\n\n$K$ 是我们事先给定的聚类数，$c^i$ 代表样例 $i$ 与 $k$ 个类中距离最近的那个类，$c^i$ 的值是 $1$ 到 $k$ 中的一个。质心 $\\mu_j$ 代表我们对属于同一个类的样本中心点的猜测。\n\n## 参考资料\n\n[K-means聚类算法](https://www.cnblogs.com/jerrylead/archive/2011/04/06/2006910.html)\n"
  },
  {
    "path": "4-deep_learning/README.md",
    "content": "## 前言\n\n本目录内容旨在分享深度学习基础、`Backbone` 网络论文以及深度学习框架使用及解析的技术笔记。\n\n## 深度学习基础\n\n1. [深度学习基础](https://github.com/HarleysZhang/deep_learning_alchemy/tree/main/2-deep_learning_basic)\n\n## Backbone 网络\n\n1. [DenseNet 网络详解](./经典backbone详解/DenseNet论文解读.md)\n2. [ResNetv2 网络详解](./经典backbone详解/ResNetv2论文解读.md)\n3. [经典 backbone 网络总结](./经典backbone详解/经典backbone总结.md)\n\n## Pytorch 框架笔记\n\n1. [Pytorch 基础-tensor 数据结构](./ml-dl-框架笔记/Pytorch基础-tensor数据结构.md)\n2. [Pytorch 基础-张量基本操作](ml-dl-框架笔记/Pytorch基础-张量基本操作.md)\n\n## 深度学习面试题\n\n- [大厂必问深度学习面试题](深度学习面试题.md)\n\n## 参考资料\n- 《深度学习》\n- 《机器学习》\n- 《动手学深度学习》\n- 《CNN 解析神经网络》\n- [PyTorch官方教程中文版](https://pytorch123.com/)\n- [eat_pytorch_in_20_days](https://github.com/lyhue1991/eat_pytorch_in_20_days)"
  },
  {
    "path": "4-deep_learning/ml-dl-框架笔记/Pytorch基础-tensor数据结构.md",
    "content": "- [torch.Tensor](#torchtensor)\n- [Tensor 数据类型](#tensor-数据类型)\n- [Tensor 的属性](#tensor-的属性)\n  - [view 和 reshape 的区别](#view-和-reshape-的区别)\n- [Tensor 与 ndarray](#tensor-与-ndarray)\n- [创建 Tensor](#创建-tensor)\n  - [传入维度的方法](#传入维度的方法)\n- [参考资料](#参考资料)\n\n## torch.Tensor\n\n`torch.Tensor` 是一种包含**单一数据类型**元素的多维矩阵，类似于 numpy 的 `array`。\n可以使用使用 torch.tensor() 方法将 python 的 list 或**序列数据**转换成 Tensor 数据，生成的是`dtype`  默认是 `torch.FloatTensor`。\n> 注意 `torch.tensor()` 总是拷贝 data。如果你有一个 tensor data 并且仅仅想改变它的 `requires_grad` 属性，可用 `requires_grad_()` 或者 `detach()` 来避免拷贝。如果你有一个 `numpy` 数组并且想避免拷贝，请使用 `torch.as_tensor()`。\n\n1，指定数据类型的 tensor 可以通过传递参数 `torch.dtype` 和/或者 `torch.device` 到构造函数生成：\n> 注意为了改变已有的 tensor 的 torch.device 和/或者 torch.dtype, 考虑使用 `to()` 方法.\n\n```python\n>>> torch.ones([2,3], dtype=torch.float64, device=\"cuda:0\")\ntensor([[1., 1., 1.],\n        [1., 1., 1.]], device='cuda:0', dtype=torch.float64)\n>>> torch.ones([2,3], dtype=torch.float32)\ntensor([[1., 1., 1.],\n        [1., 1., 1.]])\n```\n\n2，Tensor 的内容可以通过 Python 索引或者切片访问以及修改：\n\n```python\n>>> matrix = torch.tensor([[2,3,4],[5,6,7]])\n>>> print(matrix[1][2])\ntensor(7)\n>>> matrix[1][2] = 9\n>>> print(matrix)\ntensor([[2, 3, 4],\n        [5, 6, 9]])\n```\n\n3，使用 `torch.Tensor.item()` 或者 `int()` 方法从**只有一个值的 Tensor**中获取 Python Number：\n\n```python\n>>> x = torch.tensor([[4.5]])\n>>> x\ntensor([[4.5000]])\n>>> x.item()\n4.5\n>>> int(x)\n4\n```\n\n4，Tensor可以通过参数 `requires_grad=True` 创建, 这样 `torch.autograd` 会记录相关的运算实现自动求导：\n\n```python\n>>> x = torch.tensor([[1., -1.], [1., 1.]], requires_grad=True)\n>>> out = x.pow(2).sum()\n>>> out.backward()\n>>> x.grad\ntensor([[ 2.0000, -2.0000],\n [ 2.0000,  2.0000]])\n```\n\n5，每一个 tensor都有一个相应的 `torch.Storage` 保存其数据。tensor 类提供了一个多维的、strided 视图, 并定义了数值操作。\n\n## Tensor 数据类型\n\nTorch 定义了七种 CPU Tensor 类型和八种 GPU Tensor 类型：\n\n![tensor数据类型](../../data/images/tensor数据类型.png)\n\n`torch.Tensor` 是默认的 tensor 类型（`torch.FloatTensor`）的简称，即 `32` 位浮点数数据类型。\n\n## Tensor 的属性\n\nTensor 有很多属性，包括数据类型、Tensor 的维度、Tensor 的尺寸。\n\n+ **数据类型**：可通过改变 torch.tensor() 方法的 `dtype` 参数值，来设定不同的 `Tensor` 数据类型。\n+ **维度**：不同类型的数据可以用不同维度(dimension)的张量来表示。标量为 `0` 维张量，向量为 `1` 维张量，矩阵为 `2` 维张量。彩色图像有 `rgb` 三个通道，可以表示为 `3` 维张量。视频还有时间维，可以表示为 `4` 维张量，有几个中括号 `[` 维度就是几。**可使用 `dim() 方法` 获取 `tensor` 的维度**。\n+ **尺寸**：可以使用 `shape属性`或者 `size()方法`查看张量在每一维的长度，可以使用 `view()方法`或者`reshape() 方法`改变张量的尺寸。Pytorch 框架中四维张量形状的定义是 `(N, C, H, W)`。\n\n> 关于如何理解 Pytorch 的 Tensor Shape 可以参考 stackoverflow 上的这个 [回答](https://stackoverflow.com/questions/52370008/understanding-pytorch-tensor-shape)。\n\n样例代码如下：\n\n```python\nmatrix = torch.tensor([[[1,2,3,4],[5,6,7,8]],\n                       [[5,4,6,7], [5,6,8,9]]], dtype = torch.float64)\nprint(matrix)               # 打印 tensor\nprint(matrix.dtype)     # 打印 tensor 数据类型\nprint(matrix.dim())     # 打印 tensor 维度\nprint(matrix.size())     # 打印 tensor 尺寸\nprint(matrix.shape)    # 打印 tensor 尺寸\nmatrix2 = matrix.view(4, 2, 2) # 改变 tensor 尺寸\nprint(matrix2)\n```\n\n程序输出结果如下：\n\n![tensor数据类型](../../data/images/tensor属性.png)\n\n### view 和 reshape 的区别\n\n+ 两个方法都是用来改变 tensor 的 shape，view() 只适合对满足连续性条件（`contiguous`）的 tensor 进行操作，而 reshape() 同时还可以对不满足连续性条件的 tensor 进行操作。\n+ 在满足 tensor 连续性条件（`contiguous`）时，a.reshape()  返回的结果与a.view() 相同，都不会开辟新内存空间；不满足 `contiguous` 时， 直接使用 view() 方法会失败，`reshape()` 依然有用，但是会重新开辟内存空间，不与之前的 tensor 共享内存，即返回的是 **”副本“**（等价于先调用 `contiguous()` 方法再使用 `view()` 方法）。\n更多理解参考这篇[文章](https://blog.csdn.net/Flag_ing/article/details/109129752)\n\n## Tensor 与 ndarray\n\n1，张量和 numpy 数组。可以用 `.numpy()` 方法从 Tensor 得到 numpy 数组，也可以用 `torch.from_numpy` 从 numpy 数组得到Tensor。这两种方法关联的 Tensor 和 numpy 数组是共享数据内存的。可以用张量的 `clone`方法拷贝张量，中断这种关联。\n\n```python\narr = np.random.rand(4,5)\nprint(type(arr))\ntensor1 = torch.from_numpy(arr)\nprint(type(tensor1))\narr1 = tensor1.numpy()\nprint(type(arr1))\n\"\"\"\n<class 'numpy.ndarray'>\n<class 'torch.Tensor'>\n<class 'numpy.ndarray'>\n\"\"\"\n```\n\n2，`item()` 方法和 `tolist()` 方法可以将张量转换成 Python 数值和数值列表\n\n```python\n# item方法和tolist方法可以将张量转换成Python数值和数值列表\nscalar = torch.tensor(5)  # 标量\ns = scalar.item()\nprint(s)\nprint(type(s))\n\ntensor = torch.rand(3,2)  # 矩阵\nt = tensor.tolist()\nprint(t)\nprint(type(t))\n\"\"\"\n1.0\n<class 'float'>\n[[0.8211846351623535, 0.20020723342895508], [0.011571824550628662, 0.2906131148338318]]\n<class 'list'>\n\"\"\"\n```\n\n## 创建 Tensor\n\n创建 tensor ，可以传入数据或者维度，torch.tensor() 方法只能传入数据，torch.Tensor() 方法既可以传入数据也可以传维度，强烈建议 tensor() 传数据，Tensor() 传维度，否则易搞混。\n\n### 传入维度的方法\n\n|方法名|方法功能|备注|\n|-----|-------|---|\n|`torch.rand(*sizes, out=None) → Tensor`|返回一个张量，包含了从区间 `[0, 1)` 的**均匀分布**中抽取的一组随机数。张量的形状由参数sizes定义。|推荐|\n|`torch.randn(*sizes, out=None) → Tensor`|返回一个张量，包含了从**标准正态分布**（均值为0，方差为1，即高斯白噪声）中抽取的一组随机数。张量的形状由参数sizes定义。|不推荐|\n|`torch.normal(means, std, out=None) → Tensor`|返回一个张量，包含了从指定均值 `means` 和标准差 `std` 的离散正态分布中抽取的一组随机数。标准差 `std` 是一个张量，包含每个输出元素相关的正态分布标准差。|多种形式，建议看源码|\n|`torch.rand_like(a)`|根据数据 `a` 的 shape 来生成随机数据|不常用|\n|`torch.randint(low=0, high, size)`|生成指定范围(`low, hight`)和 `size` 的随机整数数据|常用|\n|`torch.full([2, 2], 4)`|生成给定维度，全部数据相等的数据|不常用|\n|`torch.arange(start=0, end, step=1, *, out=None)`|生成指定间隔的数据|易用常用|\n|`torch.ones(*size, *, out=None)`|生成给定 size 且值全为1 的矩阵数据|简单|\n|`zeros()/zeros_like()/eye()`|全 `0` 的 tensor 和 对角矩阵|简单|\n\n样例代码：\n\n```python\n>>> torch.rand([1,1,3,3])\ntensor([[[[0.3005, 0.6891, 0.4628],\n          [0.4808, 0.8968, 0.5237],\n          [0.4417, 0.2479, 0.0175]]]])\n>>> torch.normal(2, 3, size=(1, 4))\ntensor([[3.6851, 3.2853, 1.8538, 3.5181]])\n>>> torch.full([2, 2], 4)\ntensor([[4, 4],\n        [4, 4]])\n>>> torch.arange(0,10,2)\ntensor([0, 2, 4, 6, 8])\n>>> torch.eye(3,3)\ntensor([[1., 0., 0.],\n        [0., 1., 0.],\n        [0., 0., 1.]])\n```\n\n## 参考资料\n\n+ [PyTorch：view() 与 reshape() 区别详解](https://blog.csdn.net/Flag_ing/article/details/109129752)\n+ [torch.rand和torch.randn和torch.normal和linespace()](https://zhuanlan.zhihu.com/p/115997577)\n"
  },
  {
    "path": "4-deep_learning/ml-dl-框架笔记/Pytorch基础-张量基本操作.md",
    "content": "- [一，张量的基本操作](#一张量的基本操作)\n- [二，维度变换](#二维度变换)\n  - [2.1，squeeze vs unsqueeze 维度增减](#21squeeze-vs-unsqueeze-维度增减)\n  - [2.2，transpose vs permute 维度交换](#22transpose-vs-permute-维度交换)\n- [三，索引切片](#三索引切片)\n  - [3.1，规则索引切片方式](#31规则索引切片方式)\n  - [3.2，gather 和 torch.index\\_select 算子](#32gather-和-torchindex_select-算子)\n- [四，合并分割](#四合并分割)\n  - [4.1，torch.cat 和 torch.stack](#41torchcat-和-torchstack)\n  - [4.2，torch.split 和  torch.chunk](#42torchsplit-和--torchchunk)\n- [五，卷积相关算子](#五卷积相关算子)\n  - [5.1，上采样方法总结](#51上采样方法总结)\n  - [5.2，F.interpolate 采样函数](#52finterpolate-采样函数)\n  - [5.3，nn.ConvTranspose2d 反卷积](#53nnconvtranspose2d-反卷积)\n- [参考资料](#参考资料)\n\n> 授人以鱼不如授人以渔，原汁原味的知识才更富有精华，本文只是对张量基本操作知识的理解和学习笔记，看完之后，想要更深入理解，建议去 pytorch 官方网站，查阅相关函数和操作，英文版在[这里](https://pytorch.org/docs/1.7.0/torch.html)，中文版在[这里](https://pytorch-cn.readthedocs.io/zh/latest/package_references/torch/#tensors)。本文的代码是在 `pytorch1.7` 版本上测试的，其他版本一般也没问题。\n\n## 一，张量的基本操作\n`Pytorch` 中，张量的操作分为**结构操作和数学运算**，其理解就如字面意思。结构操作就是改变张量本身的结构，数学运算就是对张量的元素值完成数学运算。\n+ 常使用的张量结构操作：维度变换（`tranpose`、`view` 等）、合并分割（`split`、`chunk`等）、索引切片（`index_select`、`gather` 等）。\n+ 常使用的张量数学运算：标量运算、向量运算、矩阵运算。\n## 二，维度变换\n### 2.1，squeeze vs unsqueeze 维度增减\n+ `squeeze()`：对 tensor 进行维度的压缩，去掉维数为 `1` 的维度。用法：`torch.squeeze(a)` 将 a 中所有为 1 的维度都删除，或者 `a.squeeze(1)` 是去掉 `a`中指定的维数为 `1` 的维度。\n+ `unsqueeze()`：对数据维度进行扩充，给指定位置加上维数为 `1` 的维度。用法：`torch.unsqueeze(a, N)`，或者 `a.unsqueeze(N)`，在 `a` 中指定位置 `N` 加上一个维数为 `1` 的维度。\n\n`squeeze` 用例程序如下：\n```python\na = torch.rand(1,1,3,3)\nb = torch.squeeze(a)\nc = a.squeeze(1)\nprint(b.shape)\nprint(c.shape)\n```\n程序输出结果如下：\n> torch.Size([3, 3])\ntorch.Size([1, 3, 3])\n\n`unsqueeze` 用例程序如下：\n```python\nx = torch.rand(3,3)\ny1 = torch.unsqueeze(x, 0)\ny2 = x.unsqueeze(0)\nprint(y1.shape)\nprint(y2.shape)\n```\n程序输出结果如下：\n> torch.Size([1, 3, 3])\ntorch.Size([1, 3, 3])\n\n### 2.2，transpose vs permute 维度交换\n`torch.transpose()` 只能交换两个维度，而 `.permute()` 可以自由交换任意位置。函数定义如下：\n```python\ntranspose(dim0, dim1) → Tensor  # See torch.transpose()\npermute(*dims) → Tensor  # dim(int). Returns a view of the original tensor with its dimensions permuted.\n```\n在 `CNN` 模型中，我们经常遇到交换维度的问题，举例：四个维度表示的 tensor：`[batch, channel, h, w]`（`nchw`），如果想把 `channel` 放到最后去，形成`[batch, h, w, channel]`（`nhwc`），如果使用 `torch.transpose()` 方法，至少要交换两次（先 `1 3` 交换再 `1 2` 交换），而使用 `.permute()` 方法只需一次操作，更加方便。例子程序如下：\n```python\nimport torch\ninput = torch.rand(1,3,28,32)                    # torch.Size([1, 3, 28, 32]\nprint(b.transpose(1, 3).shape)                   # torch.Size([1, 32, 28, 3])\nprint(b.transpose(1, 3).transpose(1, 2).shape)   # torch.Size([1, 28, 32, 3])\n \nprint(b.permute(0,2,3,1).shape)                  # torch.Size([1, 28, 28, 3]\n```\n## 三，索引切片\n### 3.1，规则索引切片方式\n张量的索引切片方式和 `numpy`、python 多维列表几乎一致，都可以通过索引和切片对部分元素进行修改。切片时支持缺省参数和省略号。实例代码如下：\n```python\n>>> t = torch.randint(1,10,[3,3])\n>>> t\ntensor([[8, 2, 9],\n        [2, 5, 9],\n        [3, 9, 9]])\n>>> t[0] # 第 1 行数据\ntensor([8, 2, 9])\n>>> t[2][2]\ntensor(9)\n>>> t[0:3,:]  # 第1至第3行，全部列\ntensor([[8, 2, 9],\n        [2, 5, 9],\n        [3, 9, 9]])\n>>> t[0:2,:]  # 第1行至第2行\ntensor([[8, 2, 9],\n        [2, 5, 9]])\n>>> t[1:,-1]  # 第2行至最后行，最后一列\ntensor([9, 9])\n>>> t[1:,::2] # 第1行至最后行，第0列到最后一列每隔两列取一列\ntensor([[2, 9],\n        [3, 9]])\n```\n以上切片方式相对规则，对于不规则的切片提取,可以使用 `torch.index_select`, `torch.take`, `torch.gather`, `torch.masked_select`。\n### 3.2，gather 和 torch.index_select 算子\n> `gather` 算子的用法比较难以理解，在翻阅了官方文档和网上资料后，我有了一些自己的理解。\n\n1，`gather` 是不规则的切片提取算子（Gathers values along an axis specified by dim. 在指定维度上根据索引 index 来选取数据）。函数定义如下：\n```python\ntorch.gather(input, dim, index, *, sparse_grad=False, out=None) → Tensor\n```\n**参数解释**：\n+ `input` (Tensor) – the source tensor.\n+ `dim` (int) – the axis along which to index.\n+ `index` (LongTensor) – the indices of elements to gather.\n\n`gather` 算子的注意事项：\n+ 输入 `input` 和索引 `index` 具有相同数量的维度，即 `input.shape = index.shape`\n+ 对于任意维数，只要 `d != dim`，index.size(d) <= input.size(d)，即对于可以不用索引维数 `d` 上的全部数据。\n+ 输出 `out` 和 索引 `index` 具有相同的形状。输入和索引不会相互广播。\n\n对于 3D tensor，`output` 值的定义如下：\n`gather` 的官方定义如下：\n```python\nout[i][j][k] = input[index[i][j][k]][j][k]  # if dim == 0\nout[i][j][k] = input[i][index[i][j][k]][k]  # if dim == 1\nout[i][j][k] = input[i][j][index[i][j][k]]   # if dim == 2\n```\n通过理解前面的一些定义，相信读者对 `gather` 算子的用法有了一个基本了解，下面再结合 2D 和 3D  tensor 的用例来直观理解算子用法。\n（1），对于 2D tensor 的例子：\n```python\n>>> import torch\n>>> a = torch.arange(0, 16).view(4,4)\n>>> a\ntensor([[ 0,  1,  2,  3],\n        [ 4,  5,  6,  7],\n        [ 8,  9, 10, 11],\n        [12, 13, 14, 15]])\n>>> index = torch.tensor([[0, 1, 2, 3]])  # 选取对角线元素\n>>> torch.gather(a, 0, index)\ntensor([[ 0,  5, 10, 15]])\n```\n`output` 值定义如下：\n```shell\n# 按照 index = tensor([[0, 1, 2, 3]])顺序作用在行上索引依次为0,1,2,3\na[0][0] = 0\na[1][1] = 5\na[2][2] = 10\na[3][3] = 15\n```\n（2），索引更复杂的  2D tensor 例子：\n```python\n>>> t = torch.tensor([[1, 2], [3, 4]])\n>>> t\ntensor([[1, 2],\n        [3, 4]])\n>>> torch.gather(t, 1, torch.tensor([[0, 0], [1, 0]]))\ntensor([[ 1,  1],\n        [ 4,  3]])\n```\n\n`output` 值的计算如下：\n\n```shell\noutput[i][j] = input[i][index[i][j]]  # if dim = 1\noutput[0][0] = input[0][index[0][0]] = input[0][0] = 1\noutput[0][1] = input[0][index[0][1]] = input[0][0] = 1\noutput[1][0] = input[1][index[1][0]] = input[1][1] = 4\noutput[1][1] = input[1][index[1][1]] = input[1][0] = 3\n```\n\n总结：**可以看到 `gather` 是通过将索引在指定维度 `dim` 上的值替换为 `index` 的值，但是其他维度索引不变的情况下获取 `tensor` 数据**。直观上可以理解为对矩阵进行重排，比如对每一行(dim=1)的元素进行变换，比如 `torch.gather(a, 1, torch.tensor([[1,2,0], [1,2,0]]))` 的作用就是对 矩阵 `a` 每一行的元素，进行 `permtute(1,2,0)` 操作。\n2，理解了 `gather` 再看 `index_select` 就很简单，函数作用是返回沿着输入张量的指定维度的指定索引号进行索引的张量子集。函数定义如下：\n\n```python\ntorch.index_select(input, dim, index, *, out=None) → Tensor\n```\n\n函数返回一个新的张量，它使用数据类型为 `LongTensor` 的 `index` 中的条目沿维度 `dim` 索引输入张量。返回的张量具有与原始张量（输入）相同的维数。 维度尺寸与索引长度相同； 其他尺寸与原始张量中的尺寸相同。实例代码如下：\n\n```python\n>>> x = torch.randn(3, 4)\n>>> x\ntensor([[ 0.1427,  0.0231, -0.5414, -1.0009],\n        [-0.4664,  0.2647, -0.1228, -1.1068],\n        [-1.1734, -0.6571,  0.7230, -0.6004]])\n>>> indices = torch.tensor([0, 2])\n>>> torch.index_select(x, 0, indices)\ntensor([[ 0.1427,  0.0231, -0.5414, -1.0009],\n        [-1.1734, -0.6571,  0.7230, -0.6004]])\n>>> torch.index_select(x, 1, indices)\ntensor([[ 0.1427, -0.5414],\n        [-0.4664, -0.1228],\n        [-1.1734,  0.7230]])\n```\n## 四，合并分割\n### 4.1，torch.cat 和 torch.stack\n可以用 `torch.cat` 方法和 `torch.stack` 方法将多个张量合并，也可以用 `torch.split`方法把一个张量分割成多个张量。`torch.cat` 和 `torch.stack` 有略微的区别，`torch.cat` 是连接，不会增加维度，而 `torch.stack` 是堆叠，会增加一个维度。两者函数定义如下：\n```python\n# Concatenates the given sequence of seq tensors in the given dimension. All tensors must either have the same shape (except in the concatenating dimension) or be empty.\ntorch.cat(tensors, dim=0, *, out=None) → Tensor\n# Concatenates a sequence of tensors along **a new** dimension. All tensors need to be of the same size.\ntorch.stack(tensors, dim=0, *, out=None) → Tensor\n```\n`torch.cat` 和 `torch.stack` 用法实例代码如下：\n```python\n>>> a = torch.arange(0,9).view(3,3)\n>>> b = torch.arange(10,19).view(3,3)\n>>> c = torch.arange(20,29).view(3,3)\n>>> cat_abc = torch.cat([a,b,c], dim=0)\n>>> print(cat_abc.shape)\ntorch.Size([9, 3])\n>>> print(cat_abc)\ntensor([[ 0,  1,  2],\n        [ 3,  4,  5],\n        [ 6,  7,  8],\n        [10, 11, 12],\n        [13, 14, 15],\n        [16, 17, 18],\n        [20, 21, 22],\n        [23, 24, 25],\n        [26, 27, 28]])\n>>> stack_abc = torch.stack([a,b,c], axis=0)  # torch中dim和axis参数名可以混用\n>>> print(stack_abc.shape)\ntorch.Size([3, 3, 3])\n>>> print(stack_abc)\ntensor([[[ 0,  1,  2],\n         [ 3,  4,  5],\n         [ 6,  7,  8]],\n\n        [[10, 11, 12],\n         [13, 14, 15],\n         [16, 17, 18]],\n\n        [[20, 21, 22],\n         [23, 24, 25],\n         [26, 27, 28]]])\n>>> chunk_abc = torch.chunk(cat_abc, 3, dim=0)\n>>> chunk_abc\n(tensor([[0, 1, 2],\n         [3, 4, 5],\n         [6, 7, 8]]),\n tensor([[10, 11, 12],\n         [13, 14, 15],\n         [16, 17, 18]]),\n tensor([[20, 21, 22],\n         [23, 24, 25],\n         [26, 27, 28]]))\n```\n### 4.2，torch.split 和  torch.chunk\n`torch.split()` 和 `torch.chunk()` 可以看作是 `torch.cat()` 的逆运算。`split()` 作用是将张量拆分为多个块，每个块都是原始张量的视图。`split()` [函数定义](https://pytorch.org/docs/stable/generated/torch.split.html#torch.split)如下：\n```python\n\"\"\"\nSplits the tensor into chunks. Each chunk is a view of the original tensor.\nIf split_size_or_sections is an integer type, then tensor will be split into equally sized chunks (if possible). Last chunk will be smaller if the tensor size along the given dimension dim is not divisible by split_size.\nIf split_size_or_sections is a list, then tensor will be split into len(split_size_or_sections) chunks with sizes in dim according to split_size_or_sections.\n\"\"\"\ntorch.split(tensor, split_size_or_sections, dim=0)\n```\n`chunk()` 作用是将 `tensor` 按 `dim`（行或列）分割成 `chunks` 个 `tensor` 块，返回的是一个元组。`chunk()` 函数定义如下：\n```python\ntorch.chunk(input, chunks, dim=0) → List of Tensors\n\"\"\"\nSplits a tensor into a specific number of chunks. Each chunk is a view of the input tensor.\nLast chunk will be smaller if the tensor size along the given dimension dim is not divisible by chunks.\nParameters:\n    input (Tensor) – the tensor to split\n    chunks (int) – number of chunks to return\n    dim (int) – dimension along which to split the tensor\n\"\"\"\n```\n实例代码如下：\n```python\n>>> a = torch.arange(10).reshape(5,2)\n>>> a\ntensor([[0, 1],\n        [2, 3],\n        [4, 5],\n        [6, 7],\n        [8, 9]])\n>>> torch.split(a, 2)\n(tensor([[0, 1],\n         [2, 3]]),\n tensor([[4, 5],\n         [6, 7]]),\n tensor([[8, 9]]))\n>>> torch.split(a, [1,4])\n(tensor([[0, 1]]),\n tensor([[2, 3],\n         [4, 5],\n         [6, 7],\n         [8, 9]]))\n>>> torch.chunk(a, 2, dim=1)\n(tensor([[0],\n        [2],\n        [4],\n        [6],\n        [8]]), \ntensor([[1],\n        [3],\n        [5],\n        [7],\n        [9]]))\n```\n## 五，卷积相关算子\n### 5.1，上采样方法总结\n上采样大致被总结成了三个类别：\n1. 基于线性插值的上采样：最近邻算法（`nearest`）、双线性插值算法（`bilinear`）、双三次插值算法（`bicubic`）等，这是传统图像处理方法。\n2. 基于深度学习的上采样（转置卷积，也叫反卷积 `Conv2dTranspose2d`等）\n3. `Unpooling` 的方法（简单的补零或者扩充操作）\n\n> 计算效果：最近邻插值算法 < 双线性插值 < 双三次插值。计算速度：最近邻插值算法 > 双线性插值 > 双三次插值。\n### 5.2，F.interpolate 采样函数\n> Pytorch 老版本有 `nn.Upsample` 函数，新版本建议用 `torch.nn.functional.interpolate`，一个函数可实现定制化需求的上采样或者下采样功能，。\n\n`F.interpolate()` 函数全称是 `torch.nn.functional.interpolate()`，函数定义如下：\n```python\ndef interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None, recompute_scale_factor=None):  # noqa: F811\n    # type: (Tensor, Optional[int], Optional[List[float]], str, Optional[bool], Optional[bool]) -> Tensor\n    pass\n```\n参数解释如下：\n+ `input`(Tensor)：输入张量数据；\n+ `size`： 输出的尺寸，数据类型为 tuple： ([optional D_out], [optional H_out], W_out)，和 `scale_factor` 二选一。\n+ `scale_factor`：在高度、宽度和深度上面的放大倍数。数据类型既可以是 int`——表明高度、宽度、深度都扩大同一倍数；也可是 `tuple`——指定高度、宽度、深度等维度的扩大倍数。\n+ `mode`： 上采样的方法，包括最近邻（`nearest`），线性插值（`linear`），双线性插值（`bilinear`），三次线性插值（`trilinear`），默认是最近邻（`nearest`）。\n+ `align_corners`： 如果设为 `True`，输入图像和输出图像角点的像素将会被对齐（aligned），这只在 `mode = linear, bilinear, or trilinear` 才有效，默认为 `False`。\n\n例子程序如下：\n```python\nimport torch.nn.functional as F\nx = torch.rand(1,3,224,224)\ny = F.interpolate(x * 2, scale_factor=(2, 2), mode='bilinear').squeeze(0)\nprint(y.shape)   # torch.Size([3, 224, 224)\n```\n\n### 5.3，nn.ConvTranspose2d 反卷积\n\n转置卷积（有时候也称为反卷积，个人觉得这种叫法不是很规范），它是一种特殊的卷积，先 `padding` 来扩大图像尺寸，紧接着跟正向卷积一样，旋转卷积核 180 度，再进行卷积计算。\n\n## 参考资料\n\n+ [pytorch演示卷积和反卷积运算](https://blog.csdn.net/qq_37879432/article/details/80297263)\n+ [torch.Tensor](https://pytorch.org/docs/1.7.0/tensors.html#torch.Tensor)\n+ [PyTorch学习笔记(10)——上采样和PixelShuffle](https://blog.csdn.net/g11d111/article/details/82855946)\n+ [反卷积 Transposed convolution](https://zhuanlan.zhihu.com/p/124626648)\n+ [PyTorch中的转置卷积详解——全网最细](https://blog.csdn.net/w55100/article/details/106467776)\n+ [4-1,张量的结构操作](https://github.com/lyhue1991/eat_pytorch_in_20_days/blob/master/4-1,%E5%BC%A0%E9%87%8F%E7%9A%84%E7%BB%93%E6%9E%84%E6%93%8D%E4%BD%9C.md)"
  },
  {
    "path": "4-deep_learning/深度学习基础/反向传播与梯度下降详解.md",
    "content": "- [一，前向传播与反向传播](#一前向传播与反向传播)\n  - [1.1，神经网络训练过程](#11神经网络训练过程)\n  - [1.2，前向传播](#12前向传播)\n  - [1.3，反向传播](#13反向传播)\n  - [1.4，总结](#14总结)\n- [二，梯度下降](#二梯度下降)\n  - [2.1，深度学习中的优化](#21深度学习中的优化)\n  - [2.2，如何理解梯度下降法](#22如何理解梯度下降法)\n  - [2.3，梯度下降原理](#23梯度下降原理)\n- [三，随机梯度下降与小批量随机梯度下降](#三随机梯度下降与小批量随机梯度下降)\n  - [3.1，随机梯度下降](#31随机梯度下降)\n  - [3.2，小批量随机梯度下降](#32小批量随机梯度下降)\n- [四，总结](#四总结)\n- [参考资料](#参考资料)\n\n## 一，前向传播与反向传播\n### 1.1，神经网络训练过程\n\n神经网络训练过程是：\n1. 先通过随机参数“猜“一个结果（模型前向传播过程），这里称为预测结果 $a$；\n2. 然后计算 $a$ 与样本标签值 $y$ 的差距（即损失函数的计算过程）；\n3. 随后通过反向传播算法更新神经元参数，使用新的参数再试一次，这一次就不是“猜”了，而是有依据地向正确的方向靠近，毕竟参数的调整是有策略的（基于梯度下降策略）。\n\n以上步骤如此反复多次，一直到预测结果和真实结果之间相差无几，亦即 $|a-y|\\rightarrow 0$，则训练结束。\n\n总结：**所谓模型训练，其实就是通过如 `SGD` 优化算法指导模型参数更新的过程**。\n\n### 1.2，前向传播\n\n前向传播(forward propagation 或 forward pass)指的是: 按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。\n\n为了更深入理解前向传播的计算过程，我们可以根据网络结构绘制网络的前向传播计算图。下图是简单网络与对应的计算图示例:\n\n![网络结构与前向计算图](../../data/images/bp/网络结构与前向计算图.png)\n\n其中正方形表示变量，圆圈表示操作符。数据流的方向是从左到右依次计算。\n\n### 1.3，反向传播\n\n反向传播(`backward propagation`，简称 `BP`)指的是**计算神经网络参数梯度的方法**。其原理是基于微积分中的**链式规则**，按相反的顺序从输出层到输入层遍历网络，依次计算每个中间变量和参数的梯度。\n> 梯度的自动计算(自动微分)大大简化了深度学习算法的实现。\n\n注意，反向传播算法会重复利用前向传播中存储的中间值，以避免重复计算，因此，需要保留前向传播的中间结果，这也会导致模型训练比单纯的预测需要更多的内存（显存）。同时这些中间结果占用内存（显存）大小与网络层的数量和批量（`batch_size`）大小成正比，因此使用大 `batch_size` 训练更深层次的网络更容易导致内存不足（out of memory）的错误！\n\n### 1.4，总结\n\n- 前向传播在神经网络定义的计算图中按顺序计算和存储中间变量，它的顺序是从输入层到输出层。 \n- 反向传播按相反的顺序(从输出层到输入层)计算和存储神经网络的中间变量和参数的梯度。\n- 在训练神经网络时，在初始化模型参数后，我们交替使用前向传播和反向传播，基于反向传播计算得到的梯度，结合随机梯度下降优化算法（或者 `Adam` 等其他优化算法）来更新模型参数。\n- 深度学习模型训练比预测需要更多的内存。\n\n## 二，梯度下降\n\n### 2.1，深度学习中的优化\n\n大多数深度学习算法都涉及某种形式的优化。**优化器的目的是更新网络权重参数，使得我们平滑地到达损失面中损失值的最小点**。\n\n深度学习优化存在许多挑战。其中一些最令人烦恼的是局部最小值、鞍点和梯度消失。\n\n- **局部最小值**(`local minimum`): 对于任何目标函数 $f(x)$，如果在 $x$ 处对应的 $f(x)$ 值小于在 $x$ 附近任何其他点的 $f(x)$ 值，那么 $f(x)$ 可能是局部最小值。如果 $f(x)$ 在 $x$ 处的值是整个域上目标函数的最小值，那么 $f(x)$ 是全局最小值。\n- **鞍点**(`saddle point`): 指函数的所有梯度都消失但既不是全局最小值也不是局部最小值的任何位置。\n- **梯度消失**(`vanishing gradient`): 因为某些原因导致**目标函数** $f$ 的梯度接近零（即梯度消失问题），是在引入 `ReLU` 激活函数和 `ResNet` 之前训练深度学习模型相当棘手的原因之一。\n\n在深度学习中，大多数目标函数都很复杂，没有解析解，因此，我们需使用数值优化算法，本文中的优化算法: `SGD` 和 `Adam` 都属于此类别。\n\n### 2.2，如何理解梯度下降法\n\n梯度下降（`gradient descent`, `GD`）算法是神经网络模型训练中最为常见的优化器。尽管梯度下降(`gradient descent`)很少直接用于深度学习，但理解它是理解**随机梯度下降和小批量随机梯度下降**算法的基础。\n\n大多数文章都是以“一个人被困在山上，需要迅速下到谷底”来举例理解梯度下降法，但这并不完全准确。在自然界中，梯度下降的最好例子，就是泉水下山的过程：\n\n1. 水受重力影响，会在当前位置，沿着**最陡峭**的方向流动，有时会形成瀑布（**梯度的反方向为函数值下降最快的方向**）；\n2. 水流下山的路径不是唯一的，在同一个地点，有可能有多个位置具有同样的陡峭程度，而造成了分流（可以得到多个解）；\n3. 遇到坑洼地区，有可能形成湖泊，而终止下山过程（不能得到全局最优解，而是局部最优解）。\n\n> 示例参考 [AI-EDU: 梯度下降](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC1%E6%AD%A5%20-%20%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/02.3-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D.html)。\n\n### 2.3，梯度下降原理\n\n梯度下降的数学公式：\n\n$$\n\\theta_{n+1} = \\theta_{n} - \\eta \\cdot \\nabla J(\\theta) \\tag{1}\n$$\n\n其中：\n\n- $\\theta_{n+1}$：下一个值（神经网络中参数更新后的值）；\n- $\\theta_n$：当前值（当前网络参数值）；\n- $-$：减号，梯度的反向（梯度的反方向为函数值下降最快的方向）；\n- $\\eta$：学习率或步长，控制每一步走的距离，不要太快以免错过了最佳景点，不要太慢以免时间太长（需要手动调整的超参数）；\n- $\\nabla$：**梯度**，，表示函数当前位置的最快上升点（梯度向量指向上坡，负梯度向量指向下坡）；\n- $J(\\theta)$：函数（等待优化的目标函数）。\n\n下图展示了梯度下降法的步骤。梯度下降的目的就是使得 $x$ 值向极值点逼近。\n\n![梯度下降的步骤](../../data/images/bp/gd_concept.png)\n\n下面我通过一个简单的双变量凸函数 $J(x, y) = x^2 + 2y^2$ 为例，来描述梯度下降的优化过程。\n\n通过梯度下降法寻找函数的最小值，首先得计算其函数梯度:\n\n$$\n{\\partial{J(x,y)} \\over \\partial{x}} = 2x \\\\\n{\\partial{J(x,y)} \\over \\partial{y}} = 4y\n$$\n\n设初始点为 $(x_0, y_0) = (-3, -3)$，学习率 $\\eta = 0.1$，根据梯度下降公式(1)，可得参数迭代过程的计算公式:\n\n$$\n\\begin{align} \n(x_{n+1}, y_{n+1}) &= (x_n, y_n) - \\eta \\cdot \\nabla J(x, y) \\nonumber \\\\\n&= (x_n, y_n) - \\eta \\cdot (2x, 4y) \\tag{2}\n\\end{align}\n$$\n\n这里手动计算下下一个迭代点的值:\n\n$$\n\\begin{aligned}\n(x_1, y_1) &= (-3, -3) - 0.1*(2*-3, 4*-3) \\\\\n&= (-3 + 0.6, -3 + 1.2) \\\\\n&= (-2.4, -1.8) \\\\\n\\end{aligned}\n$$\n\n根据上述公式 (2)，假设终止条件为 $J(x,y)$ < 0. 005，迭代过程如下表1所示。\n\n表1 双变量函数梯度下降的迭代过程\n\n|迭代次数|$x$|$y$|$J(x,y)$|\n|---|---|---|---|\n|1|-3|-3|27|\n|2|-2.4|y=-1.8|12.24|\n|...|...|...|...|\n|16|-0.084442|-0.000846|0.007132|\n|17|-0.067554|y=-0.000508|0.004564|\n\n迭代 $17$ 次后，$J(x,y)$ 的值为 $0.004564$，满足小于 $0.005$ 的条件，停止迭代。\n\n由于是双变量，所以梯度下降的迭代过程需要用三维图来解释。表2可视化了三维空间内的梯度下降过程。\n\n|观察角度1|观察角度2|\n|-------|--------|\n|<img src=\"../../data/images/bp/gd_double_variable1.png\">|<img src=\"../../data/images/bp/gd_double_variable2.png\">|\n\n图中间那条隐隐的黑色线，表示梯度下降的过程，从红色的高地一直沿着坡度向下走，直到蓝色的洼地。\n\n双变量凸函数 $J(x, y) = x^2 + 2y^2$ 的梯度下降优化过程以及可视化代码如下所示:\n\n```python\n# Copyright (c) Microsoft. All rights reserved.\n# Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom mpl_toolkits.mplot3d import Axes3D\n\ndef target_function(x,y):\n    J = pow(x, 2) + 2*pow(y, 2)\n    return J\n\ndef derivative_function(theta):\n    x = theta[0]\n    y = theta[1]\n    return np.array([2*x, 4*y])\n\ndef show_3d_surface(x, y, z):\n    fig = plt.figure()\n    ax = Axes3D(fig)\n    u = np.linspace(-3, 3, 100)\n    v = np.linspace(-3, 3, 100)\n    X, Y = np.meshgrid(u, v)\n    R = np.zeros((len(u), len(v)))\n    for i in range(len(u)):\n        for j in range(len(v)):\n            R[i, j] = pow(X[i, j], 2)+ 4*pow(Y[i, j], 2)\n\n    ax.plot_surface(X, Y, R, cmap='rainbow')\n    plt.plot(x, y, z, c='black', linewidth=1.5,  marker='o', linestyle='solid')\n    plt.show()\n\nif __name__ == '__main__':\n    theta = np.array([-3, -3]) # 输入为双变量\n    eta = 0.1 # 学习率\n    error = 5e-3 # 迭代终止条件，目标函数值 < error\n    X = []\n    Y = []\n    Z = []\n    for i in range(50):\n        print(theta)\n        x = theta[0]\n        y = theta[1]\n        z = target_function(x,y)\n        X.append(x)\n        Y.append(y)\n        Z.append(z)\n        print(\"%d: x=%f, y=%f, z=%f\" % (i,x,y,z))\n        d_theta = derivative_function(theta)\n        print(\"    \", d_theta)\n        theta = theta - eta * d_theta\n        if z < error:\n            break\n    show_3d_surface(X,Y,Z)\n```\n\n注意！总结下，不同的步长 $\\eta$ ，随着迭代次数的增加，会导致被优化函数 $J$ 的值有不同的变化：\n\n![different_lr_loss](../../data/images/bp/different_lr_loss.png)\n> 图片来源[如何理解梯度下降法？](https://mp.weixin.qq.com/s/SlTV6lbPnauf36bZLXglCw)。\n\n## 三，随机梯度下降与小批量随机梯度下降\n\n### 3.1，随机梯度下降\n\n在深度学习中，目标函数通常是训练数据集中每个样本的损失函数的平均值。如果使用梯度下降法，则每个自变量迭代的计算代价为 $O(n)$，它随 $n$（样本数目）线性增⻓。因此，当训练数据集较大时，每次迭代的梯度下降计算代价将较高。\n\n随机梯度下降(`SGD`)可降低每次迭代时的计算代价。在随机梯度下降的每次迭代中，我们对数据样本**随机均匀**采样一个索引 $i$，其中 $i \\in {1, . . . , n}$，并计算梯度 $\\nabla J(\\theta)$ 以更新权重参数 $\\theta$:\n\n$$\n\\theta_{n+1} = \\theta_{n} - \\eta \\cdot \\nabla J_i(\\theta) \\tag{3}\n$$\n\n每次迭代的计算代价从梯度下降的 $O(n)$ 降至常数 $O(1)$。另外，值得强调的是，随机梯度 $\\nabla J_i(\\theta)$ 是对完整梯度 $\\nabla J(\\theta)$ 的无偏估计。\n> 无偏估计是用样本统计量来估计总体参数时的一种无偏推断。 \n\n在实际应用中，**随机梯度下降 SGD 法必须和动态学习率**方法结合起来使用，否则使用固定学习率 + SGD的组合会使得模型收敛过程变得更复杂。学习率的调整策略可参考我之前写的文章-[深度学习炼丹-超参数设定和模型训练](https://github.com/HarleysZhang/deep_learning_alchemy/blob/main/3-deep_learning_alchemy/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E7%82%BC%E4%B8%B9-%E8%B6%85%E5%8F%82%E6%95%B0%E8%AE%BE%E5%AE%9A%E5%92%8C%E6%A8%A1%E5%9E%8B%E8%AE%AD%E7%BB%83.md)。\n\n### 3.2，小批量随机梯度下降\n\n前面讲的梯度下降（`GD`）和随机梯度下降（`SGD`）方法都过于极端，要么使用完整数据集来计算梯度并更新参数，要么一次只处理一个训练样本来更新参数。在实际项目中，会对两者取折中，即小批量随机梯度下降(`minibatch stochastic gradient descent`)，其有以下优点:\n\n- 首先，小批量损失的梯度是对训练集梯度的估计，其质量随着批量大小的增加而提高。\n\n- 其次，由于现代计算平台提供的**并行性**，批量计算比单个示例的 $m$ 计算效率更高。\n\n小批量的所有样本数据元素都是从训练集中**随机抽取**的，假设样本数目个数为 $m$（`batch_size = m`），即迭代训练的每一步我们都考虑的是一个大小为 $m$ 的小批量样本 $$\\mathbf{x_{1...m}}$$。\n\n$$\n\\theta_{n+1} = \\theta_{n} - \\eta \\cdot \\frac{1}{m}\\nabla \\sum_{i}^{m}J_i(\\textrm{x},\\theta) \\tag{3}\n$$\n\n> 梯度计算符号也可用  $\\frac{\\partial \\ell(\\textrm{x}_{i}, \\theta)}{\\partial \\theta}$表示，$\\textrm{x}$ 表示训练集， $\\theta$ 表示网络参数，$\\ell(\\textrm{x}_{i}, \\theta)$ 表示待优化的目标函数。\n\n另外，一般项目中使用 `SGD` 优化算法都默认会使用小批量随机梯度下降，即 `batch_size > 1`，除非显卡显存不够了，才会设置 `batch_size = 1`。\n\n## 四，总结\n\n虽然随机梯度下降法（`SGD`）简单有效，但它需要仔细调整模型超参数，特别是优化中使用的**学习率**，以及**模型参数的初始值**。 由于每一层的输入都受到前面所有层的参数的影响，因此训练变得很复杂。（因为随着网络变得更深，网络参数的微小变化会被累积和放大）\n\n## 参考资料\n\n1. [如何理解梯度下降法？](https://mp.weixin.qq.com/s/SlTV6lbPnauf36bZLXglCw)\n2. [AI-EDU: 梯度下降](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC1%E6%AD%A5%20-%20%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/02.3-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D.html)\n3. 《动手学习深度学习11章-优化算法》"
  },
  {
    "path": "4-deep_learning/深度学习基础/深度学习基础-优化算法详解.md",
    "content": "## 前言\n\n所谓深度神经网络的优化算法，**即用来更新神经网络参数，并使损失函数最小化的算法**。优化算法对于深度学习非常重要，如果说网络参数初始化（模型迭代的初始点）能够决定模型是否收敛，那优化算法的性能则**直接**影响模型的训练效率。\n\n了解不同优化算法的原理及其超参数的作用将使我们更有效的调整优化器的超参数，从而提高模型的性能。\n\n本文的优化算法特指: 寻找神经网络上的一组参数 $\\theta $，它能显著地降低损失函数 $J(\\theta )$，该损失函数通常包括整个训练集上的性能评估和额外的正则化项。\n\n> 本文损失函数、目标函数、代价函数不严格区分定义。\n\n## 一，梯度下降优化算法\n\n### 1.1，随机梯度下降 SGD\n\n梯度下降法是最基本的一类优化器，目前主要分为三种梯度下降法：标准梯度下降法(GD, Gradient Descent)，随机梯度下降法(SGD, Stochastic Gradient Descent)及批量梯度下降法(BGD, Batch Gradient Descent)。\n\n深度学习项目中的 `SGD` 优化一般默认指批量梯度下降法。其算法描述如下:\n\n- 输入和超参数: $\\eta$ 全局学习率\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n\n- 更新参数：$\\theta_t = \\theta_{t-1} - \\eta \\cdot g_t$\n\nSGD 优化算法是最经典的神经网络优化方法，**虽然收敛速度慢，但是收敛效果比较稳定**。\n\n下图1展现了随机梯度下降算法的梯度搜索轨迹示意图。可以看出由于梯度的随机性质，梯度搜索轨迹要很嘈杂（**动荡现象**）。\n\n![sgd_algorithm](../../data/images/optimizers/sgd_algorithm.png)\n\n因此，在实际应用中，**随机梯度下降 SGD 法必须和动态学习率**方法结合起来使用，否则使用固定学习率 + SGD的组合会使得模型收敛过程变得更复杂。\n\n### 1.2，动量 Momentum\n\n虽然**随机梯度下降**仍然是非常受欢迎的优化方法，但其学习过程有时会很慢且其梯度更新方向完全依赖于当前 `batch` 样本数据计算出的梯度，因而十分不稳定，因为数据可能有噪音。\n\n受启发于物理学研究领域研究，基于动量 Momentum  (Polyak, 1964) 的 SGD 算法**用于改善参数更新时可能产生的振荡现象**。动量算法旨在加速学习，特别是处理高曲率、小但一致的梯度，或是带噪声的梯度。两种算法效果对比如下图 2所示。\n\n> 花书中对动量算法对目的解释是，解决两个问题: Hessian 矩阵的病态条件和随机梯度的方差。更偏学术化一点。\n\n![动量算法效果](../../data/images/optimizers/momentum_algorithm.png)\n\nMomentum 算法的通俗理解就是，其模拟了物体运动时的惯性，即更新参数的时候会同时结合过去以及当前 batch 的梯度。算法在更新的时候会一定程度上保留之前更新的方向，同时利用当前 batch 的梯度微调最终的更新方向。这样一来，可以在一定程度上增加稳定性，从而学习地更快，并且还有一定摆脱局部最优的能力。\n\n下图3展现了动量算法的前进方向。\n\n![动量算法的前进方向](../../data/images/optimizers/momentum_algorithm_update.png)\n\n第一次的梯度更新完毕后，会记录 $v1$ 的动量值。在“求梯度点”进行第二次梯度检查时，得到2号方向，与 $v1$ 的动量**组合**后，最终的更新为 2' 方向。这样一来，由于有 $v1$ 的存在，会迫使梯度更新方向具备“惯性”，从而可以减小随机样本造成的震荡。\n\n**Momentum 算法描述如下**:\n\n1，**输入和参数**:\n\n- $\\eta$ - 全局学习率\n- $\\alpha$ - 动量参数，一般取值为 0.5, 0.9, 0.99，取 `0`，则等效于常规的随机梯度下降法，其**控制动量信息对整体梯度更新的影响程度**。\n- $v_t$ - 当前时刻的动量，初值为 0。\n\n2，**算法计算过程**：\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n\n- 计算速度更新：$v_t = \\alpha \\cdot v_{t-1} + \\eta \\cdot g_t$ (公式1)\n\n- 更新参数：$\\theta_t = \\theta_{t-1} - v_t$ (公式2)\n\n> 注意，这里的公式1和公式2和花书上的公式形式上略有不同，但其最终结果是相同的。本文给出的手工推导迭代公式，来源文章 [15.2 梯度下降优化算法](https://github.com/microsoft/ai-edu/blob/master/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC7%E6%AD%A5%20-%20%E6%B7%B1%E5%BA%A6%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/15.2-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D%E4%BC%98%E5%8C%96%E7%AE%97%E6%B3%95.md)。\n\n通过推导参数更新迭代公式，更容易理解算法，根据算法公式(1)(2)，以$W$参数为例，有：\n\n1. $v_0 = 0$\n2. $dW_0 = \\nabla J(w)$\n3. $v_1 = \\alpha v_0 + \\eta \\cdot dW_0 = \\eta \\cdot dW_0$\n4. $W_1 = W_0 - v_1=W_0 - \\eta \\cdot dW_0$\n5. $dW_1 = \\nabla J(w)$\n6. $v_2 = \\alpha v_1 + \\eta dW_1$\n7. $W_2 = W_1 - v_2 = W_1 - (\\alpha v_1 +\\eta dW_1) = W_1 - \\alpha \\cdot \\eta \\cdot dW_0 - \\eta \\cdot dW_1$\n8. $dW_2 = \\nabla J(w)$\n9. $v_3=\\alpha v_2 + \\eta dW_2$\n10. $W_3 = W_2 - v_3=W_2-(\\alpha v_2 + \\eta dW_2) = W_2 - \\alpha^2 \\eta dW_0 - \\alpha \\eta dW_1 - \\eta dW_2$\n\n可以看出与普通 SGD 的算法 $W_3 = W_2 - \\eta dW_2$ 相比，动量法不但每次要减去当前梯度，还要减去历史梯度$W_0, W_1$ 乘以一个不断减弱的因子$\\alpha$、$\\alpha^2$，因为$\\alpha$小于1，所以$\\alpha^2$比$\\alpha$小，$\\alpha^3$比$\\alpha^2$小。这种方式的学名叫做**指数加权平均**。\n\n在实际模型训练中，SGD 和动量法的比较如下表所示。\n\n> 实验效果对比图来源于资料 1。\n\n|算法     | 损失函数和准确率|\n|--------|------------------------------------------------------------|\n|SGD     |![SGD训练损失函数曲线](../../data/images/optimizers/op_sgd_train_loss_log.png)|\n|Momentum|![momentum训练损失函数曲线](../../data/images/optimizers/op_momentum_train_loss_log.png)|\n\n从上表的对比可以看出，同一个深度模型， 普通随机梯度下降法的曲线震荡很严重，且经过 epoch=10000 次也没有到达预定 0.001 的损失值；但动量算法经过 2000 个 epoch 就迭代结束。\n\n### 1.3，Nesterov 动量\n\n受 Nesterov 加速梯度算法 (Nesterov, 1983, 2004) 启发，Sutskever *et al.* (2013) 提出了动量算法的一个变种，Nesterov 动量随机下降法（`NAG` ，英文全称是 Nesterov Accelerated Gradient，或者叫做 Nesterov Momentum 。\n\nNesterov 动量随机梯度下降方法是在上述动量梯度下降法更新梯度时加入对当前梯度的校正，简单解释就是**往标准动量方法中添加了一个校正因子**。\n\nNAG 算法描述如下:\n\n1，**输入和参数**：\n\n- $\\eta$ - 全局学习率\n- $\\alpha$ - 动量参数，缺省取值 0.9\n- $v$ - 动量，初始值为0\n\n2，**算法计算过程**：\n\n- 参数临时更新：$\\hat \\theta = \\theta_{t-1} - \\alpha \\cdot v_{t-1}$\n- 网络前向传播计算：$f(\\hat \\theta)$\n- 计算梯度：$g_t = \\nabla_{\\hat\\theta} J(\\hat \\theta)$\n- 计算速度更新：$v_t = \\alpha \\cdot v_{t-1} + \\eta \\cdot g_t$\n- 更新参数：$\\theta_t = \\theta_{t-1}  - v_t$\n\n### 1.4，代码实践\n\nPytorch 框架中把普通 SGD、Momentum 算法和 Nesterov Momentum 算法的实现结合在一起了，对应的类是 `torch.optim.SGD`。注意，其更新公式与其他框架略有不同，其中 $p$、$g$、$v$、$\\mu$ 表示分别是参数、梯度、速度和动量。\n$$\n\\begin{align}\nv_{t+1} &= \\mu * v_{t} + g_{t+1} \\nonumber \\\\ \np_{t+1} &= p_{t} - \\text{lr} * v_{t+1} \\nonumber \\\\\n&= p_{t} - \\text{lr} * \\mu * v_{t} - \\text{lr} * g_{t+1} \\nonumber\n\\end{align}\n$$\n\n```python\n# 和源码比省略了部分不常用参数\nclass torch.optim.SGD(params, lr=required, momentum=0, dampening=0,\n                 weight_decay=0, nesterov=False)\n```\n\n1，**功能解释**：\n\n可实现 SGD 优化算法、带动量 SGD 优化算法、带 NAG(Nesterov accelerated gradient)动量 SGD 优化算法，并且均可拥有 weight_decay 项。\n\n2，**参数解释**：\n\n- `params`(iterable): 参数组(参数组的概念参考优化器基类: `Optimizer`)，即优化器要管理的那部分参数。\n\n- `lr`(float): 初始学习率，可按需随着训练过程不断调整学习率。\n- `momentum`(float): 动量因子，通常设置为 0.9，0.8。\n- `weight_decay`(float): 权值衰减系数，也就是 L2 正则项的系数。\n- `nesterov`(bool)- bool 选项，是否使用 NAG(Nesterov accelerated gradient)。\n\n训练模型时常用配置如下:\n\n```python\ntorch.optim.SGD(lr=0.02, momentum=0.9, weight_decay=0.0001)\n```\n\n## 二，自适应学习率算法\n\n神经网络研究员早就意识到学习率肯定是难以设置的超参数之一，因为它对深度学习模型的性能有着显著的影响。\n\n### 2.1，AdaGrad\n\n在 AdaGrad (Duchi et al., 2011) 提出之前，我们对于所有的参数使用相同的学习率进行更新，它是第一个自适应学习率算法，通过**所有梯度历史平方值之和的平方根**，从而使得步长单调递减。它根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率，从而避免统一的学习率难以适应所有维度的问题。\n\n`AdaGrad` 法根据训练轮数的不同，对学习率进行了动态调整。具体表现在，对低频出现的参数进行大的更新（快速下降的学习率），对高频出现的参数进行小的更新（相对较小的下降学习率）。因此，他很适合于处理稀疏数据。\n\nAdaGrad 算法描述如下:\n\n1，**输入和参数**\n\n- $\\eta$ - 全局学习率\n- $\\epsilon$ - 用于数值稳定的小常数，建议缺省值为`1e-6`\n- $r=0$ 初始值\n  \n\n**2，算法计算过程**：\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n\n- 累计平方梯度：$r_t = r_{t-1} + g_t \\odot g_t$\n\n- 计算梯度更新：$\\Delta \\theta = {\\eta \\over \\epsilon + \\sqrt{r_t}} \\odot g_t$(和动手学深度学习给出的学习率调整公式形式不同)\n\n- 更新参数：$\\theta_t=\\theta_{t-1} - \\Delta \\theta$\n\n$\\odot$ 按元素相乘，开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。\n\n**AdaGrad 总结**：在凸优化背景中，AdaGrad 算法具有一些令人满意的理论性质。但是，经验上已经发现，对于训练深度神经网络模型而言，从训练开始时积累梯度平方会导致有效学习率过早和过量的减小。**AdaGrad 在某些深度学习模型上效果不错，但不是全部**。\n\nPytorch 框架中 AdaGrad 优化器:\n\n```python\nclass torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0, initial _accumulator_value=0)\n```\n\n### 2.2，RMSProp\n\n`RMSProp`（Root Mean Square Prop），均方根反向传播。\n\nRMSProp 算法 (Hinton, 2012) 和 AdaGrad 算法的不同在于， **RMSProp算法使⽤了小批量随机梯度按元素平⽅的指数加权移动平均来调整学习率**。\n\nRMSProp 算法描述如下:\n\n1，**输入和参数**：\n\n- $\\eta$ - 全局学习率，建议设置为0.001\n- $\\epsilon$ - 用于数值稳定的小常数，建议缺省值为1e-8\n- $\\alpha$ - 衰减速率，建议缺省取值0.9\n- $r$ - 累积变量矩阵，与$\\theta$尺寸相同，初始化为0\n  \n\n2，**算法计算过程**（计算梯度和更新参数公式和 AdaGrad 算法一样）：\n\n- 累计平方梯度：$r = \\alpha \\cdot r + (1-\\alpha)(g_t \\odot g_t)$\n\n- 计算梯度更新：$\\Delta \\theta = {\\eta \\over \\sqrt{r + \\epsilon}} \\odot g_t$\n\n**RMSProp 总结**：经验上，RMSProp 已被证明是一种有效且实用的深度神经网络优化算法。目前，它是深度学习从业者经常采用的优化方法之一。其初始学习率设置为 `0.01` 时比较理想。\n\nPytorch 框架中 RMSprop 优化器:\n\n```python\nclass torch.optim.RMSprop(params, lr=0.01, alpha=0.99, eps=1e- 08, weight_decay=0, momentum=0, centered=False)\n```\n\n### 2.3，AdaDelta\n\nAdaDelta 法也是对 AdaGrad 法的一种改进，它**旨在解决深度模型训练后期，学习率过小问题**。相比计算之前所有梯度值的平方和，AdaDelta 法仅计算在一个大小为 $w$ 的时间区间内梯度值的累积和。\n\nRMSProp 算法计算过程如下:\n\n1，**参数定义**：\n\n- **Adadelta 没有学习率参数**。相反，它使用参数本身的变化率来调整学习率。\n\n- $s$ \\- 累积变量，初始值 0\n\n2，**算法计算过程**:\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n- 累积平方梯度：$s_t = \\alpha \\cdot s_{t-1} + (1-\\alpha) \\cdot g_t \\odot g_t$\n- 计算梯度更新：$\\Delta \\theta = \\sqrt{r_{t-1} + \\epsilon \\over s_t + \\epsilon} \\odot g_t$\n- 更新梯度：$\\theta_t = \\theta_{t-1} - \\Delta \\theta$\n- 更新变化量：$r = \\alpha \\cdot r_{t-1} + (1-\\alpha) \\cdot \\Delta \\theta \\odot \\Delta \\theta$\n\nPytorch 框架中 Adadelta 优化器:\n\n```python\nclass torch.optim.Adadelta(params, lr=1.0, rho=0.9, eps=1e- 06, weight_decay=0)\n```\n\n### 2.4，Adam\n\n**Adam** (Adaptive Moment Estimation，Kingma and Ba, 2014) 是另一种学习率自适应的优化算法，相当于 `RMSProp + Momentum` 的效果，即**动量项的 RMSprop 算法**。\n\nAdam 算法在 RMSProp 算法基础上对小批量随机梯度也做了指数加权移动平均。**和 AdaGrad 算法、RMSProp 算法以及 AdaDelta 算法一样，目标函数自变量中每个元素都分别拥有自己的学习率**。\n\nAdam 算法计算过程如下:\n\n1，**参数定义**：\n\n- $t$ - 当前迭代次数\n- $\\eta$ - 全局学习率，建议缺省值为0.001\n- $\\epsilon$ - 用于数值稳定的小常数，建议缺省值为1e-8\n- $\\beta_1, \\beta_2$ - 矩估计的指数衰减速率，$\\in[0,1)$，建议缺省值分别为0.9和0.999\n\n2，**算法计算过程**:\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n\n- 计数器加一：$t=t+1$\n- 更新有偏一阶矩估计：$m_t = \\beta_1 \\cdot m_{t-1} + (1-\\beta_1) \\cdot g_t$\n- 更新有偏二阶矩估计：$v_t = \\beta_2 \\cdot v_{t-1} + (1-\\beta_2)(g_t \\odot g_t)$\n- 修正一阶矩的偏差：$\\hat m_t = m_t / (1-\\beta_1^t)$\n- 修正二阶矩的偏差：$\\hat v_t = v_t / (1-\\beta_2^t)$\n- 计算梯度更新：$\\Delta \\theta = \\eta \\cdot \\hat m_t /(\\epsilon + \\sqrt{\\hat v_t})$\n- 更新参数：$\\theta_t=\\theta_{t-1} - \\Delta \\theta$\n\n从上述公式可以看出 Adam 使用**指数加权移动平均值**来估算梯度的动量和二次矩，即使用了状态变量 $m_t、v_t$。\n\n**怎么理解 Adam 算法？**\n\n首先，在 Adam 中，动量直接并入了梯度一阶矩(指数加权)的估计。将动量加入 RMSProp 最直观的方法是将动量应用于缩放后的梯度。结合缩放的动量使用没有明确的理论动机。其次，Adam 包括偏置修正，修正从原点初始化的一阶矩(动量项)和(非中心的)二阶矩的估计。RMSProp 也采用了(非中心的)二阶矩估计，然而缺失了修正因子。因此，不像 Adam，RMSProp 二阶矩估计可能在训练初期有很高的偏置。 Adam 通常被认为对超参数的选择相当鲁棒，尽管学习率有时需要从建议的默认修改。\n\n> 初学者看看公式就行，这段话我也是摘抄花书，目前没有很深入理解。\n\nAdam 算法实现步骤如下。\n\n![Adam 算法实现步骤](../../data/images/optimizers/adam_algorithm.png)\n\n**Adam 总结**：由于 Adam 继承了 RMSProp 的传统，所以学习率同样不宜设置太高，初始学习率设置为 `0.01` 时比较理想。\n\nPytorch 框架中 Adam 优化器:\n\n```python\nclass torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e- 08, weight_decay=0, amsgrad=False)\n```\n\n## 三，总结\n\n### 3.1，PyTorch 的十个优化器\n\nPyTorch 中所有的优化器基类是 `Optimizer` 类，在 Optimizer 类中定义了 5 个 实用的基本方法，分别是 \n\n- `zero_grad()`：将梯度清零。\n-  `step(closure)`：执行一步权重参数值更新, 其中可传入参数 closure(一个闭包)。\n- `state_dict()`：获取模型当前参数，以一个有序字典形式返回，key 是参数名，value 是参数。\n- `load_state_dict(state_dict)` ：将 state_dict 中的参数加载到当前网络，常用于 finetune。\n-  `add_param_group(param_group)`：给 optimizer 管理的参数组中增加一组参数，可为该组参数定制 lr, momentum, weight_decay 等，在 finetune 中常用。\n\nPyTorch 基于 `Optimizer` 基类构建了十种优化器，有常见的 SGD、ASGD、Rprop、 RMSprop、Adam 等等。注意 PyTorch 中的优化器和前文描述的优化算法略有不同，PyTorch 中 给出的优化器与原始论文中提出的优化方法，多少是有些改动的，详情可直接阅读源码。\n\n### 3.2，优化算法总结\n\n- Adam 等自适应学习率算法对于稀疏数据具有优势，且收敛速度很快；但精调参数的 SGD（+Momentum）往往能够取得更好的最终结果。\n\n- 用相同数量的超参数来调参，尽管有时自适应优化算法在训练集上的 loss 更小，但是他们在测试集上的 loss 却可能比 SGD 系列方法高。\n\n- 自适应优化算法在训练前期阶段在训练集上收敛的更快，但可能在测试集上的泛化性不好。 \n- 目前，最流行并且使用很高的优化算法包括 SGD、具动量的 SGD、RMSProp、 具动量的 RMSProp、AdaDelta 和 Adam。但选择哪一个算法主要取决于使用者对算法的熟悉程度(更方便调节超参数)。\n\n## 参考资料\n\n1. [《智能之门-神经网络与深度学习入门》-15.2 梯度下降优化算法](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC7%E6%AD%A5%20-%20%E6%B7%B1%E5%BA%A6%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/15.2-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D%E4%BC%98%E5%8C%96%E7%AE%97%E6%B3%95.html)\n2. 《深度学习》-第八章 深度模型中的优化\n3. 《动手学深度学习》-优化算法\n"
  },
  {
    "path": "4-deep_learning/深度学习基础/深度学习基础-参数初始化详解.md",
    "content": "- [一，参数初始化概述](#一参数初始化概述)\n  - [1.1，进行网络参数初始化的原因](#11进行网络参数初始化的原因)\n  - [1.2，网络参数初始化为什么重要](#12网络参数初始化为什么重要)\n- [二，权重初始化方式分类](#二权重初始化方式分类)\n  - [2.1，全零初始化](#21全零初始化)\n  - [2.2，标准初始化](#22标准初始化)\n  - [2.3，Xavier 初始化](#23xavier-初始化)\n  - [2.4，He 初始化](#24he-初始化)\n  - [2.5，总结](#25总结)\n- [参考资料](#参考资料)\n\n> 本文内容参考资料为《智能之门-神经网络与深度学习入门》和《解析卷积神经网络》两本书，以及部分网络资料，加以个人理解和内容提炼总结得到，文中所有直方图的图片来源于参考资料3。\n\n## 一，参数初始化概述\n\n我们知道神经网络模型一般是依靠**随机梯度下降**优化算法进行神经网络参数更新的，而神经网络参数学习是非凸问题，利用梯度下降算法优化参数时，**网络权重参数的初始值选取十分关键**。\n\n首先得明确的是现代的**网络参数初始化策略**是简单的、启发式的。设定改进的初始化策略是一项困难的 任务，因为神经网络优化至今还未被很好地理解（即模型训练过程是一个黑盒）。\n\n大多数初始化策略基于在神经网络初始化时实现一些很好的性质。然而，我们并没有很好地理解这些性质中的哪些会在学习开始进行后的哪些情况下得以保持。进一步的难点是，有些初始点从优化的观点看或许是有利的，但是从泛化的观点看是不利的。我们对于初始点如何影响泛化的理解是相当原始的，几乎没有提供如何选择初始点的任何指导。\n\n### 1.1，进行网络参数初始化的原因\n\n深度学习模型（神经网络模型）的训练算法通常是**迭代**的，因此模型训练者需要指定开始迭代的初始点，即择网络参数初始化策略。\n\n### 1.2，网络参数初始化为什么重要\n\n训练深度学习模型是一个足够困难的问题，以至于大多数算法都很大程度受到网络初始化策略的影响。\n\n**模型迭代的初始点能够决定算法是否收敛**，有些初始点十分不稳定，使得该算法会遭遇数值困难，并可能完全失败。当学习收敛时，初始点可以决定学习收敛得多快，以及是否收敛到一个代价高或低的点。另外，即使是具有同一个损失代价的迭代点也会有差别极大的泛化误差，而迭代初始点也可以影响泛化（误差）。\n\n## 二，权重初始化方式分类\n\n在实际应用中，模型权重参数服从**高斯分布**（`Gaussian distribution`）或**均匀分布**（`uniform distribution`）都是较为**有效**的初始化方式。值得注意的是，这两种分布选择的区别，目前还没有被详尽的研究清楚，能够确定只有，初始分布的大小确实对优化过程的结果和网络泛化能力都有很大的影响。\n> 权重初始化随机值的初始化策略的分布都对模型性能有影响，但是影响的原理（神经网络优化原理）又没有被彻底研究清楚，所谓模型训练，还真不愧是炼丹，部分时候还真得靠炼丹人的经验。\n\n另一种神经网络参数初始化策略总结如下：\n\n1. **加载预训练模型参数初始化**：直接加载在大规模数据集上训练得到模型参数，一定程度上提升模型的泛化能力。\n2. **随机初始化**：注意不能将参数值全部初始化为 `0`，因为如果神经网络第一遍前向传播所有隐层神经网络激活值相同，反向传播权重更新也相同，导致隐藏层的各个神经元没有区分性，导致“对称权重”现象。较好的方式是对每个参数进行**随机初始化**。\n3. **固定参数值初始化**：比如对于偏置（bias）通常用 `0` 初始化，LSTM 遗忘门偏置通常为 1或 2，使时序上的梯度变大，对于 ReLU 神经元，偏置设为 0.01，使得训练初期更容易激活。\n\n虽然不同文章对参数初始化方法的分类有着不同的总结，因此，本文直接给出常用且有效的初始化方法名称，并以 `Pytorch` 框架为例，给出相应 `API`。\n\n### 2.1，全零初始化\n\n虽然参数(权值)在理想情况下应基本保持正负各半的状态(**期望为 0**)，但是，这不意味着可以将所有参数都初始化为 `0`（$W=0$）！零值初始化的权重矩阵值打印输出示例如下所示。\n\n```python\nW1= [[-0.82452497 -0.82452497 -0.82452497]]\nB1= [[-0.01143752 -0.01143752 -0.01143752]]\nW2= [[-0.68583865]\n     [-0.68583865]\n     [-0.68583865]]\nB2= [[0.68359678]] # 单个输出的的双层神经网络\n```\n\n可以看到 `W1、B1、W2` 内部 `3` 个单元的值都一样，这是因为初始值都是 0，所以梯度均匀回传，导致所有神经元的权重 `W` 的值都同步更新，没有任何差别，这样无论训练多少轮，结果也不会正确。\n\n### 2.2，标准初始化\n\n1，高斯分布初始化：使用高斯分布对每个参数随机初始化，即将权重 $W$ 按如下公式进行初始化:\n\n$$\nW \\sim N[0, \\sigma^2]\n$$\n其中 $N$ 表示**高斯分布**（Gaussian Distribution，也叫做正态分布，Normal Distribution），上式是位置参数 $\\mu = 0$（**期望值**），尺度参数 $\\sigma^2$（**方差**） 的高斯分布（也叫标准高斯分布）。有的地方也称为 **`Normal` 初始化**。\n\n`Pytorch` 框架中对应的 `API` 如下。\n```python\n# 一般默认采用标准高斯分布初始化方法，即均值为 0，方差为 1，\ntorch.nn.init.normal_(tensor, mean=0, std=1)\n```\n\n2，与高斯分布初始化方式类似的是**均匀分布初始化**，其参数范围区是 $[-r, r]$。\n\n`Pytorch` 框架中对应的 `API` 如下。\n```python\ntorch.nn.init.uniform_(tensor, a=0, b=1)\n```\n\n高斯分布和均匀分布都是固定方差参数的初始化方法，它们的关键是：如何设置方差！\n- 如果太小，会导致神经元输出过小，经过多层则梯度信号消失了。\n- 如果太大，`sigmoid` 激活梯度接近 `0`，导致梯度消失。一般需要配合 `BN` 层一起使用。\n\n当目标问题较为简单、网络深度不大时，一般用标准初始化就可以了。但是当使用深度神经网络时，标准初始化在 Sigmoid 激活函数上的表现会遇到如下图 1 所示的问题。\n\n![图1-标准初始化在Sigmoid激活函数上的表现](../../data/images/parameter_init/init_normal_sigmoid.png)\n\n上图是一个 `6`层的深度网络，使用 **全连接层 + Sigmoid 激活函数**的配置，图中表示的是**各层激活函数的直方图**。可以看到各层的激活值严重向两侧 $[0,1]$ 靠近，从 `Sigmoid` 的函数曲线可以知道这些值的导数趋近于 `0`（激活函数值趋近于零，导数也趋近于零），**反向传播时的梯度逐步消失**。处于中间地段的值比较少，对参数学习非常不利。\n> 传统的**固定方差**的高斯分布初始化方法，在网络变深的时候会使得模型很难收敛。\n\n### 2.3，Xavier 初始化\n\n基于上述观察（标准初始化在 `Sigmoid` 激活函数上的表现），Xavier Glorot 等人于 `2010` 年研究出了下面的`Xavier` 初始化方法。\n\n`Xavier` 初始化方法比直接用高斯分布进行初始化 $W$ 的优势在于：一般的神经网络在前向传播时神经元输出值的方差会不断增大，而使用 `Xavier` 等方法理论上可以保证**每层神经元输入输出数据分布方差一致**（和 `BN` 层效果类似，具体原理参考[Xavier 论文](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)）。\n\n条件：正向传播时，激活值的方差保持不变；反向传播时，关于状态值的梯度的方差保持不变。\n\n使用 `Sigmoid` 激活函数时，**Xavier 高斯分布**初始化的公式如下所示:\n\n$$\nW \\sim N\n\\begin{pmatrix}\n0, \\sqrt{\\frac{2}{n_{in} + n_{out}}} \n\\end{pmatrix}\n$$\n\n`Pytorch` 框架中对应的 `API` 如下。\n```python\ntorch.nn.init.xavier_normal_(tensor, gain=1)\n```\n\n另外，还有 Xavier 均匀分布的公式如下所示（个人感觉使用频率不多）。\n\n$$\nW \\sim U \n\\begin{pmatrix}\n -\\sqrt{\\frac{6}{n_{in} + n_{out}}}, \\sqrt{\\frac{6}{n_{in} + n_{out}}} \n\\end{pmatrix}\n$$\n\n下图2展示了 Xavier 初始化在 Sigmoid 激活函数上的表现，其和图1都基于同一个深度为 `6` 层的网络。可以看到，后面几层的**激活函数输出值**的分布**仍然**基本符合正态分布，这有利于神经网络的学习。\n\n![图2-Xavier初始化在Sigmoid激活函数上的表现](../../data/images/parameter_init/init_xavier_sigmoid.png)\n\n### 2.4，He 初始化\n\n随着深度学习的发展，人们觉得 `Sigmoid` 激活在反向传播算法中效果有限且会导致梯度消失问题，于是又提出了 `ReLU` 激活函数。\n\n但 Xavier 初始化在 ReLU 激活函数上的表现并不好。随着网络层的加深，使用 `ReLU` 时激活值逐步向 `0` 偏向，这同样会导致梯度消失问题。\n\n下图3展现了 Xavier 初始化在 ReLU 激活函数上的表现。\n\n![Xavier初始化在ReLU激活函数上的表现](../../data/images/parameter_init/init_xavier_relu.png)\n\n于是 He Kaiming 等人于 2015 年提出了 He 初始化法（也叫做MSRA初始化法）。\n\nHe 初始化法最主要是想解决使用 `ReLU` 激活函数后，方差会发生变化的问题。\n\n只考虑输入个数时，He 初始化是一个均值为 0，方差为 $2/n$ 的高斯分布，适合于 ReLU 激活函数:\n\n$$\nW \\sim N \n\\begin{pmatrix} \n0, \\sqrt{\\frac{2}{n}} \n\\end{pmatrix}\n$$\n\n其中 $n$ 为网络层输入神经元数量个数。\n\n`Pytorch` 框架中对应的 `API` 如下。\n\n```python\ntorch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')\n```\n\n下图4 为 He 初始化在 ReLU 激活函数上的表现，所用网络和前面一样。\n\n![图4-He初始化在ReLU激活函数上的表现](../../data/images/parameter_init/init_msra_relu.png)\n\n### 2.5，总结\n\n- Xavier 初始化和 He 初始化目的都是想**可以根据神经元连接数量自适应调整初始化分布的方差，也称为“方差缩放”**。\n- 网络参数初始化方法的选择对保持数值稳定性至关重要，其与非线性激活函数的选择得结合一起考虑。\n- 网络参数初始化的选择可以决定优化算法收敛的速度有多快。糟糕选择可能会导致我们在训练神经网络模型时遇到梯度爆炸或梯度消失。\n\n除了以上初始化，比较常用的还有“随机初始化 With BN”和“预训练模型初始化”，各种初始化方法的特点总结如下:\n\n![参数初始化方法总结](../../data/images/parameter_init/parameter_init_summary.png)\n## 参考资料\n\n- 《深度学习-8.4 参数初始化策略》\n- 《解析卷积神经网络-章 7 网络参数初始化》\n- [AIEDU-15.1 权重矩阵初始化](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC7%E6%AD%A5%20-%20%E6%B7%B1%E5%BA%A6%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/15.1-%E6%9D%83%E9%87%8D%E7%9F%A9%E9%98%B5%E5%88%9D%E5%A7%8B%E5%8C%96.html)\n- [神经网络之权重初始化](https://www.cnblogs.com/makefile/p/init-weight.html)\n- [神经网络参数初始化小结](https://zhuanlan.zhihu.com/p/133835463)"
  },
  {
    "path": "4-deep_learning/深度学习基础/深度学习基础-损失函数详解.md",
    "content": "- [一，损失函数概述](#一损失函数概述)\n- [二，交叉熵函数-分类损失](#二交叉熵函数-分类损失)\n  - [2.1，交叉熵（Cross-Entropy）的由来](#21交叉熵cross-entropy的由来)\n    - [2.1.1，熵、相对熵以及交叉熵总结](#211熵相对熵以及交叉熵总结)\n  - [2.2，二分类问题的交叉熵](#22二分类问题的交叉熵)\n  - [2.3，多分类问题的交叉熵](#23多分类问题的交叉熵)\n  - [2.4，PyTorch 中的 Cross Entropy](#24pytorch-中的-cross-entropy)\n    - [2.4.1，Softmax 多分类函数](#241softmax-多分类函数)\n  - [2.5，为什么不能使用均方差做为分类问题的损失函数？](#25为什么不能使用均方差做为分类问题的损失函数)\n- [三，回归损失](#三回归损失)\n  - [3.1，MAE 损失](#31mae-损失)\n  - [3.2，MSE 损失](#32mse-损失)\n  - [3.3，`Huber` 损失](#33huber-损失)\n  - [3.4，代码实现](#34代码实现)\n- [参考资料](#参考资料)\n\n## 一，损失函数概述\n\n大多数深度学习算法都会涉及某种形式的优化，**所谓优化指的是改变 $x$ 以最小化或最大化某个函数 $f(x)$ 的任务**，我们通常以最小化 $f(x)$ 指代大多数最优化问题。\n\n在机器学习中，损失函数是代价函数的一部分，而代价函数是目标函数的一种类型。\n- **损失函数**（`loss function`）: 用于定义单个训练样本预测值与真实值之间的误差\n- **代价函数**（`cost function`）: 用于定义单个批次/整个训练集样本预测值与真实值之间的累计误差。\n- **目标函数**（`objective function`）: 泛指任意可以被优化的函数。\n\n**损失函数定义**：损失函数是用来量化模型预测和真实标签之间差异的一个非负实数函数，其和优化算法紧密联系。深度学习算法优化的第一步便是确定损失函数形式。\n\n损失函数大致可分为两种：回归损失（针对连续型变量）和分类损失（针对离散型变量），其在深度学习实验流程中的位置如下图所示。\n\n![深度学习的实验流程](../../data/images/loss/define_loss.png)\n\n> 图片来源李宏毅 2022 机器学习暑期课程-[Machine Learning Pytorch Tutorial](https://speech.ee.ntu.edu.tw/~hylee/ml/ml2022-course-data/Pytorch%20Tutorial%201.pdf)。\n\n常用的减少损失函数的优化算法是“梯度下降法”（Gradient Descent）。\n\n## 二，交叉熵函数-分类损失\n\n交叉熵损失(`Cross-Entropy Loss`) 又称为对数似然损失(Log-likelihood Loss)、对数损失，二分类时还可称之为逻辑斯谛回归损失(Logistic Loss)。\n\n### 2.1，交叉熵（Cross-Entropy）的由来\n> 交叉熵损失的由来参考文档 [AI-EDU: 交叉熵损失函数](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/)。\n\n**1，信息量**\n\n信息论中，信息量的表示方式：\n> 《深度学习》（花书）中称为自信息(self-information) 。\n> 在本文中，我们总是用 $\\text{log}$ 来表示自然对数，**其底数**为 $e$。\n\n$$\nI(x_j) = -\\log (p(x_j))\n$$\n\n- $x_j$：表示一个事件\n- $p(x_j)$：表示事件 $x_j$ 发生的概率\n- $I(x_j)$：信息量，$x_j$ 越不可能发生时，它一旦发生后的信息量就越大\n\n**2，熵**\n\n信息量只处理单个的输出。我们可以用熵（也称香农熵 `Shannon entropy`）来对整个概率分布中的不确定性总量进行量化:\n\n$$\nH(p) = - \\sum_j^n p(x_j) \\log (p(x_j))\n$$\n\n则上面的问题的熵是：\n$$\n\\begin{aligned} H(p)&=-[p(x_1) \\ln p(x_1) + p(x_2) \\ln p(x_2) + p(x_3) \\ln p(x_3)] \\\\\\ &=0.7 \\times 0.36 + 0.2 \\times 1.61 + 0.1 \\times 2.30 \\\\\\ &=0.804 \\end{aligned}\n$$\n**3，相对熵(KL散度)**\n\n相对熵又称 `KL` 散度，如果对于同一个随机变量 $x$ 有两个单独的概率分布 $P(x)$ 和 $Q(x)$，则可以使用 KL 散度（Kullback-Leibler (KL) divergence）来**衡量这两个分布的差异**，这个相当于信息论范畴的均方差。\n\nKL散度的计算公式：\n\n$$\nD_{KL}(p||q)=\\sum_{j=1}^m p(x_j) \\log {p(x_j) \\over q(x_j)}\n$$\n\n$m$ 为事件的所有可能性（分类任务中对应类别数目）。**$D$ 的值越小，表示 $q$ 分布和 $p$ 分布越接近**。\n\n**4，交叉熵**\n\n把上述交叉熵公式变形：\n\n$$\n\\begin{aligned} D_{KL}(p||q)&=\\sum_{j=1}^m p(x_j) \\log {p(x_j)} - \\sum_{j=1}^m p(x_j) \\log q(x_j) \\\\\\ &=- H(p(x)) + H(p,q) \\end{aligned}\n$$\n\n等式的前一部分恰巧就是 $p$ 的熵，等式的后一部分，就是**交叉熵**（机器学习中 $p$ 表示真实分布（目标分布），$q$ 表示预测分布）:\n\n$$\nH(p,q) =- \\sum_{j=1}^m p(x_j) \\log q(x_j)\n$$\n\n在机器学习中，我们需要评估**标签值 $y$ 和预测值 $a$** 之间的差距熵（即**两个概率分布之间的相似性**），使用 KL 散度 $D_{KL}(y||a)$ 即可，但因为样本标签值的分布通常是固定的，即 $H(a)$ 不变。因此，为了计算方便，在优化过程中，只需要关注交叉熵就可以了。所以，**在机器学习中一般直接用交叉熵做损失函数来评估模型**。\n\n$$\nloss = \\sum_{j = 1}^{m}y_{j}\\text{log}(a_{j})\n$$\n\n上式是单个样本的情况，$m$ **并不是样本个数，而是分类个数**。所以，对于**批量样本的交叉熵损失**计算公式（很重要!）是：\n\n$$\nJ = -\\frac{1}{n}\\sum_{i=1}^n \\sum_{j=1}^{m} y_{ij} \\log a_{ij}\n$$\n\n其中，$n$ 是样本数，$m$ 是分类数。\n> 公式参考文章-[AI-EDU: 交叉熵损失函数](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC1%E6%AD%A5%20-%20%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/03.2-%E4%BA%A4%E5%8F%89%E7%86%B5%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0.html)，但是将样本数改为 $n$，类别数改为 $m$。\n\n有一类特殊问题，就是事件只有两种情况发生的可能，比如“是狗”和“不是狗”，称为 $0/1$ 分类或**二分类**。对于这类问题，由于 $m=2，y_1=1-y_2，a_1=1-a_2$，所以**二分类问题的单个样本的交叉熵**可以简化为：\n\n$$\nloss =-[y \\log a + (1-y) \\log (1-a)]\n$$\n\n**二分类对于批量样本的交叉熵**计算公式是：\n\n$$\nJ= -\\frac{1}{n} \\sum_{i=1}^n [y_i \\log a_i + (1-y_i) \\log (1-a_i)]\n$$\n> 为什么交叉熵的代价函数是求均值而不是求和?\n>  Cross entropy loss is defined as the “expectation” of the probability distribution of a random variable 𝑋, and that’s why we use mean instead of sum. 参见[这里](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/information-theory.html#cross-entropy)。\n\n#### 2.1.1，熵、相对熵以及交叉熵总结\n\n> 交叉熵 $H(p, q)$ 也记作 $CE(p, q)$、$H(P, Q)$，其另一种表达公式（公式表达形式虽然不一样，但是意义相同）:\n> $$H(P, Q)  = -\\mathbb{E}_{\\textrm{x}\\sim p}log(q(x))$$\n\n交叉熵函数常用于逻辑回归(`logistic regression`)，也就是分类(`classification`)。\n\n根据信息论中熵的性质，将熵、相对熵（KL 散度）以及交叉熵的公式放到一起总结如下:\n\n$$\\begin{aligned}\nH(p) &= -\\sum_{j}p(x_j) \\log p(x_j) \\\\\nD_{KL}(p \\parallel q) &= \\sum_{j}p(x_j)\\log \\frac{p(x_j)}{q(x_j)} = \\sum_j (p(x_j)\\log p(x_j) - p(x_j) \\log q(x_j)) \\\\\nH(p,q) &=  -\\sum_j p(x_j)\\log q(x_j) \\\\\n\\end{aligned} $$\n\n### 2.2，二分类问题的交叉熵\n\n把二分类的交叉熵公式 4 分解开两种情况：\n- 当 $y=1$ 时，即标签值是 $1$ ，是个正例，加号后面的项为: $loss = -\\log(a)$\n- 当 $y=0$ 时，即标签值是 $0$，是个反例，加号前面的项为 $0$: $loss = -\\log (1-a)$\n\n横坐标是预测输出，纵坐标是损失函数值。$y=1$ 意味着当前样本标签值是1，当预测输出越接近1时，损失函数值越小，训练结果越准确。当预测输出越接近0时，损失函数值越大，训练结果越糟糕。此时，损失函数值如下图所示。\n\n![二分类交叉熵损失函数图](../../data/images/loss/二分类交叉熵损失函数图.png)\n\n### 2.3，多分类问题的交叉熵\n\n当标签值不是非0即1的情况时，就是多分类了。\n\n假设希望根据图片动物的轮廓、颜色等特征，来预测动物的类别，有三种可预测类别：猫、狗、猪。假设我们训练了两个分类模型，其预测结果如下:\n\n**模型1**:\n\n|预测值|标签值|是否正确|\n|-----|-----|-------|\n|0.3 0.3 0.4|0 0 1（猪）|正确|\n|0.3 0.4 0.4|0 1 0（狗）|正确|\n|0.1 0.2 0.7|1 0 0（猫）|错误|\n\n每行表示不同样本的预测情况，公共 3 个样本。可以看出，模型 1 对于样本 1 和样本 2 以非常微弱的优势判断正确，对于样本 3 的判断则彻底错误。\n\n**模型2**:\n\n|预测值|标签值|是否正确|\n|-----|-----|-------|\n|0.1 0.2 0.7|0 0 1（猪）|正确|\n|0.1 0.7 0.2|0 1 0（狗）|正确|\n|0.3 0.4 0.4|1 0 0（猫）|错误|\n\n可以看出，模型 2 对于样本 1 和样本 2 判断非常准确（预测概率值更趋近于 1），对于样本 3 虽然判断错误，但是相对来说没有错得太离谱（预测概率值远小于 1）。\n\n结合多分类的交叉熵损失函数公式可得，模型 1 的交叉熵为:\n\n$$\\begin{aligned} \n\\text{sample}\\ 1\\ \\text{loss} = -(0\\times log(0.3) + 0\\times log(0.3) + 1\\times log(0.4) = 0.91 \\\\\n\\text{sample}\\ 1\\ \\text{loss} = -(0\\times log(0.3) + 1\\times log(0.4) + 0\\times log(0.4) = 0.91 \\\\\n\\text{sample}\\ 1\\ \\text{loss} = -(1\\times log(0.1) + 0\\times log(0.2) + 0\\times log(0.7) = 2.30\n\\end{aligned}$$\n\n对所有样本的 `loss` 求平均:\n\n$$\nL = \\frac{0.91 + 0.91 + 2.3}{3} = 1.37\n$$\n\n模型 2 的交叉熵为:\n\n$$\\begin{aligned} \n\\text{sample}\\ 1\\ \\text{loss} = -(0\\times log(0.1) + 0\\times log(0.2) + 1\\times log(0.7) = 0.35 \\\\\n\\text{sample}\\ 1\\ \\text{loss} = -(0\\times log(0.1) + 1\\times log(0.7) + 0\\times log(0.2) = 0.35 \\\\\n\\text{sample}\\ 1\\ \\text{loss} = -(1\\times log(0.3) + 0\\times log(0.4) + 0\\times log(0.4) = 1.20\n\\end{aligned} $$\n\n对所有样本的 `loss` 求平均:\n\n$$\nL = \\frac{0.35 + 0.35 + 1.2}{3} = 0.63\n$$\n\n可以看到，0.63 比 1.37 的损失值小很多，这说明预测值越接近真实标签值，即交叉熵损失函数可以较好的捕捉到模型 1 和模型 2 预测效果的差异。**交叉熵损失函数值越小，反向传播的力度越小**。\n> 多分类问题计算交叉熵的实例来源于知乎文章-[损失函数｜交叉熵损失函数](https://zhuanlan.zhihu.com/p/35709485)。\n\n### 2.4，PyTorch 中的 Cross Entropy\n\nPyTorch 中常用的交叉熵损失函数为 `torch.nn.CrossEntropyLoss`\n\n```python\nclass torch.nn.CrossEntropyLoss(weight=None, size_average=None,\n                                ignore_index=-100, reduce=None, \n                                reduction='elementwise_mean')\n```\n\n**1，函数功能**:\n\n将输入经过 `softmax` 激活函数之后，再计算其与 `target` 的交叉熵损失。即该方法将 `nn.LogSoftmax()` 和 `nn.NLLLoss()`进行了结合。严格意义上的交叉熵损失函数应该是 `nn.NLLLoss()`。\n\n**2，参数解释**:\n\n- `weight`(Tensor)- 为每个类别的 loss 设置权值，常用于类别不均衡问题。weight 必须是 float 类型的 tensor，其长度要于类别 `C` 一致，即每一个类别都要设置有 weight。\n- `size_average`(bool)- 当 reduce=True 时有效。为 True 时，返回的 loss 为平均值;为 False 时，返回的各样本的 loss 之和。\n- `reduce`(bool)- 返回值是否为标量，默认为 True。\n- `ignore_index`(int)- 忽略某一类别，不计算其 `loss`，其 loss 会为 0，并且，在采用 size_average 时，不会计算那一类的 loss，除的时候的分母也不会统计那一类的样本。\n\n#### 2.4.1，Softmax 多分类函数\n> 注意: Softmax 用作模型最后一层的函数通常和交叉熵作损失函数配套搭配使用，应用于多分类任务。\n\n对于二分类问题，我们使用 `Logistic` 函数计算样本的概率值，从而把样本分成了正负两类。对于多分类问题，则使用 `Softmax` 作为模型最后一层的激活函数来将**多分类的输出值转换为范围在 [0, 1] 和为 1 的概率分布**。\n\nSoftmax 从字面上来说，可以分成 soft 和 max 两个部分。max 故名思议就是最大值的意思。Softmax 的核心在于 soft，而 soft 有软的含义，与之相对的是 hard 硬，即 herdmax。下面分布演示将模型输出值**取 max 值**和**引入 Softmax** 的对比情况。\n\n**取max值（hardmax）**\n\n假设模型输出结果 $z$ 值是 $[3,1,-3]$，如果取 max 操作会变成 $[1, 0, 0]$，这符合我们的分类需要，即三者相加为1，并且认为该样本属于第一类。但是有两个不足：\n\n1. 分类结果是 $[1,0,0]$，只保留非 0 即 1 的信息，即非黑即白，没有各元素之间相差多少的信息，可以理解是“Hard Max”；\n2. max 操作本身不可导，无法用在反向传播中。\n\n**引入Softmax**\n\n`Softmax` 加了个\"soft\"来模拟 max 的行为，但同时又保留了相对大小的信息。\n\n$$\na_j = \\text{Softmax}(z_j) = \\frac{e^{z_j}}{\\sum\\limits_{i=1}^m e^{z_i}}=\\frac{e^{z_j}}{e^{z_1}+e^{z_2}+\\dots+e^{z_m}}\n$$\n\n上式中:\n\n- $z_j$ 是对第 $j$ 项的分类原始值，即矩阵运算的结果\n- $z_i$ 是参与分类计算的每个类别的原始值\n- $m$ 是总分类数\n- $a_j$ 是对第 $j$ 项的计算结果\n\n和 hardmax 相比，Softmax 的含义就在于不再唯一的确定某一个最大值，而是为每个输出分类的结果都赋予一个概率值（置信度），表示属于每个类别的可能性。\n\n下图可以形象地说明 Softmax 的计算过程。\n\n![Softmax工作过程](../../data/images/loss/softmax_process.png)\n\n当输入的数据 $[z_1,z_2,z_3]$ 是 $[3, 1, -3]$ 时，按照图示过程进行计算，可以得出输出的概率分布是 $[0.879,0.119,0.002]$。对比 max 运算和 Softmax 的不同，如下表所示。\n\n|输入原始值|MAX计算|Softmax计算|\n|--------|-------|----------|\n|$[3, 1, -3]$|$[1, 0, 0]$|$[0.879, 0.119, 0.002]$|\n\n可以看出 Softmax 运算结果两个特点：\n\n1. 三个类别的概率相加为 1\n2. 每个类别的概率都大于 0\n\n下面我再给出 hardmax 和 softmax 计算的代码实现。\n\n```python\n# example of the argmax of a list of numbers\nfrom numpy import argmax\nfrom numpy import exp\n\n# define data\ndata = [3, 1, -3]\n\ndef hardmax(data):\n    \"\"\"# calculate the argmax of the list\"\"\"\n    result = argmax(data) \n    return result\n\ndef softmax(vector):\n    \"\"\"# calculate the softmax of a vector\"\"\"\n    e = exp(vector)\n    return e / e.sum()\n\nhardmax_result = hardmax(data)\n# 运行该示例返回列表索引值“0”，该值指向包含列表“3”中最大值的数组索引 [1]。\nprint(hardmax(data)) # 0\n\n# convert list of numbers to a list of probabilities\nsoftmax_result = softmax(data) \nprint(softmax_result) # report the probabilities\nprint(sum(softmax_result)) # report the sum of the probabilitie\n```\n\n运行以上代码后，输出结果如下:\n> 0\n[0.87887824 0.11894324 0.00217852]\n1.0\n\n很明显程序的输出结果和我们手动计算的结果是一样的。\n\nPytorch 中的 Softmax 函数定义如下:\n\n```python\ndef softmax(x):\n    return torch.exp(x)/torch.sum(torch.exp(x), dim=1).view(-1,1)\n```\n\n`dim=1` 用于 `torch.sum()` 对所有列的每一行求和，`.view(-1,1)` 用于防止广播。\n\n### 2.5，为什么不能使用均方差做为分类问题的损失函数？\n\n回归问题通常用均方差损失函数，可以保证损失函数是个凸函数，即可以得到最优解。而分类问题如果用均方差的话，损失函数的表现不是凸函数，就很难得到最优解。而交叉熵函数可以保证区间内单调。\n\n分类问题的最后一层网络，需要分类函数，`Sigmoid` 或者 `Softmax`，如果再接均方差函数的话，其求导结果复杂，运算量比较大。用交叉熵函数的话，可以得到比较简单的计算结果，一个简单的减法就可以得到反向误差。\n\n## 三，回归损失\n\n与分类问题不同，回归问题解决的是**对具体数值的预测**。解决回归问题的神经网络一般只有只有一个输出节点，这个节点的输出值就是预测值。\n\n回归问题的一个基本概念是**残差**或称为**预测误差**，用于衡量模型预测值与真实标记的靠近程度。假设回归问题中对应于第 $i$ 个输入特征 $x_i$ 的**标签**为 $y^i = (y_1,y_2,...,y_M)^{\\top}$，$M$ 为标记向量总维度，则 $l_{t}^{i}$ 即表示样本 $i$ 上神经网络的回归预测值 ($y^i$) 与其样本标签值在第 $t$ 维的预测误差(亦称残差):\n\n$$\nl_{t}^{i} = y_{t}^{i} - \\hat{y}_{t}^{i}\n$$\n\n常用的两种损失函数为 $\\text{MAE}$（也叫 `L1` 损失） 和 $\\text{MSE}$ 损失函数（也叫 `L2` 损失）。\n\n\n### 3.1，MAE 损失\n\n平均绝对误差（Mean Absolute Error，`MAE`）是用于回归模型的最简单但最强大的损失函数之一。\n\n因为存在离群值（与其余数据差异很大的值），所以回归问题可能具有本质上不是严格高斯分布的变量。 在这种情况下，平均绝对误差将是一个理想的选择，因为它没有考虑异常值的方向（不切实际的高正值或负值）。\n\n顾名思义，MAE 是**目标值和预测值之差的绝对值之和**。$n$ 是数据集中数据点的总数，其公式如下:\n$$\n\\text{MAE loss} = \\frac{1}{n}\\sum_{i=1}^{N}\\sum_{t=1}^{M} |y_{t}^{i} - \\hat{y}_{t}^{i}|\n$$\n\n### 3.2，MSE 损失\n\n均方误差（Mean Square Error, `MSE`）几乎是每个数据科学家在回归损失函数方面的偏好，这是因为**大多数变量都可以建模为高斯分布**。\n\n均方误差计算方法是求**预测值与真实值之间距离的平方和**。预测值和真实值越接近，两者的均方差就越小。公式如下:\n\n$$\n\\text{MSE loss} = \\frac{1}{n}\\sum_{i=1}^{N}\\sum_{t=1}^{M} (y_{t}^{i} - \\hat{y}_{t}^{i})^2\n$$\n\n### 3.3，`Huber` 损失\n\nMAE 和 MSE 损失之间的比较产生以下结果：\n\n1. **MAE 损失比 MSE 损失更稳健**。仔细查看公式，可以观察到如果预测值和实际值之间的差异很大，与 MAE 相比，MSE 损失会放大效果。 由于 MSE 会屈服于异常值，因此 MAE 损失函数是更稳健的损失函数。\n\n2. **MAE 损失不如 MSE 损失稳定**。由于 MAE 损失处理的是距离差异，因此一个小的水平变化都可能导致回归线波动很大。在多次迭代中发生的影响将导致迭代之间的斜率发生显著变化。总结就是，MSE 可以确保回归线轻微移动以对数据点进行小幅调整。\n3. **MAE 损失更新的梯度始终相同**。即使对于很小的损失值，梯度也很大。这样不利于模型的学习。为了解决这个缺陷，我们可以使用变化的学习率，在损失接近最小值时降低学习率。\n4. **MSE 损失的梯度随损失增大而增大，而损失趋于0时则会减小**。其使用固定的学习率也可以有效收敛。\n\nHuber Loss 结合了 MAE 的稳健性和 MSE 的稳定性，本质上是 MAE 和 MSE 损失中最好的。**对于大误差，它是线性的，对于小误差，它本质上是二次的**。\n\nHuber Loss 的特征在于参数 $\\delta$。当 $|y − \\hat{y}|$ 小于一个事先指定的值 $\\delta $ 时，变为平方损失，大于 $\\delta $ 时，则变成类似于绝对值损失，因此其是比较robust 的损失函数。其定义如下:\n\n$$\\text{Huber loss} = \\left \\lbrace \\begin{matrix}\n\\frac12[y_{t}^{i} - \\hat{y}_{t}^{i}]^2 & |y_{t}^{i} - \\hat{y}_{t}^{i}| \\leq \\delta \\\\ \n\\delta|y_{t}^{i} - \\hat{y}_{t}^{i}| - \\frac12\\delta^2 & |y_{t}^{i} - \\hat{y}_{t}^{i})| > \\delta\n\\end{matrix}\\right.$$\n\n三种回归损失函数的曲线图比较如下：\n\n![loss_for_regression](../../data/images/loss/loss_for_regression.png)\n> 代码来源 [Loss Function Plot.ipynb](https://nbviewer.org/github/massquantity/Loss-Functions/blob/master/Loss%20Function%20Plot.ipynb)。\n\n三种回归损失函数的其他形式定义如下:\n\n![three_regression_loss](../../data/images/activation_function/three_regression_loss.png)\n\n### 3.4，代码实现\n\n下面是三种回归损失函数的 python 代码实现，以及对应的 `sklearn` 库的内置函数。\n\n```python\n# true: Array of true target variable\n# pred: Array of predictions\ndef mse(true, pred):\n    return np.sum((true - pred)**2)\n\ndef mae(true, pred):\n    return np.sum(np.abs(true - pred))\n\ndef huber(true, pred, delta):\n    loss = np.where(np.abs(true-pred) < delta , 0.5*((true-pred)**2),delta*np.abs(true - pred) - 0.5*(delta**2))\n\n    return np.sum(loss)\n\n# also available in sklearn\nfrom sklearn.metrics import mean_squared_error\nfrom sklearn.metrics import mean_absolute_error\n```\n\n## 参考资料\n\n1. [《动手学深度学习-22.11. Information Theory》](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/information-theory.html#cross-entropy)\n2. [损失函数｜交叉熵损失函数](https://zhuanlan.zhihu.com/p/35709485)\n3. [AI-EDU: 交叉熵损失函数](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC1%E6%AD%A5%20-%20%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/03.2-%E4%BA%A4%E5%8F%89%E7%86%B5%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0.html)\n4. [常见回归和分类损失函数比较](https://www.cnblogs.com/massquantity/p/8964029.html)\n5. 《PyTorch_tutorial_0.0.5_余霆嵩》\n6. https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html\n7. [一文详解Softmax函数](https://zhuanlan.zhihu.com/p/105722023)\n8. [AI-EDU: 多分类函数](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/en-us/Step3%20-%20LinearClassification/07.1-%E5%A4%9A%E5%88%86%E7%B1%BB%E5%87%BD%E6%95%B0.html#711-softmax)\n"
  },
  {
    "path": "4-deep_learning/深度学习基础/神经网络基础部件-BN层详解.md",
    "content": "- [一，数学基础](#一数学基础)\n  - [1.1，概率密度函数](#11概率密度函数)\n  - [1.2，正态分布](#12正态分布)\n- [二，背景](#二背景)\n  - [2.1，如何理解 Internal Covariate Shift](#21如何理解-internal-covariate-shift)\n  - [2.2，Internal Covariate Shift 带来的问题](#22internal-covariate-shift-带来的问题)\n  - [2.3，减少 Internal Covariate Shift 的一些尝试](#23减少-internal-covariate-shift-的一些尝试)\n- [三，批量归一化（BN）](#三批量归一化bn)\n  - [3.1，BN 的前向计算](#31bn-的前向计算)\n  - [3.2，BN 层如何工作](#32bn-层如何工作)\n  - [3.3，推理时的 BN 层](#33推理时的-bn-层)\n  - [3.4，实验](#34实验)\n  - [3.5，BN 层的作用](#35bn-层的作用)\n- [参考资料](#参考资料)\n\n## 一，数学基础\n\n### 1.1，概率密度函数\n\n随机变量（random variable）是可以随机地取不同值的变量。随机变量可以是离散的或者连续的。简单起见，本文用大写字母 $X$ 表示随机变量，小写字母 $x$ 表示随机变量能够取到的值。例如，$x_1$ 和 $x_2$ 都是随机变量 $X$ 可能的取值。随机变量必须伴随着一个概率分布来指定每个状态的可能性。\n\n概率分布（probability distribution）用来描述随机变量或一簇随机变量在每一个可能取到的状态的可能性大小。我们描述概率分布的方式取决于随机变量是离散的还是连续的。\n\n当我们研究的对象是连续型随机变量时，我们用**概率密度函数**（probability density function, `PDF`）而不是概率质量函数来描述它的概率分布。\n\n> 更多内容请阅读《花书》第三章-概率与信息论，或者我的文章-[深度学习数学基础-概率与信息论](https://github.com/HarleysZhang/deep_learning_alchemy/blob/main/1-math_ml_basic/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E6%95%B0%E5%AD%A6%E5%9F%BA%E7%A1%80-%E6%A6%82%E7%8E%87%E4%B8%8E%E4%BF%A1%E6%81%AF%E8%AE%BA.md)。\n\n### 1.2，正态分布\n\n> 当我们不知道数据真实分布时使用正态分布的原因之一是，正态分布拥有最大的熵，我们通过这个假设来施加尽可能少的结构。\n\n实数上最常用的分布就是正态分布(normal distribution)，也称为高斯分布 (Gaussian distribution)。\n\n如果随机变量 $X$ ，服从位置参数为 $\\mu$、尺度参数为 $\\sigma$ 的概率分布，且其概率密度函数为:\n\n$$\nf(x)=\\frac{1}{\\sigma\\sqrt{2 \\pi} } e^{- \\frac{{(x-\\mu)^2}}{2\\sigma^2}} \\tag{1}\n$$\n则这个随机变量就称为正态随机变量，正态随机变量服从的概率分布就称**为正态分布**，记作：\n$$\nX \\sim N(\\mu,\\sigma^2) \\tag{2}\n$$\n如果位置参数 $\\mu = 0$，尺度参数 $\\sigma = 1$ 时，则称为标准正态分布，记作：\n$$\nX \\sim N(0, 1) \\tag{3}\n$$\n此时，概率密度函数公式简化为:\n$$\nf(x)=\\frac{1}{\\sqrt{2 \\pi}} e^{- \\frac{x^2}{2}} \\tag{4}\n$$\n正态分布的数学期望值或期望值 $\\mu$ 等于位置参数，决定了分布的位置；其方差 $\\sigma^2$ 的开平方或标准差 $\\sigma$ 等于尺度参数，决定了分布的幅度。正态分布的概率密度函数曲线呈钟形，常称之为**钟形曲线**，如下图所示:\n\n![正态分布概率密度函数曲线](../../data/images/bn/normal-distribution-curve-1.png)\n\n可视化正态分布，可直接通过 `np.random.normal` 函数生成指定均值和标准差的正态分布随机数，然后基于 `matplotlib + seaborn` 库 `kdeplot`函数绘制概率密度曲线。示例代码如下所示：\n\n```py\nimport seaborn as sns\nx1 = np.random.normal(0, 1, 100)\nx2 = np.random.normal(0, 1.5, 100) \nx3 = np.random.normal(2, 1.5, 100) \n\nplt.figure(dpi = 200)\n\nsns.kdeplot(x1, label=\"μ=0, σ=1\")\nsns.kdeplot(x2, label=\"μ=0, σ=1.5\")\nsns.kdeplot(x3, label=\"μ=2, σ=2.5\")\n\n#显示图例\nplt.legend()\n#添加标题\nplt.title(\"Normal distribution\")\nplt.show()\n```\n\n以上代码直接运行后，输出结果如下图：\n\n![不同参数的正态分布函数曲线](../../data/images/bn/normal_distribution_curve.png)\n\n当然也可以自己实现正态分布的概率密度函数，代码和程序输出结果如下:\n\n```python\nimport numpy as np\nimport matplotlib.pyplot as plt\nplt.figure(dpi = 200)\nplt.style.use('seaborn-darkgrid') # 主题设置\n\ndef nd_func(x, sigma, mu):\n  \t\"\"\"自定义实现正态分布的概率密度函数\n  \t\"\"\"\n    a = - (x-mu)**2 / (2*sigma*sigma)\n    f = np.exp(a) / (sigma * np.sqrt(2*np.pi))\n    return f\n\nif __name__ == '__main__':\n    x = np.linspace(-5, 5)\n    f = nd_fun(x, 1, 0)\n    p1, = plt.plot(x, f)\n\n    f = nd_fun(x, 1.5, 0)\n    p2, = plt.plot(x, f)\n\n    f = nd_fun(x, 1.5, 2)\n    p3, = plt.plot(x, f)\n\n    plt.legend([p1 ,p2, p3], [\"μ=0,σ=1\", \"μ=0,σ=1.5\", \"μ=2,σ=1.5\"])\n    plt.show()\n```\n\n![自己实现的不同参数的正态分布函数曲线](../../data/images/bn/normal_distribution_curve2.png)\n\n## 二，背景\n\n训练深度神经网络的复杂性在于，因为前面的层的参数会发生变化导致每层输入的分布在训练过程中会发生变化。这又导致模型需要需要较低的学习率和非常谨慎的参数初始化策略，从而减慢了训练速度，并且具有饱和非线性的模型训练起来也非常困难。\n\n网络层输入数据分布发生变化的这种现象称为**内部协变量转移**，BN 就是来解决这个问题。\n\n### 2.1，如何理解 Internal Covariate Shift\n\n在深度神经网络训练的过程中，由于网络中参数变化而引起网络中间层数据分布发生变化的这一过程被称在论文中称之为**内部协变量偏移**（Internal Covariate Shift）。\n\n那么，为什么网络中间层数据分布会发生变化呢？\n\n在深度神经网络中，我们可以将每一层视为对输入的信号做了一次变换（暂时不考虑激活，因为激活函数不会改变输入数据的分布）：\n$$\nZ = W \\cdot X + B \\tag{5}\n$$\n其中 $W$ 和 $B$ 是模型学习的参数，这个公式涵盖了全连接层和卷积层。\n\n随着 SGD 算法更新参数，和网络的每一层的输入数据经过公式5的运算后，其 $Z$ 的**分布一直在变化**，因此网络的每一层都需要不断适应新的分布，这一过程就被叫做 Internal Covariate Shift。\n\n而深度神经网络训练的复杂性在于每层的输入受到前面所有层的参数的影响—因此当网络变得更深时，网络参数的微小变化就会被放大。\n\n### 2.2，Internal Covariate Shift 带来的问题\n\n1. 网络层需要不断适应新的分布，**导致网络学习速度的降低**。\n\n2. 网络层输入数据容易陷入到非线性的饱和状态并**减慢网络收敛**，这个影响随着网络深度的增加而放大。\n\n   随着网络层的加深，后面网络输入 $x$ 越来越大，而如果我们又采用 `Sigmoid` 型激活函数，那么每层的输入很容易移动到非线性饱和区域，此时梯度会变得很小甚至接近于 $0$，导致参数的更新速度就会减慢，进而又会放慢网络的收敛速度。\n\n饱和问题和由此产生的梯度消失通常通过使用修正线性单元激活（$ReLU(x)=max(x,0)$），更好的参数初始化方法和小的学习率来解决。然而，如果我们能保证非线性输入的分布在网络训练时保持更稳定，那么优化器将不太可能陷入饱和状态，进而训练也将加速。\n\n### 2.3，减少 Internal Covariate Shift 的一些尝试\n\n1. **白化（Whitening）**: 即输入线性变换为具有零均值和单位方差，并去相关。\n\n   **白化过程由于改变了网络每一层的分布**，因而改变了网络层中本身数据的表达能力。底层网络学习到的参数信息会被白化操作丢失掉，而且白化计算成本也高。\n\n2. **标准化（normalization）**\n\n   Normalization 操作虽然缓解了 `ICS` 问题，让每一层网络的输入数据分布都变得稳定，但却导致了数据表达能力的缺失。\n\n## 三，批量归一化（BN）\n\n### 3.1，BN 的前向计算\n\n论文中给出的 Batch Normalizing Transform 算法计算过程如下图所示。其中输入是一个考虑一个大小为 $m$ 的小批量数据 $\\cal B$。\n\n<img src=\"../../data/images/bn/bn_algorithm.png\" alt=\"Batch Normalizing Transform\" style=\"zoom:50%;\" />\n\n论文中的公式不太清晰，下面我给出更为清晰的  Batch Normalizing Transform 算法计算过程。\n\n设 $m$ 表示 batch_size 的大小，$n$ 表示 features 数量，即样本特征值数量。在训练过程中，针对每一个 `batch` 数据，`BN` 过程进行的操作是，将这组数据 `normalization`，之后对其进行线性变换，具体算法步骤如下:\n\n$$\\begin{aligned} \n\\mu_B &= \\frac{1}{m}\\sum_1^m x_i \\\\\n\\sigma^2_B &= \\frac{1}{m} \\sum_1^m (x_i-\\mu_B)^2 \\\\\nn_i &= \\frac{x_i-\\mu_B}{\\sqrt{\\sigma^2_B + \\epsilon}} \\\\\nz_i &= \\gamma n_i + \\beta = \\frac{\\gamma}{\\sqrt{\\sigma^2_B + \\epsilon}}x_i + (\\beta - \\frac{\\gamma\\mu_{B}}{\\sqrt{\\sigma^2_B + \\epsilon}})\n\\end{aligned}$$\n\n以上公式乘法都为元素乘，即 `element wise` 的乘法。其中，参数 $\\gamma,\\beta$ 是训练出来的， $\\epsilon$ 是为零防止 $\\sigma_B^2$ 为 $0$ ，加的一个很小的数值，通常为`1e-5`。公式各个符号解释如下:\n\n|     符号     |       数据类型       | 数据形状 |\n| :----------: | :------------------: | :------: |\n|     $X$      |     输入数据矩阵     |  [m, n]  |\n|    $x_i$     |  输入数据第i个样本   |  [1, n]  |\n|     $N$      | 经过归一化的数据矩阵 |  [m, n]  |\n|    $n_i$     |  经过归一化的单样本  |  [1, n]  |\n|   $\\mu_B$    |      批数据均值      |  [1, n]  |\n| $\\sigma^2_B$ |      批数据方差      |  [1, n]  |\n|     $m$      |      批样本数量      |   [1]    |\n|   $\\gamma$   |     线性变换参数     |  [1, n]  |\n|   $\\beta$    |     线性变换参数     |  [1, n]  |\n|     $Z$      |   线性变换后的矩阵   |  [1, n]  |\n|    $z_i$     |  线性变换后的单样本  |  [1, n]  |\n|   $\\delta$   |    反向传入的误差    |  [m, n]  |\n\n### 3.2，BN 层如何工作\n\n在论文中，训练一个带 `BN` 层的网络， `BN` 算法步骤如下图所示:\n\n<img src=\"../../data/images/bn/algorithm2.png\" alt=\"Training a Batch-Normalized Network\" style=\"zoom:50%;\" />\n\n在训练期间，我们一次向网络提供一小批数据。在前向传播过程中，网络的每一层都处理该小批量数据。 `BN` 网络层按如下方式执行前向传播计算：\n\n![Batch Norm 层执行的前向计算](../../data/images/bn/bn_fp.png)\n\n> 图片来源[这里](https://towardsdatascience.com/batch-norm-explained-visually-how-it-works-and-why-neural-networks-need-it-b18919692739)。\n\n注意，图中计算模型推理时 BN 层均值与方差的无偏估计方法，是吴恩达在 Coursera 上的 Deep Learning 课程上提出的方法：对 train 阶段每个 batch 计算的 mean/variance 采用**指数加权平均**来得到 test 阶段 mean/variance 的估计。\n\n在训练期间，它只是计算此 EMA，但不对其执行任何操作。在训练结束时，它只是将该值保存为层状态的一部分，以供在推理阶段使用。\n\n如下图可以展示BN 层的前向传播计算过程数据的 `shape` ，红色框出来的单个样本都指代单个矩阵，即运算都是在单个矩阵运算中计算的。\n\n![Batch Norm 向量的形状](../../data/images/bn/bn_fp_shape.png)\n\n> 图片来源 [这里](https://towardsdatascience.com/batch-norm-explained-visually-how-it-works-and-why-neural-networks-need-it-b18919692739)。\n\nBN 的反向传播过程中，会更新 BN 层中的所有 $\\beta$ 和 $\\gamma$ 参数。\n\n### 3.3，推理时的 BN 层\n\n批量归一化（batch normalization）的“批量”两个字，表示在模型的迭代训练过程中，BN 首先计算小批量（ mini-batch，如 32）的均值和方差。但是，在推理过程中，我们只有一个样本，而不是一个小批量。即在模型训练的时候，均值 $\\mu$、方差 $\\sigma^2$、$\\gamma$、$\\beta$ 是一直在更新的，但是，在推理的时候，这四个值都是固定了的。\n\n虽然 $\\gamma$、$\\beta$  参数可通过模型训练后得到，但是，有一个问题，模型推理时 BN 层的均值和方差应该如何获得呢？\n\n第一种方法是，使用的均值和方差数据是在训练过程中样本值的平均，即：\n\n$$\\begin{align}\nE[x] &= E[\\mu_B] \\nonumber \\\\\nVar[x] &= \\frac{m}{m-1} E[\\sigma^2_B] \\nonumber \\\\\n\\end{align}$$\n\n这种做法会把所有训练批次的 $\\mu$ 和 $\\sigma$ 都保存下来，然后在最后训练完成时（或做测试时）做下平均。\n\n第二种方法是使用类似动量的方法，训练时，加权平均每个批次的值，权值 $\\alpha$ 可以为0.9：\n\n$$\\begin{aligned}\n\\mu_{mov_{i}} &= \\alpha \\cdot \\mu_{mov_{i}} + (1-\\alpha) \\cdot \\mu_i \\nonumber \\\\\n\\sigma_{mov_{i}} &= \\alpha \\cdot \\sigma_{mov_{i}} + (1-\\alpha) \\cdot \\sigma_i \\nonumber \\\\\n\\end{aligned}$$\n\n模型推理或测试时，直接使用模型文件中保存的 $\\mu_{mov_{i}}$ 和 $\\sigma_{mov_{i}}$ 的值即可。\n\n同时，上面 `BN` 的计算公式（算法 1）可以变形为：\n$$\nz_i = \\gamma n_i + \\beta = \\frac{\\gamma}{\\sqrt{\\sigma^2_B + \\epsilon}}x_i + (\\beta - \\frac{\\gamma\\mu_{B}}{\\sqrt{\\sigma^2_B + \\epsilon}}) = ax_i + b\\nonumber\n$$\n因为推理时，均值 $\\mu$、方差 $\\sigma^2$、$\\gamma$、$\\beta$ 值固定，因此可以看出推理时 `BN` 本质上是做线性变换，且 $a$、$b$ 都是常数。\n\n### 3.4，实验\n\n`BN` 在 `ImageNet` 分类数据集上实验结果是 `SOTA` 的，如下表所示:\n\n![实验结果表4](../../data/images/bn/Figure_4.png)\n\n### 3.5，BN 层的作用\n\n通过对神经网络中每一层的输入数据进行归一化处理，**使得每一层的输入分布更加稳定**，从而降低了神经网络过拟合的风险。具体而言，BN 层的作用包括：\n\n1. **BN 使得网络中每层输入数据的分布相对稳定，加速模型训练和收敛速度**。\n2. **批标准化可以提高学习率**。在传统的深度网络中，学习率过高可能会导致梯度爆炸或梯度消失，以及陷入差的局部最小值。批标准化有助于解决这些问题。通过标准化整个网络的激活值，它可以防止层参数的微小变化随着数据在深度网络中的传播而放大。例如，这使 sigmoid 非线性更容易保持在它们的非饱和状态，这对训练深度 sigmoid 网络至关重要，但在传统上很难实现。\n3. **BN 允许网络使用饱和非线性激活函数（如 sigmoid，tanh 等）进行训练，其能缓解梯度消失问题**。如果不使用 BN，由于深度神经网络的“累积”效应，使得低层网络的变化效应会累加到高层网络，导致模型训练过程很容易进入到 sigmoid 激活函数的梯度饱和区；而经过 BN 层操作后可以让激活函数的输入数据落在梯度非饱和区，缓解梯度消失的问题。（bn 层一般在激活函数层前面）\n4. BN 层能够**减少过拟合提高模型的泛化能力**，不再需要 `dropout` 和 `LRN`（Local Response Normalization）层来实现正则化。BN 层将每个特征在当前的 mini-batch 中进行标准化，其中包括均值和方差的估计。由于mini-batch的选择是随机的，所以在每个 mini-batch 中使用的均值和方差的估计也是随机的，这样就引入了一定的噪声。\n5. **减少对参数初始化方法的依赖**。早期的权重是随机初始化的，异常权重值会扭曲梯度，导致网络收敛需要更长的时间，而 `Batch Norm` 有助于抑制这些权重异常值的影响。\n\nSigmoid 导数的**梯度消失区域**如下图所示：\n\n![sigmoid 函数及其导数图像](../../data/images/activation_function/sigmoid_and_gradient_curve2.png)\n\n## 参考资料\n\n1. [Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift](https://arxiv.org/abs/1502.03167)\n2. [维基百科-正态分布](https://zh.wikipedia.org/zh-hans/%E6%AD%A3%E6%80%81%E5%88%86%E5%B8%83)\n3. [Batch Norm Explained Visually — How it works, and why neural networks need it](https://towardsdatascience.com/batch-norm-explained-visually-how-it-works-and-why-neural-networks-need-it-b18919692739)\n4. [15.5 批量归一化的原理](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC7%E6%AD%A5%20-%20%E6%B7%B1%E5%BA%A6%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/15.5-%E6%89%B9%E9%87%8F%E5%BD%92%E4%B8%80%E5%8C%96%E7%9A%84%E5%8E%9F%E7%90%86.html)\n5. [Batch Normalization原理与实战](https://zhuanlan.zhihu.com/p/34879333)"
  },
  {
    "path": "4-deep_learning/深度学习基础/神经网络基础部件-卷积层详解.md",
    "content": "- [前言](#前言)\n- [一，卷积](#一卷积)\n  - [1.1，卷积运算定义](#11卷积运算定义)\n  - [1.2，卷积的意义](#12卷积的意义)\n  - [1.3，从实例理解卷积](#13从实例理解卷积)\n  - [1.4，图像卷积（二维卷积）](#14图像卷积二维卷积)\n  - [1.5，互相关和卷积](#15互相关和卷积)\n- [二，卷积层](#二卷积层)\n  - [2.1，卷积层定义](#21卷积层定义)\n    - [2.1.1，局部连接](#211局部连接)\n    - [2.1.2，权重共享](#212权重共享)\n  - [2.2，卷积层理解](#22卷积层理解)\n  - [2.3，分组卷积和DW卷积](#23分组卷积和dw卷积)\n  - [2.4，卷积层 api](#24卷积层-api)\n- [三，卷积神经网络](#三卷积神经网络)\n  - [3.1，汇聚层](#31汇聚层)\n  - [3.2.，汇聚层 api](#32汇聚层-api)\n- [四，卷积神经网络结构](#四卷积神经网络结构)\n- [参考资料](#参考资料)\n\n## 前言\n\n在全连接层构成的多层感知机网络中，我们通过将图像数据展平成一维向量来送入模型，这样会忽略了每个图像的**空间结构信息**。理想的策略应该是要利用相近像素之间的相互关联性，将图像数据二维矩阵送给模型中学习。\n\n卷积神经网络(convolutional neural network，`CNN`)正是一类强大的、专为处理图像数据（多维矩阵）而设计的神经网络。在 `Transformer` 应用到 `CV` 领域之前，基于卷积神经网络架构的模型在计算机视觉领域中占主导地位，几乎所有的图像识别、目标检测、语义分割、3D目标检测、视频理解等任务都是以 `CNN` 方法为基础。\n\n卷积神经网络核心网络层是卷积层，其使用了卷积(convolution)这种数学运算，卷积是一种特殊的线性运算。另外，通常来说，卷积神经网络中用到的卷积运算和其他领域(例如工程领域以及纯数学领域)中的定义并不完全一致。\n\n## 一，卷积\n\n在理解卷积层之前，我们首先得理解什么是卷积操作。\n\n卷积与[傅里叶变换](https://zh.wikipedia.org/wiki/傅里叶变换)有着密切的关系。例如两函数的傅里叶变换的乘积等于它们卷积后的傅里叶变换，利用此一性质，能简化傅里叶分析中的许多问题。\n\n> operation 视语境有时译作“操作”，有时译作“运算”，本文不做区分。\n\n### 1.1，卷积运算定义\n\n为了给出卷积的定义， 这里从现实世界会用到函数的例子出发。\n\n假设我们正在用激光传感器追踪一艘宇宙飞船的位置。我们的激光传感器给出 一个单独的输出 $x(t)$，表示宇宙飞船在时刻 $t$ 的位置。$x$ 和  $t$ 都是**实值**的，这意味着我们可以在任意时刻从传感器中读出飞船的位置。\n\n现在假设我们的传感器受到一定程度的噪声干扰。为了得到飞船位置的低噪声估计，我们对得到的测量结果进行平均。显然，时间上越近的测量结果越相关，所 以我们采用一种**加权平均**的方法，对于最近的测量结果赋予更高的权重。我们可以采用一个加权函数 $w(a)$ 来实现，其中 $a$ 表示测量结果距当前时刻的时间间隔。如果我们对任意时刻都采用这种加权平均的操作，就得到了一个新的对于飞船位置的平滑估计函数 $s$:\n\n$$s(t) = \\int x(a)w(t-a )da$$\n\n这种运算就叫做卷积（`convolution`）。更一般的，卷积运算的数学公式定义如下：\n$$\n连续定义: \\; h(x)=(f*g)(x) = \\int_{-\\infty}^{\\infty} f(t)g(x-t)dt \\tag{1}\n$$\n\n$$\n离散定义: \\; h(x) = (f*g)(x) = \\sum^{\\infty}_{t=-\\infty} f(t)g(x-t) \\tag{2}\n$$\n\n以上卷积计算公式可以这样理解:\n\n1. 先对函数 $g(t)$ 进行反转（`reverse`），相当于在数轴上把 $g(t)$ 函数从右边褶到左边去，也就是卷积的“卷”的由来。\n2. 然后再把 $g(t)$ 函数向左平移 $x$ 个单位，在这个位置对两个函数的对应点相乘，然后相加，这个过程是卷积的“积”的过程。\n\n### 1.2，卷积的意义\n\n对卷积这个名词，可以这样理解：**所谓两个函数的卷积（$f*g$），本质上就是先将一个函数翻转，然后进行滑动叠加**。在连续情况下，叠加指的是对两个函数的乘积求积分，在离散情况下就是加权求和，为简单起见就统一称为叠加。\n\n因此，卷积运算整体来看就是这么一个过程:\n\n翻转—>滑动—>叠加—>滑动—>叠加—>滑动—>叠加.....\n\n多次滑动得到的一系列叠加值，构成了卷积函数。\n\n> 这里多次滑动过程对应的是 $t$ 的变化过程。\n\n那么，**卷积的意义是什么呢**？可以从卷积的典型应用场景-图像处理来理解：\n\n1. 为什么要进行“卷”？进行“卷”（翻转）的目的其实是施加一种约束，它指定了在“积”的时候以什么为参照。在空间分析的场景，它指定了在哪个位置的周边进行累积处理。\n2. 在图像处理的中，卷积处理的结果，其实就是把每个像素周边的，甚至是整个图像的像素都考虑进来，对当前像素进行某种加权处理。因此，“积”是全局概念，或者说是一种“混合”，把两个函数进行时间（信号分析）或空间（图像处理）上进行混合。\n\n> 卷积意义的理解来自[知乎问答](https://www.zhihu.com/question/22298352)，有所删减和优化。\n\n### 1.3，从实例理解卷积\n\n一维卷积的实例有 “丢骰子” 等经典实例，这里不做展开描述，本文从二维卷积用于图像处理的实例来理解。\n\n一般，数字图像可以表示为如下所示矩阵：\n> 本图片摘自[知乎用户马同学的文章](https://www.zhihu.com/question/22298352)。\n\n![图像矩阵](../../data/images/conv/image1.png)\n\n而卷积核 $g$ 也可以用一个矩阵来表示，如:\n\n$$\ng = \\begin{bmatrix} \n&b_{-1,-1} &b_{-1,0} &b_{-1,1} \\\\ \n&b_{0,-1} &b_{0,0} &b_{0,1} \\\\ \n&b_{1,-1} &b_{1,0} &b_{1,1}\n\\end{bmatrix}\n$$\n\n按照卷积公式的定义，则目标图片的第 $(u, v)$ 个像素的二维卷积值为：\n\n$$\n(f * g)(u, v)=\\sum_{i} \\sum_{j} f(i, j)g(u-i, v-j)=\\sum_{i} \\sum_{j} a_{i,j} b_{u-i,v-j}\n$$\n展开来分析二维卷积计算过程就是，首先得到原始图像矩阵中 $(u, v)$ 处的矩阵：\n\n$$\nf=\\begin{bmatrix} \n&a_{u-1,v-1} &a_{u-1,v} &a_{u-1,v+1}\\\\ \n&a_{u,v-1} &a_{u,v} &a_{u,v+1} \\\\ \n&a_{u+1,v-1} &a_{u+1,v} &a_{u+1,v+1}\n\\end{bmatrix}\n$$\n\n然后将图像处理矩阵**翻转**（两种方法，结果等效），如先沿 $x$ 轴翻转，再沿 $y$ 轴翻转（**相当于将矩阵 $g$ 旋转 180 度**）：\n\n$$\n\\begin{aligned}\ng &= \\begin{bmatrix} &b_{-1,-1} &b_{-1,0} &b_{-1,1}\\\\ &b_{0,-1} &b_{0,0} &b_{0,1} \\\\ &b_{1,-1} &b_{1,0} &b_{1,1} \\end{bmatrix}\n=> \\begin{bmatrix} &b_{1,-1} &b_{1,0} &b_{1,1}\\\\ &b_{0,-1} &b_{0,0} &b_{0,1} \\\\ &b_{-1,-1} &b_{-1,0} &b_{-1,1} \\end{bmatrix} \\\\\n&= \\begin{bmatrix} &b_{1,1} &b_{1,0} &b_{1,-1}\\\\ &b_{0,1} &b_{0,0} &b_{0,-1} \\\\ &b_{-1,1} &b_{-1,0} &b_{-1,-1} \\end{bmatrix} = g^{'}\n\\end{aligned}\n$$\n\n最后，计算卷积时，就可以用 $f$ 和 $g′$ 的内积：\n\n$$\n\\begin{aligned}\nf * g(u,v) &= a_{u-1,v-1} \\times b_{1,1} + a_{u-1,v} \\times b_{1,0} + a_{u-1,v+1} \\times b_{1,-1} \\\\ \n&+ a_{u,v-1} \\times b_{0,1} + a_{u,v} \\times b_{0,0} + a_{u,v+1} \\times b_{0,-1} \\\\ \n&+ a_{u+1,v-1} \\times b_{-1,1} + a_{u+1,v} \\times b_{-1,0} + a_{u+1,v+1} \\times b_{-1,-1}\n\\end{aligned}\n$$\n\n计算过程可视化如下动图所示，注意动图给出的是 $g$ 不是 $g'$。\n\n![二维卷积计算过程](../../data/images/conv/conv_visual.gif)\n\n以上公式有一个特点，做乘法的两个对应变量 $a, b$ 的下标之和都是 $(u,v)$，其目的是对这种加权求和进行一种约束，这也是要将矩阵 $g$ 进行翻转的原因。上述计算比较麻烦，实际计算的时候，都是用翻转以后的矩阵，直接求**矩阵内积**就可以了。\n\n### 1.4，图像卷积（二维卷积）\n\n在机器学习和图像处理领域，卷积的主要功能是**在一个图像(或某种特征) 上滑动一个卷积核(即滤波器)，通过卷积操作得到一组新的特征**。一幅图像在经过卷积操作后得到结果称为特征映射(`Feature Map`)。如果把图像矩阵简写为 $I$，把卷积核 `Kernal` 简写为 $K$，则目标图片的第 $(i,j)$ 个像素的卷积值为：\n\n$$\nh(i,j) = (I*K)(i,j)=\\sum_m \\sum_n I(m,n)K(i-m,j-n) \\tag{3}\n$$\n\n可以看出，这和一维情况下的卷积公式 2 是一致的。因为卷积的可交换性，我们也可以把公式 3 等价地写作：\n\n$$\nh(i,j) = (I*K)(i,j)=\\sum_m \\sum_n I(i-m,j-n)K(m,n) \\tag{4}\n$$\n\n通常，下面的公式在机器学习库中实现更为简单，因为 $m$ 和 $n$ 的有效取值范围相对较小。\n\n卷积运算可交换性的出现是因为我们将核相对输入进行了翻转（`flip`），从 $m$ 增 大的角度来看，输入的索引在增大，但是卷积核的索引在减小。我们将**卷积核翻转的唯一目 的是实现可交换性**。尽管可交换性在证明时很有用，但在神经网络的应用中却不是一个重要的性质。相反，许多神经网络库会实现一个**互相关函数**（`corresponding function`），它与卷积相同但没有翻转核：\n\n$$\nh(i,j) = (I*K)(i,j)=\\sum_m \\sum_n I(i+m,j+n)K(m,n) \\tag{5}\n$$\n\n互相关函数的运算，是两个序列滑动相乘，两个序列都不翻转。卷积运算也是滑动相乘，但是其中一个序列需要先翻转，再相乘。\n\n### 1.5，互相关和卷积\n\n互相关和卷积运算的关系，可以通过下述公式理解：\n\n$$\n\\begin{aligned}Y \n&= W\\otimes X \\\\\n&= \\text{rot180}(W) * X \\\\\n\\end{aligned}\n$$\n\n其中 $\\otimes$ 表示互相关运算，$*$ 表示卷积运算，$\\text{rot180(⋅)}$ 表示旋转 `180` 度，$Y$ 为输出矩阵。从上式可以看出，互相关和卷积的区别仅仅在于卷积核是否进行翻转。因此互相关也可以称为不翻转卷积.\n> 离散卷积可以看作矩阵的乘法，然而，这个矩阵的一些元素被限制为必须和另外一些元素相等。\n\n在神经网络中使用卷积是为了进行特征抽取，**卷积核是否进行翻转和其特征抽取的能力无关**（特别是当卷积核是可学习的参数时），**因此卷积和互相关在能力上是等价的**。事实上，很多深度学习工具中卷积操作其实都是互相关操作，用来**减少一些不必要的操作或开销（不反转 Kernal）**。\n\n总的来说，\n1. 我们实现的卷积操作不是原始数学含义的卷积，而是工程上的卷积，但一般也简称为卷积。\n2. 在实现卷积操作时，并不会反转卷积核。\n\n## 二，卷积层\n\n> 在传统图像处理中，**线性空间滤波**的原理实质上是指指图像 $f$ 与滤波器核 $w$ 进行乘积之和（**卷积**）运算。核是一个矩阵，其大小定义了运算的邻域，其系数决定了该滤波器（也称模板、窗口滤波器）的性质，并通过设计不同核系数（卷积核）来实现低通滤波（平滑）和高通滤波（锐化）功能，因此我们可以认为卷积是利用某些设计好的参数组合（卷积核）去提取图像空域上相邻的信息。\n\n### 2.1，卷积层定义\n\n在全连接前馈神经网络中，如果第 $l$ 层有 $M_l$ 个神经元，第 $l-1$ 层有 $M_{l-1}$  个 神经元，连接边有 $M_{l}\\times M_{l-1}$ 个，也就是权重矩阵有 $M_{l}\\times M_{l-1}$  个参数。当 $M_l$ 和 $M_{l-1}$ 都很大时，权重矩阵的参数就会非常多，训练的效率也会非常低。\n\n如果采用卷积来代替全连接，第 $l$ 层的净输入 $z^{(l)}$ 为第 $l-1$ 层激活值 $a^{(l−1)}$ 和滤波器 $w^{(l)}\\in \\mathbb{R}^K$ 的卷积，即\n$$\nz^{(l)} = w^{(l)}\\otimes a^{(l−1)} + b^{(l)}\n$$\n其中 $b^{(l)}\\in \\mathbb{R}$ 为可学习的偏置。\n\n> 上述卷积层公式也可以写成这样的形式：$Z = W*A+b$\n\n根据卷积层的定义，卷积层有两个很重要的性质: 局部连接和权重共享。\n\n#### 2.1.1，局部连接\n\n**局部连接**：在卷积层(假设是第 $l$ 层)中的每一个神经元都只和下一层(第 $l − 1$ 层)中某个局部窗口内的神经元相连，构成一个局部连接网络。其实可能表达为**稀疏交互**更直观点，传统的网络层是全连接的，使用矩阵乘法来建立输入与输出的连接关系。矩阵的每个参数都是独立的，它描述了每个输入单元与输出单元的交互。这意味着每个输出单元与所有的输入单元都产生关联。而卷积层通过使用**卷积核矩阵**来实现稀疏交互（也称作稀疏连接，或者稀疏权重），每个输出单元仅仅与特定的输入神经元（其实是指定通道的 `feature map`）产生关联。\n\n下图显示了全连接层和卷积层的每个输入单元影响的输出单元比较:\n\n![全连接层和卷积层对比](../../data/images/conv/ful_conv2.png)\n\n- 对于传统全连接层，每个输入单元影响了所有的输出单元。\n- 对于卷积层，每个输入单元只影响了3个输出单元（核尺寸为3时）。\n\n#### 2.1.2，权重共享\n\n**权重共享**：卷积层中，同一个核会在输入的不同区域做卷积运算。全连接层和卷积层的权重参数比较如下图:\n\n![全连接层和卷积层对比](../../data/images/conv/ful_conv3.png)\n\n- 对于传统全连接层: $x_3\\to s_3$ 的权重 $w_{3,3}$ 只使用了一次 。\n- 对于卷积层： $x_3\\to s_3$ 的权重 $w_{3,3}$ 被共享到  $x_i \\to s_i, i = 12,4,5$。\n\n全连接层和卷积层的可视化对比如下图所示:\n\n![全连接层和卷积层对比](../../data/images/conv/ful_conv_compare.png)\n\n总结：一个滤波器（3维卷积核）只捕捉输入数据中的一种特定的局部特征。为了提高卷积网络的表示能力，可以在每一层使用多个不同的特征映射，即增加滤波器的数量，以更好地提取图像的特征。\n\n### 2.2，卷积层理解\n\n前面章节内容中，卷积的输出形状只取决于输入形状和卷积核的形状。而神经网络中的卷积层，在卷积的标准定义基础上，还引入了卷积核的滑动步长和零填充来增加卷积的多样性，从而可以更灵活地进行特征抽取。\n\n- 步长(Stride)：指卷积核每次滑动的距离\n- 零填充(Zero Padding)：在输入图像的边界填充元素(通常填充元素是0)\n\n卷积层定义：每个卷积核（`Kernel`）在输入矩阵上滑动，并通过下述过程实现卷积计算:\n\n1. 在来自卷积核的元素和输入特征图子矩阵的元素之间进行乘法以获得输出感受野。 \n2. 然后将相乘的值与添加的偏差相加以获得输出矩阵中的值。 \n\n卷积层数值计算过程可视化如下图  1 所示：\n\n![Example of Convolutional layer](../../data/images/conv/Example-of-Convolutional-layer.png)\n\n> 图片来源论文 [Improvement of Damage Segmentation Based on Pixel-Level Data Balance Using VGG-Unet](https://www.researchgate.net/figure/Example-of-Convolutional-layer_fig2_348296106)。\n\n注意，卷积层的输出 `Feature map` 的大小取决于输入的大小、Pad 数、卷积核大小和步长。在 `Pytorch` 框架中，图片（`feature map`）经卷积 `Conv2D` 后**输出大小计算公式**如下：$\\left \\lfloor N = \\frac{W-F+2P}{S}+1 \\right \\rfloor$。\n\n其中 $\\lfloor \\rfloor$ 是向下取整符号，用于结果不是整数时进行向下取整（`Pytorch` 的 `Conv2d` 卷积函数的默认参数 `ceil_mode = False`，即默认向下取整, `dilation = 1`），其他符号解释如下：\n\n+ 输入图片大小 `W×W`（默认输入尺寸为正方形）\n+ `Filter` 大小 `F×F`\n+ 步长 `S`\n+ padding的像素数 `P`\n+ 输出特征图大小 `N×N`\n\n上图1侧重于解释数值计算过程，而下图2则侧重于解释卷积层的五个核心概念的关系：\n\n- 输入 Input Channel\n- 卷积核组 WeightsBias\n- 过滤器 Filter\n- 卷积核 kernal\n- 输出 Feature Map\n\n![三通道经过两组过滤器的卷积过程](../../data/images/conv/conv3d.png)\n\n上图是三通道经过两组过滤器的卷积过程，在这个例子中，输入是三维数据 $3\\times 32 \\times32$，经过权重参数尺寸为 $2\\times 3\\times 5\\times 5$ 的卷积层后，输出为三维 $2\\times 28\\times 28$，维数并没有变化，只是每一维内部的尺寸有了变化，一般都是要向更小的尺寸变化，以便于简化计算。\n\n假设三维卷积核（也叫滤波器）尺寸为 $(c_{in}, k, k)$，一共有 $c_{out}$ 个滤波器，即卷积层参数尺寸为 $(c_{out}, c_{in}, k, k)$ ，则标准卷积层计算有以下**特点**：\n\n1. 输出的 `feature map` 的数量等于滤波器数量 $c_{out}$，即卷积层参数值确定后，feature map 的数量也确定，而不是根据前向计算自动计算出来；\n2. 对于每个输出，都有一个对应的过滤器 Filter，图例中 Feature Map-1 对应 Filter-1；\n3. **每个 Filter 内都有一个或多个卷积核 Kernal，对应每个输入通道(Input Channel)**，图例为 3，对应输入的红绿蓝三个通道；\n4. 每个 Filter 只有一个 Bias 值，图例中 Filter-1 对应 b1；\n5. 卷积核 Kernal 的大小一般是奇数如：$1\\times 1$，$3\\times 3$。\n\n**注意**，以上内容都描述的是**标准卷积**，随着技术的发展，后续陆续提出了分组卷积、深度可分离卷积、空洞卷积等。详情可参考我之前的文章-[MobileNetv1论文详解](https://github.com/HarleysZhang/cv_note/blob/master/7-model_compression/%E8%BD%BB%E9%87%8F%E7%BA%A7%E7%BD%91%E7%BB%9C%E8%AE%BA%E6%96%87%E8%A7%A3%E6%9E%90/MobileNetv1%E8%AE%BA%E6%96%87%E8%AF%A6%E8%A7%A3.md)。\n\n### 2.3，分组卷积和DW卷积\n\n和标准卷积每个 Filter 内都有一个或多个卷积核 Kernal，对应每个输入通道(Input Channel)的特性不同，分组卷积和 DW 卷积的特点如下：\n- 分组卷积：分组卷积是将输入通道分成若干组，**每组的滤波器只与其同组的输入 feature map 进行卷积**，最终将每组的输出通道拼接在一起得到最终输出。\n- DW 卷积：每个 Filter 内只有一个卷积核 Kernal，对应每个输入通道(Input Channel)，即对于每个输入通道分别使用一个固定大小的卷积核进行卷积操作。\n\n分组卷积的极致是分组数数等于输入通道数，这其实就是 `DW` 卷积，可视化如下：\n\n![DW卷积](../../data/images/conv/dw_conv.png)\n\n另外，对于 `pytorch` 的卷积层 api 是同时支持普通卷积、分组卷积/DW 卷积的。但值得注意的是，对于分组卷积，卷积层的输出通道数必须是分组数的整数倍，否则代码会报错！\n\n```python\nimport torch\ninput = torch.randn([20, 10, 224, 224]) # input_channels = 10\nconv3x3 = torch.nn.Conv2d(in_channels = 10, output_channels = 5, kernel_size=3, groups=5)\noutput = conv3x3(input)\nprint(conv3x3.weight.shape)\nprint(output.shape)\n```\n\n如果将 `groups=5` 改为 `groups=6`或者将 `output_channels  = 5` 改为 `6`，则会报错：\n```bash\nValueError: in_channels must be divisible by groups\nValueError: out_channels must be divisible by groups\n```\n### 2.4，卷积层 api\n\n注意，`2D` 卷积的卷积核权重是一个 `4D` 张量，包含输出通道，输入通道，高，宽。对于 `Pytorch/Caffe` 深度学习框架，其输入输出数据的尺寸都是 （`(N, C, H, W)`），卷积核权重 `shape` 如下：\n- 常规卷积的卷积核权重 `shape`:（`C_out, C_in, kernel_height, kernel_width`）\n- 分组卷积的卷积核权重 `shape`:（`C_out, C_in/g, kernel_height, kernel_width`）\n- `DW` 卷积的卷积核权重`shape`:（`C_in, 1, kernel_height, kernel_width`）\n\n`Pytorch` 框架中对应的 2D 卷积层 api 如下：\n\n```python\n# 对应常规卷积的卷积核权重 shape 都为（out_channels, in_channels, kernel_height, kernel_width）\nclass torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)\n```\n\n**主要参数解释：**\n\n+ `in_channels`(`int`) – 输入信号的通道。\n+ `out_channels`(`int`) – 卷积产生的通道。\n+ `kerner_size`(`int or tuple`) - 卷积核的尺寸。\n+ `stride`(`int or tuple`, `optional`) - 卷积步长，默认值为 `1` 。\n+ `padding`(`int or tuple`, `optional`) - 输入的每一条边补充 `0` 的层数，默认不填充。\n+ `dilation`(`int or tuple`, `optional`) – 卷积核元素之间的间距，默认取值 `1` 。\n+ `groups`(`int`, `optional`) – 从输入通道到输出通道的阻塞连接数。\n+ `bias`(`bool`, `optional`) - 如果 `bias=True`，添加偏置。\n\n**示例代码：**\n\n```python\n###### Pytorch卷积层输出大小验证\nimport torch\nimport torch.nn as nn\nimport torch.autograd as autograd\n# With square kernels and equal stride\n# output_shape: height = (50-3)/2+1 = 24.5，卷积向下取整，所以 height=24.\nm = nn.Conv2d(16, 33, 3, stride=2)\n# # non-square kernels and unequal stride and with padding\n# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))  # 输出shape: torch.Size([20, 33, 28, 100])\n# # non-square kernels and unequal stride and with padding and dilation\n# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1))  # 输出shape: torch.Size([20, 33, 26, 100])\ninput = autograd.Variable(torch.randn(20, 16, 50, 100))\noutput = m(input)\n\nprint(output.shape)  # 输出shape: torch.Size([20, 16, 24, 49])\n```\n\n## 三，卷积神经网络\n\n卷积神经网络一般由卷积层、汇聚层和全连接层构成。\n\n### 3.1，汇聚层\n\n通常当我们处理图像时，我们希望逐渐降低隐藏表示的空间分辨率、聚集信息，这样随着我们在神经网络中层叠的上升，每个神经元对其敏感的感受野（输入）就越大。\n\n汇聚层(Pooling Layer)也叫子采样层(Subsampling Layer)，其作用不仅是进降低卷积层对位置的敏感性，同时降低对空间降采样表示的敏感性。\n\n与卷积层类似，汇聚层运算符由一个固定形状的窗口组成，该窗口根据其步幅大小在输入的所有区域上滑动，为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。然而，不同于卷积层中的输入与卷积核之间的互相关计算，**汇聚层不包含参数**。相反，池运算是确定性的，我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为**最大汇聚层**(maximum pooling)和**平均汇聚层**(average pooling)。\n\n在这两种情况下，与互相关运算符一样，汇聚窗口从输入张量的左上⻆开始，从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置，它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。\n\n比如最大汇聚层，其计算输入中区域的最大值。\n\n![最大汇聚层计算结果](../../data/images/conv/pool_cal2.png)\n\n值得注意的是，与卷积层一样，汇聚层也可以通过改变填充和步幅以获得所需的输出形状。\n\n### 3.2.，汇聚层 api\n\n`Pytorch` 框架中对应的聚合层 api 如下：\n\n```python\nclass torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)\n```\n\n**主要参数解释**：\n\n+ `kernel_size`(`int or tuple`)：`max pooling` 的窗口大小。\n+ `stride`(`int or tuple`, `optional)：`max pooling` 的窗口移动的步长。默认值是 `kernel_size`。\n+ `padding`(`int or tuple`, `optional`)：**默认值为 `0`，即不填充像素**。输入的每一条边补充 `0` 的层数。\n+ `dilation`：滑动窗中各元素之间的距离。\n+ `ceil_mode`：默认值为 `False`，即上述公式默认向下取整，如果设为 `True`，计算输出信号大小的时候，公式会使用向上取整。\n\n> `Pytorch` 中池化层默认`ceil mode = false`，而 `Caffe` 只实现了 `ceil mode= true` 的计算方式。\n\n**示例代码：**\n\n```python\nimport torch\nimport torch.nn as nn\nimport torch.autograd as autograd\n# 大小为3，步幅为2的正方形窗口池\nm = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n# pool of non-square window\ninput = autograd.Variable(torch.randn(20, 16, 50, 32))\noutput = m(input)\nprint(output.shape)  # torch.Size([20, 16, 25, 16])\n```\n\n## 四，卷积神经网络结构\n\n一个典型的卷积网络结构是由卷积层、汇聚层、全连接层交叉堆叠而成。如下图所示：\n\n![典型的卷积网络整体结构](../../data/images/conv/cnn_structure.png)\n\n一个简单的 `CNN` 网络连接图如下所示。\n> 经典 `CNN` 网络的总结，可参考我之前写的文章-[经典 backbone 网络总结](https://github.com/HarleysZhang/cv_note/blob/master/5-deep_learning/%E7%BB%8F%E5%85%B8backbone%E8%AF%A6%E8%A7%A3/%E7%BB%8F%E5%85%B8backbone%E6%80%BB%E7%BB%93.md)。\n\n![一个简单的cnn网络结构图](../../data/images/conv/cnn_demo1.jpeg)\n\n目前，卷积网络的整体结构趋向于使用更小的卷积核(比如 $1 \\times 1$ 和 $3 \\times 3$) 以及更深的结构(比如层数大于 50)。另外，由于卷积层的操作性越来越灵活（同样可完成减少特征图分辨率），汇聚层的作用越来越小，因此目前的卷积神经网络逐渐趋向于全卷积网络。\n\n另外，可通过这个[网站](https://poloclub.github.io/cnn-explainer/#article-convolution)可视化 `cnn` 的全部过程。\n\n![cnn_explainer](../../data/images/conv/cnn_explainer.png)\n\n## 参考资料\n\n1. [AI EDU-17.3 卷积的反向传播原理](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC8%E6%AD%A5%20-%20%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/17.3-%E5%8D%B7%E7%A7%AF%E7%9A%84%E5%8F%8D%E5%90%91%E4%BC%A0%E6%92%AD%E5%8E%9F%E7%90%86.html)\n2. [Visualizing the Feature Maps and Filters by Convolutional Neural Networks](https://medium.com/dataseries/visualizing-the-feature-maps-and-filters-by-convolutional-neural-networks-e1462340518e)\n3. 《神经网络与深度学习》-第5章\n4. 《动手学深度学习》-第6章\n5. https://www.zhihu.com/question/22298352\n6. [卷积神经网络](https://www.huaxiaozhuan.com/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/chapters/5_CNN.html)\n"
  },
  {
    "path": "4-deep_learning/深度学习基础/神经网络基础部件-激活函数详解.md",
    "content": "- [一，激活函数概述](#一激活函数概述)\n  - [1.1，前言](#11前言)\n  - [1.2，激活函数定义](#12激活函数定义)\n  - [1.3，激活函数性质](#13激活函数性质)\n- [二，Sigmoid 型函数（挤压型激活函数）](#二sigmoid-型函数挤压型激活函数)\n  - [2.1，Logistic(sigmoid)函数](#21logisticsigmoid函数)\n  - [2.2，Tanh 函数](#22tanh-函数)\n- [三，ReLU 函数及其变体（半线性激活函数）](#三relu-函数及其变体半线性激活函数)\n  - [3.1，ReLU 函数](#31relu-函数)\n  - [3.2，Leaky ReLU/PReLU/ELU/Softplus 函数](#32leaky-relupreluelusoftplus-函数)\n- [四，Swish 函数](#四swish-函数)\n- [五，激活函数总结](#五激活函数总结)\n- [参考资料](#参考资料)\n\n> 本文分析了激活函数对于神经网络的必要性，同时讲解了几种常见的激活函数的原理，并给出相关公式、代码和示例图。\n\n## 一，激活函数概述\n\n### 1.1，前言\n\n人工神经元(Artificial Neuron)，简称神经元(Neuron)，是构成神经网络的基本单元，其主要是模拟生物神经元的结构和特性，接收一组输入信号并产生输出。生物神经元与人工神经元的对比图如下所示。\n\n![neuron](../../data/images/activation_function/neuron.png)\n\n从机器学习的角度来看，神经网络其实就是一个**非线性模型**，其基本组成单元为具有非线性激活函数的神经元，通过大量神经元之间的连接，使得多层神经网络成为一种高度非线性的模型。**神经元之间的连接权重就是需要学习的参数**，其可以在机器学习的框架下通过**梯度下降方法**来进行学习。\n> 深度学习一般指的是深度神经网络模型，泛指网络层数在三层或者三层以上的神经网络结构。\n\n### 1.2，激活函数定义\n\n激活函数（也称“非线性映射函数”），是深度卷积神经网络模型中必不可少的网络层。\n\n假设一个神经元接收 $D$ 个输入 $x_1, x_2,⋯, x_D$，令向量 $x = [x_1;x_2;⋯;x_𝐷]$ 来表示这组输入，并用净输入(Net Input) $z \\in \\mathbb{R}$ 表示一个神经元所获得的输入信号 $x$ 的加权和:\n\n$$\nz = \\sum_{d=1}^{D} w_{d}x_{d} + b = w^\\top x + b\n$$\n\n其中 $w = [w_1;w_2;⋯;w_𝐷]\\in \\mathbb{R}^D$ 是 $D$ 维的权重矩阵，$b \\in \\mathbb{R}$ 是偏置向量。\n\n以上公式其实就是**带有偏置项的线性变换**（类似于放射变换），本质上还是属于线形模型。为了转换成非线性模型，我们在净输入 $z$ 后添加一个**非线性函数** $f$（即激活函数）。\n\n$$a = f(z)$$\n\n由此，典型的神经元结构如下所示:\n![典型的神经元架构](../../data/images/activation_function/典型的神经元架构.png)\n\n### 1.3，激活函数性质\n\n为了增强网络的表示能力和学习能力，激活函数需要具备以下几点性质:\n1. **连续并可导(允许少数点上不可导)的非线性函数**。可导的激活函数 可以直接利用数值优化的方法来学习网络参数。\n2. 激活函数及其导函数要**尽可能的简单**，有利于提高网络计算效率。\n3. 激活函数的导函数的**值域要在一个合适的区间内**，不能太大也不能太小，否则会影响训练的效率和稳定性.\n\n## 二，Sigmoid 型函数（挤压型激活函数）\n\nSigmoid 型函数是指一类 S 型曲线函数，为两端饱和函数。常用的 Sigmoid 型函数有 Logistic 函数和 Tanh 函数。\n> 相关数学知识: 对于函数 $f(x)$，若 $x \\to −\\infty$ 时，其导数 ${f}'\\to 0$，则称其为左饱和。若 $x \\to +\\infty$ 时，其导数 ${f}'\\to 0$，则称其为右饱和。当同时满足左、右饱和时，就称为两端饱和。\n\n### 2.1，Logistic(sigmoid)函数\n\n对于一个定义域在 $\\mathbb{R}$ 中的输入，`sigmoid` 函数将输入变换为区间 `(0, 1)` 上的输出。因此，sigmoid 通常称为**挤压函数**(squashing function): 它将范围 (-inf, inf) 中的任意输入压缩到区间 (0, 1) 中的某个值:\n\n$$\n\\sigma(x) = \\frac{1}{1 + exp(-x)}\n$$\n\nsigmoid 函数常记作 $\\sigma(x)$。它的导数公式如下所示:\n\n$$\n\\frac{\\mathrm{d} }{\\mathrm{d} x}\\text{sigmoid}(x) = \\frac{exp(-x)}{(1+exp(-x))^2} = \\text{sigmoid}(x)(1 - \\text{sigmoid}(x))\n$$\n\nsigmoid 函数及其导数曲线如下所示:\n\n![sigmoid 函数及其导数图像](../../data/images/activation_function/sigmoid_and_gradient_curve2.png)\n\n可以看出，sigmoid 函数连续，光滑、严格单调，以 (0,0.5) 中心对称，是一个非常良好的阈值函数。\n\n当输入为 0 时，sigmoid 函数的导数达到最大值 0.25; 而输入在任一方向上越远离 0 点时，导数越接近 `0`，即**当sigmoid 函数的输入很大或是很小时，它的梯度都会消失**。\n\n目前 `sigmoid` 函数在隐藏层中已经较少使用，原因是 `sigmoid` 的软饱和性，使得深度神经网络在过去的二三十年里一直难以有效的训练，如今其被更简单、更容易训练的 `ReLU` 等激活函数所替代。\n\n当我们想要输出二分类或多分类、多标签问题的概率时，`sigmoid` **可用作模型最后一层的激活函数**。下表总结了常见问题类型的最后一层激活和损失函数。\n\n|问题类型|最后一层激活|损失函数|\n|-------|----------|-------|\n|二分类问题（binary）|`sigmoid`|`sigmoid + nn.BCELoss`(): 模型最后一层需要经过 ` torch.sigmoid` 函数|\n|多分类、单标签问题（Multiclass）|`softmax`|`nn.CrossEntropyLoss()`: 无需手动做 `softmax`|\n|多分类、多标签问题（Multilabel）|`sigmoid`|`sigmoid + nn.BCELoss()`: 模型最后一层需要经过 `sigmoid` 函数|\n\n> `nn.BCEWithLogitsLoss()` 函数等效于 `sigmoid + nn.BCELoss`。\n\n### 2.2，Tanh 函数\n\n`Tanh`（双曲正切）函数也是一种 Sigmoid 型函数，可以看作放大并平移的 `Sigmoid` 函数，公式如下所示：\n\n$$\n\\text{tanh}(x) = 2\\sigma(2x) - 1 = \\frac{2}{1 + e^{-2x}} - 1\n$$\n\n利用基本导数公式，可得 Tanh 函数的导数公式（推导过程省略）:\n\n$$\n\\frac{\\mathrm{d} }{\\mathrm{d} x} \\text{tanh}(x) = 1 - \\text{tanh}^{2}(x)\n$$\n\n**Logistic 和 Tanh 两种激活函数的实现及可视化代码**（复制可直接运行）如下所示:\n\n```python\n# example plot for the sigmoid activation function\nimport numpy as np\nfrom matplotlib import pyplot\nimport matplotlib.pyplot as plt\n\n# sigmoid activation function\ndef sigmoid(x):\n    \"\"\"1.0 / (1.0 + exp(-x))\n    \"\"\"\n    return 1.0 / (1.0 + np.exp(-x))\n\ndef tanh(x):\n    \"\"\"2 * sigmoid(2*x) - 1\n    (e^x – e^-x) / (e^x + e^-x)\n    \"\"\"\n    # return (exp(x) - exp(-x)) / (exp(x) + exp(-x))\n    return 2 * sigmoid(2*x) - 1\n\ndef relu(x):\n    return max(0.0, x)\n\ndef gradient_relu(x):\n    \"\"\"1 * (x > 0)\"\"\"\n    if x < 0.0:\n        return 0\n    else:\n        return 1\n\ndef gradient_sigmoid(x):\n    \"\"\"sigmoid(x)(1−sigmoid(x))\n    \"\"\"\n    a = sigmoid(x)\n    b = 1 - a\n    return a*b\n\ndef gradient_tanh(x):\n    return 1 - tanh(x)**2\n\n# 1, define input data\ninputs = [x for x in range(-6, 7)]\n\n# 2, calculate outputs\noutputs = [sigmoid(x) for x in inputs]\noutputs2 = [tanh(x) for x in inputs]\n\n# 3, plot sigmoid and tanh function curve\nplt.figure(dpi=100) # dpi 设置\nplt.style.use('ggplot') # 主题设置\n\nplt.plot(inputs, outputs, label='sigmoid')\nplt.plot(inputs, outputs2, label='tanh')\n\nplt.xlabel(\"x\") # 设置 x 轴标签\nplt.ylabel(\"y\")\nplt.title('sigmoid and tanh') # 折线图标题\nplt.legend()\nplt.show()\n```\n\n程序运行后得到的 Sigmoid 和 Tanh 函数曲线如下图所示:\n\n![Logistic函数和Tanh函数](../../data/images/activation_function/sigmoid_tanh_curve.png)\n\n以上代码的基础上，改下 plt.plot 函数的输入数据，同样可得到 Tanh 函数及其导数曲线图:\n\n![Tanh函数及其导数](../../data/images/activation_function/tanh_and_gradient_curve.png)\n\n可以看出 `Sigmoid` 和 `Tanh` 函数在输入很大或是很小的时候，**输出都几乎平滑且梯度很小趋近于 0**，不利于权重更新；不同的是 `Tanh` 函数的输出区间是在 `(-1,1)` 之间，而且整个函数是以 0 为中心的，即他本身是零均值的，也就是说，在前向传播过程中，输入数据的均值并不会发生改变，这就使他在很多应用中效果能比 Sigmoid 优异一些。\n\n**Tanh 函数优缺点总结**：\n\n- 具有 Sigmoid 的所有优点。\n- `exp` 指数计算代价大。梯度消失问题仍然存在。\n\n\n\n`Tanh` 函数及其导数曲线如下所示:\n\nTanh 和 Logistic 函数的导数很类似，都有以下特点:\n- 当输入接近 0 时，导数接近最大值 1。\n- 输入在任一方向上越远离0点，导数越接近0。\n\n## 三，ReLU 函数及其变体（半线性激活函数）\n### 3.1，ReLU 函数\n\n`ReLU`(Rectified Linear Unit，修正线性单元)，是目前深度神经网络中**最经常使用的激活函数**，它保留了类似 step 那样的生物学神经元机制: 输入超过阈值才会激发。公式如下所示:\n\n$$\nReLU(x) = max(0, x) = \\left \\lbrace \\begin{matrix}\nx & x\\geq 0 \\\\ \n0 & x< 0\n\\end{matrix}\\right.\n$$\n\n以上公式通俗理解就是，`ReLU` 函数仅保留正元素并丢弃所有负元素。注意: 虽然在 `0` 点不能求导，但是并不影响其在以梯度为主的反向传播算法中发挥有效作用。\n\n1，**优点**: \n\n- `ReLU` 激活函数**计算简单**；\n- 具有**很好的稀疏性**，大约 50% 的神经元会处于激活状态。\n- 函数在 x > 0 时导数为 1 的性质（**左饱和函数**），在一定程度上缓解了神经网络的梯度消失问题，加速梯度下降的收敛速度。\n> 相关生物知识: 人脑中在同一时刻大概只有 1% ∼ 4% 的神经元处于活跃状态。\n\n2，**缺点**: \n\n- ReLU 函数的输出是非零中心化的，给后一层的神经网络引入偏置偏移，会**影响梯度下降的效率**。\n- ReLU 神经元在训练时比较容易“死亡”。如果神经元参数值在一次不恰当的更新后，其值小于 0，那么这个神经元自身参数的梯度永远都会是 0，在以后的训练过程中永远不能被激活，这种现象被称作“**死区**”。\n\nReLU 激活函数的代码定义如下:\n\n```python\n# pytorch 框架对应函数： nn.ReLU(inplace=True)\nclass ReLU(object):\n\n    def func(self, x):\n        return np.maximum(x, 0.0)\n\n    def derivative(self, x):\n        \"\"\"简单写法: return x > 0.0\"\"\"\n        da = np.array([1 if x > 0 else 0 for x in a])\n        return da     \n```\n**ReLU 激活函数及其函数梯度图**如下所示:\n\n![relu_and_gradient_curve](../../data/images/activation_function/relu_and_gradient_curve2.png)\n\n> `ReLU` 激活函数的更多内容，请参考原论文 [Rectified Linear Units Improve Restricted Boltzmann Machines](https://www.cs.toronto.edu/~fritz/absps/reluICML.pdf)\n\n### 3.2，Leaky ReLU/PReLU/ELU/Softplus 函数\n\n1，`Leaky ReLU` **函数**: 为了缓解“**死区**”现象，研究者将 ReLU 函数中 x < 0 的部分调整为 $\\gamma \\cdot x$， 其中 $\\gamma$ 常设置为 0.01 或 0.001 数量级的较小正数。这种新型的激活函数被称作**带泄露的 ReLU**（`Leaky ReLU`）。\n\n$$\n\\text{Leaky ReLU}(x) = max(0, 𝑥) + \\gamma\\ min(0, x)\n= \\left \\lbrace \\begin{matrix}\nx & x\\geq 0 \\\\ \n\\gamma \\cdot x & x< 0\n\\end{matrix}\\right.\n$$\n\n> 详情可以参考原论文:[《Rectifier Nonlinearities Improve Neural Network Acoustic Models》](https://www.semanticscholar.org/paper/Rectifier-Nonlinearities-Improve-Neural-Network-Maas/367f2c63a6f6a10b3b64b8729d601e69337ee3cc?p2df)\n\n2，`PReLU` **函数**: 为了解决 Leaky ReLU 中**超参数 $\\gamma$ 不易设定**的问题，有研究者提出了参数化 ReLU(Parametric ReLU，`PReLU`)。参数化 ReLU 直接将 $\\gamma$ 也作为一个网络中可学习的变量融入模型的整体训练过程。对于第 $i$ 个神经元，`PReLU` 的 定义为:\n\n$$\n\\text{Leaky ReLU}(x) = max(0, 𝑥) + \\gamma_{i}\\ min(0, x)\n= \\left\\lbrace\\begin{matrix}\nx & x\\geq 0 \\\\ \n\\gamma_{i} \\cdot x & x< 0\n\\end{matrix}\\right.\n$$\n\n> 详情可以参考原论文:[《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》](https://arxiv.org/abs/1502.01852)\n\n3，`ELU` **函数**: 2016 年，Clevert 等人提出的 `ELU` (Exponential Linear Units) 在小于零的部分采用了负指数形式。`ELU`  有很多优点，一方面作为非饱和激活函数，它在所有点上都是连续的和可微的，所以不会遇到梯度爆炸或消失的问题；另一方面，与其他线性非饱和激活函数（如 ReLU 及其变体）相比，它有着更快的训练时间和更高的准确性。\n\n但是，与 ReLU 及其变体相比，其**指数操作也增加了计算量**，即模型推理时 `ELU` 的性能会比 `ReLU` 及其变体慢。 `ELU` 定义如下:\n\n$$\n\\text{Leaky ReLU}(x) = max(0, 𝑥) + min(0, \\gamma(exp(x) - 1)\n= \\left\\lbrace\\begin{matrix}\nx & x\\geq 0 \\\\ \n\\gamma(exp(x) - 1) & x< 0\n\\end{matrix}\\right.\n$$\n\n$\\gamma ≥ 0$ 是一个超参数，决定 $x ≤ 0$ 时的饱和曲线，并调整输出均值在 `0` 附近。\n\n> 详情可以参考原论文:[《Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)》](https://arxiv.org/abs/1511.07289)\n\n4，`Softplus` **函数**: Softplus 函数其导数刚好是 Logistic 函数.Softplus 函数虽然也具有单侧抑制、宽 兴奋边界的特性，却没有稀疏激活性。`Softplus` 定义为:\n\n$$\n\\text{Softplus}(x) = log(1 + exp(x))\n$$\n> 对 `Softplus` 有兴趣的可以阅读这篇论文: [《Deep Sparse Rectifier Neural Networks》](http://proceedings.mlr.press/v15/glorot11a/glorot11a.pdf)。\n\n注意: **ReLU 函数变体有很多，但是实际模型当中使用最多的还是 `ReLU` 函数本身**。\n\nReLU、Leaky ReLU、ELU 以及 Softplus 函数示意图如下图所示:\n\n![relu_more](../../data/images/activation_function/relu_more.png)\n\n## 四，Swish 函数\n\n`Swish` 函数[Ramachandran et al., 2017] 是一种自门控(Self-Gated)激活 函数，定义为\n\n$$\n\\text{swish}(x) = x\\sigma(\\beta x)\n$$\n\n其中 $\\sigma(\\cdot)$ 为 Logistic 函数，$\\beta$ 为可学习的参数或一个固定超参数。$\\sigma(\\cdot) \\in (0, 1)$ 可以看作一种软性的门控机制。当 $\\sigma(\\beta x)$ 接近于 `1` 时，门处于“开”状态，激活函数的输出近似于 $x$ 本身；当 $\\sigma(\\beta x)$ 接近于 `0` 时，门的状态为“关”，激活函数的输出近似于 `0`。\n\n`Swish` 函数代码定义如下：\n\n```python\n# Swish https://arxiv.org/pdf/1905.02244.pdf\nclass Swish(nn.Module):  #Swish激活函数\n    @staticmethod\n    def forward(x, beta = 1): # 此处beta默认定为1\n        return x * torch.sigmoid(beta*x)\n```\n\n结合前面的画曲线代码，可得 Swish 函数的示例图：\n\n![Swish 函数](../../data/images/activation_function/swish_of_different_beta2.png)\n\n**Swish 函数可以看作线性函数和 ReLU 函数之间的非线性插值函数，其程度由参数 $\\beta$ 控制**。\n## 五，激活函数总结\n\n常用的激活函数包括 `ReLU` 函数、`sigmoid` 函数和 `tanh` 函数。其标准代码总结如下（Pytorch 框架中会更复杂）\n\n```python\nfrom math import exp\n\nclass Sigmoid(object):\n\n    def func(self, x):\n        return 1.0 / (1.0 + np.exp(-x))\n\n    def derivative(self, x):\n        return self.func(x) * (1.0 - self.func(x))\n\nclass Tanh(object):\n\n    def func(self, x):\n        return np.tanh(x)\n\n    def derivative(self, x):\n        return 1.0 - self.func(x) ** 2\n    \nclass ReLU(object):\n\n    def func(self, x):\n        return np.maximum(x, 0.0)\n\n    def derivative(self, x):\n        return x > 0.0\n\nclass LeakyReLU(object):\n\n    def __init__(self, alpha=0.2):\n        super().__init__()\n        self.alpha = alpha\n\n    def func(self, x):\n        return np.array([x if x > 0 else self.alpha * x for x in z])\n\n    def derivative(self, x):\n        dx = np.array([1 if x > 0 else self.alpha for x in a])\n        return dx\n\nclass Softplus(object):\n\n    def func(self, x):\n        return np.log(1 + np.exp(z))\n\n    def derivative(self, x):\n        return 1.0 / (1.0 + np.exp(-x))\n```\n\n下表汇总比较了几个激活函数的属性:\n\n![activation_function](../../data/images/activation_function/activation_function_summary.png)\n\n**激活函数的在线可视化**移步 [Visualising Activation Functions in Neural Networks](https://dashee87.github.io/deep%20learning/visualising-activation-functions-in-neural-networks/)。\n\n## 参考资料\n\n1. [Pytorch分类问题中的交叉熵损失函数使用](https://www.cnblogs.com/hmlovetech/p/14515622.html)\n2. 《解析卷积神经网络-第8章》\n3. 《神经网络与深度学习-第4章》\n4. [How to Choose an Activation Function for Deep Learning](https://machinelearningmastery.com/choose-an-activation-function-for-deep-learning/)\n5. [深度学习中的激活函数汇总](http://spytensor.com/index.php/archives/23/)\n6. [Visualising Activation Functions in Neural Networks](https://dashee87.github.io/deep%20learning/visualising-activation-functions-in-neural-networks/)\n7. [AI-EDU: 挤压型激活函数](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC4%E6%AD%A5%20-%20%E9%9D%9E%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92/08.1-%E6%8C%A4%E5%8E%8B%E5%9E%8B%E6%BF%80%E6%B4%BB%E5%87%BD%E6%95%B0.html)\n8. https://github.com/borgwang/tinynn/blob/master/tinynn/core/layer.py"
  },
  {
    "path": "4-deep_learning/深度学习基础总结.md",
    "content": "---\nlayout: post\ntitle: 深度学习基础总结\ndate: 2021-10-10 12:00:00\nsummary: 深度学习基础知识总结。\ncategories: DeepLearning\n---\n\n\n- [一，滤波器与卷积核](#一滤波器与卷积核)\n- [二，卷积层和池化输出大小计算](#二卷积层和池化输出大小计算)\n  - [2.1，CNN 中术语解释](#21cnn-中术语解释)\n  - [2.2，卷积输出大小计算（简化型）](#22卷积输出大小计算简化型)\n  - [2.3，理解边界效应与填充 padding](#23理解边界效应与填充-padding)\n  - [参考资料](#参考资料)\n- [三，深度学习框架的张量形状格式](#三深度学习框架的张量形状格式)\n- [四，Pytorch 、Keras 的池化层函数理解](#四pytorch-keras-的池化层函数理解)\n  - [4.1，torch.nn.MaxPool2d](#41torchnnmaxpool2d)\n  - [4.2，keras.layers.MaxPooling2D](#42keraslayersmaxpooling2d)\n- [五，Pytorch 和 Keras 的卷积层函数理解](#五pytorch-和-keras-的卷积层函数理解)\n  - [5.1，torch.nn.Conv2d](#51torchnnconv2d)\n  - [5.2，keras.layers.Conv2D](#52keraslayersconv2d)\n  - [5.3，总结](#53总结)\n- [六，softmax 回归](#六softmax-回归)\n- [七，交叉熵损失函数](#七交叉熵损失函数)\n  - [7.1，为什么交叉熵可以用作代价函数](#71为什么交叉熵可以用作代价函数)\n  - [7.2，优化算法理解](#72优化算法理解)\n- [八，感受野理解](#八感受野理解)\n  - [8.1，感受野大小计算](#81感受野大小计算)\n- [九，卷积和池化操作的作用](#九卷积和池化操作的作用)\n  - [参考资料](#参考资料-1)\n- [十，卷积层与全连接层的区别](#十卷积层与全连接层的区别)\n- [十一，CNN 权值共享问题](#十一cnn-权值共享问题)\n- [十二，CNN 结构特点](#十二cnn-结构特点)\n  - [Reference](#reference)\n- [十三，深度特征的层次性](#十三深度特征的层次性)\n- [十四，什么样的数据集不适合深度学习](#十四什么样的数据集不适合深度学习)\n- [十五，什么造成梯度消失问题](#十五什么造成梯度消失问题)\n- [十六，Overfitting 和 Underfitting 问题](#十六overfitting-和-underfitting-问题)\n  - [16.1，过拟合问题怎么解决](#161过拟合问题怎么解决)\n  - [16.2，如何判断深度学习模型是否过拟合](#162如何判断深度学习模型是否过拟合)\n  - [16.3，欠拟合怎么解决](#163欠拟合怎么解决)\n  - [16.4，如何判断模型是否欠拟合](#164如何判断模型是否欠拟合)\n- [十七，L1 和 L2 区别](#十七l1-和-l2-区别)\n- [十八，TensorFlow计算图概念](#十八tensorflow计算图概念)\n- [十九，BN（批归一化）的作用](#十九bn批归一化的作用)\n- [二十，什么是梯度消失和爆炸](#二十什么是梯度消失和爆炸)\n  - [梯度消失和梯度爆炸产生的原因](#梯度消失和梯度爆炸产生的原因)\n  - [如何解决梯度消失和梯度爆炸问题](#如何解决梯度消失和梯度爆炸问题)\n- [二十一，RNN循环神经网络理解](#二十一rnn循环神经网络理解)\n- [二十二，训练过程中模型不收敛，是否说明这个模型无效，导致模型不收敛的原因](#二十二训练过程中模型不收敛是否说明这个模型无效导致模型不收敛的原因)\n- [二十三，VGG 使用 2 个 3\\*3 卷积的优势](#二十三vgg-使用-2-个-33-卷积的优势)\n  - [23.1，1\\*1 卷积的主要作用](#23111-卷积的主要作用)\n- [二十四，Relu比Sigmoid效果好在哪里？](#二十四relu比sigmoid效果好在哪里)\n  - [参考链接](#参考链接)\n- [二十五，神经网络中权值共享的理解](#二十五神经网络中权值共享的理解)\n  - [参考资料](#参考资料-2)\n- [二十六，对 fine-tuning(微调模型的理解)，为什么要修改最后几层神经网络权值？](#二十六对-fine-tuning微调模型的理解为什么要修改最后几层神经网络权值)\n  - [参考资料](#参考资料-3)\n- [二十七，什么是 dropout?](#二十七什么是-dropout)\n  - [27.1，dropout具体工作流程](#271dropout具体工作流程)\n  - [27.2，dropout在神经网络中的应用](#272dropout在神经网络中的应用)\n  - [27.3，如何选择dropout 的概率](#273如何选择dropout-的概率)\n  - [参考资料](#参考资料-4)\n- [二十八，HOG 算法原理描述](#二十八hog-算法原理描述)\n  - [HOG特征原理](#hog特征原理)\n  - [HOG特征检测步骤](#hog特征检测步骤)\n  - [参考资料](#参考资料-5)\n- [二十九，激活函数](#二十九激活函数)\n  - [29.1，激活函数的作用](#291激活函数的作用)\n  - [29.2，常见的激活函数](#292常见的激活函数)\n  - [29.3，激活函数理解及函数梯度图](#293激活函数理解及函数梯度图)\n- [三十，卷积层和池化层有什么区别](#三十卷积层和池化层有什么区别)\n- [三十一，卷积层和池化层参数量计算](#三十一卷积层和池化层参数量计算)\n- [三十二，神经网络为什么用交叉熵损失函数](#三十二神经网络为什么用交叉熵损失函数)\n- [三十三，数据增强方法有哪些](#三十三数据增强方法有哪些)\n  - [33.1，离线数据增强和在线数据增强有什么区别?](#331离线数据增强和在线数据增强有什么区别)\n  - [Reference](#reference-1)\n- [三十四，ROI Pooling替换为ROI Align的效果，及各自原理](#三十四roi-pooling替换为roi-align的效果及各自原理)\n  - [ROI Pooling原理](#roi-pooling原理)\n  - [ROI Align原理](#roi-align原理)\n  - [RoiPooling 和 RoiAlign 总结](#roipooling-和-roialign-总结)\n  - [Reference](#reference-2)\n- [三十五，CNN的反向传播算法推导](#三十五cnn的反向传播算法推导)\n- [三十六，Focal Loss 公式](#三十六focal-loss-公式)\n- [三十七，快速回答](#三十七快速回答)\n  - [37.1，为什么 Faster RCNN、Mask RCNN 需要使用 ROI Pooling、ROI Align?](#371为什么-faster-rcnnmask-rcnn-需要使用-roi-poolingroi-align)\n  - [37.2，softmax公式](#372softmax公式)\n  - [37.3，上采样方法总结](#373上采样方法总结)\n  - [37.4，移动端深度学习框架知道哪些，用过哪些？](#374移动端深度学习框架知道哪些用过哪些)\n  - [37.5，如何提升网络的泛化能力](#375如何提升网络的泛化能力)\n  - [37.6，BN算法，为什么要在后面加伽马和贝塔，不加可以吗？](#376bn算法为什么要在后面加伽马和贝塔不加可以吗)\n  - [37.7，验证集和测试集的作用](#377验证集和测试集的作用)\n- [三十八，交叉验证的理解和作用](#三十八交叉验证的理解和作用)\n- [三十九，介绍一下NMS和IOU的原理](#三十九介绍一下nms和iou的原理)\n- [Reference](#reference-3)\n\n## 一，滤波器与卷积核\n\n在只有一个通道的情况下，“卷积核”（`“kernel”`）就相当于滤波器（`“filter”`），这两个概念是可以互换的。一个 `“Kernel”` 更倾向于是 `2D` 的权重矩阵。而 `“filter”` 则是指多个 `kernel` 堆叠的 `3D` 结构。如果是一个 `2D` 的 `filter`，那么两者就是一样的。但是一个`3D` `filter`，在大多数深度学习的卷积中，它是包含 `kernel` 的。**每个卷积核都是独一无二的，主要在于强调输入通道的不同方面**。\n\n## 二，卷积层和池化输出大小计算\n\n> 不管是 `TensorFlow`、`Keras`、`Caffe` 还是 `Pytorch`，其卷积层和池化层的参数默认值可能有所不同，但是最终的卷积输出大小计算公式是一样的。\n\n### 2.1，CNN 中术语解释\n\n卷积层主要参数有下面这么几个：\n\n+ 卷积核 `Kernel` 大小（在 `Tensorflow/keras` 框架中也称为`filter`）；\n+ 填充 `Padding` ；\n+ 滑动步长 `Stride`；\n+ 输出通道数 `Channels`。\n\n### 2.2，卷积输出大小计算（简化型）\n\n1，在 `Pytorch` 框架中，图片（`feature map`）经卷积 `Conv2D` 后**输出大小计算公式**如下：$\\left \\lfloor N = \\frac{W-F+2P}{S}+1 \\right \\rfloor$，其中 $\\lfloor \\rfloor$ 是向下取整符号，用于结果不是整数时进行向下取整（`Pytorch` 的 `Conv2d` 卷积函数的默认参数 `ceil_mode = False`，即默认向下取整, `dilation = 1`）。\n\n+ 输入图片大小 `W×W`（默认输入尺寸为正方形）\n+ `Filter` 大小 `F×F`\n+ 步长 `S`\n+ padding的像素数 `P`\n+ 输出特征图大小 `N×N`\n\n2，特征图经反卷积（也叫转置卷积） `keras-Conv2DTranspose`（`pytorch-ConvTranspose2d`） 后得到的特征图大小计算方式：$out = (in - 1)s -2p + k$，另外还有一个写法：$W = (N - 1)*S - 2P + F$，这可由卷积输出大小计算公式反推得到。$in$ 是输入大小， $k$ 是卷积核大小，$s$ 是滑动步长， `padding` 的像素数 $p$，$out$ 是输出大小。\n\n**反卷积**也称为转置卷积，一般主要用来还原 `feature map` 的尺寸大小，在 `cnn` 可视化，`fcn` 中达到 `pixel classification`，以及 `gan` 中从特征生成图像都需要用到反卷积的操作。反卷积输出结果计算实例。例如，输入：`2x2`， 卷积核大小：`4x4`， 滑动步长：`3`，填充像素为 `0`， 输出：`7x7` ，其计算过程就是， `(2 - 1) * 3 + 4 = 7`。\n\n3，池化层如果设置为不填充像素（对于 `Pytorch`，设置参数`padding = 0`，对于 `Keras/TensorFlow`，设置参数`padding=\"valid\"`），池化得到的特征图大小计算方式: $N=(W-F)/S+1$，这里公式表示的是除法结果向下取整再加 `1`。\n\n总结：对于`Pytorch` 和 `tensorflow` 的卷积和池化函数，卷积函数 `padding` 参数值默认为 `0/\"valid\"`（即不填充），但在实际设计的卷积神经网络中，卷积层一般会填充像素(`same`)，池化层一般不填充像素(`valid`)，**输出 `shape` 计算是向下取整**。注意：当 `stride`为 `1` 的时候，`kernel`为 `3`、`padding`为 `1` 或者 `kernel`为 `5`、`padding`为 `2`，这两种情况可直接得出卷积前后特征图尺寸不变。\n\n> 注意不同的深度学习框架，卷积/池化函数的输出 `shape` 计算会有和上述公式有所不同，我给出的公式是简化版，适合面试题计算，实际框架的计算比这复杂，因为参数更多。\n\n### 2.3，理解边界效应与填充 padding\n\n如果希望输出特征图的空间维度`Keras/TensorFlow` 设置卷积层的过程中可以设置 `padding` 参数值为 `“valid” 或 “same”`。`“valid”` 代表只进行有效的卷积，对边界数据不处理。`“same” 代表 TensorFlow` 会自动对原图像进行补零（表示卷积核可以停留在图像边缘），也就是自动设置 `padding` 值让输出与输入形状相同。\n\n### 参考资料\n\n+ [CNN中的参数解释及计算](https://flat2010.github.io/2018/06/15/%E6%89%8B%E7%AE%97CNN%E4%B8%AD%E7%9A%84%E5%8F%82%E6%95%B0/ \"CNN中的参数解释及计算\")\n+ [CNN基础知识——卷积（Convolution）、填充（Padding）、步长(Stride)](https://zhuanlan.zhihu.com/p/77471866 \"CNN基础知识——卷积（Convolution）、填充（Padding）、步长(Stride)\")\n\n## 三，深度学习框架的张量形状格式\n\n+ 图像张量的形状有两种约定，**通道在前**（`channel-first`）和**通道在后**（`channel-last`）的约定，常用深度学习框架使用的**数据张量形状**总结如下：\n    + `Pytorch/Caffe`: (`N, C, H, W`)；\n    + `TensorFlow/Keras`: (`N, H, W, C`)。\n\n+ 举例理解就是`Pytorch` 的卷积层和池化层的输入 `shape` 格式为 `(N, C, H, W)`，`Keras` 的卷积层和池化层的输入 `shape` 格式为 `(N, H, W, C)`。\n\n值得注意的是 `OpenCV` 读取图像后返回的矩阵 `shape` 的格式是 `（H, W, C）`格式。当 OpenCV 读取的图像为彩色图像时，返回的多通道的 BGR 格式的矩阵（`HWC`），在内存中的存储如下图：\n\n![opencv矩阵存储格式](../data/images/opencv/opencv_data_format.png)\n\n## 四，Pytorch 、Keras 的池化层函数理解\n\n> 注意：对于 `Pytorch、Keras` 的卷积层和池化层函数，其 **`padding` 参数值都默认为不填充像素**，默认值为 `0`和 `valid`。\n\n### 4.1，torch.nn.MaxPool2d\n\n```python\nclass torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)\n```\n\n二维池化层，默认输入的尺度是`(N, C_in,H,W)`，输出尺度`（N,C_out,H_out,W_out）`。池化层输出尺度的 Width 默认计算公式如下（`ceil_mode= True` 时是向上取整，`Height` 计算同理）:\n\n $$\\left\\lfloor \\frac{W_{in} + 2 * \\text{padding}[0] - \\text{dilation}[0] \\times (\\text{kernel\\_size}[0] - 1) - 1}{\\text{stride[0]}} + 1 \\right\\rfloor$$\n\n**主要参数解释**：\n\n+ `kernel_size`(`int or tuple`)：`max pooling` 的窗口大小。\n+ `stride`(`int or tuple`, `optional)：`max pooling` 的窗口移动的步长。默认值是 `kernel_size`。\n+ `padding`(`int or tuple`, `optional`)：**默认值为 `0`，即不填充像素**。输入的每一条边补充 `0` 的层数。\n+ `dilation`：滑动窗中各元素之间的距离。\n+ `ceil_mode`：默认值为 `False`，即上述公式默认向下取整，如果设为 `True`，计算输出信号大小的时候，公式会使用向上取整。\n\n> `Pytorch` 中池化层默认`ceil mode = false`，而 `Caffe` 只实现了 `ceil mode= true` 的计算方式。\n\n**示例代码：**\n\n```python\nimport torch\nimport torch.nn as nn\nimport torch.autograd as autograd\n# 大小为3，步幅为2的正方形窗口池\nm = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n# pool of non-square window\ninput = autograd.Variable(torch.randn(20, 16, 50, 32))\noutput = m(input)\nprint(output.shape)  # torch.Size([20, 16, 25, 16])\n```\n\n### 4.2，keras.layers.MaxPooling2D\n\n```python\nkeras.layers.MaxPooling2D(pool_size=(2, 2), strides=None, padding='valid', data_format=None)\n```\n\n对于 `2D` 空间数据的最大池化。默认输入尺寸是 `(batch_size, rows, cols, channels)/(N, H, W, C_in)` 的 `4D` 张量，默认输出尺寸是 `(batch_size, pooled_rows, pooled_cols, channels)` 的 `4D` 张量。\n\n+ `padding = valid`：池化层输出的特征图大小为：$N=(W-F)/S+1$，**这里表示的是向下取整再加 1**。\n+ `padding = same`: 池化层输出的特征图大小为 $N = W/S$，**这里表示向上取整**。\n\n**主要参数解释：**\n\n+ `pool_size`: 整数，或者 `2` 个整数表示的元组， 沿（垂直，水平）方向缩小比例的因数。（`2，2`）会把输入张量的两个维度都缩小一半。 如果只使用一个整数，那么两个维度都会使用同样的窗口长度。\n+ `strides`: 整数，`2` 个整数表示的元组，或者是 `None`。 表示步长值。 如果是 `None`，那么默认值是 `pool_size`。\n+ `padding`: `\"valid\"` 或者 `\"same\"`（区分大小写）。\n+ `data_format`: 字符串，`channels_last` (默认)或 `channels_first` 之一。 表示输入各维度的顺序。 `channels_last` 代表尺寸是 `(batch, height, width, channels)` 的输入张量， 而 `channels_first` 代表尺寸是 `(batch, channels, height, width)` 的输入张量。 默认值根据 `Keras` 配置文件 `~/.keras/keras.json` 中的 `image_data_format` 值来设置。如果还没有设置过，那么默认值就是 `\"channels_last\"`。\n\n## 五，Pytorch 和 Keras 的卷积层函数理解\n\n### 5.1，torch.nn.Conv2d\n\n注意，`2D` 卷积的卷积核权重是一个 `4D` 张量，包含输出通道，输入通道，高，宽。对于 `Pytorch/Caffe` 深度学习框架，其输入输出数据的尺寸都是 （`(N, C, H, W)`），卷积核权重 `shape` 如下：\n- 常规卷积的卷积核权重 `shape`:（`C_out, C_in, kernel_height, kernel_width`）\n- 分组卷积的卷积核权重 `shape`:（`C_out, C_in/g, kernel_height, kernel_width`）\n- `DW` 卷积的卷积核权重`shape`:（`C_in, 1, kernel_height, kernel_width`）\n\n`Pytorch` 框架中对应的 2D 卷积层 api 如下：\n\n```python\nclass torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)\n```\n\n二维卷积层, 输入的尺度是`(N, C_in, H, W)`，输出尺度`（N,C_out,H_out,W_out）`。卷积层输出尺度的 `Weight` 计算公式如下（`Height` 同理）：\n\n$$\\left\\lfloor \\frac{W_{in} + 2 \\times \\text{padding}[0] - \\text{dilation}[0] \\times (\\text{kernel\\_size}[0] - 1) - 1}{\\text{stride}[0]} + 1\\right\\rfloor$$\n\n`kernel_size`, `stride`, `padding`, `dilation` 参数可以是以下两种形式( `Maxpool2D` 也一样)：\n\n+ `a single int`：同样的参数值被应用与 `height` 和 `width` 两个维度。\n+ `a tuple of two ints`：第一个 `int` 值应用于 `height` 维度，第二个 `int` 值应用于 `width` 维度，也就是说卷积输出后的 `height` 和 `width` 值是不同的，要分别计算。\n\n**主要参数解释：**\n\n+ `in_channels`(`int`) – 输入信号的通道。\n+ `out_channels`(`int`) – 卷积产生的通道。\n+ `kerner_size`(`int or tuple`) - 卷积核的尺寸。\n+ `stride`(`int or tuple`, `optional`) - 卷积步长，默认值为 `1` 。\n+ `padding`(`int or tuple`, `optional`) - 输入的每一条边补充 `0` 的层数，默认不填充。\n+ `dilation`(`int or tuple`, `optional`) – 卷积核元素之间的间距，默认取值 `1` 。\n+ `groups`(`int`, `optional`) – 从输入通道到输出通道的阻塞连接数。\n+ `bias`(`bool`, `optional`) - 如果 `bias=True`，添加偏置。\n\n**示例代码：**\n\n```python\n###### Pytorch卷积层输出大小验证\nimport torch\nimport torch.nn as nn\nimport torch.autograd as autograd\n# With square kernels and equal stride\n# output_shape: height = (50-3)/2+1 = 24.5，卷积向下取整，所以 height=24.\nm = nn.Conv2d(16, 33, 3, stride=2)\n# # non-square kernels and unequal stride and with padding\n# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))  # 输出shape: torch.Size([20, 33, 28, 100])\n# # non-square kernels and unequal stride and with padding and dilation\n# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1))  # 输出shape: torch.Size([20, 33, 26, 100])\ninput = autograd.Variable(torch.randn(20, 16, 50, 100))\noutput = m(input)\n\nprint(output.shape)  # 输出shape: torch.Size([20, 16, 24, 49])\n```\n\n### 5.2，keras.layers.Conv2D\n\n```python\nkeras.layers.Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', data_format=None, dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)\n```\n\n`2D` 卷积层 (例如对图像的空间卷积)。输入输出尺寸格式要求和池化层函数一样。输入尺寸：`(N, H, W, C)`，卷积核尺寸：（`K, K, C_in, C_out`）。\n\n**当使用该层作为模型第一层时，需要提供 `input_shape` 参数**（整数元组，不包含 `batch` 轴），例如，`input_shape=(128, 128, 3)` 表示 `128x128` 的 `RGB` 图像，在 `data_format=\"channels_last\"` 时。\n\n**主要参数解释：**\n\n+ `filters`: 整数，输出空间的维度 （**即卷积中滤波器的输出数量**）。\n+ `kernel_size`: 一个整数，或者 `2` 个整数表示的元组或列表，指明 `2D` 卷积窗口的宽度和高度。 **可以是一个整数，为所有空间维度指定相同的值**。\n+ `strides`: 一个整数，或者 `2` 个整数表示的元组或列表，指明卷积核模板沿宽度和高度方向的移动步长。 可以是一个整数，为所有空间维度指定相同的值。 指定任何 stride 值 != 1 与指定 dilation_rate 值 != 1 两者不兼容，默认取值 1，即代表会不遗漏的滑过输入图片（`Feature Map`）的每一个点。\n+ `padding`: `\"valid\"` 或 `\"same\"` (大小写敏感)，默认`valid`，这里的 `\"same\"` 代表给边界加上 `Padding` 让卷积的输出和输入保持同样（`\"same\"`）的尺寸（即填充像素）。\n+ `data_format`: 字符串， `channels_last (默认)` 或 `channels_first` 之一，表示输入中维度的顺序。 `channels_last` 对应输入尺寸为 `(batch_size, height, width, channels)`， channels_first 对应输入尺寸为 `(batch_size, channels, height, width)`。 它默认为从 `Keras` 配置文件 `~/.keras/keras.json` 中 找到的 `image_data_format` 值。 如果你从未设置它，将使用 `channels_last`。\n+ `dilation_rate`: 一个整数或 `2` 个整数的元组或列表， 指定膨胀卷积（空洞卷积 `dilated convolution`）的膨胀率。 可以是一个整数，为所有空间维度指定相同的值。 当前，指定任何 dilation_rate 值 != 1 与 指定 stride 值 != 1 两者不兼容。\n\n### 5.3，总结\n\n`Pytorch` 的 `Conv2d` 函数不要求提供 输入数据的大小 `(H,W)`，但是要提供输入深度，`Keras` 的 `Conv2d` 函数第一层要求提供 `input_shape` 参数 `(H,W, C)`，其他层不需要。\n\n ## 六，softmax 回归\n\n**分类问题**中，直接使用输出层的输出有两个问题：\n\n+ 神经网络输出层的输出值的范围不确定，我们难以直观上判断这些值的意义\n+ 由于真实标签是离散值，这些离散值与不确定范围的输出值之间的误差难以衡量\n\n`softmax` 回归解决了以上两个问题，它**将输出值变换为值为正且和为 1 的概率分布**，公式如下：\n$$\nsoftmax(y)_{i} = y_{i}^{'} = \\frac{e^{yi}}{\\sum_{j=1}^{n}e^{yj}}\n$$\n\n## 七，交叉熵损失函数\n\n交叉熵刻画了两个概率分布之间的距离，它是分类问题中使用比较广泛的一种损失函数，交叉熵一般会与 `softmax` 回归一起使用，公式如下：\n\n$$L = -\\sum_{c=1}^{M}y_{c}log(p_{c})或者H(p,q)=-\\sum p(x)logq(x)$$\n\n- $p$ ——代表正确答案；\n- $q$ ——代表预测值；\n- $M$ ——类别的数量；\n- $y_{c}$ ——指示变量（ `0` 或 `1`），如果该类别和样本的类别相同就是 `1`，否则是 `0`；\n- $p_{c}$ ——对于观测样本属于类别 $c$ 的预测概率。\n\n### 7.1，为什么交叉熵可以用作代价函数\n\n从数学上来理解就是，为了让学到的模型分布更接近真实数据的分布，我们需要最小化模型数据分布与训练数据之间的 `KL 散度`，而因为训练数据的分布是固定的，因此最小化 `KL 散度`等价于最小化交叉熵，而且交叉熵计算更简单，所以机器/深度学习中常用交叉熵 `cross-entroy` 作为分类问题的损失函数。\n\n### 7.2，优化算法理解\n`Adam`、`AdaGrad`、`RMSProp`优化算法具有自适应性。\n\n## 八，感受野理解\n\n感受野理解(`Receptive Field`)是指后一层神经元在前一层神经元的感受空间，也可以定义为卷积神经网络中**每层的特征图（`Feature Map`）上的像素点在原始图像中映射的区域大小**，即如下图所示：\n\n![感受野大小](../data/images/dl_basic/receptive_field.jpg)\n\n注意：感受野在 `CNN` 中是呈指数级增加的。小卷积核（如 `3*3`）通过多层叠加可取得与大卷积核（如 `7*7`）同等规模的感受野，此外采用小卷积核有两个优势：\n\n1. 小卷积核需多层叠加，加深了网络深度进而增强了网络容量(`model capacity`)和复杂度（`model complexity`）。\n2. 增强了网络容量的同时减少了参数个数。\n\n### 8.1，感受野大小计算\n\n计算感受野时，我们需要知道：\n> 参考 [感受野（receptive file）计算](https://www.starlg.cn/blog/2017/06/13/Receptive-File/ \"感受野（receptive file）计算\")\n\n+ 第一层卷积层的输出特征图像素的感受野的大小等于滤波器的大小；\n+ 深层卷积层的感受野大小和它之前所有层的滤波器大小和步长有关系；\n+ 计算感受野大小时，忽略了图像边缘的影响。\n\n感受野的计算方式有两种：自底向上和自顶向下（`top to down`），这里只讲解后者。正常卷积（且不带 `padding`）感受野计算公式如下：\n\n$$F(i, j-1) = (F(i, j)-1)*stride + kernel\\_size$$\n\n其中 $F(i, j)$ 表示第 `i` 层对第 `j` 层的局部感受野，所以这个公式是从上层向下层计算感受野的。仔细看这个公式会发现和反卷积输出大小计算公式一模一样，实际上感受野计算公式就是 `feature_map` 计算公式的反向推导。\n\n以下 `Python` 代码可以实现计算 `Alexnet zf-5` 和 `VGG16` 网络每层输出 `feature map` 的感受野大小，卷积核大小和输入图像尺寸默认定义好了，代码如下：\n\n```python\n# !/usr/bin/env python\n\n# [filter size, stride, padding]\nnet_struct = {'alexnet': {'net':[[11,4,0],[3,2,0],[5,1,2],[3,2,0],[3,1,1],[3,1,1],[3,1,1],[3,2,0]],\n                   'name':['conv1','pool1','conv2','pool2','conv3','conv4','conv5','pool5']},\n       'vgg16': {'net':[[3,1,1],[3,1,1],[2,2,0],[3,1,1],[3,1,1],[2,2,0],[3,1,1],[3,1,1],[3,1,1],\n                        [2,2,0],[3,1,1],[3,1,1],[3,1,1],[2,2,0],[3,1,1],[3,1,1],[3,1,1],[2,2,0]],\n                 'name':['conv1_1','conv1_2','pool1','conv2_1','conv2_2','pool2','conv3_1','conv3_2',\n                         'conv3_3', 'pool3','conv4_1','conv4_2','conv4_3','pool4','conv5_1','conv5_2','conv5_3','pool5']},\n       'zf-5':{'net': [[7,2,3],[3,2,1],[5,2,2],[3,2,1],[3,1,1],[3,1,1],[3,1,1]],\n               'name': ['conv1','pool1','conv2','pool2','conv3','conv4','conv5']}}\n\n\ndef outFromIn(isz, net, layernum):\n    \"\"\"\n    计算feature map大小\n    \"\"\"\n    totstride = 1\n    insize = isz\n    # for layer in range(layernum):\n    fsize, stride, pad = net[layernum]\n    outsize = (insize - fsize + 2*pad) / stride + 1\n    insize = outsize\n    totstride = totstride * stride\n    return outsize, totstride\n\ndef inFromOut(net, layernum):\n    \"\"\"\n    计算感受野receptive file大小\n    \"\"\"\n    RF = 1\n    for layer in reversed(range(layernum)):  # reversed 函数返回一个反向的迭代器\n        fsize, stride, pad = net[layer]\n        RF = ((RF -1)* stride) + fsize\n    return RF\n\nif __name__ == '__main__':\n    imsize = 224\n    feature_size = imsize\n    print (\"layer output sizes given image = %dx%d\" % (imsize, imsize))\n    \n    for net in net_struct.keys():\n        feature_size = imsize\n        print ('************net structrue name is %s**************'% net)\n        for i in range(len(net_struct[net]['net'])):\n            feature_size, stride = outFromIn(feature_size, net_struct[net]['net'], i)\n            rf = inFromOut(net_struct[net]['net'], i+1)\n            print (\"Layer Name = %s, Output size = %3d, Stride = % 3d, RF size = %3d\" % (net_struct[net]['name'][i], feature_size, stride, rf))\n```\n\n**程序输出结果如下：**\n\n```shell\nlayer output sizes given image = 224x224\n************net structrue name is alexnet**************\nLayer Name = conv1, Output size =  54, Stride =   4, RF size =  11\nLayer Name = pool1, Output size =  26, Stride =   2, RF size =  19\nLayer Name = conv2, Output size =  26, Stride =   1, RF size =  51\nLayer Name = pool2, Output size =  12, Stride =   2, RF size =  67\nLayer Name = conv3, Output size =  12, Stride =   1, RF size =  99\nLayer Name = conv4, Output size =  12, Stride =   1, RF size = 131\nLayer Name = conv5, Output size =  12, Stride =   1, RF size = 163\nLayer Name = pool5, Output size =   5, Stride =   2, RF size = 195\n************net structrue name is vgg16**************\nLayer Name = conv1_1, Output size = 224, Stride =   1, RF size =   3\nLayer Name = conv1_2, Output size = 224, Stride =   1, RF size =   5\nLayer Name = pool1, Output size = 112, Stride =   2, RF size =   6\nLayer Name = conv2_1, Output size = 112, Stride =   1, RF size =  10\nLayer Name = conv2_2, Output size = 112, Stride =   1, RF size =  14\nLayer Name = pool2, Output size =  56, Stride =   2, RF size =  16\nLayer Name = conv3_1, Output size =  56, Stride =   1, RF size =  24\nLayer Name = conv3_2, Output size =  56, Stride =   1, RF size =  32\nLayer Name = conv3_3, Output size =  56, Stride =   1, RF size =  40\nLayer Name = pool3, Output size =  28, Stride =   2, RF size =  44\nLayer Name = conv4_1, Output size =  28, Stride =   1, RF size =  60\nLayer Name = conv4_2, Output size =  28, Stride =   1, RF size =  76\nLayer Name = conv4_3, Output size =  28, Stride =   1, RF size =  92\nLayer Name = pool4, Output size =  14, Stride =   2, RF size = 100\nLayer Name = conv5_1, Output size =  14, Stride =   1, RF size = 132\nLayer Name = conv5_2, Output size =  14, Stride =   1, RF size = 164\nLayer Name = conv5_3, Output size =  14, Stride =   1, RF size = 196\nLayer Name = pool5, Output size =   7, Stride =   2, RF size = 212\n************net structrue name is zf-5**************\nLayer Name = conv1, Output size = 112, Stride =   2, RF size =   7\nLayer Name = pool1, Output size =  56, Stride =   2, RF size =  11\nLayer Name = conv2, Output size =  28, Stride =   2, RF size =  27\nLayer Name = pool2, Output size =  14, Stride =   2, RF size =  43\nLayer Name = conv3, Output size =  14, Stride =   1, RF size =  75\nLayer Name = conv4, Output size =  14, Stride =   1, RF size = 107\nLayer Name = conv5, Output size =  14, Stride =   1, RF size = 139\n```\n\n## 九，卷积和池化操作的作用\n\n> 卷积核池化的定义核过程理解是不难的，但是其作用却没有一个标准的答案，我在网上看了众多博客和魏秀参博士的书籍，总结了以下答案。\n\n卷积层和池化层的理解可参考魏秀参的《解析卷积神经网络》书籍，卷积（`convolution` ）操作的作用如下：\n\n1. `局部感知，参数共享` 的特点大大降低了网络参数，保证了网络的稀疏性。\n2. 通过卷积核的组合以及随着网络后续操作的进行，卷积操作可获取图像不同区域的不同类型特征；模型靠近底部的层提取的是局部的、高度通用的特征图，而更靠近顶部的层提取的是更加抽象的语义特征。\n\n池化/汇合（`pooling` ）操作作用如下：\n\n1. **特征不变性**（feature invariant）。汇合操作使模型更关注是否存在某些特征而不是特征具体的位置可看作是一种很强的先验，使特征学习包含某种程度自由度，能容忍一些特征微小的位移。\n2. **特征降维**。由于汇合操作的降采样作用，汇合结果中的一个元素对应于原输入数据的一个子区域（sub-region），因此汇合相当于在空间范围内做了维度约减（spatially dimension reduction），从而使模型可以抽取更广范围的特征。同时减小了下一层输入大小，进而减小计算量和参数个数。\n3. 在一定程度上能**防止过拟合（overfitting）**，更方便优化。\n\n### 参考资料\n\n魏秀参-《解析卷积神经网络》\n\n## 十，卷积层与全连接层的区别\n\n+ 卷积层学习到的是局部模式（对于图像，学到的就是在输入图像的二维小窗口中发现的模式）\n+ 全连接层学习到的是全局模式（全局模式就算设计所有像素）\n\n## 十一，CNN 权值共享问题\n\n首先**权值共享就是滤波器共享**，滤波器的参数是固定的，即是用相同的滤波器去扫一遍图像，提取一次特征特征，得到feature map。在卷积网络中，学好了一个滤波器，就相当于掌握了一种特征，这个滤波器在图像中滑动，进行特征提取，然后所有进行这样操作的区域都会被采集到这种特征，就好比上面的水平线。\n\n## 十二，CNN 结构特点\n\n典型的用于分类的CNN主要由**卷积层+激活函数+池化层**组成，最后用全连接层输出。卷积层负责提取图像中的局部特征；池化层用来大幅降低参数量级(降维)；全连接层类似传统神经网络的部分，用来输出想要的结果。\n\n`CNN` 具有局部连接、权值共享、池化操作(简单说就是下采样)和多层次结构的特点。\n\n+ 局部连接使网络可以提取数据的局部特征。\n+ 权值共享大大降低了网络的训练难度，一个Filter只提取一个特征，在整个图片（或者语音／文本） 中进行卷积。\n+ 池化操作与多层次结构一起，实现了数据的降维，将低层次的局部特征组合成为较高层次的特征，从而对整个图片进行表示。\n+ 卷积神经网络学到的模式具有平移不变性（`translation invariant`），且可以学到模式的空间层次结构。\n\n### Reference\n\n[(二)计算机视觉四大基本任务(分类、定位、检测、分割](https://zhuanlan.zhihu.com/p/31727402 \"(二)计算机视觉四大基本任务(分类、定位、检测、分割\")\n\n## 十三，深度特征的层次性\n\n卷积操作可获取图像区域不同类型特征，而汇合等操作可对这些特征进行融合和抽象，随着若干卷积、汇合等操作的堆叠，各层得到的深度特征逐渐从泛化特征（如边缘、纹理等）过渡到高层语义表示（躯干、头部等模式）。\n\n## 十四，什么样的数据集不适合深度学习\n\n+ 数据集太小，数据样本不足时，深度学习相对其它机器学习算法，没有明显优势。\n+ 数据集没有局部相关特性，目前深度学习表现比较好的领域主要是图像／语音／自然语言处理等领域，这些领域的一个共性是局部相关性。图像中像素组成物体，语音信号中音位组合成单词，文本数据中单词组合成句子，这些特征元素的组合一旦被打乱，表示的含义同时也被改变。对于没有这样的局部相关性的数据集，不适于使用深度学习算法进行处理。举个例子：预测一个人的健康状况，相关的参数会有年龄、职业、收入、家庭状况等各种元素，将这些元素打乱，并不会影响相关的结果。\n\n## 十五，什么造成梯度消失问题\n\n+ 神经网络的训练中，通过改变神经元的权重，使网络的输出值尽可能逼近标签以降低误差值，训练普遍使用BP算法，核心思想是，计算出输出与标签间的损失函数值，然后计算其相对于每个神经元的梯度，进行权值的迭代。\n+ 梯度消失会造成权值更新缓慢，模型训练难度增加。造成梯度消失的一个原因是，许多激活函数将输出值挤压在很小的区间内，在激活函数两端较大范围的定义域内梯度为0，造成学习停止。\n\n## 十六，Overfitting 和 Underfitting 问题\n\n### 16.1，过拟合问题怎么解决\n\n首先所谓过拟合，指的是一个模型过于复杂之后，它可以很好地“记忆”每一个训练数据中随机噪音的部分而忘记了去“训练”数据中的通用趋势。**训练好后的模型过拟合具体表现在：模型在训练数据上损失函数较小，预测准确率较高；但是在测试数据上损失函数比较大，预测准确率较低**。解决办法如下：\n\n+ `数据增强`, 增加数据多样性;\n+ 正则化策略：如 Parameter Norm Penalties(参数范数惩罚), `L1, L2正则化`;\n+ `dropout`;\n+ 模型融合, 比如Bagging 和其他集成方法;\n+ `BN` ,batch normalization;\n+ Early Stopping(提前终止训练)。\n\n### 16.2，如何判断深度学习模型是否过拟合\n\n1. 首先将训练数据划分为训练集和验证集，`80%` 用于训练集，`20%` 用于验证集（训练集和验证集一定不能相交）；训练都时候每隔一定 `Epoch` 比较验证集但指标和训练集是否一致，如果不一致，并且变坏了，那么意味着过拟合。\n2. 用学习曲线 `learning curve` 来判别过拟合，参考此[博客](https://blog.csdn.net/aliceyangxi1987/article/details/73598857 \"博客\")。\n\n### 16.3，欠拟合怎么解决\n\n`underfitting` 欠拟合的表现就是模型不收敛，原因有很多种，这里以神经网络拟合能力不足问题给出以下参考解决方法：\n\n+ 寻找最优的权重初始化方案：如 He正态分布初始化 `he_normal`，深度学习框架都内置了很多权重初始化方法；\n+ 使用适当的激活函数：卷积层的输出使用的激活函数一般为 `ReLu`，循环神经网络中的循环层使用的激活函数一般为 `tanh`，或者 `ReLu`；\n+ 选择合适的优化器和学习速率：`SGD` 优化器速度慢但是会达到最优.\n\n### 16.4，如何判断模型是否欠拟合\n\n神级网络欠拟合的特征就是模型训练了**足够长**但时间后, `loss` 值依然很大甚至与初始值没有太大区别，且精度很低，测试集亦如此。根据我的总结，原因一般有以下几种：\n\n+ 神经网络的拟合能力不足；\n+ 网络配置的问题；\n+ 数据集配置的问题；\n+ 训练方法出错（初学者经常碰到，原因千奇百怪）。\n\n## 十七，L1 和 L2 区别\n\nL1 范数（`L1 norm`）是指向量中各个元素绝对值之和，也有个美称叫“稀疏规则算子”（Lasso regularization）。 比如 向量 A=[1，-1，3]， 那么 A 的 L1 范数为 |1|+|-1|+|3|。简单总结一下就是：\n\n+ L1 范数: 为向量 x 各个元素绝对值之和。\n+ L2 范数: 为向量 x 各个元素平方和的 1/2 次方，L2 范数又称 Euclidean 范数或 Frobenius 范数\n+ Lp 范数: 为向量 x 各个元素绝对值 $p$ 次方和的 $1/p$ 次方.\n\n在支持向量机学习过程中，L1 范数实际是一种对于成本函数求解最优的过程，因此，L1 范数正则化通过向成本函数中添加 L1 范数，使得学习得到的结果满足稀疏化，从而方便人类提取特征。\n\n`L1` 范数可以使权值参数稀疏，方便特征提取。 L2 范数可以防止过拟合，提升模型的泛化能力。\n\n## 十八，TensorFlow计算图概念\n\n`Tensorflow` 是一个通过计算图的形式来表述计算的编程系统，计算图也叫数据流图，可以把计算图看做是一种有向图，Tensorflow 中的每一个计算都是计算图上的一个节点，而节点之间的边描述了计算之间的依赖关系。\n\n## 十九，BN（批归一化）的作用\n\n在神经网络中间层也进行归一化处理，使训练效果更好的方法，就是批归一化Batch Normalization（BN）。\n\n(1). **可以使用更高的学习率**。如果每层的 `scale` 不一致，实际上每层需要的学习率是不一样的，同一层不同维度的 scale 往往也需要不同大小的学习率，通常需要使用最小的那个学习率才能保证损失函数有效下降，Batch Normalization 将每层、每维的 scale 保持一致，那么我们就可以直接使用较高的学习率进行优化。\n\n(2). **移除或使用较低的 `dropout`**。 dropout 是常用的防止 overfitting 的方法，而导致 overfitting 的位置往往在数据边界处，如果初始化权重就已经落在数据内部，overfitting现象就可以得到一定的缓解。论文中最后的模型分别使用10%、5%和0%的dropout训练模型，与之前的 40%-50% 相比，可以大大提高训练速度。\n\n(3). **降低 L2 权重衰减系数**。 还是一样的问题，边界处的局部最优往往有几维的权重（斜率）较大，使用 L2 衰减可以缓解这一问题，现在用了 Batch Normalization，就可以把这个值降低了，论文中降低为原来的 5 倍。\n\n(4). **代替Local Response Normalization层**。 由于使用了一种 Normalization，再使用 LRN 就显得没那么必要了。而且 LRN 实际上也没那么 work。\n\n(5). **Batch Normalization调整了数据的分布，不考虑激活函数，它让每一层的输出归一化到了均值为0方差为1的分布**，这保证了梯度的有效性，可以解决反向传播过程中的梯度问题。目前大部分资料都这样解释，比如 BN 的原始论文认为的缓解了 Internal Covariate Shift(ICS) 问题。\n\n关于训练阶段和推理阶段 `BN` 的不同可以参考 [Batch Normalization详解](https://www.cnblogs.com/shine-lee/p/11989612.html \"Batch Normalization详解\") 。\n\n## 二十，什么是梯度消失和爆炸\n\n+ 梯度消失是指在深度学习训练的过程中，梯度随着 `BP` 算法中的链式求导逐层传递逐层减小，最后趋近于0，导致对某些层的训练失效；\n+ 梯度爆炸与梯度消失相反，梯度随着 `BP` 算法中的链式求导逐层传递逐层增大，最后趋于无穷，导致某些层无法收敛；\n\n> 在反向传播过程中需要对激活函数进行求导，如果导数大于 1，那么随着网络层数的增加，梯度更新将会朝着指数爆炸的方式增加这就是梯度爆炸。同样如果导数小于 1，那么随着网络层数的增加梯度更新信息会朝着指数衰减的方式减少这就是梯度消失。\n\n### 梯度消失和梯度爆炸产生的原因\n\n**出现梯度消失和梯度爆炸的问题主要是因为参数初始化不当以及激活函数选择不当造成的**。其根本原因在于反向传播训练法则，属于先天不足。当训练较多层数的模型时，一般会出现梯度消失问题（gradient vanishing problem）和梯度爆炸问题（gradient exploding problem）。注意在反向传播中，当网络模型层数较多时，梯度消失和梯度爆炸是不可避免的。\n\n**深度神经网络中的梯度不稳定性，根本原因在于前面层上的梯度是来自于后面层上梯度的乘积**。当存在过多的层次时，就出现了内在本质上的不稳定场景。前面的层比后面的层梯度变化更小，故变化更慢，故引起了梯度消失问题。前面层比后面层梯度变化更快，故引起梯度爆炸问题。\n\n### 如何解决梯度消失和梯度爆炸问题\n\n常用的有以下几个方案：\n\n+ 预训练模型 + 微调\n+ 梯度剪切 + 正则化\n+ `relu、leakrelu` 等激活函数\n+ `BN` 批归一化\n+ 参数标准初始化函数如 `xavier`\n+ `CNN` 中的残差结构\n+ `LSTM` 结构\n\n## 二十一，RNN循环神经网络理解\n\n循环神经网络（recurrent neural network, RNN）, 主要应用在语音识别、语言模型、机器翻译以及时序分析等问题上。\n**在经典应用中，卷积神经网络在不同的空间位置共享参数，循环神经网络是在不同的时间位置共享参数，从而能够使用有限的参数处理任意长度的序列。**\n\n`RNN` 可以看做作是同一神经网络结构在时间序列上被复制多次的结果，这个被复制多次的结构称为循环体，如何设计循环体的网络结构是 RNN 解决实际问题的关键。RNN 的输入有两个部分，一部分为上一时刻的状态，另一部分为当前时刻的输入样本。\n\n## 二十二，训练过程中模型不收敛，是否说明这个模型无效，导致模型不收敛的原因\n\n不一定。导致模型不收敛的原因有很多种可能，常见的有以下几种：\n\n+ 没有对数据做归一化。\n+ 没有检查过你的结果。这里的结果包括预处理结果和最终的训练测试结果。\n+ 忘了做数据预处理。\n+ 忘了使用正则化。\n+ Batch Size 设的太大。\n+ 学习率设的不对。\n+ 最后一层的激活函数用的不对。\n+ 网络存在坏梯度。比如 ReLu 对负值的梯度为 0，反向传播时，0 梯度就是不传播。\n+ 参数初始化错误。\n+ 网络太深。隐藏层神经元数量错误。\n+ 更多回答，参考此[链接](http://theorangeduck.com/page/neural-network-not-working \"链接\")。\n\n## 二十三，VGG 使用 2 个 3*3 卷积的优势\n\n(1). **减少网络层参数**。用两个 3\\*3 卷积比用 1 个 5\\*5 卷积拥有更少的参数量，只有后者的 2∗3∗3/5∗5=0.72。但是起到的效果是一样的，两个 3×3 的卷积层串联相当于一个 5×5 的卷积层，感受野的大小都是 5×5，即 1 个像素会跟周围 5×5 的像素产生关联。把下图当成动态图看，很容易看到两个 3×3 卷积层堆叠（没有空间池化）有 5×5 的有效感受野。\n\n![2个３*3卷积层](../data/images/dl_basic/3x3conv.png)\n\n(2). **更多的非线性变换**。2 个 3×3 卷积层拥有比 1 个 5×5 卷积层更多的非线性变换（前者可以使用两次 ReLU 激活函数，而后者只有一次），使得卷积神经网络对特征的学习能力更强。\n\n***paper中给出的相关解释***：三个这样的层具有 7×7 的有效感受野。那么我们获得了什么？例如通过使用三个 3×3 卷积层的堆叠来替换单个 7×7 层。首先，我们结合了三个非线性修正层，而不是单一的，这使得决策函数更具判别性。其次，我们减少参数的数量：假设三层 3×3 卷积堆叠的输入和输出有 C 个通道，堆叠卷积层的参数为 3×(3×3C) = `27C` 个权重；同时，单个 7×7 卷积层将需要 7×7×C = `49C` 个参数，即参数多 81％。这可以看作是对 7×7 卷积滤波器进行正则化，迫使它们通过 3×3 滤波器（在它们之间注入非线性）进行分解。\n\n**此回答可以参考 TensorFlow 实战 p110，网上很多回答都说的不全**。\n\n### 23.1，1*1 卷积的主要作用\n\n+ **降维（ dimension reductionality ）**。比如，一张500 \\* 500且厚度depth为100 的图片在20个filter上做1\\*1的卷积，那么结果的大小为500\\*500\\*20。\n+ **加入非线性**。卷积层之后经过激励层，1\\*1的卷积在前一层的学习表示上添加了非线性激励（ non-linear activation ），提升网络的表达能力；\n\n## 二十四，Relu比Sigmoid效果好在哪里？\n\n`Sigmoid` 函数公式如下：\n$\\sigma (x)=\\frac{1}{1+exp(-x)}$\n\nReLU激活函数公式如下：\n$$f(x) = max(x, 0) = \n\\begin{cases}\nx,  & \\text{if $x$ $\\geq$ 0} \\\\\n0, & \\text{if $x$ < 0}\n\\end{cases}$$\n\nReLU 的输出要么是 0, 要么是输入本身。虽然方程简单，但实际上效果更好。在网上看了很多版本的解释，有从程序实例分析也有从数学上分析，我找了个相对比较直白的回答，如下：\n\n1. `ReLU` 函数计算简单，可以减少很多计算量。反向传播求误差梯度时，涉及除法，计算量相对较大，采用 `ReLU` 激活函数，可以节省很多计算量；\n2. **避免梯度消失问题**。对于深层网络，`sigmoid` 函数反向传播时，很容易就会出现梯度消失问题（在sigmoid接近饱和区时，变换太缓慢，导数趋于 0，这种情况会造成信息丢失），从而无法完成深层网络的训练。\n3. 可以缓解过拟合问题的发生。`ReLU` 会使一部分神经元的输出为 0，这样就造成了网络的稀疏性，并且减少了参数的相互依存关系，缓解了过拟合问题的发生。\n4. 相比 `sigmoid` 型函数，`ReLU` 函数有助于随机梯度下降方法收敛。\n\n### 参考链接\n\n[ReLU为什么比Sigmoid效果好](https://www.twblogs.net/a/5c2dd30fbd9eee35b21c4337/zh-cn \"ReLU为什么比Sigmoid效果好\")\n\n## 二十五，神经网络中权值共享的理解\n\n权值(权重)共享这个词是由 LeNet5 模型提出来的。以 CNN 为例，在对一张图偏进行卷积的过程中，使用的是同一个卷积核的参数。\n比如一个 3×3×1 的卷积核，这个卷积核内 9 个的参数被整张图共享，而不会因为图像内位置的不同而改变卷积核内的权系数。说的再直白一些，就是用一个卷积核不改变其内权系数的情况下卷积处理整张图片（当然CNN中每一层不会只有一个卷积核的，这样说只是为了方便解释而已）。\n\n### 参考资料\n\n[如何理解CNN中的权值共享](https://blog.csdn.net/chaipp0607/article/details/73650759 \"如何理解CNN中的权值共享\")\n\n## 二十六，对 fine-tuning(微调模型的理解)，为什么要修改最后几层神经网络权值？\n\n使用预训练模型的好处，在于利用训练好的SOTA模型权重去做特征提取，可以节省我们训练模型和调参的时间。\n\n至于为什么只微调最后几层神经网络权重，是因为：\n\n(1). CNN 中更靠近底部的层（定义模型时先添加到模型中的层）编码的是更加通用的可复用特征，而更靠近顶部的层（最后添加到模型中的层）编码的是更专业业化的特征。微调这些更专业化的特征更加有用，它更代表了新数据集上的有用特征。\n(2). 训练的参数越多，过拟合的风险越大。很多SOTA模型拥有超过千万的参数，在一个不大的数据集上训练这么多参数是有过拟合风险的，除非你的数据集像Imagenet那样大。\n\n### 参考资料\n\nPython深度学习p127.\n\n## 二十七，什么是 dropout?\n\n+ dropout可以防止过拟合，dropout简单来说就是：我们在前向传播的时候，**让某个神经元的激活值以一定的概率 p 停止工作**，这样可以使模型的泛化性更强，因为它不会依赖某些局部的特征。\n+ `dropout`效果跟`bagging`效果类似（bagging是减少方差variance，而boosting是减少偏差bias）\n+ 加入dropout会使神经网络训练时间边长，模型预测时不需要dropout，记得关掉。\n\n![dropou直观展示](../data/images/dl_basic/dropout.jpg)\n\n### 27.1，dropout具体工作流程\n\n以 标准神经网络为例，正常的流程是：我们首先把输入数据x通过网络前向传播，然后把误差反向传播一决定如何更新参数让网络进行学习。使用dropout之后，过程变成如下：\n\n1，首先随机（临时）删掉网络中一半的隐藏神经元，输入输出神经元保持不变（图3中虚线为部分临时被删除的神经元）；\n\n2，然后把输入x通过修改后的网络进行前向传播计算，然后把得到的损失结果通过修改的网络反向传播。一小批训练样本执行完这个过程后，在没有被删除的神经元上按照随机梯度下降法更新对应的参数（w，b）；\n\n3，然后重复这一过程：\n\n+ 恢复被删掉的神经元（此时被删除的神经元保持原样没有更新w参数，而没有被删除的神经元已经有所更新）;\n+ 从隐藏层神经元中随机选择一个一半大小的子集临时删除掉（同时备份被删除神经元的参数）;\n+ 对一小批训练样本，先前向传播然后反向传播损失并根据随机梯度下降法更新参数（w，b） （没有被删除的那一部分参数得到更新，删除的神经元参数保持被删除前的结果）。\n\n### 27.2，dropout在神经网络中的应用\n\n(1). 在训练模型阶段\n\n不可避免的，在训练网络中的每个单元都要添加一道概率流程，标准网络和带有dropout网络的比较图如下所示：\n\n![dropout在训练阶段](../data/images/dl_basic/dropout2.png)\n\n(2). 在测试模型阶段\n\n预测模型的时候，输入是当前输入，每个神经单元的权重参数要乘以概率p。\n\n![dropout在测试模型时](../data/images/dl_basic/infer_dropout.jpg)\n\n### 27.3，如何选择dropout 的概率\n\ninput 的 dropout 概率推荐是 0.8， hidden layer 推荐是0.5， 但是也可以在一定的区间上取值。（All dropout nets use p = 0.5 for hidden units and p = 0.8 for input units.）\n\n### 参考资料\n\n1. [Dropout:A Simple Way to Prevent Neural Networks from Overfitting]\n2. [深度学习中Dropout原理解析](https://zhuanlan.zhihu.com/p/38200980 \"深度学习中Dropout原理解析\")\n\n## 二十八，HOG 算法原理描述\n\n**方向梯度直方图（Histogram of Oriented Gradient, HOG）特征**是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。它通过计算和统计图像局部区域的梯度方向直方图来构成特征。在深度学习取得成功之前，Hog特征结合SVM分类器被广泛应用于图像识别中，在行人检测中获得了较大的成功。\n\n### HOG特征原理\n\n`HOG` 的核心思想是所检测的局部物体外形能够被光强梯度或边缘方向的分布所描述。通过将整幅图像分割成小的连接区域（称为cells），每个 `cell` 生成一个方向梯度直方图或者 cell 中pixel 的边缘方向，这些直方图的组合可表示（所检测目标的目标）描述子。\n\n为改善准确率，局部直方图可以通过计算图像中一个较大区域(称为block)的光强作为 `measure` 被对比标准化，然后用这个值(measure)归一化这个 block 中的所有 cells 。这个归一化过程完成了更好的照射/阴影不变性。与其他描述子相比，HOG 得到的描述子保持了几何和光学转化不变性（除非物体方向改变）。因此HOG描述子尤其适合人的检测。\n\nHOG 特征提取方法就是将一个image：\n\n1. 灰度化（将图像看做一个x,y,z（灰度）的三维图像）\n2. 划分成小 `cells`（2*2）\n3. 计算每个 cell 中每个 pixel 的 gradient（即 orientation）\n4. 统计每个 cell 的梯度直方图（不同梯度的个数），即可形成每个 cell 的 descriptor。\n\n### HOG特征检测步骤\n\n![HOG特征检测步骤](../data/images/dl_basic/hog_process.jpg)\n\n**总结**：颜色空间归一化——>梯度计算——>梯度方向直方图——>重叠块直方图归一化———>HOG特征\n\n### 参考资料\n[HOG特征检测－简述](https://blog.csdn.net/liyuqian199695/article/details/53835989 \"HOG特征检测－简述\")\n\n## 二十九，激活函数\n\n### 29.1，激活函数的作用\n\n+ **激活函数实现去线性化**。激活函数的引入是为了增加整个网络的表达能力（引入非线性因素），否则若干线形层的堆叠仍然只能起到线性映射的作用，无法形成复杂的函数。如果将每一个神经元（也就是神经网络的节点）的输出通过一个非线性函数，那么整个神经网络的模型也就不再是线性的了，这个非线性函数就是激活函数。\n+ 激活函数可以把**当前特征空间通过一定的线性映射转换到另一个空间**，让数据能够更好的被分类。\n+ 激活函数对模型学习、理解非常复杂和非线性的函数具有重要作用。\n\n### 29.2，常见的激活函数\n\n激活函数也称非线性映射函数，常见的激活函数有：`ReLU` 函数、`sigmoid` 函数、`tanh` 函数等，其计算公式如下：\n\n+ ReLU函数：$f(x)=max(x,0)$\n+ sigmoid函数：$f(x)=\\frac{1}{1+e^{-x}}$\n+ tanh函数：$f(x)=\\frac{1+e^{-2x}}{1+e^{-2x}}=\\frac{2}{1+e^{-2x}}-1 = 2sigmoid(2x)-1$\n\n### 29.3，激活函数理解及函数梯度图\n\n1. `Sigmoid` 激活函数，可用作分类任务的输出层。可以看出经过sigmoid激活函数后，模型输出的值域被压缩到 `0到1` 区间（这和概率的取值范围一致），这正是分类任务中sigmoid很受欢迎的原因。\n\n![sigmoid激活函数及梯度图](../data/images/dl_basic/sigmoid_act.png)\n\n1. `tanh(x)` 型函数是在 `sigmoid` 型函数基础上为解决均值问题提出的激活函数。tanh 的形状和 sigmoid 类似，只不过tanh将“挤压”输入至区间(-1, 1)。至于梯度，它有一个大得多的峰值1.0（同样位于z = 0处），但它下降得更快，当|x|的值到达 3 时就已经接近零了。这是所谓梯度消失（vanishing gradients）问题背后的原因，会导致网络的训练进展变慢。\n\n![tanh函数及梯度图](../data/images/dl_basic/tanh_and_gradient.png)\n\n1. `ReLU` 处理了sigmoid、tanh中常见的梯度消失问题，同时也是计算梯度最快的激励函数。但是，ReLU函数也有自身缺陷，即在 x < 0 时，梯度便为 y。换句话说，对于小于 y 的这部分卷积结果响应，它们一旦变为负值将再无法影响网络训练——这种现象被称作“死区\"。\n\n![Relu函数及梯度图](../data/images/dl_basic/relu_and_gradient.jpg)\n\n## 三十，卷积层和池化层有什么区别\n\n1. **卷积层有参数，池化层没有参数**；\n2. **经过卷积层节点矩阵深度会改变**。池化层不会改变节点矩阵的深度，但是它可以缩小节点矩阵的大小。\n\n## 三十一，卷积层和池化层参数量计算\n\n参考 [神经网络模型复杂度分析](神经网络模型复杂度分析.md \"神经网络模型复杂度分析\") 文章。\n\n## 三十二，神经网络为什么用交叉熵损失函数\n\n判断一个输出向量和期望的向量有多接近，交叉熵（`cross entroy`）是常用的评判方法之一。交叉熵刻画了两个概率分布之间的距离，是分类问题中使用比较广泛的一种损失函数。给定两个概率分布 `p` 和 `q` ，通过 `q` 来表示 `p` 的交叉熵公式为：$H(p,q)=−∑p(x)logq(x)$\n\n## 三十三，数据增强方法有哪些\n\n常用数据增强方法：\n\n+ 翻转：`Fliplr,Flipud`。不同于旋转180度，这是类似镜面的翻折，跟人在镜子中的映射类似，常用水平、上下镜面翻转。\n+ 旋转：`rotate`。顺时针/逆时针旋转，最好旋转 `90-180` 度，否则会出现边缘缺失或者超出问题，如旋转 `45` 度。\n+ 缩放：`zoom`。图像可以被放大或缩小，imgaug库可用Scal函数实现。\n+ 裁剪：`crop`。一般叫随机裁剪，操作步骤是：随机从图像中选择一部分，然后降这部分图像裁剪出来，然后调整为原图像的大小。根本上理解，图像crop就是指从图像中移除不需要的信息，只保留需要的部分\n+ 平移：`translation`。平移是将图像沿着x或者y方向（或者两个方向）移动。我们在平移的时候需对背景进行假设，比如说假设为黑色等等，因为平移的时候有一部分图像是空的，由于图片中的物体可能出现在任意的位置，所以说平移增强方法十分有用。\n+ 放射变换：`Affine`。包含：平移(`Translation`)、旋转(`Rotation`)、放缩(`zoom`)、错切(`shear`)。\n+ 添加噪声：过拟合通常发生在神经网络学习高频特征的时候，为消除高频特征的过拟合，可以随机加入噪声数据来消除这些高频特征。`imgaug` 图像增强库使用 `GaussianBlur`函数。\n+ 亮度、对比度增强：这是图像色彩进行增强的操作\n+ 锐化：`Sharpen`。imgaug库使用Sharpen函数。\n\n### 33.1，离线数据增强和在线数据增强有什么区别?\n\n数据增强分两类，一类是**离线增强**，一类是**在线增强**：\n\n1. 离线增强 ： 直接对硬盘上的数据集进行处理，并保存增强后的数据集文件。数据的数目会变成增强因子 x 原数据集的数目 ，这种方法常常用于数据集很小的时候\n2. 在线增强 ： 这种增强的方法用于，获得 batch 数据之后，然后对这个batch的数据进行增强，如旋转、平移、翻折等相应的变化，由于有些数据集不能接受线性级别的增长，这种方法长用于大的数据集，很多机器学习框架已经支持了这种数据增强方式，并且可以使用GPU优化计算。\n`\n### Reference\n\n[深度学习中的数据增强](https://blog.csdn.net/zhelong3205/article/details/81810743 \"深度学习中的数据增强\")\n\n## 三十四，ROI Pooling替换为ROI Align的效果，及各自原理\n\n`faster rcnn` 将 `roi pooling` 替换为 `roi align` 效果有所提升。\n\n### ROI Pooling原理\n\n`RPN`　生成的 `ROI` 区域大小是对应与输入图像大小（每个roi区域大小各不相同），为了能够共享权重，所以需要将这些 `ROI`　映射回特征图上，并固定大小。ROI Pooling操作过程如下图：\n\n![roi pooling](../data/images/dl_basic/roi_pooling.png)\n\n**`ROI Pooling` 具体操作如下：**\n\n1. `Conv layers` 使用的是 `VGG16`，`feat_stride=32`(即表示，经过网络层后图片缩小为原图的 1/32),原图 $800\\times 800$,最后一层特征图 `feature map` 大小: $25\\times 25$\n2. 假定原图中有一 `region proposal`，大小为 $665\\times 665$，这样，映射到特征图中的大小：$665/32=20.78$，即大小为 $20.78\\times 20.78$，源码中，在计算的时候会进行取整操作，于是，进行所谓的第一次量化，即映射的特征图大小为20*20；\n3. 假定 `pooled_w=7、pooled_h=7`，即 `pooling` 后固定成 $7\\times 7$ 大小的特征图，所以，将上面在 `feature map`上映射的 $20\\times 20$ 的 `region proposal` 划分成 `49`个同等大小的小区域，每个小区域的大小 $20/7=2.86$,，即 $2.86\\times 2.86$，此时，进行第二次量化，故小区域大小变成 $2\\times 2$；\n4. 每个 $2\\times 2$ 的小区域里，取出其中最大的像素值，作为这一个区域的‘代表’，这样，49 个小区域就输出 49 个像素值，组成 $7\\times 7$ 大小的 `feature map`。\n\n所以，通过上面可以看出，经过**两次量化，即将浮点数取整**，原本在特征图上映射的 $20\\times 20$ 大小的 `region proposal`，偏差成大小为 $7\\times 7$ 的，这样的像素偏差势必会对后层的回归定位产生影响。所以，产生了后面的替代方案: `RoiAlign`。\n\n### ROI Align原理\n\n`ROI Align` 的工作原理如下图。\n\n![roi align](../data/images/dl_basic/roi_align.png)\n\n`ROI Align` 是在 `Mask RCNN` 中使用以便使生成的候选框`region proposal` 映射产生固定大小的 `feature map` 时提出的，根据上图，有着类似的映射：\n\n1. `Conv layers` 使用的是 `VGG16`，`feat_stride=32` (即表示，经过网络层后图片缩小为原图的1/32),原图 $800\\times 800$，最后一层特征图feature map大小: $25*\\times 25$;\n2. 假定原图中有一region proposal，大小为 $665*\\times 665$，这样，映射到特征图中的大小：$665/32=20.78$，即大小为 $20.78\\times 20.78$，此时，没有像 `RoiPooling` 那样就行取整操作，保留浮点数;\n3. 假定 `pooled_w=7,pooled_h=7`，即 `pooling` 后固定成 $7\\times 7$ 大小的特征图，所以，将在 `feature map` 上映射的 $20.78\\times 20.78$ 的 `region proposal` 划分成 49 个同等大小的小区域，每个小区域的大小 $20.78/7=2.97$,即 $2.97\\times 2.97$;\n4. 假定采样点数为 `4`，即表示，对于每个 $2.97\\times 2.97$ 的小区域，平分四份，每一份取其中心点位置，而中心点位置的像素，采用双线性插值法进行计算，这样，就会得到四个点的像素值，如下图:\n\n![roi align双线性插值](../data/images/dl_basic/roi_align_linear_interpolation.png)\n\n上图中，四个红色叉叉`‘×’`的像素值是通过双线性插值算法计算得到的。\n\n最后，取四个像素值中最大值作为这个小区域(即：$2.97\\times 2.97$ 大小的区域)的像素值，如此类推，同样是 `49` 个小区域得到 `49` 个像素值，组成 $7\\times 7$ 大小的 `feature map`。\n\n### RoiPooling 和 RoiAlign 总结\n\n总结：知道了 `RoiPooling` 和 `RoiAlign` 实现原理，在以后的项目中可以根据实际情况进行方案的选择；对于检测图片中大目标物体时，两种方案的差别不大，而如果是图片中有较多小目标物体需要检测，则优先选择 `RoiAlign`，更精准些。\n\n### Reference\n[RoIPooling、RoIAlign笔记](https://www.cnblogs.com/wangyong/p/8523814.html \"RoIPooling、RoIAlign笔记\")\n\n## 三十五，CNN的反向传播算法推导\n\n+ [四张图彻底搞懂CNN反向传播算法（通俗易懂）](https://zhuanlan.zhihu.com/p/81675803 \"四张图彻底搞懂CNN反向传播算法（通俗易懂）\")\n+ [反向传播算法推导过程（非常详细）](https://zhuanlan.zhihu.com/p/79657669 \"反向传播算法推导过程（非常详细）\")\n\n## 三十六，Focal Loss 公式\n\n为了解决正负样本不平衡的问题，我们通常会在交叉熵损失的前面加上一个参数 $\\alpha$ ，即:\n$$\nCE = \\left\\{\\begin{matrix}\n-\\alpha log(p), & if \\quad y=1\\\\ \n-(1-\\alpha)log(1-p), &  if\\quad y=0\n\\end{matrix}\\right.\n$$\n\n尽管 $\\alpha$  平衡了正负样本，但对难易样本的不平衡没有任何帮助。因此，`Focal Loss` 被提出来了，即：\n\n$$\nCE = \\left\\{\\begin{matrix}\n-\\alpha (1-p)^\\gamma log(p), & if \\quad y=1\\\\ \n-(1-\\alpha) p^\\gamma log(1-p), &  if\\quad y=0\n\\end{matrix}\\right.\n$$\n\n实验表明 $\\gamma$ 取 `2`, $\\alpha$ 取 `0.25` 的时候效果最佳。\n\n+ `Focal loss` 成功地解决了在单阶段目标检测时，**正负样本区域极不平衡**而目标检测 `loss` 易被大批量负样本所左右的问题。\n+ `RetinaNet` 达到更高的精度的原因不是网络结构的创新，而是损失函数的创新！\n\n## 三十七，快速回答\n\n### 37.1，为什么 Faster RCNN、Mask RCNN 需要使用 ROI Pooling、ROI Align?\n\n为了使得最后面的两个全连接层能够共享 `conv layers(VGG/ResNet)` 权重。在所有的 `RoIs` 都被 `pooling` 成（512×7×7）的`feature map`后，将它 `reshape` 成一个一维的向量，就可以利用 `VGG16` 的预训练的权重来初始化前两层全连接。\n\n### 37.2，softmax公式\n\n$$softmax(y)_{i} = \\frac{e^{y_{i}}}{\\sum_{j=1}^{n}e^{y_j}}$$\n\n### 37.3，上采样方法总结\n\n上采样大致被总结成了三个类别：\n\n1. 基于线性插值的上采样：最近邻算法（`nearest`）、双线性插值算法（`bilinear`）、双三次插值算法（`bicubic`）等，这是传统图像处理方法。\n2. 基于深度学习的上采样（转置卷积，也叫反卷积 `Conv2dTranspose2d`等）\n3. `Unpooling` 的方法（简单的补零或者扩充操作）\n\n> 计算效果：最近邻插值算法 < 双线性插值 < 双三次插值。计算速度：最近邻插值算法 > 双线性插值 > 双三次插值\n\n### 37.4，移动端深度学习框架知道哪些，用过哪些？\n\n知名的有TensorFlow Lite、小米MACE、腾讯的 ncnn 等，目前都没有用过。\n\n### 37.5，如何提升网络的泛化能力\n\n和防止模型过拟合的方法类似，另外还有模型融合方法。\n\n### 37.6，BN算法，为什么要在后面加伽马和贝塔，不加可以吗？\n\n最后的 `scale and shift` 操作则是为了让因训练所需而“刻意”加入的BN能够有可能还原最初的输入。不加也可以。\n\n### 37.7，验证集和测试集的作用\n+ 验证集是在训练过程中用于检验模型的训练情况，**从而确定合适的超参数**；\n+ 测试集是在模型训练结束之后，**测试模型的泛化能力**。\n\n## 三十八，交叉验证的理解和作用\n\n参考知乎文章 [N折交叉验证的作用（如何使用交叉验证）](https://zhuanlan.zhihu.com/p/113623623 \"N折交叉验证的作用（如何使用交叉验证）\")，我的理解之后补充。\n\n## 三十九，介绍一下NMS和IOU的原理\n\nNMS全称是非极大值抑制，顾名思义就是抑制不是极大值的元素。在目标检测任务中，通常在解析模型输出的预测框时，预测目标框会非常的多，其中有很多重复的框定位到了同一个目标，NMS 的作用就是用来除去这些重复框，从而获得真正的目标框。而 NMS 的过程则用到了 IOU，IOU 是一种用于衡量真实和预测之间相关度的标准，相关度越高，该值就越高。IOU 的计算是两个区域重叠的部分除以两个区域的集合部分，简单的来说就是交集除以并集。\n\n在 NMS 中，首先对预测框的置信度进行排序，依次取置信度最大的预测框与后面的框进行 IOU 比较，当 IOU 大于某个阈值时，可以认为两个预测框框到了同一个目标，而置信度较低的那个将会被剔除，依次进行比较，最终得到所有的预测框。\n\n## Reference\n\n1. [《Batch Normalization Accelerating Deep Network Training by Reducing Internal Covariate Shift》阅读笔记与实现](https://blog.csdn.net/happynear/article/details/44238541 \"《Batch Normalization Accelerating Deep Network Training by Reducing Internal Covariate Shift》阅读笔记与实现\")\n2. [详解机器学习中的梯度消失、爆炸原因及其解决方法](https://blog.csdn.net/qq_25737169/article/details/78847691 \"详解机器学习中的梯度消失、爆炸原因及其解决方法\")\n3. [N折交叉验证的作用（如何使用交叉验证）](https://zhuanlan.zhihu.com/p/113623623 \"N折交叉验证的作用（如何使用交叉验证）\")\n4. [5分钟理解Focal Loss与GHM——解决样本不平衡利器](https://zhuanlan.zhihu.com/p/80594704 \"5分钟理解Focal Loss与GHM——解决样本不平衡利器\")\n5. [深度学习CV岗位面试问题总结（目标检测篇）](https://blog.csdn.net/qq_39056987/article/details/112104199 \"深度学习CV岗位面试问题总结（目标检测篇）\")\n"
  },
  {
    "path": "5-computer_vision/2D目标检测/0-目标检测模型的基础.md",
    "content": "- [前言](#前言)\n- [一，anchor box](#一anchor-box)\n- [二，IOU](#二iou)\n- [三，Focal Loss](#三focal-loss)\n  - [3.1，Cross Entropy](#31cross-entropy)\n  - [3.2，Balanced Cross Entropy](#32balanced-cross-entropy)\n  - [3.3，Focal Loss Definition](#33focal-loss-definition)\n- [四，NMS](#四nms)\n  - [4.1，NMS 介绍](#41nms-介绍)\n  - [4.2，NMS 算法过程](#42nms-算法过程)\n- [五，Soft NMS 算法](#五soft-nms-算法)\n- [六，目标检测的不平衡问题](#六目标检测的不平衡问题)\n  - [6.1，介绍](#61介绍)\n- [参考资料](#参考资料)\n\n## 前言\n\n边界框：在⽬标检测领域⾥，我们通常使⽤边界框（`bounding box`，缩写是 `bbox`）来描述⽬标位置。边界框是⼀个矩形框，可以由矩形左上⻆的 `x` 和 `y` 轴坐标与右下⻆的 `x` 和 `y` 轴坐标确定。\n\n检测网络中的一些术语解释：\n\n1. `backbone`：翻译为主干网络，主要指用来做特征提取作用的网络，早期分类网络 `VGG`、`ResNet` 等去掉用于分类的全连接层的部分就是 `backbone` 网络。\n2. `neck`: 指放在 `backbone` 和 `head` 之间的网络，作用是更好的融合/利用 `backbone` 提取的 `feature`，可以理解为特征增强模块，典型的 `neck` 是如 `FPN` 结构。\n3. `head`：检测头，输出想要结果（分类+定位）的网络，放在模型最后。如 `YOLO` 使用特定维度的 `conv` 获取目标的类别和 `bbox` 信息。\n\n## 一，anchor box\n\n⽬标检测算法通常会在输⼊图像中采样⼤量的区域，然后判断这些区域中是否包含我们感兴趣的⽬标，并调整区域边缘从而更准确地预测⽬标的真实边界框（`ground-truth bounding box`）。**不同的模型使⽤的区域采样⽅法可能不同**。两阶段检测模型常用的⼀种⽅法是：**以每个像素为中⼼⽣成多个⼤小和宽⾼⽐（aspect ratio）不同的边界框**。这些边界框被称为锚框（`anchor box`）。\n\n在 `Faster RCNN` 模型中，每个像素都生成 `9` 个大小和宽高比都不同的 `anchors`。在代码中，`anchors` 是一组由 [generate_anchors.py](https://github.com/chenyuntc/simple-faster-rcnn-pytorch/blob/master/model/utils/bbox_tools.py) 生成的矩形框列表。其中每行的 `4` 个值 `(x1,y1,x2,y2)` 表示矩形左上和右下角点坐标。`9` 个矩形共有 `3` 种形状，长宽比为大约为 `{1:1, 1:2, 2:1}` 三种, 实际上通过 `anchors` 就引入了检测中常用到的多尺度方法。`generate_anchors.py` 的代码如下：\n> 注意，这里生成的只是 `base anchors`，其中一个 框的左上角坐标为 (0,0) 坐标（特征图左上角）的 `9` 个 anchor，后续还需网格化（`meshgrid`）生成其他 `anchor`。同一个 scale，但是不同的 anchor ratios 生成的 anchors 面积理论上是要一样的。\n\n```python\nimport numpy as np\nimport six\nfrom six import __init__  # 兼容python2和python3模块\n\n\ndef generate_anchor_base(base_size=16, ratios=[0.5, 1, 2],\n                         anchor_scales=[8, 16, 32]):\n    \"\"\"Generate base anchors by enumerating aspect ratio and scales.\n\n    Args:\n        base_size (number): The width and the height of the reference window.\n        ratios (list of floats): anchor 的宽高比\n        anchor_scales (list of numbers): anchor 的尺度\n\n    Returns: Base anchors in a single-level feature maps.`(R, 4)`.\n        bounding box is `(x_{min}, y_{min}, x_{max}, y_{max})`\n    \"\"\"\n    import numpy as np\n    py = base_size / 2.\n    px = base_size / 2.\n\n    anchor_base = np.zeros((len(ratios) * len(anchor_scales), 4),\n                           dtype=np.float32)\n    for i in six.moves.range(len(ratios)):\n        for j in six.moves.range(len(anchor_scales)):\n            // 乘以感受野值，得到缩放后的 anchor 大小\n            h = base_size * anchor_scales[j] * np.sqrt(ratios[i])\n            w = base_size * anchor_scales[j] * np.sqrt(1. / ratios[i])\n\n            index = i * len(anchor_scales) + j\n            anchor_base[index, 0] = px - w / 2.\n            anchor_base[index, 1] = py - h / 2.\n\n            anchor_base[index, 2] = px + h / 2.\n            anchor_base[index, 3] = py + w / 2.\n    return anchor_base\n\n\n# test\nif __name__ == \"__main__\":\n    bbox_list = generate_anchor_base()\n    print(bbox_list)\n```\n\n程序运行输出如下：\n> \\[[ -82.50967   -37.254833   53.254833   98.50967 ]\n [-173.01933   -82.50967    98.50967   189.01933 ]\n [-354.03867  -173.01933   189.01933   370.03867 ]\n [ -56.        -56.         72.         72.      ]\n [-120.       -120.        136.        136.      ]\n [-248.       -248.        264.        264.      ]\n [ -37.254833  -82.50967    98.50967    53.254833]\n [ -82.50967  -173.01933   189.01933    98.50967 ]\n [-173.01933  -354.03867   370.03867   189.01933 ]]\n\n## 二，IOU\n\n交并比（Intersection-over-Union，`IoU`），目标检测中使用的一个概念，是模型产生的候选框（candidate bound）与原标记框（ground truth bound）的交叠率，即它们的交集与并集的比值。最理想情况是完全重叠，即比值为 `1`。计算公式如下：\n\n![IOU计算公式](../../data/images/IOU计算公式.png)\n\n代码实现如下：\n\n```python\n# _*_ coding:utf-8 _*_\n# 计算iou\n\n\"\"\"\nbbox的数据结构为(xmin,ymin,xmax,ymax)--(x1,y1,x2,y2),\n每个bounding box的左上角和右下角的坐标\n输入：\n    bbox1, bbox2: Single numpy bounding box, Shape: [4]\n输出：\n    iou值\n\"\"\"\nimport numpy as np\nimport cv2\n\ndef iou(bbox1, bbox2):\n    \"\"\"\n    计算两个bbox(两框的交并比)的iou值\n    :param bbox1: (x1,y1,x2,y2), type: ndarray or list\n    :param bbox2: (x1,y1,x2,y2), type: ndarray or list\n    :return: iou, type float\n    \"\"\"\n    if type(bbox1) or type(bbox2) != 'ndarray':\n        bbox1 = np.array(bbox1)\n        bbox2 = np.array(bbox2)\n\n    assert bbox1.size == 4 and bbox2.size == 4, \"bounding box coordinate size must be 4\"\n    xx1 = np.max((bbox1[0], bbox2[0]))\n    yy1 = np.min((bbox1[1], bbox2[1]))\n    xx2 = np.max((bbox1[2], bbox2[2]))\n    yy2 = np.min((bbox1[3], bbox2[3]))\n    bwidth = xx2 - xx1\n    bheight = yy2 - yy1\n    area = bwidth * bheight  # 求两个矩形框的交集\n    union = (bbox1[2] - bbox1[0])*(bbox1[3] - bbox1[1]) + (bbox2[2] - bbox2[0])*(bbox2[3] - bbox2[1]) - area  # 求两个矩形框的并集\n    iou = area / union\n\n    return iou\n\n\nif __name__=='__main__':\n    rect1 = (461, 97, 599, 237)\n    # (top, left, bottom, right)\n    rect2 = (522, 127, 702, 257)\n    iou_ret = round(iou(rect1, rect2), 3) # 保留3位小数\n    print(iou_ret)\n\n    # Create a black image\n    img=np.zeros((720,720,3), np.uint8)\n    cv2.namedWindow('iou_rectangle')\n    \"\"\"\n    cv2.rectangle 的 pt1 和 pt2 参数分别代表矩形的左上角和右下角两个点,\n    coordinates for the bounding box vertices need to be integers if they are in a tuple,\n    and they need to be in the order of (left, top) and (right, bottom). \n    Or, equivalently, (xmin, ymin) and (xmax, ymax).\n    \"\"\"\n    cv2.rectangle(img,(461, 97),(599, 237),(0,255,0),3)\n    cv2.rectangle(img,(522, 127),(702, 257),(0,255,0),3)\n    font  = cv2.FONT_HERSHEY_SIMPLEX\n    cv2.putText(img, 'IoU is ' + str(iou_ret), (341,400), font, 1,(255,255,255),1)\n    cv2.imshow('iou_rectangle', img)\n    cv2.waitKey(0)\n```\n\n代码输出结果如下所示：\n\n![程序运行结果](../../data/images/iou代码输出结果.png)\n\n## 三，Focal Loss\n\n`Focal Loss` 是在二分类问题的交叉熵（`CE`）损失函数的基础上引入的，所以需要先学习下交叉熵损失的定义。\n\n### 3.1，Cross Entropy\n\n在深度学习中我们常使用交叉熵来作为分类任务中训练数据分布和模型预测结果分布间的代价函数。对于同一个离散型随机变量 $\\textrm{x}$ 有两个单独的概率分布 $P(x)$ 和 $Q(x)$，其交叉熵定义为：\n> P 表示真实分布， Q 表示预测分布。\n\n$$H(P,Q) = \\mathbb{E}_{\\textrm{x}\\sim P} log Q(x)= -\\sum_{i}P(x_i)logQ(x_i) \\tag{1} $$\n\n但在实际计算中，我们通常不这样写，因为不直观。在深度学习中，以二分类问题为例，其交叉熵损失（`CE`）函数如下：\n\n$$Loss = L(y, p) = -ylog(p)-(1-y)log(1-p) \\tag{2}$$\n\n其中 $p$ 表示当预测样本等于 $1$ 的概率，则 $1-p$ 表示样本等于 $0$ 的预测概率。因为是二分类，所以样本标签 $y$ 取值为 $\\{1,0\\}$，上式可缩写至如下：\n\n$$CE = \\left\\{\\begin{matrix} -log(p), & if \\quad y=1 \\\\  -log(1-p), &  if\\quad y=0 \\tag{3}  \\end{matrix}\\right. $$\n\n为了方便，用 $p_t$ 代表 $p$，$p_t$ 定义如下：\n\n$$p_t = \\{\\begin{matrix} p, & if \\quad y=1\\\\  1-p, &  if\\quad y=0 \\end{matrix}$$\n\n则$(3)$式可写成：\n\n$$CE(p, y) = CE(p_t) = -log(p_t) \\tag{4}$$\n\n前面的交叉熵损失计算都是针对单个样本的，对于**所有样本**，二分类的交叉熵损失计算如下：\n\n$$L = \\frac{1}{N}(\\sum_{y_i = 1}^{m}-log(p)-\\sum_{y_i = 0}^{n}log(1-p))$$\n\n其中 $m$ 为正样本个数，$n$ 为负样本个数，$N$ 为样本总数，$m+n=N$。当样本类别不平衡时，损失函数 $L$ 的分布也会发生倾斜，如 $m \\ll n$ 时，负样本的损失会在总损失占主导地位。又因为损失函数的倾斜，模型训练过程中也会倾向于样本多的类别，造成模型对少样本类别的性能较差。\n\n再衍生以下，对于**所有样本**，多分类的交叉熵损失计算如下：\n\n$$L = \\frac{1}{N} \\sum_i^N L_i = -\\frac{1}{N}(\\sum_i \\sum_{c=1}^M y_{ic}log(p_{ic})$$\n\n其中，$M$ 表示类别数量，$y_{ic}$ 是符号函数，如果样本 $i$ 的真实类别等于 $c$ 取值 1，否则取值 0; $p_{ic}$ 表示样本 $i$ 预测为类别 $c$ 的概率。\n\n对于多分类问题，交叉熵损失一般会结合 `softmax` 激活一起实现，`PyTorch` 代码如下，代码出自[这里](https://mp.weixin.qq.com/s/FGyV763yIKsXNM40lMO61g)。\n\n```python\n\nimport numpy as np\n\n# 交叉熵损失\nclass CrossEntropyLoss():\n    \"\"\"\n    对最后一层的神经元输出计算交叉熵损失\n    \"\"\"\n    def __init__(self):\n        self.X = None\n        self.labels = None\n    \n    def __call__(self, X, labels):\n        \"\"\"\n        参数：\n            X: 模型最后fc层输出\n            labels: one hot标注，shape=(batch_size, num_class)\n        \"\"\"\n        self.X = X\n        self.labels = labels\n\n        return self.forward(self.X)\n    \n    def forward(self, X):\n        \"\"\"\n        计算交叉熵损失\n        参数：\n            X：最后一层神经元输出，shape=(batch_size, C)\n            label：数据onr-hot标注，shape=(batch_size, C)\n        return：\n            交叉熵loss\n        \"\"\"\n        self.softmax_x = self.softmax(X)\n        log_softmax = self.log_softmax(self.softmax_x)\n        cross_entropy_loss = np.sum(-(self.labels * log_softmax), axis=1).mean()\n        return cross_entropy_loss\n    \n    def backward(self):\n        grad_x =  (self.softmax_x - self.labels)  # 返回的梯度需要除以batch_size\n        return grad_x / self.X.shape[0]\n        \n    def log_softmax(self, softmax_x):\n        \"\"\"\n        参数:\n            softmax_x, 在经过softmax处理过的X\n        return: \n            log_softmax处理后的结果shape = (m, C)\n        \"\"\"\n        return np.log(softmax_x + 1e-5)\n    \n    def softmax(self, X):\n        \"\"\"\n        根据输入，返回softmax\n        代码利用softmax函数的性质: softmax(x) = softmax(x + c)\n        \"\"\"\n        batch_size = X.shape[0]\n        # axis=1 表示在二维数组中沿着横轴进行取最大值的操作\n        max_value = X.max(axis=1)\n        #每一行减去自己本行最大的数字,防止取指数后出现inf，性质：softmax(x) = softmax(x + c)\n        # 一定要新定义变量，不要用-=，否则会改变输入X。因为在调用计算损失时，多次用到了softmax，input不能改变\n        tmp = X - max_value.reshape(batch_size, 1)\n        # 对每个数取指数\n        exp_input = np.exp(tmp)  # shape=(m, n)\n        # 求出每一行的和\n        exp_sum = exp_input.sum(axis=1, keepdims=True)  # shape=(m, 1)\n        return exp_input / exp_sum\n```\n\n### 3.2，Balanced Cross Entropy\n\n对于正负样本不平衡的问题，较为普遍的做法是引入 $\\alpha \\in(0,1)$ 参数来解决，上面公式重写如下：\n\n$$CE(p_t) = -\\alpha log(p_t) = \\left\\{\\begin{matrix} -\\alpha log(p), & if \\quad y=1\\\\  -(1-\\alpha)log(1-p), &  if\\quad y=0 \\end{matrix}\\right.$$\n\n对于所有样本，二分类的平衡交叉熵损失函数如下：\n\n$$L = \\frac{1}{N}(\\sum_{y_i = 1}^{m}-\\alpha log(p)-\\sum_{y_i = 0}^{n}(1 - \\alpha) log(1-p))$$\n\n其中 $\\frac{\\alpha}{1-\\alpha} = \\frac{n}{m}$，即 $\\alpha$ 参数的值是根据正负样本分布比例来决定的，\n\n### 3.3，Focal Loss Definition\n\n虽然 $\\alpha$ 参数平衡了正负样本（`positive/negative examples`），但是它并不能区分难易样本（`easy/hard examples`），而实际上，目标检测中大量的候选目标都是易分样本。这些样本的损失很低，但是由于难易样本数量极不平衡，易分样本的数量相对来讲太多，最终主导了总的损失。而本文的作者认为，易分样本（即，置信度高的样本）对模型的提升效果非常小，模型应该主要关注与那些难分样本（这个假设是有问题的，是 `GHM` 的主要改进对象）\n\n`Focal Loss` 作者建议在交叉熵损失函数上加上一个调整因子（`modulating factor`）$(1-p_t)^\\gamma$，把高置信度 $p$（易分样本）样本的损失降低一些。`Focal Loss` 定义如下：\n\n$$FL(p_t) = -(1-p_t)^\\gamma log(p_t) = \\{\\begin{matrix} -(1-p)^\\gamma log(p), & if \\quad y=1\\\\  -p^\\gamma log(1-p), &  if\\quad y=0 \\end{matrix}$$\n\n`Focal Loss` 有两个性质：\n\n+ 当样本被错误分类且 $p_t$ 值较小时，调制因子接近于 `1`，`loss` 几乎不受影响；当 $p_t$ 接近于 `1`，调质因子（`factor`）也接近于 `0`，**容易分类样本的损失被减少了权重**，整体而言，相当于增加了分类不准确样本在损失函数中的权重。\n+ $\\gamma$ 参数平滑地调整容易样本的权重下降率，当 $\\gamma = 0$ 时，`Focal Loss` 等同于 `CE Loss`。 $\\gamma$ 在增加，调制因子的作用也就增加，实验证明  $\\gamma = 2$ 时，模型效果最好。\n\n直观地说，**调制因子减少了简单样本的损失贡献，并扩大了样本获得低损失的范围**。例如，当$\\gamma = 2$ 时，与 $CE$ 相比，分类为 $p_t = 0.9$ 的样本的损耗将降低 `100` 倍，而当 $p_t = 0.968$ 时，其损耗将降低 `1000` 倍。这反过来又增加了错误分类样本的重要性（对于 $pt≤0.5$ 和 $\\gamma = 2$，其损失最多减少 `4` 倍）。在训练过程关注对象的排序为正难 > 负难 > 正易 > 负易。\n\n![难易正负样本](../../data/images/难易正负样本.jpg)\n\n在实践中，我们常采用带 $\\alpha$ 的 `Focal Loss`：\n\n$$FL(p_t) = -\\alpha (1-p_t)^\\gamma log(p_t)$$\n\n作者在实验中采用这种形式，发现它比非 $\\alpha$ 平衡形式（non-$\\alpha$-balanced）的精确度稍有提高。实验表明 $\\gamma$ 取 2，$\\alpha$ 取 0.25 的时候效果最佳。\n\n网上有各种版本的 `Focal Loss` 实现代码，大多都是基于某个深度学习框架实现的，如 `Pytorch`和 `TensorFlow`，我选取了一个较为清晰的代码作为参考，代码来自 [这里](https://github.com/yatengLG/Retinanet-Pytorch/blob/master/Model/struct/Focal_Loss.py)。\n> 后续有必要自己实现以下，有时间还要去看看 `Caffe` 的实现。\n\n```python\n# -*- coding: utf-8 -*-\n# @Author  : LG\nfrom torch import nn\nimport torch\nfrom torch.nn import functional as F\n\nclass focal_loss(nn.Module):\n    def __init__(self, alpha=0.25, gamma=2, num_classes = 3, size_average=True):\n        \"\"\"\n        focal_loss损失函数, -α(1-yi)**γ *ce_loss(xi,yi)\n        步骤详细的实现了 focal_loss损失函数.\n        :param alpha:   阿尔法α,类别权重.      当α是列表时,为各类别权重,当α为常数时,类别权重为[α, 1-α, 1-α, ....],常用于 目标检测算法中抑制背景类 , retainnet中设置为0.25\n        :param gamma:   伽马γ,难易样本调节参数. retainnet中设置为2\n        :param num_classes:     类别数量\n        :param size_average:    损失计算方式,默认取均值\n        \"\"\"\n        super(focal_loss,self).__init__()\n        self.size_average = size_average\n        if isinstance(alpha,list):\n            assert len(alpha)==num_classes   # α可以以list方式输入,size:[num_classes] 用于对不同类别精细地赋予权重\n            print(\" --- Focal_loss alpha = {}, 将对每一类权重进行精细化赋值 --- \".format(alpha))\n            self.alpha = torch.Tensor(alpha)\n        else:\n            assert alpha<1   #如果α为一个常数,则降低第一类的影响,在目标检测中为第一类\n            print(\" --- Focal_loss alpha = {} ,将对背景类进行衰减,请在目标检测任务中使用 --- \".format(alpha))\n            self.alpha = torch.zeros(num_classes)\n            self.alpha[0] += alpha\n            self.alpha[1:] += (1-alpha) # α 最终为 [ α, 1-α, 1-α, 1-α, 1-α, ...] size:[num_classes]\n\n        self.gamma = gamma\n\n    def forward(self, preds, labels):\n        \"\"\"\n        focal_loss损失计算\n        :param preds:   预测类别. size:[B,N,C] or [B,C]    分别对应与检测与分类任务, B 批次, N检测框数, C类别数\n        :param labels:  实际类别. size:[B,N] or [B]，为 one-hot 编码格式\n        :return:\n        \"\"\"\n        # assert preds.dim()==2 and labels.dim()==1\n        preds = preds.view(-1,preds.size(-1))\n        self.alpha = self.alpha.to(preds.device)\n        preds_logsoft = F.log_softmax(preds, dim=1) # log_softmax\n        preds_softmax = torch.exp(preds_logsoft)    # softmax\n\n        preds_softmax = preds_softmax.gather(1,labels.view(-1,1))\n        preds_logsoft = preds_logsoft.gather(1,labels.view(-1,1))\n        self.alpha = self.alpha.gather(0,labels.view(-1))\n        loss = -torch.mul(torch.pow((1-preds_softmax), self.gamma), preds_logsoft)  # torch.pow((1-preds_softmax), self.gamma) 为focal loss中 (1-pt)**γ\n\n        loss = torch.mul(self.alpha, loss.t())\n        if self.size_average:\n            loss = loss.mean()\n        else:\n            loss = loss.sum()\n        return loss\n```\n\n`mmdetection` 框架给出的 `focal loss` 代码如下（有所删减）：\n\n```python\n# This method is only for debugging\ndef py_sigmoid_focal_loss(pred,\n                          target,\n                          weight=None,\n                          gamma=2.0,\n                          alpha=0.25,\n                          reduction='mean',\n                          avg_factor=None):\n    \"\"\"PyTorch version of `Focal Loss <https://arxiv.org/abs/1708.02002>`_.\n    Args:\n        pred (torch.Tensor): The prediction with shape (N, C), C is the\n            number of classes\n        target (torch.Tensor): The learning label of the prediction.\n        weight (torch.Tensor, optional): Sample-wise loss weight.\n        gamma (float, optional): The gamma for calculating the modulating\n            factor. Defaults to 2.0.\n        alpha (float, optional): A balanced form for Focal Loss.\n            Defaults to 0.25.\n        reduction (str, optional): The method used to reduce the loss into\n            a scalar. Defaults to 'mean'.\n        avg_factor (int, optional): Average factor that is used to average\n            the loss. Defaults to None.\n    \"\"\"\n    pred_sigmoid = pred.sigmoid()\n    target = target.type_as(pred)\n    pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)\n    focal_weight = (alpha * target + (1 - alpha) *\n                    (1 - target)) * pt.pow(gamma)\n    loss = F.binary_cross_entropy_with_logits(\n        pred, target, reduction='none') * focal_weigh\n    return loss\n```\n\n## 四，NMS\n\n### 4.1，NMS 介绍\n\n在目标检测中，常会利用非极大值抑制算法(`NMS`，non maximum suppression)对生成的大量候选框进行后处理，去除冗余的候选框，得到最佳检测框（`bbox`），以加快目标检测的效，其本质思想**搜素局部最大值，抑制非极大值**。许多目标检测模型都利用到了 `NMS` 算法，如 DPM，YOLO，SSD，Faster R-CNN 等。`NMS过程`如下图所示：\n\n![NMS过程](../../data/images/NMS过程.png)\n\n以上图为例，每个选出来的 `Bounding Box` 检测框（即 `BBox`）用`（x,y,h,w, confidence score，Pdog,Pcat）`表示，`confidence score` 表示 `background` 和 `foreground` 的置信度得分，取值范围`[0,1]`。Pdog, Pcat 分布代表类别是狗和猫的概率。如果是 100 类的目标检测模型，`BBox` 输出向量为 `5+100=105`。\n\n### 4.2，NMS 算法过程\n\nNMS 的目的就是**除掉重复的边界框**，其主要是通过迭代的形式，不断地以最大得分的框去与其他框做 `IoU` 操作，并过滤那些 `IoU` 较大的框。\n\n**其实现的思想主要是将各个框的置信度进行排序**，然后选择其中置信度最高的框 `A`，同时设置一个阈值，当其他框如 `B` 框 与 `A` 框的重合程度超过阈值就将 `B` 舍弃掉，然后在剩余的框中选择置信度最大的框，重复上述操作。多目标检测的 `NMS` 算法过程如下：\n\n```shell\nfor object in all objects:\n    1. 将所有 bboxs 按照 confidence 排序，并标记当前 confidence 最大的 bbox，即要保留的 bbox；\n    2. 计算当前最大 confidence 对应的 bbox 和剩下所有 bbox 的 IOU；\n    3. 去除 IOU 大于设定阈值的 bbox，得到新的 bboxs；\n    4. 对于新生下来的 bboxs，循环执行步骤 2、3，直到所有的 bbox 都满足要求（即无法再移除 bbox）。\n```\n**nms 的 python 代码如下**：\n\n```python\nimport numpy as np\n\ndef py_nms(bboxs, thresh):\n    \"\"\"Pure Python NMS baseline.注意，这里的计算都是在矩阵层面上计算的\n    greedily select boxes with high confidence and overlap with current maximum <= thresh\n    rule out overlap >= thresh\n    :param bboxs: [[x1, y1, x2, y2 score],] # ndarray, shape(-1,5)\n    :param thresh: retain overlap < thresh\n    :return: indexes to keep\n    \"\"\"\n    if(bboxs) == 0:\n        return [][]\n    bboxs = npa.array(bboxs)\n    # 计算 n 个候选框的面积大小\n    x1 = bboxs[:,0]\n    x2 = bboxs[:, 1]\n    y1 = bboxs[:, 2]\n    y2 = bboxs[:, 3]\n    scores = bboxs[:, 4]\n    areas = (x2 - x1 + 1)*(y2 - y1 + 1)\n\n    # 1，对bboxs 按照置信度排序，获取排序后的下标号，argsort 函数默认从小到大排序\n    order = np.argsort(scores)  # order shape is (4,)\n    picked_bboxs = []\n\n    while order.size > 0:\n        # 1, 保留当前 confidence 最大的 bbox加入到返回框列表中\n        index = order[-1]\n        picked_bboxs.append(bboxs[index]]\n\n        # 2，计算当前 confidence 最大的 bbox 和剩下 bbox 的 IOU\n        xx1 = np.maximum(x1[-1], x1[order[:-1]])\n        xx2 = np.maximum(x2[-1], x2[order[:-1]])\n        yy1 = np.maximum(y1[-1], y1[order[:-1]])\n        yy1 = np.maximum(y2[-1], y2[order[:-1]])\n\n        # 计算相交框的面积,注意矩形框不相交时 w 或 h 算出来会是负数，用0代替\n        w = np.maximum(0.0, xx2 - xx1 + 1)\n        h = np.maximum(0.0, yy2 - yy1 + 1)\n        overlap_area = w * h\n        \n        IOUs = overlap_area/(areas[index] + areas[order[:-1]] - overlap_area)\n\n        # 3，只保留 `IOU` 小于设定阈值的 `bbox`，得到新的 `bboxs`，更新剩下来 bbox的索引\n        remain_index = np.where(IOUs < thresh)  # np.where 来找到符合条件的 index\n        order = order[remain_index]\n    return picked_bboxs\n\n# test\nif __name__ == \"__main__\":\n    bboxs = np.array([[30, 20, 230, 200, 1],\n                     [50, 50, 260, 220, 0.9],\n                     [210, 30, 420, 5, 0.8],\n                     [430, 280, 460, 360, 0.7]])\n    thresh = 0.35\n    keep_bboxs = py_nms(bboxs, thresh)\n    print(keep_bboxs)\n```\n\n程序输出如下：\n> [0, 2, 3]\n[[ 30. 20. 230. 200. 1. ]\n [210. 30. 420. 5. 0.8]\n [430. 280. 460. 360. 0.7]]\n\n**另一个版本的 nms 的 python 代码如下：**\n\n```python\nfrom __future__ import print_function\nimport numpy as np\nimport time\n\ndef intersect(box_a, box_b):\n    max_xy = np.minimum(box_a[:, 2:], box_b[2:])\n    min_xy = np.maximum(box_a[:, :2], box_b[:2])\n    inter = np.clip((max_xy - min_xy), a_min=0, a_max=np.inf)\n    return inter[:, 0] * inter[:, 1]\n\ndef get_iou(box_a, box_b):\n    \"\"\"Compute the jaccard overlap of two sets of boxes.  The jaccard overlap\n    is simply the intersection over union of two boxes.\n    E.g.:\n        A ∩ B / A ∪ B = A ∩ B / (area(A) + area(B) - A ∩ B)\n        The box should be [x1,y1,x2,y2]\n    Args:\n        box_a: Single numpy bounding box, Shape: [4] or Multiple bounding boxes, Shape: [num_boxes,4]\n        box_b: Single numpy bounding box, Shape: [4]\n    Return:\n        jaccard overlap: Shape: [box_a.shape[0], box_a.shape[1]]\n    \"\"\"\n    if box_a.ndim==1:\n        box_a=box_a.reshape([1,-1])\n    inter = intersect(box_a, box_b)\n    area_a = ((box_a[:, 2]-box_a[:, 0]) *\n              (box_a[:, 3]-box_a[:, 1]))  # [A,B]\n    area_b = ((box_b[2]-box_b[0]) *\n              (box_b[3]-box_b[1]))  # [A,B]\n    union = area_a + area_b - inter\n    return inter / union  # [A,B]\n\ndef nms(bboxs,scores,thresh):\n    \"\"\"\n    The box should be [x1,y1,x2,y2]\n    :param bboxs: multiple bounding boxes, Shape: [num_boxes,4]\n    :param scores: The score for the corresponding box\n    :return: keep inds\n    \"\"\"\n    if len(bboxs)==0:\n        return []\n    order=scores.argsort()[::-1]\n    keep=[]\n    while order.size>0:\n        i=order[0]\n        keep.append(i)\n        ious=get_iou(bboxs[order],bboxs[i])\n        order=order[ious<=thresh]\n    return keep\n```\n\n## 五，Soft NMS 算法\n\n`Soft NMS` 算法是对 `NMS` 算法的改进，是发表在 ICCV2017 的论文 中提出的。`NMS` 算法存在一个问题是可能会把一些相邻检测框框给过滤掉（即将 `IOU` 大于阈值的窗口的得分全部置为 `0` ），从而导致目标的 `recall` 指标比较低。而 `Soft NMS` 算法会为相邻检测框设置一个衰减函数而非彻底将其分数置为零。`Soft NMS` 算法流程如下图所示：\n\n![soft nms 算法流程](../../data/images/soft_nms_algorithm.png)\n\n原来的 `NMS` 算法可以通过以下分数重置函数来描述：\n\n![硬NMS算法](../../data/images/硬NMS算法.jpg)\n\n论文对 `NMS` 原有的分数重置函数的改进有两种形式，一种是`线性加权`的。设 $s_i$ 为第 $i$ 个 bbox 的 score, 则在应用 Soft NMS 时各个 bbox score 的计算公式如下：\n\n![线性加权形式的soft NMS算法](../../data/images/线性加权形式的软NMS.jpg)\n\n另一种是`高斯加权`形式的，其不需要设置 iou 阈值 $N_t$。高斯惩罚系数(与上面的线性截断惩罚不同的是, 高斯惩罚会对其他所有的 bbox 作用)，计算公式图如下：\n\n![高斯加权形式的soft NMS算法](../../data/images/高斯加权形式的NMS.jpg)\n\n注意，这两种形式，思想都是 $M$ 为当前得分最高框，$b_{i}$ 为待处理框， $b_{i}$ 和 $M$ 的 IOU 越大，bbox 的得分 $s_{i}$ 就下降的越厉害 ( $N_{t}$ 为给定阈值)。Soft NMS 在每轮迭代时，先选择分数最高的预测框作为 $M$，并对 $B$ 中的每一个检测框 $b_i$ 进行 re-score，得到新的 score，当该框的新 score 低于某设定阈值时，则立即将该框删除。\n\n`soft nms` 的 `python` 代码如下：\n\n```python\ndef soft_nms(bboxes, Nt=0.3, sigma2=0.5, score_thresh=0.3, method=2):\n    # 在 bboxes 之后添加对应的下标[0, 1, 2...], 最终 bboxes 的 shape 为 [n, 5], 前四个为坐标, 后一个为下标\n    res_bboxes = deepcopy(bboxes)\n    N = bboxes.shape[0]  # 总的 box 的数量\n    indexes = np.array([np.arange(N)])  # 下标: 0, 1, 2, ..., n-1\n    bboxes = np.concatenate((bboxes, indexes.T), axis=1)  # concatenate 之后, bboxes 的操作不会对外部变量产生影响\n\n    # 计算每个 box 的面积\n    x1 = bboxes[:, 0]\n    y1 = bboxes[:, 1]\n    x2 = bboxes[:, 2]\n    y2 = bboxes[:, 3]\n    scores = bboxes[:, 4]\n    areas = (x2 - x1 + 1) * (y2 - y1 + 1)\n\n    for i in range(N):\n        # 找出 i 后面的最大 score 及其下标\n        pos = i + 1\n        if i != N - 1:\n            maxscore = np.max(scores[pos:], axis=0)\n            maxpos = np.argmax(scores[pos:], axis=0)\n        else:\n            maxscore = scores[-1]\n            maxpos = 0\n\n        # 如果当前 i 的得分小于后面的最大 score, 则与之交换, 确保 i 上的 score 最大\n        if scores[i] < maxscore:\n            bboxes[[i, maxpos + i + 1]] = bboxes[[maxpos + i + 1, i]]\n            scores[[i, maxpos + i + 1]] = scores[[maxpos + i + 1, i]]\n            areas[[i, maxpos + i + 1]] = areas[[maxpos + i + 1, i]]\n\n        # IoU calculate\n        xx1 = np.maximum(bboxes[i, 0], bboxes[pos:, 0])\n        yy1 = np.maximum(bboxes[i, 1], bboxes[pos:, 1])\n        xx2 = np.minimum(bboxes[i, 2], bboxes[pos:, 2])\n        yy2 = np.minimum(bboxes[i, 3], bboxes[pos:, 3])\n        w = np.maximum(0.0, xx2 - xx1 + 1)\n        h = np.maximum(0.0, yy2 - yy1 + 1)\n        intersection = w * h\n        iou = intersection / (areas[i] + areas[pos:] - intersection)\n\n        # Three methods: 1.linear 2.gaussian 3.original NMS\n        if method == 1:  # linear\n            weight = np.ones(iou.shape)\n            weight[iou > Nt] = weight[iou > Nt] - iou[iou > Nt]\n        elif method == 2:  # gaussian\n            weight = np.exp(-(iou * iou) / sigma2)\n        else:  # original NMS\n            weight = np.ones(iou.shape)\n            weight[iou > Nt] = 0\n\n        scores[pos:] = weight * scores[pos:]\n\n    # select the boxes and keep the corresponding indexes\n    inds = bboxes[:, 5][scores > score_thresh]\n    keep = inds.astype(int)\n\n    return res_bboxes[keep]\n```\n\n## 六，目标检测的不平衡问题\n\n论文 [Imbalance Problems in Object Detection](https://arxiv.org/abs/1909.00169.pdf) 给出了详细的综述。这篇论文主要是系统的分析了目标检测中的不平衡问题，并按照问题进行分类，提出了四类不平衡，并对每个问题现有的解决方案批判性的提出了观点，且给出了一个实时跟踪最新的不平衡问题研究的网页。\n\n### 6.1，介绍\n\n文章指出当有关输入属性的分布影响性能时，就会出现与输入属性相关的不平衡问题。论文将不平衡问题归为四类：\n\n+ `Class imbalance`: 类别不平衡。不同类别的输入边界框的数量不同，包括前景/背景和前景/前景类别的不平衡，`RPN` 和 `Focal Loss` 就是解决这类问题。\n+ `Scale imbalance`: 尺度不平衡，主要是目标边界框的尺度不平衡引起的，也包括将物体分配至 `feature pyramid` 时的不平衡。典型如 `FPN` 就是解决物体多尺度问题的。\n+ `Spatial imbalance`: 空间不平衡。包括不同样本对回归损失贡献的不平衡，`IoU` 分布的不平衡，和目标分布位置的不平衡。\n+ `Objective imbalance`:不同任务（分类、回归）对总损失贡献的不平衡。\n\n## 参考资料\n+ [目标检测算法中检测框合并策略技术综述](https://zhuanlan.zhihu.com/p/47959344)\n+ [Focal Loss for Dense Object Detection](https://arxiv.org/abs/1708.02002)\n+ [NMS介绍](https://juejin.im/entry/5bdbc26151882516da0ddd25)\n+ [Faster RCNN 源码解读(2) -- NMS(非极大抑制)](https://blog.csdn.net/williamyi96/article/details/77996167)\n+ [nms](https://github.com/lzneu/letGo/blob/master/nms/nms.ipynb)\n+ [Imbalance Problems in Object Detection: A Review](https://zhuanlan.zhihu.com/p/82371629)\n+ [5分钟理解Focal Loss与GHM——解决样本不平衡利器](https://zhuanlan.zhihu.com/p/80594704)\n+ [focal loss 通俗讲解](https://zhuanlan.zhihu.com/p/266023273)\n+ [损失函数｜交叉熵损失函数](https://zhuanlan.zhihu.com/p/35709485)\n+ https://arxiv.org/pdf/1704.04503.pdf"
  },
  {
    "path": "5-computer_vision/2D目标检测/1-目标检测模型的评价标准-AP与mAP.md",
    "content": "- [前言](#前言)\n- [一，精确率、召回率与F1](#一精确率召回率与f1)\n  - [1.1，准确率](#11准确率)\n  - [1.2，精确率、召回率](#12精确率召回率)\n  - [1.3，F1 分数](#13f1-分数)\n  - [1.4，PR 曲线](#14pr-曲线)\n    - [1.4.1，如何理解 P-R 曲线](#141如何理解-p-r-曲线)\n  - [1.5，ROC 曲线与 AUC 面积](#15roc-曲线与-auc-面积)\n- [二，AP 与 mAP](#二ap-与-map)\n  - [2.1，AP 与 mAP 指标理解](#21ap-与-map-指标理解)\n  - [2.2，近似计算AP](#22近似计算ap)\n  - [2.3，插值计算 AP](#23插值计算-ap)\n  - [2.4，mAP 计算方法](#24map-计算方法)\n- [三，目标检测度量标准汇总](#三目标检测度量标准汇总)\n- [四，参考资料](#四参考资料)\n\n## 前言\n\n为了了解模型的泛化能力，即判断模型的好坏，我们需要用某个指标来衡量，有了评价指标，就可以对比不同模型的优劣，并通过这个指标来进一步调参优化模型。**对于分类和回归两类监督模型，分别有各自的评判标准**。\n\n**不同的问题和不同的数据集都会有不同的模型评价指标，比如分类问题，数据集类别平衡的情况下可以使用准确率作为评价指标，但是现实中的数据集几乎都是类别不平衡的，所以一般都是采用 `AP` 作为分类的评价指标，分别计算每个类别的 `AP`，再计算`mAP`**。\n\n## 一，精确率、召回率与F1\n\n### 1.1，准确率\n\n**准确率（精度） – Accuracy**，预测正确的结果占总样本的百分比，定义如下：\n\n$$ 准确率 = (TP+TN)/(TP+TN+FP+FN)$$\n\n错误率和精度虽然常用，但是并不能满足所有任务需求。以西瓜问题为例，假设瓜农拉来一车西瓜，我们用训练好的模型对西瓜进行判别，现如精度只能衡量有多少比例的西瓜被我们判断类别正确（两类：好瓜、坏瓜）。但是若我们更加关心的是“挑出的西瓜中有多少比例是好瓜”，或者”所有好瓜中有多少比例被挑出来“，那么精度和错误率这个指标显然是不够用的。\n\n虽然准确率可以判断总的正确率，但是在样本不平衡的情况下，并不能作为很好的指标来衡量结果。举个简单的例子，比如在一个总样本中，正样本占 90%，负样本占 10%，样本是严重不平衡的。对于这种情况，我们只需要将全部样本预测为正样本即可得到 90% 的高准确率，但实际上我们并没有很用心的分类，只是随便无脑一分而已。这就说明了：由于样本不平衡的问题，导致了得到的高准确率结果含有很大的水分。即如果样本不平衡，准确率就会失效。\n\n### 1.2，精确率、召回率\n\n精确率（查准率）`P`、召回率（查全率）`R` 的计算涉及到混淆矩阵的定义，混淆矩阵表格如下：\n|名称|定义|\n|---|---|\n|`True Positive`(真正例, `TP`)|将正类预测为正类数|\n|`True Negative`(真负例, `TN`)|将负类预测为负类数|\n|`False Positive`(假正例, `FP`)|将负类预测为正类数 → 误报 (Type I error)|\n|`False Negative`(假负例子, `FN`)|将正类预测为负类数 → 漏报 (Type II error)|\n\n查准率与查全率计算公式：\n\n+ 查准率（精确率）$P = TP/(TP+FP)$\n+ 查全率（召回率）$R = TP/(TP+FN)$\n\n精准率和准确率看上去有些类似，但是完全不同的两个概念。**精准率代表对正样本结果中的预测准确程度，而准确率则代表整体的预测准确程度**，既包括正样本，也包括负样本。\n\n**精确率描述了模型有多准**，即在预测为正例的结果中，有多少是真正例；**召回率则描述了模型有多全**，即在为真的样本中，有多少被我们的模型预测为正例。精确率和召回率的区别在于**分母不同**，一个分母是预测为正的样本数，另一个是原来样本中所有的正样本数。\n\n### 1.3，F1 分数\n\n如果想要找到 $P$ 和 $R$ 二者之间的一个平衡点，我们就需要一个新的指标：$F1$ 分数。$F1$ 分数同时考虑了查准率和查全率，让二者同时达到最高，取一个平衡。$F1$ 计算公式如下：\n> 这里的 $F1$ 计算是针对二分类模型，多分类任务的 $F1$ 的计算请看下面。 \n\n$$F1 = \\frac{2\\times P\\times R}{P+R} = \\frac{2\\times TP}{样例总数+TP-TN}$$\n\n$F1$ 度量的一般形式：$F_{\\beta}$，能让我们表达出对查准率/查全率的偏见，$F_{\\beta}$ 计算公式如下：\n\n$$F_{\\beta} = \\frac{1+\\beta^{2}\\times P\\times R}{(\\beta^{2}\\times P)+R}$$\n\n其中 $\\beta >1$ 对查全率有更大影响，$\\beta < 1$ 对查准率有更大影响。\n\n不同的计算机视觉问题，对两类错误有不同的偏好，常常在某一类错误不多于一定阈值的情况下，努力减少另一类错误。在目标检测中，**mAP**（mean Average Precision）作为一个统一的指标将这两种错误兼顾考虑。\n\n很多时候我们会有多个混淆矩阵，例如进行多次训练/测试，每次都能得到一个混淆矩阵；或者是在多个数据集上进行训练/测试，希望估计算法的”全局“性能；又或者是执行多分类任务，**每两两类别**的组合都对应一个混淆矩阵；....总而来说，我们希望能在 $n$ 个二分类混淆矩阵上综合考虑查准率和查全率。\n\n一种直接的做法是先在各混淆矩阵上分别计算出查准率和查全率，记为 $(P_1,R_1),(P_2,R_2),...,(P_n,R_n)$ 然后取平均，这样得到的是”宏查准率（`Macro-P`）“、”宏查准率（`Macro-R`）“及对应的”宏 $F1$（`Macro-F1`）“：\n$$Macro\\ P = \\frac{1}{n}\\sum_{i=1}^{n}P_i$$\n$$Macro\\ R = \\frac{1}{n}\\sum_{i=1}^{n}R_i$$\n$$Macro\\ F1 = \\frac{2 \\times Macro\\ P\\times Macro\\ R}{Macro\\ P + Macro\\ R}$$\n\n另一种做法是将各混淆矩阵对应元素进行平均，得到 $TP、FP、TN、FN$ 的平均值，再基于这些平均值计算出”微查准率“（`Micro-P`）、”微查全率“（`Micro-R`）和”微 $F1$“（`Mairo-F1`）\n\n$$Micro\\ P = \\frac{\\overline{TP}}{\\overline{TP}+\\overline{FP}}$$\n$$Micro\\ R = \\frac{\\overline{TP}}{\\overline{TP}+\\overline{FN}}$$\n$$Micro\\ F1 = \\frac{2 \\times Micro\\ P\\times Micro\\ R}{MacroP+Micro\\ R}$$\n\n### 1.4，PR 曲线\n\n精准率和召回率的关系可以用一个 `P-R` 图来展示，以查准率 `P` 为纵轴、查全率 `R` 为横轴作图，就得到了查准率－查全率曲线，简称 **P-R** 曲线，`PR` 曲线下的面积定义为 `AP`:\n\n![PR曲线图](../../data/images/PR曲线图.jpeg)\n\n#### 1.4.1，如何理解 P-R 曲线\n\n可以从排序型模型或者分类模型理解。以逻辑回归举例，逻辑回归的输出是一个 `0` 到 `1` 之间的概率数字，因此，如果我们想要根据这个概率判断用户好坏的话，我们就必须定义一个阈值 。通常来讲，逻辑回归的概率越大说明越接近 `1`，也就可以说他是坏用户的可能性更大。比如，我们定义了阈值为 `0.5`，即概率小于 `0.5` 的我们都认为是好用户，而大于 `0.5` 都认为是坏用户。因此，对于阈值为 `0.5` 的情况下，我们可以得到相应的**一对**查准率和查全率。\n\n但问题是：这个阈值是我们随便定义的，我们并不知道这个阈值是否符合我们的要求。 因此，为了找到一个最合适的阈值满足我们的要求，我们就必须遍历 `0` 到 `1` 之间所有的阈值，而每个阈值下都对应着一对查准率和查全率，从而我们就得到了 `PR` 曲线。\n\n最后如何找到最好的阈值点呢？ 首先，需要说明的是我们对于这两个指标的要求：我们希望查准率和查全率同时都非常高。 但实际上这两个指标是一对矛盾体，无法做到双高。图中明显看到，如果其中一个非常高，另一个肯定会非常低。选取合适的阈值点要根据实际需求，比如我们想要高的查全率，那么我们就会牺牲一些查准率，在保证查全率最高的情况下，查准率也不那么低。。\n\n### 1.5，ROC 曲线与 AUC 面积\n\n- `PR` 曲线是以 `Recall` 为横轴，`Precision` 为纵轴；而 `ROC` 曲线则是以 `FPR` 为横轴，`TPR` 为纵轴**。**P-R 曲线越靠近右上角性能越好**。`PR` 曲线的两个指标都聚焦于正例\n- `PR` 曲线展示的是 `Precision vs Recall` 的曲线，`ROC` 曲线展示的是 `FPR`（x 轴：False positive rate） vs `TPR`（True positive rate, TPR）曲线。\n\n1. [ ] ROC 曲线\n2. [ ] AUC 面积\n\n## 二，AP 与 mAP\n\n### 2.1，AP 与 mAP 指标理解\n\n`AP` 衡量的是训练好的模型在每个类别上的好坏，`mAP` 衡量的是模型在所有类别上的好坏，得到 `AP` 后 `mAP` 的计算就变得很简单了，就是取所有 `AP` 的平均值。`AP` 的计算公式比较复杂（所以单独作一章节内容），详细内容参考下文。\n\n`mAP` 这个术语有不同的定义。此度量指标通常用于信息检索、图像分类和目标检测领域。然而这两个领域计算 `mAP` 的方式却不相同。这里我们只谈论目标检测中的 `mAP` 计算方法。\n\n`mAP` 常作为目标检测算法的评价指标，具体来说就是，对于每张图片检测模型会输出多个预测框（远超真实框的个数），我们使用 `IoU` (Intersection Over Union，交并比)来标记预测框是否预测准确。标记完成后，随着预测框的增多，查全率 `R` 总会上升，**在不同查全率 `R` 水平下对准确率 `P` 做平均，即得到 AP**，最后再对所有类别按其所占比例做平均，即得到 `mAP` 指标。\n\n### 2.2，近似计算AP\n\n![分类问题的PR曲线图](../../data/images/分类PR曲线图.png)\n\n知道了`AP` 的定义，下一步就是理解`AP`计算的实现，理论上可以通过积分来计算`AP`，公式如下：\n$$AP=\\int_0^1 P(r) dr$$\n但通常情况下都是使用近似或者插值的方法来计算 $AP$。\n\n$$AP = \\sum_{k=1}^{N}P(k)\\Delta r(k)$$\n\n+ 近似计算 $AP$ (`approximated average precision`)，这种计算方式是 `approximated` 形式的；\n+ 很显然位于一条竖直线上的点对计算 $AP$ 没有贡献；\n+ 这里 $N$ 为数据总量，$k$ 为每个样本点的索引， $Δr(k)=r(k)−r(k−1)$。\n\n**近似计算** `AP` 和绘制 `PR` 曲线代码如下：\n\n```python\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nclass_names = [\"car\", \"pedestrians\", \"bicycle\"]\n\ndef draw_PR_curve(predict_scores, eval_labels, name, cls_idx=1):\n    \"\"\"calculate AP and draw PR curve, there are 3 types\n    Parameters:\n    @all_scores: single test dataset predict scores array, (-1, 3)\n    @all_labels: single test dataset predict label array, (-1, 3)\n    @cls_idx: the serial number of the AP to be calculated, example: 0,1,2,3...\n    \"\"\"\n    # print('sklearn Macro-F1-Score:', f1_score(predict_scores, eval_labels, average='macro'))\n    global class_names\n    fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(15, 10))\n    # Rank the predicted scores from large to small, extract their corresponding index(index number), and generate an array\n    idx = predict_scores[:, cls_idx].argsort()[::-1]\n    eval_labels_descend = eval_labels[idx]\n    pos_gt_num = np.sum(eval_labels == cls_idx) # number of all gt\n\n    predict_results = np.ones_like(eval_labels)\n    tp_arr = np.logical_and(predict_results == cls_idx, eval_labels_descend == cls_idx) # ndarray\n    fp_arr = np.logical_and(predict_results == cls_idx, eval_labels_descend != cls_idx)\n\n    tp_cum = np.cumsum(tp_arr).astype(float) # ndarray, Cumulative sum of array elements.\n    fp_cum = np.cumsum(fp_arr).astype(float)\n\n    precision_arr = tp_cum / (tp_cum + fp_cum) # ndarray\n    recall_arr = tp_cum / pos_gt_num\n    ap = 0.0\n    prev_recall = 0\n    for p, r in zip(precision_arr, recall_arr):\n      ap += p * (r - prev_recall)\n      # pdb.set_trace()\n      prev_recall = r\n    print(\"------%s, ap: %f-----\" % (name, ap))\n\n    fig_label = '[%s, %s] ap=%f' % (name, class_names[cls_idx], ap)\n    ax.plot(recall_arr, precision_arr, label=fig_label)\n\n    ax.legend(loc=\"lower left\")\n    ax.set_title(\"PR curve about class: %s\" % (class_names[cls_idx]))\n    ax.set(xticks=np.arange(0., 1, 0.05), yticks=np.arange(0., 1, 0.05))\n    ax.set(xlabel=\"recall\", ylabel=\"precision\", xlim=[0, 1], ylim=[0, 1])\n\n    fig.savefig(\"./pr-curve-%s.png\" % class_names[cls_idx])\n    plt.close(fig)\n```\n\n### 2.3，插值计算 AP\n\n插值计算(`Interpolated average precision`) $AP$ 的公式的演变过程这里不做讨论，详情可以参考这篇[文章](https://arleyzhang.github.io/articles/c521a01c/ \"文章\")，我这里的公式和图也是参考此文章的。`11` 点插值计算方式计算 $AP$ 公式如下：\n\n![11点插值计算方式计算AP公式](../../data/images/插值计算AP公式.png)\n\n+ 这是通常意义上的 `11` `points_Interpolated` 形式的 `AP`，选取固定的 ${0,0.1,0.2,…,1.0}$ `11` 个阈值，这个在 PASCAL2007 中使用\n+ 这里因为参与计算的只有 `11` 个点，所以 $K=11$，称为 11 points_Interpolated，$k$ 为阈值索引\n+ $P_{interp}(k)$ 取第 $k$ 个阈值所对应的样本点之后的样本中的最大值，只不过这里的阈值被限定在了 ${0,0.1,0.2,…,1.0}$ 范围内。\n\n![插值计算方式计算AP的PR曲线图](../../data/images/插值计算AP的PR曲线图.png)\n\n从曲线上看，真实 `AP< approximated AP < Interpolated AP`，`11-points Interpolated AP` 可能大也可能小，当数据量很多的时候会接近于 `Interpolated AP`，与 `Interpolated AP` 不同，前面的公式中计算 `AP` 时都是对 `PR` 曲线的面积估计，PASCAL 的论文里给出的公式就更加简单粗暴了，直接计算`11` 个阈值处的 `precision` 的平均值。`PASCAL` 论文给出的 `11` 点计算 `AP` 的公式如下。\n\n![PASCAL论文给出的11点计算AP公式](../../data/images/11点计算AP公式.png)\n\n1, 在给定 `recal` 和 `precision` 的条件下计算 `AP`：\n\n```python\ndef voc_ap(rec, prec, use_07_metric=False):\n    \"\"\" \n    ap = voc_ap(rec, prec, [use_07_metric])\n    Compute VOC AP given precision and recall.\n    If use_07_metric is true, uses the\n    VOC 07 11 point method (default:False).\n    \"\"\"\n    if use_07_metric:\n        # 11 point metric\n        ap = 0.\n        for t in np.arange(0., 1.1, 0.1):\n            if np.sum(rec >= t) == 0:\n                p = 0\n            else:\n                p = np.max(prec[rec >= t])\n            ap = ap + p / 11.\n    else:\n        # correct AP calculation\n        # first append sentinel values at the end\n        mrec = np.concatenate(([0.], rec, [1.]))\n        mpre = np.concatenate(([0.], prec, [0.]))\n\n        # compute the precision envelope\n        for i in range(mpre.size - 1, 0, -1):\n            mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])\n\n        # to calculate area under PR curve, look for points\n        # where X axis (recall) changes value\n        i = np.where(mrec[1:] != mrec[:-1])[0]\n\n        # and sum (\\Delta recall) * prec\n        ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])\n    return ap\n```\n\n2，给定目标检测结果文件和测试集标签文件 `xml` 等计算 `AP`：\n\n```python\ndef parse_rec(filename):\n    \"\"\" Parse a PASCAL VOC xml file \n    Return : list, element is dict.\n    \"\"\"\n    tree = ET.parse(filename)\n    objects = []\n    for obj in tree.findall('object'):\n        obj_struct = {}\n        obj_struct['name'] = obj.find('name').text\n        obj_struct['pose'] = obj.find('pose').text\n        obj_struct['truncated'] = int(obj.find('truncated').text)\n        obj_struct['difficult'] = int(obj.find('difficult').text)\n        bbox = obj.find('bndbox')\n        obj_struct['bbox'] = [int(bbox.find('xmin').text),\n                              int(bbox.find('ymin').text),\n                              int(bbox.find('xmax').text),\n                              int(bbox.find('ymax').text)]\n        objects.append(obj_struct)\n\n    return objects\n\ndef voc_eval(detpath,\n             annopath,\n             imagesetfile,\n             classname,\n             cachedir,\n             ovthresh=0.5,\n             use_07_metric=False):\n    \"\"\"rec, prec, ap = voc_eval(detpath,\n                                annopath,\n                                imagesetfile,\n                                classname,\n                                [ovthresh],\n                                [use_07_metric])\n    Top level function that does the PASCAL VOC evaluation.\n    detpath: Path to detections result file\n        detpath.format(classname) should produce the detection results file.\n    annopath: Path to annotations file\n        annopath.format(imagename) should be the xml annotations file.\n    imagesetfile: Text file containing the list of images, one image per line.\n    classname: Category name (duh)\n    cachedir: Directory for caching the annotations\n    [ovthresh]: Overlap threshold (default = 0.5)\n    [use_07_metric]: Whether to use VOC07's 11 point AP computation\n        (default False)\n    \"\"\"\n    # assumes detections are in detpath.format(classname)\n    # assumes annotations are in annopath.format(imagename)\n    # assumes imagesetfile is a text file with each line an image name\n    # cachedir caches the annotations in a pickle file\n\n    # first load gt\n    if not os.path.isdir(cachedir):\n        os.mkdir(cachedir)\n    cachefile = os.path.join(cachedir, '%s_annots.pkl' % imagesetfile)\n    # read list of images\n    with open(imagesetfile, 'r') as f:\n        lines = f.readlines()\n    imagenames = [x.strip() for x in lines]\n\n    if not os.path.isfile(cachefile):\n        # load annotations\n        recs = {}\n        for i, imagename in enumerate(imagenames):\n            recs[imagename] = parse_rec(annopath.format(imagename))\n            if i % 100 == 0:\n                print('Reading annotation for {:d}/{:d}'.format(\n                    i + 1, len(imagenames)))\n        # save\n        print('Saving cached annotations to {:s}'.format(cachefile))\n        with open(cachefile, 'wb') as f:\n            pickle.dump(recs, f)\n    else:\n        # load\n        with open(cachefile, 'rb') as f:\n            try:\n                recs = pickle.load(f)\n            except:\n                recs = pickle.load(f, encoding='bytes')\n\n    # extract gt objects for this class\n    class_recs = {}\n    npos = 0\n    for imagename in imagenames:\n        R = [obj for obj in recs[imagename] if obj['name'] == classname]\n        bbox = np.array([x['bbox'] for x in R])\n        difficult = np.array([x['difficult'] for x in R]).astype(np.bool)\n        det = [False] * len(R)\n        npos = npos + sum(~difficult)\n        class_recs[imagename] = {'bbox': bbox,\n                                 'difficult': difficult,\n                                 'det': det}\n\n    # read dets\n    detfile = detpath.format(classname)\n    with open(detfile, 'r') as f:\n        lines = f.readlines()\n\n    splitlines = [x.strip().split(' ') for x in lines]\n    image_ids = [x[0] for x in splitlines]\n    confidence = np.array([float(x[1]) for x in splitlines])\n    BB = np.array([[float(z) for z in x[2:]] for x in splitlines])\n\n    nd = len(image_ids)\n    tp = np.zeros(nd)\n    fp = np.zeros(nd)\n\n    if BB.shape[0] > 0:\n        # sort by confidence\n        sorted_ind = np.argsort(-confidence)\n        sorted_scores = np.sort(-confidence)\n        BB = BB[sorted_ind, :]\n        image_ids = [image_ids[x] for x in sorted_ind]\n\n        # go down dets and mark TPs and FPs\n        for d in range(nd):\n            R = class_recs[image_ids[d]]\n            bb = BB[d, :].astype(float)\n            ovmax = -np.inf\n            BBGT = R['bbox'].astype(float)\n\n            if BBGT.size > 0:\n                # compute overlaps\n                # intersection\n                ixmin = np.maximum(BBGT[:, 0], bb[0])\n                iymin = np.maximum(BBGT[:, 1], bb[1])\n                ixmax = np.minimum(BBGT[:, 2], bb[2])\n                iymax = np.minimum(BBGT[:, 3], bb[3])\n                iw = np.maximum(ixmax - ixmin + 1., 0.)\n                ih = np.maximum(iymax - iymin + 1., 0.)\n                inters = iw * ih\n\n                # union\n                uni = ((bb[2] - bb[0] + 1.) * (bb[3] - bb[1] + 1.) +\n                       (BBGT[:, 2] - BBGT[:, 0] + 1.) *\n                       (BBGT[:, 3] - BBGT[:, 1] + 1.) - inters)\n\n                overlaps = inters / uni\n                ovmax = np.max(overlaps)\n                jmax = np.argmax(overlaps)\n\n            if ovmax > ovthresh:\n                if not R['difficult'][jmax]:\n                    if not R['det'][jmax]:\n                        tp[d] = 1.\n                        R['det'][jmax] = 1\n                    else:\n                        fp[d] = 1.\n            else:\n                fp[d] = 1.\n\n    # compute precision recall\n    fp = np.cumsum(fp)\n    tp = np.cumsum(tp)\n    rec = tp / float(npos)\n    # avoid divide by zero in case the first detection matches a difficult\n    # ground truth\n    prec = tp / np.maximum(tp + fp, np.finfo(np.float64).eps)\n    ap = voc_ap(rec, prec, use_07_metric)\n\n    return rec, prec, ap\n```\n\n### 2.4，mAP 计算方法\n\n因为 $mAP$ 值的计算是对数据集中所有类别的 $AP$ 值求平均，所以我们要计算 $mAP$，首先得知道某一类别的 $AP$ 值怎么求。不同数据集的某类别的 $AP$ 计算方法大同小异，主要分为三种：\n\n（1）在 `VOC2007`，只需要选取当 $Recall >= 0, 0.1, 0.2, ..., 1$ 共 `11` 个点时的 `Precision` 最大值，然后 $AP$ 就是这 `11` 个 `Precision` 的平均值，$mAP$ 就是所有类别 $AP$ 值的平均。`VOC` 数据集中计算 $AP$ 的代码（用的是插值计算方法，代码出自[py-faster-rcnn仓库](https://github.com/rbgirshick/py-faster-rcnn/blob/master/lib/datasets/voc_eval.py \"py-faster-rcnn仓库\")）\n\n（2）在 `VOC2010` 及以后，需要针对每一个不同的 `Recall` 值（包括 0 和 1），选取其大于等于这些 `Recall` 值时的 `Precision` 最大值，然后计算 `PR` 曲线下面积作为 $AP$ 值，$mAP$ 就是所有类别 $AP$ 值的平均。\n\n（3）`COCO` 数据集，设定多个 `IOU` 阈值（`0.5-0.95`, `0.05` 为步长），在每一个 `IOU` 阈值下都有某一类别的 `AP` 值，然后求不同 `IOU` 阈值下的 `AP` 平均，就是所求的最终的某类别的 `AP` 值。\n\n## 三，目标检测度量标准汇总\n\n![目标检测指标汇总](../../data/images/目标检测度量标准汇总.jpg)\n\n\n|评价指标|定义及理解|\n|-------|--------|\n|`mAP`|mean Average Precision, 即各类别 AP 的平均值|\n|`AP`|`PR` 曲线下面积，后文会详细讲解|\n|`PR 曲线`|Precision-Recall 曲线|\n|`Precision`|$TP / (TP + FP)$|\n|`Recall`|$TP / (TP + FN)$|\n|`TP`|`IoU>0.5` 的检测框数量（同一 `Ground Truth` 只计算一次，阈值取 `0.5`）|\n|`FP`|`IoU<=0.5` 的检测框，或者是检测到同一个 `GT` 的多余检测框的数量|\n|`FN`|没有检测到的 `GT` 的数量|\n\n## 四，参考资料\n\n+ [目标检测评价标准-AP mAP](https://Harleyzhang.github.io/articles/c521a01c/ \"目标检测评价标准-AP mAP\")\n+ [目标检测的性能评价指标](https://zhuanlan.zhihu.com/p/70306015?utm_source=wechat_session&utm_medium=social&utm_oi=571954943427219456 \"目标检测的性能评价指标\")\n+ [Soft-NMS](https://hellozhaozheng.github.io/z_post/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89-SoftNMS-ICCV2017/ \"Soft-NMS\")\n+ [Recent Advances in Deep Learning for Object Detection](https://arxiv.org/abs/1908.03673v1 \"Recent Advances in Deep Learning for Object Detection\")\n+ [A Simple and Fast Implementation of Faster R-CNN](https://github.com/chenyuntc/simple-faster-rcnn-pytorch \"A Simple and Fast Implementation of Faster R-CNN\")\n+ [分类模型评估指标——准确率、精准率、召回率、F1、ROC曲线、AUC曲线](https://easyai.tech/ai-definition/accuracy-precision-recall-f1-roc-auc/ \"分类模型评估指标——准确率、精准率、召回率、F1、ROC曲线、AUC曲线\")\n+ [一文让你彻底理解准确率，精准率，召回率，真正率，假正率，ROC/AUC](https://www.6aiq.com/article/1549986548173 \"一文让你彻底理解准确率，精准率，召回率，真正率，假正率，ROC/AUC\")\n"
  },
  {
    "path": "5-computer_vision/2D目标检测/2-Faster-RCNN网络详解.md",
    "content": "- [Faster RCNN 网络概述](#faster-rcnn-网络概述)\n- [Conv layers](#conv-layers)\n- [RPN 网络](#rpn-网络)\n\t- [Anchors](#anchors)\n\t- [生成 RPN 网络训练集](#生成-rpn-网络训练集)\n\t- [positive/negative 二分类](#positivenegative-二分类)\n\t- [RPN 生成 RoIs(Proposal Layer)](#rpn-生成-roisproposal-layer)\n\t- [RPN 网络总结](#rpn-网络总结)\n- [ROIHead/Fast R-CNN](#roiheadfast-r-cnn)\n\t- [Roi pooling](#roi-pooling)\n\t- [ROI Head 训练](#roi-head-训练)\n\t- [ROI Head 测试](#roi-head-测试)\n- [概念理解](#概念理解)\n\t- [四类损失](#四类损失)\n\t- [三个 creator](#三个-creator)\n- [参考资料](#参考资料)\n\n> 本文为学习笔记，部分内容参考网上资料和论文而写的，内容涉及 `Faster RCNN` 网络结构理解和代码实现原理。\n\n## Faster RCNN 网络概述\n\n![faster-rcnn连接图](../../data/images/faster-rcnn/faster-rcnn连接图.jpg)\n\n`backbone` 为 `vgg16` 的 `faster rcnn` 网络结构如下图所示，可以清晰的看到该网络对于一副任意大小 `PxQ` 的图像，首先缩放至固定大小  `MxN`，然后将 `MxN` 图像送入网络；而 Conv layers 中包含了 13 个 conv 层 + 13 个 relu 层 + 4 个 pooling 层；`RPN` 网络首先经过 3x3 卷积，再分别生成 `positive anchors` 和对应 `bounding box regression` 偏移量，然后计算出 `proposals`；而 `Roi Pooling` 层则利用 proposals 从 feature maps 中提取 `proposal feature` 送入后续全连接和 softmax 网络作 `classification`（即分类： `proposal` 是哪种 `object`）。\n\n![faster-rcnn网络详细结构图](../../data/images/faster-rcnn/faster-rcnn网络详细结构图.png)\n\n## Conv layers\n\n> 论文中 `Faster RCNN` 虽然支持任意图片输入，但是进入 `Conv layers` 网络之前会对图片进行规整化尺度操作，如可设定图像短边不超过 600，图像长边不超过 1000，我们可以假定 $M\\times N=1000\\times 600$（如果图片少于该尺寸，可以边缘补 0，即图像会有黑色边缘）。\n\n1. `13` 个 `conv` 层：`kernel_size=3, pad=1, stride=1`，卷积公式：`N = (W − F + 2P )/S+1`，所以可知 `conv` 层不会改变图片大小\n2. `13` 个 `relu` 层: 激活函数，增加非线性，不改变图片大小\n3. `4` 个 `pooling` 层：`kernel_size=2,stride=2`，`pooling` 层会让输出图片变成输入图片的 1/2。\n\n所以经过 `Conv layers`，图片大小变成 $(M/16) \\ast (N/16)$，即：$60\\ast 40(1000/16≈60,600/16≈40)$；则 `Feature Map` 尺寸为 $60\\ast 40\\ast 512$-d (注：VGG16 是512-d, ZF 是 256-d，`d` 是指特征图通道数，也叫特征图数量)，表示特征图的大小为 $60\\ast 40$，数量为 `512`。\n\n## RPN 网络\n\n![RPN网络结构图](../../data/images/faster-rcnn/RPN网络结构图.png)\n\n`RPN` 在 `Extractor`（特征提取 backbone ）输出的 feature maps 的基础之上，先增加了一个 `3*3` 卷积（用来语义空间转换？），然后利用两个 `1x1` 的卷积分别进行**二分类**（是否为正样本）和**位置回归**。RPN 网络在分类和回归的时候，分别将每一层的每一个 anchor 分为背景和前景两类，以及回归四个位移量，进行分类的卷积核通道数为`9×2`（9 个 anchor，每个 anchor 二分类，使用交叉熵损失），进行回归的卷积核通道数为 `9×4`（9个anchor，每个 anchor 有 4 个位置参数）。**RPN是一个全卷积网络（fully convolutional network）**，这样对输入图片的尺寸就没有要求了。\nRPN 完成 `positive/negative 分类` + `bounding box regression 坐标回归`两个任务。\n\n### Anchors\n\n在`RPN`中，作者提出了`anchors`，在代码中，anchors 是一组由 generate_anchors.py 生成的矩形框列表。运行官方代码的 `generate_anchors.py` 可以得到以下示例输出.这里生成的坐标是在原图尺寸上的坐标，在特征图上的一个像素点，可以对应到原图上一个 $16\\times 16$大小的区域。\n> [[ -84. -40. 99. 55.]\n [-176. -88. 191. 103.]\n [-360. -184. 375. 199.]\n [ -56. -56. 71. 71.]\n [-120. -120. 135. 135.]\n [-248. -248. 263. 263.]\n [ -36. -80. 51. 95.]\n [ -80. -168. 95. 183.]\n [-168. -344. 183. 359.]]\n\n其中每行的 4 个值 $(x_{1},  y_{1},  x_{2},  y_{2})$ 表矩形左上和右下角点坐标。9 个矩形共有 3 种形状，长宽比为大约为 $width:height \\epsilon \\{1:1, 1:2, 2:1\\}$ 三种，如下图。**实际上通过 anchors 就引入了检测中常用到的多尺度方法**。\n\n![anchor示例](../../data/images/faster-rcnn/anchor示例.png)\n\n> 注意，`generate_anchors.py` 生成的只是 base anchors，其中一个 框的左上角坐标为 (0,0) 坐标（特征图左上角）的 9 个 anchor，后续还需网格化（meshgrid）生成其他 anchor。同一个 scale，但是不同的 anchor ratios 生成的 anchors 面积理论上是要一样的。\n\n然后利用这 `9` 种 `anchor` 在特征图左右上下移动（遍历），每一个特征图上的任意一个点都有 `9` 个 `anchor`，假设原图大小为 MxN，经过 Conv layers 下采样 16 倍，则每个 feature map 生成 `(M/16)*(N/16)*9`个 `anchor`。例如，对于一个尺寸为 62×37 的 feature map，有 62×37×9 ≈ 20000 个 anchor，**并输出特征图上面每个点对应的原图 anchor 坐标**。这种做法很像是暴力穷举，20000 多个 anchor，哪怕是蒙也能够把绝大多数的 ground truth bounding boxes 蒙中。\n\n因此可知，`anchor` 的数量和 `feature map` 大小相关，不同的 feature map 对应的 anchor 数量也不一样。\n\n### 生成 RPN 网络训练集  \n\n在这个任务中，RPN 做的事情就是利用（`AnchorTargetCreator`）将 20000 多个候选的 anchor **选出 256 个 anchor 进行分类和回归位置**。选择过程如下：\n\n+ 对于每一个 ground truth bounding box (`gt_bbox`)，选择和它重叠度（IoU）最高的一个 `anchor` 作为正样本;\n+ 对于剩下的 anchor，从中选择和任意一个 gt_bbox 重叠度超过 `0.7` 的 anchor ，同样作为正样本;特殊情况下，如果正样本不足 `128`(256 的 1/2)，则用负样本凑。\n+ 随机选择和 `gt_bbox` 重叠度小于 `0.3` 的 anchor 作为负样本。\n本和正样本的总数为`256` ，正负样本比例 `1:1`。\n\n### positive/negative 二分类\n\n由 $1\\times 1$ 卷积实现，卷积通道数为 $9\\times 2$（每个点有 9 个 anchor，每个 anchor 二分类，使用交叉熵损失），后面接 softmax 分类获得 positive anchors，也就相当于初步提取了检测目标候选区域 box（一般认为目标在 positive anchors 中）。所以可知，RPN 的一个任务就是在原图尺度上，设置了大量的候选 `anchor`，并通过 `AnchorTargetCreator` 类去挑选正负样本比为 `1:1` 的 `256` 个 `anchor`，然后再用 `CNN` ($1\\times 1$ 卷积，卷积通道数 $9\\times 2$) 去判断挑选出来的 `256` 个 `anchor` 哪些有目标的 `positive anchor`，哪些是没目标的 `negative anchor`。\n\n在挑选 `1:1` 正负样本比例的 `anchor` 用作 `RPN` 训练集后，还需要计算训练集数据对应的标签。对于每个 `anchor`, 对应的标签是 `gt_label` 和 `gt_loc`。`gt_label` 要么为 `1`（前景），要么为 `0`（背景），而 `gt_loc` 则是由 `4` 个位置参数 $(t_x,t_y,t_w,t_h)$ 组成，它们是 `anchor box` 与 `ground truth bbox` 之间的偏移量，因为回归偏移量比直接回归座标更好。在 `Faster RCNN`原文，`positive anchor` 与 `ground truth` 之间的偏移量 $(t_{x}, t_{y})$ 与尺度因子 $(t_{w}, t_{h})$ 计算公式如下:\n\n$$\\begin{aligned}\nt_{x} = (x-x_{a})/w_{a}, t_{y}=(y-y_{a})/h_{a} \\\\\nt_{w} = log(w/w_{a}), t_{h}=log(h/h_{a}) \\\\\nt_{x}^{\\ast } = (x^{\\ast }-x_{a})/w_{a}, t_{y}^{\\ast}=(y^{\\ast}-y_{a})/h_{a} \\\\\nt_{w}^{\\ast } = log(w^{\\ast }/w_{a}), t_{h}^{\\ast }=log(h^{\\ast }/h_{a})\n\\end{aligned}$$\n\n参数解释：where $x, y, w,$ and $h$ denote the box’s center coordinates and its width and height. Variables $x, x_{a}$，and $x^{*}$ are for the predicted box, anchor box, and groundtruth box respectively (likewise for $y, w, h$).\n\n计算分类损失用的是交叉熵损失，**计算回归损失用的是 `Smooth_L1_loss`**。在计算回归损失的时候，只计算正样本（前景）的损失，不计算负样本的位置损失。`RPN` 网络的 Loss 计算公式如下：\n\n> [损失函数：L1 loss, L2 loss, smooth L1 loss](https://zhuanlan.zhihu.com/p/48426076)\n\n![rpn的loss计算公式](../../data/images/faster-rcnn/rpn的loss计算公式.jpg)\n\n**公式解释**：Here, $i$ is the index of an anchor in a mini-batch and $p_{i}$ is the predicted probability of anchor i being an object. The ground-truth label $p_i^{\\ast}$ is 1 if the anchor is positive, and is 0 if the anchor is negative. $t_{i}$ is a vector representing the 4 parameterized coordinates of the predicted bounding box, and $t_i^*$ is that of theground-truth box associated with a positive anchor.\n\n### RPN 生成 RoIs(Proposal Layer)\n\n`RPN` 网络在自身训练的同时，还会由 `Proposal Layer` 层产生 `RoIs`（region of interests）给 Fast RCNN（RoIHead）作为训练样本。RPN 生成 RoIs 的过程( `ProposalCreator` )如下：\n\n1. 对于每张图片，利用它的 `feature map`， 计算 `(H/16)× (W/16)×9`（大概 20000）个 `anchor` 属于前景的概率，以及对应的位置参数，并选取概率较大的 `12000` 个 anchor；\n2. 利用回归的位置参数，修正这 `12000` 个 `anchor` 的位置，得到`RoIs`；\n3. 利用非极大值（(Non-maximum suppression, NMS）抑制，选出概率最大的 `2000` 个 `RoIs`。\n\n> 在 RPN 中，从上万个 anchor 中，选一定数目(2000 或 300)，调整大小和位置生成 `RoIs`，用于 ROI Head/Fast RCNN 训练或测试，然后 `ProposalTargetCreator` 再从 `RoIs` 中会中选择 `128` 个 `RoIs` 用以 ROIHead 的训练）。\n\n注意：RoIs 对应的尺寸是原图大小，同时在 `inference` 的时候，为了提高处理速度，12000 和 2000 分别变为 6000 和 300。**`Proposal Layer` 层，这部分的操作不需要进行反向传播，因此可以利用 numpy/tensor 实现**。\n\n### RPN 网络总结\n\n+ RPN 网络结构：**生成 anchors -> softmax 分类器提取 positvie anchors -> bbox reg 回归 positive anchors -> Proposal Layer 生成 proposals**\n+ RPN 的输出：`RoIs(region of interests)`（形如 `2000×4` 或者 `300×4` 的 `tensor`）\n\n## ROIHead/Fast R-CNN\n\n`RPN` 只是给出了 2000 个 候选框，`RoI Head` 在给出的 2000 候选框之上继续进行分类和位置参数的回归。`ROIHead` 网络包括 `RoI pooling` + `Classification`(全连接分类)两部分，网络结构如下：\n\n![ROI-Head结构图](../../data/images/faster-rcnn/ROI-Head结构图.png)\n\n由于 `RoIs` 给出的 `2000` 个 候选框，分别对应 `feature map` 不同大小的区域。首先利用 `ProposalTargetCreator` 挑选出 `128` 个 `sample_rois`, 然后使用了 `RoI Pooling` 将这些不同尺寸的区域全部 `pooling` 到同一个尺度 $7\\times 7$ 上，并输出 $7\\times 7$ 大小的 `feature map` 送入后续的**两个全连接层**。两个全连接层分别完成类别分类和 `bbox` 回归的作用：\n\n- `FC 21` 用来分类，预测 `RoIs` 属于哪个类别（20个类+背景）\n- `FC 84` 用来回归位置（21个类，每个类都有4个位置参数）\n\n> 论文中之所以设定为 pooling 成 7×7 的尺度，其实是为了网络输出是固定大小的`vector or matrix`，从而能够共享 VGG 后面两个全连接层的权重。当所有的 RoIs 都被pooling 成（512×7×7）的 feature map 后，将它 reshape 成一个一维的向量，就可以利用 VGG16 预训练的权重来初始化前两层全连接（`FC 4096`）。\n\n### Roi pooling\n\n`RoI pooling` 负责将 `128` 个 `RoI` 区域对应的 `feature map` 进行截取，而后利用 `RoI pooling` 层输出 $7\\times 7$ 大小的 `feature map`，送入后续的**全连接网络**。从论文给出的 `Faster R-CNN` 网络结构图中，可以看到 `Rol pooling` 层有 `2` 个输入：\n\n+ 原始的 `feature maps`\n+ `RPN` 输出的 `RoIs` (`proposal boxes`, 大小各不相同）\n\n**RoI Pooling 的两次量化过程**：\n\n(1) 因为 `proposals`是对应 $M\\times N$ 的原图尺寸，所以**在原图上生成的 region proposal** 需要映射到 `feature map` 上（坐标值缩小 16 倍），需要除以 $16/32$（下采样倍数），这时候边界会出现小数，自然就需要量化。\n(2) 将 `proposals` 对应的 `feature map` 区域水平划分成 $k\\times k$ ($7\\times 7$) 的 `bins`，并对每个 `bin` 中均匀选取多少个采样点，然后进行 `max pooling`，也会出现小数，自然就产生了第二次量化。\n\n**Mask RCNN 算法中的 RoI Align 如何改进**:\n\n`ROI Align` 并不需要对两步量化中产生的浮点数坐标的像素值都进行计算，而是设计了一套优雅的流程。如下图，其中虚线代表的是一个 `feature map`，实线代表的是一个 `roi` (在这个例子中，一个 `roi` 是分成了 $2\\times 2$ 个 `bins`)，实心点代表的是采样点，每个 `bin` 中有 `4` 个采样点。我们通过双线性插值的方法根据采样点周围的四个点计算每一个采样点的值，然后对着四个采样点执行最大池化操作得到当前 `bin` 的像素值。\n\n![ROI-Align](../../data/images/faster-rcnn/ROI-Align.png)\n\n`RoI Align` 具体做法：假定采样点数为 `4`，即表示，对于每个 $2.97\\times 2.97$ 的 `bin`，**平分四份小矩形，每一份取其中心点位置，而中心点位置的像素，采用双线性插值法进行计算**，这样就会得到四个小数坐标点的像素值。\n\n更多细节内容可以参考 [RoIPooling、RoIAlign笔记](https://www.cnblogs.com/wangyong/p/8523814.html)。\n\n### ROI Head 训练\n\nRPN 会产生大约 2000 个 RoIs ，ROI Head 在给出的 2000 个 RoIs 候选框基础上**继续分类(目标分类)和位置参数回归**。注意，这 2000 个 RoIs 不是都拿去训练，而是**利用 ProposalTargetCreator（官方源码可以查看类定义） 选择 128 个 RoIs 用以训练**。选择的规则如下：\n\n+ 在和 `gt_bboxes` 的 `IoU` 大于 `0.5` 的 `RoIs` 内，选择一些（比如 `32` 个）作为正样本\n+ 在和 `gt_bboxes` 的 `IoU` 小于等于 `0`（或者 `0.1` ）`RoIs` 内，的选择一些（比如 $128 - 32 = 96$ 个）作为负样本\n\n选择出的 `128` 个 `RoIs`，其正负样本比例为 `3:1`，在源码中为了便于训练，还对他们的 `gt_roi_loc` 进行标准化处理（减去均值除以标准差）。\n\n- 对于分类问题, 和 `RPN` 一样，直接利用**交叉熵损失**。\n- 对于位置的回归损失，也采用 **Smooth_L1 Loss**, 只不过**只对正样本计算损失**，而且是只对正样本中的对应类别的 $4$ 个参数计算损失。举例来说：\n  - 一个 `RoI` 在经过 `FC84` 后会输出一个 `84` 维的 `loc` 向量。 如果这个 `RoI` 是负样本, 则这 `84` 维向量不参与计算 `L1_Loss`。\n  - 如果这个 `RoI` 是正样本，且属于 类别 $k$, 那么向量的第 $(k×4，k×4+1 ，k×4+2， k×4+3)$ 这 $4$ 位置的值参与计算损失，其余的不参与计算损失。\n\n### ROI Head 测试\n\n`ROI Head` 测试的时候对所有的 `RoIs`（大概 `300` 个左右) 计算概率，并利用位置参数调整预测候选框的位置，然后**再**用一遍极大值抑制（之前在 `RPN` 的`ProposalCreator` 也用过）。这里注意：\n\n+ 在 `RPN` 的时候，已经对 `anchor` 做了一遍 `NMS`，在 `Fast RCNN` 测试的时候，还要再做一遍，**所以在Faster RCNN框架中，NMS操作总共有 2 次**。\n+ 在 `RPN` 的时候，已经对 `anchor` 的位置做了回归调整，在 `Fast RCNN` 阶段还要对 `RoI` 再做一遍。\n+ 在 `RPN` 阶段分类是二分类，而 `Fast RCNN/ROI Head` 阶段是 `21` 分类。\n\n## 概念理解\n\n在阅读 `Faster RCNN` 论文和源码中，我们经常会涉及到一些概念的理解。\n\n### 四类损失\n\n在训练 `Faster RCNN` 的时候有四个损失：\n\n+ `RPN` 分类损失：`anchor` 是否为前景（二分类）\n+ `RPN` 位置回归损失：`anchor` 位置微调\n+ `RoI` 分类损失：`RoI` 所属类别（`21` 分类，多了一个类作为背景）\n+ `RoI` 位置回归损失：继续对 `RoI` 位置微调\n\n**四个损失相加作为最后的损失，反向传播，更新参数**。\n\n### 三个 creator\n\nFaster RCNN 官方源码中有三个 `creator` 类分别实现不同的功能，不能弄混，各自功能如下：\n\n+ `AnchorTargetCreator` ： 负责在训练 `RPN` 的时候，从上万个 `anchors` 中选择一些(比如 `256` )进行训练，并使得正负样本比例大概是 `1:1`。同时给出训练的位置参数目标，即返回 `gt_rpn_loc` 和 `gt_rpn_label`。\n+ `ProposalTargetCreator`： 负责在训练 `RoIHead/Fast R-CNN` 的时候，从 `RoIs` 选择一部分(比如 `128` 个，正负样本比例 `1:3`)用以训练。同时给定训练目标, 返回（`sample_RoI`, `gt_RoI_loc`, `gt_RoI_label`）。\n+ `ProposalCreator`： 在 `RPN` 中，从上万个 `anchor` 中，选择一定数目（ `2000` 或者 `300` ），调整大小和位置，生成 `RoIs` ，用以 `Fast R-CNN` 训练或者测试。\n\n其中 `AnchorTargetCreator` 和 `ProposalTargetCreator` 类是为了生成训练的目标，只在训练阶段用到，`ProposalCreator` 是 `RPN` 为 `Fast R-CNN` 生成 `RoIs` ，在训练和测试阶段都会用到。**三个 `creator` 的共同点在于他们都不需要考虑反向传播**（因此不同框架间可以共享 `numpy` 实现）。\n\n## 参考资料\n\n- [一文读懂Faster RCNN](https://zhuanlan.zhihu.com/p/31426458)\n- [从编程实现角度学习Faster RCNN](https://zhuanlan.zhihu.com/p/32404424)\n- [你真的学会了RoI Pooling了吗](https://zhuanlan.zhihu.com/p/59692298)\n- [Faster RCNN 学习笔记](https://www.cnblogs.com/wangyong/p/8513563.html)\n"
  },
  {
    "path": "5-computer_vision/2D目标检测/3-FPN网络详解.md",
    "content": "- [论文背景](#论文背景)\n- [引言（Introduction）](#引言introduction)\n- [特征金字塔网络 FPN](#特征金字塔网络-fpn)\n  - [FPN网络建立](#fpn网络建立)\n  - [Anchor锚框生成规则](#anchor锚框生成规则)\n- [实验](#实验)\n- [代码解读](#代码解读)\n- [参考资料](#参考资料)\n\n> 本篇文章是论文阅读笔记和网络理解心得总结而来，部分资料和图参考论文和网络资料\n\n## 论文背景\n\n`FPN`(feature pyramid networks) 是何凯明等作者提出的适用于多尺度目标检测算法。原来多数的 object detection 算法（比如 faster rcnn）都是只采用顶层特征做预测，但我们知道**低层的特征语义信息比较少，但是目标位置准确；高层的特征语义信息比较丰富，但是目标位置比较粗略**。另外虽然也有些算法采用多尺度特征融合的方式，但是一般是采用融合后的特征做预测，而本文不一样的地方在于预测是在不同特征层独立进行的。\n\n## 引言（Introduction）\n\n![4种金字塔结构](../../data/images/FPN/4种金字塔结构.jpg)\n\n从上图可以看出，（a）使用图像金字塔构建特征金字塔。每个图像尺度上的特征都是独立计算的，速度很慢。（b）最近的检测系统选择（比如 Faster RCNN）只使用单一尺度特征进行更快的检测。（c）另一种方法是重用 ConvNet（卷积层）计算的金字塔特征层次结构（比如 SSD），就好像它是一个特征化的图像金字塔。（d）我们提出的特征金字塔网络（FPN）与（b）和（c）类似，但更准确。**在该图中，特征映射用蓝色轮廓表示，较粗的轮廓表示语义上较强的特征**。\n\n## 特征金字塔网络 FPN\n\n作者提出的 `FPN` 结构如下图：这个金字塔结构包括一个**自底向上的线路，一个自顶向下的线路和横向连接（lateral connections）**。\n\n![fpn结构示意图](../../data/images/FPN/fpn结构示意图.jpg)\n\n**自底向上**其实就是卷积网络的前向过程。在前向过程中，`feature map` 的大小在经过某些层后会改变，而在经过其他一些层的时候不会改变，作者将不改变 feature map 大小的层归为一个 stage，因此这里金字塔结构中每次抽取的特征都是每个 `stage` 的最后一个层的输出。在代码中我们可以看到共有`C1、C2、C3、C4、C5`五个特征图，`C1` 和 `C2` 的特征图大小是一样的，所以，`FPN` 的建立也是基于从 `C2` 到 `C5` 这四个特征层上。\n\n**自顶向下**的过程采用上采样（`upsampling`）进行，而**横向连接**则是将上采样的结果和自底向上生成的相同大小的 `feature map` 进行融合（merge）。在融合之后还会再采用 `3*3` 的卷积核对每个融合结果进行卷积，目的是消除上采样的混叠效应（aliasing effect）。并假设生成的 feature map 结果是 P2，P3，P4，P5，和原来自底向上的卷积结果 C2，C3，C4，C5一一对应。\n\n这里贴一个 `ResNet` 的结构图：论文中作者采用 `conv2_x，conv3_x，conv4_x 和 conv5_x` 的输出，对应 `C1，C2，C3，C4，C5`，因此类似 `Conv2`就可以看做一个stage。\n\n![resnet结构参数图](../../data/images/FPN/resnet结构参数图.png)\n\n### FPN网络建立\n\n> 这里自己没有总结，因为已经有篇博文总结得很不错了，[在这](https://www.cnblogs.com/wangyong/p/10614898.html)。\n\n通过 ResNet50 网络，得到图片不同阶段的特征图，最后利用 C2，C3，C4，C5 建立特征图金字塔结构：\n\n1. 将 C5 经过 256 个 1\\*1 的卷积核操作得到：32\\*32\\*256，记为 P5；\n2. 将 P5 进行步长为 2 的上采样得到 64\\*64\\*256，再与 C4 经过的 256 个 1\\*1 卷积核操作得到的结果相加，得到 64\\*64\\*256，记为 P4；\n3. 将 P4 进行步长为 2 的上采样得到 128\\*128\\*256，再与 C3 经过的 256 个 1\\*1 卷积核操作得到的结果相加，得到 128\\*128\\*256，记为 P3；\n4. 将 P3 进行步长为 2 的上采样得到 256\\*256\\*256，再与 C2 经过的 256 个 1\\*1 卷积核操作得到的结果相加，得到 256\\*256\\*256，记为 P2；\n5. 将 P5 进行步长为 2 的最大池化操作得到：16\\*16\\*256，记为 P6；\n\n结合从 P2 到 P6 特征图的大小，如果原图大小 1024\\*1024, 那各个特征图对应到原图的`步长`依次为 [P2,P3,P4,P5,P6]=>[4,8,16,32,64]。\n\n### Anchor锚框生成规则\n\n当 `Faster RCNN` 采用 `FPN` 的网络作 backbone 后，锚框的生成规则也会有所改变。基于上一步得到的特征图 [P2,P3,P4,P5,P6],再介绍下采用 `FPN` 的 Faster RCNN（或者 Mask RCNN）网络中 Anchor 锚框的生成，根据源码中介绍的规则，与之前 Faster-RCNN 中的生成规则有一点差别。\n\n1. 遍历 `P2 到 P6` 这五个特征层，以每个特征图上的每个像素点都生成 `Anchor` 锚框；\n2. 以 P2 层为例，P2 层的特征图大小为 256\\*256，相对于原图的步长为4，这样 P2上的每个像素点都可以生成一个基于坐标数组 [0,0,3,3] 即 4\\*4 面积为 16 大小的Anchor锚框，当然，可以设置一个比例 SCALE，将这个基础的锚框放大或者缩小，比如，这里设置 P2 层对应的缩放比例为 16，那边生成的锚框大小就是长和宽都扩大16倍，从 4\\*4 变成 64\\*64，面积从 16 变成 4096，当然在保证面积不变的前提下，长宽比可以变换为 32\\*128、64\\*64 或 128\\*32，这样以长、宽比率 RATIO = [0.5,1,2] 完成了三种变换，这样一个像素点都可以生成3个Anchor锚框。在 Faster-RCNN 中可以将 `Anchor scale` 也可以设置为多个值，**而在MasK RCNN 中则是每一特征层只对应着一个 `Anchor scale`即对应着上述所设置的 16**；\n3. 以 `P2` 层每个像素点位中心，对应到原图上，则可生成 256\\*256\\*3(长宽三种变换) = 196608 个锚框；\n4. 以 `P3` 层每个像素点为中心，对应到原图上，则可生成 128\\*128\\*3 = 49152 个锚框；\n5. 以 `P4` 层每个像素点为中心，对应到原图上，则可生成 64\\*64\\*3 = 12288 个锚框；\n6. 以 `P5` 层每个像素点为中心，对应到原图上，则生成 32\\*32\\*3 = 3072 个锚框；\n7. 以 `P6` 层每个像素点为中心，对应到原图上，则生成 16\\*16\\*3 = 768 个锚框。\n\n从 P2 到 P6 层一共可以在原图上生成 $196608 + 49152 + 12288 + 3072 + 768 = 261888$ 个 `Anchor` 锚框。\n\n## 实验\n\n看看加入FPN 的 RPN 网络的有效性，如下表 Table1。网络这些结果都是基于 ResNet-50。评价标准采用 AR，AR 表示 Average Recall，AR 右上角的 100 表示每张图像有 100 个 anchor，AR 的右下角 s，m，l 表示 COCO 数据集中 object 的大小分别是小，中，大。feature 列的大括号 {} 表示每层独立预测。\n\n![对比试验](../../data/images/FPN/对比试验.png)\n\n从（a）（b）（c）的对比可以看出 `FPN` 的作用确实很明显。另外（a）和（b）的对比可以看出高层特征并非比低一层的特征有效。\n\n（d）表示只有横向连接，而没有自顶向下的过程，也就是仅仅对自底向上（bottom-up）的每一层结果做一个 1\\*1 的横向连接和 3*3 的卷积得到最终的结果，有点像 `Fig1` 的（b）。从 feature 列可以看出预测还是分层独立的。作者推测（d）的结果并不好的原因在于在自底向上的不同层之间的 semantic gaps 比较大。\n\n（e）表示有自顶向下的过程，但是没有横向连接，即向下过程没有融合原来的特征。这样效果也不好的原因在于目标的 location 特征在经过多次降采样和上采样过程后变得更加不准确。\n\n（f）采用 finest level 层做预测（参考 Fig2 的上面那个结构），即经过多次特征上采样和融合到最后一步生成的特征用于预测，主要是证明金字塔分层独立预测的表达能力。显然 finest level 的效果不如 FPN 好，原因在于 PRN 网络是一个窗口大小固定的滑动窗口检测器，因此在金字塔的不同层滑动可以增加其对尺度变化的鲁棒性。**另外（f）有更多的 anchor，说明增加 anchor 的数量并不能有效提高准确率**。\n\n![Figure2](../../data/images/FPN/Figure2.png)\n\n## 代码解读\n\n这里给出一个基于 `Pytorch` 的 `FPN` 网络的代码，来自[这里](https://github.com/kuangliu/pytorch-fpn/blob/master/fpn.py)。\n\n```python\n## ResNet的block\nclass Bottleneck(nn.Module):\n    expansion = 4\n    def __init__(self, in_planes, planes, stride=1):\n        super(Bottleneck, self).__init__()\n        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(planes)\n        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(planes)\n        self.conv3 = nn.Conv2d(planes, self.expansion*planes, kernel_size=1, bias=False)\n        self.bn3 = nn.BatchNorm2d(self.expansion*planes)\n        self.shortcut = nn.Sequential()\n        if stride != 1 or in_planes != self.expansion*planes:\n            self.shortcut = nn.Sequential(\n                nn.Conv2d(in_planes, self.expansion*planes, kernel_size=1, stride=stride, bias=False),\n                nn.BatchNorm2d(self.expansion*planes)\n            )\n    def forward(self, x):\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = F.relu(self.bn2(self.conv2(out)))\n        out = self.bn3(self.conv3(out))\n        out += self.shortcut(x)\n        out = F.relu(out)\n        return out\nclass FPN(nn.Module):\n    def __init__(self, block, num_blocks):\n        super(FPN, self).__init__()\n        self.in_planes = 64\n        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)\n        self.bn1 = nn.BatchNorm2d(64)\n        # Bottom-up layers, backbone of the network\n        self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)\n        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)\n        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)\n        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)\n        # Top layer\n        # 我们需要在C5后面接一个1x1, 256 conv，得到金字塔最顶端的feature\n        self.toplayer = nn.Conv2d(2048, 256, kernel_size=1, stride=1, padding=0) # Reduce channels\n        # Smooth layers\n        # 这个是上面引文中提到的抗aliasing的3x3卷积\n        self.smooth1 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)\n        self.smooth2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)\n        self.smooth3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)\n        # Lateral layers\n        # 为了匹配channel dimension引入的1x1卷积\n        # 注意这些backbone之外的extra conv，输出都是256 channel\n        self.latlayer1 = nn.Conv2d(1024, 256, kernel_size=1, stride=1, padding=0)\n        self.latlayer2 = nn.Conv2d( 512, 256, kernel_size=1, stride=1, padding=0)\n        self.latlayer3 = nn.Conv2d( 256, 256, kernel_size=1, stride=1, padding=0)\n    def _make_layer(self, block, planes, num_blocks, stride):\n        strides = [stride] + [1]*(num_blocks-1)\n        layers = []\n        for stride in strides:\n            layers.append(block(self.in_planes, planes, stride))\n            self.in_planes = planes * block.expansion\n        return nn.Sequential(*layers)\n    ## FPN的lateral connection部分: upsample以后，element-wise相加\n    def _upsample_add(self, x, y):\n        '''Upsample and add two feature maps.\n        Args:\n          x: (Variable) top feature map to be upsampled.\n          y: (Variable) lateral feature map.\n        Returns:\n          (Variable) added feature map.\n        Note in PyTorch, when input size is odd, the upsampled feature map\n        with `F.upsample(..., scale_factor=2, mode='nearest')`\n        maybe not equal to the lateral feature map size.\n        e.g.\n        original input size: [N,_,15,15] ->\n        conv2d feature map size: [N,_,8,8] ->\n        upsampled feature map size: [N,_,16,16]\n        So we choose bilinear upsample which supports arbitrary output sizes.\n        '''\n        _,_,H,W = y.size()\n        return F.upsample(x, size=(H,W), mode='bilinear') + y\n    def forward(self, x):\n        # Bottom-up\n        c1 = F.relu(self.bn1(self.conv1(x)))\n        c1 = F.max_pool2d(c1, kernel_size=3, stride=2, padding=1)\n        c2 = self.layer1(c1)\n        c3 = self.layer2(c2)\n        c4 = self.layer3(c3)\n        c5 = self.layer4(c4)\n        # Top-down\n        # P5: 金字塔最顶上的feature\n        p5 = self.toplayer(c5)\n        # P4: 上一层 p5 + 侧边来的 c4\n        # 其余同理\n        p4 = self._upsample_add(p5, self.latlayer1(c4))\n        p3 = self._upsample_add(p4, self.latlayer2(c3))\n        p2 = self._upsample_add(p3, self.latlayer3(c2))\n        # Smooth\n        # 输出做一下smooth\n        p4 = self.smooth1(p4)\n        p3 = self.smooth2(p3)\n        p2 = self.smooth3(p2)\n        return p2, p3, p4, p5\n```\n\n## 参考资料\n\n+ [FPN（feature pyramid networks）算法讲解](https://blog.csdn.net/u014380165/article/details/72890275)\n+ [Mask RCNN 源代码解析 (1) - 整体思路](https://blog.csdn.net/hnshahao/article/details/81231211)\n+ [Mask RCNN 学习笔记](https://www.cnblogs.com/wangyong/p/10614898.html)\n+ [论文 - Feature Pyramid Networks for Object Detection (FPN)](https://xmfbit.github.io/2018/04/02/paper-fpn/)"
  },
  {
    "path": "5-computer_vision/2D目标检测/4-Mask-RCNN详解.md",
    "content": "- [ROI Pooling 和 ROI Align 的区别](#roi-pooling-和-roi-align-的区别)\n- [Mask R-CNN 网络结构](#mask-r-cnn-网络结构)\n- [骨干网络 FPN](#骨干网络-fpn)\n- [anchor 锚框生成规则](#anchor-锚框生成规则)\n- [实验](#实验)\n- [参考资料](#参考资料)\n\n> `Mask RCNN` 是作者 `Kaiming He` 于 `2018` 年发表的论文\n\n## ROI Pooling 和 ROI Align 的区别\n\n[Understanding Region of Interest — (RoI Align and RoI Warp)](https://towardsdatascience.com/understanding-region-of-interest-part-2-roi-align-and-roi-warp-f795196fc193)\n\n## Mask R-CNN 网络结构\n\n`Mask RCNN` 继承自 `Faster RCNN` 主要有三个改进：\n\n+ `feature map` 的提取采用了 `FPN` 的多尺度特征网络\n+ `ROI Pooling` 改进为 `ROI Align`\n+ 在 `RPN` 后面，增加了采用 `FCN` 结构的 `mask` 分割分支\n\n网络结构如下图所示：\n\n![mask-rcnn网络结构](../../data/images/mask-rcnn/mask-rcnn网络结构.png)\n\n可以看出，Mask RCNN 是一种先检测物体，再分割的思路，简单直接，在建模上也更有利于网络的学习。\n\n## 骨干网络 FPN\n\n卷积网络的一个重要特征：深层网络容易响应语义特征，浅层网络容易响应图像特征。`Mask RCNN` 的使用了 `ResNet` 和 `FPN` 结合的网络作为特征提取器。\n\n`FPN` 的代码出现在 `./mrcnn/model.py`中，核心代码如下：\n\n```python\nif callable(config.BACKBONE):\n    _, C2, C3, C4, C5 = config.BACKBONE(input_image, stage5=True,\n                                        train_bn=config.TRAIN_BN)\nelse:\n    _, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE,\n                                        stage5=True, train_bn=config.TRAIN_BN)\n# Top-down Layers\n# TODO: add assert to varify feature map sizes match what's in config\nP5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5')(C5)\nP4 = KL.Add(name=\"fpn_p4add\")([\n    KL.UpSampling2D(size=(2, 2), name=\"fpn_p5upsampled\")(P5),\n    KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c4p4')(C4)])\nP3 = KL.Add(name=\"fpn_p3add\")([\n    KL.UpSampling2D(size=(2, 2), name=\"fpn_p4upsampled\")(P4),\n    KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c3p3')(C3)])\nP2 = KL.Add(name=\"fpn_p2add\")([\n    KL.UpSampling2D(size=(2, 2), name=\"fpn_p3upsampled\")(P3),\n    KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c2p2')(C2)])\n# Attach 3x3 conv to all P layers to get the final feature maps.\nP2 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding=\"SAME\", name=\"fpn_p2\")(P2)\nP3 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding=\"SAME\", name=\"fpn_p3\")(P3)\nP4 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding=\"SAME\", name=\"fpn_p4\")(P4)\nP5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding=\"SAME\", name=\"fpn_p5\")(P5)\n# P6 is used for the 5th anchor scale in RPN. Generated by\n# subsampling from P5 with stride of 2.\nP6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2, name=\"fpn_p6\")(P5)\n\n# Note that P6 is used in RPN, but not in the classifier heads.\nrpn_feature_maps = [P2, P3, P4, P5, P6]\nmrcnn_feature_maps = [P2, P3, P4, P5]\n```\n\n其中 `resnet_graph` 函数定义如下：\n\n```python\ndef resnet_graph(input_image, architecture, stage5=False, train_bn=True):\n    \"\"\"Build a ResNet graph.\n        architecture: Can be resnet50 or resnet101\n        stage5: Boolean. If False, stage5 of the network is not created\n        train_bn: Boolean. Train or freeze Batch Norm layers\n    \"\"\"\n    assert architecture in [\"resnet50\", \"resnet101\"]\n    # Stage 1\n    x = KL.ZeroPadding2D((3, 3))(input_image)\n    x = KL.Conv2D(64, (7, 7), strides=(2, 2), name='conv1', use_bias=True)(x)\n    x = BatchNorm(name='bn_conv1')(x, training=train_bn)\n    x = KL.Activation('relu')(x)\n    C1 = x = KL.MaxPooling2D((3, 3), strides=(2, 2), padding=\"same\")(x)\n    # Stage 2\n    x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1), train_bn=train_bn)\n    x = identity_block(x, 3, [64, 64, 256], stage=2, block='b', train_bn=train_bn)\n    C2 = x = identity_block(x, 3, [64, 64, 256], stage=2, block='c', train_bn=train_bn)\n    # Stage 3\n    x = conv_block(x, 3, [128, 128, 512], stage=3, block='a', train_bn=train_bn)\n    x = identity_block(x, 3, [128, 128, 512], stage=3, block='b', train_bn=train_bn)\n    x = identity_block(x, 3, [128, 128, 512], stage=3, block='c', train_bn=train_bn)\n    C3 = x = identity_block(x, 3, [128, 128, 512], stage=3, block='d', train_bn=train_bn)\n    # Stage 4\n    x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a', train_bn=train_bn)\n    block_count = {\"resnet50\": 5, \"resnet101\": 22}[architecture]\n    for i in range(block_count):\n        x = identity_block(x, 3, [256, 256, 1024], stage=4, block=chr(98 + i), train_bn=train_bn)\n    C4 = x\n    # Stage 5\n    if stage5:\n        x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a', train_bn=train_bn)\n        x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b', train_bn=train_bn)\n        C5 = x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c', train_bn=train_bn)\n    else:\n        C5 = None\n    return [C1, C2, C3, C4, C5]\n```\n\n## anchor 锚框生成规则\n\n在 Faster-RCNN 中可以将 `SCALE` 也可以设置为多个值，而在 Mask RCNN 中则是每一特征层只对应着一个`SCALE` 即对应着上述所设置的 16。\n\n## 实验\n\n何凯明在论文中做了很多对比单个模块试验，并放出了对比结果表格。\n\n![对比试验结果](../../data/images/mask-rcnn/对比试验结果.png)\n\n从上图表格可以看出：\n\n+ `sigmoid` 和 `softmax` 对比，`sigmoid` 有不小提升；\n+ 特征网络选择：可以看出更深的网络和采用 `FPN` 的实验效果更好，可能因为 FPN 综合考虑了不同尺寸的 `feature map` 的信息，因此能够把握一些更精细的细节。\n+ `RoI Align` 和 `RoI Pooling` 对比：在 instance segmentation 和 object detection 上都有不小的提升。这样看来，**RoIAlign 其实就是一个更加精准的 RoIPooling**，把前者放到 Faster RCNN 中，对结果的提升应该也会有帮助。\n\n## 参考资料\n\n[Mask R-CNN 论文](https://arxiv.org/abs/1703.06870)"
  },
  {
    "path": "5-computer_vision/2D目标检测/5-Cascade-RCNN论文解读.md",
    "content": "- [摘要](#摘要)\n- [1，介绍](#1介绍)\n  - [1.1，Faster RCNN 回顾](#11faster-rcnn-回顾)\n  - [1.2，mismatch 问题](#12mismatch-问题)\n- [2，实验分析](#2实验分析)\n  - [2.1，改变IoU阈值对Detector性能的影响](#21改变iou阈值对detector性能的影响)\n  - [2.2，提高IoU阈值的影响](#22提高iou阈值的影响)\n  - [2.3，和Iterative BBox比较](#23和iterative-bbox比较)\n- [3，网络结构](#3网络结构)\n- [参考资料](#参考资料)\n\n## 摘要\n\n虽然低 `IoU` 阈值，如 `0.5`，会产生噪声检测（`noisy detections`），但是，随着 `IoU` 阈值的增加，检测性能往往会下降。造成这种情况的主要因素有两个：1）由于在训练过程中正样本呈指数下降，过少的正样本导致网络训练期间过拟合。2)**dismatch**：检测器在最优的 `IoU` 与输入预测的 `IoU` 之间会产生`mismatch`。由此，我们提出了多阶段的目标检测器结构：`Cascade R-CNN` 来解决 `IoU` 选择的问题。它**由一系列不断增加 `IoU` 阈值的检测器组成，可以逐步的更接近目标的预测。**。检测器是逐步训练的，前一个检测器输出一个良好的数据分布并作为输入，用于训练下一个更高质量的检测器。逐步改进的重采样保证了所有检测器都有一组相同大小的正样本，从而减少了过拟合问题。在 `inference` 阶段使用级联的检测器结构可以合理的提高了 `IOU` 的阈值而不会出现 `mismatch` 问题。\n\n## 1，介绍\n\n> `Cascade RCNN` 是作者 `Zhaowei Cai` 于 `2018` 年发表的论文 `Cascade R-CNN: Delving into High Quality Object Detection`.\n\n 目标检测是一个复杂的问题，需要解决两个主要任务。首先，检测器必须解决**识别**问题，区分前景目标和背景目标，并为其分配匹配的类别标签。其次，探测器必须解决**定位**问题，为不同的目标分配精确的 `bounding box`。许多目标探测器都是基于两阶段网络框架 `Faster R-CNN` 的。双阶段检测网络是一个多任务学习问题，包括目标的分类和边界回归。**与物体识别不同的是，定义正/负样本需要一个 IoU 阈值**。通常使用的 `IOU` 阈值 `u=0.5`，`0.5` 对 `IOU` 的设置是相当低的。检测的目标经常包含很多噪声，如图 (a)所示。IOU 阈值取0.5，会有很多假的预测信息也都包含在内，从而会产生很多错误的预测信息。\n\n![Figure1](../../data/images/cascade-rcnn/Figure1.png)\n\n### 1.1，Faster RCNN 回顾\n\n先回顾下 `Faster RCNN` 的结构，下图是 `Faster RCNN` 的结构图。\n\n![Faster-rcnn网络结构图](../../data/images/faster-rcnn/Faster-rcnn网络结构图.png)\n\n`training` 阶段和 `inference` 阶段的不同在于，`inference` 阶段不能对 `proposala` 进行采样（因为不知道 `gt`，自然无法计算 `IoU`），所以 `RPN` 网络输出的 `300` `RoIs`(`Proposals`)会直接输入到 `RoI pooling` 中，之后通过两个全连接层分别进行类别分类和 `bbox` 回归。\n\n值得注意的是，`Faster RCNN` 网络在 `RPN` 和 `Fast RCNN` 阶段都需要计算 `IoU`，用于判定 `positive` 和 `negative`。前者是生成 `256` 个 `Proposal` 用于 `RPN` 网络训练，后者是生成 `128` 个 `RoIs`(可以理解为 `RPN` 网络优化后的 `Proposals`)用于 `Fast RCNN` 训练。\n\n### 1.2，mismatch 问题\n\n`training` 阶段和 `inference` 阶段，`bbox` 回归器的输入 `proposals` 分布是不一样的，`training` 阶段的输入`proposals` 质量更高(被采样过，IoU > threshold)，`inference` 阶段的输入 `proposals` 质量相对较差（没有被采样过，可能包括很多 IoU < threshold 的），这就是论文中提到 `mismatch` 问题，这个问题是固有存在的，但通常 `threshold` 取 `0.5` 时，`mismatch` 问题还不会很严重。\n\n## 2，实验分析\n\n### 2.1，改变IoU阈值对Detector性能的影响\n\n![提升IOU阈值对检测器性能的影响](../../data/images/cascade-rcnn/提升IOU阈值对检测器性能的影响.png)\n\n从上图可以看出：\n\n- 同一个 detector 通常只会在一个小范围的 IoU 阈值 内性能最好，比如 IoU 阈值为 0.5 的 detector，在输入 `proposal` 和 `gt` 的阈值为 `0.55-0.6` 范围内，其性能最好。阈值为 0.6 的 detector 则在 0.6~0.75 阈值范围内性能最佳。\n- 几乎所有的检测器输出框的 IoU 都好于输入 proposal 的 IoU（红绿蓝三条曲线都在灰色对角线上方）。\n\n### 2.2，提高IoU阈值的影响\n\n主要是分析对提高 IoU 阈值对 RPN 输出 Proposal 数量的影响，实验结果如下图所示。\n\n![提高IoU阈值的影响](../../data/images/cascade-rcnn/提高IoU阈值的影响.png)\n\n上图纵坐标表示 `RPN` 输出 proposal 在各个 IoU 范围内的数量。\n\n- 第一张图表示级联结构的第一级，可以等同为没有级联结构的 `RCNN` 网络。从图中可以看出，随着 IoU 的增加，IoU 在 0.6,0.7 及以上范围内的 proposal 数量越来越少。虽然这样产生更高精度的 proposal，但是也带来了两个问题：\n  - **过拟合**。\n  - **更严重的 `mismatch` 问题**。`RCNN` 结构本身就存在这个问题，IoU 阈值的提高又加剧了这个问题。\n\n- 第二、三图表示有级联结构的 `RCNN`，从图中可以看出，随着 `stage` 的加深，相应区域的依然拥有大量的 `proposal`，因此不会出现严重的过拟合的现象。\n\n\n### 2.3，和Iterative BBox比较\n\n`Iterative BBox` 的 `H` 位置都是共享的，而且 `3` 个分支的 `IoU` 阈值都取 `0.5`。`Iterative BBox` 存在两个问题：\n\n- 单一阈值 `0.5` 是无法对所有 `proposal` 取得良好效果。\n- 此外，`detector` 会改变样本的分布，使用同一个共享的 `H` 对检测是有影响的。作者做了下面的实验证明样本分布在各个`stage` 的变化。\n\n![Figure2](../../data/images/cascade-rcnn/Figure2.png)\n\n红色表示离群点。\n\n- 从上图可以看出，没经过一次回归，样本都会更靠近 `gt`，即输出的样本分布会逐渐变化，使用同一个阈值 `0.5` 的条件下，后面两个 `stage` 就会有较多的离群点，使用共享的 `Head` 网络权重是无法满足输入的变化的。\n- 从上图还可以看出，每个阶段设置不同的 IoU 阈值，可以更好的去除离群点，从而适应不同的输入 `proposal` 分布。\n\n## 3，网络结构\n\n网络结构如下图(d)\n\n![cascade_rcnn和其他框架的网络结构简略图](../../data/images/cascade-rcnn/cascade_rcnn和其他框架的网络结构简略图.png)\n\n上图中 (d) 和 (c) 很像，`iterative bbox at inference` 是在推断时候对回归框进行后处理，即模型输出预测结果后再多次处理，而 `Cascade R-CNN` 在训练的时候就进行重新采样，不同的 `stage` 的输入数据分布已经是不同的了。\n\n简单来说 cascade R-CNN 是由一系列的检测模型组成，每个检测模型都基于不同 IOU 阈值的正负样本训练得到，前一个检测模型的输出作为后一个检测模型的输入，因此是 stage by stage 的训练方式，而且越往后的检测模型，其界定正负样本的 IOU 阈值是不断上升的。\n\nCascade R-CNN 的几个检测网络（`Head` 网络）是基于不同的 IOU 阈值确定的正负样本上训练得到的。\n\n作者在 COCO 数据集上做了对比实验，达到了 `state-of-the-art` 精度。其中 `backbone` 为`RsNet-101` 的 `Cascade RCNN` 的 `AP` 达到了 `42.8`。\n\n![对比实验结果](../../data/images/cascade-rcnn/对比实验结果.png)\n\n## 参考资料\n\n- [Cascade R-CNN 详细解读](https://zhuanlan.zhihu.com/p/42553957)\n- [Cascade R-CNN解析](https://bbs.huaweicloud.com/blogs/291101)\n"
  },
  {
    "path": "5-computer_vision/2D目标检测/6-RetinaNet网络详解.md",
    "content": "- [摘要](#摘要)\n- [1，引言](#1引言)\n- [2，相关工作](#2相关工作)\n- [3，网络架构](#3网络架构)\n- [3.1，Backbone](#31backbone)\n  - [3.2，Neck](#32neck)\n  - [3.3，Head](#33head)\n- [4，Focal Loss](#4focal-loss)\n  - [4.1，Cross Entropy](#41cross-entropy)\n  - [4.2，Balanced Cross Entropy](#42balanced-cross-entropy)\n  - [4.3，Focal Loss Definition](#43focal-loss-definition)\n- [5，代码解读](#5代码解读)\n  - [5.1，Backbone](#51backbone)\n  - [5.2，Neck](#52neck)\n  - [5.3，Head](#53head)\n  - [5.4，先验框Anchor赋值](#54先验框anchor赋值)\n  - [5.5，BBox Encoder Decoder](#55bbox-encoder-decoder)\n  - [5.6，Focal Loss](#56focal-loss)\n- [参考资料](#参考资料)\n\n## 摘要\n\n> Retinanet 是作者 Tsung-Yi Lin 和 Kaiming He（四作） 于 2018 年发表的论文 Focal Loss for Dense Object Detection.\n\n作者深入分析了极度不平衡的正负（前景背景）样本比例导致 one-stage 检测器精度低于 two-stage 检测器，基于上述分析，提出了一种简单但是非常实用的 Focal Loss 焦点损失函数，并且 Loss 设计思想可以推广到其他领域，同时针对目标检测领域特定问题，设计了 RetinaNet 网络，结合 Focal Loss 使得 one-stage 检测器在精度上能够达到乃至超过 two-stage 检测器。\n\n## 1，引言\n\n作者认为一阶段检测器的精度不能和两阶段检测相比的原因主要在于，训练过程中的类别不平衡，由此提出了一种新的损失函数-`Focal Loss`。\n\n`R-CNN(Fast RCNN)` 类似的检测器之所以能解决类别不平衡问题，是因为**两阶段级联结构和启发式采样**。提取 `proposal` 阶段（例如，选择性搜索、EdgeBoxes、DeepMask、`RPN`）很快的将候选对象位置的数量缩小到一个小数目（例如，1-2k），过滤掉大多数背景样本（其实就是筛选 `anchor` 数量）。在第二个分类阶段，执行启发式采样（`sampling heuristics`），例如固定的前景背景比（`1:3`），或在线难样本挖掘（`OHEM`），以保持前景和背景之间的平衡。\n\n相比之下，单级检测器必须处理在图像中定期采样的一组更大的候选对象位置。实际上，这通常相当于枚举 `∼100k` 个位置，这些位置密集地覆盖空间位置、尺度和纵横。虽然也可以应用类似的启发式采样方法，但效率低下，因为训练过程仍然由易于分类的背景样本主导。\n\n## 2，相关工作\n\n**Two-stage Detectors**: 与之前使用两阶段的分类器生成 `proposal` 不同，`Faster RCNN` 模型的 `RPN` 使用单个卷积就可以生成 `proposal`。\n\n**One-stage Detectors**：最近的一些研究表明，只需要降低输入图像分辨率和 `proposal` 数量，两阶段检测器速度就可以变得更快。但是，对于一阶段检测器，即使提高模型计算量，其最后的精度也落后于两阶段方法[17]。同时，作者强调，`Reinanet` 达到很好的结果的原因不在于网络结构的创新，而在于损失函数的创新。\n> 论文 [17] Speed/accuracy trade-offs for modern convolutional object detectors（注重实验）. 但是，从这几年看，一阶段检测器也可以达到很高的精度，甚至超过两阶段检测器，这几年的一阶段检测和两阶段检测器有相互融合的趋势了。\n\n**`Class Imbalance:`** 早期的目标检测器 `SSD` 等在训练过程中会面临严重的类别不平衡（`class imbalance`）的问题，即正样本太少，负样本太多，这会导致两个问题：\n\n- 训练效率低下：大多数候选区域都是容易分类的负样本，并没有提供多少有用的学习信号。\n- 模型退化：易分类的负样本太多会压倒训练，导致模型退化。\n\n通常的解决方案是执行某种形式的**难负样本挖掘**，如在训练时进行难负样本采样或更复杂的采样/重新称重方案。相比之下，`Focla Loss` 自然地处理了单级检测器所面临的类别不平衡，并且**允许在所有示例上有效地训练**，而不需要采样，也不需要容易的负样本来压倒损失和计算的梯度。\n\n**Robust Estimation**: 人们对设计稳健的损失函数（例如 `Huber loss`）很感兴趣，该函数通过降低具有大错误的示例（硬示例）的损失来减少对总损失的贡献。相反， `Focal Loss` 对容易样本(`inliers`)减少权重来解决（`address`）类别不平衡问题（`class imbalance`），这意味着即使容易样本数量大，但是其对总的损失函数贡献也很小。换句话说，`Focal Loss` 与鲁棒损失相反，它**侧重于训练稀疏的难样本**。\n\n## 3，网络架构\n\n`retinanet` 的网络架构图如下所示。\n\n![网络架构图](../../data/images/retinanet/网络架构图.png)\n\n## 3.1，Backbone\n\n`Retinanet` 的 `Backbone` 为 `ResNet` 网络，`ResNet` 一般从 `18` 层到 `152` 层（甚至更多）不等，主要区别在于采用的残差单元/模块不同或者堆叠残差单元/模块的数量和比例不同，论文主要使用 `ResNet50`。\n\n两种残差块结构如下图所示，`ResNet50` 及更深的 `ResNet` 网络使用的是 `bottleneck` 残差块。\n\n![两种残差块结构](../../data/images/retinanet/两种残差块结构.png)\n\n### 3.2，Neck\n\n`Neck` 模块即为 `FPN` 网络结构。FPN 模块接收 c3, c4, c5 三个特征图，输出 P2-P7 五个特征图，通道数都是 256, stride 为 (8,16,32,64,128)，**其中大 stride (特征图小)用于检测大物体，小 stride (特征图大)用于检测小物体**。P6 和 P7 目的是提供一个**大感受野强语义**的特征图，有利于大物体和超大物体检测。注意：在 RetinaNet 的 FPN 模块中只包括卷积，不包括 BN 和 ReLU。\n\n### 3.3，Head\n\n`Head` 即预测头网络。\n\n`YOLOv3` 的 `neck` 输出 `3` 个分支，即输出 `3` 个特征图， `head` 模块只有一个分支，由卷积层组成，该卷积层完成目标分类和位置回归的功能。总的来说，`YOLOv3` 网络的 `3` 个特征图有 `3` 个预测分支，分别预测 `3` 个框，也就是分别预测大、中、小目标。\n\n`Retinanet` 的 `neck` 输出 `5` 个分支，即输出 `5` 个特征图。`head` 模块包括分类和位置检测两个分支，每个分支都包括 `4` 个卷积层，但是 `head` 模块的这两个分支之间参数不共享，分类 `Head` 输出通道是 A\\*K，A 是类别数；检测 `head` 输出通道是 4*K, K 是 anchor 个数, 虽然每个 Head 的分类和回归分支权重不共享，但是 `5` 个输出特征图的 Head 模块权重是共享的。\n\n## 4，Focal Loss\n\n`Focal Loss` 是在二分类问题的交叉熵（`CE`）损失函数的基础上引入的，所以需要先学习下交叉熵损失的定义。\n\n### 4.1，Cross Entropy\n\n> 可额外阅读文章 [理解交叉熵损失函数](https://zhuanlan.zhihu.com/p/339684056)。\n\n在深度学习中我们常使用交叉熵来作为分类任务中训练数据分布和模型预测结果分布间的代价函数。对于同一个离散型随机变量 $\\textrm{x}$ 有两个单独的概率分布 $P(x)$ 和 $Q(x)$，其交叉熵定义为：\n> P 表示真实分布， Q 表示预测分布。\n\n$$H(P,Q) = \\mathbb{E}_{\\textrm{x}\\sim P} log Q(x)= -\\sum_{i}P(x_i)logQ(x_i) \\tag{1} $$\n\n但在实际计算中，我们通常不这样写，因为不直观。在深度学习中，以二分类问题为例，其交叉熵损失（`CE`）函数如下：\n\n$$Loss = L(y, p) = -ylog(p)-(1-y)log(1-p) \\tag{2}$$\n\n其中 $p$ 表示当预测样本等于 $1$ 的概率，则 $1-p$ 表示样本等于 $0$ 的预测概率。因为是二分类，所以样本标签 $y$ 取值为 $\\{1,0\\}$，上式可缩写至如下：\n\n$$CE = \\left\\{\\begin{matrix}\n-log(p), & if \\quad y=1 \\\\ \n-log(1-p), &  if\\quad y=0  \\tag{3} \n\\end{matrix}\\right.$$\n\n为了方便，用 $p_t$ 代表 $p$，$p_t$ 定义如下：\n\n$$p_t = \\left\\{\\begin{matrix}\np, & if \\quad y=1 \\\\ \n1-p, &  if\\quad y=0\n\\end{matrix}\\right.$$\n\n则$(3)$式可写成：\n\n$$CE(p, y) = CE(p_t) = -log(p_t) \\tag{4}$$\n\n前面的交叉熵损失计算都是针对单个样本的，对于**所有样本**，二分类的交叉熵损失计算如下：\n\n$$L = \\frac{1}{N}(\\sum_{y_i = 1}^{m}-log(p)-\\sum_{y_i = 0}^{n}log(1-p))$$\n\n其中 $m$ 为正样本个数，$n$ 为负样本个数，$N$ 为样本总数，$m+n=N$。当样本类别不平衡时，损失函数 $L$ 的分布也会发生倾斜，如 $m \\ll n$ 时，负样本的损失会在总损失占主导地位。又因为损失函数的倾斜，模型训练过程中也会倾向于样本多的类别，造成模型对少样本类别的性能较差。\n\n再衍生以下，对于**所有样本**，多分类的交叉熵损失计算如下：\n\n$$L = \\frac{1}{N} \\sum_i^N L_i = -\\frac{1}{N}(\\sum_i \\sum_{c=1}^M y_{ic}log(p_{ic})$$\n\n其中，$M$ 表示类别数量，$y_{ic}$ 是符号函数，如果样本 $i$ 的真实类别等于 $c$ 取值 1，否则取值 0; $p_{ic}$ 表示样本 $i$ 预测为类别 $c$ 的概率。\n\n对于多分类问题，交叉熵损失一般会结合 `softmax` 激活一起实现，`PyTorch` 代码如下，代码出自[这里](https://mp.weixin.qq.com/s/FGyV763yIKsXNM40lMO61g)。\n\n```python\n\nimport numpy as np\n\n# 交叉熵损失\nclass CrossEntropyLoss():\n    \"\"\"\n    对最后一层的神经元输出计算交叉熵损失\n    \"\"\"\n    def __init__(self):\n        self.X = None\n        self.labels = None\n    \n    def __call__(self, X, labels):\n        \"\"\"\n        参数：\n            X: 模型最后fc层输出\n            labels: one hot标注，shape=(batch_size, num_class)\n        \"\"\"\n        self.X = X\n        self.labels = labels\n\n        return self.forward(self.X)\n    \n    def forward(self, X):\n        \"\"\"\n        计算交叉熵损失\n        参数：\n            X：最后一层神经元输出，shape=(batch_size, C)\n            label：数据onr-hot标注，shape=(batch_size, C)\n        return：\n            交叉熵loss\n        \"\"\"\n        self.softmax_x = self.softmax(X)\n        log_softmax = self.log_softmax(self.softmax_x)\n        cross_entropy_loss = np.sum(-(self.labels * log_softmax), axis=1).mean()\n        return cross_entropy_loss\n    \n    def backward(self):\n        grad_x =  (self.softmax_x - self.labels)  # 返回的梯度需要除以batch_size\n        return grad_x / self.X.shape[0]\n        \n    def log_softmax(self, softmax_x):\n        \"\"\"\n        参数:\n            softmax_x, 在经过softmax处理过的X\n        return: \n            log_softmax处理后的结果shape = (m, C)\n        \"\"\"\n        return np.log(softmax_x + 1e-5)\n    \n    def softmax(self, X):\n        \"\"\"\n        根据输入，返回softmax\n        代码利用softmax函数的性质: softmax(x) = softmax(x + c)\n        \"\"\"\n        batch_size = X.shape[0]\n        # axis=1 表示在二维数组中沿着横轴进行取最大值的操作\n        max_value = X.max(axis=1)\n        #每一行减去自己本行最大的数字,防止取指数后出现inf，性质：softmax(x) = softmax(x + c)\n        # 一定要新定义变量，不要用-=，否则会改变输入X。因为在调用计算损失时，多次用到了softmax，input不能改变\n        tmp = X - max_value.reshape(batch_size, 1)\n        # 对每个数取指数\n        exp_input = np.exp(tmp)  # shape=(m, n)\n        # 求出每一行的和\n        exp_sum = exp_input.sum(axis=1, keepdims=True)  # shape=(m, 1)\n        return exp_input / exp_sum\n```\n\n### 4.2，Balanced Cross Entropy\n\n对于正负样本不平衡的问题，较为普遍的做法是引入 $\\alpha \\in(0,1)$ 参数来解决，上面公式重写如下：\n\n$$CE(p_t) = -\\alpha log(p_t) = \\left\\{\\begin{matrix}\n-\\alpha log(p), & if \\quad y=1\\\\ \n-(1-\\alpha)log(1-p), &  if\\quad y=0\n\\end{matrix}\\right.$$\n\n对于所有样本，二分类的平衡交叉熵损失函数如下：\n\n$$L = \\frac{1}{N}(\\sum_{y_i = 1}^{m}-\\alpha log(p)-\\sum_{y_i = 0}^{n}(1 - \\alpha) log(1-p))$$\n\n其中 $\\frac{\\alpha}{1-\\alpha} = \\frac{n}{m}$，即 $\\alpha$ 参数的值是根据正负样本分布比例来决定的，\n\n### 4.3，Focal Loss Definition\n\n虽然 $\\alpha$ 参数平衡了正负样本（`positive/negative examples`），但是它并不能区分难易样本（`easy/hard examples`），而实际上，目标检测中大量的候选目标都是易分样本。这些样本的损失很低，但是由于难易样本数量极不平衡，易分样本的数量相对来讲太多，最终主导了总的损失。而本文的作者认为，易分样本（即，置信度高的样本）对模型的提升效果非常小，模型应该主要关注与那些难分样本（这个假设是有问题的，是 `GHM` 的主要改进对象）\n\n`Focal Loss` 作者建议在交叉熵损失函数上加上一个调整因子（`modulating factor`）$(1-p_t)^\\gamma$，把高置信度 $p$（易分样本）样本的损失降低一些。`Focal Loss` 定义如下：\n\n$$FL(p_t) = -(1-p_t)^\\gamma log(p_t) = \\left\\{\\begin{matrix}\n-(1-p)^\\gamma log(p), & if \\quad y=1 \\\\ \n-p^\\gamma log(1-p), &  if\\quad y=0\n\\end{matrix}\\right.$$\n\n`Focal Loss` 有两个性质：\n\n+ 当样本被错误分类且 $p_t$ 值较小时，调制因子接近于 `1`，`loss` 几乎不受影响；当 $p_t$ 接近于 `1`，调质因子（`factor`）也接近于 `0`，**容易分类样本的损失被减少了权重**，整体而言，相当于增加了分类不准确样本在损失函数中的权重。\n+ $\\gamma$ 参数平滑地调整容易样本的权重下降率，当 $\\gamma = 0$ 时，`Focal Loss` 等同于 `CE Loss`。 $\\gamma$ 在增加，调制因子的作用也就增加，实验证明  $\\gamma = 2$ 时，模型效果最好。\n\n直观地说，**调制因子减少了简单样本的损失贡献，并扩大了样本获得低损失的范围**。例如，当$\\gamma = 2$ 时，与 $CE$ 相比，分类为 $p_t = 0.9$ 的样本的损耗将降低 `100` 倍，而当 $p_t = 0.968$ 时，其损耗将降低 `1000` 倍。这反过来又增加了错误分类样本的重要性（对于 $pt≤0.5$ 和 $\\gamma = 2$，其损失最多减少 `4` 倍）。在训练过程关注对象的排序为正难 > 负难 > 正易 > 负易。\n\n![难易正负样本](../../data/images/难易正负样本.jpg)\n\n在实践中，我们常采用带 $\\alpha$ 的 `Focal Loss`：\n\n$$FL(p_t) = -\\alpha (1-p_t)^\\gamma log(p_t)$$\n\n作者在实验中采用这种形式，发现它比非 $\\alpha$ 平衡形式（non-$\\alpha$-balanced）的精确度稍有提高。实验表明 $\\gamma$ 取 2，$\\alpha$ 取 0.25 的时候效果最佳。\n\n网上有各种版本的 `Focal Loss` 实现代码，大多都是基于某个深度学习框架实现的，如 `Pytorch`和 `TensorFlow`，我选取了一个较为清晰的通用版本代码作为参考，代码来自 [这里](https://github.com/yatengLG/Retinanet-Pytorch/blob/master/Model/struct/Focal_Loss.py)。\n> 后续有必要自己实现以下，有时间还要去看看 `Caffe` 的实现。这里的 Focal Loss 代码与后文不同，这里只是纯粹的用于分类的 Focal_loss 代码，不包含 BBox 的编码过程。\n\n```python\n# -*- coding: utf-8 -*-\n# @Author  : LG\nfrom torch import nn\nimport torch\nfrom torch.nn import functional as F\n\nclass focal_loss(nn.Module):\n    def __init__(self, alpha=0.25, gamma=2, num_classes = 3, size_average=True):\n        \"\"\"\n        focal_loss损失函数, -α(1-yi)**γ *ce_loss(xi,yi)\n        步骤详细的实现了 focal_loss损失函数.\n        :param alpha:   阿尔法α,类别权重.      当α是列表时,为各类别权重,当α为常数时,类别权重为[α, 1-α, 1-α, ....],常用于 目标检测算法中抑制背景类 , retainnet中设置为0.25\n        :param gamma:   伽马γ,难易样本调节参数. retainnet中设置为2\n        :param num_classes:     类别数量\n        :param size_average:    损失计算方式,默认取均值\n        \"\"\"\n        super(focal_loss,self).__init__()\n        self.size_average = size_average\n        if isinstance(alpha,list):\n            assert len(alpha)==num_classes   # α可以以list方式输入,size:[num_classes] 用于对不同类别精细地赋予权重\n            print(\" --- Focal_loss alpha = {}, 将对每一类权重进行精细化赋值 --- \".format(alpha))\n            self.alpha = torch.Tensor(alpha)\n        else:\n            assert alpha<1   #如果α为一个常数,则降低第一类的影响,在目标检测中为第一类\n            print(\" --- Focal_loss alpha = {} ,将对背景类进行衰减,请在目标检测任务中使用 --- \".format(alpha))\n            self.alpha = torch.zeros(num_classes)\n            self.alpha[0] += alpha\n            self.alpha[1:] += (1-alpha) # α 最终为 [ α, 1-α, 1-α, 1-α, 1-α, ...] size:[num_classes]\n\n        self.gamma = gamma\n\n    def forward(self, preds, labels):\n        \"\"\"\n        focal_loss损失计算\n        :param preds:   预测类别. size:[B,N,C] or [B,C]    分别对应与检测与分类任务, B 批次, N检测框数, C类别数\n        :param labels:  实际类别. size:[B,N] or [B]，为 one-hot 编码格式\n        :return:\n        \"\"\"\n        # assert preds.dim()==2 and labels.dim()==1\n        preds = preds.view(-1,preds.size(-1))\n        self.alpha = self.alpha.to(preds.device)\n        preds_logsoft = F.log_softmax(preds, dim=1) # log_softmax\n        preds_softmax = torch.exp(preds_logsoft)    # softmax\n\n        preds_softmax = preds_softmax.gather(1,labels.view(-1,1))\n        preds_logsoft = preds_logsoft.gather(1,labels.view(-1,1))\n        self.alpha = self.alpha.gather(0,labels.view(-1))\n        loss = -torch.mul(torch.pow((1-preds_softmax), self.gamma), preds_logsoft)  # torch.pow((1-preds_softmax), self.gamma) 为focal loss中 (1-pt)**γ\n\n        loss = torch.mul(self.alpha, loss.t())\n        if self.size_average:\n            loss = loss.mean()\n        else:\n            loss = loss.sum()\n        return loss\n```\n\n`mmdetection` 框架给出的 `focal loss` 代码如下（有所删减）：\n\n```python\n# This method is only for debugging\ndef py_sigmoid_focal_loss(pred,\n                          target,\n                          weight=None,\n                          gamma=2.0,\n                          alpha=0.25,\n                          reduction='mean',\n                          avg_factor=None):\n    \"\"\"PyTorch version of `Focal Loss <https://arxiv.org/abs/1708.02002>`_.\n    Args:\n        pred (torch.Tensor): The prediction with shape (N, C), C is the\n            number of classes\n        target (torch.Tensor): The learning label of the prediction.\n        weight (torch.Tensor, optional): Sample-wise loss weight.\n        gamma (float, optional): The gamma for calculating the modulating\n            factor. Defaults to 2.0.\n        alpha (float, optional): A balanced form for Focal Loss.\n            Defaults to 0.25.\n        reduction (str, optional): The method used to reduce the loss into\n            a scalar. Defaults to 'mean'.\n        avg_factor (int, optional): Average factor that is used to average\n            the loss. Defaults to None.\n    \"\"\"\n    pred_sigmoid = pred.sigmoid()\n    target = target.type_as(pred)\n    pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)\n    focal_weight = (alpha * target + (1 - alpha) *\n                    (1 - target)) * pt.pow(gamma)\n    loss = F.binary_cross_entropy_with_logits(\n        pred, target, reduction='none') * focal_weigh\n    return loss\n```\n\n## 5，代码解读\n\n> 代码来源[这里](https://github.com/yhenon/pytorch-retinanet)。\n\n### 5.1，Backbone\n\nRetinaNet 算法采用了 ResNet50 作为 Backbone, 并且考虑到整个目标检测网络比较大，前面部分网络没有进行训练，BN 也不会进行参数更新（来自 OpenMMLab 的经验）。\n\nResNet 不仅提出了残差结构，而且还提出了骨架网络设计范式即 `stem + n stage+ cls head`，对于 ResNet 而言，其实际 forward 流程是 stem -> 4 个 stage -> 分类 head，stem 的输出 stride 是 4，而 4 个 stage 的输出 stride 是 4,8,16,32。\n> `stride` 表示模型的下采样率，假设图片输入是 `320x320`，`stride=10`，那么输出特征图大小是 `32x32` ，假设每个位置 `anchor` 是 `9` 个，那么这个输出特征图就一共有 `32x32x9` 个 `anchor`。\n\n### 5.2，Neck\n\nResNet 输出 4 个不同尺度的特征图（c2,c3,c4,c5），stride 分别是（4,8,16,32），通道数为（256,512,1024,2048）。\n\nNeck 使用的是 `FPN` 网络，且输入是 3 个来自 ResNet 输出的特征图（c3,c4,c5），并输出 `5` 个特征图（p3,p4,p5,p6,p7），额外输出的 2 个特征图的来源是骨架网络输出，而不是 FPN 层本身输出又作为后面层的输入，并且 `FPN` 网络输出的 `5` 个特征图通道数都是 `256`。值得注意的是，**`Neck` 模块输出特征图的大小是由 `Backbone` 决定的，即输出的 `stride` 列表由 `Backbone` 确定**。\n`FPN` 结构的代码如下。\n\n```python\nclass PyramidFeatures(nn.Module):\n    def __init__(self, C3_size, C4_size, C5_size, feature_size=256):\n        super(PyramidFeatures, self).__init__()\n\n        # upsample C5 to get P5 from the FPN paper\n        self.P5_1 = nn.Conv2d(C5_size, feature_size, kernel_size=1, stride=1, padding=0)\n        self.P5_upsampled = nn.Upsample(scale_factor=2, mode='nearest')\n        self.P5_2 = nn.Conv2d(feature_size, feature_size, kernel_size=3, stride=1, padding=1)\n\n        # add P5 elementwise to C4\n        self.P4_1 = nn.Conv2d(C4_size, feature_size, kernel_size=1, stride=1, padding=0)\n        self.P4_upsampled = nn.Upsample(scale_factor=2, mode='nearest')\n        self.P4_2 = nn.Conv2d(feature_size, feature_size, kernel_size=3, stride=1, padding=1)\n\n        # add P4 elementwise to C3\n        self.P3_1 = nn.Conv2d(C3_size, feature_size, kernel_size=1, stride=1, padding=0)\n        self.P3_2 = nn.Conv2d(feature_size, feature_size, kernel_size=3, stride=1, padding=1)\n\n        # \"P6 is obtained via a 3x3 stride-2 conv on C5\"\n        self.P6 = nn.Conv2d(C5_size, feature_size, kernel_size=3, stride=2, padding=1)\n\n        # \"P7 is computed by applying ReLU followed by a 3x3 stride-2 conv on P6\"\n        self.P7_1 = nn.ReLU()\n        self.P7_2 = nn.Conv2d(feature_size, feature_size, kernel_size=3, stride=2, padding=1)\n\n    def forward(self, inputs):\n        C3, C4, C5 = inputs\n\n        P5_x = self.P5_1(C5)\n        P5_upsampled_x = self.P5_upsampled(P5_x)\n        P5_x = self.P5_2(P5_x)\n\n        P4_x = self.P4_1(C4)\n        P4_x = P5_upsampled_x + P4_x\n        P4_upsampled_x = self.P4_upsampled(P4_x)\n        P4_x = self.P4_2(P4_x)\n\n        P3_x = self.P3_1(C3)\n        P3_x = P3_x + P4_upsampled_x\n        P3_x = self.P3_2(P3_x)\n\n        P6_x = self.P6(C5)\n\n        P7_x = self.P7_1(P6_x)\n        P7_x = self.P7_2(P7_x)\n\n        return [P3_x, P4_x, P5_x, P6_x, P7_x]\n```\n\n### 5.3，Head\n\n`RetinaNet` 在特征提取网络 `ResNet-50` 和特征融合网络 `FPN` 后，对获得的五张特征图 `[P3_x, P4_x, P5_x, P6_x, P7_x]`，通过具有相同权重的框回归和分类子网络，获得所有框位置和类别信息。\n\n目标边界框回归和分类子网络（`head` 网络）定义如下：\n\n```python\nclass RegressionModel(nn.Module):\n    def __init__(self, num_features_in, num_anchors=9, feature_size=256):\n        super(RegressionModel, self).__init__()\n\n        self.conv1 = nn.Conv2d(num_features_in, feature_size, kernel_size=3, padding=1)\n        self.act1 = nn.ReLU()\n\n        self.conv2 = nn.Conv2d(feature_size, feature_size, kernel_size=3, padding=1)\n        self.act2 = nn.ReLU()\n\n        self.conv3 = nn.Conv2d(feature_size, feature_size, kernel_size=3, padding=1)\n        self.act3 = nn.ReLU()\n\n        self.conv4 = nn.Conv2d(feature_size, feature_size, kernel_size=3, padding=1)\n        self.act4 = nn.ReLU()\n        # 最后的输出层输出通道数为 num_anchors * 4\n        self.output = nn.Conv2d(feature_size, num_anchors * 4, kernel_size=3, padding=1)\n\n    def forward(self, x):\n        out = self.conv1(x)\n        out = self.act1(out)\n\n        out = self.conv2(out)\n        out = self.act2(out)\n\n        out = self.conv3(out)\n        out = self.act3(out)\n\n        out = self.conv4(out)\n        out = self.act4(out)\n\n        out = self.output(out)\n\n        # out is B x C x W x H, with C = 4*num_anchors = 4*9\n        out = out.permute(0, 2, 3, 1)\n\n        return out.contiguous().view(out.shape[0], -1, 4)\n\n\nclass ClassificationModel(nn.Module):\n    def __init__(self, num_features_in, num_anchors=9, num_classes=80, prior=0.01, feature_size=256):\n        super(ClassificationModel, self).__init__()\n\n        self.num_classes = num_classes\n        self.num_anchors = num_anchors\n\n        self.conv1 = nn.Conv2d(num_features_in, feature_size, kernel_size=3, padding=1)\n        self.act1 = nn.ReLU()\n\n        self.conv2 = nn.Conv2d(feature_size, feature_size, kernel_size=3, padding=1)\n        self.act2 = nn.ReLU()\n\n        self.conv3 = nn.Conv2d(feature_size, feature_size, kernel_size=3, padding=1)\n        self.act3 = nn.ReLU()\n        # 最后的输出层输出通道数为 num_anchors * num_classes(coco数据集9*80)\n        self.conv4 = nn.Conv2d(feature_size, feature_size, kernel_size=3, padding=1)\n        self.act4 = nn.ReLU()\n\n        self.output = nn.Conv2d(feature_size, num_anchors * num_classes, kernel_size=3, padding=1)\n        self.output_act = nn.Sigmoid()\n\n    def forward(self, x):\n        out = self.conv1(x)\n        out = self.act1(out)\n\n        out = self.conv2(out)\n        out = self.act2(out)\n\n        out = self.conv3(out)\n        out = self.act3(out)\n\n        out = self.conv4(out)\n        out = self.act4(out)\n\n        out = self.output(out)\n        out = self.output_act(out)\n\n        # out is B x C x W x H, with C = n_classes + n_anchors\n        out1 = out.permute(0, 2, 3, 1)\n\n        batch_size, width, height, channels = out1.shape\n\n        out2 = out1.view(batch_size, width, height, self.num_anchors, self.num_classes)\n\n        return out2.contiguous().view(x.shape[0], -1, self.num_classes)\n```\n\n### 5.4，先验框Anchor赋值\n\n1，生成各个特征图对应原图大小的所有 `Anchors` 坐标的代码如下。\n\n```python\nimport numpy as np\nimport torch\nimport torch.nn as nn\n\n\nclass Anchors(nn.Module):\n    def __init__(self, pyramid_levels=None, strides=None, sizes=None, ratios=None, scales=None):\n        super(Anchors, self).__init__()\n\n        if pyramid_levels is None:\n            self.pyramid_levels = [3, 4, 5, 6, 7]\n        if strides is None:\n            self.strides = [2 ** x for x in self.pyramid_levels]\n        if sizes is None:\n            self.sizes = [2 ** (x + 2) for x in self.pyramid_levels]\n        if ratios is None:\n            self.ratios = np.array([0.5, 1, 2])\n        if scales is None:\n            self.scales = np.array([2 ** 0, 2 ** (1.0 / 3.0), 2 ** (2.0 / 3.0)])\n\n    def forward(self, image):\n        \n        image_shape = image.shape[2:]\n        image_shape = np.array(image_shape)\n        image_shapes = [(image_shape + 2 ** x - 1) // (2 ** x) for x in self.pyramid_levels]\n\n        # compute anchors over all pyramid levels\n        all_anchors = np.zeros((0, 4)).astype(np.float32)\n\n        for idx, p in enumerate(self.pyramid_levels):\n            anchors         = generate_anchors(base_size=self.sizes[idx], ratios=self.ratios, scales=self.scales)\n            shifted_anchors = shift(image_shapes[idx], self.strides[idx], anchors)\n            all_anchors     = np.append(all_anchors, shifted_anchors, axis=0)\n\n        all_anchors = np.expand_dims(all_anchors, axis=0)\n\n        if torch.cuda.is_available():\n            return torch.from_numpy(all_anchors.astype(np.float32)).cuda()\n        else:\n            return torch.from_numpy(all_anchors.astype(np.float32))\n\ndef generate_anchors(base_size=16, ratios=None, scales=None):\n    \"\"\"生成的 `9` 个 `base anchors` \n    Generate anchor (reference) windows by enumerating aspect ratios X\n    scales w.r.t. a reference window.\n    \"\"\"\n\n    if ratios is None:\n        ratios = np.array([0.5, 1, 2])\n\n    if scales is None:\n        scales = np.array([2 ** 0, 2 ** (1.0 / 3.0), 2 ** (2.0 / 3.0)])\n\n    num_anchors = len(ratios) * len(scales)\n\n    # initialize output anchors\n    anchors = np.zeros((num_anchors, 4))\n\n    # scale base_size\n    anchors[:, 2:] = base_size * np.tile(scales, (2, len(ratios))).T\n\n    # compute areas of anchors\n    areas = anchors[:, 2] * anchors[:, 3]\n\n    # correct for ratios\n    anchors[:, 2] = np.sqrt(areas / np.repeat(ratios, len(scales)))\n    anchors[:, 3] = anchors[:, 2] * np.repeat(ratios, len(scales))\n\n    # transform from (x_ctr, y_ctr, w, h) -> (x1, y1, x2, y2)\n    anchors[:, 0::2] -= np.tile(anchors[:, 2] * 0.5, (2, 1)).T\n    anchors[:, 1::2] -= np.tile(anchors[:, 3] * 0.5, (2, 1)).T\n\n    return anchors\n\ndef shift(shape, stride, anchors):\n    shift_x = (np.arange(0, shape[1]) + 0.5) * stride\n    shift_y = (np.arange(0, shape[0]) + 0.5) * stride\n\n    shift_x, shift_y = np.meshgrid(shift_x, shift_y)\n\n    shifts = np.vstack((\n        shift_x.ravel(), shift_y.ravel(),\n        shift_x.ravel(), shift_y.ravel()\n    )).transpose()\n\n    # add A anchors (1, A, 4) to\n    # cell K shifts (K, 1, 4) to get\n    # shift anchors (K, A, 4)\n    # reshape to (K*A, 4) shifted anchors\n    A = anchors.shape[0]\n    K = shifts.shape[0]\n    all_anchors = (anchors.reshape((1, A, 4)) + shifts.reshape((1, K, 4)).transpose((1, 0, 2)))\n    all_anchors = all_anchors.reshape((K * A, 4))\n\n    return all_anchors\n```\n\n`shift` 函数是将 `generate_anchors` 函数生成的 `9` 个 `base anchors` 按固定长度进行平移，然后和其对应特征图的 `cell`进行对应。经过对每个特征图（`5` 个）都做类似的变换，就能生成全部`anchor`。具体过程如下图所示。\n> anchor 平移图来源[这里](https://zhuanlan.zhihu.com/p/143877125)\n\n![anchor的平移和对应](../../data/images/retinanet/anchor的平移和对应.jpg)\n\n2，计算得到输出特征图上面每个点对应的原图 `anchor `坐标输出特征图上面每个点对应的原图 `anchor `坐标后，就可以和 `gt` 信息计算每个 `anchor` 的正负样本属性。具体过程总结如下：\n\n- 如果 anchor 和所有 gt bbox 的最大 iou 值小于 0.4，那么该 anchor 就是背景样本；\n- 如果 anchor 和所有 gt bbox 的最大 iou 值大于等于 0.5，那么该 anchor 就是高质量正样本；\n- 如果 gt bbox 和所有 anchor 的最大 iou 值大于等于 0(可以看出每个 gt bbox 都一定有至少一个 anchor 匹配)，那么该 gt bbox 所对应的 anchor 也是正样本；\n- 其余样本全部为忽略样本即 anchor 和所有 gt bbox 的最大 iou 值处于 [0.4,0.5) 区间的 anchor 为忽略样本，不计算 loss\n\n### 5.5，BBox Encoder Decoder\n\n在 `anchor-based` 算法中，为了利用 `anchor` 信息进行更快更好的收敛，一般会对 `head` 输出的 `bbox` 分支 `4` 个值进行编解码操作，作用有两个：\n\n- 更好的平衡分类和回归分支 `loss`，以及平衡 `bbox` 四个预测值的 `loss`。\n- 训练过程中引入 `anchor` 信息，加快收敛。\n- `RetinaNet` 采用的编解码函数是主流的 `DeltaXYWHBBoxCoder`，在 `OpenMMlab` 代码中的配置如下：\n    ```python\n    bbox_coder=dict(\n        type='DeltaXYWHBBoxCoder',\n        target_means=[.0, .0, .0, .0],\n        target_stds=[1.0, 1.0, 1.0, 1.0]),\n    ```\n\ntarget_means 和 target_stds 相当于对 bbox 回归的 4 个 tx ty tw th 进行变换。在不考虑 target_means 和 target_stds 情况下，其编码公式如下：\n\n$$t_{x}^{\\ast } = (x^{\\ast }-x_{a})/w_{a}, t_{y}^{\\ast}=(y^{\\ast}-y_{a})/h_{a} \\\\\\\\\nt_{w}^{\\ast } = log(w^{\\ast }/w_{a}), t_{h}^{\\ast }=log(h^{\\ast }/h_{a}) $$\n\n${x}^{\\ast },y^{\\ast}$ 是 `gt bbox` 的中心 xy 坐标， $w^{\\ast },h^{\\ast }$ 是 gt bbox 的 wh 值， $x_{a},y_{a}$ 是 anchor 的中心 xy 坐标， $w_{a},h_{a}$ 是 anchor 的 wh 值， $t^{\\ast }$ 是预测头的 `bbox` 分支输出的 `4` 个值对应的 `targets`。可以看出 $t_x,t_y$ 预测值表示 gt bbox 中心相对于 anchor 中心点的偏移，并且通过除以 anchor 的 $wh$ 进行归一化；而 $t_w,t_h$ 预测值表示 gt bbox 的 $wh$ 除以 anchor 的 $wh$，然后取 log 非线性变换即可。\n> Variables $x$, $x_a$, and $x^{\\ast }$ are for the predicted box, anchor box, and groundtruth box respectively (likewise for y; w; h).\n\n1，考虑**编码**过程存在 `target_means` 和 `target_stds` 情况下，则 `anchor` 的 `bbox` 对应的 `target` 编码的核心代码如下：\n\n```python\ndx = (gx - px) / pw\ndy = (gy - py) / ph\ndw = torch.log(gw / pw)\ndh = torch.log(gh / ph)\ndeltas = torch.stack([dx, dy, dw, dh], dim=-1)\n\n# 最后减掉均值，处于标准差\nmeans = deltas.new_tensor(means).unsqueeze(0)\nstds = deltas.new_tensor(stds).unsqueeze(0)\ndeltas = deltas.sub_(means).div_(stds)\n```\n\n2，**解码**过程是编码过程的反向，比较容易理解，其核心代码如下：\n\n```python\n# 先乘上 std，加上 mean\nmeans = deltas.new_tensor(means).view(1, -1).repeat(1, deltas.size(1) // 4)\nstds = deltas.new_tensor(stds).view(1, -1).repeat(1, deltas.size(1) // 4)\ndenorm_deltas = deltas * stds + means\ndx = denorm_deltas[:, 0::4]\ndy = denorm_deltas[:, 1::4]\ndw = denorm_deltas[:, 2::4]\ndh = denorm_deltas[:, 3::4]\n# wh 解码\ngw = pw * dw.exp()\ngh = ph * dh.exp()\n# 中心点 xy 解码\ngx = px + pw * dx\ngy = py + ph * dy\n# 得到 x1y1x2y2 的 gt bbox 预测坐标\nx1 = gx - gw * 0.5\ny1 = gy - gh * 0.5\nx2 = gx + gw * 0.5\ny2 = gy + gh * 0.5\n```\n\n### 5.6，Focal Loss\n\nFocal Loss 属于 CE Loss 的动态加权版本，其可以根据样本的难易程度(预测值和 label 的差距可以反映)对每个样本单独加权，易学习样本在总的 `loss` 中的权重比较低，难样本权重比较高。特征图上输出的 `anchor` 坐标列表的大部分都是属于背景且易学习的样本，虽然单个 `loss` 比较小，但是由于数目众多最终会主导梯度，从而得到次优模型，而 Focal Loss 通过**指数效应**把大量易学习样本的权重大大降低，从而避免上述问题。\n\n![focal-loss](../../data/images/retinanet/focal-loss.png)\n\n为了便于理解，先给出 `Focal Loss` 的核心代码。\n\n```python\npred_sigmoid = pred.sigmoid()\n# one-hot 格式\ntarget = target.type_as(pred)\npt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)\nfocal_weight = (alpha * target + (1 - alpha) *\n            (1 - target)) * pt.pow(gamma)\nloss = F.binary_cross_entropy_with_logits(\n        pred, target, reduction='none') * focal_weight\nloss = weight_reduce_loss(loss, weight, reduction, avg_factor)\nreturn loss\n```\n\n结合 `BBox Assigner`（`BBox` 正负样本确定） 和 `BBox Encoder` （`BBox target` 计算）的代码，可得完整的 `Focla Loss` 代码如下所示。\n\n```python\nclass FocalLoss(nn.Module):\n    #def __init__(self):\n\n    def forward(self, classifications, regressions, anchors, annotations):\n        alpha = 0.25\n        gamma = 2.0\n        batch_size = classifications.shape[0]\n        classification_losses = []\n        regression_losses = []\n\n        anchor = anchors[0, :, :]\n\n        anchor_widths  = anchor[:, 2] - anchor[:, 0]\n        anchor_heights = anchor[:, 3] - anchor[:, 1]\n        anchor_ctr_x   = anchor[:, 0] + 0.5 * anchor_widths\n        anchor_ctr_y   = anchor[:, 1] + 0.5 * anchor_heights\n\n        for j in range(batch_size):\n\n            classification = classifications[j, :, :]\n            regression = regressions[j, :, :]\n\n            bbox_annotation = annotations[j, :, :]\n            bbox_annotation = bbox_annotation[bbox_annotation[:, 4] != -1]\n\n            classification = torch.clamp(classification, 1e-4, 1.0 - 1e-4)\n\n            if bbox_annotation.shape[0] == 0:\n                if torch.cuda.is_available():\n                    alpha_factor = torch.ones(classification.shape).cuda() * alpha\n\n                    alpha_factor = 1. - alpha_factor\n                    focal_weight = classification\n                    focal_weight = alpha_factor * torch.pow(focal_weight, gamma)\n\n                    bce = -(torch.log(1.0 - classification))\n\n                    # cls_loss = focal_weight * torch.pow(bce, gamma)\n                    cls_loss = focal_weight * bce\n                    classification_losses.append(cls_loss.sum())\n                    regression_losses.append(torch.tensor(0).float().cuda())\n\n                else:\n                    alpha_factor = torch.ones(classification.shape) * alpha\n\n                    alpha_factor = 1. - alpha_factor\n                    focal_weight = classification\n                    focal_weight = alpha_factor * torch.pow(focal_weight, gamma)\n\n                    bce = -(torch.log(1.0 - classification))\n\n                    # cls_loss = focal_weight * torch.pow(bce, gamma)\n                    cls_loss = focal_weight * bce\n                    classification_losses.append(cls_loss.sum())\n                    regression_losses.append(torch.tensor(0).float())\n\n                continue\n\n            IoU = calc_iou(anchors[0, :, :], bbox_annotation[:, :4]) # num_anchors x num_annotations\n\n            IoU_max, IoU_argmax = torch.max(IoU, dim=1) # num_anchors x 1\n\n            #import pdb\n            #pdb.set_trace()\n\n            # compute the loss for classification\n            targets = torch.ones(classification.shape) * -1\n\n            if torch.cuda.is_available():\n                targets = targets.cuda()\n\n            targets[torch.lt(IoU_max, 0.4), :] = 0\n\n            positive_indices = torch.ge(IoU_max, 0.5)\n\n            num_positive_anchors = positive_indices.sum()\n\n            assigned_annotations = bbox_annotation[IoU_argmax, :]\n\n            targets[positive_indices, :] = 0\n            targets[positive_indices, assigned_annotations[positive_indices, 4].long()] = 1\n\n            if torch.cuda.is_available():\n                alpha_factor = torch.ones(targets.shape).cuda() * alpha\n            else:\n                alpha_factor = torch.ones(targets.shape) * alpha\n\n            alpha_factor = torch.where(torch.eq(targets, 1.), alpha_factor, 1. - alpha_factor)\n            focal_weight = torch.where(torch.eq(targets, 1.), 1. - classification, classification)\n            focal_weight = alpha_factor * torch.pow(focal_weight, gamma)\n\n            bce = -(targets * torch.log(classification) + (1.0 - targets) * torch.log(1.0 - classification))\n\n            # cls_loss = focal_weight * torch.pow(bce, gamma)\n            cls_loss = focal_weight * bce\n\n            if torch.cuda.is_available():\n                cls_loss = torch.where(torch.ne(targets, -1.0), cls_loss, torch.zeros(cls_loss.shape).cuda())\n            else:\n                cls_loss = torch.where(torch.ne(targets, -1.0), cls_loss, torch.zeros(cls_loss.shape))\n\n            classification_losses.append(cls_loss.sum()/torch.clamp(num_positive_anchors.float(), min=1.0))\n\n            # compute the loss for regression\n\n            if positive_indices.sum() > 0:\n                assigned_annotations = assigned_annotations[positive_indices, :]\n\n                anchor_widths_pi = anchor_widths[positive_indices]\n                anchor_heights_pi = anchor_heights[positive_indices]\n                anchor_ctr_x_pi = anchor_ctr_x[positive_indices]\n                anchor_ctr_y_pi = anchor_ctr_y[positive_indices]\n\n                gt_widths  = assigned_annotations[:, 2] - assigned_annotations[:, 0]\n                gt_heights = assigned_annotations[:, 3] - assigned_annotations[:, 1]\n                gt_ctr_x   = assigned_annotations[:, 0] + 0.5 * gt_widths\n                gt_ctr_y   = assigned_annotations[:, 1] + 0.5 * gt_heights\n\n                # clip widths to 1\n                gt_widths  = torch.clamp(gt_widths, min=1)\n                gt_heights = torch.clamp(gt_heights, min=1)\n\n                targets_dx = (gt_ctr_x - anchor_ctr_x_pi) / anchor_widths_pi\n                targets_dy = (gt_ctr_y - anchor_ctr_y_pi) / anchor_heights_pi\n                targets_dw = torch.log(gt_widths / anchor_widths_pi)\n                targets_dh = torch.log(gt_heights / anchor_heights_pi)\n\n                targets = torch.stack((targets_dx, targets_dy, targets_dw, targets_dh))\n                targets = targets.t()\n\n                if torch.cuda.is_available():\n                    targets = targets/torch.Tensor([[0.1, 0.1, 0.2, 0.2]]).cuda()\n                else:\n                    targets = targets/torch.Tensor([[0.1, 0.1, 0.2, 0.2]])\n\n                negative_indices = 1 + (~positive_indices)\n\n                regression_diff = torch.abs(targets - regression[positive_indices, :])\n\n                regression_loss = torch.where(\n                    torch.le(regression_diff, 1.0 / 9.0),\n                    0.5 * 9.0 * torch.pow(regression_diff, 2),\n                    regression_diff - 0.5 / 9.0\n                )\n                regression_losses.append(regression_loss.mean())\n            else:\n                if torch.cuda.is_available():\n                    regression_losses.append(torch.tensor(0).float().cuda())\n                else:\n                    regression_losses.append(torch.tensor(0).float())\n\n        return torch.stack(classification_losses).mean(dim=0, keepdim=True), torch.stack(regression_losses).mean(dim=0, keepdim=True)\n```\n\n## 参考资料\n\n- [https://github.com/yhenon/pytorch-retinanet](https://github.com/yhenon/pytorch-retinanet)\n- [RetinaNet 论文和代码详解](https://zhuanlan.zhihu.com/p/143877125)\n- [轻松掌握 MMDetection 中常用算法(一)：RetinaNet 及配置详解](https://zhuanlan.zhihu.com/p/346198300)\n"
  },
  {
    "path": "5-computer_vision/2D目标检测/7-YOLOv1-v5论文解读.md",
    "content": "- [一，YOLOv1](#一yolov1)\n  - [Abstract](#abstract)\n  - [1. Introduction](#1-introduction)\n  - [2. Unified Detectron](#2-unified-detectron)\n    - [2.1. Network Design](#21-network-design)\n    - [2.2 Training](#22-training)\n    - [2.4. Inferences](#24-inferences)\n    - [4.1 Comparison to Other Real-Time Systems](#41-comparison-to-other-real-time-systems)\n  - [5，代码实现思考](#5代码实现思考)\n- [二，YOLOv2](#二yolov2)\n  - [摘要](#摘要)\n  - [YOLOv2 的改进](#yolov2-的改进)\n    - [1，中心坐标位置预测的改进](#1中心坐标位置预测的改进)\n    - [2，1 个 gird 只能对应一个目标的改进](#21-个-gird-只能对应一个目标的改进)\n    - [3，backbone 的改进](#3backbone-的改进)\n    - [4，多尺度训练](#4多尺度训练)\n  - [损失函数](#损失函数)\n- [三，YOLOv3](#三yolov3)\n  - [摘要](#摘要-1)\n  - [1，介绍](#1介绍)\n  - [2，改进](#2改进)\n    - [2.1，边界框预测](#21边界框预测)\n    - [2.2，分类预测](#22分类预测)\n    - [2.3，跨尺度预测](#23跨尺度预测)\n    - [2.4，新的特征提取网络](#24新的特征提取网络)\n    - [2.5，训练](#25训练)\n  - [2.5，推理](#25推理)\n  - [3，实验结果](#3实验结果)\n  - [4，失败的尝试](#4失败的尝试)\n  - [5，改进的意义](#5改进的意义)\n- [四，YOLOv4](#四yolov4)\n  - [1，摘要及介绍](#1摘要及介绍)\n  - [2，相关工作](#2相关工作)\n    - [2.1，目标检测方法](#21目标检测方法)\n    - [2.2，Bag of freebies（免费技巧）](#22bag-of-freebies免费技巧)\n    - [2.3，Bag of specials（即插即用模块+后处理方法）](#23bag-of-specials即插即用模块后处理方法)\n  - [3，方法](#3方法)\n    - [3.1，架构选择](#31架构选择)\n    - [3.2，Selection of BoF and BoS](#32selection-of-bof-and-bos)\n    - [3.3，额外的改进](#33额外的改进)\n    - [3.4 YOLOv4](#34-yolov4)\n  - [4，实验](#4实验)\n    - [4.1，实验设置](#41实验设置)\n    - [4.2，对于分类器训练过程中不同特性的影响](#42对于分类器训练过程中不同特性的影响)\n    - [4.3，对于检测器训练过程中不同特性的影响](#43对于检测器训练过程中不同特性的影响)\n    - [4.4，不同骨干和预训练权重对检测器训练的影响](#44不同骨干和预训练权重对检测器训练的影响)\n    - [4.5，不同小批量的大小对检测器训练的影响](#45不同小批量的大小对检测器训练的影响)\n  - [5，结果](#5结果)\n  - [6，YOLOv4 主要改进点](#6yolov4-主要改进点)\n    - [6.1，Backbone 改进](#61backbone-改进)\n      - [6.1.1，CSPDarknet53](#611cspdarknet53)\n      - [6.1.2，Mish 激活](#612mish-激活)\n    - [6.1.3，Dropblock](#613dropblock)\n    - [6.2，Neck 网络改进](#62neck-网络改进)\n    - [6.3，预测的改进](#63预测的改进)\n      - [6.3.1，使用CIoU Loss](#631使用ciou-loss)\n      - [6.3.2，使用DIoU\\_NMS](#632使用diou_nms)\n    - [6.4，输入端改进](#64输入端改进)\n      - [6.4.1，Mosaic 数据增强](#641mosaic-数据增强)\n- [五，YOLOv5](#五yolov5)\n  - [5.1，网络架构](#51网络架构)\n  - [5.2，创新点](#52创新点)\n    - [5.2.1，自适应anchor](#521自适应anchor)\n    - [5.2.2， 自适应图片缩放](#522-自适应图片缩放)\n    - [5.2.3，Focus结构](#523focus结构)\n  - [5.3，四种网络结构](#53四种网络结构)\n  - [5.4，实验结果](#54实验结果)\n- [参考资料](#参考资料)\n\n## 一，YOLOv1\nYOLOv1 出自 2016 CVPR 论文 You Only Look Once:Unified, Real-Time Object Detection.\n\nYOLO 系列算法的核心思想是将输入的图像经过 backbone 提取特征后，将得到特征图划分为 S x S 的网格，物体的中心落在哪一个网格内，这个网格就负责预测该物体的**置信度、类别以及坐标位置**。\n\n### Abstract\n\n作者提出了一种新的目标检测方法 `YOLO`，之前的目标检测工作都是重新利用分类器来执行检测。作者的神经网络模型是端到端的检测，一次运行即可同时得到所有目标的边界框和类别概率。\n\n`YOLO` 架构的速度是非常快的，`base` 版本实时帧率为 `45` 帧，`smaller` 版本能达到每秒 `155` 帧，性能由于 `DPM` 和 `R-CNN` 等检测方法。\n\n### 1. Introduction\n\n之前的目标检测器是重用分类器来执行检测，为了检测目标，这些系统在图像上不断遍历一个框，并利用分类器去判断这个框是不是目标。像可变形部件模型（`DPM`）使用互动窗口方法，其分类器在整个图像的均匀间隔的位置上运行。\n\n**作者将目标检测看作是单一的回归问题，直接从图像像素得到边界框坐标和类别概率**。\n\nYOLO 检测系统如图 1 所示。单个检测卷积网络可以同时预测多个目标的边界框和类别概率。`YOLO` 和传统的目标检测方法相比有诸多优点。\n\n![yolo检测系统](../../data/images/yolo/yolo_figure1.png)\n\n首先，`YOLO` 速度非常快，我们将检测视为**回归**问题，所以检测流程也简单。其次，`YOLO` 在进行预测时，会对图像进行全面地推理。第三，`YOLO` 模型具有泛化能力，其比 `DPM` 和`R-CNN` 更好。最后，虽然 `YOLO` 模型在精度上依然落后于最先进（state-of-the-art）的检测系统，但是其速度更快。\n\n### 2. Unified Detectron\n\n`YOLO` 系统将输入图像划分成 $S\\times S$ 的网格（`grid`），然后让每个`gird` 负责检测那些中心点落在 `grid` 内的目标。\n\n**检测任务**：每个网络都会预测 $B$ 个边界框及边界框的置信度分数，所谓置信度分数其实包含两个方面：一个是边界框含有目标的可能性，二是边界框的准确度。前者记为 $Pr(Object)$，当边界框包含目标时，$Pr(Object)$ 值为 `1`，否则为 `0`；后者记为 $IOU_{pred}^{truth}$，即预测框与真实框的 `IOU`。因此形式上，我们将置信度定义为 $Pr(Object)*IOU_{pred}^{truth}$。如果 `grid` 不存在目标，则置信度分数置为 `0`，否则，置信度分数等于预测框和真实框之间的交集（`IoU`）。\n\n每个边界框（`bounding box`）包含 `5` 个预测变量：$x$，$y$，$w$，$h$ 和 `confidence`。$(x,y)$ 坐标不是边界框中心的实际坐标，而是相对于网格单元左上角坐标的**偏移**（需要看代码才能懂，论文只描述了出“相对”的概念）。而边界框的宽度和高度是相对于整个图片的宽与高的比例，因此理论上以上 `4` 预测量都应该在 $[0,1]$ 范围之内。最后，置信度预测表示预测框与实际边界框之间的 `IOU`。\n> 值得注意的是，中心坐标的预测值 $(x,y)$ 是相对于每个单元格左上角坐标点的偏移值，偏移量 = 目标位置 - grid的位置。\n\n![yolo检测系统](../../data/images/yolo/边界框坐标定义.png)\n\n**分类任务**：每个网格单元（`grid`）还会预测 $C$ 个类别的概率 $Pr(Class_i)|Object)$。`grid` 包含目标时才会预测 $Pr$，且只预测一组类别概率，而不管边界框 $B$ 的数量是多少。\n\n在推理时，我们乘以条件概率和单个 `box` 的置信度。\n\n$$Pr(Class_i)|Object)*Pr(Object)*IOU_{pred}^{truth} = Pr(Class_i)*IOU_{pred}^{truth}$$\n\n它为我们提供了每个框特定类别的置信度分数。这些分数编码了该类出现在框中的概率以及预测框拟合目标的程度。\n\n在 `Pscal VOC` 数据集上评测 `YOLO` 模型时，我们设置 $S=7$, $B=2$（即每个 `grid` 会生成 `2` 个边界框）。`Pscal VOC` 数据集有 `20` 个类别，所以 $C=20$。所以，模型最后预测的张量维度是 $7 \\times 7\\times (20+5*2) = 1470$。\n\n![yolo 模型输出张量维度](../../data/images/yolo/yolo_figure2.png)\n\n**总结**：`YOLO` 系统将检测建模为回归问题。它将图像分成 $S \\times S$ 的 `gird`，每个 `grid` 都会预测 $B$ 个边界框，同时也包含 $C$ 个类别的概率，这些预测对应的就是 $S \\times S \\times (C + 5*B)$。\n\n这里其实就是在描述 `YOLOv1` 检测头如何设计：回归网络的设计 + 训练集标签如何构建（即 `yoloDataset` 类的构建），下面给出一份针对 `voc` 数据集编码为 `yolo` 模型的输入标签数据的函数，读懂了这个代码，就能理解前面部分的描述。\n> 代码来源[这里](https://github.com/FelixFu520/yolov1)。\n\n```python\ndef encoder(self, boxes, labels):\n    '''\n    boxes (tensor) [[x1,y1,x2,y2],[]] 目标的边界框坐标信息\n    labels (tensor) [...] 目标的类别信息\n    return 7x7x30\n    '''\n    grid_num = 7 # 论文中设为7\n    target = torch.zeros((grid_num, grid_num, 30))  # 和模型输出张量维尺寸一样都是 14*14*30\n    cell_size = 1./grid_num  # 之前已经把目标框的坐标进行了归一化（这里与原论文有区别），故这里用1.作为除数\n    # 计算目标框中心点坐标和宽高\n    wh = boxes[:, 2:]-boxes[:, :2]  \n    cxcy = (boxes[:, 2:]+boxes[:, :2])/2  \n    # 1，遍历各个目标框；\n    for i in range(cxcy.size()[0]):    # 对应于数据集中的每个框 这里cxcy.size()[0] == num_samples\n        # 2，计算第 i 个目标中心点落在哪个 `grid` 上，`target` 相应位置的两个框的置信度值设为 `1`，同时对应类别值也置为 `1`；\n        cxcy_sample = cxcy[i]  \n        ij = (cxcy_sample/cell_size).ceil()-1 # ij 是一个list, 表示目标中心点cxcy在归一化后的图片中所处的x y 方向的第几个网格\n\n        # [0,1,2,3,4,5,6,7,8,9, 10-19] 对应索引 \n        # [x,y,w,h,c,x,y,w,h,c, 20 个类别的 one-hot编码] 与原论文输出张量维度各个索引对应目标有所区别\n        target[int(ij[1]), int(ij[0]), 4] = 1  # 第一个框的置信度\n        target[int(ij[1]), int(ij[0]), 9] = 1  # 第二个框的置信度\n        target[int(ij[1]), int(ij[0]), int(labels[i])+9] = 1 # 第 int(labels[i])+9 个类别为 1\n        # 3，计算目标中心所在 `grid`（网格）的左上角相对坐标：`ij*cell_size`，然后目标中心坐标相对于子网格左上角的偏移比例 `delta_xy`；\n        xy = ij*cell_size  \n        delta_xy = (cxcy_sample -xy)/cell_size  \n        # 4，最后将 `target` 对应网格位置的 (x, y, w, h) 分别赋相应 `wh`、`delta_xy` 值。\n        target[int(ij[1]), int(ij[0]), 2:4] = wh[i]  # 范围为(0,1)\n        target[int(ij[1]), int(ij[0]), :2] = delta_xy\n        target[int(ij[1]), int(ij[0]), 7:9] = wh[i]\n        target[int(ij[1]), int(ij[0]), 5:7] = delta_xy\n    return target\n```\n\n代码分析，一张图片对应的标签张量 `target` 的维度是 $7 \\times 7 \\times 30$。然后分别对各个目标框的 `boxes`: $(x1,y1,x2,y2)$ 和 `labels`：`(0,0,...,1,0)`(`one-hot` 编码的目标类别信息）进行处理，符合检测系统要求的输入形式。算法步骤如下：\n\n1. 计算目标框中心点坐标和宽高，并遍历各个目标框；\n2. 计算目标中心点落在哪个 `grid` 上，`target` 相应位置的两个框的置信度值设为 `1`，同时对应类别值也置为 `1`；\n3. 计算目标中心所在 `grid`（网格）的左上角相对坐标：`ij*cell_size`，然后目标中心坐标相对于子网格左上角的偏移比例 `delta_xy`；\n4. 最后将 `target` 对应网格位置的 $(x, y, w, h)$ 分别赋相应 `wh`、`delta_xy` 值。\n\n#### 2.1. Network Design\n\n`YOLO` 模型使用卷积神经网络来实现，卷积层负责从图像中提取特征，全连接层预测输出类别概率和坐标。\n\n`YOLO` 的网络架构受 `GooLeNet` 图像分类模型的启发。网络有 `24` 个卷积层，最后面是 `2` 个全连接层。整个网络的卷积只有 $1 \\times 1$ 和 $3 \\times 3$ 卷积层，其中 $1 \\times 1$ 卷积负责降维 ，而不是 `GoogLeNet` 的 `Inception` 模块。\n\n![yolo 模型架构](../../data/images/yolo/yolo_figure3.png)\n**图3：网络架构**。作者在 `ImageNet` 分类任务上以一半的分辨率（输入图像大小 $224\\times 224$）训练卷积层，但预测时分辨率加倍。\n\n`Fast YOLO` 版本使用了更少的卷积，其他所有训练参数及测试参数都和 `base YOLO` 版本是一样的。\n\n网络的最终输出是 $7\\times 7\\times 30$ 的张量。这个张量所代表的具体含义如下图所示。对于每一个单元格，前 `20` 个元素是类别概率值，然后 `2` 个元素是边界框置信度，两者相乘可以得到**类别置信度**，最后 `8` 个元素是边界框的 $(x,y,w,h)$ 。之所以把置信度 $c$ 和 $(x,y,w,h)$ 都分开排列，而不是按照$(x,y,w,h,c)$ 这样排列，存粹是为了后续计算时方便。\n\n![yolo 模型架构](../../data/images/yolo/输出张量解释.png)\n\n> 划分 $7 \\times 7$ 网格，共 `98` 个边界框，`2` 个框对应一个类别，所以 `YOLOv1` 只能在一个网格中检测出一个目标、单张图片最多预测 `49` 个目标。\n\n#### 2.2 Training\n\n> 模型训练最重要的无非就是超参数的调整和损失函数的设计。\n\n因为 `YOLO` 算法将检测问题看作是回归问题，所以自然地采用了比较容易优化的均方误差作为损失函数，但是面临定位误差和分类误差权重一样的问题；同时，在每张图像中，许多网格单元并不包含对象，即负样本（不包含物体的网格）远多于正样本（包含物体的网格），这通常会压倒了正样本的梯度，导致训练早期模型发散。\n\n为了改善这点，引入了两个参数：$\\lambda_{coord}=5$ 和 $\\lambda_{noobj} =0.5$。对于边界框坐标预测损失（定位误差），采用较大的权重 $\\lambda_{coord}  =5$，然后区分不包含目标的边界框和含有目标的边界框，前者采用较小权重 $\\lambda_{noobj} =0.5$。其他权重则均设为 `0`。\n\n对于大小不同的边界框，因为较小边界框的坐标误差比较大边界框要更敏感，所以为了部分解决这个问题，将网络的边界框的宽高预测改为对其平方根的预测，即预测值变为 $(x, y, \\sqrt w, \\sqrt h)$。\n\n`YOLOv1` 每个网格单元预测多个边界框。在训练时，每个目标我们只需要一个边界框预测器来负责。我们指定一个预测器“负责”根据哪个预测与真实值之间**具有当前最高的 `IOU` 来预测目标**。这导致边界框预测器之间的专业化。每个预测器可以更好地预测特定大小，方向角，或目标的类别，从而改善整体召回率。\n\n> `YOLO` 由于每个网格仅能预测 `2` 个边界框且仅可以包含一个类别，因此是对于一个单元格存在多个目标的问题，`YOLO` 只能选择一个来预测。这使得它在预测临近物体的数量上存在不足，如钢筋、人脸和鸟群检测等。\n\n最终网络总的损失函数计算公式如下：\n\n![yolo 模型架构](../../data/images/yolo/yolo_loss.png)\n\n$I_{ij}^{obj}$ 指的是第 $i$ 个单元格存在目标，且该单元格中的第 $j$ 个边界框负责预测该目标。 $I_{i}^{obj}$ 指的是第 $i$ 个单元格存在目标。\n\n+ 前 2 行计算前景的 `geo_loss`（定位 `loss`）。\n+ 第 3 行计算前景的 `confidence_loss`（包含目标的边界框的置信度误差项）。\n+ 第 4 行计算背景的 `confidence_loss`。\n+ 第 5 行计算分类损失 `class_loss`。\n\n值得注意的是，对于不存在对应目标的边界框，其误差项就是只有置信度，坐标项误差是没法计算的。而只有当一个单元格内确实存在目标时，才计算分类误差项，否则该项也是无法计算的。\n\n#### 2.4. Inferences\n\n同样采用了 `NMS` 算法来抑制多重检测，对应的模型推理结果解码代码如下，这里要和前面的 `encoder` 函数结合起来看。\n\n```python\n# 对于网络输出预测 改为再图片上画出框及score\ndef decoder(pred):\n    \"\"\"\n    pred (tensor)  torch.Size([1, 7, 7, 30])\n    return (tensor) box[[x1,y1,x2,y2]] label[...]\n    \"\"\"\n    grid_num = 7\n    boxes = []\n    cls_indexs = []\n    probs = []\n    cell_size = 1./grid_num\n    pred = pred.data  # torch.Size([1, 14, 14, 30])\n    pred = pred.squeeze(0)  # torch.Size([14, 14, 30])\n    # 0 1      2 3   4    5 6   7 8   9\n    # [中心坐标,长宽,置信度,中心坐标,长宽,置信度, 20个类别] x 7x7\n    contain1 = pred[:, :, 4].unsqueeze(2)  # torch.Size([14, 14, 1])\n    contain2 = pred[:, :, 9].unsqueeze(2)  # torch.Size([14, 14, 1])\n    contain = torch.cat((contain1, contain2), 2)    # torch.Size([14, 14, 2])\n\n    mask1 = contain > 0.1  # 大于阈值, torch.Size([14, 14, 2])  content: tensor([False, False])\n    mask2 = (contain == contain.max())  # we always select the best contain_prob what ever it>0.9\n    mask = (mask1+mask2).gt(0)\n\n    # min_score,min_index = torch.min(contain, 2) # 每个 cell 只选最大概率的那个预测框\n    for i in range(grid_num):\n        for j in range(grid_num):\n            for b in range(2):\n                # index = min_index[i,j]\n                # mask[i,j,index] = 0\n                if mask[i, j, b] == 1:\n                    box = pred[i, j, b*5:b*5+4]\n                    contain_prob = torch.FloatTensor([pred[i, j, b*5+4]])\n                    xy = torch.FloatTensor([j, i])*cell_size  # cell左上角  up left of cell\n                    box[:2] = box[:2]*cell_size + xy  # return cxcy relative to image\n                    box_xy = torch.FloatTensor(box.size())  # 转换成xy形式 convert[cx,cy,w,h] to [x1,y1,x2,y2]\n                    box_xy[:2] = box[:2] - 0.5*box[2:]\n                    box_xy[2:] = box[:2] + 0.5*box[2:]\n                    max_prob, cls_index = torch.max(pred[i, j, 10:], 0)\n                    if float((contain_prob*max_prob)[0]) > 0.1:\n                        boxes.append(box_xy.view(1, 4))\n                        cls_indexs.append(cls_index.item())\n                        probs.append(contain_prob*max_prob)\n    if len(boxes) == 0:\n        boxes = torch.zeros((1, 4))\n        probs = torch.zeros(1)\n        cls_indexs = torch.zeros(1)\n    else:\n        boxes = torch.cat(boxes, 0)  # (n,4)\n        # print(type(probs))\n        # print(len(probs))\n        # print(probs)\n        probs = torch.cat(probs, 0)  # (n,)\n        # print(probs)\n        # print(type(cls_indexs))\n        # print(len(cls_indexs))\n        # print(cls_indexs)\n        cls_indexs = torch.IntTensor(cls_indexs)  # (n,)\n    \n    # 去除冗余的候选框，得到最佳检测框（bbox）\n    keep = nms(boxes, probs)\n    # print(\"keep:\", keep)\n\n    a = boxes[keep]\n    b = cls_indexs[keep]\n    c = probs[keep]\n    return a, b, c\n```\n\n#### 4.1 Comparison to Other Real-Time Systems\n\n基于 GPU Titan X 硬件环境下，与他检测算法的性能比较如下。\n\n![Yolov1 实验对比结果](../../data/images/yolo/yolo_compare_with_others.png)\n\n### 5，代码实现思考\n\n一些思考：快速的阅读了网上的一些 `YOLOv1` 代码实现，发现整个 `YOLOv1` 检测系统的代码可以分为以下几个部分：\n\n- 模型结构定义：特征提器模块 + 检测头模块（两个全连接层）。\n- 数据预处理，最难写的代码，需要对原有的 `VOC` 数据做预处理，编码成 `YOLOv1` 要求的格式输入，训练集的 `label` 的 `shape` 为 `(bach_size, 7, 7, 30)`。\n- 模型训练，主要由损失函数的构建组成，损失函数包括 `5` 个部分。\n- 模型预测，主要在于模型输出的解析，即解码成可方便显示的形式。\n\n## 二，[YOLOv2](http://xxx.itp.ac.cn/pdf/1612.08242.pdf)\n\n> `YOLO9000` 是 `CVPR2017` 的最佳论文提名，但是这篇论文其实提出了 `YOLOv2` 和 `YOLO9000` 两个模型，二者略有不同。前者主要是 `YOLO` 的升级版，后者的主要检测网络也是 `YOLOv2`，同时对数据集做了融合，使得模型可以检测 `9000` 多类物体。\n\n### 摘要\n\n`YOLOv2` 其实就是 `YOLO9000`，作者在 `YOLOv1` 基础上改进的一种新的 `state-of-the-art` 目标检测模型，它能检测多达 `9000` 个目标！利用了多尺度（`multi-scale`）训练方法，`YOLOv2` 可以在不同尺寸的图片上运行，并取得速度和精度的平衡。\n\n在速度达到在 `40 FPS` 同时，`YOLOv2` 获得 `78.6 mAP` 的精度，性能优于`backbone` 为 `ResNet` 的 `Faster RCNN` 和 `SSD` 等当前最优（`state-of-the-art`） 模型。最后作者提出一种联合训练目标检测和分类的方法，基于这种方法，`YOLO9000` 能实时检测多达 `9000` 种目标。\n\n`YOLOv1` 虽然速度很快，但是还有很多缺点：\n\n+ 虽然每个 `grid` 预测两个框，但是只能对应一个目标，对于同一个 `grid` 有着两个目标的情况下，`YOLOv1` 是检测不全的，且模型最多检测 $7 \\times 7 = 49$ 个目标，即表现为模型查全率低。\n+ 预测框不够准确，之前回归 $(x,y,w,h)$ 的方法不够精确，即表现为模型精确率低。\n+ 回归参数网络使用全连接层参数量太大，即模型检测头还不够块。\n\n### YOLOv2 的改进\n\n#### 1，中心坐标位置预测的改进\n\n`YOLOv1` 模型预测的边界框中心坐标 $(x,y)$ 是基于 `grid` 的偏移，这里 `grid` 的位置是固定划分出来的，偏移量 = 目标位置 - `grid` 的位置。\n\n**边界框的编码过程**：`YOLOv2` 参考了两阶段网络的 `anchor boxes` 来预测边界框相对先验框的偏移，同时沿用 `YOLOv1` 的方法预测边界框中心点相对于 `grid` 左上角位置的相对偏移值。$(x,y,w,h)$ 的偏移值和实际坐标值的关系如下图所示。\n\n![偏移量计算](../../data/images/yolov2/偏移量计算.png)\n\n各个字母的含义如下：\n\n+ $b_x,b_y,b_w,b_h$ ：模型预测结果转化为 `box` 中心坐标和宽高后的值\n+ $t_x,t_y,t_w,t_h$ ：模型要预测的偏移量。\n+ $c_x,c_y$ ：`grid` 的左上角坐标，如上图所示。\n+ $p_w,p_h$ ：`anchor` 的宽和高，这里的 `anchor` 是人为定好的一个框，宽和高是固定的。\n\n通过以上定义我们从直接预测位置改为预测一个偏移量，即基于 `anchor` 框的宽高和 `grid` 的先验位置的偏移量，位置上使用 `grid`，宽高上使用 `anchor` 框，得到最终目标的位置，这种方法叫作 `location prediction`。\n> 预测偏移不直接预测位置，是因为作者发现直接预测位置会导致神经网络在一开始训练时不稳定，使用偏移量会使得训练过程更加稳定，性能指标提升了 `5%` 左右。\n\n在数据集的预处理过程中，关键的边界框编码函数如下（代码来自 [github](https://github.com/kuangliu/pytorch-yolov2/blob/master/encoder.py)，这个版本更清晰易懂）：\n\n```python\ndef encode(self, boxes, labels, input_size):\n    '''Encode target bounding boxes and class labels into YOLOv2 format.\n    Args:\n        boxes: (tensor) bounding boxes of (xmin,ymin,xmax,ymax) in range [0,1], sized [#obj, 4].\n        labels: (tensor) object class labels, sized [#obj,].\n        input_size: (int) model input size.\n    Returns:\n        loc_targets: (tensor) encoded bounding boxes, sized [5,4,fmsize,fmsize].\n        cls_targets: (tensor) encoded class labels, sized [5,20,fmsize,fmsize].\n        box_targets: (tensor) truth boxes, sized [#obj,4].\n    '''\n    num_boxes = len(boxes)\n    # input_size -> fmsize\n    # 320->10, 352->11, 384->12, 416->13, ..., 608->19\n    fmsize = (input_size - 320) / 32 + 10\n    grid_size = input_size / fmsize\n\n    boxes *= input_size  # scale [0,1] -> [0,input_size]\n    bx = (boxes[:,0] + boxes[:,2]) * 0.5 / grid_size  # in [0,fmsize]\n    by = (boxes[:,1] + boxes[:,3]) * 0.5 / grid_size  # in [0,fmsize]\n    bw = (boxes[:,2] - boxes[:,0]) / grid_size        # in [0,fmsize]\n    bh = (boxes[:,3] - boxes[:,1]) / grid_size        # in [0,fmsize]\n\n    tx = bx - bx.floor()\n    ty = by - by.floor()\n\n    xy = meshgrid(fmsize, swap_dims=True) + 0.5  # grid center, [fmsize*fmsize,2]\n    wh = torch.Tensor(self.anchors)              # [5,2]\n\n    xy = xy.view(fmsize,fmsize,1,2).expand(fmsize,fmsize,5,2)\n    wh = wh.view(1,1,5,2).expand(fmsize,fmsize,5,2)\n    anchor_boxes = torch.cat([xy-wh/2, xy+wh/2], 3)  # [fmsize,fmsize,5,4]\n\n    ious = box_iou(anchor_boxes.view(-1,4), boxes/grid_size)  # [fmsize*fmsize*5,N]\n    ious = ious.view(fmsize,fmsize,5,num_boxes)               # [fmsize,fmsize,5,N]\n\n    loc_targets = torch.zeros(5,4,fmsize,fmsize)  # 5boxes * 4coords\n    cls_targets = torch.zeros(5,20,fmsize,fmsize)\n    for i in range(num_boxes):\n        cx = int(bx[i])\n        cy = int(by[i])\n        _, max_idx = ious[cy,cx,:,i].max(0)\n        j = max_idx[0]\n        cls_targets[j,labels[i],cy,cx] = 1\n\n        tw = bw[i] / self.anchors[j][0]\n        th = bh[i] / self.anchors[j][1]\n        loc_targets[j,:,cy,cx] = torch.Tensor([tx[i], ty[i], tw, th])\n    return loc_targets, cls_targets, boxes/grid_size\n```\n\n**边界框的解码过程**：虽然模型预测的是边界框的偏移量 $(t_x,t_y,t_w,t_h)$，但是可通过以下公式计算出边界框的实际位置。\n\n$$\nb_x = \\sigma(t_x) + c_x \\\\\\\\\nb_y = \\sigma(t_y) + c_y \\\\\\\\\nb_w = p_{w}e^{t_w} \\\\\\\\\nb_h = p_{h}e^{t_h} $$\n\n其中，$(c_x, c_y)$ 为 `grid` 的左上角坐标，因为 $\\sigma$ 表示的是 `sigmoid` 函数，所以边界框的中心坐标会被约束在 `grid` 内部，防止偏移过多。$p_w$、$p_h$ 是先验框（`anchors`）的宽度与高度，其值相对于特征图大小 $W\\times H$ = $13\\times 13$ 而言的，因为划分为 $13 \\times 13$ 个 `grid`，所以最后输出的特征图中每个 `grid` 的长和宽均是 `1`。知道了特征图的大小，就可以将边界框相对于整个特征图的位置和大小计算出来（均取值 ${0,1}$）。\n\n$$\nb_x = (\\sigma(t_x) + c_x)/W \\\\\\\\\nb_y = (\\sigma(t_y) + c_y)/H \\\\\\\\\nb_w = p_{w}e^{t_w}/W \\\\\\\\\nb_h = p_{h}e^{t_h}/H\n$$\n\n在模型推理的时候，将以上 `4` 个值分别乘以图片的宽度和长度（像素点值）就可以得到边界框的实际中心坐标和大小。\n\n在模型推理过程中，模型输出张量的解析，即边界框的解码函数如下：\n\n```python\ndef decode(self, outputs, input_size):\n    '''Transform predicted loc/conf back to real bbox locations and class labels.\n    Args:\n        outputs: (tensor) model outputs, sized [1,125,13,13].\n        input_size: (int) model input size.\n    Returns:\n        boxes: (tensor) bbox locations, sized [#obj, 4].\n        labels: (tensor) class labels, sized [#obj,1].\n    '''\n    fmsize = outputs.size(2)\n    outputs = outputs.view(5,25,13,13)\n\n    loc_xy = outputs[:,:2,:,:]   # [5,2,13,13]\n    grid_xy = meshgrid(fmsize, swap_dims=True).view(fmsize,fmsize,2).permute(2,0,1)  # [2,13,13]\n    box_xy = loc_xy.sigmoid() + grid_xy.expand_as(loc_xy)  # [5,2,13,13]\n\n    loc_wh = outputs[:,2:4,:,:]  # [5,2,13,13]\n    anchor_wh = torch.Tensor(self.anchors).view(5,2,1,1).expand_as(loc_wh)  # [5,2,13,13]\n    box_wh = anchor_wh * loc_wh.exp()  # [5,2,13,13]\n\n    boxes = torch.cat([box_xy-box_wh/2, box_xy+box_wh/2], 1)  # [5,4,13,13]\n    boxes = boxes.permute(0,2,3,1).contiguous().view(-1,4)    # [845,4]\n\n    iou_preds = outputs[:,4,:,:].sigmoid()  # [5,13,13]\n    cls_preds = outputs[:,5:,:,:]  # [5,20,13,13]\n    cls_preds = cls_preds.permute(0,2,3,1).contiguous().view(-1,20)\n    cls_preds = softmax(cls_preds)  # [5*13*13,20]\n\n    score = cls_preds * iou_preds.view(-1).unsqueeze(1).expand_as(cls_preds)  # [5*13*13,20]\n    score = score.max(1)[0].view(-1)  # [5*13*13,]\n    print(iou_preds.max())\n    print(cls_preds.max())\n    print(score.max())\n\n    ids = (score>0.5).nonzero().squeeze()\n    keep = box_nms(boxes[ids], score[ids])  # NMS 算法去除重复框\n    return boxes[ids][keep] / fmsize\n```\n\n#### 2，1 个 gird 只能对应一个目标的改进\n\n> 或者说很多目标预测不到，查全率低的改进\n\n`YOLOv2` 首先把 $7 \\times 7$ 个区域改为 $13 \\times 13$ 个 `grid`（区域），每个区域有 5 个anchor，且每个 anchor 对应着 1 个类别，那么，输出的尺寸就应该为：`[N,13,13,125]`\n> $125 = 5 \\times (5 + 20)$\n\n![anchor的挑选](../../data/images/yolov2/anchor的挑选.png)\n\n值得注意的是之前 `YOLOv1` 的每个 `grid` 只能预测一个目标的分类概率值，两个 `boxes` 共享这个置信度概率。现在 `YOLOv2` 使用了 `anchor` 先验框后，每个 `grid` 的每个 `anchor` 都单独预测一个目标的分类概率值。\n\n之所以每个 `grid` 取 `5` 个 `anchor`，是因为作者对 `VOC/COCO` 数据集进行 K-means 聚类实验，发现当 `k=5` 时，模型 recall vs. complexity 取得了较好的平衡。当然，$k$ 越好，`mAP` 肯定越高，但是为了平衡模型复杂度，作者选择了 `5` 个聚类簇，即划分成 `5` 类先验框。设置先验框的主要目的是为了使得预测框与 `ground truth` 的 `IOU` 更好，所以聚类分析时选用 `box` 与聚类中心 `box` 之间的 `IOU` 值作为距离指标：\n\n$$d(box, centroid) = 1-IOU(box, centroid)$$\n\n> 与 `Faster RCNN` 手动设置 `anchor` 的大小和宽高比不同，YOLOv2 的 anchor 是从数据集中统计得到的。\n\n#### 3，backbone 的改进\n\n作者提出了一个全新的 `backbone` 网络：`Darknet-19`，它是基于前人经典工作和该领域常识的基础上进行设计的。`Darknet-19` 网络和 `VGG` 网络类似，主要使用 $3 \\times 3$ 卷积，并且每个 $2 \\times 2$ `pooling` 操作之后将特征图通道数加倍。借鉴 `NIN` 网络的工作，作者使用 `global average pooling` 进行预测，并在 $3 \\times 3$ 卷积之间使用 $1 \\times 1$ 卷积来降低特征图通道数从而降低模型计算量和参数量。`Darknet-19` 网络的每个卷积层后面都是用了 `BN` 层来加快模型收敛，防止模型过拟合。\n\n`Darknet-19` 网络总共有 `19` 个卷积层（`convolution`）、`5` 最大池化层（`maxpooling`）。`Darknet-19` 以 `5.58` T的计算量在 `ImageNet` 数据集上取得了 `72.9%` 的 top-1 精度和 `91.2%` 的 top-5 精度。Darket19 网络参数表如下图所示。\n\n![Darket19网络参数表](../../data/images/yolov2/Darket19网络参数表.png)\n\n**检测训练**。在 `Darknet19` 网络基础上进行修改后用于目标检测。首先，移除网络的最后一个卷积层，然后添加滤波器个数为 `1024` 的 $3 \\times 3$ 卷积层，最后添加一个 $1 \\times 1$ 卷积层，其滤波器个数为模型检测需要输出的变量个数。对于 `VOC` 数据集，每个 `grid` 预测 `5` 个边界框，每个边界框有 `5` 个坐标（$t_x, t_y, t_w, t_h \\ 和\\ t_o$）和 `20` 个类别，所以共有 `125` 个滤波器。我们还添加了从最后的 `3×3×512` 层到倒数第二层卷积层的直通层，以便模型可以使用细粒度特征。\n\n$$P_r(object)*IOU(b; object) = \\sigma (t_o)$$\n\n`Yolov2` 整个模型结构代码如下：\n> 代码来源 [这里](https://github.com/kuangliu/pytorch-yolov2/blob/master/encoder.py)。\n\n```python\n'''Darknet in PyTorch.'''\nimport torch\nimport torch.nn as nn\nimport torch.nn.init as init\nimport torch.nn.functional as F\n\nfrom torch.autograd import Variable\n\n\nclass Darknet(nn.Module):\n    # (64,1) means conv kernel size is 1, by default is 3.\n    cfg1 = [32, 'M', 64, 'M', 128, (64,1), 128, 'M', 256, (128,1), 256, 'M', 512, (256,1), 512, (256,1), 512]  # conv1 - conv13\n    cfg2 = ['M', 1024, (512,1), 1024, (512,1), 1024]  # conv14 - conv18\n\n    def __init__(self):\n        super(Darknet, self).__init__()\n        self.layer1 = self._make_layers(self.cfg1, in_planes=3)\n        self.layer2 = self._make_layers(self.cfg2, in_planes=512)\n\n        #### Add new layers\n        self.conv19 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)\n        self.bn19 = nn.BatchNorm2d(1024)\n        self.conv20 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)\n        self.bn20 = nn.BatchNorm2d(1024)\n        # Currently I removed the passthrough layer for simplicity\n        self.conv21 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)\n        self.bn21 = nn.BatchNorm2d(1024)\n        # Outputs: 5boxes * (4coordinates + 1confidence + 20classes)\n        self.conv22 = nn.Conv2d(1024, 5*(5+20), kernel_size=1, stride=1, padding=0)\n\n    def _make_layers(self, cfg, in_planes):\n        layers = []\n        for x in cfg:\n            if x == 'M':\n                layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]\n            else:\n                out_planes = x[0] if isinstance(x, tuple) else x\n                ksize = x[1] if isinstance(x, tuple) else 3\n                layers += [nn.Conv2d(in_planes, out_planes, kernel_size=ksize, padding=(ksize-1)//2),\n                           nn.BatchNorm2d(out_planes),\n                           nn.LeakyReLU(0.1, True)]\n                in_planes = out_planes\n        return nn.Sequential(*layers)\n\n    def forward(self, x):\n        out = self.layer1(x)\n        out = self.layer2(out)\n        out = F.leaky_relu(self.bn19(self.conv19(out)), 0.1)\n        out = F.leaky_relu(self.bn20(self.conv20(out)), 0.1)\n        out = F.leaky_relu(self.bn21(self.conv21(out)), 0.1)\n        out = self.conv22(out)\n        return out\n\n\ndef test():\n    net = Darknet()\n    y = net(Variable(torch.randn(1,3,416,416)))\n    print(y.size())  # 模型最后输出张量大小 [1,125,13,13]\n    \nif __name__ == \"__main__\":\n    test()\n```\n\n#### 4，多尺度训练\n\n`YOLOv1` 输入图像分辨率为 $448 \\times 448$，因为使用了 `anchor boxes`，所以 `YOLOv2` 将输入分辨率改为 $416 \\times 416$。又因为 `YOLOv2` 模型中只有卷积层和池化层，所以YOLOv2的输入可以不限于 $416 \\times 416$ 大小的图片。为了增强模型的鲁棒性，`YOLOv2` 采用了**多尺度输入训练**策略，具体来说就是在训练过程中每间隔一定的 `iterations` 之后改变模型的输入图片大小。由于 `YOLOv2` 的下采样总步长为 `32`，所以输入图片大小选择一系列为 `32` 倍数的值： $\\lbrace 320, 352,...,608 \\rbrace$ ，因此输入图片分辨率最小为 $320\\times 320$，此时对应的特征图大小为  $10\\times 10$（不是奇数），而输入图片最大为 $608\\times 608$ ，对应的特征图大小为 $19\\times 19$ 。在训练过程，每隔 `10` 个 `iterations` **随机**选择一种输入图片大小，然后需要修最后的检测头以适应维度变化后，就可以重新训练。\n\n采用 `Multi-Scale Training` 策略，`YOLOv2` 可以适应不同输入大小的图片，并且预测出很好的结果。在测试时，`YOLOv2` 可以采用不同大小的图片作为输入，在 `VOC 2007` 数据集上的测试结果如下图所示。\n\n![在voc2007数据集上的测试结果](../../data/images/yolov2/voc2007数据集测试结果.png)\n\n### 损失函数\n\n**`YOLOv2` 的损失函数的计算公式归纳如下**\n\n![损失函数计算](../../data/images/yolov2/损失函数计算.jfif)\n\n第 2,3 行：$t$ 是迭代次数，即前 `12800` 步我们计算这个损失，后面不计算了。即前 `12800` 步我们会优化预测的 $(x,y,w,h)$ 与 `anchor` 的 $(x,y,w,h)$ 的距离 `+` 预测的 $(x,y,w,h)$ 与 `GT` 的 $(x,y,w,h)$ 的距离，`12800` 步之后就只优化预测的 $(x,y,w,h)$与 `GT` 的 $(x,y,w,h)$ 的距离，原因是这时的预测结果已经较为准确了，`anchor`已经满足检测系统的需要，而在一开始预测不准的时候，用上 `anchor` 可以加速训练。\n\n`YOLOv2` 的损失函数实现代码如下，损失函数计算过程中的模型预测结果的解码函数和前面的解码函数略有不同，其包含关键部分目标 `bbox` 的解析。\n\n```python\nfrom __future__ import print_function\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nfrom torch.autograd import Variable\nfrom utils import box_iou, meshgrid\n\nclass YOLOLoss(nn.Module):\n    def __init__(self):\n        super(YOLOLoss, self).__init__()\n\n    def decode_loc(self, loc_preds):\n        '''Recover predicted locations back to box coordinates.\n        Args:\n          loc_preds: (tensor) predicted locations, sized [N,5,4,fmsize,fmsize].\n        Returns:\n          box_preds: (tensor) recovered boxes, sized [N,5,4,fmsize,fmsize].\n        '''\n        anchors = [(1.3221,1.73145),(3.19275,4.00944),(5.05587,8.09892),(9.47112,4.84053),(11.2364,10.0071)]\n        N, _, _, fmsize, _ = loc_preds.size()\n        loc_xy = loc_preds[:,:,:2,:,:]   # [N,5,2,13,13]\n        grid_xy = meshgrid(fmsize, swap_dims=True).view(fmsize,fmsize,2).permute(2,0,1)  # [2,13,13]\n        grid_xy = Variable(grid_xy.cuda())\n        box_xy = loc_xy.sigmoid() + grid_xy.expand_as(loc_xy)  # [N,5,2,13,13]\n\n        loc_wh = loc_preds[:,:,2:4,:,:]  # [N,5,2,13,13]\n        anchor_wh = torch.Tensor(anchors).view(1,5,2,1,1).expand_as(loc_wh)  # [N,5,2,13,13]\n        anchor_wh = Variable(anchor_wh.cuda())\n        box_wh = anchor_wh * loc_wh.exp()  # [N,5,2,13,13]\n        box_preds = torch.cat([box_xy-box_wh/2, box_xy+box_wh/2], 2)  # [N,5,4,13,13]\n        return box_preds\n\n    def forward(self, preds, loc_targets, cls_targets, box_targets):\n        '''\n        Args:\n          preds: (tensor) model outputs, sized [batch_size,150,fmsize,fmsize].\n          loc_targets: (tensor) loc targets, sized [batch_size,5,4,fmsize,fmsize].\n          cls_targets: (tensor) conf targets, sized [batch_size,5,20,fmsize,fmsize].\n          box_targets: (list) box targets, each sized [#obj,4].\n        Returns:\n          (tensor) loss = SmoothL1Loss(loc) + SmoothL1Loss(iou) + SmoothL1Loss(cls)\n        '''\n        batch_size, _, fmsize, _ = preds.size()\n        preds = preds.view(batch_size, 5, 4+1+20, fmsize, fmsize)\n\n        ### loc_loss\n        xy = preds[:,:,:2,:,:].sigmoid()   # x->sigmoid(x), y->sigmoid(y)\n        wh = preds[:,:,2:4,:,:].exp()\n        loc_preds = torch.cat([xy,wh], 2)  # [N,5,4,13,13]\n\n        pos = cls_targets.max(2)[0].squeeze() > 0  # [N,5,13,13]\n        num_pos = pos.data.long().sum()\n        mask = pos.unsqueeze(2).expand_as(loc_preds)  # [N,5,13,13] -> [N,5,1,13,13] -> [N,5,4,13,13]\n        loc_loss = F.smooth_l1_loss(loc_preds[mask], loc_targets[mask], size_average=False)\n\n        ### iou_loss\n        iou_preds = preds[:,:,4,:,:].sigmoid()  # [N,5,13,13]\n        iou_targets = Variable(torch.zeros(iou_preds.size()).cuda()) # [N,5,13,13]\n        box_preds = self.decode_loc(preds[:,:,:4,:,:])  # [N,5,4,13,13]\n        box_preds = box_preds.permute(0,1,3,4,2).contiguous().view(batch_size,-1,4)  # [N,5*13*13,4]\n        for i in range(batch_size):\n            box_pred = box_preds[i]  # [5*13*13,4]\n            box_target = box_targets[i]  # [#obj, 4]\n            iou_target = box_iou(box_pred, box_target)  # [5*13*13, #obj]\n            iou_targets[i] = iou_target.max(1)[0].view(5,fmsize,fmsize)  # [5,13,13]\n\n        mask = Variable(torch.ones(iou_preds.size()).cuda()) * 0.1  # [N,5,13,13]\n        mask[pos] = 1\n        iou_loss = F.smooth_l1_loss(iou_preds*mask, iou_targets*mask, size_average=False)\n\n        ### cls_loss\n        cls_preds = preds[:,:,5:,:,:]  # [N,5,20,13,13]\n        cls_preds = cls_preds.permute(0,1,3,4,2).contiguous().view(-1,20)  # [N,5,20,13,13] -> [N,5,13,13,20] -> [N*5*13*13,20]\n        cls_preds = F.softmax(cls_preds)  # [N*5*13*13,20]\n        cls_preds = cls_preds.view(batch_size,5,fmsize,fmsize,20).permute(0,1,4,2,3)  # [N*5*13*13,20] -> [N,5,20,13,13]\n        pos = cls_targets > 0\n        cls_loss = F.smooth_l1_loss(cls_preds[pos], cls_targets[pos], size_average=False)\n\n        print('%f %f %f' % (loc_loss.data[0]/num_pos, iou_loss.data[0]/num_pos, cls_loss.data[0]/num_pos), end=' ')\n        return (loc_loss + iou_loss + cls_loss) / num_pos\n```\n\n`YOLOv2` 在 `VOC2007` 数据集上和其他 `state-of-the-art` 模型的测试结果的比较如下曲线所示。\n\n![voc2007数据集测试结果](../../data/images/yolov2/voc2007数据集测试结果.png)\n\n## 三，YOLOv3\n\n> `YOLOv3` 的论文写得不是很好，需要完全看懂，还是要看代码，`C/C++` 基础不好的建议看 `Pytorch` 版本的复现。下文是我对原论文的精简翻译和一些难点的个人理解，以及一些关键代码解析。\n\n### 摘要\n\n我们对 `YOLO` 再次进行了更新，包括一些小的设计和更好的网络结构。在输入图像分辨率为 $320 \\times 320$ 上运行 `YOLOv3` 模型，时间是 `22 ms` 的同时获得了 `28.2` 的 `mAP`，精度和 `SSD` 类似，但是速度更快。和其他阈值相比，`YOLOv3` 尤其在 `0.5 IOU`（也就是 $AP_{50}$）这个指标上表现非常良好。在 `Titan X` 环境下，`YOLOv3` 的检测精度为 `57.9` AP50，耗时 `51 ms`；而 `RetinaNet` 的精度只有 `57.5 AP50`，但却需要 `198 ms`，相当于 `YOLOv3`的 `3.8` 倍。\n> 一般可以认为检测模型 = 特征提取器 + 检测头。\n### 1，介绍\n\n这篇论文其实也是一个技术报告，首先我会告诉你们 `YOLOv3` 的更新（改进）情况，然后介绍一些我们失败的尝试，最后是这次更新方法意义的总结。\n\n### 2，改进\n\n`YOLOv3` 大部分有意的改进点都来源于前人的工作，当然我们也训练了一个比其他人更好的分类器网络。\n\n#### 2.1，边界框预测\n\n> 这部分内容和 `YOLOv2` 几乎一致，但是内容更细致，且阈值的取值有些不一样。\n\n和 `YOLOv2` 一样，我们依然使用维度聚类的方法来挑选 `anchor boxes` 作为边界框预测的先验框。每个边界框都会预测 $4$ 个偏移坐标 $(t_x,t_y,t_w,t_h)$。假设 $(c_x, c_y)$ 为 `grid` 的左上角坐标，$p_w$、$p_h$ 是先验框（`anchors`）的宽度与高度，那么网络预测值和边界框真实位置的关系如下所示：\n> 假设某一层的 `feature map` 的大小为 $13 \\times 13$， 那么 `grid cell` 就有 $13 \\times 13$ 个，则第 $n$ 行第 $n$ 列的 `grid cell` 的坐标 $(x_x, c_y)$ 就是 $(n-1,n)$。\n\n$$\nb_x = \\sigma(t_x) + c_x \\\\\\\\\nb_y = \\sigma(t_y) + c_y \\\\\\\\\nb_w = p_{w}e^{t_w} \\\\\\\\\nb_h = p_{h}e^{t_h} $$\n\n![偏移量计算](../../data/images/yolov2/偏移量计算.png)\n\n$b_x,b_y,b_w,b_h$ 是边界框的实际中心坐标和宽高值。在训练过程中，我们使用平方误差损失函数。利用上面的公式，可以轻松推出这样的结论：如果预测坐标的真实值（`ground truth`）是 $\\hat{t}_{\\ast}$，那么梯度就是真实值减去预测值 $\\hat{t}_{\\ast} - t_{\\ast }$。\n> 梯度变成 $\\hat{t}_{\\ast} - t_{\\ast }$ 有什么好处呢？\n\n注意，计算损失的时候，模型预测输出的 $t_x,t_y$ 外面要套一个 `sigmoid` 函数 ，否则坐标就不是 $(0,1)$ 范围内的，一旦套了 `sigmoid`，就只能用 `BCE` 损失函数去反向传播，这样第一步算出来的才是 $t_x-\\hat{t}_x$；$(t_w,t_h)$ 的预测没有使用 `sigmoid` 函数，所以损失使用 $MSE$。\n> $\\hat{t}_x$ 是预测坐标偏移的真实值（`ground truth`）。\n\n`YOLOv3` 使用逻辑回归来预测每个边界框的 `objectness score`（**置信度分数**）。如果当前先验框和 `ground truth` 的 `IOU` 超过了前面的先验框，那么它的分数就是 `1`。和 `Faster RCNN` 论文一样，如果先验框和 `ground truth` 的 `IOU`不是最好的，那么即使它超过了阈值，我们还是会忽略掉这个 `box`，正负样本判断的阈值取 `0.5`。**`YOLOv3` 检测系统只为每个 `ground truth` 对象分配一个边界框**。如果先验框（`bonding box prior`，其实就是聚类得到的 `anchors`）未分配给 `ground truth` 对象，则不会造成位置和分类预测损失，只有置信度损失（`only objectness`）。\n\n将 `coco` 数据集的标签编码成 $(t_x,t_y,t_w,t_h)$ 形式的代码如下：\n\n```python\ndef get_target(self, target, anchors, in_w, in_h, ignore_threshold):\n    \"\"\"\n    Maybe have problem.\n    target: original coco dataset label.\n    in_w, in_h: feature map size.\n    \"\"\"\n    bs = target.size(0)\n\n    mask = torch.zeros(bs, self.num_anchors, in_h, in_w, requires_grad=False)\n    noobj_mask = torch.ones(bs, self.num_anchors, in_h, in_w, requires_grad=False)\n    tx = torch.zeros(bs, self.num_anchors, in_h, in_w, requires_grad=False)\n    ty = torch.zeros(bs, self.num_anchors, in_h, in_w, requires_grad=False)\n    tw = torch.zeros(bs, self.num_anchors, in_h, in_w, requires_grad=False)\n    th = torch.zeros(bs, self.num_anchors, in_h, in_w, requires_grad=False)\n    tconf = torch.zeros(bs, self.num_anchors, in_h, in_w, requires_grad=False)\n    tcls = torch.zeros(bs, self.num_anchors, in_h, in_w, self.num_classes, requires_grad=False)\n    for b in range(bs):\n        for t in range(target.shape[1]):\n            if target[b, t].sum() == 0:\n                continue\n            # Convert to position relative to box\n            gx = target[b, t, 1] * in_w\n            gy = target[b, t, 2] * in_h\n            gw = target[b, t, 3] * in_w\n            gh = target[b, t, 4] * in_h\n            # Get grid box indices\n            gi = int(gx)\n            gj = int(gy)\n            # Get shape of gt box\n            gt_box = torch.FloatTensor(np.array([0, 0, gw, gh])).unsqueeze(0)\n            # Get shape of anchor box\n            anchor_shapes = torch.FloatTensor(np.concatenate((np.zeros((self.num_anchors, 2)),\n                                                                np.array(anchors)), 1))\n            # Calculate iou between gt and anchor shapes\n            anch_ious = bbox_iou(gt_box, anchor_shapes)\n            # Where the overlap is larger than threshold set mask to zero (ignore)\n            noobj_mask[b, anch_ious > ignore_threshold, gj, gi] = 0\n            # Find the best matching anchor box\n            best_n = np.argmax(anch_ious)\n\n            # Masks\n            mask[b, best_n, gj, gi] = 1\n            # Coordinates\n            tx[b, best_n, gj, gi] = gx - gi\n            ty[b, best_n, gj, gi] = gy - gj\n            # Width and height\n            tw[b, best_n, gj, gi] = math.log(gw/anchors[best_n][0] + 1e-16)\n            th[b, best_n, gj, gi] = math.log(gh/anchors[best_n][1] + 1e-16)\n            # object\n            tconf[b, best_n, gj, gi] = 1\n            # One-hot encoding of label\n            tcls[b, best_n, gj, gi, int(target[b, t, 0])] = 1\n\n    return mask, noobj_mask, tx, ty, tw, th, tconf, tcls\n```\n\n另一个复习版本关于数据集标签的处理代码如下：\n\n```python\ndef build_targets(p, targets, model):\n    # Build targets for compute_loss(), input targets(image,class,x,y,w,h)\n    na, nt = 3, targets.shape[0]  # number of anchors, targets #TODO\n    tcls, tbox, indices, anch = [], [], [], []\n    gain = torch.ones(7, device=targets.device)  # normalized to gridspace gain\n    # Make a tensor that iterates 0-2 for 3 anchors and repeat that as many times as we have target boxes\n    ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt)\n    # Copy target boxes anchor size times and append an anchor index to each copy the anchor index is also expressed by the new first dimension\n    targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2)\n\n    for i, yolo_layer in enumerate(model.yolo_layers):\n        # Scale anchors by the yolo grid cell size so that an anchor with the size of the cell would result in 1\n        anchors = yolo_layer.anchors / yolo_layer.stride\n        # Add the number of yolo cells in this layer the gain tensor\n        # The gain tensor matches the collums of our targets (img id, class, x, y, w, h, anchor id)\n        gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]]  # xyxy gain\n        # Scale targets by the number of yolo layer cells, they are now in the yolo cell coordinate system\n        t = targets * gain\n        # Check if we have targets\n        if nt:\n            # Calculate ration between anchor and target box for both width and height\n            r = t[:, :, 4:6] / anchors[:, None]\n            # Select the ratios that have the highest divergence in any axis and check if the ratio is less than 4\n            j = torch.max(r, 1. / r).max(2)[0] < 4  # compare #TODO\n            # Only use targets that have the correct ratios for their anchors\n            # That means we only keep ones that have a matching anchor and we loose the anchor dimension\n            # The anchor id is still saved in the 7th value of each target\n            t = t[j]\n        else:\n            t = targets[0]\n\n        # Extract image id in batch and class id\n        b, c = t[:, :2].long().T\n        # We isolate the target cell associations.\n        # x, y, w, h are allready in the cell coordinate system meaning an x = 1.2 would be 1.2 times cellwidth\n        gxy = t[:, 2:4]\n        gwh = t[:, 4:6]  # grid wh\n        # Cast to int to get an cell index e.g. 1.2 gets associated to cell 1\n        gij = gxy.long()\n        # Isolate x and y index dimensions\n        gi, gj = gij.T  # grid xy indices\n\n        # Convert anchor indexes to int\n        a = t[:, 6].long()\n        # Add target tensors for this yolo layer to the output lists\n        # Add to index list and limit index range to prevent out of bounds\n        indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1)))\n        # Add to target box list and convert box coordinates from global grid coordinates to local offsets in the grid cell\n        tbox.append(torch.cat((gxy - gij, gwh), 1))  # box\n        # Add correct anchor for each target to the list\n        anch.append(anchors[a])\n        # Add class for each target to the list\n        tcls.append(c)\n\n    return tcls, tbox, indices, anch\n```\n\n关于更多模型推理部分代码的复现和理解，可阅读这个 [github项目代码](https://github.com/ayooshkathuria/YOLO_v3_tutorial_from_scratch)。\n\n#### 2.2，分类预测\n\n每个框使用**多标签分类**来预测边界框可能包含的类。我们不使用 `softmax` 激活函数，因为我们发现它对模型的性能影响不好。相反，我们只是使用独立的逻辑分类器。在训练过程中，我们使用二元交叉熵损失来进行类别预测。\n\n在这个数据集 Open Images Dataset 中有着大量的重叠标签。如果使用 `softmax` ，意味着强加了一个假设，**即每个框只包含一个类别**，但通常情况并非如此。多标签方法能更好地模拟数据。\n\n#### 2.3，跨尺度预测\n\n`YOLOv3` 可以预测 `3` 种不同尺度（`scale`）的框。\n总的来说是，引入了类似 FPN 的多尺度特征图融合，从而加强小目标检测。与原始的 `FPN` 不同，`YOLOv3` 的 `Neck` 网络只输出 `3` 个分支，分别对应 `3` 种尺度，高层网络输出的特征图经过上采样后和低层网络输出的特征图融合是使用 `concat` 方式拼接，而不是使用 `element-wise` add 的方法。\n\n首先检测系统利用和特征金字塔网络[8]（FPN 网络）类似的概念，来提取不同尺度的特征。我们在基础的特征提取器基础上添加了一些卷积层。这些卷积层的最后会预测一个 `3` 维张量，其是用来编码边界框，框中目标和分类预测。在 `COCO` 数据集的实验中，我们每个输出尺度都预测 `3` 个 `boxes`，所以模型最后输出的张量大小是 $N \\times N \\times [3*(4+1+80)]$，其中包含 `4` 个边界框`offset`、`1` 个 `objectness` 预测（前景背景预测）以及 `80` 种分类预测。\n> `objectness` 预测其实就是前景背景预测，有些类似 `YOLOv2` 的置信度 `c` 的概念。\n\n然后我们将前面两层输出的特征图上采样 `2` 倍，并和浅层中的特征图，用 `concatenation` 方式把高低两种分辨率的特征图连接到一起，这样做能使我们同时获得上采样特征的有意义的**语义信息和来自早期特征的细粒度信息**。之后，再添加几个卷积层来处理这个融合后的特征，并输出大小是原来高层特征图两倍的张量。\n\n按照这种设计方式，来预测最后一个尺度的 `boxes`。可以知道，对第三种尺度的预测也会从所有先前的计算中（多尺度特征融合的计算中）获益，同时能从低层的网络中获得细粒度（ finegrained ）的特征。\n> 显而易见，低层网络输出的特征图语义信息比较少，但是目标位置准确；高层网络输出的特征图语义信息比较丰富，但是目标位置比较粗略。\n\n依然使用 `k-means` 聚类来确定我们的先验边界框（`box priors`，即选择的 `anchors`），但是选择了 `9` 个聚类（`clusters`）和 `3` 种尺度（`scales`，大、中、小三种 `anchor` 尺度），然后在整个尺度上均匀分割聚类。在`COCO` 数据集上，`9` 个聚类是：（10×13）;（16×30）;（33×23）;（30×61）;（62×45）;（59×119）;（116×90）;（156×198）;（373×326）。\n\n从上面的描述可知，`YOLOv3` 的检测头变成了 `3` 个分支，对于输入图像 `shape` 为 (3, 416, 416)的 `YOLOv3` 来说，`Head` 各分支的输出张量的尺寸如下：\n\n- [13, 13, 3\\*(4+1+80)]\n- [26, 2, 3\\*(4+1+80)]\n- [52, 52, 3\\*(4+1+80)]\n\n`3` 个分支分别对应 `32` 倍、`16` 倍、`8`倍下采样，也就是分别预测大、中、小目标。`32` 倍下采样的特征图的每个点感受野更大，所以用来预测大目标。\n\n每个 `sacle` 分支的每个 `grid` 都会预测 `3` 个框，每个框预测 `5` 元组+ `80` 个 `one-hot` `vector`类别，所以一共 `size` 是：`3*(4+1+80)`。\n\n根据前面的内容，可以知道，`YOLOv3` 总共预测 $(13 \\times 13 + 26 \\times 26 + 52 \\times 52) \\times 3 = 10467(YOLOv3) \\gg 845 = 13 \\times 13 \\times 5(YOLOv2)$ 个边界框。\n\n#### 2.4，新的特征提取网络\n\n我们使用一个新的网络来执行特征提取。它是 `Darknet-19`和新型残差网络方法的融合，由连续的 $3\\times 3$ 和 $1\\times 1$ 卷积层组合而成，并添加了一些 `shortcut connection`，整体体量更大。因为一共有 $53 = (1+2+8+8+4)\\times 2+4+2+1 $ 个卷积层，所以我们称为 `Darknet-53`。\n\n![Darknet-53网络参数表](../../data/images/yolov3/darknet-53.png)\n\n总的来说，`DarkNet-53` 不仅使用了全卷积网络，将 `YOLOv2` 中降采样作用 `pooling` 层都换成了 `convolution`(`3x3，stride=2`) 层；而且引入了残差（`residual`）结构，不再使用类似 `VGG` 那样的直连型网络结构，因此可以训练更深的网络，即卷积层数达到了 `53` 层。（更深的网络，特征提取效果会更好）\n\n`Darknet53` 网络的 `Pytorch` 代码如下所示。\n> 代码来源[这里](https://github.com/BobLiu20/YOLOv3_PyTorch.git)。\n\n```python\nimport torch\nimport torch.nn as nn\nimport math\nfrom collections import OrderedDict\n\n__all__ = ['darknet21', 'darknet53']\n\n\nclass BasicBlock(nn.Module):\n    \"\"\"basic residual block for Darknet53，卷积层分别是 1x1 和 3x3\n    \"\"\"\n    def __init__(self, inplanes, planes):\n        super(BasicBlock, self).__init__()\n        self.conv1 = nn.Conv2d(inplanes, planes[0], kernel_size=1,\n                               stride=1, padding=0, bias=False)\n        self.bn1 = nn.BatchNorm2d(planes[0])\n        self.relu1 = nn.LeakyReLU(0.1)\n        self.conv2 = nn.Conv2d(planes[0], planes[1], kernel_size=3,\n                               stride=1, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(planes[1])\n        self.relu2 = nn.LeakyReLU(0.1)\n\n    def forward(self, x):s\n        residual = x\n\n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = self.relu1(out)\n\n        out = self.conv2(out)\n        out = self.bn2(out)\n        out = self.relu2(out)\n\n        out += residual\n        return out\n\n\nclass DarkNet(nn.Module):\n    def __init__(self, layers):\n        super(DarkNet, self).__init__()\n        self.inplanes = 32\n        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(self.inplanes)\n        self.relu1 = nn.LeakyReLU(0.1)\n\n        self.layer1 = self._make_layer([32, 64], layers[0])\n        self.layer2 = self._make_layer([64, 128], layers[1])\n        self.layer3 = self._make_layer([128, 256], layers[2])\n        self.layer4 = self._make_layer([256, 512], layers[3])\n        self.layer5 = self._make_layer([512, 1024], layers[4])\n\n        self.layers_out_filters = [64, 128, 256, 512, 1024]\n\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels\n                m.weight.data.normal_(0, math.sqrt(2. / n))\n            elif isinstance(m, nn.BatchNorm2d):\n                m.weight.data.fill_(1)\n                m.bias.data.zero_()\n\n    def _make_layer(self, planes, blocks):\n        layers = []\n        # 每个阶段的开始都要先 downsample，然后才是 basic residual block for Darknet53\n        layers.append((\"ds_conv\", nn.Conv2d(self.inplanes, planes[1], kernel_size=3,\n                                stride=2, padding=1, bias=False)))\n        layers.append((\"ds_bn\", nn.BatchNorm2d(planes[1])))\n        layers.append((\"ds_relu\", nn.LeakyReLU(0.1)))\n        #  blocks\n        self.inplanes = planes[1]\n        for i in range(0, blocks):\n            layers.append((\"residual_{}\".format(i), BasicBlock(self.inplanes, planes)))\n        return nn.Sequential(OrderedDict(layers))\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = self.relu1(x)\n\n        x = self.layer1(x)\n        x = self.layer2(x)\n        out3 = self.layer3(x)\n        out4 = self.layer4(out3)\n        out5 = self.layer5(out4)\n\n        return out3, out4, out5\n\ndef darknet21(pretrained, **kwargs):\n    \"\"\"Constructs a darknet-21 model.\n    \"\"\"\n    model = DarkNet([1, 1, 2, 2, 1])\n    if pretrained:\n        if isinstance(pretrained, str):\n            model.load_state_dict(torch.load(pretrained))\n        else:\n            raise Exception(\"darknet request a pretrained path. got [{}]\".format(pretrained))\n    return model\n\ndef darknet53(pretrained, **kwargs):\n    \"\"\"Constructs a darknet-53 model.\n    \"\"\"\n    model = DarkNet([1, 2, 8, 8, 4])\n    if pretrained:\n        if isinstance(pretrained, str):\n            model.load_state_dict(torch.load(pretrained))\n        else:\n            raise Exception(\"darknet request a pretrained path. got [{}]\".format(pretrained))\n    return model\n```\n\n`3` 个预测分支，对应预测 `3` 种尺度（大、种、小），也都采用了全卷积的结构。\n\n`YOLOv3` 的 `backbone` 选择 `Darknet-53`后，其检测性能远超 `Darknet-19`，同时效率上也优于 `ResNet-101` 和 `ResNet-152`，对比实验结果如下：\n\n![和其他backbone的比较结果](../../data/images/yolov3/和其他backbone的比较结果.png)\n\n在对比实验中，每个网络都使用相同的设置进行训练和测试。运行速度 `FPS` 是在 `Titan X` 硬件上，输入图像大小为 $256 \\times 256$ 上测试得到的。从上表可以看出，`Darknet-53` 和 `state-of-the-art` 分类器相比，有着更少的 `FLOPs` 和更快的速度。和 `ResNet-101` 相比，精度更高并且速度是前者的 `1.5` 倍；和 `ResNet-152` 相比，精度相似，但速度是它的 `2` 倍以上。\n\n`Darknet-53` 也可以实现每秒最高的测量浮点运算。这意味着其网络结构可以更好地利用 `GPU`，从而使其评估效率更高，速度更快。这主要是因为 `ResNets` 的层数太多，效率不高。\n\n#### 2.5，训练\n\n和 `YOLOv2` 一样，我们依然训练所有图片，没有 `hard negative mining or any of that stuff`。我们依然使用多尺度训练，大量的数据增强操作和 `BN` 层以及其他标准操作。我们使用之前的 `Darknet` 神经网络框架进行训练和测试[12]。\n\n**损失函数的计算公式如下**。\n\n![YOLOv3的损失函数计算公式](../../data/images/yolov3/yolov3的损失函数计算公式.jpg)\n\n`YOLO v3` 使用多标签分类，用多个独立的 `logistic` 分类器代替 `softmax` 函数，以计算输入属于特定标签的可能性。在计算分类损失进行训练时，`YOLOv3` 对每个标签使用二元交叉熵损失。\n\n**正负样本的确定**：\n\n+ 正样本：与 `GT` 的 `IOU` 最大的框。\n+ 负样本：与 `GT` 的 `IOU<0.5` 的框。\n+ 忽略的样本：与 `GT` 的 `IOU>0.5` 但不是最大的框。\n+ 使用 $t_x$ 和 $t_y$ （而不是 $b_x$ 和 $b_y$ ）来计算损失。\n\n注意：**每个 `GT` 目标仅与一个先验边界框相关联。如果没有分配先验边界框，则不会导致分类和定位损失，只会有目标的置信度损失**。\n\n**YOLOv3 网络结构图如下所示**（这里输入图像大小为 `608*608`，来源 [这里](https://zhuanlan.zhihu.com/p/183781646?utm_source=wechat_session&utm_medium=social&utm_oi=737449911926140928) ）。\n\n![yolov3网络结构图](../../data/images/yolov3/yolov3结构图.png)\n\n### 2.5，推理\n\n总的来说还是将输出的特侦图划分成 S*S（这里的S和特征图大小一样） 的网格，**通过设置置信度阈值对网格进行筛选，只有大于指定阈值的网格才认为存在目标，即该网格会输出目标的置信度、bbox 坐标和类别信息，并通过 NMS 操作筛选掉重复的框**。\n\n值得注意的是，模型推理的 bbox 的 $xywh$ 值是对应 feature map 尺度的，所以后面还需要将 xywh 的值 * 特征图的下采样倍数。\n\n```python\n# 将 bbox 预测值, box 置信度, box 分类结果的矩阵拼接成一个新的矩阵\n# * _scale 是为了将预测的 box 对应到原图尺寸, _scale 是特征图下采样倍数。\n# 对于大目标检测分支 pred_boxes.view(bs, -1, 4) 后的 shape 为 [1, 507, 4], output 的 shape 为 [1, 507, 85]\n# bs 是 batch_size，即一次推理多少张图片。\noutput = torch.cat((pred_boxes.view(bs, -1, 4) * _scale,\n                    conf.view(bs, -1, 1), pred_cls.view(bs, -1, self.num_classes)), -1)\n```\n\n### 3，实验结果\n\n`YOLOv3` 实验结果非常好！详情见表3。\n\n![YOLOv3的实验结果表格](../../data/images/yolov3/YOLOv3的实验结果表格.png)\n\n就 `COCO` 的 `mAP` 指标而言，`YOLOv3` 和 `SSD` 变体相近，但是速度却比后者快了 `3` 倍。尽管如此，`YOLOv3` 还是比 `Retinanet` 这样的模型在精度上要差一些。\n\n但是当我们以 `IOU = 0.5` 这样的旧指标对比，`YOLOv3` 表现更好，几乎和 `Retinanet` 相近，远超 `SSD` 变体。这表面它其实是一款非常灵活的检测器，擅长为检测对象生成合适的边界框。然而，随着IOU阈值增加，YOLOv3 的性能开始同步下降，这时它预测的边界框就不能做到完美对齐了。\n\n在过去的 `YOLOv1/v2` 上，`YOLO` 一直在小目标检测领域表现不好，现在 `YOLOv3` 基本解决了这个问题，有着更好的 $AP_S$ 性能。但是它目前在中等尺寸或大尺寸物体上的表现还相对较差，仍需进一步的完善。\n\n当我们基于 $AP_{50}$ 指标绘制精度和速度曲线（`Figure 3`）时，我们发现YOLOv3与其他检测系统相比具有显着优势，换句话说，它更快更好。\n\n![yolov3在coco数据集上测试结果](../../data/images/yolov3/yolov3在coco数据集上测试结果.png)\n> 从 `Figure 3` 可以看出，`YOLOv3` 的曲线非常靠近曲线坐标的同时又非常高，这意味着 `YOLOv3` 有着良好速度的同时又有很好的精度，无愧当前最强目标检测模型。\n\n### 4，失败的尝试\n\n一些没有作用的尝试工作如下。\n\n**Anchor box x,y 偏移预测**。我们尝试了常规的 `Anchor box` 预测方法，比如利用线性激活将坐标 $x、y$ 的偏移程度，预测为边界框宽度或高度的倍数。但我们发现这种做法降低了模型的稳定性，且效果不佳。\n\n**用线性方法预测 x,y，而不是使用 logistic**。我们尝试使用线性激活函数来直接预测 $x，y$ 的偏移，而不是 `ligistic` 激活函数，但这降低了 `mAP`。\n\n**focal loss**。我们尝试使用focal loss，但它使我们的 `mAP`降低了 `2` 点。 对于 `focal loss` 函数试图解决的问题，`YOLOv3` 已经具有鲁棒性，因为它具有单独的对象性预测（`objectness predictions`）和条件类别预测。因此，对于大多数示例来说，类别预测没有损失？或者其他的东西？我们并不完全确定。\n\n**双 IOU 阈值和真值分配**。在训练过程中，`Faster RCNN` 用了两个`IOU` 阈值，如果预测的边框与的 `ground truth` 的 `IOU` 是 `0.7`，那它是正样本 ；如果 `IOU` 在 [0.3—0.7]之间，则忽略这个 `box`；如果小于 `0.3`，那它是个负样本。我们尝试了类似的策略，但效果并不好。\n\n### 5，改进的意义\n\n`YOLOv3` 是一个很好的检测器，速度很快，很准确。虽然它在 `COCO` 数据集上的 `mAP` 指标，即 $AP_{50}$ 到 $AP_{90}$ 之间的平均值上表现不好，但是在旧指标 $AP_{50}$ 上，它表现非常好。\n\n总结 `YOLOv3` 的改进点如下：\n\n+ 使用金字塔网络来实现多尺度预测，从而解决小目标检测的问题。\n+ 借鉴残差网络来实现更深的 `Darknet-53`，从而提升模型检测准确率。\n+ 使用 `sigmoid` 函数替代 `softmax` 激活来实现多标签分类器。\n+ 位置预测修改，一个 `gird` 预测 `3` 个 `box`。\n\n## 四，YOLOv4\n\n> 因为 `YOLOv1-v3` 的作者不再更新 `YOLO` 框架，所以 `Alexey Bochkovskiy` 接起了传承 `YOLO` 的重任。相比于它的前代，`YOLOv4` 不再是原创性且让人眼前一亮的研究，但是却集成了目标检测领域的各种实用 `tricks` 和即插即用模块 ，称得上是基于 `YOLOv3` 框架的各种目标检测 `tricks` 的集大成者。\n> 本文章不会对原论文进行一一翻译，但是做了系统性的总结和关键部分的翻译。\n\n### 1，摘要及介绍\n\n我们总共使用了：`WRC`、`CSP`、`CmBN`、`SAT`、`Mish` 激活和 `Mosaic` 数据增强、`CIoU` 损失方法，并组合其中的一部分，使得最终的检测模型在 `MS COCO` 数据集、`Tesla V100` 显卡上达到了 `43.5%` `AP` 精度 和 `65` `FPS` 速度。\n\n![更好训练目标检测模型的方法](../../data/images/yolov4/yolov4和其他目标检测模型的比较.png)\n\n我们的主要贡献在于：\n\n1. 构建了简单高效的 `YOLOv4` 检测器，修改了 `CBN`、`PAN`、`SAM` 方法使得 `YOLOv4` 能够在一块 `1080Ti` 上就能训练。\n2. 验证了当前最先进的 `Bag-of-Freebies` 和 `Bag-of-Specials` 方法在训练期间的影响。\n\n目前的目标检测网络分为两种：一阶段和两阶段。检测算法的组成：`Object detector = backbone + neck + head`，具体结构如下图所示。\n\n![目标检测器通用结构图](../../data/images/yolov4/目标检测器结构图.png)\n\n### 2，相关工作\n\n#### 2.1，目标检测方法\n\n按照检测头的不同（`head`）将目标检测模型分为：两阶段检测和一阶段检测模型，各自代表是 `Faster RCNN` 和 `YOLO` 等，最近也出了一些无 `anchor` 的目标检测器，如 `CenterNet` 等。近几年来的检测器会在`Backbone`网络（`backbone`）和头部网络（`head`）之间插入一些网络层，主要作用是**收集不同阶段的特征**。，称其为检测器的颈部（`neck`）。 `neck` 通常由几个自下而上（`bottom-up`）的路径和几个自上而下（`top-down`）的路径组成。 配备此机制的网络包括特征金字塔网络（`FPN`）[44]，路径聚合网络（`PAN`）[49]，`BiFPN` [77]和`NAS-FPN` [17]。\n\n一般，目标检测器由以下几部分组成：\n\n![目标检测器的各个组成部分](../../data/images/yolov4/目标检测器的各个组成部分.png)\n\n#### 2.2，Bag of freebies（免费技巧）\n\n> 不会改变模型大小，主要是针对输入和 `loss` 等做的优化工作，一切都是为了让模型训练得更好。\n\n最常用的方式是**数据增强**（`data augmentation`），目标是为了提升输入图像的可变性（`variability`），这样模型在不同环境中会有更高的鲁棒性。常用的方法分为两类：光度失真和几何失真（`photometric distortions and geometric distortions`）。在处理**光度**失真时，我们调整了图像的亮度、对比度、色调、饱和度和噪声；对于**几何**失真，我们添加随机缩放，裁剪，翻转和旋转。\n\n上述数据增强方法都是**逐像素调整**的，并且保留了调整区域中的所有原始像素信息。此外，也有些研究者将重点放在模拟**对象遮挡**的问题上，并取得了一些成果。例如随机擦除(`random-erase`)[100] 和 `CutOut` [11] 方法会随机选择图像中的矩形区域，并填充零的随机或互补值。而捉迷藏(`hide-and-seek`)[69] 和网格遮罩(`grid-mask`)[6] 方法则随机或均匀地选择图像中的多个矩形区域，并将它们替换为所有的 `zeros`。这个概念有些类似于 `Dropout`、`DropConnect` 和 `DropBlock` 这些在 `feature` 层面操作的方法，如 。此外，一些研究人员提出了使用**多个图像一起执行数据增强**的方法。 例如，`MixUp` 方法使用两个图像以不同的系数比值相乘后叠加，然后使用这些叠加的比值来调整标签。 对于 `CutMix`，它是将裁切后的图像覆盖到其他图像的矩形区域，并根据混合区域的大小调整标签。 除了以上方法之外，还有 `style transfer GAN` 方法用于数据扩充、减少 `CNN` 所学习的纹理偏差。\n> `MIX-UP`：`Mix-up` 在分类任务中，将两个图像按照不同的比例相加，例如 $A\\ast 0.1 + B\\ast 0.9=C$，那么 $C$的 `label` 就是 $[0.1A, 0.9A]$。在目标检测中的做法就是将一些框相加，这些 `label` 中就多了一些不同置信度的框。\n\n上面的方法是针对数据增强目标，第二类方法是针对解决**数据集中语义分布可能存在偏差**的问题（` semantic distribution in the dataset may have bias`）。在处理语义分布偏差问题时，**类别不平衡**（`imbalance between different classes`）问题是其中的一个关键，在两阶段对象检测器中通常通过困难负样本挖掘（`hard negative example mining`）或在线困难样本挖掘（`online hard example mining`，简称 `OHEM`）来解决。但样本挖掘方法并不能很好的应用于一阶段检测器，因为它们都是密集检测架构（`dense prediction architecture`）。因此，何凯明等作者提出了 `Focal Loss` 用来解决类别不平衡问题。 另外一个关键问题是，很难用 `one-hot hard representation` 来表达不同类别之间的关联度的关系，但执行标记时又通常使用这种表示方案。 因此在（`Rethinking the inception architecture for computer vision`）论文中提出标签平滑（`label smoothing`）的概念，将硬标签转换为软标签进行训练，使模型更健壮。为了获得更好的软标签，论文(`Label refinement network for\ncoarse-to-fine semantic segmentation`)介绍了知识蒸馏的概念来设计标签细化网络。\n\n最后一类方法是针对**边界框（`BBox`）回归的目标函数**。传统的目标检测器通常使用均方误差（$MSE$）对`BBox` 的中心点坐标以及高度和宽度直接执行回归，即 $\\lbrace x_{center}, y_{center}, w, h \\rbrace$ 或者 $\\lbrace x_{top-left}, y_{top-left}, x_{bottom-right}, y_{bottom-right} \\rbrace$ 坐标。如果基于锚的方法，则估计相应的偏移量，例如 $\\lbrace x_{cener-offset}, y_{cener-offset}, w, h \\rbrace$ 或者 $\\lbrace x_{top-left-offset}, y_{top-left-offset}, x_{bottom-right-offset}, y_{bottom-right-offset} \\rbrace$。这些直接估计 `BBox` 的各个点的坐标值的方法，是将这些点视为独立的变量，但是实际上这没有考虑对象本身的完整性。为了更好的回归 BBox，一些研究者提出了 `IOU` 损失[90]。顾名思义，`IoU` 损失既是使用 `Ground Truth` 和预测 `bounding box`（`BBox`）的交并比作为损失函数。因为 `IoU` 是尺度不变的表示，所以可以解决传统方法计算 $\\lbrace x，y，w，h \\rbrace$ 的 $L1$ 或 $L2$ 损失时，损失会随着尺度增加的问题。 最近，一些研究人员继续改善 `IoU` 损失。 例如，`GIoU` 损失除了覆盖区域外还包括对象的形状和方向，`GIoU` 损失的分母为同时包含了预测框和真实框的最小框的面积。`DIoU` 损失还考虑了对象中心的距离，而`CIoU` 损失同时考虑了重叠区域，中心点之间的距离和纵横比。 `CIoU` 损失在 `BBox` 回归问题上可以实现更好的收敛速度和准确性。\n> [90] 论文: An advanced object detection network. \n\n#### 2.3，Bag of specials（即插即用模块+后处理方法）\n\n对于那些仅增加少量推理成本但可以显著提高目标检测器准确性的**插件模块或后处理方法**，我们将其称为 `“Bag of specials”`。一般而言，这些插件模块用于增强模型中的某些属性，例如**扩大感受野，引入注意力机制或增强特征集成能力**等，而后处理是用于筛选模型预测结果的方法。\n\n**增大感受野模块**。用来增强感受野的常用模块有 `SPP`、`ASPP` 和 `RFB`。`SPP` 起源于空间金字塔匹配（`SPM`）,`SPM` 的原始方法是将特征图分割为几个 $d\\times d$ 个相等的块，其中 $d$ 可以为 $\\lbrace 1,2,3，.. \\rbrace$，从而形成空间金字塔。`SPP` 将 `SPM` 集成到 `CNN` 中，并使用最大池化操作（`max pooling`）替代 `bag-of-word operation`。原始的 `SPP` 模块是输出一维特征向量，这在 FCN 网络中不可行。\n\n**引入注意力机制**。在目标检测中经常使用的注意力模块，通常分为 `channel-wise` 注意力和 `point-wise` 注意力。代表模型是 `SE` 和 `SAM`（Spatial Attention Module ）。虽然 `SE` 模块可以提高 `ReSNet50` 在 `ImageNet` 图像分类任务 `1%` 的 `top-1` 准确率而计算量只增加 `2%`，但是在 `GPU` 上，通常情况下，它会将增加推理时间的 `10%` 左右，所以更适合用于移动端。但对于 `SAM`，它只需要增加 `0.1％` 的额外的推理时间，就可以在 `ImageNet` 图像分类任务上将 `ResNet50-SE` 的`top-1` 准确性提高 `0.5％`。 最好的是，它根本不影响 `GPU` 上的推理速度。\n\n**特征融合或特征集成**。早期的实践是使用 `skip connection` 或 `hyper-column` 将低层物理特征集成到高层语义特征。 由于诸如 `FPN` 的多尺度预测方法已变得流行，因此提出了许多集成了不同特征金字塔的轻量级模块。 这种模块包括 `SFAM`，`ASFF`和 `BiFPN`。 `SFAM` 的主要思想是使用 `SE` 模块在多尺度级联特征图上执行通道级级别的加权。 对于 `ASFF`，它使用`softmax` 作为逐点级别权重，然后添加不同比例的特征图。在`BiFPN` 中，提出了多输入加权残差连接以执行按比例的级别重新加权，然后添加不同比例的特征图。\n\n**激活函数**。良好的激活函数可以使梯度在反向传播算法中得以更有效的传播，同时不会引入过多的额外计算成本。`2010` 年 `Nair` 和 `Hinton` 提出的 `ReLU` 激活函数，实质上解决了传统的`tanh` 和 `sigmoid` 激活函数中经常遇到的梯度消失问题。随后，随后，`LReLU`，`PReLU`，`ReLU6`，比例指数线性单位（`SELU`），`Swish`，`hard-Swish` 和 `Mish`等激活函数也被提出来，用于解决梯度消失问题。`LReLU` 和 `PReLU` 的主要目的是解决当输出小于零时 `ReLU` 的梯度为零的问题。而 `ReLU6` 和 `Hard-Swish` 是专门为量化网络设计的。同时，提出了 `SELU` 激活函数来对神经网络进行自归一化。 最后，要注意 `Swish` 和 `Mish` 都是连续可区分的激活函数。\n\n**后处理**。最开始常用 `NMS` 来剔除重复检测的 `BBox`，但是 `NMS` 会不考虑上下文信息（可能会把一些相邻检测框框给过滤掉），因此 `Girshick` 提出了 `Soft NMS`，为相邻检测框设置一个衰减函数而非彻底将其分数置为零。而 `DIoU NMS` 则是在 `soft NMS` 的基础上将中心距离的信息添加到 `BBox` 筛选过程中。值得一提的是，因为上述后处理方法都没有直接涉及捕获的图像特征，因此在后续的 `anchor-free` 方法中不再需要 `NMS` 后处理。\n\n### 3，方法\n\n我们的基本目标是在生产系统中快速对神经网络进行操作和并行计算优化，而不是使用低计算量理论指示器（`BFLOP`）。 我们提供了两种实时神经网络：\n\n+ 对于 `GPU`，我们在卷积层中使用少量分组（1-8）：如`CSPResNeXt50 / CSPDarknet53`\n+ 对于 `VPU`，我们使用分组卷积，但是我们避免使用 `SE`-特别是以下模型：`EfficientNet-lite / MixNet [76] / GhostNet [21] / MobiNetNetV3`\n\n#### 3.1，架构选择\n\n我们的目标是在输入图像分辨率、卷积层数量、参数量、层输出（滤波器）数量之间找到最优平衡。我们大量的研究表面，在 `ILSVRC2012(ImageNet)` 分类数据集上，`CSPResNext50` 网络优于 `CSPDarknet`，但是在 `MS COCO` 目标检测数据集上，却相反。\n> 这是为什么呢，两种网络，一个分类数据集表现更好，一个检测数据集表现更好。\n\n![CSPDarknet53和CSPResNext50的比较](../../data/images/yolov4/CSPDarknet53和CSPResNext50的比较.png)\n\n在分类问题上表现最优的参考模型并不一定总是在检测问题上也表现最优。与分类器相比，检测器需要满足以下条件：\n\n+ 更高的输入网络尺寸（分辨率），用于检测多个小型物体。\n+ 更多的网络层，用以得到更高的感受野以覆盖更大的输入网络尺寸。\n+ 更多参数，用以得到更大的模型容量，从而可以在单个图像中检测到多个不同大小的对象。\n\n表1 显示了 `CSPResNeXt50`，`CSPDarknet53` 和`EfficientNet B3` 网络的信息。`CSPResNext50` 仅包含`16` 个 $3\\times 3$ 卷积层，最大感受野为 $425\\times 425$和网络参数量为 `20.6 M`，而 `CSPDarknet53` 包含 `29` 个 $3\\times 3$ 卷积层，最大感受野为 $725\\times 725$ 感受野和参数量为 `27.6 M`。理论上的论证再结合作者的大量实验结果，表面 `CSPDarknet53` 更适合作为目标检测器的 `backbone`。\n\n不同大小的感受野的影响总结如下：\n\n- 达到对象大小 - 允许查看整个对象\n- 达到网络大小 - 允许查看对象周围的上下文环境\n- 超过网络规模 - 增加图像点和最终激活之间的连接\n\n我们在 `CSPDarknet53` 上添加了 `SPP` 模块，因为它显著增加了感受野，分离出最重要的上下文特征，并且几乎没有降低网络运行速度。 我们使用 `PANet` 作为针对不同检测器级别的来自不同`backbone` 级别的参数聚合方法，而不是 `YOLOv3`中使用的`FPN`。\n\n最后，我们的 `YOLOv4` 架构体系如下：\n\n- `backbone`：`CSPDarknet53` + `SPP`\n- `neck`: `PANet`\n- `head`：`YOLOv3` 的 `head`\n\n#### 3.2，Selection of BoF and BoS\n\n为了更好的训练目标检测模型，`CNN` 通常使用如下方法：\n\n- 激活函数：`ReLU`，`leaky-ReLU`，`parameter-ReLU`，`ReLU6`，`SELU`，`Swish` 或 `Mish`；\n- 边界框回归损失：`MSE`，`IoU`，`GIoU`，`CIoU`，`DIoU` 损失；\n- 数据扩充：`CutOut`，`MixUp`，`CutMix`\n- 正则化方法：`DropOut`，`DropPath`，空间 `DropOut` 或 `DropBlock`\n- 通过均值和方差对网络激活进行归一化：批归一化（BN），交叉-GPU 批处理规范化（`CGBN` 或 `SyncBN`），过滤器响应规范化（`FRN`）或交叉迭代批处理规范化（`CBN`）；\n- 跳跃连接：残差连接，加残差连接，多输入加权残差连接或跨阶段局部连接（`CSP`）\n\n以上方法中，我们首先提出了难以训练的 `PRELU` 和 `SELU`，以及专为量化网络设计的 `ReLU6` 激活。因为 `DropBlock` 作者证明了其方法的有效性，所以正则化方法中我们使用 `DropBlock`。\n\n#### 3.3，额外的改进\n\n> 这些方法是作者对现有方法做的一些改进。\n\n为了让 `YOLOv4` 能更好的在单个 `GPU` 上训练，我们做了以下额外改进：\n\n- 引入了新的数据增强方法：`Mosaic` 和自我对抗训练 `self-adversarial training`（`SAT`）。\n- 通过遗传算法选择最优超参数。\n- 修改了 `SAM`、`PAN` 和 `CmBN`。\n\n`Mosaic` 是一种新的数据增强方法，不像 `cutmix` 仅混合了两张图片，它混合了 $4$ 张训练图像，从而可以检测到超出其正常上下文的对象。 此外，`BN` 在每层上计算的激活统计都是来自 `4` 张不同图像，这大大减少了对大 `batch size` 的需求。\n\n![Mosic数据增强方法](../../data/images/yolov4/Mosic数据增强方法.png)\n\n`CmBN` 仅收集单个批次中的 `mini-batch` 之间的统计信息。\n\n![CmBN](../../data/images/yolov3/和其他backbone的比较结果.png)\n\n我们将 `SAM` 从 `spatial-wise attentation` 改为 `point-wise attention`，并将 `PAN` 的 `shortcut` 连接改为 `concatenation`（拼接），分别如图 5 和图 6 所示。\n\n![修改后的SAM和PAN](../../data/images/yolov4/修改后的SAM和PAN.png)\n\n#### 3.4 YOLOv4\n\n**`YOLOv4` 网络由以下部分组成**：\n\n- `Backbone`: `CSPDarknet53`\n- `Neck`: `SPP`, `PAN`\n- `Head`: `YOLOv3`\n\n**同时，`YOLO v4` 使用了**：\n\n- 用于 `backbone` 的 `BoF`：`CutMix` 和 `Mosaic`数据增强，`DropBlock`正则化，类标签平滑。\n- 用于 `backbone` 的 `BoS`：`Mish`激活，跨阶段部分连接（`CSP`），多输入加权残余连接（`MiWRC`）。\n- 用于检测器的 `BoF`：`CIoU` 损失，`CmBN`，`DropBlock` 正则化，`mosaic` 数据增强，自我对抗训练，消除网格敏感性，在单个 `ground-truth` 上使用多个 `anchor`，余弦退火调度器，最佳超参数，随机训练形状。\n- 用于检测器 `BoS`：`Mish` 激活，`SPP` 模块，`SAM` 模块，`PAN` 路径聚集块，`DIoU-NMS`。\n\n### 4，实验\n\n#### 4.1，实验设置\n\n略\n\n#### 4.2，对于分类器训练过程中不同特性的影响\n\n图 `7` 可视化了不同数据增强方法的效果。\n\n![不同数据增强方法效果的可视化](../../data/images/yolov4/不同数据增强方法效果的可视化.png)\n\n表 `2` 的实验结果告诉我们，`CutMix` 和 `Mosaic` 数据增强，类别标签平滑及 `Mish` 激活可以提高分类器的精度，尤其是 `Mish` 激活提升效果很明显。\n\n![Mish和其他一些方法对分类器精度的影响](../../data/images/yolov4/Mish和其他一些方法对分类器精度的影响.png)\n\n#### 4.3，对于检测器训练过程中不同特性的影响\n\n- $S$: `Eliminate grid sensitivit`。原来的 $b_x = \\sigma(t_x) + c_x$，因为 `sigmoid` 函数值域范围是 $(0,1)$ 而不是 $[0,1]$，所以 $b_x$ 不能取到 `grid` 的边界位置。为了解决这个问题，作者提出将 $\\sigma(t_x)$ 乘以一个超过 $1$ 的系数，如 $b_x = 1.1\\sigma(t_x) + c_x$，$b_y$ 的公式类似。\n- $IT$：之前的 `YOLOv3` 是 $1$ 个 `anchor` 负责一个 `GT`，现在 `YOLOv4` 改用多个 `anchor` 负责一个 `GT`。对于 `GT` 来说，只要 $IoU(anchor_i, GT_j) > IoU -threshold$ ，就让 $anchor_i$ 去负责 $GT_j$。\n- $CIoU$：使用了 `GIoU，CIoU，DIoU，MSE` 这些误差算法来实现边框回归，验证出 `CIoU` 损失效果最好。\n- 略\n\n![不同方法对检测器性能的影响](../../data/images/yolov4/不同方法对检测器性能的影响.png)\n\n同时实验证明，当使用 `SPP`，`PAN` 和 `SAM` 时，检测器将获得最佳性能。\n\n#### 4.4，不同骨干和预训练权重对检测器训练的影响\n\n综合各种改进后的骨干网络对比实验，发现 `CSPDarknet53` 比 `CSPResNext` 模型显示出提高检测器精度的更大能力。\n\n![不同骨干网络对检测器性能的影响比较](../../data/images/yolov4/不同骨干网络对检测器性能的影响比较.png)\n\n#### 4.5，不同小批量的大小对检测器训练的影响\n\n实验证明，在使用了 `BoF` 和 `BoS` 训练策略后，小批量大小（`mini-batch sizes`）对检测器的性能几乎没有影响。实验结果对比表格就不放了，可以看原论文。\n\n### 5，结果\n\n与其他 `state-of-the-art` 目标检测算法相比，`YOLOv4` 在速度和准确性上都表现出了最优。详细的比较实验结果参考论文的图 `8`、表 `8`和表 `9`。\n\n### 6，YOLOv4 主要改进点\n\n> 例举出一些我认为比较关键且值得重点学习的改进点。\n\n#### 6.1，Backbone 改进\n\n> 后续所有网络的结构图来都源于江大白公众号，之后不再一一注明结构图来源。\n\n`Yolov4` 的整体结构可以拆分成四大板块，结构图如下图所示。\n\n![YOLOv4架构图2](../../data/images/yolov4/yolov4_2(1).png)\n\n`YOLOv4` 的五个基本组件如下：\n\n1. **CBM**：`Yolov4` 网络结构中的最小组件，由 `Conv+Bn+Mish` 激活函数三者组成。\n2. **CBL**：由 `Conv+Bn+Leaky_relu` 激活函数三者组成。\n3. **Res unit**：借鉴 `Resnet` 网络中的残差结构思想，让网络可以构建的更深，和 `ResNet` 的 `basic block` 由两个 `CBL（ReLU）`组成不同，这里的 `Resunit` 由 `2` 个 `CBM` 组成。\n4. **CSPX**：借鉴 `CSPNet` 网络结构，由三个卷积层和 `X` 个 `Res unint` 模块 `Concate` 组成。\n5. **SPP**：采用 `1×1，5×5，9×9，13×13` 的最大池化的方式，进行多尺度融合。\n\n其他基础操作：\n\n1. **Concat**：张量拼接，会扩充维度。\n2. **add**：逐元素相加操作，不改变维度（`element-wise add`）。\n\n因为每个 `CSPX` 模块有 $5+2\\ast X$ 个卷积层，因此整个 `backbone` 中共有 $1 + (5+2\\times 1) + (5+2\\times 2) + (5+2\\times 8) + (5+2\\times 8) + (5+2\\times 4) = 72$ 个卷积层\n> 这里卷积层的数目 `72` 虽然不等同于 `YOLOv3` 中 `53`，但是 `backbone` 依然是由 [1、2、8、8、4] 个卷积模块组成的，只是这里的 `YOLOv4` 中的卷积模块替换为了 `CSPX` 卷积模块，猜想是这个原因所以 `YOLOv4` 的作者这里依然用来 `Darknet53` 命名后缀。\n\n##### 6.1.1，CSPDarknet53\n\n`YOLOv4` 使用 `CSPDarknet53` 作为 `backbone`，它是在 `YOLOv3` 的骨干网络 `Darknet53` 基础上，同时借鉴 `2019` 年的 `CSPNet` 网络，所产生的新 `backbone`。\n\n`CSPDarknet53` 包含 `5` 个 `CSP` 模块，`CSP` 中残差单元的数量依次是 $[1, 2,8,8,4]$，这点和 `Darknet53` 类似。每个 `CSP` 模块最前面的卷积核的大小都是 $3\\times 3$，`stride=2`，因此可以起到下采样的作用（特征图大小缩小一倍）。因为 `backbone` 总共有 `5` 个 `CSP`模块，而输入图像是 $608\\times 608$，所以特征图大小变化是：`608->304->152->76->38->19`，即经过 `bckbone` 网络后得到 $19\\times 19$ 大小的特征图。`CSPDarknet53` 网络结构图如下图所示。\n\n![CSPDarknet53](../../data/images/yolov4/CSPDarknet53.jpg)\n\n> `CSPNet` 作者认为，`MobiletNet`、`ShuffleNet` 系列模型是专门为移动端（`CPU`）平台上设计的，它们所采用的深度可分离卷积技术（`DW+PW Convolution`）并不兼容用于边缘计算的 `ASIC` 芯片。\n\n`CSP` 结构是一种思想，它和ResNet、DenseNet 类似，可以看作是 DenseNet 的升级版，它将 `feature map` 拆成两个部分，一部分进行卷积操作，另一部分和上一部分卷积操作的结果进行`concate`。\n\n`CSP` 结构主要解决了四个问题：\n\n1. 增强 CNN 的学习能力，能够在轻量化的同时保持着准确性；\n2. 降低计算成本；\n3. 降低内存开销。CSPNet 改进了密集块和过渡层的信息流，优化了梯度反向传播的路径，提升了网络的学习能力，同时在处理速度和内存方面提升了不少。\n4. 能很好的和 `ResNet`、`DarkNet` 等网络嵌入在一起，增加精度的同时减少计算量和降低内存成本。\n\n##### 6.1.2，Mish 激活\n\n在 `YOLOv4` 中使用 `Mish` 函数的原因是它的**低成本和它的平滑、非单调、无上界、有下界**等特点，在表 `2` 的对比实验结果中，和其他常用激活函数如 `ReLU`、`Swish` 相比，分类器的精度更好。\n\n`Mish` 激活函数是光滑的非单调激活函数，定义如下：\n\n$$\nMish(x) = x\\cdot tanh(In(1 + e^x)) \\\\\\\\\nSwish(x) = x\\cdot sigmoid(x) \\\\\\\\\n$$\n\n`Mish` 函数曲线图和 `Swish` 类似，如下图所示。\n\n![Mish函数曲线图](../../data/images/yolov4/Mish函数曲线图.png)\n\n值得注意的是 `Yolov4` 的 `Backbone` 中的激活函数都使用了`Mish` 激活，但后面的 `neck + head` 网络则还是使用`leaky_relu` 函数。\n\n$$\nLeaky \\ ReLU(x) =\n\\begin{cases}\nx, & x > 0 \\\\\\\\\n\\lambda x, & x \\leq 0\n\\end{cases}\n$$\n\n#### 6.1.3，Dropblock\n\n`Yolov4` 中使用的 `Dropblock` ，其实和常见网络中的 `Dropout` 功能类似，也是缓解过拟合的一种正则化方式。\n> 传统 `dropout` 功能是随机删除减少神经元的数量，使网络变得更简单（缓解过拟合）。\n\n#### 6.2，Neck 网络改进\n\n在目标检测领域中，为了更好的融合 `low-level` 和 `high-level` 特征，通常会在 `backbone` 和 `head` 网络之间插入一些网络层，这个中间部分称为 `neck` 网络，典型的有 `FPN` 结构。\n\n`YOLOv4` 的 `neck` 结构采用了 `SPP` 模块 和 `FPN+PAN` 结构。\n\n先看看 `YOLOv3` 的 `neck` 网络的立体图是什么样的，如下图所示。\n\n![YOLOv3的neck结构立体图](../../data/images/yolov4/YOLOv3的neck结构立体图.jpg)\n\n`FPN` 是自顶向下的，将高层的特征信息经过上采样后和低层的特征信息进行传递融合，从而得到进行预测的特征图 ①②③。\n\n再看下图 `YOLOv4` 的 `Neck` 网络的立体图像，可以更清楚的理解 `neck` 是如何通过 `FPN+PAN` 结构进行融合的。\n\n![FPN+PAN](../../data/images/yolov4/FPN+PAN.jpg)\n\n`FPN` 层自顶向下传达强语义特征，而特征金字塔则自底向上传达强定位特征，两两联手，从不同的主干层对不同的检测层进行参数聚合，这种正向反向同时结合的操作确实 `6` 啊。\n\n值得注意的是，`Yolov3` 的 `FPN` 层输出的三个大小不一的特征图①②③直接进行预测。但`Yolov4` 输出特征图的预测是使用 `FPN` 层从最后的一个 `76*76` 特征图 ① 和而经过两次`PAN` 结构的特征图 ② 和 ③ 。\n\n另外一点是，原本的 `PANet` 网络的 `PAN` 结构中，两个特征图结合是采用 `shortcut + element-wise` 操作，而 `Yolov4` 中则采用 `concat（route）`操作，特征图融合后的尺寸会变化。原本 `PAN` 和修改后的 `PAN` 结构对比图如下图所示。\n\n![原始的PAN和YOLOv4中的PAN的不同](../../data/images/yolov4/原始的PAN和YOLOv4中的PAN的不同.png)\n\n#### 6.3，预测的改进\n\n##### 6.3.1，使用CIoU Loss\n\n`Bounding Box Regeression` 的 `Loss` 近些年的发展过程是：Smooth L1 Loss-> IoU Loss（2016）-> GIoU Loss（2019）-> DIoU Loss（2020）->CIoU Loss（2020）\n\n##### 6.3.2，使用DIoU_NMS\n\n#### 6.4，输入端改进\n\n##### 6.4.1，Mosaic 数据增强\n\n`YOLOv4` 原创的 `Mosaic` 数据增强方法是基于 `2019` 年提出的 `CutMix` 数据增强方法做的优化。`CutMix` 只对两张图片进行拼接，而 `Mosaic` 更激进，采用 `4` 张图片，在各自随机缩放、裁剪和排布后进行拼接。\n\n![CutMix->Mosaic数据增强](../../data/images/yolov4/Mosaic数据增强.jpg)\n\n在目标检测器训练过程中，小目标的 `AP` 一般比中目标和大目标低很多。而 `COCO` 数据集中也包含大量的小目标，但比较麻烦的是小目标的分布并不均匀。在整体的数据集中，它们的占比并不平衡。\n\n![COCO数据集小中大目标分布情况](../../data/images/yolov4/COCO数据集小中大目标分布情况.jpg)\n\n如上表所示，在 `COCO` 数据集中，小目标占比达到 `41.4%`，数量比中目标和大目标要大得多，但是在所有的训练集图片中，只有 `52.3%` 的图片有小目标，即**小物体数量很多、但分布非常不均匀**，而中目标和大目标的分布相对来说更加均匀一些。\n> 少部分图片却包含了大量的小目标。\n\n针对这种状况，`Yolov4` 的作者采用了 `Mosaic` 数据增强的方式。器主要有几个优点：\n\n- 丰富数据集：随机使用 `4` 张图片，随机缩放，再随机分布进行拼接，大大丰富了检测数据集，特别是随机缩放增加了很多小目标，让网络的鲁棒性更好。\n- 减少训练所需 `GPU` 数量： `Mosaic` 增强训练时，可以直接计算 `4` 张图片的数据，使得 `Mini-batch` 大小并不需要很大，一个 `GPU` 就可以训练出较好的模型。\n\n## 五，YOLOv5\n\n`YOLOv5` 仅在 `YOLOv4` 发表一个月之后就公布了，这导致很多人对 `YOLOv5` 的命名有所质疑，因为相比于它的前代 `YOLOv4`，它在理论上并没有明显的差异，虽然集成了最近的很多新的创新，但是这些集成点又和 `YOLOv4` 类似。我个人觉得之所以出现这种命名冲突应该是发布的时候出现了 “撞车”，毕竟 `YOLOv4` 珠玉在前（早一个月），`YOLOv5` 也只能命名为 `5` 了。但是，我依然认为 `YOLOv5` 和 `YOLOv4` 是不同的，至少在工程上是不同的，它的代码是用 `Python`(`Pytorch`) 写的，与 `YOLOv4` 的 [C代码](https://github.com/AlexeyAB/darknet) （基于 `darknet` 框架）有所不同，所以代码更简单、易懂，也更容易传播。\n\n另外，值得一提的是，`YOLOv4` 中提出的关键的 `Mosaic` 数据增强方法，作者之一就是 `YOLOv5` 的作者 `Glenn Jocher`。同时，`YOLOv5` 没有发表任何论文，只是在 `github` 上开源了[代码](https://github.com/ultralytics/yolov5)。\n\n### 5.1，网络架构\n\n通过解析代码仓库中的 `.yaml` 文件中的结构代码，`YOLOv5` 模型可以概括为以下几个部分：\n\n- `Backbone`: `Focus structure`, `CSP network`\n- `Neck`: `SPP block`, `PANet`\n- `Head`: `YOLOv3 head using GIoU-loss`\n\n### 5.2，创新点\n\n#### 5.2.1，自适应anchor\n\n在训练模型时，`YOLOv5` 会自己学习数据集中的最佳 `anchor boxes`，而不再需要先离线运行 `K-means` 算法聚类得到 `k` 个 `anchor box` 并修改 `head` 网络参数。总的来说，`YOLOv5` 流程简单且自动化了。\n\n#### 5.2.2， 自适应图片缩放\n\n在常用的目标检测算法中，不同的图片长宽都不相同，因此常用的方式是将原始图片统一缩放到一个标准尺寸，再送入检测网络中。\n\n#### 5.2.3，Focus结构\n\nFocus 结构可以简单理解为将 $W\\times H$ 大小的输入图片 4 个像素分别取 1 个（类似于邻近下采样）形成新的图片，这样 1 个通道的输入图片会被划分成 4 个通道，每个通道对应的 WH 尺寸大小都为原来的 1/2，并将这些通道组合在一起。这样就实现了像素信息不丢失的情况下，提高通道数（通道数对计算量影响更小），减少输入图像尺寸，从而大大减少模型计算量。\n\n以 Yolov5s 的结构为例，原始 640x640x3 的图像输入 Focus 结构，采用切片操作，先变成 320×320×12 的特征图，再经过一次 32 个卷积核的卷积操作，最终变成 320×320×32 的特征图。\n\n![focus结构示例](../../data/images/yolov5/focus结构示例.png)\n\n### 5.3，四种网络结构\n\nYOLOv5 通过在网络结构问价 `yaml` 中设置不同的 `depth_multiple` 和 `width_multiple` 参数，来创建大小不同的四种 `YOLOv5` 模型：Yolv5s、Yolv5m、Yolv5l、Yolv5x。\n### 5.4，实验结果\n\n各个版本的 `YOLOv5` 在 `COCO` 数据集上和 `V100 GPU` 平台上的模型精度和速度实验结果曲线如下所示。\n\n![yolov5实验结果](../../data/images/yolov5/yolov5对比实验曲线图.png)\n\n## 参考资料\n\n+ [你一定从未看过如此通俗易懂的YOLO系列(从v1到v5)模型解读 (上)](https://zhuanlan.zhihu.com/p/183261974?utm_source=wechat_session&utm_medium=social&utm_oi=737449911926140928)\n+ [你一定从未看过如此通俗易懂的YOLO系列(从v1到v5)模型解读 (中)](https://zhuanlan.zhihu.com/p/183781646?utm_source=wechat_session&utm_medium=social&utm_oi=737449911926140928)\n+ [目标检测|YOLO原理与实现](https://zhuanlan.zhihu.com/p/32525231)\n+ [YOLO论文翻译——中英文对照](http://noahsnail.com/2017/08/02/2017-08-02-YOLO%E8%AE%BA%E6%96%87%E7%BF%BB%E8%AF%91%E2%80%94%E2%80%94%E4%B8%AD%E8%8B%B1%E6%96%87%E5%AF%B9%E7%85%A7/)\n+ [Training Object Detection (YOLOv2) from scratch using Cyclic Learning Rates](https://towardsdatascience.com/training-object-detection-yolov2-from-scratch-using-cyclic-learning-rates-b3364f7e4755)\n+ [目标检测|YOLOv2原理与实现(附YOLOv3)](https://zhuanlan.zhihu.com/p/35325884)\n+ [YOLO v1/v2/v3 论文](http://xxx.itp.ac.cn/pdf/1804.02767.pdf)\n+ https://github.com/BobLiu20/YOLOv3_PyTorch/blob/master/nets/yolo_loss.py\n+ https://github.com/Peterisfar/YOLOV3/blob/03a834f88d57f6cf4c5016a1365d631e8bbbacea/utils/datasets.py#L88\n+ [深入浅出Yolo系列之Yolov3&Yolov4&Yolov5&Yolox核心基础知识完整讲解](https://zhuanlan.zhihu.com/p/143747206)\n+ `EVOLUTION OF YOLO ALGORITHM AND YOLOV5: THE STATE-OF-THE-ART OBJECT DETECTION ALGORITHM`"
  },
  {
    "path": "5-computer_vision/2D目标检测/8-Scaled-YOLOv4论文解读.md",
    "content": "- [一，Scaled YOLOv4](#一scaled-yolov4)\n- [摘要](#摘要)\n- [1，介绍](#1介绍)\n- [2，相关工作](#2相关工作)\n  - [2.1，模型缩放](#21模型缩放)\n- [3，模型缩放原则](#3模型缩放原则)\n  - [3.1，模型缩放的常规原则](#31模型缩放的常规原则)\n  - [3.2，为低端设备缩放的tiny模型](#32为低端设备缩放的tiny模型)\n  - [3.3，为高端设备缩放的Large模型](#33为高端设备缩放的large模型)\n- [4，Scaled-YOLOv4](#4scaled-yolov4)\n  - [4.1，CSP-ized YOLOv4](#41csp-ized-yolov4)\n  - [4.2，YOLOv4-tiny](#42yolov4-tiny)\n  - [4.3，YOLOv4-large](#43yolov4-large)\n- [5，实验](#5实验)\n- [总结](#总结)\n- [Reference](#reference)\n- [参考资料](#参考资料)\n\n## 一，Scaled YOLOv4\n\n> `Scaled YOLOv4` 的二作就是 `YOLOv4` 的作者 `Alexey Bochkovskiy`。\n\n## 摘要\n\n作者提出了一种网络缩放方法，不仅可以修改深度、宽度、分辨率，还可以修改网络的结构。\n\n## 1，介绍\n\n实验结果表明，基于 `CSP` 方法的 `YOLOv4` 目标检测模型在保持最优速度和准确率的前提下，同时也具有向上/向下可伸缩性，可用于不同大小的网络。由此，作者提出了一种网络缩放方法，它不仅改变深度、宽度、分辨率，而且还改变网络的结构。\n\n**主要工作**。`Scaled YOLOv4` 的主要工作如下：\n\n- 设计了一种针对小模型的强大的模型缩放方法，系统地平衡了浅层 `CNN` 的计算代价和存储带宽;\n- 设计一种简单有效的大型目标检测器**缩放策略**;\n- 分析各模型缩放因子之间的关系，基于最优组划分进行模型缩放;\n- 实验证实了 `FPN` 结构本质上是一种 `once-for-all` 结构;\n- 利用上述方法研制了 `YOLOv4-tiny` 和 `YOLO4v4-large` 模型。\n\n以往模型缩放，如 `EfficientDet` 无非是首先选择网络基础模块，它往往又好又快，然后针对影响目标检测的重要参数如：网络宽度 $w$、深度 $d$、输入图像分辨率`size` 等进行（满足一定条件下按照一定规律）调参，或者 `NAS` 自动调参。\n\n## 2，相关工作\n\n### 2.1，模型缩放\n\n传统的模型缩放是指改变模型的**深度**，如 `VGG` 变体，以及后边可以训练**更**深层的 `ResNet` 网络等；后面 `agoruyko` 等人开始考虑模型的宽度，通过改变卷积层卷积核的数量来实现模型缩放，并设计了 `Wide ResNet`[43]，同样的精度下，它的参数量尽管比原始 `ResNet` 多，但是推理速度却更快。随后的 `DenseNet` 和 `ResNeXt` 也设计了一个复合缩放版本，将深度和宽度都考虑在内。\n> 何凯明等人提出的 `ResNet` 网络解决了随着深度增加带来的网络退化问题。\n\n## 3，模型缩放原则\n\n### 3.1，模型缩放的常规原则\n\n> 这段内容，原作者的表达不够严谨，计算过程也没有细节，所以我不再针对原文进行一一翻译，而是在原文的基础上，给出更清晰的表达和一些计算细节。\n\n这里，我们得先知道对一个卷积神经网络来说，其模型一般是由 `conv stage`、`conv block`、`conv layer` 组成的。我以 `ResNet50` 为例进行分析，大家就能明白了。`ResNet50` 的卷积过程分成 `4` 个 `stage`，分别对应的卷积 `blocks` 数目是 $[3,4,6,3]$，卷积 `block` 是 `bottleneck` 残差单元，`bottleneck` 残差单元又是 $1\\times 1$、$3\\times 3$ 和 $1\\times 1$ 这样 `3` 个卷积层组成的，所以 `ResNet50` 总共的卷积层数目为：$3\\times 3 + 4\\times 3+ 6\\times 3 + 3\\times 3 = 48$，再加上第一层的卷积和最后一层的分类层（全连接层），总共是 `50` 层，所以命名为 `ResNet50`。`ResNet` 模型的组成和结构参数表如下图所示。\n> 大部分 `backbone` 都是分成 `4` 个 `stage`。\n\n![resnet网络参数表](../../data/images/scaled-yolov4/resnet网络参数表.png)\n\n对一个基础通道数是 $b$ 的卷积模块（`conv block`），总共有 $k$ 个这样的模块的 `CNN` 网络来说，其计算代价是这样的。如 `ResNet` 的总的卷积层的计算量为 $k\\ast [conv(1\\times 1,b/4)\\rightarrow conv(3\\times 3,b/4)\\rightarrow conv(1\\times 1,b)]$；`ResNeXt` 的总的卷积层的计算量为 $k\\ast [conv(1\\times 1,b/2)\\rightarrow gconv(3\\times 3/32, b/2)\\rightarrow conv(1\\times 1, b)]$；`Darknet` 网络总的计算量为 $k\\ast [conv(1\\times 1,b/2)\\rightarrow conv(3\\times 3, b)]$。假设可用于调整图像大小、层数和通道数的缩放因子分别为 $\\alpha$、$\\beta$ 和 $\\gamma$。当调整因子变化时，可得出它们和 `FLOPs` 的关系如下表所示。\n\n![resnet-resnext-darknet网络计算量和网络深度宽度及输入图像分辨率的关系](../../data/images/scaled-yolov4/resnet-resnext-darknet网络计算量和网络深度宽度及输入图像分辨率的关系.png)\n\n这里以 `Res layer` 为例，进行计算量分析。首先上表的 $r$ 应该是指每个 `stage` 中间的残差单元的计算量，而且还是 `bottleneck` 残差单元，因为只有 `stage` 中间的 `bottleneck conv block` 的第一个 $1\\times 1$ 卷积层的输入通道数才是输出通道数的 `4` 倍，只有这种情况算出来的计算量 $r$ 才符合表 `1` 的结论。\n\n卷积层 `FLOPs` 的计算公式如下，这里把乘加当作一次计算，公式理解请参考我之前写的 [文章](../../7-model_deploy/B-神经网络模型复杂度分析.md \"文章\")。\n\n$FLOPs=(C_i\\times K^2)\\times H\\times W\\times C_o$\n\n对于上面说的那个特殊的 `bottleneck conv block` 来说，卷积过程特征图大小没有发生变化，假设特征图大小为 $wh$，所以 `bolck` 的 `FLOPs` 为：\n\n$$\\begin{align*}\nr1 &=  (b \\times 1^2\\times \\frac{b}{4} + \\frac{b}{4} \\times 3^2\\times \\frac{b}{4} + \\frac{b}{4} \\times 1^2\\times b)\\times hw \\\\\\\\\n&= \\frac{17}{16}whb^2\n\\end{align*}$$\n\n这里值得注意的是，虽然各个 `conv block` 会略有不同，比如 每个 `conv stage` 的第一个 `conv block` 都会将特征图缩小一倍，但是其 `FLOPs` 和 $r1$ 是线性的关系，所以，对于有 $k$ 个 `conv block` 的 `ResNet` 来说，其总的计算量自然就可大概近似为 $17whkb^2/16$。`ResNeXt` 和 `Darknet` 卷积层的 `FLOPs` 计算过程类似，所以不再描述。\n\n由表 `1` 可以看出，**图像大小、深度和宽度都会导致计算代价的增加，它们分别成二次，线性，二次增长**。\n\n`Wang` 等人提出的 [CSPNet](../../5-deep_learning/轻量级网络论文解析/CSPNet%20论文详解.md \"CSPNet\") 可以应用于各种 `CNN` 架构，同时减少了参数和计算量。此外，它还提高了准确性，减少了推理时间。作者把它应用到 `ResNet, ResNeXt，DarkNet` 后，发现计算量的变化如表 `2` 所示。\n\n![csp和resnet等结合后FLOPs的变化](../../data/images/scaled-yolov4/csp和resnet等结合后FLOPs的变化.png)\n\n`CNN` 转换为 `CSPNet` 后，新的体系结构可以有效地减少 `ResNet`、`ResNeXt` 和 `Darknet` 的计算量（`FLOPs`），分别减少了 `23.5%`、`46.7%` 和 `50.0%`。因此，作者**使用 `CSP-ized` 模型作为执行模型缩放的最佳模型**。\n\n### 3.2，为低端设备缩放的tiny模型\n\n对于低端设备，设计模型的推理速度不仅受到计算量和模型大小的影响，更重要的是必须考虑外围硬件资源的限制。因此，在执行 `tiny` 模型缩放时，我们必须考虑以下因素：内存带宽、内存访问代价（`MACs`）和 `DRAM traffic`。为了考虑到以上因素，我们的设计必须遵循以下原则：\n\n1，**使计算复杂度少于 $O(whkb^2)$**。\n\n作者分析了高效利用参数的网络：`DenseNet` 和 `OSANet` 的计算量，分别为 $O(whgbk)$、$O(max(whbg, whkg^2))$。两者的计算复杂度阶数均小于 `ResNet` 系列的 $O(whkb^2)$。因此，我们基于 `OSANet` 设计 `tiny` 模型，因为它具有更小的计算复杂度。\n> 这里的 `OSANet` 其实是 [VoVNet](http://xxx.itp.ac.cn/pdf/1904.09730.pdf \"VoVNet\") 网络，专门为 `GPU` 平台设计的更高效的 `backbone` 网络架，其论文解读可参考我之前写的[文章](https://github.com/HarleysZhang/2021_algorithm_intern_information/blob/master/5-deep_learning/%E8%BD%BB%E9%87%8F%E7%BA%A7%E7%BD%91%E7%BB%9C%E8%AE%BA%E6%96%87%E8%A7%A3%E6%9E%90/VoVNet%E8%AE%BA%E6%96%87%E8%A7%A3%E8%AF%BB.md)。\n\n![OSA和Dense layer的计算复杂度](../../data/images/scaled-yolov4/OSA和Dense-layer的计算复杂度.png)\n\n2，**最小化/平衡 feature map 的大小**\n> 说实话，没看明白论文这段内容，这不是跟论文 `CSPNet` 一样的结论吗，即分割为通道数相等的两条路径。\n\n为了获得在计算速度方面的最佳平衡，我们提出了一个新概念：在`CSPOSANet` 的计算块之间执行**梯度截断**。如果我们将原来的 `CSPNet` 设计应用到 `DenseNet` 或 `ResNet` 架构上，由于这两种架构的第 $j$ 层输出是第 $1^{st}$ 层到第 $(j-1)^{th}$ 层输出的积分，我们必须将整个计算块作为一个整体来处理。由于 `OSANet` 的计算块属于 `PlainNet` 架构，从计算块的任意层制作 `CSPNet` 都可以达到梯度截断的效果。我们利用该特性对基层的 $b$ 通道和计算块（`computational block`）生成的 $kg$ 通道进行重新规划，**并将其分割为通道数相等的两条路径**，如表 `4` 所示。\n\n![Table4](../../data/images/scaled-yolov4/Table4.png)\n\n当通道数量为 $b + kg$ 时，如果要将这些通道分割成两条路径，最好将其分割成相等的两部分，即 $(b + kg)/2$。\n\n3，**在卷积后保持相同的通道数**\n\n评估低端设备的计算成本，必须考虑功耗，而影响功耗的最大因素是内存访问代价（$MAC$）。根据 `Shufflenetv2` 的推导证明，可知卷积层的输入输出通道数相等时，即 $C_{in} = C_{out}$ 时， $MAC$ 最小。\n\n4，**最小化卷积输入/输出(CIO)**\n\n`CIO` 是一个可以测量 `DRAM IO` 状态的指标。表 `5` 列出了 `OSA`、`CSP` 和我们设计的 `CSPOSANet` 的 `CIO`。当 $kg > \\frac {b}{2}$ 时，`CSPOSANet` 可以获得最佳的 `CIO`。\n\n![Table5](../../data/images/scaled-yolov4/Table5.png)\n\n### 3.3，为高端设备缩放的Large模型\n\n`feature pyramid network` (`FPN`)的架构告诉我们，更高的 `stage` 更适合预测大的物体。表 `7` 说明了感受野与几个参数之间的关系。\n\n![Table7](../../data/images/scaled-yolov4/Table7.png)\n\n从表 `7` 可以看出，宽度缩放可以独立操作。当输入图像尺寸增大时，要想对大对象有更好的预测效果，就必须增大网络的 `depth` 或 `stage` （一般每个 `stage` 都会降低特征图分辨率的一半）的数量。在表 `7` 中列出的参数中，$\\left \\{ size^{input}, \\#stage \\right \\}$ 的组合效果最好。因此，当执行缩放时，我们首先在 $\\left \\{ size^{input} \\right \\}$，`#stage` 上执行复合缩放，然后根据实时的环境，我们再分别进一步缩放深度 `depth` 和宽度 `width`。\n\n## 4，Scaled-YOLOv4\n\n### 4.1，CSP-ized YOLOv4\n\n`YOLOv4` 是为通用 `GPU` 上的实时目标检测而设计的。\n\n1，**Backbone**\n\n为了获得更好的速度/精度权衡，我们将第一个 `CSP` 阶段转换为原始的 `DarkNet` 的残差层。\n> 没能理解这段内容。\n\n2，**Neck**\n\n![CSP-YOLOv4的neck调整](../../data/images/scaled-yolov4/CSP-YOLOv4的neck调整.png)\n\n3，**SPP**\n\n### 4.2，YOLOv4-tiny\n\n`YOLOv4-tiny` 是为低端 `GPU` 设备设计的，设计将遵循 `3.2` 节中提到的原则。\n\n我们将使用 `PCB`（`partial in computational block`） 架构的 `CSPOSANet` 作为 `YOLOv4` `backbone`。我们设 $g = b/2$ 为增长率，最终使其增长到 $b/2 + kg = 2b$。通过计算，我们得到 $k = 3$。`YOLOv4` 的卷积块（`computational block`）结构如图 `3` 所示。对于每个阶段的通道数量和 `neck` 网络结构，我们采用 `YOLOv3-tiny` 一样的设计。\n\n![YOLOv4-tiny的计算块结构](../../data/images/scaled-yolov4/YOLOv4-tiny的计算块结构.png)\n\n### 4.3，YOLOv4-large\n\n专门为云端 `GPU` 设计的，主要目的就是为实现高精度的目标检测。我们设计了一个完全 `CSP` 化的模型 `YOLOv4-P5`，并将其扩展到 `YOLOv4-P6` 和 `YOLOv4-P7`。`Sacled-YOLOv4` `large` 版本的模型结构图，如下图所示。\n\n![sacled-yolov4-large版本模型结构图](../../data/images/scaled-yolov4/sacled-yolov4-large版本模型结构图.png)\n\n我们通过设计 $size^{input}$, `#stage` 来对 `backbone` 执行复合缩放。我们把每个 `stage` 的深度设置为 $2^{d_{s_{i}}}$。$d_s$ 范围为 $[1, 3, 15, 15, 7, 7, 7]$。与之前的 `ResNet` 的卷积划分为 `4` 个 `stage` 不同，这里最多划分为 `7` 个 `stage`（`YOLOv4-P7`）。\n\n## 5，实验\n\n与其他实时目标检测检测器进行比较，对比实验结果如表 `11` 所示。\n\n![与其他目标检测器相比的对比实验结果](../../data/images/scaled-yolov4/与其他目标检测器相比的对比实验结果.png)\n\n## 总结\n\n通篇论文看下来，感觉这篇论文最主要的贡献在于通过简单的理论分析和对比实验，验证了模型缩放的原则，进一步拓展了 `CSPNet` 方法，并基于此设计了一个全新的 `Scaled-YOLOv4`。个人感觉就是针对不同的 `GPU` 平台，可以根据作者分析出来的模型缩放理论且符合其他一些原则的情况下，通过选择不同的模型宽度和深度参数，让模型更深更宽。\n\n> `anchor-free` 的方法，如 `centernet` 是不需要复杂的后处理，如 `NMS`。`Backbone` 模型的宽度、深度、模块的瓶颈比（`bottleneck`）、输入图像分辨率等参数的关系。\n\n## Reference\n\n[43] Sergey Zagoruyko and Nikos Komodakis. Wide residualnet works. arXiv preprint arXiv:1605.07146, 2016.\n## 参考资料\n\n- [Scaled-YOLOv4: Scaling Cross Stage Partial Network](https://arxiv.org/abs/2011.08036 \"Scaled-YOLOv4: Scaling Cross Stage Partial Network\")\n- [Scaled-YOLOv4: Scaling Cross Stage Partial Network 论文翻译](https://blog.csdn.net/Q1u1NG/article/details/109765162 \"Scaled-YOLOv4: Scaling Cross Stage Partial Network 论文翻译\")"
  },
  {
    "path": "5-computer_vision/2D目标检测/simple_yolov2/encoder.py",
    "content": "'''Encode target locations and class labels.\nReference: https://github.com/kuangliu/pytorch-yolov2/blob/master/encoder.py\n'''\nimport math\nimport torch\n\nfrom utils import box_iou, box_nms, meshgrid, softmax\n\nclass DataEncoder:\n    def __init__(self):\n        self.anchors = [(1.3221,1.73145),(3.19275,4.00944),(5.05587,8.09892),(9.47112,4.84053),(11.2364,10.0071)]\n\n    def encode(self, boxes, labels, input_size):\n        '''Encode target bounding boxes and class labels into YOLOv2 format.\n        Args:\n          boxes: (tensor) bounding boxes of (xmin,ymin,xmax,ymax) in range [0,1], sized [#obj, 4].\n          labels: (tensor) object class labels, sized [#obj,].\n          input_size: (int) model input size.\n        Returns:\n          loc_targets: (tensor) encoded bounding boxes, sized [5,4,fmsize,fmsize].\n          cls_targets: (tensor) encoded class labels, sized [5,20,fmsize,fmsize].\n          box_targets: (tensor) truth boxes, sized [#obj,4].\n        '''\n        num_boxes = len(boxes)\n        # input_size -> fmsize\n        # 320->10, 352->11, 384->12, 416->13, ..., 608->19\n        fmsize = (input_size - 320) / 32 + 10\n        grid_size = input_size / fmsize\n\n        boxes *= input_size  # scale [0,1] -> [0,input_size]\n        bx = (boxes[:,0] + boxes[:,2]) * 0.5 / grid_size  # in [0,fmsize]\n        by = (boxes[:,1] + boxes[:,3]) * 0.5 / grid_size  # in [0,fmsize]\n        bw = (boxes[:,2] - boxes[:,0]) / grid_size        # in [0,fmsize]\n        bh = (boxes[:,3] - boxes[:,1]) / grid_size        # in [0,fmsize]\n\n        tx = bx - bx.floor()\n        ty = by - by.floor()\n\n        xy = meshgrid(fmsize, swap_dims=True) + 0.5  # grid center, [fmsize*fmsize,2]\n        wh = torch.Tensor(self.anchors)              # [5,2]\n\n        xy = xy.view(fmsize,fmsize,1,2).expand(fmsize,fmsize,5,2)\n        wh = wh.view(1,1,5,2).expand(fmsize,fmsize,5,2)\n        anchor_boxes = torch.cat([xy-wh/2, xy+wh/2], 3)  # [fmsize,fmsize,5,4]\n\n        ious = box_iou(anchor_boxes.view(-1,4), boxes/grid_size)  # [fmsize*fmsize*5,N]\n        ious = ious.view(fmsize,fmsize,5,num_boxes)               # [fmsize,fmsize,5,N]\n\n        loc_targets = torch.zeros(5,4,fmsize,fmsize)  # 5boxes * 4coords\n        cls_targets = torch.zeros(5,20,fmsize,fmsize)\n        for i in range(num_boxes):\n            cx = int(bx[i])\n            cy = int(by[i])\n            _, max_idx = ious[cy,cx,:,i].max(0)\n            j = max_idx[0]\n            cls_targets[j,labels[i],cy,cx] = 1\n\n            tw = bw[i] / self.anchors[j][0]\n            th = bh[i] / self.anchors[j][1]\n            loc_targets[j,:,cy,cx] = torch.Tensor([tx[i], ty[i], tw, th])\n        return loc_targets, cls_targets, boxes/grid_size\n\n    def decode(self, outputs, input_size):\n        '''Transform predicted loc/conf back to real bbox locations and class labels.\n        Args:\n          outputs: (tensor) model outputs, sized [1,125,13,13].\n          input_size: (int) model input size.\n        Returns:\n          boxes: (tensor) bbox locations, sized [#obj, 4].\n          labels: (tensor) class labels, sized [#obj,1].\n        '''\n        fmsize = outputs.size(2)\n        outputs = outputs.view(5,25,13,13)\n\n        loc_xy = outputs[:,:2,:,:]   # [5,2,13,13]\n        grid_xy = meshgrid(fmsize, swap_dims=True).view(fmsize,fmsize,2).permute(2,0,1)  # [2,13,13]\n        box_xy = loc_xy.sigmoid() + grid_xy.expand_as(loc_xy)  # [5,2,13,13]\n\n        loc_wh = outputs[:,2:4,:,:]  # [5,2,13,13]\n        anchor_wh = torch.Tensor(self.anchors).view(5,2,1,1).expand_as(loc_wh)  # [5,2,13,13]\n        box_wh = anchor_wh * loc_wh.exp()  # [5,2,13,13]\n\n        boxes = torch.cat([box_xy-box_wh/2, box_xy+box_wh/2], 1)  # [5,4,13,13]\n        boxes = boxes.permute(0,2,3,1).contiguous().view(-1,4)    # [845,4]\n\n        iou_preds = outputs[:,4,:,:].sigmoid()  # [5,13,13]\n        cls_preds = outputs[:,5:,:,:]  # [5,20,13,13]\n        cls_preds = cls_preds.permute(0,2,3,1).contiguous().view(-1,20)\n        cls_preds = softmax(cls_preds)  # [5*13*13,20]\n\n        score = cls_preds * iou_preds.view(-1).unsqueeze(1).expand_as(cls_preds)  # [5*13*13,20]\n        score = score.max(1)[0].view(-1)  # [5*13*13,]\n        print(iou_preds.max())\n        print(cls_preds.max())\n        print(score.max())\n\n        ids = (score>0.5).nonzero().squeeze()\n        keep = box_nms(boxes[ids], score[ids])\n        return boxes[ids][keep] / fmsize"
  },
  {
    "path": "5-computer_vision/2D目标检测/simple_yolov2/model.py",
    "content": "'''Darknet in PyTorch.'''\nimport torch\nimport torch.nn as nn\nimport torch.nn.init as init\nimport torch.nn.functional as F\n\nfrom torch.autograd import Variable\n\n\nclass Darknet(nn.Module):\n    # (64,1) means conv kernel size is 1, by default is 3.\n    cfg1 = [32, 'M', 64, 'M', 128, (64,1), 128, 'M', 256, (128,1), 256, 'M', 512, (256,1), 512, (256,1), 512]  # conv1 - conv13\n    cfg2 = ['M', 1024, (512,1), 1024, (512,1), 1024]  # conv14 - conv18\n\n    def __init__(self):\n        super(Darknet, self).__init__()\n        self.layer1 = self._make_layers(self.cfg1, in_planes=3)\n        self.layer2 = self._make_layers(self.cfg2, in_planes=512)\n\n        #### Add new layers\n        self.conv19 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)\n        self.bn19 = nn.BatchNorm2d(1024)\n        self.conv20 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)\n        self.bn20 = nn.BatchNorm2d(1024)\n        # Currently I removed the passthrough layer for simplicity\n        self.conv21 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)\n        self.bn21 = nn.BatchNorm2d(1024)\n        # Outputs: 5boxes * (4coordinates + 1confidence + 20classes)\n        self.conv22 = nn.Conv2d(1024, 5*(5+20), kernel_size=1, stride=1, padding=0)\n\n    def _make_layers(self, cfg, in_planes):\n        layers = []\n        for x in cfg:\n            if x == 'M':\n                layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]\n            else:\n                out_planes = x[0] if isinstance(x, tuple) else x\n                ksize = x[1] if isinstance(x, tuple) else 3\n                layers += [nn.Conv2d(in_planes, out_planes, kernel_size=ksize, padding=(ksize-1)//2),\n                           nn.BatchNorm2d(out_planes),\n                           nn.LeakyReLU(0.1, True)]\n                in_planes = out_planes\n        return nn.Sequential(*layers)\n\n    def forward(self, x):\n        out = self.layer1(x)\n        out = self.layer2(out)\n        out = F.leaky_relu(self.bn19(self.conv19(out)), 0.1)\n        out = F.leaky_relu(self.bn20(self.conv20(out)), 0.1)\n        out = F.leaky_relu(self.bn21(self.conv21(out)), 0.1)\n        out = self.conv22(out)\n        return out\n\n\ndef test():\n    net = Darknet()\n    y = net(Variable(torch.randn(1,3,416,416)))\n    print(y.size())  # [1,125,13,13]\n    \nif __name__ == \"__main__\":\n    test()"
  },
  {
    "path": "5-computer_vision/2D目标检测/yolov3_pytorch/darknet.py",
    "content": "import torch\nimport torch.nn as nn\nimport math\nfrom collections import OrderedDict\n\n__all__ = ['darknet21', 'darknet53']\n\n\nclass BasicBlock(nn.Module):\n    def __init__(self, inplanes, planes):\n        super(BasicBlock, self).__init__()\n        self.conv1 = nn.Conv2d(inplanes, planes[0], kernel_size=1,\n                               stride=1, padding=0, bias=False)\n        self.bn1 = nn.BatchNorm2d(planes[0])\n        self.relu1 = nn.LeakyReLU(0.1)\n        self.conv2 = nn.Conv2d(planes[0], planes[1], kernel_size=3,\n                               stride=1, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(planes[1])\n        self.relu2 = nn.LeakyReLU(0.1)\n\n    def forward(self, x):\n        residual = x\n\n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = self.relu1(out)\n\n        out = self.conv2(out)\n        out = self.bn2(out)\n        out = self.relu2(out)\n\n        out += residual\n        return out\n\n\nclass DarkNet(nn.Module):\n    def __init__(self, layers):\n        super(DarkNet, self).__init__()\n        self.inplanes = 32\n        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(self.inplanes)\n        self.relu1 = nn.LeakyReLU(0.1)\n\n        self.layer1 = self._make_layer([32, 64], layers[0])\n        self.layer2 = self._make_layer([64, 128], layers[1])\n        self.layer3 = self._make_layer([128, 256], layers[2])\n        self.layer4 = self._make_layer([256, 512], layers[3])\n        self.layer5 = self._make_layer([512, 1024], layers[4])\n\n        self.layers_out_filters = [64, 128, 256, 512, 1024]\n\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels\n                m.weight.data.normal_(0, math.sqrt(2. / n))\n            elif isinstance(m, nn.BatchNorm2d):\n                m.weight.data.fill_(1)\n                m.bias.data.zero_()\n\n    def _make_layer(self, planes, blocks):\n        layers = []\n        #  downsample\n        layers.append((\"ds_conv\", nn.Conv2d(self.inplanes, planes[1], kernel_size=3,\n                                stride=2, padding=1, bias=False)))\n        layers.append((\"ds_bn\", nn.BatchNorm2d(planes[1])))\n        layers.append((\"ds_relu\", nn.LeakyReLU(0.1)))\n        #  blocks\n        self.inplanes = planes[1]\n        for i in range(0, blocks):\n            layers.append((\"residual_{}\".format(i), BasicBlock(self.inplanes, planes)))\n        return nn.Sequential(OrderedDict(layers))\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = self.relu1(x)\n\n        x = self.layer1(x)\n        x = self.layer2(x)\n        out3 = self.layer3(x)\n        out4 = self.layer4(out3)\n        out5 = self.layer5(out4)\n\n        return out3, out4, out5\n\ndef darknet21(pretrained, **kwargs):\n    \"\"\"Constructs a darknet-21 model.\n    \"\"\"\n    model = DarkNet([1, 1, 2, 2, 1])\n    if pretrained:\n        if isinstance(pretrained, str):\n            model.load_state_dict(torch.load(pretrained))\n        else:\n            raise Exception(\"darknet request a pretrained path. got [{}]\".format(pretrained))\n    return model\n\ndef darknet53(pretrained, **kwargs):\n    \"\"\"Constructs a darknet-53 model.\n    \"\"\"\n    model = DarkNet([1, 2, 8, 8, 4])\n    if pretrained:\n        if isinstance(pretrained, str):\n            model.load_state_dict(torch.load(pretrained))\n        else:\n            raise Exception(\"darknet request a pretrained path. got [{}]\".format(pretrained))\n    return model\n\n"
  },
  {
    "path": "5-computer_vision/3D视觉算法/3D视觉算法初学概述.md",
    "content": "- [背景知识](#背景知识)\n  - [RGB-D相机](#rgb-d相机)\n- [一，基于3DMM的三维人脸重建技术概述](#一基于3dmm的三维人脸重建技术概述)\n  - [1.1，3D 人脸重建概述](#113d-人脸重建概述)\n  - [1.2，初版 3DMM](#12初版-3dmm)\n- [二，视觉SLAM算法基础概述](#二视觉slam算法基础概述)\n  - [2.1，视觉里程计](#21视觉里程计)\n  - [2.2，后端优化](#22后端优化)\n  - [2.3，回环检测](#23回环检测)\n  - [2.4，建图](#24建图)\n- [三，三维点云语义分割和实例分割综述](#三三维点云语义分割和实例分割综述)\n  - [3.1，三维数据的表示方法](#31三维数据的表示方法)\n    - [3.1.1，点云定义](#311点云定义)\n    - [3.1.2，点云的属性：](#312点云的属性)\n    - [3.1.3，点云获取](#313点云获取)\n    - [3.1.4，点云存储格式](#314点云存储格式)\n    - [3.1.5，三维点云的多种表示方法](#315三维点云的多种表示方法)\n  - [3.2，基于点云的分类和检测](#32基于点云的分类和检测)\n  - [3.3，基于点云的语义分割](#33基于点云的语义分割)\n    - [3.3.1，PointNet 网络](#331pointnet-网络)\n- [四，参考资料](#四参考资料)\n\n> 3D 视觉算法包括很多内容，此文仅当作入门了解些概念和知识概括。\n\n## 背景知识\n**3D图像描述有多种方法，常见的如下：**\n\n* **点云**\n* 网格（meshes）\n* 基于视图的描述\n* 深度图像（depth images）\n\n### RGB-D相机\n一般普通的相机拍出来的图像，其每个像素坐标（x, y）可以获得三种颜色属性（R, G, B）。但在 `RGB-D` 图像中，每个（x, y）坐标将对应于四个属性`（深度 D，R，G，B）`。\n\n## 一，基于3DMM的三维人脸重建技术概述\n### 1.1，3D 人脸重建概述\n3D 人脸重建定义：从一张或多张2D图像中重建出人脸的3D模型。数学表达式：\n\n$M = (S,T)$\n\n其中 `S` 表示人脸 3D 坐标形状向量（shape-vector），`T` 表示对应点的纹理信息向量（texture-vector）。\n\n3D 人脸重建算法分类：\n\n![image](images/od05t0HHEArHqmf5MAOoz_IKMoc7otdHf3oHXicv5cc.png)\n\n### 1.2，初版 3DMM\n1999 年论文 《A Morphable Model For The Synthesis Of 3D Faces》提出三维形变模型（3DMM），三维形变模型建立在**三维人脸数据库**的基础上，以**人脸形状和人脸纹理**统计为约束，同时考虑到了人脸的姿态和光照因素的影响，因而生成的三维人脸模型精度高。每一个人脸模型都由相应的形状向量 $S_i$ 和 $T_i$组成，其定义如下：\n\n$S_{newModel} = \\bar{S} + \\sum_{i=1}^{m-1} \\alpha_{i} s_{i}$\n\n$T_{newModel} = \\bar{T} + \\sum_{i=1}^{m-1} bata_{i} t_{i}$\n\n其中 $\\bar{S}$ 表示平均脸部形状模型，$s_i$表示 shape 的 PCA 部分（按照特征值降序排列的协方差阵的特征向量），$\\alpha_i$表示对应形状系数；纹理模型符号定义类似。通过调整形状、纹理系数系数可生成不同的人脸 3D 模型。\n\n## 二，视觉SLAM算法基础概述\n>  SLAM问题的本质:对运动主体自身和周围环境空间不确定性的估计。为了解决SLAM问题，我们需要状 态估计理论，把定位和建图的不确定性表达出来，然后采用滤波器或非线性优化，估计状态的均值和不确定性(方差)。\n\n`SLAM` 是Simultaneous Localization and Mapping的缩写，中文译作“同时定位与地图构建”。它是指搭载特定传感器（单目、双目、RGB-D相机、Lidar）的主体，在没有环境先验信息的情况下，**在运动过程中建立环境的模型**，同时估计自己的运动。如果这里的传感器主要为相机，那就称为“视觉SLAM”；如果传感器位激光，则为激光 SLAM，两者对比如下：\n\n![image](images/skVDD0wo9TdzMWwEoTzz0i9-ojaVHx_b43uCRbt_mOg.png)\n\n`SLAM` 主要解决**定位**和**地图构建**两个问题**。**视觉 SLAM 流程图如下：\n\n![image](images/zxnT-RzBEc6SF6_lNSuZMRN0ZoKcBZBIx5XCXUzyRuI.png)\n\n整个视觉 SLAM 流程包括以下几个步骤：\n\n1. **传感器信息读取**。视觉 SLAM 中主要指摄像头图像数据读取与预处理。\n2. **视觉里程计**（`Visual Odometry`, `VO`）。视觉里程计的任务是**估算相邻图像间相机的运动**，以及局部地图的样子。`VO` 又称为前端 (`Front End`)。\n3. **后端优化**（`Optimization`）。后端接受不同时刻视觉里程计测量的相机位姿，以及回环检测的信息，对它们进行优化，得到全局一致的轨迹和地图。由于接在 `VO` 之后，又称为后端(`Back End`)。\n4.  **回环检测 **（`Loop Closing`）。回环检测判断机器人是否到达过先前的位置。如果检测到回环，它会把信息提供给后端进行处理。\n5.  **建图**（`Mapping`）。它根据估计的轨迹，建立与任务要求对应的地图。\n\n### 2.1，视觉里程计\n视觉里程计 `VO` 目的是**通过相邻帧间的图像估计相机运动**，并恢复场景的空间结构。其中为了定量地估计相机运动，必须先了解相机与空间点的几何关系。同时，仅通过视觉里程计来估计轨迹，将不可避免地出现累积漂移 (`Accumulating Drift`)，即每次估计都有误差的情况下，先前时刻的误差将会传递到下一个时刻，导致经过一段时间累积之后，估计的轨迹将不再准确，如下图所示。\n\n![image](images/Mbb7-DFRD07EXmSOt8zoHyZ_bVtyPHRCkduDmGkUscA.png)\n\n### 2.2，后端优化\n概述性的说，后端优化主要目是为了处理 `SLAM` 过程中噪声的问题。后端优化要考虑的问题，就是如何从这些带有噪声的数据中估计整 个系统的状态，以及这个状态估计的不确定性有多大—这称为最大后 验概率估计(Maximum-a-Posteriori，MAP)。这里的状态既包括机器 人自身的轨迹，也包含地图。\n\n前端与后端的关系：前端给后端提供待优化的数据，以及这些数据的初始值。后端只关心数据的优化过程，不关系这些数据来源于什么传感器。因此在视觉 `SLAM` 中，前端和计算机视觉研究领域更为相关，比如图像的特征提取与匹配等，后端则主要是**滤波与非线性优化算法**。\n\n### 2.3，回环检测\n回环检测（又称闭环检测 Loop Closure Detection），主要目的是为了**解决位置估计随时间漂移的问题**。\n\n可以通过**图像相似性**来完成回环检测。在检测到回环之后，我们会把“A与B是同一个点”这样的信息告诉 后端优化算法。然后，后端根据这些新的信息，把轨迹和地图调整到符合回环检测结果的样子。这样，如果我们有充分而且正确的回环检测， 就可以消除累积误差，得到全局一致的轨迹和地图。\n\n### 2.4，建图\n建图(`Mapping`)是指构建地图的过程。这里的地图是对环境的描述，但这个描述并不是固定的，需要视SLAM 的应用而定。\n\n![image](images/-C4C_ZobpoY4ZxyqJ1wukVuzmtdQhQBz-HIoOvs0RGA.png)\n\n建图技术，根据用途的不同，可以分为稀疏重建和稠密重建，稀疏重建通常是重建一些图像特征点的三维坐标，**稀疏重建主要用于定位**。**稠密建图又称三维重建**，是对整个图像或图像中绝大部分像素进行重建，在导航、避障等方面起着举足轻重的作用。\n\n![image](images/rBXnIbC9qHXn-qf_UTwhpYQLxFxXpGqWtAU1f-neY5g.png)\n\n## 三，三维点云语义分割和实例分割综述\n### 3.1，三维数据的表示方法\n> 三维图像 = 普通的 RGB 三通道彩色图像 + Depth Map。\n\n三维数据有四种表示方法，分别是 point cloud（点云），Mesh（网格），Voxel（体素）以及 Multi-View（多角度图片）。由此也衍生出了对应的三维数据语义和示例分割的算法，但主要是针对 point cloud 的算法越来越多。三维数据集有 ShapeNet、S3DIS、ModelNet40 等。\n\n#### 3.1.1，点云定义\n点云简单来说就是一堆三维点的集合，必须包括各个点的三维坐标信息，其他信息比如各个点的法向量、颜色、分类值、强度值、时间等均是可选。\n\n点云在组成特点上分为两种，一种是有序点云，一种是无序点云。\n\n* **有序点云**：一般由深度图还原的点云，有序点云按照图方阵一行一行的，从左上角到右下角排列，当然其中有一些无效点因为。有序点云按顺序排列，可以很容易的找到它的相邻点信息。有序点云在某些处理的时候还是很便利的，但是很多情况下是无法获取有序点云的。\n* **无序点云**：无序点云就是其中的点的集合，点排列之间没有任何顺序，点的顺序交换后没有任何影响。是比较普遍的点云形式，有序点云也可看做无序点云来处理。\n\n#### 3.1.2，点云的属性：\n* 空间分辨率、点位精度、表面法向量等。\n* 点云可以表达物体的空间轮廓和具体位置，我们能看到街道、房屋的形状，物体距离摄像机的距离也是可知的；其次，点云本身和视角无关，可以任意旋转，从不同角度和方向观察一个点云，而且不同的点云只要在同一个坐标系下就可以直接融合。\n\n#### 3.1.3，点云获取\n点云一般需要通过三维成像传感器获得，比如**双目相机、RGB-D相机和 LiDAR激光传感器**。\n\n![image](images/8Ob5nzMaiIRZLVVECyi3oN-Z8XhfYzhAKhG5Xqc9Smk.png)\n\n根据激光测量原理得到的点云，包括三维坐标（XYZ）和激光反射强度（Intensity），强度信息与目标的表面材质、粗糙度、入射角方向以及仪器的发射能量、激光波长有关。根据摄影测量原理得到的点云，包括三维坐标（XYZ）和颜色信息（RGB）。结合激光测量和摄影测量原理得到点云，包括三维坐标（`XYZ`）、激光反射强度（`Intensity`）和颜色信息（`RGB`）。\n\n#### 3.1.4，点云存储格式\n点云的文件格式可以有很多种，包括 .xyz，npy，ply，obj，off 等（mesh 可以通过泊松采样等方式转化成点云）。对于单个点云，如果你使用np.loadtxt得到的实际上就是一个维度为  的张量，num\\_channels一般为 3，表示点云的三维坐标。\n\n* **pts** 点云文件格式是最简便的点云格式，直接按 `XYZ` 顺序存储点云数据， 可以是整型或者浮点型。\n\n![image](images/Pdm7Tzj8dFVqucN8eY6ndAejR3PnsKJ3b3U0NYQy5uA.png)\n\n* **LAS** 是激光雷达数据（LiDAR），存储格式比 pts 复杂，旨在提供一种开放的格式标准，允许不同的硬件和软件提供商输出可互操作的统一格式。LAS 格式点云截图，其中 C：class(所属类)，F：flight(航线号)，T：time(GPS 时间)，I：intensity(回波强度)，R：return(第几次回波)，N：number of return(回波次数)，A：scan angle(扫描角)，RGB：red green blue(RGB 颜色值)。\n\n![image](images/-pmmwckHPeNaLaysLDujqcFl5NHZB6vVD67sBKhxNOQ.png)\n\n* **.xyz** 一种文本格式，前面 3 个数字表示点坐标，后面 3 个数字是点的法向量，数字间以空格分隔。\n\n![image](images/2Hffp2Ah2aZCSjzTo7lZwi3pPaZ9MVB4szvGHwGUzxM.png)\n\n* **.pcap** 是一种通用的数据流格式，现在流行的 Velodyne 公司出品的激光雷达默认采集数据文件格式。它是一种二进制文件\n\n![image](images/b-KHrP8Z5byufrC3B4PHVrhsgdMiVhaUJ0tqKY59Lyk.png)\n\n#### 3.1.5，三维点云的多种表示方法\n三维点云除了原始点云表示还要网格 (Mesh) 表示和体素表示，如下图所示：\n\n![image](images/dIFCzrqGsBuUHpjsPHHuTxo9qKmLudmIMx0U_FONin8.png)\n\n### 3.2，基于点云的分类和检测\n**背景：**相比于图像数据，点云不直接包含空间结构，因此点云的深度模型必须解决三个主要问题:\n\n1. 如何从稀疏的点云找到高信息密度的表示。\n2. 如何构建一个网络满足必要的限制如 size-variance 和 permutation-invariance。\n3. 如何以较低的时间和计算资源消耗处理大量数据。\n\n对点云的分类通常称为三维形状分类。与图像分类模型相似，三维形状分类模型通常是先通过聚合编码器生成全局嵌入，然后将嵌入通过几个完全连通的层来获得最终结果。基于点云聚合方法，分类模型大致可分为两类: **基于投影的方法**和**基于点的方法。**\n\n### 3.3，基于点云的语义分割\n基于点云的语义分割方法大致可分为**基于投影的方法和基于点的方法。**\n\n#### 3.3.1，PointNet 网络\nPointNet 是第一个可以直接处理原始三维点云的深度神经网络，简单来说 `PointNet` 所作的事情就是对点云做特征学习，并将学习到的特征去做不同的应用：分类（shape-wise feature）、分割（point-wise feature）等。\n\n无论是分类还是分割，本质上都还是分类任务，只是粒度不同罢了。因此损失函数 `loss` 一定有有监督分类任务中常用的交叉熵 loss，另外 loss 还有之前 `alignment network`（用于实现网络对于仿射变换、刚体变换等变换的无关性）的约束 loss，也就是上面的 `mat_diff_loss` 。\n\nPointNet 网络结构如下所示：\n\n![image](images/J0OEbxtK5vPs73_KRBIRLbSue5Zu1VBkYCCZkN0zOSc.png)\n\n其大致的运算流程如下（来自[【3D视觉】PointNet和PointNet++](https://zhuanlan.zhihu.com/p/336496973)）：\n\n1. 输入为一帧的全部点云数据的集合，表示为一个 nx3 的 2d tensor，其中 n 代表点云数量，3 对应 xyz 坐标。\n2. 输入数据先通过和一个 `T-Net`**学习到的转换矩阵**相乘来对齐，保证了模型的对特定空间转换的不变性。\n3. 通过多次 mlp 对各点云数据进行特征提取后，再用一个 T-Net 对特征进行对齐。\n4. 在特征的各个维度上执行 **maxpooling **操作来得到最终的**全局特征**。\n5. 对分类任务，将全局特征通过 mlp 来预测最后的分类分数。\n6. 对分割任务，将全局特征和之前学习到的各点云的局部特征进行串联，再通过 mlp 得到每个数据点的分类结果。\n\n分割任务**针对于每一个点做分类**，在下面的图中，把全局的特征复制成 `n` 份然后与之前的 `64` 维特征进行拼接，然后接着做一个 `mlp`，最后的输出 `nxm` 就是每一个点的分类结果。\n\n![image](images/5zz0t-vreeXLIjFuy3d0_DlB0gCBxn1zXlFUBcZxt_E.png)\n\n## 四，参考资料\n1. [细嚼慢咽读论文：PointNet论文及代码详细解析](https://zhuanlan.zhihu.com/p/264627148?utm_source=wechat_session&utm_medium=social&utm_oi=1135649954939883520&utm_campaign=shareopn)\n2. [3D点云基础知识](https://zhuanlan.zhihu.com/p/344635951)\n3. [【3D视觉】PointNet和PointNet++](https://zhuanlan.zhihu.com/p/336496973)\n4. [点云+深度学习的开山之作–Pointnet](https://www.aimlab.top/1694.html)"
  },
  {
    "path": "5-computer_vision/3D视觉算法/单目3D目标检测综述.md",
    "content": "- [一，理论基础-相机与图像](#一理论基础-相机与图像)\n  - [1.1，单目相机介绍](#11单目相机介绍)\n  - [1.2，针孔相机模型](#12针孔相机模型)\n  - [1.3，坐标系间的欧式变换](#13坐标系间的欧式变换)\n  - [1.4，世界坐标与像素坐标的转换](#14世界坐标与像素坐标的转换)\n  - [1.5，三维旋转：欧拉角、旋转矩阵之间的转换](#15三维旋转欧拉角旋转矩阵之间的转换)\n- [二，单目3D目标检测概述](#二单目3d目标检测概述)\n  - [2.1，3D目标检测算法介绍](#213d目标检测算法介绍)\n  - [2.2，单目3D目标检测算法概述](#22单目3d目标检测算法概述)\n  - [2.3，无人驾驶中的3D目标检测任务](#23无人驾驶中的3d目标检测任务)\n  - [2.4，无人驾驶中的3D目标检测的难点](#24无人驾驶中的3d目标检测的难点)\n- [三，主流单目3D检测算法](#三主流单目3d检测算法)\n  - [3.1，Deep3Dbox算法介绍](#31deep3dbox算法介绍)\n  - [3.2，Deep MANTA算法介绍](#32deep-manta算法介绍)\n  - [3.3，GS3D算法介绍](#33gs3d算法介绍)\n  - [3.4，M3D-RPN算法介绍](#34m3d-rpn算法介绍)\n- [四，总结](#四总结)\n- [参考资料](#参考资料)\n\n## 一，理论基础-相机与图像\n\n相机将三维世界中的坐标点（单位为米）映射到二维图像平面（单位为像素）的过程能够用一个几何模型进行描述，这个模型有很多种，其中最简单的称为针孔相机模型。相机的成像过程是也一个射影变换（透视或中心射影）过程，这个过程需要涉及到像素坐标系、平面坐标系、相机坐标系及世界坐标系之间的相互转换。\n\n### 1.1，单目相机介绍\n\n只使用一个摄像头进行 3D 目标检测的做法称为单目3D目标检测，单目相机即单个摄像头，单目相机结构简单，成本特别低，单目相机输出的数据为我们常见的照片。照片本质上是拍照时的场景在相机的成像平面上留下的一个投影，它以二维的形式反映了三维的世界。\n\n摄像机有很多种，但是基本原理是一样的：把光学图像信号转变为电信号，以便于存储或者传输。单目相机尽管成本低，但是也存在无法直接通过单张图片来计算场景中物体与我们之间距离的问题。\n\n### 1.2，针孔相机模型\n\n相机可以抽象为最简单的形式：一个小孔和一个成像平面，小孔位于成像平面和真实的三维场景之间，任何来自真实世界的光只有通过小孔才能到达成像平面。因此，在成像平面和通过小孔看到的真实三维场景存在着一种对应关系，也就是图像中的二维像素点和真实三维世界的三维点存在某种变换关系。找到了这种变换关系，就可以利用图像中的二维坐标点的信息来恢复场景的三维信息。\n\n图2-2 是针孔相机（小孔成像）模型，为了简化模型，将成像平面放在了小孔的前面，并且成的像也是正立的。小孔成像实际就是将相机坐标系中的三维点变换到成像平面中的图像坐标系中的二维点。\n\n![image](images/W_MckQUPiPJQ1OgT9WF6IGExgZXz4ZSL5VppicOUfC4.png)\n\n针孔相机模型涉及到四个坐标系，分别是世界坐标系(world)，相机坐标系(camera)，图像物理坐标系(image)，像素坐标系(pixel)，四个坐标系的详细定义如下：\n\n1. 世界坐标系（world coordinate）$(x_w, y_w, z_w)$，是一个三维直角坐标系，也称为测量坐标系，以其为基准可以表示相机和待测物体的空间3D位置，任意指定 $x_w$轴和 $y_w$轴。如果是自动驾驶数据集，以百度Apollo为例，其$Z$ 轴通过车顶垂直于地面指向上方，$Y$轴在行驶的方向上指向车辆前方，$X$轴为自车面向前方时，指向车辆右侧，车辆坐标系的原点在车辆后轮轴的中心。\n2. 相机坐标系（camera coordinate）$(x_c, y_c, z_c)$，，其坐标原点位于相机的光心位置，$Z$轴为相机的光轴，与像平面垂直，$X_c$轴和 $Y_c$分别平行于投影面即成像图像坐标系的 $X$轴和 $Y$轴。\n3. 图像物理坐标系 $(x', y')$，其坐标原点为投影成像平面的中心，$X$ 轴和 $Y$ 轴分别平行于成像图像平面的两条垂直边，其坐标轴的单位为毫米（mm）。\n4. 像素坐标系 $(uov)$，从小孔向投影面方向看，投影面的左上角顶点为原点轴 $O_{pix}$ ，$uv$分别平行于图像物理坐标系的 $x'$ 轴和 $y'$ 轴。其坐标轴的单位是像素（整数形式）。\n\n假设世界坐标系的空间点 $P$，经过小孔投影 $O$后，落在物理成像平面的投影点为。设 $P$的坐标为$[X,Y,Z]^T$，$O'$为$[X', Y', Z']^T$，物理成像平面到小孔的距离为 $f$(焦距)。，根据三角形相似原理，可得以下公式：\n\n$$X' = f\\frac{x}{z}, Y' = f\\frac{Y}{Z} \\tag{2-1}$$\n\n式 2-1 描述了现实世界的空间点 $P$和成像平面的成像点$P'$的空间关系，但是在相机中我们得到的是一个个像素，需要对平面上的对象进行采样和量化，之后，才得到成像平面的成像点 $P'$ 的像素坐标 $[u, v]^T$。 \n\n可以知道，像素平面与成像平面之间相差了一个缩放和一个原点的平移。假设像素坐标在 $u$ 轴上缩放了 $\\alpha$ 倍，在$v$轴上缩放了 $\\beta$ 倍，并且原点平移了 $c_x, c_y]^T$。那么成像点 $P'$ 与像素坐标 $[u, v]^T$ 的关系为：\n\n![image](images/LSBZuVhPc0_WdPBECTyX4HzdLCwxnXPY1YhRXK4hnu4.png)\n\n通过以上推导，我们得到了相机坐标到像素坐标的转换公式：(式2-5)。在式（2-5）中，把中间的量组成的矩阵称为相机的内参矩阵（Camera Intrinsics）$K$。相机的内参一般是在出厂之后固定的，之后不再变化。有内参自然有外参，式（2-5）使用的是$P$在相机坐标系下的坐标，因为相机在运动，所以$P$的相机坐标（记为$P_c$）是它的世界坐标（记为$P_w$）结合相机当前的位姿转换到相机坐标系下的结果，相机的位姿可以由它的旋转矩阵$R$和平移向量$t$来描述的，两个坐标系间的变换称为欧式变换。\n\n### 1.3，坐标系间的欧式变换\n\n欧式变换是**世界坐标系到相机坐标系的变换**。向量的旋转我们可以用外积表示，与向量的旋转类似，两个坐标系的旋转也可以用**外积**表示，然后再加上平移，则可以统称为坐标系之间的变换[3]。在线性代数中，旋转矩阵是用于在欧几里得空间中进行旋转的矩阵，而描述一个欧式空间的坐标变换关系，可以用一个旋转矩阵 $R$ 和一个平移向量$t$表示，世界坐标到相机坐标的转换公式如下。\n\n$$\nP_{c} = RP_{w} + t = [R, t]P_w \\tag{2-6}\n$$\n\n这里 $P_{c}(x_c, y_c, z_c)$为相机坐标系下的坐标，$P_{w}(x_w, y_w, z_w)$为世界坐标系下的坐标。式（2-6）的变换关系并不是线性的关系，所以我们要引入齐次坐标和变换矩阵方便计算，重写的公式(2-7)如下。该式中，$T$称为变换矩阵（Transform Matrix）。\n\n$$\n\\begin{bmatrix}\nP_c \\\\ \n1\n\\end{bmatrix}\n= \\begin{bmatrix}\nR & t \\\\ \n0^T & 1\n\\end{bmatrix}\n\\begin{bmatrix}\nP_w \\\\ \n1\n\\end{bmatrix}\n= T \\begin{bmatrix}\nP_w \\\\ \n1\n\\end{bmatrix}  \\tag{2-7}\n$$\n\n### 1.4，世界坐标与像素坐标的转换\n\n通过前面两小节，我们推得了相机坐标到像素坐标和世界坐标到相机坐标的变换，则自然可以得到世界坐标到像素坐标的变换公式。转换公式如式（2-8）所示。这里的变换矩阵 $T = \\begin{bmatrix} R & t \\\\ 0^T & 1 \\end{bmatrix}$，其中 $[R|T]$为相机外参，$K$为相机内参矩阵。\n\n$$\nZP_{uv} = Z \\begin{bmatrix}\nu \\\\ \nv \\\\ \n1\n\\end{bmatrix}\n = K \\begin{bmatrix}\nR & t \\\\ \n0^T & 1\n\\end{bmatrix} \\begin{bmatrix}\ny_w \\\\ \nz_w \\\\ \n1\n\\end{bmatrix}\n = K(R|T)P_{W} = KTP_{w} \\; \\tag{2-8}\n$$\n\n### 1.5，三维旋转：欧拉角、旋转矩阵之间的转换\n\n在求解目标 3D 中心点的结算模块用到了几何投影的知识，那里我们需要用到旋转矩阵，算法只能预测出旋转角度，但是 3D 世界坐标到 2D 像素坐标的转换（即前文的欧式变换）需要用到相机内参和旋转矩阵，所以有必要给出欧拉角和旋转矩阵之间的转换公式。\n\n欧拉角是由 Lenhard Euler 引入的，用于描述刚体方向的姿态角（旋转角即物体围绕坐标系三个坐标轴旋转的角度），在 3 维欧几里得空间中描述一个方向，需要三个参数：$\\left \\{\\theta,\\phi ,\\gamma  \\right \\}$，$\\theta$ 是偏航角 yaw，绕 $Y$ 轴旋转；$\\phi$ 是偏航角 pitch，绕 $Z$ 轴旋转；$\\gamma$ 是偏航角 row，绕 $X$ 轴旋转。注意，不同领域叫法不同，$Y, Z,X$ 和 yaw、pitch、roll 并没有绝对的对应关系，其中，飞机姿态示意图如图2-3所示。\n\n![image](images/MvoWKwHhlo7R0Dmd7WMCCYAHSFhP93_1Pq4QPndaRxY.png)\n\n不同的坐标系定义，会有不同的旋转矩阵计算公式，因此需要根据实际情况计算旋转矩阵和平移矩阵。旋转矩阵可由欧拉角进行转换得到，可以知道的是，欧拉角构造旋转矩阵等同于直接把三个Elemental Rotation Matrix相乘，表示绕轴 $Y,Z,X$ 顺序的欧拉角为 $\\left \\{ \\theta ,\\phi ,\\gamma  \\right \\}$ 的外部旋转矩阵的计算公式(式2-9)[4]如下：\n\n![image](images/MVQ8XL6Habr26Nuq2pj3Jw2CuQeFU4D1EXkYZXGHFtM.png)\n\n其中：\n\n![image](images/hHM7RRXZ8Jzce9fjABlruEhYDLwutHNOtc6zsMUIDCE.png)\n\n有了根据欧拉角计算旋转矩阵的公式，在下章的内容中，就可以根据射影几何原理实现待检测目标的 3D 世界坐标到 2D 像素坐标的转换。欧拉角可以通过模型预测得到，再根据式（2-10）可以计算得到旋转矩阵，再根据前面的坐标转换公式，就可以实现2D像素坐标到 3D 世界坐标的转换。\n\n## 二，单目3D目标检测概述\n\n### 2.1，3D目标检测算法介绍\n\n按照传感器和输入数据的不同， 3D目标检测分类如图2-1所示。从图中可以看出，3D目标检测的输入数据以分为图像和点云数据，分别使用的是激光、深度相机和单目相机、双目相机等硬件。从成本上来说，单目相机是成本最低的，也是工业界迫切需要研究和发展的算法。\n\n![image](images/MaUac0nlYgvUgZVwIIp2B2Rn8kWpZpu1GXg1iP6nWBc.png)\n\n对于从激光雷达获得的 3D 点云数据，Chen[1]等人通过扩展 2D 检测算法提出了一些利用3D 点云特征来估计物体 3D 空间位置的方法。相比于使用雷达或者深度相机，使用摄像机的硬件成本更低，但是需要进行图像点的反投影，计算点在空间中的位置。此外，相对于点云数据，单目相机图像恢复深度的方法可以应用于室外大尺度场景，普通RGB-D深度相机存在视野小、噪声大、测量范围窄等问题。\n\n对于单目摄像机的 3D 目标检测问题，可以采用**以深度学习为主的 2D bbox 预测及大小姿态估计网络算法+目标 3D 中心点解算模块**[2]。因此，如何结合深度学习算法和射影几何约束，是提升算法精度的关键。下文会介绍几种单目图像进行 3D 目标检测算法，来说明和讨论如何实现基于单目相机图像的 3D 目标检测。\n\n### 2.2，单目3D目标检测算法概述\n\n3D 目标检测主要是为了获取物体的 3D bbox(`bounding box`)。3D bbox 的定义是在真实三维世界中包围目标物体的最小长方体，理论上，一个 `3D bbox` 有 9 个自由度，3 个是位置，3 个是维度大小，3 个是旋转角度。3D 目标检测任务从宏观上来讲可以分为两个部分：目标定位与目标姿态的描述。\n\n目标定位可以复用 2D 目标检测框架，对目标进行准确定位；因此在 3D 目标检测任务中，如何对目标姿态进行准确描述是3D目标检测算法的关键问题。对于目标姿态的描述信息，一些研究工作引入了与2D图像对应的深度信息图， 利用2D图像和2D图像对应的深度信息可以对目标所在空间进行描述，即可在 3D 场景上进行目标检测。深度信息即计算 2D 图像中框出的物体上的点在物理世界坐标系中距离相机成像平面的距离，对于单目 RGB 图像，可以利用几何约束（geometry）来求解，比如 `Deep3DBox` 和 `Deep MANTA` 算法。\n\n在 `Deep3DBox` 中，已知 2D box、orientation、3D size，利用 3D box 投影到图像上的顶点和 2D box 的边的对应关系可以求解出物体的 3D 坐标。在 Deep MANTA 中，已知 2D keypoints、3D size，首先通过 CAD model 获得 3D keypoints，然后求解 `PnP` 就可以得到物体的 3D 坐标和 orientation。这类方法的 `3D` 定位精度本质上受限于**重投影误差**，而 keypoints、2D box 的定位精度是有限的，所以目前也可能会存在算法瓶颈。\n\n### 2.3，无人驾驶中的3D目标检测任务\n\n感知系统是自动驾驶系统的一个核心子系统，而目标检测 Object Detection 就是感知系统的核心技术，感知系统要求我们根据不同的传感器设计不同的感知算法，从而准确的检测出车辆前方的障碍物。例如在国内百度Apollo自动驾驶系统 中，有为 3D 点云而设计的CNN-SEG算法，和为 2D 图像而设计的 YOLO-3D算法等。\n\n常见的2D目标检测是不适用于无人驾驶的规划及控制任务的，而 3D 目标检测可以获取物体的 3D bbox（真实三维世界中包围目标物体的最小长方体）。3D 目标检测系统要求算法，能够对实时视频流中的单帧图像完成3D目标检测，在有相机的内参和外参矩阵基础上，将输出结果映射到统一的世界坐标系或者车身坐标系中。\n\n任意一个相机坐标系下的目标 3d bbox，可以用 9 个自由度表示，即目标的中心点 $T = \\left \\{ x, y, z \\right \\}$，长宽高 $D = \\left \\{l, w, h \\right \\}$}，以及目标的姿态角（旋转角）$\\left \\{ \\theta ,\\phi ,\\gamma  \\right \\}$。这种 9 自由度的参数表示与 3d bbox 的 8 个角点坐标表示相比，其实是等价的，可以相互转换，而且不需要的 $8\\times 3 = 24$ 个坐标参数来表示，有利于减少复杂度。\n\n同时，对一个相机坐标系下 3D 目标，可以通过设定好的相机内参和经过转换计算得来的外参矩阵，即根据射影几何约束原理，将3D坐标投射到 2D 图像上，得到 目标的2D像素坐标。\n\n### 2.4，无人驾驶中的3D目标检测的难点\n\n尽管深入学习技术的应用已经给3D视觉目标检测带来了很大发展，但是3D目标检测依然还有很多痛点问题有待解决，主要难点如下：\n\n1. 目标被遮挡，遮挡分为两种情况，目标物体相互遮挡和目标物体被背景遮挡；\n2. 图像中存在很多小目标，相对输入图片大小，目标物体所占像素点极少；\n3. 目标被截断，部分物体被图片截断，即在图片中只能显示部分物体；\n4. 目标的旋转角度学习，在图像中物体的朝向不同，但是对应特征可能相同。\n\n\n\n## 三，主流单目3D检测算法\n\n### 3.1，Deep3Dbox算法介绍\n\n`Deep3Dbox` 是一种用于在单目 `RGB` 图像（a single image）上进行 3D 目标检测和姿态估计（pose estimation）的算法。相对于当前仅仅回归物体的 3D方向（orientation）的方法，该算法使用深度神经网络首次回归了目标相对稳定的3D属性，并结合这些属性和 2d bounding box 的几何约束（geometric constraints），生成一个 3D bounding box。主要思想是基于 2D 的目标检测框去拟合 3D 检测框，预测量主要有三个：\n\n1. 三维框的大小（在$x,y,z$ 轴上的大小）；\n2. 旋转角度；\n3. 置信度。\n\n网络的第一个输出使用一种新颖的 hybrid（混合）discrete-continuous（离散-连续）loss, （显著优于 L2 loss）预测 3D 方向（orientation），网络的第二个输出回归了物体的3D尺寸（dimension），相比于之前的模型，它准确度更高且能够识别多种物体。\n\n### 3.2，Deep MANTA算法介绍\n\n`Deep MANTA` 算法[7]的通过一张图片完成多项车辆分析任务，包括车辆检测、局部定位、可视化特征描述以及 3D 估计。基于 `coarse-to-fine object proposal` 提升车辆检测效果。并且，Deep MANTA 网络可以检测出半遮挡的车辆。在推断过程中，网络的输出作为一个鲁棒的实时位姿估计算法的输入，来进行姿态估计和三维车辆定位。算法主要特点有以下几个方面：\n\n1. 算法的网络结构是基于 `Faster R-CNN` 框架，网络不仅预测车辆包围框，同时还预测车辆部件坐标、部件可见性、车辆自身尺寸等丰富的信息，同时使用图像车辆的特征点坐标来编码车辆的 3D 信息。\n2. 网络使用了**级联结构**（`cascade`）预测以上信息，在共享底层特征（feature map）的同时提供足够的拟合能力预测多种信息，并反复回归包围框，提高定位精度。\n3. 在网络推理（inference）时使用之前预测的信息进行 2D/3D 匹配，从而得到车辆的 3D 姿态与位置信息。\n\n### 3.3，GS3D算法介绍\n\nGS3D[5]是基于引导和表面(GS)的 3D 车辆检测算法。该算法的基本流程也是先进行 2D 检测，再通过一些先验知识和投影几何原理等计算`3Dbbox` 的尺寸和方位。该算法的重要创新点是充分利用了 3D 表面在 2D 图像的投影特征，以方面模型进行区分判别。算法以单张 RGB 图像作为输入，由粗略到精细的步骤逐步恢复车辆的 3D bbox。检测流程如图 2-4 所示：\n\n![image](images/hB00c5gyeCh7We3LQFhAPh_IYp_R98Z08Oc5NhRxpr0.png)\n\n### 3.4，M3D-RPN算法介绍\n\nM3D-RPN算法[6]的主要思想在于提出将单目3D目标检测问题重新构造为独立的3D区域提议网络（RPN，类似Faster R-CNN的RPN网络），即 M3D-RPN 结构（Monocular 3D Region Proposal Network for Object Detection），来提高模型性能，利用2D和3D透视图的几何关系，允许3D边框利用图像空间生成的卷积特征，该思想和Deep3Dbox 算法是一样的，即在于结合投影几何原理和2D图像特征，得到目标的3D box。\n同时 M3D-RPN 算法设计了深度-感知（depth-aware）的卷积层，来解决繁琐的3D参数估计问题，该方法可提取特定位置的特征，从而改善了3D场景的理解性能。M3D-RPN的简单架构图如图2-5所示，其用全局卷积（橙色）和局部深度-觉察卷积（蓝色）的单个单目3D区域提议网络来预测多类3D边框。\n\n![image](images/GykkMJgoGhjNvzwm2yXnO262mGfO624uLKCkm80HIhU.png)\n\n图2-6 是 `M3D-RPN` 和以前算法： `Deep3DBox`、`Multi-Fusion` 的比较。从图中可以看出，先前的工作由内部多步（橙色）和外部网络（蓝色）组成，而 `M3D-RPN` 是**端到端训练**的单步（`single shot`）网络。\n\n![image](images/F3eNAz6AKUBO5cQ2RPsJ3_Kvo-HpyCySzXutI_F5DOI.png)\n\n## 四，总结\n\n本文首先介绍了单目 3D 目标检测的理论基础，然后概述了单目 3D 目标检测的内容，包括3D目标检测算法根据输入信号不同的分类、 单目 3D 视觉目标检测定义和难点，以及描述了无人驾驶中的3D目标检测任务，最后介绍了几个主流的单目 3D 目标检测算法，并描述了这些算法的原理和相关检测流程。总结可以发现主流的单目3D 目标检测算法都有以下共同点：\n\n1. 都复用了 2D 目标检测的结果；\n2. 都有结合投影几何知识和利用2D图像特征来得到目标的3D空间位置；\n3. 都有 3D 属性参数回归网络。\n\n值得注意的是，目前单目视觉 3D 目标检测技术依然面临着很多问题，因为算法不仅要找到物体在图像中出现的位置，还需要反投影到实际 3D 空间中，这个过程是需要有绝对的尺寸估计。\n\n## 参考资料\n\n1. [1] X. Chen, K. Kundu, Y. Zhu, A. G. Berneshawi, H. Ma, S. Fidler, and R. Urtasun, “3D object proposals for accurate object class detection”, in Neural Information Processing Systems, 2015.\n2. [2] 何志强. 基于深度估计的3D目标检测与跟踪算法研究[D].西安科技大学,2019.\n3. [3] 高翔，张涛等. 视觉SLAM十四讲从理论到时间[M]. 电子工业出版社,2017.\n4. [4] [Rotation matrix calculation [EB/OL]](https://en.wikipedia.org/wiki/Rotation\\_matrix)\n5. [5] Mousavian, Arsalan, Dragomir Anguelov, John Flynn and Jana Kosecka. “3D Bounding Box Estimation Using Deep Learning and Geometry.” 2017 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) (2016): 5632-5640.\n6. [6] Brazil, Garrick and Xiaoming Liu. “M3D-RPN: Monocular 3D Region Proposal Network for Object Detection.” 2019 IEEE/CVF International Conference on Computer Vision (ICCV) (2019): 9286-9295.\n7. [7] Chabot, Florian, Mohamed Chaouch, Jaonary Rabarisoa, Céline Teulière and Thierry Chateau. “Deep MANTA: A Coarse-to-Fine Many-Task Network for Joint 2D and 3D Vehicle Analysis from Monocular Image.” 2017 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) (2017): 1827-1836.\n\n"
  },
  {
    "path": "5-computer_vision/README.md",
    "content": "- [一，传统数字图像处理](#一传统数字图像处理)\n- [二，计算机视觉](#二计算机视觉)\n  - [2.1，目标检测基础](#21目标检测基础)\n  - [2.2，二阶段目标检测算法](#22二阶段目标检测算法)\n  - [2.3，一阶段目标检测算法](#23一阶段目标检测算法)\n  - [2.4，3D 视觉算法](#243d-视觉算法)\n  - [2.5，工业视觉](#25工业视觉)\n  - [2.6，项目实践](#26项目实践)\n- [参考资料](#参考资料)\n## 一，传统数字图像处理\n\n1. [《数字图像处理》学习笔记](./数字图像处理/《数字图像处理》学习笔记.md)\n2. [数字图像处理基础总结](./数字图像处理/数字图像处理基础总结.md)\n3. [OpenCV3 基本函数总结](./数字图像处理/OpenCV3基本函数总结.md)\n4. [视频编解码基础](./数字图像处理/视频编解码基础.md)\n\n## 二，计算机视觉\n> 基于深度学习算法的计算机视觉领域知识。\n\n### 2.1，目标检测基础\n\n1. [目标检测模型的基础](./2D目标检测/0-目标检测模型的基础.md)\n2. [目标检测模型的评价标准-AP与mAP](./2D目标检测/1-目标检测模型的评价标准-AP与mAP.md)\n  \n### 2.2，二阶段目标检测算法\n\n1. [二阶段目标检测网络-Faster R-CNN 详解](./2D目标检测/2-Faster-RCNN网络详解.md)\n2. [二阶段目标检测网络-FPN 详解](./2D目标检测/3-FPN网络详解.md)\n3. [二阶段目标检测网络-Mask R-CNN 详解](./2D目标检测/4-Mask-RCNN详解.md)\n4. [二阶段目标检测网络-Cascade R-CNN 网络理解](./2D目标检测/5-Cascade-RCNN论文解读.md)\n\n### 2.3，一阶段目标检测算法\n\n1. [一阶段目标检测网络-RetinaNet 详解](./2D目标检测/6-RetinaNet网络详解.md)\n2. [YOLOv1-v5 系列论文解读](./2D目标检测/7-YOLOv1-v5论文解读.md)\n3. [Scaled-YOLOv4 论文解读](./2D目标检测/8-Scaled-YOLOv4论文解读.md)\n\n### 2.4，3D 视觉算法\n\n- [3D 视觉算法初学概述](./3D视觉算法/3D视觉算法初学概述.md)\n\n### 2.5，工业视觉\n\n- [Halcon 快速入门](./工业视觉/Halcon快速入门.md)\n\n### 2.6，项目实践\n\n- [车牌检测识别](./项目实践/GitHub%E8%BD%A6%E7%89%8C%E6%A3%80%E6%B5%8B%E8%AF%86%E5%88%AB%E9%A1%B9%E7%9B%AE%E8%B0%83%E7%A0%94.md)\n\n## 参考资料\n\n- 《cs231 课程》\n- `resnet`、`faster rcnn`、`mask rcnn` 等论文\n"
  },
  {
    "path": "5-computer_vision/工业视觉/Halcon快速入门.md",
    "content": "- [前言](#前言)\n- [一，HALCON 概述](#一halcon-概述)\n- [1.1，HALCON 安装](#11halcon-安装)\n- [二，HALCON 架构](#二halcon-架构)\n  - [2.1，算子](#21算子)\n    - [2.1.1，参数和数据结构](#211参数和数据结构)\n  - [2.2，拓展包](#22拓展包)\n  - [2.3，接口](#23接口)\n    - [2.3.1，HALCON-Python 接口](#231halcon-python-接口)\n    - [2.3.2，HALCON-C 接口](#232halcon-c-接口)\n    - [2.3.3，HALCON-C++ 接口](#233halcon-c-接口)\n    - [2.3.4，HALCON-.NET 接口](#234halcon-net-接口)\n  - [2.4，图像获取接口](#24图像获取接口)\n  - [2.5，I/O 接口](#25io-接口)\n- [三，如何开发应用](#三如何开发应用)\n  - [3.1，HDevelop](#31hdevelop)\n  - [3.2，示例程序](#32示例程序)\n- [四，更多参考资料](#四更多参考资料)\n\n## 前言\n工业智慧视觉应用主要涉及四个场景：识别、测量、定位、检测。\n\n* **识别**：识别物体的物理特征，包括形状、颜色、字符和条码等，常见的应用场景是 OCR，读取零部件上的字母、数字、字符等用于溯源。\n* **测量**：把获取到的图像像素信息标定成常用的度量衡单位，再通过精确计算出目标的几何尺寸。\n* **定位**：获取目标的二维/三维位置信息，常用语元件定位，用以辅助机器臂进行后续的抓取等动作。\n* **检测**：一般特指缺陷检测，判断产品是否存在缺陷，如零部件缺陷检测等。\n\n![image](images/vjTTNDFp8mu84iqX4nFsNR2lz73yBCzhIrd1Q_rpwoU.png)\n\n## 一，HALCON 概述\n`HALCON` 是德国 MVtec 公司开发的一款综合性的机器视觉标准软件，拥有全球通用的集成开发环境（HDevelop）。它节约了产品成本，缩短了软件开发周期——HALCON 灵活的架构便于机器视觉，医学图像和图像分析应用的快速开发。在欧洲以及日本的工业界已经是公认具有最佳效能的机器视觉（Machine Vision）软件。\n\nMVTec 提供了 5 种软件：HALCON、MERLIC、深度学习工具、接口、嵌入式视觉，其中 HALCON 是最核心和应用最广的。\n\nHALCON 主要提供的技术有：条形码和二维码读取、BLOB 分析、物图像分类、计算光学成像、过滤技术、缺陷检查、匹配、1D/2D/3D 测量、形态学处理、OCR 和 OCV、基于样本的识别（SBI）、亚像素边缘检测和线条提取技术、深度学习和 3D 视觉技术。\n\n> 所谓 Blob 分析，即是从连通像素中提取具有相同逻辑状态的特征 (Blob)。\n\n更多技术的描述请参阅官网[资料](https://www.mvtec.com/cn/technologies)。\n\n![image](images/Y4dbFB2uVmDG4RDZR43QV9rf_RvGU9F7OkZeTdNx2W8.png)\n\n## 1.1，HALCON 安装\n> 注意：**HALCON 目前不支持 arm 处理器版的 M1 Pro 机器**，而且目前主流是在 Windows 开发居多。\n\n注意，需要先在官网**注册账号**，然后才能下载对应软件，MVTec HALCON 提供两个不同的软件版本：[HALCON 订阅版 (HALCON Progress)](https://www.mvtec.com/cn/products/halcon/why-halcon/editions/halcon-progress) 和 [HALCON 永久版 (HALCON Steady)](https://www.mvtec.com/cn/products/halcon/newest-features/halcon-20-11-1)。两个版本是完全独立的。 需要许可证，这意味着没有可能从一个版本 \"切换\" 到另一个版本。\n\nHALCON 下载安装步骤如下所示：\n\n1. 进入 HALCON [官网](https://www.mvtec.com/cn/downloads/halcon)，选择产品版本、操作系统以及架构后就会下载对应版本软件直接点击下载好的安装包即可安装。\n2. 安装的详细步骤截图如下所示，试用版不用安装 `license` 文件，跳过即可。\n\n![image](images/iNZX2hXuI-15Wpl8vgptbQt68uvNW4XKZib0ill5smc.png)\n\n![image](images/60JeE1RsfI7dZkNAbWOsNPfBCFnfYjEyPVzBhdu9608.png)\n\n![image](images/VNgcDM4MGpl71dUvQxivi2Y_ew3fTs8YfBEaW5HBhOc.png)\n\n![image](images/y71JYj6lZWGd_w1M_qYUBCCmE_F5_qwDPbJCVMpi2w8.png)\n\n## 二，HALCON 架构\n`HALCON` 架构如下图 2.1 所示。HALCON 机器视觉软件的主要部分就是**图像处理库**，其由超过 2000 多个**算子**组成，当然我们也可以通过拓展包的形式开发自定义算子，并在程序中使用。\n\n![image](images/2Z2fsCtvc8KFMdb3QiEaP8xpffXncnZa6D80X-1LxBU.png)\n\n`HALCON` 提供了**通用**的图像采集接口来支持不同的图像采集设备（3D相机、相机等），包好特定设备的实现库会在程序运行时动态加载。\n\n### 2.1，算子\n> Operators。\n\n我们使用 HALCON 库中任何功能实际上都是通过算子（`Operators`）完成的，每个功能都有多种实现方法，其可以通过算子参数选择。完整的算子列表在 HALCON Operator Reference，其提供了 HDevelop, .NET, Python, C++, 和 C syntax 接口。HALCON库提供的算子的重要特征如下:\n\n* 算子之间没有层次结构，所有算子都是一个级别的。\n* 存在逻辑算子。\n* 很多算子可以使用并行加速技术。\n* 算子的输入输出参数的排序是有**标准化**规则: 输入图标(`iconic`)、输出图标、输入控制和输出控制。\n\n#### 2.1.1，参数和数据结构\n>  Quick Guide to HALCON Parameters and Data Structures\n\n`HALCON` 算子的参数有两种基本类型：图标数据和控制数据（iconic data and control data）。图像、区域（`regions`）和 XLD（拓展线描述） 属于标志性数据。\n\n* Images（图像）即包含像素值的矩阵，由多个通道组成，其详细定义可以参考《数字图像处理》书籍。这里感兴趣区域 ROI 指的是输入图像的哪一部分区域会被处理，ROI 可以灵活定义，从简单的矩形到一组未连接到像素点都支持。 \n* Regions 由一系列像素组成。区域之中的像素可以不互相连接，任意像素集合都可以作为单个区域处理。\n* XLDS 包括所有基于轮廓和多边形的数据。像 `edges_sub_pix` 这样的亚像素精度算子将轮廓作为 XLD 数据返回。 轮廓是一系列 2D 控制点，由线连接。 通常，控制点之间的距离约为 1 个像素。 除了控制点之外，XLD 对象还包含所谓的局部和全局属性。 这些的典型示例是，例如，控制点的边缘幅度或轮廓段的回归参数。 除了提取 XLD 对象外，HALCON 还支持进一步处理。 这方面的示例是基于给定特征范围的轮廓选择，用于将轮廓分割成线、弧、多边形或平行线。\n\n控制数据（`control data`）包括句柄和基本数据类型，如整数、实数、字符串。\n\n**句柄是对复杂数据结构的引用**，例如，与图像采集接口或基于形状匹配的模型的连接。 出于效率和数据安全的原因，在操作符之间传递的不是整个结构而是只有句柄。 句柄是不能更改的神奇值(magic values)，并且可能因执行和版本而异。 一旦所有引用被覆盖，它们就会自动清除。 使用句柄的示例有**图形窗口、文件、套接字、图像采集接口、OCR、OCV、测量和匹配**。\n\n### 2.2，拓展包\n为了支持特殊硬件或实现新的算法，HALCON 支持以 **C 语言**实现的自定义算子。拓展包接口包含几个预定义的例程和宏，用于在 C 中轻松处理图像数据和内存对象。成功集成新算子后，它可以像任何其他 HALCON 算子一样使用。\n\n### 2.3，接口\nHALCON 支持Python、C、C++ 和 .NET 语言接口，对于·不同编程语言接口，其数据类型、类和算子的命名会有所不同。\n\n#### 2.3.1，HALCON-Python 接口\n读取图像并计算连接区域(`connected regions`)数量的示例代码如下。\n\n```python\nimg = ha.read_image('pcb')\nregion = ha.threshold(img, 0, 122)\nnum_regions = ha.count_obj(ha.connection(region)) print(f'Number of Regions: {num_regions}')\n```\n#### 2.3.2，HALCON-C 接口\nC 接口是 HALCON 支持的最简单的接口，每个算子由 1 或 2 个全局函数表示，其中算子的名称和参数序列和 HDevelop 语言相同。\n\n> 因为 HALCON  算子的本身就是由 C 语言实现的，所以 C 是原生接口，支持也是最好。 \n\n以下示例代码也是实现读取图像并计算连接区域(`connected regions`)数量。\n\n```cpp\nHobject img;\nread_image(&img, \"pcb\");\nHobject region;\nthreshold(img, &region, 0, 122);\nHobject connected_regions;\nconnection(region, &connected_regions);\nHlong num_regions = 0;\ncount_obj(connected_regions, &num_regions);\nprintf(\"Number of Regions: %\" PRIdPTR \"\\n\", num_regions);\n```\n#### 2.3.3，HALCON-C++ 接口\nC++ 接口比 C 接口复杂得多，应用了 C++ 面向对象编程的优点，包括自动类型转换、构造和析构函数等。另外和 C 接口一样，也为每个 HALCON 算子提供了全局函数，来实现程序化的编程风格（a procedural style of programming）。\n\n读取图像并计算连接区域(`connected regions`)数量的 C++ 接口实现代码如下。\n\n```cpp\nHImage img{\"pcb\"};\nHRegion region = img.Threshold(0, 122);\nHlong numRegions = region.Connection().CountObj();\nstd::cout << \"Number of Regions: \" << numRegions << '\\n';\n```\n#### 2.3.4，HALCON-.NET 接口\n略\n\n### 2.4，图像获取接口\nHALCON 通过动态库（Windows: 动态加载库 `DLLs`, Linux: 共享库 `shared libraries`）的形式为 50 多个图像采集卡和数百个工业相机提供采集**图像的接口**。库名称以前缀 `hAcq` 开头；HALCON XL 使用以 `xl` 结尾的库。\n\nHALCON 图像采集接口的更新会比 HALCON 库本身更新更为频繁。\n\n成功安装好图像采集设备后，通过 `open_framegrabber` 算子（需配置设备的名称和其他信息）访问设备，通过 `grab_image` 算子获取图像。\n\n### 2.5，I/O 接口\n`HALCON` 对不同 `I/O` 设备使用同一类算子实现统一访问。安装好 `I/O` 设备后，使用 `open_io_device` 算子建立连接，指定 I/O 设备接口的名称；建立连接后，通过调用 `open_io_channel` 来打开传输通道，然后使用 `read_io_channel` 和 `write_io_channel` 算子读取和写入值。\n\n## 三，如何开发应用\n官方推荐使用 HDevelop（HALCON 机器视觉库的交互式开发环境） 进行快速原型设计。在开发好 HDevelop 程序后需要将其转换为最终环境，方法有以下三种：\n\n* **Start from Scratch**: 从头(`scratch`)开始编写程序意味着手动将 HDevelop 代码翻译成目标编程语言（C++、Python...）。\n* **导出 HDevelop 代码**: 使用 HDevelop 的代码导出功能将您的 HDevelop 代码自动翻译成目标编程语言。\n* **导出库项目**：：HDevelop 的库导出会生成一个即用型项目文件夹，包括目标语言的包装代码和用于构建项目的 `CMake` 文件。 HDevelop 的库导出使用 `HDevEngine`，一个充当解释器的库。\n\n### 3.1，HDevelop\n默认情况下，`HDevelop` 窗口入下图 3.1 所示，窗口主要分为 **3 类**：\n\n1. **图形窗口: **显示（中间）结果。即显示图像、区域和 XLD 等标志性数据。\n2. **程序窗口: **即输入和编辑代码的地方。\n3. **变量窗口**: 显示所有变量。即显示图标变量（`iconic variables`）和控制变量。图标变量包含图标数据，控制变量包含控制数据。\n\n![image](images/is7S4w_iEzSLet8Nrrf5N1Xmqsmqg6-4zcjZq4XQwXY.png)\n\n### 3.2，示例程序\n推荐观看视频教程: [ Integrate HDevelop code into a C++ application using the Library Project Export](https://www.mvtec.com/services-support/videos-tutorials/single-video/hdevelop-library-project-export)。\n\n分步说明的描述可以参考 《quick\\_guide》 文档的3.2 节内容。\n\n## 四，更多参考资料\nHALCON 相关文档描述及下载链接汇总如下表所示。\n\n|REFERENCE MANUAL 参考手册|下载链接|文件大小|\n| ----- | ----- | ----- |\n|HALCON Operator Reference（HALCON 算子参考资料）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/reference/reference_hdevelop.pdf)<br>[在线阅读 （需要 Javascript）](https://www.mvtec.com/cn/products/halcon/work-with-halcon/documentation)|24.8 MB|\n\n|BASICS 基础知识|下载链接|文件大小|\n| ----- | ----- | ----- |\n|Quick Guide（快速指南）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/manuals/quick_guide.pdf)|2.6 MB|\n|Installation Guide（安装指南）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/manuals/installation_guide.pdf)|0.4 MB|\n|HDevelop Users' Guide（HDevelop 用户指南）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/manuals/hdevelop_users_guide.pdf)|6.2 MB|\n|Solution Guide I - Basics（解决方案指南 I - 基础知识）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/solution_guide/solution_guide_i.pdf)|6.7 MB|\n|Solution Guide II - A - Image Acquisition（解决方案指南 II-B - 图像采集）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/solution_guide/solution_guide_ii_a_image_acquisition.pdf)|0.7 MB|\n|Solution Guide II - B - Matching（解决方案指南 II-B - 匹配）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/solution_guide/solution_guide_ii_b_matching.pdf)|3.4 MB|\n|Solution Guide II - C - 2D Data Codes（解决方案指南 II-C - 二维码）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/solution_guide/solution_guide_ii_c_2d_data_codes.pdf)|4.6 MB|\n|Solution Guide II - D - Classification（解决方案指南 II-D - 分类）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/solution_guide/solution_guide_ii_d_classification.pdf)|4.3 MB|\n|Solution Guide III - A - 1D Measuring（解决方案指南 III-A - 1D 测量）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/solution_guide/solution_guide_iii_a_1d_measuring.pdf)|1.2 MB|\n|Solution Guide III - B - 2D Measuring（解决方案指南 III-B - 2D 测量）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/solution_guide/solution_guide_iii_b_2d_measuring.pdf)|2.5 MB|\n|Solution Guide III - C - 3D Vision（解决方案指南 III-C - 3D 视觉）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/solution_guide/solution_guide_iii_c_3d_vision.pdf)|14.2 MB|\n|Technical Updates（技术更新）|[下载 PDF](https://www.mvtec.com/fileadmin/Redaktion/mvtec.com/products/halcon/documentation/manuals/technical_updates.pdf)|0.2 MB|\n\n"
  },
  {
    "path": "5-computer_vision/数字图像处理/OpenCV3基本函数总结.md",
    "content": "> 此笔记针对 Python 版本的 opencv3，c++ 版本的函数和 python 版本的函数参数几乎一样，只是矩阵格式从 ndarray 类型变成适合 c++ 的 mat 模板类型。注意，因为 python 版本的opncv只提供接口没有实现，故函数原型还是来自 c++版本的opencv，但是参数解释中的数据类型还是和 python 保持一致。\n\n### 图像的载入：imread() 函数\n\n**函数原型：**\n\n```cpp\nMat imread(const sting& filename, int flags=None)\n```\n\n**参数解释：**\n+ `filename`：图像的文件路径，`sting` 字符串类型\n+ `flags`：载入标识，以何种方式读取图片，`int` 类型的 `flags`。常用取值解释如下：\n    + `flags = 0`：始终将图像转成灰度图再返回\n    + `flags = 1`：始终将图像转换成彩色图再返回，如果读取的是灰度图，则其返回的矩阵 `shape` 将变为 `(height, width, 3)`\n    + `flags = 2`：如果载入的图像深度为 `16` 位或者 `32` 位，就返回对应深度的图像，否则，就转换为 `8` 位图像再返回。\n\n**总结：**读取文件中的图片到 `OpenCV` 中，返回 `Mat` 或者 `ndarray` 类型的矩阵，以彩色模式载入图像时，解码后的图像会默认以 `BGR` 的通道顺序进行存储。\n\n**cv2.imread()函数：**\n\n`python-opencv` 库的 `imread` 函数的 `flags` 参数取值方式与 `C++` 版有所区别。使用函数 `cv2.imread()` 读入图像，图像要么在此程序的工作路径，要么函数参数指定了完整路径，第二个参数是要告诉函数应该如何读取这幅图片，取值如下：\n\n+ `cv2.IMREAD_COLOR` : 取值 `1`，读入一副彩色图像。图像的透明度会被忽略，这是默认参数。\n+ `cv2.IMREAD_GRAYSCALE` : 取值 `0`，以灰度模式读入图像。\n+ `cv2.IMREAD_UNCHANGED` : 取值 `-1`，读入一幅图像，并且包括图像的 alpha 通道。\n\n> Instead of these three flags, you can simply pass integers 1, 0 or -1 respectively.\n\n```python\nimport numpy as np\nimport cv2\n# Load an color image in grayscale\nimg = cv2.imread('messi5.jpg',0)\n```\n\n`opencv-python` 库的读取图像函数 `cv2.imread()` 官方定义如下图。\n\n![opencv-python库的读取图像函数官方定义](../../data/images/cv2.imread函数.png)\n\n### 图像的显示：imshow()函数\n\n**函数原型：**\n\n```cpp\nvoid imshow(const string &winname, InputArray mat)\n```\n\n**参数解释：**\n\n+ `winname`：需要显示的窗口标识名称，`string` 字符串类型\n+ `mat`：需要显示的图像矩阵，`ndarray` numpy 矩阵类型\n\n**总结：**`imshow` 函数用于在指定的窗口显示图像，窗口会自动调整为图像大小。\n\n### minMaxLoc 函数\n\n函数 `cv :: minMaxLoc` 查找最小和最大元素值及其位置，返回的位置坐标是**先列号，后行号**（列号，行号） 。在整个数组中搜索极值，或者如果mask不是空数组，则在指定的数组区域中搜索极值。（**只适合单通道矩阵**）。函数原型：\n\n```cpp\nCV_EXPORTS_W void minMaxLoc(InputArray src, CV_OUT double* minVal,\n                            CV_OUT double* maxVal = 0, CV_OUT Point* minLoc = 0,\n                            CV_OUT Point* maxLoc = 0, InputArray mask = noArray());\n```\n\n函数参数解释：\n\n+ `src`：input single-channel array.\n+ `minVal`：pointer to the returned minimum value; NULL is used if not required.\n+ `maxVal`：pointer to the returned maximum value; NULL is used if not required.\n+ `minLoc`：pointer to the returned minimum location (in 2D case); NULL is used if not required.\n+ `maxLoc`：pointer to the returned maximum location (in 2D case); NULL is used if not required.\n\n### 位深度的概念\n\n+ 灰度图的位深度是 `16`，则其矩阵的元素类型为 `uint16` ，彩色图其位深度一般是 `24` ，红色占 `8` 个位、蓝色占 `8` 个位、绿色占 `8` 个位，其矩阵的元素类型为 `uint8`。\n+ 位分辨率（ `Bit Resolution` ）又称色彩深度、色深或位深度，在位图图像或视频视频缓冲区，指一个像素中，每个颜色分量（`Red、Green、Blue、Alpha` 通道）的比特数。\n+ `matplotlib.image.imsave` 将灰度图的矩阵保存为图像格式时，其默认保存的图像通道数为 `4`：`RGBA`，其中 `RGB` 三个通道对应的二维矩阵数值完全一样。\n"
  },
  {
    "path": "5-computer_vision/数字图像处理/《数字图像处理》学习笔记.md",
    "content": "- [一，绪论](#一绪论)\n  - [1.1, 什么是数字图像处理](#11-什么是数字图像处理)\n  - [1.2，数字图像处理的起源](#12数字图像处理的起源)\n  - [1.3，数字图像处理技术应用实例](#13数字图像处理技术应用实例)\n  - [1.4，数字图像处理的基本步骤](#14数字图像处理的基本步骤)\n  - [1.5，图像处理系统的组成](#15图像处理系统的组成)\n- [二，数字图像基础](#二数字图像基础)\n  - [2.1，视觉感知要素](#21视觉感知要素)\n    - [2.1.1，人眼的结构](#211人眼的结构)\n    - [2.1.2，人眼中图像的形成](#212人眼中图像的形成)\n    - [2.1.3，亮度适应与辨别](#213亮度适应与辨别)\n  - [2.2，电磁波谱](#22电磁波谱)\n  - [2.3，图像感知与获取](#23图像感知与获取)\n    - [2.3.1，一个简单的成像模型](#231一个简单的成像模型)\n  - [2.4，图像取样和量化](#24图像取样和量化)\n    - [2.4.1，取样和量化的概念](#241取样和量化的概念)\n    - [2.4.2，数字图像表示](#242数字图像表示)\n  - [2.5，像素间的一些基本关系](#25像素间的一些基本关系)\n  - [2.6，数字图像处理所用的基本数学工具](#26数字图像处理所用的基本数学工具)\n    - [2.6.1，对应元素运算和矩阵运算](#261对应元素运算和矩阵运算)\n    - [2.6.2，线性运算和非线性运算（一般两个图像之间）](#262线性运算和非线性运算一般两个图像之间)\n    - [2.6.3，数字图像处理数学工具-算术运算（一般两个图像之间）](#263数字图像处理数学工具-算术运算一般两个图像之间)\n    - [2.6.4，集合运算和逻辑运算（一般两个图像之间）](#264集合运算和逻辑运算一般两个图像之间)\n    - [2.6.5，空间运算（单幅图像）](#265空间运算单幅图像)\n      - [2.6.5.1，单像素操作](#2651单像素操作)\n      - [2.6.5.2，领域运算](#2652领域运算)\n      - [2.6.5.3，几何变换](#2653几何变换)\n- [三，灰度变换与空间滤波](#三灰度变换与空间滤波)\n  - [3.1，背景](#31背景)\n  - [3.2，一些基本的灰度变换函数](#32一些基本的灰度变换函数)\n    - [3.2.1，图像反转](#321图像反转)\n    - [3.2.2，对数变换](#322对数变换)\n    - [3.2.3，幂律变换（伽玛变换）](#323幂律变换伽玛变换)\n    - [3.2.4，分段线性变换函数](#324分段线性变换函数)\n  - [3.3，直方图处理](#33直方图处理)\n    - [3.3.1，直方图均衡化](#331直方图均衡化)\n    - [3.3.2，直方图匹配（规定化）](#332直方图匹配规定化)\n    - [3.3.3，局部直方图处理](#333局部直方图处理)\n    - [3.3.4，使用直方图统计增强图像](#334使用直方图统计增强图像)\n  - [3.4，空间滤波基础](#34空间滤波基础)\n    - [3.4.1，线性空间滤波的原理](#341线性空间滤波的原理)\n    - [3.4.2，空间相关与卷积](#342空间相关与卷积)\n  - [3.5，平滑（低通）空间滤波器](#35平滑低通空间滤波器)\n    - [3.5.2，低通高斯滤波器核](#352低通高斯滤波器核)\n    - [3.5.3，低通滤波代码示例](#353低通滤波代码示例)\n  - [3.6，锐化（高通）空间滤波器](#36锐化高通空间滤波器)\n    - [3.6.1，基础](#361基础)\n    - [3.6.2，使用二阶导数锐化图像-拉普拉斯](#362使用二阶导数锐化图像-拉普拉斯)\n    - [3.6.3，钝化掩蔽和高提升滤波](#363钝化掩蔽和高提升滤波)\n    - [3.6.4，使用一阶导数锐化图像-梯度](#364使用一阶导数锐化图像-梯度)\n  - [3.7，低通、高通、带阻核带通滤波器](#37低通高通带阻核带通滤波器)\n  - [3.8，组合使用空间增强方法](#38组合使用空间增强方法)\n- [四，频率域滤波](#四频率域滤波)\n  - [4.1，背景](#41背景)\n  - [4.2，基本概念](#42基本概念)\n  - [4.3，取样和取样函数的傅里叶变换](#43取样和取样函数的傅里叶变换)\n  - [4.8，使用低通频率域滤波器平滑图像](#48使用低通频率域滤波器平滑图像)\n- [五，图像复原与重建](#五图像复原与重建)\n- [六，彩色图像处理](#六彩色图像处理)\n  - [6.1，彩色基础](#61彩色基础)\n  - [6.2，彩色模型](#62彩色模型)\n    - [6.2.1，RGB 彩色模型](#621rgb-彩色模型)\n    - [6.2.2，HSI 彩色模型](#622hsi-彩色模型)\n  - [6.4，全彩色图像处理基础](#64全彩色图像处理基础)\n  - [6.5，彩色变换](#65彩色变换)\n    - [6.5.1，公式](#651公式)\n    - [6.5.4，色调和彩色校正](#654色调和彩色校正)\n    - [6.5.5，彩色图像的直方图处理](#655彩色图像的直方图处理)\n  - [6.6，彩色图像平滑和锐化](#66彩色图像平滑和锐化)\n    - [6.6.1，彩色图像的平滑](#661彩色图像的平滑)\n    - [6.6.2，彩色图像的锐化](#662彩色图像的锐化)\n- [八，图像压缩和水印](#八图像压缩和水印)\n  - [8.1，基础](#81基础)\n    - [8.1.7，图像格式、存储器（容器）和压缩标准](#817图像格式存储器容器和压缩标准)\n  - [8.2，霍夫曼编码](#82霍夫曼编码)\n  - [8.12，数字图像水印](#812数字图像水印)\n- [参考资料](#参考资料)\n> 本文章是对《数字图像处理》书的学习笔记，不涉及具体代码，主要是原理概述和公式描述，及概念理解。学习数字图像处理能让我们更深入理解计算机视觉领域的内容。\n## 一，绪论\n\n### 1.1, 什么是数字图像处理\n\n一副图像可以定义为一个二维函数 $f(x,y)$，其中 $x$ 和 $y$ 是空间（平面）坐标，任意一对空间坐标 $(x,y)$ 处的幅度值 $f$ 称为图像在该坐标点的强度或灰度。当 $x,y$ 和灰度值 $f$ 都是有限的离散量时，我们称该图像为数字图像。**数字图像处理**是指借助于数字计算机来处理数字图像。注意，数字图像由有限数量的元素组成，每个元素都有一定的位置和数值，这些元素称为**像素**。\n\n图像处理止于何处或其他相关领域（如图像分析和计算机视觉）始于何处，目前人们的看法并不一致。在本书中，将**数字图像处理**定义为其输入和输出都是图像的处理。\n\n### 1.2，数字图像处理的起源\n\n略。\n### 1.3，数字图像处理技术应用实例\n\n目前数字图像处理已经应用于各行各业，在本书中，为了更简单表述，将数字图像处理的应用领域根据数字图像源分类（如可见光或 X 射线等）。目前最主要等图像源是电磁波谱，其他重要的图像源还有声波、超声波和电子（电子显微镜中所用的电子束）。\n\n基于电磁波谱辐射的图像是我们比较数字的图像，比如 X 射线和可见光谱段的图像。电磁波可定义为以各种波长传播的正弦波，或可视为无质量的粒子流，每个粒子流以波的形式传播并以光速运动。每个无质量的粒子都包含一定的能量（或一束能量），每束能量称为一个光子。根据每个光子的能量对光谱波段进行分组，得到图 1.5 所示的光谱，其范围从伽玛射线（最高能量）到无线电波（最低能量）。\n\n![电磁波谱](../../data/images/数字图像处理笔记/电磁波谱.png)\n\n根据以上电磁波谱，可得到以下电磁波谱成像：\n\n1. 伽玛射线成像：伽玛射线成像的主要用途包括核医学和天文观测。在核医学中，方法是将放射性同位素注入人体，同位素衰变时会发射伽玛射线，图像则由伽玛射线检测器收集到的放射线产生。下图展示了一幅使用伽玛射线成像得到的人体骨骼扫描图像。\n2. X 射线成像：X 射线成像除了应用于医学诊断，还被广泛应用于工业和其他领域，如天文学。\n3. 紫外波段成像。\n4. 可见光和红外波段成像：应用领域最广，如光学显微镜、遥感图像、气象监测图像、工业自动视觉检测等。\n5. 微波波段成像：主要应用是雷达。\n6. 无线电波成像：应用领域如医学等磁共振成像（MRI）。\n\n一些图像实例效果图如下图所示。\n\n![X射线成像实例](../../data/images/数字图像处理笔记/X射线成像实例.png)\n\n![卫星图像](../../data/images/数字图像处理笔记/卫星图像.png)\n\n### 1.4，数字图像处理的基本步骤\n\n本书中的数字图像处理步骤如下图所示。\n\n![数字图像处理的基本步骤](../../data/images/数字图像处理笔记/数字图像处理的基本步骤.png)\n\n### 1.5，图像处理系统的组成\n\n略。\n\n## 二，数字图像基础\n\n**相机成像的原理**：针孔相机( Pinhole Camera )通过投影变换，可以将三维相机（Camera）坐标转换为二维的图像坐标，这个变换矩阵是相机的内在属性，称为相机内参（Camera Intrinsic） K。\n> yaw 航向角，pitch 俯仰角，roll 翻滾角。\n\n### 2.1，视觉感知要素\n\n通过这节内容了解图像被人类感知的基本原理及人类视觉的物理限制。\n\n#### 2.1.1，人眼的结构\n\n下图显示了人眼的简化剖面图。\n\n![人眼剖析面简图](../../data/images/数字图像处理笔记/人眼剖析面简图.png)\n\n眼镜最靠内部的膜是视网膜，它布满了整个后部的内壁。眼睛聚焦时，来自物体的光在视网膜上成像。模式视觉由分布在视网膜表面上的哥哥分立光感受器提供，分为两类：锥状体和杆状体。锥状体视觉称为**明视觉或亮视觉**。杆状体视觉称为**暗视觉或微光视觉**。\n\n#### 2.1.2，人眼中图像的形成\n\n数码相机中，既有固定焦距的镜头，也有可变焦距的镜头，不同距离的聚焦时通过**改变镜头和成像平面之间的距离**来实现的。在人眼中，晶状体和成像区域（**视网膜**）之间的距离是固定的，正确的聚焦是通过改变晶状体的形状得到（远离压扁晶状体，接近目标则加厚晶状体），晶状体中心和沿视轴的视网膜之间的距离约为 17mm，焦距范围为 14~17 mm。下图所示的几何关系说明了在视网膜上所形成的图像的尺寸。令 $h$ 表示视网膜图像中物体的高度， 根据几何关系：$15/100 = h/17$，得到 h = 2.5 mm。\n\n![人眼观看一棵树的图解](../../data/images/数字图像处理笔记/人眼观看一棵树的图解.png)\n\n#### 2.1.3，亮度适应与辨别\n\n以下两种现象表明人眼的感知亮度不是十几灰度的简单函数：\n- “下冲”或“上冲”现象（马赫带效应）。\n- 同时对比现象。\n\n![马赫带效应图解](../../data/images/数字图像处理笔记/马赫带效应图解.png)\n\n![同时对比的例子](../../data/images/数字图像处理笔记/同时对比的例子.png)\n\n### 2.2，电磁波谱\n\n更详细的电磁波谱图如下图所示。波长常用的单位是米（m），常用的单位是微米（表示为 $\\mu m$，$1\\mu m=10{^-6}m$）。\n\n![更详细的电磁波谱](../../data/images/数字图像处理笔记/更详细的电磁波谱.png)\n\n- 感知的物体颜色由物体反射的光的性质决定。\n- 没有颜色的光称为**单色光或无色光**。\n- 彩色光源的三个属性：频率、辐射、光通量和**亮度**。\n\n### 2.3，图像感知与获取\n\n将照射能量转换为数字图像主要由三种传感器配置：\n- 使用单个传感器获取图像\n- 使用条带传感器获取图像：如磁共振成像（MRI）和正电子发射断层成像（PET）等。\n- 使用阵列传感器获取图像: 如单反相机和手机相机。\n\n![数字图像获取示例图](../../data/images/数字图像处理笔记/数字图像获取示例图.png)\n\n#### 2.3.1，一个简单的成像模型\n\n如 1.1 节所述，我们用形如 $f(x,y)$ 的**二维函数**来表示图像，在空间坐标 (x,y) 处 $f$ 的值是一个标量，其范围 $0\\leq f(x,y) < \\infty$。\n\n### 2.4，图像取样和量化\n\n多少传感器的输出都是**连续**的电压被判刑，这些波形的幅度和空间特性都与正被感测的物理现象相关。要产生一幅数字图像，就需要把感测得到的连续数据转化为数字形式，这包括两个步骤：**取样和量化**。\n\n#### 2.4.1，取样和量化的概念\n\n一幅连续图像 $f$，对坐标值进行数字化称为**取样**（或采样），对幅度值进行数字化称为**量化**。\n#### 2.4.2，数字图像表示\n\n在计算机中，数字图像可用一个 $M\\times N$矩阵表示，图像长为 $M$，宽为 $N$，矩阵中的每个元素即为图像的像素。\n\n![image](../../data/images/数字图像处理笔记/E1wFKZwvxhU5X1Mwegn-ZsPJw11ZMaKQHbz1QOgphTs.png)\n\n### 2.5，像素间的一些基本关系\n\n- 像素的相邻像素\n- 邻接、连通、区域和边界\n- 距离测度：两个像素的距离，通过欧几里得（欧式）距离计算：$D(p,q) = \\sqrt{[(x-u)^2 + (y-v)^2]}$\n### 2.6，数字图像处理所用的基本数学工具\n\n#### 2.6.1，对应元素运算和矩阵运算\n\n涉及一幅或多幅图像的对应元素运算是逐个像素操作的，有因为在数字图像处理中，图像可以等效为矩阵，所以**图像之间的运算是可以用矩阵理论执行的**。\n\n#### 2.6.2，线性运算和非线性运算（一般两个图像之间）\n\n![图像线性运算举例](../../data/images/数字图像处理笔记/图像线性运算举例.png)\n\n线性运算更为重要，包含了大量适用于图像处理的理论与实践成果；非线形运算范围比较有限。\n#### 2.6.3，数字图像处理数学工具-算术运算（一般两个图像之间）\n算术运算常用在特定的天文、医学等领域，将两幅图像经过算术运算从而得到更为清晰的图像，两幅图像的算术运算表示如下：\n\n![image](../../data/images/数字图像处理笔记/HjO-Dj6PoIUijUdtb4dPIvsHrwS1iuYiVyz6W-fvWh0.png)\n\n这些加减乘除运算都是对应的像素运算，算术运算一般有以下应用：\n\n1. 使用图像相加（平均）降低噪声。\n2. 使用图像相减比较图像。\n3. 使用图像相乘/相除校正阴影和模板。\n\n`3` 种算法运算的实际应用效果对比图如下所示：\n\n![使用图像相加（平均）降低噪声](../../data/images/数字图像处理笔记/fYxYGO2c8Y5toDIC_50NtkqyN2Tv6rMAtHZoBk4sOO8.png)\n\n![使用图像相减比较图像](../../data/images/数字图像处理笔记/TI7MEbJFOCNf1ibzQzCigqmo4Ci2Ov79FPLQThoTAKQ.png)\n\n![使用图像相乘/相除校正阴影和模板](../../data/images/数字图像处理笔记/H9GI41WhwnP1MX9nAZpU39IoDuL1MXvr9vgHg0ZaWro.png)\n\n#### 2.6.4，集合运算和逻辑运算（一般两个图像之间）\n**注意这里的集合运算针对的是二值图像，或者图像中所有像素具有相同的灰度值且**。\n\n假设有两个集合 A 和，在数字图像处理中常见的集合运算有：\n- 交集运算: $C = A\\cap B$，满足交换律、结合律和分配律。\n- 并集运算: $C = A\\cup B$。\n\n如果想要知道一幅**二值**图像中的两个目标 A 和 B 是否重叠，可通过计算 A\\cap B$。如果结果不是空集，则可确定两个目标的某些元素是有重叠的。\n#### 2.6.5，空间运算（单幅图像）\n\n空间运算是直接对单幅图像的像素执行数学操作，分为三类：（1）单像素运算；（2）领域运算；（3）几何空间运算。\n\n##### 2.6.5.1，单像素操作\n用一个变化函数$T$改变图像中各个像素的灰度: \n\n$$s = T(z)$$\n\n上述公式对应单像素操作，$z$是原图像中像素的灰度，$s$是处理后图像中对应像素的（映射）的灰度。\n\n##### 2.6.5.2，领域运算\n令 $S_{xy}$ 代表图像 $f$中以任意一点 $(x,y)$ 为中心的一个邻域的做标集，领域处理后，输出图像$g$中的相同坐标处会生成一个新的像素，该像素的值由输入图像中邻域像素的规定运算和集合$S_{xy}$中的坐标确定。假设**领域运算**对应的是计算大小为$m\\times n$、中心为$(x,y)$的矩形领域中像素的平均值，且这个区域中的像素坐标是集合$S_{xy}$的元素，那么其对应的领域运算公式如下：\n\n$$g(x,y) = \\frac{1}{mn} \\sum_{(r,c)\\in S_{xy}}f(r,c)$$\n\n上述公式中，$r$和$c$是像素的行和列坐标，属于集合$S_{xy}$，图像$g$是通过移动坐标$(x,y)$，使得领域的中心逐个移过图像$f$中的所有像素，然后在每个新位置都重复这一**领域运算**得到，对应的示意图如下：\n\n![image](../../data/images/数字图像处理笔记/5edQ4JyZCq1G_e-g9X4tVq7UkMvpmdEgDRu43MicH60.png)\n\n简单理解所谓的领域运算就是对特定 roi 区域的所有像素，做特定操作，而这个操作就是以指定位置 $(x,y)$ 为中心，邻域范围为 $m\\times n$，对这个范围内的像素取平均/求和/最大值/等。\n> 典型的就是 `CNN` 模型中卷积层的滤波器操作。\n\n##### 2.6.5.3，几何变换\n**几何变换即改变图像中像素的空间排列**，由两种基本运算组成：\n\n1. 坐标的空间变换；\n2. 灰度内插，即为变换后的像素赋灰度值（灰度图）。\n\n坐标变换公式可表示为：\n\n![image](../../data/images/数字图像处理笔记/Jot2qX9Wcf4zhC9KI7JrOitH_Gf9xlqehCb4nllSJEM.png)\n\n最为重要的是**放射变换**，它包括**缩放变换、平移变换、旋转变换和剪切变换**。式(2.44)无法表示平移变换（需要在公式右侧添加一个常数二维向量），所以需将上式升级为，如下所示的齐次坐标变换。\n\n![image](../../data/images/数字图像处理笔记/GTqFA1EvohGD4qBLAFZ0LIQ5BvmnSHjJ9IbFWUIJY30.png)\n\n常见图像几何操作对应的仿射矩阵 `A` 、变换坐标公式以及示意图如下表所示：\n\n![image](../../data/images/数字图像处理笔记/6z27mYVrISoA1cWrz6m0b6WBnEYTy1Wzz0diyCiY_38.png)\n\n## 三，灰度变换与空间滤波\n\n空间域指的是图像平面本身，空间域中的图像处理方法是**直接对图像中的像素进行处理**。空间域图像处理的两个主要类别是:\n- **灰度变换**: 如对比度处理和图像阈值处理等任务，直接对图像的给个像素进行操作。\n- **空间滤波**: 如图像平滑和锐化，对图像中的每个像素的邻域进行操作。\n\n### 3.1，背景\n\n本章中讨论的所有图像处理技术都是在空间域中实现的，所谓的空间域即包含图像中像素的平面。空间域技术直接操作图像中的像素，而频率域技术操作的是图像的傅立叶变换而非图像本身。\n> 由图像的坐标张成的实平面部分称为空间域，$x$和$y$称为空间变量或空间坐标。\n\n尽管灰度变换和空间滤波的应用范围广泛，但本书中的大多数例子是关于图像增强的。所谓图像增强技术，是为了某些特定应用对原图像进行加工的技术，不具备通用性。\n\n### 3.2，一些基本的灰度变换函数\n\n通过灰度变换函数$T$将原来的像素值$r$映射为像素值$s$。灰度变换中常用的 `3` 类基本函数是线性（反转和恒等变换: 输入灰度和输出灰度相同）函数、对数（对数和反对数变换）函数和幂律（n次幂或n次根）函数。\n\n![基本的灰度变换函数](../../data/images/数字图像处理笔记/基本的灰度变换函数.png)\n\n#### 3.2.1，图像反转\n\n假设原图像像素值为$r$，灰度级在区间为 [0, L-1]，则起反转后的图像形式为\n$$s=L-1-r$$\n\n图像反转实例效果图如下所示：\n\n![图像反转实例](../../data/images/数字图像处理笔记/图像反转实例.png)\n\n#### 3.2.2，对数变换\n\n对数变换的形式如下：\n\n$$s = clog(1+r)$$\n\n图3.3中对数曲线的形状表明，对数变换会将输入中范围较窄的低灰度值映射为输出中范围较宽的灰度级。例如区间 [0, L/4]中的输入灰度级映射到 [0, 3L/4] 中的输出灰度级；相反输入中的高灰度级被映射为输出中范围较窄的灰度级。\n\n#### 3.2.3，幂律变换（伽玛变换）\n\n幂律变换形式如下：\n\n$$s = cr^\\gamma $$\n\n#### 3.2.4，分段线性变换函数\n\n1. **对比度拉伸**\n对比度拉伸可以拓展图像中的灰度级范围，使其覆盖记录介质或显示设备的整个理想灰度范围。\n\n![对比度拉伸](../../data/images/数字图像处理笔记/对比度拉伸示例.png)\n\n2. **灰度级分层**\n\n有些图像增强应用的目的是为了突出图像中的特定灰度空间，比如增强卫星图像中的特征、增强 X 射线图像中的缺陷等。灰度级分层可以基于两个基本方法及其变体来实现。\n- 一种方法是将感兴趣范围内的所有灰度值显示为一个值（如白色），而将其他范围的灰度值显示为另一个值（黑色），这种变换会得到一个二值图像。\n- 另一种方法是基于图3.11(b)中的变换，使期望的灰度范围变亮（或变暗），但保持图像中的其他灰度级不变。\n\n![灰度分层变换函数](../../data/images/数字图像处理笔记/灰度分层变换函数.png)\n\n灰度级分层的实际应用例子如下图所示\n> 我个人感觉这个应用得根据实际专业场景结合起来使用，难点在于**灰度级范围的选择**。\n\n![灰度级分层的实际应用](../../data/images/数字图像处理笔记/灰度级分层的实际应用.png)\n\n3. **比特平面分层**\n\n略。\n\n### 3.3，直方图处理\n\n令 $r_k(k = 0,1,2...L-1)$ 表示一幅$L$级灰度数字图像 $f(x,y)$ 的灰度。 $f$ 的**非归一化直方图**定义为\n\n$$h(r_{k}) = n_{k}, k = 0,1,2...L-1$$\n\n式中,$n_k$是$f$中灰度为$r_k$的像素的数量，并且细分的灰度级称为直方图容器。类似地，归一化直方图定义为\n\n$$p(r_{k}) = \\frac{h(r_{k})}{MN} = \\frac{n_{k}}{MN}$$\n\n式中，$M$ 和 $N$ 分别是图像的行数和列数。对$k$的所有值，$p(r_{k})$的和总是 1.\n\n下显示了具有 4 个基本灰度特性的图像: \n\n![直方图示例](../../data/images/数字图像处理笔记/直方图示例.png)\n\n从上图的分析我么可以得出这样一个结论: **即像素占据整个灰度级范围并且均匀分布的图像，将具有高对比度的外观和多种灰色调**。最终结果将是显示了大量灰度细节并具有高动态范围的一副图像。\n\n#### 3.3.1，直方图均衡化\n\n1，直方图均衡化所用的**变换函数**如下（推导过程复杂，跳过，感兴趣的可以阅读原书了解过程）\n\n![直方图均衡化所用的变换函数](../../data/images/数字图像处理笔记/直方图均衡化所用的变换函数.png)\n\n2，**直方图均衡化的目的**是为了生成一幅具有均匀直方图的输出图像。\n\n3，直方图均衡化效果示例如下图所示:\n\n![直方图均衡化效果示例](../../data/images/数字图像处理笔记/直方图均衡化效果示例.png)\n\n![直方图均衡化效果示例2](../../data/images/数字图像处理笔记/直方图均衡化效果示例2.png)\n\n**直方图均衡化效果总结分析**：尽管 4 个直方图都不同，但直方图均衡化后的图像是很相似的，因为原来的 4 个图的基本区别是对比度而非内容。\n\n#### 3.3.2，直方图匹配（规定化）\n\n**直方图匹配（规定化）定义**：用于生成具有规定直方图的图像的方法称为直方图匹配（规定化）。\n\n直方图规定化的推导过程较为复杂，请参考原书。本笔记中，直接看特定的一幅图像经过直方图均衡化和规定化的对比结果。\n\n1，直方图均衡化后的效果图如下所示（有噪声）：\n\n![直方图均衡化后的效果图](../../data/images/数字图像处理笔记/直方图均衡化后的效果图.png)\n\n2，直方图规定化后的效果图如下所示（无噪声）:\n\n![直方图规定化效果图](../../data/images/数字图像处理笔记/直方图规定化效果图.png)\n\n#### 3.3.3，局部直方图处理\n\n直方图均衡化和直方图规定化的直方图处理方法都是全局性的，因为像素是由基于整个图像的灰度分布的变换函数修改的。当目的是为了解决**增强图像中几个小区域的细节**时，解决方法是设计**基于像素邻域的灰度分布的变换函数**。\n\n局部直方图的处理过程:\n1. 定义一个邻域，并将其中心在水平方向或垂直方向上从一个像素移动到另一个像素。\n2. 在每个位置，计算邻域中的各点的直方图，得到直方图均衡化或直方图规定化变换函数，将这个函数映射于邻域中心点像素的灰度。\n3. 然后将邻域的中心移到一个相邻像素位置，并重复上述过程。\n\n局部直方图均衡化效果如下图所示:\n\n![局部直方图均衡化效果](../../data/images/数字图像处理笔记/局部直方图均衡化效果.png)\n\n#### 3.3.4，使用直方图统计增强图像\n直接从图像直方图得到的统计量信息可用于增强图像。令$r$是一个离散型随机变量，它表示区间$[0,L-1]$内的灰度值；令$p(r_i)$是相对于灰度值$r_i$的归一化直方图分量。即$p(r_i)$可视为灰度$r_i$的概率密度函数，并可得到图像的直方图。\n\n1，**均值是平均灰度的测度**，图像像素灰度的均值$m$计算公式如下:\n$$m = \\sum_{i=0}^{L-1}r_{i}p(r_i)$$\n\n2，**方差（或标准差$\\sigma$）是图像对比度的测度**，方差公式如下:\n\n$$\\sigma^2 = \\mu_2 = \\sum_{i=0}^{L-1}(r_{i}-m)^{2}p(r_i)$$\n\n> 简单理解图像灰度均值和方差的意义就是，均值越大，图像越亮；方差越大，图像对比度越高。\n\n上述公式是针对图像全局的，其同样可应用于一个规定大小的邻域空间。结合以上公式和概念可以使用直方图统计增强局部图像，示例如下:\n\n![使用直方图统计增强局部图像](../../data/images/数字图像处理笔记/使用直方图统计增强局部图像.png)\n\n令$f(x,y)$表示原图在图像坐标$(x,y)$处的灰度值，令$g(x,y)$表示增强后的图像在这些坐标处的灰度值，具体的图像增强公式如下:\n\n![局部图像增强过程](../../data/images/数字图像处理笔记/局部图像增强过程.png)\n> 更详细的具体操作方法细节参考原书，本文略过。\n\n### 3.4，空间滤波基础\n\n> 本节内容讨论如何使用空间滤波器进行图像处理。**滤波有时要分多个阶段完成**。\n\n滤波器一词来自频率域处理（第四章），滤波的意思是指通过修改或抑制图像的规定分量。例如，通过低频的滤波器称为**低通滤波器**。低通滤波器的作用是**通过模糊图像**来平滑图像，使用空间滤波器可以**直接**对图像本身进行类似效果的平滑处理。\n\n#### 3.4.1，线性空间滤波的原理\n\n**线性空间滤波定义**: 指图像$f$与滤波器核$w$进行乘积之和（卷机）运算。核是一个阵列，其大小定义了运算的邻域，其系数决定了该滤波器（也称模板、窗口滤波器）的性质。\n\n下图3.28说明了使用$3\\times3$核进行**线性空间滤波的原理**。在图像任何一点$(x,y)$处，滤波器的响应$g(x,y)$是核系数核核所覆盖图像像素的乘积之和:\n\n$$g(x,y) = w(-1,-1)f(x-1,y-1)+w(-1,0)f(x-1,y)+...+w(0,0)f(x,y)+...+w(1,1)f(x+1,y+1) \\tag{3.30}$$\n\n核的中心系数值 $w(0,0)$ 对应于 $(x,y)$ 处的像素。对应大小为 $m\\times n$ 的核，假设 $m=2a+1$ 和 $n=2b+1$，其中$a$和$b$是非负整数。一般来说，大小为 $m\\times n$ 的核对大小为 $M\\times N$ 的图像的线性空间滤波可表示为:\n\n$$g(x,y) = \\sum_{s=-a}^{a}\\sum_{t=-b}^{b} w(s,t)f(x+s, y+t) \\tag{3.31}$$\n\n上式中 $x$ 和 $y$ 发生变化，使得滤波器核的中心（原点）能够遍历完图像 $f$ 中的每个像素。\n#### 3.4.2，空间相关与卷积\n\n图3.28以图形方式说明了空间相关，式(3.31)给出了其数学描述。相关的运算过程如下：在图像上移动核中心，并且在每个位置计算乘积之和。\n\n![使用3x3核的线性空间滤波原理](../../data/images/数字图像处理笔记/使用3x3核的线性空间滤波原理.png)\n\n以一维例子开始，则式(3.31)变为\n\n$$g(x) = \\sum_{s=-1}^{a}w(s)f(x+s)$$\n\n式中，卷机核为$w$，图像函数为$f$。\n\n![二维滤波器核与图像进行卷积](../../data/images/数字图像处理笔记/二维滤波器核与图像进行卷积.png)\n\n在本书中，当我们使用线性空间滤波这个术语时，指的是滤波器核与图像进行卷机（乘积和）运算。\n### 3.5，平滑（低通）空间滤波器\n\n平滑（也称平均）空间滤波器用于降低灰度的急剧过渡，因为随机噪声通常就是由灰度的急剧过渡组成，所以平滑的一个明显应用就是**降噪**。\n\n本节介绍基于可分离盒式核和高斯核的低通滤波器。\n\n1，**基于可分离盒式核的低通滤波器**\n最简单的可分离低通滤波器核是**盒式核**，其系数的值相同（通常为1）。\n\n下图3.31(a)中显示了一个大小为$3\\times 3$的盒式滤波器，即一个大小为$m\\times n$的盒式滤波器且元素值为 1 的一个$m\\times n$阵列，器前面有一个归一化的常数，它的值是 1 除以系数值之和（当所有系数都为1时，这个常数为$1/mn$）。\n\n![平滑核的例子](../../data/images/数字图像处理笔记/平滑核的例子.png)\n\n使用不同盒式核对图像进行低通滤波的效果图如下所示:\n\n![使用不同盒式核对图像进行低通滤波](../../data/images/数字图像处理笔记/使用不同盒式核对图像进行低通滤波.png)\n\n盒式滤波器较为简单，适合快速实验，其会产生视觉上能够接受的平滑效果。盒式滤波器有一些局限:\n1. 对透镜模糊特性的近视能力较差。\n2. 盒式滤波器往往沿垂直方向模糊图像，不适合精细细节或具有强几何分量的图像应用。\n\n对应的 opencv 函数如下。\n```cpp\nvoid boxFilter( InputArray f, OutputArray dst, int ddepth,\n                             Size ksize, Point anchor=Point(-1,-1),\n                             bool normalize=true,\n                             int borderType=BORDER_DEFAULT );\n```\n\n参数 6 的解释：\n- 当 `normalize = true` 时，盒式滤波就变成了均值滤波。也就是说，**均值滤波是盒式滤波归一化（normalized）后的特殊情况**。其中，归一化就是把要处理的量都缩放到一个范围内，比如(0,1)，以便统一处理和直观量化。\n- 当 `normalize = false` 时，为非归一化的盒式滤波，用于计算每个像素邻域内的积分特性，比如密集光流算法（dense optical flow algorithms）中用到的图像倒数的协方差矩阵（covariance matrices of image derivatives）。\n\n> 均值滤波，是最简单的一种线性滤波操作，输出图像的每一个像素是核窗口内输入图像对应像素的像素的平均值( 所有像素加权系数相等)，实质就是归一化后的方框滤波。均值滤波算法比较简单，计算速度快，但是均值滤波本身存在着固有的缺陷，即**它不能很好地保护图像细节**，在图像去噪的同时，也破坏了图像的细节部分，从而使图像变得模糊，不能很好地去除噪声点。但均值滤波对周期性的干扰噪声有很好的抑制作用。\n\n\n#### 3.5.2，低通高斯滤波器核\n实际应用中要求卷积核是各向同性的（圆对称），其响应与方向无关。高斯核是唯一可分离的圆对称核，因此非常适合图像处理，对于**抑制服从正态分布的噪声非常有效**。高斯核定义:\n\n$$w(s,t) = G(s,t) = Ke^{-\\frac{s^2+t^2}{2\\sigma^2}}$$\n\n令$r=[s^2+t^2]^{1/2}$，上式可写为\n\n$$G(r) = Ke^{-\\frac{r^2}{2\\sigma^2}}$$\n\n这个函数依然是圆对称的，变量$r$表示从中心到函数$G$上任意一点的距离，它必须是正数且是奇数。下图显示了不同大小的核的$r$值。\n\n![不同大小方形核到中心的距离](../../data/images/数字图像处理笔记/不同大小方形核到中心的距离.png)\n\n**希望产生更均匀的平滑结果时，通常使用高斯核平滑**；盒式核平滑则是硬过渡。\n\n高斯核和盒式核平滑特性的比较对比图如下:\n\n![高斯核和盒式核平滑特性的比较对比图](../../data/images/数字图像处理笔记/高斯核和盒式核平滑特性的比较对比图.png)\n\n#### 3.5.3，低通滤波代码示例\n\n盒式滤波、均值滤波（归一化后的盒式滤波）、高斯滤波的 `python-opencv` 应用代码如下。\n\n```python\nimport cv2 as cv\nimport numpy as np\nfrom matplotlib import pyplot as plt\n\nimg = cv.imread('test.png')\n\nblur1 = cv.boxFilter(img, -1 ,(3,3),normalize = False)\nblur2 = cv.boxFilter(img, -1 ,(3,3),normalize = True)\nblur3 = cv.GaussianBlur(img,(5,5),0)\n\nplt.figure(figsize=(20,20)) #设置窗口大小\n\nplt.subplot(221),plt.imshow(img),plt.title('Original')\nplt.xticks([]), plt.yticks([])\n\nplt.subplot(222),plt.imshow(blur1),plt.title('boxFilter_normalize_false')\nplt.xticks([]), plt.yticks([])\n\nplt.subplot(223),plt.imshow(blur2),plt.title('boxFilter_normalize_true')\nplt.xticks([]), plt.yticks([])\n\nplt.subplot(224),plt.imshow(blur3),plt.title('Gaussian')\nplt.xticks([]), plt.yticks([])\n\nplt.show()\n```\n程序运行后输出如下：\n\n![低通滤波代码示例](../../data/images/数字图像处理笔记/低通滤波代码示例.png)\n\n非归一化的时候，得到图像就是一片白色，对源图像毁坏太大，根本无法使用。而归一化的时候，得到图像是一种模糊的效果，此时与均值滤波一样。\n\n### 3.6，锐化（高通）空间滤波器\n\n**锐化的作用**是突出灰度中的过渡。前面讲的平滑（低通）滤波（图像模糊）通过积分运算实现的，那可以推断出图像锐化可以用微分实现。\n\n#### 3.6.1，基础\n\n**导数**（英语：derivative）是微积分学中的一个概念。函数在某一点的导数是指这个函数在这一点附近的变化率。导数的本质是通过极限的概念对函数进行局部的线性逼近。当函数$f$的自变量在一点 $x_{0}$ 上产生一个增量 $h$ 时，函数输出值的增量与自变量增量 $h$ 的比值在 $h$ 趋于 0 时的极限如果存在，即为 $f$ 在 $x_{0}$ 处的导数，记作 $f'(x_{0})$ 或 $\\frac{\\mathrm{d} f}{\\mathrm{d} x}x_0$ 或 $\\frac{\\mathrm{d} f}{\\mathrm{d} x}\\mid_{x=x_0}$。例如在运动学中，物体的位移对于时间的导数就是物体的瞬时速度。\n\n导数是函数的局部性质。不是所有的函数都有导数，一个函数也不一定在所有的点上都有导数。若某函数在某一点导数存在，则称其在这一点可导(可微分)，否则称为不可导(不可微分)。如果函数的自变量和取值都是实数的话，那么函数在某一点的导数就是该函数所代表的曲线在这一点上的切线斜率。\n\n![斜率与微积分学](../../data/images/数字图像处理笔记/斜率与微积分学.png)\n\n**导数一般定义如下**：\n\n直观上 $f(x)-f(a)$ 代表函数值从$a$到$x$的变化量，那么，\n$$\\frac{f(x)-f(a)}{x-a}$$\n代表的是从 $a$ 到 $x$ 的平均变化率。若实函数$f$于实数$a$有定义，且以下极限:\n$$\\lim_{x\\rightarrow a} \\frac{f(x)-f(a)}{x-a}$$\n存在则称 $f$ 在 $a$ 处可导。\n> 来源维基百科[导数定义](https://zh.wikipedia.org/wiki/%E5%AF%BC%E6%95%B0)。\n\n一维函数$f(x)$的一阶导数的一个基本定义是**差分**：\n$$\\frac{\\partial f}{\\partial x} = f(x+1)-f(x) \\tag{3.48}$$\n\n同理可将$f(x)$的**二阶导数定义为差分**:\n\n$$\\frac{\\partial ^{2}f}{\\partial x^{2}} = f(x+1)+f(x-1)-2f(x) \\tag{3.49}$$\n\n#### 3.6.2，使用二阶导数锐化图像-拉普拉斯\n\n使用二阶导数锐化图像的方法：首先定义二阶导数的离散公式，然后在这个公式的基础上构造一个滤波器核。对于两个变量的函数（图像$f(x,y)$）其中拉普拉斯算子（核）的定义如下:\n$$ \\Delta f= \\frac{\\partial ^{2}f}{\\partial x^{2}}+ \\frac{\\partial ^{2}f}{\\partial y^{2}} \\tag{3.50}$$\n\n因为任意阶的导数都是线性算子，所以**拉普拉斯是线性算子**。\n\n由差分和拉普拉斯二阶导数的定义可得如下公式:\n\n$$\\Delta f = f(x+1, y) + f(x-1, y) + f(x, y+1) + f(x, y-1)-4f(x,y) \\tag{3.53}$$\n\n上述公式可以用图3.45(a)中的核进行卷积运算来实现；因此，图像锐化的滤波原理类似于3.5节内容所述的低通滤波，**只是这里使用不同的核系数**。\n\n![拉普拉斯核矩阵](../../data/images/数字图像处理笔记/拉普拉斯核矩阵.png)\n\n**使用拉普拉斯算子锐化图像的方法描述如下**:\n\n![使用拉普拉斯算子锐化图像的方法](../../data/images/数字图像处理笔记/使用拉普拉斯算子锐化图像的方法.png)\n> 直接的拉普拉斯图像往往是黑色和无特征的。\n\n拉普拉斯应用示例如下:\n\n![拉普拉斯应用示例](../../data/images/数字图像处理笔记/拉普拉斯应用示例.png)\n\n#### 3.6.3，钝化掩蔽和高提升滤波\n\n从原图像减去一幅钝化（平滑后）图像，是20世纪30年代以来印刷和出版业一直用来锐化图像的方法，这个过程称为**钝化掩蔽**，步骤如下：\n1. 模糊原图像。\n2. 从原图像减去模糊后的图像（产生的差称为模板）。\n3. 将模板与原图像相加。\n\n![钝化掩蔽公式](../../data/images/数字图像处理笔记/钝化掩蔽公式.png)\n> 注意较大的 k 值会产生令人无法接受的图像。\n\n钝化掩蔽效果示例：\n\n![钝化掩蔽效果示例](../../data/images/数字图像处理笔记/钝化掩蔽效果示例.png)\n\n#### 3.6.4，使用一阶导数锐化图像-梯度\n\n包括罗伯特交叉梯度算子和 `Sobel` 算子，使用一阶梯度算子锐化图像常用在工业缺陷检测。梯度算子核系数矩阵如下:\n\n![梯度算子核系数矩阵](../../data/images/数字图像处理笔记/梯度算子核系数矩阵.png)\n\n### 3.7，低通、高通、带阻核带通滤波器\n\n![四种空间域滤波器原理](../../data/images/数字图像处理笔记/四种空间域滤波器原理.png)\n### 3.8，组合使用空间增强方法\n\n**使用拉普拉斯来突出细节，使用梯度来增强突出的边缘**。\n\n## 四，频率域滤波\n\n### 4.1，背景\n\n- **傅里叶级数**: 任何**周期函数**都可表示为不同频率的正弦函数和/或余弦函数之和，其中每个正线函数和/或余弦函数都乘以不同的系数（我们现在称该和为傅里叶级数）。\n- **傅里叶变换**: 任何**非周期函数**都可表示为正弦函数和/或余弦函数乘以加权函数的积分。\n略。\n\n### 4.2，基本概念\n\n1. 复数\n2. 傅里叶级数\n3. 冲激函数及其取样（筛选）性质\n4. 连续单变量函数的傅里叶变换\n5. 卷积\n\n### 4.3，取样和取样函数的傅里叶变换\n\n略。\n\n### 4.8，使用低通频率域滤波器平滑图像\n\n图像中的边缘和其他急剧的灰度变化（如噪声）主要影响其傅里叶变换的高频内容，因此，在频率域中是通过衰减高频（即低通滤波）来实现平滑（模糊）的。\n\n## 五，图像复原与重建\n\n**图像增强主要是一种主观处理，而图像复原很大程度上是一种客观处理技术**。\n\n## 六，彩色图像处理\n### 6.1，彩色基础\n光的特性是色彩科学的核心，如果光是无色（无颜色）的，那么其属性就只有亮度或数值，具体体现就是20世纪20年度的黑白电影。其中术语灰度（或亮度）级是关于（从黑色变为灰色最终变为白色）灰度的一个测量，是一个标量。\n\n彩色光在电磁波谱中的波长范围是 400～700nm，描述光源质量的 3 个基本量是辐射亮度、发光强度和亮度。人眼的生理结构特性表现为其对红色、绿色和蓝色光更为敏感，该特性使得人眼看到的颜色是三原色 \\[红 R、绿 G、蓝 B\\] 的不同组合。三原色相加可以产生光的二次色，如深红色（R+B）、青色（G+B）和黄色（R+G）。\n\n![image](../../data/images/数字图像处理笔记/0H81XJO6GYhP50aGf-FQHZkb6-bQwClofhdU7Lu2NJs.png)\n\n区分不同颜色的特性通常是**亮度、色调和饱和度**，色调与饱和度一起称为色度。\n\n* **亮度**：亮度体现的是发光强度（灰度级）的消色概念。\n* **色调**：色调是混合光波中与主导波长相关的属性，表示被观察者感知的主导色。即，当我们说一个物体颜色说红色、橙色或红色时，说的就是物体的色调。\n* **饱和度**：饱和度指的是相对的纯度（纯色被白光稀释的程度），或与一种色调混合的白光量。纯光谱颜色是完全饱和的，非光谱颜色如淡紫色（紫色加白色）是不饱和度，饱和度与所加白光量成反比。\n\n### 6.2，彩色模型\n彩色模型（也称彩色空间或彩色系统）的目的是以某种标准的方式来方便地规定颜色。彩色模型本质上规定：（1）坐标系；（2）坐标系内的字空间，模型内的每种颜色都可由字空间内包含的一个点来表示。数字图像处理中的面向硬件的彩色模型常用有以下几种：\n\n* 针对彩色显示器和彩色摄像机开发的 `RGB`（红色、绿色、蓝色）模型。\n* 针对彩色打印机开发的 `CMY`（青色、深红色、黄色）模型和 `CMYK`（青色、深红色、黄色、黑色）模型。\n* 针对人们描述和解释颜色的方式开发的 `HSI` （色调、饱和度、亮度）模型。`HSI`模型有一个优点：它能够解除图像中颜色和灰度级信息的联系。\n\n#### 6.2.1，RGB 彩色模型\n在 RGB 模型中，每种颜色都以其红色、绿色和蓝色光谱成分显示，该模型是根据笛卡尔坐标系建立的。RGB 彩色立方体简图如下所示：\n\n![image](../../data/images/数字图像处理笔记/De1j2N5zQOoKqq3t9MNyQNWqGsmiQ_d7YMMYdC3bnqA.png)\n\n全彩色图像通常是指一幅 `24` 比特的 `RGB` 彩色图像（每幅分量图像都是 `8` 位，值域是 \\[0, 255\\]），颜色总数自然是 $(2^8)^3$=16777216。\n\n#### 6.2.2，HSI 彩色模型\n`RGB` 、`CMY` 和 `CMYK` 模型适合硬件实现但是并不能很好的描述人类世纪解释的颜色；观察彩色物体时，我们会用色调、饱和度和亮度来描述这个物体，由此提出了 `HSI` 彩色模型（色调、饱和度、亮度）。\n\n从 RGB 到 HSI 的彩色变换的公式如下：\n\n![image](../../data/images/数字图像处理笔记/ZO70cfCHXWRi38NrzptOeRj6W_pVwcv1w9l6wx82itc.png)\n\n### 6.4，全彩色图像处理基础\n\n全彩色图像处理方法主要分为两种。第一种方法是首先分布处理每幅灰度级分量图像，然后将处理后的各幅分量图像合成为一幅彩色图像。第二种方法是直接处理彩色像素。因为全彩色图像至少有 `3` 个分量，因此彩色像素是向量。全彩色图像的公式定义如下：\n\n![RGB彩色模型公式](../../data/images/数字图像处理笔记/RGB彩色模型公式.png)\n\n### 6.5，彩色变换\n\n本节中描述的技术是在单个彩色模型中处理彩色图像的各个分量，而不是像6.2节中那样在彩色模型之间进行彩色变换。\n\n#### 6.5.1，公式\n\n使用如下公式对多光谱图像的颜色变换建模:\n$$s_i = T_i(r_i), i=1,2,...,n$$\n式中，$n$是分量图像的总数，$r_i$是输入分量图像的灰度值，$s_i$是输出分量图像中空间上的对应灰度，$T_i$是对$r_i$操作来产生$s_i$的一组变换或颜色映射函数。\n\n#### 6.5.4，色调和彩色校正\n\n只有校正了图像的色调范围，才能解决图像中颜色的不规则问题，如过饱和颜色和欠饱和颜色问题。图像中的色调范围（也称主特性），是指图像中颜色亮度的一般分布。高主特性图像中的大部分信息集中在高亮度上；低主特性图像中的大部分信息集中在低亮度上；中主特性图像的颜色则介于前两者之间。我们通常希望**彩色图像的亮度在高光和阴影之间是等间隔分布的**。\n\n![色调变换示例](../../data/images/数字图像处理笔记/色调变换示例.png)\n\n**彩色平衡**。图像的色调特性完成之后，通常要解决彩色不平衡的问题。校正彩色不平衡的方法有很多种，调整一度彩色图像的彩色分量时，要意识到每个操作都会影响图像的整体彩色平衡。也就是说，**对一种颜色的感知会受到周围颜色的影响**。\n\n#### 6.5.5，彩色图像的直方图处理\n\n和前一节内容是**交互式增强**方法（手动调节感知合适与否）不同，3.3 节的灰度直方图处理变换可自动地应用于彩色图像。\n> 直方图均衡化会通过一个变换函数，输出一幅具有均匀灰度值直方图的图像。\n\n`HSI` 彩色空间更适合均匀地分布颜色亮度，同时保持**颜色本身（即色调）**不变。\n\n![彩色空间的直方图均衡化](../../data/images/数字图像处理笔记/彩色空间的直方图均衡化.png)\n\n### 6.6，彩色图像平滑和锐化\n\n彩色变换是变换图像中的每个像素而不考虑像素的邻域，那么下一步自然是根据周围像素（邻域）的特性来修改像素的值。本节内容通过彩色图像的平滑和锐化处理来说明这类邻域处理的基本知识。\n\n#### 6.6.1，彩色图像的平滑\n灰度级图像的平滑（空间滤波）概念同样可适用于全彩色图像处理，只是我们处理的不再是标量灰度值，而是式(6.37)给出的分量向量。彩色图像平滑公式描述如下:\n\n![彩色图像平滑公式](../../data/images/数字图像处理笔记/彩色图像平滑公式.png)\n#### 6.6.2，彩色图像的锐化\n\n向量分析表明，一个向量的拉普拉斯也是一个向量，其分量等于输入向量的各个标量分量的拉普拉斯。在 RGB 彩色系统中，式(6.37)中向量$c$的拉普拉斯为\n\n$$\\bigtriangledown^2 [c(x,y)] = \\begin{bmatrix}\n\\bigtriangledown^2 R(x,y)\\\\ \n\\bigtriangledown^2 G(x,y)\\\\ \n\\bigtriangledown^2 B(x,y) \n\\end{bmatrix}$$\n\n上式(6.37)表明分别计算每幅图像的拉普拉斯，即可求出全彩色图像的拉普拉斯。使用拉普拉斯锐化彩色图像示例如下:\n\n![使用拉普拉斯锐化彩色图像示例](../../data/images/数字图像处理笔记/使用拉普拉斯锐化彩色图像示例.png)\n\n## 八，图像压缩和水印\n\n**图像压缩是一种减少表示一幅图像所需数据量的技术与课业**。本章内容是介绍性的，适用于图像和视频应用。\n\n### 8.1，基础\n\n二维灰度矩阵是我们查看和解释图像的首选格式，并且是评判其他表示的标准。二维灰度阵列受如下能被识别和利用的三种主要数据冗余的影响：\n1. **编码冗余**。\n2. **空间和时间冗余**。\n3. **无关信息**。\n\n#### 8.1.7，图像格式、存储器（容器）和压缩标准\n\n在数字图像处理环境中，图像文件格式是组织和存储图像数据的标准方法，它定义了数据的排列方式和所用的压缩类型。一些常用的图像压缩标准、文件格式和容器如下图所示:\n\n![图像压缩格式和容器](../../data/images/数字图像处理笔记/图像压缩格式和容器.png)\n\n### 8.2，霍夫曼编码\n\n霍夫曼提出了消除编码冗余的一种常用技术。\n\n1，霍夫曼编码过程第1步如下:\n\n![霍夫曼编码过程描述](../../data/images/数字图像处理笔记/霍夫曼编码过程描述.png)\n\n![霍夫曼信源化简](../../data/images/数字图像处理笔记/霍夫曼信源化简.png)\n\n2，霍夫曼编码过程第2步如下:\n\n![霍夫曼编码过程第二步](../../data/images/数字图像处理笔记/霍夫曼编码过程第二步.png)\n\n### 8.12，数字图像水印\n\n**简单的可见水印定义**: 若令 $f_w$ 表示添加了水印的图像，则可将它表示为未加水印图像$f$和水印$w$的线性组合:\n$$f_w = (1-\\alpha)f + \\alpha$$\n式中，常数$\\alpha$控制水印和底层图像的相对可见度。一个简单的可见水印示例图如下:\n\n![一个简单的可见水印](../../data/images/数字图像处理笔记/一个简单的可见水印.png)\n\n## 参考资料\n《数字图像处理第四版》"
  },
  {
    "path": "5-computer_vision/数字图像处理/数字图像处理基础总结.md",
    "content": "# 数字图像处理基础\n\n## 相机成像的原理\n\n相机成像的原理：针孔相机( Pinhole Camera )通过投影变换，可以将三维相机（`Camera`）坐标转换为二维的图像坐标，这个变换矩阵是相机的内在属性，称为`相机内参（Camera Intrinsic） K`。\n\n> yaw 航向角，pitch 俯仰角，roll 翻滾角\n\n## 均值模糊与高斯模糊\n\n### 均值模糊（不常用）\n\n所谓\"模糊\"，可以理解成每一个像素都取周边像素的平均值(**这种叫均值模糊**)，在图形上，就相当于产生\"模糊\"效果，但是在数值上，这是一种\"平滑化\"，\"中间点\"会失去细节。（高斯模糊效果如下图所示：）\n\n![高斯模糊效果图](../../data/images/高斯模糊.jpg)\n\n注意这里，计算平均值时，取值范围越大，\"模糊效果\"越强烈，也就是模糊半径越大，图像就越模糊。从数值角度看，就是数值越平滑。下图是分别是原图、模糊半径 3 像素、模糊半径 10 像素的效果：\n\n![不同模糊半径效果图.jpg](../../data/images/不同模糊半径效果图.jpg)\n\n### 高斯模糊\n\n从数学的角度来看，图像的高斯模糊过程就是图像像素与像素的正态分布做卷积, 同时高斯模糊对图像来说就是一个低通滤波器。简单来说高斯模糊就是一种图像模糊滤波器，它用正态分布计算图像中每个像素的变换。其中N维空间正态分布方程为：\n\n![N维空间的正太分布方程](../../data/images/N维空间正太分布方程.png)\n\n在二维空间定义为：\n\n![二维空间的正太分布方程](../../data/images/二维空间的正太分布方程.png)\n\n其中 $r$ 是模糊半径 $(r^2 = u^2 + v^2）$，$σ$ 是正态分布的标准偏差。在二维空间中，这个公式生成的曲面的等高线是从中心开始呈正态分布的同心圆。分布不为零的像素组成的卷积矩阵与原始图像做变换。每个像素的值都是周围相邻像素值的加权平均。原始像素的值有最大的高斯分布值，所以有最大的权重，相邻像素随着距离原始像素越来越远，其权重也越来越小。这样进行模糊处理比其它的均衡模糊滤波器更高地保留了边缘效果，效果如下图所示：\n\n![不同标准偏差大小(σ)效果图](../../data/images/不同模糊半径效果图.jpg)\n\n## 图像处理中平滑和锐化操作\n\n**平滑处理（smoothing）也称模糊处理（bluring）**，主要用于**消除图像中的噪声部分**，平滑处理常用的用途是用来减少图像上的噪点或失真，平滑主要使用图像滤波。在这里，我个人认为可以把图像平滑和图像滤波联系起来，因为图像平滑常用的方法就是图像滤波器。在OpenCV3中常用的图像滤波器有以下几种：\n\n+ 方框滤波——BoxBlur函数\n+ 均值滤波（邻域平均滤波）——Blur函数\n+ 高斯滤波——GaussianBlur函数（高斯低通滤波是模糊，高斯高通滤波是锐化）\n+ 中值滤波——medianBlur函数\n+ 双边滤波——bilateralFilter函数\n\n图像锐化操作是为了突出显示图像的边界和其他细节，而图像锐化实现的方法是通过各种算子和滤波器实现的——`Canny` 算子、`Sobel` 算子、`Laplacian` 算子以及 `Scharr` 滤波器。\n\n## 图像锐化方法\n\n**锐化主要影响图像中的低频分量，不影响图像中的高频分量**像锐化的主要目的有两个：\n1. 增强图像边缘，使模糊的图像变得更加清晰，颜色变得鲜明突出，图像的质量有所改善，产生更适合人眼观察和识别的图像；\n2. 过锐化处理后，目标物体的边缘鲜明，以便于提取目标的边缘、对图像进行分割、目标区域识别、区域形状提取等，进一步的图像理解与分析奠定基础。\n\n图像锐化一般有两种方法：\n1. 微分法\n2. 高通滤波法\n\n一般来说，图像的能量主要集中在其低频部分，噪声所在的频段主要在高频段，同时图像边缘信息也主要集中在其高频部分。这将导致原始图像在平滑处理之后，图像边缘和图像轮廓模糊的情况出现。为了减少这类不利效果的影响，就需要利用图像锐化技术，使图像的边缘变得清晰。**图像锐化处理的目的是为了使图像的边缘、轮廓线以及图像的细节变得清晰**.\n\n经过平滑的图像变得模糊的根本原因是因为图像受到了平均或积分运算，因此可以对其进行逆运算(如微分运算)就可以使图像变得清晰。微分运算是求信号的变化率，由傅立叶变换的微分性质可知，微分运算具有较强高频分量作用。从频率域来考虑，图像模糊的实质是因为其高频分量被衰减，因此可以用高通滤波器来使图像清晰。但要注意能够进行锐化处理的图像必须有较高的性噪比，否则锐化后图像性噪比反而更低，从而使得噪声增加的比信号还要多，因此一般是先去除或减轻噪声后再进行锐化处理。\n\n### Reference\n\n+ [图像增强－图像锐化](https://www.cnblogs.com/BYTEMAN/archive/2012/07/21/2603021.html)\n+ [高斯模糊算法](http://www.ruanyifeng.com/blog/2012/11/gaussian_blur.html)\n+ [高斯模糊](https://zh.wikipedia.org/wiki/%E9%AB%98%E6%96%AF%E6%A8%A1%E7%B3%8A)\n## numpy手撕代码\n人脸识别的场景下，输入 $105\\times 12$ 的 `feature map`，用这个在$1000 \\times 512$ 的特征库当中用欧氏距离去匹配 $105 \\times 12$ 的 feature map，用这个在 $1000\\times 512$ 的特征库当中用欧氏距离去匹配 $10\\times 1000$ 的特征，得到这个 `output`。\n\n## 单目3D目标检测笔记\n\n1. EfficientNet is a backbone. In the original paper, they used Hourglass backbone, which has a structure similar to UNet.\nCenterNet is only a type of decoder, not the whole network.\n2. 8 outputs, not exactly classes.\n1 for binary mask\n7 for regression variables x, y, z, yaw, pitch_sin, pitch_cos, roll\n3. Regression predicts raw 3d coordinates of a car x, y, z. And binary mask predicts the 2d position on the image row, col. 2d map is much more precise but contains incomplete information. Function optimize_xy is used to shift x, y (on the road) so that they correspond to row, col (in image)\n4. `CenterNet` 模型：作者在构建模型时将目标作为一个点——即目标BBox的中心点。检测器采用关键点估计来找到中心点，并回归到其他目标属性，例如尺寸，3D位置，方向，甚至姿态。\n5. I use this idea to predict x, y, z coordinates of the vehicle and also yaw, pitch_cos, pitch_sin, roll angles.\nFor pitch I predict sin and cos, because, as we will see, this angle can be both near 0 and near 3.14.\nThese 7 parameters are my regression target variables instead of shift_x, shift_y, size_x, size_y\n6. In this competition the world coordinate (Xw, Yw, Zw) is same with camera coordinate (Xc, Yc, Zc), the camera is origin."
  },
  {
    "path": "5-computer_vision/数字图像处理/视频编解码基础.md",
    "content": "- [一，基本术语](#一基本术语)\n  - [1.1，颜色亮度和我们的眼睛](#11颜色亮度和我们的眼睛)\n- [二，视频编码的实现原理](#二视频编码的实现原理)\n  - [2.1，视频编码技术概述](#21视频编码技术概述)\n  - [2.2，帧类型](#22帧类型)\n  - [2.3，帧内编码（帧内预测）](#23帧内编码帧内预测)\n  - [2.4，帧间编码（帧间预测）](#24帧间编码帧间预测)\n- [三，实际的视频编码器如何工作](#三实际的视频编码器如何工作)\n  - [3.1，视频容器（视频数据封装）](#31视频容器视频数据封装)\n  - [3.2，编码器发展历史](#32编码器发展历史)\n  - [3.3，通用编码器工作流程](#33通用编码器工作流程)\n    - [3.3.1，第一步-图片分区](#331第一步-图片分区)\n    - [3.3.2，第二步-预测](#332第二步-预测)\n    - [3.3.3，第三步-转换](#333第三步-转换)\n    - [3.3.4，第四步-量化](#334第四步-量化)\n    - [3.3.5，第五步-熵编码](#335第五步-熵编码)\n    - [3.3.6，第六步-比特流格式](#336第六步-比特流格式)\n- [参考资料](#参考资料)\n\n> 视频编解码算法分为传统算法和基于深度学习的方法，本文主要介绍基于传统算法的视频编解码技术的原理，部分内容和图片参考网上技术博客（链接已放在文章末尾）。\n\n## 一，基本术语\n> 数字图像的定义及理解可以参考这篇文章：[数字图像处理笔记｜一文搞懂数字图像基础](https://zhuanlan.zhihu.com/p/556661415)。\n\n1. **颜色深度**: 存储每个像素颜色的强度，需要占用一定大小的数据空间，这个空间大小即为颜色深度，对于 `RGB` 色彩模型，颜色深度是 `24` （8\\*3）bit。\n2. **图片分辨率**: 图像的像素的数量，通常表示为宽\\*高。\n3. **图像/视频宽高比**: 单地描述了图像或像素的宽度和高度之间的比例关系。\n4. **比特率**: 播放一段视频每秒所需的数据量，比特率 = 宽 \\* 高 \\* 颜色深度 \\* 帧每秒。例如，一段每秒 30 帧，每像素 24 bits，分辨率是 480x240 的视频，如果我们不做任何压缩，它将需要 **82,944,000 比特每秒**或 82.944 Mbps (30x480x240x24)。当比特率几乎恒定时称为恒定比特率（CBR）；但它也可以变化，称为可变比特率（VBR）。\n\n下面这个图形显示了一个受限的 `VBR`，当帧为黑色时不会花费太多的数据量。\n\n![image](../../data/images/视频编解码基础/-jXLfV_qr2zM85N4PM96IDx1SHVeBYIi4_mt7QyposI.png)\n\n### 1.1，颜色亮度和我们的眼睛\n> 因为人眼睛的视杆细胞（亮度）比视锥细胞多很多，所以一个合理的推断是相比颜色，我们有更好的能力去区分黑暗和光亮。\n\n我们的眼睛[对亮度比对颜色更敏感](http://vanseodesign.com/web-design/color-luminance/)，可以看看下面的图片来测试。\n\n![image](../../data/images/视频编解码基础/QWcQX5JHyaXT8cSps_T9IDNQlBaFEdkizVLj6PoPVKE.png)\n\n看不出左图的**方块 A 和方块 B** 的颜色是**相同的**，那是因为我们的大脑玩了一个小把戏，这让我们更多的去注意光与暗，而不是颜色。右边这里有一个使用同样颜色的连接器，那么我们（的大脑）就能轻易分辨出事实，它们是同样的颜色。\n\n## 二，视频编码的实现原理\n### 2.1，视频编码技术概述\n编码的目的是为了压缩，所谓编码算法，就是寻找规律构建一个高效模型，将视频数据中的冗余信息去除。\n\n常见的视频的冗余信息和对应的压缩方法如下表：\n\n|**种类**|**内容**|**压缩方法**|\n| ----- | ----- | ----- |\n|**空间冗余**|像素间的相关性|变换编码，预测编码|\n|**时间冗余**|时间方向上的相关性|帧间预测，运动补偿|\n|图像构造冗余|图像本身的构造|轮廓编码，区域分割|\n|知识冗余|收发两端对人物共有认识|基于知识的编码|\n|视觉冗余|人对视觉特性|非线性量化，位分配|\n|其他|不确定性因素| |\n\n视频帧冗余信息示例如下图所示：\n\n![image](../../data/images/视频编解码基础/M2CqfRSBj_lRBdg3qi865_mM1iGWgo6GqF6eRPwHkd0.png)\n\n### 2.2，帧类型\n我们知道视频是由不同的帧画面连续播放形成的，视频的帧主要分为三类，分别是（1）I 帧；（2）B 帧；（3）P 帧。\n\n* **I 帧**（关键帧，帧内编码）: 是自带全部信息的独立帧，是最完整的画面（占用的空间最大），无需参考其它图像便可独立进行解码。**视频序列中的第一个帧，始终都是I帧**。\n* **P 帧**（预测）: “帧间预测编码帧”，需要参考前面的I帧和/或P帧的不同部分，才能进行编码。P帧对前面的P和I参考帧有依赖性。但是，P帧压缩率比较高，占用的空间较小。\n* **B 帧**（双向预测）: “双向预测编码帧”，以前帧后帧作为参考帧。不仅参考前面，还参考后面的帧，所以，它的压缩率最高，可以达到200:1。不过，因为依赖后面的帧，所以不适合实时传输（例如视频会议）。\n\n对 `I` 帧的处理，是采用帧内编码（帧间预测）方式，**只利用本帧图像内的空间相关性**。\n\n对 `P` 帧的处理，采用帧间编码（前向运动估计），同时利用空间和时间上的相关性。简单来说，采用运动补偿(motion compensation)算法来去掉冗余信息。\n\n![image](../../data/images/视频编解码基础/I帧和P帧的处理.png)\n\n### 2.3，帧内编码（帧内预测）\n帧内编码/预测用于解决单帧**空间冗余**问题。如果我们分析视频的**每一帧**，会发现**许多区域是相互关联的**。\n\n![image](../../data/images/视频编解码基础/jgLT106FPhGEnBm2dW2ZuA_7_REfRxJc5EBf9TCaHJA.png)\n\n举个例子来理解帧内编码，如下图所示的图片，可以看出这个图大部分区域颜色是一样的。假设这是一个 `I 帧` ，我们即将编码红色区域，假设帧中的颜色在垂直方向上保持一致，这意味着**未知像素的颜色与临近的像素相同**。\n\n![image](../../data/images/视频编解码基础/RAfLbaiYaFqhMQXVO90zp5hL1dIPriJgFMFuOVO5h_c.png)\n\n这样的先验预测虽然会出错，但是我们可以先利用这项技术（**帧内预测**），然后**减去实际值**，算出残差，这样得出的**残差矩阵比原始数据更容易压缩。**\n\n![image](../../data/images/视频编解码基础/ppV85jK13JFn0zJHlADmfXWlz5lqcUEuYtFH2gTm4L8.png)\n\n### 2.4，帧间编码（帧间预测）\n**视频帧在时间上的重复**，解决这类冗余的技术就是帧间编码/预测。\n\n尝试**花费较少的数据量**去编码在时间上连续的 0 号帧和 1 号帧。比如做个减法，简单地**用 0 号帧减去 1 号帧**，得到残差，这样我们就只需要**对残差进行编码**。\n\n![image](../../data/images/视频编解码基础/oJ9e60nLgn-OVbbx7haL8hBhnrF0YqhwqIgwsWkKYSU.png)\n\n![image](../../data/images/视频编解码基础/KHA1uyXNRhYth4zTmuTr07LbI-EJHhx7P-YNsOciTiA.png)\n\n做减法的方法比较简单粗暴，效果不是很好，可以有更好的方法来节省数据量。首先，我们将`0 号帧` 视为一个个分块的集合，然后我们将尝试将 `帧 1` 和 `帧 0` 上的块相匹配。我们可以将这看作是**运动预测**。\n\n> 运动补偿是一种描述相邻帧（相邻在这里表示在编码关系上相邻，在播放顺序上两帧未必相邻）差别的方法，具体来说是描述前面一帧（相邻在这里表示在编码关系上的前面，在播放顺序上未必在当前帧前面）的每个小块怎样移动到当前帧中的某个位置去。”\n\n![image](../../data/images/视频编解码基础/JX3nTuItuYQTirDYMNtJ7X6F323W4RJpxEhFai7NSus.png)\n\n如上图所示，我们预计球会从 `x=0, y=25` 移动到 `x=7, y=26`，**x** 和 **y** 的值就是**运动向量**。**进一步**节省数据量的方法是，只编码这两者运动向量的差。所以，最终运动向量就是 `x=7 (6-0), y=1 (26-25)`。使用运动预测的方法会找不到完美匹配的块，但使用**运动预测**时，**编码的数据量少于**使用简单的残差帧技术，对比图如下图所示：\n\n![image](../../data/images/视频编解码基础/HzgxxQWKdq7WCEV0p5BwuZUi_ndWAKjlNety-jAg7Nk.png)\n\n## 三，实际的视频编码器如何工作\n### 3.1，视频容器（视频数据封装）\n首先视频编码器和视频容器是不一样的，我们常见的各种视频文件名后缀：`.mp4` 、`.mkv` 、`.avi` 和`.mpeg` 等其实都是视频容器。**视频容器定义**：将已经编码压缩好的视频轨和音频轨按照一定的格式放到一个文件中，这个特定的文件类型即为视频容器。\n\n### 3.2，编码器发展历史\n视频编码器的发展历史见下图：\n\n![image](../../data/images/视频编解码基础/V1hLa9oiPCvkvXIhUNK3K9BYs-qtP45t1J_2CERwNzk.png)\n\n### 3.3，通用编码器工作流程\n虽然视频编码器的已经经历了几十年的发展历史，但是其还是有一个主要的工作机制的。\n\n#### 3.3.1，第一步-图片分区\n第一步是**将帧**分成几个**分区**，**子分区**甚至更多。分区的目的是为了更精确的处理预测，在微小移动的部分使用较小的分区，而在静态背景上使用较大的分区。\n\n通常，编解码器**将这些分区组织**成切片（或瓦片），宏（或编码树单元）和许多子分区。这些分区的最大大小对于不同的编码器有所不同，比如 HEVC 设置成 64x64，而 AVC 使用 16x16，但子分区可以达到 4x4 的大小。\n\n![image](../../data/images/视频编解码基础/C1VBAUffMEOBvu2eH2Hv5e6vrZn7BWXlYq1_f_PBCHc.png)\n\n#### 3.3.2，第二步-预测\n有了分区，我们就可以在它们之上做出预测。对于帧间预测，我们需要**发送运动向量和残差**；至于帧内预测，我们需要**发送预测方向和残差**。\n\n#### 3.3.3，第三步-转换\n在我们得到残差块（`预测分区-真实分区`）之后，我们可以用一种方式**变换**它，这样我们就知道**哪些像素我们应该丢弃**，还依然能保持**整体质量**。这个确切的行为有几种变换方式，这里只介绍离散余弦变换（`DCT`），其功能如下：\n\n* 将**像素**块**转换**为相同大小的**频率系数块**。\n* **压缩**能量，更容易消除空间冗余。\n* **可逆的**，也意味着你可以还原回像素。\n\n我们知道在一张图像中，**大多数能量**会集中在低频部分，所以如果我们将图像转换成频率系数，并**丢掉高频系数**，我们就能**减少描述图像所需的数据量**，而不会牺牲太多的图像质量。 `DCT` **可以把原始图像转换为频率（系数块），然后丢掉最不重要的系数。**\n\n我们从丢掉不重要系数后的系数块重构图像，并与原始图像做对比，如下图所示。\n\n![image](../../data/images/视频编解码基础/IK93nKGvFbaMz6z2VgUdy2SY5HXkGLkaAeMJaWvh7Fw.png)\n\n可以看出它酷似原图像，与原图相比，我们**丢弃了67.1875%，而**如何**智能的选择丢弃系数**则是下一步要考虑的问题。\n\n#### 3.3.4，第四步-量化\n当我们丢掉一些（频率）系数块时，在最后一步（变换），我们做了一些形式的量化。这一步，我们选择性地剔除信息（**有损部分**）或者简单来说，我们将**量化系数以实现压缩**。\n\n如何量化一个系数块？一个简单的方法是**均匀量化**，我们取一个块并**将其除以单个的值**（10），并舍入值。\n\n![image](../../data/images/视频编解码基础/-X6f0NrOVosxjTIhMDsxi7v43Yq6BqDZ3pESu_3W0vw.png)\n\n那如何**逆转**（反量化）这个系数块呢？可以通过**乘以我们先前除以的相同的值**（10）来做到。\n\n![image](../../data/images/视频编解码基础/pY2BO3_tCwZgCcg-LKdjVgnEjBZo0aU4NCfJIu7K0G8.png)\n\n**均匀量化并不是一个最好的量化方案，因为其并没有考虑到每个系数的重要性，**我们可以使用一个**量化矩阵**来代替单个值，这个矩阵可以利用 DCT 的属性，多量化右下部，而少（量化）左上部，[JPEG 使用了类似的方法](https://www.hdm-stuttgart.de/~maucher/Python/MMCodecs/html/jpegUpToQuant.html)，我们可以通过[查看源码看看这个矩阵](https://github.com/google/guetzli/blob/master/guetzli/jpeg_data.h#L40)。\n\n#### 3.3.5，第五步-熵编码\n在我们量化数据（图像块／切片／帧）之后，我们仍然可以以无损的方式来压缩它。有许多方法（算法）可用来压缩数据：\n\n1. VLC 编码\n2. 算术编码\n\n#### 3.3.6，第六步-比特流格式\n完成上述步骤，即已经完成视频数据的编码压缩，后续我们需要将**压缩过的帧和内容打包进去**。需要明确告知解码器**编码定义**，如颜色深度，颜色空间，分辨率，预测信息（运动向量，帧内预测方向），档次\\*，级别\\*，帧率，帧类型，帧号等等更多信息。\n\n## 参考资料\n1. [digital\\_video\\_introduction](https://github.com/leandromoreira/digital_video_introduction/blob/master/README-cn.md)\n2. [零基础，史上最通俗视频编码技术入门](https://segmentfault.com/a/1190000021049773)"
  },
  {
    "path": "5-computer_vision/项目实践/GitHub车牌检测识别项目调研.md",
    "content": "- [一，EasyOCR](#一easyocr)\n  - [1.1，仓库介绍](#11仓库介绍)\n  - [1.2，使用记录](#12使用记录)\n- [二，HyperLPR](#二hyperlpr)\n  - [2.1，HyperLPR 概述](#21hyperlpr-概述)\n  - [2.3，使用记录](#23使用记录)\n  - [2.3，使用建议](#23使用建议)\n- [三，simple-car-plate-recognition-2](#三simple-car-plate-recognition-2)\n  - [3.1，仓库介绍](#31仓库介绍)\n  - [3.2，使用记录](#32使用记录)\n  - [3.3，使用建议](#33使用建议)\n- [四，车牌检测-License-Plate-Detector](#四车牌检测-license-plate-detector)\n  - [4.1，仓库介绍](#41仓库介绍)\n  - [4.2，建议](#42建议)\n- [五，MMOCR](#五mmocr)\n  - [5.1，仓库介绍](#51仓库介绍)\n  - [5.2，使用记录](#52使用记录)\n  - [5.3，使用建议](#53使用建议)\n- [六，推荐 YOLOv5-LPRNet-Licence-Recognition](#六推荐-yolov5-lprnet-licence-recognition)\n  - [6.1，仓库介绍](#61仓库介绍)\n  - [6.2，使用记录](#62使用记录)\n\n## 一，EasyOCR\n### 1.1，仓库介绍\n[EasyOCR](https://github.com/JaidedAI/EasyOCR) 是一个用于从图像中提取文本的 python 库, 它是一种通用的 OCR，既可以读取自然场景文本，也可以读取文档中的密集文本。目前支持 80 多种语言和所有流行的书写脚本，包括：拉丁文、中文、阿拉伯文、梵文、西里尔文等。\n\nEasyOCR 仓库 截止到 `2022-11-8`日，`star` 数为 `16.2k`，其文件目录和作者给出的一些示例效果如下。\n\n```bash\n├── custom_model.md\n├── Dockerfile\n├── easyocr\n├── easyocr.egg-info\n├── examples\n├── LICENSE\n├── MANIFEST.in\n├── README.md\n├── releasenotes.md\n├── requirements.txt\n├── scripts\n├── setup.cfg\n├── setup.py\n├── trainer\n└── unit_test\n```\n![image](images/VIQbFB89OddugypQOgLcDnKc1WHBMZYTWYnLRHfS8Ww.png)\n\n### 1.2，使用记录\n1，**安装较为麻烦**\n\n在自行安装了 `cuda` 库和 `pytorch` 的基础上，可通过 `pip install easyocr` 命令安装 `easyocr` 库，但是注意卸载掉之前安装的 `opencv-python` 库（如果有）。\n\n2，**代码自动下载模型速度很慢**\n\n下载的仓库里面默认是不提供任何模型的，因此第一次运行快速推理脚本会自动下载对应的 `ocr` 模型，但是！如果网络不稳定，其下载速度非常慢，试了 `n` 次，基本不可能下载成功。\n\n所以一般必须通过 [Model hub](https://www.jaided.ai/easyocr/modelhub) 页面借助浏览器**手动点击下载**对应中英文 `ocr` 识别模型，然后手动把模型文件移动到 `~/.EasyOCR/model` 文件夹下。\n\n`EasyOCR` 仓库主要是通过 `download_and_unzip` 接口下载对应模型文件的，其也是通过调用 `urllib` 模块提供的 `urlretrieve()` 函数来实现文件的下载，其定义如下:\n\n```python\ndef download_and_unzip(url, filename, model_storage_directory, verbose=True):\n    zip_path = os.path.join(model_storage_directory, 'temp.zip')\n    reporthook = printProgressBar(prefix='Progress:', suffix='Complete', length=50) if verbose else None\n    # url 下载链接，zip_path 文件保存的本地路径, reporthook 利用这个回调函数来显示当前的下载进度\n    urlretrieve(url, zip_path, reporthook=reporthook)\n    with ZipFile(zip_path, 'r') as zipObj:\n        zipObj.extract(filename, model_storage_directory) # 解压到指定目录\n    os.remove(zip_path) # 移除下载的压缩包文件\n```\n3，**车牌场景识别准确率非常低**\n\n经过我的大量测试，其在中国车牌场景下识别率几乎为 `0`，我猜测是因为作者提供的训练模型所用的训练数据没有车牌场景的，而 `ocr` 效果又非常依赖场景数据，所以导致汽车车牌识别率几乎为 `0` ，具体实践效果如下。\n\n![image](images/pUyi-ZkKb2cjGwAvKW7-BlTlvXVP-PhZn6NX7QKZYVY.png)\n\n## 二，HyperLPR\n### 2.1，HyperLPR 概述\n[HyperLPR 框架](https://github.com/szad670401/HyperLPR)是 github 作者 [szad670401](https://github.com/szad670401) 开源的基于深度学习高性能中文车牌识别框架，支持多平台，提供了 Window、Linux、Android、IOS、ROS 平台的支持。 Python 依赖于 Keras (>2.0.0) 和 Theano(>0.9) or Tensorflow(>1.1.x) 机器学习库。**项目的 C++ 实现和 Python 实现无任何关联，均为单独实现**。\n\n作者提供的测试用例效果如下：\n\n![image](images/hriCBh_8tKR3wZoZYlH2xzW-_mBkBg_qYS4LcqbK1pI.png)\n\n### 2.3，使用记录\n仓库 README 文件描述说 HyperLPR 框架对 python 包支持一键安装: `pip install hyperlpr` 。但是经过我实际测试发现，`pip install hyperlpr` 命令只能成功安装 `hyperlpr` 库.\n\n1，快速上手的 py 代码运行会出错：\n\n![image](images/wwDzK1eodp-sPMQ9tuh5Jx3f4NwyM0SiHDkszRmSpPw.png)\n\n2，我把 `demo` 代码移动到 `hyperlpr_py3` 目录下运行，不再报上图的错误，但是又报了 `opencv` 函数版本的问题。\n\n```bash\nhyperlpr) root@crowd-max:/framework/HyperLPR/hyperlpr_py3# python test.py \n(1, 3, 150, 400)\n40 22 335 123\nTraceback (most recent call last):\n  File \"test.py\", line 7, in <module>\n    print(HyperLPR_plate_recognition(image))\n  File \"/opt/miniconda3/envs/hyperlpr/lib/python3.8/site-packages/hyperlpr/__init__.py\", line 8, in HyperLPR_plate_recognition\n    return PR.plate_recognition(Input_BGR,minSize,charSelectionDeskew)\n  File \"/opt/miniconda3/envs/hyperlpr/lib/python3.8/site-packages/hyperlpr/hyperlpr.py\", line 311, in plate_recognition\n    cropped_finetuned = self.finetune(cropped)\n  File \"/opt/miniconda3/envs/hyperlpr/lib/python3.8/site-packages/hyperlpr/hyperlpr.py\", line 263, in finetune\n    g = self.to_refine(image_, pts)\n  File \"/opt/miniconda3/envs/hyperlpr/lib/python3.8/site-packages/hyperlpr/hyperlpr.py\", line 231, in to_refine\n    mat_ = cv2.estimateRigidTransform(org_pts, target_pts, True)\nAttributeError: module 'cv2' has no attribute 'estimateRigidTransform'\n```\n3，`ubuntu16.04+python3.8+cuda11.0` 环境下，`pip install -r requirements.txt` 命令安装依赖包依然会出错。\n\n![image](images/I7wWb2r4ISku4rQVXocPVqqZUKoFK_xfUiNi-TlTWps.png)\n\n### 2.3，使用建议\n**个人建议直接使用 C++ 版本**，截止到 2022-11-8 日为止，纯 Python 版本还是有各种问题。\n\n## 三，simple-car-plate-recognition-2\n### 3.1，仓库介绍\n[simple-car-plate-recognition-2仓库 ](https://github.com/airxiechao/simple-car-plate-recognition-2) 简称：简易车牌字符识别 `2-Inception/CTC` 。\n\n作者使用的字符识别模型是参考 [HyperLPR](https://github.com/zeusees/HyperLPR) 里面的一个叫 `SegmenationFree-Inception` 的模型结构，并改用 `pytorch` 框架实现，然后训练模型，最后测试用整张车牌图片进行字符识别。\n\n作者所用的车牌训练集，是利用 [generateCarPlate](https://github.com/derek285/generateCarPlate) 这个车牌生成工具生成的。\n\n### 3.2，使用记录\n直接用车牌做识别，**实际测试下来，不管用作者给的模型，还是自己训练的模型，效果都很差**。\n\n![image](images/VWygFsrxKEcxjMz0FyHDDDzZHbSTo6ld_OPQUL1bi7c.png)\n\n### 3.3，使用建议\n虽然代码简单，模型结构容易看懂，但是不建议使用，效果不稳定和太差。\n\n## 四，车牌检测-License-Plate-Detector\n### 4.1，仓库介绍\n[License-Plate-Detector 仓库](https://github.com/zeusees/License-Plate-Detector) 作者利用 Yolov5 模型进行了车牌检测，训练集使用 `CCPD` 数据集，测试效果如下：\n\n![image](images/xrKKcLUfs9MQMInqyI_6Sa-YuihZHWtR7JIMbyB8uJU.png)\n\n### 4.2，建议\n**不建议使用，代码写的不够整洁**，使用不够方便，使用 **yolov5**** 用作车牌检测的模型**的方法还是可以参考下。\n\n## 五，MMOCR\n### 5.1，仓库介绍\n`mmocr` 是商汤 + `openmmlab` 实验室开发的 **OCR 框架**。`MMOCR` 是基于 `PyTorch` 和 `mmdetection` 的开源工具箱，专注于文本检测，文本识别以及相应的下游任务，如关键信息提取。 它是 `OpenMMLab` 项目的一部分。\n\n主分支目前支持 **PyTorch 1.6 以上**的版本。mmocr 库的安装，可参考我之前的文章-[ubuntu16.04安装mmdetection库](https://github.com/HarleysZhang/2021_algorithm_intern_information/blob/master/1-computer_basics/%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85/ubuntu16.04%E5%AE%89%E8%A3%85mmdetection%E5%BA%93.md#%E4%B8%89mmdetection-%E5%AE%89%E8%A3%85)。\n\n### 5.2，使用记录\n1，官方提供**中文字符识别模型只有一个**，其使用步骤如下：\n\n1. 创建 `mmocr/data/chineseocr/labels` 目录;\n2. 为了模型推理成功，下载中文字典，并放置到 `labels` 目录;\n\n```bash\nwget -c https://download.openmmlab.com/mmocr/textrecog/sar/dict_printed_chinese_english_digits.txt\nmv dict_printed_chinese_english_digits.txt mmocr/data/chineseocr/labels \n```\n3. 运行推理脚本。\n\n```bash\npython mmocr/utils/ocr.py --det DB_r18 --recog SAR_CN demo/car1.jpeg --output='./'\n```\n**车牌识别效果不好**，测试结果如下：\n\n![image](images/APLu0--q-6TzQiYWoTi67xdCSegS4ttYvNmGrjTM-dg.png)\n\n![image](images/cQqnGpGROhQlijX3y0O49gKC2FDCB4Fi74ohbpqLM0I.png)\n\n![image](images/3P3kdiqVB-vK7HB8rDzzO6709vwzReZNyoSC6zkFcsI.png)\n\n2，官方提供的测试用例的推理效果如下:\n\n![image](images/sJ4KbMutjDf06BoNGTowQ9PlDBOtGDOubH0F2v6oVRY.png)\n\n### 5.3，使用建议\n**官方提供的不管是中文还是英文文本识别模型，在车牌场景下识别效果都不好**，不推荐在车牌识别场景下使用，更适合通用场景。\n\n## 六，推荐 YOLOv5-LPRNet-Licence-Recognition\n### 6.1，仓库介绍\n\n[YOLOv5-LPRNet-Licence-Recognition](https://github.com/HuKai97/YOLOv5-LPRNet-Licence-Recognition) 项目是使用 [YOLOv5s](https://github.com/ultralytics/yolov5) 和 [LPRNet](https://arxiv.org/pdf/1806.10447.pdf) 对中国车牌进行检测和识别，车牌数据集是使用 [CCPD](https://github.com/detectRecog/CCPD)。\n\n**车牌字符识别的准确率**如下:\n\n|`model`|数据集|`epochs`|`acc`|`size`|\n| ----- | ----- | ----- | ----- | ----- |\n|LPRNet|val|100|94.33%|1.7M|\n|LPRNet|test|100|**94.30%**|1.7M|\n\n**总体模型速度**：（`YOLOv5 + LPRNet`）速度：`47.6 FPS`（970 GPU）。\n\n### 6.2，使用记录\n作者提供的模型实际测试下来效果还不错，部分示例如下：\n\n![image](images/ezd3183AJ2p-qmagngS6xuRh0DhRr9-JNx28hfzMvAk.png)\n\n![image](images/IzWv2M5byDYrFjfEn72D3utamQBOt1VEH6QKXrtIVT0.png)\n\n![image](images/VLz6rWrCqeKwwam3nmUrkqLVNpz41UuCMyKCoEOGo0I.png)"
  },
  {
    "path": "5-computer_vision/项目实践/draw_heart.py",
    "content": "import random\nfrom math import sin, cos, pi, log\nfrom tkinter import *\n\nCANVAS_WIDTH = 640  # 画布的宽\nCANVAS_HEIGHT = 640  # 画布的高\nCANVAS_CENTER_X = CANVAS_WIDTH / 2  # 画布中心的X轴坐标\nCANVAS_CENTER_Y = CANVAS_HEIGHT / 2  # 画布中心的Y轴坐标\nIMAGE_ENLARGE = 11  # 放大比例\nHEART_COLOR = \"#ff7171\"  # 心的颜色\n\n\ndef heart_function(t, shrink_ratio: float = IMAGE_ENLARGE):\n    # 基础函数\n    x = 17 * (sin(t) ** 3)\n    y = -(13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t))\n\n    # 放大\n    x *= shrink_ratio\n    y *= shrink_ratio\n\n    # 移到画布中央\n    x += CANVAS_CENTER_X\n    y += CANVAS_CENTER_Y\n\n    return int(x), int(y)\n\n\ndef scatter_inside(x, y, beta=0.2):\n    ratio_x = - beta * log(random.random())\n    ratio_y = - beta * log(random.random())\n\n    dx = ratio_x * (x - CANVAS_CENTER_X)\n    dy = ratio_y * (y - CANVAS_CENTER_Y)\n\n    return x - dx, y - dy\n\n\ndef shrink(x, y, ratio):\n    force = -1 / (((x - CANVAS_CENTER_X) ** 2 + (y - CANVAS_CENTER_Y) ** 2) ** 0.6)  # 这个参数...\n    dx = ratio * force * (x - CANVAS_CENTER_X)\n    dy = ratio * force * (y - CANVAS_CENTER_Y)\n    return x - dx, y - dy\n\n\ndef curve(p):\n    # 可以尝试换其他的动态函数，达到更有力量的效果（贝塞尔？）\n    return 2 * (3 * sin(4 * p)) / (2 * pi)\n\n\nclass Heart:\n    \"\"\"\n    爱心类\n    \"\"\"\n\n    def __init__(self, generate_frame=30):\n        self._points = set()  # 原始爱心坐标集合\n        self._edge_diffusion_points = set()  # 边缘扩散效果点坐标集合\n        self._center_diffusion_points = set()  # 中心扩散效果点坐标集合\n        self.all_points = {}  # 每帧动态点坐标\n        self.build(2000)\n\n        self.random_halo = 1000\n\n        self.generate_frame = generate_frame\n        for frame in range(generate_frame):\n            self.calc(frame)\n\n    def build(self, number):\n        # 爱心\n        for _ in range(number):\n            t = random.uniform(0, 2 * pi)  # 随机不到的地方造成爱心有缺口\n            x, y = heart_function(t)\n            self._points.add((x, y))\n\n        # 爱心内扩散\n        for _x, _y in list(self._points):\n            for _ in range(6):\n                x, y = scatter_inside(_x, _y, 0.05)\n                self._edge_diffusion_points.add((x, y))\n\n        # 爱心内再次扩散\n        point_list = list(self._points)\n        for _ in range(4000):\n            x, y = random.choice(point_list)\n            x, y = scatter_inside(x, y, 0.17)\n            self._center_diffusion_points.add((x, y))\n\n    @staticmethod\n    def calc_position(x, y, ratio):\n        # 调整缩放比例\n        force = 1 / (((x - CANVAS_CENTER_X) ** 2 + (y - CANVAS_CENTER_Y) ** 2) ** 0.520)  # 魔法参数\n\n        dx = ratio * force * (x - CANVAS_CENTER_X) + random.randint(-1, 1)\n        dy = ratio * force * (y - CANVAS_CENTER_Y) + random.randint(-1, 1)\n\n        return x - dx, y - dy\n\n    def calc(self, generate_frame):\n        ratio = 10 * curve(generate_frame / 10 * pi)  # 圆滑的周期的缩放比例\n\n        halo_radius = int(4 + 6 * (1 + curve(generate_frame / 10 * pi)))\n        halo_number = int(3000 + 4000 * abs(curve(generate_frame / 10 * pi) ** 2))\n\n        all_points = []\n\n        # 光环\n        heart_halo_point = set()  # 光环的点坐标集合\n        for _ in range(halo_number):\n            t = random.uniform(0, 2 * pi)  # 随机不到的地方造成爱心有缺口\n            x, y = heart_function(t, shrink_ratio=11.6)  # 魔法参数\n            x, y = shrink(x, y, halo_radius)\n            if (x, y) not in heart_halo_point:\n                # 处理新的点\n                heart_halo_point.add((x, y))\n                x += random.randint(-14, 14)\n                y += random.randint(-14, 14)\n                size = random.choice((1, 2, 2))\n                all_points.append((x, y, size))\n\n        # 轮廓\n        for x, y in self._points:\n            x, y = self.calc_position(x, y, ratio)\n            size = random.randint(1, 3)\n            all_points.append((x, y, size))\n\n        # 内容\n        for x, y in self._edge_diffusion_points:\n            x, y = self.calc_position(x, y, ratio)\n            size = random.randint(1, 2)\n            all_points.append((x, y, size))\n\n        for x, y in self._center_diffusion_points:\n            x, y = self.calc_position(x, y, ratio)\n            size = random.randint(1, 2)\n            all_points.append((x, y, size))\n\n        self.all_points[generate_frame] = all_points\n\n    def render(self, render_canvas, render_frame):\n        for x, y, size in self.all_points[render_frame % self.generate_frame]:\n            render_canvas.create_rectangle(x, y, x + size, y + size, width=0, fill=HEART_COLOR)\n\n\ndef draw(main: Tk, render_canvas: Canvas, render_heart: Heart, render_frame=0):\n    render_canvas.delete('all')\n    render_heart.render(render_canvas, render_frame)\n    main.after(160, draw, main, render_canvas, render_heart, render_frame + 1)\n\n\nif __name__ == '__main__':\n    root = Tk()  \n    # 创建画布\n    canvas = Canvas(root, bg='black', height=CANVAS_HEIGHT, width=CANVAS_WIDTH)\n    canvas.pack()\n    heart = Heart()  \n    draw(root, canvas, heart)\nroot.mainloop()"
  },
  {
    "path": "6-model_compression/README.md",
    "content": "## 前言\n\n已经迁移到到 [dl_note](https://github.com/HarleysZhang/dl_note) 仓库。"
  },
  {
    "path": "6-model_compression/卷积网络压缩方法总结.md",
    "content": "## 卷积网络的压缩方法\n\n+ [一，低秩近似](#一，低秩近似)\n+ [二，剪枝与稀疏约束](#二，剪枝与稀疏约束)\n+ [三，参数量化](#三，参数量化)\n+ [四，二值化网络](#四，二值化网络)\n+ [五，知识蒸馏](#五，知识蒸馏)\n+ [六，浅层网络](#六，浅层网络)\n\n在一定程度上，网络越深，参数越多，模型也会越复杂，但其最终效果也越好。而模型压缩算法是旨在将一个庞大而复杂的预训练模型转化为一个精简的小模型。本文介绍了卷积神经网络常见的几种压缩方法。\n\n按照压缩过程对网络结构的破坏程度，我们将模型压缩技术分为“前端压缩”和“后端压缩”两部分:\n+ 前端压缩，是指在不改变原网络结构的压缩技术，主要包括`知识蒸馏`、轻量级网络（紧凑的模型结构设计）以及`滤波器（filter）层面的剪枝（结构化剪枝）`等；\n+ 后端压缩，是指包括`低秩近似`、未加限制的剪枝（非结构化剪枝/稀疏）、`参数量化`以及二值网络等，目标在于尽可能减少模型大小，会对原始网络结构造成极大程度的改造。\n\n总结：前端压缩几乎不改变原有网络结构（仅仅只是在原模型基础上减少了网络的层数或者滤波器个数），后端压缩对网络结构有不可逆的大幅度改变，造成原有深度学习库、甚至硬件设备不兼容改变之后的网络。其维护成本很高。\n\n## 一，低秩近似\n\n简单理解就是，卷积神经网络的权重矩阵往往稠密且巨大，从而计算开销大，有一种办法是**采用低秩近似的技术**将该稠密矩阵由若干个小规模矩阵近似重构出来，这种方法归类为低秩近似算法。\n> 一般地，行阶梯型矩阵的秩等于其“台阶数”-非零行的行数。\n\n**低秩近似算法能减小计算开销的原理**如下：\n\n给定权重矩阵 $W\\in \\mathbb{R}^{m\\times n}$ , 若能将其表示为若干个低秩矩阵的组合，即 $W=\\sum_{i=1}^{n}\\alpha_{i}M_{i}$ , 其中 $M_{i}\\in \\mathbb{R}^{m\\times n}$ 为低秩矩阵，其秩为 $r_{i}$ , 并满足 $r_{i}\\ll min(m,n)$ ，则其每一个低秩矩阵都可分解为小规模矩阵的乘积，$M_{i}=G_{i}H_{i}^{T}$ ，其中 $G_{i}\\in \\mathbb{R}^{m\\times r_{i}}$ ，$H_{i}\\in \\mathbb{R}^{m \\times r_{i}}$。当 $r_{i}$ 取值很小时，便能大幅降低总体的存储和计算开销。\n\n基于以上想法，`Sindhwani` 等人提出使用结构化矩阵来进行低秩分解的算法，具体原理可自行参考论文。另一种比较简便的方法是使用矩阵分解来降低权重矩阵的参数，如 Denton 等人提出使用`奇异值分解`（Singular Value Decomposition，简称 SVD）分解来重构全连接层的权重。\n\n### 1.1，总结\n\n低秩近似算法在中小型网络模型上，取得了很不错的效果，但其超参数量与网络层数呈线性变化趋势，随着网络层数的增加与模型复杂度的提升，其搜索空间会急剧增大，目前主要是学术界在研究，工业界应用不多。\n\n## 二，剪枝与稀疏约束\n\n给定一个预训练好的网络模型，常用的剪枝算法一般都遵从如下操作：\n\n1. 衡量神经元的重要程度\n2. 移除掉一部分不重要的神经元，这步比前 1 步更加简便，灵活性更高\n3. 对网络进行微调，剪枝操作不可避免地影响网络的精度，为防止对分类性能造成过大的破坏，需要对剪枝后的模型进行微调。对于大规模行图像数据集（如ImageNet）而言，微调会占用大量的计算资源，因此对网络微调到什么程度，是需要斟酌的\n4. 返回第一步，循环进行下一轮剪枝\n\n基于以上循环剪枝框架，不同学者提出了不同的方法，Han等人提出首先**将低于某个阈值的权重连接全部剪除**，之后对剪枝后的网络进行微调以完成参数更新的方法，这种方法的不足之处在于，剪枝后的网络是非结构化的，即被剪除的网络连接在分布上，没有任何连续性，这种稀疏的结构，导致CPU高速缓冲与内存频繁切换，从而限制了实际的加速效果。\n\n基于此方法，有学者尝试将剪枝的粒度提升到整个滤波器级别，即丢弃整个滤波器，但是如何衡量滤波器的重要程度是一个问题，其中一种策略是基于滤波器权重本身的统计量，如分别计算每个滤波器的 L1 或 L2 值，将相应数值大小作为衡量重要程度标准。\n\n**利用稀疏约束来对网络进行剪枝也是一个研究方向，其思路是在网络的优化目标中加入权重的稀疏正则项，使得训练时网络的部分权重趋向于 0 ，而这些 0 值就是剪枝的对象**。\n\n### 2.1，总结\n\n总体而言，剪枝是一项有效减小模型复杂度的通用压缩技术，其关键之处在于`如何衡量个别权重对于整体模型的重要程度`。剪枝操作对网络结构的破坏程度极小，将剪枝与其他后端压缩技术相结合，能够达到网络模型最大程度压缩，目前工业界有使用剪枝方法进行模型压缩的案例。\n\n## 三，参数量化\n\n相比于剪枝操作，参数量化则是一种常用的后端压缩技术。所谓“量化”，是指从权重中归纳出若干“代表”，由这些“代表”来表示某一类权重的具体数值。“代表”被存储在码本（codebook）之中，而原权重矩阵只需记录各自“代表”的索引即可，从而**极大地降低了存储开销**。这种思想可类比于经典的词包模型（bag-of-words model）。常用量化算法如下：\n\n1. 标量量化（`scalar quantization`）。\n2. 标量量化会在一定程度上降低网络的精度，为避免这个弊端，很多算法考虑结构化的向量方法，其中一种是乘积向量`（Product Quantization, PQ）`，详情咨询查阅论文。\n3. 以PQ方法为基础，Wu等人设计了一种通用的网络量化算法：`QCNN(quantized CNN)`，主要思想在于Wu等人认为最小化每一层网络输出的重构误差，比最小化量化误差更有效。\n\n标量量化算法基本思路是，对于每一个权重矩阵 $W\\in \\mathbb{R}^{m\\times n}$，首先将其转化为向量形式：$w\\in \\mathbb{R}^{1\\times mn}$。之后对该权重向量的元素进行 $k$ 个簇的聚类，这可借助于经典的 `k-均值（k-means）聚类`算法快速完成：\n\n$$\\underset{c}{arg min}\\sum_{i}^{mn}\\sum_{j}^{k}\\begin{Vmatrix}​w_{i}-c_{j}\\end{Vmatrix}_{2}^{2}$$\n\n这样，只需将 $k$ 个聚类中心（$c_{j}$，标量）存储在码本中，而原权重矩阵则只负责记录各自聚类中心在码本中索引。如果不考虑码本的存储开销，该算法能将存储空间减少为原来的 $log_{2}(k)/32$。基于 $k$ 均值算法的标量量化在很多应用中非常有效。参数量化与码本微调过程图如下：\n\n![参数量化与码本微调过程图](../data/images/参数量化与码本微调过程图.png)\n\n这三类基于聚类的参数量化算法，其本质思想在于将多个权重映射到同一个数值，从而实现权重共享，降低存储开销的目的。\n\n### 3.1，总结\n\n参数量化是一种常用的后端压缩技术，能够以很小的性能损失实现模型体积的大幅下降，不足之处在于，量化的网络是“固定”的，很难对其做任何改变，同时这种方法通用性差，需要配套专门的深度学习库来运行网络。\n\n这里，权重参数从浮点转定点、二值化等方法都是是试图避免浮点计算耗时而引入的方法，这些方法能加快运算速率，同时减少内存和存储空间的占用，并保证模型的精度损失在可接受的范围内，因此这些方法的应用是有其现实价值的。更多参数量化知识，请参考此 [github仓库](https://github.com/Ewenwan/MVision/blob/master/CNN/Deep_Compression/quantization/readme.md)。\n\n## 四，二值化网络\n\n1. 二值化网络可以视为量化方法的一种极端情况：所有的权重参数取值只能为 $\\pm 1$ ，也就是使用 `1bit`来存储`Weight` 和 `Feature`。在普通神经网络中，一个参数是由单精度浮点数来表示的，参数的二值化能将存储开销降低为原来的 `1/32`。\n2. 二值化神经网络以其高的模型压缩率和在前传中计算速度上的优势，近几年格外受到重视和发展，成为神经网络模型研究中的非常热门的一个研究方向。但是，第一篇真正意义上将神经网络中的权重值和激活函数值同时做到二值化的是 `Courbariaux` 等人 2016 年发表的名为《Binarynet: Training deep neural networks with weights and activations constrained to +1 or -1》的一篇论文。**这篇论文第一次给出了关于如何对网络进行二值化和如何训练二值化神经网络的方法**。\n3. CNN 网络一个典型的模块是由卷积(`Conv`)->批标准化(`BNorm`)->激活(`Activ`)->池化(`Pool`)这样的顺序操作组成的。对于异或神经网络，设计出的模块是由批标准化(`BNorm`)->**二值化激活(BinActiv)**->二值化卷积(`BinConv`)->池化(`Pool`)的顺序操作完成。这样做的原因是批标准化以后，保证了输入均值为 `0`，然后进行二值化激活，保证了数据为 `-1` 或者 `+1`，然后进行二值化卷积，这样能最大程度上减少特征信息的损失。二值化残差网络结构定义实例代码如下：\n\n```python\ndef residual_unit(data, num_filter, stride, dim_match, num_bits=1):\n    \"\"\"残差块 Residual Block 定义\n    \"\"\"\n    bnAct1 = bnn.BatchNorm(data=data, num_bits=num_bits)\n    conv1 = bnn.Convolution(data=bnAct1, num_filter=num_filter, kernel=(3, 3), stride=stride, pad=(1, 1))\n    convBn1 = bnn.BatchNorm(data=conv1, num_bits=num_bits)\n    conv2 = bnn.Convolution(data=convBn1, num_filter=num_filter, kernel=(3, 3), stride=(1, 1), pad=(1, 1))\n    if dim_match:\n        shortcut = data\n    else:\n        shortcut = bnn.Convolution(data=bnAct1, num_filter=num_filter, kernel=(3, 3), stride=stride, pad=(1, 1))\n    return conv2 + shortcut\n```\n\n### 4.1，二值网络的梯度下降\n\n现在的神经网络几乎都是基于梯度下降算法来训练的，但是二值网络的权重只有 $\\pm 1$，无法直接计算梯度信息，也无法进行权重更新。为解决这个问题，[Courbariaux](https://arxiv.org/pdf/1602.02830v1.pdf) 等人提出二值连接（binary connect）算法，该算法采取单精度与二值结合的方式来训练二值神经网络，这是第一次给出了关于如何对网络进行二值化和如何训练二值化神经网络的方法。过程如下：\n\n1. 权重 `weight` 初始化为浮点\n2. 前向传播 `Forward Pass`:\n    + 利用决定化方式（`sign(x)函数`）把 Weight 量化为 `+1/-1`, 以0为阈值\n    + 利用量化后的 Weight (只有+1/-1)来计算前向传播，由二值权重与输入进行卷积运算（实际上只涉及加法），获得卷积层输出。\n3. 反向传播 `Backward Pass`:\n    + 把梯度更新到浮点的 Weight 上（根据放松后的符号函数，计算相应梯度值，并根据该梯度的值对单精度的权重进行参数更新）\n    + 训练结束： 把 Weight 永久性转化为 `+1/-1`, 以便 `inference` 使用\n\n### 4.1，两个问题\n\n网络二值化需要解决两个问题：如何对权重进行二值化和如何计算二值权重的梯度。\n\n**1，如何对权重进行二值化？**\n\n权重二值化一般有两种选择：\n\n+ 直接根据权重的正负进行二值化：$x^{b}=sign(x)$。符号函数 `sign(x)` 定义如下：\n$$\nsign(x) = \\left\\{\\begin{matrix}\n-1 & x < 0 \\\\\\\\\n0 & x = 0 \\\\\\\\\n1 & x > 0\n\\end{matrix}\\right.\n$$\n\n+ 进行随机的二值化，即对每一个权重，以一定概率取 $\\pm 1$\n\n**2，如何计算二值权重的梯度？**\n\n二值权重的梯度为0，无法进行参数更新。为解决这个问题，需要**对符号函数进行放松**，即用 $Htanh(x) = max(-1, min(1,x))$ 来代替 $sinx(x)$。当 x 在区间 [-1,1] 时，存在梯度值 1，否则梯度为 0 。\n\n### 4.3，二值连接算法改进\n\n之前的二值连接算法只对权重进行了二值化，但是网络的中间输出值依然是单精度的，于是 Rastegari 等人对此进行了改进，提出用**单精度对角阵与二值矩阵之积来近似表示原矩阵的算法**，以提升二值网络的分类性能，弥补二值网络在精度上弱势。该算法将原卷积运算分解为如下过程：\n\n$$I \\times W\\approx (I \\times B)\\alpha$$\n\n其中 $I\\in \\mathbb{R}^{c\\times w_{in}\\times h_{in}}$ 为该层的输入张量，$I\\in \\mathbb{R}^{c\\times w\\times h}$ 为该层的一个滤波器，$B=sign(W)\\in \\{+1, -1\\}^{c \\times w\\times h}$为该滤波器所对应的二值权重。\n\n这里，Rastegari 等人认为单靠二值运算，很难达到原单精度卷积元素的结果，于是他们使用了一个单精度放缩因子 $\\alpha \\in \\mathbb{R}^{+}$ 来对二值滤波器卷积后的结果进行放缩。而 $\\alpha$ 的取值，则可根据优化目标：\n\n$$min \\left \\| W -\\alpha B \\right \\|^{2}$$\n\n得到 $\\alpha = \\frac{1}{n}\\left |W \\right |\\ell{1}$。二值连接改进的算法训练过程与之前的算法大致相同，不同的地方在于梯度的计算过程还考虑了 $\\alpha$ 的影响。由于 $\\alpha$ 这个单精度的缩放因子的存在，有效降低了重构误差，并首次在 ImageNet 数据集上取得了与 Alex-Net 相当的精度。如下图所示：\n\n![二值化网络精度对比](../data/images/二值化算法精度.png)\n\n可以看到的是`权重二值化神经网络（BWN）`和全精度神经网络的精确度几乎一样，但是与异或神经网络（XNOR-Net）相比而言，Top-1 和 Top-5 都有 10+% 的损失。\n> 相比于权重二值化神经网络，异或神经网络将网络的输入也转化为二进制值，所以，异或神经网络中的乘法加法 (Multiplication and ACcumulation) 运算用按位异或 (bitwise xnor) 和数 1 的个数 (popcount) 来代替。\n\n**更多内容，可以看这两篇文章：**\n\n+ [https://github.com/Ewenwan/MVision/tree/master/CNN/Deep_Compression/quantization/BNN](https://github.com/Ewenwan/MVision/tree/master/CNN/Deep_Compression/quantization/BNN)\n+ [二值神经网络（Binary Neural Network，BNN）](https://blog.csdn.net/stdcoutzyx/article/details/50926174)\n\n### 4.4，二值网络设计注意事项\n\n+ 不要使用 kernel = (1, 1) 的 `Convolution` （包括 resnet 的 bottleneck）：二值网络中的 weight 都为 1bit， 如果再是 1x1 大小， 会极大地降低表达能力\n+ 增大 `Channel` 数目 + 增大 activation bit 数 要协同配合：如果一味增大 channel 数， 最终 feature map 因为 bit 数过低， 还是浪费了模型容量。 同理反过来也是。\n+ 建议使用 4bit 及以下的 `activation bit`， 过高带来的精度收益变小， 而会显著提高 inference 计算量\n\n## 五，知识蒸馏\n\n知识蒸馏（knowledge distillation），其实也属于迁移学习（transfer learning）的一种，通俗理解就是训练一个大模型（teacher 模型）和一个小模型（student 模型），将庞大而复杂的大模型学习到的知识，通过一定技术手段迁移到精简的小模型上，从而使小模型能够获得与大模型相近的性能。也可说让小模型去拟合大模型，从而让**小模型学到与大模型相似的函数映射**。使其保持其快速的计算速度前提下，同时拥有复杂模型的性能，达到模型压缩的目的。\n\n知识蒸馏的关键在于监督特征的设计，这个领域的开篇之作-[Distilling the Knowledge in a Neural Network](https://link.zhihu.com/?target=https%3A//arxiv.org/abs/1503.02531) 使用 `Soft Target` 所提供的类间相似性作为依据去指导小模型训练（`软标签蒸馏 KD`）。后续工作也有使用大模型的中间层特征图或 attention map（`features KD` 方法）作为监督特征，对小模型进行指导训练。这个领域的开篇之作-Distilling the Knowledge in a Neural Network，是属于软标签 KD 方法，后面还出现了 features KD 的论文。\n\n以经典的知识蒸馏实验为例，我们先训练好一个 `teacher` 网络，然后将 `teacher` 的网络的输出结果 $q$ 作为 `student` 网络的目标，训练 `student` 网络，使得 `student` 网络的结果 $p$ 接近 $q$ ，因此，`student` 网络的损失函数为 $L = CE(y,p)+\\alpha CE(q,p)$。这里 `CE` 是交叉熵（Cross Entropy），$y$ 是真实标签的 `onehot` 编码，$q$ 是 `teacher` 网络的输出结果，$p$ 是 `student` 网络的输出结果。\n\n但是，直接使用 `teacher` 网络的 softmax 的输出结果 $q$，可能不大合适。因为，一个网络训练好之后，对于正确的答案会有一个很高的置信度而错误答案的置信度会很小。例如，在 MNIST 数据中，对于某个 2 的输入，对于 2 的预测概率会很高，而对于 2 类似的数字，例如 3 和 7 的预测概率为 $10^-6$ 和 $10^-9$。这样的话，`teacher` 网络学到**数据的相似信息**（例如数字 2 和 3，7 很类似）很难传达给 `student` 网络，因为它们的概率值接近`0`。因此，论文提出了 `softmax-T`(软标签计算公式)，公式如下所示：\n$$q_{i} = \\frac{z_{i}/T}{\\sum_{j}z_{j}/T}$$\n\n这里 $q_i$ 是 $student$ 网络学习的对象（soft targets），$z_i$ 是 `teacher` 模型 `softmax` 前一层的输出 `logit`。如果将 $T$ 取 1，上述公式**等同于 softmax**，根据 logit 输出各个类别的概率。如果 $T$ 接近于 0，则最大的值会越近 1，其它值会接近 0，近似于 `onehot` 编码。\n\n所以，可以知道 `student` 模型最终的损失函数由两部分组成：\n\n+ 第一项是由小模型的预测结果与大模型的“软标签”所构成的交叉熵（cross entroy）;\n+ 第二项为预测结果与普通类别标签的交叉熵。\n\n这两个损失函数的重要程度可通过一定的权重进行调节，在实际应用中，`T` 的取值会影响最终的结果，一般而言，较大的 T 能够获得较高的准确度，T（蒸馏温度参数） 属于知识蒸馏模型训练超参数的一种。**T 是一个可调节的超参数、T 值越大、概率分布越软（论文中的描述），曲线便越平滑**，相当于在迁移学习的过程中添加了扰动，从而使得学生网络在借鉴学习的时候更有效、泛化能力更强，这其实就是一种抑制过拟合的策略。\n\n知识蒸馏的整个过程如下图：\n\n![知识蒸馏模型训练过程](../data/images/知识蒸馏模型过程.png)\n\n`student` 模型的实际模型结构和小模型一样，但是损失函数包含了两部分，分类网络的知识蒸馏 mxnet 代码示例如下：：\n\n```python\n# -*-coding-*-  : utf-8  \n\"\"\"\n本程序没有给出具体的模型结构代码，主要给出了知识蒸馏 softmax 损失计算部分。\n\"\"\"\nimport mxnet as mx\n\ndef get_symbol(data, class_labels, resnet_layer_num,Temperature,mimic_weight,num_classes=2):\n    backbone = StudentBackbone(data)  # Backbone 为分类网络 backbone 类\n    flatten = mx.symbol.Flatten(data=conv1, name=\"flatten\")\n    fc_class_score_s = mx.symbol.FullyConnected(data=flatten, num_hidden=num_classes, name='fc_class_score')\n    softmax1 = mx.symbol.SoftmaxOutput(data=fc_class_score_s, label=class_labels, name='softmax_hard')\n\n    import symbol_resnet  # Teacher model\n    fc_class_score_t = symbol_resnet.get_symbol(net_depth=resnet_layer_num, num_class=num_classes, data=data)\n    \n    s_input_for_softmax=fc_class_score_s/Temperature\n    t_input_for_softmax=fc_class_score_t/Temperature\n\n    t_soft_labels=mx.symbol.softmax(t_input_for_softmax, name='teacher_soft_labels')\n    softmax2 = mx.symbol.SoftmaxOutput(data=s_input_for_softmax, label=t_soft_labels, name='softmax_soft',grad_scale=mimic_weight)\n    group=mx.symbol.Group([softmax1,softmax2])\n    group.save('group2-symbol.json')\n\n    return group\n```\n`tensorflow`代码示例如下：\n\n```python\n# 将类别标签进行one-hot编码\none_hot = tf.one_hot(y, n_classes,1.0,0.0) # n_classes为类别总数, n为类别标签\n# one_hot = tf.cast(one_hot_int, tf.float32)\nteacher_tau = tf.scalar_mul(1.0/args.tau, teacher) # teacher为teacher模型直接输出张量, tau为温度系数T\nstudent_tau = tf.scalar_mul(1.0/args.tau, student) # 将模型直接输出logits张量student处于温度系数T\nobjective1 = tf.nn.sigmoid_cross_entropy_with_logits(student_tau, one_hot)\nobjective2 = tf.scalar_mul(0.5, tf.square(student_tau-teacher_tau))\n\"\"\"\nstudent模型最终的损失函数由两部分组成：\n第一项是由小模型的预测结果与大模型的“软标签”所构成的交叉熵（cross entroy）;\n第二项为预测结果与普通类别标签的交叉熵。\n\"\"\"\ntf_loss = (args.lamda*tf.reduce_sum(objective1) + (1-args.lamda)*tf.reduce_sum(objective2))/batch_size\n```\n\n`tf.scalar_mul` 函数为对 `tf` 张量进行固定倍率 `scalar` 缩放函数。**一般 T 的取值在 1 - 20 之间，这里我参考了开源代码，取值为 3**。我发现在开源代码中 `student` 模型的训练，有些是和 `teacher` 模型一起训练的，有些是 teacher 模型训练好后直接指导 student 模型训练。\n\n## 六，浅层/轻量网络\n\n浅层网络：通过设计一个更浅（层数较少）结构更紧凑的网络来实现对复杂模型效果的逼近, 但是浅层网络的表达能力很难与深层网络相匹敌。因此，这种设计方法的局限性在于只能应用解决在较为简单问题上。如分类问题中类别数较少的 `task`。\n\n轻量网络：使用如 `MobilenetV2、ShuffleNetv2` 等轻量网络结构作为模型的 `backbone`可以大幅减少模型参数数量。\n\n## 参考资料\n\n1. [神经网络模型压缩和加速之知识蒸馏](https://www.cnblogs.com/dyl222/p/11079489.html)\n2. [https://github.com/chengshengchan/model_compression/blob/master/teacher-student.py](https://github.com/chengshengchan/model_compression/blob/master/teacher-student.py)\n3. [https://github.com/dkozlov/awesome-knowledge-distillation](https://github.com/dkozlov/awesome-knowledge-distillation)\n4. [XNOR-Net](https://arxiv.org/abs/1603.05279])\n5. 解析卷积神经网络-深度学习实践手册\n6. [知识蒸馏（Knowledge Distillation）简述（一）](https://zhuanlan.zhihu.com/p/81467832)\n"
  },
  {
    "path": "6-model_compression/神经网络量化基础.md",
    "content": "- [1，模型量化概述](#1模型量化概述)\r\n  - [1.1，模型量化优点](#11模型量化优点)\r\n  - [1.2，模型量化的方案](#12模型量化的方案)\r\n    - [1.2.1，PTQ 理解](#121ptq-理解)\r\n  - [1.3，量化的分类](#13量化的分类)\r\n    - [1.3.1，线性量化概述](#131线性量化概述)\r\n- [2，量化算术](#2量化算术)\r\n  - [2.1，定点和浮点](#21定点和浮点)\r\n  - [2.2，量化浮点](#22量化浮点)\r\n  - [2.2，量化算术](#22量化算术)\r\n- [3，量化方法的改进](#3量化方法的改进)\r\n  - [3.1，浮点数动态范围选择](#31浮点数动态范围选择)\r\n  - [3.2，最大最小值（MinMax）](#32最大最小值minmax)\r\n  - [3.3，滑动平均最大最小值(MovingAverageMinMax)](#33滑动平均最大最小值movingaverageminmax)\r\n  - [3.4，KL 距离采样方法(Kullback–Leibler divergence)](#34kl-距离采样方法kullbackleibler-divergence)\r\n  - [3.5，总结](#35总结)\r\n- [4，量化实战经验](#4量化实战经验)\r\n- [参考资料](#参考资料)\r\n\r\n> 本文为对目前线性量化优点、原理、方法和实战内容的总结，主要参考 [神经网络量化简介](https://jackwish.net/2019/neural-network-quantization-introduction-chn.html) 并加以自己的理解和总结，适合初学者阅读和自身复习用。\r\n\r\n## 1，模型量化概述\r\n\r\n### 1.1，模型量化优点\r\n\r\n模型量化是指将神经网络的浮点算法转换为定点。量化有一些相似的术语，低精度（Low precision）可能是常见的。\r\n\r\n- 低精度模型表示模型权重数值格式为 `FP16`（半精度浮点）或者 `INT8`（8位的定点整数），但是目前低精度往往就指代 `INT8`。\r\n- 常规精度模型则一般表示模型权重数值格式为 `FP32`（32位浮点，单精度）。\r\n- 混合精度（Mixed precision）则在模型中同时使用 `FP32` 和 `FP16` 的权重数值格式。 `FP16` 减少了一半的内存大小，但有些参数或操作符必须采用 `FP32` 格式才能保持准确度。\r\n\r\n模型量化有以下好处：\r\n\r\n> 参考[ TensorFlow 模型优化：模型量化-张益新](https://mp.weixin.qq.com/s/9QeVVESP3_rBZ6n_D96lwg)\r\n\r\n+ **减小模型大小**：如 `int8` 量化可减少 `75%` 的模型大小，`int8` 量化模型大小一般为 `32` 位浮点模型大小的 `1/4`：\r\n    + 减少存储空间：在端侧存储空间不足时更具备意义。\r\n    + 减少内存占用：更小的模型当然就意味着不需要更多的内存空间。\r\n    + 减少设备功耗：内存耗用少了推理速度快了自然减少了设备功耗；\r\n+ **加快推理速度**，访问一次 `32` 位浮点型可以访问四次 `int8` 整型，整型运算比浮点型运算更快；`CPU` 用 `int8` 计算的速度更快\r\n+ **某些硬件加速器如 DSP/NPU 只支持 int8**。比如有些微处理器属于 `8` 位的，低功耗运行浮点运算速度慢，需要进行 `8bit` 量化。\r\n\r\n总结：模型量化主要意义就是加快模型端侧的推理速度，并降低设备功耗和减少存储空间，\r\n\r\n工业界一般只使用 `INT8` 量化模型，如 `NCNN`、`TNN` 等移动端模型推理框架都支持模型的 `INT8` 量化和量化模型的推理功能。\r\n\r\n通常，可以根据 `FP32` 和 `INT8` 的转换机制对**量化模型推理方案**进行分类。一些框架简单地引入了 `Quantize` 和 `Dequantize` 层，当从卷积或全链接层送入或取出时，它将 `FP32` 转换为 `INT8` 或相反。在这种情况下，如下图的上半部分所示，模型本身和输入/输出采用 `FP32` 格式。深度学习推理框架加载模型时，重写网络以插入 `Quantize` 和 `Dequantize` 层，并将权重转换为 `INT8` 格式。\r\n> 注意，之所以要插入反量化层（`Dequantize`），是因为量化技术的早期，只有卷积算子支持量化，但实际网络中还包含其他算子，而其他算子又只支持 `FP32` 计算，因此需要把 INT8 转换成 FP32。但随着技术的迭代，后期估计会逐步改善乃至消除  `Dequantize` 操作，达成全网络的量化运行，而不是部分算子量化运行。\r\n\r\n![量化模型的推理](../data/images/模型量化/量化模型的推理.svg)\r\n\r\n> 图四：混合 FP32/INT8 和纯 INT8 推理。红色为 FP32，绿色为 INT8 或量化。\r\n\r\n其他一些框架将网络整体转换为 `INT8` 格式，因此在推理期间没有格式转换，如上图的下半部分。该方法要求算子（`Operator`）都支持量化，因为运算符之间的数据流是 `INT8`。对于尚未支持的那些，它可能会回落到 `Quantize/Dequantize` 方案。\r\n\r\n### 1.2，模型量化的方案\r\n\r\n在实践中将浮点模型转为量化模型的方法有以下三种方法：\r\n\r\n1. `data free`：不使用校准集，传统的方法直接将浮点参数转化成量化数，使用上非常简单，但是一般会带来很大的精度损失，但是高通最新的论文 `DFQ` 不使用校准集也得到了很高的精度。\r\n2. `calibration`：基于校准集方案，通过输入少量真实数据进行统计分析。很多芯片厂商都提供这样的功能，如 `tensorRT`、高通、海思、地平线、寒武纪\r\n3. `finetune`：基于训练 `finetune` 的方案，将量化误差在训练时仿真建模，调整权重使其更适合量化。好处是能带来更大的精度提升，缺点是要修改模型训练代码，开发周期较长。\r\n\r\n`TensorFlow` 框架按照量化阶段的不同，其模型量化功能分为以下两种：\r\n\r\n+ Post-training quantization `PTQ`（训练后量化、离线量化）；\r\n+ Quantization-aware training `QAT`（训练时量化，伪量化，在线量化）。\r\n\r\n#### 1.2.1，PTQ 理解\r\n\r\n`PTQ` `Post Training Quantization` 是训练后量化，也叫做离线量化，根据量化零点 $x_{zero\\_point}$ 是否为 `0`，训练后量化分为对称量化和非对称量化；根据数据通道顺序 `NHWC`(TensorFlow) 这一维度区分，训练后量化又分为逐层量化和逐通道量化。目前 `nvidia` 的 `TensorRT` 框架中使用了逐层量化的方法，每一层采用同一个阈值来进行量化。逐通道量化就是对每一层每个通道都有各自的阈值，对精度可以有一个很好的提升。\r\n\r\n### 1.3，量化的分类\r\n\r\n目前已知的加快推理速度概率较大的量化方法主要有：\r\n\r\n1. **二值化**，其可以用简单的位运算来同时计算大量的数。对比从 nvdia gpu 到 x86 平台，1bit 计算分别有 5 到128倍的理论性能提升。且其只会引入一个额外的量化操作，该操作可以享受到 SIMD（单指令多数据流）的加速收益。\r\n2. **线性量化**(最常见)，又可细分为非对称，对称和 `ristretto` 几种。在 `nvdia gpu`，`x86`、`arm` 和 部分 `AI` 芯片平台上，均支持 `8bit` 的计算，效率提升从 `1` 倍到 `16` 倍不等，其中 `tensor core` 甚至支持 `4bit`计算，这也是非常有潜力的方向。线性量化引入的额外量化/反量化计算都是标准的向量操作，因此也可以使用 `SIMD` 进行加速，带来的额外计算耗时不大。\r\n3. **对数量化**，一种比较特殊的量化方法。两个同底的幂指数进行相乘，那么等价于其指数相加，降低了计算强度。同时加法也被转变为索引计算。目前 `nvdia gpu`，`x86`、`arm` 三大平台上没有实现对数量化的加速库，但是目前已知海思 `351X` 系列芯片上使用了对数量化。\r\n\r\n#### 1.3.1，线性量化概述\r\n\r\n与非线性量化不同，线性量化采用均匀分布的聚类中心，原始浮点数据和量化后的定点数据存在一个简单的线性变换关系，因为卷积、全连接等网络层本身只是简单的线性计算，因此线性量化中可以直接用量化后的数据进行直接计算。\r\n\r\n## 2，量化算术\r\n\r\n**模型量化过程可以分为两部分：将模型从 FP32 转换为 INT8，以及使用 INT8 进行推理**。本节说明这两部分背后的算术原理。如果不了解基础算术原理，在考虑量化细节时通常会感到困惑。\r\n\r\n### 2.1，定点和浮点\r\n\r\n**定点和浮点都是数值的表示**（representation），它们区别在于，将整数（integer）部分和小数（fractional）部分分开的点，点在哪里。**定点保留特定位数整数和小数，而浮点保留特定位数的有效数字（significand）和指数（exponent）**。\r\n\r\n绝大多数现代的计算机系统采纳了**浮点数表示方式**，这种表达方式利用科学计数法来表达实数。即用一个尾数(Mantissa，尾数有时也称为有效数字，它实际上是有效数字的非正式说法)，一个基数(Base)，一个指数(Exponent)以及一个表示正负的符号来表达实数。具体组成如下：\r\n\r\n- 第一部分为 `sign` 符号位 $s$，占 1 bit，用来表示正负号；\r\n- 第二部分为 `exponent` 指数偏移值 $k$，占 8 bits，用来表示其是 2 的多少次幂；\r\n- 第三部分是 `fraction` 分数值（有效数字） $M$，占 23 bits，用来表示该浮点数的数值大小。\r\n\r\n基于上述表示，浮点数的值可以用以下公式计算：\r\n\r\n$$(-1)^s \\times M \\times 2^k$$\r\n\r\n值得注意是，上述公式隐藏了一些细节，如指数偏移值 $k$ 使用的时候需要加上一个固定的偏移值。\r\n\r\n比如 `123.45` 用十进制科学计数法可以表示为 $1.2345\\times 10^2$，其中 `1.2345` 为尾数，`10` 为基数，`2` 为指数。\r\n\r\n单精度浮点类型 `float` 占用 `32bit`，所以也称作 `FP32`；双精度浮点类型 `double` 占用 `64bit`。\r\n\r\n![定点和浮点的格式和示例](../data/images/模型量化/定点和浮点的格式和示例.jpg)\r\n> 图五：定点和浮点的格式和示例。\r\n\r\n### 2.2，量化浮点\r\n\r\n`32-bit` 浮点数和 `8-bit` 定点数的表示范围如下表所示：\r\n\r\n|数据类型|最小值|最大值|\r\n|------------|---------|----------|\r\n|`FP32`| -3.4e38|3.4e38|\r\n|`int8`|-128|128|\r\n|`uint8`|0|255|\r\n\r\n神经网络的推理由浮点运算构成。`FP32` 和 `INT8` 的值域是 $[(2−2^{23})×2^{127},(2^{23}−2)\\times 2^{127}]$ 和 $[−128,127]$，而取值数量大约分别为 $2^{32}$ 和 $2^8$ 。`FP32` 取值范围非常广，因此，将网络从 `FP32` 转换为 `INT8` 并不像数据类型转换截断那样简单。但是，一般神经网络权重的值分布范围很窄，非常接近零。图八给出了 `MobileNetV1` 中十层（拥有最多值的层）的权重分布。\r\n\r\n![十层 MobileNetV1 的权重分布](../data/images/模型量化/十层%20MobileNetV1%20的权重分布.svg)\r\n> 图八：十层 MobileNetV1 的权重分布。\r\n\r\n根据偏移量 $Z$ 是否为 0，可以将浮点数的线性量化分为两类-对称量化和非对称量化。\r\n\r\n当浮点值域落在 $(-1,1)$ 之间，权重浮点数据的量化运算可使用下式的方法将 FP32 映射到 INT8，这是**对称量化**。其中 $x_{float}$ 表示 FP32 权重， $x_{quantized}$ 表示量化的 INT8 权重，$x_{scale}$ 是缩放因子（映射因子、量化尺度（范围）/ `float32` 的缩放因子）。\r\n\r\n$$x_{float} = x_{scale} \\times x_{quantized}$$\r\n\r\n对称量化的浮点值和 `8` 位定点值的映射关系如下图，从图中可以看出，对称量化就是将一个 `tensor` 中的 $[-max(|\\mathrm{x}|),max(|\\mathrm{x}|)]$ 内的 `FP32` 值分别映射到 `8 bit` 数据的 `[-128, 127]` 的范围内，中间值按照线性关系进行映射，称这种映射关系是对称量化。可以看出，对称量化的浮点值和量化值范围都是相对于零对称的。\r\n\r\n![对称量化](../data/images/quantization/对称量化.png)\r\n\r\n因为对称量化的缩放方法可能会将 FP32 零映射到 INT8 零，但我们不希望这种情况出现，于是出现了数字信号处理中的均一量化，即**非对称量化**。数学表达式如下所示，其中 $x_{zero\\_point}$ 表示量化零点（量化偏移）。\r\n\r\n$$x_{float} = x_{scale} \\times (x_{quantized} - x_{zero\\_point})$$\r\n\r\n大多数情况下量化是选用无符号整数，即 `INT8` 的值域就为 $[0,255]$ ，这种情况，显然要用非对称量化。非对称量化的浮点值和 `8` 位定点值的映射关系如下图：\r\n\r\n![非对称量化](../data/images/quantization/非对称量化.png)\r\n\r\n总的来说，**权重量化浮点值可以分为两个步骤**：\r\n\r\n1. 通过在权重张量（Tensor）中找到 $min$ 和 $max$ 值从而确定 $x_{scale}$ 和$x_{zero\\_point}$。\r\n2. 将权重张量的每个值从 FP32 转换为 INT8 。\r\n$$\r\n\\begin{align}\r\nx_{float} &\\in [x_{float}^{min}, x_{float}^{max}] \\\\\r\nx_{scale} &= \\frac{x_{float}^{max} - x_{float}^{min}}{x_{quantized}^{max} - x_{quantized}^{min}} \\\\\r\nx_{zero\\_point} &= x_{quantized}^{max} - x_{float}^{max} \\div x_{scale} \\\\\r\nx_{quantized} &= x_{float} \\div x_{scale} + x_{zero\\_point}\r\n\\end{align}$$\r\n\r\n注意，当浮点运算结果不等于整数时，需要**额外的舍入步骤**。例如将 FP32 值域 [−1,1] 映射到 INT8 值域 [0,255]，有 $x_{scale}=\\frac{2}{255}$，而$x_{zero\\_point}= 255−\\frac{255}{2}≈127$。\r\n\r\n注意，量化过程中存在误差是不可避免的，就像数字信号处理中量化一样。**非对称算法一般能够较好地处理数据分布不均匀的情况**。\r\n\r\n### 2.2，量化算术\r\n\r\n量化的一个重要议题是用量化算术表示非量化算术，即量化神经网络中的 `INT8` 计算是描述常规神经网络的 `FP32` 计算，对应的就是反量化过程，也就是如何将 `INT8` 的定点数据反量化成 `FP32` 的浮点数据。\r\n\r\n下面的等式 5-10 是反量化乘法 $x_{float} \\cdot y_{float}$ 的过程。对于给定神经网络，输入 $x$、权重 $y$ 和输出 $z$ 的缩放因子肯定是已知的，因此等式 14 的 $Multiplier_{x,y,z} = \\frac{x_{scale}y_{scale}}{z_{scale}}$ 也是已知的，在反量化过程之前可预先计算。因此，除了 $Multiplier_{x,y,z}$ 和 $(x_{quantized} - x_{zero\\_point})\\cdot (y_{quantized} - y_{zero\\_point})$ 之间的乘法外，等式 16 中的运算都是整数运算。\r\n\r\n\r\n$$\\begin{align}\r\nz_{float} & = x_{float} \\cdot y_{float} \\\\\r\nz_{scale} \\cdot (z_{quantized} - z_{zero\\_point})\r\n& = (x_{scale} \\cdot (x_{quantized} - x_{zero\\_point})) \\cdot\r\n(y_{scale} \\cdot (y_{quantized} - y_{zero\\_point})) \\\\\r\nz_{quantized} - z_{zero\\_point}\r\n&= \\frac{x_{scale} \\cdot y_{scale}}{z_{scale}} \\cdot\r\n(x_{quantized} - x_{zero\\_point}) \\cdot (y_{quantized} - y_{zero\\_point}) \\\\\r\nz_{quantized}\r\n&= \\frac{x_{scale} \\cdot y_{scale}}{z_{scale}} \\cdot\r\n(x_{quantized} - x_{zero\\_point}) \\cdot (y_{quantized} - y_{zero\\_point}) + z_{zero\\_point} \\\\\r\nMultiplier_{x,y,z} &= \\frac{x_{scale} \\cdot y_{scale}}{z_{scale}} \\\\\r\nz_{quantized}\r\n&= Multiplier_{x,y,z} \\cdot (x_{quantized} - x_{zero\\_point}) \\cdot\r\n(y_{quantized} - y_{zero\\_point}) + z_{zero\\_point} \\\\\r\n\\end{align}$$\r\n> 等式：反量化算术过程。\r\n\r\n对于等式 `10` 可以应用的大多数情况，$quantized$ 和 $zero\\_point$ 变量 (x,y) 都是 `INT8` 类型，$scale$ 是 `FP32`。实际上两个 `INT8` 之间的算术运算会累加到 `INT16` 或 `INT32`，这时 `INT8` 的值域可能无法保存运算结果。例如，对于 $x_{quantized}=20$、$x_{zero\\_point} = 50$ 的情况，有 $(x_{quantized} − x_{zero_point}) = −30$ 超出 `INT8` 值范围 $[0,255]$。\r\n\r\n数据类型转换可能将 $Multiplier_{x,y,z} \\cdot (x_{quantized} - x_{zero\\_point}) \\cdot (y_{quantized} - y_{zero\\_point})$ 转换为 INT32 或 INT16，和 $z_{zero\\_point}$ 一起确保计算结果几乎全部落入 INT8 值域 [0,255] 中。\r\n\r\n对于以上情况，在工程中，比如对于卷积算子的计算，`sum(x*y)` 的结果需要用 INT32 保存，同时，`b` 值一般也是 `INT32` 格式的，之后再 `requantize` (重新量化)成 `INT8`。\r\n\r\n## 3，量化方法的改进\r\n\r\n量化浮点部分中描述权重浮点量化方法是非常简单的。在深度学习框架的早期开发中，这种简单的方法能快速跑通 `INT8` 推理功能，然而采用这种方法的网络的预测准确度通常会出现明显的下降。\r\n\r\n虽然 FP32 权重的值域很窄，在这值域中数值点数量却很大。以上文的缩放为例，$[−1,1]$ 值域中 $2^{31}$（是的，基本上是总得可表示数值的一半）个 FP32 值被映射到 $256$ 个 INT8 值。\r\n\r\n+ 量化类型：（`SYMMETRIC`） 对称量化和 (`NON-SYMMETRIC`） 非对称量化；\r\n+ 量化算法：`MINMAX`、`KL` 散度、`ADMM`；\r\n+ 权重量化类型：`per-channel` `per-layer`；\r\n\r\n采用普通量化方法时，靠近零的浮点值在量化时没有精确地用定点值表示。因此，与原始网络相比，量化网络一般会有明显的精度损失。对于线性（均匀）量化，这个问题是不可避免的。\r\n\r\n同时值映射的精度是受由 $x_{float}^{min}$ 和 $x_{float}^{max}$ 得到的 $x_{scale}$ 显著影响的。并且，如图十所示，权重中邻近 $x_{float}^{min}$ 和 $x_{float}^{max}$ 附近的值通常是可忽略的，其实就等同于**映射关系中浮点值的 `min` 和 `max` 值是可以通过算法选择的**。\r\n\r\n![图十将浮点量化为定点时调整最小值-最大值](../data/images/模型量化/图十将浮点量化为定点时调整最小值-最大值.jpg)\r\n> 图十将浮点量化为定点时调整最小值-最大值。\r\n\r\n上图展示了可以调整 `min/max` 来选择一个值域，使得值域的值更准确地量化，而范围外的值则直接映射到定点的 min/max。例如，当从原始值范围 $[−1,1]$ 中选定$x_{min}^{float} = −0.9$ 和 $x_{max}^{float} = 0.8$ ，$[−0.9,0.8]$ 中的值将能更准确地映射到 $[0,255]$ 中，而 $[−1,−0.9]$ 和 $[0.8,1]$ 中的值分别映射为 $0$ 和 $255$。\r\n\r\n### 3.1，浮点数动态范围选择\r\n\r\n> 参考[干货：深度学习模型量化（低精度推理）大总结](https://mp.weixin.qq.com/s/LR3Z2rlkxdl-1KnsU734VQ)。\r\n\r\n通过前文对量化算数的理解和上面两种量化算法的介绍我们不难发现，为了计算 `scale` 和 `zero_point`，我们需要知道 `FP32 weight/activation` 的实际动态范围。对于推理过程来说，`weight` 是一个常量张量，动态范围是固定的，`activation` 的动态范围是变化的，它的实际动态范围必须经过采样获取（一般把这个过程称为数据校准(`calibration`)）。\r\n\r\n将浮点量化转为定点时调整最小值/最大值（**值域调整**），也就是浮点数动态范围的选择，**动态范围的选取直接决定了量化数据的分布情况，处于动态范围之外的数据将被映射成量化数据的边界点，即值域的选择直接决定了量化的误差**。\r\n\r\n目前各大深度学习框架和三大平台的推理框架使用最多的有最大最小值（`MinMax`）、滑动平均最大最小值（`MovingAverageMinMax`）和 `KL` 距离（Kullback-Leibler divergence）三种方法，去确定浮点数的动态范围。如果量化过程中的每一个 `FP32` 数值都在这个实际动态范围内，我们一般称这种为不饱和状态；反之如果出现某些 `FP32` 数值不在这个实际动态范围之内我们称之为饱和状态。\r\n\r\n### 3.2，最大最小值（MinMax）\r\n\r\n`MinMax` 是使用最简单也是较为常用的一种采样方法。基本思想是直接从 `FP32` 张量中选取最大值和最小值来确定实际的动态范围，如下公式所示。\r\n$x_{min} = \\left\\{\\begin{matrix}min(X) & if\\ x_{min} = None \\\\  min(x_{min}, min(X))  & otherwise\\end{matrix}\\right.$\r\n\r\n$x_{max} = \\left\\{\\begin{matrix}max(X) & if\\ x_{max} = None \\\\  max(x_{max}, max(X))  & otherwise\\end{matrix}\\right.$\r\n\r\n对 `weights` 而言，这种采样方法是不饱和的，但是对于 `activation` 而言，如果采样数据中出现离群点，则可能明显扩大实际的动态范围，比如实际计算时 `99%` 的数据都均匀分布在 `[-100, 100]` 之间，但是在采样时有一个离群点的数值为 `10000`，这时候采样获得的动态范围就变成 `[-100, 10000]`。\r\n\r\n### 3.3，滑动平均最大最小值(MovingAverageMinMax)\r\n\r\n与 `MinMax` 算法直接替换不同，MovingAverageMinMax 会采用一个超参数 `c` (Pytorch 默认值为0.01)逐步更新动态范围。\r\n$x_{min} = \\left\\{\\begin{matrix}min(X) & if x_{min} = None \\\\ (1-c)x_{min}+c \\; min(X) & otherwise\\end{matrix}\\right.$\r\n\r\n$x_{max} = \\left\\{\\begin{matrix}max(X) & if x_{max} = None \\\\  (1-c)x_{max}+c \\; max(X) & otherwise\\end{matrix}\\right.$\r\n这种方法获得的动态范围一般要小于实际的动态范围。对于 weights 而言，由于不存在采样的迭代，因此 MovingAverageMinMax 与 MinMax 的效果是一样的。\r\n\r\n### 3.4，KL 距离采样方法(Kullback–Leibler divergence)\r\n\r\n理解 KL 散度方法之前，我们先看下 `TensorRT` 关于值域范围阈值选择的一张图：\r\n\r\n![阈值选择](../data/images/quantization/阈值选择.png)\r\n\r\n这张图展示的是不同网络结构的不同 `layer` 的激活值分布统计图，横坐标是激活值，纵坐标是统计数量的归一化表示，而不是绝对数值统计；图中有卷积层和池化层，它们之间分布很不相同，因此合理的量化方法应该是适用于不同的激活值分布，并且减小信息损失，因为从 `FP32` 到 `INT8` 其实也是一种信息再编码的过程。\r\n\r\n简单的将一个 tensor 中的 -|max| 和 |max| FP32 value 映射为 -127 和 127 ，中间值按照线性关系进行映射，这种映射关系为不饱和的（No saturation），即对称的。对于这种简单的量化浮点方法，试验结果显示会导致比较大的精度损失。\r\n\r\n通过上图可以分析出，线性量化中使用简单的量化浮点方法导致精度损失较大的原因是：\r\n\r\n+ 上图的激活值统计针对的是一批图片，不同图片输出的激活值不完全相同，所以图中是多条曲线而不是一条曲线，曲线中前面一部分数据重合在一起了（红色虚线），说明不同图片生成的大部分激活值其分布是相似的；但是在曲线的右边，激活值比较大时（红色实现圈起来的部分），曲线不重复了，一个激活值会对应多个不同的统计量，这时激活值分布是比较乱的。\r\n+ 曲线后面激活值分布比较乱的部分在整个网络层占是占少数的（比如 $10^-9$, $10^-7$, $10^-3$），因此曲线后面的激活值分布部分可以不考虑到映射关系中，只保留激活值分布的主方向。\r\n\r\n一般认为量化之后的数据分布与量化前的数据分布越相似，量化对原始数据信息的损失也就越小，即量化算法精度越高。`KL` 距离(也叫 `KL` 散度)一般被用来度量两个分布之间的相似性。这里的数据分布都是离散形式的，其离散数据的 KL 散度公式如下：\r\n\r\n$$D_{KL}(P \\| Q) = \\sum_i P(i)log_{a} \\frac{P(i)}{Q(i)} = \\sum_i P(i)[logP(x) - log Q(x)]$$\r\n\r\n式中 P 和 Q 分布表示量化前 FP32 的数据分布和量化后的 INT8 数据分布。注意公式要求 P、Q 两个统计直方图长度一样（也就是 bins 的数量一样）。\r\n\r\nTensorRT 使用 KL 散度算法进行量化校准的过程：首先在校准集上运行 FP32 推理，然后对于网络每一层执行以下步骤：\r\n\r\n1. 收集激活输出的直方图。\r\n2. 生成许多具有不同饱和度阈值的量化分布。\r\n3. 选择最小化 KL_divergence(ref_distr, quant_distr) 的阈值 `T`，并确定 `Scale`。\r\n\r\n以上使用校准集的模型量化过程通常只需几分钟时间。\r\n\r\n### 3.5，总结\r\n\r\n+ 对称的，不饱和的线性量化，会导致精度损失较大；\r\n+ 通过最小化 `KL` 散度来选择 饱和量化中的 阈值 `|T|`;\r\n\r\n## 4，量化实战经验\r\n\r\n> 参考[【商汤泰坦公开课】模型量化了解一下？](https://www.sensetime.com/cn/technology-new-detail/3563?categoryId=53)\r\n\r\n1，量化是一种已经获得了工业界认可和使用的方法，在训练 (Training) 中使用 `FP32` 精度，在推理 (Inference) 期间使用 `INT8` 精度的这套量化体系已经被包括 `TensorFlow`，`TensorRT`，`PyTorch`，`MxNet` 等众多深度学习框架和启用，地平线机器人、海思、安霸等众多 `AI` 芯片厂商也在深度学习工具链中提供了各自版本的模型量化功能。\r\n2，量化是一个大部分硬件平台都会支持的，因此比较常用；知识蒸馏有利于获得小模型，还可以进一步提升量化模型的精度，所以这个技巧也会使用，尤其是在有已经训好比较强的大模型的基础上会非常有用。剪枝用的会相对较少，因为可以被网络结构搜索覆盖。\r\n\r\n## 参考资料\r\n\r\n- [NCNN Conv量化详解（一）](https://zhuanlan.zhihu.com/p/71881443)\r\n- [卷积神经网络优化算法](https://jackwish.net/2019/convolution-neural-networks-optimization.html)\r\n- [神经网络量化简介](https://jackwish.net/2019/neural-network-quantization-introduction-chn.html)\r\n- [QNNPACK 实现揭秘](https://jackwish.net/2019/reveal-qnnpack-implementation.html)\r\n- 《8-bit Inference with TensorRT》\r\n"
  },
  {
    "path": "7-high-performance_computing/0-处理器基础知识.md",
    "content": "- [一，什么是处理器](#一什么是处理器)\n- [二，指令集基础](#二指令集基础)\n  - [什么是 ISA](#什么是-isa)\n  - [ISA 功能](#isa-功能)\n- [三，CPU 设计与实现](#三cpu-设计与实现)\n  - [整数范围](#整数范围)\n  - [时钟频率](#时钟频率)\n  - [指令周期（Instruction cycle）](#指令周期instruction-cycle)\n  - [指令流水线（Instruction pipeline）](#指令流水线instruction-pipeline)\n  - [指令并行（Instruction-level parallelism）](#指令并行instruction-level-parallelism)\n  - [数据并行（Data parallelism）：](#数据并行data-parallelism)\n  - [并发与并行](#并发与并行)\n  - [线程级并行(Thread-Lever Parallelism)](#线程级并行thread-lever-parallelism)\n  - [性能](#性能)\n- [参考资料](#参考资料)\n\n> 本文的知识点比较零散，主要是关于处理器的一些基本知识，大部分内容来源于参考资料并给出了自己的理解和整理。\n\n## 一，什么是处理器\n\n先描述下一般处理器的概念，[维基百科](https://en.wikipedia.org/wiki/Processor_(computing))的定义是 “In computing, a processor is an electronic circuit which performs operations on some external data source, usually memory or some other data stream”。最为常见的处理器有 `CPU`（可以运行任何程序）、`GPU`（图形图像处理）和 `DSP`(处理数字信号)，还有专门用来做 `DNN` 应用神经网络处理器。\n> 处理器或处理单元是对外部数据源（通常是内存或其他数据流）执行操作的电子组件（数字电路）。\n\n`CPU` 的主要运作原理，不论其外观，都是执行储存于被称为程序里的一系列指令。在此讨论的是遵循普遍的冯·诺伊曼结构（von Neumann architecture）设计的设备。程序以一系列数字储存在计算机存储器中。差不多所有的冯·诺伊曼 `CPU` 的运作原理可分为四个阶段：**提取、解码、执行和写回**。\n\n\n而专用处理器就是针对**特定应用或者领域**的处理器，类似于是我们经常说的 Domain Specific Architecture 的概念。\n\n## 二，指令集基础\n\n### 什么是 ISA\n\n指令集(`Instruction Set Architecture`, `ISA`)是计算机抽象模型的一部分，它定义了软件如何控制 `CPU`。`ISA` 充当硬件和软件的接口，指示了处理器能够实现什么功能以及如何实现。简单来说，`ISA` 就是传统上软件和硬件的分界线，是用户和硬件交互的唯一方式。\n\n`ISA` 定义了硬件支持的数据类型、寄存器、硬件如何管理内存、关键特性（如虚拟内存）、微处理器可以执行哪些指令，以及多个 ISA 实现的输入输出模型。ISA 可以通过添加指令或其他功能或通过添加对更大地址和数据值的支持来扩展。\n> 来源 [What is Instruction Set Architecture (ISA)? - Arm](https://www.arm.com/glossary/isa)\n\n### ISA 功能\n\n大多数 `ISA`（典型如 `x86`-Intel CPU 的指令集），将程序的行为描述成每条指令都是顺序执行的，一条指令结束后，下一条在开始。\n\n`ISA` 提供的主要指令可以分为四大类功能：\n1. 执行运算或处理的功能，比如算术操作指令；\n2. 控制程序流，比如循环、判断分支和跳转指令；\n3. 实现数据搬移，如内存到寄存器，寄存器之间数据搬移等指令；\n4. 最后就是一些辅助指令，如 `debug`、中断和 `cache` 之类的指令。\n\n## 三，CPU 设计与实现\n\n### 整数范围\n\n`CPU` 数字表示方法是一个设计上的选择，这个选择影响了设备的工作方式。一些早期的数字计算机内部使用电气模型来表示通用的十进制（基于 `10`进位）记数系统数字。还有一些罕见的计算机使用三进制表示数字。几乎所有的现代的 `CPU` **使用二进制系统来表示数字**，这样数字可以用具有两个值的物理量来表示，例如高低电平等等。\n\n### 时钟频率\n\n主频＝外频×倍频。大部分的 `CPU`，甚至大部分的时序逻辑设备，本质上都是同步的，即它们被设计和使用的前题是假设都在同一个同步信号中工作。\n\n### 指令周期（Instruction cycle）\n\n指令周期是指 **CPU 要执行一条机器指令经过的步骤，由若干机器周期组成**。一般会经历“取指”，“译码”，“发射/执行”和“写回”这些操作。处理器执行程序的过程就是不断重复这几个操作。\n\n### 指令流水线（Instruction pipeline）\n> 在1978年的 Intel 8086 处理器都只能一次执行单指令。 Intel首次在486芯片中开始使用，原理是：当指令之间不存在相关时，它们在流水线中是可以重叠起来并行执行。\n\n当一条指令，完成了“取指”操作，开始进行“译码”的时候，取指模块就可以取程序的下一条指令了，这样可以让这些模块不至于闲着没用，即**指令流水线可以两个以上的指令同时执行**(类似车间流水线)。一般的四层流水线架构如下图所示，**不同的颜色格表示不同的指令**。\n\n![一般的四层流水线架构](../data/images/CPU/一般的四层流水线架构.png)\n\n### 指令并行（Instruction-level parallelism）\n\n同时执行多条指令。比如，一边从 `memory` 读数据，一边进行 `fft` 处理。我们经常听到的超标量（`Superscalar`），超长指令字（`VLIW`），乱序执行（ `Out-of-order execution`）等等技术都是发掘指令级并行的技术。\n\n### 数据并行（Data parallelism）：\n\n一个人指令同时处理多个数据。我们常听到的向量处理器（`vector procesor`），张量处理器（`Tensor processor`）多数都是利用了 `SIMD`（**一条指令可以处理多个数据**，比如一个向量乘法）技术。\n\n### 并发与并行\n\n并发与并行的通俗理解参考[知乎问答-指令级并行，线程级并行，数据级并行区别？线程的概念是什么？](https://www.zhihu.com/question/21823699/answer/111606716)如下：\n\n![并发与并行的通俗理解](../data/images/CPU/并发与并行的通俗理解.png)\n\n另外，如果看并发并行的英文其实更直观:\n- `concurrency` 交替存在, 交替出现; \n- `parallelism` 平行, 两个程序流同时执行, 两条线同时延伸。\n### 线程级并行(Thread-Lever Parallelism)\n\n线程级并行主要由下面两种技术的支撑：\n1. **超线程技术**：2004年，奔腾4实现了Hyper-Threading.（单核心双线程）\n2. **多核技术-物理核心**:  2005年，英特尔宣布他的第一个双核心 EM64T 处理器，和 Pentium D840\n> 超线程技术实现了单个物理核心同时两个线程，也就是别人常说的虚拟内核数。比如单物理核心实现的双线程，它同时可以处理两个线程，它的物理核心数其实是是1个，通过Hyperthreading技术实现的线程级并行(Thread Lever Parallelism)。至于技术细节的实现，这涉及到高速缓存的知识。\n\n**线程级并行的好处**:\n- 当运行多任务时，它减少了之前的操作系统模拟出来的并发，那么用户进行多任务处理时可以运行更多的程序进行并发了。\n- 它可以使单个程序运行更快。（仅当该程序有大量线程可以并行处理时）\n\n> 虽然在 1960 年代已经通过操作系统已经实现了线程级并发, 但这种频繁的上下文切换意味损失了 `CPU` 的处理效率。\n\n### 性能\n\n`CPU` 的性能和速度取决于**时钟频率**（一般以赫兹或十亿赫兹计算，即 `hz` 与 `Ghz`）和**每周期可处理的指令**（`IPC`），两者合并起来就是每秒可处理的指令（`IPS`）。`IPS` 值代表了 `CPU` 在几种人工指令序列下“高峰期”的执行率，指示和应用。\n## 参考资料\n\n\n1. 深入理解计算机系统-第三版\n2. [专用处理器设计](https://mp.weixin.qq.com/s?__biz=MzI3MDQ2MjA3OA==&mid=2247484980&idx=1&sn=0d1b15f17b1b02bda1bf4d81fd6a01ad&chksm=ead1fb25dda672334c3d72a6d958b2a2b3202c2c5dc9d221df6367ef4dec1023e5f079ae5742&scene=178&cur_album_id=1678711252069466118#rd)\n3. https://www.zhihu.com/question/21823699/answer/111606716"
  },
  {
    "path": "7-high-performance_computing/1-卷积算法的优化.md",
    "content": "## 前言\n\n等待更新。\n\n## 参考资料\n1. [im2col方法实现卷积算法](https://zhuanlan.zhihu.com/p/63974249)\n2. [通用矩阵乘（GEMM）优化算法](https://jackwish.net/2019/gemm-optimization.html)\n3. [C/C++内存对齐详解](https://zhuanlan.zhihu.com/p/30007037)\n4. [浮点数在计算机中存储方式]()\n"
  },
  {
    "path": "7-high-performance_computing/2-模型编译优化.md",
    "content": "## 前言\n\n等待更新。"
  },
  {
    "path": "7-high-performance_computing/README.md",
    "content": "## 一，GPU的Hello world: 矩阵相乘\n英伟达GPU架构发展史如下所示:\n\n![英伟达gpu架构发展史](../data/images/gpu_programming/英伟达gpu架构发展史.png)\n\n矩阵相乘程序是GPU编程的 Hello World 程序。编写GPU程序，实质上是一种 `CPU` 和 `GPU` 之间的“互动”，即所谓的异构开发。\n\n### 1.1，GPU编程的一些名词定义\n\n`GPU`编程会涉及到一些名词概念:\n- `Host`: 代表 CPU。\n- `Device`: 代表 GPU。\n- `Host memory`: RAM 内存。\n- `Device memory`: GPU上 的存储。\n- `Kernal function`: GPU 函数，执行在 device 上面，调用者是 host。\n- `Device function`: GPU 函数，执行在 device 上面，调用者是 kernal function 或者 device function。\n\n### 1.2，GPU程序的执行流程\n\n下图可视化了 `GPU` 程序的流程:\n\n![GPU程序的流程](../data/images/gpu_programming/GPU程序的流程.png)\n\n1. 把数据从 host memory 拷贝到 device memory上。\n2. 配置 `kernal function` 的参数，参数有两种：一种用中括号[ ]，一种用小括号( )。中括号的参数是 threadperblock 和 blockpergrid；小括号就是那种普通函数的参数。\n3. 几千个线程同时调用同一个 kernal function(CUDA 的核函数)，在 GPU 里面进行计算。（`kernal function` 的编写，是一门技术。）\n4. 把 GPU 里面的运算结果，从 `device memory` 拷贝回 `host memory`。THE END。\n\n\n## 参考资料\n\n1. [github-CUDA 基础](https://github.com/Tony-Tan/CUDA_Freshman)\n2. [知乎专栏-CUDA编程入门](https://www.zhihu.com/column/c_1188568938097819648)\n3. [《CUDA C Programming Guide》(《CUDA C 编程指南》)导读](https://zhuanlan.zhihu.com/p/53773183)\n4. [CSDN专栏-NVIDIA CUDA 并行编程](https://blog.csdn.net/sunmc1204953974/category_6156113.html)\n5. [英伟达GPU架构演进近十年，从费米到安培](https://zhuanlan.zhihu.com/p/413145211)\n6. [NVIDIA CUDA C++ Programming Guide](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html)"
  },
  {
    "path": "7-high-performance_computing/通用矩阵乘算法从入门到实践.md",
    "content": "- [一，背景知识](#一背景知识)\n  - [1.1，Linux 查看 CPU 和 Cache 信息](#11linux-查看-cpu-和-cache-信息)\n  - [1.2，Windows查看cpu和cache信息](#12windows查看cpu和cache信息)\n  - [1.3， 尝试分析 `init()` 函数](#13-尝试分析-init-函数)\n- [二，优化矩阵乘法](#二优化矩阵乘法)\n  - [2.1，算法层面优化](#21算法层面优化)\n    - [2.1.1，`Strassen` 算法](#211strassen-算法)\n    - [2.1.2，Coppersmith–Winograd 算法](#212coppersmithwinograd-算法)\n  - [2.2，指令层面优化](#22指令层面优化)\n  - [2.3，访存优化](#23访存优化)\n    - [2.3.1，优化方法 1 (改进访存局部性)](#231优化方法-1-改进访存局部性)\n    - [2.3.2，优化方法2(分块矩阵+改进访存局部性)](#232优化方法2分块矩阵改进访存局部性)\n- [三，优化方法集合的完整代码](#三优化方法集合的完整代码)\n- [参考资料](#参考资料)\n\n## 一，背景知识\n\n**实践作业1**：如何通过工具得到自己当前使用 `PC` 的 `Cache` 信息（15分）。包括 `L1/L2/L3 Cache`（数据和指令）的大小，`Cache Line` 的大小。\n\n### 1.1，Linux 查看 CPU 和 Cache 信息\n1，`Linux` 查看 `cpu` 信息命令：`cat /proc/cpuinfo`。\n\n```bash\n(base) harley@harley-pc:/sys/devices/system/cpu/cpu0/cache/index3$ cat /proc/cpuinfo\nprocessor    : 0\nvendor_id    : GenuineIntel\ncpu family    : 6\nmodel        : 158\nmodel name    : Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz\nstepping    : 10\nmicrocode    : 0xb4\ncpu MHz        : 3192.005\ncache size    : 12288 KB\nphysical id    : 0\nsiblings    : 1\ncore id        : 0\ncpu cores    : 1\napicid        : 0\ninitial apicid    : 0\nfpu        : yes\nfpu_exception    : yes\ncpuid level    : 22\nwp        : yes\nflags        : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid rdseed adx smap clflushopt xsaveopt xsavec xsaves arat md_clear flush_l1d arch_capabilities\nbugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit srbds\nbogomips    : 6384.01\nclflush size    : 64\ncache_alignment    : 64\naddress sizes    : 43 bits physical, 48 bits virtual\npower management:\n...\nprocessor    : 3\n...\n```\n2， `Linux` 查询 `L1/L2/L3 cache`大小：`cat /sys/devices/system/cpu/cpu0/cache/index*/size`(`*`为 `0/1/2/3`)\n\n```bash\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index0/size\n32K\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index0/type\nData\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index1/type\nInstruction\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index1/size\n32K\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index2/size\n256K\n(base) harley@harley-pc:~$ cat /sys/devices/system/cpu/cpu0/cache/index3/size\n12288K\n```\n现代 `CPU`的 `L1 cache` 是逻辑核私有的，L1 cache 分指令 L1 cache 和数据 L1 cache，大小相等都为 `32 KB`；目前，L2 cache 也是片内私有，所以每个核只有`256 KB`；而对于 L3 cache，一个物理核CPU 的所有逻辑核共享，所以在每个逻辑核来看，L3 cache 都为`12288 KB`。本机的虚拟机总共有 `1` 个物理核，而本机共有 `6` 核 `12` 线程，所以可推算得到\\*\\*本机的 `Cache` 信息：\n\n* L1 cache：\n   * L1 Data: `192 KB = 32 x 6 KB`\n   * L1 Instruction: `192 KB = 32 x 6 KB`\n* L2 cache：`1536 KB = 256 X 6 KB`\n* L3 cache：`12288 KB`\n\n### 1.2，Windows查看cpu和cache信息\n1，任务管理器->性能，即可查看 `cpu` 和 `L1/L2/L3 Cache` 大小，如下图所示。\n\n![image](images/5bf8b57e-8a78-4de8-8499-7fc9960af1f0.png)\n\n2，或者下载安装 `cpuz` 软件，打开即可查看，如下图所示。\n\n![image](images/50ede7f0-9d94-4d16-8c61-0438f39b3900.png)\n\n### 1.3， 尝试分析 `init()` 函数\n\n**实践作业2**：尝试分析 `init()`函数使用 `O1` 和 `O3` 优化的`profile`结果差异。\n\n* O3 相对 O1，执行时间减少，尝试分析反汇编代码（O3.s 和 O1.s），给出解释（15 分）\n* O3 相对 O1，D1 Cache 的访问次数为什么从 8409K 下降到 2125K（15 分）\n* O3 相对 O1，D1 Cache 的 `miss rate` 为什么从 6.2% 上升到 24.7%（15 分）\n\n**问题分析和答案**:\n\n1. 使用 `-O3` 参数优化，编译器会采取很多向量化算法，提高代码的并行执行程度，利用现代 `CPU` 中的流水线，`Cache` 等。`O3` 优化会提高执行代码的大小，也会降低目标代码的执行时间。\n2. 访问次数下降是因为使用了 `O3` 优化，使得程序会自动访问连续的内存。\n3. 高速缓存缺失（`cache miss`）是因为访存的内存都是不连续的。\n\n## 二，优化矩阵乘法\n\n**实践作业3**：优化 $A^T*A$ 的矩阵乘法，目标是尽量减少计算时间。\n\n* 其中 `A` 的大小为 `1024x8192`，元素为 `int`类型。\n* 需要从算法层面，指令层面和访存优化的角度联合优化。\n* 通过文档说明自己的优化思路（20 分）。\n* 可以选择自己熟悉的处理器平台进行代码编写，如 `Intel` 平台或者`ARM`平台（20 分）\n\n**问题分析**：矩阵乘的算法优化可分为两类：\n\n- 基于**算法分析**的方法：根据矩阵乘计算特性，从数学角度优化，典型的算法包括 Strassen 算法和 Coppersmith–Winograd 算法。\n- 基于**软件优化**的方法：根据计算机存储系统的层次结构特性，选择性地调整计算顺序，主要有循环拆分向量化、内存重排等。\n### 2.1，算法层面优化\n从算法层面优化，首先需要分析朴素矩阵乘法的算法复杂度，分析可知，朴素的矩阵乘算法的时间复杂度为 $O(n^3)$ 。根据矩阵乘计算特性，从数学角度（算法层面）优化，典型的算法包括 `Strassen` 算法和 `Coppersmith–Winograd` 算法。\n\n#### 2.1.1，`Strassen` 算法\n`Strassen` 算法是 `1969` 年提出的复杂度为 $O(n^{log_2{7}})$ 的矩阵乘法，这是历史上第一次将矩阵乘的计算复杂度价格低到 $O(n^3)$ 以下。\n\n基于**分治（Divide and Conquer）的思想**，将矩阵 $A, B, C∈R^{n^2×n^2}$ 分别拆分为更小的矩阵，根据矩阵基本的运算法则，拆分后朴素算法的计算共需要**八次小矩阵乘法和四次小矩阵加法**计算。`Strassen` 算法的核心思想是通过引入辅助计算的中间矩阵，再将中间矩阵进行组合得到最后的矩阵，这个过程使用了**七次乘法和十八次加法**，将矩阵乘的算法复杂度降低到了 $O(n^{log_27})$ （递归地运行该算法）。算法的详细推导过程如下：\n\n1，基于分治（Divide and Conquer）的思想，Starssen 算法将矩阵 $A,\\ B,\\ C \\in R^{n^2 \\times n^2}$ 分别拆分为更小的矩阵：\n\n$$\n\\mathbf{A} =\n\\begin{bmatrix}\n\\mathbf{A}_{1,1} & \\mathbf{A}_{1,2} \\\\\n\\mathbf{A}_{2,1} & \\mathbf{A}_{2,2}\n\\end{bmatrix},\n\\mathbf{B} =\n\\begin{bmatrix}\n\\mathbf{B}_{1,1} & \\mathbf{B}_{1,2} \\\\\n\\mathbf{B}_{2,1} & \\mathbf{B}_{2,2}\n\\end{bmatrix},\n\\mathbf{C} =\n\\begin{bmatrix}\n\\mathbf{C}_{1,1} & \\mathbf{C}_{1,2} \\\\\n\\mathbf{C}_{2,1} & \\mathbf{C}_{2,2}\n\\end{bmatrix}\n$$\n\n其中，$A_{i,j},\\ B_{i,j},\\ C_{i,j} \\in R^{2^{n-1} \\times 2^{n-1}}$。拆分后朴素算法的计算如下所示，共需要八次小矩阵乘法和四次小矩阵加法计算。\n\n![image](images/7ef2efa9-1119-4c4d-9bc8-bb006ef6eaf6.png)\n\n2，引入七个如下所示的用于辅助计算的中间矩阵。\n\n![image](images/fa36902b-ad65-44ea-9fd3-9464fd0fd83c.png)\n\n3，将中间矩阵进行组合得到最后的结果矩阵。\n\n![image](images/ac614b82-a91a-4803-901e-30dc58399cf8.png)\n\n#### 2.1.2，Coppersmith–Winograd 算法\n`Strassen` 算法尽管学术意义重大，但实际应用有限，`Coppersmith–Winograd` 算法(`1990`年)的提出将矩阵乘法的算法复杂度降低到了$O(n^2.376)$。其算法的详细推导过程可参考 [Matrix multiplication via arithmetic progressions （原始论文）](https://www.sciencedirect.com/science/article/pii/S0747717108800132)。\n\n### 2.2，指令层面优化\n改进访存局部性和利用向量指令等方法都是属于软件优化方法。软件优化方法基于对计算机体系机构和软件系统的特征分析，结合具体计算的特性，设计出针对性的优化方法。\n\n现在的 `CPU` 处理器，基本上想获得高的性能，必须要用**向量化**指令，不管是老的 `SSE2`，`AVX` 或者 `AVX 2.0` 等，对于`CPU` 的优化，如果想达到高性能，必须要用到单指令多数据（`SIMD`）的向量化指令。\n\n### 2.3，访存优化\n**程序运行环境**(`g++` 编译器基础上 `Linux`系统比 `Windows` 运行程序时间更少一些)：\n\n* 操作系统：`Ubuntu`\n* 编译器：`g++`，`g++ --std=c++17 -O3 matrix_multiplication.cpp`\n* 编程语言：`C++`\n* `CPU`平台: `Intel` 的 `I7-8700` `CPU`\n\n朴素的矩阵乘算法的时间复杂度为 $O(n^3)$，以 $A^T*A$ 为例，矩阵相乘核心代码如下：\n\n```cpp\nvector<vector<int>> matrix_mul(vector<vector<int>> A, vector<vector<int>> B){\n    /*二维矩阵相乘函数\n    */\n    // vector<vector<int>> A_T = matrix_transpose(A);\n    assert((*A.begin()).size()==B.size()); //断言，第一个矩阵的列必须等于第二个矩阵的行\n    int new_rows = A.size();\n    int new_cols = (*B.begin()).size();\n    int L = B.size();\n    vector<vector<int>> C(new_rows, vector<int>(new_cols,0));\n\n    for(int i=0; i<new_rows; i++){\n        for(int j=0; j<new_cols;j++){\n           for(int k=0;k<L;k++){\n              C[i][j] += A[i][k]*B[k][j]\n           }\n            // C[i][j] = vector_mul(A[i], get_col(B, j));\n        }\n    }\n    return C;\n}\n```\n从以上代码可以看出，`B[k][j]` 读取内存中的数据，是不连续的。在最底层的循环中，随着 `k` 不断加 `1`，`B[k][j]` 不断的在内存中跳跃。这会引起缓存命中率低，循环程序不断的把内存转移至缓存，引起效率降低。在我的台式机的虚拟机上，当`A` 的大小为 `1024x8192`时，需要用时 `85.3 s`（我用的编译器是`g++ -O3`）。下面的代码是我从访存优化的角度使用的两种优化方法。\n\n#### 2.3.1，优化方法 1 (改进访存局部性)\n> 内存使用上，程序访问的内存地址之间连续性越好，程序的访问效率就越高。\n\n充分利用计算机系统的特性可以大幅度提高程序性能，参考卡内基梅隆大学的镇校神课《深入理解计算机系统》里面，给出一种方法，仅仅改变循环的次序，就可以大幅度提高性能，修改后代码如下：\n\n```cpp\nvector<vector<int>> matrix_mul_optim(vector<vector<int>> A, vector<vector<int>> B){\n    /*二维矩阵相乘函数\n    */\n    // vector<vector<int>> A_T = matrix_transpose(A);\n    assert((*A.begin()).size()==B.size()); //断言，第一个矩阵的列必须等于第二个矩阵的行\n    int new_rows = A.size();\n    int new_cols = (*B.begin()).size();\n    int L = B.size();\n    vector<vector<int>> C(new_rows, vector<int>(new_cols,0));\n    for(int k=0; k<L; k++){\n        for(int i=0; i<new_rows; i++){\n            int r = A[i][k];\n            for(int j=0; j<new_cols;j++){\n                C[i][j] += A[i][k]*B[k][j];\n            }\n            // C[i][j] = vector_mul(A[i], get_col(B, j));\n        }\n    }\n    return C;\n}\n```\n首先，最内层的循环，随着 `j` 加 `1`，`C[i][j]` 和 `B[k][j]` 都是每次只加 1，这符合空间局部性的原理，也就是说，内存每次读取都是一个接着一个的来，没有大幅度跳跃。其次，`A[i][k]` 在中间层循环是跳跃的，但是中间层执行的没有底层那么多，而且我们把 `A[i][k]` 赋给了局部变量 `r`，在编译器生成汇编代码的过程中，局部变量 `r` 应该由 `CPU` 寄存器存储，最底层循环程序读取寄存器的时间几乎可以忽略不计的。修改后的代码运行耗时 `25.2 s`。\n\n#### 2.3.2，优化方法2(分块矩阵+改进访存局部性)\n**将矩阵分块（计算拆分），每次计算一部分内容**。分块的目的就是优化访存，通过分块之后让访存都集中在一定区域，能够提高了数据局部性，从而提高 Cache 利用率，性能就会更好。结合分块矩阵和改进访存局部性两种方法的代买运行耗时 `18.8 s`。修改后的代码如下：\n\n```cpp\nvector<vector<int>> matrix_mul_optim3(vector<vector<int>> A, vector<vector<int>> B){\n    /*二维矩阵相乘函数，优化方法-分块矩阵\n    */\n    // vector<vector<int>> A_T = matrix_transpose(A);\n    assert((*A.begin()).size()==B.size()); //断言，第一个矩阵的列必须等于第二个矩阵的行\n    int M = A.size();\n    int N = (*B.begin()).size();\n    int K = B.size();  // 第二个矩阵的行\n\n    int NUM = 8;  // 分块数\n    int MT = A.size()/NUM;  // 分块矩阵的行\n    int NT = (*B.begin()).size()/NUM;  // 分块矩阵的列\n    int KT = B.size()/NUM;  // \n    vector<vector<int>> C(M, vector<int>(N,0));\n    for(int kt = 0; kt < NUM; ++kt){\n        for(int it = 0; it < NUM; ++it){\n            for(int jt = 0; jt < NUM; ++jt){\n                int ktt = kt * KT;\n                int itt = it * MT;\n                int jtt = jt * NT;\n                for(int k = ktt; k < ktt + KT; ++k){\n                    int num_k = k * NUM;\n                    for(int i = itt; i < itt + MT; ++i){\n                        // int num_i = i * NUM;\n                        int r = A[i][k];\n                        for(int j = jtt; j < jtt + NT; ++j){\n                            C[i][j] += r * B[k][j];\n                        }\n                    }\n                }\n            }\n        }\n    }\n    return C;\n}\n```\n程序输出结果如下：\n\n> Done! Timing : 18.889000 s\nThe size of result matrix is (8192, 8192)\n\n## 三，优化方法集合的完整代码\n完整可直接在`windows/Linux` 上可运行的代码如下：\n\n```cpp\n/*\n * 矩阵乘法(A^T*A)实现\n */\n\n#include<iostream>\n#include<stdlib.h>\n#include<vector>\n#include<cassert>\n#include\"iomanip\"\n#include <chrono>\n#include <iomanip>\nusing namespace std::chrono;\nusing namespace std;\n\n\ntypedef std::vector<int> Row;\ntypedef std::vector<Row> Matrix;\n\nusing namespace std;\nint m = 1024;\nint n = 8192;\n\nvector<vector<int>> init_matrix(int mm, int nn){\n    /*初始化指定行和列的二维矩阵\n    */\n    int random_integer;\n\n    vector<vector<int>> matrix(mm, vector<int>(nn,0)); // 初始化二维数组matrix为1024*8192，所有元素为0\n    // cout << \"The size of init matrix is \" << \"(\" << matrix.size() << \", \" << matrix[0].size() << \")\" << std::endl;\n    for (int i=0;i < matrix.size();i++)\n    {\n        for(int j=0;j < matrix[i].size();j++)\n        {\n            random_integer = rand() % 128;\n            matrix[i][j] = random_integer;  // 利用下标给二维数组赋值\n            // matrix[i].push_back(random_integer);  // 利用push_back给vactor添加元素\n            // cout << random_integer << endl;\n        }\n    }\n    return matrix;\n\n}\n\nvoid print_matrix(vector<vector<int>> matrix){\n    /*打印二维向量（矩阵）的元素\n    */\n    cout << \"The size of matrix is\" << \"(\" << matrix.size() << \", \" << matrix[0].size() << \")\" << std::endl;\n    //迭代器遍历\n    // vector<vector<int >>::iterator iter;\n    for(auto iter=matrix.cbegin();iter != matrix.cend(); ++iter)\n    {\n        for(int i = 0;i<(*iter).size();i++){\n            cout << (*iter)[i] << \" \";\n        }\n        cout << std::endl;\n    }\n\n    // cout << \"print success\" << endl;\n}\n\nvector<vector<int>> matrix_transpose(vector<vector<int>> A){\n    /*获取矩阵的转置\n    */\n    int rows = A.size();\n    int cols = (*A.begin()).size();\n    vector<vector<int>> A_T(cols, vector<int>(rows,0));\n    for (int j=0;j < cols; j++){\n        for(int i=0;i< rows; i++){\n            A_T[j][i] = A[i][j];\n        }\n    }\n    return A_T;\n}\n\nint vector_mul(vector<int> A1, vector<int> B1){\n    /*向量相乘函数\n    */\n    assert(A1.size()==B1.size()); //断言，两个向量的长度必须相等\n    vector<int>::iterator begin;  // 定义迭代器\n    int result;\n    // 迭代器循环遍历元素\n    for(int i=0; i<A1.size(); i++){\n        result += A1[i]*B1[i];\n    }\n    return result;\n\n}\n\nvector<int> get_col(vector<vector<int>> matrix, int n){\n    /*获取矩阵指定列的向量\n    */\n//    vector<int> col(matrix.size());\n    vector<int> col;\n    col.reserve(matrix.size());\n    for(auto row: matrix){\n        col.push_back(row[n]);\n    }\n    // cout << \"The size of vector is \" << col.size() << endl;\n    return col;\n}\n\nvector<vector<int>> matrix_mul(vector<vector<int>> A, vector<vector<int>> B){\n    /*二维矩阵相乘函数\n    */\n    // vector<vector<int>> A_T = matrix_transpose(A);\n    assert((*A.begin()).size()==B.size()); //断言，第一个矩阵的列必须等于第二个矩阵的行\n    int new_rows = A.size();\n    int new_cols = (*B.begin()).size();\n    int L = B.size();\n    vector<vector<int>> C(new_rows, vector<int>(new_cols,0));\n\n    for(int i=0; i<new_rows; i++){\n        for(int j=0; j<new_cols;j++){\n            for(int k=0; k<L; k++){\n                C[i][j] += A[i][k]*B[k][j];\n            }\n            // C[i][j] = vector_mul(A[i], get_col(B, j));\n        }\n    }\n    return C;\n}\n\nvector<vector<int>> matrix_mul_optim1(vector<vector<int>> A, vector<vector<int>> B){\n    /*二维矩阵相乘优化函数1-改进访存局部性\n    */\n    // vector<vector<int>> A_T = matrix_transpose(A);\n    assert((*A.begin()).size()==B.size()); //断言，第一个矩阵的列必须等于第二个矩阵的行\n    int new_rows = A.size();\n    int new_cols = (*B.begin()).size();\n    int L = B.size();\n    vector<vector<int>> C(new_rows, vector<int>(new_cols,0));\n    for(int k=0; k<L; k++){\n        for(int i=0; i<new_rows; i++){\n            int r = A[i][k];\n            for(int j=0; j<new_cols;j++){\n                C[i][j] += r * B[k][j];\n            }\n            // C[i][j] = vector_mul(A[i], get_col(B, j));\n        }\n    }\n    return C;\n}\n\nvector<vector<int>> matrix_mul_optim2(vector<vector<int>> A, vector<vector<int>> B){\n    /*二维矩阵相乘优化函数2-计算拆分\n    */\n    // vector<vector<int>> A_T = matrix_transpose(A);\n    assert((*A.begin()).size()==B.size()); //断言，第一个矩阵的列必须等于第二个矩阵的行\n    int M = A.size();\n    int N = (*B.begin()).size();\n    int K = B.size();  // 第二个矩阵的行\n    vector<vector<int>> C(M, vector<int>(N,0));\n    for(int m = 0; m < M; m += 4){\n        for(int n = 0; n < N; n += 4){\n            C[m + 0][n + 0] = 0;\n            C[m + 0][n + 1] = 0;\n            C[m + 0][n + 2] = 0;\n            C[m + 0][n + 3] = 0;\n\n            C[m + 1][n + 0] = 0;\n            C[m + 1][n + 1] = 0;\n            C[m + 1][n + 2] = 0;\n            C[m + 1][n + 3] = 0;\n\n            C[m + 2][n + 0] = 0;\n            C[m + 2][n + 1] = 0;\n            C[m + 2][n + 2] = 0;\n            C[m + 2][n + 3] = 0;\n\n            C[m + 3][n + 0] = 0;\n            C[m + 3][n + 1] = 0;\n            C[m + 3][n + 2] = 0;\n            C[m + 3][n + 3] = 0;\n            for(int k = 0;k < K; k +=4){\n                //**********************************************//\n                C[m + 0][n + 0] += A[m + 0][k + 0] * B[k][n + 0];\n                C[m + 0][n + 0] += A[m + 0][k + 1] * B[k][n + 0];\n                C[m + 0][n + 0] += A[m + 0][k + 2] * B[k][n + 0];\n                C[m + 0][n + 0] += A[m + 0][k + 3] * B[k][n + 0];\n\n                C[m + 0][n + 1] += A[m + 0][k + 0] * B[k][n + 1];\n                C[m + 0][n + 1] += A[m + 0][k + 1] * B[k][n + 1];\n                C[m + 0][n + 1] += A[m + 0][k + 2] * B[k][n + 1];\n                C[m + 0][n + 1] += A[m + 0][k + 3] * B[k][n + 1];\n\n                C[m + 0][n + 2] += A[m + 0][k + 0] * B[k][n + 2];\n                C[m + 0][n + 2] += A[m + 0][k + 1] * B[k][n + 2];\n                C[m + 0][n + 2] += A[m + 0][k + 2] * B[k][n + 2];\n                C[m + 0][n + 2] += A[m + 0][k + 3] * B[k][n + 2];\n\n                C[m + 0][n + 3] += A[m + 0][k + 0] * B[k][n + 3];\n                C[m + 0][n + 3] += A[m + 0][k + 1] * B[k][n + 3];\n                C[m + 0][n + 3] += A[m + 0][k + 2] * B[k][n + 3];\n                C[m + 0][n + 3] += A[m + 0][k + 3] * B[k][n + 3];\n                //**********************************************//\n                C[m + 1][n + 0] += A[m + 1][k + 0] * B[k][n + 0];\n                C[m + 1][n + 0] += A[m + 1][k + 1] * B[k][n + 0];\n                C[m + 1][n + 0] += A[m + 1][k + 2] * B[k][n + 0];\n                C[m + 1][n + 0] += A[m + 1][k + 3] * B[k][n + 0];\n\n                C[m + 1][n + 1] += A[m + 1][k + 0] * B[k][n + 1];\n                C[m + 1][n + 1] += A[m + 1][k + 1] * B[k][n + 1];\n                C[m + 1][n + 1] += A[m + 1][k + 2] * B[k][n + 1];\n                C[m + 1][n + 1] += A[m + 1][k + 3] * B[k][n + 1];\n\n                C[m + 1][n + 2] += A[m + 1][k + 0] * B[k][n + 2];\n                C[m + 1][n + 2] += A[m + 1][k + 1] * B[k][n + 2];\n                C[m + 1][n + 2] += A[m + 1][k + 2] * B[k][n + 2];\n                C[m + 1][n + 2] += A[m + 1][k + 3] * B[k][n + 2];\n\n                C[m + 1][n + 3] += A[m + 1][k + 0] * B[k][n + 3];\n                C[m + 1][n + 3] += A[m + 1][k + 1] * B[k][n + 3];\n                C[m + 1][n + 3] += A[m + 1][k + 2] * B[k][n + 3];\n                C[m + 1][n + 3] += A[m + 1][k + 3] * B[k][n + 3];\n                //**********************************************//\n                C[m + 2][n + 0] += A[m + 2][k + 0] * B[k][n + 0];\n                C[m + 2][n + 0] += A[m + 2][k + 1] * B[k][n + 0];\n                C[m + 2][n + 0] += A[m + 2][k + 2] * B[k][n + 0];\n                C[m + 2][n + 0] += A[m + 2][k + 3] * B[k][n + 0];\n\n                C[m + 2][n + 1] += A[m + 2][k + 0] * B[k][n + 1];\n                C[m + 2][n + 1] += A[m + 2][k + 1] * B[k][n + 1];\n                C[m + 2][n + 1] += A[m + 2][k + 2] * B[k][n + 1];\n                C[m + 2][n + 1] += A[m + 2][k + 3] * B[k][n + 1];\n\n                C[m + 2][n + 2] += A[m + 2][k + 0] * B[k][n + 2];\n                C[m + 2][n + 2] += A[m + 2][k + 1] * B[k][n + 2];\n                C[m + 2][n + 2] += A[m + 2][k + 2] * B[k][n + 2];\n                C[m + 2][n + 2] += A[m + 2][k + 3] * B[k][n + 2];\n\n                C[m + 2][n + 3] += A[m + 2][k + 0] * B[k][n + 3];\n                C[m + 2][n + 3] += A[m + 2][k + 1] * B[k][n + 3];\n                C[m + 2][n + 3] += A[m + 2][k + 2] * B[k][n + 3];\n                C[m + 2][n + 3] += A[m + 2][k + 3] * B[k][n + 3];\n                //**********************************************//\n                C[m + 3][n + 0] += A[m + 3][k + 0] * B[k][n + 0];\n                C[m + 3][n + 0] += A[m + 3][k + 1] * B[k][n + 0];\n                C[m + 3][n + 0] += A[m + 3][k + 2] * B[k][n + 0];\n                C[m + 3][n + 0] += A[m + 3][k + 3] * B[k][n + 0];\n\n                C[m + 3][n + 1] += A[m + 3][k + 0] * B[k][n + 1];\n                C[m + 3][n + 1] += A[m + 3][k + 1] * B[k][n + 1];\n                C[m + 3][n + 1] += A[m + 3][k + 2] * B[k][n + 1];\n                C[m + 3][n + 1] += A[m + 3][k + 3] * B[k][n + 1];\n\n                C[m + 3][n + 2] += A[m + 3][k + 0] * B[k][n + 2];\n                C[m + 3][n + 2] += A[m + 3][k + 1] * B[k][n + 2];\n                C[m + 3][n + 2] += A[m + 3][k + 2] * B[k][n + 2];\n                C[m + 3][n + 2] += A[m + 3][k + 3] * B[k][n + 2];\n\n                C[m + 3][n + 3] += A[m + 3][k + 0] * B[k][n + 3];\n                C[m + 3][n + 3] += A[m + 3][k + 1] * B[k][n + 3];\n                C[m + 3][n + 3] += A[m + 3][k + 2] * B[k][n + 3];\n                C[m + 3][n + 3] += A[m + 3][k + 3] * B[k][n + 3];\n            }\n        }\n    }\n    return C;\n}\nvector<vector<int>> matrix_mul_optim3(vector<vector<int>> A, vector<vector<int>> B){\n    /*二维矩阵相乘优化函数3-（分块矩阵+改进访存局部性）\n    */\n    // vector<vector<int>> A_T = matrix_transpose(A);\n    assert((*A.begin()).size()==B.size()); //断言，第一个矩阵的列必须等于第二个矩阵的行\n    int M = A.size();\n    int N = (*B.begin()).size();\n    int K = B.size();  // 第二个矩阵的行\n\n    int NUM = 8;  // 分块数\n    int MT = A.size()/NUM;  // 分块矩阵的行\n    int NT = (*B.begin()).size()/NUM;  // 分块矩阵的列\n    int KT = B.size()/NUM;  //\n    vector<vector<int>> C(M, vector<int>(N,0));\n    for(int kt = 0; kt < NUM; ++kt){\n        for(int it = 0; it < NUM; ++it){\n            for(int jt = 0; jt < NUM; ++jt){\n                int ktt = kt * KT;\n                int itt = it * MT;\n                int jtt = jt * NT;\n                for(int k = ktt; k < ktt + KT; ++k){\n                    int num_k = k * NUM;\n                    for(int i = itt; i < itt + MT; ++i){\n                        // int num_i = i * NUM;\n                        int r = A[i][k];\n                        for(int j = jtt; j < jtt + NT; ++j){\n                            C[i][j] += r * B[k][j];\n                        }\n                    }\n                }\n            }\n        }\n    }\n    return C;\n}\n\nint main(){\n    vector<vector<int>> A;\n    A = init_matrix(1024,8192);\n\n    auto A_T = matrix_transpose(A);\n    // print_matrix(A);\n    // print_matrix(A_T);\n    auto start = std::chrono::steady_clock::now();  // 开始时间\n    auto B = matrix_mul_optim3(A_T, A);\n    // print_matrix(B);\n    auto end = std::chrono::steady_clock::now();  // 匹配结束后时间\n    auto tt = duration_cast < std::chrono::milliseconds > (end - start);\n    printf(\"Done! Timing : %lf s\\n\", tt.count() / 1000.0);\n    cout << \"The size of result matrix is \" << \"(\" << B.size() << \", \" << B[0].size() << \")\" << std::endl;\n\n    return 0;\n}\n```\n**程序输出结果如下：**\n\n> The size of init A matrix is (1024, 8192)\nDone! Timing : 18.787000 s\nThe size of result matrix is (8192, 8192)\n\n## 参考资料\n* [通用矩阵乘（GEMM）优化算法](https://jackwish.net/2019/gemm-optimization.html)\n* [OpenBLAS项目与矩阵乘法优化 | AI 研习社](https://www.leiphone.com/news/201704/Puevv3ZWxn0heoEv.html)\n* [矩阵乘法的优化](http://blog.sciencenet.cn/blog-3316223-1085257.html)\n* [how-to-optimize-gemm](https://github.com/flame/how-to-optimize-gemm)\n\n"
  },
  {
    "path": "8-model_deploy/README.md",
    "content": "## 前言\n\n本目录旨在分享模型部署的知识，包括模型转换、模型板端推理框架、算法SDK开发等知识。\n\n## 模型推理框架\n\n1. [ONNX模型分析与使用](./推理框架/ONNX模型分析与使用.md)\n2. [TensorRT基础](./推理框架/TensorRT基础笔记.md)\n\n## 模型部署\n\n1. [模型压缩部署概述](模型压缩部署概述.md)\n2. [神经网络模型复杂度分析](神经网络模型复杂度分析.md)\n3. [模型转换总结](模型转换总结.md)\n4. [模型板端推理](模型板端推理.md)\n\n## 参考资料\n\n- https://developer.nvidia.com/tensorrt\n- https://onnx.ai/\n- https://pytorch.org/\n- https://github.com/Tencent/ncnn"
  },
  {
    "path": "8-model_deploy/推理框架/ONNX模型分析与使用.md",
    "content": "> 本文大部分内容为对 ONNX 官方资料的总结和翻译，部分知识点参考网上质量高的博客。\n\n## 一，ONNX 概述\n\n深度学习算法大多通过计算数据流图来完成神经网络的深度学习过程。 一些框架（例如CNTK，Caffe2，Theano和TensorFlow）使用**静态图形**，而其他框架（例如 PyTorch 和 Chainer）使用**动态图形**。 但是这些框架都提供了接口，使开发人员可以轻松构建计算图和运行时，以优化的方式处理图。 这些图用作中间表示（IR），捕获开发人员源代码的特定意图，有助于优化和转换在特定设备（CPU，GPU，FPGA等）上运行。\n\n**ONNX 的本质只是一套开放的 `ML` 模型标准，模型文件存储的只是网络的拓扑结构和权重（其实每个深度学习框架最后保存的模型都是类似的），脱离开框架是没办法对模型直接进行 `inference`的**。\n\n### 1.1，为什么使用通用 IR\n现在很多的深度学习框架提供的功能都是类似的，但是在 API、计算图和 runtime 方面却是独立的，这就给 AI 开发者在不同平台部署不同模型带来了很多困难和挑战，ONNX 的目的在于提供一个跨框架的模型中间表达框架，用于模型转换和部署。ONNX 提供的计算图是通用的，格式也是开源的。\n\n## 二，ONNX 规范\n> Open Neural Network Exchange Intermediate Representation (ONNX IR) Specification.\n\n`ONNX` 结构的定义文件 `.proto` 和 `.prpto3` 可以在 [onnx folder](https://github.com/onnx/onnx/tree/master/onnx) 目录下找到，文件遵循的是谷歌 `Protobuf` 协议。ONNX 是一个开放式规范，由以下组件组成：\n+ 可扩展计算图模型的定义\n+ 标准数据类型的定义\n+ 内置运算符的定义\n\n`IR6` 版本的 ONNX 只能用于推理（inference），从 `IR7` 开始 ONNX 支持训练（training）。`onnx.proto` 主要的对象如下：\n+ ModelProto\n+ GraphProto\n+ NodeProto\n+ AttributeProto\n+ ValueInfoProto\n+ TensorProto\n\n他们之间的关系：ONNX 模型 `load` 之后，得到的是一个 `ModelProto`，它包含了一些版本信息，生产者信息和一个非常重要的 `GraphProto`；在 `GraphProto` 中包含了四个关键的 `repeated` 数组，分别是`node` (NodeProto 类型)，`input`(ValueInfoProto 类型)，`output`(ValueInfoProto 类型)和 `initializer` (TensorProto 类型)，其中 `node` 中存放着模型中的所有计算节点，input 中存放着模型所有的输入节点，output 存放着模型所有的输出节点，`initializer` 存放着模型所有的权重；节点与节点之间的拓扑定义可以通过 input 和output 这两个 `string` 数组的指向关系得到，这样利用上述信息我们可以快速构建出一个深度学习模型的拓扑图。最后每个计算节点当中还包含了一个 `AttributeProto` 数组，用于描述该节点的属性，例如 `Conv` 层的属性包含 `group`，`pads` 和`strides` 等等，具体每个计算节点的属性、输入和输出可以参考这个 [Operators.md](https://github.com/onnx/onnx/blob/master/docs/Operators.md) 文档。\n\n需要注意的是，上面所说的 `GraphProto` 中的 `input` 输入数组不仅仅包含我们一般理解中的图片输入的那个节点，还包含了模型当中所有权重。举例，`Conv` 层中的 `W` 权重实体是保存在 `initializer` 当中的，那么相应的会有一个同名的输入在 `input` 当中，其背后的逻辑应该是把权重也看作是模型的输入，并通过 `initializer` 中的权重实体来对这个输入做初始化(也就是把值填充进来)\n\n### 2.1，Model\n\n模型结构的主要目的是将元数据( `meta data`)与图形(`graph`)相关联，图形包含所有可执行元素。 首先，读取模型文件时使用元数据，为实现提供所需的信息，以确定它是否能够：执行模型，生成日志消息，错误报告等功能。此外元数据对工具很有用，例如IDE和模型库，它需要它来告知用户给定模型的目的和特征。\n\n每个 model 有以下组件：\n|Name|Type|Description|\n|---|---|---|\n|ir_version|int64|The ONNX version assumed by the model.|\n|opset_import|OperatorSetId|A collection of operator set identifiers made available to the model. An implementation must support all operators in the set or reject the model.|\n|producer_name|string|The name of the tool used to generate the model.|\n|producer_version|string|The version of the generating tool.|\n|domain|string|A reverse-DNS name to indicate the model namespace or domain, for example, 'org.onnx'|\n|model_version|int64|The version of the model itself, encoded in an integer.|\n|doc_string|string|Human-readable documentation for this model. Markdown is allowed.|\n|graph|Graph|The parameterized graph that is evaluated to execute the model.|\n|metadata_props|map<string,string>|Named metadata values; keys should be distinct.|\n|training_info|TrainingInfoProto[]|An optional extension that contains information for training.|\n\n### 2.2，Operators Sets\n\n每个模型必须明确命名它依赖于其功能的运算符集。 操作员集定义可用的操作符，其版本和状态。 每个模型按其域定义导入的运算符集。 所有模型都隐式导入默认的 ONNX 运算符集。\n\n运算符集(`Operators Sets`)对象的属性如下：\n|Name|Type|Description|\n|---|---|---|\n|magic|string|T ‘ONNXOPSET’|\n|ir_version|int32|The ONNX version corresponding to the operators.|\n|ir_version_prerelease|string|The prerelease component of the SemVer of the IR.\n|ir_build_metadata|string|The build metadata of this version of the operator set.|\n|domain|string|The domain of the operator set. Must be unique among all sets.|\n|opset_version|int64|The version of the operator set.|\n|doc_string|string|Human-readable documentation for this operator set. Markdown is allowed.|\n|operator|Operator[]|The operators contained in this operator set.|\n\n### 2.3，ONNX Operator\n\n图( `graph`)中使用的每个运算符必须由模型(`model`)导入的一个运算符集明确声明。\n\n运算符（`Operator`）对象定义的属性如下：\n|Name|Type|Description|\n|---|---|---|\n|op_type|string|The name of the operator, as used in graph nodes. MUST be unique within the operator set’s domain.|\n|since_version|int64|The version of the operator set when this operator was introduced.|\n|status|OperatorStatus|One of ‘EXPERIMENTAL’ or ‘STABLE.’|\n|doc_string|string|A human-readable documentation string for this operator. Markdown is allowed.|\n\n### 2.4，ONNX Graph\n\n序列化图由一组元数据字段(`metadata`)，模型参数列表(`a list of model parameters`,)和计算节点列表组成(a list of computation nodes)。每个计算数据流图被构造为拓扑排序的节点列表，这些节点形成图形，其必须没有周期。 每个节点代表对运营商的呼叫。 每个节点具有零个或多个输入以及一个或多个输出。\n\n图表(Graph)对象具有以下属性：\n\n|Name|Type|Description|\n|---|---|---|\n|name|string|模型计算图的名称|\n|node|Node[]|节点列表，基于输入/输出数据依存关系形成部分排序的计算图，拓扑顺序排列。|\n|initializer|Tensor[]|命名张量值的列表。 当 ` initializer` 与计算图 `graph`输入名称相同，输入指定一个默认值，否则指定一个常量值。|\n|doc_string|string|用于阅读模型的文档|\n|input|ValueInfo[]|计算图 `graph` 的输入参数，在 `‘initializer.’` 中可能能找到默认的初始化值。|\n|output|ValueInfo[]|计算图 `graph` 的输出参数。|\n|value_info|ValueInfo[]|用于存储除输入、输出值之外的类型和形状信息。|\n\n### 2.5，ValueInfo\n\n`ValueInfo` 对象属性如下：\n\n|Name|Type|Description|\n|---|---|---|\n|name|string|The name of the value/parameter.|\n|type|Type|The type of the value **including shape information**.|\n|doc_string|string|Human-readable documentation for this value. Markdown is allowed.|\n### 2.6，Standard data types\nONNX 标准有两个版本，主要区别在于支持的数据类型和算子不同。计算图 `graphs`、节点 `nodes`和计算图的 `initializers` 支持的数据类型如下。原始数字，字符串和布尔类型必须用作张量的元素。\n#### 2.6.1，Tensor Element Types\n|Group|Types|Description|\n|---|---|---|\n|Floating Point Types|float16, float32, float64|浮点数遵循IEEE 754-2008标准。|\n|Signed Integer Types|int8, int16, int32, int64|支持 `8-64` 位宽的有符号整数。|\n|Unsigned Integer Types|uint8, uint16|支持 `8` 或 `16` 位的无符号整数。|\n|Complex Types|complex64, complex128|具有 `32` 位或 `64` 位实部和虚部的复数。|\n|Other|string|字符串代表的文本数据。 所有字符串均使用UTF-8编码。|\n|Other|bool|布尔值类型，表示的数据只有两个值，通常为 `true` 和 `false`。|\n#### 2.6.2，Input / Output Data Types\n以下类型用于定义计算图和节点输入和输出的类型。\n\n|Variant | Type | Description |\n|---|---|---|\n|ONNX|dense tensors|张量是向量和矩阵的一般化|\n|ONNX|sequence| `sequence` (序列)是有序的稠密元素集合。|\n|ONNX|map|映射是关联表，由键类型和值类型定义。|\n\n**`ONNX` 现阶段没有定义稀疏张量类型**。\n## 三，ONNX版本控制\n\n## 四，主要算子概述\n\n## 五，Python API 使用\n\n### 5.1，加载模型\n\n1，**Loading an ONNX model**\n\n```python\nimport onnx\n# onnx_model is an in-mempry ModelProto\nonnx_model = onnx.load('path/to/the/model.onnx') # 加载 onnx 模型\n```\n\n2，**Loading an ONNX Model with External Data**\n\n+ 【默认加载模型方式】如果外部数据(`external data`)和模型文件在同一个目录下，仅使用 `onnx.load()` 即可加载模型，方法见上小节。\n+ 如果外部数据(`external data`)和模型文件不在同一个目录下，在使用 `onnx_load()` 函数后还需使用 `load_external_data_for_model()` 函数指定外部数据路径。\n\n```python\nimport onnx\nfrom onnx.external_data_helper import load_external_data_for_model\n\nonnx_model = onnx.load('path/to/the/model.onnx', load_external_data=False)\nload_external_data_for_model(onnx_model, 'data/directory/path/')\n# Then the onnx_model has loaded the external data from the specific directory\n```\n\n**3，Converting an ONNX Model to External Data**\n\n```python\nfrom onnx.external_data_helper import convert_model_to_external_data\n\n# onnx_model is an in-memory ModelProto\nonnx_model = ...\nconvert_model_to_external_data(onnx_model, all_tensors_to_one_file=True, location='filename', size_threshold=1024, convert_attribute=False)\n# Then the onnx_model has converted raw data as external data\n# Must be followed by save\n```\n\n### 5.2，保存模型\n\n**1，Saving an ONNX Model**\n```python\nimport onnx\n\n# onnx_model is an in-memory ModelProto\nonnx_model = ...\n\n# Save the ONNX model\nonnx.save(onnx_model, 'path/to/the/model.onnx')\n```\n2，**Converting and Saving an ONNX Model to External Data**\n```python\nimport onnx\n\n# onnx_model is an in-memory ModelProto\nonnx_model = ...\nonnx.save_model(onnx_model, 'path/to/save/the/model.onnx', save_as_external_data=True, all_tensors_to_one_file=True, location='filename', size_threshold=1024, convert_attribute=False)\n# Then the onnx_model has converted raw data as external data and saved to specific directory\n```\n### 5.3，Manipulating TensorProto and Numpy Array\n```python\nimport numpy\nimport onnx\nfrom onnx import numpy_helper\n\n# Preprocessing: create a Numpy array\nnumpy_array = numpy.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=float)\nprint('Original Numpy array:\\n{}\\n'.format(numpy_array))\n\n# Convert the Numpy array to a TensorProto\ntensor = numpy_helper.from_array(numpy_array)\nprint('TensorProto:\\n{}'.format(tensor))\n\n# Convert the TensorProto to a Numpy array\nnew_array = numpy_helper.to_array(tensor)\nprint('After round trip, Numpy array:\\n{}\\n'.format(new_array))\n\n# Save the TensorProto\nwith open('tensor.pb', 'wb') as f:\n    f.write(tensor.SerializeToString())\n\n# Load a TensorProto\nnew_tensor = onnx.TensorProto()\nwith open('tensor.pb', 'rb') as f:\n    new_tensor.ParseFromString(f.read())\nprint('After saving and loading, new TensorProto:\\n{}'.format(new_tensor))\n```\n### 5.4，创建ONNX模型\n可以通过 `helper` 模块提供的函数 `helper.make_graph` 完成创建 ONNX 格式的模型。创建 `graph` 之前，需要先创建相应的 `NodeProto(node)`，参照文档设定节点的属性，指定该节点的输入与输出，如果该节点带有权重那还需要创建相应的`ValueInfoProto` 和 `TensorProto` 分别放入 `graph` 中的 `input` 和 `initializer` 中，以上步骤缺一不可。\n```python\nimport onnx\nfrom onnx import helper\nfrom onnx import AttributeProto, TensorProto, GraphProto\n\n\n# The protobuf definition can be found here:\n# https://github.com/onnx/onnx/blob/master/onnx/onnx.proto\n\n# Create one input (ValueInfoProto)\nX = helper.make_tensor_value_info('X', TensorProto.FLOAT, [3, 2])\npads = helper.make_tensor_value_info('pads', TensorProto.FLOAT, [1, 4])\n\nvalue = helper.make_tensor_value_info('value', AttributeProto.FLOAT, [1])\n\n# Create one output (ValueInfoProto)\nY = helper.make_tensor_value_info('Y', TensorProto.FLOAT, [3, 4])\n\n# Create a node (NodeProto) - This is based on Pad-11\nnode_def = helper.make_node(\n    'Pad',                  # name\n    ['X', 'pads', 'value'], # inputs\n    ['Y'],                  # outputs\n    mode='constant',        # attributes\n)\n\n# Create the graph (GraphProto)\ngraph_def = helper.make_graph(\n    [node_def],        # nodes\n    'test-model',      # name\n    [X, pads, value],  # inputs\n    [Y],               # outputs\n)\n\n# Create the model (ModelProto)\nmodel_def = helper.make_model(graph_def, producer_name='onnx-example')\n\nprint('The model is:\\n{}'.format(model_def))\nonnx.checker.check_model(model_def)\nprint('The model is checked!')\n```\n### 5.5，检查模型\n在完成 `ONNX` 模型加载或者创建后，有必要对模型进行检查，使用 `onnx.check.check_model()` 函数。\n```python\nimport onnx\n\n# Preprocessing: load the ONNX model\nmodel_path = 'path/to/the/model.onnx'\nonnx_model = onnx.load(model_path)\n\nprint('The model is:\\n{}'.format(onnx_model))\n\n# Check the model\ntry:\n    onnx.checker.check_model(onnx_model)\nexcept onnx.checker.ValidationError as e:\n    print('The model is invalid: %s' % e)\nelse:\n    print('The model is valid!')\n```\n### 5.6，实用功能函数\n函数 `extract_model()` 可以从 `ONNX` 模型中提取子模型，子模型由输入和输出张量的名称定义。这个功能方便我们 `debug` 原模型和转换后的 `ONNX` 模型输出结果是否一致(误差小于某个阈值)，不再需要我们手动去修改 `ONNX` 模型。\n```python\nimport onnx\n\ninput_path = 'path/to/the/original/model.onnx'\noutput_path = 'path/to/save/the/extracted/model.onnx'\ninput_names = ['input_0', 'input_1', 'input_2']\noutput_names = ['output_0', 'output_1']\n\nonnx.utils.extract_model(input_path, output_path, input_names, output_names)\n```\n### 5.7，工具\n函数 `update_inputs_outputs_dims()` 可以将模型输入和输出的维度更新为参数中指定的值，可以使用 `dim_param` 提供静态和动态尺寸大小。\n```python\nimport onnx\nfrom onnx.tools import update_model_dims\n\nmodel = onnx.load('path/to/the/model.onnx')\n# Here both 'seq', 'batch' and -1 are dynamic using dim_param.\nvariable_length_model = update_model_dims.update_inputs_outputs_dims(model, {'input_name': ['seq', 'batch', 3, -1]}, {'output_name': ['seq', 'batch', 1, -1]})\n# need to check model after the input/output sizes are updated\nonnx.checker.check_model(variable_length_model )\n```\n## 参考资料\n1. [ONNX--跨框架的模型中间表达框架](https://zhuanlan.zhihu.com/p/41255090)\n2. [深度学习模型转换与部署那些事(含ONNX格式详细分析)](https://bindog.github.io/blog/2020/03/13/deep-learning-model-convert-and-depoly/)\n3. [onnx](https://github.com/onnx/tutorials)"
  },
  {
    "path": "8-model_deploy/推理框架/TensorRT基础笔记.md",
    "content": "## 一，概述\n\n`TensorRT` 是 NVIDIA 官方推出的基于 `CUDA` 和 `cudnn` 的高性能深度学习推理加速引擎，能够使深度学习模型在 `GPU` 上进行低延迟、高吞吐量的部署。采用 `C++` 开发，并提供了 `C++` 和 `Python` 的 API 接口，支持 TensorFlow、Pytorch、Caffe、Mxnet 等深度学习框架，其中 `Mxnet`、`Pytorch` 的支持需要先转换为中间模型 `ONNX` 格式。截止到 2021.4.21 日， `TensorRT` 最新版本为 `v7.2.3.4`。\n\n深度学习领域**延迟和吞吐量**的一般解释：\n\n+ 延迟 (`Latency`): 人和机器做决策或采取行动时都需要反应时间。延迟是指**提出请求与收到反应之间经过的时间**。大部分人性化软件系统（不只是 AI 系统），延迟都是以**毫秒**来计量的。\n+ 吞吐量 (`Throughput`): 在给定创建或部署的深度学习网络规模的情况下，可以传递多少推断结果。简单理解就是在**一个时间单元（如：一秒）内网络能处理的最大输入样例数**。\n\n## 二，TensorRT 工作流程\n\n在描述 `TensorRT` 的优化原理之前，需要先了解 `TensorRT` 的工作流程。首先输入一个训练好的 `FP32` 模型文件，并通过 `parser` 等方式输入到 `TensorRT` 中做解析，解析完成后 `engin` 会进行计算图优化（优化原理在下一章）。得到优化好的 `engine` 可以序列化到内存（`buffer`）或文件（`file`），读的时候需要反序列化，将其变成 `engine`以供使用。然后在执行的时候创建 `context`，主要是分配预先的资源，`engine` 加 `context` 就可以做推理（`Inference`）。\n\n![TensorRT工作流程.jpg](../../data/images/TensorRT/TensorRT工作流程.jpg)\n\n## 三，TensorRT 的优化原理\n\n`TensorRT` 的优化主要有以下几点：\n\n1. **算子融合（网络层合并）**：我们知道 `GPU` 上跑的函数叫 `Kernel`，`TensorRT` 是存在 `Kernel` 调用的，频繁的 `Kernel` 调用会带来性能开销，主要体现在：数据流图的调度开销，GPU内核函数的启动开销，以及内核函数之间的数据传输开销。大多数网络中存在连续的卷积 `conv` 层、偏置 `bias` 层和 激活 `relu` 层，这三层需要调用三次 cuDNN 对应的 API，但实际上这三个算子是可以进行融合（合并）的，合并成一个 `CBR` 结构。同时目前的网络一方面越来越深，另一方面越来越宽，可能并行做若干个相同大小的卷积，这些卷积计算其实也是可以合并到一起来做的（横向融合）。比如 `GoogLeNet` 网络，把结构相同，但是权值不同的层合并成一个更宽的层。\n2. `concat` 层的消除。对于 `channel` 维度的 `concat` 层，`TensorRT` 通过非拷贝方式将层输出定向到正确的内存地址来消除 `concat` 层，从而减少内存访存次数。\n3. `Kernel` 可以根据不同 `batch size` 大小和问题的复杂度，去自动选择最合适的算法，`TensorRT` 预先写了很多 `GPU` 实现，有一个自动选择的过程（没找到资料理解）。其问题包括：怎么调用 `CUDA` 核心、怎么分配、每个 `block` 里面分配多少个线程、每个 `grid` 里面有多少个 `block`。\n\n4. `FP32->FP16、INT8、INT4`：低精度量化，模型体积更小、内存占用和延迟更低等。\n5. 不同的硬件如 `P4` 卡还是 `V100` 卡甚至是嵌入式设备的卡，`TensorRT` 都会做对应的优化，得到优化后的 `engine`。\n\n## 四，参考资料\n\n1. [内核融合：GPU深度学习的“加速神器”](https://www.msra.cn/zh-cn/news/features/kernel-fusion-20170925)\n2. [高性能深度学习支持引擎实战——TensorRT](https://zhuanlan.zhihu.com/p/35657027)\n3. 《NVIDIA TensorRT 以及实战记录》PPT\n4. https://www.tiriasresearch.com/wp-content/uploads/2018/05/TIRIAS-Research-NVIDIA-PLASTER-Deep-Learning-Framework.pdf\n"
  },
  {
    "path": "8-model_deploy/模型压缩部署概述.md",
    "content": "# 模型压缩部署概述\n- [模型压缩部署概述](#模型压缩部署概述)\n\t- [一，模型在线部署](#一模型在线部署)\n\t\t- [1.1，深度学习项目开发流程](#11深度学习项目开发流程)\n\t\t- [1.2，模型训练和推理的不同](#12模型训练和推理的不同)\n\t- [二，手机端CPU推理框架的优化](#二手机端cpu推理框架的优化)\n\t- [三，参考资料](#三参考资料)\n\n## 一，模型在线部署\n\n深度学习和计算机视觉方向除了算法训练/研究，还有两个重要的方向: 模型压缩（模型优化、量化）、模型部署（模型转换、后端功能SDK开发）。所谓模型部署，即将算法研究员训练出的模型部署到具体的**端边云**芯片平台上，并完成特定业务的视频结构化应用开发。\n\n现阶段的平台主要分为云平台（如英伟达 `GPU`）、手机移动端平台（`ARM` 系列芯片）和其他嵌入式端侧平台（海思 `3519`、安霸 `CV22`、地平线 `X3`、英伟达 `jetson tx2` 等芯片）。对于模型部署/移植/优化工程师来说，虽然模型优化、量化等是更有挑战性和技术性的知识，但是对于新手的我们往往是在做解决模型无法在端侧部署的问题，包括但不限于：实现新 `OP`、修改不兼容的属性、修改不兼容的权重形状、学习不同芯片平台的推理部署框架等。对于模型转换来说，现在行业主流是使用 `Caffe` 和 `ONNX` 模型作为中间模型。\n\n### 1.1，深度学习项目开发流程\n\n在高校做深度学习 `demo` 应用一般是这样一个过程，比如使用 `Pytorch/TensorFlow` 框架训练出一个模型，然后直接使用 `Pytorch` 框架做推理（`test`）完成功能验证，但是在工业界这是不可能的，因为这样模型推理速度很慢，一般我们必须有专门的深度学习推理加速框架去做模型推理（`inference`）。以 GPU 云平台推理框架 `TensorRT` 为例，简单描述模型训练推理过程就是：训练好网络模型（权重参数数据类型为 `FP32`）输入 `TensorRT`，然后 `TensorRT` 做解析优化，并进行在线推理和输出结果。两种不同的模型训练推理过程对比如下图所示:\n\n![训练和推理.jpg](../data/images/训练和推理.jpg)\n\n前面的描述较为简单，实际在工业届，理想的深度学习项目开发流程应该分为三个步骤: **模型离线训练、模型压缩和模型在线部署**，后面两个步骤互有交叉，具体详情如下：\n\n1. **模型离线训练**：实时性低，数据离线且更新不频繁，`batchsize` 较大，消耗大量 GPU 资源。\n    + 设计开发模型网络结构;\n    + 准备数据集并进行数据预处理、`EDA` 等操作；\n    + 深度学习框架训练模型：数据增强、超参数调整、优化器选择、训练策略调整（多尺度训练）、`TTA`、模型融合等；\n    + 模型测试。\n\n2. **模型优化压缩**：主要涉及模型优化、模型转换、模型量化和模型编译优化，这些过程很多都在高性能计算推理框架中集成了，各个芯片厂商也提供了相应的工具链和推理库来完成模型优化压缩。实际开发中，在不同的平台选择不同的推理加速引擎框架，比如 `GPU` 平台选择 `TensorRT`，手机移动端（`ARM`）选择 `NCNN/MNN`，`NPU` 芯片平台，如海思3519、地平线X3、安霸CV22等则直接在厂商给出的工具链进行模型的优化（`optimizer`）和压缩。\n    + **模型优化** `Optimizer`：主要指计算图优化。首先对计算图进行分析并应用一系列**与硬件无关的优化策略**，从而在逻辑上降低运行时的开销，常见的类似优化策略其包括：算子融合（`conv、bn、relu` 融合）、算子替换、常数折叠、公共子表达式消除等。\n    + **模型转换** `Converter`：`Pytorch->Caffe`、`Pytorch->ONNX`、`ONNX`模型->`NCNN/NPU芯片厂商模型格式`（需要踩坑非常多，`Pytorch`、`ONNX`、`NPU` 三者之间的算子要注意兼容）。注意 `ONNX` 一般用作训练框架和推理框架之间转换的中间模型格式。\n    + **模型量化** `Quantizer`：主要指训练后量化（Post-training quantization `PTQ`）；权重、激活使用不同的量化位宽，如速度最快的量化方式 `w8a8`、速度和精度平衡的量化方式 `w8a16`。\n    + **模型编译优化**（编译优化+`NPU` 指令生成+内存优化）`Compiler`：**模型编译针对不同的硬件平台有不同优化方法**，与前面的和硬件无关的模型层面的优化不同。`GPU`平台存在 `kernel fusion` 方法；而 `NPU` 平台算子是通过特定二进制指令实现，其编译优化方法包括，卷积层的拆分、卷积核权重数据重排、`NPU` 算子调优等。\n\n3. **模型部署/SDK输出**: 针对视频级应用需要输出功能接口的SDK。实时性要求高，数据线上且更新频繁，`batchsize` 为 1。主要需要完成多模型的集成、模型输入的预处理、非DL算法模块的开发、 各个模块 `pipeline` 的串联，以及最后 `c` 接口（`SDK`）的输出。\n    + **板端框架模型推理**: `Inference`：`C/C++`。不同的 `NPU` 芯片/不同的公司有着不同的推理框架，但是模型的推理流程大致是一样的。包括：输入图像数据预处理、加载模型文件并解析、填充输入图像和模型权重数据到相应地址、模型推理、释放模型资源。这里主要需要学习不同的模型部署和推理框架。\n    + **pipeline 应用开发**: 在实际的深度学习项目开发过程中，模型推理只是其中的基础功能，具体的我们还需要实现多模型的集成、模型输入前处理、以及非 `DL` 算法模块的开发: 包括检测模块、跟踪模块、选帧模块、关联模块和业务算法模块等，并将各模块串联成一个 `pipeline`，从而完成视频结构化应用的开发。\n    + **SDK集成**: 在完成了具体业务 `pipeline` 的算法开发后，一般就需要输出 `c` 接口的 `SDK` 给到下层的业务侧（前后端）人员调用了。这里主要涉及 `c/c++` 接口的转换、`pipeline` 多线程/多通道等sample的开发、以及大量的单元、性能、精度、稳定性测试。\n    + **芯片平台板端推理** `Inference`，不同的 `NPU` 芯片有着不同的 `SDK` 库代码，但是模型运行流程类似。\n\n> 不同平台的模型的编译优化是不同的，比如 `NPU` 和一般 `GPU` 的区别在于后端模型编译上，`GPU` 是编译生成 `kernel library`(`cuDNN` 函数)，`NPU` 是编译生成二进制指令；前端的计算图优化没有本质区别，基本通用。\n\n所以综上所述，深度学习项目开发流程可以大致总结为三个步骤: **模型离线训练**、**模型优化压缩**和**模型部署/SDK输出**，后两个步骤互有交叉。前面 `2` 个步骤在 `PC` 上完成，最后一个步骤开发的代码是需要在在 `AI` 芯片系统上运行的。最后以视差模型在海思 `3519` 平台的部署为例，其模型部署工作流程如下：\n\n![视差模型在海思3519平台部署工作流程.png](../data/images/视差模型在海思3519平台部署流程.jpg)\n\n### 1.2，模型训练和推理的不同\n\n为了更好进行模型优化和部署的工作，需要总结一下模型推理（`Inference`）和训练（`Training`）的不同：\n\n1. 网络权重值固定，只有前向传播（`Forward`），无需反向传播，因此：\n    + 模型权值和结构固定，可以做计算图优化，比如算子融合等；\n    + 输入输出大小固定，可以做 `memory` 优化，比如 `feature` 重排和 `kernel` 重排。\n2. `batch_size` 会很小（比如 `1`），存在 `latency` 的问题。\n3. 可以使用低精度的技术，训练阶段要进行反向传播，每次梯度的更新是很微小的，需要相对较高的精度比如 `FP32` 来处理数据。但是推理阶段，对精度要求没那么高，现在很多论文都表明使用低精度如 `in16` 或者 `int8` 数据类型来做推理，也不会带来很大的精度损失。\n\n## 二，手机端CPU推理框架的优化\n\n对于 `HPC` 和软件工程师来说，在手机 `CPU` 端做模型推理框架的优化，可以从上到下考虑：\n\n1. **算法层优化**：最上面就是算法层，如可以用winograd从数学上减少乘法的数量（仅在大channel尺寸下有效）；\n2. **框架优化**：推理框架可以实现内存池、多线程等策略；\n3. **硬件层优化**：主要包括: 适应不同的硬件架构特性、`pipeline`和`cache`优化、内存数据重排、`NEON` 汇编优化等。\n\n## 三，参考资料\n\n1. 《NVIDIA TensorRT 以及实战记录》PPT\n"
  },
  {
    "path": "8-model_deploy/模型板端推理.md",
    "content": "## 前言\n\n> 芯片的算力不一定和模型推理速度成正比，嵌入式 `AI` 的另一个核心是 `inference` 框架。对于 `CPU` 架构来说，是否使用 `SIMD`（ `ARM从v7` 开始就支持 `NEON` 指令了）、是否使用多核多线程、是否有高效的卷积实现方式、是否有做汇编优化等等都会极大影响模型运行速度；而对 `DSP/NPU` 等硬件架构来说，是否对模型进行量化推理、量化的方式、访存的优化等也会有很大影响。\n"
  },
  {
    "path": "8-model_deploy/模型转换总结.md",
    "content": "## 前言\n\n等待更新。"
  },
  {
    "path": "8-model_deploy/神经网络模型复杂度分析.md",
    "content": "- [前言](#前言)\n- [一，模型计算量分析](#一模型计算量分析)\n  - [卷积层 FLOPs 计算](#卷积层-flops-计算)\n  - [全连接层的 FLOPs 计算](#全连接层的-flops-计算)\n- [二，模型参数量分析](#二模型参数量分析)\n  - [卷积层参数量](#卷积层参数量)\n  - [BN 层参数量](#bn-层参数量)\n  - [全连接层参数量](#全连接层参数量)\n- [三，模型内存访问代价计算](#三模型内存访问代价计算)\n  - [卷积层 MAC 计算](#卷积层-mac-计算)\n- [四，一些概念](#四一些概念)\n  - [双精度、单精度和半精度](#双精度单精度和半精度)\n  - [浮点计算能力](#浮点计算能力)\n  - [硬件利用率(Utilization)](#硬件利用率utilization)\n- [五，参数量/计算量分析工具](#五参数量计算量分析工具)\n- [六，参考资料](#六参考资料)\n\n## 前言\n\n现阶段的轻量级模型 MobileNet/ShuffleNet 系列、CSPNet、RepVGG、VoVNet 等都必须依赖于于具体的计算平台（如 CPU/GPU/ASIC 等）才能更完美的发挥网络架构。\n\n1，计算平台主要有两个指标：算力 $\\pi $和 带宽 $\\beta $。\n\n- 算力指的是计算平台每秒完成的最大浮点运算次数，单位是 `FLOPS`\n- 带宽指的是计算平台一次每秒最多能搬运多少数据（每秒能完成的内存交换量），单位是 `Byte/s`。\n\n计算强度上限 $I_{max}$，上面两个指标相除得到计算平台的**计算强度上限**。它描述了单位内存交换最多用来进行多少次计算，单位是 `FLOPs/Byte`。\n\n$$I_{max} = \\frac {\\pi }{\\beta}$$\n\n> 这里所说的“内存”是广义上的内存。对于 `CPU` 而言指的就是真正的内存（`RAM`）；而对于 `GPU` 则指的是显存。\n\n2，和计算平台的两个指标相呼应，模型也有两个主要的反馈速度的**间接指标**：计算量 `FLOPs` 和访存量 `MAC`。\n\n- **计算量（FLOPs）**：指的是输入单个样本（一张图像），模型完成一次前向传播所发生的浮点运算次数，即模型的时间复杂度，单位是 `FLOPs`。\n- **访存量（MAC）**：指的是输入单个样本（一张图像），模型完成一次前向传播所发生的内存交换总量，即模型的空间复杂度，单位是 `Byte`，因为数据类型通常为 `float32`，所以需要乘以 `4`。`CNN` 网络中每个网络层 `MAC` 的计算分为读输入 `feature map` 大小、权重大小（`DDR` 读）和写输出 `feature map` 大小（`DDR` 写）三部分。\n- 模型的计算强度 $I$ ：$I = \\frac{FLOPs}{MAC}$，即计算量除以访存量后的值，**表示此模型在计算过程中，每 `Byte` 内存交换到底用于进行多少次浮点运算**。单位是 `FLOPs/Byte`。可以看到，模计算强度越大，其内存使用效率越高。\n- 模型的理论性能 $P$ ：我们最关心的指标，即模型在计算平台上所能达到的每秒浮点运算次数（理论值）。单位是 `FLOPS or FLOP/s`。`Roof-line Model` 给出的就是计算这个指标的方法。\n\n3，`Roofline` 模型讲的是程序在计算平台的算力和带宽这两个指标限制下，所能达到的理论性能上界，而不是实际达到的性能，因为实际计算过程中还有除算力和带宽之外的其他重要因素，它们也会影响模型的实际性能，这是 `Roofline Model` 未考虑到的。例如矩阵乘法，会因为 `cache` 大小的限制、`GEMM` 实现的优劣等其他限制，导致你几乎无法达到 `Roofline` 模型所定义的边界（屋顶）。\n\n所谓 “Roof-line”，指的就是由计算平台的算力和带宽上限这两个参数所决定的“屋顶”形态，如下图所示。\n\n- 算力决定“屋顶”的高度（绿色线段）\n- 带宽决定“房檐”的斜率（红色线段）\n\n![roof-line](../data/images/flops/roof-line.jpg)\n\n`Roof-line` 划分出的两个瓶颈区域定义如下：\n\n![Roof-line划分出的两个瓶颈区域](../data/images/flops/Roof-line划分出的两个瓶颈区域.png)\n\n**个人感觉如果在给定计算平台上做模型部署工作，因为芯片的算力已定，工程师能做的主要工作应该是提升带宽。**\n\n## 一，模型计算量分析\n\n> 终端设备上运行深度学习算法需要考虑内存和算力的需求，因此需要进行模型复杂度分析，涉及到模型计算量（时间/计算复杂度）和模型参数量（空间复杂度）分析。\n\n为了分析模型计算复杂度，一个广泛采用的度量方式是模型推断时浮点运算的次数 （`FLOPs`），即模型理论计算量，但是，它是一个间接的度量，是对我们真正关心的直接度量比如速度或者时延的一种近似估计。\n\n本文的卷积核尺寸假设为为一般情况，即正方形，长宽相等都为 `K`。\n\n+ `FLOPs`：floating point operations 指的是浮点运算次数,**理解为计算量**，可以用来衡量算法/模型时间的复杂度。\n+ `FLOPS`：（全部大写）,Floating-point Operations Per Second，每秒所执行的浮点运算次数，理解为计算速度,是一个衡量硬件性能/模型速度的指标。\n+ `MACCs`：multiply-accumulate operations，乘-加操作次数，`MACCs` 大约是 FLOPs 的一半。将 $w[0]*x[0] + ...$ 视为一个乘法累加或 `1` 个 `MACC`。\n\n注意相同 `FLOPs` 的两个模型其运行速度是会相差很多的，因为影响模型运行速度的两个重要因素只通过 `FLOPs` 是考虑不到的，比如 `MAC`（`Memory Access Cost`）和网络并行度；二是具有相同 `FLOPs` 的模型在不同的平台上可能运行速度不一样。\n\n> 注意，网上很多文章将 MACCs 与 MACC 概念搞混，我猜测可能是机器翻译英文文章不准确的缘故，可以参考此[链接](http://machinethink.net/blog/how-fast-is-my-model/)了解更多。需要指出的是，现有很多硬件都将**乘加运算作为一个单独的指令**。\n\n### 卷积层 FLOPs 计算\n\n> 卷积操作本质上是个线性运算，假设卷积核大小相等且为 $K$。这里给出的公式写法是为了方便理解，大多数时候为了方便记忆，会写成比如 $MACCs = H \\times W \\times K^2 \\times C_i \\times C_o$。\n\n+ $FLOPs=(2\\times C_i\\times K^2-1)\\times H\\times W\\times C_o$（不考虑bias）\n+ $FLOPs=(2\\times C_i\\times K^2)\\times H\\times W\\times C_o$（考虑bias）\n+ $MACCs=(C_i\\times K^2)\\times H\\times W\\times C_o$（考虑bias）\n\n**$C_i$ 为输入特征图通道数，$K$ 为过卷积核尺寸，$H,W,C_o$ 为输出特征图的高，宽和通道数**。`二维卷积过程`如下图所示：\n\n> 二维卷积是一个相当简单的操作：从卷积核开始，这是一个小的权值矩阵。这个卷积核在 2 维输入数据上「滑动」，对当前输入的部分元素进行矩阵乘法，然后将结果汇为单个输出像素。\n\n![卷积过程](../data/images/flops/conv_dynamic_visual.gif)\n> 图片来源 [Multi-Label Classification and Class Activation Map on Fashion-MNIST](https://towardsdatascience.com/multi-label-classification-and-class-activation-map-on-fashion-mnist-1454f09f5925)。\n\n公式解释如下：\n\n**理解 `FLOPs` 的计算公式分两步**。括号内是第一步，计算出`output feature map` 的一个 `pixel`，然后再乘以 $H\\times W\\times C_o$，从而拓展到整个 output feature map。括号内的部分又可以分为两步：$(2\\times C_i\\times K^2-1)=(C_i\\times K^2) + (C_i\\times K^2-1)$。第一项是乘法运算次数，第二项是加法运算次数，因为 $n$ 个数相加，要加 $n-1$次，所以不考虑 `bias` 的情况下，会有一个 -1，如果考虑 `bias`，刚好中和掉，括号内变为$(2\\times C_i\\times K^2)$。\n\n所以卷积层的 $FLOPs=(2\\times C_{i}\\times K^2-1)\\times H\\times W\\times C_o$ ($C_i$ 为输入特征图通道数，$K$ 为过滤器尺寸，$H, W, C_o$为输出特征图的高，宽和通道数)。\n\n### 全连接层的 FLOPs 计算\n\n全连接层的 $FLOPs = (2I − 1)O$，$I$ 是输入层的维度，$O$ 是输出层的维度。\n\n## 二，模型参数量分析\n\n> 模型参数数量（params）：指模型含有多少参数，直接决定模型的大小，也影响推断时对内存的占用量，单位通常为 `M`，`GPU` 端通常参数用 `float32` 表示，所以模型大小是参数数量的 `4` 倍。这里考虑的卷积核长宽是相同的一般情况，都为 `K`。\n\n模型参数量的分析是为了了解内存占用情况，内存带宽其实比 `FLOPs` 更重要。目前的计算机结构下，单次内存访问比单次运算慢得多的多。对每一层网络，端侧设备需要：\n\n+ 从主内存中读取输入向量 / `feature map`；\n+ 从主内存中读取权重并计算点积；\n+ 将输出向量或 `feature map` 写回主内存。\n\n`MAes`：memory accesse，内存访问次数。\n\n### 卷积层参数量\n\n**卷积层权重参数量**  =  $ C_i\\times K^2\\times C_o + C_o$。\n\n$C_i$ 为输入特征图通道数，$K$ 为过滤器(卷积核)尺寸，$C_o$ 为输出的特征图的 `channel` 数(也是 `filter` 的数量)，算式第二项是偏置项的参数量 。(一般不写偏置项，偏置项对总参数量的数量级的影响可以忽略不记，这里为了准确起见，把偏置项的参数量也考虑进来。）\n\n假设输入层矩阵维度是 96×96×3，第一层卷积层使用尺寸为 5×5、深度为 16 的过滤器（卷积核尺寸为 5×5、卷积核数量为 16），那么这层卷积层的参数个数为 ５×5×3×16+16=1216个。\n\n### BN 层参数量\n\n**`BN` 层参数量** =  $2\\times C_i$。\n\n其中 $C_i$ 为输入的 `channel` 数（BN层有两个需要学习的参数，平移因子和缩放因子）\n\n### 全连接层参数量\n\n**全连接层参数量** =  $T_i\\times T_o + T_O$。\n\n$T_i$ 为输入向量的长度， $T_o$ 为输出向量的长度，公式的第二项为偏置项参数量。(目前全连接层已经逐渐被 `Global Average Pooling` 层取代了。) 注意，全连接层的权重参数量（内存占用）远远大于卷积层。\n\n## 三，模型内存访问代价计算\n\n`MAC`(`memory access cost`)  内存访问代价也叫内存使用量，指的是输入单个样本（一张图像），模型/卷积层完成一次前向传播所发生的内存交换总量，即模型的空间复杂度，单位是 `Byte`。\n\n`CNN` 网络中每个网络层 `MAC` 的计算分为读输入 `feature map` 大小（`DDR` 读）、权重大小（`DDR` 读）和写输出 `feature map` 大小（`DDR` 写）三部分。\n\n### 卷积层 MAC 计算\n以卷积层为例计算 `MAC`，可假设某个卷积层输入 `feature map` 大小是 (`Cin, Hin, Win`)，输出 `feature map` 大小是 (`Hout, Wout, Cout`)，卷积核是 (`Cout, Cin, K, K`)，理论 MAC（理论 MAC 一般小于 实际 MAC）计算公式如下：\n\n```python\n# 端侧推理IN8量化后模型，单位一般为 1 byte\ninput = Hin x Win x Cin  # 输入 feature map 大小\noutput = Hout x Wout x Cout  # 输出 feature map 大小\nweights = K x K x Cin x Cout + bias   # bias 是卷积层偏置\nddr_read = input +  weights\nddr_write = output\nMAC = ddr_read + ddr_write\n```\n> `feature map` 大小一般表示为 （`N, C, H, W`），`MAC` 指标一般用在端侧模型推理中，端侧模型推理模式一般都是单帧图像进行推理，即 `N = 1(batch_size = 1)`，不同于模型训练时的 `batch_size` 大小一般大于 1。\n\n## 四，一些概念\n\n### 双精度、单精度和半精度\n\n`CPU/GPU` 的浮点计算能力得区分不同精度的浮点数，分为双精度 `FP64`、单精度 `FP32` 和半精度 `FP16`。因为采用不同位数的浮点数的表达精度不一样，所以造成的计算误差也不一样，对于需要处理的数字范围大而且需要精确计算的科学计算来说，就要求采用双精度浮点数，而对于常见的多媒体和图形处理计算，`32` 位的单精度浮点计算已经足够了，对于要求精度更低的机器学习等一些应用来说，半精度 `16` 位浮点数就可以甚至 `8` 位浮点数就已经够用了。\n对于浮点计算来说， `CPU` 可以同时支持不同精度的浮点运算，但在 `GPU` 里针对单精度和双精度就需要各自独立的计算单元。\n\n### 浮点计算能力\n\n`FLOPS`：每秒浮点运算次数，每秒所执行的浮点运算次数，浮点运算包括了所有涉及小数的运算，比整数运算更费时间。下面几个是表示浮点运算能力的单位。我们一般常用 `TFLOPS(Tops)` 作为衡量 `NPU/GPU` 性能/算力的指标，比如海思 `3519AV100` 芯片的算力为 `1.7Tops` 神经网络运算性能。\n\n+ `MFLOPS`（megaFLOPS）：等于每秒一佰万（=10^6）次的浮点运算。\n+ `GFLOPS`（gigaFLOPS）：等于每秒拾亿（=10^9）次的浮点运算。\n+ `TFLOPS`（teraFLOPS）：等于每秒万亿（=10^12）次的浮点运算。\n+ `PFLOPS`（petaFLOPS）：等于每秒千万亿（=10^15）次的浮点运算。\n+ `EFLOPS`（exaFLOPS）：等于每秒百亿亿（=10^18）次的浮点运算。\n\n### 硬件利用率(Utilization)\n\n在这种情况下，利用率（Utilization）是可以有效地用于实际工作负载的芯片的原始计算能力的百分比。深度学习和神经网络使用相对数量较少的计算原语（computational primitives），而这些数量很少的计算原语却占用了大部分计算时间。矩阵乘法（MM）和转置是基本操作。MM 由乘法累加（MAC）操作组成。OPs/s（每秒完成操作的数量）指标通过每秒可以完成多少个 MAC（每次乘法和累加各被认为是 1 个 operation，因此 MAC 实际上是 2 个 OP）得到。所以我们可以将利用率定义为实际使用的运算能力和原始运算能力的比值：\n\n$$ mac\\ utilization = \\frac {used\\ Ops/s}{raw\\ OPs/s} = \\frac {FLOPs/time(s)}{Raw\\_FLOPs}(Raw\\_FLOPs = 1.7T\\ at\\ 3519)$$\n\n## 五，参数量/计算量分析工具\n\n1. [torchinfo](https://github.com/TylerYep/torchinfo): torchsummay （不再更新）的替代版本，可以一键输出 `pytorch` 模型每层输出 `feature map` 大小和参数量、以及模型总的参数量 `params`、计算量 `MACs` 等信息，**支持多输入模式**。\n2. [thop: pytorch-OpCounter](https://github.com/Lyken17/pytorch-OpCounter): 一键输出模型总的计算量 `MACs` 和参数量 `params`，支持输入自定义算子。\n\n两个工具的安装都很简单，如直接 `pip install torchinfo` 即可。统计 `resnet50` 模型的参数量和计算量的示例代码如下所示。\n\n```python\n####################卷积神经网络计算量/参数量分析工具#####################\nimport torchvision, torch\n\nmodel = torchvision.models.resnet50()\n\n# 1, pytorch 自带输出\n# print(model)\n\n# 2, torchinfo 工具\nfrom torchinfo import summary\nsummary(model, (1, 3, 224, 224), depth=3) # resnet50: 25.557M 4.09G\n\n# 3, thop 工具\nfrom thop import profile, clever_format\ninput = torch.randn(1, 3, 224, 224)\nmacs, params = profile(model, inputs=(input, ))\nmacs, params = clever_format([macs, params], \"%.3f\")\nprint(\"The resnet50 model info: \", macs, params) # resnet50: 4.134G 25.557M\n```\n\n模型输出结果如下所示\n\n```bash\n==========================================================================================\nLayer (type:depth-idx)                   Output Shape              Param #\n==========================================================================================\nResNet                                   [1, 1000]                 --\n├─Conv2d: 1-1                            [1, 64, 112, 112]         9,408\n├─BatchNorm2d: 1-2                       [1, 64, 112, 112]         128\n├─ReLU: 1-3                              [1, 64, 112, 112]         --\n├─MaxPool2d: 1-4                         [1, 64, 56, 56]           --\n├─Sequential: 1-5                        [1, 256, 56, 56]          --\n│    └─Bottleneck: 2-1                   [1, 256, 56, 56]          --\n│    │    └─Conv2d: 3-1                  [1, 64, 56, 56]           4,096\n│    │    └─BatchNorm2d: 3-2             [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-3                    [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-4                  [1, 64, 56, 56]           36,864\n│    │    └─BatchNorm2d: 3-5             [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-6                    [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-7                  [1, 256, 56, 56]          16,384\n│    │    └─BatchNorm2d: 3-8             [1, 256, 56, 56]          512\n│    │    └─Sequential: 3-9              [1, 256, 56, 56]          16,896\n│    │    └─ReLU: 3-10                   [1, 256, 56, 56]          --\n│    └─Bottleneck: 2-2                   [1, 256, 56, 56]          --\n│    │    └─Conv2d: 3-11                 [1, 64, 56, 56]           16,384\n│    │    └─BatchNorm2d: 3-12            [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-13                   [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-14                 [1, 64, 56, 56]           36,864\n│    │    └─BatchNorm2d: 3-15            [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-16                   [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-17                 [1, 256, 56, 56]          16,384\n│    │    └─BatchNorm2d: 3-18            [1, 256, 56, 56]          512\n│    │    └─ReLU: 3-19                   [1, 256, 56, 56]          --\n│    └─Bottleneck: 2-3                   [1, 256, 56, 56]          --\n│    │    └─Conv2d: 3-20                 [1, 64, 56, 56]           16,384\n│    │    └─BatchNorm2d: 3-21            [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-22                   [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-23                 [1, 64, 56, 56]           36,864\n│    │    └─BatchNorm2d: 3-24            [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-25                   [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-26                 [1, 256, 56, 56]          16,384\n│    │    └─BatchNorm2d: 3-27            [1, 256, 56, 56]          512\n│    │    └─ReLU: 3-28                   [1, 256, 56, 56]          --\n├─Sequential: 1-6                        [1, 512, 28, 28]          --\n│    └─Bottleneck: 2-4                   [1, 512, 28, 28]          --\n│    │    └─Conv2d: 3-29                 [1, 128, 56, 56]          32,768\n│    │    └─BatchNorm2d: 3-30            [1, 128, 56, 56]          256\n│    │    └─ReLU: 3-31                   [1, 128, 56, 56]          --\n│    │    └─Conv2d: 3-32                 [1, 128, 28, 28]          147,456\n│    │    └─BatchNorm2d: 3-33            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-34                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-35                 [1, 512, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-36            [1, 512, 28, 28]          1,024\n│    │    └─Sequential: 3-37             [1, 512, 28, 28]          132,096\n│    │    └─ReLU: 3-38                   [1, 512, 28, 28]          --\n│    └─Bottleneck: 2-5                   [1, 512, 28, 28]          --\n│    │    └─Conv2d: 3-39                 [1, 128, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-40            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-41                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-42                 [1, 128, 28, 28]          147,456\n│    │    └─BatchNorm2d: 3-43            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-44                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-45                 [1, 512, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-46            [1, 512, 28, 28]          1,024\n│    │    └─ReLU: 3-47                   [1, 512, 28, 28]          --\n│    └─Bottleneck: 2-6                   [1, 512, 28, 28]          --\n│    │    └─Conv2d: 3-48                 [1, 128, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-49            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-50                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-51                 [1, 128, 28, 28]          147,456\n│    │    └─BatchNorm2d: 3-52            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-53                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-54                 [1, 512, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-55            [1, 512, 28, 28]          1,024\n│    │    └─ReLU: 3-56                   [1, 512, 28, 28]          --\n│    └─Bottleneck: 2-7                   [1, 512, 28, 28]          --\n│    │    └─Conv2d: 3-57                 [1, 128, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-58            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-59                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-60                 [1, 128, 28, 28]          147,456\n│    │    └─BatchNorm2d: 3-61            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-62                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-63                 [1, 512, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-64            [1, 512, 28, 28]          1,024\n│    │    └─ReLU: 3-65                   [1, 512, 28, 28]          --\n├─Sequential: 1-7                        [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-8                   [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-66                 [1, 256, 28, 28]          131,072\n│    │    └─BatchNorm2d: 3-67            [1, 256, 28, 28]          512\n│    │    └─ReLU: 3-68                   [1, 256, 28, 28]          --\n│    │    └─Conv2d: 3-69                 [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-70            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-71                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-72                 [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-73            [1, 1024, 14, 14]         2,048\n│    │    └─Sequential: 3-74             [1, 1024, 14, 14]         526,336\n│    │    └─ReLU: 3-75                   [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-9                   [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-76                 [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-77            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-78                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-79                 [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-80            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-81                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-82                 [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-83            [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-84                   [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-10                  [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-85                 [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-86            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-87                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-88                 [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-89            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-90                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-91                 [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-92            [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-93                   [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-11                  [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-94                 [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-95            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-96                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-97                 [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-98            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-99                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-100                [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-101           [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-102                  [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-12                  [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-103                [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-104           [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-105                  [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-106                [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-107           [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-108                  [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-109                [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-110           [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-111                  [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-13                  [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-112                [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-113           [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-114                  [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-115                [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-116           [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-117                  [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-118                [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-119           [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-120                  [1, 1024, 14, 14]         --\n├─Sequential: 1-8                        [1, 2048, 7, 7]           --\n│    └─Bottleneck: 2-14                  [1, 2048, 7, 7]           --\n│    │    └─Conv2d: 3-121                [1, 512, 14, 14]          524,288\n│    │    └─BatchNorm2d: 3-122           [1, 512, 14, 14]          1,024\n│    │    └─ReLU: 3-123                  [1, 512, 14, 14]          --\n│    │    └─Conv2d: 3-124                [1, 512, 7, 7]            2,359,296\n│    │    └─BatchNorm2d: 3-125           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-126                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-127                [1, 2048, 7, 7]           1,048,576\n│    │    └─BatchNorm2d: 3-128           [1, 2048, 7, 7]           4,096\n│    │    └─Sequential: 3-129            [1, 2048, 7, 7]           2,101,248\n│    │    └─ReLU: 3-130                  [1, 2048, 7, 7]           --\n│    └─Bottleneck: 2-15                  [1, 2048, 7, 7]           --\n│    │    └─Conv2d: 3-131                [1, 512, 7, 7]            1,048,576\n│    │    └─BatchNorm2d: 3-132           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-133                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-134                [1, 512, 7, 7]            2,359,296\n│    │    └─BatchNorm2d: 3-135           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-136                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-137                [1, 2048, 7, 7]           1,048,576\n│    │    └─BatchNorm2d: 3-138           [1, 2048, 7, 7]           4,096\n│    │    └─ReLU: 3-139                  [1, 2048, 7, 7]           --\n│    └─Bottleneck: 2-16                  [1, 2048, 7, 7]           --\n│    │    └─Conv2d: 3-140                [1, 512, 7, 7]            1,048,576\n│    │    └─BatchNorm2d: 3-141           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-142                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-143                [1, 512, 7, 7]            2,359,296\n│    │    └─BatchNorm2d: 3-144           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-145                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-146                [1, 2048, 7, 7]           1,048,576\n│    │    └─BatchNorm2d: 3-147           [1, 2048, 7, 7]           4,096\n│    │    └─ReLU: 3-148                  [1, 2048, 7, 7]           --\n├─AdaptiveAvgPool2d: 1-9                 [1, 2048, 1, 1]           --\n├─Linear: 1-10                           [1, 1000]                 2,049,000\n==========================================================================================\nTotal params: 25,557,032\nTrainable params: 25,557,032\nNon-trainable params: 0\nTotal mult-adds (G): 4.09\n==========================================================================================\nInput size (MB): 0.60\nForward/backward pass size (MB): 177.83\nParams size (MB): 102.23\nEstimated Total Size (MB): 280.66\n==========================================================================================\n[INFO] Register count_convNd() for <class 'torch.nn.modules.conv.Conv2d'>.\n[INFO] Register count_normalization() for <class 'torch.nn.modules.batchnorm.BatchNorm2d'>.\n[INFO] Register zero_ops() for <class 'torch.nn.modules.activation.ReLU'>.\n[INFO] Register zero_ops() for <class 'torch.nn.modules.pooling.MaxPool2d'>.\n[INFO] Register zero_ops() for <class 'torch.nn.modules.container.Sequential'>.\n[INFO] Register count_adap_avgpool() for <class 'torch.nn.modules.pooling.AdaptiveAvgPool2d'>.\n[INFO] Register count_linear() for <class 'torch.nn.modules.linear.Linear'>.\n```\n\n值得注意的是，不同工具统计出来的模型计算量和参数量可能不一样，因为计算方式不一样，但是都是比较准确的。使用 `thop` 工具统计的经典 backbone 的 `Params` 参数量与 `FLOPs` 计算量如下表所示：\n\n![模型复杂度分析结果](../data/images/flops/thop_summary.png)\n\n## 六，参考资料\n\n+ [PRUNING CONVOLUTIONAL NEURAL NETWORKS FOR RESOURCE EFFICIENT INFERENCE](https://arxiv.org/pdf/1611.06440.pdf)\n+ [神经网络参数量的计算：以UNet为例](https://zhuanlan.zhihu.com/p/57437131)\n+ [How fast is my model?](http://machinethink.net/blog/how-fast-is-my-model/)\n+ [MobileNetV1 & MobileNetV2 简介](https://blog.csdn.net/mzpmzk/article/details/82976871)\n+ [双精度，单精度和半精度](https://blog.csdn.net/sinat_24143931/article/details/78557852?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase)\n+ [AI硬件的Computational Capacity详解](https://zhuanlan.zhihu.com/p/27836831)\n+ [Roofline Model与深度学习模型的性能分析](https://zhuanlan.zhihu.com/p/34204282)\n+ [目标检测的性能评价指标](https://zhuanlan.zhihu.com/p/70306015?utm_source=wechat_session&utm_medium=social&utm_oi=571954943427219456)\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2021 OpenPPL\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\nCV 算法工程师成长之路\n</h1>\n\n<p align=\"center\">\n  <a href=\"#License\"><img src=\"./data/icons/License-Apache-2.0-green.svg\" alt=\"LICENSE\"></a>\n  <a href=\"http://www.armcvai.com/\"><img src=\"./data/icons/Website-armcvai-brightgreen.svg\", alt=\"嵌入式视觉\"></a>\n  <a href=\"https://www.zhihu.com/people/tang-fen-44-49\"><img src=\"https://img.shields.io/badge/zhihu-知乎-informational\" alt=\"进击的程序猿\"></a>\n  <a href=\"https://blog.csdn.net/qq_20986663\"><img src=\"https://img.shields.io/badge/csdn-CSDN-red.svg\" alt=\"嵌入式视觉\"></a>\n  <a href=\"https://www.cnblogs.com/armcvai/\"><img src=\"https://img.shields.io/badge/cnblogs-博客园-important.svg\" alt=\"嵌入式视觉\"></a>\n  <a href=\"https://juejin.cn/user/3034307824977127/columns\"><img src=\"https://img.shields.io/badge/juejin-%E6%8E%98%E9%87%91-important.svg\" alt=\"嵌入式视觉\"></a>\n  <a href=\"https://github.com/HarleysZhang/2021_algorithm_intern_information/stargazers\"><img src=\"https://badgen.net/github/stars/HarleysZhang/2021_algorithm_intern_information?color=cyan\" alt=\"stars\"></a>\n  <a href=\"https://github.com/HarleysZhang/2021_algorithm_intern_information/network/members\"><img src=\"https://badgen.net/github/forks/HarleysZhang/2021_algorithm_intern_information?color=cyan\" alt=\"forks\"></a>\n</p>\n\n- [我的自制大模型推理框架课程介绍](#我的自制大模型推理框架课程介绍)\n- [前言](#前言)\n- [目录](#目录)\n- [学习路线](#学习路线)\n- [算法基础](#算法基础)\n- [可投递公司](#可投递公司)\n- [我的公众号](#我的公众号)\n- [Star History](#star-history)\n\n## 我的自制大模型推理框架课程介绍\n\n1. **框架亮点**：基于 Triton + PyTorch 开发的轻量级、且简单易用的大模型推理框架，采用类 Pytorch 语法的 Triton 编写算子，绕开 Cuda 复杂语法实现 GPU 加速。\n2. **价格：499**。非常实惠和便宜，课程 + 项目 + 面经 + 答疑质量绝对对得起这个价格。\n3. **课程优势​**：\n   - **手把手教你从 0 到 1 实现大模型推理框架**。\n   - 项目导向 + 面试导向 + **分类总结的面试题**。\n   - 2025 最新的高性能计算/推理框架岗位的大厂面试题汇总\n4. **项目优势​**：\n\t- 架构清晰，代码简洁且注释详尽，覆盖大模型离线推理全流程。​\n    - 运用 OpenAI Triton 编写高性能计算 Kernel，开发矩阵乘法内核，效率堪比 cuBLAS。​\n    - 依托 PyTorch 进行高效显存管理。​\n    - 课程项目完美支持 FlashAttentionV1、V2、V3 与 `GQA`，以及 `PageAttention` 的具体实现。​\n    - 使用 `Triton` 编写融合算子，如 KV 线性层融合等。​\n    - 适配最新的 `llama/qwen2.5/llava1.5` 模型，相较 transformers 库，在 llama3 1B 和 3B 模型上，加速比最高可达 `4` 倍。\n5. **分类总结部分面试题**：\n\n<table style=\"width: 100%; table-layout: fixed;\">\n  <tr>\n    <td align=\"center\"><img src=\"./data/images/read_me/interview1.png\" width=\"100%\" alt=\"llava_output2\"></td>\n    <td align=\"center\"><img src=\"./data/images/read_me/interview2.png\" width=\"100%\" alt=\"llava_output\"></td>\n  </tr>\n</table>\n\n6. **项目运行效果**:\n\n`llama3.2-1.5B-Instruct` 模型流式输出结果测试：\n\n![流式输出](./data/images/read_me/generate.gif)\n\n`Qwen2.5-3B` 模型（社区版本）流式输出结果测试：\n\n![流式输出](./data/images/read_me/output.gif)\n\n`Llava1.5-7b-hf` 模型流式输出结果测试:\n\n<table style=\"width: 100%; table-layout: fixed;\">\n  <tr>\n    <td align=\"center\"><img src=\"./data/images/read_me/llava_output2.gif\" width=\"90%\" alt=\"llava_output2\"></td>\n    <td align=\"center\"><img src=\"./data/images/read_me/llava_output1.gif\" width=\"100%\" alt=\"llava_output\"></td>\n  </tr>\n</table>\n\n感兴趣的同学可以扫码联系课程购买，这个课程是我和[《自制深度学习推理框架》作者](https://space.bilibili.com/1822828582)一起合力打造的，内容也会持续更新优化。\n\n<div align=\"center\">\n<img src=\"./data/images/read_me/fu_qcode.jpg\" width=\"40%\" alt=\"transformer_block_mp\">\n</div>\n\n## 前言\n\n> 本项目最初是当作 cv 算法工程师实习内推表、校招可投递公司汇总以及个人面经的汇总，后面逐步转变为个人 cv 算法工程师成长之路所记录的技术栈笔记、以及少部分面经等内容。\n\n项目部分内容参考自 `github` 项目/网络博客/书籍和 [个人博客](http://www.armcvai.com/) 等，由于时间和精力有限，有些知识点还没有没有完成，请见谅。\n\n本项目正逐步废弃，大部分内容不再更新，关于深度学习、大模型推理以及大模型推理框架开发的知识，欢迎移步 [dl_note](https://github.com/HarleysZhang/dl_note)、和 [lite_llama](https://github.com/harleyszhang/lite_llama) 仓库阅读。\n\n> `GitHub` 已经支持直接显示 `latex` 公式，部分公式如果显示不全，也可在谷歌浏览器安装 [MathJax Plugin for Github](https://chrome.google.com/webstore/detail/mathjax-plugin-for-github/ioemnmodlmafdkllaclgeombjnmnbima?hl=zh-CN) 插件访问(需要翻墙下载安装)，或者下载仓库到本地，使用 `Typora` 软件阅读，也可以使用安装了 `Markdown+Math` 插件的 `VSCode` 软件阅读。\n\n## 目录\n\n作为一个计算机视觉算法工程师，需要掌握的不仅是计算机编程知识，还需要掌握**编程开发、机器学习/深度学习、图像识别/目标检测/语义分割、模型压缩、模型部署**等知识点，我整理了一个 [技术栈思维导图](./data/images/CV算法工程师应掌握知识点.png)。\n\n强调一下如何从“零”起步，首先确保基础打好。建议完整修完一门国外经典课程（从课程视频、作业到项目），然后完整阅读一本机器学习或者深度学习教科书，同时熟练掌握一门基本的编程语言以及深度学习框架。（参考 [中国人民大学赵鑫：AI 科研入坑指南](https://mp.weixin.qq.com/s/h00VmCi1E7IhIDCj7X1ZjQ)）\n\n+ [计算机基础](1-computer_basics)\n+ [编程语言](2-programming_language)\n+ [数据结构与算法](3-data_structure-algorithm)\n+ [机器学习](4-machine_learning)\n+ [深度学习](5-deep_learning)\n+ [计算机视觉](6-computer_vision)\n+ [模型压缩与量化](7-model_compression)\n+ [高性能计算](8-high-performance_computing)\n+ [模型部署](9-model_deploy)\n+ [图像算法岗面经](interview_summary)\n\n## 学习路线\n\n[cv算法工程师学习成长路线](./cv算法工程师成长路线.md)\n\n## 算法基础\n\n> 深度学习基础的和 `Python` 编程基础知识总结。\n\n+ [深度学习基础](4-deep_learning/深度学习基础总结.md)\n+ [Python3 基础](2-programming_language/python3/python3编程总结.md)\n\n## 可投递公司\n\n> 鉴于 2019 年写的春招算法实习岗位表绝大部分已经失效，本人也再无精力维护，故将其移除，故不在展示在仓库主页上。\n\n虽然算法工程师可投递的公司是较多的，但是岗位提供的 `hc` 是不及开发多的，这点需要注意。以下表格侧重于计算机视觉算法和算法优化/部署工程师岗位。\n\n|`top`级公司|互联网公司|AI独角兽公司|其他大公司|\n|------------|---------------|---------------|-------------|\n|百度|美团|地平线机器人|顺丰科技|\n|阿里巴巴|滴滴出行|图森未来|招银网络科技|\n|腾讯|拼多多/菜鸟网络|momenta|平安科技|\n|字节跳动|京东|小马智行|cvte|\n|微软|网易|蔚来汽车|海康威视|\n|谷歌|快手|小鹏汽车|虹软科技|\n|商汤|爱奇艺|科大讯飞|传音手机|\n|英伟达|小米|寒武纪/依图|大华|\n|博世|陌陌|旷视|荣耀手机|\n|大疆无人机|美图MTlab|文远知行|联想|\n|蚂蚁金服|360安全|云天励飞|汇顶科技|\n|Intel/亚马逊|搜狗|摩尔线程|美的中央研究院|\n|华为|猿辅导|思必驰|锐明技术|\n|无|新浪/搜狐/金山|奥比中光|联发科|\n|无|YY/虎牙/BIGO/斗鱼|优必选|联影医疗|\n|无|oppo/vivo/一加|度小满金融|戴尔|\n|无|贝壳找房|深睿医疗|TP-LINK|\n|无|携程/去哪儿/途家|镁佳科技|ZOOM|\n|无|瓜子二手车|猎豹移动|广联达|\n|无|作业帮/VIPKID/好未来|京东数科|深信服|\n|无|阅文集团/58集团|追一科技|中国电信云计算|\n|无|B站|深兰科技|三星电子研究所|\n|无|小红书/英语流利说|明略科技|苏宁|\n|无|趣头条/一点资讯|数美科技|微众银行|\n|无|知乎|驭势科技|中国移动成研院|\n|无|蘑菇街|随手科技|远景智能|\n|无|转转|智加科技|牧原智能科技|\n|无|同花顺/老虎证券|壁仞科技|便利蜂|\n|无|乐信/有赞|趋势科技|中兴|\n|无|金蝶软件(中国)|云从科技|航天二院706所|\n|无|汽车之家|第四范式|吉利汽车|\n|无|珍爱网/酷狗音乐|黑芝麻智能|碧桂园机器人|\n|无|巨人网络/盛大游戏|格灵深瞳|华米/极米|无|\n|无|最右/快看漫画|码隆科技|无|\n|无|猫眼娱乐/多牛传媒|轻舟智航|无|无|\n\n## 我的公众号\n\n更多知识和最新博客，欢迎扫码关注我的公众号-**嵌入式视觉**，记录 `CV` 算法工程师成长之路，分享技术总结、读书笔记和个人感悟。\n\n<p align=\"center\">\n  <a href=\"#嵌入式视觉\"><img src=\"./data/qcode.png\" alt=\"公众号-嵌入式视觉\"></a>\n</p>\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=HarleysZhang/cv_note&type=Date)](https://star-history.com/#HarleysZhang/cv_note&Date)\n"
  },
  {
    "path": "SUMMARY.md",
    "content": "# cv算法工程师成长之路\n\n* [cv算法工程师成长路线](cv算法工程师成长路线.md)\n- [1. 计算机基础](1-computer_basics/README.md)\n  - 效率工具\n    * [Docker基础和常用命令](1-computer_basics/效率工具/Docker基础和常用命令.md)\n    * [git命令学习笔记](1-computer_basics/效率工具/git命令学习笔记.md)\n    * [ubuntu16.04安装mmdetection库](1-computer_basics/效率工具/ubuntu16.04安装mmdetection库.md)\n  - 操作系统\n    * [计算机基础知识](1-computer_basics/操作系统/计算机基础知识.md)\n    * [深入理解计算机系统-第1章计算机系统漫游笔记](1-computer_basics/操作系统/深入理解计算机系统-第1章计算机系统漫游笔记.md)\n    * [深入理解计算机系统-第2章信息的表示和处理](1-computer_basics/操作系统/深入理解计算机系统-第2章信息的表示和处理.md)\n    * [深入理解计算机系统-第3章程序的机器级表示](1-computer_basics/操作系统/深入理解计算机系统-第3章程序的机器级表示.md)\n  - Linux 系统\n    * [Linux 基础-学会使用命令帮助](1-computer_basics/Linux系统/Linux基础-学会使用命令帮助.md)\n    * [Linux 基础-新手必备命令](1-computer_basics/Linux系统/Linux基础-新手必备命令.md)\n    * [Linux 基础-文件权限与属性](1-computer_basics/Linux系统/Linux基础-文件权限与属性.md)\n    * [Linux 基础-文件及目录管理](1-computer_basics/Linux系统/Linux基础-文件及目录管理.md)\n    * [Linux 基础-文本处理命令](1-computer_basics/Linux系统/Linux基础-文本处理命令.md)\n    * [Linux 基础-查看cpu、内存和环境等信息](1-computer_basics/Linux系统/Linux基础-查看cpu、内存和环境等信息.md)\n    * [Linux 基础-查看进程命令ps和top](1-computer_basics/Linux系统/Linux基础-查看进程命令ps和top.md)\n    * [Linux 基础-查看和设置环境变量](1-computer_basics/Linux系统/Linux基础-查看和设置环境变量.md)\n- [2. 编程开发](2-programming_language/README.md)\n  - python\n    * [Python3编程面试题](2-programming_language/python3/Python3编程面试题.md)\n  - shell\n    * [Shell语法基础](2-programming_language/shell/Shell语法基础.md)\n  - cpp\n    * [C++ Primer学习笔记](2-programming_language/cpp/C++%20Primer学习笔记.md)\n    * [C++基础-资源管理:堆栈与RAII](2-programming_language/cpp/C++基础-资源管理:堆栈与RAII.md)\n    * [C++日期和时间编程总结](2-programming_language/cpp/C++日期和时间编程总结.md)\n    * [C++编程基础](2-programming_language/cpp/C++编程基础.md)\n- [3. 数据结构和算法](3-data_structure-algorithm/README.md)\n  - 数据结构\n    * [数组](3-data_structure-algorithm/array/数组.md)\n    * [链表](3-data_structure-algorithm/linked_list/链表.md)\n    * [字符串](3-data_structure-algorithm/string/字符串解题模板.md)\n    * [栈](3-data_structure-algorithm/stack_queue_heap/栈-先进后出.md)\n    * [队列](3-data_structure-algorithm/stack_queue_heap/队列-先进先出.md)\n    * [跳表](3-data_structure-algorithm/skip_list/跳表.md)\n    * 散列表\n      * [散列表上](3-data_structure-algorithm/hash_table/散列表(上)-理论.md)\n      * [散列表中](3-data_structure-algorithm/hash_table/散列表(中)-实战.md)\n      * [散列表下](3-data_structure-algorithm/hash_table/散列表(下)-散列表和链表的组合.md)\n      * [哈希算法](3-data_structure-algorithm/hash_table/哈希算法.md)\n    - 二叉树\n      * [二叉树遍历](3-data_structure-algorithm/tree/二叉树(上)-二叉树遍历.md)\n      * [二叉查找树](3-data_structure-algorithm/tree/二叉树(下)-二叉查找树.md)\n      * [红黑树](3-data_structure-algorithm/tree/红黑树(上)-基础.md)\n    - 图\n      * [图的表示](3-data_structure-algorithm/map/图的表示.md)\n      * [广度和深度优先搜索](3-data_structure-algorithm/map/广度和深度优先搜索.md)\n  - 算法\n    * [双指针](3-data_structure-algorithm/two_pointers/README.md)\n    * [递归](3-data_structure-algorithm/recursion/递归-需要满足三个条件.md)\n    * [二分查找](3-data_structure-algorithm/binary_search/二分查找.md)\n    * [排序](3-data_structure-algorithm/sort/排序.md)\n    * [贪心算法](3-data_structure-algorithm/greedy/贪心算法.md)\n    * [分治算法](3-data_structure-algorithm/divide_and_conquer/分治算法.md)\n    * [回溯算法](3-data_structure-algorithm/backtracking/回溯算法.md)\n    - 动态规划算法\n      * [初始动态规划](3-data_structure-algorithm/dp/初识动态规划.md)\n      * [动态规划理论](3-data_structure-algorithm/dp/动态规划理论.md)\n      * [动态规划实战](3-data_structure-algorithm/dp/动态规划实战.md)  \n  * [剑指offer题解c++版](3-data_structure-algorithm/剑指offer题解c++版.md)\n  * [算法图解笔记](3-data_structure-algorithm/算法图解笔记.md)\n- [4. 机器学习](4-machine_learning/README.md)\n  * [1-西瓜书笔记](4-machine_learning/1-西瓜书笔记.md)\n  * [2-机器学习基础](4-machine_learning/2-机器学习基础.md)\n  * [3-常见机器学习算法](4-machine_learning/3-常见机器学习算法.md)\n  * [4-机器学习面试题](4-machine_learning/4-机器学习面试题.md)\n- [5. 深度学习](5-deep_learning/README.md)\n  - backbone 论文解读\n    * [DenseNet网络理解](5-deep_learning/backbone论文解读/DenseNet网络理解.md)\n    * [ResNetv2论文解读](5-deep_learning/backbone论文解读/ResNetv2论文解读.md)\n    * [经典backbone总结](5-deep_learning/backbone论文解读/经典backbone总结.md)\n  - 深度学习框架笔记\n    * [MXNet学习笔记](5-deep_learning/ml-dl-框架笔记/MXNet学习笔记.md)\n    * [Pytorch基础-tensor数据结构](5-deep_learning/ml-dl-框架笔记/Pytorch基础-tensor数据结构.md)\n    * [Pytorch基础-张量基本操作](5-deep_learning/ml-dl-框架笔记/Pytorch基础-张量基本操作.md)\n  - 深度学习基础\n    * [1-深度学习算法基础](5-deep_learning/深度学习基础/1-深度学习算法基础.md)\n    * [2-使用CNN进行图像分类](5-deep_learning/深度学习基础/2-使用CNN进行图像分类.md)\n    * [CNN基本部件-常用激活函数](5-deep_learning/深度学习基础/CNN基本部件-常用激活函数.md)\n  * [深度学习面试题](5-deep_learning/深度学习面试题.md)\n- [6. 计算机视觉](6-computer_vision/README.md)\n  - 数字图像处理\n    * [0-数字图像处理笔记](6-computer_vision/数字图像处理/0-数字图像处理笔记.md)\n    * [1-数字图像处理基础](6-computer_vision/数字图像处理/1-数字图像处理基础.md)\n    * [2-OpenCV3图像处理笔记](6-computer_vision/数字图像处理/2-OpenCV3图像处理笔记.md)\n    * [3-视频编解码基础](6-computer_vision/数字图像处理/3-视频编解码基础.md)\n  - 2D 目标检测\n    * [0-目标检测模型的基础](6-computer_vision/2D目标检测/0-目标检测模型的基础.md)\n    * [1-目标检测模型的评价标准-AP与mAP](6-computer_vision/2D目标检测/1-目标检测模型的评价标准-AP与mAP.md)\n    * [2-Faster-RCNN网络详解](6-computer_vision/2D目标检测/2-Faster-RCNN网络详解.md)\n    * [3-FPN网络详解](6-computer_vision/2D目标检测/3-FPN网络详解.md)\n    * [4-Mask-RCNN详解](6-computer_vision/2D目标检测/4-Mask-RCNN详解.md)\n    * [5-Cascade-RCNN论文解读](6-computer_vision/2D目标检测/5-Cascade-RCNN论文解读.md)\n    * [6-RetinaNet网络详解](6-computer_vision/2D目标检测/6-RetinaNet网络详解.md)\n    * [7-YOLOv1-v5论文解读](6-computer_vision/2D目标检测/7-YOLOv1-v5论文解读.md)\n    * [8-Scaled-YOLOv4论文解读](6-computer_vision/2D目标检测/8-Scaled-YOLOv4论文解读.md)\n  - 3D 视觉算法\n    * [3D视觉算法初学概述](6-computer_vision/3D视觉算法/3D视觉算法初学概述.md)\n  - 工业视觉\n    * [Halcon快速入门](6-computer_vision/工业视觉/Halcon快速入门.md)\n  - 项目实践\n    * [GitHub车牌检测识别项目调研](6-computer_vision/项目实践/GitHub车牌检测识别项目调研.md)\n- [7. 模型压缩](7-model_compression/README.md)\n  - 轻量级网络论文解析\n    * [CSPNet论文详解](7-model_compression/轻量级网络论文解析/CSPNet论文详解.md)\n    * [MobileNetv1论文详解](7-model_compression/轻量级网络论文解析/MobileNetv1论文详解.md)\n    * [RepVGG论文详解](7-model_compression/轻量级网络论文解析/RepVGG论文详解.md)\n    * [ShuffleNetv2论文详解](7-model_compression/轻量级网络论文解析/ShuffleNetv2论文详解.md)\n    * [VoVNet论文解读](7-model_compression/轻量级网络论文解析/VoVNet论文解读.md)\n    * [轻量级模型设计与部署总结](7-model_compression/轻量级网络论文解析/轻量级模型设计与部署总结.md)\n  - 神经网络量化 \n    * [神经网络量化](7-model_compression/神经网络量化/神经网络量化基础.md)\n- [8. 高性能计算](8-high-performance_computing/README.md)\n  * [0-处理器基础知识](8-high-performance_computing/0-处理器基础知识.md)\n  * [1-卷积算法的优化](8-high-performance_computing/1-卷积算法的优化.md)\n  * [2-模型编译优化](8-high-performance_computing/2-模型编译优化.md)\n  * [通用矩阵乘算法从入门到实践](8-high-performance_computing/通用矩阵乘算法从入门到实践.md)\n- [9. 模型部署](9-model_deploy/README.md)\n  - 推理框架\n    * [ONNX模型分析与使用](9-model_deploy/推理框架/ONNX模型分析与使用.md)\n    * [TensorRT基础笔记](9-model_deploy/推理框架/TensorRT基础笔记.md)\n  * [0-模型压缩部署概述](9-model_deploy/0-模型压缩部署概述.md)\n  * [1-神经网络模型复杂度分析](9-model_deploy/1-神经网络模型复杂度分析.md)\n  * [2-模型转换](9-model_deploy/2-模型转换总结.md)\n  * [3-模型板端推理](9-model_deploy/3-模型板端推理.md)\n- [Interview Summary](interview_summary/README.md)\n  * [计算机视觉岗 2019 届实习面经.md](interview_summary/1-计算机视觉岗2019届实习面经.md)\n  * [计算机视觉岗 2019 届暑期实习应聘总结](interview_summary/2-计算机视觉岗2019届暑期实习应聘总结.md)\n  * [2019 届地平线机器人实习总结](interview_summary/3-2019届地平线机器人实习总结.md)\n  * [计算机视觉岗 2020 届秋招面经](interview_summary/4-计算机视觉岗2020届秋招面经.md)\n  * [视觉算法岗 2021 年社招面经](interview_summary/5-视觉算法岗2021年社招面经.md)\n"
  },
  {
    "path": "book.json",
    "content": "{\n    \"title\": \"cv算法工程师成长之路\",\n    \"author\": \"zhanghonggao\",\n    \"theme-default\": {\n        \"showLevel\": true\n    },\n    \"plugins\": [\n        \"toggle-chapters\", \n        \"back-to-top-button\",\n        \"chapter-fold\",\n        \"code\",\n        \"splitter\",\n        \"-lunr\", \n        \"search-pro\",\n        \"pageview-count\",\n        \"tbfed-pagefooter\",\n        \"popup\",\n        \"sharing-plus\",\n        \"donate\"\n    ],\n    \"pluginsConfig\": {\n        \"tbfed-pagefooter\": {\n            \"copyright\": \"Copyright &copy zhg5200211@outlook.com 2022\",\n            \"modify_label\": \"该文章修订时间：\",\n            \"modify_format\": \"YYYY-MM-DD HH:mm:ss\"\n        },\n        \"sharing\": {\n            \"all\": [\n                \"douban\", \"facebook\", \"google\", \"twitter\", \"weibo\", \"qq\"\n            ]\n        },\n        \"donate\": {\n            \"wechat\": \"./data/article_cover/wechat.png\",\n            \"alipay\": \"./data/article_cover/alipay.png\",\n            \"title\": \"赞赏\",\n            \"button\": \"捐赠\",\n            \"alipayText\": \"支付宝\",\n            \"wechatText\": \"微信\"\n        }\n    }\n}"
  },
  {
    "path": "cv算法工程师成长路线.md",
    "content": "- [前言](#前言)\n- [一，计算机系统](#一计算机系统)\n  - [1.1，计算机系统书籍](#11计算机系统书籍)\n  - [1.2，设计模式教程](#12设计模式教程)\n- [二，编程语言](#二编程语言)\n  - [2.1，C++ 学习资料](#21c-学习资料)\n  - [2.2，Python 学习资料](#22python-学习资料)\n  - [2.3，Linux \\& Shell 学习](#23linux--shell-学习)\n- [三，数据结构与算法](#三数据结构与算法)\n  - [3.1，数据结构与算法课程](#31数据结构与算法课程)\n  - [3.2，算法题解](#32算法题解)\n- [四，机器学习](#四机器学习)\n  - [4.1，机器学习课程](#41机器学习课程)\n- [五，深度学习](#五深度学习)\n  - [5.1，深度学习课程](#51深度学习课程)\n  - [5.2，深度学习基础文章](#52深度学习基础文章)\n  - [5.3，经典CNN分析文章](#53经典cnn分析文章)\n  - [5.4，PyTorch 框架学习文章](#54pytorch-框架学习文章)\n  - [5.5，PyTorch/Caffe 框架分析文章](#55pytorchcaffe-框架分析文章)\n- [六，计算机视觉](#六计算机视觉)\n  - [6.1，数字图像处理教程](#61数字图像处理教程)\n  - [6.2，计算机视觉基础课程](#62计算机视觉基础课程)\n  - [6.3，深度学习模型和资源库](#63深度学习模型和资源库)\n  - [6.4，目标检测网络文章](#64目标检测网络文章)\n  - [6.5，语义分割文章](#65语义分割文章)\n  - [6.6，3D 视觉技术文章](#663d-视觉技术文章)\n  - [6.7，深度学习的评价指标文章](#67深度学习的评价指标文章)\n- [七，模型压缩](#七模型压缩)\n  - [7.1，轻量级网络设计](#71轻量级网络设计)\n  - [7.2，模型压缩文章](#72模型压缩文章)\n  - [7.3，神经网络量化文章](#73神经网络量化文章)\n  - [7.4，推理框架剖析文章](#74推理框架剖析文章)\n- [八，高性能计算](#八高性能计算)\n  - [8.1，CPU/GPU/AI 芯片科普](#81cpugpuai-芯片科普)\n  - [8.2，指令集(ISA)学习资料](#82指令集isa学习资料)\n  - [8.3，矩阵乘优化文章](#83矩阵乘优化文章)\n- [九，模型部署(算法SDK开发)](#九模型部署算法sdk开发)\n  - [9.1，模型部署文章](#91模型部署文章)\n- [十，数据分析](#十数据分析)\n  - [pandas](#pandas)\n  - [matplotlib](#matplotlib)\n- [效率工具](#效率工具)\n  - [外面的世界](#外面的世界)\n  - [快捷导航](#快捷导航)\n  - [markdown/latex 写作](#markdownlatex-写作)\n- [业余生活](#业余生活)\n- [博客阅读后的知识点总结](#博客阅读后的知识点总结)\n- [参考资料](#参考资料)\n\n\n> 文章同步发于 [github 仓库](https://github.com/HarleysZhang/cv_note) 和 [csdn 博客](http://t.csdn.cn/jDT39)，**最新版以 `github` 为主**。如果看完文章有所收获，一定要先点赞后收藏。毕竟，赠人玫瑰，手有余香.\n\n本文内容为 `cv` 算法工程师成长子路上的经典学习教材汇总，对于一些新兴领域则给出了较好的博客文章链接。本文列出的知识点目录是**成系统且由浅至深**的，可作为 `cv` 算法工程师的**常备学习路线资料**。\n\n文章所涉知识点和参考资料内容很多也很广，**建议先看目录**，心中有个大概知识点结构，后面由浅入深，慢慢学习各个知识点，由浅入深，**切忌浮躁**。\n\n> 部分学习资料存在离线 `PDF` 电子版，其可在 [github仓库-cv_books](https://github.com/HarleysZhang/cv-books) 中下载。如果仓库失效，可以关注我的[公众号-嵌入式视觉](https://github.com/HarleysZhang/cv_note/blob/master/data/article_cover/qrcode_258.jpg)，后台回复对应关键字下载高清 PDF 电子书。\n\n## 前言\n\n**课程学习方法**，三句话总结：\n\n- 看授课视频形成概念，发现个人感兴趣方向。\n- 读课程笔记理解细节，夯实工程实现的基础。\n- 码课程作业实现算法，积累实验技巧与经验。\n\n再引用一下学习金字塔的图：\n\n![way_of_learning](./data/images/cv_learn/way_of_learning.png)\n> 图片来源 `github` 仓库 [DeepLearning Tutorial](https://github.com/Mikoto10032/DeepLearning)\n\n关于科研和研发的思考，可参考文章-[中国人民大学赵鑫：AI 科研入坑指南](https://mp.weixin.qq.com/s/h00VmCi1E7IhIDCj7X1ZjQ)。\n\n## 一，计算机系统\n\n### 1.1，计算机系统书籍\n\n1. 《深入理解计算机系统第三版》： 网上有电子版，`PDF` 电子书下载方式在文章首页。\n\n### 1.2，设计模式教程\n\n1. [设计模式](https://refactoringguru.cn/design-patterns/catalog)： 内容很全，存在 `C++` 示例代码。\n\n## 二，编程语言\n\n### 2.1，C++ 学习资料\n\n1. [cpp reference](https://en.cppreference.com/w/): `C++` 库接口参考标准文档，官方文档，包含各个函数定义及使用 `example`。\n2. [http://www.cplusplus.com/reference/stl/](http://www.cplusplus.com/reference/stl/ \"http://www.cplusplus.com/reference/stl/\")\n3. [Cpp Primer 学习](https://github.com/applenob/Cpp_Primer_Practice): 《C++ Primer 中文版（第 5 版）》学习仓库，包括笔记和课后练习答案。\n4. [C++ Tips of the Week](https://abseil.io/tips/): 谷歌出品的 `C++` **编程技巧**。\n\n### 2.2，Python 学习资料\n\n1. [《廖雪峰-Python3教程》](https://www.liaoxuefeng.com/wiki/1016959663602400): 内容很全且通俗易懂，适合初学者，但代码示例不够丰富。描述的知识点有：Python 基础、函数、高级特性、函数式编程、模块、面向对象编程、面向对象高级编程、错误、调试和测试、IO 编程、进程和线程、正则表达式、常用内建模块、常用第三方模块、图形界面、网络编程、异步IO 等内容。电子书可在[github仓库-cv_books](https://github.com/HarleysZhang/cv-books) 中下载。\n2. [Python 工匠系列文章](https://github.com/piglei/one-python-craftsman): 很适合**深入理解** Python 面向对象编程、装饰器、模块、异常处理等内容。\n\n### 2.3，Linux & Shell 学习\n\n1. 《鸟哥的Linux私房菜 基础篇 第四版》: 很经典的教材，虽然内容描述有些啰嗦，但是很多人推荐，应该还是很适合初学者的。存在开源 PDF，也有[繁体中文在线网站](https://linux.vbird.org/linux_basic_train/centos8/)。\n2. [Linux 工具快速教程](https://linuxtools-rst.readthedocs.io/zh_CN/latest/index.html): 适合有一定 Linux 基础的同学，全书分为三个部分: 基础篇、进阶篇和工具参考篇。本书我看下来的感觉就是，目录和导航索引做的很好，但是部分知识点的描述很少或者不清晰，更偏向于实例使用教程的感觉，**适合当作参考书使用**。\n3. \n## 三，数据结构与算法\n\n### 3.1，数据结构与算法课程\n1. 《图解算法》：存在 `PDF` 电子版，内容较为基础且通俗易懂，适合快速了解数据结构与算法的基础知识，但深度不够，示例代码为 `Python`。\n2. [专栏-数据结构与算法之美](https://time.geekbang.org/column/intro/126): 学习数据结构与算法的知识点课程，**内容全且深度足够**。官方例子为 `java` 代码，同时 `github` 仓库提供 `C/C++/GO/Python` 等代码。\n\n### 3.2，算法题解\n1. [LABULADONG 的算法网站](https://github.com/labuladong/fucking-algorithm): 主要传递了刷题的算法思维。\n2. [《剑指Offer》面试题: Python实现](https://github.com/JushuangQiao/Python-Offer): 题目为《剑指Offer》书籍原题，代码实现为 `Python`，仓库简洁，阅读体验不错，无任何广告，适合刚学完数据结构与算法基础知识的同学。\n3. [力扣++-算法图解](https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/): `leetcode` 高频题图解，题解分析很多，部分题目有动画分析，提供 `Python/Java/C++` 实现，但也存在部分题解分析废话较多，不够精简的问题。\n4. [小浩算法](https://www.geekxh.com/): 一部图解算法题典，讲解 `105` 道高频面试算法题目，`go` 代码实现。\n5. [LeetCode题解](https://algorithm-essentials.soulmachine.me/): `leetcode` 高频题题解，全书代码默认使用 `C++11` 语法编写，题解为文字性描述，题解分析较短且不够通俗易懂。本书的目标读者是准备去硅谷找工作的码农，也适用于在国内找工作的码农，以及刚接触 `ACM` 算法竞赛的新手。\n6. [CodeTop 企业题库](https://codetop.cc/home): 针对企业的刷题题库，但是个人觉得好像也没必要去这样刷。\n\n## 四，机器学习\n\n### 4.1，机器学习课程\n1. 《机器学习》-周志华（西瓜书）：存在 `PDF` 电子版，内容很全，**很适合打下扎实的基础**。\n2. [《李宏毅-机器学习课程》](http://speech.ee.ntu.edu.tw/~tlkagk/talk.html): **机器学习经典视频教程**啊，非常适合初学者观看。\n3. [李宏毅机器学习笔记(LeeML-Notes)](https://github.com/datawhalechina/leeml-notes): 可以在线阅读，很方便，内容完成度高。\n4. [《南瓜书PumpkinBook》](https://github.com/datawhalechina/pumpkin-book): 南瓜书，是西瓜书的补充资料，**包含了西瓜书的公式的详细推导**，建议先看西瓜书，部分公式不会推导的情况下，可以查看南瓜书的对应内容。\n5. [机器学习数学基础](https://github.com/fengdu78/Data-Science-Notes/blob/master/0.math/1.CS229/2.CS229-Prob.pdf): 黄海广博士翻译的 `CS229` 机器学习课程的线性代数基础材料，英文好的建议看原版。\n\n![机器学习和深度学习的数学基础的思维导航](./data/images/cv_learn/math_basic_ml_dl.png)\n\n## 五，深度学习\n\n想要**快速入门**神经网络（深度学习）或者重新复习基础的同学，推荐看这个文章合集[Neural Networks From Scratch](https://victorzhou.com/series/neural-networks-from-scratch/)。文章内容由浅入深，既有公式推导，也有对应代码实现。\n\n### 5.1，深度学习课程\n1. 《深度学习》（花书），存在英文和中文 `PDF` 电子版，内容成系统，覆盖了深度学习的方方面面，强烈建议至少看完跟自己方向相关的章节，有利于打好扎实的基础。\n2. [《李宏毅-深度学习课程》](https://speech.ee.ntu.edu.tw/~hylee/ml/2021-spring.php)： 经典视频教程，**实例有趣**（皮卡丘），内容讲解由浅至深，李宏毅老师个人官网也提供了视频链接、 `PPT` 课件、代码资料。\n\n### 5.2，深度学习基础文章\n\n1. [CNN中参数解释及计算](https://flat2010.github.io/2018/06/15/%E6%89%8B%E7%AE%97CNN%E4%B8%AD%E7%9A%84%E5%8F%82%E6%95%B0/ \"CNN中参数解释及计算\")\n2. [深度学习推理时融合BN，轻松获得约5%的提速](https://mp.weixin.qq.com/s/P94ACKuoA0YapBKlrgZl3A)\n3. [动图形象理解深度学习卷积](https://mp.weixin.qq.com/s/aJ7AioWLoox7WBFPYa_nCQ)\n\n### 5.3，经典CNN分析文章\n\n1. [深度可分离卷积（Xception 与 MobileNet 的点滴）](https://www.jianshu.com/p/38dc74d12fcf?utm_source=oschina-app \"深度可分离卷积（Xception 与 MobileNet 的点滴）\")\n2. [[DL-架构-ResNet系] 002 ResNet-v2](https://zhuanlan.zhihu.com/p/29678910 \"[DL-架构-ResNet系] 002 ResNet-v2\")\n3. [ResNet及其变种的结构梳理、有效性分析与代码解读](https://zhuanlan.zhihu.com/p/54289848 \"ResNet及其变种的结构梳理、有效性分析与代码解读\")\n\n> 1，VGGNet 拥有 5 段 卷积，每一段有 2~3 个卷积层，同时每段尾部会连接一个最大池化层用来缩小图片尺寸，每段内的卷积核数量相同，越靠后的段的卷积核数量越多：64-128-256-512-512。ResNet 网络拥有 4 段卷积， 每段卷积代表一个 残差学习 `Blocks`，根据网络层数的不同， Blocks 的单元数量不同，例如 ResNet18 的 Blocks 单元数量分别为2、2、2 和 2。越靠后的段的卷积核数量越多：64-128-256-512，残差学习 `Blocks` 内的卷积核通道数是相同的。\n\n> 2，ResNet v2 创新点在于通过理论分析和实验证明恒等映射对于残差块的重要性，根据激活函数与相加操作的位置关系，我们称之前的组合方式（ResNet）为“后激活（post-activation）”，现在新的组合方式（ResNet v2）称之为“预激活（pre-activation）”。使用预激活有两个方面的优点：1)`f` 变为恒等映射，使得网络更易于优化；2)使用 `BN` 作为预激活可以加强对模型的正则化。\n\n### 5.4，PyTorch 框架学习文章\n\n1. [PyTorch中文文档](https://pytorch-cn.readthedocs.io/zh/latest/)；[PyTorch官方教程中文版](https://pytorch123.com/)；[PyTorch 官方教程](https://pytorch.org/tutorials/)。\n2. [PyTorch_tutorial_0.0.5_余霆嵩](https://github.com/TingsongYu/PyTorch_Tutorial/blob/master/Data/PyTorch_tutorial_0.0.5_%E4%BD%99%E9%9C%86%E5%B5%A9.pdf): 存在开源 `PDF` 电子版，且提供较为清晰的代码，**适合快速入门，教程目录结构清晰明了**。\n\n### 5.5，PyTorch/Caffe 框架分析文章\n1. [pytorch自定义层如何实现？超简单！](https://zhuanlan.zhihu.com/p/144904949 \"pytorch自定义层如何实现？超简单！\")\n2. [【PyTorch】torch.nn.Module 源码分析](https://zhuanlan.zhihu.com/p/88712978 \"【PyTorch】torch.nn.Module 源码分析\")\n3. [详解Pytorch中的网络构造，模型save和load，.pth权重文件解析](https://www.cnblogs.com/hansjorn/p/11467081.html \"详解Pytorch中的网络构造，模型save和load，.pth权重文件解析\")\n4. [半小时学会 PyTorch Hook](https://zhuanlan.zhihu.com/p/75054200 \"半小时学会 PyTorch Hook\")\n5. [详解Pytorch中的网络构造](https://zhuanlan.zhihu.com/p/53927068 \"详解Pytorch中的网络构造\")\n6. [深度学习与Pytorch入门实战（九）卷积神经网络&Batch Norm](https://www.cnblogs.com/douzujun/p/13352640.html \"深度学习与Pytorch入门实战（九）卷积神经网络&Batch Norm\")\n7.  [Pytorch 里 nn.AdaptiveAvgPool2d(output_size) 原理是什么?](https://www.zhihu.com/question/282046628 \"Pytorch 里 nn.AdaptiveAvgPool2d(output_size) 原理是什么?\")\n8.  [caffe源码解析-开篇](https://zhuanlan.zhihu.com/p/25127756 \"caffe源码解析-开篇\")\n9.  《Caffe官方教程中译本》：存在开源 `PDF` 电子版。\n## 六，计算机视觉\n\n### 6.1，数字图像处理教程\n\n1. 《数字图像处理第四版》：存在开源 `PDF` 电子版。成系统的介绍了数字图像的原理及应用，**内容多且全、深度也足够**，非常适合深入理解数学图像原理，可挑重点看。\n2. [桔子code-OpenCV-Python教程](http://www.juzicode.com/opencvpythontutorial/)\n\n### 6.2，计算机视觉基础课程\n1. 《CS231 课程》-李飞飞。[b 站视频教程](https://www.bilibili.com/video/BV1nJ411z7fe/)；[CS231n官方笔记授权翻译总集](https://zhuanlan.zhihu.com/p/21930884)。**课程非常经典，内容深入浅出**，每节课都有课后作业和对应学习笔记。\n2. 《动手学深度学习》-李沐，存在开源 `PDF` 电子书，官方代码为 `MXNet` 框架实现，`github` 上有开源的[《动⼿手学深度学习 PYTORCH 版》](https://github.com/ShusenTang/Dive-into-DL-PyTorch)。\n3. 《解析卷积神经网络-深度学习实践手册》-魏秀参：对 `CNN` 对基础部件做了深入描述，本书内容全且成系统，适合想深入学习 `CNN` 的同学，唯一的缺点没有项目案例以供实践。本书提供开源 `PDF` 电子版。\n### 6.3，深度学习模型和资源库\n\n1. [Papers With Code](https://paperswithcode.com/)\n2. [Jetson Zoo](https://developer.nvidia.com/embedded/community/jetson-projects#fruit_quality_control)\n3. [ModelZOO](https://modelzoo.co/)\n4. [MediaPipe 框架](https://google.github.io/mediapipe/)\n5. [Deci's Hardware Aware Model](https://deci.ai/)\n\n> 1. `Papers with code` 是由 `Meta AI Research` 团队主导的一个开放资源的社区，汇集了深度学习论文、数据集、算法代码、模型以及评估表。\n> 2. `Jetson Zoo`，是一个开源目录，其中包含在 `NVIDIA Jetson` 硬件平台上开发指南以及**参考案例**分享汇总。模型库资源里包括图像分类、目标检测、语义分割和姿势估计等方向的实践分享，提供**开源代码**和开发指南文章的链接。\n> 3. `Model Zoo` 包含了机器学习各领域的算法框架及预训练模型资源汇总，其中包括 `TensorFlow`、`PyTorch`、`Keras`、`Caffe`等框架，作者是 `Google` 的机器学习研究员的`Jing Yu Koh`构建。\n> 4. MediaPipe 是一个为直播和流媒体提供跨平台、可定制的机器学习解决方案的框架。MediaPipe 提供了包括人脸检测、人脸网格、虹膜识别、手部关键点检测、人体姿态估计、人体+人脸+手部组合整体、头发分割、目标检测、Box 跟踪、即时运动追踪、3D 目标检测等解决方案。\n> 5. `Deci` 旨在使用 `AI` 构建更好的 `AI`，使深度学习能够发挥其真正的潜力。借助该公司的**端到端深度学习加速平台**，人工智能开发人员可以为任何环境（包括**云、边缘或移动**）构建、优化和部署更快、更准确的模型。借助 `Deci` 的平台，开发人员可以在任何硬件上将深度学习模型推理性能提高 `3` 到 `15` 倍，同时仍然保持准确性。平台除了能够显示每个模型的准确性之外，还可以轻松选择目标推理硬件并查看模型的运行时性能结果，例如各种硬件的**吞吐量、延迟、模型大小和内存占用**。但是模型加速模块的 `demo` 是需要注册账户和购买的。\n\n![deci_modelzoo](./data/images/cv_learn/deci_modelzoo.png)\n\n### 6.4，目标检测网络文章\n\n1. [一文读懂Faster RCNN](https://zhuanlan.zhihu.com/p/31426458 \"一文读懂Faster RCNN\")\n2. [从编程实现角度学习Faster R-CNN（附极简实现）](https://zhuanlan.zhihu.com/p/32404424 \"从编程实现角度学习Faster R-CNN（附极简实现）\")\n3. [Mask RCNN学习笔记](https://www.cnblogs.com/wangyong/p/10614898.html \"Mask RCNN学习笔记\")\n4. [Mask RCNN 源代码解析 (1) - 整体思路](https://blog.csdn.net/hnshahao/article/details/81231211 \"Mask RCNN 源代码解析 (1) - 整体思路\")\n5. [物体检测之Focal Loss及RetinaNet](https://zhuanlan.zhihu.com/p/48958966 \"物体检测之Focal Loss及RetinaNet\")\n6. [CVPR18 Detection文章选介（下）](https://zhuanlan.zhihu.com/p/36431183 \"CVPR18 Detection文章选介（下）\")\n7. [2020首届海洋目标智能感知国际挑战赛 冠军方案分享](https://mp.weixin.qq.com/s/uUIJBxM0PATHSRxDbWbyTg)\n8. [目标检测中的样本不平衡处理方法——OHEM, Focal Loss, GHM, PISA](https://ranmaosong.github.io/2019/07/20/cv-imbalance-between-easy-and-hard-examples/ \"目标检测中的样本不平衡处理方法——OHEM, Focal Loss, GHM, PISA\")\n\n### 6.5，语义分割文章\n\n1. [2019年最新基于深度学习的语义分割技术讲解](https://mp.weixin.qq.com/s/ektiUl_H_JlUJdaba-NGsw)\n2. [U-Net 论文笔记](https://zhuanlan.zhihu.com/p/37496466 \"U-Net 论文笔记\")\n\n### 6.6，3D 视觉技术文章\n\n1. [3D成像方法 汇总（原理解析）--- 双目视觉、激光三角、结构光、ToF、光场、全息](https://www.eet-china.com/mp/a22843.html \"3D成像方法 汇总（原理解析）--- 双目视觉、激光三角、结构光、ToF、光场、全息\")\n2. [关于双目立体视觉的三大基本算法及发展现状的总结](https://bbs.cvmart.net/topics/3058 \"关于双目立体视觉的三大基本算法及发展现状的总结\")\n3. [3D视觉CV界的终极体现形式，计算机如何「看」这个三维世界](https://mp.weixin.qq.com/s?__biz=MzA3MzI4MjgzMw==&mid=2650803376&idx=3&sn=7d7cca1f447aaee307c1b8aa2e2f6e9f&chksm=84e5c8ceb39241d8b5a7f4e76f1fbc9a7d5284fcce4a6963f82fef6590baff39ab5c6bced5db&scene=132#wechat_redirect)\n\n### 6.7，深度学习的评价指标文章\n\n1. [ROC和AUC介绍以及如何计算AUC](http://alexkong.net/2013/06/introduction-to-auc-and-roc/ \"ROC和AUC介绍以及如何计算AUC\")\n2. [COCO目标检测测评指标](https://www.jianshu.com/p/d7a06a720a2b \"COCO目标检测测评指标\")\n3. [如何评测AI系统？](https://zhuanlan.zhihu.com/p/37098055)\n4. [PLASTER：一个与深度学习性能有关的框架](https://zhuanlan.zhihu.com/p/38315051)\n5. [The Correct Way to Measure Inference Time of Deep Neural Networks](https://towardsdatascience.com/the-correct-way-to-measure-inference-time-of-deep-neural-networks-304a54e5187f)\n\n## 七，模型压缩\n\n模型压缩与模型推理系统的知识，推荐看微软的[《人工智能系统》](https://microsoft.github.io/AI-System/)教程资料，写的很全且容易理解，个人觉得是个非常不错的教程。\n\n### 7.1，轻量级网络设计\n\n1. [轻量卷积神经网络的设计](https://zhuanlan.zhihu.com/p/64400678 \"轻量卷积神经网络的设计\")\n> 网络结构碎片化更多是指网络中的多路径连接，类似于 `short-cut`，`bottle neck` 等不同层特征融合，还有如 `FPN` 等结构。拖慢并行的一个很主要因素是，**运算快的模块总是要等待运算慢的模块执行完毕**。\n\n2. [ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design](https://www.cnblogs.com/seniusen/p/12047954.html \"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design\")\n3. [ShufflenetV2_高效网络的4条实用准则](https://zhuanlan.zhihu.com/p/42288448 \"ShufflenetV2_高效网络的4条实用准则\")\n4. [轻量级神经网络：ShuffleNetV2解读](https://www.jiqizhixin.com/articles/2019-06-03-14 \"轻量级神经网络：ShuffleNetV2解读\")\n\n### 7.2，模型压缩文章\n\n1. [解读模型压缩3：高效模型设计的自动机器学习流水线](https://zhuanlan.zhihu.com/p/299422728)\n2. [Deep compression: Compressing deep neural networks with pruning, trained quantization and huffman coding](https://arxiv.org/pdf/1510.00149.pdf)\n3. [韩松Deep compression论文讲解——PPT加说明文字](https://blog.csdn.net/weixin_36474809/article/details/80643784)\n4. [论文总结 - 模型剪枝 Model Pruning](https://xmfbit.github.io/2018/10/03/paper-summary-model-pruning/)\n5. [编译器与IR的思考: LLVM IR，SPIR-V到MLIR](https://mp.weixin.qq.com/s/G36IllLOTXXbc4LagbNH9Q)\n\n### 7.3，神经网络量化文章\n\n1. [神经网络量化简介](https://jackwish.net/2019/neural-network-quantization-introduction-chn.html \"神经网络量化简介\")\n2. [线性量化](https://www.yuque.com/yahei/hey-yahei/quantization.mxnet2 \"线性量化\")\n3. [Int8量化-介绍（一）](https://zhuanlan.zhihu.com/p/58182172 \"Int8量化-介绍（一）\")\n4. [Int8量化-ncnn社区Int8重构之路（三）](https://zhuanlan.zhihu.com/p/61451372 \"Int8量化-ncnn社区Int8重构之路（三）\")\n5. [ncnn源码学习（六）：模型量化原理笔记](https://blog.csdn.net/sinat_31425585/article/details/101607785 \"ncnn源码学习（六）：模型量化原理笔记\")\n6. [神经网络推理加速之模型量化](https://zh.mxnet.io/blog/model-quantization \"神经网络推理加速之模型量化\")\n7. [NNIE 量化感知训练](https://zhuanlan.zhihu.com/p/183176369 \"NNIE 量化感知训练\")\n\n> 1，量化是指用于执行计算并以低于浮点精度的位宽存储张量的技术，或者说量化就是将神经网络的浮点算法转换为定点。 量化模型对张量使用整数而不是浮点值执行部分或全部运算。\n\n> 2，量化简单来说就是将浮点存储（运算）转换为整型存储（运算）的一种模型压缩技术。\n\n> 3，虽然精心设计的 `MobileNet` 能在保持较小的体积时仍然具有与 `GoogleNet` 相当的准确度，不同大小的 `MobileNet` 本身就表明——也许一个好的模型设计可以改进准确度，但同类模型中仍然是更大的网络，更好的效果!\n\n> 4，权重值域调整是另一个机器学习过程，学习的目标是一对能在量化后更准确地运行网络的超参数 `min/max`。\n\n### 7.4，推理框架剖析文章\n\n1. [优化 TensorFlow Lite 推理运行环境内存占用](https://mp.weixin.qq.com/s/qWLgTKRRhwjLucRegHLcBA)\n2. [ncnn源码解析（五）：执行器Extractor](https://blog.csdn.net/sinat_31425585/article/details/100674365?spm=1001.2014.3001.5502)\n3. [动手编写深度学习推理框架 Planer](https://github.com/Image-Py/planer): 自制深度学习推理框架 `demo` 实战。\n## 八，高性能计算\n\n### 8.1，CPU/GPU/AI 芯片科普\n\n1. [一文读懂 GPU 的发展历程](https://juejin.cn/post/7125824952342675464 \"一文读懂 GPU 的发展历程\")\n2. [CPU、GPU、NPU等芯片架构、特点研究](https://www.cnblogs.com/liuyufei/p/13259264.html \"CPU、GPU、NPU等芯片架构、特点研究\")\n3. [什么是异构并行计算？CPU与GPU的区别是什么？](http://imgtec.eetrend.com/blog/2019/100046756.html \"什么是异构并行计算？CPU与GPU的区别是什么？\")\n4. [看懂芯片原来这么简单（二）：AI为什么聪明？什么是华为自研架构NPU？](https://mp.weixin.qq.com/s/OfWsbV6OIZ39t3944nUhWg)\n5. [【专利解密】如何提高AI资源利用率？ 华为卷积运算芯片](https://aijishu.com/a/1060000000144539 \"【专利解密】如何提高AI资源利用率？ 华为卷积运算芯片\")\n6. [嵌入式系统 内存模块设计](https://jachinshen.github.io/study/2018/03/06/%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F-%E5%86%85%E5%AD%98%E6%A8%A1%E5%9D%97%E8%AE%BE%E8%AE%A1.html)\n\n### 8.2，指令集(ISA)学习资料\n\n1. [Intel® Intrinsics Guide](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#ig_expand=611 \"Intel® Intrinsics Guide\")\n2. [Neon Intrinsics Reference](https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics \"Neon Intrinsics Reference\")\n3. [ARM Neon Intrinsics 学习指北：从入门、进阶到学个通透](https://mp.weixin.qq.com/s/E73KW7vxYikpcLYPBf4-HA)\n> `Neon` 是 `ARM` 平台的向量化计算指令集，通过一条指令完成多个数据的运算达到加速的目的，或者说 `Neon` 是 ARM 平台的 `SIMD`（Single Instruction Multiple Data，单指令多数据流）指令集实现。常用于AI、多媒体等计算密集型任务。\n### 8.3，矩阵乘优化文章\n\n1. [移动端arm cpu优化学习笔记----一步步优化盒子滤波（Box Filter）](https://zhuanlan.zhihu.com/p/64522357)\n2. [OpenBLAS gemm从零入门](https://zhuanlan.zhihu.com/p/65436463 \"OpenBLAS gemm从零入门\")\n3. [通用矩阵乘（GEMM）优化算法](https://jackwish.net/2019/gemm-optimization.html \"通用矩阵乘（GEMM）优化算法\")\n4. [卷积神经网络中的Winograd快速卷积算法](https://www.cnblogs.com/shine-lee/p/10906535.html \"卷积神经网络中的Winograd快速卷积算法\")\n5. [知乎专栏-深入浅出GPU优化](https://www.zhihu.com/column/c_1437330196193640448 \"知乎专栏-深入浅出GPU优化\")\n6. [CUDA GEMM 理论性能分析与 kernel 优化](https://zhuanlan.zhihu.com/p/441146275 \"CUDA GEMM 理论性能分析与 kernel 优化\")\n7. [OpenPPL 中的卷积优化技巧](https://zhuanlan.zhihu.com/p/450310581 \"OpenPPL 中的卷积优化技巧\")：概述总结类文章，无代码，非专注时刻也能阅读。\n8. [【张先轶】BLISlab学习优化矩阵乘。第一课](https://www.bilibili.com/video/BV1c94y117Uw/?vd_source=4ab49101e323bf3a37e7b81812524444 \"【张先轶】BLISlab学习优化矩阵乘。第一课\")\n9. [矩阵乘法与 SIMD](https://jcf94.com/2021/08/28/2021-08-28-simd/ \"矩阵乘法与 SIMD\")\n> `Winograd` 是一种快速卷积算法，适用于小卷积核，可以减少浮点乘法的次数。\n## 九，模型部署(算法SDK开发)\n\n### 9.1，模型部署文章\n\n1. [海思AI芯片(Hi3519A/3559A)方案学习（二十五）初识 mapper_quant 和mapper_param](https://blog.csdn.net/avideointerfaces/article/details/103070021)\n2. [部署PyTorch模型到终端](https://zhuanlan.zhihu.com/p/54665674)\n3. [多场景适配，TNN如何优化模型部署的存储与计算](https://www.infoq.cn/article/4rfxdkkvvyb6auxhjxi4)\n4. [模型转换、模型压缩、模型加速工具汇总](https://blog.csdn.net/WZZ18191171661/article/details/99700992)\n5. [深度学习模型转换与部署那些事(含ONNX格式详细分析)](https://bindog.github.io/blog/2020/03/13/deep-learning-model-convert-and-depoly/)\n6. [ONNX初探](https://mp.weixin.qq.com/s/H1tDcmrg0vTcSw9PgpgIIQ)\n\n## 十，数据分析\n\n1. [python-numpy-pandas-matplotlib-Cheatsheets](https://github.com/sentientmachine/Cheatsheets#python_pandas_intermediate): 编程备忘录，图片很好看，适合有一定基础的编程者总结回顾常用操作。\n2. [Distill](https://distill.pub/about/): 机器学习研究 应该清晰、动态和生动。\n### pandas\n\n1. [10 minutes to pandas.](https://pandas.pydata.org/docs/dev/user_guide/index.html): 官方User Guide，10 分钟快速入门 pandas。\n2. 《利用Python进行数据分析 第二版 A5》：国外很经典的python数据分析教程，内容比较通俗易懂，网上有 PDF 电子版。\n3. [《Joyful Pandas》](https://github.com/datawhalechina/joyful-pandas)：国内教程，github 开源资料。\n\n### matplotlib\n\n1. [Python绘图库Matplotlib入门教程](https://paul.pub/matplotlib-basics/): 中文版Matplotlib入门教程，文章较为容易理解，但深度和细节不够无法建立Matplotlib绘图的系统思维 ，适合快速阅览。\n2. [matplotlib-tutorial](https://github.com/rougier/matplotlib-tutorial): 初学者的 Matplotlib 教程，github stars 数高达 2.6k+。\n3. [Matplotlib中文教程](https://github.com/datawhalechina/fantastic-matplotlib): Datawhale 数据可视化小组的一个开源项目，系统梳理了 matplotlib 的绘图接口，github stars 数高达 377。\n> 还没来得及细看，后续看完更新阅读感受。\n\n## 效率工具\n\n### 外面的世界\n\n1. [还算不错的使用谷歌搜索工具](https://freegecko.com/): 非常稳定和快速的 `IPLC` 工具（\"International Private Leased Circuit\" 的缩写，即“国际私用出租线路”）。我已经稳定使用 2 年多了，可以考虑使用我的邀请码注册（`VDHtU3A7`），点击[链接](https://freegecko.com/index.php#/register?code=VDHtU3A7)直接注册。 \n> 想要技术学的更好，会使用 `google` 搜索、`github`、`stack overflow` 等网站是基础技能。\n\n### 快捷导航\n\n1. [Quick Reference](https://quickref.me/): 一些由开源天使贡献的代码备忘录和快速参考。\n2. [Qwerty Learner](https://qwerty.kaiyi.cool/): 适合用键盘学习单词的网站\n3. [GitHub Readme Stats](https://github.com/anuraghazra/github-readme-stats): 在你的 README 中获取动态生成的 GitHub 统计信息！\n\n### markdown/latex 写作\n\n1. [markdown语法大全](https://www.cnblogs.com/miki-peng/articles/12502985.html): 这篇文章对 `markdown` 语法整理得很好，文章排版也做的好，读完很容易就掌握 `markdown` 语法。\n2. [通用 LaTeX 数学公式语法手册](http://www.uinio.com/Math/LaTex/): 文章排版很好，目录结构清晰明了，阅读起来很舒服，推荐用来学习 `latex` 语法内容。\n3. https://latex.codecogs.com/eqneditor/editor.php: 在线 `latex` 语法。\n\n## 业余生活\n\n1. [小霸王游戏网页版-其乐无穷](https://www.yikm.net/)\n2. [低端影视](https://ddys.tv/category/movie/)\n3. [AGE 动漫](https://www.agemys.net/rank)\n## 博客阅读后的知识点总结\n\n1，为了尽可能地提高 `MAC阵列` 的利用率以及卷积运算效率，阵列控制模块会根据第一卷积参数矩阵的行数和第一卷积数据阵列的行数来确定第一乘法累加窗口的列数。\n\n2，`SNPE` 开发流程：\n\n![SNPE开发流程.png](./data/images/cv_learn/SNPE开发流程.png)\n\n3，目标检测模型效果提升方法：\n\n+ 以 `Cascade RCNN` 作为 `baseline`，以 `Res2Net101` 作为 `Backbone`；\n+ `Albumentation` 库做数据集增强-用在模型训练中；\n+ 多尺度训练(`MST` Multi-scale training/testing)的升级版-`SNIP`方法(Scale Normalization for Image Pyramids)，用在 `baseline` 模型训练和测试中：解决模板大小尺度不一的问题；\n+ `DCN` 可变性卷积网络-用在 `baseline` 模型的 `backone` 中；\n+ `soft-NMS`：解决目标互相重叠的问题；\n+ `HTC` 模型预训练， `Adam` 优化算法可以较好的适应陌生数据集，学习率热身(`warm-up`)来稳定训练过程。\n\n4，`SNIP` 论文解读：\n\n`SNIP` 非常 `solid` 地证明了就算是数据相对充足的情况下，`CNN` 仍然很难使用所有 `scale` 的物体。个人猜测是由于 CNN 中没有对于 scale invariant 的结构，CNN 能检测不同 scale 的“假象”，更多是通过CNN 来通过 `capacity` 来强行 `memorize` 不同 `scale` 的物体来达到的，这其实浪费了大量的 `capacity`，而 `SNIP` 这样只学习同样的 scale 可以保障有限的 capacity 用于学习语义信息。论文的关键贡献：发现现在的 CNN 网络无法很好的解决 scale invariance 的问题，提出了一个治标不治本的方法。\n\n5，高效模型设计（模型压缩）方法：\n\n一般而言，**高效模型**的设计有 6 大基本思路：1）轻量级架构、2）模型裁剪、3）`AutoML` 和 `NAS` 模型搜索、4）低精度量化、5）知识蒸馏、6）高效实现。\n> 来源旷世学术分享-[张祥雨：高效轻量级深度模型的研究和实践](https://aijishu.com/a/1060000000090519)。\n\n6，网络深度与宽度的理解及意义\n\n> 更多理解参考知乎[网络宽度对深度学习模型性能有什么影响？](https://www.zhihu.com/question/322219788 \"网络宽度对深度学习模型性能有什么影响？\")\n\n在一定的程度上，网络越深越宽，性能越好。宽度，即通道(`channel`)的数量，网络深度，及 `layer` 的层数，如 `resnet18` 有 `18` 层网络。注意我们这里说的和宽度学习一类的模型没有关系，而是特指深度卷积神经网络的(通道)宽度。\n\n+ `网络深度的意义`：CNN 的网络层能够对输入图像数据进行逐层抽象，比如第一层学习到了图像边缘特征，第二层学习到了简单形状特征，第三层学习到了目标形状的特征，网络深度增加也提高了模型的抽象能力。\n+ `网络宽度的意义`：网络的宽度（通道数）代表了滤波器（`3` 维）的数量，滤波器越多，对目标特征的提取能力越强，即让每一层网络学习到更加丰富的特征，比如不同方向、不同频率的纹理特征等。\n\n7，所有 `Inception` 模型都具有一个重要的性质——都是遵循 拆分-变换-合并（`split-transform-merge`） 的设计策略。\n\n8，对于某种指令，延迟 `latency` 主要关注单条该指令的最小执行时间，吞吐量 `throughout` 主要关注单位时间内系统（一个CPU核）最多执行多少条该指令。因为 AI 计算的数据量比较大，所以更关注吞吐量。\n\n9，`CPU` 高性能通用优化方法包括：\n+ 编译选项优化\n+ 内存性能和耗电优化：内存复用原则，小块快跑是内存设计的重要原则。\n+ 循环展开：循环的每次迭代都有一定的性能损失（分支指令）。但是现代 ARM 处理器具有分支预测的能力，它可以在执行条件之前预测是否将进入分支，从而降低性能损耗，这种情况下全部循环展开的的优势就减弱了。\n+ 并行优化和流水线重排：并行优化分为多线程核与核之间数据处理，以及单核心内部并行处理。从本质上讲，流水线重排也是一种并行优化。\n\n10，卷积性能优化方式：卷积的计算方式有很多种，通用矩阵运算（`GEMM`）方式有良好的通用性，但是仅使用 GEMM 无法实现性能最优。除 GEMM 外，常用的优化方法还包括滑窗（`Sliding Window`）、快速傅里叶变换（Fast Fourier Transform, `FFT`）、`Winograd` 等。不同的方法适合不同的输入输出场景，最佳的办法就是对算子加入逻辑判断，将不同大小的输入分别导向不同的计算方法，以最合适的方法进行卷积计算。\n+ 大多数情况下，使用滑窗方法的计算性能还是无法和 `GEMM` 方法比较，但是一般当输入小于 $32\\times 32$ 时，可以考虑采用滑窗的优化方式。\n+ `Winograd` 是存在已久的性能优化算法，在大多数场景中，`Winograd` 算法都显示了较大的优势，其用更多的加法运算代替部分乘法运算，因为乘法运算耗时远高于加法运算。`Winograd` 适用于乘法计算消耗的时钟周期数大于加法运算消耗的时钟周期数的场景，且常用于 $3\\times 3$ 卷积计算中。对于 `CPU`，一般来说，一次乘法计算消耗的时间是一次加法计算消耗时间的 `6` 倍。\n+ `FFT` 方法不适合卷积核较小的 `CNN` 模型。\n\n11，下图展示了如何在英伟达 GPU 架构发展史以及单块 GPU 上纵向扩展以满足深度学习的需求（截止2020年）。\n\n![英伟达 GPU 架构发展史](./data/images/cv_learn/英伟达架构发展史截止到2020年.png)\n\n12，[Deep compression](https://arxiv.org/pdf/1510.00149.pdf) 论文阅读总结\n- `deep compression` 是解决存储问题，对于速度问题几乎没获得改善；\n- 权值剪枝还得看另外一篇论文：learning both weights and connection for efficient neural network\n- CNN 模型的存储空间问题，主要还是在全连接层，若要改善 `inference` 速度，需要在卷积层下功夫。\n\n13，`Deep Compression` 论文介绍的神经网络压缩方法，可分为三步：\n\n- **剪枝**：舍弃权重绝对值较小的权重，并将剩余权重以稀疏矩阵表示。\n- **量化**：将剪枝结果进行进一步量化，具体的是构建一组权值码本，使模型中的权值共享码本中的其中一个权重值，以减少每个权重保存所需的比特数。\n- **霍夫曼编码**（可选）：通过霍夫曼编码，进一步地压缩索引值以及权重数值地存储空间。\n\n## 参考资料\n\n1. [DeepLearning Tutorial](https://github.com/Mikoto10032/DeepLearning)\n"
  },
  {
    "path": "data/code/pytorch_note.py",
    "content": "####################卷积神经网络计算量/参数量分析工具#####################\nimport torchvision, torch\n\nmodel = torchvision.models.resnet50()\n\n# 1, pytorch 自带输出\nprint(model)\n\n# 2, torchinfo 工具\nfrom torchinfo import summary\nsummary(model, (1, 3, 224, 224), depth=3) # resnet50: 25.557M 4.09G\n\n# 3, thop 工具\nfrom thop import profile, clever_format\ninput = torch.randn(1, 3, 224, 224)\nmacs, params = profile(model, inputs=(input, ))\nmacs, params = clever_format([macs, params], \"%.3f\")\nprint(\"The resnet50 model info: \", macs, params) # resnet50: 4.134G 25.557M"
  },
  {
    "path": "data/images/README.md",
    "content": "## 目录说明\n此目录存放深度学习面试题.md文件所用到的图片文件。\n"
  },
  {
    "path": "data/images/dl/courgette.log",
    "content": ""
  },
  {
    "path": "data/intern_info.md",
    "content": "## 春招算法实习岗位表\n\n|公司|招聘岗位|工作地点|截止时间|投递方式|内推方式|是否投递|信息来源|\n|---|-------|-------|------|-------|-------|------|-------|\n|美图影像实验室MTlab|深度学习/计算机视觉实习生|北京五道口|长期|内推邮箱：lsr@meitu.com|邮件主题：CVer推荐+姓名+电话号码+实习生|无|ai.meitu.com|\n|文行知远|感知算法实习生|北京/广州|未知|内推邮箱：hzshuai@gmail.com|简历命名格式：CVer推荐+姓名+电话号码+实习生|无|CVer公众号|\n|商汤科技|深度学习/计算机视觉见习研究员等|北京/上海/深圳/杭州|未知|内推邮箱：wwlwenli@163.com|邮件主题：CVer推荐+商汤2019春招内推、商汤2020实习内推，简历附件命名：姓名+岗位名称+到岗时间+可见习时长|无|CVer公众号|\n|阿里达摩院|计算机视觉算法实习生|北京/杭州/深圳|未知|内推邮箱：zhoujing.zjh@alibaba-inc.com|简历命名格式：CVer推荐+姓名+电话号码+方向(工程/算法)|无|CVer公众号|\n|京东AI Lab|计算机视觉算法实习生|北京朝阳区|未知|内推邮箱：wangxiaobo8@jd.com|简历命名格式：CVer推荐+姓名+电话号码|无|CVer公众号|\n|字节跳动|AI 算法岗|北京|2.25-5.31|投递[官网](https://job.bytedance.com/campus/position)|内推码：WWNUPDJ、NAATGNC，邮箱咨询简历进度：wangyang.wyang@bytedance.com|无|CVer公众号|\n|海康威视|AI算法岗|杭州/其他|未知|投递[官网](https://campushr.hikvision.com/)|内推码：P4Y8AQ、587TA3、587LAC、5SYMAJ|无|CVer公众号|\n|图森未来|感知算法实习生|北京朝阳区|未知|内推邮箱：ostnie@foxmail.com|简历命名：CVer推荐+姓名+电话号码+应聘岗位|无|CVer公众号|\n|平安科技|计算机视觉实习生|北京/深圳|3.19|[网申](http://campus.pingan.com/tech/position)|内推码：PvqgIB|无|牛客讨论区|\n|抖音火山|计算机视觉算法实习生|北京/其他|未知|[网申](https://job.bytedance.com/campus/position)|内推码：QURKAW2|无|牛客讨论区|\n|招银网络科技|算法实习生|深圳/杭州/成都/|6.30|[网申](https://mp.weixin.qq.com/s/7l6GZ88yynwb3jwRhx76KA)|内推码：9171XYCD、9211LYCD|无|牛客讨论区|\n|奇虎360|算法/安全/研发实习生|北京/苏州/其他|3.31|[网申](http://chrcmp.chinahr.com/views/2019-qihu360/index.html)|内推码：暂无|无|牛客讨论区|\n|爱奇艺|C++开发工程师|北京|4月中旬|[网申](https://zhaopin.iqiyi.com/job-school.html)|内推码：zNG4KR|无|信息学院qq群|\n|蚂蚁金服|算法实习生|杭州/其他|3-5月|邮箱：xiaoxue.sxx@antfin.com|邮件标题：【姓名】+【学校】+【应聘岗位】|无|信息学院qq群|\n|华为|算法实习生|成都/其他|3月28|[网申](http://career.huawei.com/reccampportal/campus4_index.html#campus4/pages/joblist/jobList.html?jobFamClsCode=JFC1&type=0)|无|无|信息学院qq群|\n|菜鸟网络|算法实习生|杭州/北京|未知|qq：1193304376|1193304376@qq.com|无|无|信息学院qq群|\n|oppo手机|算法实习生|北京|未知|[网申](https://oppo.zhaopin.com/jobs.html)|内推码：15209235173|无|信息学院qq群|\n|创新工厂人工智能工程院|机器人/自动化实习生|北京|未知|投递邮箱：hr@chuangxin.com|投递格式：姓名+岗位(实习生)+可到岗日期|无|我爱计算机视觉公众号|\n|旷世科技detection组|算法实习生|北京|未知|投递邮箱： zhangzhiqiang@megvii.com|暂无内推|无|我爱计算机视觉公众号|\n|锐明技术|计算机视觉算法实习生|深圳/重庆|未知|[网申](http://streamax.zhiye.com/Campus)|暂无内推|无|我爱计算机视觉公众号|\n|作业帮|计算机视觉方向实习生|北京|未知|投递邮箱：wujunbin@zuoyebang.com|暂无内推|无|我爱计算机视觉公众号|\n|爱莫科技|算法实习生|深圳|未知|投递邮箱：hr@mall-ai.com|暂无内推|无|我爱计算机视觉|\n|阿里巴巴业务平台事业部|算法-机器学习实习生|杭州|3月12-4月23|[网申](https://campus.alibaba.com/index.htm)|暂无内推|无|知乎|\n|拼多多|算法实习生|上海/其他|3月15-4月30|[网申](https://www.pinduoduo.com/home/campus/)|暂无内推|无|朋友推荐|\n|小马智行|计算机视觉实习生|北京市海淀区|未知|内推邮箱：zhijie@pony.ai|简历命名：姓名+岗位+CVer推荐+实习开始时间+可实习月数|无|CVer公众号|\n|上海媒智科技|计算机视觉算法实习生|上海|未知|内推邮箱：chenxi.huang@media-smart.cn|简历格式：姓名+电话号码+全职/实习+CVer推荐|无|CVer公众号|\n|旷视科技|算法研究员实习生|北京市海淀区|长期有效|内推邮箱：ur@megvii.com|简历命名：姓名+电话+岗位+CVer推荐(**免笔试**)|无|CVer公众号|\n|淘宝-智能制造事业部|图形图像算法实习生|杭州|未知|内推邮箱：yuanliang.syl@alibaba-inc.com|简历命名格式：姓名+电话号码+应聘岗位+CVer推荐|无|CVer公众号|\n|360安全集团|机器学习算法实习生|北京|未知|[网申](http://chrcmp.chinahr.com/pages/360safe/jobs)|内推码:nAJvUTY2(免简历筛选)|无|qq群|\n|图森未来|感知算法实习生等|北京|未知|内推邮箱:yifyang29@gmail.com|简历命名格式：姓名+电话号码+应聘岗位|无|我爱计算机视觉公众号|\n|最右|视频图像算法工程师实习|北京|未知|内推邮箱:sunhaiyong2014@xiaochuankeji.cn|简历命名格式：CVer推荐+姓名+电话+社招/校招/实习|CVer公众号|\n|爱奇艺智能平台部-视频分析组|视频理解算法实习生|北京中关村|未知|内推邮箱:zhangyuntao@qiyi.com|简历命名格式：CVer推荐+学校+研究方向+实习开始时间+毕业时间|CVer公众号|\n|依图科技|校招、实习、社招|上海、北京、杭州|长期|![依图科技-校招内推](./data/images/依图科技-校招内推.png)|微信扫描二维码（**长期有效**），链接中的岗位均可投递||"
  },
  {
    "path": "interview_summary/1-计算机视觉岗2019届实习面经.md",
    "content": "## 阿里算法工程师(计算机视觉方向)\n### 一面(1个小时10分钟)--->简历面\n1. 自我介绍，差不多 `10` 分钟。\n2. 简历项目和比赛介绍，中间有问一些项目和比赛细节，问了一些延伸和开放性问题：\n    + `Adam` 和 `SGD` 优化器哪个更好，好在哪里，哪个使模型更加容易发散?\n    + `FPN` 作用\n    + 讲下 `yolov3` 的架构，和 `two-stage` 的 `mask-rcnn` 有什么区别\n3. 代码测试，求 `n` 个数里面前k个最大的数。\n我最开始说用快排，面试说还有其他方法吗，我一紧张说了个时间复杂度更大的方法，面试官提醒我可以考虑树排序，但是我没学过，回答不上来，最后面试官说你本科没学过数据结构，那就先算了。\n4. 问了几个机器学习算法，`KNN` 和 `SVM` 的细节。\n这里答的不好，太久没用传统机器学习算法，很多东西都忘了，中间一个简单的几何中常见距离计算方式(欧式距离)，我忘了居然答余弦距离。\n5. 问了我有什么想问的。\n#### 一面总结：\n面试官人比较友好，自己项目细节一定要熟悉，简历上的东西最好清楚掌握，数据结构和常用算法一定要掌握，这是我的第一个面试经历，不管接下来的面试能否通过，都还是值得纪念和**自省**的。\n## 格灵深瞳算法实习生\n### 一面（29分钟）-->基础面/项目面/终面(4月28日晚更新，已挂)\n1. 自我介绍，差不多３分钟\n    + 自我介绍要简介些，我这里自我介绍有点太详细了\n2. 钢筋检测项目介绍和目标检测框架细节\n    + 大致介绍自己的工作和项目细节\n    + 问了 `faster rcnn`、`Mask rcnn` 的**细节**，`faster rcnn` 的 `rpn` 结构介绍下，`rpn` 的 `loss` 是什么，`masker rcnn` 和 `faster rcnn` 有什么区别和改进\n    + `retinanet` 的结构和创新点，讲一下 `ssd` 和 `retinanet` 的区别\n3. 鲸鱼识别项目介绍和图像分类网络细节介绍\n    + 大致介绍下鲸鱼识别项目\n    + `resnet` 网络的创新，为什么能解决梯度消失问题，残差模块详细介绍下，为什么能解决网络层数加深带来的梯度消失和网络退化问题。\n4. 你有什么想问的\n    + 问了去了之后我能做什么\n    + 什么时候能出面试结果\n    + 面试官给我提出建议：加强论文阅读和基础原理细节掌握、加强原理的表述和表达能力\n ### 面试总结\n１．格林深瞳实习生面试只有一面，所以项目和基础都在这一面都问了。这次面试官问的很多问题，给了我很多启发，自己项目虽然做的多，但是在很多理论和基础原理上细节功夫下的不够，论文看的不够多。\n\n２．其实自己也知道，自己在基础理论和原理方面掌握得不够深，但是由于缺乏时间，我还是没做到自己的目标，希望借这次面试反映出的自己理论缺失点，来提醒和激励自己一定要把基础理论和原理彻底掌握。\n\n３．经过阿里的面试，自己回去把更多的项目细节掌握了，这次格林深瞳面试之后一定要把基础理论和原理掌握，从图像分类网络: **resnet**等，到目标检测和图像分割网络：**faster rcnn**、**mask rcnn**、**ssd**、**yolov3**等彻底掌握基础原理和细节，多看相关论文和博客。\n## 南京地平线机器人　智能驾驶算法实习生\n## 笔试(１个小时)--> ５道编程题\n1. 给定两个字符串 `a` 和 `b`（长度超过100w），在字符类型上 `b` 是 `a` 的子集，求 `b` 在 `a` 字符类型上的补集;\n2. 给定正整数N, 返回小于等于N且至少有一位重复数字的正整数的个数;\n3. 电话号码组合。下图是一个手机按键的样例，每一个数字包含一些字母。比如字母“A”可以通过按一次“2”得到，字母“B”可以通过按两次“2”得到，以此类推。当给定一个数字字符串，我们也可以得到相应的映射，比如“22”, 代表字母组成的可能性是[“AA”, “B”]。要求：输入为一个数字字符串，例如”2321241499844211”。输出为可能代表的所有字母组合。\n4. 给定两矩形的左上角和右下角坐标，求两矩形的重叠区域面积（`overlap`），若不重叠，返回0。（其实就是计算IOU）。\n5. 实现 `softmax`，包括 `init`，`forward`，`backward`。\n\n**如果把笔试题写出来侵权，一定要联系我删除笔试题哈**。\n### 一面（48分钟）\n1. 自我介绍\n    + 面地平线的这次自我介绍，比之前的面试算是有了一些改变，不再流水线式的介绍学习经历和项目经历，而是突出性格和技术栈重点。\n2. 图像基础操作题,对图像做45度旋转，如何使图像完整不缺失，缺失和超出的部分如何处理？\n3. 项目细节\n    + 离线过采样和在线过采样哪个更快？\n    + 如何针对已有的网络做改进，提升速度？\n    + 如何解决类别不平衡问题？\n    + 训练网络的指标，除了基本的的 `acc`, `loss`，**roc、auc**有了解吗？\n 4. 算法细节\n    + `ROI Pooling` 和 `ROI Align` 的区别及演进\n    + 离线图像增强与在线图像增强有什么区别\n 5. `Python` 和计算机常考基础\n    + 装饰器怎么用\n    + 深拷贝和浅拷贝的区别\n    + 多线程和多进程的区别\n 6. `Linux` 和 `git` 命令操作基础\n    + linux查找、查看文件的3个常用命令：which、find、wheresis。（这里应该是查找命令，当时也没听清楚，连就说了cat查看文件、which、find）\n    + 统计文件夹下的文件个数:ls -l | grep \"^_\" | wc -l（这个操作，我之前用过很多次，但是没说的很清楚，不过意思应该表达清楚了）\n 7. `git` 的基本操作: 如何回退版本。\n ### 一面总结\n + `Python`一些基础还是要搞清楚，向迭代器、深拷贝、浅拷贝，我之前都看过面经和用法，都还是忘了，真是不应该。\n + 地平线机器人面试真的问的很广，偏工程向，碰到不会的也不要太紧张，之后一定要去补课。\n + 自己要加强 `Python` 基础的一些**技术盲点**。\n + 以后面试表达要有针对性，可以引导面试官往自己熟悉的方向，但不要拓展太多。\n \n ### 二面（70分钟）\n1. 项目介绍\n    + **项目细节，和由项目延伸的原理问题**\n    + 细粒度图像分类了解吗\n2. 目标检测框架原理问题\n    + `RPN` 结构讲下，`RPN` 的 `loss` 有哪些，分类 `loss` 是二分类还是多分类\n    + `ROI Pooling` 是在 `RPN` 前面还是后面，讲下原理，有什么作用\n    + `ROI Polling` 和 `ROI Align`的区别\n    + **Mask RCNN基本结构**讲下\n    + `1*1` 卷积作用（降维－改变特征通道数，加入非线性）\n    + `Faster RCNN` 的 `loss` 有哪些，分别讲下\n3. `CNN` 的 `SOTA` 模型原理\n    + **ResNet结构讲下，它解决了什么问题**\n    + `InceptionV3` 结构讲下\n4. `C/C++/Python` 基础\n    + `Python` 装饰器解释下，基本要求是什么（参数为函数，返回为函数，本质是嵌套函数）\n    + `C` 的结构体和 `C++` 类的区别(`C` 结构体不能定义函数)\n    + `__init__` 函数作用\n    + `Python` 怎么继承父类的 `__init__` 函数（`super` 操作）\n    + **面向对象编程和面向过程编程区别**\n5. `Linux` 系统基础操作\n    + 一些基本命令\n    + **管道命令**解释下\n    + 统计文件夹下的文件个数:ls -l | grep \"^_\" | wc -l\n6. `git` 相关操作\n    + git 熟不熟悉，平常怎么用\n    + 除了commit、pull等基本命令，还用过哪些\n 7. 嵌入式 `Linux` 系统\n    + tensorflow 安装是源码安装还是 pip/conda 安装，交叉编译用过吗\n    + cmake 语法了解吗\n 8. 有什么想问我的\n    + 对我此次面试评价如何，我有什么需要改进和学习的地方（在学习一定要加强系统学习专业基础，在公司很难有完整时间系统学习知识）\n    + 部门主要是做什么的，我去了之后做什么方向\n### 二面总结\n\n此次面试时间比较长，总的来说，这次面试自己还是有点进展，面试一定要保持**心态放松和良好，表达要流利、清楚**，针对面试官指定的问题，尽量不要拓展太多（超出问题本身），技术上一定要**系统学习自己的研究方向**。\n## 小鹏汽车－图像算法实习生\n### 一面（30分钟）\n1. 自我介绍\n2. 项目介绍\n    + 项目细节询问\n    + 数据增强用了哪些，为什么用\n3. 拓展问题\n    + 图像分割结果，如果边缘信息本来是直线的，但是分割出来效果线确是弯的，怎么解决（有点记不清了）\n4. 你有什么想问我的没\n### 二面（27分钟）\n1. 项目介绍\n2. 你平时看过哪些论文，最新的论文看过哪些\n3. 平时几点钟回去, 代码量多少，平时用C、C++还是Python编程.\n4. 你有什么想问的\n### 面试总结\n二面的面试官提到我最新的论文看的不多，其实**最新的论文是一定要看的**，紧跟行业发展，了解技术的最新发展动向，而且也有助于拓展自己的思维，学习下别人的idea。\n## 合心科技算法实习生（一家不尊重面试者、面试体验极其糟糕的公司）(不到10分钟)\n### 一面（不到10分钟）\n1. 基本介绍（不确定他有在听吗）\n2. 项目介绍（这个过程，面试语气度非常不友好，我也不确定他有在听我讲项目没，反正我讲完了，他也没问我什么问题，评价了下我做的东西太简单、太 `low` 了（大意是这个），**说我检测的项目就是套框架、没有自己实现框架**，目标检测的项目虽然是用了 `mask rcnn` 的框架，但是我自己也做了很多其他的工作，比如测试的程序、数据过采样、数据标签生成、训练策略调整等是自己写的。这个过程体验真的极其糟糕，我深深地感受到了面试官不尊重人、看不起人的语气和态度）\n3. 你有什么想问我的吗（到这里面试官有些不耐烦了，估计就是照着流程问下，我问了这个岗位主要做什么方向的产品，被直接怼，你都不看招聘要求吗，我说招聘信息没写清楚，面试官不耐烦的讲了下是做教育产品，面试结束）\n\n### 面试总结\n1. 我承认自己技术水平不够，还需要不断学习，但是这不能成为这家公司面试官不尊重、看不起起我的理由，既然我通过了你们的简历筛选，就说明我的简历和技术水平得到了你们的部分承认，但是面试过程，不仅是在浪费双方时间，我更直接地感受到了“合心科技”这家公司深深的恶意和不尊重人。\n2. 说实话，我**实习面了有９家公司了**，阿里、地平线机器人、格灵深瞳、小鹏汽车都面过，面试官都是很友好的，但是这家\"合心科技\"公司的面试官真的态度超级糟糕，全程一副不尊重人、看不起人的态度，面试迟到、全程一副高高在上的态度、那种看不起人、不尊重面试者的语气，对不起，我真的实在是受不了。\n3. 最后，对于合心科技，这家创业公司，公司规模（50-150）人，我在这里写出面试过程，**希望以后找实习的同学尽量避免这家公司（合心科技）的坑，不要让糟糕的面试体验影响了大家找工作的心情和态度**。\n4. 写下这个总结的过程，我是尽量平复了自己的内心，尽量希望自己糟糕的心情不要影响了我的文字表达能力。这个面经我也保持了客观的态度，以上内容没有任何虚假。\n\n"
  },
  {
    "path": "interview_summary/2-计算机视觉岗2019届暑期实习应聘总结.md",
    "content": "- [找实习感想](#找实习感想)\n- [找实习建议](#找实习建议)\n- [面试过程建议](#面试过程建议)\n- [计算机视觉岗找实习心得](#计算机视觉岗找实习心得)\n- [计算机视觉面试问题分类总结](#计算机视觉面试问题分类总结)\n- [已面试的公司和进展](#已面试的公司和进展)\n- [实习面经](#实习面经)\n\n## 找实习感想\n\n1. 从4月１日开始找实习，到5月13日收到南京地平线机器人口头offer和北京小鹏汽车技术面通过通知，这40来天的时间真的过的不容易，每天早上醒来都被“**找实习**”这三个字压得沉甸甸的，一有空闲时间就看牛客网面经和投简历，前前后后差不多有 `60` 家公司，大公司、小公司、独角兽公司，只要招深度学习/计算机视觉/图像算法相关岗位，我都投，投递的渠道最开始是官网投递，后来发现太慢了，招银、平安科技这两家公司填写的简历信息太多太耗时了，后来就转 `boss` 直聘、拉勾网、牛客网和内推邮箱渠道，这样就省事很多，**一个平台填好简历，可以投递不同公司**。\n2. 这段时间自己也学到了很多东西，弥补了些自身技术盲点和加深了对一些基本原理和理论的理解，更重要的是认识到了**与大佬之间的差距**，对于搞技术的人来说，一定要保持终身学习的态度和激情，也要保持心态良好和身体健康。\n3. 找算法实习岗对大多数人来说是心智和体力的持久战，据我所知，大多数人都花了 `1` 个月以上的时间，所以要做好长期的打算，如果**一时失利也别灰心**，出去放松一天，放松下自己的心情，**保持良好的精神和身体状况很重要**。\n4. 实习平台很重要，**一定要尽量投大公司、知名企业或者行业独角兽**，不仅是面试正规和面试官温和亲切的态度，更重要的是自己本身面试之后经过反省总结，也能学到很多东西，小公司的面试大多都不是很正规。好的面试官，不管你技术实力是否满足他们要求，面试官一般都会很温和，让你放轻松，给你足够的尊重，有些面试官甚至会给你提些很有帮助和恳切的建议。这里，给我留下良好印象的是阿里和地平线机器人的面试官，面试官人真的超级友好和亲切。\n\n## 找实习建议\n\n1. **心态第一，坚持为胜，找实习是一个持久战**。对于技术实力一般的人来说，真的要保持良好的心态，不抛弃、不放弃，这中间也许你会经历很多失败，但是真的只要坚持下来，我不保证你能拿到很好公司的offer，但是你自己本省一定能从面试中学到很多东西，尤其是很多大公司的面试官会给你一些很恳切的建议，可以让你受益匪浅。\n2. **实习要趁早**。建议有条件的 `研一` 或者 `大三` 就去实习，对于暑期实习来说，简历投递一定要趁早，最好 `2` 月份就开始，我很晚才投简历，很可能就会错过内推时间和岗位 `hc` 没有的情况，我 `4` 月份开始投简历已经算是很晚了，所以也直接导致我投了海康威视之后，一直是简历复选中，很有可能就是 `hc` 已经没了。\n3. **数据结构与算法题必须刷**。虽然这是老生常谈的建议，但是我们必须记住很多大公司一定会有笔试题，就算是内推免笔试的，面试过程中也很有可能会出数据结构与算法题。这里我建议去 **leetcode** 或者牛客网上刷**剑指offer**，一般把简单和中等难度题刷会就可以，笔试题一般都不会很难，除非是谷歌、`MSRA` 那种公司可能会对笔试题有更高的要求。刷题的话，第一次刷不会可以去看参考解题思路和答案，**看完后要自己写出代码**。\n4. **常见面试题要掌握**。其实关于计算机基础和计算机视觉算法原理的面试题，可以提前准备下，有些题频繁的问到，可以提前准备下，比如：`1*1` 卷积作用，链式求导公式，多线程与多进程区别，`tcp/udp` 通信原理等。这些常见面试题，都可以在牛客网找的到，当然**不同岗位面试题不一样，甚至每个人的面试问题都有很大不一样**，不要因为别人面经上的题不会，你就有些失去信心，但是**对于同一个岗位的频繁出现的面试问题还是必须掌握**。机器学习、算法工程师面试考点汇总，参考[这里](https://www.nowcoder.com/discuss/165930)。\n5. **简历要有亮点**。**paper、算法比赛、项目、实习必须要有一个啊，博客、`github` 最好也要有，这真的很给简历加分**！实验室没有发 `paper` 和项目条件的，可以考虑去 [天池](https://tianchi.aliyun.com/home/)、[Datafountain](https://www.datafountain.cn/)、[kaggle](https://www.kaggle.com/) 上打比赛，真的可以学到很多东西。\n\n## 面试过程建议\n\n1. **不要紧张，表达要清楚流利**，要记住，**绝大部分面试官都是很友好和亲切的**，尤其是大公司的面试官真的超级温和，**这里为阿里和地平线机器人面试官点赞**！对于那种不尊重人和看不起人的面试官，我个人觉得没必要去他们公司了，一般这种面试官会出现在小公司，一个面试官连对面试者基本的尊重都做不到，我难以想象这家公司的文化是什么样子。\n2. 要对自己有信心，但是**千万不能撒谎和装逼**，一般面试过程中不会的问题，面试官也会跳过或者安慰你没事的，不用太紧张，碰到 `１、2` 个不会的问题也属正常。\n\n## 计算机视觉岗找实习心得\n\n1. 首先，我的水平真的算是很一般的，真正的大佬都是很轻松的拿到数个满意 `offer`。我自己是本科是双非大学自动化专业，研究生是中等 `211` 大学控制工程专业，本科主要搞嵌入式方向，研究生才转为计算机视觉方向和深度学习方向，这里也给后来者一个建议，**如果不是真心热爱、喜欢你所从事的计算机视觉方向，只是为了钱的话那就真的没必要了**，**CV 岗竞争真的很激烈**，`nlp` 和开发岗好很多，而且现在开发岗工资真的很高啊！\n2. 其次，最开始找实习的时候，我没有刷过数据结构和算法题，导致我华为(程序写出来了，但是输入输出格式没注意)、百度、腾讯笔试统统挂了，那段时间真的超级难过，后悔没有提前刷题。4月20号之后，我开始在 `boss` 直聘上投简历，这里陆陆续续收到 2 家小公司面试通知和 `offer`，也算是给了我些信心。\n3. 最后，在这段时间一边把之前项目细节搞清楚，一边开始复习**栈、队列、链表、二叉树和经典数据结构算法原理**，并在 [leetcode](https://leetcode-cn.com/) 上刷题，因为时间关系题目刷的不多，然后就是把 `Faster RCNN、FPN、Mask RCNN、retinanet、ResNet、VGG、InceptionV3` 等经典网络结构原理和细节部分彻底搞清楚，并去看了部分检测框架源码，然后就是深度学习算法的一些基本原理：链式求导过程、`BP` 反向传播、`SGD` 优化器原理、激活函数公式及原理、常见图像处理算法等，这里涉及一些公式，还就是 `C/C++/Python` 编程基础，和计算机基础等。每经过一次面试，我都会自我总结，这使得我后期对面试也开始有了些自己的经验和心得。\n\n## 计算机视觉面试问题分类总结\n\n对于 `CV` 实习岗，面试涉及到的知识可参考下面的部分：\n1. 目标检测算法原理和网络结构细节:\n    + `two-stage` 算法: `Faster RCNN、FPN、Mask RCNN` 等\n    + `one-stage` 算法：`SSD、yolo、retinanet` 等\n    + 无 `anchor` 算法: `centernet`、`FCOS`等  \n2. `CNN` 的 `SOTA` 网络原理和细节: `ResNet`、`VGG`、`InceptionV3`、`DenseNet` 等.\n3. 深度学习算法公式理解：链式求导过程、`BP` 反向传播、`SGD` 优化器原理、激活函数公式及原理、常见图像处理算法等.\n4. `C/C++/Java/Python` 编程基础\n    + **C++ 构造函数与析构函数意义**、指针和引用区别、`new`，`malloc` 区别、抽象类和接口的区别等\n    + `Python` 浅拷贝和深拷贝区别、装饰器使用、`super()` 用法、高阶函数：**map/reduce/filter/sorted 用法**、`try...except` 使用等\n    + 如何用 `C++/Java/Python` **写多进程和多线程代码**\n5. 计算机基础:计算机网络、操作系统、数据库\n    + `TCP/IP` 算法, `IP` 寻址, `socket` 通信流程\n    + 大端小端存储，如何将小端存储模式转为大端存储模式\n    + `OSI` 七层模型解释\n    + 数据库基本操作，及 `sql` 语句\n    + 多进程与多线程区别\n6. 常用工程工具使用基础\n    + `cmake、git` 语法等和操作\n    + `Linux` 系统开发，常见命令使用和 `shell` 语法\n7. 项目或者 `paper` **细节**，根据项目细节延伸问如何做提升和改变\n\n## 已面试的公司和进展\n\n|投递公司|简历投递渠道|进展|\n|-------|----|------|\n|北京阿里|朋友内推|一面挂|\n|北京格林深瞳|boss 直聘投递|一面挂|\n|北京百度|网申|笔试挂|\n|深圳腾讯|朋友内推|笔试挂|\n|川渝华为|官网投递|笔试挂|\n|北京小鹏汽车|boss直聘投递|offer，婉拒|\n|南京地平线机器人|boss直聘投递|正式offer|\n|深圳平安科技|boss直聘投递|简历过，拒绝面试|\n|深圳pony.ai|boss直聘投递|简历过，拒绝笔试|\n|深圳中科类脑|boss直聘投递|简历过，婉拒面试|\n|杭州新再灵|拉钩网|技术面过，hr面挂|\n|康尚生物医疗|boss直聘投递|offer，婉拒|\n|北京矩视智能|boss直聘投递|口头offer，婉拒|\n|上海拼多多|朋友内推|简历过，没参加笔试|\n|北京合心科技|拉勾网投递|面试官极其不尊重人，放弃|\n\n## 实习面经\n\n[实习面经文章](https://link.zhihu.com/?target=https%3A//github.com/HarleysZhang/cv_note/blob/master/interview_summary/%25E8%25AE%25A1%25E7%25AE%2597%25E6%259C%25BA%25E8%25A7%2586%25E8%25A7%2589%25E5%25B2%2597%25E5%25AE%259E%25E4%25B9%25A0%25E9%259D%25A2%25E7%25BB%258F.md)。\n"
  },
  {
    "path": "interview_summary/3-2019届地平线机器人实习总结.md",
    "content": "## 关于工作内容\n\n来地平线实习差不多 `3` 个月了，在这边完成的工作内容，主要有以下几个方面：\n+ 抽烟检测模型的输出，包括 `arm`、`gpu`、`j2定点化`、`mimic`模型的输出及相关训练集及模型测试分析报告的撰写；\n+ `ARM` 端抽烟检测工程代码的优化，主要是针对数据预处理、数据可视化、模型训练、模型评测、`fp/fn` 定性分析等代码；\n+ 针对不同平台的模型难以统一评测的问题，编写了统一的评测程序，配置相关 `yaml` 文件即可完成不同平台模型的评测并输出 `pr`曲线；\n+ 通过对数据过采样的方式解决了 `阴阳脸` 误报严重的问题；\n+ 还有一些其他工作：比如数据送标、训练集/测试集制作、日常标注答疑等。\n\n关于工作内容上，自己做的抽烟检测问题，是属于多分类问题，在学术界多分类问题不是什么大问题，早已经有可行的解决方案了，但是在实际工业中发现，即使是三分类的这样简单的问题，在实际项目中也会碰到各种各样的问题，这让我认识到 `AI` 要想真正落地，是真的需要一批真心喜欢技术，又踏实的人去把 `AI` 技术落地，这中间也许会碰到一些 `dirty work` 吧，对于实习生的成长也可能是不利的，但是对于业务项目来说，这些工作可能又是必须做的。\n## 关于工作环境\n\n地平线是 `to B` 型公司，因此底层技术和业务项目显得尤为重要，需要每一个地平线研发人员扎实做底层技术和踏实做好业务产品。在这边整体工作环境还不错，虽然会加班，但是公司为每个员工配备了升降桌和电竞椅，以考虑员工的身体健康，每天也有零食来补充能量。身边的同事很友好，让我`感动`的在于有些同事虽然平时很忙，但是有问题找他的时候，他也会很耐心的帮你解决疑惑，感谢周围每一个帮助过我的同事。\n\n## 关于个人成长\n\n有些遗憾，在这边实习个人成长方面没有达到自己的要求，虽然刚来第一个月自己成长很快，但是后期成长有限，`自己的成长跟付出不成正比吧`。这一点就不细说了，希望地平线以后能完善对于实习生的培养规范，不能只是一味的要求干活。值得让我注意的是，我发现光干活真的并不一定能让你得到很多成长，但是学习别人的优秀工程代码和学习新知识，然后再应用到实际项目中，那样不仅自己能得到成长，工作效率也能大大提升。自己后期就是稍微看了一个代码优秀的系统性的项目代码，短时间内就让自己收获很多，可惜后期时间有限，留给自己学习的时间不多，否则自己的成长能更多些。\n\n## 关于技术收获\n\n虽然在公司很多事，但是自己回家后偶尔也会看会书，在公式有时间也会看些基础知识和新知识，总的来说技术收获还是有些：\n+ 开发工具熟悉/了解：`git/shell/tmux/cmake/hadoop命令`\n+ 抽烟工程代码优化及数据处理程序编写，熟练掌握 `Python`\n+ 了解`模型压缩知识`（浅层网络设计/知识蒸馏等）及熟悉模型评测指标代码编写及分析\n+ 熟悉 `mxnet` 框架\n\n## 展望\n\n实习近 `3` 个月的时间匆匆而过，这段时间虽然过得很累，但是实习是研究生必须经历的一个阶段，不管是找工作还是对于自己以后的人生职业规划都是有用的，而且**越早实习越好**！！！在研三接下来的一年时间里：\n+ 我会先刷题，复习`机器学习/深度学习/图像处理/C++/Python/`的基础知识和项目细节，为找工作做准备，这几个月要为找工作而努力\n+ 之后，做一些之前没做的事：复现论文，`faster rcnn/mask rcnn/unet/yolo/ssd` 等论文，一个就可以，尽力发一个还不错的论文\n+ 掌握 `C++`，完整的写一个 `C++` 项目\n+ 继续学习目标检测/语义分割的知识，参加 `Kaggle`比赛，尽力拿一个金牌吧（很难，但是当作目标）"
  },
  {
    "path": "interview_summary/4-计算机视觉岗2020届秋招面经.md",
    "content": "## 字节跳动AI Lab-计算机视觉算法工程师\n\n### 一面\n>（挂）\n\n1. 钢筋数量检测项目深挖。\n2. `Roi Pooling` 和 `Roi Algin`区别？\n3. `F1 Score` 如何计算？\n4. `Siamese` 网络原理，`loss` 计算方式。\n5. 算法题（忘了）\n\n## 地平线机器人-计算机时间算法工程师\n\n### 提前批一面（挂）(1个小时)\n\n1. 两道算法题：\n    + 求二叉树的右视图\n    + 输入一个数，找出含 7 的数字的数的最大个数\n2. 实习项目细节\n3. `Roi pooling` 和 `Roi Align` 怎么做的，`Roi Align`的反向传播公式写下。\n4. 几种优化器说下，说下区别及优缺点。\n5. `SGD` 公式和带动量`SGD`公式写下。\n\n## 元戎启行-计算机视觉算法工程师\n\n### 一面（50分钟）(挂)\n\n1. 自我介绍。\n2. 项目细节，深挖项目（图像增强怎么做的，过采样具体怎么实现，模型融合怎么融合等）。\n3. 实习项目，具体怎么解决了问题。\n4. `Roi pooling` 为什么不如 `Roi Align`？\n5. `Focal loss` 说下。\n6. `git` 如何回退版本：\n    + `git log` 查看历史版本\n    + `git reset -hard [版本id]` 恢复到历史版本\n    + `git push -f -u origin master` 把修改推送到远程服务器\n7. 多进程了解吗：\n    + `Python` 如何实现多进程;\n    + 多进程中如何对同一个变量进行操作;\n    + 进程之间的通信方式。\n8. 一道编程题\n\n## 涂鸦移动-软件开发工程师\n\n### 一面(45分钟)\n\n1. 实习项目介绍。\n2. Python多进程介绍下。\n3. 面向对象讲解下。\n4. 说说有哪些排序算法，讲下你最熟悉的几种，怎么实现的。\n5. 两道编程题\n    + 二维数组矩阵\n    + 求 `top k` 数\n\n### 二面(40分钟)\n\n1. 多进程与多线程区别。\n2. 两数之和（`leetcode2`）\n3. 三数之和（`leetcode15`）\n4. 排序算法:\n    + 讲下常用的排序算法，及各自时间复杂度。\n    + 快排原理过程说下，什么情况下时间复杂度最高。\n5. 其他问题\n\n## 平安智慧城-图像识别算法工程师\n\n> 玄学挂, 问题都答上来，反手就是挂，感觉看学校。\n\n### 一面（15分钟）\n\n1. `Faster RCNN` 结构画下，讲下过程。\n2. `VGG` 和 `ResNet` 结构画下。\n3. `ResNet` 结构解决了什么问题，怎么解决的？\n4. `pooling` 层作用。\n5. `Inception` 结构画下。\n\n## 华为海思-人工智能算法工程师\n\n> （一面体验不好，二面编程题自己状态出问题了，导致用错了方法（时间复杂度 O(1) 就能解决，我没想出来）\n\n### 华为海思一面\n\n1. `Faster RCNN` 为什么能精准定位到检测框的位置？\n2. 知识蒸馏方法的一个问题（这个问题问的太抽象了，我始终没有搞懂面试官想要问我什么）。\n3. 你有没有对现有网络做过改进，怎么改进的（说了对压缩模型的一些设计，但是面试官不满意）。\n4. 手撕代码：**指定位置反转链表**， 并把代码每一行解释清楚\n5. 建议我转通用软件开发，说我不适合做算法，我不同意，导致后面二面心情有点糟糕，状态不在线。\n\n### 华为海思二面\n\n1. `Python` 的一些基础知识。\n2. `Inception` 画下，以其中一个模块为例，从头到尾解释下包括：\n    + 卷积核参数的选择？\n    + 为什么 `Inception` 能为了增加网络对尺度的适应性？\n    + 感受野是什么？\n3. 聊天， 给我挑道简单的代码题、\n4. 手撕代码，代码题不难，面试官人也很好，提醒了有更少的时间复杂度，但是自己状态不好，没想到时间复杂度可以为 `O(1)`。\n\n### 总结\n\n华为今年感觉不关心你的项目了， 很看中手撕代码，只要代码那关没过，就算你基础知识原理和项目做的再出色，估计也过不了， 相对，只要手撕代码过了，前面就算答得很烂，也还是能面试通过。\n\n## 奥比中光-算法优化\n\n### 笔试\n\n1. 选择题：笔试都是考一些算法优化的问题，没有专门了解过的话，真的很多人估计都不会做。\n2. 编程题：代码优化。\n### 一面\n\n1. 实习项目介绍：\n    + 解决了什么问题\n    + 怎么解决的\n    + `inception` 结构原理描述。\n2. 博客写了什么内容\n3. 传统图像处理方法说下\n4. 深度学习（神经网络）的一些基本结构说下\n\n### HR面\n\n1. 自我介绍。\n2. 实习项目介绍：\n    + 用了什么方法\n    + 为什么用`inception`结构\n3. 目标检测了解哪些方法？\n4. 平时喜欢看什么书？\n5. 大学期间有参加什么活动吗？\n6. 手里有其他`offer`没？\n\n### 总结\n\n面试官和`hr`人都很友好，会耐心的听你把话说完，面试体验还可以。\n\n## 58同城-机器学习算法工程师\n\n>（问的很细致，公式的每一个参数都要解释清楚）\n\n### 5一面\n\n1. `KNN` 原理讲下，以一个实际问题为例，讲下 `KNN` 怎么做的\n2. `retinanet` 网络相关：\n    + `retinanet` 结构讲下\n    + `FPN` 网络画下，讲下原理\n    + `Focal loss` 讲下，写下公式，讲下 $\\alpha_{t}$ 和 $\\gamma$ 两个参数作用，为什么 $\\alpha_{t}$ 可以解决正负样本的不平衡， $\\gamma$ 可以解决解决难易样本的不平衡问题。\n3. `soft-nms` 和硬 `nms` 原理和过程各自介绍下，为什么`soft-nms` 能解决漏检问题（我没讲清楚）。\n4. `dropout` 和 `bn` 在训练阶段和预测阶段有什么不同，具体原理和过程说下。\n5. 手撕 `nms` 代码，并讲清楚过程(10分钟)。\n\n### 58同城二面\n\n1. 逻辑回归怎么解决过拟合问题？\n2. `dropout` 原理，训练和测试阶段有什么不同。\n3. `tensorflow` 一些框架的问题，有用 `tensorflow` 写过模型和项目吗（有）\n4. 实习项目介绍，`inception`结构和原理介绍下。\n5. 模型压缩都有哪些方法，说下知识蒸馏怎么做的。\n6. 防止过拟合的方法有哪些，这些方法都是怎么做的？\n\n### 58同城hr面\n\n1. 介绍自己。\n2. 奖学金拿过吗？\n3. 在大学期间的一些活动。\n4. 实习项目介绍下，解决了什么问题，还有什么问题没解决，打算怎么解决。\n5. 手里有其他 `offer` 没和秋招情况。\n\n## 瓜子二手车-机器学习算法工程师\n\n### 一面（30分钟）\n\n1. 概率题，并写出相关代码。\n2. 实习项目介绍， `inception` 网络原理。\n3. `faster rcnn` 网络讲下。\n\n### 二面（70分钟）\n\n> 中途接了个电话。\n\n1. 排好序的有重复数字的数组，从中找一个指定元素，并返回最小的那个索引值（同步 `IDE` 写代码, 要求 `AC` 并且尽可能最优解）。\n2. 回文整数（不能使用`str`）。\n3. 传统图像处理方法：\n    + `opencv` 图像处理基本方法\n    + 边缘检测算子有哪些，说下`canny` 算子具体怎么做的\n    + `HOG` 特征算法过程说下  \n4. `pooling` 层作用？\n5. 实习项目介绍，`arm` 端模型如何部署的。\n\n### 总结\n\n瓜子二手车也没怎么问项目，就是手撕代码和问一些基础知识，秋招好几家公司都是这样了，不是很关心项目，比较看中手撕代码。\n\n## 卡斯柯信号-C++软件开发工程师\n\n### 一面(35分钟)\n\n1. 项目介绍。\n2. 你是如何学习机器学习、深度学习的。\n3. 指针和引用的区别？\n4. `c++` 面向对象的三个特性：封装、继承、多态。\n5. 基类和派生类析构函数有什么区别？\n6. 讲下继承。\n\n### 二面(50分钟)\n\n1. 做4道编程题（38分钟）。\n2. 讲笔试题。\n3. 聊家常（为什么想来上海，有女朋友吗）。\n\n## 工商银行-软件开发中心: 大数据及机器学习算法工程师\n\n### 一面(15分钟)(过)\n\n1. 核心项目介绍(围绕项目提了一些问题)。\n2. 你的本科和研究所成绩，为什么投成都岗？\n3. 你的本科学校是什么类型的，研究所毕业设计打算做什么？\n4. 你的博客主要写了什么内容？\n5. 你的比赛经历介绍下。\n6. 你有什么想问的？\n\n### 面试总结\n\n银行感觉比较看`奖学金`(成绩)和比赛，技术问题问的很少，很玄学的就通过面试了，后期有事回家就没去最后的笔试(提前批先面试再笔试)。\n\n## 经纬恒润-无人驾驶算法工程师\n\n### 一面（20分钟）\n\n1. 自我介绍\n2. 实习项目介绍\n    + 解决了什么问题，怎么解决的。\n    + 类别过采样方法原理。\n    + 解决数据不平衡问题有哪些方法。\n3. 期望薪资和工作地点。\n4. 手里有哪些offer。\n\n### 面试总结\n\n挂了，虽然问题都答上来了，面试估计是为了 `KPI`，且经纬恒润的薪资不高。\n"
  },
  {
    "path": "interview_summary/5-视觉算法岗2021年社招面经.md",
    "content": "- [社招面经](#社招面经)\n- [一，项目](#一项目)\n- [二，深度学习、模型部署](#二深度学习模型部署)\n  - [2.1，目标检测相关](#21目标检测相关)\n  - [2.2，深度学习相关](#22深度学习相关)\n  - [2.3，模型部署相关](#23模型部署相关)\n  - [2.4，编程语言相关](#24编程语言相关)\n- [三，数据结构与算法 coding](#三数据结构与算法-coding)\n\n> 个人背景：硕士毕业1年，面试的岗位大部分是计算机视觉算法工程师，少部分算法优化、部署岗。然后这个面经是去年写的，今天突然看到了，就发出来防止丢失。\n## 社招面经\n总的来说，大部分公司的技术面试都分为这几个部分：项目描述和细节提问、深度学习+目标检测算法、数据结构和算法代码及编程语言相关。下面是我面试当中问到的一些问题。\n\n## 一，项目\n\n主要是描述项目背景、项目实现的功能及使用的方法和流程，面试官会针对他感兴趣的点问一些技术细节，基本上只要能把项目流利的描述出来就问题不大。\n\n## 二，深度学习、模型部署\n\n### 2.1，目标检测相关\n\n1，两阶段检测网络（`Faster RCNN` 系列）和一阶段检测网络（`YOLO` 系列）有什么区别？以及为什么两阶段比一阶段精度高？\n\n- 双阶段网络算法更精细，把任务分成了正负样本分类、`bbox` 初次回归以及类别分类和 `bbox` 二次回归。\n- 而 `YOLO` 算法更简单粗暴，使用 `backbone` 对输入图像提取特征后，将特征图划分成 $S\\times S$ 的网格，物体的中心坐标落在哪个网络内，该网格（`grid`）就负责预测目标的置信度、类别和 `bbox`；`YOLOv2-v5` 通过 $1 \\times 1$ 卷积输出特定通道数的特征图来，特征图有 `N` 个通道，对应的每个 `grid` 都会有 `N` 个值，分别对应置信度、类别和 `bbox` 坐标。\n\n> 个人感觉这种问题不好回答，也没有标准答案，可能会出现你答的点不是面试官想要的。\n\n可参考 [你一定从未看过如此通俗易懂的YOLO系列(从v1到v5)模型解读 (上)](https://zhuanlan.zhihu.com/p/183261974) 和 [一文读懂Faster RCNN](https://zhuanlan.zhihu.com/p/31426458) 文章，理解典型的双阶段检测网络和单阶段检测网络。\n\n2，说说你对 `Focal Loss` 的理解，为什么能解决分类问题中的类别不平衡问题？\n\n作者认为一阶段检测网络的精度不高的原因主要在于：极度不平衡的正负样本比例，从而导致**梯度（`gradient`）被容易样本（`easy example`）的损失主导**。\n\n作者通过 `Focal Loss` 公式让置信度高（即容易样本）的样本的损失衰减的更厉害，从而降低容易样本的 `Loss` 权重，从而让模型在后期尽量去学习那些 `hard` 的样本。\n\n3，如何在模型训练的时候判断是否过拟合，及模型过拟合问题如何解决？\n\n将训练数据划分为训练集和验证集，`80%` 用于训练集，`20%` 用于验证集（训练集和验证集一定不能相交）；训练的时候每隔一定 `Epoch` 比较验证集但指标和训练集是否一致，如果不一致，并且验证集指标变差了，即意味着过拟合。\n\n- `数据增强`, 增加数据多样性;\n- 正则化策略：如 Parameter Norm Penalties (参数范数惩罚), `L1, L2` 正则化;\n- 模型融合, 比如 `Bagging` 和其他集成方法;\n- 添加 `BN`（batch normalization）层或者 `dropout` 层（现在基本不用）;\n- `Early Stopping` (提前终止训练)。\n\n4，如何在模型训练的时候判断是否欠拟合，及模型欠拟合问题如何解决？\n\n`underfitting` 欠拟合的表现就是模型不收敛，即训练过程中验证集的指标比较差，`Loss` 不收敛。欠拟合的原因有很多种，这里以神经网络拟合能力不足问题给出以下参考解决方法：\n\n- 寻找最优的权重初始化方案：如 `He` 正态分布初始化 `he_normal`，深度学习框架都内置了很多权重初始化方法；\n- 使用适当的激活函数：卷积层的输出使用的激活函数一般为 `ReLu`，循环神经网络中的循环层使用的激活函数一般为 `tanh`，或者 `ReLu`；\n- 选择合适的优化器和学习速率：`SGD` 优化器速度慢但是会达到最优.\n\n5，描述以下 `YOLOv3` 算法及 `YOLOv4、YOLOv5` 的改进点，及为什么 `CIoU Loss` 比 `IoU Loss` 效果好？\n\n`YOLOv3` 相比前代主要的改进点如下：\n\n1. `Backbone` 从 `DarkNet19` 升级为 `DarkNet53`。\n2. 添加了类似 `FPN` 的多尺度检测网络，解决小目标检测精度低的问题。\n3. 分类预测使用多标签进行类别分类，不再使用 `softmax` 函数。\n4. 每个 `ground truth` 对象只分配一个边界框。\n\n6，描述下 `RoI Pooling` 过程和作用，以及 `RoI Align` 的改进点。\n\n参考这篇文章 [Understanding Region of Interest — (RoI Align and RoI Warp)](https://towardsdatascience.com/understanding-region-of-interest-part-2-roi-align-and-roi-warp-f795196fc193)\n\n7，`YOLOv3` 的标签编码解码过程，以及正负样本采样策略。\n\n和 `YOLOv2` 一样，`YOLOv3` 依然使用 `K-means` 聚类的方法来挑选 `anchor boxes` 作为边界框预测的先验框。每个边界框都会预测 $4$ 个偏移坐标 $(t_x,t_y,t_w,t_h)$。假设 $(c_x, c_y)$ 为 `grid` 的左上角坐标，$p_w$、$p_h$ 是先验框（`anchors`）的宽度与高度，那么网络预测值和边界框真实位置的关系如下所示：\n> 假设某一层的 `feature map` 的大小为 $13 \\times 13$， 那么 `grid cell` 就有 $13 \\times 13$ 个，则第 $n$ 行第 $n$ 列的 `grid cell` 的坐标 $(x_x, c_y)$ 就是 $(n-1,n)$。\n\n$$\nb_x = \\sigma(t_x) + c_x \\\\\\\\\nb_y = \\sigma(t_y) + c_y \\\\\\\\\nb_w = p_{w}e^{t_w} \\\\\\\\\nb_h = p_{h}e^{t_h} $$\n\n![偏移量计算](../data/images/yolov2/偏移量计算.png)\n![]()\n**正负样本的确定**：\n\n+ 正样本：与 `GT` 的 `IOU` 最大的框。\n+ 负样本：与 `GT` 的 `IOU<0.5` 的框。\n+ 忽略的样本：与 `GT` 的 `IOU>0.5` 但不是最大的框。\n+ 使用 $t_x$ 和 $t_y$ （而不是 $b_x$ 和 $b_y$ ）来计算损失。\n\n8，详细讲解下 `Faster RCNN` 和 `Mask RCNN` 算法过程。\n\n参考以下两篇文章理解 `Faster RCNN` 和 `Mask RCNN` 模型:\n\n- [二阶段目标检测网络-Faster RCNN论文解读](https://zhuanlan.zhihu.com/p/562942597)\n- [二阶段目标检测网络-Mask RCNN网络理解](https://zhuanlan.zhihu.com/p/562958404)\n\n9，最新的目标检测算法有哪些？\n\n`YOLOv4-v5`、`Scaled YOLOv4` 和 `Anchor-free` 的算法：`CenterNet`。\n\n10，手写 `Soft NMS` 和 `Focal Loss`。\n\n### 2.2，深度学习相关\n\n1，BN 的作用及 BN 工作流程，以及训练和推理的区别？\n\n2，普通卷积层、分组卷积、深度可分离卷积的 FLOPs 计算公式。\n\n3，普通卷积层、分组卷积、深度可分离卷积的 MAC 计算公式。\n\n4，详细描述下你知道的轻量级网络：MobileNetV1、ShuffleNetv1-v2。\n\n5，何谓正则化？\n\n通过给模型的代价函数（损失函数）添加被称为正则化项（`regularizer`）的惩罚，这称为将模型（学习函数为 $f(x; θ)$）正则化。正则化是一种思想（策略），**给代价函数添加惩罚**只是其中一种方法。\n\n6，`L2` 正则化（权重衰减）原理，为什么它能防止模型过拟合？系数 $\\lambda $ 如何取值？\n\n`L2` 正则化（权重衰减）是另外一种正则化技术，通过加入的正则项对参数数值进行衰减，得到更小的权值。**当 $\\lambda$ 较大时，会使得一些权重几乎衰减到零，相当于去掉了这一项特征，类似于减少特征维度**。假设待正则的网络参数为 $w$，`L2` 正则化为各个元素平方和的 $1/2$ 次方，其形式为：\n\n$$L2 = \\frac{1}{2}\\lambda ||w||^{2}_{2}$$\n\n实际使用时，一般将正则项加入目标函数，通过整体目标函数的误差反向传播，从而实现正则化影响和指导模型训练的目的。\n\n7，`L1` 正则化原理，系数 $\\lambda $ 如何取值？\n\n`L1` 范数: 为向量 `x` 各个元素绝对值之和。`L1` 正则化可以使权值参数稀疏，方便特征提取。\n\n8，`Pytorch` 的 `conv2d` 函数的参数有哪些？以及模型输出大小计算公式，并解释为什么公式是这样。\n\n9，`Pytorch` 的 `DataLoader` 原理。\n\n10，普通卷积过程描述下。\n\n### 2.3，模型部署相关\n\n1，浮点数在计算机中的表示方式？\n\n2，描述下你知道的模型量化知识。\n\n3，知识蒸馏原理，及温度系数如何取值？\n\n4，通用矩阵乘（`GEMM`）优化算法有哪些？\n\n二维矩阵相乘的 `C++` 代码如下；\n\n```cpp\nvector<vector<int>> matrix_mul(vector<vector<int>> A, vector<vector<int>> B){\n    /*二维矩阵相乘函数，时间复杂度 O(n^3)\n    */\n    // vector<vector<int>> A_T = matrix_transpose(A);\n    assert((*A.begin()).size()==B.size()); //断言，第一个矩阵的列必须等于第二个矩阵的行\n    int new_rows = A.size();\n    int new_cols = (*B.begin()).size();\n    int L = B.size();\n    vector<vector<int>> C(new_rows, vector<int>(new_cols,0));\n\n    for(int i=0; i<new_rows; i++){\n        for(int j=0; j<new_cols;j++){\n            for(int k=0; k<L; k++){\n                C[i][j] += A[i][k]*B[k][j];\n            }\n            // C[i][j] = vector_mul(A[i], get_col(B, j));\n        }\n    }\n    return C;\n}\n```\n\n对这样的矩阵乘的算法优化可分为两类：\n\n- 基于算法分析的方法：根据矩阵乘计算特性，从数学角度优化，典型的算法包括 `Strassen` 算法和 `Coppersmith–Winograd` 算法。\n- 基于软件优化的方法：根据计算机存储系统的层次结构特性，选择性地调整计算顺序，主要有循环拆分向量化、内存重排等。\n\n### 2.4，编程语言相关\n\n1，虚函数原理及作用？\n\n2，`C++` 构造函数和析构函数的初始化顺序。\n\n3，智能指针描述下？\n\n4，`static` 关键字作用？\n\n5，`STL` 库的容器有哪些，讲下你最熟悉的一种及常用函数。\n\n6，`vector` 和 数组的区别？`vector` 扩容在内存中是怎么操作的？\n\n7，引用和指针的区别？\n\n8，C++ 中定义 int a = 2,; int b = 2 和 Python 中定义 a = 2 b=3 有什么区别？\n\n9，`OpenCV` 读取图像返回后的矩阵在内存中是怎么保存的？\n\n10，内存对齐原理描述，为什么需要内存对齐？\n\n11，散列表的实现原理？\n\n12，虚拟地址和物理内存的关系？\n\n\n## 三，数据结构与算法 coding\n\n1，二分查找算法 + 可运行代码。\n\n2，白板写链表反转。\n\n3，包含 min 函数的栈 + 可运行代码（剑指 Offer 30. 包含min函数的栈）\n\n4，[最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) + 时间复杂度\n\n5，TOP k 问题-最小的 K 个数 + 说下你知道哪几种解法，及各自时间复杂度\n\n6，返回转置后的矩阵（逆时针）\n\n7，冒泡排序及优化\n\n8，求数组中比左边元素都大同时比右边元素都小的元素，返回这些元素的索引\n\n9，手写快速排序\n\n10，手写 `softmax` 算子 + 解释代码及衍生问题\n\n12，[无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)\n\n13，N 皇后问题\n\n14，求最大的第 k 个数"
  },
  {
    "path": "interview_summary/README.md",
    "content": "## 一些面经总结\n\n1. [计算机视觉岗 2019 届实习面经.md](./1-计算机视觉岗2019届实习面经.md)\n2. [计算机视觉岗 2019 届暑期实习应聘总结](./2-计算机视觉岗2019届暑期实习应聘总结.md)\n3. [2019 届地平线机器人实习总结](./3-2019届地平线机器人实习总结.md)\n4. [计算机视觉岗 2020 届秋招面经](./4-计算机视觉岗2020届秋招面经.md)\n5. [视觉算法岗 2021 年社招面经](./5-视觉算法岗2021年社招面经.md)"
  }
]